This commit is contained in:
fantasticbin 2022-06-20 11:31:05 +08:00
commit a68336073b
17 changed files with 665 additions and 0 deletions

10
.eslintrc.js Normal file
View File

@ -0,0 +1,10 @@
module.exports = {
root: true,
env: {
node: true,
},
extends: ["plugin:vue/vue3-essential", "eslint:recommended"],
parserOptions: {
parser: "@babel/eslint-parser",
},
};

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
node_modules
package-lock.json
dist

21
README.md Normal file
View File

@ -0,0 +1,21 @@
# vue_cart
> 使用 Vue 2 脚手架搭建一个简易购物车功能示例
## Build Setup
``` bash
# install dependencies
npm install
# serve with hot reload at localhost:8080
npm run dev
# build for production with minification
npm run build
# build for production and view the bundle analyzer report
npm run build --report
```
For a detailed explanation on how things work, check out the [guide](http://vuejs-templates.github.io/webpack/) and [docs for vue-loader](http://vuejs.github.io/vue-loader).

3
babel.config.js Normal file
View File

@ -0,0 +1,3 @@
module.exports = {
presets: ["@vue/cli-plugin-babel/preset"],
};

225
config/webpack.config.js Normal file
View File

@ -0,0 +1,225 @@
const path = require("path");
const ESLintWebpackPlugin = require("eslint-webpack-plugin");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const CssMinimizerPlugin = require("css-minimizer-webpack-plugin");
const TerserWebpackPlugin = require("terser-webpack-plugin");
const CopyPlugin = require("copy-webpack-plugin");
const { VueLoaderPlugin } = require("vue-loader");
const { DefinePlugin } = require("webpack");
const AutoImport = require("unplugin-auto-import/webpack");
const Components = require("unplugin-vue-components/webpack");
const { ElementPlusResolver } = require("unplugin-vue-components/resolvers");
// 需要通过 cross-env 定义环境变量
const isProduction = process.env.NODE_ENV === "production";
const getStyleLoaders = (preProcessor) => {
return [
isProduction ? MiniCssExtractPlugin.loader : "vue-style-loader",
"css-loader",
{
loader: "postcss-loader",
options: {
postcssOptions: {
plugins: ["postcss-preset-env"],
},
},
},
preProcessor && {
loader: preProcessor,
options:
preProcessor === "sass-loader"
? {
// 自定义主题自动引入我们定义的scss文件
additionalData: `@use "@/styles/element/index.scss" as *;`,
}
: {},
},
].filter(Boolean);
};
module.exports = {
entry: "./src/main.js",
output: {
path: isProduction ? path.resolve(__dirname, "../dist") : undefined,
filename: isProduction
? "static/js/[name].[contenthash:10].js"
: "static/js/[name].js",
chunkFilename: isProduction
? "static/js/[name].[contenthash:10].chunk.js"
: "static/js/[name].chunk.js",
assetModuleFilename: "static/js/[hash:10][ext][query]",
clean: true,
},
module: {
rules: [
{
test: /\.css$/,
use: getStyleLoaders(),
},
{
test: /\.less$/,
use: getStyleLoaders("less-loader"),
},
{
test: /\.s[ac]ss$/,
use: getStyleLoaders("sass-loader"),
},
{
test: /\.styl$/,
use: getStyleLoaders("stylus-loader"),
},
{
test: /\.(png|jpe?g|gif|svg)$/,
type: "asset",
parser: {
dataUrlCondition: {
maxSize: 10 * 1024,
},
},
},
{
test: /\.(ttf|woff2?)$/,
type: "asset/resource",
},
{
test: /\.(jsx|js)$/,
include: path.resolve(__dirname, "../src"),
loader: "babel-loader",
options: {
cacheDirectory: true,
cacheCompression: false,
plugins: [
// "@babel/plugin-transform-runtime" // presets中包含了
],
},
},
// vue-loader不支持oneOf
{
test: /\.vue$/,
loader: "vue-loader", // 内部会给vue文件注入HMR功能代码
options: {
// 开启缓存
cacheDirectory: path.resolve(
__dirname,
"node_modules/.cache/vue-loader"
),
},
},
],
},
plugins: [
new ESLintWebpackPlugin({
context: path.resolve(__dirname, "../src"),
exclude: "node_modules",
cache: true,
cacheLocation: path.resolve(
__dirname,
"../node_modules/.cache/.eslintcache"
),
}),
new HtmlWebpackPlugin({
template: path.resolve(__dirname, "../public/index.html"),
}),
new CopyPlugin({
patterns: [
{
from: path.resolve(__dirname, "../public"),
to: path.resolve(__dirname, "../dist"),
toType: "dir",
noErrorOnMissing: true,
globOptions: {
ignore: ["**/index.html"],
},
info: {
minimized: true,
},
},
],
}),
isProduction &&
new MiniCssExtractPlugin({
filename: "static/css/[name].[contenthash:10].css",
chunkFilename: "static/css/[name].[contenthash:10].chunk.css",
}),
new VueLoaderPlugin(),
new DefinePlugin({
__VUE_OPTIONS_API__: "true",
__VUE_PROD_DEVTOOLS__: "false",
}),
// 按需加载element-plus组件样式
AutoImport({
resolvers: [ElementPlusResolver()],
}),
Components({
resolvers: [
ElementPlusResolver({
importStyle: "sass", // 自定义主题
}),
],
}),
].filter(Boolean),
optimization: {
minimize: isProduction,
// 压缩的操作
minimizer: [
new CssMinimizerPlugin(),
new TerserWebpackPlugin(),
],
splitChunks: {
chunks: "all",
cacheGroups: {
// layouts通常是admin项目的主体布局组件所有路由组件都要使用的
// 可以单独打包,从而复用
// 如果项目中没有,请删除
/*layouts: {
name: "layouts",
test: path.resolve(__dirname, "../src/layouts"),
priority: 40,
},*/
// 如果项目中使用element-plus此时将所有node_modules打包在一起那么打包输出文件会比较大。
// 所以我们将node_modules中比较大的模块单独打包从而并行加载速度更好
// 如果项目中没有,请删除
elementUI: {
name: "chunk-elementPlus",
test: /[\\/]node_modules[\\/]_?element-plus(.*)/,
priority: 30,
},
// 将vue相关的库单独打包减少node_modules的chunk体积。
vue: {
name: "vue",
test: /[\\/]node_modules[\\/]vue(.*)[\\/]/,
chunks: "initial",
priority: 20,
},
libs: {
name: "chunk-libs",
test: /[\\/]node_modules[\\/]/,
priority: 10, // 权重最低,优先考虑前面内容
chunks: "initial",
},
},
},
runtimeChunk: {
name: (entrypoint) => `runtime~${entrypoint.name}`,
},
},
resolve: {
extensions: [".vue", ".js", ".json"],
alias: {
// 路径别名
"@": path.resolve(__dirname, "../src"),
},
},
devServer: {
open: true,
host: "localhost",
port: 3000,
hot: true,
compress: true,
historyApiFallback: true, // 解决vue-router刷新404问题
},
mode: isProduction ? "production" : "development",
devtool: isProduction ? "source-map" : "cheap-module-source-map",
performance: false,
};

52
package.json Normal file
View File

@ -0,0 +1,52 @@
{
"name": "vue_cart",
"version": "1.0.0",
"description": "",
"main": "main.js",
"scripts": {
"start": "npm run dev",
"dev": "cross-env NODE_ENV=development webpack serve --config ./config/webpack.config.js",
"build": "cross-env NODE_ENV=production webpack --config ./config/webpack.config.js"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"@babel/core": "^7.17.10",
"@babel/eslint-parser": "^7.17.0",
"@vue/cli-plugin-babel": "^5.0.4",
"babel-loader": "^8.2.5",
"copy-webpack-plugin": "^10.2.4",
"cross-env": "^7.0.3",
"css-loader": "^6.7.1",
"css-minimizer-webpack-plugin": "^3.4.1",
"eslint-plugin-vue": "^8.7.1",
"eslint-webpack-plugin": "^3.1.1",
"html-webpack-plugin": "^5.5.0",
"less-loader": "^10.2.0",
"mini-css-extract-plugin": "^2.6.0",
"postcss-preset-env": "^7.5.0",
"sass": "^1.52.3",
"sass-loader": "^12.6.0",
"stylus-loader": "^6.2.0",
"unplugin-auto-import": "^0.8.8",
"unplugin-vue-components": "^0.19.6",
"vue-loader": "^17.0.0",
"vue-style-loader": "^4.1.3",
"vue-template-compiler": "^2.6.14",
"webpack": "^5.72.0",
"webpack-cli": "^4.9.2",
"webpack-dev-server": "^4.9.0"
},
"dependencies": {
"element-plus": "^2.2.6",
"vue": "^3.2.33",
"vue-router": "^4.0.15",
"vuex": "^4.0.2"
},
"browserslist": [
"last 2 version",
"> 1%",
"not dead"
]
}

17
public/index.html Normal file
View File

@ -0,0 +1,17 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="<%= htmlWebpackPlugin.options.url %>favicon.ico">
<title>hello-world</title>
</head>
<body>
<noscript>
<strong>We're sorry but hello-world doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>

25
src/App.vue Normal file
View File

@ -0,0 +1,25 @@
<template>
<div id="app">
<h1>购物车示例</h1>
<p>账号: {{email}}</p>
<hr>
<h2>产品</h2>
<ProductList/>
<hr>
<ShoppingCart/>
</div>
</template>
<script>
import { mapState } from 'vuex'
import ProductList from './components/ProductList.vue'
import ShoppingCart from './components/ShoppingCart.vue'
import 'element-plus/es/components/button/style/css'
export default {
computed: mapState({
email: state => state.userInfo.email
}),
components: { ProductList, ShoppingCart }
}
</script>

23
src/api/shop.js Normal file
View File

@ -0,0 +1,23 @@
/**
* Mocking client-server processing
*/
const _products = [
{"id": 1, "title": "华为 Mate 20", "price": 3999, "inventory": 2},
{"id": 2, "title": "小米 9", "price": 2999, "inventory": 0},
{"id": 3, "title": "OPPO R17", "price": 2999, "inventory": 5}
]
export default {
getProducts (cb) {
setTimeout(() => cb(_products), 100)
},
buyProducts (products, cb, errorCb) {
setTimeout(() => {
// simulate random checkout failure.
Math.random() > 0.5
? cb()
: errorCb()
}, 100)
}
}

View File

@ -0,0 +1,48 @@
<template>
<ul>
<li
v-for="product in products"
:key="product.id">
{{ product.title }} - {{ product.price }} x {{ product.inventory }}
<br>
<el-input-number v-if="product.inventory" :min="1" :max="product.inventory" v-model="preSelectProducts[product.id]" />
<el-input-number v-else :disabled="true" />
<el-button
:disabled="!product.inventory"
@click="addProductToCart({ product: product, num: preSelectProducts[product.id] })">
加入购物车
</el-button>
</li>
</ul>
</template>
<script>
import { mapState, mapActions } from 'vuex'
import { ElButton } from 'element-plus'
export default {
components: { ElButton },
computed: {
...mapState({
products: state => state.products.all,
preSelectProducts: state => state.cart.preSelectProducts
})
},
// computed: {
// products(){
// return this.$store.state.products.all
// }
// },
methods: mapActions('cart', [
'addProductToCart'
]),
// methods: {
// addProductToCart(product){
// this.$store.dispatch('cart/addProductToCart', product)
// }
// },
created () {
this.$store.dispatch('products/getAllProducts');
}
}
</script>

View File

@ -0,0 +1,54 @@
<template>
<div class="cart">
<h2>清单</h2>
<p v-show="!products.length"><i>请添加产品到购物车</i></p>
<ul>
<li
v-for="product in products"
:key="product.id">
{{ product.title }} - {{ product.price }} x {{ product.quantity }}
</li>
</ul>
<p>合计: {{ total }}</p>
<p><el-button type="primary" :disabled="!products.length" @click="checkout(products)">提交</el-button></p>
<p v-show="checkoutStatus">提交 {{ checkoutStatus }}.</p>
</div>
</template>
<script>
import { mapGetters, mapState } from 'vuex'
import { ElButton } from 'element-plus'
export default {
components: { ElButton },
computed: {
...mapState({
checkoutStatus: state => state.cart.checkoutStatus
}),
...mapGetters('cart', {
products: 'cartProducts',
total: 'cartTotalPrice'
}),
// ...mapGetters({
// products: 'cart/cartProducts',
// total: 'cart/cartTotalPrice'
// })
},
// computed: {
// checkoutStatus(){
// return this.$store.state.cart.checkoutStatus
// },
// products() {
// return this.$store.getters['cart/cartProducts']
// },
// total() {
// return this.$store.getters['cart/cartTotalPrice']
// }
// },
methods: {
checkout (products) {
this.$store.dispatch('cart/checkout', products)
}
},
}
</script>

5
src/main.js Normal file
View File

@ -0,0 +1,5 @@
import { createApp } from "vue";
import store from "./store"
import App from "./App";
createApp(App).use(store).mount(document.getElementById("app"));

17
src/store/index.js Normal file
View File

@ -0,0 +1,17 @@
import { createStore } from 'vuex'
import cart from './modules/cart'
import products from './modules/products'
export default createStore({
state () {
return {
userInfo: {
email: "xxxxxx@qq.com"
}
};
},
modules: {
cart,
products
},
})

101
src/store/modules/cart.js Normal file
View File

@ -0,0 +1,101 @@
import shop from '../../api/shop'
import { CART, PRODUCTS } from '../mutation-types'
// initial state
// shape: [{ id, quantity }]
const state = {
// 购物车商品列表数据
items: [],
// 用于存放要加入购物车的商品跟数量的对应关系
preSelectProducts: {},
// 标记购买状态
checkoutStatus: null
}
// getters
const getters = {
cartProducts: (state, getters, rootState) => {
return state.items.map(({ id, quantity }) => {
const product = rootState.products.all.find(product => product.id === id)
return {
title: product.title,
price: product.price,
quantity
}
})
},
cartTotalPrice: (state, getters) => {
return getters.cartProducts.reduce((total, product) => {
return total + product.price * product.quantity
}, 0)
}
}
// actions
const actions = {
checkout ({ commit, state }, products) {
const savedCartItems = [...state.items]
commit(CART.SET_CHECKOUT_STATUS, null)
// empty cart
commit(CART.SET_CART_ITEMS, { items: [] })
shop.buyProducts(
products,
() => commit(CART.SET_CHECKOUT_STATUS, 'successful'),
() => {
commit(CART.SET_CHECKOUT_STATUS, 'failed')
// rollback to the cart saved before sending the request
commit(CART.SET_CART_ITEMS, { items: savedCartItems })
}
)
},
addProductToCart ({ state, commit }, { product, num} ) {
num = num < 1 ? 1 : num;
num = num > product.inventory ? product.inventory : num;
commit(CART.SET_CHECKOUT_STATUS, null)
if (product.inventory > 0) {
const cartItem = state.items.find(item => item.id === product.id)
if (!cartItem) {
commit(CART.PUSH_PRODUCT_TO_CART, { id: product.id, num: num })
} else {
commit(CART.INCREMENT_ITEM_QUANTITY, { id: cartItem.id, num: num })
}
// remove 1 item from stock
commit(`products/${PRODUCTS.DECREMENT_PRODUCT_INVENTORY}`, { id: product.id, num: num }, { root: true })
state.preSelectProducts[product.id] = 1;
}
}
}
// mutations
const mutations = {
[CART.PUSH_PRODUCT_TO_CART] (state, { id, num }) {
state.items.push({
id,
quantity: Number(num)
})
},
[CART.INCREMENT_ITEM_QUANTITY] (state, { id, num }) {
const cartItem = state.items.find(item => item.id === id)
cartItem.quantity += Number(num)
},
[CART.SET_CART_ITEMS] (state, { items }) {
state.items = items
},
[CART.SET_CHECKOUT_STATUS] (state, status) {
state.checkoutStatus = status
}
}
export default {
namespaced: true,
state,
getters,
actions,
mutations
}

View File

@ -0,0 +1,43 @@
import shop from '../../api/shop'
import {PRODUCTS} from '../mutation-types'
// initial state
const state = {
// 商品列表
all: []
}
// getters
const getters = {}
// actions
const actions = {
getAllProducts ({ commit, rootState }) {
shop.getProducts(products => {
for (let product of products) {
rootState.cart.preSelectProducts = Object.assign({}, rootState.cart.preSelectProducts, { [product.id]: 1 });
}
commit(PRODUCTS.SET_PRODUCTS, products)
})
}
}
// mutations
const mutations = {
[PRODUCTS.SET_PRODUCTS] (state, products) {
state.all = products
},
[PRODUCTS.DECREMENT_PRODUCT_INVENTORY] (state, { id, num }) {
const product = state.all.find(product => product.id === id)
product.inventory -= Number(num)
}
}
export default {
namespaced: true,
state,
getters,
actions,
mutations
}

View File

@ -0,0 +1,11 @@
export const CART = {
PUSH_PRODUCT_TO_CART: 'pushProductToCart',
INCREMENT_ITEM_QUANTITY: 'incrementItemQuantity',
SET_CART_ITEMS: 'setCartItems',
SET_CHECKOUT_STATUS: 'setCheckoutStatus'
}
export const PRODUCTS = {
SET_PRODUCTS:'setProducts',
DECREMENT_PRODUCT_INVENTORY: 'decrementProductInventory'
}

View File

@ -0,0 +1,7 @@
@forward 'element-plus/theme-chalk/src/common/var.scss' with (
$colors: (
'primary': (
'base': green,
),
),
);