Browse Source

📦 新闻详情页

imengyu 6 days ago
parent
commit
e1f352b5e5

+ 234 - 1
package-lock.json

@@ -10,6 +10,7 @@
       "dependencies": {
         "@imengyu/js-request-transform": "^0.3.3",
         "@vuemap/vue-amap": "^2.1.12",
+        "ant-design-vue": "^4.2.6",
         "bootstrap": "^5.3.0",
         "mitt": "^3.0.1",
         "pinia": "^3.0.1",
@@ -45,6 +46,34 @@
         "node": ">=6.0.0"
       }
     },
+    "node_modules/@ant-design/colors": {
+      "version": "6.0.0",
+      "resolved": "https://registry.npmmirror.com/@ant-design/colors/-/colors-6.0.0.tgz",
+      "integrity": "sha512-qAZRvPzfdWHtfameEGP2Qvuf838NhergR35o+EuVyB5XvSA98xod5r4utvi4TJ3ywmevm290g9nsCG5MryrdWQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@ctrl/tinycolor": "^3.4.0"
+      }
+    },
+    "node_modules/@ant-design/icons-svg": {
+      "version": "4.4.2",
+      "resolved": "https://registry.npmmirror.com/@ant-design/icons-svg/-/icons-svg-4.4.2.tgz",
+      "integrity": "sha512-vHbT+zJEVzllwP+CM+ul7reTEfBR0vgxFe7+lREAsAA7YGsYpboiq2sQNeQeRvh09GfQgs/GyFEvZpJ9cLXpXA==",
+      "license": "MIT"
+    },
+    "node_modules/@ant-design/icons-vue": {
+      "version": "7.0.1",
+      "resolved": "https://registry.npmmirror.com/@ant-design/icons-vue/-/icons-vue-7.0.1.tgz",
+      "integrity": "sha512-eCqY2unfZK6Fe02AwFlDHLfoyEFreP6rBwAZMIJ1LugmfMiVgwWDYlp1YsRugaPtICYOabV1iWxXdP12u9U43Q==",
+      "license": "MIT",
+      "dependencies": {
+        "@ant-design/colors": "^6.0.0",
+        "@ant-design/icons-svg": "^4.2.1"
+      },
+      "peerDependencies": {
+        "vue": ">=3.0.3"
+      }
+    },
     "node_modules/@antfu/utils": {
       "version": "0.7.10",
       "resolved": "https://registry.npmmirror.com/@antfu/utils/-/utils-0.7.10.tgz",
@@ -509,6 +538,27 @@
         "node": ">=6.9.0"
       }
     },
+    "node_modules/@ctrl/tinycolor": {
+      "version": "3.6.1",
+      "resolved": "https://registry.npmmirror.com/@ctrl/tinycolor/-/tinycolor-3.6.1.tgz",
+      "integrity": "sha512-SITSV6aIXsuVNV3f3O0f2n/cgyEDWoSqtZMYiAmcsYHydcKrOz3gUxB/iXd/Qf08+IZX4KpgNbvUdMBmWz+kcA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/@emotion/hash": {
+      "version": "0.9.2",
+      "resolved": "https://registry.npmmirror.com/@emotion/hash/-/hash-0.9.2.tgz",
+      "integrity": "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==",
+      "license": "MIT"
+    },
+    "node_modules/@emotion/unitless": {
+      "version": "0.8.1",
+      "resolved": "https://registry.npmmirror.com/@emotion/unitless/-/unitless-0.8.1.tgz",
+      "integrity": "sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ==",
+      "license": "MIT"
+    },
     "node_modules/@esbuild/aix-ppc64": {
       "version": "0.25.3",
       "resolved": "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.25.3.tgz",
@@ -1650,6 +1700,16 @@
       "dev": true,
       "license": "MIT"
     },
+    "node_modules/@simonwep/pickr": {
+      "version": "1.8.2",
+      "resolved": "https://registry.npmmirror.com/@simonwep/pickr/-/pickr-1.8.2.tgz",
+      "integrity": "sha512-/l5w8BIkrpP6n1xsetx9MWPWlU6OblN5YgZZphxan0Tq4BByTCETL6lyIeY8lagalS2Nbt4F2W034KHLIiunKA==",
+      "license": "MIT",
+      "dependencies": {
+        "core-js": "^3.15.1",
+        "nanopop": "^2.1.0"
+      }
+    },
     "node_modules/@sindresorhus/merge-streams": {
       "version": "4.0.0",
       "resolved": "https://registry.npmmirror.com/@sindresorhus/merge-streams/-/merge-streams-4.0.0.tgz",
@@ -2145,6 +2205,58 @@
         "url": "https://github.com/chalk/ansi-styles?sponsor=1"
       }
     },
