feat(admin): 新增用户端答题流程页面与相关功能模块

- 新增用户端答题流程主页面,包含封面、介绍、答题、答题图解四个状态
- 新增用户端组件:封面、介绍页、答题页、答题图解页、题目渲染器及通用布局组件
- 新增用户端类型定义、模拟数据及业务常量
- 新增用户路由及类型声明,包含组别和队伍选择页面
- 新增管理端首页模块:数据卡片、头部横幅、折线图、饼图、创意横幅和项目新闻
- 新增富文本编辑器组件,支持图片和视频上传至OSS
- 优化题库分类树,改为扁平化列表结构
- 完善比赛创建功能,增加轮次类型、题目分数类型及关联AP字段
- 修复模板删除函数调用参数错误
- 添加 pinyin-pro 依赖以支持拼音处理功能
This commit is contained in:
2026-02-05 18:00:11 +08:00
parent ce5ee2a289
commit ca42c6a876
49 changed files with 3569 additions and 179 deletions

View File

@ -0,0 +1,116 @@
<script setup lang="ts">
import { Editor, Toolbar } from '@wangeditor/editor-for-vue'
import { onBeforeUnmount, shallowRef } from 'vue'
import { getAliOssTokenAxios } from '@/service/api/upload'
import { browserPathJoin } from '@/utils/date'
import { initOSSClient, uploadFileToOSS } from '@/utils/oss'
import '@wangeditor/editor/dist/css/style.css'
interface Props {
modelValue: string
placeholder?: string
mode?: 'default' | 'simple'
height?: string
path?: string
}
const props = withDefaults(defineProps<Props>(), {
modelValue: '',
placeholder: '请输入内容...',
mode: 'default',
height: '300px',
path: 'temp',
})
const emit = defineEmits(['update:modelValue'])
// Editor Logic
const editorRef = shallowRef()
const toolbarConfig = {
excludeKeys: [],
}
type InsertFnType = (url: string, alt: string, href: string) => void
async function customUpload(file: File, insertFn: InsertFnType) {
try {
const { data: tokenData, error: tokenError } = await getAliOssTokenAxios()
if (tokenError || !tokenData) {
window.$message?.error('获取上传凭证失败')
return
}
const client = initOSSClient(tokenData?.data || {})
const path = `${props.path}/${Date.now()}/${file.name}`
await uploadFileToOSS(client, file, path)
const url = browserPathJoin(import.meta.env.VITE_BASE_OSS_URL, path)
insertFn(url, file.name, url)
}
catch (error) {
console.error('上传失败', error)
window.$message?.error('上传失败')
}
}
const editorConfig = {
placeholder: props.placeholder,
MENU_CONF: {
uploadImage: {
customUpload,
},
uploadVideo: {
customUpload,
},
},
}
// 监听 placeholder 变化
// watch(() => props.placeholder, (_newVal) => {
// if (editorRef.value) {
// // 似乎 wangeditor 不支持动态修改 placeholder这里作为占位
// }
// })
function handleCreated(editor: any) {
editorRef.value = editor
}
function handleChange(editor: any) {
emit('update:modelValue', editor.getHtml())
}
onBeforeUnmount(() => {
const editor = editorRef.value
if (editor == null)
return
editor.destroy()
})
</script>
<template>
<div class="overflow-hidden border border-gray-200 rounded-lg bg-white">
<Toolbar
style="border-bottom: 1px solid #eee"
:editor="editorRef"
:default-config="toolbarConfig"
:mode="mode"
/>
<div :style="{ height: props.height }">
<Editor
:model-value="modelValue"
:style="{ height: '100%', overflowY: 'hidden' }"
:default-config="editorConfig"
:mode="mode"
@on-created="handleCreated"
@on-change="handleChange"
/>
</div>
</div>
</template>
<style scoped>
:deep(.w-e-text-container) {
background-color: transparent;
}
</style>