fix: 独立窗口数据回显

This commit is contained in:
陆光LG
2025-08-13 08:41:00 +08:00
parent cf1db42bb0
commit 816fce233a
18 changed files with 2731 additions and 704 deletions

View File

@@ -0,0 +1,121 @@
# Paper/Question 页面独立窗口功能使用说明
## 功能概述
完善了 paper/question 页面的独立窗口功能,支持编程题和表格题通过独立窗口进行新增、编辑和修改操作。
## 主要改进
### 1. 独立窗口支持
- **编程题**: 点击新增/修改时会打开独立窗口
- **表格题**: 点击新增/修改时会打开独立窗口
- **其他题型**: 继续使用传统对话框模式
### 2. 路由配置
添加了两个新的独立窗口路由:
- `/cdesign-independent` - 编程题独立窗口
- `/excel-independent` - 表格题独立窗口
### 3. 事件通信
使用 Tauri 事件系统进行窗口间通信:
- `init-cdesign-form` - 初始化编程题表单数据
- `init-excel-form` - 初始化表格题表单数据
- `cdesign-form-success` / `excel-form-success` - 表单提交成功
- `cdesign-form-cancel` / `excel-form-cancel` - 表单取消
## 文件修改说明
### 1. 主要页面修改
- `src/views/paper/question/index.vue`: 修改 openForm 函数,为编程题和表格题添加独立窗口支持
- `src/views/paper/question/CdesignForm.vue`: 已支持独立窗口模式
- `src/views/paper/question/WpsXlsxForm.vue`: 添加独立窗口模式支持
### 2. 新增独立窗口页面
- `src/views/cdesign/independent.vue`: 编程题独立窗口页面
- `src/views/excel/independent.vue`: 表格题独立窗口页面
### 3. 路由配置
- `src/router/modules/remaining.ts`: 添加独立窗口路由配置
## 使用方法
### 在 paper/question 页面中
1. **选择题型**: 首先选择相应的专业和题型
2. **新增操作**:
- 编程题:点击"新增"按钮,自动打开编程题独立窗口
- 表格题:点击"新增"按钮,自动打开表格题独立窗口
- 其他题型:使用传统对话框模式
3. **修改操作**:
- 在题目列表中点击"修改"按钮
- 编程题和表格题会在独立窗口中打开
- 其他题型继续使用对话框模式
### 独立窗口功能
1. **窗口大小**: 1200x800 像素,可调整大小
2. **数据传递**: 通过 Tauri 事件系统自动传递初始化数据
3. **表单提交**: 提交成功后自动关闭窗口并刷新主页面列表
4. **取消操作**: 取消时自动关闭窗口
## 技术实现
### 1. 窗口管理
```javascript
const { WebviewWindow } = await import('@tauri-apps/api/webviewWindow')
const webview = new WebviewWindow(windowLabel, {
url: '#/cdesign-independent',
title: '新增编程题',
width: 1200,
height: 800,
resizable: true,
center: true
})
```
### 2. 事件通信
```javascript
// 发送初始化数据
import('@tauri-apps/api/event').then(({ emit }) => {
emit('init-cdesign-form', {
queryParams,
type,
id
})
})
// 监听成功事件
const { listen } = await import('@tauri-apps/api/event')
listen('cdesign-form-success', () => {
getList() // 刷新列表
})
```
### 3. 降级处理
如果独立窗口创建失败,会自动降级到传统对话框模式,确保功能的稳定性。
## 优势特点
1. **用户体验好**: 独立窗口提供更大的操作空间
2. **多任务处理**: 可以同时查看列表和编辑表单
3. **兼容性强**: 保持其他题型的传统操作方式不变
4. **容错机制**: 具备降级处理机制,保证功能稳定性
5. **事件驱动**: 使用 Tauri 事件系统实现优雅的窗口间通信
## 注意事项
1. 确保 Tauri 环境正常运行
2. 独立窗口需要相应的路由配置
3. 表单组件需要支持 `isIndependent` 属性
4. 事件名称需要保持一致以确保正常通信
## 扩展说明
如需为其他题型添加独立窗口支持:
1. 在对应的 Form 组件中添加 `isIndependent` 属性支持
2. 创建对应的独立窗口页面
3. 在路由中添加相应配置
4.`openForm` 函数中添加相应的处理逻辑
5. 定义相应的事件名称用于通信

View File

View File

@@ -38,6 +38,7 @@
"@wangeditor/editor": "^5.1.23",
"@wangeditor/editor-for-vue": "^5.1.10",
"@zxcvbn-ts/core": "^3.0.4",
"ace-builds": "^1.43.2",
"animate.css": "^4.1.1",
"axios": "^1.6.8",
"benz-amr-recorder": "^1.1.5",
@@ -79,6 +80,7 @@
"vue-i18n": "9.10.2",
"vue-router": "4.4.5",
"vue-types": "^5.1.1",
"vue3-ace-editor": "^2.2.4",
"vue3-signature": "^0.2.4",
"vuedraggable": "^4.1.0",
"web-storage-cache": "^1.1.1",

26
pnpm-lock.yaml generated
View File

