add card
This commit is contained in:
		
							
								
								
									
										93
									
								
								pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										93
									
								
								pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							@@ -108,6 +108,9 @@ importers:
 | 
			
		||||
      lucide-react:
 | 
			
		||||
        specifier: ^0.545.0
 | 
			
		||||
        version: 0.545.0(react@19.2.0)
 | 
			
		||||
      marked:
 | 
			
		||||
        specifier: ^16.4.1
 | 
			
		||||
        version: 16.4.1
 | 
			
		||||
      nanoid:
 | 
			
		||||
        specifier: ^5.1.6
 | 
			
		||||
        version: 5.1.6
 | 
			
		||||
@@ -132,6 +135,9 @@ importers:
 | 
			
		||||
      react-toastify:
 | 
			
		||||
        specifier: ^11.0.5
 | 
			
		||||
        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:
 | 
			
		||||
        specifier: ^3.0.2
 | 
			
		||||
        version: 3.0.2(graphology-types@0.24.8)
 | 
			
		||||
@@ -163,6 +169,9 @@ importers:
 | 
			
		||||
      '@types/react-dom':
 | 
			
		||||
        specifier: ^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':
 | 
			
		||||
        specifier: ^0.180.0
 | 
			
		||||
        version: 0.180.0
 | 
			
		||||
@@ -1105,11 +1114,17 @@ packages:
 | 
			
		||||
  '@types/pouchdb@6.4.2':
 | 
			
		||||
    resolution: {integrity: sha512-YsI47rASdtzR+3V3JE2UKY58snhm0AglHBpyckQBkRYoCbTvGagXHtV0x5n8nzN04jQmvTG+Sm85cIzKT3KXBA==}
 | 
			
		||||
 | 
			
		||||
  '@types/prop-types@15.7.15':
 | 
			
		||||
    resolution: {integrity: sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==}
 | 
			
		||||
 | 
			
		||||
  '@types/react-dom@19.2.2':
 | 
			
		||||
    resolution: {integrity: sha512-9KQPoO6mZCi7jcIStSnlOWn2nEF3mNmyr3rIAsGnAbQKYbRLyqmeSc39EVgtxXVia+LMT8j3knZLAZAh+xLmrw==}
 | 
			
		||||
    peerDependencies:
 | 
			
		||||
      '@types/react': ^19.2.0
 | 
			
		||||
 | 
			
		||||
  '@types/react-virtualized@9.22.3':
 | 
			
		||||
    resolution: {integrity: sha512-UKRWeBIrECaKhE4O//TSFhlgwntMwyiEIOA7WZoVkr52Jahv0dH6YIOorqc358N2V3oKFclsq5XxPmx2PiYB5A==}
 | 
			
		||||
 | 
			
		||||
  '@types/react@19.2.2':
 | 
			
		||||
    resolution: {integrity: sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==}
 | 
			
		||||
 | 
			
		||||
@@ -1319,6 +1334,10 @@ packages:
 | 
			
		||||
    resolution: {integrity: sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==}
 | 
			
		||||
    engines: {node: '>=0.8'}
 | 
			
		||||
 | 
			
		||||
  clsx@1.2.1:
 | 
			
		||||
    resolution: {integrity: sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==}
 | 
			
		||||
    engines: {node: '>=6'}
 | 
			
		||||
 | 
			
		||||
  clsx@2.1.1:
 | 
			
		||||
    resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
 | 
			
		||||
    engines: {node: '>=6'}
 | 
			
		||||
@@ -1428,6 +1447,9 @@ packages:
 | 
			
		||||
  dlv@1.1.3:
 | 
			
		||||
    resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==}
 | 
			
		||||
 | 
			
		||||
  dom-helpers@5.2.1:
 | 
			
		||||
    resolution: {integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==}
 | 
			
		||||
 | 
			
		||||
  dotenv@16.6.1:
 | 
			
		||||
    resolution: {integrity: sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==}
 | 
			
		||||
    engines: {node: '>=12'}
 | 
			
		||||
@@ -1966,6 +1988,10 @@ packages:
 | 
			
		||||
  longest-streak@3.1.0:
 | 
			
		||||
    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:
 | 
			
		||||
    resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==}
 | 
			
		||||
 | 
			
		||||
@@ -1998,6 +2024,11 @@ packages:
 | 
			
		||||
  markdown-table@3.0.4:
 | 
			
		||||
    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:
 | 
			
		||||
    resolution: {integrity: sha512-scTllyX6pnYNZH/AIp/0ePz6s4cZtARxImwoPJ7kS42n+MnVsI4XbnG6d4ibehRIldYMWM2LD7ImQblVhUejVQ==}
 | 
			
		||||
 | 
			
		||||
@@ -2258,6 +2289,10 @@ packages:
 | 
			
		||||
    resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==}
 | 
			
		||||
    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:
 | 
			
		||||
    resolution: {integrity: sha512-QZj2DfGplQAr2oj9KzceK9Hwz6Whxazmn85yYeVuS3u9XTMOGMRx0kO95MQ+vLsj/S/NwBDMMLU5hpxvI6Tklw==}
 | 
			
		||||
 | 
			
		||||
@@ -2402,6 +2437,9 @@ packages:
 | 
			
		||||
    resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==}
 | 
			
		||||
    engines: {node: '>= 6'}
 | 
			
		||||
 | 
			
		||||
  prop-types@15.8.1:
 | 
			
		||||
    resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==}
 | 
			
		||||
 | 
			
		||||
  property-information@6.5.0:
 | 
			
		||||
    resolution: {integrity: sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig==}
 | 
			
		||||
 | 
			
		||||
@@ -2456,6 +2494,12 @@ packages:
 | 
			
		||||
      typescript:
 | 
			
		||||
        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:
 | 
			
		||||
    resolution: {integrity: sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==}
 | 
			
		||||
    engines: {node: '>=0.10.0'}
 | 
			
		||||
@@ -2472,6 +2516,12 @@ packages:
 | 
			
		||||
      react: ^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:
 | 
			
		||||
    resolution: {integrity: sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==}
 | 
			
		||||
    engines: {node: '>=0.10.0'}
 | 
			
		||||
@@ -4040,10 +4090,17 @@ snapshots:
 | 
			
		||||
      '@types/pouchdb-node': 6.1.7
 | 
			
		||||
      '@types/pouchdb-replication': 6.4.7
 | 
			
		||||
 | 
			
		||||
  '@types/prop-types@15.7.15': {}
 | 
			
		||||
 | 
			
		||||
  '@types/react-dom@19.2.2(@types/react@19.2.2)':
 | 
			
		||||
    dependencies:
 | 
			
		||||
      '@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':
 | 
			
		||||
    dependencies:
 | 
			
		||||
      csstype: 3.1.3
 | 
			
		||||
@@ -4339,6 +4396,8 @@ snapshots:
 | 
			
		||||
 | 
			
		||||
  clone@2.1.2: {}
 | 
			
		||||
 | 
			
		||||
  clsx@1.2.1: {}
 | 
			
		||||
 | 
			
		||||
  clsx@2.1.1: {}
 | 
			
		||||
 | 
			
		||||
  collapse-white-space@2.1.0: {}
 | 
			
		||||
