代码初始化
|
@ -0,0 +1,28 @@
|
|||
.DS_Store
|
||||
.history
|
||||
node_modules/
|
||||
dist/
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
**/*.log
|
||||
|
||||
tests/**/coverage/
|
||||
tests/e2e/reports
|
||||
selenium-debug.log
|
||||
|
||||
# Editor directories and files
|
||||
.idea
|
||||
.vscode
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.local
|
||||
|
||||
package-lock.json
|
||||
yarn.lock
|
||||
|
||||
# 编译生成的文件
|
||||
auto-imports.d.ts
|
||||
components.d.ts
|
|
@ -0,0 +1,35 @@
|
|||
# vue-rabbit
|
||||
|
||||
This template should help get you started developing with Vue 3 in Vite.
|
||||
|
||||
## Recommended IDE Setup
|
||||
|
||||
[VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur) + [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin).
|
||||
|
||||
## Customize configuration
|
||||
|
||||
See [Vite Configuration Reference](https://vitejs.dev/config/).
|
||||
|
||||
## Project Setup
|
||||
|
||||
```sh
|
||||
npm install
|
||||
```
|
||||
|
||||
### Compile and Hot-Reload for Development
|
||||
|
||||
```sh
|
||||
npm run dev
|
||||
```
|
||||
|
||||
### Compile and Minify for Production
|
||||
|
||||
```sh
|
||||
npm run build
|
||||
```
|
||||
|
||||
### Lint with [ESLint](https://eslint.org/)
|
||||
|
||||
```sh
|
||||
npm run lint
|
||||
```
|
|
@ -0,0 +1,17 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<link rel="icon" href="/favicon.ico">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>邗江实验学校-校本资源库</title>
|
||||
<link rel="stylesheet" href="//at.alicdn.com/t/font_2143783_iq6z4ey5vu.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": "./",
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"src/*"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
{
|
||||
"name": "vue-rabbit",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs --fix --ignore-path .gitignore"
|
||||
},
|
||||
"dependencies": {
|
||||
"@element-plus/icons-vue": "^2.3.1",
|
||||
"@vueuse/core": "^9.12.0",
|
||||
"axios": "^1.2.6",
|
||||
"dayjs": "^1.11.7",
|
||||
"element-plus": "^2.2.28",
|
||||
"pinia": "^2.0.28",
|
||||
"pinia-plugin-persistedstate": "^3.0.2",
|
||||
"vue": "^3.2.45",
|
||||
"vue-router": "^4.1.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^4.0.0",
|
||||
"eslint": "^8.22.0",
|
||||
"eslint-plugin-vue": "^9.3.0",
|
||||
"sass": "^1.57.1",
|
||||
"unplugin-auto-import": "^0.13.0",
|
||||
"unplugin-vue-components": "^0.23.0",
|
||||
"vite": "^4.0.0"
|
||||
}
|
||||
}
|
After Width: | Height: | Size: 4.2 KiB |
|
@ -0,0 +1,75 @@
|
|||
<script setup>
|
||||
import zhCn from 'element-plus/es/locale/lang/zh-cn'
|
||||
const locale = zhCn
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-config-provider :locale="locale">
|
||||
<!-- 一级路由出口组件 -->
|
||||
<RouterView />
|
||||
</el-config-provider>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
header {
|
||||
line-height: 1.5;
|
||||
max-height: 100vh;
|
||||
}
|
||||
|
||||
.logo {
|
||||
display: block;
|
||||
margin: 0 auto 2rem;
|
||||
}
|
||||
|
||||
nav {
|
||||
width: 100%;
|
||||
font-size: 12px;
|
||||
text-align: center;
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
nav a.router-link-exact-active {
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
nav a.router-link-exact-active:hover {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
nav a {
|
||||
display: inline-block;
|
||||
padding: 0 1rem;
|
||||
border-left: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
nav a:first-of-type {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
header {
|
||||
display: flex;
|
||||
place-items: center;
|
||||
padding-right: calc(var(--section-gap) / 2);
|
||||
}
|
||||
|
||||
.logo {
|
||||
margin: 0 2rem 0 0;
|
||||
}
|
||||
|
||||
header .wrapper {
|
||||
display: flex;
|
||||
place-items: flex-start;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
nav {
|
||||
text-align: left;
|
||||
margin-left: -1rem;
|
||||
font-size: 1rem;
|
||||
|
||||
padding: 1rem 0;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,42 @@
|
|||
// 封装购物车相关接口
|
||||
import request from '@/utils/http'
|
||||
|
||||
// 加入购物车
|
||||
export const insertCartAPI = ({ skuId, count }) => {
|
||||
return request({
|
||||
url: '/member/cart',
|
||||
method: 'POST',
|
||||
data: {
|
||||
skuId,
|
||||
count
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 获取最新的购物车列表
|
||||
export const findNewCartListAPI = () => {
|
||||
return request({
|
||||
url: '/member/cart'
|
||||
})
|
||||
}
|
||||
|
||||
// 删除购物车
|
||||
export const delCartAPI = (ids) => {
|
||||
return request({
|
||||
url: '/member/cart',
|
||||
method: 'DELETE',
|
||||
data: {
|
||||
ids
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 合并购物车
|
||||
|
||||
export const mergeCartAPI = (data) => {
|
||||
return request({
|
||||
url: '/member/cart/merge',
|
||||
method: 'POST',
|
||||
data
|
||||
})
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
import request from '@/utils/http'
|
||||
|
||||
|
||||
export function getCategoryAPI (id) {
|
||||
return request({
|
||||
url: '/category',
|
||||
params: {
|
||||
id
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* @description: 获取二级分类列表数据
|
||||
* @param {*} id 分类id
|
||||
* @return {*}
|
||||
*/
|
||||
|
||||
export const getCategoryFilterAPI = (id) => {
|
||||
return request({
|
||||
url: '/category/sub/filter',
|
||||
params: {
|
||||
id
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* @description: 获取导航数据
|
||||
* @data {
|
||||
categoryId: 1005000 ,
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
sortField: 'publishTime' | 'orderNum' | 'evaluateNum'
|
||||
}
|
||||
* @return {*}
|
||||
*/
|
||||
export const getSubCategoryAPI = (data) => {
|
||||
return request({
|
||||
url: '/category/goods/temporary',
|
||||
method: 'POST',
|
||||
data
|
||||
})
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
import request from '@/utils/http'
|
||||
|
||||
|
||||
// 获取详情接口
|
||||
export const getCheckInfoAPI = () => {
|
||||
return request({
|
||||
url: '/member/order/pre'
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
// 创建订单
|
||||
export const createOrderAPI = (data) => {
|
||||
return request({
|
||||
url: '/member/order',
|
||||
method: 'POST',
|
||||
data
|
||||
})
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
import request from '@/utils/http'
|
||||
|
||||
|
||||
export const getDetail = (id) => {
|
||||
return request({
|
||||
url: '/goods',
|
||||
params: {
|
||||
id
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export const getHotGoodsAPI = ({ id, type, limit = 3 }) => {
|
||||
return request({
|
||||
url: '/goods/hot',
|
||||
params: {
|
||||
id,
|
||||
type,
|
||||
limit
|
||||
}
|
||||
})
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
import httpInstance from '@/utils/http'
|
||||
|
||||
|
||||
// 获取banner
|
||||
|
||||
export function getBannerAPI (params = {}) {
|
||||
// 默认为1 商品为2
|
||||
const { distributionSite = '1' } = params
|
||||
return httpInstance({
|
||||
url: '/home/banner',
|
||||
params: {
|
||||
distributionSite
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* @description: 获取新鲜好物
|
||||
* @param {*}
|
||||
* @return {*}
|
||||
*/
|
||||
export const findNewAPI = () => {
|
||||
return httpInstance({
|
||||
url: '/home/new'
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* @description: 获取人气推荐
|
||||
* @param {*}
|
||||
* @return {*}
|
||||
*/
|
||||
export const getHotAPI = () => {
|
||||
return httpInstance({
|
||||
url: '/home/hot'
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* @description: 获取所有商品模块
|
||||
* @param {*}
|
||||
* @return {*}
|
||||
*/
|
||||
export const getGoodsAPI = () => {
|
||||
return httpInstance({
|
||||
url: '/home/goods'
|
||||
})
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
|
||||
import httpInstance from "@/utils/http"
|
||||
|
||||
export function getCategoryAPI () {
|
||||
return httpInstance({
|
||||
url: '/home/category/head'
|
||||
})
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
import request from '@/utils/http'
|
||||
|
||||
/*
|
||||
params: {
|
||||
orderState:0,
|
||||
page:1,
|
||||
pageSize:2
|
||||
}
|
||||
*/
|
||||
|
||||
|
||||
export const getUserOrder = (params) => {
|
||||
return request({
|
||||
url: '/member/order',
|
||||
method: 'GET',
|
||||
params
|
||||
})
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
import request from '@/utils/http'
|
||||
|
||||
export const getOrderAPI = (id) => {
|
||||
return request({
|
||||
url: `/member/order/${id}`
|
||||
})
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
import httpInstance from "@/utils/http"
|
||||
|
||||
|
||||
export function getCategory () {
|
||||
return httpInstance({
|
||||
url: 'home/category/head'
|
||||
})
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
// 封装所有和用户相关的接口函数
|
||||
import request from '@/utils/http'
|
||||
|
||||
export const loginAPI = ({ account, password }) => {
|
||||
return request({
|
||||
url: '/login',
|
||||
method: 'POST',
|
||||
data: {
|
||||
account,
|
||||
password
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
export const getLikeListAPI = ({ limit = 4 }) => {
|
||||
return request({
|
||||
url: '/goods/relevant',
|
||||
params: {
|
||||
limit
|
||||
}
|
||||
})
|
||||
}
|
|
@ -0,0 +1,74 @@
|
|||
/* color palette from <https://github.com/vuejs/theme> */
|
||||
:root {
|
||||
--vt-c-white: #ffffff;
|
||||
--vt-c-white-soft: #f8f8f8;
|
||||
--vt-c-white-mute: #f2f2f2;
|
||||
|
||||
--vt-c-black: #181818;
|
||||
--vt-c-black-soft: #222222;
|
||||
--vt-c-black-mute: #282828;
|
||||
|
||||
--vt-c-indigo: #2c3e50;
|
||||
|
||||
--vt-c-divider-light-1: rgba(60, 60, 60, 0.29);
|
||||
--vt-c-divider-light-2: rgba(60, 60, 60, 0.12);
|
||||
--vt-c-divider-dark-1: rgba(84, 84, 84, 0.65);
|
||||
--vt-c-divider-dark-2: rgba(84, 84, 84, 0.48);
|
||||
|
||||
--vt-c-text-light-1: var(--vt-c-indigo);
|
||||
--vt-c-text-light-2: rgba(60, 60, 60, 0.66);
|
||||
--vt-c-text-dark-1: var(--vt-c-white);
|
||||
--vt-c-text-dark-2: rgba(235, 235, 235, 0.64);
|
||||
}
|
||||
|
||||
/* semantic color variables for this project */
|
||||
:root {
|
||||
--color-background: var(--vt-c-white);
|
||||
--color-background-soft: var(--vt-c-white-soft);
|
||||
--color-background-mute: var(--vt-c-white-mute);
|
||||
|
||||
--color-border: var(--vt-c-divider-light-2);
|
||||
--color-border-hover: var(--vt-c-divider-light-1);
|
||||
|
||||
--color-heading: var(--vt-c-text-light-1);
|
||||
--color-text: var(--vt-c-text-light-1);
|
||||
|
||||
--section-gap: 160px;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--color-background: var(--vt-c-black);
|
||||
--color-background-soft: var(--vt-c-black-soft);
|
||||
--color-background-mute: var(--vt-c-black-mute);
|
||||
|
||||
--color-border: var(--vt-c-divider-dark-2);
|
||||
--color-border-hover: var(--vt-c-divider-dark-1);
|
||||
|
||||
--color-heading: var(--vt-c-text-dark-1);
|
||||
--color-text: var(--vt-c-text-dark-2);
|
||||
}
|
||||
}
|
||||
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
position: relative;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
body {
|
||||
min-height: 100vh;
|
||||
color: var(--color-text);
|
||||
background: var(--color-background);
|
||||
transition: color 0.5s, background-color 0.5s;
|
||||
line-height: 1.6;
|
||||
font-family: Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu,
|
||||
Cantarell, 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
|
||||
font-size: 15px;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
After Width: | Height: | Size: 3.9 KiB |
After Width: | Height: | Size: 188 KiB |
After Width: | Height: | Size: 1.1 MiB |
After Width: | Height: | Size: 130 KiB |
After Width: | Height: | Size: 1.3 MiB |
After Width: | Height: | Size: 46 KiB |
After Width: | Height: | Size: 7.7 KiB |
After Width: | Height: | Size: 1.0 MiB |
After Width: | Height: | Size: 103 KiB |
After Width: | Height: | Size: 8.2 KiB |
After Width: | Height: | Size: 44 KiB |
After Width: | Height: | Size: 89 KiB |
After Width: | Height: | Size: 134 KiB |
After Width: | Height: | Size: 940 B |
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 261.76 226.69" xmlns:v="https://vecta.io/nano"><path d="M161.096.001l-30.225 52.351L100.647.001H-.005l130.877 226.688L261.749.001z" fill="#41b883"/><path d="M161.096.001l-30.225 52.351L100.647.001H52.346l78.526 136.01L209.398.001z" fill="#34495e"/></svg>
|
After Width: | Height: | Size: 308 B |
|
@ -0,0 +1,35 @@
|
|||
@import './base.css';
|
||||
|
||||
#app {
|
||||
max-width: 1280px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
a,
|
||||
.green {
|
||||
text-decoration: none;
|
||||
color: hsla(160, 100%, 37%, 1);
|
||||
transition: 0.4s;
|
||||
}
|
||||
|
||||
@media (hover: hover) {
|
||||
a:hover {
|
||||
background-color: hsla(160, 100%, 37%, 0.2);
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
body {
|
||||
display: flex;
|
||||
place-items: center;
|
||||
}
|
||||
|
||||
#app {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
padding: 0 2rem;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,142 @@
|
|||
<script setup>
|
||||
import { ref, watch } from 'vue'
|
||||
import { useMouseInElement } from '@vueuse/core'
|
||||
|
||||
// 图片列表
|
||||
const imageList = [
|
||||
"https://yanxuan-item.nosdn.127.net/d917c92e663c5ed0bb577c7ded73e4ec.png",
|
||||
"https://yanxuan-item.nosdn.127.net/e801b9572f0b0c02a52952b01adab967.jpg",
|
||||
"https://yanxuan-item.nosdn.127.net/b52c447ad472d51adbdde1a83f550ac2.jpg",
|
||||
"https://yanxuan-item.nosdn.127.net/f93243224dc37674dfca5874fe089c60.jpg",
|
||||
"https://yanxuan-item.nosdn.127.net/f881cfe7de9a576aaeea6ee0d1d24823.jpg"
|
||||
]
|
||||
|
||||
// 1.小图切换大图显示
|
||||
const activeIndex = ref(0)
|
||||
|
||||
const enterhandler = (i) => {
|
||||
activeIndex.value = i
|
||||
}
|
||||
|
||||
// 2. 获取鼠标相对位置
|
||||
const target = ref(null)
|
||||
const { elementX, elementY, isOutside } = useMouseInElement(target)
|
||||
|
||||
// 3. 控制滑块跟随鼠标移动(监听elementX/Y变化,一旦变化 重新设置left/top)
|
||||
const left = ref(0)
|
||||
const top = ref(0)
|
||||
|
||||
const positionX = ref(0)
|
||||
const positionY = ref(0)
|
||||
watch([elementX, elementY, isOutside], () => {
|
||||
|
||||
// 如果鼠标没有移入到盒子里面 直接不执行后面的逻辑
|
||||
if (isOutside.value) return
|
||||
|
||||
// 有效范围内控制滑块距离
|
||||
// 横向
|
||||
if (elementX.value > 100 && elementX.value < 300) {
|
||||
left.value = elementX.value - 100
|
||||
}
|
||||
// 纵向
|
||||
if (elementY.value > 100 && elementY.value < 300) {
|
||||
top.value = elementY.value - 100
|
||||
}
|
||||
|
||||
// 处理边界
|
||||
if (elementX.value > 300) { left.value = 200 }
|
||||
if (elementX.value < 100) { left.value = 0 }
|
||||
|
||||
if (elementY.value > 300) { top.value = 200 }
|
||||
if (elementY.value < 100) { top.value = 0 }
|
||||
|
||||
// 控制大图的显示
|
||||
positionX.value = -left.value * 2
|
||||
positionY.value = -top.value * 2
|
||||
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
<template>
|
||||
<div class="goods-image">
|
||||
|
||||
<!-- 左侧大图-->
|
||||
<div class="middle" ref="target">
|
||||
<img :src="imageList[activeIndex]" alt="" />
|
||||
<!-- 蒙层小滑块 -->
|
||||
<div class="layer" v-show="!isOutside" :style="{ left: `${left}px`, top: `${top}px` }"></div>
|
||||
</div>
|
||||
<!-- 小图列表 -->
|
||||
<ul class="small">
|
||||
<li v-for="(img, i) in imageList" :key="i" @mouseenter="enterhandler(i)" :class="{ active: i === activeIndex }">
|
||||
<img :src="img" alt="" />
|
||||
</li>
|
||||
</ul>
|
||||
<!-- 放大镜大图 -->
|
||||
<div class="large" :style="[
|
||||
{
|
||||
backgroundImage: `url(${imageList[0]})`,
|
||||
backgroundPositionX: `${positionX}px`,
|
||||
backgroundPositionY: `${positionY}px`,
|
||||
},
|
||||
]" v-show="!isOutside"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.goods-image {
|
||||
width: 480px;
|
||||
height: 400px;
|
||||
position: relative;
|
||||
display: flex;
|
||||
|
||||
.middle {
|
||||
width: 400px;
|
||||
height: 400px;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
.large {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 412px;
|
||||
width: 400px;
|
||||
height: 400px;
|
||||
z-index: 500;
|
||||
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
|
||||
background-repeat: no-repeat;
|
||||
// 背景图:盒子的大小 = 2:1 将来控制背景图的移动来实现放大的效果查看 background-position
|
||||
background-size: 800px 800px;
|
||||
background-color: #f8f8f8;
|
||||
}
|
||||
|
||||
.layer {
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
// 绝对定位 然后跟随咱们鼠标控制left和top属性就可以让滑块移动起来
|
||||
left: 0;
|
||||
top: 0;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.small {
|
||||
width: 80px;
|
||||
|
||||
li {
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
margin-left: 12px;
|
||||
margin-bottom: 15px;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover,
|
||||
&.active {
|
||||
border: 2px solid $xtxColor;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,201 @@
|
|||
<template>
|
||||
<div class="goods-sku">
|
||||
<dl v-for="item in goods.specs" :key="item.id">
|
||||
<dt>{{ item.name }}</dt>
|
||||
<dd>
|
||||
<template v-for="val in item.values" :key="val.name">
|
||||
<img :class="{ selected: val.selected, disabled: val.disabled }" @click="clickSpecs(item, val)"
|
||||
v-if="val.picture" :src="val.picture" />
|
||||
<span :class="{ selected: val.selected, disabled: val.disabled }" @click="clickSpecs(item, val)" v-else>{{
|
||||
val.name
|
||||
}}</span>
|
||||
</template>
|
||||
</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { watchEffect } from 'vue'
|
||||
import getPowerSet from './power-set'
|
||||
const spliter = '★'
|
||||
// 根据skus数据得到路径字典对象
|
||||
const getPathMap = (skus) => {
|
||||
const pathMap = {}
|
||||
if (skus && skus.length > 0) {
|
||||
skus.forEach(sku => {
|
||||
// 1. 过滤出有库存有效的sku
|
||||
if (sku.inventory) {
|
||||
// 2. 得到sku属性值数组
|
||||
const specs = sku.specs.map(spec => spec.valueName)
|
||||
// 3. 得到sku属性值数组的子集
|
||||
const powerSet = getPowerSet(specs)
|
||||
// 4. 设置给路径字典对象
|
||||
powerSet.forEach(set => {
|
||||
const key = set.join(spliter)
|
||||
// 如果没有就先初始化一个空数组
|
||||
if (!pathMap[key]) {
|
||||
pathMap[key] = []
|
||||
}
|
||||
pathMap[key].push(sku.id)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
return pathMap
|
||||
}
|
||||
|
||||
// 初始化禁用状态
|
||||
function initDisabledStatus (specs, pathMap) {
|
||||
if (specs && specs.length > 0) {
|
||||
specs.forEach(spec => {
|
||||
spec.values.forEach(val => {
|
||||
// 设置禁用状态
|
||||
val.disabled = !pathMap[val.name]
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 得到当前选中规格集合
|
||||
const getSelectedArr = (specs) => {
|
||||
const selectedArr = []
|
||||
specs.forEach((spec, index) => {
|
||||
const selectedVal = spec.values.find(val => val.selected)
|
||||
if (selectedVal) {
|
||||
selectedArr[index] = selectedVal.name
|
||||
} else {
|
||||
selectedArr[index] = undefined
|
||||
}
|
||||
})
|
||||
return selectedArr
|
||||
}
|
||||
|
||||
// 更新按钮的禁用状态
|
||||
const updateDisabledStatus = (specs, pathMap) => {
|
||||
// 遍历每一种规格
|
||||
specs.forEach((item, i) => {
|
||||
// 拿到当前选择的项目
|
||||
const selectedArr = getSelectedArr(specs)
|
||||
// 遍历每一个按钮
|
||||
item.values.forEach(val => {
|
||||
if (!val.selected) {
|
||||
selectedArr[i] = val.name
|
||||
// 去掉undefined之后组合成key
|
||||
const key = selectedArr.filter(value => value).join(spliter)
|
||||
val.disabled = !pathMap[key]
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
export default {
|
||||
name: 'XtxGoodSku',
|
||||
props: {
|
||||
// specs:所有的规格信息 skus:所有的sku组合
|
||||
goods: {
|
||||
type: Object,
|
||||
default: () => ({ specs: [], skus: [] })
|
||||
}
|
||||
},
|
||||
emits: ['change'],
|
||||
setup (props, { emit }) {
|
||||
let pathMap = {}
|
||||
watchEffect(() => {
|
||||
// 得到所有字典集合
|
||||
pathMap = getPathMap(props.goods.skus)
|
||||
// 组件初始化的时候更新禁用状态
|
||||
initDisabledStatus(props.goods.specs, pathMap)
|
||||
})
|
||||
|
||||
const clickSpecs = (item, val) => {
|
||||
if (val.disabled) return false
|
||||
// 选中与取消选中逻辑
|
||||
if (val.selected) {
|
||||
val.selected = false
|
||||
} else {
|
||||
item.values.forEach(bv => { bv.selected = false })
|
||||
val.selected = true
|
||||
}
|
||||
// 点击之后再次更新选中状态
|
||||
updateDisabledStatus(props.goods.specs, pathMap)
|
||||
// 把选择的sku信息传出去给父组件
|
||||
// 触发change事件将sku数据传递出去
|
||||
const selectedArr = getSelectedArr(props.goods.specs).filter(value => value)
|
||||
// 如果选中得规格数量和传入得规格总数相等则传出完整信息(都选择了)
|
||||
// 否则传出空对象
|
||||
if (selectedArr.length === props.goods.specs.length) {
|
||||
// 从路径字典中得到skuId
|
||||
const skuId = pathMap[selectedArr.join(spliter)][0]
|
||||
const sku = props.goods.skus.find(sku => sku.id === skuId)
|
||||
// 传递数据给父组件
|
||||
emit('change', {
|
||||
skuId: sku.id,
|
||||
price: sku.price,
|
||||
oldPrice: sku.oldPrice,
|
||||
inventory: sku.inventory,
|
||||
specsText: sku.specs.reduce((p, n) => `${p} ${n.name}:${n.valueName}`, '').trim()
|
||||
})
|
||||
} else {
|
||||
emit('change', {})
|
||||
}
|
||||
}
|
||||
return { clickSpecs }
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@mixin sku-state-mixin {
|
||||
border: 1px solid #e4e4e4;
|
||||
margin-right: 10px;
|
||||
cursor: pointer;
|
||||
|
||||
&.selected {
|
||||
border-color: $xtxColor;
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
opacity: 0.6;
|
||||
border-style: dashed;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
.goods-sku {
|
||||
padding-left: 10px;
|
||||
padding-top: 20px;
|
||||
|
||||
dl {
|
||||
display: flex;
|
||||
padding-bottom: 20px;
|
||||
align-items: center;
|
||||
|
||||
dt {
|
||||
width: 50px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
dd {
|
||||
flex: 1;
|
||||
color: #666;
|
||||
|
||||
>img {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
margin-bottom: 4px;
|
||||
@include sku-state-mixin;
|
||||
}
|
||||
|
||||
>span {
|
||||
display: inline-block;
|
||||
height: 30px;
|
||||
line-height: 28px;
|
||||
padding: 0 20px;
|
||||
margin-bottom: 4px;
|
||||
@include sku-state-mixin;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,29 @@
|
|||
|
||||
export default function bwPowerSet (originalSet) {
|
||||
const subSets = []
|
||||
|
||||
// We will have 2^n possible combinations (where n is a length of original set).
|
||||
// It is because for every element of original set we will decide whether to include
|
||||
// it or not (2 options for each set element).
|
||||
const numberOfCombinations = 2 ** originalSet.length
|
||||
|
||||
// Each number in binary representation in a range from 0 to 2^n does exactly what we need:
|
||||
// it shows by its bits (0 or 1) whether to include related element from the set or not.
|
||||
// For example, for the set {1, 2, 3} the binary number of 0b010 would mean that we need to
|
||||
// include only "2" to the current set.
|
||||
for (let combinationIndex = 0; combinationIndex < numberOfCombinations; combinationIndex += 1) {
|
||||
const subSet = []
|
||||
|
||||
for (let setElementIndex = 0; setElementIndex < originalSet.length; setElementIndex += 1) {
|
||||
// Decide whether we need to include current element into the subset or not.
|
||||
if (combinationIndex & (1 << setElementIndex)) {
|
||||
subSet.push(originalSet[setElementIndex])
|
||||
}
|
||||
}
|
||||
|
||||
// Add current subset to the list of all subsets.
|
||||
subSets.push(subSet)
|
||||
}
|
||||
|
||||
return subSets
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
// 把components中的所组件都进行全局化注册
|
||||
// 通过插件的方式
|
||||
import ImageView from './ImageView/index.vue'
|
||||
import Sku from './XtxSku/index.vue'
|
||||
export const componentPlugin = {
|
||||
install (app) {
|
||||
// app.component('组件名字',组件配置对象)
|
||||
app.component('XtxImageView', ImageView)
|
||||
app.component('XtxSku', Sku)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
// 封装倒计时逻辑函数
|
||||
import { computed, onUnmounted, ref } from 'vue'
|
||||
import dayjs from 'dayjs'
|
||||
export const useCountDown = () => {
|
||||
// 1. 响应式的数据
|
||||
let timer = null
|
||||
const time = ref(0)
|
||||
// 格式化时间 为 xx分xx秒
|
||||
const formatTime = computed(() => dayjs.unix(time.value).format('mm分ss秒'))
|
||||
// 2. 开启倒计时的函数
|
||||
const start = (currentTime) => {
|
||||
// 开始倒计时的逻辑
|
||||
// 核心逻辑的编写:每隔1s就减一
|
||||
time.value = currentTime
|
||||
timer = setInterval(() => {
|
||||
time.value--
|
||||
}, 1000)
|
||||
}
|
||||
// 组件销毁时清除定时器
|
||||
onUnmounted(() => {
|
||||
timer && clearInterval(timer)
|
||||
})
|
||||
return {
|
||||
formatTime,
|
||||
start
|
||||
}
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
// 定义懒加载插件
|
||||
import { useIntersectionObserver } from '@vueuse/core'
|
||||
|
||||
export const lazyPlugin = {
|
||||
install (app) {
|
||||
// 懒加载指令逻辑
|
||||
app.directive('img-lazy', {
|
||||
mounted (el, binding) {
|
||||
// el: 指令绑定的那个元素 img
|
||||
// binding: binding.value 指令等于号后面绑定的表达式的值 图片url
|
||||
// console.log(el, binding.value)
|
||||
const { stop } = useIntersectionObserver(
|
||||
el,
|
||||
([{ isIntersecting }]) => {
|
||||
console.log(isIntersecting)
|
||||
if (isIntersecting) {
|
||||
// 进入视口区域
|
||||
el.src = binding.value
|
||||
stop()
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
import { createApp } from 'vue'
|
||||
import { createPinia } from 'pinia'
|
||||
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
|
||||
// 引入初始化样式文件
|
||||
import '@/styles/common.scss'
|
||||
|
||||
// 引入懒加载指令插件并且注册
|
||||
import { lazyPlugin } from '@/directives'
|
||||
// 引入全局组件插件
|
||||
import { componentPlugin } from '@/components'
|
||||
|
||||
const app = createApp(App)
|
||||
const pinia = createPinia()
|
||||
|
||||
// 注册持久化插件
|
||||
pinia.use(piniaPluginPersistedstate)
|
||||
app.use(pinia)
|
||||
app.use(router)
|
||||
app.use(lazyPlugin)
|
||||
app.use(componentPlugin)
|
||||
app.mount('#app')
|
||||
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,104 @@
|
|||
// createRouter:创建router实例对象
|
||||
// createWebHistory:创建history模式的路由
|
||||
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import Login from '@/views/Login/index.vue'
|
||||
import Layout from '@/views/Layout/index.vue'
|
||||
import Home from '@/views/Home/index.vue'
|
||||
import Category from '@/views/Category/index.vue'
|
||||
import SubCategory from '@/views/SubCategory/index.vue'
|
||||
import Detail from '@/views/Detail/index.vue'
|
||||
import CartList from '@/views/CartList/index.vue'
|
||||
import Checkout from '@/views/Checkout/index.vue'
|
||||
import Pay from '@/views/Pay/index.vue'
|
||||
import PayBack from '@/views/Pay/PayBack.vue'
|
||||
import Member from '@/views/Member/index.vue'
|
||||
import UserInfo from '@/views/Member/components/UserInfo.vue'
|
||||
import UserOrder from '@/views/Member/components/UserOrder.vue'
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(import.meta.env.BASE_URL),
|
||||
// path和component对应关系的位置
|
||||
routes: [
|
||||
{
|
||||
path: '/',
|
||||
component: Layout,
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
component: Home
|
||||
},
|
||||
{
|
||||
path: 'news',
|
||||
component: () => import('@/views/News/index.vue')
|
||||
},
|
||||
{
|
||||
path: 'newDetail/:id',
|
||||
component: () => import('@/views/NewsDetail/index.vue')
|
||||
},
|
||||
{
|
||||
path: 'textBook',
|
||||
component: () => import('@/views/TextBook/index.vue')
|
||||
},
|
||||
{
|
||||
path: 'subject',
|
||||
component: () => import('@/views/Subject/index.vue')
|
||||
},
|
||||
{
|
||||
path: 'category/:id',
|
||||
component: Category
|
||||
},
|
||||
{
|
||||
path: 'category/sub/:id',
|
||||
component: SubCategory
|
||||
},
|
||||
{
|
||||
path: 'detail/:id',
|
||||
component: Detail
|
||||
},
|
||||
{
|
||||
path: 'cartlist',
|
||||
component: CartList
|
||||
},
|
||||
{
|
||||
path: 'checkout',
|
||||
component: Checkout
|
||||
},
|
||||
{
|
||||
path: 'pay',
|
||||
component: Pay
|
||||
},
|
||||
{
|
||||
path: 'paycallback',
|
||||
component: PayBack
|
||||
},
|
||||
{
|
||||
path: 'member',
|
||||
component: Member,
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
component: UserInfo
|
||||
},
|
||||
{
|
||||
path: 'order',
|
||||
component: UserOrder
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '/login',
|
||||
component: Login
|
||||
}
|
||||
],
|
||||
// 路由滚动行为定制
|
||||
scrollBehavior() {
|
||||
return {
|
||||
top: 0
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
export default router
|
|
@ -0,0 +1,103 @@
|
|||
// 封装购物车模块
|
||||
|
||||
import { defineStore } from 'pinia'
|
||||
import { computed, ref } from 'vue'
|
||||
import { useUserStore } from './userStore'
|
||||
import { insertCartAPI, findNewCartListAPI, delCartAPI } from '@/apis/cart'
|
||||
export const useCartStore = defineStore('cart', () => {
|
||||
const userStore = useUserStore()
|
||||
const isLogin = computed(() => userStore.userInfo.token)
|
||||
// 1. 定义state - cartList
|
||||
const cartList = ref([])
|
||||
// 获取最新购物车列表action
|
||||
const updateNewList = async () => {
|
||||
const res = await findNewCartListAPI()
|
||||
cartList.value = res.result
|
||||
}
|
||||
// 2. 定义action - addCart
|
||||
const addCart = async (goods) => {
|
||||
const { skuId, count } = goods
|
||||
if (isLogin.value) {
|
||||
// 登录之后的加入购车逻辑
|
||||
await insertCartAPI({ skuId, count })
|
||||
updateNewList()
|
||||
} else {
|
||||
// 添加购物车操作
|
||||
// 已添加过 - count + 1
|
||||
// 没有添加过 - 直接push
|
||||
// 思路:通过匹配传递过来的商品对象中的skuId能不能在cartList中找到,找到了就是添加过
|
||||
const item = cartList.value.find((item) => goods.skuId === item.skuId)
|
||||
if (item) {
|
||||
// 找到了
|
||||
item.count++
|
||||
} else {
|
||||
// 没找到
|
||||
cartList.value.push(goods)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 删除购物车
|
||||
const delCart = async (skuId) => {
|
||||
if (isLogin.value) {
|
||||
// 调用接口实现接口购物车中的删除功能
|
||||
await delCartAPI([skuId])
|
||||
updateNewList()
|
||||
} else {
|
||||
// 思路:
|
||||
// 1. 找到要删除项的下标值 - splice
|
||||
// 2. 使用数组的过滤方法 - filter
|
||||
const idx = cartList.value.findIndex((item) => skuId === item.skuId)
|
||||
cartList.value.splice(idx, 1)
|
||||
}
|
||||
}
|
||||
|
||||
// 清除购物车
|
||||
const clearCart = () => {
|
||||
cartList.value = []
|
||||
}
|
||||
|
||||
// 单选功能
|
||||
const singleCheck = (skuId, selected) => {
|
||||
// 通过skuId找到要修改的那一项 然后把它的selected修改为传过来的selected
|
||||
const item = cartList.value.find((item) => item.skuId === skuId)
|
||||
item.selected = selected
|
||||
}
|
||||
|
||||
// 全选功能
|
||||
const allCheck = (selected) => {
|
||||
// 把cartList中的每一项的selected都设置为当前的全选框状态
|
||||
cartList.value.forEach(item => item.selected = selected)
|
||||
}
|
||||
|
||||
// 计算属性
|
||||
// 1. 总的数量 所有项的count之和
|
||||
const allCount = computed(() => cartList.value.reduce((a, c) => a + c.count, 0))
|
||||
// 2. 总价 所有项的count*price之和
|
||||
const allPrice = computed(() => cartList.value.reduce((a, c) => a + c.count * c.price, 0))
|
||||
|
||||
// 3. 已选择数量
|
||||
const selectedCount = computed(() => cartList.value.filter(item => item.selected).reduce((a, c) => a + c.count, 0))
|
||||
// 4. 已选择商品价钱合计
|
||||
const selectedPrice = computed(() => cartList.value.filter(item => item.selected).reduce((a, c) => a + c.count * c.price, 0))
|
||||
|
||||
// 是否全选
|
||||
const isAll = computed(() => cartList.value.every((item) => item.selected))
|
||||
|
||||
return {
|
||||
cartList,
|
||||
allCount,
|
||||
allPrice,
|
||||
isAll,
|
||||
selectedCount,
|
||||
selectedPrice,
|
||||
clearCart,
|
||||
addCart,
|
||||
delCart,
|
||||
singleCheck,
|
||||
allCheck,
|
||||
updateNewList
|
||||
}
|
||||
}, {
|
||||
persist: true,
|
||||
})
|
|
@ -0,0 +1,19 @@
|
|||
import { ref } from 'vue'
|
||||
import { defineStore } from 'pinia'
|
||||
import { getCategoryAPI } from '@/apis/layout'
|
||||
export const useCategoryStore = defineStore('category', () => {
|
||||
// 导航列表的数据管理
|
||||
// state 导航列表数据
|
||||
const categoryList = ref([])
|
||||
|
||||
// action 获取导航数据的方法
|
||||
const getCategory = async () => {
|
||||
const res = await getCategoryAPI()
|
||||
categoryList.value = res.result
|
||||
}
|
||||
|
||||
return {
|
||||
categoryList,
|
||||
getCategory
|
||||
}
|
||||
})
|
|
@ -0,0 +1,41 @@
|
|||
// 管理用户数据相关
|
||||
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
import { loginAPI } from '@/apis/user'
|
||||
import { useCartStore } from './cartStore'
|
||||
import { mergeCartAPI } from '@/apis/cart'
|
||||
export const useUserStore = defineStore('user', () => {
|
||||
const cartStore = useCartStore()
|
||||
// 1. 定义管理用户数据的state
|
||||
const userInfo = ref({})
|
||||
// 2. 定义获取接口数据的action函数
|
||||
const getUserInfo = async ({ account, password }) => {
|
||||
const res = await loginAPI({ account, password })
|
||||
userInfo.value = res.result
|
||||
// 合并购物车的操作
|
||||
await mergeCartAPI(cartStore.cartList.map(item => {
|
||||
return {
|
||||
skuId: item.skuId,
|
||||
selected: item.selected,
|
||||
count: item.count
|
||||
}
|
||||
}))
|
||||
cartStore.updateNewList()
|
||||
}
|
||||
|
||||
// 退出时清除用户信息
|
||||
const clearUserInfo = () => {
|
||||
userInfo.value = {}
|
||||
// 执行清除购物车的action
|
||||
cartStore.clearCart()
|
||||
}
|
||||
// 3. 以对象的格式把state和action return
|
||||
return {
|
||||
userInfo,
|
||||
getUserInfo,
|
||||
clearUserInfo
|
||||
}
|
||||
}, {
|
||||
persist: true,
|
||||
})
|
|
@ -0,0 +1,103 @@
|
|||
// 重置样式
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html {
|
||||
height: 100%;
|
||||
font-size: 14px;
|
||||
}
|
||||
body {
|
||||
height: 100%;
|
||||
color: #333;
|
||||
min-width: 1240px;
|
||||
font: 1em/1.4 'Microsoft Yahei', 'PingFang SC', 'Avenir', 'Segoe UI',
|
||||
'Hiragino Sans GB', 'STHeiti', 'Microsoft Sans Serif', 'WenQuanYi Micro Hei',
|
||||
sans-serif;
|
||||
}
|
||||
body,
|
||||
ul,
|
||||
h1,
|
||||
h3,
|
||||
h4,
|
||||
p,
|
||||
dl,
|
||||
dd {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: #333;
|
||||
outline: none;
|
||||
}
|
||||
i {
|
||||
font-style: normal;
|
||||
}
|
||||
input[type='text'],
|
||||
input[type='search'],
|
||||
input[type='password'],
|
||||
input[type='checkbox'] {
|
||||
padding: 0;
|
||||
outline: none;
|
||||
border: none;
|
||||
-webkit-appearance: none;
|
||||
&::placeholder {
|
||||
color: #ccc;
|
||||
}
|
||||
}
|
||||
img {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
vertical-align: middle;
|
||||
background: #ebebeb url('@/assets/images/200.png') no-repeat center / contain;
|
||||
}
|
||||
ul {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
#app {
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.container {
|
||||
width: 1240px;
|
||||
margin: 0 auto;
|
||||
position: relative;
|
||||
}
|
||||
.ellipsis {
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.ellipsis-2 {
|
||||
word-break: break-all;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.fl {
|
||||
float: left;
|
||||
}
|
||||
|
||||
.fr {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.clearfix:after {
|
||||
content: '.';
|
||||
display: block;
|
||||
visibility: hidden;
|
||||
height: 0;
|
||||
line-height: 0;
|
||||
clear: both;
|
||||
}
|
||||
|
||||
// reset element
|
||||
.el-breadcrumb__inner.is-link {
|
||||
font-weight: 400 !important;
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
/* 只需要重写你需要的即可 */
|
||||
@forward 'element-plus/theme-chalk/src/common/var.scss' with (
|
||||
$colors: (
|
||||
'primary': (
|
||||
// 主色
|
||||
'base': #27ba9b,
|
||||
),
|
||||
'success': (
|
||||
// 成功色
|
||||
'base': #1dc779,
|
||||
),
|
||||
'warning': (
|
||||
// 警告色
|
||||
'base': #ffb302,
|
||||
),
|
||||
'danger': (
|
||||
// 危险色
|
||||
'base': #e26237,
|
||||
),
|
||||
'error': (
|
||||
// 错误色
|
||||
'base': #cf4444,
|
||||
),
|
||||
)
|
||||
);
|
|
@ -0,0 +1,5 @@
|
|||
$xtxColor: #27ba9b;
|
||||
$helpColor: #e26237;
|
||||
$sucColor: #1dc779;
|
||||
$warnColor: #ffb302;
|
||||
$priceColor: #cf4444;
|
|
@ -0,0 +1,35 @@
|
|||
// axios基础的封装
|
||||
import axios from 'axios'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { useUserStore } from '@/stores/userStore'
|
||||
const httpInstance = axios.create({
|
||||
baseURL: 'http://pcapi-xiaotuxian-front-devtest.itheima.net',
|
||||
timeout: 5000
|
||||
})
|
||||
|
||||
// 拦截器
|
||||
|
||||
// axios请求拦截器
|
||||
httpInstance.interceptors.request.use(config => {
|
||||
// 1. 从pinia获取token数据
|
||||
const userStore = useUserStore()
|
||||
// 2. 按照后端的要求拼接token数据
|
||||
const token = userStore.userInfo.token
|
||||
if (token) {
|
||||
config.headers.Authorization = `Bearer ${token}`
|
||||
}
|
||||
return config
|
||||
}, e => Promise.reject(e))
|
||||
|
||||
// axios响应式拦截器
|
||||
httpInstance.interceptors.response.use(res => res.data, e => {
|
||||
// 统一错误提示
|
||||
ElMessage({
|
||||
type: 'warning',
|
||||
message: e.response.data.message
|
||||
})
|
||||
return Promise.reject(e)
|
||||
})
|
||||
|
||||
|
||||
export default httpInstance
|
|
@ -0,0 +1,224 @@
|
|||
<script setup>
|
||||
import { useCartStore } from '@/stores/cartStore'
|
||||
const cartStore = useCartStore()
|
||||
|
||||
// 单选回调
|
||||
const singleCheck = (i, selected) => {
|
||||
console.log(i, selected)
|
||||
// store cartList 数组 无法知道要修改谁的选中状态?
|
||||
// 除了selected补充一个用来筛选的参数 - skuId
|
||||
cartStore.singleCheck(i.skuId, selected)
|
||||
}
|
||||
|
||||
|
||||
const allCheck = (selected) => {
|
||||
cartStore.allCheck(selected)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="xtx-cart-page">
|
||||
<div class="container m-top-20">
|
||||
<div class="cart">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th width="120">
|
||||
<el-checkbox :model-value="cartStore.isAll" @change="allCheck" />
|
||||
</th>
|
||||
<th width="400">商品信息</th>
|
||||
<th width="220">单价</th>
|
||||
<th width="180">数量</th>
|
||||
<th width="180">小计</th>
|
||||
<th width="140">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<!-- 商品列表 -->
|
||||
<tbody>
|
||||
<tr v-for="i in cartStore.cartList" :key="i.id">
|
||||
<td>
|
||||
<!-- 单选框 -->
|
||||
<el-checkbox :model-value="i.selected" @change="(selected) => singleCheck(i, selected)" />
|
||||
</td>
|
||||
<td>
|
||||
<div class="goods">
|
||||
<RouterLink to="/"><img :src="i.picture" alt="" /></RouterLink>
|
||||
<div>
|
||||
<p class="name ellipsis">
|
||||
{{ i.name }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="tc">
|
||||
<p>¥{{ i.price }}</p>
|
||||
</td>
|
||||
<td class="tc">
|
||||
<el-input-number v-model="i.count" />
|
||||
</td>
|
||||
<td class="tc">
|
||||
<p class="f16 red">¥{{ (i.price * i.count).toFixed(2) }}</p>
|
||||
</td>
|
||||
<td class="tc">
|
||||
<p>
|
||||
<el-popconfirm title="确认删除吗?" confirm-button-text="确认" cancel-button-text="取消" @confirm="delCart(i)">
|
||||
<template #reference>
|
||||
<a href="javascript:;">删除</a>
|
||||
</template>
|
||||
</el-popconfirm>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="cartStore.cartList.length === 0">
|
||||
<td colspan="6">
|
||||
<div class="cart-none">
|
||||
<el-empty description="购物车列表为空">
|
||||
<el-button type="primary">随便逛逛</el-button>
|
||||
</el-empty>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
|
||||
</table>
|
||||
</div>
|
||||
<!-- 操作栏 -->
|
||||
<div class="action">
|
||||
<div class="batch">
|
||||
共 {{ cartStore.allCount }} 件商品,已选择 {{ cartStore.selectedCount }} 件,商品合计:
|
||||
<span class="red">¥ {{ cartStore.selectedPrice.toFixed(2) }} </span>
|
||||
</div>
|
||||
<div class="total">
|
||||
<el-button size="large" type="primary" @click="$router.push('/checkout')">下单结算</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.xtx-cart-page {
|
||||
margin-top: 20px;
|
||||
|
||||
.cart {
|
||||
background: #fff;
|
||||
color: #666;
|
||||
|
||||
table {
|
||||
border-spacing: 0;
|
||||
border-collapse: collapse;
|
||||
line-height: 24px;
|
||||
|
||||
th,
|
||||
td {
|
||||
padding: 10px;
|
||||
border-bottom: 1px solid #f5f5f5;
|
||||
|
||||
&:first-child {
|
||||
text-align: left;
|
||||
padding-left: 30px;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
|
||||
th {
|
||||
font-size: 16px;
|
||||
font-weight: normal;
|
||||
line-height: 50px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.cart-none {
|
||||
text-align: center;
|
||||
padding: 120px 0;
|
||||
background: #fff;
|
||||
|
||||
p {
|
||||
color: #999;
|
||||
padding: 20px 0;
|
||||
}
|
||||
}
|
||||
|
||||
.tc {
|
||||
text-align: center;
|
||||
|
||||
a {
|
||||
color: $xtxColor;
|
||||
}
|
||||
|
||||
.xtx-numbox {
|
||||
margin: 0 auto;
|
||||
width: 120px;
|
||||
}
|
||||
}
|
||||
|
||||
.red {
|
||||
color: $priceColor;
|
||||
}
|
||||
|
||||
.green {
|
||||
color: $xtxColor;
|
||||
}
|
||||
|
||||
.f16 {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.goods {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
img {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
}
|
||||
|
||||
>div {
|
||||
width: 280px;
|
||||
font-size: 16px;
|
||||
padding-left: 10px;
|
||||
|
||||
.attr {
|
||||
font-size: 14px;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.action {
|
||||
display: flex;
|
||||
background: #fff;
|
||||
margin-top: 20px;
|
||||
height: 80px;
|
||||
align-items: center;
|
||||
font-size: 16px;
|
||||
justify-content: space-between;
|
||||
padding: 0 30px;
|
||||
|
||||
.xtx-checkbox {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.batch {
|
||||
a {
|
||||
margin-left: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.red {
|
||||
font-size: 18px;
|
||||
margin-right: 20px;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
.tit {
|
||||
color: #666;
|
||||
font-size: 16px;
|
||||
font-weight: normal;
|
||||
line-height: 50px;
|
||||
}
|
||||
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,21 @@
|
|||
// 封装banner轮播图相关的业务代码
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { getBannerAPI } from '@/apis/home'
|
||||
|
||||
export function useBanner () {
|
||||
const bannerList = ref([])
|
||||
|
||||
const getBanner = async () => {
|
||||
const res = await getBannerAPI({
|
||||
distributionSite: '2'
|
||||
})
|
||||
console.log(res)
|
||||
bannerList.value = res.result
|
||||
}
|
||||
|
||||
onMounted(() => getBanner())
|
||||
|
||||
return {
|
||||
bannerList
|
||||
}
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
// 封装分类数据业务相关代码
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { getCategoryAPI } from '@/apis/category'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { onBeforeRouteUpdate } from 'vue-router'
|
||||
|
||||
export function useCategory () {
|
||||
// 获取分类数据
|
||||
const categoryData = ref({})
|
||||
const route = useRoute()
|
||||
const getCategory = async (id = route.params.id) => {
|
||||
const res = await getCategoryAPI(id)
|
||||
categoryData.value = res.result
|
||||
}
|
||||
onMounted(() => getCategory())
|
||||
|
||||
// 目标:路由参数变化的时候 可以把分类数据接口重新发送
|
||||
onBeforeRouteUpdate((to) => {
|
||||
// 存在问题:使用最新的路由参数请求最新的分类数据
|
||||
getCategory(to.params.id)
|
||||
})
|
||||
return {
|
||||
categoryData
|
||||
}
|
||||
}
|
|
@ -0,0 +1,144 @@
|
|||
<script setup>
|
||||
|
||||
import GoodsItem from '../Home/components/GoodsItem.vue'
|
||||
import { useBanner } from './composables/useBanner'
|
||||
import { useCategory } from './composables/useCategory'
|
||||
const { bannerList } = useBanner()
|
||||
const { categoryData } = useCategory()
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="top-category">
|
||||
<div class="container m-top-20">
|
||||
<!-- 面包屑 -->
|
||||
<div class="bread-container">
|
||||
<el-breadcrumb separator=">">
|
||||
<el-breadcrumb-item :to="{ path: '/' }">首页</el-breadcrumb-item>
|
||||
<el-breadcrumb-item>{{ categoryData.name }}</el-breadcrumb-item>
|
||||
</el-breadcrumb>
|
||||
</div>
|
||||
<!-- 轮播图 -->
|
||||
<div class="home-banner">
|
||||
<el-carousel height="500px">
|
||||
<el-carousel-item v-for="item in bannerList" :key="item.id">
|
||||
<img :src="item.imgUrl" alt="">
|
||||
</el-carousel-item>
|
||||
</el-carousel>
|
||||
</div>
|
||||
<div class="sub-list">
|
||||
<h3>全部分类</h3>
|
||||
<ul>
|
||||
<li v-for="i in categoryData.children" :key="i.id">
|
||||
<RouterLink :to="`/category/sub/${i.id}`">
|
||||
<img :src="i.picture" />
|
||||
<p>{{ i.name }}</p>
|
||||
</RouterLink>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="ref-goods" v-for="item in categoryData.children" :key="item.id">
|
||||
<div class="head">
|
||||
<h3>- {{ item.name }}-</h3>
|
||||
</div>
|
||||
<div class="body">
|
||||
<GoodsItem v-for="good in item.goods" :goods="good" :key="good.id" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<style scoped lang="scss">
|
||||
.top-category {
|
||||
h3 {
|
||||
font-size: 28px;
|
||||
color: #666;
|
||||
font-weight: normal;
|
||||
text-align: center;
|
||||
line-height: 100px;
|
||||
}
|
||||
|
||||
.sub-list {
|
||||
margin-top: 20px;
|
||||
background-color: #fff;
|
||||
|
||||
ul {
|
||||
display: flex;
|
||||
padding: 0 32px;
|
||||
flex-wrap: wrap;
|
||||
|
||||
li {
|
||||
width: 168px;
|
||||
height: 160px;
|
||||
|
||||
|
||||
a {
|
||||
text-align: center;
|
||||
display: block;
|
||||
font-size: 16px;
|
||||
|
||||
img {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
}
|
||||
|
||||
p {
|
||||
line-height: 40px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: $xtxColor;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ref-goods {
|
||||
background-color: #fff;
|
||||
margin-top: 20px;
|
||||
position: relative;
|
||||
|
||||
.head {
|
||||
.xtx-more {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
}
|
||||
|
||||
.tag {
|
||||
text-align: center;
|
||||
color: #999;
|
||||
font-size: 20px;
|
||||
position: relative;
|
||||
top: -20px;
|
||||
}
|
||||
}
|
||||
|
||||
.body {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
padding: 0 40px 30px;
|
||||
}
|
||||
}
|
||||
|
||||
.bread-container {
|
||||
padding: 25px 0;
|
||||
}
|
||||
}
|
||||
|
||||
.home-banner {
|
||||
width: 1240px;
|
||||
height: 500px;
|
||||
margin: 0 auto;
|
||||
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 500px;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,395 @@
|
|||
<script setup>
|
||||
import { getCheckInfoAPI, createOrderAPI } from '@/apis/checkout'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { useCartStore } from '@/stores/cartStore'
|
||||
const cartStore = useCartStore()
|
||||
const router = useRouter()
|
||||
// 获取结算信息
|
||||
const checkInfo = ref({}) // 订单对象
|
||||
const curAddress = ref({}) // 默认地址
|
||||
const getCheckInfo = async () => {
|
||||
const res = await getCheckInfoAPI()
|
||||
checkInfo.value = res.result
|
||||
// 适配默认地址
|
||||
// 从地址列表中筛选出来 isDefault === 0 那一项
|
||||
const item = checkInfo.value.userAddresses.find(item => item.isDefault === 0)
|
||||
curAddress.value = item
|
||||
}
|
||||
|
||||
onMounted(() => getCheckInfo())
|
||||
|
||||
// 控制弹框打开
|
||||
const showDialog = ref(false)
|
||||
|
||||
|
||||
// 切换地址
|
||||
const activeAddress = ref({})
|
||||
const switchAddress = (item) => {
|
||||
activeAddress.value = item
|
||||
}
|
||||
const confirm = () => {
|
||||
curAddress.value = activeAddress.value
|
||||
showDialog.value = false
|
||||
activeAddress.value = {}
|
||||
}
|
||||
|
||||
// 创建订单
|
||||
const createOrder = async () => {
|
||||
const res = await createOrderAPI({
|
||||
deliveryTimeType: 1,
|
||||
payType: 1,
|
||||
payChannel: 1,
|
||||
buyerMessage: '',
|
||||
goods: checkInfo.value.goods.map(item => {
|
||||
return {
|
||||
skuId: item.skuId,
|
||||
count: item.count
|
||||
}
|
||||
}),
|
||||
addressId: curAddress.value.id
|
||||
})
|
||||
const orderId = res.result.id
|
||||
router.push({
|
||||
path: '/pay',
|
||||
query: {
|
||||
id: orderId
|
||||
}
|
||||
})
|
||||
// 更新购物车
|
||||
cartStore.updateNewList()
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="xtx-pay-checkout-page">
|
||||
<div class="container">
|
||||
<div class="wrapper">
|
||||
<!-- 收货地址 -->
|
||||
<h3 class="box-title">收货地址</h3>
|
||||
<div class="box-body">
|
||||
<div class="address">
|
||||
<div class="text">
|
||||
<div class="none" v-if="!curAddress">您需要先添加收货地址才可提交订单。</div>
|
||||
<ul v-else>
|
||||
<li><span>收<i />货<i />人:</span>{{ curAddress.receiver }}</li>
|
||||
<li><span>联系方式:</span>{{ curAddress.contact }}</li>
|
||||
<li><span>收货地址:</span>{{ curAddress.fullLocation }} {{ curAddress.address }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="action">
|
||||
<el-button size="large" @click="showDialog = true">切换地址</el-button>
|
||||
<el-button size="large">添加地址</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 商品信息 -->
|
||||
<h3 class="box-title">商品信息</h3>
|
||||
<div class="box-body">
|
||||
<table class="goods">
|
||||
<thead>
|
||||
<tr>
|
||||
<th width="520">商品信息</th>
|
||||
<th width="170">单价</th>
|
||||
<th width="170">数量</th>
|
||||
<th width="170">小计</th>
|
||||
<th width="170">实付</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="i in checkInfo.goods" :key="i.id">
|
||||
<td>
|
||||
<a href="javascript:;" class="info">
|
||||
<img :src="i.picture" alt="">
|
||||
<div class="right">
|
||||
<p>{{ i.name }}</p>
|
||||
<p>{{ i.attrsText }}</p>
|
||||
</div>
|
||||
</a>
|
||||
</td>
|
||||
<td>¥{{ i.price }}</td>
|
||||
<td>{{ i.price }}</td>
|
||||
<td>¥{{ i.totalPrice }}</td>
|
||||
<td>¥{{ i.totalPayPrice }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!-- 配送时间 -->
|
||||
<h3 class="box-title">配送时间</h3>
|
||||
<div class="box-body">
|
||||
<a class="my-btn active" href="javascript:;">不限送货时间:周一至周日</a>
|
||||
<a class="my-btn" href="javascript:;">工作日送货:周一至周五</a>
|
||||
<a class="my-btn" href="javascript:;">双休日、假日送货:周六至周日</a>
|
||||
</div>
|
||||
<!-- 支付方式 -->
|
||||
<h3 class="box-title">支付方式</h3>
|
||||
<div class="box-body">
|
||||
<a class="my-btn active" href="javascript:;">在线支付</a>
|
||||
<a class="my-btn" href="javascript:;">货到付款</a>
|
||||
<span style="color:#999">货到付款需付5元手续费</span>
|
||||
</div>
|
||||
<!-- 金额明细 -->
|
||||
<h3 class="box-title">金额明细</h3>
|
||||
<div class="box-body">
|
||||
<div class="total">
|
||||
<dl>
|
||||
<dt>商品件数:</dt>
|
||||
<dd>{{ checkInfo.summary?.goodsCount }}件</dd>
|
||||
</dl>
|
||||
<dl>
|
||||
<dt>商品总价:</dt>
|
||||
<dd>¥{{ checkInfo.summary?.totalPrice.toFixed(2) }}</dd>
|
||||
</dl>
|
||||
<dl>
|
||||
<dt>运<i></i>费:</dt>
|
||||
<dd>¥{{ checkInfo.summary?.postFee.toFixed(2) }}</dd>
|
||||
</dl>
|
||||
<dl>
|
||||
<dt>应付总额:</dt>
|
||||
<dd class="price">{{ checkInfo.summary?.totalPayPrice.toFixed(2) }}</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 提交订单 -->
|
||||
<div class="submit">
|
||||
<el-button @click="createOrder" type="primary" size="large">提交订单</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 切换地址 -->
|
||||
<el-dialog v-model="showDialog" title="切换收货地址" width="30%" center>
|
||||
<div class="addressWrapper">
|
||||
<div class="text item" :class="{ active: activeAddress.id === item.id }" @click="switchAddress(item)"
|
||||
v-for="item in checkInfo.userAddresses" :key="item.id">
|
||||
<ul>
|
||||
<li><span>收<i />货<i />人:</span>{{ item.receiver }} </li>
|
||||
<li><span>联系方式:</span>{{ item.contact }}</li>
|
||||
<li><span>收货地址:</span>{{ item.fullLocation + item.address }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button>取消</el-button>
|
||||
<el-button type="primary" @click="confirm">确定</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
<!-- 添加地址 --></template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.xtx-pay-checkout-page {
|
||||
margin-top: 20px;
|
||||
|
||||
.wrapper {
|
||||
background: #fff;
|
||||
padding: 0 20px;
|
||||
|
||||
.box-title {
|
||||
font-size: 16px;
|
||||
font-weight: normal;
|
||||
padding-left: 10px;
|
||||
line-height: 70px;
|
||||
border-bottom: 1px solid #f5f5f5;
|
||||
}
|
||||
|
||||
.box-body {
|
||||
padding: 20px 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.address {
|
||||
border: 1px solid #f5f5f5;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.text {
|
||||
flex: 1;
|
||||
min-height: 90px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.none {
|
||||
line-height: 90px;
|
||||
color: #999;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
>ul {
|
||||
flex: 1;
|
||||
padding: 20px;
|
||||
|
||||
li {
|
||||
line-height: 30px;
|
||||
|
||||
span {
|
||||
color: #999;
|
||||
margin-right: 5px;
|
||||
|
||||
>i {
|
||||
width: 0.5em;
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
>a {
|
||||
color: $xtxColor;
|
||||
width: 160px;
|
||||
text-align: center;
|
||||
height: 90px;
|
||||
line-height: 90px;
|
||||
border-right: 1px solid #f5f5f5;
|
||||
}
|
||||
}
|
||||
|
||||
.action {
|
||||
width: 420px;
|
||||
text-align: center;
|
||||
|
||||
.btn {
|
||||
width: 140px;
|
||||
height: 46px;
|
||||
line-height: 44px;
|
||||
font-size: 14px;
|
||||
|
||||
&:first-child {
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.goods {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
border-spacing: 0;
|
||||
|
||||
.info {
|
||||
display: flex;
|
||||
text-align: left;
|
||||
|
||||
img {
|
||||
width: 70px;
|
||||
height: 70px;
|
||||
margin-right: 20px;
|
||||
}
|
||||
|
||||
.right {
|
||||
line-height: 24px;
|
||||
|
||||
p {
|
||||
&:last-child {
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tr {
|
||||
th {
|
||||
background: #f5f5f5;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
td,
|
||||
th {
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
border-bottom: 1px solid #f5f5f5;
|
||||
|
||||
&:first-child {
|
||||
border-left: 1px solid #f5f5f5;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
border-right: 1px solid #f5f5f5;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.my-btn {
|
||||
width: 228px;
|
||||
height: 50px;
|
||||
border: 1px solid #e4e4e4;
|
||||
text-align: center;
|
||||
line-height: 48px;
|
||||
margin-right: 25px;
|
||||
color: #666666;
|
||||
display: inline-block;
|
||||
|
||||
&.active,
|
||||
&:hover {
|
||||
border-color: $xtxColor;
|
||||
}
|
||||
}
|
||||
|
||||
.total {
|
||||
dl {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
line-height: 50px;
|
||||
|
||||
dt {
|
||||
i {
|
||||
display: inline-block;
|
||||
width: 2em;
|
||||
}
|
||||
}
|
||||
|
||||
dd {
|
||||
width: 240px;
|
||||
text-align: right;
|
||||
padding-right: 70px;
|
||||
|
||||
&.price {
|
||||
font-size: 20px;
|
||||
color: $priceColor;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.submit {
|
||||
text-align: right;
|
||||
padding: 60px;
|
||||
border-top: 1px solid #f5f5f5;
|
||||
}
|
||||
|
||||
.addressWrapper {
|
||||
max-height: 500px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.text {
|
||||
flex: 1;
|
||||
min-height: 90px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
&.item {
|
||||
border: 1px solid #f5f5f5;
|
||||
margin-bottom: 10px;
|
||||
cursor: pointer;
|
||||
|
||||
&.active,
|
||||
&:hover {
|
||||
border-color: $xtxColor;
|
||||
background: lighten($xtxColor, 50%);
|
||||
}
|
||||
|
||||
>ul {
|
||||
padding: 10px;
|
||||
font-size: 14px;
|
||||
line-height: 30px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,94 @@
|
|||
<script setup>
|
||||
// 以24小时热榜获取数据渲染模版
|
||||
import { getHotGoodsAPI } from '@/apis/detail'
|
||||
import { computed } from 'vue'
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
// 设计props参数 适配不同的title和数据
|
||||
|
||||
const props = defineProps({
|
||||
hotType: {
|
||||
type: Number
|
||||
}
|
||||
})
|
||||
|
||||
// 适配title 1 - 24小时热榜 2-周热榜
|
||||
const TYPEMAP = {
|
||||
1: '24小时热榜',
|
||||
2: '周热榜'
|
||||
}
|
||||
const title = computed(() => TYPEMAP[props.hotType])
|
||||
|
||||
// 1. 封装接口
|
||||
// 2. 调用接口渲染模版
|
||||
const hotList = ref([])
|
||||
const route = useRoute()
|
||||
const getHotList = async () => {
|
||||
const res = await getHotGoodsAPI({
|
||||
id: route.params.id,
|
||||
type: props.hotType
|
||||
})
|
||||
hotList.value = res.result
|
||||
}
|
||||
onMounted(() => getHotList())
|
||||
</script>
|
||||
|
||||
|
||||
<template>
|
||||
<div class="goods-hot">
|
||||
<h3>{{ title }}</h3>
|
||||
<!-- 商品区块 -->
|
||||
<RouterLink to="/" class="goods-item" v-for="item in hotList" :key="item.id">
|
||||
<img :src="item.picture" alt="" />
|
||||
<p class="name ellipsis">{{ item.name }}</p>
|
||||
<p class="desc ellipsis">{{ item.desc }}</p>
|
||||
<p class="price">¥{{ item.price }}</p>
|
||||
</RouterLink>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<style scoped lang="scss">
|
||||
.goods-hot {
|
||||
h3 {
|
||||
height: 70px;
|
||||
background: $helpColor;
|
||||
color: #fff;
|
||||
font-size: 18px;
|
||||
line-height: 70px;
|
||||
padding-left: 25px;
|
||||
margin-bottom: 10px;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.goods-item {
|
||||
display: block;
|
||||
padding: 20px 30px;
|
||||
text-align: center;
|
||||
background: #fff;
|
||||
|
||||
img {
|
||||
width: 160px;
|
||||
height: 160px;
|
||||
}
|
||||
|
||||
p {
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
.name {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.desc {
|
||||
color: #999;
|
||||
height: 29px;
|
||||
}
|
||||
|
||||
.price {
|
||||
color: $priceColor;
|
||||
font-size: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,413 @@
|
|||
<script setup>
|
||||
import DetailHot from './components/DetailHot.vue'
|
||||
import { getDetail } from '@/apis/detail'
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { useCartStore } from '@/stores/cartStore'
|
||||
const cartStore = useCartStore()
|
||||
const goods = ref({})
|
||||
const route = useRoute()
|
||||
const getGoods = async () => {
|
||||
const res = await getDetail(route.params.id)
|
||||
goods.value = res.result
|
||||
}
|
||||
onMounted(() => getGoods())
|
||||
|
||||
// sku规格被操作时
|
||||
let skuObj = {}
|
||||
const skuChange = (sku) => {
|
||||
console.log(sku)
|
||||
skuObj = sku
|
||||
}
|
||||
|
||||
// count
|
||||
const count = ref(1)
|
||||
const countChange = (count) => {
|
||||
console.log(count)
|
||||
}
|
||||
|
||||
// 添加购物车
|
||||
const addCart = () => {
|
||||
if (skuObj.skuId) {
|
||||
console.log(skuObj, cartStore.addCart)
|
||||
// 规则已经选择 触发action
|
||||
cartStore.addCart({
|
||||
id: goods.value.id,
|
||||
name: goods.value.name,
|
||||
picture: goods.value.mainPictures[0],
|
||||
price: goods.value.price,
|
||||
count: count.value,
|
||||
skuId: skuObj.skuId,
|
||||
attrsText: skuObj.specsText,
|
||||
selected: true
|
||||
})
|
||||
} else {
|
||||
// 规格没有选择 提示用户
|
||||
ElMessage.warning('请选择规格')
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="xtx-goods-page">
|
||||
<div class="container" v-if="goods.details">
|
||||
<div class="bread-container">
|
||||
<el-breadcrumb separator=">">
|
||||
<el-breadcrumb-item :to="{ path: '/' }">首页</el-breadcrumb-item>
|
||||
<!--
|
||||
错误原因:goods一开始{} {}.categories -> undefined -> undefined[1]
|
||||
1. 可选链的语法?.
|
||||
2. v-if手动控制渲染时机 保证只有数据存在才渲染
|
||||
-->
|
||||
<el-breadcrumb-item :to="{ path: `/category/${goods.categories[1].id}` }">{{ goods.categories[1].name }}
|
||||
</el-breadcrumb-item>
|
||||
<el-breadcrumb-item :to="{ path: `/category/sub/${goods.categories[0].id}` }">{{
|
||||
goods.categories[0].name
|
||||
}}
|
||||
</el-breadcrumb-item>
|
||||
<el-breadcrumb-item>抓绒保暖,毛毛虫子儿童运动鞋</el-breadcrumb-item>
|
||||
</el-breadcrumb>
|
||||
</div>
|
||||
<!-- 商品信息 -->
|
||||
<div class="info-container">
|
||||
<div>
|
||||
<div class="goods-info">
|
||||
<div class="media">
|
||||
<!-- 图片预览区 -->
|
||||
<XtxImageView :image-list="goods.mainPictures" />
|
||||
<!-- 统计数量 -->
|
||||
<ul class="goods-sales">
|
||||
<li>
|
||||
<p>销量人气</p>
|
||||
<p> {{ goods.salesCount }}+ </p>
|
||||
<p><i class="iconfont icon-task-filling"></i>销量人气</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>商品评价</p>
|
||||
<p>{{ goods.commentCount }}+</p>
|
||||
<p><i class="iconfont icon-comment-filling"></i>查看评价</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>收藏人气</p>
|
||||
<p>{{ goods.collectCount }}+</p>
|
||||
<p><i class="iconfont icon-favorite-filling"></i>收藏商品</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>品牌信息</p>
|
||||
<p>{{ goods.brand.name }}</p>
|
||||
<p><i class="iconfont icon-dynamic-filling"></i>品牌主页</p>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="spec">
|
||||
<!-- 商品信息区 -->
|
||||
<p class="g-name"> {{ goods.name }} </p>
|
||||
<p class="g-desc">{{ goods.desc }} </p>
|
||||
<p class="g-price">
|
||||
<span>{{ goods.oldPrice }}</span>
|
||||
<span> {{ goods.price }}</span>
|
||||
</p>
|
||||
<div class="g-service">
|
||||
<dl>
|
||||
<dt>促销</dt>
|
||||
<dd>12月好物放送,App领券购买直降120元</dd>
|
||||
</dl>
|
||||
<dl>
|
||||
<dt>服务</dt>
|
||||
<dd>
|
||||
<span>无忧退货</span>
|
||||
<span>快速退款</span>
|
||||
<span>免费包邮</span>
|
||||
<a href="javascript:;">了解详情</a>
|
||||
</dd>
|
||||
</dl>
|
||||
</div>
|
||||
<!-- sku组件 -->
|
||||
<XtxSku :goods="goods" @change="skuChange" />
|
||||
<!-- 数据组件 -->
|
||||
<el-input-number v-model="count" @change="countChange" />
|
||||
<!-- 按钮组件 -->
|
||||
<div>
|
||||
<el-button size="large" class="btn" @click="addCart">
|
||||
加入购物车
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div class="goods-footer">
|
||||
<div class="goods-article">
|
||||
<!-- 商品详情 -->
|
||||
<div class="goods-tabs">
|
||||
<nav>
|
||||
<a>商品详情</a>
|
||||
</nav>
|
||||
<div class="goods-detail">
|
||||
<!-- 属性 -->
|
||||
<ul class="attrs">
|
||||
<li v-for="item in goods.details.properties" :key="item.value">
|
||||
<span class="dt">{{ item.name }}</span>
|
||||
<span class="dd">{{ item.value }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
<!-- 图片 -->
|
||||
<img v-for="img in goods.details.pictures" :src="img" :key="img" alt="">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 24热榜+专题推荐 -->
|
||||
<div class="goods-aside">
|
||||
<!-- 24小时 -->
|
||||
<DetailHot :hot-type="1" />
|
||||
<!-- 周 -->
|
||||
<DetailHot :hot-type="2" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<style scoped lang='scss'>
|
||||
.xtx-goods-page {
|
||||
.goods-info {
|
||||
min-height: 600px;
|
||||
background: #fff;
|
||||
display: flex;
|
||||
|
||||
.media {
|
||||
width: 580px;
|
||||
height: 600px;
|
||||
padding: 30px 50px;
|
||||
}
|
||||
|
||||
.spec {
|
||||
flex: 1;
|
||||
padding: 30px 30px 30px 0;
|
||||
}
|
||||
}
|
||||
|
||||
.goods-footer {
|
||||
display: flex;
|
||||
margin-top: 20px;
|
||||
|
||||
.goods-article {
|
||||
width: 940px;
|
||||
margin-right: 20px;
|
||||
}
|
||||
|
||||
.goods-aside {
|
||||
width: 280px;
|
||||
min-height: 1000px;
|
||||
}
|
||||
}
|
||||
|
||||
.goods-tabs {
|
||||
min-height: 600px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.goods-warn {
|
||||
min-height: 600px;
|
||||
background: #fff;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.number-box {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.label {
|
||||
width: 60px;
|
||||
color: #999;
|
||||
padding-left: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.g-name {
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
.g-desc {
|
||||
color: #999;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.g-price {
|
||||
margin-top: 10px;
|
||||
|
||||
span {
|
||||
&::before {
|
||||
content: "¥";
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
color: $priceColor;
|
||||
margin-right: 10px;
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
color: #999;
|
||||
text-decoration: line-through;
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.g-service {
|
||||
background: #f5f5f5;
|
||||
width: 500px;
|
||||
padding: 20px 10px 0 10px;
|
||||
margin-top: 10px;
|
||||
|
||||
dl {
|
||||
padding-bottom: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
dt {
|
||||
width: 50px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
dd {
|
||||
color: #666;
|
||||
|
||||
&:last-child {
|
||||
span {
|
||||
margin-right: 10px;
|
||||
|
||||
&::before {
|
||||
content: "•";
|
||||
color: $xtxColor;
|
||||
margin-right: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
color: $xtxColor;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.goods-sales {
|
||||
display: flex;
|
||||
width: 400px;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
height: 140px;
|
||||
|
||||
li {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
|
||||
~li::after {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
left: 0;
|
||||
height: 60px;
|
||||
border-left: 1px solid #e4e4e4;
|
||||
content: "";
|
||||
}
|
||||
|
||||
p {
|
||||
&:first-child {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
&:nth-child(2) {
|
||||
color: $priceColor;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
color: #666;
|
||||
margin-top: 10px;
|
||||
|
||||
i {
|
||||
color: $xtxColor;
|
||||
font-size: 14px;
|
||||
margin-right: 2px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: $xtxColor;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.goods-tabs {
|
||||
min-height: 600px;
|
||||
background: #fff;
|
||||
|
||||
nav {
|
||||
height: 70px;
|
||||
line-height: 70px;
|
||||
display: flex;
|
||||
border-bottom: 1px solid #f5f5f5;
|
||||
|
||||
a {
|
||||
padding: 0 40px;
|
||||
font-size: 18px;
|
||||
position: relative;
|
||||
|
||||
>span {
|
||||
color: $priceColor;
|
||||
font-size: 16px;
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.goods-detail {
|
||||
padding: 40px;
|
||||
|
||||
.attrs {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 30px;
|
||||
|
||||
li {
|
||||
display: flex;
|
||||
margin-bottom: 10px;
|
||||
width: 50%;
|
||||
|
||||
.dt {
|
||||
width: 100px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.dd {
|
||||
flex: 1;
|
||||
color: #666;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
>img {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.btn {
|
||||
margin-top: 20px;
|
||||
|
||||
}
|
||||
|
||||
.bread-container {
|
||||
padding: 25px 0;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,56 @@
|
|||
<script setup>
|
||||
defineProps({
|
||||
goods: {
|
||||
tppe: Object,
|
||||
default: () => { }
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
<template>
|
||||
<RouterLink to="/" class="goods-item">
|
||||
<img v-img-lazy="goods.picture" alt="" />
|
||||
<p class="name ellipsis">{{ goods.name }}</p>
|
||||
<p class="desc ellipsis">{{ goods.desc }}</p>
|
||||
<p class="price">¥{{ goods.price }}</p>
|
||||
</RouterLink>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.goods-item {
|
||||
display: block;
|
||||
width: 220px;
|
||||
padding: 20px 30px;
|
||||
text-align: center;
|
||||
transition: all .5s;
|
||||
|
||||
&:hover {
|
||||
transform: translate3d(0, -3px, 0);
|
||||
box-shadow: 0 3px 8px rgb(0 0 0 / 20%);
|
||||
}
|
||||
|
||||
img {
|
||||
width: 160px;
|
||||
height: 160px;
|
||||
}
|
||||
|
||||
p {
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
.name {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.desc {
|
||||
color: #999;
|
||||
height: 29px;
|
||||
}
|
||||
|
||||
.price {
|
||||
color: $priceColor;
|
||||
font-size: 20px;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,44 @@
|
|||
<script setup>
|
||||
import { getBannerAPI } from '@/apis/home'
|
||||
import { onMounted, ref } from 'vue'
|
||||
|
||||
// const bannerList = ref([])
|
||||
|
||||
// const getBanner = async () => {
|
||||
// const res = await getBannerAPI()
|
||||
// console.log(res)
|
||||
// bannerList.value = res.result
|
||||
// }
|
||||
|
||||
// onMounted(() => getBanner())
|
||||
</script>
|
||||
|
||||
|
||||
|
||||
<template>
|
||||
<div class="home-banner">
|
||||
<el-carousel height="500px" arrow="always">
|
||||
<!-- <el-carousel-item v-for="item in bannerList" :key="item.id">
|
||||
<img :src="item.imgUrl" alt="">
|
||||
</el-carousel-item> -->
|
||||
<el-carousel-item v-for="item in 4">
|
||||
<img src="@/assets/images/carousel-item.png" alt="">
|
||||
</el-carousel-item>
|
||||
</el-carousel>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
|
||||
<style scoped lang='scss'>
|
||||
.home-banner {
|
||||
width: 1240px;
|
||||
height: 500px;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 500px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,57 @@
|
|||
<script setup>
|
||||
// 定义props
|
||||
defineProps({
|
||||
// 主标题
|
||||
title: {
|
||||
type: String
|
||||
},
|
||||
// 副标题
|
||||
subTitle: {
|
||||
type: String
|
||||
}
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
<template>
|
||||
<div class="home-panel">
|
||||
<div class="container">
|
||||
<div class="head">
|
||||
<!-- 主标题和副标题 -->
|
||||
<h3>
|
||||
{{ title }}<small>{{ subTitle }}</small>
|
||||
</h3>
|
||||
</div>
|
||||
<!-- 主体内容区域 -->
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang='scss'>
|
||||
.home-panel {
|
||||
background-color: #fff;
|
||||
|
||||
.head {
|
||||
padding: 40px 0;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
|
||||
h3 {
|
||||
flex: 1;
|
||||
font-size: 32px;
|
||||
font-weight: normal;
|
||||
margin-left: 6px;
|
||||
height: 35px;
|
||||
line-height: 35px;
|
||||
|
||||
small {
|
||||
font-size: 16px;
|
||||
color: #999;
|
||||
margin-left: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,263 @@
|
|||
<template>
|
||||
<div class="container">
|
||||
<HomeBanner />
|
||||
|
||||
<div class="column">
|
||||
<el-row :gutter="10">
|
||||
<el-col :span="12">
|
||||
<el-card style="height: 290px;">
|
||||
<template #header>
|
||||
<div class="title">学校动态</div>
|
||||
|
||||
<div class="card-left-header">
|
||||
<span>校园资讯常看常新</span>
|
||||
<span class="more">
|
||||
<RouterLink to="news">查看更多></RouterLink>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<ul class="card-left-list">
|
||||
<li>
|
||||
<span>当AI融入课堂,会带来怎样的体验?</span>
|
||||
<span>2024-04-12</span>
|
||||
</li>
|
||||
<li>
|
||||
<span>跟胡老师一起读:现代文篇②</span>
|
||||
<span>2024-04-12</span>
|
||||
</li>
|
||||
<li>
|
||||
<span>周末实验室-不伤鱼的三维立体方便测量盆</span>
|
||||
<span>2024-04-12</span>
|
||||
</li>
|
||||
<li>
|
||||
<span>构建智慧课堂 促生深度学习|赋能小学高效课堂教学...</span>
|
||||
<span>2024-04-12</span>
|
||||
</li>
|
||||
<li>
|
||||
<span>在线学习平台应用研讨会顺利举行</span>
|
||||
<span>2024-04-12</span>
|
||||
</li>
|
||||
</ul>
|
||||
</el-card>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="12">
|
||||
<el-card style="height: 290px;">
|
||||
<template #header>
|
||||
<div class="title">同步教材</div>
|
||||
|
||||
<div class="card-right-header">
|
||||
<span>数字化教材云平台</span>
|
||||
<span class="more">查看更多></span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="card-right-content">
|
||||
<el-radio-group v-model="grade" class="grade-radio">
|
||||
<el-radio-button label="小学" value="small" />
|
||||
<el-radio-button label="初中" value="middle" />
|
||||
</el-radio-group>
|
||||
<el-tabs type="border-card">
|
||||
<el-tab-pane label="一年级" class="class-book">
|
||||
<el-tag type="info">语文</el-tag>
|
||||
<el-tag type="info">数学</el-tag>
|
||||
<el-tag type="info">英语</el-tag>
|
||||
<el-tag type="info">音乐</el-tag>
|
||||
<el-tag type="info">美术</el-tag>
|
||||
<el-tag type="info">特殊教育</el-tag>
|
||||
<el-tag type="info">科学</el-tag>
|
||||
<el-tag type="info">道德与法治</el-tag>
|
||||
<el-tag type="info">体育与健康</el-tag>
|
||||
<el-tag type="info">小学劳动与技术</el-tag>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="二年级">二年级</el-tab-pane>
|
||||
<el-tab-pane label="三年级">三年级</el-tab-pane>
|
||||
<el-tab-pane label="四年级">四年级</el-tab-pane>
|
||||
<el-tab-pane label="五年级">五年级</el-tab-pane>
|
||||
<el-tab-pane label="六年级">六年级</el-tab-pane>
|
||||
</el-tabs>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
|
||||
<div class="subject">
|
||||
<el-card>
|
||||
<template #header>
|
||||
<div class="title">专题资源</div>
|
||||
<div class="card-subject-header">
|
||||
<span>助力加强理论学习</span>
|
||||
<span class="more">查看更多>></span>
|
||||
</div>
|
||||
</template>
|
||||
<div class="subject-content">
|
||||
<el-card v-for="item in 8">
|
||||
<img src="@/assets/images/subject.png" />
|
||||
|
||||
<template #footer>
|
||||
<div class="subject-title">【金版学案】2016-2017学年高中语文人教版(必修3、必修4)检测+课件</div>
|
||||
<div class="subject-date">2024-04-12</div>
|
||||
</template>
|
||||
</el-card>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
|
||||
<div class="teacher">
|
||||
<el-card>
|
||||
<template #header>
|
||||
<div class="title">校园名师</div>
|
||||
<div class="card-teacher-header">
|
||||
<span>教育大计老师为本</span>
|
||||
<span class="more">查看更多>></span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<ul class="card-teacher-list">
|
||||
<li class="item" v-for="item in 4">
|
||||
<el-card>
|
||||
<img src="@/assets/images/teacher.png" alt="">
|
||||
<p class="teacher-name">程晨</p>
|
||||
<p class="teacher-class">一年级(1)班 | 语文</p>
|
||||
<p class="teacher-desc">教师介绍教师介绍教师介绍教师介绍教师介绍教师介绍。</p>
|
||||
</el-card>
|
||||
</li>
|
||||
</ul>
|
||||
</el-card>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import HomeBanner from './components/HomeBanner.vue'
|
||||
|
||||
const grade = ref('small')
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.container {
|
||||
.title {
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.more {
|
||||
font-size: 15px;
|
||||
color: #8A8178;
|
||||
}
|
||||
|
||||
.column {
|
||||
height: 290px;
|
||||
margin-top: 20px;
|
||||
|
||||
.card-left-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-top: 5px;
|
||||
color: #8A8178;
|
||||
}
|
||||
|
||||
.card-left-list {
|
||||
li {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 7px;
|
||||
font-size: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
.card-right-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-top: 5px;
|
||||
color: #8A8178;
|
||||
}
|
||||
|
||||
.grade-radio {
|
||||
margin-top: -25px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.card-right-content {
|
||||
padding: 0;
|
||||
|
||||
.class-book {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.subject {
|
||||
margin-top: 20px;
|
||||
|
||||
.card-subject-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.subject-content {
|
||||
padding: 0;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
grid-template-rows: repeat(2, 1fr);
|
||||
gap: 10px;
|
||||
|
||||
img {
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.subject-title {
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.subject-date {
|
||||
margin-top: 5px;
|
||||
color: #DADADA;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.teacher {
|
||||
margin-top: 20px;
|
||||
|
||||
.card-teacher-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.card-teacher-list {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
.item {
|
||||
flex: 0 1 24%;
|
||||
text-align: center;
|
||||
|
||||
.teacher-name {
|
||||
line-height: 45px;
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.teacher-class {
|
||||
line-height: 40px;
|
||||
font-size: 17px;
|
||||
}
|
||||
|
||||
.teacher-desc {
|
||||
font-size: 16px;
|
||||
color: #7F7F7F;
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,76 @@
|
|||
<script setup>
|
||||
import LayoutHeaderUl from './LayoutHeaderUl.vue'
|
||||
// vueUse
|
||||
import { useScroll } from '@vueuse/core'
|
||||
const { y } = useScroll(window)
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="app-header-sticky" :class="{ show: y > 78 }">
|
||||
<div class="container">
|
||||
<RouterLink class="logo" to="/" />
|
||||
<!-- 导航区域 -->
|
||||
|
||||
<LayoutHeaderUl />
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<style scoped lang='scss'>
|
||||
.app-header-sticky {
|
||||
width: 100%;
|
||||
height: 80px;
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
z-index: 999;
|
||||
background-color: #fff;
|
||||
border-bottom: 1px solid #e4e4e4;
|
||||
// 此处为关键样式!!!
|
||||
// 状态一:往上平移自身高度 + 完全透明
|
||||
transform: translateY(-100%);
|
||||
opacity: 0;
|
||||
|
||||
// 状态二:移除平移 + 完全不透明
|
||||
&.show {
|
||||
transition: all 0.3s linear;
|
||||
transform: none;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.logo {
|
||||
width: 300px;
|
||||
height: 80px;
|
||||
background: url("@/assets/images/logo.png") no-repeat right 15px;
|
||||
background-size: 300px auto;
|
||||
}
|
||||
|
||||
.right {
|
||||
width: 220px;
|
||||
display: flex;
|
||||
text-align: center;
|
||||
padding-left: 40px;
|
||||
border-left: 2px solid $xtxColor;
|
||||
|
||||
a {
|
||||
width: 38px;
|
||||
margin-right: 40px;
|
||||
font-size: 16px;
|
||||
line-height: 1;
|
||||
|
||||
&:hover {
|
||||
color: $xtxColor;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,85 @@
|
|||
<template>
|
||||
<footer class="app_footer">
|
||||
<div class="extra">
|
||||
<div class="container">
|
||||
<!-- 版权信息 -->
|
||||
<div class="copyright">
|
||||
<p>
|
||||
<a href="javascript:;">关于我们</a>
|
||||
<a href="javascript:;">帮助中心</a>
|
||||
<a href="javascript:;">售后服务</a>
|
||||
<a href="javascript:;">商务合作</a>
|
||||
<a href="javascript:;">搜索推荐</a>
|
||||
<a href="javascript:;">友情链接</a>
|
||||
</p>
|
||||
<p>CopyRight © 江苏省邗江实验学校</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</template>
|
||||
|
||||
<style scoped lang='scss'>
|
||||
.app_footer {
|
||||
overflow: hidden;
|
||||
background-color: #f5f5f5;
|
||||
padding-top: 20px;
|
||||
|
||||
|
||||
.extra {
|
||||
background-color: #1F6682;
|
||||
}
|
||||
|
||||
.slogan {
|
||||
height: 178px;
|
||||
line-height: 58px;
|
||||
padding: 60px 100px;
|
||||
border-bottom: 1px solid #434343;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
a {
|
||||
height: 58px;
|
||||
line-height: 58px;
|
||||
color: #fff;
|
||||
font-size: 28px;
|
||||
|
||||
i {
|
||||
font-size: 50px;
|
||||
vertical-align: middle;
|
||||
margin-right: 10px;
|
||||
font-weight: 100;
|
||||
}
|
||||
|
||||
span {
|
||||
vertical-align: middle;
|
||||
text-shadow: 0 0 1px #333;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.copyright {
|
||||
height: 170px;
|
||||
padding-top: 40px;
|
||||
text-align: center;
|
||||
color: #fff;
|
||||
font-size: 15px;
|
||||
|
||||
p {
|
||||
line-height: 1;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #fff;
|
||||
line-height: 1;
|
||||
padding: 0 10px;
|
||||
border-right: 1px solid #999;
|
||||
|
||||
&:last-child {
|
||||
border-right: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,46 @@
|
|||
<script setup>
|
||||
import LayoutHeaderUl from './LayoutHeaderUl.vue'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<header class='app-header'>
|
||||
<div class="container">
|
||||
<h1 class="logo">
|
||||
<RouterLink to="/">校本资源平台</RouterLink>
|
||||
</h1>
|
||||
|
||||
<LayoutHeaderUl />
|
||||
</div>
|
||||
<div class="bg-img"></div>
|
||||
</header>
|
||||
</template>
|
||||
|
||||
|
||||
<style scoped lang='scss'>
|
||||
.app-header {
|
||||
position: relative;
|
||||
|
||||
.bg-img {
|
||||
position: absolute;
|
||||
height: 800px;
|
||||
width: 100%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: -999;
|
||||
background: url('@/assets/images/home-bg.png');
|
||||
}
|
||||
|
||||
.logo {
|
||||
width: 300px;
|
||||
|
||||
a {
|
||||
font-size: 32px;
|
||||
color: #fff;
|
||||
display: block;
|
||||
line-height: 150px;
|
||||
height: 150px;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,55 @@
|
|||
<script setup>
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
<template>
|
||||
<ul class="app-header-nav">
|
||||
<li class="home">
|
||||
<RouterLink to="/">首页</RouterLink>
|
||||
</li>
|
||||
<li class="home">
|
||||
<RouterLink to="textBook">同步教材</RouterLink>
|
||||
</li>
|
||||
<li class="home">
|
||||
<RouterLink to="subject">专题资源</RouterLink>
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
|
||||
|
||||
<style lang="scss">
|
||||
.app-header-nav {
|
||||
margin-top: 120px;
|
||||
margin-bottom: 40px;
|
||||
width: 1000px;
|
||||
display: flex;
|
||||
|
||||
li {
|
||||
margin-right: 40px;
|
||||
width: 120px;
|
||||
text-align: center;
|
||||
|
||||
a,
|
||||
.router-link-active {
|
||||
font-size: 16px;
|
||||
line-height: 40px;
|
||||
height: 40px;
|
||||
display: inline-block;
|
||||
color: #fff;
|
||||
background-color: #025B7F;
|
||||
width: 120px;
|
||||
border-radius: 5px;
|
||||
|
||||
&:hover {
|
||||
background-color: #4094AC;
|
||||
}
|
||||
}
|
||||
|
||||
.active,
|
||||
.router-link-exact-active {
|
||||
background-color: #4094AC;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,22 @@
|
|||
<script setup>
|
||||
|
||||
import LayoutHeader from './components/LayoutHeader.vue'
|
||||
import LayoutFooter from './components/LayoutFooter.vue'
|
||||
|
||||
// 触发获取导航列表的action
|
||||
|
||||
import { useCategoryStore } from '@/stores/categoryStore'
|
||||
import { onMounted } from 'vue'
|
||||
|
||||
const categoryStore = useCategoryStore()
|
||||
|
||||
onMounted(() => categoryStore.getCategory())
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<LayoutHeader />
|
||||
<!-- 添加key 破坏复用机制 强制销毁重建 -->
|
||||
<!-- <RouterView :key="$route.fullPath" /> -->
|
||||
<RouterView />
|
||||
<LayoutFooter />
|
||||
</template>
|
|
@ -0,0 +1,358 @@
|
|||
<script setup>
|
||||
|
||||
|
||||
// 表单校验(账号名+密码)
|
||||
|
||||
import { ref } from 'vue'
|
||||
|
||||
import { ElMessage } from 'element-plus'
|
||||
import 'element-plus/theme-chalk/el-message.css'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
import { useUserStore } from '@/stores/userStore'
|
||||
|
||||
const userStore = useUserStore()
|
||||
|
||||
// 1. 准备表单对象
|
||||
const form = ref({
|
||||
account: '18610848230',
|
||||
password: '123456',
|
||||
agree: true
|
||||
})
|
||||
|
||||
// 2. 准备规则对象
|
||||
const rules = {
|
||||
account: [
|
||||
{ required: true, message: '用户名不能为空', trigger: 'blur' }
|
||||
],
|
||||
password: [
|
||||
{ required: true, message: '密码不能为空', trigger: 'blur' },
|
||||
{ min: 6, max: 14, message: '密码长度为6-14个字符', trigger: 'blur' },
|
||||
],
|
||||
agree: [
|
||||
{
|
||||
validator: (rule, value, callback) => {
|
||||
console.log(value)
|
||||
// 自定义校验逻辑
|
||||
// 勾选就通过 不勾选就不通过
|
||||
if (value) {
|
||||
callback()
|
||||
} else {
|
||||
callback(new Error('请勾选协议'))
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
// 3. 获取form实例做统一校验
|
||||
const formRef = ref(null)
|
||||
const router = useRouter()
|
||||
const doLogin = () => {
|
||||
const { account, password } = form.value
|
||||
// 调用实例方法
|
||||
formRef.value.validate(async (valid) => {
|
||||
// valid: 所有表单都通过校验 才为true
|
||||
console.log(valid)
|
||||
// 以valid做为判断条件 如果通过校验才执行登录逻辑
|
||||
if (valid) {
|
||||
// TODO LOGIN
|
||||
await userStore.getUserInfo({ account, password })
|
||||
// 1. 提示用户
|
||||
ElMessage({ type: 'success', message: '登录成功' })
|
||||
// 2. 跳转首页
|
||||
router.replace({ path: '/' })
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 1. 用户名和密码 只需要通过简单的配置(看文档的方式 - 复杂功能通过多个不同组件拆解)
|
||||
// 2. 同意协议 自定义规则 validator:(rule,value,callback)=>{}
|
||||
// 3. 统一校验 通过调用form实例的方法 validate -> true
|
||||
</script>
|
||||
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<header class="login-header">
|
||||
<div class="container m-top-20">
|
||||
<h1 class="logo">
|
||||
<RouterLink to="/">小兔鲜</RouterLink>
|
||||
</h1>
|
||||
<RouterLink class="entry" to="/">
|
||||
进入网站首页
|
||||
<i class="iconfont icon-angle-right"></i>
|
||||
<i class="iconfont icon-angle-right"></i>
|
||||
</RouterLink>
|
||||
</div>
|
||||
</header>
|
||||
<section class="login-section">
|
||||
<div class="wrapper">
|
||||
<nav>
|
||||
<a href="javascript:;">账户登录</a>
|
||||
</nav>
|
||||
<div class="account-box">
|
||||
<div class="form">
|
||||
<el-form ref="formRef" :model="form" :rules="rules" label-position="right" label-width="60px" status-icon>
|
||||
<el-form-item prop="account" label="账户">
|
||||
<el-input v-model="form.account" />
|
||||
</el-form-item>
|
||||
<el-form-item prop="password" label="密码">
|
||||
<el-input v-model="form.password" />
|
||||
</el-form-item>
|
||||
<el-form-item prop="agree" label-width="22px">
|
||||
<el-checkbox size="large" v-model="form.agree">
|
||||
我已同意隐私条款和服务条款
|
||||
</el-checkbox>
|
||||
</el-form-item>
|
||||
<el-button size="large" class="subBtn" @click="doLogin">点击登录</el-button>
|
||||
</el-form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<footer class="login-footer">
|
||||
<div class="container">
|
||||
<p>
|
||||
<a href="javascript:;">关于我们</a>
|
||||
<a href="javascript:;">帮助中心</a>
|
||||
<a href="javascript:;">售后服务</a>
|
||||
<a href="javascript:;">配送与验收</a>
|
||||
<a href="javascript:;">商务合作</a>
|
||||
<a href="javascript:;">搜索推荐</a>
|
||||
<a href="javascript:;">友情链接</a>
|
||||
</p>
|
||||
<p>CopyRight © 小兔鲜儿</p>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang='scss'>
|
||||
.login-header {
|
||||
background: #fff;
|
||||
border-bottom: 1px solid #e4e4e4;
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.logo {
|
||||
width: 200px;
|
||||
|
||||
a {
|
||||
display: block;
|
||||
height: 132px;
|
||||
width: 100%;
|
||||
text-indent: -9999px;
|
||||
background: url("@/assets/images/logo.png") no-repeat center 18px / contain;
|
||||
}
|
||||
}
|
||||
|
||||
.sub {
|
||||
flex: 1;
|
||||
font-size: 24px;
|
||||
font-weight: normal;
|
||||
margin-bottom: 38px;
|
||||
margin-left: 20px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.entry {
|
||||
width: 120px;
|
||||
margin-bottom: 38px;
|
||||
font-size: 16px;
|
||||
|
||||
i {
|
||||
font-size: 14px;
|
||||
color: $xtxColor;
|
||||
letter-spacing: -5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.login-section {
|
||||
background: url('@/assets/images/login-bg.png') no-repeat center / cover;
|
||||
height: 488px;
|
||||
position: relative;
|
||||
|
||||
.wrapper {
|
||||
width: 380px;
|
||||
background: #fff;
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 54px;
|
||||
transform: translate3d(100px, 0, 0);
|
||||
box-shadow: 0 0 10px rgba(0, 0, 0, 0.15);
|
||||
|
||||
nav {
|
||||
font-size: 14px;
|
||||
height: 55px;
|
||||
margin-bottom: 20px;
|
||||
border-bottom: 1px solid #f5f5f5;
|
||||
display: flex;
|
||||
padding: 0 40px;
|
||||
text-align: right;
|
||||
align-items: center;
|
||||
|
||||
a {
|
||||
flex: 1;
|
||||
line-height: 1;
|
||||
display: inline-block;
|
||||
font-size: 18px;
|
||||
position: relative;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.login-footer {
|
||||
padding: 30px 0 50px;
|
||||
background: #fff;
|
||||
|
||||
p {
|
||||
text-align: center;
|
||||
color: #999;
|
||||
padding-top: 20px;
|
||||
|
||||
a {
|
||||
line-height: 1;
|
||||
padding: 0 10px;
|
||||
color: #999;
|
||||
display: inline-block;
|
||||
|
||||
~a {
|
||||
border-left: 1px solid #ccc;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.account-box {
|
||||
.toggle {
|
||||
padding: 15px 40px;
|
||||
text-align: right;
|
||||
|
||||
a {
|
||||
color: $xtxColor;
|
||||
|
||||
i {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.form {
|
||||
padding: 0 20px 20px 20px;
|
||||
|
||||
&-item {
|
||||
margin-bottom: 28px;
|
||||
|
||||
.input {
|
||||
position: relative;
|
||||
height: 36px;
|
||||
|
||||
>i {
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
background: #cfcdcd;
|
||||
color: #fff;
|
||||
position: absolute;
|
||||
left: 1px;
|
||||
top: 1px;
|
||||
text-align: center;
|
||||
line-height: 34px;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
input {
|
||||
padding-left: 44px;
|
||||
border: 1px solid #cfcdcd;
|
||||
height: 36px;
|
||||
line-height: 36px;
|
||||
width: 100%;
|
||||
|
||||
&.error {
|
||||
border-color: $priceColor;
|
||||
}
|
||||
|
||||
&.active,
|
||||
&:focus {
|
||||
border-color: $xtxColor;
|
||||
}
|
||||
}
|
||||
|
||||
.code {
|
||||
position: absolute;
|
||||
right: 1px;
|
||||
top: 1px;
|
||||
text-align: center;
|
||||
line-height: 34px;
|
||||
font-size: 14px;
|
||||
background: #f5f5f5;
|
||||
color: #666;
|
||||
width: 90px;
|
||||
height: 34px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
>.error {
|
||||
position: absolute;
|
||||
font-size: 12px;
|
||||
line-height: 28px;
|
||||
color: $priceColor;
|
||||
|
||||
i {
|
||||
font-size: 14px;
|
||||
margin-right: 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.agree {
|
||||
a {
|
||||
color: #069;
|
||||
}
|
||||
}
|
||||
|
||||
.btn {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
line-height: 40px;
|
||||
background: $xtxColor;
|
||||
|
||||
&.disabled {
|
||||
background: #cfcdcd;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.action {
|
||||
padding: 20px 40px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
.url {
|
||||
a {
|
||||
color: #999;
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.subBtn {
|
||||
background: $xtxColor;
|
||||
width: 100%;
|
||||
color: #fff;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,147 @@
|
|||
<script setup>
|
||||
import { getLikeListAPI } from '@/apis/user'
|
||||
import { useUserStore } from '@/stores/userStore'
|
||||
import { onMounted, ref } from 'vue'
|
||||
import GoodsItem from '@/views/Home/components/GoodsItem.vue'
|
||||
const userStore = useUserStore()
|
||||
|
||||
|
||||
const likeList = ref([])
|
||||
|
||||
const getLikeList = async () => {
|
||||
const res = await getLikeListAPI({ limit: 4 })
|
||||
likeList.value = res.result
|
||||
}
|
||||
|
||||
onMounted(() => getLikeList())
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="home-overview">
|
||||
<!-- 用户信息 -->
|
||||
<div class="user-meta">
|
||||
<div class="avatar">
|
||||
<img :src="userStore.userInfo?.avatar" />
|
||||
</div>
|
||||
<h4>{{ userStore.userInfo?.account }}</h4>
|
||||
</div>
|
||||
<div class="item">
|
||||
<a href="javascript:;">
|
||||
<span class="iconfont icon-hy"></span>
|
||||
<p>会员中心</p>
|
||||
</a>
|
||||
<a href="javascript:;">
|
||||
<span class="iconfont icon-aq"></span>
|
||||
<p>安全设置</p>
|
||||
</a>
|
||||
<a href="javascript:;">
|
||||
<span class="iconfont icon-dw"></span>
|
||||
<p>地址管理</p>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="like-container">
|
||||
<div class="home-panel">
|
||||
<div class="header">
|
||||
<h4 data-v-bcb266e0="">猜你喜欢</h4>
|
||||
</div>
|
||||
<div class="goods-list">
|
||||
<GoodsItem v-for="good in likeList" :key="good.id" :goods="good" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.home-overview {
|
||||
height: 132px;
|
||||
background: url(@/assets/images/center-bg.png) no-repeat center / cover;
|
||||
display: flex;
|
||||
|
||||
.user-meta {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.avatar {
|
||||
width: 85px;
|
||||
height: 85px;
|
||||
border-radius: 50%;
|
||||
overflow: hidden;
|
||||
margin-left: 60px;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
h4 {
|
||||
padding-left: 26px;
|
||||
font-size: 18px;
|
||||
font-weight: normal;
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
.item {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-around;
|
||||
|
||||
&:first-child {
|
||||
border-right: 1px solid #f4f4f4;
|
||||
}
|
||||
|
||||
a {
|
||||
color: white;
|
||||
font-size: 16px;
|
||||
text-align: center;
|
||||
|
||||
.iconfont {
|
||||
font-size: 32px;
|
||||
}
|
||||
|
||||
p {
|
||||
line-height: 32px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.like-container {
|
||||
margin-top: 20px;
|
||||
border-radius: 4px;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.home-panel {
|
||||
background-color: #fff;
|
||||
padding: 0 20px;
|
||||
margin-top: 20px;
|
||||
height: 400px;
|
||||
|
||||
.header {
|
||||
height: 66px;
|
||||
border-bottom: 1px solid #f5f5f5;
|
||||
padding: 18px 0;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: baseline;
|
||||
|
||||
h4 {
|
||||
font-size: 22px;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.goods-list {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,307 @@
|
|||
<script setup>
|
||||
import { getUserOrder } from '@/apis/order'
|
||||
import { onMounted, ref } from 'vue'
|
||||
// tab列表
|
||||
const tabTypes = [
|
||||
{ name: "all", label: "全部订单" },
|
||||
{ name: "unpay", label: "待付款" },
|
||||
{ name: "deliver", label: "待发货" },
|
||||
{ name: "receive", label: "待收货" },
|
||||
{ name: "comment", label: "待评价" },
|
||||
{ name: "complete", label: "已完成" },
|
||||
{ name: "cancel", label: "已取消" }
|
||||
]
|
||||
// 获取订单列表
|
||||
const orderList = ref([])
|
||||
const total = ref(0)
|
||||
const params = ref({
|
||||
orderState: 0,
|
||||
page: 1,
|
||||
pageSize: 2
|
||||
})
|
||||
const getOrderList = async () => {
|
||||
const res = await getUserOrder(params.value)
|
||||
orderList.value = res.result.items
|
||||
total.value = res.result.counts
|
||||
}
|
||||
|
||||
onMounted(() => getOrderList())
|
||||
|
||||
// tab切换
|
||||
const tabChange = (type) => {
|
||||
console.log(type)
|
||||
params.value.orderState = type
|
||||
getOrderList()
|
||||
}
|
||||
|
||||
// 页数切换
|
||||
const pageChange = (page) => {
|
||||
console.log(page)
|
||||
params.value.page = page
|
||||
getOrderList()
|
||||
}
|
||||
|
||||
|
||||
const fomartPayState = (payState) => {
|
||||
const stateMap = {
|
||||
1: '待付款',
|
||||
2: '待发货',
|
||||
3: '待收货',
|
||||
4: '待评价',
|
||||
5: '已完成',
|
||||
6: '已取消'
|
||||
}
|
||||
return stateMap[payState]
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="order-container">
|
||||
<el-tabs @tab-change="tabChange">
|
||||
<!-- tab切换 -->
|
||||
<el-tab-pane v-for="item in tabTypes" :key="item.name" :label="item.label" />
|
||||
|
||||
<div class="main-container">
|
||||
<div class="holder-container" v-if="orderList.length === 0">
|
||||
<el-empty description="暂无订单数据" />
|
||||
</div>
|
||||
<div v-else>
|
||||
<!-- 订单列表 -->
|
||||
<div class="order-item" v-for="order in orderList" :key="order.id">
|
||||
<div class="head">
|
||||
<span>下单时间:{{ order.createTime }}</span>
|
||||
<span>订单编号:{{ order.id }}</span>
|
||||
<!-- 未付款,倒计时时间还有 -->
|
||||
<span class="down-time" v-if="order.orderState === 1">
|
||||
<i class="iconfont icon-down-time"></i>
|
||||
<b>付款截止: {{ order.countdown }}</b>
|
||||
</span>
|
||||
</div>
|
||||
<div class="body">
|
||||
<div class="column goods">
|
||||
<ul>
|
||||
<li v-for="item in order.skus" :key="item.id">
|
||||
<a class="image" href="javascript:;">
|
||||
<img :src="item.image" alt="" />
|
||||
</a>
|
||||
<div class="info">
|
||||
<p class="name ellipsis-2">
|
||||
{{ item.name }}
|
||||
</p>
|
||||
<p class="attr ellipsis">
|
||||
<span>{{ item.attrsText }}</span>
|
||||
</p>
|
||||
</div>
|
||||
<div class="price">¥{{ item.realPay?.toFixed(2) }}</div>
|
||||
<div class="count">x{{ item.quantity }}</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="column state">
|
||||
<p>{{ fomartPayState(order.orderState) }}</p>
|
||||
<p v-if="order.orderState === 3">
|
||||
<a href="javascript:;" class="green">查看物流</a>
|
||||
</p>
|
||||
<p v-if="order.orderState === 4">
|
||||
<a href="javascript:;" class="green">评价商品</a>
|
||||
</p>
|
||||
<p v-if="order.orderState === 5">
|
||||
<a href="javascript:;" class="green">查看评价</a>
|
||||
</p>
|
||||
</div>
|
||||
<div class="column amount">
|
||||
<p class="red">¥{{ order.payMoney?.toFixed(2) }}</p>
|
||||
<p>(含运费:¥{{ order.postFee?.toFixed(2) }})</p>
|
||||
<p>在线支付</p>
|
||||
</div>
|
||||
<div class="column action">
|
||||
<el-button v-if="order.orderState === 1" type="primary" size="small">
|
||||
立即付款
|
||||
</el-button>
|
||||
<el-button v-if="order.orderState === 3" type="primary" size="small">
|
||||
确认收货
|
||||
</el-button>
|
||||
<p><a href="javascript:;">查看详情</a></p>
|
||||
<p v-if="[2, 3, 4, 5].includes(order.orderState)">
|
||||
<a href="javascript:;">再次购买</a>
|
||||
</p>
|
||||
<p v-if="[4, 5].includes(order.orderState)">
|
||||
<a href="javascript:;">申请售后</a>
|
||||
</p>
|
||||
<p v-if="order.orderState === 1"><a href="javascript:;">取消订单</a></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 分页 -->
|
||||
<div class="pagination-container">
|
||||
<el-pagination :total="total" @current-change="pageChange" :page-size="params.pageSize" background
|
||||
layout="prev, pager, next" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</el-tabs>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.order-container {
|
||||
padding: 10px 20px;
|
||||
|
||||
.pagination-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.main-container {
|
||||
min-height: 500px;
|
||||
|
||||
.holder-container {
|
||||
min-height: 500px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.order-item {
|
||||
margin-bottom: 20px;
|
||||
border: 1px solid #f5f5f5;
|
||||
|
||||
.head {
|
||||
height: 50px;
|
||||
line-height: 50px;
|
||||
background: #f5f5f5;
|
||||
padding: 0 20px;
|
||||
overflow: hidden;
|
||||
|
||||
span {
|
||||
margin-right: 20px;
|
||||
|
||||
&.down-time {
|
||||
margin-right: 0;
|
||||
float: right;
|
||||
|
||||
i {
|
||||
vertical-align: middle;
|
||||
margin-right: 3px;
|
||||
}
|
||||
|
||||
b {
|
||||
vertical-align: middle;
|
||||
font-weight: normal;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.del {
|
||||
margin-right: 0;
|
||||
float: right;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
|
||||
.body {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
|
||||
.column {
|
||||
border-left: 1px solid #f5f5f5;
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
|
||||
>p {
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
border-left: none;
|
||||
}
|
||||
|
||||
&.goods {
|
||||
flex: 1;
|
||||
padding: 0;
|
||||
align-self: center;
|
||||
|
||||
ul {
|
||||
li {
|
||||
border-bottom: 1px solid #f5f5f5;
|
||||
padding: 10px;
|
||||
display: flex;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.image {
|
||||
width: 70px;
|
||||
height: 70px;
|
||||
border: 1px solid #f5f5f5;
|
||||
}
|
||||
|
||||
.info {
|
||||
width: 220px;
|
||||
text-align: left;
|
||||
padding: 0 10px;
|
||||
|
||||
p {
|
||||
margin-bottom: 5px;
|
||||
|
||||
&.name {
|
||||
height: 38px;
|
||||
}
|
||||
|
||||
&.attr {
|
||||
color: #999;
|
||||
font-size: 12px;
|
||||
|
||||
span {
|
||||
margin-right: 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.price {
|
||||
width: 100px;
|
||||
}
|
||||
|
||||
.count {
|
||||
width: 80px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.state {
|
||||
width: 120px;
|
||||
|
||||
.green {
|
||||
color: $xtxColor;
|
||||
}
|
||||
}
|
||||
|
||||
&.amount {
|
||||
width: 200px;
|
||||
|
||||
.red {
|
||||
color: $priceColor;
|
||||
}
|
||||
}
|
||||
|
||||
&.action {
|
||||
width: 140px;
|
||||
|
||||
a {
|
||||
display: block;
|
||||
|
||||
&:hover {
|
||||
color: $xtxColor;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,92 @@
|
|||
<script setup>
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="container">
|
||||
<div class="xtx-member-aside">
|
||||
<div class="user-manage">
|
||||
<h4>我的账户</h4>
|
||||
<div class="links">
|
||||
<RouterLink to="/member">个人中心</RouterLink>
|
||||
</div>
|
||||
<h4>交易管理</h4>
|
||||
<div class="links">
|
||||
<RouterLink to="/member/order">我的订单</RouterLink>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="article">
|
||||
<!-- 三级路由的挂载点 -->
|
||||
<RouterView />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.container {
|
||||
display: flex;
|
||||
padding-top: 20px;
|
||||
|
||||
.xtx-member-aside {
|
||||
width: 220px;
|
||||
margin-right: 20px;
|
||||
border-radius: 2px;
|
||||
background-color: #fff;
|
||||
|
||||
.user-manage {
|
||||
background-color: #fff;
|
||||
|
||||
h4 {
|
||||
font-size: 18px;
|
||||
font-weight: 400;
|
||||
padding: 20px 52px 5px;
|
||||
border-top: 1px solid #f6f6f6;
|
||||
}
|
||||
|
||||
.links {
|
||||
padding: 0 52px 10px;
|
||||
}
|
||||
|
||||
a {
|
||||
display: block;
|
||||
line-height: 1;
|
||||
padding: 15px 0;
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
position: relative;
|
||||
|
||||
&:hover {
|
||||
color: $xtxColor;
|
||||
}
|
||||
|
||||
&.active,
|
||||
&.router-link-exact-active {
|
||||
color: $xtxColor;
|
||||
|
||||
&:before {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
&:before {
|
||||
content: '';
|
||||
display: none;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
position: absolute;
|
||||
top: 19px;
|
||||
left: -16px;
|
||||
background-color: $xtxColor;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.article {
|
||||
width: 1000px;
|
||||
background-color: #fff;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,123 @@
|
|||
<template>
|
||||
<div class="container">
|
||||
<div class="bread-container">
|
||||
<el-breadcrumb separator=">">
|
||||
<el-breadcrumb-item :to="{ path: '/' }">首页</el-breadcrumb-item>
|
||||
<el-breadcrumb-item>学校动态</el-breadcrumb-item>
|
||||
</el-breadcrumb>
|
||||
</div>
|
||||
<el-card class="news-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<div>
|
||||
<div class="title">学校动态</div>
|
||||
<div>校园资讯常看常新</div>
|
||||
</div>
|
||||
|
||||
<el-input v-model="keyWords" style="width: 240px;" size="large" placeholder="请输入关键字" :suffix-icon="Search" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-table :data="tableData" class="table" stripe :show-header="false">
|
||||
<el-table-column prop="title" label="title" />
|
||||
<el-table-column prop="date" label="date" width="150" />
|
||||
</el-table>
|
||||
|
||||
<template #footer>
|
||||
<el-pagination class="footer" background layout="prev, pager, next, sizes,jumper" :total="1000" />
|
||||
</template>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { Search } from '@element-plus/icons-vue'
|
||||
|
||||
const keyWords = ref('')
|
||||
const tableData = [
|
||||
{
|
||||
title: '当AI融入课堂,会带来怎样的体验?',
|
||||
date: '2024-04-12',
|
||||
},
|
||||
{
|
||||
title: '当AI融入课堂,会带来怎样的体验?',
|
||||
date: '2024-04-12',
|
||||
},
|
||||
{
|
||||
title: '当AI融入课堂,会带来怎样的体验?',
|
||||
date: '2024-04-12',
|
||||
},
|
||||
{
|
||||
title: '当AI融入课堂,会带来怎样的体验?',
|
||||
date: '2024-04-12',
|
||||
},
|
||||
{
|
||||
title: '当AI融入课堂,会带来怎样的体验?',
|
||||
date: '2024-04-12',
|
||||
},
|
||||
{
|
||||
title: '当AI融入课堂,会带来怎样的体验?',
|
||||
date: '2024-04-12',
|
||||
},
|
||||
{
|
||||
title: '当AI融入课堂,会带来怎样的体验?',
|
||||
date: '2024-04-12',
|
||||
},
|
||||
{
|
||||
title: '当AI融入课堂,会带来怎样的体验?',
|
||||
date: '2024-04-12',
|
||||
},
|
||||
{
|
||||
title: '当AI融入课堂,会带来怎样的体验?',
|
||||
date: '2024-04-12',
|
||||
},
|
||||
{
|
||||
title: '当AI融入课堂,会带来怎样的体验?',
|
||||
date: '2024-04-12',
|
||||
}
|
||||
]
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.container {
|
||||
.bread-container {
|
||||
padding: 25px 0;
|
||||
|
||||
:deep(.el-breadcrumb__inner) {
|
||||
color: #fff;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
:deep(.el-breadcrumb__separator) {
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
.news-card {
|
||||
margin-top: 20px;
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
.title {
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-table__row .cell) {
|
||||
font-size: 17px;
|
||||
padding: 7px;
|
||||
}
|
||||
|
||||
.footer {
|
||||
display: flex;
|
||||
justify-content: end;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,13 @@
|
|||
<template>
|
||||
<div class="container">
|
||||
动态详情
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.container {}
|
||||
</style>
|
|
@ -0,0 +1,85 @@
|
|||
<script setup>
|
||||
import { getOrderAPI } from '@/apis/pay'
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
const route = useRoute()
|
||||
const orderInfo = ref({})
|
||||
|
||||
const getOrderInfo = async () => {
|
||||
const res = await getOrderAPI(route.query.orderId)
|
||||
orderInfo.value = res.result
|
||||
}
|
||||
|
||||
onMounted(() => getOrderInfo())
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
<template>
|
||||
<div class="xtx-pay-page">
|
||||
<div class="container">
|
||||
<!-- 支付结果 -->
|
||||
<div class="pay-result">
|
||||
<!-- 路由参数获取到的是字符串而不是布尔值 -->
|
||||
<span class="iconfont icon-queren2 green" v-if="$route.query.payResult === 'true'"></span>
|
||||
<span class="iconfont icon-shanchu red" v-else></span>
|
||||
<p class="tit">支付{{ $route.query.payResult === 'true' ? '成功' : '失败' }}</p>
|
||||
<p class="tip">我们将尽快为您发货,收货期间请保持手机畅通</p>
|
||||
<p>支付方式:<span>支付宝</span></p>
|
||||
<p>支付金额:<span>¥{{ orderInfo.payMoney?.toFixed(2) }}</span></p>
|
||||
<div class="btn">
|
||||
<el-button type="primary" style="margin-right:20px">查看订单</el-button>
|
||||
<el-button>进入首页</el-button>
|
||||
</div>
|
||||
<p class="alert">
|
||||
<span class="iconfont icon-tip"></span>
|
||||
温馨提示:小兔鲜儿不会以订单异常、系统升级为由要求您点击任何网址链接进行退款操作,保护资产、谨慎操作。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.pay-result {
|
||||
padding: 100px 0;
|
||||
background: #fff;
|
||||
text-align: center;
|
||||
margin-top: 20px;
|
||||
|
||||
>.iconfont {
|
||||
font-size: 100px;
|
||||
}
|
||||
|
||||
.green {
|
||||
color: #1dc779;
|
||||
}
|
||||
|
||||
.red {
|
||||
color: $priceColor;
|
||||
}
|
||||
|
||||
.tit {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.tip {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
p {
|
||||
line-height: 40px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
margin-top: 50px;
|
||||
}
|
||||
|
||||
.alert {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
margin-top: 50px;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,154 @@
|
|||
<script setup>
|
||||
import { getOrderAPI } from '@/apis/pay'
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { useCountDown } from '@/composables/useCountDown'
|
||||
const { formatTime, start } = useCountDown()
|
||||
// 获取订单数据
|
||||
const route = useRoute()
|
||||
const payInfo = ref({})
|
||||
const getPayInfo = async () => {
|
||||
const res = await getOrderAPI(route.query.id)
|
||||
payInfo.value = res.result
|
||||
// 初始化倒计时秒数
|
||||
start(res.result.countdown)
|
||||
}
|
||||
onMounted(() => getPayInfo())
|
||||
|
||||
// 跳转支付
|
||||
// 携带订单id以及回调地址跳转到支付地址(get)
|
||||
// 支付地址
|
||||
const baseURL = 'http://pcapi-xiaotuxian-front-devtest.itheima.net/'
|
||||
const backURL = 'http://127.0.0.1:5173/paycallback'
|
||||
const redirectUrl = encodeURIComponent(backURL)
|
||||
const payUrl = `${baseURL}pay/aliPay?orderId=${route.query.id}&redirect=${redirectUrl}`
|
||||
</script>
|
||||
|
||||
|
||||
<template>
|
||||
<div class="xtx-pay-page">
|
||||
<div class="container">
|
||||
<!-- 付款信息 -->
|
||||
<div class="pay-info">
|
||||
<span class="icon iconfont icon-queren2"></span>
|
||||
<div class="tip">
|
||||
<p>订单提交成功!请尽快完成支付。</p>
|
||||
<p>支付还剩 <span>{{ formatTime }}</span>, 超时后将取消订单</p>
|
||||
</div>
|
||||
<div class="amount">
|
||||
<span>应付总额:</span>
|
||||
<span>¥{{ payInfo.payMoney?.toFixed(2) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 付款方式 -->
|
||||
<div class="pay-type">
|
||||
<p class="head">选择以下支付方式付款</p>
|
||||
<div class="item">
|
||||
<p>支付平台</p>
|
||||
<a class="btn wx" href="javascript:;"></a>
|
||||
<a class="btn alipay" :href="payUrl"></a>
|
||||
</div>
|
||||
<div class="item">
|
||||
<p>支付方式</p>
|
||||
<a class="btn" href="javascript:;">招商银行</a>
|
||||
<a class="btn" href="javascript:;">工商银行</a>
|
||||
<a class="btn" href="javascript:;">建设银行</a>
|
||||
<a class="btn" href="javascript:;">农业银行</a>
|
||||
<a class="btn" href="javascript:;">交通银行</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.xtx-pay-page {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.pay-info {
|
||||
|
||||
background: #fff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 240px;
|
||||
padding: 0 80px;
|
||||
|
||||
.icon {
|
||||
font-size: 80px;
|
||||
color: #1dc779;
|
||||
}
|
||||
|
||||
.tip {
|
||||
padding-left: 10px;
|
||||
flex: 1;
|
||||
|
||||
p {
|
||||
&:first-child {
|
||||
font-size: 20px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
color: #999;
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.amount {
|
||||
span {
|
||||
&:first-child {
|
||||
font-size: 16px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
color: $priceColor;
|
||||
font-size: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.pay-type {
|
||||
margin-top: 20px;
|
||||
background-color: #fff;
|
||||
padding-bottom: 70px;
|
||||
|
||||
p {
|
||||
line-height: 70px;
|
||||
height: 70px;
|
||||
padding-left: 30px;
|
||||
font-size: 16px;
|
||||
|
||||
&.head {
|
||||
border-bottom: 1px solid #f5f5f5;
|
||||
}
|
||||
}
|
||||
|
||||
.btn {
|
||||
width: 150px;
|
||||
height: 50px;
|
||||
border: 1px solid #e4e4e4;
|
||||
text-align: center;
|
||||
line-height: 48px;
|
||||
margin-left: 30px;
|
||||
color: #666666;
|
||||
display: inline-block;
|
||||
|
||||
&.active,
|
||||
&:hover {
|
||||
border-color: $xtxColor;
|
||||
}
|
||||
|
||||
&.alipay {
|
||||
background: url(https://cdn.cnbj1.fds.api.mi-img.com/mi-mall/7b6b02396368c9314528c0bbd85a2e06.png) no-repeat center / contain;
|
||||
}
|
||||
|
||||
&.wx {
|
||||
background: url(https://cdn.cnbj1.fds.api.mi-img.com/mi-mall/c66f98cff8649bd5ba722c2e8067c6ca.jpg) no-repeat center / contain;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,137 @@
|
|||
<script setup>
|
||||
import { getCategoryFilterAPI, getSubCategoryAPI } from '@/apis/category'
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import GoodsItem from '../Home/components/GoodsItem.vue'
|
||||
// 获取面包屑导航数据
|
||||
const categoryData = ref({})
|
||||
const route = useRoute()
|
||||
const getCategoryData = async () => {
|
||||
const res = await getCategoryFilterAPI(route.params.id)
|
||||
categoryData.value = res.result
|
||||
}
|
||||
onMounted(() => getCategoryData())
|
||||
|
||||
// 获取基础列表数据渲染
|
||||
const goodList = ref([])
|
||||
const reqData = ref({
|
||||
categoryId: route.params.id,
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
sortField: 'publishTime'
|
||||
})
|
||||
const getGoodList = async () => {
|
||||
const res = await getSubCategoryAPI(reqData.value)
|
||||
console.log(res)
|
||||
goodList.value = res.result.items
|
||||
}
|
||||
onMounted(() => getGoodList())
|
||||
|
||||
|
||||
// tab切换回调
|
||||
const tabChange = () => {
|
||||
console.log('tab切换了', reqData.value.sortField)
|
||||
reqData.value.page = 1
|
||||
getGoodList()
|
||||
}
|
||||
|
||||
// 加载更多
|
||||
const disabled = ref(false)
|
||||
const load = async () => {
|
||||
console.log('加载更多数据咯')
|
||||
// 获取下一页的数据
|
||||
reqData.value.page++
|
||||
const res = await getSubCategoryAPI(reqData.value)
|
||||
goodList.value = [...goodList.value, ...res.result.items]
|
||||
// 加载完毕 停止监听
|
||||
if (res.result.items.length === 0) {
|
||||
disabled.value = true
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="container ">
|
||||
<!-- 面包屑 -->
|
||||
<div class="bread-container">
|
||||
<el-breadcrumb separator=">">
|
||||
<el-breadcrumb-item :to="{ path: '/' }">首页</el-breadcrumb-item>
|
||||
<el-breadcrumb-item :to="{ path: `/category/${categoryData.parentId}` }">{{ categoryData.parentName }}
|
||||
</el-breadcrumb-item>
|
||||
<el-breadcrumb-item>{{ categoryData.name }}</el-breadcrumb-item>
|
||||
</el-breadcrumb>
|
||||
</div>
|
||||
<div class="sub-container">
|
||||
<el-tabs v-model="reqData.sortField" @tab-change="tabChange">
|
||||
<el-tab-pane label="最新商品" name="publishTime"></el-tab-pane>
|
||||
<el-tab-pane label="最高人气" name="orderNum"></el-tab-pane>
|
||||
<el-tab-pane label="评论最多" name="evaluateNum"></el-tab-pane>
|
||||
</el-tabs>
|
||||
<div class="body" v-infinite-scroll="load" :infinite-scroll-disabled="disabled">
|
||||
<!-- 商品列表-->
|
||||
<GoodsItem v-for="goods in goodList" :goods="goods" :key="goods.id" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.bread-container {
|
||||
padding: 25px 0;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.sub-container {
|
||||
padding: 20px 10px;
|
||||
background-color: #fff;
|
||||
|
||||
.body {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
.goods-item {
|
||||
display: block;
|
||||
width: 220px;
|
||||
margin-right: 20px;
|
||||
padding: 20px 30px;
|
||||
text-align: center;
|
||||
|
||||
img {
|
||||
width: 160px;
|
||||
height: 160px;
|
||||
}
|
||||
|
||||
p {
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
.name {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.desc {
|
||||
color: #999;
|
||||
height: 29px;
|
||||
}
|
||||
|
||||
.price {
|
||||
color: $priceColor;
|
||||
font-size: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.pagination-container {
|
||||
margin-top: 20px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,16 @@
|
|||
<template>
|
||||
<div class="container">
|
||||
<el-breadcrumb separator=">">
|
||||
<el-breadcrumb-item :to="{ path: '/' }">首页</el-breadcrumb-item>
|
||||
<el-breadcrumb-item>专题资源</el-breadcrumb-item>
|
||||
</el-breadcrumb>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.container {}
|
||||
</style>
|
|
@ -0,0 +1,262 @@
|
|||
<template>
|
||||
<div class="container">
|
||||
<div class="bread-container">
|
||||
<el-breadcrumb separator=">">
|
||||
<el-breadcrumb-item :to="{ path: '/' }">首页</el-breadcrumb-item>
|
||||
<el-breadcrumb-item>同步教材</el-breadcrumb-item>
|
||||
</el-breadcrumb>
|
||||
</div>
|
||||
|
||||
<el-card class="card-filter">
|
||||
<el-form label-width="auto">
|
||||
<el-form-item label="学段">
|
||||
<el-checkbox-group>
|
||||
<el-checkbox-button>小学</el-checkbox-button>
|
||||
<el-checkbox-button>初中</el-checkbox-button>
|
||||
</el-checkbox-group>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="年级">
|
||||
<el-checkbox-group>
|
||||
<el-checkbox-button>一年级</el-checkbox-button>
|
||||
<el-checkbox-button>二年级</el-checkbox-button>
|
||||
<el-checkbox-button>三年级</el-checkbox-button>
|
||||
<el-checkbox-button>四年级</el-checkbox-button>
|
||||
<el-checkbox-button>五年级</el-checkbox-button>
|
||||
<el-checkbox-button>六年级</el-checkbox-button>
|
||||
</el-checkbox-group>
|
||||
</el-form-item>
|
||||
|
||||
|
||||
<el-form-item label="学科">
|
||||
<el-checkbox-group>
|
||||
<el-checkbox-button>语文</el-checkbox-button>
|
||||
<el-checkbox-button>数学</el-checkbox-button>
|
||||
<el-checkbox-button>英语</el-checkbox-button>
|
||||
<el-checkbox-button>音乐</el-checkbox-button>
|
||||
<el-checkbox-button>美术</el-checkbox-button>
|
||||
<el-checkbox-button>特殊教育</el-checkbox-button>
|
||||
<el-checkbox-button>科学</el-checkbox-button>
|
||||
<el-checkbox-button>道德与法治</el-checkbox-button>
|
||||
<el-checkbox-button>体育与健康</el-checkbox-button>
|
||||
<el-checkbox-button>小学与劳动与技术</el-checkbox-button>
|
||||
</el-checkbox-group>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="版本">
|
||||
<el-checkbox-group>
|
||||
<el-checkbox-button>人教版</el-checkbox-button>
|
||||
<el-checkbox-button>苏教版</el-checkbox-button>
|
||||
</el-checkbox-group>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="教材">
|
||||
<el-checkbox-group>
|
||||
<el-checkbox-button>语文人教部编版一上年上册</el-checkbox-button>
|
||||
<el-checkbox-button>语文人教部编版一上年下册</el-checkbox-button>
|
||||
</el-checkbox-group>
|
||||
</el-form-item>
|
||||
|
||||
|
||||
<el-form-item label="类型">
|
||||
<el-checkbox-group>
|
||||
<el-checkbox-button>全部</el-checkbox-button>
|
||||
<el-checkbox-button>课件</el-checkbox-button>
|
||||
<el-checkbox-button>精品课堂</el-checkbox-button>
|
||||
<el-checkbox-button>作业</el-checkbox-button>
|
||||
<el-checkbox-button>试卷</el-checkbox-button>
|
||||
</el-checkbox-group>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="格式">
|
||||
<el-checkbox-group>
|
||||
<el-checkbox-button>全部</el-checkbox-button>
|
||||
<el-checkbox-button>PPT</el-checkbox-button>
|
||||
<el-checkbox-button>WORD</el-checkbox-button>
|
||||
<el-checkbox-button>PDF</el-checkbox-button>
|
||||
<el-checkbox-button>图片</el-checkbox-button>
|
||||
<el-checkbox-button>音频</el-checkbox-button>
|
||||
<el-checkbox-button>视频</el-checkbox-button>
|
||||
<el-checkbox-button>其它</el-checkbox-button>
|
||||
</el-checkbox-group>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
|
||||
<el-row :gutter="10" style="margin-top: 10px;">
|
||||
<el-col :span="6">
|
||||
<el-card>
|
||||
<template #header>
|
||||
<div class="card--left-header">
|
||||
<span class="title">目录</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-table :data="tableData" row-key="id" border default-expand-all :show-header="false">
|
||||
<el-table-column prop="name" label="Name" />
|
||||
</el-table>
|
||||
</el-card>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="18">
|
||||
<el-card>
|
||||
<div class="card-right-header">
|
||||
<el-table :data="tableSortData" :default-sort="{ prop: 'date', order: 'descending' }">
|
||||
<el-table-column prop="date" label="上传时间" sortable width="180" />
|
||||
<el-table-column prop="name" label="浏览量" sortable width="180" />
|
||||
<el-table-column prop="address" label="下载量" sortable width="180" />
|
||||
</el-table>
|
||||
</div>
|
||||
|
||||
<div class="book-grid">
|
||||
<el-card v-for="item in 6">
|
||||
<div class="book-content">
|
||||
<img class="file-type" src="@/assets/images/word.png" alt="">
|
||||
<img src="@/assets/images/book.png" alt="">
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<div class="book-title">汉字文化云课堂</div>
|
||||
<div class="book-des">
|
||||
<div class="book-teacher">
|
||||
<span>王老师</span> | <span>一年级(2)班</span>
|
||||
</div>
|
||||
<div class="book-view">
|
||||
<span>
|
||||
<el-icon>
|
||||
<View />
|
||||
</el-icon>
|
||||
</span>
|
||||
<span>204</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-card>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { View } from '@element-plus/icons-vue'
|
||||
|
||||
const tableData = [
|
||||
{
|
||||
id: 1,
|
||||
name: '全部',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: '我上学了',
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: '识字',
|
||||
children: [
|
||||
{
|
||||
id: 31,
|
||||
name: '1 天地人',
|
||||
},
|
||||
{
|
||||
id: 32,
|
||||
name: '2金木水火土',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: '汉语拼音',
|
||||
},
|
||||
]
|
||||
|
||||
const tableSortData = [
|
||||
{
|
||||
date: '2016-05-03',
|
||||
name: 'Tom',
|
||||
address: 'No. 189, Grove St, Los Angeles',
|
||||
},
|
||||
{
|
||||
date: '2016-05-02',
|
||||
name: 'Tom',
|
||||
address: 'No. 189, Grove St, Los Angeles',
|
||||
},
|
||||
{
|
||||
date: '2016-05-04',
|
||||
name: 'Tom',
|
||||
address: 'No. 189, Grove St, Los Angeles',
|
||||
},
|
||||
{
|
||||
date: '2016-05-01',
|
||||
name: 'Tom',
|
||||
address: 'No. 189, Grove St, Los Angeles',
|
||||
},
|
||||
]
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.container {
|
||||
.bread-container {
|
||||
padding: 25px 0;
|
||||
|
||||
:deep(.el-breadcrumb__inner) {
|
||||
color: #fff;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
:deep(.el-breadcrumb__separator) {
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
.card-filter {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.card--left-header {
|
||||
.title {
|
||||
font-size: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.card-right-header {
|
||||
:deep(.el-table__body-wrapper) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.book-grid {
|
||||
margin-top: 10px;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
grid-template-rows: repeat(2, 1fr);
|
||||
gap: 7px;
|
||||
|
||||
.book-content {
|
||||
position: relative;
|
||||
|
||||
.file-type {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 25px;
|
||||
}
|
||||
}
|
||||
|
||||
.book-title {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.book-des{
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
color:#919DA3;
|
||||
padding: 5px 0;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,43 @@
|
|||
import { fileURLToPath, URL } from 'node:url'
|
||||
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
|
||||
// elementPlus按需导入
|
||||
import AutoImport from 'unplugin-auto-import/vite'
|
||||
import Components from 'unplugin-vue-components/vite'
|
||||
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
vue(),
|
||||
// ...
|
||||
AutoImport({
|
||||
resolvers: [ElementPlusResolver()],
|
||||
}),
|
||||
Components({
|
||||
resolvers: [
|
||||
// 1. 配置elementPlus采用sass样式配色系统
|
||||
ElementPlusResolver({ importStyle: "sass" }),
|
||||
],
|
||||
}),
|
||||
],
|
||||
resolve: {
|
||||
// 实际的路径转换 @ -> src
|
||||
alias: {
|
||||
'@': fileURLToPath(new URL('./src', import.meta.url))
|
||||
}
|
||||
},
|
||||
css: {
|
||||
preprocessorOptions: {
|
||||
scss: {
|
||||
// 2. 自动导入定制化样式文件进行样式覆盖
|
||||
additionalData: `
|
||||
@use "@/styles/element/index.scss" as *;
|
||||
@use "@/styles/var.scss" as *;
|
||||
`,
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|