Hexinkui před 3 týdny
rodič
revize
26d5fd0ef9

+ 472 - 1
package-lock.json

@@ -27,7 +27,9 @@
         "babel-plugin-import": "^1.13.8",
         "less": "^4.0.0",
         "less-loader": "^8.0.0",
-        "postcss-pxtorem": "^6.1.0"
+        "postcss-pxtorem": "^6.1.0",
+        "sass": "^1.94.2",
+        "sass-loader": "^10.5.2"
       }
     },
     "node_modules/@achrinza/node-ipc": {
@@ -374,6 +376,316 @@
         "node": ">= 8"
       }
     },
+    "node_modules/@parcel/watcher": {
+      "version": "2.5.1",
+      "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.1.tgz",
+      "integrity": "sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==",
+      "dev": true,
+      "hasInstallScript": true,
+      "license": "MIT",
+      "optional": true,
+      "dependencies": {
+        "detect-libc": "^1.0.3",
+        "is-glob": "^4.0.3",
+        "micromatch": "^4.0.5",
+        "node-addon-api": "^7.0.0"
+      },
+      "engines": {
+        "node": ">= 10.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      },
+      "optionalDependencies": {
+        "@parcel/watcher-android-arm64": "2.5.1",
+        "@parcel/watcher-darwin-arm64": "2.5.1",
+        "@parcel/watcher-darwin-x64": "2.5.1",
+        "@parcel/watcher-freebsd-x64": "2.5.1",
+        "@parcel/watcher-linux-arm-glibc": "2.5.1",
+        "@parcel/watcher-linux-arm-musl": "2.5.1",
+        "@parcel/watcher-linux-arm64-glibc": "2.5.1",
+        "@parcel/watcher-linux-arm64-musl": "2.5.1",
+        "@parcel/watcher-linux-x64-glibc": "2.5.1",
+        "@parcel/watcher-linux-x64-musl": "2.5.1",
+        "@parcel/watcher-win32-arm64": "2.5.1",
+        "@parcel/watcher-win32-ia32": "2.5.1",
+        "@parcel/watcher-win32-x64": "2.5.1"
+      }
+    },
+    "node_modules/@parcel/watcher-android-arm64": {
+      "version": "2.5.1",
+      "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.1.tgz",
+      "integrity": "sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": ">= 10.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      }
+    },
+    "node_modules/@parcel/watcher-darwin-arm64": {
+      "version": "2.5.1",
+      "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.1.tgz",
+      "integrity": "sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": ">= 10.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      }
+    },
+    "node_modules/@parcel/watcher-darwin-x64": {
+      "version": "2.5.1",
+      "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.1.tgz",
+      "integrity": "sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": ">= 10.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      }
+    },
+    "node_modules/@parcel/watcher-freebsd-x64": {
+      "version": "2.5.1",
+      "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.1.tgz",
+      "integrity": "sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "freebsd"
+      ],
+      "engines": {
+        "node": ">= 10.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      }
+    },
+    "node_modules/@parcel/watcher-linux-arm-glibc": {
+      "version": "2.5.1",
+      "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.1.tgz",
+      "integrity": "sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">= 10.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      }
+    },
+    "node_modules/@parcel/watcher-linux-arm-musl": {
+      "version": "2.5.1",
+      "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.1.tgz",
+      "integrity": "sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">= 10.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      }
+    },
+    "node_modules/@parcel/watcher-linux-arm64-glibc": {
+      "version": "2.5.1",
+      "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.1.tgz",
+      "integrity": "sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">= 10.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      }
+    },
+    "node_modules/@parcel/watcher-linux-arm64-musl": {
+      "version": "2.5.1",
+      "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.1.tgz",
+      "integrity": "sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">= 10.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      }
+    },
+    "node_modules/@parcel/watcher-linux-x64-glibc": {
+      "version": "2.5.1",
+      "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.1.tgz",
+      "integrity": "sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">= 10.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      }
+    },
+    "node_modules/@parcel/watcher-linux-x64-musl": {
+      "version": "2.5.1",
+      "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.1.tgz",
+      "integrity": "sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">= 10.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      }
+    },
+    "node_modules/@parcel/watcher-win32-arm64": {
+      "version": "2.5.1",
+      "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.1.tgz",
+      "integrity": "sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">= 10.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      }
+    },
+    "node_modules/@parcel/watcher-win32-ia32": {
+      "version": "2.5.1",
+      "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.1.tgz",
+      "integrity": "sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ==",
+      "cpu": [
+        "ia32"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">= 10.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      }
+    },
+    "node_modules/@parcel/watcher-win32-x64": {
+      "version": "2.5.1",
+      "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.1.tgz",
+      "integrity": "sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">= 10.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      }
+    },
     "node_modules/@polka/url": {
       "version": "1.0.0-next.29",
       "resolved": "https://registry.npmmirror.com/@polka/url/-/url-1.0.0-next.29.tgz",
@@ -2834,6 +3146,20 @@
         "npm": "1.2.8000 || >= 1.4.16"
       }
     },
+    "node_modules/detect-libc": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz",
+      "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "optional": true,
+      "bin": {
+        "detect-libc": "bin/detect-libc.js"
+      },
+      "engines": {
+        "node": ">=0.10"
+      }
+    },
     "node_modules/detect-node": {
       "version": "2.1.0",
       "resolved": "https://registry.npmmirror.com/detect-node/-/detect-node-2.1.0.tgz",
@@ -4101,6 +4427,13 @@
         "node": ">=0.10.0"
       }
     },
+    "node_modules/immutable": {
+      "version": "5.1.4",
+      "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.4.tgz",
+      "integrity": "sha512-p6u1bG3YSnINT5RQmx/yRZBpenIl30kVxkTLDyHLIMk0gict704Q9n+thfDI7lTRm9vXdDYutVzXhzcThxTnXA==",
+      "dev": true,
+      "license": "MIT"
+    },
     "node_modules/import-fresh": {
       "version": "3.3.1",
       "resolved": "https://registry.npmmirror.com/import-fresh/-/import-fresh-3.3.1.tgz",
@@ -5192,6 +5525,14 @@
         "tslib": "^2.0.3"
       }
     },
+    "node_modules/node-addon-api": {
+      "version": "7.1.1",
+      "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz",
+      "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==",
+      "dev": true,
+      "license": "MIT",
+      "optional": true
+    },
     "node_modules/node-fetch": {
       "version": "2.7.0",
       "resolved": "https://registry.npmmirror.com/node-fetch/-/node-fetch-2.7.0.tgz",
@@ -6733,6 +7074,136 @@
       "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
       "dev": true
     },
