Skip to content

Commit 477a24d

Browse files
authored
Merge pull request #11 from NetLogo-Mobile/wsxiaolin-fix
A more user-friendly API error handling
1 parent 7c9a377 commit 477a24d

22 files changed

Lines changed: 670 additions & 49 deletions

dev-dist/sw.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ define(['./workbox-f87553f6'], (function (workbox) { 'use strict';
8282
"revision": "3ca0b8505b4bec776b69afdba2768812"
8383
}, {
8484
"url": "index.html",
85-
"revision": "0.l8ppn8a3v0g"
85+
"revision": "0.qfjm9eb9sl8"
8686
}], {});
8787
workbox.cleanupOutdatedCaches();
8888
workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("index.html"), {
@@ -100,7 +100,7 @@ define(['./workbox-f87553f6'], (function (workbox) { 'use strict';
100100
"networkTimeoutSeconds": 6,
101101
plugins: [new workbox.ExpirationPlugin({
102102
maxEntries: 1000,
103-
maxAgeSeconds: 259200
103+
maxAgeSeconds: 2592000
104104
})]
105105
}), 'GET');
106106

src/components/friends/list.vue

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ import UserItem from "./item.vue";
2020
import { NGrid, NGi } from "naive-ui";
2121
import { ref } from "vue";
2222
import { getData } from "@services/api/getData.ts";
23+
import { showAPiError } from "@popup/index.ts";
24+
import { removeToken } from "@services/utils.ts";
2325
import infiniteScroll from "../utils/infiniteScroll.vue";
2426
import { useI18n } from "vue-i18n";
2527
import { showMessage } from "@popup/naiveui";
@@ -61,6 +63,25 @@ async function handleLoad() {
6163
Take: 24,
6264
Query: "",
6365
});
66+
if (getRelationsRes?.Status !== 200) {
67+
showAPiError(
68+
t("errors.apiErrorTitle"),
69+
t("errors.apiErrorMessage"),
70+
handleLoad,
71+
);
72+
const _req = removeToken({
73+
UserID: userid,
74+
DisplayType: type,
75+
Skip: skip.value,
76+
Take: 24,
77+
Query: "",
78+
});
79+
const _res = removeToken(getRelationsRes);
80+
window.$ErrorLogger.captureApiError("POST", "/Users/GetRelations", getRelationsRes?.Status, _res, _req);
81+
console.error(`/Users/GetRelations returned ${getRelationsRes?.Status}`, _res);
82+
loading.value = false;
83+
return;
84+
}
6485
// 在某些地方用的skip传入的是时间戳,但是这里找不到可能与时间戳有关的逻辑,skip为整数也能work
6586
// In some places, the 'skip' is a timestamp, but here there doesn't seem to be any logic related to the timestamp; 'skip' as an integer also works.
6687
noMore.value = getRelationsRes.Data.$values.length < 24;

src/components/messages/MessageList.vue

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@
2222
import { ref, watch, nextTick } from "vue";
2323
import MessageItem from "./MessageItem.vue";
2424
import { getData } from "@services/api/getData.ts";
25+
import { showAPiError } from "@popup/index.ts";
26+
import { removeToken } from "@services/utils.ts";
2527
import type { PropType } from "vue";
2628
import { showMessage } from "@popup/naiveui";
2729
import InfiniteScroll from "../utils/infiniteScroll.vue";
@@ -112,6 +114,38 @@ const handleLoad = async () => {
112114
Skip: skip.value || 0,
113115
});
114116
117+
if (getMessagesResponse?.Status !== 200) {
118+
showAPiError(
119+
t("errors.apiErrorTitle"),
120+
t("errors.apiErrorMessage", {
121+
path: "/Messages/GetComments",
122+
status: getMessagesResponse?.Status,
123+
message: getMessagesResponse?.Message || "",
124+
}),
125+
async () => {
126+
return await getData("/Messages/GetComments", {
127+
TargetID: ID,
128+
TargetType: Category,
129+
CommentID: from || "",
130+
Take: 20,
131+
Skip: skip.value || 0,
132+
});
133+
},
134+
);
135+
const _req = removeToken({
136+
TargetID: ID,
137+
TargetType: Category,
138+
CommentID: from || "",
139+
Take: 20,
140+
Skip: skip.value || 0,
141+
});
142+
const _res = removeToken(getMessagesResponse);
143+
window.$ErrorLogger.captureApiError("POST", "/Messages/GetComments", getMessagesResponse?.Status, _res, _req);
144+
console.error(`/Messages/GetComments returned ${getMessagesResponse?.Status}`, _res);
145+
loading.value = false;
146+
return;
147+
}
148+
115149
const messages = getMessagesResponse.Data.Comments;
116150
const _length = messages.length;
117151
if (from) messages.shift();

