forked from EduCraft/curriculum-project-hub
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 87bbf0bd57 | |||
| 8eda04f1d9 | |||
| 1dab83f9db | |||
| 8c26c42e0c | |||
| fe38d0b8a9 | |||
| e194670d74 |
Generated
+302
-47
@@ -7,6 +7,17 @@
|
||||
"": {
|
||||
"name": "filelib-web",
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"@codemirror/lang-css": "^6.3.1",
|
||||
"@codemirror/lang-html": "^6.4.11",
|
||||
"@codemirror/lang-javascript": "^6.2.5",
|
||||
"@codemirror/lang-json": "^6.0.2",
|
||||
"@codemirror/lang-markdown": "^6.5.1",
|
||||
"@codemirror/language": "^6.12.4",
|
||||
"codemirror": "^6.0.2",
|
||||
"codemirror-lang-typst": "^0.4.0",
|
||||
"vite-plugin-wasm": "^3.6.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sveltejs/adapter-static": "^3.0.10",
|
||||
"@sveltejs/kit": "^2.63.0",
|
||||
@@ -19,11 +30,161 @@
|
||||
"vite": "^8.0.16"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/autocomplete": {
|
||||
"version": "6.20.3",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.20.3.tgz",
|
||||
"integrity": "sha512-tlosUqb+3BbxCxZdu4tKeRghPFC+QM7q4X5YhKV2eCmPG+1r2F3f4AaSz5sCrFqUtX4Jh20VFTKecl16MgiV9g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/language": "^6.0.0",
|
||||
"@codemirror/state": "^6.0.0",
|
||||
"@codemirror/view": "^6.17.0",
|
||||
"@lezer/common": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/commands": {
|
||||
"version": "6.10.4",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.10.4.tgz",
|
||||
"integrity": "sha512-Ryk9y9T0FFVF0cUGhAknveAyUOl/A1qReTFi+qPKtOh2Z9F4AUBz3XOrYD4ZEgZirdugVzHvd/2/Wcwy5OliTg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/language": "^6.0.0",
|
||||
"@codemirror/state": "^6.7.0",
|
||||
"@codemirror/view": "^6.27.0",
|
||||
"@lezer/common": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/lang-css": {
|
||||
"version": "6.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/lang-css/-/lang-css-6.3.1.tgz",
|
||||
"integrity": "sha512-kr5fwBGiGtmz6l0LSJIbno9QrifNMUusivHbnA1H6Dmqy4HZFte3UAICix1VuKo0lMPKQr2rqB+0BkKi/S3Ejg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/autocomplete": "^6.0.0",
|
||||
"@codemirror/language": "^6.0.0",
|
||||
"@codemirror/state": "^6.0.0",
|
||||
"@lezer/common": "^1.0.2",
|
||||
"@lezer/css": "^1.1.7"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/lang-html": {
|
||||
"version": "6.4.11",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/lang-html/-/lang-html-6.4.11.tgz",
|
||||
"integrity": "sha512-9NsXp7Nwp891pQchI7gPdTwBuSuT3K65NGTHWHNJ55HjYcHLllr0rbIZNdOzas9ztc1EUVBlHou85FFZS4BNnw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/autocomplete": "^6.0.0",
|
||||
"@codemirror/lang-css": "^6.0.0",
|
||||
"@codemirror/lang-javascript": "^6.0.0",
|
||||
"@codemirror/language": "^6.4.0",
|
||||
"@codemirror/state": "^6.0.0",
|
||||
"@codemirror/view": "^6.17.0",
|
||||
"@lezer/common": "^1.0.0",
|
||||
"@lezer/css": "^1.1.0",
|
||||
"@lezer/html": "^1.3.12"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/lang-javascript": {
|
||||
"version": "6.2.5",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/lang-javascript/-/lang-javascript-6.2.5.tgz",
|
||||
"integrity": "sha512-zD4e5mS+50htS7F+TYjBPsiIFGanfVqg4HyUz6WNFikgOPf2BgKlx+TQedI1w6n/IqRBVBbBWmGFdLB/7uxO4A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/autocomplete": "^6.0.0",
|
||||
"@codemirror/language": "^6.6.0",
|
||||
"@codemirror/lint": "^6.0.0",
|
||||
"@codemirror/state": "^6.0.0",
|
||||
"@codemirror/view": "^6.17.0",
|
||||
"@lezer/common": "^1.0.0",
|
||||
"@lezer/javascript": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/lang-json": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/lang-json/-/lang-json-6.0.2.tgz",
|
||||
"integrity": "sha512-x2OtO+AvwEHrEwR0FyyPtfDUiloG3rnVTSZV1W8UteaLL8/MajQd8DpvUb2YVzC+/T18aSDv0H9mu+xw0EStoQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/language": "^6.0.0",
|
||||
"@lezer/json": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/lang-markdown": {
|
||||
"version": "6.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/lang-markdown/-/lang-markdown-6.5.1.tgz",
|
||||
"integrity": "sha512-6re5avCNfyRMIoi3XNjbEfQM1vTeVD3JS3g/Fyegyso/eoANFM71Cyvbb66LDyYtQLMEcRFlzioywCqDo9SlLA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/autocomplete": "^6.7.1",
|
||||
"@codemirror/lang-html": "^6.0.0",
|
||||
"@codemirror/language": "^6.3.0",
|
||||
"@codemirror/state": "^6.0.0",
|
||||
"@codemirror/view": "^6.0.0",
|
||||
"@lezer/common": "^1.2.1",
|
||||
"@lezer/markdown": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/language": {
|
||||
"version": "6.12.4",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.12.4.tgz",
|
||||
"integrity": "sha512-1q4PaT+o6PbgpkJt4Q8Fv5XJxTy4FUZ4MWETtyiDw3J0Pyr9E2vqcKL+k9wcvjNTIsauxvE7OfmWj3FRPHQ76A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/state": "^6.0.0",
|
||||
"@codemirror/view": "^6.23.0",
|
||||
"@lezer/common": "^1.5.0",
|
||||
"@lezer/highlight": "^1.0.0",
|
||||
"@lezer/lr": "^1.0.0",
|
||||
"style-mod": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/lint": {
|
||||
"version": "6.9.7",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.9.7.tgz",
|
||||
"integrity": "sha512-28/+iWLYxKxsvGYhSYL7zaCZqLz5+FFFDq9tVsvGv9kv8RY4fFAchJ5WX9M3YrrRlTIsECjsXPqeNgnSmNP2dg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/state": "^6.0.0",
|
||||
"@codemirror/view": "^6.42.0",
|
||||
"crelt": "^1.0.5"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/search": {
|
||||
"version": "6.7.1",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.7.1.tgz",
|
||||
"integrity": "sha512-uMe5UO6PamJtSHrXhhHOzSX3ReWtiJrva6GnPMwSOrZtiExb5X5eExhr2OUZQVvdxPsKpY3Ro2mFbQadpPWmHA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/state": "^6.0.0",
|
||||
"@codemirror/view": "^6.37.0",
|
||||
"crelt": "^1.0.5"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/state": {
|
||||
"version": "6.7.1",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.7.1.tgz",
|
||||
"integrity": "sha512-9QzNDgE4EYDnAHfrTlR2lwiPciiOymLtwKK+8yHQzCc7GXhAP9xdEbEJFy2IWB1j9UGUl9BsgMmTo/ImA02T7A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@marijn/find-cluster-break": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/view": {
|
||||
"version": "6.43.6",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.43.6.tgz",
|
||||
"integrity": "sha512-EVunGSYN1wz1p75WY1s3Xg7t3i8Yol0kGZGizNdX9BUFgMFILYVe8/u6EVpo7Ff5PwbZuILb4QAq7IZoKzIEQA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/state": "^6.7.0",
|
||||
"crelt": "^1.0.6",
|
||||
"style-mod": "^4.1.0",
|
||||
"w3c-keyname": "^2.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@emnapi/core": {
|
||||
"version": "1.11.1",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz",
|
||||
"integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
@@ -35,7 +196,6 @@
|
||||
"version": "1.11.1",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz",
|
||||
"integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
@@ -46,7 +206,6 @@
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz",
|
||||
"integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
@@ -103,11 +262,94 @@
|
||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||
}
|
||||
},
|
||||
"node_modules/@lezer/common": {
|
||||
"version": "1.5.2",
|
||||
"resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.5.2.tgz",
|
||||
"integrity": "sha512-sxQE460fPZyU3sdc8lafxiPwJHBzZRy/udNFynGQky1SePYBdhkBl1kOagA9uT3pxR8K09bOrmTUqA9wb/PjSQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@lezer/css": {
|
||||
"version": "1.3.4",
|
||||
"resolved": "https://registry.npmjs.org/@lezer/css/-/css-1.3.4.tgz",
|
||||
"integrity": "sha512-N+tn9tej2hPvyKgHEApMOQfHczDJCwxrRFS3SPn9QjYN+uwHvEDnCgKRrb3mxDYxRS8sKMM8fhC3+lc04Abz5Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@lezer/common": "^1.2.0",
|
||||
"@lezer/highlight": "^1.0.0",
|
||||
"@lezer/lr": "^1.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@lezer/highlight": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.3.tgz",
|
||||
"integrity": "sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@lezer/common": "^1.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@lezer/html": {
|
||||
"version": "1.3.13",
|
||||
"resolved": "https://registry.npmjs.org/@lezer/html/-/html-1.3.13.tgz",
|
||||
"integrity": "sha512-oI7n6NJml729m7pjm9lvLvmXbdoMoi2f+1pwSDJkl9d68zGr7a9Btz8NdHTGQZtW2DA25ybeuv/SyDb9D5tseg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@lezer/common": "^1.2.0",
|
||||
"@lezer/highlight": "^1.0.0",
|
||||
"@lezer/lr": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@lezer/javascript": {
|
||||
"version": "1.5.4",
|
||||
"resolved": "https://registry.npmjs.org/@lezer/javascript/-/javascript-1.5.4.tgz",
|
||||
"integrity": "sha512-vvYx3MhWqeZtGPwDStM2dwgljd5smolYD2lR2UyFcHfxbBQebqx8yjmFmxtJ/E6nN6u1D9srOiVWm3Rb4tmcUA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@lezer/common": "^1.2.0",
|
||||
"@lezer/highlight": "^1.1.3",
|
||||
"@lezer/lr": "^1.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@lezer/json": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@lezer/json/-/json-1.0.3.tgz",
|
||||
"integrity": "sha512-BP9KzdF9Y35PDpv04r0VeSTKDeox5vVr3efE7eBbx3r4s3oNLfunchejZhjArmeieBH+nVOpgIiBJpEAv8ilqQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@lezer/common": "^1.2.0",
|
||||
"@lezer/highlight": "^1.0.0",
|
||||
"@lezer/lr": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@lezer/lr": {
|
||||
"version": "1.4.10",
|
||||
"resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.10.tgz",
|
||||
"integrity": "sha512-rnCpTIBafOx4mRp43xOxDJbFipJm/c0cia/V5TiGlhmMa+wsSdoGmUN3w5Bqrks/09Q/D4tNAmWaT8p6NRi77A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@lezer/common": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@lezer/markdown": {
|
||||
"version": "1.7.2",
|
||||
"resolved": "https://registry.npmjs.org/@lezer/markdown/-/markdown-1.7.2.tgz",
|
||||
"integrity": "sha512-iTkYvoVcKt3WkeL7qUDyXHONZEwLio4wj8KTNi2dnjQEXBZKMV63BpQrPqfsM+OkvuRbiSTAcycYAsQzLhRNoQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@lezer/common": "^1.5.0",
|
||||
"@lezer/highlight": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@marijn/find-cluster-break": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.3.tgz",
|
||||
"integrity": "sha512-FY+MKLBoTsLNJF/eLWaOsXGdz6uh3Iu1axjPf6TUq92IYumcTcXWHoS747JARLkcdlJ/Waiaxc5wQfFO8jC6NA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@napi-rs/wasm-runtime": {
|
||||
"version": "1.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.6.tgz",
|
||||
"integrity": "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
@@ -126,7 +368,6 @@
|
||||
"version": "0.139.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.139.0.tgz",
|
||||
"integrity": "sha512-r9gHphtCs+1M7J0pw6Sn/hh/Wpa/iQrOOkrNAlVLF/gHq+/CJmHIWKKUUhdWjcD6CIa8idarspCsASiXCXvFUw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/Boshen"
|
||||
@@ -146,7 +387,6 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -163,7 +403,6 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -180,7 +419,6 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -197,7 +435,6 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -214,7 +451,6 @@
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -231,7 +467,6 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
@@ -251,7 +486,6 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
@@ -271,7 +505,6 @@
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
@@ -291,7 +524,6 @@
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
@@ -311,7 +543,6 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
@@ -331,7 +562,6 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
@@ -351,7 +581,6 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -368,7 +597,6 @@
|
||||
"cpu": [
|
||||
"wasm32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
@@ -387,7 +615,6 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -404,7 +631,6 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -418,7 +644,6 @@
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz",
|
||||
"integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@standard-schema/spec": {
|
||||
@@ -808,7 +1033,6 @@
|
||||
"version": "0.10.3",
|
||||
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz",
|
||||
"integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
@@ -895,6 +1119,34 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/codemirror": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/codemirror/-/codemirror-6.0.2.tgz",
|
||||
"integrity": "sha512-VhydHotNW5w1UGK0Qj96BwSk/Zqbp9WbnyK2W/eVMv4QyF41INRGpjUhFJY7/uDNuudSc33a/PKr4iDqRduvHw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/autocomplete": "^6.0.0",
|
||||
"@codemirror/commands": "^6.0.0",
|
||||
"@codemirror/language": "^6.0.0",
|
||||
"@codemirror/lint": "^6.0.0",
|
||||
"@codemirror/search": "^6.0.0",
|
||||
"@codemirror/state": "^6.0.0",
|
||||
"@codemirror/view": "^6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/codemirror-lang-typst": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/codemirror-lang-typst/-/codemirror-lang-typst-0.4.0.tgz",
|
||||
"integrity": "sha512-jpHz5qQRC3LE48JH+C24qZJAEAhjqRzWA4MFodPaxriz7UwLVuTLCQ672rdDx9ziObBQ2BvHaRIm5/Upz4KoBQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@codemirror/language": "^6.11.2",
|
||||
"@codemirror/state": "^6.5.2",
|
||||
"@codemirror/view": "^6.38.1",
|
||||
"@lezer/common": "^1.2.3",
|
||||
"@lezer/highlight": "^1.2.1"
|
||||
}
|
||||
},
|
||||
"node_modules/cookie": {
|
||||
"version": "0.6.0",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz",
|
||||
@@ -905,6 +1157,12 @@
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/crelt": {
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.7.tgz",
|
||||
"integrity": "sha512-aK6BbWfhf4U/wCcLHKPJl/xa6VkVstRaPywWtMKGwuOLc/wZTyQYuoxgvZnNsBvv7Kg3YTBQYYBCggcviQczuA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/deepmerge": {
|
||||
"version": "4.3.1",
|
||||
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",
|
||||
@@ -919,7 +1177,6 @@
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
|
||||
"integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
@@ -975,7 +1232,6 @@
|
||||
"version": "6.5.0",
|
||||
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
|
||||
"integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
@@ -993,7 +1249,6 @@
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
||||
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
@@ -1025,7 +1280,7 @@
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz",
|
||||
"integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"jiti": "lib/jiti-cli.mjs"
|
||||
@@ -1045,7 +1300,6 @@
|
||||
"version": "1.32.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz",
|
||||
"integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==",
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"dependencies": {
|
||||
"detect-libc": "^2.0.3"
|
||||
@@ -1078,7 +1332,6 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1099,7 +1352,6 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1120,7 +1372,6 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1141,7 +1392,6 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1162,7 +1412,6 @@
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1183,7 +1432,6 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
@@ -1207,7 +1455,6 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
@@ -1231,7 +1478,6 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
@@ -1255,7 +1501,6 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
@@ -1279,7 +1524,6 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1300,7 +1544,6 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1355,7 +1598,6 @@
|
||||
"version": "3.3.16",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz",
|
||||
"integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
@@ -1388,14 +1630,12 @@
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/picomatch": {
|
||||
"version": "4.0.5",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz",
|
||||
"integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
@@ -1408,7 +1648,6 @@
|
||||
"version": "8.5.22",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.22.tgz",
|
||||
"integrity": "sha512-KBDEIpLrvpv16pp3K0Fw+UCoZfopFjjgeB+0tA/aaThfEE74kKDLrgg603YvOWJyg3+WYtyq3xYsQWsIyZlPqQ==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
@@ -1451,7 +1690,6 @@
|
||||
"version": "1.1.5",
|
||||
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.5.tgz",
|
||||
"integrity": "sha512-t9z29cJjXf/vxQ8dyhCSpt6H6aSwHTk8cT5I3iy6SMXuFpk5mB6PL6XfC8PCwrPTx93udwKUm9HRteAlTGBLiA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@oxc-project/types": "=0.139.0",
|
||||
@@ -1520,12 +1758,17 @@
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/style-mod": {
|
||||
"version": "4.1.3",
|
||||
"resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.3.tgz",
|
||||
"integrity": "sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/svelte": {
|
||||
"version": "5.56.7",
|
||||
"resolved": "https://registry.npmjs.org/svelte/-/svelte-5.56.7.tgz",
|
||||
@@ -1604,7 +1847,6 @@
|
||||
"version": "0.2.17",
|
||||
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
|
||||
"integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fdir": "^6.5.0",
|
||||
@@ -1631,7 +1873,6 @@
|
||||
"version": "2.8.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||
"dev": true,
|
||||
"license": "0BSD",
|
||||
"optional": true
|
||||
},
|
||||
@@ -1653,7 +1894,6 @@
|
||||
"version": "8.1.5",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-8.1.5.tgz",
|
||||
"integrity": "sha512-7ULLwsCdYx/nRyrpiEwvqb5TFHrMVZyBt+rg/OAXT7rgj/z+DtTDyKFeLAdDkubDVDKD8jOsndmy7m55XcfUsw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"lightningcss": "^1.32.0",
|
||||
@@ -1727,6 +1967,15 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/vite-plugin-wasm": {
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/vite-plugin-wasm/-/vite-plugin-wasm-3.6.0.tgz",
|
||||
"integrity": "sha512-mL/QPziiIA4RAA6DkaZZzOstdwbW5jO4Vz7Zenj0wieKWBlNvIvX5L5ljum9lcUX0ShNfBgCNLKTjNkRVVqcsw==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"vite": "^2 || ^3 || ^4 || ^5 || ^6 || ^7 || ^8"
|
||||
}
|
||||
},
|
||||
"node_modules/vitefu": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/vitefu/-/vitefu-1.1.3.tgz",
|
||||
@@ -1747,6 +1996,12 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/w3c-keyname": {
|
||||
"version": "2.2.8",
|
||||
"resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz",
|
||||
"integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/zimmerframe": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.4.tgz",
|
||||
|
||||
@@ -20,5 +20,16 @@
|
||||
"tailwindcss": "^4.3.2",
|
||||
"typescript": "^5.7.0",
|
||||
"vite": "^8.0.16"
|
||||
},
|
||||
"dependencies": {
|
||||
"@codemirror/lang-css": "^6.3.1",
|
||||
"@codemirror/lang-html": "^6.4.11",
|
||||
"@codemirror/lang-javascript": "^6.2.5",
|
||||
"@codemirror/lang-json": "^6.0.2",
|
||||
"@codemirror/lang-markdown": "^6.5.1",
|
||||
"@codemirror/language": "^6.12.4",
|
||||
"codemirror": "^6.0.2",
|
||||
"codemirror-lang-typst": "^0.4.0",
|
||||
"vite-plugin-wasm": "^3.6.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
<script lang="ts">
|
||||
/**
|
||||
* CodeMirror 6 编辑器封装。自动根据文件扩展名选择语法高亮。
|
||||
* 只在浏览器 mount 后创建 EditorView(CodeMirror 依赖 DOM)。
|
||||
*/
|
||||
import { onMount, onDestroy } from "svelte";
|
||||
import { EditorView, basicSetup } from "codemirror";
|
||||
import { EditorState } from "@codemirror/state";
|
||||
import { markdown } from "@codemirror/lang-markdown";
|
||||
import { javascript } from "@codemirror/lang-javascript";
|
||||
import { json } from "@codemirror/lang-json";
|
||||
import { html } from "@codemirror/lang-html";
|
||||
import { css } from "@codemirror/lang-css";
|
||||
import { typst } from "codemirror-lang-typst";
|
||||
import type { Extension } from "@codemirror/state";
|
||||
|
||||
let { value = "", readonly = false, filename = "", onchange }: {
|
||||
value?: string;
|
||||
readonly?: boolean;
|
||||
filename?: string;
|
||||
onchange?: (value: string) => void;
|
||||
} = $props();
|
||||
|
||||
let container = $state<HTMLDivElement | null>(null);
|
||||
let view: EditorView | null = null;
|
||||
|
||||
/** 根据文件名后缀选语言扩展 */
|
||||
function langExtension(name: string): Extension[] {
|
||||
const ext = name.split(".").pop()?.toLowerCase() ?? "";
|
||||
switch (ext) {
|
||||
case "md":
|
||||
case "markdown":
|
||||
return [markdown()];
|
||||
case "js":
|
||||
case "mjs":
|
||||
case "cjs":
|
||||
return [javascript()];
|
||||
case "ts":
|
||||
case "mts":
|
||||
case "cts":
|
||||
return [javascript({ typescript: true })];
|
||||
case "jsx":
|
||||
return [javascript({ jsx: true })];
|
||||
case "tsx":
|
||||
return [javascript({ jsx: true, typescript: true })];
|
||||
case "json":
|
||||
case "jsonc":
|
||||
return [json()];
|
||||
case "html":
|
||||
case "htm":
|
||||
case "svelte":
|
||||
case "vue":
|
||||
return [html()];
|
||||
case "css":
|
||||
case "scss":
|
||||
return [css()];
|
||||
case "typ":
|
||||
case "typst":
|
||||
return [typst()];
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
if (!container) return;
|
||||
const extensions: Extension[] = [
|
||||
basicSetup,
|
||||
...langExtension(filename),
|
||||
EditorView.lineWrapping,
|
||||
EditorView.updateListener.of((update) => {
|
||||
if (update.docChanged) {
|
||||
onchange?.(update.state.doc.toString());
|
||||
}
|
||||
}),
|
||||
];
|
||||
if (readonly) extensions.push(EditorState.readOnly.of(true));
|
||||
|
||||
view = new EditorView({
|
||||
state: EditorState.create({ doc: value, extensions }),
|
||||
parent: container,
|
||||
});
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
view?.destroy();
|
||||
view = null;
|
||||
});
|
||||
|
||||
// 外部 value 变化时(如冲突载入最新),替换编辑器内容。
|
||||
$effect(() => {
|
||||
if (view && view.state.doc.toString() !== value) {
|
||||
view.dispatch({
|
||||
changes: { from: 0, to: view.state.doc.length, insert: value },
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<div bind:this={container} class="code-editor-wrapper"></div>
|
||||
|
||||
<style>
|
||||
.code-editor-wrapper {
|
||||
border: 1px solid var(--color-line);
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
font-size: 13px;
|
||||
line-height: 1.65;
|
||||
}
|
||||
.code-editor-wrapper :global(.cm-editor) {
|
||||
height: 100%;
|
||||
max-height: 60vh;
|
||||
overflow: auto;
|
||||
}
|
||||
.code-editor-wrapper :global(.cm-editor.cm-focused) {
|
||||
outline: none;
|
||||
}
|
||||
.code-editor-wrapper :global(.cm-scroller) {
|
||||
overflow: auto;
|
||||
}
|
||||
</style>
|
||||
@@ -1,11 +1,22 @@
|
||||
<script lang="ts">
|
||||
/**
|
||||
* 文件编辑器(模态框形式)。打开后加载文件内容并使用 CodeMirror 编辑,
|
||||
* 支持语法高亮、版本冲突处理、历史查看与删除。
|
||||
*/
|
||||
import { api, ApiError } from "./api.js";
|
||||
import { toastOk, toastErr, toast } from "./stores.js";
|
||||
import type { FileContent, VersionInfo, Role } from "./types.js";
|
||||
import Modal from "./Modal.svelte";
|
||||
import Icon from "./Icon.svelte";
|
||||
import CodeEditor from "./CodeEditor.svelte";
|
||||
|
||||
let { projectId, path, role, onchanged, onclose }: { projectId: string; path: string; role: Role; onchanged: () => void; onclose?: () => void } = $props();
|
||||
let { projectId, path, role, onchanged, onclose }: {
|
||||
projectId: string;
|
||||
path: string;
|
||||
role: Role;
|
||||
onchanged: () => void;
|
||||
onclose: () => void;
|
||||
} = $props();
|
||||
|
||||
let file = $state<FileContent | null>(null);
|
||||
let draft = $state("");
|
||||
@@ -15,6 +26,7 @@
|
||||
let history = $state<VersionInfo[]>([]);
|
||||
|
||||
const canEdit = $derived(role !== "VIEW");
|
||||
const filename = $derived(path.split("/").pop() ?? "");
|
||||
|
||||
async function load(): Promise<void> {
|
||||
try {
|
||||
@@ -35,6 +47,11 @@
|
||||
|
||||
async function save(): Promise<void> {
|
||||
if (file === null) return;
|
||||
// 内容未变化时不提交,避免产生空 commit。
|
||||
if (draft === file.content) {
|
||||
toastOk("内容无变化,未提交");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const r = await api<{ version: string }>(`/database/api/projects/${projectId}/file/commits`, {
|
||||
method: "POST",
|
||||
@@ -81,6 +98,7 @@
|
||||
toastOk("已删除");
|
||||
file = null;
|
||||
onchanged();
|
||||
onclose();
|
||||
} catch (e) {
|
||||
toastErr(e instanceof Error ? e.message : String(e));
|
||||
}
|
||||
@@ -104,58 +122,80 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if loadError}
|
||||
<div class="panel text-xs text-danger">{loadError}</div>
|
||||
{:else if file}
|
||||
<div class="panel">
|
||||
<div class="mb-2.5 flex items-center justify-between">
|
||||
<span class="file-meta">{file.path} @ {file.version}</span>
|
||||
<!-- 主编辑器模态框:宽屏 overlay -->
|
||||
<div
|
||||
class="fixed inset-0 z-40 flex items-center justify-center bg-black/30 p-4"
|
||||
role="presentation"
|
||||
onclick={(e) => { if (e.target === e.currentTarget) onclose(); }}
|
||||
>
|
||||
<div class="flex h-[85vh] w-full max-w-[900px] flex-col rounded-2xl border border-line-soft bg-panel shadow-[0_4px_20px_rgba(26,26,24,.07)]">
|
||||
<!-- 顶栏 -->
|
||||
<div class="flex shrink-0 items-center justify-between border-b border-line-soft px-6 py-4">
|
||||
<div class="flex min-w-0 items-center gap-3">
|
||||
<span class="text-[15px] font-semibold text-ink truncate">{filename}</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-1.5">
|
||||
<a class="btn" href="/database/api/projects/{projectId}/file/raw?path={encodeURIComponent(file.path)}" download>下载</a>
|
||||
{#if file}
|
||||
<a class="btn" href="/database/api/projects/{projectId}/file/raw?path={encodeURIComponent(file.path)}" download>
|
||||
<Icon name="download" size={13} /> 下载
|
||||
</a>
|
||||
<button class="btn" onclick={openHistory}><Icon name="clock" size={13} /> 历史</button>
|
||||
{#if canEdit}
|
||||
<button class="btn btn-danger" onclick={remove}><Icon name="trash" size={13} /> 删除文件</button>
|
||||
<button class="btn btn-danger" onclick={remove}><Icon name="trash" size={13} /> 删除</button>
|
||||
{/if}
|
||||
{#if onclose}
|
||||
<button class="btn !px-2.5" onclick={onclose} title="关闭预览" aria-label="关闭预览">✕</button>
|
||||
{/if}
|
||||
<button class="btn !px-2.5" onclick={onclose} title="关闭" aria-label="关闭编辑器">✕</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if file.encoding === "base64"}
|
||||
<!-- 编辑器主体 -->
|
||||
<div class="flex-1 overflow-y-auto px-6 py-4">
|
||||
{#if loadError}
|
||||
<div class="text-xs text-danger">{loadError}</div>
|
||||
{:else if file === null}
|
||||
<div class="quiet">加载中…</div>
|
||||
{:else if file.encoding === "base64"}
|
||||
<div class="quiet">二进制文件({file.size} B),不支持在线编辑</div>
|
||||
{:else}
|
||||
<textarea rows="14" class="textarea !leading-7" bind:value={draft} readonly={!canEdit}></textarea>
|
||||
{/if}
|
||||
|
||||
{#if canEdit && file.encoding !== "base64"}
|
||||
<div class="mt-3 flex justify-end">
|
||||
<button class="btn btn-primary" onclick={save}>提交修改</button>
|
||||
</div>
|
||||
<CodeEditor value={draft} filename={path} readonly={!canEdit} onchange={(v) => (draft = v)} />
|
||||
{/if}
|
||||
|
||||
{#if conflict}
|
||||
<div class="mt-3.5 rounded-xl border border-[#E8E2C8] bg-[#FCFBF4] p-4">
|
||||
<div class="mb-2 text-[13px] font-semibold text-[#6E6329]">冲突:他人已提交 {conflict.currentVersion},差异如下(你的基版 → 最新版)</div>
|
||||
<div class="mt-4 rounded-xl border border-[#E8E2C8] bg-[#FCFBF4] p-4">
|
||||
<div class="mb-2 text-[13px] font-semibold text-[#6E6329]">冲突:他人已提交 {conflict.currentVersion},差异如下</div>
|
||||
<pre class="diff rounded-lg border border-line-soft bg-panel p-3">{@html renderDiff(conflict.diff)}</pre>
|
||||
<div class="mt-2 text-[11.5px] text-[#8A8059]">请人工合并后,以最新内容为全文重新提交(基版将更新为 {conflict.currentVersion})</div>
|
||||
<div class="mt-2 text-[11.5px] text-[#8A8059]">请人工合并后重新提交(基版已更新为 {conflict.currentVersion})</div>
|
||||
<div class="mt-2 flex justify-end">
|
||||
<button class="btn" onclick={acceptLatest}>载入最新内容</button>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{:else}
|
||||
<div class="panel quiet">加载中…</div>
|
||||
|
||||
<!-- 底栏 -->
|
||||
{#if canEdit && file && file.encoding !== "base64"}
|
||||
<div class="flex shrink-0 justify-end border-t border-line-soft px-6 py-3">
|
||||
<button class="btn btn-primary" onclick={save}>提交修改</button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if showHistory}
|
||||
<Modal title="版本历史" onclose={() => (showHistory = false)}>
|
||||
<div class="max-h-80 overflow-y-auto">
|
||||
{#each history as v (v.version)}
|
||||
<div class="border-t border-line-soft py-2 text-xs first:border-t-0">
|
||||
<span class="font-mono text-accent">{v.version}</span> {v.message}
|
||||
<div class="text-ink-3">{new Date(v.committedAt).toLocaleString("zh-CN")}{v.author ? " · " + v.author : ""}</div>
|
||||
<div class="flex items-center gap-2.5 border-t border-line-soft py-2.5 first:border-t-0">
|
||||
<span
|
||||
class="inline-flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-accent text-[11px] font-semibold text-white"
|
||||
aria-hidden="true"
|
||||
>{(v.author ?? "?").slice(0, 1).toUpperCase()}</span>
|
||||
<div class="min-w-0 flex-1">
|
||||
<p class="text-[12.5px] text-ink">{v.message}</p>
|
||||
<p class="text-[11px] text-ink-3">
|
||||
{#if v.author}<span>{v.author}</span> · {/if}{new Date(v.committedAt).toLocaleString("zh-CN")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
@@ -31,6 +31,39 @@
|
||||
|
||||
const canEdit = $derived(node.role !== "VIEW");
|
||||
|
||||
/**
|
||||
* 当前浏览目录("" = 根,否则以 "/" 结尾)。文件夹是从扁平 path 列表派生的
|
||||
* 虚拟层(ADR-0030 —— git 版本存储里只有文件,没有目录对象),不对应任何
|
||||
* 独立的后端资源,纯前端按 "/" 分段分组即可,无需新增接口。
|
||||
*/
|
||||
let currentDir = $state("");
|
||||
let viewMode = $state<"list" | "grid">(loadViewMode());
|
||||
|
||||
function restoreCurrentDir(nodeId: string): string {
|
||||
try {
|
||||
return sessionStorage.getItem(`filelib.dir.${nodeId}`) ?? "";
|
||||
} catch {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
function loadViewMode(): "list" | "grid" {
|
||||
try {
|
||||
return localStorage.getItem("filelib.viewMode") === "grid" ? "grid" : "list";
|
||||
} catch {
|
||||
return "list";
|
||||
}
|
||||
}
|
||||
|
||||
function setViewMode(mode: "list" | "grid"): void {
|
||||
viewMode = mode;
|
||||
try {
|
||||
localStorage.setItem("filelib.viewMode", mode);
|
||||
} catch {
|
||||
// 隐私模式等场景下 localStorage 可能不可用;视图切换仍在当前会话内生效,只是不跨会话记忆。
|
||||
}
|
||||
}
|
||||
|
||||
async function loadFiles(): Promise<void> {
|
||||
try {
|
||||
const r = await api<{ files: FileEntry[] }>(`/database/api/projects/${node.id}/files`);
|
||||
@@ -47,12 +80,73 @@
|
||||
void loadFiles();
|
||||
});
|
||||
|
||||
// 切换项目时恢复对应项目的目录位置。
|
||||
$effect(() => {
|
||||
void node.id;
|
||||
currentDir = restoreCurrentDir(node.id);
|
||||
});
|
||||
|
||||
// currentDir 变化时持久化。
|
||||
$effect(() => {
|
||||
try {
|
||||
sessionStorage.setItem(`filelib.dir.${node.id}`, currentDir);
|
||||
} catch { /* ignore */ }
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
void loadConfig()
|
||||
.then((c) => (maxFileBytes = c.maxFileBytes))
|
||||
.catch(() => (maxFileBytes = null));
|
||||
});
|
||||
|
||||
interface FolderRow {
|
||||
readonly kind: "folder";
|
||||
readonly name: string;
|
||||
readonly path: string;
|
||||
}
|
||||
interface FileRow {
|
||||
readonly kind: "file";
|
||||
readonly name: string;
|
||||
readonly path: string;
|
||||
readonly size: number;
|
||||
}
|
||||
|
||||
/** 按当前目录分组:落在 currentDir 前缀下、第一段之后还有 "/" 的算子文件夹,否则是本层文件。 */
|
||||
const rows = $derived.by((): { folders: FolderRow[]; files: FileRow[] } | null => {
|
||||
if (files === null) return null;
|
||||
const folderNames = new Set<string>();
|
||||
const fileRows: FileRow[] = [];
|
||||
for (const f of files) {
|
||||
if (!f.path.startsWith(currentDir)) continue;
|
||||
const rest = f.path.slice(currentDir.length);
|
||||
const slash = rest.indexOf("/");
|
||||
if (slash === -1) fileRows.push({ kind: "file", name: rest, path: f.path, size: f.size });
|
||||
else folderNames.add(rest.slice(0, slash));
|
||||
}
|
||||
const folders = [...folderNames]
|
||||
.sort((a, b) => a.localeCompare(b))
|
||||
.map((name): FolderRow => ({ kind: "folder", name, path: `${currentDir}${name}/` }));
|
||||
fileRows.sort((a, b) => a.name.localeCompare(b.name));
|
||||
return { folders, files: fileRows };
|
||||
});
|
||||
|
||||
const breadcrumbSegs = $derived(currentDir === "" ? [] : currentDir.slice(0, -1).split("/"));
|
||||
|
||||
function enterFolder(path: string): void {
|
||||
currentDir = path;
|
||||
}
|
||||
|
||||
function goUp(): void {
|
||||
if (currentDir === "") return;
|
||||
const segs = currentDir.slice(0, -1).split("/");
|
||||
segs.pop();
|
||||
currentDir = segs.length === 0 ? "" : `${segs.join("/")}/`;
|
||||
}
|
||||
|
||||
function gotoBreadcrumb(index: number): void {
|
||||
currentDir = index < 0 ? "" : `${breadcrumbSegs.slice(0, index + 1).join("/")}/`;
|
||||
}
|
||||
|
||||
async function submitNewFile(): Promise<void> {
|
||||
const path = newPath.trim();
|
||||
if (path === "") return;
|
||||
@@ -94,7 +188,7 @@
|
||||
return;
|
||||
}
|
||||
pendingFile = file;
|
||||
uploadPath = `材料/${file.name}`;
|
||||
uploadPath = `${currentDir}${file.name}`;
|
||||
uploadMessage = "";
|
||||
}
|
||||
|
||||
@@ -135,7 +229,7 @@
|
||||
<div class="section-title">项目文件({files?.length ?? 0})</div>
|
||||
{#if canEdit}
|
||||
<div class="flex gap-1.5">
|
||||
<button class="btn" onclick={() => (showNewFile = true)}><Icon name="plus" size={13} /> 新建文件</button>
|
||||
<button class="btn" onclick={() => { newPath = currentDir; showNewFile = true; }}><Icon name="plus" size={13} /> 新建文件</button>
|
||||
<button class="btn btn-primary" onclick={() => uploadInput?.click()}>上传文件</button>
|
||||
<input bind:this={uploadInput} type="file" class="hidden" onchange={pickFile} />
|
||||
</div>
|
||||
@@ -148,20 +242,91 @@
|
||||
<div class="py-5 text-center text-xs text-danger">{loadError}</div>
|
||||
{:else if files && files.length === 0}
|
||||
<div class="quiet py-5 text-center">空仓库 · 可新建或上传文件</div>
|
||||
{:else if files}
|
||||
{:else if rows}
|
||||
<!-- 地址栏:上级 + 面包屑,与视图切换同一行,windows 资源管理器的标准布局 -->
|
||||
<div class="mb-2 flex items-center justify-between gap-2 border-b border-line-soft pb-2">
|
||||
<div class="flex min-w-0 items-center gap-0.5 overflow-x-auto text-[12.5px] text-ink-3">
|
||||
<button
|
||||
class="mr-0.5 flex h-6 w-6 shrink-0 items-center justify-center rounded-md text-ink-3 disabled:opacity-30 {currentDir !== '' ? 'hover:bg-hover hover:text-ink' : ''}"
|
||||
onclick={goUp}
|
||||
disabled={currentDir === ""}
|
||||
title="返回上级"
|
||||
aria-label="返回上级"
|
||||
><Icon name="arrowUp" size={13} /></button>
|
||||
<button class="shrink-0 rounded-md px-1.5 py-0.5 hover:bg-hover hover:text-ink" onclick={() => gotoBreadcrumb(-1)}>根目录</button>
|
||||
{#each breadcrumbSegs as seg, i (i)}
|
||||
<span class="shrink-0 text-line">/</span>
|
||||
<button class="shrink-0 truncate rounded-md px-1.5 py-0.5 hover:bg-hover hover:text-ink" onclick={() => gotoBreadcrumb(i)}>{seg}</button>
|
||||
{/each}
|
||||
</div>
|
||||
<div class="flex shrink-0 gap-1">
|
||||
<button
|
||||
class="flex h-[26px] w-[26px] items-center justify-center rounded-md {viewMode === 'list' ? 'bg-selected text-ink' : 'text-ink-3 hover:bg-hover hover:text-ink'}"
|
||||
onclick={() => setViewMode("list")}
|
||||
title="列表视图"
|
||||
aria-label="列表视图"
|
||||
aria-pressed={viewMode === "list"}
|
||||
><Icon name="viewList" size={14} /></button>
|
||||
<button
|
||||
class="flex h-[26px] w-[26px] items-center justify-center rounded-md {viewMode === 'grid' ? 'bg-selected text-ink' : 'text-ink-3 hover:bg-hover hover:text-ink'}"
|
||||
onclick={() => setViewMode("grid")}
|
||||
title="大图标视图"
|
||||
aria-label="大图标视图"
|
||||
aria-pressed={viewMode === "grid"}
|
||||
><Icon name="viewGrid" size={14} /></button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if rows.folders.length === 0 && rows.files.length === 0}
|
||||
<div class="quiet py-5 text-center">此文件夹为空</div>
|
||||
{:else if viewMode === "list"}
|
||||
<table class="list">
|
||||
<tbody>
|
||||
{#each files as f (f.path)}
|
||||
{#each rows.folders as folder (folder.path)}
|
||||
<tr
|
||||
class="cursor-pointer hover:bg-hover"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
onclick={() => enterFolder(folder.path)}
|
||||
onkeydown={(e) => e.key === "Enter" && enterFolder(folder.path)}
|
||||
>
|
||||
<td><span class="inline-flex items-center gap-2 text-[12.5px] text-ink"><span class="text-ink-3"><Icon name="folder" size={15} /></span>{folder.name}</span></td>
|
||||
<td class="file-meta text-right">—</td>
|
||||
</tr>
|
||||
{/each}
|
||||
{#each rows.files as f (f.path)}
|
||||
<tr
|
||||
class="cursor-pointer {$selectedFilePath === f.path ? 'bg-selected' : 'hover:bg-hover'}"
|
||||
onclick={() => selectedFilePath.set(f.path)}
|
||||
>
|
||||
<td class="font-mono text-[12.5px] text-ink">{f.path}</td>
|
||||
<td><span class="inline-flex items-center gap-2 font-mono text-[12.5px] text-ink"><span class="text-ink-3"><Icon name="file" size={14} /></span>{f.name}</span></td>
|
||||
<td class="file-meta text-right">{f.size} B</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
{:else}
|
||||
<div class="grid grid-cols-[repeat(auto-fill,minmax(84px,1fr))] gap-1 py-1">
|
||||
{#each rows.folders as folder (folder.path)}
|
||||
<button
|
||||
class="flex flex-col items-center gap-1.5 rounded-lg p-2.5 text-center hover:bg-hover"
|
||||
onclick={() => enterFolder(folder.path)}
|
||||
>
|
||||
<span class="text-ink-3"><Icon name="folder" size={34} /></span>
|
||||
<span class="line-clamp-2 w-full break-all text-[11.5px] text-ink">{folder.name}</span>
|
||||
</button>
|
||||
{/each}
|
||||
{#each rows.files as f (f.path)}
|
||||
<button
|
||||
class="flex flex-col items-center gap-1.5 rounded-lg p-2.5 text-center {$selectedFilePath === f.path ? 'bg-selected' : 'hover:bg-hover'}"
|
||||
onclick={() => selectedFilePath.set(f.path)}
|
||||
>
|
||||
<span class="text-ink-3"><Icon name="file" size={34} /></span>
|
||||
<span class="line-clamp-2 w-full break-all text-[11.5px] text-ink">{f.name}</span>
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
<script lang="ts">
|
||||
/**
|
||||
* 项目修改历史 tab:展示所有文件的提交记录(新→旧)。
|
||||
* 默认只显示 commit message + 作者头像 + 时间,点击可展开查看修改的文件。
|
||||
*/
|
||||
import { api } from "./api.js";
|
||||
import { toastErr } from "./stores.js";
|
||||
import type { ProjectCommitInfo, NodeDetail } from "./types.js";
|
||||
import Icon from "./Icon.svelte";
|
||||
|
||||
let { node }: { node: NodeDetail } = $props();
|
||||
|
||||
let history = $state<ProjectCommitInfo[] | null>(null);
|
||||
let loadError = $state<string | null>(null);
|
||||
let limit = $state(50);
|
||||
let expanded = $state<Set<string>>(new Set());
|
||||
|
||||
async function loadHistory(): Promise<void> {
|
||||
try {
|
||||
const r = await api<{ history: ProjectCommitInfo[] }>(
|
||||
`/database/api/projects/${node.id}/history?limit=${limit}`,
|
||||
);
|
||||
history = r.history;
|
||||
loadError = null;
|
||||
} catch (e) {
|
||||
loadError = e instanceof Error ? e.message : String(e);
|
||||
}
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
void node.id;
|
||||
void loadHistory();
|
||||
});
|
||||
|
||||
function toggle(version: string): void {
|
||||
const next = new Set(expanded);
|
||||
if (next.has(version)) next.delete(version);
|
||||
else next.add(version);
|
||||
expanded = next;
|
||||
}
|
||||
|
||||
function loadMore(): void {
|
||||
limit += 50;
|
||||
void loadHistory();
|
||||
}
|
||||
|
||||
function authorInitial(author: string | undefined): string {
|
||||
return (author ?? "?").slice(0, 1).toUpperCase();
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="panel">
|
||||
<div class="mb-3 section-title">修改历史</div>
|
||||
|
||||
{#if history === null && loadError === null}
|
||||
<div class="quiet py-5 text-center">加载中…</div>
|
||||
{:else if loadError}
|
||||
<div class="py-5 text-center text-xs text-danger">{loadError}</div>
|
||||
{:else if history && history.length === 0}
|
||||
<div class="quiet py-5 text-center">暂无提交记录</div>
|
||||
{:else if history}
|
||||
<div>
|
||||
{#each history as commit (commit.version)}
|
||||
{@const isOpen = expanded.has(commit.version)}
|
||||
<div class="border-t border-line-soft first:border-t-0">
|
||||
<button
|
||||
class="flex w-full items-center gap-2.5 py-3 text-left transition hover:bg-hover rounded-md px-1.5 -mx-1.5"
|
||||
onclick={() => toggle(commit.version)}
|
||||
aria-expanded={isOpen}
|
||||
>
|
||||
<!-- 头像 -->
|
||||
<span
|
||||
class="inline-flex h-7 w-7 shrink-0 items-center justify-center rounded-full bg-accent text-[12px] font-semibold text-white"
|
||||
aria-hidden="true"
|
||||
>{authorInitial(commit.author)}</span>
|
||||
<!-- 消息与时间 -->
|
||||
<div class="min-w-0 flex-1">
|
||||
<p class="text-[13px] text-ink">{commit.message}</p>
|
||||
<p class="mt-0.5 text-[11.5px] text-ink-3">
|
||||
{#if commit.author}<span>{commit.author}</span> · {/if}{new Date(commit.committedAt).toLocaleString("zh-CN")}
|
||||
</p>
|
||||
</div>
|
||||
<!-- 展开指示 -->
|
||||
<span class="shrink-0 text-ink-3 transition {isOpen ? 'rotate-90' : ''}">
|
||||
<Icon name="chevron" size={12} />
|
||||
</span>
|
||||
</button>
|
||||
|
||||
{#if isOpen}
|
||||
<div class="pb-3 pl-[46px]">
|
||||
<div class="flex flex-wrap gap-1">
|
||||
{#each commit.files as filePath (filePath)}
|
||||
<span class="inline-flex items-center gap-1 rounded-md bg-hover px-1.5 py-0.5 font-mono text-[11px] text-ink-2">
|
||||
<Icon name="file" size={10} />{filePath}
|
||||
</span>
|
||||
{/each}
|
||||
{#if commit.files.length === 0}
|
||||
<span class="text-[11.5px] text-ink-3">无文件变更信息</span>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
{#if history.length >= limit}
|
||||
<div class="mt-3 flex justify-center">
|
||||
<button class="btn" onclick={loadMore}>加载更多</button>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
@@ -20,6 +20,12 @@
|
||||
archive: "M3 8h18v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8Zm1-5h16l1 5H3l1-5Zm5 9h6",
|
||||
restore: "M3 12a9 9 0 1 0 3-6.7M3 4v4.5h4.5",
|
||||
download: "M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4M7 10l5 5 5-5M12 15V3",
|
||||
// Windows 资源管理器式文件浏览:文件夹/文件项与大小图标切换用。
|
||||
folder: "M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V7Z",
|
||||
file: "M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z M14 2v4a2 2 0 0 0 2 2h4",
|
||||
viewList: "M8 6h13M8 12h13M8 18h13M3 6h.01M3 12h.01M3 18h.01",
|
||||
viewGrid: "M4 4h7v7H4V4Zm9 0h7v7h-7V4ZM4 13h7v7H4v-7Zm9 0h7v7h-7v-7Z",
|
||||
arrowUp: "M12 19V5M5 12l7-7 7 7",
|
||||
} as const;
|
||||
|
||||
export type IconName = keyof typeof ICONS;
|
||||
|
||||
@@ -9,8 +9,8 @@
|
||||
import { api } from "./api.js";
|
||||
import { me, toastErr, toastOk } from "./stores.js";
|
||||
import { logout } from "./session.js";
|
||||
import { treeVersion, bumpTree, currentNode, selectedFilePath, clearSelectedFile, bumpFiles } from "./browser.js";
|
||||
import type { NodeChild } from "./types.js";
|
||||
import { treeVersion, bumpTree, currentNode, breadcrumb, selectedFilePath, clearSelectedFile, bumpFiles, restoreNodeId } from "./browser.js";
|
||||
import type { NodeChild, NodeDetail, BreadcrumbEntry } from "./types.js";
|
||||
import TreeNode from "./TreeNode.svelte";
|
||||
import NodeDetailPanel from "./NodeDetailPanel.svelte";
|
||||
import FileEditor from "./FileEditor.svelte";
|
||||
@@ -35,7 +35,22 @@
|
||||
}
|
||||
}
|
||||
|
||||
onMount(loadRoots);
|
||||
onMount(async () => {
|
||||
await loadRoots();
|
||||
// 刷新后恢复之前选中的节点。
|
||||
if (restoreNodeId) {
|
||||
try {
|
||||
const [detail, crumb] = await Promise.all([
|
||||
api<{ node: NodeDetail }>(`/database/api/nodes/${restoreNodeId}`),
|
||||
api<{ breadcrumb: BreadcrumbEntry[] }>(`/database/api/nodes/${restoreNodeId}/breadcrumb`),
|
||||
]);
|
||||
currentNode.set(detail.node);
|
||||
breadcrumb.set(crumb.breadcrumb);
|
||||
} catch {
|
||||
// 节点已删除或无权访问,静默忽略。
|
||||
}
|
||||
}
|
||||
});
|
||||
$effect(() => {
|
||||
void $treeVersion;
|
||||
void loadRoots();
|
||||
@@ -109,9 +124,8 @@
|
||||
<NodeDetailPanel />
|
||||
</main>
|
||||
|
||||
<!-- 右侧:文件预览/编辑栏(选中文件时出现) -->
|
||||
<!-- 文件编辑器(模态框) -->
|
||||
{#if $selectedFilePath && $currentNode?.kind === "PROJECT"}
|
||||
<section class="flex w-[46%] min-w-[420px] shrink-0 flex-col overflow-y-auto border-l border-line-soft bg-bg p-4">
|
||||
<FileEditor
|
||||
projectId={$currentNode.id}
|
||||
path={$selectedFilePath}
|
||||
@@ -119,7 +133,6 @@
|
||||
onchanged={bumpFiles}
|
||||
onclose={clearSelectedFile}
|
||||
/>
|
||||
</section>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,15 +1,21 @@
|
||||
<script lang="ts">
|
||||
import { api } from "./api.js";
|
||||
import { currentNode, breadcrumb, bumpTree, clearSelectedFile } from "./browser.js";
|
||||
import { currentNode, breadcrumb, bumpTree, clearSelectedFile, activeTab, restoreTab } from "./browser.js";
|
||||
import { toastOk, toastErr } from "./stores.js";
|
||||
import OverviewPanel from "./OverviewPanel.svelte";
|
||||
import FilesPanel from "./FilesPanel.svelte";
|
||||
import GrantsPanel from "./GrantsPanel.svelte";
|
||||
import HistoryPanel from "./HistoryPanel.svelte";
|
||||
import Modal from "./Modal.svelte";
|
||||
import Icon from "./Icon.svelte";
|
||||
|
||||
type Tab = "detail" | "files" | "grants";
|
||||
let tab = $state<Tab>("detail");
|
||||
type Tab = "detail" | "files" | "history" | "grants";
|
||||
const validTabs: readonly Tab[] = ["detail", "files", "history", "grants"];
|
||||
let tab = $state<Tab>((validTabs.includes(restoreTab as Tab) ? restoreTab as Tab : "detail"));
|
||||
/** 跟踪上一次见到的 node id,用于判断是否真正切换了节点。 */
|
||||
let prevNodeId: string | null = null;
|
||||
/** 首次恢复时不重置 tab。 */
|
||||
let restoredOnce = restoreTab !== null;
|
||||
let showCreateChild = $state(false);
|
||||
let newName = $state("");
|
||||
let newKind = $state<"FOLDER" | "PROJECT">("FOLDER");
|
||||
@@ -20,21 +26,38 @@
|
||||
const canManage = $derived(node?.role === "MANAGE");
|
||||
const canEdit = $derived(canManage || node?.role === "EDIT");
|
||||
|
||||
// 与旧 libraryBrowser 的 tab 组装一致:概览恒有;文件仅 PROJECT;授权仅 MANAGE
|
||||
// 与旧 libraryBrowser 的 tab 组装一致:概览恒有;文件仅 PROJECT;修改历史仅 PROJECT;授权仅 MANAGE
|
||||
// (FOLDER 也有授权 —— 它虽是透明组织节点,授权仍挂在节点上,ADR-0021)。
|
||||
const tabs = $derived.by((): ReadonlyArray<readonly [Tab, string]> => {
|
||||
const out: Array<readonly [Tab, string]> = [["detail", "概览"]];
|
||||
if (node?.kind === "PROJECT") out.push(["files", "文件"]);
|
||||
if (node?.kind === "PROJECT") out.push(["history", "修改历史"]);
|
||||
if (canManage) out.push(["grants", "授权"]);
|
||||
return out;
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
void node?.id;
|
||||
const id = node?.id ?? null;
|
||||
// node 还未加载或与上次相同时不做任何事。
|
||||
if (id === null || id === prevNodeId) return;
|
||||
prevNodeId = id;
|
||||
if (restoredOnce) {
|
||||
// 首次恢复(刷新后)保持 persisted tab,但要确保 tab 对当前节点有效。
|
||||
restoredOnce = false;
|
||||
const isProject = node?.kind === "PROJECT";
|
||||
if ((tab === "files" || tab === "history") && !isProject) tab = "detail";
|
||||
if (tab === "grants" && node?.role !== "MANAGE") tab = "detail";
|
||||
} else {
|
||||
tab = "detail";
|
||||
}
|
||||
clearSelectedFile();
|
||||
});
|
||||
|
||||
// tab 变化时同步到持久化 store。
|
||||
$effect(() => {
|
||||
activeTab.set(tab);
|
||||
});
|
||||
|
||||
async function createChild(): Promise<void> {
|
||||
const name = newName.trim();
|
||||
if (name === "" || node === null) return;
|
||||
@@ -129,6 +152,8 @@
|
||||
<GrantsPanel {node} />
|
||||
{:else if tab === "files" && node.kind === "PROJECT"}
|
||||
<FilesPanel {node} />
|
||||
{:else if tab === "history" && node.kind === "PROJECT"}
|
||||
<HistoryPanel {node} />
|
||||
{:else}
|
||||
<OverviewPanel {node} />
|
||||
{#if node.kind === "FOLDER"}
|
||||
|
||||
@@ -1,17 +1,62 @@
|
||||
import { writable } from "svelte/store";
|
||||
import { writable, get } from "svelte/store";
|
||||
import type { BreadcrumbEntry, NodeDetail } from "./types.js";
|
||||
|
||||
const STORAGE_KEY = "filelib.browser";
|
||||
|
||||
interface PersistedState {
|
||||
expanded: string[];
|
||||
currentNodeId: string | null;
|
||||
tab: string | null;
|
||||
selectedFilePath: string | null;
|
||||
}
|
||||
|
||||
function loadPersisted(): PersistedState {
|
||||
try {
|
||||
const raw = sessionStorage.getItem(STORAGE_KEY);
|
||||
if (raw) return JSON.parse(raw) as PersistedState;
|
||||
} catch { /* ignore */ }
|
||||
return { expanded: [], currentNodeId: null, tab: null, selectedFilePath: null };
|
||||
}
|
||||
|
||||
function savePersisted(): void {
|
||||
try {
|
||||
const state: PersistedState = {
|
||||
expanded: [...get(expanded)],
|
||||
currentNodeId: get(currentNode)?.id ?? null,
|
||||
tab: get(activeTab),
|
||||
selectedFilePath: get(selectedFilePath),
|
||||
};
|
||||
sessionStorage.setItem(STORAGE_KEY, JSON.stringify(state));
|
||||
} catch { /* sessionStorage 不可用时静默忽略 */ }
|
||||
}
|
||||
|
||||
const persisted = loadPersisted();
|
||||
|
||||
/** 树展开集合 / 当前选中节点 / 面包屑 / 树刷新计数。 */
|
||||
export const expanded = writable<Set<string>>(new Set());
|
||||
export const expanded = writable<Set<string>>(new Set(persisted.expanded));
|
||||
export const currentNode = writable<NodeDetail | null>(null);
|
||||
export const breadcrumb = writable<BreadcrumbEntry[]>([]);
|
||||
export const treeVersion = writable(0);
|
||||
|
||||
/** 刷新后需要恢复的节点 ID;LibraryView onMount 消费后清空。 */
|
||||
export const restoreNodeId = persisted.currentNodeId;
|
||||
/** 刷新后需要恢复的 tab;NodeDetailPanel 消费。 */
|
||||
export const restoreTab = persisted.tab;
|
||||
|
||||
/** 右侧预览栏:当前选中文件路径(项目内);切换节点时清空。 */
|
||||
export const selectedFilePath = writable<string | null>(null);
|
||||
export const selectedFilePath = writable<string | null>(persisted.selectedFilePath);
|
||||
/** 文件列表刷新计数(编辑器保存/删除后 bump,列表随之重载)。 */
|
||||
export const filesVersion = writable(0);
|
||||
|
||||
/** 当前激活的 tab(由 NodeDetailPanel 写入,持久化用)。 */
|
||||
export const activeTab = writable<string | null>(persisted.tab);
|
||||
|
||||
// 订阅需要持久化的 store,变化时写 sessionStorage。
|
||||
expanded.subscribe(() => savePersisted());
|
||||
currentNode.subscribe(() => savePersisted());
|
||||
selectedFilePath.subscribe(() => savePersisted());
|
||||
activeTab.subscribe(() => savePersisted());
|
||||
|
||||
export function bumpTree(): void {
|
||||
treeVersion.update((v) => v + 1);
|
||||
}
|
||||
|
||||
@@ -63,6 +63,15 @@ export interface VersionInfo {
|
||||
readonly committedAt: string;
|
||||
}
|
||||
|
||||
/** 项目级提交历史条目(包含受影响文件路径)。 */
|
||||
export interface ProjectCommitInfo {
|
||||
readonly version: string;
|
||||
readonly message: string;
|
||||
readonly author?: string;
|
||||
readonly committedAt: string;
|
||||
readonly files: readonly string[];
|
||||
}
|
||||
|
||||
export interface ExportJob {
|
||||
readonly id: string;
|
||||
readonly nodeId: string;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { sveltekit } from "@sveltejs/kit/vite";
|
||||
import tailwindcss from "@tailwindcss/vite";
|
||||
import wasm from "vite-plugin-wasm";
|
||||
import { defineConfig } from "vite";
|
||||
|
||||
// 老师端 /app + 管理后台 /database/* 的唯一前端工程;构建产物由 hub 后端静态托管。
|
||||
@@ -8,7 +9,7 @@ import { defineConfig } from "vite";
|
||||
const backend = "http://127.0.0.1:8788";
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [tailwindcss(), sveltekit()],
|
||||
plugins: [wasm(), tailwindcss(), sveltekit()],
|
||||
server: {
|
||||
port: 5173,
|
||||
proxy: {
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
|
||||
import { FileLibError } from "./model.js";
|
||||
import { FILE_LIB_AUDIT_ACTIONS, writeFileLibAudit } from "./audit.js";
|
||||
import type { CommitResult, FileEntry, VersionInfo, VersionStore } from "./versionStore.js";
|
||||
import type { CommitResult, FileEntry, VersionInfo, VersionStore, ProjectCommitInfo } from "./versionStore.js";
|
||||
import type { AccessDeps, FileLibActor } from "./treeService.js";
|
||||
import { requireAccessInTx } from "./treeService.js";
|
||||
import type { FileLibNode, PrismaClient } from "@prisma/client";
|
||||
@@ -228,6 +228,17 @@ export async function fileHistory(
|
||||
return deps.versionStore.history(project.storageDir, filePath, limit);
|
||||
}
|
||||
|
||||
/** 项目级提交历史(所有文件),VIEW 即可访问。 */
|
||||
export async function projectHistory(
|
||||
deps: FileDeps,
|
||||
actor: FileLibActor,
|
||||
projectId: string,
|
||||
limit?: number,
|
||||
): Promise<readonly ProjectCommitInfo[]> {
|
||||
const project = await requireProject(deps, actor, projectId, "VIEW");
|
||||
return deps.versionStore.projectHistory(project.storageDir, limit);
|
||||
}
|
||||
|
||||
export async function diffFile(
|
||||
deps: FileDeps,
|
||||
actor: FileLibActor,
|
||||
|
||||
@@ -20,6 +20,7 @@ import type {
|
||||
CommitRequest,
|
||||
CommitResult,
|
||||
FileEntry,
|
||||
ProjectCommitInfo,
|
||||
VersionId,
|
||||
VersionInfo,
|
||||
VersionStore,
|
||||
@@ -404,5 +405,39 @@ export function createGitVersionStore(): VersionStore {
|
||||
}
|
||||
return out; // git log 已是新→旧
|
||||
},
|
||||
|
||||
async projectHistory(projectDir, limit) {
|
||||
await requireRepo(projectDir);
|
||||
// %x1e 放在 format 开头作为每条记录的分隔符;--name-only 的文件列表跟在 format 行之后。
|
||||
const args = ["log", "--format=%x1e%H%x1f%an%x1f%aI%x1f%s", "--name-only"];
|
||||
if (limit !== undefined) args.push(`-${limit}`);
|
||||
const res = await runGit(projectDir, args);
|
||||
if (res.code !== 0) return []; // 空仓库
|
||||
const out: ProjectCommitInfo[] = [];
|
||||
// split 按 \x1e 分块,第一个空块跳过。
|
||||
for (const block of res.stdout.toString("utf8").split("\x1e")) {
|
||||
const trimmed = block.trim();
|
||||
if (trimmed === "") continue;
|
||||
const lines = trimmed.split("\n");
|
||||
const header = lines[0];
|
||||
if (header === undefined) continue;
|
||||
const [version, author, committedAt, message] = header.split("\x1f");
|
||||
if (version === undefined) continue;
|
||||
// header 之后的非空行是受影响的文件路径。
|
||||
// git 对含特殊字符的路径加双引号,去掉外层引号即可。
|
||||
const files = lines.slice(1)
|
||||
.map((l) => l.trim())
|
||||
.filter((l) => l !== "")
|
||||
.map((l) => (l.startsWith('"') && l.endsWith('"') ? l.slice(1, -1) : l));
|
||||
out.push({
|
||||
version,
|
||||
message: message ?? "",
|
||||
author: author === undefined || author === "" ? undefined : author,
|
||||
committedAt: committedAt ?? "",
|
||||
files,
|
||||
});
|
||||
}
|
||||
return out;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -45,6 +45,16 @@ export interface VersionInfo {
|
||||
readonly committedAt: string;
|
||||
}
|
||||
|
||||
/** 项目级提交历史条目(包含受影响文件路径)。 */
|
||||
export interface ProjectCommitInfo {
|
||||
readonly version: VersionId;
|
||||
readonly message: string;
|
||||
readonly author: string | undefined;
|
||||
readonly committedAt: string;
|
||||
/** 本次提交修改的文件路径列表。 */
|
||||
readonly files: readonly string[];
|
||||
}
|
||||
|
||||
export interface FileEntry {
|
||||
readonly path: string;
|
||||
readonly size: number;
|
||||
@@ -65,6 +75,8 @@ export interface VersionStore {
|
||||
): Promise<CommitResult>;
|
||||
diff(projectDir: string, filePath: string, from: VersionId, to: VersionId): Promise<string>;
|
||||
history(projectDir: string, filePath: string, limit?: number): Promise<VersionInfo[]>;
|
||||
/** 项目级提交历史(所有文件),新→旧排列。 */
|
||||
projectHistory(projectDir: string, limit?: number): Promise<ProjectCommitInfo[]>;
|
||||
}
|
||||
|
||||
interface StoredVersion {
|
||||
@@ -300,5 +312,24 @@ export function createInMemoryVersionStore(persistPath?: string): VersionStore {
|
||||
const ordered = infos.reverse();
|
||||
return limit !== undefined ? ordered.slice(0, limit) : ordered;
|
||||
},
|
||||
|
||||
async projectHistory(projectDir, limit) {
|
||||
const repo = requireRepo(projectDir);
|
||||
// 汇集所有文件的所有版本,按提交时间新→旧排序。
|
||||
const all: ProjectCommitInfo[] = [];
|
||||
for (const [filePath, chain] of repo.files) {
|
||||
for (const v of chain) {
|
||||
all.push({
|
||||
version: v.version,
|
||||
message: v.message,
|
||||
author: v.author,
|
||||
committedAt: v.committedAt,
|
||||
files: [filePath],
|
||||
});
|
||||
}
|
||||
}
|
||||
all.sort((a, b) => b.committedAt.localeCompare(a.committedAt));
|
||||
return limit !== undefined ? all.slice(0, limit) : all;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
diffFile,
|
||||
fileHistory,
|
||||
listFiles,
|
||||
projectHistory,
|
||||
readFile,
|
||||
readFileRaw,
|
||||
type FileContentEncoding,
|
||||
@@ -183,6 +184,22 @@ export async function registerFileRoutes(
|
||||
}
|
||||
});
|
||||
|
||||
app.get("/database/api/projects/:id/history", async (request, reply) => {
|
||||
const actor = await actorOrNull(request, reply, deps);
|
||||
if (actor === null) return reply;
|
||||
try {
|
||||
const { id } = request.params as { id: string };
|
||||
const rawLimit = (request.query as { limit?: string }).limit;
|
||||
const limit = rawLimit === undefined ? undefined : Number.parseInt(rawLimit, 10);
|
||||
if (limit !== undefined && (!Number.isSafeInteger(limit) || limit <= 0)) {
|
||||
throw new FileLibError(400, "invalid_request", "limit must be a positive integer");
|
||||
}
|
||||
return { history: await projectHistory(fileDeps, actor, id, limit) };
|
||||
} catch (error) {
|
||||
return sendRouteError(reply, error);
|
||||
}
|
||||
});
|
||||
|
||||
/* ------------------------------------------------------------ 导出(D10 异步) */
|
||||
|
||||
app.post("/database/api/projects/:id/exports", async (request, reply) => {
|
||||
|
||||
Reference in New Issue
Block a user