| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443 |
- <template>
- <!-- 培训详情:上懒加载图;中白底信息;下白底仅展示一个 up-button(随状态切换) -->
- <view :class="pageRootClass" class="tab-page td-page">
- <scroll-view scroll-y class="td-scroll" enable-back-to-top>
- <view class="td-inner">
- <view class="td-top">
- <up-lazy-load
- class="td-cover"
- :image="coverSrc"
- height="360"
- :border-radius="16"
- img-mode="aspectFill"
- :threshold="120"
- :index="courseId || 'td'"
- @click="onPreviewCover"
- />
- </view>
- <view class="td-panel td-panel--info">
- <text class="td-title">{{ displayTitle }}</text>
- <text class="text-body td-line">{{ $t('trainingDetailPage.labelRegisterTime') }}:{{ registerRange }}</text>
- <text class="text-body td-line">{{ $t('trainingDetailPage.labelEnrolled') }}:{{ enrolledDisplay }}</text>
- <text class="text-body td-line">{{ $t('trainingDetailPage.labelPlace') }}:{{ placeText }}</text>
- <text class="text-body td-line td-line--block">{{ $t('trainingDetailPage.labelIntro') }}:{{ introText }}</text>
- <text class="text-body td-line td-line--block">{{ $t('trainingDetailPage.labelOutcome') }}:{{ outcomeText }}</text>
- </view>
- <view class="td-panel td-panel--actions">
- <up-button
- class="td-btn"
- :type="actionBtn.buttonType"
- :plain="actionBtn.plain"
- :hairline="actionBtn.hairline"
- :text="actionBtn.text"
- :disabled="actionBtn.disabled"
- @click="onActionBtn"
- />
- </view>
- </view>
- </scroll-view>
- </view>
- </template>
- <script>
- import UButton from 'uview-plus/components/u-button/u-button.vue'
- import ULazyLoad from 'uview-plus/components/u-lazy-load/u-lazy-load.vue'
- import tabPage from '@/mixins/tabPage'
- import { resolveResourceUrl } from '@/utils/resourceUrl'
- import {
- getMyEnrollDetail,
- enrollPracticalTraining,
- cancelEnrollPracticalTraining
- } from '@/api/practicalTraining'
- const COVER = '/static/ai/hero.png'
- /** 006001–006004 → 按钮状态索引(未开始/报名中/已报满/已截止) */
- const TRAINING_STATUS_INDEX = {
- '006001': 0,
- '006002': 1,
- '006003': 2,
- '006004': 3
- }
- function formatDateTime(val) {
- if (val == null || val === '') return ''
- const s = String(val)
- return s.length >= 19 ? s.slice(0, 19) : s.slice(0, 10)
- }
- export default {
- components: {
- 'up-button': UButton,
- 'up-lazy-load': ULazyLoad
- },
- mixins: [tabPage],
- data() {
- return {
- navTitleKey: 'trainingDetailPage.navTitle',
- courseId: '',
- displayTitleRaw: '',
- typeCode: '',
- introduction: '',
- expectedOutcome: '',
- trainingTime: '',
- regStart: '',
- regEnd: '',
- enrolled: 0,
- total: 0,
- coverSrc: COVER,
- /** 当前用户是否已报名 */
- hasSigned: false,
- /** 接口返回:是否允许取消报名 */
- canCancelEnroll: false,
- detailLoading: false,
- actionSubmitting: false
- }
- },
- computed: {
- displayTitle() {
- return this.displayTitleRaw || this.$t('trainingDetailPage.titleFallback')
- },
- /** 0 未开始 1 报名中 2 已报满 3 已截止 */
- statusIndex() {
- if (TRAINING_STATUS_INDEX[this.typeCode] != null) {
- return TRAINING_STATUS_INDEX[this.typeCode]
- }
- return 1
- },
- registerRange() {
- if (this.regStart && this.regEnd) {
- return `${this.regStart} - ${this.regEnd}`
- }
- if (this.regStart) {
- return this.regStart
- }
- if (this.regEnd) {
- return this.regEnd
- }
- return this.$t('trainingDetailPage.noRegisterTime')
- },
- enrolledDisplay() {
- const cap = this.total > 0 ? this.total : 0
- const e = this.enrolled
- if (cap > 0) {
- return `${e}/${cap}`
- }
- return String(e)
- },
- placeText() {
- return this.trainingTime || this.$t('trainingDetailPage.noPlace')
- },
- introText() {
- return this.introduction || this.$t('trainingDetailPage.noIntro')
- },
- outcomeText() {
- return this.expectedOutcome || this.$t('trainingDetailPage.noOutcome')
- },
- canJoin() {
- return (
- this.statusIndex === 1 &&
- (this.total <= 0 || this.enrolled < this.total) &&
- !this.hasSigned &&
- !this.detailLoading &&
- !this.actionSubmitting
- )
- },
- canCancel() {
- return this.hasSigned && this.canCancelEnroll && !this.actionSubmitting
- },
- actionBtn() {
- const txt = (key) => this.$t(`trainingDetailPage.${key}`)
- const gray = (kind, key) => ({
- kind,
- text: txt(key),
- disabled: true,
- plain: true,
- hairline: true,
- buttonType: 'info'
- })
- if (this.detailLoading || this.actionSubmitting) {
- return gray('loading', 'btnLoading')
- }
- if (this.hasSigned) {
- if (this.canCancelEnroll) {
- return {
- kind: 'cancel',
- text: txt('btnCancel'),
- disabled: false,
- plain: true,
- hairline: false,
- buttonType: 'default'
- }
- }
- return gray('closed', 'btnClosed')
- }
- if (this.statusIndex === 0) {
- return gray('not_start', 'btnNotStart')
- }
- if (this.statusIndex === 3) {
- return gray('closed', 'btnClosed')
- }
- const isFull =
- this.statusIndex === 2 ||
- (this.statusIndex === 1 && this.total > 0 && this.enrolled >= this.total)
- if (isFull) {
- return gray('full', 'btnFull')
- }
- if (this.statusIndex === 1 && this.canJoin) {
- return {
- kind: 'join',
- text: txt('btnJoin'),
- disabled: false,
- plain: false,
- hairline: false,
- buttonType: 'primary'
- }
- }
- return gray('full', 'btnFull')
- }
- },
- onLoad(query) {
- const q = query || {}
- this.courseId = this.decodeQuery(q, 'id')
- this.displayTitleRaw = this.decodeQuery(q, 'title')
- this.typeCode = this.decodeQuery(q, 'type')
- this.introduction = this.decodeQuery(q, 'introduction')
- this.expectedOutcome = this.decodeQuery(q, 'expectedOutcome')
- const en = parseInt(this.decodeQuery(q, 'enrolled'), 10)
- const tot = parseInt(this.decodeQuery(q, 'total'), 10)
- this.enrolled = Number.isFinite(en) && en >= 0 ? en : 0
- this.total = Number.isFinite(tot) && tot >= 0 ? tot : 0
- const cover = this.decodeQuery(q, 'cover')
- this.coverSrc = cover ? resolveResourceUrl(cover) : COVER
- this.applyQueryDates(q)
- },
- onShow() {
- const p = uni.setNavigationBarTitle({
- title: this.displayTitleRaw || this.$t(this.navTitleKey)
- })
- if (p && typeof p.catch === 'function') {
- p.catch(() => {})
- }
- this.loadEnrollDetail()
- },
- methods: {
- applyQueryDates(q) {
- this.regStart = formatDateTime(this.decodeQuery(q, 'regStart'))
- this.regEnd = formatDateTime(this.decodeQuery(q, 'regEnd'))
- this.trainingTime = formatDateTime(this.decodeQuery(q, 'trainingTime'))
- },
- loadEnrollDetail() {
- if (!this.courseId) return Promise.resolve()
- this.detailLoading = true
- return getMyEnrollDetail(this.courseId)
- .then((res) => {
- const d = res.data
- if (d) {
- this.applyDetailVo(d)
- this.hasSigned = true
- this.canCancelEnroll = d.canCancel === true
- }
- })
- .catch(() => {
- this.hasSigned = false
- this.canCancelEnroll = false
- })
- .finally(() => {
- this.detailLoading = false
- })
- },
- applyDetailVo(d) {
- if (d.trainingTopic) {
- this.displayTitleRaw = d.trainingTopic
- }
- if (d.trainingStatus) {
- this.typeCode = d.trainingStatus
- }
- this.introduction = d.trainingIntro || this.introduction || ''
- this.expectedOutcome = d.expectedOutcome || this.expectedOutcome || ''
- const timeText = formatDateTime(d.trainingTime)
- const locationText = d.trainingLocation || ''
- this.trainingTime = timeText || locationText || this.trainingTime
- this.regStart = formatDateTime(d.registrationStartTime) || this.regStart
- this.regEnd = formatDateTime(d.registrationEndTime) || this.regEnd
- if (d.actualEnrolledCount != null) {
- this.enrolled = Number(d.actualEnrolledCount)
- }
- if (d.plannedHeadCount != null) {
- this.total = Number(d.plannedHeadCount)
- }
- if (d.coverFileUrl) {
- this.coverSrc = resolveResourceUrl(d.coverFileUrl)
- }
- uni.setNavigationBarTitle({
- title: this.displayTitleRaw || this.$t(this.navTitleKey)
- })
- },
- decodeQuery(q, key) {
- const raw = q && q[key]
- if (raw == null || raw === '') {
- return ''
- }
- try {
- return decodeURIComponent(String(raw))
- } catch (e) {
- return String(raw)
- }
- },
- onPreviewCover() {
- if (!this.coverSrc) return
- uni.previewImage({ urls: [this.coverSrc], current: 0 })
- },
- onActionBtn() {
- if (this.actionBtn.disabled) return
- const k = this.actionBtn.kind
- if (k === 'join') this.onJoin()
- else if (k === 'cancel') this.onCancelJoin()
- },
- onJoin() {
- if (!this.canJoin || !this.courseId) return
- this.actionSubmitting = true
- enrollPracticalTraining(this.courseId)
- .then(() => {
- uni.showToast({ title: this.$t('trainingDetailPage.toastJoined'), icon: 'none' })
- return this.loadEnrollDetail()
- })
- .finally(() => {
- this.actionSubmitting = false
- })
- },
- onCancelJoin() {
- if (!this.canCancel || !this.courseId) return
- this.actionSubmitting = true
- cancelEnrollPracticalTraining(this.courseId)
- .then(() => {
- uni.showToast({ title: this.$t('trainingDetailPage.toastCanceled'), icon: 'none' })
- this.hasSigned = false
- this.canCancelEnroll = false
- if (this.enrolled > 0) {
- this.enrolled -= 1
- }
- if (this.typeCode === '006003' && this.total > 0 && this.enrolled < this.total) {
- this.typeCode = '006002'
- }
- })
- .finally(() => {
- this.actionSubmitting = false
- })
- }
- }
- }
- </script>
- <style lang="scss" scoped>
- @import '@/styles/morandi.scss';
- @import '@/styles/tab-page.scss';
- .td-page {
- display: flex;
- flex-direction: column;
- min-width: 0;
- min-height: 100%;
- box-sizing: border-box;
- background: $morandi-bg-page;
- }
- .td-scroll {
- flex: 1;
- min-height: 0;
- min-width: 0;
- }
- .td-inner {
- display: flex;
- flex-direction: column;
- gap: 24rpx;
- min-width: 0;
- padding: 24rpx 24rpx 48rpx;
- box-sizing: border-box;
- }
- .td-top {
- width: 100%;
- min-width: 0;
- border-radius: 16rpx;
- overflow: hidden;
- border: 1rpx solid $morandi-border;
- background: $morandi-bg-card-inner;
- }
- .td-cover {
- width: 100%;
- display: block;
- }
- .td-panel {
- min-width: 0;
- padding: 28rpx 24rpx;
- box-sizing: border-box;
- border-radius: 16rpx;
- background: #ffffff;
- border: 1rpx solid $morandi-border-soft;
- }
- .td-panel--info {
- display: flex;
- flex-direction: column;
- gap: 16rpx;
- }
- .td-title {
- font-size: 34rpx;
- font-weight: 700;
- line-height: 1.45;
- color: #111827;
- word-break: break-word;
- overflow-wrap: anywhere;
- }
- .td-line {
- font-size: 26rpx;
- line-height: 1.5;
- color: $morandi-text-secondary;
- word-break: break-word;
- overflow-wrap: anywhere;
- }
- .td-line--block {
- line-height: 1.55;
- }
- .td-panel--actions {
- display: flex;
- flex-direction: column;
- min-width: 0;
- }
- .td-btn {
- width: 100%;
- }
- .td-page.lang-bo {
- .td-title {
- font-size: 30rpx;
- line-height: 1.75;
- letter-spacing: 2rpx;
- font-family: 'Noto Sans Tibetan', 'PingFang SC', 'Microsoft YaHei', sans-serif;
- }
- .td-line {
- font-size: 24rpx;
- line-height: 1.75;
- letter-spacing: 2rpx;
- font-family: 'Noto Sans Tibetan', 'PingFang SC', 'Microsoft YaHei', sans-serif;
- }
- }
- </style>
|