1
0
mirror of https://github.com/ZeroCatDev/Classworks.git synced 2026-03-22 01:53:11 +00:00
Classworks/src/components/PwaInstallCard.vue

351 lines
10 KiB
Vue
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<v-card
v-if="showCard"
class="mb-4"
color="surface-variant"
variant="tonal"
>
<div class="d-flex flex-no-wrap justify-space-between">
<div class="pe-4">
<v-card-title class="text-h6">
安装应用与授权
</v-card-title>
<v-card-subtitle class="pb-1">
手动点选下方项目请求安装和权限也可以直接关闭
</v-card-subtitle>
<v-card-text class="pt-0 pb-1">
<v-list density="comfortable" lines="two">
<v-list-item
v-for="item in chipList"
:key="item.key"
:disabled="isRequesting"
@click="() => handleSingleRequest(item.key)"
>
<template #prepend>
<v-avatar :color="chipColors[item.status]" size="32" variant="tonal">
<v-icon :icon="statusIcons[item.status]"></v-icon>
</v-avatar>
</template>
<v-list-item-title>{{ item.label }}</v-list-item-title>
<v-list-item-subtitle>{{ item.description }}</v-list-item-subtitle>
<template #append>
<v-chip :color="chipColors[item.status]" size="small" variant="tonal" class="me-2">
{{ statusText[item.status] }}
</v-chip>
<v-btn
variant="text"
icon="mdi-information"
size="small"
:disabled="isRequesting"
@click.stop="() => openHelp(item.key)"
></v-btn>
</template>
</v-list-item>
</v-list>
</v-card-text>
<v-card-actions>
<v-btn
class="ms-2"
variant="outlined"
size="small"
@click="dismiss"
>
关闭
</v-btn>
<v-btn
class="ms-2"
variant="elevated"
color="primary"
size="small"
:prepend-icon="isRequesting ? 'mdi-timer-sand' : 'mdi-shield-check'"
:disabled="!hasPendingRequests || isRequesting"
@click="handleRequest"
>
{{ isRequesting ? "处理中" : "一次处理全部" }}
</v-btn>
</v-card-actions>
</div>
<v-avatar
class="ma-3"
size="100"
rounded="0"
>
<v-icon icon="mdi-monitor-cellphone" size="80"></v-icon>
</v-avatar>
</div>
<v-dialog v-model="helpDialog" max-width="520">
<v-card>
<v-card-title class="text-h6">{{ helpContent.title }}</v-card-title>
<v-card-text>
<p class="mb-3">{{ helpContent.message }}</p>
<v-list density="comfortable">
<v-list-item
v-for="(link, index) in helpContent.links"
:key="index"
:href="link.href"
target="_blank"
rel="noopener"
>
<v-list-item-title>{{ link.text }}</v-list-item-title>
<v-list-item-subtitle>{{ link.desc }}</v-list-item-subtitle>
</v-list-item>
</v-list>
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn variant="text" @click="helpDialog = false">我知道了</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</v-card>
</template>
<script setup>
import { ref, reactive, computed, onMounted, onBeforeUnmount } from 'vue';
import { getSetting, setSetting, requestNotificationPermission, requestPersistentStorage } from "@/utils/settings";
const showCard = ref(false);
const isRequesting = ref(false);
const helpDialog = ref(false);
const helpContent = reactive({
title: "",
message: "",
links: [],
});
const statusText = {
pending: "待授权",
granted: "已完成",
denied: "已拒绝",
unavailable: "不可用",
};
const statusIcons = {
pending: "mdi-progress-clock",
granted: "mdi-check-circle",
denied: "mdi-close-circle",
unavailable: "mdi-help-circle",
};
const chipColors = {
pending: "primary",
granted: "success",
denied: "error",
unavailable: "surface-variant",
};
const permissionStates = reactive({
pwa: { label: "安装应用", description: "将网站安装为独立应用便于快速启动", status: "pending" },
notification: { label: "通知权限", description: "允许接收作业考试等通知提醒", status: "pending" },
storage: { label: "离线存储", description: "启用持久化存储以获得更稳健的离线体验", status: "pending" },
});
const chipList = computed(() => [
{ key: "pwa", ...permissionStates.pwa },
{ key: "notification", ...permissionStates.notification },
{ key: "storage", ...permissionStates.storage },
]);
const hasPendingRequests = computed(() => chipList.value.some((item) => item.status === "pending"));
const helpLinks = {
pwa: [
{
text: "MDN - 安装 PWA 指南",
desc: "检查浏览器是否支持并手动触发安装",
href: "https://developer.mozilla.org/zh-CN/docs/Web/Progressive_web_apps/Guides/Installing",
},
{
text: "Microsoft Edge - PWA 体验",
desc: "Edge 浏览器安装与 UX 说明",
href: "https://learn.microsoft.com/zh-cn/microsoft-edge/progressive-web-apps/ux",
},
],
notification: [
{
text: "MDN - 通知权限与用法",
desc: "浏览器通知权限的工作方式与调试",
href: "https://developer.mozilla.org/zh-CN/docs/Web/API/notification",
},
],
storage: [
{
text: "MDN - Storage 持久化说明",
desc: "了解持久化存储的可用性与申请方式",
href: "https://developer.mozilla.org/zh-CN/docs/Web/API/StorageManager/persist",
},
],
};
let displayModeMedia;
const refreshStates = async () => {
const hideCard = getSetting("pwa.hideInstallCard");
if (hideCard) {
showCard.value = false;
return;
}
const isStandalone = window.matchMedia('(display-mode: standalone)').matches ||
window.navigator.standalone === true;
if (isStandalone) {
permissionStates.pwa.status = "granted";
} else if (window.deferredPwaPrompt) {
permissionStates.pwa.status = "pending";
} else {
// 仍标记为待处理,允许点击后给出指导
permissionStates.pwa.status = "pending";
}
if (typeof Notification === "undefined") {
permissionStates.notification.status = "unavailable";
} else {
const current = Notification.permission;
permissionStates.notification.status = current === "granted"
? "granted"
: current === "denied"
? "denied"
: "pending";
}
if (navigator.storage?.persisted) {
const persisted = await navigator.storage.persisted();
permissionStates.storage.status = persisted ? "granted" : "pending";
} else {
permissionStates.storage.status = "unavailable";
}
const stillNeedAction = chipList.value.some((item) => item.status !== "granted");
showCard.value = stillNeedAction;
};
const requestPwaInstall = async () => {
const promptEvent = window.deferredPwaPrompt;
if (!promptEvent) {
permissionStates.pwa.status = "pending";
openHelp("pwa", "浏览器没有提供安装提示,可按文档手动安装。");
return;
}
promptEvent.prompt();
const { outcome } = await promptEvent.userChoice;
permissionStates.pwa.status = outcome === "accepted" ? "granted" : "denied";
if (outcome !== "accepted") {
openHelp("pwa", "如果未出现安装弹窗,或被拒绝,请按说明手动安装。");
}
window.deferredPwaPrompt = null;
};
const requestNotification = async () => {
if (typeof Notification === "undefined") {
permissionStates.notification.status = "unavailable";
openHelp("notification", "当前环境不支持通知 API可查看说明手动开启或更换浏览器。");
return;
}
const granted = await requestNotificationPermission();
permissionStates.notification.status = granted ? "granted" : "denied";
if (!granted) {
openHelp("notification", "通知请求未被授予,请按说明检查浏览器或系统设置。");
}
};
const requestStorage = async () => {
if (!navigator.storage?.persist) {
permissionStates.storage.status = "unavailable";
openHelp("storage", "当前浏览器不支持持久化存储,可查看说明或更换浏览器。");
return;
}
const persisted = await requestPersistentStorage();
permissionStates.storage.status = persisted ? "granted" : "denied";
if (!persisted) {
openHelp("storage", "未能启用持久化存储,可按说明检查浏览器或系统设置。");
}
};
const handleRequest = async () => {
if (!hasPendingRequests.value || isRequesting.value) return;
isRequesting.value = true;
try {
await requestPwaInstall();
await requestNotification();
await requestStorage();
} finally {
isRequesting.value = false;
await refreshStates();
}
};
const handleSingleRequest = async (key) => {
if (isRequesting.value) return;
isRequesting.value = true;
try {
if (key === "pwa") {
await requestPwaInstall();
} else if (key === "notification") {
await requestNotification();
} else if (key === "storage") {
await requestStorage();
}
} finally {
isRequesting.value = false;
await refreshStates();
}
};
const openHelp = (key, message = "") => {
if (key === "pwa") {
helpContent.title = "如何安装为应用";
} else if (key === "notification") {
helpContent.title = "如何开启通知";
} else {
helpContent.title = "如何启用离线存储";
}
helpContent.message = message || "查看以下步骤获取更多说明。";
helpContent.links = helpLinks[key] || [];
helpDialog.value = true;
};
const dismiss = () => {
setSetting("pwa.hideInstallCard", true);
showCard.value = false;
};
const onPromptReady = () => {
if (permissionStates.pwa.status !== "granted") {
permissionStates.pwa.status = "pending";
}
refreshStates();
};
const onDisplayModeChange = () => {
refreshStates();
};
onMounted(() => {
refreshStates();
window.addEventListener('pwa-prompt-ready', onPromptReady);
displayModeMedia = window.matchMedia('(display-mode: standalone)');
displayModeMedia.addEventListener('change', onDisplayModeChange);
});
onBeforeUnmount(() => {
window.removeEventListener('pwa-prompt-ready', onPromptReady);
if (displayModeMedia) {
displayModeMedia.removeEventListener('change', onDisplayModeChange);
}
});
</script>