Add files

This commit is contained in:
hello8693 2025-02-01 20:01:41 +08:00
commit 0d7e7f2ae0
43 changed files with 7313 additions and 0 deletions

9
.editorconfig Normal file
View File

@ -0,0 +1,9 @@
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 2
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true

4
.eslintignore Normal file
View File

@ -0,0 +1,4 @@
node_modules
dist
out
.gitignore

17
.eslintrc.cjs Normal file
View File

@ -0,0 +1,17 @@
/* eslint-env node */
require('@rushstack/eslint-patch/modern-module-resolution')
module.exports = {
extends: [
'eslint:recommended',
'plugin:vue/vue3-recommended',
'@electron-toolkit',
'@electron-toolkit/eslint-config-ts/eslint-recommended',
'@vue/eslint-config-typescript/recommended',
'@vue/eslint-config-prettier'
],
rules: {
'vue/require-default-prop': 'off',
'vue/multi-word-component-names': 'off'
}
}

5
.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
node_modules
dist
out
.DS_Store
*.log*

3
.npmrc Normal file
View File

@ -0,0 +1,3 @@
electron_mirror=https://npmmirror.com/mirrors/electron/
electron_builder_binaries_mirror=https://npmmirror.com/mirrors/electron-builder-binaries/
shamefully-hoist=true

6
.prettierignore Normal file
View File

@ -0,0 +1,6 @@
out
dist
pnpm-lock.yaml
LICENSE.md
tsconfig.json
tsconfig.*.json

4
.prettierrc.yaml Normal file
View File

@ -0,0 +1,4 @@
singleQuote: true
semi: false
printWidth: 100
trailingComma: none

3
.vscode/extensions.json vendored Normal file
View File

@ -0,0 +1,3 @@
{
"recommendations": ["dbaeumer.vscode-eslint"]
}

39
.vscode/launch.json vendored Normal file
View File

@ -0,0 +1,39 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "Debug Main Process",
"type": "node",
"request": "launch",
"cwd": "${workspaceRoot}",
"runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron-vite",
"windows": {
"runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron-vite.cmd"
},
"runtimeArgs": ["--sourcemap"],
"env": {
"REMOTE_DEBUGGING_PORT": "9222"
}
},
{
"name": "Debug Renderer Process",
"port": 9222,
"request": "attach",
"type": "chrome",
"webRoot": "${workspaceFolder}/src/renderer",
"timeout": 60000,
"presentation": {
"hidden": true
}
}
],
"compounds": [
{
"name": "Debug All",
"configurations": ["Debug Main Process", "Debug Renderer Process"],
"presentation": {
"order": 1
}
}
]
}

11
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,11 @@
{
"[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[javascript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[json]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
}
}

34
README.md Normal file
View File

@ -0,0 +1,34 @@
# examaware2-desktop
An Electron application with Vue and TypeScript
## 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) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) + [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin)
## Project Setup
### Install
```bash
$ pnpm install
```
### Development
```bash
$ pnpm dev
```
### Build
```bash
# For windows
$ pnpm build:win
# For macOS
$ pnpm build:mac
# For Linux
$ pnpm build:linux
```

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.cs.allow-jit</key>
<true/>
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
<true/>
<key>com.apple.security.cs.allow-dyld-environment-variables</key>
<true/>
</dict>
</plist>

BIN
build/icon.icns Normal file

Binary file not shown.

BIN
build/icon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 121 KiB

BIN
build/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

3
dev-app-update.yml Normal file
View File

@ -0,0 +1,3 @@
provider: generic
url: https://example.com/auto-updates
updaterCacheDirName: examaware2-desktop-updater

45
electron-builder.yml Normal file
View File