@@ -4419,6 +4478,11 @@ snapshots:
 | 
			
		||||
 | 
			
		||||
  dlv@1.1.3: {}
 | 
			
		||||
 | 
			
		||||
  dom-helpers@5.2.1:
 | 
			
		||||
    dependencies:
 | 
			
		||||
      '@babel/runtime': 7.28.4
 | 
			
		||||
      csstype: 3.1.3
 | 
			
		||||
 | 
			
		||||
  dotenv@16.6.1: {}
 | 
			
		||||
 | 
			
		||||
  dotenv@17.2.3: {}
 | 
			
		||||
@@ -5015,6 +5079,10 @@ snapshots:
 | 
			
		||||
 | 
			
		||||
  longest-streak@3.1.0: {}
 | 
			
		||||
 | 
			
		||||
  loose-envify@1.4.0:
 | 
			
		||||
    dependencies:
 | 
			
		||||
      js-tokens: 4.0.0
 | 
			
		||||
 | 
			
		||||
  lru-cache@10.4.3: {}
 | 
			
		||||
 | 
			
		||||
  lru-cache@5.1.1:
 | 
			
		||||
@@ -5045,6 +5113,8 @@ snapshots:
 | 
			
		||||
 | 
			
		||||
  markdown-table@3.0.4: {}
 | 
			
		||||
 | 
			
		||||
  marked@16.4.1: {}
 | 
			
		||||
 | 
			
		||||
  mdast-util-definitions@6.0.0:
 | 
			
		||||
    dependencies:
 | 
			
		||||
      '@types/mdast': 4.0.4
 | 
			
		||||
@@ -5548,6 +5618,8 @@ snapshots:
 | 
			
		||||
 | 
			
		||||
  normalize-path@3.0.0: {}
 | 
			
		||||
 | 
			
		||||
  object-assign@4.1.1: {}
 | 
			
		||||
 | 
			
		||||
  ofetch@1.4.1:
 | 
			
		||||
    dependencies:
 | 
			
		||||
      destr: 2.0.5
 | 
			
		||||
@@ -5759,6 +5831,12 @@ snapshots:
 | 
			
		||||
      kleur: 3.0.3
 | 
			
		||||
      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@7.1.0: {}
 | 
			
		||||
@@ -5809,6 +5887,10 @@ snapshots:
 | 
			
		||||
      react-dom: 19.2.0(react@19.2.0)
 | 
			
		||||
      typescript: 5.9.3
 | 
			
		||||
 | 
			
		||||
  react-is@16.13.1: {}
 | 
			
		||||
 | 
			
		||||
  react-lifecycles-compat@3.0.4: {}
 | 
			
		||||
 | 
			
		||||
  react-refresh@0.17.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-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: {}
 | 
			
		||||
 | 
			
		||||
  readable-stream@0.0.4: {}
 | 
			
		||||
 
 | 
			
		||||
@@ -36,6 +36,7 @@
 | 
			
		||||
    "highlight.js": "^11.11.1",
 | 
			
		||||
    "lodash-es": "^4.17.21",
 | 
			
		||||
    "lucide-react": "^0.545.0",
 | 
			
		||||
    "marked": "^16.4.1",
 | 
			
		||||
    "nanoid": "^5.1.6",
 | 
			
		||||
    "pocketbase": "^0.26.2",
 | 
			
		||||
    "pouchdb-adapter-memory": "^9.0.0",
 | 
			
		||||
@@ -44,6 +45,7 @@
 | 
			
		||||
    "react-dom": "^19.2.0",
 | 
			
		||||
    "react-resizable-panels": "^3.0.6",
 | 
			
		||||
    "react-toastify": "^11.0.5",
 | 
			
		||||
    "react-virtualized": "^9.22.6",
 | 
			
		||||
    "sigma": "^3.0.2",
 | 
			
		||||
    "tailwind-merge": "^3.3.1",
 | 
			
		||||
    "three": "^0.180.0",
 | 
			
		||||
@@ -59,6 +61,7 @@
 | 
			
		||||
    "@types/pouchdb-browser": "^6.1.5",
 | 
			
		||||
    "@types/react": "^19.2.2",
 | 
			
		||||
    "@types/react-dom": "^19.2.2",
 | 
			
		||||
    "@types/react-virtualized": "^9.22.3",
 | 
			
		||||
    "@types/three": "^0.180.0",
 | 
			
		||||
    "@vitejs/plugin-basic-ssl": "^2.1.0",
 | 
			
		||||
    "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 { markService } from "../modules/mark-service";
 | 
			
		||||
import { SigmaGraph } from "./graph/sigma/index";
 | 
			
		||||
import { DocsComponent } from "./docs";
 | 
			
		||||
import { MarkDetailList } from "./card/MarkDetailList";
 | 
			
		||||
 | 
			
		||||
const tabs = [
 | 
			
		||||
  {
 | 
			
		||||
    key: 'table',
 | 
			
		||||
    title: '表格'
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    key: 'card',
 | 
			
		||||
    title: '卡片'
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    key: 'graph',
 | 
			
		||||
    title: '关系图'
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    key: 'world',
 | 
			
		||||
    title: '世界'
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    key: 'docs',
 | 
			
		||||
    title: '文档'
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    key: 'world',
 | 
			
		||||
    title: '世界'
 | 
			
		||||
  }
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
@@ -41,18 +48,16 @@ export const BaseApp = () => {
 | 
			
		||||
            <SigmaGraph dataSource={dataSource} />
 | 
			
		||||
          </div>
 | 
			
		||||
        );
 | 
			
		||||
      case 'card':
 | 
			
		||||
        return <MarkDetailList data={dataSource} />;
 | 
			
		||||
      case 'docs':
 | 
			
		||||
        return <DocsComponent dataSource={dataSource} />;
 | 
			
		||||
      case 'world':
 | 
			
		||||
        return (
 | 
			
		||||
          <div className="flex items-center justify-center h-96 text-gray-500">
 | 
			
		||||
            世界模块暂未实现
 | 
			
		||||
          </div>
 | 
			
		||||
        );
 | 
			
		||||
      case 'docs':
 | 
			
		||||
        return (
 | 
			
		||||
          <div className="flex items-center justify-center h-96 text-gray-500">
 | 
			
		||||
            文档模块暂未实现
 | 
			
		||||
          </div>
 | 
			
		||||
        );
 | 
			
		||||
      default:
 | 
			
		||||
        return null;
 | 
			
		||||
    }
 | 
			
		||||
