二维码签到

dev
lion 3 months ago
parent c3fc351987
commit 801d60fe27

@ -9,10 +9,10 @@
<script>
window._AMapSecurityConfig = {
securityJsCode: '0d59d0a3fa5483849b52b0edc4bc97ec',
securityJsCode: '33028b966409f0eee8dd3e5febd190b8',
}
</script>
<script type="text/javascript" src='https://webapi.amap.com/maps?v=1.4.11&key=795a757114c371f42cee1f8efa527684&plugin=AMap.PlaceSearch'></script>
<script type="text/javascript" src='https://webapi.amap.com/maps?v=1.4.11&key=e8f1e5a1f7fc7e24e12ea2a81c07826a&plugin=AMap.PlaceSearch'></script>
<script src="https://webapi.amap.com/ui/1.0/main.js?v=1.0.11"></script>
<script src="/filejs/FileSaver.min.js"></script>
<script src="/filejs/xlsx.full.min.js"></script>

@ -70,4 +70,13 @@ export function sendSms(params) {
})
}
export function qrcodeGet(params) {
return request({
method: "get",
url: "/api/admin/course-contents/qrcode",
params
})
}

@ -48,7 +48,7 @@
<span v-else class="no-files">暂无课件</span>
</template>
</el-table-column>
<el-table-column label="操作" :width="250" align="center">
<el-table-column label="操作" fixed="right" :width="250" align="center">
<template slot-scope="scope">
<el-button type="primary" size="small" style='margin-left:5px;margin-bottom:5px;'
@click="editClass('editor',scope.row.id)">编辑</el-button>
@ -74,8 +74,9 @@
<div class="material-upload-container">
<div class="course-info">
<h4>课程信息</h4>
<p><strong>日期</strong>{{ selectedCourse.date }}</p>
<p><strong>日期</strong>{{ selectedCourse.date }}</p>
<p><strong>时间</strong>{{ selectedCourse.period }}</p>
<!-- <p><strong>上课时间</strong>{{ selectedCourse.start_time }}{{ selectedCourse.end_time }}</p> -->
<p><strong>主题</strong>{{ selectedCourse.theme }}</p>
<p><strong>老师</strong>{{ selectedCourse.teacher ? selectedCourse.teacher.name : '' }}</p>
</div>
@ -169,35 +170,58 @@
width: 120,
},{
prop: 'date',
label: '日期',
label: '上课日期',
align: 'center',
width: 120,
}, {
prop: 'period',
label: '时间 ',
label: '上课时间',
align: 'center',
width: 180,
}, {
},
// {
// prop: 'start_time',
// label: '',
// align: 'center',
// width: 120,
// }, {
// prop: 'end_time',
// label: '',
// align: 'center',
// width: 120,
// },
{
prop: 'teacher.name',
label: '授课老师',
align: 'center',
width: 120,
}, {
prop: 'sex',
prop: 'teacher.sex',
label: '性别',
align: 'left'
align: 'left',
align: 'center',
width:80
}, {
prop: 'mobile',
label: '联系方式',
align: 'left'
prop: 'teacher.mobile',
label: '联系方式',
align: 'center',
width: 180,
}, {
prop: 'address',
label: '上课地点',
align: 'left'
align: 'left',
width: 240,
}, {
prop: 'address_detail',
label: '签到地点',
align: 'left',
width: 240,
}, {
prop: 'teacher.introduce',
label: '老师简介',
align: 'left'
align: 'left',
width: 240,
}],
materialDialogVisible: false, //
selectedCourse: {}, //