+    "node_modules/ant-design-vue": {
+      "version": "4.2.6",
+      "resolved": "https://registry.npmmirror.com/ant-design-vue/-/ant-design-vue-4.2.6.tgz",
+      "integrity": "sha512-t7eX13Yj3i9+i5g9lqFyYneoIb3OzTvQjq9Tts1i+eiOd3Eva/6GagxBSXM1fOCjqemIu0FYVE1ByZ/38epR3Q==",
+      "license": "MIT",
+      "dependencies": {
+        "@ant-design/colors": "^6.0.0",
+        "@ant-design/icons-vue": "^7.0.0",
+        "@babel/runtime": "^7.10.5",
+        "@ctrl/tinycolor": "^3.5.0",
+        "@emotion/hash": "^0.9.0",
+        "@emotion/unitless": "^0.8.0",
+        "@simonwep/pickr": "~1.8.0",
+        "array-tree-filter": "^2.1.0",
+        "async-validator": "^4.0.0",
+        "csstype": "^3.1.1",
+        "dayjs": "^1.10.5",
+        "dom-align": "^1.12.1",
+        "dom-scroll-into-view": "^2.0.0",
+        "lodash": "^4.17.21",
+        "lodash-es": "^4.17.15",
+        "resize-observer-polyfill": "^1.5.1",
+        "scroll-into-view-if-needed": "^2.2.25",
+        "shallow-equal": "^1.0.0",
+        "stylis": "^4.1.3",
+        "throttle-debounce": "^5.0.0",
+        "vue-types": "^3.0.0",
+        "warning": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=12.22.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/ant-design-vue"
+      },
+      "peerDependencies": {
+        "vue": ">=3.2.0"
+      }
+    },
+    "node_modules/array-tree-filter": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmmirror.com/array-tree-filter/-/array-tree-filter-2.1.0.tgz",
+      "integrity": "sha512-4ROwICNlNw/Hqa9v+rk5h22KjmzB1JGTMVKP2AKJBOCgb0yL0ASf0+YvCcLNNwquOHNX48jkeZIJ3a+oOQqKcw==",
+      "license": "MIT"
+    },
+    "node_modules/async-validator": {
+      "version": "4.2.5",
+      "resolved": "https://registry.npmmirror.com/async-validator/-/async-validator-4.2.5.tgz",
+      "integrity": "sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==",
+      "license": "MIT"
+    },
     "node_modules/balanced-match": {
       "version": "1.0.2",
       "resolved": "https://registry.npmmirror.com/balanced-match/-/balanced-match-1.0.2.tgz",
@@ -2296,6 +2408,12 @@
       "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
       "license": "MIT"
     },
+    "node_modules/compute-scroll-into-view": {
+      "version": "1.0.20",
+      "resolved": "https://registry.npmmirror.com/compute-scroll-into-view/-/compute-scroll-into-view-1.0.20.tgz",
+      "integrity": "sha512-UCB0ioiyj8CRjtrvaceBLqqhZCVP+1B8+NWQhmdsm0VXOJtobBCf1dBQmebCCo34qZmUwZfIH2MZLqNHazrfjg==",
+      "license": "MIT"
+    },
     "node_modules/convert-source-map": {
       "version": "2.0.0",
       "resolved": "https://registry.npmmirror.com/convert-source-map/-/convert-source-map-2.0.0.tgz",
@@ -2318,6 +2436,17 @@
         "url": "https://github.com/sponsors/mesqueeb"
       }
     },
