完善桌面端交互体验与发布检查

This commit is contained in:
Cheng Zhou
2026-06-30 22:26:34 +08:00
parent 628ff8828b
commit c923f887a0
30 changed files with 2507 additions and 141 deletions
+302
View File
@@ -0,0 +1,302 @@
<template>
<div class="desktop-preferences">
<header class="preferences-header">
<div>
<p class="preferences-kicker">CTMS Desktop</p>
<h3>桌面偏好</h3>
</div>
<el-button text :icon="Close" aria-label="关闭桌面偏好" @click="emit('close-request')" />
</header>
<section class="preference-section">
<div class="section-title">连接</div>
<div class="server-card">
<span class="server-label">当前服务器</span>
<code>{{ desktopServerUrl || "未配置" }}</code>
<el-button size="small" @click="openServerSettings">服务器设置</el-button>
</div>
</section>
<section class="preference-section">
<div class="section-title">通知与更新</div>
<div class="preference-row">
<div>
<div class="row-title">系统通知</div>
<div class="row-desc">只显示不含项目详情的文件更新提示</div>
</div>
<div class="row-control">
<el-switch
v-model="desktopNotificationsEnabled"
:loading="desktopNotificationLoading"
@change="onDesktopNotificationChange"
/>
<el-tag size="small" :type="notificationPermissionTagType">{{ notificationPermissionText }}</el-tag>
</div>
</div>
<div class="preference-row">
<div>
<div class="row-title">桌面更新</div>
<div class="row-desc">正式版本按当前服务器发布源检查签名更新</div>
</div>
<el-button size="small" :disabled="!desktopUpdaterAvailable" :loading="desktopUpdateChecking" @click="checkDesktopUpdateNow">
检查更新
</el-button>
</div>
</section>
<section class="preference-section">
<div class="section-title">诊断信息</div>
<div class="metadata-panel">
<dl>
<template v-for="row in clientMetadataRows" :key="row.label">
<dt>{{ row.label }}</dt>
<dd>{{ row.value }}</dd>
</template>
</dl>
<el-button size="small" @click="copyClientMetadata">复制诊断信息</el-button>
</div>
</section>
</div>
</template>
<script setup lang="ts">
import { computed, onMounted, ref } from "vue";
import { useRouter } from "vue-router";
import { ElMessage } from "element-plus";
import { Close } from "@element-plus/icons-vue";
import {
getDesktopNotificationSubscription,
setDesktopNotificationSubscription,
} from "../api/desktopNotifications";
import {
clientRuntime,
getAppMetadata,
getDesktopServerUrl,
getNotificationPermission,
isDesktopUpdaterAvailable,
requestNotificationPermission,
type NotificationPermissionState,
} from "../runtime";
import { checkDesktopUpdateAndPrompt } from "../session/desktopUpdateManager";
import { triggerDesktopNotificationPoll } from "../session/desktopNotificationManager";
const emit = defineEmits<{
"close-request": [];
}>();
const router = useRouter();
const desktopServerUrl = ref(getDesktopServerUrl());
const clientMetadata = getAppMetadata();
const desktopCapabilities = clientRuntime.capabilities();
const desktopUpdaterAvailable = isDesktopUpdaterAvailable();
const desktopNotificationsEnabled = ref(false);
const desktopNotificationLoading = ref(false);
const desktopUpdateChecking = ref(false);
const notificationPermission = ref<NotificationPermissionState>("unsupported");
const clientMetadataRows = computed(() => [
{ label: "客户端", value: `${clientMetadata.clientType} ${clientMetadata.version}` },
{ label: "平台", value: clientMetadata.platform },
{ label: "构建通道", value: clientMetadata.channel },
{ label: "提交", value: clientMetadata.commit },
{ label: "服务器", value: desktopServerUrl.value || "未配置" },
{
label: "能力",
value: [
desktopCapabilities.secureSessionStorage ? "安全会话" : "浏览器会话",
desktopCapabilities.nativeFiles ? "原生文件" : "浏览器文件",
desktopCapabilities.systemNotifications ? "系统通知" : "无系统通知",
desktopCapabilities.automaticUpdates ? "自动更新" : "无自动更新",
].join(" / "),
},
]);
const notificationPermissionText = computed(() => {
if (notificationPermission.value === "granted") return "已授权";
if (notificationPermission.value === "denied") return "已拒绝";
if (notificationPermission.value === "prompt") return "待授权";
return "不可用";
});
const notificationPermissionTagType = computed(() => {
if (notificationPermission.value === "granted") return "success";
if (notificationPermission.value === "denied") return "danger";
if (notificationPermission.value === "prompt") return "warning";
return "info";
});
const loadNotificationState = async () => {
notificationPermission.value = await getNotificationPermission();
try {
const { data } = await getDesktopNotificationSubscription();
desktopNotificationsEnabled.value = data.enabled;
} catch {
desktopNotificationsEnabled.value = false;
}
};
const onDesktopNotificationChange = async (value: string | number | boolean) => {
if (desktopNotificationLoading.value) return;
desktopNotificationLoading.value = true;
try {
if (value) {
notificationPermission.value = await requestNotificationPermission();
if (notificationPermission.value !== "granted") {
desktopNotificationsEnabled.value = false;
return;
}
}
const { data } = await setDesktopNotificationSubscription(Boolean(value));
desktopNotificationsEnabled.value = data.enabled;
triggerDesktopNotificationPoll();
ElMessage.success(data.enabled ? "系统通知已开启" : "系统通知已关闭");
} catch (error: any) {
desktopNotificationsEnabled.value = !Boolean(value);
ElMessage.error(error?.response?.data?.detail || "系统通知设置失败");
} finally {
desktopNotificationLoading.value = false;
}
};
const checkDesktopUpdateNow = async () => {
if (desktopUpdateChecking.value) return;
desktopUpdateChecking.value = true;
try {
await checkDesktopUpdateAndPrompt({ notifyWhenCurrent: true });
} finally {
desktopUpdateChecking.value = false;
}
};
const copyClientMetadata = async () => {
const text = clientMetadataRows.value.map((row) => `${row.label}: ${row.value}`).join("\n");
await navigator.clipboard?.writeText(text);
ElMessage.success("诊断信息已复制");
};
const openServerSettings = () => {
emit("close-request");
router.push("/desktop/server-settings");
};
onMounted(() => {
void loadNotificationState();
});
</script>
<style scoped>
.desktop-preferences {
display: flex;
flex-direction: column;
gap: 18px;
}
.preferences-header {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 16px;
}
.preferences-kicker {
margin: 0 0 4px;
color: #2563eb;
font-size: 12px;
font-weight: 700;
text-transform: uppercase;
}
h3 {
margin: 0;
color: #0f172a;
font-size: 20px;
}
.preference-section {
display: flex;
flex-direction: column;
gap: 10px;
}
.section-title {
color: #475569;
font-size: 12px;
font-weight: 700;
}
.server-card,
.preference-row,
.metadata-panel {
border: 1px solid #dbe6f2;
border-radius: 8px;
background: #ffffff;
}
.server-card {
display: grid;
grid-template-columns: auto minmax(0, 1fr) auto;
align-items: center;
gap: 10px;
padding: 12px;
}
.server-label,
.row-desc {
color: #64748b;
font-size: 12px;
}
code {
overflow-wrap: anywhere;
color: #0f172a;
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
font-size: 12px;
}
.preference-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
padding: 12px;
}
.row-title {
color: #0f172a;
font-size: 14px;
font-weight: 700;
}
.row-control {
display: inline-flex;
align-items: center;
gap: 10px;
}
.metadata-panel {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
gap: 16px;
align-items: start;
padding: 12px;
}
dl {
display: grid;
grid-template-columns: 72px minmax(0, 1fr);
gap: 8px 12px;
margin: 0;
}
dt {
color: #64748b;
font-size: 12px;
}
dd {
margin: 0;
color: #0f172a;
font-size: 12px;
overflow-wrap: anywhere;
}
</style>