first commit

This commit is contained in:
2026-09-09 17:47:12 +08:00
commit 0cec79cd76
5672 changed files with 1015928 additions and 0 deletions
@@ -0,0 +1,645 @@
<!--#
layout("/layouts/platform_h5.html"){
#-->
<style>
body {
background-color: #f3f7fd;
}
.culture-activity-page {
min-height: 100vh;
padding-bottom: calc(24px + env(safe-area-inset-bottom));
box-sizing: border-box;
background-color: #f3f7fd;
}
.culture-activity-page .van-nav-bar {
background-color: #fff;
}
.culture-activity-page .van-nav-bar__title {
color: #1f2937;
font-size: 16px;
}
.culture-activity-page .van-nav-bar .van-icon,
.culture-activity-page .van-nav-bar__text {
color: #397fbd;
}
.culture-activity-content {
padding: 12px 12px 0;
}
.culture-activity-hero {
position: relative;
height: 134px;
overflow: hidden;
border-radius: 10px;
color: #fff;
background-color: #247fe8;
box-shadow: 0 8px 20px rgba(51, 117, 219, .18);
}
.culture-activity-hero__image {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
object-fit: cover;
object-position: center;
}
.culture-activity-hero__copy {
position: relative;
z-index: 1;
display: flex;
width: 64%;
height: 100%;
padding-left: 16px;
flex-direction: column;
justify-content: center;
box-sizing: border-box;
background: linear-gradient(90deg, rgba(22, 112, 232, .98) 0%, rgba(22, 112, 232, .88) 52%, rgba(22, 112, 232, 0) 100%);
}
.culture-activity-hero__title {
font-size: 25px;
line-height: 34px;
font-weight: 700;
letter-spacing: 1px;
text-shadow: 0 2px 6px rgba(17, 86, 182, .18);
}
.culture-activity-hero__subtitle {
margin-top: 4px;
font-size: 13px;
line-height: 20px;
letter-spacing: .5px;
opacity: .95;
}
.culture-activity-sticky {
padding: 10px 0 1px;
background-color: #f3f7fd;
}
.van-sticky--fixed .culture-activity-sticky {
padding-right: 12px;
padding-left: 12px;
box-sizing: border-box;
}
.culture-activity-search {
margin: 0 0 8px;
padding: 0;
overflow: hidden;
border-radius: 12px;
background-color: #fff;
box-shadow: 0 5px 16px rgba(43, 73, 112, .1);
}
.culture-activity-search .van-search__content {
height: 42px;
padding-left: 12px;
align-items: center;
border-radius: 12px;
background-color: #fff;
}
.culture-activity-search .van-field__control {
color: #263548;
font-size: 14px;
}
.culture-activity-search .van-field__control::placeholder {
color: #a7b0bf;
}
.culture-activity-filter {
margin: 0 0 10px;
overflow: hidden;
border-radius: 10px;
box-shadow: 0 5px 16px rgba(43, 73, 112, .08);
transition: border-radius .18s ease;
}
.culture-activity-filter.is-open {
border-radius: 10px 10px 0 0;
}
.culture-activity-filter .van-dropdown-menu__bar {
height: 44px;
box-shadow: none;
}
.culture-activity-filter .van-dropdown-menu__item {
min-width: 0;
flex: 1 1 0;
overflow: hidden;
}
.culture-activity-filter .van-dropdown-menu__item:last-child {
flex-grow: 1.35;
}
.culture-activity-filter .van-dropdown-menu__title {
width: 100%;
max-width: 100%;
padding: 0 15px 0 4px;
overflow: hidden;
color: #66758a;
font-size: 12px;
text-align: center;
text-overflow: ellipsis;
white-space: nowrap;
box-sizing: border-box;
}
.culture-activity-filter .van-dropdown-menu__title::after {
right: 5px;
}
.culture-activity-filter .van-dropdown-menu__title--active,
.culture-activity-filter .van-dropdown-item__option--active,
.culture-activity-filter .van-dropdown-item__option--active .van-dropdown-item__icon {
color: #1989fa;
}
.culture-activity-filter .van-dropdown-menu__title--down::after {
border-color: transparent transparent #1989fa #1989fa;
}
.culture-activity-filter .van-dropdown-item__content {
width: calc(100% - 24px);
margin: 0 12px;
overflow: hidden;
box-sizing: border-box;
border-radius: 0 0 12px 12px;
box-shadow: 0 10px 24px rgba(31, 65, 108, .14);
}
.culture-activity-filter .van-dropdown-item__option {
min-height: 48px;
padding: 0 16px;
color: #46566c;
font-size: 14px;
}
.culture-activity-filter .van-cell::after {
right: 16px;
left: 16px;
}
.culture-activity-list {
display: flex;
flex-direction: column;
gap: 10px;
}
.culture-activity-card {
position: relative;
overflow: hidden;
border-radius: 10px;
background-color: #fff;
box-shadow: 0 7px 20px rgba(55, 85, 126, .08);
}
.culture-activity-card__cover,
.culture-activity-card__cover-state {
display: flex;
width: 100%;
height: 150px;
align-items: center;
justify-content: center;
box-sizing: border-box;
background-color: #edf4fc;
}
.culture-activity-card__cover {
display: block;
object-fit: cover;
}
.culture-activity-card__cover-state {
flex-direction: column;
row-gap: 8px;
color: #8da1ba;
font-size: 13px;
}
.culture-activity-card__cover-state .van-icon {
font-size: 34px;
}
.culture-activity-card__cover-state.is-error {
color: #c27b75;
background-color: #fff3f1;
}
.culture-activity-card__body {
padding: 12px 38px 14px 14px;
}
.culture-activity-card__title {
min-width: 0;
overflow: hidden;
color: #303846;
font-size: 14px;
line-height: 21px;
text-overflow: ellipsis;
white-space: nowrap;
}
.culture-activity-card__time {
margin-top: 4px;
color: #98a2b2;
font-size: 12px;
line-height: 17px;
white-space: nowrap;
}
.culture-activity-card__meta {
margin-top: 10px;
padding-top: 9px;
border-top: 1px solid #edf1f6;
}
.culture-activity-card__address {
display: flex;
align-items: flex-start;
color: #69788d;
font-size: 12px;
line-height: 18px;
}
.culture-activity-card__address .van-icon {
margin: 2px 5px 0 0;
flex: none;
color: #6c9ff8;
font-size: 14px;
}
.culture-activity-card__address span {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.culture-activity-card__tags {
display: flex;
margin-top: 8px;
flex-wrap: wrap;
gap: 6px;
}
.culture-activity-card__tag {
display: inline-flex;
padding: 3px 8px;
align-items: center;
border-radius: 4px;
color: #397de0;
background-color: #edf5ff;
font-size: 11px;
line-height: 16px;
}
.culture-activity-card__tag-label {
margin-right: 4px;
color: #8aa7cf;
}
.culture-activity-card__arrow {
position: absolute;
top: 164px;
right: 14px;
color: #b4bdca;
font-size: 16px;
}
.culture-activity-empty {
display: flex;
min-height: 260px;
padding: 26px 18px 32px;
align-items: center;
justify-content: center;
flex-direction: column;
box-sizing: border-box;
text-align: center;
}
.culture-activity-empty img {
display: block;
width: 82%;
max-width: 280px;
height: auto;
border-radius: 12px;
object-fit: contain;
}
.culture-activity-empty__title {
margin-top: 14px;
color: #50627a;
font-size: 15px;
line-height: 22px;
font-weight: 600;
}
.culture-activity-empty__hint {
margin-top: 5px;
color: #9aa7b8;
font-size: 12px;
line-height: 18px;
}
.culture-activity-page .van-list__finished-text,
.culture-activity-page .van-list__loading {
color: #9aa7b8;
font-size: 13px;
line-height: 44px;
}
</style>
<div id="app" v-cloak>
<div class="culture-activity-page">
<van-nav-bar title="文化活动" @click-left="historyBack" left-arrow left-text="返回" placeholder fixed></van-nav-bar>
<div class="culture-activity-content">
<section class="culture-activity-hero">
<img class="culture-activity-hero__image" src="/assets/mobile/img/activity/hero/culture-hero-v3.png" alt="文化活动插画">
<div class="culture-activity-hero__copy">
<div class="culture-activity-hero__title">文化活动</div>
<div class="culture-activity-hero__subtitle">丰富生活 · 乐享校园</div>
</div>
</section>
<van-sticky :offset-top="46">
<div class="culture-activity-sticky">
<van-search
v-model="searchKeyword"
class="culture-activity-search"
placeholder="搜索文化活动"
shape="round"
clearable
@search="doSearch"
@clear="doSearch"
></van-search>
<van-dropdown-menu class="culture-activity-filter" :class="{'is-open': filterOpened}" active-color="#1989fa">
<year-van-dropdown-item :num="5" @change="doSearch" @open="filterOpened = true" @close="filterOpened = false" v-model="pageForm.year"></year-van-dropdown-item>
<van-dropdown-item :options="stateList" @change="doSearch" @open="filterOpened = true" @close="filterOpened = false" v-model="pageForm.state"></van-dropdown-item>
<van-dropdown-item :options="isEnrolledOptions" @change="doSearch" @open="filterOpened = true" @close="filterOpened = false" v-model="pageForm.isEnrolled"></van-dropdown-item>
<van-dropdown-item :options="activityTypeOptions" @change="doSearch" @open="filterOpened = true" @close="filterOpened = false" v-model="pageForm.activity_type"></van-dropdown-item>
</van-dropdown-menu>
</div>
</van-sticky>
<van-pull-refresh v-model="refreshing" @refresh="onRefresh">
<van-list v-model="loading" :finished="finished" @load="onLoad">
<div class="culture-activity-list">
<div v-for="row in tableData" :key="row.id" class="culture-activity-card" @click="openDetail(row)">
<img
v-if="hasCover(row) && !imageLoadErrors[row.id]"
class="culture-activity-card__cover"
:src="row.cover"
:alt="row.name"
@error="handleImageError(row)"
>
<div v-else class="culture-activity-card__cover-state" :class="{'is-error': hasCover(row)}">
<van-icon name="photo-o"></van-icon>
<span>{{ hasCover(row) ? '图片加载失败' : '暂无图片' }}</span>
</div>
<div class="culture-activity-card__body">
<div class="culture-activity-card__title">{{ row.name }}</div>
<div class="culture-activity-card__time" v-if="row.applyStartTime && row.applyEndTime">
报名时间:{{ $moment(row.applyStartTime).format('MM/DD HH:mm') }}~{{ $moment(row.applyEndTime).format('MM/DD HH:mm') }}
</div>
<div class="culture-activity-card__time" v-if="row.startTime && row.endTime">
活动时间:{{ $moment(row.startTime).format('MM/DD HH:mm') }}~{{ $moment(row.endTime).format('MM/DD HH:mm') }}
</div>
<div class="culture-activity-card__meta" v-if="row.address || row.projectTypeName || formatSignUpMethod(row.signUpMethod)">
<div class="culture-activity-card__address" v-if="row.address">
<van-icon name="location-o"></van-icon>
<span>{{ row.address }}</span>
</div>
<div class="culture-activity-card__tags" v-if="row.projectTypeName || formatSignUpMethod(row.signUpMethod)">
<span class="culture-activity-card__tag" v-if="row.projectTypeName">
<span class="culture-activity-card__tag-label">活动类型</span>{{ row.projectTypeName }}
</span>
<span class="culture-activity-card__tag" v-if="formatSignUpMethod(row.signUpMethod)">
<span class="culture-activity-card__tag-label">报名方式</span>{{ formatSignUpMethod(row.signUpMethod) }}
</span>
</div>
</div>
</div>
<van-icon class="culture-activity-card__arrow" name="arrow"></van-icon>
</div>
</div>
</van-list>
<div v-if="!loading && tableData.length === 0" class="culture-activity-empty">
<img src="/assets/mobile/img/activity/culture-empty.png" alt="文化活动空状态插画">
<div class="culture-activity-empty__title">暂无文化活动</div>
<div class="culture-activity-empty__hint">当前筛选条件下暂时没有可展示的活动</div>
</div>
</van-pull-refresh>
</div>
</div>
<van-popup :style="{ height: '100%',width: '100%'}" position="right" v-model="viewShow">
<mobile-culture-apply-user :id="viewData.id" @back="back" @submit_back="submit_back" ref="viewInfo"></mobile-culture-apply-user>
</van-popup>
</div>
<script nonce="${cspNonce!}">
<!--#include("mobileCultureApplyUser.js"){}#-->
new Vue({
el: "#app",
data() {
return {
tableData: [],
finished: false,
loading: false,
refreshing: false,
searchKeyword: "",
imageLoadErrors: {},
filterOpened: false,
pageForm: {
state: 2,
isEnrolled: 0,
year: new Date().getFullYear(),
name: "",
pageNumber: 1,
pageSize: 5,
totalCount: 0,
activity_type: 40001
},
yearList: [],
isEnrolledOptions: [
{ value: 0, text: "未报名" },
{ value: 1, text: "已报名" }
],
stateList: [
{ value: 1, text: "全部" },
{ value: 2, text: "报名中" },
{ value: 3, text: "报名已结束" }
],
subLoading: false,
viewShow: false,
viewData: {},
activityTypeOptions: [
{ value: 40001, text: "校文化活动" },
{ value: 40002, text: "分工会文化活动" },
{ value: 40003, text: "社团文化活动" },
]
}
},
components: {
"mobile-culture-apply-user": MOBILE_CULTURE_APPLY_USER
},
methods: {
onReady() {
this.doSearch()
},
hasCover(row) {
return !!(row && row.cover && String(row.cover).trim())
},
handleImageError(row) {
this.$set(this.imageLoadErrors, row.id, true)
},
formatSignUpMethod(value) {
return {
1: "个人报名",
2: "组队报名",
3: "分工会报名"
}[Number(value)] || ""
},
onLoad() {
if (!this.finished) this.loadActivities(false)
},
onRefresh() {
this.loadActivities(true)
},
loadActivities(reset) {
if (reset) {
this.pageForm.pageNumber = 1
this.pageForm.totalCount = 0
this.tableData = []
this.finished = false
this.imageLoadErrors = {}
}
this.loading = true
this.$axios.post("/platform/activity/culture/applyUser/pageData", this.pageForm).then((res) => {
if (res.code === 0) {
const list = res.data && res.data.list ? res.data.list : []
this.tableData = this.pageForm.pageNumber === 1 ? list : this.tableData.concat(list)
this.pageForm.totalCount = res.data && res.data.totalCount ? res.data.totalCount : 0
this.finished = this.tableData.length >= this.pageForm.totalCount
if (!this.finished) this.pageForm.pageNumber++
}
}).finally(() => {
this.loading = false
this.refreshing = false
})
},
openDetail(row) {
this.$axios.post('/platform/activity/basic/scope/getScopeUser',{activityGroupId:row.groupId}).then(res=>{
if(res.code===0){
if(res.data>0){
this.$pjaxReplace("/platform/h5/activity/culture/signUp?id=" + row.id)
}else{
this.$toast('您没有权限参加此活动')
}
}
})
},
submit_back(id, countUser) {
this.tableData.map((t) => {
if (t.id === id) {
t.countUser = countUser
}
})
this.viewShow = false
this.$toast.success("报名成功")
},
back() {
this.viewShow = false
},
openAudit(row) {
this.viewData = row
this.viewShow = true
setTimeout(() => {
this.$refs.viewInfo.getActivityInfo()
}, 500)
},
async doAdd(row) {
this.$dialog
.confirm({
title: "提示",
message: "确认要报名吗?"
})
.then(() => {
this.$axios.post("/platform/activity/culture/applyUser/singleSignUp", { activityId: row.id }).then((res) => {
if (res.code === 0) {
this.$toast.loading({
duration: 0,
forbidClick: true,
message: "加载中...."
})
setTimeout(() => {
this.$toast.clear()
this.$toast.success(res.msg)
this.doSearch()
this.subLoading = false
}, 1000)
} else {
this.subLoading = false
}
})
})
.catch(() => {})
},
async doDelete(row) {
this.$dialog
.confirm({
title: "提示",
message: "确认要取消报名吗?"
})
.then(() => {
this.$axios.post("/platform/activity/culture/applyUser/cancelSignUp", { activityId: row.id }).then((res) => {
if (res.code === 0) {
this.$toast.loading({
duration: 0,
forbidClick: true,
message: "加载中...."
})
setTimeout(() => {
this.$toast.clear()
this.$toast.success(res.msg)
this.doSearch()
this.subLoading = false
}, 1000)
} else {
this.$toast.fail(res.msg)
}
})
})
.catch(() => {})
},
doSearch() {
this.pageForm.name = this.searchKeyword.trim()
this.loadActivities(true)
}
},
created() {
}
})
</script>
<!--#
}
#-->
@@ -0,0 +1,334 @@
var MOBILE_CULTURE_APPLY_USER = {
template: `
<div>
<van-nav-bar :title="title" @click-left="back" fixed left-arrow left-text="返回"
placeholder></van-nav-bar>
<div class="activity_img_back">
<van-image :src="viewData.cover" height="200"
width="100%"></van-image>
</div>
<van-cell-group class="mt10" inset>
<van-cell :value="viewData.name" title="活动名称"></van-cell>
<van-cell :value="viewData.address" title="活动地址"></van-cell>
<van-cell v-if="viewData.userNumberLimit!=null">
<template #title>
<span v-if="viewData.userNumberLimit===1">
活动限制人数
</span>
<span v-else-if="viewData.userNumberLimit===2">
本工会限制名额
</span>
</template>
<template>
<span v-if="viewData.userNumberLimit===1">
<span style="color: orange">{{viewData.totalUserNumberLimit }}</span>人
</span>
<span v-else="viewData.userNumberLimit===2">
<span style="color: orange">{{viewData.unionLimitNum }}</span>人
</span>
</template>
</van-cell>
<van-cell title="当前活动报名" v-if="viewData.signUpMethod!=null">
<template>
<span style="color: red">{{viewData.tissuePersonList.length}}</span>人
</template>
</van-cell>
<van-cell title="报名方式" v-if="viewData.signUpMethod">
<template>
<span v-if="viewData.signUpMethod===1">个人报名</span>
<span v-if="viewData.signUpMethod===2">分工会报名</span>
<span v-if="viewData.signUpMethod===3">组队报名</span>
</template>
</van-cell>
<van-cell title="组队人数" v-if="viewData.signUpMethod==3">
<span style="color: red"> {{viewData.teamNum}}</span>人
</van-cell>
<van-cell title="活动内容" v-if="viewData.projectTypeCode!=='50004'">
<template #label>
<span style="white-space: pre-line">
{{viewData.activityContent}}
</span>
</template>
</van-cell>
</van-cell-group>
<van-cell-group class="mt5" inset>
<van-cell class="regCell" title="报名人员">
<template #label>
<div>
<span v-if="viewData.userNumberLimit===1">
本活动限制总报名人数:({{viewData.totalUserNumberLimit}})人
</span>
<span v-else-if="viewData.userNumberLimit===2">
当前分工会报名限额 <span style="color: orange">({{viewData.limitUnion.limitNum}})人</span>
</span>
</div>
<div class="mt5" v-loading="tagCloseable">
<div v-if="registerList && registerList.length>0">
<template v-for="item in registerList">
<van-tag closeable type="success"
:plain="false"
@close="removeRegUser(item)" style="margin: 5px;font-size: 13px;">
<span style="margin: 5px;">{{item.userName}}</span>
</van-tag>
</template>
</div>
<div v-else>
<div style="text-align: center;margin-top: 20px;">
无报名人员
</div>
</div>
</div>
<div class="mt5" style="text-align: right"
v-if="$moment(viewData.applyStartTime).valueOf() < $moment().valueOf() &&
$moment().valueOf() < $moment(viewData.applyEndTime).valueOf()">
<van-button @click="openUserActionSheet" size="mini"
type="primary" style="font-size: 15px;height:25px;margin: 5px">
<span>选择报名人员</span>
</van-button>
</div>
</template>
</van-cell>
</van-cell-group>
<van-action-sheet class="userActionPopup" title="选择报名人员" v-model="userActionSheet">
<van-search @search="onSearchUser" placeholder="请输入姓名搜索" show-action
v-model="searchKey">
<template #action>
<div @click="onSearchUser(searchKey)">搜索</div>
</template>
</van-search>
<template v-if="searchLoading">
<van-loading size="24px" vertical>搜索中...</van-loading>
</template>
<div>
<div class="van-action-sheet__content mt5"
v-if="searchUserList && searchUserList.length>0">
<template>
<template v-for="item in searchUserList"
v-if="!registerList.map(v=>v.id).includes(item.id)">
<button @click="searchUserAdd(item)"
class="van-action-sheet__item van-hairline--bottom">
<span class="van-action-sheet__name">{{item.userName}}({{item.loginName}})</span>
</button>
</template>
</template>
</div>
<van-empty v-else image="/assets/platform/plugins/vant-green/images/nodata/nodata.png" description="暂无数据"></van-empty>
</div>
</van-action-sheet>
<div style="margin: 15px">
<van-button @click="doSave()" block style="border-radius: 10px;"
type="primary">
提 交
</van-button>
</div>
</div>
`,
store,
props: {
id: {
value: { type: Object, default: "" }
}
},
data() {
return {
title: "活动报名",
viewData: {},
registerList: [],
addRegisterList: [],
userActionSheet: false,
searchKey: "",
searchUserList: [],
tagCloseable: true,
searchLoading: false
}
},
methods: {
async doSave() {
if (this.registerList.length === 0) {
this.$toast("请添加报名人员后再提交!")
return
}
//如果是组队报名
if (this.viewData.signUpMethod === 3) {
//判断报名了几个人
if (this.registerList.length !== this.viewData.teamNum) {
this.$dialog
.alert({
title: "温馨提示",
message: "此活动是双人报名模式您需要报名" + this.viewData.teamNum + "人!"
})
.then(() => {})
return
}
if (!this.registerList.map((v) => v.id).includes(this.$store.state.user.id)) {
this.$dialog
.alert({
title: "温馨提示",
message: "此活动是组队模式需要" + this.viewData.teamNum + "个人一起报名!"
})
.then(() => {})
return
}
let arr = []
//判断提交的人员当中有没有人报名
this.$axios.post("/platform/h5/activity/culture/selectRegisterList", { activityId: this.id }).then((resp) => {
this.addRegisterList = resp.data
if (this.addRegisterList && this.addRegisterList.length > 0) {
this.addRegisterList.forEach((v) => {
this.registerList.forEach((r) => {
if (v.id === r.id && v.applyUserId !== this.$store.state.user.id) {
arr.push(r.userName)
}
})
})
}
})
if (arr && arr.length > 0) {
vant.Dialog.alert({
title: "温馨提示",
message: "【" + arr.toString() + "】已成功报名您无需再报名!"
}).then(() => {})
return
}
}
const confirm = await this.$dialog.confirm({
title: "提示",
message: "您确认要报名吗?"
})
if (confirm === "confirm") {
const loading = this.$toast.loading({
message: "报名中...",
forbidClick: true,
overlay: true,
duration: 0
})
const params = {
activityId: this.viewData.id,
personIds: JSON.stringify(this.registerList.map((v) => v.id))
}
this.$axios.post("/platform/activity/culture/applyUser/doSaveUnionRegister", params).then((resp) => {
if (resp.code === 0) {
setTimeout(() => {
loading.close()
this.$emit("submit_back", this.viewData.id, this.registerList.length)
}, 1000)
} else {
this.$toast.fail(resp.msg)
}
})
}
},
searchUserAdd(item) {
const { id, userName } = item
this.registerList.push({ id, userName })
this.$toast.success("添加成功")
},
onSearchUser(val) {
if (!val) {
this.$toast.fail("请输入搜索条件")
return
}
this.searchLoading = true
this.$axios
.post("/platform/h5/activity/culture/searchNoRegisterUser", {
activityId: this.viewData.id,
searchKey: val
})
.then((resp) => {
if (resp.code === 0) {
this.searchLoading = false
this.searchUserList = resp.data.list
} else {
this.$toast.fail(resp.msg)
}
})
},
openUserActionSheet() {
this.searchKey = ""
this.searchUserList = []
this.userActionSheet = true
},
async removeRegUser(item) {
if (this.$store.state.user.id === item.id) {
this.$toast.fail("自己不能删除")
return
}
const confirm = await this.$dialog.confirm({
title: "提示",
message: "您确认要移除吗?"
})
if (confirm === "confirm") {
const index = this.registerList.findIndex((v) => v.id === item.id)
this.registerList.splice(index, 1)
}
},
back() {
this.$emit("back")
},
getRegisterUser() {
this.$axios.post("/platform/h5/activity/culture/selectRegisterList", { activityId: this.id }).then((resp) => {
if (resp.code === 0) {
if (this.viewData.signUpMethod === 3) {
this.registerList = resp.data.filter((v) => v.applyUserId === this.$store.state.user.id)
} else {
this.registerList = resp.data
}
this.addRegisterList = resp.data
if (this.registerList.length > 0) {
this.tagCloseable = false
}
}
})
},
getActivityInfo() {
this.$toast.loading({
message: "加载中...",
forbidClick: true
})
this.getRegisterUser()
this.$axios.post("/platform/activity/culture/infoManage/findOne", { id: this.id }).then((res) => {
if (res.data) {
res.data.billFiles = JSON.parse(res.data.billFiles)
res.data.photoFiles = JSON.parse(res.data.photoFiles)
res.data.otherFiles = JSON.parse(res.data.otherFiles)
res.data.unionUserNumberLimit = JSON.parse(res.data.unionUserNumberLimit)
if (!this.$store.state.user.union.id) {
this.$toast.fail("您的所属工会信息缺失,请联系管理员")
return
}
if (res.data.userNumberLimit === 2) {
const unionLimit = res.data.unionUserNumberLimit.find((v) => v.id === this.$store.state.user.union.id)
res.data.unionLimitNum = unionLimit.limitNum
}
this.viewData = res.data
if (this.viewData.signUpMethod === 3 && this.registerList.length === 0) {
this.registerList.push({
id: this.$store.state.user.id,
userName: this.$store.state.user.username
})
}
setTimeout(() => {
this.$toast.clear()
}, 1000)
} else {
this.viewData = {}
}
})
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,673 @@
<!--#
layout("/layouts/platform_h5.html"){
#-->
<style>
body {
background-color: #f3f7fd;
}
.family-activity-page {
min-height: 100vh;
padding-bottom: calc(24px + env(safe-area-inset-bottom));
box-sizing: border-box;
background-color: #f3f7fd;
}
.family-activity-page .van-nav-bar {
background-color: #fff;
}
.family-activity-page .van-nav-bar__title {
color: #1f2937;
font-size: 16px;
}
.family-activity-page .van-nav-bar .van-icon,
.family-activity-page .van-nav-bar__text {
color: #397fbd;
}
.family-activity-content {
padding: 12px 12px 0;
}
.family-activity-hero {
position: relative;
height: 134px;
overflow: hidden;
border-radius: 10px;
color: #fff;
background-color: #247fe8;
box-shadow: 0 8px 20px rgba(51, 117, 219, .18);
}
.family-activity-hero__image {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
object-fit: cover;
object-position: left center;
}
.family-activity-hero__copy {
position: relative;
z-index: 1;
display: flex;
width: 64%;
height: 100%;
padding-left: 16px;
flex-direction: column;
justify-content: center;
box-sizing: border-box;
background: linear-gradient(90deg, rgba(22, 112, 232, .98) 0%, rgba(22, 112, 232, .88) 52%, rgba(22, 112, 232, 0) 100%);
}
.family-activity-hero__title {
font-size: 25px;
line-height: 34px;
font-weight: 700;
letter-spacing: 1px;
text-shadow: 0 2px 6px rgba(17, 86, 182, .18);
}
.family-activity-hero__subtitle {
margin-top: 4px;
font-size: 13px;
line-height: 20px;
letter-spacing: .5px;
opacity: .95;
}
.family-activity-sticky {
padding: 10px 0 1px;
background-color: #f3f7fd;
}
.van-sticky--fixed .family-activity-sticky {
padding-right: 12px;
padding-left: 12px;
box-sizing: border-box;
}
.family-activity-search {
margin: 0 0 8px;
padding: 0;
overflow: hidden;
border-radius: 12px;
background-color: #fff;
box-shadow: 0 5px 16px rgba(43, 73, 112, .1);
}
.family-activity-search .van-search__content {
height: 42px;
padding-left: 12px;
align-items: center;
border-radius: 12px;
background-color: #fff;
}
.family-activity-filter {
margin-bottom: 10px;
overflow: hidden;
border-radius: 10px;
box-shadow: 0 5px 16px rgba(43, 73, 112, .08);
transition: border-radius .18s ease;
}
.family-activity-filter.is-open {
border-radius: 10px 10px 0 0;
}
.family-activity-filter .van-dropdown-menu__bar {
height: 44px;
box-shadow: none;
}
.family-activity-filter .van-dropdown-menu__title {
color: #66758a;
font-size: 14px;
}
.family-activity-filter .van-dropdown-menu__title--active,
.family-activity-filter .van-dropdown-item__option--active,
.family-activity-filter .van-dropdown-item__option--active .van-dropdown-item__icon {
color: #1989fa;
}
.family-activity-filter .van-dropdown-item__content {
width: calc(100% - 24px);
margin: 0 12px;
overflow: hidden;
border-radius: 0 0 12px 12px;
box-shadow: 0 10px 24px rgba(31, 65, 108, .14);
box-sizing: border-box;
}
.family-activity-list .table-list-container {
display: flex;
margin-top: 0 !important;
padding: 0 !important;
flex-direction: column;
gap: 10px;
}
.family-activity-list .table-list-item {
margin: 0 !important;
padding: 0 !important;
overflow: hidden;
border: 0 !important;
border-radius: 10px;
background-color: #fff;
box-shadow: 0 7px 20px rgba(55, 85, 126, .08);
}
.family-activity-list .table-list-item .item-header {
display: block !important;
margin: 0 !important;
}
.family-activity-list .table-list-item .item-actions:empty {
display: none;
}
.family-activity-card__cover,
.family-activity-card__cover-state {
display: flex;
width: 100%;
height: 150px;
align-items: center;
justify-content: center;
box-sizing: border-box;
background-color: #edf4fc;
}
.family-activity-card__cover {
display: block;
object-fit: cover;
}
.family-activity-card__cover-state {
flex-direction: column;
row-gap: 8px;
color: #8da1ba;
font-size: 13px;
}
.family-activity-card__cover-state .van-icon {
font-size: 34px;
}
.family-activity-card__cover-state.is-error {
color: #c27b75;
background-color: #fff3f1;
}
.family-activity-card__body {
position: relative;
width: 100%;
min-width: 0;
padding: 12px 38px 14px 14px;
box-sizing: border-box;
cursor: pointer;
}
.family-activity-card__title {
overflow: hidden;
color: #303846;
font-size: 14px;
line-height: 21px;
text-overflow: ellipsis;
white-space: nowrap;
}
.family-activity-card__time {
margin-top: 4px;
overflow: hidden;
color: #98a2b2;
font-size: 12px;
line-height: 17px;
text-overflow: ellipsis;
white-space: nowrap;
}
.family-activity-card__meta {
display: flex;
margin-top: 10px;
padding-top: 9px;
flex-wrap: wrap;
gap: 6px;
border-top: 1px solid #edf1f6;
}
.family-activity-card__tag {
display: inline-flex;
padding: 3px 8px;
align-items: center;
border-radius: 4px;
color: #397de0;
background-color: #edf5ff;
font-size: 11px;
line-height: 16px;
}
.family-activity-card__tag-label {
margin-right: 4px;
color: #8aa7cf;
}
.family-activity-card__arrow {
position: absolute;
top: 16px;
right: 14px;
color: #b4bdca;
font-size: 16px;
}
.family-activity-empty {
display: flex;
min-height: 260px;
padding: 26px 18px 32px;
align-items: center;
justify-content: center;
flex-direction: column;
text-align: center;
box-sizing: border-box;
}
.family-activity-empty img {
width: 82%;
max-width: 280px;
height: auto;
object-fit: contain;
}
.family-activity-empty__title {
margin-top: 14px;
color: #50627a;
font-size: 15px;
line-height: 22px;
font-weight: 600;
}
.family-activity-empty__hint {
margin-top: 5px;
color: #9aa7b8;
font-size: 12px;
line-height: 18px;
}
.family-intro-popup,
.family-intro-page {
background-color: #f3f7fd;
}
.family-intro-page {
position: relative;
min-height: 100vh;
}
.family-intro-scroll {
height: calc(100vh - 46px);
padding: 12px 12px calc(92px + env(safe-area-inset-bottom));
overflow-y: auto;
box-sizing: border-box;
}
.family-intro-hero {
display: flex;
min-height: 82px;
padding: 16px;
align-items: center;
border-radius: 12px;
color: #fff;
background: linear-gradient(120deg, #2d7eea 0%, #5ba6f7 100%);
box-shadow: 0 8px 20px rgba(45, 126, 234, .18);
box-sizing: border-box;
}
.family-intro-hero__icon {
display: flex;
width: 44px;
height: 44px;
margin-right: 12px;
flex: 0 0 44px;
align-items: center;
justify-content: center;
border-radius: 11px;
color: #2d7eea;
background-color: rgba(255, 255, 255, .92);
font-size: 25px;
}
.family-intro-hero__title {
font-size: 17px;
line-height: 24px;
font-weight: 600;
}
.family-intro-hero__subtitle {
margin-top: 3px;
font-size: 12px;
line-height: 18px;
opacity: .9;
}
.family-intro-card {
margin-top: 12px;
padding: 16px;
overflow: hidden;
border-radius: 12px;
background-color: #fff;
box-shadow: 0 7px 20px rgba(55, 85, 126, .08);
}
.family-intro-card .content {
padding: 0 !important;
color: #3f4c5e !important;
word-break: break-word;
}
.family-intro-card .content div,
.family-intro-card .content p,
.family-intro-card .content span,
.family-intro-card .content li {
color: #3f4c5e !important;
font-size: 14px !important;
line-height: 1.8 !important;
white-space: normal !important;
word-break: break-word;
}
.family-intro-card .content h1,
.family-intro-card .content h2,
.family-intro-card .content h3,
.family-intro-card .content h4 {
margin: 16px 0 9px !important;
color: #1f2f43 !important;
font-size: 18px !important;
line-height: 1.5 !important;
font-weight: 600 !important;
}
.family-intro-card .content > :first-child {
margin-top: 0 !important;
}
.family-intro-card .content p {
margin: 0 0 8px !important;
}
.family-intro-card .content ul,
.family-intro-card .content ol {
margin: 8px 0 !important;
padding-left: 22px !important;
}
.family-intro-card .content img {
max-width: 100% !important;
height: auto !important;
margin: 8px auto !important;
border-radius: 8px;
}
.family-intro-card .content table {
display: block;
max-width: 100%;
overflow-x: auto;
border-collapse: collapse;
}
.family-intro-empty {
display: flex;
min-height: 180px;
margin-top: 12px;
align-items: center;
justify-content: center;
flex-direction: column;
row-gap: 10px;
border-radius: 12px;
color: #9aa7b8;
background-color: #fff;
box-shadow: 0 7px 20px rgba(55, 85, 126, .08);
font-size: 14px;
}
.family-intro-empty .van-icon {
color: #83b5f4;
font-size: 36px;
}
.family-intro-footer {
position: fixed;
right: 0;
bottom: 0;
left: 0;
z-index: 5;
padding: 10px 12px calc(10px + env(safe-area-inset-bottom));
border-top: 1px solid #edf1f6;
background-color: rgba(255, 255, 255, .96);
box-sizing: border-box;
}
.family-intro-footer .van-button {
height: 42px;
border: 0;
border-radius: 21px;
font-size: 15px;
background-color: #1989fa;
box-shadow: 0 6px 16px rgba(25, 137, 250, .22);
}
.family-intro-footer .van-button--disabled {
color: #8b98aa !important;
background-color: #eef2f7 !important;
opacity: 1;
box-shadow: none;
}
.family-intro-footer .van-count-down {
display: inline-block;
margin-left: 4px;
color: #fff;
font-size: 13px;
vertical-align: middle;
}
</style>
<div id="app" v-cloak>
<div class="family-activity-page">
<van-nav-bar title="亲子活动" left-text="返回" left-arrow @click-left="historyBack" fixed placeholder></van-nav-bar>
<div class="family-activity-content">
<section class="family-activity-hero">
<img class="family-activity-hero__image" src="/assets/mobile/img/activity/hero/family-hero-v3.png" alt="亲子活动插画">
<div class="family-activity-hero__copy">
<div class="family-activity-hero__title">亲子活动</div>
<div class="family-activity-hero__subtitle">温馨陪伴 · 快乐成长</div>
</div>
</section>
<van-sticky :offset-top="46">
<div class="family-activity-sticky">
<van-search v-model="pageForm.searchKeyword" class="family-activity-search" placeholder="搜索亲子活动"
shape="round" clearable @search="doSearch" @clear="doSearch"></van-search>
<van-dropdown-menu class="family-activity-filter" :class="{'is-open': filterOpened}" active-color="#1989fa">
<year-van-dropdown-item :num="5" @change="doSearch" @open="filterOpened = true" @close="filterOpened = false" v-model="pageForm.year"></year-van-dropdown-item>
<van-dropdown-item v-model="pageForm.activityType" :options="typeOptions" :multiple="false"
@change="doSearch" @open="filterOpened = true" @close="filterOpened = false"></van-dropdown-item>
</van-dropdown-menu>
</div>
</van-sticky>
<table-list class="family-activity-list" api="/platform/family/apply/activityPageData"
:page_form.sync="pageForm" ref="tableListRef" @ready="onReady">
<template #empty>
<div class="family-activity-empty">
<img src="/assets/mobile/img/activity/family-empty.png" alt="亲子活动空状态插画">
<div class="family-activity-empty__title">暂无亲子活动</div>
<div class="family-activity-empty__hint">当前筛选条件下暂时没有可展示的活动</div>
</div>
</template>
<template #header="{index,row}">
<img v-if="hasCover(row) && !imageLoadErrors[row.id]" class="family-activity-card__cover"
:src="row.cover" :alt="row.activityName" @error="handleImageError(row)" @click="onView(row)">
<div v-else class="family-activity-card__cover-state" :class="{'is-error': hasCover(row)}" @click="onView(row)">
<van-icon name="photo-o"></van-icon>
<span>{{ hasCover(row) ? '图片加载失败' : '暂无图片' }}</span>
</div>
</template>
<template v-slot="{index,row}">
<div class="family-activity-card__body" @click="onView(row)">
<div class="family-activity-card__title">{{ row.activityName }}</div>
<div class="family-activity-card__time" v-if="row.activitySignUpStartTime && row.activitySignUpEndTime">
报名时间:{{ $moment(row.activitySignUpStartTime).format('MM/DD HH:mm') }}~{{ $moment(row.activitySignUpEndTime).format('MM/DD HH:mm') }}
</div>
<div class="family-activity-card__time" v-if="row.activityStartTime && row.activityEndTime">
活动时间:{{ $moment(row.activityStartTime).format('MM/DD HH:mm') }}~{{ $moment(row.activityEndTime).format('MM/DD HH:mm') }}
</div>
<div class="family-activity-card__meta" v-if="row.activityGroupName || row.trainType">
<span class="family-activity-card__tag" v-if="row.activityGroupName"><span class="family-activity-card__tag-label">面向对象</span>{{row.activityGroupName}}</span>
<span class="family-activity-card__tag" v-if="row.trainType"><span class="family-activity-card__tag-label">活动类型</span>{{row.trainType}}</span>
</div>
<van-icon class="family-activity-card__arrow" name="arrow"></van-icon>
</div>
</template>
</table-list>
</div>
</div>
<van-popup class="family-intro-popup" :style="{height: '100%', width: '100%'}" position="right" safe-area-inset-bottom v-model="infoVisible">
<div class="family-intro-page">
<van-nav-bar title="活动介绍" left-text="返回" left-arrow @click-left="infoVisible = false"></van-nav-bar>
<div class="family-intro-scroll">
<div class="family-intro-hero">
<div class="family-intro-hero__icon"><van-icon name="description"></van-icon></div>
<div>
<div class="family-intro-hero__title">亲子活动报名须知</div>
<div class="family-intro-hero__subtitle">请仔细阅读活动安排与报名要求</div>
</div>
</div>
<div class="family-intro-card" v-if="infoRow.introduce">
<pdf-preview :content="infoRow.introduce"></pdf-preview>
</div>
<div v-else class="family-intro-empty">
<van-icon name="description"></van-icon>
<span>暂无活动介绍</span>
</div>
</div>
<div class="family-intro-footer">
<van-button v-if="canApply(infoRow)" @click="onApply(infoRow)" type="primary" block round>进入报名</van-button>
<van-button v-else-if="time < 0" @click="onApply(infoRow)" type="primary" block round>
距离开始
<van-count-down :time="Math.abs(time)" format="DD 天 HH 时 mm 分 ss 秒" @finish="time = 0"></van-count-down>
</van-button>
<van-button v-else disabled block round>{{ registrationStatusText(infoRow) }}</van-button>
</div>
</div>
</van-popup>
</div>
<script nonce="${cspNonce!}">
const vue = new Vue({
el: "#app",
store,
components: {
"pdf-preview": httpVueLoader("/components/plugins/sysFilePreview/PdfIndex.vue?v=" + new Date().getTime())
},
data() {
return {
time: 0,
pageForm: {
pageNumber: 1,
pageSize: 10,
totalCount: 0,
searchKeyword: '',
year: new Date().getFullYear(),
activityType: 2,
},
typeOptions: [
{text: '全部', value: 1},
{text: '即将开始 & 报名中', value: 2},
{text: '已结束', value: 3},
],
infoVisible: false,
infoRow: {},
imageLoadErrors: {},
filterOpened: false,
id: GetQueryString('id')
}
},
methods: {
hasCover(row) {
return !!(row && row.cover && String(row.cover).trim())
},
handleImageError(row) {
this.$set(this.imageLoadErrors, row.id, true)
},
isRegistrationClosed(row) {
return !!(row && row.activitySignUpEndTime && !this.$moment().isBefore(this.$moment(row.activitySignUpEndTime)))
},
canApply(row) {
if(!row || !row.activitySignUpStartTime || !row.activitySignUpEndTime) {
return false
}
const now = Date.now()
return now >= this.$moment(row.activitySignUpStartTime).valueOf()
&& now <= this.$moment(row.activitySignUpEndTime).valueOf()
},
registrationStatusText(row) {
if(!row || !row.activitySignUpStartTime || !row.activitySignUpEndTime) {
return '当前不可报名'
}
if(Date.now() < this.$moment(row.activitySignUpStartTime).valueOf()) {
return '报名未开始'
}
return '已过报名时间'
},
onView(row) {
this.infoRow = row
this.time = this.$moment().diff(this.$moment(row.activitySignUpStartTime), 'milliseconds')
this.infoVisible = true
},
onApply(row) {
if(this.isRegistrationClosed(row)) {
return
}
if(this.$moment().isBefore(this.$moment(row.activitySignUpStartTime))) {
this.onView(row)
return
}
this.$pjaxReplace('/platform/family/apply/list/h5?id=' + row.id)
},
fetchOne() {
this.$axios.post('/platform/family/manage/findOne', {id: this.id}).then((res) => {
if(res.code === 0) {
this.onView(res.data)
}
})
},
onReady() {
this.doSearch()
if(this.id) {
this.fetchOne()
}
},
doSearch() {
this.$nextTick(() => {
this.pageForm.pageNumber = 1
this.pageForm.totalCount = 0
this.$refs.tableListRef.doSearch()
})
}
},
})
</script>
<!--#
}
#-->
@@ -0,0 +1,169 @@
const times = {
template:
/*language=HTML*/
`
<div>
<van-action-sheet class="family-course-times-sheet" :style="{height: '78%'}" :close-on-click-overlay="true" v-model="visible" title="时间段" cancel-text="取消">
<div class="family-course-times-list">
<button v-for="(item,index) in row?.courseTimes" :key="index" type="button" class="van-action-sheet__item family-course-time-item">
<div style="display: flex; justify-content: space-between; align-items: center">
<div>
<div class="van-action-sheet__name">
<label>{{ weekdayCNMap[$moment(item.courseDate).day()] }}</label>
<label>{{ $moment(item.courseDate).format('YYYY-MM-DD') }}</label>
</div>
<div class="van-action-sheet__subname">
{{$moment(item.courseStartTime).format('MM-DD HH:mm') + ' 至 ' + $moment(item.courseEndTime).format('MM-DD HH:mm')}}
</div>
</div>
<div v-if="row.isSign === true && row.isMobileSign === true" class="sign_button">
<van-button v-if="item.isAttend !== true" @click.stop="onSign(item)" size="mini" type="info">签到</van-button>
<van-button v-if="item.isAttend === true" size="mini" type="info" disabled>已签到</van-button>
</div>
</div>
</button>
</div>
</van-action-sheet>
<van-popup round :safe-area-inset-bottom="true"
:close-on-click-overlay="false"
v-model="signVisible"
:style="{ width: '80%', height: '66%' }"
get-container="#app"
@close="onSignClose"
closeable
>
<scan-code ref="scanCodeRef"></scan-code>
</van-popup>
</div>
`,
store,
data() {
return {
visible:false,
row: null,
weekdayCNMap: {0: '周日', 1: '周一', 2: '周二', 3: '周三', 4: '周四', 5: '周五', 6: '周六'},
selectCourseTime: {},
signVisible: false,
}
},
components: {
"scan-code": httpVueLoader("/components/plugins/sysScanCode/index.vue?v=" + new Date().getTime())
},
methods: {
onSignClose() {
this.$refs.scanCodeRef.closeScan()
},
onOpen(row) {
this.row = row
this.visible = true
},
onSign(courseTime) {
this.selectCourseTime = courseTime
if(this.row.signType === 1) {
this.signVisible = true
this.$nextTick(() => {
this.$refs.scanCodeRef.init()
})
}
if(this.row.signType === 2) {
this.makeCode()
}
if(this.row.signType === 3) {
this.$toast('此签到模式正在升级中')
}
},
makeCode() {
const url = '/platform/family/mine/passiveScan'
const data = url + '?id=' + this.selectCourseTime.id
console.log(data)
const content = jrQrcode.getQrBase64(data)
vant.ImagePreview([content])
},
onClose(){
this.visible = false
},
},
style: /*language=CSS*/ `
.family-course-times-sheet {
overflow: hidden;
border-radius: 16px 16px 0 0;
background-color: #f3f7fd;
}
.family-course-times-sheet /deep/ .van-action-sheet__header {
height: 48px;
color: #1f2f43;
background-color: #fff;
font-size: 16px;
line-height: 48px;
}
.family-course-times-sheet /deep/ .van-action-sheet__close {
top: 13px;
color: #b5bdc9;
font-size: 20px;
}
.family-course-times-list {
height: calc(100% - 98px);
padding: 10px 12px 0;
overflow-y: auto;
box-sizing: border-box;
}
.family-course-times-sheet /deep/ .family-course-time-item {
width: 100%;
margin-bottom: 10px;
padding: 14px;
border-radius: 10px;
color: #263548;
background-color: #fff;
box-shadow: 0 6px 18px rgba(55, 85, 126, .07);
box-sizing: border-box;
}
.family-course-times-sheet /deep/ .van-action-sheet__name {
color: #263548;
font-size: 14px;
line-height: 21px;
text-align: left;
}
.family-course-times-sheet /deep/ .van-action-sheet__subname {
margin-top: 5px;
color: #98a2b2;
font-size: 12px;
line-height: 18px;
text-align: left;
}
.family-course-times-sheet /deep/ .sign_button .van-button {
min-width: 48px;
height: 28px;
padding: 0 12px;
border: 0;
border-radius: 14px;
color: #fff;
background-color: #1989fa;
font-size: 12px;
line-height: 28px;
box-shadow: 0 4px 10px rgba(25, 137, 250, .18);
}
.family-course-times-sheet /deep/ .sign_button .van-button--disabled {
color: #8b98aa;
background-color: #eef2f7;
opacity: 1;
box-shadow: none;
}
.family-course-times-sheet /deep/ .van-action-sheet__cancel {
height: 50px;
color: #66758a;
background-color: #fff;
font-size: 14px;
}
`
}
@@ -0,0 +1,448 @@
const applyForm = {
template:
/*language=HTML*/
`
<div>
<van-action-sheet class="family-course-form-sheet" :style="{height: '78%'}" :close-on-click-overlay="true" title="报名信息" v-model="visible">
<van-form ref="formRef" class="form-container">
<van-cell-group title="活动信息" class="form-section">
<van-field label="活动名称" readonly v-model="row.courseName"></van-field>
<van-field label="校区" readonly v-model="row.campus"></van-field>
<van-field label="活动地点" readonly v-model="row.courseLocation"></van-field>
</van-cell-group>
<van-cell-group title="基础信息" class="form-section">
<van-field label="姓名" readonly v-model="formData.username"></van-field>
<van-field label="工号" readonly v-model="formData.loginname"></van-field>
<van-field label="所属单位" readonly v-model="formData.unitName"></van-field>
<van-field label="所属工会" readonly v-model="formData.unionName"></van-field>
<van-field label="性别" readonly v-model="formData.sex"></van-field>
<template v-if="row.courseIsLimitApply">
<van-field label="报名时段"
required
:rules="[{ required: true, message: '请选择报名时段' }]"
readonly
@click="showCoursePicker = true"
placeholder="请选择报名时段"
name="courseTimeName"
v-model="formData.courseTimeName">
</van-field>
<van-popup v-model="showCoursePicker" position="bottom">
<van-picker
show-toolbar
:columns="courseTimeSelectList"
@confirm="onCourseConfirm"
@cancel="showCoursePicker=false"
></van-picker>
</van-popup>
</template>
</van-cell-group>
<van-cell-group class="form-section">
<template #title>
<div class="family-member-section-title">
<div>{{activity.keyWord}}信息</div>
<div class="family-member-actions">
<button type="button" class="family-member-action family-member-action--delete" :aria-label="'删除' + activity.keyWord" @click="delFamily">
<van-icon name="delete-o"></van-icon>
</button>
<button type="button" class="family-member-action family-member-action--add" :aria-label="'添加' + activity.keyWord" @click="addFamily">
<van-icon name="plus"></van-icon>
</button>
</div>
</div>
</template>
<div v-if="formData.mobileColumnsValue && formData.mobileColumnsValue.length > 0" class="mt10">
<van-tabs v-model="familyActive" color="#1989fa" title-active-color="#1989fa" animated>
<van-tab v-for="item, index in formData.mobileColumnsValue"
:name="index + ''"
:title="activity.keyWord + (index + 1)"
:key="index">
<train-dynamic-form v-model="formData.mobileColumnsValue[index]"></train-dynamic-form>
</van-tab>
</van-tabs>
</div>
<div v-if="formData.mobileColumnsValue?.length === 0" class="companionList_empty_text">
<span>
暂无数据,请添加{{activity.keyWord}}
</span>
</div>
</van-cell-group>
<div class="button">
<van-button @click="onSubmit" round type="info" block>提交</van-button>
</div>
</van-form>
</van-action-sheet>
</div>
`,
data() {
return {
familyActive: '',
row: {},
activity: {},
visible: false,
formData: {},
showCoursePicker: false,
courseTimeSelectList: [],
courseType: {},
}
},
components: {
"train-dynamic-form": httpVueLoader("/components/module/trainSignUp/TrainDynamicForm.vue")
},
methods: {
addFamily() {
if(this.formData.mobileColumnsValue.length >= this.activity.familyMaxCount) {
this.$toast(this.activity.keyWord + '最多人数为' + this.activity.familyMaxCount)
return
}
const list = clone(this.courseType.familyMobileSignColumnList)
this.formData.mobileColumnsValue.push(list)
},
delFamily() {
this.formData.mobileColumnsValue.splice(this.familyActive, 1)
const ac = (Number(this.familyActive) - 1)
this.familyActive = '' + (ac >= 0 ? ac : 0)
},
async onOpen(row, courseType, activity) {
this.row = row
this.courseType = courseType
this.activity = activity
this.init(row, courseType)
if(row.courseIsLimitApply) {
await this.getCourseTimeSelectList(row)
}
this.visible = true
},
init(row, courseType) {
this.$set(this.formData, 'username', this.$store.state.user.username)
this.$set(this.formData, 'loginname', this.$store.state.user.loginname)
this.$set(this.formData, 'unionName', this.$store.state.user.union.name)
this.$set(this.formData, 'unitName', this.$store.state.user.unit.name)
this.$set(this.formData, 'sex', this.$store.state.user.sex)
this.$set(this.formData, 'activityId', row.activityId)
this.$set(this.formData, 'courseId', row.id)
this.$set(this.formData, 'mobileColumnsValue', [])
},
onCourseConfirm(val){
this.formData.activityCourseId = val.value
this.$set(this.formData, "activityCourseId", val.value)
this.$set(this.formData, "courseTimeName", val.text.substring(0, 12))
this.showCoursePicker = false
},
async getCourseTimeSelectList(o) {
const resp = await this.$axios.post('/platform/family/apply/getCourseTimeSelectList',{courseId: o.id})
if (resp.code === 0) {
this.courseTimeSelectList = resp.data
} else {
this.$toast.fail("获取时段信息失败,请联系管理员")
}
},
validateIdCard(idCard) {
if (!idCard) {
return false
}
// 18位身份证号码校验
if (idCard.length === 18) {
const idCardRegex = /^[1-9]\d{5}(18|19|20)?\d{2}((0[1-9])|(10|11|12))(([0|1|2][0-9])|10|20|30|31)\d{3}(\d|[Xx])$/
if (!idCardRegex.test(idCard)) {
return false
}
// 校验码计算
const weights = [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2]
const checksums = ["1", "0", "X", "9", "8", "7", "6", "5", "4", "3", "2"]
let sum = 0
for (let i = 0; i < 17; i++) {
sum += idCard[i] * weights[i]
}
const checksum = checksums[sum % 11]
return checksum === idCard[17].toUpperCase()
}
// 15位身份证号码校验
else if (idCard.length === 15) {
const idCardRegex = /^[1-9]\d{5}\d{2}((0[1-9])|(10|11|12))(([0|1|2][0-9])|10|20|30|31)\d{3}$/
return idCardRegex.test(idCard)
}
// 外国人或其他情况
else {
return false
}
},
validFamilyForm() {
// 校验规则映射
const validators = {
idCard: (value) => this.validateIdCard(value),
mobile: (value) => /^1[3-9]\d{9}$/.test(value),
email: (value) => /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/.test(value)
};
// 显示提示弹窗(直接传完整的 message)
const showAlert = (message) => {
vant.Dialog.alert({
title: '提示',
message: message, // 直接使用传入的完整消息
confirmButtonColor: '#1867b0'
});
};
const { mobileColumnsValue } = this.formData;
for (let i = 0; i < mobileColumnsValue.length; i++) {
const family = mobileColumnsValue[i];
for (const col of family) {
const value = col.columnValue?.trim(); // 安全处理空值和空格
// 必填校验
if (col.isRequired && !value) {
const message = this.activity.keyWord + (i + 1) + '的' + col.columnName + '不能为空';
showAlert(message);
return false;
}
// 格式校验
if (col.validRule && value) {
const validator = validators[col.validRule];
if (validator && !validator(value)) {
const message = this.activity.keyWord + (i + 1) + '的' + col.columnName + '格式不正确';
showAlert(message);
return false;
}
}
if(col.validRule === 'idCard' && this.row.familyAgeLimit) {
const parseCard = this.parseIdCard(value)
if(this.row.minAge && parseCard.age < this.row.minAge) {
const message = '限制报名最小年龄为' + this.row.minAge;
showAlert(message);
return false;
}
if(this.row.maxAge && parseCard.age > this.row.maxAge) {
const message = '限制报名最大年龄为' + this.row.maxAge;
showAlert(message);
return false;
}
}
if(col.validRule === 'idCard' && this.row.familySexLimit) {
const parseCard = this.parseIdCard(value)
if(this.row.familySex && parseCard.sex !== this.row.familySex) {
const message = '限制报名性别为' + this.row.familySex;
showAlert(message);
return false;
}
}
}
}
return true;
},
parseIdCard(idCard) {
if (!idCard || (idCard.length !== 18 && idCard.length !== 15)) {
return null;
}
let birthStr = '';
let sexCode = '';
if (idCard.length === 15) {
birthStr = '19' + idCard.substring(6, 12);
sexCode = idCard.substring(14, 15); // 15位:第15位是性别
} else {
birthStr = idCard.substring(6, 14);
sexCode = idCard.substring(16, 17); // 18位:第17位是性别
}
const birthYear = parseInt(birthStr.substring(0, 4), 10);
const birthMonth = parseInt(birthStr.substring(4, 6), 10) - 1;
const birthDay = parseInt(birthStr.substring(6, 8), 10);
const birthDate = new Date(birthYear, birthMonth, birthDay);
const today = new Date();
let age = today.getFullYear() - birthDate.getFullYear();
const monthDiff = today.getMonth() - birthDate.getMonth();
const dayDiff = today.getDate() - birthDate.getDate();
if (monthDiff < 0 || (monthDiff === 0 && dayDiff < 0)) {
age--;
}
const sex = parseInt(sexCode, 10) % 2 === 1 ? '男' : '女';
return { age, sex, };
},
async validateSignUp() {
// 获取家属人数
const res = await this.$axios.post("/platform/family/apply/validateSignUp", {
courseId: this.formData.courseId,
currentFamilyNumber: this.formData.mobileColumnsValue.length || 0
})
if(res.code !== 0) {
this.$dialog.alert({title: '温馨提示', message: res.msg})
}
return res.code === 0
},
async validateCourseTime() {
const res = await this.$axios.post('/platform/family/apply/validateSourceSignUp', {
activityCourseId: this.formData.activityCourseId,
courseId: this.row.id
})
if(res.code !== 0) {
this.$dialog.alert({title: '温馨提示', message: res.msg})
}
return res.code === 0
},
async onSubmit() {
if (!this.validFamilyForm()) return
if (!await this.validateSignUp()) return
this.$refs.formRef.validate().then(() => {
this.$dialog.confirm({
title: "提示",
message: "您确定要提交吗?"
}).then(async () => {
if (this.row.courseIsLimitApply) {
if(!await this.validateCourseTime()) return
}
const formData = clone(this.formData)
let array = []
formData.mobileColumnsValue.forEach(item => {
const o = item.map((v) => {
return {
columnName: v.columnName,
columnValue: v.columnValue,
columnCode: v.columnCode,
columnFormType: v.columnFormType
}
})
array.push(o)
})
formData.mobileColumnsValue = JSON.stringify(clone(array))
this.$axios.post("/platform/family/apply/doSignUp", formData).then(res => {
this.$dialog.alert({title: '温馨提示', message: res.msg})
.then(() => {
if (res.code === 0) {
this.visible = false
this.$emit('refresh')
}
})
})
})
})
},
},
style: /*language=CSS*/ `
.family-course-form-sheet {
overflow: hidden;
border-radius: 16px 16px 0 0;
background-color: #f3f7fd;
}
.family-course-form-sheet /deep/ .van-action-sheet__header {
height: 48px;
color: #1f2f43;
background-color: #fff;
font-size: 16px;
line-height: 48px;
}
.family-course-form-sheet /deep/ .van-action-sheet__close {
top: 13px;
color: #b5bdc9;
font-size: 20px;
}
.family-course-form-sheet /deep/ .form-container {
height: calc(100% - 48px);
padding: 10px 12px 72px;
overflow-y: auto;
background-color: #f3f7fd;
box-sizing: border-box;
}
.family-course-form-sheet /deep/ .form-section {
margin-bottom: 10px;
overflow: hidden;
border-radius: 10px;
background-color: #fff;
box-shadow: 0 6px 18px rgba(55, 85, 126, .07);
}
.family-course-form-sheet /deep/ .van-cell-group__title {
padding: 12px 14px 8px;
color: #263548;
background-color: #fff;
font-size: 14px;
line-height: 20px;
font-weight: 600;
}
.family-course-form-sheet /deep/ .van-cell-group__title::before {
display: none;
}
.family-course-form-sheet /deep/ .van-cell {
padding: 11px 14px;
color: #263548;
background-color: #fff;
font-size: 13px;
}
.family-course-form-sheet /deep/ .van-field__label {
color: #66758a;
font-size: 13px;
}
.family-course-form-sheet /deep/ .van-field__control {
color: #263548;
font-size: 13px;
}
.family-course-form-sheet /deep/ .van-tabs__nav--card {
overflow: hidden;
border: 0;
border-radius: 16px;
background-color: #eef3f9;
}
.family-course-form-sheet /deep/ .van-tabs__nav--card .van-tab {
border-right: 0;
color: #66758a;
}
.family-course-form-sheet /deep/ .van-tabs__nav--card .van-tab--active {
color: #1989fa;
background-color: #e7f2ff;
}
/deep/ .companionList_empty_text {
text-align: center;
padding: 10px 0;
font-size: 14px;
color: grey;
}
.family-course-form-sheet /deep/ .button {
position: absolute;
right: 0;
bottom: 0;
left: 0;
z-index: 2;
width: auto;
padding: 10px 12px calc(10px + env(safe-area-inset-bottom));
border-top: 1px solid #edf1f6;
background-color: rgba(255, 255, 255, .96);
box-sizing: border-box;
}
.family-course-form-sheet /deep/ .button .van-button {
height: 42px;
border: 0;
border-radius: 21px;
color: #fff;
background-color: #1989fa;
font-size: 15px;
box-shadow: 0 6px 16px rgba(25, 137, 250, .22);
}
`
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,550 @@
<!--#
layout("/layouts/platform_h5.html"){
#-->
<style>
body {
background-color: #f3f7fd;
}
.family-mine-page {
min-height: 100vh;
padding-bottom: calc(24px + env(safe-area-inset-bottom));
box-sizing: border-box;
background-color: #f3f7fd;
}
.family-mine-page .van-nav-bar {
background-color: #fff;
}
.family-mine-page .van-nav-bar__title {
color: #1f2937;
font-size: 16px;
}
.family-mine-page .van-nav-bar .van-icon,
.family-mine-page .van-nav-bar__text {
color: #397fbd;
}
.family-mine-content {
padding: 12px 12px 0;
}
.family-mine-hero {
position: relative;
height: 134px;
overflow: hidden;
border-radius: 10px;
color: #fff;
background: linear-gradient(115deg, #347eea 0%, #5b9cf4 58%, #cfe3ff 100%);
box-shadow: 0 8px 20px rgba(51, 117, 219, .18);
}
.family-mine-hero__image {
position: absolute;
top: 0;
right: 0;
width: 67%;
height: 100%;
object-fit: cover;
object-position: right center;
-webkit-mask-image: linear-gradient(90deg, transparent 0%, #000 26%);
mask-image: linear-gradient(90deg, transparent 0%, #000 26%);
}
.family-mine-hero__copy {
position: relative;
z-index: 1;
display: flex;
height: 100%;
padding-left: 16px;
flex-direction: column;
justify-content: center;
box-sizing: border-box;
}
.family-mine-hero__title {
font-size: 25px;
line-height: 34px;
font-weight: 700;
letter-spacing: 1px;
}
.family-mine-hero__subtitle {
margin-top: 4px;
font-size: 13px;
line-height: 20px;
letter-spacing: .5px;
opacity: .95;
}
.family-mine-sticky {
padding: 10px 0 1px;
background-color: #f3f7fd;
}
.van-sticky--fixed .family-mine-sticky {
padding-right: 12px;
padding-left: 12px;
box-sizing: border-box;
}
.family-mine-search {
margin-bottom: 8px;
padding: 0;
overflow: hidden;
border-radius: 12px;
background-color: #fff;
box-shadow: 0 5px 16px rgba(43, 73, 112, .1);
}
.family-mine-search .van-search__content {
height: 42px;
padding-left: 12px;
align-items: center;
border-radius: 12px;
background-color: #fff;
}
.family-mine-search .van-field__control {
color: #263548;
font-size: 14px;
}
.family-mine-filter {
margin-bottom: 10px;
overflow: hidden;
border-radius: 10px;
box-shadow: 0 5px 16px rgba(43, 73, 112, .08);
transition: border-radius .18s ease;
}
.family-mine-filter.is-open {
border-radius: 10px 10px 0 0;
}
.family-mine-filter .van-dropdown-menu__bar {
height: 44px;
box-shadow: none;
}
.family-mine-filter .van-dropdown-menu__title {
width: auto;
max-width: calc(100% - 24px);
padding: 0 12px 0 0;
color: #66758a;
font-size: 14px;
}
.family-mine-filter .van-dropdown-menu__title::after {
right: 0;
}
.family-mine-filter .van-dropdown-item__content {
width: calc(100% - 24px);
margin: 0 12px;
overflow: hidden;
border-radius: 0 0 12px 12px;
box-shadow: 0 10px 24px rgba(31, 65, 108, .14);
box-sizing: border-box;
}
.family-mine-list {
display: flex;
flex-direction: column;
gap: 10px;
}
.family-mine-card {
overflow: hidden;
border-radius: 10px;
background-color: #fff;
box-shadow: 0 7px 20px rgba(55, 85, 126, .08);
}
.family-mine-card__cover,
.family-mine-card__cover-state {
display: flex;
width: 100%;
height: 150px;
align-items: center;
justify-content: center;
background-color: #edf4fc;
}
.family-mine-card__cover {
display: block;
object-fit: cover;
}
.family-mine-card__cover-state {
flex-direction: column;
row-gap: 8px;
color: #8da1ba;
font-size: 13px;
}
.family-mine-card__cover-state .van-icon {
font-size: 34px;
}
.family-mine-card__cover-state.is-error {
color: #c27b75;
background-color: #fff3f1;
}
.family-mine-card__body {
padding: 12px 14px 14px;
}
.family-mine-card__title {
overflow: hidden;
color: #303846;
font-size: 14px;
line-height: 21px;
text-overflow: ellipsis;
white-space: nowrap;
}
.family-mine-card__time {
margin-top: 4px;
overflow: hidden;
color: #98a2b2;
font-size: 12px;
line-height: 17px;
text-overflow: ellipsis;
white-space: nowrap;
}
.family-mine-card__meta {
display: flex;
margin-top: 10px;
padding-top: 9px;
flex-wrap: wrap;
gap: 6px;
border-top: 1px solid #edf1f6;
}
.family-mine-card__tag {
display: inline-flex;
padding: 3px 8px;
border-radius: 4px;
color: #397de0;
background-color: #edf5ff;
font-size: 11px;
line-height: 16px;
}
.family-mine-card__tag-label {
margin-right: 4px;
color: #8aa7cf;
}
.family-mine-card__actions {
display: flex;
padding: 12px 14px;
align-items: center;
justify-content: flex-end;
gap: 10px;
border-top: 1px solid #edf1f6;
background-color: #f8fbff;
}
.family-mine-card__action {
display: inline-flex;
min-width: 82px;
height: 32px;
padding: 0 14px;
align-items: center;
justify-content: center;
border: 0;
border-radius: 16px;
color: #397de0;
background-color: #edf5ff;
font-size: 13px;
box-sizing: border-box;
}
.family-mine-card__action .van-icon {
margin-right: 6px;
font-size: 15px;
}
.family-mine-card__action.is-primary {
color: #fff;
background-color: #1989fa;
box-shadow: 0 5px 12px rgba(25, 137, 250, .18);
}
.family-mine-empty {
display: flex;
min-height: 260px;
padding: 26px 18px 32px;
align-items: center;
justify-content: center;
flex-direction: column;
text-align: center;
}
.family-mine-empty img {
width: 82%;
max-width: 280px;
height: auto;
object-fit: contain;
}
.family-mine-empty__title {
margin-top: 14px;
color: #50627a;
font-size: 15px;
line-height: 22px;
font-weight: 600;
}
.family-mine-empty__hint {
margin-top: 5px;
color: #9aa7b8;
font-size: 12px;
line-height: 18px;
}
.family-mine-intro-page {
min-height: 100vh;
background-color: #f3f7fd;
}
.family-mine-intro-scroll {
height: calc(100vh - 46px);
padding: 12px;
overflow-y: auto;
box-sizing: border-box;
}
.family-mine-intro-hero {
display: flex;
min-height: 82px;
padding: 16px;
align-items: center;
border-radius: 12px;
color: #fff;
background: linear-gradient(120deg, #2d7eea 0%, #5ba6f7 100%);
box-shadow: 0 8px 20px rgba(45, 126, 234, .18);
box-sizing: border-box;
}
.family-mine-intro-hero__icon {
display: flex;
width: 44px;
height: 44px;
margin-right: 12px;
align-items: center;
justify-content: center;
flex: 0 0 44px;
border-radius: 11px;
color: #2d7eea;
background-color: rgba(255, 255, 255, .92);
font-size: 25px;
}
.family-mine-intro-hero__title {
font-size: 17px;
line-height: 24px;
}
.family-mine-intro-hero__subtitle {
margin-top: 3px;
font-size: 12px;
line-height: 18px;
opacity: .9;
}
.family-mine-intro-card {
margin-top: 12px;
padding: 18px 16px;
overflow: hidden;
border-radius: 12px;
color: #3f4c5e;
background-color: #fff;
box-shadow: 0 7px 20px rgba(55, 85, 126, .08);
font-size: 14px;
line-height: 1.8;
word-break: break-word;
}
.family-mine-intro-card img {
max-width: 100% !important;
height: auto !important;
border-radius: 8px;
}
</style>
<div id="app" v-cloak>
<div class="family-mine-page">
<van-nav-bar title="亲子活动-我的报名" left-text="返回" left-arrow @click-left="historyBack" fixed placeholder></van-nav-bar>
<div class="family-mine-content">
<section class="family-mine-hero">
<img class="family-mine-hero__image" src="/assets/mobile/img/activity/hero/family-hero-v3.png" alt="亲子活动插画">
<div class="family-mine-hero__copy">
<div class="family-mine-hero__title">我的报名</div>
<div class="family-mine-hero__subtitle">亲子活动 · 报名记录</div>
</div>
</section>
<van-sticky :offset-top="46">
<div class="family-mine-sticky">
<van-search v-model="pageForm.searchKeyword" class="family-mine-search" placeholder="搜索已报名活动" shape="round" clearable @search="doSearch" @clear="doSearch"></van-search>
<van-dropdown-menu class="family-mine-filter" :class="{'is-open': filterOpened}" active-color="#1989fa">
<year-van-dropdown-item :num="5" @change="doSearch" @open="filterOpened = true" @close="filterOpened = false" v-model="pageForm.year"></year-van-dropdown-item>
<van-dropdown-item v-model="pageForm.activityType" :options="typeOptions" :multiple="false" @change="doSearch" @open="filterOpened = true" @close="filterOpened = false"></van-dropdown-item>
</van-dropdown-menu>
</div>
</van-sticky>
<van-pull-refresh v-model="refreshing" @refresh="onRefresh">
<van-list v-model="loading" :finished="finished" :immediate-check="false" @load="onLoad">
<div class="family-mine-list">
<div v-for="row in tableData" :key="row.id" class="family-mine-card">
<img v-if="hasCover(row) && !imageLoadErrors[row.id]" class="family-mine-card__cover" :src="row.cover" :alt="row.activityName" @error="handleImageError(row)">
<div v-else class="family-mine-card__cover-state" :class="{'is-error': hasCover(row)}">
<van-icon name="photo-o"></van-icon>
<span>{{ hasCover(row) ? '图片加载失败' : '暂无图片' }}</span>
</div>
<div class="family-mine-card__body">
<div class="family-mine-card__title">{{ row.activityName }}</div>
<div class="family-mine-card__time" v-if="row.activitySignUpStartTime && row.activitySignUpEndTime">
报名时间:{{ $moment(row.activitySignUpStartTime).format('MM/DD HH:mm') }}~{{ $moment(row.activitySignUpEndTime).format('MM/DD HH:mm') }}
</div>
<div class="family-mine-card__time" v-if="row.activityStartTime && row.activityEndTime">
活动时间:{{ $moment(row.activityStartTime).format('MM/DD HH:mm') }}~{{ $moment(row.activityEndTime).format('MM/DD HH:mm') }}
</div>
<div class="family-mine-card__meta" v-if="row.activityGroupName || row.trainType">
<span class="family-mine-card__tag" v-if="row.activityGroupName"><span class="family-mine-card__tag-label">面向对象</span>{{ row.activityGroupName }}</span>
<span class="family-mine-card__tag" v-if="row.trainType"><span class="family-mine-card__tag-label">活动类型</span>{{ row.trainType }}</span>
</div>
</div>
<div class="family-mine-card__actions">
<button type="button" class="family-mine-card__action" @click="onView(row)"><van-icon name="eye-o"></van-icon>查看介绍</button>
<button type="button" class="family-mine-card__action is-primary" @click="onApply(row)"><van-icon name="edit"></van-icon>查看报名</button>
</div>
</div>
</div>
</van-list>
<div v-if="!loading && tableData.length === 0" class="family-mine-empty">
<img src="/assets/mobile/img/activity/family-empty.png" alt="亲子活动报名空状态插画">
<div class="family-mine-empty__title">暂无报名记录</div>
<div class="family-mine-empty__hint">当前筛选条件下暂时没有已报名活动</div>
</div>
</van-pull-refresh>
</div>
</div>
<van-popup :style="{height: '100%', width: '100%'}" position="right" v-model="infoVisible">
<div class="family-mine-intro-page">
<van-nav-bar title="活动介绍" left-text="返回" left-arrow @click-left="infoVisible = false"></van-nav-bar>
<div class="family-mine-intro-scroll">
<div class="family-mine-intro-hero">
<div class="family-mine-intro-hero__icon"><van-icon name="description"></van-icon></div>
<div>
<div class="family-mine-intro-hero__title">亲子活动报名须知</div>
<div class="family-mine-intro-hero__subtitle">请仔细阅读活动安排与报名要求</div>
</div>
</div>
<div v-if="infoRow.introduce" class="family-mine-intro-card" v-html="infoRow.introduce"></div>
<div v-else class="family-mine-intro-card">暂无活动介绍</div>
</div>
</div>
</van-popup>
</div>
<script nonce="${cspNonce!}">
const vue = new Vue({
el: "#app",
store,
data() {
return {
tableData: [],
loading: false,
finished: false,
refreshing: false,
imageLoadErrors: {},
filterOpened: false,
pageForm: {
pageNumber: 1,
pageSize: 10,
totalCount: 0,
searchKeyword: '',
year: new Date().getFullYear(),
activityType: 1,
dataType: 'mine'
},
typeOptions: [
{text: '全部', value: 1},
{text: '报名中', value: 2},
{text: '已结束', value: 3},
],
infoVisible: false,
infoRow: {},
}
},
methods: {
hasCover(row) {
return !!(row && row.cover && String(row.cover).trim())
},
handleImageError(row) {
this.$set(this.imageLoadErrors, row.id, true)
},
onView(row) {
this.infoRow = row
this.infoVisible = true
},
onApply(row) {
this.$pjaxReplace('/platform/family/apply/list/h5?id=' + row.id + '&dataType=mine')
},
onLoad() {
if(!this.finished) {
this.loadActivities(false)
}
},
onRefresh() {
this.loadActivities(true)
},
loadActivities(reset) {
if(reset) {
this.pageForm.pageNumber = 1
this.pageForm.totalCount = 0
this.tableData = []
this.finished = false
this.imageLoadErrors = {}
}
this.loading = true
this.$axios.post('/platform/family/apply/activityPageData', this.pageForm).then((res) => {
if(res.code === 0) {
const list = res.data && res.data.list ? res.data.list : []
this.tableData = this.pageForm.pageNumber === 1 ? list : this.tableData.concat(list)
this.pageForm.totalCount = res.data && res.data.totalCount ? res.data.totalCount : 0
this.finished = this.tableData.length >= this.pageForm.totalCount
if(!this.finished) {
this.pageForm.pageNumber++
}
}
}).finally(() => {
this.loading = false
this.refreshing = false
})
},
doSearch() {
this.loadActivities(true)
}
},
created() {
this.loadActivities(true)
}
})
</script>
<!--#
}
#-->
@@ -0,0 +1,165 @@
<!--#
layout("/layouts/platform_h5.html"){
#-->
<style scoped>
.info-title {
font-weight: bold;
background-color: white;
padding: 13px;
box-shadow: 0 8px 12px #ebedf0;
text-align: center !important;
}
.info-container {
margin-bottom: 60px;
}
.button {
position: fixed;
bottom: 0;
width: 100%;
}
.info-img {
display: block;
}
.van-count-down {
color: #fff;
}
</style>
<div id="app">
<van-nav-bar title="品牌活动" left-text="返回" left-arrow @click-left="historyBack" fixed placeholder></van-nav-bar>
<van-sticky offset-top="46px">
<van-dropdown-menu :close-on-click-outside="false" :close-on-click-overlay="false">
<year-van-dropdown-item :num="5" @change="doSearch" v-model="pageForm.year"></year-van-dropdown-item>
<van-dropdown-item v-model="pageForm.activityType" :options="typeOptions" :multiple="false"
@change="doSearch"></van-dropdown-item>
</van-dropdown-menu>
</van-sticky>
<table-list api="/platform/fellowship/apply/activityPageData"
:page_form.sync="pageForm"
ref="tableListRef"
title="activityName"
img="cover"
@ready="onReady"
>
<template v-slot="{index,row}">
<table-column label="面向对象">{{row.activityGroupName}}</table-column>
<table-column label="报名时间">
{{$moment(row.activitySignUpStartTime).format('MM/DD HH:mm') + '~' + $moment(row.activitySignUpEndTime).format('MM/DD HH:mm')}}
</table-column>
<table-column label="活动时间">
{{$moment(row.activityStartTime).format('MM/DD HH:mm') + '~' + $moment(row.activityEndTime).format('MM/DD HH:mm')}}
</table-column>
</template>
<template #actions="{index,row}">
<template v-if="$moment().isBefore($moment(row.activitySignUpEndTime))">
<div class="action-btn" @click="onView(row)">
<i class="fa fa-eye"></i>
<span>查看介绍</span>
</div>
<div class="action-btn" @click="onApply(row)">
<i class="fa fa-edit"></i>
<span>去报名</span>
</div>
</template>
</template>
</table-list>
<van-action-sheet :close-on-click-overlay="false" title="详细信息" v-model="infoVisible">
<div class="info-container">
<div>
<van-image class="info-img" height="186" width="100%" :src="infoRow.cover"></van-image>
<div class="info-title">{{infoRow.activityName}}</div>
<pdf-preview :content="infoRow.introduce"></pdf-preview>
</div>
<div class="button">
<van-button @click="onApply(infoRow)" type="primary" block>
<span v-if="time >= 0">
去报名
</span>
<template v-else>
距离开始
<van-count-down :time="Math.abs(time)" class="count-down" format="DD 天 HH 时 mm 分 ss 秒" @finish="time = 0"></van-count-down>
</template>
</van-button>
</div>
</div>
</van-action-sheet>
</div>
<script nonce="${cspNonce!}">
const vue = new Vue({
el: "#app",
store,
components: {
"pdf-preview": httpVueLoader("/components/plugins/sysFilePreview/PdfIndex.vue?v=" + new Date().getTime())
},
data() {
return {
time: 0,
pageForm: {
pageNumber: 1,
pageSize: 10,
totalCount: 0,
searchKeyword: '',
year: new Date().getFullYear(),
activityType: 2,
},
typeOptions: [
{text: '全部', value: 1},
{text: '即将开始 & 报名中', value: 2},
{text: '已结束', value: 3},
],
infoVisible: false,
infoRow: {},
id: GetQueryString('id')
}
},
methods: {
onView(row) {
this.infoRow = row
this.time = this.$moment().diff(this.$moment(row.activitySignUpStartTime), 'milliseconds')
this.infoVisible = true
},
onApply(row) {
if(this.$moment().isBefore(this.$moment(row.activitySignUpStartTime))) {
this.onView(row)
return
}
this.$pjaxReplace('/platform/fellowship/apply/list/h5?id=' + row.id)
},
fetchOne() {
this.$axios.post('/platform/fellowship/manage/findOne', {id: this.id}).then((res) => {
if(res.code === 0) {
this.onView(res.data)
}
})
},
onReady() {
this.doSearch()
if(this.id) {
this.fetchOne()
}
},
doSearch() {
this.$nextTick(() => {
this.pageForm.pageNumber = 1
this.pageForm.totalCount = 0
this.$refs.tableListRef.doSearch()
})
}
},
mounted() {
}
})
</script>
<!--#
}
#-->
@@ -0,0 +1,89 @@
const times = {
template:
/*language=HTML*/
`
<div>
<van-action-sheet v-model="visible" title="时间段" cancel-text="取消">
<button v-for="(item,index) in row?.courseTimes" type="button" class="van-action-sheet__item">
<div style="display: flex; justify-content: space-between; align-items: center">
<div>
<div class="van-action-sheet__name">
<label>{{ weekdayCNMap[$moment(item.courseDate).day()] }}</label>
<label>{{ $moment(item.courseDate).format('YYYY-MM-DD') }}</label>
</div>
<div class="van-action-sheet__subname">
{{$moment(item.courseStartTime).format('MM-DD HH:mm') + ' 至 ' + $moment(item.courseEndTime).format('MM-DD HH:mm')}}
</div>
</div>
<div v-if="row.isSign === true && row.isMobileSign === true" class="sign_button">
<van-button v-if="item.isAttend !== true" @click.stop="onSign(item)" size="mini" type="info">签到</van-button>
<van-button v-if="item.isAttend === true" size="mini" type="info" disabled>已签到</van-button>
</div>
</div>
</button>
</van-action-sheet>
<van-popup round :safe-area-inset-bottom="true"
:close-on-click-overlay="false"
v-model="signVisible"
:style="{ width: '80%', height: '66%' }"
get-container="#app"
@close="onSignClose"
closeable
>
<scan-code ref="scanCodeRef"></scan-code>
</van-popup>
</div>
`,
store,
data() {
return {
visible:false,
row: null,
weekdayCNMap: {0: '周日', 1: '周一', 2: '周二', 3: '周三', 4: '周四', 5: '周五', 6: '周六'},
selectCourseTime: {},
signVisible: false,
}
},
components: {
"scan-code": httpVueLoader("/components/plugins/sysScanCode/index.vue?v=" + new Date().getTime())
},
methods: {
onSignClose() {
this.$refs.scanCodeRef.closeScan()
},
onOpen(row) {
this.row = row
this.visible = true
},
onSign(courseTime) {
this.selectCourseTime = courseTime
if(this.row.signType === 1) {
this.signVisible = true
this.$nextTick(() => {
this.$refs.scanCodeRef.init()
})
}
if(this.row.signType === 2) {
this.makeCode()
}
if(this.row.signType === 3) {
this.$toast('此签到模式正在升级中')
}
},
makeCode() {
const url = '/platform/fellowship/mine/passiveScan'
const data = url + '?id=' + this.selectCourseTime.id
console.log(data)
const content = jrQrcode.getQrBase64(data)
vant.ImagePreview([content])
},
onClose(){
this.visible = false
},
},
style: /*language=CSS*/ `
`
}
@@ -0,0 +1,159 @@
const applyForm = {
template:
/*language=HTML*/
`
<div>
<van-action-sheet :close-on-click-overlay="false" title="报名信息" v-model="visible">
<van-form ref="formRef" class="form-container">
<van-cell-group title="活动信息" class="form-section">
<van-field label="活动名称" readonly v-model="row.courseName"></van-field>
<van-field label="校区" readonly v-model="row.campus"></van-field>
<van-field label="活动地点" readonly v-model="row.courseLocation"></van-field>
</van-cell-group>
<van-cell-group title="基础信息" class="form-section">
<van-field label="姓名" readonly v-model="formData.username"></van-field>
<van-field label="工号" readonly v-model="formData.loginname"></van-field>
<van-field label="所属单位" readonly v-model="formData.unitName"></van-field>
<van-field label="所属工会" readonly v-model="formData.unionName"></van-field>
<van-field label="性别" readonly v-model="formData.sex"></van-field>
<template v-if="row.courseIsLimitApply">
<van-field label="报名时段"
required
:rules="[{ required: true, message: '请选择报名时段' }]"
readonly
@click="showCoursePicker = true"
placeholder="请选择报名时段"
name="courseTimeName"
v-model="formData.courseTimeName">
</van-field>
<van-popup v-model="showCoursePicker" position="bottom">
<van-picker
show-toolbar
:columns="courseTimeSelectList"
@confirm="onCourseConfirm"
@cancel="showCoursePicker=false"
></van-picker>
</van-popup>
</template>
<train-dynamic-form v-model="dynamicColumnsData" ref="dynamicForm"></train-dynamic-form>
</van-cell-group>
<div class="button">
<van-button @click="onSubmit" round type="info" block>提交</van-button>
</div>
</van-form>
</van-action-sheet>
</div>
`,
data() {
return {
row: {},
activity: {},
dynamicColumnsData: [],
visible: false,
formData: {},
showCoursePicker: false,
courseTimeSelectList: [],
}
},
components: {
"train-dynamic-form": httpVueLoader("/components/module/trainSignUp/TrainDynamicForm.vue")
},
methods: {
async onOpen(row, courseType) {
this.row = row
this.init(row, courseType)
if(row.courseIsLimitApply) {
await this.getCourseTimeSelectList(row)
}
this.visible = true
},
init(row, courseType) {
this.$set(this.formData, 'username', this.$store.state.user.username)
this.$set(this.formData, 'loginname', this.$store.state.user.loginname)
this.$set(this.formData, 'unionName', this.$store.state.user.union.name)
this.$set(this.formData, 'unitName', this.$store.state.user.unit.name)
this.$set(this.formData, 'sex', this.$store.state.user.sex)
this.$set(this.formData, 'activityId', row.activityId)
this.$set(this.formData, 'courseId', row.id)
this.dynamicColumnsData = courseType ? courseType.trainMobileSignColumnList : []
this.dynamicColumnsData.forEach((item) => {
item.columnValue = this.$store.state.user[item.columnCode] || ""
})
},
onCourseConfirm(val){
this.formData.activityCourseId = val.value
this.$set(this.formData, "activityCourseId", val.value)
this.$set(this.formData, "courseTimeName", val.text.substring(0, 12))
this.showCoursePicker = false
},
async getCourseTimeSelectList(o) {
const resp = await this.$axios.post('/platform/trainSignUp/apply/getCourseTimeSelectList',{courseId: o.id})
if (resp.code === 0) {
this.courseTimeSelectList = resp.data
} else {
this.$toast.fail("获取时段信息失败,请联系管理员")
}
},
async validateSignUp() {
// 验证自定义表单
await this.$refs.dynamicForm.$refs.form.validate()
// 获取家属人数
let familyCount = this.dynamicColumnsData.filter(o => o.columnCode === 'xdqsrs').reduce((sum, item) => {
return sum + (Number(item.columnValue) || 0)
}, 0)
const res = await this.$axios.post("/platform/trainSignUp/apply/validateSignUp", {
courseId: this.formData.courseId,
currentFamilyNumber: familyCount
})
return res.code === 0
},
async validateCourseTime() {
const res = await this.$axios.post('/platform/trainSignUp/apply/validateSourceSignUp', {
activityCourseId: this.formData.activityCourseId,
courseId: this.row.id
})
return res.code === 0
},
async onSubmit() {
if(!await this.validateSignUp()) return
this.$refs.formRef.validate().then(() => {
this.$dialog.confirm({
title: "提示",
message: "您确定要提交吗?"
}).then(async () => {
if (this.row.courseIsLimitApply) {
if(!await this.validateCourseTime()) return
}
const mobileColumnsValue = this.dynamicColumnsData.map((v) => {
return {
columnName: v.columnName,
columnValue: v.columnValue,
columnCode: v.columnCode,
columnFormType: v.columnFormType
}
})
this.formData.mobileColumnsValue = JSON.stringify(mobileColumnsValue)
this.$axios.post("/platform/trainSignUp/apply/doSignUp", this.formData).then(res => {
if (res.code === 0) {
this.$toast(res.msg)
this.visible = false
this.$emit('refresh')
}
})
})
})
},
},
style: /*language=CSS*/ `
/deep/ .button {
position: fixed;
bottom: 0;
width: 100%;
}
`
}
@@ -0,0 +1,255 @@
<!--#
layout("/layouts/platform_h5.html"){
#-->
<style scoped>
.primary-color {
color: var(--color-primary);
}
.sign_button .van-button{
width: 66px;
height: 30px;
font-size: 14px;
border-radius: 6px;
}
</style>
<div id="app">
<van-nav-bar title="活动报名" left-text="返回" left-arrow @click-left="historyBack" fixed placeholder></van-nav-bar>
<van-sticky offset-top="46px">
<van-dropdown-menu :close-on-click-outside="false" :close-on-click-overlay="false">
<van-dropdown-item v-model="pageForm.courseTypeId" :options="typeOptions" :multiple="false"
@change="doSearch"></van-dropdown-item>
</van-dropdown-menu>
</van-sticky>
<van-tabs v-if="assortList.length > 0" type="card" color="#1867B0"
style="margin-top: 10px" @click="tabClick">
<van-tab v-for="item in assortList" :name="item" :title="item">
</van-tab>
</van-tabs>
<table-list api="/platform/trainSignUp/apply/pageData"
:page_form.sync="pageForm"
ref="tableListRef"
@ready="onReady"
>
<template #header="{index,row}">
<div class="item-header">
<div class="item-title">{{ row.courseName }}</div>
<div v-html="calcSignUpCount(row)"></div>
</div>
</template>
<template v-slot="{index,row}">
<table-column label="类型">{{row.typeName}}</table-column>
<table-column label="校区">{{row.campus}}</table-column>
<table-column label="地点">{{row.courseLocation}}</table-column>
<table-column label="联系人">{{row.courseInstructor}}</table-column>
<table-column label="时间">
<span v-if="row.courseTimes && row.courseTimes.length === 1" class="primary-color" @click="onTime(row)">
{{ weekdayCNMap[$moment(row.courseTimes[0].courseDate).day()]
+ ' '
+ $moment(row.courseTimes[0].courseStartTime).format('MM月DD日 HH:mm')
+ '~'
+ $moment(row.courseTimes[0].courseEndTime).format('HH:mm') }}
</span>
<span v-else @click="onTime(row)" class="primary-color">点我查看</span>
</table-column>
<table-column v-if="row.introduce" label="详细信息">
<span @click="introduceRow = row; introduceVisible = true" class="primary-color">点我查看</span>
</table-column>
</template>
<template #actions="{index,row}">
<div v-if="activity.wechat && row.isSign" class="action-btn" @click="this.vant.ImagePreview([activity.wechat])">
<i class="fa fa-wechat"></i>
<span>微信群二维码</span>
</div>
<template v-if="$moment().isBefore($moment(activity.activitySignUpEndTime))">
<div v-if="row.isSign === false" class="action-btn" @click="onApply(row)">
<i class="fa fa-sign-in"></i>
<span>我要报名</span>
</div>
<div v-if="row.isSign === true" class="action-btn delete" @click="onCancel(row)">
<i class="fa fa-trash"></i>
<span>取消报名</span>
</div>
</template>
<div v-if="row.isSign === true && row.isMobileSign === true && $moment().isAfter($moment(activity.activitySignUpEndTime))"
class="action-btn"
@click="onTime(row)"
>
<i class="fa fa-sign-in"></i>
<span>签到</span>
</div>
</template>
</table-list>
<van-action-sheet :close-on-click-overlay="false" title="详细信息" v-model="introduceVisible" cancel-text="取消">
<pdf-preview :content="introduceRow.introduce"></pdf-preview>
</van-action-sheet>
<times ref="timesRef"></times>
<apply-form ref="formRef" @refresh="refresh"></apply-form>
</div>
<script src="${base!}/assets/platform/plugins/jr-qrcode/jr-qrcode.js"></script>
<script nonce="${cspNonce!}">
<!--#include('../common/times.js'){}#-->
<!--#include('applyForm.js'){}#-->
const vue = new Vue({
el: "#app",
store,
components: {
'times': times,
'apply-form': applyForm,
"pdf-preview": httpVueLoader("/components/plugins/sysFilePreview/PdfIndex.vue?v=" + new Date().getTime())
},
data() {
return {
pageForm: {
pageNumber: 1,
pageSize: 10,
totalCount: 0,
searchKeyword: '',
year: new Date().getFullYear(),
courseTypeId: null,
activityId: GetQueryString('id'),
dataType: GetQueryString('dataType'),
assortTypes: [],
},
typeOptions: [],
assortOptions: [],
sourceTypeOptions: [],
introduceVisible: false,
introduceRow: {},
activity: {},
assortList: [],
weekdayCNMap: {0: '周日', 1: '周一', 2: '周二', 3: '周三', 4: '周四', 5: '周五', 6: '周六'},
}
},
methods: {
tabClick(name) {
this.pageForm.assortTypes = []
this.pageForm.assortTypes.push(name)
this.pageForm.assortTypes = JSON.stringify(this.pageForm.assortTypes)
this.doSearch()
},
refresh() {
this.doSearch()
},
async onTime(row) {
// 如果设置签到,并且也报名的话
if(row.isMobileSign === true && row.isSign === true) {
const res = await this.$axios.post('/platform/trainSignUp/mine/queryCourseSign', {
courseId: row.id
})
row.courseTimes = res.data
}
this.$refs.timesRef.onOpen(row)
},
onApply(row) {
const courseType = this.sourceTypeOptions.find((v) => v.id === row.courseType)
this.$axios.post('/platform/trainSignUp/apply/validateSignUp', {
courseId: row.id
})
.then((res) => {
if (res.code !== 0) {
vant.Dialog.alert({
title: '提示',
message: res.msg,
confirmButtonColor: '#1867b0'
})
} else {
let lave = row.coursePeopleNumber - (row.hasRegisterNum + row.courseReservedNumber)
if(row.reserveMode === 2 && lave <= 0) {
vant.Dialog.alert({
title: '提示',
message: '您当前的报名为候补报名状态',
confirmButtonColor: '#1867b0'
})
}
this.$refs.formRef.onOpen(row, courseType)
}
})
},
onCancel(row) {
vant.Dialog.confirm({
title: '温馨提示',
message: "您确定要<span style='color: red'>取消【" + row.courseName + "】</span>吗?",
confirmButtonColor: '#1867b0',
}).then(async () => {
const resp = await this.$axios.post('/platform/trainSignUp/apply/cancelSignUp', {
activityId: row.activityId,
courseId: row.id
})
this.$toast(resp.msg)
if (resp.code === 0) {
this.doSearch()
}
}).catch(() => {})
},
calcSignUpCount(row) {
if(!row.coursePeopleNumber || row.coursePeopleNumber === 0) {
return "名额数不限制"
}
let lave = row.coursePeopleNumber - (row.hasRegisterNum + row.courseReservedNumber)
if(row.reserveMode === 2) {
let lave2 = row.waitingNum - row.hasWaitingNum
return "<span style='color: red'>余" + lave +"</span>/" + row.coursePeopleNumber + "人"
+ ",<span style='color: red'>候补余" + lave2 + "</span>/" + row.waitingNum + "人"
} else {
return "<span style='color: red'>余" + lave +"</span>/" + row.coursePeopleNumber + "人"
}
},
async onReady() {
this.queryCourseAssort()
const typeList = await this.getCourseTypeList()
this.sourceTypeOptions = clone(typeList)
this.typeOptions = [
{
text: "全部类型",
value: null
}
].concat(typeList.map((v) => ({ text: v.typeName, value: v.id })))
if (this.typeOptions.length > 0) {
this.pageForm.type = this.typeOptions[0].value
this.doSearch()
}
this.fetchActivity()
},
fetchActivity() {
this.$axios.post('/platform/trainSignUp/manage/findOne', {id: this.pageForm.activityId})
.then((res) => {
this.activity = res.data
})
},
async getCourseTypeList() {
const resp = await this.$axios.post("/platform/trainSignUp/type/getAllType")
return resp.data
},
queryCourseAssort() {
this.$axios.post("/platform/trainSignUp/apply/queryCourseAssort", {activityId: this.pageForm.activityId})
.then((resp) => {
this.assortList = resp.data
if(this.assortList.length > 0) {
this.pageForm.assortTypes = JSON.stringify([this.assortList[0]])
}
})
},
doSearch() {
this.$nextTick(() => {
this.pageForm.pageNumber = 1
this.pageForm.totalCount = 0
this.$refs.tableListRef.doSearch()
})
}
},
})
</script>
<!--#
}
#-->
@@ -0,0 +1,132 @@
<!--#
layout("/layouts/platform_h5.html"){
#-->
<style scoped>
.info-title {
font-weight: bold;
background-color: white;
padding: 13px;
box-shadow: 0 8px 12px #ebedf0;
text-align: center !important;
}
.info-container {
}
.button {
position: fixed;
bottom: 0;
width: 100%;
}
.info-img {
display: block;
}
.van-count-down {
color: #fff;
}
</style>
<div id="app">
<van-nav-bar title="品牌活动-我的报名" left-text="返回" left-arrow @click-left="historyBack" fixed placeholder></van-nav-bar>
<van-sticky offset-top="46px">
<van-dropdown-menu :close-on-click-outside="false" :close-on-click-overlay="false">
<year-van-dropdown-item :num="5" @change="doSearch" v-model="pageForm.year"></year-van-dropdown-item>
<van-dropdown-item v-model="pageForm.activityType" :options="typeOptions" :multiple="false"
@change="doSearch"></van-dropdown-item>
</van-dropdown-menu>
</van-sticky>
<table-list api="/platform/trainSignUp/apply/activityPageData"
:page_form.sync="pageForm"
ref="tableListRef"
title="activityName"
img="cover"
@ready="onReady"
>
<template v-slot="{index,row}">
<table-column label="面向对象">{{row.activityGroupName}}</table-column>
<table-column label="报名时间">
{{$moment(row.activitySignUpStartTime).format('MM/DD HH:mm') + '~' + $moment(row.activitySignUpEndTime).format('MM/DD HH:mm')}}
</table-column>
<table-column label="活动时间">
{{$moment(row.activityStartTime).format('MM/DD HH:mm') + '~' + $moment(row.activityEndTime).format('MM/DD HH:mm')}}
</table-column>
</template>
<template #actions="{index,row}">
<div class="action-btn" @click="onView(row)">
<i class="fa fa-eye"></i>
<span>查看介绍</span>
</div>
<div class="action-btn" @click="onApply(row)">
<i class="fa fa-edit"></i>
<span>查看</span>
</div>
</template>
</table-list>
<van-action-sheet :close-on-click-overlay="false" title="活动详细信息" v-model="infoVisible" cancel-text="取消">
<div class="info-container">
<div>
<van-image class="info-img" height="186" width="100%" :src="infoRow.cover"></van-image>
<div class="info-title">{{infoRow.activityName}}</div>
<pdf-preview :content="infoRow.introduce"></pdf-preview>
</div>
</div>
</van-action-sheet>
</div>
<script nonce="${cspNonce!}">
const vue = new Vue({
el: "#app",
store,
components: {
"pdf-preview": httpVueLoader("/components/plugins/sysFilePreview/PdfIndex.vue?v=" + new Date().getTime())
},
data() {
return {
pageForm: {
pageNumber: 1,
pageSize: 10,
totalCount: 0,
searchKeyword: '',
year: new Date().getFullYear(),
activityType: 1,
dataType: 'mine'
},
typeOptions: [
{text: '全部', value: 1},
{text: '报名中', value: 2},
{text: '已结束', value: 3},
],
infoVisible: false,
infoRow: {},
}
},
methods: {
onView(row) {
this.infoRow = row
this.infoVisible = true
},
onApply(row) {
this.$pjaxReplace('/platform/trainSignUp/apply/list/h5?id=' + row.id + '&dataType=mine')
},
onReady() {
this.doSearch()
},
doSearch() {
this.$nextTick(() => {
this.pageForm.pageNumber = 1
this.pageForm.totalCount = 0
this.$refs.tableListRef.doSearch()
})
}
},
})
</script>
<!--#
}
#-->
@@ -0,0 +1,131 @@
<!--#
layout("/layouts/platform_h5.html"){
#-->
<div id="app" v-cloak>
<van-nav-bar title="列表" fixed placeholder left-text="返回" left-arrow @click-left="historyBack"></van-nav-bar>
<van-sticky offset-top="46px">
<van-tabs v-model="pageForm.category" @change="onRefresh">
<van-tab v-for="item in dict.type.ACTIVITY_QSV_CATEGORY" :name="item.code" :title="item.label" :key="item.code"></van-tab>
</van-tabs>
</van-sticky>
<van-pull-refresh v-model="refreshing" @refresh="onRefresh" style="min-height: calc(100vh - 90px)">
<van-list v-if="list && list.length>0" v-model="loading" :finished="finished" finished-text="" @load="onLoad">
<div v-for="row in list" :key="row.id">
<div
style="display: flex; padding: 15px; margin: 20px; box-sizing: border-box; background: #ffffff; border-radius: 10px"
@click="itemClick(row)"
>
<div class="list-item-icon" style="width: 96px; height: 80px; flex-shrink: 0">
<img src="/assets/mobile/svg/qsv/icon.svg" alt="" style="width: 100%; height: 100%" />
</div>
<div style="flex-grow: 1; display: flex; flex-direction: column; justify-content: space-around">
<div
style="
font-weight: bolder;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 1;
overflow: hidden;
text-overflow: ellipsis;
"
>
{{row.title}}
</div>
<div style="font-size: 13px; color: #606266">
开始时间:{{row.startTime}}
<br />
结束时间:{{row.endTime}}
</div>
</div>
</div>
</div>
</van-list>
<van-empty v-if="list.length===0" description="没有更多了"></van-empty>
</van-pull-refresh>
</div>
<script nonce="${cspNonce!}">
new Vue({
el: "#app",
dicts: ["ACTIVITY_QSV_CATEGORY"],
data() {
return {
loading: false,
finished: false,
refreshing: false,
list: [],
pageForm: {
pageNumber: 1,
pageSize: 10,
category: "QUIZ"
},
categoryPaths: {
QUIZ: "/platform/h5/qsv/quiz",
SURVEY: "/platform/h5/qsv/survey",
VOTE: "/platform/h5/qsv/vote"
}
}
},
methods: {
onLoad() {
this.loading = true
const loading = createListLoading()
this.$axios
.post("/platform/h5/qsv/pageData", this.pageForm)
.then((res) => {
if (res.code === 0) {
this.list = this.list.concat(res.data.list)
this.pageForm.totalCount = res.data.totalCount
if (this.list.length >= this.pageForm.totalCount) {
this.finished = true
}
this.pageForm.pageNumber++
}
})
.finally(() => {
loading.close()
this.loading = false
this.refreshing = false
})
},
onRefresh() {
this.pageForm.pageNumber = 1
this.list = []
this.finished = false
this.onLoad()
},
async itemClick(item) {
if (item.groupId) {
const { code, data } = await this.$axios.post("/open/common/checkGroupPermission", { groupId: item.groupId })
if (code === 0) {
if (!data) {
this.$toast.fail("您没有权限参与")
return
}
}
}
const { startTime, endTime } = item
if (this.$moment(startTime).unix() > this.$moment().unix()) {
this.$toast("未开始")
return
}
const path = this.categoryPaths[item.category]
if (path) {
this.$pjaxReplace(path + "?id=" + item.id)
}
}
},
created() {
this.onLoad()
}
})
</script>
<!--#
}
#-->
@@ -0,0 +1,370 @@
<!--#
layout("/layouts/platform_h5.html"){
#-->
<style>
.container {
padding: 0;
}
.container .title {
text-align: center;
padding: 20px;
background: #ffffff;
/*margin: 10px 0;*/
margin-bottom: 10px;
color: var(--color-primary);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.container .card {
padding: 10px;
background: #ffffff;
}
.subject {
background: #fff;
padding: 15px;
margin-bottom: 15px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
position: relative;
}
.subject p {
font-size: 16px;
margin: 20px 0 10px 0;
font-weight: bold;
}
.subject .tag {
position: absolute;
top: 0;
}
.timer {
text-align: center;
font-size: 18px;
background: #fff;
padding: 10px 0;
}
.submit-button {
width: 100%;
margin-top: 20px;
}
.result {
text-align: center;
font-size: 20px;
margin-top: 20px;
}
.button-control {
position: fixed;
bottom: 0;
left: 0;
right: 0;
padding: 20px;
display: flex;
column-gap: 20px;
background: #fff;
}
</style>
<div id="app" v-cloak>
<van-nav-bar title="问卷调查" left-arrow @click-left="historyBack" placeholder fixed></van-nav-bar>
<div class="container" :style="{'padding-bottom': answerRecord.isFinish ? 0 : '84px'}">
<van-sticky offset-top="46px">
<!--开启计时 未完成 活动未结束-->
<div class="timer" v-if="activity.timeLimit > 0 && !answerRecord.isFinish && !isEnd">⏰{{ remainingTime }}s</div>
<h2 class="title">{{ activity.title }}</h2>
</van-sticky>
<div v-if="!answerRecord.isFinish">
<div v-for="(subject, index) in subjects" :key="index" class="subject">
<p>{{index+1}}、{{ subject.title }}</p>
<div class="tag">
<van-tag v-if="subject.type === 'checkbox'" type="primary" size="large">多选</van-tag>
<van-tag v-if="subject.type === 'radio'" type="primary" size="large">单选</van-tag>
<van-tag v-if="subject.score" type="primary" size="large">{{subject.score}}分</van-tag>
</div>
<van-checkbox-group v-model="subject.userSelectOptionIds" :ref="'subject'+subject.id">
<van-cell-group>
<van-cell
v-for="(option,index) in subject.options"
clickable
:key="option.id"
:title="option.text"
@click="cellToggle(subject,option.id)"
>
<template #icon>
<van-checkbox
:name="option.id"
:ref="'option'+option.id"
:disabled="answerRecord.isFinish"
:shape="subject.type==='checkbox' ? 'square' : 'round'"
style="margin-right: 10px"
></van-checkbox>
</template>
</van-cell>
</van-cell-group>
</van-checkbox-group>
</div>
<div class="button-control" v-if="!answerRecord.isFinish">
<van-button type="primary" @click="onSubmit" block :disabled="isEnd">{{isEnd ? '已结束' : '提交'}}</van-button>
</div>
</div>
</div>
</div>
<script nonce="${cspNonce!}">
const vue = new Vue({
el: "#app",
data() {
return {
list: [],
id: GetQueryString("id"),
subjects: [],
activity: {},
// remainingTime: 0,
isFinished: false,
answerRecord: {},
answerRecordId: null,
//历史记录
historyScores: [],
//答题用时
answerTime: 0
}
},
computed: {
isEnd() {
if (this.activity) {
return this.$moment().unix() > this.$moment(this.activity.endTime).unix()
}
return true
},
remainingTime() {
if (this.activity && this.activity.timeLimit > 0) {
return this.activity.timeLimit * 60 - this.answerTime
}
return 0
}
},
methods: {
//获取活动、题目
listSubjects() {
this.$axios.post("/platform/h5/qsv/quiz/subjects", { activityId: this.id }).then((res) => {
if (res.code === 0) {
this.activity = res.data.activity
this.subjects = res.data.subjects
this.answerRecordId = res.data.answerRecordId
this.checkGroupPermission()
this.getAnswerRecord()
}
})
},
//获取回答记录
getAnswerRecord() {
this.$axios.post("/platform/h5/qsv/quiz/answerRecord", { answerRecordId: this.answerRecordId }).then((res) => {
if (res.code === 0) {
this.answerRecord = res.data
if (this.answerRecord.isFinish || this.$moment().unix() > this.$moment(this.activity.endTime)) {
this.$pjaxReplace("/platform/h5/qsv/quiz/result?answerRecordId=" + this.answerRecordId)
}
this.initAnswer()
this.checkTimer()
}
})
},
//答案回显
initAnswer() {
this.subjects.forEach((subject, subjectIndex) => {
if (subject.type === "radio") {
this.$refs["subject" + subject.id][0].toggleAll(false)
}
if (this.answerRecord.optionIds) {
this.answerRecord.optionIds[subjectIndex].forEach((optionId) => {
this.$refs["option" + optionId][0].toggle()
})
}
})
},
//答题计时器
checkTimer() {
if (this.timerInterval) {
clearInterval(this.timerInterval)
}
//开启计时
const startInterval = () => {
this.timerInterval = setInterval(() => {
this.answerTime++
if (this.activity.timeLimit > 0 && this.answerTime >= this.activity.timeLimit * 60) {
clearInterval(this.timerInterval)
const loading = this.$toast.loading({
message: "答题时间到,自动提交中",
forbidClick: true
})
setTimeout(() => {
this.autoSubmit(loading)
}, 1500)
}
}, 1000)
}
if (!this.isEnd && !this.answerRecord.isFinish) {
if (this.activity.timeLimit > 0) {
this.$dialog
.alert({
title: "提示",
message: "本次答题时间" + this.activity.timeLimit + "分钟,点击确认开始答题"
})
.then(() => {
startInterval()
})
} else {
startInterval()
}
}
// if (this.activity.timeLimit > 0 && !this.isEnd) {
// if (!this.answerRecord.isFinish) {
// this.$dialog
// .alert({
// title: "提示",
// message: "本次答题时间" + this.activity.timeLimit + "分钟,点击确认开始答题"
// })
// .then(() => {
// this.remainingTime = this.activity.timeLimit * 60
// this.timerInterval = setInterval(() => {
// if (this.remainingTime > 0) {
// this.remainingTime--
// } else {
// clearInterval(this.timerInterval)
// const loading = this.$toast.loading({
// message: "答题时间到,自动提交中",
// forbidClick: true
// })
// this.autoSubmit(loading)
// }
// }, 1000)
// })
// }
// }
},
historyScore() {
this.$axios.post("/platform/h5/qsv/quiz/historyScore", { activityId: this.activityId }).then((res) => {
if (res.code === 0) {
this.historyScores = res.data
}
})
},
//选项点击
cellToggle(subject, optionId) {
if (this.answerRecord.isFinish) {
return
}
if (subject.type === "radio") {
this.$refs["subject" + subject.id][0].toggleAll(false)
}
this.$refs["option" + optionId][0].toggle()
},
//手动提交
onSubmit() {
if (this.isEnd) {
this.$toast("调查已结束")
return
}
//提示那些题没有作答
for (let i = 0; i < this.subjects.length; i++) {
let subject = this.subjects[i]
//["radio", "checkbox"].includes(subject.type) &&
if (!subject.userSelectOptionIds || subject.userSelectOptionIds.length === 0) {
this.$toast.fail("第" + (i + 1) + "题未做答")
return
}
}
if (this.timerInterval) {
clearInterval(this.timerInterval)
}
this.autoSubmit()
},
//自动提交
autoSubmit(loading = null) {
this.$axios
.post("/platform/h5/qsv/quiz/submitAnswer", {
answer: JSON.stringify({
activityId: this.id,
answerRecordId: this.answerRecordId,
subjects: this.subjects.map((subject) => {
return {
id: subject.id,
userSelectOptionIds: ["checkbox", "radio"].includes(subject.type) ? subject.userSelectOptionIds : []
}
}),
answerTime: this.answerTime
})
})
.then((res) => {
if (res.code === 0) {
this.$toast.success(res.msg)
this.$pjaxReplace("/platform/h5/qsv/quiz/result?answerRecordId=" + this.answerRecordId)
// this.getAnswerRecord()
}
})
.finally(() => {
if (loading) {
loading.clear()
}
})
},
async checkGroupPermission() {
const groupId = this.activity.groupId
if (groupId) {
const { code, data } = await this.$axios.post("/open/common/checkGroupPermission", { groupId })
if (code === 0) {
if (!data) {
this.$dialog
.alert({
title: "提示",
message: "您没有权限参与"
})
.then(() => {
location.back()
})
}
}
}
}
},
created() {
if (this.timerInterval) {
clearInterval(this.timerInterval)
}
if (this.id) {
this.listSubjects()
}
}
})
</script>
<!--#
}
#-->
@@ -0,0 +1,330 @@
<!--#
layout("/layouts/platform_h5.html"){
#-->
<style>
.container {
padding: 0;
}
.container .title {
text-align: center;
padding: 20px;
background: #ffffff;
color: var(--color-primary);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.container .score {
}
.container .card {
padding: 10px;
background: #ffffff;
}
.subject {
background: #fff;
padding: 15px;
margin-bottom: 15px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
position: relative;
}
.subject p {
font-size: 16px;
margin: 20px 0 10px 0;
font-weight: bold;
}
.subject .tag {
position: absolute;
top: 0;
}
.score-form {
background: #ffffff;
display: flex;
margin-bottom: 10px;
border-top: 1px solid #f1f1f1;
align-items: center;
}
.score-form-total {
flex: 1;
background: #fff;
display: flex;
padding: 10px 20px 10px 0;
flex-direction: column;
align-items: flex-end;
}
.score-text-news {
flex: 1;
background: #fff;
}
.score-font-style {
font-size: 32px;
color: #ff6a00;
word-break: keep-all;
line-height: 38px;
min-width: 47px;
text-align: center;
}
.score-underline {
background: url(//image.wjx.cn/images/newimg/score-form/score-underline@2x.png) no-repeat center;
background-size: 47px 16px;
display: inline-block;
height: 16px;
width: 47px;
margin-top: 7px;
}
.van-checkbox__icon--checked .van-icon {
color: #fff !important;
background-color: var(--color-primary) !important;
border-color: var(--color-primary) !important;
}
.van-checkbox__icon--disabled .van-icon {
background-color: #fff;
}
.answer-container {
padding: 16px;
}
.correct-answer {
color: green;
}
.incorrect-answer {
color: red;
}
.button-control {
position: fixed;
bottom: 0;
left: 0;
right: 0;
padding: 20px;
display: flex;
column-gap: 20px;
background: #fff;
}
.history-list {
background: #f1f1f1;
padding: 10px;
}
.history-item {
background: #ffffff;
border-radius: 12px;
padding: 16px;
margin: 20px 10px;
box-shadow:
8px 8px 16px #d9d9d9,
-8px -8px 16px #ffffff;
transition: transform 0.2s ease-in-out;
}
.history-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
}
.history-header span {
font-size: 14px;
font-weight: 600;
color: #333;
}
.history-header .tag {
margin-left: 8px;
color: #fff;
padding: 2px 5px;
}
.history-info {
font-size: 14px;
color: #555;
margin-bottom: 4px;
}
</style>
<div id="app" v-cloak>
<van-nav-bar title="问卷调查" left-arrow @click-left="historyBack" placeholder fixed></van-nav-bar>
<div class="container" :style="{'padding-bottom': answerRecord.isFinish ? 0 : '84px'}">
<van-sticky offset-top="46px">
<h2 class="title">{{ activity.title }}</h2>
<div class="score-form">
<div class="score-form-total">
<div class="score-font-style">{{totalScore}}</div>
<i class="score-underline"></i>
</div>
</div>
</van-sticky>
<div>
<div v-for="(subject, index) in subjects" :key="index" class="subject">
<p>{{index+1}}、{{ subject.title }}</p>
<div class="tag">
<van-tag v-if="subject.type === 'checkbox'" type="primary" size="large">多选</van-tag>
<van-tag v-if="subject.type === 'radio'" type="primary" size="large">单选</van-tag>
<van-tag v-if="subject.score" type="primary" size="large">{{subject.score}}分</van-tag>
</div>
<van-checkbox-group v-model="subject.userSelectOptionIds" :ref="'subject'+subject.id">
<van-cell-group>
<van-cell v-for="(option,index) in subject.options" clickable :key="option.id" :title="option.text">
<template #title>
<span :style="{color : option.isCorrect ? 'green' : ''}">{{option.text}}</span>
</template>
<template #icon>
<van-checkbox
:name="option.id"
:ref="'option'+option.id"
:disabled="answerRecord.isFinish"
style="margin-right: 10px"
></van-checkbox>
</template>
</van-cell>
</van-cell-group>
</van-checkbox-group>
<div class="answer-container">
<div v-if="answerRecord.extJson?.[subject.id]?.isCorrect" class="correct-answer">
<van-icon name="passed"></van-icon>
回答正确
</div>
<div v-else-if="answerRecord.extJson?.[subject.id]" class="incorrect-answer">
<van-icon name="close"></van-icon>
回答错误
</div>
</div>
</div>
<div class="button-control">
<van-button type="primary" @click="openHistory" block>查看全部答题记录</van-button>
</div>
</div>
<van-action-sheet v-model="historyShow" title="历史记录">
<div class="history-list">
<div v-for="item in historyList" class="history-item">
<div class="history-header">
<span>{{ item.date }}</span>
<van-tag v-if="item.isHighestScore" type="danger" class="tag">最高分</van-tag>
<van-tag v-if="item.isLatestScore" type="primary" class="tag">最新得分</van-tag>
</div>
<div class="history-info">答题分数:{{ item.totalScore }}</div>
<div class="history-info">答题用时:{{ item.answerTime | formatSeconds }}</div>
</div>
</div>
</van-action-sheet>
</div>
</div>
<script nonce="${cspNonce!}">
const vue = new Vue({
el: "#app",
data() {
return {
answerRecordId: GetQueryString("answerRecordId"),
answerRecord: {},
subjects: [],
activity: {},
list: [],
historyShow: false,
historyList: []
}
},
computed: {
totalScore() {
if (!this.answerRecord || !this.answerRecord.extJson) {
return 0
}
let totalScore = 0
for (const key in this.answerRecord.extJson) {
if (this.answerRecord.extJson.hasOwnProperty(key)) {
totalScore += this.answerRecord.extJson[key].score || 0
}
}
return totalScore
}
},
filters: {
formatSeconds(seconds) {
if (seconds) {
const minutes = Math.floor(seconds / 60) // 获取总分钟数
const remainingSeconds = seconds % 60 // 获取剩余秒数
// 格式化输出,确保秒数始终为两位数
return minutes + "分钟" + String(remainingSeconds).padStart(2, "0") + "秒"
}
return null
}
},
methods: {
getAnswerResult() {
this.$axios.post("/platform/h5/qsv/quiz/answerResult", { answerRecordId: this.answerRecordId }).then((res) => {
if (res.code === 0) {
this.subjects = res.data.subjects
this.activity = res.data.activity
this.getAnswerRecord()
}
})
},
//获取回答记录
getAnswerRecord() {
this.$axios.post("/platform/h5/qsv/quiz/answerRecord", { answerRecordId: this.answerRecordId }).then((res) => {
if (res.code === 0) {
this.answerRecord = res.data
this.initAnswer()
}
})
},
//答案回显
initAnswer() {
this.subjects.forEach((subject, subjectIndex) => {
const answer = this.answerRecord.extJson[subject.id]
if (["radio", "checkbox"].includes(subject.type)) {
this.$nextTick(() => {
this.$refs["subject" + subject.id][0].toggleAll(false)
})
answer?.optionIds.forEach((optionId) => {
this.$nextTick(() => {
this.$refs["option" + optionId][0].toggle()
})
})
} else if (subject.type === "text") {
subject.userFillContent = answer?.text
}
})
},
historyBack() {
this.$pjaxReplace("/platform/h5/qsv")
},
openHistory() {
this.historyShow = true
this.$axios.post("/platform/h5/qsv/quiz/historyScore", { activityId: this.activity.id }).then((res) => {
if (res.code === 0) {
this.historyList = res.data
}
})
}
},
created() {
this.getAnswerResult()
}
})
</script>
<!--#
}
#-->
@@ -0,0 +1,292 @@
<!--#
layout("/layouts/platform_h5.html"){
#-->
<style>
.container {
padding: 0;
}
.container .title {
text-align: center;
padding: 20px;
background: #ffffff;
margin: 0 0 10px 0;
}
.container .card {
padding: 10px;
background: #ffffff;
}
.subject {
background: #fff;
padding: 15px;
margin-bottom: 15px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
position: relative;
}
.subject p {
font-size: 16px;
margin: 20px 0 10px 0;
font-weight: bold;
}
.subject .tag {
position: absolute;
top: 0;
}
.button-control {
position: fixed;
bottom: 0;
left: 0;
right: 0;
padding: 20px;
display: flex;
column-gap: 20px;
background: #fff;
}
.van-checkbox__icon--checked .van-icon {
color: #fff !important;
background-color: var(--color-primary) !important;
border-color: var(--color-primary) !important;
}
.van-checkbox__icon--disabled .van-icon {
background-color: #fff;
}
.ui-input-box {
border: 1px solid #e3e3e3;
margin: 5px 0;
background-color: #fff;
padding: 0;
width: 100%;
display: flex;
}
.ui-input-box input {
background-color: #fff;
border: none !important;
padding: 10px;
font-size: 14px;
line-height: 18px;
display: inline-block;
margin: 0;
-webkit-appearance: none;
resize: none;
width: 100%;
}
</style>
<div id="app" v-cloak>
<van-nav-bar title="问卷调查" left-arrow @click-left="historyBack" placeholder fixed></van-nav-bar>
<div class="container" :style="{'padding-bottom': answerRecord.isFinish ? 0 : '84px'}">
<h2 class="title">{{ activity.title }}</h2>
<div v-if="!isFinished">
<div v-for="(subject, index) in subjects" :key="index" class="subject">
<p>{{index+1}}、{{ subject.title }}</p>
<div class="tag">
<van-tag v-if="subject.type === 'checkbox'" type="primary" size="large">多选</van-tag>
<van-tag v-if="subject.type === 'radio'" type="primary" size="large">单选</van-tag>
<van-tag v-if="subject.score" type="primary" size="large">{{subject.score}}分</van-tag>
</div>
<!--单选、多选-->
<van-checkbox-group
v-model="subject.userSelectOptionIds"
:ref="'subject'+subject.id"
v-if="['radio','checkbox'].includes(subject.type)"
>
<van-cell-group>
<van-cell
v-for="(option,index) in subject.options"
clickable
:key="option.id"
:title="option.text"
@click="cellToggle(subject,option.id)"
>
<template #icon>
<van-checkbox :name="option.id" :ref="'option'+option.id" style="margin-right: 10px"></van-checkbox>
</template>
<img
slot="right-icon"
v-if="option.imgUrl"
:src="option.imgUrl"
alt=""
style="width: 40px; height: 40px"
@click.stop="previewOptionImg(option.imgUrl)"
/>
</van-cell>
</van-cell-group>
</van-checkbox-group>
<!--填空题-->
<div class="ui-input-box" v-if="subject.type==='text'">
<input type="text" v-model="subject.userFillContent" :readonly="answerRecord.isFinish" />
</div>
</div>
<div class="button-control" v-if="!answerRecord.isFinish">
<van-button type="primary" @click="onSubmit" block :disabled="isEnd">{{isEnd ? '已结束' : '提交'}}</van-button>
</div>
</div>
</div>
</div>
<script nonce="${cspNonce!}">
const vue = new Vue({
el: "#app",
data() {
return {
list: [],
id: null,
subjects: [],
activity: {},
remainingTime: 0,
isFinished: false,
answerRecord: {},
answerRecordId: null
}
},
computed: {
isEnd() {
if (this.activity) {
return this.$moment().unix() > this.$moment(this.activity.endTime).unix()
}
return true
}
},
methods: {
//获取活动、题目
listSubjects() {
this.$axios.post("/platform/h5/qsv/survey/subjects", { activityId: this.id }).then((res) => {
if (res.code === 0) {
this.activity = res.data.activity
this.subjects = res.data.subjects
this.answerRecordId = res.data.answerRecordId
this.getAnswerRecord()
}
})
},
//获取回答记录
getAnswerRecord() {
this.$axios.post("/platform/h5/qsv/survey/answerRecord", { answerRecordId: this.answerRecordId }).then((res) => {
if (res.code === 0) {
this.answerRecord = res.data
if (this.answerRecord.isFinish) {
this.$toast.success("您已完成该调查")
}
this.initAnswer()
}
})
},
//答案回显
initAnswer() {
this.subjects.forEach((subject, subjectIndex) => {
const answer = this.answerRecord.extJson[subject.id]
if (["radio", "checkbox"].includes(subject.type)) {
this.$refs["subject" + subject.id][0].toggleAll(false)
answer?.optionIds.forEach((optionId) => {
this.$nextTick(() => {
this.$refs["option" + optionId][0].toggle()
})
})
} else if (subject.type === "text") {
subject.userFillContent = answer?.text
}
})
},
//选项点击
cellToggle(subject, optionId) {
if (this.answerRecord.isFinish) {
return
}
if (subject.type === "radio") {
this.$refs["subject" + subject.id][0].toggleAll(false)
}
this.$refs["option" + optionId][0].toggle()
},
//预览图片
previewOptionImg(img) {
vant.ImagePreview([img])
},
//手动提交
onSubmit() {
const endTime = this.activity.endTime
if (this.isEnd) {
this.$toast("调查已结束")
return
}
//提示那些题没有作答
for (let i = 0; i < this.subjects.length; i++) {
const subject = this.subjects[i]
if (["radio", "checkbox"].includes(subject.type)) {
if (!subject.userSelectOptionIds || subject.userSelectOptionIds.length === 0) {
this.$toast.fail("第" + (i + 1) + "题未做答")
return
}
} else if ("text" === subject.type) {
if (!subject.userFillContent) {
this.$toast.fail("第" + (i + 1) + "题未作答")
return
}
}
}
this.autoSubmit()
},
//自动提交
autoSubmit(loading = null) {
this.$axios
.post("/platform/h5/qsv/survey/submitAnswer", {
answer: JSON.stringify({
activityId: this.id,
answerRecordId: this.answerRecordId,
subjects: this.subjects.map((subject) => {
return {
id: subject.id,
userSelectOptionIds: ["checkbox", "radio"].includes(subject.type) ? subject.userSelectOptionIds : [],
userFillContent: subject.type === "text" ? subject.userFillContent : null
}
})
})
})
.then((res) => {
if (res.code === 0) {
this.$toast.success(res.msg)
this.getAnswerRecord()
}
})
.finally(() => {
if (loading) {
loading.clear()
}
})
}
},
created() {
const id = GetQueryString("id")
if (id) {
this.id = id
this.listSubjects()
}
}
})
</script>
<!--#
}
#-->
@@ -0,0 +1,795 @@
<!--#
layout("/layouts/platform_h5.html"){
#-->
<style>
body {
background-color: #f3f7fd;
}
.sports-activity-page {
min-height: 100vh;
padding-bottom: calc(24px + env(safe-area-inset-bottom));
box-sizing: border-box;
background-color: #f3f7fd;
}
.sports-activity-page .van-nav-bar {
background-color: #fff;
}
.sports-activity-page .van-nav-bar__title {
color: #1f2937;
font-size: 16px;
}
.sports-activity-page .van-nav-bar .van-icon,
.sports-activity-page .van-nav-bar__text {
color: #397fbd;
}
.sports-activity-content {
padding: 12px 12px 0;
}
.sports-activity-hero {
position: relative;
height: 134px;
overflow: hidden;
border-radius: 10px;
color: #fff;
background-color: #247fe8;
box-shadow: 0 8px 20px rgba(51, 117, 219, .18);
}
.sports-activity-hero__image {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
object-fit: cover;
object-position: center;
}
.sports-activity-hero__copy {
position: relative;
z-index: 1;
display: flex;
width: 64%;
height: 100%;
padding-left: 16px;
flex-direction: column;
justify-content: center;
box-sizing: border-box;
background: linear-gradient(90deg, rgba(22, 112, 232, .98) 0%, rgba(22, 112, 232, .88) 52%, rgba(22, 112, 232, 0) 100%);
}
.sports-activity-hero__title {
font-size: 25px;
line-height: 34px;
font-weight: 700;
letter-spacing: 1px;
text-shadow: 0 2px 6px rgba(17, 86, 182, .18);
}
.sports-activity-hero__subtitle {
margin-top: 4px;
font-size: 13px;
line-height: 20px;
letter-spacing: .5px;
opacity: .95;
}
.sports-activity-sticky {
padding: 10px 0 1px;
background-color: #f3f7fd;
}
.van-sticky--fixed .sports-activity-sticky {
padding-right: 12px;
padding-left: 12px;
box-sizing: border-box;
}
.sports-activity-search {
margin: 0 0 8px;
padding: 0;
overflow: hidden;
border-radius: 12px;
background-color: #fff;
box-shadow: 0 5px 16px rgba(43, 73, 112, .1);
}
.sports-activity-search .van-search__content {
height: 42px;
padding-left: 12px;
align-items: center;
border-radius: 12px;
background-color: #fff;
}
.sports-activity-search .van-field__control {
color: #263548;
font-size: 14px;
}
.sports-activity-search .van-field__control::placeholder {
color: #a7b0bf;
}
.sports-activity-filter {
margin: 0 0 10px;
overflow: hidden;
border-radius: 10px;
box-shadow: 0 5px 16px rgba(43, 73, 112, .08);
transition: border-radius .18s ease;
}
.sports-activity-filter.is-open {
border-radius: 10px 10px 0 0;
}
.sports-activity-filter .van-dropdown-menu__bar {
height: 44px;
box-shadow: none;
}
.sports-activity-filter .van-dropdown-menu__title {
color: #66758a;
font-size: 14px;
}
.sports-activity-filter .van-dropdown-menu__title--active,
.sports-activity-filter .van-dropdown-item__option--active,
.sports-activity-filter .van-dropdown-item__option--active .van-dropdown-item__icon {
color: #1989fa;
}
.sports-activity-filter .van-dropdown-menu__title--down::after {
border-color: transparent transparent #1989fa #1989fa;
}
.sports-activity-filter .van-dropdown-item__content {
width: calc(100% - 24px);
margin: 0 12px;
overflow: hidden;
box-sizing: border-box;
border-radius: 0 0 12px 12px;
box-shadow: 0 10px 24px rgba(31, 65, 108, .14);
}
.sports-activity-filter .van-dropdown-item__option {
min-height: 48px;
padding: 0 16px;
color: #46566c;
font-size: 14px;
}
.sports-activity-filter .van-cell::after {
right: 16px;
left: 16px;
}
.sports-activity-list {
display: flex;
flex-direction: column;
gap: 10px;
}
.sports-activity-card {
display: flex;
min-height: 74px;
padding: 14px;
align-items: center;
box-sizing: border-box;
border-radius: 10px;
background-color: #fff;
box-shadow: 0 7px 20px rgba(55, 85, 126, .08);
}
.sports-activity-card {
position: relative;
min-height: 0;
padding: 0;
overflow: hidden;
align-items: stretch;
flex-direction: column;
cursor: pointer;
}
.sports-activity-card__icon {
display: flex;
width: 38px;
height: 38px;
margin-right: 12px;
flex: 0 0 38px;
align-items: center;
justify-content: center;
border-radius: 9px;
color: #4d91ef;
background-color: #edf4ff;
font-size: 24px;
}
.sports-activity-card__body {
min-width: 0;
padding: 12px 38px 14px 14px;
box-sizing: border-box;
flex: none;
}
.sports-activity-card__cover {
display: block;
width: 100%;
height: 150px;
margin: 0;
flex: 0 0 150px;
object-fit: cover;
border-radius: 0;
background-color: #edf4ff;
}
.sports-activity-card__cover-state {
display: flex;
width: 100%;
height: 150px;
flex: 0 0 150px;
align-items: center;
justify-content: center;
flex-direction: column;
row-gap: 8px;
color: #8da1ba;
background-color: #edf4fc;
font-size: 13px;
}
.sports-activity-card__cover-state .van-icon {
font-size: 34px;
}
.sports-activity-card__cover-state.is-error {
color: #c27b75;
background-color: #fff3f1;
}
.sports-activity-card > .sports-activity-card__icon {
width: 100%;
height: 150px;
margin: 0;
flex: 0 0 150px;
border-radius: 0;
font-size: 40px;
}
.sports-activity-card__title {
color: #303846;
font-size: 14px;
line-height: 21px;
}
.sports-activity-card__title {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.sports-activity-card__time {
margin-top: 4px;
color: #98a2b2;
font-size: 12px;
line-height: 17px;
white-space: nowrap;
}
.sports-activity-card__meta {
margin-top: 10px;
padding-top: 9px;
border-top: 1px solid #edf1f6;
}
.sports-activity-card__address {
display: flex;
align-items: flex-start;
color: #69788d;
font-size: 12px;
line-height: 18px;
}
.sports-activity-card__address .van-icon {
margin: 2px 5px 0 0;
flex: none;
color: #6c9ff8;
font-size: 14px;
}
.sports-activity-card__address span {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.sports-activity-card__tags {
display: flex;
margin-top: 8px;
flex-wrap: wrap;
gap: 6px;
}
.sports-activity-card__tag {
display: inline-flex;
padding: 3px 8px;
align-items: center;
border-radius: 4px;
color: #397de0;
background-color: #edf5ff;
font-size: 11px;
line-height: 16px;
}
.sports-activity-card__tag-label {
margin-right: 4px;
color: #8aa7cf;
}
.sports-activity-card__arrow {
position: absolute;
top: 164px;
right: 14px;
color: #b4bdca;
font-size: 16px;
}
.sports-activity-empty-illustration {
display: flex;
min-height: 250px;
padding: 24px 18px 32px;
align-items: center;
justify-content: center;
flex-direction: column;
box-sizing: border-box;
text-align: center;
}
.sports-activity-empty-illustration img {
display: block;
width: 82%;
max-width: 280px;
height: auto;
object-fit: contain;
}
.sports-activity-empty-illustration__title {
margin-top: -8px;
color: #50627a;
font-size: 15px;
line-height: 22px;
font-weight: 500;
}
.sports-activity-empty-illustration__hint {
margin-top: 6px;
color: #9aa7b8;
font-size: 12px;
line-height: 18px;
}
.activity-notification-popup,
.activity-notification-page {
background-color: #f3f7fd;
}
.activity-notification-page {
position: relative;
min-height: 100vh;
}
.activity-notification-page .van-nav-bar {
background-color: #fff;
}
.activity-notification-page .van-nav-bar__title {
color: #1f2937;
font-size: 16px;
}
.activity-notification-page .van-nav-bar .van-icon,
.activity-notification-page .van-nav-bar__text {
color: #397fbd;
}
.activity-notification-scroll {
height: calc(100vh - 46px);
padding: 12px 12px calc(92px + env(safe-area-inset-bottom));
overflow-x: hidden;
overflow-y: auto;
box-sizing: border-box;
}
.activity-notification-hero {
display: flex;
min-height: 82px;
padding: 16px;
align-items: center;
box-sizing: border-box;
border-radius: 12px;
color: #fff;
background: linear-gradient(120deg, #2d7eea 0%, #5ba6f7 100%);
box-shadow: 0 8px 20px rgba(45, 126, 234, .18);
}
.activity-notification-hero__icon {
display: flex;
width: 44px;
height: 44px;
margin-right: 12px;
flex: 0 0 44px;
align-items: center;
justify-content: center;
border-radius: 11px;
color: #2d7eea;
background-color: rgba(255, 255, 255, .92);
font-size: 25px;
}
.activity-notification-hero__title {
font-size: 17px;
line-height: 24px;
font-weight: 600;
}
.activity-notification-hero__subtitle {
margin-top: 3px;
font-size: 12px;
line-height: 18px;
opacity: .9;
}
.activity-notification-card {
margin-top: 12px;
padding: 18px 16px;
overflow: hidden;
border-radius: 12px;
background-color: #fff;
box-shadow: 0 7px 20px rgba(55, 85, 126, .08);
}
.activity-notification-content,
.activity-notification-content div,
.activity-notification-content p,
.activity-notification-content span,
.activity-notification-content li {
color: #3f4c5e !important;
font-size: 14px !important;
line-height: 1.8 !important;
word-break: break-word;
}
.activity-notification-content h1,
.activity-notification-content h2,
.activity-notification-content h3 {
margin: 16px 0 10px !important;
color: #1f2f43 !important;
font-size: 19px !important;
line-height: 1.5 !important;
font-weight: 600 !important;
}
.activity-notification-content > :first-child {
margin-top: 0 !important;
}
.activity-notification-content p {
margin-top: 0 !important;
margin-bottom: 8px !important;
}
.activity-notification-content img {
max-width: 100% !important;
height: auto !important;
border-radius: 8px;
}
.activity-notification-content table {
display: block;
max-width: 100%;
overflow-x: auto;
border-collapse: collapse;
}
.activity-notification-empty {
display: flex;
min-height: 180px;
margin-top: 12px;
align-items: center;
justify-content: center;
flex-direction: column;
row-gap: 10px;
border-radius: 12px;
color: #9aa7b8;
background-color: #fff;
box-shadow: 0 7px 20px rgba(55, 85, 126, .08);
font-size: 14px;
}
.activity-notification-empty .van-icon {
color: #83b5f4;
font-size: 36px;
}
.activity-notification-footer {
position: fixed;
right: 0;
bottom: 0;
left: 0;
z-index: 5;
padding: 10px 12px calc(10px + env(safe-area-inset-bottom));
border-top: 1px solid #edf1f6;
background-color: rgba(255, 255, 255, .96);
box-sizing: border-box;
}
.activity-notification-footer .van-button {
height: 42px;
border: 0;
font-size: 15px;
background-color: #1989fa;
box-shadow: 0 6px 16px rgba(25, 137, 250, .22);
}
.activity-notification-footer .van-button.is-disabled,
.activity-notification-footer .van-button--disabled {
color: #8b98aa !important;
background-color: #eef2f7 !important;
opacity: 1;
box-shadow: none;
}
</style>
<div id="app" v-cloak>
<div class="sports-activity-page">
<van-nav-bar
title="体育活动"
left-arrow
left-text="返回"
fixed
placeholder
@click-left="goBack"
></van-nav-bar>
<div class="sports-activity-content">
<section class="sports-activity-hero">
<img
class="sports-activity-hero__image"
src="/assets/mobile/img/activity/hero/sports-hero-v2.png"
alt="体育活动插画"
>
<div class="sports-activity-hero__copy">
<div class="sports-activity-hero__title">体育活动</div>
<div class="sports-activity-hero__subtitle">强健体魄 · 运动同行</div>
</div>
</section>
<van-sticky :offset-top="46">
<div class="sports-activity-sticky">
<van-search
v-model="searchKeyword"
class="sports-activity-search"
placeholder="搜索体育活动"
shape="round"
clearable
@search="doSearch"
@clear="doSearch"
></van-search>
<van-dropdown-menu
class="sports-activity-filter"
:class="{'is-open': filterOpened}"
active-color="#1989fa"
>
<year-van-dropdown-item
:num="5"
v-model="pageForm.year"
@change="doSearch"
@open="filterOpened = true"
@close="filterOpened = false"
></year-van-dropdown-item>
<van-dropdown-item
v-model="pageForm.isActivity"
:options="stateList"
@change="doSearch"
@open="filterOpened = true"
@close="filterOpened = false"
></van-dropdown-item>
<van-dropdown-item
v-model="pageForm.applyStatus"
:options="applyStatusOptions"
@change="doSearch"
@open="filterOpened = true"
@close="filterOpened = false"
></van-dropdown-item>
</van-dropdown-menu>
</div>
</van-sticky>
<div class="sports-activity-list">
<div
v-for="row in activityList"
:key="row.id"
class="sports-activity-card"
@click="openView(row)"
>
<img
v-if="hasActivityImage(row) && !imageLoadErrors[row.id]"
class="sports-activity-card__cover"
:src="row.image"
:alt="row.NAME || row.name"
@error="handleImageError(row)"
>
<div
v-else
class="sports-activity-card__cover-state"
:class="{'is-error': hasActivityImage(row)}"
>
<van-icon name="photo-o"></van-icon>
<span>{{ hasActivityImage(row) ? '图片加载失败' : '暂无图片' }}</span>
</div>
<div class="sports-activity-card__body">
<div class="sports-activity-card__title">{{ row.NAME || row.name }}</div>
<div class="sports-activity-card__time" v-if="row.applyStartTime && row.applyEndTime">
报名时间:{{ $moment(row.applyStartTime).format('MM/DD HH:mm') }}~{{ $moment(row.applyEndTime).format('MM/DD HH:mm') }}
</div>
<div class="sports-activity-card__time" v-if="row.startTime && row.endTime">
活动时间:{{ $moment(row.startTime).format('MM/DD HH:mm') }}~{{ $moment(row.endTime).format('MM/DD HH:mm') }}
</div>
<div class="sports-activity-card__meta" v-if="row.address || formatGameStyle(row.gameStyle) || formatApplyType(row.applyType) || formatApplyWay(row.applyWay)">
<div class="sports-activity-card__address" v-if="row.address">
<van-icon name="location-o"></van-icon>
<span>{{ row.address }}</span>
</div>
<div class="sports-activity-card__tags" v-if="formatGameStyle(row.gameStyle) || formatApplyType(row.applyType) || formatApplyWay(row.applyWay)">
<span class="sports-activity-card__tag" v-if="formatGameStyle(row.gameStyle)">
<span class="sports-activity-card__tag-label">竞赛方式</span>{{ formatGameStyle(row.gameStyle) }}
</span>
<span class="sports-activity-card__tag" v-if="formatApplyType(row.applyType)">
<span class="sports-activity-card__tag-label">活动类型</span>{{ formatApplyType(row.applyType) }}
</span>
<span class="sports-activity-card__tag" v-if="formatApplyWay(row.applyWay)">
<span class="sports-activity-card__tag-label">报名方式</span>{{ formatApplyWay(row.applyWay) }}
</span>
</div>
</div>
</div>
<van-icon class="sports-activity-card__arrow" name="arrow"></van-icon>
</div>
<div v-if="activityList.length === 0" class="sports-activity-empty-illustration">
<img src="/assets/mobile/img/activity/sports-empty.png" alt="体育活动空状态插画">
<div class="sports-activity-empty-illustration__title">{{ emptyText }}</div>
<div class="sports-activity-empty-illustration__hint">{{ emptyHint }}</div>
</div>
</div>
</div>
<activity_event_notification
:event_notification="viewData.eventNotification"
:id="viewData.id"
:apply_start_time="viewData.applyStartTime"
:apply_end_time="viewData.applyEndTime"
ref="eventNotification"
></activity_event_notification>
</div>
</div>
<script nonce="${cspNonce!}">
<!--#include("eventNotification.js"){}#-->
new Vue({
el: "#app",
data() {
return {
activityList: [],
searchKeyword: "",
imageLoadErrors: {},
filterOpened: false,
pageForm: {
isActivity: 2,
applyStatus: 1,
year: new Date().getFullYear()
},
stateList: [
{value: 1, text: "全部活动"},
{value: 2, text: "报名中"},
{value: 3, text: "报名已结束"}
],
applyStatusOptions: [
{value: 1, text: "未报名"},
{value: 2, text: "已报名"}
],
viewData: {}
}
},
computed: {
emptyText() {
if (this.searchKeyword.trim()) return "未找到相关体育活动"
if (this.pageForm.isActivity === 3) return "暂无已结束的体育活动"
if (this.pageForm.isActivity === 2) return "暂无报名中的体育活动"
return "暂无体育活动"
},
emptyHint() {
return this.searchKeyword.trim() ? "请尝试更换搜索关键词" : "当前筛选条件下暂时没有活动"
}
},
components: {
activity_event_notification: ACTIVITY_EVENT_NOTIFICATION
},
methods: {
goBack() {
this.$pjaxReplace("/platform/home")
},
openView(row) {
this.viewData = row
this.$nextTick(() => {
this.$refs.eventNotification.viewShow = true
})
},
doSearch() {
this.loadActivities()
},
hasActivityImage(row) {
return !!(row && row.image && String(row.image).trim())
},
handleImageError(row) {
this.$set(this.imageLoadErrors, row.id, true)
},
formatGameStyle(value) {
return {
1: "竞赛类",
2: "非竞赛类"
}[Number(value)] || ""
},
formatApplyType(value) {
return {
1: "单一团体赛",
2: "综合赛",
3: "个人赛"
}[Number(value)] || ""
},
formatApplyWay(value) {
if (value === null || value === undefined || value === "") return ""
let values = value
if (typeof values === "string") {
try {
values = JSON.parse(values)
} catch (e) {
values = values.split(",")
}
}
if (!Array.isArray(values)) values = [values]
const labels = {
1: "个人报名",
2: "分工会报名",
3: "小程序报名"
}
return values.map(item => labels[Number(item)]).filter(Boolean).join("、")
},
loadActivities() {
this.activityList = []
this.imageLoadErrors = {}
const queryForm = Object.assign({}, this.pageForm, {
searchKeyword: this.searchKeyword.trim()
})
this.$axios.post("/platform/activity/apply/h5/activityData", queryForm).then((res) => {
if (res.code === 0) {
this.activityList = res.data && res.data.list ? res.data.list : []
}
})
}
},
created() {
this.loadActivities()
}
})
</script>
<!--#
}
#-->
@@ -0,0 +1,921 @@
<!--#
layout("/layouts/platform_h5.html"){
#-->
<style>
body {
background-color: #f3f7fd;
}
.sports-event-page {
min-height: 100vh;
padding-bottom: calc(24px + env(safe-area-inset-bottom));
box-sizing: border-box;
background-color: #f3f7fd;
}
.sports-event-page .van-nav-bar {
background-color: #fff;
}
.sports-event-page .van-nav-bar__title {
color: #1f2937;
font-size: 16px;
}
.sports-event-page .van-nav-bar .van-icon,
.sports-event-page .van-nav-bar__text {
color: #397fbd;
}
.sports-event-summary {
display: flex;
min-height: 82px;
margin: 12px 12px 0;
padding: 16px;
align-items: center;
box-sizing: border-box;
border-radius: 12px;
color: #fff;
background: linear-gradient(120deg, #2d7eea 0%, #5ba6f7 100%);
box-shadow: 0 8px 20px rgba(45, 126, 234, .18);
}
.sports-event-summary__icon {
display: flex;
width: 44px;
height: 44px;
margin-right: 12px;
overflow: hidden;
flex: 0 0 44px;
align-items: center;
justify-content: center;
border-radius: 11px;
color: #2d7eea;
background-color: rgba(255, 255, 255, .92);
font-size: 25px;
}
.sports-event-summary__body {
min-width: 0;
flex: 1;
}
.sports-event-summary__title {
overflow: hidden;
color: #fff;
font-size: 17px;
line-height: 24px;
font-weight: 600;
text-overflow: ellipsis;
white-space: nowrap;
}
.sports-event-summary__subtitle {
margin-top: 3px;
font-size: 12px;
line-height: 18px;
opacity: .9;
}
.sports-event-sticky {
padding: 10px 0 8px;
background-color: #f3f7fd;
}
.sports-event-search {
margin: 0 12px;
padding: 0;
background: transparent;
}
.sports-event-search .van-search__content {
height: 42px;
padding-left: 13px;
align-items: center;
border-radius: 12px;
background-color: #fff;
box-shadow: 0 5px 16px rgba(55, 85, 126, .07);
}
.sports-event-search .van-field__control {
font-size: 14px;
}
.sports-event-list-head {
display: flex;
width: 100%;
margin: 16px 0 0;
padding: 0 12px;
align-items: center;
justify-content: space-between;
box-sizing: border-box;
}
.sports-event-list-head__title {
color: #263548;
font-size: 16px;
line-height: 22px;
font-weight: 600;
}
.sports-event-list-head__count {
color: #8d9aad;
font-size: 12px;
}
.sports-event-list-head__tools {
display: flex;
align-items: center;
gap: 10px;
}
.sports-event-view-toggle {
display: inline-flex;
height: 28px;
padding: 0 10px;
align-items: center;
justify-content: center;
border: 0;
border-radius: 14px;
color: #397de0;
background-color: #eaf3ff;
font-size: 12px;
line-height: 28px;
}
.sports-event-view-toggle i {
margin-right: 5px;
font-size: 13px;
}
.sports-event-page .sports-event-card-list {
padding: 0 12px;
}
.sports-event-page .table-card {
margin-bottom: 12px;
overflow: hidden;
border-radius: 12px;
background-color: #fff;
box-shadow: 0 7px 20px rgba(55, 85, 126, .08);
}
.sports-event-page .table-card > .van-image,
.sports-event-page .table-card > .van-image img {
display: block;
border-radius: 12px 12px 0 0;
}
.sports-event-page .table-card .van-cell {
padding: 9px 16px;
color: #566579;
font-size: 13px;
background-color: #fff;
}
.sports-event-page .table-card .van-cell:first-of-type {
padding-top: 15px;
padding-bottom: 12px;
}
.sports-event-page .table-card .van-cell::after {
right: 16px;
left: 16px;
border-color: #edf1f6;
}
.sports-event-page .table-card .van-cell__title {
color: #66758a;
}
.sports-event-page .table-card .van-cell__value,
.sports-event-page .table-card .van-cell__right-icon {
color: #263548;
}
.sports-event-page .table-card .font-size3 {
color: #23344a;
font-size: 16px;
line-height: 23px;
font-weight: 600;
}
.sports-event-page .table-card .van-tag {
padding: 3px 7px;
justify-content: center;
border: 0;
border-radius: 4px;
box-sizing: border-box;
font-size: 11px;
}
.sports-event-page .table-card .sports-event-status-tag.is-pending {
color: #397de0;
background-color: #edf5ff;
}
.sports-event-page .table-card .sports-event-status-tag.is-success {
color: #35a86b;
background-color: #eaf8f1;
}
.sports-event-page .table-card .sports-event-status-tag.is-rejected {
color: #df5b59;
background-color: #fff1f0;
}
.sports-event-card__body {
padding: 16px;
}
.sports-event-card__title-row {
display: flex;
min-width: 0;
align-items: flex-start;
justify-content: space-between;
gap: 10px;
}
.sports-event-card__title {
min-width: 0;
flex: 1;
color: #23344a;
font-size: 16px;
line-height: 23px;
font-weight: 600;
}
.sports-event-card__status {
display: flex;
flex: 0 0 auto;
align-items: center;
}
.sports-event-card__activity {
display: flex;
margin-top: 5px;
align-items: center;
color: #78879a;
font-size: 12px;
line-height: 18px;
}
.sports-event-card__activity .van-icon {
margin-right: 5px;
font-size: 14px;
}
.sports-event-card__tags,
.sports-event-card__config {
display: flex;
margin-top: 10px;
flex-wrap: wrap;
gap: 6px;
}
.sports-event-card__tag,
.sports-event-card__config-tag {
display: inline-flex;
min-height: 24px;
padding: 3px 8px;
align-items: center;
border-radius: 5px;
color: #397de0;
background-color: #edf5ff;
font-size: 12px;
line-height: 18px;
box-sizing: border-box;
}
.sports-event-card__tag-label {
margin-right: 4px;
color: #7f9fc9;
}
.sports-event-card__stats {
display: grid;
margin-top: 12px;
padding: 12px 0;
grid-template-columns: repeat(3, minmax(0, 1fr));
border-top: 1px solid #edf1f6;
border-bottom: 1px solid #edf1f6;
}
.sports-event-card__stat {
min-width: 0;
padding: 0 10px;
text-align: center;
box-sizing: border-box;
}
.sports-event-card__stat:first-child {
padding-left: 0;
}
.sports-event-card__stat:last-child {
padding-right: 0;
}
.sports-event-card__stat + .sports-event-card__stat {
border-left: 1px solid #edf1f6;
}
.sports-event-card__stat-label {
color: #7b899b;
font-size: 11px;
line-height: 16px;
}
.sports-event-card__stat-value {
margin-top: 3px;
overflow: hidden;
color: #33445a;
font-size: 13px;
line-height: 18px;
text-overflow: ellipsis;
white-space: nowrap;
}
.sports-event-card__config-tag {
color: #66758a;
background-color: #f5f7fa;
}
.sports-event-card__extra {
margin-top: 10px;
}
.sports-event-card__extra-row {
display: flex;
padding: 4px 0;
align-items: flex-start;
color: #66758a;
font-size: 12px;
line-height: 18px;
}
.sports-event-card__extra-row .van-icon {
margin: 2px 6px 0 0;
flex: 0 0 auto;
font-size: 14px;
}
.sports-event-card__extra-label {
margin-right: 8px;
flex: 0 0 auto;
color: #7b899b;
}
.sports-event-card__extra-value {
min-width: 0;
flex: 1;
color: #33445a;
overflow-wrap: anywhere;
}
.sports-event-page .table-card .table-card-footer {
min-height: 56px;
margin-top: 4px;
padding: 12px 16px;
align-items: center;
border-top: 1px solid #edf1f6;
background-color: #f8fbff;
box-sizing: border-box;
}
.sports-event-page .table-card .table-card-footer::after {
display: none;
}
.sports-event-card__footer {
display: flex;
justify-content: flex-end;
}
.sports-event-card__footer > div {
display: flex;
justify-content: flex-end;
}
.sports-event-union-action-hint {
display: inline-flex;
align-items: center;
color: #ed6a0c;
font-size: 12px;
line-height: 18px;
text-align: right;
}
.sports-event-union-action-hint.is-authorized {
color: #1989fa;
}
.sports-event-page .table-card .table-card-footer .van-cell__value {
display: flex;
align-items: center;
justify-content: flex-end;
}
.sports-event-page .table-card-footer .van-button {
min-width: 78px;
height: 32px;
padding: 0 14px;
border: 0;
border-radius: 16px;
font-size: 13px;
}
.sports-event-page .table-card-footer .van-button i {
margin-right: 6px;
font-size: 16px;
}
.sports-event-page .table-card-footer .sports-event-apply-button {
width: 108px;
min-width: 108px;
height: 32px;
color: #fff !important;
border-color: #1989fa !important;
background-color: #1989fa !important;
box-shadow: 0 5px 12px rgba(25, 137, 250, .18);
}
.sports-event-page .table-card-footer .sports-event-cancel-button {
width: 108px;
min-width: 108px;
height: 32px;
color: #fff !important;
border-color: #e84d4f !important;
background-color: #e84d4f !important;
box-shadow: 0 5px 12px rgba(232, 77, 79, .16);
}
.sports-event-page .table-card-footer .van-button__content {
white-space: nowrap;
}
.sports-event-compact-list {
margin: 0 12px;
padding: 0;
overflow: hidden;
border-radius: 12px;
background-color: #fff;
box-shadow: 0 7px 20px rgba(55, 85, 126, .08);
}
.sports-event-compact-item {
display: flex;
min-height: 68px;
padding: 12px 14px;
align-items: center;
justify-content: space-between;
gap: 12px;
box-sizing: border-box;
border-bottom: 1px solid #edf1f6;
background-color: #fff;
}
.sports-event-compact-item:last-child {
border-bottom: 0;
}
.sports-event-compact-item__name {
min-width: 0;
flex: 1;
overflow: hidden;
color: #23344a;
font-size: 15px;
line-height: 22px;
font-weight: 600;
text-overflow: ellipsis;
white-space: nowrap;
}
.sports-event-compact-item__action {
flex: 0 0 auto;
}
.sports-event-page .sports-event-compact-list .sports-event-compact-button.van-button {
min-width: 0;
height: 28px !important;
padding: 0 10px !important;
border: 0 !important;
border-radius: 14px;
font-size: 12px;
line-height: 28px;
box-shadow: none !important;
}
.sports-event-compact-button i {
margin-right: 4px;
font-size: 13px;
}
.sports-event-page .sports-event-compact-list .sports-event-compact-button--apply.van-button {
color: #397de0 !important;
background: #edf5ff !important;
box-shadow: none !important;
}
.sports-event-page .sports-event-compact-list .sports-event-compact-button--cancel.van-button {
color: #df5b59 !important;
background: #fff1f0 !important;
box-shadow: none !important;
}
.sports-event-compact-button .van-button__content {
white-space: nowrap;
}
.sports-event-page .van-list__finished-text,
.sports-event-page .van-list__loading {
color: #9aa7b8;
font-size: 13px;
line-height: 44px;
}
.sports-event-refresh {
min-height: calc(100vh - 226px);
}
.sports-event-empty {
display: flex;
min-height: 300px;
padding: 28px 18px;
align-items: center;
justify-content: flex-start;
flex-direction: column;
box-sizing: border-box;
text-align: center;
}
.sports-event-empty img {
display: block;
width: 82%;
max-width: 300px;
height: auto;
object-fit: contain;
}
.sports-event-empty__title {
margin-top: -8px;
color: #50627a;
font-size: 15px;
line-height: 22px;
font-weight: 500;
}
.sports-event-empty__hint {
margin-top: 6px;
color: #9aa7b8;
font-size: 12px;
line-height: 18px;
}
</style>
<div id="app" v-cloak>
<div class="sports-event-page">
<van-sticky>
<van-nav-bar
@click-left="historyBack"
left-arrow
left-text="返回"
placeholder
title="项目列表"
></van-nav-bar>
</van-sticky>
<div class="sports-event-summary">
<div class="sports-event-summary__icon">
<van-icon name="award-o"></van-icon>
</div>
<div class="sports-event-summary__body">
<div class="sports-event-summary__title">体育活动项目</div>
<div class="sports-event-summary__subtitle">选择适合的项目,参与健康运动</div>
</div>
</div>
<van-sticky :offset-top="46">
<div class="sports-event-sticky">
<van-search
v-model="pageForm.searchKeyword"
class="sports-event-search"
placeholder="搜索项目名称"
shape="round"
clearable
@search="doSearch"
@clear="doSearch"
></van-search>
<div class="sports-event-list-head">
<button type="button" class="sports-event-view-toggle" @click="toggleViewMode">
<i :class="viewMode === 'card' ? 'fa fa-list' : 'fa fa-th-large'"></i>
<span>{{ viewMode === 'card' ? '列表' : '卡片' }}</span>
</button>
<div class="sports-event-list-head__count" v-if="pageForm.totalCount">共 {{ pageForm.totalCount }} 项</div>
</div>
</div>
</van-sticky>
<van-pull-refresh class="sports-event-refresh" v-model="refreshing" @refresh="onRefresh">
<div>
<van-list :finished="finished" @load="pageData" finished-text="没有更多了" v-if="tableData.length>0"
v-model="loading">
<div class="sports-event-card-list" v-if="viewMode === 'card'">
<div :key="row.id" class="table-card" v-for="row in tableData">
<div class="sports-event-card__body">
<div class="sports-event-card__title-row">
<div class="sports-event-card__title">{{row.allName}}</div>
<div class="sports-event-card__status">
<template v-if="row.status&&row.applyWay.length===1&&row.applyWay.includes(1)">
<van-tag class="sports-event-status-tag is-pending" v-if="row.status===1">待审核</van-tag>
<van-tag class="sports-event-status-tag is-success" v-if="row.status===2">已成功报名</van-tag>
<van-tag class="sports-event-status-tag is-rejected" v-if="row.status===3">审核不通过</van-tag>
</template>
<template v-else>
<van-tag class="sports-event-status-tag is-pending" v-if="row.status2===0">待审核</van-tag>
<van-tag class="sports-event-status-tag is-success" v-if="row.status2===2">已成功报名</van-tag>
<van-tag class="sports-event-status-tag is-rejected" v-if="row.status2===3">审核不通过</van-tag>
</template>
</div>
</div>
<div class="sports-event-card__activity" v-if="row.name">
<van-icon name="flag-o"></van-icon>
<span>{{row.name}}</span>
</div>
<div class="sports-event-card__tags">
<span class="sports-event-card__tag" v-if="row.eveProjectType"><span class="sports-event-card__tag-label">项目</span>{{row.eveProjectType == 2 ? '团体' : '单项'}}</span>
<span class="sports-event-card__tag"><span class="sports-event-card__tag-label">性别</span>{{row.isMenWomen == 1 ? '男子' : row.isMenWomen == 2 ? '女子' : '不限'}}</span>
<span class="sports-event-card__tag" v-if="row.isInterest">趣味项目</span>
<span class="sports-event-card__tag" v-if="row.registrationWays"><span class="sports-event-card__tag-label">报名</span>{{row.registrationWays == 2 ? '双人' : '单人'}}</span>
<span class="sports-event-card__tag" v-if="row.applyType"><span class="sports-event-card__tag-label">活动</span>{{row.applyType == 2 ? '综合类' : '单一类'}}</span>
<span class="sports-event-card__tag" v-if="row.applyWay && row.applyWay.includes(1)">个人报名</span>
<span class="sports-event-card__tag" v-if="row.applyWay && row.applyWay.includes(2)">分工会报名</span>
<span class="sports-event-card__tag" v-if="row.applyWay && row.applyWay.includes(3)">小程序报名</span>
</div>
<div class="sports-event-card__stats">
<div class="sports-event-card__stat">
<div class="sports-event-card__stat-label">可报队数</div>
<div class="sports-event-card__stat-value">{{row.eveProjectType == 2 && row.restrictTotalTeam != null ? row.restrictTotalTeam + '队' : '不适用'}}</div>
</div>
<div class="sports-event-card__stat">
<div class="sports-event-card__stat-label">运动员人数</div>
<div class="sports-event-card__stat-value" v-if="row.athletesMinNum != null && row.athletesMaxNum != null">{{row.athletesMinNum}}-{{row.athletesMaxNum}}人</div>
<div class="sports-event-card__stat-value" v-else-if="row.athletesMaxNum != null">最多{{row.athletesMaxNum}}人</div>
<div class="sports-event-card__stat-value" v-else>暂无限制</div>
</div>
<div class="sports-event-card__stat">
<div class="sports-event-card__stat-label">每人限报</div>
<div class="sports-event-card__stat-value">{{row.restrictRegNumber != null ? row.restrictRegNumber + '项' : '暂无限制'}}</div>
</div>
</div>
<div class="sports-event-card__config" v-if="row.restrictBoyNum != null || row.restrictGirlNum != null || row.coachNum != null || row.leanderNum != null || row.substituteNum != null || row.isLeader || row.isManGirlNum">
<span class="sports-event-card__config-tag" v-if="row.restrictBoyNum != null">男子 {{row.restrictBoyNum}}人</span>
<span class="sports-event-card__config-tag" v-if="row.restrictGirlNum != null">女子 {{row.restrictGirlNum}}人</span>
<span class="sports-event-card__config-tag" v-if="row.coachNum != null">教练 {{row.coachNum}}人</span>
<span class="sports-event-card__config-tag" v-if="row.leanderNum != null">领队 {{row.leanderNum}}人</span>
<span class="sports-event-card__config-tag" v-if="row.substituteNum != null">替补 {{row.substituteNum}}人</span>
<span class="sports-event-card__config-tag" v-if="row.isLeader">仅报领队</span>
<span class="sports-event-card__config-tag" v-if="row.isManGirlNum">男女不限</span>
</div>
<div class="sports-event-card__extra">
<div class="sports-event-card__extra-row" v-if="row.startAgeDate || row.endAgeDate"><van-icon name="calendar-o"></van-icon><span class="sports-event-card__extra-label">出生日期</span><span class="sports-event-card__extra-value">{{row.startAgeDate || '不限'}} 至 {{row.endAgeDate || '不限'}}</span></div>
<div class="sports-event-card__extra-row" v-if="row.activityTeamName"><van-icon name="friends-o"></van-icon><span class="sports-event-card__extra-label">当前小队</span><span class="sports-event-card__extra-value">{{row.activityTeamName}}</span></div>
<div class="sports-event-card__extra-row" v-if="row.userApplyNames"><van-icon name="contact-o"></van-icon><span class="sports-event-card__extra-label">已报名人员</span><span class="sports-event-card__extra-value" @click="openApplyUser(row)">{{row.userApplyNames}}</span></div>
</div>
</div>
<div class="table-card-footer sports-event-card__footer">
<div v-if="row.applyWay.length===1&&row.applyWay.includes(1)">
<van-button class="sports-event-apply-button" :loading="subLoading" @click="signUpUser(row)" round
size="small"
type="primary" v-if="activityData.applyWay.includes(1)&&row.userApply===0">
<i class="fa fa-sign-in"></i>
我要报名
</van-button>
<van-button class="sports-event-cancel-button" :loading="subLoading" @click="cancelApply(row)" round
size="small"
type="primary" v-if="activityData.applyWay.includes(1)&&row.userApply>0">
<i class="fa fa-trash"></i>
取消报名
</van-button>
</div>
<div v-else>
<van-button class="sports-event-apply-button" :loading="subLoading" @click="signUpUser(row)" round
size="small"
type="primary" v-if="activityData.applyWay.includes(1)&&row.userApply2===0">
<i class="fa fa-sign-in"></i>
我要报名
</van-button>
<van-button class="sports-event-cancel-button" :loading="subLoading" @click="cancelApply(row)" round
size="small"
type="primary" v-if="activityData.applyWay.includes(1)&&row.userApply2>0">
<i class="fa fa-trash"></i>
取消报名
</van-button>
</div>
<span
v-if="row.applyWay.includes(2) && !row.applyWay.includes(1)"
class="sports-event-union-action-hint"
:class="{'is-authorized': canUnionApplyOnPc}">
{{ canUnionApplyOnPc ? '分工会报名请在PC端进行' : '该活动为分工会报名,暂无权限' }}
</span>
</div>
</div>
</div>
<div class="sports-event-compact-list" v-else>
<div :key="row.id" class="sports-event-compact-item" v-for="row in tableData">
<div class="sports-event-compact-item__name">{{ row.allName }}</div>
<div class="sports-event-compact-item__action">
<div v-if="row.applyWay.length===1&&row.applyWay.includes(1)">
<van-button class="sports-event-compact-button sports-event-compact-button--apply" :loading="subLoading" @click="signUpUser(row)" round
size="small" type="default"
v-if="activityData.applyWay.includes(1)&&row.userApply===0">
<i class="fa fa-sign-in"></i>我要报名
</van-button>
<van-button class="sports-event-compact-button sports-event-compact-button--cancel" :loading="subLoading" @click="cancelApply(row)" round
size="small" type="default"
v-if="activityData.applyWay.includes(1)&&row.userApply>0">
<i class="fa fa-trash"></i>取消报名
</van-button>
</div>
<div v-else>
<van-button class="sports-event-compact-button sports-event-compact-button--apply" :loading="subLoading" @click="signUpUser(row)" round
size="small" type="default"
v-if="activityData.applyWay.includes(1)&&row.userApply2===0">
<i class="fa fa-sign-in"></i>我要报名
</van-button>
<van-button class="sports-event-compact-button sports-event-compact-button--cancel" :loading="subLoading" @click="cancelApply(row)" round
size="small" type="default"
v-if="activityData.applyWay.includes(1)&&row.userApply2>0">
<i class="fa fa-trash"></i>取消报名
</van-button>
</div>
<span
v-if="row.applyWay.includes(2) && !row.applyWay.includes(1)"
class="sports-event-union-action-hint"
:class="{'is-authorized': canUnionApplyOnPc}">
{{ canUnionApplyOnPc ? '分工会报名请在PC端进行' : '该活动为分工会报名,暂无权限' }}
</span>
</div>
</div>
</div>
</van-list>
<div class="sports-event-empty" v-else>
<img src="/assets/mobile/img/activity/sports-empty.png" alt="体育活动项目空状态插画">
<div class="sports-event-empty__title">暂无可报名项目</div>
<div class="sports-event-empty__hint">当前活动暂未发布可报名项目</div>
</div>
</div>
</van-pull-refresh>
</div>
</div>
<script nonce="${cspNonce!}">
new Vue({
el: "#app",
store,
data() {
return {
tableData: [],
finished: false,
loading: false,
refreshing: false,
pageForm: {
activityId: GetQueryString("activityId"),
searchKeyword: "",
pageNumber: 1,
pageSize: 5,
totalCount: 0
},
subLoading: false,
activityData: {},
viewMode: "card"
}
},
components: {},
computed: {
canUnionApplyOnPc() {
return this.$auth.hasRoleOr([
"SYSADMIN",
"SCHOOL_UNION_ADMIN",
"BRANCH_UNION_CHAIRMAN",
"BRANCH_UNION_WENTI_SPORTS"
]) && this.$auth.hasPermission("activity.apply")
}
},
methods: {
toggleViewMode() {
this.viewMode = this.viewMode === "card" ? "list" : "card"
},
openApplyUser() {
},
cancelApply(row) {
this.$dialog.confirm({
title: "提示",
message: "确认要取消报名吗?"
}).then(() => {
/* if (row.applyUser !== this.$store.state.user.id) {
this.$dialog.alert({
title: '温馨提示',
message: "报名人不是您,请联系报名人取消!",
}).then(() => {
})
return;
}*/
this.$axios.post("/platform/activity/apply/h5/cancelApply", {
activityId: row.activityId,
eventId: row.eventId,
schoolEventId: row.id
}).then(resp => {
if (resp.code === 0) {
this.doSearch()
this.$dialog.alert({
title: '温馨提示',
message: resp.msg,
}).then(() => {
})
}
})
})
},
signUpUser(row) {
this.$dialog.confirm({
title: "提示",
message: "确认要报名吗?"
}).then(async () => {
const teamList = await this.listTeamList(row)
if (teamList.length > 0) {
row.teamId = teamList[0].id
}
this.$axios.post("/platform/activity/apply/h5/signUpUser", {
activityId: row.activityId,
teamId: row.teamId,
eventId: row.eventId,
schoolEventId: row.id
}).then(resp => {
if (resp.code === 0) {
this.doSearch()
this.$dialog.alert({
title: '温馨提示',
message: resp.msg,
}).then(() => {
// on close
});
}
})
})
},
async listTeamList(row) {
const resp = await this.$axios.post("/platform/activity/apply/h5/listTeamList", {
activityId: row.activityId,
eventId: row.eventId,
})
if (resp.code === 0) {
return resp.data
}
},
doSearch() {
this.tableKey = new Date().getTime()
this.pageForm.pageNumber = 1
this.pageForm.totalCount = 0
this.finished = false
this.tableData = []
this.pageData()
},
onRefresh() {
this.doSearch()
},
pageData() {
this.loading = true
this.$axios.post("/platform/activity/apply/h5/pageData", {data: JSON.stringify(this.pageForm)}).then((res) => {
this.tableData = this.tableData.concat(res.data.list)
if (this.tableData.length === res.data.totalCount) {
this.finished = true
} else {
this.pageForm.pageNumber++
}
this.pageForm.totalCount = res.data.totalCount
}).finally(() => {
this.loading = false
this.refreshing = false
})
},
findOneActivity() {
this.$axios.post("/platform/activity/apply/h5/findOneActivity", {activityId: this.pageForm.activityId}).then((res) => {
if (res.code === 0) {
this.activityData = res.data
}
})
}
},
created() {
this.findOneActivity()
this.pageData()
}
})
</script>
<!--#
}
#-->
@@ -0,0 +1,253 @@
let ACTIVITY_EVENT_NOTIFICATION = {
template: `
<div class="activity-notification-component">
<van-popup
v-model="viewShow"
class="activity-notification-popup"
:style="{ height: '100%', width: '100%' }"
position="right"
safe-area-inset-bottom
>
<div class="activity-notification-page">
<van-sticky>
<van-nav-bar
title="活动通知"
left-arrow
left-text="返回"
@click-left="viewShow = false"
></van-nav-bar>
</van-sticky>
<div class="activity-notification-scroll">
<div class="activity-notification-hero">
<div class="activity-notification-hero__icon">
<van-icon name="description"></van-icon>
</div>
<div>
<div class="activity-notification-hero__title">活动报名须知</div>
<div class="activity-notification-hero__subtitle">请仔细阅读活动安排与报名要求</div>
</div>
</div>
<article v-if="event_notification" class="activity-notification-card">
<div class="activity-notification-content" v-html="event_notification"></div>
</article>
<div v-else class="activity-notification-empty">
<van-icon name="description"></van-icon>
<span>暂无活动通知</span>
</div>
</div>
<div class="activity-notification-footer">
<van-button v-if="inApplyTime" type="primary" round block @click="openReg">进入报名</van-button>
<van-button v-else class="is-disabled" disabled round block>{{ applyStatusText }}</van-button>
</div>
</div>
</van-popup>
</div>
`,
props: {
event_notification: {
value: { type: String, default: "暂无" }
},
id: {
value: { type: String, default: "暂无" }
},
apply_start_time: {
value: { type: String, default: "" }
},
apply_end_time: {
value: { type: String, default: "" }
}
},
data() {
return {
viewShow: false
}
},
computed: {
// 详情页统一控制报名入口状态,活动列表仅负责进入详情。
inApplyTime() {
if (!this.apply_start_time || !this.apply_end_time) return false
const now = Date.now()
return now >= this.$moment(this.apply_start_time).valueOf()
&& now <= this.$moment(this.apply_end_time).valueOf()
},
applyStatusText() {
if (!this.apply_start_time || !this.apply_end_time) return "当前不可报名"
if (Date.now() < this.$moment(this.apply_start_time).valueOf()) return "报名未开始"
return "已过报名时间"
}
},
methods: {
openReg() {
this.$pjaxReplace("/platform/activity/apply/h5/eventList?activityId=" + this.id)
}
},
style: /*language=CSS*/ `
.activity-notification-popup {
background-color: #f3f7fd;
}
.activity-notification-page {
position: relative;
min-height: 100vh;
background-color: #f3f7fd;
}
.activity-notification-page /deep/ .van-nav-bar {
background-color: #fff;
}
.activity-notification-page /deep/ .van-nav-bar__title {
color: #1f2937;
font-size: 16px;
}
.activity-notification-page /deep/ .van-nav-bar .van-icon,
.activity-notification-page /deep/ .van-nav-bar__text {
color: #397fbd;
}
.activity-notification-scroll {
height: calc(100vh - 46px);
padding: 12px 12px calc(92px + env(safe-area-inset-bottom));
overflow-x: hidden;
overflow-y: auto;
box-sizing: border-box;
}
.activity-notification-hero {
display: flex;
min-height: 82px;
padding: 16px;
align-items: center;
box-sizing: border-box;
border-radius: 12px;
color: #fff;
background: linear-gradient(120deg, #2d7eea 0%, #5ba6f7 100%);
box-shadow: 0 8px 20px rgba(45, 126, 234, .18);
}
.activity-notification-hero__icon {
display: flex;
width: 44px;
height: 44px;
margin-right: 12px;
flex: 0 0 44px;
align-items: center;
justify-content: center;
border-radius: 11px;
color: #2d7eea;
background-color: rgba(255, 255, 255, .92);
font-size: 25px;
}
.activity-notification-hero__title {
font-size: 17px;
line-height: 24px;
font-weight: 600;
}
.activity-notification-hero__subtitle {
margin-top: 3px;
font-size: 12px;
line-height: 18px;
opacity: .9;
}
.activity-notification-card {
margin-top: 12px;
padding: 18px 16px;
overflow: hidden;
border-radius: 12px;
background-color: #fff;
box-shadow: 0 7px 20px rgba(55, 85, 126, .08);
}
.activity-notification-content {
color: #3f4c5e;
font-size: 14px;
line-height: 1.8;
white-space: normal;
word-break: break-word;
}
.activity-notification-content /deep/ h1,
.activity-notification-content /deep/ h2,
.activity-notification-content /deep/ h3 {
margin: 0 0 12px !important;
color: #1f2f43 !important;
font-size: 20px !important;
line-height: 1.5 !important;
font-weight: 600 !important;
}
.activity-notification-content /deep/ p {
margin: 0 0 8px !important;
font-size: 14px !important;
line-height: 1.8 !important;
}
.activity-notification-content /deep/ img {
max-width: 100% !important;
height: auto !important;
border-radius: 8px;
}
.activity-notification-content /deep/ table {
display: block;
max-width: 100%;
overflow-x: auto;
border-collapse: collapse;
}
.activity-notification-empty {
display: flex;
min-height: 180px;
margin-top: 12px;
align-items: center;
justify-content: center;
flex-direction: column;
row-gap: 10px;
border-radius: 12px;
color: #9aa7b8;
background-color: #fff;
box-shadow: 0 7px 20px rgba(55, 85, 126, .08);
font-size: 14px;
}
.activity-notification-empty /deep/ .van-icon {
color: #83b5f4;
font-size: 36px;
}
.activity-notification-footer {
position: fixed;
right: 0;
bottom: 0;
left: 0;
z-index: 5;
padding: 10px 12px calc(10px + env(safe-area-inset-bottom));
border-top: 1px solid #edf1f6;
background-color: rgba(255, 255, 255, .96);
box-sizing: border-box;
}
.activity-notification-footer /deep/ .van-button {
height: 42px;
border: 0;
font-size: 15px;
background-color: #1989fa;
box-shadow: 0 6px 16px rgba(25, 137, 250, .22);
}
.activity-notification-footer /deep/ .van-button.is-disabled,
.activity-notification-footer /deep/ .van-button--disabled {
color: #8b98aa !important;
background-color: #eef2f7 !important;
opacity: 1;
box-shadow: none;
}
`
}
@@ -0,0 +1,763 @@
<!--#
layout("/layouts/platform_h5.html"){
#-->
<style>
body {
background-color: #f3f7fd;
}
.brand-activity-page {
min-height: 100vh;
padding-bottom: calc(24px + env(safe-area-inset-bottom));
box-sizing: border-box;
background-color: #f3f7fd;
}
.brand-activity-page .van-nav-bar {
background-color: #fff;
}
.brand-activity-page .van-nav-bar__title {
color: #1f2937;
font-size: 16px;
}
.brand-activity-page .van-nav-bar .van-icon,
.brand-activity-page .van-nav-bar__text {
color: #397fbd;
}
.brand-activity-content {
padding: 12px 12px 0;
}
.brand-activity-hero {
position: relative;
height: 134px;
overflow: hidden;
border-radius: 10px;
color: #fff;
background-color: #247fe8;
box-shadow: 0 8px 20px rgba(51, 117, 219, .18);
}
.brand-activity-hero__image {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
object-fit: cover;
object-position: center;
}
.brand-activity-hero__copy {
position: relative;
z-index: 1;
display: flex;
width: 64%;
height: 100%;
padding-left: 16px;
flex-direction: column;
justify-content: center;
box-sizing: border-box;
background: linear-gradient(90deg, rgba(22, 112, 232, .98) 0%, rgba(22, 112, 232, .88) 52%, rgba(22, 112, 232, 0) 100%);
}
.brand-activity-hero__title {
font-size: 25px;
line-height: 34px;
font-weight: 700;
letter-spacing: 1px;
text-shadow: 0 2px 6px rgba(17, 86, 182, .18);
}
.brand-activity-hero__subtitle {
margin-top: 4px;
font-size: 13px;
line-height: 20px;
letter-spacing: .5px;
opacity: .95;
}
.brand-activity-sticky {
padding: 10px 0 1px;
background-color: #f3f7fd;
}
.van-sticky--fixed .brand-activity-sticky {
padding-right: 12px;
padding-left: 12px;
box-sizing: border-box;
}
.brand-activity-search {
margin: 0 0 8px;
padding: 0;
overflow: hidden;
border-radius: 12px;
background-color: #fff;
box-shadow: 0 5px 16px rgba(43, 73, 112, .1);
}
.brand-activity-search .van-search__content {
height: 42px;
padding-left: 12px;
align-items: center;
border-radius: 12px;
background-color: #fff;
}
.brand-activity-search .van-field__control {
color: #263548;
font-size: 14px;
}
.brand-activity-search .van-field__control::placeholder {
color: #a7b0bf;
}
.brand-activity-filter {
margin: 0 0 10px;
overflow: hidden;
border-radius: 10px;
box-shadow: 0 5px 16px rgba(43, 73, 112, .08);
transition: border-radius .18s ease;
}
.brand-activity-filter.is-open {
border-radius: 10px 10px 0 0;
}
.brand-activity-filter .van-dropdown-menu__bar {
height: 44px;
box-shadow: none;
}
.brand-activity-filter .van-dropdown-menu__item {
min-width: 0;
overflow: hidden;
}
.brand-activity-filter .van-dropdown-menu__title {
width: auto;
max-width: calc(100% - 24px);
padding: 0 12px 0 0;
overflow: hidden;
color: #66758a;
font-size: 14px;
text-align: center;
text-overflow: ellipsis;
white-space: nowrap;
box-sizing: border-box;
}
.brand-activity-filter .van-dropdown-menu__title::after {
right: 0;
}
.brand-activity-filter .van-dropdown-menu__title--active,
.brand-activity-filter .van-dropdown-item__option--active,
.brand-activity-filter .van-dropdown-item__option--active .van-dropdown-item__icon {
color: #1989fa;
}
.brand-activity-filter .van-dropdown-menu__title--down::after {
border-color: transparent transparent #1989fa #1989fa;
}
.brand-activity-filter .van-dropdown-item__content {
width: calc(100% - 24px);
margin: 0 12px;
overflow: hidden;
box-sizing: border-box;
border-radius: 0 0 12px 12px;
box-shadow: 0 10px 24px rgba(31, 65, 108, .14);
}
.brand-activity-filter .van-dropdown-item__option {
min-height: 48px;
padding: 0 16px;
color: #46566c;
font-size: 14px;
}
.brand-activity-list {
display: flex;
flex-direction: column;
gap: 10px;
}
.brand-activity-card {
position: relative;
overflow: hidden;
border-radius: 10px;
background-color: #fff;
box-shadow: 0 7px 20px rgba(55, 85, 126, .08);
cursor: pointer;
}
.brand-activity-card__cover,
.brand-activity-card__cover-state {
display: flex;
width: 100%;
height: 150px;
align-items: center;
justify-content: center;
box-sizing: border-box;
background-color: #edf4fc;
}
.brand-activity-card__cover {
display: block;
object-fit: cover;
}
.brand-activity-card__cover-state {
flex-direction: column;
row-gap: 8px;
color: #8da1ba;
font-size: 13px;
}
.brand-activity-card__cover-state .van-icon {
font-size: 34px;
}
.brand-activity-card__cover-state.is-error {
color: #c27b75;
background-color: #fff3f1;
}
.brand-activity-card__body {
padding: 12px 38px 14px 14px;
}
.brand-activity-card__title {
min-width: 0;
overflow: hidden;
color: #303846;
font-size: 14px;
line-height: 21px;
text-overflow: ellipsis;
white-space: nowrap;
}
.brand-activity-card__time {
margin-top: 4px;
overflow: hidden;
color: #98a2b2;
font-size: 12px;
line-height: 17px;
text-overflow: ellipsis;
white-space: nowrap;
}
.brand-activity-card__meta {
display: flex;
margin-top: 10px;
padding-top: 9px;
flex-wrap: wrap;
gap: 6px;
border-top: 1px solid #edf1f6;
}
.brand-activity-card__tag {
display: inline-flex;
padding: 3px 8px;
align-items: center;
border-radius: 4px;
color: #397de0;
background-color: #edf5ff;
font-size: 11px;
line-height: 16px;
}
.brand-activity-card__tag-label {
margin-right: 4px;
color: #8aa7cf;
}
.brand-activity-card__arrow {
position: absolute;
top: 164px;
right: 14px;
color: #b4bdca;
font-size: 16px;
}
.brand-activity-empty {
display: flex;
min-height: 260px;
padding: 26px 18px 32px;
align-items: center;
justify-content: center;
flex-direction: column;
box-sizing: border-box;
text-align: center;
}
.brand-activity-empty img {
display: block;
width: 82%;
max-width: 280px;
height: auto;
object-fit: contain;
}
.brand-activity-empty__title {
margin-top: 14px;
color: #50627a;
font-size: 15px;
line-height: 22px;
font-weight: 600;
}
.brand-activity-empty__hint {
margin-top: 5px;
color: #9aa7b8;
font-size: 12px;
line-height: 18px;
}
.brand-activity-page .van-list__loading {
color: #9aa7b8;
font-size: 13px;
line-height: 44px;
}
.brand-intro-popup,
.brand-intro-page {
background-color: #f3f7fd;
}
.brand-intro-page {
position: relative;
min-height: 100vh;
}
.brand-intro-page .van-nav-bar {
background-color: #fff;
}
.brand-intro-page .van-nav-bar__title {
color: #1f2937;
font-size: 16px;
}
.brand-intro-page .van-nav-bar .van-icon,
.brand-intro-page .van-nav-bar__text {
color: #397fbd;
}
.brand-intro-scroll {
height: calc(100vh - 46px);
padding: 12px 12px calc(92px + env(safe-area-inset-bottom));
overflow-x: hidden;
overflow-y: auto;
box-sizing: border-box;
}
.brand-intro-hero {
display: flex;
min-height: 82px;
padding: 16px;
align-items: center;
box-sizing: border-box;
border-radius: 12px;
color: #fff;
background: linear-gradient(120deg, #2d7eea 0%, #5ba6f7 100%);
box-shadow: 0 8px 20px rgba(45, 126, 234, .18);
}
.brand-intro-hero__icon {
display: flex;
width: 44px;
height: 44px;
margin-right: 12px;
flex: 0 0 44px;
align-items: center;
justify-content: center;
border-radius: 11px;
color: #2d7eea;
background-color: rgba(255, 255, 255, .92);
font-size: 25px;
}
.brand-intro-hero__title {
font-size: 17px;
line-height: 24px;
font-weight: 600;
}
.brand-intro-hero__subtitle {
margin-top: 3px;
font-size: 12px;
line-height: 18px;
opacity: .9;
}
.brand-intro-card {
margin-top: 12px;
padding: 18px 16px;
overflow: hidden;
border-radius: 12px;
background-color: #fff;
box-shadow: 0 7px 20px rgba(55, 85, 126, .08);
}
.brand-intro-content,
.brand-intro-content div,
.brand-intro-content p,
.brand-intro-content span,
.brand-intro-content li {
color: #3f4c5e !important;
font-size: 14px !important;
line-height: 1.8 !important;
white-space: normal;
word-break: break-word;
}
.brand-intro-content h1,
.brand-intro-content h2,
.brand-intro-content h3 {
margin: 16px 0 10px !important;
color: #1f2f43 !important;
font-size: 19px !important;
line-height: 1.5 !important;
font-weight: 600 !important;
}
.brand-intro-content > :first-child {
margin-top: 0 !important;
}
.brand-intro-content p {
margin-top: 0 !important;
margin-bottom: 8px !important;
}
.brand-intro-content img {
max-width: 100% !important;
height: auto !important;
border-radius: 8px;
}
.brand-intro-content table {
display: block;
max-width: 100%;
overflow-x: auto;
border-collapse: collapse;
}
.brand-intro-empty {
display: flex;
min-height: 180px;
margin-top: 12px;
align-items: center;
justify-content: center;
flex-direction: column;
row-gap: 10px;
border-radius: 12px;
color: #9aa7b8;
background-color: #fff;
box-shadow: 0 7px 20px rgba(55, 85, 126, .08);
font-size: 14px;
}
.brand-intro-empty .van-icon {
color: #83b5f4;
font-size: 36px;
}
.brand-intro-footer {
position: fixed;
right: 0;
bottom: 0;
left: 0;
z-index: 5;
padding: 10px 12px calc(10px + env(safe-area-inset-bottom));
border-top: 1px solid #edf1f6;
background-color: rgba(255, 255, 255, .96);
box-sizing: border-box;
}
.brand-intro-footer .van-button {
height: 42px;
border: 0;
border-radius: 21px;
font-size: 15px;
background-color: #1989fa;
box-shadow: 0 6px 16px rgba(25, 137, 250, .22);
}
.brand-intro-footer .van-button--disabled {
color: #8b98aa !important;
background-color: #eef2f7 !important;
opacity: 1;
box-shadow: none;
}
.brand-intro-footer .van-count-down {
display: inline-block;
margin-left: 4px;
color: #fff;
font-size: 13px;
vertical-align: middle;
}
</style>
<div id="app" v-cloak>
<div class="brand-activity-page">
<van-nav-bar title="品牌活动" left-text="返回" left-arrow @click-left="historyBack" fixed placeholder></van-nav-bar>
<div class="brand-activity-content">
<section class="brand-activity-hero">
<img class="brand-activity-hero__image" src="/assets/mobile/img/activity/hero/brand-hero-v2.png" alt="品牌活动插画">
<div class="brand-activity-hero__copy">
<div class="brand-activity-hero__title">品牌活动</div>
<div class="brand-activity-hero__subtitle">品牌引领 · 成长同行</div>
</div>
</section>
<van-sticky :offset-top="46">
<div class="brand-activity-sticky">
<van-search
v-model="pageForm.searchKeyword"
class="brand-activity-search"
placeholder="搜索品牌活动"
shape="round"
clearable
@search="doSearch"
@clear="doSearch"
></van-search>
<van-dropdown-menu class="brand-activity-filter" :class="{'is-open': filterOpened}" active-color="#1989fa">
<year-van-dropdown-item :num="5" @change="doSearch" @open="filterOpened = true" @close="filterOpened = false" v-model="pageForm.year"></year-van-dropdown-item>
<van-dropdown-item v-model="pageForm.activityType" :options="typeOptions" :multiple="false" @change="doSearch" @open="filterOpened = true" @close="filterOpened = false"></van-dropdown-item>
</van-dropdown-menu>
</div>
</van-sticky>
<van-pull-refresh v-model="refreshing" @refresh="onRefresh">
<van-list v-model="loading" :finished="finished" :immediate-check="false" @load="onLoad">
<div class="brand-activity-list">
<div v-for="row in tableData" :key="row.id" class="brand-activity-card" @click="onView(row)">
<img
v-if="hasCover(row) && !imageLoadErrors[row.id]"
class="brand-activity-card__cover"
:src="row.cover"
:alt="row.activityName"
@error="handleImageError(row)"
>
<div v-else class="brand-activity-card__cover-state" :class="{'is-error': hasCover(row)}">
<van-icon name="photo-o"></van-icon>
<span>{{ hasCover(row) ? '图片加载失败' : '暂无图片' }}</span>
</div>
<div class="brand-activity-card__body">
<div class="brand-activity-card__title">{{ row.activityName }}</div>
<div class="brand-activity-card__time" v-if="row.activitySignUpStartTime && row.activitySignUpEndTime">
报名时间:{{ $moment(row.activitySignUpStartTime).format('MM/DD HH:mm') }}~{{ $moment(row.activitySignUpEndTime).format('MM/DD HH:mm') }}
</div>
<div class="brand-activity-card__time" v-if="row.activityStartTime && row.activityEndTime">
活动时间:{{ $moment(row.activityStartTime).format('MM/DD HH:mm') }}~{{ $moment(row.activityEndTime).format('MM/DD HH:mm') }}
</div>
<div class="brand-activity-card__meta" v-if="row.activityGroupName || row.trainType">
<span class="brand-activity-card__tag" v-if="row.activityGroupName">
<span class="brand-activity-card__tag-label">面向对象</span>{{ row.activityGroupName }}
</span>
<span class="brand-activity-card__tag" v-if="row.trainType">
<span class="brand-activity-card__tag-label">活动类型</span>{{ row.trainType }}
</span>
</div>
</div>
<van-icon class="brand-activity-card__arrow" name="arrow"></van-icon>
</div>
</div>
</van-list>
<div v-if="!loading && tableData.length === 0" class="brand-activity-empty">
<img src="/assets/mobile/img/activity/culture-empty.png" alt="品牌活动空状态插画">
<div class="brand-activity-empty__title">暂无品牌活动</div>
<div class="brand-activity-empty__hint">当前筛选条件下暂时没有可展示的活动</div>
</div>
</van-pull-refresh>
</div>
</div>
<van-popup class="brand-intro-popup" :style="{height: '100%', width: '100%'}" position="right" safe-area-inset-bottom v-model="infoVisible">
<div class="brand-intro-page">
<van-sticky>
<van-nav-bar title="活动介绍" left-text="返回" left-arrow @click-left="infoVisible = false"></van-nav-bar>
</van-sticky>
<div class="brand-intro-scroll">
<div class="brand-intro-hero">
<div class="brand-intro-hero__icon">
<van-icon name="description"></van-icon>
</div>
<div>
<div class="brand-intro-hero__title">活动报名须知</div>
<div class="brand-intro-hero__subtitle">请仔细阅读活动安排与报名要求</div>
</div>
</div>
<article v-if="infoRow.introduce" class="brand-intro-card">
<div class="brand-intro-content" v-html="infoRow.introduce"></div>
</article>
<div v-else class="brand-intro-empty">
<van-icon name="description"></van-icon>
<span>暂无活动介绍</span>
</div>
</div>
<div class="brand-intro-footer">
<van-button v-if="canApply(infoRow)" @click="onApply(infoRow)" type="primary" block round>进入报名</van-button>
<van-button v-else-if="time < 0" @click="onApply(infoRow)" type="primary" block round>
距离开始
<van-count-down :time="Math.abs(time)" format="DD 天 HH 时 mm 分 ss 秒" @finish="time = 0"></van-count-down>
</van-button>
<van-button v-else disabled block round>{{ registrationStatusText(infoRow) }}</van-button>
</div>
</div>
</van-popup>
</div>
<script nonce="${cspNonce!}">
const vue = new Vue({
el: "#app",
store,
components: {
},
data() {
return {
time: 0,
tableData: [],
loading: false,
finished: false,
refreshing: false,
imageLoadErrors: {},
filterOpened: false,
pageForm: {
pageNumber: 1,
pageSize: 10,
totalCount: 0,
searchKeyword: '',
year: new Date().getFullYear(),
activityType: 2,
},
typeOptions: [
{text: '全部', value: 1},
{text: '即将开始 & 报名中', value: 2},
{text: '已结束', value: 3},
],
infoVisible: false,
infoRow: {},
id: GetQueryString('id')
}
},
methods: {
hasCover(row) {
return !!(row && row.cover && String(row.cover).trim())
},
handleImageError(row) {
this.$set(this.imageLoadErrors, row.id, true)
},
isRegistrationClosed(row) {
return !!(row && row.activitySignUpEndTime && !this.$moment().isBefore(this.$moment(row.activitySignUpEndTime)))
},
canApply(row) {
if(!row || !row.activitySignUpStartTime || !row.activitySignUpEndTime) {
return false
}
const now = Date.now()
return now >= this.$moment(row.activitySignUpStartTime).valueOf()
&& now <= this.$moment(row.activitySignUpEndTime).valueOf()
},
registrationStatusText(row) {
if(!row || !row.activitySignUpStartTime || !row.activitySignUpEndTime) {
return '当前不可报名'
}
if(Date.now() < this.$moment(row.activitySignUpStartTime).valueOf()) {
return '报名未开始'
}
return '已过报名时间'
},
onView(row) {
this.infoRow = row
this.time = this.$moment().diff(this.$moment(row.activitySignUpStartTime), 'milliseconds')
this.infoVisible = true
},
onApply(row) {
if(this.isRegistrationClosed(row)) {
return
}
if(this.$moment().isBefore(this.$moment(row.activitySignUpStartTime))) {
this.onView(row)
return
}
// 先校验年度同活动类型报名次数,避免进入课程页后才提示不可报名。
this.$axios.post('/platform/trainSignUp/apply/validateYearTrainTypeSignUp', {activityId: row.id}).then((res) => {
if (res.code !== 0) {
vant.Dialog.alert({
title: '提示',
message: res.msg,
confirmButtonColor: '#1867b0'
})
return
}
this.$pjaxReplace('/platform/trainSignUp/apply/list/h5?id=' + row.id)
})
},
fetchOne() {
this.$axios.post('/platform/trainSignUp/manage/findOne', {id: this.id}).then((res) => {
if(res.code === 0) {
this.onView(res.data)
}
})
},
onLoad() {
if(!this.finished) {
this.loadActivities(false)
}
},
onRefresh() {
this.loadActivities(true)
},
loadActivities(reset) {
if(reset) {
this.pageForm.pageNumber = 1
this.pageForm.totalCount = 0
this.tableData = []
this.finished = false
this.imageLoadErrors = {}
}
this.loading = true
this.$axios.post('/platform/trainSignUp/apply/activityPageData', this.pageForm).then((res) => {
if(res.code === 0) {
const list = res.data && res.data.list ? res.data.list : []
this.tableData = this.pageForm.pageNumber === 1 ? list : this.tableData.concat(list)
this.pageForm.totalCount = res.data && res.data.totalCount ? res.data.totalCount : 0
this.finished = this.tableData.length >= this.pageForm.totalCount
if(!this.finished) {
this.pageForm.pageNumber++
}
}
}).finally(() => {
this.loading = false
this.refreshing = false
})
},
doSearch() {
this.loadActivities(true)
}
},
created() {
this.loadActivities(true)
if(this.id) {
this.fetchOne()
}
}
})
</script>
<!--#
}
#-->
@@ -0,0 +1,286 @@
const times = {
template:
/*language=HTML*/
`
<div>
<van-action-sheet class="brand-course-times-sheet" :style="{height: '78%'}" :close-on-click-overlay="true" v-model="visible" title="时间段" cancel-text="取消">
<div class="brand-course-times-list">
<div id="mapContainer" v-if="row?.isMobileSign === true && row?.signType === 3" class="brand-course-times-map"></div>
<button v-for="(item,index) in row?.courseTimes" :key="index" type="button" class="van-action-sheet__item brand-course-time-item">
<div style="display: flex; justify-content: space-between; align-items: center">
<div>
<div class="van-action-sheet__name">
<label>{{ weekdayCNMap[$moment(item.courseDate).day()] }}</label>
<label>{{ $moment(item.courseDate).format('YYYY-MM-DD') }}</label>
</div>
<div class="van-action-sheet__subname">
{{$moment(item.courseStartTime).format('MM-DD HH:mm') + ' 至 ' + $moment(item.courseEndTime).format('MM-DD HH:mm')}}
</div>
</div>
<div v-if="row.isSign === true && row.isMobileSign === true" class="sign_button">
<van-button v-if="item.isAttend !== true" @click.stop="onSign(item)" size="mini" type="info">签到</van-button>
<van-button v-if="item.isAttend === true" size="mini" type="info" disabled>已签到</van-button>
</div>
</div>
</button>
</div>
</van-action-sheet>
<van-popup round :safe-area-inset-bottom="true"
:close-on-click-overlay="false"
v-model="signVisible"
:style="{ width: '80%', height: '66%' }"
get-container="#app"
@close="onSignClose"
closeable
>
<scan-code ref="scanCodeRef"></scan-code>
</van-popup>
</div>
`,
store,
data() {
return {
visible:false,
row: null,
weekdayCNMap: {0: '周日', 1: '周一', 2: '周二', 3: '周三', 4: '周四', 5: '周五', 6: '周六'},
selectCourseTime: {},
signVisible: false,
markerLayer: null,
map: null,
circle: null,
}
},
components: {
"scan-code": httpVueLoader("/components/plugins/sysScanCode/index.vue?v=" + new Date().getTime())
},
methods: {
onSignClose() {
this.$refs.scanCodeRef.closeScan()
},
onOpen(row) {
this.row = row
this.visible = true
this.$nextTick(() => {
if(this.row.signType === 3) {
if(this.map) this.map.destroy()
this.initMap()
this.getLocation((coords) => {
if (coords) {
const center = new TMap.LatLng(coords.lat, coords.lng)
this.markerLayer.remove(["current"])
this.createMarker(center, 'current', 'current')
}
})
}
})
},
async onSign(courseTime) {
this.selectCourseTime = courseTime
if (this.row.signType === 1) {
this.signVisible = true
this.$nextTick(() => {
this.$refs.scanCodeRef.init()
})
}
if (this.row.signType === 2) {
this.makeCode()
}
if (this.row.signType === 3) {
this.getLocation((coords) => {
if (coords) {
this.$axios.post('/platform/trainSignUp/mine/positionSign', {
timeId: courseTime.id,
courseId: this.row.id,
lat: coords.lat,
lng: coords.lng
}).then(res => {
if (res.code === 0) {
this.$toast('签到成功')
this.visible = false
} else {
this.$toast(res.msg)
this.markerLayer.remove(["current"])
this.getLocation((coords) => {
if (coords) {
const center = new TMap.LatLng(coords.lat, coords.lng)
this.createMarker(center, 'current', 'current')
}
})
}
})
}
})
}
},
getLocation(callback) {
if (typeof callback !== 'function') {
callback = () => {};
}
if (!navigator.geolocation) {
this.$toast('当前浏览器不支持定位功能');
callback(null);
return;
}
const loading = vant.Toast.loading({
message: "获取定位中...",
forbidClick: false,
loadingType: "spinner",
duration: 0,
})
navigator.geolocation.getCurrentPosition(
(position) => {
callback({
lat: position.coords.latitude,
lng: position.coords.longitude
});
loading.close()
},
(error) => {
let msg = '定位失败,请稍后重试';
switch (error.code) {
case error.PERMISSION_DENIED:
msg = '请允许浏览器获取位置信息';
break;
case error.POSITION_UNAVAILABLE:
msg = '无法获取当前位置';
break;
case error.TIMEOUT:
msg = '定位超时,请重试';
break;
}
this.$toast(msg);
callback(null);
loading.close()
},
{
enableHighAccuracy: true,
timeout: 10000,
maximumAge: 60000
}
);
},
initMap() {
if(!this.row.courseLocationCoordinates) {
this.$toast('未设置签到点')
return
}
const posi = JSON.parse(this.row.courseLocationCoordinates)
const center = new TMap.LatLng(posi[0], posi[1])
this.map = new TMap.Map('mapContainer', {
resizeEnable: true,
zoom: 16,
center: center
})
this.markerLayer = new TMap.MultiMarker({
map: this.map,
styles: {
"current": new TMap.MarkerStyle({
"src": "https://mapapi.qq.com/web/bundles/lbs-home/prod/assets/markerActive-NhH9NoBV.png",
"width": 40, // 点标记样式宽度(像素)
})
},
geometries: []
})
this.createMarker(center)
this.createCircle(center)
},
createMarker(position, styleId = 'marker', id = 'marker_' + Date.now()) {
this.markerLayer.add([
{
id: id,
position: position,
styleId: styleId
}
]);
},
createCircle(position) {
this.circle = new TMap.MultiCircle({
map: this.map,
geometries: [{
center: position,
radius: this.row.radius || 100,
}],
});
},
makeCode() {
const url = '/platform/trainSignUp/mine/passiveScan'
const data = url + '?id=' + this.selectCourseTime.id
console.log(data)
const content = jrQrcode.getQrBase64(data)
vant.ImagePreview([content])
},
onClose(){
this.visible = false
},
},
style: /*language=CSS*/ `
.brand-course-times-sheet {
overflow: hidden;
border-radius: 16px 16px 0 0;
background-color: #f3f7fd;
}
.brand-course-times-sheet /deep/ .van-action-sheet__header {
height: 48px;
color: #1f2f43;
background-color: #fff;
font-size: 16px;
line-height: 48px;
}
.brand-course-times-sheet /deep/ .van-action-sheet__close {
top: 13px;
color: #b5bdc9;
font-size: 20px;
}
.brand-course-times-list {
height: calc(100% - 98px);
padding: 10px 12px 0;
overflow-y: auto;
box-sizing: border-box;
}
.brand-course-times-map {
width: 100%;
height: 180px;
margin-bottom: 10px;
overflow: hidden;
border-radius: 10px;
}
.brand-course-times-sheet /deep/ .brand-course-time-item {
width: 100%;
margin-bottom: 10px;
padding: 14px;
border-radius: 10px;
color: #263548;
background-color: #fff;
box-shadow: 0 6px 18px rgba(55, 85, 126, .07);
box-sizing: border-box;
}
.brand-course-times-sheet /deep/ .van-action-sheet__name {
color: #263548;
font-size: 14px;
line-height: 21px;
}
.brand-course-times-sheet /deep/ .van-action-sheet__subname {
margin-top: 5px;
color: #98a2b2;
font-size: 12px;
line-height: 18px;
}
.brand-course-times-sheet /deep/ .van-action-sheet__cancel {
height: 50px;
color: #66758a;
background-color: #fff;
font-size: 14px;
}
`
}
@@ -0,0 +1,239 @@
const applyForm = {
template:
/*language=HTML*/
`
<div>
<van-action-sheet class="brand-course-form-sheet" :style="{height: '78%'}" :close-on-click-overlay="true" title="报名信息" v-model="visible">
<van-form ref="formRef" class="form-container">
<van-cell-group title="活动信息" class="form-section">
<van-field label="活动名称" readonly v-model="row.courseName"></van-field>
<van-field label="校区" readonly v-model="row.campus"></van-field>
<van-field label="活动地点" readonly v-model="row.courseLocation"></van-field>
</van-cell-group>
<van-cell-group title="基础信息" class="form-section">
<van-field label="姓名" readonly v-model="formData.username"></van-field>
<van-field label="工号" readonly v-model="formData.loginname"></van-field>
<van-field label="所属单位" readonly v-model="formData.unitName"></van-field>
<van-field label="所属工会" readonly v-model="formData.unionName"></van-field>
<van-field label="性别" readonly v-model="formData.sex"></van-field>
<template v-if="row.courseIsLimitApply">
<van-field label="报名时段"
required
:rules="[{ required: true, message: '请选择报名时段' }]"
readonly
@click="showCoursePicker = true"
placeholder="请选择报名时段"
name="courseTimeName"
v-model="formData.courseTimeName">
</van-field>
<van-popup v-model="showCoursePicker" position="bottom">
<van-picker
show-toolbar
:columns="courseTimeSelectList"
@confirm="onCourseConfirm"
@cancel="showCoursePicker=false"
></van-picker>
</van-popup>
</template>
<train-dynamic-form v-model="dynamicColumnsData" ref="dynamicForm"></train-dynamic-form>
</van-cell-group>
<div class="button">
<van-button @click="onSubmit" round type="info" block>提交</van-button>
</div>
</van-form>
</van-action-sheet>
</div>
`,
data() {
return {
row: {},
activity: {},
dynamicColumnsData: [],
visible: false,
formData: {},
showCoursePicker: false,
courseTimeSelectList: [],
}
},
components: {
"train-dynamic-form": httpVueLoader("/components/module/trainSignUp/TrainDynamicForm.vue")
},
methods: {
async onOpen(row, courseType) {
this.row = row
this.init(row, courseType)
if(row.courseIsLimitApply) {
await this.getCourseTimeSelectList(row)
}
this.visible = true
},
init(row, courseType) {
this.$set(this.formData, 'username', this.$store.state.user.username)
this.$set(this.formData, 'loginname', this.$store.state.user.loginname)
this.$set(this.formData, 'unionName', this.$store.state.user.union.name)
this.$set(this.formData, 'unitName', this.$store.state.user.unit.name)
this.$set(this.formData, 'sex', this.$store.state.user.sex)
this.$set(this.formData, 'activityId', row.activityId)
this.$set(this.formData, 'courseId', row.id)
this.dynamicColumnsData = courseType ? courseType.trainMobileSignColumnList : []
this.dynamicColumnsData.forEach((item) => {
item.columnValue = this.$store.state.user[item.columnCode] || ""
})
},
onCourseConfirm(val){
this.formData.activityCourseId = val.value
this.$set(this.formData, "activityCourseId", val.value)
this.$set(this.formData, "courseTimeName", val.text.substring(0, 12))
this.showCoursePicker = false
},
async getCourseTimeSelectList(o) {
const resp = await this.$axios.post('/platform/trainSignUp/apply/getCourseTimeSelectList',{courseId: o.id})
if (resp.code === 0) {
this.courseTimeSelectList = resp.data
} else {
this.$toast.fail("获取时段信息失败,请联系管理员")
}
},
async validateSignUp() {
// 验证自定义表单
await this.$refs.dynamicForm.$refs.form.validate()
// 获取家属人数
let familyCount = this.dynamicColumnsData.filter(o => o.columnCode === 'xdqsrs').reduce((sum, item) => {
return sum + (Number(item.columnValue) || 0)
}, 0)
const res = await this.$axios.post("/platform/trainSignUp/apply/validateSignUp", {
courseId: this.formData.courseId,
currentFamilyNumber: familyCount
})
return res.code === 0
},
async validateCourseTime() {
const res = await this.$axios.post('/platform/trainSignUp/apply/validateSourceSignUp', {
activityCourseId: this.formData.activityCourseId,
courseId: this.row.id
})
return res.code === 0
},
async onSubmit() {
if(!await this.validateSignUp()) return
this.$refs.formRef.validate().then(() => {
this.$dialog.confirm({
title: "提示",
message: "您确定要提交吗?"
}).then(async () => {
if (this.row.courseIsLimitApply) {
if(!await this.validateCourseTime()) return
}
const mobileColumnsValue = this.dynamicColumnsData.map((v) => {
return {
columnName: v.columnName,
columnValue: v.columnValue,
columnCode: v.columnCode,
columnFormType: v.columnFormType
}
})
this.formData.mobileColumnsValue = JSON.stringify(mobileColumnsValue)
this.$axios.post("/platform/trainSignUp/apply/doSignUp", this.formData).then(res => {
if (res.code === 0) {
this.$toast(res.msg)
this.visible = false
this.$emit('refresh')
}
})
})
})
},
},
style: /*language=CSS*/ `
.brand-course-form-sheet {
overflow: hidden;
border-radius: 16px 16px 0 0;
background-color: #f3f7fd;
}
.brand-course-form-sheet /deep/ .van-action-sheet__header {
height: 48px;
color: #1f2f43;
background-color: #fff;
font-size: 16px;
line-height: 48px;
}
.brand-course-form-sheet /deep/ .van-action-sheet__close {
top: 13px;
color: #b5bdc9;
font-size: 20px;
}
.brand-course-form-sheet /deep/ .form-container {
height: calc(100% - 48px);
padding: 10px 12px 72px;
overflow-y: auto;
background-color: #f3f7fd;
box-sizing: border-box;
}
.brand-course-form-sheet /deep/ .form-section {
margin-bottom: 10px;
overflow: hidden;
border-radius: 10px;
background-color: #fff;
box-shadow: 0 6px 18px rgba(55, 85, 126, .07);
}
.brand-course-form-sheet /deep/ .van-cell-group__title {
padding: 12px 14px 8px;
color: #263548;
background-color: #fff;
font-size: 14px;
line-height: 20px;
font-weight: 600;
}
.brand-course-form-sheet /deep/ .van-cell-group__title::before {
width: 3px;
top: 12px;
bottom: 8px;
border-radius: 2px;
background-color: #1989fa;
}
.brand-course-form-sheet /deep/ .van-cell {
padding: 11px 14px;
color: #263548;
font-size: 13px;
}
.brand-course-form-sheet /deep/ .van-field__label {
color: #66758a;
font-size: 13px;
}
.brand-course-form-sheet /deep/ .button {
position: absolute;
right: 0;
bottom: 0;
left: 0;
z-index: 2;
padding: 10px 12px calc(10px + env(safe-area-inset-bottom));
border-top: 1px solid #edf1f6;
background-color: rgba(255, 255, 255, .96);
box-sizing: border-box;
}
.brand-course-form-sheet /deep/ .button .van-button {
height: 42px;
border: 0;
border-radius: 21px;
color: #fff;
background-color: #1989fa;
font-size: 15px;
box-shadow: 0 6px 16px rgba(25, 137, 250, .22);
}
`
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,550 @@
<!--#
layout("/layouts/platform_h5.html"){
#-->
<style>
body {
background-color: #f3f7fd;
}
.brand-mine-page {
min-height: 100vh;
padding-bottom: calc(24px + env(safe-area-inset-bottom));
box-sizing: border-box;
background-color: #f3f7fd;
}
.brand-mine-page .van-nav-bar {
background-color: #fff;
}
.brand-mine-page .van-nav-bar__title {
color: #1f2937;
font-size: 16px;
}
.brand-mine-page .van-nav-bar .van-icon,
.brand-mine-page .van-nav-bar__text {
color: #397fbd;
}
.brand-mine-content {
padding: 12px 12px 0;
}
.brand-mine-hero {
position: relative;
height: 134px;
overflow: hidden;
border-radius: 10px;
color: #fff;
background: linear-gradient(115deg, #347eea 0%, #5b9cf4 58%, #cfe3ff 100%);
box-shadow: 0 8px 20px rgba(51, 117, 219, .18);
}
.brand-mine-hero__image {
position: absolute;
top: 0;
right: 0;
width: 67%;
height: 100%;
object-fit: cover;
object-position: right center;
-webkit-mask-image: linear-gradient(90deg, transparent 0%, #000 26%);
mask-image: linear-gradient(90deg, transparent 0%, #000 26%);
}
.brand-mine-hero__copy {
position: relative;
z-index: 1;
display: flex;
height: 100%;
padding-left: 16px;
flex-direction: column;
justify-content: center;
box-sizing: border-box;
}
.brand-mine-hero__title {
font-size: 25px;
line-height: 34px;
font-weight: 700;
letter-spacing: 1px;
}
.brand-mine-hero__subtitle {
margin-top: 4px;
font-size: 13px;
line-height: 20px;
letter-spacing: .5px;
opacity: .95;
}
.brand-mine-sticky {
padding: 10px 0 1px;
background-color: #f3f7fd;
}
.van-sticky--fixed .brand-mine-sticky {
padding-right: 12px;
padding-left: 12px;
box-sizing: border-box;
}
.brand-mine-search {
margin-bottom: 8px;
padding: 0;
overflow: hidden;
border-radius: 12px;
background-color: #fff;
box-shadow: 0 5px 16px rgba(43, 73, 112, .1);
}
.brand-mine-search .van-search__content {
height: 42px;
padding-left: 12px;
align-items: center;
border-radius: 12px;
background-color: #fff;
}
.brand-mine-search .van-field__control {
color: #263548;
font-size: 14px;
}
.brand-mine-filter {
margin-bottom: 10px;
overflow: hidden;
border-radius: 10px;
box-shadow: 0 5px 16px rgba(43, 73, 112, .08);
transition: border-radius .18s ease;
}
.brand-mine-filter.is-open {
border-radius: 10px 10px 0 0;
}
.brand-mine-filter .van-dropdown-menu__bar {
height: 44px;
box-shadow: none;
}
.brand-mine-filter .van-dropdown-menu__title {
width: auto;
max-width: calc(100% - 24px);
padding: 0 12px 0 0;
color: #66758a;
font-size: 14px;
}
.brand-mine-filter .van-dropdown-menu__title::after {
right: 0;
}
.brand-mine-filter .van-dropdown-item__content {
width: calc(100% - 24px);
margin: 0 12px;
overflow: hidden;
border-radius: 0 0 12px 12px;
box-shadow: 0 10px 24px rgba(31, 65, 108, .14);
box-sizing: border-box;
}
.brand-mine-list {
display: flex;
flex-direction: column;
gap: 10px;
}
.brand-mine-card {
overflow: hidden;
border-radius: 10px;
background-color: #fff;
box-shadow: 0 7px 20px rgba(55, 85, 126, .08);
}
.brand-mine-card__cover,
.brand-mine-card__cover-state {
display: flex;
width: 100%;
height: 150px;
align-items: center;
justify-content: center;
background-color: #edf4fc;
}
.brand-mine-card__cover {
display: block;
object-fit: cover;
}
.brand-mine-card__cover-state {
flex-direction: column;
row-gap: 8px;
color: #8da1ba;
font-size: 13px;
}
.brand-mine-card__cover-state .van-icon {
font-size: 34px;
}
.brand-mine-card__cover-state.is-error {
color: #c27b75;
background-color: #fff3f1;
}
.brand-mine-card__body {
padding: 12px 14px 14px;
}
.brand-mine-card__title {
overflow: hidden;
color: #303846;
font-size: 14px;
line-height: 21px;
text-overflow: ellipsis;
white-space: nowrap;
}
.brand-mine-card__time {
margin-top: 4px;
overflow: hidden;
color: #98a2b2;
font-size: 12px;
line-height: 17px;
text-overflow: ellipsis;
white-space: nowrap;
}
.brand-mine-card__meta {
display: flex;
margin-top: 10px;
padding-top: 9px;
flex-wrap: wrap;
gap: 6px;
border-top: 1px solid #edf1f6;
}
.brand-mine-card__tag {
display: inline-flex;
padding: 3px 8px;
border-radius: 4px;
color: #397de0;
background-color: #edf5ff;
font-size: 11px;
line-height: 16px;
}
.brand-mine-card__tag-label {
margin-right: 4px;
color: #8aa7cf;
}
.brand-mine-card__actions {
display: flex;
padding: 12px 14px;
align-items: center;
justify-content: flex-end;
gap: 10px;
border-top: 1px solid #edf1f6;
background-color: #f8fbff;
}
.brand-mine-card__action {
display: inline-flex;
min-width: 82px;
height: 32px;
padding: 0 14px;
align-items: center;
justify-content: center;
border: 0;
border-radius: 16px;
color: #397de0;
background-color: #edf5ff;
font-size: 13px;
box-sizing: border-box;
}
.brand-mine-card__action .van-icon {
margin-right: 6px;
font-size: 15px;
}
.brand-mine-card__action.is-primary {
color: #fff;
background-color: #1989fa;
box-shadow: 0 5px 12px rgba(25, 137, 250, .18);
}
.brand-mine-empty {
display: flex;
min-height: 260px;
padding: 26px 18px 32px;
align-items: center;
justify-content: center;
flex-direction: column;
text-align: center;
}
.brand-mine-empty img {
width: 82%;
max-width: 280px;
height: auto;
object-fit: contain;
}
.brand-mine-empty__title {
margin-top: 14px;
color: #50627a;
font-size: 15px;
line-height: 22px;
font-weight: 600;
}
.brand-mine-empty__hint {
margin-top: 5px;
color: #9aa7b8;
font-size: 12px;
line-height: 18px;
}
.brand-mine-intro-page {
min-height: 100vh;
background-color: #f3f7fd;
}
.brand-mine-intro-scroll {
height: calc(100vh - 46px);
padding: 12px;
overflow-y: auto;
box-sizing: border-box;
}
.brand-mine-intro-hero {
display: flex;
min-height: 82px;
padding: 16px;
align-items: center;
border-radius: 12px;
color: #fff;
background: linear-gradient(120deg, #2d7eea 0%, #5ba6f7 100%);
box-shadow: 0 8px 20px rgba(45, 126, 234, .18);
box-sizing: border-box;
}
.brand-mine-intro-hero__icon {
display: flex;
width: 44px;
height: 44px;
margin-right: 12px;
align-items: center;
justify-content: center;
flex: 0 0 44px;
border-radius: 11px;
color: #2d7eea;
background-color: rgba(255, 255, 255, .92);
font-size: 25px;
}
.brand-mine-intro-hero__title {
font-size: 17px;
line-height: 24px;
}
.brand-mine-intro-hero__subtitle {
margin-top: 3px;
font-size: 12px;
line-height: 18px;
opacity: .9;
}
.brand-mine-intro-card {
margin-top: 12px;
padding: 18px 16px;
overflow: hidden;
border-radius: 12px;
color: #3f4c5e;
background-color: #fff;
box-shadow: 0 7px 20px rgba(55, 85, 126, .08);
font-size: 14px;
line-height: 1.8;
word-break: break-word;
}
.brand-mine-intro-card img {
max-width: 100% !important;
height: auto !important;
border-radius: 8px;
}
</style>
<div id="app" v-cloak>
<div class="brand-mine-page">
<van-nav-bar title="品牌活动-我的报名" left-text="返回" left-arrow @click-left="historyBack" fixed placeholder></van-nav-bar>
<div class="brand-mine-content">
<section class="brand-mine-hero">
<img class="brand-mine-hero__image" src="/assets/mobile/img/home/v4/feature-activity.png" alt="品牌活动插画">
<div class="brand-mine-hero__copy">
<div class="brand-mine-hero__title">我的报名</div>
<div class="brand-mine-hero__subtitle">品牌活动 · 报名记录</div>
</div>
</section>
<van-sticky :offset-top="46">
<div class="brand-mine-sticky">
<van-search v-model="pageForm.searchKeyword" class="brand-mine-search" placeholder="搜索已报名活动" shape="round" clearable @search="doSearch" @clear="doSearch"></van-search>
<van-dropdown-menu class="brand-mine-filter" :class="{'is-open': filterOpened}" active-color="#1989fa">
<year-van-dropdown-item :num="5" @change="doSearch" @open="filterOpened = true" @close="filterOpened = false" v-model="pageForm.year"></year-van-dropdown-item>
<van-dropdown-item v-model="pageForm.activityType" :options="typeOptions" :multiple="false" @change="doSearch" @open="filterOpened = true" @close="filterOpened = false"></van-dropdown-item>
</van-dropdown-menu>
</div>
</van-sticky>
<van-pull-refresh v-model="refreshing" @refresh="onRefresh">
<van-list v-model="loading" :finished="finished" :immediate-check="false" @load="onLoad">
<div class="brand-mine-list">
<div v-for="row in tableData" :key="row.id" class="brand-mine-card">
<img v-if="hasCover(row) && !imageLoadErrors[row.id]" class="brand-mine-card__cover" :src="row.cover" :alt="row.activityName" @error="handleImageError(row)">
<div v-else class="brand-mine-card__cover-state" :class="{'is-error': hasCover(row)}">
<van-icon name="photo-o"></van-icon>
<span>{{ hasCover(row) ? '图片加载失败' : '暂无图片' }}</span>
</div>
<div class="brand-mine-card__body">
<div class="brand-mine-card__title">{{ row.activityName }}</div>
<div class="brand-mine-card__time" v-if="row.activitySignUpStartTime && row.activitySignUpEndTime">
报名时间:{{ $moment(row.activitySignUpStartTime).format('MM/DD HH:mm') }}~{{ $moment(row.activitySignUpEndTime).format('MM/DD HH:mm') }}
</div>
<div class="brand-mine-card__time" v-if="row.activityStartTime && row.activityEndTime">
活动时间:{{ $moment(row.activityStartTime).format('MM/DD HH:mm') }}~{{ $moment(row.activityEndTime).format('MM/DD HH:mm') }}
</div>
<div class="brand-mine-card__meta" v-if="row.activityGroupName || row.trainType">
<span class="brand-mine-card__tag" v-if="row.activityGroupName"><span class="brand-mine-card__tag-label">面向对象</span>{{ row.activityGroupName }}</span>
<span class="brand-mine-card__tag" v-if="row.trainType"><span class="brand-mine-card__tag-label">活动类型</span>{{ row.trainType }}</span>
</div>
</div>
<div class="brand-mine-card__actions">
<button type="button" class="brand-mine-card__action" @click="onView(row)"><van-icon name="eye-o"></van-icon>查看介绍</button>
<button type="button" class="brand-mine-card__action is-primary" @click="onApply(row)"><van-icon name="edit"></van-icon>查看报名</button>
</div>
</div>
</div>
</van-list>
<div v-if="!loading && tableData.length === 0" class="brand-mine-empty">
<img src="/assets/mobile/img/activity/culture-empty.png" alt="品牌活动报名空状态插画">
<div class="brand-mine-empty__title">暂无报名记录</div>
<div class="brand-mine-empty__hint">当前筛选条件下暂时没有已报名活动</div>
</div>
</van-pull-refresh>
</div>
</div>
<van-popup :style="{height: '100%', width: '100%'}" position="right" v-model="infoVisible">
<div class="brand-mine-intro-page">
<van-nav-bar title="活动介绍" left-text="返回" left-arrow @click-left="infoVisible = false"></van-nav-bar>
<div class="brand-mine-intro-scroll">
<div class="brand-mine-intro-hero">
<div class="brand-mine-intro-hero__icon"><van-icon name="description"></van-icon></div>
<div>
<div class="brand-mine-intro-hero__title">活动报名须知</div>
<div class="brand-mine-intro-hero__subtitle">请仔细阅读活动安排与报名要求</div>
</div>
</div>
<div v-if="infoRow.introduce" class="brand-mine-intro-card" v-html="infoRow.introduce"></div>
<div v-else class="brand-mine-intro-card">暂无活动介绍</div>
</div>
</div>
</van-popup>
</div>
<script nonce="${cspNonce!}">
const vue = new Vue({
el: "#app",
store,
data() {
return {
tableData: [],
loading: false,
finished: false,
refreshing: false,
imageLoadErrors: {},
filterOpened: false,
pageForm: {
pageNumber: 1,
pageSize: 10,
totalCount: 0,
searchKeyword: '',
year: new Date().getFullYear(),
activityType: 1,
dataType: 'mine'
},
typeOptions: [
{text: '全部', value: 1},
{text: '报名中', value: 2},
{text: '已结束', value: 3},
],
infoVisible: false,
infoRow: {},
}
},
methods: {
hasCover(row) {
return !!(row && row.cover && String(row.cover).trim())
},
handleImageError(row) {
this.$set(this.imageLoadErrors, row.id, true)
},
onView(row) {
this.infoRow = row
this.infoVisible = true
},
onApply(row) {
this.$pjaxReplace('/platform/trainSignUp/apply/list/h5?id=' + row.id + '&dataType=mine')
},
onLoad() {
if(!this.finished) {
this.loadActivities(false)
}
},
onRefresh() {
this.loadActivities(true)
},
loadActivities(reset) {
if(reset) {
this.pageForm.pageNumber = 1
this.pageForm.totalCount = 0
this.tableData = []
this.finished = false
this.imageLoadErrors = {}
}
this.loading = true
this.$axios.post('/platform/trainSignUp/apply/activityPageData', this.pageForm).then((res) => {
if(res.code === 0) {
const list = res.data && res.data.list ? res.data.list : []
this.tableData = this.pageForm.pageNumber === 1 ? list : this.tableData.concat(list)
this.pageForm.totalCount = res.data && res.data.totalCount ? res.data.totalCount : 0
this.finished = this.tableData.length >= this.pageForm.totalCount
if(!this.finished) {
this.pageForm.pageNumber++
}
}
}).finally(() => {
this.loading = false
this.refreshing = false
})
},
doSearch() {
this.loadActivities(true)
}
},
created() {
this.loadActivities(true)
}
})
</script>
<!--#
}
#-->
@@ -0,0 +1,149 @@
<!--#
layout("/layouts/platform_h5.html"){
#-->
<style scoped>
.weui-msg {
min-height: calc(100vh - 40vh - 46px);
}
</style>
<div id="app">
<van-nav-bar title="活动签到" left-text="返回" left-arrow @click-left="historyBack" fixed placeholder></van-nav-bar>
<div id="mapContainer" style="width: 100%; height: 40vh"></div>
<div class="weui-msg">
<div class="weui-msg__icon-area">
<i v-if="res?.code !== 0" class="weui-icon-warn weui-icon_msg"></i>
<i v-if="res?.code === 0" class="weui-icon-success weui-icon_msg"></i>
</div>
<div class="weui-msg__text-area">
<div class="weui-msg__title">温馨提醒</div>
<div v-html="res?.msg"></div>
</div>
<div class="weui-msg__opr-area">
<p class="weui-btn-area">
<a v-if="res?.code !== 0" @click="res = {}; onSign()" class="weui-btn weui-btn_default">重新签到</a>
<a v-if="res?.code === 0" class="weui-btn weui-btn_primary">您已签到</a>
</p>
</div>
</div>
</div>
<script nonce="${cspNonce!}">
const vue = new Vue({
el: "#app",
store,
components: {
},
data() {
return {
courseId: GetQueryString('courseId'),
markerLayer: null,
map: null,
res: {},
}
},
methods: {
initMap() {
if(!this.row.courseLocationCoordinates) {
this.$toast('未设置签到点')
return
}
const posi = this.row.courseLocationCoordinates
const center = new TMap.LatLng(posi[0], posi[1])
this.map = new TMap.Map('mapContainer', {
resizeEnable: true,
zoom: 16,
center: center
})
this.markerLayer = new TMap.MultiMarker({
map: this.map,
styles: {
"current": new TMap.MarkerStyle({
"src": "https://mapapi.qq.com/web/bundles/lbs-home/prod/assets/markerActive-NhH9NoBV.png",
"width": 40, // 点标记样式宽度(像素)
})
},
geometries: []
})
this.createMarker(center)
this.createCircle(center)
},
createMarker(position, styleId = 'marker', id = 'marker_' + Date.now()) {
this.markerLayer.add([
{
id: id,
position: position,
styleId: styleId
}
]);
},
createCircle(position) {
this.circle = new TMap.MultiCircle({
map: this.map,
geometries: [{
center: position,
radius: this.row.radius || 100,
}],
});
},
getUserLocation() {
return new Promise((resolve) => {
if (!navigator.geolocation) {
vant.Toast('浏览器不支持定位');
return resolve(null);
}
const loading = vant.Toast.loading({ message: '定位中...', duration: 0 });
navigator.geolocation.getCurrentPosition(
(pos) => {
loading.close();
resolve({
lat: pos.coords.latitude,
lng: pos.coords.longitude
});
if (this.markerLayer) {
this.markerLayer.remove(["current"])
}
const transPosi = this.$coordinateUtil.wgs84ToGcj02(pos.coords.longitude, pos.coords.latitude)
console.log(transPosi)
const center = new TMap.LatLng(transPosi[0], transPosi[1])
this.createMarker(center, 'current', 'current')
},
(err) => {
console.log(err)
loading.close();
vant.Toast('定位失败,请重试');
resolve(null);
},
{ enableHighAccuracy: true, timeout: 3000 }
);
});
},
async fetchCourse() {
const res = await this.$axios.post('/platform/trainSignUp/mine/fetchCourse', {courseId: this.courseId})
this.row = res.data
},
async onSign() {
const coords = await this.getUserLocation()
if (coords) {
this.res = await this.$axios.post('/platform/trainSignUp/mine/drivingScan', {
courseId: this.row.id,
lat: coords.lat,
lng: coords.lng
})
}
}
},
async created() {
await this.fetchCourse()
this.initMap()
this.onSign()
}
})
</script>
<!--#
}
#-->
@@ -0,0 +1,639 @@
<!--#
layout("/layouts/platform_h5.html"){
#-->
<style>
body {
background-color: #f3f7fd;
}
.works-mine-page {
min-height: 100vh;
padding-bottom: calc(24px + env(safe-area-inset-bottom));
box-sizing: border-box;
background-color: #f3f7fd;
}
.works-mine-page .van-nav-bar {
background-color: #fff;
}
.works-mine-page .van-nav-bar__title {
color: #1f2937;
font-size: 16px;
}
.works-mine-page .van-nav-bar .van-icon,
.works-mine-page .van-nav-bar__text {
color: #397fbd;
}
.works-mine-content {
padding: 12px 12px 0;
}
.works-mine-hero {
position: relative;
height: 134px;
overflow: hidden;
border-radius: 10px;
color: #fff;
background-color: #247fe8;
box-shadow: 0 8px 20px rgba(51, 117, 219, .18);
}
.works-mine-hero__image {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
object-fit: cover;
object-position: center;
}
.works-mine-hero__copy {
position: relative;
z-index: 1;
display: flex;
width: 66%;
height: 100%;
padding-left: 16px;
flex-direction: column;
justify-content: center;
box-sizing: border-box;
background: linear-gradient(90deg, rgba(22, 112, 232, .98) 0%, rgba(22, 112, 232, .88) 52%, rgba(22, 112, 232, 0) 100%);
}
.works-mine-hero__title {
font-size: 25px;
line-height: 34px;
font-weight: 700;
letter-spacing: 1px;
}
.works-mine-hero__subtitle {
margin-top: 4px;
font-size: 13px;
line-height: 20px;
letter-spacing: .5px;
opacity: .95;
}
.works-mine-sticky {
padding: 10px 0 1px;
background-color: #f3f7fd;
}
.van-sticky--fixed .works-mine-sticky {
padding-right: 12px;
padding-left: 12px;
box-sizing: border-box;
}
.works-mine-search {
margin-bottom: 8px;
padding: 0;
overflow: hidden;
border-radius: 12px;
background-color: #fff;
box-shadow: 0 5px 16px rgba(43, 73, 112, .1);
}
.works-mine-search .van-search__content {
height: 42px;
padding-left: 12px;
align-items: center;
border-radius: 12px;
background-color: #fff;
}
.works-mine-filter {
margin-bottom: 10px;
overflow: hidden;
border-radius: 10px;
box-shadow: 0 5px 16px rgba(43, 73, 112, .08);
transition: border-radius .18s ease;
}
.works-mine-filter.is-open {
border-radius: 10px 10px 0 0;
}
.works-mine-filter .van-dropdown-menu__bar {
height: 44px;
box-shadow: none;
}
.works-mine-filter .van-dropdown-menu__item {
min-width: 0;
overflow: hidden;
}
.works-mine-filter .van-dropdown-menu__title {
width: auto;
max-width: calc(100% - 24px);
padding: 0 12px 0 0;
overflow: hidden;
color: #66758a;
font-size: 14px;
text-overflow: ellipsis;
white-space: nowrap;
}
.works-mine-filter .van-dropdown-menu__title::after {
right: 0;
}
.works-mine-filter .van-dropdown-item__content {
width: calc(100% - 24px);
margin: 0 12px;
overflow: hidden;
border-radius: 0 0 12px 12px;
box-shadow: 0 10px 24px rgba(31, 65, 108, .14);
box-sizing: border-box;
}
.works-mine-list {
display: flex;
flex-direction: column;
gap: 10px;
}
.works-mine-card {
overflow: hidden;
border-radius: 10px;
background-color: #fff;
box-shadow: 0 7px 20px rgba(55, 85, 126, .08);
}
.works-mine-card__cover,
.works-mine-card__cover-state {
display: flex;
width: 100%;
height: 150px;
align-items: center;
justify-content: center;
background-color: #edf4fc;
}
.works-mine-card__cover {
display: block;
object-fit: cover;
}
.works-mine-card__cover-state {
flex-direction: column;
row-gap: 8px;
color: #8da1ba;
font-size: 13px;
}
.works-mine-card__cover-state .van-icon {
font-size: 34px;
}
.works-mine-card__cover-state.is-error {
color: #c27b75;
background-color: #fff3f1;
}
.works-mine-card__body {
padding: 12px 14px 14px;
}
.works-mine-card__title {
overflow: hidden;
color: #303846;
font-size: 15px;
line-height: 22px;
font-weight: 600;
text-overflow: ellipsis;
white-space: nowrap;
}
.works-mine-card__activity,
.works-mine-card__time {
margin-top: 4px;
overflow: hidden;
color: #8794a7;
font-size: 12px;
line-height: 18px;
text-overflow: ellipsis;
white-space: nowrap;
}
.works-mine-card__meta {
display: flex;
margin-top: 10px;
padding-top: 9px;
flex-wrap: wrap;
gap: 6px;
border-top: 1px solid #edf1f6;
}
.works-mine-card__tag {
display: inline-flex;
padding: 3px 8px;
border-radius: 4px;
color: #397de0;
background-color: #edf5ff;
font-size: 11px;
line-height: 16px;
}
.works-mine-card__tag-label {
margin-right: 4px;
color: #8aa7cf;
}
.works-mine-card__actions {
display: flex;
padding: 12px 14px;
align-items: center;
justify-content: flex-end;
gap: 8px;
border-top: 1px solid #edf1f6;
background-color: #f8fbff;
}
.works-mine-card__action {
display: inline-flex;
min-width: 72px;
height: 32px;
padding: 0 12px;
align-items: center;
justify-content: center;
border: 0;
border-radius: 16px;
color: #397de0;
background-color: #edf5ff;
font-size: 13px;
box-sizing: border-box;
}
.works-mine-card__action .van-icon {
margin-right: 5px;
font-size: 15px;
}
.works-mine-card__action.is-primary {
color: #fff;
background-color: #1989fa;
box-shadow: 0 5px 12px rgba(25, 137, 250, .18);
}
.works-mine-card__action.is-danger {
color: #e45c5c;
background-color: #fff0f0;
}
.works-mine-skeleton {
display: flex;
flex-direction: column;
gap: 10px;
}
.works-mine-skeleton__card {
overflow: hidden;
border-radius: 10px;
background-color: #fff;
box-shadow: 0 7px 20px rgba(55, 85, 126, .08);
}
.works-mine-skeleton__cover,
.works-mine-skeleton__body .van-skeleton,
.works-mine-skeleton__actions .van-skeleton {
padding: 0;
}
.works-mine-skeleton__cover .van-skeleton__row {
width: 100% !important;
height: 150px;
margin: 0;
border-radius: 0;
}
.works-mine-skeleton__body {
padding: 13px 14px 14px;
}
.works-mine-skeleton__body .van-skeleton__title {
width: 52%;
height: 15px;
margin-bottom: 12px;
}
.works-mine-skeleton__body .van-skeleton__row {
height: 12px;
margin-top: 9px;
}
.works-mine-skeleton__body .van-skeleton__row:last-child {
width: 58%;
}
.works-mine-skeleton__actions {
padding: 12px 14px;
border-top: 1px solid #edf1f6;
background-color: #f8fbff;
}
.works-mine-skeleton__actions .van-skeleton__row {
width: 72%;
height: 32px;
margin: 0 0 0 auto;
border-radius: 16px;
}
.works-mine-empty {
display: flex;
min-height: 260px;
padding: 26px 18px 32px;
align-items: center;
justify-content: center;
flex-direction: column;
text-align: center;
}
.works-mine-empty img {
width: 82%;
max-width: 280px;
height: auto;
object-fit: contain;
}
.works-mine-empty__title {
margin-top: 14px;
color: #50627a;
font-size: 15px;
line-height: 22px;
font-weight: 600;
}
.works-mine-empty__hint {
margin-top: 5px;
color: #9aa7b8;
font-size: 12px;
line-height: 18px;
}
</style>
<div id="app" v-cloak>
<div class="works-mine-page">
<van-nav-bar title="我的作品" left-text="返回" left-arrow fixed placeholder @click-left="handlePageBack"></van-nav-bar>
<div class="works-mine-content">
<section class="works-mine-hero">
<img class="works-mine-hero__image" src="/assets/mobile/img/activity/hero/brand-hero-v2.png" alt="我的作品插画">
<div class="works-mine-hero__copy">
<div class="works-mine-hero__title">我的作品</div>
<div class="works-mine-hero__subtitle">作品征集 · 提交记录</div>
</div>
</section>
<van-sticky :offset-top="46">
<div class="works-mine-sticky">
<van-search v-model="pageForm.searchKeyword" class="works-mine-search" placeholder="搜索作品或征集活动"
shape="round" clearable @search="doSearch" @clear="doSearch"></van-search>
<van-dropdown-menu class="works-mine-filter" :class="{'is-open': filterOpened}" active-color="#1989fa">
<year-van-dropdown-item :num="5" v-model="pageForm.year" @change="doSearch"
@open="onFilterOpen" @close="onFilterClose"></year-van-dropdown-item>
<van-dropdown-item v-model="pageForm.activityType" :options="typeOptions" :multiple="false"
@change="doSearch" @open="onFilterOpen" @close="onFilterClose"></van-dropdown-item>
</van-dropdown-menu>
</div>
</van-sticky>
<div v-if="skeletonLoading" class="works-mine-skeleton">
<div v-for="index in 3" :key="'mine-skeleton-' + index" class="works-mine-skeleton__card">
<van-skeleton class="works-mine-skeleton__cover" :row="1" animated></van-skeleton>
<div class="works-mine-skeleton__body">
<van-skeleton title :row="3" animated></van-skeleton>
</div>
<div class="works-mine-skeleton__actions">
<van-skeleton :row="1" animated></van-skeleton>
</div>
</div>
</div>
<van-pull-refresh v-show="!skeletonLoading" v-model="refreshing" @refresh="onRefresh">
<van-list v-model="loading" :finished="finished" :immediate-check="false" @load="onLoad">
<div class="works-mine-list">
<article v-for="row in tableData" :key="row.id" class="works-mine-card">
<img v-if="hasCover(row) && !imageLoadErrors[row.id]" class="works-mine-card__cover"
:src="row.cover" :alt="row.activityName" @error="handleImageError(row)">
<div v-else class="works-mine-card__cover-state" :class="{'is-error': hasCover(row)}">
<van-icon name="photo-o"></van-icon>
<span>{{hasCover(row) ? '图片加载失败' : '暂无图片'}}</span>
</div>
<div class="works-mine-card__body">
<div class="works-mine-card__title">{{row.name}}</div>
<div class="works-mine-card__activity">所属活动:{{row.activityName || '暂无活动信息'}}</div>
<div class="works-mine-card__time" v-if="row.createdAt">
提交时间:{{$moment(row.createdAt).format('YYYY-MM-DD HH:mm')}}
</div>
<div class="works-mine-card__meta" v-if="row.subjectName || row.worksName">
<span class="works-mine-card__tag" v-if="row.subjectName">
<span class="works-mine-card__tag-label">主题</span>{{row.subjectName}}
</span>
<span class="works-mine-card__tag" v-if="row.worksName">
<span class="works-mine-card__tag-label">类型</span>{{row.worksName}}
</span>
</div>
</div>
<div class="works-mine-card__actions">
<button type="button" class="works-mine-card__action" @click="onView(row)">
<van-icon name="eye-o"></van-icon>查看
</button>
<button type="button" class="works-mine-card__action" @click="onEdit(row)">
<van-icon name="edit"></van-icon>编辑
</button>
<button type="button" class="works-mine-card__action is-danger" @click="onDelete(row)">
<van-icon name="delete-o"></van-icon>删除
</button>
</div>
</article>
</div>
</van-list>
<div v-if="!loading && tableData.length === 0" class="works-mine-empty">
<img src="/assets/mobile/img/activity/culture-empty.png" alt="作品记录空状态插画">
<div class="works-mine-empty__title">暂无作品记录</div>
<div class="works-mine-empty__hint">当前筛选条件下暂时没有已提交作品</div>
</div>
</van-pull-refresh>
</div>
</div>
<van-dialog title="删除确认" message="确认删除当前作品吗?" :value="deleteConfirmVisible"
:close-on-click-overlay="false" show-cancel-button cancel-button-text="取消" confirm-button-text="确认删除"
@cancel="closeDeleteConfirm" @confirm="confirmDelete"></van-dialog>
<apply-form ref="applyFormRef" @submit-success="handleEditSubmitSuccess"></apply-form>
<info ref="infoRef"></info>
</div>
<script nonce="${cspNonce!}">
<!--#include('../upload/applyForm.js'){}#-->
<!--#include('info.js'){}#-->
new Vue({
el: "#app",
store,
components: {
'apply-form': applyForm,
'info': INFO,
},
data() {
return {
tableData: [],
loading: false,
skeletonLoading: true,
finished: false,
refreshing: false,
imageLoadErrors: {},
filterOpened: false,
deleteConfirmVisible: false,
deleteLoading: false,
pendingDeleteRow: {},
pageForm: {
pageNumber: 1,
pageSize: 10,
totalCount: 0,
searchKeyword: '',
year: new Date().getFullYear(),
activityType: 1,
},
typeOptions: [
{text: '全部', value: 1},
{text: '征集中', value: 2},
{text: '已结束', value: 3},
],
historyLayerPageKey: 'works-collection-mine-h5',
historyLayerUnsubscribe: null,
}
},
methods: {
hasCover(row) {
return !!(row && row.cover && String(row.cover).trim())
},
handleImageError(row) {
this.$set(this.imageLoadErrors, row.id, true)
},
onFilterOpen() {
this.$set(this, 'filterOpened', true)
},
onFilterClose() {
this.$set(this, 'filterOpened', false)
},
handlePageBack() {
if (window.h5HistoryLayerManager.stack.length) {
window.h5HistoryLayerManager.close()
return
}
this.historyBack()
},
onView(row) {
this.$refs.infoRef.onOpen(row)
},
onEdit(row) {
if (this.$moment().isAfter(this.$moment(row.activityEndDateTime))) {
this.$toast.fail("活动已结束,无法修改!")
return
}
this.$refs.applyFormRef.onOpenEdit(row)
},
// 编辑提交成功后留在我的作品页面,仅刷新当前列表数据。
handleEditSubmitSuccess() {
this.loadWorks(true)
},
onDelete(row) {
this.$set(this, 'pendingDeleteRow', row)
window.h5HistoryLayerManager.open('works-collection-mine-delete-confirm')
},
closeDeleteConfirm() {
window.h5HistoryLayerManager.close('works-collection-mine-delete-confirm')
},
confirmDelete() {
const row = this.pendingDeleteRow
window.h5HistoryLayerManager.close('works-collection-mine-delete-confirm', () => {
this.deleteRow(row)
})
},
// 删除接口接收作品 id,成功后重置分页列表;接口返回 Result JSON。
deleteRow(row) {
if (!row || !row.id || this.deleteLoading) return
this.$set(this, 'deleteLoading', true)
this.$axios.post('/platform/activity/worksCollection/upload/delete', {id: row.id}).then((res) => {
if (res.code === 0) {
this.$toast.success(res.msg || '删除成功')
this.loadWorks(true)
}
}).finally(() => {
this.$set(this, 'deleteLoading', false)
})
},
onLoad() {
if (!this.finished) this.loadWorks(false)
},
onRefresh() {
this.loadWorks(true)
},
/**
* 加载当前用户作品分页数据。
* reset 为 true 时从第一页重新查询;接口返回 Pagination,list 为作品数组、totalCount 为总数。
*/
loadWorks(reset) {
const showSkeleton = !!reset
if (reset) {
this.$set(this, 'skeletonLoading', true)
this.$set(this.pageForm, 'pageNumber', 1)
this.$set(this.pageForm, 'totalCount', 0)
this.$set(this, 'tableData', [])
this.$set(this, 'finished', false)
this.$set(this, 'imageLoadErrors', {})
}
this.$set(this, 'loading', true)
this.$axios.post('/platform/activity/worksCollection/upload/pageData', this.pageForm, {
h5SkeletonLoading: showSkeleton
}).then((res) => {
if (res.code === 0) {
const list = res.data && res.data.list ? res.data.list : []
this.$set(this, 'tableData', this.pageForm.pageNumber === 1 ? list : this.tableData.concat(list))
this.$set(this.pageForm, 'totalCount', res.data && res.data.totalCount ? res.data.totalCount : 0)
this.$set(this, 'finished', this.tableData.length >= this.pageForm.totalCount)
if (!this.finished) {
this.$set(this.pageForm, 'pageNumber', this.pageForm.pageNumber + 1)
}
}
}).finally(() => {
this.$set(this, 'loading', false)
this.$set(this, 'refreshing', false)
if (showSkeleton) this.$set(this, 'skeletonLoading', false)
})
},
doSearch() {
this.loadWorks(true)
},
syncHistoryLayers(stack) {
this.$set(this, 'deleteConfirmVisible', stack.includes('works-collection-mine-delete-confirm'))
}
},
created() {
window.h5HistoryLayerManager.register(this.historyLayerPageKey, (stack) => {
this.syncHistoryLayers(stack)
})
this.$set(this, 'historyLayerUnsubscribe', window.h5HistoryLayerManager.subscribe((stack) => {
this.syncHistoryLayers(stack)
}))
this.loadWorks(true)
},
beforeDestroy() {
if (this.historyLayerUnsubscribe) this.historyLayerUnsubscribe()
window.h5HistoryLayerManager.unregister(this.historyLayerPageKey)
}
})
</script>
<!--#
}
#-->
@@ -0,0 +1,197 @@
const INFO = {
template: /*language=HTML*/
`
<div>
<van-popup class="works-detail-popup" :style="{height: '100%', width: '100%'}" position="right"
safe-area-inset-bottom :close-on-click-overlay="false" v-model="visible">
<div class="works-detail-page">
<van-sticky>
<van-nav-bar title="作品详情" left-text="返回" left-arrow @click-left="closeInfo"></van-nav-bar>
</van-sticky>
<div ref="detailScrollRef" class="works-detail-scroll">
<section class="works-detail-card">
<h2 class="works-detail-card__title">基础信息</h2>
<van-cell-group :border="false">
<van-cell title="姓名" :value="viewData.userName || '-' "></van-cell>
<van-cell title="工号" :value="viewData.loginName || '-' "></van-cell>
<van-cell title="单位" :value="viewData.unitName || '-' "></van-cell>
<van-cell title="分工会" :value="viewData.unionName || '-' "></van-cell>
</van-cell-group>
</section>
<section class="works-detail-card">
<h2 class="works-detail-card__title">作品信息</h2>
<van-cell-group :border="false">
<van-cell title="所属活动" :value="viewData.activityName || '-' "></van-cell>
<van-cell title="主题类型" :value="viewData.subjectName || '-' "></van-cell>
<van-cell title="作品类型" :value="viewData.worksName || '-' "></van-cell>
<van-cell title="作品名称" :value="viewData.name || '-' "></van-cell>
</van-cell-group>
</section>
<section class="works-detail-card">
<h2 class="works-detail-card__title">作品描述</h2>
<div class="works-detail-description">{{viewData.description || '暂无作品描述'}}</div>
</section>
<section class="works-detail-card">
<h2 class="works-detail-card__title">作品附件</h2>
<div v-if="viewData.files && viewData.files.length" class="works-detail-files">
<file-preview :files="viewData.files" complete_result></file-preview>
</div>
<div v-else class="works-detail-empty">暂无作品附件</div>
</section>
</div>
</div>
</van-popup>
</div>
`,
style: /*language=CSS*/ `
/deep/ .works-detail-popup,
/deep/ .works-detail-page {
background-color: #f3f7fd;
}
/deep/ .works-detail-page {
min-height: 100vh;
}
/deep/ .works-detail-page .van-nav-bar__title {
color: #1f2937;
font-size: 16px;
}
/deep/ .works-detail-page .van-nav-bar .van-icon,
/deep/ .works-detail-page .van-nav-bar__text {
color: #397fbd;
}
/deep/ .works-detail-scroll {
height: calc(100vh - 46px);
padding: 12px 12px calc(24px + env(safe-area-inset-bottom));
overflow-x: hidden;
overflow-y: auto;
box-sizing: border-box;
}
/deep/ .works-detail-card {
margin-top: 12px;
overflow: hidden;
border: 1px solid #e9eff7;
border-radius: 12px;
background-color: #fff;
box-shadow: 0 7px 20px rgba(55, 85, 126, .08);
}
/deep/ .works-detail-scroll > .works-detail-card:first-child {
margin-top: 0;
}
/deep/ .works-detail-card__title {
position: relative;
margin: 0;
padding: 13px 14px 9px 22px;
color: #354255;
font-size: 15px;
line-height: 21px;
font-weight: 600;
}
/deep/ .works-detail-card__title::before {
position: absolute;
top: 16px;
bottom: 12px;
left: 12px;
width: 2px;
border-radius: 2px;
background-color: #1989fa;
content: "";
}
/deep/ .works-detail-card .van-cell {
padding: 10px 14px;
color: #596779;
}
/deep/ .works-detail-card .van-cell__title {
flex: 0 0 82px;
font-size: 14px;
}
/deep/ .works-detail-card .van-cell__value {
color: #4f5d70;
font-size: 14px;
word-break: break-word;
}
/deep/ .works-detail-description,
/deep/ .works-detail-files,
/deep/ .works-detail-empty {
padding: 4px 14px 16px;
color: #536174;
font-size: 14px;
line-height: 1.8;
white-space: pre-wrap;
word-break: break-word;
}
/deep/ .works-detail-empty {
color: #9aa7b8;
}
`,
data() {
return {
viewData: {},
visible: false,
historyLayerUnsubscribe: null,
}
},
methods: {
/**
* 查询并打开作品详情。
* row 需传作品 id;接口返回作品、活动、主题、类型及附件信息的 Result JSON。
*/
onOpen(row) {
this.$set(this, "viewData", Object.assign({}, row, {files: this.normalizeFiles(row.files)}))
window.h5HistoryLayerManager.open("works-collection-mine-info")
this.resetScrollPosition()
this.$axios.post("/platform/activity/worksCollection/common/findOne", {id: row.id}).then((res) => {
if (res.code === 0) {
const viewData = res.data || {}
viewData.files = this.normalizeFiles(viewData.files)
this.$set(this, "viewData", viewData)
}
})
},
// 弹窗组件会被复用,每次打开后需清除上一次查看时保留的滚动位置。
resetScrollPosition() {
this.$nextTick(() => {
if (this.$refs.detailScrollRef) this.$refs.detailScrollRef.scrollTop = 0
})
},
normalizeFiles(files) {
if (Array.isArray(files)) return files
if (!files) return []
try {
const result = JSON.parse(files)
return Array.isArray(result) ? result : []
} catch (err) {
return []
}
},
closeInfo() {
window.h5HistoryLayerManager.close("works-collection-mine-info")
},
syncHistoryLayers(stack) {
this.$set(this, "visible", stack.includes("works-collection-mine-info"))
}
},
created() {
this.$set(this, "historyLayerUnsubscribe", window.h5HistoryLayerManager.subscribe((stack) => {
this.syncHistoryLayers(stack)
}))
},
beforeDestroy() {
if (this.historyLayerUnsubscribe) this.historyLayerUnsubscribe()
}
}
@@ -0,0 +1,667 @@
<!--#
layout("/layouts/platform_h5.html"){
#-->
<style>
body {
background-color: #f3f7fd;
}
.works-read-page {
min-height: 100vh;
padding-bottom: calc(24px + env(safe-area-inset-bottom));
box-sizing: border-box;
background-color: #f3f7fd;
}
.works-read-page .van-nav-bar {
background-color: #fff;
}
.works-read-page .van-nav-bar__title {
color: #172033;
font-size: 16px;
font-weight: 600;
}
.works-read-page .van-nav-bar .van-icon,
.works-read-page .van-nav-bar__text {
color: #1989fa;
}
.works-read-sticky {
padding: 22px 12px 1px;
box-sizing: border-box;
background-color: #f3f7fd;
}
.works-read-search {
margin: 0 0 8px;
padding: 0;
overflow: hidden;
border-radius: 12px;
background-color: #fff;
box-shadow: 0 5px 16px rgba(43, 73, 112, .1);
}
.works-read-search .van-search__content {
height: 42px;
padding-left: 12px;
align-items: center;
border-radius: 12px;
background-color: #fff;
}
.works-read-search .van-field__control {
color: #263548;
font-size: 14px;
}
.works-read-search .van-field__control::placeholder {
color: #a7b0bf;
}
.works-read-filter {
margin: 0 0 10px;
overflow: hidden;
border-radius: 10px;
box-shadow: 0 5px 16px rgba(43, 73, 112, .08);
transition: border-radius .18s ease;
}
.works-read-filter.is-open {
border-radius: 10px 10px 0 0;
}
.works-read-filter .van-dropdown-menu__bar {
height: 44px;
box-shadow: none;
}
.works-read-filter .van-dropdown-menu__item {
min-width: 0;
overflow: hidden;
}
.works-read-filter .van-dropdown-menu__title {
width: auto;
max-width: calc(100% - 24px);
padding: 0 12px 0 0;
overflow: hidden;
color: #66758a;
font-size: 14px;
text-overflow: ellipsis;
white-space: nowrap;
}
.works-read-filter .van-dropdown-menu__title::after {
right: 0;
}
.works-read-filter .van-dropdown-item__content {
width: calc(100% - 24px);
margin: 0 12px;
overflow: hidden;
border-radius: 0 0 12px 12px;
box-shadow: 0 10px 24px rgba(31, 65, 108, .14);
box-sizing: border-box;
}
.works-read-content {
padding: 0 12px;
}
.works-read-list .empty-state {
display: none;
}
.works-read-skeleton {
padding-top: 12px;
}
.works-read-skeleton__card {
margin-bottom: 12px;
padding: 14px;
overflow: hidden;
border: 1px solid #eef1f6;
border-radius: 12px;
background-color: #fff;
box-shadow: 0 7px 20px rgba(55, 85, 126, .08);
box-sizing: border-box;
}
.works-read-skeleton__card .van-skeleton {
padding: 0;
}
.works-read-skeleton__card .van-skeleton__title {
width: 42%;
height: 16px;
margin-bottom: 13px;
}
.works-read-skeleton__card .van-skeleton__row {
height: 12px;
margin-top: 10px;
}
.works-read-skeleton__card .van-skeleton__row:nth-child(2n) {
width: 72%;
}
.works-read-skeleton__card .van-skeleton__row:last-child {
width: 48%;
height: 28px;
margin-top: 16px;
border-radius: 14px;
}
.works-read-empty {
display: flex;
min-height: 280px;
padding: 46px 18px 36px;
align-items: center;
justify-content: center;
flex-direction: column;
box-sizing: border-box;
text-align: center;
}
.works-read-empty img {
display: block;
width: 86%;
max-width: 290px;
height: auto;
object-fit: contain;
}
.works-read-empty__title {
margin-top: 14px;
color: #50627a;
font-size: 15px;
line-height: 22px;
font-weight: 600;
}
.works-read-empty__hint {
margin-top: 5px;
color: #9aa7b8;
font-size: 12px;
line-height: 18px;
}
.works-read-card-header {
display: flex;
margin-bottom: 7px;
align-items: flex-start;
justify-content: space-between;
}
.works-read-card-heading {
min-width: 0;
padding-right: 10px;
flex: 1;
}
.works-read-card-title {
overflow: hidden;
color: #253247;
font-size: 15px;
line-height: 22px;
font-weight: 600;
text-overflow: ellipsis;
white-space: nowrap;
}
.works-read-card-activity {
margin-top: 1px;
overflow: hidden;
color: #9ba6b6;
font-size: 12px;
line-height: 18px;
text-overflow: ellipsis;
white-space: nowrap;
}
.works-read-like-summary {
display: inline-flex;
min-height: 24px;
align-items: center;
flex: none;
color: #7f8da1;
font-size: 12px;
line-height: 18px;
white-space: nowrap;
box-sizing: border-box;
}
/* 顶部仅展示累计点赞信息,人员图标与无底色文字用于区别底部可点击的点赞按钮。 */
.works-read-like-summary .van-icon {
display: inline-flex;
width: 20px;
height: 20px;
margin-right: 5px;
align-items: center;
justify-content: center;
border-radius: 50%;
color: #ffffff;
background-color: #8795aa;
font-size: 12px;
}
.works-read-files-row {
width: 100%;
min-width: 0;
margin-top: 8px;
padding-top: 10px;
border-top: 1px solid #edf1f6;
box-sizing: border-box;
}
.works-read-files-label {
display: flex;
margin-bottom: 7px;
align-items: center;
justify-content: space-between;
gap: 10px;
color: #98a3b3;
font-size: 12px;
line-height: 18px;
}
.works-read-files-hint {
flex: none;
color: #7893af;
font-size: 11px;
white-space: nowrap;
}
.works-read-files-scroll {
width: 100%;
min-width: 0;
overflow-x: auto;
overflow-y: hidden;
-webkit-overflow-scrolling: touch;
scrollbar-width: none;
}
.works-read-files-scroll::-webkit-scrollbar {
display: none;
}
.works-read-list .works-read-files-scroll .file-preview-container {
display: flex !important;
width: max-content;
min-width: 100%;
padding: 0;
grid-template-columns: none !important;
gap: 8px;
box-sizing: border-box;
}
.works-read-list .works-read-files-scroll .file-preview-container .file-item {
width: 88px;
height: 88px;
flex: 0 0 88px;
aspect-ratio: auto;
}
.works-read-action-icon {
font-size: 15px;
}
.works-read-like-action-wrap {
position: relative;
display: flex;
overflow: visible;
}
.works-read-like-hearts {
position: absolute;
z-index: 3;
bottom: 24px;
left: 50%;
width: 1px;
height: 1px;
pointer-events: none;
}
.works-read-like-heart {
position: absolute;
bottom: 0;
left: 0;
color: #ee3f4d;
font-size: 18px;
line-height: 1;
opacity: 0;
text-shadow: 0 3px 8px rgba(238, 63, 77, .32);
animation: works-read-heart-bubble 1s ease-out forwards;
}
.works-read-like-heart.is-heart-1 {
--heart-x: -26px;
--heart-y: -62px;
font-size: 15px;
animation-delay: 0s;
}
.works-read-like-heart.is-heart-2 {
--heart-x: -11px;
--heart-y: -82px;
font-size: 21px;
animation-delay: .08s;
}
.works-read-like-heart.is-heart-3 {
--heart-x: 7px;
--heart-y: -68px;
font-size: 14px;
animation-delay: .16s;
}
.works-read-like-heart.is-heart-4 {
--heart-x: 24px;
--heart-y: -88px;
font-size: 18px;
animation-delay: .24s;
}
.works-read-like-heart.is-heart-5 {
--heart-x: -18px;
--heart-y: -102px;
font-size: 13px;
animation-delay: .32s;
}
.works-read-like-heart.is-heart-6 {
--heart-x: 15px;
--heart-y: -108px;
font-size: 16px;
animation-delay: .4s;
}
@keyframes works-read-heart-bubble {
0% {
opacity: 0;
transform: translate(-50%, 5px) scale(.35);
}
20% {
opacity: 1;
transform: translate(-50%, -8px) scale(1.15);
}
62% {
opacity: 1;
transform: translate(-50%, -42px) scale(1);
}
100% {
opacity: 0;
transform: translate(calc(-50% + var(--heart-x)), var(--heart-y)) scale(.72);
}
}
@media (max-width: 350px) {
.works-read-content,
.works-read-sticky {
padding-right: 10px;
padding-left: 10px;
}
}
</style>
<div id="app" v-cloak>
<div class="works-read-page">
<van-nav-bar title="作品阅览" left-text="返回" left-arrow placeholder fixed
@click-left="handlePageBack"></van-nav-bar>
<van-sticky :offset-top="46">
<div class="works-read-sticky">
<van-search v-model="pageForm.searchKeyword" class="works-read-search"
placeholder="请输入姓名或工号搜索" shape="round" clearable
@search="doSearch" @clear="doSearch"></van-search>
<van-dropdown-menu class="works-read-filter" :class="{'is-open': filterOpened}" active-color="#1989fa">
<van-dropdown-item v-model="pageForm.activityId" :options="activityOptions" :multiple="false"
@change="doSearch" @open="onFilterOpen" @close="onFilterClose"></van-dropdown-item>
<van-dropdown-item v-model="pageForm.pageOrderName" :options="orderOptions" :multiple="false"
@change="doSearch" @open="onFilterOpen" @close="onFilterClose"></van-dropdown-item>
</van-dropdown-menu>
</div>
</van-sticky>
<main class="works-read-content">
<div class="works-read-list">
<div v-if="skeletonLoading" class="works-read-skeleton">
<div v-for="index in 3" :key="'read-skeleton-' + index" class="works-read-skeleton__card">
<van-skeleton title :row="5" animated></van-skeleton>
</div>
</div>
<div v-if="!skeletonLoading && pageForm.totalCount === 0" class="works-read-empty">
<img src="/assets/mobile/img/activity/culture-empty.png" alt="暂无作品记录插画">
<div class="works-read-empty__title">暂无作品记录</div>
<div class="works-read-empty__hint">当前筛选条件下暂时没有可阅览作品</div>
</div>
<table-list api="/platform/activity/worksCollection/read/h5/pageData"
:page_form.sync="pageForm" ref="tableListRef" title="name" v-show="!skeletonLoading"
:show_loading_overlay="false" @ready="onReady" @loading-change="handleListLoadingChange">
<template #header="{index,row}">
<div class="works-read-card-header">
<div class="works-read-card-heading">
<div class="works-read-card-title">{{row.name || '未命名作品'}}</div>
<div class="works-read-card-activity">{{row.activityName || '暂无活动信息'}}</div>
</div>
<div class="works-read-like-summary">
<van-icon name="friends"></van-icon>
<span>已有 {{row.num || 0}} 人点赞</span>
</div>
</div>
</template>
<template v-slot="{index,row}">
<table-column label="作品类型">{{row.worksName || '--'}}</table-column>
<table-column label="作者">{{row.userName || '--'}}</table-column>
<table-column label="工号">{{row.loginName || '--'}}</table-column>
<table-column label="上传时间">{{$moment(row.createdAt).format('YYYY-MM-DD')}}</table-column>
<div v-if="hasListFiles(row.files)" class="works-read-files-row">
<div class="works-read-files-label">
<span>上传附件</span>
<span v-if="getAttachmentCount(row.files) > 1" class="works-read-files-hint">
共{{getAttachmentCount(row.files)}}个附件 · 左滑查看更多
</span>
</div>
<div class="works-read-files-scroll">
<file-preview :files="row.files" complete_result></file-preview>
</div>
</div>
</template>
<template #actions="{index,row}">
<div class="action-btn" @click="onView(row)">
<van-icon class="works-read-action-icon" name="eye-o"></van-icon>
<span>查看</span>
</div>
<div v-if="row.nbCount > 0" class="works-read-like-action-wrap">
<div class="action-btn" v-if="!row.isThisLike" @click="onLike(row)">
<van-icon class="works-read-action-icon" name="good-job-o"></van-icon>
<span>点赞</span>
</div>
<div class="action-btn delete" v-else @click="onDeleteLike(row)">
<van-icon class="works-read-action-icon" name="good-job"></van-icon>
<span>取消点赞</span>
</div>
<span v-if="likeHeartEffects[row.id]"
:key="'like-heart-' + row.id + '-' + (likeHeartEffectKeys[row.id] || 0)"
class="works-read-like-hearts" aria-hidden="true">
<span v-for="heartIndex in 6" :key="heartIndex"
class="works-read-like-heart" :class="'is-heart-' + heartIndex">♥</span>
</span>
</div>
</template>
</table-list>
</div>
</main>
</div>
<info ref="infoRef"></info>
</div>
<script nonce="${cspNonce!}">
<!--#include('../mine/info.js'){}#-->
new Vue({
el: "#app",
store,
components: {
'info': INFO,
},
data() {
return {
pageForm: {
pageNumber: 1,
pageSize: 10,
totalCount: 0,
searchKeyword: '',
pageOrderName: 'createdAt',
activityId: '',
year: new Date().getFullYear(),
},
activityOptions: [],
orderOptions: [
{text: '按上传时间排序', value: 'createdAt'},
{text: '按点赞数量排序', value: 'num'},
],
filterOpened: false,
likeLoading: false,
likeHeartEffects: {},
likeHeartEffectKeys: {},
likeHeartEffectTimers: {},
skeletonLoading: true,
historyLayerPageKey: 'works-collection-read-h5',
}
},
methods: {
// state.initial 表示查询前列表为空,仅在首次或重置查询时显示整组卡片骨架。
handleListLoadingChange(state) {
if (state.initial) this.$set(this, 'skeletonLoading', state.loading)
},
// 判断列表附件是否包含有效记录;files 支持上传组件保存的 JSON 字符串或数组。
hasListFiles(files) {
if (Array.isArray(files)) return files.length > 0
if (!files) return false
try {
const fileList = JSON.parse(files)
return Array.isArray(fileList) && fileList.length > 0
} catch (err) {
return false
}
},
// 获取当前作品的全部附件数量;files 支持 JSON 字符串或数组,返回 Number 类型。
getAttachmentCount(files) {
if (Array.isArray(files)) return files.length
if (!files) return 0
try {
const fileList = JSON.parse(files)
return Array.isArray(fileList) ? fileList.length : 0
} catch (err) {
return 0
}
},
onLike(row) {
this.executeLikeAction(row, 'like')
},
onDeleteLike(row) {
this.executeLikeAction(row, 'cancel')
},
/**
* 执行作品点赞或取消点赞。
* row 需包含作品 id,action 传 like 或 cancel;接口返回 Result JSON,方法无返回值。
*/
executeLikeAction(row, action) {
if (!row || !row.id || this.likeLoading) return
const isCancel = action === 'cancel'
const url = isCancel
? '/platform/activity/worksCollection/read/h5/doDeleteLike'
: '/platform/activity/worksCollection/read/h5/doLike'
this.$set(this, 'likeLoading', true)
this.$axios.post(url, {uploadId: row.id}).then((res) => {
if (res.code === 0) {
const currentNum = Number(row.num) || 0
this.$set(row, 'num', isCancel ? Math.max(currentNum - 1, 0) : currentNum + 1)
this.$set(row, 'isThisLike', !isCancel)
if (!isCancel) this.playLikeHeart(row.id)
}
}).finally(() => {
this.$set(this, 'likeLoading', false)
})
},
// 点赞成功后为当前作品播放多颗红色爱心错峰上浮动画,重复触发时重新开始计时。
playLikeHeart(rowId) {
if (!rowId) return
if (this.likeHeartEffectTimers[rowId]) {
clearTimeout(this.likeHeartEffectTimers[rowId])
}
this.$set(this.likeHeartEffects, rowId, false)
this.$set(this.likeHeartEffectKeys, rowId, (this.likeHeartEffectKeys[rowId] || 0) + 1)
this.$nextTick(() => {
this.$set(this.likeHeartEffects, rowId, true)
const timer = setTimeout(() => {
this.$set(this.likeHeartEffects, rowId, false)
this.$delete(this.likeHeartEffectTimers, rowId)
}, 1400)
this.$set(this.likeHeartEffectTimers, rowId, timer)
})
},
onView(row) {
this.$refs.infoRef.onOpen(row)
},
onFilterOpen() {
this.$set(this, 'filterOpened', true)
},
onFilterClose() {
this.$set(this, 'filterOpened', false)
},
handlePageBack() {
if (window.h5HistoryLayerManager.stack.length) {
window.h5HistoryLayerManager.close()
return
}
this.historyBack()
},
// 活动接口接收年度,返回 id、name 组成的活动列表;完成后触发首次作品查询。
listActivity() {
return this.$axios.post('/platform/activity/worksCollection/common/listActivity', {
year: this.pageForm.year
}, {
h5SkeletonLoading: true
}).then((res) => {
if (res.code === 0) {
const options = (res.data || []).map((item) => {
return {text: item.name, value: item.id}
})
this.$set(this, 'activityOptions', options)
this.$set(this.pageForm, 'activityId', options.length > 0 ? options[0].value : '')
}
})
},
onReady() {
this.listActivity().then(() => {
this.doSearch()
})
},
doSearch() {
this.$nextTick(() => {
this.$set(this.pageForm, 'pageNumber', 1)
this.$set(this.pageForm, 'totalCount', 0)
this.$refs.tableListRef.doSearch()
})
}
},
created() {
window.h5HistoryLayerManager.register(this.historyLayerPageKey, null)
},
beforeDestroy() {
Object.keys(this.likeHeartEffectTimers).forEach((rowId) => {
clearTimeout(this.likeHeartEffectTimers[rowId])
})
window.h5HistoryLayerManager.unregister(this.historyLayerPageKey)
}
})
</script>
<!--#
}
#-->
@@ -0,0 +1,559 @@
const applyForm = {
template:
/*language=HTML*/
`
<div>
<van-popup class="works-apply-popup" :style="{height: '100%', width: '100%'}" position="right"
safe-area-inset-bottom :close-on-click-overlay="false" v-model="visible">
<div class="works-apply-page">
<van-sticky>
<van-nav-bar title="作品提交" left-text="返回" left-arrow @click-left="closeApply"></van-nav-bar>
</van-sticky>
<van-form ref="formRef" class="works-apply-form form-container">
<div ref="applyScrollRef" class="works-apply-scroll">
<section class="works-apply-section">
<h2 class="works-apply-section__title">基础信息</h2>
<van-cell-group :border="false">
<van-field label="姓名" readonly v-model="formData.username"></van-field>
<van-field label="工号" readonly v-model="formData.loginname"></van-field>
<van-field label="所在单位" readonly v-model="formData.unitName"></van-field>
<van-field label="所属工会" readonly v-model="formData.unionName"></van-field>
<van-field label="性别" readonly v-model="formData.sex"></van-field>
</van-cell-group>
</section>
<section class="works-apply-section">
<h2 class="works-apply-section__title">作品信息</h2>
<van-cell-group :border="false">
<van-field label="主题类型" required readonly is-link name="subjectName"
placeholder="请选择主题类型" :rules="[{required: true, message: '请选择主题类型'}]"
v-model="formData.subjectName" @click="openSubjectPicker"></van-field>
<van-field label="作品类型" required readonly is-link name="worksName"
placeholder="请先选择主题类型" :rules="[{required: true, message: '请选择作品类型'}]"
v-model="formData.worksName" @click="openWorksPicker"></van-field>
<van-field label="作品名称" required name="name" maxlength="100" show-word-limit
placeholder="请填写作品名称" :rules="[{required: true, message: '请填写作品名称'}]"
v-model="formData.name"></van-field>
<van-field class="more-text" label="作品描述" required type="textarea" name="description"
rows="4" autosize show-word-limit :maxlength="descriptionMaxLength"
placeholder="请填写作品描述" :rules="[{required: true, message: '请填写作品描述'}]"
v-model="formData.description"></van-field>
</van-cell-group>
</section>
<section class="works-apply-section">
<h2 class="works-apply-section__title">作品附件</h2>
<div class="works-apply-section__hint">附件说明:请根据所选作品类型上传符合要求的文件</div>
<van-cell-group :border="false">
<van-field class="more-text works-apply-upload-field" name="files" label="" required
:rules="[{required: true, message: '请上传附件'}]">
<template #input>
<h5-file-upload slot="input" :value.sync="formData.files"
:upload_number="chooseWorksType && chooseWorksType.allowFileNum"
upload_mode="file" upload_result_category="array"
upload_result_type="url" complete_result :accept="fileAccept">
</h5-file-upload>
</template>
</van-field>
</van-cell-group>
</section>
</div>
<div class="works-apply-footer">
<van-button type="primary" block round :loading="formLoading" loading-text="提交中..."
@click="onSubmit">提交作品</van-button>
</div>
</van-form>
</div>
</van-popup>
<van-popup class="works-option-popup" v-model="showOptionPicker" position="bottom" round
:close-on-click-overlay="false">
<div class="works-option-popup__header">
<button type="button" class="works-option-popup__action" @click="cancelOptionPopup">取消</button>
<div class="works-option-popup__title">{{optionPopupTitle}}</div>
<button type="button" class="works-option-popup__action"
:disabled="pendingOptionValue === null || pendingOptionValue === undefined || pendingOptionValue === ''"
@click="confirmOptionPopup">确认</button>
</div>
<div class="works-option-popup__list">
<div v-if="optionPopupOptions.length === 0" class="works-option-popup__empty">暂无可选内容</div>
<button v-for="option in optionPopupOptions" :key="option.value" type="button"
class="works-option-popup__option"
:class="{'is-selected': pendingOptionValue === option.value}"
:aria-pressed="pendingOptionValue === option.value"
@click="selectOptionPopupItem(option)">
<span class="works-option-popup__option-text">{{option.text}}</span>
</button>
</div>
</van-popup>
<van-dialog class="works-apply-confirm" title="提交确认" message="确认提交当前作品信息吗?"
:value="submitConfirmVisible" :close-on-click-overlay="false" show-cancel-button
cancel-button-text="取消" confirm-button-text="确认提交"
@cancel="closeSubmitConfirm" @confirm="confirmSubmit"></van-dialog>
</div>
`,
style: /*language=CSS*/ `
/deep/ .works-apply-popup {
background-color: #f3f7fd;
}
/deep/ .works-apply-page {
position: relative;
min-height: 100vh;
background-color: #f3f7fd;
}
/deep/ .works-apply-page .van-nav-bar {
background-color: #fff;
}
/deep/ .works-apply-page .van-nav-bar__title {
color: #1f2937;
font-size: 16px;
}
/deep/ .works-apply-page .van-nav-bar .van-icon,
/deep/ .works-apply-page .van-nav-bar__text {
color: #397fbd;
}
/deep/ .works-apply-form.form-container {
min-height: 100%;
padding: 0;
box-sizing: border-box;
background: #f3f7fc;
}
/deep/ .works-apply-scroll {
height: calc(100vh - 46px);
padding: 12px 15px calc(84px + env(safe-area-inset-bottom));
overflow-x: hidden;
overflow-y: auto;
box-sizing: border-box;
background: #f3f7fc;
}
/deep/ .works-apply-section {
margin-bottom: 11px;
overflow: hidden;
border: 1px solid #e9eff7;
border-radius: 10px;
background: #fff;
box-shadow: 0 3px 11px rgba(51, 87, 126, .08);
}
/deep/ .works-apply-section__title {
position: relative;
margin: 0;
padding: 12px 14px 8px 21px;
color: #354255;
font-size: 15px;
line-height: 20px;
font-weight: 600;
background: transparent;
}
/deep/ .works-apply-section__title::before {
position: absolute;
top: 15px;
bottom: 11px;
left: 12px;
width: 2px;
border-radius: 2px;
background: #1989fa;
content: "";
}
/deep/ .works-apply-section__hint {
padding: 0 14px 8px 21px;
color: orangered;
font-size: 14px;
line-height: 18px;
font-weight: normal;
}
/deep/ .works-apply-section .van-cell-group {
margin: 0;
background: transparent;
}
/deep/ .works-apply-section .van-cell {
min-height: 44px;
padding: 10px 14px;
background: transparent;
}
/deep/ .works-apply-section .van-cell:not(:last-child)::after {
right: 14px;
left: 14px;
border-color: #eef2f7;
}
/deep/ .works-apply-section .van-field__label {
width: 84px;
color: #596779;
font-size: 14px;
line-height: 24px;
}
/deep/ .works-apply-section .van-field__value,
/deep/ .works-apply-section .van-field__control {
color: #4f5d70;
font-size: 14px;
line-height: 24px;
}
/deep/ .works-apply-section .van-field__control::placeholder {
color: #b1bac6;
}
/deep/ .works-apply-section .van-cell--required::before {
left: 6px;
color: #f06464;
}
/deep/ .works-apply-upload-field .van-field__value {
overflow: visible;
}
/deep/ .works-apply-footer {
position: fixed;
z-index: 100;
right: 0;
bottom: 0;
left: 0;
display: flex;
padding: 10px 18px calc(10px + env(safe-area-inset-bottom));
border-top: 1px solid #edf1f6;
background: rgba(255, 255, 255, .98);
box-shadow: 0 -4px 14px rgba(50, 78, 112, .06);
}
/deep/ .works-apply-footer .van-button {
flex: 1;
height: 40px;
margin: 0;
border-color: #1989fa;
border-radius: 14px !important;
font-size: 16px;
background: #1989fa;
}
/deep/ .works-option-popup {
display: flex;
height: 50%;
padding-bottom: env(safe-area-inset-bottom);
flex-direction: column;
box-sizing: border-box;
background: #f5f7fa;
}
/deep/ .works-option-popup__header {
display: flex;
min-height: 52px;
padding: 0 16px;
align-items: center;
justify-content: space-between;
flex: none;
border-bottom: 1px solid #edf1f6;
background: #fff;
}
/deep/ .works-option-popup__title {
color: #263548;
font-size: 17px;
line-height: 24px;
font-weight: 600;
}
/deep/ .works-option-popup__action {
min-width: 48px;
padding: 8px 0;
border: 0;
color: #1989fa;
font-size: 15px;
line-height: 22px;
text-align: left;
background: transparent;
}
/deep/ .works-option-popup__action:last-child {
text-align: right;
}
/deep/ .works-option-popup__action:disabled {
color: #b7c0cc;
}
/deep/ .works-option-popup__list {
min-height: 0;
padding: 12px;
overflow-y: auto;
flex: 1;
-webkit-overflow-scrolling: touch;
}
/deep/ .works-option-popup__empty {
padding: 44px 18px;
color: #98a4b3;
font-size: 14px;
line-height: 22px;
text-align: center;
}
/deep/ .works-option-popup__option {
display: flex;
width: 100%;
min-height: 58px;
margin-bottom: 10px;
padding: 12px 14px;
align-items: center;
border: 1px solid #e3e9f1;
border-radius: 10px;
color: #536174;
text-align: left;
background: #fff;
}
/deep/ .works-option-popup__option:last-child {
margin-bottom: 0;
}
/deep/ .works-option-popup__option.is-selected {
border-color: #1989fa;
color: #176fbd;
background: #eef7ff;
box-shadow: 0 3px 10px rgba(25, 137, 250, .1);
}
/deep/ .works-option-popup__option-text {
min-width: 0;
flex: 1;
font-size: 15px;
line-height: 23px;
word-break: break-word;
overflow-wrap: anywhere;
}
/deep/ .works-apply-confirm .van-dialog__header {
color: #263548;
font-size: 17px;
font-weight: 600;
}
/deep/ .works-apply-confirm .van-dialog__message {
padding: 24px 20px;
color: #536174;
font-size: 15px;
line-height: 24px;
text-align: center;
}
`,
data() {
return {
row: {}, visible: false, formData: {}, formLoading: false,
subjectTypesOptions: [], worksTypeOptions: [],
showOptionPicker: false, optionPopupType: "", optionPopupTitle: "",
optionPopupOptions: [], pendingOptionValue: "",
submitConfirmVisible: false, fileAccept: null, chooseWorksType: {},
submitSuccessMode: "navigate",
historyLayerPageKey: "works-collection-upload-h5",
historyLayerUnsubscribe: null, ownsHistoryRegistration: false
}
},
computed: {
descriptionMaxLength() {
return this.chooseWorksType && this.chooseWorksType.maxWordCount ? this.chooseWorksType.maxWordCount : 150
}
},
methods: {
resetFormData() {
const user = this.$store.state.user || {}
const union = user.union || {}
const unit = user.unit || {}
this.$set(this, "formData", {})
this.$set(this.formData, "username", user.username)
this.$set(this.formData, "loginname", user.loginname)
this.$set(this.formData, "unionName", union.name)
this.$set(this.formData, "unitName", unit.name)
this.$set(this.formData, "sex", user.sex)
this.$set(this.formData, "files", [])
this.$set(this, "subjectTypesOptions", [])
this.$set(this, "worksTypeOptions", [])
this.$set(this, "chooseWorksType", {})
this.$set(this, "fileAccept", null)
},
onOpen(row) {
this.resetFormData()
this.$set(this, "submitSuccessMode", "navigate")
this.$set(this, "row", row)
this.$set(this.formData, "activityId", row.id)
this.activityChange(row.id).then(() => {
window.h5HistoryLayerManager.open("works-collection-apply")
this.resetScrollPosition()
})
},
onOpenEdit(row) {
const formData = clone(row)
formData.files = this.normalizeFiles(formData.files)
this.resetFormData()
this.$set(this, "submitSuccessMode", "close")
this.$set(this, "row", formData)
this.$set(this, "formData", formData)
const user = this.$store.state.user || {}
this.$set(this.formData, "username", user.username)
this.$set(this.formData, "loginname", user.loginname)
this.$set(this.formData, "sex", user.sex)
this.activityChange(formData.activityId).then(() => {
// 编辑时以主题主键匹配当前活动配置,避免列表未携带主题名称时无法回显。
const subjectType = this.subjectTypesOptions.find((item) => item.id === formData.subjectId)
if (subjectType) this.$set(this.formData, "subjectName", subjectType.typeName)
return this.subjectChange(formData.subjectId)
}).then(() => {
this.setSelectedWorksType(formData.worksId)
window.h5HistoryLayerManager.open("works-collection-apply")
this.resetScrollPosition()
})
},
// 表单弹窗会被复用,每次打开后需清除上一次编辑或提交时保留的滚动位置。
resetScrollPosition() {
this.$nextTick(() => {
if (this.$refs.applyScrollRef) this.$refs.applyScrollRef.scrollTop = 0
})
},
// 附件可能由列表 SQL 返回 JSON 字符串,也可能已被序列化为数组;统一转换为上传组件需要的数组。
normalizeFiles(files) {
if (Array.isArray(files)) return files
if (!files) return []
try {
const result = JSON.parse(files)
return Array.isArray(result) ? result : []
} catch (err) {
return []
}
},
closeApply() { window.h5HistoryLayerManager.close("works-collection-apply") },
// 打开统一选择弹框:主题选项由活动配置提供,确认后再写入表单。
openSubjectPicker() {
this.$set(this, "optionPopupType", "subject")
this.$set(this, "optionPopupTitle", "选择主题类型")
this.$set(this, "optionPopupOptions", this.subjectTypesOptions.map((item) => {
return {value: item.id, text: item.typeName, source: item}
}))
this.$set(this, "pendingOptionValue", this.formData.subjectId || "")
window.h5HistoryLayerManager.open("works-collection-option-picker")
},
openWorksPicker() {
if (!this.formData.subjectId) {
this.$toast("请先选择主题类型")
return
}
this.$set(this, "optionPopupType", "works")
this.$set(this, "optionPopupTitle", "选择作品类型")
this.$set(this, "optionPopupOptions", this.worksTypeOptions.map((item) => {
return {value: item.id, text: item.worksTypeName, source: item}
}))
this.$set(this, "pendingOptionValue", this.formData.worksId || "")
window.h5HistoryLayerManager.open("works-collection-option-picker")
},
cancelOptionPopup() { window.h5HistoryLayerManager.close("works-collection-option-picker") },
selectOptionPopupItem(option) { this.$set(this, "pendingOptionValue", option.value) },
/**
* 确认类型选项并回填作品表单。
* optionPopupType 为 subject 或 works,pendingOptionValue 为对应业务主键;方法无返回值。
*/
confirmOptionPopup() {
const selectedOption = this.optionPopupOptions.find((item) => {
return item.value === this.pendingOptionValue
})
if (!selectedOption) return
if (this.optionPopupType === "subject") {
this.onSubjectConfirm(selectedOption)
return
}
this.onWorksConfirm(selectedOption)
},
onWorksConfirm(selectedOption) {
this.$set(this.formData, "worksName", selectedOption.text)
this.$set(this.formData, "worksId", selectedOption.value)
this.setSelectedWorksType(selectedOption.value)
this.cancelOptionPopup()
},
onSubjectConfirm(selectedOption) {
this.$set(this.formData, "subjectName", selectedOption.text)
this.$set(this.formData, "subjectId", selectedOption.value)
this.$set(this.formData, "worksName", "")
this.$set(this.formData, "worksId", "")
this.$set(this.formData, "files", [])
this.$set(this, "chooseWorksType", {})
this.$set(this, "fileAccept", null)
this.subjectChange(selectedOption.value).then(() => { this.cancelOptionPopup() })
},
setSelectedWorksType(worksId) {
const worksType = this.worksTypeOptions.find((item) => item.id === worksId) || {}
this.$set(this, "chooseWorksType", worksType)
if (worksType.allowFileTypes && worksType.allowFileTypes.length > 0) {
this.$set(this, "fileAccept", worksType.allowFileTypes.map((item) => "." + item).join(","))
return
}
this.$set(this, "fileAccept", null)
},
onSubmit() {
this.$refs.formRef.validate().then(() => {
window.h5HistoryLayerManager.open("works-collection-submit-confirm")
}).catch(() => {
// 表单校验失败时由字段规则直接展示提示,不进入提交确认。
})
},
closeSubmitConfirm() { window.h5HistoryLayerManager.close("works-collection-submit-confirm") },
confirmSubmit() {
window.h5HistoryLayerManager.close("works-collection-submit-confirm", () => { this.submitForm() })
},
submitForm() {
this.$set(this, "formLoading", true)
this.$axios.post("/platform/activity/worksCollection/upload" + (this.formData.id ? "/update" : "/insert"), {
data: JSON.stringify(this.formData)
}).then((res) => {
if (res.code === 0) {
this.$toast.success("提交成功")
if (this.submitSuccessMode === "close") {
// 我的作品页使用弹窗编辑,成功后只关闭当前表单并通知父页面刷新列表。
window.h5HistoryLayerManager.close("works-collection-apply", () => {
this.$emit("submit-success")
})
return
}
window.h5HistoryLayerManager.closeAllAndNavigate("/platform/activity/worksCollection/mine/h5")
}
}).finally(() => { this.$set(this, "formLoading", false) })
},
activityChange(value) {
return this.$axios.post("/platform/activity/worksCollection/common/getSubjectTypes", {activityId: value}).then((res) => {
if (res.code === 0) this.$set(this, "subjectTypesOptions", res.data || [])
})
},
subjectChange(value) {
return this.$axios.post("/platform/activity/worksCollection/common/getWorksTypes", {subjectId: value}).then((res) => {
if (res.code === 0) this.$set(this, "worksTypeOptions", res.data || [])
})
},
syncHistoryLayers(stack) {
this.$set(this, "visible", stack.includes("works-collection-apply"))
this.$set(this, "showOptionPicker", stack.includes("works-collection-option-picker"))
this.$set(this, "submitConfirmVisible", stack.includes("works-collection-submit-confirm"))
}
},
created() {
if (!window.h5HistoryLayerManager.pageKey) {
window.h5HistoryLayerManager.ensureRegistered(this.historyLayerPageKey)
this.$set(this, "ownsHistoryRegistration", true)
}
this.$set(this, "historyLayerUnsubscribe", window.h5HistoryLayerManager.subscribe((stack) => {
this.syncHistoryLayers(stack)
}))
},
beforeDestroy() {
if (this.historyLayerUnsubscribe) this.historyLayerUnsubscribe()
if (this.ownsHistoryRegistration) window.h5HistoryLayerManager.unregister(this.historyLayerPageKey)
}
}
@@ -0,0 +1,834 @@
<!--#
layout("/layouts/platform_h5.html"){
#-->
<style>
body {
background-color: #f3f7fd;
}
.works-collection-page {
min-height: 100vh;
padding-bottom: calc(24px + env(safe-area-inset-bottom));
box-sizing: border-box;
background-color: #f3f7fd;
}
.works-collection-page .van-nav-bar {
background-color: #fff;
}
.works-collection-page .van-nav-bar__title {
color: #1f2937;
font-size: 16px;
}
.works-collection-page .van-nav-bar .van-icon,
.works-collection-page .van-nav-bar__text {
color: #397fbd;
}
.works-collection-content {
padding: 12px 12px 0;
}
.works-collection-hero {
position: relative;
height: 134px;
overflow: hidden;
border-radius: 10px;
color: #fff;
background-color: #247fe8;
box-shadow: 0 8px 20px rgba(51, 117, 219, .18);
}
.works-collection-hero__image {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
object-fit: cover;
object-position: center;
}
.works-collection-hero__copy {
position: relative;
z-index: 1;
display: flex;
width: 64%;
height: 100%;
padding-left: 16px;
flex-direction: column;
justify-content: center;
box-sizing: border-box;
background: linear-gradient(90deg, rgba(22, 112, 232, .98) 0%, rgba(22, 112, 232, .88) 52%, rgba(22, 112, 232, 0) 100%);
}
.works-collection-hero__title {
font-size: 25px;
line-height: 34px;
font-weight: 700;
letter-spacing: 1px;
text-shadow: 0 2px 6px rgba(17, 86, 182, .18);
}
.works-collection-hero__subtitle {
margin-top: 4px;
font-size: 13px;
line-height: 20px;
letter-spacing: .5px;
opacity: .95;
}
.works-collection-sticky {
padding: 10px 0 1px;
background-color: #f3f7fd;
}
.van-sticky--fixed .works-collection-sticky {
padding-right: 12px;
padding-left: 12px;
box-sizing: border-box;
}
.works-collection-search {
margin: 0 0 8px;
padding: 0;
overflow: hidden;
border-radius: 12px;
background-color: #fff;
box-shadow: 0 5px 16px rgba(43, 73, 112, .1);
}
.works-collection-search .van-search__content {
height: 42px;
padding-left: 12px;
align-items: center;
border-radius: 12px;
background-color: #fff;
}
.works-collection-search .van-field__control {
color: #263548;
font-size: 14px;
}
.works-collection-search .van-field__control::placeholder {
color: #a7b0bf;
}
.works-collection-filter {
margin: 0 0 10px;
overflow: hidden;
border-radius: 10px;
box-shadow: 0 5px 16px rgba(43, 73, 112, .08);
transition: border-radius .18s ease;
}
.works-collection-filter.is-open {
border-radius: 10px 10px 0 0;
}
.works-collection-filter .van-dropdown-menu__bar {
height: 44px;
box-shadow: none;
}
.works-collection-filter .van-dropdown-menu__item {
min-width: 0;
overflow: hidden;
}
.works-collection-filter .van-dropdown-menu__title {
width: auto;
max-width: calc(100% - 24px);
padding: 0 12px 0 0;
overflow: hidden;
color: #66758a;
font-size: 14px;
text-align: center;
text-overflow: ellipsis;
white-space: nowrap;
box-sizing: border-box;
}
.works-collection-filter .van-dropdown-menu__title::after {
right: 0;
}
.works-collection-filter .van-dropdown-menu__title--active,
.works-collection-filter .van-dropdown-item__option--active,
.works-collection-filter .van-dropdown-item__option--active .van-dropdown-item__icon {
color: #1989fa;
}
.works-collection-filter .van-dropdown-menu__title--down::after {
border-color: transparent transparent #1989fa #1989fa;
}
.works-collection-filter .van-dropdown-item__content {
width: calc(100% - 24px);
margin: 0 12px;
overflow: hidden;
box-sizing: border-box;
border-radius: 0 0 12px 12px;
box-shadow: 0 10px 24px rgba(31, 65, 108, .14);
}
.works-collection-filter .van-dropdown-item__option {
min-height: 48px;
padding: 0 16px;
color: #46566c;
font-size: 14px;
}
.works-collection-list {
display: flex;
flex-direction: column;
gap: 10px;
}
.works-collection-card {
position: relative;
overflow: hidden;
border-radius: 10px;
background-color: #fff;
box-shadow: 0 7px 20px rgba(55, 85, 126, .08);
cursor: pointer;
}
.works-collection-card__cover,
.works-collection-card__cover-state {
display: flex;
width: 100%;
height: 150px;
align-items: center;
justify-content: center;
box-sizing: border-box;
background-color: #edf4fc;
}
.works-collection-card__cover {
display: block;
object-fit: cover;
}
.works-collection-card__cover-state {
flex-direction: column;
row-gap: 8px;
color: #8da1ba;
font-size: 13px;
}
.works-collection-card__cover-state .van-icon {
font-size: 34px;
}
.works-collection-card__cover-state.is-error {
color: #c27b75;
background-color: #fff3f1;
}
.works-collection-card__body {
padding: 12px 38px 14px 14px;
}
.works-collection-card__title {
min-width: 0;
overflow: hidden;
color: #303846;
font-size: 14px;
line-height: 21px;
text-overflow: ellipsis;
white-space: nowrap;
}
.works-collection-card__time {
margin-top: 4px;
overflow: hidden;
color: #98a2b2;
font-size: 12px;
line-height: 17px;
text-overflow: ellipsis;
white-space: nowrap;
}
.works-collection-card__meta {
display: flex;
margin-top: 10px;
padding-top: 9px;
flex-wrap: wrap;
gap: 6px;
border-top: 1px solid #edf1f6;
}
.works-collection-card__tag {
display: inline-flex;
padding: 3px 8px;
align-items: center;
border-radius: 4px;
color: #397de0;
background-color: #edf5ff;
font-size: 11px;
line-height: 16px;
}
.works-collection-card__tag-label {
margin-right: 4px;
color: #8aa7cf;
}
.works-collection-card__arrow {
position: absolute;
top: 164px;
right: 14px;
color: #b4bdca;
font-size: 16px;
}
.works-collection-empty {
display: flex;
min-height: 260px;
padding: 26px 18px 32px;
align-items: center;
justify-content: center;
flex-direction: column;
box-sizing: border-box;
text-align: center;
}
.works-collection-empty img {
display: block;
width: 82%;
max-width: 280px;
height: auto;
object-fit: contain;
}
.works-collection-empty__title {
margin-top: 14px;
color: #50627a;
font-size: 15px;
line-height: 22px;
font-weight: 600;
}
.works-collection-empty__hint {
margin-top: 5px;
color: #9aa7b8;
font-size: 12px;
line-height: 18px;
}
.works-collection-page .van-list__loading {
color: #9aa7b8;
font-size: 13px;
line-height: 44px;
}
.works-collection-skeleton {
display: flex;
flex-direction: column;
gap: 10px;
}
.works-collection-skeleton__card {
overflow: hidden;
border-radius: 10px;
background-color: #fff;
box-shadow: 0 7px 20px rgba(55, 85, 126, .08);
}
.works-collection-skeleton__cover,
.works-collection-skeleton__body .van-skeleton {
padding: 0;
}
.works-collection-skeleton__cover .van-skeleton__row {
width: 100% !important;
height: 150px;
margin: 0;
border-radius: 0;
}
.works-collection-skeleton__body {
padding: 13px 14px 15px;
}
.works-collection-skeleton__body .van-skeleton__title {
width: 54%;
height: 15px;
margin-bottom: 12px;
}
.works-collection-skeleton__body .van-skeleton__row {
height: 12px;
margin-top: 9px;
}
.works-collection-skeleton__body .van-skeleton__row:last-child {
width: 46%;
}
.works-intro-popup,
.works-intro-page {
background-color: #f3f7fd;
}
.works-intro-page {
position: relative;
min-height: 100vh;
}
.works-intro-page .van-nav-bar {
background-color: #fff;
}
.works-intro-page .van-nav-bar__title {
color: #1f2937;
font-size: 16px;
}
.works-intro-page .van-nav-bar .van-icon,
.works-intro-page .van-nav-bar__text {
color: #397fbd;
}
.works-intro-scroll {
height: calc(100vh - 46px);
padding: 12px 12px calc(92px + env(safe-area-inset-bottom));
overflow-x: hidden;
overflow-y: auto;
box-sizing: border-box;
}
.works-intro-hero {
display: flex;
min-height: 82px;
padding: 16px;
align-items: center;
box-sizing: border-box;
border-radius: 12px;
color: #fff;
background: linear-gradient(120deg, #2d7eea 0%, #5ba6f7 100%);
box-shadow: 0 8px 20px rgba(45, 126, 234, .18);
}
.works-intro-hero__icon {
display: flex;
width: 44px;
height: 44px;
margin-right: 12px;
flex: 0 0 44px;
align-items: center;
justify-content: center;
border-radius: 11px;
color: #2d7eea;
background-color: rgba(255, 255, 255, .92);
font-size: 25px;
}
.works-intro-hero__title {
font-size: 17px;
line-height: 24px;
font-weight: 600;
}
.works-intro-hero__subtitle {
margin-top: 3px;
font-size: 12px;
line-height: 18px;
opacity: .9;
}
.works-intro-card {
margin-top: 12px;
padding: 18px 16px;
overflow: hidden;
border-radius: 12px;
background-color: #fff;
box-shadow: 0 7px 20px rgba(55, 85, 126, .08);
}
.works-intro-content,
.works-intro-content div,
.works-intro-content p,
.works-intro-content span,
.works-intro-content li {
color: #3f4c5e !important;
font-size: 14px !important;
line-height: 1.8 !important;
white-space: normal;
word-break: break-word;
}
.works-intro-content h1,
.works-intro-content h2,
.works-intro-content h3 {
margin: 16px 0 10px !important;
color: #1f2f43 !important;
font-size: 19px !important;
line-height: 1.5 !important;
font-weight: 600 !important;
}
.works-intro-content > :first-child {
margin-top: 0 !important;
}
.works-intro-content p {
margin-top: 0 !important;
margin-bottom: 8px !important;
}
.works-intro-content img {
max-width: 100% !important;
height: auto !important;
border-radius: 8px;
}
.works-intro-content table {
display: block;
max-width: 100%;
overflow-x: auto;
border-collapse: collapse;
}
.works-intro-empty {
display: flex;
min-height: 180px;
margin-top: 12px;
align-items: center;
justify-content: center;
flex-direction: column;
row-gap: 10px;
border-radius: 12px;
color: #9aa7b8;
background-color: #fff;
box-shadow: 0 7px 20px rgba(55, 85, 126, .08);
font-size: 14px;
}
.works-intro-empty .van-icon {
color: #83b5f4;
font-size: 36px;
}
.works-intro-footer {
position: fixed;
right: 0;
bottom: 0;
left: 0;
z-index: 5;
padding: 10px 12px calc(10px + env(safe-area-inset-bottom));
border-top: 1px solid #edf1f6;
background-color: rgba(255, 255, 255, .96);
box-sizing: border-box;
}
.works-intro-footer .van-button {
height: 42px;
border: 0;
border-radius: 21px;
font-size: 15px;
background-color: #1989fa;
box-shadow: 0 6px 16px rgba(25, 137, 250, .22);
}
.works-intro-footer .van-button--disabled {
color: #8b98aa !important;
background-color: #eef2f7 !important;
opacity: 1;
box-shadow: none;
}
.works-intro-footer .van-count-down {
display: inline-block;
margin-left: 4px;
color: #fff;
font-size: 13px;
vertical-align: middle;
}
</style>
<div id="app" v-cloak>
<div class="works-collection-page">
<van-nav-bar title="作品征集" left-text="返回" left-arrow @click-left="handlePageBack" fixed placeholder></van-nav-bar>
<div class="works-collection-content">
<section class="works-collection-hero">
<img class="works-collection-hero__image" src="/assets/mobile/img/activity/hero/brand-hero-v2.png" alt="作品征集插画">
<div class="works-collection-hero__copy">
<div class="works-collection-hero__title">作品征集</div>
<div class="works-collection-hero__subtitle">汇聚创意 · 展示风采</div>
</div>
</section>
<van-sticky :offset-top="46">
<div class="works-collection-sticky">
<van-search
v-model="pageForm.searchKeyword"
class="works-collection-search"
placeholder="搜索作品征集活动"
shape="round"
clearable
@search="doSearch"
@clear="doSearch"
></van-search>
<van-dropdown-menu class="works-collection-filter" :class="{'is-open': filterOpened}" active-color="#1989fa">
<year-van-dropdown-item :num="5" @change="doSearch" @open="onFilterOpen" @close="onFilterClose" v-model="pageForm.year"></year-van-dropdown-item>
<van-dropdown-item v-model="pageForm.activityType" :options="typeOptions" :multiple="false" @change="doSearch" @open="onFilterOpen" @close="onFilterClose"></van-dropdown-item>
</van-dropdown-menu>
</div>
</van-sticky>
<div v-if="skeletonLoading" class="works-collection-skeleton">
<div v-for="index in 3" :key="'upload-skeleton-' + index" class="works-collection-skeleton__card">
<van-skeleton class="works-collection-skeleton__cover" :row="1" animated></van-skeleton>
<div class="works-collection-skeleton__body">
<van-skeleton title :row="2" animated></van-skeleton>
</div>
</div>
</div>
<van-pull-refresh v-show="!skeletonLoading" v-model="refreshing" @refresh="onRefresh">
<van-list v-model="loading" :finished="finished" :immediate-check="false" @load="onLoad">
<div class="works-collection-list">
<div v-for="row in tableData" :key="row.id" class="works-collection-card" @click="onView(row)">
<img
v-if="hasCover(row) && !imageLoadErrors[row.id]"
class="works-collection-card__cover"
:src="row.cover"
:alt="row.name"
@error="handleImageError(row)"
>
<div v-else class="works-collection-card__cover-state" :class="{'is-error': hasCover(row)}">
<van-icon name="photo-o"></van-icon>
<span>{{ hasCover(row) ? '图片加载失败' : '暂无图片' }}</span>
</div>
<div class="works-collection-card__body">
<div class="works-collection-card__title">{{ row.name }}</div>
<div class="works-collection-card__time" v-if="row.startDateTime && row.endDateTime">
征集时间:{{ $moment(row.startDateTime).format('MM/DD HH:mm') }}~{{ $moment(row.endDateTime).format('MM/DD HH:mm') }}
</div>
<div class="works-collection-card__meta" v-if="row.activityGroupName">
<span class="works-collection-card__tag" v-if="row.activityGroupName">
<span class="works-collection-card__tag-label">面向对象</span>{{ row.activityGroupName }}
</span>
</div>
</div>
<van-icon class="works-collection-card__arrow" name="arrow"></van-icon>
</div>
</div>
</van-list>
<div v-if="!loading && tableData.length === 0" class="works-collection-empty">
<img src="/assets/mobile/img/activity/culture-empty.png" alt="作品征集空状态插画">
<div class="works-collection-empty__title">暂无作品征集活动</div>
<div class="works-collection-empty__hint">当前筛选条件下暂时没有可展示的活动</div>
</div>
</van-pull-refresh>
</div>
</div>
<van-popup class="works-intro-popup" :style="{height: '100%', width: '100%'}" position="right" safe-area-inset-bottom :close-on-click-overlay="false" v-model="infoVisible">
<div class="works-intro-page">
<van-sticky>
<van-nav-bar title="征集介绍" left-text="返回" left-arrow @click-left="closeInfo"></van-nav-bar>
</van-sticky>
<div class="works-intro-scroll">
<div class="works-intro-hero">
<div class="works-intro-hero__icon">
<van-icon name="description"></van-icon>
</div>
<div>
<div class="works-intro-hero__title">作品征集须知</div>
<div class="works-intro-hero__subtitle">请仔细阅读征集内容与作品要求</div>
</div>
</div>
<article v-if="infoRow.content" class="works-intro-card">
<div class="works-intro-content" v-html="infoRow.content"></div>
</article>
<div v-else class="works-intro-empty">
<van-icon name="description"></van-icon>
<span>暂无征集介绍</span>
</div>
</div>
<div class="works-intro-footer">
<van-button v-if="canApply(infoRow)" @click="onApply(infoRow)" type="primary" block round>上传作品</van-button>
<van-button v-else-if="time < 0" @click="onApply(infoRow)" type="primary" block round>
距离开始
<van-count-down :time="Math.abs(time)" format="DD 天 HH 时 mm 分 ss 秒" @finish="onCountdownFinish"></van-count-down>
</van-button>
<van-button v-else disabled block round>{{ registrationStatusText(infoRow) }}</van-button>
</div>
</div>
</van-popup>
<apply-form ref="applyFormRef"></apply-form>
</div>
<script nonce="${cspNonce!}">
<!--#include("applyForm.js"){}#-->
const vue = new Vue({
el: "#app",
store,
components: {
"apply-form": applyForm
},
data() {
return {
time: 0,
tableData: [],
loading: false,
skeletonLoading: true,
finished: false,
refreshing: false,
imageLoadErrors: {},
filterOpened: false,
pageForm: {
pageNumber: 1,
pageSize: 10,
totalCount: 0,
searchKeyword: '',
year: new Date().getFullYear(),
activityType: 2,
},
typeOptions: [
{text: '全部', value: 1},
{text: '即将开始', value: 4},
{text: '征集中', value: 2},
{text: '已结束', value: 3},
],
infoVisible: false,
infoRow: {},
historyLayerPageKey: "works-collection-upload-h5",
historyLayerUnsubscribe: null
}
},
methods: {
hasCover(row) {
return !!(row && row.cover && String(row.cover).trim())
},
handleImageError(row) {
this.$set(this.imageLoadErrors, row.id, true)
},
onFilterOpen() {
this.$set(this, "filterOpened", true)
},
onFilterClose() {
this.$set(this, "filterOpened", false)
},
onCountdownFinish() {
this.$set(this, "time", 0)
},
isRegistrationClosed(row) {
return !!(row && row.endDateTime && !this.$moment().isBefore(this.$moment(row.endDateTime)))
},
canApply(row) {
if(!row || !row.startDateTime || !row.endDateTime) {
return false
}
const now = Date.now()
return now >= this.$moment(row.startDateTime).valueOf()
&& now <= this.$moment(row.endDateTime).valueOf()
},
registrationStatusText(row) {
if(!row || !row.startDateTime || !row.endDateTime) {
return '当前不可参与'
}
if(Date.now() < this.$moment(row.startDateTime).valueOf()) {
return '征集未开始'
}
return '征集已结束'
},
onView(row) {
this.$set(this, "infoRow", row)
this.$set(this, "time", this.$moment().diff(this.$moment(row.startDateTime), "milliseconds"))
window.h5HistoryLayerManager.open("works-collection-info")
},
closeInfo() {
window.h5HistoryLayerManager.close("works-collection-info")
},
handlePageBack() {
if (window.h5HistoryLayerManager.stack.length) {
window.h5HistoryLayerManager.close()
return
}
this.historyBack()
},
syncHistoryLayers(stack) {
this.$set(this, "infoVisible", stack.includes("works-collection-info"))
},
onApply(row) {
if(this.isRegistrationClosed(row)) {
this.$toast.fail("征集已结束")
return
}
if(this.$moment().isBefore(this.$moment(row.startDateTime))) {
this.onView(row)
return
}
this.$refs.applyFormRef.onOpen(row)
},
onLoad() {
if(!this.finished) {
this.loadActivities(false)
}
},
onRefresh() {
this.loadActivities(true)
},
loadActivities(reset) {
const showSkeleton = !!reset
if(reset) {
this.$set(this, "skeletonLoading", true)
this.$set(this.pageForm, "pageNumber", 1)
this.$set(this.pageForm, "totalCount", 0)
this.$set(this, "tableData", [])
this.$set(this, "finished", false)
this.$set(this, "imageLoadErrors", {})
}
this.$set(this, "loading", true)
this.$axios.post('/platform/activity/worksCollection/upload/h5/activityPageData', this.pageForm, {
h5SkeletonLoading: showSkeleton
}).then((res) => {
if(res.code === 0) {
const list = res.data && res.data.list ? res.data.list : []
this.$set(this, "tableData", this.pageForm.pageNumber === 1 ? list : this.tableData.concat(list))
this.$set(this.pageForm, "totalCount", res.data && res.data.totalCount ? res.data.totalCount : 0)
this.$set(this, "finished", this.tableData.length >= this.pageForm.totalCount)
if(!this.finished) {
this.$set(this.pageForm, "pageNumber", this.pageForm.pageNumber + 1)
}
}
}).finally(() => {
this.$set(this, "loading", false)
this.$set(this, "refreshing", false)
if(showSkeleton) this.$set(this, "skeletonLoading", false)
})
},
doSearch() {
this.loadActivities(true)
}
},
created() {
window.h5HistoryLayerManager.register(this.historyLayerPageKey, (stack) => {
this.syncHistoryLayers(stack)
})
this.$set(this, "historyLayerUnsubscribe", window.h5HistoryLayerManager.subscribe((stack) => {
this.syncHistoryLayers(stack)
}))
this.loadActivities(true)
},
beforeDestroy() {
if (this.historyLayerUnsubscribe) {
this.historyLayerUnsubscribe()
}
window.h5HistoryLayerManager.unregister(this.historyLayerPageKey)
}
})
</script>
<!--#
}
#-->