Bladeren bron

bug_fix:
1. 修改了创建项目时,非特权人员可以创建项目负责人不是他自己的项目。

eyes4 5 maanden geleden
bovenliggende
commit
7097f608e1

+ 2 - 0
base-lib/src/core-models/AcsFunction.ts

@@ -203,6 +203,8 @@ export class AcsFunction extends Model {
             (2163, 309, 'remove', '删除项目文档', 'prj.doc.remove', true, 1),
             (2164, 309, 'detail', '获取项目文档详情', 'prj.doc.detail', true, 0),
             (2165, 309, 'rename', '修改项目文档名', 'prj.doc.rename', true, 1),
+            (2166, 309, 'copy', '复制项目文档', 'prj.doc.copy', true, 1),
+            (2167, 309, 'archive', '归档项目文档', 'prj.doc.archive', true, 1),
             
             (2180, 310, 'milestone', '查看项目里程碑', 'prj.kanban.milestone', true, 0),
             

+ 2 - 0
base-lib/src/core-models/AcsRoleFunction.ts

@@ -182,6 +182,8 @@ export class AcsRoleFunction extends Model {
             ('admin', 2163),
             ('admin', 2164),
             ('admin', 2165),
+            ('admin', 2166),
+            ('admin', 2167),
             
             ('admin', 2180),
             

+ 3 - 8
base-lib/src/core-models/BizContractInfo.ts

@@ -1,7 +1,6 @@
 import {Model} from "sequelize";
 import { Sequelize, DataTypes }  from 'sequelize';
 import {BizCustomer} from "@core-models/BizCustomer";
-import {PrjInfo} from "@core-models/PrjInfo";
 import {AcsUserInfo} from "@core-models/AcsUserInfo";
 
 /**
@@ -10,6 +9,7 @@ import {AcsUserInfo} from "@core-models/AcsUserInfo";
 
 export class BizContractInfo extends Model {
     declare id: string;
+    declare name: string;
     declare doc_uploaded: boolean;
     declare pdf_uploaded: boolean;
     declare doc_id: string;
@@ -171,14 +171,9 @@ export class BizContractInfo extends Model {
         {fields: ['name']}
     ]
 
-    static associate(sequelize: Sequelize) {
+    static associate(_sequelize: Sequelize) {
         try {
-            // AcsDomain.hasMany(BizCustomer, {foreignKey: 'region_id', sourceKey: 'id'});
-            // BizCustomer.belongsTo(AcsDomain, {foreignKey: 'region_id', targetKey: 'id'});
-            // BizCustomerLevel.hasMany(BizCustomer, {foreignKey: 'level_id', sourceKey: 'id'});
-            // BizCustomer.belongsTo(BizCustomerLevel, {foreignKey: 'level_id', targetKey: 'id'});
-            // BizCustomerIndustry.hasMany(BizCustomer, {foreignKey: 'industry_id', sourceKey: 'id'});
-            // BizCustomer.belongsTo(BizCustomerIndustry, {foreignKey: 'industry_id', targetKey: 'id'});
+
         } catch (e) {
             console.log(e);
         }

+ 2 - 1
base-lib/src/core-models/BizCustomer.ts

@@ -11,6 +11,7 @@ import {AcsUserInfo} from "@core-models/AcsUserInfo";
 
 export class BizCustomer extends Model {
     declare id: string;
+    declare name: string;
 
     static table_name = 'tb_biz_customer';
 
@@ -129,7 +130,7 @@ export class BizCustomer extends Model {
         {fields: ['name']}
     ]
 
-    static associate(sequelize: Sequelize) {
+    static associate(_sequelize: Sequelize) {
         try {
             AcsDomain.hasMany(BizCustomer, {foreignKey: 'region_id', sourceKey: 'id'});
             BizCustomer.belongsTo(AcsDomain, {foreignKey: 'region_id', targetKey: 'id'});

+ 4 - 0
base-lib/src/core-models/PrjInfo.ts

@@ -12,12 +12,16 @@ import {BizContractInfo} from "@core-models/BizContractInfo";
 
 export class PrjInfo extends Model {
     declare id: string;
+    declare name: string;
+    declare region_id: number;
     declare phase_id: string;
     declare type_id: number;
+    declare customer_id: string;
     declare flow_case_id: string;
     declare leader_id: string;
     declare bizman_id: string;
     declare progress: number;
+    declare intro: string;
     declare checkers: any;
     declare contract_id: string;
     declare created_at: string;

+ 12 - 17
pmr-biz-manager/src/routes/api/cfg/staff/modify.ts

@@ -50,28 +50,23 @@ interface IData {
     status?: number;
 }
 
-function get_where(json: IRequest, _params: IMethodParams, _cached_data: ICachedData): Promise<any> {
-    return new Promise<any>(async (resolve, reject) => {
-        try {
-            let data = <IData>json.data;
-            resolve({id: data.id});
-        } catch (e) {
-            reject(e);
-        }
-    })
-}
-
 function modify(json: IRequest, params: IMethodParams, cached_data: ICachedData): Promise<any> {
     return new Promise<any>(async (resolve, reject) => {
         let t = await AcsUserInfo.sequelize!.transaction();
         try {
             let data = <IData>json.data;
             let staff = await AcsUserInfo.findOne({where:{id: data.id}, transaction: t});
-            if (!staff) throw Resp.gen_err(Resp.ResourceNotFound);
-            // data.name? staff.name = data.name : null;
-            // data.memo? staff.memo = data.memo: null;
-            // data.email? staff.email = data.email: null;
-            // data.pass? staff.pass =data.pass: null;
+            if (!staff) throw Resp.gen_err(Resp.ResourceNotFound, "员工不存在。");
+
+            // 不允许修改自己的信息
+            if (cached_data.user_id === data.id)
+                throw Resp.gen_err(Resp.Forbidden, "不允许修改自己的信息。");
+            if (data.full_range !== undefined) {
+                let me = await AcsUserInfo.findOne({where: {id: cached_data.user_id}, transaction: t});
+                if (me && me.ext_info.full_range === false && data.full_range === true) {
+                    throw Resp.gen_err(Resp.Forbidden, "只有特权人员才可以为其它用户添加特权。");
+                }
+            }
 
 
             let ext_info: any = {
@@ -98,7 +93,7 @@ function modify(json: IRequest, params: IMethodParams, cached_data: ICachedData)
             if (data.domain_ids !== undefined) {
                 await AcsUserDomain.sequelize!.query(`
                     delete from ${AcsUserDomain.tableName} where user_id = :user_id and domain_id in (select id from ${AcsDomain.tableName} where category='org')
-                `, {type: QueryTypes.DELETE, transaction: t});
+                `, {type: QueryTypes.DELETE, transaction: t, replacements: {user_id: data.id}});
                 for (let domain_id of data.domain_ids) {
                     await AcsUserDomain.create({user_id: data.id, domain_id: domain_id}, {
                         transaction: t,

+ 14 - 1
pmr-biz-manager/src/routes/api/prj/doc/get_list.ts

@@ -45,6 +45,10 @@ interface IData {
      * 精确通过项目编号查找文档
      */
     prj_id?: string;
+    /**
+     * 是否已归档
+     */
+    archived?: boolean;
 }
 
 
@@ -101,6 +105,10 @@ function get_list(json: IRequest, _params: IMethodParams, _cached_data: ICachedD
                 condition += ' and prj_file.uploaded_at <= :end_to ';
                 replacements.end_to = data.end_to + ' 23:59:59';
             }
+            if (data.archived !== undefined) {
+                condition += ' and prj_file.archived = :archive ';
+                replacements.archive = data.archived;
+            }
 
             count_sql += condition;
             select_sql += condition;
@@ -168,6 +176,10 @@ const v1_0: IApiProcessor = {
                     "category_id": {
                         "type": "string",
                         "title": "通过文档类型查找"
+                    },
+                    "archived": {
+                        "type": "boolean",
+                        "title": "是否已归档"
                     }
                 },
                 "x-apifox-orders": [
@@ -179,7 +191,8 @@ const v1_0: IApiProcessor = {
                     "creator_id",
                     "creator_name",
                     "begin_from",
-                    "end_to"
+                    "end_to",
+                    "archived"
                 ],
                 "title": "请求参数内容",
                 "required": [

+ 14 - 4
pmr-biz-manager/src/routes/api/prj/info/add.ts

@@ -11,6 +11,8 @@ import {FlowEngine, IHandler} from "@src/bpmn/flow_engine";
 import {BpmnCase} from "@core-models/BpmnCase";
 import {bpmn_flow_on_end} from "@src/utils/bpmn_work_helper";
 import {PrjLogger} from "@src/utils/prj_logger";
+import {AcsUserInfo} from "@core-models/AcsUserInfo";
+import {is_project_privileged_account} from "@src/utils/prj_premission_helper";
 
 interface IData {
     /**
@@ -69,6 +71,13 @@ function add(json: IRequest, params: IMethodParams, cached_data: ICachedData): P
             let data = <IData>json.data;
             if (data.checker_ids.length === 0)
                 throw Resp.gen_err(Resp.ParamsError, '审核组成员列表不能为空');
+            let leader = await AcsUserInfo.findOne({where: {id: data.leader_id}, transaction: t});
+            if (!leader) throw Resp.gen_err(Resp.InternalServerError, '项目负责人数据缺失!');
+            // 如果创建人不是特权人员,也不是项目负责人,则不允许创建
+            if (leader.id !== cached_data.user_id && !await is_project_privileged_account(cached_data.user_id)) {
+                throw Resp.gen_err(Resp.Forbidden, '非特权账号只能创建项目负责人是自己的项目!');
+            }
+
             let checkers: IHandler[] = [];
             for (let checker of data.checker_ids) {
                 checkers.push({
@@ -103,23 +112,24 @@ function add(json: IRequest, params: IMethodParams, cached_data: ICachedData): P
 
             }, {transaction: t});
 
+
             // 启动工作流
             let bpmn = await BpmnModel.findOne({where: {id: 'prj'}, transaction: t});
             if (!bpmn) throw Resp.gen_err(Resp.InternalServerError, '项目流程模型数据缺失!');
-            let flow = new FlowEngine(bpmn.id, bpmn.content, {prj_id: id, owner: cached_data.user_id}, bpmn_flow_on_end);
+            let flow = new FlowEngine(bpmn.id, bpmn.content, {prj_id: id, owner: data.leader_id}, bpmn_flow_on_end);
 
             await BpmnCase.create({
                 id: flow.id,
                 model_id: bpmn.id,
                 model: bpmn.content,
                 started_at: dayjs(),
-                creator_id: cached_data.user_id,
+                creator_id: data.leader_id,
                 prj_id: id,
                 // state: await flow.state(),
             }, {transaction: t});
             await PrjLogger.created({
-                creator_id: cached_data.user_id,
-                creator_name: cached_data.user_name,
+                creator_id: data.leader_id,
+                creator_name: leader.name,
                 content: `项目`,
                 prj_id: id,
                 t: t as Transaction

+ 1 - 2
pmr-biz-manager/src/routes/api/prj/info/get_list.ts

@@ -1,9 +1,8 @@
 import {IApiProcessor, ICachedData, IMethodParams, IRequest} from "@core/Defined";
 import {Resp} from "@util/Resp";
-import {Op, QueryTypes, WhereOptions} from "sequelize";
+import {WhereOptions} from "sequelize";
 import DataCURD, {ISQLReplacements} from "@core/DataCURD";
 import {BizCustomer} from "@core-models/BizCustomer";
-import {BizCustomerLevel} from "@core-models/BizCustomerLevel";
 import {BizCustomerIndustry} from "@core-models/BizCustomerIndustry";
 import {AcsUserInfo} from "@core-models/AcsUserInfo";
 import {AcsDomain} from "@core-models/AcsDomain";

+ 105 - 11
pmr-biz-manager/src/routes/api/prj/info/modify.ts

@@ -1,6 +1,6 @@
 import {IApiProcessor, ICachedData, IMethodParams, IRequest} from "@core/Defined";
 import DataCURD from "@core/DataCURD";
-import {Op, WhereOptions} from "sequelize";
+import {Op, QueryTypes, WhereOptions} from "sequelize";
 import {PrjInfo} from "@core-models/PrjInfo";
 import dayjs from "dayjs";
 import {PrjLogger} from "@src/utils/prj_logger";
@@ -21,6 +21,8 @@ import {BpmnWork} from "@core-models/BpmnWork";
 import {FlowEngine} from "@src/bpmn/flow_engine";
 import {PrjPhaseDefine} from "@core-models/PrjPhaseDefine";
 import {WorkItemStatus} from "@src/utils/define";
+import {BizCustomer} from "@core-models/BizCustomer";
+import {AcsDomain} from "@core-models/AcsDomain";
 
 interface IData {
     /**
@@ -73,11 +75,13 @@ function permission_guard(content: IRequest, cached_data: ICachedData): Promise<
             return reject(Resp.gen_err(Resp.ResourceNotFound, '项目不存在。'));
         let phase = await PrjPhaseDefine.findOne({where: {id: prj.phase_id}, raw: true});
         if (!phase) return reject(Resp.gen_err(Resp.ResourceNotFound, '项目状态信息出错,id: ' + prj.id));
+        // 缓存项目阶段的order_index
+        cached_data.phase_order_index = phase.order_index;
 
         let is_privileged_account = await is_project_privileged_account(cached_data.user_id);
 
-        if (phase.order_index >= 20 ) {
-            if (data.type_id !== undefined && prj.type_id !== data.type_id)  {
+        if (phase.order_index >= 20) {
+            if (data.type_id !== undefined && prj.type_id !== data.type_id) {
                 return reject(Resp.gen_err(Resp.Forbidden, '项目立项后不允许修改项目类型。'))
             }
             if (!is_privileged_account) {
@@ -89,17 +93,17 @@ function permission_guard(content: IRequest, cached_data: ICachedData): Promise<
         /// 只有项目特权人员才可以修改项目负责人
         if (data.leader_id && data.leader_id !== prj.leader_id) {
             if (!is_privileged_account) {
-                return  reject(Resp.gen_err(Resp.Forbidden, '您没有权限修改项目负责人,请确认您是特权人员。'));
+                return reject(Resp.gen_err(Resp.Forbidden, '您没有权限修改项目负责人,请确认您是特权人员。'));
             }
             // 新的项目负责人必须是项目组成员
             if (!await is_project_member(data.leader_id, prj.id)) {
-                return  reject(Resp.gen_err(Resp.Forbidden, '新的项目负责人必须是项目组成员。'));
+                return reject(Resp.gen_err(Resp.Forbidden, '新的项目负责人必须是项目组成员。'));
             }
         }
         if (!await is_project_modifiable(cached_data.user_id, prj.id))
             return reject(Resp.gen_err(Resp.Forbidden, '您没有权限修改本项目的信息,请确认您是项目负责人或特权人员。'));
         if (data.deliver_at) {
-            data.deliver_at += ' 23:59:59';
+            data.deliver_at = dayjs(data.deliver_at).format('YYYY-MM-DD 23:59:59');
         }
         let task = await PrjPlanTask.findOne({where: {prj_id: prj.id, end_at: {[Op.gt]: data.deliver_at}}, raw: true});
         if (task) {
@@ -130,7 +134,7 @@ async function before_respond(content: IRequest, params: IMethodParams, cached_d
     await PrjLogger.updated({
         creator_id: cached_data.user_id,
         creator_name: cached_data.user_name,
-        content: '项目信息',
+        content: cached_data.modified_info,
         prj_id: data.id
     });
     return result;
@@ -139,7 +143,7 @@ async function before_respond(content: IRequest, params: IMethodParams, cached_d
 function modify(content: IRequest, params: IMethodParams, cached_data: ICachedData): Promise<any> {
     return new Promise<any>(async (resolve, reject) => {
         let data = <IData>content.data;
-
+        let modified = '';
         let t = await PrjInfo.sequelize!.transaction();
         try {
             let prj = await PrjInfo.findOne({where: {id: data.id}, raw: true, transaction: t});
@@ -155,15 +159,25 @@ function modify(content: IRequest, params: IMethodParams, cached_data: ICachedDa
                     throw Resp.gen_err(Resp.ResourceNotFound, '新选中的项目负责人不存在.');
                 }
                 let [_count, flow_cases] = await BpmnCase.update({creator_id: data.leader_id},
-                    {where: {prj_id: prj.id, task_id: {[Op.is]: null}, completed_at: {[Op.is]: null}}, transaction: t, returning: true});
+                    {
+                        where: {prj_id: prj.id, task_id: {[Op.is]: null}, completed_at: {[Op.is]: null}},
+                        transaction: t,
+                        returning: true
+                    });
                 await BpmnWork.update({assigned_to: data.leader_id},
-                    {where: {prj_id: prj.id, task_id: {[Op.is]: null}, status: {[Op.lt]: WorkItemStatus.completed}}, transaction: t, returning: false});
+                    {
+                        where: {prj_id: prj.id, task_id: {[Op.is]: null}, status: {[Op.lt]: WorkItemStatus.completed}},
+                        transaction: t,
+                        returning: false
+                    });
                 if (flow_cases && flow_cases.length > 0) {
                     for (let flow_case of flow_cases) {
                         let engine = FlowEngine.case_engine_map.get(flow_case.id);
                         if (engine) engine.owner = data.leader_id;
                     }
                 }
+                let old_leader = await AcsUserInfo.findOne({where: {id: prj.leader_id}, raw: true, transaction: t});
+                modified += PrjLogger.gen_modified_content('项目负责人', old_leader!.name, new_leader.name);
             }
 
             let contract_id = prj.contract_id;
@@ -211,7 +225,87 @@ function modify(content: IRequest, params: IMethodParams, cached_data: ICachedDa
                         size: contract.pdf_size
                     }, {transaction: t});
                 }
+                let old_contract = await BizContractInfo.findOne({
+                    where: {id: data.contract_id},
+                    raw: true,
+                    transaction: t
+                });
+                modified += PrjLogger.gen_modified_content('项目合同', old_contract ? old_contract.name : '无', contract.name);
+            }
+
+            //如果项目已立项,要详细记录变化内容,立项前的项目信息修改不用记录
+            if (cached_data.phase_order_index >= 20) {
+                let sql = `
+                select prj.id, prj.name, 
+                    prj.type_id, CONCAT_WS('/', pidtype.name, ptype.name) as type_name,
+                    prj.region_id, region.name as region_name,
+                    prj.leader_id, leader.name as leader_name,
+                    prj.bizman_id, bizman.name as bizman_name,
+                    prj.customer_id, customer.name as customer_name, 
+                    prj.region_id, region.name as region_name,
+                    prj.contract_id, contract.name as contract_name,
+                    prj.intro,
+                    TO_CHAR(prj.deliver_at, 'yyyy-MM-dd') as deliver_at
+                from ${PrjInfo.table_name} prj
+                left join ${AcsDomain.table_name} ptype on prj.type_id = ptype.id
+                left join ${AcsDomain.table_name} pidtype on ptype.pid = pidtype.id
+                left join ${AcsUserInfo.table_name} bizman on bizman.id = prj.bizman_id
+                left join ${AcsUserInfo.table_name} leader on leader.id = prj.leader_id
+                left join ${BizCustomer.table_name} customer on customer.id = prj.customer_id
+                left join ${AcsDomain.table_name} region on prj.region_id = region.id
+                left join ${BizContractInfo.table_name} contract on contract.id = prj.contract_id
+                where prj.id = :prj_id
+            `;
+                let record: any = await PrjInfo.sequelize!.query(sql, {
+                    type: QueryTypes.SELECT,
+                    raw: true,
+                    transaction: t,
+                    replacements: {prj_id: data.id}
+                });
+                if (!record || record.length === 0) {
+                    throw Resp.gen_err(Resp.ResourceNotFound, '项目已被其他用户删除。');
+                }
+                let old_info = record[0];
+                modified += PrjLogger.gen_modified_content('项目名称', old_info.name, data.name);
+                if (data.type_id && data.type_id !== prj.type_id) {
+                    sql = `
+                    select CONCAT_WS('/', pidtype.name, ptype.name) as type_name
+                    from ${AcsDomain.table_name} ptype
+                    left join ${AcsDomain.table_name} pidtype on ptype.pid = pidtype.id
+                    where ptype.id = :type_id
+                `
+                    let type_name: any = await PrjInfo.sequelize!.query(sql, {
+                        type: QueryTypes.SELECT,
+                        raw: true,
+                        transaction: t,
+                        replacements: {type_id: data.type_id}
+                    });
+                    modified += PrjLogger.gen_modified_content('项目类型', old_info.type_name, type_name ? type_name[0].type_name : undefined);
+                }
+                if (data.customer_id && data.customer_id !== prj.customer_id) {
+                    let customer = await BizCustomer.findOne({where: {id: data.customer_id}, transaction: t});
+                    modified += PrjLogger.gen_modified_content('客户', old_info.customer_name, customer ? customer.name : undefined);
+                }
+                if (data.region_id && data.region_id !== prj.region_id) {
+                    let region = await AcsDomain.findOne({where: {id: data.region_id}, transaction: t});
+                    modified += PrjLogger.gen_modified_content('项目区域', old_info.region_name, region ? region.name : undefined);
+                }
+                if (data.bizman_id && data.bizman_id !== prj.bizman_id) {
+                    let bizman = await AcsUserInfo.findOne({where: {id: data.bizman_id}, transaction: t});
+                    modified += PrjLogger.gen_modified_content('商务负责人', old_info.bizman_name, bizman ? bizman.name : undefined);
+                }
+                if (data.intro && data.intro !== prj.intro) {
+                    modified += PrjLogger.gen_modified_content('项目简介', old_info.intro, data.intro);
+                }
+                if (data.deliver_at && data.deliver_at !== old_info.deliver_at) {
+                    modified += PrjLogger.gen_modified_content('交付日期', old_info.deliver_at, dayjs(data.deliver_at).format('YYYY-MM-DD'));
+                }
+
+                cached_data.modified_info = modified;
+            } else {
+                cached_data.modified_info = '';
             }
+
             let result = await DataCURD.internal_update_row(content, {
                 model: PrjInfo,
                 where: get_where,
@@ -334,7 +428,7 @@ const v1_0: IApiProcessor = {
             "token"
         ]
     },
-    method: modify,//DataCURD.internal_update_row,
+    method: modify,
     on_before_respond: before_respond,
     method_params: {
         model: PrjInfo,

+ 4 - 4
pmr-biz-manager/src/routes/api/prj/plan/modify_task.ts

@@ -86,10 +86,10 @@ function guard(json: IRequest, cached_data: ICachedData): Promise<void> {
         cached_data.started_at = prj_info.started_at;   // 缓存项目的开始时间
         cached_data.deliver_at = prj_info.deliver_at; // 缓存项目的交付时间
 
-        // 检查用户是否是项目负责人,只有项目负责人才可以修改任务
-        if (cached_data.user_id !== prj_info.leader_id) {
-            return reject(Resp.gen_err(Resp.Forbidden, '您不是项目负责人,不能修改任务。'));
-        }
+        // // 检查用户是否是项目负责人,只有项目负责人才可以修改任务
+        // if (cached_data.user_id !== prj_info.leader_id) {
+        //     return reject(Resp.gen_err(Resp.Forbidden, '您不是项目负责人,不能修改任务。'));
+        // }
         if (task.status === TaskStatus.completed || task.status === TaskStatus.cancelled) {
             return reject(Resp.gen_err(Resp.InvalidFlow, '任务已完成或已取消,不允许修改。'));
         }

+ 5 - 1
pmr-biz-manager/src/routes/api/prj/week_report/get_list.ts

@@ -28,7 +28,7 @@ interface IData {
     /**
      * 精确通过项目查找合同
      */
-    prj_id: string;
+    prj_id?: string;
     /**
      * 根据项目名称过滤(模糊匹配)
      */
@@ -79,6 +79,10 @@ function get_list(json: IRequest, params: IMethodParams, cached_data: ICachedDat
                 replacements.reporter_id = cached_data.user_id;
             }
 
+            if (data.prj_id !== undefined) {
+                condition += ` and prj.id = :prj_id `;
+                replacements.prj_id = data.prj_id;
+            }
             if (data.prj_name !== undefined) {
                 condition += ` and prj.name like :keyword `;
                 replacements.keyword = `%${data.prj_name}%`;

+ 10 - 1
pmr-biz-manager/src/utils/prj_logger.ts

@@ -43,7 +43,7 @@ export class PrjLogger {
     static async updated(info: IPrjLogInfo) {
         /// 以修改了: <旧内容>-><新内容>的格式生成日志内容
         if (info.old_content) {
-            info.content = `修改了: <span style="color: red;">${info.old_content}</span>-><span style="color: rgb(22,93,255);">${info.content}</span>`;
+            info.content = `修改了: <span style="color: red;">${info.old_content}</span>-><span style="color: rgb(20, 201, 201);">${info.content}</span>`;
         } else {
             info.content = `修改了: ${info.content}`;
         }
@@ -55,4 +55,13 @@ export class PrjLogger {
         info.content = `状态变更为: <span style="color: rgb(22,93,255);">${info.content}</span>`;
         await PrjLogger.log(info);
     }
+
+    static gen_modified_content(name: string, old_value: string, new_value?: string): string {
+        if (!new_value) return '';
+        if (old_value === new_value) return '';
+        // 如果字符长度超过32字节,则只显示前32字节,加上...
+        if (old_value.length > 32) old_value = old_value.substring(0, 32) + '...';
+        if (new_value.length > 32) new_value = new_value.substring(0, 32) + '...';
+        return `<b>${name}</b><del><span style="color: red;">${old_value}</span></del>-&gt;<span style="color: rgb(20, 201, 201);">${new_value}</span>`;
+    }
 }