mirror of
https://github.com/SECTL/SecScore.git
synced 2026-07-19 21:14:21 +08:00
feat(ui): 添加窗口缩放功能支持
实现窗口缩放功能,允许用户在设置中选择70%-150%的界面缩放比例 添加相关IPC通信和设置项处理逻辑 更新设置界面添加缩放选项控件
This commit is contained in:
@@ -161,6 +161,7 @@ app.whenReady().then(async () => {
|
||||
if (logLevelSetting) {
|
||||
ctx.logger.setLevel(logLevelSetting)
|
||||
}
|
||||
|
||||
ctx.logger.info('Application starting...')
|
||||
|
||||
const mainConsole = console as any
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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,7 +418,8 @@ export const Home: React.FC<{ canEdit: boolean }> = ({ canEdit }) => {
|
||||
const navContainerRef = useRef<HTMLDivElement>(null)
|
||||
const isNavDragging = useRef(false)
|
||||
|
||||
const handleNavAction = useCallback((clientY: number) => {
|
||||
const handleNavAction = useCallback(
|
||||
(clientY: number) => {
|
||||
if (!navContainerRef.current) return
|
||||
const rect = navContainerRef.current.getBoundingClientRect()
|
||||
const y = clientY - rect.top
|
||||
@@ -433,7 +436,9 @@ export const Home: React.FC<{ canEdit: boolean }> = ({ canEdit }) => {
|
||||
if (targetGroup) {
|
||||
scrollToGroup(targetGroup.key)
|
||||
}
|
||||
}, [groupedStudents])
|
||||
},
|
||||
[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>{' '}
|
||||
分
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user