+    "node_modules/core-js": {
+      "version": "3.42.0",
+      "resolved": "https://registry.npmmirror.com/core-js/-/core-js-3.42.0.tgz",
+      "integrity": "sha512-Sz4PP4ZA+Rq4II21qkNqOEDTDrCvcANId3xpIgB34NDkWc3UduWj2dqEtN9yZIq8Dk3HyPI33x9sqqU5C8sr0g==",
+      "hasInstallScript": true,
+      "license": "MIT",
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/core-js"
+      }
+    },
     "node_modules/cross-spawn": {
       "version": "7.0.6",
       "resolved": "https://registry.npmmirror.com/cross-spawn/-/cross-spawn-7.0.6.tgz",
@@ -2450,6 +2579,18 @@
         "node": ">=0.10"
       }
     },
+    "node_modules/dom-align": {
+      "version": "1.12.4",
+      "resolved": "https://registry.npmmirror.com/dom-align/-/dom-align-1.12.4.tgz",
+      "integrity": "sha512-R8LUSEay/68zE5c8/3BDxiTEvgb4xZTF0RKmAHfiEVN3klfIpXfi2/QCoiWPccVQ0J/ZGdz9OjzL4uJEP/MRAw==",
+      "license": "MIT"
+    },
+    "node_modules/dom-scroll-into-view": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmmirror.com/dom-scroll-into-view/-/dom-scroll-into-view-2.0.1.tgz",
+      "integrity": "sha512-bvVTQe1lfaUr1oFzZX80ce9KLDlZ3iU+XGNE/bz9HnGdklTieqsbmsLHe+rT2XWqopvL0PckkYqN7ksmm5pe3w==",
+      "license": "MIT"
+    },
     "node_modules/earcut": {
       "version": "2.2.4",
       "resolved": "https://registry.npmmirror.com/earcut/-/earcut-2.2.4.tgz",
@@ -2811,6 +2952,15 @@
         "url": "https://github.com/sponsors/sindresorhus"
       }
     },
+    "node_modules/is-plain-object": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmmirror.com/is-plain-object/-/is-plain-object-3.0.1.tgz",
+      "integrity": "sha512-Xnpx182SBMrr/aBik8y+GuR4U1L9FqMSojwDQwPMmxyC6bvEqly9UBCxhauBF5vNh2gwWJNX6oDV7O+OM4z34g==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
     "node_modules/is-stream": {
       "version": "4.0.1",
       "resolved": "https://registry.npmmirror.com/is-stream/-/is-stream-4.0.1.tgz",
@@ -2879,7 +3029,6 @@
       "version": "4.0.0",
       "resolved": "https://registry.npmmirror.com/js-tokens/-/js-tokens-4.0.0.tgz",
       "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
-      "dev": true,
       "license": "MIT"
     },
     "node_modules/jsesc": {
@@ -2938,12 +3087,30 @@
       "dev": true,
       "license": "MIT"
     },
+    "node_modules/lodash": {
+      "version": "4.17.21",
+      "resolved": "https://registry.npmmirror.com/lodash/-/lodash-4.17.21.tgz",
+      "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
+      "license": "MIT"
+    },
     "node_modules/lodash-es": {
       "version": "4.17.21",
       "resolved": "https://registry.npmmirror.com/lodash-es/-/lodash-es-4.17.21.tgz",
       "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==",
       "license": "MIT"
     },
+    "node_modules/loose-envify": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmmirror.com/loose-envify/-/loose-envify-1.4.0.tgz",
+      "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
+      "license": "MIT",
+      "dependencies": {
+        "js-tokens": "^3.0.0 || ^4.0.0"
+      },
+      "bin": {
+        "loose-envify": "cli.js"
+      }
+    },
     "node_modules/lru-cache": {
       "version": "5.1.1",
       "resolved": "https://registry.npmmirror.com/lru-cache/-/lru-cache-5.1.1.tgz",
@@ -3065,6 +3232,12 @@
         "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
       }
     },
+    "node_modules/nanopop": {
+      "version": "2.4.2",
+      "resolved": "https://registry.npmmirror.com/nanopop/-/nanopop-2.4.2.tgz",
+      "integrity": "sha512-NzOgmMQ+elxxHeIha+OG/Pv3Oc3p4RU2aBhwWwAqDpXrdTbtRylbRLQztLy8dMMwfl6pclznBdfUhccEn9ZIzw==",
+      "license": "MIT"
+    },
     "node_modules/node-addon-api": {
       "version": "7.1.1",
       "resolved": "https://registry.npmmirror.com/node-addon-api/-/node-addon-api-7.1.1.tgz",
@@ -3344,6 +3517,12 @@
         "url": "https://paulmillr.com/funding/"
       }
     },
