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>
<!--#
}
#-->
@@ -0,0 +1,137 @@
let ADDRESS = {
template: `
<div>
<van-address-list
:list="addressList"
@add="onAdd"
@edit="onEdit"
add-button-text="新增地址"
:switchable="false"
default-tag-text="默认"
@click-item="onClick"
v-if="addressList && addressList.length>0"
v-model="chosenAddressId"
></van-address-list>
<van-empty image="/assets/platform/plugins/vant-green/images/nodata/nodata.png" description="暂无数据" v-else>
<van-button @click="onAdd" class="mt10" round type="primary">新增地址</van-button>
</van-empty>
<van-action-sheet :close-on-click-overlay="false" :title="title" v-model="editActionSheet">
<van-address-edit
:address-info="addressInfo"
:area-columns-placeholder="['请选择', '请选择', '请选择']"
:area-list="areaList"
:show-delete="showDelete"
@delete="onDelete"
@save="onSave"
ref="address"
show-search-result
show-set-default
tel-maxlengtfals
></van-address-edit>
</van-action-sheet>
</div>
`,
store,
data() {
return {
areaList: areaList,
addressInfo: {},
addressList: [],
editActionSheet: false,
showDelete: "",
title: "",
chosenAddressId: "",
projectId: ""
}
},
methods: {
async selectUserAddress() {
const loading = this.$toast.loading({
message: "加载中...",
forbidClick: true,
overlay: true,
duration: 0
})
const resp = await this.$axios.post("/platform/benefit/address/selectUserAddress", { userId: this.$store.state.user.id })
resp.data.map((r) => {
r.name = r.userName
r.default = r.isDefault
r.address = "收货地址:" + r.province + r.city + r.county + r.addressDetail
})
setTimeout(() => {
this.addressList = resp.data
/* for (let i = 0; i < this.addressList.length; i++) {
if (this.addressList[i].default==1) {
this.chosenAddressId = this.addressList[i].id
}
}*/
loading.close()
}, 500)
}
/*back() {
if (this.projectId) {
this.$pjaxReplace("/platform/h5/benefit/userChoose/index?id=" + this.projectId)
} else {
/!*this.$pjaxReplace("/platform/h5/home")*!/
window.history.go(-1)
}
}*/,
onEdit(row) {
this.title = "编辑地址"
const data = JSON.parse(JSON.stringify(row))
this.showDelete = !!data.id
this.addressInfo = data
this.editActionSheet = true
},
onAdd() {
this.title = "新增地址"
this.addressInfo = {}
this.editActionSheet = true
},
async onSave(row) {
const loading = this.$toast.loading({
message: "加载中...",
forbidClick: true,
overlay: true,
duration: 0
})
row.userName = row.name
const resp = await this.$axios.post("/platform/benefit/address/doSaveUserAddress", { data: JSON.stringify(row) })
if (resp.code === 0) {
this.$toast.success(resp.msg)
await this.selectUserAddress()
loading.close()
this.editActionSheet = false
} else {
this.$toast.fail(resp.msg)
}
},
async onDelete(row) {
const loading = this.$toast.loading({
message: "加载中...",
forbidClick: true,
overlay: true,
duration: 0
})
const resp = await this.$axios.post("/platform/benefit/address/deleteAddress", { id: row.id })
if (resp.code === 0) {
this.$toast.success(resp.msg)
await this.selectUserAddress()
loading.close()
this.editActionSheet = false
} else {
this.$toast.fail(resp.msg)
}
},
onClick(row, index) {
this.$emit("address_click", row)
}
},
created() {
this.projectId = GetQueryString("projectId")
this.selectUserAddress()
}
}
<!--#include("/platform/zhghh5/benefit/include/areaList.js"){}#-->
@@ -0,0 +1,31 @@
<!--#
layout("/layouts/platform_h5.html"){
#-->
<div id="app" v-cloak>
<van-sticky>
<van-nav-bar @click-left="back" left-arrow left-text="返回" placeholder title="地址管理"></van-nav-bar>
</van-sticky>
<benefit-address ref="address"></benefit-address>
</div>
<script nonce="${cspNonce!}">
<!--#include("address.js"){}#-->
new Vue({
el: "#app",
components: {
"benefit-address": ADDRESS
},
methods: {
back() {
if (this.projectId) {
this.$pjaxReplace("/platform/h5/benefit/userChoose/index?id=" + this.projectId)
} else {
/*this.$pjaxReplace("/platform/h5/home")*/
window.history.go(-1)
}
}
}
})
</script>
<!--#
}
#-->
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,153 @@
<!--#
layout("/layouts/platform_h5.html"){
#-->
<div id="app" v-cloak>
<van-sticky>
<van-nav-bar @click-left="back" left-arrow left-text="返回" placeholder title="结算"></van-nav-bar>
</van-sticky>
<van-cell
is-link
@click="toAddress"
v-for="(address,index) in 1"
size="large"
:value="addressList[index].name+addressList[index].tel"
:label="addressList[index].address"
>
<!-- 使用 title 插槽来自定义标题 -->
<template #title>
<span class="custom-title">{{addressList[index].title}}</span>
</template>
</van-cell>
<van-swipe-cell v-for="(goods,index) in cartGoodsList">
<van-card
:price="goods.benefitGoodsSpecifications.points"
:desc="goods.benefitGoods.simpleDesc"
:title="goods.benefitGoodsSpecifications.name"
:thumb="goods.benefitGoodsSpecifications.imgUrl[0].url"
></van-card>
<template #right>
<van-button square text="删除" type="danger" @click="delGoods(index)" class="delete-button" />
</template>
</van-swipe-cell>
<van-submit-bar :price="checkedGoodsAmount*100" button-text="提交订单" @submit="onSubmit"></van-submit-bar>
</div>
<script nonce="${cspNonce!}">
new Vue({
el: "#app",
store,
data() {
return {
cartGoodsList: [],
addressList: [],
checkedGoodsAmount: 0,
goodsId: ""
}
},
created() {
this.goodsId = window.location.search.substring(4)
if (this.goodsId != "") {
this.initGoods()
} else {
this.getCartGoods()
}
this.getAddress()
},
methods: {
back() {
this.$pjaxReplace("/platform/h5/benefit/shop")
},
toAddress() {
this.$pjaxReplace("/platform/h5/benefit/address")
},
getCartGoods() {
this.$axios.post("/platform/benefit/shop/getCartList").then((resp) => {
if (resp.code === 0) {
const goods = []
for (let i = 0; i < resp.data.length; i++) {
if (resp.data[i].isChecked == true) {
goods.push(resp.data[i])
this.checkedGoodsAmount += resp.data[i].benefitGoodsSpecifications.points
}
}
this.cartGoodsList = goods
}
})
},
initGoods() {
this.$axios.post("/platform/benefit/shop/findOneBySpecId", { id: this.goodsId }).then((res) => {
if (res.code === 0) {
res.data.imgUrl = JSON.parse(res.data.imgUrl)
res.data = {
benefitGoods: {
id: res.data.goodsId,
name: res.data.goodsName,
simpleDesc: res.data.simpleDesc
},
benefitGoodsSpecifications: res.data,
id: ""
}
const goods = []
goods.push(res.data)
this.checkedGoodsAmount = res.data.benefitGoodsSpecifications.points
this.cartGoodsList = goods
}
})
},
async getAddress() {
const resp = await this.$axios.post("/platform/benefit/address/selectUserAddress", { userId: this.$store.state.user.id })
resp.data.map((r) => {
r.name = r.userName
r.default = r.isDefault
r.address = r.addressDetail
r.title = r.province + r.city + r.county
})
this.addressList = resp.data
},
onSubmit() {
//获取所有的购物车id
const cartId = []
for (let i = 0; i < this.cartGoodsList.length; i++) {
cartId.push(this.cartGoodsList[i].id)
}
//获取所有的规格
const goodsSpecifications = []
for (let i = 0; i < this.cartGoodsList.length; i++) {
goodsSpecifications.push(this.cartGoodsList[i].benefitGoodsSpecifications)
}
const benefitBuyGoodsVo = {
list: JSON.stringify(goodsSpecifications),
addressId: this.addressList[0].id,
cartId: JSON.stringify(cartId)
}
this.$axios.post("/platform/benefit/shop/buyGoodsList", benefitBuyGoodsVo).then((res) => {
if (res.code === 0) {
this.$toast.success("购买成功!")
this.$pjaxReplace("/platform/h5/benefit/shop/order")
}
})
},
delGoods(index) {
this.cartGoodsList.slice(index)
}
}
})
</script>
<style lang="less" scoped>
.delete-button {
color: #fff;
height: 100%;
background-color: #ee0a24;
border: 1px solid #ee0a24;
}
</style>
<!--#
}
#-->
@@ -0,0 +1,193 @@
<!--#
layout("/layouts/platform_h5.html"){
#-->
<div id="app" v-cloak>
<van-sticky>
<van-nav-bar @click-left="back" left-arrow left-text="返回" placeholder title="购物车"></van-nav-bar>
</van-sticky>
<div class="cart-box">
<div v-for="item in cartList" :key="item.id" class="cart-item">
<!-- 每个商品前的按钮 -->
<van-checkbox :name="item.id" @click="onEdit(item)" class="checkbox-btn" v-model="item.isChecked"></van-checkbox>
<!-- 商品信息 -->
<van-card :price="item.benefitGoodsSpecifications.points" :thumb="item.benefitGoodsSpecifications.imgUrl[0].url">
<!-- 自定义标题,删除按钮 -->
<template #title>
<span>{{ item.benefitGoods.name }}</span>
<van-icon name="delete-o" class="delete-icon" @click="onDelete(item)" />
</template>
<!-- 自定义备注 -->
<template #desc>
<div style="color: #a2a2a2">{{ item.benefitGoods.simpleDesc }}</div>
</template>
</van-card>
</div>
<!-- 按钮 -->
<!-- 下方结算 -->
<!-- vant显示的数字不对,9999元会显示成99.99元,所以需要乘以100 -->
<van-submit-bar :price="checkedGoodsAmount*100" button-text="提交订单" @submit="onSubmit">
<van-checkbox @click="onClickCheckAll" v-model="checkedAll">全选</van-checkbox>
</van-submit-bar>
</div>
</div>
<script nonce="${cspNonce!}">
new Vue({
el: "#app",
data() {
return {
cartList: [], //商品总列表
cartTotal: {}, //购物车数据
// price: 0,
goodsId: "",
number: "",
productId: "",
id_: "",
isChecked: "1",
// productIdsList:[],
checkedGoodsAmount: 0, //选中的商品的总金额
checkedAll: 1
}
},
created() {
this.getCart()
},
methods: {
back() {
this.$pjaxReplace("/platform/h5/benefit/shop")
},
// 获取数据
getCart() {
this.$axios.post("/platform/benefit/shop/getCartList").then((resp) => {
if (resp.code === 0) {
this.cartList = resp.data
for (let i = 0; i < this.cartList.length; i++) {
const item = this.cartList[i]
if (item.isChecked == true) {
this.checkedGoodsAmount += item.benefitGoodsSpecifications.points
} else {
this.checkedAll = 0
}
}
}
})
},
// 删除单个商品的时候,发送删除商品的请求
async onDelete(item) {
const loading = this.$toast.loading({
message: "加载中...",
forbidClick: true,
overlay: true,
duration: 0
})
const resp = await this.$axios.post("/platform/benefit/shop/deleteCart", { id: item.id })
if (resp.code === 0) {
this.$toast.success(resp.msg)
await this.getCart()
loading.close()
} else {
this.$toast.fail(resp.msg)
}
},
// 购物车商品步进器功能接口 按下商品+1或者-1按钮,
updateCartData() {
// 直接发送更新数据请求,将当前的商品数量带着
UpdateCartData({
goodsId: this.goodsId,
id: this.id_,
number: this.number,
productId: this.productId
}).then((res) => {
console.log(999, res)
if (res.errno === 0) {
this.getData() //重新请求购物车商品数据,渲染
}
})
},
// 切换购物车商品选中状态,发送请求
async onEdit(row) {
const loading = this.$toast.loading({
message: "加载中...",
forbidClick: true,
overlay: true,
duration: 0
})
const resp = await this.$axios.post("/platform/benefit/shop/editCart", row)
if (resp.code === 0) {
await this.getCart()
loading.close()
} else {
this.$toast.fail(resp.msg)
}
},
// 点击全选,切换购物车商品选中状态,发送请求
onClickCheckAll() {
let productIdAllList = []
this.cartList.forEach((item) => {
productIdAllList.push(item.id.toString())
})
const loading = this.$toast.loading({
message: "加载中...",
forbidClick: true,
overlay: true,
duration: 0
})
const resp = this.$axios
.post("/platform/benefit/shop/editCartList", {
ids: JSON.stringify(productIdAllList),
isChecked: this.checkedAll
})
.then((res) => {
if (res.code === 0) {
this.getCart()
loading.close()
}
})
},
onSubmit() {
this.$pjaxReplace("/platform/h5/benefit/shop/buy")
}
}
})
</script>
<style lang="less" scoped>
/deep/.van-checkbox__label {
flex: 1;
}
/deep/.van-checkbox {
margin-bottom: 2px;
}
/deep/.van-submit-bar {
bottom: 50px;
}
.cart-box {
padding-bottom: 150px;
box-sizing: border-box;
.van-card {
position: relative;
}
.delete-icon {
position: absolute;
top: 5px;
right: 5px;
}
.cart-item {
position: relative;
padding-left: 40px;
.checkbox-btn {
position: absolute;
left: 20px;
top: 50%;
transform: translate(-50%, -50%);
}
}
}
</style>
<!--#
}
#-->
@@ -0,0 +1,103 @@
<!--#
layout("/layouts/platform_h5.html"){
#-->
<div id="app" v-cloak>
<van-sticky>
<van-nav-bar @click-left="back" left-arrow left-text="返回" placeholder title="分类"></van-nav-bar>
</van-sticky>
<div class="channel-box">
<van-tabs @click="changeFn">
<van-tab v-for="item in goodsCategoryList" :key="item.id" :title="item.name">
<!-- 产品列表 -->
<van-card
v-for="item in goodsList"
:key="item.id"
@click="clickFn(item.id)"
:price="item.money"
:desc="item.simpleDesc"
:title="item.name"
:thumb="item.imgUrl!=null?item.imgUrl[0].url:'https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fcbu01.alicdn.com%2Fhttps%3A%2F%2Fcbu01.alicdn.com%2Fimg%2Fibank%2FO1CN01PfO15Z1d0jv9XS9cD_%21%21980613674-0-cib.jpg&refer=http%3A%2F%2Fcbu01.alicdn.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=auto?sec=1744168614&t=d8ddb01b1cd4b7d289e491913950b215'"
></van-card>
</van-tab>
</van-tabs>
</div>
</div>
<script nonce="${cspNonce!}">
new Vue({
el: "#app",
data() {
return {
id_: "0", // 当前类别的id
goodsCategoryList: [], // 分类数组
goodsList: [], //当前类别对应的商品列表
front_desc: ""
}
},
created() {
this.initGoodsCategory() //获取所有分类数据
this.id_ = this.goodsCategoryList.length > 0 ? this.goodsCategoryList[0].id : ""
this.getCategoryListData()
},
methods: {
//获取商品类型
initGoodsCategory() {
this.$axios.post("/platform/benefit/shop/getCategoryTree").then((res) => {
if (res.code === 0) {
this.goodsCategoryList = res.data
this.goodsCategoryList.unshift({
id: "",
name: "全部",
parentId: "",
weight: 0
})
}
})
},
//获取当前类别对应的产品数组
getCategoryListData() {
this.$axios.post("/platform/benefit/goods/getGoodsListByType", { id: this.id_ }).then((res) => {
if (res.code === 0) {
this.goodsList = res.data
}
})
},
// / 切换分类
changeFn(title, name) {
console.log(this.goodsCategoryList)
// title 下标
// name: 分类标题
this.goodsList = []
this.id_ = ""
this.goodsCategoryList.forEach((item) => {
if (item.name === name) {
this.id_ = item.id
}
})
this.getCategoryListData()
},
back() {
this.$pjaxReplace("/platform/h5/benefit/shop")
},
clickFn(id) {
this.$pjaxReplace("/platform/h5/benefit/shop/detail?id=" + id)
}
}
})
</script>
<style lang="less" scoped>
.channel-box {
font-size: 16px;
line-height: 40px;
p {
color: #666;
}
}
</style>
<!--#
}
#-->
@@ -0,0 +1,249 @@
<!--#
layout("/layouts/platform_h5.html"){
#-->
<div id="app" v-cloak>
<van-sticky>
<van-nav-bar @click-left="back" left-arrow left-text="返回" placeholder title="详情"></van-nav-bar>
</van-sticky>
<div class="product-detail-box">
<van-swipe :autoplay="3000" v-if="formData.imgUrl!=null">
<van-swipe-item v-for="item in formData.imgUrl" :key="item.uid">
<img :src="item.url" />
</van-swipe-item>
</van-swipe>
<div v-else class="swipe-img">
<img
style="height: 390px"
src="https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fcbu01.alicdn.com%2Fhttps%3A%2F%2Fcbu01.alicdn.com%2Fimg%2Fibank%2FO1CN01PfO15Z1d0jv9XS9cD_%21%21980613674-0-cib.jpg&refer=http%3A%2F%2Fcbu01.alicdn.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=auto?sec=1744168614&t=d8ddb01b1cd4b7d289e491913950b215"
alt=""
/>
</div>
<div class="info">
<p class="info-name">{{ formData.name }}</p>
<p class="info-brief">{{ formData.simpleDesc }}</p>
<p class="info-price">¥{{ formData.money }}</p>
</div>
<div class="attribute">
<div class="mytitle">
<span></span>
<h3>商品参数</h3>
</div>
<ul v-if="formData.benefitGoodsSpecificationsList.length>0">
<li v-for="(item,index) in formData.benefitGoodsSpecificationsList" :key="item.id">
<span class="attribute-name"></span>
<span class="attribute-value">规格{{chineseNum(index+1)}}:{{ item.name }}</span>
</li>
</ul>
<ul v-else>
<li>
<span class="attribute-name"></span>
<span class="attribute-value">该暂无规格</span>
</li>
</ul>
</div>
<!-- 产品详情 -->
<div class="mytitle">
<span></span>
<h3>产品详情</h3>
</div>
<!-- 产品描述信息 -->
<div class="goods_desc" v-html="formData.description"></div>
<!-- 下方购物车 -->
<van-goods-action>
<van-goods-action-icon icon="cart-o" text="购物车" :badge="badge" @click="goCart"></van-goods-action-icon>
<van-goods-action-button type="warning" text="加入购物车" @click="addCar"></van-goods-action-button>
<van-goods-action-button type="danger" text="立即购买" @click="addCar"></van-goods-action-button>
</van-goods-action>
<!-- 添加购物车面板 -->
<van-sku v-model="show" ref="sku" :sku="sku" :goods="goods" @add-cart="onAddCartClicked" @buy-clicked="toBuy"></van-sku>
</div>
</div>
<script nonce="${cspNonce!}">
new Vue({
el: "#app",
store,
data() {
return {
gallery: [],
info: {},
attribute: [], //参数
show: false,
sku: {},
goods: {
// 默认商品 sku 缩略图
picture: ""
},
productList: [], // 当前产品信息
badge: 0,
id: "",
formData: {}
}
},
created() {
this.id = window.location.search.substring(4)
this.initGoods()
this.getCartData()
},
methods: {
chineseNum(num) {
const numMap = ["零", "一", "二", "三", "四", "五", "六", "七", "八", "九"]
return numMap[num]
},
back() {
this.$pjaxReplace("/platform/h5/benefit/shop")
},
goCart() {
this.$pjaxReplace("/platform/h5/benefit/shop/cart")
},
addCar() {
const spec = []
const list = []
for (let i = 0; i < this.formData.benefitGoodsSpecificationsList.length; i++) {
item = this.formData.benefitGoodsSpecificationsList[i]
spec.push({
id: item.id, // skuValueId:规格值 id
name: item.name, // skuValueName:规格值名称
imgUrl: item.imgUrl[0].url, // 规格类目图片,只有第一个规格类目可以定义图片
previewImgUrl: item.imgUrl[0].url // 用于预览显示的规格类目图片
})
list.push({
id: 2259 + i, // skuId
s1: item.id, // 规格类目 k_s 为 s1 的对应规格值 id
s2: item.id, // 规格类目 k_s 为 s1 的对应规格值 id
price: item.points * 100, // 价格(单位分)
stock_num: 110 // 当前 sku 组合对应的库存
})
}
this.sku = {
// 所有sku规格类目与其值的从属关系,比如商品有颜色和尺码两大类规格,颜色下面又有红色和蓝色两个规格值。
// 可以理解为一个商品可以有多个规格类目,一个规格类目下可以有多个规格值。
tree: [
{
k: "规格", // skuKeyName:规格类目名称
k_s: "s1", // skuKeyStr:sku 组合列表(下方 list)中当前类目对应的 key 值,value 值会是从属于当前类目的一个规格值 id
v: spec,
largeImageMode: true // 是否展示大图模式
}
],
list: list,
price: this.formData.money, // 默认价格(单位元)
stock_num: 999, // 商品总库存
none_sku: this.formData.benefitGoodsSpecificationsList.length == 0 ? true : false, // 是否无规格商品
hide_stock: true // 是否隐藏剩余库存
}
this.goods = {
// 默认商品 sku 缩略图
picture: this.formData.imgUrl[0].url
}
this.show = true
},
// 加入购物车
onAddCartClicked() {
const cart = {
goodsId: this.formData.id,
specificationsId: this.$refs.sku.getSkuData().selectedSkuComb.s1
}
this.$axios.post("/platform/benefit/shop/addCart", cart).then((res) => {
if (res.code === 0) {
this.$toast.success("添加成功")
// 隐藏 商品规格面板
this.show = false
}
})
},
// 获取购物车商品数量
getCartData() {
this.$axios.post("/platform/benefit/shop/getCartList").then((resp) => {
if (resp.code === 0) {
this.badge = resp.data.length
}
})
},
initGoods() {
this.$axios.post("/platform/benefit/shop/findOne", { id: this.id }).then((res) => {
if (res.code === 0) {
this.formData = res.data
this.formData.imgUrl = JSON.parse(this.formData.imgUrl)
}
})
},
toBuy() {
this.$pjaxReplace("/platform/h5/benefit/shop/buy?id=" + this.$refs.sku.getSkuData().selectedSkuComb.s1)
}
}
})
</script>
<style lang="less" scoped>
.product-detail-box {
font-size: 14px;
line-height: 30px;
img {
width: 100%;
}
.info {
text-align: center;
.info-brief {
color: #666;
}
.info-price {
color: red;
}
}
.attribute {
ul {
li {
border-bottom: 1px solid #eee;
font-size: 12px;
display: flex;
.attribute-name {
width: 10%;
}
.attribute-value {
flex: 1;
}
}
}
}
.mytitle {
text-align: center;
font-size: 16px;
position: relative;
height: 50px;
span {
width: 50%;
height: 2px;
background-color: #ccc;
display: inline-block;
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
}
h3 {
width: 30%;
background-color: #fff;
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
}
}
/deep/.goods_desc {
img {
width: 100%;
}
}
}
</style>
<!--#
}
#-->
@@ -0,0 +1,196 @@
<!--#
layout("/layouts/platform_h5.html"){
#-->
<div id="app" v-cloak>
<template>
<van-sticky>
<van-nav-bar @click-left="back" left-arrow left-text="返回" placeholder title="福利商城"></van-nav-bar>
</van-sticky>
<div class="home">
<!-- 轮播图 -->
<div class="swiper-com">
<van-swipe class="my-swipe" :autoplay="3000" indicator-color="white">
<van-swipe-item v-for="item in banner" :key="item.id">
<img v-if="item.imgUrl.length>0" @click="clickFn(item.id)" :src="item.imgUrl[0].url" alt="" />
<img
v-else
src="https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fcbu01.alicdn.com%2Fhttps%3A%2F%2Fcbu01.alicdn.com%2Fimg%2Fibank%2FO1CN01PfO15Z1d0jv9XS9cD_%21%21980613674-0-cib.jpg&refer=http%3A%2F%2Fcbu01.alicdn.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=auto?sec=1744168614&t=d8ddb01b1cd4b7d289e491913950b215"
alt=""
/>
</van-swipe-item>
</van-swipe>
</div>
<div class="gird">
<van-grid :column-num="channel.length">
<van-grid-item v-for="item in channel" :key="item.id" :icon="item.icon_url" :text="item.name" @click="btn(item.id)" />
</van-grid>
</div>
<!-- 分类商品 -->
<div v-if="item.goodsList.length>0" class="week-product" v-for="item in categoryList" :key="item.id">
<div class="mytitle">
<span></span>
<h3>{{ item.name }}</h3>
</div>
<ul>
<li v-for="good in item.goodsList" :key="good.id" @click="clickFn(good.id)">
<img style="height: 180px" :src="good.imgUrl[0].url" alt="" />
<p>{{ good.name }}</p>
<p>¥{{ good.money }}</p>
</li>
</ul>
</div>
<div class="week-product">
<div class="mytitle">
<span></span>
<h3>福利活动</h3>
</div>
<div v-if="projectList.length > 0" class="table-list">
<div @click="clickFl" :key="row.id" class="table-card" v-for="row in projectList">
<van-image :src="row.cover" fit="cover" height="200" width="100%"></van-image>
<div style="margin: 10px">
<div style="font-size: 15px">开始时间:{{$moment(row.startChoiceTime).format('YYYY-MM-DD HH:mm')}}</div>
<div style="font-size: 15px">结束时间:{{$moment(row.endChoiceTime).format('YYYY-MM-DD HH:mm')}}</div>
</div>
</div>
</div>
<div v-else class="table-list">
<van-empty class="custom-image" image="https://img01.yzcdn.cn/vant/custom-empty-image.png" description="暂无福利活动"></van-empty>
</div>
</div>
</div>
</template>
</div>
<script nonce="${cspNonce!}">
new Vue({
el: "#app",
data() {
return {
value: "",
banner: [], //轮播图
channel: [
{ id: "category", name: "分类", icon_url: "apps-o" },
{ id: "cart", name: "购物车", icon_url: "shopping-cart-o" },
{ id: "order", name: "我的订单", icon_url: "orders-o" }
], //居家-志趣数据
projectList: [],
hotGoodsList: [],
topicList: [],
categoryList: []
}
},
methods: {
//获取轮播图
initGoodsImg() {
this.$axios.post("/platform/benefit/shop/getGoodsList").then((res) => {
if (res.code === 0) {
this.banner = res.data
}
})
},
btn(id) {
this.$pjaxReplace("/platform/h5/benefit/shop/" + id)
},
back() {
this.$pjaxReplace("/platform/h5/home")
},
clickFn(id) {
this.$pjaxReplace("/platform/h5/benefit/shop/detail?id=" + id)
},
//获取福利商品
initGoods() {
this.$axios.post("/platform/benefit/shop/getGoodsListByType").then((res) => {
if (res.code === 0) {
this.categoryList = res.data
}
})
},
//获取福利项目
initProject() {
this.$axios.post("/platform/benefit/shop/getProjectGoodsList").then((res) => {
if (res.code === 0) {
if (res.data.length > 0) {
this.projectList = [res.data[0]]
}
}
})
},
clickFl() {
this.$pjaxReplace("/platform/h5/benefit/userSelection")
}
},
created() {
this.initGoodsImg()
this.initProject()
this.initGoods()
}
})
</script>
<style>
.home {
padding-bottom: 100px;
box-sizing: border-box;
}
.swiper-com {
width: 100%;
img {
width: 100%;
}
}
.week-product {
.mytitle {
text-align: center;
font-size: 16px;
background: #ffffff;
position: relative;
height: 50px;
span {
width: 50%;
height: 2px;
background-color: #ccc;
display: inline-block;
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
}
h3 {
width: 30%;
background-color: #fff;
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
}
}
ul {
display: flex;
justify-content: space-between;
background-color: #ffffff;
flex-wrap: wrap;
li {
width: 49%;
img {
width: 100%;
}
p {
text-align: center;
font-size: 16px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}
}
}
.top-box {
h3 {
font-size: 22px;
line-height: 60px;
text-align: center;
}
}
</style>
<!--#
}
#-->
@@ -0,0 +1,125 @@
<!--#
layout("/layouts/platform_h5.html"){
#-->
<div id="app" v-cloak>
<van-nav-bar @click-left="back" left-arrow left-text="返回" placeholder title="我的订单"></van-nav-bar>
<van-sticky offset-top="46px">
<van-search
v-model="pageForm.goodsName"
placeholder="请输入商品名称进行查询"
:show-action="false"
:reverse-color="false"
input-align="left"
@search="doSearch"
></van-search>
</van-sticky>
<van-list v-model="tableLoading" :finished="tableFinished" finished-text="没有更多了" @load="pageData">
<van-swipe-cell v-for="(order,index) in tableData">
<van-card
@click="detail(order)"
:price="order.points"
:desc="order.specName"
:title="order.goodsName"
:thumb="order.imgUrl[0].url"
></van-card>
<template #right>
<van-button square text="删除" @click="deleteOrder(order.id)" type="danger" class="delete-button" />
</template>
</van-swipe-cell>
</van-list>
<van-action-sheet v-model="show" title="详情">
<div class="content">
<van-cell-group>
<van-cell title="商品名称" :value="formData.goodsName"></van-cell>
<van-cell title="商品规格" :value="formData.specName"></van-cell>
<van-cell title="下单时间" :value="formData.orderTime"></van-cell>
<van-cell title="收货信息" :value="formData.userAddress"></van-cell>
<van-cell title="收货地址" :value="formData.receiveAddress"></van-cell>
<van-cell title="商品详情" :label="formData.simpleDesc"></van-cell>
</van-cell-group>
</div>
</van-action-sheet>
</div>
<script nonce="${cspNonce!}">
new Vue({
el: "#app",
data() {
return {
list: [],
tableLoading: false,
tableFinished: false,
pageForm: {
pageNumber: 1,
pageSize: 10,
totalCount: 0,
goodsName: ""
},
tableData: [],
show: false,
formData: {}
}
},
created() {
this.pageData()
},
methods: {
back() {
this.$pjaxReplace("/platform/h5/benefit/shop")
},
doSearch() {
this.pageForm.pageNumber = 1
this.tableData = []
this.tableFinished = false
this.pageData()
},
pageData() {
this.tableLoading = true
this.$axios.post("/platform/benefit/order/pageData", this.pageForm).then((resp) => {
this.tableData = this.tableData.concat(resp.data.list)
for (let i = 0; i < this.tableData.length; i++) {
this.tableData[i].imgUrl = JSON.parse(this.tableData[i].imgUrl)
}
this.pageForm.totalCount = resp.data.totalCount
if (this.tableData.length >= this.pageForm.totalCount) {
this.tableFinished = true
} else {
this.pageForm.pageNumber++
}
this.tableLoading = false
})
},
async deleteOrder(id) {
const loading = this.$toast.loading({
message: "加载中...",
forbidClick: true,
overlay: true,
duration: 0
})
const resp = await this.$axios.post("/platform/benefit/address/deleteAddress", { id: id })
if (resp.code === 0) {
this.$toast.success(resp.msg)
await this.doSearch()
loading.close()
} else {
this.$toast.fail(resp.msg)
}
},
detail(order) {
this.formData = order
this.show = true
}
}
})
</script>
<style lang="less" scoped>
.delete-button {
color: #fff;
height: 100%;
background-color: #ee0a24;
border: 1px solid #ee0a24;
}
</style>
<!--#
}
#-->
@@ -0,0 +1,557 @@
<!--#
layout("/layouts/platform_h5.html"){
#-->
<style>
.table-list .collapse .van-cell__left-icon {
color: red;
}
.table-list .table-card .goodsPm {
font-size: 13px;
background-color: rgb(253, 247, 229);
color: rgb(202, 156, 108);
display: flex;
justify-content: center;
align-items: center;
height: 30px;
}
.table-list .table-card .goodsYxz {
font-size: 13px;
display: flex;
justify-content: center;
align-items: center;
height: 30px;
}
</style>
<div id="app" v-cloak>
<van-sticky>
<van-nav-bar
@click-left="()=>this.$pjaxReplace('/platform/h5/benefit/userSelection')"
left-arrow
left-text="返回"
placeholder
title="商品列表"
></van-nav-bar>
</van-sticky>
<div>
<div class="table-list">
<div class="table-card">
<van-collapse class="collapse" v-model="benefitViews">
<van-collapse-item icon="star" name="1" title="福利信息">
<div>
<van-cell-group>
<van-cell :value="viewData.name" title="福利名称"></van-cell>
<van-cell title="是否电子签字">
<dict-tag :options="dict.type.BENEFIT_SIGN_MODE"
:value="viewData.signMode"></dict-tag>
</van-cell>
<van-cell title="发放形式">
<dict-tag :options="dict.type.BENEFIT_PROVIDE_MODE"
:value="viewData.provideMode"></dict-tag>
</van-cell>
<van-cell title="选择开始时间" v-if="viewData.provideMode==='BENEFIT_PROVIDE_MODE_TWO'">
{{$moment(viewData.startChoiceTime).format("YYYY-MM-DD HH:mm")}}
</van-cell>
<van-cell title="选择结束时间" v-if="viewData.provideMode==='BENEFIT_PROVIDE_MODE_TWO'">
{{$moment(viewData.endChoiceTime).format("YYYY-MM-DD HH:mm")}}
</van-cell>
<van-cell title="逾期选择时间" v-if="viewData.provideMode==='BENEFIT_PROVIDE_MODE_TWO'">
<span v-if="viewData.endOverdueChoiceTime">
{{ $moment(viewData.endOverdueChoiceTime).format("YYYY-MM-DD HH:mm") }}
</span>
<span v-else>暂无</span>
</van-cell>
<van-cell title="福利总积分" v-if="viewData.provideMode==='BENEFIT_PROVIDE_MODE_TWO'">
{{viewData.totalMoney}}
</van-cell>
</van-cell-group>
</div>
</van-collapse-item>
</van-collapse>
</div>
<!-- 如果是统一发放并且需要签字-->
<div v-if="viewData.signMode==='BENEFIT_SIGN_MODE_ONE'&&viewData.provideMode === 'BENEFIT_PROVIDE_MODE_ONE'">
<van-cell>
<van-icon class="van-cell__left-icon" color="red" name="info" slot="icon"></van-icon>
<div slot="title" style="font-weight: bold; font-size: 15px">电子签名</div>
<template #label>
<div>
<h5-signature v-model="formData.userSign"></h5-signature>
</div>
</template>
</van-cell>
<div style="padding: 10px 16px; left: 0; right: 0; bottom: 0; background: #ffffff">
<van-button @click="submit" round style="width: 100%" type="primary">提交</van-button>
</div>
</div>
<!-- 如果是意向选择-->
<div v-if="viewData.provideMode==='BENEFIT_PROVIDE_MODE_TWO'">
<div class="table-card" v-for="(item,index) in viewData.benefitGoodsList">
<div>
<van-row gutter="5">
<van-col span="2">
<van-image height="30" src="/assets/mobile/img/benefit/1.png" width="30"></van-image>
</van-col>
<van-col span="14">
<div class="goodsPm">此套餐在本次福利排行第{{index+1}}名</div>
</van-col>
<van-col span="8">
<div class="goodsYxz">已选择{{item.choiceNum}}件</div>
</van-col>
</van-row>
</div>
<van-card :price="item.money.toFixed(2)" tag="">
<template #title>
<div
@click="openViewDesc(item.description)"
style="font-weight: bold; font-size: 15px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis"
>
{{item.name}}
</div>
</template>
<template #desc>
<div @click="openViewDesc(item.description)" style="margin: 5px">{{item.simpleDesc}}</div>
</template>
<template #thumb>
<van-image :src="item.imgUrl[0].url" @click="openViewDesc(item.description)" height="80"
width="80" v-if="item.imgUrl&&item.imgUrl.length>0"></van-image>
<van-image
src="/assets/mobile/img/benefit/2.png"
@click="openViewDesc(item.description)" height="80"
width="80" v-else></van-image>
</template>
<template #num>
<van-stepper
:default-value="0"
:min="0"
async-change
button-size="20"
disable-input
integer
theme="round"
v-model="item.selectNum"
></van-stepper>
</template>
</van-card>
</div>
<div style="padding-top: 100px">
<van-submit-bar
:button-text="confirmButtonInfo.text"
:price="sumSelectMoney"
@submit="openSubmit"
class="submitBar"
tip="您可以在福利选择截至时间之前修改福利套餐"
tip-icon="info-o"
></van-submit-bar>
</div>
</div>
</div>
</div>
<van-popup :style="{ height: '100%',width: '100%'}" position="right" safe-area-inset-bottom v-model="descPopup">
<van-sticky>
<van-nav-bar @click-left="descPopup=false" fixed left-arrow left-text="返回" placeholder
title="详情"></van-nav-bar>
</van-sticky>
<div style="margin-top: 45px">
<div @click="descImgClick" class="desc" v-html="description"></div>
</div>
</van-popup>
<van-action-sheet :close-on-click-overlay="false" title="确认福利套餐" v-model="confirmPopup">
<div>
<!--选择的信息-->
<van-cell>
<van-icon class="van-cell__left-icon" color="red" name="info" slot="icon"></van-icon>
<div slot="title" style="font-weight: bold; font-size: 15px">已选福利</div>
</van-cell>
<van-cell style="padding: 0" v-for="(item,index) in formData.benefitGoodsList">
<van-card
:key="item.id"
:num="item.selectNum"
:thumb="(item.imgUrl&&item.imgUrl.length>0)?item.imgUrl[0].url:'/assets/mobile/img/benefit/2.png'"
:title="item.name"
center
desc=""
style="border-radius: 8px; background: #ffffff"
></van-card>
<!-- 有规格时出现-->
<van-radio-group v-model="item.specificationsId">
<van-cell-group>
<van-cell
:key="index"
:title="specifications.name"
@click="specificationsClick(specifications,index)"
clickable
v-for="(specifications,index) in item.benefitGoodsSpecificationsList"
>
<template #right-icon>
<van-radio :name="specifications.id" :ref="'radio'+specifications.id"></van-radio>
</template>
</van-cell>
</van-cell-group>
</van-radio-group>
<!--邮寄到家 填写收货地址-->
<div v-if="item.goodsProvideMode==='GOODS_PROVIDE_MODE_POST'">
<van-cell @click="goodsAddressClick(item,index)" class="confirmWelfareCell" is-link>
<van-icon class="van-cell__left-icon" color="red" name="info" slot="icon"></van-icon>
<div slot="title" style="font-weight: bold; font-size: 15px">收货地址</div>
<template #label>
<span v-if="!item.receiveAddress">请选择地址</span>
<span v-else>{{item.receiveAddress}}</span>
</template>
</van-cell>
</div>
<!--收货门店-->
<div v-if="item.goodsProvideMode==='GOODS_PROVIDE_MODE_SHOP'">
<van-cell @click="goodsShopAddressClick(item,index)" class="confirmWelfareCell" is-link>
<van-icon class="van-cell__left-icon" color="red" name="info" slot="icon"></van-icon>
<div slot="title" style="font-weight: bold; font-size: 15px">收货门店</div>
<template #label>
<span v-if="!item.shopAddress">请选择收货门店</span>
<span v-else>{{item.shopAddress}}</span>
</template>
</van-cell>
</div>
</van-cell>
<!--电子签字-->
<van-cell v-if="viewData.signMode==='BENEFIT_SIGN_MODE_ONE'">
<van-icon class="van-cell__left-icon" color="red" name="info" slot="icon"></van-icon>
<div slot="title" style="font-weight: bold; font-size: 15px">电子签名</div>
<template #label>
<div>
<h5-signature v-model="formData.userSign"></h5-signature>
</div>
</template>
</van-cell>
<div style="padding: 10px 16px; left: 0; right: 0; bottom: 0; background: #ffffff">
<van-button @click="submit" round style="width: 100%" type="primary">提交</van-button>
</div>
</div>
</van-action-sheet>
<van-action-sheet :close-on-click-overlay="false" title="确认收货门店" v-model="shopAddressPopup">
<div style="position: relative; height: 500px; overflow-y: auto">
<van-cell :key="index" @click="shopAddressClick(item)" clickable icon="location-o"
v-for="(item,index) in goodsData.shopAddressList">
<template #title>{{item.name}}</template>
</van-cell>
</div>
</van-action-sheet>
<van-action-sheet :close-on-click-overlay="false" title="地址管理" v-model="editAddressPopup">
<benefit-address @address_click="addressClick" ref="address"></benefit-address>
</van-action-sheet>
</div>
<script nonce="${cspNonce!}">
<!--#include("../address/address.js"){}#-->
const vue = new Vue({
el: "#app",
store,
dicts: ["BENEFIT_FESTIVAL", "BENEFIT_SIGN_MODE", "BENEFIT_PROVIDE_MODE", "BENEFIT_GOODS_PROVIDE_MODE"],
data() {
return {
benefitId: GetQueryString("benefitId"),
benefitViews: [],
viewData: {
benefitGoodsList: []
},
description: "",
descPopup: false,
confirmPopup: false,
isChoice: false,
selectTime: false,
formData: {
benefitGoodsList: []
},
selectAddressPopup: false,
addressOptions: [],
goodsData: {},
goodsDataIndex: 0,
shopAddressPopup: false,
editAddressPopup: false,
userSelection: []
}
},
components: {
"benefit-address": ADDRESS
},
computed: {
sumSelectMoney() {
if (this.viewData && this.viewData.benefitGoodsList) {
let totalMoney = 0
const benefitGoodsList = this.viewData.benefitGoodsList.filter((b) => b.selectNum > 0)
benefitGoodsList.map((b) => {
totalMoney = totalMoney + b.selectNum * b.money
})
return totalMoney * 100
}
return 0
},
confirmButtonInfo() {
if (this.viewData) {
const {startChoiceTime, endChoiceTime, endOverdueChoiceTime} = this.viewData
const now = moment().valueOf()
if (endOverdueChoiceTime == null || endOverdueChoiceTime === "") {
if (now > moment(startChoiceTime).valueOf() && now < moment(endChoiceTime).valueOf()) {
return {
text: "提交商品",
disabled: false
}
} else if (now > moment(endChoiceTime).valueOf()) {
return {text: "选择已结束", disabled: true}
} else if (now < moment(startChoiceTime).valueOf()) {
return {text: "选择未开始", disabled: true}
}
} else {
//已选择了不能再动了
if (this.isChoice) {
if (moment(this.selectTime).valueOf() > moment(endChoiceTime).valueOf() && now < moment(endOverdueChoiceTime).valueOf()) {
return {text: "提交商品", disabled: false}
}
return {text: "选择已结束", disabled: true}
} else {
if (now > moment(startChoiceTime).valueOf() && now < moment(endOverdueChoiceTime).valueOf()) {
return {
text: "提交商品",
disabled: false
}
} else if (now > moment(endOverdueChoiceTime).valueOf()) {
return {text: "选择已结束", disabled: true}
} else if (now < moment(startChoiceTime).valueOf()) {
return {text: "选择未开始", disabled: true}
}
}
}
}
return {text: "", disabled: true}
}
},
methods: {
/**
*如果福利项目发放形式是统一发放true
* @returns {boolean}
*/
getBenefitProvideMode() {
console.log(this.viewData.provideMode === "BENEFIT_PROVIDE_MODE_ONE")
return this.viewData.provideMode === "BENEFIT_PROVIDE_MODE_ONE"
},
/**
* 查询单个福利
* @param id
* @returns {Promise<*>}
*/
async findOneBenefit() {
const res = await this.$axios.post("/platform/benefit/userSelection/findOneBenefit", {id: this.benefitId})
if (res.code === 0) {
this.viewData = res.data
}
},
//查看详情
openViewDesc(description) {
if (description == null || description.length === 0) {
this.$toast("没有更多了")
return
}
this.description = description
this.descPopup = true
},
descImgClick(e) {
if (e.target.tagName === "IMG") {
vant.ImagePreview([e.target.src])
}
},
async openSubmit() {
let totalMoney = 0
const benefitGoodsList = this.viewData.benefitGoodsList.filter((b) => b.selectNum > 0)
benefitGoodsList.map((b) => {
totalMoney = totalMoney + b.selectNum * b.money
})
if (totalMoney === 0) {
this.$toast("请选择后再提交!")
return
}
if (totalMoney !== this.viewData.totalMoney) {
this.$toast("本次福利积分【" + this.viewData.totalMoney + "】请检查商品数量!")
return
}
this.formData.benefitGoodsList = benefitGoodsList
this.formData.benefitGoodsList.forEach((b) => {
if (b.goodsProvideMode === "GOODS_PROVIDE_MODE_POST" && !b.receiveAddress) {
//如果是邮寄商品,并且有默认地址,并且是第一次进来没有选择,则默认地址为收货地址
const defaultAddress = this.addressOptions.find((a) => a.isDefault)
b.receiveAddress = defaultAddress.name
if (b.postProvideConfig.choiceMunicipality || b.postProvideConfig.choiceProvince) {
//如果有地址限制
b.receiveAddress = ""
}
}
})
this.confirmPopup = true
},
submit() {
if (this.viewData.signMode === "BENEFIT_SIGN_MODE_ONE" && !this.formData.userSign) {
this.$toast("请签字后再提交!")
return
}
let flag = false
let msg = ""
this.formData.benefitGoodsList.some((b) => {
if (b.goodsProvideMode === "GOODS_PROVIDE_MODE_POST" && !b.receiveAddress) {
flag = true
msg = "商品中存在未填写的收货地址,请检查!"
return true
}
if (b.benefitGoodsSpecificationsList && b.benefitGoodsSpecificationsList.length > 0 && !b.specificationsId) {
flag = true
msg = "商品中存在未填写的规格,请检查!"
return true
}
if (b.shopAddressList && b.shopAddressList.length > 0 && !b.shopAddress) {
flag = true
msg = "商品中存在未填写的门店地址,请检查!"
return true
}
return false
})
if (flag) {
this.$toast(msg)
return
}
const loading = this.$toast.loading({
message: "提交中...",
forbidClick: true,
overlay: true,
duration: 0
})
const benefitUserSelection = this.formData.benefitGoodsList.map((b) => {
let data = {
specificationsId: b.specificationsId,
receiveAddress: b.receiveAddress,
shopAddress: b.shopAddress,
selectNum: b.selectNum,
benefitId: this.viewData.id,
goodsId: b.id,
selectUserId: b.selectUserId,
userSign: this.formData.userSign
}
return data
})
this.$axios
.post("/platform/benefit/userSelection/submit", {
benefitUserSelection: JSON.stringify(benefitUserSelection),
benefitId: this.viewData.id,
userId: this.$store.state.user.id
})
.then((res) => {
if (res.code === 0) {
setTimeout(() => {
this.selectSelectionBenefit()
this.$toast.success(res.msg)
loading.close()
this.confirmPopup = false
}, 500)
} else {
this.$toast.fail(res.msg)
loading.close()
}
})
},
/**
*查询用户选择的商品
* @param benefitId
* @param userId
* @returns {Promise<void>}
*/
async selectSelectionBenefit() {
const res = await this.$axios.post("/platform/benefit/userSelection/selectSelectionBenefit", {
benefitId: this.benefitId,
userId: this.$store.state.user.id
})
if (res.code === 0) {
this.userSelection = res.data
}
},
addressClick(item) {
const address = this.addressOptions.find((a) => a.id === item.id)
const {province, city, county, name, tel} = address
if (this.goodsData.postProvideConfig.choiceMunicipality && city !== this.goodsData.postProvideConfig.choiceMunicipality) {
this.$toast("当前福利限制只能邮寄" + this.goodsData.postProvideConfig.choiceMunicipality + "!")
return
}
if (this.goodsData.postProvideConfig.choiceProvince && province !== this.goodsData.postProvideConfig.choiceProvince) {
this.$toast("当前福利限制只能邮寄" + this.goodsData.postProvideConfig.choiceProvince + "!")
return
}
if (address) {
this.$set(this.formData.benefitGoodsList[this.goodsDataIndex], "receiveAddress", address.name)
}
this.editAddressPopup = false
},
goodsAddressClick(goods, index) {
this.goodsData = goods
this.goodsDataIndex = index
this.editAddressPopup = true
},
goodsShopAddressClick(goods, index) {
this.goodsData = goods
this.goodsDataIndex = index
this.shopAddressPopup = true
},
shopAddressClick(item) {
if (item) {
this.$set(this.formData.benefitGoodsList[this.goodsDataIndex], "shopAddress", item.name)
}
this.shopAddressPopup = false
},
/**
* 回显用户选择的数据
* @param benefitId
* @param userId
* @returns {Promise<void>}
*/
async echoUserSelection() {
await this.selectSelectionBenefit()
//获取用户当前选择的数量
this.viewData.benefitGoodsList.map((v) => {
const goods = this.userSelection.find((x) => x.goodsId === v.id)
if (goods) {
v.selectNum = goods.selectNum
v.selectUserId = goods.selectUserId
v.receiveAddress = goods.receiveAddress
v.specificationsId = goods.specificationsId
v.shopAddress = goods.shopAddress
this.formData.userSign = goods.userSign
} else {
v.selectNum = 0
}
})
},
specificationsClick(item, index) {
this.$set(this.viewData.benefitGoodsList[index], "specificationsId", item.specificationsId)
this.$refs["radio" + item.id][0].toggle()
this.$forceUpdate()
}
},
async created() {
await this.findOneBenefit()
await this.echoUserSelection()
if (this.viewData.signMode === "BENEFIT_SIGN_MODE_ONE" && this.viewData.provideMode === "BENEFIT_PROVIDE_MODE_ONE") {
this.benefitViews = ["1"]
}
}
})
</script>
<!--#
}
#-->
@@ -0,0 +1,94 @@
<!--#
layout("/layouts/platform_h5.html"){
#-->
<style>
.table-card .van-image__img {
border-radius: 5%;
}
.benefitName {
font-weight: bold;
font-size: 17px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
</style>
<div id="app" v-cloak>
<van-sticky>
<van-nav-bar @click-left="()=>this.$pjaxReplace('/platform/h5/home')" left-arrow left-text="返回" placeholder title="选择福利"></van-nav-bar>
<van-dropdown-menu>
<year-van-dropdown-item :num="5" @change="doSearch" v-model="pageForm.year"></year-van-dropdown-item>
</van-dropdown-menu>
</van-sticky>
<div>
<van-list :finished="finished" @load="pageData" finished-text="没有更多了" v-if="tableData.length>0" v-model="loading">
<div class="table-list">
<div :key="row.id" @click="openChoose(row)" class="table-card" v-for="row in tableData">
<van-image :src="row.cover" fit="cover" height="200" width="100%"></van-image>
<div style="margin: 10px">
<div class="benefitName">{{row.benefitName}}</div>
<div style="font-size: 15px">选择开始时间:{{$moment(row.startChoiceTime).format('YYYY-MM-DD HH:mm')}}</div>
<div style="font-size: 15px">选择结束时间:{{$moment(row.endChoiceTime).format('YYYY-MM-DD HH:mm')}}</div>
<div style="display: flex; justify-content: flex-end">{{row.isChoice?'已选择':'未选择'}}</div>
</div>
</div>
</div>
</van-list>
<van-empty description="暂无数据" image="/assets/platform/plugins/vant-green/images/nodata/nodata.png" v-else></van-empty>
</div>
</div>
<script nonce="${cspNonce!}">
new Vue({
el: "#app",
store,
data() {
return {
tableData: [],
finished: false,
loading: false,
pageForm: {
pageNumber: 1,
pageSize: 5,
totalCount: 0,
year: new Date().getFullYear()
},
yearList: []
}
},
methods: {
doSearch() {
this.tableKey = new Date().getTime()
this.pageForm.pageNumber = 1
this.pageForm.totalCount = 0
this.tableData = []
this.pageData()
},
pageData() {
this.loading = true
this.$axios.post("/platform/benefit/userSelection/pageData", 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
this.loading = false
})
},
async openChoose(row) {
this.$pjaxReplace("/platform/h5/benefit/userSelection/goodsSelection?benefitId=" + row.benefitId)
}
},
created() {
this.pageData()
}
})
</script>
<!--#
}
#-->
@@ -0,0 +1,217 @@
<!--#
layout("/layouts/platform_h5.html"){
#-->
<style>
.table-card .van-image__img {
border-radius: 5%;
}
.benefitName {
font-weight: bold;
font-size: 17px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.collapse .van-cell__left-icon {
color: red;
}
</style>
<div id="app" v-cloak>
<van-sticky>
<van-nav-bar @click-left="()=>this.$pjaxReplace('/platform/h5/home')" left-arrow left-text="返回" placeholder title="选择福利"></van-nav-bar>
<van-dropdown-menu>
<year-van-dropdown-item :num="5" @change="doSearch" v-model="pageForm.year"></year-van-dropdown-item>
</van-dropdown-menu>
</van-sticky>
<div>
<van-list :finished="finished" @load="pageData" finished-text="没有更多了" v-if="tableData.length>0" v-model="loading">
<div class="table-list">
<div :key="row.id" class="table-card" v-for="row in tableData">
<van-image :src="row.cover" fit="cover" height="200" width="100%"></van-image>
<div style="margin: 10px">
<div class="benefitName">{{row.benefitName}}</div>
<div style="font-size: 15px">选择开始时间:{{$moment(row.startChoiceTime).format('YYYY-MM-DD HH:mm')}}</div>
<div style="font-size: 15px">选择结束时间:{{$moment(row.endChoiceTime).format('YYYY-MM-DD HH:mm')}}</div>
<div style="display: flex; justify-content: flex-end">{{row.isChoice?'已选择':'未选择'}}</div>
<div class="van-hairline--bottom"></div>
</div>
<div style="display: flex; justify-content: flex-end; margin-top: 10px">
<van-button @click="openView(row.benefitId)" round size="small" type="primary">查看详情</van-button>
</div>
</div>
</div>
</van-list>
<van-empty description="暂无数据" image="/assets/platform/plugins/vant-green/images/nodata/nodata.png" v-else></van-empty>
</div>
<van-action-sheet title="查看详情" v-model="descPopup">
<van-collapse class="collapse" v-model="benefitViews">
<van-collapse-item icon="star" name="1" title="福利信息">
<div>
<van-cell-group>
<van-cell :value="viewData.name" title="福利名称"></van-cell>
<van-cell title="是否电子签字">
<dict-tag :options="dict.type.BENEFIT_SIGN_MODE" :value="viewData.signMode"></dict-tag>
</van-cell>
<van-cell title="发放形式">
<dict-tag :options="dict.type.BENEFIT_PROVIDE_MODE" :value="viewData.provideMode"></dict-tag>
</van-cell>
<van-cell title="选择开始时间" v-if="viewData.provideMode==='BENEFIT_PROVIDE_MODE_TWO'">
{{$moment(viewData.startChoiceTime).format("YYYY-MM-DD HH:mm")}}
</van-cell>
<van-cell title="选择结束时间" v-if="viewData.provideMode==='BENEFIT_PROVIDE_MODE_TWO'">
{{$moment(viewData.endChoiceTime).format("YYYY-MM-DD HH:mm")}}
</van-cell>
<van-cell title="逾期选择时间" v-if="viewData.provideMode==='BENEFIT_PROVIDE_MODE_TWO'">
<span v-if="viewData.endOverdueChoiceTime">{{ $moment(viewData.endOverdueChoiceTime).format("YYYY-MM-DD HH:mm") }}</span>
<span v-else>暂无</span>
</van-cell>
<van-cell title="福利总积分" v-if="viewData.provideMode==='BENEFIT_PROVIDE_MODE_TWO'">{{viewData.totalMoney}}</van-cell>
<van-cell title="福利名称" v-if="viewData.provideMode==='BENEFIT_PROVIDE_MODE_ONE'">{{viewData.giftName}}</van-cell>
</van-cell-group>
</div>
</van-collapse-item>
</van-collapse>
<div class="van-cell-group--inset">
<div v-if="userSelection&&userSelection.length>0">
<van-cell>
<template #title>
<div style="font-weight: bold; font-size: 15px">已选福利</div>
</template>
</van-cell>
<div :key="item.id" v-for="item in userSelection">
<van-card
:num="item.selectNum"
:thumb="item.goods.imgUrl[0].url"
style="border-radius: 8px; background: #ffffff"
v-if="item.goods"
>
<template #title>
<div style="font-weight: bold; font-size: 15px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis">
{{item.goods.name}}
</div>
</template>
<template #desc>
<div>{{item.goods.simpleDesc}}</div>
</template>
</van-card>
<div style="margin: 5px 20px 5px 20px">
<div v-if="item.specificationsName">所选规格:{{item.specificationsName}}</div>
<div class="van-hairline--bottom"></div>
<div v-if="item.receiveAddress">{{item.receiveAddress}}</div>
<div class="van-hairline--bottom"></div>
<div v-if="item.shopAddress">收货门店:{{item.shopAddress}}</div>
</div>
</div>
</div>
<van-cell>
<template #title>
<div style="font-weight: bold; font-size: 15px">电子签名</div>
</template>
</van-cell>
<van-cell title="">
<template #label>
<van-image :src="userSign" style="width: 100%; height: 150px; border: 1px dashed"></van-image>
</template>
</van-cell>
</div>
</van-action-sheet>
</div>
<script nonce="${cspNonce!}">
const vue = new Vue({
el: "#app",
store,
dicts: ["BENEFIT_FESTIVAL", "BENEFIT_SIGN_MODE", "BENEFIT_PROVIDE_MODE", "BENEFIT_GOODS_PROVIDE_MODE"],
data() {
return {
tableData: [],
finished: false,
loading: false,
pageForm: {
pageNumber: 1,
pageSize: 5,
totalCount: 0,
year: new Date().getFullYear()
},
yearList: [],
descPopup: false,
benefitViews: [],
viewData: {},
userSelection: [],
userSign: ""
}
},
methods: {
doSearch() {
this.tableKey = new Date().getTime()
this.pageForm.pageNumber = 1
this.pageForm.totalCount = 0
this.tableData = []
this.pageData()
},
pageData() {
this.loading = true
this.$axios.post("/platform/benefit/userSelection/mine/pageData", 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
this.loading = false
})
},
async findOneBenefit(id) {
const res = await this.$axios.post("/platform/benefit/userSelection/findOneBenefit", { id })
if (res.code === 0) {
this.viewData = res.data
}
},
async selectSelectionBenefit(benefitId) {
const res = await this.$axios.post("/platform/benefit/userSelection/selectSelectionBenefit", { benefitId })
if (res.code === 0) {
this.userSelection = res.data
}
},
async getUserSelectionSign(benefitId) {
const res = await this.$axios.post("/platform/h5/benefit/userSelection/getUserSelectionSign", { benefitId })
if (res.code === 0) {
this.userSign = res.data.userSign
}
},
async openView(id) {
await this.findOneBenefit(id)
if (this.viewData.provideMode === "BENEFIT_PROVIDE_MODE_ONE") {
this.benefitViews = ["1"]
this.userSelection = []
await this.getUserSelectionSign(id)
} else {
this.benefitViews = []
await this.selectSelectionBenefit(id)
this.userSelection.map((v) => {
const data = this.viewData.benefitGoodsList.find((b) => b.id === v.goodsId)
v.goods = data
if (v.specificationsId) {
const specifications = data.benefitGoodsSpecificationsList.find((x) => x.id === v.specificationsId)
v.specificationsName = specifications.name
}
this.userSign = v.userSign
})
}
this.descPopup = true
}
},
created() {
this.pageData()
}
})
</script>
<!--#
}
#-->
@@ -0,0 +1,329 @@
<!--#
layout("/layouts/platform_h5.html"){
#-->
<style scoped>
#app { --club-primary: #1989fa; background: #f3f7fd; }
.club-apply-page { min-height: 100vh; background: #f3f7fd; }
.club-apply-page .van-nav-bar { background: #fff; box-shadow: 0 1px 8px rgba(28, 69, 120, .06); }
.club-apply-page .van-nav-bar__title { color: #1f2d3d; font-size: 16px; font-weight: 600; }
.club-apply-page .van-nav-bar__left { color: var(--club-primary); }
.club-apply-page .van-nav-bar .van-icon, .club-apply-page .van-nav-bar .van-nav-bar__text { color: var(--club-primary); }
.club-apply-form { padding: 12px 15px calc(132px + env(safe-area-inset-bottom)); background: #f3f7fd; }
.club-apply-section { overflow: hidden; margin-bottom: 12px; background: #fff; border: 1px solid #e9eff7; border-radius: 10px; box-shadow: 0 4px 12px rgba(36, 77, 122, .04); }
.club-apply-section__title { position: relative; margin: 0; padding: 13px 15px 8px 24px; color: #1f2d3d; font-size: 14px; font-weight: 600; line-height: 20px; }
.club-apply-section__title::before { position: absolute; top: 16px; left: 15px; width: 3px; height: 14px; border-radius: 2px; background: var(--club-primary); content: ''; }
.club-apply-section .van-cell-group { margin: 0; }
.club-apply-section .van-cell { min-height: 46px; padding: 11px 15px; color: #40526a; font-size: 14px; }
.club-apply-section .van-cell::after { right: 15px; left: 15px; border-color: #edf2f8; }
.club-apply-section .van-field__label { width: 105px; color: #65778e; }
.club-apply-section .van-field__control { color: #40526a; font-size: 14px; }
.club-apply-section .van-field__control::placeholder { color: #b4c0cf; }
.club-apply-section .van-field--disabled .van-field__control { color: #8b9bb0; }
.club-apply-section .direction-column-field .van-field__label { width: 100%; margin-bottom: 8px; }
.club-apply-section .direction-column-field .van-field__value { width: 100%; }
.club-apply-section .van-field__word-limit { color: #b4c0cf; font-size: 11px; }
.club-apply-actions { position: fixed; right: 0; bottom: 0; left: 0; z-index: 5; padding: 10px 15px calc(10px + env(safe-area-inset-bottom)); background: rgba(255, 255, 255, .98); border-top: 1px solid #edf2f8; }
.club-apply-actions__agree { padding-bottom: 9px; }
.club-apply-actions__agree .van-checkbox { align-items: flex-start; }
.club-apply-actions__agree .van-checkbox__icon { margin-top: 2px; }
.club-apply-actions__agree .van-checkbox__label { color: #65778e; font-size: 12px; line-height: 18px; }
.club-apply-actions__buttons { display: flex; gap: 10px; }
.club-apply-actions .van-button { flex: 1; height: 40px; border-radius: 8px; font-size: 14px; font-weight: 500; }
.club-apply-actions .van-button--info { background: var(--club-primary); border-color: var(--club-primary); }
.club-apply-actions .van-button--info.van-button--plain { color: var(--club-primary); background: #fff; }
</style>
<div id="app" v-cloak class="club-apply-page">
<van-nav-bar title="文体社团会员申请" left-text="返回" left-arrow @click-left="onNavBack" fixed placeholder></van-nav-bar>
<van-form ref="formRef" class="club-apply-form">
<section class="club-apply-section">
<h2 class="club-apply-section__title">基本信息</h2>
<van-cell-group :border="false">
<van-field :rules="[{ required: true, message: '请选择社团' }]" v-model="formData.clubName" label="社团" placeholder="请选择社团" required is-link readonly name="clubName" @click="openClubPopup"></van-field>
<van-field label="姓名" :rules="[{ required: true }]" v-model="formData.userName" readonly required name="userName"></van-field>
<van-field label="工号" :rules="[{ required: true }]" v-model="formData.loginName" readonly required name="loginName"></van-field>
<van-field label="性别" :rules="[{ required: true }]" v-model="formData.sex" readonly required></van-field>
<van-field label="出生年月" v-model="formData.birthday" readonly placeholder="读取信息中心数据"></van-field>
<van-field label="联系电话" v-model="formData.mobile" placeholder="请输入联系电话"></van-field>
<van-field label="电子信箱" v-model="formData.email" placeholder="请输入电子信箱"></van-field>
<van-field label="分工会" v-model="formData.unionName" readonly></van-field>
<van-field label="部门" v-model="formData.unitName" readonly></van-field>
<van-field label="职务" v-model="formData.position" readonly placeholder="读取信息中心数据"></van-field>
<van-field label="职称" v-model="formData.technicalTitle" readonly placeholder="读取信息中心数据"></van-field>
<van-field label="学历" v-model="formData.education" readonly placeholder="读取信息中心数据"></van-field>
<van-field label="学位" v-model="formData.academicDegree" readonly placeholder="读取信息中心数据"></van-field>
</van-cell-group>
</section>
<section class="club-apply-section">
<h2 class="club-apply-section__title">其他信息</h2>
<van-cell-group :border="false">
<van-field class="direction-column-field" v-model="formData.sameTimeJoinOtherClubSituation" label="同时参加其他社团情况" type="textarea" rows="4" autosize maxlength="500" show-word-limit placeholder="请填写同时参加其他社团情况"></van-field>
<van-field class="direction-column-field" v-model="formData.awardsExperience" label="文化、体育方面的活动经历、获奖情况" type="textarea" rows="4" autosize maxlength="500" show-word-limit placeholder="请填写相关经历及获奖情况"></van-field>
</van-cell-group>
</section>
<section class="club-apply-section">
<h2 class="club-apply-section__title">照片</h2>
<van-cell-group :border="false">
<van-field class="direction-column-field" name="avatar" label="上传本人照片">
<template #input>
<h5-file-upload slot="input" :value.sync="formData.avatar" :upload_number="1" upload_mode="image" upload_result_category="interval" upload_result_type="url"></h5-file-upload>
</template>
</van-field>
</van-cell-group>
</section>
<section class="club-apply-section">
<h2 class="club-apply-section__title">电子签名</h2>
<van-cell-group :border="false">
<van-field class="direction-column-field" name="signature" label="请确认签名">
<template #input>
<h5-signature v-model="formData.signature" slot="input"></h5-signature>
</template>
</van-field>
</van-cell-group>
</section>
</van-form>
<div class="club-apply-actions">
<div class="club-apply-actions__agree"><van-checkbox v-model="isAgree" shape="square">注:本人已仔细阅读并愿意遵守所参加本校教职工文体社团的章程和规定,自愿加入所报名社团。</van-checkbox></div>
<div class="club-apply-actions__buttons">
<van-button native-type="button" :loading="formLoading" @click="onSave" type="info" plain>保存申请</van-button>
<van-button :loading="formLoading" @click="onSubmit" type="info" v-if="!taskId">提交申请</van-button>
<van-button :loading="formLoading" @click="onFinishTask" type="info" v-else>提交申请</van-button>
</div>
</div>
<van-popup v-model="showClubPopup" position="bottom" get-container="#app" @click-overlay="closeClubPopup">
<van-picker show-toolbar :columns="clubOptions.map((item) => item.clubName)" @confirm="onClubConfirm" @cancel="closeClubPopup"></van-picker>
</van-popup>
<van-dialog :value="confirmVisible" title="提示" :message="confirmMessage" show-cancel-button @confirm="confirmPendingAction" @cancel="closeConfirm"></van-dialog>
</div>
<script nonce="${cspNonce!}">
const vue = new Vue({
el: "#app",
store,
components: {},
data() {
return {
bizId: GetQueryString("bizId"),
taskId: GetQueryString("taskId"),
formData: {
clubId: "", clubName: "", userName: "", loginName: "", sex: "", birthday: "", mobile: "", email: "", unionName: "", unitName: "", position: "", technicalTitle: "", education: "", academicDegree: "", sameTimeJoinOtherClubSituation: "", awardsExperience: "", avatar: "", signature: ""
},
clubOptions: [],
showClubPopup: false,
isAgree: false,
formLoading: false,
confirmVisible: false,
pendingAction: "",
historyLayerUnsubscribe: null
}
},
computed: {
confirmMessage() {
return this.pendingAction === 'save' ? '您确定保存当前申请吗?' : '您确定提交申请吗?';
}
},
created() {
const manager = window.h5HistoryLayerManager;
if (manager) {
manager.ensureRegistered('club-apply');
this.historyLayerUnsubscribe = manager.subscribe((layers) => {
this.$set(this, 'showClubPopup', layers.includes('club-apply-club-picker'));
this.$set(this, 'confirmVisible', layers.includes('club-apply-confirm'));
});
}
this.listClub();
this.init();
},
beforeDestroy() {
if (this.historyLayerUnsubscribe) {
this.historyLayerUnsubscribe();
}
const manager = window.h5HistoryLayerManager;
if (manager) {
manager.unregister('club-apply');
}
},
methods: {
onNavBack() {
const manager = window.h5HistoryLayerManager;
if (manager && manager.stack.length) {
manager.close();
return;
}
historyBack();
},
openClubPopup() {
const manager = window.h5HistoryLayerManager;
if (manager) {
manager.open('club-apply-club-picker');
return;
}
this.$set(this, 'showClubPopup', true);
},
closeClubPopup(afterClose) {
const manager = window.h5HistoryLayerManager;
if (manager) {
manager.close('club-apply-club-picker', afterClose);
return;
}
this.$set(this, 'showClubPopup', false);
if (afterClose) {
afterClose();
}
},
openConfirm(action) {
this.$set(this, 'pendingAction', action);
const manager = window.h5HistoryLayerManager;
if (manager) {
manager.open('club-apply-confirm');
return;
}
this.$set(this, 'confirmVisible', true);
},
closeConfirm(afterClose) {
const manager = window.h5HistoryLayerManager;
if (manager) {
manager.close('club-apply-confirm', afterClose);
return;
}
this.$set(this, 'confirmVisible', false);
if (afterClose) {
afterClose();
}
},
onSave() {
this.openConfirm('save');
},
onSubmit() {
this.validateAndConfirm('submit');
},
onFinishTask() {
this.validateAndConfirm('submitAgain');
},
validateAndConfirm(action) {
this.$refs.formRef.validate().then(() => {
if (!this.isAgree) {
this.$toast('请先阅读并同意协议');
return;
}
this.openConfirm(action);
});
},
confirmPendingAction() {
const action = this.pendingAction;
this.closeConfirm(() => {
if (action === 'save') {
this.saveApplication();
} else if (action === 'submitAgain') {
this.submitApplication('/platform/club/join/apply/submitAgain', {data: JSON.stringify(this.formData), taskId: this.taskId});
} else {
this.submitApplication('/platform/club/join/apply/submit', {data: JSON.stringify(this.formData), mode: true});
}
});
},
saveApplication() {
this.$set(this, 'formLoading', true);
this.$axios.post('/platform/club/join/apply/save', {data: JSON.stringify(this.formData)}).then((res) => {
if (res.code === 0) {
this.$toast(res.msg);
this.clearLayersAndGoMine();
}
}).finally(() => {
this.$set(this, 'formLoading', false);
});
},
submitApplication(url, params) {
this.$set(this, 'formLoading', true);
this.$axios.post(url, params).then((res) => {
if (res.code === 0) {
this.$toast(res.msg);
this.clearLayersAndGoMine();
}
}).finally(() => {
this.$set(this, 'formLoading', false);
});
},
clearLayersAndGoMine() {
const manager = window.h5HistoryLayerManager;
if (manager) {
manager.closeAll(() => {
this.$pjaxReplace('/platform/club/join/mine/h5');
});
return;
}
this.$pjaxReplace('/platform/club/join/mine/h5');
},
checkApplyClub(clubId) {
this.$axios.post('/platform/club/join/apply/checkApplyClub', {clubId: clubId}).then((res) => {
if (res.code !== 0) {
this.$toast(res.msg);
this.$set(this.formData, 'clubId', '');
this.$set(this.formData, 'clubName', '');
}
});
},
onClubConfirm(value, index) {
const club = this.clubOptions[index];
if (club) {
this.$set(this.formData, 'clubId', club.id);
this.$set(this.formData, 'clubName', value);
this.checkApplyClub(club.id);
}
this.closeClubPopup();
},
listClub() {
this.$axios.post('/platform/club/common/listClub').then((res) => {
if (res.code === 0) {
this.$set(this, 'clubOptions', res.data || []);
}
});
},
init() {
if (this.bizId) {
this.$axios.post('/platform/club/join/apply/info', {id: this.bizId}).then((res) => {
if (res.code === 0) {
const formData = Object.assign({}, res.data || {});
// 兼容历史空数组字符串,避免编辑页把 "[]" 当作图片地址回显。
if (formData.avatar === '[]') {
formData.avatar = '';
}
this.$set(this, 'formData', formData);
this.$set(this, 'isAgree', true);
}
});
return;
}
const user = this.$store.state.user;
this.$set(this.formData, 'userId', user.id);
this.$set(this.formData, 'userName', user.username);
this.$set(this.formData, 'loginName', user.loginname);
this.$set(this.formData, 'unitId', user.unit ? user.unit.id : null);
this.$set(this.formData, 'unitName', user.unit ? user.unit.name : null);
this.$set(this.formData, 'unionId', user.union ? user.union.id : null);
this.$set(this.formData, 'unionName', user.union ? user.union.name : null);
this.$set(this.formData, 'sex', user.sex);
this.$set(this.formData, 'birthday', user.birthday ? this.$moment(user.birthday).format('YYYY-MM-DD') : '');
this.$set(this.formData, 'nation', user.nation);
this.$set(this.formData, 'mobile', user.mobile);
this.$set(this.formData, 'political', user.political);
this.$set(this.formData, 'education', user.education);
this.$set(this.formData, 'technicalTitle', user.technicalTitle);
this.$set(this.formData, 'position', user.position);
this.$set(this.formData, 'academicDegree', user.academicDegree);
}
}
})
</script>
<!--#
}
#-->
@@ -0,0 +1,265 @@
<!--#
layout("/layouts/platform_h5.html"){
#-->
<style id="style-club-user-join-h5">
<!--#include('../common/clubUserJoin.css'){}#-->
</style>
<style>
#app { --club-primary: #1989fa; }
.club-approval-page { min-height: 100vh; padding-bottom: calc(24px + env(safe-area-inset-bottom)); box-sizing: border-box; background: #f3f7fd; }
.club-approval-page .van-nav-bar, .club-approval-page .van-sticky--fixed { background: #fff; }
.club-approval-page .van-nav-bar__title { color: #172033; font-size: 16px; font-weight: 600; }
.club-approval-page .van-nav-bar .van-icon, .club-approval-page .van-nav-bar__text { color: #1989fa; }
.club-approval-sticky { padding: 22px 12px 1px; box-sizing: border-box; background: #f3f7fd; }
.club-approval-search { margin: 0 0 8px; padding: 0; overflow: hidden; border-radius: 12px; background: #fff; box-shadow: 0 5px 16px rgba(43, 73, 112, .1); }
.club-approval-search .van-search__content { height: 42px; padding-left: 12px; align-items: center; border-radius: 12px; background: #fff; }
.club-approval-search .van-field__control { color: #263548; font-size: 14px; }
.club-approval-search .van-field__control::placeholder { color: #a7b0bf; }
.club-approval-filter { margin-bottom: 8px; overflow: hidden; border-radius: 10px; box-shadow: 0 5px 16px rgba(43, 73, 112, .08); }
.club-approval-filter .van-dropdown-menu__bar { height: 44px; box-shadow: none; }
.club-approval-filter .van-dropdown-menu__item, .club-approval-filter .van-dropdown-menu__title { height: 44px; }
.club-approval-filter .van-dropdown-menu__item { align-items: center; justify-content: center; }
.club-approval-filter .van-dropdown-menu__title { display: inline-flex; align-items: center; color: #66758a; font-size: 14px; line-height: 20px; }
.club-approval-filter .van-dropdown-menu__title--active, .club-approval-filter .van-dropdown-item__option--active, .club-approval-filter .van-dropdown-item__option--active .van-dropdown-item__icon { color: #1989fa; }
.club-approval-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); }
.club-approval-filter .van-dropdown-item__option { display: flex; min-height: 48px; padding: 0 16px; align-items: center; color: #46566c; font-size: 14px; }
.club-approval-list-content { padding: 0 12px; }
.club-approval-page .table-list-container { margin-top: 0; padding-bottom: 2px; }
.club-approval-page .table-list-container .table-list-item { margin-bottom: 12px; padding: 14px; border: 1px solid #e9eff7; border-radius: 12px; box-shadow: 0 5px 16px rgba(43, 73, 112, .08); }
.club-card-header { display: flex; margin-bottom: 8px; align-items: flex-start; justify-content: space-between; }
.club-card-header__heading { min-width: 0; padding-right: 10px; flex: 1; }
.club-card-header__title { overflow: hidden; color: #253247; font-size: 15px; font-weight: 600; line-height: 22px; text-overflow: ellipsis; white-space: nowrap; }
.club-card-header__sub { margin-top: 1px; color: #9ba6b6; font-size: 12px; line-height: 18px; }
.club-card-header__status { display: inline-flex; flex: none; }
.club-card-header__status .van-tag { min-height: 24px; padding: 3px 9px; border: 0; border-radius: 12px; box-sizing: border-box; font-size: 12px; line-height: 18px; }
.club-approval-page .table-list-container .table-list-item .item-actions { margin-top: 10px; padding-top: 10px; border-top-color: #edf1f6; }
.club-approval-page .table-list-container .table-list-item .action-btn { min-height: 28px; padding: 4px 11px; border-radius: 14px; color: #1989fa; background: #eaf4ff; font-size: 12px; line-height: 18px; }
.club-approval-page .table-list-container .table-list-item .action-btn.delete { color: #f04444; background: #fff0f0; }
.club-approval-page .table-list-container .table-list-item .action-btn .van-icon { margin-right: 5px; font-size: 13px; }
.club-approval-page .empty-state { padding: 0; }
.club-audit-card { margin: 0 0 11px; overflow: hidden; border: 1px solid #e9eff7; border-radius: 10px; background: #fff; box-shadow: 0 3px 11px rgba(51, 87, 126, .08); }
.club-audit-card__title { position: relative; margin: 0; padding: 12px 14px 8px 21px; color: #354255; font-size: 15px; font-weight: 600; line-height: 22px; }
.club-audit-card__title::before { position: absolute; top: 15px; bottom: 11px; left: 12px; width: 2px; border-radius: 2px; background: var(--club-primary); content: ''; }
.club-audit-card__required { position: absolute; left: 6px; color: #ee0a24; font-size: 12px; }
.club-audit-card .club-audit-payment-field { min-height: 44px; padding: 10px 14px; background: transparent; }
.club-audit-card .club-audit-payment-field::after { right: 14px; left: 14px; border-color: #eef2f7; }
.club-audit-card .club-audit-payment-field .van-field__label { width: 102px; color: #596779; font-size: 12px; line-height: 24px; }
.club-audit-card .club-audit-payment-field .van-field__control { color: #4f5d70; font-size: 12px; line-height: 24px; }
.club-audit-card .van-radio-group { display: flex; align-items: center; min-height: 24px; white-space: nowrap; }
.club-audit-card .van-radio { margin-right: 12px; }
.club-audit-card .van-radio__label { color: #4f5d70; font-size: 12px; line-height: 24px; }
.club-audit-field-label { position: relative; margin: 0; padding: 10px 14px 6px; color: #596779; font-size: 12px; line-height: 24px; }
.club-audit-card .van-field.club-audit-field { width: calc(100% - 28px); margin: 0 14px 14px; padding: 12px; box-sizing: border-box; border: 1px solid #c9d5e5; border-radius: 10px; background: #f7f9fd; }
.club-audit-card .van-field.club-audit-field .van-field__control { min-height: 150px; color: #253247; font-size: 12px; line-height: 22px; }
.club-audit-card .van-field.club-audit-field .van-field__control::placeholder { color: #9aa7b8; }
.club-audit-card .van-field.club-audit-field .van-field__word-limit { color: #718096; font-size: 12px; }
.club-audit-card .van-field.club-audit-field::after { display: none; }
.club-audit-actions { display: flex; column-gap: 10px; padding: 10px 12px calc(12px + env(safe-area-inset-bottom)); }
.club-audit-actions .van-button { height: 40px; border-radius: 14px; font-size: 14px; }
.club-audit-actions .van-button--primary { border-color: var(--club-primary); background: var(--club-primary); }
.club-audit-actions .van-button--danger { border-color: #ff4d4f; background: #ff4d4f; }
.club-list-skeleton { padding: 0; }
.club-skeleton-card { margin-bottom: 12px; padding: 14px; background: #fff; border: 1px solid #e9eff7; border-radius: 12px; }
.club-skeleton-line { height: 13px; margin-bottom: 12px; border-radius: 7px; background: linear-gradient(90deg, #f2f3f5 25%, #e6e8eb 37%, #f2f3f5 63%); background-size: 400% 100%; animation: club-skeleton-loading 1.4s ease infinite; }
.club-skeleton-line--title { width: 46%; height: 16px; }
.club-skeleton-line--short { width: 60%; }
.club-empty { display: flex; min-height: 280px; padding: 46px 18px 36px; align-items: center; justify-content: center; box-sizing: border-box; flex-direction: column; text-align: center; }
.club-empty img { display: block; width: 86%; max-width: 290px; height: auto; object-fit: contain; }
.club-empty__title { margin-top: 14px; color: #50627a; font-size: 15px; font-weight: 600; line-height: 22px; }
.club-empty__hint { margin-top: 5px; color: #9aa7b8; font-size: 12px; line-height: 18px; }
@keyframes club-skeleton-loading { 0% { background-position: 100% 50%; } 100% { background-position: 0 50%; } }
</style>
<div id="app" v-cloak class="club-approval-page">
<van-nav-bar title="社团审核" left-text="返回" left-arrow @click-left="onNavBack" fixed placeholder></van-nav-bar>
<van-sticky offset-top="46px">
<div class="club-approval-sticky">
<van-search v-model="pageForm.searchKeyword" class="club-approval-search" placeholder="请输入姓名或者工号搜索" shape="round" clearable @search="doSearch" @clear="doSearch"></van-search>
<van-dropdown-menu class="club-approval-filter" :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.mode" :options="modeOptions" :multiple="false" @change="doSearch"></van-dropdown-item>
<!-- 全部工会筛选已按移动端审核页面要求隐藏,接口不再传 unionId。 -->
<van-dropdown-item v-model="pageForm.approvalText" :options="approvalOptions" :multiple="false" @change="onApprovalFilterChange"></van-dropdown-item>
</van-dropdown-menu>
</div>
</van-sticky>
<main class="club-approval-list-content">
<table-list api="/platform/club/join/clubApproval/pageData" :page_form.sync="pageForm" ref="tableListRef" title="clubName" :show_loading_overlay="false" @ready="onReady" @loading-change="onLoadingChange">
<template #header="{row}">
<div class="club-card-header">
<div class="club-card-header__heading"><div class="club-card-header__title">{{ row.userName || '--' }}</div><div class="club-card-header__sub">工号:{{ row.loginName || '--' }}</div></div>
<enum-tag class="club-card-header__status" v-if="row.instanceState" :value="row.instanceState" name="ProcessInstanceStateEnum" label_key="message" size="small"></enum-tag>
</div>
</template>
<template v-slot="{row}">
<table-column label="社团名称">{{ row.clubName || '--' }}</table-column>
<table-column label="所属工会">{{ row.unionName || '--' }}</table-column>
<table-column label="申请类型">{{ row.mode ? '加入社团' : '退出社团' }}</table-column>
<table-column label="申请时间">{{ row.applyDate || '--' }}</table-column>
<table-column label="所属单位">{{ row.unitName || '--' }}</table-column>
<table-column label="当前节点">{{ row.curTaskName || '--' }}</table-column>
</template>
<template #actions="{row}">
<div class="action-btn" @click="onView(row)"><van-icon name="eye-o"></van-icon><span>查看</span></div>
<div class="action-btn" v-if="row.taskState === 10" @click="onApproval(row)"><van-icon name="edit"></van-icon><span>审核</span></div>
<div class="action-btn delete" v-if="row.canRevoke" @click="onRevoke(row)"><van-icon name="revoke"></van-icon><span>撤回</span></div>
</template>
<template #empty>
<div v-if="listLoading" class="club-list-skeleton"><div v-for="item in 2" :key="item" class="club-skeleton-card"><div class="club-skeleton-line club-skeleton-line--title"></div><div class="club-skeleton-line"></div><div class="club-skeleton-line club-skeleton-line--short"></div></div></div>
<div v-else class="club-empty"><img src="/assets/mobile/img/club/club-approval-empty.png" alt="暂无社团审核记录插画"><div class="club-empty__title">暂无社团审核记录</div><div class="club-empty__hint">当前筛选条件下暂时没有社团申请</div></div>
</template>
</table-list>
</main>
<info ref="infoRef" page-key="club-approval" :merge-apply-info="true">
<template v-if="showApprovalForm">
<section class="club-audit-card">
<h2 class="club-audit-card__title">{{ formData.taskName || '社团审核' }}</h2>
<van-form ref="formRef">
<van-field v-if="formData.mode === true" v-model="formData.tf_payed" class="club-audit-payment-field" name="tf_payed" label="缴费状态" :rules="[{ required: true, message: '请选择缴费状态' }]" required>
<template #input>
<van-radio-group v-model="formData.tf_payed" direction="horizontal"><van-radio name="0">未缴费</van-radio><van-radio name="1">已缴费</van-radio></van-radio-group>
</template>
</van-field>
<p class="club-audit-field-label"><span class="club-audit-card__required">*</span>审批意见</p>
<van-field v-model="formData.tf_opinion" class="club-audit-field" name="tf_opinion" type="textarea" rows="6" label="" placeholder="请输入审批意见" :rules="[{ required: true, message: '请填写审批意见' }]" maxlength="100" show-word-limit></van-field>
</van-form>
</section>
<div class="club-audit-actions"><van-button :loading="formLoading" type="primary" block @click="handleTaskAction(1)">同意</van-button><van-button :loading="formLoading" type="danger" block @click="handleTaskAction(2)">拒绝</van-button><van-button :loading="formLoading" type="danger" block @click="handleTaskAction(6)">退回</van-button></div>
</template>
</info>
<van-dialog :value="confirmVisible" title="提示" :message="confirmMessage" show-cancel-button :confirm-button-loading="formLoading" @confirm="confirmPendingAction" @cancel="closeConfirm"></van-dialog>
</div>
<script nonce="${cspNonce!}">
<!--#include('../common/clubUserJoin.js'){}#-->
new Vue({
el: "#app",
store,
dicts: ['PROCESS_INSTANCE_STATE'],
components: {info: clubUserJoin},
data() {
return {
pageForm: {pageNumber: 1, pageSize: 10, totalCount: 0, searchKeyword: '', name: null, approvalText: '0', approval: false, year: new Date().getFullYear(), mode: null},
formData: {},
showApprovalForm: false,
modeOptions: [{text: '全部类型', value: null}, {text: '申请加入', value: true}, {text: '申请退出', value: false}],
approvalOptions: [{text: '未审核', value: '0'}, {text: '已审核', value: '1'}],
listLoading: true,
formLoading: false,
confirmVisible: false,
pendingAction: '',
pendingSubmitType: null,
pendingRevokeRow: null,
historyLayerUnsubscribe: null
}
},
computed: {
confirmMessage() { return this.pendingAction === 'revoke' ? '您确定要撤回吗?' : '您确定要提交审核结果吗?'; }
},
created() {
const manager = window.h5HistoryLayerManager;
if (manager) {
manager.ensureRegistered('club-approval');
this.historyLayerUnsubscribe = manager.subscribe((layers) => {
this.$set(this, 'confirmVisible', layers.includes('club-approval-confirm'));
});
}
},
beforeDestroy() {
if (this.historyLayerUnsubscribe) { this.historyLayerUnsubscribe(); }
const manager = window.h5HistoryLayerManager;
if (manager) { manager.unregister('club-approval'); }
},
methods: {
onNavBack() {
const manager = window.h5HistoryLayerManager;
if (manager && manager.stack.length) { manager.close(); return; }
historyBack();
},
onReady() {
this.doSearch();
},
onApprovalFilterChange(value) {
// 下拉组件使用字符串,查询接口 approval 参数需转换为布尔值;默认 false 查询未审核记录。
this.$set(this.pageForm, 'approval', value === '1');
this.doSearch();
},
onLoadingChange(state) { this.$set(this, 'listLoading', state.loading); },
onView(row) {
this.$set(this, 'showApprovalForm', false);
this.$refs.infoRef.onOpen(row, '社团申请详情');
},
onApproval(row) {
this.$set(this, 'formData', {processTaskId: row.taskId, taskName: row.curTaskName, tf_payed: '0', userName: row.userName, mode: row.mode, tf_opinion: ''});
this.$set(this, 'showApprovalForm', true);
this.$refs.infoRef.onOpen(row, row.curTaskName || '社团审核');
},
openConfirm(action, submitType, row) {
this.$set(this, 'pendingAction', action);
this.$set(this, 'pendingSubmitType', submitType);
this.$set(this, 'pendingRevokeRow', row);
const manager = window.h5HistoryLayerManager;
if (manager) { manager.open('club-approval-confirm'); return; }
this.$set(this, 'confirmVisible', true);
},
closeConfirm(afterClose) {
const manager = window.h5HistoryLayerManager;
if (manager) { manager.close('club-approval-confirm', afterClose); return; }
this.$set(this, 'confirmVisible', false);
if (afterClose) { afterClose(); }
},
// 审批意见为空时先提示并触发字段校验,校验通过后才允许打开审核确认框。
handleTaskAction(submitType) {
const opinion = (this.formData.tf_opinion || '').trim();
if (!opinion) {
this.$toast('请填写审批意见');
this.$refs.formRef.validate('tf_opinion').catch(() => {});
return;
}
this.$set(this.formData, 'tf_opinion', opinion);
this.$refs.formRef.validate().then(() => { this.openConfirm('submit', submitType, null); }).catch(() => {});
},
confirmPendingAction() {
const action = this.pendingAction;
const submitType = this.pendingSubmitType;
const row = this.pendingRevokeRow;
this.closeConfirm(() => {
if (action === 'revoke') { this.revoke(row); } else { this.submitTaskAction(submitType); }
});
},
submitTaskAction(submitType) {
this.$set(this, 'formLoading', true);
const taskData = Object.assign({}, this.formData, {submitType: submitType});
this.$axios.post('/flow/common/executeTask', {data: JSON.stringify(taskData)}).then((res) => {
if (res.code === 0) {
const afterClose = () => { this.$toast.success(res.msg); this.doSearch(); };
const manager = window.h5HistoryLayerManager;
if (manager) { manager.closeAll(afterClose); } else { this.$refs.infoRef.onClose(afterClose); }
}
}).finally(() => { this.$set(this, 'formLoading', false); });
},
onRevoke(row) { this.openConfirm('revoke', null, row); },
revoke(row) {
this.$set(this, 'formLoading', true);
this.$axios.post('/flow/common/revokeTask', {taskId: row.taskId}).then((res) => {
if (res.code === 0) { this.$toast.success(res.msg); this.doSearch(); }
}).finally(() => { this.$set(this, 'formLoading', false); });
},
doSearch() {
this.$nextTick(() => {
this.pageForm.pageNumber = 1;
this.pageForm.totalCount = 0;
this.$refs.tableListRef.doSearch();
});
}
}
})
</script>
<!--#
}
#-->
@@ -0,0 +1,221 @@
.club-user-join-popup {
overflow: hidden;
background: #f3f7fc;
}
.club-user-join-detail {
display: flex;
height: 100%;
padding-top: 46px;
overflow: hidden;
box-sizing: border-box;
background: #f3f7fc;
color: #1f2d3d;
flex-direction: column;
}
.club-user-join-detail > .van-nav-bar {
z-index: 20;
background: #fff;
}
.club-user-join-detail > .van-nav-bar .van-icon,
.club-user-join-detail > .van-nav-bar .van-nav-bar__text {
color: #1989fa;
}
.club-user-join-detail__scroll {
min-height: 0;
overflow-y: auto;
flex: 1;
-webkit-overflow-scrolling: touch;
}
.club-user-join-detail__content {
padding: 12px 15px calc(18px + env(safe-area-inset-bottom));
}
.club-user-join-card {
margin-bottom: 11px;
overflow: hidden;
border: 1px solid #e9eff7;
border-radius: 10px;
background: #fff;
box-shadow: 0 3px 11px rgba(51, 87, 126, .08);
}
.club-user-join-card__title {
position: relative;
margin: 0;
padding: 12px 14px 8px 21px;
color: #354255;
font-size: 15px;
font-weight: 600;
line-height: 22px;
}
.club-user-join-card__title::before {
position: absolute;
top: 15px;
bottom: 11px;
left: 12px;
width: 2px;
border-radius: 2px;
background: #1989fa;
content: '';
}
.club-user-join-card__grid {
display: grid;
padding: 0 14px;
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.club-user-join-card__item {
min-width: 0;
padding: 0 10px 13px 0;
}
.club-user-join-card__item--full {
grid-column: span 2;
}
.club-user-join-card__item span,
.club-user-join-material > span {
display: block;
margin-bottom: 4px;
color: #8b9bb0;
font-size: 12px;
}
.club-user-join-card__item strong {
display: block;
overflow: hidden;
color: #40526a;
font-size: 13px;
font-weight: 400;
line-height: 19px;
text-overflow: ellipsis;
white-space: nowrap;
}
.club-user-join-card__item--full strong {
white-space: normal;
}
.club-user-join-material {
display: flex;
padding: 0 14px;
align-items: center;
min-height: 40px;
margin-bottom: 12px;
}
.club-user-join-material > span {
flex: 0 0 82px;
margin: 0;
}
.club-user-join-material strong {
color: #40526a;
font-size: 13px;
font-weight: 400;
}
.club-user-join-material .van-image {
width: 56px;
height: 40px;
overflow: hidden;
border: 1px solid #edf2f8;
border-radius: 6px;
}
.club-user-join-info-card .van-cell-group {
margin: 0;
background: transparent;
}
.club-user-join-info-card .van-cell {
min-height: 44px;
padding: 10px 14px;
background: transparent;
}
.club-user-join-info-card .van-cell:not(:last-child)::after {
right: 14px;
left: 14px;
border-color: #eef2f7;
}
.club-user-join-info-card .van-cell__title {
width: 102px;
color: #596779;
font-size: 12px;
line-height: 24px;
flex: 0 0 102px;
}
.club-user-join-info-card .van-cell__value {
min-width: 0;
color: #4f5d70;
font-size: 12px;
line-height: 24px;
text-align: left;
overflow-wrap: anywhere;
}
.club-user-join-info-card .club-user-join-info-long-cell {
display: block;
}
.club-user-join-info-card .club-user-join-info-long-cell .van-cell__title {
width: auto;
max-width: 100%;
}
.club-user-join-info-card .club-user-join-info-long-cell .van-cell__value {
margin-top: 7px;
}
.club-user-join-info-image {
display: block;
width: 56px;
height: 40px;
overflow: hidden;
border: 1px solid #edf2f8;
border-radius: 6px;
}
.club-user-join-task-card .van-cell-group {
margin: 0;
background: transparent;
}
.club-user-join-task-card .van-cell {
min-height: 44px;
padding: 10px 14px;
background: transparent;
}
.club-user-join-task-card .van-cell:not(:last-child)::after {
right: 14px;
left: 14px;
border-color: #eef2f7;
}
.club-user-join-task-card .van-cell__title {
width: 102px;
color: #596779;
font-size: 12px;
line-height: 24px;
flex: 0 0 102px;
}
.club-user-join-task-card .van-cell__value {
min-width: 0;
color: #4f5d70;
font-size: 12px;
line-height: 24px;
text-align: left;
overflow-wrap: anywhere;
}
@@ -0,0 +1,178 @@
const clubUserJoin = {
template:
/*language=HTML*/
`
<van-popup v-model="visible" position="right" :style="{ width: '100%', height: '100%' }" get-container="#app" class="club-user-join-popup" :close-on-click-overlay="false">
<div class="club-user-join-detail">
<van-nav-bar :title="detailTitle" left-text="返回" left-arrow fixed @click-left="onBack"></van-nav-bar>
<div class="club-user-join-detail__scroll">
<main class="club-user-join-detail__content">
<section class="club-user-join-card club-user-join-info-card">
<h3 class="club-user-join-card__title">申请信息</h3>
<van-cell-group :border="false" v-if="mergeApplyInfo">
<van-cell title="社团名称">{{ viewData.clubName || '--' }}</van-cell>
<van-cell title="姓名">{{ viewData.userName || '--' }}</van-cell>
<van-cell title="工号">{{ viewData.loginName || '--' }}</van-cell>
<van-cell title="联系电话">{{ viewData.mobile || '--' }}</van-cell>
<van-cell title="分工会">{{ viewData.unionName || '--' }}</van-cell>
<van-cell title="部门">{{ viewData.unitName || '--' }}</van-cell>
<van-cell title="性别">{{ viewData.sex || '--' }}</van-cell>
<van-cell title="出生年月">{{ formatDate(viewData.birthday) }}</van-cell>
<van-cell title="电子信箱">{{ viewData.email || '--' }}</van-cell>
<van-cell title="职务">{{ viewData.position || viewData.governmentPosition || '--' }}</van-cell>
<van-cell title="职称">{{ viewData.technicalTitle || viewData.jobTitle || '--' }}</van-cell>
<van-cell title="学历 / 学位">{{ educationText }}</van-cell>
<van-cell title="同时参加其他社团情况" class="club-user-join-info-long-cell"><div>{{ viewData.sameTimeJoinOtherClubSituation || '暂无' }}</div></van-cell>
<van-cell title="文化、体育活动经历及获奖情况" class="club-user-join-info-long-cell"><div>{{ viewData.awardsExperience || '暂无' }}</div></van-cell>
<van-cell title="照片" class="club-user-join-info-long-cell"><van-image v-if="viewData.avatar && viewData.avatar !== '[]'" :src="viewData.avatar" class="club-user-join-info-image" fit="cover"></van-image><span v-else>暂无</span></van-cell>
<van-cell title="签字" class="club-user-join-info-long-cell"><van-image v-if="viewData.signature" :src="viewData.signature" class="club-user-join-info-image" fit="contain"></van-image><span v-else>暂无</span></van-cell>
</van-cell-group>
<div v-else class="club-user-join-card__grid">
<div class="club-user-join-card__item"><span>社团名称</span><strong>{{ viewData.clubName || '--' }}</strong></div>
<div class="club-user-join-card__item"><span>姓名</span><strong>{{ viewData.userName || '--' }}</strong></div>
<div class="club-user-join-card__item"><span>工号</span><strong>{{ viewData.loginName || '--' }}</strong></div>
<div class="club-user-join-card__item"><span>联系电话</span><strong>{{ viewData.mobile || '--' }}</strong></div>
<div class="club-user-join-card__item"><span>分工会</span><strong>{{ viewData.unionName || '--' }}</strong></div>
<div class="club-user-join-card__item"><span>部门</span><strong>{{ viewData.unitName || '--' }}</strong></div>
</div>
</section>
<section v-if="!mergeApplyInfo" class="club-user-join-card">
<h3 class="club-user-join-card__title">个人信息</h3>
<div class="club-user-join-card__grid">
<div class="club-user-join-card__item"><span>性别</span><strong>{{ viewData.sex || '--' }}</strong></div>
<div class="club-user-join-card__item"><span>出生年月</span><strong>{{ formatDate(viewData.birthday) }}</strong></div>
<div class="club-user-join-card__item"><span>电子信箱</span><strong>{{ viewData.email || '--' }}</strong></div>
<div class="club-user-join-card__item"><span>职务</span><strong>{{ viewData.position || viewData.governmentPosition || '--' }}</strong></div>
<div class="club-user-join-card__item"><span>职称</span><strong>{{ viewData.technicalTitle || viewData.jobTitle || '--' }}</strong></div>
<div class="club-user-join-card__item"><span>学历 / 学位</span><strong>{{ educationText }}</strong></div>
<div class="club-user-join-card__item club-user-join-card__item--full"><span>同时参加其他社团情况</span><strong>{{ viewData.sameTimeJoinOtherClubSituation || '暂无' }}</strong></div>
<div class="club-user-join-card__item club-user-join-card__item--full"><span>文化、体育活动经历及获奖情况</span><strong>{{ viewData.awardsExperience || '暂无' }}</strong></div>
</div>
</section>
<section v-if="!mergeApplyInfo" class="club-user-join-card">
<h3 class="club-user-join-card__title">申请材料</h3>
<div class="club-user-join-material">
<span>照片</span>
<van-image v-if="viewData.avatar && viewData.avatar !== '[]'" :src="viewData.avatar" fit="cover"></van-image>
<strong v-else>暂无</strong>
</div>
<div class="club-user-join-material">
<span>签字</span>
<van-image v-if="viewData.signature" :src="viewData.signature" fit="contain"></van-image>
<strong v-else>暂无</strong>
</div>
</section>
<section v-for="(task, index) in doneTasks" :key="task.id || index" class="club-user-join-card club-user-join-task-card">
<h3 class="club-user-join-card__title">{{ task.displayName || '审核节点' }}</h3>
<van-cell-group :border="false" v-if="task.ext && task.ext.isFirstTaskNode">
<van-cell title="申请用户">{{ task.ext.initiatorName || '--' }}({{ task.ext.initiatorAccount || '--' }})</van-cell>
<van-cell title="申请时间">{{ task.finishTime || '--' }}</van-cell>
<van-cell title="办理结果"><dict-tag :options="dict.type.PROCESS_TASK_SUBMIT_TYPE" :value="task.ext.submitType"></dict-tag></van-cell>
</van-cell-group>
<van-cell-group :border="false" v-else>
<van-cell title="办理用户">{{ task.taskFormData && task.taskFormData.userName || '--' }}({{ task.taskFormData && task.taskFormData.loginName || '--' }})</van-cell>
<van-cell title="办理时间">{{ task.finishTime || '--' }}</van-cell>
<van-cell title="办理结果"><dict-tag :options="dict.type.PROCESS_TASK_SUBMIT_TYPE" :value="task.ext && task.ext.submitType"></dict-tag></van-cell>
<van-cell title="办理意见"><div v-html="task.taskFormData && task.taskFormData.tf_opinion || '暂无'"></div></van-cell>
</van-cell-group>
</section>
<slot></slot>
</main>
</div>
</div>
</van-popup>
`,
dicts: ['PROCESS_TASK_SUBMIT_TYPE'],
data() {
return {
visible: false,
detailTitle: '社团申请详情',
viewData: {},
doneTasks: [],
row: null,
historyLayerUnsubscribe: null
}
},
props: {
pageKey: {
type: String,
default: 'club-user-join-detail'
},
// 三个社团查看页启用后,将申请、个人及材料信息合并为同一张申请信息卡片。
mergeApplyInfo: {
type: Boolean,
default: false
}
},
computed: {
educationText() {
const education = this.viewData.education || '';
const degree = this.viewData.academicDegree || '';
return education || degree ? education + (education && degree ? ' / ' : '') + degree : '--';
}
},
created() {
const manager = window.h5HistoryLayerManager;
if (manager) {
manager.ensureRegistered(this.pageKey);
this.historyLayerUnsubscribe = manager.subscribe((layers) => {
this.$set(this, 'visible', layers.includes('club-user-join-detail'));
});
}
},
beforeDestroy() {
if (this.historyLayerUnsubscribe) {
this.historyLayerUnsubscribe();
}
},
methods: {
onOpen(row, title) {
this.$set(this, 'row', row);
this.$set(this, 'detailTitle', title || '社团申请详情');
this.$set(this, 'viewData', Object.assign({}, row || {}));
this.$set(this, 'doneTasks', []);
const manager = window.h5HistoryLayerManager;
if (manager) {
manager.open('club-user-join-detail');
} else {
this.$set(this, 'visible', true);
}
if (!this.row || !this.row.id) {
return;
}
this.getInfo();
this.getDoneTasks();
},
onClose(afterClose) {
const manager = window.h5HistoryLayerManager;
if (manager) {
manager.close('club-user-join-detail', afterClose);
return;
}
this.$set(this, 'visible', false);
if (afterClose) {
afterClose();
}
},
onBack() {
this.onClose();
},
formatDate(date) {
return date ? this.$moment(date).format('YYYY-MM-DD') : '--';
},
getInfo() {
this.$axios.post('/platform/club/join/mine/info', {id: this.row.id}).then((res) => {
if (res.code === 0) {
this.$set(this, 'viewData', Object.assign({}, this.viewData, res.data || {}));
}
})
},
getDoneTasks() {
this.$axios.post('/flow/common/doneTasks', {bizId: this.row.id}).then((res) => {
if (res.code === 0) {
this.$set(this, 'doneTasks', res.data || []);
}
})
}
}
}
@@ -0,0 +1,115 @@
<!--#
layout("/layouts/platform_h5.html"){
#-->
<style id="style-club-user-join-h5">
<!--#include('../common/clubUserJoin.css'){}#-->
</style>
<style>
#app { --club-primary: #1989fa; }
.club-mine-page { min-height: 100vh; padding-bottom: calc(24px + env(safe-area-inset-bottom)); box-sizing: border-box; background: #f3f7fd; }
.club-mine-page .van-nav-bar, .club-mine-page .van-sticky--fixed { background: #fff; }
.club-mine-page .van-nav-bar__title { color: #172033; font-size: 16px; font-weight: 600; }
.club-mine-page .van-nav-bar .van-icon, .club-mine-page .van-nav-bar__text { color: #1989fa; }
.club-mine-sticky { padding: 22px 12px 1px; box-sizing: border-box; background: #f3f7fd; }
.club-mine-search { margin: 0 0 8px; padding: 0; overflow: hidden; border-radius: 12px; background: #fff; box-shadow: 0 5px 16px rgba(43, 73, 112, .1); }
.club-mine-search .van-search__content { height: 42px; padding-left: 12px; align-items: center; border-radius: 12px; background: #fff; }
.club-mine-search .van-field__control { color: #263548; font-size: 14px; }
.club-mine-search .van-field__control::placeholder { color: #a7b0bf; }
.club-mine-filter { margin-bottom: 8px; overflow: hidden; border-radius: 10px; box-shadow: 0 5px 16px rgba(43, 73, 112, .08); }
.club-mine-filter .van-dropdown-menu__bar, .club-mine-filter .van-dropdown-menu__item, .club-mine-filter .van-dropdown-menu__title { height: 44px; box-shadow: none; }
.club-mine-filter .van-dropdown-menu__item { align-items: center; justify-content: center; }
.club-mine-filter .van-dropdown-menu__title { display: inline-flex; align-items: center; color: #66758a; font-size: 14px; line-height: 20px; }
.club-mine-filter .van-dropdown-menu__title--active, .club-mine-filter .van-dropdown-item__option--active, .club-mine-filter .van-dropdown-item__option--active .van-dropdown-item__icon { color: #1989fa; }
.club-mine-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); }
.club-mine-filter .van-dropdown-item__option { display: flex; min-height: 48px; padding: 0 16px; align-items: center; color: #46566c; font-size: 14px; }
.club-mine-list-content { padding: 0 12px; }
.club-mine-page .table-list-container { margin-top: 0; padding-bottom: 2px; }
.club-mine-page .table-list-container .table-list-item { margin-bottom: 12px; padding: 14px; border: 1px solid #e9eff7; border-radius: 12px; box-shadow: 0 5px 16px rgba(43, 73, 112, .08); }
.club-mine-page .empty-state { padding: 0; }
.club-card-header { display: flex; margin-bottom: 8px; align-items: flex-start; justify-content: space-between; }
.club-card-header__heading { min-width: 0; padding-right: 10px; flex: 1; }
.club-card-header__title { overflow: hidden; color: #253247; font-size: 15px; font-weight: 600; line-height: 22px; text-overflow: ellipsis; white-space: nowrap; }
.club-card-header__sub { margin-top: 1px; color: #9ba6b6; font-size: 12px; line-height: 18px; }
.club-card-header__status { display: inline-flex; flex: none; }
.club-card-header__status .van-tag { min-height: 24px; padding: 3px 9px; border: 0; border-radius: 12px; box-sizing: border-box; font-size: 12px; line-height: 18px; }
.club-mine-page .table-list-container .table-list-item .item-actions { margin-top: 10px; padding-top: 10px; border-top-color: #edf1f6; }
.club-mine-page .table-list-container .table-list-item .action-btn { min-height: 28px; padding: 4px 11px; border-radius: 14px; color: #1989fa; background: #eaf4ff; font-size: 12px; line-height: 18px; }
.club-mine-page .table-list-container .table-list-item .action-btn.delete { color: #f04444; background: #fff0f0; }
.club-mine-page .table-list-container .table-list-item .action-btn .van-icon { margin-right: 5px; font-size: 13px; }
.club-list-skeleton { padding: 0; }
.club-skeleton-card { margin-bottom: 12px; padding: 14px; background: #fff; border: 1px solid #e9eff7; border-radius: 12px; }
.club-skeleton-line { height: 13px; margin-bottom: 12px; border-radius: 7px; background: linear-gradient(90deg, #f2f3f5 25%, #e6e8eb 37%, #f2f3f5 63%); background-size: 400% 100%; animation: club-skeleton-loading 1.4s ease infinite; }
.club-skeleton-line--title { width: 46%; height: 16px; }
.club-skeleton-line--short { width: 60%; }
.club-empty { display: flex; min-height: 280px; padding: 46px 18px 36px; align-items: center; justify-content: center; box-sizing: border-box; flex-direction: column; text-align: center; }
.club-empty img { display: block; width: 86%; max-width: 290px; height: auto; object-fit: contain; }
.club-empty__title { margin-top: 14px; color: #50627a; font-size: 15px; font-weight: 600; line-height: 22px; }
.club-empty__hint { margin-top: 5px; color: #9aa7b8; font-size: 12px; line-height: 18px; }
@keyframes club-skeleton-loading { 0% { background-position: 100% 50%; } 100% { background-position: 0 50%; } }
</style>
<div id="app" v-cloak class="club-mine-page">
<van-nav-bar title="我的申请" left-text="返回" left-arrow @click-left="onNavBack" fixed placeholder></van-nav-bar>
<van-sticky offset-top="46px">
<div class="club-mine-sticky">
<van-search v-model="pageForm.clubName" class="club-mine-search" placeholder="请输入社团名称搜索" shape="round" clearable @search="doSearch" @clear="doSearch"></van-search>
<van-dropdown-menu class="club-mine-filter" :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-menu>
</div>
</van-sticky>
<main class="club-mine-list-content">
<table-list api="/platform/club/join/mine/pageData" :page_form.sync="pageForm" ref="tableListRef" title="clubName" :show_loading_overlay="false" @ready="doSearch" @loading-change="onLoadingChange">
<template #header="{row}">
<div v-if="row" class="club-card-header"><div class="club-card-header__heading"><div class="club-card-header__title">{{ row.userName || '--' }}</div><div class="club-card-header__sub">工号:{{ row.loginName || '--' }}</div></div><enum-tag class="club-card-header__status" v-if="row.instanceState" :value="row.instanceState" name="ProcessInstanceStateEnum" label_key="message" size="small"></enum-tag></div>
</template>
<template v-slot="{row}">
<template v-if="row"><table-column label="社团名称">{{ row.clubName || '--' }}</table-column><table-column label="申请类型">{{ row.mode ? '加入社团' : '退出社团' }}</table-column><table-column label="申请时间">{{ row.applyDate || '--' }}</table-column><table-column label="当前节点">{{ row.taskName || '--' }}</table-column></template>
</template>
<template #actions="{row}">
<template v-if="row"><div class="action-btn" @click="onView(row)"><van-icon name="eye-o"></van-icon><span>查看</span></div><div class="action-btn" @click="onEdit(row)" v-if="(row.taskKey === 'startTask' || !row.instanceId) && row.mode === true"><van-icon name="edit"></van-icon><span>编辑</span></div><div class="action-btn" @click="exitSubmitAgain(row)" v-if="(row.taskKey === 'startTask' || !row.instanceId) && row.mode === false"><van-icon name="passed"></van-icon><span>提交</span></div><div class="action-btn delete" @click="onRevoke(row)" v-if="row.canRevoke"><van-icon name="revoke"></van-icon><span>撤回</span></div><div class="action-btn delete" @click="onDelete(row)" v-if="row.taskKey === 'startTask' || !row.instanceId"><van-icon name="delete-o"></van-icon><span>删除</span></div></template>
</template>
<template #empty>
<div v-if="listLoading" class="club-list-skeleton"><div v-for="item in 2" :key="item" class="club-skeleton-card"><div class="club-skeleton-line club-skeleton-line--title"></div><div class="club-skeleton-line"></div><div class="club-skeleton-line club-skeleton-line--short"></div></div></div>
<div v-else class="club-empty"><img src="/assets/mobile/img/club/club-approval-empty.png" alt="暂无社团申请记录插画"><div class="club-empty__title">暂无社团申请记录</div><div class="club-empty__hint">当前筛选条件下暂时没有社团申请</div></div>
</template>
</table-list>
</main>
<info ref="infoRef" page-key="club-mine" :merge-apply-info="true"></info>
<van-dialog :value="confirmVisible" title="提示" :message="confirmMessage" show-cancel-button :confirm-button-loading="formLoading" @confirm="confirmPendingAction" @cancel="closeConfirm"></van-dialog>
</div>
<script nonce="${cspNonce!}">
<!--#include('../common/clubUserJoin.js'){}#-->
new Vue({
el: "#app", store, dicts: ['PROCESS_INSTANCE_STATE'], components: {info: clubUserJoin},
data() { return {pageForm: {pageNumber: 1, pageSize: 10, totalCount: 0, year: new Date().getFullYear(), clubName: ''}, listLoading: true, formLoading: false, confirmVisible: false, pendingAction: '', pendingRow: null, historyLayerUnsubscribe: null} },
computed: { confirmMessage() { if (this.pendingAction === 'revoke') { return '您确定要撤回吗?'; } if (this.pendingAction === 'delete') { return '确定要删除此申请吗?'; } return '您确定要提交申请吗?'; } },
created() { const manager = window.h5HistoryLayerManager; if (manager) { manager.ensureRegistered('club-mine'); this.historyLayerUnsubscribe = manager.subscribe((layers) => { this.$set(this, 'confirmVisible', layers.includes('club-mine-confirm')); }); } },
beforeDestroy() { if (this.historyLayerUnsubscribe) { this.historyLayerUnsubscribe(); } const manager = window.h5HistoryLayerManager; if (manager) { manager.unregister('club-mine'); } },
methods: {
onNavBack() { const manager = window.h5HistoryLayerManager; if (manager && manager.stack.length) { manager.close(); return; } historyBack(); },
onLoadingChange(state) { this.$set(this, 'listLoading', state.loading); },
openConfirm(action, row) { this.$set(this, 'pendingAction', action); this.$set(this, 'pendingRow', row); const manager = window.h5HistoryLayerManager; if (manager) { manager.open('club-mine-confirm'); return; } this.$set(this, 'confirmVisible', true); },
closeConfirm(afterClose) { const manager = window.h5HistoryLayerManager; if (manager) { manager.close('club-mine-confirm', afterClose); return; } this.$set(this, 'confirmVisible', false); if (afterClose) { afterClose(); } },
confirmPendingAction() { const action = this.pendingAction; const row = this.pendingRow; this.closeConfirm(() => { if (action === 'revoke') { this.revoke(row); } else if (action === 'delete') { this.deleteApplication(row); } else { this.submitAgain(row); } }); },
exitSubmitAgain(row) { this.openConfirm('submitAgain', row); },
submitAgain(row) { this.$set(this, 'formLoading', true); this.$axios.post('/platform/club/join/apply/info', {id: row.id}).then((res) => { if (res.code === 0) { return this.$axios.post('/platform/club/join/apply/submitAgain', {data: JSON.stringify(res.data), taskId: row.startTaskId}); } this.$toast.fail(res.msg); return null; }).then((res) => { if (res && res.code === 0) { this.$toast(res.msg); this.doSearch(); } }).finally(() => { this.$set(this, 'formLoading', false); }); },
onRevoke(row) { this.openConfirm('revoke', row); },
revoke(row) { this.$set(this, 'formLoading', true); this.$axios.post('/flow/common/revokeTask', {taskId: row.startTaskId}).then((res) => { if (res.code === 0) { this.$toast(res.msg); this.doSearch(); } }).finally(() => { this.$set(this, 'formLoading', false); }); },
onView(row) { this.$refs.infoRef.onOpen(row, '社团申请详情'); },
onEdit(row) { this.$pjaxReplace('/platform/club/join/apply/h5?taskId=' + (row.startTaskId || '') + '&bizId=' + row.id); },
onDelete(row) { this.openConfirm('delete', row); },
deleteApplication(row) { this.$set(this, 'formLoading', true); this.$axios.post('/platform/club/join/mine/delete', {id: row.id}).then((res) => { if (res.code === 0) { this.$toast.success(res.msg); this.doSearch(); } else { this.$toast.fail(res.msg); } }).finally(() => { this.$set(this, 'formLoading', false); }); },
doSearch() { this.$nextTick(() => { this.$set(this.pageForm, 'pageNumber', 1); this.$set(this.pageForm, 'totalCount', 0); this.$refs.tableListRef.doSearch(); }); }
}
})
</script>
<!--#
}
#-->
@@ -0,0 +1,117 @@
<!--#
layout("/layouts/platform_h5.html"){
#-->
<style id="style-club-user-join-h5">
<!--#include('../common/clubUserJoin.css'){}#-->
</style>
<style>
#app { --club-primary: #1989fa; }
.club-member-page { min-height: 100vh; padding-bottom: calc(24px + env(safe-area-inset-bottom)); box-sizing: border-box; background: #f3f7fd; }
.club-member-page .van-nav-bar, .club-member-page .van-sticky--fixed { background: #fff; }
.club-member-page .van-nav-bar__title { color: #172033; font-size: 16px; font-weight: 600; }
.club-member-page .van-nav-bar .van-icon, .club-member-page .van-nav-bar__text { color: #1989fa; }
.club-member-sticky { padding: 22px 12px 1px; box-sizing: border-box; background: #f3f7fd; }
.club-member-search { margin: 0 0 8px; padding: 0; overflow: hidden; border-radius: 12px; background: #fff; box-shadow: 0 5px 16px rgba(43, 73, 112, .1); }
.club-member-search .van-search__content { height: 42px; padding-left: 12px; align-items: center; border-radius: 12px; background: #fff; }
.club-member-search .van-field__control { color: #263548; font-size: 14px; }
.club-member-search .van-field__control::placeholder { color: #a7b0bf; }
.club-member-list-content { padding: 0 12px; }
.club-member-page .table-list-container { margin-top: 0; padding-bottom: 2px; }
.club-member-page .table-list-container .table-list-item { margin-bottom: 12px; padding: 14px; border: 1px solid #e9eff7; border-radius: 12px; box-shadow: 0 5px 16px rgba(43, 73, 112, .08); }
.club-member-page .empty-state { padding: 0; }
.club-card-header { display: flex; margin-bottom: 8px; align-items: flex-start; justify-content: space-between; }
.club-card-header__heading { min-width: 0; padding-right: 10px; flex: 1; }
.club-card-header__title { overflow: hidden; color: #253247; font-size: 15px; font-weight: 600; line-height: 22px; text-overflow: ellipsis; white-space: nowrap; }
.club-card-header__sub { margin-top: 1px; color: #9ba6b6; font-size: 12px; line-height: 18px; }
.club-card-header__status { display: inline-flex; flex: none; }
.club-card-header__status.van-tag, .club-card-header__status .van-tag { min-height: 24px; padding: 3px 9px; border: 0; border-radius: 12px; box-sizing: border-box; font-size: 12px; line-height: 18px; }
.club-member-page .table-list-container .table-list-item .item-actions { margin-top: 10px; padding-top: 10px; border-top-color: #edf1f6; }
.club-member-page .table-list-container .table-list-item .action-btn { min-height: 28px; padding: 4px 11px; border-radius: 14px; color: #1989fa; background: #eaf4ff; font-size: 12px; line-height: 18px; }
.club-member-page .table-list-container .table-list-item .action-btn.delete { color: #f04444; background: #fff0f0; }
.club-member-page .table-list-container .table-list-item .action-btn .van-icon { margin-right: 5px; font-size: 13px; }
.club-list-skeleton { padding: 0; }
.club-skeleton-card { margin-bottom: 12px; padding: 14px; background: #fff; border: 1px solid #e9eff7; border-radius: 12px; }
.club-skeleton-line { height: 13px; margin-bottom: 12px; border-radius: 7px; background: linear-gradient(90deg, #f2f3f5 25%, #e6e8eb 37%, #f2f3f5 63%); background-size: 400% 100%; animation: club-skeleton-loading 1.4s ease infinite; }
.club-skeleton-line--title { width: 46%; height: 16px; }
.club-skeleton-line--short { width: 60%; }
.club-empty { display: flex; min-height: 280px; padding: 46px 18px 36px; align-items: center; justify-content: center; box-sizing: border-box; flex-direction: column; text-align: center; }
.club-empty img { display: block; width: 86%; max-width: 290px; height: auto; object-fit: contain; }
.club-empty__title { margin-top: 14px; color: #50627a; font-size: 15px; font-weight: 600; line-height: 22px; }
.club-empty__hint { margin-top: 5px; color: #9aa7b8; font-size: 12px; line-height: 18px; }
@keyframes club-skeleton-loading { 0% { background-position: 100% 50%; } 100% { background-position: 0 50%; } }
</style>
<div id="app" v-cloak class="club-member-page">
<van-nav-bar title="我的社团" left-text="返回" left-arrow @click-left="onNavBack" fixed placeholder></van-nav-bar>
<van-sticky offset-top="46px"><div class="club-member-sticky"><van-search v-model="pageForm.clubName" class="club-member-search" placeholder="请输入社团名称搜索" shape="round" clearable @search="doSearch" @clear="doSearch"></van-search></div></van-sticky>
<main class="club-member-list-content">
<table-list api="/platform/club/join/mine/club/pageData" :page_form.sync="pageForm" ref="tableListRef" title="userName" :show_loading_overlay="false" @ready="doSearch" @loading-change="onLoadingChange">
<template #header="{row}">
<div v-if="row" class="club-card-header"><div class="club-card-header__heading"><div class="club-card-header__title">{{ row.userName || '--' }}</div><div class="club-card-header__sub">工号:{{ row.loginName || '--' }}</div></div><van-tag class="club-card-header__status" type="success" size="small">已完成</van-tag></div>
</template>
<template v-slot="{row}">
<template v-if="row"><table-column label="社团名称">{{ row.clubName || '--' }}</table-column><table-column label="会长">{{ row.clubLeader || '--' }}</table-column><table-column label="秘书长">{{ row.clubSecretary || '--' }}</table-column><table-column label="当前人数">{{ row.currentPeopleNum || 0 }} 人</table-column></template>
</template>
<template #actions="{row}"><template v-if="row"><div class="action-btn" @click="onView(row)"><van-icon name="eye-o"></van-icon><span>查看</span></div><div class="action-btn delete" @click="onExit(row)"><van-icon name="revoke"></van-icon><span>申请退会</span></div></template></template>
<template #empty>
<div v-if="listLoading" class="club-list-skeleton"><div v-for="item in 2" :key="item" class="club-skeleton-card"><div class="club-skeleton-line club-skeleton-line--title"></div><div class="club-skeleton-line"></div><div class="club-skeleton-line club-skeleton-line--short"></div></div></div>
<div v-else class="club-empty"><img src="/assets/mobile/img/club/club-approval-empty.png" alt="暂无社团记录插画"><div class="club-empty__title">暂无社团记录</div><div class="club-empty__hint">当前筛选条件下暂时没有加入的社团</div></div>
</template>
</table-list>
</main>
<van-popup v-model="clubDetailVisible" position="right" :style="{ width: '100%', height: '100%' }" get-container="#app" class="club-user-join-popup" :close-on-click-overlay="false">
<div class="club-user-join-detail">
<van-nav-bar title="社团详情" left-text="返回" left-arrow fixed @click-left="closeClubDetail"></van-nav-bar>
<div class="club-user-join-detail__scroll">
<main class="club-user-join-detail__content">
<section class="club-user-join-card club-user-join-info-card">
<h3 class="club-user-join-card__title">社团基础信息</h3>
<van-cell-group :border="false">
<van-cell title="社团名称">{{ clubDetail.clubName || '--' }}</van-cell>
<van-cell title="成立时间">{{ clubDetail.foundTime || '--' }}</van-cell>
<van-cell title="社团编码">{{ clubDetail.clubCode || '--' }}</van-cell>
<van-cell title="社团类型"><dict-tag :options="dict.type.CLUB_REGISTER_TYPE" :value="clubDetail.clubType"></dict-tag></van-cell>
<van-cell title="会长">{{ clubDetail.clubLeader || '--' }}</van-cell>
<van-cell title="秘书长">{{ clubDetail.clubSecretary || '--' }}</van-cell>
<van-cell title="当前人数">{{ clubDetail.currentPeopleNum || 0 }} 人</van-cell>
<van-cell title="会费标准">{{ clubDetail.due || '--' }}</van-cell>
<van-cell title="社团介绍" class="club-user-join-info-long-cell"><div v-if="hasClubContent(clubDetail.introduce)" v-html="clubDetail.introduce"></div><span v-else>暂无</span></van-cell>
<van-cell title="社团宗旨" class="club-user-join-info-long-cell"><div v-if="hasClubContent(clubDetail.purpose)" v-html="clubDetail.purpose"></div><span v-else>暂无</span></van-cell>
<van-cell title="社团章程" class="club-user-join-info-long-cell"><file-preview v-if="clubDetail.rulesFile && clubDetail.rulesFile.length" :files="clubDetail.rulesFile" complete_result></file-preview><span v-else>暂无</span></van-cell>
<van-cell title="年度活动计划" class="club-user-join-info-long-cell"><file-preview v-if="clubDetail.yearPlanFile && clubDetail.yearPlanFile.length" :files="clubDetail.yearPlanFile" complete_result></file-preview><span v-else>暂无</span></van-cell>
</van-cell-group>
</section>
</main>
</div>
</div>
</van-popup>
<van-dialog :value="confirmVisible" title="提示" :message="confirmMessage" show-cancel-button :confirm-button-loading="formLoading" @confirm="confirmExit" @cancel="closeConfirm"></van-dialog>
</div>
<script nonce="${cspNonce!}">
new Vue({
el: "#app", store, dicts: ['CLUB_REGISTER_TYPE'],
data() { return {pageForm: {pageNumber: 1, pageSize: 10, totalCount: 0, clubName: ''}, listLoading: true, formLoading: false, clubDetailVisible: false, clubDetail: {}, confirmVisible: false, currentRow: null, historyLayerUnsubscribe: null} },
computed: { confirmMessage() { return this.currentRow ? '您确定要申请退出“' + this.currentRow.clubName + '”吗?' : '您确定要申请退出该社团吗?'; } },
created() { const manager = window.h5HistoryLayerManager; if (manager) { manager.ensureRegistered('club-mineclub'); this.historyLayerUnsubscribe = manager.subscribe((layers) => { this.$set(this, 'clubDetailVisible', layers.includes('club-mineclub-detail')); this.$set(this, 'confirmVisible', layers.includes('club-mineclub-confirm')); }); } },
beforeDestroy() { if (this.historyLayerUnsubscribe) { this.historyLayerUnsubscribe(); } const manager = window.h5HistoryLayerManager; if (manager) { manager.unregister('club-mineclub'); } },
methods: {
onNavBack() { const manager = window.h5HistoryLayerManager; if (manager && manager.stack.length) { manager.close(); return; } historyBack(); },
onLoadingChange(state) { this.$set(this, 'listLoading', state.loading); },
onView(row) { this.$set(this, 'clubDetail', Object.assign({}, row || {})); const manager = window.h5HistoryLayerManager; if (manager) { manager.open('club-mineclub-detail'); return; } this.$set(this, 'clubDetailVisible', true); },
closeClubDetail(afterClose) { const manager = window.h5HistoryLayerManager; if (manager) { manager.close('club-mineclub-detail', afterClose); return; } this.$set(this, 'clubDetailVisible', false); if (afterClose) { afterClose(); } },
hasClubContent(value) { return value !== null && value !== undefined && String(value).replace(/<[^>]*>/g, '').replace(/&nbsp;/gi, '').trim().length > 0; },
onExit(row) { this.$set(this, 'currentRow', row); const manager = window.h5HistoryLayerManager; if (manager) { manager.open('club-mineclub-confirm'); return; } this.$set(this, 'confirmVisible', true); },
closeConfirm(afterClose) { const manager = window.h5HistoryLayerManager; if (manager) { manager.close('club-mineclub-confirm', afterClose); return; } this.$set(this, 'confirmVisible', false); if (afterClose) { afterClose(); } },
confirmExit() { const row = this.currentRow; this.closeConfirm(() => { this.$set(this, 'formLoading', true); this.$axios.post('/platform/club/join/apply/submit', {data: JSON.stringify(row), mode: false}).then((res) => { if (res.code === 0) { this.$toast(res.msg); const manager = window.h5HistoryLayerManager; if (manager) { manager.closeAll(() => { this.$pjaxReplace('/platform/club/join/mine/h5'); }); } else { this.$pjaxReplace('/platform/club/join/mine/h5'); } } }).finally(() => { this.$set(this, 'formLoading', false); }); }); },
doSearch() { this.$nextTick(() => { this.$set(this.pageForm, 'pageNumber', 1); this.$set(this.pageForm, 'totalCount', 0); this.$refs.tableListRef.doSearch(); }); }
}
})
</script>
<!--#
}
#-->
@@ -0,0 +1,185 @@
<!--#
layout("/layouts/platform_h5.html"){
#-->
<style scoped>
</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-search
v-model="pageForm.searchKeyword"
:show-action="false"
:reverse-color="false"
input-align="left"
placeholder="请输入姓名或者工号搜索"
@search="doSearch"
></van-search>
<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.unionId" :options="unionOptions" :multiple="false"
@change="doSearch"></van-dropdown-item>
</van-dropdown-menu>
<van-tabs v-model="pageForm.approvalText"
@change="(val)=>{this.pageForm.approval = val==='1';this.doSearch();}">
<van-tab title="已审核" name="1"></van-tab>
<van-tab title="未审核" name="0"></van-tab>
</van-tabs>
</van-sticky>
<table-list api="/platform/club/join/schoolUnionApproval/pageData" :page_form.sync="pageForm" ref="tableListRef" title="clubName" @ready="onReady">
<template v-slot="{index,row}">
<table-column label="姓名">{{row.userName}}</table-column>
<table-column label="工号">{{row.loginName}}</table-column>
<table-column label="申请模式">{{row.mode ? '加入' : '退出'}}</table-column>
<table-column label="申请时间">{{row.applyDate}}</table-column>
<table-column label="当前节点">{{row.curTaskName}}</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" v-if="row.taskState === 10" @click="onApproval(row)">
<i class="fa fa-edit"></i>
<span>审核</span>
</div>
<div class="action-btn delete" v-if="row.canRevoke" @click="onRevoke(row)">
<i class="fa fa-reply"></i>
<span>撤回</span>
</div>
</template>
</table-list>
<info ref="infoRef">
<div v-if="showApprovalForm">
<div class="process-title">{{formData.taskName}}</div>
<van-form ref="formRef">
<van-field
v-model="formData.tf_opinion"
name="tf_opinion"
label="审批意见"
placeholder="请输入审批意见"
:rules="[{ required: true, message: '请填写审批意见' }]"
required
maxlength="100"
show-word-limit
></van-field>
</van-form>
<div style="display: flex; justify-content: space-between; column-gap: 10px; padding: 10px">
<van-button type="primary" block @click="handleTaskAction(1)">同意</van-button>
<van-button type="danger" block @click="handleTaskAction(20)">不同意</van-button>
<van-button type="danger" block @click="handleTaskAction(6)">退回</van-button>
</div>
</div>
</info>
</div>
<script nonce="${cspNonce!}">
<!--#include('../common/clubUserJoin.js'){}#-->
new Vue({
el: "#app",
store,
components: {
info: clubUserJoin
},
data() {
return {
pageForm: {
pageNumber: 1,
pageSize: 10,
totalCount: 0,
searchKeyword: '',
name: null,
approvalText: "0",
approval: false,
year: new Date().getFullYear(),
unionId: null,
},
unionOptions: [],
formData: {},
showApprovalForm: false
}
},
methods: {
async onReady() {
const unionList = await this.$businessTool.listUnion()
this.unionOptions = [
{
text: "全部工会",
value: null
}
].concat(unionList.map((v) => ({ text: v.name, value: v.id })))
if (this.unionOptions.length > 0) {
this.pageForm.unionId = this.unionOptions[0].value
this.doSearch()
}
},
onView(row) {
this.showApprovalForm = false
this.$refs.infoRef.onOpen(row)
},
onApproval(row) {
this.showApprovalForm = true
this.$refs.infoRef.onOpen(row)
this.formData = {
processTaskId: row.taskId,
taskName: row.curTaskName
}
},
async handleTaskAction(val) {
try {
await this.$refs.formRef.validate();
this.$dialog.confirm({
title: "提示",
message: "您确定要提交吗?"
}).then(() => {
this.$axios.post("/flow/common/executeTask", {
data: JSON.stringify({
...this.formData,
submitType: val
})
}).then((res) => {
if (res.code === 0) {
this.$refs.infoRef.onClose()
this.$toast.success(res.msg)
this.doSearch()
}
})
}).catch(() => {
})
} catch (error) {
}
},
onRevoke(row){
this.$dialog.confirm({
title: "提示",
message: "您确定要撤回吗?"
}).then(() => {
this.$axios.post("/flow/common/revokeTask", {taskId: row.taskId}).then((res) => {
if (res.code === 0) {
this.$toast.success(res.msg)
this.doSearch()
}
})
})
},
doSearch() {
this.$nextTick(() => {
this.pageForm.pageNumber = 1
this.pageForm.totalCount = 0
this.$refs.tableListRef.doSearch()
})
}
},
})
</script>
<!--#
}
#-->
@@ -0,0 +1,168 @@
<!--#
layout("/layouts/platform_h5.html"){
#-->
<div id="app">
<!-- 导航栏 -->
<van-nav-bar title="资产盘点" left-text="返回" left-arrow placeholder
@click-left="historyBack" fixed></van-nav-bar>
<van-sticky offset-top="46px">
<van-search
:reverse-color="false"
:show-action="false"
@search="doSearch"
input-align="left"
placeholder="请输入编号或名称搜索"
v-model="pageForm.searchKeyword"
></van-search>
<van-tabs v-model="pageForm.isStocktakingText"
@change="(val)=>{this.pageForm.isStocktaking = val==='1';this.doSearch();}">
<van-tab title="已盘点" name="1"></van-tab>
<van-tab title="未盘点" name="0"></van-tab>
</van-tabs>
</van-sticky>
<table-list api="/platform/asset/stocktaking/pageData" :page_form.sync="pageForm"
ref="tableListRef"
title="assetNumber" @ready="onReady">
<template v-slot="{index,row}">
<table-column label="资产名称">{{row.assetName}}</table-column>
<table-column label="类别名称"> {{row.assetTypeCode===1?'校工会资产':'分工会资产'}}</table-column>
<table-column label="存放地点">{{row.assetStorageLocation}}</table-column>
<table-column label="责任人">{{row.assetUseUserName}}</table-column>
<table-column label="折旧到期日期"> {{$moment(row.assetRetiredAssetsDate).format('YYYY-MM-DD')}}
</table-column>
<table-column label="使用状况">{{row.assetUsageStateName}}</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" v-if="!row.isStocktaking"
:disabled="stocktakingPlanOption.length==0
||!pageForm.assetStocktakingPlanId
||!stocktakingPlanOption.find(p=>p.id===pageForm.assetStocktakingPlanId).isShow"
@click="onAudit(row)">
<i class="fa fa-edit"></i>
<span>开始盘点</span>
</div>
<div class="action-btn delete" v-if="row.isStocktaking"
:disabled="stocktakingPlanOption.length==0
||!pageForm.assetStocktakingPlanId
||!stocktakingPlanOption.find(p=>p.id===pageForm.assetStocktakingPlanId).isShow"
@click="onRevoke(row)">
<i class="fa fa-undo"></i>
<span>删除盘点</span>
</div>
</template>
</table-list>
<h5-asset-info ref="h5AssetInfoRef"></h5-asset-info>
<h5-asset-stocktaking-form ref="h5AssetStocktakingFormRef" @save_success="doSearch"></h5-asset-stocktaking-form>
</div>
<script nonce="${cspNonce!}">
<!--#include("info.js"){}#-->
<!--#include("stocktakingForm.js"){}#-->
const vue = new Vue({
el: "#app",
store,
data() {
return {
assetCategoryOption: [],
stocktakingPlanOption: [],
unionList: [],
pageForm: {
pageNumber: 1,
pageSize: 10,
totalCount: 0,
isStocktakingText: "0",
isStocktaking: false,
}
}
},
components: {
"h5-asset-info": H5_ASSET_INFO,
"h5-asset-stocktaking-form": H5_ASSET_STOCKTAKING_FORM,
},
methods: {
onRevoke(row) {
this.$dialog.confirm({
title: "提示",
message: "您确定要删除本次盘点信息吗?"
}).then(() => {
this.$axios.post("/platform/asset/stocktaking/doDelete", {
id: row.id,
assetStocktakingPlanId: this.pageForm.assetStocktakingPlanId
}).then(resp => {
if (resp.code === 0) {
this.doSearch()
this.$toast.success(resp.msg)
} else {
this.$toast.fail(resp.msg)
}
})
})
},
onAudit(row) {
row.assetStocktakingPlanId = this.pageForm.assetStocktakingPlanId
this.$refs.h5AssetStocktakingFormRef.onOpen(row)
},
onView(row) {
this.$refs.h5AssetInfoRef.onOpen(row)
},
// 查询资产类别
queryCategory() {
this.$axios.post("/platform/asset/category/queryCategory").then(resp => {
if (resp.code === 0) {
this.assetCategoryOption = resp.data
}
})
},
// 按年度查询资产盘点计划
async queryStocktakingPlan() {
const resp = await this.$axios.post("/platform/asset/stocktakingPlan/queryStocktakingPlan")
if (resp.code === 0) {
resp.data.forEach(v => {
const startDate = this.$moment(v.startDate).format('YYYY-MM-DD')
const endDate = this.$moment(v.endDate).format('YYYY-MM-DD')
v.name = startDate + "至" + endDate + "(" + v.year + ")"
v.isShow = (this.$moment().format('YYYY-MM-DD hh:mm:ss') > v.startDate && this.$moment().format('YYYY-MM-DD hh:mm:ss') < v.endDate)
})
this.stocktakingPlanOption = resp.data
if (this.stocktakingPlanOption && this.stocktakingPlanOption.length > 0) {
this.$set(this.pageForm, "assetStocktakingPlanId", this.stocktakingPlanOption[0].id)
} else {
this.$set(this.pageForm, "assetStocktakingPlanId", null)
}
this.doSearch()
}
},
doSearch() {
this.$nextTick(() => {
this.pageForm.pageNumber = 1
this.pageForm.totalCount = 0
this.$refs.tableListRef.doSearch()
})
},
onReady() {
this.queryCategory()
this.queryStocktakingPlan()
}
},
created() {
}
})
</script>
<!--#
}
#-->
@@ -0,0 +1,124 @@
const H5_ASSET_INFO = {
template: /*language=HTML*/ `
<van-action-sheet v-model="visible" title="查看详情" ref="actionSheetRef">
<van-tabs v-model="activeName">
<van-tab title="资产基本信息" name="1">
</van-tab>
<van-tab title="盘点信息" name="2">
</van-tab>
</van-tabs>
<div class="detail-container" v-if="activeName==='1'">
<van-cell-group>
<van-cell title="资产编号">
{{ viewData.assetNumber }}
</van-cell>
<van-cell title="资产名称">
{{ viewData.assetName }}
</van-cell>
<van-cell title="资产类别">
{{viewData.assetCategoryName}}({{viewData.assetDepreciationYear}}年)
</van-cell>
<van-cell title="资产规格型号">
{{viewData.assetSpecs}}
</van-cell>
<van-cell title="供应商名称">
{{viewData.assetSupplierName}}
</van-cell>
<van-cell title="采购方式">
{{viewData.assetFundingSubjectName}}
</van-cell>
<van-cell title="单价(元)">
{{viewData.assetUnitPrice}}
</van-cell>
<van-cell title="数量(台/件)">
{{viewData.assetQuantity}}
</van-cell>
<van-cell title="资产类型">
{{viewData.assetTypeCode===1?'校工会资产':'分工会资产'}}
</van-cell>
<van-cell title="开始使用日期">
{{$moment(viewData.assetUsedDate).format('YYYY-MM-DD')}}
</van-cell>
<van-cell title="预计报废时间">
{{$moment(viewData.assetRetiredAssetsDate).format('YYYY-MM-DD')}}
</van-cell>
<van-cell title="责任人">
{{viewData.assetUseUserName}}
</van-cell>
<van-cell title="使用/管理部门">
{{viewData.assetUseUnionName}}
</van-cell>
<van-cell title="存放地点">
{{viewData.assetStorageLocation}}
</van-cell>
<van-cell title="使用状况">
{{viewData.assetUsageStateName}}
</van-cell>
<van-cell title="备注" class="direction-column-cell">
<div style="white-space: pre">{{viewData.assetNotes}}</div>
</van-cell>
</van-cell-group>
</div>
<div class="detail-container" v-else-if="activeName==='2'">
<template v-for="item in viewData.assetStocktakingList"
v-if="viewData.assetStocktakingList&&viewData.assetStocktakingList.length>0">
<van-cell-group :title="$moment(item.createdAt).format('YYYY-MM-DD HH:mm:ss')">
<van-cell title="原责任人">
{{ item.oldAssetUseUserName }}
</van-cell>
<van-cell title="原使用/管理部门">
{{ item.oldAssetUseUnionName }}
</van-cell>
<van-cell title="原存放地点">
{{ item.oldAssetStorageLocation }}
</van-cell>
<van-cell title="原使用状况">
{{ item.oldAssetUsageStateName }}
</van-cell>
<!-- <van-cell title="操作时间">
{{$moment(item.createdAt).format('YYYY-MM-DD HH:mm:ss')}}
</van-cell>-->
<van-cell title="旧备注" class="direction-column-cell">
<div style="white-space: pre">{{item.oldAssetNotes}}</div>
</van-cell>
<van-cell title="旧资产图片" class="direction-column-cell">
<file-preview :files="item.oldAssetFiles" complete_result></file-preview>
</van-cell>
</van-cell-group>
</template>
<template v-if="viewData.assetStocktakingList&&viewData.assetStocktakingList.length===0">
<van-empty description="暂无数据" ></van-empty>
</template>
</div>
</van-action-sheet>
`,
data() {
return {
visible: false,
viewData: {},
row: null,
activeName: "1",
actionSheetRef: null
}
},
methods: {
// 打开
onOpen(row) {
this.activeName = "1"
this.row = row
this.visible = true
this.getInfo()
},
// 获取申请信息
getInfo() {
this.$axios.post('/platform/asset/manage/fondOneAsset', {id: this.row.id}).then((res) => {
if (res.code === 0) {
this.viewData = res.data
}
})
},
}
}
@@ -0,0 +1,195 @@
let H5_ASSET_STOCKTAKING_FORM = {
template: /*language=HTML*/ `
<van-action-sheet v-model="visible" title="盘点信息">
<div>
<div class="form-container">
<van-form ref="formRef">
<van-cell-group title="资产信息">
<van-field label="资产编号"
:rules="[{ required: true }]"
v-model="formData.assetNumber"
readonly
required></van-field>
<van-field label="资产名称"
:rules="[{ required: true }]"
v-model="formData.assetName"
readonly
required></van-field>
<van-field label="开始使用日期"
:rules="[{ required: true }]"
v-model="$moment(formData.assetUsedDate).format('YYYY-MM-DD')"
readonly
required></van-field>
<van-field label="存放地点"
:rules="[{ required: true }]"
v-model="formData.assetStorageLocation"
required
name="assetStorageLocation"
placeholder="请输入存放地点"></van-field>
<van-field
v-model="formData.assetUseName"
name="assetUseName"
label="责任人"
required
readonly
:rules="[{ required: true }]"
is-link
placeholder="请输入工号或姓名选择责任人"
@click="assetUseNameShow=true"
></van-field>
<van-action-sheet v-model="assetUseNameShow" title="责任人" class="height100" :close-on-click-overlay="false">
<van-search
v-model="searchKeyword"
:show-action="false"
:reverse-color="false"
input-align="left"
placeholder="请输入工号或姓名选择责任人"
@input="(val) => {searchAssetUseUser(val)}"
@clear="(val) => {searchAssetUseUser(val)}"
shape="round"
></van-search>
<div class="van-action-sheet__content mt5"
v-if="assetUseUserOption && assetUseUserOption.length>0">
<template>
<template v-for="item in assetUseUserOption">
<van-button native-type="button" @click="onUserSelect(item)"
class="van-action-sheet__item van-hairline--bottom">
<span class="van-action-sheet__name">{{item.username}}({{item.loginname}})</span>
</van-button>
</template>
</template>
</div>
<van-empty description="暂无数据" v-else></van-empty>
</van-action-sheet>
<van-field
:rules="[{ required: true }]"
v-model="formData.assetUsageStateName"
label="使用状况"
name="assetUsageStateName"
placeholder="请选择使用状况"
required
is-link
readonly
@click="showAssetUsageStateNamePopup = true"
></van-field>
<van-popup v-model="showAssetUsageStateNamePopup" position="bottom">
<van-picker
show-toolbar
:columns="assetUsageStateNameOption.map(i => i.name)"
@confirm="onAssetUsageStateNameConfirm"
@cancel="showAssetUsageStateNamePopup = false"
></van-picker>
</van-popup>
<van-field label="备注"
v-model="formData.assetNotes"
type="textarea"
name="assetNotes"
rows="4"
autosize
maxlength="150"
class="more-text"
placeholder="请填写备注"></van-field>
</van-cell-group>
<van-cell-group title="附件">
<van-field class="more-text" name="assetFiles"
:rules="[{ required: true,message:'请上传附件' }]"
label=""
required>
<template #input>
<h5-file-upload
slot="input"
:value.sync="formData.assetFiles"
:upload_number="10"
upload_mode="file"
upload_result_category="array"
upload_result_type="url"
complete_result
></h5-file-upload>
</template>
</van-field>
</van-cell-group>
<!-- 按钮区域 -->
<div class="form-actions">
<van-button type="primary" @click="onSubmit">提交</van-button>
</div>
</van-form>
</div>
</div>
</van-action-sheet>
`,
dicts: ["ASSET_USAGE_STATE"],
data() {
return {
visible: false,
viewData: {},
row: null,
formData: {},
assetUsageStateNameOption:[],
showAssetUsageStateNamePopup:false,
assetUseUserOption:[],
assetUseNameShow:false,
searchKeyword:""
}
},
methods: {
// 打开
async onOpen(row) {
this.row = row
this.$set(this.formData, "assetId", row.id)
this.$set(this.formData, "assetStocktakingPlanId", row.assetStocktakingPlanId)
this.$set(this.formData, "assetNumber", row.assetNumber)
this.$set(this.formData, "assetName", row.assetName)
this.$set(this.formData, "assetUsedDate", row.assetUsedDate)
this.$set(this.formData, "assetStorageLocation", row.assetStorageLocation)
// 回显台账责任人;用户可通过责任人选择面板重新搜索并替换。
this.$set(this.formData, "assetUseUserId", row.assetUseUserId || null)
this.$set(this.formData, "assetUseName", row.assetUseUserName || null)
this.assetUsageStateNameOption = await this.$businessTool.getDictOptions("ASSET_USAGE_STATE")
this.visible = true
},
onAssetUsageStateNameConfirm(value,index){
this.formData.assetUsageStateName = this.assetUsageStateNameOption[index].name
this.showAssetUsageStateNamePopup=false
},
onUserSelect(o){
this.$set(this.formData, "assetUseUserId", o.id)
this.$set(this.formData, "assetUseName", o.username)
this.assetUseNameShow = false
},
searchAssetUseUser(val) {
$.get("/platform/asset/manage/searchAssetUseUser", {query: val}).then(resp => {
if (resp.code === 0) {
this.assetUseUserOption = resp.data
}
})
},
onSubmit() {
this.$refs.formRef.validate().then(() => {
this.$dialog.confirm({
title: "提示",
message: "您确定要提交吗?"
}).then(() => {
let formData = clone(this.formData)
if (formData.assetFiles) {
formData.assetFiles = JSON.stringify(formData.assetFiles)
}
this.$axios.post("/platform/asset/stocktaking/doSubmit", formData).then(res => {
if (res.code === 0) {
this.$toast(res.msg)
this.visible = false
this.$emit("save_success")
}
})
})
})
},
}
}
@@ -0,0 +1,351 @@
<!--#
layout("/layouts/platform_h5.html"){
#-->
<style>
.course-cover {
width: 100%;
height: 200px;
background: linear-gradient(45deg, #f0f2f5, #e9ecef);
display: flex;
align-items: center;
justify-content: center;
}
.course-cover .van-image {
width: 100%;
height: 100%;
}
.course-info {
padding: 16px;
overflow: hidden;
}
.course-info .van-cell-group {
padding: 20px;
background: linear-gradient(135deg, #fff 0%, #f8f9fa 100%);
}
.course-title {
font-size: 20px;
font-weight: 700;
color: #1a1a1a;
margin-bottom: 12px;
line-height: 1.3;
letter-spacing: 0.5px;
}
.course-desc {
font-size: 15px;
color: #555;
line-height: 1.7;
margin-bottom: 16px;
text-align: justify;
}
.course-meta {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 13px;
padding-top: 16px;
border-top: 1px solid #f0f0f0;
margin-top: 4px;
}
.course-date {
color: #888;
font-weight: 500;
}
.progress-section {
margin-bottom: 12px;
}
.progress-container {
display: flex;
align-items: center;
width: 100%;
}
.progress-container .van-progress {
flex: 1;
margin-right: 12px;
}
.progress-text {
font-size: 14px;
color: #333;
min-width: 40px;
}
.chapters-section {
margin-bottom: 16px;
}
.chapter-card {
background: #fff;
border: 1px solid #f0f0f0;
margin-bottom: 16px;
overflow: hidden;
}
.chapter-header {
padding: 16px;
background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
border-bottom: 1px solid #f0f0f0;
}
.chapter-title {
font-size: 16px;
font-weight: 600;
color: #333;
margin: 0;
}
.chapter-videos {
padding: 0;
}
.video-item {
display: flex;
align-items: center;
padding: 12px 16px;
border-bottom: 1px solid #f8f9fa;
cursor: pointer;
transition: background-color 0.2s;
}
.video-item:hover {
background-color: #f8f9fa;
}
.video-item:last-child {
border-bottom: none;
}
.video-icon {
margin-right: 12px;
flex-shrink: 0;
}
.video-content {
flex: 1;
min-width: 0;
}
.video-title {
font-size: 14px;
color: #333;
margin-bottom: 4px;
font-weight: 500;
}
.video-duration {
font-size: 12px;
color: #999;
}
.video-status {
margin-left: 8px;
flex-shrink: 0;
}
</style>
<div id="app" v-cloak>
<van-nav-bar @click-left="historyBack" left-arrow left-text="返回" title="课程详情" placeholder fixed></van-nav-bar>
<div v-if="loading" class="loading-container">
<van-loading type="spinner" size="24px">加载中...</van-loading>
</div>
<div v-else-if="course">
<!-- 课程封面 -->
<div class="course-cover">
<van-image v-if="course.cover" :src="course.cover" fit="cover" :alt="course.title">
<template v-slot:error>
<van-icon name="photo-fail" size="48" color="#ddd"></van-icon>
</template>
</van-image>
<van-icon v-else name="graduation" size="48" color="#ddd"></van-icon>
</div>
<!-- 课程信息 -->
<van-cell-group class="course-info">
<div class="course-title">{{ course.title || '未命名课程' }}</div>
<div class="course-desc">{{ course.description || '暂无描述' }}</div>
<div class="course-meta">
<van-tag type="primary" size="mini">{{ course.category || '未分类' }}</van-tag>
<span class="course-date">{{ formatDate(course.createdAt) }}</span>
</div>
</van-cell-group>
<!-- 学习进度 -->
<van-cell-group title="学习进度" class="progress-section">
<van-cell>
<template #default>
<div class="progress-container">
<van-progress :percentage="progressPercentage" stroke-width="6"></van-progress>
<span class="progress-text">{{ progressPercentage }}%</span>
</div>
</template>
</van-cell>
</van-cell-group>
<!-- 课程章节 -->
<div class="chapters-section">
<h3 style="margin: 0 0 16px 0; padding-left:16px;font-size: 16px; font-weight: 600; color: #333;">课程章节</h3>
<div v-if="course.chapters && course.chapters.length">
<div
v-for="(chapter, index) in course.chapters"
v-if="chapter.videos && chapter.videos.length"
:key="chapter.id"
class="chapter-card"
>
<div class="chapter-header">
<h4 class="chapter-title">{{ chapter.title || '第' + (index + 1) + '章' }}</h4>
</div>
<div class="chapter-videos">
<div
v-for="video in chapter.videos"
:key="video.id"
class="video-item"
@click="playVideo(video.id, video.title)"
>
<div class="video-icon">
<van-icon
:name="isVideoCompleted(video.id) ? 'success' : 'play-circle-o'"
:color="isVideoCompleted(video.id) ? '#07c160' : '#1989fa'"
size="20"
></van-icon>
</div>
<div class="video-content">
<div class="video-title">{{ video.title || '未命名视频' }}</div>
<div class="video-duration">{{ formatDuration(video.duration) }}</div>
</div>
<div class="video-status" v-if="isVideoCompleted(video.id)">
<van-tag type="success" size="mini">已完成</van-tag>
</div>
</div>
</div>
</div>
</div>
<van-empty v-else description="暂无章节内容" style="padding: 40px 20px;"></van-empty>
</div>
</div>
<van-empty v-else description="课程不存在"></van-empty>
</div>
<script nonce="${cspNonce!}">
const vue = new Vue({
el: "#app",
store,
data() {
return {
course: null,
loading: true,
courseId: null,
activeChapters: [],
progressData: null
}
},
computed: {
progressPercentage() {
if (!this.progressData || !this.progressData.totalVideos) return 0;
return Math.round(this.progressData.completedVideos / this.progressData.totalVideos * 100);
}
},
methods: {
historyBack,
// 加载课程详情
async loadCourseDetail() {
if (!this.courseId) {
this.$toast.fail('缺少课程ID参数');
this.loading = false;
return;
}
try {
const {code, data, msg} = await $.get('/platform/h5/edu/course/detail/data', {
courseId: this.courseId
});
if (code === 0 && data) {
this.course = data;
this.loadCourseProgress();
} else {
this.$toast.fail(msg || '加载课程详情失败');
}
} catch (error) {
this.$toast.fail('网络错误');
}
this.loading = false;
},
// 加载课程进度
async loadCourseProgress() {
try {
const {code, data} = await $.get('/platform/h5/edu/course/progress', {
courseId: this.courseId
});
if (code === 0 && data) {
this.progressData = data;
}
} catch (error) {
console.error('加载进度失败:', error);
}
},
// 检查视频是否已完成
isVideoCompleted(videoId) {
return this.progressData &&
this.progressData.completedVideoIds &&
this.progressData.completedVideoIds.includes(videoId);
},
// 播放视频
playVideo(videoId, videoTitle) {
pjaxReplace('/platform/h5/edu/video/play?videoId=' + videoId + '&courseId=' + this.courseId + '&title=' + encodeURIComponent(videoTitle || ''));
},
// 格式化日期
formatDate(dateStr) {
if (!dateStr) return '';
const date = new Date(dateStr);
return date.getFullYear() + '-' +
String(date.getMonth() + 1).padStart(2, '0') + '-' +
String(date.getDate()).padStart(2, '0');
},
// 格式化时长
formatDuration(seconds) {
if (!seconds) return '00:00';
const minutes = Math.floor(seconds / 60);
const remainingSeconds = seconds % 60;
return String(minutes).padStart(2, '0') + ':' + String(remainingSeconds).padStart(2, '0');
}
},
mounted() {
// 获取URL参数
const urlParams = new URLSearchParams(window.location.search);
this.courseId = urlParams.get('courseId');
this.loadCourseDetail();
}
});
</script>
<!--#
}
#-->
@@ -0,0 +1,230 @@
<!--#
layout("/layouts/platform_h5.html"){
#-->
<style>
.course-item {
background: rgba(255, 255, 255, 0.95);
border-radius: 6px;
margin: 12px 16px;
overflow: hidden;
display: flex;
align-items: stretch;
position: relative;
}
.course-cover {
width: 100px;
height: 100px;
border-radius: 16px;
overflow: hidden;
margin: 16px;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
position: relative;
}
.course-cover::after {
content: '';
position: absolute;
inset: 2px;
border-radius: 14px;
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(5px);
}
.course-cover .van-image {
width: 100%;
height: 100%;
border-radius: 14px;
z-index: 1;
position: relative;
}
.course-cover .van-icon {
z-index: 2;
position: relative;
color: rgba(255, 255, 255, 0.9);
filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.2));
}
.course-info {
flex: 1;
min-width: 0;
padding: 16px 16px 16px 0;
display: flex;
flex-direction: column;
justify-content: center;
}
.course-title {
font-size: 18px;
font-weight: 700;
color: #2c3e50;
margin-bottom: 8px;
line-height: 1.3;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
letter-spacing: -0.02em;
}
.course-desc {
font-size: 14px;
color: #64748b;
line-height: 1.5;
margin-bottom: 12px;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
font-weight: 400;
}
.course-meta {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 12px;
}
.course-date {
color: #94a3b8;
font-weight: 500;
font-size: 11px;
}
</style>
<div id="app" v-cloak>
<van-nav-bar @click-left="historyBack" left-arrow left-text="返回" title="教育培训课程" placeholder fixed>
<template #right>
<van-icon name="clock-o" @click="goToHistory"></van-icon>
</template>
</van-nav-bar>
<van-sticky offset-top="46px">
<van-tabs v-model="pageForm.category" @change="doSearch">
<van-tab name="" title="全部"></van-tab>
<van-tab v-for="item in dict.type.TRAIN_EDU_COURSE_TYPE"
:name="item.code"
:title="item.name"
:key="item.id"
></van-tab>
</van-tabs>
</van-sticky>
<van-pull-refresh v-model="tableRefreshing" @refresh="onRefresh">
<van-list v-model="tableLoading" :finished="tableFinished" finished-text="没有更多了" @load="onLoad">
<div class="course-item" v-for="(course, index) in tableData" :key="course.id"
@click="viewCourse(course.id)">
<div class="course-cover">
<van-image v-if="course.cover" :src="course.cover" fit="cover" :alt="course.title">
<template v-slot:error>
<van-icon name="photo-fail" size="32" color="#ddd"></van-icon>
</template>
</van-image>
<van-icon v-else name="play-circle-o" size="48" color="#ddd"></van-icon>
</div>
<div class="course-info">
<div class="course-title">{{ course.title || '未命名课程' }}</div>
<div class="course-desc">{{ course.description || '暂无描述' }}</div>
<div class="course-meta">
<van-tag type="primary" size="mini">{{ course.category || '未分类' }}</van-tag>
<span class="course-date">{{ formatDate(course.createdAt) }}</span>
</div>
</div>
</div>
</van-list>
</van-pull-refresh>
<van-empty v-if="!tableLoading && !tableData.length" description="暂无课程"></van-empty>
</div>
<script nonce="${cspNonce!}">
const vue = new Vue({
el: "#app",
store,
dicts:['TRAIN_EDU_COURSE_TYPE'],
data() {
return {
pageForm: {
pageNumber: 1,
pageSize: 10,
category: ""
},
tableData: [],
tableLoading: false,
tableFinished: false,
tableRefreshing: false
}
},
methods: {
historyBack,
// 刷新
onRefresh() {
this.tableFinished = false;
this.tableLoading = true
this.pageForm.pageNumber = 1;
this.onLoad();
},
// 加载更多
async onLoad() {
if(this.tableRefreshing){
this.tableData = []
this.tableRefreshing = false
}
this.$axios.post('/platform/h5/edu/courses/list', this.pageForm).then((res) => {
if (res.code === 0) {
this.tableData = this.tableData.concat(res.data.list)
this.pageForm.totalCount = res.data.totalCount
if (this.tableData.length >= this.pageForm.totalCount) {
this.tableFinished = true
}
this.pageForm.pageNumber++;
}
}).finally(() => {
this.tableLoading = false
this.tableRefreshing = false
})
},
// 搜索
doSearch() {
this.pageForm.pageNumber = 1;
this.tableData = [];
this.tableFinished = false;
this.tableLoading = true;
this.onLoad();
},
// 查看课程详情
viewCourse(courseId) {
pjaxReplace(`/platform/h5/edu/course/detail?courseId=` + courseId);
},
// 前往学习历史
goToHistory() {
pjaxReplace('/platform/h5/edu/studyhis/');
},
// 格式化日期
formatDate(dateStr) {
if (!dateStr) return '';
const date = new Date(dateStr);
return date.getFullYear() + '-' +
String(date.getMonth() + 1).padStart(2, '0') + '-' +
String(date.getDate()).padStart(2, '0');
}
}
});
</script>
<!--#
}
#-->
@@ -0,0 +1,410 @@
<!--#
layout("/layouts/platform_h5.html"){
#-->
<style>
.history-container {
background-color: #f5f5f5;
min-height: 100vh;
}
.stats-card {
background: white;
border-radius: 12px;
padding: 20px;
margin: 15px;
margin-bottom: 10px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.stats-title {
font-size: 16px;
font-weight: 600;
color: #333;
margin-bottom: 15px;
display: flex;
align-items: center;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 15px;
}
.stat-item {
text-align: center;
}
.stat-number {
font-size: 24px;
font-weight: 700;
color: #1989fa;
margin-bottom: 5px;
}
.stat-label {
font-size: 12px;
color: #666;
}
.history-item {
background: white;
border-radius: 12px;
margin: 15px;
margin-bottom: 10px;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.history-header {
padding: 15px 20px;
border-bottom: 1px solid #f8f9fa;
display: flex;
align-items: center;
justify-content: space-between;
}
.course-info {
flex: 1;
}
.course-title {
font-size: 16px;
font-weight: 600;
color: #333;
margin-bottom: 5px;
}
.course-meta {
font-size: 12px;
color: #999;
display: flex;
align-items: center;
}
.course-category {
background: #e3f2fd;
color: #1976d2;
padding: 2px 6px;
border-radius: 8px;
margin-right: 10px;
}
.progress-info {
text-align: right;
min-width: 80px;
}
.progress-text {
font-size: 14px;
font-weight: 600;
color: #28a745;
margin-bottom: 5px;
}
.progress-bar {
width: 60px;
height: 4px;
background: #e9ecef;
border-radius: 2px;
overflow: hidden;
margin-left: auto;
}
.progress-fill {
height: 100%;
background: linear-gradient(90deg, #28a745, #20c997);
border-radius: 2px;
}
.video-list {
padding: 0 20px 15px;
}
.video-item {
display: flex;
align-items: center;
padding: 10px 0;
border-bottom: 1px solid #f8f9fa;
cursor: pointer;
}
.video-item:last-child {
border-bottom: none;
}
.video-icon {
width: 36px;
height: 36px;
background: linear-gradient(135deg, #1989fa, #1976d2);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
color: white;
margin-right: 12px;
font-size: 14px;
}
.video-icon.completed {
background: linear-gradient(135deg, #28a745, #20c997);
}
.video-info {
flex: 1;
}
.video-title {
font-size: 14px;
color: #333;
margin-bottom: 4px;
line-height: 1.4;
}
.video-meta {
font-size: 12px;
color: #999;
display: flex;
align-items: center;
}
.video-duration {
margin-right: 15px;
}
.video-progress {
color: #28a745;
}
.watch-time {
font-size: 12px;
color: #666;
text-align: right;
}
</style>
<div id="app" class="history-container">
<van-nav-bar
title="学习历史"
left-text="返回"
left-arrow
@click-left="onClickLeft"
></van-nav-bar>
<van-tabs v-model="activeTab" @change="onTabChange">
<van-tab title="全部" name="all"></van-tab>
<van-tab title="已完成" name="completed"></van-tab>
<van-tab title="学习中" name="in_progress"></van-tab>
<van-tab title="最近观看" name="recent"></van-tab>
</van-tabs>
<!-- 学习统计 -->
<div v-if="showStats && stats" class="stats-card">
<div class="stats-title">
<van-icon name="chart-trending-o" style="margin-right: 8px; color: #1989fa;"/>
学习统计
</div>
<div class="stats-grid">
<div class="stat-item">
<div class="stat-number">{{ stats.totalCourses || 0 }}</div>
<div class="stat-label">学习课程</div>
</div>
<div class="stat-item">
<div class="stat-number">{{ stats.completedVideos || 0 }}</div>
<div class="stat-label">完成视频</div>
</div>
<div class="stat-item">
<div class="stat-number">{{ Math.round((stats.totalWatchTime || 0) / 60) }}</div>
<div class="stat-label">学习时长(分)</div>
</div>
</div>
</div>
<!-- 学习历史列表 -->
<van-loading v-if="loading" type="spinner" color="#1989fa" style="margin: 40px auto; display: block;">
加载中...
</van-loading>
<van-empty v-else-if="historyList.length === 0" description="暂无学习记录">
<van-button round type="primary" @click="loadData">刷新</van-button>
</van-empty>
<div v-else>
<div v-for="course in historyList" :key="course.courseId" class="history-item">
<div class="history-header" @click="viewCourse(course.courseId)">
<div class="course-info">
<div class="course-title">{{ course.courseTitle || '未命名课程' }}</div>
<div class="course-meta">
<span class="course-category">{{ course.courseCategory || '未分类' }}</span>
<span>最后学习:{{ formatDate(course.lastWatchTime) }}</span>
</div>
</div>
<div class="progress-info">
<div class="progress-text">{{ getProgressPercent(course) }}%</div>
<div class="progress-bar">
<div class="progress-fill" :style="{ width: getProgressPercent(course) + '%' }"></div>
</div>
</div>
</div>
<div v-if="course.videos && course.videos.length > 0" class="video-list">
<div
v-for="video in course.videos"
:key="video.videoId"
class="video-item"
@click="playVideo(video.videoId, course.courseId, video.videoTitle)"
>
<div class="video-icon" :class="{ completed: video.isCompleted }">
<van-icon :name="video.isCompleted ? 'success' : 'play-circle-o'"/>
</div>
<div class="video-info">
<div class="video-title">{{ video.videoTitle || '未命名视频' }}</div>
<div class="video-meta">
<span class="video-duration">{{ formatDuration(video.duration) }}</span>
<span class="video-progress">
{{ getVideoProgress(video) }}
</span>
</div>
</div>
<div class="watch-time">
{{ formatDate(video.lastWatchTime) }}
</div>
</div>
</div>
</div>
</div>
</div>
<script nonce="${cspNonce!}">
new Vue({
el: '#app',
data() {
return {
activeTab: 'all',
loading: false,
showStats: false,
stats: null,
historyList: []
};
},
mounted() {
this.loadData();
},
methods: {
onClickLeft() {
window.location.href = '/platform/h5/edu/courses';
},
onTabChange(name) {
this.activeTab = name;
this.loadData();
},
async loadData() {
this.loading = true;
try {
// 如果是全部标签,先加载统计信息
if (this.activeTab === 'all') {
await this.loadStats();
this.showStats = true;
} else {
this.showStats = false;
}
// 加载学习历史
await this.loadHistory();
} catch (error) {
this.$toast('加载失败,请重试');
} finally {
this.loading = false;
}
},
async loadStats() {
try {
const response = await this.$axios.post('/platform/h5/edu/study/stats');
if (response.data.code === 0) {
this.stats = response.data.data;
}
} catch (error) {
console.error('加载统计失败:', error);
}
},
async loadHistory() {
try {
const response = await this.$axios.post('/platform/h5/edu/study/history', {
params: {filter: this.activeTab}
});
if (response.data.code === 0) {
this.historyList = response.data.data || [];
} else {
this.$toast(response.data.msg || '加载失败');
}
} catch (error) {
this.$toast('网络错误,请检查网络连接');
}
},
viewCourse(courseId) {
window.location.href = `/platform/h5/edu/course/detail?courseId=` + courseId;
},
playVideo(videoId, courseId, videoTitle) {
pjaxReplace('/platform/h5/edu/video/play?videoId=' + videoId + '&courseId=' + courseId + '&title=' + encodeURIComponent(videoTitle))
},
getProgressPercent(course) {
if (!course.videos || course.videos.length === 0) return 0;
const completedCount = course.videos.filter(v => v.isCompleted).length;
return Math.round((completedCount / course.videos.length) * 100);
},
getVideoProgress(video) {
if (video.isCompleted) return '已完成';
if (video.duration > 0 && video.watchDuration > 0) {
const percent = Math.round((video.watchDuration / video.duration) * 100);
return `观看` + percent + '%';
}
return '未开始';
},
formatDate(dateStr) {
if (!dateStr) return '未知';
const date = new Date(dateStr);
const now = new Date();
const diffTime = now - date;
const diffDays = Math.floor(diffTime / (1000 * 60 * 60 * 24));
if (diffDays === 0) {
const diffHours = Math.floor(diffTime / (1000 * 60 * 60));
if (diffHours === 0) {
const diffMinutes = Math.floor(diffTime / (1000 * 60));
return diffMinutes <= 0 ? '刚刚' : diffMinutes + `分钟前`;
}
return diffHours + `小时前`;
} else if (diffDays === 1) {
return '昨天';
} else if (diffDays < 7) {
return diffDays + `天前`;
} else {
return date.getFullYear() + '-' +
String(date.getMonth() + 1).padStart(2, '0') + '-' +
String(date.getDate()).padStart(2, '0');
}
},
formatDuration(seconds) {
if (!seconds) return '00:00';
const minutes = Math.floor(seconds / 60);
const remainingSeconds = seconds % 60;
return String(minutes).padStart(2, '0') + ':' + String(remainingSeconds).padStart(2, '0');
}
}
});
</script>
<!--#
}
#-->
@@ -0,0 +1,547 @@
<!--#
layout("/layouts/platform_h5.html"){
#-->
<style>
.study-his-container {
background-color: #f5f5f5;
min-height: 100vh;
}
.stats-overview {
background: linear-gradient(135deg, #1989fa, #1976d2);
padding: 20px 15px;
color: white;
}
.stats-title {
font-size: 18px;
font-weight: 600;
margin-bottom: 20px;
text-align: center;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 20px;
}
.stat-item {
text-align: center;
background: rgba(255, 255, 255, 0.1);
border-radius: 12px;
padding: 15px 10px;
}
.stat-number {
font-size: 28px;
font-weight: 700;
margin-bottom: 5px;
color: #fff;
}
.stat-label {
font-size: 13px;
color: rgba(255, 255, 255, 0.8);
}
.filter-tabs {
background: white;
padding: 0 15px;
border-bottom: 1px solid #f0f0f0;
}
.tab-item {
display: inline-block;
padding: 15px 20px;
font-size: 14px;
color: #666;
cursor: pointer;
border-bottom: 2px solid transparent;
margin-right: 20px;
}
.tab-item.active {
color: #1989fa;
border-bottom-color: #1989fa;
font-weight: 600;
}
.history-list {
padding: 15px;
}
.history-card {
background: white;
border-radius: 12px;
margin-bottom: 15px;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
}
.course-header {
padding: 15px 20px;
border-bottom: 1px solid #f8f9fa;
display: flex;
align-items: center;
justify-content: space-between;
cursor: pointer;
}
.course-info {
flex: 1;
}
.course-title {
font-size: 16px;
font-weight: 600;
color: #333;
margin-bottom: 8px;
line-height: 1.4;
}
.course-meta {
font-size: 12px;
color: #999;
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 10px;
}
.course-category {
background: #e3f2fd;
color: #1976d2;
padding: 3px 8px;
border-radius: 10px;
font-size: 11px;
}
.last-study-time {
color: #666;
}
.progress-section {
text-align: right;
min-width: 100px;
}
.progress-percent {
font-size: 16px;
font-weight: 600;
color: #28a745;
margin-bottom: 8px;
}
.progress-bar {
width: 80px;
height: 6px;
background: #e9ecef;
border-radius: 3px;
overflow: hidden;
margin-left: auto;
}
.progress-fill {
height: 100%;
background: linear-gradient(90deg, #28a745, #20c997);
border-radius: 3px;
transition: width 0.3s ease;
}
.video-list {
max-height: 0;
overflow: hidden;
transition: max-height 0.3s ease;
}
.video-list.expanded {
max-height: 1000px;
padding: 0 20px 15px;
}
.video-item {
display: flex;
align-items: center;
padding: 12px 0;
border-bottom: 1px solid #f8f9fa;
cursor: pointer;
transition: background-color 0.2s ease;
}
.video-item:hover {
background-color: #f8f9fa;
margin: 0 -10px;
padding-left: 10px;
padding-right: 10px;
border-radius: 8px;
}
.video-item:last-child {
border-bottom: none;
}
.video-icon {
width: 40px;
height: 40px;
background: linear-gradient(135deg, #1989fa, #1976d2);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
color: white;
margin-right: 15px;
font-size: 16px;
flex-shrink: 0;
}
.video-icon.completed {
background: linear-gradient(135deg, #28a745, #20c997);
}
.video-icon.in-progress {
background: linear-gradient(135deg, #ffc107, #ff9800);
}
.video-content {
flex: 1;
min-width: 0;
}
.video-title {
font-size: 14px;
color: #333;
margin-bottom: 6px;
line-height: 1.4;
font-weight: 500;
}
.video-meta {
font-size: 12px;
color: #999;
display: flex;
align-items: center;
gap: 15px;
}
.video-duration {
color: #666;
}
.video-progress {
color: #28a745;
font-weight: 500;
}
.video-time {
font-size: 11px;
color: #999;
text-align: right;
min-width: 60px;
flex-shrink: 0;
}
.expand-btn {
color: #1989fa;
font-size: 12px;
margin-left: 10px;
transition: transform 0.3s ease;
}
.expand-btn.expanded {
transform: rotate(180deg);
}
.empty-state {
text-align: center;
padding: 60px 20px;
color: #999;
}
.empty-icon {
font-size: 48px;
color: #ddd;
margin-bottom: 15px;
}
.empty-text {
font-size: 14px;
margin-bottom: 20px;
}
.loading-container {
display: flex;
justify-content: center;
align-items: center;
padding: 40px;
}
</style>
<div id="app" class="study-his-container">
<van-nav-bar
title="学习统计"
left-text="返回"
left-arrow
@click-left="onClickLeft"
></van-nav-bar>
<!-- 学习统计概览 -->
<div v-if="stats" class="stats-overview">
<div class="stats-title">我的学习成果</div>
<div class="stats-grid">
<div class="stat-item">
<div class="stat-number">{{ stats.totalCourses || 0 }}</div>
<div class="stat-label">学习课程</div>
</div>
<div class="stat-item">
<div class="stat-number">{{ stats.completedVideos || 0 }}</div>
<div class="stat-label">完成视频</div>
</div>
<div class="stat-item">
<div class="stat-number">{{ Math.round((stats.totalWatchTime || 0) / 60) }}</div>
<div class="stat-label">学习时长(分)</div>
</div>
</div>
</div>
<!-- 筛选标签 -->
<div class="filter-tabs">
<div
v-for="tab in tabs"
:key="tab.key"
class="tab-item"
:class="{ active: activeTab === tab.key }"
@click="switchTab(tab.key)"
>
{{ tab.label }}
</div>
</div>
<!-- 加载状态 -->
<div v-if="loading" class="loading-container">
<van-loading type="spinner" color="#1989fa" size="24px">加载中...</van-loading>
</div>
<!-- 学习历史列表 -->
<div v-else-if="historyList.length > 0" class="history-list">
<div v-for="course in historyList" :key="course.courseId" class="history-card">
<div class="course-header" @click="toggleCourseExpand(course.courseId)">
<div class="course-info">
<div class="course-title">{{ course.courseTitle || '未命名课程' }}</div>
<div class="course-meta">
<span class="course-category">{{ course.courseCategory || '未分类' }}</span>
<span class="last-study-time">{{ formatDate(course.lastWatchTime) }}</span>
</div>
</div>
<div class="progress-section">
<div class="progress-percent">{{ getProgressPercent(course) }}%</div>
<div class="progress-bar">
<div class="progress-fill" :style="{ width: getProgressPercent(course) + '%' }"></div>
</div>
</div>
<van-icon
name="arrow-down"
class="expand-btn"
:class="{ expanded: expandedCourses.includes(course.courseId) }"
></van-icon>
</div>
<div
class="video-list"
:class="{ expanded: expandedCourses.includes(course.courseId) }"
>
<div
v-for="video in course.videos"
:key="video.videoId"
class="video-item"
@click="playVideo(video.videoId, course.courseId, video.videoTitle)"
>
<div
class="video-icon"
:class="getVideoIconClass(video)"
>
<van-icon :name="getVideoIconName(video)" ></van-icon>
</div>
<div class="video-content">
<div class="video-title">{{ video.videoTitle || '未命名视频' }}</div>
<div class="video-meta">
<span class="video-duration">{{ formatDuration(video.duration) }}</span>
<span class="video-progress">{{ getVideoProgressText(video) }}</span>
</div>
</div>
<div class="video-time">
{{ formatDate(video.lastWatchTime) }}
</div>
</div>
</div>
</div>
</div>
<!-- 空状态 -->
<div v-else class="empty-state">
<div class="empty-icon">📚</div>
<div class="empty-text">暂无学习记录</div>
<van-button round type="primary" size="small" @click="goToCourses">
开始学习
</van-button>
</div>
</div>
<script nonce="${cspNonce!}">
new Vue({
el: '#app',
data() {
return {
loading: false,
stats: null,
activeTab: 'all',
tabs: [
{ key: 'all', label: '全部' },
{ key: 'completed', label: '已完成' },
{ key: 'in_progress', label: '学习中' },
{ key: 'recent', label: '最近' }
],
historyList: [],
expandedCourses: []
};
},
mounted() {
this.loadData();
},
methods: {
onClickLeft() {
window.location.href = '/platform/h5/edu/courses';
},
async loadData() {
this.loading = true;
try {
await Promise.all([
this.loadStats(),
this.loadHistory()
]);
} catch (error) {
this.$toast('加载失败,请重试');
} finally {
this.loading = false;
}
},
async loadStats() {
try {
const response = await this.$axios.post('/platform/h5/edu/studyhis/stats');
if (response.data.code === 0) {
this.stats = response.data.data;
}
} catch (error) {
console.error('加载统计失败:', error);
}
},
async loadHistory() {
try {
const response = await this.$axios.post('/platform/h5/edu/studyhis/history', {
filter: this.activeTab
});
if (response.data.code === 0) {
this.historyList = response.data.data || [];
} else {
this.$toast(response.data.msg || '加载失败');
}
} catch (error) {
this.$toast('网络错误,请检查网络连接');
}
},
switchTab(tabKey) {
if (this.activeTab === tabKey) return;
this.activeTab = tabKey;
this.expandedCourses = [];
this.loadData();
},
toggleCourseExpand(courseId) {
const index = this.expandedCourses.indexOf(courseId);
if (index > -1) {
this.expandedCourses.splice(index, 1);
} else {
this.expandedCourses.push(courseId);
}
},
playVideo(videoId, courseId, videoTitle) {
const url = '/platform/h5/edu/video/play?videoId=' + videoId +
'&courseId=' + courseId +
'&title=' + encodeURIComponent(videoTitle || '');
window.location.href = url;
},
goToCourses() {
window.location.href = '/platform/h5/edu/courses';
},
getProgressPercent(course) {
if (!course.videos || course.videos.length === 0) return 0;
const completedCount = course.videos.filter(v => v.isCompleted).length;
return Math.round((completedCount / course.videos.length) * 100);
},
getVideoIconClass(video) {
if (video.isCompleted) return 'completed';
if (video.watchDuration > 0) return 'in-progress';
return '';
},
getVideoIconName(video) {
if (video.isCompleted) return 'success';
if (video.watchDuration > 0) return 'pause-circle-o';
return 'play-circle-o';
},
getVideoProgressText(video) {
if (video.isCompleted) return '已完成';
if (video.duration > 0 && video.watchDuration > 0) {
const percent = Math.round((video.watchDuration / video.duration) * 100);
return '观看' + percent + '%';
}
return '未开始';
},
formatDate(dateStr) {
if (!dateStr) return '未知';
const date = new Date(dateStr);
const now = new Date();
const diffTime = now - date;
const diffDays = Math.floor(diffTime / (1000 * 60 * 60 * 24));
if (diffDays === 0) {
const diffHours = Math.floor(diffTime / (1000 * 60 * 60));
if (diffHours === 0) {
const diffMinutes = Math.floor(diffTime / (1000 * 60));
return diffMinutes <= 0 ? '刚刚' : diffMinutes + '分钟前';
}
return diffHours + '小时前';
} else if (diffDays === 1) {
return '昨天';
} else if (diffDays < 7) {
return diffDays + '天前';
} else {
return (date.getMonth() + 1) + '-' + date.getDate();
}
},
formatDuration(seconds) {
if (!seconds) return '00:00';
const minutes = Math.floor(seconds / 60);
const remainingSeconds = seconds % 60;
return String(minutes).padStart(2, '0') + ':' + String(remainingSeconds).padStart(2, '0');
}
}
});
</script>
<!--#
}
#-->
@@ -0,0 +1,305 @@
<!--#
layout("/layouts/platform_h5.html"){
#-->
<style>
.video-container {
background: #000;
min-height: calc(100vh - 46px);
display: flex;
align-items: center;
justify-content: center;
}
.loading-container {
display: flex;
justify-content: center;
align-items: center;
height: 300px;
color: white;
}
.video-player {
width: 100%;
height: 100%;
}
.video-element {
width: 100%;
height: auto;
max-height: calc(100vh - 46px);
object-fit: contain;
}
</style>
<div id="app" v-cloak>
<van-nav-bar @click-left="pageBack" left-arrow left-text="返回" :title="videoTitle" placeholder fixed></van-nav-bar>
<div class="video-container">
<div v-if="loading" class="loading-container">
<van-loading type="spinner" size="24px">加载中...</van-loading>
</div>
<div v-else-if="videoInfo" class="video-player">
<video
ref="videoElement"
class="video-element"
:src="videoSrc"
controls
preload="metadata"
@loadedmetadata="onVideoLoaded"
@canplay="onVideoCanPlay"
@play="onVideoPlay"
@pause="onVideoPause"
@timeupdate="onTimeUpdate"
@ended="onVideoEnded"
@error="onVideoError"
>
您的浏览器不支持视频播放
</video>
</div>
<van-empty v-else description="视频加载失败"></van-empty>
</div>
<!-- 进度保存提示 -->
<van-toast v-model="showProgressToast" message="进度已保存" :duration="1000"/>
<!-- 完成学习弹窗 -->
<van-dialog
v-model="showCompletionDialog"
title="恭喜完成学习!"
message="您已完成本视频的学习,学习记录已保存。"
show-cancel-button
cancel-button-text="继续观看"
confirm-button-text="返回课程"
@confirm="backToCourse"
></van-dialog>
</div>
<script nonce="${cspNonce!}">
const vue = new Vue({
el: "#app",
store,
data() {
return {
videoInfo: null,
loading: true,
videoId: null,
courseId: null,
videoTitle: '视频播放',
isPlaying: false,
progressSaveTimer: null,
showProgressToast: false,
showCompletionDialog: false,
savedProgress: 0,
progressSet: false // 标记进度是否已设置,避免死循环
}
},
computed: {
videoSrc() {
if (this.videoInfo && this.videoInfo.url) {
const id = this.videoInfo.url.split('id=')[1];
console.log('/platform/sys/file/videoPlay?id=' + id)
return '/platform/sys/file/videoPlay?id=' + id;
}
return null
}
},
methods: {
pageBack() {
this.historyBack(this.clearProgressSave)
},
// 加载视频信息
async loadVideoInfo() {
if (!this.videoId) {
this.$toast.fail('缺少视频ID参数');
this.loading = false;
return;
}
try {
const {code, data, msg} = await this.$axios.post('/platform/h5/edu/video/detail', {
videoId: this.videoId
});
if (code === 0 && data) {
this.videoInfo = data;
this.$nextTick(() => {
this.loadWatchProgress();
});
} else {
this.$toast.fail(msg || '加载视频信息失败');
}
} catch (error) {
this.$toast.fail('网络错误');
}
this.loading = false;
},
// 视频加载完成
onVideoLoaded() {
console.log('视频元数据加载完成');
},
// 视频可以播放
onVideoCanPlay() {
console.log('视频可以播放');
// 视频可以播放时,设置观看进度
this.setWatchProgress();
},
// 视频开始播放
onVideoPlay() {
this.isPlaying = true;
this.startProgressSave();
},
// 视频暂停
onVideoPause() {
this.isPlaying = false;
this.saveProgress();
},
// 时间更新
onTimeUpdate() {
console.log('时间更新')
// 可以在这里添加进度更新逻辑
},
// 视频播放结束
onVideoEnded() {
this.isPlaying = false;
this.markVideoCompleted();
},
// 视频加载错误
onVideoError() {
this.$toast.fail('视频加载失败');
},
// 加载观看进度
async loadWatchProgress() {
if (!this.videoId || !this.courseId) return;
try {
const {code, data} = await this.$axios.post('/platform/h5/edu/study/record', {
videoId: this.videoId
});
if (code === 0 && data && data.watchedDuration > 0) {
this.savedProgress = data.watchedDuration;
}
} catch (error) {
console.error('加载观看进度失败:', error);
}
},
// 设置观看进度
setWatchProgress() {
if (this.savedProgress > 0 && !this.progressSet) {
const video = this.$refs.videoElement;
if (video && video.readyState >= 3) {
// 使用setTimeout确保DOM更新完成
setTimeout(() => {
try {
this.progressSet = true; // 标记进度已设置,避免重复设置
video.currentTime = this.savedProgress;
console.log('设置观看进度成功:', this.savedProgress);
} catch (error) {
console.error('设置观看进度失败:', error);
}
}, 100);
} else {
console.log('视频还未准备好,readyState:', video ? video.readyState : 'video不存在');
}
}
},
// 保存观看进度
async saveProgress() {
const video = this.$refs.videoElement;
if (!video || !this.videoId || !this.courseId) return;
try {
await $.post('/platform/h5/edu/study/progress/save', {
videoId: this.videoId,
courseId: this.courseId,
watchedDuration: Math.floor(video.currentTime)
});
this.showProgressToast = true;
} catch (error) {
console.error('保存进度失败:', error);
}
},
// 开始定时保存进度
startProgressSave() {
this.clearProgressSave();
this.progressSaveTimer = setInterval(() => {
this.saveProgress();
}, 10000); // 每10秒保存一次
},
// 清除定时保存
clearProgressSave() {
if (this.progressSaveTimer) {
clearInterval(this.progressSaveTimer);
this.progressSaveTimer = null;
}
},
// 标记视频完成
async markVideoCompleted() {
if (!this.videoId || !this.courseId) return;
try {
const {code} = await $.post('/platform/h5/edu/study/complete', {
videoId: this.videoId,
courseId: this.courseId
});
if (code === 0) {
this.showCompletionDialog = true;
}
} catch (error) {
console.error('标记完成失败:', error);
}
},
// 返回课程
backToCourse() {
if (this.courseId) {
} else {
this.historyBack();
}
}
},
mounted() {
// 获取URL参数
const urlParams = new URLSearchParams(window.location.search);
this.videoId = urlParams.get('videoId');
this.courseId = urlParams.get('courseId');
this.videoTitle = decodeURIComponent(urlParams.get('title') || '视频播放');
this.loadVideoInfo();
},
beforeDestroy() {
// 页面销毁前保存进度
this.saveProgress();
this.clearProgressSave();
}
});
</script>
<!--#
}
#-->
@@ -0,0 +1,479 @@
<!--#
layout("/layouts/platform_h5.html"){
#-->
<style>
</style>
<div id="app">
<!-- 导航栏 -->
<van-nav-bar title="子女入学" left-text="返回" left-arrow
@click-left="historyBack" fixed></van-nav-bar>
<!-- 表单容器 -->
<div class="form-container">
<van-form ref="formRef">
<!-- 登记类型信息 -->
<van-cell-group title="登记类型">
<van-field
:rules="[{ required: true }]"
v-model="formData.registrationTypeName"
label="登记类型"
placeholder="请选择登记类型"
required
is-link
readonly
@click="showRegistrationTypePopup = true"
></van-field>
<van-popup v-model="showRegistrationTypePopup" position="bottom">
<van-picker
show-toolbar
:columns="registrationTypeOption.map(i => i.registrationTypeName)"
@confirm="onRegistrationTypeConfirm"
@cancel="showRegistrationTypePopup = false"
></van-picker>
</van-popup>
</van-cell-group>
<!-- 教职工信息 -->
<van-cell-group title="教职工信息">
<van-field label="监护人(教工)姓名" :rules="[{ required: true }]" v-model="formData.userName" readonly
required></van-field>
<van-field label="工号" :rules="[{ required: true }]" v-model="formData.loginName" readonly
required></van-field>
<van-field label="手机号码" name="mobile" :rules="[{ required: true }]" v-model="formData.mobile"
required
placeholder="请输入手机号码"></van-field>
<van-field label="所在单位" :rules="[{ required: true }]" v-model="formData.unitName" readonly
required></van-field>
<van-field
:rules="[{ required: true }]"
v-model="formData.childRelationshipName"
label="监护人与学生关系"
name="childRelationshipName"
placeholder="请选择监护人与学生关系"
required
is-link
readonly
@click="showChildRelationshipPopup = true"
></van-field>
<van-popup v-model="showChildRelationshipPopup" position="bottom">
<van-picker
show-toolbar
:columns="childRelationshipOption.map(i => i.name)"
@confirm="onChildRelationshipConfirm"
@cancel="showChildRelationshipPopup = false"
></van-picker>
</van-popup>
</van-cell-group>
<!-- 子女信息 -->
<van-cell-group title="子女信息">
<van-field label="子女姓名" name="childrenName" :rules="[{ required: true }]"
v-model="formData.childrenName" required
placeholder="请输入子女姓名"></van-field>
<van-field
:rules="[{ required: true }]"
v-model="formData.sex"
label="性别"
name="sex"
placeholder="请选择性别"
required
is-link
readonly
@click="showSexPopup = true"
></van-field>
<van-popup v-model="showSexPopup" position="bottom">
<van-picker
show-toolbar
:columns="sexOption"
@confirm="onSexConfirm"
@cancel="showSexPopup = false"
></van-picker>
</van-popup>
<van-field label="身份证号"
name="childrenIdCard"
:rules="[{ required: true }]"
v-model="formData.childrenIdCard"
placeholder="请输入身份证号"
required
></van-field>
<van-field label="出生年月"
name="childrenBirthday"
:rules="[{ required: true }]"
:value="formData.childrenBirthday"
readonly
is-link
placeholder="请填写出生年月"
required
@click="childrenBirthdayPopup = true"></van-field>
<van-popup v-model="childrenBirthdayPopup" position="bottom">
<van-datetime-picker
v-model="formData.childrenBirthdayDate"
type="date"
title="选择出生年月"
:min-date="childrenBirthdayMinDate"
:max-date="childrenBirthdayMaxDate"
@confirm="childrenBirthdayConfirm"
@cancel="childrenBirthdayPopup=false"
></van-datetime-picker>
</van-popup>
<van-field label="现就读学校"
:rules="[{ required: true }]"
v-model="formData.childrenCurrentSchool"
required
placeholder="请输入现就读学校"></van-field>
<van-field
:rules="[{ required: true }]"
v-model="formData.childrenPlanSchoolName"
label="拟报就读学校"
name="childrenPlanSchoolName"
placeholder="请选择拟报就读学校"
required
is-link
readonly
@click="showChildrenPlanSchoolPopup = true"
class="van-field"
></van-field>
<van-popup v-model="showChildrenPlanSchoolPopup" position="bottom">
<van-picker
show-toolbar
:columns="childrenPlanSchoolOption.map(i => i.name)"
@confirm="onChildrenPlanSchoolConfirm"
@cancel="showChildrenPlanSchoolPopup = false"
></van-picker>
</van-popup>
<van-field label="子女户口所在地"
:rules="[{ required: true }]"
v-model="formData.childrenHuKouAddress"
required
type="textarea"
name="childrenHuKouAddress"
rows="4"
autosize
class="more-text"
placeholder="请输入子女户口所在地"></van-field>
<van-field label="备注"
:rules="[{ required: true }]"
v-model="formData.note"
required
type="textarea"
name="note"
rows="4"
autosize
class="more-text"
placeholder="请填写户籍所在地派出所"></van-field>
</van-cell-group>
<!-- 户口簿照片 -->
<van-cell-group title="户口簿照片">
<van-field class="more-text" name="huKouFiles" :rules="[{ required: true,message:'请上传户口簿照片' }]"
label="" required>
<template #input>
<h5-file-upload
slot="input"
:value.sync="formData.huKouFiles"
:upload_number="10"
upload_mode="file"
upload_result_category="array"
upload_result_type="url"
complete_result
></h5-file-upload>
</template>
</van-field>
</van-cell-group>
<!-- 子女出生证照片 -->
<van-cell-group title="子女出生证照片">
<van-field class="more-text" name="birthCertificateFiles" label="">
<template #input>
<h5-file-upload
slot="input"
:value.sync="formData.birthCertificateFiles"
:upload_number="10"
upload_mode="image"
upload_result_category="array"
upload_result_type="url"
complete_result
></h5-file-upload>
</template>
</van-field>
</van-cell-group>
<!-- 按钮区域 -->
<div class="form-actions">
<van-button native-type="button" plain type="info" @click="onSave">保存</van-button>
<van-button type="primary" @click="onSubmit" v-if="!taskId">提交</van-button>
<van-button type="primary" @click="onFinishTask" v-else>提交</van-button>
</div>
</van-form>
</div>
</div>
<script nonce="${cspNonce!}">
const vue = new Vue({
el: "#app",
store,
dicts: ["ENROLLMENT_REGISTRATION_CHILD_RELATIONSHIP", "ENROLLMENT_REGISTRATION_CHILDREN_PLAN_SCHOOL"],
components: {},
data() {
return {
bizId: GetQueryString("bizId"),
taskId: GetQueryString("taskId"),
formData: {},
registrationTypeOption: [],
showRegistrationTypePopup: false,
childRelationshipOption: [],
showChildRelationshipPopup: false,
sexOption: ["男", "女"],
showSexPopup: false,
childrenBirthdayMinDate: new Date(1900, 0, 1),
childrenBirthdayMaxDate: new Date(),
childrenBirthdayPopup: false,
childrenPlanSchoolOption: [],
showChildrenPlanSchoolPopup: false,
}
},
methods: {
async getEnrollmentRegistrationPlan() {
const res = await this.$axios.post("/platform/enrollmentRegistration/apply/getEnrollmentRegistrationPlan")
if (res.code === 0) {
this.registrationTypeOption = res.data
}
},
init() {
setTimeout(() => {
this.childRelationshipOption = this.dict.type.ENROLLMENT_REGISTRATION_CHILD_RELATIONSHIP
this.childrenPlanSchoolOption = this.dict.type.ENROLLMENT_REGISTRATION_CHILDREN_PLAN_SCHOOL
}, 300)
if (this.bizId) {
this.findOne(this.bizId).then(async data => {
this.formData = data
//登记类型回显
const registrationType = this.registrationTypeOption.find(v => v.registrationType === data.registrationType)
if (registrationType) {
this.$set(this.formData, "registrationTypeName", registrationType.registrationTypeName)
}
//监护人与学生关系回显
const childRelationship = this.dict.type.ENROLLMENT_REGISTRATION_CHILD_RELATIONSHIP.find(v => v.code === data.childRelationship)
if (childRelationship) {
this.$set(this.formData, "childRelationshipName", childRelationship.name)
}
//出生年月回显
if (data.childrenBirthday) {
this.$set(this.formData, "childrenBirthdayDate", new Date(data.childrenBirthday))
}
//拟报就读学校
const childrenPlanSchool = this.dict.type.ENROLLMENT_REGISTRATION_CHILDREN_PLAN_SCHOOL.find(v => v.code === data.childrenPlanSchool)
if (childrenPlanSchool) {
this.$set(this.formData, "childrenPlanSchoolName", childrenPlanSchool.name)
}
})
} else {
const {id, username, loginname, mobile, union, unit} = this.$store.state.user
this.formData = {
userId: id,
userName: username,
loginName: loginname,
unitName: unit.name,
unitId: unit.id,
unionName: union.name,
unionId: union.id,
mobile: mobile,
}
}
},
onChildrenPlanSchoolConfirm(value, index) {
this.formData.childrenPlanSchool = this.childrenPlanSchoolOption[index].code;
this.formData.childrenPlanSchoolName = value;
this.showChildrenPlanSchoolPopup = false;
},
childrenBirthdayConfirm(value) {
this.formData.childrenBirthdayDate = value
this.formData.childrenBirthday = this.$moment(value).format("YYYY-MM-DD")
this.childrenBirthdayPopup = false
},
onSexConfirm(value) {
this.formData.sex = value;
this.showSexPopup = false;
},
onChildRelationshipConfirm(value, index) {
this.formData.childRelationship = this.childRelationshipOption[index].code;
this.formData.childRelationshipName = value;
this.showChildRelationshipPopup = false;
},
onRegistrationTypeConfirm(value, index) {
this.formData.registrationType = this.registrationTypeOption[index].registrationType;
this.formData.registrationTypeName = value;
this.showRegistrationTypePopup = false;
},
validateBirthday() {
if (!this.formData.registrationType) {
return "请选择登记类型"
}
if (!this.formData.childrenBirthday) {
return "请选择子女出生年月"
}
const childrenBirthday = new Date(this.formData.childrenBirthday);
if (isNaN(childrenBirthday.getTime())) {
return "出生日期格式无效";
}
const registrationType = this.registrationTypeOption.find(item => item.registrationType === this.formData.registrationType)
if (registrationType.greaterThanBirthday) {
const greaterThanBirthday = new Date(registrationType.greaterThanBirthday);
if (isNaN(greaterThanBirthday.getTime())) {
return "限制日期格式无效";
}
if (childrenBirthday < greaterThanBirthday) {
return "出生日期不能小于" + registrationType.greaterThanBirthday;
}
}
if (registrationType.lessThanBirthday) {
const lessThanBirthday = new Date(registrationType.lessThanBirthday);
if (isNaN(lessThanBirthday.getTime())) {
return "限制日期格式无效";
}
if (childrenBirthday > lessThanBirthday) {
return "出生日期不能大于" + registrationType.lessThanBirthday;
}
}
return null; // 表示通过
},
async getIsRepeatByIdCard() {
if (!this.formData.childrenIdCard) {
this.$toast.fail("请填写子女身份证号码")
return
}
const res = await this.$axios.post('/platform/enrollmentRegistration/apply/getIsRepeatByIdCard', {
idCard: this.formData.childrenIdCard,
id: this.formData.id
})
if (res.code === 0) {
if (res.data > 0) {
this.$toast.fail("该身份证在本年度已填报!")
return true
} else {
this.$toast.success("该身份证在本年度暂未填报!")
return false
}
}
},
async findOne(id) {
const resp = await $.get('/platform/enrollmentRegistration/apply/findOne', {id})
if (resp.code === 0) {
return resp.data
}
},
onSave() {
const msg = this.validateBirthday()
if (msg) {
this.$toast.fail(msg)
return
}
this.$dialog.confirm({
title: '温馨提示',
message: '您确定保存吗?',
}).then(() => {
this.$axios.post('/platform/enrollmentRegistration/apply/save', {
data: JSON.stringify(this.formData)
}).then(res => {
if (res.code === 0) {
this.$toast.success("提交成功")
pjaxReplace("/platform/enrollmentRegistration/applyList/h5")
}
})
}).catch(() => {
// on cancel
});
},
onSubmit() {
const msg = this.validateBirthday()
if (msg) {
this.$toast.fail(msg)
return
}
this.$refs.formRef.validate().then(() => {
this.$dialog.confirm({
title: '温馨提示',
message: '您确定要提交吗?',
}).then(() => {
this.$axios.post('/platform/enrollmentRegistration/apply/submit', {
data: JSON.stringify(this.formData),
taskId: GetQueryString("taskId")
}).then(res => {
if (res.code === 0) {
this.$toast.success("提交成功")
pjaxReplace("/platform/enrollmentRegistration/applyList/h5")
}
})
}).catch(() => {
// on cancel
});
}).catch();
},
onFinishTask() {
const msg = this.validateBirthday()
if (msg) {
this.$toast.fail(msg)
return
}
this.$refs.formRef.validate().then(() => {
this.$dialog.confirm({
title: '温馨提示',
message: '您确定要提交吗?',
}).then(() => {
this.$axios.post('/platform/enrollmentRegistration/apply/submitAgain', {
data: JSON.stringify(this.formData),
taskId: GetQueryString("taskId")
}).then(res => {
if (res.code === 0) {
this.$toast.success("提交成功")
pjaxReplace("/platform/enrollmentRegistration/applyList/h5")
}
})
}).catch(() => {
// on cancel
});
}).catch();
}
},
async created() {
await this.getEnrollmentRegistrationPlan()
this.init()
}
})
</script>
<!--#
}
#-->
@@ -0,0 +1,170 @@
const H5_ENROLLMENT_REGISTRATION_INFO = {
template: /*language=HTML*/ `
<van-action-sheet v-model="visible" title="查看详情">
<div>
<div class="process-title">教职工信息</div>
<van-cell-group>
<van-cell title="监护人(教工)姓名">
{{ viewData.userName }}
</van-cell>
<van-cell title="工号">
{{ viewData.loginName }}
</van-cell>
<van-cell title="手机号码">
{{ viewData.mobile }}
</van-cell>
<van-cell title="所在单位">
{{ viewData.unitName }}
</van-cell>
<van-cell title="监护人与学生关系">
<dict-tag :options="dict.type.ENROLLMENT_REGISTRATION_CHILD_RELATIONSHIP"
:value="viewData.childRelationship"></dict-tag>
</van-cell>
<van-cell title="登记类型">
<dict-tag :options="dict.type.ENROLLMENT_REGISTRATION_TYPE"
:value="viewData.registrationType"></dict-tag>
</van-cell>
</van-cell-group>
<div class="process-title">子女信息</div>
<van-cell-group>
<van-cell title="子女姓名">
{{viewData.childrenName}}
</van-cell>
<van-cell title="性别">
{{viewData.sex}}
</van-cell>
<van-cell title="身份证号">
{{viewData.childrenIdCard}}
</van-cell>
<van-cell title="出生日期">
{{viewData.childrenBirthday}}
</van-cell>
<van-cell title="现就读学校">
{{viewData.childrenCurrentSchool}}
</van-cell>
<van-cell title="拟报就读学校">
<dict-tag :options="dict.type.ENROLLMENT_REGISTRATION_CHILDREN_PLAN_SCHOOL"
:value="viewData.childrenPlanSchool"></dict-tag>
</van-cell>
<van-cell title="子女户口所在地">
{{viewData.childrenHuKouAddress}}
</van-cell>
<van-cell title="备注">
{{viewData.note}}
</van-cell>
<van-cell title="户口簿照片">
<template #label>
<template v-for="(item,index) in viewData.huKouFiles">
<van-image :src="item.url"
v-if="item.url"
class="signature-image"
@click="previewOptionImg(viewData.huKouFiles,index)"></van-image>
</template>
</template>
</van-cell>
<van-cell title="子女出生证照片">
<template #label>
<template v-for="(item,index) in viewData.birthCertificateFiles">
<van-image :src="item.url"
v-if="item.url"
class="signature-image"
@click="previewOptionImg(viewData.birthCertificateFiles,index)"></van-image>
</template>
</template>
</van-cell>
</van-cell-group>
<template v-for="(task,index) in doneTasks">
<div class="process-title">
{{ task.displayName }}
</div>
<van-cell-group v-if="task.ext.isFirstTaskNode">
<van-cell title="申请用户">
{{ task.ext.initiatorName}}({{task.ext.initiatorAccount}})
</van-cell>
<van-cell title="申请时间">
{{ task.finishTime}}
</van-cell>
<van-cell title="办理结果">
<dict-tag :options="dict.type.PROCESS_TASK_SUBMIT_TYPE"
:value="task.ext.submitType"></dict-tag>
</van-cell>
</van-cell-group>
<van-cell-group v-else>
<van-cell title="办理用户">
{{ task.taskFormData.userName}}({{task.taskFormData.loginName}})
</van-cell>
<van-cell title="办理时间">
{{ task.finishTime}}
</van-cell>
<van-cell title="办理结果">
<dict-tag :options="dict.type.PROCESS_TASK_SUBMIT_TYPE"
:value="task.ext.submitType"></dict-tag>
</van-cell>
<van-cell title="办理意见" v-if="!task.ext.isFirstTaskNode">
<template #label>
{{
task.taskFormData.opinion
}}
</template>
</van-cell>
<van-cell title="签字" v-if="!task.ext.isFirstTaskNode">
<van-image :src="task.ext.tf_userSign"
v-if="task.ext.tf_userSign"
class="signature-image"></van-image>
</van-cell>
</van-cell-group>
</template>
<slot></slot>
</div>
</van-action-sheet>
`,
dicts: ["ENROLLMENT_REGISTRATION_TYPE", "ENROLLMENT_REGISTRATION_CHILD_RELATIONSHIP", "ENROLLMENT_REGISTRATION_CHILDREN_PLAN_SCHOOL", "PROCESS_TASK_SUBMIT_TYPE"],
data() {
return {
visible: false,
viewData: {},
doneTasks: [],
row: null
}
},
methods: {
//预览图片
previewOptionImg(files, index) {
const urls = files.map(item => item.url)
vant.ImagePreview({images: urls, startPosition: index})
},
// 打开
onOpen(row) {
this.row = row
this.visible = true
this.getInfo()
this.getDoneTasks()
},
// 关闭
onClose() {
this.visible = false
},
// 获取申请信息
getInfo() {
this.$axios.post('/platform/enrollmentRegistration/apply/findOne', {id: this.row.id}).then((res) => {
if (res.code === 0) {
this.viewData = res.data
}
})
},
// 获取已办任务审批记录
getDoneTasks() {
this.$axios.post("/flow/common/doneTasks", {bizId: this.row.id}).then((res) => {
if (res.code === 0) {
this.doneTasks = res.data
}
})
},
}
}
@@ -0,0 +1,134 @@
<!--#
layout("/layouts/platform_h5.html"){
#-->
<div id="app">
<!-- 导航栏 -->
<van-nav-bar title="我的申报" left-text="返回" left-arrow placeholder
@click-left="historyBack" fixed></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-menu>
</van-sticky>
<table-list api="/platform/enrollmentRegistration/applyList/pageData" :page_form.sync="pageForm" ref="tableListRef"
@ready="doSearch">
<template v-slot="{index,row}">
<table-column label="子女姓名">{{row.childrenName}}</table-column>
<table-column label="教职工姓名">{{row.userName}}</table-column>
<table-column label="手机号码">{{row.mobile}}</table-column>
<table-column label="所属单位">{{row.unitName}}</table-column>
<table-column label="登记类型">
<dict-tag :options="dict.type.ENROLLMENT_REGISTRATION_TYPE"
:value="row.registrationType"></dict-tag>
</table-column>
<table-column label="填报时间">{{row.applyTime}}</table-column>
<table-column label="当前节点">{{row.taskName}}</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="onEdit(row)">
<i class="fa fa-edit"></i>
<span>编辑</span>
</div>
<div class="action-btn delete" v-if="row.canRevoke" @click="onRevoke(row)">
<i class="fa fa-undo"></i>
<span>撤回</span>
</div>
<div class="action-btn delete" @click="onDelete(row.id)"
v-if="row.taskKey === 'startTask' || !row.instanceId">
<i class="fa fa-trash"></i>
<span>删除</span>
</div>
</template>
</table-list>
<h5-enrollment-registration-info ref="h5EnrollmentRegistrationInfoRef"></h5-enrollment-registration-info>
</div>
<script nonce="${cspNonce!}">
<!--#include('../info.js'){}#-->
const vue = new Vue({
el: "#app",
store,
dicts: ["ENROLLMENT_REGISTRATION_TYPE", "ENROLLMENT_REGISTRATION_CHILD_RELATIONSHIP", "ENROLLMENT_REGISTRATION_CHILDREN_PLAN_SCHOOL"],
components: {
"h5-enrollment-registration-info": H5_ENROLLMENT_REGISTRATION_INFO
},
data() {
return {
pageForm: {
pageNumber: 1,
pageSize: 10,
totalCount: 0,
year: new Date().getFullYear(),
},
}
},
methods: {
onView(row) {
this.$refs.h5EnrollmentRegistrationInfoRef.onOpen(row)
},
onEdit(row) {
pjaxReplace('/platform/enrollmentRegistration/apply/h5?taskId=' + (row.startTaskId || '') + "&bizId=" + row.id)
},
onDelete(id) {
this.$dialog.confirm({
title: '温馨提示',
message: '您确定要删除吗?',
}).then(() => {
this.$axios.post("/platform/enrollmentRegistration/applyList/doDelete", {id}).then((res) => {
if (res.code === 0) {
this.$toast.success(res.msg)
this.doSearch()
} else {
this.$toast.fail(res.msg)
}
})
}).catch(() => {
// on cancel
});
},
onRevoke(row) {
this.$dialog.confirm({
title: '温馨提示',
message: '您确定要撤回吗?',
}).then(() => {
this.$axios.post("/flow/common/revokeTask", {taskId: row.startTaskId}).then((res) => {
if (res.code === 0) {
this.$toast.success(res.msg)
this.doSearch()
} else {
this.$toast.fail(res.msg)
}
})
}).catch(() => {
// on cancel
});
},
doSearch() {
this.pageForm.pageNumber = 1
this.pageForm.totalCount = 0
this.$refs.tableListRef.doSearch()
},
},
created() {
}
})
</script>
<!--#
}
#-->
@@ -0,0 +1,187 @@
<!--#
layout("/layouts/platform_h5.html"){
#-->
<div id="app">
<van-nav-bar @click-left="historyBack" left-arrow left-text="返回" title="校工会审核" placeholder
fixed></van-nav-bar>
<van-sticky offset-top="46px">
<van-search
:reverse-color="false"
:show-action="false"
@search="doSearch"
input-align="left"
placeholder="请输入姓名搜索"
v-model="pageForm.searchKeyword"
></van-search>
<van-tabs v-model="pageForm.approvalText"
@change="(val)=>{this.pageForm.approval = val==='1';this.doSearch();}">
<van-tab title="已审核" name="1"></van-tab>
<van-tab title="未审核" name="0"></van-tab>
</van-tabs>
</van-sticky>
<table-list api="/platform/enrollmentRegistration/schoolAudit/pageData" :page_form.sync="pageForm"
ref="tableListRef"
@ready="doSearch">
<template v-slot="{index,row}">
<table-column label="子女姓名">{{row.childrenName}}</table-column>
<table-column label="教职工姓名">{{row.userName}}</table-column>
<table-column label="手机号码">{{row.mobile}}</table-column>
<table-column label="所属单位">{{row.unitName}}</table-column>
<table-column label="登记类型">
<dict-tag :options="dict.type.ENROLLMENT_REGISTRATION_TYPE"
:value="row.registrationType"></dict-tag>
</table-column>
<table-column label="填报时间">{{row.applyTime}}</table-column>
<table-column label="当前节点">{{row.curTaskName}}</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" v-if="row.taskState === 10" @click="onAudit(row)">
<i class="fa fa-edit"></i>
<span>审核</span>
</div>
<div class="action-btn delete" v-if="row.canRevoke" @click="onRevoke(row)">
<i class="fa fa-undo"></i>
<span>撤回</span>
</div>
</template>
</table-list>
<h5-enrollment-registration-info ref="h5EnrollmentRegistrationInfoRef">
<div v-if="showApprovalForm">
<div class="process-title">
校工会审核
</div>
<div class="form-container">
<van-form ref="formRef">
<van-cell-group title="审批意见">
<van-field label=""
:rules="[{ required: true,message:'请填审批意见' }]"
v-model="formData.tf_opinion"
required
type="textarea"
name="tf_opinion"
rows="4"
autosize
class="more-text"
placeholder="请填审批意见"></van-field>
</van-cell-group>
<van-cell-group title="电子签名" class="form-section">
<van-field class="more-text" name="tf_userSign" label="">
<template #input>
<h5-signature v-model="formData.tf_userSign" slot="input"></h5-signature>
</template>
</van-field>
</van-cell-group>
<!-- 按钮区域 -->
<div class="form-actions">
<van-button type="primary" @click="handleTaskAction(1)">同意申请</van-button>
<van-button type="danger" @click="handleTaskAction(2)">拒绝申请</van-button>
<van-button type="danger" @click="handleTaskAction(6)">退回到发起人</van-button>
</div>
</van-form>
</div>
</div>
</h5-enrollment-registration-info>
</div>
<script nonce="${cspNonce!}">
<!--#include('../info.js'){}#-->
const vue = new Vue({
el: "#app",
store,
dicts: ["ENROLLMENT_REGISTRATION_TYPE", "ENROLLMENT_REGISTRATION_CHILD_RELATIONSHIP", "ENROLLMENT_REGISTRATION_CHILDREN_PLAN_SCHOOL"],
components: {
"h5-enrollment-registration-info": H5_ENROLLMENT_REGISTRATION_INFO
},
data() {
return {
pageForm: {
pageNumber: 1,
pageSize: 10,
totalCount: 0,
year: new Date().getFullYear(),
approvalText: "0",
approval: false,
searchName: "info.userName",
},
formData: {},
infoShow: false,
showApprovalForm: false,
}
},
methods: {
handleTaskAction(val) {
this.$refs.formRef.validate().then(() => {
this.$dialog.confirm({
title: '温馨提示',
message: '您确定要提交吗?',
}).then(() => {
this.$axios.post('/flow/common/executeTask', {
data: JSON.stringify({
...this.formData,
submitType: val
})
}).then(res => {
if (res.code === 0) {
this.$toast.success("提交成功")
this.doSearch()
this.$refs.h5EnrollmentRegistrationInfoRef.onClose()
}
})
}).catch(() => {
// on cancel
});
}).catch();
},
onAudit(row) {
this.showApprovalForm = true
this.formData = {
tf_opinion: null,
tf_userSign: null,
processTaskId: row.taskId,
taskName: row.curTaskName
}
this.$refs.h5EnrollmentRegistrationInfoRef.onOpen(row)
},
onRevoke(row) {
this.$dialog.confirm({
title: '温馨提示',
message: '您确定要撤回吗?',
}).then(() => {
this.$axios.post("/flow/common/revokeTask", {taskId: row.taskId}).then(res => {
if (res.code === 0) {
this.$toast.success("提交成功")
this.doSearch()
this.infoShow = false
}
})
}).catch(() => {
// on cancel
});
},
onView(row) {
this.showApprovalForm = false
this.$refs.h5EnrollmentRegistrationInfoRef.onOpen(row)
},
doSearch() {
this.$nextTick(() => {
this.pageForm.pageNumber = 1
this.pageForm.totalCount = 0
this.$refs.tableListRef.doSearch()
})
},
}
})
</script>
<!--#
}
#-->
@@ -0,0 +1,189 @@
<!--#
layout("/layouts/platform_h5.html"){
#-->
<div id="app">
<van-nav-bar @click-left="historyBack" left-arrow left-text="返回" title="分工会审核" placeholder
fixed></van-nav-bar>
<van-sticky offset-top="46px">
<van-search
:reverse-color="false"
:show-action="false"
@search="doSearch"
input-align="left"
placeholder="请输入姓名搜索"
v-model="pageForm.searchKeyword"
></van-search>
<!--<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-menu>-->
<van-tabs v-model="pageForm.approvalText"
@change="(val)=>{this.pageForm.approval = val==='1';this.doSearch();}">
<van-tab title="已审核" name="1"></van-tab>
<van-tab title="未审核" name="0"></van-tab>
</van-tabs>
</van-sticky>
<table-list api="/platform/enrollmentRegistration/unionAudit/pageData" :page_form.sync="pageForm" ref="tableListRef"
@ready="doSearch">
<template v-slot="{index,row}">
<table-column label="子女姓名">{{row.childrenName}}</table-column>
<table-column label="教职工姓名">{{row.userName}}</table-column>
<table-column label="手机号码">{{row.mobile}}</table-column>
<table-column label="所属单位">{{row.unitName}}</table-column>
<table-column label="登记类型">
<dict-tag :options="dict.type.ENROLLMENT_REGISTRATION_TYPE"
:value="row.registrationType"></dict-tag>
</table-column>
<table-column label="填报时间">{{row.applyTime}}</table-column>
<table-column label="当前节点">{{row.curTaskName}}</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" v-if="row.taskState === 10" @click="onAudit(row)">
<i class="fa fa-edit"></i>
<span>审核</span>
</div>
<div class="action-btn delete" v-if="row.canRevoke" @click="onRevoke(row)">
<i class="fa fa-undo"></i>
<span>撤回</span>
</div>
</template>
</table-list>
<h5-enrollment-registration-info ref="h5EnrollmentRegistrationInfoRef">
<div v-if="showApprovalForm">
<div class="process-title">
分工会审核
</div>
<div class="form-container">
<van-form ref="formRef">
<van-cell-group title="审批意见">
<van-field label=""
:rules="[{ required: true,message:'请填审批意见' }]"
v-model="formData.tf_opinion"
required
type="textarea"
name="tf_opinion"
rows="4"
autosize
class="more-text"
placeholder="请填审批意见"></van-field>
</van-cell-group>
<van-cell-group title="电子签名" class="form-section">
<van-field class="more-text" name="tf_userSign" label="">
<template #input>
<h5-signature v-model="formData.tf_userSign" slot="input"></h5-signature>
</template>
</van-field>
</van-cell-group>
<!-- 按钮区域 -->
<div class="form-actions">
<van-button type="primary" @click="handleTaskAction(1)">同意申请</van-button>
<van-button type="danger" @click="handleTaskAction(2)">拒绝申请</van-button>
<van-button type="danger" @click="handleTaskAction(6)">退回到发起人</van-button>
</div>
</van-form>
</div>
</div>
</h5-enrollment-registration-info>
</div>
<script nonce="${cspNonce!}">
<!--#include('../info.js'){}#-->
const vue = new Vue({
el: "#app",
store,
dicts: ["ENROLLMENT_REGISTRATION_TYPE", "ENROLLMENT_REGISTRATION_CHILD_RELATIONSHIP", "ENROLLMENT_REGISTRATION_CHILDREN_PLAN_SCHOOL"],
components: {
"h5-enrollment-registration-info": H5_ENROLLMENT_REGISTRATION_INFO
},
data() {
return {
pageForm: {
pageNumber: 1,
pageSize: 10,
totalCount: 0,
year: new Date().getFullYear(),
approvalText: "0",
approval: false,
searchName: "info.userName",
},
formData: {},
infoShow: false,
showApprovalForm: false,
}
},
methods: {
handleTaskAction(val) {
this.$refs.formRef.validate().then(() => {
this.$dialog.confirm({
title: '温馨提示',
message: '您确定要提交吗?',
}).then(() => {
this.$axios.post('/flow/common/executeTask', {
data: JSON.stringify({
...this.formData,
submitType: val
})
}).then(res => {
if (res.code === 0) {
this.$toast.success("提交成功")
this.doSearch()
this.$refs.h5EnrollmentRegistrationInfoRef.onClose()
}
})
}).catch(() => {
// on cancel
});
}).catch();
},
onAudit(row) {
this.showApprovalForm = true
this.formData = {
tf_opinion: null,
tf_userSign: null,
processTaskId: row.taskId,
taskName: row.curTaskName
}
this.$refs.h5EnrollmentRegistrationInfoRef.onOpen(row)
},
onRevoke(row) {
this.$dialog.confirm({
title: '温馨提示',
message: '您确定要撤回吗?',
}).then(() => {
this.$axios.post("/flow/common/revokeTask", {taskId: row.taskId}).then(res => {
if (res.code === 0) {
this.$toast.success("提交成功")
this.doSearch()
this.infoShow = false
}
})
}).catch(() => {
// on cancel
});
},
onView(row) {
this.showApprovalForm = false
this.$refs.h5EnrollmentRegistrationInfoRef.onOpen(row)
},
doSearch() {
this.$nextTick(() => {
this.pageForm.pageNumber = 1
this.pageForm.totalCount = 0
this.$refs.tableListRef.doSearch()
})
},
}
})
</script>
<!--#
}
#-->
@@ -0,0 +1,123 @@
<!--#
layout("/layouts/platform_h5.html"){
#-->
<style>
</style>
<div id="app">
<van-nav-bar @click-left="historyBack" left-arrow left-text="返回" title="体检项目选择" placeholder
fixed></van-nav-bar>
<van-sticky offset-top="46px">
<van-search
:reverse-color="false"
:show-action="false"
@search="doSearch"
input-align="left"
placeholder="请输入项目名称搜索"
v-model="pageForm.name"
></van-search>
<van-tabs v-model="pageForm.status"
@change="onStatusChange">
<van-tab title="进行中" name="ongoing"></van-tab>
<van-tab title="已结束" name="finished"></van-tab>
</van-tabs>
</van-sticky>
<table-list api="/platform/healthCheckup/h5/pageData" :page_form.sync="pageForm" ref="tableListRef"
@ready="doSearch" img="cover">
<template v-slot="{index,row}">
<table-column label="开始时间">
{{$moment(row.choiceTimeStart).format('MM/DD HH:mm')}}
</table-column>
<table-column label="结束时间">
{{$moment(row.choiceTimeEnd).format('MM/DD HH:mm')}}
</table-column>
</template>
<template #actions="{index,row}">
<template v-if="$moment().isBefore($moment(row.choiceTimeEnd))">
<div class="action-btn" @click="onView(row)">
<i class="fa fa-edit"></i>
<span>去报名</span>
</div>
</template>
</template>
</table-list>
</div>
<script nonce="${cspNonce!}">
const vue = new Vue({
el: "#app",
store,
components: {
},
data() {
return {
yearList: [],
pageForm: {
pageNumber: 1,
pageSize: 10,
totalCount: 0,
year: "",
status: "ongoing", // 默认为进行中
name: "" // 添加搜索名称字段
},
}
},
methods: {
// 查看项目详情
viewProjectDetail(row) {
// 这里可以添加查看项目详情的逻辑
console.log('查看项目详情:', row);
},
onView(row) {
/* if(row.selectCount > 0) {
vant.Dialog.alert({
title: '温馨提示',
message: '此项活动您已选择',
})
return
}*/
// 检查项目是否已结束
const now = new Date();
const endTime = new Date(row.choiceTimeEnd);
if (now > endTime) {
vant.Toast('报名已经结束');
return;
}
this.$pjaxReplace("/platform/healthCheckup/h5/projectManageForm?projectId=" + row.id)
},
doSearch() {
this.$nextTick(() => {
this.pageForm.pageNumber = 1
this.pageForm.totalCount = 0
this.$refs.tableListRef.doSearch()
})
},
// 新增状态切换处理方法
onStatusChange() {
this.doSearch();
},
initData() {
for (let i = new Date().getFullYear() - 10; i <= new Date().getFullYear(); i++) {
this.yearList.unshift({value: i, text: i + "年"})
}
this.$set(this.pageForm, "year", this.yearList[0].value)
// 初始化状态
this.$set(this.pageForm, "status", "ongoing")
},
},
created() {
this.initData()
}
})
</script>
<!--#
}
#-->
@@ -0,0 +1,122 @@
<!--#
layout("/layouts/platform_h5.html"){
#-->
<div id="app">
<van-nav-bar @click-left="historyBack" left-arrow left-text="返回" title="我的体检" placeholder fixed></van-nav-bar>
<van-sticky offset-top="46px">
<van-tabs v-model="pageForm.status"
@change="onStatusChange">
<van-tab title="进行中" name="ongoing"></van-tab>
<van-tab title="已结束" name="finished"></van-tab>
</van-tabs>
</van-sticky>
<table-list api="/platform/healthCheckup/h5/mineData" :page_form.sync="pageForm" ref="tableListRef"
@ready="doSearch" title="healthCheckupName">
<template v-slot="{index,row}">
<table-column label="项目名称">{{row.name}}</table-column>
<table-column label="开始时间">{{row.choiceTimeStart}}</table-column>
<table-column label="结束时间">{{row.choiceTimeEnd}}</table-column>
<table-column label="选择医院">{{row.optionName}}</table-column>
<table-column label="邮寄地址">{{row.receiveAddress}}</table-column>
</template>
<template #actions="{index,row}">
<div class="action-btn" @click="edit(row)" v-if="new Date().getTime() < new Date(row.choiceTimeEnd).getTime()">
<i class="fa fa-edit"></i>
<span>修改</span>
</div>
<div class="action-btn delete" @click="cancel(row)" v-if="new Date().getTime() < new Date(row.choiceTimeEnd).getTime()">
<i class="fa fa-trash"></i>
<span>取消</span>
</div>
</template>
</table-list>
</div>
<script nonce="${cspNonce!}">
const vue = new Vue({
el: "#app",
store,
components: {
},
data() {
return {
viewShow: false,
yearList: [],
pageForm: {
pageNumber: 1,
pageSize: 10,
totalCount: 0,
year: "",
status: "ongoing", // 默认为进行中
name: "" // 添加搜索名称字段
},
}
},
methods: {
edit(row) {
this.$pjaxReplace("/platform/healthCheckup/h5/projectManageForm?projectId=" + row.projectId)
},
async cancel(o) {
vant.Dialog.confirm({
title: '温馨提醒',
message: '您确定要取消此选择吗?',
}).then(async () => {
const res = await $.post('/platform/healthCheckup/h5/cancel', {id: o.id, projectId: o.projectId})
if (res.code === 0) {
vant.Toast.success('取消成功')
this.doSearch()
} else {
vant.Toast(res.msg)
}
}).catch(() => {
})
},
tabChange(val) {
this.tabName = val
this.startLoading()
this.tableData = []
this.pageForm.pageNumber = 1
this.pageData()
this.closeLoading()
},
doSearch() {
this.$nextTick(() => {
this.pageForm.pageNumber = 1
this.pageForm.totalCount = 0
this.$refs.tableListRef.doSearch()
})
},
// 新增状态切换处理方法
onStatusChange() {
this.doSearch();
},
init() {
this.startLoading()
this.pageForm.tabName = 'ing'
this.pageData()
this.closeLoading()
},
initData() {
for (let i = new Date().getFullYear() - 10; i <= new Date().getFullYear(); i++) {
this.yearList.unshift({value: i, text: i + "年"})
}
this.$set(this.pageForm, "year", this.yearList[0].value)
// 初始化状态
this.$set(this.pageForm, "status", "ongoing")
},
},
created() {
this.initData()
}
})
</script>
<!--#
}
#-->
@@ -0,0 +1,516 @@
<!--#
layout("/layouts/platform_h5.html"){
#-->
<div id="app">
<van-nav-bar @click-left="historyBack" left-arrow left-text="返回" title="体检套餐选择" placeholder
fixed></van-nav-bar>
<div class="form-container">
<van-form ref="formRef" @submit="doSubmit">
<van-cell-group title="基础信息">
<van-field
input-align="right"
label="身份证号"
name="idCard"
readonly
required
placeholder="请联系校工会补充身份证号"
v-model="formData.idCard"
:rules="[{ required: true, message: '请输入身份证号' }]">
</van-field>
<van-cell title="人员类型">{{formData.userState}}</van-cell>
<van-field
input-align="right"
readonly
required
label="婚姻状况"
name="marriage"
placeholder="请联系校工会补充婚姻状况"
v-model="formData.marriage"
:rules="[{ required: true, message: '请选择婚姻状况' }]"></van-field>
<van-cell title="出生年月">{{$moment(formData.birthday).format('YYYY-MM-DD')}}</van-cell>
<van-cell title="单位">{{formData.unitName}}</van-cell>
</van-cell-group>
<van-cell-group title="本年度预约体检医院">
<div class="van-card-body">
<van-radio-group v-model="formData.subjectId" class="basic">
<van-cell-group>
<van-cell
v-for="item in project.healthCheckupProjectSubjects"
:key="item.id"
:title="item.optionName">
<template #label>
<div style="display: flex; justify-content: space-between; margin-left: 8px">
<!-- <div>{{ item.optionName }}</div>-->
<div></div>
<div @click.stop="viewDesc(item)" style="color: #0e78c5">查看说明</div>
</div>
</template>
<template #right-icon>
<van-radio :name="item.id" @click.stop="clickRadio(item)"></van-radio>
</template>
</van-cell>
</van-cell-group>
</van-radio-group>
</div>
<van-field
input-align="right"
readonly
required
label="所选医院额度"
name="marriage"
placeholder="请选择医院"
v-model="formData.money"
:rules="[{ required: true, message: '请选择医院' }]"></van-field>
</van-cell-group>
<van-cell-group title="体检报告邮寄地址">
<van-radio-group v-model="formData.postMethod">
<van-cell-group>
<van-cell title="单位" clickable
@click="formData.postMethod = '1';formData.receiveAddress=formData.unitName">
<template #right-icon>
<van-radio name="1"></van-radio>
</template>
</van-cell>
<van-cell title="自选" clickable
@click="formData.postMethod = '2';formData.receiveAddress=''">
<template #right-icon>
<van-radio name="2"></van-radio>
</template>
</van-cell>
</van-cell-group>
</van-radio-group>
<template>
<van-field
v-if="formData.postMethod === '2'"
v-model="formData.receiveAddress"
label="邮寄地址"
placeholder="请选择邮寄地址"
readonly
type="textarea"
autosize
rows="2"
is-link
@click="editAddressPopup = true"
required
></van-field>
<van-cell title="邮寄地址" v-else>{{formData.receiveAddress}}</van-cell>
</template>
</van-cell-group>
<!-- <van-cell-group title="基础信息">
<div class="van-card-body">
<van-field
@click="campusVisible = true"
readonly
is-link
label="院区"
name="campus"
placeholder="请选择体检院区"
v-model="formData.campusName"
:rules="[{ required: true, message: '请选择体检院区' }]">
</van-field>
<van-field
:rules="[{ required: true, message: '请选择家属是否体检' }]"
label="家属体检"
name="isFamily">
<template #input>
<van-radio-group direction="horizontal" v-model="formData.isFamily">
<van-radio name="是" shape="square">是</van-radio>
<van-radio name="否" shape="square">否</van-radio>
</van-radio-group>
</template>
</van-field>
</div>
</van-cell-group>-->
<van-cell-group title="家属信息" v-if="formData.isFamily === '是'">
<div style="padding: 10px;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;">
<span>家属信息</span>
<div>
<van-tag @click="delCompanion" size="large" type="primary" color="lightgrey">
删除家属
</van-tag>
<van-tag @click="addCompanion" size="large" type="primary" color="#1867b0">
添加家属
</van-tag>
</div>
</div>
<div v-if="formData.companionList && formData.companionList.length > 0">
<van-tabs v-model="active" type="card" color="#0e78c5" animated>
<van-tab
v-for="(item, index) in formData.companionList"
:key="index"
:title="'家属' + (index + 1)">
<van-field
label="姓名"
name="userName"
placeholder="请填写姓名"
v-model="item.userName"
:rules="[{ required: true, message: '请填写姓名' }]">
</van-field>
<van-field
:rules="[{ required: true, message: '请选择性别' }]"
label="性别"
name="sex">
<template #input>
<van-radio-group direction="horizontal" v-model="item.sex">
<van-radio name="男" shape="square">男</van-radio>
<van-radio name="女" shape="square">女</van-radio>
</van-radio-group>
</template>
</van-field>
<van-field
:rules="[{ required: true, message: '请选择婚否' }]"
label="婚否"
name="marry">
<template #input>
<van-radio-group direction="horizontal" v-model="item.marry">
<van-radio name="未婚" shape="square">未婚</van-radio>
<van-radio name="已婚" shape="square">已婚</van-radio>
</van-radio-group>
</template>
</van-field>
<van-field
label="年龄"
name="age"
placeholder="请填写年龄"
type="digit"
:rules="[{ required: true, message: '请填写年龄' }]"
v-model="item.age">
</van-field>
<van-field
label="身份证号"
name="idCard"
placeholder="请填写身份证号"
type="digit"
:rules="[{ required: true, message: '请填写身份证号' }]"
v-model="item.idCard">
</van-field>
<van-field
label="手机号"
name="mobile"
placeholder="请填写手机号"
type="digit"
:rules="[{ required: true, message: '请填写手机号' }]"
v-model="item.mobile">
</van-field>
</van-tab>
</van-tabs>
</div>
<div v-else class="empty_text">
<span>如有家属,请添加家属</span>
</div>
</div>
</van-cell-group>
<!-- 按钮区域 -->
<div class="form-actions">
<van-button type="primary" native-type="submit">提交</van-button>
</div>
</van-form>
</div>
<!--体检院区弹出框-->
<van-action-sheet title="体检院区" v-model="campusVisible">
<van-picker
title="院区"
show-toolbar
:columns="campusList"
value-key="campusName"
@confirm="onCampusConfirm"
@cancel="campusVisible = false">
</van-picker>
</van-action-sheet>
<!--查看说明-->
<van-action-sheet title="医院说明" v-model="descVisible" :style="{ height: '60%' }">
<div class="form-container" style="margin: 10px;">
<van-cell-group title="说明"></van-cell-group>
<div v-html="desc" class="pre_text"></div>
<div style="margin-top: 10px">
<van-cell-group title="以下内容由医院方提供"></van-cell-group>
<file-preview :files="files" complete_result></file-preview>
</div>
</div>
</van-action-sheet>
<!-- 收货地址选择弹窗 -->
<van-action-sheet v-model="editAddressPopup" title="选择收货地址" :round="true">
<div style="padding: 16px 0 0">
<div
v-for="address in addressOptions"
:key="address.id"
class="address-item"
@click="selectAddress(address)"
style="padding: 12px 16px; border-bottom: 1px solid #f0f0f0; cursor: pointer"
>
<div style="display: flex; justify-content: space-between; align-items: flex-start">
<div style="flex: 1">
<div style="font-size: 16px; font-weight: 500; color: #323233; margin-bottom: 4px">
{{ address.userName }} {{ address.tel }}
</div>
<div style="font-size: 14px; color: #646566; line-height: 1.4">
{{ address.province }}{{ address.city }}{{ address.county }}{{ address.addressDetail
}}
</div>
</div>
<van-tag v-if="address.isDefault" type="primary" size="mini" style="margin-left: 8px">默认
</van-tag>
</div>
</div>
<div v-if="addressOptions.length === 0"
style="text-align: center; padding: 40px 16px; color: #969799">暂无收货地址
</div>
</div>
<div style="padding: 16px; border-top: 1px solid #f0f0f0; background: #fafafa">
<van-button type="info" block round @click="goToAddressManage">收货地址管理</van-button>
</div>
</van-action-sheet>
</div>
<script nonce="${cspNonce!}">
const vue = new Vue({
el: "#app",
store,
dicts: ["ASSET_USAGE_STATE"],
data() {
return {
viewData: {},
formData: {
subjectId: '',
campus: '',
campusName: '',
isFamily: '否',
companionList: [],
projectId: '',
postMethod: ''
},
assetUsageStateNameOption: [],
showAssetUsageStateNamePopup: false,
assetUseUserOption: [],
assetUseNameShow: false,
searchKeyword: "",
active: 0,
campusList: [],
campusVisible: false,
descVisible: false,
desc: '',
files: [],
project: {},
marriageList: ["已婚", "未婚"],
marriageVisible: false,
editAddressPopup: false,
addressOptions: []
}
},
methods: {
// 选择收货地址
selectAddress(address) {
this.formData.receiveAddress =
address.userName + " " + address.tel + " " + address.province + address.city + address.county + address.addressDetail
this.editAddressPopup = false
},
// 跳转到地址管理页面
goToAddressManage() {
this.$pjaxReplace("/platform/h5/welfare/address/list")
},
// 获取收货地址
getAddress() {
this.$axios.post("/platform/welfare/addressManage/selectUserAddress").then(resp => {
if (resp.code === 0) {
this.addressOptions = resp.data
}
})
},
isEmptyObject(obj) {
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
return false
}
}
return true
},
addressClick() {
},
// 打开表单
async initData() {
await this.getCampus()
this.getAddress()
await this.findSubject(GetQueryString("projectId"))
if (GetQueryString("projectId")) {
await this.selectInfo(GetQueryString("projectId"));
const {idCard, userState, marriage, birthday, unit} = this.$store.state.user
this.$set(this.formData, 'projectId', GetQueryString("projectId"))
this.$set(this.formData, 'idCard', idCard)
this.$set(this.formData, 'userState', userState)
this.$set(this.formData, 'marriage', marriage)
this.$set(this.formData, 'birthday', birthday)
this.$set(this.formData, 'unitName', unit.name)
this.$set(this.formData, 'postMethod', this.formData?.postMethod ? this.formData.postMethod : "1")
this.$set(this.formData, 'receiveAddress', this.formData?.receiveAddress ? this.formData.receiveAddress : unit.name)
}
},
clickRadio(item) {
const toast = this.$toast.loading({
duration: 0, // 持续展示 toast
forbidClick: true,
message: '查询中....',
});
$.post("/platform/healthCheckup/h5/getSubjectMoney", {
id: item.id,
marriage: this.formData.marriage
}).then(resp => {
if (resp.code === 0) {
this.$set(this.formData, 'subjectId', item.id)
this.$set(this.formData, 'money', resp.data.money)
this.$set(this.formData, 'subjectMoneyId', resp.data.subjectMoneyId)
} else {
this.$set(this.formData, 'money', 0)
}
toast.clear();
})
},
async selectInfo(projectId) {
const resp = await this.$axios.post('/platform/healthCheckup/h5/selectInfo', {projectId: projectId});
if (resp.data !== null) {
this.formData = resp.data;
/* const o = this.campusList.find(o => o.id === this.formData.campus)
this.formData.campusName = o.campusName*/
}
},
// 查看说明
viewDesc(item) {
if (item.description || item.files) {
this.files = item.files;
this.desc = item.description;
this.descVisible = true;
} else {
vant.Toast('暂无详细说明');
}
},
// 执行提交
async doSubmit() {
const formData = clone(this.formData);
if (!formData.receiveAddress) {
this.$toast.fail('请填写邮寄地址!')
}
const subject = this.project.healthCheckupProjectSubjects.find(v => v.id === formData.subjectId)
this.$dialog.confirm({
title: '温馨提示',
message: '您确定要提交选择的【' + subject.optionName + '】吗?',
}).then(() => {
const toast = this.$toast.loading({
duration: 0, // 持续展示 toast
forbidClick: true,
message: '提交中....',
});
/* if (formData.isFamily === '否') {
formData.companionList = [];
} else {
// 过滤空的家属信息
formData.companionList = formData.companionList.filter(item => item.userName && item.userName.trim() !== '');
}*/
this.$axios.post('/platform/healthCheckup/h5/doSubmit', {
userSelection: JSON.stringify(formData),
}).then(resp => {
if (resp.code === 0) {
toast.clear();
this.$toast.success(resp.msg)
this.historyBack()
}
})
}).catch(() => {
});
},
// 添加家属
addCompanion() {
// 检查是否已达到最大数量限制
if (this.companionList && this.formData.companionList.length >= 5) {
vant.Toast('最多只能添加5个家属');
return;
}
if (!this.formData.companionList) {
this.$set(this.formData, 'companionList', []);
}
this.formData.companionList.push({
userName: '', sex: '', marry: '', age: '', idCard: '', mobile: ''
});
},
// 删除家属
delCompanion() {
if (this.formData.companionList && this.formData.companionList.length > 0) {
this.formData.companionList.splice(this.active, 1);
// 确保active索引有效
if (this.active >= this.formData.companionList.length && this.active > 0) {
this.active = this.formData.companionList.length - 1;
}
}
},
// 院区选择确认
onCampusConfirm(value) {
this.formData.campusName = value.campusName;
this.formData.campus = value.id;
this.campusVisible = false;
},
// 婚姻状况确认
onMarriageConfirm(value) {
this.$set(this.formData, 'marriage', value)
this.$set(this.formData, 'subjectId', null)
this.marriageVisible = false;
},
// 查询体检套餐
async findSubject(id) {
try {
const resp = await this.$axios.post('/platform/healthCheckup/project/mange/findOne', {
id: id
});
this.project = resp.data || {};
} catch (error) {
vant.Toast('获取体检套餐失败');
}
},
// 获取院区
async getCampus() {
try {
const resp = await this.$axios.post('/platform/healthCheckup/h5/getCampus');
this.campusList = resp.data || [];
} catch (error) {
vant.Toast('获取院区失败');
}
}
},
created() {
this.initData()
}
})
</script>
<!--#
}
#-->
@@ -0,0 +1,494 @@
<!--#
layout("/layouts/platform_h5.html"){
#-->
<div id="app" v-cloak>
<div class="honor-showcase">
<van-nav-bar
title="劳模先进"
left-text="返回"
left-arrow
fixed
placeholder
@click-left="goBack"
></van-nav-bar>
<div class="honor-tabs">
<button
type="button"
class="honor-tab"
:class="{ active: activeTab === 'labor' }"
@click="switchTab('labor')">
劳模风采
</button>
<button
type="button"
class="honor-tab"
:class="{ active: activeTab === 'studio' }"
@click="switchTab('studio')">
创新工作室
</button>
</div>
<section class="honor-intro">
<div class="honor-intro__corner honor-intro__corner--left"></div>
<div class="honor-intro__corner honor-intro__corner--right"></div>
<p>一份坚守,淬炼出奋斗的生命价值;一份专注,迸发不断创新的发展动力。</p>
<p>他们扎根岗位、追求卓越,以实干书写担当,用匠心点亮前行方向。</p>
<p>致敬劳模先进,礼赞奋斗精神。</p>
</section>
<van-loading v-if="loading" class="honor-loading" color="#fff">加载中...</van-loading>
<div v-else class="honor-list">
<article v-for="item in honorList" :key="item.id" class="honor-card">
<div class="honor-card__image-wrap">
<img v-if="item.cover" class="honor-card__image" :src="item.cover" alt="">
<div v-else class="honor-card__placeholder">
<div class="honor-card__placeholder-title">{{ getDisplayName(item) }}</div>
<div class="honor-card__placeholder-sub">劳模先进</div>
</div>
<div class="honor-card__name">{{ getDisplayName(item) }}</div>
</div>
<div class="honor-card__body">
<div class="honor-card__gold-line"></div>
<div class="honor-card__prize">《{{ item.prizeName || '荣誉奖项' }}》</div>
<div class="honor-card__date" v-if="item.grantDate">授予时间:{{ item.grantDate }}</div>
</div>
</article>
<van-empty v-if="honorList.length === 0" image="search" description="暂无当年荣誉"></van-empty>
</div>
</div>
</div>
<script nonce="${cspNonce!}">
new Vue({
el: "#app",
data: function () {
return {
activeTab: "labor",
currentYear: new Date().getFullYear(),
honorTypeList: [],
honorTypeMap: {
labor: null,
studio: null
},
honorList: [],
loading: false
}
},
methods: {
goBack: function () {
this.returnHome()
},
returnHome: function () {
if (window.store && typeof window.store.commit === "function") {
window.store.commit("setActiveTarBar", "home")
}
if (!window.$ || !$.support || !$.support.pjax) {
window.location.replace("/platform/h5/home")
return
}
var fallbackTimer = window.setTimeout(function () {
window.location.replace("/platform/h5/home")
}, 1200)
$(document).one("pjax:complete", function () {
window.clearTimeout(fallbackTimer)
})
$(document).one("pjax:error pjax:timeout", function () {
window.clearTimeout(fallbackTimer)
window.location.replace("/platform/h5/home")
})
$.pjax({
url: "/platform/h5/home",
container: "#container",
fragment: "#container",
push: false,
replace: true,
timeout: 8000
})
},
switchTab: function (tab) {
if (this.activeTab === tab) {
return
}
this.activeTab = tab
this.listHonor()
},
getDisplayName: function (item) {
if (this.activeTab === "studio") {
return item.applyUnionName || item.unionName || item.userName || "创新工作室"
}
return item.userName || item.applyUnionName || item.unionName || "劳模先进"
},
getHonorTypes: function () {
var self = this
return this.$axios.post("/platform/honor/basic/settings/getHonorType").then(function (res) {
if (res.code === 0) {
self.honorTypeList = res.data || []
self.honorTypeMap.labor = self.findHonorType("HONOR_SINGLE", "个人荣誉")
self.honorTypeMap.studio = self.findHonorType("HONOR_LIST", "集体荣誉")
}
})
},
findHonorType: function (queryTypeCode, name) {
var target = this.honorTypeList.find(function (item) {
return item.queryTypeCode === queryTypeCode
})
if (!target) {
target = this.honorTypeList.find(function (item) {
return item.name === name
})
}
return target || null
},
listHonor: function () {
var self = this
var type = this.activeTab === "studio" ? this.honorTypeMap.studio : this.honorTypeMap.labor
if (!type) {
this.honorList = []
return
}
this.loading = true
this.$axios.post("/platform/honor/manage/h5Data", {
year: this.currentYear,
honorType: type.id,
pageNumber: 1,
pageSize: 50
}).then(function (res) {
if (res.code === 0) {
var list = (res.data && res.data.list) || []
self.honorList = list.map(function (item) {
item.cover = ""
item.photoId = self.getFirstPhotoId(item.photoFiles)
return item
})
self.loadPhotoMap()
} else {
self.honorList = []
}
}).finally(function () {
self.loading = false
})
},
getFirstPhotoId: function (photoFiles) {
var files = []
if (Array.isArray(photoFiles)) {
files = photoFiles
} else if (photoFiles) {
try {
files = JSON.parse(photoFiles)
} catch (e) {
files = []
}
}
if (!files.length) {
return ""
}
var first = files[0]
var data = first && first.response ? first.response.data : first
if (!data) {
return ""
}
data = String(data)
return data.indexOf("=") > -1 ? data.substring(data.lastIndexOf("=") + 1) : data
},
loadPhotoMap: function () {
var self = this
var ids = this.honorList.map(function (item) {
return item.photoId
}).filter(function (id) {
return !!id
})
if (!ids.length) {
return
}
this.$axios.post("/platform/sys/file/previewFileData", { ids: JSON.stringify(ids) }).then(function (res) {
if (res.code !== 0) {
return
}
var fileMap = {}
;(res.data || []).forEach(function (file) {
fileMap[file.id] = file.thumbnail || file.downloadPath || ""
})
self.honorList = self.honorList.map(function (item) {
item.cover = fileMap[item.photoId] || ""
return item
})
})
}
},
created: function () {
var self = this
this.getHonorTypes().then(function () {
self.listHonor()
})
},
style: /*language=CSS*/ `
body {
background: #c80019;
}
.honor-showcase {
min-height: 100vh;
padding: 0 14px 28px;
box-sizing: border-box;
background:
radial-gradient(circle at 50% 0, rgba(255, 113, 89, .42) 0, rgba(255, 113, 89, 0) 32%),
linear-gradient(180deg, #d6001c 0%, #bd0018 56%, #a90016 100%);
color: #fff;
}
.honor-showcase /deep/ .van-nav-bar {
background: #fff;
}
.honor-showcase /deep/ .van-nav-bar__title {
color: #20242a;
font-weight: 700;
}
.honor-showcase /deep/ .van-nav-bar .van-icon,
.honor-showcase /deep/ .van-nav-bar__text {
color: #1989fa;
}
.honor-showcase /deep/ .van-hairline--bottom::after {
border-color: #eef1f6;
}
.honor-tabs {
display: flex;
justify-content: space-around;
align-items: center;
height: 76px;
}
.honor-tab {
position: relative;
border: 0;
background: transparent;
color: rgba(255, 247, 220, .76);
font-size: 19px;
font-weight: 700;
line-height: 28px;
padding: 0 8px 14px;
outline: none;
}
.honor-tab.active {
color: #fff6d9;
}
.honor-tab.active::after {
content: "";
position: absolute;
left: 50%;
bottom: 4px;
width: 38px;
height: 5px;
border-radius: 999px;
background: #ffe6a3;
transform: translateX(-50%);
box-shadow: 0 2px 8px rgba(255, 230, 163, .38);
}
.honor-intro {
position: relative;
margin-bottom: 18px;
padding: 20px 20px 46px;
border-radius: 8px;
overflow: hidden;
color: #ffe9bd;
background:
linear-gradient(165deg, rgba(255, 255, 255, .12), rgba(255, 255, 255, 0) 46%),
linear-gradient(180deg, #d73535 0%, #c9262f 100%);
box-shadow: 0 10px 22px rgba(90, 0, 8, .22);
}
.honor-intro::before,
.honor-intro::after {
content: "";
position: absolute;
left: -30px;
right: -30px;
height: 58px;
border: 5px solid rgba(255, 223, 151, .86);
border-top: 0;
border-radius: 0 0 50% 50%;
pointer-events: none;
}
.honor-intro::before {
bottom: 14px;
transform: rotate(-2deg);
}
.honor-intro::after {
bottom: -12px;
opacity: .75;
transform: rotate(2deg);
}
.honor-intro p {
position: relative;
z-index: 1;
margin: 0 0 9px;
font-size: 14px;
line-height: 24px;
}
.honor-intro p:last-child {
margin-bottom: 0;
}
.honor-intro__corner {
position: absolute;
top: 14px;
width: 24px;
height: 24px;
border-top: 3px solid rgba(255, 224, 161, .82);
}
.honor-intro__corner--left {
left: 14px;
border-left: 3px solid rgba(255, 224, 161, .82);
}
.honor-intro__corner--right {
right: 14px;
border-right: 3px solid rgba(255, 224, 161, .82);
}
.honor-loading {
margin-top: 36px;
text-align: center;
}
.honor-list {
padding-bottom: 8px;
}
.honor-card {
margin-bottom: 18px;
border-radius: 8px;
overflow: hidden;
background: #be0018;
box-shadow: 0 10px 22px rgba(83, 0, 9, .28);
}
.honor-card__image-wrap {
position: relative;
height: 206px;
overflow: hidden;
background: #e6b06d;
}
.honor-card__image {
display: block;
width: 100%;
height: 100%;
object-fit: cover;
}
.honor-card__placeholder {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
color: #fff5d8;
background:
radial-gradient(circle at 78% 30%, rgba(255, 221, 148, .36), rgba(255, 221, 148, 0) 26%),
linear-gradient(140deg, #d93a31 0%, #b60018 100%);
}
.honor-card__placeholder-title {
font-size: 24px;
font-weight: 700;
}
.honor-card__placeholder-sub {
margin-top: 10px;
font-size: 14px;
letter-spacing: 0;
color: rgba(255, 245, 216, .82);
}
.honor-card__image-wrap::after {
content: "";
position: absolute;
left: 0;
right: 0;
bottom: 0;
height: 58px;
background: linear-gradient(165deg, rgba(177, 0, 24, .86) 0%, rgba(177, 0, 24, .74) 62%, rgba(177, 0, 24, .1) 63%);
}
.honor-card__name {
position: absolute;
left: 20px;
bottom: 16px;
z-index: 1;
color: #fff;
font-size: 22px;
line-height: 28px;
font-weight: 800;
text-shadow: 0 2px 8px rgba(89, 0, 12, .32);
}
.honor-card__body {
position: relative;
min-height: 82px;
padding: 22px 20px 20px;
box-sizing: border-box;
color: #ffe5ad;
background:
radial-gradient(circle at 92% 14%, rgba(255, 230, 168, .4), rgba(255, 230, 168, 0) 16%),
#be0018;
}
.honor-card__body::after {
content: "★";
position: absolute;
right: 18px;
top: -20px;
color: #ffd886;
font-size: 42px;
text-shadow: 0 3px 8px rgba(85, 0, 9, .28);
transform: rotate(-12deg);
}
.honor-card__gold-line {
width: 44px;
height: 3px;
border-radius: 999px;
background: #e7b13c;
margin-bottom: 12px;
}
.honor-card__prize {
font-size: 15px;
line-height: 22px;
font-weight: 700;
color: #ffe5ad;
}
.honor-card__date {
margin-top: 5px;
font-size: 12px;
line-height: 18px;
color: rgba(255, 229, 173, .72);
}
.honor-showcase /deep/ .van-empty__description {
color: rgba(255, 255, 255, .78);
}
`
})
</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/literacy/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/literacy/apply/list/h5?id=' + row.id)
},
fetchOne() {
this.$axios.post('/platform/literacy/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/literacy/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/literacy/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/literacy/apply/validateSignUp", {
courseId: this.formData.courseId,
currentFamilyNumber: familyCount
})
return res.code === 0
},
async validateCourseTime() {
const res = await this.$axios.post('/platform/literacy/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/literacy/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,index in assortList" :name="item" :title="item" :key="index">
</van-tab>
</van-tabs>
<table-list api="/platform/literacy/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/literacy/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/literacy/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/literacy/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/literacy/manage/findOne', {id: this.pageForm.activityId})
.then((res) => {
this.activity = res.data
})
},
async getCourseTypeList() {
const resp = await this.$axios.post("/platform/literacy/type/getAllType")
return resp.data
},
queryCourseAssort() {
this.$axios.post("/platform/literacy/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/literacy/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/literacy/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,77 @@
const leaveInfo = {
template:
/*language=HTML*/
`
<van-action-sheet v-model="visible" title="查看详情">
<div class="detail-container">
<van-cell-group title="基本信息">
<van-cell title="请假人姓名">{{ viewData.userName }}</van-cell>
<van-cell title="请假人工号">{{ viewData.loginName }}</van-cell>
<van-cell title="所属单位">{{ viewData.unitName }}</van-cell>
<van-cell title="所属工会">{{ viewData.unionName }}</van-cell>
<van-cell title="会议名称">{{ viewData.periodName }}</van-cell>
<van-cell title="开始时间">{{ viewData.startTime }}</van-cell>
<van-cell title="结束时间">{{ viewData.endTime }}</van-cell>
<van-cell title="请假事由" class="direction-column-cell">{{ viewData.leaveReason }}</van-cell>
<van-cell title="所属会议">{{ viewData.meetingName }}</van-cell>
<van-cell title="会议类型">{{ viewData.typeName }}</van-cell>
</van-cell-group>
<template v-for="task in doneTasks">
<van-cell-group :title="task.displayName" v-if="task.ext.isFirstTaskNode">
<van-cell title="申请用户">{{ task.ext.initiatorName }}({{task.ext.initiatorAccount}})
</van-cell>
<van-cell title="申请时间">{{ task.finishTime }}</van-cell>
<van-cell title="办理结果">
<dict-tag :options="dict.type.PROCESS_TASK_SUBMIT_TYPE"
:value="task.ext.submitType"></dict-tag>
</van-cell>
</van-cell-group>
<van-cell-group :title="task.displayName" v-else>
<van-cell title="办理用户">{{ task.taskFormData.userName }}({{task.taskFormData.loginName}})
</van-cell>
<van-cell title="办理时间">{{ task.finishTime }}</van-cell>
<van-cell title="办理结果">
<dict-tag :options="dict.type.PROCESS_TASK_SUBMIT_TYPE"
:value="task.ext.submitType"></dict-tag>
</van-cell>
<van-cell title="办理意见" v-if="!task.ext.isFirstTaskNode" class="direction-column-cell">
<div v-html="task.taskFormData.tf_opinion"></div>
</van-cell>
</van-cell-group>
</template>
</div>
<slot></slot>
</van-action-sheet>
`,
dicts: ["PROCESS_TASK_SUBMIT_TYPE"],
data() {
return {
visible:false,
viewData: {},
doneTasks: [],
row: null,
}
},
methods: {
onOpen(row) {
row.leaveReason = JSON.parse(row.instanceVariable).f_data.leaveReason || ''
this.row = row
this.visible = true
this.viewData = clone(row)
this.getDoneTasks()
},
onClose(){
this.visible = false
},
getDoneTasks() {
this.$axios.post("/flow/common/doneTasks", {bizId: this.row.id}).then((res) => {
if (res.code === 0) {
this.doneTasks = res.data
}
})
},
},
style: /*language=CSS*/ `
`
}
@@ -0,0 +1,96 @@
const meetingInfo = {
template: /*language=HTML*/ `
<div>
<van-action-sheet v-model="visible" title="会议信息">
<div class="detail-container">
<van-cell-group title="基本信息">
<van-cell title="会议名称">{{ viewData.name }}</van-cell>
<van-cell title="会议地点">{{ viewData.address }}</van-cell>
<van-cell title="会议类型">{{ viewData.typeName }}</van-cell>
<van-cell title="会议描述" class="direction-column-cell">
<span v-html="viewData.description"></span>
</van-cell>
</van-cell-group>
<van-cell-group title="场次信息" v-if="viewData.timePeriods && viewData.timePeriods.length > 0">
<table class="table-class">
<thead>
<tr>
<th>场次名称</th>
<th>开始时间</th>
<th>结束时间</th>
<th>是否可以请假</th>
</tr>
</thead>
<tbody>
<tr v-for="(item,index) in viewData.timePeriods" :key="index">
<td>{{ item.periodName }}</td>
<td>{{ item.startTime }}</td>
<td>{{ item.endTime }}</td>
<td>{{ item.canLeave ? '是' : '否' }}</td>
</tr>
</tbody>
</table>
</van-cell-group>
<van-cell-group title="会议参与人员" v-if="viewData.users && viewData.users.length > 0">
<table class="table-class">
<thead>
<tr>
<th>工号</th>
<th>姓名</th>
<th>性别</th>
<th>所属单位</th>
</tr>
</thead>
<tbody>
<tr v-for="(item,index) in viewData.users" :key="index">
<td>{{ item.loginName }}</td>
<td>{{ item.userName }}</td>
<td>{{ item.sex }}</td>
<td>{{ item.unitName }}</td>
</tr>
</tbody>
</table>
</van-cell-group>
</div>
</van-action-sheet>
</div>
`,
data() {
return {
viewData: {},
visible: false
}
},
methods: {
onOpen(row) {
this.$axios.post('/platform/meeting/manage/info', {id: row.id}).then((res) => {
if (res.code === 0) {
this.viewData = res.data
}
})
this.visible = true
},
},
style: /*language=CSS*/ `
/deep/ .table-class{
width: 100%;
border-radius: 5px;
overflow: hidden;
line-height: 1.5rem;
font-size: 13px;
table-layout: fixed;
border-collapse: collapse;
}
/deep/ .table-class th {
background-color: #f2f2f2;
border: 1px solid #dddddd;
}
/deep/ .table-class tr {
text-align: center;
border-bottom: 1px solid #dddddd;
}
/deep/ .table-class td {
border: 1px solid #dddddd;
}
`
}
@@ -0,0 +1,190 @@
<!--#
layout("/layouts/platform_h5.html"){
#-->
<style scoped>
</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-search
v-model="pageForm.searchKeyword"
:show-action="false"
:reverse-color="false"
input-align="left"
placeholder="请输入名称或地点查询"
@search="doSearch"
></van-search>
<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.type" :options="typeOptions" :multiple="false"
@change="doSearch"></van-dropdown-item>
</van-dropdown-menu>
<van-tabs v-model="pageForm.approvalText"
@change="(val)=>{this.pageForm.approval = val==='1';this.doSearch();}">
<van-tab title="已审核" name="1"></van-tab>
<van-tab title="未审核" name="0"></van-tab>
</van-tabs>
</van-sticky>
<table-list api="/platform/meeting/delegationApproval/pageData" :page_form.sync="pageForm" ref="tableListRef" title="periodName" @ready="onReady">
<template v-slot="{index,row}">
<table-column label="开始时间">{{row.startTime}}</table-column>
<table-column label="结束时间">{{row.endTime}}</table-column>
<table-column label="请假人">{{row.userName}}</table-column>
<table-column label="请假事由">{{JSON.parse(row.instanceVariable).f_data.leaveReason || ''}}</table-column>
<table-column label="所属会议">{{row.meetingName}}</table-column>
<table-column label="会议类型">{{row.typeName}}</table-column>
<table-column label="当前节点">{{row.curTaskName}}</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" v-if="row.taskState === 10" @click="onApproval(row)">
<i class="fa fa-edit"></i>
<span>审核</span>
</div>
<div class="action-btn delete" v-if="row.canRevoke" @click="onRevoke(row)">
<i class="fa fa-reply"></i>
<span>撤回</span>
</div>
</template>
</table-list>
<leave-info ref="infoRef">
<div v-if="showApprovalForm">
<div class="process-title">{{formData.taskName}}</div>
<van-form ref="formRef">
<van-field
v-model="formData.tf_opinion"
name="tf_opinion"
label="审批意见"
placeholder="请输入审批意见"
:rules="[{ required: true, message: '请填写审批意见' }]"
required
maxlength="100"
show-word-limit
></van-field>
</van-form>
<div style="display: flex; justify-content: space-between; column-gap: 10px; padding: 10px">
<van-button type="primary" block @click="handleTaskAction(1)">同意</van-button>
<van-button type="danger" block @click="handleTaskAction(20)">不同意</van-button>
<van-button type="danger" block @click="handleTaskAction(6)">退回</van-button>
</div>
</div>
</leave-info>
</div>
<script nonce="${cspNonce!}">
<!--#include('../common/leaveInfo.js'){}#-->
new Vue({
el: "#app",
store,
components: {
'leave-info': leaveInfo,
},
data() {
return {
pageForm: {
pageNumber: 1,
pageSize: 10,
totalCount: 0,
searchKeyword: '',
approvalText: "0",
approval: false,
year: new Date().getFullYear(),
type: null,
},
typeOptions: [],
formData: {},
showApprovalForm: false
}
},
methods: {
async onReady() {
const typeList = await this.queryMeetingType()
this.typeOptions = [
{
text: "全部类型",
value: null
}
].concat(typeList.map((v) => ({ text: v.name, value: v.id })))
if (this.typeOptions.length > 0) {
this.pageForm.type = this.typeOptions[0].value
this.doSearch()
}
},
onView(row) {
this.showApprovalForm = false
this.$refs.infoRef.onOpen(row)
},
onApproval(row) {
this.showApprovalForm = true
this.$refs.infoRef.onOpen(row)
this.formData = {
processTaskId: row.taskId,
taskName: row.curTaskName
}
},
async handleTaskAction(val) {
try {
await this.$refs.formRef.validate();
this.$dialog.confirm({
title: "提示",
message: "您确定要提交吗?"
}).then(() => {
this.$axios.post("/flow/common/executeTask", {
data: JSON.stringify({
...this.formData,
submitType: val
})
}).then((res) => {
if (res.code === 0) {
this.$refs.infoRef.onClose()
this.$toast.success(res.msg)
this.doSearch()
}
})
}).catch(() => {
})
} catch (error) {
}
},
onRevoke(row){
this.$dialog.confirm({
title: "提示",
message: "您确定要撤回吗?"
}).then(() => {
this.$axios.post("/flow/common/revokeTask", {taskId: row.taskId}).then((res) => {
if (res.code === 0) {
this.$toast.success(res.msg)
this.doSearch()
}
})
})
},
async queryMeetingType() {
const res = await this.$axios.post('/platform/meeting/type/queryMeetingType')
return res.data
},
doSearch() {
this.$nextTick(() => {
this.pageForm.pageNumber = 1
this.pageForm.totalCount = 0
this.$refs.tableListRef.doSearch()
})
}
},
})
</script>
<!--#
}
#-->
@@ -0,0 +1,197 @@
<!--#
layout("/layouts/platform_h5.html"){
#-->
<style scoped>
</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-search
v-model="pageForm.searchKeyword"
:show-action="false"
:reverse-color="false"
input-align="left"
placeholder="请输入名称或地点查询"
@search="doSearch"
></van-search>
<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.type" :options="typeOptions" :multiple="false"
@change="doSearch"></van-dropdown-item>
</van-dropdown-menu>
</van-sticky>
<table-list api="/platform/meeting/leave/pageData" :page_form.sync="pageForm" ref="tableListRef" title="periodName" @ready="onReady">
<template v-slot="{index,row}">
<table-column label="开始时间">{{row.startTime}}</table-column>
<table-column label="结束时间">{{row.endTime}}</table-column>
<table-column label="请假人">{{row.userName}}</table-column>
<table-column label="请假事由">{{JSON.parse(row.instanceVariable).f_data.leaveReason || ''}}</table-column>
<table-column label="所属会议">{{row.meetingName}}</table-column>
<table-column label="会议类型">{{row.typeName}}</table-column>
<table-column label="当前节点">{{row.taskName}}</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="onEdit(row)"
v-if="row.taskKey === 'startTask' || !row.instanceId">
<i class="fa fa-edit"></i>
<span>编辑</span>
</div>
<div class="action-btn delete" @click="onRevoke(row)"
v-if="row.canRevoke">
<i class="fa fa-undo"></i>
<span>撤回</span>
</div>
<div class="action-btn delete" @click="onDelete(row)"
v-if="row.taskKey === 'startTask' || !row.instanceId">
<i class="fa fa-trash"></i>
<span>删除</span>
</div>
</template>
</table-list>
<van-dialog v-model="visible" title="请假事由" show-cancel-button
:before-close="handleBeforeClose">
<van-field
style="padding: 20px 16px"
v-model="leaveReason"
label="请假事由"
placeholder="请输入请假事由"
required
rows="1"
autosize
type="textarea"
></van-field>
</van-dialog>
<leave-info ref="infoRef"></leave-info>
</div>
<script nonce="${cspNonce!}">
<!--#include('../common/leaveInfo.js'){}#-->
new Vue({
el: "#app",
store,
components: {
'leave-info': leaveInfo,
},
data() {
return {
pageForm: {
pageNumber: 1,
pageSize: 10,
totalCount: 0,
year: new Date().getFullYear(),
type: null
},
typeOptions: [],
visible: false,
leaveReason: '',
editRow: {},
}
},
methods: {
async onReady() {
const typeList = await this.queryMeetingType()
this.typeOptions = [
{
text: "全部类型",
value: null
}
].concat(typeList.map((v) => ({ text: v.name, value: v.id })))
if (this.typeOptions.length > 0) {
this.pageForm.type = this.typeOptions[0].value
this.doSearch()
}
},
handleBeforeClose(action, done) {
if (action === 'confirm') {
if(!this.leaveReason) {
this.$toast('请输入请假事由')
done(false)
} else {
this.$axios.post("/platform/meeting/mine/submitAgain", {
id: this.editRow.id,
leaveReason: this.leaveReason,
taskId: this.editRow.taskId
})
.then((res) => {
done()
this.$toast(res.msg)
if (res.code === 0) {
this.doSearch()
this.reasonVisible = false
}
}).catch(() => { done() })
}
} else {
done()
}
},
onRevoke(row) {
this.$dialog.confirm({
title: "提示",
message: "您确定要撤回吗?"
}).then(() => {
this.$axios.post("/flow/common/revokeTask", { taskId: row.startTaskId }).then((res) => {
if (res.code === 0) {
this.$toast(res.msg)
this.doSearch()
}
})
})
},
onView(row) {
this.$refs.infoRef.onOpen(row)
},
onEdit(row) {
if(!row.canLeave) {
this.$toast.success('该场次不能请假')
return
}
this.editRow = row
this.visible = true
},
onDelete(row) {
this.$dialog.confirm({
title: "提示",
message: "确定要删除此申请吗?"
})
.then(() => {
this.$axios.post("/platform/meeting/leave/delete", { id: row.id }).then((res) => {
if (res.code === 0) {
this.$toast.success(res.msg)
this.doSearch()
} else {
this.$toast.fail(res.msg)
}
})
})
.catch(() => {})
},
async queryMeetingType() {
const res = await this.$axios.post('/platform/meeting/type/queryMeetingType')
return res.data
},
doSearch() {
this.$nextTick(() => {
this.pageForm.pageNumber = 1
this.pageForm.totalCount = 0
this.$refs.tableListRef.doSearch()
})
}
},
})
</script>
<!--#
}
#-->
@@ -0,0 +1,224 @@
<!--#
layout("/layouts/platform_h5.html"){
#-->
<style scoped>
.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-search
v-model="pageForm.searchKeyword"
:show-action="false"
:reverse-color="false"
input-align="left"
placeholder="请输入名称或地点查询"
@search="doSearch"
></van-search>
<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.type" :options="typeOptions" :multiple="false"
@change="doSearch"></van-dropdown-item>
</van-dropdown-menu>
</van-sticky>
<table-list api="/platform/meeting/mine/pageData" :page_form.sync="pageForm" ref="tableListRef" title="name" @ready="onReady">
<template v-slot="{index,row}">
<table-column label="会议类型">{{row.typeName}}</table-column>
<table-column label="会议地点">{{row.address}}</table-column>
<table-column label="创建时间">{{row.createTime}}</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="leaveRow = row; leaveVisible = true">
<i class="fa fa-edit"></i>
<span>展开</span>
</div>
</template>
</table-list>
<van-action-sheet title="会议信息" v-model="leaveVisible" cancel-text="取消" close-on-click-action>
<button v-for="(item,index) in leaveRow.timePeriods" 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">{{item.periodName}}</div>
<div class="van-action-sheet__subname">
{{$moment(item.startTime).format('MM-DD HH:mm') + ' 至 ' + $moment(item.endTime).format('MM-DD HH:mm')}}
</div>
</div>
<div class="sign_button">
<van-button v-if="item.joinStatus === true"
:disabled="leaveLoading || isMeetingHandled(item)" :loading="leaveLoading"
@click.stop="onLeave(item)" size="mini" type="info">请假</van-button>
<van-button v-if="item.joinStatus === false" size="mini" type="info" disabled>您已请假</van-button>
<van-button v-if="item.signStatus === false"
:disabled="leaveLoading || isMeetingHandled(item) || isSignDisabled(item.startTime)"
@click.stop="onSign(item)" size="mini" type="info">签到</van-button>
<van-button v-if="item.signStatus === true" size="mini" type="info" disabled>您已签到</van-button>
</div>
</div>
</button>
</van-action-sheet>
<van-dialog v-model="reasonVisible" title="请假事由" show-cancel-button
:before-close="handleBeforeClose">
<van-field
style="padding: 20px 16px"
v-model="leaveReason"
label="请假事由"
placeholder="请输入请假事由"
required
rows="1"
autosize
type="textarea"
></van-field>
</van-dialog>
<meeting-info ref="infoRef"></meeting-info>
</div>
<script nonce="${cspNonce!}">
<!--#include('../common/meetingInfo.js'){}#-->
new Vue({
el: "#app",
store,
components: {
'meeting-info': meetingInfo,
},
data() {
return {
pageForm: {
pageNumber: 1,
pageSize: 10,
totalCount: 0,
year: new Date().getFullYear(),
type: null
},
typeOptions: [],
leaveRow: {},
leaveVisible: false,
reasonVisible: false,
leaveReason: '',
periodRow: {},
leaveLoading: false,
}
},
methods: {
// 已请假或已签到后同时禁用请假、签到按钮,保证移动端与PC端状态一致。
isMeetingHandled(timePeriod) {
return timePeriod.joinStatus === false || timePeriod.signStatus === true
},
// 场次开始前禁用签到,避免用户在会议开始前提交签到操作。
isSignDisabled(startTime) {
return this.$moment().isBefore(this.$moment(startTime))
},
async onReady() {
const typeList = await this.queryMeetingType()
this.typeOptions = [
{
text: "全部类型",
value: null
}
].concat(typeList.map((v) => ({ text: v.name, value: v.id })))
if (this.typeOptions.length > 0) {
this.pageForm.siteType = this.typeOptions[0].value
this.doSearch()
}
},
handleBeforeClose(action, done) {
if (action === 'confirm') {
if(!this.leaveReason) {
this.$toast('请输入请假事由')
done(false)
} else {
this.leaveLoading = true
this.$axios.post("/platform/meeting/mine/leave", {
periodId: this.periodRow.id,
leaveReason: this.leaveReason,
})
.then((res) => {
this.$toast(res.msg)
if (res.code === 0) {
done()
this.doSearch()
this.reasonVisible = false
this.leaveVisible = false
} else {
done(false)
}
}).catch(() => {
done(false)
}).finally(() => {
// 请假请求结束后恢复按钮状态,接口失败时允许用户修正事由后重新提交。
this.leaveLoading = false
})
}
} else {
done()
}
},
onLeave(row) {
if (this.isMeetingHandled(row)) {
this.$toast(row.signStatus === true ? '您已签到,不能请假' : '您已请假,请勿重复操作')
return
}
if(!row.canLeave) {
this.$toast('该场次不能请假')
return
}
this.periodRow = row
this.reasonVisible = true
},
onSign(row) {
if (this.isMeetingHandled(row)) {
this.$toast(row.signStatus === true ? '您已签到,请勿重复操作' : '您已请假,不能签到')
return
}
this.$dialog.confirm({
title: "提示",
message: "您确定要签到吗?"
}).then(() => {
this.$axios.post("/platform/meeting/mine/sign", { periodId: row.id })
.then((res) => {
this.$toast(res.msg)
if (res.code === 0) {
this.doSearch()
this.leaveVisible = false
}
})
})
},
onView(row) {
this.$refs.infoRef.onOpen(row)
},
async queryMeetingType() {
const res = await this.$axios.post('/platform/meeting/type/queryMeetingType')
return res.data
},
doSearch() {
this.$nextTick(() => {
this.pageForm.pageNumber = 1
this.pageForm.totalCount = 0
this.$refs.tableListRef.doSearch()
})
}
},
})
</script>
<!--#
}
#-->
@@ -0,0 +1,190 @@
<!--#
layout("/layouts/platform_h5.html"){
#-->
<style scoped>
</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-search
v-model="pageForm.searchKeyword"
:show-action="false"
:reverse-color="false"
input-align="left"
placeholder="请输入名称或地点查询"
@search="doSearch"
></van-search>
<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.type" :options="typeOptions" :multiple="false"
@change="doSearch"></van-dropdown-item>
</van-dropdown-menu>
<van-tabs v-model="pageForm.approvalText"
@change="(val)=>{this.pageForm.approval = val==='1';this.doSearch();}">
<van-tab title="已审核" name="1"></van-tab>
<van-tab title="未审核" name="0"></van-tab>
</van-tabs>
</van-sticky>
<table-list api="/platform/meeting/schoolUnionApproval/pageData" :page_form.sync="pageForm" ref="tableListRef" title="periodName" @ready="onReady">
<template v-slot="{index,row}">
<table-column label="开始时间">{{row.startTime}}</table-column>
<table-column label="结束时间">{{row.endTime}}</table-column>
<table-column label="请假人">{{row.userName}}</table-column>
<table-column label="请假事由">{{JSON.parse(row.instanceVariable).f_data.leaveReason || ''}}</table-column>
<table-column label="所属会议">{{row.meetingName}}</table-column>
<table-column label="会议类型">{{row.typeName}}</table-column>
<table-column label="当前节点">{{row.curTaskName}}</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" v-if="row.taskState === 10" @click="onApproval(row)">
<i class="fa fa-edit"></i>
<span>审核</span>
</div>
<div class="action-btn delete" v-if="row.canRevoke" @click="onRevoke(row)">
<i class="fa fa-reply"></i>
<span>撤回</span>
</div>
</template>
</table-list>
<leave-info ref="infoRef">
<div v-if="showApprovalForm">
<div class="process-title">{{formData.taskName}}</div>
<van-form ref="formRef">
<van-field
v-model="formData.tf_opinion"
name="tf_opinion"
label="审批意见"
placeholder="请输入审批意见"
:rules="[{ required: true, message: '请填写审批意见' }]"
required
maxlength="100"
show-word-limit
></van-field>
</van-form>
<div style="display: flex; justify-content: space-between; column-gap: 10px; padding: 10px">
<van-button type="primary" block @click="handleTaskAction(1)">同意</van-button>
<van-button type="danger" block @click="handleTaskAction(20)">不同意</van-button>
<van-button type="danger" block @click="handleTaskAction(6)">退回</van-button>
</div>
</div>
</leave-info>
</div>
<script nonce="${cspNonce!}">
<!--#include('../common/leaveInfo.js'){}#-->
new Vue({
el: "#app",
store,
components: {
'leave-info': leaveInfo,
},
data() {
return {
pageForm: {
pageNumber: 1,
pageSize: 10,
totalCount: 0,
searchKeyword: '',
approvalText: "0",
approval: false,
year: new Date().getFullYear(),
type: null,
},
typeOptions: [],
formData: {},
showApprovalForm: false
}
},
methods: {
async onReady() {
const typeList = await this.queryMeetingType()
this.typeOptions = [
{
text: "全部类型",
value: null
}
].concat(typeList.map((v) => ({ text: v.name, value: v.id })))
if (this.typeOptions.length > 0) {
this.pageForm.type = this.typeOptions[0].value
this.doSearch()
}
},
onView(row) {
this.showApprovalForm = false
this.$refs.infoRef.onOpen(row)
},
onApproval(row) {
this.showApprovalForm = true
this.$refs.infoRef.onOpen(row)
this.formData = {
processTaskId: row.taskId,
taskName: row.curTaskName
}
},
async handleTaskAction(val) {
try {
await this.$refs.formRef.validate();
this.$dialog.confirm({
title: "提示",
message: "您确定要提交吗?"
}).then(() => {
this.$axios.post("/flow/common/executeTask", {
data: JSON.stringify({
...this.formData,
submitType: val
})
}).then((res) => {
if (res.code === 0) {
this.$refs.infoRef.onClose()
this.$toast.success(res.msg)
this.doSearch()
}
})
}).catch(() => {
})
} catch (error) {
}
},
onRevoke(row){
this.$dialog.confirm({
title: "提示",
message: "您确定要撤回吗?"
}).then(() => {
this.$axios.post("/flow/common/revokeTask", {taskId: row.taskId}).then((res) => {
if (res.code === 0) {
this.$toast.success(res.msg)
this.doSearch()
}
})
})
},
async queryMeetingType() {
const res = await this.$axios.post('/platform/meeting/type/queryMeetingType')
return res.data
},
doSearch() {
this.$nextTick(() => {
this.pageForm.pageNumber = 1
this.pageForm.totalCount = 0
this.$refs.tableListRef.doSearch()
})
}
},
})
</script>
<!--#
}
#-->
@@ -0,0 +1,464 @@
<!--#
layout("/layouts/platform_h5.html"){
#-->
<style>
</style>
<div id="app">
<!-- 导航栏 -->
<van-nav-bar title="报销申请" left-text="返回" left-arrow
@click-left="historyBack" fixed></van-nav-bar>
<!-- 表单容器 -->
<div class="form-container">
<van-form ref="formRef">
<van-cell-group title="活动类型">
<van-field
:rules="[{ required: true }]"
v-model="formData.outlayManageSourceName"
label="活动类型"
placeholder="请选择活动类型"
required
is-link
readonly
@click="showOutlayManageSourcePopup = true"
></van-field>
<van-popup v-model="showOutlayManageSourcePopup" position="bottom">
<van-picker
show-toolbar
:columns="budgetTypeOption.map(i => i.name)"
@confirm="onOutlayManageSourceConfirm"
@cancel="showOutlayManageSourcePopup = false"
></van-picker>
</van-popup>
</van-cell-group>
<van-cell-group title="申请人信息">
<van-field label="申请人姓名" :rules="[{ required: true }]" v-model="formData.userName" readonly
required></van-field>
<van-field label="申请人工号" :rules="[{ required: true }]" v-model="formData.loginName" readonly
required></van-field>
<van-field label="手机号码" name="mobile" :rules="[{ required: true }]" v-model="formData.mobile"
required
placeholder="请输入手机号码"
maxlength="11"
type="tel"></van-field>
<van-field
:rules="[{ required: true }]"
v-model="formData.clubName"
label="所属社团"
placeholder="请选择所属社团"
required
is-link
readonly
@click="showClubNamePopup = true"
v-if="['ACTIVITY_BUDGET_TYPE_THREE'].includes(formData.outlayManageSource)"
></van-field>
<van-popup v-model="showClubNamePopup" position="bottom">
<van-picker
show-toolbar
:columns="clubOption.map(i => i.clubName)"
@confirm="onClubNameConfirm"
@cancel="showClubNamePopup = false"
></van-picker>
</van-popup>
<van-field label="经费余额" :rules="[{ required: true }]" v-model="budgetMoney" readonly
required></van-field>
<van-field label="活动事项" name="activityMatter"
:rules="[{ required: true }]"
v-model="formData.activityMatter"
required
maxlength="50"
placeholder="请输入活动事项"
v-if="['ACTIVITY_BUDGET_TYPE_THREE'].includes(formData.outlayManageSource)"></van-field>
<van-field
:rules="[{ required: true }]"
v-model="formData.budgetName"
label="活动事项"
placeholder="请选择活动事项"
required
is-link
readonly
@click="showBudgetNamePopup = true"
v-if="!['ACTIVITY_BUDGET_TYPE_THREE'].includes(formData.outlayManageSource)"
></van-field>
<van-popup v-model="showBudgetNamePopup" position="bottom">
<van-picker
show-toolbar
:columns="activityList.map(i => i.activityMatter)"
@confirm="onBudgetNameConfirm"
@cancel="showBudgetNamePopup = false"
></van-picker>
</van-popup>
<van-field label="活动人数" name="activityNumber"
:rules="[{ required: true }]"
v-model="formData.activityNumber"
required
maxlength="4"
placeholder="请输入活动人数"
type="digit"></van-field>
<van-field label="金额" name="money"
:rules="[{ required: true }]"
v-model="formData.money"
required
:placeholder="moneyPlaceholder"
type="number"></van-field>
<van-field label="活动时间"
name="activityTime"
:rules="[{ required: true }]"
:value="formData.activityTime"
readonly
is-link
placeholder="请填写活动时间"
required
@click="showActivityTimePopup = true"></van-field>
<van-popup v-model="showActivityTimePopup" position="bottom">
<van-datetime-picker
v-model="formData.activityTimeDate"
type="date"
title="选择活动时间"
:max-date="activityTimeMaxDate"
@confirm="onActivityTimeConfirm"
@cancel="showActivityTimePopup=false"
></van-datetime-picker>
</van-popup>
<van-field label="支付内容"
:rules="[{ required: true }]"
v-model="formData.paymentContent"
required
type="textarea"
name="paymentContent"
rows="4"
autosize
maxlength="500"
class="more-text"
placeholder="请填写支付内容"></van-field>
</van-cell-group>
<van-cell-group title="附件">
<van-field class="more-text" name="files"
:rules="[{ required: true,message:'请上传附件' }]"
label="" required>
<template #input>
<h5-file-upload
slot="input"
:value.sync="formData.files"
:upload_number="10"
upload_mode="file"
upload_result_category="array"
upload_result_type="url"
complete_result
></h5-file-upload>
</template>
</van-field>
</van-cell-group>
<van-cell-group title="电子签名">
<van-field class="more-text"
name="userSign"
label=""
required>
<template #input>
<h5-signature v-model="formData.userSign" slot="input"></h5-signature>
</template>
</van-field>
</van-cell-group>
<!-- 按钮区域 -->
<div class="form-actions">
<van-button native-type="button" plain type="info" @click="onSave">保存</van-button>
<van-button type="primary" @click="onSubmit" v-if="!taskId">提交</van-button>
<van-button type="primary" @click="onFinishTask" v-else>提交</van-button>
</div>
</van-form>
</div>
</div>
<script nonce="${cspNonce!}">
const vue = new Vue({
el: "#app",
store,
dicts: ["ACTIVITY_BUDGET_TYPE"],
data() {
return {
bizId: GetQueryString("bizId"),
taskId: GetQueryString("taskId"),
formData: {},
moneyPlaceholder: "请输入金额",
budgetMoney: 0,
budgetTypeOption: [],
showOutlayManageSourcePopup: false,
clubOption: [],
showClubNamePopup: false,
activityList: [],
showBudgetNamePopup: false,
showActivityTimePopup: false,
activityTimeMaxDate: new Date(),
}
},
methods: {
onSave() {
this.$dialog.confirm({
title: '温馨提示',
message: '您确定保存吗?',
}).then(() => {
this.$axios.post('/platform/outlay/reimburse/apply/save', {
data: JSON.stringify(this.formData)
}).then(res => {
if (res.code === 0) {
this.$toast.success("提交成功")
pjaxReplace("/platform/outlay/reimburse/applyList/h5")
}
})
}).catch(() => {
// on cancel
});
},
onSubmit() {
this.$refs.formRef.validate().then(() => {
this.$dialog.confirm({
title: '温馨提示',
message: '您确定要提交吗?',
}).then(() => {
this.$axios.post('/platform/outlay/reimburse/apply/submit', {
data: JSON.stringify(this.formData),
taskId: GetQueryString("taskId")
}).then(res => {
if (res.code === 0) {
this.$toast.success("提交成功")
pjaxReplace("/platform/outlay/reimburse/applyList/h5")
}
})
}).catch(() => {
});
}).catch();
},
onFinishTask() {
this.$refs.formRef.validate().then(() => {
this.$dialog.confirm({
title: '温馨提示',
message: '您确定要提交吗?',
}).then(() => {
this.$axios.post('/platform/outlay/reimburse/apply/submitAgain', {
data: JSON.stringify(this.formData),
taskId: GetQueryString("taskId")
}).then(res => {
if (res.code === 0) {
this.$toast.success("提交成功")
pjaxReplace("/platform/outlay/reimburse/applyList/h5")
}
})
}).catch(() => {
// on cancel
});
}).catch();
},
onActivityTimeConfirm(value, index) {
this.formData.activityTimeDate = value
this.formData.activityTime = this.$moment(value).format("YYYY-MM-DD")
this.showActivityTimePopup = false
},
async budgetIdChange(val) {
if (val) {
if (["ACTIVITY_BUDGET_TYPE_TWO", "ACTIVITY_BUDGET_TYPE_ONE"].includes(this.formData.outlayManageSource)) {
//如果是分工会和校工会
const data = this.activityList.find(a => a.id === val)
if (this.formData.outlayManageSource === "ACTIVITY_BUDGET_TYPE_TWO") {
//如果是分工会
const money = await this.getBxMoneyByBudgetId(val)
if (data.isSchoolBudget) {
//如果这一条分工会活动预算金额是属于校工会的
this.moneyPlaceholder = "预算金额" + data.totalBudgetMoney + "元,已报销金额:" + money
} else {
//如果这一条分工会活动活动,并且预算金额也是自己分工会的
if (data.isRepeatReimburse) {
//如果这一条活动预算可以重复报销
this.moneyPlaceholder = "预算金额:" + data.totalBudgetMoney + "元,已报销金额:" + money + ",不能超过20%"
} else {
//如果如果不能重复报销暂无判断
}
}
} else {
//如果是校工会
if (data.twoLevelBudgetList.length > 0) {
//如果大于0代表肯定有分工会使用校工会的预算,这里要减去分工会的预算
this.moneyPlaceholder = "预算金额:" + data.totalBudgetMoney + "元,已报销金额:" + data.twoLevelTotalBudgetMoney
} else {
if (data.isRepeatReimburse) {
const money = await this.getBxMoneyByBudgetId(val)
this.moneyPlaceholder = "预算金额:" + data.totalBudgetMoney + "元,已报销金额:" + money + ",不能超过20%"
} else {
//如果如果不能重复报销暂无判断
}
}
}
this.totalBudgetMoney = data.totalBudgetMoney
this.$set(this.formData, 'activityMatter', data.activityMatter)
} else {
this.moneyPlaceholder = "预算金额" + this.budgetMoney + "元"
this.totalBudgetMoney = this.budgetMoney
this.$set(this.formData, 'activityMatter', val)
}
} else {
this.moneyPlaceholder = "请输入金额"
this.totalBudgetMoney = 0
this.$set(this.formData, 'activityMatter', null)
}
},
async getBxMoneyByBudgetId(budgetId) {
const resp = await this.$axios.post("/platform/outlay/reimburse/apply/getBxMoneyByBudgetId", {
budgetId
})
if (resp.code === 0) {
return resp.data
} else {
return 0
}
},
async onBudgetNameConfirm(value, index) {
this.formData.budgetId = this.activityList[index].id;
this.formData.budgetName = value;
await this.budgetIdChange(this.formData.budgetId)
this.showBudgetNamePopup = false
},
async onClubNameConfirm(value, index) {
this.formData.clubId = this.clubOption[index].id;
this.formData.clubName = value;
await this.getBudgetMoneyOrActivity();
this.showClubNamePopup = false
},
async onOutlayManageSourceConfirm(value, index) {
this.formData.outlayManageSource = this.budgetTypeOption[index].code;
this.formData.outlayManageSourceName = value;
if (!this.formData.outlayManageSource) {
this.budgetMoney = 0
return
}
this.$set(this.formData, "budgetId", null)
this.$set(this.formData, "clubId", null)
this.$set(this.formData, "clubName", null)
await this.getBudgetMoneyOrActivity()
if (this.formData.outlayManageSource === "ACTIVITY_BUDGET_TYPE_TWO") {
this.$set(this.formData, "unionId", this.$store.state.user.union.id)
}
this.showOutlayManageSourcePopup = false;
},
async getBudgetMoneyOrActivity() {
const resp = await this.$axios.post("/platform/outlay/reimburse/apply/getBudgetMoneyOrActivity", {
outlayManageSource: this.formData.outlayManageSource,
clubId: this.formData.clubId,
unionId: this.formData.unionId,
id: this.formData.id
})
if (resp.code === 0) {
this.budgetMoney = resp.data.budgetMoney
this.activityList = resp.data.activityList
}
},
async findOne(id) {
const resp = await $.get('/platform/outlay/reimburse/apply/findOne', {id})
if (resp.code === 0) {
return resp.data
}
},
async init() {
this.clubOption = await this.$businessTool.listClubByRole()
this.budgetTypeOption = await this.$businessTool.getDictOptions("ACTIVITY_BUDGET_TYPE")
const budgetTypeOption = []
if (this.$auth.hasRoleOr(["SYSADMIN", "SCHOOL_UNION_ADMIN"])) {
} else {
if (this.$auth.hasRoleOr(["SCHOOL_OUTLAY_ADMIN", "SCHOOL_UNION_ADMIN"])) {
this.budgetTypeOption.map(v => {
if (["ACTIVITY_BUDGET_TYPE_ONE"].includes(v.code)) {
budgetTypeOption.push(v)
}
})
}
if (this.$auth.hasRoleOr(["BRANCH_UNION_CHAIRMAN", "BRANCH_UNION_ADMIN"])) {
this.budgetTypeOption.map(v => {
if (["ACTIVITY_BUDGET_TYPE_TWO"].includes(v.code)) {
budgetTypeOption.push(v)
}
})
}
if (this.$auth.hasRoleOr(["CLUB_MANAGER", "CLUB_PRESIDENT"])) {
this.budgetTypeOption.map(v => {
if (["ACTIVITY_BUDGET_TYPE_THREE"].includes(v.code)) {
budgetTypeOption.push(v)
}
})
}
this.budgetTypeOption = budgetTypeOption
}
if (this.bizId) {
this.findOne(this.bizId).then(async data => {
this.formData = data
await this.getBudgetMoneyOrActivity()
if (data.budgetId) {
await this.budgetIdChange(data.budgetId)
}
//活动时间回显
if (data.activityTime) {
this.$set(this.formData, "activityTimeDate", new Date(data.activityTime))
}
//活动类型
const outlayManageSource = this.dict.type.ACTIVITY_BUDGET_TYPE.find(v => v.code === data.outlayManageSource)
if (outlayManageSource) {
this.$set(this.formData, "outlayManageSourceName", outlayManageSource.name)
}
if (data.budgetId) {
const activity= this.activityList.find(v => v.id === data.budgetId)
this.$set(this.formData, "budgetName", activity.activityMatter)
}
})
} else {
const {id, username, loginname, mobile, union, unit} = this.$store.state.user
this.formData = {
userId: id,
userName: username,
loginName: loginname,
unitName: unit.name,
unitId: unit.id,
unionName: union.name,
unionId: union.id,
mobile: mobile,
}
}
}
},
created() {
this.init()
}
})
</script>
<!--#
}
#-->
@@ -0,0 +1,136 @@
<!--#
layout("/layouts/platform_h5.html"){
#-->
<div id="app">
<!-- 导航栏 -->
<van-nav-bar title="我的申请" left-text="返回" left-arrow placeholder
@click-left="historyBack" fixed></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-menu>
</van-sticky>
<table-list api="/platform/outlay/reimburse/applyList/pageData" :page_form.sync="pageForm"
ref="tableListRef"
@ready="doSearch" title="taskName">
<template v-slot="{index,row}">
<table-column label="经办人工号">{{row.loginName}}</table-column>
<table-column label="姓名">{{row.userName}}</table-column>
<table-column label="活动类型">
<dict-tag :options="dict.type.ACTIVITY_BUDGET_TYPE"
:value="row.outlayManageSource"></dict-tag>
</table-column>
<table-column label="活动事项">{{row.activityMatter}}</table-column>
<table-column label="申报单位">
<span v-if="['ACTIVITY_BUDGET_TYPE_ONE'].includes(row.outlayManageSource)">校工会</span>
<span v-if="['ACTIVITY_BUDGET_TYPE_TWO'].includes(row.outlayManageSource)">{{row.unionName}}</span>
<span v-if="['ACTIVITY_BUDGET_TYPE_THREE','ACTIVITY_BUDGET_TYPE_FOUR'].includes(row.outlayManageSource)">{{row.clubName}}</span>
</table-column>
<table-column label="金额">{{row.money}}</table-column>
<table-column label="申请时间">{{row.applyTime}}</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="onEdit(row)">
<i class="fa fa-edit"></i>
<span>编辑</span>
</div>
<div class="action-btn delete" v-if="row.canRevoke" @click="onRevoke(row)">
<i class="fa fa-undo"></i>
<span>撤回</span>
</div>
<div class="action-btn delete" @click="onDelete(row.id)"
v-if="row.taskKey === 'startTask' || !row.instanceId">
<i class="fa fa-trash"></i>
<span>删除</span>
</div>
</template>
</table-list>
<h5-outlay-reimburse-info ref="h5OutlayReimburseInfo"></h5-outlay-reimburse-info>
</div>
<script nonce="${cspNonce!}">
<!--#include('../info.js'){}#-->
const vue = new Vue({
el: "#app",
store,
dicts: ["ACTIVITY_BUDGET_TYPE"],
components: {
"h5-outlay-reimburse-info": H5_OUTLAY_REIMBURSE_INFO
},
data() {
return {
pageForm: {
pageNumber: 1,
pageSize: 10,
totalCount: 0,
year: new Date().getFullYear(),
},
}
},
methods: {
onView(row) {
this.$refs.h5OutlayReimburseInfo.onOpen(row)
},
onEdit(row) {
pjaxReplace('/platform/outlay/reimburse/apply/h5?taskId=' + (row.startTaskId || '') + "&bizId=" + row.id)
},
onDelete(id) {
this.$dialog.confirm({
title: '温馨提示',
message: '您确定要删除吗?',
}).then(() => {
this.$axios.post("/platform/outlay/reimburse/applyList/doDelete", {id}).then((res) => {
if (res.code === 0) {
this.$toast.success(res.msg)
this.doSearch()
} else {
this.$toast.fail(res.msg)
}
})
}).catch(() => {
// on cancel
});
},
onRevoke(row) {
this.$dialog.confirm({
title: '温馨提示',
message: '您确定要撤回吗?',
}).then(() => {
this.$axios.post("/flow/common/revokeTask", {taskId: row.startTaskId}).then((res) => {
if (res.code === 0) {
this.$toast.success(res.msg)
this.doSearch()
} else {
this.$toast.fail(res.msg)
}
})
}).catch(() => {
// on cancel
});
},
doSearch() {
this.pageForm.pageNumber = 1
this.pageForm.totalCount = 0
this.$refs.tableListRef.doSearch()
},
},
created() {
}
})
</script>
<!--#
}
#-->
@@ -0,0 +1,185 @@
<!--#
layout("/layouts/platform_h5.html"){
#-->
<div id="app">
<van-nav-bar @click-left="historyBack" left-arrow left-text="返回" title="社团审核" placeholder
fixed></van-nav-bar>
<van-sticky offset-top="46px">
<van-search
:reverse-color="false"
:show-action="false"
@search="doSearch"
input-align="left"
placeholder="请输入姓名搜索"
v-model="pageForm.searchKeyword"
></van-search>
<van-tabs v-model="pageForm.approvalText"
@change="(val)=>{this.pageForm.approval = val==='1';this.doSearch();}">
<van-tab title="已审核" name="1"></van-tab>
<van-tab title="未审核" name="0"></van-tab>
</van-tabs>
</van-sticky>
<table-list api="/platform/outlay/reimburse/clubAudit/pageData" :page_form.sync="pageForm"
ref="tableListRef"
title="curTaskName" @ready="doSearch">
<template v-slot="{index,row}">
<table-column label="经办人工号">{{row.loginName}}</table-column>
<table-column label="姓名">{{row.userName}}</table-column>
<table-column label="活动类型">
<dict-tag :options="dict.type.ACTIVITY_BUDGET_TYPE"
:value="row.outlayManageSource"></dict-tag>
</table-column>
<table-column label="活动事项">{{row.activityMatter}}</table-column>
<table-column label="申报单位">
<span v-if="['ACTIVITY_BUDGET_TYPE_ONE'].includes(row.outlayManageSource)">校工会</span>
<span v-if="['ACTIVITY_BUDGET_TYPE_TWO'].includes(row.outlayManageSource)">{{row.unionName}}</span>
<span v-if="['ACTIVITY_BUDGET_TYPE_THREE','ACTIVITY_BUDGET_TYPE_FOUR'].includes(row.outlayManageSource)">{{row.clubName}}</span>
</table-column>
<table-column label="金额">{{row.money}}</table-column>
<table-column label="申请时间">{{row.applyTime}}</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" v-if="row.taskState === 10" @click="onAudit(row)">
<i class="fa fa-edit"></i>
<span>审核</span>
</div>
<div class="action-btn delete" v-if="row.canRevoke" @click="onRevoke(row)">
<i class="fa fa-undo"></i>
<span>撤回</span>
</div>
</template>
</table-list>
<h5-outlay-reimburse-info ref="h5OutlayReimburseInfo">
<div v-if="showApprovalForm">
<div class="process-title">{{formData.taskName}}</div>
<div class="form-container">
<van-form ref="formRef">
<van-field label="审批意见"
:rules="[{ required: true,message:'请填审批意见' }]"
v-model="formData.tf_opinion"
required
type="textarea"
name="tf_opinion"
rows="4"
autosize
placeholder="请填审批意见"></van-field>
<van-field name="tf_userSign" label="签字">
<template #input>
<h5-signature v-model="formData.tf_userSign" slot="input"></h5-signature>
</template>
</van-field>
<!-- 按钮区域 -->
<div class="form-actions">
<van-button type="primary" @click="handleTaskAction(1)">同意申请</van-button>
<van-button type="danger" @click="handleTaskAction(2)">拒绝申请</van-button>
<van-button @click="handleTaskAction(6)">退回到发起人</van-button>
</div>
</van-form>
</div>
</div>
</h5-outlay-reimburse-info>
</div>
<script nonce="${cspNonce!}">
<!--#include('../info.js'){}#-->
const vue = new Vue({
el: "#app",
store,
dicts: ["ACTIVITY_BUDGET_TYPE"],
components: {
"h5-outlay-reimburse-info": H5_OUTLAY_REIMBURSE_INFO
},
data() {
return {
pageForm: {
pageNumber: 1,
pageSize: 10,
totalCount: 0,
year: new Date().getFullYear(),
approvalText: "0",
approval: false,
searchName: "info.userName",
},
formData: {},
infoShow: false,
showApprovalForm: false,
}
},
methods: {
handleTaskAction(val) {
this.$refs.formRef.validate().then(() => {
this.$dialog.confirm({
title: '温馨提示',
message: '您确定要提交吗?',
}).then(() => {
this.$axios.post('/platform/outlay/reimburse/clubAudit/submit', {
data: JSON.stringify({
...this.formData,
submitType: val
})
}).then(res => {
if (res.code === 0) {
this.$toast.success("提交成功")
this.doSearch()
this.$refs.h5OutlayReimburseInfo.onClose()
}
})
}).catch(() => {
// on cancel
});
}).catch();
},
onAudit(row) {
this.showApprovalForm = true
this.formData = {
tf_opinion: null,
tf_userSign: null,
processTaskId: row.taskId,
taskName: row.curTaskName
}
this.$refs.h5OutlayReimburseInfo.onOpen(row)
},
onRevoke(row) {
this.$dialog.confirm({
title: '温馨提示',
message: '您确定要撤回吗?',
}).then(() => {
this.$axios.post("/flow/common/revokeTask", {taskId: row.taskId}).then(res => {
if (res.code === 0) {
this.$toast.success("提交成功")
this.doSearch()
this.infoShow = false
}
})
}).catch(() => {
// on cancel
});
},
onView(row) {
this.showApprovalForm = false
this.$refs.h5OutlayReimburseInfo.onOpen(row)
},
doSearch() {
this.$nextTick(() => {
this.pageForm.pageNumber = 1
this.pageForm.totalCount = 0
this.$refs.tableListRef.doSearch()
})
},
}
})
</script>
<!--#
}
#-->
@@ -0,0 +1,185 @@
<!--#
layout("/layouts/platform_h5.html"){
#-->
<div id="app">
<van-nav-bar @click-left="historyBack" left-arrow left-text="返回" title="社团分管主席审核" placeholder
fixed></van-nav-bar>
<van-sticky offset-top="46px">
<van-search
:reverse-color="false"
:show-action="false"
@search="doSearch"
input-align="left"
placeholder="请输入姓名搜索"
v-model="pageForm.searchKeyword"
></van-search>
<van-tabs v-model="pageForm.approvalText"
@change="(val)=>{this.pageForm.approval = val==='1';this.doSearch();}">
<van-tab title="已审核" name="1"></van-tab>
<van-tab title="未审核" name="0"></van-tab>
</van-tabs>
</van-sticky>
<table-list api="/platform/outlay/reimburse/clubZxAudit/pageData" :page_form.sync="pageForm"
ref="tableListRef"
title="curTaskName" @ready="doSearch">
<template v-slot="{index,row}">
<table-column label="经办人工号">{{row.loginName}}</table-column>
<table-column label="姓名">{{row.userName}}</table-column>
<table-column label="活动类型">
<dict-tag :options="dict.type.ACTIVITY_BUDGET_TYPE"
:value="row.outlayManageSource"></dict-tag>
</table-column>
<table-column label="活动事项">{{row.activityMatter}}</table-column>
<table-column label="申报单位">
<span v-if="['ACTIVITY_BUDGET_TYPE_ONE'].includes(row.outlayManageSource)">校工会</span>
<span v-if="['ACTIVITY_BUDGET_TYPE_TWO'].includes(row.outlayManageSource)">{{row.unionName}}</span>
<span v-if="['ACTIVITY_BUDGET_TYPE_THREE','ACTIVITY_BUDGET_TYPE_FOUR'].includes(row.outlayManageSource)">{{row.clubName}}</span>
</table-column>
<table-column label="金额">{{row.money}}</table-column>
<table-column label="申请时间">{{row.applyTime}}</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" v-if="row.taskState === 10" @click="onAudit(row)">
<i class="fa fa-edit"></i>
<span>审核</span>
</div>
<div class="action-btn delete" v-if="row.canRevoke" @click="onRevoke(row)">
<i class="fa fa-undo"></i>
<span>撤回</span>
</div>
</template>
</table-list>
<h5-outlay-reimburse-info ref="h5OutlayReimburseInfo">
<div v-if="showApprovalForm">
<div class="process-title">{{formData.taskName}}</div>
<div class="form-container">
<van-form ref="formRef">
<van-field label="审批意见"
:rules="[{ required: true,message:'请填审批意见' }]"
v-model="formData.tf_opinion"
required
type="textarea"
name="tf_opinion"
rows="4"
autosize
placeholder="请填审批意见"></van-field>
<van-field name="tf_userSign" label="签字">
<template #input>
<h5-signature v-model="formData.tf_userSign" slot="input"></h5-signature>
</template>
</van-field>
<!-- 按钮区域 -->
<div class="form-actions">
<van-button type="primary" @click="handleTaskAction(1)">同意申请</van-button>
<van-button type="danger" @click="handleTaskAction(2)">拒绝申请</van-button>
<van-button @click="handleTaskAction(6)">退回到发起人</van-button>
</div>
</van-form>
</div>
</div>
</h5-outlay-reimburse-info>
</div>
<script nonce="${cspNonce!}">
<!--#include('../info.js'){}#-->
const vue = new Vue({
el: "#app",
store,
dicts: ["ACTIVITY_BUDGET_TYPE"],
components: {
"h5-outlay-reimburse-info": H5_OUTLAY_REIMBURSE_INFO
},
data() {
return {
pageForm: {
pageNumber: 1,
pageSize: 10,
totalCount: 0,
year: new Date().getFullYear(),
approvalText: "0",
approval: false,
searchName: "info.userName",
},
formData: {},
infoShow: false,
showApprovalForm: false,
}
},
methods: {
handleTaskAction(val) {
this.$refs.formRef.validate().then(() => {
this.$dialog.confirm({
title: '温馨提示',
message: '您确定要提交吗?',
}).then(() => {
this.$axios.post('/flow/common/executeTask', {
data: JSON.stringify({
...this.formData,
submitType: val
})
}).then(res => {
if (res.code === 0) {
this.$toast.success("提交成功")
this.doSearch()
this.$refs.h5OutlayReimburseInfo.onClose()
}
})
}).catch(() => {
// on cancel
});
}).catch();
},
onAudit(row) {
this.showApprovalForm = true
this.formData = {
tf_opinion: null,
tf_userSign: null,
processTaskId: row.taskId,
taskName: row.curTaskName
}
this.$refs.h5OutlayReimburseInfo.onOpen(row)
},
onRevoke(row) {
this.$dialog.confirm({
title: '温馨提示',
message: '您确定要撤回吗?',
}).then(() => {
this.$axios.post("/flow/common/revokeTask", {taskId: row.taskId}).then(res => {
if (res.code === 0) {
this.$toast.success("提交成功")
this.doSearch()
this.infoShow = false
}
})
}).catch(() => {
// on cancel
});
},
onView(row) {
this.showApprovalForm = false
this.$refs.h5OutlayReimburseInfo.onOpen(row)
},
doSearch() {
this.$nextTick(() => {
this.pageForm.pageNumber = 1
this.pageForm.totalCount = 0
this.$refs.tableListRef.doSearch()
})
},
}
})
</script>
<!--#
}
#-->
@@ -0,0 +1,146 @@
const H5_OUTLAY_REIMBURSE_INFO = {
template: /*language=HTML*/ `
<van-action-sheet v-model="visible" title="查看详情">
<div>
<div class="process-title">申请信息</div>
<van-cell-group>
<van-cell title="申请人">
{{ viewData.userName }}
</van-cell>
<van-cell title="工号">
{{ viewData.loginName }}
</van-cell>
<van-cell title="联系方式">
{{ viewData.mobile }}
</van-cell>
<van-cell title="预算类型">
<dict-tag :options="dict.type.ACTIVITY_BUDGET_TYPE"
:value="viewData.outlayManageSource"></dict-tag>
</van-cell>
<van-cell title="预算类型">
{{viewData.activityMatter}}
</van-cell>
<van-cell title="申请金额">
{{viewData.money}}
</van-cell>
<van-cell title="活动时间">
{{viewData.activityTime}}
</van-cell>
<van-cell title="活动人数">
{{viewData.activityNumber}}
</van-cell>
<van-cell title="支付内容">
<div style="white-space: pre-line">{{viewData.paymentContent}}</div>
</van-cell>
<van-cell title="附件">
<template #label>
<template v-for="(item,index) in viewData.files">
<van-image :src="item.url"
v-if="item.url"
class="signature-image"
@click="previewOptionImg(viewData.files,index)"></van-image>
</template>
</template>
</van-cell>
<van-cell title="签字" >
<van-image :src="viewData.userSign"
class="signature-image"></van-image>
</van-cell>
</van-cell-group>
<template v-for="(task,index) in doneTasks">
<div class="process-title">
{{ task.displayName }}
</div>
<van-cell-group v-if="task.ext.isFirstTaskNode">
<van-cell title="申请用户">
{{ task.ext.initiatorName}}({{task.ext.initiatorAccount}})
</van-cell>
<van-cell title="申请时间">
{{ task.finishTime}}
</van-cell>
<van-cell title="办理结果">
<dict-tag :options="dict.type.PROCESS_TASK_SUBMIT_TYPE"
:value="task.ext.submitType"></dict-tag>
</van-cell>
</van-cell-group>
<van-cell-group v-else>
<van-cell title="办理用户">
{{ task.taskFormData.userName}}({{task.taskFormData.loginName}})
</van-cell>
<van-cell title="办理时间">
{{ task.finishTime}}
</van-cell>
<van-cell title="办理结果">
<dict-tag :options="dict.type.PROCESS_TASK_SUBMIT_TYPE"
:value="task.ext.submitType"></dict-tag>
</van-cell>
<van-cell title="办理意见" v-if="!task.ext.isFirstTaskNode">
<template #label>
{{
task.taskFormData.opinion
}}
</template>
</van-cell>
<van-cell title="签字" v-if="!task.ext.isFirstTaskNode">
<van-image :src="task.ext.tf_userSign"
v-if="task.ext.tf_userSign"
class="signature-image"></van-image>
</van-cell>
</van-cell-group>
</template>
<slot></slot>
</div>
</van-action-sheet>
`,
dicts: ["PROCESS_TASK_SUBMIT_TYPE","ACTIVITY_BUDGET_TYPE","ACTIVITY_BUDGET_DETAILS_TYPE"],
data() {
return {
visible: false,
viewData: {},
doneTasks: [],
row: null
}
},
methods: {
//预览图片
previewOptionImg(files, index) {
const urls = files.map(item => item.url)
vant.ImagePreview({images: urls, startPosition: index})
},
// 打开
onOpen(row) {
this.row = row
this.visible = true
this.getInfo()
this.getDoneTasks()
},
// 关闭
onClose() {
this.visible = false
},
// 获取申请信息
getInfo() {
this.$axios.post('/platform/outlay/reimburse/apply/findOne', {id: this.row.id}).then((res) => {
if (res.code === 0) {
this.viewData = res.data
}
})
},
// 获取已办任务审批记录
getDoneTasks() {
this.$axios.post("/flow/common/doneTasks", {bizId: this.row.id}).then((res) => {
if (res.code === 0) {
this.doneTasks = res.data
}
})
},
}
}
@@ -0,0 +1,185 @@
<!--#
layout("/layouts/platform_h5.html"){
#-->
<div id="app">
<van-nav-bar @click-left="historyBack" left-arrow left-text="返回" title="校工会出纳审核" placeholder
fixed></van-nav-bar>
<van-sticky offset-top="46px">
<van-search
:reverse-color="false"
:show-action="false"
@search="doSearch"
input-align="left"
placeholder="请输入姓名搜索"
v-model="pageForm.searchKeyword"
></van-search>
<van-tabs v-model="pageForm.approvalText"
@change="(val)=>{this.pageForm.approval = val==='1';this.doSearch();}">
<van-tab title="已审核" name="1"></van-tab>
<van-tab title="未审核" name="0"></van-tab>
</van-tabs>
</van-sticky>
<table-list api="/platform/outlay/reimburse/schoolCnAudit/pageData" :page_form.sync="pageForm"
ref="tableListRef"
title="curTaskName" @ready="doSearch">
<template v-slot="{index,row}">
<table-column label="经办人工号">{{row.loginName}}</table-column>
<table-column label="姓名">{{row.userName}}</table-column>
<table-column label="活动类型">
<dict-tag :options="dict.type.ACTIVITY_BUDGET_TYPE"
:value="row.outlayManageSource"></dict-tag>
</table-column>
<table-column label="活动事项">{{row.activityMatter}}</table-column>
<table-column label="申报单位">
<span v-if="['ACTIVITY_BUDGET_TYPE_ONE'].includes(row.outlayManageSource)">校工会</span>
<span v-if="['ACTIVITY_BUDGET_TYPE_TWO'].includes(row.outlayManageSource)">{{row.unionName}}</span>
<span v-if="['ACTIVITY_BUDGET_TYPE_THREE','ACTIVITY_BUDGET_TYPE_FOUR'].includes(row.outlayManageSource)">{{row.clubName}}</span>
</table-column>
<table-column label="金额">{{row.money}}</table-column>
<table-column label="申请时间">{{row.applyTime}}</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" v-if="row.taskState === 10" @click="onAudit(row)">
<i class="fa fa-edit"></i>
<span>审核</span>
</div>
<div class="action-btn delete" v-if="row.canRevoke" @click="onRevoke(row)">
<i class="fa fa-undo"></i>
<span>撤回</span>
</div>
</template>
</table-list>
<h5-outlay-reimburse-info ref="h5OutlayReimburseInfo">
<div v-if="showApprovalForm">
<div class="process-title">{{formData.taskName}}</div>
<div class="form-container">
<van-form ref="formRef">
<van-field label="审批意见"
:rules="[{ required: true,message:'请填审批意见' }]"
v-model="formData.tf_opinion"
required
type="textarea"
name="tf_opinion"
rows="4"
autosize
placeholder="请填审批意见"></van-field>
<van-field name="tf_userSign" label="签字">
<template #input>
<h5-signature v-model="formData.tf_userSign" slot="input"></h5-signature>
</template>
</van-field>
<!-- 按钮区域 -->
<div class="form-actions">
<van-button type="primary" @click="handleTaskAction(1)">同意申请</van-button>
<van-button type="danger" @click="handleTaskAction(2)">拒绝申请</van-button>
<van-button @click="handleTaskAction(6)">退回到发起人</van-button>
</div>
</van-form>
</div>
</div>
</h5-outlay-reimburse-info>
</div>
<script nonce="${cspNonce!}">
<!--#include('../info.js'){}#-->
const vue = new Vue({
el: "#app",
store,
dicts: ["ACTIVITY_BUDGET_TYPE"],
components: {
"h5-outlay-reimburse-info": H5_OUTLAY_REIMBURSE_INFO
},
data() {
return {
pageForm: {
pageNumber: 1,
pageSize: 10,
totalCount: 0,
year: new Date().getFullYear(),
approvalText: "0",
approval: false,
searchName: "info.userName",
},
formData: {},
infoShow: false,
showApprovalForm: false,
}
},
methods: {
handleTaskAction(val) {
this.$refs.formRef.validate().then(() => {
this.$dialog.confirm({
title: '温馨提示',
message: '您确定要提交吗?',
}).then(() => {
this.$axios.post('/flow/common/executeTask', {
data: JSON.stringify({
...this.formData,
submitType: val
})
}).then(res => {
if (res.code === 0) {
this.$toast.success("提交成功")
this.doSearch()
this.$refs.h5OutlayReimburseInfo.onClose()
}
})
}).catch(() => {
// on cancel
});
}).catch();
},
onAudit(row) {
this.showApprovalForm = true
this.formData = {
tf_opinion: null,
tf_userSign: null,
processTaskId: row.taskId,
taskName: row.curTaskName
}
this.$refs.h5OutlayReimburseInfo.onOpen(row)
},
onRevoke(row) {
this.$dialog.confirm({
title: '温馨提示',
message: '您确定要撤回吗?',
}).then(() => {
this.$axios.post("/flow/common/revokeTask", {taskId: row.taskId}).then(res => {
if (res.code === 0) {
this.$toast.success("提交成功")
this.doSearch()
this.infoShow = false
}
})
}).catch(() => {
// on cancel
});
},
onView(row) {
this.showApprovalForm = false
this.$refs.h5OutlayReimburseInfo.onOpen(row)
},
doSearch() {
this.$nextTick(() => {
this.pageForm.pageNumber = 1
this.pageForm.totalCount = 0
this.$refs.tableListRef.doSearch()
})
},
}
})
</script>
<!--#
}
#-->
@@ -0,0 +1,207 @@
<!--#
layout("/layouts/platform_h5.html"){
#-->
<div id="app">
<van-nav-bar @click-left="historyBack" left-arrow left-text="返回" title="校工会会计审核" placeholder
fixed></van-nav-bar>
<van-sticky offset-top="46px">
<van-search
:reverse-color="false"
:show-action="false"
@search="doSearch"
input-align="left"
placeholder="请输入姓名搜索"
v-model="pageForm.searchKeyword"
></van-search>
<van-tabs v-model="pageForm.approvalText"
@change="(val)=>{this.pageForm.approval = val==='1';this.doSearch();}">
<van-tab title="已审核" name="1"></van-tab>
<van-tab title="未审核" name="0"></van-tab>
</van-tabs>
</van-sticky>
<table-list api="/platform/outlay/reimburse/schoolKjAudit/pageData"
:page_form.sync="pageForm"
ref="tableListRef"
title="curTaskName" @ready="doSearch">
<template v-slot="{index,row}">
<table-column label="经办人工号">{{row.loginName}}</table-column>
<table-column label="姓名">{{row.userName}}</table-column>
<table-column label="活动类型">
<dict-tag :options="dict.type.ACTIVITY_BUDGET_TYPE"
:value="row.outlayManageSource"></dict-tag>
</table-column>
<table-column label="活动事项">{{row.activityMatter}}</table-column>
<table-column label="申报单位">
<span v-if="['ACTIVITY_BUDGET_TYPE_ONE'].includes(row.outlayManageSource)">校工会</span>
<span v-if="['ACTIVITY_BUDGET_TYPE_TWO'].includes(row.outlayManageSource)">{{row.unionName}}</span>
<span v-if="['ACTIVITY_BUDGET_TYPE_THREE','ACTIVITY_BUDGET_TYPE_FOUR'].includes(row.outlayManageSource)">{{row.clubName}}</span>
</table-column>
<table-column label="金额">{{row.money}}</table-column>
<table-column label="申请时间">{{row.applyTime}}</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" v-if="row.taskState === 10" @click="onAudit(row)">
<i class="fa fa-edit"></i>
<span>审核</span>
</div>
<div class="action-btn delete" v-if="row.canRevoke" @click="onRevoke(row)">
<i class="fa fa-undo"></i>
<span>撤回</span>
</div>
</template>
</table-list>
<h5-outlay-reimburse-info ref="h5OutlayReimburseInfo">
<div v-if="showApprovalForm">
<div class="process-title">{{formData.taskName}}</div>
<div class="form-container">
<van-form ref="formRef">
<van-field label="审批意见"
:rules="[{ required: true,message:'请填审批意见' }]"
v-model="formData.tf_opinion"
required
type="textarea"
name="tf_opinion"
rows="4"
autosize
placeholder="请填审批意见"></van-field>
<van-field name="tf_userSign" label="签字">
<template #input>
<h5-signature v-model="formData.tf_userSign" slot="input"></h5-signature>
</template>
</van-field>
<van-popup v-model="showDetailsTypePopup" position="bottom">
<van-picker
show-toolbar
:columns="detailsTypeOption.map(i => i.name)"
@confirm="onDetailsTypeConfirm"
@cancel="showDetailsTypePopup = false"
></van-picker>
</van-popup>
<!-- 按钮区域 -->
<div class="form-actions">
<van-button type="primary" @click="openHandleTaskAction">同意申请</van-button>
<van-button type="danger" @click="handleTaskAction(2)">拒绝申请</van-button>
<van-button @click="handleTaskAction(6)">退回到发起人</van-button>
</div>
</van-form>
</div>
</div>
</h5-outlay-reimburse-info>
</div>
<script nonce="${cspNonce!}">
<!--#include('../info.js'){}#-->
const vue = new Vue({
el: "#app",
store,
dicts: ["ACTIVITY_BUDGET_TYPE"],
components: {
"h5-outlay-reimburse-info": H5_OUTLAY_REIMBURSE_INFO
},
data() {
return {
pageForm: {
pageNumber: 1,
pageSize: 10,
totalCount: 0,
year: new Date().getFullYear(),
approvalText: "0",
approval: false,
searchName: "info.userName",
},
formData: {},
detailsTypeOption: [],
showApprovalForm: false,
showDetailsTypePopup: false,
}
},
methods: {
async openHandleTaskAction() {
await this.$refs.formRef.validate();
this.detailsTypeOption = await this.$businessTool.getDictOptions("ACTIVITY_BUDGET_DETAILS_TYPE")
this.showDetailsTypePopup = true
},
onDetailsTypeConfirm(value, index) {
this.formData.detailsType = this.detailsTypeOption[index].value
this.handleTaskAction(1)
},
handleTaskAction(val) {
this.$refs.formRef.validate().then(() => {
this.$dialog.confirm({
title: '温馨提示',
message: '您确定要提交吗?',
}).then(() => {
this.$axios.post('/flow/common/executeTask', {
data: JSON.stringify({
...this.formData,
submitType: val
})
}).then(res => {
if (res.code === 0) {
this.$toast.success("提交成功")
this.doSearch()
this.$refs.h5OutlayReimburseInfo.onClose()
this.showDetailsTypePopup = false
}
})
}).catch(() => {
// on cancel
});
}).catch();
},
onAudit(row) {
this.showApprovalForm = true
this.formData = {
tf_opinion: null,
tf_userSign: null,
processTaskId: row.taskId,
taskName: row.curTaskName
}
this.$refs.h5OutlayReimburseInfo.onOpen(row)
},
onRevoke(row) {
this.$dialog.confirm({
title: '温馨提示',
message: '您确定要撤回吗?',
}).then(() => {
this.$axios.post("/flow/common/revokeTask", {taskId: row.taskId}).then(res => {
if (res.code === 0) {
this.$toast.success("提交成功")
this.doSearch()
this.infoShow = false
}
})
}).catch(() => {
// on cancel
});
},
onView(row) {
this.showApprovalForm = false
this.$refs.h5OutlayReimburseInfo.onOpen(row)
},
doSearch() {
this.$nextTick(() => {
this.pageForm.pageNumber = 1
this.pageForm.totalCount = 0
this.$refs.tableListRef.doSearch()
})
},
}
})
</script>
<!--#
}
#-->
@@ -0,0 +1,185 @@
<!--#
layout("/layouts/platform_h5.html"){
#-->
<div id="app">
<van-nav-bar @click-left="historyBack" left-arrow left-text="返回" title="校工会主席终审" placeholder
fixed></van-nav-bar>
<van-sticky offset-top="46px">
<van-search
:reverse-color="false"
:show-action="false"
@search="doSearch"
input-align="left"
placeholder="请输入姓名搜索"
v-model="pageForm.searchKeyword"
></van-search>
<van-tabs v-model="pageForm.approvalText"
@change="(val)=>{this.pageForm.approval = val==='1';this.doSearch();}">
<van-tab title="已审核" name="1"></van-tab>
<van-tab title="未审核" name="0"></van-tab>
</van-tabs>
</van-sticky>
<table-list api="/platform/outlay/reimburse/schoolZxAudit/pageData" :page_form.sync="pageForm"
ref="tableListRef"
title="curTaskName" @ready="doSearch">
<template v-slot="{index,row}">
<table-column label="经办人工号">{{row.loginName}}</table-column>
<table-column label="姓名">{{row.userName}}</table-column>
<table-column label="活动类型">
<dict-tag :options="dict.type.ACTIVITY_BUDGET_TYPE"
:value="row.outlayManageSource"></dict-tag>
</table-column>
<table-column label="活动事项">{{row.activityMatter}}</table-column>
<table-column label="申报单位">
<span v-if="['ACTIVITY_BUDGET_TYPE_ONE'].includes(row.outlayManageSource)">校工会</span>
<span v-if="['ACTIVITY_BUDGET_TYPE_TWO'].includes(row.outlayManageSource)">{{row.unionName}}</span>
<span v-if="['ACTIVITY_BUDGET_TYPE_THREE','ACTIVITY_BUDGET_TYPE_FOUR'].includes(row.outlayManageSource)">{{row.clubName}}</span>
</table-column>
<table-column label="金额">{{row.money}}</table-column>
<table-column label="申请时间">{{row.applyTime}}</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" v-if="row.taskState === 10" @click="onAudit(row)">
<i class="fa fa-edit"></i>
<span>审核</span>
</div>
<div class="action-btn delete" v-if="row.canRevoke" @click="onRevoke(row)">
<i class="fa fa-undo"></i>
<span>撤回</span>
</div>
</template>
</table-list>
<h5-outlay-reimburse-info ref="h5OutlayReimburseInfo">
<div v-if="showApprovalForm">
<div class="process-title">{{formData.taskName}}</div>
<div class="form-container">
<van-form ref="formRef">
<van-field label="审批意见"
:rules="[{ required: true,message:'请填审批意见' }]"
v-model="formData.tf_opinion"
required
type="textarea"
name="tf_opinion"
rows="4"
autosize
placeholder="请填审批意见"></van-field>
<van-field name="tf_userSign" label="签字">
<template #input>
<h5-signature v-model="formData.tf_userSign" slot="input"></h5-signature>
</template>
</van-field>
<!-- 按钮区域 -->
<div class="form-actions">
<van-button type="primary" @click="handleTaskAction(1)">同意申请</van-button>
<van-button type="danger" @click="handleTaskAction(2)">拒绝申请</van-button>
<van-button @click="handleTaskAction(6)">退回到发起人</van-button>
</div>
</van-form>
</div>
</div>
</h5-outlay-reimburse-info>
</div>
<script nonce="${cspNonce!}">
<!--#include('../info.js'){}#-->
const vue = new Vue({
el: "#app",
store,
dicts: ["ACTIVITY_BUDGET_TYPE"],
components: {
"h5-outlay-reimburse-info": H5_OUTLAY_REIMBURSE_INFO
},
data() {
return {
pageForm: {
pageNumber: 1,
pageSize: 10,
totalCount: 0,
year: new Date().getFullYear(),
approvalText: "0",
approval: false,
searchName: "info.userName",
},
formData: {},
infoShow: false,
showApprovalForm: false,
}
},
methods: {
handleTaskAction(val) {
this.$refs.formRef.validate().then(() => {
this.$dialog.confirm({
title: '温馨提示',
message: '您确定要提交吗?',
}).then(() => {
this.$axios.post('/flow/common/executeTask', {
data: JSON.stringify({
...this.formData,
submitType: val
})
}).then(res => {
if (res.code === 0) {
this.$toast.success("提交成功")
this.doSearch()
this.$refs.h5OutlayReimburseInfo.onClose()
}
})
}).catch(() => {
// on cancel
});
}).catch();
},
onAudit(row) {
this.showApprovalForm = true
this.formData = {
tf_opinion: null,
tf_userSign: null,
processTaskId: row.taskId,
taskName: row.curTaskName
}
this.$refs.h5OutlayReimburseInfo.onOpen(row)
},
onRevoke(row) {
this.$dialog.confirm({
title: '温馨提示',
message: '您确定要撤回吗?',
}).then(() => {
this.$axios.post("/flow/common/revokeTask", {taskId: row.taskId}).then(res => {
if (res.code === 0) {
this.$toast.success("提交成功")
this.doSearch()
this.infoShow = false
}
})
}).catch(() => {
// on cancel
});
},
onView(row) {
this.showApprovalForm = false
this.$refs.h5OutlayReimburseInfo.onOpen(row)
},
doSearch() {
this.$nextTick(() => {
this.pageForm.pageNumber = 1
this.pageForm.totalCount = 0
this.$refs.tableListRef.doSearch()
})
},
}
})
</script>
<!--#
}
#-->
@@ -0,0 +1,185 @@
<!--#
layout("/layouts/platform_h5.html"){
#-->
<div id="app">
<van-nav-bar @click-left="historyBack" left-arrow left-text="返回" title="分工会审核" placeholder
fixed></van-nav-bar>
<van-sticky offset-top="46px">
<van-search
:reverse-color="false"
:show-action="false"
@search="doSearch"
input-align="left"
placeholder="请输入姓名搜索"
v-model="pageForm.searchKeyword"
></van-search>
<van-tabs v-model="pageForm.approvalText"
@change="(val)=>{this.pageForm.approval = val==='1';this.doSearch();}">
<van-tab title="已审核" name="1"></van-tab>
<van-tab title="未审核" name="0"></van-tab>
</van-tabs>
</van-sticky>
<table-list api="/platform/outlay/reimburse/unionAudit/pageData" :page_form.sync="pageForm"
ref="tableListRef"
title="curTaskName" @ready="doSearch">
<template v-slot="{index,row}">
<table-column label="经办人工号">{{row.loginName}}</table-column>
<table-column label="姓名">{{row.userName}}</table-column>
<table-column label="活动类型">
<dict-tag :options="dict.type.ACTIVITY_BUDGET_TYPE"
:value="row.outlayManageSource"></dict-tag>
</table-column>
<table-column label="活动事项">{{row.activityMatter}}</table-column>
<table-column label="申报单位">
<span v-if="['ACTIVITY_BUDGET_TYPE_ONE'].includes(row.outlayManageSource)">校工会</span>
<span v-if="['ACTIVITY_BUDGET_TYPE_TWO'].includes(row.outlayManageSource)">{{row.unionName}}</span>
<span v-if="['ACTIVITY_BUDGET_TYPE_THREE','ACTIVITY_BUDGET_TYPE_FOUR'].includes(row.outlayManageSource)">{{row.clubName}}</span>
</table-column>
<table-column label="金额">{{row.money}}</table-column>
<table-column label="申请时间">{{row.applyTime}}</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" v-if="row.taskState === 10" @click="onAudit(row)">
<i class="fa fa-edit"></i>
<span>审核</span>
</div>
<div class="action-btn delete" v-if="row.canRevoke" @click="onRevoke(row)">
<i class="fa fa-undo"></i>
<span>撤回</span>
</div>
</template>
</table-list>
<h5-outlay-reimburse-info ref="h5OutlayReimburseInfo">
<div v-if="showApprovalForm">
<div class="process-title">{{formData.taskName}}</div>
<div class="form-container">
<van-form ref="formRef">
<van-field label="审批意见"
:rules="[{ required: true,message:'请填审批意见' }]"
v-model="formData.tf_opinion"
required
type="textarea"
name="tf_opinion"
rows="4"
autosize
placeholder="请填审批意见"></van-field>
<van-field name="tf_userSign" label="签字">
<template #input>
<h5-signature v-model="formData.tf_userSign" slot="input"></h5-signature>
</template>
</van-field>
<!-- 按钮区域 -->
<div class="form-actions">
<van-button type="primary" @click="handleTaskAction(1)">同意申请</van-button>
<van-button type="danger" @click="handleTaskAction(2)">拒绝申请</van-button>
<van-button @click="handleTaskAction(6)">退回到发起人</van-button>
</div>
</van-form>
</div>
</div>
</h5-outlay-reimburse-info>
</div>
<script nonce="${cspNonce!}">
<!--#include('../info.js'){}#-->
const vue = new Vue({
el: "#app",
store,
dicts: ["ACTIVITY_BUDGET_TYPE"],
components: {
"h5-outlay-reimburse-info": H5_OUTLAY_REIMBURSE_INFO
},
data() {
return {
pageForm: {
pageNumber: 1,
pageSize: 10,
totalCount: 0,
year: new Date().getFullYear(),
approvalText: "0",
approval: false,
searchName: "info.userName",
},
formData: {},
infoShow: false,
showApprovalForm: false,
}
},
methods: {
handleTaskAction(val) {
this.$refs.formRef.validate().then(() => {
this.$dialog.confirm({
title: '温馨提示',
message: '您确定要提交吗?',
}).then(() => {
this.$axios.post('/flow/common/executeTask', {
data: JSON.stringify({
...this.formData,
submitType: val
})
}).then(res => {
if (res.code === 0) {
this.$toast.success("提交成功")
this.doSearch()
this.$refs.h5OutlayReimburseInfo.onClose()
}
})
}).catch(() => {
// on cancel
});
}).catch();
},
onAudit(row) {
this.showApprovalForm = true
this.formData = {
tf_opinion: null,
tf_userSign: null,
processTaskId: row.taskId,
taskName: row.curTaskName
}
this.$refs.h5OutlayReimburseInfo.onOpen(row)
},
onRevoke(row) {
this.$dialog.confirm({
title: '温馨提示',
message: '您确定要撤回吗?',
}).then(() => {
this.$axios.post("/flow/common/revokeTask", {taskId: row.taskId}).then(res => {
if (res.code === 0) {
this.$toast.success("提交成功")
this.doSearch()
this.infoShow = false
}
})
}).catch(() => {
// on cancel
});
},
onView(row) {
this.showApprovalForm = false
this.$refs.h5OutlayReimburseInfo.onOpen(row)
},
doSearch() {
this.$nextTick(() => {
this.pageForm.pageNumber = 1
this.pageForm.totalCount = 0
this.$refs.tableListRef.doSearch()
})
},
}
})
</script>
<!--#
}
#-->
@@ -0,0 +1,535 @@
<!--#
layout("/layouts/platform_h5.html"){
#-->
<div id="app" v-cloak class="qsv-h5-page">
<div class="qsv-h5-header">
<div class="qsv-h5-nav">
<button class="qsv-h5-back-btn" @click="historyBack">
<i class="fa fa-angle-left"></i>
</button>
<div class="qsv-h5-nav-title">问卷服务</div>
<div class="qsv-h5-nav-placeholder"></div>
</div>
<div class="qsv-h5-search-row">
<van-search
v-model="pageForm.title"
shape="round"
background="transparent"
placeholder="搜索标题名称"
clearable
@search="onRefresh"
@clear="onRefresh">
</van-search>
<button class="qsv-h5-search-btn" @click="onRefresh">
<i class="fa fa-search"></i>
</button>
</div>
<div class="qsv-h5-filter-row">
<button
v-for="item in categoryOptions"
:key="item.value"
:class="['qsv-h5-filter-btn', pageForm.category === item.value ? 'active' : '']"
@click="changeCategory(item.value)">
<i :class="pageForm.category === item.value ? 'fa fa-check-circle' : 'fa fa-circle-o'"></i>
<span>{{item.label}}</span>
</button>
</div>
</div>
<van-pull-refresh v-model="refreshing" @refresh="onRefresh" class="qsv-h5-list-wrap">
<van-list v-model="loading" :finished="finished" finished-text="暂无更多内容" @load="onLoad">
<div
v-for="row in list"
:key="row.id"
class="qsv-h5-card"
@click="onEnter(row)">
<div class="qsv-h5-cover">
<img :src="row.cover || defaultCover" alt="">
</div>
<div class="qsv-h5-card-body">
<div class="qsv-h5-title-row">
<div class="qsv-h5-title">{{row.title}}</div>
<div class="qsv-h5-answered" v-if="row.answeredText || row.isAnswered">{{row.answeredText || getAnsweredText(row.category)}}</div>
</div>
<div class="qsv-h5-meta-row">
<div class="qsv-h5-type">
<i :class="getCategoryIcon(row.category)"></i>
<span>{{getCategoryText(row.category)}}</span>
</div>
</div>
<div class="qsv-h5-time">
<div>开始时间:{{formatDateTime(row.startTime)}}</div>
<div>结束时间:{{formatDateTime(row.endTime)}}</div>
</div>
</div>
</div>
<van-empty v-if="!loading && list.length === 0" description="暂无内容"></van-empty>
</van-list>
</van-pull-refresh>
</div>
<script nonce="${cspNonce!}">
const vue = new Vue({
el: "#app",
dicts: ['ACTIVITY_QSV_CATEGORY'],
store,
data() {
return {
loading: false,
requesting: false,
finished: false,
refreshing: false,
cacheRestored: false,
list: [],
defaultCover: "/assets/mobile/svg/qsv/icon.svg",
pageForm: {
pageNumber: 1,
pageSize: 10,
totalCount: 0,
title: "",
category: ""
},
categoryOptions: [
{label: "全部", value: ""},
{label: "答题", value: "QUIZ"},
{label: "问卷", value: "SURVEY"},
{label: "投票", value: "VOTE"}
],
categoryPaths: {
QUIZ: "/platform/h5/qsv/quiz",
SURVEY: "/platform/h5/qsv/survey",
VOTE: "/platform/h5/qsv/vote"
}
}
},
methods: {
historyBack() {
this.$pjaxReplace('/platform/h5/home')
},
changeCategory(category) {
if (this.pageForm.category === category) {
return
}
this.pageForm.category = category
this.onRefresh()
},
onRefresh() {
this.pageForm.pageNumber = 1
this.pageForm.totalCount = 0
this.list = []
this.finished = false
this.clearListCache()
this.onLoad()
},
onLoad() {
if (this.requesting || this.finished) {
return
}
this.requesting = true
this.loading = true
this.$axios.post("/platform/h5/qsv/pageData", this.pageForm).then((res) => {
if (res.code === 0) {
const pageData = res.data || {}
const rows = pageData.list || []
this.list = this.list.concat(rows)
this.pageForm.totalCount = pageData.totalCount || 0
if (this.list.length >= this.pageForm.totalCount || rows.length === 0) {
this.finished = true
}
this.pageForm.pageNumber++
this.saveListCache()
}
}).finally(() => {
this.requesting = false
this.loading = false
this.refreshing = false
})
},
refreshListSilently() {
if (this.requesting) {
return
}
const requestForm = Object.assign({}, this.pageForm, {
pageNumber: 1
})
this.requesting = true
this.$axios.post("/platform/h5/qsv/pageData", requestForm).then((res) => {
if (res.code === 0) {
const pageData = res.data || {}
const rows = pageData.list || []
this.list = rows
this.pageForm = Object.assign({}, this.pageForm, {
pageNumber: 2,
totalCount: pageData.totalCount || 0
})
this.finished = rows.length >= this.pageForm.totalCount || rows.length === 0
this.saveListCache()
}
}).finally(() => {
this.requesting = false
this.loading = false
this.refreshing = false
})
},
getCategoryText(category) {
const map = {
QUIZ: "答题",
SURVEY: "问卷",
VOTE: "投票"
}
return map[category] || "活动"
},
getCategoryIcon(category) {
const map = {
QUIZ: "fa fa-check-square-o",
SURVEY: "fa fa-list-alt",
VOTE: "fa fa-bar-chart"
}
return map[category] || "fa fa-tags"
},
getAnsweredText(category) {
const map = {
QUIZ: "已答题",
SURVEY: "已填写",
VOTE: "已投票"
}
return map[category] || "已完成"
},
formatDateTime(value) {
if (!value) {
return "-"
}
return this.$moment(value).format("YYYY-MM-DD HH:mm")
},
onEnter(item) {
const {startTime} = item
if (this.$moment(startTime).unix() > this.$moment().unix()) {
this.$toast("未开始")
return
}
const path = this.categoryPaths[item.category]
if (path) {
this.saveListCache()
pjaxReplace(path + "?id=" + item.id)
}
},
getCacheKey() {
return "qsv:h5:list"
},
saveListCache() {
try {
sessionStorage.setItem(this.getCacheKey(), JSON.stringify({
time: Date.now(),
list: this.list,
pageForm: this.pageForm,
finished: this.finished
}))
} catch (e) {
}
},
restoreListCache() {
try {
const cacheText = sessionStorage.getItem(this.getCacheKey())
if (!cacheText) {
return
}
const cache = JSON.parse(cacheText)
if (!cache || Date.now() - cache.time > 5 * 60 * 1000) {
this.clearListCache()
return
}
this.list = cache.list || []
this.pageForm = Object.assign({}, this.pageForm, cache.pageForm || {})
this.finished = !!cache.finished
this.cacheRestored = this.list.length > 0
} catch (e) {
this.clearListCache()
}
},
clearListCache() {
try {
sessionStorage.removeItem(this.getCacheKey())
} catch (e) {
}
}
},
created() {
this.restoreListCache()
},
mounted() {
if (this.cacheRestored) {
setTimeout(() => {
this.refreshListSilently()
}, 200)
}
}
})
</script>
<style>
.qsv-h5-page {
min-height: 100vh;
background: #f4f8ff;
padding-bottom: 18px;
box-sizing: border-box;
}
.qsv-h5-header {
padding: 10px 14px 16px;
background: linear-gradient(180deg, #1688f8 0%, #0f83ee 100%);
box-sizing: border-box;
}
.qsv-h5-nav {
height: 38px;
display: flex;
align-items: center;
margin-bottom: 8px;
color: #ffffff;
}
.qsv-h5-back-btn,
.qsv-h5-nav-placeholder {
width: 36px;
height: 36px;
flex-shrink: 0;
}
.qsv-h5-back-btn {
border: 0;
border-radius: 50%;
background: rgba(255, 255, 255, .16);
color: #ffffff;
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
padding: 0;
}
.qsv-h5-nav-title {
flex: 1;
min-width: 0;
text-align: center;
font-size: 18px;
font-weight: 700;
line-height: 36px;
}
.qsv-h5-search-row {
display: flex;
align-items: center;
gap: 10px;
}
.qsv-h5-header .van-search {
flex: 1;
min-width: 0;
padding: 0;
}
.qsv-h5-header .van-search__content {
background: rgba(255, 255, 255, .92);
}
.qsv-h5-search-btn {
width: 42px;
height: 42px;
border: 1px solid rgba(255, 255, 255, .45);
border-radius: 50%;
background: rgba(255, 255, 255, .18);
color: #ffffff;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
font-size: 18px;
box-sizing: border-box;
}
.qsv-h5-filter-row {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 10px;
margin-top: 12px;
padding-bottom: 1px;
}
.qsv-h5-filter-row::-webkit-scrollbar {
display: none;
}
.qsv-h5-filter-btn {
height: 32px;
width: 100%;
padding: 0 6px;
border: 0;
border-radius: 17px;
background: rgba(255, 255, 255, .92);
color: #1688f8;
font-size: 14px;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 6px;
white-space: nowrap;
}
.qsv-h5-filter-btn.active {
background: #ffffff;
font-weight: 600;
}
.qsv-h5-filter-btn i {
font-size: 15px;
}
.qsv-h5-list-wrap {
min-height: calc(100vh - 170px);
padding: 14px;
box-sizing: border-box;
}
.qsv-h5-card {
display: flex;
gap: 14px;
padding: 14px;
margin-bottom: 14px;
background: #ffffff;
border-radius: 13px;
box-shadow: 0 8px 24px rgba(35, 102, 168, .1);
box-sizing: border-box;
}
.qsv-h5-cover {
width: 116px;
height: 87px;
aspect-ratio: 4 / 3;
border-radius: 8px;
overflow: hidden;
background: #eaf2ff;
flex-shrink: 0;
}
.qsv-h5-cover img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
.qsv-h5-card-body {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
justify-content: space-between;
gap: 8px;
padding: 2px 0;
}
.qsv-h5-title-row {
display: flex;
align-items: flex-start;
gap: 8px;
}
.qsv-h5-title {
flex: 1;
min-width: 0;
color: #1f2f46;
font-size: 16px;
font-weight: 700;
line-height: 1.35;
overflow: hidden;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
}
.qsv-h5-answered {
height: 22px;
min-width: 50px;
padding: 0 8px;
border: 1px solid #19a05f;
border-radius: 11px;
background: #f0fff8;
color: #149356;
font-size: 12px;
font-weight: 700;
line-height: 20px;
text-align: center;
white-space: nowrap;
flex-shrink: 0;
box-sizing: border-box;
}
.qsv-h5-meta-row {
display: flex;
align-items: center;
min-height: 20px;
}
.qsv-h5-type {
display: inline-flex;
align-items: center;
gap: 4px;
height: 20px;
padding: 0 6px;
border: 1px solid #ff9f43;
border-radius: 4px;
background: #fff7ec;
color: #f28b18;
font-size: 12px;
font-weight: 600;
line-height: 18px;
flex-shrink: 0;
box-sizing: border-box;
}
.qsv-h5-type i {
color: #f28b18;
font-size: 12px;
}
.qsv-h5-time {
color: #71829b;
font-size: 12px;
line-height: 1.75;
}
@media (max-width: 360px) {
.qsv-h5-card {
gap: 10px;
padding: 12px;
}
.qsv-h5-cover {
width: 96px;
height: 72px;
}
.qsv-h5-title {
font-size: 16px;
}
}
</style>
<!--#
}
#-->
@@ -0,0 +1,474 @@
<!--#
layout("/layouts/platform_h5.html"){
#-->
<div id="app" v-cloak>
<van-nav-bar @click-left="historyBack" left-arrow left-text="返回" :title="pageTitle" placeholder fixed></van-nav-bar>
<div class="container" :style="{'padding-bottom': answerRecord.isFinish ? 0 : '84px'}">
<div class="quiz-hero" :style="heroStyle">
<div class="quiz-hero-mask">
<div class="timer" v-if="activity.timeLimit > 0 && !answerRecord.isFinish && !isEnd">
⏰ {{ remainingTime }}s
</div>
<h2 class="quiz-title">{{ activity.title }}</h2>
</div>
</div>
<div class="quiz-card-scroll" 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="info" @click="onSubmit" block :disabled="isEnd">{{isEnd ? '已结束' : '提交'}}
</van-button>
</div>
</div>
</div>
</div>
<script nonce="${cspNonce!}">
const vue = new Vue({
el: "#app",
store,
data() {
return {
list: [],
id: GetQueryString("id"),
H5JumpTo: GetQueryString("H5JumpTo"),
subjects: [],
activity: {},
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
},
pageTitle() {
return this.getCategoryPageTitle(this.activity && this.activity.category, "在线答题")
},
heroStyle() {
if (this.activity && this.activity.cover) {
return {
backgroundImage: "url(" + this.activity.cover + ")"
}
}
return {}
}
},
methods: {
getCategoryPageTitle(category, defaultTitle) {
const titleMap = {
QUIZ: "在线答题",
SURVEY: "问卷调查",
VOTE: "投票"
}
return titleMap[category] || defaultTitle
},
//获取活动、题目
listSubjects() {
this.$axios.post("/platform/h5/qsv/quiz/subjects", {activityId: this.id}).then((res) => {
if (res.code === 0) {
this.activity = res.data.activity
// 历史链接可能误将投票或问卷指向答题页,按活动实际类型跳转到对应的移动端入口。
if (this.activity.category === "VOTE") {
pjaxReplace("/platform/h5/qsv/vote?id=" + this.id)
return
}
if (this.activity.category === "SURVEY") {
pjaxReplace("/platform/h5/qsv/survey?id=" + this.id)
return
}
this.subjects = res.data.subjects
this.answerRecordId = res.data.answerRecordId
// this.checkGroupPermission()
if (!this.H5JumpTo && this.activity.repeatable && res.data.repeatTips) {
// 如果是可重复答题,第二次答题进来弹出提示
vant.Dialog.confirm({
title: '温馨提示',
message: '您已完成本次答题,是否要重新答题?',
confirmButtonText: '重新答题',
cancelButtonText: '查看答题记录',
}).then(() => {
this.getAnswerRecord()
}).catch(() => {
pjaxReplace("/platform/h5/qsv/quiz/result?answerRecordId=" + this.answerRecordId)
// pjaxReplace("/mobile/index")
});
} else {
this.getAnswerRecord()
}
} else {
vant.Dialog.alert({
title: '温馨提示',
message: res.msg,
}).then(() => {
pjaxReplace("/mobile/index")
})
}
})
},
//获取回答记录
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)) {
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()
})
}
})
},
historyBack() {
if (this.timerInterval) {
clearInterval(this.timerInterval)
}
historyBack()
},
//答题计时器
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)
this.$dialog.alert({
title: "提示",
message: "答题时间到,请刷新页面重新答题",
}).then(() => {
window.location.reload()
})
// 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)
pjaxReplace("/platform/h5/qsv/quiz/result?answerRecordId=" + this.answerRecordId)
}
})
.always(() => {
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()
}
},
style: /*language=CSS*/ `
.container {
padding: 0;
background: #f4f8ff;
height: calc(100vh - 46px);
overflow: hidden;
display: flex;
flex-direction: column;
box-sizing: border-box;
}
/deep/ .quiz-hero {
flex: 0 0 auto;
min-height: 170px;
background: linear-gradient(135deg, #1688f8, #0a55c5);
background-size: cover;
background-position: center;
position: relative;
overflow: hidden;
margin: 12px 12px 14px;
border-radius: 16px;
box-shadow: 0 8px 24px rgba(35, 102, 168, .14);
}
/deep/ .quiz-hero-mask {
min-height: 170px;
padding: 24px 20px 22px;
display: flex;
flex-direction: column;
justify-content: flex-end;
box-sizing: border-box;
background: linear-gradient(180deg, rgba(0, 0, 0, .16), rgba(0, 0, 0, .56));
}
/deep/ .quiz-title {
margin: 0;
color: #fff;
font-size: 22px;
font-weight: 700;
line-height: 1.35;
text-shadow: 0 2px 8px rgba(0, 0, 0, .28);
}
.quiz-card-scroll {
flex: 1;
min-height: 0;
overflow-y: auto;
-webkit-overflow-scrolling: touch;
padding: 0 12px;
box-sizing: border-box;
}
.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;
}
/deep/ .timer {
align-self: flex-start;
margin-bottom: 12px;
padding: 5px 10px;
border-radius: 14px;
background: rgba(255, 255, 255, .22);
color: #fff;
font-size: 14px;
font-weight: 600;
backdrop-filter: blur(6px);
}
.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;
}
`
})
</script>
<!--#
}
#-->
@@ -0,0 +1,364 @@
<!--#
layout("/layouts/platform_h5.html"){
#-->
<div id="app" v-cloak>
<van-nav-bar @click-left="historyBack" left-arrow left-text="返回" title="问卷列表" placeholder fixed></van-nav-bar>
<div class="container" :style="{'padding-bottom': answerRecord.isFinish ? 0 : '84px'}">
<div class="quiz-hero" :style="heroStyle">
<div class="quiz-hero-mask">
<h2 class="quiz-title">{{ activity.title }}</h2>
<div class="quiz-score">
<span>{{totalScore}}</span>
<em>分</em>
</div>
</div>
</div>
<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 v-if="canAgainQuestion" type="primary" @click="againQuestion" block>重新答题</van-button>
<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: [],
canAgainQuestion: false
}
},
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
},
heroStyle() {
if (this.activity && this.activity.cover) {
return {
backgroundImage: "url(" + this.activity.cover + ")"
}
}
return {}
}
},
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: {
/**
* 重新答题
*/
againQuestion() {
vant.Dialog.confirm({
title: '温馨提示',
message: '确定要重新答题吗?',
}).then(() => {
pjaxReplace('/platform/h5/qsv/quiz?id=' + this.activity.id + "&H5JumpTo=true")
}).catch(() => {
// on cancel
});
},
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()
this.getHistoryQuestion()
}
})
},
//获取回答记录
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() {
pjaxReplace('/platform/h5/qsv')
},
openHistory() {
this.historyShow = true
},
getHistoryQuestion() {
this.$axios.post("/platform/h5/qsv/quiz/historyScore", { activityId: this.activity.id }).then((res) => {
if (res.code === 0) {
this.historyList = res.data
if (this.activity.maxAttempts > res.data.length) {
this.canAgainQuestion = true
}
}
})
}
},
created() {
this.getAnswerResult()
},
style: /*language=CSS*/ `
.container {
padding: 0;
background: #f4f8ff;
min-height: 100vh;
}
/deep/ .quiz-hero {
min-height: 190px;
background: linear-gradient(135deg, #1688f8, #0a55c5);
background-size: cover;
background-position: center;
position: relative;
overflow: hidden;
margin: 12px 12px 14px;
border-radius: 16px;
box-shadow: 0 8px 24px rgba(35, 102, 168, .14);
}
/deep/ .quiz-hero-mask {
min-height: 190px;
padding: 28px 20px 24px;
display: flex;
flex-direction: column;
justify-content: flex-end;
box-sizing: border-box;
background: linear-gradient(180deg, rgba(0, 0, 0, .18), rgba(0, 0, 0, .58));
}
/deep/ .quiz-title {
margin: 0 0 14px;
color: #fff;
font-size: 22px;
font-weight: 700;
line-height: 1.35;
text-shadow: 0 2px 8px rgba(0, 0, 0, .28);
}
/deep/ .quiz-score {
display: flex;
align-items: flex-end;
color: #fff;
text-shadow: 0 2px 10px rgba(0, 0, 0, .3);
}
/deep/ .quiz-score span {
font-size: 48px;
font-weight: 800;
line-height: 1;
}
/deep/ .quiz-score em {
margin-left: 6px;
margin-bottom: 5px;
font-style: normal;
font-size: 18px;
font-weight: 600;
}
.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;
}
.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;
}
/deep/ .history-list {
background: #f1f1f1;
padding: 10px;
}
/deep/ .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;
}
/deep/ .history-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
}
/deep/ .history-header span {
font-size: 14px;
font-weight: 600;
color: #333;
}
/deep/ .history-header .tag {
margin-left: 8px;
color: #fff;
padding: 2px 5px;
}
/deep/ .history-info {
font-size: 14px;
color: #555;
margin-bottom: 4px;
}
`
})
</script>
<!--#
}
#-->
@@ -0,0 +1,666 @@
<!--#
layout("/layouts/platform_h5.html"){
#-->
<style>
.container {
padding: 0;
background: #f4f8ff;
height: calc(100vh - 46px);
overflow: hidden;
display: flex;
flex-direction: column;
box-sizing: border-box;
}
.quiz-hero {
flex: 0 0 auto;
min-height: 170px;
background: linear-gradient(135deg, #1688f8, #0a55c5);
background-size: cover;
background-position: center;
position: relative;
overflow: hidden;
margin: 12px 12px 14px;
border-radius: 16px;
box-shadow: 0 8px 24px rgba(35, 102, 168, .14);
}
.quiz-hero-mask {
min-height: 170px;
padding: 24px 20px 22px;
display: flex;
flex-direction: column;
justify-content: flex-end;
box-sizing: border-box;
background: linear-gradient(180deg, rgba(0, 0, 0, .16), rgba(0, 0, 0, .56));
}
.quiz-title {
margin: 0;
color: #fff;
font-size: 22px;
font-weight: 700;
line-height: 1.35;
text-shadow: 0 2px 8px rgba(0, 0, 0, .28);
}
.quiz-card-scroll {
flex: 1;
min-height: 0;
overflow-y: auto;
-webkit-overflow-scrolling: touch;
padding: 0 12px;
box-sizing: border-box;
}
.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,
.van-radio__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,
.van-radio__icon--disabled .van-icon {
background-color: #fff;
}
.survey-option-finished-selected .survey-option-text {
color: #00a000;
font-weight: 600;
}
.survey-option-finished-selected .van-radio__icon--disabled .van-icon,
.survey-option-finished-selected .van-checkbox__icon--disabled .van-icon {
color: #fff !important;
background-color: #d8d8d8 !important;
border-color: #d8d8d8 !important;
}
.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%;
}
.option-fill-box {
border: 1px solid #e3e3e3;
margin: 0 16px 8px 46px;
background-color: #fff;
padding: 0;
display: flex;
}
.option-fill-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%;
}
.required-star {
color: #ee0a24;
margin-left: 4px;
font-weight: bold;
}
</style>
<div id="app" v-cloak>
<van-nav-bar :title="pageTitle" left-arrow left-text="返回" @click-left="historyBack" placeholder fixed></van-nav-bar>
<div class="container" :style="{'padding-bottom': '84px'}">
<div class="quiz-hero" :style="heroStyle">
<div class="quiz-hero-mask">
<h2 class="quiz-title">{{ activity.title }}</h2>
</div>
</div>
<div class="quiz-card-scroll" v-if="!isFinished">
<div v-for="(subject, index) in visibleSubjects" :key="subject.id" class="subject">
<p>{{index+1}}、{{ subject.title }}<span class="required-star">*</span></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-radio-group
:value="getRadioValue(subject)"
v-if="subject.type === 'radio'"
>
<van-cell-group>
<template v-for="(option,index) in subject.options">
<van-cell
clickable
:key="option.id"
:title="option.text"
:class="getFinishedOptionClass(subject, option)"
@click="cellToggle(subject,option.id)"
>
<template #title>
<span class="survey-option-text">{{option.text}}</span>
</template>
<template #icon>
<van-radio :name="option.id" :disabled="answerRecord.isFinish" style="margin-right: 10px"></van-radio>
</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>
<div
class="option-fill-box"
v-if="isOptionFillVisible(subject, option)"
:key="option.id + '-fill'">
<input
v-model="subject.optionFillContents[option.id]"
:placeholder="option.fillPlaceholder || '请输入补充内容'">
</div>
</template>
</van-cell-group>
</van-radio-group>
<!--多选-->
<van-checkbox-group
v-model="subject.userSelectOptionIds"
v-if="subject.type === 'checkbox'"
>
<van-cell-group>
<template v-for="(option,index) in subject.options">
<van-cell
clickable
:key="option.id"
:title="option.text"
:class="getFinishedOptionClass(subject, option)"
@click="cellToggle(subject,option.id)"
>
<template #title>
<span class="survey-option-text">{{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>
<img
slot="right-icon"
v-if="option.imgUrl"
:src="option.imgUrl"
alt=""
style="width: 40px; height: 40px"
@click.stop="previewOptionImg(option.imgUrl)"
/>
</van-cell>
<div
class="option-fill-box"
:key="option.id + '-fill'"
v-if="isOptionFillVisible(subject, option)">
<input
type="text"
v-model="subject.optionFillContents[option.id]"
:placeholder="option.fillPlaceholder || '请填写补充内容'"
:readonly="answerRecord.isFinish"
@click.stop />
</div>
</template>
</van-cell-group>
</van-checkbox-group>
<!--填空题-->
<div class="ui-input-box" v-if="subject.type==='text'">
<input type="text" v-model="subject.userFillContent" :placeholder="'请输入' + subject.title" :readonly="answerRecord.isFinish" />
</div>
</div>
<div class="button-control">
<van-button v-if="!answerRecord.isFinish" type="primary" @click="onSubmit" block :disabled="isEnd">{{isEnd ? '已结束' : '提交'}}</van-button>
<van-button v-else type="primary" @click="historyBack" block>返回</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
},
visibleSubjects() {
return this.subjects.filter((subject) => {
return this.isSubjectVisible(subject)
})
},
pageTitle() {
return this.getCategoryPageTitle(this.activity && this.activity.category, "问卷调查")
},
heroStyle() {
if (this.activity && this.activity.cover) {
return {
backgroundImage: "url(" + this.activity.cover + ")"
}
}
return {}
}
},
methods: {
getCategoryPageTitle(category, defaultTitle) {
const titleMap = {
QUIZ: "在线答题",
SURVEY: "问卷调查",
VOTE: "投票"
}
return titleMap[category] || defaultTitle
},
//获取活动、题目
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)) {
const optionIds = answer?.optionIds || []
this.$set(subject, "userSelectOptionIds", subject.type === "radio" ? optionIds.slice(0, 1) : optionIds)
this.$set(subject, "optionFillContents", answer?.optionFillContents || {})
} else if (subject.type === "text") {
this.$set(subject, "userFillContent", answer?.text)
}
})
if (!this.answerRecord.isFinish) {
this.clearHiddenSubjectAnswers()
}
},
//判断题目是否显示,未配置隐显逻辑的题目默认显示;已配置的题目按条件树递归计算后再执行显示或隐藏动作
isSubjectVisible(subject) {
if (!subject.visibleRuleEnabled) {
return true
}
const rootGroup = this.getVisibleRuleRootGroup(subject)
if (!rootGroup) {
return true
}
const matched = this.evaluateVisibleRuleGroup(rootGroup)
if (subject.visibleRuleAction === "hide") {
return !matched
}
return matched
},
//读取隐显逻辑根分组,兼容旧版 subjectId/optionId 平铺条件
getVisibleRuleRootGroup(subject) {
let conditions = subject.visibleRuleConditions || []
if (typeof conditions === "string") {
try {
conditions = JSON.parse(conditions)
} catch (e) {
conditions = []
}
}
if (!Array.isArray(conditions) || conditions.length === 0) {
return null
}
if (conditions.length === 1 && conditions[0].type === "group") {
return this.normalizeVisibleRuleGroup(conditions[0])
}
const children = conditions.filter((condition) => {
return condition.subjectId && condition.optionId
}).map((condition) => {
return {
type: "item",
field: condition.subjectId,
operator: "contains",
value: condition.optionId
}
})
if (children.length === 0) {
return null
}
return {
type: "group",
logic: subject.visibleRuleLogic === "OR" ? "or" : "and",
children
}
},
//规整条件分组,避免历史数据或空字段影响递归判断
normalizeVisibleRuleGroup(group) {
return {
type: "group",
logic: (group.logic || "and").toLowerCase() === "or" ? "or" : "and",
children: Array.isArray(group.children) ? group.children.map((child) => {
if (child.type === "group") {
return this.normalizeVisibleRuleGroup(child)
}
return {
type: "item",
field: child.field || child.subjectId || "",
operator: child.operator || (child.optionId ? "contains" : "=="),
value: child.value !== undefined && child.value !== null ? child.value : (child.optionId || "")
}
}) : []
}
},
//递归计算条件分组,支持并且、或者和嵌套分组
evaluateVisibleRuleGroup(group) {
const children = (group.children || []).filter((child) => {
return child.type === "group" || (child.field && child.operator)
})
if (children.length === 0) {
return true
}
const results = children.map((child) => {
if (child.type === "group") {
return this.evaluateVisibleRuleGroup(child)
}
return this.evaluateVisibleRuleItem(child)
})
return group.logic === "or"
? results.some((item) => {
return item
})
: results.every((item) => {
return item
})
},
//计算单条条件,选择题按选项ID判断,填空题支持文本包含和数字大小比较
evaluateVisibleRuleItem(condition) {
const triggerSubject = this.subjects.find((item) => {
return item.id === condition.field
})
if (!triggerSubject) {
return false
}
const answer = ["radio", "checkbox"].includes(triggerSubject.type)
? (triggerSubject.userSelectOptionIds || [])
: triggerSubject.userFillContent
return this.compareVisibleRuleValue(answer, condition.operator, condition.value)
},
//比较条件值,和后台逻辑抽屉实时预览保持同一套语义
compareVisibleRuleValue(answer, operator, value) {
if (Array.isArray(answer)) {
if (operator === "!=") {
return !answer.includes(value)
}
return answer.includes(value)
}
if ([">", "<", ">=", "<="].includes(operator)) {
const answerNumber = Number(answer)
const valueNumber = Number(value)
if (isNaN(answerNumber) || isNaN(valueNumber)) {
return false
}
if (operator === ">") {
return answerNumber > valueNumber
}
if (operator === "<") {
return answerNumber < valueNumber
}
if (operator === ">=") {
return answerNumber >= valueNumber
}
return answerNumber <= valueNumber
}
const answerText = answer === undefined || answer === null ? "" : String(answer)
const valueText = value === undefined || value === null ? "" : String(value)
if (operator === "!=") {
return answerText !== valueText
}
if (operator === "contains") {
return answerText.indexOf(valueText) !== -1
}
return answerText === valueText
},
//清空当前隐藏题目的答案,避免隐藏题残留旧选项或填空内容参与后续提交
clearHiddenSubjectAnswers() {
this.subjects.forEach((subject) => {
if (this.isSubjectVisible(subject)) {
return
}
if (["radio", "checkbox"].includes(subject.type)) {
this.$set(subject, "userSelectOptionIds", [])
this.$set(subject, "optionFillContents", {})
} else if (subject.type === "text") {
this.$set(subject, "userFillContent", null)
}
})
},
//判断选项是否已被选择,用于控制选项补充填写框显示和提交校验
isOptionSelected(subject, optionId) {
return subject.userSelectOptionIds && subject.userSelectOptionIds.includes(optionId)
},
getFinishedOptionClass(subject, option) {
return this.answerRecord.isFinish && this.isOptionSelected(subject, option.id)
? "survey-option-finished-selected"
: ""
},
//单选题仍按数组保存,这里只取第一个值用于 radio 组件展示
getRadioValue(subject) {
return subject.userSelectOptionIds && subject.userSelectOptionIds.length > 0
? subject.userSelectOptionIds[0]
: ""
},
//选中需要补充填写的选项后显示输入框,例如选择“否”后填写原因
isOptionFillVisible(subject, option) {
return option.fillRequired && this.isOptionSelected(subject, option.id)
},
//清空未选中选项的补充填写内容,避免取消选择后残留旧内容
clearUnselectedOptionFillContents(subject) {
if (!subject.optionFillContents) {
this.$set(subject, "optionFillContents", {})
}
if (!subject.options) {
return
}
subject.options.forEach((option) => {
if (!this.isOptionSelected(subject, option.id)) {
this.$delete(subject.optionFillContents, option.id)
}
})
},
//选项点击
cellToggle(subject, optionId) {
if (this.answerRecord.isFinish) {
return
}
if (subject.type === "radio") {
this.$set(subject, "userSelectOptionIds", [optionId])
} else {
this.$refs["option" + optionId][0].toggle()
}
this.$nextTick(() => {
this.clearUnselectedOptionFillContents(subject)
this.clearHiddenSubjectAnswers()
})
},
//预览图片
previewOptionImg(img) {
vant.ImagePreview([img])
},
//手动提交
onSubmit() {
const endTime = this.activity.endTime
if (this.isEnd) {
this.$toast("调查已结束")
return
}
//提示那些题没有作答
for (let i = 0; i < this.visibleSubjects.length; i++) {
const subject = this.visibleSubjects[i]
if (["radio", "checkbox"].includes(subject.type)) {
if (!subject.userSelectOptionIds || subject.userSelectOptionIds.length === 0) {
this.$toast.fail("第" + (i + 1) + "题未做答")
return
}
for (let j = 0; j < subject.options.length; j++) {
const option = subject.options[j]
if (this.isOptionFillVisible(subject, option) && !subject.optionFillContents[option.id]) {
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.visibleSubjects.map((subject) => {
return {
id: subject.id,
userSelectOptionIds: ["checkbox", "radio"].includes(subject.type) ? subject.userSelectOptionIds : [],
userFillContent: subject.type === "text" ? subject.userFillContent : null,
optionFillContents: ["checkbox", "radio"].includes(subject.type) ? subject.optionFillContents : {}
}
})
})
})
.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>
<!--#
}
#-->
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,366 @@
const apply = {
template: /*language=HTML*/ `
<div>
<van-popup v-model:show="visible" :style="{ height: '100%',width: '100%', 'background-color': '#F7F8FA'}" position="right" safe-area-inset-bottom>
<van-nav-bar :title="row.name" left-text="返回" left-arrow @click-left="visible = false" fixed placeholder></van-nav-bar>
<div class="top-calendar">
<div class="calendar-left">
<div :class="getDayClass(item)" @click="dayClick(item)" v-for="item in weekList">
<div class="day-info-week">
{{item.weekdayCN}}
</div>
<div class="day-info-month">
{{$moment(item.day).format('MM-DD')}}
</div>
</div>
</div>
<div class="calendar-right">
<van-icon size="16" color="white" name="arrow-down" @click="calendarVisible = true"></van-icon>
<van-calendar class="calendar-popup" v-model="calendarVisible" color="#246fb4" position="top" @confirm="calendarConfirm"
:default-date="new Date(selectDate)"
:formatter="calendarFormatter"
:first-day-of-week="1"
title="选择开始日期"></van-calendar>
</div>
</div>
<div class="container">
<table v-if="weekList.find(o => $moment(o.day).unix() === $moment(selectDate).unix())?.disabled === false" style="width: 100%">
<template v-for="item in allowTimeList">
<tr>
<td class="container-times">{{item.startTime + '-' + item.endTime}}</td>
<td>
<div v-if="item.disabled === true" class="container-state">{{ item.tooltip }}</div>
<div @click="selectTime(item)" v-else class="container-state" style="background-color: #e8ffef; color: #52986a">
<label v-if="(item.dateStr + ' ' + item.startTime + '-' + item.endTime) !== (selected.dateStr + ' ' + selected.startTime + '-' + selected.endTime)">
可预约
</label>
<span v-else>
<van-icon name="passed" size="26" style="line-height: 36px;position: unset"></van-icon>
</span>
</div>
</td>
</tr>
</template>
</table>
<van-empty v-else image="error" :description="selectDate + '不能预约'"></van-empty>
</div>
<div style="position: fixed; bottom: 0; width: 100%">
<van-button @click="onApply" type="primary" color="#246fb4" style="width: 100%">确定预约</van-button>
</div>
</van-popup>
<van-action-sheet v-model="formVisible" title="确认预约信息">
<van-notice-bar left-icon="volume-o"
:text="'您已选择的预约日期为:' + selectDate"></van-notice-bar>
<van-form ref="formRef" class="form-container">
<van-field label="预约人" :rules="[{ required: true }]" v-model="formData.applyUserName" readonly
required name="applyUserName"></van-field>
<van-field label="性别" :rules="[{ required: true }]" v-model="formData.sex" readonly
required name="sex"></van-field>
<van-field label="工号" :rules="[{ required: true }]" v-model="formData.applyLoginName" readonly
required name="applyLoginName"></van-field>
<van-field label="所属单位" :rules="[{ required: true }]" v-model="formData.applyUnitName" readonly
required name="applyUnitName"></van-field>
<van-field label="联系电话" :rules="[{ required: true }]" v-model="formData.applyMobile" type="number"
required name="applyMobile"></van-field>
<van-field label="预约时间" :rules="[{ required: true }]" v-model="formData.applyTime"
required readonly name="applyTime"></van-field>
<van-field name="stepper" label="预约人数" required :rules="[{ required: true }]">
<template #input>
<van-stepper
:disabled="row.reserveTarget === 1 || row.reserveTarget === 3"
v-model="formData.joinCount"></van-stepper>
</template>
</van-field>
<van-field
v-model="formData.applyCause"
rows="4"
autosize
label="预约事由"
type="textarea"
maxlength="50"
placeholder="请输入预约事由"
show-word-limit
required
name="applyCause"
:rules="[{ required: true }]"
></van-field>
<div class="form-actions">
<van-button @click="formVisible = false" type="info" plain round>关闭</van-button>
<van-button @click="onSubmit" v-if="!taskId" type="info" round>提交</van-button>
<van-button @click="onFinishTask" v-else type="info" round>提交</van-button>
</div>
</van-form>
</van-action-sheet>
</div>
`,
store,
data() {
return {
taskId: '',
bizId: '',
row: {},
visible: false,
selectDate: '',
selected: {},
weekList: [],
allowDayList: [],
allowTimeList: [],
weekdayCNMap: {0: '周日', 1: '周一', 2: '周二', 3: '周三', 4: '周四', 5: '周五', 6: '周六'},
formVisible: false,
formData: {
applyCause: '',
},
calendarVisible: false,
}
},
computed: {
calendarFormatter(day) {
return (day) => {
if(!this.allowDayList.includes(this.$moment(day.date).format('YYYY-MM-DD'))) {
day.type = 'disabled'
}
return day
}
}
},
methods: {
async onOpen(row, applyRow = null) {
this.row = row
this.visible = true
await this.init()
if(applyRow) {
this.taskId = applyRow.taskId
this.bizId = applyRow.bizId
this.applyTime = applyRow.applyDay
this.selectDate = applyRow.applyDay
this.selected = {
dateStr: applyRow.applyDay,
startTime: applyRow.startTime,
endTime: applyRow.endTime,
}
this.formVisible = true
this.formData = clone(applyRow)
this.$set(this.formData, 'applyTime', this.formData.startTime + '-' + this.formData.endTime)
this.$set(this.formData, 'sex', this.$store.state.user.sex)
const day = this.weekList.find(o => o.dateStr === applyRow.applyDay)
if(day) {
await this.dayClick(day)
}
}
},
async init() {
this.selectDate = this.$moment().format('YYYY-MM-DD')
await this.queryAllowDay()
await this.queryTimesByDay()
this.setWeekList(this.selectDate)
},
async calendarConfirm(date) {
this.selectDate = this.$moment(date).format('YYYY-MM-DD')
this.setWeekList(date)
await this.queryTimesByDay()
this.calendarVisible = false
},
onApply() {
if(Object.keys(this.selected).length === 0) {
this.$toast('请选择要预约的时间')
return
}
this.$set(this.formData, "applyUserId", this.$store.state.user.id)
this.$set(this.formData, "applyUserName", this.$store.state.user.username)
this.$set(this.formData, "applyLoginName", this.$store.state.user.loginname,)
this.$set(this.formData, "applyUnitId", this.$store.state.user?.unit?.id)
this.$set(this.formData, "applyUnitName", this.$store.state.user?.unit?.name)
this.$set(this.formData, "applyUnionId", this.$store.state.user?.union?.id)
this.$set(this.formData, "applyUnionName", this.$store.state.user?.union?.name)
this.$set(this.formData, "applyMobile", this.$store.state.user.mobile)
this.$set(this.formData, "sex", this.$store.state.user.sex)
this.$set(this.formData, "siteId", this.row.id)
this.$set(this.formData, "applyTime", this.selected.startTime + '-' + this.selected.endTime)
this.$set(this.formData, "startTime", this.selected.startTime)
this.$set(this.formData, "endTime", this.selected.endTime)
if (this.row.reserveTarget === 1 || this.row.reserveTarget === 3) {
this.$set(this.formData, "joinCount", this.row.maxNum)
} else {
this.$set(this.formData, "joinCount", 1)
}
this.formVisible = true
},
onSubmit() {
this.$refs.formRef.validate().then(() => {
this.$dialog.confirm({
title: "提示",
message: "您确定要提交吗?"
}).then(async () => {
const res = await this.$axios.post("/platform/site/apply/submit", {
data: JSON.stringify(this.formData),
days: JSON.stringify([this.selected.dateStr])
})
this.$toast(res.msg)
if (res.code === 0) {
this.$pjaxReplace("/platform/site/mine/h5")
}
})
})
},
onFinishTask() {
if (!this.formData.applyTime) {
this.$toast('未选择预约时间')
return
}
this.$refs.formRef.validate().then(() => {
this.$dialog.confirm({
title: "提示",
message: "您确定要提交吗?"
}).then(() => {
this.$axios.post('/platform/site/apply/submitAgain', {
data: JSON.stringify(this.formData),
days: JSON.stringify([this.selected.dateStr]),
taskId: this.taskId
}).then(res => {
this.$toast(res.msg)
if (res.code === 0) {
this.$pjaxReplace("/platform/site/mine/h5")
}
})
})
})
},
selectTime(item) {
if((item.dateStr + ' ' + item.startTime + '-' + item.endTime) !== (this.selected.dateStr + ' ' + this.selected.startTime + '-' + this.selected.endTime)) {
this.$set(this.selected, 'day', item.dateStr)
this.$set(this.selected, "dateStr", item.dateStr)
this.$set(this.selected, 'startTime', item.startTime)
this.$set(this.selected, 'endTime', item.endTime)
} else {
this.selected = {}
}
},
// 获取每天的样式
getDayClass(row) {
let classStr = 'calendar-left-day'
if(row.disabled) {
classStr += ' left-day-disabled'
} else {
classStr += this.$moment(row.day).unix() === this.$moment(this.selectDate).unix() ? ' left-day-active' : ''
}
return classStr
},
// 点击某一天
async dayClick(item) {
this.selectDate = item.dateStr
if(item.disabled) {
this.$toast(this.$moment(item.day).format('YYYY-MM-DD') + '不可预约')
} else {
await this.queryTimesByDay()
}
},
// 获取一周
setWeekList(startDate) {
const start = this.$moment(startDate)
const allowDaySet = new Set(this.allowDayList)
this.weekList = Array.from({ length: 8 }, (_, index) => {
const day = start.clone().add(index, 'd')
const dateStr = day.format('YYYY-MM-DD')
const disabled = this.allowDayList.length > 0 && !allowDaySet.has(dateStr)
const weekdayCN = this.weekdayCNMap[day.day()]
return { day, disabled, weekdayCN, dateStr }
})
},
// 查询哪些天是开放的
async queryAllowDay() {
const {data} = await this.$axios.post("/platform/site/apply/queryAllowDay", {
siteId: this.row.id,
})
this.allowDayList = data
},
// 查询时间段
async queryTimesByDay() {
const {data} = await this.$axios.post("/platform/site/apply/queryTimesByDay", {
siteId: this.row.id,
day: this.selectDate,
})
this.allowTimeList = data
},
},
style: /*language=CSS*/ `
/deep/ .top-calendar {
height: 70px;
max-height: 70px;
position: sticky;
top: 0;
left: 0;
width: 100%;
max-width: 100%;
display: flex;
align-items: center;
background-color: #0e78c5;
}
/deep/ .calendar-left {
/*width: calc(100% - 20px);*/
overflow-x: auto;
display: flex;
height: 90%;
}
/deep/ .calendar-left-day {
min-width: 65px;
height: 100%;
margin: 0 5px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
color: #FFF;
border-radius: 10px;
}
/deep/ .left-day-active {
background-color: #0000cc;
}
/deep/ .left-day-disabled {
background-color: lightgrey;
}
/deep/ .container {
background-color: white;
margin: 10px 0;
padding: 10px 0;
overflow-y: auto;
height: calc(100vh - 46px - 70px - 56px - 30px);
}
/deep/ .container-times {
width: 58%;
text-align: center;
}
/deep/ .container-state {
width: 80px;
height: 36px;
line-height: 36px;
text-align: center;
background-color: #f2f2f2;
border-radius: 2px;
color: #929292;
}
/deep/ table {
border-collapse: collapse;
}
/deep/ .calendar-right {
width: 30px;
max-width: 30px;
text-align: center;
height: 60%;
border-left: 1px solid #fff3f3;
display: flex;
align-items: center;
justify-content: center;
margin-left: 5px;
}
/deep/ .calendar-popup {
top: 46px;
}
`
}
@@ -0,0 +1,121 @@
<!--#
layout("/layouts/platform_h5.html"){
#-->
<style scoped>
</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-search
v-model="pageForm.searchKeyword"
:show-action="false"
:reverse-color="false"
input-align="left"
placeholder="请输入姓名或者工号搜索"
@search="doSearch"
></van-search>
<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.type" :options="typeOptions" :multiple="false"
@change="doSearch"></van-dropdown-item>
</van-dropdown-menu>
</van-sticky>
<table-list api="/platform/site/apply/pageData" :page_form.sync="pageForm" ref="tableListRef" title="name" @ready="onReady">
<template v-slot="{index,row}">
<table-column label="场地地址">{{row.address}}</table-column>
<table-column label="联系人">{{row.contactName}}</table-column>
<table-column label="联系方式">{{row.contactPhone}}</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>
<info ref="infoRef"></info>
<site-apply ref="applyRef"></site-apply>
</div>
<script nonce="${cspNonce!}">
<!--#include('../common/siteInfo.js'){}#-->
<!--#include('apply.js'){}#-->
new Vue({
el: "#app",
store,
components: {
"info": siteInfo,
"site-apply": apply,
},
data() {
return {
pageForm: {
pageNumber: 1,
pageSize: 10,
totalCount: 0,
searchKeyword: '',
year: new Date().getFullYear(),
type: null
},
typeOptions: [],
}
},
methods: {
onView(row) {
this.$refs.infoRef.onOpen(row)
},
onApply(row) {
if(row.reserveTarget === 1 && !this.$auth.hasRoleOr('BRANCH_UNION_ADMIN, BRANCH_UNION_CHAIRMAN') && !this.$auth.hasRole('SYSADMIN')) {
this.$message.warning('该场地只能分工会预约')
return
}
if (row.sexLimit === 1 || row.sexLimit === 2) {
const sex = row.sexLimit === 1 ? '男' : '女'
if(!this.$store.state.user.sex.includes(sex)) {
this.$toast('抱歉,该场地仅限' + sex + '性会员预约')
return
}
}
this.$refs.applyRef.onOpen(row)
},
async onReady() {
const typeList = await this.querySiteType()
this.typeOptions = [
{
text: "全部类型",
value: null
}
].concat(typeList.map((v) => ({ text: v.name, value: v.id })))
if (this.typeOptions.length > 0) {
this.pageForm.type = this.typeOptions[0].value
this.doSearch()
}
},
async querySiteType() {
const res = await this.$axios.post("/platform/site/type/querySiteType")
return res.data
},
doSearch() {
this.$nextTick(() => {
this.pageForm.pageNumber = 1
this.pageForm.totalCount = 0
this.$refs.tableListRef.doSearch()
})
}
},
})
</script>
<!--#
}
#-->
@@ -0,0 +1,76 @@
const applyInfo = {
template:
/*language=HTML*/
`
<van-action-sheet v-model="visible" title="查看详情">
<div class="detail-container">
<van-cell-group title="基本信息">
<van-cell title="预约场地">{{ viewData.siteName }}</van-cell>
<van-cell title="预约人">{{ viewData.applyUserName }}</van-cell>
<van-cell title="预约人工号">{{ viewData.applyLoginName }}</van-cell>
<van-cell title="所属单位">{{ viewData.applyUnitName }}</van-cell>
<van-cell title="预约日期">{{ viewData.applyDay }}</van-cell>
<van-cell title="开始时间">{{ viewData.startTime }}</van-cell>
<van-cell title="结束时间">{{ viewData.endTime }}</van-cell>
<van-cell title="联系电话">{{ viewData.applyMobile }}</van-cell>
<van-cell title="预约人数">{{ viewData.joinCount }}</van-cell>
<van-cell title="预约事由" class="direction-column-cell">{{ viewData.applyCause }}</van-cell>
</van-cell-group>
<template v-for="task in doneTasks">
<van-cell-group :title="task.displayName" v-if="task.ext.isFirstTaskNode">
<van-cell title="申请用户">{{ task.ext.initiatorName }}({{task.ext.initiatorAccount}})
</van-cell>
<van-cell title="申请时间">{{ task.finishTime }}</van-cell>
<van-cell title="办理结果">
<dict-tag :options="dict.type.PROCESS_TASK_SUBMIT_TYPE"
:value="task.ext.submitType"></dict-tag>
</van-cell>
</van-cell-group>
<van-cell-group :title="task.displayName" v-else>
<van-cell title="办理用户">{{ task.taskFormData.userName }}({{task.taskFormData.loginName}})
</van-cell>
<van-cell title="办理时间">{{ task.finishTime }}</van-cell>
<van-cell title="办理结果">
<dict-tag :options="dict.type.PROCESS_TASK_SUBMIT_TYPE"
:value="task.ext.submitType"></dict-tag>
</van-cell>
<van-cell title="办理意见" v-if="!task.ext.isFirstTaskNode" class="direction-column-cell">
<div v-html="task.taskFormData.tf_opinion"></div>
</van-cell>
</van-cell-group>
</template>
</div>
<slot></slot>
</van-action-sheet>
`,
dicts: ["PROCESS_TASK_SUBMIT_TYPE"],
data() {
return {
visible:false,
viewData: {},
doneTasks: [],
row: null,
}
},
methods: {
onOpen(row) {
this.row = row
this.visible = true
this.viewData = row
this.getDoneTasks()
},
onClose(){
this.visible = false
},
getDoneTasks() {
this.$axios.post("/flow/common/doneTasks", {bizId: this.row.id}).then((res) => {
if (res.code === 0) {
this.doneTasks = res.data
}
})
},
},
style: /*language=CSS*/ `
`
}
@@ -0,0 +1,123 @@
const siteInfo = {
template: /*language=HTML*/ `
<div>
<van-action-sheet v-model="visible" title="场地信息">
<div class="detail-container">
<van-cell-group title="基本信息">
<van-cell title="创建人">{{ viewData.createUserName }}</van-cell>
<van-cell title="创建时间">{{ viewData.createTime }}</van-cell>
<van-cell title="场地名称">{{ viewData.name }}</van-cell>
<van-cell title="场地地址" class="direction-column-cell">{{ viewData.address }}</van-cell>
<van-cell title="联系人">{{ viewData.contactName }}</van-cell>
<van-cell title="联系电话">{{ viewData.contactPhone }}</van-cell>
<van-cell title="排序编号">{{ viewData.sortNum }}</van-cell>
<van-cell title="容纳人数">{{ viewData.maxNum }}</van-cell>
<van-cell title="场地类型">{{ viewData.typeName }}</van-cell>
<van-cell title="面向对象">
<span v-if="viewData.reserveTarget === 1">面向分工会</span>
<span v-if="viewData.reserveTarget === 2">面向个人</span>
<span v-if="viewData.reserveTarget === 3">面向集体</span>
</van-cell>
<van-cell title="性别限制">
<span v-if="viewData.sexLimit === 0">不限制</span>
<span v-if="viewData.sexLimit === 1">男</span>
<span v-if="viewData.sexLimit === 2">女</span>
</van-cell>
<van-cell title="开启状态">
<span v-if="viewData.state">开启</span>
<span v-else>禁用</span>
</van-cell>
<van-cell title="排除节假日">
<span v-if="viewData.state">是</span>
<span v-else>否</span>
</van-cell>
<van-cell title="场地介绍" class="direction-column-cell">
<div v-if="viewData.introduce" v-html="viewData.introduce"></div>
<div v-else>暂无场地介绍</div>
</van-cell>
</van-cell-group>
<van-cell-group title="禁用时间" v-if="viewData.notApplyTimeList && viewData.notApplyTimeList.length > 0">
<table class="table-class">
<thead>
<tr>
<th>日期</th>
<th>开始时间</th>
<th>结束时间</th>
</tr>
</thead>
<tbody>
<tr v-for="(item,index) in viewData.notApplyTimeList" :key="index">
<td>{{ item.date }}</td>
<td>{{ item.startTime }}</td>
<td>{{ item.endTime }}</td>
</tr>
</tbody>
</table>
</van-cell-group>
<van-cell-group title="场次" v-if="viewData.openHours && viewData.openHours.length > 0">
<table class="table-class">
<thead>
<tr>
<th>星期</th>
<th>开始时间</th>
<th>结束时间</th>
<th>预约时间单位(小时)</th>
</tr>
</thead>
<tbody>
<tr v-for="(item,index) in viewData.openHours" :key="index">
<td>
<span v-if="item.weekNum === 1">周一</span>
<span v-if="item.weekNum === 2">周二</span>
<span v-if="item.weekNum === 3">周三</span>
<span v-if="item.weekNum === 4">周四</span>
<span v-if="item.weekNum === 5">周五</span>
<span v-if="item.weekNum === 6">周六</span>
<span v-if="item.weekNum === 0">周日</span>
</td>
<td>{{ item.startTime }}</td>
<td>{{ item.endTime }}</td>
<td>{{ item.timeUnit }}</td>
</tr>
</tbody>
</table>
</van-cell-group>
</div>
</van-action-sheet>
</div>
`,
data() {
return {
viewData: {},
visible: false
}
},
methods: {
onOpen(row) {
this.viewData = row
this.visible = true
},
},
style: /*language=CSS*/ `
/deep/ .table-class{
width: 100%;
border-radius: 5px;
overflow: hidden;
line-height: 1.5rem;
font-size: 13px;
table-layout: fixed;
border-collapse: collapse;
}
/deep/ .table-class th {
background-color: #f2f2f2;
border: 1px solid #dddddd;
}
/deep/ .table-class tr {
text-align: center;
border-bottom: 1px solid #dddddd;
}
/deep/ .table-class td {
border: 1px solid #dddddd;
}
`
}
@@ -0,0 +1,154 @@
<!--#
layout("/layouts/platform_h5.html"){
#-->
<style scoped>
</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-search
v-model="pageForm.searchKeyword"
:show-action="false"
:reverse-color="false"
input-align="left"
placeholder="请输入姓名或者工号搜索"
@search="doSearch"
></van-search>
<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.siteType" :options="typeOptions" :multiple="false"
@change="doSearch"></van-dropdown-item>
</van-dropdown-menu>
</van-sticky>
<table-list api="/platform/site/mine/pageData" :page_form.sync="pageForm" ref="tableListRef" title="applyUserName" @ready="onReady">
<template v-slot="{index,row}">
<table-column label="场地名称">{{row.siteName}}</table-column>
<table-column label="所属单位">{{row.applyUnitName}}</table-column>
<table-column label="预约日期">{{row.applyDay}}</table-column>
<table-column label="开始时间">{{row.startTime}}</table-column>
<table-column label="结束时间">{{row.endTime}}</table-column>
<table-column label="当前节点">{{row.taskName}}</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="onEdit(row)"
v-if="row.taskKey === 'startTask' || !row.instanceId">
<i class="fa fa-edit"></i>
<span>编辑</span>
</div>
<div class="action-btn delete" @click="onRevoke(row)"
v-if="row.canRevoke">
<i class="fa fa-undo"></i>
<span>撤回</span>
</div>
<div class="action-btn delete" @click="onDelete(row)"
v-if="row.taskKey === 'startTask' || !row.instanceId">
<i class="fa fa-trash"></i>
<span>删除</span>
</div>
</template>
</table-list>
<info ref="infoRef"></info>
<site-apply ref="applyRef"></site-apply>
</div>
<script nonce="${cspNonce!}">
<!--#include('../apply/apply.js'){}#-->
<!--#include('../common/info.js'){}#-->
new Vue({
el: "#app",
store,
components: {
info: applyInfo,
"site-apply": apply,
},
data() {
return {
pageForm: {
pageNumber: 1,
pageSize: 10,
totalCount: 0,
year: new Date().getFullYear(),
siteType: null
},
typeOptions: [],
}
},
methods: {
async onReady() {
const typeList = await this.querySiteType()
this.typeOptions = [
{
text: "全部类型",
value: null
}
].concat(typeList.map((v) => ({ text: v.name, value: v.id })))
if (this.typeOptions.length > 0) {
this.pageForm.siteType = this.typeOptions[0].value
this.doSearch()
}
},
onRevoke(row) {
this.$dialog.confirm({
title: "提示",
message: "您确定要撤回吗?"
}).then(() => {
this.$axios.post("/flow/common/revokeTask", { taskId: row.startTaskId }).then((res) => {
if (res.code === 0) {
this.$toast(res.msg)
this.doSearch()
}
})
})
},
onView(row) {
this.$refs.infoRef.onOpen(row)
},
async onEdit(row) {
const res = await this.$axios.post('/platform/site/manage/info', { id: row.siteId })
this.$refs.applyRef.onOpen(res.data, row)
},
onDelete(row) {
this.$dialog.confirm({
title: "提示",
message: "确定要删除此申请吗?"
})
.then(() => {
this.$axios.post("/platform/site/mine/delete", { id: row.id }).then((res) => {
if (res.code === 0) {
this.$toast.success(res.msg)
this.doSearch()
} else {
this.$toast.fail(res.msg)
}
})
})
.catch(() => {})
},
async querySiteType() {
const res = await this.$axios.post("/platform/site/type/querySiteType")
return res.data
},
doSearch() {
this.$nextTick(() => {
this.pageForm.pageNumber = 1
this.pageForm.totalCount = 0
this.$refs.tableListRef.doSearch()
})
}
},
})
</script>
<!--#
}
#-->
@@ -0,0 +1,189 @@
<!--#
layout("/layouts/platform_h5.html"){
#-->
<style scoped>
</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-search
v-model="pageForm.searchKeyword"
:show-action="false"
:reverse-color="false"
input-align="left"
placeholder="请输入姓名或者工号搜索"
@search="doSearch"
></van-search>
<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.siteType" :options="typeOptions" :multiple="false"
@change="doSearch"></van-dropdown-item>
</van-dropdown-menu>
<van-tabs v-model="pageForm.approvalText"
@change="(val)=>{this.pageForm.approval = val==='1';this.doSearch();}">
<van-tab title="已审核" name="1"></van-tab>
<van-tab title="未审核" name="0"></van-tab>
</van-tabs>
</van-sticky>
<table-list api="/platform/site/schoolUnionAudit/pageData" :page_form.sync="pageForm" ref="tableListRef" title="applyUserName" @ready="onReady">
<template v-slot="{index,row}">
<table-column label="场地名称">{{row.siteName}}</table-column>
<table-column label="所属单位">{{row.applyUnitName}}</table-column>
<table-column label="预约日期">{{row.applyDay}}</table-column>
<table-column label="开始时间">{{row.startTime}}</table-column>
<table-column label="结束时间">{{row.endTime}}</table-column>
<table-column label="当前节点">{{row.curTaskName}}</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" v-if="row.taskState === 10" @click="onApproval(row)">
<i class="fa fa-edit"></i>
<span>审核</span>
</div>
<div class="action-btn delete" v-if="row.canRevoke" @click="onRevoke(row)">
<i class="fa fa-reply"></i>
<span>撤回</span>
</div>
</template>
</table-list>
<info ref="infoRef">
<div v-if="showApprovalForm">
<div class="process-title">{{formData.taskName}}</div>
<van-form ref="formRef">
<van-field
v-model="formData.tf_opinion"
name="tf_opinion"
label="审批意见"
placeholder="请输入审批意见"
:rules="[{ required: true, message: '请填写审批意见' }]"
required
maxlength="100"
show-word-limit
></van-field>
</van-form>
<div style="display: flex; justify-content: space-between; column-gap: 10px; padding: 10px">
<van-button type="primary" block @click="handleTaskAction(1)">同意</van-button>
<van-button type="danger" block @click="handleTaskAction(20)">不同意</van-button>
<van-button type="danger" block @click="handleTaskAction(6)">退回</van-button>
</div>
</div>
</info>
</div>
<script nonce="${cspNonce!}">
<!--#include('../common/info.js'){}#-->
new Vue({
el: "#app",
store,
components: {
info: applyInfo,
},
data() {
return {
pageForm: {
pageNumber: 1,
pageSize: 10,
totalCount: 0,
searchKeyword: '',
approvalText: "0",
approval: false,
year: new Date().getFullYear(),
siteType: null,
},
typeOptions: [],
formData: {},
showApprovalForm: false
}
},
methods: {
async onReady() {
const typeList = await this.querySiteType()
this.typeOptions = [
{
text: "全部类型",
value: null
}
].concat(typeList.map((v) => ({ text: v.name, value: v.id })))
if (this.typeOptions.length > 0) {
this.pageForm.siteType = this.typeOptions[0].value
this.doSearch()
}
},
onView(row) {
this.showApprovalForm = false
this.$refs.infoRef.onOpen(row)
},
onApproval(row) {
this.showApprovalForm = true
this.$refs.infoRef.onOpen(row)
this.formData = {
processTaskId: row.taskId,
taskName: row.curTaskName
}
},
async handleTaskAction(val) {
try {
await this.$refs.formRef.validate();
this.$dialog.confirm({
title: "提示",
message: "您确定要提交吗?"
}).then(() => {
this.$axios.post("/flow/common/executeTask", {
data: JSON.stringify({
...this.formData,
submitType: val
})
}).then((res) => {
if (res.code === 0) {
this.$refs.infoRef.onClose()
this.$toast.success(res.msg)
this.doSearch()
}
})
}).catch(() => {
})
} catch (error) {
}
},
onRevoke(row){
this.$dialog.confirm({
title: "提示",
message: "您确定要撤回吗?"
}).then(() => {
this.$axios.post("/flow/common/revokeTask", {taskId: row.taskId}).then((res) => {
if (res.code === 0) {
this.$toast.success(res.msg)
this.doSearch()
}
})
})
},
async querySiteType() {
const res = await this.$axios.post("/platform/site/type/querySiteType")
return res.data
},
doSearch() {
this.$nextTick(() => {
this.pageForm.pageNumber = 1
this.pageForm.totalCount = 0
this.$refs.tableListRef.doSearch()
})
}
},
})
</script>
<!--#
}
#-->
@@ -0,0 +1,609 @@
<!--#
layout("/layouts/platform_h5.html"){
#-->
<style scoped>
.page-container {
padding: 12px 12px 96px;
background: #f5f7fb;
min-height: calc(100vh - 46px);
box-sizing: border-box;
}
.panel-card {
margin-bottom: 12px;
overflow: hidden;
border-radius: 22px;
background: #ffffff;
box-shadow:
0 2px 6px rgba(15, 23, 42, 0.04),
0 12px 28px rgba(15, 23, 42, 0.08),
0 0 0 1px rgba(226, 232, 240, 0.95);
}
.notice-card__header {
width: 100%;
padding: 16px;
border: none;
background: #ffffff;
display: flex;
align-items: center;
justify-content: space-between;
color: #0f172a;
font-size: 16px;
font-weight: 700;
line-height: 1.4;
box-sizing: border-box;
}
.notice-card__icon {
color: #64748b;
font-size: 16px;
transition: transform 0.2s ease;
}
.notice-card__icon--expanded {
transform: rotate(180deg);
}
.notice-card__body {
padding: 0 16px 14px;
border-top: 1px solid #eef2f7;
}
.notice-list {
display: flex;
flex-direction: column;
gap: 10px;
}
.notice-item {
padding: 10px 12px;
border-radius: 14px;
background: #f8fafc;
}
.notice-item__label {
color: #334155;
font-size: 13px;
font-weight: 600;
line-height: 1.5;
}
.notice-item__value {
margin-top: 4px;
color: #64748b;
font-size: 12px;
line-height: 1.7;
}
.disabled-time-list {
display: flex;
flex-direction: column;
gap: 6px;
}
.disabled-time-item {
padding: 8px 10px;
border-radius: 10px;
background: #ffffff;
color: #475569;
line-height: 1.7;
}
.form-container {
margin: 0;
}
.section-tip {
padding: 0 16px 14px;
color: #94a3b8;
font-size: 12px;
line-height: 1.6;
}
.selection-summary {
margin: 0 16px 14px;
padding: 10px 12px;
border-radius: 12px;
background: rgba(37, 99, 235, 0.08);
color: #2563eb;
font-size: 12px;
line-height: 1.7;
}
.batch-panel {
padding: 0 16px 14px;
}
.batch-summary {
margin-top: 10px;
padding: 10px 12px;
border-radius: 12px;
background: rgba(37, 99, 235, 0.08);
color: #2563eb;
font-size: 12px;
line-height: 1.7;
}
.availability-popup {
height: 82vh;
display: flex;
flex-direction: column;
background: #f8fafc;
}
.availability-popup__header {
display: flex;
align-items: flex-start;
justify-content: space-between;
padding: 16px;
background: #ffffff;
border-bottom: 1px solid #eef2f7;
}
.availability-popup__title {
color: #0f172a;
font-size: 17px;
font-weight: 700;
line-height: 1.4;
}
.availability-popup__subtitle {
margin-top: 4px;
color: #64748b;
font-size: 12px;
line-height: 1.6;
}
.availability-popup__close {
color: #94a3b8;
font-size: 18px;
line-height: 1;
}
.availability-popup__tabs {
display: flex;
gap: 8px;
padding: 12px 16px 8px;
overflow-x: auto;
background: #f8fafc;
scrollbar-width: none;
-ms-overflow-style: none;
}
.availability-popup__tabs::-webkit-scrollbar {
display: none;
}
.availability-tab {
flex: 0 0 auto;
min-width: 72px;
min-height: 34px;
padding: 6px 14px;
border: 1px solid #cbd5e1;
border-radius: 999px;
background: #ffffff;
color: #475569;
font-size: 13px;
font-weight: 500;
line-height: 1.2;
white-space: nowrap;
box-sizing: border-box;
}
.availability-tab--active {
border-color: #2563eb;
background: #2563eb;
color: #ffffff;
box-shadow: 0 8px 16px rgba(37, 99, 235, 0.22);
}
.availability-popup__legend {
display: flex;
flex-wrap: wrap;
gap: 12px;
padding: 12px 16px 0;
color: #64748b;
font-size: 12px;
line-height: 1.5;
}
.availability-legend__item {
display: inline-flex;
align-items: center;
gap: 6px;
}
.availability-legend__dot {
width: 10px;
height: 10px;
border-radius: 999px;
display: inline-block;
}
.availability-legend__dot--available {
background: #22c55e;
}
.availability-legend__dot--reserved {
background: #9ca3af;
}
.availability-legend__dot--selected {
background: #2563eb;
}
.availability-legend__dot--closed {
background: #e5e7eb;
}
.availability-popup__body {
flex: 1;
overflow-y: auto;
padding: 16px;
box-sizing: border-box;
}
.slot-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 10px;
}
.slot-block {
min-height: 48px;
padding: 8px 10px;
border: 1px solid #dbe4ee;
border-radius: 14px;
background: #ffffff;
color: #475569;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
font-size: 13px;
font-weight: 500;
line-height: 1.4;
box-sizing: border-box;
}
.slot-block__name,
.slot-block__time {
display: block;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.slot-block__time {
margin-top: 2px;
font-size: 12px;
line-height: 1.2;
}
.slot-block--available {
border-color: #86efac;
background: rgba(34, 197, 94, 0.1);
color: #15803d;
}
.slot-block--reserved {
background: #e5e7eb;
color: #6b7280;
}
.slot-block--closed {
background: #f8fafc;
color: #94a3b8;
}
.slot-block--selected {
border-color: #2563eb;
background: #2563eb;
color: #ffffff;
box-shadow: 0 10px 20px rgba(37, 99, 235, 0.24);
}
.availability-popup__footer {
padding: 12px 16px calc(env(safe-area-inset-bottom) + 12px);
background: #ffffff;
border-top: 1px solid #eef2f7;
}
.availability-popup__summary {
margin-bottom: 12px;
color: #475569;
font-size: 12px;
line-height: 1.7;
}
.availability-popup__actions {
display: flex;
gap: 10px;
}
.availability-popup__actions .van-button {
flex: 1;
}
.footer-actions {
position: fixed;
left: 0;
right: 0;
bottom: 0;
display: flex;
gap: 12px;
padding: 10px 12px calc(env(safe-area-inset-bottom) + 10px);
background: rgba(255, 255, 255, 0.96);
backdrop-filter: blur(10px);
box-shadow: 0 -8px 24px rgba(15, 23, 42, 0.08);
}
.footer-actions .van-button {
flex: 1;
height: 44px;
}
/deep/ .panel-card .van-cell-group {
background: transparent;
}
/deep/ .panel-card .van-cell-group__title {
padding: 16px 16px 8px;
margin: 0;
color: #0f172a;
font-size: 16px;
font-weight: 700;
line-height: 1.4;
background: #ffffff;
}
/deep/ .panel-card .van-cell {
padding-top: 12px;
padding-bottom: 12px;
}
/deep/ .panel-card .van-field__label,
/deep/ .panel-card .van-cell__title {
color: #334155;
font-weight: 600;
}
/deep/ .direction-column-cell .van-cell__value {
white-space: normal;
text-align: left;
}
</style>
<div id="app" v-cloak>
<van-nav-bar title="场馆申请" left-text="返回" left-arrow @click-left="historyBack" fixed placeholder></van-nav-bar>
<div class="page-container" v-if="siteLoaded">
<div class="panel-card">
<button type="button" class="notice-card__header" @click="noticeCollapsed = !noticeCollapsed">
<span>预约须知</span>
<van-icon :class="['notice-card__icon', noticeCollapsed ? '' : 'notice-card__icon--expanded']" name="arrow-down"></van-icon>
</button>
<div v-if="!noticeCollapsed" class="notice-card__body">
<div class="notice-list">
<div class="notice-item">
<div class="notice-item__label">可预约日期</div>
<div class="notice-item__value">仅限工作日</div>
</div>
<div class="notice-item">
<div class="notice-item__label">节假日限制</div>
<div class="notice-item__value">{{ timeLimitConfig.filterHolidays ? '节假日不可预约' : '不排除节假日' }}</div>
</div>
<div class="notice-item">
<div class="notice-item__label">禁用时段</div>
<div class="notice-item__value">
<div v-if="timeLimitConfig.notApplyTimeList.length" class="disabled-time-list">
<div
class="disabled-time-item"
v-for="(item, index) in timeLimitConfig.notApplyTimeList"
:key="item.date + item.startTime + item.endTime + index">
{{ item.date }} {{ item.startTime }} - {{ item.endTime }}
</div>
</div>
<div v-else>暂无禁用时段</div>
</div>
</div>
</div>
</div>
</div>
<van-form ref="formRef" class="form-container" :show-error-message="false">
<div class="panel-card">
<van-cell-group title="申请信息">
<van-field
:value="reserveTypeText"
label="预约类型"
name="reserveType"
readonly
clickable
is-link
required
placeholder="请选择预约类型"
@click="openReserveTypePicker"
:rules="[{ required: true, message: '请选择预约类型' }]">
</van-field>
<van-field v-model="formData.applyUserName" label="预约人" name="applyUserName" readonly
:rules="[{ required: true, message: '请确认预约人' }]"></van-field>
<van-field v-model="formData.applyLoginName" label="工号" name="applyLoginName" readonly
:rules="[{ required: true, message: '请确认工号' }]"></van-field>
<van-field v-model="formData.applyUnitName" label="所属单位" name="applyUnitName" readonly
:rules="[{ required: true, message: '请确认所属单位' }]"></van-field>
<van-field
v-if="formData.reserveType === 'union'"
v-model="formData.applyUnionName"
label="分工会"
name="applyUnionName"
readonly
placeholder="自动读取当前登录人的分工会">
</van-field>
<van-field
v-if="formData.reserveType === 'club'"
v-model="formData.clubName"
label="协会"
name="clubName"
readonly
clickable
is-link
required
placeholder="请选择您管理的协会"
@click="openClubPicker"
:rules="[{ required: true, message: '请选择您管理的协会' }]">
</van-field>
<van-field v-model="formData.applyMobile" label="联系电话" name="applyMobile" type="tel" maxlength="11"
required placeholder="请输入联系电话"
:rules="[{ required: true, message: '请输入联系电话' }]"></van-field>
<van-field
:value="scheduleDate"
label="预约日期"
name="scheduleDate"
readonly
clickable
is-link
required
placeholder="请选择预约日期"
@click="openScheduleDatePicker">
</van-field>
<van-field
:value="selectionSummary || ''"
label="预约时段"
name="timeSelection"
readonly
clickable
is-link
required
:placeholder="scheduleDate ? '请选择预约时段' : '请先选择预约日期'"
@click="openAvailabilityPopup">
</van-field>
<div v-if="selectionSummary" class="selection-summary">当前已选:{{ selectionSummary }}</div>
<van-cell title="批量预约">
<template #default>
<van-switch v-model="yearlyReserve" size="22px"></van-switch>
</template>
</van-cell>
<div v-if="yearlyReserve" class="batch-panel">
<div class="section-tip">预约本月后续每周同一时段,默认到本月底。</div>
<van-field
:value="formData.yearlyReserveEndDate"
label="截至日期"
name="yearlyReserveEndDate"
readonly
clickable
is-link
required
placeholder="请选择批量预约截止日期"
@click="openYearlyEndDatePicker">
</van-field>
<div v-if="batchReserveSummary" class="batch-summary">{{ batchReserveSummary }}</div>
</div>
<van-field v-model="formData.applyCause" label="预约事由" name="applyCause" required rows="4" autosize
type="textarea" maxlength="1000" show-word-limit placeholder="请输入预约事由"
:rules="[{ required: true, message: '请输入预约事由' }]"></van-field>
</van-cell-group>
</div>
</van-form>
</div>
<div class="footer-actions" v-if="siteLoaded">
<van-button plain round type="info" @click="historyBack">取消</van-button>
<van-button round type="primary" color="#246fb4" @click="onSubmit">提交预约</van-button>
</div>
<van-popup v-model="showReserveTypePicker" position="bottom" round>
<van-picker show-toolbar :columns="reserveTypeColumns" @confirm="onReserveTypeConfirm"
@cancel="showReserveTypePicker = false"></van-picker>
</van-popup>
<van-popup v-model="showClubPicker" position="bottom" round>
<van-picker show-toolbar :columns="clubColumns" @confirm="onClubConfirm"
@cancel="showClubPicker = false"></van-picker>
</van-popup>
<van-popup v-model="showScheduleDatePicker" position="bottom" round>
<van-datetime-picker
v-model="schedulePickerDate"
type="date"
title="选择预约日期"
:min-date="scheduleMinDate"
:max-date="scheduleMaxDate"
@confirm="onScheduleDateConfirm"
@cancel="showScheduleDatePicker = false">
</van-datetime-picker>
</van-popup>
<van-popup v-model="showAvailabilityPopup" position="bottom" round class="availability-popup">
<div class="availability-popup__header">
<div>
<div class="availability-popup__title">选择预约时段</div>
<div class="availability-popup__subtitle">{{ scheduleDate || '请选择预约日期' }} {{ reserveModeText }}</div>
</div>
<van-icon class="availability-popup__close" name="cross" @click="showAvailabilityPopup = false"></van-icon>
</div>
<div v-if="timeGroupTabs.length" class="availability-popup__tabs">
<button
v-for="tab in timeGroupTabs"
:key="tab.key"
type="button"
:class="['availability-tab', activeTimeGroupKey === tab.key ? 'availability-tab--active' : '']"
@click="activeTimeGroupKey = tab.key">
{{ tab.label }}
</button>
</div>
<div class="availability-popup__legend">
<span class="availability-legend__item"><i class="availability-legend__dot availability-legend__dot--available"></i>可预约</span>
<span class="availability-legend__item"><i class="availability-legend__dot availability-legend__dot--reserved"></i>已预约</span>
<span class="availability-legend__item"><i class="availability-legend__dot availability-legend__dot--selected"></i>已选择</span>
<span class="availability-legend__item"><i class="availability-legend__dot availability-legend__dot--closed"></i>不可预约</span>
</div>
<div class="availability-popup__body">
<van-loading v-if="availabilityLoading" vertical>时段加载中</van-loading>
<van-empty v-else-if="!availabilityBlocks.length" description="当前日期暂无可选时段"></van-empty>
<van-empty v-else-if="!displayedAvailabilityBlocks.length" description="当前标签暂无可选时段"></van-empty>
<div v-else class="slot-grid">
<button
v-for="block in displayedAvailabilityBlocks"
:key="block.key"
type="button"
:class="getBlockClass(block)"
@click="onAvailabilityBlockClick(block)">
<span class="slot-block__name">{{ formatSlotLabel(block).name }}</span>
<span class="slot-block__time">{{ formatSlotLabel(block).time }}</span>
</button>
</div>
</div>
<div class="availability-popup__footer">
<div class="availability-popup__summary">{{ selectionSummary || '请选择一个或多个连续时段,不可跨越间隔选择。' }}</div>
<div class="availability-popup__actions">
<van-button plain round type="default" @click="clearSelection(true)">清空</van-button>
<van-button round type="primary" color="#246fb4" @click="confirmAvailabilitySelection">确定</van-button>
</div>
</div>
</van-popup>
<van-popup v-model="showYearlyEndDatePicker" position="bottom" round>
<van-datetime-picker
v-model="yearlyReservePickerDate"
type="date"
title="选择批量预约截止日期"
:min-date="yearlyReserveMinDate"
:max-date="yearlyReserveMaxDate"
@confirm="onYearlyReserveEndDateConfirm"
@cancel="showYearlyEndDatePicker = false">
</van-datetime-picker>
</van-popup>
</div>
<script nonce="${cspNonce!}">
<!--#include('apply.js'){}#-->
</script>
<!--#
}
#-->

Some files were not shown because too many files have changed in this diff Show More