feat(ui): 添加窗口缩放功能支持

实现窗口缩放功能,允许用户在设置中选择70%-150%的界面缩放比例
添加相关IPC通信和设置项处理逻辑
更新设置界面添加缩放选项控件
This commit is contained in:
Linkon
2026-01-18 21:54:45 +08:00
parent e625ef0c50
commit e9f994b511
7 changed files with 180 additions and 37 deletions
+1
View File
@@ -161,6 +161,7 @@ app.whenReady().then(async () => {
if (logLevelSetting) {
ctx.logger.setLevel(logLevelSetting)
}
ctx.logger.info('Application starting...')
const mainConsole = console as any
+12 -1
View File
@@ -1,6 +1,6 @@
import { Service } from '../../shared/kernel'
import { MainContext } from '../context'
import { BrowserWindow } from 'electron'
import { BrowserWindow, webContents } from 'electron'
import type { IpcMainInvokeEvent } from 'electron'
import type { settingsKey, settingsSpec, settingChange } from '../../preload/types'
import type { permissionLevel } from './PermissionService'
@@ -48,6 +48,17 @@ export class SettingsService extends Service {
onChanged: (ctx, next) => {
ctx.logger.setLevel(next as any)
}
},
window_zoom: {
kind: 'number',
defaultValue: 1.0,
writePermission: 'admin',
onChanged: (_ctx, next) => {
const zoom = Number(next) || 1.0
webContents.getAllWebContents().forEach((wc: any) => {
wc.setZoomFactor(zoom)
})
}
}
}
+10
View File
@@ -72,6 +72,9 @@ export class WindowManager extends Service {
...input.options
})
const zoom = Number(this.mainCtx.settings.getValue('window_zoom')) || 1.0
win.webContents.setZoomFactor(zoom)
this.windows.set(input.key, win)
win.on('closed', () => {
this.windows.delete(input.key)
@@ -182,6 +185,13 @@ export class WindowManager extends Service {
return win ? win.isMaximized() : false
})
this.mainCtx.handle('window:set-zoom', (event, zoom: number) => {
const win = BrowserWindow.fromWebContents(event.sender)
if (win && zoom >= 0.5 && zoom <= 2.0) {
win.webContents.setZoomFactor(zoom)
}
})
this.mainCtx.handle('window:toggle-devtools', (event) => {
const win = BrowserWindow.fromWebContents(event.sender)
if (win) {
+4 -1
View File
@@ -86,7 +86,10 @@ export function ContentArea({
<Content style={{ flex: 1, overflowY: 'auto' }}>
<Routes>
<Route path="/" element={<Home canEdit={permission === 'admin' || permission === 'points'} />} />
<Route
path="/"
element={<Home canEdit={permission === 'admin' || permission === 'points'} />}
/>
<Route path="/students" element={<StudentManager canEdit={permission === 'admin'} />} />
<Route
path="/score"
+108 -33
View File
@@ -11,7 +11,7 @@ import {
InputNumber,
Divider
} from 'tdesign-react'
import { SearchIcon, AddIcon, MinusIcon, DeleteIcon } from 'tdesign-icons-react'
import { SearchIcon, DeleteIcon } from 'tdesign-icons-react'
import { match, pinyin } from 'pinyin-pro'
interface student {
@@ -128,7 +128,7 @@ export const Home: React.FC<{ canEdit: boolean }> = ({ canEdit }) => {
// 过滤和排序学生
const sortedStudents = useMemo(() => {
let filtered = students.filter((s) => matchStudentName(s.name, searchKeyword))
const filtered = students.filter((s) => matchStudentName(s.name, searchKeyword))
switch (sortType) {
case 'alphabet':
@@ -394,7 +394,9 @@ export const Home: React.FC<{ canEdit: boolean }> = ({ canEdit }) => {
}}
>
<span style={{ color: 'var(--td-brand-color)' }}>{group.key}</span>
<span style={{ fontSize: '12px', color: 'var(--ss-text-secondary)', fontWeight: 'normal' }}>
<span
style={{ fontSize: '12px', color: 'var(--ss-text-secondary)', fontWeight: 'normal' }}
>
({group.students.length} )
</span>
</div>
@@ -416,24 +418,27 @@ export const Home: React.FC<{ canEdit: boolean }> = ({ canEdit }) => {
const navContainerRef = useRef<HTMLDivElement>(null)
const isNavDragging = useRef(false)
const handleNavAction = useCallback((clientY: number) => {
if (!navContainerRef.current) return
const rect = navContainerRef.current.getBoundingClientRect()
const y = clientY - rect.top
const items = navContainerRef.current.children
const itemCount = items.length
if (itemCount === 0) return
const handleNavAction = useCallback(
(clientY: number) => {
if (!navContainerRef.current) return
const rect = navContainerRef.current.getBoundingClientRect()
const y = clientY - rect.top
const items = navContainerRef.current.children
const itemCount = items.length
if (itemCount === 0) return
// 计算当前指向第几个项
const itemHeight = rect.height / itemCount
const index = Math.floor(y / itemHeight)
const safeIndex = Math.max(0, Math.min(itemCount - 1, index))
// 计算当前指向第几个项
const itemHeight = rect.height / itemCount
const index = Math.floor(y / itemHeight)
const safeIndex = Math.max(0, Math.min(itemCount - 1, index))
const targetGroup = groupedStudents[safeIndex]
if (targetGroup) {
scrollToGroup(targetGroup.key)
}
}, [groupedStudents])
const targetGroup = groupedStudents[safeIndex]
if (targetGroup) {
scrollToGroup(targetGroup.key)
}
},
[groupedStudents]
)
const onNavMouseDown = (e: React.MouseEvent) => {
isNavDragging.current = true
@@ -476,7 +481,12 @@ export const Home: React.FC<{ canEdit: boolean }> = ({ canEdit }) => {
// 渲染快速导航
const renderQuickNav = () => {
if (groupedStudents.length <= 1 || sortType === 'score' || (sortType === 'alphabet' && searchKeyword)) return null
if (
groupedStudents.length <= 1 ||
sortType === 'score' ||
(sortType === 'alphabet' && searchKeyword)
)
return null
return (
<div
@@ -541,7 +551,9 @@ export const Home: React.FC<{ canEdit: boolean }> = ({ canEdit }) => {
}}
>
<div>
<h2 style={{ margin: 0, color: 'var(--ss-text-main)', fontSize: '24px' }}></h2>
<h2 style={{ margin: 0, color: 'var(--ss-text-main)', fontSize: '24px' }}>
</h2>
<p style={{ margin: '4px 0 0', color: 'var(--ss-text-secondary)', fontSize: '13px' }}>
{students.length}
</p>
@@ -595,7 +607,12 @@ export const Home: React.FC<{ canEdit: boolean }> = ({ canEdit }) => {
{searchKeyword ? '未找到匹配的学生' : '暂无学生数据,请前往学生管理添加'}
</div>
{searchKeyword && (
<Button variant="text" theme="primary" onClick={() => setSearchKeyword('')} style={{ marginTop: '8px' }}>
<Button
variant="text"
theme="primary"
onClick={() => setSearchKeyword('')}
style={{ marginTop: '8px' }}
>
</Button>
)}
@@ -649,9 +666,17 @@ export const Home: React.FC<{ canEdit: boolean }> = ({ canEdit }) => {
<span style={{ fontWeight: 600 }}>{selectedStudent.name}</span>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<span style={{ color: 'var(--ss-text-secondary)', fontSize: '13px' }}></span>
<span style={{ color: 'var(--ss-text-secondary)', fontSize: '13px' }}>
</span>
<Tag
theme={selectedStudent.score > 0 ? 'success' : selectedStudent.score < 0 ? 'danger' : 'default'}
theme={
selectedStudent.score > 0
? 'success'
: selectedStudent.score < 0
? 'danger'
: 'default'
}
variant="light"
style={{ fontWeight: 'bold' }}
>
@@ -663,7 +688,14 @@ export const Home: React.FC<{ canEdit: boolean }> = ({ canEdit }) => {
{/* 快捷理由 */}
{groupedReasons.length > 0 && (
<div>
<div style={{ marginBottom: '12px', display: 'flex', alignItems: 'center', gap: '8px' }}>
<div
style={{
marginBottom: '12px',
display: 'flex',
alignItems: 'center',
gap: '8px'
}}
>
<span style={{ fontWeight: 600, fontSize: '14px' }}></span>
<Divider style={{ flex: 1, margin: 0 }} />
</div>
@@ -697,14 +729,24 @@ export const Home: React.FC<{ canEdit: boolean }> = ({ canEdit }) => {
size="small"
onClick={() => handleReasonSelect(r)}
style={{
borderColor: r.delta > 0 ? 'var(--td-success-color-3)' : r.delta < 0 ? 'var(--td-error-color-3)' : undefined
borderColor:
r.delta > 0
? 'var(--td-success-color-3)'
: r.delta < 0
? 'var(--td-error-color-3)'
: undefined
}}
>
{r.content}{' '}
<span
style={{
marginLeft: '4px',
color: r.delta > 0 ? 'var(--td-success-color)' : r.delta < 0 ? 'var(--td-error-color)' : 'inherit',
color:
r.delta > 0
? 'var(--td-success-color)'
: r.delta < 0
? 'var(--td-error-color)'
: 'inherit',
fontWeight: 'bold'
}}
>
@@ -721,7 +763,9 @@ export const Home: React.FC<{ canEdit: boolean }> = ({ canEdit }) => {
{/* 自定义分值 */}
<div>
<div style={{ marginBottom: '12px', display: 'flex', alignItems: 'center', gap: '8px' }}>
<div
style={{ marginBottom: '12px', display: 'flex', alignItems: 'center', gap: '8px' }}
>
<span style={{ fontWeight: 600, fontSize: '14px' }}></span>
<Divider style={{ flex: 1, margin: 0 }} />
</div>
@@ -765,7 +809,9 @@ export const Home: React.FC<{ canEdit: boolean }> = ({ canEdit }) => {
{/* 理由内容 */}
<div>
<div style={{ marginBottom: '12px', display: 'flex', alignItems: 'center', gap: '8px' }}>
<div
style={{ marginBottom: '12px', display: 'flex', alignItems: 'center', gap: '8px' }}
>
<span style={{ fontWeight: 600, fontSize: '14px' }}></span>
<Divider style={{ flex: 1, margin: 0 }} />
</div>
@@ -773,7 +819,14 @@ export const Home: React.FC<{ canEdit: boolean }> = ({ canEdit }) => {
value={reasonContent}
onChange={setReasonContent}
placeholder="输入加分/扣分的原因(可选)"
suffixIcon={reasonContent ? <DeleteIcon onClick={() => setReasonContent('')} style={{ cursor: 'pointer' }} /> : undefined}
suffixIcon={
reasonContent ? (
<DeleteIcon
onClick={() => setReasonContent('')}
style={{ cursor: 'pointer' }}
/>
) : undefined
}
/>
</div>
@@ -782,18 +835,40 @@ export const Home: React.FC<{ canEdit: boolean }> = ({ canEdit }) => {
<div
style={{
padding: '16px',
backgroundColor: customScore > 0 ? 'var(--td-success-color-1)' : customScore < 0 ? 'var(--td-error-color-1)' : 'var(--ss-bg-color)',
backgroundColor:
customScore > 0
? 'var(--td-success-color-1)'
: customScore < 0
? 'var(--td-error-color-1)'
: 'var(--ss-bg-color)',
borderRadius: '8px',
border: `1px solid ${customScore > 0 ? 'var(--td-success-color-2)' : customScore < 0 ? 'var(--td-error-color-2)' : 'var(--ss-border-color)'}`,
marginTop: '4px'
}}
>
<div style={{ fontSize: '13px', fontWeight: 600, marginBottom: '4px', color: 'var(--ss-text-main)' }}>
<div
style={{
fontSize: '13px',
fontWeight: 600,
marginBottom: '4px',
color: 'var(--ss-text-main)'
}}
>
</div>
<div style={{ fontSize: '15px' }}>
{selectedStudent.name}{' '}
<span style={{ fontWeight: 'bold', color: customScore > 0 ? 'var(--td-success-color)' : customScore < 0 ? 'var(--td-error-color)' : 'inherit' }}>
<span
style={{
fontWeight: 'bold',
color:
customScore > 0
? 'var(--td-success-color)'
: customScore < 0
? 'var(--td-error-color)'
: 'inherit'
}}
>
{customScore > 0 ? `+${customScore}` : customScore}
</span>{' '}
+37 -1
View File
@@ -18,6 +18,7 @@ type permissionLevel = 'admin' | 'points' | 'view'
type appSettings = {
is_wizard_completed: boolean
log_level: 'debug' | 'info' | 'warn' | 'error'
window_zoom?: string
}
export const Settings: React.FC<{ permission: permissionLevel }> = ({ permission }) => {
@@ -25,7 +26,8 @@ export const Settings: React.FC<{ permission: permissionLevel }> = ({ permission
const [activeTab, setActiveTab] = useState('appearance')
const [settings, setSettings] = useState<appSettings>({
is_wizard_completed: false,
log_level: 'info'
log_level: 'info',
window_zoom: '1.0'
})
const [securityStatus, setSecurityStatus] = useState<{
@@ -91,6 +93,7 @@ export const Settings: React.FC<{ permission: permissionLevel }> = ({ permission
if (change?.key === 'log_level') return { ...prev, log_level: change.value }
if (change?.key === 'is_wizard_completed')
return { ...prev, is_wizard_completed: change.value }
if (change?.key === 'window_zoom') return { ...prev, window_zoom: change.value }
return prev
})
})
@@ -268,6 +271,39 @@ export const Settings: React.FC<{ permission: permissionLevel }> = ({ permission
))}
</Select>
</Form.FormItem>
<Form.FormItem label="界面缩放">
<Select
value={settings.window_zoom || '1.0'}
onChange={async (v) => {
if (!(window as any).api) return
const next = String(v)
const res = await (window as any).api.setSetting('window_zoom', next)
if (res.success) {
setSettings((prev) => ({ ...prev, window_zoom: next }))
MessagePlugin.success('界面缩放已更新')
} else {
MessagePlugin.error(res.message || '更新失败')
}
}}
style={{ width: '320px' }}
disabled={!canAdmin}
>
<Select.Option value="0.7" label="70% (较小)" />
<Select.Option value="0.8" label="80%" />
<Select.Option value="0.9" label="90%" />
<Select.Option value="1.0" label="100% (默认)" />
<Select.Option value="1.1" label="110%" />
<Select.Option value="1.2" label="120%" />
<Select.Option value="1.3" label="130%" />
<Select.Option value="1.5" label="150% (较大)" />
</Select>
<div
style={{ marginTop: '4px', fontSize: '12px', color: 'var(--ss-text-secondary)' }}
>
</div>
</Form.FormItem>
</Form>
</Card>
</Tabs.TabPanel>
+8 -1
View File
@@ -1,5 +1,12 @@
import { Layout, Menu } from 'tdesign-react'
import { UserIcon, SettingIcon, HistoryIcon, RootListIcon, ViewListIcon, HomeIcon } from 'tdesign-icons-react'
import {
UserIcon,
SettingIcon,
HistoryIcon,
RootListIcon,
ViewListIcon,
HomeIcon
} from 'tdesign-icons-react'
import appLogo from '../assets/logo.svg'
const { Aside } = Layout