@@ -67,9 +72,9 @@ export const BaseApp = () => {
 | 
			
		||||
            <button
 | 
			
		||||
              key={tab.key}
 | 
			
		||||
              onClick={() => setActiveTab(tab.key)}
 | 
			
		||||
              className={`py-2 px-1 border-b-2 font-medium text-sm ${activeTab === tab.key
 | 
			
		||||
                ? 'border-blue-500 text-blue-600'
 | 
			
		||||
                : 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
 | 
			
		||||
              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 bg-blue-50'
 | 
			
		||||
                : 'border-transparent text-gray-500'
 | 
			
		||||
                }`}
 | 
			
		||||
            >
 | 
			
		||||
              {tab.title}
 | 
			
		||||
@@ -79,7 +84,7 @@ export const BaseApp = () => {
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
      {/* Tab 内容区域 */}
 | 
			
		||||
      <div className="flex-1">
 | 
			
		||||
      <div className="flex-1 h-full">
 | 
			
		||||
        {renderContent()}
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
# 数据管理表格组件
 | 
			
		||||
 | 
			
		||||
这是一个功能完整的React表格组件,支持多选、排序、分页、操作等功能,并集成了Mock数据。
 | 
			
		||||
这是一个功能完整的React表格组件,支持多选、排序、虚拟滚动、操作等功能,并集成了Mock数据。
 | 
			
		||||
 | 
			
		||||
## 功能特性
 | 
			
		||||
 | 
			
		||||
@@ -21,11 +21,11 @@
 | 
			
		||||
   - 升序/降序/取消排序
 | 
			
		||||
   - 排序状态可视化指示
 | 
			
		||||
 | 
			
		||||
4. **分页功能**
 | 
			
		||||
   - 支持页码切换
 | 
			
		||||
   - 可调整每页显示数量(10/20/50/100条)
 | 
			
		||||
   - 显示总数和当前范围
 | 
			
		||||
   - 快速跳转页码
 | 
			
		||||
4. **虚拟滚动功能**
 | 
			
		||||
   - 基于 react-virtualized 实现高性能虚拟滚动
 | 
			
		||||
   - 支持大量数据展示而不影响性能
 | 
			
		||||
   - 可配置行高和表格高度
 | 
			
		||||
   - 固定表头,滚动内容区域
 | 
			
		||||
 | 
			
		||||
5. **操作功能**
 | 
			
		||||
   - 详情查看(弹窗形式)
 | 
			
		||||
@@ -148,23 +148,19 @@ base/table/
 | 
			
		||||
}
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
### 修改分页配置
 | 
			
		||||
调整 `paginationConfig` 对象的属性:
 | 
			
		||||
### 修改虚拟滚动配置
 | 
			
		||||
调整 `virtualScrollConfig` 对象的属性:
 | 
			
		||||
 | 
			
		||||
```tsx
 | 
			
		||||
const paginationConfig = {
 | 
			
		||||
  current: currentPage,
 | 
			
		||||
  pageSize: pageSize,
 | 
			
		||||
  total: data.length,
 | 
			
		||||
  showSizeChanger: true,
 | 
			
		||||
  showQuickJumper: true,
 | 
			
		||||
  // 其他配置...
 | 
			
		||||
const virtualScrollConfig = {
 | 
			
		||||
  rowHeight: 60,    // 行高度
 | 
			
		||||
  height: 600       // 表格容器高度
 | 
			
		||||
};
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
## 性能优化
 | 
			
		||||
 | 
			
		||||
1. **虚拟化**:对于大量数据,可以考虑实现虚拟滚动
 | 
			
		||||
1. **虚拟滚动**:已实现基于 react-virtualized 的虚拟滚动,支持大量数据高性能展示
 | 
			
		||||
2. **懒加载**:支持服务端分页和按需加载
 | 
			
		||||
3. **缓存**:实现数据缓存机制
 | 
			
		||||
4. **防抖**:搜索和过滤功能添加防抖处理
 | 
			
		||||
 
 | 
			
		||||
@@ -1,19 +1,24 @@
 | 
			
		||||
import React, { useState, useMemo } from 'react';
 | 
			
		||||
import { AutoSizer, List } from 'react-virtualized';
 | 
			
		||||
import { Mark } from '../mock/collection';
 | 
			
		||||
import { TableProps, SortState } from './types';
 | 
			
		||||
import './table.css';
 | 
			
		||||
 | 
			
		||||
// 虚拟滚动常量
 | 
			
		||||
const DEFAULT_ROW_HEIGHT = 48; // 每行高度
 | 
			
		||||
const HEADER_HEIGHT = 48; // 表头高度
 | 
			
		||||
 | 
			
		||||
export const Table: React.FC<TableProps> = ({
 | 
			
		||||
  data,
 | 
			
		||||
  columns,
 | 
			
		||||
  loading = false,
 | 
			
		||||
  rowSelection,
 | 
			
		||||
  pagination,
 | 
			
		||||
  virtualScroll,
 | 
			
		||||
  actions,
 | 
			
		||||
  onSort
 | 
			
		||||
}) => {
 | 
			
		||||
  const rowHeight = virtualScroll?.rowHeight || DEFAULT_ROW_HEIGHT;
 | 
			
		||||
  const [sortState, setSortState] = useState<SortState>({ field: null, order: null });
 | 
			
		||||
  const [currentPage, setCurrentPage] = useState(1);
 | 
			
		||||
 | 
			
		||||
  // 处理排序
 | 
			
		||||
  const handleSort = (field: string) => {
 | 
			
		||||
@@ -46,38 +51,16 @@ export const Table: React.FC<TableProps> = ({
 | 
			
		||||
    });
 | 
			
		||||
  }, [data, sortState]);
 | 
			
		||||
 | 
			
		||||
  // 分页数据
 | 
			
		||||
  const paginatedData = useMemo(() => {
 | 
			
		||||
    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 displayData = sortedData;
 | 
			
		||||
 | 
			
		||||
  // 全选/取消全选
 | 
			
		||||
  const handleSelectAll = (checked: boolean) => {
 | 
			
		||||
    if (!rowSelection) return;
 | 
			
		||||
    
 | 
			
		||||
    const allKeys = paginatedData.map(item => item.id);
 | 
			
		||||
    const allKeys = displayData.map(item => item.id);
 | 
			
		||||
    const selectedKeys = checked ? allKeys : [];
 | 
			
		||||
    const selectedRows = checked ? paginatedData : [];
 | 
			
		||||
    const selectedRows = checked ? displayData : [];
 | 
			
		||||
    
 | 
			
		||||
    rowSelection.onChange?.(selectedKeys, selectedRows);
 | 
			
		||||
  };
 | 
			
		||||
@@ -100,6 +83,52 @@ export const Table: React.FC<TableProps> = ({
 | 
			
		||||
    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) {
 | 
			
		||||
    return (
 | 
			
		||||
      <div className="table-loading">
 | 
			
		||||
@@ -110,11 +139,11 @@ export const Table: React.FC<TableProps> = ({
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  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;
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div className="table-container">
 | 
			
		||||
    <div className="table-container ">
 | 
			
		||||
      {/* 表格工具栏 */}
 | 
			
		||||
      {rowSelection && selectedKeys.length > 0 && (
 | 
			
		||||
        <div className="table-toolbar">
 | 
			
		||||
@@ -134,173 +163,73 @@ export const Table: React.FC<TableProps> = ({
 | 
			
		||||
 | 
			
		||||
      {/* 表格 */}
 | 
			
		||||
      <div className="table-wrapper">
 | 
			
		||||
        <table className="data-table">
 | 
			
		||||
          <thead>
 | 
			
		||||
            <tr>
 | 
			
		||||
              {rowSelection && (
 | 
			
		||||
                <th className="selection-column">
 | 
			
		||||
                  <input
 | 
			
		||||
                    type="checkbox"
 | 
			
		||||
                    checked={isAllSelected}
 | 
			
		||||
                    ref={input => {
 | 
			
		||||
                      if (input) input.indeterminate = isIndeterminate;
 | 
			
		||||
                    }}
 | 
			
		||||
                    onChange={(e) => handleSelectAll(e.target.checked)}
 | 
			
		||||
                  />
 | 
			
		||||
                </th>
 | 
			
		||||
              )}
 | 
			
		||||
              {columns.map(column => (
 | 
			
		||||
                <th 
 | 
			
		||||
                  key={column.key}
 | 
			
		||||
                  style={{ width: column.width }}
 | 
			
		||||
                  className={column.sortable ? 'sortable' : ''}
 | 
			
		||||
                >
 | 
			
		||||
                  <div className="table-header">
 | 
			
		||||
                    <span>{column.title}</span>
 | 
			
		||||
                    {column.sortable && (
 | 
			
		||||
                      <div 
 | 
			
		||||
                        className="sort-indicators"
 | 
			
		||||
                        onClick={() => handleSort(column.dataIndex)}
 | 
			
		||||
                      >
 | 
			
		||||
                        <span className={`sort-arrow sort-up ${
 | 
			
		||||
                          sortState.field === column.dataIndex && sortState.order === 'asc' ? 'active' : ''
 | 
			
		||||
                        }`}>▲</span>
 | 
			
		||||
                        <span className={`sort-arrow sort-down ${
 | 
			
		||||
                          sortState.field === column.dataIndex && sortState.order === 'desc' ? 'active' : ''
 | 
			
		||||
                        }`}>▼</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 className="table-header-wrapper" style={{ height: HEADER_HEIGHT }}>
 | 
			
		||||
          <div className="table-header-row">
 | 
			
		||||
            {rowSelection && (
 | 
			
		||||
              <div className="selection-column">
 | 
			
		||||
                <input
 | 
			
		||||
                  type="checkbox"
 | 
			
		||||
                  checked={isAllSelected}
 | 
			
		||||
                  ref={input => {
 | 
			
		||||
                    if (input) input.indeterminate = isIndeterminate;
 | 
			
		||||
                  }}
 | 
			
		||||
                  onChange={(e) => handleSelectAll(e.target.checked)}
 | 
			
		||||
                />
 | 
			
		||||
              </div>
 | 
			
		||||
            )}
 | 
			
		||||
            {columns.map(column => (
 | 
			
		||||
              <div 
 | 
			
		||||
                key={column.key}
 | 
			
		||||
                style={{ width: column.width }}
 | 
			
		||||
                className={`table-header-cell ${column.sortable ? 'sortable' : ''}`}
 | 
			
		||||
              >
 | 
			
		||||
                <div className="table-header">
 | 
			
		||||
                  <span>{column.title}</span>
 | 
			
		||||
                  {column.sortable && (
 | 
			
		||||
                    <div 
 | 
			
		||||
                      className="sort-indicators"
 | 
			
		||||
                      onClick={() => handleSort(column.dataIndex)}
 | 
			
		||||
                    >
 | 
			
		||||
                      <span className={`sort-arrow sort-up ${
 | 
			
		||||
                        sortState.field === column.dataIndex && sortState.order === 'asc' ? 'active' : ''
 | 
			
		||||
                      }`}>▲</span>
 | 
			
		||||
                      <span className={`sort-arrow sort-down ${
 | 
			
		||||
                        sortState.field === column.dataIndex && sortState.order === 'desc' ? 'active' : ''
 | 
			
		||||
                      }`}>▼</span>
 | 
			
		||||
                    </div>
 | 
			
		||||
                  </td>
 | 
			
		||||
                )}
 | 
			
		||||
              </tr>
 | 
			
		||||
                  )}
 | 
			
		||||
                </div>
 | 
			
		||||
              </div>
 | 
			
		||||
            ))}
 | 
			
		||||
          </tbody>
 | 
			
		||||
        </table>
 | 
			
		||||
 | 
			
		||||
        {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)
 | 
			
		||||
              ]
 | 
			
		||||
            {actions && actions.length > 0 && (
 | 
			
		||||
              <div className="actions-column">操作</div>
 | 
			
		||||
            )}
 | 
			
		||||
          </div>
 | 
			
		||||
          <div className="pagination-controls">
 | 
			
		||||
            <button
 | 
			
		||||
              className="btn btn-default"
 | 
			
		||||
              onClick={() => handlePageChange(currentPage - 1)}
 | 
			
		||||
              disabled={currentPage <= 1}
 | 
			
		||||
            >
 | 
			
		||||
              上一页
 | 
			
		||||
            </button>
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
            <div className="page-numbers">
 | 
			
		||||
              {Array.from({ length: Math.ceil(pagination.total / pagination.pageSize) })
 | 
			
		||||
                .map((_, i) => i + 1)
 | 
			
		||||
                .filter(page => {
 | 
			
		||||
                  const distance = Math.abs(page - currentPage);
 | 
			
		||||
                  return distance === 0 || distance <= 2 || page === 1 || page === Math.ceil(pagination.total / pagination.pageSize);
 | 
			
		||||
                })
 | 
			
		||||
                .map((page, index, pages) => {
 | 
			
		||||
                  const prevPage = pages[index - 1];
 | 
			
		||||
                  const showEllipsis = prevPage && page - prevPage > 1;
 | 
			
		||||
                  
 | 
			
		||||
                  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 className="table-body-wrapper">
 | 
			
		||||
          {displayData.length > 0 ? (
 | 
			
		||||
            <AutoSizer>
 | 
			
		||||
              {({ height, width }) => (
 | 
			
		||||
                <List
 | 
			
		||||
                  height={height}
 | 
			
		||||
                  width={width}
 | 
			
		||||
                  rowCount={displayData.length}
 | 
			
		||||
                  rowHeight={rowHeight}
 | 
			
		||||
                  rowRenderer={rowRenderer}
 | 
			
		||||
                />
 | 
			
		||||
              )}
 | 
			
		||||
            </AutoSizer>
 | 
			
		||||
          ) : (
 | 
			
		||||
            <div className="empty-state">
 | 
			
		||||
              <div className="empty-icon">📭</div>
 | 
			
		||||
              <p>暂无数据</p>
 | 
			
		||||
            </div>
 | 
			
		||||
          )}
 | 
			
		||||
        </div>
 | 
			
		||||
      )}
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
@@ -1,4 +1,5 @@
 | 
			
		||||
import React, { useState, useMemo, useEffect } from 'react';
 | 
			
		||||
import { Eye, Edit, Trash2 } from 'lucide-react';
 | 
			
		||||
import { Table } from './Table';
 | 
			
		||||
import { DetailModal } from './DetailModal';
 | 
			
		||||
import { Mark } from '../mock/collection';
 | 
			
		||||
@@ -10,8 +11,6 @@ type Props = {
 | 
			
		||||
export const Base = (props: Props) => {
 | 
			
		||||
  const { dataSource = [] } = props;
 | 
			
		||||
  const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
 | 
			
		||||
  const [currentPage, setCurrentPage] = useState(1);
 | 
			
		||||
  const [pageSize, setPageSize] = useState(10);
 | 
			
		||||
  const [data, setData] = useState<Mark[]>([]);
 | 
			
		||||
  const [detailModalVisible, setDetailModalVisible] = useState(false);
 | 
			
		||||
  const [currentRecord, setCurrentRecord] = useState<Mark | null>(null);
 | 
			
		||||
@@ -127,16 +126,15 @@ export const Base = (props: Props) => {
 | 
			
		||||
    {
 | 
			
		||||
      key: 'view',
 | 
			
		||||
      label: '详情',
 | 
			
		||||
      type: 'primary',
 | 
			
		||||
      icon: '👁',
 | 
			
		||||
      icon: <Eye className="w-4 h-4 text-blue-600 hover:text-blue-700 cursor-pointer transition-colors" />,
 | 
			
		||||
      onClick: (record: Mark) => {
 | 
			
		||||
        handleViewDetail(record);
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      key: 'edit',
 | 
			
		||||
      icon: <Edit className="w-4 h-4 text-green-600 hover:text-green-700 cursor-pointer transition-colors" />,
 | 
			
		||||
      label: '编辑',
 | 
			
		||||
      icon: '✏️',
 | 
			
		||||
      onClick: (record: Mark) => {
 | 
			
		||||
        handleEdit(record);
 | 
			
		||||
      }
 | 
			
		||||
@@ -144,8 +142,7 @@ export const Base = (props: Props) => {
 | 
			
		||||
    {
 | 
			
		||||
      key: 'delete',
 | 
			
		||||
      label: '删除',
 | 
			
		||||
      type: 'danger',
 | 
			
		||||
      icon: '🗑️',
 | 
			
		||||
      icon: <Trash2 className="w-4 h-4 text-red-600 hover:text-red-700 cursor-pointer transition-colors" />,
 | 
			
		||||
      onClick: (record: Mark) => {
 | 
			
		||||
        handleDelete(record);
 | 
			
		||||
      }
 | 
			
		||||
@@ -222,72 +219,75 @@ export const Base = (props: Props) => {
 | 
			
		||||
    setData(sortedData);
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  // 分页配置
 | 
			
		||||
  const paginationConfig = {
 | 
			
		||||
    current: currentPage,
 | 
			
		||||
    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);
 | 
			
		||||
    }
 | 
			
		||||
  // 虚拟滚动配置
 | 
			
		||||
  const virtualScrollConfig = {
 | 
			
		||||
    rowHeight: 60, // 因为有两行内容,需要更高的行高
 | 
			
		||||
    // 移除固定高度,让表格自适应容器高度
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div style={{ padding: '24px' }}>
 | 
			
		||||
      <div style={{ marginBottom: '16px' }}>
 | 
			
		||||
        <h2>数据管理表格</h2>
 | 
			
		||||
        <p style={{ color: '#666', margin: '8px 0' }}>
 | 
			
		||||
          支持多选、排序、分页等功能的数据表格示例
 | 
			
		||||
        </p>
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
      {selectedRowKeys.length > 0 && (
 | 
			
		||||
        <div style={{
 | 
			
		||||
          marginBottom: '16px',
 | 
			
		||||
          padding: '12px',
 | 
			
		||||
          backgroundColor: '#e6f7ff',
 | 
			
		||||
          borderRadius: '4px',
 | 
			
		||||
          display: 'flex',
 | 
			
		||||
          justifyContent: 'space-between',
 | 
			
		||||
          alignItems: 'center'
 | 
			
		||||
        }}>
 | 
			
		||||
          <span>已选择 {selectedRowKeys.length} 项</span>
 | 
			
		||||
          <button
 | 
			
		||||
            className="btn btn-danger"
 | 
			
		||||
            onClick={handleBatchDelete}
 | 
			
		||||
          >
 | 
			
		||||
            批量删除
 | 
			
		||||
          </button>
 | 
			
		||||
    <div className='h-full flex flex-col'>
 | 
			
		||||
      <div className='flex flex-col h-full' style={{ padding: '24px' }}>
 | 
			
		||||
        {/* 固定头部区域 */}
 | 
			
		||||
        <div style={{ marginBottom: '16px', flexShrink: 0 }}>
 | 
			
		||||
          <h2 style={{ margin: '0 0 8px 0' }}>数据管理表格</h2>
 | 
			
		||||
          <p style={{ color: '#666', margin: '0' }}>
 | 
			
		||||
            支持多选、排序、虚拟滚动等功能的数据表格示例
 | 
			
		||||
          </p>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div>
 | 
			
		||||
          {/* 批量操作条 */}
 | 
			
		||||
          {selectedRowKeys.length > 0 && (
 | 
			
		||||
            <div style={{
 | 
			
		||||
              marginBottom: '16px',
 | 
			
		||||
              padding: '12px',
 | 
			
		||||
              backgroundColor: '#e6f7ff',
 | 
			
		||||
              borderRadius: '4px',
 | 
			
		||||
              display: 'flex',
 | 
			
		||||
              justifyContent: 'space-between',
 | 
			
		||||
              alignItems: 'center',
 | 
			
		||||
              flexShrink: 0
 | 
			
		||||
            }}>
 | 
			
		||||
              <span>已选择 {selectedRowKeys.length} 项</span>
 | 
			
		||||
              <button
 | 
			
		||||
                className="btn btn-danger"
 | 
			
		||||
                onClick={handleBatchDelete}
 | 
			
		||||
              >
 | 
			
		||||
                批量删除
 | 
			
		||||
              </button>
 | 
			
		||||
            </div>
 | 
			
		||||
          )}
 | 
			
		||||
        </div>
 | 
			
		||||
      )}
 | 
			
		||||
 | 
			
		||||
      <Table
 | 
			
		||||
        data={data}
 | 
			
		||||
        columns={columns}
 | 
			
		||||
        actions={actions}
 | 
			
		||||
        rowSelection={{
 | 
			
		||||
          selectedRowKeys,
 | 
			
		||||
          onChange: (keys, rows) => {
 | 
			
		||||
            setSelectedRowKeys(keys);
 | 
			
		||||
          }
 | 
			
		||||
        }}
 | 
			
		||||
        pagination={paginationConfig}
 | 
			
		||||
        onSort={handleSort}
 | 
			
		||||
      />
 | 
			
		||||
        {/* 表格容器 - 占据剩余空间并支持滚动 */}
 | 
			
		||||
        <div style={{
 | 
			
		||||
          flex: 1,
 | 
			
		||||
          minHeight: 0 // 重要:允许flex容器收缩
 | 
			
		||||
        }}>
 | 
			
		||||
          <Table
 | 
			
		||||
            data={data}
 | 
			
		||||
            columns={columns}
 | 
			
		||||
            actions={actions}
 | 
			
		||||
            rowSelection={{
 | 
			
		||||
              selectedRowKeys,
 | 
			
		||||
              onChange: (keys, rows) => {
 | 
			
		||||
                setSelectedRowKeys(keys);
 | 
			
		||||
              }
 | 
			
		||||
            }}
 | 
			
		||||
            virtualScroll={virtualScrollConfig}
 | 
			
		||||
            onSort={handleSort}
 | 
			
		||||
          />
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
      <DetailModal
 | 
			
		||||
        visible={detailModalVisible}
 | 
			
		||||
        data={currentRecord}
 | 
			
		||||
        onClose={() => {
 | 
			
		||||
          setDetailModalVisible(false);
 | 
			
		||||
          setCurrentRecord(null);
 | 
			
		||||
        }}
 | 
			
		||||
      />
 | 
			
		||||
        <DetailModal
 | 
			
		||||
          visible={detailModalVisible}
 | 
			
		||||
          data={currentRecord}
 | 
			
		||||
          onClose={() => {
 | 
			
		||||
            setDetailModalVisible(false);
 | 
			
		||||
            setCurrentRecord(null);
 | 
			
		||||
          }}
 | 
			
		||||
        />
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
@@ -4,6 +4,10 @@
 | 
			
		||||
  border-radius: 8px;
 | 
			
		||||
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
 | 
			
		||||
  overflow: hidden;
 | 
			
		||||
  height: calc(100% - 24px); /* 距离底部保持24px的间距 */
 | 
			
		||||
  margin-bottom: 24px; /* 底部间距 */
 | 
			
		||||
  display: flex;
 | 
			
		||||
  flex-direction: column;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* 工具栏 */
 | 
			
		||||
@@ -14,6 +18,7 @@
 | 
			
		||||
  padding: 16px;
 | 
			
		||||
  background: #f5f5f5;
 | 
			
		||||
  border-bottom: 1px solid #e8e8e8;
 | 
			
		||||
  flex-shrink: 0; /* 工具栏不压缩,保持固定高度 */
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.selected-info {
 | 
			
		||||
@@ -26,37 +31,83 @@
 | 
			
		||||
  gap: 8px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* 表格主体 */
 | 
			
		||||
/* 表格主体 - 虚拟滚动布局 */
 | 
			
		||||
.table-wrapper {
 | 
			
		||||
  overflow-x: auto;
 | 
			
		||||
  display: flex;
 | 
			
		||||
  flex-direction: column;
 | 
			
		||||
  flex: 1; /* 占满剩余空间 */
 | 
			
		||||
  height: 100%; /* 占满父容器高度 */
 | 
			
		||||
  min-height: 200px; /* 最小高度,避免过小 */
 | 
			
		||||
  overflow: hidden; /* 防止溢出 */
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.data-table {
 | 
			
		||||
  width: 100%;
 | 
			
		||||
  border-collapse: collapse;
 | 
			
		||||
  font-size: 14px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.data-table th,
 | 
			
		||||
.data-table td {
 | 
			
		||||
  padding: 12px 16px;
 | 
			
		||||
  text-align: left;
 | 
			
		||||
/* 固定表头容器 */
 | 
			
		||||
.table-header-wrapper {
 | 
			
		||||
  flex-shrink: 0;
 | 
			
		||||
  border-bottom: 1px solid #e8e8e8;
 | 
			
		||||
  background: #fafafa;
 | 
			
		||||
  overflow: hidden;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.data-table th {
 | 
			
		||||
  background: #fafafa;
 | 
			
		||||
.table-header-row {
 | 
			
		||||
  display: flex;
 | 
			
		||||
  align-items: center;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.table-header-cell {
 | 
			
		||||
  display: flex;
 | 
			
		||||
  align-items: center;
 | 
			
		||||
  padding: 12px 16px;
 | 
			
		||||
  border-right: 1px solid #e8e8e8;
 | 
			
		||||
  font-weight: 600;
 | 
			
		||||
  color: #333;
 | 
			
		||||
  position: sticky;
 | 
			
		||||
  top: 0;
 | 
			
		||||
  z-index: 10;
 | 
			
		||||
  background: #fafafa;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.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;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.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 {
 | 
			
		||||
  display: flex;
 | 
			
		||||
@@ -103,66 +154,220 @@
 | 
			
		||||
 | 
			
		||||
/* 操作列 */
 | 
			
		||||
.actions-column {
 | 
			
		||||
  width: 200px;
 | 
			
		||||
  width: auto;
 | 
			
		||||
  min-width: 120px;
 | 
			
		||||
  text-align: right;
 | 
			
		||||
  padding: 8px 16px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.action-buttons {
 | 
			
		||||
  position: relative;
 | 
			
		||||
  display: flex;
 | 
			
		||||
  gap: 8px;
 | 
			
		||||
  gap: 6px;
 | 
			
		||||
  justify-content: flex-end;
 | 
			
		||||
  align-items: center;
 | 
			
		||||
  flex-wrap: wrap;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* 按钮样式 */
 | 
			
		||||
.btn {
 | 
			
		||||
  position: relative;
 | 
			
		||||
  display: inline-flex;
 | 
			
		||||
  align-items: center;
 | 
			
		||||
  justify-content: center;
 | 
			
		||||
  gap: 4px;
 | 
			
		||||
  padding: 6px 12px;
 | 
			
		||||
  padding: 8px 12px;
 | 
			
		||||
  min-width: 32px;
 | 
			
		||||
  height: 32px;
 | 
			
		||||
  border: 1px solid #d9d9d9;
 | 
			
		||||
  border-radius: 4px;
 | 
			
		||||
  border-radius: 6px;
 | 
			
		||||
  background: #fff;
 | 
			
		||||
  color: #333;
 | 
			
		||||
  font-size: 12px;
 | 
			
		||||
  font-weight: 500;
 | 
			
		||||
  cursor: pointer;
 | 
			
		||||
  transition: all 0.2s;
 | 
			
		||||
  transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
 | 
			
		||||
  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;
 | 
			
		||||
  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 {
 | 
			
		||||
  opacity: 0.5;
 | 
			
		||||
  cursor: not-allowed;
 | 
			
		||||
  transform: none !important;
 | 
			
		||||
  box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05) !important;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.btn-primary {
 | 
			
		||||
  background: #1890ff;
 | 
			
		||||
  background: linear-gradient(135deg, #1890ff 0%, #096dd9 100%);
 | 
			
		||||
  border-color: #1890ff;
 | 
			
		||||
  color: #fff;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.btn-primary:hover:not(:disabled) {
 | 
			
		||||
  background: #40a9ff;
 | 
			
		||||
  background: linear-gradient(135deg, #40a9ff 0%, #1890ff 100%);
 | 
			
		||||
  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 {
 | 
			
		||||
  background: #ff4d4f;
 | 
			
		||||
  background: linear-gradient(135deg, #ff4d4f 0%, #f5222d 100%);
 | 
			
		||||
  border-color: #ff4d4f;
 | 
			
		||||
  color: #fff;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.btn-danger:hover:not(:disabled) {
 | 
			
		||||
  background: #ff7875;
 | 
			
		||||
  background: linear-gradient(135deg, #ff7875 0%, #ff4d4f 100%);
 | 
			
		||||
  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 {
 | 
			
		||||
  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-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); }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* 分页 */
 | 
			
		||||
.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) {
 | 
			
		||||
  .table-toolbar {
 | 
			
		||||
@@ -281,32 +419,62 @@
 | 
			
		||||
    justify-content: flex-end;
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  .pagination-wrapper {
 | 
			
		||||
    flex-direction: column;
 | 
			
		||||
    gap: 12px;
 | 
			
		||||
    align-items: stretch;
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  .pagination-controls {
 | 
			
		||||
    justify-content: center;
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  .page-size-selector {
 | 
			
		||||
    margin-left: 0;
 | 
			
		||||
    text-align: center;
 | 
			
		||||
  .table-wrapper {
 | 
			
		||||
    min-height: 300px; /* 移动端最小高度,仍然使用flex: 1占满剩余空间 */
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  .action-buttons {
 | 
			
		||||
    flex-direction: column;
 | 
			
		||||
    flex-direction: row;
 | 
			
		||||
    gap: 4px;
 | 
			
		||||
    justify-content: flex-end;
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  .actions-column {
 | 
			
		||||
    width: 120px;
 | 
			
		||||
    min-width: 80px;
 | 
			
		||||
    padding: 6px 12px;
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  .btn {
 | 
			
		||||
    font-size: 11px;
 | 
			
		||||
    padding: 6px 10px;
 | 
			
		||||
    min-width: 28px;
 | 
			
		||||
    height: 28px;
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  .btn-icon {
 | 
			
		||||
    font-size: 12px;
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  /* 移动端tooltip调整 */
 | 
			
		||||
  .tooltip {
 | 
			
		||||
    font-size: 11px;
 | 
			
		||||
    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 };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 分页配置
 | 
			
		||||
export interface PaginationConfig {
 | 
			
		||||
  current: number;
 | 
			
		||||
  pageSize: number;
 | 
			
		||||
  total: number;
 | 
			
		||||
  showSizeChanger?: boolean;
 | 
			
		||||
  showQuickJumper?: boolean;
 | 
			
		||||
  showTotal?: (total: number, range: [number, number]) => string;
 | 
			
		||||
  onChange?: (page: number, pageSize: number) => void;
 | 
			
		||||
// 虚拟滚动配置
 | 
			
		||||
export interface VirtualScrollConfig {
 | 
			
		||||
  rowHeight?: number;  // 行高度,默认 48px
 | 
			
		||||
  height?: number;     // 表格容器高度,默认 400px
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 表格操作按钮类型
 | 
			
		||||
export interface ActionButton {
 | 
			
		||||
  key: string;
 | 
			
		||||
  label: string;
 | 
			
		||||
  type?: 'primary' | 'default' | 'danger';
 | 
			
		||||
  className?: string;
 | 
			
		||||
  icon?: React.ReactNode;
 | 
			
		||||
  onClick: (record: Mark) => void;
 | 
			
		||||
  disabled?: (record: Mark) => boolean;
 | 
			
		||||
  tooltip?: string; // 可选的自定义tooltip文本
 | 
			
		||||
  size?: 'small' | 'medium' | 'large'; // 按钮尺寸
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 表格属性
 | 
			
		||||
@@ -46,7 +43,7 @@ export interface TableProps {
 | 
			
		||||
  columns: TableColumn<Mark>[];
 | 
			
		||||
  loading?: boolean;
 | 
			
		||||
  rowSelection?: RowSelection<Mark>;
 | 
			
		||||
  pagination?: PaginationConfig | false;
 | 
			
		||||
  virtualScroll?: VirtualScrollConfig;
 | 
			
		||||
  actions?: ActionButton[];
 | 
			
		||||
  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 { Panel, PanelGroup, PanelResizeHandle } from 'react-resizable-panels';
 | 
			
		||||
import { useState } from 'react';
 | 
			
		||||
import { useState, useRef } from 'react';
 | 
			
		||||
 | 
			
		||||
import { VadVoice } from './videos/modules/VadVoice.tsx';
 | 
			
		||||
import { ChatInterface } from './prompts/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 = () => {
 | 
			
		||||
  return (
 | 
			
		||||
@@ -47,6 +47,64 @@ export const MuseApp = () => {
 | 
			
		||||
  const [showRightPanel, setShowRightPanel] = useState(true);
 | 
			
		||||
  const [showLeftPanel, setShowLeftPanel] = 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 (
 | 
			
		||||
    <div className="h-screen flex flex-col">
 | 
			
		||||
@@ -85,11 +143,32 @@ export const MuseApp = () => {
 | 
			
		||||
          }}>
 | 
			
		||||
            初始化DB
 | 
			
		||||
          </button>
 | 
			
		||||
          <button className="px-3 py-1 rounded text-sm bg-red-500 text-white hover:bg-red-600" onClick={() => {
 | 
			
		||||
            // 删除DB的逻辑
 | 
			
		||||
          }}>
 | 
			
		||||
          <button 
 | 
			
		||||
            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
 | 
			
		||||
          </button>
 | 
			
		||||
          {/* 隐藏的文件输入元素 */}
 | 
			
		||||
          <input
 | 
			
		||||
            ref={fileInputRef}
 | 
			
		||||
            type="file"
 | 
			
		||||
            accept=".json"
 | 
			
		||||
            onChange={handleFileImport}
 | 
			
		||||
            style={{ display: 'none' }}
 | 
			
		||||
          />
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -27,12 +27,32 @@ export class MarkDB {
 | 
			
		||||
    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() {
 | 
			
		||||
    if (!this.db) {
 | 
			
		||||
      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 {
 | 
			
		||||
      // PouchDB 创建索引的正确方式
 | 
			
		||||
      const indexes = [
 | 
			
		||||
@@ -64,7 +84,8 @@ export class MarkDB {
 | 
			
		||||
      console.log('索引初始化完成');
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      console.error('创建索引失败:', error);
 | 
			
		||||
      throw error;
 | 
			
		||||
      // 不再抛出错误,而是警告用户
 | 
			
		||||
      console.warn('索引创建失败,但数据库可以继续使用(性能可能受影响)');
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@@ -123,14 +144,18 @@ export class MarkDB {
 | 
			
		||||
  // 按用户 ID 获取 Marks
 | 
			
		||||
  async getByUserId(uid: string): Promise<Mark[]> {
 | 
			
		||||
    try {
 | 
			
		||||
      const result = await this.db.find({
 | 
			
		||||
        selector: {
 | 
			
		||||
          uid: uid
 | 
			
		||||
        },
 | 
			
		||||
        sort: [{ createdAt: 'desc' }]
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      return result.docs.map(doc => docToMark(doc));
 | 
			
		||||
      if (this.supportsFindAPI()) {
 | 
			
		||||
        const result = await this.db.find({
 | 
			
		||||
          selector: {
 | 
			
		||||
            uid: uid
 | 
			
		||||
          },
 | 
			
		||||
          sort: [{ createdAt: 'desc' }]
 | 
			
		||||
        });
 | 
			
		||||
        return result.docs.map(doc => docToMark(doc));
 | 
			
		||||
      } else {
 | 
			
		||||
        // 回退方案:使用 allDocs 过滤
 | 
			
		||||
        return await this.fallbackFind((mark: Mark) => mark.uid === uid);
 | 
			
		||||
      }
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      console.error('按用户获取 Marks 失败:', error);
 | 
			
		||||
      throw error;
 | 
			
		||||
@@ -140,14 +165,18 @@ export class MarkDB {
 | 
			
		||||
  // 按类型获取 Marks
 | 
			
		||||
  async getByType(markType: MarkEnsureType): Promise<Mark[]> {
 | 
			
		||||
    try {
 | 
			
		||||
      const result = await this.db.find({
 | 
			
		||||
        selector: {
 | 
			
		||||
          markType: markType
 | 
			
		||||
        },
 | 
			
		||||
        sort: [{ createdAt: 'desc' }]
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      return result.docs.map(doc => docToMark(doc));
 | 
			
		||||
      if (this.supportsFindAPI()) {
 | 
			
		||||
        const result = await this.db.find({
 | 
			
		||||
          selector: {
 | 
			
		||||
            markType: markType
 | 
			
		||||
          },
 | 
			
		||||
          sort: [{ createdAt: 'desc' }]
 | 
			
		||||
        });
 | 
			
		||||
        return result.docs.map(doc => docToMark(doc));
 | 
			
		||||
      } else {
 | 
			
		||||
        // 回退方案:使用 allDocs 过滤
 | 
			
		||||
        return await this.fallbackFind((mark: Mark) => mark.markType === markType);
 | 
			
		||||
      }
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      console.error('按类型获取 Marks 失败:', error);
 | 
			
		||||
      throw error;
 | 
			
		||||
@@ -157,14 +186,20 @@ export class MarkDB {
 | 
			
		||||
  // 按标签搜索 Marks
 | 
			
		||||
  async getByTag(tag: string): Promise<Mark[]> {
 | 
			
		||||
    try {
 | 
			
		||||
      const result = await this.db.find({
 | 
			
		||||
        selector: {
 | 
			
		||||
          tags: { $elemMatch: { $eq: tag } }
 | 
			
		||||
        },
 | 
			
		||||
        sort: [{ createdAt: 'desc' }]
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      return result.docs.map(doc => docToMark(doc));
 | 
			
		||||
      if (this.supportsFindAPI()) {
 | 
			
		||||
        const result = await this.db.find({
 | 
			
		||||
          selector: {
 | 
			
		||||
            tags: { $elemMatch: { $eq: tag } }
 | 
			
		||||
          },
 | 
			
		||||
          sort: [{ createdAt: 'desc' }]
 | 
			
		||||
        });
 | 
			
		||||
        return result.docs.map(doc => docToMark(doc));
 | 
			
		||||
      } else {
 | 
			
		||||
        // 回退方案:使用 allDocs 过滤
 | 
			
		||||
        return await this.fallbackFind((mark: Mark) => 
 | 
			
		||||
          Boolean(mark.tags && mark.tags.includes(tag))
 | 
			
		||||
        );
 | 
			
		||||
      }
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      console.error('按标签获取 Marks 失败:', error);
 | 
			
		||||
      throw error;
 | 
			
		||||
@@ -174,18 +209,30 @@ export class MarkDB {
 | 
			
		||||
  // 搜索 Marks(按标题或描述)
 | 
			
		||||
  async search(query: string): Promise<Mark[]> {
 | 
			
		||||
    try {
 | 
			
		||||
      const result = await this.db.find({
 | 
			
		||||
        selector: {
 | 
			
		||||
          $or: [
 | 
			
		||||
            { title: { $regex: query, $options: 'i' } },
 | 
			
		||||
            { description: { $regex: query, $options: 'i' } },
 | 
			
		||||
            { summary: { $regex: query, $options: 'i' } }
 | 
			
		||||
          ]
 | 
			
		||||
        },
 | 
			
		||||
        sort: [{ createdAt: 'desc' }]
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      return result.docs.map(doc => docToMark(doc));
 | 
			
		||||
      if (this.supportsFindAPI()) {
 | 
			
		||||
        const result = await this.db.find({
 | 
			
		||||
          selector: {
 | 
			
		||||
            $or: [
 | 
			
		||||
              { title: { $regex: query, $options: 'i' } },
 | 
			
		||||
              { description: { $regex: query, $options: 'i' } },
 | 
			
		||||
              { summary: { $regex: query, $options: 'i' } }
 | 
			
		||||
            ]
 | 
			
		||||
          },
 | 
			
		||||
          sort: [{ createdAt: 'desc' }]
 | 
			
		||||
        });
 | 
			
		||||
        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) {
 | 
			
		||||
      console.error('搜索 Marks 失败:', error);
 | 
			
		||||
      throw error;
 | 
			
		||||
@@ -256,47 +303,91 @@ export class MarkDB {
 | 
			
		||||
    totalPages: number;
 | 
			
		||||
  }> {
 | 
			
		||||
    try {
 | 
			
		||||
      // 构建查询选择器
 | 
			
		||||
      let selector: any = {};
 | 
			
		||||
      if (this.supportsFindAPI()) {
 | 
			
		||||
        // 使用 find API
 | 
			
		||||
        let selector: any = {};
 | 
			
		||||
 | 
			
		||||
      if (filters?.uid) {
 | 
			
		||||
        selector.uid = filters.uid;
 | 
			
		||||
        if (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) {
 | 
			
		||||
      console.error('分页获取 Marks 失败:', error);
 | 
			
		||||
      throw error;
 | 
			
		||||
@@ -363,8 +454,9 @@ export class MarkDB {
 | 
			
		||||
  // 清理数据库
 | 
			
		||||
  async clear(): Promise<void> {
 | 
			
		||||
    try {
 | 
			
		||||
      const dbName = this.db.name;
 | 
			
		||||
      await this.db.destroy();
 | 
			
		||||
      this.db = createDB(this.db.name);
 | 
			
		||||
      this.db = createDB(dbName);
 | 
			
		||||
      await this.createIndexes();
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      console.error('清理数据库失败:', error);
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,5 @@
 | 
			
		||||
import { markDB, initMarkDB } from './db';
 | 
			
		||||
import { Mark } from './mark';
 | 
			
		||||
import { mockMarks } from '../base/mock/collection';
 | 
			
		||||
 | 
			
		||||
// Mark 服务类 - 提供业务逻辑层
 | 
			
		||||
export class MarkService {
 | 
			
		||||
@@ -79,24 +78,6 @@ export class MarkService {
 | 
			
		||||
    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[]> {
 | 
			
		||||
    return await this.getAllMarks();
 | 
			
		||||
@@ -116,6 +97,103 @@ export class MarkService {
 | 
			
		||||
    }
 | 
			
		||||
    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. 初始化服务
 | 
			
		||||
  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. 获取所有标记
 | 
			
		||||
  const allMarks = await markService.getAllMarks();
 | 
			
		||||
  console.log('所有标记数量:', allMarks.length);
 | 
			
		||||
@@ -169,18 +229,4 @@ export const exampleUsage = async () => {
 | 
			
		||||
  const stats = await markService.getStats();
 | 
			
		||||
  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