src/components/messages/NotificationList.vue

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
import { ref, onActivated } from "vue";
1414
import Notification from "./NotificationItem.vue";
1515
import { getData } from "@services/api/getData.ts";
16+
import { showAPiError } from "@popup/index.ts";
17+
import { removeToken } from "@services/utils.ts";
1618
import { showMessage } from "@popup/naiveui";
1719
import InfiniteScroll from "../utils/infiniteScroll.vue";
1820
import { NDivider } from "naive-ui";
@@ -168,6 +170,31 @@ const handleLoad = async (noTemplates = true) => {
168170
NoTemplates: noTemplates,
169171
});
170172
173+
if (getMessagesResponse?.Status !== 200) {
174+
showAPiError(
175+
t("errors.apiErrorTitle"),
176+
t("errors.apiErrorMessage", {
177+
path: "/Messages/GetMessages",
178+
status: getMessagesResponse?.Status,
179+
message: getMessagesResponse?.Message || "",
180+
}),
181+
async () => {
182+
return await getData("/Messages/GetMessages", {
183+
CategoryID: convertUIIndexToCategoryID(notificationTypeIndexOfUI),
184+
Take: 20,
185+
Skip: skip.value,
186+
NoTemplates: noTemplates,
187+
});
188+
},
189+
);
190+
const _req = removeToken({ CategoryID: convertUIIndexToCategoryID(notificationTypeIndexOfUI), Take: 20, Skip: skip.value, NoTemplates: noTemplates });
191+
const _res = removeToken(getMessagesResponse);
192+
window.$ErrorLogger.captureApiError("POST", "/Messages/GetMessages", getMessagesResponse?.Status, _res, _req);
193+
console.error(`/Messages/GetMessages returned ${getMessagesResponse?.Status}`, _res);
194+
loading.value = false;
195+
return;
196+
}
197+
171198
if (!noTemplates) {
172199
templates = getMessagesResponse.Data.Templates;
173200
}
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
<template>
2+
<div class="api-error-popup">
3+
<div class="overlay" @click="onCancel" />
4+
<div class="dialog" role="dialog" aria-modal="true">
5+
<h3 class="title">{{ displayTitle }}</h3>
6+
<p class="message" v-html="displayMessage"></p>
7+
<div class="buttons">
8+
<button class="btn cancel" @click="onCancel">{{ cancelLabel }}</button>
9+
<button class="btn confirm" :disabled="loading" @click="onConfirmClick">
10+
<span v-if="loading">{{ confirmingLabel }}</span>
11+
<span v-else>{{ confirmLabel }}</span>
12+
</button>
13+
</div>
14+
</div>
15+
</div>
16+
</template>
17+
18+
<script setup lang="ts">
19+
import { ref, computed } from "vue";
20+
import type { PropType, Ref } from "vue";
21+
22+
const props = defineProps({
23+
title: String,
24+
message: String,
25+
// Optional reactive refs for live updates
26+
titleRef: { type: Object as PropType<Ref<string> | undefined> },
27+
messageRef: { type: Object as PropType<Ref<string> | undefined> },
28+
icon: { type: String as PropType<string>, default: "/assets/messages/Message-Default.png" },
29+
confirmLabel: { type: String as PropType<string>, default: "OK" },
30+
cancelLabel: { type: String as PropType<string>, default: "Cancel" },
31+
confirmingLabel: { type: String as PropType<string>, default: "Retrying..." },
32+
onConfirm: Function as PropType<() => Promise<any> | void>,
33+
close: Function as PropType<() => void>,
34+
});
35+
36+
const loading = ref(false);
37+
38+
const displayTitle = computed(() => {
39+
const tr = props.titleRef as Ref<string> | undefined;
40+
return (tr?.value ?? props.title ?? "");
41+
});
42+
43+
const displayMessage = computed(() => {
44+
const mr = props.messageRef as Ref<string> | undefined;
45+
return (mr?.value ?? props.message ?? "");
46+
});
47+
48+
async function onConfirmClick() {
49+
if (!props.onConfirm) {
50+
props.close?.();
51+
return;
52+
}
53+
try {
54+
loading.value = true;
55+
await props.onConfirm();
56+
// on success the caller may close via close()
57+
} catch (e) {
58+
// keep dialog open on failure – caller can decide behavior
59+
} finally {
60+
loading.value = false;
61+
}
62+
}
63+
64+
function onCancel() {
65+
props.close?.();
66+
}
67+
</script>
68+
69+
<style scoped>
70+
.api-error-popup {
71+
position: fixed;
72+
inset: 0;
73+
display: flex;
74+
align-items: center;
75+
justify-content: center;
76+
z-index: 10000;
77+
}
78+
.overlay {
79+
position: absolute;
80+
inset: 0;
81+
background: rgba(0, 0, 0, 0.45);
82+
}
83+
.dialog {
84+
position: relative;
85+
min-width: 300px;
86+
max-width: 88%;
87+
background: #fff;
88+
padding: 18px 12px 0 12px;
89+
border-radius: 10px;
90+
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.25);
91+
text-align: center;
92+
}
93+
.title {
94+
font-size: 22px;
95+
font-weight: 600;
96+
color: #222;
97+
margin: 6px 0 8px 0;
98+
}
99+
.message {
100+
color: #333;
101+
margin: 6px 10px 18px 10px;
102+
line-height: 1.6;
103+
text-align: center;
104+
}
105+
.message a {
106+
color: #0b6fff;
107+
text-decoration: underline;
108+
}
109+
.buttons {
110+
display: flex;
111+
border-top: 1px solid #eee;
112+
margin-top: 6px;
113+
}
114+
.btn {
115+
flex: 1 1 50%;
116+
padding: 14px 4px;
117+
border: none;
118+
background: transparent;
119+
color: #0b6fff;
120+
font-size: 17px;
121+
}
122+
.btn.cancel {
123+
border-right: 1px solid #eee;
124+
}
125+
.btn:disabled {
126+
opacity: 0.6;
127+
pointer-events: none;}
128+
</style>