@ -5,23 +5,37 @@
<template v-slot:date>
<div class="xy-table-item">
<div class="xy-table-item-label" style="font-weight: bold">
<span style="color: red;font-weight: bold;padding-right: 4px;"></span>日期
<span style="color: red;font-weight: bold;padding-right: 4px;">*</span>日期
</div>
<div class="xy-table-item-content">
<el-input v-model="form.date" placeholder="请输入日期" clearable style="width: 100%;"></el-input>
<el-date-picker style="width: 100%;" v-model="form.date" value-format="yyyy-MM-dd" format="yyyy-MM-dd" type="date" placeholder="选择日期">
</el-date-picker>
</div>
</div>
</template>
<template v-slot:period>
<div class="xy-table-item">
<div class="xy-table-item-label" style="font-weight: bold">
<span style="color: red;font-weight: bold;padding-right: 4px;"></span>上课时间
</div>
<div class="xy-table-item-content">
<el-input style="width: 100%;" v-model="form.period" placeholder="请输入上课时间" clearable></el-input>
</div>
</div>
</template>
<template v-slot:period>
<!-- <template v-slot:timeRange>
<div class="xy-table-item">
<div class="xy-table-item-label" style="font-weight: bold">
<span style="color: red;font-weight: bold;padding-right: 4px;"></span>时间
<span style="color: red;font-weight: bold;padding-right: 4px;"></span>上课时间
</div>
<div class="xy-table-item-content">
<el-input v-model="form.period" placeholder="请输入时间" clearable style="width: 100%;"></el-input>
<el-time-picker @change="changeTime" style="width: 100%;" format="HH:mm" value-format="HH:mm" is-range
v-model="form.timeRange" range-separator="至" start-placeholder="开始时间" end-placeholder="结束时间"
placeholder="选择时间范围">
</el-time-picker>
</div>
</div>
</template>
</template> -->
<template v-slot:teacher_id>
<div class="xy-table-item">
<div class="xy-table-item-label" style="font-weight: bold">
@ -56,7 +70,17 @@
</div>
</div>
</template>
<template v-slot:introduce>
<template v-slot:address_detail>
<div class="xy-table-item">
<div class="xy-table-item-label" style="font-weight: bold">
<span style="color: red;font-weight: bold;padding-right: 4px;"></span>签到地点
</div>
<div class="xy-table-item-content">
<avue-input-map v-model="mapform" :params="mapparams" style="width:100%" placeholder="请选择地图" />
</div>
</div>
</template>
<!-- <template v-slot:introduce>
<div class="xy-table-item">
<div class="xy-table-item-label" style="font-weight: bold">
<span style="color: red;font-weight: bold;padding-right: 4px;"></span>老师简介
@ -65,7 +89,7 @@
<el-input style="width: 100%;" v-model="form.introduce" placeholder="请输入老师简介" clearable></el-input>
</div>
</div>
</template>
</template> -->
<template v-slot:files>
<div class="xy-table-item">
<div class="xy-table-item-label" style="font-weight: bold">
@ -85,20 +109,13 @@
</el-button>
</div>
</div>
<!-- 上传新课件 -->
<div class="upload-section">
<h5>上传新课件</h5>
<el-upload
ref="fileUpload"
:action="uploadAction"
:headers="uploadHeaders"
:file-list="newFileList"
:on-success="handleFileUploadSuccess"
:on-remove="handleFileUploadRemove"
:on-error="handleFileUploadError"
:before-upload="beforeFileUpload"
multiple
<el-upload ref="fileUpload" :action="uploadAction" :headers="uploadHeaders" :file-list="newFileList"
:on-success="handleFileUploadSuccess" :on-remove="handleFileUploadRemove"
:on-error="handleFileUploadError" :before-upload="beforeFileUpload" multiple
accept=".pdf,.doc,.docx,.ppt,.pptx,.xls,.xlsx,.zip,.rar,.txt,.jpg,.jpeg,.png,.gif">
<el-button size="small" type="primary">
<i class="el-icon-upload"></i> 点击上传
@ -133,17 +150,28 @@
type: 'add',
id: '',
teacher_options: [],
mapparams: {
zoom: 11
},
mapform: [],
form: {
date: '',
period: '',
// timeRange: [],
period:'',
teacher_id: "",
theme: '',
address: '',
introduce: '',
address_detail: '',
latitude: '',
longitude: '',
// introduce: '',
files: [], //
},
rules: {
date: [{
required: true,
message: '请选择上课日期'
}]
},
uploadAction: `${process.env.VUE_APP_UPLOAD_API}`, // URL
uploadHeaders: {
@ -163,16 +191,26 @@
this.teacher_options.map(item => {
if (e === item.id) {
this.form.teacher_id = item.id
this.form.introduce = item.introduce
// this.form.introduce = item.introduce
}
})
}
console.log("e", e)
},
changeTime(e) {
console.log("eeee", e)
if (e) {
this.form.start_time = e[0]
this.form.end_time = e[1]
} else {
this.form.start_time = ''
this.form.end_time = ''
}
},
submit() {
if (this.id) {
this.form.id = this.id
}else{
} else {
this.form.id = ''
}
@ -181,9 +219,9 @@
const newFileIds = this.newFileList.map(file => {
return file.response ? file.response.id : file.id;
});
// IDIDID
const finalFileIds = [...existingFileIds, ...newFileIds].filter(id =>
const finalFileIds = [...existingFileIds, ...newFileIds].filter(id =>
!this.removedFileIds.includes(id)
);
@ -191,18 +229,16 @@
...this.form,
file_ids: finalFileIds
};
save(submitData).then(res => {
this.$message({
type: 'success',
message: '保存课表成功'
})
saveTeacher({
id: this.form.teacher_id,
introduce: this.form.introduce
}).then(res => {
// saveTeacher({
// id: this.form.teacher_id
// }).then(res => {
})
// })
this.isShow = false
this.$emit('refresh')
// this.active = 1
@ -214,8 +250,13 @@
id: this.id,
show_relation: ['teacher']
}).then(res => {
this.form = this.base.deepCopy(res)
this.form.introduce = res.teacher.introduce
this.form = this.base.requestToForm(res, this.form)
this.form.files = res.files ? res.files : []
// this.form.timeRange = [this.form.start_time ? this.form.start_time : '', this.form.end_time ? this.form
// .end_time : ''
// ]
// this.form.introduce = res.teacher.introduce
this.mapform = [res.longitude, res.latitude, res.address_detail]
})
},
//
@ -276,13 +317,17 @@
}
} else {
this.id = ''
this.mapform = []
this.form = {
date: '',
period: '',
// timeRange: [],
period:'',
teacher_id: "",
theme: '',
address: '',
introduce: '',
address_detail: '',
latitude: '',
longitude: '',
files: [], //
}
this.newFileList = [] //
@ -290,21 +335,31 @@
this.$refs['dialog'].reset()
}
},
mapform(newVal, oldVal) {
console.log(newVal)
this.form.longitude = newVal[0]
this.form.latitude = newVal[1]
this.form.address_detail = newVal[2]
}
}
}
</script>
<style scoped lang="scss">
::v-deep .files {
flex-basis: 100%;
}
.existing-files {
margin-bottom: 20px;
h5 {
margin: 0 0 10px 0;
color: #303133;
font-size: 14px;
}
.file-item {
display: flex;
align-items: center;
@ -312,33 +367,33 @@
padding: 8px;
background: #f5f7fa;
border-radius: 4px;
&:last-child {
margin-bottom: 0;
}
.el-link {
flex: 1;
font-size: 12px;
i {
margin-right: 4px;
}
}
}
}
.upload-section {
h5 {
margin: 0 0 10px 0;
color: #303133;
font-size: 14px;
}
.el-upload__tip {
font-size: 12px;
color: #909399;
margin-top: 5px;
}
}
</style>
</style>

