Merge remote-tracking branch 'origin/feature_周何峰' into release_20260831

This commit is contained in:
2026-09-02 09:20:52 +08:00
23 changed files with 1181 additions and 215 deletions
@@ -15,6 +15,10 @@ import java.lang.reflect.Method;
public class SaCheckLoginInterceptor implements MethodInterceptor {
@Override
public void filter(InterceptorChain chain) throws Throwable {
if (SaTokenAuthIgnoreUtil.isIgnore()) {
chain.doChain();
return;
}
Method method = chain.getCallingMethod();
SaCheckLogin at = method.getAnnotation(SaCheckLogin.class);
SaManager.getStpLogic(at.type()).checkByAnnotation(at);
@@ -15,6 +15,10 @@ import java.lang.reflect.Method;
public class SaCheckPermissionInterceptor implements MethodInterceptor {
@Override
public void filter(InterceptorChain chain) throws Throwable {
if (SaTokenAuthIgnoreUtil.isIgnore()) {
chain.doChain();
return;
}
Method method = chain.getCallingMethod();
SaCheckPermission at = method.getAnnotation(SaCheckPermission.class);
SaManager.getStpLogic(at.type()).checkByAnnotation(at);
@@ -15,6 +15,10 @@ import java.lang.reflect.Method;
public class SaCheckRoleInterceptor implements MethodInterceptor {
@Override
public void filter(InterceptorChain chain) throws Throwable {
if (SaTokenAuthIgnoreUtil.isIgnore()) {
chain.doChain();
return;
}
Method method = chain.getCallingMethod();
SaCheckRole at = method.getAnnotation(SaCheckRole.class);
SaManager.getStpLogic(at.type()).checkByAnnotation(at);
@@ -0,0 +1,60 @@
package com.budwk.app.web.commons.auth.satoken.aop;
import com.budwk.app.web.commons.base.Globals;
import org.nutz.lang.Lang;
import org.nutz.lang.Strings;
import org.nutz.mvc.Mvcs;
import javax.servlet.http.HttpServletRequest;
import java.util.Arrays;
import java.util.regex.Pattern;
/**
* Sa-Token 注解认证白名单工具。
* <p>
* 用于集中维护不需要登录、权限、角色校验即可访问的 URL,匹配规则与旧 Shiro 体系中的
* authIgnoreUrlArr 保持一致:数组中的每一项都按正则表达式与当前请求 URI 完整匹配。
*/
public class SaTokenAuthIgnoreUtil {
/**
* 不需要认证授权的 URL。
* <p>
* 参数说明:数组中的字符串为请求 URI 正则表达式,匹配的是去除项目 contextPath 后的路径。
* 返回值说明:通过 {@link #isIgnore()} 返回 booleantrue 表示当前请求跳过 Sa-Token 注解校验。
*/
private static final String[] AUTH_IGNORE_URL_ARR = Lang.array("/",
"/platform/activity/basic/scope/getScopeUser",
"/platform/fitnessWalk/stepManage/getActivityDateStep",
"/platform/fitnessWalk/stepManage/winningRecord",
"/platform/fitnessWalk/stepManage/updateStepMonth",
"/platform/fitnessWalk/stepRanking/getUserStepRanking",
"/platform/fitnessWalk/stepManage/getActivityQualifyProgressBar",
"/platform/fitnessWalk/stepWining/getUserStepWining",
"/platform/fitnessWalk/common/getIssueById",
"/platform/fitnessWalk/common/saveAnswer",
"/platform/fitnessWalk/stepWining/prizeOption",
"/platform/fitnessWalk/stepWining/prizeUsers",
"/platform/fitnessWalk/punchLottery/judgeWinningToPunch",
"/platform/fitnessWalk/punchLottery/getLotteryRecord",
"/platform/fitnessWalk/punchLottery/doExchange",
"/platform/fitnessWalk/punchLottery/getPunchWining",
"/platform/fitnessWalk/punchLottery/doReadLottery",
"/platform/fitnessWalk/activityRelation/isCurrentUserRelationActivityQualified",
"/platform/fitnessWalk/welfare/addWelfareList",
"/platform/fitnessWalk/weapp/.*");
/**
* 判断当前请求是否命中匿名访问白名单。
*
* @return boolean true 表示当前 URL 不需要执行登录、权限、角色注解校验;false 表示继续走原有 Sa-Token 校验逻辑
*/
public static boolean isIgnore() {
HttpServletRequest request = Mvcs.getReq();
if (request == null) {
return false;
}
String requestURI = Strings.sNull(request.getRequestURI()).replaceFirst("^" + Pattern.quote(Strings.sNull(Globals.AppBase)), "");
return Arrays.stream(AUTH_IGNORE_URL_ARR).anyMatch(v -> Pattern.compile(v).matcher(requestURI).matches());
}
}
@@ -162,27 +162,10 @@ public class ActivityWorksCollectionManageController {
@SaCheckPermission(value = {"activity.workscollection.manage", "activity.workscollection.new"}, mode = SaMode.OR)
@Aop(TransAop.READ_COMMITTED)
public Result update(@Param("data") @Valid Activity_works_collection worksCollection) {
String validateMessage = activityWorksCollectionService.validateReferencedTypesBeforeUpdate(worksCollection);
String validateMessage = activityWorksCollectionService.updatePreservingTypeIds(worksCollection);
if (StrUtil.isNotBlank(validateMessage)) {
return Result.error(validateMessage);
}
dao.update(worksCollection);
dao.updateLinks(worksCollection, "subjectTypes");
dao.insertLinks(worksCollection, "subjectTypes");
//清除页面已经删除的
dao.clear(Activity_works_subjectType.class,
Cnd.where(Activity_works_subjectType::getActivityId, "=", worksCollection.getId())
.andEX("id", "not in", worksCollection.getSubjectTypes().stream().map(Activity_works_subjectType::getId).toList()));
worksCollection.getSubjectTypes().forEach(item -> {
dao.updateLinks(item, "worksTypes");
dao.insertLinks(item, "worksTypes");
//清除页面已经删除的
List<String> workIds = item.getWorksTypes().stream().map(Activity_works_worksType::getId).toList();
dao.clear(Activity_works_worksType.class,
Cnd.where(Activity_works_worksType::getId, "not in ", workIds)
.and(Activity_works_worksType::getSubjectId,"=",item.getId()));
});
syncHomeActivity(worksCollection);
return Result.success();
}
@@ -35,6 +35,14 @@ public interface ActivityWorksCollectionService extends BaseService<Activity_wor
*/
void cancelTemplate(String id);
/**
* 更新作品征集活动及其主题、作品类型,并保留所有原有类型ID。
*
* @param worksCollection 前端提交的活动及其主题、作品类型配置
* @return 更新成功返回 {@code null};数据层级异常或删除了投稿引用类型时返回提示信息
*/
String updatePreservingTypeIds(Activity_works_collection worksCollection);
/**
* 校验编辑活动时删除的主题类型、作品类型是否已被投稿引用。
*
@@ -21,7 +21,9 @@ import org.nutz.ioc.aop.Aop;
import org.nutz.ioc.loader.annotation.IocBean;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
@@ -116,6 +118,136 @@ public class ActivityWorksCollectionServiceImpl extends BaseServiceImpl<Activity
Cnd.where("id", "=", id));
}
/**
* 活动编辑采用显式新增、更新、删除,避免通用关联保存重新生成已有类型ID。
* 原有主题和作品类型必须携带数据库ID;只有页面新增的类型允许生成新ID。
*/
@Override
@Aop(TransAop.READ_COMMITTED)
public String updatePreservingTypeIds(Activity_works_collection worksCollection) {
if (worksCollection == null || StrUtil.isBlank(worksCollection.getId())) {
return "活动数据不存在,请刷新页面后重试";
}
Activity_works_collection persistedActivity = dao.fetch(Activity_works_collection.class, worksCollection.getId());
if (persistedActivity == null) {
return "活动不存在或已被删除";
}
List<Activity_works_subjectType> submittedSubjectTypes = CollUtil.defaultIfEmpty(
worksCollection.getSubjectTypes(), Collections.emptyList());
List<Activity_works_subjectType> persistedSubjectTypes = dao.query(Activity_works_subjectType.class,
Cnd.where(Activity_works_subjectType::getActivityId, "=", worksCollection.getId()));
Map<String, Activity_works_subjectType> persistedSubjectMap = persistedSubjectTypes.stream()
.collect(Collectors.toMap(Activity_works_subjectType::getId, item -> item));
Set<String> submittedSubjectIds = new HashSet<>();
for (Activity_works_subjectType subjectType : submittedSubjectTypes) {
if (subjectType == null) {
return "主题类型数据不能为空";
}
if (StrUtil.isNotBlank(subjectType.getId())) {
if (!persistedSubjectMap.containsKey(subjectType.getId())) {
return "主题类型数据已发生变化,请刷新页面后重新编辑";
}
if (!submittedSubjectIds.add(subjectType.getId())) {
return "主题类型数据重复,请刷新页面后重新编辑";
}
}
List<Activity_works_worksType> submittedWorksTypes = CollUtil.defaultIfEmpty(
subjectType.getWorksTypes(), Collections.emptyList());
if (StrUtil.isBlank(subjectType.getId())) {
boolean containsPersistedWorksType = submittedWorksTypes.stream()
.anyMatch(worksType -> worksType != null && StrUtil.isNotBlank(worksType.getId()));
if (containsPersistedWorksType) {
return "新增主题不能关联原有作品类型,请刷新页面后重新编辑";
}
continue;
}
List<Activity_works_worksType> persistedWorksTypes = dao.query(Activity_works_worksType.class,
Cnd.where(Activity_works_worksType::getSubjectId, "=", subjectType.getId()));
Map<String, Activity_works_worksType> persistedWorksTypeMap = persistedWorksTypes.stream()
.collect(Collectors.toMap(Activity_works_worksType::getId, item -> item));
Set<String> submittedWorksIds = new HashSet<>();
for (Activity_works_worksType worksType : submittedWorksTypes) {
if (worksType == null) {
return "作品类型数据不能为空";
}
if (StrUtil.isNotBlank(worksType.getId()) && !persistedWorksTypeMap.containsKey(worksType.getId())) {
return "作品类型数据已发生变化,请刷新页面后重新编辑";
}
if (StrUtil.isNotBlank(worksType.getId()) && !submittedWorksIds.add(worksType.getId())) {
return "作品类型数据重复,请刷新页面后重新编辑";
}
}
}
String validateMessage = validateReferencedTypesBeforeUpdate(worksCollection);
if (StrUtil.isNotBlank(validateMessage)) {
return validateMessage;
}
dao.update(worksCollection);
for (Activity_works_subjectType subjectType : submittedSubjectTypes) {
subjectType.setActivityId(worksCollection.getId());
if (StrUtil.isBlank(subjectType.getId())) {
dao.insert(subjectType);
} else {
dao.update(subjectType);
}
submittedSubjectIds.add(subjectType.getId());
List<Activity_works_worksType> submittedWorksTypes = CollUtil.defaultIfEmpty(
subjectType.getWorksTypes(), Collections.emptyList());
Set<String> retainedWorksIds = new HashSet<>();
for (Activity_works_worksType worksType : submittedWorksTypes) {
worksType.setSubjectId(subjectType.getId());
if (StrUtil.isBlank(worksType.getId())) {
dao.insert(worksType);
} else {
dao.update(worksType);
}
retainedWorksIds.add(worksType.getId());
}
clearRemovedWorksTypes(subjectType.getId(), retainedWorksIds);
}
clearRemovedSubjectTypes(worksCollection.getId(), submittedSubjectIds);
return null;
}
/**
* 删除页面已移除且未被投稿引用的作品类型;空集合表示删除当前主题下全部原有类型。
*/
private void clearRemovedWorksTypes(String subjectId, Set<String> retainedWorksIds) {
Cnd cnd = Cnd.where(Activity_works_worksType::getSubjectId, "=", subjectId);
if (CollUtil.isNotEmpty(retainedWorksIds)) {
cnd.and(Activity_works_worksType::getId, "not in", retainedWorksIds);
}
dao.clear(Activity_works_worksType.class, cnd);
}
/**
* 删除页面已移除且未被投稿引用的主题类型;空集合表示删除当前活动全部原有主题。
*/
private void clearRemovedSubjectTypes(String activityId, Set<String> retainedSubjectIds) {
Cnd cnd = Cnd.where(Activity_works_subjectType::getActivityId, "=", activityId);
if (CollUtil.isNotEmpty(retainedSubjectIds)) {
cnd.and(Activity_works_subjectType::getId, "not in", retainedSubjectIds);
}
List<String> removedSubjectIds = dao.query(Activity_works_subjectType.class, cnd).stream()
.map(Activity_works_subjectType::getId)
.toList();
if (CollUtil.isEmpty(removedSubjectIds)) {
return;
}
// 先删除未被投稿引用主题下的作品类型,避免产生作品类型孤儿数据。
dao.clear(Activity_works_worksType.class,
Cnd.where(Activity_works_worksType::getSubjectId, "in", removedSubjectIds));
dao.clear(Activity_works_subjectType.class,
Cnd.where(Activity_works_subjectType::getId, "in", removedSubjectIds));
}
@Override
public String validateReferencedTypesBeforeUpdate(Activity_works_collection worksCollection) {
if (worksCollection == null || StrUtil.isBlank(worksCollection.getId())) {
@@ -173,10 +173,7 @@ public class ProposalSecondedController {
}
ProposalSearchParam.buildSearch(cnd, pageForm);
cnd.groupBy("t.id");
// 用户未指定有效排序字段时,继续沿用原有任务创建时间倒序。
if (!proposalCommonService.applySafePageOrder(cnd, pageForm, ORDER_COLUMNS)) {
cnd.desc("t.createdAt");
}
cnd.desc("t.createdAt");
sql.setCondition(cnd);
Pagination pagination = baseService.listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), sql);
return Result.success(pagination);
@@ -244,7 +244,7 @@ public class ProposalUnderTakeReplyController {
@SaCheckPermission("proposal.unitReply")
@ApiOperation("查询本单位可转办用户")
public Result selectViceUser(@Param("keyword") @Valid String keyword) {
Sql sql = Sqls.create("select id,loginname,username from vw_user $condition");
Sql sql = Sqls.create("select id,loginname,username,unitName from vw_user $condition");
Cnd cnd = Cnd.NEW();
cnd.andEX("unitId", "=", SecurityUtil.getUnitId());
cnd.andEX("id", "!=", SecurityUtil.getUserId());
@@ -79,7 +79,7 @@ public class ProposalSearchParam extends PageForm {
if (StrUtil.isAllNotBlank(searchParam.getPageOrderName(), searchParam.getPageOrderBy())) {
cnd.orderBy(searchParam.getPageOrderName(), PageUtil.getOrder(searchParam.getPageOrderBy()));
} else {
cnd.asc("info.code");
cnd.desc("info.code");
}
}
}
@@ -179,16 +179,17 @@ layout("/layouts/platform.html"){
</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item prop="tf_attachment" label="附件">
<file-upload
:value.sync="formData.tf_attachment"
upload_mode="drag"
:upload_number="5"
upload_result_category="array"
complete_result
upload_mode="file"
></file-upload>
</el-form-item>
<!-- 承办单位答复附件上传入口暂时停用,保留原配置便于后续恢复。 -->
<!-- <el-form-item prop="tf_attachment" label="附件">-->
<!-- <file-upload-->
<!-- :value.sync="formData.tf_attachment"-->
<!-- upload_mode="drag"-->
<!-- :upload_number="5"-->
<!-- upload_result_category="array"-->
<!-- complete_result-->
<!-- upload_mode="file"-->
<!-- ></file-upload>-->
<!-- </el-form-item>-->
<el-form-item label="答复内容" prop="tf_opinion"
:rules="[{required:true,message:'必填',trigger:['change','blur']}]">
<text-editor v-model="formData.tf_opinion"></text-editor>
@@ -444,7 +444,7 @@ layout("/layouts/platform_h5.html"){
<button type="button" class="works-mine-card__action" @click="onView(row)">
<van-icon name="eye-o"></van-icon>查看
</button>
<button type="button" class="works-mine-card__action is-primary" @click="onEdit(row)">
<button type="button" class="works-mine-card__action" @click="onEdit(row)">
<van-icon name="edit"></van-icon>编辑
</button>
<button type="button" class="works-mine-card__action is-danger" @click="onDelete(row)">
@@ -467,7 +467,7 @@ layout("/layouts/platform_h5.html"){
:close-on-click-overlay="false" show-cancel-button cancel-button-text="取消" confirm-button-text="确认删除"
@cancel="closeDeleteConfirm" @confirm="confirmDelete"></van-dialog>
<apply-form ref="applyFormRef"></apply-form>
<apply-form ref="applyFormRef" @submit-success="handleEditSubmitSuccess"></apply-form>
<info ref="infoRef"></info>
</div>
@@ -541,6 +541,10 @@ layout("/layouts/platform_h5.html"){
}
this.$refs.applyFormRef.onOpenEdit(row)
},
// 编辑提交成功后留在我的作品页面,仅刷新当前列表数据。
handleEditSubmitSuccess() {
this.loadWorks(true)
},
onDelete(row) {
this.$set(this, 'pendingDeleteRow', row)
window.h5HistoryLayerManager.open('works-collection-mine-delete-confirm')
@@ -8,7 +8,7 @@ const INFO = {
<van-sticky>
<van-nav-bar title="作品详情" left-text="返回" left-arrow @click-left="closeInfo"></van-nav-bar>
</van-sticky>
<div class="works-detail-scroll">
<div ref="detailScrollRef" class="works-detail-scroll">
<section class="works-detail-card">
<h2 class="works-detail-card__title">基础信息</h2>
<van-cell-group :border="false">
@@ -154,6 +154,7 @@ const INFO = {
onOpen(row) {
this.$set(this, "viewData", Object.assign({}, row, {files: this.normalizeFiles(row.files)}))
window.h5HistoryLayerManager.open("works-collection-mine-info")
this.resetScrollPosition()
this.$axios.post("/platform/activity/worksCollection/common/findOne", {id: row.id}).then((res) => {
if (res.code === 0) {
const viewData = res.data || {}
@@ -162,6 +163,12 @@ const INFO = {
}
})
},
// 弹窗组件会被复用,每次打开后需清除上一次查看时保留的滚动位置。
resetScrollPosition() {
this.$nextTick(() => {
if (this.$refs.detailScrollRef) this.$refs.detailScrollRef.scrollTop = 0
})
},
normalizeFiles(files) {
if (Array.isArray(files)) return files
if (!files) return []
@@ -222,24 +222,30 @@ layout("/layouts/platform_h5.html"){
white-space: nowrap;
}
.works-read-like-tag {
.works-read-like-summary {
display: inline-flex;
min-height: 24px;
padding: 3px 9px;
align-items: center;
flex: none;
border: 0;
border-radius: 12px;
color: #397de0;
background-color: #edf5ff;
color: #7f8da1;
font-size: 12px;
line-height: 18px;
white-space: nowrap;
box-sizing: border-box;
}
.works-read-like-tag .van-icon {
margin-right: 4px;
font-size: 14px;
/* 顶部仅展示累计点赞信息,人员图标与无底色文字用于区别底部可点击的点赞按钮。 */
.works-read-like-summary .van-icon {
display: inline-flex;
width: 20px;
height: 20px;
margin-right: 5px;
align-items: center;
justify-content: center;
border-radius: 50%;
color: #ffffff;
background-color: #8795aa;
font-size: 12px;
}
.works-read-files-row {
@@ -441,8 +447,9 @@ layout("/layouts/platform_h5.html"){
<div class="works-read-card-title">{{row.name || '未命名作品'}}</div>
<div class="works-read-card-activity">{{row.activityName || '暂无活动信息'}}</div>
</div>
<div class="works-read-like-tag">
<van-icon name="good-job-o"></van-icon>{{row.num || 0}}
<div class="works-read-like-summary">
<van-icon name="friends"></van-icon>
<span>已有 {{row.num || 0}} 人点赞</span>
</div>
</div>
</template>
@@ -10,7 +10,7 @@ const applyForm = {
<van-nav-bar title="作品提交" left-text="返回" left-arrow @click-left="closeApply"></van-nav-bar>
</van-sticky>
<van-form ref="formRef" class="works-apply-form form-container">
<div class="works-apply-scroll">
<div ref="applyScrollRef" class="works-apply-scroll">
<section class="works-apply-section">
<h2 class="works-apply-section__title">基础信息</h2>
<van-cell-group :border="false">
@@ -356,6 +356,7 @@ const applyForm = {
showOptionPicker: false, optionPopupType: "", optionPopupTitle: "",
optionPopupOptions: [], pendingOptionValue: "",
submitConfirmVisible: false, fileAccept: null, chooseWorksType: {},
submitSuccessMode: "navigate",
historyLayerPageKey: "works-collection-upload-h5",
historyLayerUnsubscribe: null, ownsHistoryRegistration: false
}
@@ -384,16 +385,19 @@ const applyForm = {
},
onOpen(row) {
this.resetFormData()
this.$set(this, "submitSuccessMode", "navigate")
this.$set(this, "row", row)
this.$set(this.formData, "activityId", row.id)
this.activityChange(row.id).then(() => {
window.h5HistoryLayerManager.open("works-collection-apply")
this.resetScrollPosition()
})
},
onOpenEdit(row) {
const formData = clone(row)
formData.files = this.normalizeFiles(formData.files)
this.resetFormData()
this.$set(this, "submitSuccessMode", "close")
this.$set(this, "row", formData)
this.$set(this, "formData", formData)
const user = this.$store.state.user || {}
@@ -408,6 +412,13 @@ const applyForm = {
}).then(() => {
this.setSelectedWorksType(formData.worksId)
window.h5HistoryLayerManager.open("works-collection-apply")
this.resetScrollPosition()
})
},
// 表单弹窗会被复用,每次打开后需清除上一次编辑或提交时保留的滚动位置。
resetScrollPosition() {
this.$nextTick(() => {
if (this.$refs.applyScrollRef) this.$refs.applyScrollRef.scrollTop = 0
})
},
// 附件可能由列表 SQL 返回 JSON 字符串,也可能已被序列化为数组;统一转换为上传组件需要的数组。
@@ -505,6 +516,13 @@ const applyForm = {
}).then((res) => {
if (res.code === 0) {
this.$toast.success("提交成功")
if (this.submitSuccessMode === "close") {
// 我的作品页使用弹窗编辑,成功后只关闭当前表单并通知父页面刷新列表。
window.h5HistoryLayerManager.close("works-collection-apply", () => {
this.$emit("submit-success")
})
return
}
window.h5HistoryLayerManager.closeAllAndNavigate("/platform/activity/worksCollection/mine/h5")
}
}).finally(() => { this.$set(this, "formLoading", false) })
@@ -82,6 +82,22 @@
background-color: #ff976a;
}
/* 校工会预审核四个操作按钮与 PC 端 Element UI 的 primary、danger、info 配色保持一致。 */
.proposal-pre-audit-actions .proposal-pre-audit-button--primary {
border-color: var(--color-primary);
background-color: var(--color-primary);
}
.proposal-pre-audit-actions .proposal-pre-audit-button--danger {
border-color: #f56c6c;
background-color: #f56c6c;
}
.proposal-pre-audit-actions .proposal-pre-audit-button--info {
border-color: #909399;
background-color: #909399;
}
.proposal-soft-pagination .van-pagination__item {
color: var(--proposal-soft-primary);
background-color: #f8fbff;
@@ -280,11 +296,18 @@
.proposal-view-card .van-cell__title {
flex: 0 0 92px;
width: 92px;
color: #596779;
color: #354255;
font-size: 14px;
font-weight: 600;
line-height: 24px;
}
/* 纵向字段中 flex-basis 会转为标题高度,需要取消横向字段使用的 92px 固定占位。 */
.proposal-view-card .direction-column-field .van-cell__title {
flex: none;
width: auto;
}
.proposal-view-card .van-cell__value {
min-width: 0;
color: #4f5d70;
@@ -495,7 +518,7 @@
.proposal-view-table-wrap .el-table {
min-width: 560px;
font-size: 16px;
font-size: 14px;
}
.proposal-view-slot:empty {
@@ -19,7 +19,8 @@ const PROPOSAL_INFO = {
:value="viewData.caseFilingType"></dict-tag>
</van-cell>
<van-cell title="提案类型">{{ viewData.typeName || '--' }}</van-cell>
<van-cell title="提案方式">{{ getDictText(dict.type.PROPOSAL_SOURCE, viewData.source) }}</van-cell>
<van-cell title="提案方式">{{ getDictText(dict.type.PROPOSAL_SOURCE, viewData.source) }}
</van-cell>
<van-cell title="代表姓名">{{ viewData.createUserName || '--' }}</van-cell>
<van-cell title="所属教代会">{{ viewData.sessionName || viewData.fullName || '--' }}</van-cell>
<van-cell :title="viewData.committeeName ? '所属委员会' : '所属代表团'">
@@ -28,7 +29,7 @@ const PROPOSAL_INFO = {
<van-cell title="单位">{{ viewData.unitName || '--' }}</van-cell>
<van-cell title="联系电话">{{ viewData.mobile || '--' }}</van-cell>
<van-cell title="提案时间">{{ viewData.createTime || '--' }}</van-cell>
<van-cell title="提案案由" class="proposal-view-long-cell">
<van-cell title="案由" class="proposal-view-long-cell">
<div class="proposal-view-rich-value" v-html="viewData.brief || '--'"></div>
</van-cell>
<van-cell title="建议措施" class="proposal-view-long-cell">
@@ -62,7 +63,9 @@ const PROPOSAL_INFO = {
<h2 class="proposal-view-card__title">委员会成员意见 {{ index + 1 }}</h2>
<van-cell-group :border="false">
<van-cell title="委员">
{{ opinion.commissionerName || '--' }}<template v-if="opinion.commissionerLoginName">{{ opinion.commissionerLoginName }}</template>
{{ opinion.commissionerName || '--' }}
<template v-if="opinion.commissionerLoginName">{{ opinion.commissionerLoginName }}
</template>
</van-cell>
<van-cell title="立案结果">
<dict-tag :options="dict.type.PROPOSAL_CASE_FILING_RESULT"
@@ -109,12 +112,12 @@ const PROPOSAL_INFO = {
<!-- 点击时间轴节点后只在此处展示该任务原有的完整详情 -->
<section class="proposal-view-card proposal-flow-detail" v-if="selectedTask">
<h2 class="proposal-view-card__title">节点详情</h2>
<h3 class="proposal-flow-detail__name">{{ selectedTask.displayName || '--' }}</h3>
<h2 class="proposal-view-card__title">{{ selectedTask.displayName || '--' }}</h2>
<van-cell-group :border="false">
<template v-if="selectedTask.ext && selectedTask.ext.isFirstTaskNode">
<van-cell title="申请用户">
{{ selectedTask.ext.initiatorName || '--' }}{{ selectedTask.ext.initiatorAccount || '--' }}
{{ selectedTask.ext.initiatorName || '--' }}{{ selectedTask.ext.initiatorAccount ||
'--' }}
</van-cell>
<van-cell title="申请时间">{{ selectedTask.finishTime || '--' }}</van-cell>
<van-cell title="办理结果">
@@ -125,26 +128,35 @@ const PROPOSAL_INFO = {
<template v-else-if="selectedTask.taskName === 'invite'">
<van-cell title="办理用户">
{{ selectedTask.taskFormData.userName || '--' }}{{ selectedTask.taskFormData.loginName || '--' }}
{{ selectedTask.taskFormData.userName || '--' }}{{ selectedTask.taskFormData.loginName
|| '--' }}
</van-cell>
<van-cell title="办理时间">{{ selectedTask.finishTime || '--' }}</van-cell>
<van-cell title="附议人" class="proposal-view-long-cell">
<div class="proposal-view-table-wrap">
<el-table :data="selectedTask?.taskFormData?.seconder">
<el-table-column label="姓名" prop="userName"></el-table-column>
<el-table-column label="工号" prop="loginName"></el-table-column>
<el-table-column label="性别" prop="sex"></el-table-column>
<el-table-column label="单位" prop="unitName" show-overflow-tooltip></el-table-column>
<el-table-column label="分工会" prop="unionName" show-overflow-tooltip></el-table-column>
<el-table-column label="代表团" prop="delegationName" show-overflow-tooltip></el-table-column>
<el-table-column label="姓名" prop="userName" width="80px"
header-align="center" align="center"></el-table-column>
<el-table-column label="工号" prop="loginName" width="80px"
header-align="center" align="center"></el-table-column>
<el-table-column label="性别" prop="sex" width="50px" header-align="center"
align="center"></el-table-column>
<el-table-column label="单位" prop="unitName"
show-overflow-tooltip header-align="center"
align="center"></el-table-column>
<el-table-column label="代表团" prop="delegationName"
show-overflow-tooltip header-align="center"
align="center"></el-table-column>
</el-table>
</div>
</van-cell>
</template>
<template v-else-if="selectedTask.taskName === 'committee' || selectedTask.taskName === 'committeeFilingUnit'">
<template
v-else-if="selectedTask.taskName === 'committee' || selectedTask.taskName === 'committeeFilingUnit'">
<van-cell title="办理用户">
{{ selectedTask.taskFormData.userName || '--' }}{{ selectedTask.taskFormData.loginName || '--' }}
{{ selectedTask.taskFormData.userName || '--' }}{{ selectedTask.taskFormData.loginName
|| '--' }}
</van-cell>
<van-cell title="办理时间">{{ selectedTask.finishTime || '--' }}</van-cell>
<van-cell title="立案结果">
@@ -155,19 +167,23 @@ const PROPOSAL_INFO = {
<dict-tag :options="dict.type.PROPOSAL_CASE_FILING_TYPE"
:value="selectedTask.ext.tf_caseFilingType"></dict-tag>
</van-cell>
<template v-if="['CONFIRM_FILING', 'SUGGESTION'].includes(selectedTask.ext.tf_caseFilingResult)">
<template
v-if="['CONFIRM_FILING', 'SUGGESTION'].includes(selectedTask.ext.tf_caseFilingResult)">
<van-cell v-if="selectedTask.ext.tf_caseFilingResult === 'SUGGESTION'" title="承办单位">
{{ selectedTask?.ext?.tf_masterUnitNameStr || selectedTask?.ext?.tf_slaveUnitNameStr || '--' }}
{{ selectedTask?.ext?.tf_masterUnitNameStr || selectedTask?.ext?.tf_slaveUnitNameStr
|| '--' }}
</van-cell>
<template v-else>
<van-cell title="主办单位">{{ selectedTask?.ext?.tf_masterUnitNameStr || '--' }}</van-cell>
<van-cell title="协办单位">{{ selectedTask?.ext?.tf_slaveUnitNameStr || '--' }}</van-cell>
<van-cell title="主办单位">{{ selectedTask?.ext?.tf_masterUnitNameStr || '--' }}
</van-cell>
<van-cell title="协办单位">{{ selectedTask?.ext?.tf_slaveUnitNameStr || '--' }}
</van-cell>
</template>
</template>
<van-cell title="办理意见"
v-if="selectedTask.taskFormData.tf_opinion"
class="proposal-view-long-cell">
<div class="proposal-view-rich-value" v-html="selectedTask.taskFormData.tf_opinion"></div>
v-if="selectedTask.taskFormData.tf_opinion">
<div class="proposal-view-rich-value"
v-html="selectedTask.taskFormData.tf_opinion"></div>
</van-cell>
</template>
@@ -177,7 +193,8 @@ const PROPOSAL_INFO = {
{{ selectedTask.ext.underTakeName || '--' }}
</van-cell>
<van-cell title="办理用户">
{{ selectedTask.taskFormData.userName || '--' }}{{ selectedTask.taskFormData.loginName || '--' }}
{{ selectedTask.taskFormData.userName || '--' }}{{ selectedTask.taskFormData.loginName
|| '--' }}
</van-cell>
<van-cell title="办理时间">{{ selectedTask.finishTime || '--' }}</van-cell>
<van-cell title="办理评价" v-if="selectedTask.taskName === 'feedback'">
@@ -189,9 +206,9 @@ const PROPOSAL_INFO = {
:value="selectedTask.ext.submitType"></dict-tag>
</van-cell>
<van-cell title="办理意见"
v-if="selectedTask.taskFormData && selectedTask.taskFormData.tf_opinion"
class="proposal-view-long-cell">
<div class="proposal-view-rich-value" v-html="selectedTask.taskFormData.tf_opinion"></div>
v-if="selectedTask.taskFormData && selectedTask.taskFormData.tf_opinion">
<div class="proposal-view-rich-value"
v-html="selectedTask.taskFormData.tf_opinion"></div>
</van-cell>
</template>
</van-cell-group>
@@ -1,114 +1,389 @@
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">
<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>
<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'">
<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>
<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>
<h2 class="proposal-view-card__title">反馈评价<span
class="proposal-audit-card__required">*</span></h2>
<van-field v-model="formData.tf_feedback_name" label="满意度"
placeholder="请选择满意度"
:rules="[{required:true,message:'请选择满意度'}]" required readonly
is-link @click="openLayer('feedback-picker')"></van-field>
<van-field v-model="formData.tf_opinion" class="proposal-audit-field"
name="tf_opinion" type="textarea" rows="6" label=""
placeholder="请输入反馈意见"
:rules="[{required:true,message:'请填写反馈意见'}]"
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>
<van-field v-model="formData.result" label="承办意向"
placeholder="请选择承办意向"
:rules="[{required:true,message:'请选择承办意向'}]" required readonly
is-link @click="openLayer('result-picker')"></van-field>
<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'">
<section class="proposal-view-card">
<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>
<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>
<h2 class="proposal-view-card__title">承办单位答复<span
class="proposal-audit-card__required">*</span></h2>
<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>
<!-- 承办单位答复附件上传入口暂时停用保留公用上传组件配置便于后续恢复 -->
<!-- <van-field label="附件" name="tf_attachment" class="direction-column-field">-->
<!-- <template #input>-->
<!-- <h5-file-upload-->
<!-- :value.sync="formData.tf_attachment"-->
<!-- :upload_number="5"-->
<!-- upload_mode="file"-->
<!-- upload_result_category="array"-->
<!-- upload_result_type="url"-->
<!-- complete_result>-->
<!-- </h5-file-upload>-->
<!-- </template>-->
<!-- </van-field>-->
<van-field v-model="formData.tf_opinion" class="proposal-audit-field"
name="tf_opinion" type="textarea" rows="6" label=""
placeholder="请输入答复内容"
:rules="[{required:true,message:'请填写答复内容'}]"
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>
<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 :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>
<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>
<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" class="proposal-soft-button proposal-soft-button--danger" @click="prepareSubmit(6)">退回</van-button><van-button type="danger" block :loading="formLoading" class="proposal-soft-button proposal-soft-button--danger" @click="prepareSubmit(2)"></van-button><van-button type="primary" block :loading="formLoading" class="proposal-soft-button" @click="prepareSubmit(1)"></van-button>
<van-button type="primary" block :loading="formLoading" class="proposal-soft-button"
@click="prepareSubmit(1)">同意
</van-button>
<van-button type="danger" block :loading="formLoading"
class="proposal-soft-button proposal-soft-button--danger"
@click="prepareSubmit(2)">不同意
</van-button>
<van-button type="danger" block :loading="formLoading"
class="proposal-soft-button proposal-soft-button--danger"
@click="prepareSubmit(6)">退回
</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" class="proposal-soft-button proposal-soft-button--warning" @click="prepareSubmit(4)">退回不审核</van-button><van-button type="warning" :loading="formLoading" class="proposal-soft-button proposal-soft-button--warning" @click="prepareSubmit(6)">退</van-button><van-button type="danger" :loading="formLoading" class="proposal-soft-button proposal-soft-button--danger" @click="prepareSubmit(2)"></van-button><van-button type="primary" :loading="formLoading" class="proposal-soft-button" @click="prepareSubmit(1)"></van-button>
<div class="proposal-popup-form-actions proposal-pre-audit-actions" v-else-if="kind === 'preAudit'"
style="display:grid;grid-template-columns:repeat(2,minmax(0,1fr))">
<van-button type="primary" :loading="formLoading"
class="proposal-soft-button proposal-pre-audit-button--primary"
@click="prepareSubmit(1)">同意
</van-button>
<van-button type="danger" :loading="formLoading"
class="proposal-soft-button proposal-pre-audit-button--danger"
@click="prepareSubmit(2)">不同意
</van-button>
<van-button type="info" :loading="formLoading"
class="proposal-soft-button proposal-pre-audit-button--info"
@click="prepareSubmit(4)">退回不审核
</van-button>
<van-button type="info" :loading="formLoading"
class="proposal-soft-button proposal-pre-audit-button--info"
@click="prepareSubmit(6)">退回审核
</van-button>
</div>
<div class="proposal-popup-form-actions" v-else-if="kind === 'schoolLeaderApproval'">
<van-button type="primary" block :loading="formLoading" class="proposal-soft-button"
@click="prepareSubmit('agree')">同意答复
</van-button>
<van-button type="danger" block :loading="formLoading"
class="proposal-soft-button proposal-soft-button--danger"
@click="prepareSubmit('back')">退回答复
</van-button>
</div>
<div class="proposal-popup-form-actions" v-else>
<van-button type="primary" block :loading="formLoading" class="proposal-soft-button"
@click="prepareSubmit(1)">提交
</van-button>
<van-button block :disabled="formLoading" class="proposal-rounded-button"
@click="close">返回
</van-button>
</div>
<div class="proposal-popup-form-actions" v-else-if="kind === 'schoolLeaderApproval'"><van-button type="danger" block :loading="formLoading" class="proposal-soft-button proposal-soft-button--danger" @click="prepareSubmit('back')">退回答复</van-button><van-button type="primary" block :loading="formLoading" class="proposal-soft-button" @click="prepareSubmit('agree')"></van-button></div>
<div class="proposal-popup-form-actions" v-else><van-button block :disabled="formLoading" class="proposal-rounded-button" @click="close">取消</van-button><van-button type="primary" block :loading="formLoading" class="proposal-soft-button" @click="prepareSubmit(1)"></van-button></div>
</div>
</proposal-info>
</div>
</div>
<!-- 字典通过异步请求加载初始化阶段可能为 null使用空数组保证弹层首次渲染不执行 null.map -->
<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>
<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-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} },
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("、") }
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") },
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) }
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.$set(this.formData, "tf_attachment", [])
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 },
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) {
// 各办理页面提交前统一校验意见字段;空白字符按未填写处理,提示后不再打开确认弹框。
const opinionConfig = this.kind === "suggestion"
@@ -120,24 +395,63 @@ const PROPOSAL_TASK_POPUP = {
this.$toast(opinionConfig.message)
return
}
this.$refs.formRef.validate().then(() => { this.$set(this,"pendingSubmitType",value); this.openLayer("submit-confirm") }).catch(() => {})
this.$refs.formRef.validate().then(() => {
this.$set(this, "pendingSubmitType", value);
this.openLayer("submit-confirm")
}).catch(() => {
})
},
/** 根据页面类型保留原接口和参数分支,成功后清理全部弹层历史并通知列表刷新。 */
submit() {
this.$set(this,"formLoading",true)
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)})
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) })
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) }
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)
}
}
@@ -47,6 +47,190 @@ layout("/layouts/platform_h5.html"){
color: #1989fa;
}
/* Popup 内固定导航栏不依赖 placeholder 占位,避免遮挡邀请附议筛选框。 */
.proposal-invite-popup {
display: flex;
padding-top: 46px;
overflow: hidden;
box-sizing: border-box;
flex-direction: column;
}
/* 邀请附议人弹框固定展示工号、姓名查询入口,清空关键词后恢复完整人员列表。 */
.proposal-invite-search {
padding: 12px 12px 8px;
background-color: #f3f7fd;
}
.proposal-invite-search .van-search__content {
height: 42px;
padding-left: 12px;
align-items: center;
border-radius: 12px;
background-color: #fff;
box-shadow: 0 5px 16px rgba(43, 73, 112, .1);
}
.proposal-invite-search .van-field__control {
color: #263548;
font-size: 14px;
}
.proposal-invite-search .van-field__control::placeholder {
color: #a7b0bf;
}
/* 邀请状态使用双分段切换,当前状态通过白色底和蓝色短线突出展示。 */
.proposal-invite-tabs {
padding: 4px 12px 8px;
background-color: #f3f7fd;
}
.proposal-invite-tabs .van-tabs__wrap {
height: 48px;
}
.proposal-invite-tabs .van-tabs__nav {
padding: 4px;
box-sizing: border-box;
border-radius: 14px;
background-color: #edf4ff;
}
.proposal-invite-tabs .van-tab {
height: 40px;
color: #6e7d92;
font-size: 14px;
line-height: 40px;
}
.proposal-invite-tabs .van-tab--active {
border-radius: 10px;
color: #1989fa;
font-weight: 600;
background-color: #fff;
box-shadow: 0 4px 10px rgba(31, 89, 164, .08);
}
.proposal-invite-tabs .van-tabs__line {
bottom: 4px;
width: 30px;
height: 3px;
border-radius: 3px;
background-color: #1989fa;
}
/* 人员卡片区域独立滚动,顶部筛选区和底部操作区始终保留在视口内。 */
.proposal-invite-scroll {
min-height: 0;
overflow-y: auto;
flex: 1;
-webkit-overflow-scrolling: touch;
}
/* 人员列表以独立卡片呈现,姓名、工号、单位和勾选区域层级清晰。 */
.proposal-invite-list {
padding: 0 12px 12px;
}
.proposal-invite-list .van-cell-group {
background-color: transparent;
}
.proposal-invite-person {
min-height: 70px;
margin-top: 12px;
padding: 12px 14px;
align-items: center;
border-radius: 12px;
background-color: #fff;
box-shadow: 0 6px 16px rgba(43, 73, 112, .08);
}
.proposal-invite-person::after {
display: none;
}
.proposal-invite-person__title {
display: block;
color: #253247;
font-size: 15px;
font-weight: 600;
line-height: 22px;
}
.proposal-invite-person__login-name {
color: #79879a;
font-size: 14px;
font-weight: 400;
}
.proposal-invite-person__unit {
display: block;
margin-top: 3px;
color: #8c99aa;
font-size: 12px;
line-height: 18px;
}
.proposal-invite-person__checkbox {
display: flex;
width: 36px;
height: 36px;
align-items: center;
justify-content: center;
}
.proposal-invite-person__checkbox .van-icon {
width: 20px;
height: 20px;
box-sizing: border-box;
border: 1px solid #4b8cff;
border-radius: 50%;
background-color: #fff;
}
.proposal-invite-person__checkbox .van-checkbox__icon--checked .van-icon {
border-color: #1989fa;
background-color: #1989fa;
}
/* 分页和邀请按钮沿用同一圆角语言,与人员卡片形成统一的操作区域。 */
.proposal-invite-actions {
padding: 0 12px calc(12px + env(safe-area-inset-bottom));
background-color: #f3f7fd;
flex: none;
}
.proposal-invite-pagination {
margin-top: 2px;
}
.proposal-invite-pagination .van-pagination__item {
min-height: 36px;
border-radius: 10px;
background-color: #fff;
box-shadow: 0 4px 12px rgba(43, 73, 112, .06);
}
.proposal-invite-pagination .van-pagination__item--active,
.proposal-invite-pagination .van-pagination__item:not(.van-pagination__item--disabled):active {
color: #fff;
background-color: #1989fa;
}
.proposal-invite-submit {
height: 42px;
margin-top: 12px;
border-radius: 12px;
}
.proposal-invite-empty {
min-height: 0;
overflow-y: auto;
flex: 1;
}
/* 详情弹框复用项目全屏 Popup 的固定导航结构,内容区域单独滚动。 */
.proposal-detail-popup {
overflow: hidden;
@@ -11,60 +11,51 @@ const PROPOSAL_INVITE_SECONDER_COMPONENT = {
left-arrow
left-text="返回"
fixed
placeholder
@click-left="$emit('close')">
</van-nav-bar>
<van-search
v-model="pageForm.searchKeyword"
class="proposal-invite-search"
:show-action="false"
shape="round"
clearable
placeholder="请输入工号或者姓名查询"
@search="doSearch">
@search="doSearch"
@clear="doSearch">
</van-search>
<van-tabs v-model="isInvite" @click="isInviteClick">
<van-tabs v-model="isInvite" class="proposal-invite-tabs" @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
class="proposal-soft-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 proposal-soft-button">
邀请
</van-button>
</div>
</van-checkbox-group>
<div class="proposal-invite-scroll" v-if="tableData && tableData.length > 0">
<van-checkbox-group class="proposal-invite-list" v-model="tableSelection">
<van-cell-group>
<van-cell
class="proposal-invite-person"
v-for="(item, index) in tableData"
clickable
:key="item.loginName"
@click="tableRowToggle(index)">
<span slot="title" class="proposal-invite-person__title">
{{item.userName}} <span class="proposal-invite-person__login-name">{{item.loginName}}</span>
</span>
<span slot="label" class="proposal-invite-person__unit">
{{item.unitName}}
</span>
<van-checkbox
:name="item"
:ref="'checkboxes' + item.loginName"
class="proposal-invite-person__checkbox"
slot="right-icon"
@click.stop
v-if="!pageForm.isInvite">
</van-checkbox>
</van-cell>
</van-cell-group>
</van-checkbox-group>
</div>
<!-- 空状态提示随已邀请 / 可邀请标签切换便于用户判断当前无数据的具体范围 -->
<div v-else class="proposal-invite-empty">
@@ -75,6 +66,24 @@ const PROPOSAL_INVITE_SECONDER_COMPONENT = {
</div>
</div>
<div class="proposal-invite-actions" v-if="tableData && tableData.length > 0">
<van-pagination
class="proposal-soft-pagination proposal-invite-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="proposal-soft-button proposal-invite-submit">
邀请
</van-button>
</div>
<van-dialog
:value="confirmVisible"
title="提示"
@@ -102,7 +111,7 @@ const PROPOSAL_INVITE_SECONDER_COMPONENT = {
tableData: [],
pageForm: {
pageNumber: 1,
pageSize: 5,
pageSize: 6,
totalCount: 0,
pageOrderName: "",
pageOrderBy: "",
@@ -207,8 +216,16 @@ const PROPOSAL_INVITE_SECONDER_COMPONENT = {
this.doSearch()
},
/**
* 点击人员卡片时切换对应的附议人勾选状态
* index 为当前页人员索引已邀请状态或找不到对应复选框时不处理无返回值
*/
tableRowToggle(index) {
if (this.pageForm.isInvite || index < 0) return
const item = this.tableData[index]
const checkboxRef = item ? this.$refs["checkboxes" + item.loginName] : null
if (!checkboxRef || !checkboxRef[0]) return
checkboxRef[0].toggle()
},
/**
@@ -411,8 +411,8 @@ layout("/layouts/platform_h5.html"){
</van-form>
</section>
<div class="proposal-seconded-form-actions">
<van-button type="danger" block :loading="formLoading" class="proposal-soft-button proposal-soft-button--danger" @click="submitSeconded(false)">不同意</van-button>
<van-button type="primary" block :loading="formLoading" class="proposal-soft-button" @click="submitSeconded(true)">同意</van-button>
<van-button type="danger" block :loading="formLoading" class="proposal-soft-button proposal-soft-button--danger" @click="submitSeconded(false)">不同意</van-button>
</div>
</div>
</proposal-info>
@@ -237,6 +237,109 @@ layout("/layouts/platform_h5.html"){
box-shadow: 0 4px 12px rgba(43, 73, 112, .08), inset 0 1px 1px rgba(255, 255, 255, .82);
}
/* 选择转交人弹框沿用提案列表的圆角搜索框、人员卡片和插图空状态。 */
.proposal-transfer-user-popup .proposal-full-popup__page {
background-color: #f3f7fd;
}
.proposal-transfer-user-search {
padding: 12px 12px 8px;
background-color: #f3f7fd;
flex: none;
}
.proposal-transfer-user-search .van-search__content {
height: 42px;
padding-left: 12px;
align-items: center;
border-radius: 12px;
background-color: #fff;
box-shadow: 0 5px 16px rgba(43, 73, 112, .1);
}
.proposal-transfer-user-search .van-field__control {
color: #263548;
font-size: 14px;
}
.proposal-transfer-user-search .van-field__control::placeholder {
color: #a7b0bf;
}
.proposal-transfer-user-scroll {
min-height: 0;
overflow-y: auto;
flex: 1;
-webkit-overflow-scrolling: touch;
}
.proposal-transfer-user-list {
padding: 0 12px 12px;
background-color: transparent;
}
.proposal-transfer-user-item {
min-height: 64px;
margin-top: 12px;
padding: 12px 14px;
align-items: center;
border-radius: 12px;
background-color: #fff;
box-shadow: 0 6px 16px rgba(43, 73, 112, .08);
}
.proposal-transfer-user-item::after {
display: none;
}
.proposal-transfer-user-item__name {
display: block;
color: #253247;
font-size: 15px;
font-weight: 600;
line-height: 22px;
}
.proposal-transfer-user-item__unit {
color: #79879a;
font-size: 13px;
font-weight: 400;
}
.proposal-transfer-user-empty {
display: flex;
min-height: 420px;
padding: 40px 18px 36px;
align-items: center;
justify-content: center;
flex-direction: column;
box-sizing: border-box;
text-align: center;
}
.proposal-transfer-user-empty img {
display: block;
width: 82%;
max-width: 270px;
height: auto;
object-fit: contain;
}
.proposal-transfer-user-empty__title {
margin-top: 14px;
color: #50627a;
font-size: 15px;
font-weight: 600;
line-height: 22px;
}
.proposal-transfer-user-empty__hint {
margin-top: 5px;
color: #9aa7b8;
font-size: 12px;
line-height: 18px;
}
@media (max-width: 350px) {
.proposal-unit-reply-content,
.proposal-unit-reply-sticky {
@@ -340,7 +443,7 @@ layout("/layouts/platform_h5.html"){
<div class="action-btn" v-if="!pageForm.approval && $auth.hasRoleOr('SYSADMIN,PROPOSAL_UNIT_LEADER')"
@click="openTransfer(row)">
<van-icon class="proposal-action-icon" name="exchange"></van-icon>
<span></span>
<span></span>
</div>
</template>
</table-list>
@@ -350,6 +453,16 @@ layout("/layouts/platform_h5.html"){
<transfer ref="transferRef" @success="doSearch"></transfer>
<proposal-task-popup ref="taskPopup" kind="unitReply" page-key="proposal-unit-reply-task" @success="doSearch"></proposal-task-popup>
<van-dialog
:value="communicationConfirmVisible"
title="提示"
message="是否已与提案代表进行充分沟通?"
confirm-button-text="是"
cancel-button-text="否"
show-cancel-button
@confirm="confirmCommunication"
@cancel="closeCommunicationConfirm">
</van-dialog>
</div>
<script nonce="${cspNonce!}">
<!--#include("transfer.js"){}#-->
@@ -378,6 +491,10 @@ layout("/layouts/platform_h5.html"){
{text: "未审核", value: "0"},
{text: "已审核", value: "1"}
],
communicationConfirmVisible: false,
pendingApprovalRow: null,
historyLayerPageKey: "proposal-unit-reply",
unsubscribeHistoryLayer: null
}
},
components: {
@@ -414,7 +531,29 @@ layout("/layouts/platform_h5.html"){
},
onApproval(row, index) {
this.$refs.taskPopup.open(row, "approval")
// row 为当前待答复提案;确认已充分沟通后,才进入原有答复表单。
this.$set(this, "pendingApprovalRow", row)
window.h5HistoryLayerManager.open("communication-confirm")
},
/** stack 为当前页面弹层名称数组;同步沟通确认框状态,无返回值。 */
syncHistoryLayers(stack) {
const visible = stack.includes("communication-confirm")
this.$set(this, "communicationConfirmVisible", visible)
if (!visible) this.$set(this, "pendingApprovalRow", null)
},
// 取消确认时同步回退浏览器历史,手机返回键与弹窗按钮行为保持一致。
closeCommunicationConfirm() {
window.h5HistoryLayerManager.close("communication-confirm")
},
// 确认后先关闭沟通弹层,再使用暂存的提案数据打开原有答复表单。
confirmCommunication() {
const row = this.pendingApprovalRow
window.h5HistoryLayerManager.close("communication-confirm", () => {
if (row) this.$refs.taskPopup.open(row, "approval")
})
},
onRevoke(row) {
@@ -445,6 +584,10 @@ layout("/layouts/platform_h5.html"){
}
},
created() {
window.h5HistoryLayerManager.ensureRegistered(this.historyLayerPageKey)
this.$set(this, "unsubscribeHistoryLayer", window.h5HistoryLayerManager.subscribe((stack) => {
this.syncHistoryLayers(stack)
}))
const tab = new URLSearchParams(window.location.search).get("tab")
if (tab === "done") {
this.$set(this.pageForm, "approval", true)
@@ -453,6 +596,12 @@ layout("/layouts/platform_h5.html"){
this.$set(this.pageForm, "approval", false)
this.$set(this.pageForm, "approvalText", "0")
}
},
beforeDestroy() {
if (this.unsubscribeHistoryLayer) this.unsubscribeHistoryLayer()
if (window.h5HistoryLayerManager.pageKey === this.historyLayerPageKey) {
window.h5HistoryLayerManager.unregister(this.historyLayerPageKey)
}
}
})
</script>
@@ -1,28 +1,58 @@
const transfer = {
template: /*language=HTML*/ `
<van-popup v-model="showTransferDialog" position="right" class="proposal-full-popup" :close-on-click-overlay="false" get-container="#app">
<van-popup v-model="showTransferDialog" position="right" class="proposal-full-popup proposal-transfer-dialog" :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>
<div class="proposal-info-card">
<van-field v-model="searchKeyword" label="转交给" placeholder="搜索用户" required readonly is-link @click="openUserList"></van-field>
</div>
<section class="proposal-view-card proposal-transfer-info-card">
<van-cell-group :border="false">
<van-cell title="提案编号">{{transferForm.code || '--'}}</van-cell>
<van-cell title="提案名称">{{transferForm.name || '--'}}</van-cell>
<van-cell title="代表团">{{transferForm.delegationName || '--'}}</van-cell>
</van-cell-group>
</section>
<section class="proposal-view-card proposal-transfer-user-card">
<van-cell-group :border="false">
<van-field v-model="searchKeyword" label="转交给" placeholder="搜索用户" required readonly is-link @click="openUserList"></van-field>
</van-cell-group>
</section>
</div>
<div class="button-group proposal-transfer-button-group">
<van-button block type="info" class="proposal-soft-button" @click="handleTransfer">提交</van-button>
<van-button block plain class="proposal-transfer-cancel-button" @click="closeTransfer">返回</van-button>
</div>
<div class="button-group proposal-transfer-button-group"><van-button block plain class="proposal-transfer-cancel-button" @click="closeTransfer">取消</van-button><van-button block type="info" class="proposal-soft-button" @click="handleTransfer"></van-button></div>
</div>
<van-popup v-model="showUserList" position="right" class="proposal-full-popup" :close-on-click-overlay="false" get-container="#app">
<van-popup v-model="showUserList" position="right" class="proposal-full-popup proposal-transfer-user-popup" :close-on-click-overlay="false" get-container="#app">
<div class="proposal-full-popup__page">
<van-nav-bar @click-left="closeUserList" left-arrow left-text="返回" title="选择转交人" placeholder fixed></van-nav-bar>
<div class="proposal-full-popup__scroll">
<van-search v-model="keyword" placeholder="搜索用户" @search="selectTransferUser(keyword)"></van-search>
<div class="proposal-view-main">
<div v-if="transferUserOptions.length" class="proposal-info-card"><van-cell v-for="item in transferUserOptions" :key="item.id" :title="item.username" @click="selectUser(item)"></van-cell></div>
<van-empty v-else class="empty-tip">请搜索需要转办的用户</van-empty>
<van-search
v-model="keyword"
class="proposal-transfer-user-search"
shape="round"
clearable
placeholder="请输入姓名或工号搜索"
@search="selectTransferUser(keyword)"
@clear="resetTransferUserSearch">
</van-search>
<div class="proposal-transfer-user-scroll">
<van-cell-group v-if="transferUserOptions.length" class="proposal-transfer-user-list" :border="false">
<van-cell
v-for="item in transferUserOptions"
:key="item.id"
class="proposal-transfer-user-item"
clickable
@click="selectUser(item)">
<span slot="title" class="proposal-transfer-user-item__name">
{{item.username}}<span class="proposal-transfer-user-item__unit">{{item.unitName || '--'}}</span>
</span>
</van-cell>
</van-cell-group>
<div v-else class="proposal-transfer-user-empty">
<img src="/assets/mobile/img/proposal/proposal-empty.png" alt="暂无转交用户插画">
<div class="proposal-transfer-user-empty__title">
{{hasUserSearched ? '暂无搜索记录' : '请搜索需要转办的用户'}}
</div>
<div v-if="hasUserSearched" class="proposal-transfer-user-empty__hint">请调整搜索条件后重试</div>
</div>
</div>
</div>
@@ -31,7 +61,7 @@ const transfer = {
</van-popup>
`,
data() {
return {showTransferDialog:false,transferForm:{},transferUserOptions:[],searchKeyword:"",showUserList:false,keyword:"",confirmVisible:false,historyLayerPageKey:"proposal-unit-reply-transfer",unsubscribeHistoryLayer:null}
return {showTransferDialog:false,transferForm:{},transferUserOptions:[],searchKeyword:"",showUserList:false,keyword:"",hasUserSearched:false,confirmVisible:false,historyLayerPageKey:"proposal-unit-reply-transfer",unsubscribeHistoryLayer:null}
},
computed: {
confirmMessage() {
@@ -43,12 +73,15 @@ const transfer = {
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") },
/** 打开用户列表前重置搜索条件和结果,确保首次进入展示搜索引导空状态。 */
openUserList() { this.resetTransferUserSearch(); 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 || []) }) },
/** 清空姓名或工号关键词、搜索状态和人员结果,无返回值。 */
resetTransferUserSearch() { this.$set(this,"keyword",""); this.$set(this,"hasUserSearched",false); this.$set(this,"transferUserOptions",[]) },
/** keyword 为姓名或工号关键词;空值仅恢复搜索引导,非空值查询并返回可转交用户数组。 */
selectTransferUser(keyword) { const searchText=(keyword || "").trim(); if (!searchText) { this.resetTransferUserSearch(); return } this.$set(this,"hasUserSearched",true); this.$axios.post("/platform/proposal/unitReply/selectViceUser",{keyword:searchText}).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")) } }) }