src/components/projects/wortList.vue

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ import { NGrid, NGi } from "naive-ui";
1414
import Works from "./item.vue";
1515
import { ref } from "vue";
1616
import { getData } from "@services/api/getData.ts";
17+
import { showAPiError } from "@popup/index.ts";
18+
import { removeToken } from "@services/utils.ts";
1719
import { showMessage } from "@popup/naiveui";
1820
import infiniteScroll from "../utils/infiniteScroll.vue";
1921
import { useI18n } from "vue-i18n";
@@ -73,6 +75,43 @@ async function handleLoad() {
7375
...q,
7476
},
7577
});
78+
if (getProjectsRes?.Status !== 200) {
79+
showAPiError(
80+
t("errors.apiErrorTitle"),
81+
t("errors.apiErrorMessage", {
82+
path: "/Contents/QueryExperiments",
83+
status: getProjectsRes?.Status,
84+
message: getProjectsRes?.Message || "",
85+
}),
86+
handleLoad,
87+
);
88+
const _req = removeToken({
89+
Query: {
90+
Category: "Discussion",
91+
Languages: [],
92+
ExcludeLanguages: null,
93+
Tags: ["精选"],
94+
ModelTags: null,
95+
ExcludeTags: null,
96+
ModelID: null,
97+
ParentID: null,
98+
UserID: null,
99+
Special: null,
100+
From: from.value === "" ? null : from.value,
101+
Skip: skip.value,
102+
Take: 24,
103+
Days: 0,
104+
Sort: 0,
105+
ShowAnnouncement: false,
106+
...q,
107+
},
108+
});
109+
const _res = removeToken(getProjectsRes);
110+
window.$ErrorLogger.captureApiError("POST", "/Contents/QueryExperiments", getProjectsRes?.Status, _res, _req);
111+
console.error(`/Contents/QueryExperiments returned ${getProjectsRes?.Status}`, _res);
112+
isGettingData.value = false;
113+
return;
114+
}
76115
if (getProjectsRes.Data.$values.length < 24) {
77116
if (!hasInformed.value)
78117
showMessage("warning", t("ui.messages.noMore"), { duration: 1000 });

src/i18n/de.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
12
export default {
23
login: {
34
emailOrPhone: "E-Mail / Telefon",
@@ -85,6 +86,8 @@ export default {
8586
serverError: "Serverfehler, bitte versuchen Sie es später",
8687
contentFilter:
8788
'Der eingegebene Inhalt "...{word}..." enthält unangemessene Wörter',
89+
apiErrorTitle: "Fehler bei der Anfrage",
90+
apiErrorMessage: "{path} hat {status} zurückgegeben: {message}",
8891
},
8992
expeSummary: {
9093
enterExp: "Experiment eingeben ",
@@ -139,5 +142,10 @@ export default {
139142
astrophysicsExp: "Astrophysik-Experiment",
140143
electromagneticsExp: "Elektromagnetismus-Experiment",
141144
},
145+
ok: "OK",
146+
cancel: "Abbrechen",
147+
retrying: "Wiederholung...",
148+
retrySuccess: "Wiederholung erfolgreich",
149+
retryFailed: "Wiederholung fehlgeschlagen",
142150
},
143151
};

src/i18n/en.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,8 @@ export default {
8383
serverError: "Server error, please try again later",
8484
contentFilter:
8585
'The content you entered "...{word}..." contains inappropriate words',
86+
apiErrorTitle: "Request Error",
87+
apiErrorMessage: "{path} returned {status}: {message}",
8688
},
8789
expeSummary: {
8890
enterExp: "Enter experience ",
@@ -137,5 +139,10 @@ export default {
137139
astrophysicsExp: "Astrophysics Experiment",
138140
electromagneticsExp: "Electromagnetics Experiment",
139141
},
142+
ok: "OK",
143+
cancel: "Cancel",
144+
retrying: "Retrying...",
145+
retrySuccess: "Retry succeeded",
146+
retryFailed: "Retry failed",
140147
},
141148
};

src/i18n/fr.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,8 @@ export default {
8484
serverError: "Erreur serveur, veuillez réessayer plus tard",
8585
contentFilter:
8686
'Le contenu que vous avez saisi "...{word}..." contient des mots inappropriés',
87+
apiErrorTitle: "Erreur de requête",
88+
apiErrorMessage: "{path} a retourné {status} : {message}",
8789
},
8890
expeSummary: {
8991
enterExp: "Entrer dans l'expérience ",
@@ -138,5 +140,10 @@ export default {
138140
astrophysicsExp: "Expérience d'astrophysique",
139141
electromagneticsExp: "Expérience d'électromagnétisme",
140142
},
143+
ok: "OK",
144+
cancel: "Annuler",
145+
retrying: "Nouvelle tentative...",
146+
retrySuccess: "Nouvelle tentative réussie",
147+
retryFailed: "Nouvelle tentative échouée",
141148
},
142149
};

0 commit comments

Comments
 (0)