From b43fa2416527340c28dd795799f1673393eb8bbf Mon Sep 17 00:00:00 2001 From: Lxy Date: Sun, 7 Jun 2026 12:21:33 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=A2=9E=E5=8A=A0=E6=9D=83=E9=99=90?= =?UTF-8?q?=E7=B3=BB=E7=BB=9F=EF=BC=8C=E7=9B=AE=E5=89=8D=E6=B2=A1=E6=9C=89?= =?UTF-8?q?=E5=8E=86=E5=8F=B2=E5=BD=A2=E6=88=90=E7=AD=89=E8=AE=B0=E5=BD=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .data/trip-planner.db | Bin 0 -> 4096 bytes .data/trip-planner.db-shm | Bin 0 -> 32768 bytes .data/trip-planner.db-wal | Bin 0 -> 288432 bytes package-lock.json | 558 ++++++++++++++++++++++++++++++++ package.json | 4 + server.js | 14 +- src/App.vue | 31 ++ src/components/AuthModal.vue | 294 +++++++++++++++++ src/components/HistoryPanel.vue | 253 +++++++++++++++ src/db/database.js | 87 +++++ src/router.js | 4 +- src/routes/auth.js | 219 +++++++++++++ src/routes/plans.js | 142 ++++++++ src/routes/stats.js | 162 ++++++++++ src/services/statsService.js | 52 +++ src/stores/auth.js | 218 +++++++++++++ src/views/AdminPage.vue | 313 ++++++++++++++++++ src/views/HomePage.vue | 146 ++++++++- 18 files changed, 2492 insertions(+), 5 deletions(-) create mode 100644 .data/trip-planner.db create mode 100644 .data/trip-planner.db-shm create mode 100644 .data/trip-planner.db-wal create mode 100644 src/components/AuthModal.vue create mode 100644 src/components/HistoryPanel.vue create mode 100644 src/db/database.js create mode 100644 src/routes/auth.js create mode 100644 src/routes/plans.js create mode 100644 src/routes/stats.js create mode 100644 src/services/statsService.js create mode 100644 src/stores/auth.js create mode 100644 src/views/AdminPage.vue diff --git a/.data/trip-planner.db b/.data/trip-planner.db new file mode 100644 index 0000000000000000000000000000000000000000..73a069272b27c57da406e6748d103073062bafc4 GIT binary patch literal 4096 zcmWFz^vNtqRY=P(%1ta$FlG>7U}9o$P*7lCU|@t|AVoG{WYC*>k{6_1fNV2HHI9bB nXb6mkz-S1JhQMeDjE2By2#kinXb6mkz-S1JhQMeDP#6LLBNzv7 literal 0 HcmV?d00001 diff --git a/.data/trip-planner.db-shm b/.data/trip-planner.db-shm new file mode 100644 index 0000000000000000000000000000000000000000..01bf770732f56303c12efc9d1ef104812e5f71ed GIT binary patch literal 32768 zcmeI*IZgvX5QX7~*$mj&U>1Yf_kG{ZV%8H7Ad!)fa|$wuh~WT8+<|)#a05;NT~d!^ zkr5%e{*p@78ud);eFx|%elE(Y5V}svU&QsqbN=fl{qeM#x_-QUyT5w5yn85>U*C&= z+;88us)YWoDtlI4;cTTs*N1Kh-5A=QHHB`D>vWhcaorkbTU=-2%*NRsXD-fsoE>p? z#%Y;DrvBW6B7-AL?ET%WXVLpnLxcpoFyhM0*OF_Mw2BI0cQe@8grJI zxCkTycAp}JfKLItMU+Cor-0pKOCjJ>z;2AD5b!CG)wpK0tWA}5?2~}hZUx#kp*gK+ zOZz%;t3Ys{0`@-(3IU%2_7@il0iOb0K4X&_fgsR>0s?LYikj5CR<*4Iow`*ZxKDw8 zO=&@E+R>rTd=ik_t-ye$wWxLN>PY8q6$tKAU`R7s(uVd_k^R+}LI42-5I_KdMBoQY CN-+}v literal 0 HcmV?d00001 diff --git a/.data/trip-planner.db-wal b/.data/trip-planner.db-wal new file mode 100644 index 0000000000000000000000000000000000000000..d5790e14a9577cedc07e98109d9b2718a384725d GIT binary patch literal 288432 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 literal 0 HcmV?d00001 diff --git a/package-lock.json b/package-lock.json index b14e071..0dbf24a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,11 +8,15 @@ "name": "trip-planner", "version": "0.0.0", "dependencies": { + "bcryptjs": "^3.0.3", + "better-sqlite3": "^12.10.0", "cors": "^2.8.6", "express": "^4.22.2", + "jsonwebtoken": "^9.0.3", "leaflet": "^1.9.4", "pinia": "^3.0.4", "sortablejs": "^1.15.7", + "uuid": "^14.0.0", "vue": "^3.5.34", "vue-router": "^4.6.4" }, @@ -987,6 +991,58 @@ "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", "license": "MIT" }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/bcryptjs": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-3.0.3.tgz", + "integrity": "sha512-GlF5wPWnSa/X5LKM1o0wz0suXIINz1iHRLvTS+sLyi7XPbe5ycmYI3DlZqVGZZtDgl4DmasFg7gOB3JYbphV5g==", + "license": "BSD-3-Clause", + "bin": { + "bcrypt": "bin/bcrypt" + } + }, + "node_modules/better-sqlite3": { + "version": "12.10.0", + "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.10.0.tgz", + "integrity": "sha512-CyzaZRQKyHkB2ZInfTTl2nvT33EbDpjkLEbE8/Zck3Ll6O0qqvuGdrJ45HgtH+HykRg88ITY3AdreBGN70aBSQ==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "bindings": "^1.5.0", + "prebuild-install": "^7.1.1" + }, + "engines": { + "node": "20.x || 22.x || 23.x || 24.x || 25.x || 26.x" + } + }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "license": "MIT", + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, "node_modules/birpc": { "version": "2.9.0", "resolved": "https://registry.npmjs.org/birpc/-/birpc-2.9.0.tgz", @@ -996,6 +1052,17 @@ "url": "https://github.com/sponsors/antfu" } }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, "node_modules/body-parser": { "version": "1.20.5", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.5.tgz", @@ -1020,6 +1087,36 @@ "npm": "1.2.8000 || >= 1.4.16" } }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -1058,6 +1155,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "license": "ISC" + }, "node_modules/content-disposition": { "version": "0.5.4", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", @@ -1141,6 +1244,30 @@ "ms": "2.0.0" } }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -1160,6 +1287,15 @@ "npm": "1.2.8000 || >= 1.4.16" } }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -1174,6 +1310,15 @@ "node": ">= 0.4" } }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -1189,6 +1334,15 @@ "node": ">= 0.8" } }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, "node_modules/entities": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", @@ -1291,6 +1445,15 @@ "node": ">= 0.6" } }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "license": "(MIT OR WTFPL)", + "engines": { + "node": ">=6" + } + }, "node_modules/express": { "version": "4.22.2", "resolved": "https://registry.npmjs.org/express/-/express-4.22.2.tgz", @@ -1337,6 +1500,12 @@ "url": "https://opencollective.com/express" } }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "license": "MIT" + }, "node_modules/finalhandler": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", @@ -1373,6 +1542,12 @@ "node": ">= 0.6" } }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "license": "MIT" + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -1434,6 +1609,12 @@ "node": ">= 0.4" } }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "license": "MIT" + }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", @@ -1508,12 +1689,38 @@ "node": ">=0.10.0" } }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "license": "ISC" }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "license": "ISC" + }, "node_modules/ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", @@ -1535,12 +1742,103 @@ "url": "https://github.com/sponsors/mesqueeb" } }, + "node_modules/jsonwebtoken": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", + "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==", + "license": "MIT", + "dependencies": { + "jws": "^4.0.1", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jsonwebtoken/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, "node_modules/leaflet": { "version": "1.9.4", "resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz", "integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==", "license": "BSD-2-Clause" }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "license": "MIT" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "license": "MIT" + }, "node_modules/magic-string": { "version": "0.30.21", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", @@ -1619,12 +1917,39 @@ "node": ">= 0.6" } }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/mitt": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", "license": "MIT" }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "license": "MIT" + }, "node_modules/ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", @@ -1649,6 +1974,12 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "license": "MIT" + }, "node_modules/negotiator": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", @@ -1658,6 +1989,18 @@ "node": ">= 0.6" } }, + "node_modules/node-abi": { + "version": "3.92.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.92.0.tgz", + "integrity": "sha512-KdHvFWZjEKDf0cakgFjebl371GPsISX2oZHcuyKqM7DtogIsHrqKeLTo8wBHxaXRAQlY2PsPlZmfo+9ZCxEREQ==", + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmmirror.com/object-assign/-/object-assign-4.1.1.tgz", @@ -1691,6 +2034,15 @@ "node": ">= 0.8" } }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -1767,6 +2119,33 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.", + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -1780,6 +2159,16 @@ "node": ">= 0.10" } }, + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, "node_modules/qs": { "version": "6.15.2", "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz", @@ -1819,6 +2208,35 @@ "node": ">= 0.8" } }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/rfdc": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", @@ -1896,6 +2314,18 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "license": "MIT" }, + "node_modules/semver": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.2.tgz", + "integrity": "sha512-c8jsqUZm3omBOI66G90z1Dyw5z622G8oLG+omfsHBJf3CWQTlOcwOjvOG6wtiNfW6anKm/eA39LMwMtMez2TiQ==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/send": { "version": "0.19.2", "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", @@ -2019,6 +2449,51 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, "node_modules/sortablejs": { "version": "1.15.7", "resolved": "https://registry.npmjs.org/sortablejs/-/sortablejs-1.15.7.tgz", @@ -2052,6 +2527,24 @@ "node": ">= 0.8" } }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/superjson": { "version": "2.2.6", "resolved": "https://registry.npmjs.org/superjson/-/superjson-2.2.6.tgz", @@ -2064,6 +2557,34 @@ "node": ">=16" } }, + "node_modules/tar-fs": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", + "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", + "license": "MIT", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/toidentifier": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", @@ -2073,6 +2594,18 @@ "node": ">=0.6" } }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, "node_modules/type-is": { "version": "1.6.18", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", @@ -2095,6 +2628,12 @@ "node": ">= 0.8" } }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, "node_modules/utils-merge": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", @@ -2104,6 +2643,19 @@ "node": ">= 0.4.0" } }, + "node_modules/uuid": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-14.0.0.tgz", + "integrity": "sha512-Qo+uWgilfSmAhXCMav1uYFynlQO7fMFiMVZsQqZRMIXp0O7rR7qjkj+cPvBHLgBqi960QCoo/PH2/6ZtVqKvrg==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist-node/bin/uuid" + } + }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", @@ -2214,6 +2766,12 @@ "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.4.tgz", "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==", "license": "MIT" + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" } } } diff --git a/package.json b/package.json index 42e74e8..75c7d01 100644 --- a/package.json +++ b/package.json @@ -9,11 +9,15 @@ "preview": "vite preview" }, "dependencies": { + "bcryptjs": "^3.0.3", + "better-sqlite3": "^12.10.0", "cors": "^2.8.6", "express": "^4.22.2", + "jsonwebtoken": "^9.0.3", "leaflet": "^1.9.4", "pinia": "^3.0.4", "sortablejs": "^1.15.7", + "uuid": "^14.0.0", "vue": "^3.5.34", "vue-router": "^4.6.4" }, diff --git a/server.js b/server.js index 102f3df..a1aa727 100644 --- a/server.js +++ b/server.js @@ -45,15 +45,25 @@ function saveConfig(config) { import cors from 'cors' +// 导入路由 +import authRoutes from './src/routes/auth.js' +import plansRoutes from './src/routes/plans.js' +import statsRoutes from './src/routes/stats.js' + const app = express() app.use(cors({ origin: 'http://localhost:5173', - methods: ['GET', 'POST', 'OPTIONS'], - allowedHeaders: ['Content-Type'], + methods: ['GET', 'POST', 'OPTIONS', 'DELETE'], + allowedHeaders: ['Content-Type', 'Authorization'], exposedHeaders: ['Content-Type'] })) app.use(express.json()) +// 注册路由 +app.use('/api/auth', authRoutes) +app.use('/api/plans', plansRoutes) +app.use('/api/stats', statsRoutes) + // API: Get config app.get('/api/config', (req, res) => { const config = loadConfig() diff --git a/src/App.vue b/src/App.vue index bbf831a..f0b60c9 100644 --- a/src/App.vue +++ b/src/App.vue @@ -1,9 +1,40 @@ diff --git a/src/components/HistoryPanel.vue b/src/components/HistoryPanel.vue new file mode 100644 index 0000000..d2d5c8f --- /dev/null +++ b/src/components/HistoryPanel.vue @@ -0,0 +1,253 @@ + + + + + diff --git a/src/db/database.js b/src/db/database.js new file mode 100644 index 0000000..7aba3c6 --- /dev/null +++ b/src/db/database.js @@ -0,0 +1,87 @@ +// SQLite 数据库初始化和表创建 +import Database from 'better-sqlite3' +import path from 'path' +import fs from 'fs' +import { fileURLToPath } from 'url' + +const __dirname = path.dirname(fileURLToPath(import.meta.url)) + +const DB_PATH = path.join(__dirname, '../../.data', 'trip-planner.db') + +// 确保数据目录存在 +const dbDir = path.dirname(DB_PATH) +if (!fs.existsSync(dbDir)) { + fs.mkdirSync(dbDir, { recursive: true }) +} + +const db = new Database(DB_PATH) + +// 启用 WAL 模式提高并发性能 +db.pragma('journal_mode = WAL') + +// 创建表 +db.exec(` + -- 用户表 + CREATE TABLE IF NOT EXISTS users ( + id TEXT PRIMARY KEY, + username TEXT UNIQUE NOT NULL, + email TEXT UNIQUE, + password_hash TEXT NOT NULL, + role TEXT DEFAULT 'user' CHECK(role IN ('user', 'admin')), + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP + ); + + -- 行程计划表 + CREATE TABLE IF NOT EXISTS plans ( + id TEXT PRIMARY KEY, + user_id TEXT REFERENCES users(id) ON DELETE CASCADE, + guest_id TEXT, + name TEXT NOT NULL, + description TEXT, + data TEXT NOT NULL, -- JSON 格式的完整行程数据 + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP + ); + + -- 游客临时数据表(24小时过期) + CREATE TABLE IF NOT EXISTS guest_plans ( + id TEXT PRIMARY KEY, + guest_id TEXT NOT NULL, + name TEXT NOT NULL, + description TEXT, + data TEXT NOT NULL, + expires_at DATETIME NOT NULL, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP + ); + + -- 使用统计表 + CREATE TABLE IF NOT EXISTS stats ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id TEXT, + guest_id TEXT, + event_type TEXT NOT NULL, -- 'page_view', 'plan_create', 'plan_load', 'plan_export', 'chat_start', etc. + event_data TEXT, -- JSON 格式的额外数据 + created_at DATETIME DEFAULT CURRENT_TIMESTAMP + ); + + -- 创建索引 + CREATE INDEX IF NOT EXISTS idx_plans_user_id ON plans(user_id); + CREATE INDEX IF NOT EXISTS idx_plans_guest_id ON plans(guest_id); + CREATE INDEX IF NOT EXISTS idx_guest_plans_guest_id ON guest_plans(guest_id); + CREATE INDEX IF NOT EXISTS idx_guest_plans_expires_at ON guest_plans(expires_at); + CREATE INDEX IF NOT EXISTS idx_stats_user_id ON stats(user_id); + CREATE INDEX IF NOT EXISTS idx_stats_guest_id ON stats(guest_id); + CREATE INDEX IF NOT EXISTS idx_stats_event_type ON stats(event_type); + CREATE INDEX IF NOT EXISTS idx_stats_created_at ON stats(created_at); +`) + +// 清理过期游客数据的函数 +function cleanExpiredGuestPlans() { + db.prepare('DELETE FROM guest_plans WHERE expires_at < ?').run(new Date().toISOString()) +} + +// 每次启动时清理过期数据 +cleanExpiredGuestPlans() + +export { db, cleanExpiredGuestPlans } diff --git a/src/router.js b/src/router.js index a23c73e..5c982dd 100644 --- a/src/router.js +++ b/src/router.js @@ -1,10 +1,12 @@ import { createRouter, createWebHistory } from 'vue-router' import HomePage from './views/HomePage.vue' import SettingsPage from './views/SettingsPage.vue' +import AdminPage from './views/AdminPage.vue' const routes = [ { path: '/', component: HomePage }, - { path: '/settings', component: SettingsPage } + { path: '/settings', component: SettingsPage }, + { path: '/admin', component: AdminPage } ] const router = createRouter({ diff --git a/src/routes/auth.js b/src/routes/auth.js new file mode 100644 index 0000000..7131249 --- /dev/null +++ b/src/routes/auth.js @@ -0,0 +1,219 @@ +// 用户认证路由 +import express from 'express' +import jwt from 'jsonwebtoken' +import bcrypt from 'bcryptjs' +import { v4 as uuidv4 } from 'uuid' +import { db } from '../db/database.js' + +const router = express.Router() + +// JWT 密钥(生产环境应使用环境变量) +const JWT_SECRET = process.env.JWT_SECRET || 'trip-planner-secret-key-2024' +const JWT_EXPIRES_IN = '7d' // Token 7天过期 + +// 中间件:验证 JWT Token +function authenticateToken(req, res, next) { + const authHeader = req.headers['authorization'] + const token = authHeader && authHeader.split(' ')[1] // Bearer TOKEN + + if (!token) { + return res.status(401).json({ error: '未提供认证令牌' }) + } + + try { + const user = jwt.verify(token, JWT_SECRET) + req.user = user + next() + } catch (err) { + return res.status(403).json({ error: '无效的认证令牌' }) + } +} + +// 中间件:可选认证(有 token 则验证,没有则继续) +function optionalAuth(req, res, next) { + const authHeader = req.headers['authorization'] + const token = authHeader && authHeader.split(' ')[1] + + if (token) { + try { + const user = jwt.verify(token, JWT_SECRET) + req.user = user + } catch (err) { + // Token 无效但继续,视为游客 + } + } + next() +} + +// 中间件:管理员权限检查 +function requireAdmin(req, res, next) { + if (!req.user || req.user.role !== 'admin') { + return res.status(403).json({ error: '需要管理员权限' }) + } + next() +} + +// 注册新用户 +router.post('/register', async (req, res) => { + try { + const { username, email, password } = req.body + + if (!username || !password) { + return res.status(400).json({ error: '用户名和密码不能为空' }) + } + + if (password.length < 6) { + return res.status(400).json({ error: '密码至少需要6个字符' }) + } + + // 检查用户名是否已存在 + const existing = db.prepare('SELECT id FROM users WHERE username = ?').get(username) + if (existing) { + return res.status(409).json({ error: '用户名已被使用' }) + } + + // 检查是否是第一个注册用户(自动成为管理员) + const userCount = db.prepare('SELECT COUNT(*) as count FROM users').get() + const role = userCount.count === 0 ? 'admin' : 'user' + + // 哈希密码 + const passwordHash = await bcrypt.hash(password, 10) + + // 创建用户 + const userId = uuidv4() + db.prepare('INSERT INTO users (id, username, email, password_hash, role) VALUES (?, ?, ?, ?, ?)').run( + userId, username, email || null, passwordHash, role + ) + + // 生成 JWT + const token = jwt.sign( + { id: userId, username, role }, + JWT_SECRET, + { expiresIn: JWT_EXPIRES_IN } + ) + + // 记录统计 + db.prepare('INSERT INTO stats (user_id, event_type, event_data) VALUES (?, ?, ?)').run( + userId, 'user_register', JSON.stringify({ username, role }) + ) + + res.status(201).json({ + message: '注册成功', + user: { id: userId, username, role }, + token + }) + } catch (err) { + console.error('注册失败:', err) + res.status(500).json({ error: '注册失败,请稍后重试' }) + } +}) + +// 用户登录 +router.post('/login', async (req, res) => { + try { + const { username, password } = req.body + + if (!username || !password) { + return res.status(400).json({ error: '用户名和密码不能为空' }) + } + + // 查找用户 + const user = db.prepare('SELECT * FROM users WHERE username = ?').get(username) + if (!user) { + return res.status(401).json({ error: '用户名或密码错误' }) + } + + // 验证密码 + const validPassword = await bcrypt.compare(password, user.password_hash) + if (!validPassword) { + return res.status(401).json({ error: '用户名或密码错误' }) + } + + // 生成 JWT + const token = jwt.sign( + { id: user.id, username: user.username, role: user.role }, + JWT_SECRET, + { expiresIn: JWT_EXPIRES_IN } + ) + + // 记录统计 + db.prepare('INSERT INTO stats (user_id, event_type) VALUES (?, ?)').run( + user.id, 'user_login' + ) + + res.json({ + message: '登录成功', + user: { id: user.id, username: user.username, role: user.role }, + token + }) + } catch (err) { + console.error('登录失败:', err) + res.status(500).json({ error: '登录失败,请稍后重试' }) + } +}) + +// 获取当前用户信息 +router.get('/me', authenticateToken, (req, res) => { + const user = db.prepare('SELECT id, username, email, role, created_at FROM users WHERE id = ?').get(req.user.id) + if (!user) { + return res.status(404).json({ error: '用户不存在' }) + } + res.json({ user }) +}) + +// 生成游客 ID +router.post('/guest', (req, res) => { + const guestId = uuidv4() + const expiresAt = new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString() // 24小时后过期 + + res.json({ + guestId, + expiresAt, + message: '游客会话已创建,数据将保留24小时' + }) +}) + +// 迁移游客数据到登录用户 +router.post('/migrate-guest-data', authenticateToken, (req, res) => { + try { + const { guestId } = req.body + if (!guestId) { + return res.status(400).json({ error: '缺少游客ID' }) + } + + // 获取游客的所有计划 + const guestPlans = db.prepare('SELECT * FROM plans WHERE guest_id = ?').all(guestId) + + // 将游客计划转移到用户 + let migrated = 0 + const stmt = db.prepare('UPDATE plans SET user_id = ?, guest_id = NULL WHERE id = ?') + for (const plan of guestPlans) { + stmt.run(req.user.id, plan.id) + migrated++ + } + + // 同时迁移游客计划 + const guestTempPlans = db.prepare('SELECT * FROM guest_plans WHERE guest_id = ?').all(guestId) + const insertPlan = db.prepare('INSERT INTO plans (id, user_id, name, description, data) VALUES (?, ?, ?, ?, ?)') + for (const gp of guestTempPlans) { + insertPlan.run(uuidv4(), req.user.id, gp.name, gp.description, gp.data) + migrated++ + } + + // 删除游客临时数据 + db.prepare('DELETE FROM guest_plans WHERE guest_id = ?').run(guestId) + + // 记录统计 + db.prepare('INSERT INTO stats (user_id, event_type, event_data) VALUES (?, ?, ?)').run( + req.user.id, 'guest_data_migrated', JSON.stringify({ guestId, migrated }) + ) + + res.json({ message: '数据迁移成功', migrated }) + } catch (err) { + console.error('数据迁移失败:', err) + res.status(500).json({ error: '数据迁移失败' }) + } +}) + +export default router +export { authenticateToken, optionalAuth, requireAdmin } diff --git a/src/routes/plans.js b/src/routes/plans.js new file mode 100644 index 0000000..f2cc5e4 --- /dev/null +++ b/src/routes/plans.js @@ -0,0 +1,142 @@ +// 行程计划路由 +import express from 'express' +import { v4 as uuidv4 } from 'uuid' +import { db } from '../db/database.js' +import { authenticateToken, optionalAuth } from './auth.js' + +const router = express.Router() + +// 保存行程计划(需要认证或游客ID) +router.post('/', optionalAuth, (req, res) => { + try { + const { name, description, data } = req.body + + if (!name || !data) { + return res.status(400).json({ error: '名称和行程数据不能为空' }) + } + + const planId = uuidv4() + + if (req.user) { + // 已登录用户 + db.prepare('INSERT INTO plans (id, user_id, name, description, data) VALUES (?, ?, ?, ?, ?)').run( + planId, req.user.id, name, description || null, JSON.stringify(data) + ) + + // 记录统计 + db.prepare('INSERT INTO stats (user_id, event_type, event_data) VALUES (?, ?, ?)').run( + req.user.id, 'plan_create', JSON.stringify({ planId, name }) + ) + } else { + // 游客 + const { guestId } = req.body + if (!guestId) { + return res.status(400).json({ error: '游客需要提供guestId' }) + } + + const expiresAt = new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString() + db.prepare('INSERT INTO guest_plans (id, guest_id, name, description, data, expires_at) VALUES (?, ?, ?, ?, ?, ?)').run( + planId, guestId, name, description || null, JSON.stringify(data), expiresAt + ) + + // 记录统计 + db.prepare('INSERT INTO stats (guest_id, event_type, event_data) VALUES (?, ?, ?)').run( + guestId, 'plan_create', JSON.stringify({ planId, name }) + ) + } + + res.status(201).json({ id: planId, message: '行程保存成功' }) + } catch (err) { + console.error('保存行程失败:', err) + res.status(500).json({ error: '保存行程失败' }) + } +}) + +// 获取用户的所有行程计划 +router.get('/', authenticateToken, (req, res) => { + try { + const plans = db.prepare('SELECT id, name, description, created_at, updated_at FROM plans WHERE user_id = ? ORDER BY created_at DESC').all(req.user.id) + + // 记录统计 + db.prepare('INSERT INTO stats (user_id, event_type) VALUES (?, ?)').run(req.user.id, 'plans_list') + + res.json({ plans }) + } catch (err) { + console.error('获取行程列表失败:', err) + res.status(500).json({ error: '获取行程列表失败' }) + } +}) + +// 获取游客的行程计划 +router.get('/guest/:guestId', (req, res) => { + try { + const { guestId } = req.params + + // 清理过期数据 + db.prepare('DELETE FROM guest_plans WHERE expires_at < ?').run(new Date().toISOString()) + + const plans = db.prepare('SELECT id, name, description, created_at, expires_at FROM guest_plans WHERE guest_id = ? ORDER BY created_at DESC').all(guestId) + + res.json({ plans }) + } catch (err) { + console.error('获取游客行程失败:', err) + res.status(500).json({ error: '获取行程失败' }) + } +}) + +// 获取单个行程详情 +router.get('/:id', optionalAuth, (req, res) => { + try { + const { id } = req.params + + let plan + if (req.user) { + plan = db.prepare('SELECT * FROM plans WHERE id = ? AND user_id = ?').get(id, req.user.id) + } else { + const { guestId } = req.query + plan = db.prepare('SELECT * FROM guest_plans WHERE id = ? AND guest_id = ?').get(id, guestId) + } + + if (!plan) { + return res.status(404).json({ error: '行程不存在' }) + } + + // 解析 JSON 数据 + plan.data = JSON.parse(plan.data) + + // 记录统计 + if (req.user) { + db.prepare('INSERT INTO stats (user_id, event_type, event_data) VALUES (?, ?, ?)').run( + req.user.id, 'plan_load', JSON.stringify({ planId: id }) + ) + } + + res.json({ plan }) + } catch (err) { + console.error('获取行程详情失败:', err) + res.status(500).json({ error: '获取行程详情失败' }) + } +}) + +// 删除行程 +router.delete('/:id', authenticateToken, (req, res) => { + try { + const { id } = req.params + const result = db.prepare('DELETE FROM plans WHERE id = ? AND user_id = ?').run(id, req.user.id) + + if (result.changes === 0) { + return res.status(404).json({ error: '行程不存在' }) + } + + db.prepare('INSERT INTO stats (user_id, event_type, event_data) VALUES (?, ?, ?)').run( + req.user.id, 'plan_delete', JSON.stringify({ planId: id }) + ) + + res.json({ message: '行程删除成功' }) + } catch (err) { + console.error('删除行程失败:', err) + res.status(500).json({ error: '删除行程失败' }) + } +}) + +export default router diff --git a/src/routes/stats.js b/src/routes/stats.js new file mode 100644 index 0000000..38d44f4 --- /dev/null +++ b/src/routes/stats.js @@ -0,0 +1,162 @@ +// 使用统计和管理员路由 +import express from 'express' +import { db } from '../db/database.js' +import { requireAdmin } from './auth.js' + +const router = express.Router() + +// 记录事件(前端调用) +router.post('/event', (req, res) => { + try { + const { userId, guestId, eventType, eventData } = req.body + + if (!eventType) { + return res.status(400).json({ error: '事件类型不能为空' }) + } + + db.prepare('INSERT INTO stats (user_id, guest_id, event_type, event_data) VALUES (?, ?, ?, ?)').run( + userId || null, + guestId || null, + eventType, + eventData ? JSON.stringify(eventData) : null + ) + + res.json({ message: '事件记录成功' }) + } catch (err) { + console.error('记录事件失败:', err) + res.status(500).json({ error: '记录事件失败' }) + } +}) + +// 管理员:获取总体统计数据 +router.get('/admin/overview', requireAdmin, (req, res) => { + try { + // 用户总数 + const totalUsers = db.prepare('SELECT COUNT(*) as count FROM users').get() + + // 管理员数量 + const adminCount = db.prepare('SELECT COUNT(*) as count FROM users WHERE role = ?', ['admin']).get() + + // 总计划数 + const totalPlans = db.prepare('SELECT COUNT(*) as count FROM plans').get() + + // 游客计划数(未过期) + const guestPlans = db.prepare('SELECT COUNT(*) as count FROM guest_plans WHERE expires_at > ?', [new Date().toISOString()]).get() + + // 总事件数 + const totalEvents = db.prepare('SELECT COUNT(*) as count FROM stats').get() + + // 今日事件数 + const todayEvents = db.prepare(`SELECT COUNT(*) as count FROM stats WHERE date(created_at) = date('now')`).get() + + // 今日活跃用户 + const todayActiveUsers = db.prepare(`SELECT COUNT(DISTINCT user_id) as count FROM stats WHERE date(created_at) = date('now') AND user_id IS NOT NULL`).get() + + // 今日活跃游客 + const todayActiveGuests = db.prepare(`SELECT COUNT(DISTINCT guest_id) as count FROM stats WHERE date(created_at) = date('now') AND guest_id IS NOT NULL`).get() + + res.json({ + overview: { + totalUsers: totalUsers.count, + adminCount: adminCount.count, + totalPlans: totalPlans.count, + activeGuestPlans: guestPlans.count, + totalEvents: totalEvents.count, + todayEvents: todayEvents.count, + todayActiveUsers: todayActiveUsers.count, + todayActiveGuests: todayActiveGuests.count + } + }) + } catch (err) { + console.error('获取统计数据失败:', err) + res.status(500).json({ error: '获取统计数据失败' }) + } +}) + +// 管理员:按类型统计事件 +router.get('/admin/events-by-type', requireAdmin, (req, res) => { + try { + const { days = 30 } = req.query + const since = new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString() + + const events = db.prepare(` + SELECT event_type, COUNT(*) as count + FROM stats + WHERE created_at > ? + GROUP BY event_type + ORDER BY count DESC + `).all(since) + + res.json({ events, days }) + } catch (err) { + console.error('获取事件统计失败:', err) + res.status(500).json({ error: '获取事件统计失败' }) + } +}) + +// 管理员:每日活跃趋势 +router.get('/admin/daily-active', requireAdmin, (req, res) => { + try { + const { days = 30 } = req.query + + const dailyUsers = db.prepare(` + SELECT date(created_at) as date, COUNT(DISTINCT user_id) as users, COUNT(DISTINCT guest_id) as guests + FROM stats + WHERE created_at > datetime('now', ?) + GROUP BY date(created_at) + ORDER BY date ASC + `).all(`-${days} days`) + + res.json({ daily: dailyUsers, days }) + } catch (err) { + console.error('获取活跃趋势失败:', err) + res.status(500).json({ error: '获取活跃趋势失败' }) + } +}) + +// 管理员:用户列表 +router.get('/admin/users', requireAdmin, (req, res) => { + try { + const users = db.prepare(` + SELECT u.id, u.username, u.email, u.role, u.created_at, + COUNT(DISTINCT p.id) as plan_count, + COUNT(DISTINCT s.id) as event_count, + MAX(s.created_at) as last_active + FROM users u + LEFT JOIN plans p ON u.id = p.user_id + LEFT JOIN stats s ON u.id = s.user_id + GROUP BY u.id + ORDER BY u.created_at DESC + `).all() + + res.json({ users }) + } catch (err) { + console.error('获取用户列表失败:', err) + res.status(500).json({ error: '获取用户列表失败' }) + } +}) + +// 管理员:热门行程 +router.get('/admin/popular-plans', requireAdmin, (req, res) => { + try { + const { limit = 10 } = req.query + + const plans = db.prepare(` + SELECT p.id, p.name, p.description, p.created_at, u.username, + COUNT(s.id) as view_count + FROM plans p + LEFT JOIN users u ON p.user_id = u.id + LEFT JOIN stats s ON s.event_data LIKE '%' || p.id || '%' AND s.event_type = 'plan_load' + GROUP BY p.id + ORDER BY view_count DESC + LIMIT ? + `).all(limit) + + res.json({ plans }) + } catch (err) { + console.error('获取热门行程失败:', err) + res.status(500).json({ error: '获取热门行程失败' }) + } +}) + +export default router diff --git a/src/services/statsService.js b/src/services/statsService.js new file mode 100644 index 0000000..bd0bc1b --- /dev/null +++ b/src/services/statsService.js @@ -0,0 +1,52 @@ +// 使用统计服务 +import { useAuthStore } from '../stores/auth' + +const API_BASE = 'http://localhost:3001/api' + +export function trackEvent(eventType, eventData = {}) { + const authStore = useAuthStore() + + const payload = { + userId: authStore.user?.id || null, + guestId: authStore.guestId || null, + eventType, + eventData + } + + // 异步发送,不阻塞主流程 + fetch(`${API_BASE}/stats/event`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payload) + }).catch(err => console.error('统计记录失败:', err)) +} + +// 页面访问统计 +export function trackPageView(pageName, extra = {}) { + trackEvent('page_view', { page: pageName, ...extra }) +} + +// 规划创建统计 +export function trackPlanCreate(planName, mode) { + trackEvent('plan_create', { planName, mode }) +} + +// 规划加载统计 +export function trackPlanLoad(planId) { + trackEvent('plan_load', { planId }) +} + +// 规划导出统计 +export function trackPlanExport(planId, format) { + trackEvent('plan_export', { planId, format }) +} + +// 聊天开始统计 +export function trackChatStart(mode) { + trackEvent('chat_start', { mode }) +} + +// 方案选择统计 +export function trackSchemeSelect(schemeName, schemeIndex) { + trackEvent('scheme_select', { schemeName, schemeIndex }) +} diff --git a/src/stores/auth.js b/src/stores/auth.js new file mode 100644 index 0000000..0fa6993 --- /dev/null +++ b/src/stores/auth.js @@ -0,0 +1,218 @@ +// 用户认证状态管理 +import { defineStore } from 'pinia' +import { ref, computed } from 'vue' + +const API_BASE = 'http://localhost:3001/api' + +export const useAuthStore = defineStore('auth', () => { + // 状态 + const user = ref(null) + const token = ref(localStorage.getItem('auth_token') || null) + const guestId = ref(localStorage.getItem('guest_id') || null) + const guestExpiresAt = ref(localStorage.getItem('guest_expires_at') || null) + const loading = ref(false) + const error = ref(null) + + // 计算属性 + const isAuthenticated = computed(() => !!user.value && !!token.value) + const isGuest = computed(() => !!guestId.value && !user.value) + const isAdmin = computed(() => user.value?.role === 'admin') + const isGuestDataExpiring = computed(() => { + if (!guestExpiresAt.value) return false + const expires = new Date(guestExpiresAt.value) + const now = new Date() + const hoursLeft = (expires - now) / (1000 * 60 * 60) + return hoursLeft < 2 // 少于2小时提醒 + }) + const isGuestDataExpired = computed(() => { + if (!guestExpiresAt.value) return false + return new Date(guestExpiresAt.value) < new Date() + }) + + // 方法 + function setAuthHeader() { + if (token.value) { + return { 'Authorization': `Bearer ${token.value}` } + } + return {} + } + + async function login(username, password) { + loading.value = true + error.value = null + try { + const res = await fetch(`${API_BASE}/auth/login`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ username, password }) + }) + const data = await res.json() + if (!res.ok) throw new Error(data.error || '登录失败') + + user.value = data.user + token.value = data.token + localStorage.setItem('auth_token', data.token) + + // 如果有游客数据,提示迁移 + return { success: true, user: data.user } + } catch (err) { + error.value = err.message + return { success: false, error: err.message } + } finally { + loading.value = false + } + } + + async function register(username, email, password) { + loading.value = true + error.value = null + try { + const res = await fetch(`${API_BASE}/auth/register`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ username, email, password }) + }) + const data = await res.json() + if (!res.ok) throw new Error(data.error || '注册失败') + + user.value = data.user + token.value = data.token + localStorage.setItem('auth_token', data.token) + + return { success: true, user: data.user } + } catch (err) { + error.value = err.message + return { success: false, error: err.message } + } finally { + loading.value = false + } + } + + function logout() { + user.value = null + token.value = null + localStorage.removeItem('auth_token') + } + + async function initGuestSession() { + // 检查现有游客会话是否过期 + if (guestId.value && guestExpiresAt.value) { + const expires = new Date(guestExpiresAt.value) + if (expires > new Date()) { + return guestId.value // 会话仍有效 + } + // 过期了,清除 + clearGuestSession() + } + + // 创建新游客会话 + loading.value = true + try { + const res = await fetch(`${API_BASE}/auth/guest`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' } + }) + const data = await res.json() + if (!res.ok) throw new Error(data.error || '创建游客会话失败') + + guestId.value = data.guestId + guestExpiresAt.value = data.expiresAt + localStorage.setItem('guest_id', data.guestId) + localStorage.setItem('guest_expires_at', data.expiresAt) + + return data.guestId + } catch (err) { + error.value = err.message + throw err + } finally { + loading.value = false + } + } + + function clearGuestSession() { + guestId.value = null + guestExpiresAt.value = null + localStorage.removeItem('guest_id') + localStorage.removeItem('guest_expires_at') + } + + async function migrateGuestData() { + if (!token.value || !guestId.value) return + + loading.value = true + try { + const res = await fetch(`${API_BASE}/auth/migrate-guest-data`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + ...setAuthHeader() + }, + body: JSON.stringify({ guestId: guestId.value }) + }) + const data = await res.json() + if (!res.ok) throw new Error(data.error || '数据迁移失败') + + clearGuestSession() + return data + } catch (err) { + error.value = err.message + throw err + } finally { + loading.value = false + } + } + + async function loadCurrentUser() { + if (!token.value) return + + loading.value = true + try { + const res = await fetch(`${API_BASE}/auth/me`, { + headers: setAuthHeader() + }) + if (res.ok) { + const data = await res.json() + user.value = data.user + } else { + // Token 无效,清除 + logout() + } + } catch (err) { + console.error('加载用户信息失败:', err) + } finally { + loading.value = false + } + } + + // 初始化时加载用户信息 + async function init() { + if (token.value) { + await loadCurrentUser() + } else { + await initGuestSession() + } + } + + return { + user, + token, + guestId, + guestExpiresAt, + loading, + error, + isAuthenticated, + isGuest, + isAdmin, + isGuestDataExpiring, + isGuestDataExpired, + login, + register, + logout, + initGuestSession, + clearGuestSession, + migrateGuestData, + loadCurrentUser, + init, + setAuthHeader + } +}) diff --git a/src/views/AdminPage.vue b/src/views/AdminPage.vue new file mode 100644 index 0000000..0250f71 --- /dev/null +++ b/src/views/AdminPage.vue @@ -0,0 +1,313 @@ + + + + + diff --git a/src/views/HomePage.vue b/src/views/HomePage.vue index f7fe234..f017d63 100644 --- a/src/views/HomePage.vue +++ b/src/views/HomePage.vue @@ -3,7 +3,20 @@ @@ -43,26 +56,55 @@ + + + + + +