@ -0,0 +1,45 @@
appId: com.electron.app
productName: examaware2-desktop
directories:
buildResources: build
files:
- '!**/.vscode/*'
- '!src/*'
- '!electron.vite.config.{js,ts,mjs,cjs}'
- '!{.eslintignore,.eslintrc.cjs,.prettierignore,.prettierrc.yaml,dev-app-update.yml,CHANGELOG.md,README.md}'
- '!{.env,.env.*,.npmrc,pnpm-lock.yaml}'
- '!{tsconfig.json,tsconfig.node.json,tsconfig.web.json}'
asarUnpack:
- resources/**
win:
executableName: examaware2-desktop
nsis:
artifactName: ${name}-${version}-setup.${ext}
shortcutName: ${productName}
uninstallDisplayName: ${productName}
createDesktopShortcut: always
mac:
entitlementsInherit: build/entitlements.mac.plist
extendInfo:
- NSCameraUsageDescription: Application requests access to the device's camera.
- NSMicrophoneUsageDescription: Application requests access to the device's microphone.
- NSDocumentsFolderUsageDescription: Application requests access to the user's Documents folder.
- NSDownloadsFolderUsageDescription: Application requests access to the user's Downloads folder.
notarize: false
dmg:
artifactName: ${name}-${version}.${ext}
linux:
target:
- AppImage
- snap
- deb
maintainer: electronjs.org
category: Utility
appImage:
artifactName: ${name}-${version}.${ext}
npmRebuild: false
publish:
provider: generic
url: https://example.com/auto-updates
electronDownload:
mirror: https://npmmirror.com/mirrors/electron/

44
electron.vite.config.ts Normal file
View File

@ -0,0 +1,44 @@
import { resolve } from 'path'
import { defineConfig, externalizeDepsPlugin } from 'electron-vite'
import vue from '@vitejs/plugin-vue'
import vueJsx from '@vitejs/plugin-vue-jsx'
import vueDevTools from 'vite-plugin-vue-devtools'
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { TDesignResolver } from 'unplugin-vue-components/resolvers'
export default defineConfig({
main: {
plugins: [externalizeDepsPlugin()]
},
preload: {
plugins: [externalizeDepsPlugin()]
},
renderer: {
resolve: {
alias: {
'@renderer': resolve('src/renderer/src')
}
},
plugins: [
vue(),
vueJsx(),
vueDevTools(),
AutoImport({
resolvers: [
TDesignResolver({
library: 'vue-next'
})
]
}),
Components({
resolvers: [
TDesignResolver({
library: 'vue-next'
})
]
})
]
}
})

67
package.json Normal file
View File

@ -0,0 +1,67 @@
{
"name": "examaware2-desktop",
"version": "2.0.0",
"description": "DSZ知试",
"main": "./out/main/index.js",
"author": "hello8693 <hello8693@hello8693.xyz>",
"homepage": "https://dsz.hello8693.xyz/",
"scripts": {
"format": "prettier --write src/",
"lint": "eslint . --ext .js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts,.vue --fix",
"typecheck:node": "tsc --noEmit -p tsconfig.node.json --composite false",
"typecheck:web": "vue-tsc --noEmit -p tsconfig.web.json --composite false",
"typecheck": "npm run typecheck:node && npm run typecheck:web",
"start": "electron-vite preview",
"dev": "electron-vite dev",
"build": "npm run typecheck && electron-vite build",
"postinstall": "electron-builder install-app-deps",
"build:unpack": "npm run build && electron-builder --dir",
"build:win": "npm run build && electron-builder --win",
"build:mac": "npm run build && electron-builder --mac",
"build:linux": "npm run build && electron-builder --linux"
},
"dependencies": {
"@electron-toolkit/preload": "^3.0.0",
"@electron-toolkit/utils": "^3.0.0",
"electron-updater": "^6.1.7"
},
"devDependencies": {
"@electron-toolkit/eslint-config": "^1.0.2",
"@electron-toolkit/eslint-config-ts": "^2.0.0",
"@electron-toolkit/tsconfig": "^1.0.1",
"@rushstack/eslint-patch": "^1.10.3",
"electron": "^31.0.2",
"electron-builder": "^24.13.3",
"electron-vite": "^2.3.0",
"@imengyu/vue3-context-menu": "^1.4.4",
"misans": "^4.0.0",
"moment": "^2.30.1",
"pinia": "^2.3.1",
"tdesign-icons-vue-next": "^0.3.4",
"tdesign-vue-next": "^1.10.7",
"unplugin-auto-import": "^19.0.0",
"unplugin-vue-components": "^28.0.0",
"vue": "^3.5.13",
"vue-code-layout": "^1.1.2",
"vue-router": "^4.5.0",
"@tsconfig/node22": "^22.0.0",
"@types/node": "^22.10.7",
"@vitejs/plugin-vue": "^5.2.1",
"@vitejs/plugin-vue-jsx": "^4.1.1",
"@vue/eslint-config-prettier": "^10.1.0",
"@vue/eslint-config-typescript": "^14.3.0",
"@vue/tsconfig": "^0.7.0",
"eslint": "^9.18.0",
"eslint-plugin-oxlint": "^0.15.6",
"eslint-plugin-vue": "^9.32.0",
"jiti": "^2.4.2",
"npm-run-all2": "^7.0.2",
"oxlint": "^0.15.6",
"prettier": "^3.4.2",
"typescript": "~5.7.3",
"vite": "^6.0.11",
"vite-plugin-vue-devtools": "^7.7.0",
"vue-tsc": "^2.2.0"
}
}

6041
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

BIN
resources/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

80
src/main/index.ts Normal file
View File

@ -0,0 +1,80 @@
import { app, shell, BrowserWindow, ipcMain } from 'electron'
import { join } from 'path'
import { electronApp, optimizer, is } from '@electron-toolkit/utils'
import icon from '../../resources/icon.png?asset'
function createWindow(): void {
// Create the browser window.
const mainWindow = new BrowserWindow({
width: 960,
height: 700,
show: false,
autoHideMenuBar: true,
...(process.platform === 'linux' ? { icon } : {}),
webPreferences: {
preload: join(__dirname, '../preload/index.js'),
sandbox: false
},
// titleBarStyle: 'hidden',
// titleBarOverlay: {
// color: 'rgba(0,0,0,0)',
// height: 35,
// symbolColor: 'white'
// }
})
mainWindow.on('ready-to-show', () => {
mainWindow.show()
})
mainWindow.webContents.setWindowOpenHandler((details) => {
shell.openExternal(details.url)
return { action: 'deny' }
})
// HMR for renderer base on electron-vite cli.
// Load the remote URL for development or the local html file for production.
if (is.dev && process.env['ELECTRON_RENDERER_URL']) {
mainWindow.loadURL(process.env['ELECTRON_RENDERER_URL'])
} else {
mainWindow.loadFile(join(__dirname, '../renderer/index.html#'))
}
}
// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs.
app.whenReady().then(() => {
// Set app user model id for windows
electronApp.setAppUserModelId('com.electron')
// Default open or close DevTools by F12 in development
// and ignore CommandOrControl + R in production.
// see https://github.com/alex8088/electron-toolkit/tree/master/packages/utils
app.on('browser-window-created', (_, window) => {
optimizer.watchWindowShortcuts(window)
})
// IPC test
ipcMain.on('ping', () => console.log('pong'))
createWindow()
app.on('activate', function () {
// On macOS it's common to re-create a window in the app when the
// dock icon is clicked and there are no other windows open.
if (BrowserWindow.getAllWindows().length === 0) createWindow()
})
})
// Quit when all windows are closed, except on macOS. There, it's common
// for applications and their menu bar to stay active until the user quits
// explicitly with Cmd + Q.
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit()
}
})
// In this file you can include the rest of your app"s specific main process
// code. You can also put them in separate files and require them here.

8
src/preload/index.d.ts vendored Normal file
View File

@ -0,0 +1,8 @@
import { ElectronAPI } from '@electron-toolkit/preload'
declare global {
interface Window {
electron: ElectronAPI
api: unknown
}
}

22
src/preload/index.ts Normal file
View File

@ -0,0 +1,22 @@
import { contextBridge } from 'electron'
import { electronAPI } from '@electron-toolkit/preload'
// Custom APIs for renderer
const api = {}
// Use `contextBridge` APIs to expose Electron APIs to
// renderer only if context isolation is enabled, otherwise
// just add to the DOM global.
if (process.contextIsolated) {
try {
contextBridge.exposeInMainWorld('electron', electronAPI)
contextBridge.exposeInMainWorld('api', api)
} catch (error) {
console.error(error)
}
} else {
// @ts-ignore (define in dts)
window.electron = electronAPI
// @ts-ignore (define in dts)
window.api = api
}

10
src/renderer/auto-imports.d.ts vendored Normal file
View File

@ -0,0 +1,10 @@
/* eslint-disable */
/* prettier-ignore */
// @ts-nocheck
// noinspection JSUnusedGlobalSymbols
// Generated by unplugin-auto-import
// biome-ignore lint: disable
export {}
declare global {
}