+    "node_modules/sass": {
+      "version": "1.94.2",
+      "resolved": "https://registry.npmjs.org/sass/-/sass-1.94.2.tgz",
+      "integrity": "sha512-N+7WK20/wOr7CzA2snJcUSSNTCzeCGUTFY3OgeQP3mZ1aj9NMQ0mSTXwlrnd89j33zzQJGqIN52GIOmYrfq46A==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "chokidar": "^4.0.0",
+        "immutable": "^5.0.2",
+        "source-map-js": ">=0.6.2 <2.0.0"
+      },
+      "bin": {
+        "sass": "sass.js"
+      },
+      "engines": {
+        "node": ">=14.0.0"
+      },
+      "optionalDependencies": {
+        "@parcel/watcher": "^2.4.1"
+      }
+    },
+    "node_modules/sass-loader": {
+      "version": "10.5.2",
+      "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-10.5.2.tgz",
+      "integrity": "sha512-vMUoSNOUKJILHpcNCCyD23X34gve1TS7Rjd9uXHeKqhvBG39x6XbswFDtpbTElj6XdMFezoWhkh5vtKudf2cgQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "klona": "^2.0.4",
+        "loader-utils": "^2.0.0",
+        "neo-async": "^2.6.2",
+        "schema-utils": "^3.0.0",
+        "semver": "^7.3.2"
+      },
+      "engines": {
+        "node": ">= 10.13.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/webpack"
+      },
+      "peerDependencies": {
+        "fibers": ">= 3.1.0",
+        "node-sass": "^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 || ^9.0.0",
+        "sass": "^1.3.0",
+        "webpack": "^4.36.0 || ^5.0.0"
+      },
+      "peerDependenciesMeta": {
+        "fibers": {
+          "optional": true
+        },
+        "node-sass": {
+          "optional": true
+        },
+        "sass": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/sass-loader/node_modules/json5": {
+      "version": "2.2.3",
+      "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
+      "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
+      "dev": true,
+      "license": "MIT",
+      "bin": {
+        "json5": "lib/cli.js"
+      },
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/sass-loader/node_modules/loader-utils": {
+      "version": "2.0.4",
+      "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz",
+      "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "big.js": "^5.2.2",
+        "emojis-list": "^3.0.0",
+        "json5": "^2.1.2"
+      },
+      "engines": {
+        "node": ">=8.9.0"
+      }
+    },
+    "node_modules/sass-loader/node_modules/semver": {
+      "version": "7.7.3",
+      "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
+      "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
+      "dev": true,
+      "license": "ISC",
+      "bin": {
+        "semver": "bin/semver.js"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/sass/node_modules/chokidar": {
+      "version": "4.0.3",
+      "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
+      "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "readdirp": "^4.0.1"
+      },
+      "engines": {
+        "node": ">= 14.16.0"
+      },
+      "funding": {
+        "url": "https://paulmillr.com/funding/"
+      }
+    },
+    "node_modules/sass/node_modules/readdirp": {
+      "version": "4.1.2",
+      "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
+      "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 14.18.0"
+      },
+      "funding": {
+        "type": "individual",
+        "url": "https://paulmillr.com/funding/"
+      }
+    },
     "node_modules/sax": {
       "version": "1.4.3",
       "resolved": "https://registry.npmmirror.com/sax/-/sax-1.4.3.tgz",

+ 3 - 1
package.json

@@ -31,7 +31,9 @@
     "babel-plugin-import": "^1.13.8",
     "less": "^4.0.0",
     "less-loader": "^8.0.0",
-    "postcss-pxtorem": "^6.1.0"
+    "postcss-pxtorem": "^6.1.0",
+    "sass": "^1.94.2",
+    "sass-loader": "^10.5.2"
   },
   "browserslist": [
     "> 1%",

+ 2 - 1
src/api/index.js

@@ -24,7 +24,8 @@ export function GetCandlestickChart(id) {
     method: 'get',
     params: {
         interval: id.period,
-        limit: 150
+        limit: 150,
+        end_time: id.to || undefined,
     }
   })
 }

+ 1 - 1
src/utils/request.js

