This commit is contained in:
lincube
2026-02-14 20:13:43 +08:00
parent aa0154db25
commit 5a8fa98646
3 changed files with 301 additions and 137 deletions

View File

@@ -1,26 +1,47 @@
# desktop
# LanMontainDesktop
An Electron application with React and TypeScript
一个使用 Electron 打包的桌面应用:前端采用 Vue 3Renderer主进程内置 Elysia.js 作为本地后端服务Main
## Recommended IDE Setup
## 技术栈
- [VSCode](https://code.visualstudio.com/) + [ESLint](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint) + [Prettier](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode)
- Electron + electron-vite主进程/构建/开发)
- Vue 3 + Vite + TypeScript渲染进程 UI
- Elysia.js + @elysiajs/node(主进程内的本地后端 API
## Project Setup
## 架构说明
### Install
这个项目不是传统意义上“浏览器前端 + 远程后端”的部署形态,而是:
- 主进程Electron Main负责创建窗口并启动 Elysia.jsHTTP Server 绑定到 127.0.0.1 的随机端口)。
- 预加载Preload通过 `ipcRenderer.invoke('eiysia:request', ...)` 把“类 HTTP 请求”转发到主进程里的 Elysia 路由。
- 渲染进程Vue 3 Renderer通过 `window.api.call({ method, path, body })` 调用后端接口(例如 `/apps/list``/apps/launch``/open/external`)。
## 目录结构(关键)
- `src/main/`Electron 主进程入口(创建窗口、启动 Elysia 服务)
- `src/preload/`Preload 桥接层(暴露 `window.api`
- `src/renderer/`Vue 3 渲染进程UI 与交互)
- `src/eiysia/`Elysia.js “后端”路由与启动逻辑
## 推荐 IDE
- VSCode + VolarVue Language Features+ ESLint + Prettier
## 开发与构建
### 安装
```bash
$ pnpm install
```
### Development
### 开发
```bash
$ pnpm dev
```
### Build
### 构建
```bash
# For windows

View File

@@ -20,7 +20,8 @@
class="deskTile touchButton"
:class="[
tile.variant ? `deskTile--${tile.variant}` : '',
tile.id === 'writingPanel' ? 'deskTile--panel' : ''
(tile.id === 'writingPanel' || tile.id === 'notesPanel') ? 'deskTile--panel' : '',
tile.id === 'notesPanel' ? 'deskTile--notes' : ''
]"
:style="{
gridColumn: `${tile.col} / span ${tile.colSpan}`,
@@ -54,6 +55,21 @@
</button>
</div>
</template>
<template v-else-if="tile.id === 'notesPanel'">
<div class="notesWidget">
<div class="notesHeader">
<div class="notesHeaderTitle">作业版</div>
<button class="notesAddButton touchButton" type="button" @pointerup="addNote">
新建
</button>
</div>
<div class="notesGrid">
<div v-for="note in notes" :key="note.id" class="noteCard">
<textarea v-model="note.text" class="noteTextarea" placeholder="写点什么..." />
</div>
</div>
</div>
</template>
<template v-else>
<div class="deskTileRow">
<svg
@@ -73,18 +89,6 @@
</component>
</div>
</div>
<div class="notesPanel">
<div class="notesHeader">
<div class="notesHeaderTitle">作业版</div>
<button class="notesAddButton touchButton" type="button" @pointerup="addNote">新建</button>
</div>
<div class="notesGrid">
<div v-for="note in notes" :key="note.id" class="noteCard">
<textarea v-model="note.text" class="noteTextarea" placeholder="写点什么..." />
</div>
</div>
</div>
</div>
</div>
@@ -243,7 +247,7 @@
<path fill="currentColor" :d="iconPath(w.icon)" />
</svg>
<div class="homeWidgetCardTitle">{{ w.title }}</div>
<div class="homeWidgetCardMeta">{{ w.size === '2x4' ? '2×4' : w.size === '4x2' ? '4×2' : '2×2' }}</div>
<div class="homeWidgetCardMeta">{{ w.size }}</div>
</button>
</div>
</div>
@@ -271,8 +275,8 @@
<div class="settingsSection">
<div class="settingsSectionTitle">详细设置{{ widgetTitle(homeEditSelectedId) }}</div>
<div class="settingsConfigArea">
<template v-if="homeEditSelectedId === 'writingPanel'">
<div v-for="a in writingActions" :key="a.id" class="settingsSubAction">
<template v-if="homeEditSelectedId === 'writingPanel' || homeEditSelectedId === 'notesPanel'">
<div v-if="homeEditSelectedId === 'writingPanel'" v-for="a in writingActions" :key="a.id" class="settingsSubAction">
<div class="settingsSubActionTitle">{{ a.label }}</div>
<select
class="settingsSelect touchButton"
@@ -293,6 +297,9 @@
清空
</button>
</div>
<div v-else class="settingsSingleActionRow">
<div class="settingsActionHint">作业版目前不支持自定义点击行为</div>
</div>
</template>
<template v-else>
<div class="settingsSingleActionRow">
@@ -392,7 +399,7 @@ const notes = ref<Note[]>([])
type ActionKind = 'app' | 'url'
type ActionConfig = { kind: ActionKind; target: string }
type WidgetSize = '2x2' | '4x2' | '2x4'
type WidgetSize = '1x1' | '2x1' | '1x2' | '2x2' | '4x2' | '2x4' | '4x4' | '8x4'
type TileIcon = 'pen' | 'folder' | 'camera' | 'globe' | 'apps' | 'note' | 'doc' | 'comment'
type DesktopTile = {
id: string
@@ -456,19 +463,20 @@ let pageSwipeStartMainPage: MainPage = 'home'
let suppressTapUntil = 0
const availableWidgets: DesktopTile[] = [
{ id: 'writingPanel', title: '书写', size: '2x4', icon: 'pen', clickable: false, variant: 'soft' },
{ id: 'writingPanel', title: '书写', size: '2x4', icon: 'pen', clickable: false, variant: 'accent' },
{ id: 'notesPanel', title: '作业版', size: '4x4', icon: 'note', clickable: false, variant: 'soft' },
{
id: 'whiteboard',
title: '白板书写',
title: '白板',
size: '2x2',
icon: 'pen',
icon: 'doc',
clickable: true,
onActivate: () => void activateAction('whiteboard'),
variant: 'soft'
},
{
id: 'fileManager',
title: '文件管理',
title: '文件',
size: '2x2',
icon: 'folder',
clickable: true,
@@ -476,7 +484,7 @@ const availableWidgets: DesktopTile[] = [
},
{
id: 'visualPresenter',
title: '视频展台',
title: '展台',
size: '2x2',
icon: 'camera',
clickable: true,
@@ -492,18 +500,59 @@ const availableWidgets: DesktopTile[] = [
},
{
id: 'moreApps',
title: '更多应用',
title: '全部应用',
size: '2x2',
icon: 'apps',
clickable: true,
onActivate: () => void activateAction('moreApps', () => void openApps()),
variant: 'accent'
},
{
id: 'calculator',
title: '计算器',
size: '1x1',
icon: 'apps',
clickable: true,
onActivate: () => void activateAction('calculator')
},
{
id: 'calendar',
title: '日历',
size: '2x1',
icon: 'apps',
clickable: true,
onActivate: () => void activateAction('calendar')
},
{
id: 'timer',
title: '计时器',
size: '1x1',
icon: 'apps',
clickable: true,
onActivate: () => void activateAction('timer')
},
{
id: 'camera',
title: '相机',
size: '1x1',
icon: 'camera',
clickable: true,
onActivate: () => void activateAction('camera')
}
]
const widgetsOrder = ref<string[]>(['writingPanel', 'whiteboard', 'fileManager', 'visualPresenter', 'browser', 'moreApps'])
const widgetsOrder = ref<string[]>([
'writingPanel',
'notesPanel',
'whiteboard',
'fileManager',
'visualPresenter',
'browser',
'moreApps'
])
const widgetsEnabled = ref<Record<string, boolean>>({
writingPanel: true,
notesPanel: true,
whiteboard: true,
fileManager: true,
visualPresenter: true,
@@ -536,13 +585,36 @@ const enabledWidgets = computed<DesktopTile[]>(() => {
})
const placedDesktopTiles = computed<PlacedDesktopTile[]>(() => {
const cols = 4
const rows = 4
const cols = 8
const rows = 5
const occupied = Array.from({ length: rows }, () => Array.from({ length: cols }, () => false))
const tryPlace = (tile: DesktopTile): PlacedDesktopTile | null => {
const rowSpan = tile.size === '2x4' ? 4 : 2
const colSpan = tile.size === '4x2' ? 4 : 2
let rowSpan = 1
let colSpan = 1
if (tile.size === '8x4') {
rowSpan = 4
colSpan = 8
} else if (tile.size === '4x4') {
rowSpan = 4
colSpan = 4
} else if (tile.size === '2x4') {
rowSpan = 4
colSpan = 2
} else if (tile.size === '4x2') {
rowSpan = 2
colSpan = 4
} else if (tile.size === '2x2') {
rowSpan = 2
colSpan = 2
} else if (tile.size === '2x1') {
rowSpan = 1
colSpan = 2
} else if (tile.size === '1x2') {
rowSpan = 2
colSpan = 1
}
for (let r = 1; r <= rows - rowSpan + 1; r += 1) {
for (let c = 1; c <= cols - colSpan + 1; c += 1) {
let ok = true
@@ -579,7 +651,7 @@ const placedWidgetIds = computed(() => new Set(placedDesktopTiles.value.map((t)
const settingsWidgets = computed(() => {
return widgetsInOrder.value.map((w) => {
const sizeLabel = w.size === '2x4' ? '2×4' : w.size === '4x2' ? '4×2' : '2×2'
const sizeLabel = w.size
const enabled = widgetsEnabled.value[w.id] !== false
const visible = placedWidgetIds.value.has(w.id)
return { id: w.id, title: w.title, sizeLabel, enabled, visible }
@@ -931,9 +1003,18 @@ const handlePagePointerCancel = (event: PointerEvent): void => {
}
const restoreDefaultWidgets = (): void => {
widgetsOrder.value = ['writingPanel', 'whiteboard', 'fileManager', 'visualPresenter', 'browser', 'moreApps']
widgetsOrder.value = [
'writingPanel',
'notesPanel',
'whiteboard',
'fileManager',
'visualPresenter',
'browser',
'moreApps'
]
widgetsEnabled.value = {
writingPanel: true,
notesPanel: true,
whiteboard: true,
fileManager: true,
visualPresenter: true,

View File

@@ -55,19 +55,7 @@ body {
-webkit-tap-highlight-color: transparent;
}
.launcher {
position: fixed;
left: 16px;
top: 84px;
width: 520px;
height: calc(100vh - 168px);
padding: 12px;
background: rgba(0, 0, 0, 0.28);
border: 1px solid rgba(255, 255, 255, 0.12);
border-radius: 14px;
backdrop-filter: blur(12px);
box-sizing: border-box;
}
/* 移除旧的 .notesPanel 和 .launcher 相关样式,它们已不再使用 */
.home {
position: fixed;
@@ -81,20 +69,21 @@ body {
}
.desktopGridShell {
position: fixed;
position: absolute;
left: 16px;
right: 16px;
top: 88px;
bottom: 96px;
right: calc(50vw + 8px);
display: block;
box-sizing: border-box;
}
.desktopGrid {
display: grid;
grid-template-columns: repeat(4, minmax(160px, 1fr));
grid-template-rows: repeat(4, minmax(120px, 1fr));
grid-template-columns: repeat(8, minmax(100px, 1fr));
grid-template-rows: repeat(5, minmax(100px, 1fr));
gap: 12px;
height: 100%;
box-sizing: border-box;
}
@@ -104,7 +93,7 @@ body {
border: 1px solid rgba(255, 255, 255, 0.12);
background: rgba(0, 0, 0, 0.25);
color: var(--ev-c-text-1);
padding: 14px;
padding: 12px;
box-sizing: border-box;
display: flex;
flex-direction: column;
@@ -112,6 +101,50 @@ body {
align-items: flex-start;
text-align: left;
overflow: hidden;
position: relative;
}
.deskTile--notes {
padding: 0;
}
.notesWidget {
width: 100%;
height: 100%;
display: grid;
grid-template-rows: 48px 1fr;
overflow: hidden;
}
.notesWidget .notesHeader {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 12px;
background: rgba(255, 255, 255, 0.05);
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
}
.notesWidget .notesHeaderTitle {
font-size: 16px;
font-weight: 800;
}
.notesWidget .notesAddButton {
height: 32px;
padding: 0 12px;
font-size: 14px;
}
.notesWidget .notesGrid {
padding: 10px;
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
grid-auto-rows: 140px;
gap: 10px;
}
.notesWidget .noteCard {
background: rgba(255, 255, 255, 0.04);
}
button.deskTile {
@@ -143,22 +176,75 @@ button.deskTile {
.deskTileRow {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
justify-content: center;
gap: 6px;
padding: 8px;
box-sizing: border-box;
}
.deskTileIcon {
width: 34px;
height: 34px;
width: 28px;
height: 28px;
flex: 0 0 auto;
opacity: 0.95;
transition: transform 0.2s ease;
}
.deskTile:active .deskTileIcon {
transform: scale(0.9);
}
.deskTileText {
font-size: 20px;
line-height: 24px;
font-weight: 900;
font-size: 13px;
line-height: 1.2;
font-weight: 700;
text-align: center;
word-break: break-all;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
opacity: 0.9;
}
/* 针对较宽组件的特殊布局 (如 2x1, 4x2) */
.deskTile[style*="span 2"] .deskTileRow,
.deskTile[style*="span 4"] .deskTileRow,
.deskTile[style*="span 8"] .deskTileRow {
flex-direction: row;
justify-content: flex-start;
padding: 0 16px;
gap: 14px;
}
.deskTile[style*="span 2"] .deskTileIcon,
.deskTile[style*="span 4"] .deskTileIcon,
.deskTile[style*="span 8"] .deskTileIcon {
width: 32px;
height: 32px;
}
.deskTile[style*="span 2"] .deskTileText,
.deskTile[style*="span 4"] .deskTileText,
.deskTile[style*="span 8"] .deskTileText {
font-size: 16px;
text-align: left;
-webkit-line-clamp: 1;
}
/* 针对较高组件的特殊布局 (如 1x2, 2x4) */
.deskTile[style*="grid-row: span 2"] .deskTileRow,
.deskTile[style*="grid-row: span 4"] .deskTileRow {
justify-content: center;
}
/* 针对 1x1 极小组件的微调 */
.deskTile[style*="span 1 / span 1"] .deskTileText {
font-size: 12px;
}
.deskTile--panel {
@@ -176,8 +262,14 @@ button.deskTile {
.writingPanelButtons {
width: 100%;
display: grid;
grid-template-columns: 1fr;
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
gap: 10px;
overflow-y: auto;
scrollbar-width: none;
}
.writingPanelButtons::-webkit-scrollbar {
display: none;
}
.writingActionButton {
@@ -226,107 +318,77 @@ button.deskTile {
background: rgba(0, 0, 0, 0.18);
}
.notesPanel {
position: fixed;
top: 88px;
right: 16px;
bottom: 96px;
left: calc(50vw + 8px);
background: rgba(0, 0, 0, 0.28);
border: 1px solid rgba(255, 255, 255, 0.12);
border-radius: 14px;
backdrop-filter: blur(12px);
box-sizing: border-box;
/* 移除旧的绝对定位 notesPanel */
/* .notesPanel {...} 已被集成到 grid 中的 .deskTile--notes */
.notesWidget {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
overflow: hidden;
display: grid;
grid-template-rows: 56px 1fr;
background: rgba(0, 0, 0, 0.1);
border-radius: 14px;
}
.notesHeader {
.notesWidget .notesHeader {
flex: 0 0 48px;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 14px;
font-size: 18px;
line-height: 22px;
font-weight: 800;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
background: rgba(0, 0, 0, 0.2);
padding: 0 12px;
background: rgba(255, 255, 255, 0.05);
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
}
.notesHeaderTitle {
font-size: 18px;
line-height: 22px;
font-weight: 900;
}
.notesAddButton {
cursor: pointer;
border-radius: 14px;
padding: 0 14px;
border: 1px solid rgba(255, 255, 255, 0.12);
color: var(--ev-c-text-1);
background: rgba(0, 0, 0, 0.18);
.notesWidget .notesHeaderTitle {
font-size: 16px;
line-height: 18px;
font-weight: 800;
height: 40px;
}
.notesAddButton:hover {
border-color: rgba(255, 255, 255, 0.22);
background: rgba(0, 0, 0, 0.32);
.notesWidget .notesAddButton {
height: 32px;
padding: 0 12px;
font-size: 14px;
border-radius: 8px;
background: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.1);
color: white;
cursor: pointer;
}
.notesGrid {
height: 100%;
overflow: auto;
padding: 12px;
box-sizing: border-box;
.notesWidget .notesGrid {
flex: 1;
padding: 10px;
display: grid;
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
grid-auto-rows: 200px;
gap: 12px;
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
grid-auto-rows: 120px;
gap: 10px;
overflow-y: auto;
scrollbar-width: none;
-ms-overflow-style: none;
}
.notesGrid::-webkit-scrollbar {
width: 0;
height: 0;
.notesWidget .notesGrid::-webkit-scrollbar {
display: none;
}
.noteCard {
border-radius: 14px;
border: 1px solid rgba(255, 255, 255, 0.12);
background: rgba(0, 0, 0, 0.22);
.notesWidget .noteCard {
border-radius: 10px;
border: 1px solid rgba(255, 255, 255, 0.1);
background: rgba(255, 255, 255, 0.04);
overflow: hidden;
box-sizing: border-box;
}
.noteTextarea {
.notesWidget .noteTextarea {
width: 100%;
height: 100%;
resize: none;
border: none;
outline: none;
padding: 8px;
font-size: 14px;
background: transparent;
color: var(--ev-c-text-1);
padding: 12px;
box-sizing: border-box;
font-size: 16px;
line-height: 22px;
font-weight: 700;
user-select: text;
scrollbar-width: none;
-ms-overflow-style: none;
}
.noteTextarea::-webkit-scrollbar {
width: 0;
height: 0;
display: none;
border: none;
color: white;
resize: none;
outline: none;
}
.openAppsButton {