Files
cursor-byok/frontend/src/views/Home.vue
T

214 lines
6.2 KiB
Vue
Raw 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.
<script setup>
import Button from "@/components/ui/Button.vue";
import Card from "@/components/ui/Card.vue";
import Switch from "@/components/ui/Switch.vue";
import HomeMetricsCard from "@/components/HomeMetricsCard.vue";
import CursorAccountCard from "@/components/CursorAccountCard.vue";
import { useMessage } from "@/composables/useMessage";
import { showModal } from "@/composables/useModal";
import { getAdRuntime } from "@/services/clientApi";
import {
appState,
appViewState,
openConfigWindow,
openModelConfigWindow,
saveRoutingMode,
syncHomeMetrics,
syncServiceState,
toUserError,
toggleService,
} from "@/state/appState";
import { Events } from "@wailsio/runtime";
import { computed, onBeforeUnmount, onMounted, ref } from "vue";
const directModeEnabled = computed(() => appState.routingMode === "upstream");
const message = useMessage();
const AD_UPDATED_EVENT = "ad:updated";
const OPEN_AD_EVENT = "cursor:open-ad";
const adRuntime = ref(null);
let unsubscribeAdUpdated = null;
function asString(value) {
if (typeof value === "string") {
return value.trim();
}
if (typeof value === "number" || typeof value === "boolean") {
return String(value);
}
return "";
}
function asBoolean(value) {
return value === true || value === "true" || value === 1 || value === "1";
}
const homeAds = computed(() => {
const runtime = adRuntime.value && typeof adRuntime.value === "object" ? adRuntime.value : {};
const slots = Array.isArray(runtime.slots) && runtime.slots.length > 0 ? runtime.slots : [runtime];
return slots
.map((slot, index) => {
const item = slot && typeof slot === "object" ? slot : {};
const home = item.home && typeof item.home === "object" ? item.home : {};
const title = asString(home.title);
if (
!title ||
!asBoolean(item.available) ||
!asBoolean(item.enabled) ||
!asString(item.packageHash)
) {
return null;
}
return {
id: asString(item.id) || String(index + 1),
title,
subtitle: asString(home.subtitle),
};
})
.filter(Boolean);
});
async function syncAdRuntimeQuietly() {
try {
adRuntime.value = await getAdRuntime();
} catch (_error) {
adRuntime.value = null;
}
}
function handleAdUpdated() {
void syncAdRuntimeQuietly();
}
function handleOpenHomeAd(slotId) {
window.dispatchEvent(new CustomEvent(OPEN_AD_EVENT, { detail: { slotId: asString(slotId) } }));
}
async function showActionError(title, error) {
await showModal({
title,
content: String(error || "服务错误").trim() || "服务错误",
});
}
async function handleToggleService() {
const result = await toggleService();
if (!result.ok) {
await showActionError("服务操作失败", result.error);
}
}
async function handleRefreshState() {
const [serviceStateResult] = await Promise.allSettled([
syncServiceState(),
syncHomeMetrics(),
]);
if (serviceStateResult.status === "rejected") {
await showActionError("刷新失败", toUserError(serviceStateResult.reason));
}
}
async function handleRefreshMetrics() {
await syncHomeMetrics().catch(() => {});
}
async function handleOpenConfig() {
try {
await openConfigWindow();
} catch (error) {
await showActionError("打开失败", toUserError(error));
}
}
async function handleOpenModelConfig() {
try {
await openModelConfigWindow();
} catch (error) {
await showActionError("打开失败", toUserError(error));
}
}
async function handleDirectModeChange(enabled) {
const result = await saveRoutingMode(enabled ? "upstream" : "local");
if (!result.ok) {
await showActionError("切换失败", result.error);
return;
}
message.success(enabled ? "已切换到直连 Cursor 模式" : "已切换到本地服务模式");
}
onMounted(() => {
unsubscribeAdUpdated = Events.On(AD_UPDATED_EVENT, handleAdUpdated);
void syncAdRuntimeQuietly();
});
onBeforeUnmount(() => {
if (unsubscribeAdUpdated) {
unsubscribeAdUpdated();
}
});
</script>
<template>
<div class="flex h-full min-h-0 flex-col gap-4 overflow-y-auto p-4 pt-0 text-[#e5e5e5]">
<HomeMetricsCard
:metrics="appState.homeMetrics"
:loading="appState.homeMetricsLoading"
:error="appState.homeMetricsError"
:home-ads="homeAds"
@refresh="handleRefreshMetrics"
@open-ad="handleOpenHomeAd"
/>
<Card>
<div class="flex flex-col gap-4">
<div class="flex items-start justify-between gap-4">
<div class="flex flex-col gap-1">
<div class="text-sm" :class="appViewState.serviceStatusClass">
{{ appViewState.serviceStatusText }}
</div>
</div>
<div class="center-row gap-2">
<Button variant="primary" :disabled="appState.serviceBusy" @click="handleToggleService">
<span class="icon-[mdi--pause] text-[16px]" v-if="appState.serviceRunning"></span>
<span class="icon-[mdi--play] text-[16px]" v-else></span>
<span> {{ appViewState.serviceButtonText }}</span>
</Button>
</div>
</div>
<div v-if="appState.serviceLastError"
class="rounded-[8px] border border-[#4b1d1d] bg-[#2a1313] px-3 py-2 text-sm text-[#fca5a5]">
{{ appState.serviceLastError }}
</div>
<Switch
label="直连模式"
description="开启后,Cursor将直接接通官方,请勿开启"
enabled-text="当前为直连模式"
disabled-text="当前为本地服务模式"
:enabled="directModeEnabled"
:busy="appState.configSaving"
:disabled="appState.configSaving"
@change="handleDirectModeChange"
/>
</div>
</Card>
<CursorAccountCard />
<Card>
<div class="flex items-center justify-between gap-4">
<div>
<h2 class="text-base font-medium text-white">本地配置</h2>
<div class="text-sm text-[#a3a3a3]">打开设置目录或单独管理模型配置</div>
</div>
<div class="center-row gap-2">
<Button variant="default" @click="handleOpenConfig">设置文件夹</Button>
<Button variant="primary" @click="handleOpenModelConfig">模型配置</Button>
</div>
</div>
</Card>
</div>
</template>