This commit is contained in:
2026-04-01 18:53:08 +08:00
parent dc71dc22d8
commit d799d11f69
34 changed files with 5509 additions and 0 deletions
@@ -0,0 +1,531 @@
const apply = {
template: /*language=HTML*/ `
<div>
<el-row :gutter="20" class="mb10">
<el-col :span="24" style="text-align: center">
<div style="font-size: large;color: #303133">
您正在预约【<span style="color: #409EFF">{{ row.name }}</span>】活动场地
</div>
</el-col>
</el-row>
<el-form :model="formData" ref="formRef" :rules="formRules" label-position="left" label-width="110px">
<el-row :gutter="10">
<el-col :span="12">
<el-form-item label="预约人" prop="applyUserName">
<el-input readonly v-model="formData.applyUserName"></el-input>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="工号" prop="applyLoginName">
<el-input readonly v-model="formData.applyLoginName"></el-input>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="所属单位" prop="applyUnitName">
<el-input readonly v-model="formData.applyUnitName"></el-input>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="预约类型" prop="reserveType">
<el-radio-group v-model="formData.reserveType" @change="reserveTypeChange">
<el-radio-button label="union">分工会预约</el-radio-button>
<el-radio-button label="club">协会预约</el-radio-button>
</el-radio-group>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="分工会" prop="applyUnionName" v-if="formData.reserveType === 'union'">
<el-input readonly v-model="formData.applyUnionName"
placeholder="自动读取当前登录人的分工会"></el-input>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item
label="协会"
prop="clubId"
v-if="formData.reserveType === 'club'"
:rules="[{ required: true, message: '请选择您管理的协会', trigger: ['change', 'blur'] }]">
<el-select v-model="formData.clubId" placeholder="请选择您管理的协会" filterable clearable
style="width: 100%" @change="clubChange">
<el-option
v-for="item in clubOptions"
:key="item.id"
:label="item.clubName"
:value="item.id">
</el-option>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="联系电话" prop="applyMobile">
<el-input maxlength="11" placeholder="请输入联系电话" v-model="formData.applyMobile"></el-input>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="预约开始时间" prop="reserveStartTime">
<el-date-picker
:key="'start-' + pickerRefreshKey"
v-model="formData.reserveStartTime"
type="datetime"
value-format="yyyy-MM-dd HH:mm:ss"
placeholder="请选择预约开始时间"
:picker-options="startPickerOptions"
style="width: 100%"
@change="timeFieldChange('reserveStartTime')">
</el-date-picker>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="预约结束时间" prop="reserveEndTime">
<el-date-picker
:key="'end-' + pickerRefreshKey"
v-model="formData.reserveEndTime"
type="datetime"
value-format="yyyy-MM-dd HH:mm:ss"
placeholder="请选择预约结束时间"
:picker-options="endPickerOptions"
style="width: 100%"
@change="timeFieldChange('reserveEndTime')">
</el-date-picker>
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="批量预约">
<div style="display:flex;align-items:flex-start;column-gap:12px;flex-wrap:wrap;line-height:1.7;">
<el-checkbox v-model="yearlyReserve">预约本年后续每周同一时段</el-checkbox>
<span style="color:#909399;">
例如先选某个周一 09:00-11:00,勾选后会自动预约本年剩余所有周一的这个时段。
</span>
</div>
<div v-if="yearlyReserve && yearlyReserveSummary" style="margin-top:8px;color:#E6A23C;">
{{ yearlyReserveSummary }}
</div>
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="预约事由" prop="applyCause">
<el-input maxlength="1000" placeholder="请输入预约事由" v-model="formData.applyCause"
type="textarea" :autosize="{ minRows: 4, maxRows: 6}"></el-input>
</el-form-item>
</el-col>
</el-row>
</el-form>
<el-row class="mt10" justify="end" type="flex">
<el-button @click="$emit('refresh')">取消</el-button>
<el-button type="primary" @click="onSubmit">提交预约</el-button>
</el-row>
</div>
`,
store,
data() {
return {
row: {},
clubOptions: [],
yearlyReserve: false,
timeLimitConfig: {
filterHolidays: false,
holidayList: [],
notApplyTimeList: [],
},
pickerRefreshKey: 0,
formData: {
reserveType: 'union',
applyUnionId: '',
applyUnionName: '',
clubId: '',
clubName: '',
reserveStartTime: '',
reserveEndTime: '',
joinCount: 1,
applyCause: '',
},
formRules: {
applyUserName: [{required: true, message: '必填', trigger: ['blur', 'change']}],
applySex: [{required: true, message: '必填', trigger: ['blur', 'change']}],
applyLoginName: [{required: true, message: '必填', trigger: ['blur', 'change']}],
applyUnitName: [{required: true, message: '必填', trigger: ['blur', 'change']}],
reserveType: [{required: true, message: '必填', trigger: ['blur', 'change']}],
applyMobile: [{required: true, message: '必填', trigger: ['blur', 'change']}],
reserveStartTime: [{required: true, message: '必填', trigger: ['blur', 'change']}],
reserveEndTime: [{required: true, message: '必填', trigger: ['blur', 'change']}],
joinCount: [{required: true, message: '必填', trigger: ['blur', 'change']}],
applyCause: [{required: true, message: '必填', trigger: ['blur', 'change']}],
clubId: [{ validator: (rule, value, callback) => {
if (this.formData.reserveType === 'club' && !value) {
callback(new Error('请选择您管理的协会'))
return
}
callback()
}, trigger: ['blur', 'change']}],
},
}
},
computed: {
startPickerOptions() {
return {
disabledDate: (time) => this.isDisabledDate(time),
selectableRange: this.buildSelectableRange(this.formData.reserveStartTime),
}
},
endPickerOptions() {
return {
disabledDate: (time) => this.isDisabledDate(time),
selectableRange: this.buildSelectableRange(this.formData.reserveEndTime),
}
},
yearlyReserveDates() {
if (!this.yearlyReserve || !this.formData.reserveStartTime || !this.formData.reserveEndTime) {
return []
}
const start = this.$moment(this.formData.reserveStartTime)
const end = this.$moment(this.formData.reserveEndTime)
if (!start.isValid() || !end.isValid() || !end.isAfter(start)) {
return []
}
const result = []
const current = start.clone().startOf('day')
const endOfYear = start.clone().endOf('year').startOf('day')
const targetWeekDay = start.day()
const startClock = start.format('HH:mm:ss')
const endClock = end.format('HH:mm:ss')
while (current.isSameOrBefore(endOfYear, 'day')) {
if (current.day() === targetWeekDay) {
const day = current.format('YYYY-MM-DD')
const startTime = day + ' ' + startClock
const endTime = day + ' ' + endClock
if (this.validateTimeLimit(false, startTime, endTime)) {
result.push(day)
}
}
current.add(1, 'day')
}
return result
},
yearlyReserveSummary() {
if (!this.yearlyReserve) {
return ''
}
if (!this.formData.reserveStartTime || !this.formData.reserveEndTime) {
return '请先选择开始和结束时间后,再批量预约本年同星期时段'
}
if (!this.yearlyReserveDates.length) {
return '当前时间段无法生成本年批量预约日期'
}
return '将从 ' + this.yearlyReserveDates[0] + ' 开始,自动预约到 ' + this.yearlyReserveDates[this.yearlyReserveDates.length - 1] + ',共 ' + this.yearlyReserveDates.length + ' 个时段'
},
},
methods: {
async onOpen(row) {
this.row = row
this.clubOptions = []
this.yearlyReserve = false
this.formData = {
siteId: row.id,
applyUserId: this.$store.state.user.id,
applyUserName: this.$store.state.user.username,
applySex: this.$store.state.user.sex,
applyLoginName: this.$store.state.user.loginname,
applyUnitId: this.$store.state.user?.unit?.id,
applyUnitName: this.$store.state.user?.unit?.name,
reserveType: 'union',
applyUnionId: this.$store.state.user?.union?.id,
applyUnionName: this.$store.state.user?.union?.name,
clubId: '',
clubName: '',
applyMobile: this.$store.state.user.mobile,
reserveStartTime: '',
reserveEndTime: '',
joinCount: 1,
applyCause: '',
}
await this.queryTimeLimitConfig()
},
async queryTimeLimitConfig() {
const fallback = {
filterHolidays: !!this.row.filterHolidays,
holidayList: [],
notApplyTimeList: Array.isArray(this.row.notApplyTimeList) ? this.row.notApplyTimeList : [],
}
try {
const res = await this.$axios.post('/platform/siteCug/apply/timeLimitConfig', {
siteId: this.row.id,
})
if (res.code === 0 && res.data) {
this.timeLimitConfig = {
filterHolidays: !!res.data.filterHolidays,
holidayList: Array.isArray(res.data.holidayList) ? res.data.holidayList : [],
notApplyTimeList: Array.isArray(res.data.notApplyTimeList) ? res.data.notApplyTimeList : [],
}
} else {
this.timeLimitConfig = fallback
}
} catch (e) {
this.timeLimitConfig = fallback
}
this.pickerRefreshKey += 1
},
async reserveTypeChange(value) {
if (value === 'union') {
this.formData.applyUnionId = this.$store.state.user?.union?.id || ''
this.formData.applyUnionName = this.$store.state.user?.union?.name || ''
this.formData.clubId = ''
this.formData.clubName = ''
}
if (value === 'club') {
this.formData.applyUnionId = ''
this.formData.applyUnionName = ''
this.formData.clubId = ''
this.formData.clubName = ''
await this.loadManagedClubs()
}
this.$nextTick(() => {
this.$refs.formRef?.clearValidate(['applyUnionName', 'clubId'])
})
},
async loadManagedClubs() {
try {
const res = await this.$axios.post('/platform/club/examine/apply/getClubsByRole')
this.clubOptions = Array.isArray(res.data) ? res.data : []
if (!this.clubOptions.length) {
this.$message.warning('您当前没有可预约的协会管理权限')
}
} catch (e) {
this.clubOptions = []
this.$message.warning('协会列表加载失败,请稍后重试')
}
},
clubChange(clubId) {
const club = this.clubOptions.find(item => item.id === clubId)
this.formData.clubName = club ? club.clubName : ''
},
async normalizeReserveTypeData() {
if (this.formData.reserveType === 'union') {
this.formData.applyUnionId = this.$store.state.user?.union?.id || ''
this.formData.applyUnionName = this.$store.state.user?.union?.name || ''
this.formData.clubId = ''
this.formData.clubName = ''
return true
}
if (this.formData.reserveType === 'club') {
if (!this.clubOptions.length) {
await this.loadManagedClubs()
}
const club = this.clubOptions.find(item => item.id === this.formData.clubId)
this.formData.clubName = club ? club.clubName : ''
this.formData.applyUnionId = ''
this.formData.applyUnionName = ''
return true
}
return false
},
isDisabledDate(time) {
const day = this.$moment(time).format('YYYY-MM-DD')
const today = this.$moment().startOf('day')
const currentDay = this.$moment(day)
if (currentDay.isBefore(today)) {
return true
}
if (currentDay.day() === 0 || currentDay.day() === 6) {
return true
}
return this.timeLimitConfig.filterHolidays && this.timeLimitConfig.holidayList.includes(day)
},
normalizeTime(timeStr) {
if (!timeStr) {
return '00:00:00'
}
return timeStr.length === 5 ? timeStr + ':00' : timeStr
},
getDisabledRangesByDay(dayStr) {
const list = Array.isArray(this.timeLimitConfig.notApplyTimeList) ? this.timeLimitConfig.notApplyTimeList : []
return list
.filter(item => item && item.date && this.$moment(item.date).format('YYYY-MM-DD') === dayStr)
.sort((a, b) => this.normalizeTime(a.startTime).localeCompare(this.normalizeTime(b.startTime)))
},
buildSelectableRange(dateTimeValue) {
if (!dateTimeValue) {
return ['00:00:00 - 23:59:59']
}
const dayStr = this.$moment(dateTimeValue).format('YYYY-MM-DD')
const disabledRanges = this.getDisabledRangesByDay(dayStr)
if (!disabledRanges.length) {
return ['00:00:00 - 23:59:59']
}
const result = []
let cursor = '00:00:00'
disabledRanges.forEach(item => {
const start = this.normalizeTime(item.startTime)
const end = this.normalizeTime(item.endTime)
if (cursor < start) {
result.push(cursor + ' - ' + start)
}
if (cursor < end) {
cursor = end
}
})
if (cursor < '23:59:59') {
result.push(cursor + ' - 23:59:59')
}
return result.length ? result : ['00:00:00 - 00:00:00']
},
isDateTimeBlocked(dateTimeStr) {
if (!dateTimeStr) {
return false
}
const target = this.$moment(dateTimeStr)
const dayStr = target.format('YYYY-MM-DD')
if (this.timeLimitConfig.filterHolidays && this.timeLimitConfig.holidayList.includes(dayStr)) {
return true
}
const disabledRanges = this.getDisabledRangesByDay(dayStr)
return disabledRanges.some(item => {
const start = this.$moment(dayStr + ' ' + this.normalizeTime(item.startTime))
const end = this.$moment(dayStr + ' ' + this.normalizeTime(item.endTime))
return target.isSameOrAfter(start) && target.isBefore(end)
})
},
hasHolidayInRange(start, end) {
const current = start.clone().startOf('day')
const endDay = end.clone().startOf('day')
while (current.isSameOrBefore(endDay)) {
if (current.day() === 0 || current.day() === 6) {
return true
}
if (this.timeLimitConfig.filterHolidays && this.timeLimitConfig.holidayList.includes(current.format('YYYY-MM-DD'))) {
return true
}
current.add(1, 'day')
}
return false
},
overlapsDisabledRange(start, end) {
const list = Array.isArray(this.timeLimitConfig.notApplyTimeList) ? this.timeLimitConfig.notApplyTimeList : []
return list.some(item => {
if (!item || !item.date || !item.startTime || !item.endTime) {
return false
}
const rangeStart = this.$moment(this.$moment(item.date).format('YYYY-MM-DD') + ' ' + this.normalizeTime(item.startTime))
const rangeEnd = this.$moment(this.$moment(item.date).format('YYYY-MM-DD') + ' ' + this.normalizeTime(item.endTime))
return start.isBefore(rangeEnd) && end.isAfter(rangeStart)
})
},
validateTimeLimit(showMessage = true, startTime = this.formData.reserveStartTime, endTime = this.formData.reserveEndTime) {
if (!startTime || !endTime) {
return true
}
const start = this.$moment(startTime)
const end = this.$moment(endTime)
if (this.isDateTimeBlocked(startTime) || this.isDateTimeBlocked(endTime)) {
if (showMessage) {
this.$message.warning('预约时间不能选择周末、节假日或禁用时间')
}
return false
}
if (this.hasHolidayInRange(start, end)) {
if (showMessage) {
this.$message.warning('预约时间范围内包含周末或节假日,请重新选择')
}
return false
}
if (this.overlapsDisabledRange(start, end)) {
if (showMessage) {
this.$message.warning('预约时间范围与禁用时间冲突,请重新选择')
}
return false
}
return true
},
timeFieldChange(field) {
const value = this.formData[field]
if (!value) {
return
}
if (this.isDateTimeBlocked(value)) {
this.$message.warning('该时间点不可预约,请重新选择')
this.$set(this.formData, field, '')
return
}
if (this.formData.reserveStartTime && this.formData.reserveEndTime && !this.validateTimeLimit()) {
this.$set(this.formData, field, '')
}
},
validateYearlyReserve() {
if (!this.yearlyReserve) {
return true
}
if (!this.yearlyReserveDates.length) {
this.$message.warning('?????????????????????')
return false
}
return true
},
buildSubmitApi() {
return this.yearlyReserve ? '/platform/siteCug/apply/submitYearly' : '/platform/siteCug/apply/submit'
},
buildSubmitConfirmMessage() {
if (!this.yearlyReserve) {
return '您确定要提交吗?'
}
return '将一次性预约本年后续 ' + this.yearlyReserveDates.length + ' 个同星期时段,确认提交吗?'
},
onSubmit() {
this.$refs.formRef.validate(async (valid) => {
if (!valid) return
if (!this.formData.reserveType) {
this.$message.warning('请选择预约类型')
return
}
await this.normalizeReserveTypeData()
if (this.formData.reserveType === 'union' && !this.formData.applyUnionId) {
this.$message.warning('当前用户未关联分工会,不能发起分工会预约')
return
}
if (this.formData.reserveType === 'club' && !this.formData.clubId) {
this.$message.warning('请选择您管理的协会')
return
}
if (!this.formData.reserveStartTime || !this.formData.reserveEndTime) {
this.$message.warning('请选择预约开始和结束时间')
return
}
const start = this.$moment(this.formData.reserveStartTime)
const end = this.$moment(this.formData.reserveEndTime)
if (!start.isAfter(this.$moment())) {
this.$message.warning('预约开始时间必须晚于当前时间')
return
}
if (!end.isAfter(start)) {
this.$message.warning('预约结束时间必须晚于开始时间')
return
}
if (!this.validateTimeLimit()) {
return
}
if (!this.validateYearlyReserve()) {
return
}
this.$confirm(this.buildSubmitConfirmMessage(), '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
this.$axios.post(this.buildSubmitApi(), {
data: JSON.stringify(this.formData),
}).then((resp) => {
if (resp.code === 0) {
this.$message.success(resp.msg || '提交成功')
this.$emit('refresh')
} else {
this.$message.warning(resp.msg || '提交失败')
}
}).catch((err) => {
this.$message.warning((err && err.msg) || '提交失败')
})
}).catch(() => {})
})
},
},
};
@@ -0,0 +1,125 @@
<!--#
layout("/layouts/platform.html"){
#-->
<div id="app" v-cloak>
<guava ref="guava">
<template>
<el-card shadow="never">
<search @search="doSearch">
<search-item label="名称/地址">
<el-input placeholder="请输入名称或地址" clearable v-model="pageForm.searchKeyword"
@keyup.enter.native="doSearch">
</el-input>
</search-item>
<search-item label="场地类型">
<el-select v-model="pageForm.type" clearable filterable placeholder="请选择场地类型" @change="doSearch">
<el-option
v-for="item in typeOptions"
:label="item.name"
:value="item.id"
:key="item.id"
></el-option>
</el-select>
</search-item>
</search>
</el-card>
<el-card shadow="never" class="mt20">
<table-tool label="场地列表"></table-tool>
<el-table :data="tableData" @sort-change="pageOrder" :size="tableSize">
<el-table-column label="序号" type="index" :index="indexMethod" width="60"></el-table-column>
<el-table-column
:label="column.label"
:prop="column.prop"
:key="column.prop"
:width="column.width"
:sortable="column.sortable"
align="center"
header-align="center"
show-overflow-tooltip
v-for="column in tableColumns"
>
</el-table-column>
<el-table-column label="操作" width="180">
<template v-slot="{ row }">
<el-button @click="onViewSite(row)" size="mini" type="primary">查看场地</el-button>
<el-button @click="onApply(row)" size="mini" type="primary">预约</el-button>
</template>
</el-table-column>
</el-table>
<!--#include("/layouts/pagination.html"){}#-->
</el-card>
</template>
<template #edit>
<site-apply ref="applyRef" @refresh="refresh"></site-apply>
</template>
<template #view>
<info ref="infoRef"></info>
</template>
</guava>
</div>
<script nonce="${cspNonce!}">
<!--#include('../manage/info.js'){}#-->
<!--#include('apply.js'){}#-->
const vue = new Vue({
el: "#app",
mixins: [initTableMixins],
components: {
"info": siteInfo,
"site-apply": apply,
},
data() {
return {
typeOptions: [],
tableColumns: [
{prop: 'name', label: '场地名称'},
{prop: 'address', label: '场地地址'},
{prop: 'contactName', label: '联系人'},
{prop: 'contactPhone', label: '联系电话'},
{prop: 'maxNum', label: '容纳人数'},
{prop: 'typeName', label: '场地类型'},
],
}
},
methods: {
refresh() {
this.doSearch()
this.$refs.guava.index()
},
onApply(row) {
this.$refs.guava.edit(() => {
this.$refs.applyRef.onOpen(row)
})
},
onViewSite(row) {
this.$refs.guava.view(() => {
this.$refs.infoRef.onOpen(row)
})
},
querySiteType() {
this.$axios.post('/platform/siteCug/function/type/queryFunctionType').then((res) => {
this.typeOptions = res.data
})
},
pageData() {
this.$axios.post(loc() + '/pageData', this.pageForm).then((res) => {
if (res.code === 0) {
this.tableData = res.data.list
this.pageForm.totalCount = res.data.totalCount
}
})
},
},
async created() {
this.querySiteType()
this.pageData()
}
})
</script>
<!--#
}
#-->
@@ -0,0 +1,80 @@
const siteCugApplyInfo = {
template: /*language=HTML*/ `
<div>
<div class="process-title">
申请信息
<el-link type="primary" @click="openChart">点击查看流程图</el-link>
</div>
<el-descriptions :column="2" border>
<el-descriptions-item label="预约场地">{{ viewData.siteName }}</el-descriptions-item>
<el-descriptions-item label="预约人">{{ viewData.applyUserName }}</el-descriptions-item>
<el-descriptions-item label="预约人工号">{{ viewData.applyLoginName }}</el-descriptions-item>
<el-descriptions-item label="所属单位">{{ viewData.applyUnitName }}</el-descriptions-item>
<el-descriptions-item label="预约类型">{{ reserveTypeLabel }}</el-descriptions-item>
<el-descriptions-item label="预约主体">{{ reserveTargetName }}</el-descriptions-item>
<el-descriptions-item label="开始时间">{{ viewData.reserveStartTime }}</el-descriptions-item>
<el-descriptions-item label="结束时间">{{ viewData.reserveEndTime }}</el-descriptions-item>
<el-descriptions-item label="联系电话">{{ viewData.applyMobile }}</el-descriptions-item>
<el-descriptions-item label="预约人数">{{ viewData.joinCount }}</el-descriptions-item>
<el-descriptions-item label="预约事由" :span="2">{{ viewData.applyCause }}</el-descriptions-item>
</el-descriptions>
<template v-for="task in doneTasks">
<div class="mt10">
<div class="process-title">{{ task.displayName }}</div>
<el-descriptions border class="flow-task-form" :column="3" :key="task.id" v-if="task.ext.isFirstTaskNode">
<el-descriptions-item label="申请用户">{{ task.ext.initiatorName }}({{task.ext.initiatorAccount}})</el-descriptions-item>
<el-descriptions-item label="申请时间">{{ task.finishTime }}</el-descriptions-item>
<el-descriptions-item label="办理结果">
<dict-tag :options="dict.type.PROCESS_TASK_SUBMIT_TYPE" :value="task.ext.submitType"></dict-tag>
</el-descriptions-item>
</el-descriptions>
<el-descriptions border class="flow-task-form" :column="3" :key="task.id" v-else>
<el-descriptions-item label="办理用户">{{ task.taskFormData.userName }}({{task.taskFormData.loginName}})</el-descriptions-item>
<el-descriptions-item label="办理时间">{{ task.finishTime }}</el-descriptions-item>
<el-descriptions-item label="办理结果">
<dict-tag :options="dict.type.PROCESS_TASK_SUBMIT_TYPE" :value="task.ext.submitType"></dict-tag>
</el-descriptions-item>
<el-descriptions-item label="办理意见">{{ task.taskFormData.opinion }}</el-descriptions-item>
</el-descriptions>
</div>
</template>
<slot></slot>
<snaker-chart ref="snakerChartRef"></snaker-chart>
</div>
`,
store,
dicts: ["PROCESS_TASK_SUBMIT_TYPE"],
data() {
return {
viewData: {},
doneTasks: [],
row: null
}
},
computed: {
reserveTypeLabel() {
return this.viewData.reserveType === 'club' ? '协会预约' : '分工会预约'
},
reserveTargetName() {
return this.viewData.reserveType === 'club' ? (this.viewData.clubName || '-') : (this.viewData.applyUnionName || '-')
},
},
methods: {
onOpen(row) {
this.row = row
this.viewData = row
this.getDoneTasks()
},
getDoneTasks() {
this.$axios.post('/flow/common/doneTasks', {bizId: this.row.id}).then((res) => {
if (res.code === 0) {
this.doneTasks = res.data
}
})
},
openChart() {
this.$refs.snakerChartRef.onOpenFull(this.row.instanceProcessDefineId, this.row.instanceId)
}
},
}
@@ -0,0 +1,341 @@
const basicForm = {
template: /*language=HTML*/ `
<div>
<el-form :model="formData" ref="formRef" :rules="formRules" label-width="110px" label-position="left">
<el-row :gutter="20" type="flex">
<el-col :span="12">
<el-form-item label="创建人" prop="createUserName">
<el-input disabled type="text" v-model="formData.createUserName" maxlength="20" placeholder="请输入创建人"></el-input>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item prop="sortNum" label="排序编号">
<el-input v-model="formData.sortNum" placeholder="请输入排序编号" type="number"></el-input>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20" type="flex">
<el-col :span="12">
<el-form-item label="场地名称" prop="name">
<el-input type="text" v-model="formData.name" maxlength="50" placeholder="请输入场地名称"></el-input>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="场地地址" prop="address">
<el-input type="text" v-model="formData.address" maxlength="100" placeholder="请输入场地地址"></el-input>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20" type="flex">
<el-col :span="12">
<el-form-item label="联系人" prop="contactName">
<el-input type="text" v-model="formData.contactName" maxlength="20" placeholder="请输入联系人"></el-input>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="联系电话" prop="contactPhone">
<el-input type="text" v-model="formData.contactPhone" maxlength="20" placeholder="请输入联系电话"></el-input>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20" type="flex">
<el-col :span="12">
<el-form-item label="容纳人数" prop="maxNum">
<el-input v-model="formData.maxNum" placeholder="请输入容纳人数" type="number"></el-input>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="场地类型" prop="typeId">
<el-select v-model="formData.typeId" clearable filterable placeholder="请选择场地类型" style="width: 100%">
<el-option
v-for="item in typeList"
:label="item.name"
:value="item.id"
:key="item.id"
></el-option>
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20" type="flex">
<el-col :span="12">
<el-form-item prop="sexLimit" label="性别限制">
<el-radio-group v-model="formData.sexLimit" size="medium">
<el-radio-button :label="0">不限制</el-radio-button>
<el-radio-button :label="1">男</el-radio-button>
<el-radio-button :label="2">女</el-radio-button>
</el-radio-group>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item prop="state" label="开启状态">
<el-radio-group v-model="formData.state" size="medium">
<el-radio-button :label="true">开启</el-radio-button>
<el-radio-button :label="false">禁用</el-radio-button>
</el-radio-group>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20" type="flex">
<el-col :span="12">
<el-form-item prop="filterHolidays" label="排除节假日">
<el-radio-group v-model="formData.filterHolidays" size="medium">
<el-radio-button :label="true">是</el-radio-button>
<el-radio-button :label="false">否</el-radio-button>
</el-radio-group>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="禁用时间">
<el-button type="primary" @click="openSetUpTime" size="small">点击设置禁用时间</el-button>
<span style="color: #c64120">您已设置{{ formData.notApplyTimeList ? formData.notApplyTimeList.length : 0 }}个禁用时间</span>
</el-form-item>
</el-col>
</el-row>
<el-form-item label="场地介绍" prop="introduce">
<text-editor v-model="formData.introduce"></text-editor>
</el-form-item>
</el-form>
<el-row class="mt10" justify="end" type="flex">
<el-button @click="$emit('refresh')">取消</el-button>
<el-button @click="onSubmit" type="primary">提交</el-button>
</el-row>
<el-dialog append-to-body :close-on-click-modal="false" :visible.sync="setUpTimeDialog" title="设置时间">
<div class="left-span-label">选择日期</div>
<el-date-picker
@change="setUpDateChange"
placeholder="请选择一个或多个日期"
style="width: 100%"
type="dates"
v-model="formData.setUpDate"
value-format="yyyy-MM-dd">
</el-date-picker>
<div class="left-span-label mt20">设置禁用时间</div>
<el-row>
<el-time-select
:picker-options="{
start: '00:00',
step: '00:05',
end: '23:55'
}"
placeholder="开始时间"
v-model="timeOneKeySet.startTime">
</el-time-select>
<el-time-select
:picker-options="{
start: '00:00',
step: '00:05',
end: '23:55'
}"
placeholder="结束时间"
v-model="timeOneKeySet.endTime">
</el-time-select>
<el-button @click="oneKeySetStartEndTime" type="primary">一键设置开始/结束时间</el-button>
</el-row>
<el-table :data="formData.notApplyTimeList" class="mt10" max-height="520px" size="mini" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55"></el-table-column>
<el-table-column label="日期" width="200">
<template v-slot="{row}">
<i class="el-icon-time"></i>
{{$moment(row.date).format('YYYY-MM-DD')}}
</template>
</el-table-column>
<el-table-column label="开始时间">
<template v-slot="{row}">
<el-time-select
size="mini"
:picker-options="{
start: '00:00',
step: '00:05',
end: '23:55'
}"
v-model="row.startTime">
</el-time-select>
</template>
</el-table-column>
<el-table-column label="结束时间">
<template v-slot="{row}">
<el-time-select
size="mini"
:picker-options="{
start: '00:00',
step: '00:05',
end: '23:55'
}"
v-model="row.endTime">
</el-time-select>
</template>
</el-table-column>
<el-table-column label="操作" width="200">
<template v-slot="{row, $index}">
<el-button
size="mini"
@click="formData.notApplyTimeList.splice($index,0,{date:row.date,startTime:'',endTime:''})"
type="primary">
新增同天时段
</el-button>
<el-button size="mini" @click="removeSetUpTableRow(row,$index)" type="danger">删除</el-button>
</template>
</el-table-column>
</el-table>
<el-row class="mt20" justify="end" type="flex">
<el-button @click="setUpTimeDialog = false">取消</el-button>
<el-button @click="doConfirmSetUpCourse" type="primary">确定</el-button>
</el-row>
</el-dialog>
</div>
`,
props: {
typeList: {
type: Array,
required: false,
default: [],
}
},
store,
data() {
return {
formData: {
state: true,
sexLimit: 0,
filterHolidays: false,
notApplyTimeList: [],
createUserName: this.$store.state.user.username,
createUserId: this.$store.state.user.id,
},
formRules: {
createUserName: [{required: true, message: '必填', trigger: ['blur', 'change']}],
sortNum: [{required: true, message: '必填', trigger: ['blur', 'change']}],
name: [{required: true, message: '必填', trigger: ['blur', 'change']}],
address: [{required: true, message: '必填', trigger: ['blur', 'change']}],
contactName: [{required: true, message: '必填', trigger: ['blur', 'change']}],
contactPhone: [{required: true, message: '必填', trigger: ['blur', 'change']}],
maxNum: [{required: true, message: '必填', trigger: ['blur', 'change']}],
typeId: [{required: true, message: '必填', trigger: ['blur', 'change']}],
sexLimit: [{required: true, message: '必填', trigger: ['blur', 'change']}],
filterHolidays: [{required: true, message: '必填', trigger: ['blur', 'change']}],
state: [{required: true, message: '必填', trigger: ['blur', 'change']}],
},
setUpTimeDialog: false,
timeOneKeySet: {
startTime: "",
endTime: ""
},
multipleSelection: [],
}
},
methods: {
doConfirmSetUpCourse() {
const timeList = this.formData.notApplyTimeList
if (timeList && timeList.length > 0) {
const valid = timeList.every(v => v.date && v.startTime && v.endTime && (v.startTime < v.endTime))
if (!valid) {
this.$message.warning("时间不完整或者有误")
return
}
}
this.setUpTimeDialog = false
},
removeSetUpTableRow(row, index) {
this.formData.notApplyTimeList.splice(index, 1)
const dateArray = this.formData.notApplyTimeList.map(v => this.$moment(v.date).format("YYYY-MM-DD"))
const setUpDate = this.formData.setUpDate || []
this.formData.setUpDate = setUpDate.filter(v => {
return dateArray.includes(this.$moment(v).format("YYYY-MM-DD"))
})
},
handleSelectionChange(val) {
this.multipleSelection = val
},
oneKeySetStartEndTime() {
if (this.multipleSelection.length === 0) {
this.$message.warning('请在下面表格多选框中选择需要一键设置的时间')
return
}
const { startTime, endTime } = this.timeOneKeySet
this.formData.notApplyTimeList.forEach(v => {
const selected = this.multipleSelection.find(o => o.date === v.date && o.startTime === v.startTime && o.endTime === v.endTime)
if (selected) {
this.$set(v, "startTime", startTime)
this.$set(v, "endTime", endTime)
}
})
this.$forceUpdate()
},
setUpDateChange(val) {
if (!val) {
this.formData.notApplyTimeList = []
return
}
if (this.formData.notApplyTimeList === undefined) {
this.$set(this.formData, "notApplyTimeList", [])
}
const dateSet = new Set(this.formData.notApplyTimeList.map(v => this.$moment(v.date).format("YYYY-MM-DD")))
val.forEach(v => {
if (!dateSet.has(v)) {
this.formData.notApplyTimeList.push({
date: v, startTime: "", endTime: ""
})
}
})
this.formData.notApplyTimeList = this.formData.notApplyTimeList.filter(v => {
return val.includes(this.$moment(v.date).format("YYYY-MM-DD"))
})
this.formData.notApplyTimeList.sort((a, b) => {
return Date.parse(a.date) - Date.parse(b.date)
})
},
openSetUpTime() {
if (this.formData.notApplyTimeList) {
this.$set(this.formData, 'setUpDate', this.formData.notApplyTimeList.map(o => o.date))
}
this.setUpTimeDialog = true
},
onOpen(row) {
if (row && row.id) {
this.formData = clone(row)
if (!Array.isArray(this.formData.notApplyTimeList)) {
this.$set(this.formData, 'notApplyTimeList', [])
}
} else {
this.formData = {
state: true,
sexLimit: 0,
filterHolidays: false,
notApplyTimeList: [],
createUserName: this.$store.state.user.username,
createUserId: this.$store.state.user.id,
}
}
},
onSubmit() {
this.$refs.formRef.validate((valid) => {
if (valid) {
this.$confirm("您确定要提交吗?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
}).then(async () => {
const resp = await this.$axios.post("/platform/siteCug/manage/submit", {data: JSON.stringify(this.formData)})
if (resp.code === 0) {
this.$message.success(resp.msg)
this.$emit('refresh')
} else {
this.$message.warning(resp.msg)
}
})
}
})
},
},
style: /*language=CSS*/ `
`
};
@@ -0,0 +1,180 @@
<!--#
layout("/layouts/platform.html"){
#-->
<style>
</style>
<div id="app" v-cloak>
<guava ref="guava">
<template>
<el-card shadow="never">
<search @search="doSearch">
<search-item label="名称/地址">
<el-input placeholder="请输入名称或地址" clearable v-model="pageForm.searchKeyword"
@keyup.enter.native="doSearch">
</el-input>
</search-item>
<search-item label="场地类型">
<el-select v-model="pageForm.type" clearable filterable placeholder="请选择场地类型" @change="doSearch">
<el-option
v-for="item in typeOptions"
:label="item.name"
:value="item.id"
:key="item.id"
></el-option>
</el-select>
</search-item>
</search>
</el-card>
<el-card shadow="never" class="mt20">
<table-tool label="场地列表">
<el-button type="primary" size="small" @click="onAdd">
<i class="ti-plus"></i>
新增场地
</el-button>
</table-tool>
<el-table :data="tableData" @sort-change="pageOrder" :size="tableSize">
<el-table-column label="序号" type="index" :index="indexMethod" width="60"></el-table-column>
<el-table-column
:label="column.label"
:prop="column.prop"
:key="column.prop"
:width="column.width"
:sortable="column.sortable"
align="center"
header-align="center"
show-overflow-tooltip
v-for="column in tableColumns"
>
<template v-slot="{ row }" v-if="column.prop === 'state'">
<el-switch
@change="switchChange(row)"
v-model="row.state"
active-color="#13ce66"
inactive-color="#ff4949">
</el-switch>
</template>
<template v-slot="{ row }" v-else-if="column.prop === 'sexLimit'">
<span v-if="row.sexLimit === 0">不限制</span>
<span v-else-if="row.sexLimit === 1"></span>
<span v-else-if="row.sexLimit === 2"></span>
<span v-else>--</span>
</template>
</el-table-column>
<el-table-column label="操作" width="230">
<template v-slot="{ row }">
<el-button @click="onView(row)" size="mini" type="primary">查看</el-button>
<el-button @click="onEdit(row)" size="mini" type="primary">编辑</el-button>
<el-button @click="onDelete(row)" size="mini" type="danger">删除</el-button>
</template>
</el-table-column>
</el-table>
<!--#include("/layouts/pagination.html"){}#-->
</el-card>
</template>
<template #edit>
<basic-form :type-list="typeOptions" ref="basicFormRef" @refresh="refresh"></basic-form>
</template>
<template #view>
<info ref="infoRef"></info>
</template>
</guava>
</div>
<script nonce="${cspNonce!}">
<!--#include('basicForm.js'){}#-->
<!--#include('info.js'){}#-->
const vue = new Vue({
el: "#app",
mixins: [initTableMixins],
components: {
"basic-form": basicForm,
"info": siteInfo,
},
data() {
return {
typeOptions: [],
tableColumns: [
{prop: 'createUserName', label: '创建人'},
{prop: 'sortNum', label: '排序编号'},
{prop: 'name', label: '场地名称'},
{prop: 'address', label: '场地地址'},
{prop: 'contactName', label: '联系人'},
{prop: 'contactPhone', label: '联系电话'},
{prop: 'maxNum', label: '容纳人数'},
{prop: 'typeName', label: '场地类型'},
{prop: 'sexLimit', label: '性别限制'},
{prop: 'state', label: '开启状态'},
],
}
},
methods: {
refresh() {
this.doSearch()
this.$refs.guava.index()
},
onAdd() {
this.$refs.guava.edit(() => {
this.$refs.basicFormRef.onOpen()
})
},
onEdit(row) {
this.$refs.guava.edit(() => {
this.$refs.basicFormRef.onOpen(row)
})
},
onView(row) {
this.$refs.guava.view(() => {
this.$refs.infoRef.onOpen(row)
})
},
onDelete(row) {
this.$confirm("您确定要删除吗, 是否继续?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
})
.then(() => {
this.$axios.post("/platform/siteCug/manage/delete", { id: row.id }).then((res) => {
if (res.code === 0) {
this.$message.success(res.msg)
this.pageData()
}
})
})
.catch(() => {})
},
switchChange(row) {
this.$axios.post("/platform/siteCug/manage/submit", row).then((res) => {
if (res.code === 0) {
this.$message.success(res.msg)
this.pageData()
}
})
},
querySiteType() {
this.$axios.post("/platform/siteCug/function/type/queryFunctionType").then((res) => {
this.typeOptions = res.data
})
},
pageData() {
this.$axios.post(loc() + "/pageData", this.pageForm).then((res) => {
if (res.code === 0) {
this.tableData = res.data.list
this.pageForm.totalCount = res.data.totalCount
}
})
},
},
async created() {
this.querySiteType()
this.pageData()
}
})
</script>
<!--#
}
#-->
@@ -0,0 +1,59 @@
const siteInfo = {
template: /*language=HTML*/ `
<div>
<el-descriptions :column="2" border>
<el-descriptions-item label="创建人">{{ viewData.createUserName }}</el-descriptions-item>
<el-descriptions-item label="排序编号">{{ viewData.sortNum }}</el-descriptions-item>
<el-descriptions-item label="场地名称">{{ viewData.name }}</el-descriptions-item>
<el-descriptions-item label="场地地址">{{ viewData.address }}</el-descriptions-item>
<el-descriptions-item label="联系人">{{ viewData.contactName }}</el-descriptions-item>
<el-descriptions-item label="联系电话">{{ viewData.contactPhone }}</el-descriptions-item>
<el-descriptions-item label="容纳人数">{{ viewData.maxNum }}</el-descriptions-item>
<el-descriptions-item label="场地类型">{{ viewData.typeName }}</el-descriptions-item>
<el-descriptions-item label="排除节假日">
<span v-if="viewData.filterHolidays">是</span>
<span v-else>否</span>
</el-descriptions-item>
<el-descriptions-item label="禁用时间" :span="2">
<el-table v-if="viewData.notApplyTimeList && viewData.notApplyTimeList.length > 0"
:data="viewData.notApplyTimeList" max-height="300" size="mini">
<el-table-column prop="date" label="日期"></el-table-column>
<el-table-column prop="startTime" label="开始时间"></el-table-column>
<el-table-column prop="endTime" label="结束时间"></el-table-column>
</el-table>
<span v-else>暂无禁用时间</span>
</el-descriptions-item>
<el-descriptions-item label="性别限制">
<span v-if="viewData.sexLimit === 0">不限制</span>
<span v-if="viewData.sexLimit === 1">男</span>
<span v-if="viewData.sexLimit === 2">女</span>
</el-descriptions-item>
<el-descriptions-item label="开启状态">
<span v-if="viewData.state">开启</span>
<span v-else>禁用</span>
</el-descriptions-item>
<el-descriptions-item label="场地介绍" :span="2">
<div v-if="viewData.introduce" v-html="viewData.introduce"></div>
<div v-else>暂无场地介绍</div>
</el-descriptions-item>
</el-descriptions>
</div>
`,
data() {
return {
viewData: {},
}
},
methods: {
onOpen(row) {
this.viewData = row
},
},
style: /*language=CSS*/ `
.el-descriptions-item__label {
width: 200px;
min-width: 200px;
max-width: 200px;
}
`
};
@@ -0,0 +1,140 @@
<!--#
layout("/layouts/platform.html"){
#-->
<div id="app" v-cloak>
<guava ref="guava">
<template>
<el-card shadow="never">
<search @search="doSearch">
<search-item label="姓名/工号/场地">
<el-input placeholder="请输入姓名/工号/场地" clearable v-model="pageForm.searchKeyword"
@keyup.enter.native="doSearch">
</el-input>
</search-item>
<search-item label="活动场地">
<el-select v-model="pageForm.siteId" @change="doSearch" style="width: 100%" placeholder="请选择活动场地" filterable clearable>
<el-option v-for="item in siteOptions" :value="item.id" :key="item.id" :label="item.name"></el-option>
</el-select>
</search-item>
<search-item label="场地类型">
<el-select v-model="pageForm.siteType" @change="doSearch" style="width: 100%" placeholder="请选择场地类型" filterable clearable>
<el-option v-for="item in typeOptions" :value="item.id" :key="item.id" :label="item.name"></el-option>
</el-select>
</search-item>
</search>
</el-card>
<el-card shadow="never">
<table-tool label="申请列表"></table-tool>
<el-table :data="tableData" @sort-change="pageOrder" style="width: 100%">
<el-table-column type="index" width="50" label="序号" :index="indexMethod"></el-table-column>
<el-table-column
:label="column.label"
:prop="column.prop"
:key="column.prop"
:width="column.width"
:sortable="column.sortable"
align="center"
header-align="center"
show-overflow-tooltip
v-for="column in tableColumns"
>
<template v-slot="{ row }" v-if="column.prop === 'instanceState'">
<enum-tag :value="row.instanceState" name="ProcessInstanceStateEnum" label_key="message" size="small"></enum-tag>
</template>
<template v-slot="{ row }" v-else-if="column.prop === 'reserveType'">
<span>{{ row.reserveType === 'club' ? '协会预约' : '分工会预约' }}</span>
</template>
<template v-slot="{ row }" v-else-if="column.prop === 'reserveTargetName'">
<span>{{ row.reserveType === 'club' ? (row.clubName || '-') : (row.applyUnionName || '-') }}</span>
</template>
</el-table-column>
<el-table-column label="操作" fixed="right" width="180">
<template v-slot="{row}">
<el-button @click="onView(row)" size="mini" type="primary">查看</el-button>
<el-button v-if="row.canCancel" @click="onDelete(row)" size="mini" type="danger">删除</el-button>
</template>
</el-table-column>
</el-table>
<!--#include("/layouts/pagination.html"){}#-->
</el-card>
</template>
<template #edit>
<info ref="infoRef"></info>
</template>
</guava>
</div>
<script nonce="${cspNonce!}">
<!--#include('../common/applyInfo.js'){}#-->
new Vue({
el: '#app',
store,
mixins: [initTableMixins],
components: {
'info': siteCugApplyInfo,
},
data() {
return {
typeOptions: [],
siteOptions: [],
tableColumns: [
{prop: 'siteName', label: '场地名称'},
{prop: 'applyUserName', label: '预约人'},
{prop: 'reserveType', label: '预约类型'},
{prop: 'reserveTargetName', label: '预约单位'},
{prop: 'applyUnitName', label: '所属单位'},
{prop: 'reserveStartTime', label: '开始时间'},
{prop: 'reserveEndTime', label: '结束时间'},
{prop: 'applyMobile', label: '联系方式'},
{prop: 'taskName', label: '当前节点'},
{prop: 'instanceState', label: '流程状态'},
],
}
},
methods: {
onView(row) {
this.$refs.guava.edit(() => {
this.$refs.infoRef.onOpen(row)
})
},
onDelete(row) {
const message = row.yearlyBatch
? ('该记录属于“预约本年”批量预约,确认后将一并删除同批次的 ' + (row.batchDeleteCount || 0) + ' 条预约记录,是否继续?')
: '您确定要删除吗?'
this.$confirm(message, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
this.$axios.post('/platform/siteCug/mine/delete', { id: row.id }).then((res) => {
if (res.code === 0) {
this.$message.success(res.msg)
this.pageData()
}
})
})
},
querySiteType() {
this.$axios.post('/platform/siteCug/function/type/queryFunctionType').then((res) => {
this.typeOptions = res.data
})
},
querySites() {
this.$axios.post('/platform/siteCug/manage/querySites').then((res) => {
this.siteOptions = res.data
})
},
},
async created() {
this.querySiteType()
this.querySites()
this.pageData()
}
})
</script>
<!--#
}
#-->
@@ -0,0 +1,187 @@
<!--#
layout("/layouts/platform.html"){
#-->
<div id="app" v-cloak>
<guava ref="guava">
<template>
<el-card shadow="never">
<search @search="doSearch">
<search-item label="姓名/工号/场地">
<el-input placeholder="请输入姓名/工号/场地" clearable v-model="pageForm.searchKeyword"
@keyup.enter.native="doSearch">
</el-input>
</search-item>
<search-item label="活动场地">
<el-select v-model="pageForm.siteId" @change="doSearch" style="width: 100%"
placeholder="请选择活动场地" filterable clearable>
<el-option v-for="item in siteOptions"
:value="item.id"
:key="item.id"
:label="item.name"></el-option>
</el-select>
</search-item>
<search-item label="场地类型">
<el-select v-model="pageForm.siteType" @change="doSearch" style="width: 100%"
placeholder="请选择场地类型" filterable clearable>
<el-option v-for="item in typeOptions"
:value="item.id"
:key="item.id"
:label="item.name"></el-option>
</el-select>
</search-item>
<search-item label="预约类型">
<el-select v-model="pageForm.reserveType" @change="doSearch" style="width: 100%"
placeholder="请选择预约类型" clearable>
<el-option label="分工会预约" value="union"></el-option>
<el-option label="协会预约" value="club"></el-option>
</el-select>
</search-item>
<search-item label="预约单位">
<el-input placeholder="请输入分工会或协会名称" clearable v-model="pageForm.reserveTargetKeyword"
@keyup.enter.native="doSearch">
</el-input>
</search-item>
<search-item label="预约开始时间">
<el-date-picker v-model="pageForm.reserveTimeStart"
type="datetime"
value-format="yyyy-MM-dd HH:mm:ss"
placeholder="请选择预约开始时间"
style="width: 100%"
clearable
@change="doSearch">
</el-date-picker>
</search-item>
<search-item label="预约结束时间">
<el-date-picker v-model="pageForm.reserveTimeEnd"
type="datetime"
value-format="yyyy-MM-dd HH:mm:ss"
placeholder="请选择预约结束时间"
style="width: 100%"
clearable
@change="doSearch">
</el-date-picker>
</search-item>
</search>
</el-card>
<el-card shadow="never">
<table-tool label="申请列表">
<el-button type="primary" size="small" @click="doExport">导出表格</el-button>
</table-tool>
<el-table :data="tableData" @sort-change="pageOrder" style="width: 100%">
<el-table-column type="index" width="50px" label="序号" :index="indexMethod"></el-table-column>
<el-table-column
:label="column.label"
:prop="column.prop"
:key="column.prop"
:width="column.width"
:sortable="column.sortable"
align="center"
header-align="center"
show-overflow-tooltip
v-for="column in tableColumns">
<template v-slot="{ row }" v-if="column.prop === 'reserveType'">
<span>{{ row.reserveType === 'club' ? '协会预约' : '分工会预约' }}</span>
</template>
<template v-slot="{ row }" v-else-if="column.prop === 'reserveTargetName'">
<span>{{ row.reserveType === 'club' ? (row.clubName || '-') : (row.applyUnionName || '-') }}</span>
</template>
</el-table-column>
<el-table-column label="操作" fixed="right" width="180">
<template v-slot="{row}">
<el-button @click="onView(row)" size="mini" type="primary">查看</el-button>
<el-button @click="onDelete(row.id)" size="mini" type="danger">删除</el-button>
</template>
</el-table-column>
</el-table>
<!--#include("/layouts/pagination.html"){}#-->
</el-card>
</template>
<template #edit>
<info ref="infoRef"></info>
</template>
</guava>
</div>
<script nonce="${cspNonce!}">
<!--#include('../common/applyInfo.js'){}#-->
new Vue({
el: '#app',
store,
mixins: [initTableMixins],
components: {
'info': siteCugApplyInfo,
},
data() {
return {
pageForm: {
audit: false,
siteType: '',
siteId: '',
reserveType: '',
reserveTargetKeyword: '',
reserveTimeStart: '',
reserveTimeEnd: '',
},
typeOptions: [],
siteOptions: [],
tableColumns: [
{prop: 'siteName', label: '场地名称'},
{prop: 'applyUserName', label: '预约人'},
{prop: 'reserveType', label: '预约类型'},
{prop: 'reserveTargetName', label: '预约单位'},
{prop: 'applyUnitName', label: '所属单位'},
{prop: 'reserveStartTime', label: '开始时间'},
{prop: 'reserveEndTime', label: '结束时间'},
{prop: 'applyMobile', label: '联系方式'},
],
}
},
methods: {
onView(row) {
this.$refs.guava.edit(() => {
this.$refs.infoRef.onOpen(row)
})
},
onDelete(id) {
this.$confirm('您确定要删除吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
this.$axios.post('/platform/siteCug/record/delete', { id }).then((res) => {
if (res.code === 0) {
this.$message.success(res.msg)
this.pageData()
}
})
})
},
// 导出内容和列表当前筛选条件保持一致,实现所见即所得
doExport() {
this.$downLoad('/platform/siteCug/record/doExport', this.pageForm)
},
querySiteType() {
this.$axios.post('/platform/siteCug/function/type/queryFunctionType').then((res) => {
this.typeOptions = res.data
})
},
querySites() {
this.$axios.post('/platform/siteCug/manage/querySites').then((res) => {
this.siteOptions = res.data
})
},
},
created() {
// 查询字段在 data 中一次性声明完整,避免 Vue 2 对后加属性渲染不稳定
this.querySiteType()
this.querySites()
this.pageData()
}
})
</script>
<!--#
}
#-->
@@ -0,0 +1,237 @@
<!--#
layout("/layouts/platform.html"){
#-->
<div id="app">
<guava ref="guava">
<el-card shadow="never">
<search @search="doSearch">
<search-item label="姓名/工号/场地">
<el-input placeholder="请输入姓名/工号/场地" clearable v-model="pageForm.searchKeyword"
@keyup.enter.native="doSearch">
</el-input>
</search-item>
<search-item label="活动场地">
<el-select v-model="pageForm.siteId" @change="doSearch" style="width: 100%"
placeholder="请选择活动场地" filterable clearable>
<el-option v-for="item in siteOptions"
:value="item.id"
:key="item.id"
:label="item.name"
></el-option>
</el-select>
</search-item>
<search-item label="场地类型">
<el-select v-model="pageForm.siteType" @change="doSearch" style="width: 100%"
placeholder="请选择场地类型" filterable clearable>
<el-option v-for="item in typeOptions"
:value="item.id"
:key="item.id"
:label="item.name"
></el-option>
</el-select>
</search-item>
</search>
</el-card>
<el-card shadow="never">
<table-tool label="申请列表">
<el-radio-group v-model="pageForm.approval" size="small" @change="doSearch">
<el-radio-button :label="true">已审核</el-radio-button>
<el-radio-button :label="false">未审核</el-radio-button>
</el-radio-group>
</table-tool>
<el-table :data="tableData" @sort-change="pageOrder" style="width: 100%">
<el-table-column type="index" width="50px" label="序号" :index="indexMethod"></el-table-column>
<el-table-column
:label="column.label"
:prop="column.prop"
:key="column.prop"
:width="column.width"
:sortable="column.sortable"
align="center"
header-align="center"
show-overflow-tooltip
v-for="column in tableColumns"
>
<template v-slot="{ row }" v-if="column.prop === 'instanceState'">
<enum-tag :value="row.instanceState" name="ProcessInstanceStateEnum" label_key="message" size="small"></enum-tag>
</template>
<template v-slot="{ row }" v-else-if="column.prop === 'reserveType'">
<span>{{ row.reserveType === 'club' ? '协会预约' : '分工会预约' }}</span>
</template>
<template v-slot="{ row }" v-else-if="column.prop === 'reserveTargetName'">
<span>{{ row.reserveType === 'club' ? (row.clubName || '-') : (row.applyUnionName || '-') }}</span>
</template>
</el-table-column>
<el-table-column label="操作" fixed="right" width="300px">
<template v-slot="{row}">
<el-button @click="onView(row)" size="mini" type="primary">查看</el-button>
<el-button v-if="row.taskState === 10" @click="onAudit(row)" size="mini" type="primary">审核</el-button>
<el-button v-if="canRevoke(row)" :loading="revokeLoading" :disabled="revokeLoading" @click="onRevoke(row)" size="mini" type="danger">撤回</el-button>
</template>
</el-table-column>
</el-table>
<!--#include("/layouts/pagination.html"){}#-->
</el-card>
<template #edit>
<info ref="infoRef">
<div v-if="showApprovalForm">
<div class="process-title">
{{formData.taskName}}
</div>
<el-form :model="formData" ref="formRef" :rules="formRules" label-width="0" label-suffix=""
class="flow-task-form">
<el-form-item label="审批意见" prop="tf_opinion"
:rules="[{required:true,message:'必填',trigger:['change','blur']}]">
<user-opinion-textarea v-model="formData.tf_opinion"></user-opinion-textarea>
</el-form-item>
</el-form>
<el-row type="flex" justify="end">
<el-button @click="$refs.guava.index()" size="small">取消</el-button>
<el-button @click="handleTaskAction(6)" :loading="auditLoading" :disabled="auditLoading" size="small" type="info">退回到发起人</el-button>
<el-button @click="handleTaskAction(2)" :loading="auditLoading" :disabled="auditLoading" size="small" type="danger">拒绝申请</el-button>
<el-button @click="handleTaskAction(1)" :loading="auditLoading" :disabled="auditLoading" size="small" type="primary">同意申请</el-button>
</el-row>
</div>
</info>
</template>
</guava>
</div>
<script nonce="${cspNonce!}">
<!--#include('../common/applyInfo.js'){}#-->
new Vue({
el: "#app",
store,
mixins: [initTableMixins],
components: {
"info": siteCugApplyInfo,
},
data() {
return {
pageForm: {
approval: false
},
tableColumns: [
{prop: 'siteName', label: '场地名称'},
{prop: 'applyUserName', label: '预约人'},
{prop: 'reserveType', label: '预约类型'},
{prop: 'reserveTargetName', label: '预约单位'},
{prop: 'applyUnitName', label: '所属单位'},
{prop: 'reserveStartTime', label: '开始时间'},
{prop: 'reserveEndTime', label: '结束时间'},
{prop: 'applyMobile', label: '联系方式'},
{prop: 'curTaskName', label: '当前节点'},
{prop: 'instanceState', label: '流程状态'},
],
typeOptions: [],
siteOptions: [],
formData: {},
formRules: {},
showApprovalForm: false,
auditLoading: false,
revokeLoading: false,
}
},
methods: {
canRevoke(row) {
return Number(row.instanceState) === 20
},
onView(row) {
this.$refs.guava.edit(() => {
this.showApprovalForm = false
this.$refs.infoRef.onOpen(row)
})
},
onAudit(row) {
this.$refs.guava.edit(() => {
this.showApprovalForm = true
this.formData = {
processTaskId: row.taskId,
applyId: row.id,
taskName: row.curTaskName,
tf_opinion: '',
yearlyBatch: row.yearlyBatch,
batchAuditCount: row.batchAuditCount,
}
this.$refs.infoRef.onOpen(row)
})
},
handleTaskAction(val) {
if (this.auditLoading) {
return
}
const message = this.formData.yearlyBatch
? ('该记录属于“预约本年”批量预约,提交后将一并审核同批次的 ' + (this.formData.batchAuditCount || 0) + ' 条预约记录,是否继续?')
: '您确定要提交吗?'
this.$confirm(message, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
this.auditLoading = true
this.$axios.post('/platform/siteCug/schoolUnionAudit/executeTask', {
data: JSON.stringify({
...this.formData,
submitType: val
})
}).then((res) => {
if (res.code === 0) {
this.$refs.guava.index()
this.$message.success(res.msg)
this.doSearch()
}
}).finally(() => {
this.auditLoading = false
})
}).catch(() => {})
},
onRevoke(row) {
if (!this.canRevoke(row) || this.revokeLoading) {
return
}
const message = row.yearlyBatch
? ('该记录属于“预约本年”批量预约,撤回后将一并撤回同批次的 ' + (row.batchAuditCount || 0) + ' 条审核任务,是否继续?')
: '您确定要撤回吗?'
this.$confirm(message, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'info'
}).then(() => {
this.revokeLoading = true
this.$axios.post('/platform/siteCug/schoolUnionAudit/revokeTask', { taskId: row.taskId, applyId: row.id }).then((res) => {
if (res.code === 0) {
this.$message.success(res.msg)
this.doSearch()
}
}).finally(() => {
this.revokeLoading = false
})
}).catch(() => {})
},
querySiteType() {
this.$axios.post('/platform/siteCug/function/type/queryFunctionType').then((res) => {
this.typeOptions = res.data
})
},
querySites() {
this.$axios.post('/platform/siteCug/manage/querySites').then((res) => {
this.siteOptions = res.data
})
},
},
async created() {
this.querySiteType()
this.querySites()
this.pageData()
}
})
</script>
<!--#
}
#-->
@@ -0,0 +1,68 @@
const basicForm = {
template: /*language=HTML*/ `
<div>
<el-form :model="formData" ref="formRef" :rules="formRules" label-width="110px" label-position="left">
<el-form-item label="类型编码" prop="code">
<el-input type="text" v-model="formData.code" maxlength="50"
placeholder="请输入类型编码"></el-input>
</el-form-item>
<el-form-item label="类型名称" prop="name">
<el-input type="text" v-model="formData.name" maxlength="50"
placeholder="请输入类型名称"></el-input>
</el-form-item>
<el-form-item label="是否启用" prop="enable">
<el-switch
v-model="formData.enable"
active-color="#13ce66"
inactive-color="#ff4949">
</el-switch>
</el-form-item>
</el-form>
<el-row class="mt10" justify="end" type="flex">
<el-button @click="$emit('refresh')">取消</el-button>
<el-button @click="onSubmit" type="primary">提交</el-button>
</el-row>
</div>
`,
data() {
return {
formData: {
enable: true
},
formRules: {
name: [{required: true, message: '必填', trigger: ['blur', 'change']}],
code: [{required: true, message: '必填', trigger: ['blur', 'change']}],
enable: [{required: true, message: '必填', trigger: ['blur', 'change']}],
},
}
},
methods: {
onOpen(row) {
if (row && row.id) {
this.formData = clone(row)
}
},
onSubmit() {
this.$refs.formRef.validate((valid) => {
if (valid) {
this.$confirm("您确定要提交吗?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
}).then(async () => {
const resp = await this.$axios.post("/platform/siteCug/function/type/submit", this.formData)
if (resp.code === 0) {
this.$message.success(resp.msg)
this.$emit('refresh')
} else {
this.$message.warning(resp.msg)
}
})
}
})
},
},
style: /*language=CSS*/ `
`
}
@@ -0,0 +1,139 @@
<!--#
layout("/layouts/platform.html"){
#-->
<style>
</style>
<div id="app" v-cloak>
<guava ref="guava">
<template>
<el-card shadow="never">
<search @search="doSearch">
<search-item label="名称/编码">
<el-input placeholder="请输入名称或编码" clearable v-model="pageForm.searchKeyword"
@keyup.enter.native="doSearch">
</el-input>
</search-item>
</search>
</el-card>
<el-card shadow="never" class="mt20">
<table-tool label="功能类型列表">
<el-button type="primary" size="small" @click="onAdd">
<i class="ti-plus"></i>
新增类型
</el-button>
</table-tool>
<el-table :data="tableData" @sort-change="pageOrder" :size="tableSize">
<el-table-column label="序号" type="index" :index="indexMethod" width="60"></el-table-column>
<el-table-column
:label="column.label"
:prop="column.prop"
:key="column.prop"
:width="column.width"
:sortable="column.sortable"
align="center"
header-align="center"
show-overflow-tooltip
v-for="column in tableColumns"
>
<template v-slot="{ row }" v-if="column.prop === 'enable'">
<el-switch
@change="switchChange(row)"
v-model="row.enable"
active-color="#13ce66"
inactive-color="#ff4949">
</el-switch>
</template>
</el-table-column>
<el-table-column label="操作" width="230">
<template v-slot="{ row }">
<el-button @click="onEdit(row)" size="mini" type="primary">编辑</el-button>
<el-button @click="onDelete(row)" size="mini" type="danger">删除</el-button>
</template>
</el-table-column>
</el-table>
<!--#include("/layouts/pagination.html"){}#-->
</el-card>
</template>
<template #edit>
<basic-form ref="basicFormRef" @refresh="refresh"></basic-form>
</template>
</guava>
</div>
<script nonce="${cspNonce!}">
<!--#include('basicForm.js'){}#-->
const vue = new Vue({
el: "#app",
mixins: [initTableMixins],
components: {
"basic-form": basicForm,
},
data() {
return {
tableColumns: [
{prop: 'code', label: '类型编码'},
{prop: 'name', label: '类型名称'},
{prop: 'enable', label: '是否启用'},
],
}
},
methods: {
refresh() {
this.doSearch()
this.$refs.guava.index()
},
onAdd() {
this.$refs.guava.edit(() => {
this.$refs.basicFormRef.onOpen()
})
},
onEdit(row) {
this.$refs.guava.edit(() => {
this.$refs.basicFormRef.onOpen(row)
})
},
onDelete(row) {
this.$confirm("您确定要删除吗, 是否继续?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
})
.then(() => {
this.$axios.post("/platform/siteCug/function/type/delete", { id: row.id }).then((res) => {
if (res.code === 0) {
this.$message.success(res.msg)
this.pageData()
}
})
})
.catch(() => {})
},
switchChange(row) {
this.$axios.post("/platform/siteCug/function/type/submit", row).then((res) => {
if (res.code === 0) {
this.$message.success(res.msg)
this.pageData()
}
})
},
pageData() {
this.$axios.post(loc() + "/pageData", this.pageForm).then((res) => {
if (res.code === 0) {
this.tableData = res.data.list
this.pageForm.totalCount = res.data.totalCount
}
})
},
},
async created() {
this.pageData()
}
})
</script>
<!--#
}
#-->
@@ -0,0 +1,887 @@
new Vue({
el: '#app',
store,
data() {
return {
siteId: GetQueryString('siteId'),
row: {},
siteLoaded: false,
clubOptions: [],
timeLimitConfig: {
filterHolidays: false,
holidayList: [],
notApplyTimeList: [],
},
formData: {
reserveType: 'union',
applyUnionId: '',
applyUnionName: '',
clubId: '',
clubName: '',
applyMobile: '',
reserveStartTime: '',
reserveEndTime: '',
joinCount: 1,
applyCause: '',
},
showReserveTypePicker: false,
showClubPicker: false,
showTimePicker: false,
timePickerField: '',
timePickerColumns: [],
timePickerParts: {
date: '',
hour: null,
minute: null,
},
timePickerSyncing: false,
pickerFilterCache: {},
}
},
computed: {
reserveTypeColumns() {
return ['分工会预约', '协会预约']
},
reserveTypeText() {
if (this.formData.reserveType === 'club') {
return '协会预约'
}
if (this.formData.reserveType === 'union') {
return '分工会预约'
}
return ''
},
clubColumns() {
return this.clubOptions.map(item => item.clubName)
},
timePickerTitle() {
return this.timePickerField === 'reserveEndTime' ? '选择预约结束时间' : '选择预约开始时间'
},
},
methods: {
historyBack,
clearTimePickerCache() {
this.pickerFilterCache = {}
},
buildDefaultFormData() {
const user = this.$store.state.user || {}
return {
siteId: this.row.id || '',
applyUserId: user.id || '',
applyUserName: user.username || '',
applySex: user.sex || '',
applyLoginName: user.loginname || '',
applyUnitId: user.unit ? user.unit.id : '',
applyUnitName: user.unit ? user.unit.name : '',
reserveType: 'union',
applyUnionId: user.union ? user.union.id : '',
applyUnionName: user.union ? user.union.name : '',
clubId: '',
clubName: '',
applyMobile: user.mobile || '',
reserveStartTime: '',
reserveEndTime: '',
joinCount: 1,
applyCause: '',
}
},
getNormalizedMoment(value) {
return this.$moment(value).seconds(0).milliseconds(0)
},
getFieldMinMoment(field) {
const now = this.$moment().add(1, 'minute').seconds(0).milliseconds(0)
if (field === 'reserveEndTime' && this.formData.reserveStartTime) {
const start = this.getNormalizedMoment(this.formData.reserveStartTime).add(1, 'minute')
return start.isAfter(now) ? start : now
}
return now
},
getFieldLatestMoment(field) {
if (field === 'reserveStartTime' && this.formData.reserveEndTime) {
return this.getNormalizedMoment(this.formData.reserveEndTime).subtract(1, 'minute')
}
return null
},
getFieldMaxMoment(field) {
const defaultMax = this.$moment().add(365, 'day').endOf('day').seconds(0).milliseconds(0)
const latest = this.getFieldLatestMoment(field)
if (latest && latest.isBefore(defaultMax)) {
return latest.clone().seconds(0).milliseconds(0)
}
return defaultMax
},
getContinuousEndBounds() {
if (!this.formData.reserveStartTime) {
return null
}
const minMoment = this.getFieldMinMoment('reserveEndTime')
let maxMoment = this.getFieldMaxMoment('reserveEndTime')
if (maxMoment.isBefore(minMoment)) {
return null
}
let boundary = null
const blockedDayCursor = minMoment.clone().startOf('day').add(1, 'day')
while (blockedDayCursor.isSameOrBefore(maxMoment, 'day')) {
if (this.isBlockedDay(blockedDayCursor)) {
boundary = blockedDayCursor.clone().startOf('day')
break
}
blockedDayCursor.add(1, 'day')
}
const list = Array.isArray(this.timeLimitConfig.notApplyTimeList) ? this.timeLimitConfig.notApplyTimeList : []
list.forEach(item => {
if (!item || !item.date || !item.startTime || !item.endTime) {
return
}
const dayStr = this.$moment(item.date).format('YYYY-MM-DD')
const rangeStart = this.$moment(dayStr + ' ' + this.normalizeTime(item.startTime))
const rangeEnd = this.$moment(dayStr + ' ' + this.normalizeTime(item.endTime))
if (rangeEnd.isSameOrBefore(minMoment)) {
return
}
if (rangeStart.isSameOrBefore(minMoment) && rangeEnd.isAfter(minMoment)) {
boundary = minMoment.clone()
return
}
if (rangeStart.isAfter(minMoment) && rangeStart.isSameOrBefore(maxMoment)) {
if (!boundary || rangeStart.isBefore(boundary)) {
boundary = rangeStart.clone()
}
}
})
if (boundary && boundary.isSameOrBefore(maxMoment)) {
maxMoment = boundary.clone().subtract(1, 'minute')
}
if (maxMoment.isBefore(minMoment)) {
return null
}
return {
minMoment: minMoment,
maxMoment: maxMoment,
}
},
getEffectiveFieldMaxMoment(field) {
if (field === 'reserveEndTime' && this.formData.reserveStartTime) {
const bounds = this.getContinuousEndBounds()
return bounds ? bounds.maxMoment.clone() : this.getFieldMinMoment(field).clone().subtract(1, 'minute')
}
return this.getFieldMaxMoment(field)
},
buildPickerMoment(parts) {
if (parts && parts.date) {
return this.$moment(parts.date, 'YYYY-MM-DD')
.hour(parts.hour || 0)
.minute(parts.minute || 0)
.second(0)
.millisecond(0)
}
return this.$moment({
year: parts.year,
month: parts.month - 1,
date: parts.day,
hour: parts.hour,
minute: parts.minute,
second: 0,
millisecond: 0,
})
},
extractTimePickerPartsFromMoment(momentValue) {
const pickerMoment = this.getNormalizedMoment(momentValue)
return {
date: pickerMoment.format('YYYY-MM-DD'),
hour: pickerMoment.hour(),
minute: pickerMoment.minute(),
}
},
getPickerPartValue(item, fallback) {
if (item && typeof item === 'object' && item !== null && typeof item.value !== 'undefined') {
return item.value
}
if (item === '' || item === null || typeof item === 'undefined') {
return fallback
}
return item
},
extractTimePickerParts(values) {
const fallback = this.timePickerParts || {}
return {
date: this.getPickerPartValue(values && values[0], fallback.date),
hour: Number(this.getPickerPartValue(values && values[1], fallback.hour)),
minute: Number(this.getPickerPartValue(values && values[2], fallback.minute)),
}
},
isValidCalendarMoment(momentValue, month, day) {
return momentValue.isValid() && momentValue.month() + 1 === month && momentValue.date() === day
},
isBlockedDay(momentValue) {
if (!momentValue || !momentValue.isValid()) {
return true
}
if (momentValue.day() === 0 || momentValue.day() === 6) {
return true
}
return this.timeLimitConfig.filterHolidays && this.timeLimitConfig.holidayList.includes(momentValue.format('YYYY-MM-DD'))
},
isRangeSelectable(start, end) {
if (!start || !end || !start.isValid() || !end.isValid()) {
return false
}
if (!end.isAfter(start)) {
return false
}
if (this.hasHolidayInRange(start, end)) {
return false
}
if (this.overlapsDisabledRange(start, end)) {
return false
}
return true
},
isExactTimeSelectable(momentValue, field) {
if (!momentValue || !momentValue.isValid()) {
return false
}
const candidate = momentValue.clone().seconds(0).milliseconds(0)
const minMoment = this.getFieldMinMoment(field)
if (candidate.isBefore(minMoment)) {
return false
}
const latestMoment = this.getFieldLatestMoment(field)
if (latestMoment && candidate.isAfter(latestMoment)) {
return false
}
if (field === 'reserveEndTime' && this.formData.reserveStartTime) {
const bounds = this.getContinuousEndBounds()
if (!bounds) {
return false
}
return candidate.isSameOrAfter(bounds.minMoment) && candidate.isSameOrBefore(bounds.maxMoment)
}
if (this.isDateTimeBlocked(candidate.format('YYYY-MM-DD HH:mm:ss'))) {
return false
}
if (field === 'reserveStartTime' && this.formData.reserveEndTime) {
const end = this.getNormalizedMoment(this.formData.reserveEndTime)
if (!candidate.isBefore(end)) {
return false
}
return this.isRangeSelectable(candidate, end)
}
return true
},
dayHasAvailableTime(year, month, day, field) {
const cacheKey = ['day', field, year, month, day, this.formData.reserveStartTime || '', this.formData.reserveEndTime || ''].join('|')
if (cacheKey in this.pickerFilterCache) {
return this.pickerFilterCache[cacheKey]
}
const dayMoment = this.buildPickerMoment({ year: year, month: month, day: day, hour: 0, minute: 0 })
if (!this.isValidCalendarMoment(dayMoment, month, day) || this.isBlockedDay(dayMoment)) {
this.pickerFilterCache[cacheKey] = false
return false
}
const minMoment = this.getFieldMinMoment(field)
const maxMoment = this.getEffectiveFieldMaxMoment(field)
const dayStart = dayMoment.clone().startOf('day')
const dayEnd = dayMoment.clone().endOf('day')
if (dayEnd.isBefore(minMoment) || dayStart.isAfter(maxMoment)) {
this.pickerFilterCache[cacheKey] = false
return false
}
if (field === 'reserveEndTime' && this.formData.reserveStartTime) {
this.pickerFilterCache[cacheKey] = true
return true
}
for (let hour = 0; hour < 24; hour++) {
if (this.hourHasAvailableTime(year, month, day, hour, field)) {
this.pickerFilterCache[cacheKey] = true
return true
}
}
this.pickerFilterCache[cacheKey] = false
return false
},
hourHasAvailableTime(year, month, day, hour, field) {
const cacheKey = ['hour', field, year, month, day, hour, this.formData.reserveStartTime || '', this.formData.reserveEndTime || ''].join('|')
if (cacheKey in this.pickerFilterCache) {
return this.pickerFilterCache[cacheKey]
}
const hourMoment = this.buildPickerMoment({ year: year, month: month, day: day, hour: hour, minute: 0 })
if (!this.isValidCalendarMoment(hourMoment, month, day)) {
this.pickerFilterCache[cacheKey] = false
return false
}
const minMoment = this.getFieldMinMoment(field)
const maxMoment = this.getEffectiveFieldMaxMoment(field)
const hourStart = hourMoment.clone().startOf('hour')
const hourEnd = hourMoment.clone().endOf('hour')
if (hourEnd.isBefore(minMoment) || hourStart.isAfter(maxMoment)) {
this.pickerFilterCache[cacheKey] = false
return false
}
if (field === 'reserveEndTime' && this.formData.reserveStartTime) {
this.pickerFilterCache[cacheKey] = true
return true
}
for (let minute = 0; minute < 60; minute++) {
const candidate = this.buildPickerMoment({ year: year, month: month, day: day, hour: hour, minute: minute })
if (this.isExactTimeSelectable(candidate, field)) {
this.pickerFilterCache[cacheKey] = true
return true
}
}
this.pickerFilterCache[cacheKey] = false
return false
},
findFirstSelectableDateTime(field, baseMoment) {
let startMoment = this.getNormalizedMoment(baseMoment)
const minMoment = this.getFieldMinMoment(field)
const maxMoment = this.getEffectiveFieldMaxMoment(field)
if (maxMoment.isBefore(minMoment)) {
return null
}
if (startMoment.isBefore(minMoment)) {
startMoment = minMoment.clone()
}
for (let dayOffset = 0; dayOffset <= 366; dayOffset++) {
const dayMoment = startMoment.clone().startOf('day').add(dayOffset, 'day')
if (dayMoment.isAfter(maxMoment, 'day')) {
break
}
if (this.isBlockedDay(dayMoment)) {
continue
}
const startHour = dayMoment.isSame(startMoment, 'day') ? startMoment.hour() : 0
for (let hour = startHour; hour < 24; hour++) {
const startMinute = dayMoment.isSame(startMoment, 'day') && hour === startMoment.hour() ? startMoment.minute() : 0
for (let minute = startMinute; minute < 60; minute++) {
const candidate = dayMoment.clone().hour(hour).minute(minute).second(0).millisecond(0)
if (candidate.isAfter(maxMoment)) {
return null
}
if (this.isExactTimeSelectable(candidate, field)) {
return candidate
}
}
}
}
return null
},
formatPickerNumber(value) {
return value < 10 ? '0' + value : '' + value
},
getWeekdayText(momentValue) {
const weekdayList = ['周日', '周一', '周二', '周三', '周四', '周五', '周六']
return weekdayList[momentValue.day()]
},
createPickerOption(text, value, disabled) {
return {
text: text,
value: value,
disabled: !!disabled,
}
},
findNearestEnabledIndex(options, preferredValue) {
if (!Array.isArray(options) || !options.length) {
return -1
}
const enabledIndexes = []
options.forEach((item, index) => {
if (!item.disabled) {
enabledIndexes.push(index)
}
})
if (!enabledIndexes.length) {
return -1
}
if (preferredValue === '' || preferredValue === null || typeof preferredValue === 'undefined' || Number.isNaN(preferredValue)) {
return enabledIndexes[0]
}
let targetIndex = enabledIndexes[0]
let targetDistance = Math.abs(Number(options[targetIndex].value) - Number(preferredValue))
enabledIndexes.forEach(index => {
const distance = Math.abs(Number(options[index].value) - Number(preferredValue))
if (distance < targetDistance) {
targetIndex = index
targetDistance = distance
}
})
return targetIndex
},
buildDateOptions(field) {
const minMoment = this.getFieldMinMoment(field).clone().startOf('day')
const maxMoment = this.getEffectiveFieldMaxMoment(field).clone().startOf('day')
const options = []
const cursor = minMoment.clone()
while (cursor.isSameOrBefore(maxMoment, 'day')) {
const text = cursor.format('MM-DD') + ' ' + this.getWeekdayText(cursor)
options.push(this.createPickerOption(text, cursor.format('YYYY-MM-DD'), !this.dayHasAvailableTime(cursor.year(), cursor.month() + 1, cursor.date(), field)))
cursor.add(1, 'day')
}
return options
},
buildHourOptionsByDate(field, dateValue) {
const dateMoment = this.$moment(dateValue, 'YYYY-MM-DD')
const year = dateMoment.year()
const month = dateMoment.month() + 1
const day = dateMoment.date()
const options = []
for (let hour = 0; hour < 24; hour++) {
options.push(this.createPickerOption(this.formatPickerNumber(hour) + '时', hour, !this.hourHasAvailableTime(year, month, day, hour, field)))
}
return options
},
buildMinuteOptionsByDateHour(field, dateValue, hour) {
const dateMoment = this.$moment(dateValue, 'YYYY-MM-DD')
const options = []
for (let minute = 0; minute < 60; minute++) {
const candidate = this.buildPickerMoment({
year: dateMoment.year(),
month: dateMoment.month() + 1,
day: dateMoment.date(),
hour: hour,
minute: minute,
})
options.push(this.createPickerOption(this.formatPickerNumber(minute) + '分', minute, !this.isExactTimeSelectable(candidate, field)))
}
return options
},
buildTimePickerColumns(field, preferredParts) {
const dateOptions = this.buildDateOptions(field)
const enabledDateIndex = dateOptions.findIndex(item => !item.disabled)
if (enabledDateIndex < 0) {
return null
}
let dateIndex = enabledDateIndex
if (preferredParts && preferredParts.date) {
const exactIndex = dateOptions.findIndex(item => item.value === preferredParts.date && !item.disabled)
if (exactIndex >= 0) {
dateIndex = exactIndex
}
}
const selectedDate = dateOptions[dateIndex].value
const hourOptions = this.buildHourOptionsByDate(field, selectedDate)
const hourIndex = this.findNearestEnabledIndex(hourOptions, preferredParts ? preferredParts.hour : null)
if (hourIndex < 0) {
return null
}
const selectedHour = hourOptions[hourIndex].value
const minuteOptions = this.buildMinuteOptionsByDateHour(field, selectedDate, selectedHour)
const minuteIndex = this.findNearestEnabledIndex(minuteOptions, preferredParts ? preferredParts.minute : null)
if (minuteIndex < 0) {
return null
}
const selectedMinute = minuteOptions[minuteIndex].value
return {
parts: {
date: selectedDate,
hour: selectedHour,
minute: selectedMinute,
},
indexes: [dateIndex, hourIndex, minuteIndex],
columns: [
{ values: dateOptions, defaultIndex: dateIndex },
{ values: hourOptions, defaultIndex: hourIndex },
{ values: minuteOptions, defaultIndex: minuteIndex },
],
}
},
sameTimePickerParts(left, right) {
if (!left || !right) {
return false
}
return left.date === right.date
&& left.hour === right.hour
&& left.minute === right.minute
},
syncTimePickerColumns(picker, built) {
if (!picker || !built) {
this.timePickerSyncing = false
return
}
this.timePickerSyncing = true
built.columns.forEach((column, index) => {
picker.setColumnValues(index, column.values)
})
picker.setIndexes(built.indexes)
this.timePickerParts = built.parts
setTimeout(() => {
this.timePickerSyncing = false
}, 0)
},
applyTimePickerState(built, picker, syncColumns = true) {
if (!built) {
return false
}
const currentPicker = picker || this.$refs.timePickerRef
if (syncColumns) {
this.timePickerColumns = built.columns
this.$nextTick(() => {
this.syncTimePickerColumns(currentPicker || this.$refs.timePickerRef, built)
})
return true
}
this.syncTimePickerColumns(currentPicker, built)
return true
},
async init() {
if (!this.siteId) {
this.$toast('未获取到场馆信息')
this.historyBack()
return
}
const siteRes = await this.$axios.post('/platform/siteCug/manage/info', { id: this.siteId })
if (siteRes.code !== 0 || !siteRes.data) {
this.$toast(siteRes.msg || '场馆信息加载失败')
this.historyBack()
return
}
this.row = siteRes.data
this.formData = this.buildDefaultFormData()
await this.queryTimeLimitConfig()
this.siteLoaded = true
},
async queryTimeLimitConfig() {
const fallback = {
filterHolidays: !!this.row.filterHolidays,
holidayList: [],
notApplyTimeList: Array.isArray(this.row.notApplyTimeList) ? this.row.notApplyTimeList : [],
}
try {
const res = await this.$axios.post('/platform/siteCug/apply/timeLimitConfig', {
siteId: this.row.id,
})
if (res.code === 0 && res.data) {
this.timeLimitConfig = {
filterHolidays: !!res.data.filterHolidays,
holidayList: Array.isArray(res.data.holidayList) ? res.data.holidayList : [],
notApplyTimeList: Array.isArray(res.data.notApplyTimeList) ? res.data.notApplyTimeList : [],
}
this.clearTimePickerCache()
return
}
} catch (e) {
}
this.timeLimitConfig = fallback
this.clearTimePickerCache()
},
async reserveTypeChange(value) {
const user = this.$store.state.user || {}
if (value === 'union') {
this.formData.applyUnionId = user.union ? user.union.id : ''
this.formData.applyUnionName = user.union ? user.union.name : ''
this.formData.clubId = ''
this.formData.clubName = ''
return
}
if (value === 'club') {
this.formData.applyUnionId = ''
this.formData.applyUnionName = ''
this.formData.clubId = ''
this.formData.clubName = ''
await this.loadManagedClubs()
}
},
async loadManagedClubs() {
try {
const res = await this.$axios.post('/platform/club/examine/apply/getClubsByRole')
this.clubOptions = Array.isArray(res.data) ? res.data : []
if (!this.clubOptions.length) {
this.$toast('您当前没有可预约的协会管理权限')
}
} catch (e) {
this.clubOptions = []
this.$toast('协会列表加载失败,请稍后重试')
}
},
onReserveTypeConfirm(value) {
this.showReserveTypePicker = false
const reserveType = value === '协会预约' ? 'club' : 'union'
this.$set(this.formData, 'reserveType', reserveType)
this.reserveTypeChange(reserveType)
},
async openClubPicker() {
if (this.formData.reserveType !== 'club') {
this.$toast('请先选择协会预约')
return
}
if (!this.clubOptions.length) {
await this.loadManagedClubs()
}
if (!this.clubOptions.length) {
return
}
this.showClubPicker = true
},
onClubConfirm(value, index) {
this.showClubPicker = false
const club = this.clubOptions[index]
this.formData.clubId = club ? club.id : ''
this.formData.clubName = club ? club.clubName : value
},
openTimePicker(field) {
this.timePickerField = field
this.timePickerSyncing = false
this.clearTimePickerCache()
let defaultTime = this.formData[field] ? this.getNormalizedMoment(this.formData[field]) : null
if (!defaultTime || !defaultTime.isValid()) {
defaultTime = field === 'reserveEndTime' && this.formData.reserveStartTime
? this.getNormalizedMoment(this.formData.reserveStartTime).add(1, 'hour')
: this.$moment().add(1, 'hour').startOf('hour')
}
const availableTime = this.findFirstSelectableDateTime(field, defaultTime)
if (!availableTime) {
this.$toast('当前没有可预约的时间')
return
}
const built = this.buildTimePickerColumns(field, this.extractTimePickerPartsFromMoment(availableTime))
if (!built) {
this.$toast('当前没有可预约的时间')
return
}
this.showTimePicker = true
this.applyTimePickerState(built, null, true)
},
onTimePickerChange(picker, values) {
if (!this.timePickerField || this.timePickerSyncing) {
return
}
const parts = this.extractTimePickerParts(values)
const built = this.buildTimePickerColumns(this.timePickerField, parts)
if (!built) {
return
}
if (this.sameTimePickerParts(parts, built.parts)) {
this.timePickerParts = built.parts
return
}
this.applyTimePickerState(built, picker, false)
},
onTimeConfirm(values) {
const parts = this.extractTimePickerParts(values)
const candidate = this.buildPickerMoment(parts)
if (!this.isExactTimeSelectable(candidate, this.timePickerField)) {
this.$toast('该时间不可预约,请重新选择')
const built = this.buildTimePickerColumns(this.timePickerField, parts)
this.applyTimePickerState(built, this.$refs.timePickerRef)
return
}
const formatted = candidate.format('YYYY-MM-DD HH:mm:ss')
this.timePickerSyncing = false
this.showTimePicker = false
this.$set(this.formData, this.timePickerField, formatted)
this.clearTimePickerCache()
this.timeFieldChange(this.timePickerField)
},
async normalizeReserveTypeData() {
const user = this.$store.state.user || {}
if (this.formData.reserveType === 'union') {
this.formData.applyUnionId = user.union ? user.union.id : ''
this.formData.applyUnionName = user.union ? user.union.name : ''
this.formData.clubId = ''
this.formData.clubName = ''
return true
}
if (this.formData.reserveType === 'club') {
if (!this.clubOptions.length) {
await this.loadManagedClubs()
}
const club = this.clubOptions.find(item => item.id === this.formData.clubId)
this.formData.clubName = club ? club.clubName : ''
this.formData.applyUnionId = ''
this.formData.applyUnionName = ''
return true
}
return false
},
normalizeTime(timeStr) {
if (!timeStr) {
return '00:00:00'
}
return timeStr.length === 5 ? timeStr + ':00' : timeStr
},
getDisabledRangesByDay(dayStr) {
const list = Array.isArray(this.timeLimitConfig.notApplyTimeList) ? this.timeLimitConfig.notApplyTimeList : []
return list
.filter(item => item && item.date && this.$moment(item.date).format('YYYY-MM-DD') === dayStr)
.sort((a, b) => this.normalizeTime(a.startTime).localeCompare(this.normalizeTime(b.startTime)))
},
isDateTimeBlocked(dateTimeStr) {
if (!dateTimeStr) {
return false
}
const target = this.$moment(dateTimeStr)
const dayStr = target.format('YYYY-MM-DD')
if (target.day() === 0 || target.day() === 6) {
return true
}
if (this.timeLimitConfig.filterHolidays && this.timeLimitConfig.holidayList.includes(dayStr)) {
return true
}
const disabledRanges = this.getDisabledRangesByDay(dayStr)
return disabledRanges.some(item => {
const start = this.$moment(dayStr + ' ' + this.normalizeTime(item.startTime))
const end = this.$moment(dayStr + ' ' + this.normalizeTime(item.endTime))
return target.isSameOrAfter(start) && target.isBefore(end)
})
},
hasHolidayInRange(start, end) {
const current = start.clone().startOf('day')
const endDay = end.clone().startOf('day')
while (current.isSameOrBefore(endDay)) {
if (current.day() === 0 || current.day() === 6) {
return true
}
if (this.timeLimitConfig.filterHolidays && this.timeLimitConfig.holidayList.includes(current.format('YYYY-MM-DD'))) {
return true
}
current.add(1, 'day')
}
return false
},
overlapsDisabledRange(start, end) {
const list = Array.isArray(this.timeLimitConfig.notApplyTimeList) ? this.timeLimitConfig.notApplyTimeList : []
return list.some(item => {
if (!item || !item.date || !item.startTime || !item.endTime) {
return false
}
const rangeStart = this.$moment(this.$moment(item.date).format('YYYY-MM-DD') + ' ' + this.normalizeTime(item.startTime))
const rangeEnd = this.$moment(this.$moment(item.date).format('YYYY-MM-DD') + ' ' + this.normalizeTime(item.endTime))
return start.isBefore(rangeEnd) && end.isAfter(rangeStart)
})
},
showSubmitError(message) {
return this.$dialog.alert({
title: '提示',
message: message || '提交失败',
}).catch(() => {})
},
validateTimeLimit(showMessage = true, useDialog = false) {
const reserveStartTime = this.formData.reserveStartTime
const reserveEndTime = this.formData.reserveEndTime
if (!reserveStartTime || !reserveEndTime) {
return true
}
const start = this.$moment(reserveStartTime)
const end = this.$moment(reserveEndTime)
const showError = (message) => {
if (!showMessage) {
return
}
if (useDialog) {
this.showSubmitError(message)
return
}
this.$toast(message)
}
if (this.isDateTimeBlocked(reserveStartTime) || this.isDateTimeBlocked(reserveEndTime)) {
showError('预约时间不能选择周末、节假日或禁用时间')
return false
}
if (this.hasHolidayInRange(start, end)) {
showError('预约时间范围内包含周末或节假日,请重新选择')
return false
}
if (this.overlapsDisabledRange(start, end)) {
showError('预约时间范围与禁用时间冲突,请重新选择')
return false
}
return true
},
timeFieldChange(field) {
const value = this.formData[field]
this.clearTimePickerCache()
if (!value) {
return
}
if (this.isDateTimeBlocked(value)) {
this.$toast('该时间点不可预约,请重新选择')
this.$set(this.formData, field, '')
return
}
if (field === 'reserveStartTime' && this.formData.reserveEndTime) {
const start = this.$moment(value)
const end = this.$moment(this.formData.reserveEndTime)
if (!end.isAfter(start)) {
this.$set(this.formData, 'reserveEndTime', '')
this.$toast('结束时间需晚于开始时间,请重新选择')
return
}
}
if (field === 'reserveEndTime' && this.formData.reserveStartTime) {
const start = this.$moment(this.formData.reserveStartTime)
const end = this.$moment(value)
if (!end.isAfter(start)) {
this.$toast('预约结束时间必须晚于开始时间')
this.$set(this.formData, field, '')
return
}
}
if (this.formData.reserveStartTime && this.formData.reserveEndTime && !this.validateTimeLimit()) {
this.$set(this.formData, field, '')
}
},
onSubmit() {
this.$refs.formRef.validate().then(async () => {
if (!this.formData.reserveType) {
await this.showSubmitError('请选择预约类型')
return
}
await this.normalizeReserveTypeData()
if (this.formData.reserveType === 'union' && !this.formData.applyUnionId) {
await this.showSubmitError('当前用户未关联分工会,不能发起分工会预约')
return
}
if (this.formData.reserveType === 'club' && !this.formData.clubId) {
await this.showSubmitError('请选择您管理的协会')
return
}
if (!this.formData.reserveStartTime || !this.formData.reserveEndTime) {
await this.showSubmitError('请选择预约开始和结束时间')
return
}
const start = this.$moment(this.formData.reserveStartTime)
const end = this.$moment(this.formData.reserveEndTime)
if (!start.isAfter(this.$moment())) {
await this.showSubmitError('预约开始时间必须晚于当前时间')
return
}
if (!end.isAfter(start)) {
await this.showSubmitError('预约结束时间必须晚于开始时间')
return
}
if (!this.validateTimeLimit(true, true)) {
return
}
this.$dialog.confirm({
title: '提示',
message: '您确定要提交吗?',
}).then(() => {
const loading = this.$toast.loading({
message: '提交中...',
forbidClick: true,
overlay: true,
duration: 0,
})
this.$axios.post('/platform/siteCug/apply/submit', {
data: JSON.stringify(this.formData),
}).then((res) => {
if (res.code === 0) {
this.$toast.success(res.msg || '提交成功')
this.$pjaxReplace('/platform/siteCug/apply/h5')
}
}).catch((err) => {
this.showSubmitError((err && err.msg) || '提交失败')
}).finally(() => {
loading.close()
})
}).catch(() => {})
}).catch(() => {})
},
},
created() {
this.init()
}
})
@@ -0,0 +1,167 @@
<!--#
layout("/layouts/platform_h5.html"){
#-->
<style scoped>
.page-container {
padding-bottom: 84px;
background: #f7f8fa;
min-height: calc(100vh - 46px);
}
.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: #ffffff;
box-shadow: 0 -2px 12px rgba(0, 0, 0, 0.06);
}
.footer-actions .van-button {
flex: 1;
}
.disabled-time-item {
line-height: 20px;
margin-bottom: 4px;
}
/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">
<van-cell-group title="场馆信息">
<van-cell title="场馆名称" :value="row.name || '-' "></van-cell>
<van-cell title="场地地址" class="direction-column-cell">
<template #default>
{{ row.address || '-' }}
</template>
</van-cell>
<van-cell title="联系人" :value="row.contactName || '-' "></van-cell>
<van-cell title="联系电话" :value="row.contactPhone || '-' "></van-cell>
<van-cell title="场地类型" :value="row.typeName || '-' "></van-cell>
</van-cell-group>
<van-cell-group title="预约须知">
<van-cell title="可预约日期" value="仅限工作日"></van-cell>
<van-cell title="节假日限制"
:value="timeLimitConfig.filterHolidays ? '节假日不可预约' : '不排除节假日'"></van-cell>
<van-cell title="禁用时段" class="direction-column-cell">
<template #default>
<div v-if="timeLimitConfig.notApplyTimeList.length">
<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>
</template>
</van-cell>
</van-cell-group>
<van-form ref="formRef" class="form-container" :show-error-message="false">
<van-cell-group title="申请信息">
<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
:value="reserveTypeText"
label="预约类型"
name="reserveType"
readonly
clickable
is-link
required
placeholder="请选择预约类型"
@click="showReserveTypePicker = true"
: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 v-model="formData.reserveStartTime" label="开始时间" name="reserveStartTime" readonly
clickable is-link required placeholder="请选择预约开始时间"
@click="openTimePicker('reserveStartTime')"
:rules="[{ required: true, message: '请选择预约开始时间' }]"></van-field>
<van-field v-model="formData.reserveEndTime" label="结束时间" name="reserveEndTime" readonly clickable
is-link required placeholder="请选择预约结束时间" @click="openTimePicker('reserveEndTime')"
:rules="[{ required: true, message: '请选择预约结束时间' }]"></van-field>
<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>
</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="showTimePicker" position="bottom" round>
<van-picker
ref="timePickerRef"
show-toolbar
value-key="text"
:title="timePickerTitle"
:columns="timePickerColumns"
@change="onTimePickerChange"
@confirm="onTimeConfirm"
@cancel="timePickerSyncing = false; showTimePicker = false">
</van-picker>
</van-popup>
</div>
<script nonce="${cspNonce!}">
<!--#include('apply.js'){}#-->
</script>
<!--#
}
#-->
@@ -0,0 +1,106 @@
<!--#
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">
<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/siteCug/apply/pageData" :page_form.sync="pageForm" ref="tableListRef" title="name" @ready="onReady">
<template v-slot="{ row }">
<table-column label="场地地址">{{ row.address }}</table-column>
<table-column label="联系人">{{ row.contactName }}</table-column>
<table-column label="联系方式">{{ row.contactPhone }}</table-column>
<table-column label="场地类型">{{ row.typeName }}</table-column>
</template>
<template #actions="{ 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>
</div>
<script nonce="${cspNonce!}">
<!--#include('../common/siteInfo.js'){}#-->
new Vue({
el: '#app',
store,
components: {
info: siteInfo,
},
data() {
return {
pageForm: {
pageNumber: 1,
pageSize: 10,
totalCount: 0,
searchKeyword: '',
type: null,
},
typeOptions: [],
}
},
methods: {
historyBack,
onView(row) {
this.$refs.infoRef.onOpen(row)
},
onApply(row) {
this.$pjaxReplace('/platform/siteCug/apply/form/h5?siteId=' + row.id)
},
async onReady() {
const typeList = await this.querySiteType()
this.typeOptions = [
{
text: '全部类型',
value: null,
}
].concat(typeList.map((item) => ({ text: item.name, value: item.id })))
if (this.typeOptions.length > 0) {
this.pageForm.type = this.typeOptions[0].value
this.doSearch()
}
},
async querySiteType() {
const res = await this.$axios.post('/platform/siteCug/function/type/queryFunctionType')
return Array.isArray(res.data) ? res.data : []
},
doSearch() {
this.$nextTick(() => {
this.pageForm.pageNumber = 1
this.pageForm.totalCount = 0
this.$refs.tableListRef.doSearch()
})
},
},
})
</script>
<!--#
}
#-->
@@ -0,0 +1,80 @@
const siteCugApplyInfoH5 = {
template:
/*language=HTML*/
`
<van-action-sheet v-model="visible" :title="sheetTitle">
<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="预约类型">{{ reserveTypeLabel }}</van-cell>
<van-cell title="预约主体">{{ reserveTargetName }}</van-cell>
<van-cell title="开始时间">{{ viewData.reserveStartTime }}</van-cell>
<van-cell title="结束时间">{{ viewData.reserveEndTime }}</van-cell>
<van-cell title="联系电话">{{ viewData.applyMobile }}</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" :key="task.id + '-first'">
<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 :key="task.id + '-done'">
<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="办理意见" class="direction-column-cell">
<div v-html="task.taskFormData.opinion || 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,
}
},
computed: {
reserveTypeLabel() {
return this.viewData.reserveType === 'club' ? '协会预约' : '分工会预约'
},
reserveTargetName() {
return this.viewData.reserveType === 'club' ? (this.viewData.clubName || '-') : (this.viewData.applyUnionName || '-')
},
sheetTitle() {
return this.visible && this.$slots.default ? '审核预约' : '预约详情'
},
},
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
}
})
},
},
};
@@ -0,0 +1,105 @@
const siteInfo = {
template: /*language=HTML*/ `
<div>
<van-action-sheet v-model="visible" title="场馆信息">
<div class="detail-container">
<van-cell-group title="基本信息">
<van-cell title="创建人" :value="viewData.createUserName || '-' "></van-cell>
<van-cell title="场馆名称" :value="viewData.name || '-' "></van-cell>
<van-cell title="场地地址" class="direction-column-cell">
<template #default>
{{ viewData.address || '-' }}
</template>
</van-cell>
<van-cell title="联系人" :value="viewData.contactName || '-' "></van-cell>
<van-cell title="联系电话" :value="viewData.contactPhone || '-' "></van-cell>
<van-cell title="排序编号" :value="viewData.sortNum || '-' "></van-cell>
<van-cell title="容纳人数" :value="viewData.maxNum || '-' "></van-cell>
<van-cell title="场地类型" :value="viewData.typeName || '-' "></van-cell>
<van-cell title="性别限制">
<template #default>
<span v-if="viewData.sexLimit === 1">男</span>
<span v-else-if="viewData.sexLimit === 2">女</span>
<span v-else>不限制</span>
</template>
</van-cell>
<van-cell title="开启状态">
<template #default>
<span v-if="viewData.state">开启</span>
<span v-else>禁用</span>
</template>
</van-cell>
<van-cell title="排除节假日">
<template #default>
<span v-if="viewData.filterHolidays">是</span>
<span v-else>否</span>
</template>
</van-cell>
<van-cell title="场地介绍" class="direction-column-cell">
<template #default>
<div v-if="viewData.introduce" v-html="viewData.introduce"></div>
<div v-else>暂无场地介绍</div>
</template>
</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="item.date + item.startTime + item.endTime + index">
<td>{{ item.date }}</td>
<td>{{ item.startTime }}</td>
<td>{{ item.endTime }}</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/ .direction-column-cell .van-cell__value {
white-space: normal;
text-align: left;
}
/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,151 @@
<!--#
layout("/layouts/platform_h5.html"){
#-->
<style scoped>
</style>
<div id="app" v-cloak>
<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.siteType" :options="typeOptions" :multiple="false" @change="doSearch"></van-dropdown-item>
<van-dropdown-item v-model="pageForm.siteId" :options="siteOptions" :multiple="false" @change="doSearch"></van-dropdown-item>
</van-dropdown-menu>
</van-sticky>
<table-list api="/platform/siteCug/mine/pageData" :page_form.sync="pageForm" ref="tableListRef" title="siteName" @ready="onReady">
<template v-slot="{index,row}">
<table-column label="场地名称">{{row.siteName}}</table-column>
<table-column label="预约类型">{{row.reserveType === 'club' ? '协会预约' : '分工会预约'}}</table-column>
<table-column label="预约主体">{{row.reserveType === 'club' ? (row.clubName || '-') : (row.applyUnionName || '-')}}</table-column>
<table-column label="所属单位">{{row.applyUnitName}}</table-column>
<table-column label="开始时间">{{row.reserveStartTime}}</table-column>
<table-column label="结束时间">{{row.reserveEndTime}}</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 delete" @click="onDelete(row)" v-if="row.canCancel">
<i class="fa fa-trash"></i>
<span>取消预约</span>
</div>
</template>
</table-list>
<info ref="infoRef"></info>
</div>
<script nonce="${cspNonce!}">
<!--#include('../common/applyInfo.js'){}#-->
new Vue({
el: '#app',
store,
components: {
info: siteCugApplyInfoH5,
},
data() {
return {
pageForm: {
pageNumber: 1,
pageSize: 10,
totalCount: 0,
siteType: null,
siteId: null,
},
typeOptions: [
{
text: '全部类型',
value: null,
}
],
siteOptions: [
{
text: '全部场地',
value: null,
}
],
}
},
methods: {
historyBack,
async onReady() {
await this.querySiteType()
await this.querySites()
this.doSearch()
},
onView(row) {
this.$refs.infoRef.onOpen(row)
},
onDelete(row) {
this.$dialog.confirm({
title: '提示',
message: row.yearlyBatch
? ('该记录属于“预约本年”批量预约,确认后将一并删除同批次的 ' + (row.batchDeleteCount || 0) + ' 条预约记录,是否继续?')
: '您确定要取消该预约吗?',
}).then(() => {
this.$axios.post('/platform/siteCug/mine/delete', { id: row.id }).then((res) => {
if (res.code === 0) {
this.$toast.success(res.msg || '取消成功')
this.doSearch()
} else {
this.$dialog.alert({
title: '提示',
message: res.msg || '取消失败',
}).catch(() => {})
}
}).catch((err) => {
this.$dialog.alert({
title: '提示',
message: (err && err.msg) || '取消失败',
}).catch(() => {})
})
}).catch(() => {})
},
querySiteType() {
return this.$axios.post('/platform/siteCug/function/type/queryFunctionType').then((res) => {
const list = Array.isArray(res.data) ? res.data : []
this.typeOptions = [
{
text: '全部类型',
value: null,
}
].concat(list.map((item) => ({
text: item.name,
value: item.id,
})))
})
},
querySites() {
return this.$axios.post('/platform/siteCug/manage/querySites').then((res) => {
const list = Array.isArray(res.data) ? res.data : []
this.siteOptions = [
{
text: '全部场地',
value: null,
}
].concat(list.map((item) => ({
text: item.name,
value: item.id,
})))
})
},
doSearch() {
this.$nextTick(() => {
this.pageForm.pageNumber = 1
this.pageForm.totalCount = 0
this.$refs.tableListRef.doSearch()
})
},
},
})
</script>
<!--#
}
#-->
@@ -0,0 +1,272 @@
<!--#
layout("/layouts/platform_h5.html"){
#-->
<style scoped>
.action-btn.disabled {
opacity: 0.45;
pointer-events: none;
}
.action-btn.loading {
opacity: 0.9;
}
.action-btn .van-loading {
margin-right: 4px;
}
</style>
<div id="app" v-cloak>
<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">
<van-dropdown-item v-model="pageForm.siteType" :options="typeOptions" :multiple="false" @change="doSearch"></van-dropdown-item>
<van-dropdown-item v-model="pageForm.siteId" :options="siteOptions" :multiple="false" @change="doSearch"></van-dropdown-item>
</van-dropdown-menu>
<van-tabs v-model="pageForm.approvalText" @change="onApprovalTabChange">
<van-tab title="已审核" name="1"></van-tab>
<van-tab title="未审核" name="0"></van-tab>
</van-tabs>
</van-sticky>
<table-list api="/platform/siteCug/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.reserveType === 'club' ? '协会预约' : '分工会预约'}}</table-column>
<table-column label="预约主体">{{row.reserveType === 'club' ? (row.clubName || '-') : (row.applyUnionName || '-')}}</table-column>
<table-column label="所属单位">{{row.applyUnitName}}</table-column>
<table-column label="开始时间">{{row.reserveStartTime}}</table-column>
<table-column label="结束时间">{{row.reserveEndTime}}</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="canRevoke(row)" :class="{ loading: revokeLoading }" @click="onRevoke(row)">
<van-loading v-if="revokeLoading" size="14px" color="#fff"></van-loading>
<i v-else 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="info" block :loading="auditLoading" :disabled="auditLoading" @click="handleTaskAction(6)">退回</van-button>
<van-button type="danger" block :loading="auditLoading" :disabled="auditLoading" @click="handleTaskAction(2)">不同意</van-button>
<van-button type="primary" block :loading="auditLoading" :disabled="auditLoading" @click="handleTaskAction(1)">同意</van-button>
</div>
</div>
</info>
</div>
<script nonce="${cspNonce!}">
<!--#include('../common/applyInfo.js'){}#-->
new Vue({
el: '#app',
store,
components: {
info: siteCugApplyInfoH5,
},
data() {
return {
pageForm: {
pageNumber: 1,
pageSize: 10,
totalCount: 0,
searchKeyword: '',
approvalText: '0',
approval: false,
siteType: null,
siteId: null,
},
typeOptions: [
{
text: '全部类型',
value: null,
}
],
siteOptions: [
{
text: '全部场地',
value: null,
}
],
formData: {},
showApprovalForm: false,
auditLoading: false,
revokeLoading: false,
}
},
methods: {
historyBack,
async onReady() {
await this.querySiteType()
await this.querySites()
this.doSearch()
},
onApprovalTabChange(val) {
this.pageForm.approval = val === '1'
this.doSearch()
},
canRevoke(row) {
return Number(row.instanceState) === 20
},
onView(row) {
this.showApprovalForm = false
this.$refs.infoRef.onOpen(row)
},
onAudit(row) {
this.showApprovalForm = true
this.formData = {
processTaskId: row.taskId,
applyId: row.id,
taskName: row.curTaskName,
tf_opinion: '',
yearlyBatch: row.yearlyBatch,
batchAuditCount: row.batchAuditCount,
}
this.$refs.infoRef.onOpen(row)
},
async handleTaskAction(submitType) {
if (this.auditLoading) {
return
}
try {
await this.$refs.formRef.validate()
this.$dialog.confirm({
title: '提示',
message: this.formData.yearlyBatch
? ('该记录属于“预约本年”批量预约,提交后将一并审核同批次的 ' + (this.formData.batchAuditCount || 0) + ' 条预约记录,是否继续?')
: '您确定要提交吗?',
}).then(() => {
this.auditLoading = true
this.$axios.post('/platform/siteCug/schoolUnionAudit/executeTask', {
data: JSON.stringify({
...this.formData,
submitType: submitType,
})
}).then((res) => {
if (res.code === 0) {
this.$refs.infoRef.onClose()
this.$toast.success(res.msg || '提交成功')
this.doSearch()
} else {
this.$dialog.alert({
title: '提示',
message: res.msg || '提交失败',
}).catch(() => {})
}
}).catch((err) => {
this.$dialog.alert({
title: '提示',
message: (err && err.msg) || '提交失败',
}).catch(() => {})
}).finally(() => {
this.auditLoading = false
})
}).catch(() => {})
} catch (e) {
}
},
onRevoke(row) {
if (!this.canRevoke(row) || this.revokeLoading) {
return
}
this.$dialog.confirm({
title: '提示',
message: row.yearlyBatch
? ('该记录属于“预约本年”批量预约,撤回后将一并撤回同批次的 ' + (row.batchAuditCount || 0) + ' 条审核任务,是否继续?')
: '您确定要撤回吗?',
}).then(() => {
this.revokeLoading = true
this.$axios.post('/platform/siteCug/schoolUnionAudit/revokeTask', { taskId: row.taskId, applyId: row.id }).then((res) => {
if (res.code === 0) {
this.$toast.success(res.msg || '撤回成功')
this.doSearch()
} else {
this.$dialog.alert({
title: '提示',
message: res.msg || '撤回失败',
}).catch(() => {})
}
}).catch((err) => {
this.$dialog.alert({
title: '提示',
message: (err && err.msg) || '撤回失败',
}).catch(() => {})
}).finally(() => {
this.revokeLoading = false
})
}).catch(() => {})
},
querySiteType() {
return this.$axios.post('/platform/siteCug/function/type/queryFunctionType').then((res) => {
const list = Array.isArray(res.data) ? res.data : []
this.typeOptions = [
{
text: '全部类型',
value: null,
}
].concat(list.map((item) => ({
text: item.name,
value: item.id,
})))
})
},
querySites() {
return this.$axios.post('/platform/siteCug/manage/querySites').then((res) => {
const list = Array.isArray(res.data) ? res.data : []
this.siteOptions = [
{
text: '全部场地',
value: null,
}
].concat(list.map((item) => ({
text: item.name,
value: item.id,
})))
})
},
doSearch() {
this.$nextTick(() => {
this.pageForm.pageNumber = 1
this.pageForm.totalCount = 0
this.$refs.tableListRef.doSearch()
})
},
},
})
</script>
<!--#
}
#-->