26
src/renderer/components.d.ts vendored Normal file
View File

@ -0,0 +1,26 @@
/* eslint-disable */
// @ts-nocheck
// Generated by unplugin-vue-components
// Read more: https://github.com/vuejs/core/pull/3399
export {}
/* prettier-ignore */
declare module 'vue' {
export interface GlobalComponents {
AboutDialog: typeof import('./src/components/AboutDialog.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
SideExamInfoPanel: typeof import('./src/components/SideExamInfoPanel.vue')['default']
SideExamsPanel: typeof import('./src/components/SideExamsPanel.vue')['default']
TDatePicker: typeof import('tdesign-vue-next')['DatePicker']
TDialog: typeof import('tdesign-vue-next')['Dialog']
TForm: typeof import('tdesign-vue-next')['Form']
TFormItem: typeof import('tdesign-vue-next')['FormItem']
TInput: typeof import('tdesign-vue-next')['Input']
TInputNumber: typeof import('tdesign-vue-next')['InputNumber']
TLink: typeof import('tdesign-vue-next')['Link']
TList: typeof import('tdesign-vue-next')['List']
TListItem: typeof import('tdesign-vue-next')['ListItem']
TListItemMeta: typeof import('tdesign-vue-next')['ListItemMeta']
}
}

13
src/renderer/index.html Normal file
View File

@ -0,0 +1,13 @@
<!doctype html>
<html lang="">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>DSZ ExamAware</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

7
src/renderer/src/App.vue Normal file
View File

@ -0,0 +1,7 @@
<script setup lang="ts">
import { RouterLink, RouterView } from 'vue-router'
</script>
<template>
<RouterView />
</template>

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 31 KiB

View File

@ -0,0 +1,44 @@
html,
body,
#app {
height: 100%;
margin: 0;
padding: 0;
font-family: 'Misans', sans-serif;
overflow: hidden;
}
.root {
/* 使用 flex 来实现 */
display: flex;
flex-direction: column;
height: 100%;
background-color: black;
color: white;
}
.code-layout-title-bar {
/* display: flex;
align-items: center; */
/* 避免被收缩 */
flex-shrink: 0;
/* 高度与 main.js 中 titleBarOverlay.height 一致 */
height: 35px;
width: 100%;
/* 标题栏始终在最顶层(避免后续被 Modal 之类的覆盖) */
/* z-index: 9999; */
/* padding-left: 4px; */
/* font-size: 14px; */
-webkit-app-region: drag !important;
}
#app > div.code-layout-root > div.code-layout-title-bar > div:nth-child(2) {
-webkit-app-region: drag;
}
/*
.content {
overflow: auto;
}
*/

