签字优化
This commit is contained in:
@@ -172,7 +172,7 @@ layout("/mobile/platform.html"){
|
||||
|
||||
<div id="app">
|
||||
<!--首页-->
|
||||
<div style="margin-bottom: 60px" v-if="active === 0">
|
||||
<div v-if="active === 0">
|
||||
<!--轮播图-->
|
||||
<van-swipe :autoplay="3000" class="swiper">
|
||||
<van-swipe-item :key="index" v-for="(image, index) in images">
|
||||
@@ -385,7 +385,7 @@ layout("/mobile/platform.html"){
|
||||
</div>
|
||||
|
||||
<!--工作台-->
|
||||
<div style="margin-bottom: 60px" v-if="active === 2">
|
||||
<div v-if="active === 2">
|
||||
<van-row class="module" v-for="m in moduleMenus">
|
||||
<div class="van-sidebar-item van-sidebar-item--select">
|
||||
{{m.moduleName}}
|
||||
@@ -401,7 +401,8 @@ layout("/mobile/platform.html"){
|
||||
</van-row>
|
||||
</div>
|
||||
|
||||
<van-tabbar @change="tabbrChange" v-model="active">
|
||||
<!-- 按底部导航实际高度(含手机安全区域)占位,确保末行菜单可完整滚动到导航上方。 -->
|
||||
<van-tabbar @change="tabbrChange" v-model="active" placeholder>
|
||||
<van-tabbar-item icon="home-o">首页</van-tabbar-item>
|
||||
<van-tabbar-item :dot="needCount" icon="envelop-o">我的办理</van-tabbar-item>
|
||||
<van-tabbar-item icon="apps-o">工作台</van-tabbar-item>
|
||||
|
||||
@@ -33,6 +33,8 @@ layout("/mobile/platform.html"){
|
||||
.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-budget-item + .association-budget-item { border-top: 1px solid #f2f3f5; }
|
||||
.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; }
|
||||
@@ -127,7 +129,7 @@ layout("/mobile/platform.html"){
|
||||
</van-popup>
|
||||
</div>
|
||||
<script>
|
||||
<!--#include('info.js'){}#-->
|
||||
<!--#include('/mobile/jf/activitybx/review/info.js'){}#-->
|
||||
<!--#include('page.js'){}#-->
|
||||
</script>
|
||||
<!--#
|
||||
|
||||
@@ -1,104 +0,0 @@
|
||||
/** 协会活动申报详情:仅展示服务端已校验权限的数据,审核表单通过末尾插槽传入。 */
|
||||
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 || '')
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
// 独立手机审核页面;不复用旧 declare 页面及其有缺陷的滚动分页。
|
||||
const vue = new Vue({
|
||||
el: '#app',
|
||||
components: {'association-info': associationInfo},
|
||||
components: {'association-info': activityReviewInfo},
|
||||
data() {
|
||||
return {
|
||||
baseUrl: '/platform/jf/activity_bx/association',
|
||||
|
||||
@@ -1,67 +1,69 @@
|
||||
/** 活动申报审核详情:仅展示服务端已校验权限的数据,审核表单通过末尾插槽传入。 */
|
||||
/** 四个活动申报审核入口共用详情;可选字段按返回数据显示,审核表单通过末尾插槽传入。 */
|
||||
const activityReviewInfo = {
|
||||
template: /*language=HTML*/ `
|
||||
<div class="review-info">
|
||||
<section class="review-section">
|
||||
<!-- 同时保留两套入口的既有样式类,详情内容和显隐规则共用一份。 -->
|
||||
<div class="review-info association-info">
|
||||
<section class="review-section 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.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 v-if="data.bxr_unionname && String(data.bxr_unionname).trim()" title="所属工会" :value="data.bxr_unionname"></van-cell>
|
||||
<van-cell v-if="data.bxr_unitname && String(data.bxr_unitname).trim()" title="所属单位" :value="data.bxr_unitname"></van-cell>
|
||||
<van-cell v-if="data.activity_type" title="活动主体" :value="activityType"></van-cell>
|
||||
<van-cell v-if="data.apply_type" 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="Number(data.activity_type) === 40003 ? '承办协会' : '承办单位/协会'" :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 v-if="data.startTime || data.endTime" title="活动时间" :value="(data.startTime || '—') + ' 至 ' + (data.endTime || '—')"></van-cell>
|
||||
<!-- 零元是有效费用;仅未提供金额时隐藏,不能用金额真假判断。 -->
|
||||
<van-cell v-if="data.activity_money != null && String(data.activity_money).trim() !== ''" 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">
|
||||
<section class="review-section association-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})">
|
||||
<div class="review-description association-description" style="white-space:normal;min-width:0"
|
||||
v-html="activityHtml || '暂无'" @click="previewActivityImage"></div>
|
||||
</section>
|
||||
<section class="review-section">
|
||||
<section class="review-section association-section">
|
||||
<h3>预算明细</h3>
|
||||
<div v-for="(item, index) in budgets" :key="index" class="review-budget-item">
|
||||
<div v-for="(item, index) in budgets" :key="index" class="review-budget-item association-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>
|
||||
<van-cell v-if="item.actualPrice != null && String(item.actualPrice).trim() !== ''" title="实际费用" :value="'¥' + money(item.actualPrice)"></van-cell>
|
||||
<van-cell v-if="item.username" title="收款人" :value="item.username"></van-cell>
|
||||
<van-cell v-if="item.bankName" title="支行名称" :value="item.bankName"></van-cell>
|
||||
<van-cell v-if="item.bankCardNum" title="报销卡号" :value="item.bankCardNum"></van-cell>
|
||||
<van-cell v-if="item.remark" title="备注" :value="item.remark"></van-cell>
|
||||
</template>
|
||||
</div>
|
||||
<van-cell title="预算合计" :value="'¥' + money(data.funds_money)" class="review-total"></van-cell>
|
||||
<van-cell title="预算合计" :value="'¥' + money(data.funds_money)" class="review-total association-total"></van-cell>
|
||||
</section>
|
||||
<section class="review-section">
|
||||
<section class="review-section association-section">
|
||||
<h3>申请附件</h3>
|
||||
<van-cell v-for="(file, index) in files" :key="file.id || index" :title="file.filename || file.name || '附件'" class="review-file"
|
||||
<van-cell v-for="(file, index) in files" :key="file.id || index" :title="file.filename || file.name || '附件'" class="review-file association-file"
|
||||
value="查看" is-link @click="$emit('preview', file)"></van-cell>
|
||||
<p v-if="!files.length" class="review-muted">暂无附件</p>
|
||||
<p v-if="!files.length" class="review-muted association-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>
|
||||
<img v-if="data.us_s" class="review-sign-image association-sign-image" :src="signUrl(data.us_s)" alt="申请人签字">
|
||||
<p v-else class="review-muted association-muted">暂无签字</p>
|
||||
</section>
|
||||
<section v-for="group in reimbursementFiles" :key="group.key" class="review-section">
|
||||
<section v-for="group in reimbursementFiles" :key="group.key" class="review-section association-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>
|
||||
class="review-file association-file" value="查看" is-link @click="$emit('preview', file)"></van-cell>
|
||||
<p v-if="!group.files.length" class="review-muted association-muted">暂无附件</p>
|
||||
</section>
|
||||
<section v-for="audit in audits" :key="audit.key" class="review-section">
|
||||
<section v-for="audit in audits" :key="audit.key" class="review-section 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="review-sign-image" :src="signUrl(audit.sign)" :alt="audit.title + '签字'">
|
||||
<img v-if="audit.sign" class="review-sign-image association-sign-image" :src="signUrl(audit.sign)" :alt="audit.title + '签字'">
|
||||
</section>
|
||||
<slot></slot>
|
||||
</div>
|
||||
@@ -74,9 +76,11 @@ const activityReviewInfo = {
|
||||
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])}))
|
||||
.filter((group) => group.files.length)
|
||||
},
|
||||
budgets() {
|
||||
return this.asList(this.data.budgets)
|
||||
@@ -84,18 +88,94 @@ const activityReviewInfo = {
|
||||
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 属性和事件。
|
||||
activityHtml() {
|
||||
// activity_content 为历史富文本;只重建允许的内容节点,不复制脚本、事件、类名或任意内联样式。
|
||||
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))
|
||||
const container = content.createElement('div')
|
||||
const allowed = new Set(['P', 'DIV', 'SPAN', 'BR', 'B', 'STRONG', 'I', 'EM', 'U', 'S', 'DEL', 'SUB', 'SUP',
|
||||
'H1', 'H2', 'H3', 'H4', 'H5', 'H6', 'UL', 'OL', 'LI', 'BLOCKQUOTE', 'PRE', 'CODE', 'HR',
|
||||
'TABLE', 'THEAD', 'TBODY', 'TFOOT', 'TR', 'TH', 'TD', 'CAPTION', 'IMG', 'FONT', 'MARK'])
|
||||
const blocked = new Set(['SCRIPT', 'STYLE', 'IFRAME', 'OBJECT', 'EMBED', 'SVG', 'MATH', 'TEMPLATE',
|
||||
'FORM', 'INPUT', 'BUTTON', 'TEXTAREA', 'SELECT', 'LINK', 'META', 'BASE'])
|
||||
const appendContent = (source, parent) => {
|
||||
if (source.nodeType === 3) {
|
||||
parent.appendChild(content.createTextNode(source.textContent))
|
||||
return
|
||||
}
|
||||
if (source.nodeType !== 1 || blocked.has(source.tagName)) return
|
||||
const tag = source.tagName
|
||||
if (!allowed.has(tag)) {
|
||||
Array.from(source.childNodes).forEach((child) => appendContent(child, parent))
|
||||
return
|
||||
}
|
||||
const node = content.createElement(tag === 'FONT' ? 'span' : tag.toLowerCase())
|
||||
// 仅保留文字强调、颜色和对齐;固定宽高、定位和 Word 段落间距不带入手机页面。
|
||||
const textStyles = ['color', 'backgroundColor', 'fontWeight', 'fontStyle', 'textDecorationLine', 'textAlign', 'verticalAlign']
|
||||
textStyles.forEach((name) => { node.style[name] = source.style[name] })
|
||||
if (tag === 'FONT' && !node.style.color) node.style.color = source.getAttribute('color') || ''
|
||||
if (!node.style.textAlign && /^(left|center|right|justify)$/i.test(source.getAttribute('align') || '')) {
|
||||
node.style.textAlign = source.getAttribute('align')
|
||||
}
|
||||
if (/^(P|DIV|H[1-6]|BLOCKQUOTE|PRE|UL|OL)$/.test(tag)) {
|
||||
node.style.margin = '0 0 8px'
|
||||
node.style.lineHeight = '1.8'
|
||||
}
|
||||
if (/^H[1-6]$/.test(tag)) node.style.fontSize = '16px'
|
||||
if (tag === 'UL' || tag === 'OL') {
|
||||
node.style.paddingLeft = '24px'
|
||||
node.style.listStyleType = tag === 'UL' ? 'disc' : 'decimal'
|
||||
if (tag === 'OL' && /^-?\d{1,6}$/.test(source.getAttribute('start') || '')) {
|
||||
node.setAttribute('start', source.getAttribute('start'))
|
||||
}
|
||||
}
|
||||
if (tag === 'PRE') {
|
||||
node.style.whiteSpace = 'pre-wrap'
|
||||
node.style.overflowWrap = 'anywhere'
|
||||
}
|
||||
if (tag === 'IMG') {
|
||||
// 图片沿用原有地址范围;保留正文中的位置,点击后交给现有预览弹框和历史栈。
|
||||
const url = (source.getAttribute('src') || '').trim()
|
||||
if (!/^(https?:\/\/|\/(?![\/\\])|data:image\/(?:png|jpeg|gif|webp);base64,)/i.test(url)) return
|
||||
node.setAttribute('src', url)
|
||||
node.setAttribute('alt', source.getAttribute('alt') || '活动方案配图')
|
||||
node.style.cssText = 'display:block;max-width:100%;height:auto;margin:8px 0;cursor:pointer'
|
||||
}
|
||||
if (tag === 'TABLE') {
|
||||
// 每张表单独横向滚动,保留行列关系;不让宽表撑开整个审核页面。
|
||||
const scroll = content.createElement('div')
|
||||
scroll.style.cssText = 'max-width:100%;overflow-x:auto;margin:8px 0;white-space:normal'
|
||||
parent.appendChild(scroll)
|
||||
parent = scroll
|
||||
node.style.width = '100%'
|
||||
node.style.borderCollapse = 'collapse'
|
||||
node.style.tableLayout = 'auto'
|
||||
}
|
||||
if (tag === 'TD' || tag === 'TH') {
|
||||
node.style.border = '1px solid #dcdfe6'
|
||||
node.style.padding = '6px 8px'
|
||||
node.style.minWidth = '72px'
|
||||
node.style.overflowWrap = 'anywhere'
|
||||
// 合并单元格只接受浏览器支持范围内的整数,保留预算表的表头和分组结构。
|
||||
;['rowspan', 'colspan'].forEach((name) => {
|
||||
const value = source.getAttribute(name)
|
||||
if (/^\d{1,4}$/.test(value || '') && Number(value) >= 1 && Number(value) <= 1000) {
|
||||
node.setAttribute(name, value)
|
||||
}
|
||||
})
|
||||
}
|
||||
Array.from(source.childNodes).forEach((child) => appendContent(child, node))
|
||||
parent.appendChild(node)
|
||||
}
|
||||
if (!content.body.children.length) {
|
||||
// 旧手机申请可能保存纯文字;保留用户输入的换行,但不把它作为 HTML 再解析。
|
||||
const plain = content.createElement('div')
|
||||
plain.style.whiteSpace = 'pre-wrap'
|
||||
plain.textContent = content.body.textContent.trim()
|
||||
container.appendChild(plain)
|
||||
} else {
|
||||
Array.from(content.body.childNodes).forEach((node) => appendContent(node, container))
|
||||
}
|
||||
return container.textContent.trim() || container.querySelector('img') ? container.innerHTML : ''
|
||||
},
|
||||
audits() {
|
||||
const data = this.data
|
||||
@@ -124,6 +204,11 @@ const activityReviewInfo = {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
/** event 为正文点击事件;仅图片触发 preview,参数 imageUrl 交由父页面现有弹框处理,无返回值。 */
|
||||
previewActivityImage(event) {
|
||||
const target = event.target
|
||||
if (target && target.tagName === 'IMG') this.$emit('preview', {imageUrl: target.getAttribute('src')})
|
||||
},
|
||||
/** 历史接口可能返回 JSON 字符串或数组;空值、异常数据均不阻断整页展示。 */
|
||||
asList(value) {
|
||||
if (Array.isArray(value)) return value
|
||||
|
||||
@@ -23,9 +23,8 @@ const reimbursementInfo = {
|
||||
</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})">
|
||||
<div class="reimbursement-review-description" style="white-space:normal;min-width:0"
|
||||
v-html="activityHtml || '暂无'" @click="previewActivityImage"></div>
|
||||
</section>
|
||||
<section class="reimbursement-review-section">
|
||||
<h3>费用及收款信息</h3>
|
||||
@@ -85,18 +84,94 @@ const reimbursementInfo = {
|
||||
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 属性和事件。
|
||||
activityHtml() {
|
||||
// activity_content 为历史富文本;只重建允许的内容节点,不复制脚本、事件、类名或任意内联样式。
|
||||
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))
|
||||
const container = content.createElement('div')
|
||||
const allowed = new Set(['P', 'DIV', 'SPAN', 'BR', 'B', 'STRONG', 'I', 'EM', 'U', 'S', 'DEL', 'SUB', 'SUP',
|
||||
'H1', 'H2', 'H3', 'H4', 'H5', 'H6', 'UL', 'OL', 'LI', 'BLOCKQUOTE', 'PRE', 'CODE', 'HR',
|
||||
'TABLE', 'THEAD', 'TBODY', 'TFOOT', 'TR', 'TH', 'TD', 'CAPTION', 'IMG', 'FONT', 'MARK'])
|
||||
const blocked = new Set(['SCRIPT', 'STYLE', 'IFRAME', 'OBJECT', 'EMBED', 'SVG', 'MATH', 'TEMPLATE',
|
||||
'FORM', 'INPUT', 'BUTTON', 'TEXTAREA', 'SELECT', 'LINK', 'META', 'BASE'])
|
||||
const appendContent = (source, parent) => {
|
||||
if (source.nodeType === 3) {
|
||||
parent.appendChild(content.createTextNode(source.textContent))
|
||||
return
|
||||
}
|
||||
if (source.nodeType !== 1 || blocked.has(source.tagName)) return
|
||||
const tag = source.tagName
|
||||
if (!allowed.has(tag)) {
|
||||
Array.from(source.childNodes).forEach((child) => appendContent(child, parent))
|
||||
return
|
||||
}
|
||||
const node = content.createElement(tag === 'FONT' ? 'span' : tag.toLowerCase())
|
||||
// 仅保留文字强调、颜色和对齐;固定宽高、定位和 Word 段落间距不带入手机页面。
|
||||
const textStyles = ['color', 'backgroundColor', 'fontWeight', 'fontStyle', 'textDecorationLine', 'textAlign', 'verticalAlign']
|
||||
textStyles.forEach((name) => { node.style[name] = source.style[name] })
|
||||
if (tag === 'FONT' && !node.style.color) node.style.color = source.getAttribute('color') || ''
|
||||
if (!node.style.textAlign && /^(left|center|right|justify)$/i.test(source.getAttribute('align') || '')) {
|
||||
node.style.textAlign = source.getAttribute('align')
|
||||
}
|
||||
if (/^(P|DIV|H[1-6]|BLOCKQUOTE|PRE|UL|OL)$/.test(tag)) {
|
||||
node.style.margin = '0 0 8px'
|
||||
node.style.lineHeight = '1.8'
|
||||
}
|
||||
if (/^H[1-6]$/.test(tag)) node.style.fontSize = '16px'
|
||||
if (tag === 'UL' || tag === 'OL') {
|
||||
node.style.paddingLeft = '24px'
|
||||
node.style.listStyleType = tag === 'UL' ? 'disc' : 'decimal'
|
||||
if (tag === 'OL' && /^-?\d{1,6}$/.test(source.getAttribute('start') || '')) {
|
||||
node.setAttribute('start', source.getAttribute('start'))
|
||||
}
|
||||
}
|
||||
if (tag === 'PRE') {
|
||||
node.style.whiteSpace = 'pre-wrap'
|
||||
node.style.overflowWrap = 'anywhere'
|
||||
}
|
||||
if (tag === 'IMG') {
|
||||
// 图片沿用原有地址范围;保留正文中的位置,点击后交给现有预览弹框和历史栈。
|
||||
const url = (source.getAttribute('src') || '').trim()
|
||||
if (!/^(https?:\/\/|\/(?![\/\\])|data:image\/(?:png|jpeg|gif|webp);base64,)/i.test(url)) return
|
||||
node.setAttribute('src', url)
|
||||
node.setAttribute('alt', source.getAttribute('alt') || '活动方案配图')
|
||||
node.style.cssText = 'display:block;max-width:100%;height:auto;margin:8px 0;cursor:pointer'
|
||||
}
|
||||
if (tag === 'TABLE') {
|
||||
// 每张表单独横向滚动,保留行列关系;不让宽表撑开整个审核页面。
|
||||
const scroll = content.createElement('div')
|
||||
scroll.style.cssText = 'max-width:100%;overflow-x:auto;margin:8px 0;white-space:normal'
|
||||
parent.appendChild(scroll)
|
||||
parent = scroll
|
||||
node.style.width = '100%'
|
||||
node.style.borderCollapse = 'collapse'
|
||||
node.style.tableLayout = 'auto'
|
||||
}
|
||||
if (tag === 'TD' || tag === 'TH') {
|
||||
node.style.border = '1px solid #dcdfe6'
|
||||
node.style.padding = '6px 8px'
|
||||
node.style.minWidth = '72px'
|
||||
node.style.overflowWrap = 'anywhere'
|
||||
// 合并单元格只接受浏览器支持范围内的整数,保留预算表的表头和分组结构。
|
||||
;['rowspan', 'colspan'].forEach((name) => {
|
||||
const value = source.getAttribute(name)
|
||||
if (/^\d{1,4}$/.test(value || '') && Number(value) >= 1 && Number(value) <= 1000) {
|
||||
node.setAttribute(name, value)
|
||||
}
|
||||
})
|
||||
}
|
||||
Array.from(source.childNodes).forEach((child) => appendContent(child, node))
|
||||
parent.appendChild(node)
|
||||
}
|
||||
if (!content.body.children.length) {
|
||||
// 旧手机申请可能保存纯文字;保留用户输入的换行,但不把它作为 HTML 再解析。
|
||||
const plain = content.createElement('div')
|
||||
plain.style.whiteSpace = 'pre-wrap'
|
||||
plain.textContent = content.body.textContent.trim()
|
||||
container.appendChild(plain)
|
||||
} else {
|
||||
Array.from(content.body.childNodes).forEach((node) => appendContent(node, container))
|
||||
}
|
||||
return container.textContent.trim() || container.querySelector('img') ? container.innerHTML : ''
|
||||
},
|
||||
audits() {
|
||||
const data = this.data
|
||||
@@ -125,6 +200,11 @@ const reimbursementInfo = {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
/** event 为正文点击事件;仅图片触发 preview,参数 imageUrl 交由父页面现有弹框处理,无返回值。 */
|
||||
previewActivityImage(event) {
|
||||
const target = event.target
|
||||
if (target && target.tagName === 'IMG') this.$emit('preview', {imageUrl: target.getAttribute('src')})
|
||||
},
|
||||
/** 历史接口可能返回 JSON 字符串或数组;空值、异常数据均不阻断整页展示。 */
|
||||
asList(value) {
|
||||
if (Array.isArray(value)) return value
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<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"
|
||||
<mobile-sign :value="sign" :is_value_base64="isValueBase64" :popup-visible="showSignaturePanel"
|
||||
:disabled="subDis || formLoading" @input="save" @open="openSignature" @close="closeSignature"></mobile-sign>
|
||||
</div>
|
||||
<script>
|
||||
@@ -27,7 +27,7 @@
|
||||
'mobile-sign': httpVueLoader("/components/sign/mobileSign.vue?v=" + new Date().getTime())
|
||||
},
|
||||
data() {
|
||||
return {prefix: '', id: '', sign: '', subDis: false, formLoading: false, showSignaturePanel: false}
|
||||
return {prefix: '', id: '', sign: '', isValueBase64: true, subDis: false, formLoading: false, showSignaturePanel: false}
|
||||
},
|
||||
methods: {
|
||||
openSignature() {
|
||||
@@ -37,8 +37,8 @@
|
||||
if (!this.formLoading) this.$options.popupSession.close()
|
||||
},
|
||||
/**
|
||||
* signature 为公共签字板回传的 PNG Base64;prefix/id 来自二维码,关联 PC 端轮询。
|
||||
* 保留 is_value_base64=false 的文件保存方式;接口返回 {code, msg},code=0 表示成功。
|
||||
* signature 为组件已保存的个人签名:true 为 Base64,false 为文件路径。
|
||||
* prefix/id 和 is_value_base64 来自二维码;接口返回 {code,msg,data},code=0 表示发布给 PC 成功。
|
||||
*/
|
||||
save(signature) {
|
||||
if (this.formLoading || this.subDis) return
|
||||
@@ -48,7 +48,7 @@
|
||||
prefix: this.prefix,
|
||||
id: this.id,
|
||||
base64: signature,
|
||||
is_value_base64: false
|
||||
is_value_base64: this.isValueBase64
|
||||
})
|
||||
.then((res) => {
|
||||
if (res.code === 0) {
|
||||
@@ -70,7 +70,10 @@
|
||||
const query = new URLSearchParams(window.location.search)
|
||||
this.id = query.get('id') || ''
|
||||
this.prefix = query.get('prefix') || ''
|
||||
this.subDis = !this.id || !this.prefix
|
||||
// 缺省沿用 PC 默认 true;显式 false 使用文件签名,异常参数不默默切换类型。
|
||||
const signType = query.get('is_value_base64')
|
||||
this.isValueBase64 = signType !== 'false'
|
||||
this.subDis = !this.id || !this.prefix || (signType !== null && signType !== 'true' && signType !== 'false')
|
||||
},
|
||||
beforeDestroy() {
|
||||
const session = this.$options.popupSession
|
||||
|
||||
Reference in New Issue
Block a user