commit a68336073bf802fd4c064fa3188a8e54722d4dd2 Author: fantasticbin Date: Mon Jun 20 11:31:05 2022 +0800 代码 diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 0000000..3a5a471 --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,10 @@ +module.exports = { + root: true, + env: { + node: true, + }, + extends: ["plugin:vue/vue3-essential", "eslint:recommended"], + parserOptions: { + parser: "@babel/eslint-parser", + }, +}; \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..897cb9f --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +node_modules +package-lock.json +dist \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..6d2bd71 --- /dev/null +++ b/README.md @@ -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). diff --git a/babel.config.js b/babel.config.js new file mode 100644 index 0000000..eaea3d8 --- /dev/null +++ b/babel.config.js @@ -0,0 +1,3 @@ +module.exports = { + presets: ["@vue/cli-plugin-babel/preset"], +}; \ No newline at end of file diff --git a/config/webpack.config.js b/config/webpack.config.js new file mode 100644 index 0000000..8a4cb83 --- /dev/null +++ b/config/webpack.config.js @@ -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, +}; \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..f88f7f1 --- /dev/null +++ b/package.json @@ -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" + ] +} diff --git a/public/index.html b/public/index.html new file mode 100644 index 0000000..e442957 --- /dev/null +++ b/public/index.html @@ -0,0 +1,17 @@ + + + + + + + + hello-world + + + +
+ + + \ No newline at end of file diff --git a/src/App.vue b/src/App.vue new file mode 100644 index 0000000..57a1be1 --- /dev/null +++ b/src/App.vue @@ -0,0 +1,25 @@ + + + \ No newline at end of file diff --git a/src/api/shop.js b/src/api/shop.js new file mode 100644 index 0000000..16a4a54 --- /dev/null +++ b/src/api/shop.js @@ -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) + } +} \ No newline at end of file diff --git a/src/components/ProductList.vue b/src/components/ProductList.vue new file mode 100644 index 0000000..f6a4c1b --- /dev/null +++ b/src/components/ProductList.vue @@ -0,0 +1,48 @@ + + + \ No newline at end of file diff --git a/src/components/ShoppingCart.vue b/src/components/ShoppingCart.vue new file mode 100644 index 0000000..270978e --- /dev/null +++ b/src/components/ShoppingCart.vue @@ -0,0 +1,54 @@ + + + \ No newline at end of file diff --git a/src/main.js b/src/main.js new file mode 100644 index 0000000..c5fadc3 --- /dev/null +++ b/src/main.js @@ -0,0 +1,5 @@ +import { createApp } from "vue"; +import store from "./store" +import App from "./App"; + +createApp(App).use(store).mount(document.getElementById("app")); \ No newline at end of file diff --git a/src/store/index.js b/src/store/index.js new file mode 100644 index 0000000..27f81c7 --- /dev/null +++ b/src/store/index.js @@ -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 + }, +}) \ No newline at end of file diff --git a/src/store/modules/cart.js b/src/store/modules/cart.js new file mode 100644 index 0000000..8b5d43f --- /dev/null +++ b/src/store/modules/cart.js @@ -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 +} \ No newline at end of file diff --git a/src/store/modules/products.js b/src/store/modules/products.js new file mode 100644 index 0000000..8921af4 --- /dev/null +++ b/src/store/modules/products.js @@ -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 +} \ No newline at end of file diff --git a/src/store/mutation-types.js b/src/store/mutation-types.js new file mode 100644 index 0000000..f81cffa --- /dev/null +++ b/src/store/mutation-types.js @@ -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' +} \ No newline at end of file diff --git a/src/styles/element/index.scss b/src/styles/element/index.scss new file mode 100644 index 0000000..526e86f --- /dev/null +++ b/src/styles/element/index.scss @@ -0,0 +1,7 @@ +@forward 'element-plus/theme-chalk/src/common/var.scss' with ( + $colors: ( + 'primary': ( + 'base': green, + ), + ), +); \ No newline at end of file