View File

@ -0,0 +1,95 @@
<template>
<t-dialog
v-bind:visible="visible"
theme="info"
header="关于 DSZ知试"
:cancel-btn="{
content: '确定',
variant: 'outline',
}"
:confirm-btn="{
content: '复制',
variant: 'base',
}"
:on-close="close1"
:on-confirm="copyToClipboard"
>
<template #body>
<t-list :split="true">
<t-list-item>
<t-list-item-meta title="App 版本" :description="versionInfo.appVersion" />
</t-list-item>
<t-list-item>
<t-list-item-meta title="浏览器" :description="versionInfo.browserVersion" />
</t-list-item>
<t-list-item>
<t-list-item-meta title="User Agent" :description="versionInfo.userAgent" />
</t-list-item>
</t-list>
</template>
</t-dialog>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import { NotifyPlugin } from 'tdesign-vue-next'
import packageJson from '../../../../package.json'
const versionInfo = ref({
appVersion: packageJson.version,
userAgent: navigator.userAgent,
browserVersion: (() => {
const ua = navigator.userAgent
let tem,
M = ua.match(/(opera|chrome|safari|firefox|msie|trident(?=\/))\/?\s*(\d+)/i) || []
if (/trident/i.test(M[1])) {
tem = /\brv[ :]+(\d+)/g.exec(ua) || []
return `IE ${tem[1] || ''}`
}
if (M[1] === 'Chrome') {
tem = ua.match(/\b(OPR|Edge)\/(\d+)/)
if (tem != null) return tem.slice(1).join(' ').replace('OPR', 'Opera')
}
M = M[2] ? [M[1], M[2]] : [navigator.appName, navigator.appVersion, '-?']
if ((tem = ua.match(/version\/(\d+)/i)) != null) M.splice(1, 1, tem[1])
return M.join(' ')
})(),
})
const readableVersionInfo = computed(() => {
return `About DSZ ExamAware\n============\nApp Version: ${versionInfo.value.appVersion}\nBrowser: ${versionInfo.value.browserVersion}\nUser Agent: ${versionInfo.value.userAgent}`
})
const props = defineProps({
visible: Boolean,
})
const emit = defineEmits(['update:closedialog'])
function close1() {
console.log('close1')
emit('update:closedialog')
}
function copyToClipboard() {
navigator.clipboard
.writeText(readableVersionInfo.value)
.then(() => {
NotifyPlugin.success({
title: '复制成功',
content: '“关于”信息已复制到您的剪贴板',
placement: 'bottom-right',
closeBtn: true,
})
})
.catch((err) => {
NotifyPlugin.error({
title: '复制失败',
content: err,
placement: 'bottom-right',
closeBtn: true,
})
})
}
</script>

