阅行客电子档案
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

925 lines
38 KiB

​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
3 weeks ago
​
​
​
​
​
​
​
​
​
​
​
​
​
3 weeks ago
​
​
​
​
​
​
4 weeks ago
​
​
​
​
​
​
​
​
​
​
​
​
​
​
4 weeks ago
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
4 weeks ago
​
​
​
​
​
​
​
​
​
4 weeks ago
​
​
3 weeks ago
​
​
4 weeks ago
​
4 weeks ago
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
4 weeks ago
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
3 weeks ago
​
3 weeks ago
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
3 weeks ago
4 weeks ago
​
​
​
​
​
​
​
​
​
​
​
​
​
​
3 weeks ago
​
4 weeks ago
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
4 weeks ago
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
4 weeks ago
​
​
​
4 weeks ago
​
4 weeks ago
​
​
4 weeks ago
​
​
​
​
​
​
​
​
​
​
​
​
3 weeks ago
​
​
​
​
4 weeks ago
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
3 weeks ago
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
3 weeks ago
​
4 weeks ago
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
4 weeks ago
​
3 weeks ago
​
​
​
​
​
​
​
​
​
​
​
​
​
​
4 weeks ago
​
​
​
​
​
​
​
​
​
​
4 weeks ago
​
​
​
​
​
3 weeks ago
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
4 weeks ago
3 weeks ago
​
​
​
​
​
​
​
​
​
​
4 weeks ago
​
​
3 weeks ago
​
3 weeks ago
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
3 weeks ago
​
​
​
​
​
​
4 weeks ago
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
  1. <template>
  2. <div v-loading="loading" class="da3">
  3. <!-- <div class="toolbar">
  4. <button @click="fillDemo">填入示例数据</button>
  5. <button @click="clearAll">清空</button>
  6. <button @click="exportJSON">导出当前填报 JSON</button>
  7. <button @click="exportExcel">导出 Excel</button>
  8. <span class="hint">数据驱动渲染 · 来自 template_item 等表</span>
  9. </div> -->
  10. <table v-if="data" class="form">
  11. <!-- 附件2 + 题名 -->
  12. <tr><td class="attach" colspan="9">{{ data.template.attach }}</td></tr>
  13. <tr><td class="title" colspan="9">{{ data.template.title }}</td></tr>
  14. <!-- 元信息(表号/制定机关/批准机关) -->
  15. <tr v-for="(m, i) in metaTop" :key="'mt' + i" class="meta-row">
  16. <td colspan="9" class="meta">
  17. <div class="meta-flex">
  18. <!-- <i class="colon">:</i> -->
  19. <span class="meta-label">{{ m.label }}</span>
  20. <i class="colon">:</i>
  21. <span class="meta-value">{{ m.value }}</span>
  22. </div>
  23. </td>
  24. </tr>
  25. <!-- 单位信息卡 01-07(左列 01-05 + 右列 06-07 + 批准文号/有效期) -->
  26. <tr v-for="(u, i) in leftUnits" :key="'lu' + i" :class="['unit-row', { 'unit-row-bordered': i >= 2 }]">
  27. <td class="ucode">{{ u.code }}</td>
  28. <td class="ulabel" colspan="4">{{ u.label }}:<span v-if="isView" class="plain">{{ form['u_' + u.code] }}</span><input v-else v-model="form['u_' + u.code]" type="text"></td>
  29. <template v-if="i === 0">
  30. <td class="meta" colspan="4">
  31. <div class="meta-flex">
  32. <span class="meta-label">批准文号</span>
  33. <i class="colon">:</i>
  34. <span class="meta-value">{{ data.template.approve_doc_no }}</span>
  35. </div>
  36. </td></template>
  37. <template v-else-if="i === 1">
  38. <td class="meta" colspan="4">
  39. <div class="meta-flex">
  40. <span class="meta-label">有效期至</span>
  41. <i class="colon">:</i>
  42. <span class="meta-value" style="text-align: right !important; text-align-last: right !important;">{{ data.template.valid_to }}</span>
  43. </div>
  44. </td>
  45. </template>
  46. <template v-if="i === 2 && rightUnits[0]">
  47. <td class="ucode">{{ rightUnits[0].code }}</td>
  48. <td class="ulabel" colspan="3">{{ rightUnits[0].label }}:<span v-if="isView" class="plain">{{ form['u_' + rightUnits[0].code] }}</span><input v-else v-model="form['u_' + rightUnits[0].code]" type="text"></td>
  49. </template>
  50. <template v-else-if="i === 3 && rightUnits[1]">
  51. <td class="ucode ucell" rowspan="2">{{ rightUnits[1].code }}</td>
  52. <td class="ulabel ucell" colspan="3" rowspan="2">{{ rightUnits[1].label }}:<span v-if="isView" class="plain">{{ form['u_' + rightUnits[1].code] }}</span><input v-else v-model="form['u_' + rightUnits[1].code]" type="text"></td>
  53. </template>
  54. <template v-else-if="i === 4" />
  55. <template v-else>
  56. <td colspan="4" />
  57. </template>
  58. </tr>
  59. <!-- 列头 -->
  60. <tr>
  61. <th class="colname" colspan="5">指标名称</th>
  62. <th class="colname">计量单位</th>
  63. <th class="colname">代码</th>
  64. <th class="colname">数量({{ data.template.cur_year }}年)</th>
  65. <th class="colname">上年度数据({{ data.template.prev_year }}年)</th>
  66. </tr>
  67. <tr>
  68. <th class="colsub" colspan="5">甲</th>
  69. <th class="colsub">乙</th>
  70. <th class="colsub">丙</th>
  71. <th class="colsub">1</th>
  72. <th class="colsub" />
  73. </tr>
  74. <!-- 主体 -->
  75. <template v-for="(it, idx) in data.body">
  76. <!-- 章节 / 分组 -->
  77. <tr v-if="it.type === 'section' || it.type === 'group'" :key="'s' + idx" :class="it.type + ' body-row'">
  78. <td class="nm" colspan="5" :style="{ paddingLeft: (it.level * 1.6) + 'em' }">{{ it.name }}</td>
  79. <td class="dash">{{ it.unit || '—' }}</td>
  80. <td class="dash">—</td>
  81. <td class="dash">—</td>
  82. <td class="dash" />
  83. </tr>
  84. <!-- 指标 / 续行 -->
  85. <tr v-if="it.type !== 'section' && it.type !== 'group'" :key="'i' + idx" class="body-row">
  86. <td class="nm" colspan="5" :style="{ paddingLeft: (it.level * 1.6) + 'em' }">{{ it.name }}</td>
  87. <td class="unit">{{ it.unit }}</td>
  88. <td class="code">{{ it.code }}</td>
  89. <td class="data">
  90. <span v-if="isView" class="plain">{{ form[it.code + '_c'] }}</span>
  91. <select v-else-if="it.input === 'yesno'" v-model="form[it.code + '_c']">
  92. <option value="" /><option value="是">是</option><option value="否">否</option>
  93. </select>
  94. <input v-else-if="it.input === 'text'" v-model="form[it.code + '_c']" type="text">
  95. <input v-else v-model="form[it.code + '_c']" type="text" inputmode="decimal">
  96. </td>
  97. <td class="data">
  98. <span v-if="isView" class="plain">{{ form[it.code + '_p'] }}</span>
  99. <select v-else-if="it.input === 'yesno'" v-model="form[it.code + '_p']">
  100. <option value="" /><option value="是">是</option><option value="否">否</option>
  101. </select>
  102. <input v-else-if="it.input === 'text'" v-model="form[it.code + '_p']" type="text">
  103. <input v-else v-model="form[it.code + '_p']" type="text" inputmode="decimal">
  104. </td>
  105. </tr>
  106. </template>
  107. <!-- 页脚签章 -->
  108. <tr class="footer-row">
  109. <!-- 单位负责人 col1-4 -->
  110. <td colspan="4" class="footer-cell">
  111. <span class="footer-label">单位负责人:</span>
  112. <span v-if="isView" class="plain">{{ footerForm.unitHead }}</span>
  113. <input v-else v-model="footerForm.unitHead" type="text" class="footer-input">
  114. </td>
  115. <!-- 统计负责人 col5 -->
  116. <td class="footer-cell">
  117. <span class="footer-label">统计负责人:</span>
  118. <span v-if="isView" class="plain">{{ footerForm.statsHead }}</span>
  119. <input v-else v-model="footerForm.statsHead" type="text" class="footer-input">
  120. </td>
  121. <!-- 填表人 col6 -->
  122. <td class="footer-cell">
  123. <span class="footer-label">填表人:</span>
  124. <span v-if="isView" class="plain">{{ footerForm.filler }}</span>
  125. <input v-else v-model="footerForm.filler" type="text" class="footer-input">
  126. </td>
  127. <!-- 报出时间 col7-9 -->
  128. <td colspan="3" class="footer-cell">
  129. <span class="footer-label">报出时间:</span>
  130. <span v-if="isView" class="plain">{{ footerForm.submitTime }}</span>
  131. <input v-else v-model="footerForm.submitTime" type="text" class="footer-input footer-input-wide" placeholder="20 年 月 日">
  132. </td>
  133. </tr>
  134. </table>
  135. <!-- 编辑模式:底部固定保存栏 -->
  136. <!-- 查看模式工具栏 -->
  137. <div v-if="isView" class="save-bar">
  138. <el-button @click="exportExcel">导出Excel</el-button>
  139. <el-button @click="handleCancel">关闭</el-button>
  140. </div>
  141. <!-- 编辑模式工具栏 -->
  142. <div v-if="!isView" class="save-bar">
  143. <el-button class="saveetch-btn" type="primary" :loading="refetchLoading" @click="handleRefetch">重新获取统计数据</el-button>
  144. <el-button @click="exportExcel">导出Excel</el-button>
  145. <el-button @click="handleCancel">取消</el-button>
  146. <el-button class="save-btn" type="primary" :loading="saving" @click="handleSave">保存</el-button>
  147. </div>
  148. </div>
  149. </template>
  150. <script>
  151. import { FetchInitStatisticsTemplate, FetchRecalculateArchivesDoReport, FetchEditArchivesReportDetails } from '@/api/statistics/statistics'
  152. import ExcelJS from 'exceljs'
  153. import { saveAs } from 'file-saver'
  154. import { mapGetters } from 'vuex'
  155. export default {
  156. name: 'Da3Report',
  157. data() {
  158. return {
  159. form: {},
  160. // 接口返回的报表模型
  161. remoteModel: null,
  162. // 接口返回的原始数据(保存时按同结构回传)
  163. rawReport: null,
  164. loading: false,
  165. saving: false,
  166. refetchLoading: false,
  167. // 页脚签章:单位负责人 / 统计负责人 / 填表人 / 报出时间
  168. footerForm: {
  169. unitHead: '',
  170. statsHead: '',
  171. filler: '',
  172. submitTime: ''
  173. }
  174. }
  175. },
  176. computed: {
  177. ...mapGetters(['user']),
  178. data() {
  179. return this.remoteModel
  180. },
  181. // 查看模式(不可输入)还是编辑模式(可输入),默认查看
  182. isView() {
  183. return this.$route.query.mode !== 'edit'
  184. },
  185. leftUnits() {
  186. if (!this.data) return []
  187. return this.data.units.filter(u => u.card === 1) // 01-05
  188. },
  189. rightUnits() {
  190. if (!this.data) return []
  191. return this.data.units.filter(u => u.card === 2) // 06-07
  192. },
  193. metaTop() {
  194. if (!this.data) return []
  195. return [
  196. { label: '表号', value: this.data.template.form_no },
  197. { label: '制定机关', value: this.data.template.issuing_org },
  198. { label: '批准机关', value: this.data.template.approving_org }
  199. // { label: '批准文号', value: this.data.template.approve_doc_no },
  200. // { label: '有效期至', value: this.data.template.valid_to }
  201. ]
  202. }
  203. },
  204. watch: {
  205. data: {
  206. handler(d) {
  207. if (!d) return
  208. this.initForm(d)
  209. // 接口模型自带填报值时回显
  210. if (d.formData) this.fillFormData(d.formData)
  211. // 回显页脚签章
  212. if (d.footerForm) {
  213. Object.keys(this.footerForm).forEach(k => {
  214. this.footerForm[k] = d.footerForm[k] != null ? d.footerForm[k] : ''
  215. })
  216. }
  217. },
  218. immediate: true
  219. }
  220. },
  221. created() {
  222. this.fetchReport()
  223. },
  224. beforeDestroy() {
  225. sessionStorage.removeItem('da3_fondsName')
  226. },
  227. methods: {
  228. fetchReport() {
  229. const { reportingYear, unitId, templateId } = this.$route.query
  230. this.loading = true
  231. FetchInitStatisticsTemplate({
  232. reportingYear,
  233. unitId,
  234. templateId: Number(templateId) || 1
  235. }).then(res => {
  236. console.log('[Da3Report] FetchInitStatisticsTemplate res:', res)
  237. if (res) {
  238. // 留存接口原始数据,保存时按同结构回传
  239. this.rawReport = res
  240. // 将接口结构(顶层表头字段 + rows 树形嵌套)归一化为组件模型 { template/units/body/footer/formData }
  241. const model = this.normalizeModel(res)
  242. if (model) {
  243. // remoteModel 变化会触发 watch(data):initForm 重建表单后自动用 formData 回填
  244. this.remoteModel = model
  245. }
  246. }
  247. }).catch(() => {
  248. // 接口异常时兜底展示 mock 数据
  249. // this.fillDemo()
  250. }).finally(() => {
  251. this.loading = false
  252. this.refetchLoading = false
  253. })
  254. },
  255. // 将接口返回结构归一化为组件内置模型结构:{ template, units, body, footer, formData }
  256. normalizeModel(res) {
  257. if (!res || !Array.isArray(res.rows)) return null
  258. // 1. rows 是 children 树形嵌套,先递归拍平,再按 displayOrder 升序还原真实展示顺序
  259. const flat = []
  260. const walk = list => {
  261. list.forEach(r => {
  262. flat.push(r)
  263. if (r.children && r.children.length) walk(r.children)
  264. })
  265. }
  266. walk(res.rows)
  267. flat.sort((a, b) => (a.displayOrder || 0) - (b.displayOrder || 0))
  268. // 2. 01-07 单位信息填报值(顶层字段优先,row.valCurText 兜底)
  269. const unitValues = {
  270. '01': res.unitCreditCode,
  271. '02': res.unitName,
  272. '03': res.unitCategoryCode,
  273. '04': res.postalCode,
  274. '05': res.unitAddress,
  275. '06': res.telephone,
  276. '07': res.orgSituation
  277. }
  278. const units = []
  279. const body = []
  280. const formData = { units: {}, data: {}}
  281. const leftCards = ['01', '02', '03', '04', '05']
  282. // 填报值:优先按 inputType 取对应字段,没有则取另一个字段,都没有则为空
  283. const pickVal = (r, which) => {
  284. const t = r.inputType
  285. if (which === 'cur') {
  286. // 本年:code/number 优先 valCurNum,text 优先 valCurText
  287. if (t === 'code' || t === 'number') {
  288. if (r.valCurNum != null) return r.valCurNum
  289. if (r.valCurText != null) return r.valCurText
  290. } else {
  291. if (r.valCurText != null) return r.valCurText
  292. if (r.valCurNum != null) return r.valCurNum
  293. }
  294. } else {
  295. // 上年:code/number 优先 valPrevNum,text 优先 valPrevText
  296. if (t === 'code' || t === 'number') {
  297. if (r.valPrevNum != null) return r.valPrevNum
  298. if (r.valPrevText != null) return r.valPrevText
  299. } else {
  300. if (r.valPrevText != null) return r.valPrevText
  301. if (r.valPrevNum != null) return r.valPrevNum
  302. }
  303. }
  304. return ''
  305. }
  306. flat.forEach(r => {
  307. // 章节(section)或顶级叶子指标(如 82/95/96/97/109)需把中文序号拼到名称前:一、人员情况 / 五、检索工具
  308. const needPrefix = (r.itemType === 'section' || (r.isLeaf && r.level === 0)) &&
  309. r.sectionNo && r.sectionNo !== '表头'
  310. const fullName = needPrefix ? r.sectionNo + '、' + r.name : r.name
  311. // 3. 表头单位信息 01-07
  312. if (r.itemType === 'unit') {
  313. units.push({
  314. code: r.code,
  315. label: r.name,
  316. card: leftCards.indexOf(r.code) !== -1 ? 1 : 2,
  317. kind: r.inputType === 'code' ? 'code' : 'text'
  318. })
  319. const curVal = pickVal(r, 'cur')
  320. formData.units[r.code] = unitValues[r.code] != null ? unitValues[r.code] : (curVal != null && curVal !== '' ? curVal : (r.valCurText || r.valCurNum || ''))
  321. return
  322. }
  323. // 4. 主体行:section / group / indicator / continuation
  324. let type
  325. if (r.itemType === 'section') {
  326. type = 'section'
  327. } else if (r.isGroup) {
  328. type = 'group'
  329. } else if (r.name === '(续行)' || r.name === '') {
  330. type = 'continuation'
  331. } else {
  332. type = 'indicator'
  333. }
  334. const item = {
  335. type,
  336. code: r.code || '',
  337. name: type === 'continuation' ? '' : fullName,
  338. unit: (type === 'section' || type === 'group') ? (r.unit || '—') : (r.unit || (r.inputType === 'yesno' ? '—' : '')),
  339. level: r.level
  340. }
  341. if (type === 'indicator' || type === 'continuation') {
  342. item.input = r.inputType === 'yesno' ? 'yesno' : (r.inputType || 'number')
  343. const cur = pickVal(r, 'cur')
  344. const prev = pickVal(r, 'prev')
  345. formData.data[r.code] = { cur, prev }
  346. }
  347. body.push(item)
  348. })
  349. // 5. 模板表头元信息
  350. const template = {
  351. attach: '附件2',
  352. title: res.formTitle,
  353. form_no: res.formNo,
  354. issuing_org: res.issuingOrg,
  355. approving_org: res.approvingOrg,
  356. approve_doc_no: res.approveDocNo,
  357. valid_to: res.validTo || '',
  358. cur_year: res.reportingYear,
  359. prev_year: res.prevYear
  360. }
  361. // 6. 页脚签章(值取自顶层字段,同时存入 formData 用于回填)
  362. const footerFormData = {
  363. unitHead: res.unitHead || '',
  364. statsHead: res.statsHead || '',
  365. filler: res.filler || '',
  366. submitTime: res.submitTime || ''
  367. }
  368. const footer = [
  369. { col: 1, label: '单位负责人:' + footerFormData.unitHead },
  370. { col: 5, label: '统计负责人:' + footerFormData.statsHead },
  371. { col: 6, label: '填表人:' + footerFormData.filler },
  372. { col: 8, label: '报出时间:' + footerFormData.submitTime }
  373. ]
  374. return { template, units, body, footer, formData, footerForm: footerFormData }
  375. },
  376. // 回填填报值:{ u_01: 'xx', '08_c': '12' } 或 { units: {01:'xx'}, data: {08:{cur:'12'}} }
  377. fillFormData(formData) {
  378. console.log('fillFormData', formData)
  379. if (!formData) return
  380. if (formData.units) {
  381. Object.keys(formData.units).forEach(code => {
  382. this.$set(this.form, 'u_' + code, formData.units[code])
  383. })
  384. }
  385. if (formData.data) {
  386. Object.keys(formData.data).forEach(code => {
  387. const item = formData.data[code] || {}
  388. this.$set(this.form, code + '_c', item.cur)
  389. this.$set(this.form, code + '_p', item.prev)
  390. })
  391. }
  392. // 回填页脚签章(formData.footer 或 d.footerForm 都会走到这里)
  393. if (formData.footer) {
  394. Object.keys(this.footerForm).forEach(k => {
  395. if (formData.footer[k] != null) this.footerForm[k] = formData.footer[k]
  396. })
  397. }
  398. Object.keys(formData).forEach(k => {
  399. if (Object.prototype.hasOwnProperty.call(this.form, k)) {
  400. this.$set(this.form, k, formData[k])
  401. }
  402. })
  403. },
  404. initForm(d) {
  405. if (!d || !Array.isArray(d.units) || !Array.isArray(d.body)) {
  406. this.form = {}
  407. return
  408. }
  409. const form = {}
  410. d.units.forEach(u => { form['u_' + u.code] = '' })
  411. d.body.forEach(it => {
  412. if (it.type === 'indicator' || it.type === 'continuation') {
  413. form[it.code + '_c'] = '' // 本年
  414. form[it.code + '_p'] = '' // 上年
  415. }
  416. })
  417. this.form = form
  418. },
  419. fillDemo() {
  420. const DEMO = {
  421. u_01: '91420100MA4KXXXXXX', u_02: '阅行客(武汉)科技有限公司', u_03: '130',
  422. u_04: '430000', u_05: '武汉市xx区xx路1号', u_06: '027-88888888', u_07: '独立档案室',
  423. '08_c': '12', '08_p': '10', '09_c': '8', '09_p': '7', '10_c': '2',
  424. '11_c': '5', '12_c': '5', '34_c': '1200', '35_c': '35000', '36_c': '42.5',
  425. '37_c': '800', '38_c': '600', '39_c': '120', '82_c': '5', '95_c': '是',
  426. '96_c': '否', '97_c': '否', '109_c': '1'
  427. }
  428. Object.assign(this.form, DEMO)
  429. },
  430. clearAll() {
  431. Object.keys(this.form).forEach(k => { this.form[k] = '' })
  432. },
  433. buildPayload() {
  434. const units = {}; const reportData = {}
  435. this.data.units.forEach(u => { units[u.code] = this.form['u_' + u.code] || '' })
  436. this.data.body.forEach(it => {
  437. if (it.type === 'indicator' || it.type === 'continuation') {
  438. reportData[it.code] = { cur: this.form[it.code + '_c'] || '', prev: this.form[it.code + '_p'] || '' }
  439. }
  440. })
  441. return { units, data: reportData, footer: { ...this.footerForm }}
  442. },
  443. exportJSON() {
  444. const payload = this.buildPayload()
  445. console.log(JSON.stringify(payload, null, 2))
  446. this.$emit('export', payload)
  447. // alert('已导出到控制台(F12),并通过 emit("export") 回传父组件')
  448. },
  449. // 导出 Excel
  450. async exportExcel() {
  451. if (!this.data) {
  452. this.$message.warning('数据未加载完成,请稍后')
  453. return
  454. }
  455. const wb = new ExcelJS.Workbook()
  456. const ws = wb.addWorksheet('档案室基本情况表')
  457. // 9 列布局:A-E 指标名称(合并) F计量单位 G代码 H本年 I上年
  458. ws.columns = [
  459. { width: 4 }, // A
  460. { width: 4 }, // B
  461. { width: 4 }, // C
  462. { width: 4 }, // D
  463. { width: 4 }, // E
  464. { width: 10 }, // F - 计量单位
  465. { width: 8 }, // G - 代码
  466. { width: 14 }, // H - 本年
  467. { width: 14 } // I - 上年
  468. ]
  469. // ===== 第1行:附件2 =====
  470. ws.getCell('A1').value = '附件2'
  471. ws.getCell('A1').font = { size: 12 }
  472. ws.getCell('A1').alignment = { horizontal: 'left', vertical: 'middle' }
  473. ws.getRow(1).height = 18
  474. // ===== 第2行:标题 =====
  475. ws.mergeCells('A2:I2')
  476. const titleCell = ws.getCell('A2')
  477. titleCell.value = this.data.template.title
  478. titleCell.font = { size: 22, bold: true, name: '黑体' }
  479. titleCell.alignment = { horizontal: 'center', vertical: 'middle' }
  480. ws.getRow(2).height = 35
  481. // ===== 第3行:表号(右对齐) =====
  482. ws.getCell('I3').value = `表号:${this.data.template.form_no}`
  483. ws.getCell('I3').font = { size: 10 }
  484. ws.getCell('I3').alignment = { horizontal: 'right', vertical: 'middle' }
  485. ws.getRow(3).height = 16
  486. // ===== 第4行:制定机关(右对齐) =====
  487. ws.getCell('I4').value = `制定机关:${this.data.template.issuing_org}`
  488. ws.getCell('I4').font = { size: 10 }
  489. ws.getCell('I4').alignment = { horizontal: 'right', vertical: 'middle' }
  490. ws.getRow(4).height = 16
  491. // ===== 第5行:批准机关(右对齐) =====
  492. ws.getCell('I5').value = `批准机关:${this.data.template.approving_org}`
  493. ws.getCell('I5').font = { size: 10 }
  494. ws.getCell('I5').alignment = { horizontal: 'right', vertical: 'middle' }
  495. ws.getRow(5).height = 16
  496. // ===== 第6-10行:单位信息(左右两栏) =====
  497. const units = this.data.units || []
  498. // const leftUnits = units.filter(u => u.card === 1) // 01-05
  499. // const rightUnits = units.filter(u => u.card === 2) // 06-07
  500. const unitValueMap = {}
  501. units.forEach(u => { unitValueMap[u.code] = this.form['u_' + u.code] || '' })
  502. // 左侧 01-05(A-E列),右侧 06-07(F-I列)
  503. const leftRows = [
  504. { code: '01', label: '统一社会信用代码:' },
  505. { code: '02', label: '单位名称:' },
  506. { code: '03', label: '单位类别代码:' },
  507. { code: '04', label: '邮政编码:' },
  508. { code: '05', label: '单位地址:' }
  509. ]
  510. const rightRows = [
  511. { code: '06', label: '电话:' },
  512. { code: '07', label: '机构情况:' }
  513. ]
  514. leftRows.forEach((item, idx) => {
  515. const r = 6 + idx
  516. // 左侧代码 A列
  517. ws.getCell(`A${r}`).value = item.code
  518. ws.getCell(`A${r}`).font = { size: 10, bold: true }
  519. ws.getCell(`A${r}`).alignment = { horizontal: 'center', vertical: 'middle' }
  520. // 左侧标签 B-E列
  521. ws.mergeCells(`B${r}:E${r}`)
  522. ws.getCell(`B${r}`).value = item.label + unitValueMap[item.code]
  523. ws.getCell(`B${r}`).font = { size: 10 }
  524. ws.getCell(`B${r}`).alignment = { horizontal: 'left', vertical: 'middle' }
  525. ws.getRow(r).height = 16
  526. })
  527. // 右侧 06-07 从第8行开始
  528. rightRows.forEach((item, idx) => {
  529. const r = 8 + idx
  530. ws.getCell(`F${r}`).value = item.code
  531. ws.getCell(`F${r}`).font = { size: 10, bold: true }
  532. ws.getCell(`F${r}`).alignment = { horizontal: 'center', vertical: 'middle' }
  533. ws.mergeCells(`G${r}:I${r}`)
  534. ws.getCell(`G${r}`).value = item.label + unitValueMap[item.code]
  535. ws.getCell(`G${r}`).font = { size: 10 }
  536. ws.getCell(`G${r}`).alignment = { horizontal: 'left', vertical: 'middle' }
  537. })
  538. // 批准文号、有效期至(右侧第6、7行)
  539. ws.getCell('I6').value = `批准文号:${this.data.template.approve_doc_no}`
  540. ws.getCell('I6').font = { size: 10 }
  541. ws.getCell('I6').alignment = { horizontal: 'right', vertical: 'middle' }
  542. ws.getCell('I7').value = `有效期至:${this.data.template.valid_to}`
  543. ws.getCell('I7').font = { size: 10 }
  544. // 第7行 06 上方加当前年报年份
  545. ws.getCell('G7').value = `${this.data.template.cur_year}年`
  546. ws.getCell('G7').font = { size: 10 }
  547. ws.getCell('G7').alignment = { horizontal: 'center', vertical: 'middle' }
  548. ws.getCell('I7').alignment = { horizontal: 'right', vertical: 'middle' }
  549. // ===== 第11行:表头(指标名称合并A-E) =====
  550. ws.mergeCells('A11:E11')
  551. ws.getCell('A11').value = '指标名称'
  552. ws.getCell('F11').value = '计量单位'
  553. ws.getCell('G11').value = '代码'
  554. ws.getCell('H11').value = `数量(${this.data.template.cur_year}年)`
  555. ws.getCell('I11').value = `上年度数据(${this.data.template.prev_year}年)`
  556. const headerRow = ws.getRow(11)
  557. for (let c = 1; c <= 9; c++) {
  558. const cell = headerRow.getCell(c)
  559. cell.font = { size: 10, bold: true }
  560. cell.alignment = { horizontal: 'center', vertical: 'middle' }
  561. cell.border = {
  562. top: { style: 'thin' }, bottom: { style: 'thin' },
  563. left: { style: 'thin' }, right: { style: 'thin' }
  564. }
  565. }
  566. ws.getRow(11).height = 20
  567. // ===== 第12行:甲乙丙1 =====
  568. ws.mergeCells('A12:E12')
  569. ws.getCell('A12').value = '甲'
  570. ws.getCell('F12').value = '乙'
  571. ws.getCell('G12').value = '丙'
  572. ws.getCell('H12').value = '1'
  573. ws.getCell('I12').value = ''
  574. const subHeaderRow = ws.getRow(12)
  575. for (let c = 1; c <= 9; c++) {
  576. const cell = subHeaderRow.getCell(c)
  577. cell.font = { size: 10 }
  578. cell.alignment = { horizontal: 'center', vertical: 'middle' }
  579. cell.border = {
  580. top: { style: 'thin' }, bottom: { style: 'thin' },
  581. left: { style: 'thin' }, right: { style: 'thin' }
  582. }
  583. }
  584. ws.getRow(12).height = 18
  585. // ===== 数据行:从第13行开始 =====
  586. const body = this.data.body || []
  587. let rowIdx = 13
  588. body.forEach(it => {
  589. const row = ws.getRow(rowIdx)
  590. const indent = ' '.repeat(it.level || 0)
  591. const name = it.type === 'section' || it.type === 'group' ? it.name : indent + it.name
  592. // A-E 合并为指标名称
  593. ws.mergeCells(`A${rowIdx}:E${rowIdx}`)
  594. row.getCell(1).value = name
  595. row.getCell(6).value = it.unit || '—' // F - 计量单位
  596. row.getCell(7).value = it.code || '' // G - 代码
  597. row.getCell(8).value = this.form[it.code + '_c'] || '' // H - 本年
  598. row.getCell(9).value = this.form[it.code + '_p'] || '' // I - 上年
  599. // 样式
  600. row.font = { size: 10 }
  601. row.getCell(1).alignment = { horizontal: 'left', vertical: 'middle' }
  602. row.getCell(6).alignment = { horizontal: 'center', vertical: 'middle' }
  603. row.getCell(7).alignment = { horizontal: 'center', vertical: 'middle' }
  604. row.getCell(8).alignment = { horizontal: 'center', vertical: 'middle' }
  605. row.getCell(9).alignment = { horizontal: 'center', vertical: 'middle' }
  606. // section/group 加粗
  607. if (it.type === 'section' || it.type === 'group') {
  608. row.font = { size: 10, bold: true }
  609. }
  610. // 边框(A-I 共9列)
  611. for (let c = 1; c <= 9; c++) {
  612. row.getCell(c).border = {
  613. top: { style: 'thin' }, bottom: { style: 'thin' },
  614. left: { style: 'thin' }, right: { style: 'thin' }
  615. }
  616. }
  617. row.height = 16
  618. rowIdx++
  619. })
  620. // ===== 页脚:单位负责人 | 统计负责人 | 填表人 | 报出时间 =====
  621. rowIdx++
  622. // 单位负责人(A-D 合并)
  623. ws.mergeCells(`A${rowIdx}:D${rowIdx}`)
  624. const unitHeadCell = ws.getCell(`A${rowIdx}`)
  625. unitHeadCell.value = '单位负责人:' + (this.footerForm.unitHead || '')
  626. unitHeadCell.font = { size: 10 }
  627. unitHeadCell.alignment = { horizontal: 'left', vertical: 'middle' }
  628. // 统计负责人(E 列)
  629. const statsHeadCell = ws.getCell(`E${rowIdx}`)
  630. statsHeadCell.value = '统计负责人:' + (this.footerForm.statsHead || '')
  631. statsHeadCell.font = { size: 10 }
  632. statsHeadCell.alignment = { horizontal: 'left', vertical: 'middle' }
  633. // 填表人(F-G 合并)
  634. ws.mergeCells(`F${rowIdx}:G${rowIdx}`)
  635. const fillerCell = ws.getCell(`F${rowIdx}`)
  636. fillerCell.value = '填表人:' + (this.footerForm.filler || '')
  637. fillerCell.font = { size: 10 }
  638. fillerCell.alignment = { horizontal: 'left', vertical: 'middle' }
  639. // 报出时间(H-I 合并)
  640. ws.mergeCells(`H${rowIdx}:I${rowIdx}`)
  641. const submitTimeCell = ws.getCell(`H${rowIdx}`)
  642. submitTimeCell.value = '报出时间:' + (this.footerForm.submitTime || '')
  643. submitTimeCell.font = { size: 10 }
  644. submitTimeCell.alignment = { horizontal: 'left', vertical: 'middle' }
  645. const footerRow = ws.getRow(rowIdx)
  646. footerRow.height = 20
  647. // 生成并下载
  648. const buffer = await wb.xlsx.writeBuffer()
  649. const fondsName = sessionStorage.getItem('da3_fondsName') || (this.rawReport && this.rawReport.fondsName) || this.form.u_02 || ''
  650. const fileName = `${this.data.template.title}_${this.data.template.cur_year}年_${fondsName}.xlsx`
  651. saveAs(new Blob([buffer]), fileName)
  652. },
  653. // 保存:基于接口原始结构回填用户输入(数值也写入 text 字段,与回显口径一致)
  654. handleSave() {
  655. if (!this.rawReport) {
  656. this.$message.warning('报表数据加载中,请稍后再试')
  657. return
  658. }
  659. const payload = JSON.parse(JSON.stringify(this.rawReport))
  660. // 01-07 单位信息回填到顶层字段
  661. const unitFieldMap = {
  662. '01': 'unitCreditCode',
  663. '02': 'unitName',
  664. '03': 'unitCategoryCode',
  665. '04': 'postalCode',
  666. '05': 'unitAddress',
  667. '06': 'telephone',
  668. '07': 'orgSituation'
  669. }
  670. Object.keys(unitFieldMap).forEach(code => {
  671. const v = this.form['u_' + code]
  672. payload[unitFieldMap[code]] = v === '' || v == null ? null : v
  673. })
  674. // 指标行回填:inputType 为 'code'/'number' 写 valCurNum/valPrevNum,'text' 写 valCurText/valPrevText
  675. const walk = list => {
  676. (list || []).forEach(r => {
  677. // 单位信息行(01-07)回填到 rows 对应节点
  678. if (r.itemType === 'unit' && r.code) {
  679. const v = this.form['u_' + r.code]
  680. const val = v === '' || v == null ? null : v
  681. if (r.inputType === 'code' || r.inputType === 'number') {
  682. const num = Number(val)
  683. if (val != null && !isNaN(num)) {
  684. r.valCurNum = num
  685. } else {
  686. r.valCurText = val != null ? String(val) : null
  687. }
  688. } else {
  689. r.valCurText = val != null ? String(val) : null
  690. }
  691. }
  692. if (r.itemType === 'indicator' && r.isLeaf && r.code) {
  693. const cur = this.form[r.code + '_c']
  694. const prev = this.form[r.code + '_p']
  695. const curVal = cur === '' || cur == null ? null : cur
  696. const prevVal = prev === '' || prev == null ? null : prev
  697. if (r.inputType === 'code' || r.inputType === 'number') {
  698. r.valCurNum = curVal != null ? Number(curVal) : null
  699. r.valPrevNum = prevVal != null ? Number(prevVal) : null
  700. } else {
  701. r.valCurText = curVal != null ? String(curVal) : null
  702. r.valPrevText = prevVal != null ? String(prevVal) : null
  703. }
  704. }
  705. if (r.children && r.children.length) walk(r.children)
  706. })
  707. }
  708. walk(payload.rows)
  709. // 拍平 rows,只保留接口需要的字段
  710. const flatList = []
  711. const flatten = list => {
  712. (list || []).forEach(r => {
  713. if (r.code) {
  714. flatList.push({
  715. indicatorCode: r.code,
  716. reportId: payload.reportId != null ? payload.reportId : null,
  717. valCurNum: r.valCurNum != null ? r.valCurNum : null,
  718. valCurText: r.valCurText != null ? r.valCurText : null,
  719. valPrevNum: r.valPrevNum != null ? r.valPrevNum : null,
  720. valPrevText: r.valPrevText != null ? r.valPrevText : null
  721. })
  722. }
  723. if (r.children && r.children.length) flatten(r.children)
  724. })
  725. }
  726. flatten(payload.rows)
  727. // 组装最终传参
  728. const savePayload = {
  729. reportId: payload.reportId != null ? payload.reportId : null,
  730. unitCreditCode: this.form.u_01 || null,
  731. unitName: this.form.u_02 || null,
  732. unitCategoryCode: this.form.u_03 || null,
  733. postalCode: this.form.u_04 || null,
  734. unitAddress: this.form.u_05 || null,
  735. telephone: this.form.u_06 || null,
  736. orgSituation: this.form.u_07 || null,
  737. unitHead: this.footerForm.unitHead || null,
  738. statsHead: this.footerForm.statsHead || null,
  739. filler: this.footerForm.filler || null,
  740. submitTime: this.footerForm.submitTime || null,
  741. reportDataList: flatList
  742. }
  743. this.saving = true
  744. console.log('payload', savePayload)
  745. FetchEditArchivesReportDetails(savePayload).then((res) => {
  746. console.log('保存成功', res)
  747. if (res.code !== 500) {
  748. this.$message({ message: res, type: 'success', offset: 80 })
  749. this.fetchReport()
  750. } else {
  751. this.$message({ message: res.message || '保存失败', type: 'error', offset: 80 })
  752. }
  753. }).catch(() => {
  754. this.$message({ message: '保存失败', type: 'error', offset: 80 })
  755. }).finally(() => {
  756. this.saving = false
  757. })
  758. },
  759. // 重新获取统计数据
  760. handleRefetch() {
  761. this.refetchLoading = true
  762. FetchRecalculateArchivesDoReport(this.rawReport.reportId).then((res) => {
  763. console.log('重新计算成功', res)
  764. if (res.code !== 500) {
  765. // 根据 value{code}_{1|2} 回填表单:1=本年,2=上年
  766. if (res && typeof res === 'object') {
  767. Object.keys(res).forEach(key => {
  768. const m = key.match(/^value(\d+)_(1|2)$/)
  769. if (m) {
  770. const code = m[1]
  771. const type = m[2] // 1=本年 2=上年
  772. const val = res[key]
  773. if (type === '1') {
  774. this.$set(this.form, code + '_c', val)
  775. } else {
  776. this.$set(this.form, code + '_p', val)
  777. }
  778. }
  779. })
  780. }
  781. this.$message({ message: '重新计算成功', type: 'success', offset: 80 })
  782. } else {
  783. this.$message({ message: res.message || '重新计算失败', type: 'error', offset: 80 })
  784. }
  785. }).finally(() => {
  786. this.refetchLoading = false
  787. })
  788. },
  789. // 取消:点击后关闭当前路由
  790. handleCancel() {
  791. sessionStorage.removeItem('da3_fondsName')
  792. window.close()
  793. }
  794. }
  795. }
  796. </script>
  797. <style scoped>
  798. * { box-sizing: border-box; }
  799. .da3 { font-family: "SimSun", "宋体", serif; color: #000; background: #fff; padding: 20px 20px 76px; }
  800. /* 编辑模式底部固定保存栏 */
  801. .save-bar {
  802. position: fixed; left: 0; right: 0; bottom: 0; z-index: 2000;
  803. display: flex; justify-content: flex-end; align-items: center;
  804. padding: 10px 28px; background: #fff;
  805. border-top: 1px solid #e4e7ed;
  806. box-shadow: 0 -2px 8px rgba(0, 0, 0, .06);
  807. }
  808. .save-bar .el-button.save-btn,.save-bar .el-button.saveetch-btn { min-width: 88px; color: #fff; background-color: #0348f3; border-color: #0348f3; }
  809. .toolbar { max-width: 1040px; margin: 0 auto 12px; font-size: 13px; color: #444; }
  810. .toolbar button { font-family: inherit; padding: 4px 10px; margin-right: 8px; cursor: pointer; }
  811. .toolbar .hint { color: #888; }
  812. table.form {
  813. width: 1000px; margin: 0 auto; border-collapse: collapse;
  814. background: #fff; border: hidden;
  815. }
  816. /* 标题、附件、元信息区无边框 */
  817. table.form .attach, table.form .title,
  818. table.form .meta-row td {
  819. border: none;
  820. }
  821. /* 01-02 无边框 */
  822. table.form .unit-row td {
  823. border: none;
  824. }
  825. /* 03-07 有边框 */
  826. table.form .unit-row-bordered td {
  827. border: 1px solid #000;
  828. }
  829. /* 列头 + 数据区有边框 */
  830. table.form .colname, table.form .colsub,
  831. table.form .body-row td {
  832. border-right: 1px solid #000;
  833. }
  834. table.form .colname:last-child,
  835. table.form .colsub:last-child,
  836. table.form .body-row td:last-child {
  837. border-right: none;
  838. }
  839. table.form td, table.form th { padding: 2px 6px; vertical-align: middle; font-size: 13px; line-height: 1.6; }
  840. .attach { font-size: 12px; text-align: left; }
  841. table.form td.title { font-size: 30px !important; font-weight: bold; text-align: center; letter-spacing: 6px; padding: 12px 0; font-family: "SimHei", "黑体", sans-serif; }
  842. .meta { font-size: 12px; white-space: nowrap; }
  843. .meta-flex { display: flex; justify-content: flex-end; align-items: baseline; width: 210px; margin-left: auto; }
  844. .meta-label { display: block; width: 60px; text-align: justify; text-align-last: justify; }
  845. .meta-flex .colon { font-style: normal; }
  846. .meta-value { display: block; flex: 1; margin-left: 5px; text-align: justify; text-align-last: justify; word-break: break-all; }
  847. .ucode { text-align: center; width: 30px; font-weight: bold; white-space: nowrap; }
  848. .ulabel { font-size: 13px; white-space: nowrap; border-right: none !important; }
  849. .ulabel input { width: auto; min-width: 200px; border: none; border: 1px solid #ccc ; font-family: inherit; font-size: 13px; padding: 2px; outline: none; }
  850. .check-line { text-align: left; font-size: 13px; line-height: 1.8; }
  851. .check-cell { white-space: nowrap; border-right: none !important; }
  852. .colname { font-weight: bold; text-align: center; background: #fff; border-top: 1px solid #000; border-bottom: 1px solid #000; white-space: nowrap; }
  853. .colsub { text-align: center; font-size: 12px; color: #000; font-weight: normal; border-bottom: 1px solid #000; white-space: nowrap; }
  854. .section td { font-weight: bold; }
  855. .nm { white-space: pre; }
  856. .unit { text-align: center; width: 60px; white-space: nowrap; }
  857. .code { text-align: center; width: 42px; white-space: nowrap; }
  858. .data { width: 160px; text-align: center; white-space: nowrap; }
  859. .data input, .data select { width: 96%; border: none; border: 1px solid #ccc; padding: 4px 0; font-family: inherit; font-size: 13px; text-align: center; outline: none; }
  860. /* 查看模式纯文本展示 */
  861. .plain { display: inline-block; min-width: 60px; min-height: 1em; }
  862. .dash { text-align: center; }
  863. .footer-row td { border: none; border-top: 1px solid #000; padding: 8px 6px; }
  864. .footer-cell { white-space: nowrap; }
  865. .footer-label { white-space: nowrap; }
  866. .footer-input { border: 1px solid #ccc; font-family: inherit; font-size: 13px; padding: 2px; outline: none; width: 70px; display: inline-block; vertical-align: middle; }
  867. .footer-input-wide { width: 120px; }
  868. .footer-note td { border: none; padding: 8px 6px; font-size: 12px; }
  869. </style>