+    "node_modules/resize-observer-polyfill": {
+      "version": "1.5.1",
+      "resolved": "https://registry.npmmirror.com/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz",
+      "integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==",
+      "license": "MIT"
+    },
     "node_modules/rfdc": {
       "version": "1.4.1",
       "resolved": "https://registry.npmmirror.com/rfdc/-/rfdc-1.4.1.tgz",
@@ -3430,6 +3609,15 @@
         "@parcel/watcher": "^2.4.1"
       }
     },
+    "node_modules/scroll-into-view-if-needed": {
+      "version": "2.2.31",
+      "resolved": "https://registry.npmmirror.com/scroll-into-view-if-needed/-/scroll-into-view-if-needed-2.2.31.tgz",
+      "integrity": "sha512-dGCXy99wZQivjmjIqihaBQNjryrz5rueJY7eHfTdyWEiR4ttYpsajb14rn9s5d4DY4EcY6+4+U/maARBXJedkA==",
+      "license": "MIT",
+      "dependencies": {
+        "compute-scroll-into-view": "^1.0.20"
+      }
+    },
     "node_modules/semver": {
       "version": "6.3.1",
       "resolved": "https://registry.npmmirror.com/semver/-/semver-6.3.1.tgz",
@@ -3440,6 +3628,12 @@
         "semver": "bin/semver.js"
       }
     },
+    "node_modules/shallow-equal": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmmirror.com/shallow-equal/-/shallow-equal-1.2.1.tgz",
+      "integrity": "sha512-S4vJDjHHMBaiZuT9NPb616CSmLf618jawtv3sufLl6ivK8WocjAo58cXwbRV1cgqxH0Qbv+iUt6m05eqEa2IRA==",
+      "license": "MIT"
+    },
     "node_modules/shebang-command": {
       "version": "2.0.0",
       "resolved": "https://registry.npmmirror.com/shebang-command/-/shebang-command-2.0.0.tgz",
@@ -3541,6 +3735,12 @@
         "url": "https://github.com/sponsors/sindresorhus"
       }
     },
+    "node_modules/stylis": {
+      "version": "4.3.6",
+      "resolved": "https://registry.npmmirror.com/stylis/-/stylis-4.3.6.tgz",
+      "integrity": "sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ==",
+      "license": "MIT"
+    },
     "node_modules/superjson": {
       "version": "2.2.2",
       "resolved": "https://registry.npmmirror.com/superjson/-/superjson-2.2.2.tgz",
@@ -3553,6 +3753,15 @@
         "node": ">=16"
       }
     },
+    "node_modules/throttle-debounce": {
+      "version": "5.0.2",
+      "resolved": "https://registry.npmmirror.com/throttle-debounce/-/throttle-debounce-5.0.2.tgz",
+      "integrity": "sha512-B71/4oyj61iNH0KeCamLuE2rmKuTO5byTOSVwECM5FA7TiAiAW+UqTKZ9ERueC4qvgSttUhdmq1mXC3kJqGX7A==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=12.22"
+      }
+    },
     "node_modules/tinyglobby": {
       "version": "0.2.13",
       "resolved": "https://registry.npmmirror.com/tinyglobby/-/tinyglobby-0.2.13.tgz",
@@ -3912,6 +4121,21 @@
         "typescript": ">=5.0.0"
       }
     },
+    "node_modules/vue-types": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmmirror.com/vue-types/-/vue-types-3.0.2.tgz",
+      "integrity": "sha512-IwUC0Aq2zwaXqy74h4WCvFCUtoV0iSWr0snWnE9TnU18S66GAQyqQbRf2qfJtUuiFsBf6qp0MEwdonlwznlcrw==",
+      "license": "MIT",
+      "dependencies": {
+        "is-plain-object": "3.0.1"
+      },
+      "engines": {
+        "node": ">=10.15.0"
+      },
+      "peerDependencies": {
+        "vue": "^3.0.0"
+      }
+    },
     "node_modules/vue3-carousel": {
       "version": "0.15.0",
       "resolved": "https://registry.npmmirror.com/vue3-carousel/-/vue3-carousel-0.15.0.tgz",
@@ -3921,6 +4145,15 @@
         "vue": "^3.5.0"
       }
     },