View File

@ -0,0 +1,52 @@
<template>
<div class="exam_info_editor">
<t-form labelAlign="top">
<t-form-item label="考试名称" name="examName">
<t-input v-model="localProfile.examName"></t-input>
</t-form-item>
<t-form-item label="考试信息" name="message">
<t-input v-model="localProfile.message"></t-input>
</t-form-item>
<t-form-item label="考场名称" name="room">
<t-input v-model="localProfile.room"></t-input>
</t-form-item>
</t-form>
</div>
</template>
<script setup lang="ts">
import { defineProps, defineEmits, watch, ref } from 'vue'
import type { ExamConfig } from '@renderer/core/configTypes'
const props = defineProps({
profile: Object as () => ExamConfig,
})
const emit = defineEmits(['update:profile'])
const localProfile = ref({ ...props.profile })
watch(
() => props.profile,
(newProfile) => {
localProfile.value = { ...newProfile }
},
{ deep: true },
)
watch(
localProfile,
(newProfile) => {
console.log('更新考试信息', newProfile)
emit('update:profile', newProfile)
},
{ deep: true },
)
</script>
<style scoped>
.exam_info_editor {
padding: 20px;
overflow: auto;
}
</style>

View File

@ -0,0 +1,73 @@
<template>
<div>
<t-list v-for="(item, index) in localProfile.examInfos" :key="index" :split="true">
<t-list-item>
{{ item.name }}
<template #action>
<t-link
theme="primary"
hover="color"
style="margin-left: 16px"
@click="switchExamInfo(index)"
>
编辑
</t-link>
<t-link theme="danger" hover="color" style="margin-left: 16px" @click="deleteExam(index)">
删除
</t-link>
</template>
</t-list-item>
</t-list>
</div>
</template>
<script setup lang="ts">
import { defineEmits, watch, ref } from 'vue'
import type { ExamConfig } from '@renderer/core/configTypes'
import { ArrowUpIcon, ArrowDownIcon } from 'tdesign-icons-vue-next'
const props = defineProps({
profile: Object as () => ExamConfig,
})
const emit = defineEmits(['switch-exam-info', 'update:profile'])
function switchExamInfo(examId: number) {
emit('switch-exam-info', { examId }) // examInfo ID
}
function deleteExam(examId: number) {
if (localProfile.value.examInfos) {
localProfile.value.examInfos.splice(examId, 1)
}
emit('update:profile', localProfile.value)
emit('switch-exam-info', { examId: null }) // currentExamIndex null
}
const localProfile = ref({ ...props.profile })
watch(
localProfile,
(newProfile) => {
emit('update:profile', newProfile)
},
{ deep: true },
)
watch(
() => props.profile,
(newProfile) => {
localProfile.value = { ...newProfile }
},
{ deep: true },
)
watch(
localProfile,
(newProfile) => {
console.log('更新考试信息', newProfile)
emit('update:profile', newProfile)
},
{ deep: true },
)
</script>