@@ -41,6 +41,9 @@ importers:
'@zxcvbn-ts/core':
specifier: ^3.0.4
version: 3.0.4
ace-builds:
specifier: ^1.43.2
version: 1.43.2
animate.css:
specifier: ^4.1.1
version: 4.1.1
@@ -164,6 +167,9 @@ importers:
vue-types:
specifier: ^5.1.1
version: 5.1.3(vue@3.5.12(typescript@5.3.3))
vue3-ace-editor:
specifier: ^2.2.4
version: 2.2.4(ace-builds@1.43.2)(vue@3.5.12(typescript@5.3.3))
vue3-signature:
specifier: ^0.2.4
version: 0.2.4(vue@3.5.12(typescript@5.3.3))
@@ -2420,8 +2426,8 @@ packages:
resolution: {integrity: sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==, tarball: https://registry.npmmirror.com/JSONStream/-/JSONStream-1.3.5.tgz}
hasBin: true
ace-builds@1.39.1:
resolution: {integrity: sha512-HcJbBzx8qY66t9gZo/sQu7pi0wO/CFLdYn1LxQO1WQTfIkMfyc7LRnBpsp/oNCSSU/LL83jXHN1fqyOTuIhUjg==}
ace-builds@1.43.2:
resolution: {integrity: sha512-3wzJUJX0RpMc03jo0V8Q3bSb/cKPnS7Nqqw8fVHsCCHweKMiTIxT3fP46EhjmVy6MCuxwP801ere+RW245phGw==}
acorn-jsx@5.3.2:
resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==, tarball: https://registry.npmmirror.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz}
@@ -5159,6 +5165,12 @@ packages:
vue:
optional: true
vue3-ace-editor@2.2.4:
resolution: {integrity: sha512-FZkEyfpbH068BwjhMyNROxfEI8135Sc+x8ouxkMdCNkuj/Tuw83VP/gStFQqZHqljyX9/VfMTCdTqtOnJZGN8g==}
peerDependencies:
ace-builds: '*'
vue: ^3
vue3-signature@0.2.4:
resolution: {integrity: sha512-XFwwFVK9OG3F085pKIq2SlNVqx32WdFH+TXbGEWc5FfEKpx8oMmZuAwZZ50K/pH2FgmJSE8IRwU9DDhrLpd6iA==}
peerDependencies:
@@ -7788,7 +7800,7 @@ snapshots:
jsonparse: 1.3.1
through: 2.3.8
ace-builds@1.39.1: {}
ace-builds@1.43.2: {}
acorn-jsx@5.3.2(acorn@8.14.0):
dependencies:
@@ -9253,7 +9265,7 @@ snapshots:
jsoneditor@9.10.5:
dependencies:
ace-builds: 1.39.1
ace-builds: 1.43.2
ajv: 6.12.6
javascript-natural-sort: 0.7.1
jmespath: 0.16.0
@@ -10675,6 +10687,12 @@ snapshots:
optionalDependencies:
vue: 3.5.12(typescript@5.3.3)
vue3-ace-editor@2.2.4(ace-builds@1.43.2)(vue@3.5.12(typescript@5.3.3)):
dependencies:
ace-builds: 1.43.2
resize-observer-polyfill: 1.5.1
vue: 3.5.12(typescript@5.3.3)
vue3-signature@0.2.4(vue@3.5.12(typescript@5.3.3)):
dependencies:
default-passive-events: 2.0.0

View File

@@ -36,6 +36,9 @@
"core:window:allow-set-skip-taskbar",
"core:webview:allow-set-webview-position",
"core:webview:allow-set-webview-size",
"core:window:allow-create"
"core:window:allow-create",
"core:event:allow-emit",
"core:event:allow-listen",
"core:event:allow-unlisten"
]
}

View File

@@ -273,7 +273,7 @@ watch(
<div
:id="prefixCls"
:class="prefixCls"
class="relative w-full flex bg-[#fff] dark:bg-[var(--el-bg-color)]"
class="relative w-full h-[var(--tags-view-height)] flex bg-[#fff] dark:bg-[var(--el-bg-color)]"
>
<span
:class="tagsViewImmerse ? '' : `${prefixCls}__tool ${prefixCls}__tool--first`"

View File

@@ -45,9 +45,9 @@ export const useRenderLayout = () => {
`${prefixCls}-content-scrollbar`,
{
'!h-[calc(100%-var(--top-tool-height)-var(--tags-view-height))] mt-[calc(var(--top-tool-height)+var(--tags-view-height))]':
fixedHeader.value && tagsView.value,
fixedHeader.value && (tagsView.value || !isDashboardPage.value),
'!h-[calc(100%-var(--top-tool-height))] mt-[var(--top-tool-height)]':
fixedHeader.value && !tagsView.value
fixedHeader.value && !tagsView.value && isDashboardPage.value
}
]}
>

View File

@@ -746,6 +746,27 @@ const remainingRouter: AppRouteRecordRaw[] = [
component: () => import('@/views/iot/plugin/detail/index.vue')
}
]
},
// 独立窗口路由
{
path: '/cdesign-independent',
name: 'CdesignIndependent',
component: () => import('@/views/cdesign/independent.vue'),
meta: {
title: '编程题独立窗口',
hidden: true,
noTagsView: true
}
},
{
path: '/excel-independent',
name: 'ExcelIndependent',
component: () => import('@/views/excel/independent.vue'),
meta: {
title: '表格题独立窗口',
hidden: true,
noTagsView: true
}
}
]

