add card
This commit is contained in:
		
							
								
								
									
										93
									
								
								pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										93
									
								
								pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							@@ -108,6 +108,9 @@ importers:
 | 
				
			|||||||
      lucide-react:
 | 
					      lucide-react:
 | 
				
			||||||
        specifier: ^0.545.0
 | 
					        specifier: ^0.545.0
 | 
				
			||||||
        version: 0.545.0(react@19.2.0)
 | 
					        version: 0.545.0(react@19.2.0)
 | 
				
			||||||
 | 
					      marked:
 | 
				
			||||||
 | 
					        specifier: ^16.4.1
 | 
				
			||||||
 | 
					        version: 16.4.1
 | 
				
			||||||
      nanoid:
 | 
					      nanoid:
 | 
				
			||||||
        specifier: ^5.1.6
 | 
					        specifier: ^5.1.6
 | 
				
			||||||
        version: 5.1.6
 | 
					        version: 5.1.6
 | 
				
			||||||
@@ -132,6 +135,9 @@ importers:
 | 
				
			|||||||
      react-toastify:
 | 
					      react-toastify:
 | 
				
			||||||
        specifier: ^11.0.5
 | 
					        specifier: ^11.0.5
 | 
				
			||||||
        version: 11.0.5(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
 | 
					        version: 11.0.5(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
 | 
				
			||||||
 | 
					      react-virtualized:
 | 
				
			||||||
 | 
					        specifier: ^9.22.6
 | 
				
			||||||
 | 
					        version: 9.22.6(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
 | 
				
			||||||
      sigma:
 | 
					      sigma:
 | 
				
			||||||
        specifier: ^3.0.2
 | 
					        specifier: ^3.0.2
 | 
				
			||||||
        version: 3.0.2(graphology-types@0.24.8)
 | 
					        version: 3.0.2(graphology-types@0.24.8)
 | 
				
			||||||
@@ -163,6 +169,9 @@ importers:
 | 
				
			|||||||
      '@types/react-dom':
 | 
					      '@types/react-dom':
 | 
				
			||||||
        specifier: ^19.2.2
 | 
					        specifier: ^19.2.2
 | 
				
			||||||
        version: 19.2.2(@types/react@19.2.2)
 | 
					        version: 19.2.2(@types/react@19.2.2)
 | 
				
			||||||
 | 
					      '@types/react-virtualized':
 | 
				
			||||||
 | 
					        specifier: ^9.22.3
 | 
				
			||||||
 | 
					        version: 9.22.3
 | 
				
			||||||
      '@types/three':
 | 
					      '@types/three':
 | 
				
			||||||
        specifier: ^0.180.0
 | 
					        specifier: ^0.180.0
 | 
				
			||||||
        version: 0.180.0
 | 
					        version: 0.180.0
 | 
				
			||||||
@@ -1105,11 +1114,17 @@ packages:
 | 
				
			|||||||
  '@types/pouchdb@6.4.2':
 | 
					  '@types/pouchdb@6.4.2':
 | 
				
			||||||
    resolution: {integrity: sha512-YsI47rASdtzR+3V3JE2UKY58snhm0AglHBpyckQBkRYoCbTvGagXHtV0x5n8nzN04jQmvTG+Sm85cIzKT3KXBA==}
 | 
					    resolution: {integrity: sha512-YsI47rASdtzR+3V3JE2UKY58snhm0AglHBpyckQBkRYoCbTvGagXHtV0x5n8nzN04jQmvTG+Sm85cIzKT3KXBA==}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  '@types/prop-types@15.7.15':
 | 
				
			||||||
 | 
					    resolution: {integrity: sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  '@types/react-dom@19.2.2':
 | 
					  '@types/react-dom@19.2.2':
 | 
				
			||||||
    resolution: {integrity: sha512-9KQPoO6mZCi7jcIStSnlOWn2nEF3mNmyr3rIAsGnAbQKYbRLyqmeSc39EVgtxXVia+LMT8j3knZLAZAh+xLmrw==}
 | 
					    resolution: {integrity: sha512-9KQPoO6mZCi7jcIStSnlOWn2nEF3mNmyr3rIAsGnAbQKYbRLyqmeSc39EVgtxXVia+LMT8j3knZLAZAh+xLmrw==}
 | 
				
			||||||
    peerDependencies:
 | 
					    peerDependencies:
 | 
				
			||||||
      '@types/react': ^19.2.0
 | 
					      '@types/react': ^19.2.0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  '@types/react-virtualized@9.22.3':
 | 
				
			||||||
 | 
					    resolution: {integrity: sha512-UKRWeBIrECaKhE4O//TSFhlgwntMwyiEIOA7WZoVkr52Jahv0dH6YIOorqc358N2V3oKFclsq5XxPmx2PiYB5A==}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  '@types/react@19.2.2':
 | 
					  '@types/react@19.2.2':
 | 
				
			||||||
    resolution: {integrity: sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==}
 | 
					    resolution: {integrity: sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -1319,6 +1334,10 @@ packages:
 | 
				
			|||||||
    resolution: {integrity: sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==}
 | 
					    resolution: {integrity: sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==}
 | 
				
			||||||
    engines: {node: '>=0.8'}
 | 
					    engines: {node: '>=0.8'}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  clsx@1.2.1:
 | 
				
			||||||
 | 
					    resolution: {integrity: sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==}
 | 
				
			||||||
 | 
					    engines: {node: '>=6'}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  clsx@2.1.1:
 | 
					  clsx@2.1.1:
 | 
				
			||||||
    resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
 | 
					    resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
 | 
				
			||||||
    engines: {node: '>=6'}
 | 
					    engines: {node: '>=6'}
 | 
				
			||||||
@@ -1428,6 +1447,9 @@ packages:
 | 
				
			|||||||
  dlv@1.1.3:
 | 
					  dlv@1.1.3:
 | 
				
			||||||
    resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==}
 | 
					    resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  dom-helpers@5.2.1:
 | 
				
			||||||
 | 
					    resolution: {integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  dotenv@16.6.1:
 | 
					  dotenv@16.6.1:
 | 
				
			||||||
    resolution: {integrity: sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==}
 | 
					    resolution: {integrity: sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==}
 | 
				
			||||||
    engines: {node: '>=12'}
 | 
					    engines: {node: '>=12'}
 | 
				
			||||||
@@ -1966,6 +1988,10 @@ packages:
 | 
				
			|||||||
  longest-streak@3.1.0:
 | 
					  longest-streak@3.1.0:
 | 
				
			||||||
    resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==}
 | 
					    resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  loose-envify@1.4.0:
 | 
				
			||||||
 | 
					    resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==}
 | 
				
			||||||
 | 
					    hasBin: true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  lru-cache@10.4.3:
 | 
					  lru-cache@10.4.3:
 | 
				
			||||||
    resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==}
 | 
					    resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -1998,6 +2024,11 @@ packages:
 | 
				
			|||||||
  markdown-table@3.0.4:
 | 
					  markdown-table@3.0.4:
 | 
				
			||||||
    resolution: {integrity: sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==}
 | 
					    resolution: {integrity: sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  marked@16.4.1:
 | 
				
			||||||
 | 
					    resolution: {integrity: sha512-ntROs7RaN3EvWfy3EZi14H4YxmT6A5YvywfhO+0pm+cH/dnSQRmdAmoFIc3B9aiwTehyk7pESH4ofyBY+V5hZg==}
 | 
				
			||||||
 | 
					    engines: {node: '>= 20'}
 | 
				
			||||||
 | 
					    hasBin: true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  mdast-util-definitions@6.0.0:
 | 
					  mdast-util-definitions@6.0.0:
 | 
				
			||||||
    resolution: {integrity: sha512-scTllyX6pnYNZH/AIp/0ePz6s4cZtARxImwoPJ7kS42n+MnVsI4XbnG6d4ibehRIldYMWM2LD7ImQblVhUejVQ==}
 | 
					    resolution: {integrity: sha512-scTllyX6pnYNZH/AIp/0ePz6s4cZtARxImwoPJ7kS42n+MnVsI4XbnG6d4ibehRIldYMWM2LD7ImQblVhUejVQ==}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -2258,6 +2289,10 @@ packages:
 | 
				
			|||||||
    resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==}
 | 
					    resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==}
 | 
				
			||||||
    engines: {node: '>=0.10.0'}
 | 
					    engines: {node: '>=0.10.0'}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  object-assign@4.1.1:
 | 
				
			||||||
 | 
					    resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
 | 
				
			||||||
 | 
					    engines: {node: '>=0.10.0'}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  ofetch@1.4.1:
 | 
					  ofetch@1.4.1:
 | 
				
			||||||
    resolution: {integrity: sha512-QZj2DfGplQAr2oj9KzceK9Hwz6Whxazmn85yYeVuS3u9XTMOGMRx0kO95MQ+vLsj/S/NwBDMMLU5hpxvI6Tklw==}
 | 
					    resolution: {integrity: sha512-QZj2DfGplQAr2oj9KzceK9Hwz6Whxazmn85yYeVuS3u9XTMOGMRx0kO95MQ+vLsj/S/NwBDMMLU5hpxvI6Tklw==}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -2402,6 +2437,9 @@ packages:
 | 
				
			|||||||
    resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==}
 | 
					    resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==}
 | 
				
			||||||
    engines: {node: '>= 6'}
 | 
					    engines: {node: '>= 6'}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  prop-types@15.8.1:
 | 
				
			||||||
 | 
					    resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  property-information@6.5.0:
 | 
					  property-information@6.5.0:
 | 
				
			||||||
    resolution: {integrity: sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig==}
 | 
					    resolution: {integrity: sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig==}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -2456,6 +2494,12 @@ packages:
 | 
				
			|||||||
      typescript:
 | 
					      typescript:
 | 
				
			||||||
        optional: true
 | 
					        optional: true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  react-is@16.13.1:
 | 
				
			||||||
 | 
					    resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  react-lifecycles-compat@3.0.4:
 | 
				
			||||||
 | 
					    resolution: {integrity: sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  react-refresh@0.17.0:
 | 
					  react-refresh@0.17.0:
 | 
				
			||||||
    resolution: {integrity: sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==}
 | 
					    resolution: {integrity: sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==}
 | 
				
			||||||
    engines: {node: '>=0.10.0'}
 | 
					    engines: {node: '>=0.10.0'}
 | 
				
			||||||
@@ -2472,6 +2516,12 @@ packages:
 | 
				
			|||||||
      react: ^18 || ^19
 | 
					      react: ^18 || ^19
 | 
				
			||||||
      react-dom: ^18 || ^19
 | 
					      react-dom: ^18 || ^19
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  react-virtualized@9.22.6:
 | 
				
			||||||
 | 
					    resolution: {integrity: sha512-U5j7KuUQt3AaMatlMJ0UJddqSiX+Km0YJxSqbAzIiGw5EmNz0khMyqP2hzgu4+QUtm+QPIrxzUX4raJxmVJnHg==}
 | 
				
			||||||
 | 
					    peerDependencies:
 | 
				
			||||||
 | 
					      react: ^16.3.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
 | 
				
			||||||
 | 
					      react-dom: ^16.3.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  react@19.2.0:
 | 
					  react@19.2.0:
 | 
				
			||||||
    resolution: {integrity: sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==}
 | 
					    resolution: {integrity: sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==}
 | 
				
			||||||
    engines: {node: '>=0.10.0'}
 | 
					    engines: {node: '>=0.10.0'}
 | 
				
			||||||
@@ -4040,10 +4090,17 @@ snapshots:
 | 
				
			|||||||
      '@types/pouchdb-node': 6.1.7
 | 
					      '@types/pouchdb-node': 6.1.7
 | 
				
			||||||
      '@types/pouchdb-replication': 6.4.7
 | 
					      '@types/pouchdb-replication': 6.4.7
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  '@types/prop-types@15.7.15': {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  '@types/react-dom@19.2.2(@types/react@19.2.2)':
 | 
					  '@types/react-dom@19.2.2(@types/react@19.2.2)':
 | 
				
			||||||
    dependencies:
 | 
					    dependencies:
 | 
				
			||||||
      '@types/react': 19.2.2
 | 
					      '@types/react': 19.2.2
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  '@types/react-virtualized@9.22.3':
 | 
				
			||||||
 | 
					    dependencies:
 | 
				
			||||||
 | 
					      '@types/prop-types': 15.7.15
 | 
				
			||||||
 | 
					      '@types/react': 19.2.2
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  '@types/react@19.2.2':
 | 
					  '@types/react@19.2.2':
 | 
				
			||||||
    dependencies:
 | 
					    dependencies:
 | 
				
			||||||
      csstype: 3.1.3
 | 
					      csstype: 3.1.3
 | 
				
			||||||
@@ -4339,6 +4396,8 @@ snapshots:
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  clone@2.1.2: {}
 | 
					  clone@2.1.2: {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  clsx@1.2.1: {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  clsx@2.1.1: {}
 | 
					  clsx@2.1.1: {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  collapse-white-space@2.1.0: {}
 | 
					  collapse-white-space@2.1.0: {}
 | 
				
			||||||
@@ -4419,6 +4478,11 @@ snapshots:
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  dlv@1.1.3: {}
 | 
					  dlv@1.1.3: {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  dom-helpers@5.2.1:
 | 
				
			||||||
 | 
					    dependencies:
 | 
				
			||||||
 | 
					      '@babel/runtime': 7.28.4
 | 
				
			||||||
 | 
					      csstype: 3.1.3
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  dotenv@16.6.1: {}
 | 
					  dotenv@16.6.1: {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  dotenv@17.2.3: {}
 | 
					  dotenv@17.2.3: {}
 | 
				
			||||||
@@ -5015,6 +5079,10 @@ snapshots:
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  longest-streak@3.1.0: {}
 | 
					  longest-streak@3.1.0: {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  loose-envify@1.4.0:
 | 
				
			||||||
 | 
					    dependencies:
 | 
				
			||||||
 | 
					      js-tokens: 4.0.0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  lru-cache@10.4.3: {}
 | 
					  lru-cache@10.4.3: {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  lru-cache@5.1.1:
 | 
					  lru-cache@5.1.1:
 | 
				
			||||||
@@ -5045,6 +5113,8 @@ snapshots:
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  markdown-table@3.0.4: {}
 | 
					  markdown-table@3.0.4: {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  marked@16.4.1: {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  mdast-util-definitions@6.0.0:
 | 
					  mdast-util-definitions@6.0.0:
 | 
				
			||||||
    dependencies:
 | 
					    dependencies:
 | 
				
			||||||
      '@types/mdast': 4.0.4
 | 
					      '@types/mdast': 4.0.4
 | 
				
			||||||
@@ -5548,6 +5618,8 @@ snapshots:
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  normalize-path@3.0.0: {}
 | 
					  normalize-path@3.0.0: {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  object-assign@4.1.1: {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  ofetch@1.4.1:
 | 
					  ofetch@1.4.1:
 | 
				
			||||||
    dependencies:
 | 
					    dependencies:
 | 
				
			||||||
      destr: 2.0.5
 | 
					      destr: 2.0.5
 | 
				
			||||||
@@ -5759,6 +5831,12 @@ snapshots:
 | 
				
			|||||||
      kleur: 3.0.3
 | 
					      kleur: 3.0.3
 | 
				
			||||||
      sisteransi: 1.0.5
 | 
					      sisteransi: 1.0.5
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  prop-types@15.8.1:
 | 
				
			||||||
 | 
					    dependencies:
 | 
				
			||||||
 | 
					      loose-envify: 1.4.0
 | 
				
			||||||
 | 
					      object-assign: 4.1.1
 | 
				
			||||||
 | 
					      react-is: 16.13.1
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  property-information@6.5.0: {}
 | 
					  property-information@6.5.0: {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  property-information@7.1.0: {}
 | 
					  property-information@7.1.0: {}
 | 
				
			||||||
@@ -5809,6 +5887,10 @@ snapshots:
 | 
				
			|||||||
      react-dom: 19.2.0(react@19.2.0)
 | 
					      react-dom: 19.2.0(react@19.2.0)
 | 
				
			||||||
      typescript: 5.9.3
 | 
					      typescript: 5.9.3
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  react-is@16.13.1: {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  react-lifecycles-compat@3.0.4: {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  react-refresh@0.17.0: {}
 | 
					  react-refresh@0.17.0: {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  react-resizable-panels@3.0.6(react-dom@19.2.0(react@19.2.0))(react@19.2.0):
 | 
					  react-resizable-panels@3.0.6(react-dom@19.2.0(react@19.2.0))(react@19.2.0):
 | 
				
			||||||
@@ -5822,6 +5904,17 @@ snapshots:
 | 
				
			|||||||
      react: 19.2.0
 | 
					      react: 19.2.0
 | 
				
			||||||
      react-dom: 19.2.0(react@19.2.0)
 | 
					      react-dom: 19.2.0(react@19.2.0)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  react-virtualized@9.22.6(react-dom@19.2.0(react@19.2.0))(react@19.2.0):
 | 
				
			||||||
 | 
					    dependencies:
 | 
				
			||||||
 | 
					      '@babel/runtime': 7.28.4
 | 
				
			||||||
 | 
					      clsx: 1.2.1
 | 
				
			||||||
 | 
					      dom-helpers: 5.2.1
 | 
				
			||||||
 | 
					      loose-envify: 1.4.0
 | 
				
			||||||
 | 
					      prop-types: 15.8.1
 | 
				
			||||||
 | 
					      react: 19.2.0
 | 
				
			||||||
 | 
					      react-dom: 19.2.0(react@19.2.0)
 | 
				
			||||||
 | 
					      react-lifecycles-compat: 3.0.4
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  react@19.2.0: {}
 | 
					  react@19.2.0: {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  readable-stream@0.0.4: {}
 | 
					  readable-stream@0.0.4: {}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -36,6 +36,7 @@
 | 
				
			|||||||
    "highlight.js": "^11.11.1",
 | 
					    "highlight.js": "^11.11.1",
 | 
				
			||||||
    "lodash-es": "^4.17.21",
 | 
					    "lodash-es": "^4.17.21",
 | 
				
			||||||
    "lucide-react": "^0.545.0",
 | 
					    "lucide-react": "^0.545.0",
 | 
				
			||||||
 | 
					    "marked": "^16.4.1",
 | 
				
			||||||
    "nanoid": "^5.1.6",
 | 
					    "nanoid": "^5.1.6",
 | 
				
			||||||
    "pocketbase": "^0.26.2",
 | 
					    "pocketbase": "^0.26.2",
 | 
				
			||||||
    "pouchdb-adapter-memory": "^9.0.0",
 | 
					    "pouchdb-adapter-memory": "^9.0.0",
 | 
				
			||||||
@@ -44,6 +45,7 @@
 | 
				
			|||||||
    "react-dom": "^19.2.0",
 | 
					    "react-dom": "^19.2.0",
 | 
				
			||||||
    "react-resizable-panels": "^3.0.6",
 | 
					    "react-resizable-panels": "^3.0.6",
 | 
				
			||||||
    "react-toastify": "^11.0.5",
 | 
					    "react-toastify": "^11.0.5",
 | 
				
			||||||
 | 
					    "react-virtualized": "^9.22.6",
 | 
				
			||||||
    "sigma": "^3.0.2",
 | 
					    "sigma": "^3.0.2",
 | 
				
			||||||
    "tailwind-merge": "^3.3.1",
 | 
					    "tailwind-merge": "^3.3.1",
 | 
				
			||||||
    "three": "^0.180.0",
 | 
					    "three": "^0.180.0",
 | 
				
			||||||
@@ -59,6 +61,7 @@
 | 
				
			|||||||
    "@types/pouchdb-browser": "^6.1.5",
 | 
					    "@types/pouchdb-browser": "^6.1.5",
 | 
				
			||||||
    "@types/react": "^19.2.2",
 | 
					    "@types/react": "^19.2.2",
 | 
				
			||||||
    "@types/react-dom": "^19.2.2",
 | 
					    "@types/react-dom": "^19.2.2",
 | 
				
			||||||
 | 
					    "@types/react-virtualized": "^9.22.3",
 | 
				
			||||||
    "@types/three": "^0.180.0",
 | 
					    "@types/three": "^0.180.0",
 | 
				
			||||||
    "@vitejs/plugin-basic-ssl": "^2.1.0",
 | 
					    "@vitejs/plugin-basic-ssl": "^2.1.0",
 | 
				
			||||||
    "dotenv": "^17.2.3",
 | 
					    "dotenv": "^17.2.3",
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										46
									
								
								web/src/apps/muse/base/card/MarkDetailList.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								web/src/apps/muse/base/card/MarkDetailList.css
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,46 @@
 | 
				
			|||||||
 | 
					/* MarkDetailList 组件样式 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* 虚拟化列表容器 */
 | 
				
			||||||
 | 
					.ReactVirtualized__List {
 | 
				
			||||||
 | 
					  outline: none;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* 列表行容器 */
 | 
				
			||||||
 | 
					.ReactVirtualized__List .ReactVirtualized__Grid__innerScrollContainer > div {
 | 
				
			||||||
 | 
					  box-sizing: border-box;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* 防止内容溢出 */
 | 
				
			||||||
 | 
					.mark-detail-row {
 | 
				
			||||||
 | 
					  overflow: hidden;
 | 
				
			||||||
 | 
					  box-sizing: border-box;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* 卡片样式优化 */
 | 
				
			||||||
 | 
					.mark-detail-card {
 | 
				
			||||||
 | 
					  margin-bottom: 8px;
 | 
				
			||||||
 | 
					  box-sizing: border-box;
 | 
				
			||||||
 | 
					  position: relative;
 | 
				
			||||||
 | 
					  z-index: 1;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* 确保图片不会影响布局 */
 | 
				
			||||||
 | 
					.mark-detail-card img {
 | 
				
			||||||
 | 
					  flex-shrink: 0;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* 代码块样式优化 */
 | 
				
			||||||
 | 
					.mark-detail-card pre {
 | 
				
			||||||
 | 
					  word-wrap: break-word;
 | 
				
			||||||
 | 
					  white-space: pre-wrap;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* 链接样式 */
 | 
				
			||||||
 | 
					.mark-detail-card a {
 | 
				
			||||||
 | 
					  word-break: break-all;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* 标签容器 */
 | 
				
			||||||
 | 
					.mark-detail-tags {
 | 
				
			||||||
 | 
					  min-height: 24px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										364
									
								
								web/src/apps/muse/base/card/MarkDetailList.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										364
									
								
								web/src/apps/muse/base/card/MarkDetailList.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,364 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					import React, { useState, useMemo } from 'react';
 | 
				
			||||||
 | 
					import { AutoSizer, List } from 'react-virtualized';
 | 
				
			||||||
 | 
					import './MarkDetailList.css';
 | 
				
			||||||
 | 
					export type MarkShow = {
 | 
				
			||||||
 | 
					  id: string;
 | 
				
			||||||
 | 
					  title?: string;
 | 
				
			||||||
 | 
					  description?: string;
 | 
				
			||||||
 | 
					  tags?: string[];
 | 
				
			||||||
 | 
					  markType?: string;
 | 
				
			||||||
 | 
					  cover?: string;
 | 
				
			||||||
 | 
					  link?: string;
 | 
				
			||||||
 | 
					  summary?: string;
 | 
				
			||||||
 | 
					  key?: string;
 | 
				
			||||||
 | 
					  data: any;
 | 
				
			||||||
 | 
					  createdAt?: string;
 | 
				
			||||||
 | 
					  updatedAt?: string;
 | 
				
			||||||
 | 
					  markedAt?: Date;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export type SimpleMarkShow = {
 | 
				
			||||||
 | 
					  id: string;
 | 
				
			||||||
 | 
					  title?: string;
 | 
				
			||||||
 | 
					  description?: string;
 | 
				
			||||||
 | 
					  tags?: string[];
 | 
				
			||||||
 | 
					  cover?: string;
 | 
				
			||||||
 | 
					  link?: string;
 | 
				
			||||||
 | 
					  summary?: string;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface MarkDetailProps {
 | 
				
			||||||
 | 
					  data: MarkShow[];
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const MarkDetailList: React.FC<MarkDetailProps> = ({ data = [] }) => {
 | 
				
			||||||
 | 
					  const [showAll, setShowAll] = useState(false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // 根据显示模式过滤数据
 | 
				
			||||||
 | 
					  const displayData = useMemo(() => {
 | 
				
			||||||
 | 
					    if (showAll) {
 | 
				
			||||||
 | 
					      // 显示所有字段
 | 
				
			||||||
 | 
					      return data;
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      // 仅显示 SimpleMarkShow 字段
 | 
				
			||||||
 | 
					      return data.map(item => ({
 | 
				
			||||||
 | 
					        id: item.id,
 | 
				
			||||||
 | 
					        title: item.title,
 | 
				
			||||||
 | 
					        description: item.description,
 | 
				
			||||||
 | 
					        tags: item.tags,
 | 
				
			||||||
 | 
					        cover: item.cover,
 | 
				
			||||||
 | 
					        link: item.link,
 | 
				
			||||||
 | 
					        summary: item.summary
 | 
				
			||||||
 | 
					      } as SimpleMarkShow));
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }, [data, showAll]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // 计算行高 - 根据视图模式计算固定高度
 | 
				
			||||||
 | 
					  const getRowHeight = () => {
 | 
				
			||||||
 | 
					    return showAll ? 400 : 240;
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // 渲染单个简化项目
 | 
				
			||||||
 | 
					  const renderSimpleItem = (item: SimpleMarkShow) => {
 | 
				
			||||||
 | 
					    return (
 | 
				
			||||||
 | 
					      <div className="mark-detail-card border border-gray-200 rounded-lg p-4 bg-white shadow-sm">
 | 
				
			||||||
 | 
					        <div className="space-y-2">
 | 
				
			||||||
 | 
					          <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
 | 
				
			||||||
 | 
					            <div>
 | 
				
			||||||
 | 
					              <label className="text-sm font-medium text-gray-600">ID:</label>
 | 
				
			||||||
 | 
					              <p className="text-sm text-gray-800 mt-1">{item.id}</p>
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					            <div>
 | 
				
			||||||
 | 
					              <label className="text-sm font-medium text-gray-600">标题:</label>
 | 
				
			||||||
 | 
					              <p className="text-sm text-gray-800 mt-1">{item.title || '-'}</p>
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					          
 | 
				
			||||||
 | 
					          <div>
 | 
				
			||||||
 | 
					            <label className="text-sm font-medium text-gray-600">描述:</label>
 | 
				
			||||||
 | 
					            <p className="text-sm text-gray-800 mt-1">{item.description || '-'}</p>
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					          
 | 
				
			||||||
 | 
					          <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
 | 
				
			||||||
 | 
					            <div>
 | 
				
			||||||
 | 
					              <label className="text-sm font-medium text-gray-600">标签:</label>
 | 
				
			||||||
 | 
					              <div className="mt-1 mark-detail-tags">{renderTags(item.tags)}</div>
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					            <div>
 | 
				
			||||||
 | 
					              <label className="text-sm font-medium text-gray-600">摘要:</label>
 | 
				
			||||||
 | 
					              <p className="text-sm text-gray-800 mt-1">{item.summary || '-'}</p>
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					          
 | 
				
			||||||
 | 
					          <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
 | 
				
			||||||
 | 
					            <div>
 | 
				
			||||||
 | 
					              <label className="text-sm font-medium text-gray-600">封面:</label>
 | 
				
			||||||
 | 
					              <div className="mt-1">
 | 
				
			||||||
 | 
					                {item.cover ? (
 | 
				
			||||||
 | 
					                  <img 
 | 
				
			||||||
 | 
					                    src={item.cover} 
 | 
				
			||||||
 | 
					                    alt="封面" 
 | 
				
			||||||
 | 
					                    className="w-16 h-16 object-cover rounded-md"
 | 
				
			||||||
 | 
					                    onError={(e) => {
 | 
				
			||||||
 | 
					                      (e.target as HTMLImageElement).style.display = 'none';
 | 
				
			||||||
 | 
					                    }}
 | 
				
			||||||
 | 
					                  />
 | 
				
			||||||
 | 
					                ) : (
 | 
				
			||||||
 | 
					                  <span className="text-sm text-gray-500">-</span>
 | 
				
			||||||
 | 
					                )}
 | 
				
			||||||
 | 
					              </div>
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					            <div>
 | 
				
			||||||
 | 
					              <label className="text-sm font-medium text-gray-600">链接:</label>
 | 
				
			||||||
 | 
					              <div className="mt-1">
 | 
				
			||||||
 | 
					                {item.link ? (
 | 
				
			||||||
 | 
					                  <a 
 | 
				
			||||||
 | 
					                    href={item.link} 
 | 
				
			||||||
 | 
					                    target="_blank" 
 | 
				
			||||||
 | 
					                    rel="noopener noreferrer"
 | 
				
			||||||
 | 
					                    className="text-sm text-blue-600 hover:text-blue-800 underline"
 | 
				
			||||||
 | 
					                  >
 | 
				
			||||||
 | 
					                    {item.link}
 | 
				
			||||||
 | 
					                  </a>
 | 
				
			||||||
 | 
					                ) : (
 | 
				
			||||||
 | 
					                  <span className="text-sm text-gray-500">-</span>
 | 
				
			||||||
 | 
					                )}
 | 
				
			||||||
 | 
					              </div>
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // 渲染单个完整项目
 | 
				
			||||||
 | 
					  const renderFullItem = (item: MarkShow) => {
 | 
				
			||||||
 | 
					    return (
 | 
				
			||||||
 | 
					      <div className="mark-detail-card border border-gray-200 rounded-lg p-4 bg-white shadow-sm">
 | 
				
			||||||
 | 
					        <div className="space-y-3">
 | 
				
			||||||
 | 
					          <div className="grid grid-cols-1 md:grid-cols-3 gap-4">
 | 
				
			||||||
 | 
					            <div>
 | 
				
			||||||
 | 
					              <label className="text-sm font-medium text-gray-600">ID:</label>
 | 
				
			||||||
 | 
					              <p className="text-sm text-gray-800 mt-1">{item.id}</p>
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					            <div>
 | 
				
			||||||
 | 
					              <label className="text-sm font-medium text-gray-600">标题:</label>
 | 
				
			||||||
 | 
					              <p className="text-sm text-gray-800 mt-1">{item.title || '-'}</p>
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					            <div>
 | 
				
			||||||
 | 
					              <label className="text-sm font-medium text-gray-600">类型:</label>
 | 
				
			||||||
 | 
					              <div className="mt-1">{renderMarkType(item.markType)}</div>
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					          
 | 
				
			||||||
 | 
					          <div>
 | 
				
			||||||
 | 
					            <label className="text-sm font-medium text-gray-600">描述:</label>
 | 
				
			||||||
 | 
					            <p className="text-sm text-gray-800 mt-1">{item.description || '-'}</p>
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					          
 | 
				
			||||||
 | 
					          <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
 | 
				
			||||||
 | 
					            <div>
 | 
				
			||||||
 | 
					              <label className="text-sm font-medium text-gray-600">标签:</label>
 | 
				
			||||||
 | 
					              <div className="mt-1 mark-detail-tags">{renderTags(item.tags)}</div>
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					            <div>
 | 
				
			||||||
 | 
					              <label className="text-sm font-medium text-gray-600">摘要:</label>
 | 
				
			||||||
 | 
					              <p className="text-sm text-gray-800 mt-1">{item.summary || '-'}</p>
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					          
 | 
				
			||||||
 | 
					          <div className="grid grid-cols-1 md:grid-cols-3 gap-4">
 | 
				
			||||||
 | 
					            <div>
 | 
				
			||||||
 | 
					              <label className="text-sm font-medium text-gray-600">键值:</label>
 | 
				
			||||||
 | 
					              <p className="text-sm text-gray-800 mt-1">{item.key || '-'}</p>
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					            <div>
 | 
				
			||||||
 | 
					              <label className="text-sm font-medium text-gray-600">创建时间:</label>
 | 
				
			||||||
 | 
					              <p className="text-sm text-gray-800 mt-1">{formatDate(item.createdAt)}</p>
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					            <div>
 | 
				
			||||||
 | 
					              <label className="text-sm font-medium text-gray-600">更新时间:</label>
 | 
				
			||||||
 | 
					              <p className="text-sm text-gray-800 mt-1">{formatDate(item.updatedAt)}</p>
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					          
 | 
				
			||||||
 | 
					          <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
 | 
				
			||||||
 | 
					            <div>
 | 
				
			||||||
 | 
					              <label className="text-sm font-medium text-gray-600">标记时间:</label>
 | 
				
			||||||
 | 
					              <p className="text-sm text-gray-800 mt-1">{formatDate(item.markedAt)}</p>
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					            <div>
 | 
				
			||||||
 | 
					              <label className="text-sm font-medium text-gray-600">封面:</label>
 | 
				
			||||||
 | 
					              <div className="mt-1">
 | 
				
			||||||
 | 
					                {item.cover ? (
 | 
				
			||||||
 | 
					                  <img 
 | 
				
			||||||
 | 
					                    src={item.cover} 
 | 
				
			||||||
 | 
					                    alt="封面" 
 | 
				
			||||||
 | 
					                    className="w-16 h-16 object-cover rounded-md"
 | 
				
			||||||
 | 
					                    onError={(e) => {
 | 
				
			||||||
 | 
					                      (e.target as HTMLImageElement).style.display = 'none';
 | 
				
			||||||
 | 
					                    }}
 | 
				
			||||||
 | 
					                  />
 | 
				
			||||||
 | 
					                ) : (
 | 
				
			||||||
 | 
					                  <span className="text-sm text-gray-500">-</span>
 | 
				
			||||||
 | 
					                )}
 | 
				
			||||||
 | 
					              </div>
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					          
 | 
				
			||||||
 | 
					          <div>
 | 
				
			||||||
 | 
					            <label className="text-sm font-medium text-gray-600">链接:</label>
 | 
				
			||||||
 | 
					            <div className="mt-1">
 | 
				
			||||||
 | 
					              {item.link ? (
 | 
				
			||||||
 | 
					                <a 
 | 
				
			||||||
 | 
					                  href={item.link} 
 | 
				
			||||||
 | 
					                  target="_blank" 
 | 
				
			||||||
 | 
					                  rel="noopener noreferrer"
 | 
				
			||||||
 | 
					                  className="text-sm text-blue-600 hover:text-blue-800 underline"
 | 
				
			||||||
 | 
					                >
 | 
				
			||||||
 | 
					                  {item.link}
 | 
				
			||||||
 | 
					                </a>
 | 
				
			||||||
 | 
					              ) : (
 | 
				
			||||||
 | 
					                <span className="text-sm text-gray-500">-</span>
 | 
				
			||||||
 | 
					              )}
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					          
 | 
				
			||||||
 | 
					          <div>
 | 
				
			||||||
 | 
					            <label className="text-sm font-medium text-gray-600">数据:</label>
 | 
				
			||||||
 | 
					            <div className="mt-1">
 | 
				
			||||||
 | 
					              <pre className="text-xs bg-gray-50 p-2 rounded-md overflow-x-auto max-h-32">
 | 
				
			||||||
 | 
					                {JSON.stringify(item.data, null, 2)}
 | 
				
			||||||
 | 
					              </pre>
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // 行渲染函数
 | 
				
			||||||
 | 
					  const rowRenderer = ({ index, key, style }: any) => {
 | 
				
			||||||
 | 
					    const item = displayData[index];
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    return (
 | 
				
			||||||
 | 
					      <div key={key} style={{ ...style, overflow: 'hidden' }} className="mark-detail-row">
 | 
				
			||||||
 | 
					        <div className="px-4 py-2">
 | 
				
			||||||
 | 
					          {showAll ? renderFullItem(item as MarkShow) : renderSimpleItem(item as SimpleMarkShow)}
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // 格式化日期显示
 | 
				
			||||||
 | 
					  const formatDate = (date: string | Date | undefined) => {
 | 
				
			||||||
 | 
					    if (!date) return '-';
 | 
				
			||||||
 | 
					    return new Date(date).toLocaleString('zh-CN');
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // 渲染标签
 | 
				
			||||||
 | 
					  const renderTags = (tags?: string[]) => {
 | 
				
			||||||
 | 
					    if (!tags || tags.length === 0) return '-';
 | 
				
			||||||
 | 
					    return (
 | 
				
			||||||
 | 
					      <div className="flex flex-wrap gap-1">
 | 
				
			||||||
 | 
					        {tags.map((tag, index) => (
 | 
				
			||||||
 | 
					          <span
 | 
				
			||||||
 | 
					            key={index}
 | 
				
			||||||
 | 
					            className="inline-block px-2 py-1 text-xs bg-blue-100 text-blue-800 rounded-md"
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
 | 
					            {tag}
 | 
				
			||||||
 | 
					          </span>
 | 
				
			||||||
 | 
					        ))}
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // 渲染类型徽章
 | 
				
			||||||
 | 
					  const renderMarkType = (markType?: string) => {
 | 
				
			||||||
 | 
					    if (!markType) return '-';
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    const typeColors: Record<string, string> = {
 | 
				
			||||||
 | 
					      markdown: 'bg-green-100 text-green-800',
 | 
				
			||||||
 | 
					      json: 'bg-yellow-100 text-yellow-800',
 | 
				
			||||||
 | 
					      html: 'bg-orange-100 text-orange-800',
 | 
				
			||||||
 | 
					      image: 'bg-purple-100 text-purple-800',
 | 
				
			||||||
 | 
					      video: 'bg-red-100 text-red-800',
 | 
				
			||||||
 | 
					      audio: 'bg-pink-100 text-pink-800',
 | 
				
			||||||
 | 
					      code: 'bg-gray-100 text-gray-800',
 | 
				
			||||||
 | 
					      link: 'bg-blue-100 text-blue-800',
 | 
				
			||||||
 | 
					      file: 'bg-indigo-100 text-indigo-800',
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const colorClass = typeColors[markType] || 'bg-gray-100 text-gray-800';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return (
 | 
				
			||||||
 | 
					      <span className={`inline-block px-2 py-1 text-xs rounded-md ${colorClass}`}>
 | 
				
			||||||
 | 
					        {markType}
 | 
				
			||||||
 | 
					      </span>
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // 渲染虚拟滚动列表
 | 
				
			||||||
 | 
					  const renderVirtualizedList = () => {
 | 
				
			||||||
 | 
					    return (
 | 
				
			||||||
 | 
					      <AutoSizer>
 | 
				
			||||||
 | 
					        {({ height, width }) => (
 | 
				
			||||||
 | 
					          <List
 | 
				
			||||||
 | 
					            height={height}
 | 
				
			||||||
 | 
					            width={width}
 | 
				
			||||||
 | 
					            rowCount={displayData.length}
 | 
				
			||||||
 | 
					            rowHeight={getRowHeight()}
 | 
				
			||||||
 | 
					            rowRenderer={rowRenderer}
 | 
				
			||||||
 | 
					          />
 | 
				
			||||||
 | 
					        )}
 | 
				
			||||||
 | 
					      </AutoSizer>
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <div className="h-full flex flex-col">
 | 
				
			||||||
 | 
					      {/* 头部控制区域 */}
 | 
				
			||||||
 | 
					      <div className="flex-shrink-0 bg-white border-b border-gray-200 p-4">
 | 
				
			||||||
 | 
					        <div className="flex items-center justify-between">
 | 
				
			||||||
 | 
					          <h2 className="text-lg font-semibold text-gray-900">
 | 
				
			||||||
 | 
					            SimpleMark 信息显示
 | 
				
			||||||
 | 
					          </h2>
 | 
				
			||||||
 | 
					          <div className="flex items-center space-x-3">
 | 
				
			||||||
 | 
					            <span className="text-sm text-gray-600">
 | 
				
			||||||
 | 
					              显示模式:
 | 
				
			||||||
 | 
					            </span>
 | 
				
			||||||
 | 
					            <label className="flex items-center space-x-2 cursor-pointer">
 | 
				
			||||||
 | 
					              <input
 | 
				
			||||||
 | 
					                type="checkbox"
 | 
				
			||||||
 | 
					                checked={showAll}
 | 
				
			||||||
 | 
					                onChange={(e) => setShowAll(e.target.checked)}
 | 
				
			||||||
 | 
					                className="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 focus:ring-2"
 | 
				
			||||||
 | 
					              />
 | 
				
			||||||
 | 
					              <span className="text-sm text-gray-700">
 | 
				
			||||||
 | 
					                {showAll ? '显示全部字段' : '仅显示基础字段'}
 | 
				
			||||||
 | 
					              </span>
 | 
				
			||||||
 | 
					            </label>
 | 
				
			||||||
 | 
					            <div className="text-sm text-gray-500">
 | 
				
			||||||
 | 
					              共 {data.length} 条记录
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      {/* 数据显示区域 */}
 | 
				
			||||||
 | 
					      <div className="flex-1 overflow-y-auto p-4 bg-gray-50">
 | 
				
			||||||
 | 
					        {data.length === 0 ? (
 | 
				
			||||||
 | 
					          <div className="flex items-center justify-center h-full">
 | 
				
			||||||
 | 
					            <div className="text-center">
 | 
				
			||||||
 | 
					              <div className="text-4xl text-gray-400 mb-4">📝</div>
 | 
				
			||||||
 | 
					              <p className="text-gray-500">暂无数据</p>
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					        ) : (
 | 
				
			||||||
 | 
					          renderVirtualizedList()
 | 
				
			||||||
 | 
					        )}
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
							
								
								
									
										191
									
								
								web/src/apps/muse/base/docs/README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										191
									
								
								web/src/apps/muse/base/docs/README.md
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,191 @@
 | 
				
			|||||||
 | 
					# 文档组件 (DocsComponent)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					一个现代化的左侧导航右侧内容的文档显示组件,支持多种内容类型和优雅的样式设计。
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## 特性
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- 🎨 **现代化设计**: 清新的UI设计,优雅的色彩搭配
 | 
				
			||||||
 | 
					- 📱 **响应式布局**: 支持桌面和移动端适配
 | 
				
			||||||
 | 
					- 📝 **Markdown支持**: 使用 marked 库,支持 GitHub Flavored Markdown
 | 
				
			||||||
 | 
					- 🔄 **多内容类型**: 支持Markdown、代码、JSON、图片等多种类型
 | 
				
			||||||
 | 
					- ⚡ **流畅交互**: 带加载状态和平滑过渡动画
 | 
				
			||||||
 | 
					- 🏷️ **标签系统**: 支持文档标签和分类
 | 
				
			||||||
 | 
					- 🔍 **清晰导航**: 左侧树形导航,快速定位文档
 | 
				
			||||||
 | 
					- ✨ **丰富语法**: 支持表格、任务列表、代码高亮等 GFM 特性
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## 使用方法
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### 基础用法
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					```tsx
 | 
				
			||||||
 | 
					import React from 'react';
 | 
				
			||||||
 | 
					import { DocsComponent } from './docs';
 | 
				
			||||||
 | 
					import { mockMarks } from './mock/collection';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function App() {
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <div style={{ height: '100vh' }}>
 | 
				
			||||||
 | 
					      <DocsComponent dataSource={mockMarks} />
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### 自定义数据
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					```tsx
 | 
				
			||||||
 | 
					import { DocsComponent, Mark } from './docs';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const customDocs: Mark[] = [
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    id: '1',
 | 
				
			||||||
 | 
					    title: '快速开始',
 | 
				
			||||||
 | 
					    description: '了解如何快速开始使用我们的产品',
 | 
				
			||||||
 | 
					    tags: ['入门', '指南'],
 | 
				
			||||||
 | 
					    markType: 'markdown',
 | 
				
			||||||
 | 
					    data: {
 | 
				
			||||||
 | 
					      content: `# 快速开始
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					这里是文档内容...`
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    createdAt: new Date(),
 | 
				
			||||||
 | 
					    updatedAt: new Date()
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function App() {
 | 
				
			||||||
 | 
					  return <DocsComponent dataSource={customDocs} />;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## 数据结构
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					组件接受一个 `Mark[]` 类型的数据源,每个Mark对象包含:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					```typescript
 | 
				
			||||||
 | 
					type Mark = {
 | 
				
			||||||
 | 
					  id: string;                    // 唯一标识
 | 
				
			||||||
 | 
					  title?: string;                // 文档标题
 | 
				
			||||||
 | 
					  description?: string;          // 文档描述
 | 
				
			||||||
 | 
					  tags?: string[];              // 标签数组
 | 
				
			||||||
 | 
					  markType?: string;            // 内容类型
 | 
				
			||||||
 | 
					  data: any;                    // 内容数据
 | 
				
			||||||
 | 
					  createdAt: Date;              // 创建时间
 | 
				
			||||||
 | 
					  updatedAt: Date;              // 更新时间
 | 
				
			||||||
 | 
					  // ... 其他字段
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## 支持的内容类型
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### Markdown
 | 
				
			||||||
 | 
					```typescript
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					  markType: 'markdown',
 | 
				
			||||||
 | 
					  data: {
 | 
				
			||||||
 | 
					    content: '# 标题\n\n这是Markdown内容...'
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### 代码
 | 
				
			||||||
 | 
					```typescript
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					  markType: 'code',
 | 
				
			||||||
 | 
					  data: {
 | 
				
			||||||
 | 
					    code: 'const hello = "world";',
 | 
				
			||||||
 | 
					    language: 'javascript'
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### JSON数据
 | 
				
			||||||
 | 
					```typescript
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					  markType: 'json',
 | 
				
			||||||
 | 
					  data: {
 | 
				
			||||||
 | 
					    // 任何JSON数据
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### 图片
 | 
				
			||||||
 | 
					```typescript
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					  markType: 'image',
 | 
				
			||||||
 | 
					  data: {
 | 
				
			||||||
 | 
					    src: 'https://example.com/image.jpg',
 | 
				
			||||||
 | 
					    alt: '图片描述'
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## 样式定制
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					组件使用CSS类名,你可以通过覆盖这些类名来定制样式:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					```css
 | 
				
			||||||
 | 
					/* 主容器 */
 | 
				
			||||||
 | 
					.docs-container { }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* 导航区域 */
 | 
				
			||||||
 | 
					.docs-nav { }
 | 
				
			||||||
 | 
					.docs-nav-item { }
 | 
				
			||||||
 | 
					.docs-nav-link { }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* 内容区域 */
 | 
				
			||||||
 | 
					.docs-content { }
 | 
				
			||||||
 | 
					.docs-content-header { }
 | 
				
			||||||
 | 
					.docs-content-body { }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* Markdown内容 */
 | 
				
			||||||
 | 
					.docs-markdown { }
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## 组件API
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### Props
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					| 属性 | 类型 | 默认值 | 描述 |
 | 
				
			||||||
 | 
					|------|------|--------|------|
 | 
				
			||||||
 | 
					| dataSource | Mark[] | [] | 文档数据源 |
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### 导出组件
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- `DocsComponent`: 主要的文档组件
 | 
				
			||||||
 | 
					- `App`: DocsComponent的别名,保持向后兼容
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## 示例
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					查看 `example.tsx` 文件获取完整的使用示例。
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## 开发
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					```bash
 | 
				
			||||||
 | 
					# 安装依赖
 | 
				
			||||||
 | 
					npm install marked
 | 
				
			||||||
 | 
					# 或
 | 
				
			||||||
 | 
					pnpm install marked
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					组件使用 `marked` 库进行 Markdown 渲染,支持:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- ✅ GitHub Flavored Markdown (GFM)
 | 
				
			||||||
 | 
					- ✅ 表格语法
 | 
				
			||||||
 | 
					- ✅ 任务列表 
 | 
				
			||||||
 | 
					- ✅ 代码块语法高亮
 | 
				
			||||||
 | 
					- ✅ 自动链接识别
 | 
				
			||||||
 | 
					- ✅ 删除线语法
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## 注意事项
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					1. 确保容器有足够的高度(建议设置为 `100vh`)
 | 
				
			||||||
 | 
					2. 组件会自动选中第一个文档项
 | 
				
			||||||
 | 
					3. 支持键盘导航和无障碍访问
 | 
				
			||||||
 | 
					4. 在移动端会自动调整为上下布局
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## 更新日志
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- v1.0.0: 初始版本,支持基础的文档显示功能
 | 
				
			||||||
 | 
					- 支持Markdown、代码、JSON、图片等内容类型
 | 
				
			||||||
 | 
					- 响应式设计和现代化UI
 | 
				
			||||||
							
								
								
									
										542
									
								
								web/src/apps/muse/base/docs/docs.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										542
									
								
								web/src/apps/muse/base/docs/docs.css
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,542 @@
 | 
				
			|||||||
 | 
					/* 文档组件样式 */
 | 
				
			||||||
 | 
					.docs-container {
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  height: 100%;
 | 
				
			||||||
 | 
					  background: #f8fafc;
 | 
				
			||||||
 | 
					  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* 左侧导航区域 */
 | 
				
			||||||
 | 
					.docs-nav {
 | 
				
			||||||
 | 
					  width: 280px;
 | 
				
			||||||
 | 
					  background: #ffffff;
 | 
				
			||||||
 | 
					  border-right: 1px solid #e2e8f0;
 | 
				
			||||||
 | 
					  flex-shrink: 0;
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  flex-direction: column;
 | 
				
			||||||
 | 
					  overflow: hidden;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.docs-nav-header {
 | 
				
			||||||
 | 
					  padding: 20px;
 | 
				
			||||||
 | 
					  border-bottom: 1px solid #e2e8f0;
 | 
				
			||||||
 | 
					  background: #f8fafc;
 | 
				
			||||||
 | 
					  position: sticky;
 | 
				
			||||||
 | 
					  top: 0;
 | 
				
			||||||
 | 
					  z-index: 10;
 | 
				
			||||||
 | 
					  flex-shrink: 0;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.docs-nav-title {
 | 
				
			||||||
 | 
					  font-size: 18px;
 | 
				
			||||||
 | 
					  font-weight: 600;
 | 
				
			||||||
 | 
					  color: #1e293b;
 | 
				
			||||||
 | 
					  margin: 0;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.docs-nav-list {
 | 
				
			||||||
 | 
					  list-style: none;
 | 
				
			||||||
 | 
					  padding: 0;
 | 
				
			||||||
 | 
					  margin: 0;
 | 
				
			||||||
 | 
					  flex: 1;
 | 
				
			||||||
 | 
					  overflow-y: auto;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.docs-nav-item {
 | 
				
			||||||
 | 
					  border-bottom: 1px solid #f1f5f9;
 | 
				
			||||||
 | 
					  transition: all 0.2s ease;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.docs-nav-item:last-child {
 | 
				
			||||||
 | 
					  border-bottom: none;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.docs-nav-link {
 | 
				
			||||||
 | 
					  display: block;
 | 
				
			||||||
 | 
					  padding: 16px 20px;
 | 
				
			||||||
 | 
					  color: #64748b;
 | 
				
			||||||
 | 
					  text-decoration: none;
 | 
				
			||||||
 | 
					  transition: all 0.2s ease;
 | 
				
			||||||
 | 
					  cursor: pointer;
 | 
				
			||||||
 | 
					  border-left: 3px solid transparent;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.docs-nav-link:hover {
 | 
				
			||||||
 | 
					  background: #f8fafc;
 | 
				
			||||||
 | 
					  color: #334155;
 | 
				
			||||||
 | 
					  border-left-color: #e2e8f0;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.docs-nav-link.active {
 | 
				
			||||||
 | 
					  background: #eff6ff;
 | 
				
			||||||
 | 
					  color: #2563eb;
 | 
				
			||||||
 | 
					  border-left-color: #2563eb;
 | 
				
			||||||
 | 
					  font-weight: 500;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.docs-nav-link-title {
 | 
				
			||||||
 | 
					  font-size: 14px;
 | 
				
			||||||
 | 
					  font-weight: 500;
 | 
				
			||||||
 | 
					  margin-bottom: 4px;
 | 
				
			||||||
 | 
					  line-height: 1.4;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.docs-nav-link-desc {
 | 
				
			||||||
 | 
					  font-size: 12px;
 | 
				
			||||||
 | 
					  color: #94a3b8;
 | 
				
			||||||
 | 
					  line-height: 1.3;
 | 
				
			||||||
 | 
					  overflow: hidden;
 | 
				
			||||||
 | 
					  text-overflow: ellipsis;
 | 
				
			||||||
 | 
					  display: -webkit-box;
 | 
				
			||||||
 | 
					  -webkit-line-clamp: 2;
 | 
				
			||||||
 | 
					  line-clamp: 2;
 | 
				
			||||||
 | 
					  -webkit-box-orient: vertical;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.docs-nav-link.active .docs-nav-link-desc {
 | 
				
			||||||
 | 
					  color: #60a5fa;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* 右侧内容区域 */
 | 
				
			||||||
 | 
					.docs-content {
 | 
				
			||||||
 | 
					  flex: 1;
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  flex-direction: column;
 | 
				
			||||||
 | 
					  overflow: hidden;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.docs-content-header {
 | 
				
			||||||
 | 
					  padding: 20px 32px;
 | 
				
			||||||
 | 
					  background: #ffffff;
 | 
				
			||||||
 | 
					  border-bottom: 1px solid #e2e8f0;
 | 
				
			||||||
 | 
					  flex-shrink: 0;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.docs-content-title {
 | 
				
			||||||
 | 
					  font-size: 24px;
 | 
				
			||||||
 | 
					  font-weight: 700;
 | 
				
			||||||
 | 
					  color: #1e293b;
 | 
				
			||||||
 | 
					  margin: 0 0 8px 0;
 | 
				
			||||||
 | 
					  line-height: 1.3;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.docs-content-meta {
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  gap: 16px;
 | 
				
			||||||
 | 
					  align-items: center;
 | 
				
			||||||
 | 
					  margin-top: 8px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.docs-content-tag {
 | 
				
			||||||
 | 
					  display: inline-flex;
 | 
				
			||||||
 | 
					  align-items: center;
 | 
				
			||||||
 | 
					  padding: 4px 8px;
 | 
				
			||||||
 | 
					  background: #f1f5f9;
 | 
				
			||||||
 | 
					  color: #475569;
 | 
				
			||||||
 | 
					  border-radius: 6px;
 | 
				
			||||||
 | 
					  font-size: 12px;
 | 
				
			||||||
 | 
					  font-weight: 500;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.docs-content-date {
 | 
				
			||||||
 | 
					  font-size: 12px;
 | 
				
			||||||
 | 
					  color: #94a3b8;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.docs-content-body {
 | 
				
			||||||
 | 
					  flex: 1;
 | 
				
			||||||
 | 
					  padding: 32px;
 | 
				
			||||||
 | 
					  overflow-y: auto;
 | 
				
			||||||
 | 
					  background: #ffffff;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.docs-content-body.empty {
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  align-items: center;
 | 
				
			||||||
 | 
					  justify-content: center;
 | 
				
			||||||
 | 
					  flex-direction: column;
 | 
				
			||||||
 | 
					  color: #94a3b8;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.docs-empty-icon {
 | 
				
			||||||
 | 
					  font-size: 48px;
 | 
				
			||||||
 | 
					  margin-bottom: 16px;
 | 
				
			||||||
 | 
					  opacity: 0.5;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.docs-empty-text {
 | 
				
			||||||
 | 
					  font-size: 16px;
 | 
				
			||||||
 | 
					  text-align: center;
 | 
				
			||||||
 | 
					  line-height: 1.5;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* Markdown内容样式 */
 | 
				
			||||||
 | 
					.docs-markdown {
 | 
				
			||||||
 | 
					  max-width: none;
 | 
				
			||||||
 | 
					  color: #374151;
 | 
				
			||||||
 | 
					  line-height: 1.7;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.docs-markdown h1,
 | 
				
			||||||
 | 
					.docs-markdown h2,
 | 
				
			||||||
 | 
					.docs-markdown h3,
 | 
				
			||||||
 | 
					.docs-markdown h4,
 | 
				
			||||||
 | 
					.docs-markdown h5,
 | 
				
			||||||
 | 
					.docs-markdown h6 {
 | 
				
			||||||
 | 
					  color: #111827;
 | 
				
			||||||
 | 
					  font-weight: 600;
 | 
				
			||||||
 | 
					  margin: 24px 0 16px 0;
 | 
				
			||||||
 | 
					  line-height: 1.25;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.docs-markdown h1 {
 | 
				
			||||||
 | 
					  font-size: 32px;
 | 
				
			||||||
 | 
					  border-bottom: 1px solid #e5e7eb;
 | 
				
			||||||
 | 
					  padding-bottom: 12px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.docs-markdown h2 {
 | 
				
			||||||
 | 
					  font-size: 24px;
 | 
				
			||||||
 | 
					  border-bottom: 1px solid #f3f4f6;
 | 
				
			||||||
 | 
					  padding-bottom: 8px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.docs-markdown h3 {
 | 
				
			||||||
 | 
					  font-size: 20px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.docs-markdown h4 {
 | 
				
			||||||
 | 
					  font-size: 16px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.docs-markdown p {
 | 
				
			||||||
 | 
					  margin: 16px 0;
 | 
				
			||||||
 | 
					  line-height: 1.7;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.docs-markdown ul,
 | 
				
			||||||
 | 
					.docs-markdown ol {
 | 
				
			||||||
 | 
					  margin: 16px 0;
 | 
				
			||||||
 | 
					  padding-left: 24px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.docs-markdown li {
 | 
				
			||||||
 | 
					  margin: 8px 0;
 | 
				
			||||||
 | 
					  line-height: 1.6;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.docs-markdown blockquote {
 | 
				
			||||||
 | 
					  margin: 16px 0;
 | 
				
			||||||
 | 
					  padding: 16px 20px;
 | 
				
			||||||
 | 
					  background: #f9fafb;
 | 
				
			||||||
 | 
					  border-left: 4px solid #d1d5db;
 | 
				
			||||||
 | 
					  color: #6b7280;
 | 
				
			||||||
 | 
					  font-style: italic;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.docs-markdown code {
 | 
				
			||||||
 | 
					  background: #f3f4f6;
 | 
				
			||||||
 | 
					  color: #e11d48;
 | 
				
			||||||
 | 
					  padding: 2px 6px;
 | 
				
			||||||
 | 
					  border-radius: 4px;
 | 
				
			||||||
 | 
					  font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, Courier, monospace;
 | 
				
			||||||
 | 
					  font-size: 0.875em;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.docs-markdown pre {
 | 
				
			||||||
 | 
					  background: #1f2937;
 | 
				
			||||||
 | 
					  color: #f9fafb;
 | 
				
			||||||
 | 
					  padding: 20px;
 | 
				
			||||||
 | 
					  border-radius: 8px;
 | 
				
			||||||
 | 
					  overflow-x: auto;
 | 
				
			||||||
 | 
					  margin: 16px 0;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.docs-markdown pre code {
 | 
				
			||||||
 | 
					  background: none;
 | 
				
			||||||
 | 
					  color: inherit;
 | 
				
			||||||
 | 
					  padding: 0;
 | 
				
			||||||
 | 
					  border-radius: 0;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.docs-markdown a {
 | 
				
			||||||
 | 
					  color: #2563eb;
 | 
				
			||||||
 | 
					  text-decoration: none;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.docs-markdown a:hover {
 | 
				
			||||||
 | 
					  text-decoration: underline;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.docs-markdown img {
 | 
				
			||||||
 | 
					  max-width: 100%;
 | 
				
			||||||
 | 
					  height: auto;
 | 
				
			||||||
 | 
					  border-radius: 8px;
 | 
				
			||||||
 | 
					  margin: 16px 0;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.docs-markdown table {
 | 
				
			||||||
 | 
					  width: 100%;
 | 
				
			||||||
 | 
					  border-collapse: collapse;
 | 
				
			||||||
 | 
					  margin: 16px 0;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.docs-markdown th,
 | 
				
			||||||
 | 
					.docs-markdown td {
 | 
				
			||||||
 | 
					  border: 1px solid #e5e7eb;
 | 
				
			||||||
 | 
					  padding: 12px;
 | 
				
			||||||
 | 
					  text-align: left;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.docs-markdown th {
 | 
				
			||||||
 | 
					  background: #f9fafb;
 | 
				
			||||||
 | 
					  font-weight: 600;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* 响应式设计 */
 | 
				
			||||||
 | 
					@media (max-width: 768px) {
 | 
				
			||||||
 | 
					  .docs-container {
 | 
				
			||||||
 | 
					    flex-direction: column;
 | 
				
			||||||
 | 
					    height: auto;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
 | 
					  .docs-nav {
 | 
				
			||||||
 | 
					    width: 100%;
 | 
				
			||||||
 | 
					    max-height: 300px;
 | 
				
			||||||
 | 
					    display: flex;
 | 
				
			||||||
 | 
					    flex-direction: column;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
 | 
					  .docs-nav-header {
 | 
				
			||||||
 | 
					    position: sticky;
 | 
				
			||||||
 | 
					    top: 0;
 | 
				
			||||||
 | 
					    z-index: 10;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
 | 
					  .docs-nav-list {
 | 
				
			||||||
 | 
					    flex: 1;
 | 
				
			||||||
 | 
					    overflow-y: auto;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
 | 
					  .docs-content-header {
 | 
				
			||||||
 | 
					    padding: 16px 20px;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
 | 
					  .docs-content-body {
 | 
				
			||||||
 | 
					    padding: 20px;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
 | 
					  .docs-content-title {
 | 
				
			||||||
 | 
					    font-size: 20px;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* 滚动条样式 */
 | 
				
			||||||
 | 
					.docs-nav-list::-webkit-scrollbar,
 | 
				
			||||||
 | 
					.docs-content-body::-webkit-scrollbar {
 | 
				
			||||||
 | 
					  width: 6px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.docs-nav-list::-webkit-scrollbar-track,
 | 
				
			||||||
 | 
					.docs-content-body::-webkit-scrollbar-track {
 | 
				
			||||||
 | 
					  background: #f1f5f9;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.docs-nav-list::-webkit-scrollbar-thumb,
 | 
				
			||||||
 | 
					.docs-content-body::-webkit-scrollbar-thumb {
 | 
				
			||||||
 | 
					  background: #cbd5e1;
 | 
				
			||||||
 | 
					  border-radius: 3px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.docs-nav-list::-webkit-scrollbar-thumb:hover,
 | 
				
			||||||
 | 
					.docs-content-body::-webkit-scrollbar-thumb:hover {
 | 
				
			||||||
 | 
					  background: #94a3b8;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* 不同内容类型的样式 */
 | 
				
			||||||
 | 
					.docs-json-content {
 | 
				
			||||||
 | 
					  background: #f8fafc;
 | 
				
			||||||
 | 
					  border: 1px solid #e2e8f0;
 | 
				
			||||||
 | 
					  border-radius: 8px;
 | 
				
			||||||
 | 
					  padding: 20px;
 | 
				
			||||||
 | 
					  font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, Courier, monospace;
 | 
				
			||||||
 | 
					  font-size: 14px;
 | 
				
			||||||
 | 
					  line-height: 1.5;
 | 
				
			||||||
 | 
					  color: #374151;
 | 
				
			||||||
 | 
					  overflow-x: auto;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.docs-code-content {
 | 
				
			||||||
 | 
					  background: #1e293b;
 | 
				
			||||||
 | 
					  color: #f1f5f9;
 | 
				
			||||||
 | 
					  border-radius: 8px;
 | 
				
			||||||
 | 
					  padding: 20px;
 | 
				
			||||||
 | 
					  font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, Courier, monospace;
 | 
				
			||||||
 | 
					  font-size: 14px;
 | 
				
			||||||
 | 
					  line-height: 1.6;
 | 
				
			||||||
 | 
					  overflow-x: auto;
 | 
				
			||||||
 | 
					  margin: 16px 0;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.docs-code-content code {
 | 
				
			||||||
 | 
					  background: none;
 | 
				
			||||||
 | 
					  color: inherit;
 | 
				
			||||||
 | 
					  padding: 0;
 | 
				
			||||||
 | 
					  border-radius: 0;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.docs-image-content {
 | 
				
			||||||
 | 
					  text-align: center;
 | 
				
			||||||
 | 
					  margin: 20px 0;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.docs-image-content img {
 | 
				
			||||||
 | 
					  max-width: 100%;
 | 
				
			||||||
 | 
					  height: auto;
 | 
				
			||||||
 | 
					  border-radius: 8px;
 | 
				
			||||||
 | 
					  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.docs-default-content {
 | 
				
			||||||
 | 
					  background: #f9fafb;
 | 
				
			||||||
 | 
					  border: 1px solid #e5e7eb;
 | 
				
			||||||
 | 
					  border-radius: 8px;
 | 
				
			||||||
 | 
					  padding: 20px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.docs-default-content pre {
 | 
				
			||||||
 | 
					  background: none;
 | 
				
			||||||
 | 
					  color: #374151;
 | 
				
			||||||
 | 
					  padding: 0;
 | 
				
			||||||
 | 
					  margin: 0;
 | 
				
			||||||
 | 
					  font-size: 14px;
 | 
				
			||||||
 | 
					  line-height: 1.5;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* 内容区域优化 */
 | 
				
			||||||
 | 
					.docs-content-body .docs-markdown h1:first-child {
 | 
				
			||||||
 | 
					  margin-top: 0;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.docs-content-body .docs-markdown h1:last-child,
 | 
				
			||||||
 | 
					.docs-content-body .docs-markdown h2:last-child,
 | 
				
			||||||
 | 
					.docs-content-body .docs-markdown h3:last-child,
 | 
				
			||||||
 | 
					.docs-content-body .docs-markdown p:last-child {
 | 
				
			||||||
 | 
					  margin-bottom: 0;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* 导航项类型标识 */
 | 
				
			||||||
 | 
					.docs-nav-item::before {
 | 
				
			||||||
 | 
					  content: '';
 | 
				
			||||||
 | 
					  position: absolute;
 | 
				
			||||||
 | 
					  left: 0;
 | 
				
			||||||
 | 
					  top: 50%;
 | 
				
			||||||
 | 
					  transform: translateY(-50%);
 | 
				
			||||||
 | 
					  width: 3px;
 | 
				
			||||||
 | 
					  height: 0;
 | 
				
			||||||
 | 
					  background: #2563eb;
 | 
				
			||||||
 | 
					  transition: height 0.2s ease;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.docs-nav-item {
 | 
				
			||||||
 | 
					  position: relative;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.docs-nav-link.active ~ ::before,
 | 
				
			||||||
 | 
					.docs-nav-item:has(.docs-nav-link.active)::before {
 | 
				
			||||||
 | 
					  height: 100%;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* 标签优化 */
 | 
				
			||||||
 | 
					.docs-content-tag.type-markdown {
 | 
				
			||||||
 | 
					  background: #dbeafe;
 | 
				
			||||||
 | 
					  color: #1e40af;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.docs-content-tag.type-code {
 | 
				
			||||||
 | 
					  background: #f3e8ff;
 | 
				
			||||||
 | 
					  color: #7c3aed;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.docs-content-tag.type-json {
 | 
				
			||||||
 | 
					  background: #ecfdf5;
 | 
				
			||||||
 | 
					  color: #059669;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.docs-content-tag.type-image {
 | 
				
			||||||
 | 
					  background: #fef3c7;
 | 
				
			||||||
 | 
					  color: #d97706;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* 加载状态 */
 | 
				
			||||||
 | 
					.docs-loading {
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  align-items: center;
 | 
				
			||||||
 | 
					  justify-content: center;
 | 
				
			||||||
 | 
					  padding: 40px;
 | 
				
			||||||
 | 
					  color: #94a3b8;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.docs-loading-spinner {
 | 
				
			||||||
 | 
					  width: 20px;
 | 
				
			||||||
 | 
					  height: 20px;
 | 
				
			||||||
 | 
					  border: 2px solid #e2e8f0;
 | 
				
			||||||
 | 
					  border-top: 2px solid #2563eb;
 | 
				
			||||||
 | 
					  border-radius: 50%;
 | 
				
			||||||
 | 
					  animation: docs-spin 1s linear infinite;
 | 
				
			||||||
 | 
					  margin-right: 12px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@keyframes docs-spin {
 | 
				
			||||||
 | 
					  0% { transform: rotate(0deg); }
 | 
				
			||||||
 | 
					  100% { transform: rotate(360deg); }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* 搜索和过滤功能样式 */
 | 
				
			||||||
 | 
					.docs-nav-search {
 | 
				
			||||||
 | 
					  padding: 16px 20px;
 | 
				
			||||||
 | 
					  border-bottom: 1px solid #e2e8f0;
 | 
				
			||||||
 | 
					  background: #ffffff;
 | 
				
			||||||
 | 
					  position: sticky;
 | 
				
			||||||
 | 
					  top: 0;
 | 
				
			||||||
 | 
					  z-index: 10;
 | 
				
			||||||
 | 
					  flex-shrink: 0;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.docs-nav-search-input {
 | 
				
			||||||
 | 
					  width: 100%;
 | 
				
			||||||
 | 
					  padding: 8px 12px;
 | 
				
			||||||
 | 
					  border: 1px solid #d1d5db;
 | 
				
			||||||
 | 
					  border-radius: 6px;
 | 
				
			||||||
 | 
					  font-size: 14px;
 | 
				
			||||||
 | 
					  transition: border-color 0.2s ease;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.docs-nav-search-input:focus {
 | 
				
			||||||
 | 
					  outline: none;
 | 
				
			||||||
 | 
					  border-color: #2563eb;
 | 
				
			||||||
 | 
					  box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.docs-nav-search-input::placeholder {
 | 
				
			||||||
 | 
					  color: #9ca3af;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* 内容区域的打印样式 */
 | 
				
			||||||
 | 
					@media print {
 | 
				
			||||||
 | 
					  .docs-nav {
 | 
				
			||||||
 | 
					    display: none;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
 | 
					  .docs-content {
 | 
				
			||||||
 | 
					    width: 100%;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
 | 
					  .docs-content-body {
 | 
				
			||||||
 | 
					    padding: 0;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
 | 
					  .docs-markdown {
 | 
				
			||||||
 | 
					    color: #000;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										222
									
								
								web/src/apps/muse/base/docs/example.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										222
									
								
								web/src/apps/muse/base/docs/example.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,222 @@
 | 
				
			|||||||
 | 
					import React from 'react';
 | 
				
			||||||
 | 
					import { DocsComponent } from './index';
 | 
				
			||||||
 | 
					import { mockMarks, generateMarkWithType } from '../mock/collection';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// 创建一些示例文档数据
 | 
				
			||||||
 | 
					const createSampleDocs = () => {
 | 
				
			||||||
 | 
					  const markdownDoc = generateMarkWithType('markdown');
 | 
				
			||||||
 | 
					  markdownDoc.title = '项目介绍';
 | 
				
			||||||
 | 
					  markdownDoc.description = '了解我们的项目背景和目标';
 | 
				
			||||||
 | 
					  markdownDoc.tags = ['介绍', '项目'];
 | 
				
			||||||
 | 
					  markdownDoc.data = {
 | 
				
			||||||
 | 
					    content: `# 欢迎使用文档系统
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					这是一个现代化的文档展示系统,现在使用 **marked** 库进行 Markdown 渲染。
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## 主要功能
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- **响应式设计**: 适配各种屏幕尺寸
 | 
				
			||||||
 | 
					- **左侧导航**: 清晰的文档结构  
 | 
				
			||||||
 | 
					- **Markdown支持**: 使用 marked 库,支持 GitHub Flavored Markdown
 | 
				
			||||||
 | 
					- **多种内容类型**: 支持文本、代码、图片等多种内容
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## 技术特性
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### 样式设计
 | 
				
			||||||
 | 
					使用了现代化的CSS设计,包括:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					1. 清新的配色方案
 | 
				
			||||||
 | 
					2. 优雅的阴影和边框
 | 
				
			||||||
 | 
					3. 流畅的过渡动画
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### 功能特性
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					> **提示**: 这是一个引用块,展示了 marked 的渲染能力
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- [x] 点击导航自动切换内容
 | 
				
			||||||
 | 
					- [x] 加载状态提示
 | 
				
			||||||
 | 
					- [x] 标签和日期显示
 | 
				
			||||||
 | 
					- [ ] 响应式布局
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## 代码示例
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### TypeScript 代码
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					\`\`\`typescript
 | 
				
			||||||
 | 
					import { DocsComponent } from './docs';
 | 
				
			||||||
 | 
					import { mockMarks } from './mock/collection';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const App: React.FC = () => {
 | 
				
			||||||
 | 
					  return <DocsComponent dataSource={mockMarks} />;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					\`\`\`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### 内联代码
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					使用 \`marked\` 库可以更好地处理 Markdown 语法。
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## 表格支持
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					| 功能 | 状态 | 描述 |
 | 
				
			||||||
 | 
					|------|------|------|
 | 
				
			||||||
 | 
					| Markdown | ✅ | 完整支持 |
 | 
				
			||||||
 | 
					| 代码高亮 | ✅ | 语法高亮 |
 | 
				
			||||||
 | 
					| 表格 | ✅ | GFM 表格 |
 | 
				
			||||||
 | 
					| 任务列表 | ✅ | 支持复选框 |
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## 链接和图片
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- 外部链接: [GitHub](https://github.com)
 | 
				
			||||||
 | 
					- 图片支持: 
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					---
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					希望你喜欢这个使用 **marked** 的文档系统!🎉`
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const apiDoc = generateMarkWithType('markdown');
 | 
				
			||||||
 | 
					  apiDoc.title = 'API 文档';
 | 
				
			||||||
 | 
					  apiDoc.description = 'API接口使用说明和示例';
 | 
				
			||||||
 | 
					  apiDoc.tags = ['API', '开发'];
 | 
				
			||||||
 | 
					  apiDoc.data = {
 | 
				
			||||||
 | 
					    content: `# API 文档
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## 🔐 用户认证
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### 登录接口
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					**请求地址**: \`POST /api/auth/login\`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					**请求参数**:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					| 参数 | 类型 | 必填 | 描述 |
 | 
				
			||||||
 | 
					|------|------|------|------|
 | 
				
			||||||
 | 
					| username | string | ✅ | 用户名 |
 | 
				
			||||||
 | 
					| password | string | ✅ | 密码 |
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					**响应示例**:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					\`\`\`json
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					  "code": 200,
 | 
				
			||||||
 | 
					  "message": "success",
 | 
				
			||||||
 | 
					  "data": {
 | 
				
			||||||
 | 
					    "token": "eyJhbGciOiJIUzI1NiIs...",
 | 
				
			||||||
 | 
					    "user": {
 | 
				
			||||||
 | 
					      "id": 1,
 | 
				
			||||||
 | 
					      "username": "admin",
 | 
				
			||||||
 | 
					      "email": "admin@example.com"
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					\`\`\`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## 📊 数据操作
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### 获取列表
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					**请求地址**: \`GET /api/data/list\`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					**查询参数**:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- \`page\`: 页码 (默认: 1)
 | 
				
			||||||
 | 
					- \`size\`: 每页数量 (默认: 10) 
 | 
				
			||||||
 | 
					- \`keyword\`: 搜索关键词
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### 创建数据
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					**请求地址**: \`POST /api/data/create\`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					**请求体**:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					\`\`\`json
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					  "title": "标题",
 | 
				
			||||||
 | 
					  "content": "内容", 
 | 
				
			||||||
 | 
					  "tags": ["tag1", "tag2"]
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					\`\`\`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## ⚠️ 错误码
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					| 错误码 | 描述 | 解决方案 |
 | 
				
			||||||
 | 
					|--------|------|----------|
 | 
				
			||||||
 | 
					| 400 | 请求参数错误 | 检查请求参数格式 |
 | 
				
			||||||
 | 
					| 401 | 未授权 | 重新登录获取 token |
 | 
				
			||||||
 | 
					| 403 | 禁止访问 | 检查用户权限 |
 | 
				
			||||||
 | 
					| 500 | 服务器错误 | 联系技术支持 |
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					> **注意**: 所有 API 请求都需要在 Header 中包含 \`Authorization: Bearer <token>\`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					更多 API 详情请参考 [完整文档](https://docs.example.com) 📖`
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const codeDoc = generateMarkWithType('code');
 | 
				
			||||||
 | 
					  codeDoc.title = '代码示例';
 | 
				
			||||||
 | 
					  codeDoc.description = '常用的代码片段和最佳实践';
 | 
				
			||||||
 | 
					  codeDoc.tags = ['代码', '示例'];
 | 
				
			||||||
 | 
					  codeDoc.data = {
 | 
				
			||||||
 | 
					    code: `// React Hook 示例
 | 
				
			||||||
 | 
					import { useState, useEffect, useCallback } from 'react';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const useDocuments = (initialData = []) => {
 | 
				
			||||||
 | 
					  const [docs, setDocs] = useState(initialData);
 | 
				
			||||||
 | 
					  const [loading, setLoading] = useState(false);
 | 
				
			||||||
 | 
					  const [selectedId, setSelectedId] = useState(null);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // 获取文档列表
 | 
				
			||||||
 | 
					  const fetchDocs = useCallback(async () => {
 | 
				
			||||||
 | 
					    setLoading(true);
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      const response = await fetch('/api/docs');
 | 
				
			||||||
 | 
					      const data = await response.json();
 | 
				
			||||||
 | 
					      setDocs(data);
 | 
				
			||||||
 | 
					    } catch (error) {
 | 
				
			||||||
 | 
					      console.error('Failed to fetch docs:', error);
 | 
				
			||||||
 | 
					    } finally {
 | 
				
			||||||
 | 
					      setLoading(false);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }, []);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // 选择文档
 | 
				
			||||||
 | 
					  const selectDoc = useCallback((id) => {
 | 
				
			||||||
 | 
					    setSelectedId(id);
 | 
				
			||||||
 | 
					  }, []);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  useEffect(() => {
 | 
				
			||||||
 | 
					    fetchDocs();
 | 
				
			||||||
 | 
					  }, [fetchDocs]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return {
 | 
				
			||||||
 | 
					    docs,
 | 
				
			||||||
 | 
					    loading,
 | 
				
			||||||
 | 
					    selectedId,
 | 
				
			||||||
 | 
					    selectDoc,
 | 
				
			||||||
 | 
					    refetch: fetchDocs
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default useDocuments;`,
 | 
				
			||||||
 | 
					    language: 'typescript'
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const configDoc = generateMarkWithType('json');
 | 
				
			||||||
 | 
					  configDoc.title = '配置说明';
 | 
				
			||||||
 | 
					  configDoc.description = '系统配置项说明和默认值';
 | 
				
			||||||
 | 
					  configDoc.tags = ['配置', '设置'];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return [markdownDoc, apiDoc, codeDoc, configDoc];
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// 示例组件
 | 
				
			||||||
 | 
					export const DocsExample: React.FC = () => {
 | 
				
			||||||
 | 
					  const sampleDocs = createSampleDocs();
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <div style={{ height: '100vh' }}>
 | 
				
			||||||
 | 
					      <DocsComponent dataSource={sampleDocs} />
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default DocsExample;
 | 
				
			||||||
							
								
								
									
										222
									
								
								web/src/apps/muse/base/docs/index.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										222
									
								
								web/src/apps/muse/base/docs/index.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,222 @@
 | 
				
			|||||||
 | 
					import React, { useState, useEffect, useMemo } from 'react';
 | 
				
			||||||
 | 
					import { marked } from 'marked';
 | 
				
			||||||
 | 
					import dayjs from 'dayjs';
 | 
				
			||||||
 | 
					import { Mark } from '../mock/collection';
 | 
				
			||||||
 | 
					import './docs.css';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type Props = {
 | 
				
			||||||
 | 
					  dataSource?: Mark[];
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// 配置 marked 选项
 | 
				
			||||||
 | 
					marked.setOptions({
 | 
				
			||||||
 | 
					  breaks: true,
 | 
				
			||||||
 | 
					  gfm: true,
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Markdown渲染组件
 | 
				
			||||||
 | 
					const MarkdownRenderer: React.FC<{ content: string }> = ({ content }) => {
 | 
				
			||||||
 | 
					  const [htmlContent, setHtmlContent] = useState<string>('');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  useEffect(() => {
 | 
				
			||||||
 | 
					    const renderMarkdown = async () => {
 | 
				
			||||||
 | 
					      try {
 | 
				
			||||||
 | 
					        const html = await marked(content);
 | 
				
			||||||
 | 
					        setHtmlContent(html);
 | 
				
			||||||
 | 
					      } catch (error) {
 | 
				
			||||||
 | 
					        console.error('Markdown rendering error:', error);
 | 
				
			||||||
 | 
					        const errorMessage = error instanceof Error ? error.message : '未知错误';
 | 
				
			||||||
 | 
					        setHtmlContent(`<p>渲染错误: ${errorMessage}</p>`);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    renderMarkdown();
 | 
				
			||||||
 | 
					  }, [content]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <div
 | 
				
			||||||
 | 
					      className="docs-markdown"
 | 
				
			||||||
 | 
					      dangerouslySetInnerHTML={{ __html: htmlContent }}
 | 
				
			||||||
 | 
					    />
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// 内容渲染组件
 | 
				
			||||||
 | 
					const ContentRenderer: React.FC<{ mark: Mark }> = ({ mark }) => {
 | 
				
			||||||
 | 
					  const renderContent = () => {
 | 
				
			||||||
 | 
					    if (!mark.data) {
 | 
				
			||||||
 | 
					      return <div className="docs-empty-text">暂无内容</div>;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if (mark.description) {
 | 
				
			||||||
 | 
					      return <MarkdownRenderer content={mark.description} />;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // 根据markType渲染不同类型的内容
 | 
				
			||||||
 | 
					    switch (mark.markType) {
 | 
				
			||||||
 | 
					      case 'markdown':
 | 
				
			||||||
 | 
					        if (mark.data.content) {
 | 
				
			||||||
 | 
					          return <MarkdownRenderer content={mark.data.content} />;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        break;
 | 
				
			||||||
 | 
					      case 'json':
 | 
				
			||||||
 | 
					        return (
 | 
				
			||||||
 | 
					          <pre className="docs-json-content">
 | 
				
			||||||
 | 
					            {JSON.stringify(mark.data, null, 2)}
 | 
				
			||||||
 | 
					          </pre>
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					      case 'code':
 | 
				
			||||||
 | 
					        return (
 | 
				
			||||||
 | 
					          <pre className="docs-code-content">
 | 
				
			||||||
 | 
					            <code>{mark.data.code || JSON.stringify(mark.data, null, 2)}</code>
 | 
				
			||||||
 | 
					          </pre>
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					      case 'image':
 | 
				
			||||||
 | 
					        if (mark.data.src) {
 | 
				
			||||||
 | 
					          return (
 | 
				
			||||||
 | 
					            <div className="docs-image-content">
 | 
				
			||||||
 | 
					              <img src={mark.data.src} alt={mark.data.alt || mark.title} />
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					          );
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        break;
 | 
				
			||||||
 | 
					      default:
 | 
				
			||||||
 | 
					        // 对于其他类型,尝试显示内容字段
 | 
				
			||||||
 | 
					        if (mark.data.content) {
 | 
				
			||||||
 | 
					          return <MarkdownRenderer content={mark.data.content} />;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        // 如果没有内容字段,显示JSON格式
 | 
				
			||||||
 | 
					        return (
 | 
				
			||||||
 | 
					          <div className="docs-default-content">
 | 
				
			||||||
 | 
					            <pre>{JSON.stringify(mark.data, null, 2)}</pre>
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return <div className="docs-empty-text">无法显示此类型的内容</div>;
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return <>{renderContent()}</>;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// 主要的Docs组件
 | 
				
			||||||
 | 
					export const DocsComponent: React.FC<Props> = ({ dataSource = [] }) => {
 | 
				
			||||||
 | 
					  const [selectedMarkId, setSelectedMarkId] = useState<string | null>(null);
 | 
				
			||||||
 | 
					  const [isLoading, setIsLoading] = useState(false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // 过滤和处理数据源
 | 
				
			||||||
 | 
					  const validMarks = useMemo(() => {
 | 
				
			||||||
 | 
					    return dataSource.filter(mark => mark.title || mark.description);
 | 
				
			||||||
 | 
					  }, [dataSource]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // 获取当前选中的Mark
 | 
				
			||||||
 | 
					  const selectedMark = useMemo(() => {
 | 
				
			||||||
 | 
					    return validMarks.find(mark => mark.id === selectedMarkId) || null;
 | 
				
			||||||
 | 
					  }, [validMarks, selectedMarkId]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // 默认选中第一项
 | 
				
			||||||
 | 
					  useEffect(() => {
 | 
				
			||||||
 | 
					    if (validMarks.length > 0 && !selectedMarkId) {
 | 
				
			||||||
 | 
					      setSelectedMarkId(validMarks[0].id);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }, [validMarks, selectedMarkId]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // 处理导航项点击
 | 
				
			||||||
 | 
					  const handleNavItemClick = (markId: string) => {
 | 
				
			||||||
 | 
					    if (markId !== selectedMarkId) {
 | 
				
			||||||
 | 
					      setIsLoading(true);
 | 
				
			||||||
 | 
					      setSelectedMarkId(markId);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // 模拟加载时间
 | 
				
			||||||
 | 
					      setTimeout(() => {
 | 
				
			||||||
 | 
					        setIsLoading(false);
 | 
				
			||||||
 | 
					      }, 200);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // 格式化日期
 | 
				
			||||||
 | 
					  const formatDate = (date: Date) => {
 | 
				
			||||||
 | 
					    return dayjs(date).format('YYYY年MM月DD日');
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <div className="docs-container">
 | 
				
			||||||
 | 
					      {/* 左侧导航 */}
 | 
				
			||||||
 | 
					      <nav className="docs-nav">
 | 
				
			||||||
 | 
					        <div className="docs-nav-header">
 | 
				
			||||||
 | 
					          <h2 className="docs-nav-title">文档导航</h2>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					        <ul className="docs-nav-list">
 | 
				
			||||||
 | 
					          {validMarks.map((mark) => (
 | 
				
			||||||
 | 
					            <li key={mark.id} className="docs-nav-item">
 | 
				
			||||||
 | 
					              <a
 | 
				
			||||||
 | 
					                className={`docs-nav-link ${selectedMarkId === mark.id ? 'active' : ''}`}
 | 
				
			||||||
 | 
					                onClick={() => handleNavItemClick(mark.id)}
 | 
				
			||||||
 | 
					              >
 | 
				
			||||||
 | 
					                <div className="docs-nav-link-title">
 | 
				
			||||||
 | 
					                  {mark.title || '未命名文档'}
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
 | 
					                {mark.description && (
 | 
				
			||||||
 | 
					                  <div className="docs-nav-link-desc">
 | 
				
			||||||
 | 
					                    {mark.description}
 | 
				
			||||||
 | 
					                  </div>
 | 
				
			||||||
 | 
					                )}
 | 
				
			||||||
 | 
					              </a>
 | 
				
			||||||
 | 
					            </li>
 | 
				
			||||||
 | 
					          ))}
 | 
				
			||||||
 | 
					        </ul>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        {validMarks.length === 0 && (
 | 
				
			||||||
 | 
					          <div className="docs-loading">
 | 
				
			||||||
 | 
					            <div className="docs-empty-text">暂无文档</div>
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					        )}
 | 
				
			||||||
 | 
					      </nav>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      {/* 右侧内容 */}
 | 
				
			||||||
 | 
					      <main className="docs-content">
 | 
				
			||||||
 | 
					        {selectedMark ? (
 | 
				
			||||||
 | 
					          <>
 | 
				
			||||||
 | 
					            {/* 内容标题栏 */}
 | 
				
			||||||
 | 
					            <header className="docs-content-header">
 | 
				
			||||||
 | 
					              <h1 className="docs-content-title">
 | 
				
			||||||
 | 
					                {selectedMark.title || '未命名文档'}
 | 
				
			||||||
 | 
					              </h1>
 | 
				
			||||||
 | 
					              <div className="docs-content-meta">
 | 
				
			||||||
 | 
					                {selectedMark.tags && selectedMark.tags.map((tag, index) => (
 | 
				
			||||||
 | 
					                  <span key={index} className="docs-content-tag">
 | 
				
			||||||
 | 
					                    {tag}
 | 
				
			||||||
 | 
					                  </span>
 | 
				
			||||||
 | 
					                ))}
 | 
				
			||||||
 | 
					                <span className="docs-content-date">
 | 
				
			||||||
 | 
					                  {formatDate(selectedMark.updatedAt)}
 | 
				
			||||||
 | 
					                </span>
 | 
				
			||||||
 | 
					              </div>
 | 
				
			||||||
 | 
					            </header>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            {/* 内容主体 */}
 | 
				
			||||||
 | 
					            <div className="docs-content-body">
 | 
				
			||||||
 | 
					              {isLoading ? (
 | 
				
			||||||
 | 
					                <div className="docs-loading">
 | 
				
			||||||
 | 
					                  <div className="docs-loading-spinner"></div>
 | 
				
			||||||
 | 
					                  <span>加载中...</span>
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
 | 
					              ) : (
 | 
				
			||||||
 | 
					                <ContentRenderer mark={selectedMark} />
 | 
				
			||||||
 | 
					              )}
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					          </>
 | 
				
			||||||
 | 
					        ) : (
 | 
				
			||||||
 | 
					          <div className="docs-content-body empty">
 | 
				
			||||||
 | 
					            <div className="docs-empty-icon">📄</div>
 | 
				
			||||||
 | 
					            <div className="docs-empty-text">
 | 
				
			||||||
 | 
					              {validMarks.length === 0 ? '暂无文档可显示' : '请选择一个文档查看'}
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					        )}
 | 
				
			||||||
 | 
					      </main>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// 兼容性导出
 | 
				
			||||||
 | 
					export const App = DocsComponent;
 | 
				
			||||||
@@ -2,22 +2,29 @@ import { useEffect, useState } from "react";
 | 
				
			|||||||
import { Base } from "./table/index";
 | 
					import { Base } from "./table/index";
 | 
				
			||||||
import { markService } from "../modules/mark-service";
 | 
					import { markService } from "../modules/mark-service";
 | 
				
			||||||
import { SigmaGraph } from "./graph/sigma/index";
 | 
					import { SigmaGraph } from "./graph/sigma/index";
 | 
				
			||||||
 | 
					import { DocsComponent } from "./docs";
 | 
				
			||||||
 | 
					import { MarkDetailList } from "./card/MarkDetailList";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const tabs = [
 | 
					const tabs = [
 | 
				
			||||||
  {
 | 
					  {
 | 
				
			||||||
    key: 'table',
 | 
					    key: 'table',
 | 
				
			||||||
    title: '表格'
 | 
					    title: '表格'
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    key: 'card',
 | 
				
			||||||
 | 
					    title: '卡片'
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
  {
 | 
					  {
 | 
				
			||||||
    key: 'graph',
 | 
					    key: 'graph',
 | 
				
			||||||
    title: '关系图'
 | 
					    title: '关系图'
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  {
 | 
					 | 
				
			||||||
    key: 'world',
 | 
					 | 
				
			||||||
    title: '世界'
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
  {
 | 
					  {
 | 
				
			||||||
    key: 'docs',
 | 
					    key: 'docs',
 | 
				
			||||||
    title: '文档'
 | 
					    title: '文档'
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    key: 'world',
 | 
				
			||||||
 | 
					    title: '世界'
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
];
 | 
					];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -41,18 +48,16 @@ export const BaseApp = () => {
 | 
				
			|||||||
            <SigmaGraph dataSource={dataSource} />
 | 
					            <SigmaGraph dataSource={dataSource} />
 | 
				
			||||||
          </div>
 | 
					          </div>
 | 
				
			||||||
        );
 | 
					        );
 | 
				
			||||||
 | 
					      case 'card':
 | 
				
			||||||
 | 
					        return <MarkDetailList data={dataSource} />;
 | 
				
			||||||
 | 
					      case 'docs':
 | 
				
			||||||
 | 
					        return <DocsComponent dataSource={dataSource} />;
 | 
				
			||||||
      case 'world':
 | 
					      case 'world':
 | 
				
			||||||
        return (
 | 
					        return (
 | 
				
			||||||
          <div className="flex items-center justify-center h-96 text-gray-500">
 | 
					          <div className="flex items-center justify-center h-96 text-gray-500">
 | 
				
			||||||
            世界模块暂未实现
 | 
					            世界模块暂未实现
 | 
				
			||||||
          </div>
 | 
					          </div>
 | 
				
			||||||
        );
 | 
					        );
 | 
				
			||||||
      case 'docs':
 | 
					 | 
				
			||||||
        return (
 | 
					 | 
				
			||||||
          <div className="flex items-center justify-center h-96 text-gray-500">
 | 
					 | 
				
			||||||
            文档模块暂未实现
 | 
					 | 
				
			||||||
          </div>
 | 
					 | 
				
			||||||
        );
 | 
					 | 
				
			||||||
      default:
 | 
					      default:
 | 
				
			||||||
        return null;
 | 
					        return null;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@@ -67,9 +72,9 @@ export const BaseApp = () => {
 | 
				
			|||||||
            <button
 | 
					            <button
 | 
				
			||||||
              key={tab.key}
 | 
					              key={tab.key}
 | 
				
			||||||
              onClick={() => setActiveTab(tab.key)}
 | 
					              onClick={() => setActiveTab(tab.key)}
 | 
				
			||||||
              className={`py-2 px-1 border-b-2 font-medium text-sm ${activeTab === tab.key
 | 
					              className={`py-2 px-4 border-b-2 font-medium text-sm transition-all duration-200 ease-in-out transform cursor-pointer ${activeTab === tab.key
 | 
				
			||||||
                ? 'border-blue-500 text-blue-600'
 | 
					                ? 'border-blue-500 text-blue-600 bg-blue-50'
 | 
				
			||||||
                : 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
 | 
					                : 'border-transparent text-gray-500'
 | 
				
			||||||
                }`}
 | 
					                }`}
 | 
				
			||||||
            >
 | 
					            >
 | 
				
			||||||
              {tab.title}
 | 
					              {tab.title}
 | 
				
			||||||
@@ -79,7 +84,7 @@ export const BaseApp = () => {
 | 
				
			|||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      {/* Tab 内容区域 */}
 | 
					      {/* Tab 内容区域 */}
 | 
				
			||||||
      <div className="flex-1">
 | 
					      <div className="flex-1 h-full">
 | 
				
			||||||
        {renderContent()}
 | 
					        {renderContent()}
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,6 +1,6 @@
 | 
				
			|||||||
# 数据管理表格组件
 | 
					# 数据管理表格组件
 | 
				
			||||||
 | 
					
 | 
				
			||||||
这是一个功能完整的React表格组件,支持多选、排序、分页、操作等功能,并集成了Mock数据。
 | 
					这是一个功能完整的React表格组件,支持多选、排序、虚拟滚动、操作等功能,并集成了Mock数据。
 | 
				
			||||||
 | 
					
 | 
				
			||||||
## 功能特性
 | 
					## 功能特性
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -21,11 +21,11 @@
 | 
				
			|||||||
   - 升序/降序/取消排序
 | 
					   - 升序/降序/取消排序
 | 
				
			||||||
   - 排序状态可视化指示
 | 
					   - 排序状态可视化指示
 | 
				
			||||||
 | 
					
 | 
				
			||||||
4. **分页功能**
 | 
					4. **虚拟滚动功能**
 | 
				
			||||||
   - 支持页码切换
 | 
					   - 基于 react-virtualized 实现高性能虚拟滚动
 | 
				
			||||||
   - 可调整每页显示数量(10/20/50/100条)
 | 
					   - 支持大量数据展示而不影响性能
 | 
				
			||||||
   - 显示总数和当前范围
 | 
					   - 可配置行高和表格高度
 | 
				
			||||||
   - 快速跳转页码
 | 
					   - 固定表头,滚动内容区域
 | 
				
			||||||
 | 
					
 | 
				
			||||||
5. **操作功能**
 | 
					5. **操作功能**
 | 
				
			||||||
   - 详情查看(弹窗形式)
 | 
					   - 详情查看(弹窗形式)
 | 
				
			||||||
@@ -148,23 +148,19 @@ base/table/
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
```
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
### 修改分页配置
 | 
					### 修改虚拟滚动配置
 | 
				
			||||||
调整 `paginationConfig` 对象的属性:
 | 
					调整 `virtualScrollConfig` 对象的属性:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
```tsx
 | 
					```tsx
 | 
				
			||||||
const paginationConfig = {
 | 
					const virtualScrollConfig = {
 | 
				
			||||||
  current: currentPage,
 | 
					  rowHeight: 60,    // 行高度
 | 
				
			||||||
  pageSize: pageSize,
 | 
					  height: 600       // 表格容器高度
 | 
				
			||||||
  total: data.length,
 | 
					 | 
				
			||||||
  showSizeChanger: true,
 | 
					 | 
				
			||||||
  showQuickJumper: true,
 | 
					 | 
				
			||||||
  // 其他配置...
 | 
					 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
```
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
## 性能优化
 | 
					## 性能优化
 | 
				
			||||||
 | 
					
 | 
				
			||||||
1. **虚拟化**:对于大量数据,可以考虑实现虚拟滚动
 | 
					1. **虚拟滚动**:已实现基于 react-virtualized 的虚拟滚动,支持大量数据高性能展示
 | 
				
			||||||
2. **懒加载**:支持服务端分页和按需加载
 | 
					2. **懒加载**:支持服务端分页和按需加载
 | 
				
			||||||
3. **缓存**:实现数据缓存机制
 | 
					3. **缓存**:实现数据缓存机制
 | 
				
			||||||
4. **防抖**:搜索和过滤功能添加防抖处理
 | 
					4. **防抖**:搜索和过滤功能添加防抖处理
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,19 +1,24 @@
 | 
				
			|||||||
import React, { useState, useMemo } from 'react';
 | 
					import React, { useState, useMemo } from 'react';
 | 
				
			||||||
 | 
					import { AutoSizer, List } from 'react-virtualized';
 | 
				
			||||||
import { Mark } from '../mock/collection';
 | 
					import { Mark } from '../mock/collection';
 | 
				
			||||||
import { TableProps, SortState } from './types';
 | 
					import { TableProps, SortState } from './types';
 | 
				
			||||||
import './table.css';
 | 
					import './table.css';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// 虚拟滚动常量
 | 
				
			||||||
 | 
					const DEFAULT_ROW_HEIGHT = 48; // 每行高度
 | 
				
			||||||
 | 
					const HEADER_HEIGHT = 48; // 表头高度
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const Table: React.FC<TableProps> = ({
 | 
					export const Table: React.FC<TableProps> = ({
 | 
				
			||||||
  data,
 | 
					  data,
 | 
				
			||||||
  columns,
 | 
					  columns,
 | 
				
			||||||
  loading = false,
 | 
					  loading = false,
 | 
				
			||||||
  rowSelection,
 | 
					  rowSelection,
 | 
				
			||||||
  pagination,
 | 
					  virtualScroll,
 | 
				
			||||||
  actions,
 | 
					  actions,
 | 
				
			||||||
  onSort
 | 
					  onSort
 | 
				
			||||||
}) => {
 | 
					}) => {
 | 
				
			||||||
 | 
					  const rowHeight = virtualScroll?.rowHeight || DEFAULT_ROW_HEIGHT;
 | 
				
			||||||
  const [sortState, setSortState] = useState<SortState>({ field: null, order: null });
 | 
					  const [sortState, setSortState] = useState<SortState>({ field: null, order: null });
 | 
				
			||||||
  const [currentPage, setCurrentPage] = useState(1);
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // 处理排序
 | 
					  // 处理排序
 | 
				
			||||||
  const handleSort = (field: string) => {
 | 
					  const handleSort = (field: string) => {
 | 
				
			||||||
@@ -46,38 +51,16 @@ export const Table: React.FC<TableProps> = ({
 | 
				
			|||||||
    });
 | 
					    });
 | 
				
			||||||
  }, [data, sortState]);
 | 
					  }, [data, sortState]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // 分页数据
 | 
					  // 当前显示的数据(移除分页,直接使用排序后的数据)
 | 
				
			||||||
  const paginatedData = useMemo(() => {
 | 
					  const displayData = sortedData;
 | 
				
			||||||
    if (!pagination) return sortedData;
 | 
					 | 
				
			||||||
    
 | 
					 | 
				
			||||||
    const start = (currentPage - 1) * pagination.pageSize;
 | 
					 | 
				
			||||||
    const end = start + pagination.pageSize;
 | 
					 | 
				
			||||||
    return sortedData.slice(start, end);
 | 
					 | 
				
			||||||
  }, [sortedData, currentPage, pagination]);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  // 处理分页变化
 | 
					 | 
				
			||||||
  const handlePageChange = (page: number) => {
 | 
					 | 
				
			||||||
    setCurrentPage(page);
 | 
					 | 
				
			||||||
    if (pagination && typeof pagination === 'object') {
 | 
					 | 
				
			||||||
      pagination.onChange?.(page, pagination.pageSize);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  // 处理页大小变化
 | 
					 | 
				
			||||||
  const handlePageSizeChange = (pageSize: number) => {
 | 
					 | 
				
			||||||
    setCurrentPage(1);
 | 
					 | 
				
			||||||
    if (pagination && typeof pagination === 'object') {
 | 
					 | 
				
			||||||
      pagination.onChange?.(1, pageSize);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // 全选/取消全选
 | 
					  // 全选/取消全选
 | 
				
			||||||
  const handleSelectAll = (checked: boolean) => {
 | 
					  const handleSelectAll = (checked: boolean) => {
 | 
				
			||||||
    if (!rowSelection) return;
 | 
					    if (!rowSelection) return;
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    const allKeys = paginatedData.map(item => item.id);
 | 
					    const allKeys = displayData.map(item => item.id);
 | 
				
			||||||
    const selectedKeys = checked ? allKeys : [];
 | 
					    const selectedKeys = checked ? allKeys : [];
 | 
				
			||||||
    const selectedRows = checked ? paginatedData : [];
 | 
					    const selectedRows = checked ? displayData : [];
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    rowSelection.onChange?.(selectedKeys, selectedRows);
 | 
					    rowSelection.onChange?.(selectedKeys, selectedRows);
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
@@ -100,6 +83,52 @@ export const Table: React.FC<TableProps> = ({
 | 
				
			|||||||
    return path.split('.').reduce((o, p) => o?.[p], obj);
 | 
					    return path.split('.').reduce((o, p) => o?.[p], obj);
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // 渲染虚拟滚动行
 | 
				
			||||||
 | 
					  const rowRenderer = ({ index, key, style }: any) => {
 | 
				
			||||||
 | 
					    const record = displayData[index];
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    return (
 | 
				
			||||||
 | 
					      <div key={key} style={style} className="table-row virtual-row">
 | 
				
			||||||
 | 
					        {rowSelection && (
 | 
				
			||||||
 | 
					          <div className="selection-column">
 | 
				
			||||||
 | 
					            <input
 | 
				
			||||||
 | 
					              type="checkbox"
 | 
				
			||||||
 | 
					              checked={selectedKeys.includes(record.id)}
 | 
				
			||||||
 | 
					              onChange={(e) => handleRowSelect(record, e.target.checked)}
 | 
				
			||||||
 | 
					              disabled={rowSelection.getCheckboxProps?.(record)?.disabled}
 | 
				
			||||||
 | 
					            />
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					        )}
 | 
				
			||||||
 | 
					        {columns.map(column => (
 | 
				
			||||||
 | 
					          <div key={column.key} className="table-cell" style={{ width: column.width }}>
 | 
				
			||||||
 | 
					            {column.render 
 | 
				
			||||||
 | 
					              ? column.render(getNestedValue(record, column.dataIndex), record, index)
 | 
				
			||||||
 | 
					              : getNestedValue(record, column.dataIndex)
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					        ))}
 | 
				
			||||||
 | 
					        {actions && actions.length > 0 && (
 | 
				
			||||||
 | 
					          <div className="actions-column">
 | 
				
			||||||
 | 
					            <div className="action-buttons">
 | 
				
			||||||
 | 
					              {actions.map(action => (
 | 
				
			||||||
 | 
					                <button
 | 
				
			||||||
 | 
					                  key={action.key}
 | 
				
			||||||
 | 
					                  className={action.className}
 | 
				
			||||||
 | 
					                  onClick={() => action.onClick(record)}
 | 
				
			||||||
 | 
					                  disabled={action.disabled?.(record)}
 | 
				
			||||||
 | 
					                  aria-label={action.tooltip || action.label}
 | 
				
			||||||
 | 
					                >
 | 
				
			||||||
 | 
					                  {action.icon && <span className="btn-icon">{action.icon}</span>}
 | 
				
			||||||
 | 
					                  <span className="tooltip">{action.tooltip || action.label}</span>
 | 
				
			||||||
 | 
					                </button>
 | 
				
			||||||
 | 
					              ))}
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					        )}
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  if (loading) {
 | 
					  if (loading) {
 | 
				
			||||||
    return (
 | 
					    return (
 | 
				
			||||||
      <div className="table-loading">
 | 
					      <div className="table-loading">
 | 
				
			||||||
@@ -110,11 +139,11 @@ export const Table: React.FC<TableProps> = ({
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const selectedKeys = rowSelection?.selectedRowKeys || [];
 | 
					  const selectedKeys = rowSelection?.selectedRowKeys || [];
 | 
				
			||||||
  const isAllSelected = paginatedData.length > 0 && paginatedData.every(item => selectedKeys.includes(item.id));
 | 
					  const isAllSelected = displayData.length > 0 && displayData.every(item => selectedKeys.includes(item.id));
 | 
				
			||||||
  const isIndeterminate = selectedKeys.length > 0 && !isAllSelected;
 | 
					  const isIndeterminate = selectedKeys.length > 0 && !isAllSelected;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <div className="table-container">
 | 
					    <div className="table-container ">
 | 
				
			||||||
      {/* 表格工具栏 */}
 | 
					      {/* 表格工具栏 */}
 | 
				
			||||||
      {rowSelection && selectedKeys.length > 0 && (
 | 
					      {rowSelection && selectedKeys.length > 0 && (
 | 
				
			||||||
        <div className="table-toolbar">
 | 
					        <div className="table-toolbar">
 | 
				
			||||||
@@ -134,173 +163,73 @@ export const Table: React.FC<TableProps> = ({
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
      {/* 表格 */}
 | 
					      {/* 表格 */}
 | 
				
			||||||
      <div className="table-wrapper">
 | 
					      <div className="table-wrapper">
 | 
				
			||||||
        <table className="data-table">
 | 
					        {/* 固定表头 */}
 | 
				
			||||||
          <thead>
 | 
					        <div className="table-header-wrapper" style={{ height: HEADER_HEIGHT }}>
 | 
				
			||||||
            <tr>
 | 
					          <div className="table-header-row">
 | 
				
			||||||
              {rowSelection && (
 | 
					            {rowSelection && (
 | 
				
			||||||
                <th className="selection-column">
 | 
					              <div className="selection-column">
 | 
				
			||||||
                  <input
 | 
					                <input
 | 
				
			||||||
                    type="checkbox"
 | 
					                  type="checkbox"
 | 
				
			||||||
                    checked={isAllSelected}
 | 
					                  checked={isAllSelected}
 | 
				
			||||||
                    ref={input => {
 | 
					                  ref={input => {
 | 
				
			||||||
                      if (input) input.indeterminate = isIndeterminate;
 | 
					                    if (input) input.indeterminate = isIndeterminate;
 | 
				
			||||||
                    }}
 | 
					                  }}
 | 
				
			||||||
                    onChange={(e) => handleSelectAll(e.target.checked)}
 | 
					                  onChange={(e) => handleSelectAll(e.target.checked)}
 | 
				
			||||||
                  />
 | 
					                />
 | 
				
			||||||
                </th>
 | 
					              </div>
 | 
				
			||||||
              )}
 | 
					            )}
 | 
				
			||||||
              {columns.map(column => (
 | 
					            {columns.map(column => (
 | 
				
			||||||
                <th 
 | 
					              <div 
 | 
				
			||||||
                  key={column.key}
 | 
					                key={column.key}
 | 
				
			||||||
                  style={{ width: column.width }}
 | 
					                style={{ width: column.width }}
 | 
				
			||||||
                  className={column.sortable ? 'sortable' : ''}
 | 
					                className={`table-header-cell ${column.sortable ? 'sortable' : ''}`}
 | 
				
			||||||
                >
 | 
					              >
 | 
				
			||||||
                  <div className="table-header">
 | 
					                <div className="table-header">
 | 
				
			||||||
                    <span>{column.title}</span>
 | 
					                  <span>{column.title}</span>
 | 
				
			||||||
                    {column.sortable && (
 | 
					                  {column.sortable && (
 | 
				
			||||||
                      <div 
 | 
					                    <div 
 | 
				
			||||||
                        className="sort-indicators"
 | 
					                      className="sort-indicators"
 | 
				
			||||||
                        onClick={() => handleSort(column.dataIndex)}
 | 
					                      onClick={() => handleSort(column.dataIndex)}
 | 
				
			||||||
                      >
 | 
					                    >
 | 
				
			||||||
                        <span className={`sort-arrow sort-up ${
 | 
					                      <span className={`sort-arrow sort-up ${
 | 
				
			||||||
                          sortState.field === column.dataIndex && sortState.order === 'asc' ? 'active' : ''
 | 
					                        sortState.field === column.dataIndex && sortState.order === 'asc' ? 'active' : ''
 | 
				
			||||||
                        }`}>▲</span>
 | 
					                      }`}>▲</span>
 | 
				
			||||||
                        <span className={`sort-arrow sort-down ${
 | 
					                      <span className={`sort-arrow sort-down ${
 | 
				
			||||||
                          sortState.field === column.dataIndex && sortState.order === 'desc' ? 'active' : ''
 | 
					                        sortState.field === column.dataIndex && sortState.order === 'desc' ? 'active' : ''
 | 
				
			||||||
                        }`}>▼</span>
 | 
					                      }`}>▼</span>
 | 
				
			||||||
                      </div>
 | 
					 | 
				
			||||||
                    )}
 | 
					 | 
				
			||||||
                  </div>
 | 
					 | 
				
			||||||
                </th>
 | 
					 | 
				
			||||||
              ))}
 | 
					 | 
				
			||||||
              {actions && actions.length > 0 && (
 | 
					 | 
				
			||||||
                <th className="actions-column">操作</th>
 | 
					 | 
				
			||||||
              )}
 | 
					 | 
				
			||||||
            </tr>
 | 
					 | 
				
			||||||
          </thead>
 | 
					 | 
				
			||||||
          <tbody>
 | 
					 | 
				
			||||||
            {paginatedData.map((record, index) => (
 | 
					 | 
				
			||||||
              <tr key={record.id} className="table-row">
 | 
					 | 
				
			||||||
                {rowSelection && (
 | 
					 | 
				
			||||||
                  <td className="selection-column">
 | 
					 | 
				
			||||||
                    <input
 | 
					 | 
				
			||||||
                      type="checkbox"
 | 
					 | 
				
			||||||
                      checked={selectedKeys.includes(record.id)}
 | 
					 | 
				
			||||||
                      onChange={(e) => handleRowSelect(record, e.target.checked)}
 | 
					 | 
				
			||||||
                      disabled={rowSelection.getCheckboxProps?.(record)?.disabled}
 | 
					 | 
				
			||||||
                    />
 | 
					 | 
				
			||||||
                  </td>
 | 
					 | 
				
			||||||
                )}
 | 
					 | 
				
			||||||
                {columns.map(column => (
 | 
					 | 
				
			||||||
                  <td key={column.key}>
 | 
					 | 
				
			||||||
                    {column.render 
 | 
					 | 
				
			||||||
                      ? column.render(getNestedValue(record, column.dataIndex), record, index)
 | 
					 | 
				
			||||||
                      : getNestedValue(record, column.dataIndex)
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
                  </td>
 | 
					 | 
				
			||||||
                ))}
 | 
					 | 
				
			||||||
                {actions && actions.length > 0 && (
 | 
					 | 
				
			||||||
                  <td className="actions-column">
 | 
					 | 
				
			||||||
                    <div className="action-buttons">
 | 
					 | 
				
			||||||
                      {actions.map(action => (
 | 
					 | 
				
			||||||
                        <button
 | 
					 | 
				
			||||||
                          key={action.key}
 | 
					 | 
				
			||||||
                          className={`btn btn-${action.type || 'default'}`}
 | 
					 | 
				
			||||||
                          onClick={() => action.onClick(record)}
 | 
					 | 
				
			||||||
                          disabled={action.disabled?.(record)}
 | 
					 | 
				
			||||||
                          title={action.label}
 | 
					 | 
				
			||||||
                        >
 | 
					 | 
				
			||||||
                          {action.icon && <span className="btn-icon">{action.icon}</span>}
 | 
					 | 
				
			||||||
                          {action.label}
 | 
					 | 
				
			||||||
                        </button>
 | 
					 | 
				
			||||||
                      ))}
 | 
					 | 
				
			||||||
                    </div>
 | 
					                    </div>
 | 
				
			||||||
                  </td>
 | 
					                  )}
 | 
				
			||||||
                )}
 | 
					                </div>
 | 
				
			||||||
              </tr>
 | 
					              </div>
 | 
				
			||||||
            ))}
 | 
					            ))}
 | 
				
			||||||
          </tbody>
 | 
					            {actions && actions.length > 0 && (
 | 
				
			||||||
        </table>
 | 
					              <div className="actions-column">操作</div>
 | 
				
			||||||
 | 
					 | 
				
			||||||
        {paginatedData.length === 0 && (
 | 
					 | 
				
			||||||
          <div className="empty-state">
 | 
					 | 
				
			||||||
            <div className="empty-icon">📭</div>
 | 
					 | 
				
			||||||
            <p>暂无数据</p>
 | 
					 | 
				
			||||||
          </div>
 | 
					 | 
				
			||||||
        )}
 | 
					 | 
				
			||||||
      </div>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      {/* 分页 */}
 | 
					 | 
				
			||||||
      {pagination && (
 | 
					 | 
				
			||||||
        <div className="pagination-wrapper">
 | 
					 | 
				
			||||||
          <div className="pagination-info">
 | 
					 | 
				
			||||||
            {pagination.showTotal && pagination.showTotal(
 | 
					 | 
				
			||||||
              pagination.total,
 | 
					 | 
				
			||||||
              [
 | 
					 | 
				
			||||||
                (currentPage - 1) * pagination.pageSize + 1,
 | 
					 | 
				
			||||||
                Math.min(currentPage * pagination.pageSize, pagination.total)
 | 
					 | 
				
			||||||
              ]
 | 
					 | 
				
			||||||
            )}
 | 
					            )}
 | 
				
			||||||
          </div>
 | 
					          </div>
 | 
				
			||||||
          <div className="pagination-controls">
 | 
					        </div>
 | 
				
			||||||
            <button
 | 
					
 | 
				
			||||||
              className="btn btn-default"
 | 
					        {/* 虚拟滚动内容区域 */}
 | 
				
			||||||
              onClick={() => handlePageChange(currentPage - 1)}
 | 
					        <div className="table-body-wrapper">
 | 
				
			||||||
              disabled={currentPage <= 1}
 | 
					          {displayData.length > 0 ? (
 | 
				
			||||||
            >
 | 
					            <AutoSizer>
 | 
				
			||||||
              上一页
 | 
					              {({ height, width }) => (
 | 
				
			||||||
            </button>
 | 
					                <List
 | 
				
			||||||
            
 | 
					                  height={height}
 | 
				
			||||||
            <div className="page-numbers">
 | 
					                  width={width}
 | 
				
			||||||
              {Array.from({ length: Math.ceil(pagination.total / pagination.pageSize) })
 | 
					                  rowCount={displayData.length}
 | 
				
			||||||
                .map((_, i) => i + 1)
 | 
					                  rowHeight={rowHeight}
 | 
				
			||||||
                .filter(page => {
 | 
					                  rowRenderer={rowRenderer}
 | 
				
			||||||
                  const distance = Math.abs(page - currentPage);
 | 
					                />
 | 
				
			||||||
                  return distance === 0 || distance <= 2 || page === 1 || page === Math.ceil(pagination.total / pagination.pageSize);
 | 
					              )}
 | 
				
			||||||
                })
 | 
					            </AutoSizer>
 | 
				
			||||||
                .map((page, index, pages) => {
 | 
					          ) : (
 | 
				
			||||||
                  const prevPage = pages[index - 1];
 | 
					            <div className="empty-state">
 | 
				
			||||||
                  const showEllipsis = prevPage && page - prevPage > 1;
 | 
					              <div className="empty-icon">📭</div>
 | 
				
			||||||
                  
 | 
					              <p>暂无数据</p>
 | 
				
			||||||
                  return (
 | 
					 | 
				
			||||||
                    <React.Fragment key={page}>
 | 
					 | 
				
			||||||
                      {showEllipsis && <span className="page-ellipsis">...</span>}
 | 
					 | 
				
			||||||
                      <button
 | 
					 | 
				
			||||||
                        className={`btn page-btn ${currentPage === page ? 'active' : ''}`}
 | 
					 | 
				
			||||||
                        onClick={() => handlePageChange(page)}
 | 
					 | 
				
			||||||
                      >
 | 
					 | 
				
			||||||
                        {page}
 | 
					 | 
				
			||||||
                      </button>
 | 
					 | 
				
			||||||
                    </React.Fragment>
 | 
					 | 
				
			||||||
                  );
 | 
					 | 
				
			||||||
                })
 | 
					 | 
				
			||||||
              }
 | 
					 | 
				
			||||||
            </div>
 | 
					 | 
				
			||||||
            
 | 
					 | 
				
			||||||
            <button
 | 
					 | 
				
			||||||
              className="btn btn-default"
 | 
					 | 
				
			||||||
              onClick={() => handlePageChange(currentPage + 1)}
 | 
					 | 
				
			||||||
              disabled={currentPage >= Math.ceil(pagination.total / pagination.pageSize)}
 | 
					 | 
				
			||||||
            >
 | 
					 | 
				
			||||||
              下一页
 | 
					 | 
				
			||||||
            </button>
 | 
					 | 
				
			||||||
          </div>
 | 
					 | 
				
			||||||
          
 | 
					 | 
				
			||||||
          {pagination.showSizeChanger && (
 | 
					 | 
				
			||||||
            <div className="page-size-selector">
 | 
					 | 
				
			||||||
              <select
 | 
					 | 
				
			||||||
                value={pagination.pageSize}
 | 
					 | 
				
			||||||
                onChange={(e) => handlePageSizeChange(Number(e.target.value))}
 | 
					 | 
				
			||||||
              >
 | 
					 | 
				
			||||||
                <option value={10}>10条/页</option>
 | 
					 | 
				
			||||||
                <option value={20}>20条/页</option>
 | 
					 | 
				
			||||||
                <option value={50}>50条/页</option>
 | 
					 | 
				
			||||||
                <option value={100}>100条/页</option>
 | 
					 | 
				
			||||||
              </select>
 | 
					 | 
				
			||||||
            </div>
 | 
					            </div>
 | 
				
			||||||
          )}
 | 
					          )}
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
      )}
 | 
					      </div>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
@@ -1,4 +1,5 @@
 | 
				
			|||||||
import React, { useState, useMemo, useEffect } from 'react';
 | 
					import React, { useState, useMemo, useEffect } from 'react';
 | 
				
			||||||
 | 
					import { Eye, Edit, Trash2 } from 'lucide-react';
 | 
				
			||||||
import { Table } from './Table';
 | 
					import { Table } from './Table';
 | 
				
			||||||
import { DetailModal } from './DetailModal';
 | 
					import { DetailModal } from './DetailModal';
 | 
				
			||||||
import { Mark } from '../mock/collection';
 | 
					import { Mark } from '../mock/collection';
 | 
				
			||||||
@@ -10,8 +11,6 @@ type Props = {
 | 
				
			|||||||
export const Base = (props: Props) => {
 | 
					export const Base = (props: Props) => {
 | 
				
			||||||
  const { dataSource = [] } = props;
 | 
					  const { dataSource = [] } = props;
 | 
				
			||||||
  const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
 | 
					  const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
 | 
				
			||||||
  const [currentPage, setCurrentPage] = useState(1);
 | 
					 | 
				
			||||||
  const [pageSize, setPageSize] = useState(10);
 | 
					 | 
				
			||||||
  const [data, setData] = useState<Mark[]>([]);
 | 
					  const [data, setData] = useState<Mark[]>([]);
 | 
				
			||||||
  const [detailModalVisible, setDetailModalVisible] = useState(false);
 | 
					  const [detailModalVisible, setDetailModalVisible] = useState(false);
 | 
				
			||||||
  const [currentRecord, setCurrentRecord] = useState<Mark | null>(null);
 | 
					  const [currentRecord, setCurrentRecord] = useState<Mark | null>(null);
 | 
				
			||||||
@@ -127,16 +126,15 @@ export const Base = (props: Props) => {
 | 
				
			|||||||
    {
 | 
					    {
 | 
				
			||||||
      key: 'view',
 | 
					      key: 'view',
 | 
				
			||||||
      label: '详情',
 | 
					      label: '详情',
 | 
				
			||||||
      type: 'primary',
 | 
					      icon: <Eye className="w-4 h-4 text-blue-600 hover:text-blue-700 cursor-pointer transition-colors" />,
 | 
				
			||||||
      icon: '👁',
 | 
					 | 
				
			||||||
      onClick: (record: Mark) => {
 | 
					      onClick: (record: Mark) => {
 | 
				
			||||||
        handleViewDetail(record);
 | 
					        handleViewDetail(record);
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
      key: 'edit',
 | 
					      key: 'edit',
 | 
				
			||||||
 | 
					      icon: <Edit className="w-4 h-4 text-green-600 hover:text-green-700 cursor-pointer transition-colors" />,
 | 
				
			||||||
      label: '编辑',
 | 
					      label: '编辑',
 | 
				
			||||||
      icon: '✏️',
 | 
					 | 
				
			||||||
      onClick: (record: Mark) => {
 | 
					      onClick: (record: Mark) => {
 | 
				
			||||||
        handleEdit(record);
 | 
					        handleEdit(record);
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
@@ -144,8 +142,7 @@ export const Base = (props: Props) => {
 | 
				
			|||||||
    {
 | 
					    {
 | 
				
			||||||
      key: 'delete',
 | 
					      key: 'delete',
 | 
				
			||||||
      label: '删除',
 | 
					      label: '删除',
 | 
				
			||||||
      type: 'danger',
 | 
					      icon: <Trash2 className="w-4 h-4 text-red-600 hover:text-red-700 cursor-pointer transition-colors" />,
 | 
				
			||||||
      icon: '🗑️',
 | 
					 | 
				
			||||||
      onClick: (record: Mark) => {
 | 
					      onClick: (record: Mark) => {
 | 
				
			||||||
        handleDelete(record);
 | 
					        handleDelete(record);
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
@@ -222,72 +219,75 @@ export const Base = (props: Props) => {
 | 
				
			|||||||
    setData(sortedData);
 | 
					    setData(sortedData);
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // 分页配置
 | 
					  // 虚拟滚动配置
 | 
				
			||||||
  const paginationConfig = {
 | 
					  const virtualScrollConfig = {
 | 
				
			||||||
    current: currentPage,
 | 
					    rowHeight: 60, // 因为有两行内容,需要更高的行高
 | 
				
			||||||
    pageSize: pageSize,
 | 
					    // 移除固定高度,让表格自适应容器高度
 | 
				
			||||||
    total: data.length,
 | 
					 | 
				
			||||||
    showSizeChanger: true,
 | 
					 | 
				
			||||||
    showQuickJumper: true,
 | 
					 | 
				
			||||||
    showTotal: (total: number, range: [number, number]) =>
 | 
					 | 
				
			||||||
      `第 ${range[0]}-${range[1]} 条,共 ${total} 条`,
 | 
					 | 
				
			||||||
    onChange: (page: number, size: number) => {
 | 
					 | 
				
			||||||
      setCurrentPage(page);
 | 
					 | 
				
			||||||
      setPageSize(size);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <div style={{ padding: '24px' }}>
 | 
					    <div className='h-full flex flex-col'>
 | 
				
			||||||
      <div style={{ marginBottom: '16px' }}>
 | 
					      <div className='flex flex-col h-full' style={{ padding: '24px' }}>
 | 
				
			||||||
        <h2>数据管理表格</h2>
 | 
					        {/* 固定头部区域 */}
 | 
				
			||||||
        <p style={{ color: '#666', margin: '8px 0' }}>
 | 
					        <div style={{ marginBottom: '16px', flexShrink: 0 }}>
 | 
				
			||||||
          支持多选、排序、分页等功能的数据表格示例
 | 
					          <h2 style={{ margin: '0 0 8px 0' }}>数据管理表格</h2>
 | 
				
			||||||
        </p>
 | 
					          <p style={{ color: '#666', margin: '0' }}>
 | 
				
			||||||
      </div>
 | 
					            支持多选、排序、虚拟滚动等功能的数据表格示例
 | 
				
			||||||
 | 
					          </p>
 | 
				
			||||||
      {selectedRowKeys.length > 0 && (
 | 
					        </div>
 | 
				
			||||||
        <div style={{
 | 
					        <div>
 | 
				
			||||||
          marginBottom: '16px',
 | 
					          {/* 批量操作条 */}
 | 
				
			||||||
          padding: '12px',
 | 
					          {selectedRowKeys.length > 0 && (
 | 
				
			||||||
          backgroundColor: '#e6f7ff',
 | 
					            <div style={{
 | 
				
			||||||
          borderRadius: '4px',
 | 
					              marginBottom: '16px',
 | 
				
			||||||
          display: 'flex',
 | 
					              padding: '12px',
 | 
				
			||||||
          justifyContent: 'space-between',
 | 
					              backgroundColor: '#e6f7ff',
 | 
				
			||||||
          alignItems: 'center'
 | 
					              borderRadius: '4px',
 | 
				
			||||||
        }}>
 | 
					              display: 'flex',
 | 
				
			||||||
          <span>已选择 {selectedRowKeys.length} 项</span>
 | 
					              justifyContent: 'space-between',
 | 
				
			||||||
          <button
 | 
					              alignItems: 'center',
 | 
				
			||||||
            className="btn btn-danger"
 | 
					              flexShrink: 0
 | 
				
			||||||
            onClick={handleBatchDelete}
 | 
					            }}>
 | 
				
			||||||
          >
 | 
					              <span>已选择 {selectedRowKeys.length} 项</span>
 | 
				
			||||||
            批量删除
 | 
					              <button
 | 
				
			||||||
          </button>
 | 
					                className="btn btn-danger"
 | 
				
			||||||
 | 
					                onClick={handleBatchDelete}
 | 
				
			||||||
 | 
					              >
 | 
				
			||||||
 | 
					                批量删除
 | 
				
			||||||
 | 
					              </button>
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					          )}
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
      )}
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
      <Table
 | 
					        {/* 表格容器 - 占据剩余空间并支持滚动 */}
 | 
				
			||||||
        data={data}
 | 
					        <div style={{
 | 
				
			||||||
        columns={columns}
 | 
					          flex: 1,
 | 
				
			||||||
        actions={actions}
 | 
					          minHeight: 0 // 重要:允许flex容器收缩
 | 
				
			||||||
        rowSelection={{
 | 
					        }}>
 | 
				
			||||||
          selectedRowKeys,
 | 
					          <Table
 | 
				
			||||||
          onChange: (keys, rows) => {
 | 
					            data={data}
 | 
				
			||||||
            setSelectedRowKeys(keys);
 | 
					            columns={columns}
 | 
				
			||||||
          }
 | 
					            actions={actions}
 | 
				
			||||||
        }}
 | 
					            rowSelection={{
 | 
				
			||||||
        pagination={paginationConfig}
 | 
					              selectedRowKeys,
 | 
				
			||||||
        onSort={handleSort}
 | 
					              onChange: (keys, rows) => {
 | 
				
			||||||
      />
 | 
					                setSelectedRowKeys(keys);
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
 | 
					            }}
 | 
				
			||||||
 | 
					            virtualScroll={virtualScrollConfig}
 | 
				
			||||||
 | 
					            onSort={handleSort}
 | 
				
			||||||
 | 
					          />
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      <DetailModal
 | 
					        <DetailModal
 | 
				
			||||||
        visible={detailModalVisible}
 | 
					          visible={detailModalVisible}
 | 
				
			||||||
        data={currentRecord}
 | 
					          data={currentRecord}
 | 
				
			||||||
        onClose={() => {
 | 
					          onClose={() => {
 | 
				
			||||||
          setDetailModalVisible(false);
 | 
					            setDetailModalVisible(false);
 | 
				
			||||||
          setCurrentRecord(null);
 | 
					            setCurrentRecord(null);
 | 
				
			||||||
        }}
 | 
					          }}
 | 
				
			||||||
      />
 | 
					        />
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
@@ -4,6 +4,10 @@
 | 
				
			|||||||
  border-radius: 8px;
 | 
					  border-radius: 8px;
 | 
				
			||||||
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
 | 
					  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
 | 
				
			||||||
  overflow: hidden;
 | 
					  overflow: hidden;
 | 
				
			||||||
 | 
					  height: calc(100% - 24px); /* 距离底部保持24px的间距 */
 | 
				
			||||||
 | 
					  margin-bottom: 24px; /* 底部间距 */
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  flex-direction: column;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/* 工具栏 */
 | 
					/* 工具栏 */
 | 
				
			||||||
@@ -14,6 +18,7 @@
 | 
				
			|||||||
  padding: 16px;
 | 
					  padding: 16px;
 | 
				
			||||||
  background: #f5f5f5;
 | 
					  background: #f5f5f5;
 | 
				
			||||||
  border-bottom: 1px solid #e8e8e8;
 | 
					  border-bottom: 1px solid #e8e8e8;
 | 
				
			||||||
 | 
					  flex-shrink: 0; /* 工具栏不压缩,保持固定高度 */
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.selected-info {
 | 
					.selected-info {
 | 
				
			||||||
@@ -26,37 +31,83 @@
 | 
				
			|||||||
  gap: 8px;
 | 
					  gap: 8px;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/* 表格主体 */
 | 
					/* 表格主体 - 虚拟滚动布局 */
 | 
				
			||||||
.table-wrapper {
 | 
					.table-wrapper {
 | 
				
			||||||
  overflow-x: auto;
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  flex-direction: column;
 | 
				
			||||||
 | 
					  flex: 1; /* 占满剩余空间 */
 | 
				
			||||||
 | 
					  height: 100%; /* 占满父容器高度 */
 | 
				
			||||||
 | 
					  min-height: 200px; /* 最小高度,避免过小 */
 | 
				
			||||||
 | 
					  overflow: hidden; /* 防止溢出 */
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.data-table {
 | 
					/* 固定表头容器 */
 | 
				
			||||||
  width: 100%;
 | 
					.table-header-wrapper {
 | 
				
			||||||
  border-collapse: collapse;
 | 
					  flex-shrink: 0;
 | 
				
			||||||
  font-size: 14px;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.data-table th,
 | 
					 | 
				
			||||||
.data-table td {
 | 
					 | 
				
			||||||
  padding: 12px 16px;
 | 
					 | 
				
			||||||
  text-align: left;
 | 
					 | 
				
			||||||
  border-bottom: 1px solid #e8e8e8;
 | 
					  border-bottom: 1px solid #e8e8e8;
 | 
				
			||||||
 | 
					  background: #fafafa;
 | 
				
			||||||
 | 
					  overflow: hidden;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.data-table th {
 | 
					.table-header-row {
 | 
				
			||||||
  background: #fafafa;
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  align-items: center;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.table-header-cell {
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  align-items: center;
 | 
				
			||||||
 | 
					  padding: 12px 16px;
 | 
				
			||||||
 | 
					  border-right: 1px solid #e8e8e8;
 | 
				
			||||||
  font-weight: 600;
 | 
					  font-weight: 600;
 | 
				
			||||||
  color: #333;
 | 
					  color: #333;
 | 
				
			||||||
  position: sticky;
 | 
					  background: #fafafa;
 | 
				
			||||||
  top: 0;
 | 
					 | 
				
			||||||
  z-index: 10;
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.data-table tbody tr:hover {
 | 
					.table-header-cell:last-child {
 | 
				
			||||||
 | 
					  border-right: none;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.table-header-cell.sortable {
 | 
				
			||||||
 | 
					  cursor: pointer;
 | 
				
			||||||
 | 
					  user-select: none;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* 虚拟滚动内容区域 */
 | 
				
			||||||
 | 
					.table-body-wrapper {
 | 
				
			||||||
 | 
					  flex: 1;
 | 
				
			||||||
 | 
					  overflow: hidden;
 | 
				
			||||||
 | 
					  background: #fff;
 | 
				
			||||||
 | 
					  min-height: 0; /* 重要:允许flex子项收缩 */
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* 虚拟行样式 */
 | 
				
			||||||
 | 
					.virtual-row {
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  align-items: center;
 | 
				
			||||||
 | 
					  border-bottom: 1px solid #e8e8e8;
 | 
				
			||||||
 | 
					  background: #fff;
 | 
				
			||||||
 | 
					  transition: background-color 0.2s;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.virtual-row:hover {
 | 
				
			||||||
  background: #f5f5f5;
 | 
					  background: #f5f5f5;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.table-cell {
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  align-items: center;
 | 
				
			||||||
 | 
					  padding: 12px 16px;
 | 
				
			||||||
 | 
					  border-right: 1px solid #e8e8e8;
 | 
				
			||||||
 | 
					  overflow: hidden;
 | 
				
			||||||
 | 
					  text-overflow: ellipsis;
 | 
				
			||||||
 | 
					  white-space: nowrap;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.table-cell:last-child {
 | 
				
			||||||
 | 
					  border-right: none;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/* 表头 */
 | 
					/* 表头 */
 | 
				
			||||||
.table-header {
 | 
					.table-header {
 | 
				
			||||||
  display: flex;
 | 
					  display: flex;
 | 
				
			||||||
@@ -103,66 +154,220 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
/* 操作列 */
 | 
					/* 操作列 */
 | 
				
			||||||
.actions-column {
 | 
					.actions-column {
 | 
				
			||||||
  width: 200px;
 | 
					  width: auto;
 | 
				
			||||||
 | 
					  min-width: 120px;
 | 
				
			||||||
  text-align: right;
 | 
					  text-align: right;
 | 
				
			||||||
 | 
					  padding: 8px 16px;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.action-buttons {
 | 
					.action-buttons {
 | 
				
			||||||
 | 
					  position: relative;
 | 
				
			||||||
  display: flex;
 | 
					  display: flex;
 | 
				
			||||||
  gap: 8px;
 | 
					  gap: 6px;
 | 
				
			||||||
  justify-content: flex-end;
 | 
					  justify-content: flex-end;
 | 
				
			||||||
 | 
					  align-items: center;
 | 
				
			||||||
 | 
					  flex-wrap: wrap;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/* 按钮样式 */
 | 
					/* 按钮样式 */
 | 
				
			||||||
.btn {
 | 
					.btn {
 | 
				
			||||||
 | 
					  position: relative;
 | 
				
			||||||
  display: inline-flex;
 | 
					  display: inline-flex;
 | 
				
			||||||
  align-items: center;
 | 
					  align-items: center;
 | 
				
			||||||
 | 
					  justify-content: center;
 | 
				
			||||||
  gap: 4px;
 | 
					  gap: 4px;
 | 
				
			||||||
  padding: 6px 12px;
 | 
					  padding: 8px 12px;
 | 
				
			||||||
 | 
					  min-width: 32px;
 | 
				
			||||||
 | 
					  height: 32px;
 | 
				
			||||||
  border: 1px solid #d9d9d9;
 | 
					  border: 1px solid #d9d9d9;
 | 
				
			||||||
  border-radius: 4px;
 | 
					  border-radius: 6px;
 | 
				
			||||||
  background: #fff;
 | 
					  background: #fff;
 | 
				
			||||||
  color: #333;
 | 
					  color: #333;
 | 
				
			||||||
  font-size: 12px;
 | 
					  font-size: 12px;
 | 
				
			||||||
 | 
					  font-weight: 500;
 | 
				
			||||||
  cursor: pointer;
 | 
					  cursor: pointer;
 | 
				
			||||||
  transition: all 0.2s;
 | 
					  transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
 | 
				
			||||||
  text-decoration: none;
 | 
					  text-decoration: none;
 | 
				
			||||||
 | 
					  user-select: none;
 | 
				
			||||||
 | 
					  box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
 | 
				
			||||||
 | 
					  outline: none;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.btn:hover {
 | 
					.btn:hover:not(:disabled) {
 | 
				
			||||||
  border-color: #40a9ff;
 | 
					  border-color: #40a9ff;
 | 
				
			||||||
  color: #40a9ff;
 | 
					  color: #40a9ff;
 | 
				
			||||||
 | 
					  transform: translateY(-1px);
 | 
				
			||||||
 | 
					  box-shadow: 0 4px 8px 0 rgba(64, 169, 255, 0.15);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.btn:focus:not(:disabled) {
 | 
				
			||||||
 | 
					  border-color: #40a9ff;
 | 
				
			||||||
 | 
					  box-shadow: 0 0 0 2px rgba(64, 169, 255, 0.2);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.btn:active:not(:disabled) {
 | 
				
			||||||
 | 
					  transform: translateY(0);
 | 
				
			||||||
 | 
					  box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.1);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.btn:disabled {
 | 
					.btn:disabled {
 | 
				
			||||||
  opacity: 0.5;
 | 
					  opacity: 0.5;
 | 
				
			||||||
  cursor: not-allowed;
 | 
					  cursor: not-allowed;
 | 
				
			||||||
 | 
					  transform: none !important;
 | 
				
			||||||
 | 
					  box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05) !important;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.btn-primary {
 | 
					.btn-primary {
 | 
				
			||||||
  background: #1890ff;
 | 
					  background: linear-gradient(135deg, #1890ff 0%, #096dd9 100%);
 | 
				
			||||||
  border-color: #1890ff;
 | 
					  border-color: #1890ff;
 | 
				
			||||||
  color: #fff;
 | 
					  color: #fff;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.btn-primary:hover:not(:disabled) {
 | 
					.btn-primary:hover:not(:disabled) {
 | 
				
			||||||
  background: #40a9ff;
 | 
					  background: linear-gradient(135deg, #40a9ff 0%, #1890ff 100%);
 | 
				
			||||||
  border-color: #40a9ff;
 | 
					  border-color: #40a9ff;
 | 
				
			||||||
 | 
					  color: #fff;
 | 
				
			||||||
 | 
					  box-shadow: 0 4px 12px 0 rgba(24, 144, 255, 0.3);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.btn-primary:focus:not(:disabled) {
 | 
				
			||||||
 | 
					  box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.3);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.btn-danger {
 | 
					.btn-danger {
 | 
				
			||||||
  background: #ff4d4f;
 | 
					  background: linear-gradient(135deg, #ff4d4f 0%, #f5222d 100%);
 | 
				
			||||||
  border-color: #ff4d4f;
 | 
					  border-color: #ff4d4f;
 | 
				
			||||||
  color: #fff;
 | 
					  color: #fff;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.btn-danger:hover:not(:disabled) {
 | 
					.btn-danger:hover:not(:disabled) {
 | 
				
			||||||
  background: #ff7875;
 | 
					  background: linear-gradient(135deg, #ff7875 0%, #ff4d4f 100%);
 | 
				
			||||||
  border-color: #ff7875;
 | 
					  border-color: #ff7875;
 | 
				
			||||||
 | 
					  color: #fff;
 | 
				
			||||||
 | 
					  box-shadow: 0 4px 12px 0 rgba(255, 77, 79, 0.3);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.btn-danger:focus:not(:disabled) {
 | 
				
			||||||
 | 
					  box-shadow: 0 0 0 2px rgba(255, 77, 79, 0.3);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.btn-success {
 | 
				
			||||||
 | 
					  background: linear-gradient(135deg, #52c41a 0%, #389e0d 100%);
 | 
				
			||||||
 | 
					  border-color: #52c41a;
 | 
				
			||||||
 | 
					  color: #fff;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.btn-success:hover:not(:disabled) {
 | 
				
			||||||
 | 
					  background: linear-gradient(135deg, #73d13d 0%, #52c41a 100%);
 | 
				
			||||||
 | 
					  border-color: #73d13d;
 | 
				
			||||||
 | 
					  color: #fff;
 | 
				
			||||||
 | 
					  box-shadow: 0 4px 12px 0 rgba(82, 196, 26, 0.3);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.btn-success:focus:not(:disabled) {
 | 
				
			||||||
 | 
					  box-shadow: 0 0 0 2px rgba(82, 196, 26, 0.3);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.btn-warning {
 | 
				
			||||||
 | 
					  background: linear-gradient(135deg, #faad14 0%, #d48806 100%);
 | 
				
			||||||
 | 
					  border-color: #faad14;
 | 
				
			||||||
 | 
					  color: #fff;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.btn-warning:hover:not(:disabled) {
 | 
				
			||||||
 | 
					  background: linear-gradient(135deg, #ffc53d 0%, #faad14 100%);
 | 
				
			||||||
 | 
					  border-color: #ffc53d;
 | 
				
			||||||
 | 
					  color: #fff;
 | 
				
			||||||
 | 
					  box-shadow: 0 4px 12px 0 rgba(250, 173, 20, 0.3);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.btn-warning:focus:not(:disabled) {
 | 
				
			||||||
 | 
					  box-shadow: 0 0 0 2px rgba(250, 173, 20, 0.3);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.btn-icon {
 | 
					.btn-icon {
 | 
				
			||||||
 | 
					  font-size: 14px;
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  align-items: center;
 | 
				
			||||||
 | 
					  justify-content: center;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* Tooltip 样式 */
 | 
				
			||||||
 | 
					.tooltip {
 | 
				
			||||||
 | 
					  position: absolute;
 | 
				
			||||||
 | 
					  bottom: 100%;
 | 
				
			||||||
 | 
					  left: 50%;
 | 
				
			||||||
 | 
					  transform: translateX(-50%);
 | 
				
			||||||
 | 
					  margin-bottom: 8px;
 | 
				
			||||||
 | 
					  padding: 6px 10px;
 | 
				
			||||||
 | 
					  background: rgba(0, 0, 0, 0.85);
 | 
				
			||||||
 | 
					  color: #fff;
 | 
				
			||||||
  font-size: 12px;
 | 
					  font-size: 12px;
 | 
				
			||||||
 | 
					  font-weight: 400;
 | 
				
			||||||
 | 
					  line-height: 1.4;
 | 
				
			||||||
 | 
					  border-radius: 4px;
 | 
				
			||||||
 | 
					  white-space: nowrap;
 | 
				
			||||||
 | 
					  opacity: 0;
 | 
				
			||||||
 | 
					  visibility: hidden;
 | 
				
			||||||
 | 
					  transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
 | 
				
			||||||
 | 
					  pointer-events: none;
 | 
				
			||||||
 | 
					  z-index: 1000;
 | 
				
			||||||
 | 
					  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.tooltip::after {
 | 
				
			||||||
 | 
					  content: '';
 | 
				
			||||||
 | 
					  position: absolute;
 | 
				
			||||||
 | 
					  top: 100%;
 | 
				
			||||||
 | 
					  left: 50%;
 | 
				
			||||||
 | 
					  transform: translateX(-50%);
 | 
				
			||||||
 | 
					  border: 4px solid transparent;
 | 
				
			||||||
 | 
					  border-top-color: rgba(0, 0, 0, 0.85);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.btn:hover .tooltip {
 | 
				
			||||||
 | 
					  opacity: 1;
 | 
				
			||||||
 | 
					  visibility: visible;
 | 
				
			||||||
 | 
					  transform: translateX(-50%) translateY(-4px);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* 确保tooltip在不同位置的按钮中都能正确显示 */
 | 
				
			||||||
 | 
					.action-buttons {
 | 
				
			||||||
 | 
					  position: relative;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* 按钮尺寸变体 */
 | 
				
			||||||
 | 
					.btn-small {
 | 
				
			||||||
 | 
					  padding: 4px 8px;
 | 
				
			||||||
 | 
					  min-width: 24px;
 | 
				
			||||||
 | 
					  height: 24px;
 | 
				
			||||||
 | 
					  font-size: 11px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.btn-small .btn-icon {
 | 
				
			||||||
 | 
					  font-size: 12px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.btn-medium {
 | 
				
			||||||
 | 
					  padding: 8px 12px;
 | 
				
			||||||
 | 
					  min-width: 32px;
 | 
				
			||||||
 | 
					  height: 32px;
 | 
				
			||||||
 | 
					  font-size: 12px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.btn-medium .btn-icon {
 | 
				
			||||||
 | 
					  font-size: 14px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.btn-large {
 | 
				
			||||||
 | 
					  padding: 12px 16px;
 | 
				
			||||||
 | 
					  min-width: 40px;
 | 
				
			||||||
 | 
					  height: 40px;
 | 
				
			||||||
 | 
					  font-size: 14px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.btn-large .btn-icon {
 | 
				
			||||||
 | 
					  font-size: 16px;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/* 空状态 */
 | 
					/* 空状态 */
 | 
				
			||||||
@@ -202,73 +407,6 @@
 | 
				
			|||||||
  100% { transform: rotate(360deg); }
 | 
					  100% { transform: rotate(360deg); }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/* 分页 */
 | 
					 | 
				
			||||||
.pagination-wrapper {
 | 
					 | 
				
			||||||
  display: flex;
 | 
					 | 
				
			||||||
  align-items: center;
 | 
					 | 
				
			||||||
  justify-content: space-between;
 | 
					 | 
				
			||||||
  padding: 16px;
 | 
					 | 
				
			||||||
  border-top: 1px solid #e8e8e8;
 | 
					 | 
				
			||||||
  background: #fafafa;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.pagination-info {
 | 
					 | 
				
			||||||
  color: #666;
 | 
					 | 
				
			||||||
  font-size: 14px;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.pagination-controls {
 | 
					 | 
				
			||||||
  display: flex;
 | 
					 | 
				
			||||||
  align-items: center;
 | 
					 | 
				
			||||||
  gap: 8px;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.page-numbers {
 | 
					 | 
				
			||||||
  display: flex;
 | 
					 | 
				
			||||||
  align-items: center;
 | 
					 | 
				
			||||||
  gap: 4px;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.page-btn {
 | 
					 | 
				
			||||||
  min-width: 32px;
 | 
					 | 
				
			||||||
  height: 32px;
 | 
					 | 
				
			||||||
  padding: 0 8px;
 | 
					 | 
				
			||||||
  border: 1px solid #d9d9d9;
 | 
					 | 
				
			||||||
  background: #fff;
 | 
					 | 
				
			||||||
  color: #333;
 | 
					 | 
				
			||||||
  cursor: pointer;
 | 
					 | 
				
			||||||
  transition: all 0.2s;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.page-btn:hover {
 | 
					 | 
				
			||||||
  border-color: #40a9ff;
 | 
					 | 
				
			||||||
  color: #40a9ff;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.page-btn.active {
 | 
					 | 
				
			||||||
  background: #1890ff;
 | 
					 | 
				
			||||||
  border-color: #1890ff;
 | 
					 | 
				
			||||||
  color: #fff;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.page-ellipsis {
 | 
					 | 
				
			||||||
  padding: 0 8px;
 | 
					 | 
				
			||||||
  color: #999;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.page-size-selector {
 | 
					 | 
				
			||||||
  margin-left: 16px;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.page-size-selector select {
 | 
					 | 
				
			||||||
  padding: 4px 8px;
 | 
					 | 
				
			||||||
  border: 1px solid #d9d9d9;
 | 
					 | 
				
			||||||
  border-radius: 4px;
 | 
					 | 
				
			||||||
  background: #fff;
 | 
					 | 
				
			||||||
  color: #333;
 | 
					 | 
				
			||||||
  font-size: 14px;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/* 响应式 */
 | 
					/* 响应式 */
 | 
				
			||||||
@media (max-width: 768px) {
 | 
					@media (max-width: 768px) {
 | 
				
			||||||
  .table-toolbar {
 | 
					  .table-toolbar {
 | 
				
			||||||
@@ -281,32 +419,62 @@
 | 
				
			|||||||
    justify-content: flex-end;
 | 
					    justify-content: flex-end;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
  
 | 
					  
 | 
				
			||||||
  .pagination-wrapper {
 | 
					  .table-wrapper {
 | 
				
			||||||
    flex-direction: column;
 | 
					    min-height: 300px; /* 移动端最小高度,仍然使用flex: 1占满剩余空间 */
 | 
				
			||||||
    gap: 12px;
 | 
					 | 
				
			||||||
    align-items: stretch;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
  
 | 
					 | 
				
			||||||
  .pagination-controls {
 | 
					 | 
				
			||||||
    justify-content: center;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
  
 | 
					 | 
				
			||||||
  .page-size-selector {
 | 
					 | 
				
			||||||
    margin-left: 0;
 | 
					 | 
				
			||||||
    text-align: center;
 | 
					 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
  
 | 
					  
 | 
				
			||||||
  .action-buttons {
 | 
					  .action-buttons {
 | 
				
			||||||
    flex-direction: column;
 | 
					    flex-direction: row;
 | 
				
			||||||
    gap: 4px;
 | 
					    gap: 4px;
 | 
				
			||||||
 | 
					    justify-content: flex-end;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
  
 | 
					  
 | 
				
			||||||
  .actions-column {
 | 
					  .actions-column {
 | 
				
			||||||
    width: 120px;
 | 
					    min-width: 80px;
 | 
				
			||||||
 | 
					    padding: 6px 12px;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
  
 | 
					  
 | 
				
			||||||
  .btn {
 | 
					  .btn {
 | 
				
			||||||
 | 
					    font-size: 11px;
 | 
				
			||||||
 | 
					    padding: 6px 10px;
 | 
				
			||||||
 | 
					    min-width: 28px;
 | 
				
			||||||
 | 
					    height: 28px;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
 | 
					  .btn-icon {
 | 
				
			||||||
 | 
					    font-size: 12px;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
 | 
					  /* 移动端tooltip调整 */
 | 
				
			||||||
 | 
					  .tooltip {
 | 
				
			||||||
    font-size: 11px;
 | 
					    font-size: 11px;
 | 
				
			||||||
    padding: 4px 8px;
 | 
					    padding: 4px 8px;
 | 
				
			||||||
 | 
					    margin-bottom: 6px;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
 | 
					  .tooltip::after {
 | 
				
			||||||
 | 
					    border-width: 3px;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
 | 
					  /* 移动端按钮尺寸调整 */
 | 
				
			||||||
 | 
					  .btn-small {
 | 
				
			||||||
 | 
					    padding: 3px 6px;
 | 
				
			||||||
 | 
					    min-width: 20px;
 | 
				
			||||||
 | 
					    height: 20px;
 | 
				
			||||||
 | 
					    font-size: 10px;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
 | 
					  .btn-medium {
 | 
				
			||||||
 | 
					    padding: 6px 10px;
 | 
				
			||||||
 | 
					    min-width: 28px;
 | 
				
			||||||
 | 
					    height: 28px;
 | 
				
			||||||
 | 
					    font-size: 11px;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
 | 
					  .btn-large {
 | 
				
			||||||
 | 
					    padding: 8px 12px;
 | 
				
			||||||
 | 
					    min-width: 32px;
 | 
				
			||||||
 | 
					    height: 32px;
 | 
				
			||||||
 | 
					    font-size: 12px;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -19,25 +19,22 @@ export interface RowSelection<T = any> {
 | 
				
			|||||||
  getCheckboxProps?: (record: T) => { disabled?: boolean };
 | 
					  getCheckboxProps?: (record: T) => { disabled?: boolean };
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// 分页配置
 | 
					// 虚拟滚动配置
 | 
				
			||||||
export interface PaginationConfig {
 | 
					export interface VirtualScrollConfig {
 | 
				
			||||||
  current: number;
 | 
					  rowHeight?: number;  // 行高度,默认 48px
 | 
				
			||||||
  pageSize: number;
 | 
					  height?: number;     // 表格容器高度,默认 400px
 | 
				
			||||||
  total: number;
 | 
					 | 
				
			||||||
  showSizeChanger?: boolean;
 | 
					 | 
				
			||||||
  showQuickJumper?: boolean;
 | 
					 | 
				
			||||||
  showTotal?: (total: number, range: [number, number]) => string;
 | 
					 | 
				
			||||||
  onChange?: (page: number, pageSize: number) => void;
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// 表格操作按钮类型
 | 
					// 表格操作按钮类型
 | 
				
			||||||
export interface ActionButton {
 | 
					export interface ActionButton {
 | 
				
			||||||
  key: string;
 | 
					  key: string;
 | 
				
			||||||
  label: string;
 | 
					  label: string;
 | 
				
			||||||
  type?: 'primary' | 'default' | 'danger';
 | 
					  className?: string;
 | 
				
			||||||
  icon?: React.ReactNode;
 | 
					  icon?: React.ReactNode;
 | 
				
			||||||
  onClick: (record: Mark) => void;
 | 
					  onClick: (record: Mark) => void;
 | 
				
			||||||
  disabled?: (record: Mark) => boolean;
 | 
					  disabled?: (record: Mark) => boolean;
 | 
				
			||||||
 | 
					  tooltip?: string; // 可选的自定义tooltip文本
 | 
				
			||||||
 | 
					  size?: 'small' | 'medium' | 'large'; // 按钮尺寸
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// 表格属性
 | 
					// 表格属性
 | 
				
			||||||
@@ -46,7 +43,7 @@ export interface TableProps {
 | 
				
			|||||||
  columns: TableColumn<Mark>[];
 | 
					  columns: TableColumn<Mark>[];
 | 
				
			||||||
  loading?: boolean;
 | 
					  loading?: boolean;
 | 
				
			||||||
  rowSelection?: RowSelection<Mark>;
 | 
					  rowSelection?: RowSelection<Mark>;
 | 
				
			||||||
  pagination?: PaginationConfig | false;
 | 
					  virtualScroll?: VirtualScrollConfig;
 | 
				
			||||||
  actions?: ActionButton[];
 | 
					  actions?: ActionButton[];
 | 
				
			||||||
  onSort?: (field: string, order: 'asc' | 'desc' | null) => void;
 | 
					  onSort?: (field: string, order: 'asc' | 'desc' | null) => void;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										201
									
								
								web/src/apps/muse/components/README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										201
									
								
								web/src/apps/muse/components/README.md
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,201 @@
 | 
				
			|||||||
 | 
					# MarkDetail 组件功能说明
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## 概述
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					`MarkDetail` 组件是一个用于显示 SimpleMark 信息的 React 组件,支持两种显示模式:
 | 
				
			||||||
 | 
					1. **简化模式** - 仅显示 `SimpleMarkShow` 类型的基础字段
 | 
				
			||||||
 | 
					2. **完整模式** - 显示 `MarkShow` 类型的所有字段
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## 功能特性
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### ✨ 主要功能
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- 🔄 **切换显示模式**:通过复选框切换显示所有字段或仅基础字段
 | 
				
			||||||
 | 
					- 📊 **数据过滤**:根据用户选择动态过滤显示的数据字段
 | 
				
			||||||
 | 
					- 🎨 **美观界面**:现代化的卡片式布局,响应式设计
 | 
				
			||||||
 | 
					- 🏷️ **标签系统**:彩色标签显示,支持多标签
 | 
				
			||||||
 | 
					- 🖼️ **图片展示**:支持封面图片显示和错误处理
 | 
				
			||||||
 | 
					- 🔗 **链接跳转**:可点击的外部链接
 | 
				
			||||||
 | 
					- 📅 **时间格式化**:友好的时间显示格式
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### 🎯 类型定义
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#### MarkShow (完整类型)
 | 
				
			||||||
 | 
					```typescript
 | 
				
			||||||
 | 
					export type MarkShow = {
 | 
				
			||||||
 | 
					  id: string;
 | 
				
			||||||
 | 
					  title?: string;
 | 
				
			||||||
 | 
					  description?: string;
 | 
				
			||||||
 | 
					  tags?: string[];
 | 
				
			||||||
 | 
					  markType?: string;
 | 
				
			||||||
 | 
					  cover?: string;
 | 
				
			||||||
 | 
					  link?: string;
 | 
				
			||||||
 | 
					  summary?: string;
 | 
				
			||||||
 | 
					  key?: string;
 | 
				
			||||||
 | 
					  data: any;
 | 
				
			||||||
 | 
					  createdAt?: string;
 | 
				
			||||||
 | 
					  updatedAt?: string;
 | 
				
			||||||
 | 
					  markedAt?: Date;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#### SimpleMarkShow (简化类型)
 | 
				
			||||||
 | 
					```typescript
 | 
				
			||||||
 | 
					export type SimpleMarkShow = {
 | 
				
			||||||
 | 
					  id: string;
 | 
				
			||||||
 | 
					  title?: string;
 | 
				
			||||||
 | 
					  description?: string;
 | 
				
			||||||
 | 
					  tags?: string[];
 | 
				
			||||||
 | 
					  cover?: string;
 | 
				
			||||||
 | 
					  link?: string;
 | 
				
			||||||
 | 
					  summary?: string;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### 🔄 显示模式
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#### 简化模式(默认)
 | 
				
			||||||
 | 
					仅显示 `SimpleMarkShow` 的字段:
 | 
				
			||||||
 | 
					- ID
 | 
				
			||||||
 | 
					- 标题
 | 
				
			||||||
 | 
					- 描述
 | 
				
			||||||
 | 
					- 标签
 | 
				
			||||||
 | 
					- 封面
 | 
				
			||||||
 | 
					- 链接
 | 
				
			||||||
 | 
					- 摘要
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#### 完整模式
 | 
				
			||||||
 | 
					显示 `MarkShow` 的所有字段:
 | 
				
			||||||
 | 
					- 简化模式的所有字段
 | 
				
			||||||
 | 
					- 标记类型(带颜色标识)
 | 
				
			||||||
 | 
					- 键值
 | 
				
			||||||
 | 
					- 创建时间
 | 
				
			||||||
 | 
					- 更新时间
 | 
				
			||||||
 | 
					- 标记时间
 | 
				
			||||||
 | 
					- 数据内容(JSON 格式)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## 使用方法
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### 基础使用
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					```tsx
 | 
				
			||||||
 | 
					import { MarkDetail, MarkShow } from './components/MarkDetail';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const data: MarkShow[] = [
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    id: '1',
 | 
				
			||||||
 | 
					    title: '示例标题',
 | 
				
			||||||
 | 
					    description: '示例描述',
 | 
				
			||||||
 | 
					    tags: ['React', 'TypeScript'],
 | 
				
			||||||
 | 
					    // ... 其他字段
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function App() {
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <div className="h-screen">
 | 
				
			||||||
 | 
					      <MarkDetail data={data} />
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### 在主应用中使用
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					组件已集成到主应用的 "Mark详情" 标签页中:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					1. 启动应用后,点击 "Mark详情" 标签
 | 
				
			||||||
 | 
					2. 使用右上角的复选框切换显示模式:
 | 
				
			||||||
 | 
					   - ☐ 仅显示基础字段(SimpleMarkShow)
 | 
				
			||||||
 | 
					   - ☑️ 显示全部字段(MarkShow)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## 界面说明
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### 头部控制区域
 | 
				
			||||||
 | 
					- **标题**:显示组件名称
 | 
				
			||||||
 | 
					- **切换开关**:复选框用于切换显示模式
 | 
				
			||||||
 | 
					- **记录统计**:显示当前数据记录总数
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### 数据显示区域
 | 
				
			||||||
 | 
					- **卡片布局**:每条记录显示为一个独立的卡片
 | 
				
			||||||
 | 
					- **响应式网格**:字段按网格布局,适配不同屏幕尺寸
 | 
				
			||||||
 | 
					- **空状态处理**:无数据时显示友好的空状态提示
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### 字段显示特性
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#### 标签显示
 | 
				
			||||||
 | 
					- 蓝色背景的小标签
 | 
				
			||||||
 | 
					- 支持多标签横向排列
 | 
				
			||||||
 | 
					- 自动换行处理
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#### 类型标识
 | 
				
			||||||
 | 
					不同的 `markType` 显示不同颜色:
 | 
				
			||||||
 | 
					- `markdown` - 绿色
 | 
				
			||||||
 | 
					- `json` - 黄色
 | 
				
			||||||
 | 
					- `html` - 橙色
 | 
				
			||||||
 | 
					- `image` - 紫色
 | 
				
			||||||
 | 
					- `video` - 红色
 | 
				
			||||||
 | 
					- `audio` - 粉色
 | 
				
			||||||
 | 
					- `code` - 灰色
 | 
				
			||||||
 | 
					- `link` - 蓝色
 | 
				
			||||||
 | 
					- `file` - 靛蓝色
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#### 图片处理
 | 
				
			||||||
 | 
					- 封面图片自动缩放为 64x64 像素
 | 
				
			||||||
 | 
					- 图片加载失败时自动隐藏
 | 
				
			||||||
 | 
					- 圆角样式处理
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#### 链接处理
 | 
				
			||||||
 | 
					- 外部链接在新标签页打开
 | 
				
			||||||
 | 
					- 蓝色下划线样式
 | 
				
			||||||
 | 
					- 悬停效果
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#### 时间格式化
 | 
				
			||||||
 | 
					- 使用 `toLocaleString('zh-CN')` 格式化
 | 
				
			||||||
 | 
					- 支持中文本地化显示
 | 
				
			||||||
 | 
					- 未设置时显示 "-"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## 样式系统
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					使用 Tailwind CSS 构建,主要样式类:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### 布局类
 | 
				
			||||||
 | 
					- `h-full flex flex-col` - 全高度弹性布局
 | 
				
			||||||
 | 
					- `grid grid-cols-1 md:grid-cols-2 gap-4` - 响应式网格
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### 卡片样式
 | 
				
			||||||
 | 
					- `border border-gray-200 rounded-lg p-4 bg-white shadow-sm`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### 文本样式
 | 
				
			||||||
 | 
					- `text-sm font-medium text-gray-600` - 标签文本
 | 
				
			||||||
 | 
					- `text-sm text-gray-800` - 内容文本
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### 交互样式
 | 
				
			||||||
 | 
					- `hover:text-blue-800` - 悬停效果
 | 
				
			||||||
 | 
					- `focus:ring-blue-500` - 聚焦状态
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## 扩展性
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					组件设计考虑了扩展性:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					1. **类型安全**:完整的 TypeScript 类型定义
 | 
				
			||||||
 | 
					2. **可配置**:通过 props 传入数据和配置
 | 
				
			||||||
 | 
					3. **可定制**:基于 Tailwind CSS,易于定制样式
 | 
				
			||||||
 | 
					4. **响应式**:适配不同屏幕尺寸
 | 
				
			||||||
 | 
					5. **可维护**:清晰的代码结构和注释
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## 注意事项
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					1. **数据类型**:确保传入的数据符合 `MarkShow` 类型定义
 | 
				
			||||||
 | 
					2. **图片链接**:封面图片需要是有效的 URL
 | 
				
			||||||
 | 
					3. **外部链接**:确保链接格式正确,组件会在新标签页打开
 | 
				
			||||||
 | 
					4. **性能**:大量数据时建议使用虚拟滚动或分页
 | 
				
			||||||
 | 
					5. **容器高度**:建议为组件容器设置明确的高度
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## 更新日志
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- v1.0.0 (2024-10-21)
 | 
				
			||||||
 | 
					  - 初始版本发布
 | 
				
			||||||
 | 
					  - 支持简化/完整两种显示模式
 | 
				
			||||||
 | 
					  - 响应式布局和现代化UI设计
 | 
				
			||||||
 | 
					  - 完整的类型安全支持
 | 
				
			||||||
@@ -1,12 +1,12 @@
 | 
				
			|||||||
import { ToastContainer } from 'react-toastify';
 | 
					import { ToastContainer, toast } from 'react-toastify';
 | 
				
			||||||
import { AuthProvider } from '../login/AuthProvider';
 | 
					import { AuthProvider } from '../login/AuthProvider';
 | 
				
			||||||
import { Panel, PanelGroup, PanelResizeHandle } from 'react-resizable-panels';
 | 
					import { Panel, PanelGroup, PanelResizeHandle } from 'react-resizable-panels';
 | 
				
			||||||
import { useState } from 'react';
 | 
					import { useState, useRef } from 'react';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { VadVoice } from './videos/modules/VadVoice.tsx';
 | 
					import { VadVoice } from './videos/modules/VadVoice.tsx';
 | 
				
			||||||
import { ChatInterface } from './prompts/index.tsx';
 | 
					import { ChatInterface } from './prompts/index.tsx';
 | 
				
			||||||
import { BaseApp } from './base/index.tsx';
 | 
					import { BaseApp } from './base/index.tsx';
 | 
				
			||||||
import { exampleUsage } from './modules/mark-service.ts';
 | 
					import { exampleUsage, markService } from './modules/mark-service.ts';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const LeftPanel = () => {
 | 
					const LeftPanel = () => {
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
@@ -47,6 +47,64 @@ export const MuseApp = () => {
 | 
				
			|||||||
  const [showRightPanel, setShowRightPanel] = useState(true);
 | 
					  const [showRightPanel, setShowRightPanel] = useState(true);
 | 
				
			||||||
  const [showLeftPanel, setShowLeftPanel] = useState(true);
 | 
					  const [showLeftPanel, setShowLeftPanel] = useState(true);
 | 
				
			||||||
  const [showCenterPanel, setShowCenterPanel] = useState(true);
 | 
					  const [showCenterPanel, setShowCenterPanel] = useState(true);
 | 
				
			||||||
 | 
					  const fileInputRef = useRef<HTMLInputElement>(null);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // 导出数据库
 | 
				
			||||||
 | 
					  const handleExportDB = async () => {
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      const filename = `marks_backup_${new Date().toISOString().split('T')[0]}.json`;
 | 
				
			||||||
 | 
					      await markService.exportToFile(filename);
 | 
				
			||||||
 | 
					      toast.success('数据库导出成功!');
 | 
				
			||||||
 | 
					    } catch (error) {
 | 
				
			||||||
 | 
					      console.error('导出失败:', error);
 | 
				
			||||||
 | 
					      toast.error('导出失败: ' + (error as Error).message);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // 触发导入文件选择
 | 
				
			||||||
 | 
					  const handleImportDB = () => {
 | 
				
			||||||
 | 
					    fileInputRef.current?.click();
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // 处理文件选择并导入
 | 
				
			||||||
 | 
					  const handleFileImport = async (event: React.ChangeEvent<HTMLInputElement>) => {
 | 
				
			||||||
 | 
					    const file = event.target.files?.[0];
 | 
				
			||||||
 | 
					    if (!file) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      const result = await markService.importFromFile(file);
 | 
				
			||||||
 | 
					      toast.success(
 | 
				
			||||||
 | 
					        `导入完成!成功: ${result.success}条,失败: ${result.failed}条,总计: ${result.total}条`
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					    } catch (error) {
 | 
				
			||||||
 | 
					      console.error('导入失败:', error);
 | 
				
			||||||
 | 
					      toast.error('导入失败: ' + (error as Error).message);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // 重置文件输入
 | 
				
			||||||
 | 
					    if (fileInputRef.current) {
 | 
				
			||||||
 | 
					      fileInputRef.current.value = '';
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // 删除数据库
 | 
				
			||||||
 | 
					  const handleDeleteDB = async () => {
 | 
				
			||||||
 | 
					    if (!window.confirm('确定要删除所有数据吗?此操作无法撤销!')) {
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      const success = await markService.clearDatabase();
 | 
				
			||||||
 | 
					      if (success) {
 | 
				
			||||||
 | 
					        toast.success('数据库已清空!');
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        toast.error('清空数据库失败!');
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    } catch (error) {
 | 
				
			||||||
 | 
					      console.error('删除失败:', error);
 | 
				
			||||||
 | 
					      toast.error('删除失败: ' + (error as Error).message);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <div className="h-screen flex flex-col">
 | 
					    <div className="h-screen flex flex-col">
 | 
				
			||||||
@@ -85,11 +143,32 @@ export const MuseApp = () => {
 | 
				
			|||||||
          }}>
 | 
					          }}>
 | 
				
			||||||
            初始化DB
 | 
					            初始化DB
 | 
				
			||||||
          </button>
 | 
					          </button>
 | 
				
			||||||
          <button className="px-3 py-1 rounded text-sm bg-red-500 text-white hover:bg-red-600" onClick={() => {
 | 
					          <button 
 | 
				
			||||||
            // 删除DB的逻辑
 | 
					            className="px-3 py-1 rounded text-sm bg-blue-500 text-white hover:bg-blue-600" 
 | 
				
			||||||
          }}>
 | 
					            onClick={handleExportDB}
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
 | 
					            导出DB
 | 
				
			||||||
 | 
					          </button>
 | 
				
			||||||
 | 
					          <button 
 | 
				
			||||||
 | 
					            className="px-3 py-1 rounded text-sm bg-purple-500 text-white hover:bg-purple-600" 
 | 
				
			||||||
 | 
					            onClick={handleImportDB}
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
 | 
					            导入DB
 | 
				
			||||||
 | 
					          </button>
 | 
				
			||||||
 | 
					          <button 
 | 
				
			||||||
 | 
					            className="px-3 py-1 rounded text-sm bg-red-500 text-white hover:bg-red-600" 
 | 
				
			||||||
 | 
					            onClick={handleDeleteDB}
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
            删除DB
 | 
					            删除DB
 | 
				
			||||||
          </button>
 | 
					          </button>
 | 
				
			||||||
 | 
					          {/* 隐藏的文件输入元素 */}
 | 
				
			||||||
 | 
					          <input
 | 
				
			||||||
 | 
					            ref={fileInputRef}
 | 
				
			||||||
 | 
					            type="file"
 | 
				
			||||||
 | 
					            accept=".json"
 | 
				
			||||||
 | 
					            onChange={handleFileImport}
 | 
				
			||||||
 | 
					            style={{ display: 'none' }}
 | 
				
			||||||
 | 
					          />
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -27,12 +27,32 @@ export class MarkDB {
 | 
				
			|||||||
    this.db = createDB(dbName);
 | 
					    this.db = createDB(dbName);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // 检查是否支持 find 方法
 | 
				
			||||||
 | 
					  private supportsFindAPI(): boolean {
 | 
				
			||||||
 | 
					    return typeof this.db.find === 'function';
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // 回退方案:使用 allDocs 过滤数据
 | 
				
			||||||
 | 
					  private async fallbackFind(filterFn: (doc: Mark) => boolean): Promise<Mark[]> {
 | 
				
			||||||
 | 
					    const allDocs = await this.getAll();
 | 
				
			||||||
 | 
					    return allDocs.filter(filterFn).sort((a, b) => 
 | 
				
			||||||
 | 
					      new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // 创建索引以支持查询
 | 
					  // 创建索引以支持查询
 | 
				
			||||||
  async createIndexes() {
 | 
					  async createIndexes() {
 | 
				
			||||||
    if (!this.db) {
 | 
					    if (!this.db) {
 | 
				
			||||||
      throw new Error('数据库未初始化');
 | 
					      throw new Error('数据库未初始化');
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // 检查是否支持 createIndex 方法 (需要 pouchdb-find 插件)
 | 
				
			||||||
 | 
					    if (typeof this.db.createIndex !== 'function') {
 | 
				
			||||||
 | 
					      console.warn('PouchDB Find plugin not available. Skipping index creation.');
 | 
				
			||||||
 | 
					      console.warn('Some query features may not work optimally without indexes.');
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
      // PouchDB 创建索引的正确方式
 | 
					      // PouchDB 创建索引的正确方式
 | 
				
			||||||
      const indexes = [
 | 
					      const indexes = [
 | 
				
			||||||
@@ -64,7 +84,8 @@ export class MarkDB {
 | 
				
			|||||||
      console.log('索引初始化完成');
 | 
					      console.log('索引初始化完成');
 | 
				
			||||||
    } catch (error) {
 | 
					    } catch (error) {
 | 
				
			||||||
      console.error('创建索引失败:', error);
 | 
					      console.error('创建索引失败:', error);
 | 
				
			||||||
      throw error;
 | 
					      // 不再抛出错误,而是警告用户
 | 
				
			||||||
 | 
					      console.warn('索引创建失败,但数据库可以继续使用(性能可能受影响)');
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -123,14 +144,18 @@ export class MarkDB {
 | 
				
			|||||||
  // 按用户 ID 获取 Marks
 | 
					  // 按用户 ID 获取 Marks
 | 
				
			||||||
  async getByUserId(uid: string): Promise<Mark[]> {
 | 
					  async getByUserId(uid: string): Promise<Mark[]> {
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
      const result = await this.db.find({
 | 
					      if (this.supportsFindAPI()) {
 | 
				
			||||||
        selector: {
 | 
					        const result = await this.db.find({
 | 
				
			||||||
          uid: uid
 | 
					          selector: {
 | 
				
			||||||
        },
 | 
					            uid: uid
 | 
				
			||||||
        sort: [{ createdAt: 'desc' }]
 | 
					          },
 | 
				
			||||||
      });
 | 
					          sort: [{ createdAt: 'desc' }]
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
      return result.docs.map(doc => docToMark(doc));
 | 
					        return result.docs.map(doc => docToMark(doc));
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        // 回退方案:使用 allDocs 过滤
 | 
				
			||||||
 | 
					        return await this.fallbackFind((mark: Mark) => mark.uid === uid);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
    } catch (error) {
 | 
					    } catch (error) {
 | 
				
			||||||
      console.error('按用户获取 Marks 失败:', error);
 | 
					      console.error('按用户获取 Marks 失败:', error);
 | 
				
			||||||
      throw error;
 | 
					      throw error;
 | 
				
			||||||
@@ -140,14 +165,18 @@ export class MarkDB {
 | 
				
			|||||||
  // 按类型获取 Marks
 | 
					  // 按类型获取 Marks
 | 
				
			||||||
  async getByType(markType: MarkEnsureType): Promise<Mark[]> {
 | 
					  async getByType(markType: MarkEnsureType): Promise<Mark[]> {
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
      const result = await this.db.find({
 | 
					      if (this.supportsFindAPI()) {
 | 
				
			||||||
        selector: {
 | 
					        const result = await this.db.find({
 | 
				
			||||||
          markType: markType
 | 
					          selector: {
 | 
				
			||||||
        },
 | 
					            markType: markType
 | 
				
			||||||
        sort: [{ createdAt: 'desc' }]
 | 
					          },
 | 
				
			||||||
      });
 | 
					          sort: [{ createdAt: 'desc' }]
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
      return result.docs.map(doc => docToMark(doc));
 | 
					        return result.docs.map(doc => docToMark(doc));
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        // 回退方案:使用 allDocs 过滤
 | 
				
			||||||
 | 
					        return await this.fallbackFind((mark: Mark) => mark.markType === markType);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
    } catch (error) {
 | 
					    } catch (error) {
 | 
				
			||||||
      console.error('按类型获取 Marks 失败:', error);
 | 
					      console.error('按类型获取 Marks 失败:', error);
 | 
				
			||||||
      throw error;
 | 
					      throw error;
 | 
				
			||||||
@@ -157,14 +186,20 @@ export class MarkDB {
 | 
				
			|||||||
  // 按标签搜索 Marks
 | 
					  // 按标签搜索 Marks
 | 
				
			||||||
  async getByTag(tag: string): Promise<Mark[]> {
 | 
					  async getByTag(tag: string): Promise<Mark[]> {
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
      const result = await this.db.find({
 | 
					      if (this.supportsFindAPI()) {
 | 
				
			||||||
        selector: {
 | 
					        const result = await this.db.find({
 | 
				
			||||||
          tags: { $elemMatch: { $eq: tag } }
 | 
					          selector: {
 | 
				
			||||||
        },
 | 
					            tags: { $elemMatch: { $eq: tag } }
 | 
				
			||||||
        sort: [{ createdAt: 'desc' }]
 | 
					          },
 | 
				
			||||||
      });
 | 
					          sort: [{ createdAt: 'desc' }]
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
      return result.docs.map(doc => docToMark(doc));
 | 
					        return result.docs.map(doc => docToMark(doc));
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        // 回退方案:使用 allDocs 过滤
 | 
				
			||||||
 | 
					        return await this.fallbackFind((mark: Mark) => 
 | 
				
			||||||
 | 
					          Boolean(mark.tags && mark.tags.includes(tag))
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
    } catch (error) {
 | 
					    } catch (error) {
 | 
				
			||||||
      console.error('按标签获取 Marks 失败:', error);
 | 
					      console.error('按标签获取 Marks 失败:', error);
 | 
				
			||||||
      throw error;
 | 
					      throw error;
 | 
				
			||||||
@@ -174,18 +209,30 @@ export class MarkDB {
 | 
				
			|||||||
  // 搜索 Marks(按标题或描述)
 | 
					  // 搜索 Marks(按标题或描述)
 | 
				
			||||||
  async search(query: string): Promise<Mark[]> {
 | 
					  async search(query: string): Promise<Mark[]> {
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
      const result = await this.db.find({
 | 
					      if (this.supportsFindAPI()) {
 | 
				
			||||||
        selector: {
 | 
					        const result = await this.db.find({
 | 
				
			||||||
          $or: [
 | 
					          selector: {
 | 
				
			||||||
            { title: { $regex: query, $options: 'i' } },
 | 
					            $or: [
 | 
				
			||||||
            { description: { $regex: query, $options: 'i' } },
 | 
					              { title: { $regex: query, $options: 'i' } },
 | 
				
			||||||
            { summary: { $regex: query, $options: 'i' } }
 | 
					              { description: { $regex: query, $options: 'i' } },
 | 
				
			||||||
          ]
 | 
					              { summary: { $regex: query, $options: 'i' } }
 | 
				
			||||||
        },
 | 
					            ]
 | 
				
			||||||
        sort: [{ createdAt: 'desc' }]
 | 
					          },
 | 
				
			||||||
      });
 | 
					          sort: [{ createdAt: 'desc' }]
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
      return result.docs.map(doc => docToMark(doc));
 | 
					        return result.docs.map(doc => docToMark(doc));
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        // 回退方案:使用 allDocs 过滤,简单的字符串匹配
 | 
				
			||||||
 | 
					        const lowerQuery = query.toLowerCase();
 | 
				
			||||||
 | 
					        return await this.fallbackFind((mark: Mark) => {
 | 
				
			||||||
 | 
					          const title = mark.title?.toLowerCase() || '';
 | 
				
			||||||
 | 
					          const description = mark.description?.toLowerCase() || '';
 | 
				
			||||||
 | 
					          const summary = mark.summary?.toLowerCase() || '';
 | 
				
			||||||
 | 
					          return title.includes(lowerQuery) || 
 | 
				
			||||||
 | 
					                 description.includes(lowerQuery) || 
 | 
				
			||||||
 | 
					                 summary.includes(lowerQuery);
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
    } catch (error) {
 | 
					    } catch (error) {
 | 
				
			||||||
      console.error('搜索 Marks 失败:', error);
 | 
					      console.error('搜索 Marks 失败:', error);
 | 
				
			||||||
      throw error;
 | 
					      throw error;
 | 
				
			||||||
@@ -256,47 +303,91 @@ export class MarkDB {
 | 
				
			|||||||
    totalPages: number;
 | 
					    totalPages: number;
 | 
				
			||||||
  }> {
 | 
					  }> {
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
      // 构建查询选择器
 | 
					      if (this.supportsFindAPI()) {
 | 
				
			||||||
      let selector: any = {};
 | 
					        // 使用 find API
 | 
				
			||||||
 | 
					        let selector: any = {};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      if (filters?.uid) {
 | 
					        if (filters?.uid) {
 | 
				
			||||||
        selector.uid = filters.uid;
 | 
					          selector.uid = filters.uid;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (filters?.markType) {
 | 
				
			||||||
 | 
					          selector.markType = filters.markType;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (filters?.tags && filters.tags.length > 0) {
 | 
				
			||||||
 | 
					          selector.tags = { $elemMatch: { $in: filters.tags } };
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // 获取总数
 | 
				
			||||||
 | 
					        const countResult = await this.db.find({
 | 
				
			||||||
 | 
					          selector,
 | 
				
			||||||
 | 
					          fields: []
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					        const total = countResult.docs.length;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // 计算分页
 | 
				
			||||||
 | 
					        const skip = (page - 1) * limit;
 | 
				
			||||||
 | 
					        const totalPages = Math.ceil(total / limit);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // 获取数据
 | 
				
			||||||
 | 
					        const result = await this.db.find({
 | 
				
			||||||
 | 
					          selector,
 | 
				
			||||||
 | 
					          sort: [{ createdAt: 'desc' }],
 | 
				
			||||||
 | 
					          skip,
 | 
				
			||||||
 | 
					          limit
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return {
 | 
				
			||||||
 | 
					          marks: result.docs.map(doc => docToMark(doc)),
 | 
				
			||||||
 | 
					          total,
 | 
				
			||||||
 | 
					          page,
 | 
				
			||||||
 | 
					          limit,
 | 
				
			||||||
 | 
					          totalPages
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        // 回退方案:获取所有数据后在内存中分页
 | 
				
			||||||
 | 
					        let allMarks = await this.getAll();
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // 应用过滤器
 | 
				
			||||||
 | 
					        if (filters) {
 | 
				
			||||||
 | 
					          allMarks = allMarks.filter(mark => {
 | 
				
			||||||
 | 
					            let matches = true;
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            if (filters.uid && mark.uid !== filters.uid) {
 | 
				
			||||||
 | 
					              matches = false;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            if (filters.markType && mark.markType !== filters.markType) {
 | 
				
			||||||
 | 
					              matches = false;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            if (filters.tags && filters.tags.length > 0) {
 | 
				
			||||||
 | 
					              const hasMatchingTag = filters.tags.some(tag => 
 | 
				
			||||||
 | 
					                mark.tags && mark.tags.includes(tag)
 | 
				
			||||||
 | 
					              );
 | 
				
			||||||
 | 
					              if (!hasMatchingTag) {
 | 
				
			||||||
 | 
					                matches = false;
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            return matches;
 | 
				
			||||||
 | 
					          });
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const total = allMarks.length;
 | 
				
			||||||
 | 
					        const totalPages = Math.ceil(total / limit);
 | 
				
			||||||
 | 
					        const skip = (page - 1) * limit;
 | 
				
			||||||
 | 
					        const marks = allMarks.slice(skip, skip + limit);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return {
 | 
				
			||||||
 | 
					          marks,
 | 
				
			||||||
 | 
					          total,
 | 
				
			||||||
 | 
					          page,
 | 
				
			||||||
 | 
					          limit,
 | 
				
			||||||
 | 
					          totalPages
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					 | 
				
			||||||
      if (filters?.markType) {
 | 
					 | 
				
			||||||
        selector.markType = filters.markType;
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      if (filters?.tags && filters.tags.length > 0) {
 | 
					 | 
				
			||||||
        selector.tags = { $elemMatch: { $in: filters.tags } };
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      // 获取总数
 | 
					 | 
				
			||||||
      const countResult = await this.db.find({
 | 
					 | 
				
			||||||
        selector,
 | 
					 | 
				
			||||||
        fields: []
 | 
					 | 
				
			||||||
      });
 | 
					 | 
				
			||||||
      const total = countResult.docs.length;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      // 计算分页
 | 
					 | 
				
			||||||
      const skip = (page - 1) * limit;
 | 
					 | 
				
			||||||
      const totalPages = Math.ceil(total / limit);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      // 获取数据
 | 
					 | 
				
			||||||
      const result = await this.db.find({
 | 
					 | 
				
			||||||
        selector,
 | 
					 | 
				
			||||||
        sort: [{ createdAt: 'desc' }],
 | 
					 | 
				
			||||||
        skip,
 | 
					 | 
				
			||||||
        limit
 | 
					 | 
				
			||||||
      });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      return {
 | 
					 | 
				
			||||||
        marks: result.docs.map(doc => docToMark(doc)),
 | 
					 | 
				
			||||||
        total,
 | 
					 | 
				
			||||||
        page,
 | 
					 | 
				
			||||||
        limit,
 | 
					 | 
				
			||||||
        totalPages
 | 
					 | 
				
			||||||
      };
 | 
					 | 
				
			||||||
    } catch (error) {
 | 
					    } catch (error) {
 | 
				
			||||||
      console.error('分页获取 Marks 失败:', error);
 | 
					      console.error('分页获取 Marks 失败:', error);
 | 
				
			||||||
      throw error;
 | 
					      throw error;
 | 
				
			||||||
@@ -363,8 +454,9 @@ export class MarkDB {
 | 
				
			|||||||
  // 清理数据库
 | 
					  // 清理数据库
 | 
				
			||||||
  async clear(): Promise<void> {
 | 
					  async clear(): Promise<void> {
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
 | 
					      const dbName = this.db.name;
 | 
				
			||||||
      await this.db.destroy();
 | 
					      await this.db.destroy();
 | 
				
			||||||
      this.db = createDB(this.db.name);
 | 
					      this.db = createDB(dbName);
 | 
				
			||||||
      await this.createIndexes();
 | 
					      await this.createIndexes();
 | 
				
			||||||
    } catch (error) {
 | 
					    } catch (error) {
 | 
				
			||||||
      console.error('清理数据库失败:', error);
 | 
					      console.error('清理数据库失败:', error);
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,6 +1,5 @@
 | 
				
			|||||||
import { markDB, initMarkDB } from './db';
 | 
					import { markDB, initMarkDB } from './db';
 | 
				
			||||||
import { Mark } from './mark';
 | 
					import { Mark } from './mark';
 | 
				
			||||||
import { mockMarks } from '../base/mock/collection';
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Mark 服务类 - 提供业务逻辑层
 | 
					// Mark 服务类 - 提供业务逻辑层
 | 
				
			||||||
export class MarkService {
 | 
					export class MarkService {
 | 
				
			||||||
@@ -79,24 +78,6 @@ export class MarkService {
 | 
				
			|||||||
    return await this.db.getStats();
 | 
					    return await this.db.getStats();
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // 初始化示例数据
 | 
					 | 
				
			||||||
  async initSampleData(): Promise<void> {
 | 
					 | 
				
			||||||
    try {
 | 
					 | 
				
			||||||
      // 检查是否已有数据
 | 
					 | 
				
			||||||
      const existingMarks = await this.getAllMarks();
 | 
					 | 
				
			||||||
      if (existingMarks.length === 0) {
 | 
					 | 
				
			||||||
        // 添加示例数据
 | 
					 | 
				
			||||||
        for (const mockMark of mockMarks) {
 | 
					 | 
				
			||||||
          const { id, createdAt, updatedAt, ...markData } = mockMark;
 | 
					 | 
				
			||||||
          await this.createMark(markData);
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        console.log(`已初始化 ${mockMarks.length} 条示例数据`);
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    } catch (error) {
 | 
					 | 
				
			||||||
      console.error('初始化示例数据失败:', error);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  // 导出数据
 | 
					  // 导出数据
 | 
				
			||||||
  async exportData(): Promise<Mark[]> {
 | 
					  async exportData(): Promise<Mark[]> {
 | 
				
			||||||
    return await this.getAllMarks();
 | 
					    return await this.getAllMarks();
 | 
				
			||||||
@@ -116,6 +97,103 @@ export class MarkService {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
    return importedCount;
 | 
					    return importedCount;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // 导出数据到文件
 | 
				
			||||||
 | 
					  async exportToFile(filename: string = 'marks_backup.json'): Promise<void> {
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      const marks = await this.exportData();
 | 
				
			||||||
 | 
					      const exportData = {
 | 
				
			||||||
 | 
					        version: '1.0',
 | 
				
			||||||
 | 
					        exportTime: new Date().toISOString(),
 | 
				
			||||||
 | 
					        totalCount: marks.length,
 | 
				
			||||||
 | 
					        marks: marks
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      const jsonData = JSON.stringify(exportData, null, 2);
 | 
				
			||||||
 | 
					      const blob = new Blob([jsonData], { type: 'application/json' });
 | 
				
			||||||
 | 
					      const url = URL.createObjectURL(blob);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // 创建下载链接
 | 
				
			||||||
 | 
					      const link = document.createElement('a');
 | 
				
			||||||
 | 
					      link.href = url;
 | 
				
			||||||
 | 
					      link.download = filename;
 | 
				
			||||||
 | 
					      document.body.appendChild(link);
 | 
				
			||||||
 | 
					      link.click();
 | 
				
			||||||
 | 
					      document.body.removeChild(link);
 | 
				
			||||||
 | 
					      URL.revokeObjectURL(url);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      console.log(`成功导出 ${marks.length} 条记录到文件: ${filename}`);
 | 
				
			||||||
 | 
					    } catch (error) {
 | 
				
			||||||
 | 
					      console.error('导出文件失败:', error);
 | 
				
			||||||
 | 
					      throw new Error('导出文件失败: ' + (error as Error).message);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // 从文件导入数据
 | 
				
			||||||
 | 
					  async importFromFile(file: File): Promise<{ success: number; failed: number; total: number }> {
 | 
				
			||||||
 | 
					    return new Promise((resolve, reject) => {
 | 
				
			||||||
 | 
					      const reader = new FileReader();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      reader.onload = async (e) => {
 | 
				
			||||||
 | 
					        try {
 | 
				
			||||||
 | 
					          const content = e.target?.result as string;
 | 
				
			||||||
 | 
					          const importData = JSON.parse(content);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          // 验证数据格式
 | 
				
			||||||
 | 
					          if (!importData.marks || !Array.isArray(importData.marks)) {
 | 
				
			||||||
 | 
					            throw new Error('无效的数据格式:缺少marks数组');
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          let successCount = 0;
 | 
				
			||||||
 | 
					          let failedCount = 0;
 | 
				
			||||||
 | 
					          const totalCount = importData.marks.length;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          // 批量导入数据
 | 
				
			||||||
 | 
					          for (const mark of importData.marks) {
 | 
				
			||||||
 | 
					            try {
 | 
				
			||||||
 | 
					              // 移除ID相关字段,让系统重新生成
 | 
				
			||||||
 | 
					              const { id, createdAt, updatedAt, _id, _rev, ...markData } = mark;
 | 
				
			||||||
 | 
					              await this.createMark(markData);
 | 
				
			||||||
 | 
					              successCount++;
 | 
				
			||||||
 | 
					            } catch (error) {
 | 
				
			||||||
 | 
					              console.warn('导入单条记录失败:', error);
 | 
				
			||||||
 | 
					              failedCount++;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          const result = {
 | 
				
			||||||
 | 
					            success: successCount,
 | 
				
			||||||
 | 
					            failed: failedCount,
 | 
				
			||||||
 | 
					            total: totalCount
 | 
				
			||||||
 | 
					          };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          console.log(`导入完成: 成功${successCount}条,失败${failedCount}条,总计${totalCount}条`);
 | 
				
			||||||
 | 
					          resolve(result);
 | 
				
			||||||
 | 
					        } catch (error) {
 | 
				
			||||||
 | 
					          console.error('解析导入文件失败:', error);
 | 
				
			||||||
 | 
					          reject(new Error('解析导入文件失败: ' + (error as Error).message));
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      reader.onerror = () => {
 | 
				
			||||||
 | 
					        reject(new Error('读取文件失败'));
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      reader.readAsText(file);
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // 清空数据库
 | 
				
			||||||
 | 
					  async clearDatabase(): Promise<boolean> {
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      await this.db.clear();
 | 
				
			||||||
 | 
					      console.log('数据库已清空');
 | 
				
			||||||
 | 
					      return true;
 | 
				
			||||||
 | 
					    } catch (error) {
 | 
				
			||||||
 | 
					      console.error('清空数据库失败:', error);
 | 
				
			||||||
 | 
					      return false;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// 创建默认服务实例
 | 
					// 创建默认服务实例
 | 
				
			||||||
@@ -126,24 +204,6 @@ export const exampleUsage = async () => {
 | 
				
			|||||||
  // 1. 初始化服务
 | 
					  // 1. 初始化服务
 | 
				
			||||||
  await markService.init();
 | 
					  await markService.init();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // 2. 初始化示例数据
 | 
					 | 
				
			||||||
  await markService.initSampleData();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  // 3. 创建新的 Mark
 | 
					 | 
				
			||||||
  const newMark = await markService.createMark({
 | 
					 | 
				
			||||||
    title: '我的第一个标记',
 | 
					 | 
				
			||||||
    description: '这是一个测试标记',
 | 
					 | 
				
			||||||
    markType: 'markdown',
 | 
					 | 
				
			||||||
    tags: ['测试', '示例'],
 | 
					 | 
				
			||||||
    data: {
 | 
					 | 
				
			||||||
      md: '# 测试内容\n\n这是一个测试标记的内容。',
 | 
					 | 
				
			||||||
      type: 'markdown'
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    uid: 'user123',
 | 
					 | 
				
			||||||
    uname: '测试用户'
 | 
					 | 
				
			||||||
  });
 | 
					 | 
				
			||||||
  console.log('创建的标记:', newMark);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  // 4. 获取所有标记
 | 
					  // 4. 获取所有标记
 | 
				
			||||||
  const allMarks = await markService.getAllMarks();
 | 
					  const allMarks = await markService.getAllMarks();
 | 
				
			||||||
  console.log('所有标记数量:', allMarks.length);
 | 
					  console.log('所有标记数量:', allMarks.length);
 | 
				
			||||||
@@ -169,18 +229,4 @@ export const exampleUsage = async () => {
 | 
				
			|||||||
  const stats = await markService.getStats();
 | 
					  const stats = await markService.getStats();
 | 
				
			||||||
  console.log('统计信息:', stats);
 | 
					  console.log('统计信息:', stats);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // 9. 更新标记
 | 
					 | 
				
			||||||
  if (newMark.id) {
 | 
					 | 
				
			||||||
    const updatedMark = await markService.updateMark(newMark.id, {
 | 
					 | 
				
			||||||
      title: '更新后的标题',
 | 
					 | 
				
			||||||
      description: '更新后的描述'
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
    console.log('更新后的标记:', updatedMark);
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  // 10. 删除标记
 | 
					 | 
				
			||||||
  if (newMark.id) {
 | 
					 | 
				
			||||||
    const deleted = await markService.deleteMark(newMark.id);
 | 
					 | 
				
			||||||
    console.log('删除结果:', deleted);
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
							
								
								
									
										12
									
								
								web/src/apps/muse/modules/speak.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								web/src/apps/muse/modules/speak.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,12 @@
 | 
				
			|||||||
 | 
					type Speak = {
 | 
				
			||||||
 | 
					  id: string;
 | 
				
			||||||
 | 
					  no: number; // 序号, 当天的序号
 | 
				
			||||||
 | 
					  file?: string; // base64 编码的音频文件
 | 
				
			||||||
 | 
					  text?: string; // 文字内容,识别的内容
 | 
				
			||||||
 | 
					  timestamp: Date; // 生成时间戳
 | 
				
			||||||
 | 
					  day: number; // 365天中的第几天
 | 
				
			||||||
 | 
					  duration: number; // 音频时长,单位秒
 | 
				
			||||||
 | 
					  speaker?: string; // 说话人
 | 
				
			||||||
 | 
					  type?: 'merge' | 'normal'; // 语音类型,默认录制或者合并的
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		Reference in New Issue
	
	Block a user