提交 39a4b633 编写于 作者: J Jeff Fox

Chapter07

上级 115eac30
.DS_Store
node_modules
/dist
# local env files
.env.local
.env.*.local
# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
# admin-frontend
## Project setup
```
npm install
```
### Compiles and hot-reloads for development
```
npm run serve
```
### Compiles and minifies for production
```
npm run build
```
### Lints and fixes files
```
npm run lint
```
### Customize configuration
See [Configuration Reference](https://cli.vuejs.org/config/).
module.exports = {
presets: [
'@vue/cli-plugin-babel/preset'
]
}
此差异已折叠。
{
"name": "admin-frontend",
"version": "0.1.0",
"private": true,
"scripts": {
"serve": "vue-cli-service serve --port 8090",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint"
},
"dependencies": {
"core-js": "^3.6.5",
"graphql": "^15.5.0",
"graphql-request": "^3.4.0",
"vee-validate": "^4.2.1",
"vue": "^3.0.0",
"vue-router": "^4.0.4",
"yup": "^0.32.9"
},
"devDependencies": {
"@vue/cli-plugin-babel": "~4.5.0",
"@vue/cli-plugin-eslint": "~4.5.0",
"@vue/cli-service": "~4.5.0",
"@vue/compiler-sfc": "^3.0.0",
"babel-eslint": "^10.1.0",
"eslint": "^6.7.2",
"eslint-plugin-vue": "^7.0.0-0"
},
"eslintConfig": {
"root": true,
"env": {
"node": true
},
"extends": [
"plugin:vue/vue3-essential",
"eslint:recommended"
],
"parserOptions": {
"parser": "babel-eslint"
},
"rules": {}
},
"browserslist": [
"> 1%",
"last 2 versions",
"not dead"
]
}
<!DOCTYPE html>
<html lang="">
<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="<%= BASE_URL %>favicon.ico">
<title><%= htmlWebpackPlugin.options.title %></title>
</head>
<body>
<noscript>
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> 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>
<template>
<router-view></router-view>
</template>
<script>
export default {
name: "App",
};
</script>
<template>
<p>
<router-link to="/orders">Orders</router-link>
<router-link to="/shop-items">Shop Items</router-link>
<button @click="logOut">Log Out</button>
</p>
</template>
<script>
export default {
name: "TopBar",
methods: {
logOut() {
localStorage.clear();
this.$router.push("/");
},
},
};
</script>
<style scoped>
a {
margin-right: 5px;
}
</style>
import { createApp } from 'vue'
import App from './App.vue'
import router from '@/plugins/router'
const app = createApp(App)
app.use(router)
app.mount('#app')
import { createRouter, createWebHashHistory } from 'vue-router'
import Login from '@/views/Login'
import Orders from '@/views/Orders'
import ShopItems from '@/views/ShopItems'
const beforeEnter = (to, from, next) => {
try {
const token = localStorage.getItem('token')
if (to.fullPath !== '/' && !token) {
return next({ fullPath: '/' })
}
return next()
} catch (error) {
return next({ fullPath: '/' })
}
}
const routes = [
{ path: '/', component: Login },
{ path: '/orders', component: Orders, beforeEnter },
{ path: '/shop-items', component: ShopItems, beforeEnter },
]
const router = createRouter({
history: createWebHashHistory(),
routes,
})
export default router
<template>
<h1>Admin Login</h1>
<Form :validationSchema="schema" @submit="submitForm">
<div>
<label for="name">Username</label>
<br />
<Field name="username" type="text" placeholder="Username" />
<ErrorMessage name="username" />
</div>
<br />
<div>
<label for="password">Password</label>
<br />
<Field name="password" placeholder="Password" type="password" />
<ErrorMessage name="password" />
</div>
<input type="submit" />
</Form>
</template>
<script>
import { GraphQLClient, gql } from "graphql-request";
import * as yup from "yup";
import { Form, Field, ErrorMessage } from "vee-validate";
const APIURL = "http://localhost:3000/graphql";
const graphQLClient = new GraphQLClient(APIURL, {
headers: {
authorization: "",
},
});
const schema = yup.object({
name: yup.string().required(),
password: yup.string().required(),
});
export default {
name: "Login",
components: {
Form,
Field,
ErrorMessage,
},
data() {
return {
schema,
};
},
methods: {
async submitForm({ username, password }) {
const mutation = gql`
mutation login($username: String, $password: String) {
login(user: { username: $username, password: $password }) {
token
}
}
`;
const variables = {
username,
password,
};
try {
const {
login: { token },
} = await graphQLClient.request(mutation, variables);
localStorage.setItem("token", token);
this.$router.push('/orders')
} catch (error) {
alert("Login failed");
}
},
},
};
</script>
<template>
<TopBar />
<h1>Orders</h1>
<div v-for="order of orders" :key="order.order_id">
<h2>Order ID: {{ order.order_id }}</h2>
<p>Name: {{ order.name }}</p>
<p>Address: {{ order.address }}</p>
<p>Phone: {{ order.phone }}</p>
<div>
<h3>Ordered Items</h3>
<div
v-for="orderedItems of order.ordered_items"
:key="orderedItems.shop_item_id"
>
<h4>Name: {{ orderedItems.name }}</h4>
<p>Description: {{ orderedItems.description }}</p>
<p>Price: ${{ orderedItems.price }}</p>
</div>
</div>
<p>
<b>Total: ${{ calcTotal(order.ordered_items) }}</b>
</p>
<button type="button" @click="deleteOrder(order)">Delete Order</button>
</div>
</template>
<script>
import { GraphQLClient, gql } from "graphql-request";
import TopBar from '@/components/TopBar'
const APIURL = "http://localhost:3000/graphql";
const graphQLClient = new GraphQLClient(APIURL, {
headers: {
authorization: localStorage.getItem("token"),
},
});
export default {
name: "Orders",
components: {
TopBar
},
data() {
return {
orders: [],
};
},
beforeMount() {
this.getOrders();
},
methods: {
calcTotal(orderedItems) {
return orderedItems.map((o) => o.price).reduce((a, b) => a + b, 0);
},
async getOrders() {
const query = gql`
{
getOrders {
order_id
name
address
phone
ordered_items {
shop_item_id
name
description
image_url
price
}
}
}
`;
const { getOrders: data } = await graphQLClient.request(query);
this.orders = data;
},
async deleteOrder({ order_id: orderId }) {
const mutation = gql`
mutation removeOrder($orderId: Int) {
removeOrder(orderId: $orderId) {
status
}
}
`;
const variables = {
orderId,
};
await graphQLClient.request(mutation, variables);
await this.getOrders();
},
},
};
</script>
<template>
<TopBar />
<h1>Shop Items</h1>
<button @click="showDialog = true">Add Item to Shop</button>
<div v-for="shopItem of shopItems" :key="shopItem.shop_item_id">
<h2>{{ shopItem.name }}</h2>
<p>Description: {{ shopItem.description }}</p>
<p>Price: ${{ shopItem.price }}</p>
<img :src="shopItem.image_url" :alt="shopItem.name" />
<br />
<button type="button" @click="deleteItem(shopItem)">
Delete Item from Shop
</button>
</div>
<dialog :open="showDialog" class="center">
<h2>Add Item to Shop</h2>
<Form :validationSchema="schema" @submit="submitForm">
<div>
<label for="name">Name</label>
<br />
<Field name="name" type="text" placeholder="Name" />
<ErrorMessage name="name" />
</div>
<br />
<div>
<label for="description">Description</label>
<br />
<Field name="description" type="text" placeholder="Description" />
<ErrorMessage name="description" />
</div>
<br />
<div>
<label for="imageUrl">Image URL</label>
<br />
<Field name="imageUrl" type="text" placeholder="Image URL" />
<ErrorMessage name="imageUrl" />
</div>
<br />
<div>
<label for="price">Price</label>
<br />
<Field name="price" type="text" placeholder="Price" />
<ErrorMessage name="price" />
</div>
<br />
<input type="submit" />
<button @click="showDialog = false" type="button">Cancel</button>
</Form>
</dialog>
</template>
<script>
import { GraphQLClient, gql } from "graphql-request";
import * as yup from "yup";
import TopBar from "@/components/TopBar";
import { Form, Field, ErrorMessage } from "vee-validate";
const APIURL = "http://localhost:3000/graphql";
const graphQLClient = new GraphQLClient(APIURL, {
headers: {
authorization: localStorage.getItem("token"),
},
});
const schema = yup.object({
name: yup.string().required(),
description: yup.string().required(),
imageUrl: yup.string().required(),
price: yup.number().required().min(0),
});
export default {
name: "ShopItems",
components: {
Form,
Field,
ErrorMessage,
TopBar,
},
data() {
return {
shopItems: [],
showDialog: false,
schema,
};
},
beforeMount() {
this.getShopItems();
},
methods: {
async getShopItems() {
const query = gql`
{
getShopItems {
shop_item_id
name
description
image_url
price
}
}
`;
const { getShopItems: data } = await graphQLClient.request(query);
this.shopItems = data;
},
async submitForm({ name, description, imageUrl, price: oldPrice }) {
const mutation = gql`
mutation addShopItem(
$name: String
$description: String
$image_url: String
$price: Float
) {
addShopItem(
shopItem: {
name: $name
description: $description
image_url: $image_url
price: $price
}
) {
status
}
}
`;
const variables = {
name,
description,
image_url: imageUrl,
price: +oldPrice,
};
await graphQLClient.request(mutation, variables);
this.showDialog = false;
await this.getShopItems();
},
async deleteItem({ shop_item_id: shopItemId }) {
const mutation = gql`
mutation removeShopItem($shopItemId: Int) {
removeShopItem(shopItemId: $shopItemId) {
status
}
}
`;
const variables = {
shopItemId,
};
await graphQLClient.request(mutation, variables);
await this.getShopItems();
},
},
};
</script>
<style scoped>
img {
width: 100px;
}
.center {
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
min-width: 400px;
}
.center input[type="text"] {
width: 100%;
}
</style>
const createError = require('http-errors');
const express = require('express');
const path = require('path');
const cookieParser = require('cookie-parser');
const logger = require('morgan');
const { graphqlHTTP } = require('express-graphql');
const { buildSchema } = require('graphql');
const cors = require('cors')
const shopItemResolvers = require('./resolvers/shopItems')
const orderResolvers = require('./resolvers/orders')
const authResolvers = require('./resolvers/auth')
const jwt = require('jsonwebtoken');
const schema = buildSchema(`
type Response {
status: String
}
type Token {
token: String
}
input User {
username: String
password: String
token: String
}
input ShopItem {
shop_item_id: Int
name: String
description: String
image_url: String
price: Float
}
type ShopItemOutput {
shop_item_id: Int
name: String
description: String
image_url: String
price: Float
}
type OrderOutput {
order_id: Int
name: String
address: String
phone: String
ordered_items: [ShopItemOutput]
}
input Order {
order_id: Int
name: String
address: String
phone: String
ordered_items: [ShopItem]
}
type Query {
getShopItems: [ShopItemOutput],
getOrders: [OrderOutput]
}
type Mutation {
addShopItem(shopItem: ShopItem): Response
removeShopItem(shopItemId: Int): Response
addOrder(order: Order): Response
removeOrder(orderId: Int): Response
login(user: User): Token
}
`);
const root = {
...shopItemResolvers,
...orderResolvers,
...authResolvers
}
const authMiddleware = (req, res, next) => {
const { query = '' } = req.body
const token = req.get('authorization')
const requiresAuth = query.includes('removeOrder') ||
query.includes('removeShopItem') ||
query.includes('addShopItem')
if (requiresAuth) {
try {
jwt.verify(token, 'secret');
next()
return
} catch (error) {
res.status(401).json({})
return
}
}
next();
}
const app = express();
app.use(cors())
app.use(logger('dev'));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(authMiddleware)
app.use('/graphql', graphqlHTTP({
schema,
rootValue: root,
graphiql: true,
}));
// view engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'jade');
app.use(express.static(path.join(__dirname, 'public')));
// catch 404 and forward to error handler
app.use(function (req, res, next) {
next(createError(404));
});
// error handler
app.use(function (err, req, res, next) {
// set locals, only providing error in development
res.locals.message = err.message;
res.locals.error = req.app.get('env') === 'development' ? err : {};
// render the error page
res.status(err.status || 500);
res.render('error');
});
module.exports = app;
#!/usr/bin/env node
/**
* Module dependencies.
*/
var app = require('../app');
var debug = require('debug')('backend:server');
var http = require('http');
/**
* Get port from environment and store in Express.
*/
var port = normalizePort(process.env.PORT || '3000');
app.set('port', port);
/**
* Create HTTP server.
*/
var server = http.createServer(app);
/**
* Listen on provided port, on all network interfaces.
*/
server.listen(port);
server.on('error', onError);
server.on('listening', onListening);
/**
* Normalize a port into a number, string, or false.
*/
function normalizePort(val) {
var port = parseInt(val, 10);
if (isNaN(port)) {
// named pipe
return val;
}
if (port >= 0) {
// port number
return port;
}
return false;
}
/**
* Event listener for HTTP server "error" event.
*/
function onError(error) {
if (error.syscall !== 'listen') {
throw error;
}
var bind = typeof port === 'string'
? 'Pipe ' + port
: 'Port ' + port;
// handle specific listen errors with friendly messages
switch (error.code) {
case 'EACCES':
console.error(bind + ' requires elevated privileges');
process.exit(1);
break;
case 'EADDRINUSE':
console.error(bind + ' is already in use');
process.exit(1);
break;
default:
throw error;
}
}
/**
* Event listener for HTTP server "listening" event.
*/
function onListening() {
var addr = server.address();
var bind = typeof addr === 'string'
? 'pipe ' + addr
: 'port ' + addr.port;
debug('Listening on ' + bind);
}
DROP TABLE IF EXISTS order_shop_items;
DROP TABLE IF EXISTS orders;
DROP TABLE IF EXISTS shop_items;
CREATE TABLE shop_items (
shop_item_id INTEGER NOT NULL PRIMARY KEY,
name TEXT NOT NULL,
description TEXT NOT NULL,
price NUMBER NOT NULL,
image_url TEXT NOT NULL
);
CREATE TABLE orders (
order_id INTEGER NOT NULL PRIMARY KEY,
name TEXT NOT NULL,
address TEXT NOT NULL,
phone TEXT NOT NULL
);
CREATE TABLE order_shop_items (
order_id INTEGER NOT NULL,
shop_item_id INTEGER NOT NULL,
FOREIGN KEY (order_id) REFERENCES orders(order_id)
FOREIGN KEY (shop_item_id) REFERENCES shop_items(shop_item_id)
);
此差异已折叠。
{
"name": "backend",
"version": "0.0.0",
"private": true,
"scripts": {
"start": "nodemon ./bin/www"
},
"dependencies": {
"cookie-parser": "~1.4.4",
"cors": "^2.8.5",
"debug": "~2.6.9",
"express": "~4.16.1",
"express-graphql": "^0.12.0",
"graphql": "^15.5.0",
"http-errors": "~1.6.3",
"jade": "~1.11.0",
"jsonwebtoken": "^8.5.1",
"morgan": "~1.9.1",
"sqlite3": "^5.0.2"
}
}
body {
padding: 50px;
font: 14px "Lucida Grande", Helvetica, Arial, sans-serif;
}
a {
color: #00B7FF;
}
const jwt = require('jsonwebtoken');
module.exports = {
login: ({ user: { username, password } }) => {
if (username === 'admin' && password === 'password') {
return { token: jwt.sign({ username }, 'secret') }
}
throw new Error('authentication failed');
}
}
const sqlite3 = require('sqlite3').verbose();
module.exports = {
getOrders: () => {
const db = new sqlite3.Database('./db.sqlite');
return new Promise((resolve, reject) => {
db.serialize(() => {
db.all(`
SELECT *,
orders.name AS purchaser_name,
shop_items.name AS shop_item_name
FROM orders
INNER JOIN order_shop_items ON orders.order_id = order_shop_items.order_id
INNER JOIN shop_items ON order_shop_items.shop_item_id = shop_items.shop_item_id
`, [], (err, rows = []) => {
if (err) {
reject(err)
}
const orders = {}
for (const row of rows) {
const {
order_id,
purchaser_name: name,
address,
phone
} = row
orders[order_id] = {
order_id,
name,
address,
phone
}
}
const orderArr = Object.values(orders)
for (const order of orderArr) {
order.ordered_items = rows
.filter(({ order_id }) => order_id === order.order_id)
.map(({ shop_item_id, shop_item_name: name, price, description }) => ({
shop_item_id, name, price, description
}))
}
resolve(orderArr)
});
})
db.close();
})
},
addOrder: ({ order: { name, address, phone, ordered_items: orderedItems } }) => {
const db = new sqlite3.Database('./db.sqlite');
return new Promise((resolve) => {
db.serialize(() => {
const orderStmt = db.prepare(`
INSERT INTO orders (
name,
address,
phone
) VALUES (?, ?, ?)
`);
orderStmt.run(name, address, phone)
orderStmt.finalize();
db.all(`
SELECT last_insert_rowid() AS order_id
`,
[],
(err, rows = []) => {
const [{ order_id: orderId }] = rows
db.serialize(() => {
const orderShopItemStmt = db.prepare(`
INSERT INTO order_shop_items (
order_id,
shop_item_id
) VALUES (?, ?)
`);
for (const orderItem of orderedItems) {
const {
shop_item_id: shopItemId
} = orderItem
orderShopItemStmt.run(orderId, shopItemId)
}
orderShopItemStmt.finalize()
})
resolve({ status: 'success' })
db.close();
});
})
})
},
removeOrder: ({ orderId }) => {
const db = new sqlite3.Database('./db.sqlite');
return new Promise((resolve) => {
db.serialize(() => {
const delOrderShopItemsStmt = db.prepare("DELETE FROM order_shop_items WHERE order_id = (?)");
delOrderShopItemsStmt.run(orderId)
delOrderShopItemsStmt.finalize();
const delOrderStmt = db.prepare("DELETE FROM orders WHERE order_id = (?)");
delOrderStmt.run(orderId)
delOrderStmt.finalize();
resolve({ status: 'success' })
})
db.close();
})
},
}
const sqlite3 = require('sqlite3').verbose();
module.exports = {
getShopItems: () => {
const db = new sqlite3.Database('./db.sqlite');
return new Promise((resolve, reject) => {
db.serialize(() => {
db.all("SELECT * FROM shop_items", [], (err, rows = []) => {
if (err) {
reject(err)
}
resolve(rows)
});
})
db.close();
})
},
addShopItem: ({ shopItem: { name, description, image_url: imageUrl, price } }) => {
const db = new sqlite3.Database('./db.sqlite');
return new Promise((resolve) => {
db.serialize(() => {
const stmt = db.prepare(`
INSERT INTO shop_items (
name,
description,
image_url,
price
) VALUES (?, ?, ?, ?)
`
);
stmt.run(name, description, imageUrl, price)
stmt.finalize();
resolve({ status: 'success' })
})
db.close();
})
},
removeShopItem: ({ shopItemId }) => {
const db = new sqlite3.Database('./db.sqlite');
return new Promise((resolve) => {
db.serialize(() => {
const stmt = db.prepare("DELETE FROM shop_items WHERE shop_item_id = (?)");
stmt.run(shopItemId)
stmt.finalize();
resolve({ status: 'success' })
})
db.close();
})
},
}
var express = require('express');
var router = express.Router();
/* GET home page. */
router.get('/', function(req, res, next) {
res.render('index', { title: 'Express' });
});
module.exports = router;
var express = require('express');
var router = express.Router();
/* GET users listing. */
router.get('/', function(req, res, next) {
res.send('respond with a resource');
});
module.exports = router;
extends layout
block content
h1= message
h2= error.status
pre #{error.stack}
extends layout
block content
h1= title
p Welcome to #{title}
doctype html
html
head
title= title
link(rel='stylesheet', href='/stylesheets/style.css')
body
block content
.DS_Store
node_modules
/dist
# local env files
.env.local
.env.*.local
# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
# frontend
## Project setup
```
npm install
```
### Compiles and hot-reloads for development
```
npm run serve
```
### Compiles and minifies for production
```
npm run build
```
### Lints and fixes files
```
npm run lint
```
### Customize configuration
See [Configuration Reference](https://cli.vuejs.org/config/).
module.exports = {
presets: [
'@vue/cli-plugin-babel/preset'
]
}
此差异已折叠。
{
"name": "frontend",
"version": "0.1.0",
"private": true,
"scripts": {
"serve": "vue-cli-service serve --port 8091",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint"
},
"dependencies": {
"core-js": "^3.6.5",
"graphql": "^15.5.0",
"graphql-request": "^3.4.0",
"vee-validate": "^4.2.1",
"vue": "^3.0.0",
"vue-router": "^4.0.4",
"vuex": "^4.0.0",
"vuex-persistedstate": "^4.0.0-beta.3",
"yup": "^0.32.9"
},
"devDependencies": {
"@vue/cli-plugin-babel": "~4.5.0",
"@vue/cli-plugin-eslint": "~4.5.0",
"@vue/cli-service": "~4.5.0",
"@vue/compiler-sfc": "^3.0.0",
"babel-eslint": "^10.1.0",
"eslint": "^6.7.2",
"eslint-plugin-vue": "^7.0.0-0"
},
"eslintConfig": {
"root": true,
"env": {
"node": true
},
"extends": [
"plugin:vue/vue3-essential",
"eslint:recommended"
],
"parserOptions": {
"parser": "babel-eslint"
},
"rules": {}
},
"browserslist": [
"> 1%",
"last 2 versions",
"not dead"
]
}
<!DOCTYPE html>
<html lang="">
<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="<%= BASE_URL %>favicon.ico">
<title><%= htmlWebpackPlugin.options.title %></title>
</head>
<body>
<noscript>
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> 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>
<template>
<router-view></router-view>
</template>
<script>
export default {
name: "App",
};
</script>
<template>
<div class="hello">
<h1>{{ msg }}</h1>
<p>
For a guide and recipes on how to configure / customize this project,<br>
check out the
<a href="https://cli.vuejs.org" target="_blank" rel="noopener">vue-cli documentation</a>.
</p>
<h3>Installed CLI Plugins</h3>
<ul>
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-babel" target="_blank" rel="noopener">babel</a></li>
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-eslint" target="_blank" rel="noopener">eslint</a></li>
</ul>
<h3>Essential Links</h3>
<ul>
<li><a href="https://vuejs.org" target="_blank" rel="noopener">Core Docs</a></li>
<li><a href="https://forum.vuejs.org" target="_blank" rel="noopener">Forum</a></li>
<li><a href="https://chat.vuejs.org" target="_blank" rel="noopener">Community Chat</a></li>
<li><a href="https://twitter.com/vuejs" target="_blank" rel="noopener">Twitter</a></li>
<li><a href="https://news.vuejs.org" target="_blank" rel="noopener">News</a></li>
</ul>
<h3>Ecosystem</h3>
<ul>
<li><a href="https://router.vuejs.org" target="_blank" rel="noopener">vue-router</a></li>
<li><a href="https://vuex.vuejs.org" target="_blank" rel="noopener">vuex</a></li>
<li><a href="https://github.com/vuejs/vue-devtools#vue-devtools" target="_blank" rel="noopener">vue-devtools</a></li>
<li><a href="https://vue-loader.vuejs.org" target="_blank" rel="noopener">vue-loader</a></li>
<li><a href="https://github.com/vuejs/awesome-vue" target="_blank" rel="noopener">awesome-vue</a></li>
</ul>
</div>
</template>
<script>
export default {
name: 'HelloWorld',
props: {
msg: String
}
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
h3 {
margin: 40px 0 0;
}
ul {
list-style-type: none;
padding: 0;
}
li {
display: inline-block;
margin: 0 10px;
}
a {
color: #42b983;
}
</style>
import { createApp } from 'vue'
import App from './App.vue'
import router from '@/plugins/router'
import store from '@/plugins/vuex'
const app = createApp(App)
app.use(router)
app.use(store)
app.mount('#app')
import { createRouter, createWebHashHistory } from 'vue-router'
import Shop from '@/views/Shop'
import OrderForm from '@/views/OrderForm'
import Success from '@/views/Success'
const routes = [
{ path: '/', component: Shop },
{ path: '/order-form', component: OrderForm },
{ path: '/success', component: Success },
]
const router = createRouter({
history: createWebHashHistory(),
routes,
})
export default router
import { createStore } from "vuex";
import createPersistedState from "vuex-persistedstate";
const store = createStore({
state() {
return {
cartItems: []
}
},
getters: {
cartItemsAdded(state) {
return state.cartItems
}
},
mutations: {
addCartItem(state, cartItem) {
const cartItemIds = state.cartItems.map(c => c.cartItemId).filter(id => typeof id === 'number')
state.cartItems.push({
cartItemId: cartItemIds.length > 0 ? Math.max(...cartItemIds) : 1,
...cartItem
})
},
removeCartItem(state, index) {
state.cartItems.splice(index, 1)
},
clearCart(state) {
state.cartItems = []
}
},
plugins: [createPersistedState({
key: 'cart'
})],
});
export default store
<template>
<h1>Order Form</h1>
<div v-for="(cartItem, index) of cartItemsAdded" :key="cartItem.cartItemId">
<h2>{{ cartItem.name }}</h2>
<p>Description: {{ cartItem.description }}</p>
<p>Price: ${{ cartItem.price }}</p>
<br />
<button type="button" @click="removeCartItem(index)">
Remove From Cart
</button>
</div>
<Form :validationSchema="schema" @submit="submitOrder">
<div>
<label for="name">Name</label>
<br />
<Field name="name" type="text" placeholder="Name" />
<ErrorMessage name="name" />
</div>
<br />
<div>
<label for="phone">Phone</label>
<br />
<Field name="phone" type="text" placeholder="Phone" />
<ErrorMessage name="phone" />
</div>
<br />
<div>
<label for="address">Address</label>
<br />
<Field name="address" type="text" placeholder="Address" />
<ErrorMessage name="address" />
</div>
<br />
<input type="submit" />
</Form>
</template>
<script>
import { GraphQLClient, gql } from "graphql-request";
import { mapMutations, mapGetters } from "vuex";
import { Form, Field, ErrorMessage } from "vee-validate";
import * as yup from "yup";
const APIURL = "http://localhost:3000/graphql";
const graphQLClient = new GraphQLClient(APIURL);
const schema = yup.object({
name: yup.string().required(),
phone: yup.string().required(),
address: yup.string().required(),
});
export default {
name: "OrderForm",
data() {
return {
schema,
};
},
components: {
Form,
Field,
ErrorMessage,
},
computed: {
...mapGetters(["cartItemsAdded"]),
},
methods: {
async submitOrder({ name, phone, address }) {
const mutation = gql`
mutation addOrder(
$name: String
$phone: String
$address: String
$ordered_items: [ShopItem]
) {
addOrder(
order: {
name: $name
phone: $phone
address: $address
ordered_items: $ordered_items
}
) {
status
}
}
`;
const variables = {
name,
phone,
address,
ordered_items: this.cartItemsAdded.map(
({ shop_item_id, name, description, image_url, price }) => ({
shop_item_id,
name,
description,
image_url,
price,
})
),
};
await graphQLClient.request(mutation, variables);
this.clearCart();
this.$router.push("/success");
},
...mapMutations(["addCartItem", "removeCartItem", "clearCart"]),
},
};
</script>
<template>
<h1>Shop</h1>
<div>
<router-link to="/order-form">Check Out</router-link>
</div>
<button type="button" @click="clearCart()">Clear Shopping Cart</button>
<p>{{ cartItemsAdded.length }} item(s) added to cart.</p>
<div v-for="shopItem of shopItems" :key="shopItem.shop_item_id">
<h2>{{ shopItem.name }}</h2>
<p>Description: {{ shopItem.description }}</p>
<p>Price: ${{ shopItem.price }}</p>
<img :src="shopItem.image_url" :alt="shopItem.name" />
<br />
<button type="button" @click="addCartItem(shopItem)">Add to Cart</button>
</div>
</template>
<script>
import { GraphQLClient, gql } from "graphql-request";
import { mapMutations, mapGetters } from "vuex";
const APIURL = "http://localhost:3000/graphql";
const graphQLClient = new GraphQLClient(APIURL);
export default {
name: "Shop",
data() {
return {
shopItems: [],
};
},
beforeMount() {
this.getShopItems();
},
computed: {
...mapGetters(["cartItemsAdded"]),
},
methods: {
async getShopItems() {
const query = gql`
{
getShopItems {
shop_item_id
name
description
image_url
price
}
}
`;
const { getShopItems: data } = await graphQLClient.request(query);
this.shopItems = data;
},
...mapMutations(["addCartItem", "clearCart"]),
},
};
</script>
<style scoped>
img {
width: 100px;
}
</style>
<template>
<div>
<h1>Order Successful</h1>
<router-link to="/">Go Back to Shop</router-link>
</div>
</template>
<script>
export default {
name: "Success",
};
</script>
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册