calcCheckItemFee.vue 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545
  1. <template>
  2. <CustomDialog
  3. :model-value="modelValue"
  4. title="费用计算"
  5. width="600px"
  6. :before-close="handleClose"
  7. class="calcCheckItemDialog"
  8. v-loading="loading"
  9. >
  10. <div class="content" v-if="!isManualInput">
  11. <div class="content-title">
  12. 用户输入
  13. </div>
  14. <el-form ref="enterFormRef" :model="enterForm" :rules="enterFormRules">
  15. <el-table :data="enterDataList" border v-loading="loading">
  16. <el-table-column label="输入项" prop="name"/>
  17. <el-table-column label="用户输入区" prop="">
  18. <template #default="{row}">
  19. <el-form-item :prop="row.code">
  20. <el-input-number
  21. v-if="row.valType === 'number' || row.valType === 'NUMBER' || row.colType === 'NUMBER'"
  22. v-model="row.inputText"
  23. placeholder="请输入"
  24. :controls="false"
  25. />
  26. <el-input
  27. v-else
  28. v-model="row.inputText"
  29. placeholder="请输入"
  30. />
  31. </el-form-item>
  32. </template>
  33. </el-table-column>
  34. <el-table-column label="说明" prop="description"/>
  35. </el-table>
  36. <div class="button-box" v-if="enterDataList.length > 0">
  37. <el-button type="primary" @click="handleCalcFee">费用计算</el-button>
  38. </div>
  39. <div v-else class="empty-text">
  40. 暂无输入项
  41. </div>
  42. </el-form>
  43. </div>
  44. <div class="content" v-if="!isManualInput">
  45. <div class="content-title">
  46. 自动计算
  47. </div>
  48. <el-form ref="outputFormRef" :model="outputForm" :rules="outputFormRules">
  49. <el-table :data="outputDataList" border v-loading="loading">
  50. <el-table-column label="输出项" prop="name"/>
  51. <el-table-column label="自动计算" prop="">
  52. <template #default="{row}">
  53. <el-form-item :prop="row.code">
  54. <el-input
  55. v-model="row.inputText"
  56. placeholder="请输入"
  57. :disabled="true"
  58. />
  59. </el-form-item>
  60. </template>
  61. </el-table-column>
  62. <el-table-column label="说明" prop="description"/>
  63. </el-table>
  64. <div v-if="outputDataList.length === 0" class="empty-text">
  65. 暂无输出项
  66. </div>
  67. </el-form>
  68. </div>
  69. <div class="content">
  70. <div class="content-title">
  71. 项目检验费用
  72. </div>
  73. <el-form ref="feeFormRef" :model="feeForm" :rules="feeFormRules" label-width="160px">
  74. <el-form-item label="自动计算总费用:" prop="" v-if="!isBatch">
  75. {{ isManualInput ? '项目未设置计算规则,请手动填入实际费用' : feeForm.totalCost }}
  76. </el-form-item>
  77. <el-form-item label="历史收费:" prop="">
  78. {{ templateInfo.fee || 0 }}
  79. </el-form-item>
  80. <el-form-item label="实际费用:" prop="actualFee">
  81. <el-input-number v-model="feeForm.actualFee" :controls="false" :precision="2" :min="0"
  82. :step="0.01" style="width: 220px;"/>
  83. </el-form-item>
  84. </el-form>
  85. </div>
  86. <template #footer>
  87. <el-button type="primary" @click="handleSave">保存</el-button>
  88. <el-button type="default" @click="handleClose">取消</el-button>
  89. </template>
  90. </CustomDialog>
  91. <!-- 费用计算模板:不在页面显示 -->
  92. <Teleport to="body">
  93. <Designer
  94. v-if="!isManualInput"
  95. id="gc-designer-container"
  96. ref="spreadDesignerRef"
  97. @designer-initialized="handleDesignerInit"
  98. />
  99. </Teleport>
  100. </template>
  101. <script setup>
  102. import {nextTick} from 'vue'
  103. import {ElMessage} from 'element-plus'
  104. import CustomDialog from '@/components/CustomDialog/index.vue'
  105. import {BoilerTaskOrderApi} from '@/api/pressure2/boilertaskorder'
  106. import {buildFileUrl} from '@/utils'
  107. import axios from 'axios'
  108. import {is} from '@/utils/is'
  109. // 导入葡萄城相关依赖
  110. //import '@grapecity/spread-sheets-resources-zh'
  111. import '@grapecity-software/spread-sheets-designer-resources-cn'
  112. import '@grapecity-software/spread-sheets/styles/gc.spread.sheets.excel2013white.css'
  113. import '@grapecity-software/spread-sheets-designer/styles/gc.spread.sheets.designer.min.css'
  114. import '@grapecity-software/spread-sheets-io'
  115. import '@grapecity-software/spread-sheets-pdf'
  116. import '@grapecity-software/spread-sheets-print'
  117. import '@grapecity-software/spread-sheets-shapes'
  118. import '@grapecity-software/spread-sheets-barcode'
  119. import '@grapecity-software/spread-sheets-pivot-addon'
  120. import '@grapecity-software/spread-sheets-tablesheet'
  121. import '@grapecity-software/spread-sheets-designer'
  122. import Designer from '@grapecity-software/spread-sheets-designer-vue'
  123. import GC from "@/components/SpreadDesigner/tools/gc";
  124. const {queryCheckItemCalcPreFillField} = BoilerTaskOrderApi
  125. import {getPressureReportTemplateInfo} from '@/api/pressure2/reportTemplate'
  126. import {PressureCheckerMyTaskStatus} from "@/utils/constants";
  127. import {BoilerConnectRecordReportApi} from "@/api/pressure2/boilerconnectrecordreport";
  128. import {DynamicTbFeeColApi} from "@/api/pressure2/dynamictbfeecol";
  129. const props = defineProps({
  130. modelValue: {
  131. type: Boolean,
  132. required: true
  133. },
  134. templateInfo: {
  135. type: Object,
  136. required: true,
  137. default: () => ({})
  138. },
  139. equipmentId: {
  140. type: String
  141. },
  142. isBatch: {
  143. type: Boolean,
  144. default: false
  145. }
  146. })
  147. const emit = defineEmits(['update:modelValue', 'refresh', 'save'])
  148. const loading = ref(true)
  149. const enterFormRef = ref(null)
  150. const outputFormRef = ref(null)
  151. const feeFormRef = ref(null)
  152. const enterForm = ref({})
  153. const outputForm = ref({})
  154. const feeForm = ref({
  155. actualFee: 0,
  156. totalCost: 0
  157. })
  158. const enterFormRules = ref({})
  159. const outputFormRules = ref({})
  160. const feeFormRules = ref({})
  161. const enterDataList = ref([])
  162. const outputDataList = ref([])
  163. const isReport = ref(false)
  164. let designer = null
  165. const spreadDesignerRef = ref(null)
  166. // 费用模板初始化
  167. const handleDesignerInit = async (instance) => {
  168. designer = instance
  169. // 设置设计器配置
  170. let config = JSON.parse(JSON.stringify(GC.Spread.Sheets.Designer.DefaultConfig))
  171. instance.setConfig(config)
  172. // 切换到设计模式
  173. let designModeCommand = GC.Spread.Sheets.Designer.getCommand(
  174. GC.Spread.Sheets.Designer.CommandNames.DesignMode
  175. )
  176. designModeCommand.execute(designer)
  177. isReport.value = props.templateInfo.taskStatus >= PressureCheckerMyTaskStatus.REPORT_INPUT
  178. await handleGetPressureReportTemplateInfo()
  179. // 加载费用计算模板
  180. await handleRenderFormulaTemplate()
  181. }
  182. // 获取费用模板信息
  183. const fetchTemplateData = ref({})
  184. const combineTemplateInfo = computed(() => ({...props.templateInfo, ...fetchTemplateData.value}))
  185. const feeCalculateJson = computed(() => !props.templateInfo.feeCalculateJson ? {} : JSON.parse(props.templateInfo.feeCalculateJson))
  186. // 是否手动录入 true是,false否
  187. const isManualInput = computed(() => combineTemplateInfo.value.feeCalcType === '1' || !combineTemplateInfo.value.feeCalcType )
  188. const handleGetPressureReportTemplateInfo = async () => {
  189. console.log('combineTemplateInfo', combineTemplateInfo.value)
  190. const byTemplateId = await BoilerConnectRecordReportApi.getByTemplateId({
  191. templateId: isReport.value ? combineTemplateInfo.value.reportTemplateId : combineTemplateInfo.value.templateId,
  192. type: isReport.value ? 'report' : 'record'
  193. });
  194. if (byTemplateId.length !== 1) {
  195. ElMessage.error('获取费用模板信息失败')
  196. }
  197. fetchTemplateData.value = byTemplateId[0]
  198. }
  199. // 渲染费用计算模板
  200. const handleRenderFormulaTemplate = async () => {
  201. const spread = designer?.getWorkbook()
  202. if (isManualInput.value){
  203. return
  204. }
  205. if (!combineTemplateInfo.value.feeFileUrl) {
  206. console.warn('公式模板URL不存在')
  207. ElMessage.warning('费用计算模板未配置,请联系管理员')
  208. return
  209. }
  210. try {
  211. const newFileUrl = (combineTemplateInfo.value.feeFileUrl || '').startsWith('http')
  212. ? combineTemplateInfo.value.feeFileUrl
  213. : import.meta.env.VITE_FILE_URL + '/' + combineTemplateInfo.value.feeFileUrl
  214. // 直接使用 fetch 获取 JSON 数据
  215. const response = await fetch(newFileUrl)
  216. if (!response.ok) {
  217. throw new Error(`加载报表失败: ${response.status} ${response.statusText}`)
  218. }
  219. // 不严格检查 content-type,尝试直接解析 JSON
  220. const text = await response.text()
  221. try {
  222. const json = JSON.parse(text)
  223. // 使用 fromJSON 方法加载 JSON 数据
  224. spread.fromJSON(json)
  225. // 让活动工作表的单元格全部失焦
  226. spread.getActiveSheet().setActiveCell(null);
  227. ElMessage.success('费用计算模板加载成功')
  228. } catch (jsonError) {
  229. console.error('JSON 解析失败:', jsonError)
  230. throw new Error('报表文件格式错误:不是有效的 JSON 格式')
  231. }
  232. } catch (error) {
  233. console.error('加载公式模板失败:', error)
  234. ElMessage.error('加载费用计算模板失败: ' + (error.message || '未知错误'))
  235. }
  236. }
  237. const allCalcPreFillFieldKeyValue = ref({})
  238. const allCalcPreFillField = ref([])
  239. // 获取费用计算字段
  240. const handleQueryCheckItemCalcPreFillField = async () => {
  241. loading.value = true
  242. try {
  243. const fields = await DynamicTbFeeColApi.getAllField(isReport.value ? 'report' : 'record', isReport.value ? props.templateInfo.reportTemplateId : props.templateInfo.templateId)
  244. allCalcPreFillField.value = fields || []
  245. // 保存所有的输入项 (colType为INPUT或NUMBER的为输入项)
  246. enterDataList.value = (fields || []).filter(field => field.colType === 'INPUT' || field.colType === 'NUMBER').map(field => ({
  247. inputText: feeCalculateJson.value[field.code] || field.defaultValue || '',
  248. ...field
  249. }))
  250. // 保存所有的输出项 (colType为TOTAL或OUTPUT的为输出项)
  251. outputDataList.value = JSON.parse(JSON.stringify((fields || []).filter(field => field.colType === 'TOTAL' || field.colType === 'OUTPUT').map(field => ({
  252. inputText: feeCalculateJson.value[field.code] || '',
  253. ...field
  254. }))))
  255. // 获取所有需要field的code(包含输入项和输出项等所有字段),转成{[code]: ''}格式,set到费用模板中
  256. allCalcPreFillFieldKeyValue.value = Object.fromEntries(allCalcPreFillField.value.map(item => ([
  257. item.code,
  258. feeCalculateJson.value[item.code] || item.defaultValue || ''
  259. ])))
  260. } catch (error) {
  261. console.error('获取费用计算字段报错了', error)
  262. } finally {
  263. loading.value = false
  264. }
  265. }
  266. onMounted(() => {
  267. // 获取费用计算字段
  268. if (!isManualInput.value){
  269. handleQueryCheckItemCalcPreFillField()
  270. }
  271. if (combineTemplateInfo.value.feeCalcType === '1'){
  272. feeForm.value = {
  273. actualFee: feeCalculateJson.value['actualFee'] || feeCalculateJson.value['总费用'] || 0,
  274. totalCost: feeCalculateJson.value['totalCost'] || feeCalculateJson.value['总费用'] || 0
  275. }
  276. }else {
  277. console.log(combineTemplateInfo)
  278. feeForm.value = {
  279. actualFee: combineTemplateInfo.value.fee,
  280. totalCost: combineTemplateInfo.value.fee
  281. }
  282. }
  283. })
  284. // 自动计算费用
  285. const handleCalcFee = () => {
  286. if (!designer || !spreadDesignerRef.value) {
  287. console.error('SpreadDesigner 未初始化')
  288. ElMessage.error('费用计算模板未初始化,请刷新页面重试')
  289. return
  290. }
  291. const spread = designer.getWorkbook()
  292. if (!spread) {
  293. console.error('无法获取 Spread 工作簿')
  294. ElMessage.error('费用计算模板加载失败,请刷新页面重试')
  295. return
  296. }
  297. // 如果 bindingPathSchema 不存在,使用默认值
  298. let bindingPathSchema = {
  299. "dataFields": allCalcPreFillField.value.map(item => ({
  300. "name": item.code,
  301. "displayName": item.name,
  302. "bindingPath": item.code
  303. }))
  304. }
  305. // 确保输入数据是数字类型
  306. const entryData = Object.fromEntries(enterDataList.value.map(item => {
  307. let value = item.inputText
  308. // 对于数值类型的字段,确保转换为数字
  309. if (item.colType === 'NUMBER' || item.valType === 'NUMBER') {
  310. value = Number(value) || '0'
  311. }
  312. return [item.code, value]
  313. }))
  314. // 确保预填充数据也是正确类型
  315. const preFillData = Object.fromEntries(Object.entries(allCalcPreFillFieldKeyValue.value).map(([key, value]) => {
  316. const field = enterDataList.value.find(item => item.code === key)
  317. if (field && (field.colType === 'NUMBER' || field.valType === 'NUMBER')) {
  318. return [key, Number(value) || '0']
  319. }
  320. return [key, value]
  321. }))
  322. const combineDataSource = {
  323. ...preFillData,
  324. ...entryData
  325. }
  326. console.log('计算数据源:', combineDataSource)
  327. console.log('绑定路径模式:', bindingPathSchema)
  328. // 使用葡萄城标准 API 计算费用
  329. try {
  330. console.log('开始计算费用...')
  331. // 获取 Spread 工作簿
  332. const spread = designer.getWorkbook()
  333. if (!spread) {
  334. throw new Error('无法获取 Spread 工作簿')
  335. }
  336. // 获取活动工作表
  337. const sheet = spread.getActiveSheet()
  338. if (!sheet) {
  339. throw new Error('无法获取活动工作表')
  340. }
  341. // 确保模板已经加载
  342. if (!combineTemplateInfo.value.feeFileUrl) {
  343. throw new Error('费用计算模板未配置')
  344. }
  345. // 准备符合葡萄城要求的数据源
  346. console.log('数据源:', combineDataSource)
  347. try {
  348. sheet.setDataSource( new GC.Spread.Sheets.Bindings.CellBindingSource(combineDataSource))
  349. console.log('设置数据源成功')
  350. } catch (e) {
  351. ElMessage.error('设置数据源失败,尝试其他方法:', e.message)
  352. }
  353. // 强制重新计算
  354. spread.repaint()
  355. // 获取计算结果
  356. nextTick(() => {
  357. try {
  358. // 获取计算结果
  359. let getDataSource = {}
  360. // 尝试从工作表获取数据源
  361. getDataSource = sheet.getDataSource().rT
  362. console.log('从工作表获取数据源成功:', getDataSource)
  363. // 检查是否有计算结果
  364. if (!getDataSource || (Array.isArray(getDataSource) && getDataSource.length === 0) || (typeof getDataSource === 'object' && Object.keys(getDataSource).length === 0)) {
  365. throw new Error('计算结果为空')
  366. }
  367. // 检查是否有计算结果
  368. if (Object.keys(getDataSource).length === 0) {
  369. throw new Error('计算结果为空')
  370. }
  371. // 检查总费用
  372. const totalCostItem = allCalcPreFillField.value.find(x => x.colType === 'TOTAL')
  373. const totalCostCode = totalCostItem?.code || '总费用'
  374. const calculatedTotal = getDataSource[totalCostCode] || 0
  375. if (calculatedTotal === 0) {
  376. console.warn('计算结果为0,可能是模板配置问题或输入数据问题')
  377. ElMessage.warning('费用计算结果为0,请检查输入数据和模板配置')
  378. }
  379. // 更新输出项
  380. outputDataList.value = outputDataList.value.map(item => ({
  381. ...item,
  382. inputText: getDataSource[item.code] || ''
  383. }))
  384. // 更新总费用
  385. feeForm.value.totalCost = calculatedTotal
  386. feeForm.value.actualFee = calculatedTotal
  387. console.log('计算的总费用:', calculatedTotal)
  388. ElMessage.success('费用计算完成')
  389. } catch (error) {
  390. console.error('获取计算结果时出错:', error)
  391. ElMessage.error('费用计算失败:' + (error.message || '未知错误'))
  392. }
  393. })
  394. } catch (error) {
  395. console.error('计算费用时出错:', error)
  396. ElMessage.error('费用计算失败:' + (error.message || '未知错误'))
  397. }
  398. }
  399. // 保存
  400. const handleSave = () => {
  401. const inputParams = Object.fromEntries(enterDataList.value.map(item => ([item.code, item.inputText])))
  402. const outputParams = Object.fromEntries(outputDataList.value.map(item => ([item.code, item.inputText])))
  403. emit('save', {
  404. ...props.templateInfo,
  405. fee: feeForm.value.actualFee || 0,
  406. feeCalculateJson: JSON.stringify({...inputParams, ...feeForm.value, ...outputParams})
  407. })
  408. handleClose()
  409. }
  410. // 关闭弹窗
  411. const handleClose = () => {
  412. emit('update:modelValue', false)
  413. }
  414. </script>
  415. <style lang="scss" scoped>
  416. .content-title {
  417. position: sticky;
  418. left: 0;
  419. top: 0;
  420. display: flex;
  421. justify-content: flex-start;
  422. align-items: center;
  423. width: 100%;
  424. height: 36px;
  425. margin-bottom: 16px;
  426. line-height: 36px;
  427. background-color: var(--el-color-primary-light-9);
  428. z-index: 1000;
  429. &::before {
  430. content: '';
  431. height: 70%;
  432. width: 4px;
  433. margin-right: 12px;
  434. background-color: var(--el-color-primary);
  435. }
  436. }
  437. .content {
  438. position: relative;
  439. margin-bottom: 16px;
  440. max-height: 400px;
  441. overflow-y: auto;
  442. .button-box {
  443. position: sticky;
  444. left: 0;
  445. bottom: 0;
  446. background-color: #fff;
  447. z-index: 1000;
  448. }
  449. }
  450. :deep(.el-input-number) {
  451. width: 100%;
  452. .el-input__inner {
  453. text-align: left;
  454. }
  455. }
  456. .button-box {
  457. display: flex;
  458. flex-direction: row;
  459. justify-content: center;
  460. padding-top: 16px;
  461. }
  462. .empty-text {
  463. text-align: center;
  464. color: #909399;
  465. padding: 20px 0;
  466. font-size: 14px;
  467. }
  468. </style>
  469. <style lang="scss">
  470. .calcCheckItemDialog {
  471. .el-dialog__footer {
  472. text-align: center;
  473. }
  474. }
  475. </style>