registration.vue 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596
  1. <template>
  2. <section>
  3. <!-- 工具条 -->
  4. <el-col :span="24" class="toolbar" style="padding-bottom: 0px;">
  5. <el-tabs v-model="activeTab" @tab-click="handleClick">
  6. <el-tab-pane label="预报名信息" name="pre-registration"></el-tab-pane>
  7. <el-tab-pane label="报名课程" name="course-registration">
  8. <div class="registerForCourses">
  9. <el-form :inline="true" @submit.native.prevent>
  10. <el-form-item label="姓名">
  11. <el-input v-model="listQuery.name" placeholder="请输入姓名" clearable @change="handleFilter"
  12. size="small"></el-input>
  13. </el-form-item>
  14. <el-form-item label="手机号">
  15. <el-input v-model="listQuery.phone" placeholder="请输入手机号" clearable @change="handleFilter"
  16. size="small"></el-input>
  17. </el-form-item>
  18. </el-form>
  19. <div>
  20. <el-button type="primary" size="small" @click="showAddDialog">添加报名</el-button>
  21. <el-button type="primary" size="small" @click="batchManage">线下课程管理</el-button>
  22. </div>
  23. </div>
  24. </el-tab-pane>
  25. </el-tabs>
  26. </el-col>
  27. <!-- 预报名列表 -->
  28. <transition name="fade" mode="out-in">
  29. <el-table v-if="activeTab === 'pre-registration'" :data="preRegistrationList" highlight-current-row
  30. v-loading="listLoading" :height="tableHeight" style="width: 100%;">
  31. <el-table-column label="序号" prop="id" width="80" align="center"></el-table-column>
  32. <el-table-column label="姓名" prop="name" align="center"></el-table-column>
  33. <el-table-column label="手机号" prop="phone" align="center"></el-table-column>
  34. <el-table-column label="操作" width="180" align="center" fixed="right">
  35. <template slot-scope="scope">
  36. <el-button size="small" type="danger" @click="handleDelete(scope.row)">删除</el-button>
  37. </template>
  38. </el-table-column>
  39. </el-table>
  40. </transition>
  41. <!-- 报名课程列表 -->
  42. <transition name="fade" mode="out-in">
  43. <div>
  44. <el-table v-if="activeTab === 'course-registration'" :data="courseRegistrationList" highlight-current-row
  45. v-loading="listLoading" :height="tableHeight - 58" style="width: 100%;">
  46. <el-table-column label="序号" width="80" align="center">
  47. <template slot-scope="scope">{{ scope.$index + 1 }}</template>
  48. </el-table-column>
  49. <el-table-column label="姓名" prop="name" width="140" align="center"></el-table-column>
  50. <el-table-column label="手机号" prop="phone" width="200" align="center"></el-table-column>
  51. <el-table-column label="报名课程" prop="course" align="center"></el-table-column>
  52. <el-table-column label="操作" width="260" align="center" fixed="right">
  53. <template slot-scope="scope">
  54. <el-button size="small" type="primary" @click="editCourseRegistration(scope.row)">编辑</el-button>
  55. <el-button size="small" type="danger" @click="handleDelete(scope.row)">删除</el-button>
  56. <el-button size="small" type="primary" @click="openExam(scope.row)">开通考试</el-button>
  57. </template>
  58. </el-table-column>
  59. </el-table>
  60. </div>
  61. </transition>
  62. <!-- 添加报名弹窗 -->
  63. <el-dialog title="线下研修班报名" :visible.sync="dialogVisible" width="30%">
  64. <el-form :model="registrationForm" label-width="80px">
  65. <el-form-item label="姓名">
  66. <el-input v-model.trim="registrationForm.name" placeholder="请输入姓名"></el-input>
  67. </el-form-item>
  68. <el-form-item label="手机">
  69. <el-input v-model.trim="registrationForm.phone" placeholder="请输入手机号"></el-input>
  70. </el-form-item>
  71. <el-form-item label="报名课程">
  72. <el-select v-model="registrationForm.course" multiple placeholder="请选择课程" style="width: 100%" clearable>
  73. <el-option v-for="item in categoryList" :key="item.value" :label="item.label" :value="item.value">
  74. </el-option>
  75. </el-select>
  76. </el-form-item>
  77. </el-form>
  78. <span slot="footer" class="dialog-footer">
  79. <el-button @click="dialogVisible = false">取消</el-button>
  80. <el-button type="primary" @click="submitRegistration">确定</el-button>
  81. </span>
  82. </el-dialog>
  83. <!-- 分页 -->
  84. <el-col :span="24" class="toolbar">
  85. <el-pagination :current-page="pageIndex" :page-size="pageSize" :total="total"
  86. layout="total, prev, pager, next, sizes" @current-change="handleCurrentChange" @size-change="handleSizeChange"
  87. style="float:right;"></el-pagination>
  88. </el-col>
  89. <!-- 课程管理 -->
  90. <el-dialog :visible.sync="categoryManageVisible" title="线下课程管理" width="600px" :before-close="handleClose">
  91. <el-table :data="categoryList" style="width: 100%" max-height="400">
  92. <el-table-column prop="label" label="课程名称" width="180"></el-table-column>
  93. <el-table-column label="封面" width="180">
  94. <template slot-scope="scope">
  95. <img v-if="scope.row.coverImage" :src="scope.row.coverImage" class="category-cover-image" />
  96. <span v-else>无封面</span>
  97. </template>
  98. </el-table-column>
  99. <el-table-column label="操作" width="180">
  100. <template slot-scope="scope">
  101. <el-button size="mini" type="primary" @click="setCategoryCover(scope.$index, scope.row)">设置封面</el-button>
  102. <el-button size="mini" type="danger" @click="deleteCategory(scope.$index, scope.row)">删除</el-button>
  103. </template>
  104. </el-table-column>
  105. </el-table>
  106. <div style="margin-top: 20px;">
  107. <el-form :inline="true" :model="newCategory" class="demo-form-inline">
  108. <el-form-item label="课程名称">
  109. <el-input v-model="newCategory.label" placeholder="请输入课程名称"></el-input>
  110. </el-form-item>
  111. <el-form-item>
  112. <el-button type="primary" @click="addCategory">添加</el-button>
  113. </el-form-item>
  114. </el-form>
  115. </div>
  116. <span slot="footer" class="dialog-dialog">
  117. <el-button @click="categoryManageVisible = false">关 闭</el-button>
  118. </span>
  119. </el-dialog>
  120. <!-- 设置课程封面 -->
  121. <el-dialog :visible.sync="coverDialogVisible" title="设置课程封面" width="500px" :before-close="handleClose">
  122. <div class="cover-upload-container">
  123. <el-upload class="cover-uploader" action="#" :show-file-list="false" :on-change="handleCoverChange"
  124. :auto-upload="false" :before-upload="beforeCoverUpload">
  125. <img v-if="coverImageUrl" :src="coverImageUrl" class="cover-image" />
  126. <i v-else class="el-icon-plus cover-uploader-icon"></i>
  127. </el-upload>
  128. <div class="cover-tip">请上传课程封面图片,建议尺寸 16:9</div>
  129. </div>
  130. <span slot="footer" class="dialog-footer">
  131. <el-button @click="coverDialogVisible = false">取 消</el-button>
  132. <el-button type="primary" @click="saveCategoryCover">确 定</el-button>
  133. </span>
  134. </el-dialog>
  135. <!-- 开通考试 -->
  136. <el-dialog title="开通考试" :visible.sync="openExamDialogVisible" width="600px" top="6.5vh" :before-close="handleClose">
  137. <div style="width: 100%;">
  138. <el-select v-model="examRow.openExamVal" multiple placeholder="请选择" style="width: 100%;">
  139. <el-option v-for="item in (examRow.offlineSignList || [])" :key="item.id" :label="item.courseOfflineName"
  140. :value="item.courseOfflineId">
  141. </el-option>
  142. </el-select>
  143. </div>
  144. <span slot="footer" class="dialog-footer">
  145. <el-button type="primary" @click="openExamDialogVisible = false">取消</el-button>
  146. <el-button type="primary" @click="openExamCli()">确 定</el-button>
  147. </span>
  148. </el-dialog>
  149. </section>
  150. </template>
  151. <script>
  152. import { post, checkAndAddUpload } from '../../api'
  153. export default {
  154. name: 'OfflineRegistration',
  155. data() {
  156. return {
  157. activeTab: 'pre-registration',
  158. listQuery: {
  159. name: '',
  160. phone: ''
  161. },
  162. pageIndex: 1,
  163. pageSize: 20,
  164. total: 0,
  165. listLoading: false,
  166. tableHeight: 0,
  167. preRegistrationList: [],
  168. courseRegistrationList: [],
  169. dialogVisible: false,
  170. registrationForm: {
  171. name: '',
  172. phone: '',
  173. course: ''
  174. },
  175. courseOptions: [
  176. { value: 'Vue高级课程', label: 'Vue高级课程' },
  177. { value: 'React入门', label: 'React入门' },
  178. { value: 'JavaScript基础', label: 'JavaScript基础' },
  179. { value: 'Node.js实战', label: 'Node.js实战' }
  180. ],
  181. // 课程管理相关
  182. categoryManageVisible: false,
  183. categoryList: [],
  184. newCategory: {
  185. label: '',
  186. value: '',
  187. coverImage: ''
  188. },
  189. // 课程封面设置
  190. coverDialogVisible: false,
  191. currentCategoryIndex: -1,
  192. coverImageUrl: '',
  193. coverImageFile: null,
  194. // 开通考试
  195. openExamDialogVisible: false,
  196. examRow: {},
  197. }
  198. },
  199. created() {
  200. let height = window.innerHeight
  201. this.tableHeight = height - 195
  202. const that = this
  203. window.onresize = function temp() {
  204. that.tableHeight = window.innerHeight - 195
  205. console.log(that.tableHeight, '<==== that.tableHeight')
  206. }
  207. },
  208. methods: {
  209. handleClick() {
  210. this.getList()
  211. },
  212. getList() {
  213. this.listLoading = true
  214. if (this.activeTab === 'pre-registration') {
  215. // 预报名
  216. post(`/user-offline-sign-up/signPageList`, {
  217. page: this.pageIndex,
  218. size: this.pageSize,
  219. }).then((res) => {
  220. const { records = [], total = 0 } = res.data
  221. this.preRegistrationList = records
  222. this.total = total
  223. }).finally(() => {
  224. this.listLoading = false
  225. })
  226. }
  227. // 报名课程
  228. if (this.activeTab === 'course-registration') {
  229. post(`/course-offline-sign/userSignPageList`, {
  230. page: this.pageIndex,
  231. size: this.pageSize,
  232. ...this.listQuery
  233. }).then((res) => {
  234. const { records = [], total = 0 } = res.data
  235. this.courseRegistrationList = records.map((item) => {
  236. return {
  237. ...item,
  238. course: item.offlineSignList.map(ua => ua.courseOfflineName).join(','),
  239. courseIds: item.offlineSignList.map(ua => ua.courseOfflineId).join(','),
  240. courseIdList: item.offlineSignList.map(ua => ua.courseOfflineId)
  241. }
  242. })
  243. this.total = total
  244. }).finally(() => {
  245. this.listLoading = false
  246. })
  247. }
  248. },
  249. handleFilter() {
  250. this.pageIndex = 1
  251. this.getList()
  252. },
  253. handleCurrentChange(val) {
  254. this.pageIndex = val
  255. this.getList()
  256. },
  257. handleSizeChange(val) {
  258. this.pageIndex = 1
  259. this.pageSize = val
  260. this.getList()
  261. },
  262. editCourseRegistration(row) {
  263. console.log(row, '<=== 编辑')
  264. this.dialogVisible = true
  265. this.registrationForm = {
  266. id: row.id,
  267. name: row.name,
  268. phone: row.phone,
  269. course: row.courseIdList
  270. }
  271. },
  272. handleDelete(row) {
  273. const url = this.activeTab == 'pre-registration' ? '/user-offline-sign-up/deleteSignUp' : '/course-offline-sign/deleteUserSign'
  274. const text = `确认删除【${row.name}】吗?`
  275. const prompt = this.activeTab == 'pre-registration' ? '删除预报名信息人员' : '删除报名课程人员'
  276. this.$confirm(text, prompt, {
  277. confirmButtonText: '确定',
  278. cancelButtonText: '取消',
  279. type: 'warning'
  280. }).then(() => {
  281. post(url, { id: row.id }).then(() => {
  282. this.$message({
  283. type: 'success',
  284. message: '删除成功!'
  285. });
  286. this.getList()
  287. })
  288. });
  289. },
  290. showAddDialog() {
  291. this.registrationForm = {
  292. name: '',
  293. phone: '',
  294. course: []
  295. }
  296. this.dialogVisible = true
  297. },
  298. submitRegistration() {
  299. // 这里添加提交逻辑
  300. const { id, name, phone, course = [] } = this.registrationForm
  301. if(!name || !phone || !course.length) {
  302. this.$message({
  303. type: 'error',
  304. message: '请填写完整信息'
  305. })
  306. return
  307. }
  308. const urls = !id ? '/course-offline-sign/save' : '/course-offline-sign/update'
  309. let obj = { name, phone, courseOffLineIdStr: course.join(',') }
  310. if (id) {
  311. obj.userSignId = id
  312. }
  313. post(urls, { ...obj }).then(() => {
  314. this.$message({
  315. type: 'success',
  316. message: id ? '操作成功!' : '报名成功!'
  317. });
  318. this.getList()
  319. this.dialogVisible = false;
  320. })
  321. },
  322. // 课程管理
  323. batchManage(flag = true) {
  324. if (flag) {
  325. this.categoryManageVisible = true;
  326. }
  327. // 加载课程数据
  328. this.http.post('/course-offline/list', {}, res => {
  329. if (res.code == "ok") {
  330. // 将后端返回的数据转换为前端需要的格式
  331. this.categoryList = res.data.map(item => ({
  332. label: item.courseName,
  333. value: item.id,
  334. coverImage: checkAndAddUpload(item.coverImage || '')
  335. }));
  336. // 同步更新下拉选项
  337. this.courseOptions = [...this.categoryList];
  338. } else {
  339. this.$message({
  340. message: res.msg || '获取课程列表失败',
  341. type: 'error'
  342. });
  343. }
  344. }, error => {
  345. this.$message({
  346. message: error || '获取课程列表失败',
  347. type: 'error'
  348. });
  349. });
  350. },
  351. // 设置课程封面
  352. setCategoryCover(index, row) {
  353. this.currentCategoryIndex = index;
  354. this.coverImageUrl = row.coverImage || '';
  355. this.coverDialogVisible = true;
  356. },
  357. // 处理封面图片变更
  358. handleCoverChange(file) {
  359. this.coverImageFile = file.raw;
  360. if (this.coverImageFile) {
  361. this.coverImageUrl = URL.createObjectURL(this.coverImageFile);
  362. // 获取当前课程的ID
  363. if (this.currentCategoryIndex >= 0) {
  364. const categoryId = this.categoryList[this.currentCategoryIndex].value;
  365. // 立即上传封面图片
  366. this.listLoading = true;
  367. // 创建FormData对象用于上传文件
  368. const formData = new FormData();
  369. formData.append('id', categoryId);
  370. formData.append('coverImage', this.coverImageFile);
  371. // 调用上传图片的API
  372. this.http.uploadFile('/course-offline/uploadCover', formData, res => {
  373. this.listLoading = false;
  374. if (res.code == "ok") {
  375. // 上传成功后,获取返回的图片URL
  376. const imageUrl = res.data && res.data.url ? res.data.url : this.coverImageUrl;
  377. // 更新本地数据
  378. this.categoryList[this.currentCategoryIndex].coverImage = imageUrl;
  379. // 同步更新到课程选项中
  380. const optionIndex = this.courseOptions.findIndex(item => item.value === categoryId);
  381. if (optionIndex !== -1) {
  382. this.courseOptions[optionIndex].coverImage = imageUrl;
  383. }
  384. this.$message({
  385. type: 'success',
  386. message: '封面图片上传成功!'
  387. });
  388. } else {
  389. this.$message({
  390. message: res.msg || '上传封面图片失败',
  391. type: 'error'
  392. });
  393. }
  394. }, error => {
  395. this.listLoading = false;
  396. this.$message({
  397. message: error || '上传封面图片失败',
  398. type: 'error'
  399. });
  400. });
  401. }
  402. }
  403. },
  404. // 处理封面图片上传前的验证
  405. beforeCoverUpload(file) {
  406. const isImage = file.type.indexOf('image/') === 0;
  407. const isLt2M = file.size / 1024 / 1024 < 2;
  408. if (!isImage) {
  409. this.$message.error('上传封面图片只能是图片格式!');
  410. }
  411. if (!isLt2M) {
  412. this.$message.error('上传封面图片大小不能超过 2MB!');
  413. }
  414. return isImage && isLt2M;
  415. },
  416. // 保存课程封面
  417. saveCategoryCover() {
  418. if (!this.coverImageUrl) {
  419. this.$message.warning('请先上传封面图片!');
  420. return;
  421. }
  422. // 关闭对话框
  423. this.coverDialogVisible = false;
  424. this.$message({
  425. type: 'success',
  426. message: '设置封面成功!'
  427. });
  428. },
  429. // 添加课程
  430. addCategory() {
  431. if (!this.newCategory.label || !this.newCategory.label.trim()) {
  432. this.$message({
  433. type: 'warning',
  434. message: '请输入课程名称!'
  435. });
  436. return;
  437. }
  438. // 调用后端API保存课程课程
  439. this.http.post('/course-offline/saveOrUpdate', {
  440. courseName: this.newCategory.label
  441. }, res => {
  442. if (res.code == "ok") {
  443. // 生成唯一ID作为value,实际项目中应该使用后端返回的ID
  444. const categoryId = res.data && res.data.id ? res.data.id : 'category_' + Date.now();
  445. this.newCategory.value = categoryId;
  446. // 添加到课程列表
  447. this.categoryList.push({ ...this.newCategory });
  448. this.courseOptions.push({ ...this.newCategory });
  449. // 清空输入
  450. this.newCategory.label = '';
  451. this.newCategory.value = '';
  452. this.$message({
  453. type: 'success',
  454. message: '添加课程成功!'
  455. });
  456. } else {
  457. this.$message({
  458. message: res.msg || '添加课程失败',
  459. type: 'error'
  460. });
  461. }
  462. }, error => {
  463. this.$message({
  464. message: error || '添加课程失败',
  465. type: 'error'
  466. });
  467. });
  468. },
  469. // 删除课程
  470. deleteCategory(index, row) {
  471. this.$confirm('确认删除该线下课程?', '提示', {
  472. confirmButtonText: '确定',
  473. cancelButtonText: '取消',
  474. type: 'warning'
  475. }).then(() => {
  476. // 调用后端API删除课程课程
  477. this.http.post('/course-offline/delete', {
  478. id: row.value
  479. }, res => {
  480. if (res.code == "ok") {
  481. // 从课程列表中删除
  482. this.categoryList.splice(index, 1);
  483. // 从下拉选项中删除
  484. const optionIndex = this.courseOptions.findIndex(item => item.value === row.value);
  485. if (optionIndex !== -1) {
  486. this.courseOptions.splice(optionIndex, 1);
  487. }
  488. this.$message({
  489. type: 'success',
  490. message: '删除课程成功!'
  491. });
  492. } else {
  493. this.$message({
  494. message: res.msg || '删除课程失败',
  495. type: 'error'
  496. });
  497. }
  498. }, error => {
  499. this.$message({
  500. message: error || '删除课程失败',
  501. type: 'error'
  502. });
  503. });
  504. });
  505. },
  506. // 打开开通考试弹窗
  507. openExam(row) {
  508. const obj = { ...row, openExamVal: [] }
  509. this.$set(this, 'examRow', obj)
  510. this.openExamDialogVisible = true;
  511. },
  512. // 开通考试
  513. openExamCli() {
  514. const { id, openExamVal = [] } = this.examRow;
  515. console.log(openExamVal, '<==== openExamVal')
  516. if(!openExamVal.length) {
  517. this.$message({
  518. type: 'warning',
  519. message: '请选择要开通的考试'
  520. })
  521. return
  522. }
  523. post(`/course-offline-sign/openCourseExam`, {
  524. userSignId: id,
  525. courseOffLineIdStr: openExamVal.join(',')
  526. }).then(() => {
  527. this.$message({
  528. type: 'success',
  529. message: '开通考试成功'
  530. })
  531. this.openExamDialogVisible = false;
  532. this.getList()
  533. })
  534. },
  535. handleClose(done) {
  536. done()
  537. }
  538. },
  539. mounted() {
  540. this.getList()
  541. this.batchManage(false)
  542. }
  543. }
  544. </script>
  545. <style lang="scss" scoped>
  546. .registerForCourses {
  547. display: flex;
  548. justify-content: space-between;
  549. align-items: center;
  550. }
  551. .toolbar {
  552. padding-bottom: 10px;
  553. }
  554. .fade-enter-active,
  555. .fade-leave-active {
  556. transition: opacity .3s ease;
  557. }
  558. .fade-enter,
  559. .fade-leave-to {
  560. opacity: 0;
  561. }
  562. </style>