@@ -67,7 +67,7 @@ service.interceptors.response.use(
     console.log('网络报错' + error) // for debug
     let message = error.message
     
-    if (message == 'Network Error') {
+    if (message === 'Network Error') {
       message = '后端接口连接异常'
     } else if (message.includes('timeout')) {
       message = '系统接口请求超时'

Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 7 - 1
src/views/bitcoin/TradeFutures.vue


Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 14 - 0
src/views/bitcoin/lever/CryptocurrencyTrading.vue


+ 66 - 42
src/views/bitcoin/lever/components/KLineChart.vue

@@ -8,6 +8,8 @@
 import { onMounted, onBeforeUnmount, ref, watch, nextTick, toRaw, defineExpose } from 'vue'
 import * as klinecharts from 'klinecharts'
 
+const emit = defineEmits(['load-more'])
+
 const props = defineProps({
   data: { type: Array, default: () => [] },
   height: { type: String, default: '100%' },
@@ -17,7 +19,7 @@ const props = defineProps({
     default: () => ({
       up: '#2EBD85',
       down: '#F6465D',
-      grid: '#F2F4F6', // 浅色网格
+      grid: '#F2F4F6',
       text: '#929AA5',
       targetLine: '#4A6EF5'
     })
@@ -33,7 +35,6 @@ const initChart = () => {
 
   chartInstance = klinecharts.init(chartContainer.value)
 
-  // 1. 设置精度
   const { price, volume } = props.precision
   if (chartInstance.setPriceVolumePrecision) {
     chartInstance.setPriceVolumePrecision(price, volume)
@@ -41,7 +42,6 @@ const initChart = () => {
     chartInstance.setPrecision(price, volume)
   }
 
-  // 2. 样式配置
   const { up, down, grid, text } = props.colors
   chartInstance.setStyleOptions({
     grid: { show: true, horizontal: { show: true, size: 1, color: grid, style: 'dash', dashValue: [5, 5] }, vertical: { show: false } },
@@ -54,41 +54,36 @@ const initChart = () => {
     yAxis: { inside: true, axisLine: { show: false }, tickLine: { show: false }, tickText: { color: text, size: 10, paddingLeft: 4 } },
   })
 
-  // 3. 默认指标: 主图 MA
-  chartInstance.createTechnicalIndicator('MA', false, { id: 'candle_pane' })
+  // 监听加载更多
+  chartInstance.loadMore(timestamp => {
+    emit('load-more', timestamp)
+  })
 
-  // 4. 默认两个副图 (初始就应用权重逻辑)
+  // 初始化指标
+  chartInstance.createTechnicalIndicator('MA', false, { id: 'candle_pane' })
   chartInstance.createTechnicalIndicator('VOL', false, { id: 'pane_1', dragEnabled: false })
   chartInstance.createTechnicalIndicator('MACD', false, { id: 'pane_2', dragEnabled: false })
 
-  // 🔥 关键初始化:强制分配权重
-  // 主图权重 8,副图权重 1
   chartInstance.setPaneOptions({ id: 'candle_pane', heightRatio: 8 })
   chartInstance.setPaneOptions({ id: 'pane_1', heightRatio: 1 })
   chartInstance.setPaneOptions({ id: 'pane_2', heightRatio: 1 })
 
-  // 5. 加载数据
   if (props.data && props.data.length > 0) {
     chartInstance.applyNewData(toRaw(props.data))
   }
 }
 
-// --- 设置主图指标 ---
 const setMainIndicator = (name) => {
   if (!chartInstance) return
   const mains = ['MA', 'EMA', 'BOLL', 'SAR', 'AVL', 'TRIX']
   mains.forEach(m => chartInstance.removeTechnicalIndicator('candle_pane', m))
-
   if (name && name !== 'Hide') {
     chartInstance.createTechnicalIndicator(name, false, { id: 'candle_pane' })
   }
 }
 
-// --- 🟢 核心修复:基于权重的动态高度控制 ---
 const setSubIndicators = (names) => {
   if (!chartInstance) return
-
-  // 1. 彻底清除旧指标
   const paneIds = ['pane_1', 'pane_2', 'pane_3', 'pane_4', 'pane_5']
   const allSubs = ['VOL', 'MACD', 'KDJ', 'RSI', 'WR', 'CCI', 'OBV', 'AO', 'DMA', 'TRIX', 'BRAR', 'VR']
 
@@ -97,54 +92,84 @@ const setSubIndicators = (names) => {
   })
 
   const validNames = names.filter(n => n && n !== 'Hide')
-
-  // 2. 重新创建并分配权重
-  // 核心思路:主图(candle_pane)永远设为 8,每个副图设为 1
-  // 这样无论加多少个副图,主图都会占据绝大部分空间
-
-  // 步骤A: 必须先显式重置主图权重,确保它是“老大”
-  chartInstance.setPaneOptions({
-    id: 'candle_pane',
-    heightRatio: 8
-  })
+  chartInstance.setPaneOptions({ id: 'candle_pane', heightRatio: 8 })
 
   validNames.forEach((name, index) => {
     const paneId = `pane_${index + 1}`
+    chartInstance.createTechnicalIndicator(name, false, { id: paneId, dragEnabled: false })
+    chartInstance.setPaneOptions({ id: paneId, heightRatio: 1 })
+  })
+}
 
-    // 步骤B: 创建指标
-    chartInstance.createTechnicalIndicator(name, false, {
-      id: paneId,
-      dragEnabled: false
-    })
+// ---------------------------------------------------------
+// 核心修复区域:applyHistoryData
+// ---------------------------------------------------------
+const applyHistoryData = (dataList, hasMore) => {
+  if (!chartInstance) return
 
-    // 步骤C: 强制给副图分配“小弟”权重 (1)
-    chartInstance.setPaneOptions({
-      id: paneId,
-      heightRatio: 1
-    })
-  })
+  // 1. 获取当前图表里【最左侧/最早】的一条数据
+  const currentDataList = chartInstance.getDataList()
+  const firstCurrentParams = currentDataList[0]
+
+  // 2. 只有当图表已有数据,且新加载的数据不为空时,才进行去重检测
+  if (firstCurrentParams && dataList.length > 0) {
+    // 获取新历史数据的【最后一条】(因为传入的 dataList 是按时间升序排列的)
+    // 这条数据是时间最晚的,最容易和 currentDataList[0] 重叠
+    const lastHistoryParams = dataList[dataList.length - 1]
+
+    // 3. 如果时间戳完全相等,说明重叠了
+    if (lastHistoryParams.timestamp === firstCurrentParams.timestamp) {
+      // 移除新加载数据中的最后一条 (保留图表中已有的那条)
+      // 原因:通常图表里的数据可能已经被 WS 更新过,或者为了避免 UI 闪烁,不替换边界点
+      dataList.pop()
+    }
+  }
+
+  // 4. 将处理后的数据拼接到左侧
+  chartInstance.applyMoreData(dataList, hasMore)
 }
 
 defineExpose({
   setMainIndicator,
-  setSubIndicators
+  setSubIndicators,
+  applyHistoryData
 })
 
-// --- 数据监听 ---
 watch(() => props.data, (newData) => {
   if (!chartInstance) return
   const rawData = toRaw(newData)
   const currentList = chartInstance.getDataList()
 
-  if (rawData.length === 0) { chartInstance.clearData(); return }
-  if (currentList.length === 0) { chartInstance.applyNewData(rawData); return }
+  if (rawData.length === 0) {
+    chartInstance.clearData();
+    return
+  }
+
+  if (currentList.length === 0) {
+    chartInstance.applyNewData(rawData);
+    return
+  }
 
   const firstOld = currentList[0]
   const firstNew = rawData[0]
-  if (firstOld.timestamp !== firstNew.timestamp) {
-    chartInstance.applyNewData(rawData)
+  const lastOld = currentList[currentList.length - 1]
+  const lastNew = rawData[rawData.length - 1]
+
+  // 防止历史数据回弹 (Safety check)
+  if (firstNew.timestamp < firstOld.timestamp) {
     return
   }
+
+  // 切换 Symbol 或周期时的全量更新检测
+  if (lastNew.timestamp !== lastOld.timestamp && rawData.length > 1) {
+     const isUpdate = lastNew.timestamp > lastOld.timestamp && rawData.length === currentList.length + 1;
+     if (!isUpdate) {
+        chartInstance.applyNewData(rawData)
+        return
+     }
+  }
+
+  // WS 实时更新
   if (rawData.length > 0) {
     const lastData = rawData[rawData.length - 1]
     chartInstance.updateData(lastData)
@@ -177,7 +202,6 @@ const handleResize = () => {
 </script>
 
 <style scoped>
-/* 亮色背景配置 */
 .kline-wrapper { width: 100%; position: relative; background: #ffffff; }
 .kline-chart { width: 100%; height: 100%; }
 </style>

+ 452 - 0
src/views/bitcoin/lever/components/UserAssetsPanel.vue

@@ -0,0 +1,452 @@
+<template>
+  <div class="panel-container">
+    <div class="tabs-header">
+      <div class="tabs-left">
+        <div
+          class="tab-item"
+          :class="{ active: activeTab === 'assets' }"
+          @click="activeTab = 'assets'"
+        >
+          币种资产
+        </div>
+        <div
+          class="tab-item"
+          :class="{ active: activeTab === 'positions' }"
+          @click="activeTab = 'positions'"
+        >
+          持有仓位({{ positionsList.length }})
+        </div>
+        <div
+          class="tab-item"
+          :class="{ active: activeTab === 'orders' }"
+          @click="activeTab = 'orders'"
+        >
+          当前委托({{ ordersList.length }})
+        </div>
+      </div>
+      <div class="tabs-right" @click="router.push('/historyIndex')">
+        <span class="history-icon">
+          <svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
+          <path d="M17.025 10.125L15.5254 8.625L14.025 10.125M15.75 9C15.75 12.7279 12.7279 15.75 9 15.75C5.27208 15.75 2.25 12.7279 2.25 9C2.25 5.27208 5.27208 2.25 9 2.25C11.4764 2.25 13.6414 3.5836 14.8159 5.57182M9 5.25V9L11.25 10.5" stroke="#A8A8A8" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+          </svg>
+        </span> 全部
+      </div>
+    </div>
+
+    <div class="panel-content">
+
+      <div v-if="activeTab === 'assets'" class="list-container">
+        <div v-for="(item, index) in assetsList" :key="index" class="card asset-card">
+          <div class="card-header">
+            <div class="coin-info">
+              <div class="coin-icon icon-btc">₿</div>
+              <div class="coin-name">
+                <span class="symbol">{{ item.symbol }}</span>
+                <span class="name-cn">{{ item.nameCn }}</span>
+              </div>
+            </div>
+            <div class="asset-total">
+              <div class="main-val">{{ item.totalValue }}</div>
+              <div class="sub-val">≈ ¥ {{ item.cnyValue }}</div>
+            </div>
+          </div>
+          <div class="divider"></div>
+          <div class="card-row">
+            <span class="label">可用</span>
+            <span class="value">{{ item.available }}</span>
+          </div>
+          <div class="card-row">
+            <span class="label">冻结</span>
+            <span class="value red-text">{{ item.frozen }}</span>
+          </div>
+        </div>
+      </div>
+
+      <div v-else-if="activeTab === 'positions'" class="list-container">
+        <div v-for="(item, index) in positionsList" :key="index" class="card position-card">
+          <div class="card-header">
+            <div class="coin-info">
+              <div class="coin-icon icon-btc">₿</div>
+              <div class="coin-name-block">
+                <span class="symbol">{{ item.pair }}</span>
+                <div class="tags-row">
+                  <span class="tag" :class="item.type === 'buy' ? 'tag-green' : 'tag-red'">
+                    {{ item.type === 'buy' ? '买入' : '卖出' }}
+                  </span>
+                  <span class="tag tag-grey">{{ item.leverage }}X</span>
+                  <span class="tag tag-grey">市价</span>
+                </div>
+              </div>
+            </div>
+            <div class="header-right-time">
+              {{ item.time }} <br> 下单时间
+            </div>
+          </div>
+          <div class="divider"></div>
+          <div class="data-grid">
+            <div class="data-col">
+              <div class="label">倍数</div>
+              <div class="val">{{ item.leverage }}X</div>
+            </div>
+            <div class="data-col center">
+              <div class="label">数量</div>
+              <div class="val">{{ item.amount }} {{ item.unit }}</div>
+            </div>
+            <div class="data-col right">
+              <div class="label">价格</div>
+              <div class="val">{{ item.price }} {{ item.unit }}</div>
+            </div>
+          </div>
+          <button class="btn-block-red">平仓</button>
+        </div>
+      </div>
+
+      <div v-else-if="activeTab === 'orders'" class="list-container">
+        <div v-for="(item, index) in ordersList" :key="index" class="card order-card">
+          <div class="card-header">
+            <div class="coin-info">
+              <div class="coin-icon icon-btc">₿</div>
+              <div class="coin-name-block">
+                <span class="symbol">{{ item.pair }}</span>
+                <div class="tags-row">
+                  <span class="tag" :class="item.type === 'buy' ? 'tag-green' : 'tag-red'">
+                    {{ item.type === 'buy' ? '买入' : '卖出' }}
+                  </span>
+                  <span class="tag tag-grey">{{ item.leverage }}X</span>
+                  <span class="tag tag-grey">市价</span>
+                </div>
+              </div>
+            </div>
+            <button class="btn-small-red">撤单</button>
+          </div>
+          <div class="divider"></div>
+          <div class="data-grid">
+            <div class="data-col">
+              <div class="label">数量</div>
+              <div class="val">{{ item.amount }} {{ item.unit }}</div>
+            </div>
+            <div class="data-col center">
+              <div class="label">下单时间</div>
+              <div class="val">{{ item.time }}</div>
+            </div>
+            <div class="data-col right">
+              <div class="label">价格</div>
+              <div class="val">{{ item.price }} {{ item.unit }}</div>
+            </div>
+          </div>
+        </div>
+      </div>
+
+    </div>
+  </div>
+</template>
+
+<script setup>
+import { ref } from 'vue';
+import { useRouter } from 'vue-router'; // 【新增】 引入路由
+
+const router = useRouter(); // 【新增】 实例化路由
+
+// 当前选中的 Tab:'assets', 'positions', 'orders'
+const activeTab = ref('orders');
+
+// 模拟数据 - 委托
+const ordersList = ref([
+  {
+    pair: 'BTC/USDT',
+    type: 'buy',
+    leverage: 20,
+    amount: '0.215',
+    unit: 'USDT',
+    price: '0.215',
+    time: '2025-11-04, 16:30'
+  },
+  {
+    pair: 'BTC/USDT',
+    type: 'sell',
+    leverage: 20,
+    amount: '0.215',
+    unit: 'USDT',
+    price: '0.215',
+    time: '2025-11-04, 16:30'
+  }
+]);
+
+// 模拟数据 - 仓位
+const positionsList = ref([
+  {
+    pair: 'BTC/USDT',
+    type: 'buy',
+    leverage: 200,
+    amount: '0.215',
+    unit: 'USDT',
+    price: '0.215',
+    time: '2025-11-04, 16:30'
+  },
+  {
+    pair: 'ETH/USDT',
+    type: 'sell',
+    leverage: 50,
+    amount: '1.5',
+    unit: 'USDT',
+    price: '1800.00',
+    time: '2025-11-04, 14:20'
+  }
+]);
+
+// 模拟数据 - 资产
+const assetsList = ref([
+  {
+    symbol: 'BTC',
+    nameCn: '比特币',
+    totalValue: '1,125,158.00',
+    cnyValue: '2150508',
+    available: '18802.0850',
+    frozen: '151.2050'
+  },
+  {
+    symbol: 'BTC', // 重复模拟图3效果
+    nameCn: '比特币',
+    totalValue: '1,125,158.00',
+    cnyValue: '2150508',
+    available: '18802.0850',
+    frozen: '151.2050'
+  }
+]);
+</script>
+
+<style scoped lang="scss">
+/* 基础变量 */
+$c-bg: #fff;
+$c-white: #ffffff;
+$c-text-main: #333333;
+$c-text-sub: #969799;
+$c-green: #00b86b; /* 买入绿 */
+$c-red: #dc3545;   /* 卖出/撤单红 */
+$c-line: #f5f5f5;
+$c-orange: #f7931a; /* BTC颜色 */
+
+.panel-container {
+  width: 100%;
+  background-color: $c-bg;
+  font-family: -apple-system, BlinkMacSystemFont, "Helvetica Neue", Helvetica, Segoe UI, Arial, Roboto, "PingFang SC", "miui", "Hiragino Sans GB", "Microsoft Yahei", sans-serif;
+  padding-bottom: 20px;
+}
+
+/* Tab 头部样式 */
+.tabs-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  background: $c-white;
+  padding: 12px 16px;
+  position: sticky;
+  top: 0;
+  z-index: 10;
+}
+
+.tabs-left {
+  display: flex;
+  gap: 20px;
+}
+
+.tab-item {
+  font-size: 15px;
+  color: $c-text-sub;
+  font-weight: 400;
+  position: relative;
+  cursor: pointer;
+  padding-bottom: 4px;
+  transition: all 0.2s;
+
+  &.active {
+    color: $c-text-main;
+    font-weight: 600;
+    font-size: 16px;
+
+    &::after {
+      content: '';
+      position: absolute;
+      bottom: -2px;
+      left: 50%;
+      transform: translateX(-50%);
+      width: 20px;
+      height: 3px;
+      background-color: #000;
+      border-radius: 2px;
+    }
+  }
+}
+
+.tabs-right {
+  font-size: 13px;
+  color: $c-text-sub;
+  display: flex;
+  align-items: flex-start;
+  gap: 4px;
+  margin-top: 2px;
+}
+
+/* 列表容器 */
+.list-container {
+  padding: 10px 16px;
+}
+
+/* 卡片通用样式 */
+.card {
+  background: $c-white;
+  border-radius: 8px;
+  padding: 12px 12px;
+  margin-bottom: 12px;
+  box-shadow: 0 2px 8px rgba(0,0,0,0.02);
+  border: 1px solid #ebedf0;
+}
+
+.card-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: flex-start;
+  margin-bottom: 12px;
+}
+
+.coin-info {
+  display: flex;
+  align-items: flex-start;
+}
+
+.coin-icon {
+  width: 36px;
+  height: 36px;
+  border-radius: 50%;
+  background: $c-orange;
+  color: #fff;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  font-size: 20px;
+  font-weight: bold;
+  margin-right: 10px;
+}
+
+.coin-name-block {
+  display: flex;
+  flex-direction: column;
+  gap: 4px;
+}
+
+.symbol {
+  font-size: 16px;
+  font-weight: 600;
+  color: $c-text-main;
+}
+
+/* 标签样式 */
+.tags-row {
+  display: flex;
+  gap: 4px;
+}
+
+.tag {
+  font-size: 10px;
+  padding: 1px 4px;
+  border-radius: 2px;
+  color: #fff;
+  display: inline-block;
+  line-height: 1.4;
+}
+
+.tag-green { background: $c-green; }
+.tag-red { background: $c-red; }
+.tag-grey { background: #c8c9cc; color: #fff; }
+
+/* 分割线 */
+.divider {
+  height: 1px;
+  background: $c-line;
+  margin: 0 0 12px 0;
+}
+
+/* 数据网格 (三列) */
+.data-grid {
+  display: flex;
+  justify-content: space-between;
+  margin-bottom: 8px;
+}
+
+.data-col {
+  text-align: left;
+
+
+  &.center { text-align: center; width: 50%; }
+  &.right { text-align: right; }
+
+  .label {
+    font-size: 12px;
+    color: $c-text-sub;
+    margin-bottom: 4px;
+  }
+  .val {
+    font-size: 14px;
+    color: $c-text-main;
+    font-weight: 500;
+  }
+}
+
+/* 按钮样式 */
+.btn-small-red {
+  background: $c-red;
+  color: #fff;
+  border: none;
+  padding: 6px 16px;
+  border-radius: 4px;
+  font-size: 13px;
+}
+
+.btn-block-red {
+  width: 100%;
+  background: $c-red;
+  color: #fff;
+  border: none;
+  padding: 10px 0;
+  border-radius: 20px; /* 圆角按钮 */
+  font-size: 15px;
+  margin-top: 8px;
+  font-weight: 500;
+}
+
+/* 特殊样式:Position 页面的时间 */
+.header-right-time {
+  text-align: right;
+  font-size: 12px;
+  color: $c-text-sub;
+  line-height: 1.4;
+}
+
+/* 特殊样式:Asset 页面 */
+.asset-card {
+  .card-header {
+    align-items: center;
+  }
+  .coin-name {
+    display: flex;
+    flex-direction: column;
+    .name-cn { font-size: 12px; color: $c-text-sub; }
+  }
+  .asset-total {
+    text-align: right;
+    .main-val { font-size: 18px; font-weight: bold; color: $c-text-main; }
+    .sub-val { font-size: 12px; color: $c-text-sub; margin-top: 2px; }
+  }
+  .card-row {
+    display: flex;
+    justify-content: space-between;
+    font-size: 13px;
+    margin-bottom: 6px;
+    &:last-child { margin-bottom: 0; }
+
+    .label { color: $c-text-sub; }
+    .value { color: $c-text-sub; font-family: sans-serif; }
+    .red-text { color: $c-red; font-weight: 500; }
+  }
+}
+</style>

+ 1 - 1
src/views/market/Index.vue

@@ -486,7 +486,7 @@ const formatCNY = (val) => {
 }
 
 .col { display: flex; }
-.col-left { width: 38%; justify-content: flex-start; align-items: center; }
+.col-left { width: 52%; justify-content: flex-start; align-items: center; }
 .col-center { width: 25%; justify-content: center; align-items: center; margin-left: 10px; }
 /* 修正右侧列样式,支持排序图标 */
 .col-right { width: 37%; justify-content: flex-end; align-items: center; }

+ 111 - 96
src/views/market/details/MarketConditions.vue

@@ -62,37 +62,28 @@
         </div>
       </div>
 
-      <!-- 指标按钮 -->
       <div class="tab-item icon-btn" @click.stop="toggleIndicators">
-        <!-- 保持你的图标路径 -->
         <img src="@/assets/icon/bitcoin/lishidingdan.svg" alt="" />
       </div>
     </nav>
 
     <!-- 3. 指标设置面板 -->
     <div class="indicator-panel" v-show="showIndicatorMenu" @click.stop>
-      <!-- 主图设置 (单选) -->
       <div class="panel-section">
         <div class="section-title">主图指标</div>
         <div class="btn-group">
           <div v-for="m in ['MA', 'EMA', 'BOLL', 'SAR']" :key="m"
                class="idx-btn" :class="{ active: mainIdx === m }"
-               @click="changeMain(m)">
-            {{ m }}
-          </div>
+               @click="changeMain(m)">{{ m }}</div>
           <div class="idx-btn" :class="{ active: mainIdx === 'Hide' }" @click="changeMain('Hide')">隐藏</div>
         </div>
       </div>
-
-      <!-- 副图设置 (多选) -->
       <div class="panel-section">
         <div class="section-title">副图指标 (可多选)</div>
         <div class="btn-group">
           <div v-for="s in ['VOL', 'MACD', 'KDJ', 'RSI', 'WR', 'CCI', 'OBV']" :key="s"
                class="idx-btn" :class="{ active: subIdx.includes(s) }"
-               @click="toggleSub(s)">
-            {{ s }}
-          </div>
+               @click="toggleSub(s)">{{ s }}</div>
           <div class="idx-btn" :class="{ active: subIdx.length === 0 }" @click="clearSub">隐藏</div>
         </div>
       </div>
@@ -100,14 +91,15 @@
 
     <!-- 4. K线图组件 -->
     <div class="k-line-main">
-      <KlineChart
+      <KLineChart
         ref="klineRef"
         :data="kLineData"
         height="70vh"
-        :precision="{ price: getPricePrecision(marketInfo.price), volume: 2 }" />
+        :precision="{ price: getPricePrecision(marketInfo.price), volume: 2 }"
+        @load-more="handleLoadMore" />
     </div>
 
-    <!-- 5. 底部 -->
+    <!-- 5. 底部原有的 委托挂单/最新成交 -->
     <div class="notifi-classifi">
       <div class="pf600 fs14" :class="current === 'entrustingOrder' ? 'fc121212' : 'fcA8A8A8'" @click="messageChange('entrustingOrder')">委托挂单</div>
       <div class="sys-notifi pf600 fs14" :class="current === 'latestTransactions' ? 'fc121212' : 'fcA8A8A8'" @click="messageChange('latestTransactions')">最新成交</div>
@@ -118,64 +110,44 @@
 </template>
 
 <script setup>
-import { ref, computed, onMounted, onBeforeUnmount, watch, nextTick } from "vue";
+import { ref, computed, onMounted, onBeforeUnmount, watch } from "vue";
 import { useRoute } from "vue-router";
 import { GetCandlestickChart } from "@/api/index.js";
+
+// 引入组件
+import KLineChart from "@/views/bitcoin/lever/components/KLineChart.vue";
 import EntrustingOrder from "./EntrustingOrder.vue";
 import LatestTransactions from "./LatestTransactions.vue";
-import KlineChart from "@/views/bitcoin/lever/components/KLineChart.vue";
 
 const route = useRoute();
 const symbolId = computed(() => route.query.id || "6");
 
-// 周期
+// --- 周期 ---
 const currentTab = ref("1d");
 const visibleTabs = ["15m", "1h", "4h", "1d"];
 const moreTabs = ["1m", "5m", "30m", "1w", "1M"];
 const isMoreActive = computed(() => moreTabs.includes(currentTab.value));
 const getTabLabel = (t) => ({ '1m':'1分', '5m':'5分', '15m':'15分', '30m':'30分', '1h':'1小时', '4h':'4小时', '1d':'日线', '1w':'周线', '1M':'月线' }[t] || t);
 
-// --- 指标管理 (升级版) ---
+// --- 指标管理 ---
 const showMoreMenu = ref(false);
 const showIndicatorMenu = ref(false);
 const klineRef = ref(null);
-
-// 主图状态 (单选)
 const mainIdx = ref('MA');
-
-// 副图状态 (多选,默认选两个)
 const subIdx = ref(['VOL', 'MACD']);
 
-// 切换主图
-const changeMain = (name) => {
-  mainIdx.value = name;
-  if (klineRef.value) klineRef.value.setMainIndicator(name);
-};
-
-// 切换副图 (多选逻辑)
+const changeMain = (name) => { mainIdx.value = name; if (klineRef.value) klineRef.value.setMainIndicator(name); };
 const toggleSub = (name) => {
   const index = subIdx.value.indexOf(name);
-  if (index > -1) {
-    // 已经选中 -> 取消选中
-    subIdx.value.splice(index, 1);
-  } else {
-    // 未选中 -> 添加选中 (限制最多3个防止太挤,或者不做限制)
-    if (subIdx.value.length >= 3) {
-        subIdx.value.shift(); // 移除最早选的一个,保持最多3个
-    }
+  if (index > -1) subIdx.value.splice(index, 1);
+  else {
+    if (subIdx.value.length >= 3) subIdx.value.shift();
     subIdx.value.push(name);
   }
-  // 更新图表
   if (klineRef.value) klineRef.value.setSubIndicators(subIdx.value);
 };
+const clearSub = () => { subIdx.value = []; if (klineRef.value) klineRef.value.setSubIndicators([]); };
 
-// 隐藏所有副图
-const clearSub = () => {
-  subIdx.value = [];
-  if (klineRef.value) klineRef.value.setSubIndicators([]);
-};
-
-// --- 交互 ---
 const toggleMore = () => { showIndicatorMenu.value = false; showMoreMenu.value = !showMoreMenu.value; };
 const toggleIndicators = () => { showMoreMenu.value = false; showIndicatorMenu.value = !showIndicatorMenu.value; };
 const closePopups = () => { showMoreMenu.value = false; showIndicatorMenu.value = false; };
@@ -188,44 +160,92 @@ const switchPeriod = (period) => {
   getKlineData();
 };
 
-// --- 数据/WS (保持不变) ---
+// --- 数据/WS ---
 const kLineData = ref([]);
 const socket = ref(null);
 const WS_BASE_URL = "ws://backend.66linknow.com/ws/kline/";
 const HEARTBEAT_INTERVAL = 15000;
 const RECONNECT_DELAY = 3000;
 let heartbeatTimer = null, reconnectTimer = null, isUnmounted = false;
+const isLoadingMore = ref(false);
 
 const marketInfo = ref({ price: "0.00", fiatPrice: "0.00", change: 0.0, high: "0.00", low: "0.00", vol: "0", amount: "0" });
+
 const orderPlacement = ref();
 const latestTransactionData = ref();
 const current = ref("entrustingOrder");
 const componentsMap = { entrustingOrder: EntrustingOrder, latestTransactions: LatestTransactions };
 const currentComponent = computed(() => componentsMap[current.value]);
+const messageChange = (key) => current.value = key;
 
-const getKlineData = async () => {
+// --- 获取 K线数据 (核心) ---
+const getKlineData = async (endTime = null) => {
   if (typeof GetCandlestickChart !== "function") return;
+
   try {
-    const res = await GetCandlestickChart({ symbol: symbolId.value, period: currentTab.value });
+    const params = {
+      symbol: symbolId.value,
+      period: currentTab.value,
+      limit: 150,
+      to: endTime || undefined
+    };
+
+    const res = await GetCandlestickChart(params);
+
     let rawList = Array.isArray(res) ? res : (res && res.data ? res.data : []);
+
     if (rawList.length > 0) {
       const formattedData = rawList.map((item) => ({
-        timestamp: Number(item[0]), open: parseFloat(item[1]), high: parseFloat(item[2]),
-        low: parseFloat(item[3]), close: parseFloat(item[4]), volume: parseFloat(item[5])
+        timestamp: Number(item[0]),
+        open: parseFloat(item[1]),
+        high: parseFloat(item[2]),
+        low: parseFloat(item[3]),
+        close: parseFloat(item[4]),
+        volume: parseFloat(item[5])
       }));
+
+      // 保证按时间升序
       formattedData.sort((a, b) => a.timestamp - b.timestamp);
-      kLineData.value = formattedData;
 
-      if (marketInfo.value.price === "0.00") {
-        const lastBar = formattedData[formattedData.length - 1];
-        let totalVol = 0; formattedData.forEach(i => totalVol += i.volume);
-        const changeRate = formattedData[0].open ? ((lastBar.close - formattedData[0].open) / formattedData[0].open) * 100 : 0;
-        marketInfo.value = { price: lastBar.close, fiatPrice: lastBar.close, change: changeRate.toFixed(2), high: lastBar.high, low: lastBar.low, vol: totalVol, amount: totalVol * lastBar.close };
+      if (endTime) {
+        // --- 加载历史模式 ---
+        if (klineRef.value) {
+          const hasMore = formattedData.length >= 150;
+          // 核心点:子组件 KLineChart 内部现在会自动处理 overlap
+          klineRef.value.applyHistoryData(formattedData, hasMore);
+        }
+      } else {
+        // --- 初始化/重置模式 ---
+        kLineData.value = formattedData;
+        if (formattedData.length > 0) {
+           const lastBar = formattedData[formattedData.length - 1];
+           marketInfo.value = { ...marketInfo.value, price: lastBar.close, fiatPrice: lastBar.close, high: lastBar.high, low: lastBar.low };
+        }
       }
+    } else {
+      // 没数据了
+      if (endTime && klineRef.value) {
+        klineRef.value.applyHistoryData([], false);
+      }
+    }
+  } catch (error) {
+    console.error("API Error", error);
+    if (endTime && klineRef.value) {
+      // 发生错误时也要告知 KLineChart 停止加载,否则 loading 动画会一直转
+      klineRef.value.applyHistoryData([], false);
     }
-  } catch (error) { console.error("API Error", error); }
+  } finally {
+    isLoadingMore.value = false;
+  }
 };
 
+const handleLoadMore = (timestamp) => {
+  if (isLoadingMore.value) return;
+  isLoadingMore.value = true;
+  getKlineData(timestamp);
+};
+
+// --- WebSocket ---
 const connectWebSocket = () => {
   closeWebSocket();
   const symbolStr = String(route.query.type || "btcusdt").toLowerCase();
@@ -267,17 +287,25 @@ const updateKlineData = (newBar) => {
   if (!kLineData.value?.length) { kLineData.value = [newBar]; return; }
   const lastIndex = kLineData.value.length - 1;
   const lastBar = kLineData.value[lastIndex];
+  // 简单的 WebSocket 去重
   if (newBar.timestamp === lastBar.timestamp) kLineData.value.splice(lastIndex, 1, newBar);
   else if (newBar.timestamp > lastBar.timestamp) kLineData.value.push(newBar);
 };
 
-const handleVisibilityChange = () => { if (document.visibilityState === "visible" && socket.value?.readyState !== 1) connectWebSocket(); };
-onMounted(() => { isUnmounted = false; getKlineData(); connectWebSocket(); document.addEventListener("visibilitychange", handleVisibilityChange); });
+const handleVisibilityChange = () => {
+  if (document.visibilityState === "visible" && socket.value?.readyState !== 1)
+    connectWebSocket(); };
+onMounted(() => { isUnmounted = false;
+  getKlineData(); connectWebSocket();
+  document.addEventListener("visibilitychange", handleVisibilityChange); });
 onBeforeUnmount(() => { isUnmounted = true; closeWebSocket(); document.removeEventListener("visibilitychange", handleVisibilityChange); });
 watch(symbolId, () => { kLineData.value = []; getKlineData(); connectWebSocket(); });
-const messageChange = (key) => current.value = key;
 const formatNumber = (num) => num ? Number(num).toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 6 }) : "0.00";
-const abbreviateNumber = (v) => { if (!v) return "0.00"; let n = parseFloat(v); if (n>=1e9) return (n/1e9).toFixed(2)+"B"; if (n>=1e6) return (n/1e6).toFixed(2)+"M"; if (n>=1e3) return (n/1e3).toFixed(2)+"K"; return n.toFixed(2); };
+const abbreviateNumber = (v) => { if (!v) return "0.00"; let n = parseFloat(v);
+  if (n>=1e9) return (n/1e9).toFixed(2)+"B";
+  if (n>=1e6) return (n/1e6).toFixed(2)+"M";
+  if (n>=1e3) return (n/1e3).toFixed(2)+"K";
+  return n.toFixed(2); };
 const getPricePrecision = (p) => (p < 1 ? 6 : p < 10 ? 4 : 2);
 const getPriceColor = (c) => (c >= 0 ? "fc45B26B" : "fcF6465D");
 const getUpDownClass = (c) => (c >= 0 ? "fc45B26B" : "fcF6465D");
@@ -287,51 +315,38 @@ const getUpDownClass = (c) => (c >= 0 ? "fc45B26B" : "fcF6465D");
 .fc45B26B { color: #2ebd85 !important; }
 .fcF6465D { color: #f6465d !important; }
 .fc1F2937 { color: #1f2937; }
-
-.time-tabs {
-  display: flex; align-items: center; justify-content: space-between;
-  width: 100%; box-sizing: border-box; padding: 0 15px 0 15px; position: relative; z-index: 20;
-}
-.tab-item {
-  font-size: 14px; color: #929aa5; padding: 4px 8px; border-radius: 4px; cursor: pointer; font-weight: 500; display: flex; align-items: center; position: relative;
-}
+.time-tabs { display: flex; align-items: center; justify-content: space-between; width: 100%;
+  box-sizing: border-box; padding:5px 0 15px 15px; position: relative; z-index: 20; }
+.tab-item { font-size: 14px; color: #929aa5; padding: 4px 8px; border-radius: 4px; cursor: pointer;
+  font-weight: 500; display: flex; align-items: center; position: relative; }
 .tab-item.icon-btn { padding: 4px 4px; }
 .active-text { color: #fff; font-weight: 600; }
 .triangle { font-size: 8px; margin-left: 2px; transform: scale(0.9); }
 .tab-item img { display: block; height: 16px; width: auto; }
-
-.dropdown-menu {
-  position: absolute; top: 30px; left: -20px; width: 80px; background: #fff;
-  box-shadow: 0 4px 12px rgba(0,0,0,0.15); border-radius: 6px; padding: 5px 0;
-  z-index: 100; display: flex; flex-direction: column;
-}
+.dropdown-menu { position: absolute; top: 30px; left: -20px; width: 80px; background: #fff;
+  box-shadow: 0 4px 12px rgba(0,0,0,0.15); border-radius: 6px; padding: 5px 0; z-index: 100;
+  display: flex; flex-direction: column; }
 .drop-item { padding: 8px 15px; font-size: 13px; color: #666; text-align: center; }
 .drop-item.active { color: #F6465D; background: #fff5f5; }
-
-.indicator-panel {
-  position: absolute; top: 155px; left: 15px; right: 15px; background: #fff;
-  box-shadow: 0 4px 15px rgba(0,0,0,0.1); border-radius: 8px; padding: 15px; z-index: 99;
-}
+.indicator-panel { position: absolute; top: 155px; left: 15px; right: 15px; background: #fff;
+  box-shadow: 0 4px 15px rgba(0,0,0,0.1); border-radius: 8px; padding: 15px; z-index: 99; }
 .panel-section { margin-bottom: 15px; }
 .section-title { font-size: 12px; color: #999; margin-bottom: 8px; }
 .btn-group { display: flex; flex-wrap: wrap; gap: 10px; }
-.idx-btn { padding: 4px 12px; border: 1px solid #eee; border-radius: 14px; font-size: 12px; color: #666; cursor: pointer; }
+.idx-btn { padding: 4px 12px; border: 1px solid #eee; border-radius: 14px; font-size: 12px;
+  color: #666; cursor: pointer; }
 .idx-btn.active { border-color: #F6465D; color: #F6465D; background-color: #fff5f5; }
-
-.market-conditions { display: flex; flex-direction: column; align-items: center; margin-bottom: 50px; width: 100%; position: relative; }
-.market-price {
-  display: flex; justify-content: space-between; margin-top: 8px;
+.market-conditions { display: flex; flex-direction: column; align-items: center; width: 100%;
+  position: relative; min-height: 100vh; background: #fff; }
+.market-price { display: flex; justify-content: space-between; margin-top: 4px;
   width: 100%; padding: 0 15px; box-sizing: border-box;
-  .price-left { display: flex; flex-direction: column; width: 144px; line-height: 20px;
-    height: 69px;
-    .left-price { display: flex; align-items: center; height: 18px; }
-    .left-number { margin-top: 5px; }
-    .left-appro { display: flex; align-items: center;margin-top: 3px;
-      .appro { margin-left: 9px; } } }
-  .price-right { display: flex; flex-direction: column; height: 100%; line-height: 20px;
-    .right-number-top, .right-number-bottom { display: flex; justify-content: flex-end; width: 100%; height: 32px; .right-number-top-price, .right-number-top-number { margin-left: 10px; text-align: right; div { height: 16px; line-height: 16px; text-align: end; } } } .right-number-bottom {  } }
-}
+  .price-left { display: flex;line-height: 20px; flex-direction: column; width: 144px;
+    .left-price { height: 18px; line-height: 18px; } .left-number { margin-top: 5px; }
+    .left-appro { margin-top: 3px; .appro { margin-left: 9px; } } }
+  .price-right { display: flex; flex-direction: column; height: 100%;
+    .right-number-top, .right-number-bottom { display: flex; justify-content: flex-end; width: 100%; height: 32px; .right-number-top-price, .right-number-top-number { margin-left: 10px; text-align: right; div { height: 16px; line-height: 16px; text-align: end; } } } } }
 .k-line-main { height: 50vh; min-height: 350px; width: 100%; padding: 0 15px; }
-.notifi-classifi { display: flex; align-items: flex-end; margin-top: 140px; width: 100%; padding: 0 15px;
-  box-sizing: border-box; height: 24px; border-bottom: 1px solid #f5f5f5; padding-bottom: 5px; .sys-notifi { margin-left: 47px; } }
+.notifi-classifi { display: flex; align-items: flex-end; margin-top: 140px;
+  width: 100%; padding: 0 15px 5px 15px; box-sizing: border-box; height: 24px;
+  border-bottom: 1px solid #f5f5f5; .sys-notifi { margin-left: 47px; } }
 </style>

+ 2 - 1
vue.config.js

@@ -12,7 +12,8 @@ module.exports = defineConfig({
         // 如果后端在本地,可能是 http://localhost:8000
         // 如果是线上测试服,可能是 http://47.100.xx.xx
           //'http://63.141.230.43:57676',
-        target: 'http://backend.66linknow.com', // ✅ 必须加上协议
+          // 'http://backend.66linknow.com'
+        target:'http://63.141.230.43:57676' , // ✅ 必须加上协议
         changeOrigin: true, // 允许跨域
 
       },

Některé soubory nejsou zobrazeny, neboť je v těchto rozdílových datech změněno mnoho souborů