소스 검색

设计量细目,ai图片识别表格数据1.0

MaiXinRong 2 주 전
부모
커밋
d1f4052eb0

+ 52 - 0
app/controller/tender_controller.js

@@ -1819,6 +1819,58 @@ module.exports = app => {
                 ctx.ajaxErrorBody(err, '保存计算式失败');
                 ctx.ajaxErrorBody(err, '保存计算式失败');
             }
             }
         }
         }
+
+        async aiTrans(ctx) {
+            let stream;
+            try {
+                const parts = ctx.multipart({ autoFields: true });
+
+                let index = 0;
+                const create_time = Date.parse(new Date()) / 1000;
+                stream = await parts();
+                const user = await ctx.service.projectAccount.getDataById(ctx.session.sessionUser.accountId);
+                const rela_id = parts.field.rela_id;
+                const rela_type = parts.field.rela_type;
+
+                const uploadfiles = [];
+                while (stream !== undefined) {
+                    if (!stream.filename) throw '未发现上传文件!';
+
+                    const fileInfo = path.parse(stream.filename);
+                    const filepath = `app/public/upload/${ctx.tender.id}/temp/${ctx.moment().format('YYYYMMDD')}/${create_time + '_' + index + fileInfo.ext}`;
+
+                    // 保存文件
+                    await ctx.app.fujianOss.put(ctx.app.config.fujianOssFolder + filepath, stream);
+                    await sendToWormhole(stream);
+
+                    // 插入到stage_pay对应的附件列表中
+                    uploadfiles.push({
+                        rela_id, rela_type,
+                        filename: fileInfo.name,
+                        fileext: fileInfo.ext,
+                        filesize: Array.isArray(parts.field.size) ? parts.field.size[index] : parts.field.size,
+                        filepath,
+                    });
+                    ++index;
+                    if (Array.isArray(parts.field.size) && index < parts.field.size.length) {
+                        stream = await parts();
+                    } else {
+                        stream = undefined;
+                    }
+                }
+
+                const result = await ctx.service.tempFile.addFiles(ctx.tender.id, uploadfiles, user);
+                const AIInspect = require('../lib/ai_inspect');
+                const aiInspect = new AIInspect({ apiKey: 'app-UGCJ6nlhZIcldyArOUyNwM21', baseUrl: 'http://39.108.81.93:8080/v1' });
+                const transResult = await aiInspect.imageTransExcel(result);
+                ctx.body = { err: 0, msg: '', data: transResult.res };
+            } catch (err) {
+                ctx.log(err);
+                // 失败需要消耗掉stream 以防卡死
+                if (stream) await sendToWormhole(stream);
+                ctx.ajaxErrorBody(err, '图片转换失败');
+            }
+        }
     }
     }
 
 
     return TenderController;
     return TenderController;

+ 55 - 0
app/lib/ai_inspect.js

@@ -48,6 +48,61 @@ class AiInspect {
             throw new Error('Dify 请求失败');
             throw new Error('Dify 请求失败');
         }
         }
     }
     }
+
+    async posCalcDetailMatch(template, file, userId = `user_123`) {
+        const url = `${this.baseUrl}/workflows/run`;
+        const headers = {
+            'Authorization': `Bearer ${this.apiKey}`,
+            'Content-Type': 'application/json',
+        };
+        const body = {
+            inputs: {
+                headerList: template.col_set.map(x => { return x.title }).join('|'),
+                imgList: [{
+                    type: "image",
+                    transfer_method: "remote_url",
+                    url: file.viewpath,
+                }],
+            },
+            user: userId,
+        };
+
+        try {
+            const res = await axios.post(url, body, { headers });
+            return res.data;
+        } catch (err) {
+            console.error('Dify 请求失败:', err.response || err.response.data || err.message);
+            throw new Error('Dify 请求失败');
+        }
+    }
+
+    async imageTransExcel(files, userId = `user_123`) {
+        const url = `${this.baseUrl}/workflows/run`;
+        const headers = {
+            'Authorization': `Bearer ${this.apiKey}`,
+            'Content-Type': 'application/json',
+        };
+        const body = {
+            inputs: { imgList: [] },
+            user: userId,
+            response_mode: 'blocking',
+        };
+        for (const f of files) {
+            body.inputs.imgList.push({
+                type: "image",
+                transfer_method: "remote_url",
+                url: f.viewpath,
+            })
+        }
+
+        try {
+            const res = await axios.post(url, body, { headers });
+            return res.data.data.outputs;
+        } catch (err) {
+            console.error('Dify 请求失败:', err.response || err.response.data || err.message);
+            throw new Error('Dify 请求失败');
+        }
+    }
 }
 }
 
 
 module.exports = AiInspect;
 module.exports = AiInspect;

