refactor(app): rename the desktop app and refine the interface

This commit is contained in:
lzt404 committed 2026-07-19 02:47:57 +08:00
1 parent 91032f3a06
commit ad02c2a8a6
15 files changed
+206 -87

No files matched your search

+39 -4
View File
@@ -1,5 +1,18 @@
import { useEffect, useState } from 'react';
import { Bot, FileKey, Gauge, GitFork, History, LogIn, Network, ServerCog, Settings } from 'lucide-react';
import { invoke } from '@tauri-apps/api/core';
import {
Bot,
ExternalLink,
FileKey,
Gauge,
GitFork,
History,
LogIn,
MessageCircle,
Network,
ServerCog,
Settings,
} from 'lucide-react';
import appLogo from './assets/logo.jpg';
import { CoreRuntimeProvider, useCoreRuntime } from './coreRuntime';
import { ConfigPanelPage } from './pages/ConfigPanel';
@@ -12,6 +25,8 @@ import { AgentsPage } from './pages/AgentsPage';
import { ThinkingAliasesPage } from './pages/ThinkingAliasesPage';
import { UsageRecordsPage } from './pages/UsageRecordsPage';
const CONTACT_URL = 'https://qm.qq.com/q/3queDaIG';
const pages = [
{
id: 'kernel',
@@ -99,6 +114,14 @@ function AppContent() {
setActive(pageId);
};
const openContact = async () => {
try {
await invoke('open_external_url', { url: CONTACT_URL });
} catch (error) {
console.error('打开联系我们链接失败', error);
}
};
return (
<div className="app-shell">
<aside className="sidebar">
@@ -132,9 +155,21 @@ function AppContent() {
})}
</nav>
<div className="sidebar-footer">
<span>CPA GUI</span>
<strong>0.1</strong>
<div className="sidebar-bottom">
<button
type="button"
className="sidebar-contact"
title="通过 QQ 联系我们"
onClick={() => void openContact()}
>
<MessageCircle size={16} aria-hidden="true" />
<span>联系我们</span>
<ExternalLink size={13} aria-hidden="true" />
</button>
<div className="sidebar-footer">
<span>Easy_CLIProxyAPI</span>
<strong>0.1</strong>
</div>
</div>
</aside>
+1 -1
View File
@@ -11,7 +11,7 @@ export class AppErrorBoundary extends Component<Props, State> {
}
componentDidCatch(error: Error, info: ErrorInfo) {
console.error('CPA GUI 渲染异常', error, info.componentStack);
console.error('Easy_CLIProxyAPI 渲染异常', error, info.componentStack);
}
render() {
+60 -1
View File
@@ -1,6 +1,7 @@
import {
useCallback,
useEffect,
useLayoutEffect,
useMemo,
useRef,
useState,
@@ -215,6 +216,13 @@ type AgentModelPickerProps = {
onRefresh: () => void;
};
type AgentModelDropdownLayout = {
top: number;
left: number;
width: number;
height: number;
};
function AgentModelPicker({
models,
value,
@@ -227,6 +235,7 @@ function AgentModelPicker({
const [open, setOpen] = useState(false);
const [search, setSearch] = useState('');
const [activeIndex, setActiveIndex] = useState(0);
const [dropdownLayout, setDropdownLayout] = useState<AgentModelDropdownLayout | null>(null);
const rootRef = useRef<HTMLDivElement>(null);
const searchRef = useRef<HTMLInputElement>(null);
const visibleModels = useMemo(() => filterAgentModels(models, search), [models, search]);
@@ -236,6 +245,51 @@ function AgentModelPicker({
);
const selectedAlias = agentModelAlias(models, value);
const updateDropdownLayout = useCallback(() => {
const root = rootRef.current;
if (!root) return;
const rect = root.getBoundingClientRect();
const edgeGap = 12;
const triggerGap = 6;
const preferredHeight = 282;
const minimumHeight = 150;
const spaceBelow = Math.max(0, window.innerHeight - rect.bottom - triggerGap - edgeGap);
const spaceAbove = Math.max(0, rect.top - triggerGap - edgeGap);
const placeAbove = spaceBelow < preferredHeight && spaceAbove > spaceBelow;
const availableHeight = placeAbove ? spaceAbove : spaceBelow;
const height = Math.min(preferredHeight, Math.max(minimumHeight, availableHeight));
const width = Math.min(rect.width, window.innerWidth - edgeGap * 2);
const left = Math.min(
Math.max(edgeGap, rect.left),
Math.max(edgeGap, window.innerWidth - edgeGap - width),
);
const desiredTop = placeAbove
? rect.top - triggerGap - height
: rect.bottom + triggerGap;
const top = Math.min(
Math.max(edgeGap, desiredTop),
Math.max(edgeGap, window.innerHeight - edgeGap - height),
);
setDropdownLayout({ top, left, width, height });
}, []);
useLayoutEffect(() => {
if (!open) {
setDropdownLayout(null);
return undefined;
}
updateDropdownLayout();
window.addEventListener('resize', updateDropdownLayout);
window.addEventListener('scroll', updateDropdownLayout);
return () => {
window.removeEventListener('resize', updateDropdownLayout);
window.removeEventListener('scroll', updateDropdownLayout);
};
}, [open, updateDropdownLayout]);
useEffect(() => {
if (!open) return undefined;
const close = (event: MouseEvent) => {
@@ -311,7 +365,12 @@ function AgentModelPicker({
</button>
{open ? (
<div className="agent-model-dropdown">
<div
className="agent-model-dropdown"
style={dropdownLayout
? dropdownLayout
: { top: 0, left: 0, width: 0, height: 0, visibility: 'hidden' }}
>
<div className="agent-model-search">
<Search size={15} aria-hidden />
<input
-18
View File
@@ -82,17 +82,6 @@ function requestLatestCore() {
return latestCheckPromise;
}
const compareVersions = (left: string, right: string) => {
const parse = (value: string) => value.replace(/^v/i, '').split('.').map((part) => Number(part) || 0);
const leftParts = parse(left);
const rightParts = parse(right);
for (let index = 0; index < Math.max(leftParts.length, rightParts.length); index += 1) {
const difference = (leftParts[index] ?? 0) - (rightParts[index] ?? 0);
if (difference !== 0) return difference;
}
return 0;
};
export function KernelPage() {
const {
status: coreStatus,
@@ -491,13 +480,6 @@ export function KernelPage() {
const installBundledCore = async () => {
if (!bundledCore) return;
const comparison = currentVersion ? compareVersions(currentVersion, bundledCore.version) : 0;
const prompt = comparison > 0
? `当前内核 ${currentVersion} 高于内置版本 ${bundledCore.version},继续会降级。是否确认?`
: currentVersion === bundledCore.version
? `将使用内置压缩包重新安装 ${bundledCore.version},是否继续?`
: `将安装内置内核 ${bundledCore.version},是否继续?`;
if (!window.confirm(prompt)) return;
setInstalling(true);
setMessage(`正在安装内置内核 ${bundledCore.version}`);
+49 -13
View File
@@ -181,14 +181,52 @@ input:focus-visible {
color: #2d2a26;
}
.sidebar-bottom {
display: grid;
gap: 10px;
margin-top: auto;
padding-top: 18px;
border-top: 1px solid #e3e1db;
}
.sidebar-contact {
display: grid;
grid-template-columns: 18px minmax(0, 1fr) 14px;
align-items: center;
gap: 8px;
width: 100%;
min-height: 38px;
padding: 0 10px;
border: 1px solid #d8d4cc;
border-radius: 7px;
background: #fffdf8;
color: #4f4a43;
text-align: left;
}
.sidebar-contact:hover {
border-color: #c7c2b8;
background: #e8e5dc;
color: #2d2a26;
}
.sidebar-contact svg {
color: #7a746c;
}
.sidebar-contact span {
overflow: hidden;
font-size: 12px;
font-weight: 700;
text-overflow: ellipsis;
white-space: nowrap;
}
.sidebar-footer {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
margin-top: auto;
padding-top: 18px;
border-top: 1px solid #e3e1db;
color: #7a746c;
font-size: 12px;
}
@@ -3600,13 +3638,10 @@ input:focus-visible {
}
.agent-model-dropdown {
position: absolute;
position: fixed;
z-index: 45;
top: calc(100% + 6px);
right: 0;
left: 0;
display: grid;
grid-template-rows: 40px 260px 28px;
grid-template-rows: 40px minmax(0, 1fr) 28px;
gap: 7px;
padding: 9px;
border: 1px solid #e3e1db;
@@ -4880,6 +4915,12 @@ input:focus-visible {
display: none;
}
.sidebar-bottom {
margin-top: 0;
padding-top: 0;
border-top: 0;
}
.content {
padding: 18px;
}
@@ -5132,11 +5173,6 @@ input:focus-visible {
flex-direction: column;
}
.agent-model-dropdown {
right: -4px;
left: -4px;
}
.agent-config-footer .primary-button {
width: 100%;
}