View File

@ -0,0 +1,31 @@
export interface ExamInfo {
name: string
start: string
end: string
alertTime: number // 考试结束前几分钟提醒
}
/**
* Represents the configuration for an exam.
*/
export interface ExamConfig {
/**
* The name of the exam.
*/
examName: string
/**
* A message related to the exam.
*/
message: string
/**
* The room where the exam will take place.
*/
room: string
/**
* An array of information related to the exam.
*/
examInfos: ExamInfo[]
}

View File

@ -0,0 +1,52 @@
import type { ExamConfig } from './configTypes'
/**
* JSON `ExamConfig`
*
* @param jsonString - JSON
* @returns `examInfos` `ExamConfig` `null`
*/
export function parseExamConfig(jsonString: string): ExamConfig | null {
try {
const data = JSON.parse(jsonString)
if (!data.examInfos) return null
return data as ExamConfig
} catch {
return null
}
}
export function validateExamConfig(config: ExamConfig): boolean {
if (!config.examName || !config.examInfos?.length) return false
return config.examInfos.every((info) => info.name && info.start && info.end)
}
/**
*
*
* @param config -
* @returns true false
*/
export function hasExamTimeOverlap(config: ExamConfig): boolean {
const sortedExams = config.examInfos
.slice()
.sort((a, b) => new Date(a.start).getTime() - new Date(b.start).getTime())
for (let i = 0; i < sortedExams.length - 1; i++) {
if (new Date(sortedExams[i].end).getTime() > new Date(sortedExams[i + 1].start).getTime()) {
return true
}
}
return false
}
/**
*
*
* @param config -
* @returns
*/
export function getSortedExamInfos(config: ExamConfig) {
return config.examInfos
.slice()
.sort((a, b) => new Date(a.start).getTime() - new Date(b.start).getTime())
}

28
src/renderer/src/main.ts Normal file
View File

@ -0,0 +1,28 @@
import 'misans/lib/Normal/MiSans-Normal.min.css'
// import 'misans/lib/Normal/MiSans-Thin.min.css'
// import 'misans/lib/Normal/MiSans-Semibold.min.css'
import './assets/main.css'
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
import router from './router'
import 'vue-code-layout/lib/vue-code-layout.css'
import CodeLayout from 'vue-code-layout'
// import TDesign from 'tdesign-vue-next'
import 'tdesign-vue-next/es/style/index.css'
const app = createApp(App)
app.use(createPinia())
app.use(router)
app.use(CodeLayout)
// app.use(TDesign)
document.documentElement.setAttribute('theme-mode', 'dark')
app.mount('#app')

View File

@ -0,0 +1,16 @@
import { createRouter, createWebHashHistory } from 'vue-router'
import HomeView from '../views/EditorView.vue'
const router = createRouter({
history: createWebHashHistory(import.meta.env.BASE_URL),
routes: [
{
path: '/',
name: 'home',
component: HomeView,
},
],
})
export default router

View File

@ -0,0 +1,12 @@
import { ref, computed } from 'vue'
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', () => {
const count = ref(0)
const doubleCount = computed(() => count.value * 2)
function increment() {
count.value++
}
return { count, doubleCount, increment }
})

View File