+ 16 - 1
app/public/js/ledger.js

@@ -133,6 +133,7 @@ $(document).ready(function() {
     const ancGclSheet = ancGclSpread.getActiveSheet();
     const ancGclSheet = ancGclSpread.getActiveSheet();
 
 
     // 设计量明细相关
     // 设计量明细相关
+    const aiTrans = AITrans();
     const posCalcDetail = (function() {
     const posCalcDetail = (function() {
         const detail = createPosCalcDetail({ id: 'id', masterId: 'pid', topId: 'lid', sort: [['pcd_order', 'asc']]});
         const detail = createPosCalcDetail({ id: 'id', masterId: 'pid', topId: 'lid', sort: [['pcd_order', 'asc']]});
         const spread = SpreadJsObj.createNewSpread($('#pos-detail-spread')[0]);
         const spread = SpreadJsObj.createNewSpread($('#pos-detail-spread')[0]);
@@ -499,6 +500,20 @@ $(document).ready(function() {
                             ctrlObj.baseOpr('up-move');
                             ctrlObj.baseOpr('up-move');
                         }
                         }
                     },
                     },
+                    'ai-trans': {
+                        name: 'AI识别',
+                        disabled: function (key, opt) {
+                            const pos = SpreadJsObj.getSelectObject(posSheet);
+                            return !pos;
+                        },
+                        callback: function(key, opt) {
+                            const pos = SpreadJsObj.getSelectObject(posSheet);
+                            aiTrans.show('pos-calc-detail', pos.id);
+                        },
+                        visible: function(key, opt) {
+                            return is_debug;
+                        }
+                    }
                 }
                 }
             });
             });
         }
         }
