feat:修复
This commit is contained in:
parent
155db7a1e4
commit
c35f54fdd9
@ -100,6 +100,65 @@ export class ExamApi {
|
||||
return response
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出题库
|
||||
*/
|
||||
static async exportRepo(repoId: string): Promise<Blob> {
|
||||
console.log('🚀 导出题库:', { repoId })
|
||||
|
||||
try {
|
||||
const response = await fetch(`${import.meta.env.VITE_API_BASE_URL}/aiol/aiolRepo/exportXls?repoId=${repoId}`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'X-Access-Token': localStorage.getItem('token') || '',
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`导出失败: ${response.status} ${response.statusText}`)
|
||||
}
|
||||
|
||||
const blob = await response.blob()
|
||||
console.log('✅ 题库导出成功:', blob)
|
||||
return blob
|
||||
} catch (error) {
|
||||
console.error('❌ 导出题库失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 导入题库
|
||||
*/
|
||||
static async importRepo(repoId: string, file: File): Promise<ApiResponse<any>> {
|
||||
console.log('🚀 导入题库:', { repoId, fileName: file.name })
|
||||
|
||||
try {
|
||||
const formData = new FormData()
|
||||
formData.append('file', file)
|
||||
|
||||
const response = await fetch(`${import.meta.env.VITE_API_BASE_URL}/aiol/aiolRepo/importXls?repoId=${repoId}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-Access-Token': localStorage.getItem('token') || ''
|
||||
},
|
||||
body: formData
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`导入失败: ${response.status} ${response.statusText}`)
|
||||
}
|
||||
|
||||
const result = await response.json()
|
||||
console.log('✅ 题库导入成功:', result)
|
||||
return result
|
||||
} catch (error) {
|
||||
console.error('❌ 导入题库失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// ========== 题目管理 ==========
|
||||
|
||||
/**
|
||||
@ -184,7 +243,7 @@ export class ExamApi {
|
||||
*/
|
||||
static async updateQuestionOption(data: UpdateQuestionOptionRequest): Promise<ApiResponse<string>> {
|
||||
console.log('🚀 编辑题目选项:', data)
|
||||
const response = await ApiRequest.put<string>('/gen/questionoption/questionOption/edit', data)
|
||||
const response = await ApiRequest.put<string>('/aiol/aiolQuestionOption/edit', data)
|
||||
console.log('✅ 编辑题目选项成功:', response)
|
||||
return response
|
||||
}
|
||||
@ -204,11 +263,11 @@ export class ExamApi {
|
||||
// ========== 题目答案管理 ==========
|
||||
|
||||
/**
|
||||
* 添加题目答案
|
||||
* 添加题目答案 - 使用新的API接口
|
||||
*/
|
||||
static async createQuestionAnswer(data: CreateQuestionAnswerRequest): Promise<ApiResponse<string>> {
|
||||
console.log('🚀 添加题目答案:', data)
|
||||
const response = await ApiRequest.post<string>('/gen/questionanswer/questionAnswer/add', data)
|
||||
const response = await ApiRequest.post<string>('/aiol/aiolQuestionAnswer/add', data)
|
||||
console.log('✅ 添加题目答案成功:', response)
|
||||
return response
|
||||
}
|
||||
@ -218,7 +277,7 @@ export class ExamApi {
|
||||
*/
|
||||
static async updateQuestionAnswer(data: UpdateQuestionAnswerRequest): Promise<ApiResponse<string>> {
|
||||
console.log('🚀 编辑题目答案:', data)
|
||||
const response = await ApiRequest.put<string>('/gen/questionanswer/questionAnswer/edit', data)
|
||||
const response = await ApiRequest.put<string>('/aiol/aiolQuestionAnswer/edit', data)
|
||||
console.log('✅ 编辑题目答案成功:', response)
|
||||
return response
|
||||
}
|
||||
|
@ -16,54 +16,32 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 视频控制工具栏 -->
|
||||
<div v-if="playerInitialized" class="video-controls-overlay">
|
||||
<!-- 功能按钮组 -->
|
||||
<div class="video-function-buttons">
|
||||
<!-- 清晰度选择器 -->
|
||||
<div v-if="videoQualities.length > 1" class="dplayer-quality-selector">
|
||||
<button class="dplayer-control-btn dplayer-quality-btn" @click="showQualityMenu = !showQualityMenu" title="清晰度">
|
||||
</div>
|
||||
|
||||
<!-- 播放器下方的清晰度选择器 -->
|
||||
<div v-show="props.videoQualities.length > 1" class="video-controls-bottom">
|
||||
<div class="quality-selector-bottom">
|
||||
<span class="quality-label">清晰度:</span>
|
||||
<div class="quality-dropdown-bottom">
|
||||
<button class="quality-btn-bottom" @click="showQualityMenu = !showQualityMenu">
|
||||
{{ getCurrentQualityLabel() }}
|
||||
<svg width="12" height="12" viewBox="0 0 12 12" class="quality-dropdown-icon">
|
||||
<svg width="12" height="12" viewBox="0 0 12 12" class="quality-dropdown-icon" :class="{ 'rotated': showQualityMenu }">
|
||||
<path d="M3 4.5l3 3 3-3" stroke="currentColor" stroke-width="1.5" fill="none" />
|
||||
</svg>
|
||||
</button>
|
||||
<div v-if="showQualityMenu" class="dplayer-quality-menu">
|
||||
<div v-for="quality in videoQualities" :key="quality.value"
|
||||
class="dplayer-quality-option"
|
||||
:class="{ active: quality.value === currentQuality }"
|
||||
<div v-if="showQualityMenu" class="quality-menu-bottom">
|
||||
<div v-for="quality in props.videoQualities" :key="quality.value"
|
||||
class="quality-option-bottom"
|
||||
:class="{ active: quality.value === props.currentQuality }"
|
||||
@click="switchQuality(quality); showQualityMenu = false">
|
||||
{{ quality.label }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 截屏按钮 -->
|
||||
<button class="dplayer-control-btn" @click="takeScreenshot" title="截屏">
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
|
||||
<path d="M15 5v6c0 .55-.45 1-1 1H2c-.55 0-1-.45-1-1V5c0-.55.45-1 1-1h1.5l.5-1h7l.5 1H14c.55 0 1 .45 1 1zM8 10.5c1.38 0 2.5-1.12 2.5-2.5S9.38 5.5 8 5.5 5.5 6.62 5.5 8 6.62 10.5 8 10.5z"/>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<!-- 画中画按钮 -->
|
||||
<button class="dplayer-control-btn" @click="togglePictureInPicture" title="画中画">
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
|
||||
<path d="M0 1a1 1 0 0 1 1-1h14a1 1 0 0 1 1 1v14a1 1 0 0 1-1 1H1a1 1 0 0 1-1-1V1zm1 0v14h14V1H1zm5.5 4a.5.5 0 0 1 .5-.5h4a.5.5 0 0 1 .5.5v3a.5.5 0 0 1-.5.5H7a.5.5 0 0 1-.5-.5V5z"/>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<!-- 弹幕开关 -->
|
||||
<button class="dplayer-control-btn" @click="toggleDanmaku" :class="{ active: danmakuEnabled }" title="弹幕">
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
|
||||
<path d="M2.5 3A1.5 1.5 0 0 0 1 4.5v7A1.5 1.5 0 0 0 2.5 13h11a1.5 1.5 0 0 0 1.5-1.5v-7A1.5 1.5 0 0 0 13.5 3h-11zM2 4.5a.5.5 0 0 1 .5-.5h11a.5.5 0 0 1 .5.5v7a.5.5 0 0 1-.5.5h-11a.5.5 0 0 1-.5-.5v-7z"/>
|
||||
<path d="M3.5 6h9v1h-9V6zm0 2h7v1h-7V8zm0 2h5v1h-5v-1z"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -102,7 +80,6 @@ const emit = defineEmits<{
|
||||
error: [error: any]
|
||||
qualityChange: [quality: string]
|
||||
screenshot: [dataUrl: string]
|
||||
danmakuSend: [text: string]
|
||||
}>()
|
||||
|
||||
const dplayerContainer = ref<HTMLDivElement>()
|
||||
@ -110,9 +87,7 @@ let player: any = null
|
||||
const playerInitialized = ref(false)
|
||||
const isPlaying = ref(false)
|
||||
|
||||
// 新增功能状态
|
||||
const danmakuEnabled = ref(true)
|
||||
const danmakuText = ref('')
|
||||
// 功能状态
|
||||
const isPictureInPicture = ref(false)
|
||||
const showQualityMenu = ref(false)
|
||||
|
||||
@ -224,17 +199,6 @@ const initializePlayer = async (videoUrl?: string) => {
|
||||
playbackSpeed: [0.5, 0.75, 1, 1.25, 1.5, 2],
|
||||
loop: false,
|
||||
screenshot: true, // 启用截屏功能
|
||||
danmaku: {
|
||||
id: 'course-video-' + Date.now(),
|
||||
api: '/api/danmaku/', // 弹幕API地址
|
||||
token: 'demo-token',
|
||||
maximum: 1000,
|
||||
// 移除有问题的addition API
|
||||
// addition: ['https://api.prprpr.me/dplayer/'],
|
||||
user: 'student',
|
||||
bottom: '15%',
|
||||
unlimited: true
|
||||
},
|
||||
contextmenu: [
|
||||
{
|
||||
text: '截屏',
|
||||
@ -420,32 +384,7 @@ const togglePictureInPicture = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
// 弹幕功能
|
||||
const toggleDanmaku = () => {
|
||||
danmakuEnabled.value = !danmakuEnabled.value
|
||||
if (player && player.danmaku) {
|
||||
if (danmakuEnabled.value) {
|
||||
player.danmaku.show()
|
||||
} else {
|
||||
player.danmaku.hide()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const sendDanmaku = () => {
|
||||
if (danmakuText.value.trim() && player && player.danmaku) {
|
||||
const danmaku = {
|
||||
text: danmakuText.value.trim(),
|
||||
color: '#ffffff',
|
||||
type: 'right'
|
||||
}
|
||||
|
||||
player.danmaku.send(danmaku)
|
||||
emit('danmakuSend', danmakuText.value.trim())
|
||||
danmakuText.value = ''
|
||||
console.log('发送弹幕:', danmaku.text)
|
||||
}
|
||||
}
|
||||
|
||||
// 清晰度切换相关函数
|
||||
const getCurrentQualityLabel = () => {
|
||||
@ -540,8 +479,6 @@ defineExpose({
|
||||
initializePlayer,
|
||||
takeScreenshot,
|
||||
togglePictureInPicture,
|
||||
toggleDanmaku,
|
||||
sendDanmaku,
|
||||
switchQuality
|
||||
})
|
||||
|
||||
@ -742,64 +679,98 @@ onUnmounted(() => {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* 弹幕输入框 */
|
||||
.danmaku-input-container {
|
||||
position: absolute;
|
||||
bottom: 60px;
|
||||
left: 16px;
|
||||
right: 16px;
|
||||
z-index: 15;
|
||||
}
|
||||
|
||||
.danmaku-input-wrapper {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
padding: 8px;
|
||||
/* 播放器下方的清晰度选择器 */
|
||||
.video-controls-bottom {
|
||||
margin-top: 12px;
|
||||
padding: 12px 16px;
|
||||
background: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
backdrop-filter: blur(4px);
|
||||
border: 1px solid #e9ecef;
|
||||
}
|
||||
|
||||
.danmaku-input {
|
||||
flex: 1;
|
||||
padding: 8px 12px;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
color: white;
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
border-radius: 4px;
|
||||
.quality-selector-bottom {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.quality-label {
|
||||
font-size: 14px;
|
||||
outline: none;
|
||||
transition: all 0.2s;
|
||||
color: #495057;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.danmaku-input::placeholder {
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
.quality-dropdown-bottom {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.danmaku-input:focus {
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
border-color: #007bff;
|
||||
}
|
||||
|
||||
.danmaku-send-btn {
|
||||
padding: 8px 16px;
|
||||
background: #007bff;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
.quality-btn-bottom {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 8px 12px;
|
||||
min-width: 80px;
|
||||
background: #fff;
|
||||
color: #495057;
|
||||
border: 1px solid #ced4da;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
white-space: nowrap;
|
||||
transition: all 0.2s ease;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.danmaku-send-btn:hover:not(:disabled) {
|
||||
background: #0056b3;
|
||||
.quality-btn-bottom:hover {
|
||||
border-color: #007bff;
|
||||
box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);
|
||||
}
|
||||
|
||||
.danmaku-send-btn:disabled {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
cursor: not-allowed;
|
||||
.quality-dropdown-icon {
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
.quality-dropdown-icon.rotated {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.quality-menu-bottom {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
margin-top: 4px;
|
||||
background: #fff;
|
||||
border: 1px solid #ced4da;
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
z-index: 1000;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.quality-option-bottom {
|
||||
padding: 10px 12px;
|
||||
font-size: 14px;
|
||||
color: #495057;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s ease;
|
||||
border-bottom: 1px solid #f8f9fa;
|
||||
}
|
||||
|
||||
.quality-option-bottom:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.quality-option-bottom:hover {
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
.quality-option-bottom.active {
|
||||
background-color: #007bff;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.quality-option-bottom.active:hover {
|
||||
background-color: #0056b3;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
|
@ -328,6 +328,10 @@ const questionForm = reactive({
|
||||
explanation: '' // 解析
|
||||
});
|
||||
|
||||
// 存储现有的选项和答案ID(用于编辑模式)
|
||||
const existingOptionsIds = ref<string[]>([]);
|
||||
const existingAnswersIds = ref<string[]>([]);
|
||||
|
||||
// 表单验证规则
|
||||
const formRules = {
|
||||
type: {
|
||||
@ -471,9 +475,8 @@ const saveQuestion = async () => {
|
||||
|
||||
// 根据编辑模式调用不同接口
|
||||
if (isEditMode.value && questionId) {
|
||||
// TODO: 实现编辑模式的接口调用
|
||||
message.info('编辑模式暂未实现');
|
||||
return;
|
||||
// 编辑模式 - 更新现有题目
|
||||
await updateExistingQuestion(questionId);
|
||||
} else {
|
||||
// 新增模式 - 按照接口调用顺序执行
|
||||
await createNewQuestion(bankId);
|
||||
@ -550,10 +553,14 @@ const createNewQuestion = async (bankId: string) => {
|
||||
|
||||
console.log('✅ 题目创建成功,题目ID:', questionId);
|
||||
|
||||
// 如果是选择题或判断题,需要创建选项
|
||||
// 根据题目类型创建选项或答案
|
||||
const questionType = getQuestionTypeNumber(questionForm.type);
|
||||
if (questionType === 0 || questionType === 1 || questionType === 2) { // 单选、多选、判断题
|
||||
if (questionType === 0 || questionType === 1 || questionType === 2) {
|
||||
// 单选、多选、判断题:创建选项
|
||||
await createQuestionOptions(questionId, questionType);
|
||||
} else if (questionType === 3 || questionType === 4) {
|
||||
// 填空题、简答题:创建答案
|
||||
await createQuestionAnswers(questionId, questionType);
|
||||
}
|
||||
|
||||
message.success('题目创建成功!');
|
||||
@ -582,9 +589,15 @@ const createQuestionOptions = async (questionId: string, questionType: number) =
|
||||
const options = questionForm.options || [];
|
||||
const correctAnswer = questionForm.correctAnswer;
|
||||
|
||||
console.log('🔍 单选题选项创建逻辑:', {
|
||||
options,
|
||||
correctAnswer,
|
||||
correctAnswerType: typeof correctAnswer
|
||||
});
|
||||
|
||||
optionsToCreate = options.map((option: any, index: number) => ({
|
||||
content: option.content || option.text || '',
|
||||
izCorrent: option.letter === correctAnswer ? 1 : 0,
|
||||
izCorrent: index === correctAnswer ? 1 : 0,
|
||||
orderNo: index
|
||||
}));
|
||||
|
||||
@ -592,16 +605,29 @@ const createQuestionOptions = async (questionId: string, questionType: number) =
|
||||
const options = questionForm.options || [];
|
||||
const correctAnswers = questionForm.correctAnswers || [];
|
||||
|
||||
console.log('🔍 多选题选项创建逻辑:', {
|
||||
options,
|
||||
correctAnswers,
|
||||
correctAnswersType: typeof correctAnswers
|
||||
});
|
||||
|
||||
optionsToCreate = options.map((option: any, index: number) => ({
|
||||
content: option.content || option.text || '',
|
||||
izCorrent: correctAnswers.includes(option.letter) ? 1 : 0,
|
||||
izCorrent: correctAnswers.includes(index) ? 1 : 0,
|
||||
orderNo: index
|
||||
}));
|
||||
|
||||
} else if (questionType === 2) { // 判断题
|
||||
// 判断题固定两个选项:正确和错误
|
||||
const correctAnswer = questionForm.correctAnswer;
|
||||
const isCorrectTrue = String(correctAnswer) === 'true' || correctAnswer === 1;
|
||||
const trueFalseAnswer = questionForm.trueFalseAnswer;
|
||||
const isCorrectTrue = trueFalseAnswer === true;
|
||||
|
||||
console.log('🔍 判断题选项创建逻辑:', {
|
||||
trueFalseAnswer,
|
||||
isCorrectTrue,
|
||||
trueFalseAnswerType: typeof trueFalseAnswer
|
||||
});
|
||||
|
||||
optionsToCreate = [
|
||||
{
|
||||
content: '正确',
|
||||
@ -640,6 +666,231 @@ const createQuestionOptions = async (questionId: string, questionType: number) =
|
||||
}
|
||||
};
|
||||
|
||||
// 创建题目答案(填空题和简答题)
|
||||
const createQuestionAnswers = async (questionId: string, questionType: number) => {
|
||||
try {
|
||||
console.log('🚀 开始创建题目答案,题目ID:', questionId, '题目类型:', questionType);
|
||||
|
||||
let answersToCreate: Array<{
|
||||
answerText: string;
|
||||
orderNo: number;
|
||||
}> = [];
|
||||
|
||||
if (questionType === 3) { // 填空题
|
||||
const fillBlankAnswers = questionForm.fillBlankAnswers || [];
|
||||
answersToCreate = fillBlankAnswers.map((answer: any, index: number) => ({
|
||||
answerText: answer.content || '',
|
||||
orderNo: index
|
||||
}));
|
||||
|
||||
} else if (questionType === 4) { // 简答题
|
||||
const shortAnswer = questionForm.shortAnswer || '';
|
||||
if (shortAnswer.trim()) {
|
||||
answersToCreate = [{
|
||||
answerText: shortAnswer,
|
||||
orderNo: 0
|
||||
}];
|
||||
}
|
||||
}
|
||||
|
||||
console.log('📊 准备创建的答案:', answersToCreate);
|
||||
|
||||
// 批量创建答案
|
||||
for (const answer of answersToCreate) {
|
||||
const answerData = {
|
||||
questionId: questionId,
|
||||
answerText: answer.answerText,
|
||||
orderNo: answer.orderNo
|
||||
};
|
||||
|
||||
console.log('🚀 创建答案:', answerData);
|
||||
const answerResponse = await ExamApi.createQuestionAnswer(answerData);
|
||||
console.log('✅ 答案创建成功:', answerResponse);
|
||||
}
|
||||
|
||||
console.log('✅ 所有答案创建完成');
|
||||
|
||||
} catch (error: any) {
|
||||
console.error('❌ 创建题目答案失败:', error);
|
||||
throw new Error('创建题目答案失败:' + (error.message || '未知错误'));
|
||||
}
|
||||
};
|
||||
|
||||
// 更新现有题目的完整流程
|
||||
const updateExistingQuestion = async (questionId: string) => {
|
||||
try {
|
||||
console.log('🚀 开始更新题目,题目ID:', questionId);
|
||||
|
||||
// 1. 更新题目基本信息
|
||||
const questionData = {
|
||||
id: questionId,
|
||||
parentId: undefined,
|
||||
type: getQuestionTypeNumber(questionForm.type),
|
||||
content: questionForm.title,
|
||||
analysis: questionForm.explanation || '',
|
||||
difficulty: getDifficultyNumber(questionForm.difficulty),
|
||||
score: questionForm.score
|
||||
};
|
||||
|
||||
console.log('🚀 更新题目基本信息:', questionData);
|
||||
const questionResponse = await ExamApi.updateQuestion(questionData);
|
||||
console.log('✅ 题目基本信息更新成功:', questionResponse);
|
||||
|
||||
// 2. 根据题目类型更新选项或答案
|
||||
const questionType = getQuestionTypeNumber(questionForm.type);
|
||||
if (questionType === 0 || questionType === 1 || questionType === 2) {
|
||||
// 单选、多选、判断题:更新选项
|
||||
await updateQuestionOptions(questionId, questionType);
|
||||
} else if (questionType === 3 || questionType === 4) {
|
||||
// 填空题、简答题:更新答案
|
||||
await updateQuestionAnswers(questionId, questionType);
|
||||
}
|
||||
|
||||
message.success('题目更新成功!');
|
||||
router.back();
|
||||
|
||||
} catch (error: any) {
|
||||
console.error('❌ 更新题目失败:', error);
|
||||
message.error(error.message || '更新题目失败');
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
// 更新题目答案(填空题和简答题)
|
||||
const updateQuestionAnswers = async (questionId: string, questionType: number) => {
|
||||
try {
|
||||
console.log('🚀 开始更新题目答案,题目ID:', questionId, '题目类型:', questionType);
|
||||
|
||||
let answersToUpdate: Array<{
|
||||
id: string;
|
||||
answerText: string;
|
||||
orderNo: number;
|
||||
}> = [];
|
||||
|
||||
if (questionType === 3) { // 填空题
|
||||
const fillBlankAnswers = questionForm.fillBlankAnswers || [];
|
||||
answersToUpdate = fillBlankAnswers.map((answer: any, index: number) => ({
|
||||
id: existingAnswersIds.value[index] || '',
|
||||
answerText: answer.content || '',
|
||||
orderNo: index
|
||||
})).filter(answer => answer.id); // 只更新有ID的答案
|
||||
|
||||
} else if (questionType === 4) { // 简答题
|
||||
const shortAnswer = questionForm.shortAnswer || '';
|
||||
if (shortAnswer.trim() && existingAnswersIds.value.length > 0) {
|
||||
answersToUpdate = [{
|
||||
id: existingAnswersIds.value[0],
|
||||
answerText: shortAnswer,
|
||||
orderNo: 0
|
||||
}];
|
||||
}
|
||||
}
|
||||
|
||||
console.log('📊 准备更新的答案:', answersToUpdate);
|
||||
|
||||
// 批量更新答案
|
||||
for (const answer of answersToUpdate) {
|
||||
const answerData = {
|
||||
id: answer.id,
|
||||
questionId: questionId,
|
||||
answerText: answer.answerText,
|
||||
orderNo: answer.orderNo
|
||||
};
|
||||
|
||||
console.log('🚀 更新答案:', answerData);
|
||||
const answerResponse = await ExamApi.updateQuestionAnswer(answerData);
|
||||
console.log('✅ 答案更新成功:', answerResponse);
|
||||
}
|
||||
|
||||
console.log('✅ 所有答案更新完成');
|
||||
|
||||
} catch (error: any) {
|
||||
console.error('❌ 更新题目答案失败:', error);
|
||||
throw new Error('更新题目答案失败:' + (error.message || '未知错误'));
|
||||
}
|
||||
};
|
||||
|
||||
// 更新题目选项(单选、多选、判断题)
|
||||
const updateQuestionOptions = async (questionId: string, questionType: number) => {
|
||||
try {
|
||||
console.log('🚀 开始更新题目选项,题目ID:', questionId, '题目类型:', questionType);
|
||||
|
||||
let optionsToUpdate: Array<{
|
||||
id: string;
|
||||
content: string;
|
||||
izCorrent: number;
|
||||
orderNo: number;
|
||||
}> = [];
|
||||
|
||||
if (questionType === 0) { // 单选题
|
||||
const options = questionForm.options || [];
|
||||
const correctAnswer = questionForm.correctAnswer;
|
||||
|
||||
optionsToUpdate = options.map((option: any, index: number) => ({
|
||||
id: existingOptionsIds.value[index] || '',
|
||||
content: option.content || '',
|
||||
izCorrent: index === correctAnswer ? 1 : 0,
|
||||
orderNo: index
|
||||
})).filter(option => option.id); // 只更新有ID的选项
|
||||
|
||||
} else if (questionType === 1) { // 多选题
|
||||
const options = questionForm.options || [];
|
||||
const correctAnswers = questionForm.correctAnswers || [];
|
||||
|
||||
optionsToUpdate = options.map((option: any, index: number) => ({
|
||||
id: existingOptionsIds.value[index] || '',
|
||||
content: option.content || '',
|
||||
izCorrent: correctAnswers.includes(index) ? 1 : 0,
|
||||
orderNo: index
|
||||
})).filter(option => option.id); // 只更新有ID的选项
|
||||
|
||||
} else if (questionType === 2) { // 判断题
|
||||
const correctAnswer = questionForm.trueFalseAnswer;
|
||||
const isCorrectTrue = correctAnswer === true;
|
||||
|
||||
if (existingOptionsIds.value.length >= 2) {
|
||||
optionsToUpdate = [
|
||||
{
|
||||
id: existingOptionsIds.value[0],
|
||||
content: '正确',
|
||||
izCorrent: isCorrectTrue ? 1 : 0,
|
||||
orderNo: 0
|
||||
},
|
||||
{
|
||||
id: existingOptionsIds.value[1],
|
||||
content: '错误',
|
||||
izCorrent: isCorrectTrue ? 0 : 1,
|
||||
orderNo: 1
|
||||
}
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
console.log('📊 准备更新的选项:', optionsToUpdate);
|
||||
|
||||
// 批量更新选项
|
||||
for (const option of optionsToUpdate) {
|
||||
const optionData = {
|
||||
id: option.id,
|
||||
questionId: questionId,
|
||||
content: option.content,
|
||||
izCorrent: option.izCorrent,
|
||||
orderNo: option.orderNo
|
||||
};
|
||||
|
||||
console.log('🚀 更新选项:', optionData);
|
||||
const optionResponse = await ExamApi.updateQuestionOption(optionData);
|
||||
console.log('✅ 选项更新成功:', optionResponse);
|
||||
}
|
||||
|
||||
console.log('✅ 所有选项更新完成');
|
||||
|
||||
} catch (error: any) {
|
||||
console.error('❌ 更新题目选项失败:', error);
|
||||
throw new Error('更新题目选项失败:' + (error.message || '未知错误'));
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
// 验证答案设置
|
||||
@ -911,69 +1162,81 @@ const renderQuestionData = (questionData: any) => {
|
||||
};
|
||||
|
||||
// 渲染单选题数据
|
||||
const renderSingleChoiceData = (answers: any[]) => {
|
||||
console.log('🔘 渲染单选题数据:', answers);
|
||||
const renderSingleChoiceData = (options: any[]) => {
|
||||
console.log('🔘 渲染单选题数据:', options);
|
||||
|
||||
if (answers && answers.length > 0) {
|
||||
if (options && options.length > 0) {
|
||||
// 按orderNo排序
|
||||
const sortedAnswers = answers.sort((a, b) => a.orderNo - b.orderNo);
|
||||
const sortedOptions = options.sort((a, b) => a.orderNo - b.orderNo);
|
||||
|
||||
// 保存现有选项ID
|
||||
existingOptionsIds.value = sortedOptions.map(option => option.id);
|
||||
|
||||
// 设置选项
|
||||
questionForm.options = sortedAnswers.map(answer => ({
|
||||
content: answer.content || ''
|
||||
questionForm.options = sortedOptions.map(option => ({
|
||||
content: option.content || ''
|
||||
}));
|
||||
|
||||
// 设置正确答案(orderNo从1开始,数组索引从0开始)
|
||||
const correctAnswer = sortedAnswers.find(answer => answer.izCorrent === 1);
|
||||
if (correctAnswer) {
|
||||
questionForm.correctAnswer = correctAnswer.orderNo - 1;
|
||||
// 设置正确答案
|
||||
const correctOption = sortedOptions.find(option => option.izCorrent === 1);
|
||||
if (correctOption) {
|
||||
questionForm.correctAnswer = correctOption.orderNo;
|
||||
}
|
||||
|
||||
console.log('✅ 单选题渲染完成:', {
|
||||
options: questionForm.options,
|
||||
correctAnswer: questionForm.correctAnswer
|
||||
correctAnswer: questionForm.correctAnswer,
|
||||
existingOptionsIds: existingOptionsIds.value
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 渲染多选题数据
|
||||
const renderMultipleChoiceData = (answers: any[]) => {
|
||||
console.log('☑️ 渲染多选题数据:', answers);
|
||||
const renderMultipleChoiceData = (options: any[]) => {
|
||||
console.log('☑️ 渲染多选题数据:', options);
|
||||
|
||||
if (answers && answers.length > 0) {
|
||||
if (options && options.length > 0) {
|
||||
// 按orderNo排序
|
||||
const sortedAnswers = answers.sort((a, b) => a.orderNo - b.orderNo);
|
||||
const sortedOptions = options.sort((a, b) => a.orderNo - b.orderNo);
|
||||
|
||||
// 保存现有选项ID
|
||||
existingOptionsIds.value = sortedOptions.map(option => option.id);
|
||||
|
||||
// 设置选项
|
||||
questionForm.options = sortedAnswers.map(answer => ({
|
||||
content: answer.content || ''
|
||||
questionForm.options = sortedOptions.map(option => ({
|
||||
content: option.content || ''
|
||||
}));
|
||||
|
||||
// 设置正确答案(可能有多个)
|
||||
questionForm.correctAnswers = sortedAnswers
|
||||
.filter(answer => answer.izCorrent === 1)
|
||||
.map(answer => answer.orderNo - 1);
|
||||
questionForm.correctAnswers = sortedOptions
|
||||
.filter(option => option.izCorrent === 1)
|
||||
.map(option => option.orderNo);
|
||||
|
||||
console.log('✅ 多选题渲染完成:', {
|
||||
options: questionForm.options,
|
||||
correctAnswers: questionForm.correctAnswers
|
||||
correctAnswers: questionForm.correctAnswers,
|
||||
existingOptionsIds: existingOptionsIds.value
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 渲染判断题数据
|
||||
const renderTrueFalseData = (answers: any[]) => {
|
||||
console.log('✔️ 渲染判断题数据:', answers);
|
||||
const renderTrueFalseData = (options: any[]) => {
|
||||
console.log('✔️ 渲染判断题数据:', options);
|
||||
|
||||
if (answers && answers.length > 0) {
|
||||
const correctAnswer = answers.find(answer => answer.izCorrent === 1);
|
||||
if (correctAnswer) {
|
||||
if (options && options.length > 0) {
|
||||
// 保存现有选项ID
|
||||
existingOptionsIds.value = options.map(option => option.id);
|
||||
|
||||
const correctOption = options.find(option => option.izCorrent === 1);
|
||||
if (correctOption) {
|
||||
// 假设判断题的正确答案content为"正确"或"错误"
|
||||
questionForm.trueFalseAnswer = correctAnswer.content === '正确' || correctAnswer.content === 'true';
|
||||
questionForm.trueFalseAnswer = correctOption.content === '正确' || correctOption.content === 'true';
|
||||
}
|
||||
|
||||
console.log('✅ 判断题渲染完成:', {
|
||||
trueFalseAnswer: questionForm.trueFalseAnswer
|
||||
trueFalseAnswer: questionForm.trueFalseAnswer,
|
||||
existingOptionsIds: existingOptionsIds.value
|
||||
});
|
||||
}
|
||||
};
|
||||
@ -983,14 +1246,18 @@ const renderFillBlankData = (answers: any[]) => {
|
||||
console.log('📝 渲染填空题数据:', answers);
|
||||
|
||||
if (answers && answers.length > 0) {
|
||||
// 保存现有答案ID
|
||||
existingAnswersIds.value = answers.map(answer => answer.id);
|
||||
|
||||
questionForm.fillBlankAnswers = answers.map(answer => ({
|
||||
content: answer.content || '',
|
||||
content: answer.answerText || answer.content || '',
|
||||
score: 1,
|
||||
caseSensitive: false
|
||||
}));
|
||||
|
||||
console.log('✅ 填空题渲染完成:', {
|
||||
fillBlankAnswers: questionForm.fillBlankAnswers
|
||||
fillBlankAnswers: questionForm.fillBlankAnswers,
|
||||
existingAnswersIds: existingAnswersIds.value
|
||||
});
|
||||
}
|
||||
};
|
||||
@ -1000,10 +1267,14 @@ const renderShortAnswerData = (answers: any[]) => {
|
||||
console.log('📄 渲染简答题数据:', answers);
|
||||
|
||||
if (answers && answers.length > 0) {
|
||||
questionForm.shortAnswer = answers[0]?.content || '';
|
||||
// 保存现有答案ID
|
||||
existingAnswersIds.value = answers.map(answer => answer.id);
|
||||
|
||||
questionForm.shortAnswer = answers[0]?.answerText || answers[0]?.content || '';
|
||||
|
||||
console.log('✅ 简答题渲染完成:', {
|
||||
shortAnswer: questionForm.shortAnswer
|
||||
shortAnswer: questionForm.shortAnswer,
|
||||
existingAnswersIds: existingAnswersIds.value
|
||||
});
|
||||
}
|
||||
};
|
||||
|
@ -46,18 +46,74 @@
|
||||
</n-modal>
|
||||
|
||||
<!-- 导入弹窗 -->
|
||||
<ImportModal v-model:show="showImportModal" template-name="question_bank_template.xlsx"
|
||||
import-type="questionBank" @success="handleImportSuccess" @template-download="handleTemplateDownload" />
|
||||
<n-modal v-model:show="showImportModal" preset="dialog" title="导入题库" style="width: 600px;">
|
||||
<div class="import-content">
|
||||
<div class="import-info">
|
||||
<p>将题目导入到题库:<strong>{{ getSelectedBankName() }}</strong></p>
|
||||
<p class="import-tip">请选择要导入的Excel文件(支持.xlsx, .xls格式)</p>
|
||||
</div>
|
||||
|
||||
<n-upload
|
||||
ref="uploadRef"
|
||||
v-model:file-list="importFileList"
|
||||
:max="1"
|
||||
accept=".xlsx,.xls"
|
||||
:custom-request="handleImportUpload"
|
||||
@change="handleImportFileChange"
|
||||
show-file-list
|
||||
list-type="text"
|
||||
:default-upload="false"
|
||||
>
|
||||
<n-upload-dragger>
|
||||
<div style="margin-bottom: 12px">
|
||||
<n-icon size="48" :depth="3">
|
||||
<CloudUploadOutline />
|
||||
</n-icon>
|
||||
</div>
|
||||
<n-text style="font-size: 16px">
|
||||
点击或者拖动文件到该区域来上传
|
||||
</n-text>
|
||||
<n-p depth="3" style="margin: 8px 0 0 0">
|
||||
请上传Excel格式的题库文件
|
||||
</n-p>
|
||||
</n-upload-dragger>
|
||||
</n-upload>
|
||||
|
||||
<div v-if="importResult" class="import-result">
|
||||
<n-alert :type="importResult.success ? 'success' : 'error'" :title="importResult.message">
|
||||
<div v-if="importResult.details">
|
||||
<p>成功导入:{{ importResult.details.success }} 条</p>
|
||||
<p v-if="importResult.details.failed > 0">失败:{{ importResult.details.failed }} 条</p>
|
||||
</div>
|
||||
</n-alert>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template #action>
|
||||
<n-space>
|
||||
<n-button @click="closeImportModal">取消</n-button>
|
||||
<n-button
|
||||
type="primary"
|
||||
@click="startImport"
|
||||
:loading="importing"
|
||||
:disabled="!selectedImportFile"
|
||||
>
|
||||
{{ importing ? '导入中...' : '开始导入' }}
|
||||
</n-button>
|
||||
</n-space>
|
||||
</template>
|
||||
</n-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed, onMounted, h, VNode } from 'vue';
|
||||
import { NButton, NSpace, NSelect, useMessage, useDialog } from 'naive-ui';
|
||||
import { NButton, NSpace, NSelect, NIcon, NText, NP, NAlert, NUpload, NUploadDragger, useMessage, useDialog } from 'naive-ui';
|
||||
import { CloudUploadOutline } from '@vicons/ionicons5';
|
||||
import { useRouter, useRoute } from 'vue-router';
|
||||
import ImportModal from '@/components/common/ImportModal.vue';
|
||||
import { ExamApi } from '@/api';
|
||||
import type { Repo } from '@/api/types';
|
||||
import type { UploadCustomRequestOptions, UploadFileInfo } from 'naive-ui';
|
||||
|
||||
// 消息提示和对话框
|
||||
const message = useMessage();
|
||||
@ -112,6 +168,17 @@ const createForm = reactive({
|
||||
|
||||
// 导入弹窗状态
|
||||
const showImportModal = ref(false);
|
||||
const importFileList = ref<UploadFileInfo[]>([]);
|
||||
const selectedImportFile = ref<File | null>(null);
|
||||
const importing = ref(false);
|
||||
const importResult = ref<{
|
||||
success: boolean;
|
||||
message: string;
|
||||
details?: {
|
||||
success: number;
|
||||
failed: number;
|
||||
};
|
||||
} | null>(null);
|
||||
|
||||
// 分页配置
|
||||
const pagination = reactive({
|
||||
@ -375,12 +442,63 @@ const addQuestionBank = () => {
|
||||
};
|
||||
|
||||
const importQuestionBank = () => {
|
||||
console.log('导入题库,选中的题库:', selectedRowKeys.value);
|
||||
|
||||
if (selectedRowKeys.value.length === 0) {
|
||||
message.warning('请先选择要导入到的题库');
|
||||
return;
|
||||
}
|
||||
|
||||
if (selectedRowKeys.value.length > 1) {
|
||||
message.warning('一次只能向一个题库导入,请选择单个题库');
|
||||
return;
|
||||
}
|
||||
|
||||
showImportModal.value = true;
|
||||
};
|
||||
|
||||
const exportQuestionBank = () => {
|
||||
console.log('导出题库');
|
||||
message.info('导出功能开发中...');
|
||||
const exportQuestionBank = async () => {
|
||||
console.log('导出题库,选中的题库:', selectedRowKeys.value);
|
||||
|
||||
if (selectedRowKeys.value.length === 0) {
|
||||
message.warning('请先选择要导出的题库');
|
||||
return;
|
||||
}
|
||||
|
||||
if (selectedRowKeys.value.length > 1) {
|
||||
message.warning('一次只能导出一个题库,请选择单个题库');
|
||||
return;
|
||||
}
|
||||
|
||||
const repoId = selectedRowKeys.value[0];
|
||||
const selectedBank = questionBankList.value.find(bank => bank.id === repoId);
|
||||
const bankName = selectedBank?.name || '题库';
|
||||
|
||||
try {
|
||||
message.loading('正在导出题库,请稍候...', { duration: 0 });
|
||||
|
||||
console.log('🚀 开始导出题库:', { repoId, bankName });
|
||||
const blob = await ExamApi.exportRepo(repoId);
|
||||
|
||||
// 创建下载链接
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = `${bankName}_题库导出_${new Date().toISOString().slice(0, 10)}.xlsx`;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
window.URL.revokeObjectURL(url);
|
||||
|
||||
message.destroyAll();
|
||||
message.success('题库导出成功!');
|
||||
console.log('✅ 题库导出完成');
|
||||
|
||||
} catch (error: any) {
|
||||
message.destroyAll();
|
||||
console.error('❌ 导出题库失败:', error);
|
||||
message.error(error.message || '导出题库失败,请重试');
|
||||
}
|
||||
};
|
||||
|
||||
const deleteSelected = () => {
|
||||
@ -550,7 +668,125 @@ const submitQuestionBank = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
// 处理导入成功
|
||||
// 获取选中题库名称
|
||||
const getSelectedBankName = () => {
|
||||
if (selectedRowKeys.value.length === 0) return '';
|
||||
const selectedBank = questionBankList.value.find(bank => bank.id === selectedRowKeys.value[0]);
|
||||
return selectedBank?.name || '';
|
||||
};
|
||||
|
||||
// 处理导入文件变化
|
||||
const handleImportFileChange = (options: { fileList: UploadFileInfo[] }) => {
|
||||
console.log('文件列表变化:', options.fileList);
|
||||
if (options.fileList.length > 0) {
|
||||
selectedImportFile.value = options.fileList[0].file || null;
|
||||
} else {
|
||||
selectedImportFile.value = null;
|
||||
}
|
||||
importResult.value = null;
|
||||
};
|
||||
|
||||
// 自定义上传处理
|
||||
const handleImportUpload = (options: UploadCustomRequestOptions) => {
|
||||
const { file, onProgress, onFinish, onError } = options;
|
||||
|
||||
// 文件大小检查 (10MB)
|
||||
if (file.file && file.file.size > 10 * 1024 * 1024) {
|
||||
message.error('文件大小不能超过 10MB');
|
||||
onError();
|
||||
return;
|
||||
}
|
||||
|
||||
// 文件类型检查
|
||||
if (file.file && !file.file.name.match(/\.(xlsx|xls)$/i)) {
|
||||
message.error('只支持 Excel 文件格式');
|
||||
onError();
|
||||
return;
|
||||
}
|
||||
|
||||
// 模拟上传进度
|
||||
let progress = 0;
|
||||
const progressInterval = setInterval(() => {
|
||||
if (progress < 90) {
|
||||
progress += Math.random() * 20;
|
||||
onProgress({ percent: Math.min(progress, 90) });
|
||||
}
|
||||
}, 200);
|
||||
|
||||
// 完成上传
|
||||
setTimeout(() => {
|
||||
clearInterval(progressInterval);
|
||||
onProgress({ percent: 100 });
|
||||
onFinish();
|
||||
console.log('文件准备完成:', file.name);
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
// 开始导入
|
||||
const startImport = async () => {
|
||||
if (!selectedImportFile.value) {
|
||||
message.warning('请先选择要导入的文件');
|
||||
return;
|
||||
}
|
||||
|
||||
if (selectedRowKeys.value.length === 0) {
|
||||
message.warning('请先选择目标题库');
|
||||
return;
|
||||
}
|
||||
|
||||
const repoId = selectedRowKeys.value[0];
|
||||
importing.value = true;
|
||||
importResult.value = null;
|
||||
|
||||
try {
|
||||
console.log('🚀 开始导入题库:', { repoId, fileName: selectedImportFile.value.name });
|
||||
|
||||
const result = await ExamApi.importRepo(repoId, selectedImportFile.value);
|
||||
|
||||
console.log('✅ 导入结果:', result);
|
||||
|
||||
if (result.success || result.code === 200) {
|
||||
importResult.value = {
|
||||
success: true,
|
||||
message: '题库导入成功!',
|
||||
details: {
|
||||
success: result.data?.success || 0,
|
||||
failed: result.data?.failed || 0
|
||||
}
|
||||
};
|
||||
message.success('题库导入成功!');
|
||||
|
||||
// 刷新题库列表
|
||||
setTimeout(() => {
|
||||
loadQuestionBanks();
|
||||
closeImportModal();
|
||||
}, 2000);
|
||||
} else {
|
||||
throw new Error(result.message || '导入失败');
|
||||
}
|
||||
|
||||
} catch (error: any) {
|
||||
console.error('❌ 导入题库失败:', error);
|
||||
importResult.value = {
|
||||
success: false,
|
||||
message: error.message || '导入失败,请重试'
|
||||
};
|
||||
message.error(error.message || '导入失败,请重试');
|
||||
} finally {
|
||||
importing.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 关闭导入弹窗
|
||||
const closeImportModal = () => {
|
||||
showImportModal.value = false;
|
||||
importFileList.value = [];
|
||||
selectedImportFile.value = null;
|
||||
importing.value = false;
|
||||
importResult.value = null;
|
||||
};
|
||||
|
||||
// 处理导入成功(保留兼容性)
|
||||
const handleImportSuccess = (result: any) => {
|
||||
console.log('导入成功:', result);
|
||||
message.success('题库导入成功');
|
||||
@ -670,4 +906,34 @@ onMounted(() => {
|
||||
width: 80px !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* 导入弹窗样式 */
|
||||
.import-content {
|
||||
padding: 16px 0;
|
||||
}
|
||||
|
||||
.import-info {
|
||||
margin-bottom: 20px;
|
||||
padding: 12px;
|
||||
background-color: #f8f9fa;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.import-info p {
|
||||
margin: 0 0 8px 0;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.import-info p:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.import-tip {
|
||||
color: #666;
|
||||
font-size: 13px !important;
|
||||
}
|
||||
|
||||
.import-result {
|
||||
margin-top: 16px;
|
||||
}
|
||||
</style>
|
||||
|
Loading…
x
Reference in New Issue
Block a user