Browse Source

年报管理2

master
xuhuajiao 4 weeks ago
parent
commit
3a9afd6740
  1. 12
      src/api/statistics/statistics.js
  2. 242
      src/views/archivesStatistics/Da3Report.vue
  3. 61
      src/views/archivesStatistics/annualReport‌/index.vue

12
src/api/statistics/statistics.js

@ -60,9 +60,17 @@ export function FetchBorrowdRegisterStatisticsList(params) {
// } // }
// 报表数据详情 // 报表数据详情
export function FetchInitStatisticsTemplate(data) {
export function FetchInitStatisticsTemplate(params) {
return request({ return request({
url: 'api/archives-report/init',
url: 'api/archives-report/init' + '?' + qs.stringify(params, { indices: false }),
method: 'get'
})
}
// 保存报表填报数据
export function SaveStatisticsTemplate(data) {
return request({
url: 'api/archives-report/save',
method: 'post', method: 'post',
data data
}) })

242
src/views/archivesStatistics/Da3Report.vue

@ -1,20 +1,23 @@
<script> <script>
import embeddedModel from './da3-model.js'
// import embeddedModel from './da3-model.js'
import { FetchInitStatisticsTemplate, SaveStatisticsTemplate } from '@/api/statistics/statistics'
export default { export default {
name: 'Da3Report', name: 'Da3Report',
props: {
// 后台聚合返回的数据模型(template/units/body/footer);不传则用内置 demo 数据
model: { type: Object, default: null }
},
data() { data() {
return { return {
form: {}
form: {},
// 接口返回的报表模型
remoteModel: null,
// 接口返回的原始数据(保存时按同结构回传)
rawReport: null,
loading: false,
saving: false
} }
}, },
computed: { computed: {
data() { data() {
return this.model || embeddedModel
return this.remoteModel
}, },
// 查看模式(不可输入)还是编辑模式(可输入),默认查看 // 查看模式(不可输入)还是编辑模式(可输入),默认查看
isView() { isView() {
@ -40,13 +43,172 @@ export default {
data: { data: {
handler(d) { handler(d) {
this.initForm(d) this.initForm(d)
// TODO: 接口就绪后,改为按路由参数(id)请求真实填报数据后回显
this.fillDemo()
// 接口模型自带填报值时回显
if (d.formData) this.fillFormData(d.formData)
}, },
immediate: true immediate: true
} }
}, },
created() {
this.fetchReport()
},
methods: { methods: {
// 按路由参数拉取年报详情;无参数(独立预览)时使用 mock 数据
fetchReport() {
const { reportingYear, unitId, templateId } = this.$route.query
// if (!reportingYear || !unitId) {
// this.fillDemo()
// return
// }
this.loading = true
FetchInitStatisticsTemplate({
reportingYear,
unitId,
templateId: Number(templateId) || 1
}).then(res => {
console.log('[Da3Report] FetchInitStatisticsTemplate res:', res)
if (res) {
// 留存接口原始数据,保存时按同结构回传
this.rawReport = res
// 将接口结构(顶层表头字段 + rows 树形嵌套)归一化为组件模型 { template/units/body/footer/formData }
const model = this.normalizeModel(res)
if (model) {
// remoteModel 变化会触发 watch(data):initForm 重建表单后自动用 formData 回填
this.remoteModel = model
}
}
}).catch(() => {
// 接口异常时兜底展示 mock 数据
// this.fillDemo()
}).finally(() => {
this.loading = false
})
},
// 将接口返回结构归一化为组件内置模型结构:{ template, units, body, footer, formData }
normalizeModel(res) {
if (!res || !Array.isArray(res.rows)) return null
// 1. rows 是 children 树形嵌套,先递归拍平,再按 displayOrder 升序还原真实展示顺序
const flat = []
const walk = list => {
list.forEach(r => {
flat.push(r)
if (r.children && r.children.length) walk(r.children)
})
}
walk(res.rows)
flat.sort((a, b) => (a.displayOrder || 0) - (b.displayOrder || 0))
// 2. 01-07 单位信息填报值(顶层字段优先,row.valCurText 兜底)
const unitValues = {
'01': res.unitCreditCode,
'02': res.unitName,
'03': res.unitCategoryCode,
'04': res.postalCode,
'05': res.unitAddress,
'06': res.telephone,
'07': res.orgSituation
}
const units = []
const body = []
const formData = { units: {}, data: {}}
const leftCards = ['01', '02', '03', '04', '05']
// 填报值统一以文本字段(valCurText/valPrevText)为准,text 为空时再回退取数值字段
const pickVal = (text, num) => (text != null && text !== '' ? text : (num != null ? num : ''))
flat.forEach(r => {
// 章节(section)或顶级叶子指标(如 82/95/96/97/109)需把中文序号拼到名称前:一、人员情况 / 五、检索工具
const needPrefix = (r.itemType === 'section' || (r.isLeaf && r.level === 0)) &&
r.sectionNo && r.sectionNo !== '表头'
const fullName = needPrefix ? r.sectionNo + '、' + r.name : r.name
// 3. 表头单位信息 01-07
if (r.itemType === 'unit') {
units.push({
code: r.code,
label: r.name,
card: leftCards.indexOf(r.code) !== -1 ? 1 : 2,
kind: r.inputType === 'code' ? 'code' : 'text'
})
formData.units[r.code] = unitValues[r.code] != null ? unitValues[r.code] : (r.valCurText || '')
return
}
// 4. 主体行:section / group / indicator / continuation
let type
if (r.itemType === 'section') {
type = 'section'
} else if (r.isGroup) {
type = 'group'
} else if (r.name === '(续行)' || r.name === '') {
type = 'continuation'
} else {
type = 'indicator'
}
const item = {
type,
code: r.code || '',
name: type === 'continuation' ? '' : fullName,
unit: (type === 'section' || type === 'group') ? (r.unit || '—') : (r.unit || (r.inputType === 'yesno' ? '—' : '')),
level: r.level
}
if (type === 'indicator' || type === 'continuation') {
item.input = r.inputType === 'yesno' ? 'yesno' : 'number'
const cur = pickVal(r.valCurText, r.valCurNum)
const prev = pickVal(r.valPrevText, r.valPrevNum)
formData.data[r.code] = { cur, prev }
}
body.push(item)
})
// 5. 模板表头元信息
const template = {
attach: '附件2',
title: res.formTitle,
form_no: res.formNo,
issuing_org: res.issuingOrg,
approving_org: res.approvingOrg,
approve_doc_no: res.approveDocNo,
valid_to: res.validTo || '',
cur_year: res.reportingYear,
prev_year: res.prevYear
}
// 6. 页脚签章(值取自顶层字段)
const footer = [
{ col: 1, label: '单位负责人:' + (res.unitHead || '') },
{ col: 5, label: '统计负责人:' + (res.statsHead || '') },
{ col: 6, label: '填表人:' + (res.filler || '') },
{ col: 8, label: '报出时间:' + (res.submitTime || '') }
]
return { template, units, body, footer, formData }
},
// 回填填报值:{ u_01: 'xx', '08_c': '12' } 或 { units: {01:'xx'}, data: {08:{cur:'12'}} }
fillFormData(formData) {
if (!formData) return
if (formData.units) {
Object.keys(formData.units).forEach(code => {
this.$set(this.form, 'u_' + code, formData.units[code])
})
}
if (formData.data) {
Object.keys(formData.data).forEach(code => {
const item = formData.data[code] || {}
this.$set(this.form, code + '_c', item.cur)
this.$set(this.form, code + '_p', item.prev)
})
}
Object.keys(formData).forEach(k => {
if (Object.prototype.hasOwnProperty.call(this.form, k)) {
this.$set(this.form, k, formData[k])
}
})
},
initForm(d) { initForm(d) {
const form = {} const form = {}
d.units.forEach(u => { form['u_' + u.code] = '' }) d.units.forEach(u => { form['u_' + u.code] = '' })
@ -87,13 +249,57 @@ export default {
console.log(JSON.stringify(payload, null, 2)) console.log(JSON.stringify(payload, null, 2))
this.$emit('export', payload) this.$emit('export', payload)
alert('已导出到控制台(F12),并通过 emit("export") 回传父组件') alert('已导出到控制台(F12),并通过 emit("export") 回传父组件')
},
// 保存:基于接口原始结构回填用户输入(数值也写入 text 字段,与回显口径一致)
handleSave() {
if (!this.rawReport) {
this.$message.warning('报表数据加载中,请稍后再试')
return
}
const payload = JSON.parse(JSON.stringify(this.rawReport))
// 01-07 单位信息回填到顶层字段
const unitFieldMap = {
'01': 'unitCreditCode',
'02': 'unitName',
'03': 'unitCategoryCode',
'04': 'postalCode',
'05': 'unitAddress',
'06': 'telephone',
'07': 'orgSituation'
}
Object.keys(unitFieldMap).forEach(code => {
const v = this.form['u_' + code]
payload[unitFieldMap[code]] = v === '' || v == null ? null : v
})
// 指标行回填 valCurText / valPrevText(递归 children 树形结构)
const walk = list => {
(list || []).forEach(r => {
if (r.itemType === 'indicator' && r.isLeaf && r.code) {
const cur = this.form[r.code + '_c']
const prev = this.form[r.code + '_p']
r.valCurText = cur === '' || cur == null ? null : String(cur)
r.valPrevText = prev === '' || prev == null ? null : String(prev)
}
if (r.children && r.children.length) walk(r.children)
})
}
walk(payload.rows)
this.saving = true
SaveStatisticsTemplate(payload).then(() => {
this.$message({ message: '保存成功', type: 'success', offset: 80 })
}).finally(() => {
this.saving = false
})
} }
} }
} }
</script> </script>
<template> <template>
<div class="da3">
<div v-loading="loading" class="da3">
<!-- <div class="toolbar"> <!-- <div class="toolbar">
<button @click="fillDemo">填入示例数据</button> <button @click="fillDemo">填入示例数据</button>
<button @click="clearAll">清空</button> <button @click="clearAll">清空</button>
@ -207,12 +413,26 @@ export default {
<td v-for="(f, fi) in data.footer" :key="'f' + fi" :colspan="fi < data.footer.length - 1 ? data.footer[fi + 1].col - f.col : (10 - f.col)">{{ f.label }}</td> <td v-for="(f, fi) in data.footer" :key="'f' + fi" :colspan="fi < data.footer.length - 1 ? data.footer[fi + 1].col - f.col : (10 - f.col)">{{ f.label }}</td>
</tr> </tr>
</table> </table>
<!-- 编辑模式:底部固定保存栏 -->
<div v-if="!isView" class="save-bar">
<el-button type="primary" :loading="saving" @click="handleSave">保存</el-button>
</div>
</div> </div>
</template> </template>
<style scoped> <style scoped>
* { box-sizing: border-box; } * { box-sizing: border-box; }
.da3 { font-family: "SimSun", "宋体", serif; color: #000; background: #fff; padding: 20px; }
.da3 { font-family: "SimSun", "宋体", serif; color: #000; background: #fff; padding: 20px 20px 76px; }
/* 编辑模式底部固定保存栏 */
.save-bar {
position: fixed; left: 0; right: 0; bottom: 0; z-index: 2000;
display: flex; justify-content: flex-end; align-items: center;
padding: 10px 28px; background: #fff;
border-top: 1px solid #e4e7ed;
box-shadow: 0 -2px 8px rgba(0, 0, 0, .06);
}
.save-bar .el-button { min-width: 88px; color: #fff; background-color: #0348f3; border-color: #0348f3; }
.toolbar { max-width: 1040px; margin: 0 auto 12px; font-size: 13px; color: #444; } .toolbar { max-width: 1040px; margin: 0 auto 12px; font-size: 13px; color: #444; }
.toolbar button { font-family: inherit; padding: 4px 10px; margin-right: 8px; cursor: pointer; } .toolbar button { font-family: inherit; padding: 4px 10px; margin-right: 8px; cursor: pointer; }
.toolbar .hint { color: #888; } .toolbar .hint { color: #888; }

61
src/views/archivesStatistics/annualReport‌/index.vue

@ -74,7 +74,8 @@
<div class="setting-dialog"> <div class="setting-dialog">
<el-form ref="form" :model="form" :rules="formRules" label-width="90px"> <el-form ref="form" :model="form" :rules="formRules" label-width="90px">
<el-form-item label="全宗" prop="fondsId"> <el-form-item label="全宗" prop="fondsId">
<el-select v-model="form.fondsId" :disabled="isEdit" placeholder="请选择全宗" style="width: 100%;">
<!-- :disabled="isEdit" -->
<el-select v-model="form.fondsId" placeholder="请选择全宗" style="width: 100%;">
<el-option <el-option
v-for="item in fondsOptions" v-for="item in fondsOptions"
:key="item.id" :key="item.id"
@ -84,7 +85,8 @@
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item label="年度" prop="year"> <el-form-item label="年度" prop="year">
<el-date-picker v-model="form.year" type="year" value-format="yyyy" :disabled="isEdit" placeholder="请选择年度" style="width: 100%;" />
<!-- :disabled="isEdit" -->
<el-date-picker v-model="form.year" type="year" value-format="yyyy" placeholder="请选择年度" style="width: 100%;" />
</el-form-item> </el-form-item>
<el-form-item label="统计模板" prop="modelId"> <el-form-item label="统计模板" prop="modelId">
<el-select v-model="form.modelId" :disabled="isEdit" placeholder="请选择统计模板" style="width: 100%;"> <el-select v-model="form.modelId" :disabled="isEdit" placeholder="请选择统计模板" style="width: 100%;">
@ -102,7 +104,7 @@
</el-form> </el-form>
</div> </div>
<div slot="footer" class="dialog-footer"> <div slot="footer" class="dialog-footer">
<el-button v-if="isEdit" type="primary" @click="handleEditDetail">编辑详情</el-button>
<el-button v-if="isEdit" type="primary" :loading="editDetailLoading" @click="handleEditDetail">编辑详情</el-button>
<el-button type="text" @click="closeFormDialog">取消</el-button> <el-button type="text" @click="closeFormDialog">取消</el-button>
<el-button :loading="submitLoading" type="primary" @click="handleSubmit">确定</el-button> <el-button :loading="submitLoading" type="primary" @click="handleSubmit">确定</el-button>
</div> </div>
@ -120,7 +122,7 @@
<div class="detail-item"><span class="detail-label">统计模板:</span>{{ detailData.modelName }}</div> <div class="detail-item"><span class="detail-label">统计模板:</span>{{ detailData.modelName }}</div>
</div> </div>
<div slot="footer" class="dialog-footer"> <div slot="footer" class="dialog-footer">
<el-button type="primary" @click="handleReportDetail">报表详情</el-button>
<el-button type="primary" :loading="reportDetailLoading" @click="handleReportDetail">报表详情</el-button>
<el-button type="primary" @click="closeDetailDialog">确定</el-button> <el-button type="primary" @click="closeDetailDialog">确定</el-button>
</div> </div>
</el-dialog> </el-dialog>
@ -129,7 +131,7 @@
<script> <script>
import { FetchFondsAll } from '@/api/system/fonds' import { FetchFondsAll } from '@/api/system/fonds'
import { FetchTotalStatistics, FetchArchivedAllYear } from '@/api/statistics/statistics'
import { FetchTotalStatistics, FetchArchivedAllYear, FetchInitStatisticsTemplate } from '@/api/statistics/statistics'
import { mapGetters } from 'vuex' import { mapGetters } from 'vuex'
export default { export default {
@ -162,6 +164,9 @@ export default {
// 详情弹框 // 详情弹框
detailVisible: false, detailVisible: false,
detailData: {}, detailData: {},
// 跳转报表前的数据预检 loading
editDetailLoading: false,
reportDetailLoading: false,
// 统计模板 mock 数据 // 统计模板 mock 数据
modelOptions: [ modelOptions: [
{ id: 1, name: '《档案室基本情况表》(2025版)' } { id: 1, name: '《档案室基本情况表》(2025版)' }
@ -252,8 +257,11 @@ export default {
}) })
}, },
handleEditDetail() { handleEditDetail() {
const route = this.$router.resolve({ path: '/da3Report', query: { mode: 'edit' }})
window.open(route.href, '_blank')
this.checkAndOpenReport('edit', {
reportingYear: this.form.year,
unitId: this.form.fondsId,
templateId: Number(this.form.modelId) || 1
}, 'editDetailLoading')
}, },
handleSizeChange(val) { handleSizeChange(val) {
this.page.size = val this.page.size = val
@ -272,8 +280,43 @@ export default {
this.detailData = {} this.detailData = {}
}, },
handleReportDetail() { handleReportDetail() {
const route = this.$router.resolve({ path: '/da3Report', query: { mode: 'view' }})
window.open(route.href, '_blank')
// 行数据无全宗 id 时,按填报单位名称匹配
const fondsItem = this.fondsOptions.find(f => f.fondsName === this.detailData.archivesCount)
const unitId = this.detailData.unitId || this.detailData.fondsId || (fondsItem && fondsItem.id)
this.checkAndOpenReport('view', {
reportingYear: this.detailData.year,
unitId,
templateId: 1
}, 'reportDetailLoading')
},
// 跳转报表前先调初始化接口预检:有数据才打开新页,否则提示且不跳转
checkAndOpenReport(mode, params, loadingKey) {
if (!params.reportingYear || !params.unitId) {
this.$message.warning('缺少年度或填报单位信息,无法打开报表')
return
}
// 用户点击手势内先开空白窗口,避免异步回调里 window.open 被浏览器拦截
const win = window.open('', '_blank')
this[loadingKey] = true
FetchInitStatisticsTemplate(params).then(res => {
// 接口正常且模板行有数据才放行
if (res && Array.isArray(res.rows) && res.rows.length) {
const route = this.$router.resolve({
path: '/da3Report',
query: { mode, ...params }
})
if (win) win.location.href = route.href
else window.open(route.href, '_blank')
} else {
if (win) win.close()
this.$message.warning('暂无可展示的报表数据,请先完成报表初始化或填报')
}
}).catch(() => {
if (win) win.close()
// 网络/服务异常时 request 拦截器已给出错误提示,此处不再重复弹窗
}).finally(() => {
this[loadingKey] = false
})
}, },
handleDelete() { handleDelete() {
this.$confirm('此操作将删除当前数据' + '<span>你是否还要继续?</span>', '提示', { this.$confirm('此操作将删除当前数据' + '<span>你是否还要继续?</span>', '提示', {

Loading…
Cancel
Save