View File

@@ -35,3 +35,20 @@
border-left-color: var(--el-color-primary);
}
}
/* Layout border styles */
.layout-border__top {
border-top: 1px solid var(--el-border-color);
}
.layout-border__bottom {
border-bottom: 1px solid var(--el-border-color);
}
.layout-border__left {
border-left: 1px solid var(--el-border-color);
}
.layout-border__right {
border-right: 1px solid var(--el-border-color);
}

53
src/utils/aceConfig.ts Normal file
View File

@@ -0,0 +1,53 @@
import ace from 'ace-builds'
// ACE 编辑器配置
export const configureAce = () => {
try {
// 设置基础路径
ace.config.set('basePath', '/node_modules/ace-builds/src-noconflict/')
ace.config.set('workerPath', '/node_modules/ace-builds/src-noconflict/')
ace.config.set('modePath', '/node_modules/ace-builds/src-noconflict/')
ace.config.set('themePath', '/node_modules/ace-builds/src-noconflict/')
// 禁用严格的CSP模式来避免worker问题
ace.config.set('useStrictCSP', true)
console.log('ACE Editor configured successfully')
} catch (error) {
console.warn('ACE Editor configuration warning:', error)
}
}
// 默认编辑器选项
export const defaultAceOptions = {
useWorker: false, // 禁用worker避免加载问题
enableBasicAutocompletion: true,
enableSnippets: true,
enableLiveAutocompletion: true,
fontSize: 14,
showPrintMargin: false,
highlightActiveLine: true,
showGutter: true,
wrap: true,
autoScrollEditorIntoView: true
}
// 初始化编辑器实例
export const initAceEditor = (editor: any) => {
try {
// 禁用worker
editor.session.setUseWorker(false)
// 设置样式
editor.container.style.lineHeight = 1.5
editor.renderer.updateFontSize()
// 设置其他选项
editor.setShowPrintMargin(false)
editor.setHighlightActiveLine(true)
console.log('ACE Editor instance initialized successfully')
} catch (error) {
console.warn('ACE Editor instance initialization warning:', error)
}
}

View File

@@ -0,0 +1,49 @@
<template>
<div class="independent-window">
<CdesignForm ref="formRef" :isIndependent="true" />
</div>
</template>
<script lang="ts" setup>
import { ref, onMounted } from 'vue'
import { listen } from '@tauri-apps/api/event'
import { getCurrentWindow } from '@tauri-apps/api/window'
import CdesignForm from '@/views/paper/question/CdesignForm.vue'
defineOptions({ name: 'CdesignIndependent' })
const formRef = ref()
onMounted(async () => {
console.log('CdesignIndependent mounted, sending ready signal...')
// 发送窗口准备就绪信号
try {
const { emit } = await import('@tauri-apps/api/event')
await emit('cdesign-window-ready', { timestamp: Date.now() })
console.log('Sent cdesign-window-ready event')
} catch (error) {
console.error('Failed to send ready signal:', error)
}
// 监听表单提交成功事件,关闭窗口
await listen('cdesign-form-success', async () => {
const appWindow = getCurrentWindow()
await appWindow.close()
})
// 监听表单取消事件,关闭窗口
await listen('cdesign-form-cancel', async () => {
const appWindow = getCurrentWindow()
await appWindow.close()
})
})
</script>
<style lang="scss" scoped>
.independent-window {
width: 100vw;
height: 100vh;
overflow: hidden;
}
</style>

View File