@ -1,209 +1,434 @@
<template>
<div class="course-qr-admin">
<!-- 顶部标题 -->
<div class="admin-header">
<h1 class="admin-title">
<i class="el-icon-qr-code"></i>
课程二维码管理
</h1>
</div>
<div class="admin-main">
<!-- 课程列表面板 -->
<div class="course-list-panel">
<div class="panel-title">
<i class="el-icon-menu"></i>
选择课程
</div>
<template>
<div class="course-qr-admin">
<!-- 顶部标题 -->
<!-- <div class="admin-header">
<h1 class="admin-title">
<i class="el-icon-qr-code"></i>
课表二维码管理
</h1>
</div> -->
<div class="admin-main">
<!-- 课程列表面板 -->
<div class="course-list-panel">
<div class="panel-title">
<i class="el-icon-menu"></i>
选择课表
</div>
<div class="course-list-select">
<el-select style="margin:0 10px 0 0" v-model="course_id" @change="changeCourse" placeholder="请选择课程" clearable>
<el-option v-for="item in courseList" :key="item.id" :label="item.name" :value="item.id"></el-option>
</el-select>
<el-date-picker @change="changeDate" format="yyyy-MM-dd" value-format="yyyy-MM-dd" v-model="course_date" type="date" placeholder="选择上课日期">
</el-date-picker>
</div>
<el-scrollbar style="height: 600px;">
<div v-for="course in courses" :key="course.id" :class="['course-item', {active: selectedCourse && selectedCourse.id === course.id}]" @click="selectCourse(course)">
<div class="course-name">{{ course.name }}</div>
<div class="course-info">
<span><i class="el-icon-date"></i> {{ course.time }}</span>
<span><i class="el-icon-location"></i> {{ course.location }}</span>
<span><i class="el-icon-user"></i> {{ course.teacher }}</span>
</div>
</div>
</el-scrollbar>
</div>
<!-- 二维码展示面板 -->
<div class="qr-display-panel">
<div class="panel-title">
<i class="el-icon-qr-code"></i>
课程签到二维码
</div>
<div v-if="!selectedCourse" class="empty-state">
<i class="el-icon-qr-code" style="font-size:64px;opacity:0.5;"></i>
<h5>请选择课程生成二维码</h5>
<p class="text-muted">选择左侧课程后将自动生成对应的签到二维码</p>
</div>
<div v-else>
<div class="qr-container">
<h4 style="margin-bottom: 20px; color: #2c3e50;">{{ selectedCourse.name }}</h4>
<div class="qr-code">
<img :src="qrImgUrl" alt="二维码" style="width:200px;height:200px;" />
</div>
<div class="qr-info">
<h6><i class="el-icon-info"></i> 课程信息</h6>
<div class="qr-info-grid">
<div><strong>时间</strong>{{ selectedCourse.time }}</div>
<div><strong>地点</strong>{{ selectedCourse.location }}</div>
<div><strong>主讲</strong>{{ selectedCourse.teacher }}</div>
<div><strong>类型</strong>{{ getTypeName(selectedCourse.type) }}</div>
<template v-if="courseContentList.length>0">
<div v-for="course in courseContentList" :key="course.id"
:class="['course-item', {active: selectedCourse && selectedCourse.id === course.id}]"
@click="selectCourse(course)">
<div class="course-name">{{ course.theme }}</div>
<div class="course-info">
<span><i class="el-icon-date"></i> {{ course.date }} - {{course.period}}</span>
<span><i class="el-icon-location"></i> {{ course.address }}</span>
<span><i class="el-icon-user"></i> {{ course.teacher.name }}</span>
</div>
</div>
<div class="qr-actions">
<el-button type="primary" icon="el-icon-download" class="btn-qr" @click="downloadQR"></el-button>
<el-button type="success" icon="el-icon-view" class="btn-qr" @click="previewCheckin"></el-button>
<el-button type="info" icon="el-icon-document-copy" class="btn-qr" @click="copyUrl"></el-button>
</div>
</div>
<div class="usage-tips">
<h6><i class="el-icon-lightning"></i> 使用说明</h6>
<ul>
<li>学员扫描此二维码可直接进入课程签到页面</li>
<li>二维码包含课程信息自动识别对应课程</li>
<li>支持微信扫一扫</li>
<li>建议在课程开始前30分钟展示二维码</li>
<li>可下载二维码图片用于打印或分享</li>
</ul>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'CourseQr',
data() {
return {
courses: [
{
id: 1,
name: '2025产业加速营 | 智能制造专题',
time: '2025-06-04 09:00-17:00',
location: '商学院A101',
teacher: '王教授',
type: 'course'
},
{
id: 2,
name: '校友企业参访 | 新能源企业',
time: '2025-06-10 13:30-17:00',
location: '苏州高新区',
teacher: '李总监',
type: 'activity'
},
{
id: 3,
name: '移动课堂 | AI商业落地',
time: '2025-06-15 09:00-16:00',
location: '创新实验室B202',
teacher: '张教授',
type: 'workshop'
},
{
id: 4,
name: '2025校友论坛 | 数字经济新机遇',
time: '2025-06-20 14:00-17:30',
location: '报告厅',
teacher: '特邀嘉宾',
type: 'activity'
},
{
id: 5,
name: '2025暑期创新营 | 项目路演',
time: '2025-06-27 09:00-12:00',
location: '多功能厅',
teacher: '创业导师团',
type: 'course'
}
],
selectedCourse: null
}
},
watch: {
selectedCourse(val) {
if (val) this.$nextTick(this.renderQRCode)
}
},
methods: {
selectCourse(course) {
this.selectedCourse = course
},
renderQRCode() {
const checkinUrl = `${window.location.origin}/course-checkin.html?courseId=${this.selectedCourse.id}&courseName=${encodeURIComponent(this.selectedCourse.name)}`
const qrDiv = this.$refs.qrcode
qrDiv.innerHTML = ''
//
//
this.qrImgUrl = checkinUrl
},
getTypeName(type) {
const types = { course: '课程', activity: '活动', workshop: '移动课堂' }
return types[type] || type
},
downloadQR() {
if (!this.selectedCourse) return
//
const link = document.createElement('a')
link.download = `${this.selectedCourse.name}-签到二维码.png`
link.href = this.qrImgUrl
link.click()
},
previewCheckin() {
if (!this.selectedCourse) return
const checkinUrl = `${window.location.origin}/course-checkin.html?courseId=${this.selectedCourse.id}&courseName=${encodeURIComponent(this.selectedCourse.name)}`
window.open(checkinUrl, '_blank')
},
copyUrl() {
if (!this.selectedCourse) return
const checkinUrl = `${window.location.origin}/course-checkin.html?courseId=${this.selectedCourse.id}&courseName=${encodeURIComponent(this.selectedCourse.name)}`
if (navigator.clipboard) {
navigator.clipboard.writeText(checkinUrl).then(() => {
this.$message.success('签到链接已复制到剪贴板')
}).catch(() => {
this.$prompt('请复制以下链接:', '复制签到链接', { inputValue: checkinUrl })
})
} else {
this.$prompt('请复制以下链接:', '复制签到链接', { inputValue: checkinUrl })
}
}
},
computed: {
qrImgUrl() {
if (!this.selectedCourse) return ''
// /api/qr/course?id=xxx
return `/api/qr/course?id=${this.selectedCourse.id}`
}
</template>
<div v-else>
<el-empty description="暂无课表"></el-empty>
</div>
</el-scrollbar>
</div>
<!-- 二维码展示面板 -->
<div class="qr-display-panel">
<div class="panel-title">
<i class="el-icon-qr-code"></i>
课表签到二维码
</div>
<div v-if="!selectedCourse" class="empty-state">
<i class="el-icon-qr-code" style="font-size:64px;opacity:0.5;"></i>
<h2>请选择课表生成二维码</h2>
<p class="text-muted">选择左侧课表后将自动生成对应的签到二维码</p>
</div>
<div v-else>
<div class="qr-container">
<h4 style="margin-bottom: 20px; color: #2c3e50;">{{ selectedCourse.course.name }}</h4>
<div class="qr-code" v-if="qrImgUrl">
<img :src="qrImgUrl" alt="二维码" style="width:200px;height:200px;" />
</div>
<div class="qr-info">
<h3><i class="el-icon-info"></i> 课表信息</h3>
<div class="qr-info-grid">
<div><strong>主题</strong>{{ selectedCourse.theme }}</div>
<div><strong>时间</strong>{{ selectedCourse.date }} - {{selectedCourse.period}}</div>
<div><strong>地点</strong>{{ selectedCourse.address }}</div>
<div><strong>主讲</strong>{{ selectedCourse.teacher.name }}</div>
</div>
</div>
<div class="qr-actions">
<el-button type="primary" icon="el-icon-download" class="btn-qr" @click="downloadQR"></el-button>
<!-- <el-button type="success" icon="el-icon-view" class="btn-qr" @click="previewCheckin"></el-button> -->
<!-- <el-button type="info" icon="el-icon-document-copy" class="btn-qr" @click="copyUrl"></el-button> -->
</div>
</div>
<div class="usage-tips">
<h3><i class="el-icon-lightning"></i> 使用说明</h3>
<ul>
<li>学员扫描此二维码可直接进入课程签到页面</li>
<li>二维码包含课程信息自动识别对应课程</li>
<li>支持微信扫一扫</li>
<li>建议在课程开始前30分钟展示二维码</li>
<li>可下载二维码图片用于打印或分享</li>
</ul>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import {
index as courseGet
} from "@/api/course/index.js"
import {
index as courseContentGet,
qrcodeGet
} from '@/api/course/courseContent.js'
export default {
name: 'CourseQr',
data() {
return {
courseContentList: [],
courseList: [],
course_id: '',
course_date: '',
selectedCourse: null,
qrImgUrl:''
}
},
watch: {
selectedCourse(val) {
if (val) {
this.$nextTick(function(){
this.renderQRCode(val.id)
})
}
}
},
created() {
this.getCourse()
},
methods: {
//
changeCourse(e) {
console.log("e", e)
if (e) {
this.getCourseContent(e)
} else {
if (this.course_date) {
this.getCourseContent('', this.course_date)
} else {
this.courseContentList = []
}
}
},
changeDate(e) {
if (e) {
this.getCourseContent(this.course_id, e)
} else {
if (this.course_id) {
this.getCourseContent(this.course_id)
} else {
this.courseContentList = []
}
}
},
async getCourse() {
const res = await courseGet({
page: 1,
page_size: 999,
filter: [{
key: 'course_status',
op: 'eq',
value: 10
}]
})
this.courseList = res.data
},
async getCourseContent(course_id, date) {
const res = await courseContentGet({
page: 1,
page_size: 999,
show_relation: ['teacher','course'],
sort_name: 'date',
sort_type: 'DESC',
filter: [{
key: 'course_id',
op: 'eq',
value: course_id ? course_id : ''
}, {
key: 'date',
op: 'eq',
value: date ? date : ''
}]
})
this.courseContentList = res.data
},
selectCourse(course) {
this.selectedCourse = course
},
async renderQRCode(id) {
const res = await qrcodeGet({
id:id
})
this.qrImgUrl = res.msg
},
downloadQR() {
if (!this.selectedCourse) return
//
const link = document.createElement('a')
link.href = this.qrImgUrl
link.download = `${this.selectedCourse.theme}-签到二维码.png`
link.target = '_blank'
link.click()
link.remove(); //
},
previewCheckin() {
if (!this.selectedCourse) return
const checkinUrl =
`${window.location.origin}/course-checkin.html?courseId=${this.selectedCourse.id}&courseName=${encodeURIComponent(this.selectedCourse.name)}`
window.open(checkinUrl, '_blank')
},
copyUrl() {
if (!this.selectedCourse) return
const checkinUrl =
`${window.location.origin}/course-checkin.html?courseId=${this.selectedCourse.id}&courseName=${encodeURIComponent(this.selectedCourse.name)}`
if (navigator.clipboard) {
navigator.clipboard.writeText(checkinUrl).then(() => {
this.$message.success('签到链接已复制到剪贴板')
}).catch(() => {
this.$prompt('请复制以下链接:', '复制签到链接', {
inputValue: checkinUrl
})
})
} else {
this.$prompt('请复制以下链接:', '复制签到链接', {
inputValue: checkinUrl
})
}
}
},
computed: {
}
}
</script>
<style scoped>
.course-qr-admin {
background: #f8f9fa;
min-height: 100vh;
}
.admin-header {
background: white;
padding: 20px 30px;
border-bottom: 1px solid #e9ecef;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.admin-title {
font-size: 24px;
font-weight: 700;
color: #2c3e50;
margin: 0;
display: flex;
align-items: center;
gap: 12px;
}
.admin-main {
display: flex;
gap: 24px;
padding: 30px;
min-height: calc(100vh - 100px);
}
.course-list-panel {
flex: 1;
background: white;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
padding: 24px;
max-width: 400px;
}
.course-list-select{
display: flex;
align-items: center;
margin-bottom:10px;
}
}
</script>
<style scoped>
.course-qr-admin { background: #f8f9fa; min-height: 100vh; }
.admin-header { background: white; padding: 20px 30px; border-bottom: 1px solid #e9ecef; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
.admin-title { font-size: 24px; font-weight: 700; color: #2c3e50; margin: 0; display: flex; align-items: center; gap: 12px; }
.admin-main { display: flex; gap: 24px; padding: 30px; min-height: calc(100vh - 100px); }
.course-list-panel { flex: 1; background: white; border-radius: 12px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); padding: 24px; max-width: 400px; }
.qr-display-panel { flex: 2; background: white; border-radius: 12px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); padding: 24px; display: flex; flex-direction: column; align-items: center; text-align: center; }
.panel-title { font-size: 18px; font-weight: 600; margin-bottom: 20px; color: #3498db; display: flex; align-items: center; gap: 8px; }
.course-item { border: 1px solid #e9ecef; border-radius: 8px; padding: 16px; margin-bottom: 12px; cursor: pointer; transition: all 0.3s ease; }
.course-item:hover { border-color: #3498db; box-shadow: 0 2px 8px rgba(52, 152, 219, 0.2); }
.course-item.active { border-color: #3498db; background-color: #e3f2fd; }
.course-name { font-weight: 600; font-size: 16px; margin-bottom: 8px; color: #2c3e50; }
.course-info { font-size: 14px; color: #6c757d; display: flex; flex-direction: column; gap: 4px; }
.qr-container { background: white; border: 2px solid #e9ecef; border-radius: 16px; padding: 30px; margin: 20px 0; box-shadow: 0 4px 12px rgba(0,0,0,0.1); }
.qr-code { margin: 0 auto; }
.qr-info { margin-top: 20px; padding: 20px; background: #f8f9fa; border-radius: 12px; text-align: left; }
.qr-info-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 12px; margin-top: 12px; }
.qr-actions { display: flex; gap: 12px; margin-top: 20px; justify-content: center; }
.btn-qr { padding: 12px 24px; border-radius: 8px; font-weight: 500; display: flex; align-items: center; gap: 8px; transition: all 0.3s ease; }
.btn-qr:hover { transform: translateY(-2px); box-shadow: 0 4px 8px rgba(0,0,0,0.2); }
.empty-state { display: flex; flex-direction: column; align-items: center; justify-content: center; height: 400px; color: #6c757d; }
.usage-tips { background: #e8f5e9; border: 1px solid #c8e6c9; border-radius: 8px; padding: 16px; margin-top: 20px; text-align: left; }
.usage-tips h6 { color: #2e7d32; margin-bottom: 12px; }
.usage-tips ul { margin: 0; padding-left: 20px; color: #2e7d32; }
@media (max-width: 768px) { .admin-main { flex-direction: column; padding: 15px; gap: 15px; } .course-list-panel { max-width: none; } .qr-container { padding: 20px; } .qr-actions { flex-direction: column; } .btn-qr { width: 100%; justify-content: center; } }
.qr-display-panel {
flex: 2;
background: white;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
padding: 24px;
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
}
.panel-title {
font-size: 18px;
font-weight: 600;
margin-bottom: 20px;
color: #3498db;
display: flex;
align-items: center;
gap: 8px;
}
.course-item {
border: 1px solid #e9ecef;
border-radius: 8px;
padding: 16px;
margin-bottom: 12px;
cursor: pointer;
transition: all 0.3s ease;
}
.course-item:hover {
border-color: #3498db;
box-shadow: 0 2px 8px rgba(52, 152, 219, 0.2);
}
.course-item.active {
border-color: #3498db;
background-color: #e3f2fd;
}
.course-name {
font-weight: 600;
font-size: 16px;
margin-bottom: 8px;
color: #2c3e50;
}
.course-info {
font-size: 14px;
color: #6c757d;
display: flex;
flex-direction: column;
gap: 4px;
}
.qr-container {
background: white;
border: 2px solid #e9ecef;
border-radius: 16px;
padding: 30px;
margin: 20px 0;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.qr-code {
margin: 0 auto;
}
.qr-info {
margin-top: 20px;
padding: 20px;
background: #f8f9fa;
border-radius: 12px;
text-align: left;
}
.qr-info-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(600px, 1fr));
gap: 12px;
margin-top: 12px;
}
.qr-actions {
display: flex;
gap: 12px;
margin-top: 20px;
justify-content: center;
}
.btn-qr {
padding: 12px 24px;
border-radius: 8px;
font-weight: 500;
display: flex;
align-items: center;
gap: 8px;
transition: all 0.3s ease;
}
.btn-qr:hover {
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
}
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 400px;
color: #6c757d;
}
.usage-tips {
background: #e8f5e9;
border: 1px solid #c8e6c9;
border-radius: 8px;
padding: 16px;
margin-top: 20px;
text-align: left;
}
.usage-tips h6 {
color: #2e7d32;
margin-bottom: 12px;
}
.usage-tips ul {
margin: 0;
padding-left: 20px;
color: #2e7d32;
}
@media (max-width: 768px) {
.admin-main {
flex-direction: column;
padding: 15px;
gap: 15px;
}
.course-list-panel {
max-width: none;
}
.qr-container {
padding: 20px;
}
.qr-actions {
flex-direction: column;
}
.btn-qr {
width: 100%;
justify-content: center;
}
}
</style>

Loading…
Cancel
Save