活动申报手机端审核
活动报销手机端审核
This commit is contained in:
@@ -0,0 +1,18 @@
|
||||
<!--#
|
||||
layout("/mobile/platform.html"){
|
||||
#-->
|
||||
<!--#include('/mobile/jf/activitybx/review/index.html'){}#-->
|
||||
<script>
|
||||
// 节点配置由当前页面固定提供,业务权限及状态仍由服务端校验。
|
||||
const activityReviewConfig = {
|
||||
stage: 'accounting', title: '校工会负责人审核', state: 1020,
|
||||
confirmMessage: '通过后将转交校工会主席审核。',
|
||||
canChooseUnion: ${@shiro.hasRole('sysadmin')||@shiro.hasRole('A06')||@shiro.hasRole('H03')},
|
||||
currentUnionId: '${@io.v.nutz.base.utils.Vi.getUnionId()}'
|
||||
}
|
||||
<!--#include('/mobile/jf/activitybx/review/info.js'){}#-->
|
||||
<!--#include('/mobile/jf/activitybx/review/page.js'){}#-->
|
||||
</script>
|
||||
<!--#
|
||||
}
|
||||
#-->
|
||||
@@ -0,0 +1,135 @@
|
||||
<!--#
|
||||
layout("/mobile/platform.html"){
|
||||
#-->
|
||||
<style>
|
||||
.association-page { min-height: 100vh; background: #f6f7f9; color: #323233; font-size: 13px; }
|
||||
.association-page .van-nav-bar__title { font-size: 15px; font-weight: 400; }
|
||||
.association-filters .van-dropdown-menu__bar { box-shadow: none; border-top: 1px solid #f8f8f8; border-bottom: 1px solid #f8f8f8; }
|
||||
.association-filters .van-dropdown-menu__item { flex: 1; min-width: 0; }
|
||||
.association-filters .van-dropdown-menu__title { max-width: calc(100% - 40px); padding: 0 8px; font-size: 14px; }
|
||||
.association-filters .van-dropdown-item__content { max-height: 70vh; border-radius: 0 0 15px 15px; }
|
||||
.association-list { padding: 2px 0 12px; }
|
||||
.association-card { margin: 12px 13px 0; padding: 11px 12px 10px; background: white; border-radius: 10px; box-shadow: 0 5px 12px rgba(30,50,70,.04); }
|
||||
.association-card h3 { position: relative; margin: 0 0 7px; color: #086bc3; font-size: 15px; line-height: 21px; font-weight: 600; overflow-wrap: anywhere; }
|
||||
.association-card h3:before { content: ''; position: absolute; left: -10px; top: 3px; height: 15px; border-left: 2px solid #1989fa; }
|
||||
.association-card p { margin: 0; line-height: 26px; color: #85878d; overflow-wrap: anywhere; }
|
||||
.association-card label { color: #242424; }
|
||||
.association-card .association-pair { display: flex; gap: 10px; justify-content: space-between; }
|
||||
.association-pair span:last-child { flex-shrink: 0; }
|
||||
.association-card-footer { display: flex; align-items: center; gap: 8px; margin-top: 7px; padding-top: 7px; border-top: 1px solid #f5f5f5; }
|
||||
.association-state { flex: 1; min-width: 0; font-size: 12px; color: #b7790c; overflow-wrap: anywhere; }
|
||||
.association-card-footer .van-button { height: 25px; padding: 0 8px; font-size: 13px; border-radius: 6px; flex-shrink: 0; }
|
||||
.association-popup { display: flex; flex-direction: column; overflow: hidden; background: #f6f7f9; height: 100%; width: 100%; }
|
||||
.association-popup > .van-nav-bar { flex-shrink: 0; }
|
||||
.association-detail-body { flex: 1; overflow-y: auto; min-height: 0; padding-bottom: 12px; }
|
||||
.association-section { margin: 12px; padding: 10px 0; border-radius: 10px; background: white; overflow: hidden; }
|
||||
.association-section h3 { margin: 4px 14px 8px; font-size: 15px; color: #1768b0; }
|
||||
.association-section .van-cell { font-size: 13px; padding: 8px 14px; }
|
||||
.association-section .van-cell__title { flex: 0 0 88px; }
|
||||
.association-section .van-cell__value { white-space: pre-wrap; overflow-wrap: anywhere; }
|
||||
.association-section .association-file .van-cell__title { flex: 1; overflow-wrap: anywhere; }
|
||||
.association-section .association-file .van-cell__value { flex: 0 0 40px; }
|
||||
.association-content-image { display: block; max-width: calc(100% - 28px); margin: 10px 14px; }
|
||||
.association-description { padding: 0 14px; line-height: 1.8; white-space: pre-wrap; overflow-wrap: anywhere; }
|
||||
.association-muted { margin: 10px 14px; color: #969799; }
|
||||
.association-total .van-cell__value { color: #086bc3; font-weight: 600; }
|
||||
.association-sign-image { display: block; max-width: calc(100% - 28px); width: 240px; height: 100px; object-fit: contain; margin: 8px 14px; }
|
||||
.association-actions { flex-shrink: 0; display: flex; gap: 12px; padding: 10px 14px calc(10px + env(safe-area-inset-bottom)); background: white; border-top: 1px solid #eee; z-index: 2; }
|
||||
.association-actions .van-button { flex: 1; border-radius: 6px; height: 40px; }
|
||||
.association-section .association-sign-field { display: block; }
|
||||
.association-sign-field .van-field__label { width: auto; margin: 0 0 10px; }
|
||||
.association-sign-field .main-panel .van-image { width: 100%; height: 112px; min-height: 112px; overflow: hidden; border-radius: 6px; }
|
||||
.association-sign-field .main-panel .open-button { width: 100%; border-radius: 6px; }
|
||||
.association-preview { display: block; width: 100%; height: calc(100% - 46px); border: none; background: white; }
|
||||
.association-preview-image { width: 100%; height: calc(100% - 46px); object-fit: contain; }
|
||||
.association-loading { padding: 30px; text-align: center; }
|
||||
</style>
|
||||
<div id="app" class="association-page" v-cloak>
|
||||
<van-sticky>
|
||||
<van-nav-bar title="协会活动审核" left-arrow @click-left="back"></van-nav-bar>
|
||||
<van-dropdown-menu class="association-filters" active-color="#1867b0"
|
||||
:close-on-click-outside="false" :close-on-click-overlay="false">
|
||||
<van-dropdown-item ref="yearFilter" :options="yearOptions" :value="pageForm.year" :disabled="filterClosing"
|
||||
@change="selectFilter('year', $event)"
|
||||
@open="setFilterOpen('year', true)" @close="setFilterOpen('year', false)"></van-dropdown-item>
|
||||
<van-dropdown-item ref="clubIdFilter" :options="clubs" :value="pageForm.clubId" :disabled="filterClosing"
|
||||
@change="selectFilter('clubId', $event)"
|
||||
@open="setFilterOpen('clubId', true)" @close="setFilterOpen('clubId', false)">
|
||||
<van-loading v-if="clubsLoading" class="association-loading">加载协会中</van-loading>
|
||||
</van-dropdown-item>
|
||||
<van-dropdown-item ref="isAuditFilter" :options="auditOptions" :value="pageForm.isAudit" :disabled="filterClosing"
|
||||
@change="selectFilter('isAudit', $event)"
|
||||
@open="setFilterOpen('isAudit', true)" @close="setFilterOpen('isAudit', false)"></van-dropdown-item>
|
||||
</van-dropdown-menu>
|
||||
</van-sticky>
|
||||
<van-pull-refresh :value="refreshing" @input="setRefreshing" @refresh="doSearch">
|
||||
<van-list :value="tableLoading" :finished="finished" :error="listError" error-text="加载失败,点击重试"
|
||||
:finished-text="tableData.length ? '没有更多了' : ''" :immediate-check="false"
|
||||
@input="setTableLoading" @update:error="setListError" @load="pageData" class="association-list">
|
||||
<article v-for="item in tableData" :key="item.id" class="association-card">
|
||||
<h3>{{ item.activity_name }}</h3>
|
||||
<p><label>申请人:</label>{{ item.user_name }}<span v-if="item.bxr_loginname">({{ item.bxr_loginname }})</span></p>
|
||||
<p><label>所属协会:</label>{{ item.clubName || item.fundsUnitName || '—' }}</p>
|
||||
<p class="association-pair"><span><label>预算:</label>¥{{ money(item.funds_money) }}</span><span><label>申请:</label>{{ shortDate(item.apply_time) }}</span></p>
|
||||
<div class="association-card-footer">
|
||||
<span class="association-state" :style="{color: item.state_color || '#b7790c'}">{{ item.state_name || '待协会负责人审核' }}</span>
|
||||
<van-button type="info" size="small" :disabled="formLoading" @click="openDetail(item.id, false)">查看</van-button>
|
||||
<van-button v-if="Number(item.state_id) === 1000" type="primary" size="small" :disabled="formLoading" @click="openDetail(item.id, true)">审核</van-button>
|
||||
</div>
|
||||
</article>
|
||||
<van-empty v-if="!tableLoading && !listError && !tableData.length" description="暂无申请"></van-empty>
|
||||
</van-list>
|
||||
</van-pull-refresh>
|
||||
|
||||
<van-popup :value="popups.detail" position="right" class="association-popup" :close-on-popstate="false" :close-on-click-overlay="false">
|
||||
<van-nav-bar :title="auditMode ? '协会负责人审核' : '申请详情'" left-arrow @click-left="closePopup"></van-nav-bar>
|
||||
<div class="association-detail-body">
|
||||
<van-loading v-if="detailLoading" class="association-loading">加载申请中</van-loading>
|
||||
<van-empty v-else-if="!detail.id" description="详情加载失败">
|
||||
<van-button size="small" type="info" @click="loadDetail(detailId)">重新加载</van-button>
|
||||
</van-empty>
|
||||
<association-info v-else :data="detail" @preview="previewFile">
|
||||
<section v-if="auditMode && detail.canReview" class="association-section">
|
||||
<h3>审核信息</h3>
|
||||
<van-field label="审核人" :value="reviewerName" readonly></van-field>
|
||||
<van-field label="审核时间" :value="reviewDate" readonly></van-field>
|
||||
<van-field label="审核意见" :value="formData.idea" @input="setIdea" type="textarea" rows="3"
|
||||
maxlength="500" show-word-limit required placeholder="请填写审核意见" :disabled="formLoading"></van-field>
|
||||
<van-field label="审核签字" required class="association-sign-field">
|
||||
<template #input>
|
||||
<mobile-sign :value="formData.sign" @input="setSignature" :disabled="formLoading"
|
||||
:save-to-profile="false" :popup-visible="popups.sign" panel-title="审核签字"
|
||||
@open="openSignature" @close="closePopup"></mobile-sign>
|
||||
</template>
|
||||
</van-field>
|
||||
</section>
|
||||
</association-info>
|
||||
</div>
|
||||
<div v-if="detail.id && !detailLoading" class="association-actions">
|
||||
<template v-if="auditMode && detail.canReview">
|
||||
<van-button type="danger" plain :disabled="formLoading" @click="confirmReview(false)">拒绝</van-button>
|
||||
<van-button type="primary" :loading="formLoading" :disabled="formLoading" @click="confirmReview(true)">通过</van-button>
|
||||
</template>
|
||||
<van-button v-else-if="detail.canReview" type="primary" @click="startAudit">去审核</van-button>
|
||||
<van-button v-else @click="closePopup">关闭</van-button>
|
||||
</div>
|
||||
</van-popup>
|
||||
|
||||
<van-dialog :value="popups.confirm" :title="reviewFlag ? '确认通过' : '确认拒绝'"
|
||||
:message="reviewFlag ? '通过后将转交校工会负责人审核。' : '拒绝后将通知申请人,请核对审核意见。'"
|
||||
show-cancel-button :allow-html="false" :close-on-click-overlay="false" :close-on-popstate="false"
|
||||
:before-close="beforeReviewClose"></van-dialog>
|
||||
|
||||
<van-popup :value="popups.preview" position="right" class="association-popup" :close-on-popstate="false" :close-on-click-overlay="false">
|
||||
<van-nav-bar title="附件查看" left-arrow @click-left="closePopup"></van-nav-bar>
|
||||
<img v-if="popups.preview && previewIsImage" :src="previewUrl" class="association-preview-image" alt="申请附件">
|
||||
<iframe v-else-if="popups.preview" :src="previewUrl" title="申请附件" class="association-preview"></iframe>
|
||||
</van-popup>
|
||||
</div>
|
||||
<script>
|
||||
<!--#include('info.js'){}#-->
|
||||
<!--#include('page.js'){}#-->
|
||||
</script>
|
||||
<!--#
|
||||
}
|
||||
#-->
|
||||
@@ -0,0 +1,104 @@
|
||||
/** 协会活动申报详情:仅展示服务端已校验权限的数据,审核表单通过末尾插槽传入。 */
|
||||
const associationInfo = {
|
||||
template: /*language=HTML*/ `
|
||||
<div class="association-info">
|
||||
<section class="association-section">
|
||||
<h3>{{ data.activity_name }}</h3>
|
||||
<van-cell title="当前状态" :value="data.state_name"></van-cell>
|
||||
<van-cell title="申请人" :value="data.user_name + '(' + (data.loginname || '—') + ')'"></van-cell>
|
||||
<van-cell title="联系方式" :value="data.mobile || '—'"></van-cell>
|
||||
<van-cell title="申请时间" :value="data.apply_time || '—'"></van-cell>
|
||||
<van-cell title="承办协会" :value="data.clubName || data.fundsUnitName || '—'"></van-cell>
|
||||
<van-cell title="项目编号" :value="data.projectCode || '—'"></van-cell>
|
||||
<van-cell title="经费类型" :value="data.projectJfName || '—'"></van-cell>
|
||||
<van-cell title="计划时间" :value="(data.startPlannedDate || '—') + ' 至 ' + (data.endPlannedDate || '—')"></van-cell>
|
||||
<van-cell title="活动地点" :value="data.address || '—'"></van-cell>
|
||||
<van-cell title="活动人数" :value="data.activity_number || '—'"></van-cell>
|
||||
</section>
|
||||
<section class="association-section">
|
||||
<h3>活动方案及简介</h3>
|
||||
<div class="association-description">{{ activityText || '暂无' }}</div>
|
||||
<img v-for="(url, index) in activityImages" :key="index" :src="url" class="association-content-image"
|
||||
alt="活动方案配图" @click="$emit('preview', {imageUrl: url})">
|
||||
</section>
|
||||
<section class="association-section">
|
||||
<h3>预算明细</h3>
|
||||
<van-cell v-for="(item, index) in budgets" :key="index" :title="item.name || '费用项目'"
|
||||
:value="'¥' + money(item.budgetPrice)"></van-cell>
|
||||
<van-cell title="预算合计" :value="'¥' + money(data.funds_money)" class="association-total"></van-cell>
|
||||
</section>
|
||||
<section class="association-section">
|
||||
<h3>申请附件</h3>
|
||||
<van-cell v-for="(file, index) in files" :key="file.id || index" :title="file.filename || file.name || '附件'" class="association-file"
|
||||
value="查看" is-link @click="$emit('preview', file)"></van-cell>
|
||||
<p v-if="!files.length" class="association-muted">暂无附件</p>
|
||||
<h3>申请人签字</h3>
|
||||
<img v-if="data.us_s" class="association-sign-image" :src="signUrl(data.us_s)" alt="申请人签字">
|
||||
<p v-else class="association-muted">暂无签字</p>
|
||||
</section>
|
||||
<section v-for="audit in audits" :key="audit.key" class="association-section">
|
||||
<h3>{{ audit.title }}</h3>
|
||||
<van-cell title="审核人" :value="audit.name || '—'"></van-cell>
|
||||
<van-cell title="审核时间" :value="audit.time || '—'"></van-cell>
|
||||
<van-cell title="审核意见" :value="audit.idea || '暂无'"></van-cell>
|
||||
<img v-if="audit.sign" class="association-sign-image" :src="signUrl(audit.sign)" :alt="audit.title + '签字'">
|
||||
</section>
|
||||
<slot></slot>
|
||||
</div>
|
||||
`,
|
||||
props: {
|
||||
data: {type: Object, default: () => ({})}
|
||||
},
|
||||
computed: {
|
||||
budgets() {
|
||||
return this.asList(this.data.budgets)
|
||||
},
|
||||
files() {
|
||||
return this.asList(this.data.files)
|
||||
},
|
||||
activityText() {
|
||||
// 申请内容来自富文本,按段落提取文本,避免在审核页面执行其中的脚本或事件属性。
|
||||
const container = document.createElement('div')
|
||||
container.innerHTML = (this.data.activity_content || '').replace(/<\/(p|div|li)>/gi, '\n')
|
||||
container.querySelectorAll('script,style').forEach((node) => node.remove())
|
||||
return container.textContent.trim()
|
||||
},
|
||||
activityImages() {
|
||||
// 保留富文本中的方案配图,只提取图片地址,不执行原文中的 HTML 属性和事件。
|
||||
const content = new DOMParser().parseFromString(this.data.activity_content || '', 'text/html')
|
||||
return Array.from(content.querySelectorAll('img')).map((img) => img.getAttribute('src') || '')
|
||||
.filter((url) => /^(https?:\/\/|\/(?!\/)|data:image\/(?:png|jpeg|gif|webp);base64,)/i.test(url))
|
||||
},
|
||||
audits() {
|
||||
const data = this.data
|
||||
return [
|
||||
{key: 'association', title: '协会负责人审核', id: data.association_principal_id,
|
||||
name: data.ap_username, time: data.association_principal_time, idea: data.association_principal_idea, sign: data.ap_s},
|
||||
{key: 'accounting', title: '校工会负责人审核', id: data.accounting_name,
|
||||
name: data.a_username, time: data.accounting_time, idea: data.accounting_idea, sign: data.a_s},
|
||||
{key: 'union', title: '校工会主席审核', id: data.union_chairman_name,
|
||||
name: data.ucc_username, time: data.union_chairman_time, idea: data.union_chairman_idea, sign: data.ucc_s}
|
||||
].filter((audit) => audit.id)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
/** 历史接口可能返回 JSON 字符串或数组;空值、异常数据均不阻断整页展示。 */
|
||||
asList(value) {
|
||||
if (Array.isArray(value)) return value
|
||||
if (!value) return []
|
||||
try {
|
||||
const list = JSON.parse(value)
|
||||
return Array.isArray(list) ? list : []
|
||||
} catch (error) {
|
||||
return []
|
||||
}
|
||||
},
|
||||
money(value) {
|
||||
return value == null || !Number.isFinite(Number(value)) ? '—' : Number(value).toLocaleString('zh-CN', {minimumFractionDigits: 2, maximumFractionDigits: 2})
|
||||
},
|
||||
/** 签字兼容 PC 保存的 Base64 图片和历史文件路径。 */
|
||||
signUrl(value) {
|
||||
return value && value.startsWith('data:image/') ? value : '/signature/getSignData?path=' + encodeURIComponent(value || '')
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,272 @@
|
||||
// 独立手机审核页面;不复用旧 declare 页面及其有缺陷的滚动分页。
|
||||
const vue = new Vue({
|
||||
el: '#app',
|
||||
components: {'association-info': associationInfo},
|
||||
data() {
|
||||
return {
|
||||
baseUrl: '/platform/jf/activity_bx/association',
|
||||
pageForm: {year: new Date().getFullYear(), clubId: '', isAudit: 3, pageNumber: 1, pageSize: 10},
|
||||
tableData: [], tableLoading: false, refreshing: false, finished: false, listError: false,
|
||||
listSerial: 0, listBusy: false, detailSerial: 0,
|
||||
clubs: [{text: '全部协会', value: ''}], clubsLoading: false,
|
||||
auditOptions: [{text: '未审核', value: 3}, {text: '已审核', value: 2}, {text: '全部', value: 1}],
|
||||
filterOpen: {year: false, clubId: false, isAudit: false}, filterClosing: false,
|
||||
popups: {filter: false, detail: false, sign: false, confirm: false, preview: false},
|
||||
detail: {}, detailId: '', detailLoading: false, auditMode: false,
|
||||
formData: {id: '', idea: '', sign: ''}, formLoading: false, reviewFlag: true,
|
||||
reviewerName: '', reviewDate: '', previewUrl: '', previewIsImage: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
yearOptions() {
|
||||
return Array.from({length: 11}, (_, index) => {
|
||||
const year = new Date().getFullYear() - index
|
||||
return {text: year + '年', value: year}
|
||||
})
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
money(value) {
|
||||
return value == null || !Number.isFinite(Number(value)) ? '—' : Number(value).toLocaleString('zh-CN', {minimumFractionDigits: 2, maximumFractionDigits: 2})
|
||||
},
|
||||
shortDate(value) {
|
||||
return value ? String(value).slice(5, 10) : '—'
|
||||
},
|
||||
setRefreshing(value) { this.refreshing = value },
|
||||
setTableLoading(value) { this.tableLoading = value },
|
||||
setListError(value) { this.listError = value },
|
||||
setIdea(value) { this.$set(this.formData, 'idea', value) },
|
||||
/** 筛选切换或提交后,从第一页替换数据;旧请求晚到时不能混入当前筛选结果。 */
|
||||
doSearch() {
|
||||
this.listSerial++
|
||||
this.listBusy = false
|
||||
this.$set(this.pageForm, 'pageNumber', 1)
|
||||
this.tableData = []
|
||||
this.finished = false
|
||||
this.listError = false
|
||||
this.pageData()
|
||||
},
|
||||
/** 列表返回 data.list 和 totalCount;失败保留页码供重试,成功后才推进下一页。 */
|
||||
pageData() {
|
||||
if (this.listBusy || this.finished) return
|
||||
const serial = this.listSerial
|
||||
this.listBusy = true
|
||||
this.tableLoading = true
|
||||
this.listError = false
|
||||
$.post(this.baseUrl + '/pageData', Object.assign({}, this.pageForm))
|
||||
.then((res) => {
|
||||
if (serial !== this.listSerial || this._isDestroyed) return
|
||||
if (res.code !== 0 || !res.data) {
|
||||
this.listError = true
|
||||
this.$toast(res.msg || '列表加载失败')
|
||||
return
|
||||
}
|
||||
const list = res.data.list || []
|
||||
this.tableData = this.tableData.concat(list)
|
||||
this.finished = !list.length || this.tableData.length >= Number(res.data.totalCount)
|
||||
this.$set(this.pageForm, 'pageNumber', this.pageForm.pageNumber + 1)
|
||||
})
|
||||
.fail(() => {
|
||||
if (serial === this.listSerial && !this._isDestroyed) this.listError = true
|
||||
})
|
||||
.always(() => {
|
||||
if (serial !== this.listSerial || this._isDestroyed) return
|
||||
this.listBusy = false
|
||||
this.tableLoading = false
|
||||
this.refreshing = false
|
||||
})
|
||||
},
|
||||
loadClubs() {
|
||||
this.clubsLoading = true
|
||||
$.post(this.baseUrl + '/clubs')
|
||||
.then((res) => {
|
||||
if (res.code === 0) {
|
||||
this.clubs = [{text: '全部协会', value: ''}].concat((res.data || []).map((club) => ({text: club.name, value: club.id})))
|
||||
} else {
|
||||
this.$toast(res.msg || '协会加载失败')
|
||||
}
|
||||
})
|
||||
.fail(() => this.$toast('协会加载失败,请重新打开筛选'))
|
||||
.always(() => { this.clubsLoading = false })
|
||||
},
|
||||
/** key 为年度/协会/状态字段,open 为菜单展开状态;三个菜单切换时共用一层历史。 */
|
||||
setFilterOpen(key, open) {
|
||||
this.$set(this.filterOpen, key, open)
|
||||
if (open && key === 'clubId' && this.clubs.length === 1 && !this.clubsLoading) this.loadClubs()
|
||||
// Vant 切换菜单会同时触发旧菜单 close 和新菜单 open,等本轮更新结束后统一判断。
|
||||
this.$nextTick(() => {
|
||||
if (this._isDestroyed || this.filterClosing) return
|
||||
if (Object.values(this.filterOpen).some(Boolean)) {
|
||||
if (!this.popups.filter) this.$options.popupSession.open('filter')
|
||||
} else if (this.popups.filter) {
|
||||
this.filterClosing = true
|
||||
this.$options.popupSession.close().then(() => { this.filterClosing = false })
|
||||
}
|
||||
})
|
||||
},
|
||||
/** key 为筛选字段,value 为对应选项值;仅值改变时刷新,菜单收起事件负责回退历史。 */
|
||||
selectFilter(key, value) {
|
||||
this.$set(this.pageForm, key, value)
|
||||
this.doSearch()
|
||||
},
|
||||
/** 每次打开申请清空上次的意见和签字,防止在不同申请之间串用审核数据。 */
|
||||
openDetail(id, audit) {
|
||||
if (this.formLoading) return
|
||||
this.detailId = id
|
||||
this.detail = {}
|
||||
this.auditMode = audit
|
||||
this.formData = {id, idea: '', sign: ''}
|
||||
this.reviewDate = moment().format('YYYY-MM-DD')
|
||||
this.$options.popupSession.open('detail')
|
||||
this.loadDetail(id)
|
||||
},
|
||||
loadDetail(id) {
|
||||
const serial = ++this.detailSerial
|
||||
this.detailLoading = true
|
||||
$.post(this.baseUrl + '/findOne', {id})
|
||||
.then((res) => {
|
||||
if (serial !== this.detailSerial || !this.popups.detail || this._isDestroyed) return
|
||||
if (res.code === 0 && res.data) {
|
||||
this.detail = res.data
|
||||
if (!this.detail.canReview) this.auditMode = false
|
||||
} else {
|
||||
this.$toast(res.msg || '详情加载失败')
|
||||
}
|
||||
})
|
||||
.fail(() => {
|
||||
if (serial === this.detailSerial && !this._isDestroyed) this.$toast('详情加载失败,请重试')
|
||||
})
|
||||
.always(() => {
|
||||
if (serial === this.detailSerial && !this._isDestroyed) this.detailLoading = false
|
||||
})
|
||||
},
|
||||
startAudit() {
|
||||
this.auditMode = true
|
||||
this.$nextTick(() => {
|
||||
const body = this.$el.querySelector('.association-detail-body')
|
||||
if (body) body.scrollTop = body.scrollHeight
|
||||
})
|
||||
},
|
||||
openSignature() {
|
||||
if (!this.formLoading) this.$options.popupSession.open('sign')
|
||||
},
|
||||
/** signature 是 mobile-sign 回传的本次签字 Base64,随审核请求一起保存。 */
|
||||
setSignature(signature) {
|
||||
this.$set(this.formData, 'sign', signature)
|
||||
},
|
||||
confirmReview(flag) {
|
||||
if (this.formLoading || !this.detail.canReview) return
|
||||
if (!this.formData.idea.trim() || !this.formData.sign) {
|
||||
this.$toast('请填写审核意见并签字')
|
||||
return
|
||||
}
|
||||
this.reviewFlag = flag
|
||||
this.$options.popupSession.open('confirm')
|
||||
},
|
||||
/** Vant 用 done 结束按钮 loading;显隐仍由历史栈驱动,避免组件自行关闭后留下历史层。 */
|
||||
beforeReviewClose(action, done) {
|
||||
if (this.formLoading) {
|
||||
done(false)
|
||||
return
|
||||
}
|
||||
if (action === 'confirm') {
|
||||
this.submitReview().always(() => done(false))
|
||||
} else {
|
||||
this.$options.popupSession.close().then(() => done(false))
|
||||
}
|
||||
},
|
||||
/** id/idea/sign/flag 与 PC 共用;审核时间和人员由服务器取值,不信任前端身份字段。 */
|
||||
submitReview() {
|
||||
if (this.formLoading) return
|
||||
this.formLoading = true
|
||||
return $.post(this.baseUrl + '/doReview', Object.assign({}, this.formData, {flag: this.reviewFlag}))
|
||||
.then((res) => {
|
||||
if (this._isDestroyed) return
|
||||
if (res.code === 0) {
|
||||
this.$toast.success('审核成功')
|
||||
this.$options.popupSession.clear().then(() => {
|
||||
this.formData = {id: '', idea: '', sign: ''}
|
||||
this.doSearch()
|
||||
})
|
||||
} else {
|
||||
this.$toast(res.msg || '审核失败')
|
||||
// 关闭确认层后重读状态;若已被 PC 处理则切换为只读,保留未提交的意见便于核对。
|
||||
if (this.popups.confirm) this.$options.popupSession.close().then(() => this.loadDetail(this.detailId))
|
||||
}
|
||||
})
|
||||
.fail(() => this.$toast('提交失败,请检查网络后重试'))
|
||||
.always(() => { this.formLoading = false })
|
||||
},
|
||||
/** 图片使用文件流,文档沿用项目预览接口;预览自身同样占一层历史。 */
|
||||
previewFile(file) {
|
||||
if (file.imageUrl) {
|
||||
this.previewIsImage = true
|
||||
this.previewUrl = file.imageUrl
|
||||
this.$options.popupSession.open('preview')
|
||||
return
|
||||
}
|
||||
if (!file.id) {
|
||||
this.$toast('附件信息不完整')
|
||||
return
|
||||
}
|
||||
this.previewIsImage = /\.(png|jpe?g|gif|webp|bmp)$/i.test(file.filename || file.filepath || '')
|
||||
const streamUrl = '/file_server/fileStreamPreview?id=' + encodeURIComponent(file.id)
|
||||
this.previewUrl = this.previewIsImage
|
||||
? streamUrl
|
||||
: '/assets/platform/plugins/pdfJs/web/viewer.html?file=' + encodeURIComponent(streamUrl)
|
||||
this.$options.popupSession.open('preview')
|
||||
},
|
||||
closePopup() {
|
||||
if (this.formLoading) {
|
||||
this.$toast('正在提交,请稍候')
|
||||
return
|
||||
}
|
||||
this.$options.popupSession.close()
|
||||
},
|
||||
back() {
|
||||
if (Object.values(this.popups).some(Boolean)) {
|
||||
this.closePopup()
|
||||
return
|
||||
}
|
||||
this.$options.popupSession.clear().then(() => {
|
||||
this.$destroy()
|
||||
pjaxReplace('/mobile/index')
|
||||
})
|
||||
}
|
||||
},
|
||||
created() {
|
||||
// 仅注册状态同步回调;popstate 由公共管理器统一监听。
|
||||
this.$options.popupSession = window.mobilePopupHistory.register('associationAudit', (layers) => {
|
||||
Object.keys(this.popups).forEach((name) => this.$set(this.popups, name, layers.includes(name)))
|
||||
// 系统返回或页面返回先收起下拉菜单;由公共历史栈驱动,页面不另绑 popstate。
|
||||
if (!layers.includes('filter')) {
|
||||
Object.keys(this.filterOpen).forEach((key) => {
|
||||
this.$set(this.filterOpen, key, false)
|
||||
const dropdown = this.$refs[key + 'Filter']
|
||||
if (dropdown) dropdown.toggle(false)
|
||||
})
|
||||
}
|
||||
})
|
||||
const user = JSON.parse(window.sessionStorage.getItem('user') || '{}')
|
||||
this.reviewerName = user.username || ''
|
||||
this.loadClubs()
|
||||
this.doSearch()
|
||||
const query = new URLSearchParams(window.location.search)
|
||||
const id = query.get('id')
|
||||
if (id) this.openDetail(id, query.get('mode') === 'audit')
|
||||
// PJAX 不会自动销毁旧 Vue 实例,在替换容器前注销页面回调。
|
||||
$(document).on('pjax:beforeReplace.associationAudit', () => this.$destroy())
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.listSerial++
|
||||
this.detailSerial++
|
||||
const session = this.$options.popupSession
|
||||
if (Object.values(this.popups).some(Boolean)) {
|
||||
session.clear().then(() => session.dispose())
|
||||
} else {
|
||||
// PJAX 替换后可能立即创建新页面;空栈必须同步注销,不能等 Promise 回调再释放注册。
|
||||
session.dispose()
|
||||
}
|
||||
$(document).off('pjax:beforeReplace.associationAudit')
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,18 @@
|
||||
<!--#
|
||||
layout("/mobile/platform.html"){
|
||||
#-->
|
||||
<!--#include('/mobile/jf/activitybx/review/index.html'){}#-->
|
||||
<script>
|
||||
// 节点配置由当前页面固定提供,业务权限及状态仍由服务端校验。
|
||||
const activityReviewConfig = {
|
||||
stage: 'division', title: '基层工会主席审核', state: 1010,
|
||||
confirmMessage: '通过后将转交校工会负责人审核。',
|
||||
canChooseUnion: ${@shiro.hasRole('sysadmin')||@shiro.hasRole('A06')||@shiro.hasRole('H03')},
|
||||
currentUnionId: '${@io.v.nutz.base.utils.Vi.getUnionId()}'
|
||||
}
|
||||
<!--#include('/mobile/jf/activitybx/review/info.js'){}#-->
|
||||
<!--#include('/mobile/jf/activitybx/review/page.js'){}#-->
|
||||
</script>
|
||||
<!--#
|
||||
}
|
||||
#-->
|
||||
@@ -0,0 +1,223 @@
|
||||
<style>
|
||||
.review-page { min-height: 100vh; background: #f6f7f9; color: #323233; font-size: 13px; }
|
||||
.review-page .van-nav-bar__title { font-size: 15px; font-weight: 400; }
|
||||
.review-filters .van-dropdown-menu__bar { box-shadow: none; border-top: 1px solid #f8f8f8; border-bottom: 1px solid #f8f8f8; }
|
||||
.review-filters .van-dropdown-menu__item { flex: 1; min-width: 0; }
|
||||
.review-filters .van-dropdown-menu__title { max-width: calc(100% - 40px); padding: 0 8px; font-size: 14px; }
|
||||
.review-filters .van-dropdown-item__content { max-height: 70vh; border-radius: 0 0 15px 15px; }
|
||||
.review-list { padding: 2px 0 12px; }
|
||||
.review-card { margin: 12px 13px 0; padding: 11px 12px 10px; background: white; border-radius: 10px; box-shadow: 0 5px 12px rgba(30,50,70,.04); }
|
||||
.review-card h3 { position: relative; margin: 0 0 7px; color: #086bc3; font-size: 15px; line-height: 21px; font-weight: 600; overflow-wrap: anywhere; }
|
||||
.review-card h3:before { content: ''; position: absolute; left: -10px; top: 3px; height: 15px; border-left: 2px solid #1989fa; }
|
||||
.review-card p { margin: 0; line-height: 26px; color: #85878d; overflow-wrap: anywhere; }
|
||||
.review-card label { color: #242424; }
|
||||
.review-card .review-pair { display: flex; gap: 10px; justify-content: space-between; }
|
||||
.review-pair span:last-child { flex-shrink: 0; }
|
||||
.review-card-footer { display: flex; align-items: center; gap: 8px; margin-top: 7px; padding-top: 7px; border-top: 1px solid #f5f5f5; }
|
||||
.review-state { flex: 1; min-width: 0; font-size: 12px; color: #b7790c; overflow-wrap: anywhere; }
|
||||
.review-card-footer .van-button { height: 25px; padding: 0 8px; font-size: 13px; border-radius: 6px; flex-shrink: 0; }
|
||||
.review-popup { display: flex; flex-direction: column; overflow: hidden; background: #f6f7f9; height: 100%; width: 100%; }
|
||||
.review-popup > .van-nav-bar { flex-shrink: 0; }
|
||||
.review-detail-body { flex: 1; overflow-y: auto; min-height: 0; padding-bottom: 12px; }
|
||||
.review-section { margin: 12px; padding: 10px 0; border-radius: 10px; background: white; overflow: hidden; }
|
||||
.review-section h3 { margin: 4px 14px 8px; font-size: 15px; color: #1768b0; }
|
||||
.review-section .van-cell { font-size: 13px; padding: 8px 14px; }
|
||||
.review-section .van-cell__title { flex: 0 0 88px; }
|
||||
.review-section .van-cell__value { white-space: pre-wrap; overflow-wrap: anywhere; }
|
||||
.review-section .review-file .van-cell__title { flex: 1; overflow-wrap: anywhere; }
|
||||
.review-section .review-file .van-cell__value { flex: 0 0 40px; }
|
||||
.review-content-image { display: block; max-width: calc(100% - 28px); margin: 10px 14px; }
|
||||
.review-description { padding: 0 14px; line-height: 1.8; white-space: pre-wrap; overflow-wrap: anywhere; }
|
||||
.review-muted { margin: 10px 14px; color: #969799; }
|
||||
.review-total .van-cell__value { color: #086bc3; font-weight: 600; }
|
||||
.review-sign-image { display: block; max-width: calc(100% - 28px); width: 240px; height: 100px; object-fit: contain; margin: 8px 14px; }
|
||||
.review-actions { flex-shrink: 0; display: flex; gap: 12px; padding: 10px 14px calc(10px + env(safe-area-inset-bottom)); background: white; border-top: 1px solid #eee; z-index: 2; }
|
||||
.review-actions .van-button { flex: 1; border-radius: 6px; height: 40px; }
|
||||
.review-section .review-sign-field { display: block; }
|
||||
.review-sign-field .van-field__label { width: auto; margin: 0 0 10px; }
|
||||
.review-sign-field .main-panel .van-image { width: 100%; height: 112px; min-height: 112px; overflow: hidden; border-radius: 6px; }
|
||||
.review-sign-field .main-panel .open-button { width: 100%; border-radius: 6px; }
|
||||
.review-preview { display: block; width: 100%; height: calc(100% - 46px); border: none; background: white; }
|
||||
.review-preview-image { width: 100%; height: calc(100% - 46px); object-fit: contain; }
|
||||
.review-loading { padding: 30px; text-align: center; }
|
||||
.review-more-bar { display: flex; justify-content: space-between; align-items: center; gap: 8px; padding: 7px 13px; background: white; color: #969799; font-size: 12px; }
|
||||
.review-more-bar > span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.review-more-popup, .review-choice-popup { display: flex; flex-direction: column; max-height: 80vh; background: #f7f8fa; }
|
||||
.review-choice-popup { height: 60vh; }
|
||||
.review-more-body { flex: 1; overflow-y: auto; min-height: 0; }
|
||||
/* 筛选面板独立限定样式,避免影响详情、审核按钮和其他公共弹框。 */
|
||||
.review-more-popup { background: #fff; border-radius: 18px 18px 0 0; overflow: hidden; }
|
||||
.review-more-handle { flex-shrink: 0; width: 38px; height: 4px; margin: 8px auto 0; border-radius: 4px; background: #d5d8df; }
|
||||
.review-more-header { display: flex; align-items: center; justify-content: space-between; flex-shrink: 0; padding: 4px 12px 8px 18px; }
|
||||
.review-more-header h2 { margin: 0; font-size: 18px; line-height: 32px; font-weight: 600; color: #20242d; }
|
||||
.review-more-close.van-button { width: 36px; height: 36px; padding: 0; border: 0; color: #92969f; background: transparent; font-size: 20px; }
|
||||
.review-more-popup > .review-more-body { flex: 0 1 auto; padding: 0 18px 14px; }
|
||||
.review-filter-group + .review-filter-group { margin-top: 16px; }
|
||||
.review-filter-label { margin: 0 0 8px; color: #252a34; font-size: 14px; line-height: 20px; font-weight: 600; }
|
||||
.review-filter-row { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 8px; }
|
||||
.review-filter-row .review-filter-label { margin: 0; }
|
||||
.review-unit-select.van-cell { align-items: center; padding: 8px 12px; border-radius: 8px; background: #f5f6f8; font-size: 13px; }
|
||||
.review-unit-select .van-cell__title { min-width: 0; overflow-wrap: anywhere; }
|
||||
.review-unit-select:after, .review-filter-input:after { display: none; }
|
||||
.review-filter-input.van-cell { padding: 8px 12px; border-radius: 8px; background: #f5f6f8; font-size: 13px; }
|
||||
.review-filter-input .van-field__left-icon { color: #969ba4; margin-right: 8px; }
|
||||
.review-segment { display: flex; flex-shrink: 0; gap: 2px; padding: 2px; border-radius: 8px; background: #f5f6f8; }
|
||||
.review-filter-option.van-button { min-width: 0; height: 30px; padding: 0 8px; border: 1px solid transparent; border-radius: 7px; background: #f5f6f8; color: #333943; font-size: 12px; }
|
||||
.review-segment .review-filter-option { min-width: 58px; height: 28px; }
|
||||
.review-filter-option.is-selected { background: #eaf3ff; border-color: #1989fa; color: #1989fa; font-weight: 500; }
|
||||
.review-segment .review-filter-option.is-selected { border-color: transparent; }
|
||||
.review-sort-options { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 6px; }
|
||||
.review-sort-options .review-filter-option { position: relative; width: 100%; height: 30px; padding: 0 4px; white-space: nowrap; }
|
||||
.review-sort-options .is-selected { padding-right: 15px; }
|
||||
.review-sort-check { position: absolute; right: 3px; top: 50%; transform: translateY(-50%); font-size: 12px; }
|
||||
.review-sort-direction { margin: 14px 0 0; }
|
||||
.review-sort-direction .review-segment { background: transparent; gap: 6px; padding: 0; }
|
||||
.review-more-footer { display: flex; flex-shrink: 0; gap: 10px; padding: 10px 16px calc(10px + env(safe-area-inset-bottom)); border-top: 1px solid #f1f2f5; background: #fff; }
|
||||
.review-more-footer .van-button { flex: 1; height: 40px; border-radius: 8px; font-size: 14px; }
|
||||
.review-more-footer .van-button--info { flex: 2; }
|
||||
.review-more-footer .van-button--default { color: #60656e; border-color: #e4e7ed; }
|
||||
.review-choice-popup { background: #fff; }
|
||||
.review-choice-popup .van-search { padding: 8px 16px; }
|
||||
.review-choice-popup .van-search__content { border-radius: 8px; background: #f5f6f8; }
|
||||
.review-choice-popup .van-cell { padding: 12px 18px; font-size: 13px; }
|
||||
.review-choice-popup .review-choice-selected { color: #1989fa; background: #f0f6ff; }
|
||||
.review-budget-item + .review-budget-item { border-top: 1px solid #f2f3f5; }
|
||||
</style>
|
||||
<div id="app" class="review-page" v-cloak>
|
||||
<van-sticky>
|
||||
<van-nav-bar :title="config.title" left-arrow @click-left="back"></van-nav-bar>
|
||||
<van-dropdown-menu class="review-filters" active-color="#1867b0"
|
||||
:close-on-click-outside="false" :close-on-click-overlay="false">
|
||||
<van-dropdown-item ref="yearFilter" :options="yearOptions" :value="pageForm.year" :disabled="filterClosing"
|
||||
@change="selectFilter('year', $event)"
|
||||
@open="setFilterOpen('year', true)" @close="setFilterOpen('year', false)"></van-dropdown-item>
|
||||
<van-dropdown-item ref="organizationFilter" :options="organizationOptions" :value="pageForm[organizationKey]" :disabled="filterClosing"
|
||||
@change="selectFilter(organizationKey, $event)"
|
||||
@open="setFilterOpen('organization', true)" @close="setFilterOpen('organization', false)">
|
||||
<van-loading v-if="organizationsLoading" class="review-loading">加载中</van-loading>
|
||||
</van-dropdown-item>
|
||||
<van-dropdown-item ref="isAuditFilter" :options="auditOptions" :value="pageForm.isAudit" :disabled="filterClosing"
|
||||
@change="selectFilter('isAudit', $event)"
|
||||
@open="setFilterOpen('isAudit', true)" @close="setFilterOpen('isAudit', false)"></van-dropdown-item>
|
||||
</van-dropdown-menu>
|
||||
<div class="review-more-bar"><span>{{ searchSummary }}</span><van-button size="mini" plain type="info" @click="openMore">更多筛选</van-button></div>
|
||||
</van-sticky>
|
||||
<van-pull-refresh :value="refreshing" @input="setRefreshing" @refresh="doSearch">
|
||||
<van-list :value="tableLoading" :finished="finished" :error="listError" error-text="加载失败,点击重试"
|
||||
:finished-text="tableData.length ? '没有更多了' : ''" :immediate-check="false"
|
||||
@input="setTableLoading" @update:error="setListError" @load="pageData" class="review-list">
|
||||
<article v-for="item in tableData" :key="item.id" class="review-card">
|
||||
<h3>{{ item.activity_name }}</h3>
|
||||
<p><label>申请人:</label>{{ item.user_name }}<span v-if="item.bxr_loginname">({{ item.bxr_loginname }})</span></p>
|
||||
<p><label>所属工会:</label>{{ item.bxr_unionname || item.fundsUnitName || '—' }}</p>
|
||||
<p class="review-pair"><span><label>预算:</label>¥{{ money(item.funds_money) }}</span><span><label>申请:</label>{{ shortDate(item.apply_time) }}</span></p>
|
||||
<div class="review-card-footer">
|
||||
<span class="review-state" :style="{color: item.state_color || '#b7790c'}">{{ item.state_name || config.title }}</span>
|
||||
<van-button type="info" size="small" :disabled="formLoading" @click="openDetail(item.id, false)">查看</van-button>
|
||||
<van-button v-if="Number(item.state_id) === config.state" type="primary" size="small" :disabled="formLoading" @click="openDetail(item.id, true)">审核</van-button>
|
||||
</div>
|
||||
</article>
|
||||
<van-empty v-if="!tableLoading && !listError && !tableData.length" description="暂无申请"></van-empty>
|
||||
</van-list>
|
||||
</van-pull-refresh>
|
||||
|
||||
<van-popup :value="popups.detail" position="right" class="review-popup" :close-on-popstate="false" :close-on-click-overlay="false">
|
||||
<van-nav-bar :title="auditMode ? config.title : '申请详情'" left-arrow @click-left="closePopup"></van-nav-bar>
|
||||
<div class="review-detail-body">
|
||||
<van-loading v-if="detailLoading" class="review-loading">加载申请中</van-loading>
|
||||
<van-empty v-else-if="!detail.id" description="详情加载失败">
|
||||
<van-button size="small" type="info" @click="loadDetail(detailId)">重新加载</van-button>
|
||||
</van-empty>
|
||||
<review-info v-else :data="detail" @preview="previewFile">
|
||||
<section v-if="auditMode && detail.canReview" class="review-section">
|
||||
<h3>审核信息</h3>
|
||||
<van-field label="审核人" :value="reviewerName" readonly></van-field>
|
||||
<van-field label="审核时间" :value="reviewDate" readonly></van-field>
|
||||
<van-field label="审核意见" :value="formData.idea" @input="setIdea" type="textarea" rows="3"
|
||||
maxlength="500" show-word-limit required placeholder="请填写审核意见" :disabled="formLoading"></van-field>
|
||||
<van-field label="审核签字" required class="review-sign-field">
|
||||
<template #input>
|
||||
<mobile-sign :value="formData.sign" @input="setSignature" :disabled="formLoading"
|
||||
:save-to-profile="false" :popup-visible="popups.sign" panel-title="审核签字"
|
||||
@open="openSignature" @close="closePopup"></mobile-sign>
|
||||
</template>
|
||||
</van-field>
|
||||
</section>
|
||||
</review-info>
|
||||
</div>
|
||||
<div v-if="detail.id && !detailLoading" class="review-actions">
|
||||
<template v-if="auditMode && detail.canReview">
|
||||
<van-button type="danger" plain :disabled="formLoading" @click="confirmReview(false)">拒绝</van-button>
|
||||
<van-button type="primary" :loading="formLoading" :disabled="formLoading" @click="confirmReview(true)">通过</van-button>
|
||||
</template>
|
||||
<van-button v-else-if="detail.canReview" type="primary" @click="startAudit">去审核</van-button>
|
||||
<van-button v-else @click="closePopup">关闭</van-button>
|
||||
</div>
|
||||
</van-popup>
|
||||
|
||||
<van-dialog :value="popups.confirm" :title="reviewFlag ? '确认通过' : '确认拒绝'"
|
||||
:message="reviewFlag ? config.confirmMessage : '拒绝后将通知申请人,请核对审核意见。'"
|
||||
show-cancel-button :allow-html="false" :close-on-click-overlay="false" :close-on-popstate="false"
|
||||
:before-close="beforeReviewClose"></van-dialog>
|
||||
|
||||
<van-popup :value="popups.preview" position="right" class="review-popup" :close-on-popstate="false" :close-on-click-overlay="false">
|
||||
<van-nav-bar title="附件查看" left-arrow @click-left="closePopup"></van-nav-bar>
|
||||
<img v-if="popups.preview && previewIsImage" :src="previewUrl" class="review-preview-image" alt="申请附件">
|
||||
<iframe v-else-if="popups.preview" :src="previewUrl" title="申请附件" class="review-preview"></iframe>
|
||||
</van-popup>
|
||||
<van-popup :value="popups.more" position="bottom" round class="review-more-popup" :close-on-popstate="false" :close-on-click-overlay="false">
|
||||
<div class="review-more-handle" aria-hidden="true"></div>
|
||||
<header class="review-more-header">
|
||||
<h2>更多筛选</h2>
|
||||
<van-button icon="cross" class="review-more-close" aria-label="关闭更多筛选" @click="closePopup"></van-button>
|
||||
</header>
|
||||
<div class="review-more-body">
|
||||
<section v-if="config.canChooseUnion" class="review-filter-group">
|
||||
<h3 class="review-filter-label">所属单位</h3>
|
||||
<van-cell :title="optionText('unitId', moreForm.unitId)" class="review-unit-select" is-link @click="openChoice('unitId')"></van-cell>
|
||||
</section>
|
||||
<section class="review-filter-group">
|
||||
<div class="review-filter-row">
|
||||
<h3 class="review-filter-label">查询人员</h3>
|
||||
<div class="review-segment" role="group" aria-label="查询类型">
|
||||
<van-button v-for="option in choiceOptions.searchName" :key="option.value" class="review-filter-option"
|
||||
:class="{'is-selected': moreForm.searchName === option.value}" :aria-pressed="moreForm.searchName === option.value"
|
||||
@click="setMoreField('searchName', option.value)">{{ option.text }}</van-button>
|
||||
</div>
|
||||
</div>
|
||||
<van-field class="review-filter-input" left-icon="search" :value="moreForm.searchKeyword"
|
||||
@input="setMoreField('searchKeyword', $event)" maxlength="100" clearable
|
||||
:placeholder="moreSearchPlaceholder" :aria-label="moreSearchPlaceholder"></van-field>
|
||||
</section>
|
||||
<section class="review-filter-group">
|
||||
<h3 class="review-filter-label">排序方式</h3>
|
||||
<div class="review-sort-options" role="group" aria-label="排序字段">
|
||||
<van-button v-for="option in choiceOptions.pageOrderName" :key="option.value" class="review-filter-option"
|
||||
:class="{'is-selected': moreForm.pageOrderName === option.value}" :aria-pressed="moreForm.pageOrderName === option.value"
|
||||
@click="setMoreField('pageOrderName', option.value)">
|
||||
{{ option.text }}<van-icon v-if="moreForm.pageOrderName === option.value" name="success" class="review-sort-check"></van-icon>
|
||||
</van-button>
|
||||
</div>
|
||||
<div v-if="moreForm.pageOrderName" class="review-filter-row review-sort-direction">
|
||||
<h3 class="review-filter-label">排列顺序</h3>
|
||||
<div class="review-segment" role="group" aria-label="排列顺序">
|
||||
<van-button v-for="option in choiceOptions.pageOrderBy" :key="option.value" class="review-filter-option"
|
||||
:class="{'is-selected': moreForm.pageOrderBy === option.value}" :aria-pressed="moreForm.pageOrderBy === option.value"
|
||||
@click="setMoreField('pageOrderBy', option.value)">{{ option.text }}</van-button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
<div class="review-more-footer"><van-button @click="resetMore">重置</van-button><van-button type="info" @click="applyMore">确定筛选</van-button></div>
|
||||
</van-popup>
|
||||
<van-popup :value="popups.choice" position="bottom" round class="review-choice-popup" :close-on-popstate="false" :close-on-click-overlay="false">
|
||||
<van-nav-bar :title="choiceTitle" left-text="返回" @click-left="closePopup"></van-nav-bar>
|
||||
<van-search v-if="choiceKey === 'unitId'" :value="choiceQuery" @input="setChoiceQuery" placeholder="搜索单位"></van-search>
|
||||
<div class="review-more-body">
|
||||
<van-cell v-for="option in filteredChoices" :key="option.value" :title="option.text" :class="{'review-choice-selected': moreForm[choiceKey] === option.value}" clickable @click="selectChoice(option.value)">
|
||||
<van-icon v-if="moreForm[choiceKey] === option.value" name="success" color="#1989fa"></van-icon>
|
||||
</van-cell>
|
||||
<van-empty v-if="!filteredChoices.length" description="暂无选项"></van-empty>
|
||||
</div>
|
||||
</van-popup>
|
||||
</div>
|
||||
@@ -0,0 +1,146 @@
|
||||
/** 活动申报审核详情:仅展示服务端已校验权限的数据,审核表单通过末尾插槽传入。 */
|
||||
const activityReviewInfo = {
|
||||
template: /*language=HTML*/ `
|
||||
<div class="review-info">
|
||||
<section class="review-section">
|
||||
<h3>{{ data.activity_name }}</h3>
|
||||
<van-cell title="当前状态" :value="data.state_name"></van-cell>
|
||||
<van-cell title="申请人" :value="data.user_name + '(' + (data.loginname || '—') + ')'"></van-cell>
|
||||
<van-cell title="所属工会" :value="data.bxr_unionname || '—'"></van-cell>
|
||||
<van-cell title="所属单位" :value="data.bxr_unitname || '—'"></van-cell>
|
||||
<van-cell title="活动主体" :value="activityType"></van-cell>
|
||||
<van-cell title="申请类型" :value="Number(data.apply_type) === 1 ? '经费申报' : '报销申请'"></van-cell>
|
||||
<van-cell title="联系方式" :value="data.mobile || '—'"></van-cell>
|
||||
<van-cell title="申请时间" :value="data.apply_time || '—'"></van-cell>
|
||||
<van-cell title="承办单位/协会" :value="data.clubName || data.fundsUnitName || '—'"></van-cell>
|
||||
<van-cell title="项目编号" :value="data.projectCode || '—'"></van-cell>
|
||||
<van-cell title="经费类型" :value="data.projectJfName || '—'"></van-cell>
|
||||
<van-cell title="计划时间" :value="(data.startPlannedDate || '—') + ' 至 ' + (data.endPlannedDate || '—')"></van-cell>
|
||||
<van-cell title="活动时间" :value="data.startTime ? data.startTime + ' 至 ' + (data.endTime || '—') : '暂无'"></van-cell>
|
||||
<van-cell title="实际费用" :value="'¥' + money(data.activity_money)"></van-cell>
|
||||
<van-cell title="活动地点" :value="data.address || '—'"></van-cell>
|
||||
<van-cell title="活动人数" :value="data.activity_number || '—'"></van-cell>
|
||||
</section>
|
||||
<section class="review-section">
|
||||
<h3>活动方案及简介</h3>
|
||||
<div class="review-description">{{ activityText || '暂无' }}</div>
|
||||
<img v-for="(url, index) in activityImages" :key="index" :src="url" class="review-content-image"
|
||||
alt="活动方案配图" @click="$emit('preview', {imageUrl: url})">
|
||||
</section>
|
||||
<section class="review-section">
|
||||
<h3>预算明细</h3>
|
||||
<div v-for="(item, index) in budgets" :key="index" class="review-budget-item">
|
||||
<van-cell :title="item.name || '费用项目'" :value="'¥' + money(item.budgetPrice)"></van-cell>
|
||||
<template v-if="data.reimbursement_state_id">
|
||||
<van-cell title="实际费用" :value="'¥' + money(item.actualPrice)"></van-cell>
|
||||
<van-cell title="收款人" :value="item.username || '—'"></van-cell>
|
||||
<van-cell title="支行名称" :value="item.bankName || '—'"></van-cell>
|
||||
<van-cell title="报销卡号" :value="item.bankCardNum || '—'"></van-cell>
|
||||
<van-cell title="备注" :value="item.remark || '—'"></van-cell>
|
||||
</template>
|
||||
</div>
|
||||
<van-cell title="预算合计" :value="'¥' + money(data.funds_money)" class="review-total"></van-cell>
|
||||
</section>
|
||||
<section class="review-section">
|
||||
<h3>申请附件</h3>
|
||||
<van-cell v-for="(file, index) in files" :key="file.id || index" :title="file.filename || file.name || '附件'" class="review-file"
|
||||
value="查看" is-link @click="$emit('preview', file)"></van-cell>
|
||||
<p v-if="!files.length" class="review-muted">暂无附件</p>
|
||||
<h3>申请人签字</h3>
|
||||
<img v-if="data.us_s" class="review-sign-image" :src="signUrl(data.us_s)" alt="申请人签字">
|
||||
<p v-else class="review-muted">暂无签字</p>
|
||||
</section>
|
||||
<section v-for="group in reimbursementFiles" :key="group.key" class="review-section">
|
||||
<h3>{{ group.title }}</h3>
|
||||
<van-cell v-for="(file, index) in group.files" :key="file.id || index" :title="file.filename || file.name || '附件'"
|
||||
class="review-file" value="查看" is-link @click="$emit('preview', file)"></van-cell>
|
||||
<p v-if="!group.files.length" class="review-muted">暂无附件</p>
|
||||
</section>
|
||||
<section v-for="audit in audits" :key="audit.key" class="review-section">
|
||||
<h3>{{ audit.title }}</h3>
|
||||
<van-cell title="审核人" :value="audit.name || '—'"></van-cell>
|
||||
<van-cell title="审核时间" :value="audit.time || '—'"></van-cell>
|
||||
<van-cell title="审核意见" :value="audit.idea || '暂无'"></van-cell>
|
||||
<img v-if="audit.sign" class="review-sign-image" :src="signUrl(audit.sign)" :alt="audit.title + '签字'">
|
||||
</section>
|
||||
<slot></slot>
|
||||
</div>
|
||||
`,
|
||||
props: {
|
||||
data: {type: Object, default: () => ({})}
|
||||
},
|
||||
computed: {
|
||||
activityType() {
|
||||
return {40001: '校工会活动', 40002: '基层工会活动', 40003: '协会活动', 40004: '女职工活动'}[this.data.activity_type] || '—'
|
||||
},
|
||||
reimbursementFiles() {
|
||||
if (!this.data.reimbursement_state_id) return []
|
||||
return [{key: 'billFiles', title: '发票'}, {key: 'photoFiles', title: '活动报道照片/总结/链接'}, {key: 'otherFiles', title: '奖品/纪念品/服装/道具等活动人员名单'}]
|
||||
.map((group) => Object.assign({}, group, {files: this.asList(this.data[group.key])}))
|
||||
},
|
||||
budgets() {
|
||||
return this.asList(this.data.budgets)
|
||||
},
|
||||
files() {
|
||||
return this.asList(this.data.files)
|
||||
},
|
||||
activityText() {
|
||||
// 申请内容来自富文本,按段落提取文本,避免在审核页面执行其中的脚本或事件属性。
|
||||
const container = document.createElement('div')
|
||||
container.innerHTML = (this.data.activity_content || '').replace(/<\/(p|div|li)>/gi, '\n')
|
||||
container.querySelectorAll('script,style').forEach((node) => node.remove())
|
||||
return container.textContent.trim()
|
||||
},
|
||||
activityImages() {
|
||||
// 保留富文本中的方案配图,只提取图片地址,不执行原文中的 HTML 属性和事件。
|
||||
const content = new DOMParser().parseFromString(this.data.activity_content || '', 'text/html')
|
||||
return Array.from(content.querySelectorAll('img')).map((img) => img.getAttribute('src') || '')
|
||||
.filter((url) => /^(https?:\/\/|\/(?!\/)|data:image\/(?:png|jpeg|gif|webp);base64,)/i.test(url))
|
||||
},
|
||||
audits() {
|
||||
const data = this.data
|
||||
const records = [
|
||||
{key: 'division', title: '基层工会主席审核', id: data.division_chairman_name, name: data.dc_username, time: data.division_chairman_time, idea: data.division_chairman_idea, sign: data.dc_s},
|
||||
{key: 'association', title: '协会负责人审核', id: data.association_principal_id,
|
||||
name: data.ap_username, time: data.association_principal_time, idea: data.association_principal_idea, sign: data.ap_s},
|
||||
{key: 'accounting', title: '校工会负责人审核', id: data.accounting_name,
|
||||
name: data.a_username, time: data.accounting_time, idea: data.accounting_idea, sign: data.a_s},
|
||||
{key: 'confirm', title: '校工会副主席审核', id: data.confirm_trade_name, name: data.ct_username, time: data.confirm_trade_time, idea: data.confirm_trade_idea, sign: data.ct_s},
|
||||
{key: 'cashier', title: '校工会出纳审核', id: data.union_cashier_name, name: data.uc_username, time: data.union_cashier_time, idea: data.union_cashier_idea, sign: data.uc_s},
|
||||
{key: 'union', title: '校工会主席审核', id: data.union_chairman_name,
|
||||
name: data.ucc_username, time: data.union_chairman_time, idea: data.union_chairman_idea, sign: data.ucc_s}
|
||||
].filter((audit) => audit.id)
|
||||
// PC 详情同时展示已有报销审批记录;只读呈现,不把报销流程混入当前申请审核。
|
||||
const reimbursementAudits = [
|
||||
{key: 'reimbursement_club_audit', title: '协会负责人审核(报销)'},
|
||||
{key: 'reimbursement_union_audit', title: '基层工会主席审核(报销)'},
|
||||
{key: 'reimbursement_school_director', title: '校工会负责人审核(报销)'},
|
||||
{key: 'reimbursement_school_chairman', title: '校工会主席审核(报销)'}
|
||||
].filter((item) => data[item.key]).map((item) => {
|
||||
const audit = data[item.key]
|
||||
return {key: item.key, title: item.title, name: audit.username, time: audit.auditTime, idea: audit.auditOpinion, sign: audit.auditSign}
|
||||
})
|
||||
return records.concat(reimbursementAudits)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
/** 历史接口可能返回 JSON 字符串或数组;空值、异常数据均不阻断整页展示。 */
|
||||
asList(value) {
|
||||
if (Array.isArray(value)) return value
|
||||
if (!value) return []
|
||||
try {
|
||||
const list = JSON.parse(value)
|
||||
return Array.isArray(list) ? list : []
|
||||
} catch (error) {
|
||||
return []
|
||||
}
|
||||
},
|
||||
money(value) {
|
||||
return value == null || !Number.isFinite(Number(value)) ? '—' : Number(value).toLocaleString('zh-CN', {minimumFractionDigits: 2, maximumFractionDigits: 2})
|
||||
},
|
||||
/** 签字兼容 PC 保存的 Base64 图片和历史文件路径。 */
|
||||
signUrl(value) {
|
||||
return value && value.startsWith('data:image/') ? value : '/signature/getSignData?path=' + encodeURIComponent(value || '')
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,353 @@
|
||||
// 三个节点共用协会页的手机交互,节点与提示文案由独立入口配置。
|
||||
const vue = new Vue({
|
||||
el: '#app',
|
||||
components: {'review-info': activityReviewInfo},
|
||||
data() {
|
||||
return {
|
||||
config: activityReviewConfig,
|
||||
baseUrl: '/platform/jf/activity_bx/' + activityReviewConfig.stage,
|
||||
pageForm: {year: new Date().getFullYear(), unionId: '', unitId: '', isAudit: 3, pageNumber: 1, pageSize: 10, searchName: 'username', searchKeyword: '', pageOrderName: '', pageOrderBy: 'desc'},
|
||||
tableData: [], tableLoading: false, refreshing: false, finished: false, listError: false,
|
||||
listSerial: 0, listBusy: false, detailSerial: 0,
|
||||
unions: [{text: '全部工会', value: ''}], units: [{text: '全部单位', value: ''}], organizationsLoading: false, unitsSerial: 0,
|
||||
moreForm: {}, choiceKey: '', choiceQuery: '',
|
||||
auditOptions: [{text: '未审核', value: 3}, {text: '已审核', value: 2}, {text: '全部', value: 1}],
|
||||
filterOpen: {year: false, organization: false, isAudit: false}, filterClosing: false,
|
||||
popups: {filter: false, detail: false, sign: false, confirm: false, preview: false, more: false, choice: false},
|
||||
detail: {}, detailId: '', detailLoading: false, auditMode: false,
|
||||
formData: {id: '', idea: '', sign: ''}, formLoading: false, reviewFlag: true,
|
||||
reviewerName: '', reviewDate: '', previewUrl: '', previewIsImage: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
organizationKey() { return this.config.canChooseUnion ? 'unionId' : 'unitId' },
|
||||
organizationOptions() { return this.config.canChooseUnion ? this.unions : this.units },
|
||||
/** 查询类型直接切换草稿字段,输入提示同步变化;确定筛选前不触发列表请求。 */
|
||||
moreSearchPlaceholder() { return this.moreForm.searchName === 'loginname' ? '请输入工号' : '请输入姓名' },
|
||||
searchSummary() {
|
||||
const parts = []
|
||||
if (this.pageForm.searchKeyword) parts.push(this.pageForm.searchKeyword)
|
||||
if (this.config.canChooseUnion && this.pageForm.unitId) parts.push(this.optionText('unitId', this.pageForm.unitId))
|
||||
if (this.pageForm.pageOrderName) parts.push(this.optionText('pageOrderName', this.pageForm.pageOrderName))
|
||||
return parts.join(' · ') || '按姓名、工号、单位筛选'
|
||||
},
|
||||
choiceOptions() {
|
||||
return {
|
||||
unitId: this.units,
|
||||
searchName: [{text: '姓名', value: 'username'}, {text: '工号', value: 'loginname'}],
|
||||
pageOrderName: [{text: '默认排序', value: ''}, {text: '所属工会', value: 'bxr_unionname'}, {text: '活动主体类型', value: 'activity_type'}, {text: '经费类型', value: 'absName'}, {text: '申请类型', value: 'apply_type'}, {text: '申请时间', value: 'apply_time'}, {text: '申请状态', value: 'state_id'}],
|
||||
pageOrderBy: [{text: '升序', value: 'asc'}, {text: '降序', value: 'desc'}]
|
||||
}
|
||||
},
|
||||
choiceTitle() { return {unitId: '所属单位', searchName: '查询类型', pageOrderName: '排序字段', pageOrderBy: '排序方式'}[this.choiceKey] || '请选择' },
|
||||
filteredChoices() {
|
||||
return (this.choiceOptions[this.choiceKey] || []).filter((item) => item.text.includes(this.choiceQuery.trim()))
|
||||
},
|
||||
yearOptions() {
|
||||
return [{text: '全部年份', value: ''}].concat(Array.from({length: 11}, (_, index) => {
|
||||
const year = new Date().getFullYear() - index
|
||||
return {text: year + '年', value: year}
|
||||
}))
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
money(value) {
|
||||
return value == null || !Number.isFinite(Number(value)) ? '—' : Number(value).toLocaleString('zh-CN', {minimumFractionDigits: 2, maximumFractionDigits: 2})
|
||||
},
|
||||
shortDate(value) {
|
||||
return value ? String(value).slice(5, 10) : '—'
|
||||
},
|
||||
setRefreshing(value) { this.refreshing = value },
|
||||
setTableLoading(value) { this.tableLoading = value },
|
||||
setListError(value) { this.listError = value },
|
||||
setIdea(value) { this.$set(this.formData, 'idea', value) },
|
||||
/** 筛选切换或提交后,从第一页替换数据;旧请求晚到时不能混入当前筛选结果。 */
|
||||
doSearch() {
|
||||
this.listSerial++
|
||||
this.listBusy = false
|
||||
this.$set(this.pageForm, 'pageNumber', 1)
|
||||
this.tableData = []
|
||||
this.finished = false
|
||||
this.listError = false
|
||||
this.pageData()
|
||||
},
|
||||
/** 列表返回 data.list 和 totalCount;失败保留页码供重试,成功后才推进下一页。 */
|
||||
pageData() {
|
||||
if (this.listBusy || this.finished) return
|
||||
const serial = this.listSerial
|
||||
this.listBusy = true
|
||||
this.tableLoading = true
|
||||
this.listError = false
|
||||
$.post(this.baseUrl + '/pageData', Object.assign({}, this.pageForm))
|
||||
.then((res) => {
|
||||
if (serial !== this.listSerial || this._isDestroyed) return
|
||||
if (res.code !== 0 || !res.data) {
|
||||
this.listError = true
|
||||
this.$toast(res.msg || '列表加载失败')
|
||||
return
|
||||
}
|
||||
const list = res.data.list || []
|
||||
this.tableData = this.tableData.concat(list)
|
||||
this.finished = !list.length || this.tableData.length >= Number(res.data.totalCount)
|
||||
this.$set(this.pageForm, 'pageNumber', this.pageForm.pageNumber + 1)
|
||||
})
|
||||
.fail(() => {
|
||||
if (serial === this.listSerial && !this._isDestroyed) this.listError = true
|
||||
})
|
||||
.always(() => {
|
||||
if (serial !== this.listSerial || this._isDestroyed) return
|
||||
this.listBusy = false
|
||||
this.tableLoading = false
|
||||
this.refreshing = false
|
||||
})
|
||||
},
|
||||
/** 工会和单位沿用 PC 的公共选项接口;接口返回 data 数组,组织权限仍由服务端控制。 */
|
||||
loadOrganizations() {
|
||||
this.organizationsLoading = true
|
||||
$.post('/platform/vi/common/unions', {})
|
||||
.then((res) => {
|
||||
if (this._isDestroyed) return
|
||||
if (res.code === 0) this.unions = [{text: '全部工会', value: ''}].concat((res.data || []).map((item) => ({text: item.unionname, value: item.id})))
|
||||
else this.$toast(res.msg || '工会加载失败')
|
||||
})
|
||||
.fail(() => this.$toast('工会加载失败,请重试'))
|
||||
.always(() => { this.organizationsLoading = false })
|
||||
this.loadUnits()
|
||||
},
|
||||
/** 更换工会后清空旧单位;序号避免前一次请求晚返回而覆盖新工会的选项。 */
|
||||
loadUnits() {
|
||||
const serial = ++this.unitsSerial
|
||||
return $.post('/platform/vi/common/units', {unionId: this.config.canChooseUnion ? this.pageForm.unionId : this.config.currentUnionId})
|
||||
.then((res) => {
|
||||
if (serial !== this.unitsSerial || this._isDestroyed) return
|
||||
if (res.code === 0) this.units = [{text: '全部单位', value: ''}].concat((res.data || []).map((item) => ({text: item.name, value: item.id})))
|
||||
else this.$toast(res.msg || '单位加载失败')
|
||||
})
|
||||
.fail(() => { if (serial === this.unitsSerial && !this._isDestroyed) this.$toast('单位加载失败,请重试') })
|
||||
},
|
||||
openMore() {
|
||||
this.moreForm = Object.assign({}, this.pageForm)
|
||||
this.loadUnits()
|
||||
this.$options.popupSession.open('more')
|
||||
},
|
||||
setMoreField(key, value) { this.$set(this.moreForm, key, value) },
|
||||
setChoiceQuery(value) { this.choiceQuery = value },
|
||||
/** 选择项弹框压在更多筛选上层,返回时只退出当前选择,不丢弃其他筛选值。 */
|
||||
openChoice(key) {
|
||||
this.choiceKey = key
|
||||
this.choiceQuery = ''
|
||||
this.$options.popupSession.open('choice')
|
||||
},
|
||||
selectChoice(value) {
|
||||
this.$set(this.moreForm, this.choiceKey, value)
|
||||
this.$options.popupSession.close()
|
||||
},
|
||||
optionText(key, value) {
|
||||
const option = (this.choiceOptions[key] || []).find((item) => item.value === value)
|
||||
return option ? option.text : '请选择'
|
||||
},
|
||||
resetMore() {
|
||||
this.moreForm = Object.assign({}, this.moreForm, {unitId: this.config.canChooseUnion ? '' : this.pageForm.unitId, searchName: 'username', searchKeyword: '', pageOrderName: '', pageOrderBy: 'desc'})
|
||||
},
|
||||
applyMore() {
|
||||
const values = this.moreForm
|
||||
this.$options.popupSession.close().then(() => {
|
||||
if (this._isDestroyed) return
|
||||
const keys = ['searchName', 'searchKeyword', 'pageOrderName', 'pageOrderBy']
|
||||
if (this.config.canChooseUnion) keys.push('unitId')
|
||||
keys.forEach((key) => this.$set(this.pageForm, key, values[key]))
|
||||
this.doSearch()
|
||||
})
|
||||
},
|
||||
/** key 为年度/组织/状态字段,open 为菜单展开状态;三个菜单切换时共用一层历史。 */
|
||||
setFilterOpen(key, open) {
|
||||
this.$set(this.filterOpen, key, open)
|
||||
if (open && key === 'organization') {
|
||||
if (this.organizationOptions.length === 1 && !this.organizationsLoading) this.loadOrganizations()
|
||||
}
|
||||
// Vant 切换菜单会同时触发旧菜单 close 和新菜单 open,等本轮更新结束后统一判断。
|
||||
this.$nextTick(() => {
|
||||
if (this._isDestroyed || this.filterClosing) return
|
||||
if (Object.values(this.filterOpen).some(Boolean)) {
|
||||
if (!this.popups.filter) this.$options.popupSession.open('filter')
|
||||
} else if (this.popups.filter) {
|
||||
this.filterClosing = true
|
||||
this.$options.popupSession.close().then(() => { this.filterClosing = false })
|
||||
}
|
||||
})
|
||||
},
|
||||
/** key 为筛选字段,value 为对应选项值;仅值改变时刷新,菜单收起事件负责回退历史。 */
|
||||
selectFilter(key, value) {
|
||||
this.$set(this.pageForm, key, value)
|
||||
if (key === 'unionId') {
|
||||
this.$set(this.pageForm, 'unitId', '')
|
||||
this.units = [{text: '全部单位', value: ''}]
|
||||
this.loadUnits()
|
||||
}
|
||||
this.doSearch()
|
||||
},
|
||||
/** 每次打开申请清空上次的意见和签字,防止在不同申请之间串用审核数据。 */
|
||||
openDetail(id, audit) {
|
||||
if (this.formLoading) return
|
||||
this.detailId = id
|
||||
this.detail = {}
|
||||
this.auditMode = audit
|
||||
this.formData = {id, idea: '', sign: ''}
|
||||
this.reviewDate = moment().format('YYYY-MM-DD')
|
||||
this.$options.popupSession.open('detail')
|
||||
this.loadDetail(id)
|
||||
},
|
||||
loadDetail(id) {
|
||||
const serial = ++this.detailSerial
|
||||
this.detailLoading = true
|
||||
$.post(this.baseUrl + '/findOne', {id})
|
||||
.then((res) => {
|
||||
if (serial !== this.detailSerial || !this.popups.detail || this._isDestroyed) return
|
||||
if (res.code === 0 && res.data) {
|
||||
this.detail = res.data
|
||||
if (!this.detail.canReview) this.auditMode = false
|
||||
} else {
|
||||
this.$toast(res.msg || '详情加载失败')
|
||||
}
|
||||
})
|
||||
.fail(() => {
|
||||
if (serial === this.detailSerial && !this._isDestroyed) this.$toast('详情加载失败,请重试')
|
||||
})
|
||||
.always(() => {
|
||||
if (serial === this.detailSerial && !this._isDestroyed) this.detailLoading = false
|
||||
})
|
||||
},
|
||||
startAudit() {
|
||||
this.auditMode = true
|
||||
this.$nextTick(() => {
|
||||
const body = this.$el.querySelector('.review-detail-body')
|
||||
if (body) body.scrollTop = body.scrollHeight
|
||||
})
|
||||
},
|
||||
openSignature() {
|
||||
if (!this.formLoading) this.$options.popupSession.open('sign')
|
||||
},
|
||||
/** signature 是 mobile-sign 回传的本次签字 Base64,随审核请求一起保存。 */
|
||||
setSignature(signature) {
|
||||
this.$set(this.formData, 'sign', signature)
|
||||
},
|
||||
confirmReview(flag) {
|
||||
if (this.formLoading || !this.detail.canReview) return
|
||||
if (!this.formData.idea.trim() || !this.formData.sign) {
|
||||
this.$toast('请填写审核意见并签字')
|
||||
return
|
||||
}
|
||||
this.reviewFlag = flag
|
||||
this.$options.popupSession.open('confirm')
|
||||
},
|
||||
/** Vant 用 done 结束按钮 loading;显隐仍由历史栈驱动,避免组件自行关闭后留下历史层。 */
|
||||
beforeReviewClose(action, done) {
|
||||
if (this.formLoading) {
|
||||
done(false)
|
||||
return
|
||||
}
|
||||
if (action === 'confirm') {
|
||||
this.submitReview().always(() => done(false))
|
||||
} else {
|
||||
this.$options.popupSession.close().then(() => done(false))
|
||||
}
|
||||
},
|
||||
/** id/idea/sign/flag 与 PC 共用;审核时间和人员由服务器取值,不信任前端身份字段。 */
|
||||
submitReview() {
|
||||
if (this.formLoading) return
|
||||
this.formLoading = true
|
||||
const form = Object.assign({}, this.formData, {flag: this.reviewFlag})
|
||||
const params = this.config.stage === 'division' ? form : {data: JSON.stringify(form)}
|
||||
return $.post(this.baseUrl + '/doReview', params)
|
||||
.then((res) => {
|
||||
if (this._isDestroyed) return
|
||||
if (res.code === 0) {
|
||||
this.$toast.success('审核成功')
|
||||
this.$options.popupSession.clear().then(() => {
|
||||
if (this._isDestroyed) return
|
||||
this.formData = {id: '', idea: '', sign: ''}
|
||||
this.doSearch()
|
||||
})
|
||||
} else {
|
||||
this.$toast(res.msg || '审核失败')
|
||||
// 关闭确认层后重读状态;若已被 PC 处理则切换为只读,保留未提交的意见便于核对。
|
||||
if (this.popups.confirm) this.$options.popupSession.close().then(() => this.loadDetail(this.detailId))
|
||||
}
|
||||
})
|
||||
.fail(() => this.$toast('提交失败,请检查网络后重试'))
|
||||
.always(() => { this.formLoading = false })
|
||||
},
|
||||
/** 图片使用文件流,文档沿用项目预览接口;预览自身同样占一层历史。 */
|
||||
previewFile(file) {
|
||||
if (file.imageUrl) {
|
||||
this.previewIsImage = true
|
||||
this.previewUrl = file.imageUrl
|
||||
this.$options.popupSession.open('preview')
|
||||
return
|
||||
}
|
||||
if (!file.id) {
|
||||
this.$toast('附件信息不完整')
|
||||
return
|
||||
}
|
||||
this.previewIsImage = /\.(png|jpe?g|gif|webp|bmp)$/i.test(file.filename || file.filepath || '')
|
||||
const streamUrl = '/file_server/fileStreamPreview?id=' + encodeURIComponent(file.id)
|
||||
this.previewUrl = this.previewIsImage
|
||||
? streamUrl
|
||||
: '/assets/platform/plugins/pdfJs/web/viewer.html?file=' + encodeURIComponent(streamUrl)
|
||||
this.$options.popupSession.open('preview')
|
||||
},
|
||||
closePopup() {
|
||||
if (this.formLoading) {
|
||||
this.$toast('正在提交,请稍候')
|
||||
return
|
||||
}
|
||||
this.$options.popupSession.close()
|
||||
},
|
||||
back() {
|
||||
if (Object.values(this.popups).some(Boolean)) {
|
||||
this.closePopup()
|
||||
return
|
||||
}
|
||||
this.$options.popupSession.clear().then(() => {
|
||||
this.$destroy()
|
||||
pjaxReplace('/mobile/index')
|
||||
})
|
||||
}
|
||||
},
|
||||
created() {
|
||||
// 仅注册状态同步回调;popstate 由公共管理器统一监听。
|
||||
this.$options.popupSession = window.mobilePopupHistory.register('activityBxReview-' + this.config.stage, (layers) => {
|
||||
Object.keys(this.popups).forEach((name) => this.$set(this.popups, name, layers.includes(name)))
|
||||
// 系统返回或页面返回先收起下拉菜单;由公共历史栈驱动,页面不另绑 popstate。
|
||||
if (!layers.includes('filter')) {
|
||||
Object.keys(this.filterOpen).forEach((key) => {
|
||||
this.$set(this.filterOpen, key, false)
|
||||
const dropdown = this.$refs[key + 'Filter']
|
||||
if (dropdown) dropdown.toggle(false)
|
||||
})
|
||||
}
|
||||
})
|
||||
const user = JSON.parse(window.sessionStorage.getItem('user') || '{}')
|
||||
this.reviewerName = user.username || ''
|
||||
this.loadOrganizations()
|
||||
this.doSearch()
|
||||
const query = new URLSearchParams(window.location.search)
|
||||
const id = query.get('id')
|
||||
if (id) this.openDetail(id, query.get('mode') === 'audit')
|
||||
// PJAX 不会自动销毁旧 Vue 实例,在替换容器前注销页面回调。
|
||||
$(document).on('pjax:beforeReplace.activityBxReview', () => this.$destroy())
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.listSerial++
|
||||
this.detailSerial++
|
||||
this.unitsSerial++
|
||||
const session = this.$options.popupSession
|
||||
if (Object.values(this.popups).some(Boolean)) {
|
||||
session.clear().then(() => session.dispose())
|
||||
} else {
|
||||
// PJAX 替换后可能立即创建新页面;空栈必须同步注销,不能等 Promise 回调再释放注册。
|
||||
session.dispose()
|
||||
}
|
||||
$(document).off('pjax:beforeReplace.activityBxReview')
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,18 @@
|
||||
<!--#
|
||||
layout("/mobile/platform.html"){
|
||||
#-->
|
||||
<!--#include('/mobile/jf/activitybx/review/index.html'){}#-->
|
||||
<script>
|
||||
// 节点配置由当前页面固定提供,业务权限及状态仍由服务端校验。
|
||||
const activityReviewConfig = {
|
||||
stage: 'union', title: '校工会主席审核', state: 1040,
|
||||
confirmMessage: '确认通过后,将按原流程生成活动并通知申请人。',
|
||||
canChooseUnion: ${@shiro.hasRole('sysadmin')||@shiro.hasRole('A06')||@shiro.hasRole('H03')},
|
||||
currentUnionId: '${@io.v.nutz.base.utils.Vi.getUnionId()}'
|
||||
}
|
||||
<!--#include('/mobile/jf/activitybx/review/info.js'){}#-->
|
||||
<!--#include('/mobile/jf/activitybx/review/page.js'){}#-->
|
||||
</script>
|
||||
<!--#
|
||||
}
|
||||
#-->
|
||||
@@ -0,0 +1,18 @@
|
||||
<!--#
|
||||
layout("/mobile/platform.html"){
|
||||
#-->
|
||||
<!--#include('/mobile/jf/reimbursement/review/index.html'){}#-->
|
||||
<script>
|
||||
// 本页固定审核节点;服务端按相同节点校验权限、归属及报销状态。
|
||||
const reimbursementReviewConfig = {
|
||||
stage: 'clubChairman', title: '协会负责人审核(报销)', state: 1000,
|
||||
confirmMessage: '通过后将转交校工会负责人审核。', showClub: true, showUnit: false,
|
||||
canChooseUnion: false,
|
||||
currentUnionId: '${@io.v.nutz.base.utils.Vi.getUnionId()}'
|
||||
}
|
||||
<!--#include('/mobile/jf/reimbursement/review/info.js'){}#-->
|
||||
<!--#include('/mobile/jf/reimbursement/review/page.js'){}#-->
|
||||
</script>
|
||||
<!--#
|
||||
}
|
||||
#-->
|
||||
@@ -0,0 +1,233 @@
|
||||
<style>
|
||||
.reimbursement-review-page { min-height: 100vh; background: #f6f7f9; color: #323233; font-size: 13px; }
|
||||
.reimbursement-review-page .van-nav-bar__title { font-size: 15px; font-weight: 400; }
|
||||
.reimbursement-review-filters .van-dropdown-menu__bar { box-shadow: none; border-top: 1px solid #f8f8f8; border-bottom: 1px solid #f8f8f8; }
|
||||
.reimbursement-review-filters .van-dropdown-menu__item { flex: 1; min-width: 0; }
|
||||
.reimbursement-review-filters .van-dropdown-menu__title { max-width: calc(100% - 40px); padding: 0 8px; font-size: 14px; }
|
||||
.reimbursement-review-filters .van-dropdown-item__content { max-height: 70vh; border-radius: 0 0 15px 15px; }
|
||||
.reimbursement-review-list { padding: 2px 0 12px; }
|
||||
.reimbursement-review-card { margin: 12px 13px 0; padding: 11px 12px 10px; background: white; border-radius: 10px; box-shadow: 0 5px 12px rgba(30,50,70,.04); }
|
||||
.reimbursement-review-card h3 { position: relative; margin: 0 0 7px; color: #086bc3; font-size: 15px; line-height: 21px; font-weight: 600; overflow-wrap: anywhere; }
|
||||
.reimbursement-review-card h3:before { content: ''; position: absolute; left: -10px; top: 3px; height: 15px; border-left: 2px solid #1989fa; }
|
||||
.reimbursement-review-card p { margin: 0; line-height: 26px; color: #85878d; overflow-wrap: anywhere; }
|
||||
.reimbursement-review-card label { color: #242424; }
|
||||
.reimbursement-review-card .reimbursement-review-pair { display: flex; gap: 10px; justify-content: space-between; }
|
||||
.reimbursement-review-pair span:last-child { flex-shrink: 0; }
|
||||
.reimbursement-review-card-footer { display: flex; align-items: center; gap: 8px; margin-top: 7px; padding-top: 7px; border-top: 1px solid #f5f5f5; }
|
||||
.reimbursement-review-state { flex: 1; min-width: 0; font-size: 12px; color: #b7790c; overflow-wrap: anywhere; }
|
||||
.reimbursement-review-card-footer .van-button { height: 25px; padding: 0 8px; font-size: 13px; border-radius: 6px; flex-shrink: 0; }
|
||||
.reimbursement-review-popup { display: flex; flex-direction: column; overflow: hidden; background: #f6f7f9; height: 100%; width: 100%; }
|
||||
.reimbursement-review-popup > .van-nav-bar { flex-shrink: 0; }
|
||||
.reimbursement-review-detail-body { flex: 1; overflow-y: auto; min-height: 0; padding-bottom: 12px; }
|
||||
.reimbursement-review-section { margin: 12px; padding: 10px 0; border-radius: 10px; background: white; overflow: hidden; }
|
||||
.reimbursement-review-section h3 { margin: 4px 14px 8px; font-size: 15px; color: #1768b0; }
|
||||
.reimbursement-review-section .van-cell { font-size: 13px; padding: 8px 14px; }
|
||||
.reimbursement-review-section .van-cell__title { flex: 0 0 88px; }
|
||||
.reimbursement-review-section .van-cell__value { white-space: pre-wrap; overflow-wrap: anywhere; }
|
||||
.reimbursement-review-section .reimbursement-review-file .van-cell__title { flex: 1; overflow-wrap: anywhere; }
|
||||
.reimbursement-review-section .reimbursement-review-file .van-cell__value { flex: 0 0 40px; }
|
||||
.reimbursement-review-content-image { display: block; max-width: calc(100% - 28px); margin: 10px 14px; }
|
||||
.reimbursement-review-description { padding: 0 14px; line-height: 1.8; white-space: pre-wrap; overflow-wrap: anywhere; }
|
||||
.reimbursement-review-muted { margin: 10px 14px; color: #969799; }
|
||||
.reimbursement-review-total .van-cell__value { color: #086bc3; font-weight: 600; }
|
||||
.reimbursement-review-sign-image { display: block; max-width: calc(100% - 28px); width: 240px; height: 100px; object-fit: contain; margin: 8px 14px; }
|
||||
.reimbursement-review-actions { flex-shrink: 0; display: flex; gap: 12px; padding: 10px 14px calc(10px + env(safe-area-inset-bottom)); background: white; border-top: 1px solid #eee; z-index: 2; }
|
||||
.reimbursement-review-actions .van-button { flex: 1; border-radius: 6px; height: 40px; }
|
||||
.reimbursement-review-section .reimbursement-review-sign-field { display: block; }
|
||||
.reimbursement-review-sign-field .van-field__label { width: auto; margin: 0 0 10px; }
|
||||
.reimbursement-review-sign-field .main-panel .van-image { width: 100%; height: 112px; min-height: 112px; overflow: hidden; border-radius: 6px; }
|
||||
.reimbursement-review-sign-field .main-panel .open-button { width: 100%; border-radius: 6px; }
|
||||
.reimbursement-review-preview { display: block; width: 100%; height: calc(100% - 46px); border: none; background: white; }
|
||||
.reimbursement-review-preimbursement-review-image { width: 100%; height: calc(100% - 46px); object-fit: contain; }
|
||||
.reimbursement-review-loading { padding: 30px; text-align: center; }
|
||||
.reimbursement-review-more-bar { display: flex; justify-content: space-between; align-items: center; gap: 8px; padding: 7px 13px; background: white; color: #969799; font-size: 12px; }
|
||||
.reimbursement-review-more-bar > span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.reimbursement-review-more-popup, .reimbursement-review-choice-popup { display: flex; flex-direction: column; max-height: 80vh; background: #f7f8fa; }
|
||||
.reimbursement-review-choice-popup { height: 60vh; }
|
||||
.reimbursement-review-more-body { flex: 1; overflow-y: auto; min-height: 0; }
|
||||
/* 筛选面板独立限定样式,避免影响详情、审核按钮和其他公共弹框。 */
|
||||
.reimbursement-review-more-popup { background: #fff; border-radius: 18px 18px 0 0; overflow: hidden; }
|
||||
.reimbursement-review-more-handle { flex-shrink: 0; width: 38px; height: 4px; margin: 8px auto 0; border-radius: 4px; background: #d5d8df; }
|
||||
.reimbursement-review-more-header { display: flex; align-items: center; justify-content: space-between; flex-shrink: 0; padding: 4px 12px 8px 18px; }
|
||||
.reimbursement-review-more-header h2 { margin: 0; font-size: 18px; line-height: 32px; font-weight: 600; color: #20242d; }
|
||||
.reimbursement-review-more-close.van-button { width: 36px; height: 36px; padding: 0; border: 0; color: #92969f; background: transparent; font-size: 20px; }
|
||||
.reimbursement-review-more-popup > .reimbursement-review-more-body { flex: 0 1 auto; padding: 0 18px 14px; }
|
||||
.reimbursement-review-filter-group + .reimbursement-review-filter-group { margin-top: 16px; }
|
||||
.reimbursement-review-filter-label { margin: 0 0 8px; color: #252a34; font-size: 14px; line-height: 20px; font-weight: 600; }
|
||||
.reimbursement-review-filter-row { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 8px; }
|
||||
.reimbursement-review-filter-row .reimbursement-review-filter-label { margin: 0; }
|
||||
.reimbursement-review-unit-select.van-cell { align-items: center; padding: 8px 12px; border-radius: 8px; background: #f5f6f8; font-size: 13px; }
|
||||
.reimbursement-review-unit-select .van-cell__title { min-width: 0; overflow-wrap: anywhere; }
|
||||
.reimbursement-review-unit-select:after, .reimbursement-review-filter-input:after { display: none; }
|
||||
.reimbursement-review-filter-input.van-cell { padding: 8px 12px; border-radius: 8px; background: #f5f6f8; font-size: 13px; }
|
||||
.reimbursement-review-filter-input .van-field__left-icon { color: #969ba4; margin-right: 8px; }
|
||||
.reimbursement-review-segment { display: flex; flex-shrink: 0; gap: 2px; padding: 2px; border-radius: 8px; background: #f5f6f8; }
|
||||
.reimbursement-review-filter-option.van-button { min-width: 0; height: 30px; padding: 0 8px; border: 1px solid transparent; border-radius: 7px; background: #f5f6f8; color: #333943; font-size: 12px; }
|
||||
.reimbursement-review-segment .reimbursement-review-filter-option { min-width: 58px; height: 28px; }
|
||||
.reimbursement-review-filter-option.is-selected { background: #eaf3ff; border-color: #1989fa; color: #1989fa; font-weight: 500; }
|
||||
.reimbursement-review-segment .reimbursement-review-filter-option.is-selected { border-color: transparent; }
|
||||
.reimbursement-review-sort-options { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 6px; }
|
||||
.reimbursement-review-sort-options .reimbursement-review-filter-option { position: relative; width: 100%; height: 30px; padding: 0 4px; white-space: nowrap; }
|
||||
.reimbursement-review-sort-options .is-selected { padding-right: 15px; }
|
||||
.reimbursement-review-sort-check { position: absolute; right: 3px; top: 50%; transform: translateY(-50%); font-size: 12px; }
|
||||
.reimbursement-review-sort-direction { margin: 14px 0 0; }
|
||||
.reimbursement-review-sort-direction .reimbursement-review-segment { background: transparent; gap: 6px; padding: 0; }
|
||||
.reimbursement-review-more-footer { display: flex; flex-shrink: 0; gap: 10px; padding: 10px 16px calc(10px + env(safe-area-inset-bottom)); border-top: 1px solid #f1f2f5; background: #fff; }
|
||||
.reimbursement-review-more-footer .van-button { flex: 1; height: 40px; border-radius: 8px; font-size: 14px; }
|
||||
.reimbursement-review-more-footer .van-button--info { flex: 2; }
|
||||
.reimbursement-review-more-footer .van-button--default { color: #60656e; border-color: #e4e7ed; }
|
||||
.reimbursement-review-choice-popup { background: #fff; }
|
||||
.reimbursement-review-choice-popup .van-search { padding: 8px 16px; }
|
||||
.reimbursement-review-choice-popup .van-search__content { border-radius: 8px; background: #f5f6f8; }
|
||||
.reimbursement-review-choice-popup .van-cell { padding: 12px 18px; font-size: 13px; }
|
||||
.reimbursement-review-choice-popup .reimbursement-review-choice-selected { color: #1989fa; background: #f0f6ff; }
|
||||
.reimbursement-review-budget-item + .reimbursement-review-budget-item { border-top: 1px solid #f2f3f5; }
|
||||
</style>
|
||||
<div id="app" class="reimbursement-review-page" v-cloak>
|
||||
<van-sticky>
|
||||
<van-nav-bar :title="config.title" left-arrow @click-left="back"></van-nav-bar>
|
||||
<van-dropdown-menu class="reimbursement-review-filters" active-color="#1867b0"
|
||||
:close-on-click-outside="false" :close-on-click-overlay="false">
|
||||
<van-dropdown-item ref="yearFilter" :options="yearOptions" :value="pageForm.year" :disabled="filterClosing"
|
||||
@change="selectFilter('year', $event)"
|
||||
@open="setFilterOpen('year', true)" @close="setFilterOpen('year', false)"></van-dropdown-item>
|
||||
<van-dropdown-item ref="organizationFilter" :options="organizationOptions" :value="pageForm[organizationKey]" :disabled="filterClosing"
|
||||
@change="selectFilter(organizationKey, $event)"
|
||||
@open="setFilterOpen('organization', true)" @close="setFilterOpen('organization', false)">
|
||||
<van-loading v-if="organizationLoading" class="reimbursement-review-loading">加载中</van-loading>
|
||||
</van-dropdown-item>
|
||||
<van-dropdown-item ref="isAuditFilter" :options="auditOptions" :value="pageForm.isAudit" :disabled="filterClosing"
|
||||
@change="selectFilter('isAudit', $event)"
|
||||
@open="setFilterOpen('isAudit', true)" @close="setFilterOpen('isAudit', false)"></van-dropdown-item>
|
||||
</van-dropdown-menu>
|
||||
<div class="reimbursement-review-more-bar"><span>{{ searchSummary }}</span><van-button size="mini" plain type="info" @click="openMore">更多筛选</van-button></div>
|
||||
</van-sticky>
|
||||
<van-pull-refresh :value="refreshing" @input="setRefreshing" @refresh="doSearch">
|
||||
<van-list :value="tableLoading" :finished="finished" :error="listError" error-text="加载失败,点击重试"
|
||||
:finished-text="tableData.length ? '没有更多了' : ''" :immediate-check="false"
|
||||
@input="setTableLoading" @update:error="setListError" @load="pageData" class="reimbursement-review-list">
|
||||
<article v-for="item in tableData" :key="item.id" class="reimbursement-review-card">
|
||||
<h3>{{ item.activity_name }}</h3>
|
||||
<p><label>申请人:</label>{{ item.user_name }}<span v-if="item.bxr_loginname">({{ item.bxr_loginname }})</span></p>
|
||||
<p><label>{{ config.stage === 'clubChairman' ? '所属协会:' : '所属工会:' }}</label>{{ config.stage === 'clubChairman' ? (item.clubName || item.fundsUnitName || '—') : (item.unionname || item.fundsUnitName || '—') }}</p>
|
||||
<p class="reimbursement-review-pair"><span><label>报销:</label>¥{{ money(item.activity_money) }}</span><span><label>申请:</label>{{ shortDate(item.reimbursement_apply_time) }}</span></p>
|
||||
<div class="reimbursement-review-card-footer">
|
||||
<span class="reimbursement-review-state" :style="{color: item.state_color || '#b7790c'}">{{ item.state_name || config.title }}</span>
|
||||
<van-button type="info" size="small" :disabled="formLoading" @click="openDetail(item.id, false)">查看</van-button>
|
||||
<van-button v-if="Number(item.reimbursement_state_id) === config.state" type="primary" size="small" :disabled="formLoading" @click="openDetail(item.id, true)">审核</van-button>
|
||||
</div>
|
||||
</article>
|
||||
<van-empty v-if="!tableLoading && !listError && !tableData.length" description="暂无报销申请"></van-empty>
|
||||
</van-list>
|
||||
</van-pull-refresh>
|
||||
|
||||
<van-popup :value="popups.detail" position="right" class="reimbursement-review-popup" :close-on-popstate="false" :close-on-click-overlay="false">
|
||||
<van-nav-bar :title="auditMode ? config.title : '报销详情'" left-arrow @click-left="closePopup"></van-nav-bar>
|
||||
<div class="reimbursement-review-detail-body">
|
||||
<van-loading v-if="detailLoading" class="reimbursement-review-loading">加载报销中</van-loading>
|
||||
<van-empty v-else-if="!detail.id" description="详情加载失败">
|
||||
<van-button size="small" type="info" @click="loadDetail(detailId)">重新加载</van-button>
|
||||
</van-empty>
|
||||
<reimbursement-info v-else :data="detail" @preview="previewFile">
|
||||
<section v-if="auditMode && detail.canReview" class="reimbursement-review-section">
|
||||
<h3>审核信息</h3>
|
||||
<van-field label="审核人" :value="reviewerName" readonly></van-field>
|
||||
<van-field label="审核时间" :value="reviewDate" readonly></van-field>
|
||||
<van-field label="审核意见" :value="formData.auditOpinion" @input="setIdea" type="textarea" rows="3"
|
||||
maxlength="500" show-word-limit required placeholder="请填写审核意见" :disabled="formLoading"></van-field>
|
||||
<van-field label="审核签字" required class="reimbursement-review-sign-field">
|
||||
<template #input>
|
||||
<mobile-sign :value="formData.auditSign" @input="setSignature" :disabled="formLoading"
|
||||
:save-to-profile="false" :popup-visible="popups.sign" panel-title="审核签字"
|
||||
@open="openSignature" @close="closePopup"></mobile-sign>
|
||||
</template>
|
||||
</van-field>
|
||||
</section>
|
||||
</reimbursement-info>
|
||||
</div>
|
||||
<div v-if="detail.id && !detailLoading" class="reimbursement-review-actions">
|
||||
<template v-if="auditMode && detail.canReview">
|
||||
<van-button type="danger" plain :disabled="formLoading" @click="confirmReview(false)">拒绝</van-button>
|
||||
<van-button type="primary" :loading="formLoading" :disabled="formLoading" @click="confirmReview(true)">通过</van-button>
|
||||
</template>
|
||||
<van-button v-else-if="detail.canReview" type="primary" @click="startAudit">去审核</van-button>
|
||||
<van-button v-else @click="closePopup">关闭</van-button>
|
||||
</div>
|
||||
</van-popup>
|
||||
|
||||
<van-dialog :value="popups.confirm" :title="reviewFlag ? '确认通过' : '确认拒绝'"
|
||||
:message="reviewFlag ? config.confirmMessage : '拒绝后将结束当前报销流程,请核对审核意见。'"
|
||||
show-cancel-button :allow-html="false" :close-on-click-overlay="false" :close-on-popstate="false"
|
||||
:before-close="beforeReviewClose"></van-dialog>
|
||||
|
||||
<van-popup :value="popups.preview" position="right" class="reimbursement-review-popup" :close-on-popstate="false" :close-on-click-overlay="false">
|
||||
<van-nav-bar title="附件查看" left-arrow @click-left="closePopup"></van-nav-bar>
|
||||
<img v-if="popups.preview && previewIsImage" :src="previewUrl" class="reimbursement-review-preimbursement-review-image" alt="报销附件">
|
||||
<iframe v-else-if="popups.preview" :src="previewUrl" title="报销附件" class="reimbursement-review-preview"></iframe>
|
||||
</van-popup>
|
||||
<van-popup :value="popups.more" position="bottom" round class="reimbursement-review-more-popup" :close-on-popstate="false" :close-on-click-overlay="false">
|
||||
<div class="reimbursement-review-more-handle" aria-hidden="true"></div>
|
||||
<header class="reimbursement-review-more-header">
|
||||
<h2>更多筛选</h2>
|
||||
<van-button icon="cross" class="reimbursement-review-more-close" aria-label="关闭更多筛选" @click="closePopup"></van-button>
|
||||
</header>
|
||||
<div class="reimbursement-review-more-body">
|
||||
<section v-if="config.canChooseUnion" class="reimbursement-review-filter-group">
|
||||
<h3 class="reimbursement-review-filter-label">所属单位</h3>
|
||||
<van-cell :title="optionText('unitId', moreForm.unitId)" class="reimbursement-review-unit-select" is-link @click="openChoice('unitId')"></van-cell>
|
||||
</section>
|
||||
<section v-if="config.showClub && config.stage !== 'clubChairman'" class="reimbursement-review-filter-group">
|
||||
<h3 class="reimbursement-review-filter-label">所属协会</h3>
|
||||
<van-cell :title="optionText('clubId', moreForm.clubId)" class="reimbursement-review-unit-select" is-link @click="openChoice('clubId')"></van-cell>
|
||||
</section>
|
||||
<section class="reimbursement-review-filter-group">
|
||||
<h3 class="reimbursement-review-filter-label">活动名称</h3>
|
||||
<van-field class="reimbursement-review-filter-input" left-icon="search" :value="moreForm.activityName"
|
||||
@input="setMoreField('activityName', $event)" maxlength="100" clearable placeholder="请输入活动名称"></van-field>
|
||||
</section>
|
||||
<section class="reimbursement-review-filter-group">
|
||||
<div class="reimbursement-review-filter-row">
|
||||
<h3 class="reimbursement-review-filter-label">查询人员</h3>
|
||||
<div class="reimbursement-review-segment" role="group" aria-label="查询类型">
|
||||
<van-button v-for="option in choiceOptions.searchName" :key="option.value" class="reimbursement-review-filter-option"
|
||||
:class="{'is-selected': moreForm.searchName === option.value}" :aria-pressed="moreForm.searchName === option.value"
|
||||
@click="setMoreField('searchName', option.value)">{{ option.text }}</van-button>
|
||||
</div>
|
||||
</div>
|
||||
<van-field class="reimbursement-review-filter-input" left-icon="search" :value="moreForm.searchKeyword"
|
||||
@input="setMoreField('searchKeyword', $event)" maxlength="100" clearable
|
||||
:placeholder="moreSearchPlaceholder" :aria-label="moreSearchPlaceholder"></van-field>
|
||||
</section>
|
||||
<section class="reimbursement-review-filter-group">
|
||||
<h3 class="reimbursement-review-filter-label">排序方式</h3>
|
||||
<div class="reimbursement-review-sort-options" role="group" aria-label="排序字段">
|
||||
<van-button v-for="option in choiceOptions.pageOrderName" :key="option.value" class="reimbursement-review-filter-option"
|
||||
:class="{'is-selected': moreForm.pageOrderName === option.value}" :aria-pressed="moreForm.pageOrderName === option.value"
|
||||
@click="setMoreField('pageOrderName', option.value)">
|
||||
{{ option.text }}<van-icon v-if="moreForm.pageOrderName === option.value" name="success" class="reimbursement-review-sort-check"></van-icon>
|
||||
</van-button>
|
||||
</div>
|
||||
<div v-if="moreForm.pageOrderName" class="reimbursement-review-filter-row reimbursement-review-sort-direction">
|
||||
<h3 class="reimbursement-review-filter-label">排列顺序</h3>
|
||||
<div class="reimbursement-review-segment" role="group" aria-label="排列顺序">
|
||||
<van-button v-for="option in choiceOptions.pageOrderBy" :key="option.value" class="reimbursement-review-filter-option"
|
||||
:class="{'is-selected': moreForm.pageOrderBy === option.value}" :aria-pressed="moreForm.pageOrderBy === option.value"
|
||||
@click="setMoreField('pageOrderBy', option.value)">{{ option.text }}</van-button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
<div class="reimbursement-review-more-footer"><van-button @click="resetMore">重置</van-button><van-button type="info" @click="applyMore">确定筛选</van-button></div>
|
||||
</van-popup>
|
||||
<van-popup :value="popups.choice" position="bottom" round class="reimbursement-review-choice-popup" :close-on-popstate="false" :close-on-click-overlay="false">
|
||||
<van-nav-bar :title="choiceTitle" left-text="返回" @click-left="closePopup"></van-nav-bar>
|
||||
<van-search v-if="choiceKey === 'unitId' || choiceKey === 'clubId'" :value="choiceQuery" @input="setChoiceQuery" :placeholder="choiceKey === 'clubId' ? '搜索协会' : '搜索单位'"></van-search>
|
||||
<div class="reimbursement-review-more-body">
|
||||
<van-loading v-if="choiceKey === 'unitId' ? unitsLoading : clubsLoading" class="reimbursement-review-loading">加载选项中</van-loading>
|
||||
<van-cell v-for="option in filteredChoices" :key="option.value" :title="option.text" :class="{'reimbursement-review-choice-selected': moreForm[choiceKey] === option.value}" clickable @click="selectChoice(option.value)">
|
||||
<van-icon v-if="moreForm[choiceKey] === option.value" name="success" color="#1989fa"></van-icon>
|
||||
</van-cell>
|
||||
<van-empty v-if="!filteredChoices.length" description="暂无选项"></van-empty>
|
||||
</div>
|
||||
</van-popup>
|
||||
</div>
|
||||
@@ -0,0 +1,147 @@
|
||||
/** 活动报销审核详情:仅展示服务端已校验权限的数据,审核表单通过末尾插槽传入。 */
|
||||
const reimbursementInfo = {
|
||||
template: /*language=HTML*/ `
|
||||
<div class="reimbursement-review-info">
|
||||
<section class="reimbursement-review-section">
|
||||
<h3>{{ data.activity_name }}</h3>
|
||||
<van-cell title="报销状态" :value="data.state_name"></van-cell>
|
||||
<van-cell title="申请人" :value="data.user_name + '(' + (data.loginname || '—') + ')'"></van-cell>
|
||||
<van-cell title="所属工会" :value="data.bxr_unionname || '—'"></van-cell>
|
||||
<van-cell title="所属单位" :value="data.bxr_unitname || '—'"></van-cell>
|
||||
<van-cell title="活动主体" :value="activityType"></van-cell>
|
||||
<van-cell title="申请类型" :value="Number(data.apply_type) === 1 ? '经费申报' : '报销申请'"></van-cell>
|
||||
<van-cell title="联系方式" :value="data.mobile || '—'"></van-cell>
|
||||
<van-cell title="报销申请时间" :value="data.reimbursement_apply_time || '—'"></van-cell>
|
||||
<van-cell title="承办单位/协会" :value="data.clubName || data.fundsUnitName || '—'"></van-cell>
|
||||
<van-cell title="项目编号" :value="data.projectCode || '—'"></van-cell>
|
||||
<van-cell title="经费类型" :value="data.projectJfName || '—'"></van-cell>
|
||||
<van-cell title="计划时间" :value="(data.startPlannedDate || '—') + ' 至 ' + (data.endPlannedDate || '—')"></van-cell>
|
||||
<van-cell title="活动时间" :value="data.startTime ? data.startTime + ' 至 ' + (data.endTime || '—') : '暂无'"></van-cell>
|
||||
<van-cell title="实际费用" :value="'¥' + money(data.activity_money)"></van-cell>
|
||||
<van-cell title="活动地点" :value="data.address || '—'"></van-cell>
|
||||
<van-cell title="活动人数" :value="data.activity_number || '—'"></van-cell>
|
||||
</section>
|
||||
<section class="reimbursement-review-section">
|
||||
<h3>活动方案及简介</h3>
|
||||
<div class="reimbursement-review-description">{{ activityText || '暂无' }}</div>
|
||||
<img v-for="(url, index) in activityImages" :key="index" :src="url" class="reimbursement-review-content-image"
|
||||
alt="活动方案配图" @click="$emit('preview', {imageUrl: url})">
|
||||
</section>
|
||||
<section class="reimbursement-review-section">
|
||||
<h3>费用及收款信息</h3>
|
||||
<div v-for="(item, index) in budgets" :key="index" class="reimbursement-review-budget-item">
|
||||
<van-cell :title="item.name || '费用项目'" :value="'¥' + money(item.budgetPrice)"></van-cell>
|
||||
<template v-if="data.reimbursement_state_id">
|
||||
<van-cell title="实际费用" :value="'¥' + money(item.actualPrice)"></van-cell>
|
||||
<van-cell title="收款人" :value="item.username || '—'"></van-cell>
|
||||
<van-cell title="支行名称" :value="item.bankName || '—'"></van-cell>
|
||||
<van-cell title="报销卡号" :value="item.bankCardNum || '—'"></van-cell>
|
||||
<van-cell title="备注" :value="item.remark || '—'"></van-cell>
|
||||
</template>
|
||||
</div>
|
||||
<van-cell title="报销合计" :value="'¥' + money(data.activity_money)" class="reimbursement-review-total"></van-cell>
|
||||
<van-cell title="预算合计" :value="'¥' + money(data.funds_money)" class="reimbursement-review-total"></van-cell>
|
||||
</section>
|
||||
<section class="reimbursement-review-section">
|
||||
<h3>申请附件</h3>
|
||||
<van-cell v-for="(file, index) in files" :key="file.id || index" :title="file.filename || file.name || '附件'" class="reimbursement-review-file"
|
||||
value="查看" is-link @click="$emit('preview', file)"></van-cell>
|
||||
<p v-if="!files.length" class="reimbursement-review-muted">暂无附件</p>
|
||||
<h3>申请人签字</h3>
|
||||
<img v-if="data.us_s" class="reimbursement-review-sign-image" :src="signUrl(data.us_s)" alt="申请人签字">
|
||||
<p v-else class="reimbursement-review-muted">暂无签字</p>
|
||||
</section>
|
||||
<section v-for="group in reimbursementFiles" :key="group.key" class="reimbursement-review-section">
|
||||
<h3>{{ group.title }}</h3>
|
||||
<van-cell v-for="(file, index) in group.files" :key="file.id || index" :title="file.filename || file.name || '附件'"
|
||||
class="reimbursement-review-file" value="查看" is-link @click="$emit('preview', file)"></van-cell>
|
||||
<p v-if="!group.files.length" class="reimbursement-review-muted">暂无附件</p>
|
||||
</section>
|
||||
<section v-for="audit in audits" :key="audit.key" class="reimbursement-review-section">
|
||||
<h3>{{ audit.title }}</h3>
|
||||
<van-cell title="审核人" :value="audit.name || '—'"></van-cell>
|
||||
<van-cell title="审核时间" :value="audit.time || '—'"></van-cell>
|
||||
<van-cell title="审核意见" :value="audit.idea || '暂无'"></van-cell>
|
||||
<img v-if="audit.sign" class="reimbursement-review-sign-image" :src="signUrl(audit.sign)" :alt="audit.title + '签字'">
|
||||
</section>
|
||||
<slot></slot>
|
||||
</div>
|
||||
`,
|
||||
props: {
|
||||
data: {type: Object, default: () => ({})}
|
||||
},
|
||||
computed: {
|
||||
activityType() {
|
||||
return {40001: '校工会活动', 40002: '基层工会活动', 40003: '协会活动', 40004: '女职工活动'}[this.data.activity_type] || '—'
|
||||
},
|
||||
reimbursementFiles() {
|
||||
if (!this.data.reimbursement_state_id) return []
|
||||
return [{key: 'billFiles', title: '发票'}, {key: 'photoFiles', title: '活动报道照片/总结/链接'}, {key: 'otherFiles', title: '奖品/纪念品/服装/道具等活动人员名单'}]
|
||||
.map((group) => Object.assign({}, group, {files: this.asList(this.data[group.key])}))
|
||||
},
|
||||
budgets() {
|
||||
return this.asList(this.data.budgets)
|
||||
},
|
||||
files() {
|
||||
return this.asList(this.data.files)
|
||||
},
|
||||
activityText() {
|
||||
// 申请内容来自富文本,按段落提取文本,避免在审核页面执行其中的脚本或事件属性。
|
||||
const container = document.createElement('div')
|
||||
container.innerHTML = (this.data.activity_content || '').replace(/<\/(p|div|li)>/gi, '\n')
|
||||
container.querySelectorAll('script,style').forEach((node) => node.remove())
|
||||
return container.textContent.trim()
|
||||
},
|
||||
activityImages() {
|
||||
// 保留富文本中的方案配图,只提取图片地址,不执行原文中的 HTML 属性和事件。
|
||||
const content = new DOMParser().parseFromString(this.data.activity_content || '', 'text/html')
|
||||
return Array.from(content.querySelectorAll('img')).map((img) => img.getAttribute('src') || '')
|
||||
.filter((url) => /^(https?:\/\/|\/(?!\/)|data:image\/(?:png|jpeg|gif|webp);base64,)/i.test(url))
|
||||
},
|
||||
audits() {
|
||||
const data = this.data
|
||||
const records = [
|
||||
{key: 'division', title: '基层工会主席审核(申报)', id: data.division_chairman_name, name: data.dc_username, time: data.division_chairman_time, idea: data.division_chairman_idea, sign: data.dc_s},
|
||||
{key: 'association', title: '协会负责人审核(申报)', id: data.association_principal_id,
|
||||
name: data.ap_username, time: data.association_principal_time, idea: data.association_principal_idea, sign: data.ap_s},
|
||||
{key: 'accounting', title: '校工会负责人审核(申报)', id: data.accounting_name,
|
||||
name: data.a_username, time: data.accounting_time, idea: data.accounting_idea, sign: data.a_s},
|
||||
{key: 'confirm', title: '校工会副主席审核(申报)', id: data.confirm_trade_name, name: data.ct_username, time: data.confirm_trade_time, idea: data.confirm_trade_idea, sign: data.ct_s},
|
||||
{key: 'cashier', title: '校工会出纳审核(申报)', id: data.union_cashier_name, name: data.uc_username, time: data.union_cashier_time, idea: data.union_cashier_idea, sign: data.uc_s},
|
||||
{key: 'union', title: '校工会主席审核(申报)', id: data.union_chairman_name,
|
||||
name: data.ucc_username, time: data.union_chairman_time, idea: data.union_chairman_idea, sign: data.ucc_s}
|
||||
].filter((audit) => audit.id)
|
||||
// 按 PC 详情展示报销审核记录,并与前期申报记录明确区分。
|
||||
const reimbursementAudits = [
|
||||
{key: 'reimbursement_club_audit', title: '协会负责人审核(报销)'},
|
||||
{key: 'reimbursement_union_audit', title: '基层工会主席审核(报销)'},
|
||||
{key: 'reimbursement_school_director', title: '校工会负责人审核(报销)'},
|
||||
{key: 'reimbursement_school_chairman', title: '校工会主席审核(报销)'}
|
||||
].filter((item) => data[item.key]).map((item) => {
|
||||
const audit = data[item.key]
|
||||
return {key: item.key, title: item.title, name: audit.username, time: audit.auditTime, idea: audit.auditOpinion, sign: audit.auditSign}
|
||||
})
|
||||
return records.concat(reimbursementAudits)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
/** 历史接口可能返回 JSON 字符串或数组;空值、异常数据均不阻断整页展示。 */
|
||||
asList(value) {
|
||||
if (Array.isArray(value)) return value
|
||||
if (!value) return []
|
||||
try {
|
||||
const list = JSON.parse(value)
|
||||
return Array.isArray(list) ? list : []
|
||||
} catch (error) {
|
||||
return []
|
||||
}
|
||||
},
|
||||
money(value) {
|
||||
return value == null || !Number.isFinite(Number(value)) ? '—' : Number(value).toLocaleString('zh-CN', {minimumFractionDigits: 2, maximumFractionDigits: 2})
|
||||
},
|
||||
/** 签字兼容 PC 保存的 Base64 图片和历史文件路径。 */
|
||||
signUrl(value) {
|
||||
return value && value.startsWith('data:image/') ? value : '/signature/getSignData?path=' + encodeURIComponent(value || '')
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,381 @@
|
||||
// 四个报销节点沿用活动申报手机页交互;入口固定节点,报销状态与审核字段独立。
|
||||
const vue = new Vue({
|
||||
el: '#app',
|
||||
components: {'reimbursement-info': reimbursementInfo},
|
||||
data() {
|
||||
return {
|
||||
config: reimbursementReviewConfig,
|
||||
baseUrl: '/platform/jf/reimbursement/' + reimbursementReviewConfig.stage,
|
||||
pageForm: {year: new Date().getFullYear(), unionId: '', unitId: '', clubId: '', activityName: '', isAudit: 2, pageNumber: 1, pageSize: 10, searchName: 'u.username', searchKeyword: '', pageOrderName: '', pageOrderBy: 'desc'},
|
||||
tableData: [], tableLoading: false, refreshing: false, finished: false, listError: false,
|
||||
listSerial: 0, listBusy: false, detailSerial: 0,
|
||||
unions: [{text: '全部工会', value: ''}], units: [{text: '全部单位', value: ''}], organizationsLoading: false, unitsLoading: false, unitsSerial: 0,
|
||||
clubs: [{text: '全部协会', value: ''}], clubsLoading: false,
|
||||
moreForm: {}, choiceKey: '', choiceQuery: '',
|
||||
auditOptions: [{text: '未审核', value: 2}, {text: '已审核', value: 1}, {text: '全部', value: 0}],
|
||||
filterOpen: {year: false, organization: false, isAudit: false}, filterClosing: false,
|
||||
popups: {filter: false, detail: false, sign: false, confirm: false, preview: false, more: false, choice: false},
|
||||
detail: {}, detailId: '', detailLoading: false, auditMode: false,
|
||||
formData: {id: '', auditOpinion: '', auditSign: ''}, formLoading: false, reviewFlag: true,
|
||||
reviewerName: '', reviewDate: '', previewUrl: '', previewIsImage: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
organizationKey() { return this.config.stage === 'clubChairman' ? 'clubId' : this.config.canChooseUnion ? 'unionId' : 'unitId' },
|
||||
organizationOptions() { return this.config.stage === 'clubChairman' ? this.clubs : this.config.canChooseUnion ? this.unions : this.units },
|
||||
organizationLoading() { return this.config.stage === 'clubChairman' ? this.clubsLoading : this.config.canChooseUnion ? this.organizationsLoading : this.unitsLoading },
|
||||
/** 查询类型直接切换草稿字段,输入提示同步变化;确定筛选前不触发列表请求。 */
|
||||
moreSearchPlaceholder() { return this.moreForm.searchName === 'u.loginname' ? '请输入工号' : '请输入姓名' },
|
||||
searchSummary() {
|
||||
const parts = []
|
||||
if (this.pageForm.activityName) parts.push(this.pageForm.activityName)
|
||||
if (this.config.stage !== 'clubChairman' && this.pageForm.clubId) parts.push(this.optionText('clubId', this.pageForm.clubId))
|
||||
if (this.pageForm.searchKeyword) parts.push(this.pageForm.searchKeyword)
|
||||
if (this.config.canChooseUnion && this.pageForm.unitId) parts.push(this.optionText('unitId', this.pageForm.unitId))
|
||||
if (this.pageForm.pageOrderName) parts.push(this.optionText('pageOrderName', this.pageForm.pageOrderName))
|
||||
return parts.join(' · ') || '按活动名称、姓名、工号筛选'
|
||||
},
|
||||
choiceOptions() {
|
||||
return {
|
||||
unitId: this.units,
|
||||
clubId: this.clubs,
|
||||
searchName: [{text: '姓名', value: 'u.username'}, {text: '工号', value: 'u.loginname'}],
|
||||
pageOrderName: [{text: '默认排序', value: ''}, {text: '所属工会', value: 'unionname'}, {text: '活动主体类型', value: 'activity_type'}, {text: '经费类型', value: 'absName'}, {text: '申请时间', value: 'reimbursement_apply_time'}, {text: '申请状态', value: 'reimbursement_state_id'}],
|
||||
pageOrderBy: [{text: '升序', value: 'asc'}, {text: '降序', value: 'desc'}]
|
||||
}
|
||||
},
|
||||
choiceTitle() { return {clubId: '所属协会', unitId: '所属单位', searchName: '查询类型', pageOrderName: '排序字段', pageOrderBy: '排序方式'}[this.choiceKey] || '请选择' },
|
||||
filteredChoices() {
|
||||
return (this.choiceOptions[this.choiceKey] || []).filter((item) => item.text.includes(this.choiceQuery.trim()))
|
||||
},
|
||||
yearOptions() {
|
||||
return [{text: '全部年份', value: ''}].concat(Array.from({length: 11}, (_, index) => {
|
||||
const year = new Date().getFullYear() - index
|
||||
return {text: year + '年', value: year}
|
||||
}))
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
money(value) {
|
||||
return value == null || !Number.isFinite(Number(value)) ? '—' : Number(value).toLocaleString('zh-CN', {minimumFractionDigits: 2, maximumFractionDigits: 2})
|
||||
},
|
||||
shortDate(value) {
|
||||
return value ? String(value).slice(5, 10) : '—'
|
||||
},
|
||||
setRefreshing(value) { this.$set(this, 'refreshing', value) },
|
||||
setTableLoading(value) { this.$set(this, 'tableLoading', value) },
|
||||
setListError(value) { this.$set(this, 'listError', value) },
|
||||
setIdea(value) { this.$set(this.formData, 'auditOpinion', value) },
|
||||
/** 筛选切换或提交后,从第一页替换数据;旧请求晚到时不能混入当前筛选结果。 */
|
||||
doSearch() {
|
||||
this.listSerial++
|
||||
this.listBusy = false
|
||||
this.$set(this.pageForm, 'pageNumber', 1)
|
||||
this.tableData = []
|
||||
this.finished = false
|
||||
this.listError = false
|
||||
this.pageData()
|
||||
},
|
||||
/** 列表返回 data.list 和 totalCount;失败保留页码供重试,成功后才推进下一页。 */
|
||||
pageData() {
|
||||
if (this.listBusy || this.finished) return
|
||||
const serial = this.listSerial
|
||||
this.listBusy = true
|
||||
this.tableLoading = true
|
||||
this.listError = false
|
||||
$.post(this.baseUrl + '/pageData', Object.assign({}, this.pageForm))
|
||||
.then((res) => {
|
||||
if (serial !== this.listSerial || this._isDestroyed) return
|
||||
if (res.code !== 0 || !res.data) {
|
||||
this.listError = true
|
||||
this.$toast(res.msg || '列表加载失败')
|
||||
return
|
||||
}
|
||||
const list = res.data.list || []
|
||||
this.tableData = this.tableData.concat(list)
|
||||
this.finished = !list.length || this.tableData.length >= Number(res.data.totalCount)
|
||||
this.$set(this.pageForm, 'pageNumber', this.pageForm.pageNumber + 1)
|
||||
})
|
||||
.fail(() => {
|
||||
if (serial === this.listSerial && !this._isDestroyed) this.listError = true
|
||||
})
|
||||
.always(() => {
|
||||
if (serial !== this.listSerial || this._isDestroyed) return
|
||||
this.listBusy = false
|
||||
this.tableLoading = false
|
||||
this.refreshing = false
|
||||
})
|
||||
},
|
||||
/** 返回 data 数组的组织接口沿用 PC;组织数据范围由服务端控制。 */
|
||||
loadOrganizations() {
|
||||
if (this.config.showClub) this.loadClubs()
|
||||
if (!this.config.showUnit) return
|
||||
if (this.config.canChooseUnion) {
|
||||
this.organizationsLoading = true
|
||||
$.post('/platform/vi/common/unions', {isPermission: false})
|
||||
.then((res) => {
|
||||
if (this._isDestroyed) return
|
||||
if (res.code === 0) this.unions = [{text: '全部工会', value: ''}].concat((res.data || []).map((item) => ({text: item.unionname, value: item.id})))
|
||||
else this.$toast(res.msg || '工会加载失败')
|
||||
})
|
||||
.fail(() => { if (!this._isDestroyed) this.$toast('工会加载失败,请重试') })
|
||||
.always(() => { this.organizationsLoading = false })
|
||||
}
|
||||
this.loadUnits()
|
||||
},
|
||||
/** 协会选项返回 stid/name,与 PC getClubsByRole 的筛选值一致。 */
|
||||
loadClubs() {
|
||||
if (this.clubsLoading) return
|
||||
this.clubsLoading = true
|
||||
$.post('/platform/vi/common/getClubsByRole', {})
|
||||
.then((res) => {
|
||||
if (this._isDestroyed) return
|
||||
if (res.code === 0) this.clubs = [{text: '全部协会', value: ''}].concat((res.data || []).map((item) => ({text: item.name, value: item.stid})))
|
||||
else this.$toast(res.msg || '协会加载失败')
|
||||
})
|
||||
.fail(() => { if (!this._isDestroyed) this.$toast('协会加载失败,请重试') })
|
||||
.always(() => { this.clubsLoading = false })
|
||||
},
|
||||
/** 更换工会清空单位;序号避免旧响应覆盖新选项。无单位筛选的协会页不请求该接口。 */
|
||||
loadUnits() {
|
||||
if (!this.config.showUnit) return
|
||||
const serial = ++this.unitsSerial
|
||||
this.unitsLoading = true
|
||||
return $.post('/platform/vi/common/units', {unionId: this.config.canChooseUnion ? this.pageForm.unionId : this.config.currentUnionId})
|
||||
.then((res) => {
|
||||
if (serial !== this.unitsSerial || this._isDestroyed) return
|
||||
if (res.code === 0) this.units = [{text: '全部单位', value: ''}].concat((res.data || []).map((item) => ({text: item.name, value: item.id})))
|
||||
else this.$toast(res.msg || '单位加载失败')
|
||||
})
|
||||
.fail(() => { if (serial === this.unitsSerial && !this._isDestroyed) this.$toast('单位加载失败,请重试') })
|
||||
.always(() => { if (serial === this.unitsSerial) this.unitsLoading = false })
|
||||
},
|
||||
openMore() {
|
||||
this.moreForm = Object.assign({}, this.pageForm)
|
||||
this.loadUnits()
|
||||
if (this.config.showClub && this.clubs.length === 1) this.loadClubs()
|
||||
this.$options.popupSession.open('more')
|
||||
},
|
||||
setMoreField(key, value) { this.$set(this.moreForm, key, value) },
|
||||
setChoiceQuery(value) { this.$set(this, 'choiceQuery', value) },
|
||||
/** 选择项弹框压在更多筛选上层,返回时只退出当前选择,不丢弃其他筛选值。 */
|
||||
openChoice(key) {
|
||||
this.choiceKey = key
|
||||
this.choiceQuery = ''
|
||||
this.$options.popupSession.open('choice')
|
||||
},
|
||||
selectChoice(value) {
|
||||
this.$set(this.moreForm, this.choiceKey, value)
|
||||
this.$options.popupSession.close()
|
||||
},
|
||||
optionText(key, value) {
|
||||
const option = (this.choiceOptions[key] || []).find((item) => item.value === value)
|
||||
return option ? option.text : '请选择'
|
||||
},
|
||||
resetMore() {
|
||||
this.moreForm = Object.assign({}, this.moreForm, {unitId: this.config.canChooseUnion ? '' : this.pageForm.unitId, searchName: 'u.username', searchKeyword: '', activityName: '', clubId: this.config.stage === 'clubChairman' ? this.pageForm.clubId : '', pageOrderName: '', pageOrderBy: 'desc'})
|
||||
},
|
||||
applyMore() {
|
||||
const values = this.moreForm
|
||||
this.$options.popupSession.close().then(() => {
|
||||
if (this._isDestroyed) return
|
||||
const keys = ['searchName', 'searchKeyword', 'activityName', 'pageOrderName', 'pageOrderBy']
|
||||
if (this.config.showClub && this.config.stage !== 'clubChairman') keys.push('clubId')
|
||||
if (this.config.canChooseUnion) keys.push('unitId')
|
||||
keys.forEach((key) => this.$set(this.pageForm, key, values[key]))
|
||||
this.doSearch()
|
||||
})
|
||||
},
|
||||
/** key 为年度/组织/状态字段,open 为菜单展开状态;三个菜单切换时共用一层历史。 */
|
||||
setFilterOpen(key, open) {
|
||||
this.$set(this.filterOpen, key, open)
|
||||
if (open && key === 'organization') {
|
||||
if (this.organizationOptions.length === 1 && !this.organizationLoading) this.loadOrganizations()
|
||||
}
|
||||
// Vant 切换菜单会同时触发旧菜单 close 和新菜单 open,等本轮更新结束后统一判断。
|
||||
this.$nextTick(() => {
|
||||
if (this._isDestroyed || this.filterClosing) return
|
||||
if (Object.values(this.filterOpen).some(Boolean)) {
|
||||
if (!this.popups.filter) this.$options.popupSession.open('filter')
|
||||
} else if (this.popups.filter) {
|
||||
this.filterClosing = true
|
||||
this.$options.popupSession.close().then(() => { this.filterClosing = false })
|
||||
}
|
||||
})
|
||||
},
|
||||
/** key 为筛选字段,value 为对应选项值;仅值改变时刷新,菜单收起事件负责回退历史。 */
|
||||
selectFilter(key, value) {
|
||||
if (this.pageForm[key] === value) return
|
||||
this.$set(this.pageForm, key, value)
|
||||
if (key === 'unionId') {
|
||||
this.$set(this.pageForm, 'unitId', '')
|
||||
this.units = [{text: '全部单位', value: ''}]
|
||||
this.loadUnits()
|
||||
}
|
||||
this.doSearch()
|
||||
},
|
||||
/** 每次打开申请清空上次的意见和签字,防止在不同申请之间串用审核数据。 */
|
||||
openDetail(id, audit) {
|
||||
if (this.formLoading) return
|
||||
this.detailId = id
|
||||
this.detail = {}
|
||||
this.auditMode = audit
|
||||
this.formData = {id, auditOpinion: '', auditSign: ''}
|
||||
this.reviewDate = moment().format('YYYY-MM-DD')
|
||||
this.$options.popupSession.open('detail')
|
||||
this.loadDetail(id)
|
||||
},
|
||||
loadDetail(id) {
|
||||
const serial = ++this.detailSerial
|
||||
this.detailLoading = true
|
||||
$.post(this.baseUrl + '/findOne', {id})
|
||||
.then((res) => {
|
||||
if (serial !== this.detailSerial || !this.popups.detail || this._isDestroyed) return
|
||||
if (res.code === 0 && res.data) {
|
||||
this.detail = res.data
|
||||
if (!this.detail.canReview) this.auditMode = false
|
||||
} else {
|
||||
this.$toast(res.msg || '详情加载失败')
|
||||
}
|
||||
})
|
||||
.fail(() => {
|
||||
if (serial === this.detailSerial && !this._isDestroyed) this.$toast('详情加载失败,请重试')
|
||||
})
|
||||
.always(() => {
|
||||
if (serial === this.detailSerial && !this._isDestroyed) this.detailLoading = false
|
||||
})
|
||||
},
|
||||
startAudit() {
|
||||
this.auditMode = true
|
||||
this.$nextTick(() => {
|
||||
const body = this.$el.querySelector('.reimbursement-review-detail-body')
|
||||
if (body) body.scrollTop = body.scrollHeight
|
||||
})
|
||||
},
|
||||
openSignature() {
|
||||
if (!this.formLoading) this.$options.popupSession.open('sign')
|
||||
},
|
||||
/** signature 是 mobile-sign 回传的本次签字 Base64,随审核请求一起保存。 */
|
||||
setSignature(signature) {
|
||||
this.$set(this.formData, 'auditSign', signature)
|
||||
},
|
||||
confirmReview(flag) {
|
||||
if (this.formLoading || !this.detail.canReview) return
|
||||
if (!this.formData.auditOpinion.trim() || this.formData.auditOpinion.trim().length > 500 || !this.formData.auditSign) {
|
||||
this.$toast('请填写1至500字审核意见并签字')
|
||||
return
|
||||
}
|
||||
this.reviewFlag = flag
|
||||
this.$options.popupSession.open('confirm')
|
||||
},
|
||||
/** Vant 用 done 结束按钮 loading;显隐仍由历史栈驱动,避免组件自行关闭后留下历史层。 */
|
||||
beforeReviewClose(action, done) {
|
||||
if (this.formLoading) {
|
||||
done(false)
|
||||
return
|
||||
}
|
||||
if (action === 'confirm') {
|
||||
this.submitReview().always(() => done(false))
|
||||
} else {
|
||||
this.$options.popupSession.close().then(() => done(false))
|
||||
}
|
||||
},
|
||||
/** id 为报销主键,audit 为意见/签字/通过标识 JSON;返回 code=0 成功,身份与时间由服务端确定。 */
|
||||
submitReview() {
|
||||
if (this.formLoading) return
|
||||
this.formLoading = true
|
||||
const audit = {auditOpinion: this.formData.auditOpinion.trim(), auditSign: this.formData.auditSign, auditPass: this.reviewFlag}
|
||||
return $.post(this.baseUrl + '/doAuditing', {id: this.formData.id, audit: JSON.stringify(audit)})
|
||||
.then((res) => {
|
||||
if (this._isDestroyed) return
|
||||
if (res.code === 0) {
|
||||
this.$toast.success('审核成功')
|
||||
this.$options.popupSession.clear().then(() => {
|
||||
if (this._isDestroyed) return
|
||||
this.formData = {id: '', auditOpinion: '', auditSign: ''}
|
||||
this.doSearch()
|
||||
})
|
||||
} else {
|
||||
this.$toast(res.msg || '审核失败')
|
||||
// 关闭确认层后重读状态;若已被 PC 处理则切换为只读,保留未提交的意见便于核对。
|
||||
if (this.popups.confirm) this.$options.popupSession.close().then(() => this.loadDetail(this.detailId))
|
||||
}
|
||||
})
|
||||
.fail(() => this.$toast('提交失败,请检查网络后重试'))
|
||||
.always(() => { this.formLoading = false })
|
||||
},
|
||||
/** 图片使用文件流,文档沿用项目预览接口;预览自身同样占一层历史。 */
|
||||
previewFile(file) {
|
||||
if (file.imageUrl) {
|
||||
this.previewIsImage = true
|
||||
this.previewUrl = file.imageUrl
|
||||
this.$options.popupSession.open('preview')
|
||||
return
|
||||
}
|
||||
if (!file.id) {
|
||||
this.$toast('附件信息不完整')
|
||||
return
|
||||
}
|
||||
this.previewIsImage = /\.(png|jpe?g|gif|webp|bmp)$/i.test(file.filename || file.filepath || '')
|
||||
const streamUrl = '/file_server/fileStreamPreview?id=' + encodeURIComponent(file.id)
|
||||
this.previewUrl = this.previewIsImage
|
||||
? streamUrl
|
||||
: '/assets/platform/plugins/pdfJs/web/viewer.html?file=' + encodeURIComponent(streamUrl)
|
||||
this.$options.popupSession.open('preview')
|
||||
},
|
||||
closePopup() {
|
||||
if (this.formLoading) {
|
||||
this.$toast('正在提交,请稍候')
|
||||
return
|
||||
}
|
||||
this.$options.popupSession.close()
|
||||
},
|
||||
/** 有弹框时仅关闭顶层;列表返回沿用活动申报页的 PJAX 首页入口,避免浏览器历史指向其他页面。 */
|
||||
back() {
|
||||
if (Object.values(this.popups).some(Boolean)) {
|
||||
this.closePopup()
|
||||
return
|
||||
}
|
||||
this.$options.popupSession.clear().then(() => {
|
||||
this.$destroy()
|
||||
pjaxReplace('/mobile/index')
|
||||
})
|
||||
}
|
||||
},
|
||||
created() {
|
||||
// 仅注册状态同步回调;popstate 由公共管理器统一监听。
|
||||
this.$options.popupSession = window.mobilePopupHistory.register('reimbursementReview-' + this.config.stage, (layers) => {
|
||||
Object.keys(this.popups).forEach((name) => this.$set(this.popups, name, layers.includes(name)))
|
||||
// 系统返回或页面返回先收起下拉菜单;由公共历史栈驱动,页面不另绑 popstate。
|
||||
if (!layers.includes('filter')) {
|
||||
Object.keys(this.filterOpen).forEach((key) => {
|
||||
this.$set(this.filterOpen, key, false)
|
||||
const dropdown = this.$refs[key + 'Filter']
|
||||
if (dropdown) dropdown.toggle(false)
|
||||
})
|
||||
}
|
||||
})
|
||||
const user = JSON.parse(window.sessionStorage.getItem('user') || '{}')
|
||||
this.reviewerName = user.username || ''
|
||||
this.loadOrganizations()
|
||||
this.doSearch()
|
||||
const query = new URLSearchParams(window.location.search)
|
||||
const id = query.get('id')
|
||||
if (id) this.openDetail(id, query.get('mode') === 'audit')
|
||||
// PJAX 不会自动销毁旧 Vue 实例,在替换容器前注销页面回调。
|
||||
$(document).on('pjax:beforeReplace.reimbursementReview', () => this.$destroy())
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.listSerial++
|
||||
this.detailSerial++
|
||||
this.unitsSerial++
|
||||
const session = this.$options.popupSession
|
||||
if (Object.values(this.popups).some(Boolean)) {
|
||||
session.clear().then(() => session.dispose())
|
||||
} else {
|
||||
// PJAX 替换后可能立即创建新页面;空栈必须同步注销,不能等 Promise 回调再释放注册。
|
||||
session.dispose()
|
||||
}
|
||||
$(document).off('pjax:beforeReplace.reimbursementReview')
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,18 @@
|
||||
<!--#
|
||||
layout("/mobile/platform.html"){
|
||||
#-->
|
||||
<!--#include('/mobile/jf/reimbursement/review/index.html'){}#-->
|
||||
<script>
|
||||
// 本页固定审核节点;服务端按相同节点校验权限、归属及报销状态。
|
||||
const reimbursementReviewConfig = {
|
||||
stage: 'schoolChairman', title: '校工会主席审核(报销)', state: 1040,
|
||||
confirmMessage: '通过后将完成本次活动报销流程。', showClub: true, showUnit: true,
|
||||
canChooseUnion: ${@shiro.hasRole('sysadmin')||@shiro.hasRole('A06')||@shiro.hasRole('H03')},
|
||||
currentUnionId: '${@io.v.nutz.base.utils.Vi.getUnionId()}'
|
||||
}
|
||||
<!--#include('/mobile/jf/reimbursement/review/info.js'){}#-->
|
||||
<!--#include('/mobile/jf/reimbursement/review/page.js'){}#-->
|
||||
</script>
|
||||
<!--#
|
||||
}
|
||||
#-->
|
||||
@@ -0,0 +1,18 @@
|
||||
<!--#
|
||||
layout("/mobile/platform.html"){
|
||||
#-->
|
||||
<!--#include('/mobile/jf/reimbursement/review/index.html'){}#-->
|
||||
<script>
|
||||
// 本页固定审核节点;服务端按相同节点校验权限、归属及报销状态。
|
||||
const reimbursementReviewConfig = {
|
||||
stage: 'schoolDirector', title: '校工会负责人审核(报销)', state: 1020,
|
||||
confirmMessage: '通过后将转交校工会主席审核。', showClub: true, showUnit: true,
|
||||
canChooseUnion: ${@shiro.hasRole('sysadmin')||@shiro.hasRole('A06')||@shiro.hasRole('H03')},
|
||||
currentUnionId: '${@io.v.nutz.base.utils.Vi.getUnionId()}'
|
||||
}
|
||||
<!--#include('/mobile/jf/reimbursement/review/info.js'){}#-->
|
||||
<!--#include('/mobile/jf/reimbursement/review/page.js'){}#-->
|
||||
</script>
|
||||
<!--#
|
||||
}
|
||||
#-->
|
||||
@@ -0,0 +1,18 @@
|
||||
<!--#
|
||||
layout("/mobile/platform.html"){
|
||||
#-->
|
||||
<!--#include('/mobile/jf/reimbursement/review/index.html'){}#-->
|
||||
<script>
|
||||
// 本页固定审核节点;服务端按相同节点校验权限、归属及报销状态。
|
||||
const reimbursementReviewConfig = {
|
||||
stage: 'unionChairman', title: '基层工会主席审核(报销)', state: 1010,
|
||||
confirmMessage: '通过后将转交校工会负责人审核。', showClub: false, showUnit: true,
|
||||
canChooseUnion: ${@shiro.hasRole('sysadmin')||@shiro.hasRole('A06')},
|
||||
currentUnionId: '${@io.v.nutz.base.utils.Vi.getUnionId()}'
|
||||
}
|
||||
<!--#include('/mobile/jf/reimbursement/review/info.js'){}#-->
|
||||
<!--#include('/mobile/jf/reimbursement/review/page.js'){}#-->
|
||||
</script>
|
||||
<!--#
|
||||
}
|
||||
#-->
|
||||
@@ -25,6 +25,8 @@
|
||||
|
||||
<script src="${base!}/assets/platform/plugins/jquery/jquery-1.11.1.min.js"></script>
|
||||
<script src="${base!}/assets/platform/plugins/pjax/jquery.pjax.js"></script>
|
||||
<!-- PJAX 会晚于片段内脚本加载外链脚本,弹框历史管理必须随公共布局提前加载。 -->
|
||||
<script src="${base!}/assets/mobile/js/popupHistory.js"></script>
|
||||
<script src="${base!}/assets/platform/plugins/jSignature/jSignature.min.js"></script>
|
||||
<script src="${base!}/assets/platform/plugins/jSignature/flashcanvas.js"></script>
|
||||
<script src="${base!}/assets/platform/js/pinyin.js"></script>
|
||||
@@ -79,7 +81,9 @@
|
||||
}
|
||||
})()
|
||||
|
||||
function pjaxReplace(url){
|
||||
window.pjaxReplace = (url) => {
|
||||
// 普通表单的导航也先消费公共签字层,避免携带未回退的弹框历史进入下一页面。
|
||||
if (window.mobilePopupHistory.closeTop()) return
|
||||
$.pjax({
|
||||
url: url,
|
||||
container: "#container",
|
||||
|
||||
@@ -1,53 +1,49 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<title>签字</title>
|
||||
<meta charset="UTF-8" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, viewport-fit=cover" />
|
||||
|
||||
<link rel="stylesheet" href="${base!}/assets/platform/plugins/vant/vant.css">
|
||||
<script src="${base!}/assets/platform/plugins/vue/vue.js"></script>
|
||||
<script src="${base!}/assets/platform/plugins/vant/vant.js"></script>
|
||||
<script src="${base!}/assets/platform/plugins/vue-loader/httpVueLoader.js"></script>
|
||||
<script src="${base!}/assets/platform/plugins/smooth-signature/index.umd.min.js"></script>
|
||||
<script src="${base!}/assets/platform/plugins/jquery/jquery-1.11.1.min.js"></script>
|
||||
<script src="${base!}/assets/mobile/js/popupHistory.js"></script>
|
||||
<style>body { margin: 0; background: #f7f8fa; } [v-cloak] { display: none; }</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app">
|
||||
<signature @save="save"></signature>
|
||||
<div id="app" v-cloak>
|
||||
<van-nav-bar title="签字"></van-nav-bar>
|
||||
<van-notice-bar v-if="subDis" text="缺少签字参数,请重新扫码。"></van-notice-bar>
|
||||
<mobile-sign :value="sign" :save-to-profile="false" :popup-visible="showSignaturePanel"
|
||||
:disabled="subDis || formLoading" @input="save" @open="openSignature" @close="closeSignature"></mobile-sign>
|
||||
</div>
|
||||
</body>
|
||||
<script>
|
||||
function getQueryVariable(variable) {
|
||||
var query = window.location.search.substring(1);
|
||||
var vars = query.split("&");
|
||||
for (var i = 0; i < vars.length; i++) {
|
||||
var pair = vars[i].split("=");
|
||||
if (pair[0] == variable) {
|
||||
return pair[1];
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
new Vue({
|
||||
el: "#app",
|
||||
components: {
|
||||
signature: httpVueLoader("/components/sign/index.vue?v=" + new Date().getTime())
|
||||
'mobile-sign': httpVueLoader("/components/sign/mobileSign.vue?v=" + new Date().getTime())
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
prefix: '',
|
||||
id: '',
|
||||
isNull: function (val) {
|
||||
if (val === null || val === undefined || val === '') {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
},
|
||||
}
|
||||
return {prefix: '', id: '', sign: '', subDis: false, formLoading: false, showSignaturePanel: false}
|
||||
},
|
||||
methods: {
|
||||
openSignature() {
|
||||
if (!this.subDis && !this.formLoading) this.$options.popupSession.open('sign')
|
||||
},
|
||||
closeSignature() {
|
||||
if (!this.formLoading) this.$options.popupSession.close()
|
||||
},
|
||||
/**
|
||||
* signature 为公共签字板回传的 PNG Base64;prefix/id 来自二维码,关联 PC 端轮询。
|
||||
* 保留 is_value_base64=false 的文件保存方式;接口返回 {code, msg},code=0 表示成功。
|
||||
*/
|
||||
save(signature) {
|
||||
if (this.formLoading || this.subDis) return
|
||||
this.sign = signature
|
||||
this.formLoading = true
|
||||
$.post("/signature/doSub", {
|
||||
prefix: this.prefix,
|
||||
id: this.id,
|
||||
@@ -56,44 +52,31 @@
|
||||
})
|
||||
.then((res) => {
|
||||
if (res.code === 0) {
|
||||
alert("保存成功")
|
||||
this.showSignaturePanel = false
|
||||
this.$toast.success("保存成功")
|
||||
this.$options.popupSession.clear()
|
||||
} else {
|
||||
alert("保存失败")
|
||||
this.$toast.fail(res.msg || "保存失败,请重试")
|
||||
}
|
||||
})
|
||||
|
||||
},
|
||||
base64ToFile(base64Data, filename) {
|
||||
// 将base64的数据部分提取出来
|
||||
const parts = base64Data.split(";base64,")
|
||||
const contentType = parts[0].split(":")[1]
|
||||
const raw = window.atob(parts[1])
|
||||
const rawLength = raw.length
|
||||
const uInt8Array = new Uint8Array(rawLength)
|
||||
|
||||
for (let i = 0; i < rawLength; ++i) {
|
||||
uInt8Array[i] = raw.charCodeAt(i)
|
||||
}
|
||||
|
||||
// 使用Blob对象创建File对象
|
||||
const blob = new Blob([uInt8Array], { type: contentType })
|
||||
blob.lastModifiedDate = new Date()
|
||||
blob.name = filename
|
||||
return new File([blob], filename, { type: contentType })
|
||||
.fail(() => this.$toast.fail("保存失败,请重试"))
|
||||
.always(() => { this.formLoading = false })
|
||||
}
|
||||
},
|
||||
created: function () {
|
||||
if (this.isNull(getQueryVariable("id")) || this.isNull(getQueryVariable("prefix"))) {
|
||||
this.$notify({
|
||||
message: '缺少参数!',
|
||||
type: 'danger'
|
||||
});
|
||||
this.subDis = true
|
||||
}
|
||||
this.id = getQueryVariable("id")
|
||||
this.prefix = getQueryVariable("prefix")
|
||||
created() {
|
||||
// 扫码入口也是独立页面,使用公共历史栈统一处理返回、取消及保存成功后的清理。
|
||||
this.$options.popupSession = window.mobilePopupHistory.register('qrSignature', (layers) => {
|
||||
this.showSignaturePanel = layers.includes('sign')
|
||||
})
|
||||
const query = new URLSearchParams(window.location.search)
|
||||
this.id = query.get('id') || ''
|
||||
this.prefix = query.get('prefix') || ''
|
||||
this.subDis = !this.id || !this.prefix
|
||||
},
|
||||
beforeDestroy() {
|
||||
const session = this.$options.popupSession
|
||||
session.clear().then(() => session.dispose())
|
||||
}
|
||||
})
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user