734 lines
18 KiB
Vue

<template>
<div>
<div class="action-bar">
<input
ref="excel-upload-input"
class="excel-upload-input"
type="file"
accept=".xlsx, .xls"
@change="handleClick"
>
<el-button
type="primary"
icon="el-icon-edit"
@click="handleAddQuestion"
>
添加
</el-button>
<el-button
type="success"
icon="el-icon-s-promotion"
@click="importFromOther"
>
从其他挑战导入
</el-button>
<el-button
type="success"
icon="el-icon-upload2"
@click="handleImport"
>
导入Excel
</el-button>
<el-button
type="warning"
icon="el-icon-download"
:loading="downloadLoading"
@click="handleExport"
>
{{exportBtnName}}
</el-button>
<el-button
type="danger"
icon="el-icon-delete-solid"
@click="handleRemoveAll"
>
{{deleteBtnName}}
</el-button>
</div>
<el-table
:data="tableData"
border
fit
stripe
row-key="question"
ref="question_table"
highlight-current-row
@selection-change="handleSelectionChange"
>
<el-table-column
type="selection"
:reserve-selection="true"
width="55">
</el-table-column>
<el-table-column
type="index"
:index="computeTableIndex"
width="50">
</el-table-column>
<el-table-column
label="题目"
prop="question"
>
</el-table-column>
<el-table-column
label="正确答案"
prop="a1"
>
</el-table-column>
<el-table-column
label="混淆答案1"
prop="a2"
>
</el-table-column>
<el-table-column
label="混淆答案2"
prop="a3"
>
</el-table-column>
<el-table-column
label="混淆答案3"
prop="a4"
>
</el-table-column>
<el-table-column
label="题目类型"
>
<template slot-scope="{row}">
<span>{{ row.type === 3 ? '问卷' : '普通' }}</span>
</template>
</el-table-column>
<el-table-column
align="center"
width="180"
label="操作"
fixed="right"
>
<template slot-scope="scope">
<el-button
type="primary"
size="small"
icon="el-icon-edit"
v-permission="['shopexam:edit']"
@click="handleEditQuestion(scope)"
>
编辑
</el-button>
<el-button
type="danger"
size="small"
style="margin-left: 10px"
v-permission="['shopexam:delete']"
@click="handleDelete(scope)"
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
<!-- pagination -->
<el-pagination
:hide-on-single-page="false"
:current-page="currentPage"
:page-sizes="[5, 10, 20, 50, 100]"
:page-size="pageSize"
layout="total, sizes, prev, pager, next, jumper"
:total="dataCount"
class="al-r"
@size-change="sizeChange"
@current-change="pageChange"
/>
<el-dialog
:visible.sync="dialogVisible"
:title="dialogType==='edit'?'Edit Question':'New Question'"
>
<el-form
:model="record"
ref="modalForm"
:rules="modalRules"
label-width="120px"
label-position="left"
>
<el-form-item label="题目" prop="question">
<el-input
v-model="record.question"
placeholder="题目"
clearable
/>
</el-form-item>
<el-form-item label="正确答案" prop="a1">
<el-input
v-model="record.a1"
placeholder="正确答案"
clearable
/>
</el-form-item>
<el-form-item label="混淆答案1" prop="a2">
<el-input
v-model="record.a2"
placeholder="混淆答案1"
clearable
/>
</el-form-item>
<el-form-item label="混淆答案2" prop="a3">
<el-input
v-model="record.a3"
placeholder="混淆答案2"
clearable
/>
</el-form-item>
<el-form-item label="混淆答案3" prop="a4">
<el-input
v-model="record.a4"
placeholder="混淆答案3"
clearable
/>
</el-form-item>
<el-form-item label="题目类型" prop="type">
<el-select
v-model="record.type"
placeholder="选择类型"
name="type"
required
class="w100"
>
<el-option
label="普通"
:value="1"
/>
<el-option
label="问卷"
:value="3"
/>
</el-select>
</el-form-item>
</el-form>
<div style="text-align:right;">
<el-button
type="danger"
@click="closeModal"
>
{{$t('permission.cancel')}}
</el-button>
<el-button
type="primary"
@click="saveModalData"
>
{{$t('permission.confirm')}}
</el-button>
</div>
</el-dialog>
<el-dialog
:visible.sync="listVisible"
title="选择活动"
>
<div class="action-bar">
<el-select
v-model="shop"
:placeholder="'选择'+$t('main.shop')"
name="shop"
required
class="w100"
v-if="userLevel === 1"
>
<el-option
v-for="item in allDepts"
:key="item._id"
:label="item.name"
:value="item._id"
/>
</el-select>
</div>
<el-table
v-loading="listLoading"
:data="shopExamList"
border
fit
stripe
highlight-current-row
@current-change="clickChange"
style="width: 100%;margin-top:30px;"
>
<el-table-column label="选择" width="55">
<template slot-scope="scope">
<el-radio v-model="tableRadio" :label="scope.row"><i></i></el-radio>
</template>
</el-table-column>
<el-table-column
:label="$t('main.shop')"
prop="shop"
v-if="userLevel === 1"
:formatter = "formatDept"
>
</el-table-column>
<el-table-column
min-width="200px"
label="名称"
>
<template slot-scope="{row}">
<span>{{ row.name }}</span>
</template>
</el-table-column>
<el-table-column
min-width="200px"
label="题目数量"
>
<template slot-scope="{row}">
<span>{{ row.questions.length }}</span>
</template>
</el-table-column>
</el-table>
<div style="text-align:right;">
<el-button
type="danger"
@click="closeListModal"
>
{{$t('permission.cancel')}}
</el-button>
<el-button
type="primary"
@click="importQuestions"
>
{{$t('permission.confirm')}}
</el-button>
</div>
</el-dialog>
</div>
</template>
<script lang="ts">
import { Component, Vue, Watch } from 'vue-property-decorator'
import XLSX from 'xlsx'
import { getExams, IExamData, IExamQuerstion } from '@/api/exam'
import { exportJson2Excel } from '@/utils/excel'
import { formatJson, parseTime } from '@/utils'
import { cloneDeep } from 'lodash'
import { UserModule } from '@/store/modules/user'
import { IShopData } from '@/api/types'
import { getShops } from '@/api/shop'
declare module 'vue/types/vue' {
interface Vue {
questions: IExamQuerstion[]
examid?: string
}
}
@Component({
name: 'PuzzleList',
props: ['questions', 'examid'],
model: {
prop: 'questions',
event: 'update'
}
})
export default class extends Vue {
private excelData = {
header: null,
results: null
}
// pagination
private currentPage = 1
private pageSize = 5
private dataCount = 0
private tableData: IExamQuerstion[] = []
private loading = false
private downloadLoading = false
private filename : string | null= ''
private multipleSelection : IExamQuerstion[] = []
private deleteBtnName = '删除所有'
private exportBtnName = '导出所有'
private dialogVisible = false
private listVisible = false
private dialogType = 'new'
private record = Object.assign({}, this.defaultPuzzle())
private shop = UserModule.department || ''
private allDepts: IShopData[] = []
private listLoading = false
private shopExamList: IExamData[] = []
private tableRadio: any = {}
$refs!: {
modalForm: HTMLFormElement
'question_table': HTMLTableElement
'excel-upload-input': HTMLFormElement
}
get userLevel() {
return UserModule.level
}
async created() {
if (UserModule.level === 1) {
await this.getRemoteDeptList()
} else if (UserModule.department) {
this.shop = UserModule.department
}
}
private modalRules = {
question: [{ required: true, message: '请输入题目', trigger: 'blur' },
{ min: 2, max: 35, message: '长度在 2 到 35 个字符', trigger: 'blur' }],
a1: [{ required: true, message: '请输入正确答案', trigger: 'blur' }],
a2: [{ required: true, message: '至少输入一个错误答案', trigger: 'blur' }]
}
@Watch('questions')
private initDataChange() {
this.sliceData()
}
@Watch('multipleSelection')
private selectChange() {
this.deleteBtnName = this.multipleSelection.length > 0 ? '删除选中项' : '删除所有'
this.exportBtnName = this.multipleSelection.length > 0 ? '导出选中项' : '导出所有'
}
@Watch('shop')
private shopChange() {
console.log(`shop change: ${this.shop}`)
this.getList()
}
private defaultPuzzle() {
return {
_id: undefined,
question: '',
a1: '',
a2: '',
a3: '',
a4: '',
type: 1
}
}
// begin of 题库编辑
private handleClick(e: MouseEvent) {
const files = (e.target as HTMLInputElement).files
if (files) {
const rawFile = files[0] // only use files[0]
this.upload(rawFile)
}
}
private handleAddQuestion() {
this.dialogType = 'new'
this.dialogVisible = true
this.record = Object.assign({}, this.defaultPuzzle())
}
private handleEditQuestion(scope: any) {
this.dialogType = 'edit'
this.dialogVisible = true
this.record = cloneDeep(scope.row)
}
private handleImport() {
(this.$refs['excel-upload-input']).click()
}
private async handleRemoveAll() {
try {
const str = this.multipleSelection.length > 0 ? '确定删除选中的题目' : '确定删除所有题目'
await this.$confirm(str, 'Warning', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
if (this.multipleSelection.length > 0) {
for (const s of this.multipleSelection) {
this.questions.splice(this.questions.indexOf(s), 1)
}
(this.$refs.question_table as any).clearSelection()
} else {
this.questions.length = 0
}
this.sliceData()
this.$message({
type: 'success',
message: 'Deleted!'
})
} catch (err) {
console.log(err)
}
}
private handleExport() {
this.handleDownload()
}
private async handleDelete(scope: any) {
const { $index, row } = scope
console.log($index, row)
try {
await this.$confirm('确定删除当前记录?', 'Warning', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
this.questions.splice(this.questions.indexOf(row), 1)
this.sliceData()
this.$message({
type: 'success',
message: 'Deleted!'
})
} catch (err) {
console.log(err)
}
}
private handleSelectionChange(val: any) {
this.multipleSelection = val
console.log(this.multipleSelection)
}
private upload(rawFile: File) {
this.$refs['excel-upload-input'].value = '' // Fixes can't select the same excel
this.readerData(rawFile)
}
private readerData(rawFile: File) {
this.loading = true
const reader = new FileReader()
reader.onload = e => {
const data = (e.target as FileReader).result
const workbook = XLSX.read(data, { type: 'array' })
const firstSheetName = workbook.SheetNames[0]
const worksheet = workbook.Sheets[firstSheetName]
const header = this.getHeaderRow(worksheet)
const results = XLSX.utils.sheet_to_json(worksheet)
this.generateData(header, results)
this.loading = false
}
reader.readAsArrayBuffer(rawFile)
}
private getHeaderRow(sheet: { [key: string]: any }) {
const headers: string[] = []
const range = XLSX.utils.decode_range(sheet['!ref'])
const R = range.s.r
// start in the first row
for (let C = range.s.c; C <= range.e.c; ++C) { // walk every column in the range
const cell = sheet[XLSX.utils.encode_cell({ c: C, r: R })]
// find the cell in the first row
let hdr = ''
if (cell && cell.t) hdr = XLSX.utils.format_cell(cell)
if (hdr === '') {
hdr = 'UNKNOWN ' + C // replace with your desired default
}
headers.push(hdr)
}
return headers
}
private generateData(header: any, results: any) {
this.excelData.header = header
this.excelData.results = results
const questionSet = new Set()
for (const _q of this.questions) {
questionSet.add(_q.question)
}
let count = 0
for (const _q of results) {
if (!questionSet.has(_q.question)) {
this.questions.push(_q)
questionSet.add(_q.question)
count += 1
}
}
this.sliceData()
console.log(this.excelData)
const msg = `操作成功, 共导入${count}个题目`
this.$message({
type: 'success',
message: msg
})
}
// pagination
private sizeChange(val: number) {
this.pageSize = val
this.sliceData()
}
private pageChange(val: number) {
this.currentPage = val
this.sliceData()
}
private computeTableIndex(index: number) {
return (this.currentPage - 1) * this.pageSize + index + 1
}
private sliceData() {
// 满足过滤条件 -> 分页
const data = this.filterData()
this.tableData = data.slice(
(this.currentPage - 1) * this.pageSize,
this.currentPage * this.pageSize
)
if (this.tableData.length === 0 && this.currentPage > 1) {
this.currentPage -= 1
this.sliceData()
}
}
private filterData() {
const result: IExamQuerstion[] = []
for (const _d of this.questions) {
result.push(_d)
}
this.dataCount = result.length
return result
}
private valchange(val: any) {
console.log('valchange', val)
this.$emit('update', val)
}
private handleDownload() {
this.downloadLoading = true
const tHeader = ['question', 'a1', 'a2', 'a3', 'a4', 'type']
const list = this.multipleSelection.length > 0 ? this.multipleSelection.slice(0) : this.questions.slice(0)
this.filename = parseTime(new Date(), '{y}{m}{d}{h}{i}{s}')
const data = formatJson(tHeader, list)
console.log('begin generate excel')
exportJson2Excel(tHeader, data, this.filename ? this.filename : undefined, undefined, undefined, true, 'xlsx')
this.downloadLoading = false
}
private async importFromOther() {
console.log('importFromOther')
if (UserModule.level === 9 && UserModule.department) {
await this.getList()
}
this.listVisible = true
}
private closeModal() {
this.dialogVisible = false
this.$refs.modalForm.clearValidate()
}
private closeListModal() {
this.listVisible = false
}
private saveModalData() {
const isEdit = this.dialogType === 'edit'
this.$refs.modalForm.validate(async(valid: boolean) => {
if (!valid) {
this.$message.error('请按要求填写表单')
return false
}
if (isEdit) {
for (let index = 0; index < this.questions.length; index++) {
if (this.questions[index]._id === this.record._id) {
this.questions.splice(index, 1, Object.assign({}, this.record))
break
}
}
} else {
this.questions.push(this.record)
}
this.dialogVisible = false
this.$notify({
title: 'Success',
dangerouslyUseHTMLString: true,
message: `
题目编辑成功, 请点击保存
`,
type: 'success'
})
})
}
// begin of 活动选择
private async getRemoteDeptList() {
const { data } = await getShops({ })
if (!data.records) return
this.allDepts = data.records
}
private formatDept(row: number, column: number, cellValue: string) {
let result = '未指定'
for (const dep of this.allDepts) {
if (dep._id === cellValue) {
result = dep.name
break
}
}
return result
}
private async getList() {
this.listLoading = true
const { data } = await getExams({
page: 1,
limit: 20,
key: '',
shop: this.shop
})
this.listLoading = false
const records: IExamData[] = data.records
this.shopExamList = records.filter(o => o._id !== this.examid)
}
private clickChange(item: any) {
this.tableRadio = item
console.log(this.tableRadio)
}
private async importQuestions() {
if (!(this.tableRadio?.questions?.length > 0)) {
this.$message.error('请先选择一个有题目的活动')
return
}
try {
await this.$confirm('确定导入当前挑战活动的所有题目?', 'Warning', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
const questionSet = new Set()
for (const _q of this.questions) {
questionSet.add(_q.question)
}
let count = 0
for (const _q of this.tableRadio.questions) {
if (!questionSet.has(_q.question)) {
this.questions.push(_q)
questionSet.add(_q.question)
count += 1
}
}
this.sliceData()
console.log(this.excelData)
this.closeListModal()
const msg = `操作成功, 共导入${count}个题目`
this.$message({
type: 'success',
message: msg
})
} catch (err) {
console.log(err)
}
}
}
</script>
<style lang="scss" scoped>
.action-bar {
margin-bottom: 15px;
}
.excel-upload-input {
display: none;
z-index: -9999;
}
.el-form-item{
margin-bottom: 22px;
}
</style>