This commit is contained in:
2026-08-20 16:41:26 +08:00
parent de46a2404d
commit 4803c7320e
41 changed files with 5873 additions and 1075 deletions
@@ -1,6 +1,7 @@
package com.budwk.app.zhgh.democratic.proposal.controller.transact;
import cn.dev33.satoken.annotation.SaCheckPermission;
import cn.dev33.satoken.annotation.SaMode;
import com.budwk.app.base.constant.RoleConstant;
import com.budwk.app.base.page.Pagination;
import com.budwk.app.base.param.ExportTableColumns;
@@ -55,8 +56,26 @@ public class ProposalCaseCheckController {
public void index() {
}
/**
* 打开提案委员会审查手机端列表页。
*/
@At("/h5")
@Ok("beetl:/platform/zhghh5/democratic/proposal/transact/caseCheck/index.html")
@SaCheckPermission("h5.proposal.caseCheck")
public void h5Index() {
}
/**
* 打开提案委员会审查手机端详情及审核页。
*/
@At("/info/h5")
@Ok("beetl:/platform/zhghh5/democratic/proposal/transact/caseCheck/info.html")
@SaCheckPermission("h5.proposal.caseCheck")
public void h5Info() {
}
@At
@SaCheckPermission("proposal.caseCheck")
@SaCheckPermission(value = {"proposal.caseCheck", "h5.proposal.caseCheck"}, mode = SaMode.OR)
@ApiOperation("分页列表")
public Result pageData(@Valid ProposalSearchParam pageForm, boolean approval) {
Sql sql = Sqls.create("""
@@ -129,7 +148,7 @@ public class ProposalCaseCheckController {
}
@At
@SaCheckPermission("proposal.caseCheck")
@SaCheckPermission(value = {"proposal.caseCheck", "h5.proposal.caseCheck"}, mode = SaMode.OR)
@ApiOperation("查询当前提案承办单位")
public Result listReplyUnits(String proposalId) {
return Result.success(proposalCaseCheckService.listReplyUnits(proposalId));
@@ -49,6 +49,12 @@ public class ProposalCommissionerController {
public void h5Index() {
}
@At("/info/h5")
@Ok("beetl:/platform/zhghh5/democratic/proposal/transact/commissioner/info.html")
@SaCheckPermission("h5.proposal.commissioner")
public void h5Info() {
}
@At
@SaCheckPermission("proposal.commissioner")
@@ -67,6 +67,12 @@ public class ProposalDelegationController {
public void h5Index() {
}
@At("/info/h5")
@Ok("beetl:/platform/zhghh5/democratic/proposal/transact/delegation/info.html")
@SaCheckPermission("h5.proposal.delegation")
public void h5Info() {
}
@At
@SaCheckPermission("proposal.delegation")
@@ -54,6 +54,12 @@ public class ProposalFeedbackEvaluationController {
public void h5Index() {
}
@At("/info/h5")
@Ok("beetl:/platform/zhghh5/democratic/proposal/transact/feedbackEvaluation/info.html")
@SaCheckPermission("h5.proposal.feedbackEvaluation")
public void h5Info() {
}
@At
@SaCheckPermission("proposal.feedbackEvaluation")
@@ -107,6 +107,12 @@ public class ProposalMineController {
public void h5Index() {
}
@At("/info/h5")
@Ok("beetl:/platform/zhghh5/democratic/proposal/transact/mine/info.html")
@SaCheckPermission("h5.proposal.mine")
public void h5Info() {
}
@At
@SaCheckPermission("proposal.mine")
@Aop(TransAop.READ_COMMITTED)
@@ -52,6 +52,18 @@ public class ProposalPreAuditController {
public void index() {
}
@At("/h5")
@Ok("beetl:/platform/zhghh5/democratic/proposal/transact/preAudit/index.html")
@SaCheckPermission("proposal.preAudit")
public void h5Index() {
}
@At("/info/h5")
@Ok("beetl:/platform/zhghh5/democratic/proposal/transact/preAudit/info.html")
@SaCheckPermission("h5.proposal.preAudit")
public void h5Info() {
}
@At
@SaCheckPermission("proposal.preAudit")
@@ -74,6 +74,12 @@ public class ProposalSchoolLeaderApprovalController {
public void h5Index() {
}
@At("/info/h5")
@Ok("beetl:/platform/zhghh5/democratic/proposal/transact/schoolLeaderApproval/info.html")
@SaCheckPermission("h5.proposal.schoolLeaderApproval")
public void h5Info() {
}
@At
@SaCheckPermission("proposal.schoolLeaderApproval")
@@ -88,6 +88,12 @@ public class ProposalSecondedController {
public void h5Index() {
}
@At("/info/h5")
@Ok("beetl:/platform/zhghh5/democratic/proposal/transact/seconded/info.html")
@SaCheckPermission("h5.proposal.seconded")
public void h5Info() {
}
@At
@SaCheckPermission("proposal.seconded")
public Result pageData(@Valid ProposalSearchParam pageForm, boolean approval) {
@@ -76,6 +76,12 @@ public class ProposalUnderTakeReplyController {
public void h5Index() {
}
@At("/info/h5")
@Ok("beetl:/platform/zhghh5/democratic/proposal/transact/unitReply/info.html")
@SaCheckPermission("h5.proposal.unitReply")
public void h5Info() {
}
@At
@SaCheckPermission("proposal.unitReply")
@ApiOperation("分页列表")
@@ -62,6 +62,12 @@ public class ProposalUndertakeSuggestionController {
public void h5Index() {
}
@At("/info/h5")
@Ok("beetl:/platform/zhghh5/democratic/proposal/transact/suggestion/info.html")
@SaCheckPermission("h5.proposal.suggestion")
public void h5Info() {
}
@At
@SaCheckPermission(value = {"proposal.suggestion", "h5.proposal.suggestion"}, mode = SaMode.OR)
@ApiOperation("分页列表")
@@ -42,6 +42,12 @@ public class ProposalViceDelegationController {
public void h5Index() {
}
@At("/info/h5")
@Ok("beetl:/platform/zhghh5/democratic/proposal/transact/viceDelegation/info.html")
@SaCheckPermission("h5.proposal.vice.delegation")
public void h5Info() {
}
@At
@SaCheckPermission("proposal.vice.delegation")
@ApiOperation("分页列表")
@@ -19,4 +19,10 @@ public class H5ProposalQueryComprehensiveController {
public void index() {
}
@At("/info")
@Ok("beetl:/platform/zhghh5/democratic/proposal/query/comprehensive/info.html")
@SaCheckPermission("h5.proposal.query.comprehensive")
public void info() {
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

@@ -9,7 +9,8 @@
finished-text="没有更多了"
@load="onLoad">
<div class="table-list-container">
<div v-for="(row, index) in tableData" :key="index" class="table-list-item">
<div v-for="(row, index) in tableData" :key="index" class="table-list-item"
:data-row-id="row && row.id ? row.id : ''">
<slot name="header" :index="index" :row="row">
<div class="item-header">
<div class="item-title">{{ calcTitle(row) }}</div>
@@ -91,6 +92,33 @@ module.exports = {
}
},
methods: {
/**
* 获取当前列表快照。
* 返回值包含已加载记录、分页参数和加载完成状态,供业务页面在进入详情前暂存列表现场。
*/
getListState() {
return {
tableData: JSON.parse(JSON.stringify(this.tableData)),
localPageForm: JSON.parse(JSON.stringify(this.localPageForm)),
tableFinished: this.tableFinished
}
},
/**
* 恢复列表快照。
* state 应包含 tableData、localPageForm 和 tableFinished;返回 true 表示恢复成功。
*/
restoreListState(state) {
if (!state || !Array.isArray(state.tableData)) return false
this.$set(this, "tableData", state.tableData)
this.$set(this, "localPageForm", {...this.localPageForm, ...(state.localPageForm || {})})
this.$set(this, "tableFinished", !!state.tableFinished)
this.$set(this, "tableLoading", false)
this.$set(this, "tableRefreshing", false)
this.$emit("update:page_form", {...this.localPageForm})
return true
},
calcTitle(row) {
if (!this.title) return ""
// 处理key1.key2.key3 这样的形式
@@ -1,7 +1,7 @@
const PROPOSAL_INFO = {
name: "ProposalInfo",
template: /*language=HTML*/ `
<van-action-sheet v-model="visible" title="查看详情">
<div class="proposal-info-content">
<div class="detail-container">
<van-cell-group title="基本信息">
<van-cell title="提案名称">
@@ -178,35 +178,41 @@ const PROPOSAL_INFO = {
</div>
<slot></slot>
</van-action-sheet>
</div>
`,
props: {
// bizId 为提案主键;组件根据该参数加载提案详情和对应的已办流程记录。
bizId: {
type: String,
required: true
}
},
dicts: ["PROPOSAL_TYPE", "PROPOSAL_CASE_FILING_RESULT", "PROPOSAL_CASE_FILING_TYPE", "PROCESS_TASK_SUBMIT_TYPE", "PROPOSAL_FEEDBACK"],
data() {
return {
visible:false,
viewData: {},
doneTasks: [],
row: null,
originalRow: null, // 用于保存原始数据,支持返回功能
currentBizId: ""
}
},
watch: {
bizId: {
immediate: true,
handler(value) {
if (!value) return
this.currentBizId = value
this.getInfo()
this.getDoneTasks()
}
}
},
methods: {
// 打开
onOpen(row) {
this.row = row
this.visible = true
this.getInfo()
this.getDoneTasks()
},
// 关闭
onClose(){
this.visible = false
},
// 获取申请信息
/**
* 使用 currentBizId 查询提案详情。
* 接口参数 id 为提案主键;成功响应的 data 为详情对象并写入 viewData。
*/
getInfo() {
this.$axios.post("/platform/proposal/common/proposalInfo", {id: this.row.id}).then((res) => {
this.$axios.post("/platform/proposal/common/proposalInfo", {id: this.currentBizId}).then((res) => {
if (res.code === 0) {
this.viewData = res.data
}
@@ -215,31 +221,19 @@ const PROPOSAL_INFO = {
// 查看提案
openView(row) {
// 如果传入的是并案对象,则打开新的详情页面
if (row && typeof row === 'object' && row.id) {
// 创建一个新的提案详情组件实例来显示并案详情
this.$nextTick(() => {
// 可以通过事件总线或者其他方式来打开新的详情页面
// 这里暂时使用简单的方式,直接在当前组件中显示
const currentRow = this.row;
this.row = row;
this.getInfo();
// 保存原来的数据,以便返回时恢复
this.originalRow = currentRow;
})
} else {
// 原有的逻辑,通过ID查看
this.$nextTick(() => {
if (this.$refs.infoDialogRef) {
this.$refs.infoDialogRef.onOpen(row)
}
})
this.currentBizId = row.id
this.getInfo()
this.getDoneTasks()
}
},
// 获取已办任务审批记录
/**
* 使用 currentBizId 查询已办流程记录。
* 接口参数 bizId 为提案主键;成功响应的 data 为流程任务数组并写入 doneTasks。
*/
getDoneTasks() {
this.$axios.post("/flow/common/doneTasks", {bizId: this.row.id}).then((res) => {
this.$axios.post("/flow/common/doneTasks", {bizId: this.currentBizId}).then((res) => {
if (res.code === 0) {
this.doneTasks = res.data
}
@@ -57,17 +57,8 @@ layout("/layouts/platform_h5.html"){
<van-empty v-else image="/assets/platform/plugins/vant-green/images/nodata/nodata.png" description="暂无数据"></van-empty>
</div>
<van-popup :style="{ height: '100%', width: '100%' }" position="right" safe-area-inset-bottom v-model="infoShow">
<van-sticky>
<van-nav-bar @click-left="infoShow=false" left-arrow left-text="返回" title="提案信息"></van-nav-bar>
</van-sticky>
<div style="height: calc(100vh - 44px); overflow-y: auto">
<info ref="infoRef"></info>
</div>
</van-popup>
</div>
<script nonce="${cspNonce!}">
<!--#include("../../common/info.js"){}#-->
const vue = new Vue({
el: "#app",
store,
@@ -98,13 +89,9 @@ layout("/layouts/platform_h5.html"){
unitOptions: [],
moreOptions: {},
id: "",
formValidSuccess: false,
infoShow: false
formValidSuccess: false
}
},
components: {
info: H5_PROPOSAL_INFO_COMPONENT
},
methods: {
historyBack,
sessionChange(val) {
@@ -161,10 +148,7 @@ layout("/layouts/platform_h5.html"){
},
openView(row) {
this.infoShow = true
this.$nextTick(() => {
this.$refs.infoRef.onOpen(row.id)
})
this.$pjaxReplace("/platform/h5/proposal/query/comprehensive/info?bizId=" + encodeURIComponent(row.id))
},
pageData() {
@@ -0,0 +1,32 @@
<!--#
layout("/layouts/platform_h5.html"){
#-->
<div id="app" v-cloak>
<van-nav-bar @click-left="historyBack" left-arrow left-text="返回" title="提案信息" placeholder fixed></van-nav-bar>
<proposal-info v-if="bizId" :biz-id="bizId"></proposal-info>
</div>
<script nonce="${cspNonce!}">
<!--#include("../../common/info.js"){}#-->
const vue = new Vue({
el: "#app",
store,
components: {
"proposal-info": PROPOSAL_INFO
},
data() {
return {
bizId: ""
}
},
methods: {
historyBack
},
created() {
const params = new URLSearchParams(window.location.search)
this.bizId = params.get("bizId") || ""
}
})
</script>
<!--#
}
#-->
@@ -0,0 +1,678 @@
<!--#
layout("/layouts/platform_h5.html"){
#-->
<style>
body {
background-color: #f3f7fd;
}
.proposal-case-check-page {
min-height: 100vh;
padding-bottom: calc(24px + env(safe-area-inset-bottom));
box-sizing: border-box;
background-color: #f3f7fd;
}
.proposal-case-check-page .van-nav-bar {
background-color: #fff;
}
.proposal-case-check-page .van-nav-bar__title {
color: #172033;
font-size: 16px;
font-weight: 600;
}
.proposal-case-check-page .van-nav-bar .van-icon,
.proposal-case-check-page .van-nav-bar__text {
color: #1989fa;
}
.proposal-case-check-content {
padding: 0 12px;
}
.proposal-case-check-sticky {
padding: 22px 12px 1px;
box-sizing: border-box;
background-color: #f3f7fd;
}
.proposal-case-check-search {
margin: 0 0 8px;
padding: 0;
overflow: hidden;
border-radius: 12px;
background-color: #fff;
box-shadow: 0 5px 16px rgba(43, 73, 112, .1);
}
.proposal-case-check-search .van-search__content {
height: 42px;
padding-left: 12px;
align-items: center;
border-radius: 12px;
background-color: #fff;
}
.proposal-case-check-search .van-field__control {
color: #263548;
font-size: 14px;
}
.proposal-case-check-search .van-field__control::placeholder {
color: #a7b0bf;
}
.proposal-case-check-filter {
margin: 0 0 10px;
overflow: hidden;
border-radius: 10px;
box-shadow: 0 5px 16px rgba(43, 73, 112, .08);
transition: border-radius .18s ease;
}
.proposal-case-check-filter.is-open {
border-radius: 10px 10px 0 0;
}
.proposal-case-check-filter .van-dropdown-menu__bar,
.proposal-case-check-filter .van-dropdown-menu__item,
.proposal-case-check-filter .van-dropdown-menu__title {
height: 44px;
}
.proposal-case-check-filter .van-dropdown-menu__bar {
box-shadow: none;
}
.proposal-case-check-filter .van-dropdown-menu__item,
.proposal-case-check-filter .van-dropdown-menu__title {
align-items: center;
}
.proposal-case-check-filter .van-dropdown-menu__item {
justify-content: center;
}
.proposal-case-check-filter .van-dropdown-menu__title {
display: inline-flex;
color: #66758a;
font-size: 14px;
line-height: 20px;
}
.proposal-case-check-filter .van-dropdown-menu__title--active,
.proposal-case-check-filter .van-dropdown-item__option--active,
.proposal-case-check-filter .van-dropdown-item__option--active .van-dropdown-item__icon {
color: #1989fa;
}
.proposal-case-check-filter .van-dropdown-menu__title--down::after {
border-color: transparent transparent #1989fa #1989fa;
}
.proposal-case-check-filter .van-dropdown-item__content {
width: calc(100% - 24px);
margin: 0 12px;
overflow: hidden;
box-sizing: border-box;
border-radius: 0 0 12px 12px;
box-shadow: 0 10px 24px rgba(31, 65, 108, .14);
}
.proposal-case-check-filter .van-dropdown-item__option {
display: flex;
min-height: 48px;
padding: 0 16px;
align-items: center;
color: #46566c;
font-size: 14px;
}
.proposal-case-check-filter .van-cell::after {
right: 16px;
left: 16px;
}
.proposal-case-check-list .empty-state {
display: none;
}
.proposal-case-check-empty {
display: flex;
min-height: 280px;
padding: 46px 18px 36px;
align-items: center;
justify-content: center;
flex-direction: column;
box-sizing: border-box;
text-align: center;
}
.proposal-case-check-empty img {
display: block;
width: 86%;
max-width: 290px;
height: auto;
object-fit: contain;
}
.proposal-case-check-empty__title {
margin-top: 14px;
color: #50627a;
font-size: 15px;
line-height: 22px;
font-weight: 600;
}
.proposal-case-check-empty__hint {
margin-top: 5px;
color: #9aa7b8;
font-size: 12px;
line-height: 18px;
}
.proposal-case-check-card-header {
display: flex;
margin-bottom: 7px;
align-items: flex-start;
justify-content: space-between;
}
.proposal-case-check-card-heading {
min-width: 0;
padding-right: 10px;
flex: 1;
}
.proposal-case-check-card-title {
overflow: hidden;
color: #253247;
font-size: 15px;
line-height: 22px;
font-weight: 600;
text-overflow: ellipsis;
white-space: nowrap;
}
.proposal-case-check-card-code {
margin-top: 1px;
color: #9ba6b6;
font-size: 12px;
line-height: 18px;
}
.proposal-case-check-status {
display: inline-flex;
flex: none;
}
.proposal-case-check-status .van-tag {
display: inline-flex;
min-height: 24px;
padding: 3px 9px;
align-items: center;
box-sizing: border-box;
border: 0;
border-radius: 12px;
font-size: 12px;
line-height: 18px;
white-space: nowrap;
}
@media (max-width: 350px) {
.proposal-case-check-content,
.proposal-case-check-sticky {
padding-right: 10px;
padding-left: 10px;
}
}
</style>
<div id="app" v-cloak>
<div class="proposal-case-check-page">
<van-nav-bar @click-left="historyBack" left-arrow left-text="返回" title="提案委员会审查" placeholder fixed></van-nav-bar>
<van-sticky :offset-top="46">
<div class="proposal-case-check-sticky">
<van-search
v-model="pageForm.name"
class="proposal-case-check-search"
placeholder="请输入提案名称搜索"
shape="round"
clearable
@search="doSearch"
@clear="doSearch"
></van-search>
<van-dropdown-menu
class="proposal-case-check-filter"
:class="{'is-open': filterOpened}"
active-color="#1989fa"
>
<van-dropdown-item
v-model="pageForm.sessionId"
:options="sessionOptions"
:multiple="false"
@change="doSearch"
@open="onFilterOpen"
@close="onFilterClose"
></van-dropdown-item>
<van-dropdown-item
v-model="pageForm.approvalText"
:options="approvalOptions"
:multiple="false"
@change="approvalChange"
@open="onFilterOpen"
@close="onFilterClose"
></van-dropdown-item>
</van-dropdown-menu>
</div>
</van-sticky>
<main class="proposal-case-check-content">
<div class="proposal-case-check-list">
<!-- 列表无记录时使用提案 H5 统一空状态。 -->
<div v-if="pageForm.totalCount === 0" class="proposal-case-check-empty">
<img src="/assets/mobile/img/proposal/proposal-empty.png" alt="暂无提案记录插画">
<div class="proposal-case-check-empty__title">暂无提案记录</div>
<div class="proposal-case-check-empty__hint">当前筛选条件下暂时没有提案</div>
</div>
<table-list api="/platform/proposal/case/check/pageData" :page_form.sync="pageForm" @ready="onReady"
ref="tableListRef"
title="name">
<template #header="{index,row}">
<div class="proposal-case-check-card-header">
<div class="proposal-case-check-card-heading">
<div class="proposal-case-check-card-title">{{row.name}}</div>
<div class="proposal-case-check-card-code">编号:{{row.code || '--'}}</div>
</div>
<enum-tag
class="proposal-case-check-status"
v-if="row.instanceState"
:value="row.instanceState"
name="ProcessInstanceStateEnum"
label_key="message"
size="small"
></enum-tag>
</div>
</template>
<template v-slot="{index,row}">
<table-column label="立案编号">{{row.caseFilingCode || '--'}}</table-column>
<table-column label="提案类别">{{row.typeName || '--'}}</table-column>
<table-column label="提案人">{{row.createUserName || '--'}}</table-column>
<table-column label="教代会">{{row.sessionName || '--'}}</table-column>
<table-column label="代表团">{{row.delegationName || '--'}}</table-column>
<table-column label="当前节点">{{row.curTaskName || '--'}}</table-column>
</template>
<template #actions="{index,row}">
<div class="action-btn" @click="onView(row,index)">
<i class="fa fa-eye"></i>
<span>查看</span>
</div>
<div class="action-btn" v-if="row.taskState === 10" @click="onApproval(row,index)">
<i class="fa fa-edit"></i>
<span>审核</span>
</div>
<div class="action-btn delete" v-if="row.canRevoke" @click="onRevoke(row)">
<i class="fa fa-reply"></i>
<span>撤回</span>
</div>
</template>
</table-list>
</div>
</main>
</div>
</div>
<script nonce="${cspNonce!}">
new Vue({
el: "#app",
store,
data() {
return {
pageForm: {
pageNumber: 1,
pageSize: 10,
totalCount: 0,
pageOrderName: "",
pageOrderBy: "",
name: null,
sessionId: null,
approvalText: "0",
approval: false
},
sessionOptions: [],
filterOpened: false,
approvalOptions: [
{text: "未审核", value: "0"},
{text: "已审核", value: "1"}
],
pendingListCache: null
}
},
methods: {
onReady() {
if (this.pendingListCache) {
this.restoreListCache()
this.listSession(true)
return
}
this.listSession(false)
},
// 审核状态下拉框使用字符串值展示,查询接口使用布尔值区分已审核和未审核。
approvalChange(value) {
this.$set(this.pageForm, "approval", value === "1")
this.doSearch()
},
onFilterOpen() {
this.$set(this, "filterOpened", true)
},
onFilterClose() {
this.$set(this, "filterOpened", false)
},
// 缓存恢复时只加载教代会选项,不覆盖离开列表前的筛选条件和列表快照。
listSession(skipDefaultSearch) {
this.$axios.post("/platform/proposal/common/listSession").then((res) => {
if (res.code === 0) {
this.$set(this, "sessionOptions", [{text: "全部教代会", value: null}]
.concat(res.data.map((item) => ({text: item.fullName, value: item.id}))))
if (skipDefaultSearch) return
this.$set(this.pageForm, "sessionId", res.data.length > 0 ? res.data[0].id : null)
this.doSearch()
}
})
},
onView(row, index) {
this.saveListCache(row, index)
this.$pjaxReplace("/platform/proposal/case/check/info/h5?bizId=" + encodeURIComponent(row.id) + "&action=view")
},
onApproval(row, index) {
this.saveListCache(row, index)
const params = new URLSearchParams()
params.set("bizId", row.id)
params.set("action", "approval")
params.set("taskId", row.taskId)
params.set("taskName", row.curTaskName || row.taskName || "提案委员会审查")
this.$pjaxReplace("/platform/proposal/case/check/info/h5?" + params.toString())
},
getListCacheKey() {
return "proposal:case-check:h5:list-state"
},
getListCacheTokenParam() {
return "caseCheckListCacheToken"
},
getCurrentListCacheToken() {
return new URLSearchParams(window.location.search).get(this.getListCacheTokenParam()) || ""
},
/**
* 将委员会审查列表缓存标识绑定到当前历史记录。
* PJAX 返回时仅恢复当前历史记录对应的筛选条件、分页数据和滚动快照。
*/
bindListCacheToken(token) {
const currentUrl = new URL(window.location.href)
currentUrl.searchParams.set(this.getListCacheTokenParam(), token)
const currentState = window.history.state ? {...window.history.state} : null
if (currentState && currentState.url) {
currentState.url = currentUrl.toString()
}
window.history.replaceState(currentState, document.title, currentUrl.toString())
if ($.pjax && $.pjax.state) {
$.pjax.state.url = currentUrl.toString()
}
},
// 用户主动搜索、切换教代会或审核状态时使旧返回缓存失效。
clearListCache() {
const token = this.getCurrentListCacheToken()
if (!token) return
const currentUrl = new URL(window.location.href)
currentUrl.searchParams.delete(this.getListCacheTokenParam())
const currentState = window.history.state ? {...window.history.state} : null
if (currentState && currentState.url) {
currentState.url = currentUrl.toString()
}
window.history.replaceState(currentState, document.title, currentUrl.toString())
if ($.pjax && $.pjax.state) {
$.pjax.state.url = currentUrl.toString()
}
sessionStorage.removeItem(this.getListCacheKey())
},
// platform_h5 的实际滚动区域是 page-wrapper,列表位置必须基于该容器保存和恢复。
getListScrollContainer() {
return document.querySelector(".page-wrapper") || document.scrollingElement || document.documentElement
},
/**
* 进入查看或审核详情前保存列表现场。
* row 为当前 WF 任务,index 为已加载列表索引;当前任务和相邻锚点均使用唯一 taskId。
*/
saveListCache(row, index) {
const tableList = this.$refs.tableListRef
if (!tableList) return
const cacheToken = Date.now() + "-" + Math.random().toString(36).slice(2)
const tableState = typeof tableList.getListState === "function"
? tableList.getListState()
: {
tableData: JSON.parse(JSON.stringify(tableList.tableData || [])),
localPageForm: JSON.parse(JSON.stringify(tableList.localPageForm || this.pageForm)),
tableFinished: !!tableList.tableFinished
}
const cards = document.querySelectorAll(".proposal-case-check-list .table-list-item")
const currentCard = cards[index]
const scrollContainer = this.getListScrollContainer()
const cache = {
time: Date.now(),
token: cacheToken,
pageForm: JSON.parse(JSON.stringify(this.pageForm)),
tableState: tableState,
completedTaskId: "",
anchor: {
taskId: row.taskId,
previousTaskId: index > 0 && tableState.tableData[index - 1]
? tableState.tableData[index - 1].taskId
: "",
nextTaskId: tableState.tableData[index + 1] ? tableState.tableData[index + 1].taskId : "",
viewportTop: currentCard ? currentCard.getBoundingClientRect().top : 0,
scrollTop: scrollContainer ? scrollContainer.scrollTop : 0
}
}
try {
sessionStorage.setItem(this.getListCacheKey(), JSON.stringify(cache))
this.bindListCacheToken(cacheToken)
} catch (error) {
sessionStorage.removeItem(this.getListCacheKey())
}
},
// 仅恢复当前历史记录对应且十分钟内有效的委员会审查列表缓存。
prepareListCache() {
try {
const cacheToken = this.getCurrentListCacheToken()
if (!cacheToken) return
const cacheText = sessionStorage.getItem(this.getListCacheKey())
if (!cacheText) return
const cache = JSON.parse(cacheText)
if (!cache || cache.token !== cacheToken || !cache.tableState) return
if (Date.now() - cache.time > 10 * 60 * 1000) {
this.clearListCache()
return
}
Object.keys(cache.pageForm || {}).forEach((key) => {
this.$set(this.pageForm, key, cache.pageForm[key])
})
this.$set(this, "pendingListCache", cache)
} catch (error) {
sessionStorage.removeItem(this.getListCacheKey())
}
},
// 普通查看返回恢复旧快照;审核完成返回重新查询,避免未审核列表继续显示已处理任务。
restoreListCache() {
const cache = this.pendingListCache
const tableList = this.$refs.tableListRef
if (!cache || !tableList) return
const anchor = cache.anchor || {}
if (cache.completedTaskId) {
this.refreshCompletedListCache(cache)
return
}
this.applyListCache(cache.tableState, anchor.taskId, anchor)
},
/**
* 按离开前已加载页数重新获取审核后的列表。
* 返回值转换为 TableList 状态,并优先定位下一任务,没有下一任务时定位上一任务。
*/
refreshCompletedListCache(cache) {
const originalPageSize = Number(cache.pageForm.pageSize || 10)
const loadedCount = Math.max(cache.tableState.tableData.length, originalPageSize)
const loadedPageCount = Math.max(1, Math.ceil(loadedCount / originalPageSize))
const requestForm = {
...cache.pageForm,
pageNumber: 1,
pageSize: loadedPageCount * originalPageSize
}
const loading = createListLoading()
this.$axios.post("/platform/proposal/case/check/pageData", requestForm, {h5SkeletonLoading: true})
.then((res) => {
if (res.code !== 0) {
this.applyCompletedCacheFallback(cache)
return
}
const freshState = {
tableData: res.data.list || [],
localPageForm: {
...cache.pageForm,
pageNumber: loadedPageCount,
pageSize: originalPageSize,
totalCount: res.data.totalCount
},
tableFinished: (res.data.list || []).length >= res.data.totalCount
}
const anchor = cache.anchor || {}
this.applyListCache(freshState, anchor.nextTaskId || anchor.previousTaskId, anchor)
})
.catch(() => {
this.applyCompletedCacheFallback(cache)
})
.finally(() => {
loading.close()
})
},
// 审核后刷新失败时只处理已完成 taskId,避免误改其他流程记录。
applyCompletedCacheFallback(cache) {
const tableState = cache.tableState
const completedIndex = tableState.tableData.findIndex((item) => String(item.taskId) === String(cache.completedTaskId))
if (completedIndex >= 0 && cache.pageForm.approval === false) {
tableState.tableData.splice(completedIndex, 1)
if (tableState.localPageForm && tableState.localPageForm.totalCount > 0) {
tableState.localPageForm.totalCount--
}
} else if (completedIndex >= 0) {
this.$set(tableState.tableData[completedIndex], "taskState", 20)
}
const anchor = cache.anchor || {}
this.applyListCache(tableState, anchor.nextTaskId || anchor.previousTaskId, anchor)
},
/**
* 恢复 TableList 数据并将目标任务卡片放回离开页面前的视口位置。
* sticky 吸顶会改变一次布局,因此首次滚动后再执行一次位置校正。
*/
applyListCache(tableState, targetTaskId, anchor) {
const tableList = this.$refs.tableListRef
if (!tableList) return
if (typeof tableList.restoreListState === "function") {
if (!tableList.restoreListState(tableState)) return
} else {
this.$set(tableList, "tableData", tableState.tableData || [])
this.$set(tableList, "localPageForm", {...tableList.localPageForm, ...(tableState.localPageForm || {})})
this.$set(tableList, "tableFinished", !!tableState.tableFinished)
this.$set(tableList, "tableLoading", false)
this.$set(tableList, "tableRefreshing", false)
tableList.$emit("update:page_form", {...tableList.localPageForm})
}
this.$set(this, "pendingListCache", null)
this.$nextTick(() => {
setTimeout(() => {
const cards = Array.from(document.querySelectorAll(".proposal-case-check-list .table-list-item"))
const scrollContainer = this.getListScrollContainer()
if (!scrollContainer) return
const candidateTaskIds = [targetTaskId, anchor.nextTaskId, anchor.previousTaskId]
.filter((taskId) => taskId)
.map((taskId) => String(taskId))
.filter((taskId, index, values) => values.indexOf(taskId) === index)
let targetIndex = -1
candidateTaskIds.some((candidateTaskId) => {
targetIndex = (tableState.tableData || [])
.findIndex((item) => String(item.taskId) === candidateTaskId)
return targetIndex >= 0
})
const targetCard = targetIndex >= 0 ? cards[targetIndex] : null
if (targetCard) {
const restoreTargetCard = () => {
if (!targetCard.isConnected) return
const targetTop = scrollContainer.scrollTop
+ targetCard.getBoundingClientRect().top
- Number(anchor.viewportTop || 0)
scrollContainer.scrollTo(0, Math.max(0, targetTop))
}
restoreTargetCard()
setTimeout(restoreTargetCard, 100)
return
}
scrollContainer.scrollTo(0, Number(anchor.scrollTop || 0))
}, 0)
})
},
onRevoke(row) {
this.$dialog.confirm({
title: "提示",
message: "您确定要撤回吗?"
}).then(() => {
this.$axios.post("/flow/common/revokeTask", {taskId: row.taskId}).then((res) => {
if (res.code === 0) {
this.$toast.success(res.msg)
this.doSearch()
}
})
})
},
doSearch() {
this.clearListCache()
this.$nextTick(() => {
this.$set(this.pageForm, "pageNumber", 1)
this.$set(this.pageForm, "totalCount", 0)
this.$refs.tableListRef.doSearch()
})
}
},
created() {
this.prepareListCache()
if (this.pendingListCache) return
const tab = new URLSearchParams(window.location.search).get("tab")
if (tab === "done") {
this.$set(this.pageForm, "approval", true)
this.$set(this.pageForm, "approvalText", "1")
} else if (tab === "todo") {
this.$set(this.pageForm, "approval", false)
this.$set(this.pageForm, "approvalText", "0")
}
}
})
</script>
<!--#
}
#-->
@@ -0,0 +1,360 @@
<!--#
layout("/layouts/platform_h5.html"){
#-->
<style>
.case-check-tip {
margin: 0 16px 10px;
padding: 10px 12px;
color: #8c6a28;
font-size: 12px;
line-height: 18px;
background: #fff8e8;
border-radius: 8px;
}
.case-check-actions {
display: grid;
grid-template-columns: 88px 1fr;
gap: 12px;
padding: 12px 16px calc(12px + env(safe-area-inset-bottom));
background: #ffffff;
}
.case-check-actions .van-button {
height: 40px;
border-radius: 8px;
}
.reply-unit-popup {
display: flex;
flex-direction: column;
height: 100%;
}
.reply-unit-toolbar {
display: grid;
grid-template-columns: 64px 1fr 64px;
align-items: center;
min-height: 48px;
border-bottom: 1px solid #eeeeee;
}
.reply-unit-toolbar button {
height: 48px;
padding: 0;
color: #666666;
font-size: 14px;
background: transparent;
border: 0;
}
.reply-unit-toolbar button:last-child {
color: #1989fa;
}
.reply-unit-toolbar-title {
color: #222222;
font-size: 16px;
font-weight: 600;
text-align: center;
}
.reply-unit-list {
flex: 1;
overflow-y: auto;
-webkit-overflow-scrolling: touch;
}
.reply-unit-empty {
padding: 48px 16px;
color: #999999;
font-size: 14px;
text-align: center;
}
</style>
<div id="app" v-cloak>
<van-nav-bar @click-left="historyBack" left-arrow left-text="返回" :title="pageTitle" placeholder fixed></van-nav-bar>
<proposal-info v-if="bizId" :biz-id="bizId">
<div v-if="isApproval">
<div class="process-title">{{formData.taskName}}</div>
<van-form ref="formRef">
<van-field :value="formData.tf_username" label="审核人" readonly></van-field>
<van-field :value="formData.tf_auditTime" label="审核时间" readonly></van-field>
<van-field
:value="formData.tf_caseCheckNeedSecondReply"
name="tf_caseCheckNeedSecondReply"
label="二次答复"
required
:rules="[{ validator: validateSecondReply, message: '请选择是否需要二次答复' }]"
>
<template #input>
<van-radio-group
v-model="formData.tf_caseCheckNeedSecondReply"
direction="horizontal"
@change="onSecondReplyChange"
>
<van-radio :name="0">无需二次答复</van-radio>
<van-radio :name="1">需要二次答复</van-radio>
</van-radio-group>
</template>
</van-field>
<div class="case-check-tip" v-if="formData.tf_caseCheckNeedSecondReply === 1">
请选择需要进入二次答复的办理单位,可多选。
</div>
<van-field
v-if="formData.tf_caseCheckNeedSecondReply === 1"
:value="selectedReplyUnitNames"
name="tf_secondReplyUnitIds"
label="答复单位"
placeholder="请选择办理单位"
readonly
required
is-link
:rules="[{ validator: validateSecondReplyUnits, message: '请选择需要二次答复的办理单位' }]"
@click="openReplyUnitPicker"
></van-field>
<van-field
v-model="formData.tf_opinion"
name="tf_opinion"
label="审批意见"
type="textarea"
rows="3"
autosize
placeholder="请输入审批意见"
required
maxlength="100"
show-word-limit
:rules="[{ required: true, message: '请填写审批意见' }]"
></van-field>
</van-form>
<div class="case-check-actions">
<van-button plain @click="historyBack">取消</van-button>
<van-button type="primary" :loading="formLoading" @click="handleTaskAction">同意</van-button>
</div>
</div>
</proposal-info>
<van-popup v-model="showReplyUnitPicker" position="bottom" round :style="{ height: '70%' }">
<div class="reply-unit-popup">
<div class="reply-unit-toolbar">
<button type="button" @click="cancelReplyUnitPicker">取消</button>
<div class="reply-unit-toolbar-title">选择答复单位</div>
<button type="button" @click="confirmReplyUnitPicker">确定</button>
</div>
<div class="reply-unit-list" v-if="replyUnitOptions.length">
<van-checkbox-group v-model="pendingReplyUnitIds">
<van-cell
v-for="item in replyUnitOptions"
:key="item.unitId"
clickable
:title="item.unitName"
:label="item.isMaster ? '主办单位' : '协办单位'"
@click="toggleReplyUnit(item)"
>
<template #right-icon>
<van-checkbox :name="item.unitId" @click.stop></van-checkbox>
</template>
</van-cell>
</van-checkbox-group>
</div>
<div class="reply-unit-empty" v-else>暂无可选办理单位</div>
</div>
</van-popup>
</div>
<script nonce="${cspNonce!}">
<!--#include("../../common/info.js"){}#-->
new Vue({
el: "#app",
store,
components: {
"proposal-info": PROPOSAL_INFO
},
data() {
return {
bizId: "",
action: "view",
formData: {},
formLoading: false,
replyUnitOptions: [],
pendingReplyUnitIds: [],
showReplyUnitPicker: false
}
},
computed: {
isApproval() {
return this.action === "approval"
},
pageTitle() {
return this.isApproval ? "提案委员会审查" : "提案详情"
},
selectedReplyUnitNames() {
const selectedIds = this.formData.tf_secondReplyUnitIds || []
return this.replyUnitOptions
.filter((item) => selectedIds.indexOf(item.unitId) > -1)
.map((item) => item.unitName + "" + (item.isMaster ? "主办" : "协办") + "")
.join("、")
}
},
methods: {
// 二次答复使用数字 0/1,与 PC 端和流程拦截器读取的字段保持一致。
validateSecondReply(value) {
return value === 0 || value === 1
},
// 选择需要二次答复时,至少需要选择一个当前提案的办理单位。
validateSecondReplyUnits() {
return this.formData.tf_caseCheckNeedSecondReply !== 1
|| (this.formData.tf_secondReplyUnitIds || []).length > 0
},
onSecondReplyChange(value) {
if (value !== 1) {
this.$set(this.formData, "tf_secondReplyUnitIds", [])
}
},
/**
* 查询当前提案的承办单位。
* proposalId 为提案主键;接口返回 ProposalReplyUnit 数组,包含 unitId、unitName、isMaster。
*/
loadReplyUnits(proposalId) {
this.$axios.post("/platform/proposal/case/check/listReplyUnits", {proposalId: proposalId})
.then((res) => {
if (res && res.code === 0) {
this.$set(this, "replyUnitOptions", res.data || [])
}
})
},
openReplyUnitPicker() {
this.$set(this, "pendingReplyUnitIds", [...(this.formData.tf_secondReplyUnitIds || [])])
this.$set(this, "showReplyUnitPicker", true)
},
toggleReplyUnit(item) {
const ids = [...this.pendingReplyUnitIds]
const index = ids.indexOf(item.unitId)
if (index > -1) {
ids.splice(index, 1)
} else {
ids.push(item.unitId)
}
this.$set(this, "pendingReplyUnitIds", ids)
},
cancelReplyUnitPicker() {
this.$set(this, "showReplyUnitPicker", false)
},
confirmReplyUnitPicker() {
this.$set(this.formData, "tf_secondReplyUnitIds", [...this.pendingReplyUnitIds])
this.$set(this, "showReplyUnitPicker", false)
},
/**
* 组装流程提交参数。
* 根据选中的 ProposalReplyUnit 拆分主办和协办单位,返回 flow/common/executeTask 所需对象。
*/
buildSubmitData() {
const submitData = {...this.formData}
if (submitData.tf_caseCheckNeedSecondReply === 1) {
const secondReplyUnits = this.replyUnitOptions.filter((item) => {
return submitData.tf_secondReplyUnitIds.indexOf(item.unitId) > -1
})
const masterUnits = secondReplyUnits.filter((item) => item.isMaster)
const slaveUnits = secondReplyUnits.filter((item) => !item.isMaster)
const masterUnitIds = masterUnits.map((item) => item.unitId)
const masterUnitNames = masterUnits.map((item) => item.unitName)
const slaveUnitIds = slaveUnits.map((item) => item.unitId)
const slaveUnitNames = slaveUnits.map((item) => item.unitName)
submitData.tf_masterUnitIds = masterUnitIds
submitData.tf_masterUnitId = masterUnitIds.length ? masterUnitIds[0] : ""
submitData.tf_masterUnitName = masterUnitNames.length ? masterUnitNames[0] : ""
submitData.tf_masterUnitNames = masterUnitNames
submitData.tf_masterUnitNameStr = masterUnitNames.join(",")
submitData.tf_slaveUnitIds = slaveUnitIds
submitData.tf_slaveUnitNames = slaveUnitNames
submitData.tf_slaveUnitNameStr = slaveUnitNames.join(",")
} else {
submitData.tf_secondReplyUnitIds = []
submitData.tf_masterUnitIds = []
submitData.tf_masterUnitId = ""
submitData.tf_masterUnitName = ""
submitData.tf_masterUnitNames = []
submitData.tf_masterUnitNameStr = ""
submitData.tf_slaveUnitIds = []
submitData.tf_slaveUnitNames = []
submitData.tf_slaveUnitNameStr = ""
}
return submitData
},
// 完成审批后记录任务 ID,列表返回缓存只移除该条记录,不影响其他查看位置。
markListCacheCompleted() {
const cacheKey = "proposal:case-check:h5:list-state"
const cacheText = sessionStorage.getItem(cacheKey)
if (!cacheText) return
try {
const cache = JSON.parse(cacheText)
cache.completedTaskId = this.formData.processTaskId || ""
sessionStorage.setItem(cacheKey, JSON.stringify(cache))
} catch (e) {
sessionStorage.removeItem(cacheKey)
}
},
/**
* 提交提案委员会审查结果。
* 参数包含 taskId 对应的 processTaskId、proposalId、审批意见和二次答复单位;Result.code 为 0 表示成功。
*/
handleTaskAction() {
this.$refs.formRef.validate()
.then(() => this.$dialog.confirm({title: "提示", message: "您确定要提交吗?"}))
.then(() => {
this.$set(this, "formLoading", true)
const submitData = this.buildSubmitData()
return this.$axios.post("/flow/common/executeTask", {
data: JSON.stringify({
...submitData,
submitType: 1
})
})
})
.then((res) => {
if (res && res.code === 0) {
this.markListCacheCompleted()
this.$toast.success(res.msg)
this.historyBack()
}
})
.catch(() => {})
.finally(() => {
this.$set(this, "formLoading", false)
})
}
},
created() {
const params = new URLSearchParams(window.location.search)
// bizId 为提案主键;taskId 和 taskName 用于执行当前 wf 流程任务。
this.$set(this, "bizId", params.get("bizId") || "")
this.$set(this, "action", params.get("action") || "view")
if (this.isApproval) {
this.$set(this.formData, "processTaskId", params.get("taskId") || "")
this.$set(this.formData, "taskName", params.get("taskName") || "提案委员会审查")
this.$set(this.formData, "proposalId", this.bizId)
this.$set(this.formData, "tf_auditTime", moment().format("YYYY-MM-DD"))
this.$set(this.formData, "tf_username", (this.$store.state.user && this.$store.state.user.username) || "")
this.$set(this.formData, "tf_caseCheckNeedSecondReply", 0)
this.$set(this.formData, "tf_secondReplyUnitIds", [])
this.loadReplyUnits(this.bizId)
}
if (!this.bizId) {
this.$toast.fail("缺少提案参数")
}
}
})
</script>
<!--#
}
#-->
@@ -51,76 +51,11 @@ layout("/layouts/platform_h5.html"){
</template>
</table-list>
<proposal-info ref="proposalInfoRef">
<div v-if="showApprovalForm">
<div class="process-title">提案委员会成员意见</div>
<van-form ref="formRef">
<van-field
v-model="formData.caseFilingResultName"
name="caseFilingResultName"
label="立案结果"
placeholder="请选择立案结果"
:rules="[{ required: true, message: '请选择立案结果' }]"
required
is-link
@click="showCaseFilingResultPicker = true"
></van-field>
<van-popup v-model="showCaseFilingResultPicker" position="bottom">
<van-picker
show-toolbar
:columns="dict.type.PROPOSAL_CASE_FILING_RESULT.map(item => ({text: item.label, value: item.code}))"
@confirm="onCaseFilingResultConfirm"
@cancel="showCaseFilingResultPicker = false"
></van-picker>
</van-popup>
<template v-if="['CONFIRM_FILING'].includes(formData.caseFilingResult)">
<van-field
v-model="formData.caseFilingTypeName"
name="caseFilingTypeName"
label="立案结果"
placeholder="请选择立案类型"
:rules="[{ required: true, message: '请选择立案类型' }]"
required
is-link
@click="showCaseFilingTypePicker = true"
></van-field>
<van-popup v-model="showCaseFilingTypePicker" position="bottom">
<van-picker
show-toolbar
:columns="dict.type.PROPOSAL_CASE_FILING_TYPE.map(item => ({text: item.label, value: item.code}))"
@confirm="onCaseFilingTypeConfirm"
@cancel="showCaseFilingTypePicker = false"
></van-picker>
</van-popup>
</template>
<van-field
v-model="formData.opinion"
name="opinion"
label="审批意见"
placeholder="请输入审批意见"
:rules="[{ required: true, message: '请填写审批意见' }]"
required
maxlength="100"
show-word-limit
></van-field>
</van-form>
<div style="display: flex; justify-content: space-between; column-gap: 10px; padding: 10px">
<van-button block @click="$refs.proposalInfoRef.onClose()">取消</van-button>
<van-button type="primary" block @click="doSubmit">提交</van-button>
</div>
</div>
</proposal-info>
</div>
<script nonce="${cspNonce!}">
<!--#include("../../common/info.js"){}#-->
const vue = new Vue({
el: "#app",
store,
dicts: ["PROPOSAL_CASE_FILING_RESULT", "PROPOSAL_CASE_FILING_TYPE"],
data() {
return {
pageForm: {
@@ -133,17 +68,9 @@ layout("/layouts/platform_h5.html"){
approval: false
},
sessionOptions: [],
formData: {},
showApprovalForm: false,
showCaseFilingResultPicker: false,
showCaseFilingTypePicker: false
sessionOptions: []
}
},
components: {
"proposal-info": PROPOSAL_INFO
},
methods: {
onReady() {
this.listSession()
@@ -166,55 +93,11 @@ layout("/layouts/platform_h5.html"){
},
onView(row) {
this.showApprovalForm = false
this.$refs.proposalInfoRef.onOpen(row)
},
// 确定立案类型
onCaseFilingResultConfirm(value) {
this.$set(this.formData, "caseFilingResultName", value.text)
this.$set(this.formData, "caseFilingResult", value.value)
this.showCaseFilingResultPicker = false
},
// 确定立案类型
onCaseFilingTypeConfirm(value) {
this.$set(this.formData, "caseFilingTypeName", value.text)
this.$set(this.formData, "caseFilingType", value.value)
this.showCaseFilingTypePicker = false
this.$pjaxReplace("/platform/proposal/commissioner/info/h5?bizId=" + encodeURIComponent(row.id) + "&action=view")
},
onApproval(row) {
this.showApprovalForm = true
this.$refs.proposalInfoRef.onOpen(row)
this.formData = {
proposalId: row.id,
caseFilingResult: null,
caseFilingType: null,
opinion: null
}
},
async doSubmit() {
try {
await this.$refs.formRef.validate();
this.$dialog.confirm({
title: '提示',
message: "您确定要提交吗?",
}).then(() => {
this.$axios.post('/platform/proposal/commissioner/doApproval', this.formData).then(res => {
if (res.code === 0) {
this.$refs.proposalInfoRef.onClose()
this.$toast.success(res.msg)
this.doSearch()
}
})
}).catch(() => {
})
} catch (error) {
}
this.$pjaxReplace("/platform/proposal/commissioner/info/h5?bizId=" + encodeURIComponent(row.id) + "&action=approval")
},
doSearch() {
@@ -0,0 +1,149 @@
<!--#
layout("/layouts/platform_h5.html"){
#-->
<div id="app" v-cloak>
<van-nav-bar @click-left="historyBack" left-arrow left-text="返回" :title="pageTitle" placeholder fixed></van-nav-bar>
<proposal-info v-if="bizId" :biz-id="bizId">
<div v-if="isApproval">
<div class="process-title">提案委员会成员意见</div>
<van-form ref="formRef">
<van-field
v-model="formData.caseFilingResultName"
name="caseFilingResultName"
label="立案结果"
placeholder="请选择立案结果"
:rules="[{ required: true, message: '请选择立案结果' }]"
required
is-link
@click="showCaseFilingResultPicker = true"
></van-field>
<van-popup v-model="showCaseFilingResultPicker" position="bottom">
<van-picker
show-toolbar
:columns="dict.type.PROPOSAL_CASE_FILING_RESULT.map(item => ({text: item.label, value: item.code}))"
@confirm="onCaseFilingResultConfirm"
@cancel="showCaseFilingResultPicker = false"
></van-picker>
</van-popup>
<template v-if="formData.caseFilingResult === 'CONFIRM_FILING'">
<van-field
v-model="formData.caseFilingTypeName"
name="caseFilingTypeName"
label="立案类型"
placeholder="请选择立案类型"
:rules="[{ required: true, message: '请选择立案类型' }]"
required
is-link
@click="showCaseFilingTypePicker = true"
></van-field>
<van-popup v-model="showCaseFilingTypePicker" position="bottom">
<van-picker
show-toolbar
:columns="dict.type.PROPOSAL_CASE_FILING_TYPE.map(item => ({text: item.label, value: item.code}))"
@confirm="onCaseFilingTypeConfirm"
@cancel="showCaseFilingTypePicker = false"
></van-picker>
</van-popup>
</template>
<van-field
v-model="formData.opinion"
name="opinion"
label="审批意见"
placeholder="请输入审批意见"
:rules="[{ required: true, message: '请填写审批意见' }]"
required
maxlength="100"
show-word-limit
></van-field>
</van-form>
<div style="display: flex; justify-content: space-between; column-gap: 10px; padding: 10px">
<van-button block :disabled="formLoading" @click="historyBack">取消</van-button>
<van-button type="primary" block :loading="formLoading" @click="doSubmit">提交</van-button>
</div>
</div>
</proposal-info>
</div>
<script nonce="${cspNonce!}">
<!--#include("../../common/info.js"){}#-->
new Vue({
el: "#app",
store,
dicts: ["PROPOSAL_CASE_FILING_RESULT", "PROPOSAL_CASE_FILING_TYPE"],
components: {
"proposal-info": PROPOSAL_INFO
},
data() {
return {
bizId: "",
action: "view",
formData: {},
formLoading: false,
showCaseFilingResultPicker: false,
showCaseFilingTypePicker: false
}
},
computed: {
isApproval() {
return this.action === "approval"
},
pageTitle() {
return this.isApproval ? "委员会成员意见" : "提案详情"
}
},
methods: {
onCaseFilingResultConfirm(value) {
this.$set(this.formData, "caseFilingResultName", value.text)
this.$set(this.formData, "caseFilingResult", value.value)
this.showCaseFilingResultPicker = false
},
onCaseFilingTypeConfirm(value) {
this.$set(this.formData, "caseFilingTypeName", value.text)
this.$set(this.formData, "caseFilingType", value.value)
this.showCaseFilingTypePicker = false
},
/**
* 提交委员会成员意见。
* 参数包含 proposalId、立案结果、立案类型和意见;接口返回 Result。
*/
doSubmit() {
this.$refs.formRef.validate()
.then(() => this.$dialog.confirm({title: "提示", message: "您确定要提交吗?"}))
.then(() => {
this.formLoading = true
return this.$axios.post("/platform/proposal/commissioner/doApproval", this.formData)
})
.then((res) => {
if (res && res.code === 0) {
this.$toast.success(res.msg)
this.historyBack()
}
})
.catch(() => {})
.finally(() => {
this.formLoading = false
})
}
},
created() {
const params = new URLSearchParams(window.location.search)
// bizId 为详情查询和委员会意见提交共同使用的提案主键。
this.bizId = params.get("bizId") || ""
this.action = params.get("action") || "view"
if (this.isApproval) {
this.$set(this.formData, "proposalId", this.bizId)
this.$set(this.formData, "caseFilingResult", null)
this.$set(this.formData, "caseFilingType", null)
this.$set(this.formData, "opinion", null)
}
if (!this.bizId) {
this.$toast.fail("缺少提案参数")
}
}
})
</script>
<!--#
}
#-->
@@ -1,82 +1,328 @@
<!--#
layout("/layouts/platform_h5.html"){
#-->
<style>
body {
background-color: #f3f7fd;
}
.proposal-delegation-page {
min-height: 100vh;
padding-bottom: calc(24px + env(safe-area-inset-bottom));
box-sizing: border-box;
background-color: #f3f7fd;
}
.proposal-delegation-page .van-nav-bar {
background-color: #fff;
}
.proposal-delegation-page .van-nav-bar__title {
color: #172033;
font-size: 16px;
font-weight: 600;
}
.proposal-delegation-page .van-nav-bar .van-icon,
.proposal-delegation-page .van-nav-bar__text {
color: #1989fa;
}
.proposal-delegation-content {
padding: 0 12px;
}
.proposal-delegation-sticky {
padding: 22px 12px 1px;
box-sizing: border-box;
background-color: #f3f7fd;
}
.proposal-delegation-search {
margin: 0 0 8px;
padding: 0;
overflow: hidden;
border-radius: 12px;
background-color: #fff;
box-shadow: 0 5px 16px rgba(43, 73, 112, .1);
}
.proposal-delegation-search .van-search__content {
height: 42px;
padding-left: 12px;
align-items: center;
border-radius: 12px;
background-color: #fff;
}
.proposal-delegation-search .van-field__control {
color: #263548;
font-size: 14px;
}
.proposal-delegation-search .van-field__control::placeholder {
color: #a7b0bf;
}
.proposal-delegation-filter {
margin: 0 0 10px;
overflow: hidden;
border-radius: 10px;
box-shadow: 0 5px 16px rgba(43, 73, 112, .08);
transition: border-radius .18s ease;
}
.proposal-delegation-filter.is-open {
border-radius: 10px 10px 0 0;
}
.proposal-delegation-filter .van-dropdown-menu__bar,
.proposal-delegation-filter .van-dropdown-menu__item,
.proposal-delegation-filter .van-dropdown-menu__title {
height: 44px;
}
.proposal-delegation-filter .van-dropdown-menu__bar {
box-shadow: none;
}
.proposal-delegation-filter .van-dropdown-menu__item,
.proposal-delegation-filter .van-dropdown-menu__title {
align-items: center;
}
.proposal-delegation-filter .van-dropdown-menu__item {
justify-content: center;
}
.proposal-delegation-filter .van-dropdown-menu__title {
display: inline-flex;
color: #66758a;
font-size: 14px;
line-height: 20px;
}
.proposal-delegation-filter .van-dropdown-menu__title--active,
.proposal-delegation-filter .van-dropdown-item__option--active,
.proposal-delegation-filter .van-dropdown-item__option--active .van-dropdown-item__icon {
color: #1989fa;
}
.proposal-delegation-filter .van-dropdown-menu__title--down::after {
border-color: transparent transparent #1989fa #1989fa;
}
.proposal-delegation-filter .van-dropdown-item__content {
width: calc(100% - 24px);
margin: 0 12px;
overflow: hidden;
box-sizing: border-box;
border-radius: 0 0 12px 12px;
box-shadow: 0 10px 24px rgba(31, 65, 108, .14);
}
.proposal-delegation-filter .van-dropdown-item__option {
display: flex;
min-height: 48px;
padding: 0 16px;
align-items: center;
color: #46566c;
font-size: 14px;
}
.proposal-delegation-filter .van-cell::after {
right: 16px;
left: 16px;
}
.proposal-delegation-list .empty-state {
display: none;
}
.proposal-delegation-empty {
display: flex;
min-height: 280px;
padding: 46px 18px 36px;
align-items: center;
justify-content: center;
flex-direction: column;
box-sizing: border-box;
text-align: center;
}
.proposal-delegation-empty img {
display: block;
width: 86%;
max-width: 290px;
height: auto;
object-fit: contain;
}
.proposal-delegation-empty__title {
margin-top: 14px;
color: #50627a;
font-size: 15px;
line-height: 22px;
font-weight: 600;
}
.proposal-delegation-empty__hint {
margin-top: 5px;
color: #9aa7b8;
font-size: 12px;
line-height: 18px;
}
.proposal-delegation-card-header {
display: flex;
margin-bottom: 7px;
align-items: flex-start;
justify-content: space-between;
}
.proposal-delegation-card-heading {
min-width: 0;
padding-right: 10px;
flex: 1;
}
.proposal-delegation-card-title {
overflow: hidden;
color: #253247;
font-size: 15px;
line-height: 22px;
font-weight: 600;
text-overflow: ellipsis;
white-space: nowrap;
}
.proposal-delegation-card-code {
margin-top: 1px;
color: #9ba6b6;
font-size: 12px;
line-height: 18px;
}
.proposal-delegation-status {
display: inline-flex;
flex: none;
}
.proposal-delegation-status .van-tag {
display: inline-flex;
min-height: 24px;
padding: 3px 9px;
align-items: center;
box-sizing: border-box;
border: 0;
border-radius: 12px;
font-size: 12px;
line-height: 18px;
white-space: nowrap;
}
@media (max-width: 350px) {
.proposal-delegation-content,
.proposal-delegation-sticky {
padding-right: 10px;
padding-left: 10px;
}
}
</style>
<div id="app" v-cloak>
<van-nav-bar @click-left="historyBack" left-arrow left-text="返回" title="团长审核" placeholder fixed></van-nav-bar>
<van-sticky offset-top="46px">
<van-search
v-model="pageForm.name"
:show-action="false"
:reverse-color="false"
input-align="left"
placeholder="请输入提案名称搜索"
@search="doSearch"
></van-search>
<van-dropdown-menu :close-on-click-outside="false" :close-on-click-overlay="false">
<van-dropdown-item v-model="pageForm.sessionId" :options="sessionOptions" :multiple="false"
@change="doSearch"></van-dropdown-item>
</van-dropdown-menu>
<van-tabs v-model="pageForm.approvalText"
@change="(val)=>{this.pageForm.approval = val==='1';this.doSearch();}">
<van-tab title="已审核" name="1"></van-tab>
<van-tab title="未审核" name="0"></van-tab>
</van-tabs>
</van-sticky>
<div class="proposal-delegation-page">
<van-nav-bar @click-left="historyBack" left-arrow left-text="返回" title="团长审核" placeholder fixed></van-nav-bar>
<van-sticky :offset-top="46">
<div class="proposal-delegation-sticky">
<van-search
v-model="pageForm.name"
class="proposal-delegation-search"
placeholder="请输入提案名称搜索"
shape="round"
clearable
@search="doSearch"
@clear="doSearch"
></van-search>
<van-dropdown-menu
class="proposal-delegation-filter"
:class="{'is-open': filterOpened}"
active-color="#1989fa"
>
<van-dropdown-item
v-model="pageForm.sessionId"
:options="sessionOptions"
:multiple="false"
@change="doSearch"
@open="filterOpened = true"
@close="filterOpened = false"
></van-dropdown-item>
<van-dropdown-item
v-model="pageForm.approvalText"
:options="approvalOptions"
:multiple="false"
@change="approvalChange"
@open="filterOpened = true"
@close="filterOpened = false"
></van-dropdown-item>
</van-dropdown-menu>
</div>
</van-sticky>
<table-list api="/platform/proposal/delegation/pageData" :page_form.sync="pageForm" @ready="onReady"
ref="tableListRef"
title="name">
<template v-slot="{index,row}">
<table-column label="提案编号">{{row.code}}</table-column>
<table-column label="提案类别">{{row.typeName}}</table-column>
<table-column label="提案人">{{row.createUserName}}</table-column>
<table-column label="代表团">{{row.delegationName}}</table-column>
<table-column label="当前节点">{{row.curTaskName}}</table-column>
</template>
<template #actions="{index,row}">
<div class="action-btn" @click="onView(row)">
<i class="fa fa-eye"></i>
<span>查看</span>
<main class="proposal-delegation-content">
<div class="proposal-delegation-list">
<!-- 列表无记录时使用与“我的提案”一致的提案专用空状态。 -->
<div v-if="pageForm.totalCount === 0" class="proposal-delegation-empty">
<img src="/assets/mobile/img/proposal/proposal-empty.png" alt="暂无提案记录插画">
<div class="proposal-delegation-empty__title">暂无提案记录</div>
<div class="proposal-delegation-empty__hint">当前筛选条件下暂时没有提案</div>
</div>
<table-list api="/platform/proposal/delegation/pageData" :page_form.sync="pageForm" @ready="onReady"
ref="tableListRef"
title="name">
<template #header="{index,row}">
<div class="proposal-delegation-card-header">
<div class="proposal-delegation-card-heading">
<div class="proposal-delegation-card-title">{{row.name}}</div>
<div class="proposal-delegation-card-code">编号:{{row.code || '--'}}</div>
</div>
<enum-tag
class="proposal-delegation-status"
v-if="row.instanceState"
:value="row.instanceState"
name="ProcessInstanceStateEnum"
label_key="message"
size="small"
></enum-tag>
</div>
</template>
<template v-slot="{index,row}">
<table-column label="提案类别">{{row.typeName || '--'}}</table-column>
<table-column label="提案人">{{row.createUserName || '--'}}</table-column>
<table-column label="代表团">{{row.delegationName || '--'}}</table-column>
<table-column label="当前节点">{{row.curTaskName || '--'}}</table-column>
</template>
<template #actions="{index,row}">
<div class="action-btn" @click="onView(row,index)">
<i class="fa fa-eye"></i>
<span>查看</span>
</div>
<div class="action-btn" v-if="row.taskState === 10" @click="onApproval(row,index)">
<i class="fa fa-edit"></i>
<span>审核</span>
</div>
<div class="action-btn delete" v-if="row.canRevoke" @click="onRevoke(row)">
<i class="fa fa-reply"></i>
<span>撤回</span>
</div>
</template>
</table-list>
</div>
<div class="action-btn" v-if="row.taskState === 10" @click="onApproval(row)">
<i class="fa fa-edit"></i>
<span>审核</span>
</div>
<div class="action-btn delete" v-if="row.canRevoke" @click="onRevoke(row)">
<i class="fa fa-reply"></i>
<span>撤回</span>
</div>
</template>
</table-list>
<proposal-info ref="proposalInfoRef">
<div v-if="showApprovalForm">
<div class="process-title">{{formData.taskName}}</div>
<van-form ref="formRef">
<van-field
v-model="formData.tf_opinion"
name="tf_opinion"
label="审批意见"
placeholder="请输入审批意见"
:rules="[{ required: true, message: '请填写审批意见' }]"
required
maxlength="100"
show-word-limit
></van-field>
</van-form>
<div style="display: flex; justify-content: space-between; column-gap: 10px; padding: 10px">
<van-button type="danger" block @click="handleTaskAction(6)">退回</van-button>
<van-button type="danger" block @click="handleTaskAction(2)">不同意</van-button>
<van-button type="primary" block @click="handleTaskAction(1)">同意</van-button>
</div>
</div>
</proposal-info>
</main>
</div>
</div>
<script nonce="${cspNonce!}">
<!--#include("../../common/info.js"){}#-->
const vue = new Vue({
new Vue({
el: "#app",
store,
data() {
@@ -85,6 +331,8 @@ layout("/layouts/platform_h5.html"){
pageNumber: 1,
pageSize: 10,
totalCount: 0,
pageOrderName: "",
pageOrderBy: "",
name: null,
sessionId: null,
approvalText: "0",
@@ -92,75 +340,286 @@ layout("/layouts/platform_h5.html"){
},
sessionOptions: [],
formData: {},
showApprovalForm: false
filterOpened: false,
approvalOptions: [
{text: "未审核", value: "0"},
{text: "已审核", value: "1"}
],
pendingListCache: null
}
},
components: {
"proposal-info": PROPOSAL_INFO
},
methods: {
onReady() {
this.listSession()
if (this.pendingListCache) {
this.restoreListCache()
this.listSession(true)
return
}
this.listSession(false)
},
listSession() {
// 审核状态下拉框使用字符串值展示,查询接口使用布尔值区分已审核和未审核。
approvalChange(value) {
this.$set(this.pageForm, "approval", value === "1")
this.doSearch()
},
listSession(skipDefaultSearch) {
this.$axios.post("/platform/proposal/common/listSession").then((res) => {
if (res.code === 0) {
this.sessionOptions = [
{
text: "全部届次",
value: null
}
].concat(res.data.map((v) => ({text: v.fullName, value: v.id})))
if (this.sessionOptions.length > 0) {
this.pageForm.sessionId = this.sessionOptions[0].value
this.doSearch()
}
this.sessionOptions = [{text: "全部届次", value: null}]
.concat(res.data.map((v) => ({text: v.fullName, value: v.id})))
if (skipDefaultSearch) return
this.$set(this.pageForm, "sessionId", res.data.length > 0 ? res.data[0].id : null)
this.doSearch()
}
})
},
onView(row) {
this.showApprovalForm = false
this.$refs.proposalInfoRef.onOpen(row)
onView(row, index) {
this.saveListCache(row, index)
this.$pjaxReplace("/platform/proposal/delegation/info/h5?bizId=" + encodeURIComponent(row.id) + "&action=view")
},
onApproval(row) {
this.showApprovalForm = true
this.$refs.proposalInfoRef.onOpen(row)
this.formData = {
processTaskId: row.taskId,
taskName: row.curTaskName
onApproval(row, index) {
this.saveListCache(row, index)
const params = new URLSearchParams()
params.set("bizId", row.id)
params.set("action", "approval")
params.set("taskId", row.taskId)
params.set("taskName", row.curTaskName || row.taskName || "团长审核")
this.$pjaxReplace("/platform/proposal/delegation/info/h5?" + params.toString())
},
getListCacheKey() {
return "proposal:delegation:h5:list-state"
},
getListCacheTokenParam() {
return "delegationListCacheToken"
},
getCurrentListCacheToken() {
return new URLSearchParams(window.location.search).get(this.getListCacheTokenParam()) || ""
},
/**
* 将团长审核列表缓存标识绑定到当前历史记录。
* PJAX 重复初始化时,每个列表实例都通过该标识恢复同一份筛选、分页和滚动快照。
*/
bindListCacheToken(token) {
const currentUrl = new URL(window.location.href)
currentUrl.searchParams.set(this.getListCacheTokenParam(), token)
const currentState = window.history.state ? {...window.history.state} : null
if (currentState && currentState.url) {
currentState.url = currentUrl.toString()
}
window.history.replaceState(currentState, document.title, currentUrl.toString())
if ($.pjax && $.pjax.state) {
$.pjax.state.url = currentUrl.toString()
}
},
async handleTaskAction(val) {
// 用户主动查询时清除详情返回标识,避免重新进入页面后误用旧列表现场。
clearListCache() {
const token = this.getCurrentListCacheToken()
if (!token) return
const currentUrl = new URL(window.location.href)
currentUrl.searchParams.delete(this.getListCacheTokenParam())
const currentState = window.history.state ? {...window.history.state} : null
if (currentState && currentState.url) {
currentState.url = currentUrl.toString()
}
window.history.replaceState(currentState, document.title, currentUrl.toString())
if ($.pjax && $.pjax.state) {
$.pjax.state.url = currentUrl.toString()
}
sessionStorage.removeItem(this.getListCacheKey())
},
// platform_h5 的实际滚动区域是 page-wrapper,列表返回位置必须基于该容器保存和恢复。
getListScrollContainer() {
return document.querySelector(".page-wrapper") || document.scrollingElement || document.documentElement
},
/**
* 进入查看或审核详情前保存列表现场。
* row 为当前提案,index 为已加载列表索引;缓存包含查询条件、分页数据和相邻记录锚点。
*/
saveListCache(row, index) {
const tableList = this.$refs.tableListRef
if (!tableList) return
const cacheToken = Date.now() + "-" + Math.random().toString(36).slice(2)
const tableState = typeof tableList.getListState === "function"
? tableList.getListState()
: {
tableData: JSON.parse(JSON.stringify(tableList.tableData || [])),
localPageForm: JSON.parse(JSON.stringify(tableList.localPageForm || this.pageForm)),
tableFinished: !!tableList.tableFinished
}
const cards = document.querySelectorAll(".proposal-delegation-list .table-list-item")
const currentCard = cards[index]
const scrollContainer = this.getListScrollContainer()
const cache = {
time: Date.now(),
token: cacheToken,
pageForm: JSON.parse(JSON.stringify(this.pageForm)),
tableState: tableState,
completedBizId: "",
anchor: {
rowId: row.id,
previousRowId: index > 0 && tableState.tableData[index - 1] ? tableState.tableData[index - 1].id : "",
nextRowId: tableState.tableData[index + 1] ? tableState.tableData[index + 1].id : "",
viewportTop: currentCard ? currentCard.getBoundingClientRect().top : 0,
scrollTop: scrollContainer ? scrollContainer.scrollTop : 0
}
}
try {
await this.$refs.formRef.validate();
this.$dialog.confirm({
title: "提示",
message: "您确定要提交吗?"
}).then(() => {
this.$axios.post("/flow/common/executeTask", {
data: JSON.stringify({
...this.formData,
submitType: val
})
}).then((res) => {
if (res.code === 0) {
this.$refs.proposalInfoRef.onClose()
this.$toast.success(res.msg)
this.doSearch()
}
})
}).catch(() => {
})
sessionStorage.setItem(this.getListCacheKey(), JSON.stringify(cache))
this.bindListCacheToken(cacheToken)
} catch (error) {
sessionStorage.removeItem(this.getListCacheKey())
}
},
// 仅恢复当前历史记录对应且十分钟内有效的团长审核列表缓存。
prepareListCache() {
try {
const cacheToken = this.getCurrentListCacheToken()
if (!cacheToken) return
const cacheText = sessionStorage.getItem(this.getListCacheKey())
if (!cacheText) return
const cache = JSON.parse(cacheText)
if (!cache || cache.token !== cacheToken || !cache.tableState) return
if (Date.now() - cache.time > 10 * 60 * 1000) {
this.clearListCache()
return
}
Object.keys(cache.pageForm || {}).forEach((key) => {
this.$set(this.pageForm, key, cache.pageForm[key])
})
this.$set(this, "pendingListCache", cache)
} catch (error) {
sessionStorage.removeItem(this.getListCacheKey())
}
},
// 普通查看返回恢复旧快照;审核完成返回重新查询,避免未审核列表继续显示已处理记录。
restoreListCache() {
const cache = this.pendingListCache
const tableList = this.$refs.tableListRef
if (!cache || !tableList) return
const anchor = cache.anchor || {}
if (cache.completedBizId) {
this.refreshCompletedListCache(cache)
return
}
this.applyListCache(cache.tableState, anchor.rowId, anchor)
},
/**
* 按离开前已加载页数重新获取审核后的列表。
* 返回值仍转换为 TableList 状态,并定位下一条;没有下一条时定位上一条。
*/
refreshCompletedListCache(cache) {
const originalPageSize = Number(cache.pageForm.pageSize || 10)
const loadedCount = Math.max(cache.tableState.tableData.length, originalPageSize)
const loadedPageCount = Math.max(1, Math.ceil(loadedCount / originalPageSize))
const requestForm = {
...cache.pageForm,
pageNumber: 1,
pageSize: loadedPageCount * originalPageSize
}
const loading = createListLoading()
this.$axios.post("/platform/proposal/delegation/pageData", requestForm, {h5SkeletonLoading: true})
.then((res) => {
if (res.code !== 0) {
this.applyCompletedCacheFallback(cache)
return
}
const freshState = {
tableData: res.data.list || [],
localPageForm: {
...cache.pageForm,
pageNumber: loadedPageCount,
pageSize: originalPageSize,
totalCount: res.data.totalCount
},
tableFinished: (res.data.list || []).length >= res.data.totalCount
}
const anchor = cache.anchor || {}
this.applyListCache(freshState, anchor.nextRowId || anchor.previousRowId, anchor)
})
.catch(() => {
this.applyCompletedCacheFallback(cache)
})
.finally(() => {
loading.close()
})
},
// 审核后刷新失败时从旧快照移除已处理记录,保证未审核列表不会保留脏数据。
applyCompletedCacheFallback(cache) {
const tableState = cache.tableState
const completedIndex = tableState.tableData.findIndex((item) => String(item.id) === String(cache.completedBizId))
if (completedIndex >= 0 && cache.pageForm.approval === false) {
tableState.tableData.splice(completedIndex, 1)
if (tableState.localPageForm && tableState.localPageForm.totalCount > 0) {
tableState.localPageForm.totalCount--
}
} else if (completedIndex >= 0) {
this.$set(tableState.tableData[completedIndex], "taskState", 20)
}
const anchor = cache.anchor || {}
this.applyListCache(tableState, anchor.nextRowId || anchor.previousRowId, anchor)
},
/**
* 恢复 TableList 数据并将目标卡片放回离开页面前的视口位置。
* sticky 吸顶会改变一次布局,因此首次滚动后再执行一次位置校正。
*/
applyListCache(tableState, targetRowId, anchor) {
const tableList = this.$refs.tableListRef
if (!tableList) return
if (typeof tableList.restoreListState === "function") {
if (!tableList.restoreListState(tableState)) return
} else {
this.$set(tableList, "tableData", tableState.tableData || [])
this.$set(tableList, "localPageForm", {...tableList.localPageForm, ...(tableState.localPageForm || {})})
this.$set(tableList, "tableFinished", !!tableState.tableFinished)
this.$set(tableList, "tableLoading", false)
this.$set(tableList, "tableRefreshing", false)
tableList.$emit("update:page_form", {...tableList.localPageForm})
}
this.$set(this, "pendingListCache", null)
this.$nextTick(() => {
setTimeout(() => {
const cards = Array.from(document.querySelectorAll(".proposal-delegation-list .table-list-item"))
const scrollContainer = this.getListScrollContainer()
if (!scrollContainer) return
const candidateIds = [targetRowId, anchor.nextRowId, anchor.previousRowId]
.filter((id) => id)
.map((id) => String(id))
.filter((id, index, values) => values.indexOf(id) === index)
let targetIndex = -1
candidateIds.some((candidateId) => {
targetIndex = (tableState.tableData || []).findIndex((item) => String(item.id) === candidateId)
return targetIndex >= 0
})
const targetCard = targetIndex >= 0 ? cards[targetIndex] : null
if (targetCard) {
const restoreTargetCard = () => {
if (!targetCard.isConnected) return
const targetTop = scrollContainer.scrollTop + targetCard.getBoundingClientRect().top - Number(anchor.viewportTop || 0)
scrollContainer.scrollTo(0, Math.max(0, targetTop))
}
restoreTargetCard()
setTimeout(restoreTargetCard, 100)
return
}
scrollContainer.scrollTo(0, Number(anchor.scrollTop || 0))
}, 0)
})
},
onRevoke(row){
this.$dialog.confirm({
title: "提示",
@@ -176,12 +635,25 @@ layout("/layouts/platform_h5.html"){
},
doSearch() {
this.clearListCache()
this.$nextTick(() => {
this.pageForm.pageNumber = 1
this.pageForm.totalCount = 0
this.$refs.tableListRef.doSearch()
})
},
},
created() {
this.prepareListCache()
if (this.pendingListCache) return
const tab = new URLSearchParams(window.location.search).get("tab")
if (tab === "done") {
this.$set(this.pageForm, "approval", true)
this.$set(this.pageForm, "approvalText", "1")
} else if (tab === "todo") {
this.$set(this.pageForm, "approval", false)
this.$set(this.pageForm, "approvalText", "0")
}
}
})
</script>
@@ -0,0 +1,119 @@
<!--#
layout("/layouts/platform_h5.html"){
#-->
<div id="app" v-cloak>
<van-nav-bar @click-left="historyBack" left-arrow left-text="返回" :title="pageTitle" placeholder fixed></van-nav-bar>
<proposal-info v-if="bizId" :biz-id="bizId">
<div v-if="isApproval">
<div class="process-title">{{formData.taskName}}</div>
<van-form ref="formRef">
<van-field
v-model="formData.tf_opinion"
name="tf_opinion"
label="审批意见"
placeholder="请输入审批意见"
:rules="[{ required: true, message: '请填写审批意见' }]"
required
maxlength="100"
show-word-limit
></van-field>
</van-form>
<div style="display: flex; justify-content: space-between; column-gap: 10px; padding: 10px">
<van-button type="danger" block :loading="formLoading" @click="handleTaskAction(6)">退回</van-button>
<van-button type="danger" block :loading="formLoading" @click="handleTaskAction(2)">不同意</van-button>
<van-button type="primary" block :loading="formLoading" @click="handleTaskAction(1)">同意</van-button>
</div>
</div>
</proposal-info>
</div>
<script nonce="${cspNonce!}">
<!--#include("../../common/info.js"){}#-->
new Vue({
el: "#app",
store,
components: {
"proposal-info": PROPOSAL_INFO
},
data() {
return {
bizId: "",
action: "view",
formData: {},
formLoading: false
}
},
computed: {
isApproval() {
return this.action === "approval"
},
pageTitle() {
return this.isApproval ? "团长审核" : "提案详情"
}
},
methods: {
/**
* 提交团长审核结果。
* val 为 WF 提交类型:1 同意、2 不同意、6 退回;接口返回 Result。
*/
handleTaskAction(val) {
this.$refs.formRef.validate()
.then(() => this.$dialog.confirm({title: "提示", message: "您确定要提交吗?"}))
.then(() => {
this.formLoading = true
return this.$axios.post("/flow/common/executeTask", {
data: JSON.stringify({
...this.formData,
submitType: val
})
})
})
.then((res) => {
if (res && res.code === 0) {
this.markListCacheCompleted()
this.$toast.success(res.msg)
this.historyBack()
}
})
.catch(() => {})
.finally(() => {
this.formLoading = false
})
},
/**
* 团长审核成功后标记已处理提案主键。
* 返回未审核列表时根据该标记重新查询原浏览范围,并定位到相邻记录。
*/
markListCacheCompleted() {
const cacheKey = "proposal:delegation:h5:list-state"
try {
const cacheText = sessionStorage.getItem(cacheKey)
if (!cacheText) return
const cache = JSON.parse(cacheText)
cache.completedBizId = this.bizId
cache.time = Date.now()
sessionStorage.setItem(cacheKey, JSON.stringify(cache))
} catch (error) {
sessionStorage.removeItem(cacheKey)
}
}
},
created() {
const params = new URLSearchParams(window.location.search)
// bizId 用于查询详情,taskId 和 taskName 用于构造 WF 审核提交数据。
this.bizId = params.get("bizId") || ""
this.action = params.get("action") || "view"
if (this.isApproval) {
this.$set(this.formData, "processTaskId", params.get("taskId") || "")
this.$set(this.formData, "taskName", params.get("taskName") || "团长审核")
}
if (!this.bizId) {
this.$toast.fail("缺少提案参数")
}
}
})
</script>
<!--#
}
#-->
@@ -1,134 +1,329 @@
<!--#
layout("/layouts/platform_h5.html"){
#-->
<style>
body {
background-color: #f3f7fd;
}
.proposal-feedback-evaluation-page {
min-height: 100vh;
padding-bottom: calc(24px + env(safe-area-inset-bottom));
box-sizing: border-box;
background-color: #f3f7fd;
}
.proposal-feedback-evaluation-page .van-nav-bar {
background-color: #fff;
}
.proposal-feedback-evaluation-page .van-nav-bar__title {
color: #172033;
font-size: 16px;
font-weight: 600;
}
.proposal-feedback-evaluation-page .van-nav-bar .van-icon,
.proposal-feedback-evaluation-page .van-nav-bar__text {
color: #1989fa;
}
.proposal-feedback-evaluation-content {
padding: 0 12px;
}
.proposal-feedback-evaluation-sticky {
padding: 22px 12px 1px;
box-sizing: border-box;
background-color: #f3f7fd;
}
.proposal-feedback-evaluation-search {
margin: 0 0 8px;
padding: 0;
overflow: hidden;
border-radius: 12px;
background-color: #fff;
box-shadow: 0 5px 16px rgba(43, 73, 112, .1);
}
.proposal-feedback-evaluation-search .van-search__content {
height: 42px;
padding-left: 12px;
align-items: center;
border-radius: 12px;
background-color: #fff;
}
.proposal-feedback-evaluation-search .van-field__control {
color: #263548;
font-size: 14px;
}
.proposal-feedback-evaluation-search .van-field__control::placeholder {
color: #a7b0bf;
}
.proposal-feedback-evaluation-filter {
margin: 0 0 10px;
overflow: hidden;
border-radius: 10px;
box-shadow: 0 5px 16px rgba(43, 73, 112, .08);
transition: border-radius .18s ease;
}
.proposal-feedback-evaluation-filter.is-open {
border-radius: 10px 10px 0 0;
}
.proposal-feedback-evaluation-filter .van-dropdown-menu__bar,
.proposal-feedback-evaluation-filter .van-dropdown-menu__item,
.proposal-feedback-evaluation-filter .van-dropdown-menu__title {
height: 44px;
}
.proposal-feedback-evaluation-filter .van-dropdown-menu__bar {
box-shadow: none;
}
.proposal-feedback-evaluation-filter .van-dropdown-menu__item,
.proposal-feedback-evaluation-filter .van-dropdown-menu__title {
align-items: center;
}
.proposal-feedback-evaluation-filter .van-dropdown-menu__item {
justify-content: center;
}
.proposal-feedback-evaluation-filter .van-dropdown-menu__title {
display: inline-flex;
color: #66758a;
font-size: 14px;
line-height: 20px;
}
.proposal-feedback-evaluation-filter .van-dropdown-menu__title--active,
.proposal-feedback-evaluation-filter .van-dropdown-item__option--active,
.proposal-feedback-evaluation-filter .van-dropdown-item__option--active .van-dropdown-item__icon {
color: #1989fa;
}
.proposal-feedback-evaluation-filter .van-dropdown-menu__title--down::after {
border-color: transparent transparent #1989fa #1989fa;
}
.proposal-feedback-evaluation-filter .van-dropdown-item__content {
width: calc(100% - 24px);
margin: 0 12px;
overflow: hidden;
box-sizing: border-box;
border-radius: 0 0 12px 12px;
box-shadow: 0 10px 24px rgba(31, 65, 108, .14);
}
.proposal-feedback-evaluation-filter .van-dropdown-item__option {
display: flex;
min-height: 48px;
padding: 0 16px;
align-items: center;
color: #46566c;
font-size: 14px;
}
.proposal-feedback-evaluation-filter .van-cell::after {
right: 16px;
left: 16px;
}
.proposal-feedback-evaluation-list .empty-state {
display: none;
}
.proposal-feedback-evaluation-empty {
display: flex;
min-height: 280px;
padding: 46px 18px 36px;
align-items: center;
justify-content: center;
flex-direction: column;
box-sizing: border-box;
text-align: center;
}
.proposal-feedback-evaluation-empty img {
display: block;
width: 86%;
max-width: 290px;
height: auto;
object-fit: contain;
}
.proposal-feedback-evaluation-empty__title {
margin-top: 14px;
color: #50627a;
font-size: 15px;
line-height: 22px;
font-weight: 600;
}
.proposal-feedback-evaluation-empty__hint {
margin-top: 5px;
color: #9aa7b8;
font-size: 12px;
line-height: 18px;
}
.proposal-feedback-evaluation-card-header {
display: flex;
margin-bottom: 7px;
align-items: flex-start;
justify-content: space-between;
}
.proposal-feedback-evaluation-card-heading {
min-width: 0;
padding-right: 10px;
flex: 1;
}
.proposal-feedback-evaluation-card-title {
overflow: hidden;
color: #253247;
font-size: 15px;
line-height: 22px;
font-weight: 600;
text-overflow: ellipsis;
white-space: nowrap;
}
.proposal-feedback-evaluation-card-code {
margin-top: 1px;
color: #9ba6b6;
font-size: 12px;
line-height: 18px;
}
.proposal-feedback-evaluation-status {
display: inline-flex;
flex: none;
}
.proposal-feedback-evaluation-status .van-tag {
display: inline-flex;
min-height: 24px;
padding: 3px 9px;
align-items: center;
box-sizing: border-box;
border: 0;
border-radius: 12px;
font-size: 12px;
line-height: 18px;
white-space: nowrap;
}
@media (max-width: 350px) {
.proposal-feedback-evaluation-content,
.proposal-feedback-evaluation-sticky {
padding-right: 10px;
padding-left: 10px;
}
}
</style>
<div id="app" v-cloak>
<van-nav-bar @click-left="historyBack" left-arrow left-text="返回" title="反馈评价" placeholder fixed></van-nav-bar>
<van-sticky offset-top="46px">
<van-search
v-model="pageForm.name"
:show-action="false"
:reverse-color="false"
input-align="left"
placeholder="请输入提案名称搜索"
@search="doSearch"
></van-search>
<van-dropdown-menu :close-on-click-outside="false" :close-on-click-overlay="false">
<van-dropdown-item v-model="pageForm.sessionId" :options="sessionOptions" :multiple="false"
@change="doSearch"></van-dropdown-item>
</van-dropdown-menu>
<van-tabs v-model="pageForm.approvalText"
@change="(val)=>{this.pageForm.approval = val==='1';this.doSearch();}">
<van-tab title="已评价" name="1"></van-tab>
<van-tab title="未评价" name="0"></van-tab>
</van-tabs>
</van-sticky>
<table-list api="/platform/proposal/feedbackEvaluation/pageData" :page_form.sync="pageForm" @ready="onReady"
ref="tableListRef"
title="name">
<template v-slot="{index,row}">
<table-column label="提案编号">{{row.code}}</table-column>
<table-column label="提案类别">{{row.typeName}}</table-column>
<table-column label="提案人">{{row.createUserName}}</table-column>
<table-column label="代表团">{{row.delegationName}}</table-column>
<table-column label="当前节点">{{row.curTaskName}}</table-column>
</template>
<template #actions="{index,row}">
<div class="action-btn" @click="onView(row)">
<i class="fa fa-eye"></i>
<span>查看</span>
<div class="proposal-feedback-evaluation-page">
<van-nav-bar @click-left="historyBack" left-arrow left-text="返回" title="反馈评价" placeholder fixed></van-nav-bar>
<van-sticky :offset-top="46">
<div class="proposal-feedback-evaluation-sticky">
<van-search
v-model="pageForm.name"
class="proposal-feedback-evaluation-search"
placeholder="请输入提案名称搜索"
shape="round"
clearable
@search="doSearch"
@clear="doSearch"
></van-search>
<van-dropdown-menu
class="proposal-feedback-evaluation-filter"
:class="{'is-open': filterOpened}"
active-color="#1989fa"
>
<van-dropdown-item
v-model="pageForm.sessionId"
:options="sessionOptions"
:multiple="false"
@change="doSearch"
@open="filterOpened = true"
@close="filterOpened = false"
></van-dropdown-item>
<van-dropdown-item
v-model="pageForm.approvalText"
:options="approvalOptions"
:multiple="false"
@change="approvalChange"
@open="filterOpened = true"
@close="filterOpened = false"
></van-dropdown-item>
</van-dropdown-menu>
</div>
<div class="action-btn" v-if="row.taskState === 10" @click="onApproval(row)">
<i class="fa fa-edit"></i>
<span>评价</span>
</div>
<div class="action-btn delete" v-if="row.canRevoke" @click="onRevoke(row)">
<i class="fa fa-reply"></i>
<span>撤回</span>
</div>
</template>
</table-list>
</van-sticky>
<proposal-info ref="proposalInfoRef">
<div v-if="showApprovalForm">
<div class="process-title">{{formData.taskName}}</div>
<van-form ref="formRef">
<van-field
v-model="formData.tf_feedback_name"
name="tf_feedback_name"
label="满意度"
placeholder="请选择满意度"
:rules="[{ required: true, message: '请选择满意度' }]"
required
is-link
@click="showFeedBackPicker = true"
></van-field>
<van-popup v-model="showFeedBackPicker" position="bottom">
<van-picker
show-toolbar
:columns="dict.type.PROPOSAL_FEEDBACK.map(item => ({text: item.label, value: item.code}))"
@confirm="onFeedBackConfirm"
@cancel="showFeedBackPicker = false"
></van-picker>
</van-popup>
<van-field
v-model="formData.tf_opinion"
name="tf_opinion"
label="审批意见"
placeholder="请输入审批意见"
:rules="[{ required: true, message: '请填写审批意见' }]"
required
maxlength="100"
show-word-limit
></van-field>
<template v-if="['SATISFIED','FAIRLY_SATISFIED'].includes(formData.tf_feedback)">
<van-field
v-model="formData.tf_address"
name="tf_address"
label="通讯地址"
placeholder="请输入通讯地址"
:rules="[{ required: true, message: '请输入通讯地址' }]"
required
show-word-limit
></van-field>
<!--联系电话-->
<van-field
v-model="formData.tf_phone"
name="tf_phone"
label="联系电话"
placeholder="请输入联系电话"
:rules="[{ required: true, message: '请输入联系电话' }]"
required
show-word-limit
></van-field>
<!--邮政编码-->
<van-field
v-model="formData.tf_postcode"
name="tf_postcode"
label="邮政编码"
placeholder="请输入邮政编码"
:rules="[{ required: true, message: '请输入邮政编码' }]"
required
show-word-limit
></van-field>
</template>
</van-form>
<div style="display: flex; justify-content: space-between; column-gap: 10px; padding: 10px">
<van-button type="danger" block @click="$refs.proposalInfoRef.onClose()">取消</van-button>
<van-button type="primary" block @click="handleTaskAction(1)">提交</van-button>
<main class="proposal-feedback-evaluation-content">
<div class="proposal-feedback-evaluation-list">
<!-- 列表无记录时使用与团长审核一致的提案专用空状态。 -->
<div v-if="pageForm.totalCount === 0" class="proposal-feedback-evaluation-empty">
<img src="/assets/mobile/img/proposal/proposal-empty.png" alt="暂无提案记录插画">
<div class="proposal-feedback-evaluation-empty__title">暂无提案记录</div>
<div class="proposal-feedback-evaluation-empty__hint">当前筛选条件下暂时没有提案</div>
</div>
<table-list api="/platform/proposal/feedbackEvaluation/pageData" :page_form.sync="pageForm" @ready="onReady"
ref="tableListRef"
title="name">
<template #header="{index,row}">
<div class="proposal-feedback-evaluation-card-header">
<div class="proposal-feedback-evaluation-card-heading">
<div class="proposal-feedback-evaluation-card-title">{{row.name}}</div>
<div class="proposal-feedback-evaluation-card-code">编号:{{row.code || '--'}}</div>
</div>
<enum-tag
class="proposal-feedback-evaluation-status"
v-if="row.instanceState"
:value="row.instanceState"
name="ProcessInstanceStateEnum"
label_key="message"
size="small"
></enum-tag>
</div>
</template>
<template v-slot="{index,row}">
<table-column label="提案类别">{{row.typeName || '--'}}</table-column>
<table-column label="提案人">{{row.createUserName || '--'}}</table-column>
<table-column label="代表团">{{row.delegationName || '--'}}</table-column>
<table-column label="当前节点">{{row.curTaskName || '--'}}</table-column>
</template>
<template #actions="{index,row}">
<div class="action-btn" @click="onView(row,index)">
<i class="fa fa-eye"></i>
<span>查看</span>
</div>
<div class="action-btn" v-if="row.taskState === 10" @click="onApproval(row,index)">
<i class="fa fa-edit"></i>
<span>评价</span>
</div>
<div class="action-btn delete" v-if="row.canRevoke" @click="onRevoke(row)">
<i class="fa fa-reply"></i>
<span>撤回</span>
</div>
</template>
</table-list>
</div>
</div>
</proposal-info>
</main>
</div>
</div>
<script nonce="${cspNonce!}">
<!--#include("../../common/info.js"){}#-->
const vue = new Vue({
el: "#app",
dicts: ["PROPOSAL_FEEDBACK"],
store,
data() {
return {
@@ -136,108 +331,304 @@ layout("/layouts/platform_h5.html"){
pageNumber: 1,
pageSize: 10,
totalCount: 0,
pageOrderName: "",
pageOrderBy: "",
name: null,
sessionId: null,
approvalText: "0",
approval: false
},
sessionOptions: [],
formData: {},
showApprovalForm: false,
showFeedBackPicker: false
filterOpened: false,
approvalOptions: [
{text: "未评价", value: "0"},
{text: "已评价", value: "1"}
],
pendingListCache: null
}
},
components: {
"proposal-info": PROPOSAL_INFO
},
methods: {
onReady() {
this.listSession()
if (this.pendingListCache) {
this.restoreListCache()
this.listSession(true)
return
}
this.listSession(false)
},
listSession() {
// 评价状态下拉框使用字符串值展示,查询接口使用布尔值区分已评价和未评价。
approvalChange(value) {
this.$set(this.pageForm, "approval", value === "1")
this.doSearch()
},
// 缓存恢复时只加载届次选项,不覆盖离开列表前的筛选条件和列表快照。
listSession(skipDefaultSearch) {
this.$axios.post("/platform/proposal/common/listSession").then((res) => {
if (res.code === 0) {
this.sessionOptions = [
{
text: "全部届次",
value: null
}
].concat(res.data.map((v) => ({text: v.fullName, value: v.id})))
if (this.sessionOptions.length > 0) {
this.pageForm.sessionId = this.sessionOptions[0].value
this.doSearch()
}
this.sessionOptions = [{text: "全部届次", value: null}]
.concat(res.data.map((item) => ({text: item.fullName, value: item.id})))
if (skipDefaultSearch) return
// 保留反馈评价原有默认口径:首次进入查询全部届次。
this.$set(this.pageForm, "sessionId", null)
this.doSearch()
}
})
},
onView(row) {
this.showApprovalForm = false
this.$refs.proposalInfoRef.onOpen(row)
onView(row, index) {
this.saveListCache(row, index)
this.$pjaxReplace("/platform/proposal/feedbackEvaluation/info/h5?bizId=" + encodeURIComponent(row.id) + "&action=view")
},
onApproval(row) {
this.showApprovalForm = true
this.$refs.proposalInfoRef.onOpen(row)
this.formData = {
processTaskId: row.taskId,
taskName: row.curTaskName,
instanceId: row.instanceId
onApproval(row, index) {
this.saveListCache(row, index)
const params = new URLSearchParams()
params.set("bizId", row.id)
params.set("action", "approval")
params.set("taskId", row.taskId)
params.set("taskName", row.curTaskName || "反馈评价")
params.set("instanceId", row.instanceId)
this.$pjaxReplace("/platform/proposal/feedbackEvaluation/info/h5?" + params.toString())
},
getListCacheKey() {
return "proposal:feedback-evaluation:h5:list-state"
},
getListCacheTokenParam() {
return "feedbackEvaluationListCacheToken"
},
getCurrentListCacheToken() {
return new URLSearchParams(window.location.search).get(this.getListCacheTokenParam()) || ""
},
/**
* 将反馈评价列表缓存标识绑定到当前历史记录。
* PJAX 返回时仅恢复当前历史记录对应的筛选条件、分页数据和滚动快照。
*/
bindListCacheToken(token) {
const currentUrl = new URL(window.location.href)
currentUrl.searchParams.set(this.getListCacheTokenParam(), token)
const currentState = window.history.state ? {...window.history.state} : null
if (currentState && currentState.url) {
currentState.url = currentUrl.toString()
}
window.history.replaceState(currentState, document.title, currentUrl.toString())
if ($.pjax && $.pjax.state) {
$.pjax.state.url = currentUrl.toString()
}
},
onFeedBackConfirm({value, text}) {
this.formData.tf_feedback_name = text
this.formData.tf_feedback = value
this.showFeedBackPicker = false
// 用户主动搜索、切换届次或评价状态时使旧返回缓存失效。
clearListCache() {
const token = this.getCurrentListCacheToken()
if (!token) return
const currentUrl = new URL(window.location.href)
currentUrl.searchParams.delete(this.getListCacheTokenParam())
const currentState = window.history.state ? {...window.history.state} : null
if (currentState && currentState.url) {
currentState.url = currentUrl.toString()
}
window.history.replaceState(currentState, document.title, currentUrl.toString())
if ($.pjax && $.pjax.state) {
$.pjax.state.url = currentUrl.toString()
}
sessionStorage.removeItem(this.getListCacheKey())
},
async handleTaskAction(val) {
// platform_h5 的实际滚动区域是 page-wrapper,列表位置必须基于该容器保存和恢复。
getListScrollContainer() {
return document.querySelector(".page-wrapper") || document.scrollingElement || document.documentElement
},
/**
* 进入查看或评价详情前保存列表现场。
* row 为当前反馈任务,index 为已加载列表索引;任务和相邻锚点统一使用唯一 taskId。
*/
saveListCache(row, index) {
const tableList = this.$refs.tableListRef
if (!tableList) return
const cacheToken = Date.now() + "-" + Math.random().toString(36).slice(2)
const tableState = typeof tableList.getListState === "function"
? tableList.getListState()
: {
tableData: JSON.parse(JSON.stringify(tableList.tableData || [])),
localPageForm: JSON.parse(JSON.stringify(tableList.localPageForm || this.pageForm)),
tableFinished: !!tableList.tableFinished
}
const cards = document.querySelectorAll(".proposal-feedback-evaluation-list .table-list-item")
const currentCard = cards[index]
const scrollContainer = this.getListScrollContainer()
const cache = {
time: Date.now(),
token: cacheToken,
pageForm: JSON.parse(JSON.stringify(this.pageForm)),
tableState: tableState,
completedTaskId: "",
anchor: {
taskId: row.taskId,
previousTaskId: index > 0 && tableState.tableData[index - 1]
? tableState.tableData[index - 1].taskId
: "",
nextTaskId: tableState.tableData[index + 1] ? tableState.tableData[index + 1].taskId : "",
viewportTop: currentCard ? currentCard.getBoundingClientRect().top : 0,
scrollTop: scrollContainer ? scrollContainer.scrollTop : 0
}
}
try {
await this.$refs.formRef.validate();
this.$dialog.confirm({
title: "提示",
message: "您确定要提交吗?"
}).then(async () => {
debugger
const formData = {
...this.formData,
submitType: val
}
// 不满意
if (this.formData.tf_feedback === 'DISSATISFIED') {
const caseInfo = await this.getCaseInfo(this.formData.instanceId)
formData.tf_masterUnitId = caseInfo.tf_masterUnitId
}
this.$axios.post("/flow/common/executeTask", {
data: JSON.stringify(formData)
}).then((res) => {
if (res.code === 0) {
this.$refs.proposalInfoRef.onClose()
this.$toast.success(res.msg)
this.doSearch()
}
})
}).catch(() => {
})
sessionStorage.setItem(this.getListCacheKey(), JSON.stringify(cache))
this.bindListCacheToken(cacheToken)
} catch (error) {
sessionStorage.removeItem(this.getListCacheKey())
}
},
// 仅恢复当前历史记录对应且十分钟内有效的反馈评价列表缓存。
prepareListCache() {
try {
const cacheToken = this.getCurrentListCacheToken()
if (!cacheToken) return
const cacheText = sessionStorage.getItem(this.getListCacheKey())
if (!cacheText) return
const cache = JSON.parse(cacheText)
if (!cache || cache.token !== cacheToken || !cache.tableState) return
if (Date.now() - cache.time > 10 * 60 * 1000) {
this.clearListCache()
return
}
Object.keys(cache.pageForm || {}).forEach((key) => {
this.$set(this.pageForm, key, cache.pageForm[key])
})
this.$set(this, "pendingListCache", cache)
} catch (error) {
sessionStorage.removeItem(this.getListCacheKey())
}
},
// 获取立案信息
async getCaseInfo(instId) {
const {
data,
code
} = await this.$axios.post('/platform/proposal/feedbackEvaluation/caseInfo', {instId})
if (code === 0) {
return data
// 普通查看返回恢复旧快照;评价完成返回重新查询,避免未评价列表继续显示已处理任务。
restoreListCache() {
const cache = this.pendingListCache
const tableList = this.$refs.tableListRef
if (!cache || !tableList) return
const anchor = cache.anchor || {}
if (cache.completedTaskId) {
this.refreshCompletedListCache(cache)
return
}
this.applyListCache(cache.tableState, anchor.taskId, anchor)
},
/**
* 按离开前已加载页数重新获取评价后的列表。
* 返回值转换为 TableList 状态,并优先定位下一任务,没有下一任务时定位上一任务。
*/
refreshCompletedListCache(cache) {
const originalPageSize = Number(cache.pageForm.pageSize || 10)
const loadedCount = Math.max(cache.tableState.tableData.length, originalPageSize)
const loadedPageCount = Math.max(1, Math.ceil(loadedCount / originalPageSize))
const requestForm = {
...cache.pageForm,
pageNumber: 1,
pageSize: loadedPageCount * originalPageSize
}
const loading = createListLoading()
this.$axios.post("/platform/proposal/feedbackEvaluation/pageData", requestForm, {h5SkeletonLoading: true})
.then((res) => {
if (res.code !== 0) {
this.applyCompletedCacheFallback(cache)
return
}
const freshState = {
tableData: res.data.list || [],
localPageForm: {
...cache.pageForm,
pageNumber: loadedPageCount,
pageSize: originalPageSize,
totalCount: res.data.totalCount
},
tableFinished: (res.data.list || []).length >= res.data.totalCount
}
const anchor = cache.anchor || {}
this.applyListCache(freshState, anchor.nextTaskId || anchor.previousTaskId, anchor)
})
.catch(() => {
this.applyCompletedCacheFallback(cache)
})
.finally(() => {
loading.close()
})
},
// 评价后刷新失败时只处理已完成 taskId,避免误改同一提案的其他流程记录。
applyCompletedCacheFallback(cache) {
const tableState = cache.tableState
const completedIndex = tableState.tableData.findIndex((item) => String(item.taskId) === String(cache.completedTaskId))
if (completedIndex >= 0 && cache.pageForm.approval === false) {
tableState.tableData.splice(completedIndex, 1)
if (tableState.localPageForm && tableState.localPageForm.totalCount > 0) {
tableState.localPageForm.totalCount--
}
} else if (completedIndex >= 0) {
this.$set(tableState.tableData[completedIndex], "taskState", 20)
}
const anchor = cache.anchor || {}
this.applyListCache(tableState, anchor.nextTaskId || anchor.previousTaskId, anchor)
},
/**
* 恢复 TableList 数据并将目标任务卡片放回离开页面前的视口位置。
* sticky 吸顶会改变一次布局,因此首次滚动后再执行一次位置校正。
*/
applyListCache(tableState, targetTaskId, anchor) {
const tableList = this.$refs.tableListRef
if (!tableList) return
if (typeof tableList.restoreListState === "function") {
if (!tableList.restoreListState(tableState)) return
} else {
this.$set(tableList, "tableData", tableState.tableData || [])
this.$set(tableList, "localPageForm", {...tableList.localPageForm, ...(tableState.localPageForm || {})})
this.$set(tableList, "tableFinished", !!tableState.tableFinished)
this.$set(tableList, "tableLoading", false)
this.$set(tableList, "tableRefreshing", false)
tableList.$emit("update:page_form", {...tableList.localPageForm})
}
this.$set(this, "pendingListCache", null)
this.$nextTick(() => {
setTimeout(() => {
const cards = Array.from(document.querySelectorAll(".proposal-feedback-evaluation-list .table-list-item"))
const scrollContainer = this.getListScrollContainer()
if (!scrollContainer) return
const candidateTaskIds = [targetTaskId, anchor.nextTaskId, anchor.previousTaskId]
.filter((taskId) => taskId)
.map((taskId) => String(taskId))
.filter((taskId, index, values) => values.indexOf(taskId) === index)
let targetIndex = -1
candidateTaskIds.some((candidateTaskId) => {
targetIndex = (tableState.tableData || [])
.findIndex((item) => String(item.taskId) === candidateTaskId)
return targetIndex >= 0
})
const targetCard = targetIndex >= 0 ? cards[targetIndex] : null
if (targetCard) {
const restoreTargetCard = () => {
if (!targetCard.isConnected) return
const targetTop = scrollContainer.scrollTop
+ targetCard.getBoundingClientRect().top
- Number(anchor.viewportTop || 0)
scrollContainer.scrollTo(0, Math.max(0, targetTop))
}
restoreTargetCard()
setTimeout(restoreTargetCard, 100)
return
}
scrollContainer.scrollTo(0, Number(anchor.scrollTop || 0))
}, 0)
})
},
onRevoke(row) {
this.$dialog.confirm({
title: "提示",
@@ -253,12 +644,25 @@ layout("/layouts/platform_h5.html"){
},
doSearch() {
this.clearListCache()
this.$nextTick(() => {
this.pageForm.pageNumber = 1
this.pageForm.totalCount = 0
this.$refs.tableListRef.doSearch()
})
},
}
},
created() {
this.prepareListCache()
if (this.pendingListCache) return
const tab = new URLSearchParams(window.location.search).get("tab")
if (tab === "done") {
this.$set(this.pageForm, "approval", true)
this.$set(this.pageForm, "approvalText", "1")
} else if (tab === "todo") {
this.$set(this.pageForm, "approval", false)
this.$set(this.pageForm, "approvalText", "0")
}
}
})
</script>
@@ -0,0 +1,184 @@
<!--#
layout("/layouts/platform_h5.html"){
#-->
<div id="app" v-cloak>
<van-nav-bar @click-left="historyBack" left-arrow left-text="返回" :title="pageTitle" placeholder fixed></van-nav-bar>
<proposal-info v-if="bizId" :biz-id="bizId">
<div v-if="isApproval">
<div class="process-title">{{formData.taskName}}</div>
<van-form ref="formRef">
<van-field
v-model="formData.tf_feedback_name"
name="tf_feedback_name"
label="满意度"
placeholder="请选择满意度"
:rules="[{ required: true, message: '请选择满意度' }]"
required
is-link
@click="showFeedBackPicker = true"
></van-field>
<van-popup v-model="showFeedBackPicker" position="bottom">
<van-picker
show-toolbar
:columns="dict.type.PROPOSAL_FEEDBACK.map(item => ({text: item.label, value: item.code}))"
@confirm="onFeedBackConfirm"
@cancel="showFeedBackPicker = false"
></van-picker>
</van-popup>
<van-field
v-model="formData.tf_opinion"
name="tf_opinion"
label="审批意见"
placeholder="请输入审批意见"
:rules="[{ required: true, message: '请填写审批意见' }]"
required
maxlength="100"
show-word-limit
></van-field>
<!-- <template v-if="['SATISFIED','FAIRLY_SATISFIED'].includes(formData.tf_feedback)">
<van-field
v-model="formData.tf_address"
name="tf_address"
label="通讯地址"
placeholder="请输入通讯地址"
:rules="[{ required: true, message: '请输入通讯地址' }]"
required
></van-field>
<van-field
v-model="formData.tf_phone"
name="tf_phone"
label="联系电话"
placeholder="请输入联系电话"
:rules="[{ required: true, message: '请输入联系电话' }]"
required
></van-field>
<van-field
v-model="formData.tf_postcode"
name="tf_postcode"
label="邮政编码"
placeholder="请输入邮政编码"
:rules="[{ required: true, message: '请输入邮政编码' }]"
required
></van-field>
</template>-->
</van-form>
<div style="display: flex; justify-content: space-between; column-gap: 10px; padding: 10px">
<van-button type="danger" block :disabled="formLoading" @click="historyBack">取消</van-button>
<van-button type="primary" block :loading="formLoading" @click="handleTaskAction">提交</van-button>
</div>
</div>
</proposal-info>
</div>
<script nonce="${cspNonce!}">
<!--#include("../../common/info.js"){}#-->
new Vue({
el: "#app",
store,
dicts: ["PROPOSAL_FEEDBACK"],
components: {
"proposal-info": PROPOSAL_INFO
},
data() {
return {
bizId: "",
action: "view",
formData: {},
formLoading: false,
showFeedBackPicker: false
}
},
computed: {
isApproval() {
return this.action === "approval"
},
pageTitle() {
return this.isApproval ? "反馈评价" : "提案详情"
}
},
methods: {
onFeedBackConfirm(value) {
this.$set(this.formData, "tf_feedback_name", value.text)
this.$set(this.formData, "tf_feedback", value.value)
this.showFeedBackPicker = false
},
/**
* 提交反馈评价;不满意时先用 instanceId 查询立案主办单位,再提交完整 WF 表单数据。
* 两个接口均返回 Result,code 为 0 表示成功。
*/
handleTaskAction() {
this.$refs.formRef.validate()
.then(() => this.$dialog.confirm({title: "提示", message: "您确定要提交吗?"}))
.then(() => {
this.formLoading = true
const submitData = {...this.formData, submitType: 1}
if (this.formData.tf_feedback !== "DISSATISFIED") {
return submitData
}
return this.$axios.post("/platform/proposal/feedbackEvaluation/caseInfo", {
instId: this.formData.instanceId
}).then((res) => {
if (res.code !== 0) {
return Promise.reject(new Error(res.msg || "查询立案信息失败"))
}
submitData.tf_masterUnitId = res.data.tf_masterUnitId
return submitData
})
})
.then((submitData) => this.$axios.post("/flow/common/executeTask", {
data: JSON.stringify(submitData)
}))
.then((res) => {
if (res && res.code === 0) {
this.markListCacheCompleted()
this.$toast.success(res.msg)
this.historyBack()
}
})
.catch((error) => {
if (error && error.message && error.message !== "cancel") {
this.$toast.fail(error.message)
}
})
.finally(() => {
this.formLoading = false
})
},
/**
* 评价成功后记录已处理 WF 任务主键。
* 返回列表时按原筛选和已加载范围刷新数据,并定位到相邻评价任务。
*/
markListCacheCompleted() {
const cacheKey = "proposal:feedback-evaluation:h5:list-state"
try {
const cacheText = sessionStorage.getItem(cacheKey)
if (!cacheText) return
const cache = JSON.parse(cacheText)
cache.completedTaskId = this.formData.processTaskId || ""
cache.time = Date.now()
sessionStorage.setItem(cacheKey, JSON.stringify(cache))
} catch (error) {
sessionStorage.removeItem(cacheKey)
}
}
},
created() {
const params = new URLSearchParams(window.location.search)
// bizId 用于详情查询,taskId、taskName、instanceId 用于反馈评价的 WF 提交与立案信息查询。
this.bizId = params.get("bizId") || ""
this.action = params.get("action") || "view"
if (this.isApproval) {
this.$set(this.formData, "processTaskId", params.get("taskId") || "")
this.$set(this.formData, "taskName", params.get("taskName") || "反馈评价")
this.$set(this.formData, "instanceId", params.get("instanceId") || "")
}
if (!this.bizId) {
this.$toast.fail("缺少提案参数")
}
}
})
</script>
<!--#
}
#-->
@@ -1,75 +1,384 @@
<!--#
layout("/layouts/platform_h5.html"){
#-->
<style>
body {
background-color: #f3f7fd;
}
.proposal-mine-page {
min-height: 100vh;
padding-bottom: calc(24px + env(safe-area-inset-bottom));
box-sizing: border-box;
background-color: #f3f7fd;
}
.proposal-mine-page .van-nav-bar {
background-color: #fff;
}
.proposal-mine-page .van-nav-bar__title {
color: #172033;
font-size: 16px;
font-weight: 600;
}
.proposal-mine-page .van-nav-bar .van-icon,
.proposal-mine-page .van-nav-bar__text {
color: #1989fa;
}
.proposal-mine-content {
padding: 0 12px;
}
.proposal-mine-sticky {
padding: 22px 12px 1px;
box-sizing: border-box;
background-color: #f3f7fd;
}
.proposal-mine-search {
margin: 0 0 8px;
padding: 0;
overflow: hidden;
border-radius: 12px;
background-color: #fff;
box-shadow: 0 5px 16px rgba(43, 73, 112, .1);
}
.proposal-mine-search .van-search__content {
height: 42px;
padding-left: 12px;
align-items: center;
border-radius: 12px;
background-color: #fff;
}
.proposal-mine-search .van-field__control {
color: #263548;
font-size: 14px;
}
.proposal-mine-search .van-field__control::placeholder {
color: #a7b0bf;
}
.proposal-mine-filter {
margin: 0 0 10px;
overflow: hidden;
border-radius: 10px;
box-shadow: 0 5px 16px rgba(43, 73, 112, .08);
transition: border-radius .18s ease;
}
.proposal-mine-filter.is-open {
border-radius: 10px 10px 0 0;
}
.proposal-mine-filter .van-dropdown-menu__bar {
height: 44px;
box-shadow: none;
}
.proposal-mine-filter .van-dropdown-menu__item {
height: 44px;
align-items: center;
justify-content: center;
}
.proposal-mine-filter .van-dropdown-menu__title {
display: inline-flex;
height: 44px;
align-items: center;
line-height: 20px;
color: #66758a;
font-size: 14px;
}
.proposal-mine-filter .van-dropdown-menu__title--active,
.proposal-mine-filter .van-dropdown-item__option--active,
.proposal-mine-filter .van-dropdown-item__option--active .van-dropdown-item__icon {
color: #1989fa;
}
.proposal-mine-filter .van-dropdown-item__content {
width: calc(100% - 24px);
margin: 0 12px;
overflow: hidden;
box-sizing: border-box;
border-radius: 0 0 12px 12px;
box-shadow: 0 10px 24px rgba(31, 65, 108, .14);
}
.proposal-mine-filter .van-dropdown-item__option {
display: flex;
min-height: 48px;
padding: 0 16px;
align-items: center;
color: #46566c;
font-size: 14px;
}
.proposal-mine-filter .van-dropdown-item__option .van-cell__title {
display: flex;
min-height: 20px;
align-items: center;
line-height: 20px;
}
.proposal-mine-list {
padding-bottom: 2px;
}
.proposal-mine-list .empty-state {
display: none;
}
.proposal-mine-empty {
display: flex;
min-height: 280px;
padding: 46px 18px 36px;
align-items: center;
justify-content: center;
flex-direction: column;
box-sizing: border-box;
text-align: center;
}
.proposal-mine-empty img {
display: block;
width: 86%;
max-width: 290px;
height: auto;
object-fit: contain;
}
.proposal-mine-empty__title {
margin-top: 14px;
color: #50627a;
font-size: 15px;
line-height: 22px;
font-weight: 600;
}
.proposal-mine-empty__hint {
margin-top: 5px;
color: #9aa7b8;
font-size: 12px;
line-height: 18px;
}
.proposal-invite-empty {
display: flex;
min-height: 300px;
padding: 40px 18px 36px;
align-items: center;
justify-content: center;
flex-direction: column;
box-sizing: border-box;
text-align: center;
}
.proposal-invite-empty img {
display: block;
width: 82%;
max-width: 270px;
height: auto;
object-fit: contain;
}
.proposal-invite-empty__title {
margin-top: 14px;
color: #50627a;
font-size: 15px;
line-height: 22px;
font-weight: 600;
}
.proposal-invite-empty__hint {
margin-top: 5px;
color: #9aa7b8;
font-size: 12px;
line-height: 18px;
}
.proposal-card-header {
display: flex;
margin-bottom: 7px;
align-items: flex-start;
justify-content: space-between;
}
.proposal-card-heading {
min-width: 0;
padding-right: 10px;
flex: 1;
}
.proposal-card-title {
overflow: hidden;
color: #253247;
font-size: 15px;
line-height: 22px;
font-weight: 600;
text-overflow: ellipsis;
white-space: nowrap;
}
.proposal-card-code {
margin-top: 1px;
color: #9ba6b6;
font-size: 12px;
line-height: 18px;
}
.proposal-card-status {
display: inline-flex;
flex: none;
}
.proposal-card-status .van-tag,
.proposal-card-status--plain {
display: inline-flex;
min-height: 24px;
padding: 3px 9px;
align-items: center;
box-sizing: border-box;
border: 0;
border-radius: 12px;
font-size: 12px;
line-height: 18px;
white-space: nowrap;
}
.proposal-card-status--plain {
color: #1989fa;
background-color: #eaf4ff;
}
@media (max-width: 350px) {
.proposal-mine-content {
padding-right: 10px;
padding-left: 10px;
}
.proposal-mine-sticky {
padding-right: 10px;
padding-left: 10px;
}
}
</style>
<div id="app" v-cloak>
<van-nav-bar @click-left="historyBack" left-arrow left-text="返回" title="我的提案" placeholder fixed></van-nav-bar>
<van-sticky offset-top="46px">
<van-search
:reverse-color="false"
:show-action="false"
@search="doSearch"
input-align="left"
placeholder="请输入提案名称搜索"
v-model="pageForm.name"
></van-search>
<van-dropdown-menu :close-on-click-outside="false" :close-on-click-overlay="false">
<van-dropdown-item :multiple="false" :options="sessionOptions" @change="doSearch"
v-model="pageForm.sessionId"></van-dropdown-item>
</van-dropdown-menu>
</van-sticky>
<div class="proposal-mine-page">
<van-nav-bar @click-left="historyBack" left-arrow left-text="返回" title="我的提案" placeholder fixed></van-nav-bar>
<table-list api="/platform/proposal/mine/pageData" :page_form.sync="pageForm" @ready="onReady" ref="tableListRef"
title="name">
<template v-slot="{index,row}">
<table-column label="提案编号">{{row.code}}</table-column>
<table-column label="提案类别">{{row.typeName}}</table-column>
<table-column label="提案人">{{row.createUserName}}</table-column>
<table-column label="代表团">{{row.delegationName}}</table-column>
<table-column label="当前节点">{{ (row.startTaskId) ? row.taskName : '待提交' }}</table-column>
</template>
<template #actions="{index,row}">
<div class="action-btn" @click="onView(row)">
<i class="fa fa-eye"></i>
<span>查看</span>
<van-sticky :offset-top="46">
<div class="proposal-mine-sticky">
<van-search
v-model="pageForm.name"
class="proposal-mine-search"
placeholder="请输入提案名称搜索"
shape="round"
clearable
@search="doSearch"
@clear="doSearch"
></van-search>
<van-dropdown-menu
class="proposal-mine-filter"
:class="{'is-open': filterOpened}"
active-color="#1989fa"
:close-on-click-outside="false"
:close-on-click-overlay="false"
>
<van-dropdown-item
v-model="pageForm.sessionId"
:multiple="false"
:options="sessionOptions"
@change="doSearch"
@open="filterOpened = true"
@close="filterOpened = false"
></van-dropdown-item>
</van-dropdown-menu>
</div>
<div class="action-btn" v-if="row.taskKey === 'startTask' || !row.instanceId"
@click="onEdit(row)">
<i class="fa fa-edit"></i>
<span>编辑</span>
</div>
<div class="action-btn"
v-if="row.taskKey === 'invite' || row.taskKey === 'second'"
@click="onInvite(row)">
<i class="fa fa-user-plus"></i>
<span>邀请附议人</span>
</div>
<div class="action-btn delete" v-if="row.canRevoke" @click="onRevoke(row)">
<i class="fa fa-undo"></i>
<span>撤回</span>
</div>
<div class="action-btn delete" v-if="row.taskKey === 'startTask' || !row.instanceId"
@click="onDelete(row)">
<i class="fa fa-trash"></i>
<span>删除</span>
</div>
</template>
</table-list>
</van-sticky>
<invite-seconder @refresh="doSearch" ref="inviteSeconderRef"></invite-seconder>
<main class="proposal-mine-content">
<div class="proposal-mine-list">
<div v-if="pageForm.totalCount === 0" class="proposal-mine-empty">
<img src="/assets/mobile/img/proposal/proposal-empty.png" alt="暂无提案记录插画">
<div class="proposal-mine-empty__title">暂无提案记录</div>
<div class="proposal-mine-empty__hint">当前筛选条件下暂时没有提案</div>
</div>
<table-list api="/platform/proposal/mine/pageData" :page_form.sync="pageForm" @ready="onReady" ref="tableListRef"
title="name">
<template #header="{index,row}">
<div class="proposal-card-header">
<div class="proposal-card-heading">
<div class="proposal-card-title">{{row.name}}</div>
<div class="proposal-card-code">编号:{{row.code || '--'}}</div>
</div>
<!-- 流程已启动时显示真实实例状态,尚未启动的提案统一标记为待提交。 -->
<enum-tag
class="proposal-card-status"
v-if="row.instanceState"
:value="row.instanceState"
name="ProcessInstanceStateEnum"
label_key="message"
size="small"
></enum-tag>
<span v-else class="proposal-card-status proposal-card-status--plain">待提交</span>
</div>
</template>
<template v-slot="{index,row}">
<table-column label="提案类别">{{row.typeName || '--'}}</table-column>
<table-column label="提案人">{{row.createUserName || '--'}}</table-column>
<table-column label="代表团">{{row.delegationName || '--'}}</table-column>
<table-column label="当前节点">{{ (row.startTaskId) ? row.taskName : '待提交' }}</table-column>
</template>
<template #actions="{index,row}">
<div class="action-btn" @click="onView(row)">
<i class="fa fa-eye"></i>
<span>查看</span>
</div>
<div class="action-btn" v-if="row.taskKey === 'startTask' || !row.instanceId"
@click="onEdit(row)">
<i class="fa fa-edit"></i>
<span>编辑</span>
</div>
<div class="action-btn"
v-if="row.taskKey === 'invite' || row.taskKey === 'second'"
@click="onInvite(row)">
<i class="fa fa-user-plus"></i>
<span>邀请附议人</span>
</div>
<div class="action-btn delete" v-if="row.canRevoke" @click="onRevoke(row)">
<i class="fa fa-undo"></i>
<span>撤回</span>
</div>
<div class="action-btn delete" v-if="row.taskKey === 'startTask' || !row.instanceId"
@click="onDelete(row)">
<i class="fa fa-trash"></i>
<span>删除</span>
</div>
</template>
</table-list>
</div>
</main>
<proposal-info ref="proposalInfoRef"></proposal-info>
<invite-seconder @refresh="doSearch" ref="inviteSeconderRef"></invite-seconder>
</div>
</div>
<script nonce="${cspNonce!}">
<!--#include("../../common/info.js"){}#-->
<!--#include("inviteSeconder.js"){}#-->
const vue = new Vue({
el: "#app",
store,
components: {
"invite-seconder": PROPOSAL_INVITE_SECONDER_COMPONENT,
"proposal-info": PROPOSAL_INFO
"invite-seconder": PROPOSAL_INVITE_SECONDER_COMPONENT
},
data() {
return {
@@ -78,34 +387,33 @@ layout("/layouts/platform_h5.html"){
pageSize: 10,
totalCount: 0,
searchKeyword: "",
pageOrderName: "",
pageOrderBy: "",
sessionId: null
},
sessionOptions: [],
infoShow: false
filterOpened: false
}
},
methods: {
listSession() {
this.$axios.post("/platform/proposal/common/listSession").then((res) => {
if (res.code === 0) {
this.sessionOptions = res.data
.map((v) => ({
this.sessionOptions = [{text: "全部届次", value: null}]
.concat(res.data.map((v) => ({
text: v.fullName,
value: v.id
}))
.concat([{text: "全部届次", value: null}])
if (this.sessionOptions.length > 0) {
this.pageForm.sessionId = this.sessionOptions[0].value
this.doSearch()
}
})))
// “全部届次”固定展示在首位,但首次查询仍默认使用接口倒序返回的最新届次。
this.$set(this.pageForm, "sessionId", res.data.length > 0 ? res.data[0].id : null)
this.doSearch()
}
})
},
onView(row) {
this.showApprovalForm = false
this.$refs.proposalInfoRef.onOpen(row)
this.$pjaxReplace("/platform/proposal/mine/info/h5?bizId=" + encodeURIComponent(row.id))
},
onEdit(row) {
@@ -0,0 +1,33 @@
<!--#
layout("/layouts/platform_h5.html"){
#-->
<div id="app" v-cloak>
<van-nav-bar @click-left="historyBack" left-arrow left-text="返回" title="提案详情" placeholder fixed></van-nav-bar>
<proposal-info v-if="bizId" :biz-id="bizId"></proposal-info>
</div>
<script nonce="${cspNonce!}">
<!--#include("../../common/info.js"){}#-->
new Vue({
el: "#app",
store,
components: {
"proposal-info": PROPOSAL_INFO
},
data() {
return {
bizId: ""
}
},
created() {
// bizId 为提案主键,公共详情组件根据该参数返回提案详情对象和流程记录数组。
this.bizId = new URLSearchParams(window.location.search).get("bizId") || ""
if (!this.bizId) {
this.$toast.fail("缺少提案参数")
}
}
})
</script>
<!--#
}
#-->
@@ -44,7 +44,14 @@ const PROPOSAL_INVITE_SECONDER_COMPONENT = {
<van-button @click="invite" type="primary" block :disabled="pageForm.isInvite" class="mt10">邀请</van-button>
</div>
</van-checkbox-group>
<van-empty v-else :figure="2"></van-empty>
<!-- 空状态提示随“已邀请 / 可邀请”标签切换,便于用户判断当前无数据的具体范围。 -->
<div v-else class="proposal-invite-empty">
<img src="/assets/mobile/img/proposal/proposal-empty.png" alt="暂无附议人插画">
<div class="proposal-invite-empty__title">暂无附议人</div>
<div class="proposal-invite-empty__hint">
{{ isInvite === "1" ? "当前暂无已邀请的附议人" : "当前暂无可邀请的附议人" }}
</div>
</div>
</van-action-sheet>
`,
data() {
@@ -57,6 +64,8 @@ const PROPOSAL_INVITE_SECONDER_COMPONENT = {
pageNumber: 1,
pageSize: 5,
totalCount: 0,
pageOrderName: "",
pageOrderBy: "",
isInvite: false,
searchKeyword: null
},
@@ -0,0 +1,656 @@
<!--#
layout("/layouts/platform_h5.html"){
#-->
<style>
body {
background-color: #f3f7fd;
}
.proposal-pre-audit-page {
min-height: 100vh;
padding-bottom: calc(24px + env(safe-area-inset-bottom));
box-sizing: border-box;
background-color: #f3f7fd;
}
.proposal-pre-audit-page .van-nav-bar {
background-color: #fff;
}
.proposal-pre-audit-page .van-nav-bar__title {
color: #172033;
font-size: 16px;
font-weight: 600;
}
.proposal-pre-audit-page .van-nav-bar .van-icon,
.proposal-pre-audit-page .van-nav-bar__text {
color: #1989fa;
}
.proposal-pre-audit-content {
padding: 0 12px;
}
.proposal-pre-audit-sticky {
padding: 22px 12px 1px;
box-sizing: border-box;
background-color: #f3f7fd;
}
.proposal-pre-audit-search {
margin: 0 0 8px;
padding: 0;
overflow: hidden;
border-radius: 12px;
background-color: #fff;
box-shadow: 0 5px 16px rgba(43, 73, 112, .1);
}
.proposal-pre-audit-search .van-search__content {
height: 42px;
padding-left: 12px;
align-items: center;
border-radius: 12px;
background-color: #fff;
}
.proposal-pre-audit-search .van-field__control {
color: #263548;
font-size: 14px;
}
.proposal-pre-audit-search .van-field__control::placeholder {
color: #a7b0bf;
}
.proposal-pre-audit-filter {
margin: 0 0 10px;
overflow: hidden;
border-radius: 10px;
box-shadow: 0 5px 16px rgba(43, 73, 112, .08);
transition: border-radius .18s ease;
}
.proposal-pre-audit-filter.is-open {
border-radius: 10px 10px 0 0;
}
.proposal-pre-audit-filter .van-dropdown-menu__bar,
.proposal-pre-audit-filter .van-dropdown-menu__item,
.proposal-pre-audit-filter .van-dropdown-menu__title {
height: 44px;
}
.proposal-pre-audit-filter .van-dropdown-menu__bar {
box-shadow: none;
}
.proposal-pre-audit-filter .van-dropdown-menu__title {
display: inline-flex;
align-items: center;
color: #66758a;
font-size: 14px;
line-height: 20px;
}
.proposal-pre-audit-filter .van-dropdown-menu__title--active,
.proposal-pre-audit-filter .van-dropdown-item__option--active,
.proposal-pre-audit-filter .van-dropdown-item__option--active .van-dropdown-item__icon {
color: #1989fa;
}
.proposal-pre-audit-filter .van-dropdown-item__content {
width: calc(100% - 24px);
margin: 0 12px;
overflow: hidden;
box-sizing: border-box;
border-radius: 0 0 12px 12px;
box-shadow: 0 10px 24px rgba(31, 65, 108, .14);
}
.proposal-pre-audit-filter .van-dropdown-item__option {
display: flex;
min-height: 48px;
padding: 0 16px;
align-items: center;
color: #46566c;
font-size: 14px;
}
.proposal-pre-audit-list .empty-state {
display: none;
}
.proposal-pre-audit-empty {
display: flex;
min-height: 280px;
padding: 46px 18px 36px;
align-items: center;
justify-content: center;
flex-direction: column;
box-sizing: border-box;
text-align: center;
}
.proposal-pre-audit-empty img {
display: block;
width: 86%;
max-width: 290px;
height: auto;
object-fit: contain;
}
.proposal-pre-audit-empty__title {
margin-top: 14px;
color: #50627a;
font-size: 15px;
line-height: 22px;
font-weight: 600;
}
.proposal-pre-audit-empty__hint {
margin-top: 5px;
color: #9aa7b8;
font-size: 12px;
line-height: 18px;
}
.proposal-pre-audit-card-header {
display: flex;
margin-bottom: 7px;
align-items: flex-start;
justify-content: space-between;
}
.proposal-pre-audit-card-heading {
min-width: 0;
padding-right: 10px;
flex: 1;
}
.proposal-pre-audit-card-title {
overflow: hidden;
color: #253247;
font-size: 15px;
line-height: 22px;
font-weight: 600;
text-overflow: ellipsis;
white-space: nowrap;
}
.proposal-pre-audit-card-code {
margin-top: 1px;
color: #9ba6b6;
font-size: 12px;
line-height: 18px;
}
.proposal-pre-audit-status {
display: inline-flex;
flex: none;
}
.proposal-pre-audit-status .van-tag {
display: inline-flex;
min-height: 24px;
padding: 3px 9px;
align-items: center;
box-sizing: border-box;
border: 0;
border-radius: 12px;
font-size: 12px;
line-height: 18px;
white-space: nowrap;
}
@media (max-width: 350px) {
.proposal-pre-audit-content,
.proposal-pre-audit-sticky {
padding-right: 10px;
padding-left: 10px;
}
}
</style>
<div id="app" v-cloak>
<div class="proposal-pre-audit-page">
<van-nav-bar @click-left="historyBack" left-arrow left-text="返回" title="校工会预审核" placeholder fixed></van-nav-bar>
<van-sticky :offset-top="46">
<div class="proposal-pre-audit-sticky">
<van-search
v-model="pageForm.name"
class="proposal-pre-audit-search"
placeholder="请输入提案名称搜索"
shape="round"
clearable
@search="doSearch"
@clear="doSearch"
></van-search>
<van-dropdown-menu
class="proposal-pre-audit-filter"
:class="{'is-open': filterOpened}"
active-color="#1989fa"
>
<van-dropdown-item
v-model="pageForm.sessionId"
:options="sessionOptions"
:multiple="false"
@change="doSearch"
@open="onFilterOpen"
@close="onFilterClose"
></van-dropdown-item>
<van-dropdown-item
v-model="pageForm.approvalText"
:options="approvalOptions"
:multiple="false"
@change="approvalChange"
@open="onFilterOpen"
@close="onFilterClose"
></van-dropdown-item>
</van-dropdown-menu>
</div>
</van-sticky>
<main class="proposal-pre-audit-content">
<div class="proposal-pre-audit-list">
<!-- 列表无记录时使用与“我的提案”一致的提案专用空状态。 -->
<div v-if="pageForm.totalCount === 0" class="proposal-pre-audit-empty">
<img src="/assets/mobile/img/proposal/proposal-empty.png" alt="暂无提案记录插画">
<div class="proposal-pre-audit-empty__title">暂无提案记录</div>
<div class="proposal-pre-audit-empty__hint">当前筛选条件下暂时没有提案</div>
</div>
<table-list api="/platform/proposal/pre/audit/pageData" :page_form.sync="pageForm" @ready="onReady"
ref="tableListRef" title="name">
<template #header="{index,row}">
<div class="proposal-pre-audit-card-header">
<div class="proposal-pre-audit-card-heading">
<div class="proposal-pre-audit-card-title">{{row.name}}</div>
<div class="proposal-pre-audit-card-code">编号:{{row.code || '--'}}</div>
</div>
<enum-tag
class="proposal-pre-audit-status"
v-if="row.instanceState"
:value="row.instanceState"
name="ProcessInstanceStateEnum"
label_key="message"
size="small"
></enum-tag>
</div>
</template>
<template v-slot="{index,row}">
<table-column label="立案编号">{{row.caseFilingCode || '--'}}</table-column>
<table-column label="提案类别">{{row.typeName || '--'}}</table-column>
<table-column label="提案人">{{row.createUserName || '--'}}</table-column>
<table-column label="教代会">{{row.sessionName || '--'}}</table-column>
<table-column label="代表团">{{row.delegationName || '--'}}</table-column>
<table-column label="当前节点">{{row.curTaskName || '--'}}</table-column>
</template>
<template #actions="{index,row}">
<div class="action-btn" @click="onView(row,index)">
<i class="fa fa-eye"></i>
<span>查看</span>
</div>
<div class="action-btn" v-if="row.taskState === 10" @click="onApproval(row,index)">
<i class="fa fa-edit"></i>
<span>审核</span>
</div>
<div class="action-btn delete" v-if="row.canRevoke" @click="onRevoke(row)">
<i class="fa fa-reply"></i>
<span>撤回</span>
</div>
</template>
</table-list>
</div>
</main>
</div>
</div>
<script nonce="${cspNonce!}">
new Vue({
el: "#app",
store,
data() {
return {
pageForm: {
pageNumber: 1,
pageSize: 10,
totalCount: 0,
pageOrderName: "",
pageOrderBy: "",
name: null,
sessionId: null,
approvalText: "0",
approval: false
},
sessionOptions: [],
filterOpened: false,
approvalOptions: [
{text: "未审核", value: "0"},
{text: "已审核", value: "1"}
],
pendingListCache: null
}
},
methods: {
onReady() {
if (this.pendingListCache) {
this.restoreListCache()
this.listSession(true)
return
}
this.listSession(false)
},
onFilterOpen() {
this.filterOpened = true
},
onFilterClose() {
this.filterOpened = false
},
// 审核状态下拉使用字符串展示,查询接口使用布尔值区分已审核和未审核。
approvalChange(value) {
this.$set(this.pageForm, "approval", value === "1")
this.doSearch()
},
// 与其他提案 H5 页面一致加载全部教代会;首次进入默认选中最新一届,缓存恢复时不覆盖原筛选条件。
listSession(skipDefaultSearch) {
this.$axios.post("/platform/proposal/common/listSession").then((res) => {
if (res.code === 0) {
this.sessionOptions = [{text: "全部教代会", value: null}]
.concat(res.data.map((item) => ({text: item.fullName, value: item.id})))
if (skipDefaultSearch) return
this.$set(this.pageForm, "sessionId", res.data.length > 0 ? res.data[0].id : null)
this.doSearch()
}
})
},
onView(row, index) {
this.saveListCache(row, index)
this.$pjaxReplace("/platform/proposal/pre/audit/info/h5?bizId=" + encodeURIComponent(row.id) + "&action=view")
},
onApproval(row, index) {
this.saveListCache(row, index)
const params = new URLSearchParams()
params.set("bizId", row.id)
params.set("action", "approval")
params.set("taskId", row.taskId)
params.set("taskKey", row.taskKey || "preAudit")
params.set("taskName", row.curTaskName || row.taskName || "校工会预审核")
this.$pjaxReplace("/platform/proposal/pre/audit/info/h5?" + params.toString())
},
getListCacheKey() {
return "proposal:pre-audit:h5:list-state"
},
getListCacheTokenParam() {
return "preAuditListCacheToken"
},
getCurrentListCacheToken() {
return new URLSearchParams(window.location.search).get(this.getListCacheTokenParam()) || ""
},
/**
* 将预审核列表缓存标识绑定到当前历史记录。
* PJAX 返回时仅恢复当前历史记录对应的筛选、分页数据和滚动快照。
*/
bindListCacheToken(token) {
const currentUrl = new URL(window.location.href)
currentUrl.searchParams.set(this.getListCacheTokenParam(), token)
const currentState = window.history.state ? {...window.history.state} : null
if (currentState && currentState.url) {
currentState.url = currentUrl.toString()
}
window.history.replaceState(currentState, document.title, currentUrl.toString())
if ($.pjax && $.pjax.state) {
$.pjax.state.url = currentUrl.toString()
}
},
// 用户主动搜索或切换筛选时使旧返回缓存失效。
clearListCache() {
const token = this.getCurrentListCacheToken()
if (!token) return
const currentUrl = new URL(window.location.href)
currentUrl.searchParams.delete(this.getListCacheTokenParam())
const currentState = window.history.state ? {...window.history.state} : null
if (currentState && currentState.url) {
currentState.url = currentUrl.toString()
}
window.history.replaceState(currentState, document.title, currentUrl.toString())
if ($.pjax && $.pjax.state) {
$.pjax.state.url = currentUrl.toString()
}
sessionStorage.removeItem(this.getListCacheKey())
},
// platform_h5 的实际滚动区域是 page-wrapper,列表位置必须基于该容器保存和恢复。
getListScrollContainer() {
return document.querySelector(".page-wrapper") || document.scrollingElement || document.documentElement
},
/**
* 进入查看或审核详情前保存列表现场。
* row 为当前提案,index 为已加载列表索引;缓存同时保存相邻记录作为审核后的定位候选。
*/
saveListCache(row, index) {
const tableList = this.$refs.tableListRef
if (!tableList) return
const cacheToken = Date.now() + "-" + Math.random().toString(36).slice(2)
const tableState = typeof tableList.getListState === "function"
? tableList.getListState()
: {
tableData: JSON.parse(JSON.stringify(tableList.tableData || [])),
localPageForm: JSON.parse(JSON.stringify(tableList.localPageForm || this.pageForm)),
tableFinished: !!tableList.tableFinished
}
const cards = document.querySelectorAll(".proposal-pre-audit-list .table-list-item")
const currentCard = cards[index]
const scrollContainer = this.getListScrollContainer()
const cache = {
time: Date.now(),
token: cacheToken,
pageForm: JSON.parse(JSON.stringify(this.pageForm)),
tableState: tableState,
completedBizId: "",
anchor: {
rowId: row.id,
previousRowId: index > 0 && tableState.tableData[index - 1] ? tableState.tableData[index - 1].id : "",
nextRowId: tableState.tableData[index + 1] ? tableState.tableData[index + 1].id : "",
viewportTop: currentCard ? currentCard.getBoundingClientRect().top : 0,
scrollTop: scrollContainer ? scrollContainer.scrollTop : 0
}
}
try {
sessionStorage.setItem(this.getListCacheKey(), JSON.stringify(cache))
this.bindListCacheToken(cacheToken)
} catch (error) {
sessionStorage.removeItem(this.getListCacheKey())
}
},
// 仅恢复当前历史记录对应且十分钟内有效的校工会预审核列表缓存。
prepareListCache() {
try {
const cacheToken = this.getCurrentListCacheToken()
if (!cacheToken) return
const cacheText = sessionStorage.getItem(this.getListCacheKey())
if (!cacheText) return
const cache = JSON.parse(cacheText)
if (!cache || cache.token !== cacheToken || !cache.tableState) return
if (Date.now() - cache.time > 10 * 60 * 1000) {
this.clearListCache()
return
}
Object.keys(cache.pageForm || {}).forEach((key) => {
this.$set(this.pageForm, key, cache.pageForm[key])
})
this.$set(this, "pendingListCache", cache)
} catch (error) {
sessionStorage.removeItem(this.getListCacheKey())
}
},
// 普通查看返回恢复原快照;审核完成返回重新查询,避免未审核列表保留已处理记录。
restoreListCache() {
const cache = this.pendingListCache
const tableList = this.$refs.tableListRef
if (!cache || !tableList) return
const anchor = cache.anchor || {}
if (cache.completedBizId) {
this.refreshCompletedListCache(cache)
return
}
this.applyListCache(cache.tableState, anchor.rowId, anchor)
},
/**
* 按离开前已加载页数重新查询审核后的列表。
* 查询结果转换为 TableList 状态,并优先定位原记录的下一条、其次定位上一条。
*/
refreshCompletedListCache(cache) {
const originalPageSize = Number(cache.pageForm.pageSize || 10)
const loadedCount = Math.max(cache.tableState.tableData.length, originalPageSize)
const loadedPageCount = Math.max(1, Math.ceil(loadedCount / originalPageSize))
const requestForm = {
...cache.pageForm,
pageNumber: 1,
pageSize: loadedPageCount * originalPageSize
}
const loading = createListLoading()
this.$axios.post("/platform/proposal/pre/audit/pageData", requestForm, {h5SkeletonLoading: true})
.then((res) => {
if (res.code !== 0) {
this.applyCompletedCacheFallback(cache)
return
}
const freshState = {
tableData: res.data.list || [],
localPageForm: {
...cache.pageForm,
pageNumber: loadedPageCount,
pageSize: originalPageSize,
totalCount: res.data.totalCount
},
tableFinished: (res.data.list || []).length >= res.data.totalCount
}
const anchor = cache.anchor || {}
this.applyListCache(freshState, anchor.nextRowId || anchor.previousRowId, anchor)
})
.catch(() => {
this.applyCompletedCacheFallback(cache)
})
.finally(() => {
loading.close()
})
},
// 审核后刷新失败时从旧快照移除已处理记录,避免未审核列表保留脏数据。
applyCompletedCacheFallback(cache) {
const tableState = cache.tableState
const completedIndex = tableState.tableData.findIndex((item) => String(item.id) === String(cache.completedBizId))
if (completedIndex >= 0 && cache.pageForm.approval === false) {
tableState.tableData.splice(completedIndex, 1)
if (tableState.localPageForm && tableState.localPageForm.totalCount > 0) {
tableState.localPageForm.totalCount--
}
} else if (completedIndex >= 0) {
this.$set(tableState.tableData[completedIndex], "taskState", 20)
}
const anchor = cache.anchor || {}
this.applyListCache(tableState, anchor.nextRowId || anchor.previousRowId, anchor)
},
/**
* 恢复 TableList 数据并将目标卡片放回离开页面前的视口位置。
* sticky 吸顶会改变一次布局,因此首次滚动后再执行一次位置校正。
*/
applyListCache(tableState, targetRowId, anchor) {
const tableList = this.$refs.tableListRef
if (!tableList) return
if (typeof tableList.restoreListState === "function") {
if (!tableList.restoreListState(tableState)) return
} else {
this.$set(tableList, "tableData", tableState.tableData || [])
this.$set(tableList, "localPageForm", {...tableList.localPageForm, ...(tableState.localPageForm || {})})
this.$set(tableList, "tableFinished", !!tableState.tableFinished)
this.$set(tableList, "tableLoading", false)
this.$set(tableList, "tableRefreshing", false)
tableList.$emit("update:page_form", {...tableList.localPageForm})
}
this.$set(this, "pendingListCache", null)
this.$nextTick(() => {
setTimeout(() => {
const cards = Array.from(document.querySelectorAll(".proposal-pre-audit-list .table-list-item"))
const scrollContainer = this.getListScrollContainer()
if (!scrollContainer) return
const candidateIds = [targetRowId, anchor.nextRowId, anchor.previousRowId]
.filter((id) => id)
.map((id) => String(id))
.filter((id, index, values) => values.indexOf(id) === index)
let targetIndex = -1
candidateIds.some((candidateId) => {
targetIndex = (tableState.tableData || []).findIndex((item) => String(item.id) === candidateId)
return targetIndex >= 0
})
const targetCard = targetIndex >= 0 ? cards[targetIndex] : null
if (targetCard) {
const restoreTargetCard = () => {
if (!targetCard.isConnected) return
const targetTop = scrollContainer.scrollTop + targetCard.getBoundingClientRect().top - Number(anchor.viewportTop || 0)
scrollContainer.scrollTo(0, Math.max(0, targetTop))
}
restoreTargetCard()
setTimeout(restoreTargetCard, 100)
return
}
scrollContainer.scrollTo(0, Number(anchor.scrollTop || 0))
}, 0)
})
},
onRevoke(row) {
this.$dialog.confirm({
title: "提示",
message: "您确定要撤回吗?"
}).then(() => {
this.$axios.post("/flow/common/revokeTask", {taskId: row.taskId}).then((res) => {
if (res.code === 0) {
this.$toast.success(res.msg)
this.doSearch()
}
})
})
},
doSearch() {
this.clearListCache()
this.$nextTick(() => {
this.pageForm.pageNumber = 1
this.pageForm.totalCount = 0
this.$refs.tableListRef.doSearch()
})
}
},
created() {
this.prepareListCache()
if (this.pendingListCache) return
const tab = new URLSearchParams(window.location.search).get("tab")
if (tab === "done") {
this.$set(this.pageForm, "approval", true)
this.$set(this.pageForm, "approvalText", "1")
} else if (tab === "todo") {
this.$set(this.pageForm, "approval", false)
this.$set(this.pageForm, "approvalText", "0")
}
}
})
</script>
<!--#
}
#-->
@@ -0,0 +1,140 @@
<!--#
layout("/layouts/platform_h5.html"){
#-->
<style>
.proposal-pre-audit-actions {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 10px;
padding: 10px 12px calc(12px + env(safe-area-inset-bottom));
}
.proposal-pre-audit-actions .van-button {
width: 100%;
margin: 0;
}
</style>
<div id="app" v-cloak>
<van-nav-bar @click-left="historyBack" left-arrow left-text="返回" :title="pageTitle" placeholder fixed></van-nav-bar>
<proposal-info v-if="bizId" :biz-id="bizId">
<div v-if="isApproval">
<div class="process-title">{{formData.taskDisplayName}}</div>
<van-form ref="formRef">
<van-field
v-model="formData.tf_opinion"
name="tf_opinion"
label="审批意见"
placeholder="请输入审批意见"
:rules="[{ required: true, message: '请填写审批意见' }]"
required
maxlength="100"
show-word-limit
></van-field>
</van-form>
<div class="proposal-pre-audit-actions">
<van-button type="warning" :loading="formLoading" @click="handleTaskAction(4)">退回(不审核)</van-button>
<van-button type="warning" :loading="formLoading" @click="handleTaskAction(6)">退回(审核)</van-button>
<van-button type="danger" :loading="formLoading" @click="handleTaskAction(2)">不同意</van-button>
<van-button type="primary" :loading="formLoading" @click="handleTaskAction(1)">同意</van-button>
</div>
</div>
</proposal-info>
</div>
<script nonce="${cspNonce!}">
<!--#include("../../common/info.js"){}#-->
new Vue({
el: "#app",
store,
components: {
"proposal-info": PROPOSAL_INFO
},
data() {
return {
bizId: "",
action: "view",
formData: {},
formLoading: false
}
},
computed: {
isApproval() {
return this.action === "approval"
},
pageTitle() {
return this.isApproval ? "校工会预审核" : "提案详情"
}
},
methods: {
/**
* 提交校工会预审核结果。
* val 为 WF 提交类型:1 同意、2 不同意、4 退回且不重复审核、6 退回并重新审核;接口返回 Result。
*/
handleTaskAction(val) {
this.$refs.formRef.validate()
.then(() => this.$dialog.confirm({title: "提示", message: "您确定要提交吗?"}))
.then(() => {
const params = {
...this.formData,
submitType: val,
taskName: "startTask"
}
if (val === 4) {
params.tf_sourceTaskKey = this.formData.taskKey
params.tf_audit = false
}
this.formLoading = true
return this.$axios.post("/flow/common/executeTask", {
data: JSON.stringify(params)
})
})
.then((res) => {
if (res && res.code === 0) {
this.markListCacheCompleted()
this.$toast.success(res.msg)
this.historyBack()
}
})
.catch(() => {})
.finally(() => {
this.formLoading = false
})
},
/**
* 审核成功后记录已处理提案主键。
* 返回列表时按原筛选和已加载范围刷新数据,并定位到相邻记录。
*/
markListCacheCompleted() {
const cacheKey = "proposal:pre-audit:h5:list-state"
try {
const cacheText = sessionStorage.getItem(cacheKey)
if (!cacheText) return
const cache = JSON.parse(cacheText)
cache.completedBizId = this.bizId
cache.time = Date.now()
sessionStorage.setItem(cacheKey, JSON.stringify(cache))
} catch (error) {
sessionStorage.removeItem(cacheKey)
}
}
},
created() {
const params = new URLSearchParams(window.location.search)
// bizId 查询提案详情;taskId、taskKey 和 taskName 构造 WF 预审核提交参数。
this.bizId = params.get("bizId") || ""
this.action = params.get("action") || "view"
if (this.isApproval) {
this.$set(this.formData, "processTaskId", params.get("taskId") || "")
this.$set(this.formData, "taskKey", params.get("taskKey") || "preAudit")
this.$set(this.formData, "taskDisplayName", params.get("taskName") || "校工会预审核")
}
if (!this.bizId) {
this.$toast.fail("缺少提案参数")
}
}
})
</script>
<!--#
}
#-->
@@ -49,32 +49,8 @@ layout("/layouts/platform_h5.html"){
</template>
</table-list>
<proposal-info ref="proposalInfoRef">
<div v-if="showApprovalForm">
<div class="process-title">{{formData.taskName}}</div>
<van-form ref="formRef">
<van-field
v-model="formData.tf_opinion"
name="tf_opinion"
label="审批意见"
placeholder="请输入审批意见"
:rules="[{ required: true, message: '请填写审批意见' }]"
required
maxlength="100"
show-word-limit
></van-field>
</van-form>
<div style="display: flex; justify-content: space-between; column-gap: 10px; padding: 10px">
<van-button type="danger" block @click="handleTaskAction('back')">退回答复</van-button>
<van-button type="primary" block @click="handleTaskAction('agree')">同意答复</van-button>
</div>
</div>
</proposal-info>
</div>
<script nonce="${cspNonce!}">
<!--#include("../../common/info.js"){}#-->
const vue = new Vue({
el: "#app",
store,
@@ -90,14 +66,9 @@ layout("/layouts/platform_h5.html"){
approval: false
},
sessionOptions: [],
formData: {},
showApprovalForm: false
sessionOptions: []
}
},
components: {
"proposal-info": PROPOSAL_INFO
},
methods: {
onReady() {
this.listSession()
@@ -120,52 +91,17 @@ layout("/layouts/platform_h5.html"){
},
onView(row) {
this.showApprovalForm = false
this.$refs.proposalInfoRef.onOpen(row)
this.$pjaxReplace("/platform/proposal/schoolLeaderApproval/info/h5?bizId=" + encodeURIComponent(row.id) + "&action=view")
},
onApproval(row) {
this.showApprovalForm = true
this.$refs.proposalInfoRef.onOpen(row)
this.formData = {
processTaskId: row.taskId,
taskKey: row.taskKey,
taskName: row.taskName,
proposalId: row.id
}
},
async handleTaskAction(val) {
try {
await this.$refs.formRef.validate();
this.$dialog.confirm({
title: "提示",
message: "您确定要提交吗?"
}).then(() => {
const formData = {
...this.formData,
tf_approval: val
}
delete formData.taskKey
delete formData.taskName
this.$axios.post("/platform/proposal/schoolLeaderApproval/executeTask", {
data: JSON.stringify(formData)
}).then((res) => {
if (res.code === 0) {
this.$refs.proposalInfoRef.onClose()
this.$toast.success(res.msg)
this.doSearch()
}
})
}).catch(() => {
})
} catch (error) {
}
const params = new URLSearchParams()
params.set("bizId", row.id)
params.set("action", "approval")
params.set("taskId", row.taskId)
params.set("taskKey", row.taskKey || "")
params.set("taskName", row.taskName || "分管校领导审批")
this.$pjaxReplace("/platform/proposal/schoolLeaderApproval/info/h5?" + params.toString())
},
onRevoke(row){
@@ -0,0 +1,104 @@
<!--#
layout("/layouts/platform_h5.html"){
#-->
<div id="app" v-cloak>
<van-nav-bar @click-left="historyBack" left-arrow left-text="返回" :title="pageTitle" placeholder fixed></van-nav-bar>
<proposal-info v-if="bizId" :biz-id="bizId">
<div v-if="isApproval">
<div class="process-title">{{formData.taskName}}</div>
<van-form ref="formRef">
<van-field
v-model="formData.tf_opinion"
name="tf_opinion"
label="审批意见"
placeholder="请输入审批意见"
:rules="[{ required: true, message: '请填写审批意见' }]"
required
maxlength="100"
show-word-limit
></van-field>
</van-form>
<div style="display: flex; justify-content: space-between; column-gap: 10px; padding: 10px">
<van-button type="danger" block :loading="formLoading" @click="handleTaskAction('back')">退回答复</van-button>
<van-button type="primary" block :loading="formLoading" @click="handleTaskAction('agree')">同意答复</van-button>
</div>
</div>
</proposal-info>
</div>
<script nonce="${cspNonce!}">
<!--#include("../../common/info.js"){}#-->
new Vue({
el: "#app",
store,
components: {
"proposal-info": PROPOSAL_INFO
},
data() {
return {
bizId: "",
action: "view",
formData: {},
formLoading: false
}
},
computed: {
isApproval() {
return this.action === "approval"
},
pageTitle() {
return this.isApproval ? "分管校领导审批" : "提案详情"
}
},
methods: {
/**
* 提交分管校领导审批结果。
* val 为 back 或 agree;接口返回 Resultcode 为 0 表示处理成功。
*/
handleTaskAction(val) {
this.$refs.formRef.validate()
.then(() => this.$dialog.confirm({title: "提示", message: "您确定要提交吗?"}))
.then(() => {
const submitData = {
...this.formData,
tf_approval: val
}
delete submitData.taskKey
delete submitData.taskName
this.formLoading = true
return this.$axios.post("/platform/proposal/schoolLeaderApproval/executeTask", {
data: JSON.stringify(submitData)
})
})
.then((res) => {
if (res && res.code === 0) {
this.$toast.success(res.msg)
this.historyBack()
}
})
.catch(() => {})
.finally(() => {
this.formLoading = false
})
}
},
created() {
const params = new URLSearchParams(window.location.search)
// bizId 用于详情查询;taskId、taskKey、taskName 和 proposalId 构成校领导审批接口参数。
this.bizId = params.get("bizId") || ""
this.action = params.get("action") || "view"
if (this.isApproval) {
this.$set(this.formData, "processTaskId", params.get("taskId") || "")
this.$set(this.formData, "taskKey", params.get("taskKey") || "")
this.$set(this.formData, "taskName", params.get("taskName") || "分管校领导审批")
this.$set(this.formData, "proposalId", this.bizId)
}
if (!this.bizId) {
this.$toast.fail("缺少提案参数")
}
}
})
</script>
<!--#
}
#-->
@@ -1,90 +1,336 @@
<!--#
layout("/layouts/platform_h5.html"){
#-->
<style>
body {
background-color: #f3f7fd;
}
.proposal-seconded-page {
min-height: 100vh;
padding-bottom: calc(24px + env(safe-area-inset-bottom));
box-sizing: border-box;
background-color: #f3f7fd;
}
.proposal-seconded-page .van-nav-bar {
background-color: #fff;
}
.proposal-seconded-page .van-nav-bar__title {
color: #172033;
font-size: 16px;
font-weight: 600;
}
.proposal-seconded-page .van-nav-bar .van-icon,
.proposal-seconded-page .van-nav-bar__text {
color: #1989fa;
}
.proposal-seconded-content {
padding: 0 12px;
}
.proposal-seconded-sticky {
padding: 22px 12px 1px;
box-sizing: border-box;
background-color: #f3f7fd;
}
.proposal-seconded-search {
margin: 0 0 8px;
padding: 0;
overflow: hidden;
border-radius: 12px;
background-color: #fff;
box-shadow: 0 5px 16px rgba(43, 73, 112, .1);
}
.proposal-seconded-search .van-search__content {
height: 42px;
padding-left: 12px;
align-items: center;
border-radius: 12px;
background-color: #fff;
}
.proposal-seconded-search .van-field__control {
color: #263548;
font-size: 14px;
}
.proposal-seconded-search .van-field__control::placeholder {
color: #a7b0bf;
}
.proposal-seconded-filter {
margin: 0 0 10px;
overflow: hidden;
border-radius: 10px;
box-shadow: 0 5px 16px rgba(43, 73, 112, .08);
transition: border-radius .18s ease;
}
.proposal-seconded-filter.is-open {
border-radius: 10px 10px 0 0;
}
.proposal-seconded-filter .van-dropdown-menu__bar {
height: 44px;
box-shadow: none;
}
.proposal-seconded-filter .van-dropdown-menu__item {
height: 44px;
align-items: center;
justify-content: center;
}
.proposal-seconded-filter .van-dropdown-menu__title {
display: inline-flex;
height: 44px;
align-items: center;
line-height: 20px;
color: #66758a;
font-size: 14px;
}
.proposal-seconded-filter .van-dropdown-menu__title--active,
.proposal-seconded-filter .van-dropdown-item__option--active,
.proposal-seconded-filter .van-dropdown-item__option--active .van-dropdown-item__icon {
color: #1989fa;
}
.proposal-seconded-filter .van-dropdown-menu__title--down::after {
border-color: transparent transparent #1989fa #1989fa;
}
.proposal-seconded-filter .van-dropdown-item__content {
width: calc(100% - 24px);
margin: 0 12px;
overflow: hidden;
box-sizing: border-box;
border-radius: 0 0 12px 12px;
box-shadow: 0 10px 24px rgba(31, 65, 108, .14);
}
.proposal-seconded-filter .van-dropdown-item__option {
display: flex;
min-height: 48px;
padding: 0 16px;
align-items: center;
color: #46566c;
font-size: 14px;
}
.proposal-seconded-filter .van-dropdown-item__option .van-cell__title {
display: flex;
min-height: 20px;
align-items: center;
line-height: 20px;
}
.proposal-seconded-filter .van-cell::after {
right: 16px;
left: 16px;
}
.proposal-seconded-list .empty-state {
display: none;
}
.proposal-seconded-empty {
display: flex;
min-height: 280px;
padding: 46px 18px 36px;
align-items: center;
justify-content: center;
flex-direction: column;
box-sizing: border-box;
text-align: center;
}
.proposal-seconded-empty img {
display: block;
width: 86%;
max-width: 290px;
height: auto;
object-fit: contain;
}
.proposal-seconded-empty__title {
margin-top: 14px;
color: #50627a;
font-size: 15px;
line-height: 22px;
font-weight: 600;
}
.proposal-seconded-empty__hint {
margin-top: 5px;
color: #9aa7b8;
font-size: 12px;
line-height: 18px;
}
.proposal-seconded-card-header {
display: flex;
margin-bottom: 7px;
align-items: flex-start;
justify-content: space-between;
}
.proposal-seconded-card-heading {
min-width: 0;
padding-right: 10px;
flex: 1;
}
.proposal-seconded-card-title {
overflow: hidden;
color: #253247;
font-size: 15px;
line-height: 22px;
font-weight: 600;
text-overflow: ellipsis;
white-space: nowrap;
}
.proposal-seconded-card-code {
margin-top: 1px;
color: #9ba6b6;
font-size: 12px;
line-height: 18px;
}
.proposal-seconded-status {
display: inline-flex;
flex: none;
}
.proposal-seconded-status .van-tag {
display: inline-flex;
min-height: 24px;
padding: 3px 9px;
align-items: center;
box-sizing: border-box;
border: 0;
border-radius: 12px;
font-size: 12px;
line-height: 18px;
white-space: nowrap;
}
@media (max-width: 350px) {
.proposal-seconded-content {
padding-right: 10px;
padding-left: 10px;
}
.proposal-seconded-sticky {
padding-right: 10px;
padding-left: 10px;
}
}
</style>
<div id="app" v-cloak>
<van-nav-bar @click-left="historyBack" left-arrow left-text="返回" title="附议提案" placeholder fixed></van-nav-bar>
<van-sticky offset-top="46px">
<van-search
v-model="pageForm.name"
:show-action="false"
:reverse-color="false"
input-align="left"
placeholder="请输入提案名称搜索"
@search="doSearch"
></van-search>
<van-dropdown-menu :close-on-click-outside="false" :close-on-click-overlay="false">
<van-dropdown-item v-model="pageForm.sessionId" :options="sessionOptions" :multiple="false"
@change="doSearch"></van-dropdown-item>
</van-dropdown-menu>
<van-tabs v-model="pageForm.approvalText"
@change="(val)=>{this.pageForm.approval = val==='1';this.doSearch();}">
<van-tab title="已附议" name="1"></van-tab>
<van-tab title="未附议" name="0"></van-tab>
</van-tabs>
</van-sticky>
<div class="proposal-seconded-page">
<van-nav-bar @click-left="historyBack" left-arrow left-text="返回" title="附议提案" placeholder fixed></van-nav-bar>
<table-list api="/platform/proposal/seconded/pageData" :page_form.sync="pageForm" @ready="onReady"
ref="tableListRef"
title="name">
<template v-slot="{index,row}">
<table-column label="提案编号">{{row.code}}</table-column>
<table-column label="提案类别">{{row.typeName}}</table-column>
<table-column label="提案人">{{row.createUserName}}</table-column>
<table-column label="代表团">{{row.delegationName}}</table-column>
<table-column label="当前节点">{{row.curTaskName}}</table-column>
</template>
<template #actions="{index,row}">
<div class="action-btn" @click="onView(row)">
<i class="fa fa-eye"></i>
<span>查看</span>
<van-sticky :offset-top="46">
<div class="proposal-seconded-sticky">
<van-search
v-model="pageForm.name"
class="proposal-seconded-search"
placeholder="请输入提案名称搜索"
shape="round"
clearable
@search="doSearch"
@clear="doSearch"
></van-search>
<van-dropdown-menu
class="proposal-seconded-filter"
:class="{'is-open': filterOpened}"
active-color="#1989fa"
>
<van-dropdown-item
v-model="pageForm.sessionId"
:options="sessionOptions"
:multiple="false"
@change="doSearch"
@open="filterOpened = true"
@close="filterOpened = false"
></van-dropdown-item>
<van-dropdown-item
v-model="pageForm.approvalText"
:options="approvalOptions"
:multiple="false"
@change="approvalChange"
@open="filterOpened = true"
@close="filterOpened = false"
></van-dropdown-item>
</van-dropdown-menu>
</div>
<div class="action-btn" v-if="showSecondAction(row)" @click="onApproval(row)">
<i class="fa fa-edit"></i>
<span>附议</span>
</div>
<!-- <div class="action-btn delete" v-if="row.canRevoke" @click="onRevoke(row)">-->
<!-- <i class="fa fa-reply"></i>-->
<!-- <span>撤回</span>-->
<!-- </div>-->
</template>
</table-list>
</van-sticky>
<proposal-info ref="proposalInfoRef">
<div v-if="showApprovalForm">
<div class="process-title">提案附议</div>
<!-- <div class="process-title">{{formData.taskName}}</div>-->
<van-form ref="formRef">
<van-field
v-model="formData.tf_opinion"
name="tf_opinion"
label="审批意见"
placeholder="请输入审批意见"
:rules="[{ required: true, message: '请填写审批意见' }]"
required
maxlength="100"
show-word-limit
></van-field>
<!-- <van-field label="签字" name="approvalSignature" v-model="formData.signature" required class="direction-column-field">-->
<!-- <h5-signature v-model="formData.approvalSignature" slot="input"></h5-signature>-->
<!-- </van-field>-->
</van-form>
<div v-if="row && row.curTaskCode === 'second'"
style="display: flex; justify-content: space-between; column-gap: 10px; padding: 10px">
<van-button type="danger" block @click="handleTaskAction(20)">不同意</van-button>
<van-button type="primary" block @click="handleTaskAction(1)">同意</van-button>
<main class="proposal-seconded-content">
<div class="proposal-seconded-list">
<!-- 列表无记录时使用与“我的提案”一致的提案专用空状态。 -->
<div v-if="pageForm.totalCount === 0" class="proposal-seconded-empty">
<img src="/assets/mobile/img/proposal/proposal-empty.png" alt="暂无提案记录插画">
<div class="proposal-seconded-empty__title">暂无提案记录</div>
<div class="proposal-seconded-empty__hint">当前筛选条件下暂时没有提案</div>
</div>
<table-list api="/platform/proposal/seconded/pageData" :page_form.sync="pageForm" @ready="onReady"
ref="tableListRef"
title="name">
<template #header="{index,row}">
<div class="proposal-seconded-card-header">
<div class="proposal-seconded-card-heading">
<div class="proposal-seconded-card-title">{{row.name}}</div>
<div class="proposal-seconded-card-code">编号:{{row.code || '--'}}</div>
</div>
<!-- 流程状态固定显示在卡片右上角,并使用胶囊形标签突出当前进度。 -->
<enum-tag
class="proposal-seconded-status"
v-if="row.instanceState"
:value="row.instanceState"
name="ProcessInstanceStateEnum"
label_key="message"
size="small"
></enum-tag>
</div>
</template>
<template v-slot="{index,row}">
<table-column label="提案类别">{{row.typeName || '--'}}</table-column>
<table-column label="提案人">{{row.createUserName || '--'}}</table-column>
<table-column label="代表团">{{row.delegationName || '--'}}</table-column>
<table-column label="当前节点">{{row.curTaskName || '--'}}</table-column>
</template>
<template #actions="{index,row}">
<div class="action-btn" @click="onView(row,index)">
<i class="fa fa-eye"></i>
<span>查看</span>
</div>
<div class="action-btn" v-if="showSecondAction(row)" @click="onApproval(row,index)">
<i class="fa fa-edit"></i>
<span>附议</span>
</div>
<!-- <div class="action-btn delete" v-if="row.canRevoke" @click="onRevoke(row)">-->
<!-- <i class="fa fa-reply"></i>-->
<!-- <span>撤回</span>-->
<!-- </div>-->
</template>
</table-list>
</div>
<div v-else style="display: flex; justify-content: space-between; column-gap: 10px; padding: 10px">
<van-button type="danger" block @click="updateSecond(2)">不同意</van-button>
<van-button type="primary" block @click="updateSecond(1)">同意</van-button>
</div>
</div>
</proposal-info>
</main>
</div>
</div>
<script nonce="${cspNonce!}">
<!--#include("../../common/info.js"){}#-->
const vue = new Vue({
new Vue({
el: "#app",
store,
data() {
@@ -93,6 +339,9 @@ layout("/layouts/platform_h5.html"){
pageNumber: 1,
pageSize: 10,
totalCount: 0,
// 排序字段和方向为空时,后端使用任务创建时间倒序返回分页数据。
pageOrderName: "",
pageOrderBy: "",
name: null,
sessionId: null,
approvalText: "0",
@@ -100,17 +349,15 @@ layout("/layouts/platform_h5.html"){
},
sessionOptions: [],
row: null,
formData: {},
showApprovalForm: false,
rules: {
approvalSignature: [{required: true, message: "请签字"}]
}
// 任一筛选项展开时,将筛选栏切换为与下拉面板衔接的上圆角样式。
filterOpened: false,
approvalOptions: [
{text: "未附议", value: "0"},
{text: "已附议", value: "1"}
],
pendingListCache: null
}
},
components: {
"proposal-info": PROPOSAL_INFO
},
methods: {
// 附议按钮显示规则:
// 1. 当前任务进行中(taskState=10)时显示
@@ -119,101 +366,298 @@ layout("/layouts/platform_h5.html"){
return row.taskState === 10 || (row.taskState === 99 && ["invite", "second", "preAudit", "delegation", "committee"].includes(row.taskKey))
},
onReady() {
this.listSession()
if (this.pendingListCache) {
this.restoreListCache()
this.listSession(true)
return
}
this.listSession(false)
},
listSession() {
// 附议状态下拉框使用字符串值展示,查询接口使用布尔值区分已附议和未附议。
approvalChange(value) {
this.$set(this.pageForm, "approval", value === "1")
this.doSearch()
},
listSession(skipDefaultSearch) {
this.$axios.post("/platform/proposal/common/listSession").then((res) => {
if (res.code === 0) {
this.sessionOptions = [
{
text: "全部届次",
value: null
}
].concat(res.data.map((v) => ({text: v.fullName, value: v.id})))
if (this.sessionOptions.length > 0) {
this.pageForm.sessionId = this.sessionOptions[0].value
this.doSearch()
}
this.sessionOptions = [{text: "全部届次", value: null}]
.concat(res.data.map((v) => ({text: v.fullName, value: v.id})))
if (skipDefaultSearch) return
// “全部届次”固定展示在首位,但首次查询仍默认使用接口倒序返回的最新届次。
this.$set(this.pageForm, "sessionId", res.data.length > 0 ? res.data[0].id : null)
this.doSearch()
}
})
},
onView(row) {
this.showApprovalForm = false
this.$refs.proposalInfoRef.onOpen(row)
onView(row, index) {
this.saveListCache(row, index)
this.$pjaxReplace("/platform/proposal/seconded/info/h5?bizId=" + encodeURIComponent(row.id) + "&action=view")
},
onApproval(row) {
this.row = row
this.showApprovalForm = true
this.$refs.proposalInfoRef.onOpen(row)
this.formData = {
processTaskId: row.taskId,
taskId: row.taskId,
proposalId: row.id,
taskActorUserId: row.taskActorUserId,
taskName: row.curTaskName,
tf_opinion: "同意该提案"
onApproval(row, index) {
this.saveListCache(row, index)
const params = new URLSearchParams()
params.set("bizId", row.id)
params.set("action", "approval")
params.set("taskId", row.taskId)
params.set("taskActorUserId", row.taskActorUserId)
params.set("curTaskCode", row.curTaskCode || "")
params.set("taskName", row.curTaskName || "提案附议")
this.$pjaxReplace("/platform/proposal/seconded/info/h5?" + params.toString())
},
getListCacheKey() {
return "proposal:seconded:h5:list-state"
},
getListCacheTokenParam() {
return "secondedListCacheToken"
},
getCurrentListCacheToken() {
return new URLSearchParams(window.location.search).get(this.getListCacheTokenParam()) || ""
},
/**
* 将本次列表缓存标识绑定到当前历史记录。
* PJAX 返回时可能重复初始化列表,两个实例通过同一标识读取同一份快照,避免后一次初始化覆盖恢复结果。
*/
bindListCacheToken(token) {
const currentUrl = new URL(window.location.href)
currentUrl.searchParams.set(this.getListCacheTokenParam(), token)
const currentState = window.history.state ? {...window.history.state} : null
if (currentState && currentState.url) {
currentState.url = currentUrl.toString()
}
window.history.replaceState(currentState, document.title, currentUrl.toString())
if ($.pjax && $.pjax.state) {
$.pjax.state.url = currentUrl.toString()
}
},
async handleTaskAction(val) {
/**
* 用户主动重新查询时清除详情返回标识,防止后续重新进入列表时误用旧筛选和旧锚点。
*/
clearListCache() {
const token = this.getCurrentListCacheToken()
if (!token) return
const currentUrl = new URL(window.location.href)
currentUrl.searchParams.delete(this.getListCacheTokenParam())
const currentState = window.history.state ? {...window.history.state} : null
if (currentState && currentState.url) {
currentState.url = currentUrl.toString()
}
window.history.replaceState(currentState, document.title, currentUrl.toString())
if ($.pjax && $.pjax.state) {
$.pjax.state.url = currentUrl.toString()
}
sessionStorage.removeItem(this.getListCacheKey())
},
// H5 页面由 page-wrapper 承担滚动,缓存和恢复位置时不能使用 window 的滚动值。
getListScrollContainer() {
return document.querySelector(".page-wrapper") || document.scrollingElement || document.documentElement
},
/**
* 进入详情前保存查询条件、已加载分页和点击卡片锚点。
* row/index 分别为当前记录和它在已加载列表中的位置;缓存仅在当前标签页保留。
*/
saveListCache(row, index) {
const tableList = this.$refs.tableListRef
if (!tableList) return
const cacheToken = Date.now() + "-" + Math.random().toString(36).slice(2)
const tableState = typeof tableList.getListState === "function"
? tableList.getListState()
: {
tableData: JSON.parse(JSON.stringify(tableList.tableData || [])),
localPageForm: JSON.parse(JSON.stringify(tableList.localPageForm || this.pageForm)),
tableFinished: !!tableList.tableFinished
}
const cards = document.querySelectorAll(".proposal-seconded-list .table-list-item")
const currentCard = cards[index]
const scrollContainer = this.getListScrollContainer()
const cache = {
time: Date.now(),
token: cacheToken,
pageForm: JSON.parse(JSON.stringify(this.pageForm)),
tableState: tableState,
completedBizId: "",
anchor: {
rowId: row.id,
previousRowId: index > 0 && tableState.tableData[index - 1] ? tableState.tableData[index - 1].id : "",
nextRowId: tableState.tableData[index + 1] ? tableState.tableData[index + 1].id : "",
viewportTop: currentCard ? currentCard.getBoundingClientRect().top : 0,
scrollTop: scrollContainer ? scrollContainer.scrollTop : 0
}
}
try {
await this.$refs.formRef.validate();
this.$dialog.confirm({
title: "提示",
message: "您确定要提交吗?"
}).then(() => {
this.$axios.post("/platform/proposal/seconded/executeTask", {
data: JSON.stringify({
...this.formData,
submitType: val
})
}).then((res) => {
if (res.code === 0) {
this.$refs.proposalInfoRef.onClose()
this.$toast.success(res.msg)
this.doSearch()
}
})
}).catch(() => {
})
sessionStorage.setItem(this.getListCacheKey(), JSON.stringify(cache))
this.bindListCacheToken(cacheToken)
} catch (error) {
sessionStorage.removeItem(this.getListCacheKey())
}
},
updateSecond(submitType) {
this.$dialog.confirm({
title: "提示",
message: "您确定要提交吗?"
}).then(() => {
this.$axios.post("/platform/proposal/seconded/updateInfo", {
proposalId: this.formData.proposalId,
taskId: this.formData.taskId,
taskActorUserId: this.formData.taskActorUserId,
opinion: this.formData.tf_opinion,
submitType: submitType,
}).then((res) => {
if (res.code === 0) {
this.$refs.proposalInfoRef.onClose()
this.$toast.success(res.msg)
this.doSearch()
}
/**
* 读取十分钟内的列表缓存,并先恢复查询参数,保证 TableList 创建时使用原查询条件。
*/
prepareListCache() {
try {
const cacheToken = this.getCurrentListCacheToken()
if (!cacheToken) return
const cacheText = sessionStorage.getItem(this.getListCacheKey())
if (!cacheText) return
const cache = JSON.parse(cacheText)
if (!cache || cache.token !== cacheToken || !cache.tableState) return
if (Date.now() - cache.time > 10 * 60 * 1000) {
this.clearListCache()
return
}
Object.keys(cache.pageForm || {}).forEach((key) => {
this.$set(this.pageForm, key, cache.pageForm[key])
})
}).catch(() => {
this.$set(this, "pendingListCache", cache)
} catch (error) {
sessionStorage.removeItem(this.getListCacheKey())
}
},
/**
* 恢复列表并按业务结果选择定位锚点。
* 未提交时定位原记录;附议成功且原筛选为未附议时移除原记录并定位下一条,下一条不存在则定位上一条。
*/
restoreListCache() {
const cache = this.pendingListCache
const tableList = this.$refs.tableListRef
if (!cache || !tableList) return
const anchor = cache.anchor || {}
if (cache.completedBizId) {
this.refreshCompletedListCache(cache)
return
}
this.applyListCache(cache.tableState, anchor.rowId, anchor)
},
/**
* 附议成功后按原来已加载的页数重新查询最新数据。
* 请求参数沿用原筛选条件,并临时扩大 pageSize,一次取回原浏览范围,避免删除记录造成后续分页漏数。
*/
refreshCompletedListCache(cache) {
const originalPageSize = Number(cache.pageForm.pageSize || 10)
const loadedCount = Math.max(cache.tableState.tableData.length, originalPageSize)
const loadedPageCount = Math.max(1, Math.ceil(loadedCount / originalPageSize))
const requestForm = {
...cache.pageForm,
pageNumber: 1,
pageSize: loadedPageCount * originalPageSize
}
const loading = createListLoading()
this.$axios.post("/platform/proposal/seconded/pageData", requestForm, {h5SkeletonLoading: true})
.then((res) => {
if (res.code !== 0) {
this.applyCompletedCacheFallback(cache)
return
}
const freshState = {
tableData: res.data.list || [],
localPageForm: {
...cache.pageForm,
pageNumber: loadedPageCount,
pageSize: originalPageSize,
totalCount: res.data.totalCount
},
tableFinished: (res.data.list || []).length >= res.data.totalCount
}
const anchor = cache.anchor || {}
this.applyListCache(freshState, anchor.nextRowId || anchor.previousRowId, anchor)
})
.catch(() => {
this.applyCompletedCacheFallback(cache)
})
.finally(() => {
loading.close()
})
},
// 最新列表查询失败时,仅从旧快照移除已处理记录,仍保证用户能够回到原浏览区域。
applyCompletedCacheFallback(cache) {
const tableState = cache.tableState
const completedIndex = tableState.tableData.findIndex((item) => String(item.id) === String(cache.completedBizId))
if (completedIndex >= 0 && cache.pageForm.approval === false) {
tableState.tableData.splice(completedIndex, 1)
if (tableState.localPageForm && tableState.localPageForm.totalCount > 0) {
tableState.localPageForm.totalCount--
}
} else if (completedIndex >= 0) {
this.$set(tableState.tableData[completedIndex], "taskState", 20)
}
const anchor = cache.anchor || {}
this.applyListCache(tableState, anchor.nextRowId || anchor.previousRowId, anchor)
},
/**
* 将列表状态写回 TableList,并把目标卡片恢复到离开页面前的屏幕位置。
*/
applyListCache(tableState, targetRowId, anchor) {
const tableList = this.$refs.tableListRef
if (!tableList) return
if (typeof tableList.restoreListState === "function") {
if (!tableList.restoreListState(tableState)) return
} else {
this.$set(tableList, "tableData", tableState.tableData || [])
this.$set(tableList, "localPageForm", {...tableList.localPageForm, ...(tableState.localPageForm || {})})
this.$set(tableList, "tableFinished", !!tableState.tableFinished)
this.$set(tableList, "tableLoading", false)
this.$set(tableList, "tableRefreshing", false)
tableList.$emit("update:page_form", {...tableList.localPageForm})
}
this.$set(this, "pendingListCache", null)
this.$nextTick(() => {
setTimeout(() => {
const cards = Array.from(document.querySelectorAll(".proposal-seconded-list .table-list-item"))
const scrollContainer = this.getListScrollContainer()
if (!scrollContainer) return
const candidateIds = [targetRowId, anchor.nextRowId, anchor.previousRowId]
.filter((id) => id)
.map((id) => String(id))
.filter((id, index, values) => values.indexOf(id) === index)
// 严格按当前记录、下一条、上一条的顺序选择锚点,避免列表中靠前的上一条被提前命中。
let targetIndex = -1
candidateIds.some((candidateId) => {
targetIndex = (tableState.tableData || []).findIndex((item) => String(item.id) === candidateId)
return targetIndex >= 0
})
const targetCard = targetIndex >= 0 ? cards[targetIndex] : null
if (targetCard) {
// 首次滚动会触发筛选栏吸顶,再次校正可消除 sticky 布局切换造成的位置偏差。
const restoreTargetCard = () => {
if (!targetCard.isConnected) return
const targetTop = scrollContainer.scrollTop + targetCard.getBoundingClientRect().top - Number(anchor.viewportTop || 0)
scrollContainer.scrollTo(0, Math.max(0, targetTop))
}
restoreTargetCard()
setTimeout(restoreTargetCard, 100)
return
}
scrollContainer.scrollTo(0, Number(anchor.scrollTop || 0))
}, 0)
})
},
doSearch() {
this.clearListCache()
this.$nextTick(() => {
this.pageForm.pageNumber = 1
this.pageForm.totalCount = 0
this.$refs.tableListRef.doSearch()
})
},
},
created() {
this.prepareListCache()
}
})
</script>
@@ -0,0 +1,133 @@
<!--#
layout("/layouts/platform_h5.html"){
#-->
<div id="app" v-cloak>
<van-nav-bar @click-left="historyBack" left-arrow left-text="返回" :title="pageTitle" placeholder fixed></van-nav-bar>
<proposal-info v-if="bizId" :biz-id="bizId">
<div v-if="isApproval">
<div class="process-title">提案附议</div>
<van-form ref="formRef">
<van-field
v-model="formData.tf_opinion"
name="tf_opinion"
label="审批意见"
placeholder="请输入审批意见"
:rules="[{ required: true, message: '请填写审批意见' }]"
required
maxlength="100"
show-word-limit
></van-field>
</van-form>
<div style="display: flex; justify-content: space-between; column-gap: 10px; padding: 10px">
<van-button type="danger" block :loading="formLoading" @click="submitSeconded(false)">不同意</van-button>
<van-button type="primary" block :loading="formLoading" @click="submitSeconded(true)">同意</van-button>
</div>
</div>
</proposal-info>
</div>
<script nonce="${cspNonce!}">
<!--#include("../../common/info.js"){}#-->
new Vue({
el: "#app",
store,
components: {
"proposal-info": PROPOSAL_INFO
},
data() {
return {
bizId: "",
action: "view",
curTaskCode: "",
formData: {},
formLoading: false
}
},
computed: {
isApproval() {
return this.action === "approval"
},
pageTitle() {
return this.isApproval ? "提案附议" : "提案详情"
}
},
methods: {
/**
* 提交附议结果。
* isAgree 为 true 表示同意、false 表示不同意;接口返回 Result,code 为 0 表示提交成功。
*/
submitSeconded(isAgree) {
this.$refs.formRef.validate()
.then(() => this.$dialog.confirm({title: "提示", message: "您确定要提交吗?"}))
.then(() => {
this.formLoading = true
if (this.curTaskCode === "second") {
return this.$axios.post("/platform/proposal/seconded/executeTask", {
data: JSON.stringify({
...this.formData,
submitType: isAgree ? 1 : 20
})
})
}
return this.$axios.post("/platform/proposal/seconded/updateInfo", {
proposalId: this.formData.proposalId,
taskId: this.formData.taskId,
taskActorUserId: this.formData.taskActorUserId,
opinion: this.formData.tf_opinion,
submitType: isAgree ? 1 : 2
})
})
.then((res) => {
if (res && res.code === 0) {
this.markListCacheCompleted()
this.$toast.success(res.msg)
this.historyBack()
}
})
.catch(() => {})
.finally(() => {
this.formLoading = false
})
},
/**
* 附议成功后标记已处理的提案主键。
* 返回未附议列表时据此移除原记录,并改用相邻卡片恢复浏览位置。
*/
markListCacheCompleted() {
const cacheKey = "proposal:seconded:h5:list-state"
try {
const cacheText = sessionStorage.getItem(cacheKey)
if (!cacheText) return
const cache = JSON.parse(cacheText)
cache.completedBizId = this.bizId
cache.time = Date.now()
sessionStorage.setItem(cacheKey, JSON.stringify(cache))
} catch (error) {
sessionStorage.removeItem(cacheKey)
}
}
},
created() {
const params = new URLSearchParams(window.location.search)
// bizId、taskId、taskActorUserId 和 curTaskCode 分别用于详情查询、任务定位、附议人校验和提交链路选择。
this.bizId = params.get("bizId") || ""
this.action = params.get("action") || "view"
this.curTaskCode = params.get("curTaskCode") || ""
if (this.isApproval) {
this.$set(this.formData, "processTaskId", params.get("taskId") || "")
this.$set(this.formData, "taskId", params.get("taskId") || "")
this.$set(this.formData, "proposalId", this.bizId)
this.$set(this.formData, "taskActorUserId", params.get("taskActorUserId") || "")
this.$set(this.formData, "taskName", params.get("taskName") || "提案附议")
this.$set(this.formData, "tf_opinion", "同意该提案")
}
if (!this.bizId) {
this.$toast.fail("缺少提案参数")
}
}
})
</script>
<!--#
}
#-->
@@ -57,45 +57,6 @@ layout("/layouts/platform_h5.html"){
</template>
</table-list>
<proposal-info ref="proposalInfoRef">
<div v-if="showApprovalForm">
<div class="process-title">承办单位意见</div>
<van-form ref="formRef">
<van-field
v-model="formData.result"
name="result"
label="承办意向"
placeholder="请选择承办意向"
:rules="[{ required: true, message: '必填' }]"
required
is-link
@click="showPicker = true"
></van-field>
<van-popup v-model="showPicker" position="bottom">
<van-picker
show-toolbar
:columns="dict.type.UNDERTAKE_SUGGESTION.map(item => item.code)"
@confirm="(val) => {formData.result = val; showPicker = false}"
@cancel="showPicker = false"
></van-picker>
</van-popup>
<van-field
v-model="formData.opinion"
name="opinion"
label="反馈意见"
placeholder="请输入反馈意见"
:rules="[{ required: true, message: '必填' }]"
required
show-word-limit
></van-field>
</van-form>
<div style="display: flex; justify-content: space-between; column-gap: 10px; padding: 10px">
<van-button block @click="">取消</van-button>
<van-button type="primary" block @click="doSubmit">提交</van-button>
</div>
</div>
</proposal-info>
<van-action-sheet v-model="visible" title="意见信息">
<div class="detail-container">
<van-cell-group :key="index" v-for="item,index in suggestionList">
@@ -120,14 +81,9 @@ layout("/layouts/platform_h5.html"){
</div>
<script nonce="${cspNonce!}">
<!--#include("../../common/info.js"){}#-->
new Vue({
el: "#app",
store,
dicts: ["UNDERTAKE_SUGGESTION"],
components: {
"proposal-info": PROPOSAL_INFO
},
data() {
return {
pageForm: {
@@ -141,10 +97,6 @@ layout("/layouts/platform_h5.html"){
},
sessionOptions: [],
formData: {},
showApprovalForm: false,
showPicker: false,
suggestionList: [],
visible: false,
}
@@ -203,33 +155,10 @@ layout("/layouts/platform_h5.html"){
})
},
onAudit(row) {
this.showApprovalForm = true
this.$refs.proposalInfoRef.onOpen(row)
this.formData = {
proposalId: row.id,
}
},
async doSubmit() {
try {
await this.$refs.formRef.validate()
this.$dialog.confirm({
title: "提示",
message: "您确定要提交吗?"
}).then(() => {
this.$axios.post("/platform/proposal/suggestion/suggest", this.formData)
.then((res) => {
if (res.code === 0) {
this.$refs.proposalInfoRef.onClose()
this.$toast.success(res.msg)
this.doSearch()
}
})
}).catch(() => {})
} catch (error) {}
this.$pjaxReplace("/platform/proposal/suggestion/info/h5?bizId=" + encodeURIComponent(row.id) + "&action=approval")
},
onView(row) {
this.showApprovalForm = false
this.$refs.proposalInfoRef.onOpen(row)
this.$pjaxReplace("/platform/proposal/suggestion/info/h5?bizId=" + encodeURIComponent(row.id) + "&action=view")
},
doSearch() {
this.$nextTick(() => {
@@ -0,0 +1,115 @@
<!--#
layout("/layouts/platform_h5.html"){
#-->
<div id="app" v-cloak>
<van-nav-bar @click-left="historyBack" left-arrow left-text="返回" :title="pageTitle" placeholder fixed></van-nav-bar>
<proposal-info v-if="bizId" :biz-id="bizId">
<div v-if="isApproval">
<div class="process-title">承办单位意见</div>
<van-form ref="formRef">
<van-field
v-model="formData.result"
name="result"
label="承办意向"
placeholder="请选择承办意向"
:rules="[{ required: true, message: '请选择承办意向' }]"
required
is-link
@click="showPicker = true"
></van-field>
<van-popup v-model="showPicker" position="bottom">
<van-picker
show-toolbar
:columns="dict.type.UNDERTAKE_SUGGESTION.map(item => item.code)"
@confirm="onResultConfirm"
@cancel="showPicker = false"
></van-picker>
</van-popup>
<van-field
v-model="formData.opinion"
name="opinion"
label="反馈意见"
placeholder="请输入反馈意见"
:rules="[{ required: true, message: '请输入反馈意见' }]"
required
show-word-limit
></van-field>
</van-form>
<div style="display: flex; justify-content: space-between; column-gap: 10px; padding: 10px">
<van-button block :disabled="formLoading" @click="historyBack">取消</van-button>
<van-button type="primary" block :loading="formLoading" @click="doSubmit">提交</van-button>
</div>
</div>
</proposal-info>
</div>
<script nonce="${cspNonce!}">
<!--#include("../../common/info.js"){}#-->
new Vue({
el: "#app",
store,
dicts: ["UNDERTAKE_SUGGESTION"],
components: {
"proposal-info": PROPOSAL_INFO
},
data() {
return {
bizId: "",
action: "view",
formData: {},
formLoading: false,
showPicker: false
}
},
computed: {
isApproval() {
return this.action === "approval"
},
pageTitle() {
return this.isApproval ? "承办单位意见" : "提案详情"
}
},
methods: {
onResultConfirm(value) {
this.$set(this.formData, "result", value)
this.showPicker = false
},
/**
* 提交承办单位意见;参数包含 proposalId、result 和 opinion,接口返回 Result。
*/
doSubmit() {
this.$refs.formRef.validate()
.then(() => this.$dialog.confirm({title: "提示", message: "您确定要提交吗?"}))
.then(() => {
this.formLoading = true
return this.$axios.post("/platform/proposal/suggestion/suggest", this.formData)
})
.then((res) => {
if (res && res.code === 0) {
this.$toast.success(res.msg)
this.historyBack()
}
})
.catch(() => {})
.finally(() => {
this.formLoading = false
})
}
},
created() {
const params = new URLSearchParams(window.location.search)
// bizId 为提案详情查询和承办单位意见保存共同使用的提案主键。
this.bizId = params.get("bizId") || ""
this.action = params.get("action") || "view"
if (this.isApproval) {
this.$set(this.formData, "proposalId", this.bizId)
}
if (!this.bizId) {
this.$toast.fail("缺少提案参数")
}
}
})
</script>
<!--#
}
#-->
@@ -1,260 +1,650 @@
<!--#
layout("/layouts/platform_h5.html"){
#-->
<style>
body {
background-color: #f3f7fd;
}
.proposal-unit-reply-page {
min-height: 100vh;
padding-bottom: calc(24px + env(safe-area-inset-bottom));
box-sizing: border-box;
background-color: #f3f7fd;
}
.proposal-unit-reply-page .van-nav-bar {
background-color: #fff;
}
.proposal-unit-reply-page .van-nav-bar__title {
color: #172033;
font-size: 16px;
font-weight: 600;
}
.proposal-unit-reply-page .van-nav-bar .van-icon,
.proposal-unit-reply-page .van-nav-bar__text {
color: #1989fa;
}
.proposal-unit-reply-content {
padding: 0 12px;
}
.proposal-unit-reply-sticky {
padding: 22px 12px 1px;
box-sizing: border-box;
background-color: #f3f7fd;
}
.proposal-unit-reply-search {
margin: 0 0 8px;
padding: 0;
overflow: hidden;
border-radius: 12px;
background-color: #fff;
box-shadow: 0 5px 16px rgba(43, 73, 112, .1);
}
.proposal-unit-reply-search .van-search__content {
height: 42px;
padding-left: 12px;
align-items: center;
border-radius: 12px;
background-color: #fff;
}
.proposal-unit-reply-search .van-field__control {
color: #263548;
font-size: 14px;
}
.proposal-unit-reply-search .van-field__control::placeholder {
color: #a7b0bf;
}
.proposal-unit-reply-filter {
margin: 0 0 10px;
overflow: hidden;
border-radius: 10px;
box-shadow: 0 5px 16px rgba(43, 73, 112, .08);
transition: border-radius .18s ease;
}
.proposal-unit-reply-filter.is-open {
border-radius: 10px 10px 0 0;
}
.proposal-unit-reply-filter .van-dropdown-menu__bar,
.proposal-unit-reply-filter .van-dropdown-menu__item,
.proposal-unit-reply-filter .van-dropdown-menu__title {
height: 44px;
}
.proposal-unit-reply-filter .van-dropdown-menu__bar {
box-shadow: none;
}
.proposal-unit-reply-filter .van-dropdown-menu__item,
.proposal-unit-reply-filter .van-dropdown-menu__title {
align-items: center;
}
.proposal-unit-reply-filter .van-dropdown-menu__item {
justify-content: center;
}
.proposal-unit-reply-filter .van-dropdown-menu__title {
display: inline-flex;
color: #66758a;
font-size: 14px;
line-height: 20px;
}
.proposal-unit-reply-filter .van-dropdown-menu__title--active,
.proposal-unit-reply-filter .van-dropdown-item__option--active,
.proposal-unit-reply-filter .van-dropdown-item__option--active .van-dropdown-item__icon {
color: #1989fa;
}
.proposal-unit-reply-filter .van-dropdown-menu__title--down::after {
border-color: transparent transparent #1989fa #1989fa;
}
.proposal-unit-reply-filter .van-dropdown-item__content {
width: calc(100% - 24px);
margin: 0 12px;
overflow: hidden;
box-sizing: border-box;
border-radius: 0 0 12px 12px;
box-shadow: 0 10px 24px rgba(31, 65, 108, .14);
}
.proposal-unit-reply-filter .van-dropdown-item__option {
display: flex;
min-height: 48px;
padding: 0 16px;
align-items: center;
color: #46566c;
font-size: 14px;
}
.proposal-unit-reply-filter .van-cell::after {
right: 16px;
left: 16px;
}
.proposal-unit-reply-list .empty-state {
display: none;
}
.proposal-unit-reply-empty {
display: flex;
min-height: 280px;
padding: 46px 18px 36px;
align-items: center;
justify-content: center;
flex-direction: column;
box-sizing: border-box;
text-align: center;
}
.proposal-unit-reply-empty img {
display: block;
width: 86%;
max-width: 290px;
height: auto;
object-fit: contain;
}
.proposal-unit-reply-empty__title {
margin-top: 14px;
color: #50627a;
font-size: 15px;
line-height: 22px;
font-weight: 600;
}
.proposal-unit-reply-empty__hint {
margin-top: 5px;
color: #9aa7b8;
font-size: 12px;
line-height: 18px;
}
.proposal-unit-reply-card-header {
display: flex;
margin-bottom: 7px;
align-items: flex-start;
justify-content: space-between;
}
.proposal-unit-reply-card-heading {
min-width: 0;
padding-right: 10px;
flex: 1;
}
.proposal-unit-reply-card-title {
overflow: hidden;
color: #253247;
font-size: 15px;
line-height: 22px;
font-weight: 600;
text-overflow: ellipsis;
white-space: nowrap;
}
.proposal-unit-reply-card-code {
margin-top: 1px;
color: #9ba6b6;
font-size: 12px;
line-height: 18px;
}
.proposal-unit-reply-status {
display: inline-flex;
flex: none;
}
.proposal-unit-reply-status .van-tag {
display: inline-flex;
min-height: 24px;
padding: 3px 9px;
align-items: center;
box-sizing: border-box;
border: 0;
border-radius: 12px;
font-size: 12px;
line-height: 18px;
white-space: nowrap;
}
@media (max-width: 350px) {
.proposal-unit-reply-content,
.proposal-unit-reply-sticky {
padding-right: 10px;
padding-left: 10px;
}
}
</style>
<div id="app" v-cloak>
<van-nav-bar @click-left="historyBack" left-arrow left-text="返回" title="承办单位答复" placeholder
fixed></van-nav-bar>
<van-sticky offset-top="46px">
<van-search
v-model="pageForm.name"
:show-action="false"
:reverse-color="false"
input-align="left"
placeholder="请输入提案名称搜索"
@search="doSearch"
></van-search>
<van-dropdown-menu :close-on-click-outside="false" :close-on-click-overlay="false">
<van-dropdown-item v-model="pageForm.sessionId" :options="sessionOptions" :multiple="false"
@change="doSearch"></van-dropdown-item>
</van-dropdown-menu>
<van-tabs v-model="pageForm.approvalText"
@change="(val)=>{this.pageForm.approval = val==='1';this.doSearch();}">
<van-tab title="已审核" name="1"></van-tab>
<van-tab title="未审核" name="0"></van-tab>
</van-tabs>
</van-sticky>
<table-list api="/platform/proposal/unitReply/pageData" :page_form.sync="pageForm" @ready="onReady"
ref="tableListRef"
title="name">
<template v-slot="{index,row}">
<table-column label="提案编号">{{row.code}}</table-column>
<table-column label="提案类别">{{row.typeName}}</table-column>
<table-column label="提案人">{{row.createUserName}}</table-column>
<table-column label="代表团">{{row.delegationName}}</table-column>
<table-column label="承办单位">{{row.underTakeName}}{{row.underTakeIsMaster ? '主办' : '协办'}}
</table-column>
<table-column label="当前节点">{{row.curTaskName}}</table-column>
</template>
<template #actions="{index,row}">
<div class="action-btn" @click="onView(row)">
<i class="fa fa-eye"></i>
<span>查看</span>
<div class="proposal-unit-reply-page">
<van-nav-bar @click-left="historyBack" left-arrow left-text="返回" title="承办单位答复" placeholder fixed></van-nav-bar>
<van-sticky :offset-top="46">
<div class="proposal-unit-reply-sticky">
<van-search
v-model="pageForm.name"
class="proposal-unit-reply-search"
placeholder="请输入提案名称搜索"
shape="round"
clearable
@search="doSearch"
@clear="doSearch"
></van-search>
<van-dropdown-menu
class="proposal-unit-reply-filter"
:class="{'is-open': filterOpened}"
active-color="#1989fa"
>
<van-dropdown-item
v-model="pageForm.sessionId"
:options="sessionOptions"
:multiple="false"
@change="doSearch"
@open="filterOpened = true"
@close="filterOpened = false"
></van-dropdown-item>
<van-dropdown-item
v-model="pageForm.approvalText"
:options="approvalOptions"
:multiple="false"
@change="approvalChange"
@open="filterOpened = true"
@close="filterOpened = false"
></van-dropdown-item>
</van-dropdown-menu>
</div>
<div class="action-btn" v-if="row.taskState === 10" @click="onApproval(row)">
<i class="fa fa-edit"></i>
<span>答复</span>
</van-sticky>
<main class="proposal-unit-reply-content">
<div class="proposal-unit-reply-list">
<!-- 列表无记录时使用与团长审核一致的提案专用空状态。 -->
<div v-if="pageForm.totalCount === 0" class="proposal-unit-reply-empty">
<img src="/assets/mobile/img/proposal/proposal-empty.png" alt="暂无提案记录插画">
<div class="proposal-unit-reply-empty__title">暂无提案记录</div>
<div class="proposal-unit-reply-empty__hint">当前筛选条件下暂时没有提案</div>
</div>
<table-list api="/platform/proposal/unitReply/pageData" :page_form.sync="pageForm" @ready="onReady"
ref="tableListRef"
title="name">
<template #header="{index,row}">
<div class="proposal-unit-reply-card-header">
<div class="proposal-unit-reply-card-heading">
<div class="proposal-unit-reply-card-title">{{row.name}}</div>
<div class="proposal-unit-reply-card-code">编号:{{row.code || '--'}}</div>
</div>
<enum-tag
class="proposal-unit-reply-status"
v-if="row.instanceState"
:value="row.instanceState"
name="ProcessInstanceStateEnum"
label_key="message"
size="small"
></enum-tag>
</div>
</template>
<template v-slot="{index,row}">
<table-column label="提案类别">{{row.typeName || '--'}}</table-column>
<table-column label="提案人">{{row.createUserName || '--'}}</table-column>
<table-column label="代表团">{{row.delegationName || '--'}}</table-column>
<table-column label="承办单位">{{row.underTakeName || '--'}}{{row.underTakeIsMaster ? '主办' : '协办'}}</table-column>
<table-column label="当前节点">{{row.curTaskName || '--'}}</table-column>
</template>
<template #actions="{index,row}">
<div class="action-btn" @click="onView(row,index)">
<i class="fa fa-eye"></i>
<span>查看</span>
</div>
<div class="action-btn" v-if="row.taskState === 10" @click="onApproval(row,index)">
<i class="fa fa-edit"></i>
<span>答复</span>
</div>
<div class="action-btn delete" v-if="row.canRevoke" @click="onRevoke(row)">
<i class="fa fa-reply"></i>
<span>撤回</span>
</div>
<!-- 承办单位领导、超级管理员才能转办。 -->
<div class="action-btn" v-if="!pageForm.approval && $auth.hasRoleOr('SYSADMIN,PROPOSAL_UNIT_LEADER')"
@click="openTransfer(row)">
<i class="fa fa-arrow-right"></i>
<span>转交</span>
</div>
</template>
</table-list>
</div>
<div class="action-btn delete" v-if="row.canRevoke" @click="onRevoke(row)">
<i class="fa fa-reply"></i>
<span>撤回</span>
</div>
<!--承办单位领导、超级管理员才能转办-->
<div class="action-btn" v-if="!pageForm.approval && $auth.hasRoleOr('SYSADMIN,PROPOSAL_UNIT_LEADER')"
@click="openTransfer(row)">
<i class="fa fa-arrow-right"></i>
<span>转交</span>
</div>
</template>
</table-list>
<proposal-info ref="proposalInfoRef">
<div v-if="showApprovalForm">
<div class="process-title">{{formData.taskName}}</div>
<van-form ref="formRef">
<van-field :value="formData.underTakeName"
label="承办单位"
readonly></van-field>
<van-field
v-model="formData.tf_implementStateName"
name="tf_implementStateName"
label="落实情况"
placeholder="请选择落实情况"
:rules="[{ required: true, message: '请选择落实情况' }]"
required
is-link
@click="showImplementPicker = true"
></van-field>
<van-popup v-model="showImplementPicker" position="bottom">
<van-picker
show-toolbar
:columns="dict.type.PROPOSAL_REPLY_IMPLEMENT.map(item => ({text: item.label, value: item.code}))"
@confirm="onImplementConfirm"
@cancel="showImplementPicker = false"
></van-picker>
</van-popup>
<van-field
v-model="formData.tf_opinion"
name="tf_opinion"
label="答复内容"
placeholder="请输入答复内容"
:rules="[{ required: true, message: '请填写答复内容' }]"
required
maxlength="100"
show-word-limit
></van-field>
<van-field
v-if="supportCandidate && formData.underTakeIsMaster"
v-model="formData.tf_nextNodeOperatorName"
name="tf_nextNodeOperatorName"
label="校领导"
placeholder="校领导"
:rules="[{ required: true, message: '请选择校领导' }]"
required
is-link
@click="showCandidatesPicker = true"
></van-field>
<van-popup v-model="showCandidatesPicker" position="bottom">
<van-picker
show-toolbar
:columns="candidates.map(item => ({text: item.userName, value: item.userId}))"
@confirm="onCandidatesConfirm"
@cancel="showCandidatesPicker = false"
></van-picker>
</van-popup>
</van-form>
<div style="display: flex; justify-content: space-between; column-gap: 10px; padding: 10px">
<van-button block @click="$refs.proposalInfoRef.onClose()">取消</van-button>
<van-button type="primary" block @click="handleTaskAction(1)">提交</van-button>
</div>
</div>
</proposal-info>
</main>
</div>
<transfer ref="transferRef"></transfer>
</div>
<script nonce="${cspNonce!}">
<!--#include("../../common/info.js"){}#-->
<!--#include("transfer.js"){}#-->
const vue = new Vue({
el: "#app",
store,
dicts: ['PROPOSAL_REPLY_IMPLEMENT'],
data() {
return {
pageForm: {
pageNumber: 1,
pageSize: 10,
totalCount: 0,
pageOrderName: "",
pageOrderBy: "",
name: null,
sessionId: null,
approvalText: "0",
approval: false
},
sessionOptions: [],
formData: {},
showApprovalForm: false,
showImplementPicker: false,
supportCandidate: false,
candidates: [],
showCandidatesPicker: false,
filterOpened: false,
approvalOptions: [
{text: "未审核", value: "0"},
{text: "已审核", value: "1"}
],
pendingListCache: null
}
},
components: {
"proposal-info": PROPOSAL_INFO,
"transfer": transfer
},
methods: {
onReady() {
this.listSession()
if (this.pendingListCache) {
this.restoreListCache()
this.listSession(true)
return
}
this.listSession(false)
},
listSession() {
// 审核状态下拉框使用字符串值展示,查询接口使用布尔值区分已审核和未审核。
approvalChange(value) {
this.$set(this.pageForm, "approval", value === "1")
this.doSearch()
},
// 缓存恢复时只加载届次选项,不覆盖离开列表前的筛选条件和列表快照。
listSession(skipDefaultSearch) {
this.$axios.post("/platform/proposal/common/listSession").then((res) => {
if (res.code === 0) {
this.sessionOptions = [
{
text: "全部届次",
value: null
}
].concat(res.data.map((v) => ({text: v.fullName, value: v.id})))
if (this.sessionOptions.length > 0) {
this.pageForm.sessionId = this.sessionOptions[0].value
this.doSearch()
}
this.sessionOptions = [{text: "全部届次", value: null}]
.concat(res.data.map((item) => ({text: item.fullName, value: item.id})))
if (skipDefaultSearch) return
// 保留承办单位答复原有默认口径:首次进入查询全部届次。
this.$set(this.pageForm, "sessionId", null)
this.doSearch()
}
})
},
onView(row) {
this.showApprovalForm = false
this.$refs.proposalInfoRef.onOpen(row)
onView(row, index) {
this.saveListCache(row, index)
this.$pjaxReplace("/platform/proposal/unitReply/info/h5?bizId=" + encodeURIComponent(row.id) + "&action=view")
},
onApproval(row) {
this.loadCandidates(row.taskId)
onApproval(row, index) {
this.saveListCache(row, index)
const params = new URLSearchParams()
params.set("bizId", row.id)
params.set("action", "approval")
params.set("taskId", row.taskId)
params.set("taskKey", row.taskKey || "")
params.set("taskName", row.taskName || "承办单位答复")
params.set("underTakeName", row.underTakeName || "")
params.set("underTakeIsMaster", row.underTakeIsMaster ? "1" : "0")
this.$pjaxReplace("/platform/proposal/unitReply/info/h5?" + params.toString())
},
this.showApprovalForm = true
this.$refs.proposalInfoRef.onOpen(row)
this.formData = {
processTaskId: row.taskId,
taskKey: row.taskKey,
taskName: row.taskName,
proposalId: row.id,
underTakeName: row.underTakeName,
underTakeIsMaster: row.underTakeIsMaster
getListCacheKey() {
return "proposal:unit-reply:h5:list-state"
},
getListCacheTokenParam() {
return "unitReplyListCacheToken"
},
getCurrentListCacheToken() {
return new URLSearchParams(window.location.search).get(this.getListCacheTokenParam()) || ""
},
/**
* 将承办单位答复列表缓存标识绑定到当前历史记录。
* PJAX 返回时仅恢复当前历史记录对应的筛选条件、分页数据和滚动快照。
*/
bindListCacheToken(token) {
const currentUrl = new URL(window.location.href)
currentUrl.searchParams.set(this.getListCacheTokenParam(), token)
const currentState = window.history.state ? {...window.history.state} : null
if (currentState && currentState.url) {
currentState.url = currentUrl.toString()
}
window.history.replaceState(currentState, document.title, currentUrl.toString())
if ($.pjax && $.pjax.state) {
$.pjax.state.url = currentUrl.toString()
}
},
// 加载候选人列表
loadCandidates(taskId) {
$.get("/flow/common/candidate", {taskId}).then((res) => {
if (res.code === 0) {
this.candidates = res.data.candidates
this.supportCandidate = res.data.support
// 用户主动搜索、切换届次或审核状态时使旧返回缓存失效。
clearListCache() {
const token = this.getCurrentListCacheToken()
if (!token) return
const currentUrl = new URL(window.location.href)
currentUrl.searchParams.delete(this.getListCacheTokenParam())
const currentState = window.history.state ? {...window.history.state} : null
if (currentState && currentState.url) {
currentState.url = currentUrl.toString()
}
window.history.replaceState(currentState, document.title, currentUrl.toString())
if ($.pjax && $.pjax.state) {
$.pjax.state.url = currentUrl.toString()
}
sessionStorage.removeItem(this.getListCacheKey())
},
// platform_h5 的实际滚动区域是 page-wrapper,列表位置必须基于该容器保存和恢复。
getListScrollContainer() {
return document.querySelector(".page-wrapper") || document.scrollingElement || document.documentElement
},
/**
* 进入查看或答复详情前保存列表现场。
* 承办答复可能一案多任务,因此 row 和相邻锚点均使用唯一 taskId,不能使用重复的提案 id。
*/
saveListCache(row, index) {
const tableList = this.$refs.tableListRef
if (!tableList) return
const cacheToken = Date.now() + "-" + Math.random().toString(36).slice(2)
const tableState = typeof tableList.getListState === "function"
? tableList.getListState()
: {
tableData: JSON.parse(JSON.stringify(tableList.tableData || [])),
localPageForm: JSON.parse(JSON.stringify(tableList.localPageForm || this.pageForm)),
tableFinished: !!tableList.tableFinished
}
})
},
onImplementConfirm(value) {
this.$set(this.formData, "tf_implementStateName", value.text)
this.$set(this.formData, "tf_implementState", value.value)
this.showImplementPicker = false
},
onCandidatesConfirm(value) {
this.$set(this.formData, "tf_nextNodeOperatorName", value.text)
this.$set(this.formData, "tf_nextNodeOperator", value.value)
this.showCandidatesPicker = false
},
async handleTaskAction(val) {
const cards = document.querySelectorAll(".proposal-unit-reply-list .table-list-item")
const currentCard = cards[index]
const scrollContainer = this.getListScrollContainer()
const cache = {
time: Date.now(),
token: cacheToken,
pageForm: JSON.parse(JSON.stringify(this.pageForm)),
tableState: tableState,
completedTaskId: "",
anchor: {
taskId: row.taskId,
previousTaskId: index > 0 && tableState.tableData[index - 1]
? tableState.tableData[index - 1].taskId
: "",
nextTaskId: tableState.tableData[index + 1] ? tableState.tableData[index + 1].taskId : "",
viewportTop: currentCard ? currentCard.getBoundingClientRect().top : 0,
scrollTop: scrollContainer ? scrollContainer.scrollTop : 0
}
}
try {
await this.$refs.formRef.validate();
this.$dialog.confirm({
title: "提示",
message: "您确定要提交吗?"
}).then(() => {
delete this.formData.underTakeIsMaster
delete this.formData.underTakeName
this.$axios.post("/flow/common/executeTask", {
data: JSON.stringify({
...this.formData,
submitType: val
})
}).then((res) => {
if (res.code === 0) {
this.$refs.proposalInfoRef.onClose()
this.$toast.success(res.msg)
this.doSearch()
}
})
}).catch(() => {
})
sessionStorage.setItem(this.getListCacheKey(), JSON.stringify(cache))
this.bindListCacheToken(cacheToken)
} catch (error) {
sessionStorage.removeItem(this.getListCacheKey())
}
},
// 仅恢复当前历史记录对应且十分钟内有效的承办单位答复列表缓存。
prepareListCache() {
try {
const cacheToken = this.getCurrentListCacheToken()
if (!cacheToken) return
const cacheText = sessionStorage.getItem(this.getListCacheKey())
if (!cacheText) return
const cache = JSON.parse(cacheText)
if (!cache || cache.token !== cacheToken || !cache.tableState) return
if (Date.now() - cache.time > 10 * 60 * 1000) {
this.clearListCache()
return
}
Object.keys(cache.pageForm || {}).forEach((key) => {
this.$set(this.pageForm, key, cache.pageForm[key])
})
this.$set(this, "pendingListCache", cache)
} catch (error) {
sessionStorage.removeItem(this.getListCacheKey())
}
},
// 普通查看返回恢复旧快照;答复完成返回重新查询,避免未审核列表继续显示已处理任务。
restoreListCache() {
const cache = this.pendingListCache
const tableList = this.$refs.tableListRef
if (!cache || !tableList) return
const anchor = cache.anchor || {}
if (cache.completedTaskId) {
this.refreshCompletedListCache(cache)
return
}
this.applyListCache(cache.tableState, anchor.taskId, anchor)
},
/**
* 按离开前已加载页数重新获取答复后的列表。
* 返回值转换为 TableList 状态,并优先定位下一任务,没有下一任务时定位上一任务。
*/
refreshCompletedListCache(cache) {
const originalPageSize = Number(cache.pageForm.pageSize || 10)
const loadedCount = Math.max(cache.tableState.tableData.length, originalPageSize)
const loadedPageCount = Math.max(1, Math.ceil(loadedCount / originalPageSize))
const requestForm = {
...cache.pageForm,
pageNumber: 1,
pageSize: loadedPageCount * originalPageSize
}
const loading = createListLoading()
this.$axios.post("/platform/proposal/unitReply/pageData", requestForm, {h5SkeletonLoading: true})
.then((res) => {
if (res.code !== 0) {
this.applyCompletedCacheFallback(cache)
return
}
const freshState = {
tableData: res.data.list || [],
localPageForm: {
...cache.pageForm,
pageNumber: loadedPageCount,
pageSize: originalPageSize,
totalCount: res.data.totalCount
},
tableFinished: (res.data.list || []).length >= res.data.totalCount
}
const anchor = cache.anchor || {}
this.applyListCache(freshState, anchor.nextTaskId || anchor.previousTaskId, anchor)
})
.catch(() => {
this.applyCompletedCacheFallback(cache)
})
.finally(() => {
loading.close()
})
},
// 答复后刷新失败时只处理已完成 taskId,避免误删同一提案下的其他承办任务。
applyCompletedCacheFallback(cache) {
const tableState = cache.tableState
const completedIndex = tableState.tableData.findIndex((item) => String(item.taskId) === String(cache.completedTaskId))
if (completedIndex >= 0 && cache.pageForm.approval === false) {
tableState.tableData.splice(completedIndex, 1)
if (tableState.localPageForm && tableState.localPageForm.totalCount > 0) {
tableState.localPageForm.totalCount--
}
} else if (completedIndex >= 0) {
this.$set(tableState.tableData[completedIndex], "taskState", 20)
}
const anchor = cache.anchor || {}
this.applyListCache(tableState, anchor.nextTaskId || anchor.previousTaskId, anchor)
},
/**
* 恢复 TableList 数据并将目标任务卡片放回离开页面前的视口位置。
* sticky 吸顶会改变一次布局,因此首次滚动后再执行一次位置校正。
*/
applyListCache(tableState, targetTaskId, anchor) {
const tableList = this.$refs.tableListRef
if (!tableList) return
if (typeof tableList.restoreListState === "function") {
if (!tableList.restoreListState(tableState)) return
} else {
this.$set(tableList, "tableData", tableState.tableData || [])
this.$set(tableList, "localPageForm", {...tableList.localPageForm, ...(tableState.localPageForm || {})})
this.$set(tableList, "tableFinished", !!tableState.tableFinished)
this.$set(tableList, "tableLoading", false)
this.$set(tableList, "tableRefreshing", false)
tableList.$emit("update:page_form", {...tableList.localPageForm})
}
this.$set(this, "pendingListCache", null)
this.$nextTick(() => {
setTimeout(() => {
const cards = Array.from(document.querySelectorAll(".proposal-unit-reply-list .table-list-item"))
const scrollContainer = this.getListScrollContainer()
if (!scrollContainer) return
const candidateTaskIds = [targetTaskId, anchor.nextTaskId, anchor.previousTaskId]
.filter((taskId) => taskId)
.map((taskId) => String(taskId))
.filter((taskId, index, values) => values.indexOf(taskId) === index)
let targetIndex = -1
candidateTaskIds.some((candidateTaskId) => {
targetIndex = (tableState.tableData || [])
.findIndex((item) => String(item.taskId) === candidateTaskId)
return targetIndex >= 0
})
const targetCard = targetIndex >= 0 ? cards[targetIndex] : null
if (targetCard) {
const restoreTargetCard = () => {
if (!targetCard.isConnected) return
const targetTop = scrollContainer.scrollTop
+ targetCard.getBoundingClientRect().top
- Number(anchor.viewportTop || 0)
scrollContainer.scrollTo(0, Math.max(0, targetTop))
}
restoreTargetCard()
setTimeout(restoreTargetCard, 100)
return
}
scrollContainer.scrollTo(0, Number(anchor.scrollTop || 0))
}, 0)
})
},
onRevoke(row) {
this.$dialog.confirm({
title: "提示",
@@ -269,18 +659,31 @@ layout("/layouts/platform_h5.html"){
})
},
// 打开转办
openTransfer(row){
// 打开转办
openTransfer(row) {
this.$refs.transferRef.openTransfer(row)
},
doSearch() {
this.clearListCache()
this.$nextTick(() => {
this.pageForm.pageNumber = 1
this.pageForm.totalCount = 0
this.$refs.tableListRef.doSearch()
})
},
}
},
created() {
this.prepareListCache()
if (this.pendingListCache) return
const tab = new URLSearchParams(window.location.search).get("tab")
if (tab === "done") {
this.$set(this.pageForm, "approval", true)
this.$set(this.pageForm, "approvalText", "1")
} else if (tab === "todo") {
this.$set(this.pageForm, "approval", false)
this.$set(this.pageForm, "approvalText", "0")
}
}
})
</script>
@@ -0,0 +1,185 @@
<!--#
layout("/layouts/platform_h5.html"){
#-->
<div id="app" v-cloak>
<van-nav-bar @click-left="historyBack" left-arrow left-text="返回" :title="pageTitle" placeholder fixed></van-nav-bar>
<proposal-info v-if="bizId" :biz-id="bizId">
<div v-if="isApproval">
<div class="process-title">{{formData.taskName}}</div>
<van-form ref="formRef">
<van-field :value="formData.underTakeName" label="承办单位" readonly></van-field>
<van-field
v-model="formData.tf_implementStateName"
name="tf_implementStateName"
label="落实情况"
placeholder="请选择落实情况"
:rules="[{ required: true, message: '请选择落实情况' }]"
required
is-link
@click="showImplementPicker = true"
></van-field>
<van-popup v-model="showImplementPicker" position="bottom">
<van-picker
show-toolbar
:columns="dict.type.PROPOSAL_REPLY_IMPLEMENT.map(item => ({text: item.label, value: item.code}))"
@confirm="onImplementConfirm"
@cancel="showImplementPicker = false"
></van-picker>
</van-popup>
<van-field
v-model="formData.tf_opinion"
name="tf_opinion"
label="答复内容"
placeholder="请输入答复内容"
:rules="[{ required: true, message: '请填写答复内容' }]"
required
maxlength="100"
show-word-limit
></van-field>
<van-field
v-if="supportCandidate && formData.underTakeIsMaster"
v-model="formData.tf_nextNodeOperatorName"
name="tf_nextNodeOperatorName"
label="校领导"
placeholder="请选择校领导"
:rules="[{ required: true, message: '请选择校领导' }]"
required
is-link
@click="showCandidatesPicker = true"
></van-field>
<van-popup v-model="showCandidatesPicker" position="bottom">
<van-picker
show-toolbar
:columns="candidates.map(item => ({text: item.userName, value: item.userId}))"
@confirm="onCandidatesConfirm"
@cancel="showCandidatesPicker = false"
></van-picker>
</van-popup>
</van-form>
<div style="display: flex; justify-content: space-between; column-gap: 10px; padding: 10px">
<van-button block :disabled="formLoading" @click="historyBack">取消</van-button>
<van-button type="primary" block :loading="formLoading" @click="handleTaskAction">提交</van-button>
</div>
</div>
</proposal-info>
</div>
<script nonce="${cspNonce!}">
<!--#include("../../common/info.js"){}#-->
new Vue({
el: "#app",
store,
dicts: ["PROPOSAL_REPLY_IMPLEMENT"],
components: {
"proposal-info": PROPOSAL_INFO
},
data() {
return {
bizId: "",
action: "view",
formData: {},
formLoading: false,
showImplementPicker: false,
supportCandidate: false,
candidates: [],
showCandidatesPicker: false
}
},
computed: {
isApproval() {
return this.action === "approval"
},
pageTitle() {
return this.isApproval ? "承办单位答复" : "提案详情"
}
},
methods: {
/**
* taskId 为当前 WF 任务主键;接口返回 candidates 候选人数组和 support 是否需要选择候选人。
*/
loadCandidates(taskId) {
this.$axios.post("/flow/common/candidate", {taskId: taskId}).then((res) => {
if (res.code === 0) {
this.candidates = res.data.candidates
this.supportCandidate = res.data.support
}
})
},
onImplementConfirm(value) {
this.$set(this.formData, "tf_implementStateName", value.text)
this.$set(this.formData, "tf_implementState", value.value)
this.showImplementPicker = false
},
onCandidatesConfirm(value) {
this.$set(this.formData, "tf_nextNodeOperatorName", value.text)
this.$set(this.formData, "tf_nextNodeOperator", value.value)
this.showCandidatesPicker = false
},
/**
* 提交承办单位答复;请求 data 包含任务、落实情况、答复内容及可选校领导,返回 Result。
*/
handleTaskAction() {
this.$refs.formRef.validate()
.then(() => this.$dialog.confirm({title: "提示", message: "您确定要提交吗?"}))
.then(() => {
const submitData = {...this.formData, submitType: 1}
delete submitData.underTakeIsMaster
delete submitData.underTakeName
this.formLoading = true
return this.$axios.post("/flow/common/executeTask", {data: JSON.stringify(submitData)})
})
.then((res) => {
if (res && res.code === 0) {
this.markListCacheCompleted()
this.$toast.success(res.msg)
this.historyBack()
}
})
.catch(() => {})
.finally(() => {
this.formLoading = false
})
},
/**
* 答复成功后记录已处理 WF 任务主键。
* 返回列表时按原筛选和已加载范围刷新数据,只移除本次完成的承办任务。
*/
markListCacheCompleted() {
const cacheKey = "proposal:unit-reply:h5:list-state"
try {
const cacheText = sessionStorage.getItem(cacheKey)
if (!cacheText) return
const cache = JSON.parse(cacheText)
cache.completedTaskId = this.formData.processTaskId || ""
cache.time = Date.now()
sessionStorage.setItem(cacheKey, JSON.stringify(cache))
} catch (error) {
sessionStorage.removeItem(cacheKey)
}
}
},
created() {
const params = new URLSearchParams(window.location.search)
// bizId 用于详情查询,其余参数用于恢复当前承办答复任务的表单上下文。
this.bizId = params.get("bizId") || ""
this.action = params.get("action") || "view"
if (this.isApproval) {
const taskId = params.get("taskId") || ""
this.$set(this.formData, "processTaskId", taskId)
this.$set(this.formData, "taskKey", params.get("taskKey") || "")
this.$set(this.formData, "taskName", params.get("taskName") || "承办单位答复")
this.$set(this.formData, "proposalId", this.bizId)
this.$set(this.formData, "underTakeName", params.get("underTakeName") || "")
this.$set(this.formData, "underTakeIsMaster", params.get("underTakeIsMaster") === "1")
this.loadCandidates(taskId)
}
if (!this.bizId) {
this.$toast.fail("缺少提案参数")
}
}
})
</script>
<!--#
}
#-->
@@ -41,12 +41,8 @@ layout("/layouts/platform_h5.html"){
</template>
</table-list>
<proposal-info ref="proposalInfoRef"></proposal-info>
</div>
<script nonce="${cspNonce!}">
<!--#include("../../common/info.js"){}#-->
const vue = new Vue({
el: "#app",
store,
@@ -67,9 +63,6 @@ layout("/layouts/platform_h5.html"){
showApprovalForm: false
}
},
components: {
"proposal-info": PROPOSAL_INFO
},
methods: {
onReady() {
this.listSession()
@@ -92,8 +85,7 @@ layout("/layouts/platform_h5.html"){
},
onView(row) {
this.showApprovalForm = false
this.$refs.proposalInfoRef.onOpen(row)
this.$pjaxReplace("/platform/proposal/vice/delegation/info/h5?bizId=" + encodeURIComponent(row.id))
},
doSearch() {
@@ -0,0 +1,33 @@
<!--#
layout("/layouts/platform_h5.html"){
#-->
<div id="app" v-cloak>
<van-nav-bar @click-left="historyBack" left-arrow left-text="返回" title="提案详情" placeholder fixed></van-nav-bar>
<proposal-info v-if="bizId" :biz-id="bizId"></proposal-info>
</div>
<script nonce="${cspNonce!}">
<!--#include("../../common/info.js"){}#-->
new Vue({
el: "#app",
store,
components: {
"proposal-info": PROPOSAL_INFO
},
data() {
return {
bizId: ""
}
},
created() {
// bizId 为提案主键,公共详情组件根据该参数返回提案详情对象和流程记录数组。
this.bizId = new URLSearchParams(window.location.search).get("bizId") || ""
if (!this.bizId) {
this.$toast.fail("缺少提案参数")
}
}
})
</script>
<!--#
}
#-->
@@ -409,7 +409,7 @@ layout("/layouts/platform_h5.html"){
@click="setVisibleState('showSessionPicker', true)"
:rules="[{ required: true, message: '请选择所属教代会' }]"
></van-field>
<van-popup v-model="showSessionPicker" position="bottom">
<van-popup v-model="showSessionPicker" position="bottom" round get-container="#app">
<van-picker
show-toolbar
:columns="sessionOptions.map(item => ({text: item.fullName, value: item.id}))"
@@ -436,7 +436,7 @@ layout("/layouts/platform_h5.html"){
@click="setVisibleState('showCommitteePicker', true)"
:rules="[{ required: true, message: '请选择所属委员会' }]"
></van-field>
<van-popup v-model="showCommitteePicker" position="bottom">
<van-popup v-model="showCommitteePicker" position="bottom" round get-container="#app">
<van-picker
show-toolbar
:columns="committeeOptions.map(item => ({text: item.name, value: item.id}))"
@@ -489,7 +489,7 @@ layout("/layouts/platform_h5.html"){
:rules="[{ required: true, message: '请选择提案方式' }]"
required
></van-field>
<van-popup v-model="showSourcePicker" position="bottom">
<van-popup v-model="showSourcePicker" position="bottom" round get-container="#app">
<van-picker
show-toolbar
:columns="sourceOptions.map(item => ({text: item.name, value: item.code}))"
@@ -555,7 +555,7 @@ layout("/layouts/platform_h5.html"){
</section>
<!-- 各选择器沿用原始数据源与回显逻辑。 -->
<van-popup v-model="showTypePicker" position="bottom">
<van-popup v-model="showTypePicker" position="bottom" round get-container="#app">
<van-picker
show-toolbar
:columns="typeOptions.map(item => ({text: item.name, value: item.id}))"
@@ -564,7 +564,7 @@ layout("/layouts/platform_h5.html"){
></van-picker>
</van-popup>
<van-popup v-model="showSuggestionUnitPicker" position="bottom" :style="{ height: '70%' }">
<van-popup v-model="showSuggestionUnitPicker" position="bottom" :style="{ height: '70%' }" round get-container="#app">
<div class="suggestion-unit-picker">
<div class="picker-toolbar">
<button type="button" class="picker-cancel" @click="onSuggestionUnitCancel">取消</button>