@ -0,0 +1,313 @@
<script setup lang="ts">
import {
type CodeLayoutConfig,
type CodeLayoutInstance,
defaultCodeLayoutConfig,
} from 'vue-code-layout'
import { ref, reactive, h, onMounted, nextTick } from 'vue'
import { FileIcon, SearchIcon, InfoCircleIcon, AddIcon } from 'tdesign-icons-vue-next'
import SideExamsPanel from '@renderer/components/SideExamsPanel.vue'
import type { Component } from 'vue'
import type { ExamConfig } from '@renderer/core/configTypes'
import type { MenuOptions } from '@imengyu/vue3-context-menu'
import { parseExamConfig, getSortedExamInfos } from '@renderer/core/parser'
import AboutDialog from '@renderer/components/AboutDialog.vue'
import SideExamInfoPanel from '@renderer/components/SideExamInfoPanel.vue'
// CodeLayout
const config = reactive<CodeLayoutConfig>({
...defaultCodeLayoutConfig,
primarySideBarSwitchWithActivityBar: true,
primarySideBarPosition: 'left',
bottomAlignment: 'center',
titleBar: true,
titleBarShowCustomizeLayout: true,
activityBar: true,
primarySideBar: true,
secondarySideBar: false,
bottomPanel: true,
statusBar: true,
menuBar: true,
bottomPanelMaximize: false,
})
// ref reactive
const codeLayout = ref<CodeLayoutInstance>()
const showAboutDialog = ref(false)
const windowTitle = ref('ExamAware Editor')
const eaProfile = reactive<ExamConfig>({
examName: '未命名考试',
message: '考试信息',
room: '114考场',
examInfos: [],
})
const currentExamIndex = ref<number | null>(null)
//
const panelComponents: Record<string, Component> = {
'explorer.examlist': SideExamsPanel,
'explorer.examinfo': SideExamInfoPanel,
}
//
function handleConfigFileUpload(file: File) {
const reader = new FileReader()
reader.onload = () => {
const result = reader.result?.toString() || ''
const parsed = parseExamConfig(result)
if (parsed) {
Object.assign(eaProfile, {
examName: parsed.examName,
message: parsed.message,
room: parsed.room,
examInfos: parsed.examInfos,
})
saveProfileToLocalStorage()
}
}
reader.readAsText(file)
}
//
function loadLayout() {
if (codeLayout.value) {
const groupExplorer = codeLayout.value.addGroup(
{
title: 'Explorer',
tooltip: 'Explorer',
name: 'explorer',
badge: '2',
iconLarge: () => h(FileIcon, { size: '16pt' }),
},
'primarySideBar',
)
groupExplorer.addPanel({
title: '考试列表',
tooltip: 'vue-code-layout',
name: 'explorer.examlist',
noHide: true,
startOpen: true,
iconLarge: () => h(FileIcon, { size: '16pt' }),
iconSmall: () => h(FileIcon),
actions: [
{
name: 'add-exam',
icon: () => h(AddIcon),
onClick() {
const now = new Date()
const lastExam = eaProfile.examInfos[eaProfile.examInfos.length - 1]
const start = lastExam ? new Date(new Date(lastExam.end).getTime() + 10 * 60000) : now
const end = new Date(start.getTime() + 60 * 60000)
eaProfile.examInfos.push({
name: '未命名考试' + (eaProfile.examInfos.length + 1),
start: start.toISOString(),
end: end.toISOString(),
alertTime: 15,
})
currentExamIndex.value = eaProfile.examInfos.length - 1
saveProfileToLocalStorage()
},
},
],
})
groupExplorer.addPanel({
title: '考试信息',
tooltip: 'Exam info',
name: 'explorer.examinfo',
noHide: true,
startOpen: true,
iconSmall: () => h(InfoCircleIcon),
iconLarge: () => h(InfoCircleIcon, { size: '16pt' }),
})
}
}
//
function getPanelComponent(name: string) {
return panelComponents[name] || 'div'
}
//
function handleSwitchExamInfo(payload: { examId: number }) {
currentExamIndex.value = payload.examId
const examInfo = eaProfile.examInfos[payload.examId]
if (examInfo) {
console.log('切换到考试信息:', examInfo)
}
}
//
function closeAboutDialog() {
showAboutDialog.value = false
}
//
const menuData: MenuOptions = {
x: 0,
y: 0,
items: [
{
label: '文件',
children: [
{
label: '新建',
onClick: () => {
eaProfile.examName = '未命名考试'
eaProfile.message = '考试信息'
eaProfile.room = '114考场'
currentExamIndex.value = null
eaProfile.examInfos = []
saveProfileToLocalStorage()
},
},
{
label: '打开...',
onClick: () => {
const input = document.createElement('input')
input.type = 'file'
input.accept = '.json'
input.onchange = (e) => {
const files = (e.target as HTMLInputElement).files
if (files && files.length > 0) {
handleConfigFileUpload(files[0])
}
}
input.click()
},
},
{
label: '另存为...',
divided: true,
onClick: () => {
const blob = new Blob([JSON.stringify(getSortedExamInfos(eaProfile))], {
type: 'application/json',
})
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = 'exam.json'
a.click()
URL.revokeObjectURL(url)
},
},
],
},
{
label: '放映',
children: [{ label: '开始全屏放映' }],
},
{
label: '帮助',
children: [
{
label: 'Github',
onClick: () => {
window.open('https://github.com/ExamAware/')
},
},
{
label: '关于 DSZ知试',
onClick: () => {
showAboutDialog.value = true
},
},
],
},
],
zIndex: 3,
minWidth: 230,
}
//
function updateProfile(newProfile: ExamConfig) {
eaProfile.examName = newProfile.examName
eaProfile.message = newProfile.message
eaProfile.room = newProfile.room
eaProfile.examInfos = newProfile.examInfos
saveProfileToLocalStorage()
}
//
function saveProfileToLocalStorage() {
localStorage.setItem('eaProfile', JSON.stringify(eaProfile))
}
//
function loadProfileFromLocalStorage() {
const storedProfile = localStorage.getItem('eaProfile')
if (storedProfile) {
const parsedProfile = JSON.parse(storedProfile) as ExamConfig
Object.assign(eaProfile, parsedProfile)
}
}
//
onMounted(() => {
nextTick(() => {
loadLayout()
loadProfileFromLocalStorage()
})
})
</script>
<template>
<CodeLayout ref="codeLayout" :layout-config="config" :mainMenuConfig="menuData">
<template #statusBar></template>
<template #panelRender="{ panel }">
<component
:is="getPanelComponent(panel.name)"
:profile="eaProfile"
@switch-exam-info="handleSwitchExamInfo"
@update:profile="updateProfile"
/>
</template>
<template #titleBarIcon>
<img src="@renderer/assets/logo.svg" style="margin: 10px" alt="logo" width="25px" />
</template>
<template #titleBarCenter>
<span>{{ windowTitle }}</span>
</template>
<template #centerArea>
<div style="padding: 20px">
<p v-if="currentExamIndex === null">
请从左侧的考试列表中选择一个考试进行编辑如果列表中没有考试请先添加一个考试
</p>
<div v-else>
<t-form labelAlign="top">
<t-form-item label="考试名称" name="examName">
<t-input v-model="eaProfile.examInfos[currentExamIndex].name"></t-input>
</t-form-item>
<t-form-item label="开始时间" name="start">
<t-date-picker
enable-time-picker
allow-input
clearable
v-model="eaProfile.examInfos[currentExamIndex].start"
/>
</t-form-item>
<t-form-item label="结束时间" name="end">
<t-date-picker
enable-time-picker
allow-input
clearable
v-model="eaProfile.examInfos[currentExamIndex].end"
/>
</t-form-item>
<t-form-item label="考试结束提醒时间" name="alertTime">
<t-input-number
v-model="eaProfile.examInfos[currentExamIndex].alertTime"
suffix="分钟"
style="width: 200px"
:min="5"
/>
</t-form-item>
</t-form>
</div>
</div>
</template>
</CodeLayout>
<AboutDialog :visible="showAboutDialog" @update:closedialog="closeAboutDialog" />
</template>

View File

@ -0,0 +1,3 @@
<template>
<p>这是主窗口</p>
</template>

4
tsconfig.json Normal file
View File

@ -0,0 +1,4 @@
{
"files": [],
"references": [{ "path": "./tsconfig.node.json" }, { "path": "./tsconfig.web.json" }]
}

8
tsconfig.node.json Normal file
View File

@ -0,0 +1,8 @@
{
"extends": "@electron-toolkit/tsconfig/tsconfig.node.json",
"include": ["electron.vite.config.*", "src/main/**/*", "src/preload/**/*"],
"compilerOptions": {
"composite": true,
"types": ["electron-vite/node"]
}
}

18
tsconfig.web.json Normal file
View File

@ -0,0 +1,18 @@
{
"extends": "@electron-toolkit/tsconfig/tsconfig.web.json",
"include": [
"src/renderer/src/env.d.ts",
"src/renderer/src/**/*",
"src/renderer/src/**/*.vue",
"src/preload/*.d.ts"
],
"compilerOptions": {
"composite": true,
"baseUrl": ".",
"paths": {
"@renderer/*": [
"src/renderer/src/*"
]
}
}
}