# Conflicts:
#	src/main/resources/static/components/module/activity/UserScope.vue
This commit is contained in:
=
2026-04-30 11:09:23 +08:00
53 changed files with 7027 additions and 1610 deletions
@@ -148,6 +148,7 @@
:prop="column.prop"
:key="column.prop"
:sortable="column.sortable"
:formatter="column.formatter"
></el-table-column>
<el-table-column align="center" header-align="center" label="操作" width="100">
<template slot-scope="{ row }">
@@ -232,7 +233,7 @@ module.exports = {
{prop: "loginName", label: "工号"},
{prop: "userName", label: "姓名"},
{prop: "sex", label: "性别", sortable: true},
{prop: "birthday", label: "出生年月"},
{prop: "birthday", label: "出生年月", formatter: this.formatDate},
{prop: "mobile", label: "联系电话"},
{prop: "personType", label: "教职工类别", sortable: true},
{prop: "userState", label: "在职状态", sortable: true},
@@ -263,6 +264,9 @@ module.exports = {
}
return group.groupName + "(人员结果)"
},
formatDate(row, column, cellValue) {
return cellValue ? moment(cellValue).format("YYYY-MM-DD") : ""
},
viewGroupName() {
if (this.pageForm.groupId) {
const group = this.activityGroupList.find((v) => v.groupId === this.pageForm.groupId)
File diff suppressed because it is too large Load Diff
@@ -43,7 +43,7 @@ layout("/layouts/platform.html"){
<el-table-column type="index" width="50px" label="序号" :index="indexMethod"></el-table-column>
<el-table-column prop="displayName" label="流程定义名称"></el-table-column>
<el-table-column prop="name" label="流程定义编码"></el-table-column>
<el-table-column prop="category" label="流程分类">
<el-table-column prop="category" label="流程分类" sortable="custom">
<template slot-scope="{row}">
{{categoryOptions.find(v=>v.id === row.category)?.name}}
</template>
@@ -54,6 +54,19 @@ layout("/layouts/platform.html"){
<el-tag size="mini" v-else-if="row.isDeployed===0" type="info"></el-tag>
</template>
</el-table-column>
<el-table-column label="排序编码" prop="sortNo" width="120" align="center" header-align="center" sortable="custom">
<template slot-scope="{row}">
<el-input-number
v-model="row.sortNo"
:controls="false"
:min="0"
:precision="0"
size="mini"
style="width: 88px"
@change="updateSortNo(row)"
></el-input-number>
</template>
</el-table-column>
<el-table-column label="操作" fixed="right" width="450px">
<template slot-scope="{row}">
<el-button size="mini" type="primary" @click="onDesign(row)">设计</el-button>
@@ -237,6 +250,22 @@ layout("/layouts/platform.html"){
}
})
},
updateSortNo(row) {
const sortNo = Number(row.sortNo)
if (!Number.isInteger(sortNo) || sortNo < 0) {
this.$message.error("排序编码必须是非负整数")
return
}
$.post("/flow/design/updateSortNo", {
id: row.id,
sortNo
}).then((res) => {
if (res.code === 0) {
this.$message.success(res.msg)
this.pageData()
}
})
},
},
created() {
this.pageData()
@@ -0,0 +1,524 @@
<!--#
layout("/layouts/platform.html"){
#-->
<style>
#app {
display: flex;
justify-content: center;
align-items: center;
min-height: calc(100vh - 40px);
}
.tool-card {
width: calc(100% - 20px);
min-height: calc(100vh - 60px);
}
.tool-card .el-card__body {
height: calc(100% - 57px);
}
.tool-body {
display: flex;
justify-content: center;
min-height: calc(100vh - 180px);
}
.tool-body-center {
width: 70%;
min-width: 900px;
position: relative;
}
.wizard-box {
margin-top: 20px;
border: 1px solid #ebeef5;
border-radius: 8px;
background: #fff;
overflow: hidden;
}
.wizard-panel {
padding: 18px;
min-height: 520px;
}
.option-toolbar {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 16px;
}
.option-list {
width: 100%;
display: block;
max-height: 470px;
overflow-y: auto;
padding-right: 8px;
box-sizing: border-box;
}
.option-list .el-radio,
.option-list .el-checkbox {
display: flex;
width: 100%;
margin: 0 0 12px 0 !important;
align-items: center;
}
.option-list .el-radio.is-bordered,
.option-list .el-checkbox.is-bordered {
margin-left: 0 !important;
width: 100%;
box-sizing: border-box;
}
.option-list .el-radio__label,
.option-list .el-checkbox__label {
display: flex;
flex: 1;
min-width: 0;
padding-right: 0;
}
.option-extra {
color: #f56c6c;
margin-left: auto;
padding-left: 12px;
flex-shrink: 0;
}
.option-item-content {
display: flex;
align-items: center;
width: 100%;
min-width: 0;
gap: 8px;
}
.option-item-main {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.relation-table {
height: 500px;
}
.submit-panel {
max-width: 900px;
margin: 0 auto;
padding-top: 10px;
}
.result-panel {
min-height: 420px;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
}
.wizard-footer {
display: flex;
justify-content: center;
gap: 12px;
margin-top: 18px;
}
</style>
<div id="app" v-cloak>
<el-card class="tool-card" shadow="never">
<div slot="header">
<span>高级工具</span>
</div>
<div class="tool-body">
<div class="tool-body-center">
<el-steps :active="active" align-center finish-status="success">
<el-step title="选择数据表" icon="el-icon-coin"></el-step>
<el-step title="选择字段" icon="el-icon-s-grid"></el-step>
<el-step title="字段对应" icon="el-icon-connection"></el-step>
<el-step title="提交导入" icon="el-icon-upload2"></el-step>
<el-step title="导入结果" icon="el-icon-success"></el-step>
</el-steps>
<div class="wizard-box">
<div class="wizard-panel" v-if="active === 0">
<div class="option-toolbar">
<el-input
v-model.trim="tableKeyword"
clearable
placeholder="输入表名或备注搜索"
style="width: 260px">
</el-input>
<el-button type="primary" icon="el-icon-search" @click="filterTables">搜索</el-button>
<el-button v-if="formData.tableName" type="text">当前选择:{{ formData.tableName }}</el-button>
</div>
<el-radio-group v-model="formData.tableName" class="option-list">
<el-radio
v-for="item in filteredTables"
:key="item.table_name"
:label="item.table_name"
border>
<div class="option-item-content">
<span class="option-item-main">{{ item.table_name }}</span>
<span v-if="item.table_comment" class="option-item-main">{{ item.table_comment }}</span>
<span class="option-extra">rows: {{ item.table_rows || 0 }}</span>
</div>
</el-radio>
</el-radio-group>
</div>
<div class="wizard-panel" v-if="active === 1">
<div class="option-toolbar">
<el-input
v-model.trim="columnKeyword"
clearable
placeholder="输入字段名或备注搜索"
style="width: 260px">
</el-input>
<el-button type="primary" icon="el-icon-search" @click="filterColumns">搜索</el-button>
<el-button type="text">{{ formData.tableName }}</el-button>
</div>
<el-checkbox-group v-model="formData.columns" class="option-list">
<el-checkbox
v-for="item in filteredColumns"
:key="item.column_name"
:label="item.column_name"
border>
<div class="option-item-content">
<span class="option-item-main">{{ item.column_name }}</span>
<span v-if="item.column_comment" class="option-item-main">{{ item.column_comment }}</span>
<span class="option-extra">{{ item.column_type }}</span>
</div>
</el-checkbox>
</el-checkbox-group>
</div>
<div class="wizard-panel" v-if="active === 2">
<el-alert
title="这里填写 Excel 第一行表头名称,用来和数据库字段建立对应关系。"
type="info"
:closable="false"
style="margin-bottom: 16px">
</el-alert>
<el-table :data="relation" border class="relation-table">
<el-table-column prop="column_name" label="字段名" min-width="180"></el-table-column>
<el-table-column prop="column_comment" label="字段备注" min-width="220" show-overflow-tooltip></el-table-column>
<el-table-column prop="column_type" label="字段类型" width="160"></el-table-column>
<el-table-column prop="column_key" label="键" width="80"></el-table-column>
<el-table-column label="Excel 表头" min-width="260">
<template slot-scope="{ row }">
<el-input v-model.trim="row.relation" placeholder="请输入 Excel 表头名称"></el-input>
</template>
</el-table-column>
</el-table>
</div>
<div class="wizard-panel" v-if="active === 3">
<div class="submit-panel">
<el-form label-width="120px">
<el-form-item label="导入方式">
<el-radio-group v-model="formData.method">
<el-radio-button :label="1">追加或更新</el-radio-button>
<el-radio-button :label="2">仅追加</el-radio-button>
<el-radio-button :label="3">仅更新</el-radio-button>
</el-radio-group>
</el-form-item>
<el-form-item label="关键字段" v-if="[1, 3].includes(formData.method)">
<el-select
v-model="formData.field"
clearable
filterable
placeholder="请选择关键字段"
style="width: 360px">
<el-option
v-for="item in relation"
:key="item.column_name"
:label="item.column_name"
:value="item.column_name">
</el-option>
</el-select>
</el-form-item>
<el-form-item label="可选插件">
<el-checkbox-group v-model="formData.plugins">
<el-checkbox
v-for="item in plugins"
:key="item.name"
:label="item.name"
border>
{{ item.description }}
</el-checkbox>
</el-checkbox-group>
</el-form-item>
<el-form-item label="Excel 文件" required>
<el-upload
ref="upload"
drag
action="#"
:auto-upload="false"
:limit="1"
:file-list="fileList"
:on-change="handleFileChange"
:on-remove="handleFileRemove">
<i class="el-icon-upload"></i>
<div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div>
<div slot="tip" class="el-upload__tip">仅支持 `xls`、`xlsx` 文件</div>
</el-upload>
</el-form-item>
</el-form>
</div>
</div>
<div class="wizard-panel" v-if="active === 4">
<div class="result-panel">
<el-progress type="circle" :percentage="resultPercentage" status="success"></el-progress>
<div style="margin-top: 18px; color: #606266;">
共 {{ result.total || 0 }} 条,成功 {{ result.success || 0 }} 条
</div>
<div v-if="result.cacheKey" style="margin-top: 10px;">
<el-link type="primary" :href="loc() + '/exportErrors?cacheKey=' + result.cacheKey">下载错误记录</el-link>
</div>
</div>
</div>
</div>
<div class="wizard-footer">
<el-button :disabled="active === 0 || loading" @click="prevStep">上一步</el-button>
<el-button v-if="active < 4" type="primary" :loading="loading" @click="nextStep">下一步</el-button>
<el-button v-else type="primary" @click="restart">重新开始</el-button>
</div>
</div>
</div>
</el-card>
</div>
<script nonce="${cspNonce!}">
new Vue({
el: "#app",
data() {
return {
active: 0,
loading: false,
tableKeyword: "",
columnKeyword: "",
tables: [],
filteredTables: [],
columns: [],
filteredColumns: [],
relation: [],
plugins: [],
fileList: [],
result: {
total: 0,
success: 0,
cacheKey: ""
},
formData: {
tableName: "",
columns: [],
method: 1,
field: "",
plugins: []
}
}
},
computed: {
resultPercentage() {
if (!this.result.total) {
return 0
}
return Number(((this.result.success / this.result.total) * 100).toFixed(2))
}
},
methods: {
normalizeRecord(item) {
const normalized = {}
Object.keys(item || {}).forEach(key => {
normalized[(key || "").toLowerCase()] = item[key]
})
return normalized
},
async loadTables() {
const resp = await this.$axios.post(loc() + "/tableNames")
if (resp.code === 0) {
this.tables = Array.isArray(resp.data)
? resp.data.map(item => this.normalizeRecord(item))
: []
this.filteredTables = this.tables.slice()
}
},
async loadPlugins() {
this.plugins = await this.$businessTool.getEnumOptions("SysDataImportPlugin")
},
async loadColumns() {
const resp = await this.$axios.post(loc() + "/columns", {
tableName: this.formData.tableName
})
if (resp.code === 0) {
this.columns = Array.isArray(resp.data)
? resp.data.map(item => this.normalizeRecord(item))
: []
this.filteredColumns = this.columns.slice()
}
},
filterTables() {
const keyword = (this.tableKeyword || "").toLowerCase()
if (!keyword) {
this.filteredTables = this.tables.slice()
return
}
this.filteredTables = this.tables.filter(item => {
const tableName = (item.table_name || "").toLowerCase()
const tableComment = (item.table_comment || "").toLowerCase()
return tableName.includes(keyword) || tableComment.includes(keyword)
})
},
filterColumns() {
const keyword = (this.columnKeyword || "").toLowerCase()
if (!keyword) {
this.filteredColumns = this.columns.slice()
return
}
this.filteredColumns = this.columns.filter(item => {
const columnName = (item.column_name || "").toLowerCase()
const columnComment = (item.column_comment || "").toLowerCase()
return columnName.includes(keyword) || columnComment.includes(keyword)
})
},
buildRelation() {
this.relation = this.columns
.filter(item => this.formData.columns.includes(item.column_name))
.map(item => Object.assign({}, item, { relation: "" }))
},
async nextStep() {
if (this.active === 0) {
if (!this.formData.tableName) {
this.$message.warning("请选择数据表")
return
}
await this.loadColumns()
this.formData.columns = []
this.formData.field = ""
} else if (this.active === 1) {
if (!this.formData.columns.length) {
this.$message.warning("请至少选择一个字段")
return
}
this.buildRelation()
} else if (this.active === 2) {
const hasEmptyRelation = this.relation.some(item => !item.relation)
if (hasEmptyRelation) {
this.$message.warning("请填写所有 Excel 表头对应关系")
return
}
} else if (this.active === 3) {
const success = await this.submitImport()
if (!success) {
return
}
}
this.active += 1
},
prevStep() {
if (this.active > 0) {
this.active -= 1
}
},
handleFileRemove(file, fileList) {
this.fileList = fileList
},
handleFileChange(file, fileList) {
const extension = ((file.name || "").split(".").pop() || "").toLowerCase()
const removeIndex = fileList.findIndex(item => item.uid === file.uid)
if (!["xls", "xlsx"].includes(extension)) {
this.$message.warning("仅支持 xls、xlsx 文件")
if (removeIndex > -1) {
fileList.splice(removeIndex, 1)
}
} else if (file.size === 0) {
this.$message.warning("上传文件不能为空")
if (removeIndex > -1) {
fileList.splice(removeIndex, 1)
}
}
this.fileList = fileList
},
async submitImport() {
if ([1, 3].includes(this.formData.method) && !this.formData.field) {
this.$message.warning("请选择关键字段")
return false
}
if (!this.fileList.length) {
this.$message.warning("请上传 Excel 文件")
return false
}
const formData = new FormData()
formData.append("tableName", this.formData.tableName)
formData.append("method", this.formData.method)
formData.append("field", this.formData.field || "")
formData.append("relation", JSON.stringify(this.relation))
formData.append("plugins", JSON.stringify(this.formData.plugins || []))
formData.append("file", this.fileList[0].raw, this.fileList[0].name)
this.loading = true
try {
const resp = await this.$axios.post(loc() + "/importData", formData, {
headers: {
"Content-Type": "multipart/form-data"
}
})
if (resp.code !== 0) {
this.$message.error(resp.msg)
return false
}
this.result = resp.data || { total: 0, success: 0, cacheKey: "" }
this.$message.success(resp.msg)
return true
} finally {
this.loading = false
}
},
restart() {
this.active = 0
this.tableKeyword = ""
this.columnKeyword = ""
this.columns = []
this.filteredColumns = []
this.relation = []
this.fileList = []
this.result = {
total: 0,
success: 0,
cacheKey: ""
}
this.formData = {
tableName: "",
columns: [],
method: 1,
field: "",
plugins: []
}
if (this.$refs.upload) {
this.$refs.upload.clearFiles()
}
this.filterTables()
}
},
async created() {
await this.loadTables()
await this.loadPlugins()
}
})
</script>
<!--#
}
#-->
@@ -65,6 +65,19 @@ layout("/layouts/platform.html"){
row-key="id"
>
<el-table-column align="left" header-align="left" label="菜单名称" prop="name" width="200"></el-table-column>
<el-table-column align="center" header-align="center" label="排序编码" prop="location" width="120">
<template slot-scope="{row}">
<el-input-number
v-model="row.location"
:controls="false"
:min="0"
:precision="0"
size="mini"
style="width: 88px"
@change="updateLocation(row)"
></el-input-number>
</template>
</el-table-column>
<el-table-column align="center" header-align="center" label="菜单图标" prop="icon" width="100">
<template slot-scope="scope">
<template v-if="pageForm.platform==='PC'">
@@ -325,6 +338,26 @@ layout("/layouts/platform.html"){
this.moduleOptions = res.data
}
})
},
updateLocation(row) {
const location = Number(row.location)
if (!Number.isInteger(location) || location < 0) {
this.$message.error("排序编码必须是非负整数")
return
}
$.post("/platform/sys/menu/updateLocation", {
id: row.id,
location
}).then((res) => {
if (res.code === 0) {
this.$message.success(res.msg)
if (row.parentId) {
this.loadChildByExpandedKeys()
} else {
this.initTreeTable()
}
}
})
}
},
mounted() {
@@ -20,6 +20,24 @@ const branchUnionUserManage = {
<el-option label="工号" value="loginname"></el-option>
</el-select>
</el-input>
<el-select
v-model="pageForm.j"
placeholder="届数"
size="small"
clearable
class="j-search-select"
@change="handleJChange"
>
<el-option
v-for="item in jOptions"
:key="item.code"
:label="item.name"
:value="item.code"
>
<span>{{ item.name }}</span>
<span v-if="hasCadreData(item.code)" class="j-used-mark">*</span>
</el-option>
</el-select>
<el-button type="primary" size="small" icon="el-icon-search" @click="doSearch"></el-button>
<el-button type="primary" size="small" icon="el-icon-plus" style="margin-left: auto"
@click="openAdd" v-if="$auth.hasPermission('sys.manager.union.branchOfficer')">添加
@@ -31,11 +49,23 @@ const branchUnionUserManage = {
<el-table-column prop="loginName" label="工号"></el-table-column>
<el-table-column prop="userName" label="姓名"></el-table-column>
<el-table-column prop="mobile" label="联系方式"></el-table-column>
<el-table-column label="届数">
<template scope="{row}">
{{ row.jName || row.j }}
</template>
</el-table-column>
<el-table-column prop="roleName" label="职务"></el-table-column>
<el-table-column prop="taskName" label="当前状态"></el-table-column>
<el-table-column label="操作" width="100px"
<el-table-column prop="displayStatus" label="当前状态"></el-table-column>
<el-table-column label="操作" width="150px"
v-if="$auth.hasPermission('sys.manager.union.branchOfficer')">
<template scope="scope">
<el-button
v-if="!isLeft(scope.row)"
size="mini"
type="warning"
icon="el-icon-switch-button"
@click="openLeave(scope.row)"
></el-button>
<el-button size="mini" type="danger" icon="el-icon-delete"
@click="doDelete(scope.row)"></el-button>
</template>
@@ -47,6 +77,9 @@ const branchUnionUserManage = {
<el-form-item prop="roleCode" label="角色">
<dict-select v-model="formData.roleCode" code="BRANCH_UNION_ROLES" placeholder="请选择角色"></dict-select>
</el-form-item>
<el-form-item prop="j" label="届数">
<dict-select v-model="formData.j" code="TEACHER_CONGRESS_J" placeholder="请选择届数"></dict-select>
</el-form-item>
<el-form-item prop="userId" label="人员">
<user-select
v-model="formData.userId"
@@ -66,6 +99,23 @@ const branchUnionUserManage = {
<el-button type="primary" @click="doSubmit">确 定</el-button>
</div>
</el-dialog>
<el-dialog title="离任" :visible.sync="leaveDialogVisible" width="420px" :close-on-click-modal="false">
<el-form :model="leaveForm" ref="leaveForm" size="small" label-width="90px">
<el-form-item label="离任时间" required>
<el-date-picker
v-model="leaveForm.leaveDate"
type="date"
value-format="yyyy-MM-dd"
placeholder="请选择离任时间"
style="width: 100%"
></el-date-picker>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="leaveDialogVisible = false">取 消</el-button>
<el-button type="primary" @click="doLeave">确 定</el-button>
</div>
</el-dialog>
</div>
`,
mixins: [initTableMixins],
@@ -78,32 +128,114 @@ const branchUnionUserManage = {
data() {
return {
pageForm: {
searchName: "username"
searchName: "username",
j: ""
},
dialogFormVisible: false,
formData: {}
formData: {},
leaveDialogVisible: false,
leaveForm: {
id: "",
leaveDate: ""
},
jOptions: [],
usedJCodes: []
}
},
watch: {
union_id(val) {
this.doSearch()
this.pageForm.j = ""
this.initJSearch()
}
},
methods: {
loadJOptions() {
return $.get("/open/common/dictOptions", { code: "TEACHER_CONGRESS_J" }).then((res) => {
this.jOptions = res.data || []
})
},
loadUsedJCodes() {
if (!this.union_id) {
this.usedJCodes = []
return $.Deferred().resolve().promise()
}
return $.get("/platform/sys/union/branchUnionUserUsedJData", {
unionId: this.union_id
}).then((res) => {
if (res.code === 0) {
this.usedJCodes = res.data || []
}
})
},
hasCadreData(code) {
return this.usedJCodes.includes(code)
},
getHighestUsedJ() {
if (!this.usedJCodes.length) {
return ""
}
for (let i = this.jOptions.length - 1; i >= 0; i--) {
if (this.usedJCodes.includes(this.jOptions[i].code)) {
return this.jOptions[i].code
}
}
return this.usedJCodes[0]
},
initJSearch() {
const optionTask = this.jOptions.length ? $.Deferred().resolve().promise() : this.loadJOptions()
$.when(optionTask, this.loadUsedJCodes()).then(() => {
this.pageForm.j = this.getHighestUsedJ()
this.pageData()
})
},
handleJChange(val) {
this.pageForm.j = val || ""
this.doSearch()
},
isLeft(row) {
return row.isServing === false || row.isServing === 0 || row.displayStatus === "离任"
},
openAdd() {
this.dialogFormVisible = true
this.$nextTick(() => {
this.formData = {}
})
},
openLeave(row) {
this.leaveForm = {
id: row.id,
leaveDate: ""
}
this.leaveDialogVisible = true
},
doSubmit() {
this.$axios.post("/platform/sys/union/insertBranchUnionUserRole", {
if (!this.formData.j) {
this.$message.error("请选择届数")
return
}
$.post("/platform/sys/union/insertBranchUnionUserRole", {
...this.formData,
unionId: this.union_id
}).then((res) => {
if (res.code === 0) {
this.dialogFormVisible = false
this.$message.success(res.msg)
this.loadUsedJCodes()
this.doSearch()
} else {
this.$message.error(res.msg)
}
})
},
doLeave() {
if (!this.leaveForm.leaveDate) {
this.$message.error("请选择离任时间")
return
}
$.post("/platform/sys/union/leaveBranchUnionUserRole", this.leaveForm).then((res) => {
if (res.code === 0) {
this.leaveDialogVisible = false
this.$message.success(res.msg)
this.doSearch()
} else {
this.$message.error(res.msg)
@@ -123,6 +255,7 @@ const branchUnionUserManage = {
}).then((res) => {
if (res.code === 0) {
this.$message.success(res.msg)
this.loadUsedJCodes()
this.doSearch()
}
})
@@ -131,7 +264,8 @@ const branchUnionUserManage = {
pageData() {
$.get("/platform/sys/union/branchUnionUserPageData", {
...this.pageForm,
unionId: this.union_id
unionId: this.union_id,
j: this.pageForm.j || ""
}).then((res) => {
if (res.code === 0) {
this.tableData = res.data
@@ -140,11 +274,19 @@ const branchUnionUserManage = {
}
},
created() {
this.pageData()
this.initJSearch()
},
style: /*language=CSS*/ `
/deep/ .el-select {
width: 100% !important;
}
/deep/ .j-search-select {
width: 80px !important;
}
.j-used-mark {
color: #f56c6c;
margin-left: 4px;
font-weight: 700;
}
`
}
@@ -3,6 +3,13 @@ layout("/layouts/platform.html"){
#-->
<style>
.page-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 16px;
}
.page-tip {
color: #c64120;
line-height: 24px;
@@ -11,9 +18,14 @@ layout("/layouts/platform.html"){
<div id="app" v-cloak>
<el-card shadow="never">
<div class="left-span-label">活动类型年度报名次数配置</div>
<div class="page-tip mt10">
注:这里配置的是“同一活动类型在同一年度内最多可报名多少次”。未单独配置过的活动类型默认按 2 次处理。
<div class="page-header">
<div>
<div class="left-span-label">活动分类年度报名次数配置</div>
<div class="page-tip mt10">
注:这里配置的是“同一活动分类在同一年度内最多可报名多少次”。未单独配置过的活动分类默认按 2 次处理。
</div>
</div>
<el-button plain icon="el-icon-back" @click="goBack">返回</el-button>
</div>
</el-card>
@@ -22,8 +34,8 @@ layout("/layouts/platform.html"){
<el-form ref="formRef" :model="formData">
<el-table :data="formData.configList">
<el-table-column label="序号" type="index" width="80"></el-table-column>
<el-table-column label="活动类" prop="trainTypeName"></el-table-column>
<el-table-column label="活动类编码" prop="trainType"></el-table-column>
<el-table-column label="活动类" prop="trainTypeName"></el-table-column>
<el-table-column label="活动类编码" prop="trainType"></el-table-column>
<el-table-column label="每年可报名次数" prop="annualLimitCount" width="220">
<template v-slot="{row}">
<el-input-number v-model="row.annualLimitCount" :min="1" :precision="0" :step="1"></el-input-number>
@@ -33,7 +45,8 @@ layout("/layouts/platform.html"){
</el-form>
<div style="margin-top: 20px; text-align: right">
<el-button type="primary" @click="onSave">保 存</el-button>
<el-button @click="goBack">返回</el-button>
<el-button type="primary" @click="onSave">保存</el-button>
</div>
</el-card>
</div>
@@ -49,6 +62,9 @@ layout("/layouts/platform.html"){
}
},
methods: {
goBack: function () {
commonUtil.pjaxPush('/platform/trainSignUp/type')
},
fetchData: function () {
this.$axios.post('/platform/trainSignUp/annualLimit/listData').then((res) => {
if (res.code === 0) {
@@ -63,10 +79,10 @@ layout("/layouts/platform.html"){
return !item.annualLimitCount || item.annualLimitCount <= 0
})
if (hasInvalidData) {
this.$message.warning('每年可报名次数必须大于0')
this.$message.warning('每年可报名次数必须大于 0')
return
}
// 页面直接按活动类整表提交,后端会按活动类编码进行新增或更新。
// 页面直接按活动类整表提交,后端会按活动类编码进行新增或更新。
this.$axios.post('/platform/trainSignUp/annualLimit/save', {
configList: JSON.stringify(this.formData.configList)
}).then((res) => {
@@ -2,9 +2,9 @@ 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>】活动场地
<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>
@@ -28,26 +28,33 @@ const apply = {
</el-col>
<el-col :span="12">
<el-form-item label="预约类型" prop="reserveType">
<el-radio-group v-model="formData.reserveType" @change="reserveTypeChange">
<el-radio-group v-if="availableReserveTypes.length" v-model="formData.reserveType" @change="reserveTypeChange" style="display: none;">
<el-radio-button label="union">分工会预约</el-radio-button>
<el-radio-button label="club">协会预约</el-radio-button>
</el-radio-group>
<el-radio-group v-if="availableReserveTypes.length" v-model="formData.reserveType" @change="reserveTypeChange">
<el-radio-button
v-for="item in availableReserveTypeOptions"
:key="item.value"
:label="item.value">
{{ item.label }}
</el-radio-button>
</el-radio-group>
<span v-else style="color: #909399;">当前无可用的预约权限</span>
</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-col :span="12" v-if="formData.reserveType === 'union'">
<el-form-item label="分工会" prop="applyUnionName">
<el-input readonly v-model="formData.applyUnionName" placeholder="自动读取当前登录人的分工会"></el-input>
</el-form-item>
</el-col>
<el-col :span="12">
<el-col :span="12" v-if="formData.reserveType === 'club'">
<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">
style="width: 100%;" @change="clubChange">
<el-option
v-for="item in clubOptions"
:key="item.id"
@@ -62,6 +69,81 @@ const apply = {
<el-input maxlength="11" placeholder="请输入联系电话" v-model="formData.applyMobile"></el-input>
</el-form-item>
</el-col>
<el-col :span="24">
<div class="apply-visual-panel">
<div class="apply-visual-panel__toolbar">
<div>
<div class="apply-visual-panel__title">可预约时段</div>
<div class="apply-visual-panel__subtitle">{{ scheduleRuleText }}</div>
</div>
<div class="apply-visual-panel__actions">
<el-tag size="small" type="success">{{ reserveModeText }}</el-tag>
<el-date-picker
v-model="scheduleDate"
type="date"
value-format="yyyy-MM-dd"
placeholder="请选择预约日期"
:picker-options="{ disabledDate: isDisabledDate }"
@change="onScheduleDateChange">
</el-date-picker>
</div>
</div>
<div class="apply-visual-panel__legend">
<span class="apply-legend__item"><i class="apply-legend__dot apply-legend__dot--available"></i>未预约</span>
<span class="apply-legend__item"><i class="apply-legend__dot apply-legend__dot--reserved"></i>已预约</span>
<span class="apply-legend__item"><i class="apply-legend__dot apply-legend__dot--selected"></i>已选择</span>
<span class="apply-legend__item"><i class="apply-legend__dot apply-legend__dot--closed"></i>不可预约</span>
</div>
<el-skeleton :loading="availabilityLoading" animated>
<template slot="template">
<div style="padding-top: 8px;">
<el-skeleton-item variant="p" style="width: 100%; height: 80px;"></el-skeleton-item>
</div>
</template>
<template>
<el-empty v-if="!availabilityBlocks.length" description="当前日期暂无可选时段"></el-empty>
<div v-else-if="isSegmentedMode">
<div v-for="group in segmentedBlockGroups" :key="group.groupIndex" class="apply-block-group">
<div class="apply-block-group__title">{{ group.groupLabel }}</div>
<div class="apply-block-grid">
<button
v-for="block in group.blocks"
:key="block.key"
type="button"
:class="getBlockClass(block)"
@click="onAvailabilityBlockClick(block)">
<span>{{ block.label }}</span>
</button>
</div>
</div>
</div>
<div v-else class="apply-block-group">
<div class="apply-block-group__title">全天候时段</div>
<div class="apply-block-grid">
<button
v-for="block in availabilityBlocks"
:key="block.key"
type="button"
:class="getBlockClass(block)"
@click="onAvailabilityBlockClick(block)">
<span>{{ block.label }}</span>
</button>
</div>
</div>
</template>
</el-skeleton>
<div class="apply-visual-panel__summary" v-if="selectionSummary">
当前选择:{{ selectionSummary }}
</div>
</div>
</el-col>
<el-col :span="12">
<el-form-item label="预约开始时间" prop="reserveStartTime">
<el-date-picker
@@ -71,7 +153,7 @@ const apply = {
value-format="yyyy-MM-dd HH:mm:ss"
placeholder="请选择预约开始时间"
:picker-options="startPickerOptions"
style="width: 100%"
style="width: 100%;"
@change="timeFieldChange('reserveStartTime')">
</el-date-picker>
</el-form-item>
@@ -85,21 +167,30 @@ const apply = {
value-format="yyyy-MM-dd HH:mm:ss"
placeholder="请选择预约结束时间"
:picker-options="endPickerOptions"
style="width: 100%"
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;">
<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;">
<span style="color: #909399;">
例如先选某个周一 09:00-11:00,勾选后会自动预约本年剩余所有周一的这个时段。
</span>
</div>
<div v-if="yearlyReserve && yearlyReserveSummary" style="margin-top:8px;color:#E6A23C;">
{{ yearlyReserveSummary }}
<div v-if="yearlyReserve" style="margin-top: 10px; max-width: 320px;">
<el-date-picker
v-model="formData.yearlyReserveEndDate"
type="date"
value-format="yyyy-MM-dd"
placeholder="请选择批量预约截止日期"
:picker-options="yearlyReserveEndDatePickerOptions">
</el-date-picker>
</div>
<div v-if="yearlyReserve && batchReserveSummary" style="margin-top: 8px; color: #E6A23C;">
{{ batchReserveSummary }}
</div>
</el-form-item>
</el-col>
@@ -124,20 +215,30 @@ const apply = {
row: {},
clubOptions: [],
yearlyReserve: false,
scheduleDate: '',
availabilityLoading: false,
selectedBlockKeys: [],
timeLimitConfig: {
filterHolidays: false,
holidayList: [],
notApplyTimeList: [],
},
availabilityData: {
date: '',
reserveTimeType: 1,
reserveTimeTypeName: '分段预约',
blocks: [],
},
pickerRefreshKey: 0,
formData: {
reserveType: 'union',
reserveType: '',
applyUnionId: '',
applyUnionName: '',
clubId: '',
clubName: '',
reserveStartTime: '',
reserveEndTime: '',
yearlyReserveEndDate: '',
joinCount: 1,
applyCause: '',
},
@@ -152,17 +253,77 @@ const apply = {
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']}],
clubId: [{
validator: (rule, value, callback) => {
if (this.formData.reserveType === 'club' && !value) {
callback(new Error('请选择您管理的协会'))
return
}
callback()
},
trigger: ['blur', 'change']
}],
},
}
},
computed: {
canApplyUnion() {
return this.$auth.hasPermission('siteCug.apply.union')
},
canApplyClub() {
return this.$auth.hasPermission('siteCug.apply.club')
},
availableReserveTypes() {
const result = []
if (this.canApplyUnion) {
result.push('union')
}
if (this.canApplyClub) {
result.push('club')
}
return result
},
availableReserveTypeOptions() {
const result = []
if (this.canApplyUnion) {
result.push({value: 'union', label: '分工会预约'})
}
if (this.canApplyClub) {
result.push({value: 'club', label: '协会预约'})
}
return result
},
availabilityBlocks() {
return Array.isArray(this.availabilityData.blocks) ? this.availabilityData.blocks : []
},
isSegmentedMode() {
return Number(this.availabilityData.reserveTimeType || this.row.reserveTimeType || 1) === 1
},
reserveModeText() {
return this.availabilityData.reserveTimeTypeName || (this.isSegmentedMode ? '分段预约' : '全天候预约')
},
scheduleRuleText() {
if (this.isSegmentedMode) {
return '分段预约场地需按场次时间单位选择一个或多个连续时段,不能跨间隔跳选。'
}
return '全天候预约场地需按预约时间单位选择一个或多个连续时段,开始和结束时间会自动回填。'
},
segmentedBlockGroups() {
const groups = []
this.availabilityBlocks.forEach(block => {
let target = groups.find(item => item.groupIndex === block.groupIndex)
if (!target) {
target = {
groupIndex: block.groupIndex,
groupLabel: block.groupLabel,
blocks: [],
}
groups.push(target)
}
target.blocks.push(block)
})
return groups
},
startPickerOptions() {
return {
disabledDate: (time) => this.isDisabledDate(time),
@@ -175,27 +336,38 @@ const apply = {
selectableRange: this.buildSelectableRange(this.formData.reserveEndTime),
}
},
yearlyReserveEndDatePickerOptions() {
return {
disabledDate: (time) => {
if (!this.formData.reserveStartTime) {
return false
}
return this.$moment(time).format('YYYY-MM-DD') < this.formData.reserveStartTime.slice(0, 10)
},
}
},
yearlyReserveDates() {
if (!this.yearlyReserve || !this.formData.reserveStartTime || !this.formData.reserveEndTime) {
if (!this.yearlyReserve || !this.formData.reserveStartTime || !this.formData.reserveEndTime || !this.formData.yearlyReserveEndDate) {
return []
}
const start = this.$moment(this.formData.reserveStartTime)
const end = this.$moment(this.formData.reserveEndTime)
if (!start.isValid() || !end.isValid() || !end.isAfter(start)) {
const reserveEndDate = this.$moment(this.formData.yearlyReserveEndDate, 'YYYY-MM-DD')
if (!start.isValid() || !end.isValid() || !end.isAfter(start) || !reserveEndDate.isValid() || reserveEndDate.isBefore(start, 'day')) {
return []
}
const result = []
const current = start.clone().startOf('day')
const endOfYear = start.clone().endOf('year').startOf('day')
const reserveEndDay = reserveEndDate.clone().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')) {
while (current.isSameOrBefore(reserveEndDay, '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)) {
if (this.validateTimeLimit(false, startTime, endTime) && this.validateGraphSelection(false, startTime, endTime, day)) {
result.push(day)
}
}
@@ -203,6 +375,21 @@ const apply = {
}
return result
},
batchReserveSummary() {
if (!this.yearlyReserve) {
return ''
}
if (!this.formData.yearlyReserveEndDate) {
return '请选择批量预约截止日期'
}
if (!this.formData.reserveStartTime || !this.formData.reserveEndTime) {
return '请先选择开始和结束时间,再批量预约截止日期内的同星期时段'
}
if (!this.yearlyReserveDates.length) {
return '当前时间段在截止日期内无法生成可批量预约的日期'
}
return '将从 ' + this.yearlyReserveDates[0] + ' 开始,自动预约到 ' + this.formData.yearlyReserveEndDate + ',共 ' + this.yearlyReserveDates.length + ' 个时段'
},
yearlyReserveSummary() {
if (!this.yearlyReserve) {
return ''
@@ -215,13 +402,39 @@ const apply = {
}
return '将从 ' + this.yearlyReserveDates[0] + ' 开始,自动预约到 ' + this.yearlyReserveDates[this.yearlyReserveDates.length - 1] + ',共 ' + this.yearlyReserveDates.length + ' 个时段'
},
batchReserveSummary() {
if (!this.yearlyReserve) {
return ''
}
if (!this.formData.yearlyReserveEndDate) {
return '请选择批量预约截止日期'
}
if (!this.formData.reserveStartTime || !this.formData.reserveEndTime) {
return '请先选择开始和结束时间,再设置批量预约截止日期'
}
if (!this.yearlyReserveDates.length) {
return '当前截止日期内没有可批量预约的同星期时段'
}
return '将从 ' + this.formData.reserveStartTime.slice(0, 10) + ' 开始,自动预约到 ' + this.formData.yearlyReserveEndDate
},
selectionSummary() {
if (!this.formData.reserveStartTime || !this.formData.reserveEndTime) {
return ''
}
return this.formData.reserveStartTime + ' 至 ' + this.formData.reserveEndTime
},
},
watch: {
yearlyReserve(val) {
if (val) {
this.syncYearlyReserveEndDate(true)
}
},
},
methods: {
async onOpen(row) {
this.row = row
this.clubOptions = []
this.yearlyReserve = false
this.formData = {
createDefaultFormData(row) {
const reserveType = this.getDefaultReserveType()
return {
siteId: row.id,
applyUserId: this.$store.state.user.id,
applyUserName: this.$store.state.user.username,
@@ -229,18 +442,71 @@ const apply = {
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,
reserveType,
applyUnionId: reserveType === 'union' ? (this.$store.state.user?.union?.id || '') : '',
applyUnionName: reserveType === 'union' ? (this.$store.state.user?.union?.name || '') : '',
clubId: '',
clubName: '',
applyMobile: this.$store.state.user.mobile,
reserveStartTime: '',
reserveEndTime: '',
yearlyReserveEndDate: '',
joinCount: 1,
applyCause: '',
}
},
getDefaultReserveType() {
return this.availableReserveTypes[0] || ''
},
async onOpen(row) {
this.row = row || {}
this.clubOptions = []
this.yearlyReserve = false
this.selectedBlockKeys = []
this.scheduleDate = ''
this.availabilityData = {
date: '',
reserveTimeType: Number(row.reserveTimeType || 1),
reserveTimeTypeName: row.reserveTimeTypeName || (Number(row.reserveTimeType || 1) === 2 ? '全天候预约' : '分段预约'),
blocks: [],
}
this.formData = this.createDefaultFormData(row || {})
if (this.formData.reserveType) {
await this.reserveTypeChange(this.formData.reserveType)
}
this.syncYearlyReserveEndDate(true)
await this.queryTimeLimitConfig()
this.scheduleDate = this.getInitialScheduleDate()
await this.queryAvailability()
},
getDefaultYearlyReserveEndDate() {
const base = this.formData.reserveStartTime
? this.$moment(this.formData.reserveStartTime)
: (this.scheduleDate ? this.$moment(this.scheduleDate, 'YYYY-MM-DD') : this.$moment())
if (!base.isValid()) {
return this.$moment().endOf('month').format('YYYY-MM-DD')
}
return base.clone().endOf('month').format('YYYY-MM-DD')
},
syncYearlyReserveEndDate(force = false) {
const defaultDate = this.getDefaultYearlyReserveEndDate()
if (force || !this.formData.yearlyReserveEndDate) {
this.$set(this.formData, 'yearlyReserveEndDate', defaultDate)
return
}
if (this.formData.reserveStartTime && this.formData.yearlyReserveEndDate < this.formData.reserveStartTime.slice(0, 10)) {
this.$set(this.formData, 'yearlyReserveEndDate', defaultDate)
}
},
getInitialScheduleDate() {
let current = this.$moment().startOf('day')
for (let i = 0; i < 90; i++) {
if (!this.isDisabledDate(current.toDate())) {
return current.format('YYYY-MM-DD')
}
current = current.add(1, 'day')
}
return this.$moment().add(1, 'day').format('YYYY-MM-DD')
},
async queryTimeLimitConfig() {
const fallback = {
@@ -266,7 +532,57 @@ const apply = {
}
this.pickerRefreshKey += 1
},
async queryAvailability(date) {
const targetDate = date || this.scheduleDate
if (!targetDate || !this.row.id) {
this.availabilityData.blocks = []
return
}
this.availabilityLoading = true
try {
const res = await this.$axios.post('/platform/siteCug/apply/availability', {
siteId: this.row.id,
date: targetDate,
})
if (res.code === 0 && res.data) {
this.availabilityData = {
date: res.data.date || targetDate,
reserveTimeType: Number(res.data.reserveTimeType || this.row.reserveTimeType || 1),
reserveTimeTypeName: res.data.reserveTimeTypeName || this.reserveModeText,
blocks: Array.isArray(res.data.blocks) ? res.data.blocks : [],
}
} else {
this.availabilityData = {
date: targetDate,
reserveTimeType: Number(this.row.reserveTimeType || 1),
reserveTimeTypeName: Number(this.row.reserveTimeType || 1) === 2 ? '全天候预约' : '分段预约',
blocks: [],
}
this.$message.warning((res && res.msg) || '预约时段加载失败')
}
} catch (e) {
this.availabilityData = {
date: targetDate,
reserveTimeType: Number(this.row.reserveTimeType || 1),
reserveTimeTypeName: Number(this.row.reserveTimeType || 1) === 2 ? '全天候预约' : '分段预约',
blocks: [],
}
this.$message.warning('预约时段加载失败,请稍后重试')
} finally {
this.availabilityLoading = false
this.syncSelectionFromFields()
}
},
async onScheduleDateChange(value) {
this.scheduleDate = value || ''
this.clearSelection(true)
await this.queryAvailability(value)
},
async reserveTypeChange(value) {
if (!this.availableReserveTypes.includes(value)) {
this.formData.reserveType = this.getDefaultReserveType()
return
}
if (value === 'union') {
this.formData.applyUnionId = this.$store.state.user?.union?.id || ''
this.formData.applyUnionName = this.$store.state.user?.union?.name || ''
@@ -288,19 +604,44 @@ const apply = {
try {
const res = await this.$axios.post('/platform/club/examine/apply/getClubsByRole')
this.clubOptions = Array.isArray(res.data) ? res.data : []
this.syncManagedClubSelection()
if (!this.clubOptions.length) {
this.$message.warning('您当前没有可预约的协会管理权限')
}
} catch (e) {
this.clubOptions = []
this.formData.clubId = ''
this.formData.clubName = ''
this.$message.warning('协会列表加载失败,请稍后重试')
}
},
syncManagedClubSelection() {
if (!this.clubOptions.length) {
this.formData.clubId = ''
this.formData.clubName = ''
return
}
const currentClub = this.clubOptions.find(item => item.id === this.formData.clubId)
if (currentClub) {
this.formData.clubName = currentClub.clubName || ''
return
}
if (this.clubOptions.length === 1) {
this.formData.clubId = this.clubOptions[0].id
this.formData.clubName = this.clubOptions[0].clubName || ''
return
}
this.formData.clubId = ''
this.formData.clubName = ''
},
clubChange(clubId) {
const club = this.clubOptions.find(item => item.id === clubId)
this.formData.clubName = club ? club.clubName : ''
},
async normalizeReserveTypeData() {
if (!this.availableReserveTypes.includes(this.formData.reserveType)) {
this.formData.reserveType = this.getDefaultReserveType()
}
if (this.formData.reserveType === 'union') {
this.formData.applyUnionId = this.$store.state.user?.union?.id || ''
this.formData.applyUnionName = this.$store.state.user?.union?.name || ''
@@ -417,6 +758,15 @@ const apply = {
}
const start = this.$moment(startTime)
const end = this.$moment(endTime)
if (!start.isValid() || !end.isValid()) {
return false
}
if (start.format('YYYY-MM-DD') !== end.format('YYYY-MM-DD')) {
if (showMessage) {
this.$message.warning('预约时间必须在同一天内')
}
return false
}
if (this.isDateTimeBlocked(startTime) || this.isDateTimeBlocked(endTime)) {
if (showMessage) {
this.$message.warning('预约时间不能选择周末、节假日或禁用时间')
@@ -437,9 +787,173 @@ const apply = {
}
return true
},
timeFieldChange(field) {
clearSelection(clearTimeFields = false) {
this.selectedBlockKeys = []
if (clearTimeFields) {
this.formData.reserveStartTime = ''
this.formData.reserveEndTime = ''
}
},
getSelectedIndices() {
return this.availabilityBlocks
.map((block, index) => this.selectedBlockKeys.includes(block.key) ? index : -1)
.filter(index => index > -1)
},
getBlockClass(block) {
return {
'apply-block': true,
'apply-block--available': block.status === 'available' && !this.selectedBlockKeys.includes(block.key),
'apply-block--reserved': block.status === 'reserved',
'apply-block--closed': block.status === 'closed',
'apply-block--selected': this.selectedBlockKeys.includes(block.key),
}
},
onAvailabilityBlockClick(block) {
if (!block || !block.key) {
return
}
if (block.status !== 'available' && !this.selectedBlockKeys.includes(block.key)) {
this.$message.warning(block.status === 'reserved' ? '该时段已被预约,请选择其他时段' : '该时段当前不可预约')
return
}
const index = this.availabilityBlocks.findIndex(item => item.key === block.key)
if (index < 0) {
return
}
this.toggleContinuousSelection(index)
},
toggleContinuousSelection(index) {
const selectedIndices = this.getSelectedIndices()
if (!selectedIndices.length) {
this.applySelectionRange(index, index)
return
}
const selectedStart = selectedIndices[0]
const selectedEnd = selectedIndices[selectedIndices.length - 1]
if (selectedIndices.includes(index)) {
if (selectedIndices.length === 1) {
this.clearSelection(true)
return
}
if (index === selectedStart) {
this.applySelectionRange(selectedStart + 1, selectedEnd)
return
}
if (index === selectedEnd) {
this.applySelectionRange(selectedStart, selectedEnd - 1)
return
}
this.applySelectionRange(index, index)
return
}
const nextStart = Math.min(index, selectedStart)
const nextEnd = Math.max(index, selectedEnd)
if (!this.canSelectRange(nextStart, nextEnd)) {
this.$message.warning(this.isSegmentedMode ? '分段预约只能连续选择一个或多个时间段,不能跨间隔跳选' : '全天候预约只能连续选择一个时间段')
return
}
this.applySelectionRange(nextStart, nextEnd)
},
canSelectRange(startIndex, endIndex) {
if (startIndex < 0 || endIndex >= this.availabilityBlocks.length || startIndex > endIndex) {
return false
}
for (let i = startIndex; i <= endIndex; i++) {
const block = this.availabilityBlocks[i]
if (!block || block.status !== 'available') {
return false
}
if (i > startIndex) {
const prev = this.availabilityBlocks[i - 1]
if (!prev || prev.endDateTime !== block.startDateTime) {
return false
}
}
}
return true
},
applySelectionRange(startIndex, endIndex) {
const selected = this.availabilityBlocks.slice(startIndex, endIndex + 1)
this.selectedBlockKeys = selected.map(item => item.key)
if (selected.length) {
this.formData.reserveStartTime = selected[0].startDateTime
this.formData.reserveEndTime = selected[selected.length - 1].endDateTime
this.scheduleDate = selected[0].startDateTime.slice(0, 10)
}
},
findRangeIndicesByFields(startTime, endTime) {
if (!startTime || !endTime) {
return null
}
for (let i = 0; i < this.availabilityBlocks.length; i++) {
const block = this.availabilityBlocks[i]
if (block.status !== 'available' || block.startDateTime !== startTime) {
continue
}
let currentEnd = block.endDateTime
if (currentEnd === endTime) {
return [i, i]
}
for (let j = i + 1; j < this.availabilityBlocks.length; j++) {
const prev = this.availabilityBlocks[j - 1]
const next = this.availabilityBlocks[j]
if (next.status !== 'available' || prev.endDateTime !== next.startDateTime) {
break
}
currentEnd = next.endDateTime
if (currentEnd === endTime) {
return [i, j]
}
}
}
return null
},
syncSelectionFromFields() {
const startTime = this.formData.reserveStartTime
const endTime = this.formData.reserveEndTime
if (!startTime || !endTime) {
this.selectedBlockKeys = []
return
}
if (startTime.slice(0, 10) !== this.scheduleDate || endTime.slice(0, 10) !== this.scheduleDate) {
this.selectedBlockKeys = []
return
}
const range = this.findRangeIndicesByFields(startTime, endTime)
if (!range) {
this.selectedBlockKeys = []
return
}
this.selectedBlockKeys = this.availabilityBlocks.slice(range[0], range[1] + 1).map(item => item.key)
},
validateGraphSelection(showMessage = true, startTime = this.formData.reserveStartTime, endTime = this.formData.reserveEndTime, targetDay = this.scheduleDate) {
if (!startTime || !endTime) {
return true
}
if (!targetDay || startTime.slice(0, 10) !== targetDay || endTime.slice(0, 10) !== targetDay) {
if (showMessage) {
this.$message.warning('预约时间必须与当前选择的预约日期一致')
}
return false
}
const range = this.findRangeIndicesByFields(startTime, endTime)
if (range) {
return true
}
if (showMessage) {
this.$message.warning(this.isSegmentedMode
? '分段预约请按场次时间单位选择一个或多个连续时段'
: '全天候预约请按预约时间单位选择一个或多个连续时段')
}
return false
},
async timeFieldChange(field) {
const value = this.formData[field]
if (!value) {
if (field === 'reserveStartTime') {
this.syncYearlyReserveEndDate(true)
}
this.syncSelectionFromFields()
return
}
if (this.isDateTimeBlocked(value)) {
@@ -447,16 +961,38 @@ const apply = {
this.$set(this.formData, field, '')
return
}
const day = this.$moment(value).format('YYYY-MM-DD')
if (day !== this.scheduleDate) {
this.scheduleDate = day
await this.queryAvailability(day)
}
if (this.formData.reserveStartTime && this.formData.reserveEndTime && !this.validateTimeLimit()) {
this.$set(this.formData, field, '')
return
}
if (field === 'reserveStartTime') {
this.syncYearlyReserveEndDate()
}
this.syncSelectionFromFields()
},
validateYearlyReserve() {
if (!this.yearlyReserve) {
return true
}
if (!this.formData.yearlyReserveEndDate) {
this.$message.warning('请选择批量预约截止日期')
return false
}
if (this.formData.reserveStartTime && this.formData.yearlyReserveEndDate < this.formData.reserveStartTime.slice(0, 10)) {
this.$message.warning('批量预约截止日期不能早于开始日期')
return false
}
if (!this.yearlyReserveDates.length) {
this.$message.warning('?????????????????????')
this.$message.warning('当前选择的时间段在截止日期内无法生成可批量预约的日期')
return false
}
if (!this.yearlyReserveDates.length) {
this.$message.warning('当前选择的时间段无法生成本年批量预约日期')
return false
}
return true
@@ -465,10 +1001,58 @@ const apply = {
return this.yearlyReserve ? '/platform/siteCug/apply/submitYearly' : '/platform/siteCug/apply/submit'
},
buildSubmitConfirmMessage() {
const baseText = '您已选择 ' + this.formData.reserveStartTime + ' 至 ' + this.formData.reserveEndTime
if (!this.yearlyReserve) {
return '您确定提交'
return baseText + ',是否确定提交预约'
}
return '将一次性预约本年后续 ' + this.yearlyReserveDates.length + ' 个同星期时段,确认提交'
return baseText + '。系统将继续预约本年后续 ' + this.yearlyReserveDates.length + ' 个同星期时段,是否确定提交?'
},
validateYearlyReserve() {
if (!this.yearlyReserve) {
return true
}
if (!this.formData.yearlyReserveEndDate) {
this.$message.warning('请选择批量预约截止日期')
return false
}
if (this.formData.reserveStartTime && this.formData.yearlyReserveEndDate < this.formData.reserveStartTime.slice(0, 10)) {
this.$message.warning('批量预约截止日期不能早于开始日期')
return false
}
if (!this.yearlyReserveDates.length) {
this.$message.warning('当前选择的时间段在截止日期内无法生成可批量预约的日期')
return false
}
return true
},
buildSubmitConfirmMessage() {
const baseText = '您已选择 ' + this.formData.reserveStartTime + ' 至 ' + this.formData.reserveEndTime
if (!this.yearlyReserve) {
return baseText + ',是否确定提交预约?'
}
return baseText + '。系统将继续预约到 ' + this.formData.yearlyReserveEndDate + ',共 ' + this.yearlyReserveDates.length + ' 个同星期时段,是否确定提交?'
},
batchReserveSummary() {
if (!this.yearlyReserve) {
return ''
}
if (!this.formData.yearlyReserveEndDate) {
return '请选择批量预约截止日期'
}
if (!this.formData.reserveStartTime || !this.formData.reserveEndTime) {
return '请先选择开始和结束时间,再设置批量预约截止日期'
}
if (!this.yearlyReserveDates.length) {
return '当前截止日期内没有可批量预约的同星期时段'
}
return '将从 ' + this.yearlyReserveDates[0] + ' 开始,自动预约到 ' + this.formData.yearlyReserveEndDate
},
buildSubmitConfirmMessage() {
const baseText = '您已选择 ' + this.formData.reserveStartTime + ' 至 ' + this.formData.reserveEndTime
if (!this.yearlyReserve) {
return baseText + ',是否确定提交预约?'
}
return baseText + '。系统将继续预约到 ' + this.formData.yearlyReserveEndDate + ',是否确定提交?'
},
onSubmit() {
this.$refs.formRef.validate(async (valid) => {
@@ -503,6 +1087,9 @@ const apply = {
if (!this.validateTimeLimit()) {
return
}
if (!this.validateGraphSelection()) {
return
}
if (!this.validateYearlyReserve()) {
return
}
@@ -27,27 +27,71 @@ layout("/layouts/platform.html"){
<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>
<div v-if="tableData.length > 0" class="site-card-grid">
<div v-for="row in tableData" :key="row.id" class="site-card">
<div class="site-card__media">
<el-image
v-if="row.sitePhoto"
:preview-src-list="[row.sitePhoto]"
:src="row.sitePhoto"
fit="cover"
class="site-card__image">
</el-image>
<div v-else class="site-card__image site-card__image--placeholder">
<i class="el-icon-picture-outline"></i>
<span>暂无场地照片</span>
</div>
</div>
<div class="site-card__content">
<div class="site-card__header">
<div class="site-card__title-wrap">
<div class="site-card__title" :title="row.name">{{ row.name || '--' }}</div>
<div class="site-card__meta">
<i class="el-icon-location-outline"></i>
<span :title="row.address">{{ row.address || '暂无场地地址' }}</span>
</div>
</div>
<div class="site-card__actions">
<el-tag size="mini" effect="dark" type="success">{{ row.reserveTimeTypeName || '分段预约' }}</el-tag>
<el-button size="mini" plain @click="onViewSite(row)">场地详情</el-button>
<el-button size="mini" type="primary" @click="onApply(row)">预约</el-button>
</div>
</div>
<div class="site-card__summary">
<span>场地类型:{{ row.typeName || '--' }}</span>
<span>容纳人数:{{ row.maxNum || 0 }}人</span>
<span>联系人:{{ row.contactName || '--' }}</span>
</div>
<div class="site-card__timeline">
<div class="site-card__timeline-title">今日预约情况</div>
<div class="timeline-bar">
<span
v-for="(segment, index) in row.timelineSegments || []"
:key="row.id + '-segment-' + index"
:class="['timeline-bar__segment', 'timeline-bar__segment--' + (segment.status || 'closed')]">
</span>
</div>
<div class="timeline-scale">
<span v-for="hour in hourMarks" :key="row.id + '-hour-' + hour">{{ hour }}</span>
</div>
<div class="timeline-legend">
<span class="timeline-legend__item">
<i class="timeline-legend__dot timeline-legend__dot--available"></i>未预约
</span>
<span class="timeline-legend__item">
<i class="timeline-legend__dot timeline-legend__dot--reserved"></i>已预约
</span>
<span class="timeline-legend__item">
<i class="timeline-legend__dot timeline-legend__dot--closed"></i>未开放
</span>
</div>
</div>
</div>
</div>
</div>
<el-empty v-else description="暂无可预约场地"></el-empty>
<!--#include("/layouts/pagination.html"){}#-->
</el-card>
</template>
@@ -74,14 +118,7 @@ layout("/layouts/platform.html"){
data() {
return {
typeOptions: [],
tableColumns: [
{prop: 'name', label: '场地名称'},
{prop: 'address', label: '场地地址'},
{prop: 'contactName', label: '联系人'},
{prop: 'contactPhone', label: '联系电话'},
{prop: 'maxNum', label: '容纳人数'},
{prop: 'typeName', label: '场地类型'},
],
hourMarks: ['0', '2', '4', '6', '8', '10', '12', '14', '16', '18', '20', '22']
}
},
methods: {
@@ -120,6 +157,374 @@ layout("/layouts/platform.html"){
})
</script>
<style>
.site-card-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 20px;
}
.site-card {
display: flex;
min-height: 240px;
padding: 18px;
border: 1px solid #e8edf5;
border-radius: 16px;
background: linear-gradient(180deg, #ffffff 0%, #f8fbff 100%);
box-shadow: 0 10px 30px rgba(21, 66, 120, 0.06);
}
.site-card__media {
flex: 0 0 180px;
margin-right: 18px;
}
.site-card__image {
width: 180px;
height: 180px;
border-radius: 14px;
overflow: hidden;
}
.site-card__image--placeholder {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
color: #8c9bb1;
background: linear-gradient(135deg, #eef4fb 0%, #dde8f6 100%);
font-size: 14px;
gap: 10px;
}
.site-card__image--placeholder i {
font-size: 34px;
}
.site-card__content {
display: flex;
flex: 1;
flex-direction: column;
min-width: 0;
}
.site-card__header {
display: flex;
justify-content: space-between;
gap: 16px;
}
.site-card__title-wrap {
min-width: 0;
}
.site-card__title {
overflow: hidden;
color: #1f2d3d;
font-size: 22px;
font-weight: 700;
line-height: 1.3;
text-overflow: ellipsis;
white-space: nowrap;
}
.site-card__meta {
display: flex;
align-items: center;
margin-top: 10px;
color: #7f8ea3;
font-size: 14px;
gap: 6px;
}
.site-card__meta span {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.site-card__actions {
display: flex;
flex-wrap: wrap;
justify-content: flex-end;
align-items: center;
gap: 8px;
}
.site-card__summary {
display: flex;
flex-wrap: wrap;
margin-top: 16px;
color: #4f6277;
font-size: 13px;
gap: 16px;
}
.site-card__timeline {
margin-top: auto;
padding-top: 20px;
}
.site-card__timeline-title {
margin-bottom: 10px;
color: #25364d;
font-size: 14px;
font-weight: 600;
}
.timeline-bar {
display: grid;
grid-template-columns: repeat(48, minmax(0, 1fr));
gap: 2px;
align-items: center;
height: 14px;
}
.timeline-bar__segment {
height: 8px;
border-radius: 999px;
background: #e5ebf3;
}
.timeline-bar__segment--available {
background: #32b56c;
}
.timeline-bar__segment--reserved {
background: #9aa6b2;
}
.timeline-bar__segment--closed {
background: #e9edf3;
}
.timeline-scale {
display: grid;
grid-template-columns: repeat(12, minmax(0, 1fr));
margin-top: 10px;
color: #607389;
font-size: 12px;
}
.timeline-scale span {
text-align: left;
}
.timeline-scale span:last-child {
text-align: right;
}
.timeline-legend {
display: flex;
flex-wrap: wrap;
margin-top: 12px;
color: #607389;
font-size: 13px;
gap: 14px;
}
.timeline-legend__item {
display: inline-flex;
align-items: center;
gap: 6px;
}
.timeline-legend__dot {
width: 10px;
height: 10px;
border-radius: 50%;
}
.timeline-legend__dot--available {
background: #32b56c;
}
.timeline-legend__dot--reserved {
background: #9aa6b2;
}
.timeline-legend__dot--closed {
background: #e9edf3;
border: 1px solid #d6dde8;
}
.apply-visual-panel {
margin-bottom: 18px;
padding: 18px;
border: 1px solid #e8edf5;
border-radius: 14px;
background: linear-gradient(180deg, #ffffff 0%, #f6faff 100%);
}
.apply-visual-panel__toolbar {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 16px;
}
.apply-visual-panel__title {
color: #1f2d3d;
font-size: 16px;
font-weight: 700;
}
.apply-visual-panel__subtitle {
margin-top: 6px;
color: #708399;
font-size: 13px;
line-height: 1.6;
}
.apply-visual-panel__actions {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 10px;
}
.apply-visual-panel__legend {
display: flex;
flex-wrap: wrap;
margin-top: 14px;
gap: 14px;
}
.apply-legend__item {
display: inline-flex;
align-items: center;
color: #607389;
font-size: 13px;
gap: 6px;
}
.apply-legend__dot {
width: 10px;
height: 10px;
border-radius: 50%;
}
.apply-legend__dot--available {
background: #32b56c;
}
.apply-legend__dot--reserved {
background: #9aa6b2;
}
.apply-legend__dot--selected {
background: #2f7df6;
}
.apply-legend__dot--closed {
background: #e9edf3;
border: 1px solid #d6dde8;
}
.apply-block-group {
margin-top: 16px;
}
.apply-block-group__title {
margin-bottom: 10px;
color: #1f2d3d;
font-size: 14px;
font-weight: 600;
}
.apply-block-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
gap: 10px;
}
.apply-block {
min-height: 40px;
padding: 8px 10px;
border: 1px solid transparent;
border-radius: 10px;
font-size: 13px;
cursor: pointer;
transition: all 0.2s ease;
}
.apply-block--available {
color: #0f5132;
background: #d9f6e5;
border-color: #87d7a7;
}
.apply-block--reserved {
color: #4b5663;
background: #e1e6ec;
border-color: #c5ced8;
cursor: not-allowed;
}
.apply-block--closed {
color: #8b97a6;
background: #f3f5f8;
border-color: #e2e8f0;
cursor: not-allowed;
}
.apply-block--selected {
color: #ffffff;
background: #2f7df6;
border-color: #2f7df6;
box-shadow: 0 6px 16px rgba(47, 125, 246, 0.22);
}
.apply-block:not(.apply-block--reserved):not(.apply-block--closed):hover {
transform: translateY(-1px);
box-shadow: 0 8px 16px rgba(30, 72, 124, 0.12);
}
.apply-visual-panel__summary {
margin-top: 16px;
padding: 12px 14px;
color: #1f4e8c;
font-size: 13px;
background: #eef5ff;
border-radius: 10px;
}
@media screen and (max-width: 1400px) {
.site-card-grid {
grid-template-columns: 1fr;
}
}
@media screen and (max-width: 768px) {
.site-card {
flex-direction: column;
}
.site-card__media {
flex: none;
margin-right: 0;
margin-bottom: 16px;
}
.site-card__image {
width: 100%;
height: 220px;
}
.site-card__header {
flex-direction: column;
}
.site-card__actions {
justify-content: flex-start;
}
.apply-visual-panel__toolbar {
flex-direction: column;
}
}
</style>
<!--#
}
#-->
@@ -2,109 +2,226 @@
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-tabs v-model="activeTab">
<el-tab-pane label="场地基本信息" name="basicInfo">
<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="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="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 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 label="场地介绍" prop="introduce">
<text-editor v-model="formData.introduce"></text-editor>
</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 label="场地照片" prop="sitePhoto">
<file-upload
:value.sync="formData.sitePhoto"
:upload_number="1"
:upload_size="1024 * 1024 * 10"
accept=".jpg,.jpeg,.png"
upload_result_category="interval"
complete_result>
</file-upload>
</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-tab-pane>
<el-form-item label="场地介绍" prop="introduce">
<text-editor v-model="formData.introduce"></text-editor>
</el-form-item>
<el-tab-pane label="设置场地" name="siteSetting">
<el-row :gutter="20" type="flex">
<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-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-row>
<el-row :gutter="20" type="flex">
<el-col :span="12">
<el-form-item prop="reserveTimeType" label="预约时间段类型">
<el-radio-group v-model="formData.reserveTimeType" @change="handleReserveTimeTypeChange" size="medium">
<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="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-divider content-position="left">场次信息</el-divider>
<div v-if="formData.reserveTimeType === 1">
<div class="left-span-label">分段预约:可设置一个或多个场次,预约人按拆分后的场次进行预约。</div>
<el-table :data="formData.openHours" border size="mini">
<el-table-column type="index" label="序号" align="center" header-align="center" width="80"></el-table-column>
<el-table-column prop="startTime" label="开始时间" align="center" header-align="center">
<template v-slot="{ row }">
<el-time-select
style="width: 100%"
placeholder="开始时间"
v-model="row.startTime"
@change="checkSegmentedOpenHoursOverlap"
:picker-options="{ start: '00:00', step: '00:30', end: '23:59' }">
</el-time-select>
</template>
</el-table-column>
<el-table-column prop="endTime" label="结束时间" align="center" header-align="center">
<template v-slot="{ row }">
<el-time-select
style="width: 100%"
placeholder="结束时间"
v-model="row.endTime"
@change="checkSegmentedOpenHoursOverlap"
:picker-options="{ start: '00:00', step: '00:30', end: '23:59' }">
</el-time-select>
</template>
</el-table-column>
<el-table-column prop="timeUnit" label="预约时间单位(分钟)" align="center" header-align="center">
<template v-slot="{ row }">
<el-input-number style="width: 100%" v-model="row.timeUnit" :min="1" :step="1" :precision="0" placeholder="请输入预约时间单位"></el-input-number>
</template>
</el-table-column>
<el-table-column label="操作" align="center" header-align="center" width="130">
<template slot="header">
<el-button size="mini" type="primary" icon="el-icon-plus" @click="addOpenHour">添加场次</el-button>
</template>
<template v-slot="scope">
<el-button
type="danger"
icon="el-icon-delete"
size="mini"
:disabled="formData.openHours.length <= 1"
@click="formData.openHours.splice(scope.$index, 1)">
</el-button>
</template>
</el-table-column>
</el-table>
</div>
<div v-else>
<div class="left-span-label">全天候预约:设置可预约的开始时间、结束时间和预约时间单位(分钟),预约人按设定单位选择连续时段。</div>
<el-row :gutter="20" type="flex">
<el-col :span="12">
<el-form-item label="开始时间">
<el-time-select
style="width: 100%"
placeholder="开始时间"
v-model="fullDayOpenHour.startTime"
:picker-options="{ start: '00:00', step: '00:30', end: '23:59' }">
</el-time-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="结束时间">
<el-time-select
style="width: 100%"
placeholder="结束时间"
v-model="fullDayOpenHour.endTime"
:picker-options="{ start: '00:00', step: '00:30', end: '23:59' }">
</el-time-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="预约时间单位(分钟)">
<el-input-number
style="width: 100%"
v-model="fullDayOpenHour.timeUnit"
:min="1"
:step="1"
:precision="0"
placeholder="请输入预约时间单位">
</el-input-number>
</el-form-item>
</el-col>
</el-row>
</div>
</el-tab-pane>
</el-tabs>
</el-form>
<div class="mt10" style="color: #909399; font-size: 13px;">
暂存:保存当前填写内容并留在本页继续编辑。正式提交:保存后返回列表页。
</div>
<el-row class="mt10" justify="end" type="flex">
<el-button @click="$emit('refresh')">取消</el-button>
<el-button @click="onSubmit" type="primary">提交</el-button>
<el-button @click="onTempSave" plain type="warning">暂存并继续编辑</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="设置时间">
@@ -202,10 +319,17 @@
store,
data() {
return {
activeTab: 'basicInfo',
segmentedOpenHoursCache: [{ startTime: '', endTime: '', timeUnit: 30 }],
fullDayOpenHourCache: { startTime: '', endTime: '', timeUnit: 30 },
formData: {
state: true,
sexLimit: 0,
filterHolidays: false,
reserveTimeType: 1,
openHours: [{ startTime: '', endTime: '', timeUnit: 30 }],
segmentedOpenHours: [{ startTime: '', endTime: '', timeUnit: 30 }],
fullDayOpenHour: { startTime: '', endTime: '', timeUnit: 30 },
notApplyTimeList: [],
createUserName: this.$store.state.user.username,
createUserId: this.$store.state.user.id,
@@ -222,6 +346,7 @@
sexLimit: [{required: true, message: '必填', trigger: ['blur', 'change']}],
filterHolidays: [{required: true, message: '必填', trigger: ['blur', 'change']}],
state: [{required: true, message: '必填', trigger: ['blur', 'change']}],
reserveTimeType: [{required: true, message: '必填', trigger: ['blur', 'change']}],
},
setUpTimeDialog: false,
timeOneKeySet: {
@@ -231,7 +356,218 @@
multipleSelection: [],
}
},
computed: {
fullDayOpenHour() {
if (!Array.isArray(this.formData.openHours)) {
this.$set(this.formData, 'openHours', [])
}
if (this.formData.openHours.length === 0) {
this.formData.openHours.push({ startTime: '', endTime: '', timeUnit: 30 })
}
if (!this.formData.openHours[0].timeUnit || this.formData.openHours[0].timeUnit <= 0) {
this.$set(this.formData.openHours[0], 'timeUnit', 30)
}
return this.formData.openHours[0]
}
},
methods: {
normalizeTimeUnit(value, defaultValue = 30) {
const num = Number(value)
if (!Number.isFinite(num) || num <= 0) {
return defaultValue
}
return Math.round(num)
},
syncOpenHoursCache() {
if (this.formData.reserveTimeType === 1) {
this.segmentedOpenHoursCache = Array.isArray(this.formData.openHours) && this.formData.openHours.length > 0
? clone(this.formData.openHours).map(item => ({
startTime: item.startTime || '',
endTime: item.endTime || '',
timeUnit: this.normalizeTimeUnit(item.timeUnit),
}))
: [{ startTime: '', endTime: '', timeUnit: 30 }]
this.formData.segmentedOpenHours = clone(this.segmentedOpenHoursCache)
} else {
const first = (this.formData.openHours && this.formData.openHours[0]) || {}
this.fullDayOpenHourCache = {
startTime: first.startTime || '',
endTime: first.endTime || '',
timeUnit: this.normalizeTimeUnit(first.timeUnit),
}
this.formData.fullDayOpenHour = clone(this.fullDayOpenHourCache)
}
},
buildSubmitFormData() {
this.syncOpenHoursCache()
const submitData = clone(this.formData)
submitData.segmentedOpenHours = clone(this.segmentedOpenHoursCache || [])
submitData.fullDayOpenHour = clone(this.fullDayOpenHourCache || { startTime: '', endTime: '', timeUnit: 30 })
if (submitData.reserveTimeType === 1) {
submitData.openHours = clone(this.segmentedOpenHoursCache || [])
} else {
submitData.openHours = [clone(this.fullDayOpenHourCache || { startTime: '', endTime: '', timeUnit: 60 })]
}
return submitData
},
initOpenHoursCache() {
const segmentedOpenHours = Array.isArray(this.formData.segmentedOpenHours) && this.formData.segmentedOpenHours.length > 0
? clone(this.formData.segmentedOpenHours)
: (Array.isArray(this.formData.openHours) ? clone(this.formData.openHours) : [])
const fullDayOpenHour = this.formData.fullDayOpenHour && Object.keys(this.formData.fullDayOpenHour).length > 0
? clone(this.formData.fullDayOpenHour)
: ((Array.isArray(this.formData.openHours) && this.formData.openHours.length > 0) ? clone(this.formData.openHours[0]) : {})
if (this.formData.reserveTimeType === 2) {
this.fullDayOpenHourCache = {
startTime: fullDayOpenHour.startTime || '',
endTime: fullDayOpenHour.endTime || '',
timeUnit: this.normalizeTimeUnit(fullDayOpenHour.timeUnit),
}
this.segmentedOpenHoursCache = segmentedOpenHours && segmentedOpenHours.length > 0
? segmentedOpenHours.map(item => ({
startTime: item.startTime || '',
endTime: item.endTime || '',
timeUnit: this.normalizeTimeUnit(item.timeUnit),
}))
: [{ startTime: '', endTime: '', timeUnit: 30 }]
this.formData.openHours = [clone(this.fullDayOpenHourCache)]
} else {
this.segmentedOpenHoursCache = segmentedOpenHours.length > 0
? segmentedOpenHours.map(item => ({
startTime: item.startTime || '',
endTime: item.endTime || '',
timeUnit: this.normalizeTimeUnit(item.timeUnit),
}))
: [{ startTime: '', endTime: '', timeUnit: 30 }]
const first = this.segmentedOpenHoursCache[0] || {}
this.fullDayOpenHourCache = {
startTime: fullDayOpenHour.startTime || first.startTime || '',
endTime: fullDayOpenHour.endTime || first.endTime || '',
timeUnit: this.normalizeTimeUnit(fullDayOpenHour.timeUnit || first.timeUnit),
}
this.formData.openHours = clone(this.segmentedOpenHoursCache)
}
this.formData.segmentedOpenHours = clone(this.segmentedOpenHoursCache)
this.formData.fullDayOpenHour = clone(this.fullDayOpenHourCache)
},
addOpenHour() {
if (!Array.isArray(this.formData.openHours)) {
this.$set(this.formData, 'openHours', [])
}
this.formData.openHours.push({ startTime: '', endTime: '', timeUnit: 30 })
this.segmentedOpenHoursCache = clone(this.formData.openHours)
},
hasSegmentedOpenHoursOverlap(openHours) {
const validHours = (openHours || [])
.map((item, index) => ({ ...item, index }))
.filter(item => item.startTime && item.endTime && item.startTime < item.endTime)
.sort((a, b) => a.startTime.localeCompare(b.startTime))
for (let i = 1; i < validHours.length; i++) {
const prev = validHours[i - 1]
const current = validHours[i]
if (current.startTime < prev.endTime) {
return {
overlap: true,
prevIndex: prev.index,
currentIndex: current.index,
}
}
}
return { overlap: false }
},
checkSegmentedOpenHoursOverlap() {
if (this.formData.reserveTimeType !== 1) {
return false
}
const result = this.hasSegmentedOpenHoursOverlap(this.formData.openHours)
if (result.overlap) {
this.$message.warning('第' + (result.prevIndex + 1) + '条和第' + (result.currentIndex + 1) + '条场次时间有重叠,请调整后再保存')
return true
}
return false
},
handleReserveTimeTypeChange(val) {
if (!Array.isArray(this.formData.openHours)) {
this.$set(this.formData, 'openHours', [])
}
if (val === 1) {
const currentFullDay = this.formData.openHours[0] || this.fullDayOpenHourCache || {}
this.fullDayOpenHourCache = {
startTime: currentFullDay.startTime || '',
endTime: currentFullDay.endTime || '',
timeUnit: this.normalizeTimeUnit(currentFullDay.timeUnit),
}
if (!Array.isArray(this.segmentedOpenHoursCache) || this.segmentedOpenHoursCache.length === 0) {
this.segmentedOpenHoursCache = [{
startTime: this.fullDayOpenHourCache.startTime || '',
endTime: this.fullDayOpenHourCache.endTime || '',
timeUnit: this.normalizeTimeUnit(this.fullDayOpenHourCache.timeUnit),
}]
}
this.formData.openHours = clone(this.segmentedOpenHoursCache)
} else if (val === 2) {
this.segmentedOpenHoursCache = Array.isArray(this.formData.openHours) && this.formData.openHours.length > 0
? clone(this.formData.openHours)
: this.segmentedOpenHoursCache
const first = this.fullDayOpenHourCache && (this.fullDayOpenHourCache.startTime || this.fullDayOpenHourCache.endTime)
? this.fullDayOpenHourCache
: (this.segmentedOpenHoursCache[0] || {})
this.fullDayOpenHourCache = {
startTime: first.startTime || '',
endTime: first.endTime || '',
timeUnit: this.normalizeTimeUnit(first.timeUnit || this.fullDayOpenHourCache.timeUnit),
}
this.formData.openHours = [clone(this.fullDayOpenHourCache)]
}
},
validateOpenHours() {
const openHours = this.formData.openHours || []
if (this.formData.reserveTimeType === 1) {
if (openHours.length === 0) {
this.$message.warning('请至少添加一条场次信息')
return false
}
for (let i = 0; i < openHours.length; i++) {
const item = openHours[i] || {}
if (!item.startTime) {
this.$message.warning('第' + (i + 1) + '条场次信息中,开始时间必填')
return false
}
if (!item.endTime) {
this.$message.warning('第' + (i + 1) + '条场次信息中,结束时间必填')
return false
}
if (item.startTime >= item.endTime) {
this.$message.warning('第' + (i + 1) + '条场次信息中,开始时间必须早于结束时间')
return false
}
if (!item.timeUnit || item.timeUnit <= 0) {
this.$message.warning('第' + (i + 1) + '条场次信息中,预约时间单位必须大于0分钟')
return false
}
}
const overlapResult = this.hasSegmentedOpenHoursOverlap(openHours)
if (overlapResult.overlap) {
this.$message.warning('第' + (overlapResult.prevIndex + 1) + '条和第' + (overlapResult.currentIndex + 1) + '条场次时间有重叠,请调整后再保存')
return false
}
} else {
const item = openHours[0] || {}
if (!item.startTime || !item.endTime) {
this.$message.warning('请设置全天候预约的开始时间和结束时间')
return false
}
if (item.startTime >= item.endTime) {
this.$message.warning('全天候预约的开始时间必须早于结束时间')
return false
}
if (!item.timeUnit || item.timeUnit <= 0) {
this.$message.warning('全天候预约的预约时间单位必须大于0分钟')
return false
}
}
return true
},
doConfirmSetUpCourse() {
const timeList = this.formData.notApplyTimeList
if (timeList && timeList.length > 0) {
@@ -296,40 +632,103 @@
if (this.formData.notApplyTimeList) {
this.$set(this.formData, 'setUpDate', this.formData.notApplyTimeList.map(o => o.date))
}
this.activeTab = 'siteSetting'
this.setUpTimeDialog = true
},
onOpen(row) {
this.activeTab = 'basicInfo'
if (row && row.id) {
this.formData = clone(row)
if (!Array.isArray(this.formData.notApplyTimeList)) {
this.$set(this.formData, 'notApplyTimeList', [])
}
if (!this.formData.reserveTimeType) {
this.$set(this.formData, 'reserveTimeType', 1)
}
if (!Array.isArray(this.formData.openHours) || this.formData.openHours.length === 0) {
this.$set(this.formData, 'openHours', this.formData.reserveTimeType === 2 ? [{ startTime: '', endTime: '', timeUnit: 30 }] : [{ startTime: '', endTime: '', timeUnit: 30 }])
}
if (!Array.isArray(this.formData.segmentedOpenHours) || this.formData.segmentedOpenHours.length === 0) {
this.$set(this.formData, 'segmentedOpenHours', [{ startTime: '', endTime: '', timeUnit: 30 }])
}
if (!this.formData.fullDayOpenHour) {
this.$set(this.formData, 'fullDayOpenHour', { startTime: '', endTime: '', timeUnit: 30 })
}
this.initOpenHoursCache()
} else {
this.formData = {
state: true,
sexLimit: 0,
filterHolidays: false,
reserveTimeType: 1,
openHours: [{ startTime: '', endTime: '', timeUnit: 30 }],
segmentedOpenHours: [{ startTime: '', endTime: '', timeUnit: 30 }],
fullDayOpenHour: { startTime: '', endTime: '', timeUnit: 30 },
notApplyTimeList: [],
createUserName: this.$store.state.user.username,
createUserId: this.$store.state.user.id,
}
this.segmentedOpenHoursCache = [{ startTime: '', endTime: '', timeUnit: 30 }]
this.fullDayOpenHourCache = { startTime: '', endTime: '', timeUnit: 30 }
}
},
async doSave(stayOnPage) {
if (!this.validateOpenHours()) {
this.activeTab = 'siteSetting'
return
}
const submitData = this.buildSubmitFormData()
const resp = await this.$axios.post("/platform/siteCug/manage/submit", {data: JSON.stringify(submitData)})
if (resp.code === 0) {
this.$message.success(stayOnPage ? '暂存成功,您可以继续编辑当前内容' : '正式提交成功,已返回列表页')
if (resp.data) {
this.formData = clone(resp.data)
if (!Array.isArray(this.formData.notApplyTimeList)) {
this.$set(this.formData, 'notApplyTimeList', [])
}
if (!this.formData.reserveTimeType) {
this.$set(this.formData, 'reserveTimeType', 1)
}
if (!Array.isArray(this.formData.openHours) || this.formData.openHours.length === 0) {
this.$set(this.formData, 'openHours', this.formData.reserveTimeType === 2 ? [{ startTime: '', endTime: '', timeUnit: 30 }] : [{ startTime: '', endTime: '', timeUnit: 30 }])
}
if (!Array.isArray(this.formData.segmentedOpenHours) || this.formData.segmentedOpenHours.length === 0) {
this.$set(this.formData, 'segmentedOpenHours', [{ startTime: '', endTime: '', timeUnit: 30 }])
}
if (!this.formData.fullDayOpenHour) {
this.$set(this.formData, 'fullDayOpenHour', { startTime: '', endTime: '', timeUnit: 30 })
}
this.initOpenHoursCache()
}
if (!stayOnPage) {
this.$emit('refresh')
}
} else {
this.$message.warning(resp.msg)
}
},
onTempSave() {
this.$refs.formRef.validate(async (valid) => {
if (valid) {
this.$confirm("确认暂存当前内容,并继续留在本页编辑吗?", "暂存确认", {
confirmButtonText: "确认暂存",
cancelButtonText: "取消",
type: "warning"
}).then(async () => {
await this.doSave(true)
}).catch(() => {})
}
})
},
onSubmit() {
this.$refs.formRef.validate((valid) => {
if (valid) {
this.$confirm("您确定要提交吗?", "提示", {
confirmButtonText: "确",
this.$confirm("确认正式提交当前内容吗?提交成功后将返回列表页。", "正式提交确认", {
confirmButtonText: "确认提交",
cancelButtonText: "取消",
type: "warning"
type: "primary"
}).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)
}
await this.doSave(false)
})
}
})
@@ -39,15 +39,15 @@ layout("/layouts/platform.html"){
<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
v-for="column in tableColumns"
:key="column.prop"
: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
@@ -57,6 +57,10 @@ layout("/layouts/platform.html"){
inactive-color="#ff4949">
</el-switch>
</template>
<template v-slot="{ row }" v-else-if="column.prop === 'reserveTimeType'">
<span v-if="row.reserveTimeType === 2">全天候预约</span>
<span v-else>分段预约</span>
</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>
@@ -98,16 +102,16 @@ layout("/layouts/platform.html"){
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: 'createUserName', label: '创建人', sortable: 'custom'},
{prop: 'name', label: '场地名称', sortable: 'custom'},
{prop: 'address', label: '场地地址', sortable: 'custom'},
{prop: 'contactName', label: '联系人', sortable: 'custom'},
{prop: 'contactPhone', label: '联系电话', sortable: 'custom'},
{prop: 'maxNum', label: '容纳人数', sortable: 'custom'},
{prop: 'typeName', label: '场地类型'},
{prop: 'sexLimit', label: '性别限制'},
{prop: 'state', label: '开启状态'},
{prop: 'reserveTimeType', label: '预约时间段类型', sortable: 'custom'},
{prop: 'sexLimit', label: '性别限制', sortable: 'custom'},
{prop: 'state', label: '开启状态', sortable: 'custom'},
],
}
},
@@ -132,7 +136,7 @@ layout("/layouts/platform.html"){
})
},
onDelete(row) {
this.$confirm("您确定要删除吗, 是否继续?", "提示", {
this.$confirm("您确定要删除吗? 是否继续?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
@@ -177,4 +181,4 @@ layout("/layouts/platform.html"){
</script>
<!--#
}
#-->
#-->
@@ -1,51 +1,99 @@
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>
<el-tabs v-model="activeTab">
<el-tab-pane label="场地基本信息" name="basicInfo">
<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="2">
<div v-if="viewData.introduce" v-html="viewData.introduce"></div>
<div v-else>暂无场地介绍</div>
</el-descriptions-item>
<el-descriptions-item label="场地照片" :span="2">
<el-image
v-if="viewData.sitePhoto"
:preview-src-list="[viewData.sitePhoto]"
:src="viewData.sitePhoto"
fit="cover"
style="width: 160px; height: 160px; border-radius: 4px;">
</el-image>
<div v-else>暂无场地照片</div>
</el-descriptions-item>
</el-descriptions>
</el-tab-pane>
<el-tab-pane label="设置场地" name="siteSetting">
<el-descriptions :column="2" border>
<el-descriptions-item label="开启状态">
<span v-if="viewData.state">开启</span>
<span v-else>禁用</span>
</el-descriptions-item>
<el-descriptions-item label="性别限制">
<span v-if="viewData.sexLimit === 0">不限制</span>
<span v-else-if="viewData.sexLimit === 1">男</span>
<span v-else-if="viewData.sexLimit === 2">女</span>
<span v-else>--</span>
</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 v-if="viewData.reserveTimeType === 1">分段预约</span>
<span v-else-if="viewData.reserveTimeType === 2">全天候预约</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="2">
<div v-if="viewData.reserveTimeType === 1">
<el-table v-if="viewData.openHours && viewData.openHours.length > 0"
:data="viewData.openHours" max-height="300" size="mini">
<el-table-column type="index" label="序号" width="80"></el-table-column>
<el-table-column prop="startTime" label="开始时间"></el-table-column>
<el-table-column prop="endTime" label="结束时间"></el-table-column>
<el-table-column prop="timeUnit" label="预约时间单位(分钟)"></el-table-column>
</el-table>
<span v-else>暂无场次信息</span>
</div>
<div v-else-if="viewData.reserveTimeType === 2">
<div v-if="viewData.openHours && viewData.openHours.length > 0">
<div><span>开始时间:</span><span>{{ viewData.openHours[0].startTime || '--' }}</span></div>
<div><span>结束时间:</span><span>{{ viewData.openHours[0].endTime || '--' }}</span></div>
<div><span>预约时间单位(分钟):</span><span>{{ viewData.openHours[0].timeUnit || '--' }}</span></div>
</div>
<span v-else>暂无场次信息</span>
</div>
<span v-else>暂无场次信息</span>
</el-descriptions-item>
</el-descriptions>
</el-tab-pane>
</el-tabs>
</div>
`,
data() {
return {
activeTab: 'basicInfo',
viewData: {},
}
},
methods: {
onOpen(row) {
this.activeTab = 'basicInfo'
this.viewData = row
},
},
@@ -1,23 +1,55 @@
<!--#
layout("/layouts/platform.html"){
#-->
<style>
.mine-batch-toggle {
padding: 0;
color: #409EFF;
font-size: 16px;
}
.mine-batch-empty {
display: inline-block;
width: 16px;
height: 16px;
}
.mine-batch-child-row {
background: #fafcff;
}
.mine-batch-child-label {
display: inline-flex;
align-items: center;
gap: 8px;
color: #7a8a9a;
}
.mine-batch-child-label::before {
content: "";
width: 16px;
height: 1px;
background: #c8d3df;
}
</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"
<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-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-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>
@@ -26,33 +58,46 @@ layout("/layouts/platform.html"){
<el-card shadow="never">
<table-tool label="申请列表"></table-tool>
<el-table :data="tableData" @sort-change="pageOrder" style="width: 100%">
<el-table :data="tableData" @sort-change="pageOrder" :row-class-name="tableRowClassName" style="width: 100%;">
<el-table-column label="" width="54" align="center" header-align="center">
<template v-slot="{ row }">
<el-button
v-if="row._hasFoldChildren"
class="mine-batch-toggle"
type="text"
@click="toggleBatchGroup(row)">
<i :class="row._expanded ? 'el-icon-caret-bottom' : 'el-icon-caret-right'"></i>
</el-button>
<span v-else class="mine-batch-empty"></span>
</template>
</el-table-column>
<el-table-column type="index" width="50" label="序号" :index="indexMethod"></el-table-column>
<el-table-column
v-for="column in tableColumns"
:key="column.prop"
: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'">
<template v-slot="{ row }" v-if="column.prop === 'siteName'">
<span v-if="row._isBatchChild" class="mine-batch-child-label">{{ row.siteName }}</span>
<span v-else>{{ row.siteName }}</span>
</template>
<template v-slot="{ row }" v-else-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 v-slot="{ row }" v-else-if="column.prop === 'yearlyBatch'">
<span>{{ normalizeBatchFlag(row) ? '是' : '否' }}</span>
</template>
</el-table-column>
<el-table-column label="操作" fixed="right" width="180">
<template v-slot="{row}">
<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>
<el-button v-if="row.canCancel && !row._isBatchChild" @click="onDelete(row)" size="mini" type="danger">删除</el-button>
</template>
</el-table-column>
</el-table>
@@ -79,29 +124,85 @@ layout("/layouts/platform.html"){
return {
typeOptions: [],
siteOptions: [],
rawTableData: [],
expandedBatchKeys: {},
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: '流程状态'},
{prop: 'siteName', label: '场地名称', sortable: 'custom'},
{prop: 'applyUserName', label: '预约人', sortable: 'custom'},
{prop: 'applyLoginName', label: '工号', sortable: 'custom'},
{prop: 'yearlyBatch', label: '是否批量预约', sortable: 'custom', width: 130},
{prop: 'reserveTargetName', label: '预约单位', sortable: 'custom'},
{prop: 'reserveStartTime', label: '开始时间', sortable: 'custom'},
{prop: 'reserveEndTime', label: '结束时间', sortable: 'custom'},
{prop: 'applyMobile', label: '联系方式', sortable: 'custom'},
{prop: 'taskName', label: '当前节点', sortable: 'custom'},
{prop: 'instanceState', label: '流程状态', sortable: 'custom', width: 120},
],
}
},
methods: {
normalizeBatchFlag(row) {
return row && (row.yearlyBatch === 1 || row.yearlyBatch === '1' || row.yearlyBatch === true)
},
getBatchGroupKey(row) {
if (!this.normalizeBatchFlag(row)) {
return 'single_' + row.id
}
const batchNo = row.backOption || row.id
return 'batch_' + batchNo + '_' + (row.applyUserName || '') + '_' + (row.siteName || '')
},
buildTableData(rows) {
const groups = new Map()
;(rows || []).forEach(row => {
const key = this.getBatchGroupKey(row)
if (!groups.has(key)) {
groups.set(key, [])
}
groups.get(key).push({...row})
})
const displayRows = []
groups.forEach((groupRows, key) => {
const sortedRows = groupRows.slice().sort((a, b) => {
return (b.reserveStartTime || '').localeCompare(a.reserveStartTime || '')
})
const parent = {...sortedRows[0]}
const children = sortedRows.slice(1).map(item => ({
...item,
_isBatchChild: true,
_groupKey: key,
_hasFoldChildren: false,
}))
parent._groupKey = key
parent._isBatchChild = false
parent._hasFoldChildren = children.length > 0
parent._expanded = !!this.expandedBatchKeys[key]
parent._batchCount = sortedRows.length
parent._foldedRecords = children
displayRows.push(parent)
if (parent._expanded) {
displayRows.push(...children)
}
})
return displayRows
},
tableRowClassName({ row }) {
return row && row._isBatchChild ? 'mine-batch-child-row' : ''
},
toggleBatchGroup(row) {
if (!row || !row._groupKey || !row._hasFoldChildren) {
return
}
this.$set(this.expandedBatchKeys, row._groupKey, !this.expandedBatchKeys[row._groupKey])
this.tableData = this.buildTableData(this.rawTableData)
},
onView(row) {
this.$refs.guava.edit(() => {
this.$refs.infoRef.onOpen(row)
})
},
onDelete(row) {
const message = row.yearlyBatch
? ('该记录属于“预约本年”批量预约,确认后将一并删除同批次的 ' + (row.batchDeleteCount || 0) + ' 条预约记录,是否继续?')
const message = this.normalizeBatchFlag(row)
? ('该记录属于“预约本年”批量预约,确认后将一并删除同批次的 ' + (row.batchDeleteCount || row._batchCount || 0) + ' 条预约记录,是否继续?')
: '您确定要删除吗?'
this.$confirm(message, '提示', {
confirmButtonText: '确定',
@@ -126,6 +227,15 @@ layout("/layouts/platform.html"){
this.siteOptions = res.data
})
},
pageData() {
this.$axios.post(loc() + '/pageData', this.pageForm).then((res) => {
if (res.code === 0) {
this.rawTableData = Array.isArray(res.data.list) ? res.data.list : []
this.tableData = this.buildTableData(this.rawTableData)
this.pageForm.totalCount = res.data.totalCount
}
})
},
},
async created() {
this.querySiteType()
@@ -1,64 +1,111 @@
<!--#
layout("/layouts/platform.html"){
#-->
<style>
.record-batch-toggle {
padding: 0;
color: #409EFF;
font-size: 16px;
}
.record-batch-empty {
display: inline-block;
width: 16px;
height: 16px;
}
.record-batch-child-row {
background: #fafcff;
}
.record-batch-child-label {
display: inline-flex;
align-items: center;
gap: 8px;
color: #7a8a9a;
}
.record-batch-child-label::before {
content: "";
width: 16px;
height: 1px;
background: #c8d3df;
}
</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">
<search-item label="姓名/工号">
<el-input
v-model="pageForm.searchKeyword"
placeholder="请输入姓名或工号"
clearable
@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
v-model="pageForm.siteId"
@change="doSearch"
style="width: 100%"
placeholder="请选择活动场地"
filterable
clearable>
<el-option
v-for="item in siteOptions"
:key="item.id"
:label="item.name"
:value="item.id">
</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
v-model="pageForm.siteType"
@change="doSearch"
style="width: 100%"
placeholder="请选择场地类型"
filterable
clearable>
<el-option
v-for="item in typeOptions"
:key="item.id"
:label="item.name"
:value="item.id">
</el-option>
</el-select>
</search-item>
<search-item label="预约单位">
<el-input placeholder="请输入分工会或协会名称" clearable v-model="pageForm.reserveTargetKeyword"
@keyup.enter.native="doSearch">
<el-input
v-model="pageForm.reserveTargetKeyword"
placeholder="请输入分工会或协会名称"
clearable
@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
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
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>
@@ -68,27 +115,47 @@ layout("/layouts/platform.html"){
<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
:data="tableData"
:row-class-name="tableRowClassName"
@sort-change="pageOrder"
style="width: 100%">
<el-table-column label="" width="54" align="center" header-align="center">
<template v-slot="{ row }">
<el-button
v-if="row._hasFoldChildren"
class="record-batch-toggle"
type="text"
@click="toggleBatchGroup(row)">
<i :class="row._expanded ? 'el-icon-caret-bottom' : 'el-icon-caret-right'"></i>
</el-button>
<span v-else class="record-batch-empty"></span>
</template>
</el-table-column>
<el-table-column type="index" width="50" label="序号" :index="indexMethod"></el-table-column>
<el-table-column
v-for="column in tableColumns"
:key="column.prop"
: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>
show-overflow-tooltip>
<template v-slot="{ row }" v-if="column.prop === 'siteName'">
<span v-if="row._isBatchChild" class="record-batch-child-label">{{ row.siteName }}</span>
<span v-else>{{ row.siteName }}</span>
</template>
<template v-slot="{ row }" v-else-if="column.prop === 'reserveTargetName'">
<span>{{ row.reserveType === 'club' ? (row.clubName || '-') : (row.applyUnionName || '-') }}</span>
</template>
<template v-slot="{ row }" v-else-if="column.prop === 'yearlyBatch'">
<span>{{ normalizeBatchFlag(row) ? '是' : '否' }}</span>
</template>
</el-table-column>
<el-table-column label="操作" fixed="right" width="180">
<template v-slot="{row}">
<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>
@@ -111,7 +178,7 @@ layout("/layouts/platform.html"){
store,
mixins: [initTableMixins],
components: {
'info': siteCugApplyInfo,
info: siteCugApplyInfo,
},
data() {
return {
@@ -126,19 +193,75 @@ layout("/layouts/platform.html"){
},
typeOptions: [],
siteOptions: [],
rawTableData: [],
expandedBatchKeys: {},
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: 'siteName', label: '场地名称', sortable: 'custom' },
{ prop: 'applyUserName', label: '预约人', sortable: 'custom' },
{ prop: 'applyLoginName', label: '工号', sortable: 'custom' },
{ prop: 'yearlyBatch', label: '是否批量预约', sortable: 'custom', width: 130 },
{ prop: 'reserveTargetName', label: '预约单位', sortable: 'custom' },
{ prop: 'reserveStartTime', label: '开始时间', sortable: 'custom' },
{ prop: 'reserveEndTime', label: '结束时间', sortable: 'custom' },
{ prop: 'applyMobile', label: '联系方式', sortable: 'custom' },
],
}
},
methods: {
normalizeBatchFlag(row) {
return row && (row.yearlyBatch === 1 || row.yearlyBatch === '1' || row.yearlyBatch === true)
},
getBatchGroupKey(row) {
if (!this.normalizeBatchFlag(row)) {
return 'single_' + row.id
}
const batchNo = row.backOption || row.id
return 'batch_' + batchNo + '_' + (row.applyUserName || '') + '_' + (row.siteName || '')
},
buildTableData(rows) {
const groups = new Map()
;(rows || []).forEach(row => {
const key = this.getBatchGroupKey(row)
if (!groups.has(key)) {
groups.set(key, [])
}
groups.get(key).push({ ...row })
})
const displayRows = []
groups.forEach((groupRows, key) => {
const sortedRows = groupRows.slice().sort((a, b) => {
return (b.reserveStartTime || '').localeCompare(a.reserveStartTime || '')
})
const parent = { ...sortedRows[0] }
const children = sortedRows.slice(1).map(item => ({
...item,
_isBatchChild: true,
_groupKey: key,
_hasFoldChildren: false,
}))
parent._groupKey = key
parent._isBatchChild = false
parent._hasFoldChildren = children.length > 0
parent._expanded = !!this.expandedBatchKeys[key]
parent._batchCount = sortedRows.length
parent._foldedRecords = children
displayRows.push(parent)
if (parent._expanded) {
displayRows.push(...children)
}
})
return displayRows
},
tableRowClassName({ row }) {
return row && row._isBatchChild ? 'record-batch-child-row' : ''
},
toggleBatchGroup(row) {
if (!row || !row._groupKey || !row._hasFoldChildren) {
return
}
this.$set(this.expandedBatchKeys, row._groupKey, !this.expandedBatchKeys[row._groupKey])
this.tableData = this.buildTableData(this.rawTableData)
},
onView(row) {
this.$refs.guava.edit(() => {
this.$refs.infoRef.onOpen(row)
@@ -148,7 +271,7 @@ layout("/layouts/platform.html"){
this.$confirm('您确定要删除吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
type: 'warning',
}).then(() => {
this.$axios.post('/platform/siteCug/record/delete', { id }).then((res) => {
if (res.code === 0) {
@@ -158,7 +281,6 @@ layout("/layouts/platform.html"){
})
})
},
// 导出内容和列表当前筛选条件保持一致,实现所见即所得
doExport() {
this.$downLoad('/platform/siteCug/record/doExport', this.pageForm)
},
@@ -172,13 +294,21 @@ layout("/layouts/platform.html"){
this.siteOptions = res.data
})
},
pageData() {
this.$axios.post(loc() + '/pageData', this.pageForm).then((res) => {
if (res.code === 0) {
this.rawTableData = Array.isArray(res.data.list) ? res.data.list : []
this.tableData = this.buildTableData(this.rawTableData)
this.pageForm.totalCount = res.data.totalCount
}
})
},
},
created() {
// 查询字段在 data 中一次性声明完整,避免 Vue 2 对后加属性渲染不稳定
this.querySiteType()
this.querySites()
this.pageData()
}
},
})
</script>
@@ -1,35 +1,81 @@
<!--#
layout("/layouts/platform.html"){
#-->
<style>
.audit-batch-toggle {
padding: 0;
color: #409EFF;
font-size: 16px;
}
.audit-batch-empty {
display: inline-block;
width: 16px;
height: 16px;
}
.audit-batch-child-row {
background: #fafcff;
}
.audit-batch-child-label {
display: inline-flex;
align-items: center;
gap: 8px;
color: #7a8a9a;
}
.audit-batch-child-label::before {
content: "";
width: 16px;
height: 1px;
background: #c8d3df;
}
</style>
<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">
<search-item label="姓名/工号">
<el-input
v-model="pageForm.searchKeyword"
placeholder="请输入姓名或工号"
clearable
@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
v-model="pageForm.siteId"
@change="doSearch"
style="width: 100%"
placeholder="请选择活动场地"
filterable
clearable>
<el-option
v-for="item in siteOptions"
:key="item.id"
:label="item.name"
:value="item.id">
</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
v-model="pageForm.siteType"
@change="doSearch"
style="width: 100%"
placeholder="请选择场地类型"
filterable
clearable>
<el-option
v-for="item in typeOptions"
:key="item.id"
:label="item.name"
:value="item.id">
</el-option>
</el-select>
</search-item>
</search>
@@ -42,34 +88,53 @@ layout("/layouts/platform.html"){
<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
:data="tableData"
:row-class-name="tableRowClassName"
@sort-change="pageOrder"
style="width: 100%">
<el-table-column label="" width="54" align="center" header-align="center">
<template v-slot="{ row }">
<el-button
v-if="row._hasFoldChildren"
class="audit-batch-toggle"
type="text"
@click="toggleBatchGroup(row)">
<i :class="row._expanded ? 'el-icon-caret-bottom' : 'el-icon-caret-right'"></i>
</el-button>
<span v-else class="audit-batch-empty"></span>
</template>
</el-table-column>
<el-table-column type="index" width="50" label="序号" :index="indexMethod"></el-table-column>
<el-table-column
v-for="column in tableColumns"
:key="column.prop"
: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>
show-overflow-tooltip>
<template v-slot="{ row }" v-if="column.prop === 'siteName'">
<span v-if="row._isBatchChild" class="audit-batch-child-label">{{ row.siteName }}</span>
<span v-else>{{ row.siteName }}</span>
</template>
<template v-slot="{ row }" v-else-if="column.prop === 'reserveTargetName'">
<span>{{ row.reserveType === 'club' ? (row.clubName || '-') : (row.applyUnionName || '-') }}</span>
</template>
<template v-slot="{ row }" v-else-if="column.prop === 'yearlyBatch'">
<span>{{ normalizeBatchFlag(row) ? '是' : '否' }}</span>
</template>
<template v-slot="{ row }" v-else-if="column.prop === 'instanceState'">
<enum-tag :value="row.instanceState" name="ProcessInstanceStateEnum" label_key="message" size="small"></enum-tag>
</template>
</el-table-column>
<el-table-column label="操作" fixed="right" width="300px">
<template v-slot="{row}">
<el-table-column label="操作" fixed="right" width="220">
<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>
<el-button v-if="!row._isBatchChild && row.taskState === 10" @click="onAudit(row)" size="mini" type="primary">审核</el-button>
<el-button v-if="!row._isBatchChild && canRevoke(row)" :loading="revokeLoading" :disabled="revokeLoading" @click="onRevoke(row)" size="mini" type="danger">撤回</el-button>
</template>
</el-table-column>
</el-table>
@@ -80,12 +145,13 @@ layout("/layouts/platform.html"){
<info ref="infoRef">
<div v-if="showApprovalForm">
<div class="process-title">
{{formData.taskName}}
{{ 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']}]">
<el-form :model="formData" ref="formRef" :rules="formRules" label-width="0" 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>
@@ -105,28 +171,30 @@ layout("/layouts/platform.html"){
<script nonce="${cspNonce!}">
<!--#include('../common/applyInfo.js'){}#-->
new Vue({
el: "#app",
el: '#app',
store,
mixins: [initTableMixins],
components: {
"info": siteCugApplyInfo,
info: siteCugApplyInfo,
},
data() {
return {
pageForm: {
approval: false
approval: false,
},
rawTableData: [],
expandedBatchKeys: {},
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: '流程状态'},
{ prop: 'siteName', label: '场地名称', sortable: 'custom' },
{ prop: 'applyUserName', label: '预约人', sortable: 'custom' },
{ prop: 'applyLoginName', label: '工号', sortable: 'custom' },
{ prop: 'yearlyBatch', label: '是否批量预约', sortable: 'custom', width: 130 },
{ prop: 'reserveTargetName', label: '预约单位', sortable: 'custom' },
{ prop: 'reserveStartTime', label: '开始时间', sortable: 'custom' },
{ prop: 'reserveEndTime', label: '结束时间', sortable: 'custom' },
{ prop: 'applyMobile', label: '联系方式', sortable: 'custom' },
{ prop: 'curTaskName', label: '当前节点', sortable: 'custom' },
{ prop: 'instanceState', label: '流程状态', sortable: 'custom', width: 120 },
],
typeOptions: [],
siteOptions: [],
@@ -138,6 +206,60 @@ layout("/layouts/platform.html"){
}
},
methods: {
normalizeBatchFlag(row) {
return row && (row.yearlyBatch === 1 || row.yearlyBatch === '1' || row.yearlyBatch === true)
},
getBatchGroupKey(row) {
if (!this.normalizeBatchFlag(row)) {
return 'single_' + row.id
}
const batchNo = row.backOption || row.id
return 'batch_' + batchNo + '_' + (row.applyUserName || '') + '_' + (row.siteName || '')
},
buildTableData(rows) {
const groups = new Map()
;(rows || []).forEach(row => {
const key = this.getBatchGroupKey(row)
if (!groups.has(key)) {
groups.set(key, [])
}
groups.get(key).push({ ...row })
})
const displayRows = []
groups.forEach((groupRows, key) => {
const sortedRows = groupRows.slice().sort((a, b) => {
return (b.reserveStartTime || '').localeCompare(a.reserveStartTime || '')
})
const parent = { ...sortedRows[0] }
const children = sortedRows.slice(1).map(item => ({
...item,
_isBatchChild: true,
_groupKey: key,
_hasFoldChildren: false,
}))
parent._groupKey = key
parent._isBatchChild = false
parent._hasFoldChildren = children.length > 0
parent._expanded = !!this.expandedBatchKeys[key]
parent._batchCount = sortedRows.length
parent._foldedRecords = children
displayRows.push(parent)
if (parent._expanded) {
displayRows.push(...children)
}
})
return displayRows
},
tableRowClassName({ row }) {
return row && row._isBatchChild ? 'audit-batch-child-row' : ''
},
toggleBatchGroup(row) {
if (!row || !row._groupKey || !row._hasFoldChildren) {
return
}
this.$set(this.expandedBatchKeys, row._groupKey, !this.expandedBatchKeys[row._groupKey])
this.tableData = this.buildTableData(this.rawTableData)
},
canRevoke(row) {
return Number(row.instanceState) === 20
},
@@ -156,7 +278,7 @@ layout("/layouts/platform.html"){
taskName: row.curTaskName,
tf_opinion: '',
yearlyBatch: row.yearlyBatch,
batchAuditCount: row.batchAuditCount,
batchAuditCount: row.batchAuditCount || row._batchCount,
}
this.$refs.infoRef.onOpen(row)
})
@@ -171,14 +293,14 @@ layout("/layouts/platform.html"){
this.$confirm(message, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
type: 'warning',
}).then(() => {
this.auditLoading = true
this.$axios.post('/platform/siteCug/schoolUnionAudit/executeTask', {
data: JSON.stringify({
...this.formData,
submitType: val
})
submitType: val,
}),
}).then((res) => {
if (res.code === 0) {
this.$refs.guava.index()
@@ -194,13 +316,13 @@ layout("/layouts/platform.html"){
if (!this.canRevoke(row) || this.revokeLoading) {
return
}
const message = row.yearlyBatch
? ('该记录属于“预约本年”批量预约,撤回后将一并撤回同批次的 ' + (row.batchAuditCount || 0) + ' 条审核任务,是否继续?')
const message = this.normalizeBatchFlag(row)
? ('该记录属于“预约本年”批量预约,撤回后将一并撤回同批次的 ' + (row.batchAuditCount || row._batchCount || 0) + ' 条审核任务,是否继续?')
: '您确定要撤回吗?'
this.$confirm(message, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'info'
type: 'info',
}).then(() => {
this.revokeLoading = true
this.$axios.post('/platform/siteCug/schoolUnionAudit/revokeTask', { taskId: row.taskId, applyId: row.id }).then((res) => {
@@ -223,12 +345,21 @@ layout("/layouts/platform.html"){
this.siteOptions = res.data
})
},
pageData() {
this.$axios.post(loc() + '/pageData', this.pageForm).then((res) => {
if (res.code === 0) {
this.rawTableData = Array.isArray(res.data.list) ? res.data.list : []
this.tableData = this.buildTableData(this.rawTableData)
this.pageForm.totalCount = res.data.totalCount
}
})
},
},
async created() {
created() {
this.querySiteType()
this.querySites()
this.pageData()
}
},
})
</script>
@@ -1,6 +1,45 @@
<!--#
layout("/layouts/platform.html"){
#-->
<style>
.query-row {
display: flex;
align-items: flex-start;
margin-bottom: 12px;
}
.query-row-title {
width: 120px;
min-width: 120px;
line-height: 32px;
}
.query-row-content {
flex: 1;
display: flex;
flex-wrap: wrap;
align-items: flex-start;
}
.query-row-content-tag {
display: flex;
flex-wrap: wrap;
align-items: flex-start;
gap: 8px;
}
.query-tag-item {
margin-right: 0;
cursor: pointer;
}
.query-tag-actions {
display: inline-flex;
align-items: center;
gap: 10px;
line-height: 32px;
}
</style>
<div id="app" v-cloak>
<guava ref="guava">
<template>
@@ -40,6 +79,56 @@ layout("/layouts/platform.html"){
</search-item>
</search>
</el-card>
<el-card shadow="never">
<el-row type="flex" align="middle" class="query-row query-row-tag">
<el-col class="query-row-title">人员分类:</el-col>
<el-col class="query-row-content query-row-content-tag">
<el-tag
class="query-tag-item"
v-for="item in aidFundMemberUserTypeOptions"
:key="item.code"
:effect="pageForm.aidFundMemberUserTypes.includes(item.code)?'dark':'plain'"
@click="tagClick('aidFundMemberUserTypes',item.code)">
{{ item.name }}
</el-tag>
<span class="query-tag-actions">
<el-link type="danger"
v-if="aidFundMemberUserTypeOptions.length&&pageForm.aidFundMemberUserTypes.length"
:underline="false"
@click="pageForm.aidFundMemberUserTypes=[];doSearch()">清空
</el-link>
<el-link :underline="false"
@click="pageForm.aidFundMemberUserTypes=aidFundMemberUserTypeOptions.map(p=>p.code);doSearch()"
type="success">全部
</el-link>
</span>
</el-col>
</el-row>
<el-row type="flex" align="middle" class="query-row query-row-tag">
<el-col class="query-row-title">人员属性:</el-col>
<el-col class="query-row-content query-row-content-tag">
<el-tag class="query-tag-item"
v-for="item in userAttributeOptions"
:key="item.code"
:effect="pageForm.userAttributes.includes(item.code)?'dark':'plain'"
@click="tagClick('userAttributes',item.code)">
{{ item.name }}
</el-tag>
<span class="query-tag-actions">
<el-link type="danger"
v-if="userAttributeOptions.length&&pageForm.userAttributes.length"
:underline="false"
@click="pageForm.userAttributes=[];doSearch()">清空
</el-link>
<el-link :underline="false"
@click="pageForm.userAttributes=userAttributeOptions.map(p=>p.code);doSearch()"
type="success">全部
</el-link>
</span>
</el-col>
</el-row>
</el-card>
<el-card shadow="never" class="mt10">
<table-tool label="会员分析"></table-tool>
@@ -122,9 +211,13 @@ layout("/layouts/platform.html"){
},
unions: [],
units: [],
aidFundMemberUserTypeOptions: [],
userAttributeOptions: [],
pageForm: {
currentYear: moment().format("YYYY"),
queryType: "fgh"
queryType: "fgh",
userAttributes: [],
aidFundMemberUserTypes: []
},
fullTableData: []
},
@@ -146,15 +239,42 @@ layout("/layouts/platform.html"){
this.units = data
})
}
this.$businessTool.getDictOptions("AIDFUND_MEMBER_USER_TYPE").then((data) => {
this.aidFundMemberUserTypeOptions = data
})
this.$businessTool.getDictOptions("USER_ATTRIBUTE").then((data) => {
this.userAttributeOptions = data
})
},
pageData() {
this.$axios.post(loc() + "/pageData", this.pageForm).then((res) => {
// 统计接口按 JSON 字符串接收多选条件,这里统一在提交前做转换,保证人员分类和人员属性都能正确过滤。
const pageForm = clone(this.pageForm)
pageForm.aidFundMemberUserTypes = JSON.stringify(this.pageForm.aidFundMemberUserTypes)
pageForm.userAttributes = JSON.stringify(this.pageForm.userAttributes)
this.$axios.post(loc() + "/pageData", pageForm).then((res) => {
if (res.code === 0) {
this.tableData = res.data
this.pageForm.totalCount = res.data.totalCount
}
})
}
},
tagClick(key,code) {
if (key === "userAttributes") {
if (this.pageForm.userAttributes.includes(code)) {
this.pageForm.userAttributes.splice(this.pageForm.userAttributes.indexOf(code), 1)
} else {
this.pageForm.userAttributes.push(code)
}
}
if (key === "aidFundMemberUserTypes") {
if (this.pageForm.aidFundMemberUserTypes.includes(code)) {
this.pageForm.aidFundMemberUserTypes.splice(this.pageForm.aidFundMemberUserTypes.indexOf(code), 1)
} else {
this.pageForm.aidFundMemberUserTypes.push(code)
}
}
this.doSearch()
},
},
created() {
this.initData()
@@ -3,24 +3,40 @@ layout("/layouts/platform.html"){
#-->
<style> .query-row {
display: flex;
align-items: center;
margin-bottom: 10px;
align-items: flex-start;
margin-bottom: 12px;
}
.query-row-title {
width: 120px;
min-width: 120px;
line-height: 32px;
}
.query-row-content {
flex: 1;
display: flex;
flex-wrap: wrap;
align-items: center;
align-items: flex-start;
}
.query-row-content-tag {
flex: 1;
display: flex;
flex-wrap: wrap;
align-items: flex-start;
gap: 8px;
}
.query-tag-item {
margin-right: 0;
cursor: pointer;
}
.query-tag-actions {
display: inline-flex;
align-items: center;
gap: 10px;
line-height: 32px;
}
</style>
<div id="app" v-cloak>
@@ -65,27 +81,51 @@ layout("/layouts/platform.html"){
</el-card>
<el-card shadow="never" class="mt10">
<el-row type="flex" align="middle" class="query-row query-row-tag">
<el-col class="query-row-title">人员分类:</el-col>
<el-col class="query-row-content query-row-content-tag">
<el-tag
class="query-tag-item"
v-for="item in aidFundMemberUserTypeOptions"
:key="item.code"
:effect="pageForm.aidFundMemberUserTypes.includes(item.code)?'dark':'plain'"
@click="tagClick('aidFundMemberUserTypes',item.code)">
{{ item.name }}
</el-tag>
<span class="query-tag-actions">
<el-link type="danger"
v-if="aidFundMemberUserTypeOptions.length&&pageForm.aidFundMemberUserTypes.length"
:underline="false"
@click="pageForm.aidFundMemberUserTypes=[];doSearch()">清空
</el-link>
<el-link :underline="false"
@click="pageForm.aidFundMemberUserTypes=aidFundMemberUserTypeOptions.map(p=>p.code);doSearch()"
type="success">全部
</el-link>
</span>
</el-col>
</el-row>
<el-row type="flex" align="middle" class="query-row query-row-tag">
<el-col class="query-row-title">人员属性:</el-col>
<el-col class="query-row-content query-row-content-tag">
<el-tag style="margin-right: 10px;cursor: pointer"
<el-tag class="query-tag-item"
v-for="item in userAttributeOptions"
:key="item.code"
:type="item.name"
:effect="pageForm.userAttributes.includes(item.code)?'dark':'plain'"
@click="tagClick(item.code)">
@click="tagClick('userAttributes',item.code)">
{{ item.name }}
</el-tag>
<el-link type="danger"
v-if="userAttributeOptions.length&&pageForm.userAttributes.length"
:underline="false"
@click="pageForm.userAttributes=[];doSearch()">清空
</el-link>
<el-link :underline="false"
@click="pageForm.userAttributes=userAttributeOptions.map(p=>p.code);doSearch()"
type="success">全部
</el-link>
<span class="query-tag-actions">
<el-link type="danger"
v-if="userAttributeOptions.length&&pageForm.userAttributes.length"
:underline="false"
@click="pageForm.userAttributes=[];doSearch()">清空
</el-link>
<el-link :underline="false"
@click="pageForm.userAttributes=userAttributeOptions.map(p=>p.code);doSearch()"
type="success">全部
</el-link>
</span>
</el-col>
</el-row>
</el-card>
@@ -137,8 +177,10 @@ layout("/layouts/platform.html"){
pageForm: {
currentYear: moment().format("YYYY"),
queryType: "fgh",
userAttributes: []
}
userAttributes: [],
aidFundMemberUserTypes: []
},
aidFundMemberUserTypeOptions: [],
},
methods: {
doExport() {},
@@ -158,20 +200,34 @@ layout("/layouts/platform.html"){
this.units = data
})
}
this.$businessTool.getDictOptions("USER_ATTRIBUTE").then((data) => {
this.userAttributeOptions = data
})
this.$businessTool.getDictOptions("AIDFUND_MEMBER_USER_TYPE").then((data) => {
this.aidFundMemberUserTypeOptions = data
})
this.$businessTool.getDictOptions("USER_ATTRIBUTE").then((data) => {
this.userAttributeOptions = data
})
},
tagClick(code) {
if (this.pageForm.userAttributes.includes(code)) {
this.pageForm.userAttributes.splice(this.pageForm.userAttributes.indexOf(code), 1)
} else {
this.pageForm.userAttributes.push(code)
}
tagClick(key,code) {
if (key === "userAttributes") {
if (this.pageForm.userAttributes.includes(code)) {
this.pageForm.userAttributes.splice(this.pageForm.userAttributes.indexOf(code), 1)
} else {
this.pageForm.userAttributes.push(code)
}
}
if (key === "aidFundMemberUserTypes") {
if (this.pageForm.aidFundMemberUserTypes.includes(code)) {
this.pageForm.aidFundMemberUserTypes.splice(this.pageForm.aidFundMemberUserTypes.indexOf(code), 1)
} else {
this.pageForm.aidFundMemberUserTypes.push(code)
}
}
this.doSearch()
},
pageData() {
const pageForm = clone(this.pageForm)
// 统计接口按 JSON 字符串接收多选条件,这里把人员分类和人员属性统一成同一提交口径。
pageForm.aidFundMemberUserTypes = JSON.stringify(this.pageForm.aidFundMemberUserTypes)
pageForm.userAttributes = JSON.stringify(this.pageForm.userAttributes)
this.$axios.post(loc() + "/pageData", pageForm).then((res) => {
if (res.code === 0) {
@@ -61,6 +61,14 @@ layout("/layouts/platform.html"){
<el-option :key="item.id" :label="item.name" :value="item.id" v-for="item in unitOptions"></el-option>
</el-select>
</search-item>
<search-item label="人员分类">
<dict-select v-model="pageForm.aidFundMemberUserType" placeholder="请选择人员分类" @change="doSearch"
code="AIDFUND_MEMBER_USER_TYPE"></dict-select>
</search-item>
<search-item label="人员属性">
<dict-select v-model="pageForm.userAttribute" placeholder="请选择人员属性" @change="doSearch"
code="USER_ATTRIBUTE"></dict-select>
</search-item>
</search>
</el-card>
@@ -109,6 +109,14 @@ layout("/layouts/platform.html"){
v-model="pageForm.userStates"
></dict-select>
</search-item>
<search-item label="人员分类">
<dict-select v-model="pageForm.aidFundMemberUserType" placeholder="请选择人员分类" @change="doSearch"
code="AIDFUND_MEMBER_USER_TYPE"></dict-select>
</search-item>
<search-item label="人员属性">
<dict-select v-model="pageForm.userAttribute" placeholder="请选择人员属性" @change="doSearch"
code="USER_ATTRIBUTE"></dict-select>
</search-item>
<!-- <search-item label="所选福利:">-->
<!-- <el-select clearable placeholder="请选择所选福利" style="width: 100%" v-model="pageForm.optionId">-->
File diff suppressed because it is too large Load Diff
@@ -4,9 +4,304 @@ layout("/layouts/platform_h5.html"){
<style scoped>
.page-container {
padding-bottom: 84px;
background: #f7f8fa;
padding: 12px 12px 96px;
background: #f5f7fb;
min-height: calc(100vh - 46px);
box-sizing: border-box;
}
.panel-card {
margin-bottom: 12px;
overflow: hidden;
border-radius: 22px;
background: #ffffff;
box-shadow:
0 2px 6px rgba(15, 23, 42, 0.04),
0 12px 28px rgba(15, 23, 42, 0.08),
0 0 0 1px rgba(226, 232, 240, 0.95);
}
.notice-card__header {
width: 100%;
padding: 16px;
border: none;
background: #ffffff;
display: flex;
align-items: center;
justify-content: space-between;
color: #0f172a;
font-size: 16px;
font-weight: 700;
line-height: 1.4;
box-sizing: border-box;
}
.notice-card__icon {
color: #64748b;
font-size: 16px;
transition: transform 0.2s ease;
}
.notice-card__icon--expanded {
transform: rotate(180deg);
}
.notice-card__body {
padding: 0 16px 14px;
border-top: 1px solid #eef2f7;
}
.notice-list {
display: flex;
flex-direction: column;
gap: 10px;
}
.notice-item {
padding: 10px 12px;
border-radius: 14px;
background: #f8fafc;
}
.notice-item__label {
color: #334155;
font-size: 13px;
font-weight: 600;
line-height: 1.5;
}
.notice-item__value {
margin-top: 4px;
color: #64748b;
font-size: 12px;
line-height: 1.7;
}
.disabled-time-list {
display: flex;
flex-direction: column;
gap: 6px;
}
.disabled-time-item {
padding: 8px 10px;
border-radius: 10px;
background: #ffffff;
color: #475569;
line-height: 1.7;
}
.form-container {
margin: 0;
}
.section-tip {
padding: 0 16px 14px;
color: #94a3b8;
font-size: 12px;
line-height: 1.6;
}
.selection-summary {
margin: 0 16px 14px;
padding: 10px 12px;
border-radius: 12px;
background: rgba(37, 99, 235, 0.08);
color: #2563eb;
font-size: 12px;
line-height: 1.7;
}
.batch-panel {
padding: 0 16px 14px;
}
.batch-summary {
margin-top: 10px;
padding: 10px 12px;
border-radius: 12px;
background: rgba(37, 99, 235, 0.08);
color: #2563eb;
font-size: 12px;
line-height: 1.7;
}
.availability-popup {
height: 82vh;
display: flex;
flex-direction: column;
background: #f8fafc;
}
.availability-popup__header {
display: flex;
align-items: flex-start;
justify-content: space-between;
padding: 16px;
background: #ffffff;
border-bottom: 1px solid #eef2f7;
}
.availability-popup__title {
color: #0f172a;
font-size: 17px;
font-weight: 700;
line-height: 1.4;
}
.availability-popup__subtitle {
margin-top: 4px;
color: #64748b;
font-size: 12px;
line-height: 1.6;
}
.availability-popup__close {
color: #94a3b8;
font-size: 18px;
line-height: 1;
}
.availability-popup__tabs {
display: flex;
gap: 8px;
padding: 12px 16px 0;
overflow-x: auto;
background: #f8fafc;
}
.availability-tab {
min-width: 72px;
height: 34px;
padding: 0 14px;
border: 1px solid #cbd5e1;
border-radius: 999px;
background: #ffffff;
color: #475569;
font-size: 13px;
font-weight: 500;
box-sizing: border-box;
}
.availability-tab--active {
border-color: #2563eb;
background: #2563eb;
color: #ffffff;
box-shadow: 0 8px 16px rgba(37, 99, 235, 0.22);
}
.availability-popup__legend {
display: flex;
flex-wrap: wrap;
gap: 12px;
padding: 12px 16px 0;
color: #64748b;
font-size: 12px;
line-height: 1.5;
}
.availability-legend__item {
display: inline-flex;
align-items: center;
gap: 6px;
}
.availability-legend__dot {
width: 10px;
height: 10px;
border-radius: 999px;
display: inline-block;
}
.availability-legend__dot--available {
background: #22c55e;
}
.availability-legend__dot--reserved {
background: #9ca3af;
}
.availability-legend__dot--selected {
background: #2563eb;
}
.availability-legend__dot--closed {
background: #e5e7eb;
}
.availability-popup__body {
flex: 1;
overflow-y: auto;
padding: 16px;
box-sizing: border-box;
}
.slot-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 10px;
}
.slot-block {
min-height: 48px;
padding: 0 12px;
border: 1px solid #dbe4ee;
border-radius: 14px;
background: #ffffff;
color: #475569;
display: inline-flex;
align-items: center;
justify-content: center;
text-align: center;
font-size: 13px;
font-weight: 500;
line-height: 1.4;
box-sizing: border-box;
}
.slot-block--available {
border-color: #86efac;
background: rgba(34, 197, 94, 0.1);
color: #15803d;
}
.slot-block--reserved {
background: #e5e7eb;
color: #6b7280;
}
.slot-block--closed {
background: #f8fafc;
color: #94a3b8;
}
.slot-block--selected {
border-color: #2563eb;
background: #2563eb;
color: #ffffff;
box-shadow: 0 10px 20px rgba(37, 99, 235, 0.24);
}
.availability-popup__footer {
padding: 12px 16px calc(env(safe-area-inset-bottom) + 12px);
background: #ffffff;
border-top: 1px solid #eef2f7;
}
.availability-popup__summary {
margin-bottom: 12px;
color: #475569;
font-size: 12px;
line-height: 1.7;
}
.availability-popup__actions {
display: flex;
gap: 10px;
}
.availability-popup__actions .van-button {
flex: 1;
}
.footer-actions {
@@ -17,17 +312,39 @@ layout("/layouts/platform_h5.html"){
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);
background: rgba(255, 255, 255, 0.96);
backdrop-filter: blur(10px);
box-shadow: 0 -8px 24px rgba(15, 23, 42, 0.08);
}
.footer-actions .van-button {
flex: 1;
height: 44px;
}
.disabled-time-item {
line-height: 20px;
margin-bottom: 4px;
/deep/ .panel-card .van-cell-group {
background: transparent;
}
/deep/ .panel-card .van-cell-group__title {
padding: 16px 16px 8px;
margin: 0;
color: #0f172a;
font-size: 16px;
font-weight: 700;
line-height: 1.4;
background: #ffffff;
}
/deep/ .panel-card .van-cell {
padding-top: 12px;
padding-bottom: 12px;
}
/deep/ .panel-card .van-field__label,
/deep/ .panel-card .van-cell__title {
color: #334155;
font-weight: 600;
}
/deep/ .direction-column-cell .van-cell__value {
@@ -40,46 +357,43 @@ layout("/layouts/platform_h5.html"){
<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 class="panel-card">
<button type="button" class="notice-card__header" @click="noticeCollapsed = !noticeCollapsed">
<span>预约须知</span>
<van-icon :class="['notice-card__icon', noticeCollapsed ? '' : 'notice-card__icon--expanded']" name="arrow-down"></van-icon>
</button>
<div v-if="!noticeCollapsed" class="notice-card__body">
<div class="notice-list">
<div class="notice-item">
<div class="notice-item__label">可预约日期</div>
<div class="notice-item__value">仅限工作日</div>
</div>
<div class="notice-item">
<div class="notice-item__label">节假日限制</div>
<div class="notice-item__value">{{ timeLimitConfig.filterHolidays ? '节假日不可预约' : '不排除节假日' }}</div>
</div>
<div class="notice-item">
<div class="notice-item__label">禁用时段</div>
<div class="notice-item__value">
<div v-if="timeLimitConfig.notApplyTimeList.length" class="disabled-time-list">
<div
class="disabled-time-item"
v-for="(item, index) in timeLimitConfig.notApplyTimeList"
:key="item.date + item.startTime + item.endTime + index">
{{ item.date }} {{ item.startTime }} - {{ item.endTime }}
</div>
</div>
<div v-else>暂无禁用时段</div>
</div>
</div>
<div v-else>暂无禁用时段</div>
</template>
</van-cell>
</van-cell-group>
</div>
</div>
</div>
<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
<div class="panel-card">
<van-cell-group title="申请信息">
<van-field
:value="reserveTypeText"
label="预约类型"
name="reserveType"
@@ -88,18 +402,24 @@ layout("/layouts/platform_h5.html"){
is-link
required
placeholder="请选择预约类型"
@click="showReserveTypePicker = true"
@click="openReserveTypePicker"
:rules="[{ required: true, message: '请选择预约类型' }]">
</van-field>
<van-field
</van-field>
<van-field v-model="formData.applyUserName" label="预约人" name="applyUserName" readonly
:rules="[{ required: true, message: '请确认预约人' }]"></van-field>
<van-field v-model="formData.applyLoginName" label="工号" name="applyLoginName" readonly
:rules="[{ required: true, message: '请确认工号' }]"></van-field>
<van-field v-model="formData.applyUnitName" label="所属单位" name="applyUnitName" readonly
:rules="[{ required: true, message: '请确认所属单位' }]"></van-field>
<van-field
v-if="formData.reserveType === 'union'"
v-model="formData.applyUnionName"
label="分工会"
name="applyUnionName"
readonly
placeholder="自动读取当前登录人的分工会">
</van-field>
<van-field
</van-field>
<van-field
v-if="formData.reserveType === 'club'"
v-model="formData.clubName"
label="协会"
@@ -111,21 +431,58 @@ layout("/layouts/platform_h5.html"){
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-field>
<van-field v-model="formData.applyMobile" label="联系电话" name="applyMobile" type="tel" maxlength="11"
required placeholder="请输入联系电话"
:rules="[{ required: true, message: '请输入联系电话' }]"></van-field>
<van-field
:value="scheduleDate"
label="预约日期"
name="scheduleDate"
readonly
clickable
is-link
required
placeholder="请选择预约日期"
@click="openScheduleDatePicker">
</van-field>
<van-field
:value="selectionSummary || ''"
label="预约时段"
name="timeSelection"
readonly
clickable
is-link
required
:placeholder="scheduleDate ? '请选择预约时段' : '请先选择预约日期'"
@click="openAvailabilityPopup">
</van-field>
<div v-if="selectionSummary" class="selection-summary">当前已选:{{ selectionSummary }}</div>
<van-cell title="批量预约">
<template #default>
<van-switch v-model="yearlyReserve" size="22px"></van-switch>
</template>
</van-cell>
<div v-if="yearlyReserve" class="batch-panel">
<div class="section-tip">将自动预约截止日期内每周同一时段,默认到本月底。</div>
<van-field
:value="formData.yearlyReserveEndDate"
label="截至日期"
name="yearlyReserveEndDate"
readonly
clickable
is-link
required
placeholder="请选择批量预约截止日期"
@click="openYearlyEndDatePicker">
</van-field>
<div v-if="batchReserveSummary" class="batch-summary">{{ batchReserveSummary }}</div>
</div>
<van-field v-model="formData.applyCause" label="预约事由" name="applyCause" required rows="4" autosize
type="textarea" maxlength="1000" show-word-limit placeholder="请输入预约事由"
:rules="[{ required: true, message: '请输入预约事由' }]"></van-field>
</van-cell-group>
</div>
</van-form>
</div>
@@ -144,17 +501,76 @@ layout("/layouts/platform_h5.html"){
@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 v-model="showScheduleDatePicker" position="bottom" round>
<van-datetime-picker
v-model="schedulePickerDate"
type="date"
title="选择预约日期"
:min-date="scheduleMinDate"
:max-date="scheduleMaxDate"
@confirm="onScheduleDateConfirm"
@cancel="showScheduleDatePicker = false">
</van-datetime-picker>
</van-popup>
<van-popup v-model="showAvailabilityPopup" position="bottom" round class="availability-popup">
<div class="availability-popup__header">
<div>
<div class="availability-popup__title">选择预约时段</div>
<div class="availability-popup__subtitle">{{ scheduleDate || '请选择预约日期' }} {{ reserveModeText }}</div>
</div>
<van-icon class="availability-popup__close" name="cross" @click="showAvailabilityPopup = false"></van-icon>
</div>
<div v-if="timeGroupTabs.length" class="availability-popup__tabs">
<button
v-for="tab in timeGroupTabs"
:key="tab.key"
type="button"
:class="['availability-tab', activeTimeGroupKey === tab.key ? 'availability-tab--active' : '']"
@click="activeTimeGroupKey = tab.key">
{{ tab.label }}
</button>
</div>
<div class="availability-popup__legend">
<span class="availability-legend__item"><i class="availability-legend__dot availability-legend__dot--available"></i>可预约</span>
<span class="availability-legend__item"><i class="availability-legend__dot availability-legend__dot--reserved"></i>已预约</span>
<span class="availability-legend__item"><i class="availability-legend__dot availability-legend__dot--selected"></i>已选择</span>
<span class="availability-legend__item"><i class="availability-legend__dot availability-legend__dot--closed"></i>不可预约</span>
</div>
<div class="availability-popup__body">
<van-loading v-if="availabilityLoading" vertical>时段加载中</van-loading>
<van-empty v-else-if="!availabilityBlocks.length" description="当前日期暂无可选时段"></van-empty>
<van-empty v-else-if="!displayedAvailabilityBlocks.length" description="当前标签暂无可选时段"></van-empty>
<div v-else class="slot-grid">
<button
v-for="block in displayedAvailabilityBlocks"
:key="block.key"
type="button"
:class="getBlockClass(block)"
@click="onAvailabilityBlockClick(block)">
<span>{{ block.label }}</span>
</button>
</div>
</div>
<div class="availability-popup__footer">
<div class="availability-popup__summary">{{ selectionSummary || '请选择一个或多个连续时段,不可跨越间隔选择。' }}</div>
<div class="availability-popup__actions">
<van-button plain round type="default" @click="clearSelection(true)">清空</van-button>
<van-button round type="primary" color="#246fb4" @click="confirmAvailabilitySelection">确定</van-button>
</div>
</div>
</van-popup>
<van-popup v-model="showYearlyEndDatePicker" position="bottom" round>
<van-datetime-picker
v-model="yearlyReservePickerDate"
type="date"
title="选择批量预约截止日期"
:min-date="yearlyReserveMinDate"
:max-date="yearlyReserveMaxDate"
@confirm="onYearlyReserveEndDateConfirm"
@cancel="showYearlyEndDatePicker = false">
</van-datetime-picker>
</van-popup>
</div>
@@ -3,13 +3,311 @@ layout("/layouts/platform_h5.html"){
#-->
<style scoped>
.site-page {
background: #f5f7fb;
min-height: 100vh;
}
.toolbar-wrap {
padding: 10px 10px 8px;
background: #f5f7fb;
box-shadow: 0 8px 18px rgba(15, 23, 42, 0.04);
}
.search-toolbar {
display: flex;
align-items: center;
gap: 8px;
}
.search-panel {
flex: 1;
min-width: 0;
border-radius: 16px;
overflow: hidden;
background: #ffffff;
box-shadow: 0 4px 14px rgba(15, 23, 42, 0.06);
}
.search-panel /deep/ .van-search {
padding: 0;
background: transparent;
}
.search-panel /deep/ .van-search__content {
height: 42px;
background: #ffffff;
border-radius: 16px;
}
.toolbar-btn,
.toolbar-icon-btn {
height: 42px;
border: none;
border-radius: 14px;
background: #ffffff;
box-shadow: 0 4px 14px rgba(15, 23, 42, 0.08);
display: inline-flex;
align-items: center;
justify-content: center;
color: #2563eb;
box-sizing: border-box;
}
.toolbar-btn {
min-width: 62px;
padding: 0 14px;
font-size: 14px;
font-weight: 600;
}
.toolbar-icon-btn {
width: 42px;
font-size: 18px;
}
.selected-type-bar {
display: flex;
align-items: center;
justify-content: space-between;
margin-top: 8px;
padding: 8px 12px;
border-radius: 14px;
background: rgba(37, 99, 235, 0.08);
color: #2563eb;
font-size: 13px;
line-height: 1.4;
}
.selected-type-text {
min-width: 0;
flex: 1;
padding-right: 12px;
font-weight: 500;
}
.selected-type-clear {
border: none;
padding: 0;
background: transparent;
color: #2563eb;
font-size: 13px;
font-weight: 600;
}
.type-popup {
width: calc(100vw - 48px);
max-width: 320px;
padding: 16px 16px 14px;
box-sizing: border-box;
}
.type-popup__header {
color: #111827;
font-size: 16px;
font-weight: 700;
line-height: 1.4;
}
.type-popup__list {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-top: 14px;
}
.type-popup__item {
min-width: calc(50% - 5px);
min-height: 38px;
padding: 0 12px;
border: 1px solid #dbe4f0;
border-radius: 12px;
background: #ffffff;
color: #475569;
display: inline-flex;
align-items: center;
justify-content: space-between;
gap: 8px;
font-size: 13px;
line-height: 1.2;
box-sizing: border-box;
}
.type-popup__item--active {
border-color: #2563eb;
background: rgba(37, 99, 235, 0.08);
color: #2563eb;
font-weight: 600;
}
.site-card-header {
display: flex;
gap: 12px;
align-items: flex-start;
margin-bottom: 10px;
}
.site-thumb {
width: 92px;
height: 92px;
flex-shrink: 0;
overflow: hidden;
border-radius: 14px;
background: linear-gradient(135deg, #e5eefb 0%, #f5f9ff 100%);
box-shadow: inset 0 0 0 1px rgba(37, 99, 235, 0.06);
}
.site-thumb img,
.site-thumb .van-image {
width: 100%;
height: 100%;
display: block;
}
.site-thumb-empty {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
color: #94a3b8;
font-size: 12px;
text-align: center;
line-height: 1.4;
padding: 8px;
box-sizing: border-box;
}
.site-head-main {
min-width: 0;
flex: 1;
padding-top: 2px;
}
.site-name {
color: #111827;
font-size: 18px;
font-weight: 700;
line-height: 1.35;
word-break: break-all;
}
.site-address {
display: flex;
align-items: center;
gap: 4px;
margin-top: 8px;
color: #6b7280;
font-size: 13px;
line-height: 1.4;
}
.site-address i {
color: #9ca3af;
font-size: 12px;
}
.site-tags {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin-top: 10px;
}
.site-tag {
display: inline-flex;
align-items: center;
justify-content: center;
min-height: 24px;
padding: 0 8px;
border-radius: 8px;
border: 1px solid #93c5fd;
color: #2563eb;
font-size: 12px;
line-height: 1;
background: #ffffff;
box-sizing: border-box;
}
.site-tag--solid {
border-color: #2563eb;
background: #2563eb;
color: #ffffff;
}
.site-tag--muted {
border-color: #cbd5e1;
color: #64748b;
}
.site-timeline {
margin-top: 4px;
}
.timeline-bar {
display: grid;
grid-template-columns: repeat(48, minmax(0, 1fr));
gap: 1px;
align-items: center;
padding: 0 2px;
}
.timeline-bar__segment {
height: 4px;
border-radius: 999px;
background: #e5e7eb;
}
.timeline-bar__segment--available {
background: #22c55e;
}
.timeline-bar__segment--reserved {
background: #9ca3af;
}
.timeline-scale {
display: flex;
justify-content: space-between;
margin-top: 8px;
padding: 0 2px;
color: #64748b;
font-size: 11px;
line-height: 1;
}
/deep/ .table-list-container {
margin-top: 8px;
padding: 0 8px 12px;
}
/deep/ .table-list-container .table-list-item {
margin-bottom: 12px;
padding: 14px;
border: none;
overflow: hidden;
border-radius: 22px;
background: #ffffff;
box-shadow:
0 2px 6px rgba(15, 23, 42, 0.05),
0 10px 24px rgba(15, 23, 42, 0.08),
0 0 0 1px rgba(226, 232, 240, 0.9);
}
/deep/ .table-list-container .table-list-item .item-actions {
margin-top: 12px;
padding-top: 10px;
border-top: 1px solid #eef2f7;
}
</style>
<div id="app">
<div id="app" class="site-page">
<van-nav-bar title="场馆预约" left-text="返回" left-arrow @click-left="historyBack" fixed placeholder></van-nav-bar>
<van-sticky offset-top="46px">
<div class="toolbar-wrap">
<div class="search-toolbar">
<div class="search-panel">
<van-search
v-model="pageForm.searchKeyword"
:show-action="false"
@@ -17,18 +315,54 @@ layout("/layouts/platform_h5.html"){
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-search>
</div>
<button type="button" class="toolbar-btn" @click="doSearch">搜索</button>
<button type="button" class="toolbar-icon-btn" @click="typePopupVisible = true">
<van-icon name="apps-o"></van-icon>
</button>
</div>
<div v-if="selectedTypeText" class="selected-type-bar">
<div class="selected-type-text">已选场地类型:{{ selectedTypeText }}</div>
<button type="button" class="selected-type-clear" @click="selectType(null)">清除</button>
</div>
</div>
</van-sticky>
<table-list api="/platform/siteCug/apply/pageData" :page_form.sync="pageForm" ref="tableListRef" title="name" @ready="onReady">
<table-list api="/platform/siteCug/apply/pageData" :page_form.sync="pageForm" ref="tableListRef" @ready="onReady">
<template #header="{ row }">
<div class="site-card-header">
<div class="site-thumb" @click="previewSitePhoto(row)">
<van-image v-if="row.sitePhoto" :src="row.sitePhoto" fit="cover"></van-image>
<div v-else class="site-thumb-empty">暂无场地照片</div>
</div>
<div class="site-head-main">
<div class="site-name">{{ row.name || '-' }}</div>
<div class="site-address">
<i class="fa fa-map-marker"></i>
<span>{{ row.address || '-' }}</span>
</div>
<div class="site-tags">
<span class="site-tag site-tag--solid">{{ row.typeName || '活动场地' }}</span>
<span class="site-tag">容{{ row.maxNum || '-' }}</span>
<span :class="['site-tag', row.state ? '' : 'site-tag--muted']">{{ row.state ? '已开启' : '未开启' }}</span>
</div>
</div>
</div>
</template>
<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>
<div class="site-timeline">
<div class="timeline-bar">
<span
v-for="(segment, index) in row.timelineSegments || []"
:key="row.id + '-segment-' + index"
:class="['timeline-bar__segment', 'timeline-bar__segment--' + (segment.status || 'closed')]">
</span>
</div>
<div class="timeline-scale">
<span v-for="mark in timeMarks" :key="row.id + '-mark-' + mark">{{ mark }}</span>
</div>
</div>
</template>
<template #actions="{ row }">
<div class="action-btn" @click="onView(row)">
@@ -42,6 +376,21 @@ layout("/layouts/platform_h5.html"){
</template>
</table-list>
<van-popup v-model="typePopupVisible" round class="type-popup">
<div class="type-popup__header">选择场地类型</div>
<div class="type-popup__list">
<button
v-for="item in typeFilterOptions"
:key="String(item.value)"
type="button"
:class="['type-popup__item', pageForm.type === item.value ? 'type-popup__item--active' : '']"
@click="selectType(item.value)">
<span>{{ item.text }}</span>
<van-icon v-if="pageForm.type === item.value" name="success"></van-icon>
</button>
</div>
</van-popup>
<info ref="infoRef"></info>
</div>
@@ -62,11 +411,25 @@ layout("/layouts/platform_h5.html"){
searchKeyword: '',
type: null,
},
typeOptions: [],
typeFilterOptions: [],
typePopupVisible: false,
timeMarks: ['0', '2', '4', '6', '8', '10', '12', '14', '16', '18', '20', '22'],
}
},
computed: {
selectedTypeText() {
const current = this.typeFilterOptions.find((item) => item.value === this.pageForm.type)
return current && current.value !== null ? current.text : ''
},
},
methods: {
historyBack,
previewSitePhoto(row) {
if (!row || !row.sitePhoto) {
return
}
this.vant.ImagePreview([row.sitePhoto])
},
onView(row) {
this.$refs.infoRef.onOpen(row)
},
@@ -75,14 +438,14 @@ layout("/layouts/platform_h5.html"){
},
async onReady() {
const typeList = await this.querySiteType()
this.typeOptions = [
this.typeFilterOptions = [
{
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
if (this.typeFilterOptions.length > 0) {
this.pageForm.type = this.typeFilterOptions[0].value
this.doSearch()
}
},
@@ -90,6 +453,11 @@ layout("/layouts/platform_h5.html"){
const res = await this.$axios.post('/platform/siteCug/function/type/queryFunctionType')
return Array.isArray(res.data) ? res.data : []
},
selectType(value) {
this.pageForm.type = value
this.typePopupVisible = false
this.doSearch()
},
doSearch() {
this.$nextTick(() => {
this.pageForm.pageNumber = 1
@@ -18,14 +18,14 @@ const siteCugApplyInfoH5 = {
</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.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.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>
@@ -77,29 +77,66 @@ const siteInfo = {
},
},
style: /*language=CSS*/ `
/deep/ .van-action-sheet {
border-top-left-radius: 24px;
border-top-right-radius: 24px;
overflow: hidden;
background: #f8fafc;
}
/deep/ .van-action-sheet__header {
font-size: 17px;
font-weight: 700;
color: #0f172a;
background: #ffffff;
}
/deep/ .detail-container {
padding: 10px 12px 18px;
background: #f8fafc;
}
/deep/ .detail-container .van-cell-group {
margin-bottom: 12px;
overflow: hidden;
border-radius: 18px;
box-shadow: 0 8px 24px rgba(15, 23, 42, 0.06);
}
/deep/ .detail-container .van-cell-group__title {
padding: 12px 16px 8px;
color: #334155;
font-size: 14px;
font-weight: 700;
}
/deep/ .detail-container .van-cell {
min-height: 52px;
}
/deep/ .direction-column-cell .van-cell__value {
white-space: normal;
text-align: left;
}
/deep/ .table-class {
width: 100%;
border-radius: 5px;
border-radius: 10px;
overflow: hidden;
line-height: 1.5rem;
line-height: 1.8rem;
font-size: 13px;
table-layout: fixed;
border-collapse: collapse;
background: #ffffff;
}
/deep/ .table-class th {
background-color: #f2f2f2;
border: 1px solid #dddddd;
padding: 10px 8px;
background-color: #f1f5f9;
border: 1px solid #dbe4ee;
color: #334155;
font-weight: 600;
}
/deep/ .table-class tr {
text-align: center;
border-bottom: 1px solid #dddddd;
border-bottom: 1px solid #dbe4ee;
}
/deep/ .table-class td {
border: 1px solid #dddddd;
padding: 10px 8px;
border: 1px solid #dbe4ee;
color: #475569;
}
`
}
@@ -18,10 +18,8 @@ layout("/layouts/platform_h5.html"){
<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.applyUserName || '-'}}</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>
@@ -73,7 +71,6 @@ layout("/layouts/platform_h5.html"){
}
},
methods: {
historyBack,
async onReady() {
await this.querySiteType()
await this.querySites()
@@ -148,4 +145,4 @@ layout("/layouts/platform_h5.html"){
<!--#
}
#-->
#-->