+    "node_modules/warning": {
+      "version": "4.0.3",
+      "resolved": "https://registry.npmmirror.com/warning/-/warning-4.0.3.tgz",
+      "integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==",
+      "license": "MIT",
+      "dependencies": {
+        "loose-envify": "^1.0.0"
+      }
+    },
     "node_modules/which": {
       "version": "5.0.0",
       "resolved": "https://registry.npmmirror.com/which/-/which-5.0.0.tgz",

+ 1 - 0
package.json

@@ -13,6 +13,7 @@
   "dependencies": {
     "@imengyu/js-request-transform": "^0.3.3",
     "@vuemap/vue-amap": "^2.1.12",
+    "ant-design-vue": "^4.2.6",
     "bootstrap": "^5.3.0",
     "mitt": "^3.0.1",
     "pinia": "^3.0.1",

BIN
src/assets/images/news/IconBack.png


+ 63 - 0
src/assets/scss/scroll.scss

@@ -0,0 +1,63 @@
+/* Common Scroll bar */
+
+/* PC Scrollbar */
+
+@mixin pc-hide-scrollbar(){
+  &::-webkit-scrollbar {
+      width: 5px;
+      height: 5px;
+  }
+  &::-webkit-scrollbar-thumb {
+      background: transparent;
+
+      &:hover {
+        background: transparent;
+      }
+  }
+  &::-webkit-scrollbar-track {
+      background: transparent;
+  }
+}
+
+@mixin pc-fix-scrollbar(){
+    &::-webkit-scrollbar {
+        width: 5px;
+        height: 5px;
+    }
+    &::-webkit-scrollbar-thumb {
+        background: #707070;
+        border-radius: 3px;
+
+        &:hover {
+            background: #e0e0e0;
+        }
+    }
+    &::-webkit-scrollbar-track {
+        background: transparent;
+    }
+}
+
+@mixin pc-fix-scrollbar-white(){
+    &::-webkit-scrollbar {
+        width: 5px;
+        height: 5px;
+    }
+    &::-webkit-scrollbar-thumb {
+        background: #d6d6d6;
+        opacity: .7;
+        border-radius: 3px;
+
+        &:hover {
+            background: #707070;
+        }
+    }
+    &::-webkit-scrollbar-track {
+        background: transparent;
+    }
+}
+
+.vertical-scroll {
+  overflow-y: scroll;
+
+  @include pc-fix-scrollbar-white();
+}

+ 142 - 0
src/components/content/CommonCatalog.vue

@@ -0,0 +1,142 @@
+<script setup lang="ts">
+import SimpleScrollView from '../display/SimpleScrollView.vue';
+import { ref, watch, type PropType } from 'vue';
+
+export interface CatalogItem {
+  title: string,
+  level: number,
+  scrollPos: number, 
+  anchor: string,
+}
+
+const emit = defineEmits([	
+  "goToItem"	
+])
+const props = defineProps({	
+  items: {
+    type: Object as PropType<CatalogItem[]>,
+    default: () => []
+  },
+  scrollContainer: {
+    type: Object as PropType<HTMLElement|null>,
+    default: () => null,
+  },
+})
+
+const activeIndex = ref(-1);
+
+function handlerContainerScroll(e: Event) {
+  const container = e.target as HTMLElement;
+  const scrollTop = container.scrollTop;
+
+  activeIndex.value = 0;
+  for (let i = props.items.length - 1; i >= 0; i--) {
+    const item = props.items[i];
+    if (scrollTop >= item.scrollPos) {
+      activeIndex.value = i;
+      break;
+    }
+  }
+}
+function handlerItemClick(item: CatalogItem) {
+  if (item.anchor) {
+    const el = document.getElementById(item.anchor);
+    if (el) {
+      el.scrollIntoView({ behavior: 'smooth' });
+    }
+  }
+  emit('goToItem', item);
+}
+
+watch(() => props.scrollContainer, (newVal, oldVal) => {
+  if (oldVal && oldVal instanceof HTMLElement)
+    oldVal.removeEventListener('scroll', handlerContainerScroll);
+  if (newVal && newVal instanceof HTMLElement)
+    newVal.addEventListener('scroll', handlerContainerScroll);
+}, { immediate: true });
+
+</script>
+
+<template>
+  <SimpleScrollView class="nana-catalog" :scrollY="true">
+    <div>
+      <div 
+        v-for="(item, index) in props.items"
+        :key="index"
+        :class="[
+          'nana-catalog-item',
+          `level-${item.level}`,
+          activeIndex === index ? 'active' : '',
+        ]"
+        @click="handlerItemClick(item)"
+      >
+        {{ item.title }}
+      </div>
+    </div>
+  </SimpleScrollView>
+</template>
+
+<style lang="scss">
+.nana-catalog {
+  position: relative; 
+  margin-left: 0.5rem;
+
+  > div {
+    display: flex;
+    flex-direction: column;
+  }
+
+  &::before {
+    content: '';
+    position: absolute;
+    top: 0;
+    bottom: 0;
+    left: 0;
+    width: 1px;
+    background-color: var(--nana-text-6);
+  }
+
+  .nana-catalog-item {
+    position: relative;
+    padding: 0.4rem 0.8rem;
+    font-size: 1rem;
+    color: var(--nana-text-6);
+    user-select: none;
+    cursor: pointer;
+
+    &.active {
+      font-weight: bold; 
+      color: var(--nana-text-1);
+
+      &::after {
+        content: '';
+        position: absolute;
+        top: calc(50% - 6px);
+        left: 0;
+        border: 8px solid transparent;
+        border-left: 8px solid var(--nana-text-1);
+      }
+    }
+    &.level-1 {
+      font-size: 1.2rem;
+      padding-left: 1rem;
+    }
+    &.level-3,
+    &.level-4,
+    &.level-5 {
+      font-size: 0.8rem;
+      padding-left: 1.2rem;
+      
+      &::before {
+        content: '·';
+        display: inline-block;
+        padding-right: 0.6rem;
+      }
+    }
+    &.level-6 {
+      font-size: 0.7rem;
+      padding-left: 1.6rem;
+    }
+  }
+}
+</style>

+ 37 - 0
src/components/display/SimpleRemoveRichHtml.vue

@@ -0,0 +1,37 @@
+<template>
+  <div ref="solveHtmlDiv" class="nana-no-rich-html" v-html="content" />
+  {{ pureContent }}
+</template>
+
+<script setup lang="ts">
+import { ref, watch } from 'vue';
+
+const props = defineProps({	
+  content: {
+    type: String,
+    default: '',
+  },
+})
+
+const solveHtmlDiv = ref<HTMLElement|null>(null);
+const pureContent = ref('');
+
+watch(() => props.content, () => {
+  setTimeout(() => {
+    if (solveHtmlDiv.value) {
+      pureContent.value = solveHtmlDiv.value.textContent || '';
+    }
+  }, 200);
+}, { immediate: true })
+
+</script>
+
+<style lang="scss">
+.nana-no-rich-html {
+  position: absolute;
+  opacity: 0;
+  left: -1000px;
+  pointer-events: none;
+}
+
+</style>

+ 134 - 0
src/components/display/SimpleRichHtml.vue

@@ -0,0 +1,134 @@
+<template>
+  <div ref="scrollContainer" class="nana-rich-html-container">
+    <div class="rich-html">
+      <slot name="prepend" />
+      <template 
+        v-for="(content, i) in contents"
+        :key="i"
+      >
+        <div 
+          v-if="content"
+          :data-r-id="id"
+          class="content"
+          v-html="content"
+        />
+      </template>
+      <slot name="append" />
+    </div>
+    <div class="rich-html-catalog" v-if="catalog && catalogItems.length > 0">
+      <CommonCatalog
+        :items="catalogItems"
+        :scrollContainer="scrollContainer"
+      />
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import CommonUtils from '@/common/utils/CommonUtils';
+import CommonCatalog, { type CatalogItem } from '../content/CommonCatalog.vue';
+import { onBeforeUnmount, onMounted, ref, watch, type PropType } from 'vue';
+
+const props = defineProps({	
+  contents: {
+    type: Array as PropType<string[]>,
+    default: () => ([]),
+  },
+  tagStyle: {
+    type: Object as PropType<Record<string, string>>,
+    default: () => ({}),
+  },
+  catalog: {
+    type: Boolean,
+    default: true,
+  },
+  noStyle: {
+    type: Boolean,
+    default: false,
+  },
+})
+
+const id = CommonUtils.genNonDuplicateIDHEX(12);
+const catalogItems = ref<CatalogItem[]>([]);
+const scrollContainer = ref<HTMLElement|null>(null);
+let lastStyleTag : HTMLElement|null = null;
+
+function genTagCss() {
+  if (Object.keys(props.tagStyle).length > 0) {
+    const style = document.createElement('style');
+    let css = '';
+    for (const key in props.tagStyle) {
+      css += `.rich-html div[data-r-id="${id}"] ${key} { ${props.tagStyle[key]} } `
+    }
+    style.innerHTML = css;
+    document.body.appendChild(style);
+    lastStyleTag = style;
+  }
+}
+function generateCatalog() {
+  catalogItems.value = [];
+
+  if (!props.catalog) 
+    return;
+
+  let anchrId = 0;
+  for (let i = 1; i <= 6; i++) {
+    const heades = document.querySelectorAll(`.rich-html div[data-r-id="${id}"] h${i}`);
+    for (const header of heades) {
+      anchrId++;
+      if (header instanceof HTMLHeadingElement) {
+        if (header.id == '')
+          header.id = 'header' + anchrId + 'a' + CommonUtils.genNonDuplicateIDHEX(12);
+        catalogItems.value.push({
+          title: header.textContent || '',
+          scrollPos: header.offsetTop,
+          level: i,
+          anchor: header.id,
+        });
+      }
+    }
+  }
+  catalogItems.value.sort((a, b) => {
+    return a.scrollPos - b.scrollPos;
+  })
+}
+
+watch(() => props.contents, () => {
+  setTimeout(() => {
+    generateCatalog();
+  }, 200);
+}, { immediate: true })
+
+onBeforeUnmount(() => {
+  if (lastStyleTag) {
+    lastStyleTag.parentElement?.removeChild(lastStyleTag);
+    lastStyleTag = null;
+  }
+})
+onMounted(() => {
+  genTagCss()
+});
+</script>
+
+<style lang="scss">
+@use "@/assets/scss/Scroll" as *;
+
+.nana-rich-html-container {
+  position: relative;
+  display: flex;
+  flex-direction: row;
+  overflow-y: scroll;
+
+  @include pc-fix-scrollbar();
+
+  .rich-html {
+    flex: 1 1 100%;
+  }
+  .rich-html-catalog {
+    position: sticky;
+    top: 0px;
+    width: 15rem;
+  }
+}
+
+</style>

+ 57 - 0
src/components/display/SimpleScrollView.vue

@@ -0,0 +1,57 @@
+<template>
+  <div 
+    :class="[
+      'nana-scroll-view',
+      scrollX ? 'x' : '',
+      scrollY ? 'y' : ''
+    ]"
+  >
+    <slot />
+  </div>
+</template>
+
+<script lang="ts" setup>
+/**
+ * 组件说明:可滚动的容器。
+ */
+const props = defineProps({	
+  scrollX: {
+    type: Boolean,
+    default: false
+  },
+  scrollY: {
+    type: Boolean,
+    default: false 
+  }
+})
+</script>
+
+<style lang="scss" scoped>
+.nana-scroll-view {
+  overflow: hidden;
+  
+  &::-webkit-scrollbar {
+    width: 5px;
+    height: 5px;
+  }
+  &::-webkit-scrollbar-thumb {
+    background: #d6d6d6;
+    opacity: .7;
+    border-radius: 3px;
+
+    &:hover {
+      background: #707070;
+    }
+  }
+  &::-webkit-scrollbar-track {
+    background: transparent;
+  }
+
+  &.x {
+    overflow-x: scroll; 
+  }
+  &.y {
+    overflow-y: scroll; 
+  }
+}
+</style>

+ 5 - 0
src/router/index.ts

@@ -24,6 +24,11 @@ const router = createRouter({
       component: () => import('../views/NewsView.vue'),
     },
     {
+      path: '/news/detail',
+      name: 'newsdetail',
+      component: () => import('../views/NewsDetailView.vue'),
+    },
+    {
       path: '/introduction',
       name: 'introduction',
       component: () => import('../views/IntrodView.vue'),

File diff suppressed because it is too large
+ 78 - 193
src/views/NewsDetailView.vue