提交
This commit is contained in:
1 parent
c3bd2c00ae
commit
a52fe77e34
39 files changed
+609647
-9238
No files matched your search
@@ -191,40 +191,36 @@
|
||||
"189": "Community 189",
|
||||
"190": "error-handle.ts",
|
||||
"191": "formatYmd",
|
||||
"192": "loadDefaultCustomerOptions",
|
||||
"193": "fetchData",
|
||||
"194": "fetchData",
|
||||
"192": "prepareLineAliasAndBrandForSubmit",
|
||||
"193": "crm-product-display.ts",
|
||||
"194": "loadDefaultCustomerOptions",
|
||||
"195": "Community 195",
|
||||
"196": "Community 196",
|
||||
"197": "handleEdit",
|
||||
"198": "fetchData",
|
||||
"199": "fetchData",
|
||||
"199": "price-letter-template.ts",
|
||||
"200": "index.js",
|
||||
"201": "fetchCustomerDetail",
|
||||
"201": "contractType.ts",
|
||||
"202": "Community 202",
|
||||
"203": "menuFavorite.ts",
|
||||
"203": "price-letter.ts",
|
||||
"204": "fetchData",
|
||||
"205": "index.vue",
|
||||
"206": "price-letter.ts",
|
||||
"205": "menuFavorite.ts",
|
||||
"206": "index.vue",
|
||||
"207": "emit",
|
||||
"208": "handleSearch",
|
||||
"208": "v2.5.7 - 2025-09-14",
|
||||
"209": "Community 209",
|
||||
"210": "Community 210",
|
||||
"211": "loadUserOptions",
|
||||
"212": "index.js",
|
||||
"213": "Community 213",
|
||||
"214": "Community 214",
|
||||
"215": "handleDelete",
|
||||
"216": "handleSearch",
|
||||
"215": "index.vue",
|
||||
"216": "zh_CN.js",
|
||||
"217": "index.js",
|
||||
"218": "index.js",
|
||||
"219": "handleAdd",
|
||||
"220": "index.vue",
|
||||
"221": "getInventoryStatCellValue",
|
||||
"222": "handleEdit",
|
||||
"223": "Community 223",
|
||||
"224": "Community 224",
|
||||
"225": "zh_CN.js",
|
||||
"226": "form.vue",
|
||||
"227": "Community 227",
|
||||
"228": "Community 228",
|
||||
|
||||
@@ -1 +1 @@
|
||||
.
|
||||
/Users/wuzilong/work/00code/shanxia2.0/web-admin
|
||||
@@ -0,0 +1,801 @@
|
||||
{
|
||||
"0": "Community 0",
|
||||
"1": "Community 1",
|
||||
"2": "Community 2",
|
||||
"3": "Community 3",
|
||||
"4": "Community 4",
|
||||
"5": "Community 5",
|
||||
"6": "Community 6",
|
||||
"7": "Community 7",
|
||||
"8": "Community 8",
|
||||
"9": "Community 9",
|
||||
"10": "Community 10",
|
||||
"11": "Community 11",
|
||||
"12": "Community 12",
|
||||
"13": "Community 13",
|
||||
"14": "Community 14",
|
||||
"15": "Community 15",
|
||||
"16": "Community 16",
|
||||
"17": "Ha",
|
||||
"18": "Community 18",
|
||||
"19": "Community 19",
|
||||
"20": "Community 20",
|
||||
"21": "Community 21",
|
||||
"22": "Community 22",
|
||||
"23": "Community 23",
|
||||
"24": "Community 24",
|
||||
"25": "Community 25",
|
||||
"26": "theme.min.js",
|
||||
"27": "Community 27",
|
||||
"28": "useCommon",
|
||||
"29": "Community 29",
|
||||
"30": "loader.ts",
|
||||
"31": "RenewDialog.vue",
|
||||
"32": "index.vue",
|
||||
"33": "Community 33",
|
||||
"34": "main.ts",
|
||||
"35": "Community 35",
|
||||
"36": "VoidDialog.vue",
|
||||
"37": "pl-batch-print.ts",
|
||||
"38": "view-fields.ts",
|
||||
"39": "Community 39",
|
||||
"40": "Community 40",
|
||||
"41": "colors.ts",
|
||||
"42": "Community 42",
|
||||
"43": "Community 43",
|
||||
"44": "Community 44",
|
||||
"45": "Community 45",
|
||||
"46": "Community 46",
|
||||
"47": "fetchCustomerDetail",
|
||||
"48": "Community 48",
|
||||
"49": "Community 49",
|
||||
"50": "Community 50",
|
||||
"51": "price-letter-template.ts",
|
||||
"52": "Community 52",
|
||||
"53": "Community 53",
|
||||
"54": "Community 54",
|
||||
"55": "Community 55",
|
||||
"56": "Community 56",
|
||||
"57": "Community 57",
|
||||
"58": "Community 58",
|
||||
"59": "fetchData",
|
||||
"60": "index.vue",
|
||||
"61": "Community 61",
|
||||
"62": "highlight.ts",
|
||||
"63": "price-letter.ts",
|
||||
"64": "Community 64",
|
||||
"65": "detail.vue",
|
||||
"66": "Community 66",
|
||||
"67": "index.vue",
|
||||
"68": "Community 68",
|
||||
"69": "Community 69",
|
||||
"70": "Community 70",
|
||||
"71": "Community 71",
|
||||
"72": "Community 72",
|
||||
"73": "Community 73",
|
||||
"74": "Community 74",
|
||||
"75": "openEdit",
|
||||
"76": "handleSearch",
|
||||
"77": "loadUserOptions",
|
||||
"78": "customerPool.ts",
|
||||
"79": "Community 79",
|
||||
"80": "product.ts",
|
||||
"81": "Community 81",
|
||||
"82": "Community 82",
|
||||
"83": "useDictStore",
|
||||
"84": "Community 84",
|
||||
"85": "index.vue",
|
||||
"86": "Community 86",
|
||||
"87": "canDeleteByCreator",
|
||||
"88": "index.vue",
|
||||
"89": "detail.vue",
|
||||
"90": "theme.min.js",
|
||||
"91": "Community 91",
|
||||
"92": "Community 92",
|
||||
"93": "ArtUserMenu.vue",
|
||||
"94": "formatYmd",
|
||||
"95": "Community 95",
|
||||
"96": "Community 96",
|
||||
"97": "stringifyBody",
|
||||
"98": "index.vue",
|
||||
"99": "stringifyBody",
|
||||
"100": "v2.4.2.1 - 2025-06-16",
|
||||
"101": "price-letter.ts",
|
||||
"102": "price-letter-template.ts",
|
||||
"103": "SettingDrawer.vue",
|
||||
"104": "contractType.ts",
|
||||
"105": "index.vue",
|
||||
"106": "Community 106",
|
||||
"107": "Community 107",
|
||||
"108": "Community 108",
|
||||
"109": "Community 109",
|
||||
"110": "Community 110",
|
||||
"111": "format.ts",
|
||||
"112": "inventory.ts",
|
||||
"113": "Community 113",
|
||||
"114": "index.vue",
|
||||
"115": "useDictStore",
|
||||
"116": "Community 116",
|
||||
"117": "Community 117",
|
||||
"118": "zh_CN.js",
|
||||
"119": "Community 119",
|
||||
"120": "Community 120",
|
||||
"121": "chart.ts",
|
||||
"122": "handleDisburse",
|
||||
"123": "detail.vue",
|
||||
"124": "index.vue",
|
||||
"125": "Community 125",
|
||||
"126": "Community 126",
|
||||
"127": "selectContractRow",
|
||||
"128": "Community 128",
|
||||
"129": "Community 129",
|
||||
"130": "system.ts",
|
||||
"131": "index.vue",
|
||||
"132": "Community 132",
|
||||
"133": "Community 133",
|
||||
"134": "index.vue",
|
||||
"135": "detail.vue",
|
||||
"136": "Community 136",
|
||||
"137": "Community 137",
|
||||
"138": "FollowDialog.vue",
|
||||
"139": "useTableColumns.ts",
|
||||
"140": "Community 140",
|
||||
"141": "Community 141",
|
||||
"142": "Community 142",
|
||||
"143": "index.js",
|
||||
"144": "Community 144",
|
||||
"145": "api",
|
||||
"146": "price-letter-template.ts",
|
||||
"147": "contractType.ts",
|
||||
"148": "Community 148",
|
||||
"149": "fetchCustomerDetail",
|
||||
"150": "Community 150",
|
||||
"151": "Community 151",
|
||||
"152": "SettingDrawer.vue",
|
||||
"153": "index.vue",
|
||||
"154": "index.vue",
|
||||
"155": "error.ts",
|
||||
"156": "v2.4.2.1 - 2025-06-16",
|
||||
"157": "form.vue",
|
||||
"158": "removeProductsNotAllowedByHazard",
|
||||
"159": "detail.vue",
|
||||
"160": "index.vue",
|
||||
"161": "Community 161",
|
||||
"162": "Community 162",
|
||||
"163": "system.ts",
|
||||
"164": "Community 164",
|
||||
"165": "handleDisburse",
|
||||
"166": "Community 166",
|
||||
"167": "Community 167",
|
||||
"168": "crypto.ts",
|
||||
"169": "Community 169",
|
||||
"170": "Community 170",
|
||||
"171": "index.vue",
|
||||
"172": "index.js",
|
||||
"173": "index.vue",
|
||||
"174": "Community 174",
|
||||
"175": "handleEdit",
|
||||
"176": "fetchData",
|
||||
"177": "fetchCustomerDetail",
|
||||
"178": "Community 178",
|
||||
"179": "Community 179",
|
||||
"180": "index.vue",
|
||||
"181": "Community 181",
|
||||
"182": "Community 182",
|
||||
"183": "Ha",
|
||||
"184": "highlight.ts",
|
||||
"185": "handleConfirmApproval",
|
||||
"186": "StorageKeyManager",
|
||||
"187": "Community 187",
|
||||
"188": "Community 188",
|
||||
"189": "Community 189",
|
||||
"190": "error-handle.ts",
|
||||
"191": "formatYmd",
|
||||
"195": "Community 195",
|
||||
"196": "Community 196",
|
||||
"197": "handleEdit",
|
||||
"198": "fetchData",
|
||||
"200": "index.js",
|
||||
"202": "Community 202",
|
||||
"204": "fetchData",
|
||||
"207": "emit",
|
||||
"209": "Community 209",
|
||||
"210": "Community 210",
|
||||
"211": "loadUserOptions",
|
||||
"212": "index.js",
|
||||
"213": "Community 213",
|
||||
"214": "Community 214",
|
||||
"217": "index.js",
|
||||
"218": "index.js",
|
||||
"219": "handleAdd",
|
||||
"223": "Community 223",
|
||||
"224": "Community 224",
|
||||
"226": "form.vue",
|
||||
"227": "Community 227",
|
||||
"228": "Community 228",
|
||||
"233": "Community 233",
|
||||
"234": "Community 234",
|
||||
"239": "Community 239",
|
||||
"242": "Community 242",
|
||||
"243": "Community 243",
|
||||
"247": "Community 247",
|
||||
"248": "Community 248",
|
||||
"251": "handleDisburse",
|
||||
"253": "Community 253",
|
||||
"254": "Community 254",
|
||||
"255": "Community 255",
|
||||
"259": "Community 259",
|
||||
"260": "Community 260",
|
||||
"261": "Community 261",
|
||||
"265": "Community 265",
|
||||
"269": "Community 269",
|
||||
"270": "Community 270",
|
||||
"280": "Community 280",
|
||||
"283": "Community 283",
|
||||
"284": "Community 284",
|
||||
"285": "Community 285",
|
||||
"286": "Community 286",
|
||||
"292": "Community 292",
|
||||
"295": "Community 295",
|
||||
"296": "Community 296",
|
||||
"302": "Community 302",
|
||||
"303": "Community 303",
|
||||
"304": "Community 304",
|
||||
"305": "isIoPlanFrozen",
|
||||
"306": "handleArrange",
|
||||
"307": "Community 307",
|
||||
"308": "Community 308",
|
||||
"309": "Community 309",
|
||||
"310": "Community 310",
|
||||
"315": "Community 315",
|
||||
"316": "Community 316",
|
||||
"317": "Community 317",
|
||||
"318": "Community 318",
|
||||
"319": "Community 319",
|
||||
"320": "Community 320",
|
||||
"321": "Community 321",
|
||||
"322": "Community 322",
|
||||
"323": "Community 323",
|
||||
"324": "Community 324",
|
||||
"325": "Community 325",
|
||||
"326": "Community 326",
|
||||
"327": "Community 327",
|
||||
"328": "Community 328",
|
||||
"329": "Community 329",
|
||||
"330": "Community 330",
|
||||
"331": "Community 331",
|
||||
"332": "Community 332",
|
||||
"333": "Community 333",
|
||||
"334": "Community 334",
|
||||
"335": "Community 335",
|
||||
"336": "Community 336",
|
||||
"337": "Community 337",
|
||||
"338": "Community 338",
|
||||
"339": "Community 339",
|
||||
"340": "Community 340",
|
||||
"341": "Community 341",
|
||||
"342": "error-handle.ts",
|
||||
"344": "ArtException.vue",
|
||||
"346": "Community 346",
|
||||
"347": "Community 347",
|
||||
"349": "Community 349",
|
||||
"350": "Community 350",
|
||||
"354": "Community 354",
|
||||
"355": "Community 355",
|
||||
"362": "Community 362",
|
||||
"364": "Community 364",
|
||||
"367": "Community 367",
|
||||
"372": "detail.vue",
|
||||
"377": "Community 377",
|
||||
"379": "Community 379",
|
||||
"380": "Community 380",
|
||||
"381": "Community 381",
|
||||
"391": "Community 391",
|
||||
"392": "Community 392",
|
||||
"393": "Community 393",
|
||||
"405": "zh_CN.js",
|
||||
"406": "Community 406",
|
||||
"408": "Community 408",
|
||||
"410": "Community 410",
|
||||
"412": "Community 412",
|
||||
"416": "Community 416",
|
||||
"417": "Community 417",
|
||||
"422": "Community 422",
|
||||
"423": "Community 423",
|
||||
"424": "Community 424",
|
||||
"425": "Community 425",
|
||||
"426": "Community 426",
|
||||
"427": "Community 427",
|
||||
"428": "Community 428",
|
||||
"435": "index.vue",
|
||||
"443": "index.vue",
|
||||
"444": "index.vue",
|
||||
"445": "Community 445",
|
||||
"459": "Community 459",
|
||||
"460": "Community 460",
|
||||
"462": "Community 462",
|
||||
"471": "Community 471",
|
||||
"472": "Community 472",
|
||||
"473": "Community 473",
|
||||
"474": "Community 474",
|
||||
"475": "Community 475",
|
||||
"476": "Community 476",
|
||||
"487": "Community 487",
|
||||
"488": "Community 488",
|
||||
"496": "Community 496",
|
||||
"499": "Community 499",
|
||||
"521": "Community 521",
|
||||
"522": "Community 522",
|
||||
"528": "Community 528",
|
||||
"534": "Community 534",
|
||||
"535": "Community 535",
|
||||
"536": "Community 536",
|
||||
"537": "Community 537",
|
||||
"539": "Community 539",
|
||||
"540": "Community 540",
|
||||
"550": "Community 550",
|
||||
"551": "Community 551",
|
||||
"552": "Community 552",
|
||||
"553": "Community 553",
|
||||
"555": "Community 555",
|
||||
"556": "Community 556",
|
||||
"557": "Community 557",
|
||||
"559": "Community 559",
|
||||
"560": "Community 560",
|
||||
"561": "Community 561",
|
||||
"562": "Community 562",
|
||||
"571": "Community 571",
|
||||
"572": "Community 572",
|
||||
"573": "Community 573",
|
||||
"574": "Community 574",
|
||||
"575": "Community 575",
|
||||
"576": "Community 576",
|
||||
"580": "Community 580",
|
||||
"581": "Community 581",
|
||||
"582": "Community 582",
|
||||
"583": "Community 583",
|
||||
"584": "Community 584",
|
||||
"585": "Community 585",
|
||||
"586": "Community 586",
|
||||
"597": "Community 597",
|
||||
"598": "Community 598",
|
||||
"599": "Community 599",
|
||||
"600": "Community 600",
|
||||
"601": "Community 601",
|
||||
"602": "Community 602",
|
||||
"603": "Community 603",
|
||||
"604": "Community 604",
|
||||
"607": "Community 607",
|
||||
"608": "Community 608",
|
||||
"609": "Community 609",
|
||||
"610": "Community 610",
|
||||
"611": "Community 611",
|
||||
"612": "Community 612",
|
||||
"613": "Community 613",
|
||||
"614": "Community 614",
|
||||
"615": "Community 615",
|
||||
"630": "Community 630",
|
||||
"631": "Community 631",
|
||||
"632": "Community 632",
|
||||
"633": "Community 633",
|
||||
"634": "Community 634",
|
||||
"635": "Community 635",
|
||||
"636": "Community 636",
|
||||
"637": "Community 637",
|
||||
"638": "Community 638",
|
||||
"639": "Community 639",
|
||||
"640": "Community 640",
|
||||
"641": "Community 641",
|
||||
"642": "Community 642",
|
||||
"643": "Community 643",
|
||||
"644": "Community 644",
|
||||
"645": "Community 645",
|
||||
"650": "Community 650",
|
||||
"651": "Community 651",
|
||||
"652": "Community 652",
|
||||
"653": "Community 653",
|
||||
"654": "Community 654",
|
||||
"655": "Community 655",
|
||||
"658": "Community 658",
|
||||
"659": "Community 659",
|
||||
"660": "Community 660",
|
||||
"662": "Community 662",
|
||||
"663": "Community 663",
|
||||
"664": "Community 664",
|
||||
"665": "Community 665",
|
||||
"666": "Community 666",
|
||||
"667": "Community 667",
|
||||
"668": "Community 668",
|
||||
"669": "Community 669",
|
||||
"670": "Community 670",
|
||||
"671": "Community 671",
|
||||
"700": "Community 700",
|
||||
"701": "Fixed",
|
||||
"702": "Community 702",
|
||||
"703": "Community 703",
|
||||
"704": "Community 704",
|
||||
"705": "Community 705",
|
||||
"706": "Community 706",
|
||||
"707": "Community 707",
|
||||
"708": "Community 708",
|
||||
"709": "Community 709",
|
||||
"710": "Community 710",
|
||||
"711": "Community 711",
|
||||
"713": "Community 713",
|
||||
"715": "Community 715",
|
||||
"716": "Community 716",
|
||||
"717": "Community 717",
|
||||
"718": "Community 718",
|
||||
"720": "Community 720",
|
||||
"722": "Community 722",
|
||||
"723": "Community 723",
|
||||
"724": "Community 724",
|
||||
"725": "Community 725",
|
||||
"726": "Community 726",
|
||||
"727": "Community 727",
|
||||
"729": "Community 729",
|
||||
"730": "Community 730",
|
||||
"731": "Community 731",
|
||||
"732": "Community 732",
|
||||
"733": "Community 733",
|
||||
"734": "修复",
|
||||
"766": "新增",
|
||||
"770": "移除",
|
||||
"772": "重构",
|
||||
"779": "重构",
|
||||
"785": "⚠️ 重要提示",
|
||||
"786": "⚠️ 重要提示",
|
||||
"788": "⚠️ 重要提示",
|
||||
"796": "OpportunityForm.vue",
|
||||
"806": "index.vue",
|
||||
"836": "BxInvoiceFileSelect.vue",
|
||||
"838": "CustomerSelector.vue",
|
||||
"840": "index.vue",
|
||||
"857": "index.ts",
|
||||
"886": "index.vue",
|
||||
"889": "QuoteSelector.vue",
|
||||
"892": "index.vue",
|
||||
"896": "MarkWonDialog.vue",
|
||||
"924": "OpportunitySelector.vue",
|
||||
"950": "BatchUpdateStageDialog.vue",
|
||||
"1010": "BxApprovalFlowTabs.vue",
|
||||
"1015": "DealDialog.vue",
|
||||
"1016": "TransferDialog.vue",
|
||||
"1017": "BatchUpdateDateDialog.vue",
|
||||
"1027": "IntoPoolDialog.vue",
|
||||
"1115": "v2.4.2.7 - 2025-06-25",
|
||||
"1117": "v2.5.2 - 2025-07-13",
|
||||
"1118": "v2.4.2.2 - 2025-06-16",
|
||||
"1123": "v2.4.1.1 - 2025-06-07",
|
||||
"1126": "v2.4.2.1 - 2025-06-16",
|
||||
"1127": "v2.4.2.4 - 2025-06-18",
|
||||
"1197": "icons.js",
|
||||
"1198": "icons.min.js",
|
||||
"1199": "index.js",
|
||||
"1200": "index.js",
|
||||
"1201": "index.js",
|
||||
"1202": "index.js",
|
||||
"1203": "index.js",
|
||||
"1204": "plugin.js",
|
||||
"1205": "plugin.min.js",
|
||||
"1206": "index.js",
|
||||
"1207": "plugin.js",
|
||||
"1208": "plugin.min.js",
|
||||
"1209": "index.js",
|
||||
"1210": "plugin.js",
|
||||
"1211": "plugin.min.js",
|
||||
"1212": "index.js",
|
||||
"1213": "plugin.js",
|
||||
"1214": "plugin.min.js",
|
||||
"1215": "index.js",
|
||||
"1216": "index.js",
|
||||
"1217": "plugin.js",
|
||||
"1218": "plugin.min.js",
|
||||
"1219": "index.js",
|
||||
"1220": "index.js",
|
||||
"1222": "emojiimages.js",
|
||||
"1223": "emojiimages.min.js",
|
||||
"1224": "emojis.js",
|
||||
"1225": "emojis.min.js",
|
||||
"1228": "ar.js",
|
||||
"1229": "bg_BG.js",
|
||||
"1230": "ca.js",
|
||||
"1231": "cs.js",
|
||||
"1232": "da.js",
|
||||
"1233": "de.js",
|
||||
"1234": "el.js",
|
||||
"1235": "es.js",
|
||||
"1236": "eu.js",
|
||||
"1237": "fa.js",
|
||||
"1238": "fi.js",
|
||||
"1239": "fr_FR.js",
|
||||
"1240": "he_IL.js",
|
||||
"1241": "hi.js",
|
||||
"1242": "hr.js",
|
||||
"1243": "hu_HU.js",
|
||||
"1244": "id.js",
|
||||
"1245": "it.js",
|
||||
"1246": "ja.js",
|
||||
"1247": "kk.js",
|
||||
"1248": "ko_KR.js",
|
||||
"1249": "ms.js",
|
||||
"1250": "nb_NO.js",
|
||||
"1251": "nl.js",
|
||||
"1252": "pl.js",
|
||||
"1253": "pt_BR.js",
|
||||
"1254": "pt_PT.js",
|
||||
"1255": "ro.js",
|
||||
"1256": "ru.js",
|
||||
"1257": "sk.js",
|
||||
"1258": "sl_SI.js",
|
||||
"1259": "sv_SE.js",
|
||||
"1260": "th_TH.js",
|
||||
"1261": "tr.js",
|
||||
"1262": "uk.js",
|
||||
"1263": "vi.js",
|
||||
"1265": "zh_TW.js",
|
||||
"1266": "index.js",
|
||||
"1267": "index.js",
|
||||
"1268": "plugin.js",
|
||||
"1269": "_",
|
||||
"1270": "index.js",
|
||||
"1271": "plugin.js",
|
||||
"1272": "plugin.min.js",
|
||||
"1275": "index.js",
|
||||
"1277": "plugin.js",
|
||||
"1278": "plugin.min.js",
|
||||
"1280": "plugin.js",
|
||||
"1281": "plugin.min.js",
|
||||
"1282": "index.js",
|
||||
"1283": "plugin.js",
|
||||
"1284": "plugin.min.js",
|
||||
"1287": "plugin.js",
|
||||
"1288": "plugin.min.js",
|
||||
"1289": "index.js",
|
||||
"1290": "index.js",
|
||||
"1291": "index.js",
|
||||
"1292": "index.js",
|
||||
"1293": "plugin.js",
|
||||
"1294": "plugin.min.js",
|
||||
"1295": "index.js",
|
||||
"1297": "plugin.js",
|
||||
"1298": "plugin.min.js",
|
||||
"1299": "content.js",
|
||||
"1300": "content.js",
|
||||
"1301": "content.js",
|
||||
"1302": "content.js",
|
||||
"1303": "content.js",
|
||||
"1304": "content.js",
|
||||
"1305": "content.inline.js",
|
||||
"1306": "content.js",
|
||||
"1307": "skin.js",
|
||||
"1308": "skin.shadowdom.js",
|
||||
"1309": "content.inline.js",
|
||||
"1310": "content.js",
|
||||
"1311": "skin.js",
|
||||
"1312": "skin.shadowdom.js",
|
||||
"1313": "content.inline.js",
|
||||
"1314": "content.js",
|
||||
"1315": "skin.js",
|
||||
"1316": "skin.shadowdom.js",
|
||||
"1317": "content.inline.js",
|
||||
"1318": "content.js",
|
||||
"1319": "skin.js",
|
||||
"1320": "skin.shadowdom.js",
|
||||
"1323": "index.vue",
|
||||
"1324": "index.vue",
|
||||
"1325": "index.vue",
|
||||
"1326": "route-parent.vue",
|
||||
"1327": "setupSubpackSlipEditorTypeCheckboxes",
|
||||
"1328": "修复",
|
||||
"1329": "优化",
|
||||
"1330": "新增",
|
||||
"1331": "⚠️ 重要提示",
|
||||
"1332": "重构",
|
||||
"1333": "优化",
|
||||
"1334": "修复",
|
||||
"1335": "新增",
|
||||
"1336": "升级",
|
||||
"1337": "⚠️ 重要提示",
|
||||
"1338": "修复",
|
||||
"1339": "优化",
|
||||
"1340": "新增",
|
||||
"1341": "优化",
|
||||
"1342": "修复",
|
||||
"1343": "⚠️ 重要提示",
|
||||
"1344": "升级",
|
||||
"1345": "修复",
|
||||
"1346": "优化",
|
||||
"1347": "⚠️ 重要提示",
|
||||
"1348": "新增",
|
||||
"1349": "修复",
|
||||
"1350": "优化",
|
||||
"1351": "新增",
|
||||
"1352": "优化",
|
||||
"1353": "修复",
|
||||
"1354": "新增",
|
||||
"1355": "重构",
|
||||
"1356": "优化",
|
||||
"1357": "修复",
|
||||
"1358": "移除",
|
||||
"1359": "新增",
|
||||
"1360": "优化",
|
||||
"1361": "修复",
|
||||
"1362": "新增",
|
||||
"1363": "重构",
|
||||
"1364": "升级",
|
||||
"1365": "优化",
|
||||
"1366": "修复",
|
||||
"1367": "新增",
|
||||
"1368": "优化",
|
||||
"1369": "修复",
|
||||
"1370": "优化",
|
||||
"1371": "修复",
|
||||
"1372": "新增",
|
||||
"1373": "重构",
|
||||
"1374": "升级",
|
||||
"1375": "移除",
|
||||
"1376": "修复",
|
||||
"1377": "⚠️ 重要提示",
|
||||
"1378": "重构",
|
||||
"1379": "优化",
|
||||
"1380": "修复",
|
||||
"1381": "修复",
|
||||
"1382": "新增",
|
||||
"1383": "优化",
|
||||
"1384": "修复",
|
||||
"1385": "重构",
|
||||
"1386": "优化",
|
||||
"1387": "新增",
|
||||
"1388": "重构",
|
||||
"1389": "重构",
|
||||
"1390": "优化",
|
||||
"1391": "重构",
|
||||
"1392": "优化",
|
||||
"1393": "重构",
|
||||
"1394": "重构",
|
||||
"1395": "修复",
|
||||
"1396": "重构",
|
||||
"1397": "新增",
|
||||
"1398": "优化",
|
||||
"1399": "修复",
|
||||
"1400": "⚠️ 重要提示",
|
||||
"1401": "优化",
|
||||
"1402": "修复",
|
||||
"1403": "新增",
|
||||
"1404": "优化",
|
||||
"1405": "修复",
|
||||
"1406": "重构",
|
||||
"1407": "新增",
|
||||
"1408": "优化",
|
||||
"1409": "⚠️ 重要提示",
|
||||
"1410": "code:block1 (crm/clue/)",
|
||||
"1411": "code:typescript ({)",
|
||||
"1412": "code:block3 (crm:clue:list # 线索列表查询)",
|
||||
"1413": "code:bash (mysql -u root -p your_database < crm/sql/crm/12-CRM字典数据汇总.md)",
|
||||
"9890": "README.md",
|
||||
"9959": "Fixed",
|
||||
"9960": "Fixed",
|
||||
"9961": "Fixed",
|
||||
"9962": "Fixed",
|
||||
"9963": "Fixed",
|
||||
"9964": "Fixed",
|
||||
"9965": "Fixed",
|
||||
"9966": "Fixed",
|
||||
"9967": "Fixed",
|
||||
"9968": "Fixed",
|
||||
"9969": "Fixed",
|
||||
"9970": "Fixed",
|
||||
"9971": "Fixed",
|
||||
"9972": "Fixed",
|
||||
"9973": "Fixed",
|
||||
"9974": "⚠️ Important Notice",
|
||||
"9975": "⚠️ Important Notice",
|
||||
"9976": "⚠️ Important Notice",
|
||||
"9977": "⚠️ Important Notice",
|
||||
"9978": "⚠️ Important Notice",
|
||||
"9979": "⚠️ Important Notice",
|
||||
"9980": "⚠️ Important Notice",
|
||||
"9981": "Optimized",
|
||||
"9982": "Optimized",
|
||||
"9983": "Optimized",
|
||||
"9984": "Optimized",
|
||||
"9985": "Optimized",
|
||||
"9986": "Optimized",
|
||||
"9987": "Optimized",
|
||||
"9988": "Optimized",
|
||||
"9989": "Optimized",
|
||||
"9990": "Optimized",
|
||||
"9991": "Optimized",
|
||||
"9992": "Optimized",
|
||||
"9993": "Optimized",
|
||||
"9994": "Optimized",
|
||||
"9995": "Optimized",
|
||||
"9996": "Optimized",
|
||||
"9997": "Optimized",
|
||||
"9998": "Optimized",
|
||||
"9999": "Optimized",
|
||||
"10000": "Optimized",
|
||||
"10001": "Refactored",
|
||||
"10002": "Refactored",
|
||||
"10003": "Refactored",
|
||||
"10004": "Refactored",
|
||||
"10005": "Refactored",
|
||||
"10006": "Refactored",
|
||||
"10007": "Refactored",
|
||||
"10008": "Refactored",
|
||||
"10009": "Refactored",
|
||||
"10010": "Refactored",
|
||||
"10011": "Refactored",
|
||||
"10012": "Refactored",
|
||||
"10013": "Removed",
|
||||
"10014": "Removed",
|
||||
"10015": "Upgraded",
|
||||
"10016": "Upgraded",
|
||||
"10017": "Upgraded",
|
||||
"10018": "Upgraded",
|
||||
"10019": "优化",
|
||||
"10020": "修复",
|
||||
"10021": "⚠️ 重要提示",
|
||||
"10022": "修复",
|
||||
"10023": "优化",
|
||||
"10024": "优化",
|
||||
"10025": "⚠️ 重要提示",
|
||||
"10026": "修复",
|
||||
"10027": "优化",
|
||||
"10028": "优化",
|
||||
"10029": "修复",
|
||||
"10030": "重构",
|
||||
"10031": "优化",
|
||||
"10032": "修复",
|
||||
"10033": "新增",
|
||||
"10034": "移除",
|
||||
"10035": "优化",
|
||||
"10036": "修复",
|
||||
"10037": "重构",
|
||||
"10038": "升级",
|
||||
"10039": "优化",
|
||||
"10040": "修复",
|
||||
"10041": "优化",
|
||||
"10042": "修复",
|
||||
"10043": "修复",
|
||||
"10044": "重构",
|
||||
"10045": "升级",
|
||||
"10046": "移除",
|
||||
"10047": "修复",
|
||||
"10048": "⚠️ 重要提示",
|
||||
"10049": "⚠️ 重要提示",
|
||||
"10050": "优化",
|
||||
"10051": "修复",
|
||||
"10052": "优化",
|
||||
"10053": "修复",
|
||||
"10054": "优化",
|
||||
"10055": "新增",
|
||||
"10056": "重构",
|
||||
"10057": "优化",
|
||||
"10058": "优化",
|
||||
"10059": "重构",
|
||||
"10060": "修复",
|
||||
"10061": "新增",
|
||||
"10062": "优化",
|
||||
"10063": "修复",
|
||||
"10064": "优化",
|
||||
"10065": "⚠️ 重要提示",
|
||||
"10066": "优化",
|
||||
"10067": "修复",
|
||||
"10068": "优化",
|
||||
"10069": "修复",
|
||||
"10070": "新增",
|
||||
"10071": "优化",
|
||||
"10072": "⚠️ 重要提示",
|
||||
"10073": "修复",
|
||||
"10074": "新增",
|
||||
"10075": "升级",
|
||||
"10076": "⚠️ 重要提示",
|
||||
"10077": "修复",
|
||||
"10078": "优化",
|
||||
"10079": "code:bash (# Install dependencies)",
|
||||
"10080": "code:bash (pnpm clean:dev)",
|
||||
"10081": "code:bash (# 安装依赖)",
|
||||
"10082": "code:bash (pnpm clean:dev)"
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
+3833
-2361
File diff suppressed because it is too large.
Load diff
@@ -195,6 +195,14 @@ export const reimburseApi = {
|
||||
},
|
||||
delete(id: BxId) {
|
||||
return bxRequest.delete(`/reimburses/${id}`)
|
||||
},
|
||||
downloadImportTemplate() {
|
||||
return bxRequest.get('/reimburses/import-template', { responseType: 'blob' })
|
||||
},
|
||||
importExcel(file: File) {
|
||||
const formData = new FormData()
|
||||
formData.append('file', file)
|
||||
return bxRequest.post('/reimburses/import', formData)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,512 @@
|
||||
<!-- 表头 Excel 风格列过滤:漏斗靠右,支持文本/枚举/数字/时间 -->
|
||||
<template>
|
||||
<el-popover
|
||||
:visible="visible"
|
||||
placement="bottom-end"
|
||||
:width="300"
|
||||
trigger="click"
|
||||
:teleported="true"
|
||||
popper-class="column-header-filter-popper"
|
||||
@update:visible="onVisibleChange"
|
||||
>
|
||||
<template #reference>
|
||||
<button
|
||||
type="button"
|
||||
class="column-header-filter-btn"
|
||||
:class="{ active: isActive }"
|
||||
:aria-label="`过滤${label}`"
|
||||
>
|
||||
<el-icon :size="14"><Filter /></el-icon>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<div class="column-header-filter-panel" @click.stop>
|
||||
<div class="panel-hd">
|
||||
<div class="panel-title">过滤 · {{ label }}</div>
|
||||
<div class="panel-sub">{{ typeHint }}</div>
|
||||
</div>
|
||||
|
||||
<div class="panel-bd">
|
||||
<!-- 文本 / 枚举 -->
|
||||
<template v-if="type === 'text' || type === 'enum'">
|
||||
<div class="field">
|
||||
<label>搜索可选值</label>
|
||||
<el-input
|
||||
v-model="draftKeyword"
|
||||
clearable
|
||||
placeholder="输入关键字过滤下方列表"
|
||||
size="small"
|
||||
/>
|
||||
</div>
|
||||
<div class="select-all-row">
|
||||
<span>本页可选值</span>
|
||||
<span>
|
||||
<el-button link type="primary" size="small" @click="selectAllVisible">全选</el-button>
|
||||
<el-button link type="primary" size="small" @click="selectNoneVisible"
|
||||
>全不选</el-button
|
||||
>
|
||||
</span>
|
||||
</div>
|
||||
<div class="value-list">
|
||||
<label v-for="opt in visibleOptions" :key="opt.value" class="value-item">
|
||||
<el-checkbox
|
||||
:model-value="draftSelected.includes(opt.value)"
|
||||
@change="(v) => toggleSelected(opt.value, !!v)"
|
||||
>
|
||||
<span class="value-label">{{ opt.label }}</span>
|
||||
</el-checkbox>
|
||||
<span class="value-count">{{ opt.count }}</span>
|
||||
</label>
|
||||
<div v-if="!visibleOptions.length" class="empty-tip">无匹配项</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 数字 -->
|
||||
<template v-else-if="type === 'number'">
|
||||
<div class="field">
|
||||
<label>条件</label>
|
||||
<el-select v-model="draftOp" size="small" style="width: 100%">
|
||||
<el-option label="介于" value="between" />
|
||||
<el-option label="等于" value="eq" />
|
||||
<el-option label="大于" value="gt" />
|
||||
<el-option label="大于等于" value="gte" />
|
||||
<el-option label="小于" value="lt" />
|
||||
<el-option label="小于等于" value="lte" />
|
||||
</el-select>
|
||||
</div>
|
||||
<div v-if="draftOp === 'between'" class="field">
|
||||
<label>数值区间</label>
|
||||
<div class="range-row">
|
||||
<el-input
|
||||
v-model="draftMin"
|
||||
type="number"
|
||||
size="small"
|
||||
placeholder="最小值"
|
||||
clearable
|
||||
/>
|
||||
<span class="sep">至</span>
|
||||
<el-input
|
||||
v-model="draftMax"
|
||||
type="number"
|
||||
size="small"
|
||||
placeholder="最大值"
|
||||
clearable
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="field">
|
||||
<label>数值</label>
|
||||
<el-input
|
||||
v-model="draftValue"
|
||||
type="number"
|
||||
size="small"
|
||||
placeholder="输入数值"
|
||||
clearable
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 时间 -->
|
||||
<template v-else-if="type === 'datetime'">
|
||||
<div class="field">
|
||||
<label>开始日期</label>
|
||||
<el-date-picker
|
||||
v-model="draftFrom"
|
||||
type="date"
|
||||
value-format="YYYY-MM-DD"
|
||||
placeholder="开始日期"
|
||||
size="small"
|
||||
clearable
|
||||
style="width: 100%"
|
||||
/>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>结束日期</label>
|
||||
<el-date-picker
|
||||
v-model="draftTo"
|
||||
type="date"
|
||||
value-format="YYYY-MM-DD"
|
||||
placeholder="结束日期"
|
||||
size="small"
|
||||
clearable
|
||||
style="width: 100%"
|
||||
/>
|
||||
</div>
|
||||
<p class="field-tip">闭区间:含起止当天</p>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div class="panel-ft">
|
||||
<el-button size="small" @click="clearColumn">
|
||||
<el-icon><Delete /></el-icon>清空本列
|
||||
</el-button>
|
||||
<div class="panel-ft-end">
|
||||
<el-button size="small" @click="close">
|
||||
<el-icon><Close /></el-icon>取消
|
||||
</el-button>
|
||||
<el-button type="primary" size="small" @click="confirm">
|
||||
<el-icon><Check /></el-icon>确定
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-popover>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { Filter, Delete, Close, Check } from '@element-plus/icons-vue'
|
||||
import {
|
||||
isColumnFilterActive,
|
||||
type ColumnFilterType,
|
||||
type ColumnFilterState,
|
||||
type TextColumnFilterState,
|
||||
type NumberColumnFilterState,
|
||||
type DatetimeColumnFilterState,
|
||||
type NumberFilterOp
|
||||
} from '@/composables/useClientColumnFilters'
|
||||
|
||||
defineOptions({ name: 'ColumnHeaderFilter' })
|
||||
|
||||
const props = defineProps<{
|
||||
prop: string
|
||||
label: string
|
||||
type: ColumnFilterType
|
||||
isActive: boolean
|
||||
modelValue?: ColumnFilterState | null
|
||||
options?: Array<{ value: string; label: string; count: number }>
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: ColumnFilterState | null]
|
||||
clear: []
|
||||
}>()
|
||||
|
||||
const visible = ref(false)
|
||||
const draftKeyword = ref('')
|
||||
const draftSelected = ref<string[]>([])
|
||||
const draftOp = ref<NumberFilterOp>('between')
|
||||
const draftValue = ref('')
|
||||
const draftMin = ref('')
|
||||
const draftMax = ref('')
|
||||
const draftFrom = ref('')
|
||||
const draftTo = ref('')
|
||||
|
||||
const typeHint = computed(() => {
|
||||
if (props.type === 'text') return '文本:搜索并勾选本页出现的值'
|
||||
if (props.type === 'enum') return '枚举:勾选固定选项'
|
||||
if (props.type === 'number') return '数字:运算符 + 单值或区间'
|
||||
return '时间:按日期闭区间过滤'
|
||||
})
|
||||
|
||||
const visibleOptions = computed(() => {
|
||||
const list = props.options || []
|
||||
const q = draftKeyword.value.trim().toLowerCase()
|
||||
if (!q) return list
|
||||
return list.filter(
|
||||
(opt) => opt.label.toLowerCase().includes(q) || String(opt.value).toLowerCase().includes(q)
|
||||
)
|
||||
})
|
||||
|
||||
function resetDraftFromModel() {
|
||||
const m = props.modelValue
|
||||
if (props.type === 'text' || props.type === 'enum') {
|
||||
const s = (m || { selected: [], keyword: '' }) as TextColumnFilterState
|
||||
draftSelected.value = [...(s.selected || [])]
|
||||
draftKeyword.value = s.keyword || ''
|
||||
return
|
||||
}
|
||||
if (props.type === 'number') {
|
||||
const s = (m || { op: 'between', value: '', min: '', max: '' }) as NumberColumnFilterState
|
||||
draftOp.value = s.op || 'between'
|
||||
draftValue.value = s.value ?? ''
|
||||
draftMin.value = s.min ?? ''
|
||||
draftMax.value = s.max ?? ''
|
||||
return
|
||||
}
|
||||
if (props.type === 'datetime') {
|
||||
const s = (m || { from: '', to: '' }) as DatetimeColumnFilterState
|
||||
draftFrom.value = s.from || ''
|
||||
draftTo.value = s.to || ''
|
||||
}
|
||||
}
|
||||
|
||||
function onVisibleChange(next: boolean) {
|
||||
if (next) resetDraftFromModel()
|
||||
visible.value = next
|
||||
}
|
||||
|
||||
function close() {
|
||||
visible.value = false
|
||||
}
|
||||
|
||||
function toggleSelected(value: string, checked: boolean) {
|
||||
if (checked) {
|
||||
if (!draftSelected.value.includes(value)) draftSelected.value.push(value)
|
||||
} else {
|
||||
draftSelected.value = draftSelected.value.filter((x) => x !== value)
|
||||
}
|
||||
}
|
||||
|
||||
function selectAllVisible() {
|
||||
for (const opt of visibleOptions.value) {
|
||||
if (!draftSelected.value.includes(opt.value)) draftSelected.value.push(opt.value)
|
||||
}
|
||||
}
|
||||
|
||||
function selectNoneVisible() {
|
||||
const hide = new Set(visibleOptions.value.map((o) => o.value))
|
||||
draftSelected.value = draftSelected.value.filter((v) => !hide.has(v))
|
||||
}
|
||||
|
||||
function buildState(): ColumnFilterState {
|
||||
if (props.type === 'text' || props.type === 'enum') {
|
||||
return {
|
||||
selected: [...draftSelected.value],
|
||||
keyword: draftKeyword.value
|
||||
}
|
||||
}
|
||||
if (props.type === 'number') {
|
||||
return {
|
||||
op: draftOp.value,
|
||||
value: draftValue.value,
|
||||
min: draftMin.value,
|
||||
max: draftMax.value
|
||||
}
|
||||
}
|
||||
return {
|
||||
from: draftFrom.value || '',
|
||||
to: draftTo.value || ''
|
||||
}
|
||||
}
|
||||
|
||||
function confirm() {
|
||||
if (props.type === 'number' && draftOp.value === 'between') {
|
||||
if (
|
||||
draftMin.value !== '' &&
|
||||
draftMax.value !== '' &&
|
||||
Number(draftMin.value) > Number(draftMax.value)
|
||||
) {
|
||||
ElMessage.warning('最小值不能大于最大值')
|
||||
return
|
||||
}
|
||||
}
|
||||
if (props.type === 'datetime') {
|
||||
if (draftFrom.value && draftTo.value && draftFrom.value > draftTo.value) {
|
||||
ElMessage.warning('开始日期不能晚于结束日期')
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
const state = buildState()
|
||||
if (isColumnFilterActive(props.type, state)) {
|
||||
emit('update:modelValue', state)
|
||||
} else {
|
||||
emit('update:modelValue', null)
|
||||
emit('clear')
|
||||
}
|
||||
close()
|
||||
}
|
||||
|
||||
function clearColumn() {
|
||||
emit('update:modelValue', null)
|
||||
emit('clear')
|
||||
close()
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
() => {
|
||||
if (visible.value) resetDraftFromModel()
|
||||
}
|
||||
)
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.column-header-filter-btn {
|
||||
display: inline-flex;
|
||||
flex-shrink: 0;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
color: #94a3b8;
|
||||
cursor: pointer;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
|
||||
&:hover {
|
||||
color: var(--mint-text, #1e293b);
|
||||
background: #e2e8f0;
|
||||
}
|
||||
|
||||
&.active {
|
||||
color: #fff;
|
||||
background: var(--primary, #10b981);
|
||||
}
|
||||
}
|
||||
|
||||
.column-header-filter-panel {
|
||||
margin: -4px;
|
||||
}
|
||||
|
||||
.panel-hd {
|
||||
padding: 4px 4px 10px;
|
||||
border-bottom: 1px solid var(--el-border-color-lighter, #e2e8f0);
|
||||
}
|
||||
|
||||
.panel-title {
|
||||
margin-bottom: 2px;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
color: var(--mint-text, #1e293b);
|
||||
}
|
||||
|
||||
.panel-sub {
|
||||
font-size: 11px;
|
||||
color: var(--mint-text-muted, #64748b);
|
||||
}
|
||||
|
||||
.panel-bd {
|
||||
max-height: 280px;
|
||||
padding: 10px 4px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.field {
|
||||
margin-bottom: 10px;
|
||||
|
||||
label {
|
||||
display: block;
|
||||
margin-bottom: 4px;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
color: var(--mint-text-muted, #64748b);
|
||||
}
|
||||
}
|
||||
|
||||
.field-tip {
|
||||
margin: 0;
|
||||
font-size: 11px;
|
||||
color: var(--mint-text-muted, #64748b);
|
||||
}
|
||||
|
||||
.range-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto 1fr;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.sep {
|
||||
font-size: 12px;
|
||||
color: var(--mint-text-muted, #64748b);
|
||||
}
|
||||
|
||||
.select-all-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 6px;
|
||||
font-size: 12px;
|
||||
color: var(--mint-text-muted, #64748b);
|
||||
}
|
||||
|
||||
.value-list {
|
||||
max-height: 180px;
|
||||
padding: 4px;
|
||||
overflow: auto;
|
||||
border: 1px solid var(--el-border-color-lighter, #e2e8f0);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.value-item {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
padding: 4px 6px;
|
||||
cursor: pointer;
|
||||
border-radius: 6px;
|
||||
|
||||
&:hover {
|
||||
background: var(--primary-bg, #ecfdf5);
|
||||
}
|
||||
|
||||
:deep(.el-checkbox) {
|
||||
flex: 1;
|
||||
height: auto;
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.value-label {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.value-count {
|
||||
flex-shrink: 0;
|
||||
font-size: 11px;
|
||||
color: var(--mint-text-muted, #64748b);
|
||||
}
|
||||
}
|
||||
|
||||
.empty-tip {
|
||||
padding: 16px 8px;
|
||||
font-size: 12px;
|
||||
color: var(--mint-text-muted, #64748b);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.panel-ft {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 10px 4px 4px;
|
||||
border-top: 1px solid var(--el-border-color-lighter, #e2e8f0);
|
||||
}
|
||||
|
||||
.panel-ft-end {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss">
|
||||
.column-header-filter-popper.el-popover {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.col-header-with-filter {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.col-header-with-filter__label {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
font-weight: 600;
|
||||
color: inherit;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.col-header-with-filter__label.is-left {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.col-header-with-filter__label.is-right {
|
||||
text-align: right;
|
||||
}
|
||||
</style>
|
||||
@@ -18,7 +18,7 @@ import { PL_PLAN_TYPE_TABS } from '@/utils/pl-display'
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: string
|
||||
/** 覆盖默认单据类型 Tab(如危化品证上传页追加「未上传证书」) */
|
||||
/** 覆盖默认单据类型 Tab(如运输计划追加「待安排」、危化品证上传页追加「未上传证书」) */
|
||||
tabs?: { label: string; name: string }[]
|
||||
}>()
|
||||
|
||||
|
||||
@@ -0,0 +1,262 @@
|
||||
import { computed, reactive, type Ref, type ComputedRef } from 'vue'
|
||||
|
||||
export type ColumnFilterType = 'text' | 'enum' | 'number' | 'datetime'
|
||||
|
||||
export type NumberFilterOp = 'between' | 'eq' | 'gt' | 'gte' | 'lt' | 'lte'
|
||||
|
||||
export interface ColumnFilterEnumOption {
|
||||
value: string
|
||||
label: string
|
||||
}
|
||||
|
||||
export interface ColumnFilterDef {
|
||||
prop: string
|
||||
label: string
|
||||
type: ColumnFilterType
|
||||
/** 枚举固定选项;未提供时从当前页去重 */
|
||||
enumOptions?: ColumnFilterEnumOption[]
|
||||
/** 自定义取值(展示值 / 比较值) */
|
||||
getValue?: (row: any) => unknown
|
||||
}
|
||||
|
||||
export interface TextColumnFilterState {
|
||||
selected: string[]
|
||||
keyword: string
|
||||
}
|
||||
|
||||
export interface NumberColumnFilterState {
|
||||
op: NumberFilterOp
|
||||
value: string
|
||||
min: string
|
||||
max: string
|
||||
}
|
||||
|
||||
export interface DatetimeColumnFilterState {
|
||||
from: string
|
||||
to: string
|
||||
}
|
||||
|
||||
export type ColumnFilterState =
|
||||
| TextColumnFilterState
|
||||
| NumberColumnFilterState
|
||||
| DatetimeColumnFilterState
|
||||
|
||||
function parseDateMs(value: unknown): number | null {
|
||||
const text = String(value ?? '').trim()
|
||||
if (!text) return null
|
||||
const t = Date.parse(text.replace(/-/g, '/'))
|
||||
return Number.isNaN(t) ? null : t
|
||||
}
|
||||
|
||||
function startOfDayMs(yyyyMmDd: string): number | null {
|
||||
return parseDateMs(`${yyyyMmDd} 00:00:00`)
|
||||
}
|
||||
|
||||
function endOfDayMs(yyyyMmDd: string): number | null {
|
||||
return parseDateMs(`${yyyyMmDd} 23:59:59`)
|
||||
}
|
||||
|
||||
function defaultState(type: ColumnFilterType): ColumnFilterState {
|
||||
if (type === 'number') {
|
||||
return { op: 'between', value: '', min: '', max: '' }
|
||||
}
|
||||
if (type === 'datetime') {
|
||||
return { from: '', to: '' }
|
||||
}
|
||||
return { selected: [], keyword: '' }
|
||||
}
|
||||
|
||||
export function isColumnFilterActive(
|
||||
type: ColumnFilterType,
|
||||
state?: ColumnFilterState | null
|
||||
): boolean {
|
||||
if (!state) return false
|
||||
if (type === 'text' || type === 'enum') {
|
||||
const s = state as TextColumnFilterState
|
||||
return Array.isArray(s.selected) && s.selected.length > 0
|
||||
}
|
||||
if (type === 'number') {
|
||||
const s = state as NumberColumnFilterState
|
||||
if (s.op === 'between') {
|
||||
return (s.min !== '' && s.min != null) || (s.max !== '' && s.max != null)
|
||||
}
|
||||
return s.value !== '' && s.value != null
|
||||
}
|
||||
if (type === 'datetime') {
|
||||
const s = state as DatetimeColumnFilterState
|
||||
return !!(s.from || s.to)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
function resolveRawValue(row: any, def: ColumnFilterDef): unknown {
|
||||
if (def.getValue) return def.getValue(row)
|
||||
return row?.[def.prop]
|
||||
}
|
||||
|
||||
function resolveCompareKey(row: any, def: ColumnFilterDef): string {
|
||||
const raw = resolveRawValue(row, def)
|
||||
if (raw == null) return ''
|
||||
return String(raw)
|
||||
}
|
||||
|
||||
function matchOne(row: any, def: ColumnFilterDef, state: ColumnFilterState): boolean {
|
||||
if (!isColumnFilterActive(def.type, state)) return true
|
||||
|
||||
if (def.type === 'text' || def.type === 'enum') {
|
||||
const s = state as TextColumnFilterState
|
||||
const key = resolveCompareKey(row, def)
|
||||
return s.selected.includes(key)
|
||||
}
|
||||
|
||||
if (def.type === 'number') {
|
||||
const s = state as NumberColumnFilterState
|
||||
const val = Number(resolveRawValue(row, def))
|
||||
if (Number.isNaN(val)) return false
|
||||
if (s.op === 'eq') return s.value !== '' && val === Number(s.value)
|
||||
if (s.op === 'gt') return s.value !== '' && val > Number(s.value)
|
||||
if (s.op === 'gte') return s.value !== '' && val >= Number(s.value)
|
||||
if (s.op === 'lt') return s.value !== '' && val < Number(s.value)
|
||||
if (s.op === 'lte') return s.value !== '' && val <= Number(s.value)
|
||||
const minOk = s.min === '' || s.min == null || val >= Number(s.min)
|
||||
const maxOk = s.max === '' || s.max == null || val <= Number(s.max)
|
||||
return minOk && maxOk
|
||||
}
|
||||
|
||||
if (def.type === 'datetime') {
|
||||
const s = state as DatetimeColumnFilterState
|
||||
const t = parseDateMs(resolveRawValue(row, def))
|
||||
if (t == null) return false
|
||||
const fromOk =
|
||||
!s.from ||
|
||||
(() => {
|
||||
const from = startOfDayMs(s.from)
|
||||
return from != null && t >= from
|
||||
})()
|
||||
const toOk =
|
||||
!s.to ||
|
||||
(() => {
|
||||
const to = endOfDayMs(s.to)
|
||||
return to != null && t <= to
|
||||
})()
|
||||
return fromOk && toOk
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
export function useClientColumnFilters(
|
||||
sourceRows: Ref<any[]> | ComputedRef<any[]>,
|
||||
columnDefs: ColumnFilterDef[]
|
||||
) {
|
||||
const defMap = new Map(columnDefs.map((d) => [d.prop, d]))
|
||||
const filters = reactive<Record<string, ColumnFilterState>>({})
|
||||
|
||||
const filteredRows = computed(() => {
|
||||
const list = sourceRows.value || []
|
||||
const activeProps = Object.keys(filters).filter((prop) => {
|
||||
const def = defMap.get(prop)
|
||||
return def && isColumnFilterActive(def.type, filters[prop])
|
||||
})
|
||||
if (!activeProps.length) return list
|
||||
return list.filter((row) =>
|
||||
activeProps.every((prop) => {
|
||||
const def = defMap.get(prop)!
|
||||
return matchOne(row, def, filters[prop])
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
const activeFilterProps = computed(() =>
|
||||
Object.keys(filters).filter((prop) => {
|
||||
const def = defMap.get(prop)
|
||||
return def && isColumnFilterActive(def.type, filters[prop])
|
||||
})
|
||||
)
|
||||
|
||||
const hasActiveFilters = computed(() => activeFilterProps.value.length > 0)
|
||||
|
||||
function getFilterState(prop: string): ColumnFilterState {
|
||||
const def = defMap.get(prop)
|
||||
if (!def) return defaultState('text')
|
||||
if (!filters[prop]) {
|
||||
filters[prop] = defaultState(def.type)
|
||||
}
|
||||
return filters[prop]
|
||||
}
|
||||
|
||||
function setFilterState(prop: string, state: ColumnFilterState | null) {
|
||||
const def = defMap.get(prop)
|
||||
if (!def) return
|
||||
if (!state || !isColumnFilterActive(def.type, state)) {
|
||||
delete filters[prop]
|
||||
return
|
||||
}
|
||||
filters[prop] = JSON.parse(JSON.stringify(state))
|
||||
}
|
||||
|
||||
function clearFilter(prop: string) {
|
||||
delete filters[prop]
|
||||
}
|
||||
|
||||
function clearAllFilters() {
|
||||
Object.keys(filters).forEach((k) => delete filters[k])
|
||||
}
|
||||
|
||||
function isFilterActive(prop: string): boolean {
|
||||
const def = defMap.get(prop)
|
||||
if (!def) return false
|
||||
return isColumnFilterActive(def.type, filters[prop])
|
||||
}
|
||||
|
||||
/** 当前页去重可选值(含计数) */
|
||||
function collectUniqueOptions(
|
||||
prop: string
|
||||
): Array<{ value: string; label: string; count: number }> {
|
||||
const def = defMap.get(prop)
|
||||
if (!def) return []
|
||||
|
||||
if (def.type === 'enum' && def.enumOptions?.length) {
|
||||
const counts = new Map<string, number>()
|
||||
for (const row of sourceRows.value || []) {
|
||||
const key = resolveCompareKey(row, def)
|
||||
counts.set(key, (counts.get(key) || 0) + 1)
|
||||
}
|
||||
return def.enumOptions.map((opt) => ({
|
||||
value: opt.value,
|
||||
label: opt.label,
|
||||
count: counts.get(opt.value) || 0
|
||||
}))
|
||||
}
|
||||
|
||||
const map = new Map<string, { label: string; count: number }>()
|
||||
for (const row of sourceRows.value || []) {
|
||||
const key = resolveCompareKey(row, def)
|
||||
const label = key || '(空)'
|
||||
const prev = map.get(key)
|
||||
if (prev) prev.count += 1
|
||||
else map.set(key, { label, count: 1 })
|
||||
}
|
||||
return [...map.entries()]
|
||||
.map(([value, meta]) => ({ value, label: meta.label, count: meta.count }))
|
||||
.sort((a, b) => a.label.localeCompare(b.label, 'zh-CN'))
|
||||
}
|
||||
|
||||
function getColumnDef(prop: string): ColumnFilterDef | undefined {
|
||||
return defMap.get(prop)
|
||||
}
|
||||
|
||||
return {
|
||||
filters,
|
||||
filteredRows,
|
||||
activeFilterProps,
|
||||
hasActiveFilters,
|
||||
getFilterState,
|
||||
setFilterState,
|
||||
clearFilter,
|
||||
clearAllFilters,
|
||||
isFilterActive,
|
||||
collectUniqueOptions,
|
||||
getColumnDef
|
||||
}
|
||||
}
|
||||
@@ -324,6 +324,9 @@ export function canPrintSlipCommand(planType: string | undefined, command: PlPri
|
||||
|
||||
export type PlPlanTypeTab = 'all' | 'inbound' | 'outbound' | 'direct'
|
||||
|
||||
/** 运输计划列表 Tab(在单据类型后增加「待安排」) */
|
||||
export type PlPlanListTab = PlPlanTypeTab | 'pending_arrange'
|
||||
|
||||
/** 危化品证上传页 Tab(在单据类型后增加「未上传证书」) */
|
||||
export type PlPlanCertTab = PlPlanTypeTab | 'cert_incomplete'
|
||||
|
||||
@@ -334,6 +337,11 @@ export const PL_PLAN_TYPE_TABS: { label: string; name: PlPlanTypeTab }[] = [
|
||||
{ label: '直销单', name: 'direct' }
|
||||
]
|
||||
|
||||
export const PL_PLAN_LIST_TABS: { label: string; name: PlPlanListTab }[] = [
|
||||
...PL_PLAN_TYPE_TABS,
|
||||
{ label: '待安排', name: 'pending_arrange' }
|
||||
]
|
||||
|
||||
export const PL_PLAN_CERT_TABS: { label: string; name: PlPlanCertTab }[] = [
|
||||
...PL_PLAN_TYPE_TABS,
|
||||
{ label: '未上传证书', name: 'cert_incomplete' }
|
||||
@@ -343,6 +351,16 @@ export function plPlanTypeTabToFilter(tab: PlPlanTypeTab): string {
|
||||
return tab === 'all' ? '' : tab
|
||||
}
|
||||
|
||||
export function plPlanListTabToFilters(tab: PlPlanListTab): {
|
||||
planType: string
|
||||
followStatus?: string
|
||||
} {
|
||||
if (tab === 'pending_arrange') {
|
||||
return { planType: '', followStatus: 'pending_arrange' }
|
||||
}
|
||||
return { planType: plPlanTypeTabToFilter(tab) }
|
||||
}
|
||||
|
||||
export function plPlanCertTabToFilters(tab: PlPlanCertTab): {
|
||||
planType: string
|
||||
certUploadStatus?: string
|
||||
|
||||
@@ -57,6 +57,12 @@
|
||||
<el-button class="btn-main" type="primary" @click="openAddDialog">
|
||||
<el-icon><Plus /></el-icon>新增报销申请
|
||||
</el-button>
|
||||
<el-button class="btn-ghost" :loading="templateLoading" @click="downloadImportTemplate">
|
||||
<el-icon><Download /></el-icon>下载模板
|
||||
</el-button>
|
||||
<el-button class="btn-ghost" @click="openImportDialog">
|
||||
<el-icon><FolderAdd /></el-icon>批量导入
|
||||
</el-button>
|
||||
<el-button
|
||||
class="btn-ghost"
|
||||
:disabled="selectedRows.length !== 1 || !isDraftEditable(selectedRows[0])"
|
||||
@@ -83,6 +89,13 @@
|
||||
<el-button class="btn-ghost" @click="openFlowDialog()">
|
||||
<el-icon><View /></el-icon>查看审批流
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="hasActiveFilters"
|
||||
class="btn-ghost"
|
||||
@click="clearAllFilters"
|
||||
>
|
||||
<el-icon><Filter /></el-icon>清空列过滤
|
||||
</el-button>
|
||||
<ListToolbarEndActions
|
||||
:loading="loading"
|
||||
:column-defs="listColumnDefs"
|
||||
@@ -100,7 +113,7 @@
|
||||
<div ref="reimburseApplyTableWrapRef" class="table-scroll bx-list-table-wrap">
|
||||
<ListTable table-key="bx-reimburse-apply"
|
||||
class="list-table"
|
||||
:data="rows"
|
||||
:data="filteredRows"
|
||||
v-loading="loading"
|
||||
stripe
|
||||
@selection-change="onSelectionChange"
|
||||
@@ -113,20 +126,51 @@
|
||||
v-if="isColumnVisible('applyTime')"
|
||||
prop="applyTime"
|
||||
label="申请时间"
|
||||
min-width="160"
|
||||
align="center"
|
||||
header-align="center"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
<el-table-column
|
||||
prop="applyCode"
|
||||
label="报销单号"
|
||||
class-name="col-apply-code"
|
||||
min-width="140"
|
||||
align="center"
|
||||
header-align="center"
|
||||
show-overflow-tooltip
|
||||
>
|
||||
<template #header>
|
||||
<div class="col-header-with-filter">
|
||||
<span class="col-header-with-filter__label">申请时间</span>
|
||||
<ColumnHeaderFilter
|
||||
prop="applyTime"
|
||||
label="申请时间"
|
||||
type="datetime"
|
||||
:is-active="isFilterActive('applyTime')"
|
||||
:model-value="filters.applyTime || null"
|
||||
@update:model-value="(v) => setFilterState('applyTime', v)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<template #default="{ row }">
|
||||
{{ formatApplyDateOnly(row.applyTime) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="applyCode"
|
||||
label="报销单号"
|
||||
class-name="col-apply-code"
|
||||
min-width="160"
|
||||
align="center"
|
||||
header-align="center"
|
||||
show-overflow-tooltip
|
||||
>
|
||||
<template #header>
|
||||
<div class="col-header-with-filter">
|
||||
<span class="col-header-with-filter__label">报销单号</span>
|
||||
<ColumnHeaderFilter
|
||||
prop="applyCode"
|
||||
label="报销单号"
|
||||
type="text"
|
||||
:is-active="isFilterActive('applyCode')"
|
||||
:model-value="filters.applyCode || null"
|
||||
:options="collectUniqueOptions('applyCode')"
|
||||
@update:model-value="(v) => setFilterState('applyCode', v)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<template #default="{ row }">
|
||||
<el-link type="primary" @click="openView(row)">{{ row.applyCode }}</el-link>
|
||||
</template>
|
||||
@@ -135,20 +179,49 @@
|
||||
v-if="isColumnVisible('title')"
|
||||
prop="title"
|
||||
label="报销标题"
|
||||
min-width="180"
|
||||
align="left"
|
||||
header-align="center"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
<el-table-column
|
||||
v-if="isColumnVisible('expenseName')"
|
||||
prop="expenseName"
|
||||
label="费用项目"
|
||||
min-width="260"
|
||||
min-width="200"
|
||||
align="left"
|
||||
header-align="center"
|
||||
show-overflow-tooltip
|
||||
>
|
||||
<template #header>
|
||||
<div class="col-header-with-filter">
|
||||
<span class="col-header-with-filter__label">报销标题</span>
|
||||
<ColumnHeaderFilter
|
||||
prop="title"
|
||||
label="报销标题"
|
||||
type="text"
|
||||
:is-active="isFilterActive('title')"
|
||||
:model-value="filters.title || null"
|
||||
:options="collectUniqueOptions('title')"
|
||||
@update:model-value="(v) => setFilterState('title', v)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
v-if="isColumnVisible('expenseName')"
|
||||
prop="expenseName"
|
||||
label="费用项目"
|
||||
min-width="280"
|
||||
align="left"
|
||||
header-align="center"
|
||||
show-overflow-tooltip
|
||||
>
|
||||
<template #header>
|
||||
<div class="col-header-with-filter">
|
||||
<span class="col-header-with-filter__label">费用项目</span>
|
||||
<ColumnHeaderFilter
|
||||
prop="expenseName"
|
||||
label="费用项目"
|
||||
type="text"
|
||||
:is-active="isFilterActive('expenseName')"
|
||||
:model-value="filters.expenseName || null"
|
||||
:options="collectUniqueOptions('expenseName')"
|
||||
@update:model-value="(v) => setFilterState('expenseName', v)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<template #default="{ row }">
|
||||
<div class="expense-items-lines">
|
||||
<div
|
||||
@@ -167,18 +240,46 @@
|
||||
label="报销金额(元)"
|
||||
:formatter="moneyTableFormatter"
|
||||
class-name="col-money"
|
||||
min-width="120"
|
||||
min-width="150"
|
||||
align="right"
|
||||
header-align="center"
|
||||
/>
|
||||
>
|
||||
<template #header>
|
||||
<div class="col-header-with-filter">
|
||||
<span class="col-header-with-filter__label is-right">报销金额(元)</span>
|
||||
<ColumnHeaderFilter
|
||||
prop="amount"
|
||||
label="报销金额(元)"
|
||||
type="number"
|
||||
:is-active="isFilterActive('amount')"
|
||||
:model-value="filters.amount || null"
|
||||
@update:model-value="(v) => setFilterState('amount', v)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
v-if="isColumnVisible('status')"
|
||||
prop="status"
|
||||
label="状态"
|
||||
min-width="110"
|
||||
min-width="130"
|
||||
align="center"
|
||||
header-align="center"
|
||||
>
|
||||
<template #header>
|
||||
<div class="col-header-with-filter">
|
||||
<span class="col-header-with-filter__label">状态</span>
|
||||
<ColumnHeaderFilter
|
||||
prop="status"
|
||||
label="状态"
|
||||
type="enum"
|
||||
:is-active="isFilterActive('status')"
|
||||
:model-value="filters.status || null"
|
||||
:options="collectUniqueOptions('status')"
|
||||
@update:model-value="(v) => setFilterState('status', v)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="statusTagType(row.status)" size="small">
|
||||
{{ statusLabel(row.status) }}
|
||||
@@ -189,10 +290,24 @@
|
||||
v-if="isColumnVisible('currentNode')"
|
||||
prop="currentNode"
|
||||
label="当前节点"
|
||||
min-width="120"
|
||||
min-width="140"
|
||||
align="center"
|
||||
header-align="center"
|
||||
>
|
||||
<template #header>
|
||||
<div class="col-header-with-filter">
|
||||
<span class="col-header-with-filter__label">当前节点</span>
|
||||
<ColumnHeaderFilter
|
||||
prop="currentNode"
|
||||
label="当前节点"
|
||||
type="text"
|
||||
:is-active="isFilterActive('currentNode')"
|
||||
:model-value="filters.currentNode || null"
|
||||
:options="collectUniqueOptions('currentNode')"
|
||||
@update:model-value="(v) => setFilterState('currentNode', v)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<template #default="{ row }">
|
||||
<el-tag
|
||||
v-if="shouldShowWorkflowNodeTag(row.currentNode, row.status)"
|
||||
@@ -209,7 +324,7 @@
|
||||
ref="reimburseApplyStatsRowRef"
|
||||
storage-key="bx-reimburse-apply-column-stats"
|
||||
:columns="reimburseApplyStatsRowColumns"
|
||||
:data="rows"
|
||||
:data="filteredRows"
|
||||
:table-wrap-el="reimburseApplyTableWrapRef"
|
||||
:get-cell-value="getReimburseApplyStatCellValue"
|
||||
/>
|
||||
@@ -459,6 +574,81 @@
|
||||
@confirm="onInvoicesPicked"
|
||||
/>
|
||||
|
||||
<el-dialog
|
||||
v-model="importDialogVisible"
|
||||
title="批量导入报销申请"
|
||||
width="720px"
|
||||
class="dialog-form-layout"
|
||||
align-center
|
||||
destroy-on-close
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<p class="import-hint">
|
||||
请先下载最新模板填写。灰色【示例】行导入时自动跳过。导入结果为「待提交」草稿,发票请事后在系统中关联。导入完成后可下载带「导入结果」列的结果表。
|
||||
</p>
|
||||
<el-upload
|
||||
drag
|
||||
:auto-upload="false"
|
||||
:limit="1"
|
||||
accept=".xlsx,.xls"
|
||||
:file-list="importFileList"
|
||||
:on-change="onImportFileChange"
|
||||
:on-remove="onImportFileRemove"
|
||||
>
|
||||
<el-icon class="el-icon--upload"><UploadFilled /></el-icon>
|
||||
<div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div>
|
||||
</el-upload>
|
||||
<div v-if="importResult" class="import-result">
|
||||
<p v-if="importResult.tip" class="import-tip">{{ importResult.tip }}</p>
|
||||
<p>
|
||||
成功 {{ importResult.successGroups || 0 }} 组({{ importResult.successRows || 0 }} 行),失败
|
||||
{{ importResult.failGroups || 0 }} 组({{ importResult.failRows || 0 }} 行)
|
||||
<span v-if="importResult.skippedRows">,跳过 {{ importResult.skippedRows }} 行</span>
|
||||
</p>
|
||||
<el-table
|
||||
v-if="importResult.rowResults?.length"
|
||||
:data="importResult.rowResults"
|
||||
size="small"
|
||||
max-height="280"
|
||||
>
|
||||
<el-table-column prop="excelRow" label="行号" min-width="70" align="center" />
|
||||
<el-table-column prop="groupNo" label="分组号" min-width="80" />
|
||||
<el-table-column prop="status" label="导入结果" min-width="90" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag
|
||||
:type="row.status === '成功' ? 'success' : row.status === '失败' ? 'danger' : 'info'"
|
||||
size="small"
|
||||
>
|
||||
{{ row.status }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="message" label="说明" min-width="220" show-overflow-tooltip />
|
||||
</el-table>
|
||||
</div>
|
||||
<template #footer>
|
||||
<el-button class="btn-ghost" @click="importDialogVisible = false">
|
||||
<el-icon><Close /></el-icon>关闭
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="importResult?.resultFileBase64"
|
||||
class="btn-ghost"
|
||||
@click="downloadImportResultFile"
|
||||
>
|
||||
<el-icon><Download /></el-icon>下载结果表
|
||||
</el-button>
|
||||
<el-button
|
||||
class="btn-main"
|
||||
type="primary"
|
||||
:loading="importLoading"
|
||||
:disabled="!importFileList.length"
|
||||
@click="submitImport"
|
||||
>
|
||||
<el-icon><Check /></el-icon>开始导入
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 详情 -->
|
||||
<el-dialog
|
||||
v-model="dialogs.view"
|
||||
@@ -600,8 +790,13 @@
|
||||
<script setup lang="ts">
|
||||
import ListToolbarEndActions from '@/components/business/list-toolbar-end-actions/index.vue'
|
||||
import ListColumnStatsRow from '@/components/business/list-column-stats-row/index.vue'
|
||||
import ColumnHeaderFilter from '@/components/business/column-header-filter/index.vue'
|
||||
import { useDbInstantSearch } from '@/composables/useDbInstantSearch'
|
||||
import { usePersistedListColumns } from '@/composables/usePersistedListColumns'
|
||||
import {
|
||||
useClientColumnFilters,
|
||||
type ColumnFilterDef
|
||||
} from '@/composables/useClientColumnFilters'
|
||||
import ListQuickSearchInput from '@/components/business/list-quick-search-input/index.vue'
|
||||
import { reactive, ref, computed, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
@@ -619,8 +814,13 @@ import {
|
||||
Promotion,
|
||||
VideoPlay,
|
||||
Document,
|
||||
Download
|
||||
Download,
|
||||
FolderAdd,
|
||||
UploadFilled,
|
||||
Check,
|
||||
Filter
|
||||
} from '@element-plus/icons-vue'
|
||||
import type { UploadFile } from 'element-plus'
|
||||
import { reimburseApi, invoiceWalletApi } from '@/api/bx'
|
||||
import { buildApplyDateRangeQuery, type ApplyDateRange } from '@/utils/bx/apply-date-filter'
|
||||
import { canDeleteByCreator, allRowsDeletableByCreator, CREATOR_DELETE_TIP } from '@/utils/creator-delete'
|
||||
@@ -680,6 +880,49 @@ const { rows, total, currentPage, pageSize, rangeText, loading, refresh, search,
|
||||
}
|
||||
)
|
||||
|
||||
const columnFilterDefs: ColumnFilterDef[] = [
|
||||
{ prop: 'applyTime', label: '申请时间', type: 'datetime' },
|
||||
{ prop: 'applyCode', label: '报销单号', type: 'text' },
|
||||
{ prop: 'title', label: '报销标题', type: 'text' },
|
||||
{
|
||||
prop: 'expenseName',
|
||||
label: '费用项目',
|
||||
type: 'text',
|
||||
getValue: (row) => {
|
||||
const lines = getReimburseExpenseItemLines(row.details, row)
|
||||
return lines?.length ? lines.join('\n') : row.expenseName || ''
|
||||
}
|
||||
},
|
||||
{ prop: 'amount', label: '报销金额(元)', type: 'number' },
|
||||
{
|
||||
prop: 'status',
|
||||
label: '状态',
|
||||
type: 'enum',
|
||||
enumOptions: [
|
||||
{ value: 'draft', label: '待提交' },
|
||||
{ value: 'pending', label: '审批中' },
|
||||
{ value: 'approved', label: '审批通过' },
|
||||
{ value: 'rejected', label: '审批驳回' }
|
||||
]
|
||||
},
|
||||
{
|
||||
prop: 'currentNode',
|
||||
label: '当前节点',
|
||||
type: 'text',
|
||||
getValue: (row) => workflowNodeLabel(row.currentNode, row.status) || ''
|
||||
}
|
||||
]
|
||||
|
||||
const {
|
||||
filters,
|
||||
filteredRows,
|
||||
hasActiveFilters,
|
||||
setFilterState,
|
||||
clearAllFilters,
|
||||
isFilterActive,
|
||||
collectUniqueOptions
|
||||
} = useClientColumnFilters(rows, columnFilterDefs)
|
||||
|
||||
const { expenseTree } = useExpenseItemTree()
|
||||
const { applicantOptions, loadApplicantOptions } = useApplicantOptions()
|
||||
const userStore = useUserStore()
|
||||
@@ -742,11 +985,20 @@ const reimburseApplyStatsRowColumns = computed((): StatsRowColumn[] => {
|
||||
return cols
|
||||
})
|
||||
|
||||
function formatApplyDateOnly(value: unknown): string {
|
||||
const text = String(value ?? '').trim()
|
||||
if (!text) return '-'
|
||||
return text.slice(0, 10)
|
||||
}
|
||||
|
||||
function getReimburseApplyStatCellValue(row: any, prop: string): unknown {
|
||||
if (prop === 'expenseName') {
|
||||
const lines = getReimburseExpenseItemLines(row.details, row)
|
||||
return lines?.length ? lines.join('\n') : row.expenseName
|
||||
}
|
||||
if (prop === 'applyTime') {
|
||||
return formatApplyDateOnly(row?.applyTime)
|
||||
}
|
||||
return row?.[prop]
|
||||
}
|
||||
|
||||
@@ -837,6 +1089,12 @@ const walletBalance = ref(0)
|
||||
const editingId = ref<number | null>(null)
|
||||
const titleAutoLinked = ref(true)
|
||||
|
||||
const templateLoading = ref(false)
|
||||
const importDialogVisible = ref(false)
|
||||
const importLoading = ref(false)
|
||||
const importFileList = ref<UploadFile[]>([])
|
||||
const importResult = ref<Record<string, any> | null>(null)
|
||||
|
||||
/** 同一发票共用边框色(与 BxInvoiceSelectDialog / bx-mobile 一致) */
|
||||
const INVOICE_GROUP_COLORS = [
|
||||
'#0891b2',
|
||||
@@ -1113,7 +1371,8 @@ async function openEditDialog(row: any) {
|
||||
titleAutoLinked.value = isAutoReimburseTitle(addForm.title)
|
||||
addForm.remark = data.remark || ''
|
||||
addForm.details = (data.details || []).map((d: any) => ({
|
||||
expenseItemId: d.expenseId,
|
||||
// 费用项 id 可能是纯数字字符串;必须 String,否则 el-tree-select 对不上树节点会直接显示数字
|
||||
expenseItemId: d.expenseId != null && d.expenseId !== '' ? String(d.expenseId) : '',
|
||||
amount: d.amount,
|
||||
remark: d.remark || '',
|
||||
occurDate: d.occurDate || ''
|
||||
@@ -1150,6 +1409,94 @@ async function refreshList() {
|
||||
await refresh()
|
||||
}
|
||||
|
||||
async function downloadImportTemplate() {
|
||||
templateLoading.value = true
|
||||
try {
|
||||
const res: any = await reimburseApi.downloadImportTemplate()
|
||||
const blob =
|
||||
res instanceof Blob
|
||||
? res
|
||||
: new Blob([res as BlobPart], {
|
||||
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
|
||||
})
|
||||
const url = URL.createObjectURL(blob)
|
||||
const link = document.createElement('a')
|
||||
link.href = url
|
||||
link.download = '报销申请导入模板.xlsx'
|
||||
link.click()
|
||||
URL.revokeObjectURL(url)
|
||||
} catch {
|
||||
ElMessage.error('下载模板失败')
|
||||
} finally {
|
||||
templateLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function openImportDialog() {
|
||||
importFileList.value = []
|
||||
importResult.value = null
|
||||
importDialogVisible.value = true
|
||||
}
|
||||
|
||||
function onImportFileChange(_file: UploadFile, files: UploadFile[]) {
|
||||
importFileList.value = files.slice(-1)
|
||||
}
|
||||
|
||||
function onImportFileRemove() {
|
||||
importFileList.value = []
|
||||
}
|
||||
|
||||
async function submitImport() {
|
||||
const raw = importFileList.value[0]?.raw
|
||||
if (!raw) {
|
||||
ElMessage.warning('请选择要导入的文件')
|
||||
return
|
||||
}
|
||||
importLoading.value = true
|
||||
importResult.value = null
|
||||
try {
|
||||
const res: any = await reimburseApi.importExcel(raw)
|
||||
const data = pickBxData(res) || res?.data || res
|
||||
importResult.value = data
|
||||
const ok = Number(data?.successGroups || 0)
|
||||
const fail = Number(data?.failGroups || 0)
|
||||
if (data?.tip) {
|
||||
ElMessage.warning(String(data.tip))
|
||||
} else if (ok > 0 && fail === 0) {
|
||||
ElMessage.success(`成功导入 ${ok} 组`)
|
||||
} else if (ok > 0) {
|
||||
ElMessage.warning(`部分成功:成功 ${ok} 组,失败 ${fail} 组`)
|
||||
} else if (fail > 0) {
|
||||
ElMessage.error('导入失败,请查看结果清单或下载结果表')
|
||||
} else {
|
||||
ElMessage.warning('没有导入任何数据')
|
||||
}
|
||||
if (ok > 0) await refreshList()
|
||||
} finally {
|
||||
importLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function downloadImportResultFile() {
|
||||
const b64 = importResult.value?.resultFileBase64
|
||||
if (!b64) {
|
||||
ElMessage.warning('暂无结果文件可下载')
|
||||
return
|
||||
}
|
||||
const binary = atob(String(b64))
|
||||
const bytes = new Uint8Array(binary.length)
|
||||
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i)
|
||||
const blob = new Blob([bytes], {
|
||||
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
|
||||
})
|
||||
const url = URL.createObjectURL(blob)
|
||||
const link = document.createElement('a')
|
||||
link.href = url
|
||||
link.download = String(importResult.value?.resultFileName || '报销申请导入结果.xlsx')
|
||||
link.click()
|
||||
URL.revokeObjectURL(url)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
refreshList()
|
||||
})
|
||||
@@ -1484,6 +1831,24 @@ function viewDetailSummary({ columns, data }: { columns: any[]; data: any[] }) {
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.import-hint {
|
||||
margin: 0 0 12px;
|
||||
color: var(--mint-text-muted, #64748b);
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.import-tip {
|
||||
margin: 0 0 8px;
|
||||
color: #d97706;
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.import-result {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.invoice-balance-tip {
|
||||
margin: 0 0 10px;
|
||||
font-size: 13px;
|
||||
|
||||
@@ -2,7 +2,11 @@
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<section class="panel">
|
||||
<PlPlanTypeTabs v-model="activePlanTypeTab" @change="handlePlanTypeTabChange" />
|
||||
<PlPlanTypeTabs
|
||||
:model-value="activePlanTypeTab"
|
||||
:tabs="PL_PLAN_LIST_TABS"
|
||||
@change="handlePlanTypeTabChange"
|
||||
/>
|
||||
|
||||
<!-- 筛选区 -->
|
||||
<div class="filter-block">
|
||||
@@ -17,7 +21,12 @@
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="跟进状态">
|
||||
<el-select v-model="searchForm.followStatus" placeholder="全部" clearable>
|
||||
<el-select
|
||||
v-model="searchForm.followStatus"
|
||||
placeholder="全部"
|
||||
clearable
|
||||
:disabled="activePlanTypeTab === 'pending_arrange'"
|
||||
>
|
||||
<el-option
|
||||
v-for="opt in PL_PLAN_FOLLOW_STATUS_OPTIONS"
|
||||
:key="opt.value"
|
||||
@@ -226,9 +235,7 @@
|
||||
align="center"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="getPlanTypeTagType(row.planType)" size="small">
|
||||
{{ getPlanTypeLabel(row.planType) }}
|
||||
</el-tag>
|
||||
{{ getPlanTypeLabel(row.planType) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
@@ -557,8 +564,9 @@ import {
|
||||
getPlPlanFollowStatusTagType,
|
||||
normalizePlPersonnelSummaryDisplay,
|
||||
PL_PLAN_FOLLOW_STATUS_OPTIONS,
|
||||
plPlanTypeTabToFilter,
|
||||
type PlPlanTypeTab,
|
||||
PL_PLAN_LIST_TABS,
|
||||
plPlanListTabToFilters,
|
||||
type PlPlanListTab,
|
||||
type PlPrintSlipCommand
|
||||
} from '@/utils/pl-display'
|
||||
import { syncViewRowFromFlatList } from '@/utils/view-dialog-refresh'
|
||||
@@ -742,7 +750,7 @@ async function handlePlanMutationSuccess(payload?: { planId?: string; openArrang
|
||||
const selectedPlan = computed(() => (selectedRows.value.length === 1 ? selectedRows.value[0] : null))
|
||||
const dateRange = ref<string[]>([])
|
||||
const quickSearch = ref('')
|
||||
const activePlanTypeTab = ref<PlPlanTypeTab>('all')
|
||||
const activePlanTypeTab = ref<PlPlanListTab>('all')
|
||||
|
||||
const searchForm = reactive({
|
||||
planNo: '',
|
||||
@@ -759,11 +767,6 @@ const pagination = reactive({
|
||||
total: 0
|
||||
})
|
||||
|
||||
function getPlanTypeTagType(type: string) {
|
||||
const map: Record<string, string> = { direct: 'primary', inbound: 'success', outbound: 'warning' }
|
||||
return map[type] || 'info'
|
||||
}
|
||||
|
||||
function getPlanTypeLabel(type?: string | null) {
|
||||
if (!type) return '—'
|
||||
const map: Record<string, string> = { direct: '直销单', inbound: '入库单', outbound: '出库单' }
|
||||
@@ -788,11 +791,12 @@ async function fetchData() {
|
||||
loading.value = true
|
||||
try {
|
||||
const keyword = quickSearch.value.trim()
|
||||
const tabFilters = plPlanListTabToFilters(activePlanTypeTab.value)
|
||||
const res = await planApi.list({
|
||||
q: keyword || undefined,
|
||||
planNo: keyword ? undefined : searchForm.planNo || undefined,
|
||||
planType: searchForm.planType || undefined,
|
||||
followStatus: searchForm.followStatus || undefined,
|
||||
planType: tabFilters.planType || undefined,
|
||||
followStatus: tabFilters.followStatus || searchForm.followStatus || undefined,
|
||||
customerName: keyword ? undefined : searchForm.customerName || undefined,
|
||||
productName: keyword ? undefined : searchForm.productName || undefined,
|
||||
licensePlate: keyword ? undefined : searchForm.licensePlate || undefined,
|
||||
@@ -814,8 +818,17 @@ function handleSearch() {
|
||||
fetchData()
|
||||
}
|
||||
|
||||
function handlePlanTypeTabChange(tab: PlPlanTypeTab) {
|
||||
searchForm.planType = plPlanTypeTabToFilter(tab)
|
||||
function handlePlanTypeTabChange(tab: PlPlanListTab | string) {
|
||||
const nextTab = tab as PlPlanListTab
|
||||
const prevTab = activePlanTypeTab.value
|
||||
activePlanTypeTab.value = nextTab
|
||||
const filters = plPlanListTabToFilters(nextTab)
|
||||
searchForm.planType = filters.planType
|
||||
if (nextTab === 'pending_arrange') {
|
||||
searchForm.followStatus = 'pending_arrange'
|
||||
} else if (prevTab === 'pending_arrange') {
|
||||
searchForm.followStatus = ''
|
||||
}
|
||||
pagination.page = 1
|
||||
fetchData()
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user