@@ -0,0 +1,38 @@
<template>
<div class="independent-window">
<WpsXlsxForm ref="formRef" :isIndependent="true" />
</div>
</template>
<script lang="ts" setup>
import { ref, onMounted } from 'vue'
import { listen } from '@tauri-apps/api/event'
import { getCurrentWindow } from '@tauri-apps/api/window'
import WpsXlsxForm from '@/views/paper/question/WpsXlsxForm.vue'
defineOptions({ name: 'ExcelIndependent' })
const formRef = ref()
onMounted(async () => {
// 监听表单提交成功事件,关闭窗口
await listen('excel-form-success', async () => {
const appWindow = getCurrentWindow()
await appWindow.close()
})
// 监听表单取消事件,关闭窗口
await listen('excel-form-cancel', async () => {
const appWindow = getCurrentWindow()
await appWindow.close()
})
})
</script>
<style lang="scss" scoped>
.independent-window {
width: 100vw;
height: 100vh;
overflow: hidden;
}
</style>

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,11 @@
<template>
<div class="edit-dialog">
<Dialog v-model="dialogVisible" :title="dialogTitle" width="85%" top="10vh">
<el-scrollbar>
<!-- 独立窗口模式 -->
<div v-if="isIndependent" class="independent-form">
<div class="independent-header">
<h2>{{ dialogTitle }}</h2>
</div>
<div class="independent-content">
<el-scrollbar height="calc(100vh - 150px)">
<div class="main">
<el-form
ref="formRef"
@@ -270,12 +274,13 @@
</div>
</div>
</el-scrollbar>
<template #footer>
<el-button :disabled="formLoading" type="primary" @click="submitForm"> </el-button>
<el-button @click="dialogVisible = false"> </el-button>
</template>
</Dialog>
<div class="independent-footer">
<el-button @click="handleCancel"> </el-button>
<el-button type="primary" @click="submitForm" :loading="formLoading"> </el-button>
</div>
</div>
</div>
<!-- 表单弹窗添加/修改 -->
<FileForm ref="FileRef" @success="handleUploadSuccess" />
@@ -348,10 +353,28 @@ import { cloneDeep } from 'lodash-es'
import FileForm from './components/FileForm.vue'
defineOptions({ name: 'WpsXlsxFrom' })
// 定义Tree接口
interface Tree {
functions?: string
chineseName?: string
parameter?: string
[key: string]: any
}
// 定义组件 props
interface Props {
isIndependent?: boolean
}
const props = withDefaults(defineProps<Props>(), {
isIndependent: false
})
const xlsxPointsList = ref<Tree[]>([]) // 树形结构
const xlsxPointsInfoList = ref<Tree[]>([]) // 树形结构
const list = ref([]) // 列表的数
const list = ref<any[]>([]) // 列表的数
const { t } = useI18n() // 国际化
const message = useMessage() // 消息弹窗
const loading = ref(false) // 列表的加载中
@@ -404,11 +427,11 @@ const formData = ref({
]
})
let xlsxPointsInfosList: (typeof xlsxPoints)[] = []
let xlsxPointsInfosList: any[] = []
const removePoint = (index: number) => {
list.value.splice(index, 1)
}
function fileTypeFormatter(row, column, cellValue) {
function fileTypeFormatter(_row, _column, cellValue) {
if (cellValue === '0') return '素材文件(上传ZIP)'
if (cellValue === '1') return '考试文件'
if (cellValue === '2') return '结果文件'
@@ -439,22 +462,23 @@ const dialogFormVisibleXlsxInfos = ref(false)
const titles = ref('')
const filePath = ref('')
const handleCheckChange = (data: Tree, checked: boolean, indeterminate: boolean) => {
const handleCheckChange = (data: Tree, _checked: boolean, _indeterminate: boolean) => {
console.log(data)
const xlsxPoints = {
firstName: '',
index: '',
function: '',
examName: '',
examCode: ''
examCode: '',
method: ''
}
if (data.functions != null && data.functions != "") {
if (data.functions != null && data.functions != '') {
xlsxPoints.firstName = chineseName.value
xlsxPoints.index = textIndex.value
xlsxPoints.function = data.functions
xlsxPoints.examName = data.chineseName
xlsxPoints.function = data.functions || ''
xlsxPoints.examName = data.chineseName || ''
xlsxPoints.examCode = '111'
xlsxPoints.method = data.parameter
xlsxPoints.method = data.parameter || ''
xlsxPointsInfosList.push(cloneDeep(xlsxPoints))
}
}
@@ -462,7 +486,7 @@ const file = ref()
// 获取xlsx文件并使用文件流进行解析
const getXlsxDataInfo = async () => {
const fileInput = document.getElementById('xlsxFile') as HTMLInputElement
if (fileInput != null) {
if (fileInput != null && fileInput.files && fileInput.files.length > 0) {
file.value = fileInput.files[0]
const res = await XlsxApi.getXlsxDataInfo({ file: file.value })
xlsxPointsList.value = []
@@ -487,7 +511,7 @@ const handleNodelClick = async (row: any) => {
// 获取名称
chineseName.value = '【' + row.name + '】'
textIndex.value = row.index
const res = await XlsxApi.getXlsxByNameList(row.type)
const res = await XlsxApi.getSlideByNameList(row.type)
xlsxPointsInfoList.value = []
xlsxPointsInfoList.value.push(...handleTree(res))
dialogFormVisibleXlsxInfos.value = true
@@ -532,7 +556,7 @@ const formRef = ref() // 表单 Ref
const leftActiveName = ref('desc')
// 右侧tab
const rightActiveName = ref('annex')
const rightHandleClick = (tab, e) => {
const rightHandleClick = (tab, _e) => {
rightActiveName.value = tab.paneName.value
}
const selections = ref([])
@@ -620,6 +644,48 @@ const open = async (queryParams: any, type: string, id?: number) => {
}
}
defineExpose({ open }) // 提供 open 方法,用于打开弹窗
/** 独立窗口模式下的数据加载函数 */
const loadFormData = async (queryParams: any, type: string, id?: number) => {
console.log('WpsXlsx loadFormData called with:', { queryParams, type, id })
formType.value = type
// 修改时,设置数据
if (id) {
console.log('Loading existing Excel question with ID:', id)
formLoading.value = true
try {
const res = await QuestionApi.getQuestion(id)
console.log('Excel question data loaded:', res)
formData.value = res
console.log(formData.value)
list.value = formData.value.answerList
documentList.value = res.fileUploads
console.log('Excel form data set:', {
formData: formData.value,
list: list.value,
documentList: documentList.value
})
} catch (error) {
console.error('Failed to load question data:', error)
message.error('加载题目数据失败')
} finally {
formLoading.value = false
}
} else {
console.log('Creating new Excel question with params:', queryParams)
resetForm()
formData.value.specialtyName = queryParams.specialtyName
formData.value.courseName = queryParams.courseName
formData.value.subjectName = queryParams.subjectName
formData.value.pointNames = queryParams.pointNamesVo
formData.value.pointNamesVo = queryParams.pointNames
formData.value.chapteridDictText = queryParams.chapteridDictTextVo
formData.value.chapteridDictTextVo = queryParams.chapteridDictText
console.log('New Excel form data set:', formData.value)
}
}
/** 提交表单 */
const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
const submitForm = async () => {
@@ -642,17 +708,46 @@ const submitForm = async () => {
await QuestionApi.editQuestion(data)
message.success(t('common.updateSuccess'))
}
dialogVisible.value = false
// 发送操作成功的事件
emit('success')
// 如果是独立窗口模式通过Tauri事件通信
if (props.isIndependent) {
const { emit: tauriEmit } = await import('@tauri-apps/api/event')
await tauriEmit('excel-form-success', { message: 'Form submitted successfully' })
// 关闭独立窗口
const { getCurrentWindow } = await import('@tauri-apps/api/window')
await getCurrentWindow().close()
} else {
dialogVisible.value = false
// 发送操作成功的事件
emit('success')
}
} finally {
formLoading.value = false
}
}
/** 取消操作 */
const handleCancel = async () => {
if (props.isIndependent) {
// 独立窗口模式通过Tauri事件通信并关闭窗口
const { emit: tauriEmit } = await import('@tauri-apps/api/event')
await tauriEmit('excel-form-cancel', { message: 'Form cancelled' })
// 关闭独立窗口
const { getCurrentWindow } = await import('@tauri-apps/api/window')
await getCurrentWindow().close()
} else {
// 对话框模式:关闭对话框
dialogVisible.value = false
}
}
/** 重置表单 */
const resetForm = () => {
formData.value = {
pointNamesVo: '',
chapteridDictTextVo: '',
content: '',
specialtyName: '',
courseName: '',
@@ -733,7 +828,7 @@ const handleTreeWithLevel = (list, level = 1) => {
// 只允许点击第三级节点
const treeRef = ref() // 引用 el-tree
const handleNodeClick = (data, node) => {
const handleNodeClick = (data, _node) => {
if (data.level === 3) {
formData.value.pointNames = data.id
formData.value.pointNamesVo = data.name
@@ -765,8 +860,283 @@ const openPoints = async () => {
await getTree()
dialogVisiblePoints.value = true
}
// 独立窗口模式下的初始化
onMounted(async () => {
if (props.isIndependent) {
try {
const { listen } = await import('@tauri-apps/api/event')
// 监听表单初始化事件
await listen('init-excel-form', (event: any) => {
const { queryParams, type, id, theme } = event.payload
console.log('Received init-excel-form event:', { queryParams, type, id, theme })
// 应用主题
if (theme) {
applyTheme(theme)
}
// 独立窗口模式下直接加载数据,不设置 dialog 状态
loadFormData(queryParams, type, id)
})
} catch (error) {
console.error('Failed to setup Tauri event listeners:', error)
}
}
})
// 应用主题函数
const applyTheme = (theme: any) => {
try {
const setCssVar = (prop: string, val: any) => {
document.documentElement.style.setProperty(prop, val)
}
// 应用主题变量
if (theme.elColorPrimary) {
setCssVar('--el-color-primary', theme.elColorPrimary)
setCssVar('--el-color-primary-light-3', lightenColor(theme.elColorPrimary, 0.3))
setCssVar('--el-color-primary-light-9', lightenColor(theme.elColorPrimary, 0.9))
}
// 设置其他主题变量
setCssVar('--app-content-bg-color', '#f5f7f9')
setCssVar('--el-bg-color', '#ffffff')
setCssVar('--el-text-color-primary', '#303133')
setCssVar('--el-border-color', '#e4e7ed')
setCssVar('--el-border-color-lighter', '#f0f0f0')
setCssVar('--el-fill-color-light', '#f5f7fa')
setCssVar('--el-fill-color-lighter', '#fafafa')
setCssVar('--el-box-shadow', '0 2px 8px rgba(0, 0, 0, 0.1)')
setCssVar('--el-box-shadow-light', '0 2px 4px rgba(0, 0, 0, 0.1)')
} catch (error) {
console.error('Failed to apply theme:', error)
}
}
// 颜色工具函数
const lightenColor = (color: string, amount: number) => {
const num = parseInt(color.replace('#', ''), 16)
const amt = Math.round(2.55 * amount * 100)
const R = (num >> 16) + amt
const G = ((num >> 8) & 0x00ff) + amt
const B = (num & 0x0000ff) + amt
return (
'#' +
(
0x1000000 +
(R < 255 ? (R < 1 ? 0 : R) : 255) * 0x10000 +
(G < 255 ? (G < 1 ? 0 : G) : 255) * 0x100 +
(B < 255 ? (B < 1 ? 0 : B) : 255)
)
.toString(16)
.slice(1)
)
}
</script>
<style lang="scss" scoped>
// 独立窗口模式样式
.independent-form {
width: 100vw;
height: 100vh;
display: flex;
flex-direction: column;
background-color: var(--app-content-bg-color);
overflow: hidden;
.independent-header {
background: var(--el-bg-color);
border-bottom: 1px solid var(--el-border-color);
padding: 16px 24px;
box-shadow: var(--el-box-shadow-light);
h2 {
margin: 0;
font-size: 18px;
font-weight: 600;
color: var(--el-text-color-primary);
}
}
.independent-content {
flex: 1;
overflow: hidden;
background: var(--el-bg-color);
margin: 16px;
border-radius: 8px;
box-shadow: var(--el-box-shadow);
padding: 20px; // 主要内容区域样式
:deep(.el-form) {
.el-row {
margin-bottom: 20px;
.el-col {
padding: 0 8px;
}
}
.el-form-item {
margin-bottom: 20px;
.el-form-item__label {
font-weight: 600;
color: var(--el-text-color-primary);
line-height: 1.6;
}
.el-input,
.el-select,
.el-textarea {
.el-input__inner,
.el-select__inner,
.el-textarea__inner {
border-radius: 6px;
border: 1px solid var(--el-border-color);
transition: border-color 0.3s;
&:focus {
border-color: var(--el-color-primary);
}
}
}
}
}
// 标签页和表格样式
:deep(.el-tabs) {
height: 100%;
.el-tabs__header {
margin-bottom: 20px;
.el-tabs__item {
padding: 0 20px;
height: 40px;
line-height: 40px;
font-weight: 500;
&.is-active {
color: var(--el-color-primary);
font-weight: 600;
}
}
.el-tabs__active-bar {
height: 3px;
border-radius: 2px;
}
}
.el-tabs__content {
height: calc(100% - 60px);
overflow-y: auto;
padding-right: 8px;
.block {
padding: 16px;
background: var(--el-fill-color-lighter);
border-radius: 6px;
border: 1px solid var(--el-border-color);
margin-bottom: 16px;
.tip {
color: #8a6d3b;
background-color: #fcf8e3;
border: 1px solid #faebcc;
padding: 16px;
border-radius: 6px;
margin-bottom: 16px;
line-height: 1.6;
p {
margin: 0;
}
}
.btn-line {
display: flex;
gap: 12px;
margin: 16px 0;
flex-wrap: wrap;
}
.flex {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 12px;
flex-wrap: wrap;
}
}
}
.custom-tabs-label {
display: flex;
align-items: center;
.setting_icon {
width: 16px;
height: 16px;
background: url('@/assets/icon/setting_blue.png') no-repeat center;
background-size: 100%;
margin-left: 8px;
opacity: 0;
transition: opacity 0.3s;
}
}
.el-tabs__item.is-active {
.custom-tabs-label .setting_icon {
opacity: 1;
}
}
}
}
.independent-footer {
background: var(--el-bg-color);
border-top: 1px solid var(--el-border-color);
padding: 16px 24px;
display: flex;
justify-content: flex-end;
gap: 12px;
box-shadow: var(--el-box-shadow-light);
.el-button {
border-radius: 6px;
font-weight: 500;
padding: 8px 16px;
&.el-button--primary {
background: var(--el-color-primary);
border-color: var(--el-color-primary);
color: #ffffff !important;
&:hover {
background: var(--el-color-primary-light-3);
border-color: var(--el-color-primary-light-3);
color: #ffffff !important;
}
}
&.el-button--default {
background: #ffffff;
border-color: var(--el-border-color);
color: var(--el-text-color-primary);
&:hover {
background: var(--el-fill-color-lighter);
border-color: var(--el-color-primary);
color: var(--el-color-primary);
}
}
}
}
}
// Dialog模式样式保持原有样式
.edit-dialog {
:deep(.el-dialog) {
display: flex;
@@ -889,23 +1259,215 @@ const openPoints = async () => {
}
}
:deep(.tox-tinymce) {
.tox-statusbar {
display: none;
// 全局样式优化
:deep(.el-form-item) {
margin-bottom: 20px;
.el-form-item__label {
font-weight: 600;
color: var(--el-text-color-primary);
line-height: 1.6;
}
.el-input,
.el-select,
.el-textarea {
.el-input__inner,
.el-select__inner,
.el-textarea__inner {
border-radius: 6px;
border: 1px solid var(--el-border-color);
transition: border-color 0.3s;
&:focus {
border-color: var(--el-color-primary);
}
}
}
}
:deep(.el-table) {
border-radius: 6px;
overflow: hidden;
.el-table__header-wrapper {
.el-table__header {
thead {
tr {
th {
background: #ebebeb;
background: var(--el-fill-color-light);
color: var(--el-text-color-primary);
font-weight: 600;
border-bottom: 1px solid var(--el-border-color);
}
}
}
}
}
.el-table__body-wrapper {
.el-table__body {
tbody {
tr {
td {
border-bottom: 1px solid var(--el-border-color-lighter);
padding: 12px 0;
}
&:hover {
background-color: var(--el-fill-color-lighter);
}
}
}
}
}
}
:deep(.el-tabs) {
.el-tabs__item {
padding: 0 20px;
height: 40px;
line-height: 40px;
font-weight: 500;
&.is-active {
color: var(--el-color-primary);
font-weight: 600;
}
}
.el-tabs__active-bar {
height: 3px;
border-radius: 2px;
}
}
:deep(.el-checkbox) {
margin-right: 24px;
.el-checkbox__label {
font-weight: 500;
color: var(--el-text-color-primary);
}
}
:deep(.el-radio) {
margin-right: 24px;
.el-radio__label {
font-weight: 500;
color: var(--el-text-color-primary);
}
}
:deep(.el-button) {
border-radius: 6px;
font-weight: 500;
padding: 8px 16px;
&.el-button--primary {
background: var(--el-color-primary);
border-color: var(--el-color-primary);
color: #ffffff; // 确保文字颜色为白色
&:hover {
background: var(--el-color-primary-light-3);
border-color: var(--el-color-primary-light-3);
color: #ffffff; // 悬停时文字也保持白色
}
}
&.el-button--default {
background: #ffffff;
border-color: var(--el-border-color);
color: var(--el-text-color-primary);
&:hover {
background: var(--el-fill-color-lighter);
border-color: var(--el-color-primary);
color: var(--el-color-primary);
}
}
}
:deep(.tox-tinymce) {
.tox-statusbar {
display: none;
}
}
// 上传组件样式
:deep(.el-upload) {
.el-upload-dragger {
border-radius: 6px;
border: 2px dashed var(--el-border-color);
transition: border-color 0.3s;
&:hover {
border-color: var(--el-color-primary);
}
}
}
// 树形控件样式
:deep(.el-tree) {
.el-tree-node {
.el-tree-node__content {
height: 32px;
border-radius: 4px;
&:hover {
background-color: var(--el-fill-color-lighter);
}
}
&.is-current > .el-tree-node__content {
background-color: var(--el-color-primary-light-9);
color: var(--el-color-primary);
}
}
}
// 对话框样式
:deep(.el-dialog) {
border-radius: 8px;
.el-dialog__header {
padding: 16px 20px;
border-bottom: 1px solid var(--el-border-color);
.el-dialog__title {
font-size: 16px;
font-weight: 600;
}
}
.el-dialog__body {
padding: 20px;
}
.el-dialog__footer {
padding: 16px 20px;
border-top: 1px solid var(--el-border-color);
}
}
// 按钮样式修复
:deep(.el-button) {
&.el-button--primary {
color: #ffffff !important;
background-color: var(--el-color-primary) !important;
border-color: var(--el-color-primary) !important;
&:hover {
background-color: var(--el-color-primary-light-3) !important;
border-color: var(--el-color-primary-light-3) !important;
}
&:active {
background-color: var(--el-color-primary-dark-2) !important;
border-color: var(--el-color-primary-dark-2) !important;
}
}
}
</style>

View File

@@ -0,0 +1,89 @@
<template>
<v-ace-editor
v-model:value="content"
@init="editorInit"
lang="c_cpp"
theme="tomorrow_night"
:style="{ height: editorHeight }"
:options="{
useWorker: true,
enableBasicAutocompletion: true,
enableSnippets: true,
enableLiveAutocompletion: true,
fontSize: 14,
showPrintMargin: false,
highlightActiveLine: true,
showGutter: true
}"
/>
</template>
<script setup lang="ts">
import { ref, watch, computed, defineEmits, defineProps, defineExpose } from 'vue'
import { VAceEditor } from 'vue3-ace-editor'
// 引入需要的语言模式、主题和扩展
import 'ace-builds/src-noconflict/mode-c_cpp'
import 'ace-builds/src-noconflict/theme-tomorrow_night'
import 'ace-builds/src-noconflict/ext-language_tools'
const props = defineProps({
modelValue: {
type: String,
default: ''
},
rows: {
type: Number,
default: 8 // 默认行数
}
})
const emit = defineEmits(['update:modelValue'])
const content = ref(props.modelValue)
let editorInstance: any = null
// 根据行数计算编辑器高度
const editorHeight = computed(() => {
// 假设每行大约 21px
return `${props.rows * 21}px`
})
watch(
() => props.modelValue,
(newValue) => {
if (content.value !== newValue) {
content.value = newValue
}
}
)
watch(content, (newValue) => {
emit('update:modelValue', newValue)
})
const editorInit = (editor) => {
editorInstance = editor
editor.container.style.lineHeight = 1.5
editor.renderer.updateFontSize()
}
// 定义获取选中内容的方法
const getSelectedText = () => {
if (editorInstance) {
return editorInstance.getSelectedText()
}
return ''
}
// 向父组件暴露方法
defineExpose({
getSelectedText
})
</script>
<style scoped>
.ace_editor {
border: 1px solid #444;
border-radius: 7px;
}
</style>

View File

@@ -369,8 +369,10 @@ import SpecialtyTree from './SpecialtyTree.vue'
import { handleTree } from '@/utils/tree'
import * as SpecialtyApi from '@/api/points'
import { useSettingStore } from '@/store/modules/settings'
import { useAppStore } from '@/store/modules/app'
import { TaskStatusEnum } from '@/api/bpm/task'
const settingStore = useSettingStore()
const appStore = useAppStore()
defineOptions({ name: 'SystemUser' })
const message = useMessage() // 消息弹窗
@@ -746,15 +748,165 @@ const setformRef = ref()
const emailformRef = ref()
const xlsxformRef = ref()
const psformRef = ref()
const openForm = (type: string, id?: number) => {
const openForm = async (type: string, id?: number) => {
console.log(queryParams)
if (queryParams.subjectName == '') {
return message.confirm('请选择题型!')
}
if (chooseQuestionType.value.includes('选择题')) {
// 对编程题和表格题使用独立窗口
if (chooseQuestionType.value.includes('编程题')) {
try {
const { WebviewWindow } = await import('@tauri-apps/api/webviewWindow')
const windowLabel = `cdesign-form-${Date.now()}`
const webview = new WebviewWindow(windowLabel, {
url: '#/cdesign-independent',
title: type === 'create' ? '新增编程题' : '修改编程题',
width: 1200,
height: 800,
resizable: true,
center: true
})
// 监听窗口加载完成,然后传递数据
webview.once('tauri://created', () => {
console.log('Webview created, setting up data transfer...')
// 延迟发送事件,确保组件完全挂载
setTimeout(() => {
console.log('Sending init-cdesign-form event with data:', { queryParams, type, id })
import('@tauri-apps/api/event').then(({ emit }) => {
emit('init-cdesign-form', {
queryParams,
type,
id,
theme: appStore.getTheme // 传入当前主题
})
.then(() => {
console.log('init-cdesign-form event sent successfully')
})
.catch((error) => {
console.error('Failed to send init-cdesign-form event:', error)
})
})
}, 500) // 增加延迟到500ms
// 多次尝试发送事件,确保成功
setTimeout(() => {
console.log('Second attempt: Sending init-cdesign-form event')
import('@tauri-apps/api/event').then(({ emit }) => {
emit('init-cdesign-form', {
queryParams,
type,
id,
theme: appStore.getTheme
})
})
}, 1000)
// 第三次尝试
setTimeout(() => {
console.log('Third attempt: Sending init-cdesign-form event')
import('@tauri-apps/api/event').then(({ emit }) => {
emit('init-cdesign-form', {
queryParams,
type,
id,
theme: appStore.getTheme
})
})
}, 2000)
})
// 监听窗口准备就绪信号
const eventModule = await import('@tauri-apps/api/event')
const unlistenReady = await eventModule.listen('cdesign-window-ready', () => {
console.log('Received cdesign-window-ready, sending init data...')
eventModule
.emit('init-cdesign-form', {
queryParams,
type,
id,
theme: appStore.getTheme
})
.then(() => {
console.log('Data sent in response to window ready signal')
})
})
// 监听组件请求初始化数据的事件
const unlistenRequest = await eventModule.listen('request-cdesign-init', (event) => {
console.log('Received request-cdesign-init from component:', event.payload)
eventModule
.emit('init-cdesign-form', {
queryParams,
type,
id,
theme: appStore.getTheme
})
.then(() => {
console.log('Data sent in response to component request')
})
})
// 清理监听器
setTimeout(() => {
unlistenReady()
unlistenRequest()
}, 10000) // 10秒后清理监听器
// 监听表单提交成功事件
eventModule.listen('cdesign-form-success', () => {
getList() // 刷新列表
})
} catch (error) {
console.error('Failed to open independent window:', error)
// 降级到对话框模式
cformRef.value.open(queryParams, type, id)
}
} else if (chooseQuestionType.value.includes('表格')) {
try {
const { WebviewWindow } = await import('@tauri-apps/api/webviewWindow')
const windowLabel = `excel-form-${Date.now()}`
const webview = new WebviewWindow(windowLabel, {
url: '#/excel-independent',
title: type === 'create' ? '新增表格题' : '修改表格题',
width: 1200,
height: 800,
resizable: true,
center: true
})
// 监听窗口加载完成,然后传递数据
webview.once('tauri://created', () => {
// 延迟发送事件,确保组件完全挂载
setTimeout(() => {
import('@tauri-apps/api/event').then(({ emit }) => {
emit('init-excel-form', {
queryParams,
type,
id,
theme: appStore.getTheme // 传入当前主题
})
})
}, 100)
})
// 监听表单提交成功事件
const { listen } = await import('@tauri-apps/api/event')
listen('excel-form-success', () => {
getList() // 刷新列表
})
} catch (error) {
console.error('Failed to open independent window:', error)
// 降级到对话框模式
xlsxformRef.value.open(queryParams, type, id)
}
}
// 其他题型继续使用对话框模式
else if (chooseQuestionType.value.includes('选择题')) {
formRef.value.open(queryParams, type, id)
} else if (chooseQuestionType.value.includes('编程题')) {
cformRef.value.open(queryParams, type, id)
} else if (chooseQuestionType.value.includes('程序设计')) {
mformRef.value.open(queryParams, type, id)
} else if (chooseQuestionType.value.includes('网络题')) {
@@ -769,8 +921,6 @@ const openForm = (type: string, id?: number) => {
setformRef.value.open(queryParams, type, id)
} else if (chooseQuestionType.value.includes('邮箱')) {
emailformRef.value.open(queryParams, type, id)
} else if (chooseQuestionType.value.includes('表格')) {
xlsxformRef.value.open(queryParams, type, id)
} else if (chooseQuestionType.value.includes('图像处理')) {
psformRef.value.open(queryParams, type, id)
}

View File

View File

@@ -127,7 +127,7 @@ const openIndependentWindow = async (row: any) => {
try {
// 直接使用 newWindow 方法创建独立窗口
await newWindow(`excel-edit-${row.id}`, {
url: `/wps/xlsx?id=${row.id}&mode=edit`,
url: `/xlsx-independent?id=${row.id}&mode=edit&name=${encodeURIComponent(row.name)}`,
title: `编辑 Excel - ${row.name}`,
width: 900,
height: 650,