代码初始化

This commit is contained in:
jiangzhe 2024-06-12 15:47:55 +08:00
commit 1d82370c48
78 changed files with 7722 additions and 0 deletions

28
.gitignore vendored Normal file
View File

@ -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

35
README.md Normal file
View File

@ -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
```

17
index.html Normal file
View File

@ -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>

10
jsconfig.json Normal file
View File

@ -0,0 +1,10 @@
{
"compilerOptions": {
"baseUrl": "./",
"paths": {
"@/*": [
"src/*"
]
}
}
}

31
package.json Normal file
View File

@ -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"
}
}

2521
pnpm-lock.yaml Normal file

File diff suppressed because it is too large Load Diff

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

75
src/App.vue Normal file
View File

@ -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>

42
src/apis/cart.js Normal file
View File

@ -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
})
}

44
src/apis/category.js Normal file
View File

@ -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
})
}

19
src/apis/checkout.js Normal file
View File

@ -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
})
}

22
src/apis/detail.js Normal file
View File

@ -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
}
})
}

48
src/apis/home.js Normal file
View File

@ -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'
})
}

8
src/apis/layout.js Normal file
View File

@ -0,0 +1,8 @@
import httpInstance from "@/utils/http"
export function getCategoryAPI () {
return httpInstance({
url: '/home/category/head'
})
}

18
src/apis/order.js Normal file
View File

@ -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
})
}

7
src/apis/pay.js Normal file
View File

@ -0,0 +1,7 @@
import request from '@/utils/http'
export const getOrderAPI = (id) => {
return request({
url: `/member/order/${id}`
})
}

8
src/apis/testAPI.js Normal file
View File

@ -0,0 +1,8 @@
import httpInstance from "@/utils/http"
export function getCategory () {
return httpInstance({
url: 'home/category/head'
})
}

23
src/apis/user.js Normal file
View File

@ -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
}
})
}

74
src/assets/base.css Normal file
View File

@ -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;
}

BIN
src/assets/images/200.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

BIN
src/assets/images/book.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 188 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

BIN
src/assets/images/load.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

BIN
src/assets/images/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 KiB

BIN
src/assets/images/none.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 134 KiB

BIN
src/assets/images/word.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 940 B

1
src/assets/logo.svg Normal file
View File

@ -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

35
src/assets/main.css Normal file
View File

@ -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;
}
}

View File

@ -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);
// lefttop
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>

View File

@ -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
// undefinedkey
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
// changesku
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>

View File

@ -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
}

11
src/components/index.js Normal file
View File

@ -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)
}
}

View File

@ -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
}
}

26
src/directives/index.js Normal file
View File

@ -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()
}
},
)
}
})
}
}

28
src/main.js Normal file
View File

@ -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')

104
src/router/index.js Normal file
View File

@ -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

103
src/stores/cartStore.js Normal file
View File

@ -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,
})

View File

@ -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
}
})

41
src/stores/userStore.js Normal file
View File

@ -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,
})

103
src/styles/common.scss Normal file
View File

@ -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;
}

View File

@ -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,
),
)
);

5
src/styles/var.scss Normal file
View File

@ -0,0 +1,5 @@
$xtxColor: #27ba9b;
$helpColor: #e26237;
$sucColor: #1dc779;
$warnColor: #ffb302;
$priceColor: #cf4444;

35
src/utils/http.js Normal file
View File

@ -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

View File

@ -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>&yen;{{ i.price }}</p>
</td>
<td class="tc">
<el-input-number v-model="i.count" />
</td>
<td class="tc">
<p class="f16 red">&yen;{{ (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>

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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>

View File

@ -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>&yen;{{ i.price }}</td>
<td>{{ i.price }}</td>
<td>&yen;{{ i.totalPrice }}</td>
<td>&yen;{{ 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>

View File

@ -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">&yen;{{ 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>

413
src/views/Detail/index.vue Normal file
View File

@ -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>

View File

@ -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">&yen;{{ 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>

View File

@ -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>

View File

@ -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>

263
src/views/Home/index.vue Normal file
View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

358
src/views/Login/index.vue Normal file
View File

@ -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 &copy; 小兔鲜儿</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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

123
src/views/News/index.vue Normal file
View File

@ -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>

View File

@ -0,0 +1,13 @@
<template>
<div class="container">
动态详情
</div>
</template>
<script setup>
import { ref } from 'vue'
</script>
<style lang="scss" scoped>
.container {}
</style>

85
src/views/Pay/PayBack.vue Normal file
View File

@ -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>

154
src/views/Pay/index.vue Normal file
View File

@ -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())
//
// idget
//
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>

View File

@ -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>

View File

@ -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>

View File

@ -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>&nbsp;|&nbsp;<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>

43
vite.config.js Normal file
View File

@ -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 *;
`,
}
}
}
})