Merge remote-tracking branch 'origin/feature_界面UI优化' into feature_界面UI优化

This commit is contained in:
2026-08-25 10:09:17 +08:00
43 changed files with 1398 additions and 3676 deletions
@@ -65,15 +65,6 @@ public class ProposalCaseCheckController {
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(value = {"proposal.caseCheck", "h5.proposal.caseCheck"}, mode = SaMode.OR)
@ApiOperation("分页列表")
@@ -49,13 +49,6 @@ 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")
public Result pageData(@Valid ProposalSearchParam pageForm, boolean approval) {
@@ -67,13 +67,6 @@ 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")
@ApiOperation("分页列表")
@@ -54,13 +54,6 @@ 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")
@ApiOperation("分页列表")
@@ -107,12 +107,6 @@ 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)
@@ -58,13 +58,6 @@ public class ProposalPreAuditController {
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")
@ApiOperation("分页列表")
@@ -74,13 +74,6 @@ 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")
@ApiOperation("分页列表")
@@ -88,12 +88,6 @@ 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,12 +76,6 @@ 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,12 +62,6 @@ 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,12 +42,6 @@ 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,10 +19,4 @@ 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() {
}
}
@@ -166,7 +166,7 @@ layout("/layouts/platform_tour_signup_h5.html"){
</style>
<div id="app" class="tour-signup-h5">
<van-nav-bar title="疗休养报名" left-text="返回" left-arrow @click-left="historyBack" fixed placeholder></van-nav-bar>
<van-nav-bar title="疗休养报名" left-text="返回" left-arrow @click-left="$pjaxReplace('/platform/h5/home')" fixed placeholder></van-nav-bar>
<div class="tour-signup-banner">
<van-swipe class="tour-signup-swipe" :autoplay="3500" indicator-color="white">
@@ -438,7 +438,7 @@ layout("/layouts/platform_tour_signup_h5.html"){
if (this.readCountdown > 0) {
return
}
window.location.href = "/platform/tour/signup/h5/signup"
this.$pjaxReplace('/platform/tour/signup/h5/signup')
}
},
created() {
@@ -9,6 +9,129 @@
background: #f3f7fc;
}
/* 提案 H5 列表统一使用全屏 Popup 承载详情和办理表单。 */
.proposal-full-popup {
width: 100%;
height: 100%;
overflow: hidden;
background: #f3f7fc;
}
.proposal-full-popup__page {
display: flex;
height: 100%;
padding-top: 46px;
overflow: hidden;
box-sizing: border-box;
flex-direction: column;
}
.proposal-full-popup__page > .van-nav-bar {
z-index: 20;
background: #ffffff;
}
.proposal-full-popup__scroll {
min-height: 0;
overflow-y: auto;
flex: 1;
-webkit-overflow-scrolling: touch;
}
.proposal-full-popup__scroll .proposal-view-main {
padding-top: 18px;
}
.proposal-popup-form-actions {
display: flex;
padding: 10px 12px calc(12px + env(safe-area-inset-bottom));
justify-content: space-between;
column-gap: 10px;
}
.proposal-action-icon {
margin-right: 6px;
font-size: 15px;
}
.reply-unit-popup {
display: flex;
height: 100%;
flex-direction: column;
}
.reply-unit-toolbar {
display: grid;
min-height: 48px;
border-bottom: 1px solid #eee;
grid-template-columns: 64px 1fr 64px;
align-items: center;
}
.reply-unit-toolbar button {
height: 48px;
padding: 0;
color: #666;
font-size: 14px;
background: transparent;
border: 0;
}
.reply-unit-toolbar button:last-child {
color: #1989fa;
}
.reply-unit-toolbar-title {
color: #222;
font-size: 16px;
font-weight: 600;
text-align: center;
}
.reply-unit-list {
overflow-y: auto;
flex: 1;
-webkit-overflow-scrolling: touch;
}
.reply-unit-empty {
padding: 48px 16px;
color: #999;
font-size: 14px;
text-align: center;
}
.transfer-dialog {
padding: 18px 16px 76px;
box-sizing: border-box;
}
.proposal-info-card {
margin-bottom: 16px;
padding: 16px;
background: #fff;
border-radius: 10px;
}
.proposal-info-card .info-item {
display: flex;
margin-bottom: 10px;
flex-direction: column;
row-gap: 4px;
}
.proposal-info-card .info-item label {
color: #969799;
font-size: 12px;
}
.proposal-full-popup__page > .button-group {
display: flex;
padding: 10px 16px calc(10px + env(safe-area-inset-bottom));
background: #fff;
column-gap: 10px;
}
.proposal-view-main {
position: relative;
z-index: 2;
@@ -45,6 +168,41 @@
content: "";
}
/* 审核、附议、反馈及答复输入框统一复用提案查询 Card 的视觉规范。 */
.proposal-audit-card__required {
margin-left: 4px;
color: #ee0a24;
font-size: 14px;
}
.proposal-view-card .van-field.proposal-audit-field {
width: calc(100% - 28px);
margin: 0 14px 14px;
padding: 12px;
border: 1px solid #c9d5e5;
border-radius: 10px;
background-color: #f7f9fd;
box-sizing: border-box;
}
.proposal-view-card .van-field.proposal-audit-field .van-field__control {
min-height: 150px;
color: #253247;
line-height: 22px;
}
.proposal-view-card .van-field.proposal-audit-field .van-field__control::placeholder {
color: #9aa7b8;
}
.proposal-view-card .van-field.proposal-audit-field .van-field__word-limit {
color: #718096;
}
.proposal-view-card .van-field.proposal-audit-field::after {
display: none;
}
.proposal-view-card .van-cell-group {
margin: 0;
background: transparent;
@@ -210,6 +210,11 @@ const PROPOSAL_INFO = {
bizId: {
type: String,
required: true
},
// visible 表示承载详情的 Popup 是否打开;每次重新打开时都需要查询最新详情和流程记录。
visible: {
type: Boolean,
default: true
}
},
dicts: ["PROPOSAL_TYPE", "PROPOSAL_SOURCE", "PROPOSAL_CASE_FILING_RESULT", "PROPOSAL_CASE_FILING_TYPE", "PROCESS_TASK_SUBMIT_TYPE", "PROPOSAL_FEEDBACK"],
@@ -218,7 +223,8 @@ const PROPOSAL_INFO = {
viewData: {},
doneTasks: [],
selectedTask: null,
currentBizId: ""
currentBizId: "",
reloadPending: false
}
},
watch: {
@@ -227,12 +233,31 @@ const PROPOSAL_INFO = {
handler(value) {
if (!value) return
this.$set(this, "currentBizId", value)
this.getInfo()
this.getDoneTasks()
if (this.visible) this.queueReload()
}
},
visible(value) {
if (!value || !this.bizId) return
this.$set(this, "currentBizId", this.bizId)
this.queueReload()
}
},
methods: {
/**
* 合并同一轮 bizId 与 Popup 显示状态变化触发的刷新。
* 无参数;组件可见且存在提案主键时调用详情和已办流程接口,无返回值。
*/
queueReload() {
if (this.reloadPending) return
this.$set(this, "reloadPending", true)
this.$nextTick(() => {
this.$set(this, "reloadPending", false)
if (!this.visible || !this.currentBizId) return
this.getInfo()
this.getDoneTasks()
})
},
/**
* 获取字典字段的展示文本。
* options 为 PROPOSAL_SOURCE 等字典数组,value 为业务保存的字典 code;返回名称字符串,无匹配项时返回原值或 --。
@@ -308,8 +333,7 @@ const PROPOSAL_INFO = {
openView(row) {
if (row && typeof row === 'object' && row.id) {
this.$set(this, "currentBizId", row.id)
this.getInfo()
this.getDoneTasks()
this.queueReload()
}
},
@@ -0,0 +1,130 @@
const PROPOSAL_TASK_POPUP = {
template: /*language=HTML*/ `
<van-popup v-model="visible" position="right" class="proposal-full-popup" :close-on-click-overlay="false" get-container="#app">
<div class="proposal-full-popup__page">
<van-nav-bar @click-left="close" left-arrow left-text="返回" :title="title" placeholder fixed></van-nav-bar>
<div class="proposal-full-popup__scroll">
<proposal-info v-if="bizId" :biz-id="bizId" :visible="visible">
<div v-if="isApproval">
<van-form ref="formRef">
<template v-if="kind === 'delegation' || kind === 'preAudit' || kind === 'schoolLeaderApproval'">
<section class="proposal-view-card">
<h2 class="proposal-view-card__title">审批意见<span class="proposal-audit-card__required">*</span></h2>
<van-field v-model="formData.tf_opinion" class="proposal-audit-field" name="tf_opinion" type="textarea" rows="6" label="" placeholder="请输入审批意见" :rules="[{required:true,message:'请填写审批意见'}]" required maxlength="100" show-word-limit></van-field>
</section>
</template>
<template v-if="kind === 'feedbackEvaluation'">
<van-field v-model="formData.tf_feedback_name" label="满意度" placeholder="请选择满意度" :rules="[{required:true,message:'请选择满意度'}]" required readonly is-link @click="openLayer('feedback-picker')"></van-field>
<section class="proposal-view-card">
<h2 class="proposal-view-card__title">审批意见<span class="proposal-audit-card__required">*</span></h2>
<van-field v-model="formData.tf_opinion" class="proposal-audit-field" name="tf_opinion" type="textarea" rows="6" label="" placeholder="请输入审批意见" :rules="[{required:true,message:'请填写审批意见'}]" required maxlength="100" show-word-limit></van-field>
</section>
</template>
<template v-if="kind === 'suggestion'">
<van-field v-model="formData.result" label="承办意向" placeholder="请选择承办意向" :rules="[{required:true,message:'请选择承办意向'}]" required readonly is-link @click="openLayer('result-picker')"></van-field>
<section class="proposal-view-card">
<h2 class="proposal-view-card__title">反馈意见<span class="proposal-audit-card__required">*</span></h2>
<van-field v-model="formData.opinion" class="proposal-audit-field" name="opinion" type="textarea" rows="6" label="" placeholder="请输入反馈意见" :rules="[{required:true,message:'请输入反馈意见'}]" required show-word-limit></van-field>
</section>
</template>
<template v-if="kind === 'unitReply'">
<van-field :value="formData.underTakeName" label="承办单位" readonly></van-field>
<van-field v-model="formData.tf_implementStateName" label="落实情况" placeholder="请选择落实情况" :rules="[{required:true,message:'请选择落实情况'}]" required readonly is-link @click="openLayer('implement-picker')"></van-field>
<section class="proposal-view-card">
<h2 class="proposal-view-card__title">答复内容<span class="proposal-audit-card__required">*</span></h2>
<van-field v-model="formData.tf_opinion" class="proposal-audit-field" name="tf_opinion" type="textarea" rows="6" label="" placeholder="请输入答复内容" :rules="[{required:true,message:'请填写答复内容'}]" required maxlength="100" show-word-limit></van-field>
</section>
<van-field v-if="supportCandidate && formData.underTakeIsMaster" v-model="formData.tf_nextNodeOperatorName" label="校领导" placeholder="请选择校领导" :rules="[{required:true,message:'请选择校领导'}]" required readonly is-link @click="openLayer('candidate-picker')"></van-field>
</template>
<template v-if="kind === 'caseCheck'">
<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" 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>
<van-field v-if="formData.tf_caseCheckNeedSecondReply === 1" :value="selectedReplyUnitNames" label="答复单位" placeholder="请选择办理单位" readonly required is-link :rules="[{validator:validateSecondReplyUnits,message:'请选择需要二次答复的办理单位'}]" @click="openReplyUnitPicker"></van-field>
<section class="proposal-view-card">
<h2 class="proposal-view-card__title">审批意见<span class="proposal-audit-card__required">*</span></h2>
<van-field v-model="formData.tf_opinion" class="proposal-audit-field" name="tf_opinion" type="textarea" rows="6" label="" placeholder="请输入审批意见" required maxlength="100" show-word-limit :rules="[{required:true,message:'请填写审批意见'}]"></van-field>
</section>
</template>
</van-form>
<div class="proposal-popup-form-actions" v-if="kind === 'delegation'">
<van-button type="danger" block :loading="formLoading" @click="prepareSubmit(6)">退回</van-button><van-button type="danger" block :loading="formLoading" @click="prepareSubmit(2)">不同意</van-button><van-button type="primary" block :loading="formLoading" @click="prepareSubmit(1)">同意</van-button>
</div>
<div class="proposal-popup-form-actions" v-else-if="kind === 'preAudit'" style="display:grid;grid-template-columns:repeat(2,minmax(0,1fr))">
<van-button type="warning" :loading="formLoading" @click="prepareSubmit(4)">退回(不审核)</van-button><van-button type="warning" :loading="formLoading" @click="prepareSubmit(6)">退回(审核)</van-button><van-button type="danger" :loading="formLoading" @click="prepareSubmit(2)">不同意</van-button><van-button type="primary" :loading="formLoading" @click="prepareSubmit(1)">同意</van-button>
</div>
<div class="proposal-popup-form-actions" v-else-if="kind === 'schoolLeaderApproval'"><van-button type="danger" block :loading="formLoading" @click="prepareSubmit('back')">退回答复</van-button><van-button type="primary" block :loading="formLoading" @click="prepareSubmit('agree')">同意答复</van-button></div>
<div class="proposal-popup-form-actions" v-else><van-button block :disabled="formLoading" @click="close">取消</van-button><van-button type="primary" block :loading="formLoading" @click="prepareSubmit(1)">提交</van-button></div>
</div>
</proposal-info>
</div>
</div>
<van-popup v-model="feedbackPickerVisible" position="bottom" :close-on-click-overlay="false"><van-picker show-toolbar :columns="dict.type.PROPOSAL_FEEDBACK.map((item) => ({text:item.label,value:item.code}))" @confirm="confirmFeedback" @cancel="closeLayer('feedback-picker')"></van-picker></van-popup>
<van-popup v-model="resultPickerVisible" position="bottom" :close-on-click-overlay="false"><van-picker show-toolbar :columns="dict.type.UNDERTAKE_SUGGESTION.map((item) => item.code)" @confirm="confirmResult" @cancel="closeLayer('result-picker')"></van-picker></van-popup>
<van-popup v-model="implementPickerVisible" position="bottom" :close-on-click-overlay="false"><van-picker show-toolbar :columns="dict.type.PROPOSAL_REPLY_IMPLEMENT.map((item) => ({text:item.label,value:item.code}))" @confirm="confirmImplement" @cancel="closeLayer('implement-picker')"></van-picker></van-popup>
<van-popup v-model="candidatePickerVisible" position="bottom" :close-on-click-overlay="false"><van-picker show-toolbar :columns="candidates.map((item) => ({text:item.userName,value:item.userId}))" @confirm="confirmCandidate" @cancel="closeLayer('candidate-picker')"></van-picker></van-popup>
<van-popup v-model="replyUnitPickerVisible" position="bottom" round :close-on-click-overlay="false" :style="{height:'70%'}">
<div class="reply-unit-popup"><div class="reply-unit-toolbar"><button type="button" @click="closeLayer('reply-unit-picker')">取消</button><div class="reply-unit-toolbar-title">选择答复单位</div><button type="button" @click="confirmReplyUnits">确定</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>
<van-dialog :value="confirmVisible" title="提示" message="您确定要提交吗?" show-cancel-button @confirm="submit" @cancel="closeLayer('submit-confirm')"></van-dialog>
</van-popup>
`,
dicts: ["PROPOSAL_FEEDBACK", "UNDERTAKE_SUGGESTION", "PROPOSAL_REPLY_IMPLEMENT"],
components: {"proposal-info": PROPOSAL_INFO},
props: {kind: {type: String, required: true}, pageKey: {type: String, required: true}},
data() { return {visible:false,bizId:"",action:"view",formData:{},formLoading:false,pendingSubmitType:1,feedbackPickerVisible:false,resultPickerVisible:false,implementPickerVisible:false,candidatePickerVisible:false,replyUnitPickerVisible:false,confirmVisible:false,supportCandidate:false,candidates:[],replyUnitOptions:[],pendingReplyUnitIds:[],unsubscribeHistoryLayer:null} },
computed: {
isApproval() { return this.action === "approval" },
title() { const titles={delegation:"团长审核",preAudit:"校工会预审核",schoolLeaderApproval:"分管校领导审批",feedbackEvaluation:"反馈评价",suggestion:"承办单位意见",unitReply:"承办单位答复",caseCheck:"提案委员会审查"}; return this.isApproval ? titles[this.kind] : "提案详情" },
selectedReplyUnitNames() { const ids=this.formData.tf_secondReplyUnitIds || []; return this.replyUnitOptions.filter((item) => ids.indexOf(item.unitId)>-1).map((item) => item.unitName+""+(item.isMaster?"主办":"协办")+"").join("、") }
},
methods: {
syncLayers(stack) { this.$set(this,"visible",stack.includes("detail")); this.$set(this,"feedbackPickerVisible",stack.includes("feedback-picker")); this.$set(this,"resultPickerVisible",stack.includes("result-picker")); this.$set(this,"implementPickerVisible",stack.includes("implement-picker")); this.$set(this,"candidatePickerVisible",stack.includes("candidate-picker")); this.$set(this,"replyUnitPickerVisible",stack.includes("reply-unit-picker")); this.$set(this,"confirmVisible",stack.includes("submit-confirm")) },
openLayer(name) { window.h5HistoryLayerManager.open(name) }, closeLayer(name,callback) { window.h5HistoryLayerManager.close(name,callback) }, close() { this.closeLayer("detail") },
/** row 提供详情主键和当前 WF 任务参数,action 为 view 或 approval。 */
open(row,action) {
const defaultTaskNames={delegation:"团长审核",preAudit:"校工会预审核",feedbackEvaluation:"反馈评价",unitReply:"承办单位答复",caseCheck:"提案委员会审查"}
const taskName=this.kind === "unitReply" ? (row.taskName || defaultTaskNames[this.kind]) : (row.curTaskName || row.taskName || defaultTaskNames[this.kind] || "")
this.$set(this,"bizId",row.id); this.$set(this,"action",action); this.$set(this,"formData",{proposalId:row.id,processTaskId:row.taskId || "",taskKey:row.taskKey || "",taskName:taskName,instanceId:row.instanceId || "",underTakeName:row.underTakeName || "",underTakeIsMaster:!!row.underTakeIsMaster,tf_opinion:"",opinion:""})
if (action === "approval" && this.kind === "unitReply") this.loadCandidates(row.taskId || "")
if (action === "approval" && this.kind === "caseCheck") { 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(row.id) }
this.openLayer("detail")
},
loadCandidates(taskId) { this.$axios.post("/flow/common/candidate",{taskId:taskId}).then((res) => { if (res.code === 0) { this.$set(this,"candidates",res.data.candidates || []); this.$set(this,"supportCandidate",res.data.support) } }) },
confirmFeedback(value) { this.$set(this.formData,"tf_feedback_name",value.text); this.$set(this.formData,"tf_feedback",value.value); this.closeLayer("feedback-picker") },
confirmResult(value) { this.$set(this.formData,"result",value); this.closeLayer("result-picker") },
confirmImplement(value) { this.$set(this.formData,"tf_implementStateName",value.text); this.$set(this.formData,"tf_implementState",value.value); this.closeLayer("implement-picker") },
confirmCandidate(value) { this.$set(this.formData,"tf_nextNodeOperatorName",value.text); this.$set(this.formData,"tf_nextNodeOperator",value.value); this.closeLayer("candidate-picker") },
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",[]) },
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.openLayer("reply-unit-picker") },
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) },
confirmReplyUnits() { this.$set(this.formData,"tf_secondReplyUnitIds",[...this.pendingReplyUnitIds]); this.closeLayer("reply-unit-picker") },
buildCaseCheckData() { const data={...this.formData}; const units=data.tf_caseCheckNeedSecondReply === 1 ? this.replyUnitOptions.filter((item) => data.tf_secondReplyUnitIds.indexOf(item.unitId)>-1) : []; const masters=units.filter((item) => item.isMaster); const slaves=units.filter((item) => !item.isMaster); data.tf_masterUnitIds=masters.map((item) => item.unitId); data.tf_masterUnitId=data.tf_masterUnitIds[0] || ""; data.tf_masterUnitNames=masters.map((item) => item.unitName); data.tf_masterUnitName=data.tf_masterUnitNames[0] || ""; data.tf_masterUnitNameStr=data.tf_masterUnitNames.join(","); data.tf_slaveUnitIds=slaves.map((item) => item.unitId); data.tf_slaveUnitNames=slaves.map((item) => item.unitName); data.tf_slaveUnitNameStr=data.tf_slaveUnitNames.join(","); return data },
prepareSubmit(value) { this.$refs.formRef.validate().then(() => { this.$set(this,"pendingSubmitType",value); this.openLayer("submit-confirm") }).catch(() => {}) },
/** 根据页面类型保留原接口和参数分支,成功后清理全部弹层历史并通知列表刷新。 */
submit() {
this.$set(this,"formLoading",true)
let request
if (this.kind === "suggestion") request=this.$axios.post("/platform/proposal/suggestion/suggest",this.formData)
else if (this.kind === "schoolLeaderApproval") { const data={...this.formData,tf_approval:this.pendingSubmitType}; delete data.taskKey; delete data.taskName; request=this.$axios.post("/platform/proposal/schoolLeaderApproval/executeTask",{data:JSON.stringify(data)}) }
else {
const data=this.kind === "caseCheck" ? {...this.buildCaseCheckData(),submitType:this.pendingSubmitType} : {...this.formData,submitType:this.pendingSubmitType}
if (this.kind === "preAudit") { data.taskName="startTask"; if (this.pendingSubmitType === 4) { data.tf_sourceTaskKey=this.formData.taskKey; data.tf_audit=false } }
if (this.kind === "unitReply") { delete data.underTakeIsMaster; delete data.underTakeName }
if (this.kind === "feedbackEvaluation" && data.tf_feedback === "DISSATISFIED") request=this.$axios.post("/platform/proposal/feedbackEvaluation/caseInfo",{instId:data.instanceId}).then((res) => { if (res.code !== 0) return Promise.reject(new Error(res.msg || "查询立案信息失败")); data.tf_masterUnitId=res.data.tf_masterUnitId; return this.$axios.post("/flow/common/executeTask",{data:JSON.stringify(data)}) })
else request=this.$axios.post("/flow/common/executeTask",{data:JSON.stringify(data)})
}
request.then((res) => { if (res && res.code === 0) { this.$toast.success(res.msg); window.h5HistoryLayerManager.closeAll(() => this.$emit("success")) } }).catch((error) => { if (error && error.message && error.message !== "cancel") this.$toast.fail(error.message) }).finally(() => { this.$set(this,"formLoading",false) })
}
},
created() { window.h5HistoryLayerManager.ensureRegistered(this.pageKey); this.$set(this,"unsubscribeHistoryLayer",window.h5HistoryLayerManager.subscribe((stack) => this.syncLayers(stack))) },
beforeDestroy() { if (this.unsubscribeHistoryLayer) this.unsubscribeHistoryLayer(); if (window.h5HistoryLayerManager.pageKey === this.pageKey) window.h5HistoryLayerManager.unregister(this.pageKey) }
}
@@ -1,8 +1,11 @@
<!--#
layout("/layouts/platform_h5.html"){
#-->
<style>
<!--#include("../../common/info.css"){}#-->
</style>
<div id="app" v-cloak>
<van-nav-bar @click-left="historyBack" left-arrow left-text="返回" title="提案查询" placeholder fixed></van-nav-bar>
<van-nav-bar @click-left="handlePageBack" left-arrow left-text="返回" title="提案查询" placeholder fixed></van-nav-bar>
<van-sticky offset-top="46px">
<van-search
v-model="pageForm.name"
@@ -49,7 +52,9 @@ layout("/layouts/platform_h5.html"){
<van-cell title="代表团" :value="row.delegationName"></van-cell>
<van-cell title="当前节点" :value="row.processInstanceNodeName"></van-cell>
<van-cell class="table-card-footer">
<van-button size="small" @click="openView(row)">查看</van-button>
<van-button size="small" @click="openView(row)">
<van-icon class="proposal-action-icon" name="eye-o"></van-icon>查看
</van-button>
</van-cell>
</div>
</div>
@@ -57,11 +62,26 @@ 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 v-model="detailVisible" class="proposal-full-popup" position="right"
:close-on-click-overlay="false" safe-area-inset-bottom>
<div class="proposal-full-popup__page">
<van-nav-bar title="提案信息" left-arrow left-text="返回" fixed
@click-left="closeHistoryLayer('detail')"></van-nav-bar>
<div class="proposal-full-popup__scroll">
<proposal-info v-if="detailVisible && detailBizId" :biz-id="detailBizId" :visible="detailVisible"></proposal-info>
</div>
</div>
</van-popup>
</div>
<script nonce="${cspNonce!}">
<!--#include("../../common/info.js"){}#-->
const vue = new Vue({
el: "#app",
store,
components: {
"proposal-info": PROPOSAL_INFO
},
data() {
return {
pageForm: {
@@ -89,11 +109,34 @@ layout("/layouts/platform_h5.html"){
unitOptions: [],
moreOptions: {},
id: "",
formValidSuccess: false
formValidSuccess: false,
detailVisible: false,
detailBizId: "",
historyLayerPageKey: "proposal-query-comprehensive-h5"
}
},
methods: {
historyBack,
registerHistoryLayers() {
if (!window.h5HistoryLayerManager) return
window.h5HistoryLayerManager.register(this.historyLayerPageKey, (stack) => {
this.$set(this, "detailVisible", stack.includes("detail"))
})
},
openHistoryLayer(layerName) {
if (window.h5HistoryLayerManager && window.h5HistoryLayerManager.open(layerName)) return
if (layerName === "detail") this.$set(this, "detailVisible", true)
},
closeHistoryLayer(layerName) {
if (window.h5HistoryLayerManager && window.h5HistoryLayerManager.close(layerName)) return
if (layerName === "detail") this.$set(this, "detailVisible", false)
},
handlePageBack() {
if (window.h5HistoryLayerManager && window.h5HistoryLayerManager.stack.length) {
window.h5HistoryLayerManager.close()
return
}
historyBack()
},
sessionChange(val) {
this.pageForm.delegationId = null
if (this.pageForm.sessionId) {
@@ -148,7 +191,8 @@ layout("/layouts/platform_h5.html"){
},
openView(row) {
this.$pjaxReplace("/platform/h5/proposal/query/comprehensive/info?bizId=" + encodeURIComponent(row.id))
this.$set(this, "detailBizId", row.id)
this.openHistoryLayer("detail")
},
pageData() {
@@ -176,8 +220,14 @@ layout("/layouts/platform_h5.html"){
}
},
created() {
this.registerHistoryLayers()
this.pageData()
this.listSession()
},
beforeDestroy() {
if (window.h5HistoryLayerManager) {
window.h5HistoryLayerManager.unregister(this.historyLayerPageKey)
}
}
})
</script>
@@ -1,35 +0,0 @@
<!--#
layout("/layouts/platform_h5.html"){
#-->
<style id="style-proposal-info-h5">
<!--#include("../../common/info.css"){}#-->
</style>
<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>
<!--#
}
#-->
@@ -229,6 +229,9 @@ layout("/layouts/platform_h5.html"){
}
}
</style>
<style id="style-proposal-task-popup-h5">
<!--#include("../../common/info.css"){}#-->
</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>
@@ -305,15 +308,15 @@ layout("/layouts/platform_h5.html"){
</template>
<template #actions="{index,row}">
<div class="action-btn" @click="onView(row,index)">
<i class="fa fa-eye"></i>
<van-icon class="proposal-action-icon" name="eye-o"></van-icon>
<span>查看</span>
</div>
<div class="action-btn" v-if="row.taskState === 10" @click="onApproval(row,index)">
<i class="fa fa-edit"></i>
<van-icon class="proposal-action-icon" name="edit"></van-icon>
<span>审核</span>
</div>
<div class="action-btn delete" v-if="row.canRevoke" @click="onRevoke(row)">
<i class="fa fa-reply"></i>
<van-icon class="proposal-action-icon" name="revoke"></van-icon>
<span>撤回</span>
</div>
</template>
@@ -321,11 +324,15 @@ layout("/layouts/platform_h5.html"){
</div>
</main>
</div>
<proposal-task-popup ref="taskPopup" kind="caseCheck" page-key="proposal-case-check-task" @success="doSearch"></proposal-task-popup>
</div>
<script nonce="${cspNonce!}">
<!--#include("../../common/info.js"){}#-->
<!--#include("../../common/task-popup.js"){}#-->
new Vue({
el: "#app",
store,
components: {"proposal-task-popup": PROPOSAL_TASK_POPUP},
data() {
return {
pageForm: {
@@ -345,16 +352,10 @@ layout("/layouts/platform_h5.html"){
{text: "未审核", value: "0"},
{text: "已审核", value: "1"}
],
pendingListCache: null
}
},
methods: {
onReady() {
if (this.pendingListCache) {
this.restoreListCache()
this.listSession(true)
return
}
this.listSession(false)
},
@@ -386,254 +387,11 @@ layout("/layouts/platform_h5.html"){
},
onView(row, index) {
this.saveListCache(row, index)
this.$pjaxReplace("/platform/proposal/case/check/info/h5?bizId=" + encodeURIComponent(row.id) + "&action=view")
this.$refs.taskPopup.open(row, "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)
})
this.$refs.taskPopup.open(row, "approval")
},
onRevoke(row) {
@@ -651,7 +409,6 @@ layout("/layouts/platform_h5.html"){
},
doSearch() {
this.clearListCache()
this.$nextTick(() => {
this.$set(this.pageForm, "pageNumber", 1)
this.$set(this.pageForm, "totalCount", 0)
@@ -660,8 +417,6 @@ layout("/layouts/platform_h5.html"){
}
},
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)
@@ -1,363 +0,0 @@
<!--#
layout("/layouts/platform_h5.html"){
#-->
<style id="style-proposal-info-h5">
<!--#include("../../common/info.css"){}#-->
</style>
<style id="style-case-check-info-h5">
.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>
<!--#
}
#-->
@@ -1,8 +1,11 @@
<!--#
layout("/layouts/platform_h5.html"){
#-->
<style>
<!--#include("../../common/info.css"){}#-->
</style>
<div id="app" v-cloak>
<van-nav-bar @click-left="historyBack" left-arrow left-text="返回" title="委员会成员意见" placeholder
<van-nav-bar @click-left="handlePageBack" left-arrow left-text="返回" title="委员会成员意见" placeholder
fixed></van-nav-bar>
<van-sticky offset-top="46px">
<van-search
@@ -41,21 +44,81 @@ layout("/layouts/platform_h5.html"){
</template>
<template #actions="{index,row}">
<div class="action-btn" @click="onView(row)">
<i class="fa fa-eye"></i>
<van-icon class="proposal-action-icon" name="eye-o"></van-icon>
<span>查看</span>
</div>
<div class="action-btn" v-if="!row.pcoId" @click="onApproval(row)">
<i class="fa fa-edit"></i>
<van-icon class="proposal-action-icon" name="edit"></van-icon>
<span>审核</span>
</div>
</template>
</table-list>
<van-popup v-model="detailVisible" class="proposal-full-popup" position="right"
:close-on-click-overlay="false" safe-area-inset-bottom>
<div class="proposal-full-popup__page">
<van-nav-bar :title="popupTitle" left-arrow left-text="返回" fixed
@click-left="closeHistoryLayer('detail')"></van-nav-bar>
<div class="proposal-full-popup__scroll">
<proposal-info v-if="detailVisible && detailBizId" :biz-id="detailBizId" :visible="detailVisible">
<div v-if="isApproval">
<van-form ref="formRef">
<van-field v-model="formData.caseFilingResultName" name="caseFilingResultName"
label="立案结果" placeholder="请选择立案结果" readonly required is-link
:rules="[{ required: true, message: '请选择立案结果' }]"
@click="openHistoryLayer('result-picker')"></van-field>
<template v-if="formData.caseFilingResult === 'CONFIRM_FILING'">
<van-field v-model="formData.caseFilingTypeName" name="caseFilingTypeName"
label="立案类型" placeholder="请选择立案类型" readonly required is-link
:rules="[{ required: true, message: '请选择立案类型' }]"
@click="openHistoryLayer('type-picker')"></van-field>
</template>
<section class="proposal-view-card">
<h2 class="proposal-view-card__title">
审批意见
<span class="proposal-audit-card__required">*</span>
</h2>
<van-field v-model="formData.opinion" class="proposal-audit-field" name="opinion"
type="textarea" rows="6" label="" placeholder="请输入审批意见"
required maxlength="100" show-word-limit
:rules="[{ required: true, message: '请填写审批意见' }]"></van-field>
</section>
</van-form>
<div class="proposal-popup-form-actions">
<van-button block :disabled="formLoading" @click="closeHistoryLayer('detail')">取消</van-button>
<van-button type="primary" block :loading="formLoading" @click="submitApproval">提交</van-button>
</div>
</div>
</proposal-info>
</div>
</div>
</van-popup>
<van-popup v-model="showCaseFilingResultPicker" position="bottom" :close-on-click-overlay="false">
<van-picker show-toolbar
:columns="dict.type.PROPOSAL_CASE_FILING_RESULT.map(item => ({text: item.label, value: item.code}))"
@confirm="onCaseFilingResultConfirm"
@cancel="closeHistoryLayer('result-picker')"></van-picker>
</van-popup>
<van-popup v-model="showCaseFilingTypePicker" position="bottom" :close-on-click-overlay="false">
<van-picker show-toolbar
:columns="dict.type.PROPOSAL_CASE_FILING_TYPE.map(item => ({text: item.label, value: item.code}))"
@confirm="onCaseFilingTypeConfirm"
@cancel="closeHistoryLayer('type-picker')"></van-picker>
</van-popup>
<van-dialog :value="submitConfirmVisible" title="提示" message="您确定要提交吗?" show-cancel-button
@confirm="confirmApproval" @cancel="closeHistoryLayer('submit-confirm')"></van-dialog>
</div>
<script nonce="${cspNonce!}">
<!--#include("../../common/info.js"){}#-->
const vue = new Vue({
el: "#app",
store,
dicts: ["PROPOSAL_CASE_FILING_RESULT", "PROPOSAL_CASE_FILING_TYPE"],
components: {
"proposal-info": PROPOSAL_INFO
},
data() {
return {
pageForm: {
@@ -68,10 +131,58 @@ layout("/layouts/platform_h5.html"){
approval: false
},
sessionOptions: []
sessionOptions: [],
detailVisible: false,
detailBizId: "",
popupAction: "view",
formData: {},
formLoading: false,
showCaseFilingResultPicker: false,
showCaseFilingTypePicker: false,
submitConfirmVisible: false,
historyLayerPageKey: "proposal-commissioner-h5"
}
},
computed: {
isApproval() {
return this.popupAction === "approval"
},
popupTitle() {
return this.isApproval ? "委员会成员意见" : "提案详情"
}
},
methods: {
registerHistoryLayers() {
if (!window.h5HistoryLayerManager) return
window.h5HistoryLayerManager.register(this.historyLayerPageKey, (stack) => {
this.$set(this, "detailVisible", stack.includes("detail"))
this.$set(this, "showCaseFilingResultPicker", stack.includes("result-picker"))
this.$set(this, "showCaseFilingTypePicker", stack.includes("type-picker"))
this.$set(this, "submitConfirmVisible", stack.includes("submit-confirm"))
})
},
openHistoryLayer(layerName) {
if (window.h5HistoryLayerManager && window.h5HistoryLayerManager.open(layerName)) return
this.setHistoryLayerVisible(layerName, true)
},
closeHistoryLayer(layerName, afterClose = null) {
if (window.h5HistoryLayerManager && window.h5HistoryLayerManager.close(layerName, afterClose)) return
this.setHistoryLayerVisible(layerName, false)
if (typeof afterClose === "function") afterClose()
},
setHistoryLayerVisible(layerName, visible) {
if (layerName === "detail") this.$set(this, "detailVisible", visible)
if (layerName === "result-picker") this.$set(this, "showCaseFilingResultPicker", visible)
if (layerName === "type-picker") this.$set(this, "showCaseFilingTypePicker", visible)
if (layerName === "submit-confirm") this.$set(this, "submitConfirmVisible", visible)
},
handlePageBack() {
if (window.h5HistoryLayerManager && window.h5HistoryLayerManager.stack.length) {
window.h5HistoryLayerManager.close()
return
}
this.historyBack()
},
onReady() {
this.listSession()
},
@@ -93,11 +204,49 @@ layout("/layouts/platform_h5.html"){
},
onView(row) {
this.$pjaxReplace("/platform/proposal/commissioner/info/h5?bizId=" + encodeURIComponent(row.id) + "&action=view")
this.$set(this, "detailBizId", row.id)
this.$set(this, "popupAction", "view")
this.$set(this, "formData", {})
this.openHistoryLayer("detail")
},
onApproval(row) {
this.$pjaxReplace("/platform/proposal/commissioner/info/h5?bizId=" + encodeURIComponent(row.id) + "&action=approval")
this.$set(this, "detailBizId", row.id)
this.$set(this, "popupAction", "approval")
this.$set(this, "formData", {
proposalId: row.id,
caseFilingResult: null,
caseFilingType: null,
opinion: null
})
this.openHistoryLayer("detail")
},
onCaseFilingResultConfirm(value) {
this.$set(this.formData, "caseFilingResultName", value.text)
this.$set(this.formData, "caseFilingResult", value.value)
this.closeHistoryLayer("result-picker")
},
onCaseFilingTypeConfirm(value) {
this.$set(this.formData, "caseFilingTypeName", value.text)
this.$set(this.formData, "caseFilingType", value.value)
this.closeHistoryLayer("type-picker")
},
submitApproval() {
this.$refs.formRef.validate().then(() => this.openHistoryLayer("submit-confirm")).catch(() => {})
},
confirmApproval() {
this.closeHistoryLayer("submit-confirm", () => this.doSubmitApproval())
},
doSubmitApproval() {
this.$set(this, "formLoading", true)
this.$axios.post("/platform/proposal/commissioner/doApproval", this.formData)
.then((res) => {
if (res && res.code === 0) {
this.$toast.success(res.msg)
this.closeHistoryLayer("detail", () => this.doSearch())
}
})
.finally(() => this.$set(this, "formLoading", false))
},
doSearch() {
@@ -107,6 +256,12 @@ layout("/layouts/platform_h5.html"){
this.$refs.tableListRef.doSearch()
})
},
},
created() {
this.registerHistoryLayers()
},
beforeDestroy() {
if (window.h5HistoryLayerManager) window.h5HistoryLayerManager.unregister(this.historyLayerPageKey)
}
})
</script>
@@ -1,152 +0,0 @@
<!--#
layout("/layouts/platform_h5.html"){
#-->
<style id="style-proposal-info-h5">
<!--#include("../../common/info.css"){}#-->
</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">提案委员会成员意见</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>
<!--#
}
#-->
@@ -229,6 +229,9 @@ layout("/layouts/platform_h5.html"){
}
}
</style>
<style id="style-proposal-task-popup-h5">
<!--#include("../../common/info.css"){}#-->
</style>
<div id="app" v-cloak>
<div class="proposal-delegation-page">
<van-nav-bar @click-left="historyBack" left-arrow left-text="返回" title="团长审核" placeholder fixed></van-nav-bar>
@@ -303,15 +306,15 @@ layout("/layouts/platform_h5.html"){
</template>
<template #actions="{index,row}">
<div class="action-btn" @click="onView(row,index)">
<i class="fa fa-eye"></i>
<van-icon class="proposal-action-icon" name="eye-o"></van-icon>
<span>查看</span>
</div>
<div class="action-btn" v-if="row.taskState === 10" @click="onApproval(row,index)">
<i class="fa fa-edit"></i>
<van-icon class="proposal-action-icon" name="edit"></van-icon>
<span>审核</span>
</div>
<div class="action-btn delete" v-if="row.canRevoke" @click="onRevoke(row)">
<i class="fa fa-reply"></i>
<van-icon class="proposal-action-icon" name="revoke"></van-icon>
<span>撤回</span>
</div>
</template>
@@ -319,12 +322,15 @@ layout("/layouts/platform_h5.html"){
</div>
</main>
</div>
<proposal-task-popup ref="taskPopup" kind="delegation" page-key="proposal-delegation-task" @success="doSearch"></proposal-task-popup>
</div>
<script nonce="${cspNonce!}">
<!--#include("../../common/info.js"){}#-->
<!--#include("../../common/task-popup.js"){}#-->
new Vue({
el: "#app",
store,
components: {"proposal-task-popup": PROPOSAL_TASK_POPUP},
data() {
return {
pageForm: {
@@ -345,16 +351,10 @@ layout("/layouts/platform_h5.html"){
{text: "未审核", value: "0"},
{text: "已审核", value: "1"}
],
pendingListCache: null
}
},
methods: {
onReady() {
if (this.pendingListCache) {
this.restoreListCache()
this.listSession(true)
return
}
this.listSession(false)
},
// 审核状态下拉框使用字符串值展示,查询接口使用布尔值区分已审核和未审核。
@@ -375,249 +375,11 @@ layout("/layouts/platform_h5.html"){
},
onView(row, index) {
this.saveListCache(row, index)
this.$pjaxReplace("/platform/proposal/delegation/info/h5?bizId=" + encodeURIComponent(row.id) + "&action=view")
this.$refs.taskPopup.open(row, "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/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()
}
},
// 用户主动查询时清除详情返回标识,避免重新进入页面后误用旧列表现场。
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 {
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)
})
this.$refs.taskPopup.open(row, "approval")
},
onRevoke(row){
@@ -635,17 +397,14 @@ layout("/layouts/platform_h5.html"){
},
doSearch() {
this.clearListCache()
this.$nextTick(() => {
this.pageForm.pageNumber = 1
this.pageForm.totalCount = 0
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)
@@ -1,122 +0,0 @@
<!--#
layout("/layouts/platform_h5.html"){
#-->
<style id="style-proposal-info-h5">
<!--#include("../../common/info.css"){}#-->
</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
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>
<!--#
}
#-->
@@ -229,6 +229,9 @@ layout("/layouts/platform_h5.html"){
}
}
</style>
<style id="style-proposal-task-popup-h5">
<!--#include("../../common/info.css"){}#-->
</style>
<div id="app" v-cloak>
<div class="proposal-feedback-evaluation-page">
<van-nav-bar @click-left="historyBack" left-arrow left-text="返回" title="反馈评价" placeholder fixed></van-nav-bar>
@@ -303,15 +306,15 @@ layout("/layouts/platform_h5.html"){
</template>
<template #actions="{index,row}">
<div class="action-btn" @click="onView(row,index)">
<i class="fa fa-eye"></i>
<van-icon class="proposal-action-icon" name="eye-o"></van-icon>
<span>查看</span>
</div>
<div class="action-btn" v-if="row.taskState === 10" @click="onApproval(row,index)">
<i class="fa fa-edit"></i>
<van-icon class="proposal-action-icon" name="edit"></van-icon>
<span>评价</span>
</div>
<div class="action-btn delete" v-if="row.canRevoke" @click="onRevoke(row)">
<i class="fa fa-reply"></i>
<van-icon class="proposal-action-icon" name="revoke"></van-icon>
<span>撤回</span>
</div>
</template>
@@ -320,11 +323,15 @@ layout("/layouts/platform_h5.html"){
</main>
</div>
<proposal-task-popup ref="taskPopup" kind="feedbackEvaluation" page-key="proposal-feedback-task" @success="doSearch"></proposal-task-popup>
</div>
<script nonce="${cspNonce!}">
<!--#include("../../common/info.js"){}#-->
<!--#include("../../common/task-popup.js"){}#-->
const vue = new Vue({
el: "#app",
store,
components: {"proposal-task-popup": PROPOSAL_TASK_POPUP},
data() {
return {
pageForm: {
@@ -344,16 +351,10 @@ layout("/layouts/platform_h5.html"){
{text: "未评价", value: "0"},
{text: "已评价", value: "1"}
],
pendingListCache: null
}
},
methods: {
onReady() {
if (this.pendingListCache) {
this.restoreListCache()
this.listSession(true)
return
}
this.listSession(false)
},
@@ -378,255 +379,11 @@ layout("/layouts/platform_h5.html"){
},
onView(row, index) {
this.saveListCache(row, index)
this.$pjaxReplace("/platform/proposal/feedbackEvaluation/info/h5?bizId=" + encodeURIComponent(row.id) + "&action=view")
this.$refs.taskPopup.open(row, "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 || "反馈评价")
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()
}
},
// 用户主动搜索、切换届次或评价状态时使旧返回缓存失效。
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 为已加载列表索引;任务和相邻锚点统一使用唯一 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 {
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/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)
})
this.$refs.taskPopup.open(row, "approval")
},
onRevoke(row) {
@@ -644,17 +401,14 @@ layout("/layouts/platform_h5.html"){
},
doSearch() {
this.clearListCache()
this.$nextTick(() => {
this.pageForm.pageNumber = 1
this.pageForm.totalCount = 0
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)
@@ -1,187 +0,0 @@
<!--#
layout("/layouts/platform_h5.html"){
#-->
<style id="style-proposal-info-h5">
<!--#include("../../common/info.css"){}#-->
</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
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>
<!--#
}
#-->
@@ -2,6 +2,8 @@
layout("/layouts/platform_h5.html"){
#-->
<style>
<!--#include("../../common/info.css"){}#-->
body {
background-color: #f3f7fd;
}
@@ -28,6 +30,51 @@ layout("/layouts/platform_h5.html"){
color: #1989fa;
}
.proposal-mine-popup {
width: 100%;
height: 100%;
overflow-y: auto;
background-color: #f3f7fd;
}
.proposal-mine-popup .van-nav-bar {
background-color: #fff;
}
.proposal-mine-popup .van-nav-bar .van-icon,
.proposal-mine-popup .van-nav-bar__text {
color: #1989fa;
}
/* 详情弹框复用项目全屏 Popup 的固定导航结构,内容区域单独滚动。 */
.proposal-detail-popup {
overflow: hidden;
}
.proposal-detail-popup-page {
display: flex;
height: 100%;
padding-top: 46px;
overflow: hidden;
box-sizing: border-box;
flex-direction: column;
}
.proposal-detail-popup-page .van-nav-bar {
z-index: 20;
}
.proposal-detail-scroll {
min-height: 0;
overflow-y: auto;
flex: 1;
-webkit-overflow-scrolling: touch;
}
.proposal-detail-scroll .proposal-view-main {
padding-top: 18px;
}
.proposal-mine-content {
padding: 0 12px;
}
@@ -257,6 +304,11 @@ layout("/layouts/platform_h5.html"){
background-color: #eaf4ff;
}
.proposal-mine-page .table-list-container .table-list-item .action-btn .van-icon {
margin-right: 6px;
font-size: 15px;
}
@media (max-width: 350px) {
.proposal-mine-content {
padding-right: 10px;
@@ -271,7 +323,7 @@ layout("/layouts/platform_h5.html"){
</style>
<div id="app" v-cloak>
<div class="proposal-mine-page">
<van-nav-bar @click-left="historyBack" left-arrow left-text="返回" title="我的提案" placeholder fixed></van-nav-bar>
<van-nav-bar @click-left="handlePageBack" left-arrow left-text="返回" title="我的提案" placeholder fixed></van-nav-bar>
<van-sticky :offset-top="46">
<div class="proposal-mine-sticky">
@@ -338,27 +390,27 @@ layout("/layouts/platform_h5.html"){
</template>
<template #actions="{index,row}">
<div class="action-btn" @click="onView(row)">
<i class="fa fa-eye"></i>
<van-icon name="eye-o"></van-icon>
<span>查看</span>
</div>
<div class="action-btn" v-if="row.taskKey === 'startTask' || !row.instanceId"
@click="onEdit(row)">
<i class="fa fa-edit"></i>
<van-icon class="proposal-action-icon" name="edit"></van-icon>
<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>
<van-icon class="proposal-action-icon" name="friends-o"></van-icon>
<span>邀请附议人</span>
</div>
<div class="action-btn delete" v-if="row.canRevoke" @click="onRevoke(row)">
<i class="fa fa-undo"></i>
<van-icon class="proposal-action-icon" name="revoke"></van-icon>
<span>撤回</span>
</div>
<div class="action-btn delete" v-if="row.taskKey === 'startTask' || !row.instanceId"
@click="onDelete(row)">
<i class="fa fa-trash"></i>
<van-icon class="proposal-action-icon" name="delete-o"></van-icon>
<span>删除</span>
</div>
</template>
@@ -366,18 +418,49 @@ layout("/layouts/platform_h5.html"){
</div>
</main>
<invite-seconder @refresh="doSearch" ref="inviteSeconderRef"></invite-seconder>
<van-popup
v-model="detailVisible"
class="proposal-mine-popup proposal-detail-popup"
position="right"
:close-on-click-overlay="false"
safe-area-inset-bottom>
<div class="proposal-detail-popup-page">
<van-nav-bar
title="提案详情"
left-arrow
left-text="返回"
fixed
@click-left="closeHistoryLayer('detail')">
</van-nav-bar>
<div class="proposal-detail-scroll">
<proposal-info v-if="detailVisible && detailBizId" :biz-id="detailBizId" :visible="detailVisible"></proposal-info>
</div>
</div>
</van-popup>
<invite-seconder
:visible="inviteVisible"
:confirm-visible="inviteConfirmVisible"
@close="closeHistoryLayer('invite')"
@open-confirm="openHistoryLayer('invite-confirm')"
@close-confirm="closeHistoryLayer('invite-confirm')"
@confirm-invite="confirmInvite"
@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: {
"proposal-info": PROPOSAL_INFO,
"invite-seconder": PROPOSAL_INVITE_SECONDER_COMPONENT
},
data() {
@@ -393,10 +476,61 @@ layout("/layouts/platform_h5.html"){
},
sessionOptions: [],
filterOpened: false
filterOpened: false,
detailVisible: false,
detailBizId: "",
inviteVisible: false,
inviteConfirmVisible: false,
historyLayerPageKey: "proposal-mine-h5"
}
},
methods: {
/**
* 注册“我的提案”页面弹层历史。
* pageKey 使用固定页面标识;回调参数 stack 为当前弹层名称数组,无返回值。
*/
registerHistoryLayers() {
if (!window.h5HistoryLayerManager) return
window.h5HistoryLayerManager.register(this.historyLayerPageKey, (stack) => {
this.syncHistoryLayerStack(stack)
})
},
// 根据浏览器历史栈同步详情、邀请附议和邀请确认弹层的显示状态。
syncHistoryLayerStack(stack) {
this.$set(this, "detailVisible", stack.includes("detail"))
this.$set(this, "inviteVisible", stack.includes("invite"))
this.$set(this, "inviteConfirmVisible", stack.includes("invite-confirm"))
},
// 打开弹层时同步增加当前地址的浏览器历史记录。
openHistoryLayer(layerName) {
if (window.h5HistoryLayerManager && window.h5HistoryLayerManager.open(layerName)) return
this.setHistoryLayerVisible(layerName, true)
},
// 主动关闭弹层时回退对应历史记录,afterClose 在历史状态同步后执行。
closeHistoryLayer(layerName, afterClose = null) {
if (window.h5HistoryLayerManager && window.h5HistoryLayerManager.close(layerName, afterClose)) return
this.setHistoryLayerVisible(layerName, false)
if (typeof afterClose === "function") afterClose()
},
setHistoryLayerVisible(layerName, visible) {
if (layerName === "detail") this.$set(this, "detailVisible", visible)
if (layerName === "invite") this.$set(this, "inviteVisible", visible)
if (layerName === "invite-confirm") this.$set(this, "inviteConfirmVisible", visible)
},
// 页面返回时优先关闭最上层弹框,没有弹框时再返回上一业务页面。
handlePageBack() {
if (window.h5HistoryLayerManager && window.h5HistoryLayerManager.stack.length) {
window.h5HistoryLayerManager.close()
return
}
this.historyBack()
},
listSession() {
this.$axios.post("/platform/proposal/common/listSession").then((res) => {
if (res.code === 0) {
@@ -413,7 +547,8 @@ layout("/layouts/platform_h5.html"){
},
onView(row) {
this.$pjaxReplace("/platform/proposal/mine/info/h5?bizId=" + encodeURIComponent(row.id))
this.$set(this, "detailBizId", row.id)
this.openHistoryLayer("detail")
},
onEdit(row) {
@@ -463,14 +598,30 @@ layout("/layouts/platform_h5.html"){
onInvite(row) {
this.$axios.post("/platform/proposal/mine/inviteCheck", {id: row.id}).then((res) => {
if (res.code === 0) {
this.$refs.inviteSeconderRef.onOpen(row)
this.$refs.inviteSeconderRef.prepare(row)
this.openHistoryLayer("invite")
}
})
},
// 邀请确认后先关闭确认层,再提交邀请,避免浏览器历史中残留已关闭的 Dialog。
confirmInvite() {
this.closeHistoryLayer("invite-confirm", () => {
this.$refs.inviteSeconderRef.submitInvite()
})
},
onReady() {
this.listSession()
}
},
created() {
this.registerHistoryLayers()
},
beforeDestroy() {
if (window.h5HistoryLayerManager) {
window.h5HistoryLayerManager.unregister(this.historyLayerPageKey)
}
}
})
</script>
@@ -1,36 +0,0 @@
<!--#
layout("/layouts/platform_h5.html"){
#-->
<style id="style-proposal-info-h5">
<!--#include("../../common/info.css"){}#-->
</style>
<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>
<!--#
}
#-->
@@ -1,62 +1,101 @@
const PROPOSAL_INVITE_SECONDER_COMPONENT = {
template: `
<van-action-sheet v-model="visible" title="邀请附议人" class="height100">
<van-search
v-model="pageForm.searchKeyword"
:show-action="false"
placeholder="请输入工号或者姓名查询"
@search="doSearch"
></van-search>
<van-tabs v-model="isInvite" @click="isInviteClick">
<van-tab title="已邀请" name="1"></van-tab>
<van-tab title="可邀请" name="0"></van-tab>
</van-tabs>
<van-checkbox-group v-model="tableSelection" v-if="tableData && tableData.length>0">
<van-cell-group >
<van-cell
v-for="(item, index) in tableData"
clickable
:key="item.loginName"
@click="tableRowToggle(index)"
>
<span slot="title">
{{item.userName}}
({{item.loginName}})
</span>
<span slot="label" class="font-size0 van-color-text-assist">
{{item.unitName}}
</span>
<van-checkbox
:name="item"
:ref="'checkboxes'+item.loginName"
slot="right-icon"
v-if="!pageForm.isInvite"></van-checkbox>
</van-cell>
</van-cell-group>
<div class="p10">
<van-pagination
v-model="pageForm.pageNumber"
:total-items="pageForm.totalCount"
:items-per-page="pageForm.pageSize"
@change="pageData"
></van-pagination>
<van-button @click="invite" type="primary" block :disabled="pageForm.isInvite" class="mt10">邀请</van-button>
</div>
</van-checkbox-group>
<!-- 空状态提示随“已邀请 / 可邀请”标签切换,便于用户判断当前无数据的具体范围。 -->
<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>
template: /*language=HTML*/ `
<van-popup
:value="visible"
class="proposal-mine-popup proposal-invite-popup"
position="right"
:close-on-click-overlay="false"
safe-area-inset-bottom>
<van-nav-bar
title="邀请附议人"
left-arrow
left-text="返回"
fixed
placeholder
@click-left="$emit('close')">
</van-nav-bar>
<van-search
v-model="pageForm.searchKeyword"
:show-action="false"
placeholder="请输入工号或者姓名查询"
@search="doSearch">
</van-search>
<van-tabs v-model="isInvite" @click="isInviteClick">
<van-tab title="已邀请" name="1"></van-tab>
<van-tab title="可邀请" name="0"></van-tab>
</van-tabs>
<van-checkbox-group v-model="tableSelection" v-if="tableData && tableData.length > 0">
<van-cell-group>
<van-cell
v-for="(item, index) in tableData"
clickable
:key="item.loginName"
@click="tableRowToggle(index)">
<span slot="title">
{{item.userName}}{{item.loginName}}
</span>
<span slot="label" class="font-size0 van-color-text-assist">
{{item.unitName}}
</span>
<van-checkbox
:name="item"
:ref="'checkboxes' + item.loginName"
slot="right-icon"
v-if="!pageForm.isInvite">
</van-checkbox>
</van-cell>
</van-cell-group>
<div class="p10">
<van-pagination
v-model="pageForm.pageNumber"
:total-items="pageForm.totalCount"
:items-per-page="pageForm.pageSize"
@change="pageData">
</van-pagination>
<van-button
@click="invite"
type="primary"
block
:disabled="pageForm.isInvite"
class="mt10">
邀请
</van-button>
</div>
</van-action-sheet>
`,
</van-checkbox-group>
<!-- 空状态提示随“已邀请 / 可邀请”标签切换,便于用户判断当前无数据的具体范围。 -->
<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-dialog
:value="confirmVisible"
title="提示"
:message="confirmMessage"
show-cancel-button
@confirm="$emit('confirm-invite')"
@cancel="$emit('close-confirm')">
</van-dialog>
</van-popup>
`,
props: {
visible: {
type: Boolean,
default: false
},
confirmVisible: {
type: Boolean,
default: false
}
},
data() {
return {
visible: false,
secondedNum: 0,
record: {},
tableData: [],
@@ -71,72 +110,86 @@ const PROPOSAL_INVITE_SECONDER_COMPONENT = {
},
config: {},
isInvite: "0",
tableSelection: []
tableSelection: [],
confirmMessage: ""
}
},
watch: {
visible(value) {
if (!value || !this.record.id) return
this.getConfig().then(() => {
this.pageData()
})
}
},
methods: {
async onOpen(record) {
this.visible = true
this.record = record
await this.getConfig()
this.pageData()
},
detail(id) {
/**
* 准备邀请附议弹框数据。
* record 为列表当前提案对象,至少包含 id 和 sessionId;该方法只缓存提案并重置查询条件,无返回值。
*/
prepare(record) {
this.$set(this, "record", record || {})
this.$set(this, "isInvite", "0")
this.$set(this, "tableSelection", [])
this.$set(this.pageForm, "pageNumber", 1)
this.$set(this.pageForm, "isInvite", false)
this.$set(this.pageForm, "searchKeyword", null)
},
// 校验勾选结果后打开确认 Dialog,实际提交在确认并回退 Dialog 历史层后执行。
invite() {
if (this.tableSelection.length === 0) {
this.$toast("请先选择附议人后点击邀请")
this.$toast("请先选择附议人后点击邀请")
return
}
const userNames = [...new Set(this.tableSelection.map((item) => item.userName))]
this.$set(this, "confirmMessage", "您确定要邀请" + userNames.join("、") + "代表附议吗?")
this.$emit("open-confirm")
},
const loginNames = [...new Set(this.tableSelection.map((s) => s.loginName))]
const userName = [...new Set(this.tableSelection.map((s) => s.userName))]
console.log(this.tableSelection)
this.$dialog.confirm({
title: '提示',
message: "您确定要邀请" + userName.join("、") + "代表附议吗?",
}).then(() => {
this.$axios.post("/platform/proposal/mine/inviteSeconder", {
proposalId: this.record.id,
seconders: JSON.stringify(loginNames)
}).then((res) => {
if (res.code === 0) {
this.pageData()
this.$toast.success(res.msg)
this.tableSelection = []
// this.visible = false
this.$emit("refresh", null)
}
})
}).catch(() => {
/**
* 提交邀请附议人。
* 参数 proposalId 为当前提案主键,seconders 为已勾选人员工号数组;接口返回 Result,code 为 0 表示邀请成功。
*/
submitInvite() {
const loginNames = [...new Set(this.tableSelection.map((item) => item.loginName))]
this.$axios.post("/platform/proposal/mine/inviteSeconder", {
proposalId: this.record.id,
seconders: JSON.stringify(loginNames)
}).then((res) => {
if (res.code === 0) {
this.pageData()
this.$toast.success(res.msg)
this.$set(this, "tableSelection", [])
this.$emit("refresh")
}
})
},
/**
* 查询邀请附议人列表。
* 参数包含当前提案 ID、届次 ID、邀请状态、关键词及分页条件;返回分页对象并更新列表和总数。
*/
pageData() {
this.$axios.post("/platform/proposal/mine/listSeconder", {
...this.pageForm,
sessionId: this.record.sessionId,
proposalId: this.record.id
}).then((res) => {
if (res.code === 0) {
this.$set(this, "tableData", res.data.list || [])
this.$set(this.pageForm, "totalCount", res.data.totalCount || 0)
this.echoCheckBox()
}
})
.then((res) => {
if (res.code === 0) {
this.tableData = res.data.list
this.pageForm.totalCount = res.data.totalCount
this.echoCheckBox()
}
})
},
// 翻页回显勾选
// 翻页回显当前已勾选的附议人。
echoCheckBox() {
this.$nextTick(() => {
this.tableData.forEach((item) => {
const checkboxRef = this.$refs['checkboxes' + item.loginName]
if (checkboxRef && this.tableSelection.map((s) => s.loginName).includes(item.loginName)) {
const checkboxRef = this.$refs["checkboxes" + item.loginName]
if (checkboxRef && this.tableSelection.map((selected) => selected.loginName).includes(item.loginName)) {
checkboxRef[0].toggle()
}
})
@@ -144,24 +197,29 @@ const PROPOSAL_INVITE_SECONDER_COMPONENT = {
},
doSearch() {
this.pageForm.pageNumber = 1
this.$set(this.pageForm, "pageNumber", 1)
this.pageData()
},
isInviteClick(index, title) {
this.pageForm.isInvite = index === "1"
isInviteClick(name) {
this.$set(this.pageForm, "isInvite", name === "1")
this.doSearch()
},
tableRowToggle(index) {
console.log(index)
if (this.pageForm.isInvite || index < 0) return
},
async getConfig() {
const {code, data} = await this.$axios.post("/platform/proposal/common/config")
if (code === 0) {
this.config = data
}
/**
* 查询提案附议配置。
* 接口无业务参数,返回 ProposalConfig 配置对象;Promise 用于确保配置完成后再查询人员列表。
*/
getConfig() {
return this.$axios.post("/platform/proposal/common/config").then((res) => {
if (res.code === 0) {
this.$set(this, "config", res.data || {})
}
})
}
}
}
@@ -212,6 +212,9 @@ layout("/layouts/platform_h5.html"){
}
}
</style>
<style id="style-proposal-task-popup-h5">
<!--#include("../../common/info.css"){}#-->
</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>
@@ -287,15 +290,15 @@ layout("/layouts/platform_h5.html"){
</template>
<template #actions="{index,row}">
<div class="action-btn" @click="onView(row,index)">
<i class="fa fa-eye"></i>
<van-icon class="proposal-action-icon" name="eye-o"></van-icon>
<span>查看</span>
</div>
<div class="action-btn" v-if="row.taskState === 10" @click="onApproval(row,index)">
<i class="fa fa-edit"></i>
<van-icon class="proposal-action-icon" name="edit"></van-icon>
<span>审核</span>
</div>
<div class="action-btn delete" v-if="row.canRevoke" @click="onRevoke(row)">
<i class="fa fa-reply"></i>
<van-icon class="proposal-action-icon" name="revoke"></van-icon>
<span>撤回</span>
</div>
</template>
@@ -303,11 +306,15 @@ layout("/layouts/platform_h5.html"){
</div>
</main>
</div>
<proposal-task-popup ref="taskPopup" kind="preAudit" page-key="proposal-pre-audit-task" @success="doSearch"></proposal-task-popup>
</div>
<script nonce="${cspNonce!}">
<!--#include("../../common/info.js"){}#-->
<!--#include("../../common/task-popup.js"){}#-->
new Vue({
el: "#app",
store,
components: {"proposal-task-popup": PROPOSAL_TASK_POPUP},
data() {
return {
pageForm: {
@@ -327,16 +334,10 @@ layout("/layouts/platform_h5.html"){
{text: "未审核", value: "0"},
{text: "已审核", value: "1"}
],
pendingListCache: null
}
},
methods: {
onReady() {
if (this.pendingListCache) {
this.restoreListCache()
this.listSession(true)
return
}
this.listSession(false)
},
@@ -368,250 +369,11 @@ layout("/layouts/platform_h5.html"){
},
onView(row, index) {
this.saveListCache(row, index)
this.$pjaxReplace("/platform/proposal/pre/audit/info/h5?bizId=" + encodeURIComponent(row.id) + "&action=view")
this.$refs.taskPopup.open(row, "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)
})
this.$refs.taskPopup.open(row, "approval")
},
onRevoke(row) {
@@ -629,17 +391,14 @@ layout("/layouts/platform_h5.html"){
},
doSearch() {
this.clearListCache()
this.$nextTick(() => {
this.pageForm.pageNumber = 1
this.pageForm.totalCount = 0
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)
@@ -1,143 +0,0 @@
<!--#
layout("/layouts/platform_h5.html"){
#-->
<style id="style-proposal-info-h5">
<!--#include("../../common/info.css"){}#-->
</style>
<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>
<!--#
}
#-->
@@ -1,131 +1,58 @@
<!--#
layout("/layouts/platform_h5.html"){
#-->
<style id="style-proposal-school-leader-h5">
<!--#include("../../common/info.css"){}#-->
</style>
<div id="app" v-cloak>
<van-nav-bar @click-left="historyBack" left-arrow left-text="返回" title="分管校领导审批" placeholder fixed></van-nav-bar>
<van-nav-bar @click-left="handlePageBack" 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-search v-model="pageForm.name" 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" @change="doSearch"></van-dropdown-item></van-dropdown-menu>
<van-tabs v-model="pageForm.approvalText" @change="approvalChange"><van-tab title="已审核" name="1"></van-tab><van-tab title="未审核" name="0"></van-tab></van-tabs>
</van-sticky>
<table-list api="/platform/proposal/schoolLeaderApproval/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>
<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>
<table-list api="/platform/proposal/schoolLeaderApproval/pageData" :page_form.sync="pageForm" @ready="onReady" ref="tableListRef" title="name">
<template v-slot="{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="{row}">
<div class="action-btn" @click="onView(row)"><van-icon class="proposal-action-icon" name="eye-o"></van-icon><span>查看</span></div>
<div class="action-btn" v-if="row.taskState === 10" @click="onApproval(row)"><van-icon class="proposal-action-icon" name="edit"></van-icon><span>审核</span></div>
<div class="action-btn delete" v-if="row.canRevoke" @click="onRevoke(row)"><van-icon class="proposal-action-icon" name="revoke"></van-icon><span>撤回</span></div>
</template>
</table-list>
<van-popup v-model="detailVisible" position="right" class="proposal-full-popup" :close-on-click-overlay="false" get-container="#app">
<div class="proposal-full-popup__page"><van-nav-bar @click-left="closeDetail" left-arrow left-text="返回" :title="popupTitle" placeholder fixed></van-nav-bar>
<div class="proposal-full-popup__scroll"><proposal-info v-if="detailBizId" :biz-id="detailBizId" :visible="detailVisible">
<div v-if="isApproval"><van-form ref="formRef"><section class="proposal-view-card"><h2 class="proposal-view-card__title">审批意见<span class="proposal-audit-card__required">*</span></h2><van-field v-model="formData.tf_opinion" class="proposal-audit-field" name="tf_opinion" type="textarea" rows="6" label="" placeholder="请输入审批意见" :rules="[{required:true,message:'请填写审批意见'}]" required maxlength="100" show-word-limit></van-field></section></van-form>
<div class="proposal-popup-form-actions"><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>
</div>
</van-popup>
<van-dialog :value="submitConfirmVisible" title="提示" message="您确定要提交吗?" show-cancel-button @confirm="confirmTaskAction" @cancel="closeSubmitConfirm"></van-dialog>
</div>
<script nonce="${cspNonce!}">
const vue = new Vue({
el: "#app",
store,
data() {
return {
pageForm: {
pageNumber: 1,
pageSize: 10,
totalCount: 0,
name: null,
sessionId: null,
approvalText: "0",
approval: false
},
sessionOptions: []
}
},
<!--#include("../../common/info.js"){}#-->
new Vue({
el: "#app", store, components: {"proposal-info": PROPOSAL_INFO},
data() { return {pageForm:{pageNumber:1,pageSize:10,totalCount:0,name:null,sessionId:null,approvalText:"0",approval:false},sessionOptions:[],detailVisible:false,detailBizId:"",action:"view",formData:{},formLoading:false,submitConfirmVisible:false,pendingSubmitType:"",historyLayerPageKey:"proposal-school-leader-approval"} },
computed: {isApproval() { return this.action === "approval" }, popupTitle() { return this.isApproval ? "分管校领导审批" : "提案详情" }},
methods: {
onReady() {
this.listSession()
},
listSession() {
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()
}
}
})
},
onView(row) {
this.$pjaxReplace("/platform/proposal/schoolLeaderApproval/info/h5?bizId=" + encodeURIComponent(row.id) + "&action=view")
},
onApproval(row) {
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){
this.$dialog.confirm({
title: "提示",
message: "您确定要撤回吗?"
}).then(() => {
this.$axios.post("/flow/common/revokeTask", {taskId: row.taskId}).then((res) => {
if (res.code === 0) {
this.$toast.success(res.msg)
this.doSearch()
}
})
})
},
doSearch() {
this.$nextTick(() => {
this.pageForm.pageNumber = 1
this.pageForm.totalCount = 0
this.$refs.tableListRef.doSearch()
})
},
}
registerHistoryLayers() { window.h5HistoryLayerManager.register(this.historyLayerPageKey, (stack) => { this.$set(this,"detailVisible",stack.includes("detail")); this.$set(this,"submitConfirmVisible",stack.includes("submit-confirm")) }) },
openHistoryLayer(name) { window.h5HistoryLayerManager.open(name) },
closeHistoryLayer(name,callback) { window.h5HistoryLayerManager.close(name,callback) },
handlePageBack() { if (window.h5HistoryLayerManager.stack.length) { window.h5HistoryLayerManager.close(); return } historyBack() },
closeDetail() { this.closeHistoryLayer("detail") }, closeSubmitConfirm() { this.closeHistoryLayer("submit-confirm") },
approvalChange(value) { this.$set(this.pageForm,"approval",value === "1"); this.doSearch() }, onReady() { this.listSession() },
listSession() { this.$axios.post("/platform/proposal/common/listSession").then((res) => { if (res.code !== 0) return; this.$set(this,"sessionOptions",[{text:"全部届次",value:null}].concat(res.data.map((v) => ({text:v.fullName,value:v.id})))); this.$set(this.pageForm,"sessionId",this.sessionOptions[0].value); this.doSearch() }) },
onView(row) { this.$set(this,"detailBizId",row.id); this.$set(this,"action","view"); this.$set(this,"formData",{}); this.openHistoryLayer("detail") },
onApproval(row) { this.$set(this,"detailBizId",row.id); this.$set(this,"action","approval"); this.$set(this,"formData",{processTaskId:row.taskId,taskKey:row.taskKey || "",taskName:row.taskName || "分管校领导审批",proposalId:row.id,tf_opinion:""}); this.openHistoryLayer("detail") },
/** val 为 back 或 agree,确认后提交分管校领导审批接口。 */
handleTaskAction(val) { this.$refs.formRef.validate().then(() => { this.$set(this,"pendingSubmitType",val); this.openHistoryLayer("submit-confirm") }).catch(() => {}) },
confirmTaskAction() { const submitData={...this.formData,tf_approval:this.pendingSubmitType}; delete submitData.taskKey; delete submitData.taskName; this.$set(this,"formLoading",true); this.$axios.post("/platform/proposal/schoolLeaderApproval/executeTask",{data:JSON.stringify(submitData)}).then((res) => { if (res && res.code === 0) { this.$toast.success(res.msg); window.h5HistoryLayerManager.closeAll(() => this.doSearch()) } }).finally(() => { this.$set(this,"formLoading",false) }) },
onRevoke(row) { this.$dialog.confirm({title:"提示",message:"您确定要撤回吗?"}).then(() => this.$axios.post("/flow/common/revokeTask",{taskId:row.taskId})).then((res) => { if (res && res.code === 0) { this.$toast.success(res.msg); this.doSearch() } }).catch(() => {}) },
doSearch() { this.$nextTick(() => { this.$set(this.pageForm,"pageNumber",1); this.$set(this.pageForm,"totalCount",0); this.$refs.tableListRef.doSearch() }) }
},
created() { this.registerHistoryLayers() }, beforeDestroy() { window.h5HistoryLayerManager.unregister(this.historyLayerPageKey) }
})
</script>
<!--#
@@ -1,107 +0,0 @@
<!--#
layout("/layouts/platform_h5.html"){
#-->
<style id="style-proposal-info-h5">
<!--#include("../../common/info.css"){}#-->
</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
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>
<!--#
}
#-->
@@ -2,6 +2,8 @@
layout("/layouts/platform_h5.html"){
#-->
<style>
<!--#include("../../common/info.css"){}#-->
body {
background-color: #f3f7fd;
}
@@ -28,6 +30,45 @@ layout("/layouts/platform_h5.html"){
color: #1989fa;
}
.proposal-seconded-popup {
width: 100%;
height: 100%;
overflow: hidden;
background-color: #f3f7fd;
}
.proposal-seconded-popup-page {
display: flex;
height: 100%;
padding-top: 46px;
overflow: hidden;
box-sizing: border-box;
flex-direction: column;
}
.proposal-seconded-popup-page .van-nav-bar {
z-index: 20;
background-color: #fff;
}
.proposal-seconded-popup-scroll {
min-height: 0;
overflow-y: auto;
flex: 1;
-webkit-overflow-scrolling: touch;
}
.proposal-seconded-popup-scroll .proposal-view-main {
padding-top: 18px;
}
.proposal-seconded-form-actions {
display: flex;
padding: 10px;
justify-content: space-between;
column-gap: 10px;
}
.proposal-seconded-content {
padding: 0 12px;
}
@@ -222,6 +263,11 @@ layout("/layouts/platform_h5.html"){
white-space: nowrap;
}
.proposal-seconded-page .table-list-container .table-list-item .action-btn .van-icon {
margin-right: 6px;
font-size: 15px;
}
@media (max-width: 350px) {
.proposal-seconded-content {
padding-right: 10px;
@@ -236,7 +282,7 @@ layout("/layouts/platform_h5.html"){
</style>
<div id="app" v-cloak>
<div class="proposal-seconded-page">
<van-nav-bar @click-left="historyBack" left-arrow left-text="返回" title="附议提案" placeholder fixed></van-nav-bar>
<van-nav-bar @click-left="handlePageBack" left-arrow left-text="返回" title="附议提案" placeholder fixed></van-nav-bar>
<van-sticky :offset-top="46">
<div class="proposal-seconded-sticky">
@@ -309,16 +355,15 @@ layout("/layouts/platform_h5.html"){
<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>
<div class="action-btn" @click="onView(row)">
<van-icon name="eye-o"></van-icon>
<span>查看</span>
</div>
<div class="action-btn" v-if="showSecondAction(row)" @click="onApproval(row,index)">
<i class="fa fa-edit"></i>
<div class="action-btn" v-if="showSecondAction(row)" @click="onApproval(row)">
<van-icon name="edit"></van-icon>
<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>
@@ -326,13 +371,75 @@ layout("/layouts/platform_h5.html"){
</div>
</main>
<van-popup
v-model="detailVisible"
class="proposal-seconded-popup"
position="right"
:close-on-click-overlay="false"
safe-area-inset-bottom>
<div class="proposal-seconded-popup-page">
<van-nav-bar
:title="popupTitle"
left-arrow
left-text="返回"
fixed
@click-left="closeHistoryLayer('detail')">
</van-nav-bar>
<div class="proposal-seconded-popup-scroll">
<proposal-info v-if="detailVisible && detailBizId" :biz-id="detailBizId" :visible="detailVisible">
<div v-if="isApproval">
<section class="proposal-view-card proposal-seconded-opinion-card">
<h2 class="proposal-view-card__title">
提案附议
<span class="proposal-audit-card__required">*</span>
</h2>
<van-form ref="formRef">
<van-field
v-model="formData.tf_opinion"
class="proposal-audit-field"
name="tf_opinion"
type="textarea"
rows="6"
label=""
placeholder="请输入附议意见"
:rules="[{ required: true, message: '请填写附议意见' }]"
required
maxlength="200"
show-word-limit>
</van-field>
</van-form>
</section>
<div class="proposal-seconded-form-actions">
<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>
</div>
</van-popup>
<van-dialog
:value="submitConfirmVisible"
title="提示"
message="您确定要提交吗?"
show-cancel-button
@confirm="confirmSubmitSeconded"
@cancel="closeHistoryLayer('submit-confirm')">
</van-dialog>
</div>
</div>
<script nonce="${cspNonce!}">
<!--#include("../../common/info.js"){}#-->
new Vue({
el: "#app",
store,
components: {
"proposal-info": PROPOSAL_INFO
},
data() {
return {
pageForm: {
@@ -355,10 +462,70 @@ layout("/layouts/platform_h5.html"){
{text: "未附议", value: "0"},
{text: "已附议", value: "1"}
],
pendingListCache: null
detailVisible: false,
detailBizId: "",
popupAction: "view",
curTaskCode: "",
formData: {},
formLoading: false,
submitConfirmVisible: false,
pendingSubmitAgree: true,
historyLayerPageKey: "proposal-seconded-h5"
}
},
computed: {
isApproval() {
return this.popupAction === "approval"
},
popupTitle() {
return this.isApproval ? "提案附议" : "提案详情"
}
},
methods: {
/**
* 注册附议提案页面弹层历史。
* pageKey 使用固定页面标识;回调参数 stack 为当前弹层名称数组,无返回值。
*/
registerHistoryLayers() {
if (!window.h5HistoryLayerManager) return
window.h5HistoryLayerManager.register(this.historyLayerPageKey, (stack) => {
this.syncHistoryLayerStack(stack)
})
},
// 根据浏览器历史栈同步详情 Popup 和提交确认 Dialog 的显示状态。
syncHistoryLayerStack(stack) {
this.$set(this, "detailVisible", stack.includes("detail"))
this.$set(this, "submitConfirmVisible", stack.includes("submit-confirm"))
},
// 打开弹层时同步增加当前地址的浏览器历史记录。
openHistoryLayer(layerName) {
if (window.h5HistoryLayerManager && window.h5HistoryLayerManager.open(layerName)) return
this.setHistoryLayerVisible(layerName, true)
},
// 主动关闭弹层时回退对应历史记录,afterClose 在历史状态同步后执行。
closeHistoryLayer(layerName, afterClose = null) {
if (window.h5HistoryLayerManager && window.h5HistoryLayerManager.close(layerName, afterClose)) return
this.setHistoryLayerVisible(layerName, false)
if (typeof afterClose === "function") afterClose()
},
setHistoryLayerVisible(layerName, visible) {
if (layerName === "detail") this.$set(this, "detailVisible", visible)
if (layerName === "submit-confirm") this.$set(this, "submitConfirmVisible", visible)
},
// 页面返回时优先关闭最上层弹框,没有弹框时再返回上一业务页面。
handlePageBack() {
if (window.h5HistoryLayerManager && window.h5HistoryLayerManager.stack.length) {
window.h5HistoryLayerManager.close()
return
}
this.historyBack()
},
// 附议按钮显示规则:
// 1. 当前任务进行中(taskState=10)时显示
// 2. 当前任务已废弃(taskState=99)时,仅 invite、second、delegation、committee 节点显示
@@ -366,11 +533,6 @@ layout("/layouts/platform_h5.html"){
return row.taskState === 10 || (row.taskState === 99 && ["invite", "second", "preAudit", "delegation", "committee"].includes(row.taskKey))
},
onReady() {
if (this.pendingListCache) {
this.restoreListCache()
this.listSession(true)
return
}
this.listSession(false)
},
// 附议状态下拉框使用字符串值展示,查询接口使用布尔值区分已附议和未附议。
@@ -391,273 +553,98 @@ layout("/layouts/platform_h5.html"){
})
},
onView(row, index) {
this.saveListCache(row, index)
this.$pjaxReplace("/platform/proposal/seconded/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("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()) || ""
onView(row) {
this.$set(this, "detailBizId", row.id)
this.$set(this, "popupAction", "view")
this.$set(this, "curTaskCode", "")
this.$set(this, "formData", {})
this.openHistoryLayer("detail")
},
/**
* 将本次列表缓存标识绑定到当前历史记录
* PJAX 返回时可能重复初始化列表,两个实例通过同一标识读取同一份快照,避免后一次初始化覆盖恢复结果
* 打开提案附议 Popup
* row 包含提案、任务和附议人信息;这些字段用于详情查询、任务定位、附议人校验和提交链路选择
*/
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()
}
onApproval(row) {
this.$set(this, "detailBizId", row.id)
this.$set(this, "popupAction", "approval")
this.$set(this, "curTaskCode", row.curTaskCode || "")
this.$set(this, "formData", {
processTaskId: row.taskId,
taskId: row.taskId,
proposalId: row.id,
taskActorUserId: row.taskActorUserId,
taskName: row.curTaskName || "提案附议",
tf_opinion: "同意该提案"
})
this.openHistoryLayer("detail")
},
/**
* 用户主动重新查询时清除详情返回标识,防止后续重新进入列表时误用旧筛选和旧锚点。
*/
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 {
sessionStorage.setItem(this.getListCacheKey(), JSON.stringify(cache))
this.bindListCacheToken(cacheToken)
} catch (error) {
sessionStorage.removeItem(this.getListCacheKey())
}
},
/**
* 读取十分钟内的列表缓存,并先恢复查询参数,保证 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])
// 表单校验通过后打开提交确认 Dialog,并记录本次附议结果。
submitSeconded(isAgree) {
this.$refs.formRef.validate()
.then(() => {
this.$set(this, "pendingSubmitAgree", isAgree)
this.openHistoryLayer("submit-confirm")
})
this.$set(this, "pendingListCache", cache)
} catch (error) {
sessionStorage.removeItem(this.getListCacheKey())
}
.catch(() => {})
},
// 确认提交时先关闭 Dialog 历史层,再调用附议接口。
confirmSubmitSeconded() {
this.closeHistoryLayer("submit-confirm", () => {
this.doSubmitSeconded()
})
},
/**
* 恢复列表并按业务结果选择定位锚点
* 未提交时定位原记录;附议成功且原筛选为未附议时移除原记录并定位下一条,下一条不存在则定位上一条
* 提交附议结果
* second 节点向 executeTask 提交完整 WF 数据,其他关联节点向 updateInfo 提交提案、任务、附议人和意见;接口返回 Result
*/
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
doSubmitSeconded() {
const isAgree = this.pendingSubmitAgree
this.$set(this, "formLoading", true)
const request = this.curTaskCode === "second"
? this.$axios.post("/platform/proposal/seconded/executeTask", {
data: JSON.stringify({
...this.formData,
submitType: isAgree ? 1 : 20
})
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)
})
: 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
})
request.then((res) => {
if (res && res.code === 0) {
this.$toast.success(res.msg)
this.closeHistoryLayer("detail", () => {
this.doSearch()
})
}
}).finally(() => {
this.$set(this, "formLoading", false)
})
},
doSearch() {
this.clearListCache()
this.$nextTick(() => {
this.pageForm.pageNumber = 1
this.pageForm.totalCount = 0
this.$set(this.pageForm, "pageNumber", 1)
this.$set(this.pageForm, "totalCount", 0)
this.$refs.tableListRef.doSearch()
})
},
},
created() {
this.prepareListCache()
this.registerHistoryLayers()
},
beforeDestroy() {
if (window.h5HistoryLayerManager) {
window.h5HistoryLayerManager.unregister(this.historyLayerPageKey)
}
}
})
</script>
@@ -1,136 +0,0 @@
<!--#
layout("/layouts/platform_h5.html"){
#-->
<style id="style-proposal-info-h5">
<!--#include("../../common/info.css"){}#-->
</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">提案附议</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>
<!--#
}
#-->
@@ -7,9 +7,12 @@ layout("/layouts/platform_h5.html"){
height: 26px;
}
</style>
<style id="style-proposal-task-popup-h5">
<!--#include("../../common/info.css"){}#-->
</style>
<div id="app">
<van-nav-bar title="承办单位意见" left-text="返回" left-arrow @click-left="historyBack" fixed placeholder></van-nav-bar>
<van-nav-bar title="承办单位意见" left-text="返回" left-arrow @click-left="handlePageBack" fixed placeholder></van-nav-bar>
<van-sticky offset-top="46px">
<van-search
@@ -43,28 +46,30 @@ layout("/layouts/platform_h5.html"){
</template>
<template #actions="{index,row}">
<div class="action-btn" @click="onView(row)">
<i class="fa fa-eye"></i>
<van-icon class="proposal-action-icon" name="eye-o"></van-icon>
<span>查看</span>
</div>
<div class="action-btn" @click="openSuggestion(row)" v-if="pageForm.approval === true">
<i class="fa fa-eye"></i>
<van-icon class="proposal-action-icon" name="eye-o"></van-icon>
<span>查看意见</span>
</div>
<div class="action-btn" @click="onAudit(row)">
<i class="fa fa-edit"></i>
<van-icon class="proposal-action-icon" name="edit"></van-icon>
<span>反馈意见</span>
</div>
</template>
</table-list>
<van-action-sheet v-model="visible" title="意见信息">
<div class="detail-container">
<van-popup v-model="visible" position="right" class="proposal-full-popup" :close-on-click-overlay="false" get-container="#app">
<div class="proposal-full-popup__page">
<van-nav-bar @click-left="closeSuggestion" left-arrow left-text="返回" title="意见信息" placeholder fixed></van-nav-bar>
<div class="proposal-full-popup__scroll detail-container">
<van-cell-group :key="index" v-for="item,index in suggestionList">
<template #title>
<div style="display: flex; justify-content: space-between; align-items: center">
<div>{{ '意见' + (index + 1) }}</div>
<div>
<van-button type="danger" @click="onDelete(item)" size="small">删除</van-button>
<van-button type="danger" @click="onDelete(item)" size="small"><van-icon name="delete-o"></van-icon> 删除</van-button>
</div>
</div>
</template>
@@ -76,14 +81,19 @@ layout("/layouts/platform_h5.html"){
<span v-html="item.opinion"></span>
</van-cell>
</van-cell-group>
</div>
</div>
</van-action-sheet>
</van-popup>
<proposal-task-popup ref="taskPopup" kind="suggestion" page-key="proposal-suggestion-task" @success="doSearch"></proposal-task-popup>
</div>
<script nonce="${cspNonce!}">
<!--#include("../../common/info.js"){}#-->
<!--#include("../../common/task-popup.js"){}#-->
new Vue({
el: "#app",
store,
components: {"proposal-task-popup": PROPOSAL_TASK_POPUP},
data() {
return {
pageForm: {
@@ -99,9 +109,13 @@ layout("/layouts/platform_h5.html"){
sessionOptions: [],
suggestionList: [],
visible: false,
historyLayerPageKey: "proposal-suggestion-list"
}
},
methods: {
registerHistoryLayers() { window.h5HistoryLayerManager.register(this.historyLayerPageKey, (stack) => this.$set(this, "visible", stack.includes("opinions"))) },
handlePageBack() { if (window.h5HistoryLayerManager.stack.length) { window.h5HistoryLayerManager.close(); return } historyBack() },
closeSuggestion() { window.h5HistoryLayerManager.close("opinions") },
onDelete(row) {
this.$dialog.confirm({
title: "提示",
@@ -111,7 +125,7 @@ layout("/layouts/platform_h5.html"){
.then(res => {
if(res.code === 0) {
this.$toast.success(res.msg)
this.visible = false
this.closeSuggestion()
this.doSearch()
} else {
this.$message.fail(res.msg)
@@ -124,12 +138,12 @@ layout("/layouts/platform_h5.html"){
proposalId: row.id,
}).then(res => {
if(res.code === 0) {
this.suggestionList = res.data
this.$set(this, "suggestionList", res.data)
if(this.suggestionList.length === 0) {
this.$toast('暂无意见数')
return
}
this.visible = true
window.h5HistoryLayerManager.open("opinions")
} else {
this.$toast.fail(res.msg)
}
@@ -155,19 +169,21 @@ layout("/layouts/platform_h5.html"){
})
},
onAudit(row) {
this.$pjaxReplace("/platform/proposal/suggestion/info/h5?bizId=" + encodeURIComponent(row.id) + "&action=approval")
this.$refs.taskPopup.open(row, "approval")
},
onView(row) {
this.$pjaxReplace("/platform/proposal/suggestion/info/h5?bizId=" + encodeURIComponent(row.id) + "&action=view")
this.$refs.taskPopup.open(row, "view")
},
doSearch() {
this.$nextTick(() => {
this.pageForm.pageNumber = 1
this.pageForm.totalCount = 0
this.$set(this.pageForm, "pageNumber", 1)
this.$set(this.pageForm, "totalCount", 0)
this.$refs.tableListRef.doSearch()
})
}
},
created() { this.registerHistoryLayers() },
beforeDestroy() { window.h5HistoryLayerManager.unregister(this.historyLayerPageKey) }
})
</script>
@@ -1,118 +0,0 @@
<!--#
layout("/layouts/platform_h5.html"){
#-->
<style id="style-proposal-info-h5">
<!--#include("../../common/info.css"){}#-->
</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">承办单位意见</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>
<!--#
}
#-->
@@ -229,6 +229,9 @@ layout("/layouts/platform_h5.html"){
}
}
</style>
<style id="style-proposal-task-popup-h5">
<!--#include("../../common/info.css"){}#-->
</style>
<div id="app" v-cloak>
<div class="proposal-unit-reply-page">
<van-nav-bar @click-left="historyBack" left-arrow left-text="返回" title="承办单位答复" placeholder fixed></van-nav-bar>
@@ -304,22 +307,22 @@ layout("/layouts/platform_h5.html"){
</template>
<template #actions="{index,row}">
<div class="action-btn" @click="onView(row,index)">
<i class="fa fa-eye"></i>
<van-icon class="proposal-action-icon" name="eye-o"></van-icon>
<span>查看</span>
</div>
<div class="action-btn" v-if="row.taskState === 10" @click="onApproval(row,index)">
<i class="fa fa-edit"></i>
<van-icon class="proposal-action-icon" name="edit"></van-icon>
<span>答复</span>
</div>
<div class="action-btn delete" v-if="row.canRevoke" @click="onRevoke(row)">
<i class="fa fa-reply"></i>
<van-icon class="proposal-action-icon" name="revoke"></van-icon>
<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>
<van-icon class="proposal-action-icon" name="exchange"></van-icon>
<span>转交</span>
</div>
</template>
@@ -328,10 +331,13 @@ layout("/layouts/platform_h5.html"){
</main>
</div>
<transfer ref="transferRef"></transfer>
<transfer ref="transferRef" @success="doSearch"></transfer>
<proposal-task-popup ref="taskPopup" kind="unitReply" page-key="proposal-unit-reply-task" @success="doSearch"></proposal-task-popup>
</div>
<script nonce="${cspNonce!}">
<!--#include("transfer.js"){}#-->
<!--#include("../../common/info.js"){}#-->
<!--#include("../../common/task-popup.js"){}#-->
const vue = new Vue({
el: "#app",
@@ -355,19 +361,14 @@ layout("/layouts/platform_h5.html"){
{text: "未审核", value: "0"},
{text: "已审核", value: "1"}
],
pendingListCache: null
}
},
components: {
"transfer": transfer
"transfer": transfer,
"proposal-task-popup": PROPOSAL_TASK_POPUP
},
methods: {
onReady() {
if (this.pendingListCache) {
this.restoreListCache()
this.listSession(true)
return
}
this.listSession(false)
},
@@ -392,257 +393,11 @@ layout("/layouts/platform_h5.html"){
},
onView(row, index) {
this.saveListCache(row, index)
this.$pjaxReplace("/platform/proposal/unitReply/info/h5?bizId=" + encodeURIComponent(row.id) + "&action=view")
this.$refs.taskPopup.open(row, "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 || "")
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())
},
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()
}
},
// 用户主动搜索、切换届次或审核状态时使旧返回缓存失效。
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
}
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 {
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)
})
this.$refs.taskPopup.open(row, "approval")
},
onRevoke(row) {
@@ -665,17 +420,14 @@ layout("/layouts/platform_h5.html"){
},
doSearch() {
this.clearListCache()
this.$nextTick(() => {
this.pageForm.pageNumber = 1
this.pageForm.totalCount = 0
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)
@@ -1,188 +0,0 @@
<!--#
layout("/layouts/platform_h5.html"){
#-->
<style id="style-proposal-info-h5">
<!--#include("../../common/info.css"){}#-->
</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.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>
<!--#
}
#-->
@@ -1,147 +1,49 @@
const transfer = {
template: /*language=HTML*/ `
<van-action-sheet v-model="showTransferDialog" title="提案转办">
<div class="transfer-dialog">
<!-- 提案信息 -->
<div class="proposal-info-card">
<div class="info-item"><label>提案编号:</label><span>{{transferForm.code}}</span></div>
<div class="info-item"><label>提案名称</label><span>{{transferForm.name}}</span></div>
<div class="info-item"><label>代表团</label><span>{{transferForm.delegationName}}</span></div>
</div>
<!-- 用户选择 -->
<van-field v-model="searchKeyword" label="转交给" placeholder="搜索用户" required readonly @click="showUserList=true"></van-field>
<van-popup v-model="showUserList" position="bottom">
<van-search v-model="keyword" placeholder="搜索用户" @search="selectTransferUser(keyword)" @cancel="showUserList=false"></van-search>
<van-empty v-if="transferUserOptions.length === 0" class="empty-tip">请搜索需要转办的用户</van-empty>
<van-cell v-for="item in transferUserOptions" :key="item.id" :title="item.username" @click="selectUser(item)"></van-cell>
</van-popup>
<!-- 底部按钮 -->
<div class="button-group">
<van-button block plain @click="showTransferDialog = false" class="cancel-btn">取消</van-button>
<van-button block type="info" @click="handleTransfer" class="submit-btn">提交</van-button>
<van-popup v-model="showTransferDialog" position="right" class="proposal-full-popup" :close-on-click-overlay="false" get-container="#app">
<div class="proposal-full-popup__page">
<van-nav-bar @click-left="closeTransfer" left-arrow left-text="返回" title="提案转办" placeholder fixed></van-nav-bar>
<div class="proposal-full-popup__scroll transfer-dialog">
<div class="proposal-info-card">
<div class="info-item"><label>提案编号</label><span>{{transferForm.code}}</span></div>
<div class="info-item"><label>提案名称</label><span>{{transferForm.name}}</span></div>
<div class="info-item"><label>代表团:</label><span>{{transferForm.delegationName}}</span></div>
</div>
<van-field v-model="searchKeyword" label="转交给" placeholder="搜索用户" required readonly is-link @click="openUserList"></van-field>
</div>
<div class="button-group"><van-button block plain @click="closeTransfer">取消</van-button><van-button block type="info" @click="handleTransfer">提交</van-button></div>
</div>
</van-action-sheet>
<van-popup v-model="showUserList" position="bottom" :close-on-click-overlay="false">
<van-search v-model="keyword" placeholder="搜索用户" @search="selectTransferUser(keyword)" @cancel="closeUserList"></van-search>
<van-empty v-if="transferUserOptions.length === 0" class="empty-tip">请搜索需要转办的用户</van-empty>
<van-cell v-for="item in transferUserOptions" :key="item.id" :title="item.username" @click="selectUser(item)"></van-cell>
</van-popup>
<van-dialog :value="confirmVisible" title="提示" :message="confirmMessage" show-cancel-button @confirm="confirmTransfer" @cancel="closeConfirm"></van-dialog>
</van-popup>
`,
data() {
return {
showTransferDialog: false,
transferForm: {},
transferUserOptions: [],
searchKeyword: '',
showUserList: false,
keyword: ''
return {showTransferDialog:false,transferForm:{},transferUserOptions:[],searchKeyword:"",showUserList:false,keyword:"",confirmVisible:false,historyLayerPageKey:"proposal-unit-reply-transfer",unsubscribeHistoryLayer:null}
},
computed: {
confirmMessage() {
const item=this.transferUserOptions.find((option) => option.id === this.transferForm.userId)
return "您确定要将" + (this.transferForm.name || "") + "提案转交给" + (item ? item.username : "") + "办理吗?"
}
},
methods: {
openTransfer(row) {
this.showTransferDialog = true
this.transferForm = { taskId: row.taskId, name: row.name, code: row.code, delegationName: row.delegationName }
this.searchKeyword = ''
},
selectUser(item) {
this.transferForm.userId = item.id
this.searchKeyword = item.username
this.showUserList = false
},
selectTransferUser(keyword) {
this.$axios.post("/platform/proposal/unitReply/selectViceUser", {keyword}).then(res => {
if (res.code === 0) this.transferUserOptions = res.data
})
},
// 转办
handleTransfer() {
if (!this.transferForm.userId) {
this.$toast('请选择转交用户')
return
}
const transferUserName = this.transferUserOptions.find(item => item.id === this.transferForm.userId)?.username
const name = this.transferForm.name
this.$dialog.confirm({
title: '提示',
message: `您确定要将` + name + `提案转交给` + transferUserName + `办理吗?`,
confirmButtonText: '确定',
cancelButtonText: '取消'
}).then(() => {
this.$axios.post('/platform/proposal/unitReply/transfer', this.transferForm).then(res => {
if (res.code === 0) {
this.$toast.success(res.msg)
this.showTransferDialog = false
this.doSearch()
}
})
}).catch(() => {
// 取消操作
})
}
syncLayers(stack) { this.$set(this,"showTransferDialog",stack.includes("transfer")); this.$set(this,"showUserList",stack.includes("user-list")); this.$set(this,"confirmVisible",stack.includes("confirm")) },
openTransfer(row) { this.$set(this,"transferForm",{taskId:row.taskId,name:row.name,code:row.code,delegationName:row.delegationName}); this.$set(this,"searchKeyword",""); window.h5HistoryLayerManager.open("transfer") },
closeTransfer() { window.h5HistoryLayerManager.close("transfer") },
openUserList() { window.h5HistoryLayerManager.open("user-list") },
closeUserList() { window.h5HistoryLayerManager.close("user-list") },
closeConfirm() { window.h5HistoryLayerManager.close("confirm") },
selectUser(item) { this.$set(this.transferForm,"userId",item.id); this.$set(this,"searchKeyword",item.username); this.closeUserList() },
/** keyword 为姓名关键字,接口返回可转交用户数组。 */
selectTransferUser(keyword) { this.$axios.post("/platform/proposal/unitReply/selectViceUser",{keyword:keyword}).then((res) => { if (res.code === 0) this.$set(this,"transferUserOptions",res.data || []) }) },
handleTransfer() { if (!this.transferForm.userId) { this.$toast("请选择转交用户"); return } window.h5HistoryLayerManager.open("confirm") },
/** 提交 taskId 和 userId 完成转办,成功后清理弹层并通知父列表刷新。 */
confirmTransfer() { this.$axios.post("/platform/proposal/unitReply/transfer",this.transferForm).then((res) => { if (res.code === 0) { this.$toast.success(res.msg); window.h5HistoryLayerManager.closeAll(() => this.$emit("success")) } }) }
},
style: /*language=CSS*/ `
.transfer-dialog {
height: 100%;
background: #fff;
padding: 16px 16px 65px;
}
.proposal-info-card {
background: #f8f9fa;
border-radius: 8px;
padding: 16px;
margin-bottom: 16px;
}
.proposal-info-content {
display: flex;
flex-direction: column;
gap: 12px;
}
.info-item {
display: flex;
flex-direction: column;
gap: 4px;
}
.info-item label {
font-size: 12px;
color: #969799;
}
.info-item span {
font-size: 14px;
color: #323233;
font-weight: 500;
}
.button-group {
position: fixed;
bottom: 0;
left: 0;
right: 0;
padding: 16px;
background: #fff;
border-top: 1px solid #ebedf0;
z-index: 10;
display: flex;
justify-content: space-between;
column-gap: 10px;
padding: 10px
}
.cancel-btn,
.submit-btn {
}
.empty-tip {
text-align: center;
padding: 50px 16px;
color: #969799;
font-size: 14px;
}
`
created() { window.h5HistoryLayerManager.ensureRegistered(this.historyLayerPageKey); this.$set(this,"unsubscribeHistoryLayer",window.h5HistoryLayerManager.subscribe((stack) => this.syncLayers(stack))) },
beforeDestroy() { if (this.unsubscribeHistoryLayer) this.unsubscribeHistoryLayer(); if (window.h5HistoryLayerManager.pageKey === this.historyLayerPageKey) window.h5HistoryLayerManager.unregister(this.historyLayerPageKey) }
}
@@ -1,8 +1,11 @@
<!--#
layout("/layouts/platform_h5.html"){
#-->
<style>
<!--#include("../../common/info.css"){}#-->
</style>
<div id="app" v-cloak>
<van-nav-bar @click-left="historyBack" left-arrow left-text="返回" title="团长审核" placeholder fixed></van-nav-bar>
<van-nav-bar @click-left="handlePageBack" left-arrow left-text="返回" title="团长审核" placeholder fixed></van-nav-bar>
<van-sticky offset-top="46px">
<van-search
v-model="pageForm.name"
@@ -35,17 +38,32 @@ layout("/layouts/platform_h5.html"){
</template>
<template #actions="{index,row}">
<div class="action-btn" @click="onView(row)">
<i class="fa fa-eye"></i>
<van-icon class="proposal-action-icon" name="eye-o"></van-icon>
<span>查看</span>
</div>
</template>
</table-list>
<van-popup v-model="detailVisible" class="proposal-full-popup" position="right"
:close-on-click-overlay="false" safe-area-inset-bottom>
<div class="proposal-full-popup__page">
<van-nav-bar title="提案详情" left-arrow left-text="返回" fixed
@click-left="closeHistoryLayer('detail')"></van-nav-bar>
<div class="proposal-full-popup__scroll">
<proposal-info v-if="detailVisible && detailBizId" :biz-id="detailBizId" :visible="detailVisible"></proposal-info>
</div>
</div>
</van-popup>
</div>
<script nonce="${cspNonce!}">
<!--#include("../../common/info.js"){}#-->
const vue = new Vue({
el: "#app",
store,
components: {
"proposal-info": PROPOSAL_INFO
},
data() {
return {
pageForm: {
@@ -60,10 +78,34 @@ layout("/layouts/platform_h5.html"){
sessionOptions: [],
formData: {},
showApprovalForm: false
showApprovalForm: false,
detailVisible: false,
detailBizId: "",
historyLayerPageKey: "proposal-vice-delegation-h5"
}
},
methods: {
registerHistoryLayers() {
if (!window.h5HistoryLayerManager) return
window.h5HistoryLayerManager.register(this.historyLayerPageKey, (stack) => {
this.$set(this, "detailVisible", stack.includes("detail"))
})
},
openHistoryLayer(layerName) {
if (window.h5HistoryLayerManager && window.h5HistoryLayerManager.open(layerName)) return
if (layerName === "detail") this.$set(this, "detailVisible", true)
},
closeHistoryLayer(layerName) {
if (window.h5HistoryLayerManager && window.h5HistoryLayerManager.close(layerName)) return
if (layerName === "detail") this.$set(this, "detailVisible", false)
},
handlePageBack() {
if (window.h5HistoryLayerManager && window.h5HistoryLayerManager.stack.length) {
window.h5HistoryLayerManager.close()
return
}
this.historyBack()
},
onReady() {
this.listSession()
},
@@ -85,7 +127,8 @@ layout("/layouts/platform_h5.html"){
},
onView(row) {
this.$pjaxReplace("/platform/proposal/vice/delegation/info/h5?bizId=" + encodeURIComponent(row.id))
this.$set(this, "detailBizId", row.id)
this.openHistoryLayer("detail")
},
doSearch() {
@@ -95,6 +138,14 @@ layout("/layouts/platform_h5.html"){
this.$refs.tableListRef.doSearch()
})
},
},
created() {
this.registerHistoryLayers()
},
beforeDestroy() {
if (window.h5HistoryLayerManager) {
window.h5HistoryLayerManager.unregister(this.historyLayerPageKey)
}
}
})
</script>
@@ -1,36 +0,0 @@
<!--#
layout("/layouts/platform_h5.html"){
#-->
<style id="style-proposal-info-h5">
<!--#include("../../common/info.css"){}#-->
</style>
<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>
<!--#
}
#-->
@@ -353,7 +353,7 @@ layout("/layouts/platform_h5.html"){
</style>
<div id="app" v-cloak>
<van-nav-bar title="撰写提案" left-text="返回" left-arrow @click-left="historyBack" placeholder fixed z-index="999"
<van-nav-bar title="撰写提案" left-text="返回" left-arrow @click-left="handlePageBack" placeholder fixed z-index="999"
class="custom-nav"></van-nav-bar>
<div class="proposal-write-page">
@@ -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" round get-container="#app">
<van-popup v-model="showSessionPicker" position="bottom" round :close-on-click-overlay="false" 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" round get-container="#app">
<van-popup v-model="showCommitteePicker" position="bottom" round :close-on-click-overlay="false" 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" round get-container="#app">
<van-popup v-model="showSourcePicker" position="bottom" round :close-on-click-overlay="false" 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" round get-container="#app">
<van-popup v-model="showTypePicker" position="bottom" round :close-on-click-overlay="false" 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%' }" round get-container="#app">
<van-popup v-model="showSuggestionUnitPicker" position="bottom" :style="{ height: '70%' }" round :close-on-click-overlay="false" get-container="#app">
<div class="suggestion-unit-picker">
<div class="picker-toolbar">
<button type="button" class="picker-cancel" @click="onSuggestionUnitCancel">取消</button>
@@ -626,7 +626,9 @@ layout("/layouts/platform_h5.html"){
<van-action-sheet v-model="noticeDialogVisible"
title="提案撰写须知"
cancel-text="我已知晓"
close-on-click-action>
close-on-click-action
:close-on-click-overlay="false"
@cancel="setVisibleState('noticeDialogVisible', false)">
<div class="notice-dialog-content" v-html="proposalConfig.writeRemind"></div>
</van-action-sheet>
</div>
@@ -670,10 +672,18 @@ layout("/layouts/platform_h5.html"){
showSuggestionUnitPicker: false,
selectedSuggestionUnits: [], // 当前选中的承办单位
suggestionUnitSearchText: '', // 搜索文本
filteredSuggestionUnits: [] // 过滤后的承办单位列表
filteredSuggestionUnits: [], // 过滤后的承办单位列表
historyLayerPageKey: "proposal-write"
}
},
methods: {
handlePageBack() {
if (window.h5HistoryLayerManager.stack.length) {
window.h5HistoryLayerManager.close();
return;
}
historyBack();
},
/**
* 更新页面弹层显示状态,避免在模板事件中直接为 Vue 数据赋值。
* @param {String} stateName showSessionPicker、showSourcePicker 等弹层状态字段名。
@@ -681,7 +691,30 @@ layout("/layouts/platform_h5.html"){
* @return {void} 无返回值,仅更新当前页面状态。
*/
setVisibleState(stateName, visible) {
this.$set(this, stateName, visible);
const layerName = {
showSessionPicker: "session-picker",
showSourcePicker: "source-picker",
showTypePicker: "type-picker",
showCommitteePicker: "committee-picker",
showSuggestionUnitPicker: "suggestion-unit-picker",
noticeDialogVisible: "notice"
}[stateName];
if (visible) {
window.h5HistoryLayerManager.open(layerName);
return;
}
window.h5HistoryLayerManager.close(layerName);
},
registerHistoryLayers() {
window.h5HistoryLayerManager.register(this.historyLayerPageKey, (stack) => {
this.$set(this, "showSessionPicker", stack.includes("session-picker"));
this.$set(this, "showSourcePicker", stack.includes("source-picker"));
this.$set(this, "showTypePicker", stack.includes("type-picker"));
this.$set(this, "showCommitteePicker", stack.includes("committee-picker"));
this.$set(this, "showSuggestionUnitPicker", stack.includes("suggestion-unit-picker"));
this.$set(this, "noticeDialogVisible", stack.includes("notice"));
});
},
// 选择器确认事件
@@ -691,33 +724,33 @@ layout("/layouts/platform_h5.html"){
if (selectedOption) {
this.$set(this.formData, 'sessionName', selectedOption.fullName);
}
this.showSessionPicker = false;
this.setVisibleState("showSessionPicker", false);
this.meetingChange(value.value);
},
onSourceConfirm(value) {
this.formData.source = value.value;
this.$set(this.formData, 'sourceName', value.text);
this.showSourcePicker = false;
this.setVisibleState("showSourcePicker", false);
},
onTypeConfirm(value) {
this.$set(this.formData, 'typeId', value.value);
this.$set(this.formData, 'typeName', value.text);
this.showTypePicker = false;
this.setVisibleState("showTypePicker", false);
},
onSuggestionUnitConfirm() {
// 将选中的承办单位保存到表单数据中
this.formData.suggestUnits = [...this.selectedSuggestionUnits];
this.showSuggestionUnitPicker = false;
this.setVisibleState("showSuggestionUnitPicker", false);
},
onSuggestionUnitCancel() {
// 取消时恢复到之前的选择状态
this.selectedSuggestionUnits = [...this.formData.suggestUnits];
this.suggestionUnitSearchText = '';
this.showSuggestionUnitPicker = false;
this.setVisibleState("showSuggestionUnitPicker", false);
},
// 切换承办单位选择状态
@@ -750,7 +783,7 @@ layout("/layouts/platform_h5.html"){
onCommitteeConfirm(value) {
this.formData.committeeId = value.value;
this.$set(this.formData, 'committeeName', value.text);
this.showCommitteePicker = false;
this.setVisibleState("showCommitteePicker", false);
},
// 保存草稿:校验当前表单后保存 ProposalInfo,接口成功时返回提案数据并回到我的提案。
@@ -1049,7 +1082,7 @@ layout("/layouts/platform_h5.html"){
}
});
} else {
this.noticeDialogVisible = true;
this.setVisibleState("noticeDialogVisible", true);
this.listOpenSession(false);
// 设置用户信息
@@ -1063,10 +1096,14 @@ layout("/layouts/platform_h5.html"){
}
},
created() {
this.registerHistoryLayers();
this.init();
this.listSuggestUnit();
this.getProposalConfig();
},
beforeDestroy() {
window.h5HistoryLayerManager.unregister(this.historyLayerPageKey);
},
watch: {
// 监听suggestUnitOptions变化,初始化过滤列表
suggestUnitOptions: {