Skip to content

Commit e931344

Browse files
authored
Print by QR code (#19)
- QRPrint button - Scanner - Logic
1 parent 1875d38 commit e931344

9 files changed

Lines changed: 338 additions & 14 deletions

File tree

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
"register-service-worker": "^1.7.1",
1919
"vue": "^3.0.0",
2020
"vue-router": "^4.0.0-0",
21+
"vue3-qrcode-reader": "^0.0.1",
2122
"vuex": "^4.0.0-0",
2223
"yarn": "^1.22.19"
2324
},

src/components/TaskCompleteDialog.vue

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,14 @@
2626
</sup>
2727
</div>
2828
<div class="form-actions">
29+
<router-link
30+
class="btn btn-success btn-lg"
31+
type="button"
32+
:to="`/qr#${pin}`"
33+
v-if="isMobile"
34+
>
35+
Распечатать по QR
36+
</router-link>
2937
<button
3038
class="btn btn-primary btn-lg"
3139
type="button"
@@ -38,7 +46,14 @@
3846
</template>
3947

4048
<script>
49+
import { isMobile } from "@/utils/mobile";
50+
4151
export default {
52+
computed: {
53+
isMobile() {
54+
return isMobile();
55+
},
56+
},
4257
props: {
4358
status: {
4459
type: String,

src/router/index.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,11 @@ const routes = [
1313
component: () =>
1414
import(/* webpackChunkName: "history" */ "@/views/History.vue"),
1515
},
16+
{
17+
path: "/qr",
18+
name: "QR Print",
19+
component: () => import(/* webpackChunkName: "qr" */ "@/views/QrPrint.vue"),
20+
},
1621
];
1722

1823
const router = createRouter({

src/utils/marketing.js

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,78 @@ export function log_print(status, pin) {
5757
}
5858
}
5959

60+
export function log_open_qr(status, pins) {
61+
const marketing_id = get_marketing_id();
62+
if (!marketing_id) return;
63+
64+
try {
65+
fetch(`${process.env.VUE_APP_API_MARKETING}/action`, {
66+
method: "POST",
67+
cache: "no-cache",
68+
redirect: "follow",
69+
headers: { "Content-Type": "application/json" },
70+
body: JSON.stringify({
71+
user_id: marketing_id,
72+
action: "printer webapp open qr",
73+
path_from: document.location.href,
74+
path_to: `${process.env.VUE_APP_API_PRINTER}/qr`,
75+
additional_data: JSON.stringify({ status: status, pins: pins }),
76+
}),
77+
});
78+
} catch {
79+
//Failed, skip silent
80+
}
81+
}
82+
83+
export function log_print_qr(pins) {
84+
const marketing_id = get_marketing_id();
85+
if (!marketing_id) return;
86+
87+
try {
88+
fetch(`${process.env.VUE_APP_API_MARKETING}/action`, {
89+
method: "POST",
90+
cache: "no-cache",
91+
redirect: "follow",
92+
headers: { "Content-Type": "application/json" },
93+
body: JSON.stringify({
94+
user_id: marketing_id,
95+
action: "printer webapp print qr",
96+
path_from: document.location.href,
97+
path_to: `${process.env.VUE_APP_API_PRINTER}/qr`,
98+
additional_data: JSON.stringify({ pins: pins }),
99+
}),
100+
});
101+
} catch {
102+
//Failed, skip silent
103+
}
104+
}
105+
106+
export function log_error_qr(pins, error) {
107+
const marketing_id = get_marketing_id();
108+
if (!marketing_id) return;
109+
110+
try {
111+
fetch(`${process.env.VUE_APP_API_MARKETING}/action`, {
112+
method: "POST",
113+
cache: "no-cache",
114+
redirect: "follow",
115+
headers: { "Content-Type": "application/json" },
116+
body: JSON.stringify({
117+
user_id: marketing_id,
118+
action: "printer webapp print qr error",
119+
path_from: document.location.href,
120+
path_to: `${process.env.VUE_APP_API_PRINTER}/qr`,
121+
additional_data: JSON.stringify({
122+
pins: pins,
123+
error: error,
124+
}),
125+
}),
126+
});
127+
} catch {
128+
//Failed, skip silent
129+
}
130+
}
131+
60132
export function log_open_history() {
61133
const marketing_id = get_marketing_id();
62134
if (!marketing_id) return;

src/utils/mobile.js

Lines changed: 6 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/views/History.vue

Lines changed: 41 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,32 @@
11
<template>
22
<div>
3-
<div>
4-
<h1 class="accordion-header" id="histAccordion-headingOne">
5-
Последние файлы
6-
</h1>
7-
<small><router-link to="/">← Вернуться к печати</router-link></small>
8-
<ul class="hist" v-if="hist.length > 0">
9-
<li v-for="item in hist" v-bind:key="item.pin" class="hist-item">
10-
<code>{{ item.pin }}</code> <small>{{ item.name }}</small>
11-
</li>
12-
</ul>
13-
<p v-else>Вы еще ничего не печатали</p>
14-
</div>
3+
<h1 class="accordion-header" id="histAccordion-headingOne">
4+
Последние файлы
5+
</h1>
6+
<small><router-link to="/">← Вернуться к печати</router-link></small>
7+
<ul class="hist" v-if="hist.length > 0">
8+
<li v-for="item in hist" v-bind:key="item.pin" class="hist-item">
9+
<code>{{ item.pin }}</code>
10+
<small>{{ item.name }}</small>
11+
<router-link
12+
class="material-icons"
13+
type="button"
14+
:to="`/qr#${item.pin}`"
15+
v-if="isMobile"
16+
>
17+
qr_code_scanner
18+
</router-link>
19+
</li>
20+
</ul>
21+
<p v-else>Вы еще ничего не печатали</p>
1522
</div>
1623
</template>
1724

1825
<script>
1926
import { defineComponent } from "vue";
2027
import { get_history } from "@/utils/history";
2128
import { log_open_history } from "@/utils/marketing";
29+
import { isMobile } from "@/utils/mobile";
2230
2331
export default defineComponent({
2432
name: "Home",
@@ -27,6 +35,11 @@ export default defineComponent({
2735
hist: [],
2836
};
2937
},
38+
computed: {
39+
isMobile() {
40+
return isMobile();
41+
},
42+
},
3043
mounted() {
3144
this.hist = get_history();
3245
this.hist.reverse();
@@ -44,15 +57,30 @@ export default defineComponent({
4457
</script>
4558

4659
<style>
60+
@import url(https://fonts.googleapis.com/icon?family=Material+Icons);
61+
4762
.hist {
4863
list-style: none;
64+
padding: 0;
65+
margin: 0;
4966
}
5067
.hist-item {
51-
display: block;
68+
display: flex;
69+
flex-flow: row;
70+
align-items: center;
71+
justify-content: space-between;
5272
margin: 20px;
5373
font-size: 1.5rem;
5474
}
75+
.hist-item > * {
76+
display: block;
77+
}
5578
.hist-item > small {
5679
font-size: 1rem;
5780
}
81+
82+
.material-icons {
83+
text-decoration: none;
84+
color: black;
85+
}
5886
</style>

src/views/Home.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ p,
7676
margin: 20px auto;
7777
}
7878
79-
.form-actions > .btn {
79+
.form-actions .btn {
8080
width: 100%;
8181
margin: 10px auto;
8282
}

src/views/QrPrint.vue

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
<template>
2+
<div>
3+
<h1 class="accordion-header" id="histAccordion-headingOne">
4+
Печать по QR коду
5+
</h1>
6+
<small
7+
><a href="#" @click.prevent="this.$router.go(-1)">← Вернуться</a></small
8+
>
9+
<p>
10+
Для быстрой печати по QR подойдите к принтеру и отсканируйте код под
11+
кнопкой "Печать".
12+
</p>
13+
<div v-if="qrInitSuccess === undefined">
14+
<p class="alert alert-success" role="alert">
15+
Попытка подключиться к камере
16+
</p>
17+
<p>
18+
Пожалуйста, предоставьте сайту доступ к камере вашего устройства. Вся
19+
обработка происходит на вашем устройстве, разработчики получают доступ
20+
только к отсканированному коду.
21+
</p>
22+
</div>
23+
<div v-else-if="qrInitSuccess === false">
24+
<p class="alert alert-secondary" role="alert">
25+
Для печати необходимо предоставить сайту доступ к камере!
26+
</p>
27+
<p>
28+
Пожалуйста, предоставьте сайту доступ к камере вашего устройства. Вся
29+
обработка происходит на вашем устройстве, разработчики получают доступ
30+
только к отсканированному коду.
31+
</p>
32+
</div>
33+
<qrcode-stream
34+
@decode="onDecode"
35+
@init="onInit"
36+
v-if="(qrInitSuccess !== false) & !qrPrintStatus"
37+
>
38+
</qrcode-stream>
39+
<div class="form-actions">
40+
<div v-if="qrPrintStatus === 'pending'">
41+
<p>Обработка...</p>
42+
</div>
43+
<div v-if="qrPrintStatus === 'success'">
44+
<p>Готово!</p>
45+
<router-link to="/" class="btn btn-lg btn-primary">
46+
Вернуться на главную
47+
</router-link>
48+
<router-link to="/history" class="btn btn-lg btn-primary">
49+
Перейти к истории
50+
</router-link>
51+
</div>
52+
<div v-if="qrPrintStatus === 'error'">
53+
<p>Не вышло!</p>
54+
<p>{{ qrPrintErrorMsg }}</p>
55+
<router-link to="/" class="btn btn-lg btn-primary">
56+
Вернуться на главную
57+
</router-link>
58+
<router-link to="/history" class="btn btn-lg btn-primary">
59+
Перейти к истории
60+
</router-link>
61+
<div class="btn btn-lg btn-success" @click="reset">
62+
Попробовать еще раз
63+
</div>
64+
</div>
65+
</div>
66+
</div>
67+
</template>
68+
69+
<script>
70+
import { QrcodeStream } from "vue3-qrcode-reader";
71+
import { log_open_qr, log_print_qr, log_error_qr } from "@/utils/marketing";
72+
73+
export default {
74+
data: () => ({
75+
pins: [],
76+
qrInitSuccess: undefined,
77+
qrPrintStatus: undefined,
78+
qrPrintErrorMsg: "",
79+
}),
80+
components: {
81+
QrcodeStream,
82+
},
83+
methods: {
84+
reset() {
85+
this.qrInitSuccess = undefined;
86+
this.qrPrintStatus = undefined;
87+
this.qrPrintErrorMsg = "";
88+
},
89+
onDecode(decodedString) {
90+
console.log(decodedString);
91+
this.qrPrintStatus = "pending";
92+
93+
fetch(`${process.env.VUE_APP_API_PRINTER}/qr`, {
94+
method: "POST",
95+
cache: "no-cache",
96+
redirect: "follow",
97+
headers: { "Content-Type": "application/json" },
98+
body: JSON.stringify({
99+
qr_token: decodedString,
100+
files: this.pins,
101+
}),
102+
})
103+
.then((response) => response.json())
104+
.then((response) => {
105+
console.log(response);
106+
if (response.status === "ok") {
107+
this.qrPrintStatus = "success";
108+
log_print_qr(this.pins);
109+
} else if (response.detail === "Terminal not found by qr") {
110+
this.qrPrintStatus = "error";
111+
this.qrPrintErrorMsg = "QR код недействительный";
112+
log_error_qr(this.pins, response);
113+
} else if (response.detail === "Terminal not found by qr") {
114+
this.qrPrintStatus = "error";
115+
this.qrPrintErrorMsg = "QR код недействительный";
116+
log_error_qr(this.pins, response);
117+
} else {
118+
this.qrPrintStatus = "error";
119+
this.qrPrintErrorMsg = "Неизвестная ошибка";
120+
log_error_qr(this.pins, response);
121+
}
122+
})
123+
.catch((error) => {
124+
console.log(error);
125+
this.qrPrintStatus = "error";
126+
this.qrPrintErrorMsg = "Ошибка сети";
127+
log_error_qr(this.pins, error);
128+
});
129+
},
130+
async onInit(promise) {
131+
// show loading indicator
132+
133+
try {
134+
await promise;
135+
this.qrInitSuccess = true;
136+
} catch (error) {
137+
this.qrInitSuccess = false;
138+
if (error.name === "NotAllowedError") {
139+
// user denied camera access permisson
140+
} else if (error.name === "NotFoundError") {
141+
// no suitable camera device installed
142+
} else if (error.name === "NotSupportedError") {
143+
// page is not served over HTTPS (or localhost)
144+
} else if (error.name === "NotReadableError") {
145+
// maybe camera is already in use
146+
} else if (error.name === "OverconstrainedError") {
147+
// did you requested the front camera although there is none?
148+
} else if (error.name === "StreamApiNotSupportedError") {
149+
// browser seems to be lacking features
150+
}
151+
} finally {
152+
// hide loading indicator
153+
}
154+
},
155+
},
156+
mounted() {
157+
const location = document.location.hash.replace(/^#?/, "");
158+
this.pins = location.split(",");
159+
log_open_qr();
160+
},
161+
};
162+
</script>

0 commit comments

Comments
 (0)