@@ -3493,7 +3508,7 @@ $(document).ready(function() {
                 toastr.error('父节点不可插入计量单元');
                 toastr.error('父节点不可插入计量单元');
                 SpreadJsObj.reLoadRowData(info.sheet, info.row);
                 SpreadJsObj.reLoadRowData(info.sheet, info.row);
                 return;
                 return;
-            }else if (!node.b_code || node.b_code === '') {
+            } else if (!node.b_code || node.b_code === '') {
                 toastr.error('项目节不可插入计量单元');
                 toastr.error('项目节不可插入计量单元');
                 SpreadJsObj.reLoadRowData(info.sheet, info.row);
                 SpreadJsObj.reLoadRowData(info.sheet, info.row);
                 return;
                 return;

+ 56 - 0
app/public/js/shares/ai_trans.js

@@ -0,0 +1,56 @@
+const AITrans = function() {
+    const transResultObj = {
+        spread: null,
+        sheet: null,
+        initSpread: function() {
+            if (!this.spread) {
+                this.spread = SpreadJsObj.createNewSpread($('#ai-trans-spread')[0]);
+                this.sheet = this.spread.getActiveSheet();
+            }
+        }
+    };
+    $('#ai-trans').bind('shown.bs.modal', function() {
+        transResultObj.spread.refresh();
+    });
+    $('#ai-trans-file').change(function() {
+        const files = this.files;
+        if (!files || files.length === 0) return;
+
+        const formData = new FormData();
+        formData.append('rela_type', transResultObj.rela_type);
+        formData.append('rela_id', transResultObj.rela_id);
+        for (const file of files) {
+            if (file === undefined) {
+                toastr.error('未选择上传文件!');
+                return false;
+            }
+            const filesize = file.size;
+            if (filesize > 50 * 1024 * 1024) {
+                toastr.error('存在上传文件大小过大!');
+                return false;
+            }
+            const fileext = '.' + file.name.toLowerCase().split('.').splice(-1)[0];
+            if (whiteList.indexOf(fileext) === -1) {
+                toastr.error('只能上传指定格式的附件!');
+                return false;
+            }
+            formData.append('size', filesize);
+            formData.append('file[]', file);
+        }
+        postDataWithFile('ai-trans', formData, function (data) {
+            const sheetParts = data.split('==++==');
+            for (const sp of sheetParts) {
+                const rows = sp.split('\n').map(r => { return r.split('|')});
+                console.log(rows);
+                SpreadJsObj.loadSimpleSheetData(transResultObj.sheet, rows);
+            }
+        });
+    });
+    const show = function(rela_type, rela_id) {
+        transResultObj.initSpread();
+        transResultObj.rela_type = rela_type;
+        transResultObj.rela_id = rela_id;
+        $('#ai-trans').modal('show');
+    };
+    return { show }
+};

+ 35 - 0
app/public/js/spreadjs_rela/spreadjs_zh.js

@@ -1146,6 +1146,41 @@ const SpreadJsObj = {
         this.protectedSheet(sheet);
         this.protectedSheet(sheet);
         this.reLoadSheetData(sheet, force);
         this.reLoadSheetData(sheet, force);
     },
     },
+    loadSimpleSheetData: function(sheet, data, font) {
+        const sortData = data;
+        this.beginMassOperation(sheet);
+        try {
+            sheet.setRowCount(0, spreadNS.SheetArea.viewport);
+            sheet.clear(0, 0, sheet.getRowCount(), sheet.getColumnCount(), spreadNS.SheetArea.viewport, spreadNS.StorageType.data);
+            sheet.getRange(0, 0, sheet.getRowCount(), sheet.getColumnCount()).backColor(sheet.getDefaultStyle().backColor);
+            // 设置总行数
+            const totalRow = sortData.length;
+            sheet.setRowCount(totalRow, spreadNS.SheetArea.viewport);
+            if (sortData && sortData.length > 0) sheet.setColumnCount(data[0].length, spreadNS.SheetArea.viewport);
+
+            if (sortData) {
+                // 单元格写入数据
+                sortData.forEach(function (d, i) {
+                    // 单元格重新写入数据
+                    if (!d) return;
+
+                    d.forEach(function (value, j) {
+                        const cell = sheet.getCell(i, j);
+                        cell.value(value).vAlign(1).hAlign(1);
+                        font && cell.font(font);
+                        cell.setBorder(sheet.borderLine, {all: true});
+                    });
+                });
+                sortData[0].forEach(function(value, j) {
+                    sheet.autoFitColumn(j);
+                })
+            }
+            this.endMassOperation(sheet);
+        } catch (err) {
+            console.log(err);
+            this.endMassOperation(sheet);
+        }
+    },
     /**
     /**
      * 获取复制数据HTML格式(过滤不可见单元格)
      * 获取复制数据HTML格式(过滤不可见单元格)
      * @param {GC.Spread.Sheets.Worksheet} sheet
      * @param {GC.Spread.Sheets.Worksheet} sheet

+ 1 - 0
app/router.js

@@ -641,6 +641,7 @@ module.exports = app => {
     app.post('/tender/:id/saveRela', sessionAuth, tenderCheck, subProjectCheck, 'tenderController.saveRelaData');
     app.post('/tender/:id/saveRela', sessionAuth, tenderCheck, subProjectCheck, 'tenderController.saveRelaData');
     app.get('/tender/:id/cert', sessionAuth, tenderCheck, subProjectCheck, 'tenderController.certSet');
     app.get('/tender/:id/cert', sessionAuth, tenderCheck, subProjectCheck, 'tenderController.certSet');
     app.post('/tender/:id/cert/save', sessionAuth, tenderCheck, subProjectCheck, 'tenderController.saveCert');
     app.post('/tender/:id/cert/save', sessionAuth, tenderCheck, subProjectCheck, 'tenderController.saveCert');
+    app.post('/tender/:id/ai-trans', sessionAuth, tenderCheck, subProjectCheck, 'tenderController.aiTrans');
 
 
     app.get('/tender/:id/ctrl-price', sessionAuth, tenderCheck, subProjectCheck, 'ctrlPriceController.index');
     app.get('/tender/:id/ctrl-price', sessionAuth, tenderCheck, subProjectCheck, 'ctrlPriceController.index');
     app.post('/tender/:id/ctrl-price/load', sessionAuth, tenderCheck, subProjectCheck, 'ctrlPriceController.load');
     app.post('/tender/:id/ctrl-price/load', sessionAuth, tenderCheck, subProjectCheck, 'ctrlPriceController.load');

+ 96 - 0
app/service/temp_file.js

@@ -0,0 +1,96 @@
+'use strict';
+
+/**
+ *
+ *  临时文件
+ * @author Mai
+ * @date 2026/4/21
+ * @version
+ */
+
+
+module.exports = app => {
+    class TempFile extends app.BaseService {
+        /**
+         * 构造函数
+         *
+         * @param {Object} ctx - egg全局变量
+         * @return {void}
+         */
+        constructor(ctx) {
+            super(ctx);
+            this.tableName = 'temp_file';
+        }
+
+        _analysisData(files) {
+            const helper = this.ctx.helper;
+            const userId = this.ctx.session.sessionUser.accountId;
+            const ossPath = this.ctx.app.config.fujianOssPath;
+            files.forEach(x => {
+                x.viewpath = helper.canPreview(x.fileext) ? ossPath + x.filepath : '';
+                x.filepath = ossPath + x.filepath;
+                x.fileext_str = helper.fileExtStr(x.fileext);
+                x.canEdit = x.user_id === userId;
+            });
+        }
+
+        async getFiles(condition) {
+            condition.orders = [['create_time', 'desc']];
+            const result = await this.getAllDataByCondition(condition);
+            this._analysisData(result);
+            return result;
+        }
+
+        async addFiles(tenderId, fileInfo, user) {
+            const conn = await this.db.beginTransaction();
+            const result = {};
+            try {
+                const insertData = fileInfo.map(x => {
+                    return {
+                        id: this.uuid.v4(), tid: tenderId, rela_type: x.rela_type, rela_id: x.rela_id,
+                        user_id: user.id, user_name: user.name, user_company: user.company, user_role: user.role,
+                        filename: x.filename, fileext: x.fileext, filesize: x.filesize, filepath: x.filepath,
+                    };
+                });
+                await conn.insert(this.tableName, insertData);
+                await conn.commit();
+                result.files = { id: insertData.map(x => { return x.id; })};
+            } catch (err) {
+                await conn.rollback();
+                throw err;
+            }
+            return await this.getFiles({ where: result.files });
+        }
+
+        async delFiles(files) {
+            const fileDatas = await this.getAllDataByCondition({ where: { id: files } });
+            const result = {};
+
+            const conn = await this.db.beginTransaction();
+            try {
+                for (const f of fileDatas) {
+                    await this.ctx.app.fujianOss.delete(this.ctx.app.config.fujianOssFolder + f.filepath);
+                }
+                await conn.delete(this.tableName, { id: files });
+                await conn.commit();
+                result.del = files;
+            } catch (err) {
+                await conn.rollback();
+                throw err;
+            }
+            return result;
+        }
+
+        async clearFiles(days) {
+            const fileDatas = await this.db.query(`SELECT * FROM ${this.tableName} WHERE create_time < DATE_SUB(CURDATE(), INTERVAL ${days} day)`);
+            if (fileDatas.length === 0) return;
+
+            for (const f of fileDatas) {
+                await this.ctx.app.fujianOss.delete(this.ctx.app.config.fujianOssFolder + f.filepath);
+                await this.deleteById(f.id);
+            }
+        }
+    }
+
+    return TempFile;
+};

+ 1 - 0
app/view/ledger/explode_modal.ejs

@@ -616,6 +616,7 @@
 <% include ../shares/hint_modal.ejs %>
 <% include ../shares/hint_modal.ejs %>
 <% include ../shares/batch_replace_modal.ejs %>
 <% include ../shares/batch_replace_modal.ejs %>
 <% include ../shares/dsk_modal.ejs %>
 <% include ../shares/dsk_modal.ejs %>
+<% include ../shares/ai_trans.ejs %>
 <% if (ctx.subProject.fun_rela.ledgerAss && ctx.session.sessionUser.accountId === tender.user_id && (tender.ledger_status === auditConst.status.uncheck || tender.ledger_status === auditConst.status.checkNo)) { %>
 <% if (ctx.subProject.fun_rela.ledgerAss && ctx.session.sessionUser.accountId === tender.user_id && (tender.ledger_status === auditConst.status.uncheck || tender.ledger_status === auditConst.status.checkNo)) { %>
     <% include ../shares/tree_ass_modal.ejs %>
     <% include ../shares/tree_ass_modal.ejs %>
 <% } %>
 <% } %>

+ 21 - 0
app/view/shares/ai_trans.ejs

@@ -0,0 +1,21 @@
+<div class="modal fade" id="ai-trans" data-backdrop="static">
+    <div class="modal-dialog modal-lg" role="document">
+        <div class="modal-content">
+            <div class="modal-header">
+                <h5 class="modal-title">AI识别(图片转表格)</h5>
+                <button type="button" class="close" data-dismiss="modal" aria-label="Close">
+                    <span aria-hidden="true">&times;</span>
+                </button>
+            </div>
+            <div class="modal-body">
+                <div class="form-group">
+                    <label for="ai-trans-file">选择文件</label><i class="fa fa-spinner fa-pulse fa-lg fa-fw text-primary" id="select-excel-loading" style="display: none;"></i>
+                    <input type="file" class="form-control-file" id="ai-trans-file" accept="*.json" name="file">
+                </div>
+            </div>
+            <div class="modal-body">
+                <div id="ai-trans-spread" class="modal-height-500"></div>
+            </div>
+        </div>
+    </div>
+</div>

+ 1 - 0
config/web.js

@@ -230,6 +230,7 @@ const JsFiles = {
                     '/public/js/shares/tenders2tree.js',
                     '/public/js/shares/tenders2tree.js',
                     '/public/js/shares/tender_select.js',
                     '/public/js/shares/tender_select.js',
                     '/public/js/shares/db2full_code.js',
                     '/public/js/shares/db2full_code.js',
+                    '/public/js/shares/ai_trans.js',
                     '/public/js/ledger.js',
                     '/public/js/ledger.js',
                 ],
                 ],
                 mergeFile: 'explode',
                 mergeFile: 'explode',