...
 
Commits (2)
    https://gitcode.net/u012663988/my-vue-app/-/commit/4915c8f013ae462cb00b0052ec0f4a86458f25aa Chapter 2 2021-09-01T18:19:21+08:00 Jeff Fox luck7@live.cn https://gitcode.net/u012663988/my-vue-app/-/commit/fcbfd5621dc7e570a0e84d9fb1d9a70a37315742 Chapter 3 2021-09-01T18:19:44+08:00 Jeff Fox luck7@live.cn
.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?
# vue-example-ch2-github-app-vue-cli
## 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": "vue-example-ch2-github-app-vue-cli",
"version": "0.1.0",
"private": true,
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint"
},
"dependencies": {
"core-js": "^3.6.5",
"register-service-worker": "^1.7.1",
"vue": "^3.0.0-0"
},
"devDependencies": {
"@vue/cli-plugin-babel": "~4.5.0",
"@vue/cli-plugin-eslint": "~4.5.0",
"@vue/cli-plugin-pwa": "^4.5.6",
"@vue/cli-service": "~4.5.0",
"@vue/compiler-sfc": "^3.0.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"
]
}
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8.00251 14.9297L0 1.07422H6.14651L8.00251 4.27503L9.84583 1.07422H16L8.00251 14.9297Z" fill="black"/>
</svg>
<!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="<%= BASE_URL %>favicon.ico">
<title><%= htmlWebpackPlugin.options.title %></title>
<script src="<%= BASE_URL %>octokit-rest.min.js"></script>
</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>
因为 它太大了无法显示 source diff 。你可以改为 查看blob
<template>
<div>
<h1>Github App</h1>
<GitHubTokenForm />
<User />
<Repos />
</div>
</template>
<script>
import GitHubTokenForm from "./components/GitHubTokenForm.vue";
import Repos from "./components/Repos.vue";
import User from "./components/User.vue";
export default {
name: "App",
components: {
GitHubTokenForm,
Repos,
User,
},
};
</script>
<template>
<form @submit.prevent="saveToken">
<div>
<label for="githubToken">Github Token</label>
<br />
<input id="githubToken" v-model="githubToken" />
</div>
<div>
<input type="submit" value="Save token" />
<button type="button" @click="clearToken">Clear token</button>
</div>
</form>
</template>
<script>
export default {
name: "GitHubTokenForm",
data() {
return {
githubToken: "",
};
},
beforeMount() {
this.githubToken = localStorage.getItem("github-token");
},
methods: {
saveToken() {
localStorage.setItem("github-token", this.githubToken);
},
clearToken() {
localStorage.clear();
},
},
};
</script>
<template>
<div>
<h1>Repos</h1>
<div v-for="r of repos" :key="r.id">
<h2>{{r.owner.login}}/{{r.name}}</h2>
<Issues :owner="r.owner.login" :repo="r.name" />
</div>
</div>
</template>
<script>
import Issues from "./repo/Issues.vue";
import { octokitMixin } from "../mixins/octokitMixin";
export default {
name: "Repos",
components: {
Issues,
},
data() {
return {
repos: [],
};
},
mixins: [octokitMixin],
async mounted() {
const octokit = this.createOctokitClient();
const { data: repos } = await octokit.request("/user/repos");
this.repos = repos;
},
};
</script>
<template>
<div>
<h1>User Info</h1>
<ul>
<li>
<img :src="userData.avatar_url" id="avatar" />
</li>
<li>username: {{userData.login}}</li>
<li>followers: {{userData.followers}}</li>
<li>plan: {{userData.pla && userData.plan.name}}</li>
</ul>
</div>
</template>
<script>
import { octokitMixin } from "../mixins/octokitMixin";
export default {
name: "User",
data() {
return {
userData: {},
};
},
mixins: [octokitMixin],
async mounted() {
const octokit = this.createOctokitClient();
const { data: userData } = await octokit.request("/user");
this.userData = userData;
},
methods: {
saveToken() {},
},
};
</script>
<style scoped>
#avatar {
width: 50px;
height: 50px;
}
</style>
<template>
<div v-if="issues.length > 0">
<button @click="showIssues = !showIssues">{{showIssues ? 'Hide' : 'Show'}} issues</button>
<div v-if="showIssues">
<div v-for="i of issues" :key="i.id">
<h3>{{i.title}}</h3>
<a :href="i.url">Go to issue</a>
<IssueComments :owner="owner" :repo="repo" :issueNumber="i.number" />
</div>
</div>
</div>
</template>
<script>
import { octokitMixin } from "../../mixins/octokitMixin";
import IssueComments from "./issue/Comments.vue";
export default {
name: "RepoIssues",
components: {
IssueComments,
},
props: {
owner: {
type: String,
required: true,
},
repo: {
type: String,
required: true,
},
},
mixins: [octokitMixin],
data() {
return {
issues: [],
showIssues: false,
};
},
methods: {
async getRepoIssues(owner, repo) {
if (typeof owner !== "string" || typeof repo !== "string") {
return;
}
const octokit = this.createOctokitClient();
const { data: issues } = await octokit.issues.listForRepo({
owner,
repo,
});
this.issues = issues;
},
},
watch: {
owner: {
immediate: true,
handler(val) {
this.getRepoIssues(val, this.repo);
},
},
repo: {
immediate: true,
handler(val) {
this.getRepoIssues(this.issues, val);
},
},
},
};
</script>
<template>
<div>
<div v-if="comments.length > 0">
<h4>Comments</h4>
<div v-for="c of comments" :key="c.id">{{c.user && c.user.login}} - {{c.body}}</div>
</div>
</div>
</template>
<script>
import { octokitMixin } from "../../../mixins/octokitMixin";
export default {
name: "IssueComments",
props: {
owner: {
type: String,
required: true,
},
repo: {
type: String,
required: true,
},
issueNumber: {
type: Number,
required: true,
},
},
data() {
return {
comments: [],
};
},
mixins: [octokitMixin],
methods: {
async getIssueComments(owner, repo, issueNumber) {
if (
typeof owner !== "string" ||
typeof repo !== "string" ||
typeof issueNumber !== "number"
) {
return;
}
const octokit = this.createOctokitClient();
const { data: comments } = await octokit.issues.listComments({
owner,
repo,
issue_number: issueNumber,
});
this.comments = comments;
},
},
watch: {
owner: {
immediate: true,
handler(val) {
this.getIssueComments(val, this.repo, this.issueNumber);
},
},
repo: {
immediate: true,
handler(val) {
this.getIssueComments(this.owner, val, this.issueNumber);
},
},
issueNumber: {
immediate: true,
handler(val) {
this.getIssueComments(this.owner, this.repo, val);
},
},
},
};
</script>
import { createApp } from 'vue'
import App from './App.vue'
import './registerServiceWorker'
createApp(App).mount('#app')
export const octokitMixin = {
methods: {
createOctokitClient() {
return new window.Octokit({
auth: localStorage.getItem("github-token"),
});
}
}
}
/* eslint-disable no-console */
import { register } from 'register-service-worker'
if (process.env.NODE_ENV === 'production') {
register(`${process.env.BASE_URL}service-worker.js`, {
ready () {
console.log(
'App is being served from cache by a service worker.\n' +
'For more details, visit https://goo.gl/AFskqB'
)
},
registered () {
console.log('Service worker has been registered.')
},
cached () {
console.log('Content has been cached for offline use.')
},
updatefound () {
console.log('New content is downloading.')
},
updated () {
console.log('New content is available; please refresh.')
},
offline () {
console.log('No internet connection found. App is running in offline mode.')
},
error (error) {
console.error('Error during service worker registration:', error)
}
})
}
> 1%
last 2 versions
not dead
module.exports = {
root: true,
env: {
node: true
},
'extends': [
'plugin:vue/vue3-essential',
'eslint:recommended'
],
parserOptions: {
parser: 'babel-eslint'
},
rules: {
'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off'
},
overrides: [
{
files: [
'**/__tests__/*.{j,t}s?(x)',
'**/tests/unit/**/*.spec.{j,t}s?(x)'
],
env: {
jest: true
}
}
]
}
.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?
# vue-example-ch3-slider-puzzle
## Project setup
```
npm install
```
### Compiles and hot-reloads for development
```
npm run serve
```
### Compiles and minifies for production
```
npm run build
```
### Run your unit tests
```
npm run test:unit
```
### 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'
]
}
module.exports = {
preset: '@vue/cli-plugin-unit-jest',
transform: {
'^.+\\.vue$': 'vue-jest'
}
}
此差异已折叠。
{
"name": "vue-example-ch3-slider-puzzle",
"version": "0.1.0",
"private": true,
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"test:unit": "vue-cli-service test:unit",
"lint": "vue-cli-service lint"
},
"dependencies": {
"core-js": "^3.6.5",
"lodash": "^4.17.20",
"moment": "^2.28.0",
"vue": "^3.0.0-0"
},
"devDependencies": {
"@vue/cli-plugin-babel": "~4.5.0",
"@vue/cli-plugin-eslint": "~4.5.0",
"@vue/cli-plugin-unit-jest": "~4.5.0",
"@vue/cli-service": "~4.5.0",
"@vue/compiler-sfc": "^3.0.0-0",
"@vue/test-utils": "^2.0.0-0",
"babel-eslint": "^10.1.0",
"eslint": "^6.7.2",
"eslint-plugin-vue": "^7.0.0-0",
"jest-localstorage-mock": "^2.4.3",
"typescript": "~3.9.3",
"vue-jest": "^5.0.0-0"
},
"jest": {
"setupFiles": [
"jest-localstorage-mock"
]
}
}
<!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="<%= 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>
<div>
<Puzzles @puzzle-changed="selectedPuzzleId = $event" />
<Records />
<SliderPuzzle :puzzleId="selectedPuzzleId" />
</div>
</template>
<script>
import SliderPuzzle from "./components/SliderPuzzle.vue";
import Puzzles from "./components/Puzzles.vue";
import Records from "./components/Records.vue";
export default {
name: "App",
components: {
SliderPuzzle,
Puzzles,
Records,
},
data() {
return {
selectedPuzzleId: "cut-pink",
};
},
};
</script>
<template>
<div>
<h1>Select a Puzzle</h1>
<div v-for="p of puzzles" :key="p.id" class="row">
<div>
<img :src="require(`../assets/${p.image}`)" />
</div>
<div>
<h2>{{p.title}}</h2>
</div>
<div class="play-button">
<button @click="selectPuzzle(p)">Play</button>
</div>
</div>
</div>
</template>
<script>
export default {
data() {
return {
puzzles: [
{ id: 'cut-pink', image: "pink.jpg", title: "Pink Flower" },
{ id: 'cut-purple', image: "purple.jpg", title: "Purple Flower" },
{ id: 'cut-red', image: "red.jpg", title: "Red Flower" },
],
};
},
methods: {
selectPuzzle(puzzle){
this.$emit('puzzle-changed', puzzle.id);
}
}
};
</script>
<style scoped>
.row {
display: flex;
max-width: 90vw;
flex-wrap: wrap;
justify-content: space-between;
}
.row img {
width: 100px;
}
.row .play-button {
padding-top: 25px;
}
</style>
<template>
<div>
<h1>Records</h1>
<button @click="getRecords">Refresh</button>
<div v-for="(r, index) of records" :key="index">{{index + 1}} - {{r.elapsedTime}}</div>
</div>
</template>
<script>
export default {
data() {
return {
records: [],
};
},
created() {
this.getRecords();
},
methods: {
getRecords() {
const records = JSON.parse(localStorage.getItem("records")) || [];
this.records = records;
},
},
};
</script>
<template>
<div>
<h1>Swap the Images to Win</h1>
<button @click="start" id="start-button">Start Game</button>
<button @click="stop" id="quit-button">Quit</button>
<p>Elapsed Time: {{ elapsedTime }}</p>
<p v-if="isWinning">You win</p>
<div class="row">
<div
class="column"
v-for="(s, index) of shuffledPuzzleArray"
:key="s"
@click="swap(index)"
>
<img :src="require(`../assets/${puzzleId}/${s}`)" />
</div>
</div>
</div>
</template>
<script>
import moment from "moment";
const correctPuzzleArray = [
"image_part_001.jpg",
"image_part_002.jpg",
"image_part_003.jpg",
"image_part_004.jpg",
"image_part_005.jpg",
"image_part_006.jpg",
"image_part_007.jpg",
"image_part_008.jpg",
"image_part_009.jpg",
];
export default {
name: "SliderPuzzle",
props: {
puzzleId: {
type: String,
default: "cut-pink",
},
},
data() {
return {
correctPuzzleArray,
shuffledPuzzleArray: [...correctPuzzleArray].sort(
() => Math.random() - 0.5
),
indexesToSwap: [],
timer: undefined,
startDateTime: new Date(),
currentDateTime: new Date(),
};
},
computed: {
isWinning() {
for (let i = 0; i < correctPuzzleArray.length; i++) {
if (correctPuzzleArray[i] !== this.shuffledPuzzleArray[i]) {
return false;
}
}
return true;
},
elapsedDiff() {
const currentDateTime = moment(this.currentDateTime);
const startDateTime = moment(this.startDateTime);
return currentDateTime.diff(startDateTime);
},
elapsedTime() {
return moment.utc(this.elapsedDiff).format("HH:mm:ss");
},
},
methods: {
swap(index) {
if (!this.timer) {
return;
}
if (this.indexesToSwap.length < 2) {
this.indexesToSwap.push(index);
}
if (this.indexesToSwap.length === 2) {
const [index1, index2] = this.indexesToSwap;
[this.shuffledPuzzleArray[index1], this.shuffledPuzzleArray[index2]] = [
this.shuffledPuzzleArray[index2],
this.shuffledPuzzleArray[index1],
];
this.indexesToSwap = [];
}
},
start() {
this.resetTime();
this.shuffledPuzzleArray = [...correctPuzzleArray].sort(
() => Math.random() - 0.5
);
this.indexesToSwap = [];
this.timer = setInterval(() => {
this.currentDateTime = new Date();
if (this.isWinning) {
this.recordSpeedRecords();
this.stop();
}
}, 1000);
},
stop() {
this.resetTime();
clearInterval(this.timer);
},
resetTime() {
this.startDateTime = new Date();
this.currentDateTime = new Date();
},
recordSpeedRecords() {
let records = JSON.parse(localStorage.getItem("records")) || [];
const { elapsedTime, elapsedDiff } = this;
records.push({ elapsedTime, elapsedDiff });
const sortedRecords = records
.sort((a, b) => a.elapsedDiff - b.elapsedDiff)
.slice(0, 10);
const stringifiedRecords = JSON.stringify(sortedRecords);
localStorage.setItem("records", stringifiedRecords);
},
},
};
</script>
<style scoped>
.row {
display: flex;
max-width: 90vw;
flex-wrap: wrap;
}
.column {
flex-grow: 1;
width: 33%;
}
.column img {
max-width: 100%;
}
</style>
import { createApp } from 'vue'
import App from './App.vue'
createApp(App).mount('#app')
import { mount } from '@vue/test-utils'
import Puzzles from '@/components/Puzzles.vue'
describe('Puzzles.vue', () => {
it('emit puzzled-changed event when Play button is clicked', () => {
const wrapper = mount(Puzzles)
wrapper.find('.play-button button').trigger('click');
expect(wrapper.emitted()).toHaveProperty('puzzle-changed');
})
it('emit puzzled-changed event with the puzzle ID when Play button is clicked', () => {
const wrapper = mount(Puzzles)
wrapper.find('.play-button button').trigger('click');
const puzzleChanged = wrapper.emitted('puzzle-changed');
expect(puzzleChanged[0]).toEqual([wrapper.vm.puzzles[0].id]);
})
})
import { shallowMount } from '@vue/test-utils'
import 'jest-localstorage-mock';
import Records from '@/components/Records.vue'
describe('Records.vue', () => {
it('gets records from local storage', () => {
shallowMount(Records, {})
expect(localStorage.getItem).toHaveBeenCalledWith('records');
})
})
import { mount } from '@vue/test-utils'
import SliderPuzzle from '@/components/SliderPuzzle.vue'
import 'jest-localstorage-mock';
jest.useFakeTimers();
describe('SliderPuzzle.vue', () => {
it('inserts the index of the image to swap when we click on an image', () => {
const wrapper = mount(SliderPuzzle)
wrapper.find('#start-button').trigger('click')
wrapper.find('img').trigger('click');
expect(wrapper.vm.indexesToSwap.length).toBeGreaterThan(0);
})
it('swaps the image order when 2 images are clicked', () => {
const wrapper = mount(SliderPuzzle)
wrapper.find('#start-button').trigger('click')
const [firstImage, secondImage] = wrapper.vm.shuffledPuzzleArray;
wrapper.get('.column:nth-child(1) img').trigger('click');
wrapper.get('.column:nth-child(2) img').trigger('click');
expect(wrapper.vm.indexesToSwap.length).toBe(0);
const [newFirstImage, newSecondImage] = wrapper.vm.shuffledPuzzleArray;
expect(firstImage).toBe(newSecondImage);
expect(secondImage).toBe(newFirstImage);
})
it('starts timer when start method is called', () => {
const wrapper = mount(SliderPuzzle);
wrapper.vm.start();
expect(setInterval).toHaveBeenCalledTimes(1);
expect(setInterval).toHaveBeenLastCalledWith(expect.any(Function), 1000);
})
it('stops timer when stop method is called', () => {
const wrapper = mount(SliderPuzzle);
wrapper.vm.stop();
expect(clearInterval).toHaveBeenCalledTimes(1);
})
it('records record to local storage', () => {
const wrapper = mount(SliderPuzzle, {
data() {
return {
currentDateTime: new Date(),
startDateTime: new Date()
}
}
})
wrapper.vm.recordSpeedRecords();
const { elapsedDiff, elapsedTime } = wrapper.vm;
const stringifiedRecords = JSON.stringify([{ elapsedTime, elapsedDiff }])
expect(localStorage.setItem).toHaveBeenCalledWith('records', stringifiedRecords);
})
it('starts timer with Start button is clicked', () => {
const wrapper = mount(SliderPuzzle);
wrapper.get('#start-button').trigger('click');
expect(setInterval).toHaveBeenCalledTimes(1);
})
it('stops timer with Quit button is clicked', () => {
const wrapper = mount(SliderPuzzle);
wrapper.get('#quit-button').trigger('click');
expect(clearInterval).toHaveBeenCalledTimes(1);
})
it('shows the elapsed time', () => {
const wrapper = mount(SliderPuzzle, {
data() {
return {
currentDateTime: new Date(2020, 0, 1, 0, 0, 1),
startDateTime: new Date(2020, 0, 1, 0, 0, 0),
}
}
});
expect(wrapper.html()).toContain('00:00:01')
})
afterEach(() => {
jest.clearAllMocks();
});
})