打包工具由webpack变为vite、将vue2代码重构成vue3、vuex替换成pinia,并使用typescript来编码
This commit is contained in:
parent
812bf3afc0
commit
8ca1dc38f5
8
.eslintignore
Normal file
8
.eslintignore
Normal file
@ -0,0 +1,8 @@
|
||||
/build/
|
||||
/config/
|
||||
/dist/
|
||||
/*.js
|
||||
/*.zip
|
||||
/*.rar
|
||||
/node_modules/
|
||||
/test/unit/coverage/
|
32
.eslintrc.js
32
.eslintrc.js
@ -1,10 +1,32 @@
|
||||
// https://eslint.org/docs/user-guide/configuring
|
||||
|
||||
module.exports = {
|
||||
root: true,
|
||||
env: {
|
||||
node: true,
|
||||
extends: [
|
||||
'eslint:recommended',
|
||||
"plugin:vue/vue3-recommended",
|
||||
"prettier"
|
||||
],
|
||||
// required to lint *.vue files
|
||||
plugins: [
|
||||
'vue'
|
||||
],
|
||||
// add your custom rules here
|
||||
rules: {
|
||||
// allow async-await
|
||||
'generator-star-spacing': 'off',
|
||||
// allow debugger during development
|
||||
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',
|
||||
'no-labels': ['warn', {'allowLoop': true}],
|
||||
'no-unused-vars': 'off',
|
||||
'vue/no-mutating-props': 'off',
|
||||
'vue/require-default-prop': 'off',
|
||||
'vue/no-unused-vars': 'off',
|
||||
'vue/prop-name-casing': 'off',
|
||||
'vue/attribute-hyphenation': 'off',
|
||||
'vue/no-template-shadow': 'off',
|
||||
'vue/component-definition-name-casing': 'off'
|
||||
},
|
||||
extends: ["plugin:vue/vue3-essential", "eslint:recommended"],
|
||||
parserOptions: {
|
||||
parser: "@babel/eslint-parser",
|
||||
},
|
||||
};
|
||||
}
|
23
.gitignore
vendored
23
.gitignore
vendored
@ -1,3 +1,24 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
package-lock.json
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
23
README.md
23
README.md
@ -1,21 +1,16 @@
|
||||
# vue_cart
|
||||
# 使用 Vue 3 + Pinia + TypeScript + Vite 搭建一个简易购物车功能示例
|
||||
|
||||
> 使用 Vue 2 脚手架搭建一个简易购物车功能示例
|
||||
This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
|
||||
|
||||
## Build Setup
|
||||
## Recommended IDE Setup
|
||||
|
||||
``` bash
|
||||
# install dependencies
|
||||
npm install
|
||||
- [VS Code](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar)
|
||||
|
||||
# serve with hot reload at localhost:8080
|
||||
npm run dev
|
||||
## Type Support For `.vue` Imports in TS
|
||||
|
||||
# build for production with minification
|
||||
npm run build
|
||||
Since TypeScript cannot handle type information for `.vue` imports, they are shimmed to be a generic Vue component type by default. In most cases this is fine if you don't really care about component prop types outside of templates. However, if you wish to get actual prop types in `.vue` imports (for example to get props validation when using manual `h(...)` calls), you can enable Volar's Take Over mode by following these steps:
|
||||
|
||||
# build for production and view the bundle analyzer report
|
||||
npm run build --report
|
||||
```
|
||||
1. Run `Extensions: Show Built-in Extensions` from VS Code's command palette, look for `TypeScript and JavaScript Language Features`, then right click and select `Disable (Workspace)`. By default, Take Over mode will enable itself if the default TypeScript extension is disabled.
|
||||
2. Reload the VS Code window by running `Developer: Reload Window` from the command palette.
|
||||
|
||||
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).
|
||||
You can learn more about Take Over mode [here](https://github.com/johnsoncodehk/volar/discussions/471).
|
||||
|
5
auto-imports.d.ts
vendored
Normal file
5
auto-imports.d.ts
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
// Generated by 'unplugin-auto-import'
|
||||
export {}
|
||||
declare global {
|
||||
|
||||
}
|
@ -1,3 +0,0 @@
|
||||
module.exports = {
|
||||
presets: ["@vue/cli-plugin-babel/preset"],
|
||||
};
|
14
components.d.ts
vendored
Normal file
14
components.d.ts
vendored
Normal file
@ -0,0 +1,14 @@
|
||||
// generated by unplugin-vue-components
|
||||
// We suggest you to commit this file into source control
|
||||
// Read more: https://github.com/vuejs/vue-next/pull/3399
|
||||
import '@vue/runtime-core'
|
||||
|
||||
declare module '@vue/runtime-core' {
|
||||
export interface GlobalComponents {
|
||||
ElInputNumber: typeof import('element-plus/es')['ElInputNumber']
|
||||
ProductList: typeof import('./src/components/ProductList.vue')['default']
|
||||
ShoppingCart: typeof import('./src/components/ShoppingCart.vue')['default']
|
||||
}
|
||||
}
|
||||
|
||||
export {}
|
@ -1,225 +0,0 @@
|
||||
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,
|
||||
};
|
13
index.html
Normal file
13
index.html
Normal file
@ -0,0 +1,13 @@
|
||||
<!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>Vite App</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
73
package.json
73
package.json
@ -1,52 +1,35 @@
|
||||
{
|
||||
"name": "vue_cart",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "main.js",
|
||||
"name": "vue3_vite_pinia_cart",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"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"
|
||||
"dev": "vite",
|
||||
"build": "vue-tsc --noEmit && vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"element-plus": "^2.2.6",
|
||||
"vue": "^3.2.33",
|
||||
"vue-router": "^4.0.15",
|
||||
"vuex": "^4.0.2"
|
||||
"pinia": "^2.0.14",
|
||||
"vue": "^3.2.25"
|
||||
},
|
||||
"browserslist": [
|
||||
"last 2 version",
|
||||
"> 1%",
|
||||
"not dead"
|
||||
]
|
||||
"devDependencies": {
|
||||
"@babel/eslint-parser": "^7.18.2",
|
||||
"@typescript-eslint/eslint-plugin": "^5.29.0",
|
||||
"@typescript-eslint/parser": "^5.29.0",
|
||||
"@vitejs/plugin-vue": "^2.3.3",
|
||||
"eslint": "^8.18.0",
|
||||
"eslint-config-alloy": "^4.5.1",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"eslint-plugin-prettier": "^4.0.0",
|
||||
"eslint-plugin-vue": "^9.1.1",
|
||||
"less": "^4.1.3",
|
||||
"less-loader": "^11.0.0",
|
||||
"typescript": "^4.5.4",
|
||||
"unplugin-auto-import": "^0.8.8",
|
||||
"unplugin-vue-components": "^0.19.6",
|
||||
"vite": "^2.9.9",
|
||||
"vite-plugin-eslint": "^1.6.1",
|
||||
"vue-eslint-parser": "^9.0.2",
|
||||
"vue-tsc": "^0.34.7"
|
||||
}
|
||||
}
|
||||
|
BIN
public/favicon.ico
Normal file
BIN
public/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.2 KiB |
@ -1,17 +0,0 @@
|
||||
<!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>
|
12
src/App.vue
12
src/App.vue
@ -10,16 +10,12 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState } from 'vuex'
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { useUser } from './store/user'
|
||||
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 }
|
||||
}
|
||||
const email = computed(() => useUser().userInfo.email)
|
||||
</script>
|
@ -1,23 +0,0 @@
|
||||
/**
|
||||
* 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)
|
||||
}
|
||||
}
|
30
src/api/shop.ts
Normal file
30
src/api/shop.ts
Normal file
@ -0,0 +1,30 @@
|
||||
type product = {
|
||||
id: number,
|
||||
title: string,
|
||||
price: number,
|
||||
inventory: number,
|
||||
}
|
||||
|
||||
const _products: product[] = [
|
||||
{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}
|
||||
]
|
||||
|
||||
const shop = {
|
||||
getProducts (cb: (product: product[]) => void) {
|
||||
setTimeout(() => cb(_products), 100)
|
||||
},
|
||||
|
||||
buyProducts (cb: () => void, errorCb: () => void) {
|
||||
setTimeout(() => {
|
||||
// simulate random checkout failure.
|
||||
Math.random() > 0.5
|
||||
? cb()
|
||||
: errorCb()
|
||||
}, 100)
|
||||
}
|
||||
}
|
||||
|
||||
export { shop }
|
||||
export type { product }
|
BIN
src/assets/logo.png
Normal file
BIN
src/assets/logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 6.7 KiB |
@ -16,54 +16,41 @@
|
||||
</ul>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState } from 'vuex'
|
||||
import { ElButton } from 'element-plus'
|
||||
<script lang="ts">
|
||||
import type { product } from '../api/shop'
|
||||
|
||||
export default {
|
||||
data () {
|
||||
return {
|
||||
numbers: {}
|
||||
};
|
||||
},
|
||||
components: { ElButton },
|
||||
computed: mapState({
|
||||
products: state => state.products.all,
|
||||
}),
|
||||
// computed: {
|
||||
// products(){
|
||||
// return this.$store.state.products.all
|
||||
// }
|
||||
// },
|
||||
watch: {
|
||||
products: {
|
||||
handler: function (val) {
|
||||
val.forEach(product => {
|
||||
if (this.numbers[product.id] === undefined) {
|
||||
// this.$set(this.numbers, product.id, 1);
|
||||
// Vue 3 已不再需要动态设置数据属性绑定了
|
||||
this.numbers[product.id] = 1;
|
||||
}
|
||||
});
|
||||
},
|
||||
// 添加 immediate: true 后该回调将会在侦听开始之后被立即调用
|
||||
immediate: true
|
||||
}
|
||||
},
|
||||
// methods: mapActions('cart', [
|
||||
// 'addProductToCart'
|
||||
// ]),
|
||||
methods: {
|
||||
addProductToCart(product){
|
||||
this.numbers[product.id] = this.numbers[product.id] < 1 ? 1 : this.numbers[product.id];
|
||||
this.numbers[product.id] = this.numbers[product.id] > product.inventory ? product.inventory : this.numbers[product.id];
|
||||
|
||||
this.$store.dispatch('cart/addProductToCart', { product, number: this.numbers[product.id] });
|
||||
this.numbers[product.id] = 1;
|
||||
}
|
||||
},
|
||||
created () {
|
||||
this.$store.dispatch('products/getAllProducts');
|
||||
}
|
||||
type Numbers = {
|
||||
[id: number]: number
|
||||
}
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { reactive, computed, watch } from 'vue'
|
||||
import { useCart } from '../store/cart'
|
||||
import { useProducts } from '../store/products'
|
||||
import { ElButton } from 'element-plus'
|
||||
|
||||
const numbers = reactive({}) as Numbers
|
||||
const products = computed(() => useProducts().all)
|
||||
const addProductToCart = (product: product) => {
|
||||
numbers[product.id] = numbers !== {} && numbers[product.id] < 1 ? 1 : numbers[product.id]
|
||||
numbers[product.id] = numbers[product.id] > product.inventory ? product.inventory : numbers[product.id]
|
||||
|
||||
useCart().addProductToCart(product, numbers[product.id])
|
||||
numbers[product.id] = 1
|
||||
}
|
||||
|
||||
watch(products, (val) => {
|
||||
val.forEach((product) => {
|
||||
if (!numbers.hasOwnProperty(product.id)) {
|
||||
numbers[product.id] = 1
|
||||
}
|
||||
})
|
||||
}, {
|
||||
// 添加 immediate: true 后该回调将会在侦听开始之后被立即调用
|
||||
immediate: true
|
||||
})
|
||||
|
||||
// 加载商品列表
|
||||
useProducts().getAllProducts()
|
||||
</script>
|
@ -10,45 +10,20 @@
|
||||
</li>
|
||||
</ul>
|
||||
<p>合计: {{ total }}</p>
|
||||
<p><el-button type="primary" :disabled="!products.length" @click="checkout(products)">提交</el-button></p>
|
||||
<p><el-button type="primary" :disabled="!products.length" @click="checkout()">提交</el-button></p>
|
||||
<p v-show="checkoutStatus">提交 {{ checkoutStatus }}.</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters, mapState } from 'vuex'
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { useCart } from '../store/cart'
|
||||
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)
|
||||
}
|
||||
},
|
||||
const checkoutStatus = computed(() => useCart().checkoutStatus)
|
||||
const products = computed(() => useCart().cartProducts)
|
||||
const total = computed(() => useCart().cartTotalPrice)
|
||||
const checkout = function () {
|
||||
useCart().checkout()
|
||||
}
|
||||
</script>
|
8
src/env.d.ts
vendored
Normal file
8
src/env.d.ts
vendored
Normal file
@ -0,0 +1,8 @@
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
declare module '*.vue' {
|
||||
import type { DefineComponent } from 'vue'
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types
|
||||
const component: DefineComponent<{}, {}, any>
|
||||
export default component
|
||||
}
|
@ -1,5 +0,0 @@
|
||||
import { createApp } from "vue";
|
||||
import store from "./store"
|
||||
import App from "./App";
|
||||
|
||||
createApp(App).use(store).mount(document.getElementById("app"));
|
5
src/main.ts
Normal file
5
src/main.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import { createApp } from 'vue'
|
||||
import { createPinia } from 'pinia'
|
||||
import App from './App.vue'
|
||||
|
||||
createApp(App).use(createPinia()).mount('#app')
|
102
src/store/cart.ts
Normal file
102
src/store/cart.ts
Normal file
@ -0,0 +1,102 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { shop, product } from '../api/shop'
|
||||
import { useProducts } from './products'
|
||||
|
||||
/**
|
||||
* 购物车商品列表数据定义
|
||||
*/
|
||||
type CartItem = {
|
||||
id: number,
|
||||
quantity: number,
|
||||
}
|
||||
|
||||
/**
|
||||
* 购物车商品列表信息定义
|
||||
*/
|
||||
type CartItemInfo = {
|
||||
// 商品ID
|
||||
id: number,
|
||||
// 标题
|
||||
title: string,
|
||||
// 价格
|
||||
price: number,
|
||||
// 数量
|
||||
quantity: number,
|
||||
}
|
||||
|
||||
/**
|
||||
* state定义
|
||||
*/
|
||||
type state = {
|
||||
// 购物车商品列表数据
|
||||
items: CartItem[],
|
||||
// 标记购买状态
|
||||
checkoutStatus: null | string
|
||||
}
|
||||
|
||||
export const useCart = defineStore('cart', {
|
||||
state: (): state => {
|
||||
return {
|
||||
items: [],
|
||||
checkoutStatus: null
|
||||
}
|
||||
},
|
||||
|
||||
getters: {
|
||||
cartProducts (): CartItemInfo[] {
|
||||
return this.items.map(({ id, quantity }) => {
|
||||
const product = useProducts().all.find(product => product.id === id) as product
|
||||
|
||||
return {
|
||||
id: product.id,
|
||||
title: product.title,
|
||||
price: product.price,
|
||||
quantity
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
cartTotalPrice () {
|
||||
const products = this.cartProducts as CartItemInfo[] // 不知道为啥直接this.cartProducts.reduce就会报错,必须要转换一下
|
||||
return products.reduce((total, product) => {
|
||||
return total + product.price * product.quantity
|
||||
}, 0)
|
||||
}
|
||||
},
|
||||
|
||||
actions: {
|
||||
checkout () {
|
||||
const savedCartItems = [...this.items]
|
||||
this.checkoutStatus = null
|
||||
// empty cart
|
||||
this.items = []
|
||||
shop.buyProducts(
|
||||
() => this.checkoutStatus = 'successful',
|
||||
() => {
|
||||
this.checkoutStatus = 'failed'
|
||||
this.items = savedCartItems
|
||||
}
|
||||
)
|
||||
},
|
||||
|
||||
addProductToCart (product: product, number: number) {
|
||||
this.checkoutStatus = null
|
||||
|
||||
if (product.inventory > 0) {
|
||||
const cartItem = this.items.find(item => item.id === product.id)
|
||||
|
||||
if (!cartItem) {
|
||||
this.items.push({
|
||||
id: product.id,
|
||||
quantity: number
|
||||
})
|
||||
} else {
|
||||
cartItem.quantity += number
|
||||
}
|
||||
|
||||
// remove 1 item from stock
|
||||
useProducts().decrementProductInventory(product.id, number)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
@ -1,17 +0,0 @@
|
||||
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
|
||||
},
|
||||
})
|
@ -1,95 +0,0 @@
|
||||
import shop from '../../api/shop'
|
||||
import { CART, PRODUCTS } from '../mutation-types'
|
||||
|
||||
// initial state
|
||||
// shape: [{ id, quantity }]
|
||||
const state = {
|
||||
// 购物车商品列表数据
|
||||
items: [],
|
||||
// 标记购买状态
|
||||
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, number} ) {
|
||||
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, number: number })
|
||||
} else {
|
||||
commit(CART.INCREMENT_ITEM_QUANTITY, { id: cartItem.id, number: number })
|
||||
}
|
||||
// remove 1 item from stock
|
||||
commit(`products/${PRODUCTS.DECREMENT_PRODUCT_INVENTORY}`, { id: product.id, number: number }, { root: true })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// mutations
|
||||
const mutations = {
|
||||
[CART.PUSH_PRODUCT_TO_CART] (state, { id, number }) {
|
||||
state.items.push({
|
||||
id,
|
||||
quantity: Number(number)
|
||||
})
|
||||
},
|
||||
|
||||
[CART.INCREMENT_ITEM_QUANTITY] (state, { id, number }) {
|
||||
const cartItem = state.items.find(item => item.id === id)
|
||||
cartItem.quantity += Number(number)
|
||||
},
|
||||
|
||||
[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
|
||||
}
|
@ -1,40 +0,0 @@
|
||||
import shop from '../../api/shop'
|
||||
import {PRODUCTS} from '../mutation-types'
|
||||
|
||||
// initial state
|
||||
const state = {
|
||||
// 商品列表
|
||||
all: []
|
||||
}
|
||||
|
||||
// getters
|
||||
const getters = {}
|
||||
|
||||
// actions
|
||||
const actions = {
|
||||
getAllProducts ({ commit }) {
|
||||
shop.getProducts(products => {
|
||||
commit(PRODUCTS.SET_PRODUCTS, products)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// mutations
|
||||
const mutations = {
|
||||
[PRODUCTS.SET_PRODUCTS] (state, products) {
|
||||
state.all = products
|
||||
},
|
||||
|
||||
[PRODUCTS.DECREMENT_PRODUCT_INVENTORY] (state, { id, number }) {
|
||||
const product = state.all.find(product => product.id === id)
|
||||
product.inventory -= Number(number)
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
namespaced: true,
|
||||
state,
|
||||
getters,
|
||||
actions,
|
||||
mutations
|
||||
}
|
@ -1,11 +0,0 @@
|
||||
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'
|
||||
}
|
31
src/store/products.ts
Normal file
31
src/store/products.ts
Normal file
@ -0,0 +1,31 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { shop, product } from '../api/shop'
|
||||
|
||||
/**
|
||||
* state定义
|
||||
*/
|
||||
type state = {
|
||||
all: product[]
|
||||
}
|
||||
|
||||
export const useProducts = defineStore('products', {
|
||||
state: (): state => {
|
||||
return {
|
||||
// 商品列表
|
||||
all: []
|
||||
}
|
||||
},
|
||||
|
||||
actions: {
|
||||
getAllProducts () {
|
||||
shop.getProducts(products => {
|
||||
this.all = products
|
||||
})
|
||||
},
|
||||
|
||||
decrementProductInventory (id: number, number: number) {
|
||||
const product = this.all.find(product => product.id === id) as product
|
||||
product.inventory -= number
|
||||
}
|
||||
}
|
||||
})
|
11
src/store/user.ts
Normal file
11
src/store/user.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
export const useUser = defineStore('user', {
|
||||
state: () => {
|
||||
return {
|
||||
userInfo: {
|
||||
email: "xxxxxx@qq.com"
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
@ -1,7 +0,0 @@
|
||||
@forward 'element-plus/theme-chalk/src/common/var.scss' with (
|
||||
$colors: (
|
||||
'primary': (
|
||||
'base': green,
|
||||
),
|
||||
),
|
||||
);
|
18
tsconfig.json
Normal file
18
tsconfig.json
Normal file
@ -0,0 +1,18 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "esnext",
|
||||
"useDefineForClassFields": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"strict": true,
|
||||
"jsx": "preserve",
|
||||
"sourceMap": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"esModuleInterop": true,
|
||||
"lib": ["esnext", "dom"],
|
||||
"skipLibCheck": true
|
||||
},
|
||||
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"],
|
||||
"references": [{ "path": "./tsconfig.node.json" }]
|
||||
}
|
8
tsconfig.node.json
Normal file
8
tsconfig.node.json
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node"
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
18
vite.config.ts
Normal file
18
vite.config.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import AutoImport from 'unplugin-auto-import/vite'
|
||||
import Components from 'unplugin-vue-components/vite'
|
||||
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
vue(),
|
||||
AutoImport({
|
||||
resolvers: [ElementPlusResolver()],
|
||||
}),
|
||||
Components({
|
||||
resolvers: [ElementPlusResolver()],
|
||||
}),
|
||||
]
|
||||
})
|
Loading…
Reference in New Issue
Block a user