detail.vue 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647
  1. <template>
  2. <div id="index">
  3. <el-row>
  4. <el-col :span="24" class="main animate__animated animate__backInRight" v-loading="loading">
  5. <div class="w_1200">
  6. <el-col :span="24" class="one">
  7. <el-row :span="24" class="list">
  8. <el-col :span="4" class="left">
  9. <el-image v-if="info.file && info.file.length > 0" class="image" :src="info.file[0].url" fit="fill" />
  10. <el-image v-else class="image" :src="news" fit="fill" />
  11. </el-col>
  12. <el-col :span="18" class="right">
  13. <div class="join" :class="[info.match_status == '0' ? 'join0' : 'join1']">
  14. {{ getDict(info.match_status, 'status') }}
  15. </div>
  16. <el-col :span="24" class="right_1">
  17. <span class="type">{{ getDict(info.form, 'form') }}</span>
  18. <span class="title">{{ info.name || '暂无比赛名称' }}</span>
  19. </el-col>
  20. <el-col :span="24" class="right_2"> 组织单位:{{ info.organization || '暂无组织单位' }} </el-col>
  21. <el-col :span="24" class="right_2"> 行业:{{ getDict(info.industry, 'industry') }} </el-col>
  22. <el-col :span="24" class="right_3">
  23. <el-col :span="20" class="right_3Left"> 比赛日期:{{ getTime(info.time) }} </el-col>
  24. <el-col :span="4" class="right_3Right">
  25. {{ info.money || '免费' }}
  26. </el-col>
  27. </el-col>
  28. <el-col :span="24" class="right_4">
  29. <el-tag type="primary">{{ getDict(info.type, 'type') }}</el-tag>
  30. </el-col>
  31. </el-col>
  32. <el-col :span="2" class="file" @click="toCollection(0)" v-if="info.is_collection">
  33. <el-icon :size="16"><StarFilled /></el-icon>
  34. <span>已收藏</span>
  35. </el-col>
  36. <el-col :span="2" class="file" @click="toCollection(1)" v-else>
  37. <el-icon :size="16"><Star /></el-icon>
  38. <span>收藏</span>
  39. </el-col>
  40. </el-row>
  41. </el-col>
  42. <el-col :span="24" class="two">
  43. <el-row class="two_1">
  44. <el-col :span="22" class="twoLeft">
  45. <el-tabs v-model="activeName" class="demo-tabs" @tab-click="handleClick">
  46. <el-tab-pane label="赛制规则" name="first"></el-tab-pane>
  47. <el-tab-pane label="常见问题" name="second"></el-tab-pane>
  48. </el-tabs>
  49. </el-col>
  50. <el-col :span="2" class="twoRight" v-if="info.match_status == '1'">
  51. <a-button type="primary" @click="toSign"> 报名参赛 </a-button>
  52. </el-col>
  53. </el-row>
  54. <el-row class="two_2" v-if="activeName === 'first'">
  55. <el-container style="height: 500px">
  56. <el-aside width="200px">
  57. <el-menu default-active="rules1" class="el-menu" @select="selectOpen">
  58. <el-menu-item :index="item.label" v-for="(item, index) in menuList" :key="index">
  59. <el-icon><Opportunity /></el-icon>
  60. <span>{{ item.title }}</span>
  61. </el-menu-item>
  62. </el-menu>
  63. </el-aside>
  64. <el-main>
  65. <el-col :span="24">
  66. <h2>{{ rulesInfo.title }}</h2>
  67. <div class="rich-text-container" v-html="rulesInfo.content"></div>
  68. </el-col>
  69. </el-main>
  70. </el-container>
  71. </el-row>
  72. <el-row class="two_2" v-if="activeName === 'second'">
  73. <div v-html="info.brief"></div>
  74. </el-row>
  75. </el-col>
  76. <el-col :span="24" class="thr">
  77. <el-col :span="24" class="thr_1"> 相关推荐 </el-col>
  78. <el-col :span="24" class="thr_2">
  79. <el-col :span="11" class="list" v-for="(item, index) in list" :key="index" @click="toView(item)">
  80. <div class="join" :class="[item.match_status == '0' ? 'join0' : 'join1']">
  81. {{ getDict(item.match_status, 'status') }}
  82. </div>
  83. <el-col :span="6" class="left">
  84. <el-image v-if="item.file && item.file.length > 0" class="image" :src="item.file[0].url" fit="fill" />
  85. <el-image v-else class="image" :src="news" fit="fill" />
  86. </el-col>
  87. <el-col :span="18" class="right">
  88. <el-col :span="24" class="right_1">
  89. <span class="type">{{ getDict(item.form, 'form') }}</span>
  90. <span class="title">{{ item.name || '暂无比赛名称' }}</span>
  91. </el-col>
  92. <el-col :span="24" class="right_2"> 组织单位:{{ item.organization || '暂无组织单位' }} </el-col>
  93. <el-col :span="24" class="right_3">
  94. <el-col :span="16" class="right_3Left"> 比赛日期:{{ getTime(item.time) }} </el-col>
  95. <el-col :span="8" class="right_3Right">
  96. {{ item.money || '免费' }}
  97. </el-col>
  98. </el-col>
  99. <el-col :span="24" class="right_4">
  100. <el-tag type="primary">{{ getDict(item.type, 'type') }}</el-tag>
  101. </el-col>
  102. </el-col>
  103. </el-col>
  104. </el-col>
  105. </el-col>
  106. </div>
  107. </el-col>
  108. </el-row>
  109. <el-dialog v-model="dialog" title="报名参赛" :destroy-on-close="true" @close="toClose">
  110. <data-form></data-form>
  111. </el-dialog>
  112. </div>
  113. </template>
  114. <script setup>
  115. import moment from 'moment'
  116. import { get, cloneDeep } from 'lodash-es'
  117. const $checkRes = inject('$checkRes')
  118. // 组件
  119. import dataForm from './parts/index.vue'
  120. // 接口
  121. import { UserStore } from '@/store/user'
  122. const userStore = UserStore()
  123. const user = computed(() => userStore.user)
  124. import { DictDataStore } from '@/store/api/system/dictData'
  125. import { CollectionStore } from '@/store/api/platform/collection'
  126. import { MatchStore } from '@/store/api/platform/match'
  127. import { SignStore } from '@/store/api/platform/sign'
  128. const store = MatchStore()
  129. const signStore = SignStore()
  130. const collectionStore = CollectionStore()
  131. const dictDataStore = DictDataStore()
  132. // 图片引入
  133. import news from '@/assets/news.png'
  134. // 路由
  135. const route = useRoute()
  136. const router = useRouter()
  137. // 加载中
  138. const loading = ref(false)
  139. const info = ref({})
  140. const rulesInfo = ref({})
  141. const activeName = ref('first')
  142. // 字典表
  143. const statusList = ref([])
  144. const typeList = ref([])
  145. const formList = ref([])
  146. const cardTypeList = ref([])
  147. const industryList = ref([])
  148. const menuList = ref([
  149. { title: '大赛背景', label: 'rules1' },
  150. { title: '大赛主题和目标', label: 'rules2' },
  151. { title: '大赛基本情况介绍', label: 'rules3' },
  152. { title: '赛题任务', label: 'rules4' },
  153. { title: '赛程安排', label: 'rules5' },
  154. { title: '赛制阶段', label: 'rules6' },
  155. { title: '参赛资格', label: 'rules7' },
  156. { title: '参赛报名', label: 'rules8' },
  157. { title: '奖项设置与奖励办法', label: 'rules9' },
  158. { title: '组织单位', label: 'rules10' },
  159. { title: '赛事联络', label: 'rules11' },
  160. { title: '赛事交流', label: 'rules12' }
  161. ])
  162. // 弹框
  163. const ruleFormRef = ref()
  164. const form = ref({})
  165. const validatePhoneNumber = (rule, value, callback) => {
  166. const reg = /^1[3-9]\d{9}$/
  167. if (!value) {
  168. return callback(new Error('手机号不能为空'))
  169. }
  170. if (!reg.test(value)) {
  171. return callback(new Error('请输入正确的手机号'))
  172. }
  173. callback()
  174. }
  175. const validateCardNumber = (rule, value, callback) => {
  176. var reg = /(^\d{15}$)|(^\d{18}$)|(^\d{17}(\d|X|x)$)/
  177. if (!value) {
  178. return callback(new Error('证件号码不能为空'))
  179. }
  180. if (!reg.test(value)) {
  181. return callback(new Error('请输入正确的证件号码'))
  182. }
  183. callback()
  184. }
  185. const rules = reactive({
  186. name: [{ required: true, message: '请输入姓名', trigger: 'blur' }],
  187. phone: [{ required: true, validator: validatePhoneNumber, trigger: 'blur' }],
  188. cardType: [{ required: true, message: '请选择证件类型', trigger: 'change' }],
  189. card: [{ required: true, validator: validateCardNumber, trigger: 'change' }],
  190. remark: [{ required: true, message: '请输入备注', trigger: 'blur' }]
  191. })
  192. const dialog = ref(false)
  193. // 列表
  194. const list = ref([])
  195. // 请求
  196. onMounted(async () => {
  197. loading.value = true
  198. await searchOther()
  199. await search()
  200. await searchMatch()
  201. loading.value = false
  202. })
  203. const searchOther = async () => {
  204. let result
  205. // 类型
  206. result = await dictDataStore.query({ code: 'matchType', is_use: '0' })
  207. if ($checkRes(result)) typeList.value = result.data
  208. // 类别
  209. result = await dictDataStore.query({ code: 'matchForm', is_use: '0' })
  210. if ($checkRes(result)) formList.value = result.data
  211. // 赛事状态
  212. result = await dictDataStore.query({ code: 'matchStatus', is_use: '0' })
  213. if ($checkRes(result)) statusList.value = result.data
  214. // 证件类型
  215. result = await dictDataStore.query({ code: 'cardType', is_use: '0' })
  216. if ($checkRes(result)) cardTypeList.value = result.data
  217. // 赛事行业
  218. result = await dictDataStore.query({ code: 'matchIndustry', is_use: '0' })
  219. if ($checkRes(result)) industryList.value = result.data
  220. }
  221. const searchMatch = async () => {
  222. const info = {
  223. skip: 0,
  224. limit: 10,
  225. is_use: '0',
  226. status: '1'
  227. }
  228. const res = await store.query(info)
  229. if (res.errcode == '0') list.value = res.data
  230. }
  231. const search = async () => {
  232. let id = route.query.id
  233. if (id) {
  234. let res = await store.detail(id)
  235. if (res.errcode == '0') {
  236. info.value = res.data
  237. rulesInfo.value = { title: '大赛背景', key: 'rules1', content: res.data.rules.rules1 }
  238. }
  239. }
  240. }
  241. // 字典数据转换
  242. const getDict = (data, model) => {
  243. let res
  244. if (model == 'status') res = statusList.value.find((f) => f.value == data)
  245. else if (model == 'type') res = typeList.value.find((f) => f.value == data)
  246. else if (model == 'industry') res = industryList.value.find((f) => f.value == data)
  247. else if (model == 'form') res = formList.value.find((f) => f.value == data)
  248. return get(res, 'label')
  249. }
  250. // 时间
  251. const getTime = (data) => {
  252. if (data) return `${data[0]} - ${data[1]}`
  253. }
  254. const selectOpen = (key) => {
  255. const res = menuList.value.find((f) => f.label == key)
  256. if (res) {
  257. rulesInfo.value = { title: get(res, 'title'), key, content: get(info.value.rules, key) }
  258. }
  259. }
  260. // 查看
  261. const toView = (item) => {
  262. router.push({ path: '/innovation/detail', query: { id: item.id || item._id } })
  263. }
  264. // 报名参赛
  265. const toSign = () => {
  266. dialog.value = true
  267. }
  268. const toClose = () => {
  269. dialog.value = false
  270. form.value = {}
  271. }
  272. // 收藏
  273. const toCollection = async (status) => {
  274. if (user.value._id) {
  275. let res
  276. let message
  277. const data = {
  278. user: user.value._id,
  279. source: info.value._id,
  280. type: 'match',
  281. time: moment().format('YYYY-MM-DD')
  282. }
  283. if (status == '1') {
  284. message = '收藏成功'
  285. res = await collectionStore.create(data)
  286. } else {
  287. message = '取消收藏成功'
  288. res = await collectionStore.cancel(data)
  289. }
  290. if (res.errcode === 0) {
  291. ElMessage({
  292. message,
  293. type: 'success'
  294. })
  295. await search()
  296. }
  297. } else {
  298. ElMessage({
  299. message: '未登录无法进行收藏 请登录',
  300. type: 'warning'
  301. })
  302. }
  303. }
  304. // 报名
  305. const submitForm = async (formEl) => {
  306. if (!formEl) return
  307. await formEl.validate(async (valid, fields) => {
  308. if (valid) {
  309. const data = cloneDeep(form.value)
  310. const other = {
  311. match: info.value._id,
  312. user: user.value._id,
  313. time: moment().format('YYYY-MM-DD')
  314. }
  315. let res = await signStore.create({ ...data, ...other })
  316. if ($checkRes(res, true)) toClose()
  317. } else {
  318. console.log('error submit!', fields)
  319. }
  320. })
  321. }
  322. // provide
  323. provide('form', form)
  324. provide('rules', rules)
  325. provide('ruleFormRef', ruleFormRef)
  326. provide('cardTypeList', cardTypeList)
  327. provide('submitForm', submitForm)
  328. </script>
  329. <style scoped lang="scss">
  330. .main {
  331. background: rgb(248, 248, 248);
  332. .one {
  333. margin-top: 20px;
  334. background: #ffffff;
  335. border-radius: 10px;
  336. padding: 15px;
  337. .list {
  338. display: flex;
  339. align-items: center;
  340. min-height: 150px;
  341. width: 100%;
  342. margin-bottom: 10px;
  343. position: relative;
  344. overflow: hidden;
  345. border: 1px solid #f5f5f5;
  346. .left {
  347. display: flex;
  348. align-items: center;
  349. justify-content: center;
  350. .image {
  351. width: 180px;
  352. height: 120px;
  353. }
  354. }
  355. .right {
  356. .right_1 {
  357. padding: 10px 0 0 0;
  358. .type {
  359. padding-left: 4px;
  360. padding-right: 4px;
  361. height: 22px;
  362. line-height: 20px;
  363. background: #f8f9fc;
  364. border-radius: 1px;
  365. border: 1px solid #dde2e7;
  366. font-size: 12px;
  367. font-family:
  368. PingFangSC-Regular,
  369. PingFang SC;
  370. font-weight: 400;
  371. color: #a9b2c6;
  372. text-align: center;
  373. vertical-align: top;
  374. display: inline-block;
  375. margin-right: 8px;
  376. }
  377. .title {
  378. max-width: 88%;
  379. display: inline-block;
  380. overflow: hidden;
  381. text-overflow: ellipsis;
  382. white-space: nowrap;
  383. font-size: 16px;
  384. font-family:
  385. PingFangSC-Medium,
  386. PingFang SC;
  387. font-weight: 500;
  388. color: #222;
  389. line-height: 22px;
  390. }
  391. }
  392. .right_2 {
  393. padding: 5px 0;
  394. font-size: 12px;
  395. font-family:
  396. PingFangSC-Regular,
  397. PingFang SC;
  398. font-weight: 400;
  399. color: #666;
  400. }
  401. .right_3 {
  402. padding: 5px 0;
  403. display: flex;
  404. justify-content: space-between;
  405. .right_3Left {
  406. font-size: 12px;
  407. font-family:
  408. PingFangSC-Regular,
  409. PingFang SC;
  410. font-weight: 400;
  411. color: #949fb8;
  412. }
  413. .right_3Right {
  414. text-align: right;
  415. font-size: 16px;
  416. font-family:
  417. PingFangSC-Semibold,
  418. PingFang SC;
  419. font-weight: 600;
  420. color: #ff5602;
  421. }
  422. }
  423. .right_4 {
  424. padding-top: 10px;
  425. border-top: 1px solid #f3f3f3;
  426. }
  427. }
  428. .file {
  429. display: flex;
  430. align-items: center;
  431. justify-content: center;
  432. font-family: PingFangSC-Regular;
  433. font-size: 14px;
  434. color: #2374ff;
  435. span {
  436. margin: 0 0 0 5px;
  437. }
  438. }
  439. .join {
  440. position: absolute;
  441. right: -23px;
  442. top: 10px;
  443. font-size: 12px;
  444. font-family:
  445. PingFangSC-Normal,
  446. PingFang SC;
  447. color: #fff;
  448. line-height: 21px;
  449. height: 21px;
  450. width: 100px;
  451. text-align: center;
  452. -ms-transform: rotate(45deg);
  453. transform: rotate(45deg);
  454. -webkit-transform: rotate(45deg);
  455. -moz-transform: rotate(45deg);
  456. padding-left: 9px;
  457. }
  458. .join0 {
  459. background: #e6f4fe;
  460. color: #638aa5;
  461. }
  462. .join1 {
  463. background: #ededed;
  464. color: #666c7d;
  465. }
  466. }
  467. }
  468. .two {
  469. margin: 20px 0;
  470. background: #ffffff;
  471. border-radius: 10px;
  472. padding: 15px;
  473. .two_1 {
  474. border-bottom: 1px solid #e4e7ed;
  475. :deep(.el-tabs__nav-wrap:after) {
  476. background-color: transparent !important;
  477. }
  478. }
  479. .two_2 {
  480. padding: 15px;
  481. min-height: 500px;
  482. :deep(.el-aside)::-webkit-scrollbar {
  483. display: none;
  484. }
  485. h2 {
  486. padding-bottom: 0.3em;
  487. border-bottom: 1px solid #eaecef;
  488. }
  489. .rich-text-container {
  490. :deep(table) {
  491. border-collapse: collapse;
  492. }
  493. :deep(table) {
  494. border: 1px solid black;
  495. }
  496. :deep(th) {
  497. border: 1px solid black;
  498. }
  499. :deep(td) {
  500. border: 1px solid black;
  501. }
  502. }
  503. }
  504. }
  505. .thr {
  506. margin: 20px 0;
  507. background: #ffffff;
  508. border-radius: 10px;
  509. padding: 15px;
  510. .thr_1 {
  511. padding: 10px;
  512. font-family: PingFangSC-Semibold;
  513. font-size: 18px;
  514. color: #383b40;
  515. letter-spacing: 0;
  516. line-height: 18px;
  517. font-weight: 600;
  518. }
  519. .thr_2 {
  520. display: flex;
  521. flex-wrap: wrap;
  522. margin: 10px 0 0 0;
  523. .list {
  524. display: flex;
  525. align-items: center;
  526. min-height: 150px;
  527. width: 100%;
  528. margin: 10px;
  529. position: relative;
  530. overflow: hidden;
  531. border: 1px solid #f5f5f5;
  532. .left {
  533. display: flex;
  534. align-items: center;
  535. justify-content: center;
  536. .image {
  537. width: 120px;
  538. height: 120px;
  539. }
  540. }
  541. .right {
  542. .right_1 {
  543. padding: 10px 0 0 0;
  544. .type {
  545. padding-left: 4px;
  546. padding-right: 4px;
  547. height: 22px;
  548. line-height: 20px;
  549. background: #f8f9fc;
  550. border-radius: 1px;
  551. border: 1px solid #dde2e7;
  552. font-size: 12px;
  553. font-family:
  554. PingFangSC-Regular,
  555. PingFang SC;
  556. font-weight: 400;
  557. color: #a9b2c6;
  558. text-align: center;
  559. vertical-align: top;
  560. display: inline-block;
  561. margin-right: 8px;
  562. }
  563. .title {
  564. max-width: 88%;
  565. display: inline-block;
  566. overflow: hidden;
  567. text-overflow: ellipsis;
  568. white-space: nowrap;
  569. font-size: 16px;
  570. font-family:
  571. PingFangSC-Medium,
  572. PingFang SC;
  573. font-weight: 500;
  574. color: #222;
  575. line-height: 22px;
  576. }
  577. }
  578. .right_2 {
  579. padding: 5px 0;
  580. font-size: 12px;
  581. font-family:
  582. PingFangSC-Regular,
  583. PingFang SC;
  584. font-weight: 400;
  585. color: #666;
  586. }
  587. .right_3 {
  588. padding: 5px 5px 5px 0;
  589. display: flex;
  590. justify-content: space-between;
  591. .right_3Left {
  592. font-size: 12px;
  593. font-family:
  594. PingFangSC-Regular,
  595. PingFang SC;
  596. font-weight: 400;
  597. color: #949fb8;
  598. }
  599. .right_3Right {
  600. text-align: right;
  601. font-size: 16px;
  602. font-family:
  603. PingFangSC-Semibold,
  604. PingFang SC;
  605. font-weight: 600;
  606. color: #ff5602;
  607. }
  608. }
  609. .right_4 {
  610. padding-top: 10px;
  611. border-top: 1px solid #f3f3f3;
  612. }
  613. }
  614. .join {
  615. position: absolute;
  616. right: -23px;
  617. top: 10px;
  618. font-size: 12px;
  619. font-family:
  620. PingFangSC-Normal,
  621. PingFang SC;
  622. color: #fff;
  623. line-height: 21px;
  624. height: 21px;
  625. width: 100px;
  626. text-align: center;
  627. -ms-transform: rotate(45deg);
  628. transform: rotate(45deg);
  629. -webkit-transform: rotate(45deg);
  630. -moz-transform: rotate(45deg);
  631. padding-left: 9px;
  632. }
  633. .join0 {
  634. background: #e6f4fe;
  635. color: #638aa5;
  636. }
  637. .join1 {
  638. background: #ededed;
  639. color: #666c7d;
  640. }
  641. }
  642. }
  643. }
  644. }
  645. </style>