diff --git a/Uft30ChangeCode.pro b/Uft30ChangeCode.pro
index 301ba0f..d193445 100644
--- a/Uft30ChangeCode.pro
+++ b/Uft30ChangeCode.pro
@@ -15,6 +15,7 @@ SOURCES += main.cpp \
src/pages/functionsearch/functionsearchpagebase.cpp \
src/pages/functionsearch/uf20functionsearchpage.cpp \
src/pages/functionsearch/uft3functionsearchpage.cpp \
+ src/pages/functionsearch/tablesearchpage.cpp \
src/components/elasearchedit.cpp \
src/components/interdialog.cpp \
src/pages/settings/settingspage.cpp \
@@ -60,6 +61,7 @@ HEADERS += src/mainwindow/mainwindow.h \
src/pages/functionsearch/functionsearchpagebase.h \
src/pages/functionsearch/uf20functionsearchpage.h \
src/pages/functionsearch/uft3functionsearchpage.h \
+ src/pages/functionsearch/tablesearchpage.h \
src/components/elasearchedit.h \
src/components/interdialog.h \
src/pages/settings/settingspage.h \
diff --git a/config/table_mapping.json b/config/table_mapping.json
new file mode 100644
index 0000000..894830f
--- /dev/null
+++ b/config/table_mapping.json
@@ -0,0 +1,812 @@
+{
+ "tableCompareUf2_Uf3": {
+ "accountcharacter": "account_character",
+ "accountconfig": "usps_account_config",
+ "accountdeploy": [
+ "upbs_account_deploy",
+ "uft_account_deploy"
+ ],
+ "acctassure": "ucrt_acct_assure_code",
+ "acctassurearg": "acct_assure_arg",
+ "acctassureresult": "acct_assure_result",
+ "acctassurerule": "acct_assure_rule",
+ "acctassurescale": [
+ "ucrt_acct_assure_scale",
+ "uarg_acct_assure_scale"
+ ],
+ "acctassurescalejour": "acct_assurescale_jour",
+ "acctcompactfloat": [
+ "ucrt_acct_compact_float",
+ "uarg_acct_compact_float"
+ ],
+ "acctensurescale": [
+ "ucrt_acct_ensure_scale",
+ "uarg_acct_ensure_scale"
+ ],
+ "acctfloatconcentratio": "acct_float_concentratio",
+ "acctspcash": "ucrt_acctspcash",
+ "acctstkconcentratio": "ucrt_acct_stk_concentrate",
+ "acctstkriskratio": "acct_stkrisk_ratio",
+ "acctststockquota": "uqms_ststock_quota_allocation",
+ "acctunderly": "ucrt_acct_underly_code",
+ "advcontract": "adviser_contract",
+ "advisercode": "adviser_code",
+ "adviserproduct": "adviser_product",
+ "afofagencyno": "afof_agencyno",
+ "afofbcrate": "afof_bcrate",
+ "afofbfare": "afof_bfare",
+ "afofcode": "afof_code",
+ "afofdiscount": "afof_discount",
+ "afofentrust": "afof_entrust",
+ "afofffare": "afof_ffare",
+ "afofquota": "afof_quota",
+ "afofrealtime": "afof_realtime",
+ "agreementctrl": "client_agreement",
+ "allbranch": "upbs_all_branch",
+ "allcompany": "upbs_all_company",
+ "allseats": "all_seats",
+ "arpacctarg": "arp_acct_arg",
+ "arpacctargjour": "arp_acct_arg_jour",
+ "arpapply": "arp_apply",
+ "arpapplyjour": "arp_apply_jour",
+ "arpassureratio": "ucbp_arp_assure_ratio",
+ "arpcode": "arp_code",
+ "arpcontract": "arp_contract",
+ "arpcontractjour": "arp_contract_jour",
+ "arpcrquota": "arp_crquota",
+ "arpcrquotajour": "arp_crquota_jour",
+ "arpequity": "arp_equity",
+ "arpequityjour": "arp_equity_jour",
+ "arpfare1": "arp_fare1",
+ "arpfare2": "arp_fare2",
+ "arpfixedrate": "arp_fixed_rate",
+ "arpfixedratejour": "arp_fixed_rate_jour",
+ "arpfunder": "arp_funder",
+ "arpkind": "arp_kind",
+ "arpmarratio": "arp_mar_ratio",
+ "arpquota": "arp_quota",
+ "arpregister": "arp_register",
+ "arpstkusedrate": "arp_stk_used_rate",
+ "assetbdstock": "ucrt_asset_bd_stock",
+ "assetbondjour": "bond_undue_quota",
+ "assetcltlimitstock": [
+ "client_limit_stock",
+ "usms_client_limit_stock"
+ ],
+ "assetimpawnstock": "uqms_impawn_stock",
+ "assetriskacctlist": "asset_risk_acct_list",
+ "assetriskalertlist": "asset_risk_alert_list",
+ "assetunfinished": "unfinished",
+ "assunderlyarg": "assunderly_arg",
+ "assunderlyloginfo": "assunderly_loginfo",
+ "assurecode": "ucrt_assure_code",
+ "assurecodecp": "assure_code_cp",
+ "assurecodeextinfo": "assure_code_extinfo",
+ "assurepackage": "assure_package",
+ "assureprodcode": "ucrt_assure_prod_code",
+ "assureratio": "ucrt_assure_ratio",
+ "assureriskdetail": "assure_risk_detail",
+ "assurescale": "ucrt_assure_scale",
+ "assurestock": "assure_stock",
+ "assurestockjour": "assure_stock_jour",
+ "authority": "usps_authority",
+ "backdebtquota": "debt_quota",
+ "badfineinterest": "bad_fine_interest",
+ "badfineinterestjour": "bad_fine_interest_jour",
+ "bcommmodel": "bcommmodel",
+ "bfare1": "usps_bfare1",
+ "bfare1seg": "bfare1seg",
+ "bfare2": "usps_bfare2",
+ "bfare2seg": "usps_bfare2seg",
+ "bidcontract": "bid_contract",
+ "blobfile": "ucbp_blob_file",
+ "blockquota": "usps_block_quota",
+ "blpcontract": "blp_contract",
+ "blpcontractext": "blp_contractext",
+ "bondbreachinfo": "bond_breach_info",
+ "bondfinrate": "bond_fin_rate",
+ "bondimpawnarg": "bond_impawn_arg",
+ "bondimpcashassure": "bond_imp_cash_assure",
+ "bondinvestorinfo": "usps_bondinvestorinfo",
+ "bondlevelinfo": "usps_bond_level",
+ "bondputback": "bond_put_back",
+ "bondrate": "usps_bond_rate",
+ "bondrateinf": "bond_rateinf",
+ "bondriskarg": "bond_risk_arg",
+ "bondtrusteeratio": "bond_trustee_ratio",
+ "bprentrusttriext": "bpr_entrust_tri_ext",
+ "bprinfo": "uqms_bpr_info",
+ "branchacctinfo": "upbs_branch_acct_info",
+ "branchprefix": "usps_branch_prefix",
+ "brpcontract": "brp_contract",
+ "brpcontractext": "brp_contract_ext",
+ "brpcontractextjour": "brp_contract_ext_jour",
+ "brpcontractjour": "brp_contract_jour",
+ "btbidbookinginfo": "usps_bt_bookinginfo",
+ "btoaspeacct": "btoa_speacct",
+ "bttransinfo": "bt_transinfo",
+ "busiblacklist": [
+ "ucrt_busi_blacklist",
+ "uses_busi_blacklist",
+ "uarg_busi_blacklist"
+ ],
+ "businflag": "upbs_business_flag",
+ "busiplatparam": "busi_plat_param",
+ "busiwhiteinfo": "business_white",
+ "busiwhitelist": "business_white_list",
+ "buypromise": "uses_buy_promise",
+ "cashaccount": [
+ "uqms_cash_account",
+ "ucrt_spcash_account"
+ ],
+ "cashfund": [
+ "uqms_cash_fund",
+ "ucrt_spcash_fund"
+ ],
+ "cashgroup": "cashgroup",
+ "cashquota": "ucrt_busiarg",
+ "cashstock": [
+ "uqms_cash_stock",
+ "ucrt_spcash_stock"
+ ],
+ "cbpentrust": [
+ "qrp_quota_total",
+ "ucbp_entrust"
+ ],
+ "cbpentrustbrpext": "brp_cbpentrust_ext",
+ "cbpentrustbtinqrspext": "cbp_entrust_bt_inqrsp_ext",
+ "cbpentrustbtinquiryext": "cbp_entrust_bt_inquiry_ext",
+ "cbpentrusttriext": "cbp_entrust_tri_ext",
+ "cbpetfentrustdetail": "ucbp_etf_entrust_detail",
+ "cbplimitsell": "ucbp_account_limit_stkcode",
+ "cbpprice": "usps_cbpprice",
+ "cbprealtime": "ucbp_realtime",
+ "cbpstock": "cbp_stock",
+ "cbsentrust": "cbs_entrust",
+ "cbsentrustext": "cbs_entrust_ext",
+ "cbsfundaccount": "uses_fund_account",
+ "cbsrealtime": "cbs_realtime",
+ "cbsstockholder": "uses_stock_holder",
+ "cbssubfundaccount": "uses_sub_fund_account",
+ "cdrfare": "usps_cdr_fare",
+ "cfare2": "usps_cfare2",
+ "cfare2seg": "usps_cfare2seg",
+ "chfare2": "chfare2",
+ "chfare2seg": "chfare2seg",
+ "client": [
+ "uact_client",
+ "usms_client",
+ "uopt_client"
+ ],
+ "clientbondlevel": "bond_account_level",
+ "clienteligmatchrel": "upbs_elig_match_rel",
+ "clientgradeinfo": "client_grade",
+ "clientindivfare": "client_indivfare",
+ "clientindivfarejour": "client_indivfare_jour",
+ "clientminfare": "client_min_fare",
+ "clientploy": "ucrt_client_ploy",
+ "clientprefer": "usms_client_prefer",
+ "clientpreferctrl": "client_prefer",
+ "clientsvrfarectrl": "usps_client_svrfare",
+ "clientsvrfarectrljour": "usps_client_svrfare_jour",
+ "cmcreditquota": "cm_credit_quota",
+ "coffare2": "usps_coffare2",
+ "coffare2seg": "usps_coffare2seg",
+ "combentrustdetail": "uopt_combentrust_detail",
+ "combholdreal": "uopt_combholdreal",
+ "combholdrealjour": "uopt_combholdreal_jour",
+ "commgroup": "commgroup",
+ "commmodelseg": "commmodelseg",
+ "compact": [
+ "ucrt_total_compact",
+ "ucrt_compact_repay_order",
+ "ucrt_compact",
+ "ucrt_split_compact"
+ ],
+ "compactapply": "ucrt_compact_apply",
+ "compactfare": "compactfare",
+ "compactfloat": "ucrt_compact_float",
+ "compactjour": "ucrt_compact_jour",
+ "compactprimerate": "ucrt_compact_primerate",
+ "compactrate": "ucrt_compact_rate",
+ "compactrealjour": "ucrt_compact_real_jour",
+ "confernolog": "confer_no_log",
+ "contract": "ucrt_contract",
+ "couponstkgroup": "coupon_stkgroup",
+ "coverstock": "uopt_covered_stock",
+ "coverstockjour": "uopt_coveredstock_jour",
+ "crdtacctextend": "ucrt_acct_extend",
+ "crdtacctrelationship": "crdt_acct_relationship",
+ "crdtaudit": "ucrt_compact_audit",
+ "crdtauthstkctrl": "ucrt_authority_stock",
+ "crdtblackcode": "ucrt_black_code",
+ "crdtblocktraderisk": "ucrt_blocktrade_risk",
+ "crdtbondputback": "ucrt_bond_putback",
+ "crdtbusiwhitelist": [
+ "ucrt_acct_busi_control",
+ "crdt_busi_whitelist"
+ ],
+ "crdtclientminfare": [
+ "crdt_client_min_fare",
+ "uarg_crt_client_minfare"
+ ],
+ "crdtclientprefer": "ucrt_client_prefer",
+ "crdtclientstrategyfare": "client_strategyfare",
+ "crdtclientstrategyfarejour": "client_strategyfare_jour",
+ "crdtclientsvrfare": [
+ "crdt_client_svr_fare",
+ "uarg_crt_client_svrfare"
+ ],
+ "crdtclientsvrfarejour": "crdt_client_svrfare_jour",
+ "crdtcontract": "ucrt_client_primerate",
+ "crdtcoupon": "crdt_coupon",
+ "crdtcouponjour": "crdt_coupon_jour",
+ "crdtdictate": "ucrt_dictate",
+ "crdtdynafairprice": "crdt_dyna_fairprice",
+ "crdtentrust": [
+ "ucrt_total_stk_entrust",
+ "ucrt_entrust",
+ "crtnentrust"
+ ],
+ "crdtentrustrepay": "ucrt_entrust_repay",
+ "crdtequity": "crdt_equity",
+ "crdtetfright": "ucrt_etf_right",
+ "crdtexemptriconbonds": [
+ "ucrt_bond_exemptricon",
+ "uarg_crt_bond_exemptricon"
+ ],
+ "crdtffarelog": "ucrt_ffare_log",
+ "crdtfiles": "crdt_files",
+ "crdtfundaccount": "ucrt_fund_account",
+ "crdtgroupconcentratio": "ucrt_stk_group_concentrate",
+ "crdtgroupconcwhite": "ucrt_stk_group_concwhite",
+ "crdtlevelrate": "ucrt_level_rate",
+ "crdtlimitsell": "ucrt_crdt_limit_sell",
+ "crdtofelecagreement": "ucrt_ofelecagreement",
+ "crdtoutasset": "ucrt_out_asset",
+ "crdtoutassetapply": "ucrt_out_asset_apply",
+ "crdtoutassetjour": "ucrt_out_asset_jour",
+ "crdtpersonalizedparam": [
+ "ucrt_stkcode_assure_scale",
+ "crdt_personalized_param"
+ ],
+ "crdtratiologinfo": "crdt_ratio_loginfo",
+ "crdtrealtime": "ucrt_realtime",
+ "crdtrestrictacct": "ucrt_acct_compact_audit",
+ "crdtrestrictcode": "ucrt_compact_audit_stk",
+ "crdtrisk": "ucrt_risk_parameter",
+ "crdtsettlejour": "crdt_settle_jour",
+ "crdtshare": "ucrt_share",
+ "crdtsloequityctrl": "ucrt_slo_equity",
+ "crdtstbipoaskresult": "ucrt_stb_ipoaskresult",
+ "crdtstkadfare": "crdtstkadfare",
+ "crdtstkholderuse": "ucrt_stock_holder",
+ "crdtstkresskacct": "ucrt_fundacct_stkrestrict",
+ "crdtstkrestrict": "ucrt_stkrestrict",
+ "crdtstockholder": "ucrt_stock_holder",
+ "crdtstockreal": "ucrt_stock_real",
+ "crdtstockrealjour": [
+ "uqms_cash_stock_jour",
+ "ucrt_stock_real_jour",
+ "ucrt_spcash_stock_jour"
+ ],
+ "crdtstockrealtotal": "ucrt_stock_real_total",
+ "crdtstockrealtotaljour": "ucrt_stock_real_total_jour",
+ "crdtsubequity": "ucrt_subequity",
+ "crdttimecontrol": "ucrt_time_control",
+ "crdtunfinished": "ucrt_unfinished",
+ "crdtunityvideo": "ucrt_unity_video",
+ "creditquota": "credit_quotas",
+ "csdcacodeacct": "csdc_acode_acct",
+ "csdcdicttrans": "upbs_csdc_dicttrans",
+ "cstbfare2": "cstbfare2",
+ "cstbfare2seg": "cstbfare2seg",
+ "dataswap": "ucbp_data_swap",
+ "debtinterest": "usps_debtinterest",
+ "delistcode": "stb_delist_code",
+ "dfare2": "usps_dfare2",
+ "dfare2seg": "usps_dfare2seg",
+ "dictentry": "upbs_dict_entry",
+ "discountmodel": "usps_discount_model",
+ "dividendtax": [
+ "ucbp_dividend_tax",
+ "usms_dividend_tax"
+ ],
+ "ebstkcode": "eb_stkcode",
+ "eligbusiarg": "upbs_elig_busi_arg",
+ "eligproduct": "upbs_elig_product",
+ "eligriskmatch": "upbs_elig_risk_match",
+ "eligvideoarg": "upbs_elig_video_arg",
+ "entrust": "uses_entrust",
+ "entrustwayconv": "usps_entrustway_conv",
+ "errormsg": "upbs_error_msg",
+ "etbbondcode": "etb_bond_code",
+ "etbbondprice": "etb_bond_price",
+ "etfcashbalanceinfo": "etf_cash_balanceinfo",
+ "etfcode": "usps_etf_code",
+ "etfcomponent": "usps_etf_component",
+ "etfdsfquotaarg": "etf_dsf_quota_arg",
+ "etfdsfquotaargjour": "etf_dsf_quota_arg_jour",
+ "etfdsfsharequota": "etf_dsf_share_quota",
+ "etfdsfsharequotajour": "etf_dsf_share_quota_jour",
+ "etfentrustdetail": "etf_entrust_detail",
+ "etfexchdate": "usps_etf_exchdate",
+ "etfrealdsfhzinfo": "etfreal_dsf_hzinfo",
+ "etfright": "etf_right",
+ "etfrightctrl": "etf_right",
+ "etfstock": "uses_stock_real",
+ "etfufundentrust": "etf_ufund_entrust",
+ "etfufundinfo": "etf_ufund",
+ "etfustockentrust": "etf_ustock_entrust",
+ "etfustockinfo": "etf_ustock",
+ "exchangedate": "upbs_init_date_model",
+ "exchangerate": "exchange_rates",
+ "exchangetime": "usps_exchange_time",
+ "excharg": "usps_exch_arg",
+ "exchboardacctdaydata": "exch_boardacct_day_data",
+ "exitstkcode": "usps_exit_stkcode",
+ "exitstock": "usms_exit_stock",
+ "externerror": "upbs_extern_error",
+ "extfarelog": [
+ "ucrt_ext_fare_log",
+ "uses_ext_fare_log"
+ ],
+ "extstockreal": "uses_extstock_real",
+ "extstockrealjour": "uses_extstock_real_jour",
+ "fareconsult": "usps_fare_consult",
+ "farefundacct": "usps_farefundacct",
+ "faregroup": "usps_faregroup",
+ "faresort": "usps_faresort",
+ "farestandard": "usps_farestandard",
+ "ffare": "usps_ffare",
+ "ffarelog": "uses_ffare_log",
+ "finexeapply": "finexe_apply",
+ "finexeapplyjour": "finexe_apply_jour",
+ "finexeassurecode": "finexe_assure_code",
+ "finexeclarg": "finexe_clarg",
+ "finexecode": "finexe_code",
+ "finexecontract": "finexe_contract",
+ "finexecontractjour": "finexe_contract_jour",
+ "finexedictate": "finexe_dictate",
+ "finexeoutimpawn": "finexe_out_impawn",
+ "finexepreassure": "finexe_preassure",
+ "finexepreprodrate": "finexe_preprodrate",
+ "finexeprodrate": "finexe_prodrate",
+ "finexequota": "finexe_quota",
+ "finexestkquota": "finexe_stk_quota",
+ "finmaininfo": "fin_main_info",
+ "firmoffercode": "firm_offer_code",
+ "fixedpriceparams": "usps_fixed_price_params",
+ "fjystkinfo": "usps_fjy_stkinfo",
+ "followdaterate": "followdate_rate",
+ "frozendetail": "frozen_detail",
+ "fundaccount": [
+ "usps_acct_assinfo",
+ "usms_fund_account",
+ "uopt_fund_account"
+ ],
+ "fundaccount_sleep": "fund_account_sleep",
+ "fundaccountctrl": "uses_fund_account",
+ "fundacctcomm": "fundacctcomm",
+ "fundbondimpawnarg": "fund_bond_impawn_arg",
+ "fundbusinsettlejour": "fund_busin_settle_jour",
+ "fundjour": "uses_fund_changeinfo",
+ "fundreal": [
+ "ucrt_fund_real",
+ "ucrt_fund_detail",
+ "uses_fund_real",
+ "uses_fund_detail",
+ "uopt_fundreal"
+ ],
+ "fundrealjour": [
+ "uqms_cash_fund_jour",
+ "ucrt_fund_real_jour",
+ "ucrt_spcash_fund_jour",
+ "uses_fund_real_jour",
+ "uopt_fundreal_jour"
+ ],
+ "fundrealtotal": "ucrt_fund_real_total",
+ "fundrealtotaljour": "ucrt_fund_real_total_jour",
+ "fundrevertjour": [
+ "uqms_fund_revert_jour",
+ "fund_revert_jour",
+ "ucrt_fund_revert_jour"
+ ],
+ "fundxjour": "fund_x_jour",
+ "gemaccinfo": "usms_gem_acct_info",
+ "gmcreditquota": "gm_credit_quota",
+ "gradeimpawnrate": "grade_impawn_rate",
+ "hcommgroup": "hcommgroup",
+ "hcommmodel": "hcommmodel",
+ "hfare1": "usps_hfare1",
+ "hfare1seg": "hfare1seg",
+ "hfare2": "hfare2",
+ "hfare2seg": "hfare2seg",
+ "hkquota": "usps_hkquota",
+ "hkvotelist": "hkvote_list",
+ "hkvotestock": "hkvote_stock",
+ "hsfunction": "upbs_hs_function",
+ "hstkcodearg": "hstkcode_arg",
+ "hstockclientinfo": "hstock_client_info",
+ "hstockquotanew": "hstock_quota_new",
+ "hstockquotanewjour": "hstock_quota_new_jour",
+ "hstockquotarel": "hstock_quota_rel",
+ "hstockquotareljour": "hstock_quota_rel_jour",
+ "icsetfprodinfo": "ics_etf_prod_info",
+ "icsfarekind": "ics_farekind",
+ "icsincludefare1model": "ics_includefare1_model",
+ "icsprodfareratio": "ics_prodfare_ratio",
+ "impawnstock": "impawn_stock",
+ "incomeaccttraderrel": "income_accttraderrel",
+ "incomeagency": "usps_income_agency",
+ "incomepriinfo": "income_price_info",
+ "incomepriinfoext": "income_price_info_ext",
+ "incomeproduct": "income_product",
+ "incomepubinfo": "income_pub_info",
+ "incomesaler": "usps_income_saler",
+ "incomesalerrpt": "income_saler_declare",
+ "incomestkcode": "usps_income_stkcode",
+ "incounfininfo": "inco_unfin_info",
+ "incounfininfoext": "inco_unfin_info_ext",
+ "indexcode": "indexcode",
+ "indexcomponent": "index_component",
+ "indexconfig": "index_config",
+ "indivfare": "indivfare",
+ "initconfig": "pbs_init_config",
+ "intentacct": "act_intent_account",
+ "intentarg": "intent_arg",
+ "intentorgan": "intent_organ",
+ "iposeatcontrast": "usps_ipo_seat_contrast",
+ "irregularacct": "irregular_acct",
+ "issuemainacoderel": "issue_mainacoderel",
+ "issuemaingrade": "issue_main_level",
+ "issuemainrelinfo": "issue_main_relinfo",
+ "judifrozenjour": "judifrozenjour",
+ "licenseinfo": "upbs_license_info",
+ "limitclient": "limit_client",
+ "limitsellstkcode": "limit_sell_stkcode",
+ "limitsellstock": "limit_sell_stock",
+ "limitstockprice": "limit_stock_price",
+ "loginpbu": "usps_login_pbu",
+ "monitoracct": "monitor_acct",
+ "noaccountjour": "usms_noaccount_jour",
+ "nosettledate": "nosettle_date",
+ "nsriskinfo": "usps_nsrisk_info",
+ "ofcasharg": "of_cash_arg",
+ "ofcode": "usps_ofcode",
+ "ofcommmodel": "ofcommmodel",
+ "ofelecagreementctrl": "of_client_agreement",
+ "ofexcharg": "usps_of_exch_arg",
+ "offare2": "usps_offare2",
+ "offare2seg": "usps_offare2seg",
+ "ofgradectrl": "ofgradectrl",
+ "optacctholdlimit": "uopt_acct_holdlimit",
+ "optacctmarginfloat": "uopt_acct_marginfloat",
+ "optaidentrust": "uopt_entrustaid",
+ "optauthority": "uopt_authority",
+ "optbillcheck": "uopt_bill_check",
+ "optblackacctctrl": "uopt_black_acctctrl",
+ "optbusiconfig": "uopt_busiconfig",
+ "optbusiwhitelist": "uopt_busiwhitelist",
+ "optcashsettlement": "uopt_cash_settlement",
+ "optclientprefer": "uopt_client_prefer",
+ "optcmpacctinfo": "uopt_company_account",
+ "optcode": "uopt_code",
+ "optcombarg": "uopt_comb_arg",
+ "optentrust": "uopt_entrust",
+ "optexcharg": "uopt_excharg",
+ "optexchtime": "uopt_exchtime",
+ "optexeagreement": "uopt_exe_agreement",
+ "optexeassign": "uopt_exercise_assign",
+ "optexedeliver": "uopt_exercise_deliver",
+ "optfare1": "uopt_fare1",
+ "optfare2": "uopt_fare2",
+ "optfrozeninfo": "uopt_frozen_info",
+ "optholdlimit": "uopt_hold_limit",
+ "optholdreal": "uopt_holdreal",
+ "optholdrealassist": "uopt_holdreal_assist",
+ "optholdrealjour": "uopt_holdreal_jour",
+ "optloancompact": [
+ "optloancompact",
+ "uopt_loancompact"
+ ],
+ "optloancompactjour": "uopt_loancompact_jour",
+ "optmarginfloat": "uopt_margin_float",
+ "optnearfinalmarmodel": "uopt_nearfinalmarmodel",
+ "optpersonalityrisk": "uopt_personality_risk",
+ "optprice": "uopt_price",
+ "optrates": "uopt_rates",
+ "optrealtime": [
+ "uopt_realtime",
+ "uopt_realtime_sum"
+ ],
+ "optrisk": "uopt_risk",
+ "optstockholder": "uopt_stock_holder",
+ "opttradename": "uopt_trade_name",
+ "optunderlycode": "uopt_underly_code",
+ "packagetemplet": "package_template",
+ "payoffconfig": "payoff_config",
+ "payoffploy": [
+ "ucrt_ploy_template",
+ "uarg_client_ploy"
+ ],
+ "pendfare": [
+ "ucrt_pend_fare",
+ "ucrt_pend_fare_jour"
+ ],
+ "persecurate": "persecurate",
+ "persrpcode": "per_srp_code",
+ "persrpkind": "per_srp_kind",
+ "pertransbindparam": "usps_pertrans_bindparam",
+ "pledgestkinfo": "pledge_stkinfo",
+ "preassurecode": "pre_assure_code",
+ "prestockinfo": "pre_stock_info",
+ "preunderlycode": "pre_underly_code",
+ "prevotecode": "pre_vote_code",
+ "prevoteinfo": "pre_vote_info",
+ "price": "usps_price",
+ "primerateaudit": "ucrt_primerate_audit",
+ "primeratestr": "ucrt_primerate_str",
+ "productbasicinfo": "product_basic_info",
+ "productbasicinfoext": "product_basic_info_ext",
+ "promiseinfo": "usps_promise_info",
+ "qrpacctipwstock": "usms_qrpacct_ipwstock",
+ "qrpagentacct": [
+ "qrp_agent_acct",
+ "usms_qrp_agent_acct"
+ ],
+ "qrpagentacctjour": "usms_qrp_agent_acct_jour",
+ "qrpbusin": "qrp_busin",
+ "qrpbusinjour": "qrp_busin_jour",
+ "qrpcode": "usps_qrp_code",
+ "qrpcoderegister": "qrp_code_register",
+ "qrpcoderegisterjour": "qrp_code_register_jour",
+ "qrperrinfo": "qrp_errinfo",
+ "qrpmultdateinfo": "qrp_mult_date_info",
+ "qrpptrate": "qrp_ptrate",
+ "qrpquota": "qrp_quota",
+ "qrpquotajour": "qrp_quota_jour",
+ "qrpunduepawn": "qrp_undue_pawn",
+ "quotaaudit": "quota_audit",
+ "realdsfbusiness": "real_dsf_business",
+ "realdsfmxinfo": "real_dsf_mxinfo",
+ "realsqwhitelist": "realsq_white_list",
+ "realtime": "realtime",
+ "reitsexpandinfo": "reits_expand_info",
+ "reloadfundacct": "reload_fundacct",
+ "reportabstract": "report_abstract",
+ "reptriskaccount": "ucrt_risk_account",
+ "reptsdsjour": "usms_sdsjour",
+ "reserveaccount": "usps_reserve_account",
+ "riskacctlist": [
+ "ucrt_riskacct_list",
+ "uarg_riskacct_list"
+ ],
+ "riskcrdtlist": [
+ "ucrt_risk_list",
+ "uarg_risk_list"
+ ],
+ "rsfcontract": "rsf_contract",
+ "rsfquota": "rsf_quota",
+ "rtgssettinfo": "rtgs_sett_info",
+ "seats": "usps_seats",
+ "secuauthoritystock": "uses_authority_stock",
+ "secubondconcratioinfo": "bond_concratio",
+ "secubondinfo": "secu_bond_info",
+ "secubondrisk": "secu_bond_risk",
+ "secubusictrl": "secu_busi",
+ "secubusictrljour": "secu_busi_jour",
+ "secuexemptriconbonds": "bond_exemptricon",
+ "secufundrealtotal": "uses_fund_real_total",
+ "secufundrealtotaljour": "uses_fund_real_total_jour",
+ "secuipoinfo": "ucbp_ipoinfo",
+ "secuipoinfojour": "ucbp_ipoinfojour",
+ "secuofacctrel": "secu_ofacct_rel",
+ "securate": "usps_securate",
+ "securisk": "secu_risk",
+ "secuserialcounter": "secu_serial_counter",
+ "secusettlejour": "secu_settle_jour",
+ "secusubequity": "uses_subequity",
+ "secuunfinished": "secu_unfinished",
+ "secuvotejour": "vote_jour",
+ "selfholder": "self_holder",
+ "sellfrozenstock": [
+ "uref_sellfrozen_stock",
+ "uses_sell_frozen_stock"
+ ],
+ "shhkequity": "shhk_equity",
+ "shreduction": "uqms_shreduction",
+ "shrtgssettinfo": "sh_rtgs_sett_info",
+ "shsoptqryresult": "sh_sopt_qryresult",
+ "shstockdetail": "uqms_shstock_detail",
+ "slosellbalance": "ucrt_slo_sell_balance",
+ "slosellbalancejour": "ucrt_slo_sell_balance_jour",
+ "soptcode": "usps_sopt_code",
+ "soptentrust": "sopt_entrust",
+ "soptreg": [
+ "uqms_sopt_reg",
+ "sopt_reg"
+ ],
+ "soptregjour": "uqms_sopt_reg_jour",
+ "sopttax": "usps_sopt_tax",
+ "soptwindow": "sopt_window",
+ "spebusindate": "usps_spe_busin_date",
+ "specbranchacct": "spec_branch_acct",
+ "spectlfundlist": "spectl_fundlist",
+ "spreadtype": "usps_spread_type",
+ "srpacctarg": "srp_acct_arg",
+ "srpadvarg": "srp_adv_arg",
+ "srpadvreg": "srp_adv_reg",
+ "srpapply": "srp_apply",
+ "srpapplyjour": "srp_apply_jour",
+ "srpassure": "srp_assure",
+ "srpassurejour": "srp_assure_jour",
+ "srpattentionclient": "srp_attention_client",
+ "srpcode": "usps_srp_code",
+ "srpcompact": "srp_compact",
+ "srpcompactjour": "srp_compact_jour",
+ "srpcontract": "srp_contract",
+ "srpcontractjour": "srp_contract_jour",
+ "srpcontractrate": "srp_contract_rate",
+ "srpequity": "srp_equity",
+ "srpequityjour": "srp_equity_jour",
+ "srpfare2": "srp_fare2",
+ "srpfareseg": "srp_fare2_seg",
+ "srpfpreclear": "srp_fpre_clear",
+ "srpfunder": "srp_funder",
+ "srpfunderquota": "srp_funder_quota",
+ "srpintegralinfo": "srp_integral_info",
+ "srpkind": "ucbp_srp_kind",
+ "srpmarginmode": "srp_margin_mode",
+ "srpmarratio": "srp_mar_ratio",
+ "srpquota": "srp_quota",
+ "srprate": "usps_srp_rate",
+ "srpregister": "srp_register",
+ "srprisklist": "srp_risk_list",
+ "srpwhitelist": "srp_white_list",
+ "sstaccount": "usms_sst_account",
+ "standardfare": "usps_farestandard",
+ "stbarg": "stb_arg",
+ "stbbp": "stb_bp",
+ "stbdlsinfo": "stb_dlsinfo",
+ "stbentrust": "stb_entrust",
+ "stbfare": "stb_fare",
+ "stbfare2": "usps_stbfare2",
+ "stbfare2seg": "usps_stbfare2seg",
+ "stbipoaskresult": "stb_ipo_ask_result",
+ "stblaycode": "usps_stb_lay_code",
+ "stblaycodechange": "usps_stb_laycode_change",
+ "stbqqcode": "stb_qqcode",
+ "stbstdholder": "stb_stdholder",
+ "stbstkcode": "usps_stb_stkcode",
+ "stbstockctrl": "stb_resstock",
+ "stbstockdetail": "uqms_stbstock_detail",
+ "stbstockdetailjour": "uqms_stbstock_detail_jour",
+ "stkadfare": "usps_stkadfare",
+ "stkcode": "usps_stkcode",
+ "stkcodeext": "usps_stkcode_ext",
+ "stkconcentratio": "ucrt_stk_concentrate",
+ "stkmodel": "usps_stkmodel",
+ "stknoticeinfo": "usps_stknotice_info",
+ "stkresone": [
+ "ucrt_fundacct_stkrestrict",
+ "usms_crt_fundacct_stkrestrict"
+ ],
+ "stkresonectrl": "uses_fundacct_stkrestrict",
+ "stkresstkacct": "stkresstkacct",
+ "stkresstkacctctrl": "uses_fundacct_stkrestrict",
+ "stkrestrict": "stkrestrict",
+ "stkrestrictctrl": "uses_stkrestrict",
+ "stktradequota": "stktrade_quota",
+ "stktradeusedquota": "stktrade_quota_used",
+ "stktype": "usps_stktype",
+ "stock": "ucrt_stock_changeinfo",
+ "stockcodematch": "stock_code_match",
+ "stockfirm": "stock_firm",
+ "stockgroup": "ucrt_stk_group",
+ "stockholder": "usms_stock_holder",
+ "stockholder_sleep": "stock_holder_sleep",
+ "stockholderctrl": "uses_stock_holder",
+ "stocklockingjour": "stock_locking_jour",
+ "stocknet": "stock_net",
+ "stocknetjour": "stock_net_jour",
+ "stockreal": "uses_stock_real",
+ "stockrealjour": "uses_stock_real_jour",
+ "stockrealtotal": "uses_stock_real_total",
+ "stockrealtotaljour": "uses_stock_real_total_jour",
+ "stockrevertjour": [
+ "uqms_stock_revert_jour",
+ "uses_stock_revert_jour",
+ "ucrt_stock_revert_jour"
+ ],
+ "stockriskratio": "stockrisk_ratio",
+ "strategyfare": "strategyfare",
+ "strategyfarecontract": "strategyfare_contract",
+ "strategyfarecontractjour": "strategyfare_contract_jour",
+ "ststockquota": "uqms_ststock_quota_used",
+ "subfundaccount": "uses_sub_fund_account",
+ "surstock": "ucrt_surplus_stock",
+ "surstockjour": "ucrt_surplus_stock_jour",
+ "svrfare": "usps_svrfare",
+ "sysarg": "upbs_arg",
+ "sysconfig": [
+ "upbs_sysconfig",
+ "ucrt_sys_config",
+ "uses_sys_config",
+ "uopt_sys_config"
+ ],
+ "sysdeploy": "pbs_sysdeploy",
+ "sysdictionary": "upbs_dictionary",
+ "sysnode": "pbs_sysnode",
+ "sysnodedeploy": "pbs_sysnode_deploy",
+ "szhkequity": "szhk_equity",
+ "szreduction": "uqms_szreduction",
+ "szstockdetail": "uqms_szstock_detail",
+ "tprbasket": "tpr_basket",
+ "tprcontract": "tpr_contract",
+ "tprcontractext": [
+ "tpr_contract_ext",
+ "tpr_contractext"
+ ],
+ "tprcontractextjour": "tpr_contract_ext_jour",
+ "tprcontractjour": "tpr_contract_jour",
+ "tprimpawnstock": "tpr_impawn_stock",
+ "tprimpawnstockjour": "tpr_impawn_stock_jour",
+ "tprspacctcontrast": "tpr_sp_acct_contrast",
+ "tprtransinfo": "uqms_tpr_transinfo",
+ "tprtransinfoext": "uqms_tpr_transinfo_ext",
+ "tradename": "usps_trade_name",
+ "traderinvestorright": "usps_trader_investor_right",
+ "transferboard": "transfer_board",
+ "transferentrust": "ucrt_transfer_entrust",
+ "transferfare": [
+ "usps_transfer_fare",
+ "ucrt_transfer_fare",
+ "transferfare"
+ ],
+ "trasferboard": "transfer_board",
+ "trpttransinfo": "trpt_transinfo",
+ "uftaccountdeploy": "uopt_uft_account_deploy",
+ "uftassetbondjour": "uft_bond_undue_quota",
+ "uftassetimpawnstock": "uft_qms_impawn_stock",
+ "uftcbpstock": "uft_cbp_stock",
+ "uftconfig": "uft_config",
+ "uftetfstock": "uft_etf_stock",
+ "uftfundaccountsysno": "uft_fundaccount_sysno",
+ "uftfundnetreal": "uft_ses_fund_detail",
+ "uftfundreal": "uft_ses_fund_real",
+ "uftimpawnstock": "uft_impawn_stock",
+ "uftpendfare": "uft_pend_fare",
+ "uftshreduction": [
+ "uft_qms_szreduction",
+ "uft_qms_shreduction"
+ ],
+ "uftstocknet": "uft_ses_stock_net",
+ "uftstockreal": [
+ "uft_ses_stock_real",
+ "uft_ucrt_stock_real"
+ ],
+ "uftsubfundreal": "uft_ses_sub_fund_real",
+ "uftsurstock": "uft_surplus_stock",
+ "umt_back_acct": "umt_back_acct",
+ "umtquotajour": "umt_quota_jour",
+ "uncompact": "ucrt_uncompact",
+ "underlycode": "ucrt_underly_code",
+ "underlycodecp": "underly_code_cp",
+ "unityvideoctrl": "unity_video",
+ "unpreclear": "usms_unpreclear",
+ "upbs_act_rule": "upbs_acct_rule",
+ "upbs_asset_prop_deploy": "upbs_asset_prop_deploy",
+ "upbs_umtconfig": "upbs_umtconfig",
+ "useroperlog": "uarg_useroper_log",
+ "usersyncinfo": "uarg_usersyncinfo",
+ "usersyncinfobizarg": "uarg_usersyncinfobizarg",
+ "vipbranchprefix": "vipbranch_prefix",
+ "votecode": "usps_vote_code",
+ "voteinfo": "usps_vote_info",
+ "votelist": "usps_vote_list",
+ "voteresult": "vote_result",
+ "warrantcode": "usps_warrant_code",
+ "witcode": "usps_witcode"
+ }
+}
\ No newline at end of file
diff --git a/docs/使用手册.md b/docs/使用手册.md
index 618bb1c..fde935d 100644
--- a/docs/使用手册.md
+++ b/docs/使用手册.md
@@ -4,7 +4,7 @@
| --- | --- |
| 软件名称 | Uft30ChangeCode(UF20 → UFT3 转码工具) |
| 手册版本 | V1.6.0 |
-| 更新日期 | 2026-09-14 |
+| 更新日期 | 2026-10-02 |
| 适用对象 | 使用本工具进行代码转码的开发/维护人员 |
---
@@ -51,10 +51,14 @@ Uft30ChangeCode 是一款桌面端 UF20 → UFT3 代码转码工具,主要提
| 设置(底部) | 项目环境路径与主题配置 |
| 帮助 / 关于(底部) | 在线帮助、版本信息 |
+
+
## 5. 功能操作详解
### 5.1 元数据更新(首次使用必做)
+
+
**前提**:元数据更新按"设置"页配置的项目环境路径扫描源码,请先完成 §5.5 的路径配置。
**操作步骤:**
@@ -84,6 +88,8 @@ Uft30ChangeCode 是一款桌面端 UF20 → UFT3 代码转码工具,主要提
##### 5.2.1.1 全量转码
+
+
**说明**:全量转码仅支持 **LS_ 开头**的功能("内存交易"改造范围),按清单对功能进行整体转码。清单保存在 `cust.json` 的 `convert.dir` 节点(见 §6.1)。
**表格列**:`UF20函数名` | `是否存在` | `核心` | `是否柜面接口`(**双击函数名所在行**可在弹窗中修改函数名)。
@@ -105,6 +111,8 @@ Uft30ChangeCode 是一款桌面端 UF20 → UFT3 代码转码工具,主要提
##### 5.2.1.2 按需转码
+
+
**说明**:按需转码针对**非 LS 功能**转码,支持**单文件转码**与**递归转码**两种模式,转码范围不包含核心文件、module 模块文件。
- **非递归模式**:仅对目标文件本身执行转码,不会处理该文件内调用的函数;
@@ -132,6 +140,8 @@ Uft30ChangeCode 是一款桌面端 UF20 → UFT3 代码转码工具,主要提
#### 5.2.2 代码检索
+
+
**说明**:代码检索用于查看功能的**调用链路**——该功能调用了哪些下游函数、被哪些上游功能调用,用于转码前评估影响范围、转码后核对结果。
页面顶部 Tab 切换 **转码业务** / **非转码业务** 两种模式,两种模式各自维护检索路径与索引,互不干扰。
@@ -168,6 +178,8 @@ Uft30ChangeCode 是一款桌面端 UF20 → UFT3 代码转码工具,主要提
#### 5.3.1 UF20 功能查询
+
+
**操作步骤:**
1. 进入 **UF20功能查询** 页;
@@ -184,16 +196,42 @@ Uft30ChangeCode 是一款桌面端 UF20 → UFT3 代码转码工具,主要提
#### 5.3.2 UFT3 功能查询
+
+
- 界面与 UF20 功能查询一致(函数名 / 功能号检索、**批量关键词**、精确匹配),结果列为 `函数名`、`英文名`、`功能号`,查询提示同样在页面底部提示栏反馈;
- 仅用于查询核对 UFT3 功能元数据,**双击/右键不加入转码清单**;
- 代码检索中"原代码已支持"行前的功能号即来自 UFT3 元数据。
+#### 5.3.3 UF20 & 内存表映射查询
+
+
+
+检索 UF20(2.0)表与内存表之间的映射关系,结果四列:`UF20表名`、`UF20表中文名`、`内存表名`、`内存表中文名`;**双击结果行可添加映射到「数据映射 → 表替换」**。
+
+**操作步骤:**
+
+1. 进入 **功能查询 → UF20 & 内存表映射查询**;
+2. 输入 **UF20 表名或内存表名**(二者均可检索),回车或点搜索图标;
+ - **批量查询**:多个表名用**逗号、分号或空格**分隔(中英文均可),各关键词命中结果取并集展示;
+ - 搜索框内可切换**精确匹配**(默认模糊匹配,不区分大小写);
+3. 查看结果列表:
+ - 映射关系命中(表名在映射文件 `bin/uf2touft3/config/table_mapping.json` 中配置过)→ 四列完整展示,中文名分别取自 `uf20_cache.db` / `uft3_cache.db`;一个 UF20 表映射到多个内存表(value 为数组)时展示为多条记录;
+ - 无映射关系 → 仅展示数据库中能取到的一侧字段,映射相关列置空,底部提示注明无映射数量;
+4. **双击结果行**将该映射关系添加到「数据映射 → 表替换」:
+ - 仅当该行 UF20 表名与内存表名均非空(有完整映射关系)才可添加;
+ - 表替换中已存在该 UF20 表名时自动跳过,并在提示栏注明当前映射值(不覆盖);
+ - 添加成功后**自动保存**到 `cust.json`(提示栏回执"已添加并自动保存",无需再到表替换页手动保存),同时同步追加到映射文件 `table_mapping.json` 中缺失的条目,**再次查询立即可见**。
+
+**查询提示**(页面底部提示栏,不弹窗):默认提示"💡 输入 UF20 表名或内存表名进行查询,支持精确/模糊匹配;双击记录行可添加到「数据映射 → 表替换」";无命中提示"⚠️ 未找到匹配的表";缓存库无表数据提示"请先在「元数据更新」页面执行更新";**映射文件解析失败**(手工编辑 `table_mapping.json` 留下尾随逗号、缺引号等)提示"⚠️ 映射文件 table_mapping.json 解析失败(第 N 行附近)…"并停止查询(不再单侧展示,避免误判为映射丢失),按行号修正文件后重新查询即可;成功显示"已查询 X 条记录(其中 Y 个无映射关系,N 个表中文名缺失,M 个关键词无匹配)"——一对多映射按多条记录计;中文名缺失表示映射关系里有该表、但本地缓存库未收录(元数据更新未覆盖该表来源工程),需扩大元数据更新的工程范围。
+
### 5.4 数据映射
**说明**:数据映射维护 UF20 → UFT3 的**替换对照表**与**不转码清单**,转码引擎按映射替换代码中的表名 / 函数名,清单内的功能函数名则跳过替换。各页数据均保存在 `cust.json`(见 §6.1);界面修改后点 **保存** 写盘生效,外部编辑 `cust.json` 保存后界面自动刷新(程序自身保存后的 2 秒内暂停文件监控,此间的外部修改需再次保存文件才会被检测到)。
#### 5.4.1 表替换
+
+
维护 UF20 表名 → UFT3 表名的映射。
**操作步骤:**
@@ -203,14 +241,22 @@ Uft30ChangeCode 是一款桌面端 UF20 → UFT3 代码转码工具,主要提
3. **双击单元格**直接修改;
4. 点 **添加** 弹窗输入 UF20 / UFT3 表名(UF20 表名必填且不可重复);
5. **删除选中** 需二次确认(支持多选);
-6. 修改后点 **保存** 生效。
+6. 修改后点 **保存** 生效;保存成功后自动与映射文件 `bin/uf2touft3/config/table_mapping.json` 对账同步,「UF20 & 内存表映射查询」中**立即可查**:
+ - **新增**的映射自动追加到该文件;
+ - **修改一对一映射的内存表名**后,文件中同名条目的值同步更新;
+ - 该文件中配置为**一对多数组**的条目(一个 UF20 表对应多个内存表)保持不变,不会被表替换中的单值覆盖;
+ - 在表替换中**删除**条目暂不同步到该文件(映射文件是全量参考库,删除仅表示转码时不再使用该替换)。
#### 5.4.2 函数替换
-维护 UF20 函数名 → UFT3 函数名的映射,操作方式与表替换完全相同。
+
+
+维护 UF20 函数名 → UFT3 函数名的映射,操作方式与表替换相同,但**不涉及** `table_mapping.json` 自动同步(该同步为表替换独有)。
#### 5.4.3 不转码功能函数
+
+
维护**不参与函数名替换**的功能函数清单,对应 `cust.json` 顶层键 `noChangeFullNameCode`(字符串数组)。转码时,清单内的功能函数名保持原样,不做 UF20 → UFT3 全名替换。
**操作步骤:**
@@ -226,6 +272,8 @@ Uft30ChangeCode 是一款桌面端 UF20 → UFT3 代码转码工具,主要提
### 5.5 系统设置(底部"设置")
+
+
**环境配置**:
| 配置项 | 说明 |
@@ -264,7 +312,7 @@ Uft30ChangeCode 是一款桌面端 UF20 → UFT3 代码转码工具,主要提
| `functionReplace` | 对象 `{UF20函数名: UFT3函数名}` | 函数替换映射 |
| `noChangeFullNameCode` | 字符串数组 | 不转码功能函数清单,转码时跳过这些函数名的替换 |
-`convert.dir` 元素字段说明(详见 `docs/转码配置格式契约.md`):
+`convert.dir` 元素字段说明(详见 `docs/详细设计文档.md` §7.5):
| 字段 | 说明 |
| --- | --- |
diff --git a/docs/详细设计文档.md b/docs/详细设计文档.md
index 240d722..29bfdde 100644
--- a/docs/详细设计文档.md
+++ b/docs/详细设计文档.md
@@ -317,7 +317,7 @@ sequenceDiagram
**要点:**
-- **全量转码**:引擎读取 `cust.json` 的 `convert.dir` 清单(V2 对象数组 `{name, core, viaUmgr, dialect}`),对清单内 LS_ 功能整体转码,核心/柜面接口/SQL 方言随清单下发(契约见 `docs/转码配置格式契约.md`);
+- **全量转码**:引擎读取 `cust.json` 的 `convert.dir` 清单(V2 对象数组 `{name, core, viaUmgr, dialect}`),对清单内 LS_ 功能整体转码,核心/柜面接口/SQL 方言随清单下发(契约见 §7.5);
- **按需转码**:`NonLsConvertPage::buildArguments()` 组装引擎参数(含 `-m 2` 模式标识),功能清单、递归标志(depth)与 SQL 方言(dialect)由 `cust_nonls.json` 提供,引擎按映射关系(`cust.json`)完成表名/函数名替换;
- **成败判定**:引擎在个别文件转码失败时退出码仍可能为 0,因此 `MainWindow` 在输出解析链最前端检测 `FAIL` 行(`[N/M] FAIL 函数名`)与汇总行(`批量转码完成: X/Y 通过, Z 失败`)累计 `m_failCount`;收尾按 `正常退出 && 退出码==0 && m_failCount==0` 判定成功,失败时页面调用 `onConvertFailed`——进度条**保持当前进度**(不置 100%)、状态文字"转换失败(N 个文件转换失败)"(详见 §8.7);
- **转码后联动**:任务成功后自动静默刷新代码检索索引,保证调用链路反映最新代码。
@@ -461,7 +461,7 @@ flowchart TD
两种模式均**不包含核心文件、module 模块文件**。执行时 `buildArguments()` 组装引擎参数(含 `-m 2`),由 `MainWindow` 统一经 `PythonRunner` 启动引擎。
-### 5.4 功能查询模块(FunctionSearchPage 基类 + UF20/UFT3 子页)
+### 5.4 功能查询模块(FunctionSearchPage 基类 + UF20/UFT3 子页 + UF20 & 内存表映射查询页)
**职责**:检索 UF20 / UFT3 功能元数据(依赖 `DataCache`),列:`函数名 / 英文名 / 功能号`;支持函数名/功能号字段切换、精确匹配开关与**多关键词批量查询**。UF20 页结果表支持多选,双击/右键一键加入转码工作台;UFT3 页为纯查询(不接入转码链路)。
@@ -504,6 +504,63 @@ sequenceDiagram
> **UFT3 功能查询**:界面与 UF20 版一致(含批量关键词查询),仅用于核对 UFT3 元数据,**双击/右键不加入转码清单**;代码检索中"原代码已支持"行的功能号前缀即来自 UFT3 元数据。
+#### 5.4.3 UF20 & 内存表映射查询页(TableSearchPage)
+
+**职责**:检索 UF20(2.0)表与内存表(UFT3)之间的映射关系,四列结果:`UF20表名 / UF20表中文名 / 内存表名 / 内存表中文名`;**双击记录行可添加映射到「数据映射 → 表替换」页**。不继承 `FunctionSearchPageBase`(结果结构为 4 列表信息,与 `FunctionInfo` 3 列函数信息不同),UI 风格(边距/搜索框/结果表/主题感知 tip)与功能查询页对齐。
+
+**数据源三件套**:
+
+| 列 | 来源 |
+| --- | --- |
+| UF20表名 / 内存表名 | `bin/uf2touft3/config/table_mapping.json`(独立映射文件,顶层键 `tableCompareUf2_Uf3`:key=UF20表名,value=内存表名;value 可为字符串(一对一)或字符串数组(一对多,拆为多条映射对,一个 UF20 表展示多行)),每次查询现读(文件小) |
+| UF20表中文名 | `uf20_cache.db` 的 `uf2_table`:`data` JSON 的 `cname` 字段 |
+| 内存表中文名 | `uft3_cache.db` 的 `uft3_table`:`data` JSON 的 `chineseName` 字段 |
+
+**查询流程**(每关键词,支持多关键词 OR 并集,`splitKeywords` 与功能查询页同口径):
+
+1. **映射命中**(key 或 value)→ 完整行:以 key 取 UF20 中文名、以 value 取内存表中文名;一对多映射(value 为数组)拆为多条记录展示;
+2. **映射未命中** → 双库单侧探测:命中 `uf2_table` 出单侧行(映射两列置空),命中 `uft3_table` 同理,两侧同时命中出两行;
+3. **去重**:映射整体行已覆盖的表名不再补单侧行(covered 集合),同一映射对不被多关键词重复输出(seenPairs 集合);
+4. 精确匹配为全等;模糊匹配为不区分大小写的包含。
+
+**轻量数据访问**:两库 `data` 列为完整表定义 JSON(单行可达数十 KB),禁止整表搬运逐行解析——`DataCache` 新增 `getUf20TableNameMap()` / `getUft3TableNameMap()`,SQL 层 `json_extract(data, '$.cname' / '$.chineseName')` 只提取中文名,首次调用全量构建"表名→中文名"映射后**内存缓存**(懒加载,表数量远小于接口表,秒级内完成),元数据落库后随 `refreshFunctionCache()` 一并失效重建;构建失败不置有效、下次自动重试;`json_extract` 不可用时回退 `getAllRows` + C++ 逐行解析兜底。
+
+**tip 反馈**(页面底部,不弹窗):默认提示"💡 输入 UF20 表名或内存表名进行查询,支持精确/模糊匹配;双击记录行可添加到「数据映射 → 表替换」";空输入提示输入表名;双库均为空提示"请先在「元数据更新」页面执行更新";无命中提示"未找到匹配的表";成功显示"已查询 X 条记录(其中 Y 个无映射关系,N 个表中文名缺失,M 个关键词无匹配)"——一对多映射按多条记录计;中文名缺失指映射里有该表但缓存库未收录(元数据更新未覆盖其来源工程,如 `table_mapping.json` 覆盖完整标准套件而本地仅更新了部分工程);`table_mapping.json` 读取失败时附加降级说明(仅展示数据库检索结果)。
+
+#### 5.4.4 双击添加映射到表替换页
+
+查询结果行**双击**即可将映射关系添加到「数据映射 → 表替换」页,跨页信号链路与 UF20 功能查询一键加入转码工作台同构:
+
+```mermaid
+sequenceDiagram
+ participant U as 用户
+ participant S as TableSearchPage(映射查询)
+ participant M as MainWindow
+ participant T as TableMappingPage(表替换)
+
+ U->>S: 双击结果行
+ S->>S: 取第 1 列(UF20表名)/第 3 列(内存表名),
任一为空即无完整映射关系,tip 警告
+ S->>M: requestAddMapping(uf20Name, memName)
+ M->>T: appendPairIfAbsent(uf20Name, memName)
+ Note over T: trimmed 口径判重(与添加/收集同口径);
已存在返回提示(含当前映射值,不覆盖);
新增入模型置脏后立即落盘(自动保存)
+ M->>S: onMappingAddResult(error)
+ S->>U: tip 反馈:成功"已添加到表替换并自动保存" / 失败原因
+```
+
+**要点:**
+
+- **完整映射校验**:仅当该行 UF20 表名与内存表名均非空(即有完整映射关系)才可添加;单侧行(无映射)双击提示警告不入库;
+- **判重不覆盖**:UF20 表名在表替换中已存在时跳过,tip 注明当前映射值——与表替换页手动添加口径一致,避免隐式覆盖;
+- **自动保存语义**:`appendPairIfAbsent` 追加到模型并置 `m_dirty` 后立即调用 `saveToDisk()` 落盘(无弹窗,成功静默)——`onSave` 的弹窗壳与落盘核心分离,保存窗口(监控抑制)逻辑共用;自动保存失败时原因带回映射查询页 tip 展示;
+- **table_mapping.json 同步**:表替换页覆写基类落盘钩子 `onSavedToDisk()`——保存成功(含双击自动保存)后,以表替换模型为源与 `table_mapping.json` 的 `tableCompareUf2_Uf3` 对象逐成员对账:
+ - **新增**:模型中存在、tm 中缺失的 key(且 value 非空)按原缩进/换行格式追加到对象末尾;
+ - **修改**:tm 中为字符串值(一对一)且与模型 value 不同的条目就地更新为模型值;
+ - **一对多数组跳过**:tm 中 value 为数组的条目(41 个)一律保留原文不覆盖——表替换是单值模型,覆盖会丢失其余映射;跳过数计入同步日志;
+ - **删除暂不同步**:表替换删除条目仅作用于 cust.json(转码口径),tm 中同名条目保留(tm 是全量参考映射,含 328 个 cust 未维护的独有条目);空 value 也不同步;
+ - **实现要点**:成员级文本解析(key 串/冒号/value 串或跨行数组,括号配对且跳过字符串字面量,记录每个成员原文起止),未变更成员与数组保持原文字节不变,仅替换/追加成员行后按原缩进与换行重建对象内部;写前整体重解析校验,任何结构异常放弃写入并记错误日志;同步结果记 info 日志(新增 N,更新 M,一对多数组跳过 X,空值跳过 Y);
+ - 映射查询页每次查询现读该文件,同步后的新增/修改即时可查;
+- **页面持有**:`MainWindow` 新增 `m_tableMappingPage` 成员,页面创建 lambda 运行时解引用(构造时序上映射查询页先建、表替换页后建,两页构造完成后成员指针非空)。
+
### 5.5 代码检索模块(CodeSearchPage + CallChainAnalyzer)
**职责**:功能**调用链路查询**(非内容检索)——展示某功能的向上调用方(谁调用了它)与向下被调(它调用了谁),用于转码前评估影响范围、转码后核对结果。
@@ -626,7 +683,8 @@ flowchart TD
- **写闸门**:`saveDataset()` 全程持有 `m_writeGate` 写闸门;`clearCache()` 以 `tryLock` 探测在途落库任务——在途时仅重置内存状态并记录警告、跳过连接重置,避免关闭正在写入的连接(闸门在 `database()` 取连接之后获取,锁序安全无死锁,详见 §8.4);
- **内存功能索引**:`refreshFunctionCache()` 将两库功能表加载至内存(`FunctionInfo{cname, eName, functionNo}`),支撑功能查询页即时检索;启动时 `MainWindow` 经 `warmupFunctionCacheAsync(db)` **异步预热**(后台线程构建内存索引,幂等:已就绪/在途跳过,详见 §8.6);
- **检索 API**:`searchUF20Functions()` / `searchUFT3Functions()` 支持按中文名/英文名/功能号字段过滤,均有**单词与多词两个重载**——多词版(`QStringList` + `keywordsWithoutMatch` 出参)实现 OR 并集、按 `cname|functionNo` 去重与零命中统计,单词版内部委托多词版;两库共用匿名命名空间 `filterFunctionInfos()` 过滤实现——代码检索"按功能号检索"即经 `searchUFT3Functions(raw, "function_no", true)` 将功能号解析为函数英文名;
-- **数据集 API**:`datasetExists()` / `getAllRows()` / `getRowData()` 供功能查询页读取指定功能完整数据。
+- **数据集 API**:`datasetExists()` / `getAllRows()` / `getRowData()` 供功能查询页读取指定功能完整数据;
+- **表查询映射缓存**:`getUf20TableNameMap()` / `getUft3TableNameMap()` 返回"表名→中文名"全量轻量映射(SQL 层 `json_extract` 只取中文名字段,避免整表搬运大 JSON;`json_extract` 不可用时回退 `getAllRows` + 逐行解析),首次调用构建后内存缓存,随 `refreshFunctionCache()` 一并失效重建,供 UF20 & 内存表映射查询页使用(见 §5.4.3)。
### 6.2 LogManager(单例)
@@ -726,7 +784,7 @@ flowchart TD
| `tableCompareUf2_Uf3` | UF20→UFT3 表名映射 | 全量/按需转码的表替换 |
| `functionReplace` | 函数替换规则 | 转码的函数替换 |
| `noChangeFullNameCode` | 字符串数组(功能函数全名清单) | 转码引擎对清单内功能函数名不做全名替换 |
-| `convert.dir` | 对象数组 `{name, core, viaUmgr, dialect}`(全量转码清单,V2 格式,见 `docs/转码配置格式契约.md`) | 全量转码页维护、引擎读取 |
+| `convert.dir` | 对象数组 `{name, core, viaUmgr, dialect}`(全量转码清单,V2 格式,见 §7.5) | 全量转码页维护、引擎读取 |
| 其余键 | 引擎自身配置 | 原样透传 |
- 数据映射三页各自只读写所属顶层键(键名统一引用 `Constants::CustKeys`,见 §6.4),其余内容原样保留;
@@ -758,6 +816,140 @@ flowchart TD
| config.ini | `[projectPath]` | UFT30/UF20/UF20账户/清算项目环境路径(`uft30` / `uf20` / `ufAcct20` / `UftdbSett`)、转码生成路径;保存时同步维护 `uft30pub = \upub\dev_codes` |
| change_code.ini | `[log]` | 日志级别/开关,LogManager 启动时读取 |
+### 7.5 转码配置格式契约(cust.json / cust_nonls.json)
+
+> 版本:1.0(2026-09-16)
+> 变更发起方:转码工具 UI(Uft30ChangeCode,Qt 客户端)
+> 契约接收方:转码引擎(uf2touft3)
+> 状态:已评审确认,待引擎侧实现联调
+
+#### 1. 变更背景
+
+全量转码页新增【核心】【是否柜面接口】两列,按需转码页新增【db模式】列(支持 SQL 的 db/mdb 语法转换)。相应地,两份配置文件的 `convert.dir` 节点格式升级,引擎需按本契约完成解析与转码逻辑适配。
+
+#### 2. cust.json(全量转码清单)
+
+##### 2.1 格式变更
+
+```jsonc
+// 旧格式(V1):字符串数组
+"convert": {
+ "dir": [
+ "LS_证券参数_股票名单增加.service_design"
+ ],
+ "ext": [ ".service_design", ".function_design", ... ]
+}
+
+// 新格式(V2):对象数组
+"convert": {
+ "dir": [
+ {
+ "name": "LS_债券质押协议回购_初始交易委托.service_design",
+ "core": "ucbp",
+ "viaUmgr": true,
+ "dialect": "db"
+ }
+ ],
+ "ext": [ ".service_design", ".function_design", ... ]
+}
+```
+
+##### 2.2 字段定义
+
+| 字段 | 类型 | 必填 | 语义 | 合法取值 |
+|---|---|---|---|---|
+| `name` | string | 是 | 功能短名 + 设计器扩展名(后缀由 UI 按清单扩展名拼接,当前固定 `.service_design`) | 非空字符串 |
+| `core` | string | 是 | 目标核心 | `"ucbp"` \| `"uses"` \| `"uarg"` \| `"usms"` |
+| `viaUmgr` | bool | 是 | 是否柜面接口 | `true` \| `false` |
+| `dialect` | string | 是 | SQL 语法方言,由 `core` 派生,UI 不提供独立修改入口 | `"mdb"` \| `"db"` |
+
+**dialect 派生规则(UI 侧保证,引擎可据此校验):**
+
+| core | dialect |
+|---|---|
+| `ucbp` | `mdb` |
+| `uses` | `mdb` |
+| `uarg` | `db` |
+| `usms` | `db` |
+
+##### 2.3 引擎解析要求
+
+1. **双格式兼容**:`dir` 元素为字符串时按旧格式处理(无 core/viaUmgr/dialect,行为与现有引擎一致);为对象时按新格式处理。两者可混合出现的可能性极低,但解析器不应因此崩溃。
+2. **字段兜底建议**:对象缺字段时,建议按 UI 默认值兜底并记录警告日志——`core="ucbp"`、`viaUmgr=false`、`dialect` 按 2.2 派生表由 core 推导。
+3. **非法值处理**:`core` 不在枚举内、`dialect` 与派生规则矛盾时,记录警告并按 `dialect` 字段实际值执行(以文件为准,不做强制纠正),或按团队约定明确拒绝该条目——**请引擎侧反馈选定哪种策略**。
+4. `viaUmgr=true` 时柜面接口的转码行为差异由引擎侧定义并实现。
+
+#### 3. cust_nonls.json(按需转码清单)
+
+##### 3.1 格式变更
+
+```jsonc
+// 旧格式(V1):功能名 → 数值
+"convert": {
+ "dir": {
+ "*": -1,
+ "AS_证券日终(用户)_证券信息更新": 0
+ },
+ "ext": [ ".service_design", ... ]
+}
+
+// 新格式(V2):清单条目为对象;引擎兜底键 "*" 保持数值原样
+"convert": {
+ "dir": {
+ "AS_证券参数_交易风险提示证券代码批量导入": { "depth": 0, "dialect": "db" },
+ "*": -1
+ },
+ "ext": [ ".service_design", ... ]
+}
+```
+
+##### 3.2 字段定义
+
+| 字段 | 类型 | 必填 | 语义 | 合法取值 |
+|---|---|---|---|---|
+| `depth` | int | 是 | 递归深度标志,语义与旧格式数值完全一致:`0`=否(单文件转码)、`-1`=是(递归转码) | `0` \| `-1` |
+| `dialect` | string | 是 | SQL 语法方言,对应 UI【db模式】列:选"是"→`db`,选"否"→`mdb` | `"db"` \| `"mdb"` |
+
+##### 3.3 特殊键与解析要求
+
+1. **`"*"` 兜底键**:保持旧格式数值(`0`/`-1`),**不升级为对象、不携带 dialect**。引擎对非清单功能的默认行为不变,UI 侧原样透传、不展示、不修改。
+2. **双格式兼容**:`dir` 条目值为数值时按旧格式处理(dialect 默认 `mdb`);为对象时按新格式解析。
+3. **字段兜底建议**:对象缺 `depth` 按 `0`,缺 `dialect` 按 `mdb`,均记警告日志。
+4. **旧格式数组兼容**:更早期版本 `dir` 为字符串数组(无递归标志),引擎若仍有该格式在途配置,维持现有兼容逻辑即可。
+
+#### 4. UI 侧写入保证(引擎可信赖的前提)
+
+| 保证项 | 说明 |
+|---|---|
+| 原子写入 | 两份配置均经 `QSaveFile` 原子提交,不会出现写一半的截断文件 |
+| JSON 合法性 | cust.json 文本级替换写回前做整体 JSON 解析校验,非法即中止保存 |
+| 字段完整性 | V2 对象条目四字段/两字段总是齐全,不输出缺字段条目(兜底逻辑仅为防御外部手改) |
+| `ext` 节点 | 不变,引擎按现状使用 |
+| 顶层键顺序 | cust.json 其余顶层键与格式不被重排(文本级区间替换) |
+
+#### 5. 版本与联调约定
+
+1. **不兼容变更**:V2 格式对旧引擎不可解析(全量转码清单会为空)。**UI 与引擎必须同版本发布**,禁止单独升级任一侧。
+2. **升级方向兼容**:新引擎需兼容读 V1(存量用户配置),新 UI 兼容读 V1/V2。
+3. **回退方案**:若需回退到旧 UI,旧 UI 读 V2 配置会得到空清单,需手工将配置改回 V1 或删除配置重新生成。
+4. **待引擎侧反馈**:
+ - 2.3-3 非法值处理策略(告警放行 or 拒绝条目);
+ - `viaUmgr=true` 柜面接口转码的具体行为定义;
+ - `dialect` 驱动 SQL 语法转换(mdb/db)的实现范围与差异点清单。
+
+#### 6. 格式速查(供引擎实现对照)
+
+```
+cust.json convert.dir : V1 string[] → V2 {name, core, viaUmgr, dialect}[]
+ core 枚举 : ucbp | uses | uarg | usms
+ dialect 派生 : ucbp/uses→mdb ; uarg/usms→db
+
+cust_nonls.json convert.dir : V1 {name: 0|-1, "*": -1}
+ → V2 {name: {depth: 0|-1, dialect: "db"|"mdb"}, "*": -1}
+ depth 语义 : 0=单文件转码 ; -1=递归转码(与 V1 数值一致)
+ "*" 键 : 保持数值,恒不升级为对象
+```
+
---
## 8. 关键设计决策与问题记录
@@ -825,7 +1017,7 @@ MainWindow 启动即校验 `bin/uf2touft3/uf2touft3.exe` 是否存在:缺失
| --- | --- |
| 主窗口与导航、引擎调度 | `src/mainwindow/` |
| 业务转码 | `src/pages/businessconvert/`(容器)+ `src/pages/batchconvert/`(全量)+ `src/pages/nonlsconvert/`(按需) |
-| 功能查询 | `src/pages/functionsearch/`(UF20 / UFT3 双模式) |
+| 功能查询 | `src/pages/functionsearch/`(UF20 / UFT3 双模式 + UF20 & 内存表映射查询) |
| 代码检索 | `src/pages/codesearch/`(含 CallChainAnalyzer) |
| 数据映射 | `src/pages/datamapping/`(TableMappingPage / FunctionMappingPage / NoChangeFullNamePage) |
| 元数据更新 | `src/pages/metadataupdate/`(页面)+ `src/metadataupdate/`(解析器组) |
diff --git a/src/mainwindow/mainwindow.cpp b/src/mainwindow/mainwindow.cpp
index eb3e134..0492ae6 100644
--- a/src/mainwindow/mainwindow.cpp
+++ b/src/mainwindow/mainwindow.cpp
@@ -155,6 +155,8 @@ void MainWindow::initContent()
markPage("UF20功能查询");
addPageNode("UFT3功能查询", createUFT3FunctionSearchPage(), m_funcSearchKey, ElaIconType::MagnifyingGlassPlus);
markPage("UFT3功能查询");
+ addPageNode("UF20 & 内存表映射查询", createTableSearchPage(), m_funcSearchKey, ElaIconType::MagnifyingGlassPlus);
+ markPage("UF20 & 内存表映射查询");
expandNavigationNode(m_funcConvertKey);
addExpanderNode("数据映射", m_dataMappingKey, ElaIconType::TableList);
@@ -260,6 +262,24 @@ QWidget* MainWindow::createUFT3FunctionSearchPage()
return page;
}
+QWidget* MainWindow::createTableSearchPage()
+{
+ // UF20 & 内存表映射查询仅作检索展示, 不接入转码工作台;
+ // 双击记录行 → 添加映射到「表替换」页(追加后自动落盘保存)
+ TableSearchPage *page = new TableSearchPage;
+ connect(page, &TableSearchPage::requestAddMapping, this,
+ [this, page](const QString &uf20Name, const QString &memName) {
+ QString error;
+ if (m_tableMappingPage) {
+ error = m_tableMappingPage->appendPairIfAbsent(uf20Name, memName);
+ } else {
+ error = "表替换页面未初始化,无法添加";
+ }
+ page->onMappingAddResult(error);
+ });
+ return page;
+}
+
QWidget* MainWindow::createSettingsPage()
{
return new SettingsPage();
@@ -272,7 +292,9 @@ QWidget* MainWindow::createMetadataUpdatePage()
QWidget* MainWindow::createTableMappingPage()
{
- return new TableMappingPage();
+ // 持有实例: 供映射查询页双击添加条目(lambda 运行时两页均已创建)
+ m_tableMappingPage = new TableMappingPage();
+ return m_tableMappingPage;
}
QWidget* MainWindow::createFunctionMappingPage()
diff --git a/src/mainwindow/mainwindow.h b/src/mainwindow/mainwindow.h
index ee24213..0601db2 100644
--- a/src/mainwindow/mainwindow.h
+++ b/src/mainwindow/mainwindow.h
@@ -10,6 +10,7 @@
#include "src/pages/codesearch/codesearchpage.h"
#include "src/pages/functionsearch/uf20functionsearchpage.h"
#include "src/pages/functionsearch/uft3functionsearchpage.h"
+#include "src/pages/functionsearch/tablesearchpage.h"
#include "src/pages/about/aboutdialog.h"
#include "src/pages/settings/settingspage.h"
@@ -37,6 +38,7 @@ private:
QWidget* createCodeSearchPage();
QWidget* createUF20FunctionSearchPage();
QWidget* createUFT3FunctionSearchPage();
+ QWidget* createTableSearchPage();
QWidget* createSettingsPage();
QWidget* createMetadataUpdatePage();
QWidget* createTableMappingPage();
@@ -74,6 +76,7 @@ private:
BatchConvertPage *m_batchConvertPage;
NonLsConvertPage *m_nonLsConvertPage;
CodeSearchPage *_codeSearchPage;
+ TableMappingPage *m_tableMappingPage = nullptr; // 表替换页实例: 供映射查询页双击添加条目
VersionChecker *m_versionChecker = nullptr; // 复用: 每次"检查更新"都 new 会累积泄漏
ConvertKind m_activeKind;
diff --git a/src/pages/batchconvert/batchconvertpage.cpp b/src/pages/batchconvert/batchconvertpage.cpp
index 6ca5f81..eefb6e5 100644
--- a/src/pages/batchconvert/batchconvertpage.cpp
+++ b/src/pages/batchconvert/batchconvertpage.cpp
@@ -20,7 +20,7 @@
namespace {
-// V2 默认值(契约 docs/转码配置格式契约.md): core 默认 ucbp, 柜面接口默认否;
+// V2 默认值(契约 详细设计文档.md §7.5): core 默认 ucbp, 柜面接口默认否;
// dialect 不落 UI, 保存时由 core 派生(ucbp/uses→mdb, uarg/usms→db)
bool validateLsFunctionNames(QWidget *parent, const QStringList &functionNames)
@@ -355,6 +355,11 @@ bool BatchConvertPage::saveConfig(const QStringList &funcList)
}
// 5. 原子写回(二进制模式: 内容字节原样落盘, 不做任何换行转换)
+ // 保存窗口: 写入前开启, 窗口期内 QSaveFile 替换目标文件引发的
+ // fileChanged/directoryChanged 监控事件直接忽略, 防止自身保存触发重载
+ // (重载会 refreshTable 强制 scrollToTop, 表现为下拉选择后滚动条回顶);
+ // 失败路径(打开/提交失败)由组件 2s 兜底自动关窗
+ m_configMonitor->beginSaveWindow();
QSaveFile saveFile(configFilePath());
if (!saveFile.open(QIODevice::WriteOnly)) {
LogManager::instance()->logError(
@@ -368,6 +373,8 @@ bool BatchConvertPage::saveConfig(const QStringList &funcList)
return false;
}
+ // 程序自身写入后更新监控 mtime 基准, 避免触发无谓的重载
+ m_configMonitor->endSave();
return true;
}
diff --git a/src/pages/batchconvert/batchconvertpage.h b/src/pages/batchconvert/batchconvertpage.h
index 2831dcc..bb31228 100644
--- a/src/pages/batchconvert/batchconvertpage.h
+++ b/src/pages/batchconvert/batchconvertpage.h
@@ -14,7 +14,7 @@ class QStandardItem;
// 保存时对原文件做文本级区间替换(保留 cust.json 其余全部格式不重序列化)。
// core 决定目标核心(ucbp/uses/uarg/usms);dialect 由 core 派生、不提供独立 UI
// (ucbp/uses→mdb,uarg/usms→db);viaUmgr 为是否柜面接口。
-// 格式契约见 docs/转码配置格式契约.md
+// 格式契约见 详细设计文档.md §7.5
class BatchConvertPage : public ConvertWorkbenchBase
{
Q_OBJECT
diff --git a/src/pages/codesearch/codesearchpage.cpp b/src/pages/codesearch/codesearchpage.cpp
index 7f30d71..7878ee4 100644
--- a/src/pages/codesearch/codesearchpage.cpp
+++ b/src/pages/codesearch/codesearchpage.cpp
@@ -63,7 +63,7 @@ namespace {
for (const QJsonValue &v : dirArray) {
// V1 字符串元素 / V2 对象元素(取 name 字段)双格式兼容
- // (全量转码 convert.dir 格式升级, 见 docs/转码配置格式契约.md)
+ // (全量转码 convert.dir 格式升级, 见 详细设计文档.md §7.5)
QString name = v.isObject()
? v.toObject().value(Constants::DirEntry::Name).toString()
: v.toString();
@@ -960,12 +960,29 @@ void CodeSearchPage::onQueryByInput()
return;
}
const QList results = cache->searchUFT3Functions(raw, Constants::FunctionSearch::FieldFunctionNo, true);
- if (results.isEmpty() || results.first().eName.isEmpty()) {
- QMessageBox::information(this, "提示",
- QString("未找到功能号:%1 对应的函数").arg(raw));
- return;
+ if (!results.isEmpty() && !results.first().eName.isEmpty()) {
+ target = results.first().eName;
+ } else {
+ // 元数据查不到时回退代码索引按 objectId 查找: 元数据接口扫描会排除
+ // cbp/ucbp 目录(moduleGenerationCheck 开启时)且同名中文名接口互相覆盖,
+ // 部分功能号在元数据中缺失; 代码索引全量覆盖检索目录, 无此限制。
+ // 后台任务可能正在重建索引(写 objectId 哈希), 主线程读前须确认无在途任务
+ if (!m_activeWatchers.isEmpty()) {
+ QMessageBox::information(this, "提示", "当前有任务正在执行, 请稍候再试");
+ return;
+ }
+ const QList> byId =
+ currentAnalyzer()->lookupByObjectId(raw);
+ if (byId.isEmpty()) {
+ QMessageBox::information(this, "提示",
+ QString("未找到功能号:%1 对应的函数\n"
+ "请确认该功能号属于 UF30 工程接口,且元数据已更新、"
+ "代码检索路径正确并已刷新索引。").arg(raw));
+ return;
+ }
+ // 同功能号多接口时取第一个候选(均在当前检索目录内, 皆可展示调用链)
+ target = byId.first()->chineseName;
}
- target = results.first().eName;
}
runQuery(target);
diff --git a/src/pages/common/convertworkbenchbase.cpp b/src/pages/common/convertworkbenchbase.cpp
index f63a1f1..e3f8760 100644
--- a/src/pages/common/convertworkbenchbase.cpp
+++ b/src/pages/common/convertworkbenchbase.cpp
@@ -17,6 +17,8 @@
#include
#include
#include
+#include
+#include
#include "ElaPushButton.h"
#include "ElaTheme.h"
@@ -196,6 +198,11 @@ QStandardItem *ConvertWorkbenchBase::createNameItem(const QString &funcName)
void ConvertWorkbenchBase::refreshTable()
{
+ // 记录重建前的滚动像素位置: 编辑/增删/外部重载后恢复原位, 避免全量重建
+ // 后视图跳顶(曾表现为"修改 UF20 函数名后滚动条自动跳回顶部")
+ QScrollBar *vbar = m_funcTable->verticalScrollBar();
+ const int savedScrollPos = vbar->value();
+
// 不使用 blockSignals:QTableView 依赖 rowsRemoved/rowsInserted 信号更新内部布局缓存,
// 若屏蔽会导致新增行不可见。
// 批量重建期间抑制逐行重绘:N 行的 removeRow/appendRow/setIndexWidget 每步都会触发
@@ -234,11 +241,24 @@ void ConvertWorkbenchBase::refreshTable()
// 恢复视图更新并整体重绘一次
m_funcTable->setUpdatesEnabled(true);
- // 视图刷新保险:强制重绘并滚动到顶部,确保新增行可见
+ // 视图刷新保险:强制重绘;恢复滚动位置。
+ // 关键时序: 重建后滚动范围(maximum)要等 Qt 延迟布局事件才最终确定, 直接 setValue
+ // 会被 clamp(表现为位置漂移到中间); scrollTo(PositionAtTop) 按视口相对坐标换算,
+ // 在布局中间态下会错位甚至因"目标已可见"短路不滚。因此先 doItemsLayout() 强制
+ // 同步完成延迟布局(滚动范围就绪), 再按绝对像素值精确恢复
m_funcTable->viewport()->update();
m_funcTable->resizeRowsToContents();
- m_funcTable->scrollToTop();
+ m_funcTable->doItemsLayout();
+ vbar->setValue(savedScrollPos);
m_funcTable->clearSelection();
+
+ // 延迟一拍兜底补设: 布局/滚动范围在此后才最终确定的场景下校正,
+ // 防止其他代码在重建后改动滚动条导致位置漂移
+ QTimer::singleShot(0, m_funcTable, [vbar, savedScrollPos]() {
+ if (vbar->value() != savedScrollPos) {
+ vbar->setValue(savedScrollPos);
+ }
+ });
}
void ConvertWorkbenchBase::refreshExistColumn()
@@ -283,6 +303,16 @@ void ConvertWorkbenchBase::showEvent(QShowEvent *event)
// 构造期 loadConfig 计算列宽时页面尚未布局, viewport 宽度非真实值,
// 首次切入本页时宽度已就绪, 重算一次保证列宽比例正确(幂等, 开销可忽略)
applyColumnLayout();
+ // 行高定稿: 页面在启动期(窗口未显示、字体/控件未 polish)构建, 那时 refreshTable
+ // 的 resizeRowsToContents 算出的行高偏小(如 28px); 首次编辑触发的重建在可见
+ // 状态下算出最终行高(如 33px), 行高突变使同一滚动数值下的可视内容整体位移,
+ // 表现为"首次修改后滚动条回滚"。切页显示后补算一次, 让行高在用户操作前定稿
+ // (延迟一拍确保布局事件处理完毕后再测量, 测量结果与重建路径一致)
+ QTimer::singleShot(0, this, [this]() {
+ if (m_funcTable && isVisible()) {
+ m_funcTable->resizeRowsToContents();
+ }
+ });
// 元数据更新后 DataCache 可能变化: 切页回来仅轻量刷新"是否存在"列,
// 不做全量表格重建(数据变更已由 refreshTable/外部修改监控统一触发)
refreshExistColumn();
diff --git a/src/pages/datamapping/mappingtablepage.cpp b/src/pages/datamapping/mappingtablepage.cpp
index 19d4d4b..51f6fd0 100644
--- a/src/pages/datamapping/mappingtablepage.cpp
+++ b/src/pages/datamapping/mappingtablepage.cpp
@@ -210,6 +210,21 @@ void MappingTablePage::onModelItemChanged(QStandardItem *item)
void MappingTablePage::onSave()
{
+ QString errorMessage;
+ const SaveResult result = saveToDisk(&errorMessage);
+ if (result == SaveResult::Ok) {
+ QMessageBox::information(this, "提示", "保存成功!");
+ } else if (result == SaveResult::ValidationFailed) {
+ QMessageBox::warning(this, "提示", errorMessage);
+ } else {
+ QMessageBox::critical(this, "错误", errorMessage);
+ }
+}
+
+MappingTablePage::SaveResult MappingTablePage::saveToDisk(QString *errorMessage)
+{
+ if (errorMessage) errorMessage->clear();
+
// 开启保存窗口: 窗口期内监控事件直接忽略
m_monitor->beginSaveWindow();
@@ -218,8 +233,8 @@ void MappingTablePage::onSave()
QFile file(jsonPath);
if (!file.open(QIODevice::ReadOnly)) {
m_monitor->endSave(); // 早退必须关闭窗口,否则监控被永久抑制
- QMessageBox::critical(this, "错误", "无法读取 cust.json!");
- return;
+ if (errorMessage) *errorMessage = "无法读取 cust.json!";
+ return SaveResult::Error;
}
const QString rawJson = QString::fromUtf8(file.readAll());
file.close();
@@ -228,21 +243,21 @@ void MappingTablePage::onSave()
QJsonDocument doc = QJsonDocument::fromJson(rawJson.toUtf8(), &error);
if (error.error != QJsonParseError::NoError) {
m_monitor->endSave();
- QMessageBox::critical(this, "错误", "cust.json 解析失败: " + error.errorString());
- return;
+ if (errorMessage) *errorMessage = "cust.json 解析失败: " + error.errorString();
+ return SaveResult::Error;
}
QJsonObject custJson = doc.object();
// 提取原始 key 顺序
const QStringList keyOrder = CustJsonIo::extractTopLevelKeyOrder(rawJson);
- // 子类校验并回填数据: 校验失败置 errorMessage, 弹警告不落盘
- QString errorMessage;
- const QHash overrideRaw = collectMappingData(custJson, errorMessage);
- if (!errorMessage.isEmpty()) {
+ // 子类校验并回填数据: 校验失败不落盘
+ QString validateError;
+ const QHash overrideRaw = collectMappingData(custJson, validateError);
+ if (!validateError.isEmpty()) {
m_monitor->endSave();
- QMessageBox::warning(this, "提示", errorMessage);
- return;
+ if (errorMessage) *errorMessage = validateError;
+ return SaveResult::ValidationFailed;
}
// endSave 以保存后的新 mtime 为基准(writeCustJsonOrdered 同步写完才返回),
@@ -254,10 +269,15 @@ void MappingTablePage::onSave()
if (writeOk) {
m_dirty = false; // 保存成功后与磁盘内容一致
LogManager::instance()->logInfo(saveLogText(m_model->rowCount()));
- QMessageBox::information(this, "提示", "保存成功!");
- } else {
- QMessageBox::critical(this, "错误", "保存到 cust.json 失败!");
+ onSavedToDisk(); // 落盘成功钩子(表替换页覆写: 同步 table_mapping.json)
+ return SaveResult::Ok;
}
+ if (errorMessage) *errorMessage = "保存到 cust.json 失败!";
+ return SaveResult::Error;
+}
+
+void MappingTablePage::onSavedToDisk()
+{
}
void MappingTablePage::onReloadRequested()
diff --git a/src/pages/datamapping/mappingtablepage.h b/src/pages/datamapping/mappingtablepage.h
index 06fdf56..85285eb 100644
--- a/src/pages/datamapping/mappingtablepage.h
+++ b/src/pages/datamapping/mappingtablepage.h
@@ -73,6 +73,18 @@ protected:
// 子类构造体内调用(规避构造期虚函数绑定到基类版本)
void load();
+ // 保存结果分类: Ok=成功落盘; ValidationFailed=子类校验未过(用户级警告);
+ // Error=读文件/解析/写盘失败(程序级错误)
+ enum class SaveResult { Ok, ValidationFailed, Error };
+
+ // 落盘保存核心(无任何弹窗): onSave 与「映射查询页双击自动添加保存」共用。
+ // 失败原因经 errorMessage 出参带回(ValidationFailed=校验文案, Error=IO/解析文案)
+ SaveResult saveToDisk(QString *errorMessage);
+
+ // 落盘成功后的钩子(默认空实现),子类按需覆写
+ // (表替换页覆写: 把新增映射同步追加到 table_mapping.json)
+ virtual void onSavedToDisk();
+
// 虚点:从 custJson 填充模型
virtual void populateModel(const QJsonObject &custJson) = 0;
// 虚点:保存时校验并回填数据到 custJson;返回 overrideRaw(键名→格式化原文);
diff --git a/src/pages/datamapping/tablemappingpage.cpp b/src/pages/datamapping/tablemappingpage.cpp
index 580bf66..290c9ac 100644
--- a/src/pages/datamapping/tablemappingpage.cpp
+++ b/src/pages/datamapping/tablemappingpage.cpp
@@ -1,5 +1,24 @@
#include "tablemappingpage.h"
-#include "utils/Constants.h"
+#include "src/utils/Constants.h"
+#include "src/utils/logmanager.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
+
+namespace {
+ // JSON 字符串转义: 表名为普通标识符, 实际仅需处理引号与反斜杠
+ QString escapeJsonString(const QString &text)
+ {
+ QString out = text;
+ out.replace('\\', "\\\\");
+ out.replace('"', "\\\"");
+ return out;
+ }
+} // namespace
TableMappingPage::TableMappingPage(QWidget *parent)
: TwoColumnKVMappingPage(Constants::CustKeys::TableCompare,
@@ -7,3 +26,248 @@ TableMappingPage::TableMappingPage(QWidget *parent)
{
load();
}
+
+void TableMappingPage::onSavedToDisk()
+{
+ syncTableMappingJson();
+}
+
+void TableMappingPage::syncTableMappingJson()
+{
+ // 模型当前映射(模型序): 刚落盘的 cust.json 与模型一致, 以其为同步源
+ QHash modelMap;
+ for (int row = 0; row < m_model->rowCount(); ++row) {
+ QStandardItem *keyItem = m_model->item(row, 0);
+ if (!keyItem || keyItem->text().trimmed().isEmpty()) {
+ continue; // 空键行在 collectMappingData 已拦截, 正常不会走到
+ }
+ QStandardItem *valItem = m_model->item(row, 1);
+ modelMap.insert(keyItem->text().trimmed(),
+ valItem ? valItem->text().trimmed() : QString());
+ }
+
+ const QString jsonPath = Constants::tableMappingJson();
+ QFile file(jsonPath);
+ if (!file.open(QIODevice::ReadOnly)) {
+ LogManager::instance()->logError("表替换同步 - 无法读取 table_mapping.json: " + jsonPath);
+ return;
+ }
+ const QString raw = QString::fromUtf8(file.readAll());
+ file.close();
+
+ QJsonParseError parseErr;
+ const QJsonDocument doc = QJsonDocument::fromJson(raw.toUtf8(), &parseErr);
+ if (parseErr.error != QJsonParseError::NoError) {
+ LogManager::instance()->logError(
+ QString("表替换同步 - table_mapping.json 解析失败: %1 (偏移 %2)")
+ .arg(parseErr.errorString()).arg(parseErr.offset));
+ return;
+ }
+ const QJsonObject tmObj = doc.object()[Constants::CustKeys::TableCompare].toObject();
+
+ // 文本定位 tableCompareUf2_Uf3 对象的开/闭括号, 成员级解析后按原格式重建,
+ // 保持文件其余部分(条目顺序/一对多数组/空行风格)原样不动
+ const QString tmKey = "\"" + Constants::CustKeys::TableCompare + "\"";
+ const qsizetype keyPos = raw.indexOf(tmKey);
+ const qsizetype openPos = keyPos < 0 ? -1 : raw.indexOf('{', keyPos + tmKey.size());
+ if (openPos < 0) {
+ LogManager::instance()->logError(
+ "表替换同步 - table_mapping.json 中未找到 tableCompareUf2_Uf3 对象");
+ return;
+ }
+
+ // 成员描述: key + 原文起止(从 key 引号到 value 结束, 含跨行数组原文)
+ struct Member {
+ QString key;
+ qsizetype start;
+ qsizetype end;
+ };
+ QList members;
+
+ // 花括号/方括号配对跳过字符串字面量, 从开括号后逐成员解析
+ qsizetype i = openPos + 1;
+ while (i < raw.size()) {
+ while (i < raw.size() && raw.at(i).isSpace()) {
+ ++i;
+ }
+ if (i >= raw.size() || raw.at(i) == '}') {
+ break;
+ }
+ if (raw.at(i) != '"') {
+ LogManager::instance()->logError(
+ QString("表替换同步 - tableCompareUf2_Uf3 成员解析异常(偏移 %1), 已放弃同步").arg(i));
+ return;
+ }
+ const qsizetype memberStart = i;
+ // 解析 key 字符串(处理 \" 与 \\ 转义, 与 QJsonDocument 解析口径一致)
+ ++i;
+ QString key;
+ while (i < raw.size()) {
+ const QChar ch = raw.at(i++);
+ if (ch == '\\' && i < raw.size()) {
+ const QChar esc = raw.at(i++);
+ if (esc == '"' || esc == '\\') {
+ key += esc;
+ } else {
+ key += '\\';
+ key += esc;
+ }
+ } else if (ch == '"') {
+ break;
+ } else {
+ key += ch;
+ }
+ }
+ while (i < raw.size() && raw.at(i).isSpace()) {
+ ++i;
+ }
+ if (i >= raw.size() || raw.at(i) != ':') {
+ LogManager::instance()->logError(
+ QString("表替换同步 - 成员 %1 缺少冒号, 已放弃同步").arg(key));
+ return;
+ }
+ ++i;
+ while (i < raw.size() && raw.at(i).isSpace()) {
+ ++i;
+ }
+ // 解析 value: 字符串 / 数组 / 对象, 括号配对方括号与花括号都计数
+ int bracketDepth = 0;
+ bool valString = false;
+ const qsizetype valueEnd = [&]() {
+ while (i < raw.size()) {
+ const QChar ch = raw.at(i);
+ if (valString) {
+ if (ch == '\\') {
+ i += 2;
+ continue;
+ }
+ if (ch == '"') {
+ valString = false;
+ }
+ ++i;
+ continue;
+ }
+ if (ch == '"') {
+ valString = true;
+ } else if (ch == '[' || ch == '{') {
+ ++bracketDepth;
+ } else if (ch == ']' || ch == '}') {
+ --bracketDepth;
+ if (bracketDepth < 0) {
+ return i; // 到达本对象闭括号(value 已结束于上一字符)
+ }
+ } else if (ch == ',' && bracketDepth == 0) {
+ return i;
+ }
+ ++i;
+ }
+ return i;
+ }();
+ // memberEnd 回退 value 尾部空白, 确保原文片段以 value 末字符结尾
+ qsizetype trimmedEnd = valueEnd;
+ while (trimmedEnd > memberStart && raw.at(trimmedEnd - 1).isSpace()) {
+ --trimmedEnd;
+ }
+ members.append({key, memberStart, trimmedEnd});
+ i = valueEnd;
+ if (i < raw.size() && raw.at(i) == ',') {
+ ++i;
+ }
+ }
+
+ // 缩进/换行探测: 以首个既有成员所在行的行首空白为准
+ const QString newline = raw.contains("\r\n") ? "\r\n" : "\n";
+ QString entryIndent(" "); // tm 当前缩进风格兜底
+ QString closeIndent(" ");
+ if (!members.isEmpty()) {
+ const qsizetype lineStart = raw.lastIndexOf('\n', members.first().start) + 1;
+ entryIndent = raw.mid(lineStart, members.first().start - lineStart);
+ }
+ {
+ const qsizetype closePos = raw.indexOf('}', i);
+ if (closePos >= 0) {
+ const qsizetype lineStart = raw.lastIndexOf('\n', closePos) + 1;
+ closeIndent = raw.mid(lineStart, closePos - lineStart);
+ }
+ }
+
+ // 逐成员对账: 同 key 时字符串 value 变了才更新; 数组(一对多)一律保留原文;
+ // 模型中多余的 key 追加到末尾。删除暂不同步(成员不因模型缺失而移除)
+ QStringList blocks;
+ QSet processedKeys;
+ int updated = 0;
+ int arraySkipped = 0;
+ int emptyValueSkipped = 0;
+ for (const Member &member : members) {
+ processedKeys.insert(member.key);
+ const auto modelIt = modelMap.constFind(member.key);
+ if (modelIt == modelMap.constEnd()) {
+ blocks.append(raw.mid(member.start, member.end - member.start)); // cust 已删: tm 保留
+ continue;
+ }
+ const QJsonValue tmVal = tmObj.value(member.key);
+ if (tmVal.isArray()) {
+ blocks.append(raw.mid(member.start, member.end - member.start)); // 一对多数组不覆盖
+ ++arraySkipped;
+ continue;
+ }
+ const QString newVal = modelIt.value();
+ if (newVal.isEmpty()) {
+ blocks.append(raw.mid(member.start, member.end - member.start)); // 空值不同步
+ ++emptyValueSkipped;
+ continue;
+ }
+ if (tmVal.isString() && tmVal.toString() != newVal) {
+ blocks.append('"' + escapeJsonString(member.key) + "\": \""
+ + escapeJsonString(newVal) + '"');
+ ++updated;
+ } else {
+ blocks.append(raw.mid(member.start, member.end - member.start));
+ }
+ }
+ int appended = 0;
+ for (auto it = modelMap.constBegin(); it != modelMap.constEnd(); ++it) {
+ if (processedKeys.contains(it.key()) || it.value().isEmpty()) {
+ continue;
+ }
+ blocks.append('"' + escapeJsonString(it.key()) + "\": \""
+ + escapeJsonString(it.value()) + '"');
+ ++appended;
+ }
+ if (appended == 0 && updated == 0) {
+ return; // 无新增无更新(数组跳过/空值跳过不产生写入)
+ }
+
+ // 重建对象内部: 每成员统一行首缩进, 成员间逗号分隔; 成员原文若跨行(数组),
+ // 其内部各行已有更大缩进, 保持原样
+ QStringList indented;
+ indented.reserve(blocks.size());
+ for (const QString &block : blocks) {
+ indented.append(entryIndent + block);
+ }
+ const QString interior = newline + indented.join(QString(",") + newline) + newline + closeIndent;
+ const QString updatedRaw = raw.left(openPos + 1) + interior + raw.mid(raw.indexOf('}', i));
+
+ // 写前校验: 拼接结果必须仍是合法 JSON, 防止文本插入破坏文件
+ QJsonParseError verifyErr;
+ QJsonDocument::fromJson(updatedRaw.toUtf8(), &verifyErr);
+ if (verifyErr.error != QJsonParseError::NoError) {
+ LogManager::instance()->logError(
+ QString("表替换同步 - 拼接结果校验失败, 已放弃写入: %1").arg(verifyErr.errorString()));
+ return;
+ }
+
+ QFile outFile(jsonPath);
+ if (!outFile.open(QIODevice::WriteOnly | QIODevice::Truncate)) {
+ LogManager::instance()->logError("表替换同步 - 无法写入 table_mapping.json: " + jsonPath);
+ return;
+ }
+ outFile.write(updatedRaw.toUtf8());
+ outFile.close();
+
+ LogManager::instance()->logInfo(
+ QString("表替换保存: table_mapping.json 同步完成(新增 %1, 更新 %2%3%4)")
+ .arg(appended).arg(updated)
+ .arg(arraySkipped > 0 ? QString(", 一对多数组跳过 %1").arg(arraySkipped) : QString())
+ .arg(emptyValueSkipped > 0 ? QString(", 空值跳过 %1").arg(emptyValueSkipped) : QString()));
+}
diff --git a/src/pages/datamapping/tablemappingpage.h b/src/pages/datamapping/tablemappingpage.h
index 42279ba..530bdc6 100644
--- a/src/pages/datamapping/tablemappingpage.h
+++ b/src/pages/datamapping/tablemappingpage.h
@@ -5,13 +5,22 @@
// 维护 cust.json 顶层键 tableCompareUf2_Uf3(UF20→UFT3 表替换映射):
// 两列映射表, 支持 新增(弹窗)/双击修改/删除/搜索过滤, 保存按顶层键顺序保序写回;
-// 填充/校验/新增弹窗样板由 TwoColumnKVMappingPage 中间基类收编
+// 填充/校验/新增弹窗样板由 TwoColumnKVMappingPage 中间基类收编。
+// 保存成功后把模型中 table_mapping.json 缺失的新增 key 追加到该文件
+// (只增不改: 已有条目含一对多数组一律以 table_mapping.json 为准)
class TableMappingPage : public TwoColumnKVMappingPage
{
Q_OBJECT
public:
explicit TableMappingPage(QWidget *parent = nullptr);
+
+protected:
+ void onSavedToDisk() override;
+
+private:
+ // 把模型中 table_mapping.json 缺失的 key 追加到该文件(文本定位插入, 保序保格式)
+ void syncTableMappingJson();
};
#endif // TABLEMAPPINGPAGE_H
diff --git a/src/pages/datamapping/twocolumnkvmappingpage.cpp b/src/pages/datamapping/twocolumnkvmappingpage.cpp
index e9059d2..f9a028b 100644
--- a/src/pages/datamapping/twocolumnkvmappingpage.cpp
+++ b/src/pages/datamapping/twocolumnkvmappingpage.cpp
@@ -73,6 +73,35 @@ QHash TwoColumnKVMappingPage::collectMappingData(QJsonObject &
CustJsonIo::formatOrderedJsonObject(orderedData)}};
}
+QString TwoColumnKVMappingPage::appendPairIfAbsent(const QString &key, const QString &value)
+{
+ // 判重口径与 onAddRow/collectMappingData 一致: trimmed 后比较
+ const QString k = key.trimmed();
+ if (k.isEmpty()) {
+ return QString("UF20 %1为空,无法添加").arg(m_entityName);
+ }
+ for (int row = 0; row < m_model->rowCount(); ++row) {
+ if (m_model->item(row, 0) && m_model->item(row, 0)->text().trimmed() == k) {
+ return QString("UF20 %1 \"%2\" 在表替换中已存在(当前映射: %3),未重复添加")
+ .arg(m_entityName).arg(k)
+ .arg(m_model->item(row, 1) ? m_model->item(row, 1)->text() : QString());
+ }
+ }
+ QList rowItems;
+ rowItems.append(new QStandardItem(k));
+ rowItems.append(new QStandardItem(value.trimmed()));
+ m_model->appendRow(rowItems);
+ // 显式置脏纳入外部文件变更保护, 随后立即落盘保存(自动保存语义,
+ // 无弹窗: 成功静默, 失败把原因带回给调用页 tip 展示)
+ m_dirty = true;
+ QString saveError;
+ const MappingTablePage::SaveResult result = saveToDisk(&saveError);
+ if (result != MappingTablePage::SaveResult::Ok) {
+ return QString("已添加但自动保存失败:%1").arg(saveError);
+ }
+ return QString();
+}
+
void TwoColumnKVMappingPage::onAddRow()
{
QDialog dialog(this);
diff --git a/src/pages/datamapping/twocolumnkvmappingpage.h b/src/pages/datamapping/twocolumnkvmappingpage.h
index 68afb43..331c562 100644
--- a/src/pages/datamapping/twocolumnkvmappingpage.h
+++ b/src/pages/datamapping/twocolumnkvmappingpage.h
@@ -23,6 +23,12 @@ public:
const QString &entityName,
QWidget *parent = nullptr);
+ // 追加一对映射(UF20 & 内存表映射查询页双击添加的入口)。
+ // key 按 trimmed 口径判重: 已存在返回提示文案(含当前映射值,不覆盖)。
+ // 追加成功后自动落盘保存(无弹窗),失败时返回"已添加但自动保存失败:原因"文案,
+ // 成功返回空串
+ QString appendPairIfAbsent(const QString &key, const QString &value);
+
protected:
void populateModel(const QJsonObject &custJson) override;
QHash collectMappingData(QJsonObject &custJson,
diff --git a/src/pages/functionsearch/tablesearchpage.cpp b/src/pages/functionsearch/tablesearchpage.cpp
new file mode 100644
index 0000000..80308b7
--- /dev/null
+++ b/src/pages/functionsearch/tablesearchpage.cpp
@@ -0,0 +1,371 @@
+#include "tablesearchpage.h"
+#include "src/utils/Constants.h"
+#include "src/utils/datacache.h"
+#include "src/utils/logmanager.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include "ElaTheme.h"
+
+namespace {
+ // 拆分多关键词: 中英文逗号/分号 + 空白统一视为分隔符(与功能查询页口径一致)
+ // 全角字符用字符串字面量替换, 多字节字符不能写成字符常量(会成为 multichar)
+ const QRegularExpression kWhitespaceRegex(QStringLiteral("\\s+"));
+
+ QStringList splitKeywords(const QString& text)
+ {
+ QString normalized = text;
+ normalized.replace(QStringLiteral(","), QStringLiteral(" "))
+ .replace(QStringLiteral(";"), QStringLiteral(" "))
+ .replace(',', ' ')
+ .replace(';', ' ');
+ return normalized.split(kWhitespaceRegex, Qt::SkipEmptyParts);
+ }
+}
+
+TableSearchPage::TableSearchPage(QWidget *parent)
+ : QWidget(parent)
+ , m_tipLabel(nullptr)
+ , m_exactMatch(false)
+{
+ initUI();
+}
+
+void TableSearchPage::initUI()
+{
+ QVBoxLayout *layout = new QVBoxLayout(this);
+ layout->setContentsMargins(30, 30, 30, 30);
+ layout->setSpacing(20);
+
+ QHBoxLayout *searchLayout = new QHBoxLayout;
+ searchLayout->setSpacing(10);
+
+ m_searchEdit = new ElaSearchEdit;
+ m_searchEdit->setPlaceholderText("搜索 UF20 表名或内存表名...");
+ m_searchEdit->setSearchEditSize(330, 35);
+ connect(m_searchEdit, &ElaSearchEdit::returnPressed, this, &TableSearchPage::onSearch);
+ connect(m_searchEdit, &ElaSearchEdit::searchClicked, this, &TableSearchPage::onSearch);
+ connect(m_searchEdit, &ElaSearchEdit::exactMatchChanged, this, &TableSearchPage::onExactMatchToggled);
+ searchLayout->addWidget(m_searchEdit);
+ searchLayout->addStretch();
+ layout->addLayout(searchLayout);
+
+ m_tableModel = new QStandardItemModel(this);
+ m_tableModel->setColumnCount(4);
+ m_tableModel->setHorizontalHeaderLabels({"UF20表名", "UF20表中文名", "内存表名", "内存表中文名"});
+
+ m_resultTable = new ElaTableView;
+ m_resultTable->setModel(m_tableModel);
+ m_resultTable->horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch);
+ m_resultTable->horizontalHeader()->setDefaultAlignment(Qt::AlignCenter); // 表头居中
+ m_resultTable->setMinimumHeight(400);
+ m_resultTable->setSelectionBehavior(QAbstractItemView::SelectRows);
+ m_resultTable->setEditTriggers(QAbstractItemView::NoEditTriggers);
+ layout->addWidget(m_resultTable);
+
+ // 双击记录行 → 添加映射到「数据映射 → 表替换」页(经 MainWindow 接线)
+ connect(m_resultTable, &QTableView::doubleClicked, this, &TableSearchPage::onDoubleClicked);
+
+ QHBoxLayout *tipLayout = new QHBoxLayout;
+ m_tipLabel = new QLabel("💡 输入 UF20 表名或内存表名进行查询,支持精确/模糊匹配;双击记录行可添加到「数据映射 → 表替换」");
+ // 不开折行: wordWrap(true) 的 QLabel 在布局中会自选偏窄宽度把提示拆成多行,
+ // 与功能查询页保持一致, 提示单行显示
+ m_tipLabel->setWordWrap(false);
+ tipLayout->addWidget(m_tipLabel);
+ tipLayout->addStretch();
+ layout->addLayout(tipLayout);
+
+ // tip 标签为主题感知: 跟随 Ela 主题切换, 并按当前主题初始化
+ connect(ElaTheme::getInstance(), &ElaTheme::themeModeChanged,
+ this, &TableSearchPage::updateTipStyle);
+ updateTipStyle();
+}
+
+void TableSearchPage::updateTipStyle()
+{
+ if (!m_tipLabel) {
+ return;
+ }
+ const ElaThemeType::ThemeMode mode = ElaTheme::getInstance()->getThemeMode();
+ const QString textColor = ElaThemeColor(mode, BasicText).name();
+ m_tipLabel->setStyleSheet(QString("color: %1; font-size: 13px;").arg(textColor));
+}
+
+void TableSearchPage::onExactMatchToggled(bool checked)
+{
+ m_exactMatch = checked;
+}
+
+void TableSearchPage::onDoubleClicked(const QModelIndex& index)
+{
+ // 双击行取第 1 列(UF20表名)与第 3 列(内存表名): 任一为空即无完整映射关系, 不能添加
+ const QString uf20Name = m_tableModel->item(index.row(), 0)
+ ? m_tableModel->item(index.row(), 0)->text() : QString();
+ const QString memName = m_tableModel->item(index.row(), 2)
+ ? m_tableModel->item(index.row(), 2)->text() : QString();
+ if (uf20Name.isEmpty() || memName.isEmpty()) {
+ if (m_tipLabel) {
+ m_tipLabel->setText("⚠️ 该行无完整映射关系(UF20 表名或内存表名为空),无法添加到表替换");
+ }
+ return;
+ }
+ emit requestAddMapping(uf20Name, memName);
+}
+
+void TableSearchPage::onMappingAddResult(const QString& error)
+{
+ // 双击添加的回调结果: error 为空即添加并自动保存成功, 非空为原因(已存在/保存失败等)
+ if (!m_tipLabel) {
+ return;
+ }
+ if (error.isEmpty()) {
+ m_tipLabel->setText("✅ 已添加到「数据映射 → 表替换」并自动保存");
+ } else {
+ m_tipLabel->setText("⚠️ " + error);
+ }
+}
+
+QList> TableSearchPage::loadTableMapping(bool* readOk,
+ QString* errorOut) const
+{
+ if (readOk) *readOk = false;
+ if (errorOut) errorOut->clear();
+ QList> pairs;
+ const QString jsonPath = Constants::tableMappingJson();
+ QFile file(jsonPath);
+ if (!file.open(QIODevice::ReadOnly)) {
+ const QString msg = "无法读取映射文件 table_mapping.json(" + jsonPath + ")";
+ LogManager::instance()->logError("表查询 - " + msg);
+ if (errorOut) *errorOut = msg;
+ return pairs;
+ }
+ const QByteArray raw = file.readAll();
+ file.close();
+ QJsonParseError err;
+ const QJsonDocument doc = QJsonDocument::fromJson(raw, &err);
+ if (err.error != QJsonParseError::NoError) {
+ // 字符偏移转行号(从 1 起), 便于手工维护文件时定位(常见手误: 末项尾随逗号)
+ int line = 1;
+ for (qint64 pos = 0; pos < err.offset && pos < raw.size(); ++pos) {
+ if (raw.at(pos) == '\n') {
+ ++line;
+ }
+ }
+ const QString msg = QString("映射文件 table_mapping.json 解析失败(第 %1 行附近):%2。"
+ "映射关系暂不可用,请检查该文件(常见原因:最后一项后多了逗号、引号缺失)")
+ .arg(line).arg(err.errorString());
+ LogManager::instance()->logError("表查询 - " + msg);
+ if (errorOut) *errorOut = msg;
+ return pairs;
+ }
+ // 顶层键 tableCompareUf2_Uf3: key=UF20表名, value=内存表名;
+ // value 可为字符串(一对一)或数组(一对多, 拆为多条映射对, 展示时一个 UF20 表对应多行)
+ const QJsonObject mapping = doc.object()[Constants::CustKeys::TableCompare].toObject();
+ for (auto it = mapping.constBegin(); it != mapping.constEnd(); ++it) {
+ if (it.value().isArray()) {
+ const QJsonArray arr = it.value().toArray();
+ for (const QJsonValue& v : arr) {
+ pairs.append({it.key(), v.toString()});
+ }
+ } else {
+ pairs.append({it.key(), it.value().toString()});
+ }
+ }
+ if (readOk) *readOk = true;
+ return pairs;
+}
+
+void TableSearchPage::onSearch()
+{
+ // 多关键词查询: 支持逗号/空格等分隔多个表名, 任一关键词命中即展示(OR 并集)
+ const QStringList keywords = splitKeywords(m_searchEdit->text());
+
+ m_tableModel->removeRows(0, m_tableModel->rowCount());
+
+ if (keywords.isEmpty()) {
+ // 空关键字: 页内 tip 反馈, 不弹模态框打断操作
+ if (m_tipLabel) {
+ m_tipLabel->setText("请输入 UF20 表名或内存表名");
+ }
+ return;
+ }
+
+ // 数据源三件套: 双库"表名→中文名"映射 + table_mapping.json 表映射(key=UF20表名, value=内存表名)。
+ // 双库映射由 DataCache 内存缓存, 重复查询零开销
+ const QMap uf20Names = DataCache::instance()->getUf20TableNameMap();
+ const QMap memNames = DataCache::instance()->getUft3TableNameMap();
+ if (uf20Names.isEmpty() && memNames.isEmpty()) {
+ if (m_tipLabel) {
+ m_tipLabel->setText("⚠️ 缓存库中暂无表数据,请先在「元数据更新」页面执行更新");
+ }
+ return;
+ }
+
+ bool mappingReadOk = false;
+ QString mappingError;
+ const QList> mapping = loadTableMapping(&mappingReadOk, &mappingError);
+ // 映射文件是本页核心数据源: 读取/解析失败时醒目报错并停止检索,
+ // 不再降级展示单侧结果(否则表现为"有表名却查不到映射", 极具误导性)
+ if (!mappingReadOk) {
+ if (m_tipLabel) {
+ m_tipLabel->setText("⚠️ " + mappingError);
+ }
+ return;
+ }
+
+ // 单行结果: 四列与页面列定义一致, 任一表名侧为空即"无映射关系"行
+ struct TableRow {
+ QString uf20Name, uf20Cname, memName, memCname;
+ };
+ QList rows;
+ // 已覆盖表名集合: 映射整体行里的表不再补单侧行, 防止同一表重复出现
+ QSet coveredUf20;
+ QSet coveredMem;
+ // 已输出的映射对(以换行分隔拼接): 多关键词可能重复命中同一映射对, 去重
+ QSet seenPairs;
+
+ // 映射命中(key 或 value)→ 完整行: 按 key/value 分别取双库中文名
+ auto appendFullRow = [&](const QString& key, const QString& value) {
+ const QString pairKey = key + QLatin1Char('\n') + value;
+ if (seenPairs.contains(pairKey)) {
+ return;
+ }
+ seenPairs.insert(pairKey);
+ rows.append({key, uf20Names.value(key), value, memNames.value(value)});
+ coveredUf20.insert(key);
+ coveredMem.insert(value);
+ };
+
+ QStringList noMatchKeywords;
+ for (const QString& keyword : keywords) {
+ bool hit = false;
+ if (m_exactMatch) {
+ // 精确匹配: 先查映射(key 或 value 全等 → 整行), 再查双库(单侧行);
+ // 映射已覆盖的表不再补单侧行, 否则同一表会多出一条映射列置空的重复记录
+ for (const auto& pair : mapping) {
+ if (pair.first == keyword || pair.second == keyword) {
+ appendFullRow(pair.first, pair.second);
+ hit = true;
+ }
+ }
+ if (!coveredUf20.contains(keyword) && uf20Names.contains(keyword)) {
+ rows.append({keyword, uf20Names.value(keyword), QString(), QString()});
+ coveredUf20.insert(keyword);
+ hit = true;
+ }
+ if (!coveredMem.contains(keyword) && memNames.contains(keyword)) {
+ rows.append({QString(), QString(), keyword, memNames.value(keyword)});
+ coveredMem.insert(keyword);
+ hit = true;
+ }
+ } else {
+ // 模糊匹配: 包含即命中(不区分大小写); 映射命中→整行, 库命中→单侧行
+ for (const auto& pair : mapping) {
+ if (pair.first.contains(keyword, Qt::CaseInsensitive) ||
+ pair.second.contains(keyword, Qt::CaseInsensitive)) {
+ appendFullRow(pair.first, pair.second);
+ hit = true;
+ }
+ }
+ for (auto it = uf20Names.constBegin(); it != uf20Names.constEnd(); ++it) {
+ if (!coveredUf20.contains(it.key()) &&
+ it.key().contains(keyword, Qt::CaseInsensitive)) {
+ rows.append({it.key(), it.value(), QString(), QString()});
+ coveredUf20.insert(it.key());
+ hit = true;
+ }
+ }
+ for (auto it = memNames.constBegin(); it != memNames.constEnd(); ++it) {
+ if (!coveredMem.contains(it.key()) &&
+ it.key().contains(keyword, Qt::CaseInsensitive)) {
+ rows.append({QString(), QString(), it.key(), it.value()});
+ coveredMem.insert(it.key());
+ hit = true;
+ }
+ }
+ }
+ if (!hit) {
+ noMatchKeywords.append(keyword);
+ }
+ }
+
+ if (rows.isEmpty()) {
+ if (m_tipLabel) {
+ m_tipLabel->setText("⚠️ 未找到匹配的表,请确认表名是否正确(UF20 表与内存表为两套命名)");
+ }
+ return;
+ }
+
+ // 无映射行数: UF20 表名或内存表名任一侧为空即视为无映射关系;
+ // 中文名缺失: 表名侧有值但缓存库查不到该表(元数据更新未覆盖其来源工程),
+ // 中文名无从取得, 只能留空并在 tip 中提示数量
+ int unmappedCount = 0;
+ int cnameMissingCount = 0;
+ for (const TableRow& row : rows) {
+ if (row.uf20Name.isEmpty() || row.memName.isEmpty()) {
+ ++unmappedCount;
+ }
+ if ((!row.uf20Name.isEmpty() && row.uf20Cname.isEmpty()) ||
+ (!row.memName.isEmpty() && row.memCname.isEmpty())) {
+ ++cnameMissingCount;
+ }
+ }
+
+ // 查询成功: tip 显示统计(一对多映射会产生多条记录); 映射读取失败时附加降级说明
+ if (m_tipLabel) {
+ QString tip = QString("已查询 %1 条记录").arg(rows.size());
+ if (unmappedCount > 0) {
+ tip += QString("(其中 %1 个无映射关系").arg(unmappedCount);
+ if (cnameMissingCount > 0) {
+ tip += QString(",%1 个表中文名缺失(未在缓存库收录)").arg(cnameMissingCount);
+ }
+ tip += ")";
+ } else if (cnameMissingCount > 0) {
+ tip += QString("(%1 个表中文名缺失,未在缓存库收录)").arg(cnameMissingCount);
+ }
+ if (!noMatchKeywords.isEmpty()) {
+ tip += QString(",%1 个关键词无匹配").arg(noMatchKeywords.size());
+ }
+ m_tipLabel->setText(tip);
+ }
+
+ // 结果按 UF20 表名排序(不区分大小写); 仅内存表命中的行(UF20 名为空)排末尾;
+ // 同一 UF20 表的一对多记录按内存表名排序, 保证相邻展示
+ std::sort(rows.begin(), rows.end(), [](const TableRow& a, const TableRow& b) {
+ if (a.uf20Name.isEmpty() != b.uf20Name.isEmpty()) {
+ return b.uf20Name.isEmpty(); // a 的 UF20 名非空在前
+ }
+ const QString an = a.uf20Name.isEmpty() ? a.memName : a.uf20Name;
+ const QString bn = b.uf20Name.isEmpty() ? b.memName : b.uf20Name;
+ const int primary = an.compare(bn, Qt::CaseInsensitive);
+ if (primary != 0) {
+ return primary < 0;
+ }
+ return a.memName.compare(b.memName, Qt::CaseInsensitive) < 0;
+ });
+
+ // 批量填充期间抑制逐行重绘(与功能查询页一致): 关闭更新后仅在填充完成时整体呈现
+ m_resultTable->setUpdatesEnabled(false);
+ for (const TableRow& row : rows) {
+ const QString cells[4] = {row.uf20Name, row.uf20Cname, row.memName, row.memCname};
+ QList rowItems;
+ for (const QString& cell : cells) {
+ QStandardItem *item = new QStandardItem(cell);
+ item->setFlags(item->flags() & ~Qt::ItemIsEditable);
+ item->setTextAlignment(Qt::AlignCenter); // 列内容居中
+ rowItems.append(item);
+ }
+ m_tableModel->appendRow(rowItems);
+ }
+ m_resultTable->setUpdatesEnabled(true);
+}
diff --git a/src/pages/functionsearch/tablesearchpage.h b/src/pages/functionsearch/tablesearchpage.h
new file mode 100644
index 0000000..2cff7ad
--- /dev/null
+++ b/src/pages/functionsearch/tablesearchpage.h
@@ -0,0 +1,57 @@
+#ifndef TABLESEARCHPAGE_H
+#define TABLESEARCHPAGE_H
+
+#include
+#include
+#include "components/elasearchedit.h"
+#include "ElaTableView.h"
+
+class QLabel;
+
+// UF20 & 内存表映射查询页(原"表查询"): 检索 UF20(2.0) 表与内存表(UFT3)之间的映射关系。
+// 结果四列: UF20表名 / UF20表中文名(uf20_cache.db 按 key 取) /
+// 内存表名(table_mapping.json 映射的 value) / 内存表中文名(uft3_cache.db 按 value 取)。
+// 输入 UF20 表名或内存表名均可检索, 支持精确/模糊(ElaSearchEdit 内置开关)两种模式;
+// 无映射命中的表仅展示数据库侧字段, 映射相关列置空。
+// 双击有完整映射的记录行可添加到「数据映射 → 表替换」页(经 MainWindow 接线, 追加后自动落盘)。
+// 不继承 FunctionSearchPageBase: 结果结构为 4 列表信息, 与 FunctionInfo(3 列函数信息)
+// 结构不同, 数据源与交互(无批量加入)亦不同, 强行复用基类不值
+class TableSearchPage : public QWidget
+{
+ Q_OBJECT
+
+public:
+ explicit TableSearchPage(QWidget *parent = nullptr);
+ // 双击添加的回调结果(经 MainWindow 接线后回传): error 为空即添加成功, 非空为原因
+ void onMappingAddResult(const QString& error);
+
+signals:
+ // 双击有完整映射的记录行 → 请求把 (UF20表名, 内存表名) 添加到表替换页
+ void requestAddMapping(const QString& uf20Name, const QString& memName);
+
+private slots:
+ void onSearch();
+ void onExactMatchToggled(bool checked);
+ // 双击记录行: 有完整映射发出 requestAddMapping, 单侧行仅 tip 提示
+ void onDoubleClicked(const QModelIndex& index);
+ // tip 标签主题感知(Ela 原生控件颜色不随主题切换, 需手动跟随)
+ void updateTipStyle();
+
+private:
+ void initUI();
+ // 读取表映射文件(Constants::tableMappingJson(), 即 bin/uf2touft3/config/table_mapping.json,
+ // 顶层键 tableCompareUf2_Uf3: key=UF20表名, value=内存表名; value 可为字符串(一对一)
+ // 或字符串数组(一对多, 拆为多条映射对))。
+ // readOk 出参: 文件读取/解析成功为 true(映射本身可为空), 失败为 false;
+ // errorOut 出参: 失败时带回可展示的错误原因(含行号, 常见手误如末项尾随逗号)
+ QList> loadTableMapping(bool* readOk,
+ QString* errorOut = nullptr) const;
+
+ ElaSearchEdit *m_searchEdit;
+ ElaTableView *m_resultTable;
+ QStandardItemModel *m_tableModel;
+ QLabel *m_tipLabel;
+ bool m_exactMatch;
+};
+
+#endif
diff --git a/src/pages/nonlsconvert/nonlsconvertpage.h b/src/pages/nonlsconvert/nonlsconvertpage.h
index 0fd1623..7214b8b 100644
--- a/src/pages/nonlsconvert/nonlsconvertpage.h
+++ b/src/pages/nonlsconvert/nonlsconvertpage.h
@@ -16,7 +16,7 @@ class QStandardItem;
// 调用契约:仅携带 -m 2(按需转码模式);函数清单与递归开关由引擎直接读取 cust_nonls.json,
// 不再需要 -t/-o 等命令行参数
// 历史格式兼容:旧版条目为数值(无 dialect)按"否/mdb"兜底;更早期字符串数组(无递归
-// 标志)统一按"否/mdb"兜底。格式契约见 docs/转码配置格式契约.md
+// 标志)统一按"否/mdb"兜底。格式契约见 详细设计文档.md §7.5
class NonLsConvertPage : public ConvertWorkbenchBase
{
Q_OBJECT
diff --git a/src/utils/Constants.h b/src/utils/Constants.h
index 3755935..11c1a0d 100644
--- a/src/utils/Constants.h
+++ b/src/utils/Constants.h
@@ -66,6 +66,7 @@ namespace ToolFiles {
inline const QString Cust2Json = "config/cust2.json";
inline const QString CustNonLsJson = "config/cust_nonls.json"; // 按需转码 UI 清单持久化
inline const QString NonLsRunList = "config/run_nonls_list.txt"; // 大批量时生成的 @列表文件
+ inline const QString TableMappingJson = "config/table_mapping.json"; // 表查询: UF20表↔内存表映射文件
}
// ---------------------------------------------------------------------------
@@ -180,7 +181,7 @@ namespace DesignExts {
// ---------------------------------------------------------------------------
// convert.dir 条目字段与取值(V2 对象格式, 全量/按需转码页与代码检索清单读取共用,
-// 拼错会静默丢字段/错方言; 契约见 docs/转码配置格式契约.md)
+// 拼错会静默丢字段/错方言; 契约见 详细设计文档.md §7.5)
// ---------------------------------------------------------------------------
namespace DirEntry {
inline const QString Name = "name"; // 功能短名+设计器扩展名(全量转码)
@@ -235,6 +236,9 @@ namespace Timings {
inline const int UpdateCheckDelayMs = 3000; // 启动后延迟检查版本更新
inline const int JsonDebounceMs = 300; // JSON 配置外部修改防抖
inline const int JsonSaveWindowResetMs = 2000; // 保存窗口异常路径的自动复位兜底
+ inline const int JsonQuietWindowMs = 1500; // 保存后静默窗: QSaveFile 替换文件引发的
+ // 目录事件风暴/杀软扫描 touch 造成的 mtime
+ // 漂移视为保存余波, 不判为外部修改重载
// 转换引擎整体运行超时: 超时自动走 stop() 升级链终止疑似卡死的引擎,
// 防止任务无限挂起(全量大工程正常耗时可观的场景下取宽裕值)
inline const int CacheWarmupWaitMs = 5000; // 功能缓存后台预热的主线程有界等待上限(超限转同步重建)
@@ -267,6 +271,7 @@ inline QString toolExe() { return toolDir() + "/" + ToolFiles::Exe; }
inline QString configIni() { return toolDir() + "/" + ToolFiles::ConfigIni; }
inline QString custJson() { return toolDir() + "/" + ToolFiles::CustJson; }
inline QString custNonLsJson() { return toolDir() + "/" + ToolFiles::CustNonLsJson; }
+inline QString tableMappingJson() { return toolDir() + "/" + ToolFiles::TableMappingJson; }
inline QString cacheDir() { return toolDir() + "/" + Paths::CacheDir; } // bin/uf2touft3/cache
inline QString uf20CacheDb() { return cacheDir() + "/" + Cache::Uf20DbFile; }
inline QString uft3CacheDb() { return cacheDir() + "/" + Cache::Uft3DbFile; }
diff --git a/src/utils/callchainanalyzer.cpp b/src/utils/callchainanalyzer.cpp
index 2e54eab..8dd02a1 100644
--- a/src/utils/callchainanalyzer.cpp
+++ b/src/utils/callchainanalyzer.cpp
@@ -102,6 +102,7 @@ bool CallChainAnalyzer::buildIndex(QString *errorMessage)
{
m_chineseNameIndex.clear();
m_canonicalNameIndex.clear();
+ m_objectIdIndex.clear();
m_allInfos.clear();
m_indexed = false;
@@ -159,6 +160,10 @@ bool CallChainAnalyzer::buildIndex(QString *errorMessage)
if (!info->canonicalName.isEmpty()) {
insertPrefer(m_canonicalNameIndex, info->canonicalName, info);
}
+ // 按功能号索引(同号多文件时全量保留, 查询方按序取用)
+ if (!info->objectId.isEmpty()) {
+ m_objectIdIndex[info->objectId].append(info);
+ }
// 登记全量集合(saveToCache 导出底座; 同名落选变体也保留,
// 保证重启后 loadFromCache 重建的索引与内存一致)
m_allInfos.append(info);
@@ -350,6 +355,7 @@ bool CallChainAnalyzer::loadFromCache(const QString &path, QString *message, boo
{
m_chineseNameIndex.clear();
m_canonicalNameIndex.clear();
+ m_objectIdIndex.clear();
m_allInfos.clear();
m_indexed = false;
@@ -415,6 +421,10 @@ bool CallChainAnalyzer::loadFromCache(const QString &path, QString *message, boo
if (!info->canonicalName.isEmpty()) {
insertPrefer(m_canonicalNameIndex, info->canonicalName, info);
}
+ // 按功能号索引, 与 buildIndex 构建口径一致
+ if (!info->objectId.isEmpty()) {
+ m_objectIdIndex[info->objectId].append(info);
+ }
// 缓存导出顺序即加载顺序, 全量集合与索引同步重建
m_allInfos.append(info);
++count;
@@ -462,6 +472,19 @@ CallChainAnalyzer::lookupBest(const QString &name) const
return nullptr;
}
+QList>
+CallChainAnalyzer::lookupByObjectId(const QString &objectId) const
+{
+ // QSharedPointer 到 const 版本的隐式转换无 QList 级快捷路径, 逐个转
+ QList> result;
+ const QList> hits = m_objectIdIndex.value(objectId);
+ result.reserve(hits.size());
+ for (const QSharedPointer &info : hits) {
+ result.append(info);
+ }
+ return result;
+}
+
CallChainAnalyzer::TreeNode
CallChainAnalyzer::buildCallTree(const QString &startChineseName, int maxDepth) const
{
diff --git a/src/utils/callchainanalyzer.h b/src/utils/callchainanalyzer.h
index 98998b9..2fda1d1 100644
--- a/src/utils/callchainanalyzer.h
+++ b/src/utils/callchainanalyzer.h
@@ -62,6 +62,11 @@ public:
// 与 resolveNode 使用同一解析入口,供外部还原节点的展示名(如蓝底高亮匹配)。
QSharedPointer lookupBest(const QString &name) const;
+ // 按功能号(objectId)查找索引中的函数, 返回全部同名功能号候选(一个功能号可能
+ // 对应多个接口文件)。元数据接口扫描会排除 cbp/ucbp 目录(moduleGenerationCheck
+ // 开启时)且同名中文名接口互相覆盖, 部分功能号在元数据中缺失, 代码索引无此限制。
+ QList> lookupByObjectId(const QString &objectId) const;
+
// 构建从 startChineseName 出发的调用链路树(下游:被其调用的函数)
TreeNode buildCallTree(const QString &startChineseName, int maxDepth = 32) const;
@@ -98,6 +103,9 @@ private:
QString m_rootPath;
QHash> m_chineseNameIndex;
QHash> m_canonicalNameIndex;
+ // 功能号 -> 函数列表: 一个功能号可能挂在多个接口文件上(不同模块同名 objectId),
+ // 与 chineseNameIndex 同口径(仅 chineseName 非空的文件), 两条构建路径同步维护
+ QHash>> m_objectIdIndex;
// 全量函数集合(buildIndex/loadFromCache 两条构建路径的共同底座):
// saveToCache 的单一导出事实来源, 天然无重复, 索引哈希则允许同名落选变体缺席
QList> m_allInfos;
diff --git a/src/utils/datacache.cpp b/src/utils/datacache.cpp
index 491d749..2dcf527 100644
--- a/src/utils/datacache.cpp
+++ b/src/utils/datacache.cpp
@@ -697,6 +697,9 @@ void DataCache::refreshFunctionCache()
m_uf20FuncCacheValid = false;
m_uft3FuncCacheValid = false;
+ // 表查询"表名→中文名"缓存一并失效(同源于本次元数据落库)
+ m_uf20TableNameMapValid = false;
+ m_uft3TableNameMapValid = false;
// 触发后台重建, 不阻塞主线程; 后续查询在预热完成前按原全表路径兜底
warmupFunctionCacheAsync(Constants::Cache::Uf20DbName);
warmupFunctionCacheAsync(Constants::Cache::Uft3DbName);
@@ -750,3 +753,79 @@ QList DataCache::searchUFT3Functions(const QStringList& keywords,
{
return searchFunctions(Constants::Cache::Uft3DbName, keywords, field, exactMatch, keywordsWithoutMatch);
}
+
+// ---- 表查询: 表名→中文名轻量映射(uf2_table / uft3_table) ----
+
+// 构建指定库的"表名→中文名"全量轻量映射。
+// data 列为完整表定义 JSON(单行可达数十 KB), 不能 getAllRows 整表搬运后逐行解析,
+// 优先在 SQL 层用 json_extract 只提取中文名字段(传输量降为表名+中文名);
+// json_extract 不可用(SQL 执行失败)时回退 getAllRows + C++ 逐行解析兜底。
+// 可在任意线程调用: database() 按线程建立专用连接, 与主线程查询互不阻塞
+QMap DataCache::buildTableNameMap(const QString& db, const QString& table,
+ const QString& cnameField, bool* ok) const
+{
+ if (ok) *ok = false;
+ QMap result;
+ QSqlDatabase conn = database(db, /*createIfMissing=*/false);
+ if (!conn.isOpen()) {
+ return result;
+ }
+ QSqlQuery query(conn);
+ // cnameField 为内部常量字面量("cname"/"chineseName"), 非外部输入, 无注入风险
+ query.prepare(QString("SELECT name, json_extract(data, '$.%1') FROM %2").arg(cnameField, table));
+ if (query.exec()) {
+ while (query.next()) {
+ result[query.value(0).toString()] = query.value(1).toString();
+ }
+ if (ok) *ok = true;
+ return result;
+ }
+ LogManager::instance()->logWarning(QString("数据缓存 - json_extract 提取 %1.%2 失败: %3, 回退全表解析")
+ .arg(db, cnameField, query.lastError().text()));
+ bool rowsOk = false;
+ const QMap rows = getAllRows(db, table, &rowsOk);
+ if (!rowsOk) {
+ return result; // 库未打开/查询失败: 返回空表交由调用方重试
+ }
+ for (auto it = rows.constBegin(); it != rows.constEnd(); ++it) {
+ const QJsonObject obj = QJsonDocument::fromJson(it.value().toUtf8()).object();
+ result[it.key()] = obj.value(cnameField).toString();
+ }
+ if (ok) *ok = true;
+ return result;
+}
+
+// 懒加载并缓存指定库的"表名→中文名"映射。首次调用全量构建(表数量远小于接口表,
+// 秒级内完成), 之后直接返回内存副本; 构建失败不置有效, 下次查询自动重试,
+// 避免空缓存被标有效后表查询永远返回空结果
+QMap DataCache::getUf20TableNameMap()
+{
+ if (m_uf20TableNameMapValid) {
+ return m_uf20TableNameMap;
+ }
+ bool ok = false;
+ const QMap map = buildTableNameMap(
+ Constants::Cache::Uf20DbName, Constants::Cache::TblUf2Table, QStringLiteral("cname"), &ok);
+ if (ok) {
+ m_uf20TableNameMap = map;
+ m_uf20TableNameMapValid = true;
+ LogManager::instance()->logInfo(QString("数据缓存 - 重建 UF20 表名缓存 %1 条").arg(map.size()));
+ }
+ return map;
+}
+
+QMap DataCache::getUft3TableNameMap()
+{
+ if (m_uft3TableNameMapValid) {
+ return m_uft3TableNameMap;
+ }
+ bool ok = false;
+ const QMap map = buildTableNameMap(
+ Constants::Cache::Uft3DbName, Constants::Cache::TblUft3Table, QStringLiteral("chineseName"), &ok);
+ if (ok) {
+ m_uft3TableNameMap = map;
+ m_uft3TableNameMapValid = true;
+ LogManager::instance()->logInfo(QString("数据缓存 - 重建 UFT3 表名缓存 %1 条").arg(map.size()));
+ }
+ return map;
+}
diff --git a/src/utils/datacache.h b/src/utils/datacache.h
index c32373c..8e46067 100644
--- a/src/utils/datacache.h
+++ b/src/utils/datacache.h
@@ -87,6 +87,15 @@ public:
QList searchUFT3Functions(const QStringList& keywords, const QString& field = "",
bool exactMatch = false, int* keywordsWithoutMatch = nullptr);
+ // ---- 表查询 (表名→中文名映射, 从 uf2_table / uft3_table 读取) ----
+ // 表名→中文名全量轻量映射: UF20 取 data.cname, 内存表取 data.chineseName。
+ // data 列为完整表定义 JSON(单行可达数十 KB), 不能整表搬运后逐行解析,
+ // SQL 层用 json_extract 只取中文名字段; 首次调用全量构建后内存缓存,
+ // 元数据落库后随 refreshFunctionCache 一并失效重建。
+ // 查询失败(库未打开/SQL 出错)返回空表且不缓存, 下次调用自动重试
+ QMap getUf20TableNameMap();
+ QMap getUft3TableNameMap();
+
// 重新构建功能查询内存缓存(元数据落库后调用, 使后续查询走缓存而非全表扫描)
void refreshFunctionCache();
@@ -126,6 +135,11 @@ private:
QList searchFunctions(const QString& db, const QStringList& keywords,
const QString& field, bool exactMatch, int* keywordsWithoutMatch);
+ // 构建指定库的"表名→中文名"全量轻量映射(仅库名/表名/中文字段名不同, 成对方法一行转发)。
+ // 可在任意线程调用: database() 按线程建立专用连接, 与主线程查询互不阻塞
+ QMap buildTableNameMap(const QString& db, const QString& table,
+ const QString& cnameField, bool* ok = nullptr) const;
+
// 按线程缓存的连接(QThreadStorage 线程本地存储)。连接只能在创建它的线程中使用,
// 线程退出时 ScopedConnection 析构自动 removeDatabase——避免 QtConcurrent 空闲线程
// 回收后 QThread* 地址被系统复用时, 拿到已死线程的脏连接(写入静默失败)并泄漏注册表条目。
@@ -151,6 +165,12 @@ private:
mutable bool m_uf20FuncCacheValid = false;
mutable bool m_uft3FuncCacheValid = false;
+ // 表查询"表名→中文名"内存缓存(UF20/UFT3 各一份, 落库后随 refreshFunctionCache 失效重建)
+ mutable QMap m_uf20TableNameMap;
+ mutable QMap m_uft3TableNameMap;
+ mutable bool m_uf20TableNameMapValid = false;
+ mutable bool m_uft3TableNameMapValid = false;
+
// 功能缓存的在途后台预热任务(结果暂存 QFuture, 由主线程 cachedFunctions 接管,
// 共享成员的读写仍全部发生在主线程, 无需额外加锁)
mutable QFuture> m_uf20WarmupFuture;
diff --git a/src/utils/jsonfilemonitor.cpp b/src/utils/jsonfilemonitor.cpp
index ca05cf0..31a7a90 100644
--- a/src/utils/jsonfilemonitor.cpp
+++ b/src/utils/jsonfilemonitor.cpp
@@ -3,6 +3,7 @@
#include
#include
+#include
#include "logmanager.h"
JsonFileMonitor::JsonFileMonitor(QObject *parent)
@@ -97,6 +98,10 @@ void JsonFileMonitor::endSave()
// 以保存后的新 mtime 为基准(writeCustJsonOrdered 同步写完才返回),
// 否则保存后的 mtime > 旧基准会误触发一次重载
m_lastMtime = currentMtime();
+ // 开启保存余波静默窗: 替换文件后的目录事件风暴/杀软扫描 touch 会让 mtime
+ // 在基准之后继续漂移, 曾被 onReloadTimeout 误判为外部修改引发页面重建两轮
+ m_quietUntilMs = QDateTime::currentMSecsSinceEpoch()
+ + Constants::Timings::JsonQuietWindowMs;
refreshWatch();
}
@@ -121,6 +126,13 @@ void JsonFileMonitor::onDirChanged(const QString &path)
void JsonFileMonitor::onReloadTimeout()
{
+ // 保存余波静默期: 挂接失败重试/杀软扫描 touch 造成的 mtime 漂移属于保存本身,
+ // 只刷新基准并补挂监控, 不触发重载; 静默期后恢复正常外部修改判定
+ if (QDateTime::currentMSecsSinceEpoch() < m_quietUntilMs) {
+ m_lastMtime = currentMtime();
+ refreshWatch();
+ return;
+ }
const qint64 mtime = currentMtime();
if (mtime <= 0) {
refreshWatch();
diff --git a/src/utils/jsonfilemonitor.h b/src/utils/jsonfilemonitor.h
index 72147b2..83bb91a 100644
--- a/src/utils/jsonfilemonitor.h
+++ b/src/utils/jsonfilemonitor.h
@@ -15,6 +15,9 @@
// 3. mtime 基准过滤:自身写入(保存)引发的 mtime 变化不触发重载
// 4. 保存窗口语义:beginSaveWindow()/endSave() 包住保存动作,
// 窗口期内监控事件直接忽略;窗口结束后 m_saveSeq 代数立即失效
+// 5. 保存余波静默窗:endSave 后短窗内(QuietWindowMs) QSaveFile 替换目标文件
+// 引发的目录事件风暴/杀软扫描 touch 造成的 mtime 漂移只刷新基准不重载,
+// 防止自身保存被误判为外部修改导致页面重建两轮
//
// 用法(页面侧):
// m_monitor = new JsonFileMonitor(this);
@@ -61,6 +64,7 @@ private:
qint64 m_lastMtime; // mtime 基准:<= 基准的变化视为自身写入回环
bool m_isSaving; // 保存窗口期内为 true
quint32 m_saveSeq; // 保存代数:begin/end 各 +1,使窗口内事件过期
+ qint64 m_quietUntilMs = 0; // 保存余波静默窗截止时刻(epoch ms), 期间 mtime 漂移只刷基准
};
#endif // JSONFILEMONITOR_H