diff --git a/package-lock.json b/package-lock.json
index 005c2e5fb5..e8c9d89e43 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -22,6 +22,7 @@
"chartjs-adapter-dayjs-4": "1.0.4",
"chartjs-plugin-zoom": "2.0.1",
"clippie": "4.1.3",
+ "cropperjs": "1.6.2",
"css-loader": "7.1.2",
"dayjs": "1.11.13",
"dropzone": "6.0.0-beta.2",
@@ -6787,6 +6788,12 @@
}
}
},
+ "node_modules/cropperjs": {
+ "version": "1.6.2",
+ "resolved": "https://registry.npmjs.org/cropperjs/-/cropperjs-1.6.2.tgz",
+ "integrity": "sha512-nhymn9GdnV3CqiEHJVai54TULFAE3VshJTXSqSJKa8yXAKyBKDWdhHarnlIPrshJ0WMFTGuFvG02YjLXfPiuOA==",
+ "license": "MIT"
+ },
"node_modules/cross-spawn": {
"version": "7.0.5",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.5.tgz",
diff --git a/package.json b/package.json
index c65f0617d8..a962c72c80 100644
--- a/package.json
+++ b/package.json
@@ -21,6 +21,7 @@
"chartjs-adapter-dayjs-4": "1.0.4",
"chartjs-plugin-zoom": "2.0.1",
"clippie": "4.1.3",
+ "cropperjs": "1.6.2",
"css-loader": "7.1.2",
"dayjs": "1.11.13",
"dropzone": "6.0.0-beta.2",
diff --git a/templates/user/settings/profile.tmpl b/templates/user/settings/profile.tmpl
index 9c7e2de218..829dfe97ff 100644
--- a/templates/user/settings/profile.tmpl
+++ b/templates/user/settings/profile.tmpl
@@ -127,6 +127,21 @@
+
+
+
预览
+
+
+
+
+
+
编辑
+
+
+
+
+
+
diff --git a/web_src/css/features/cropper.css b/web_src/css/features/cropper.css
new file mode 100644
index 0000000000..74cd9a2808
--- /dev/null
+++ b/web_src/css/features/cropper.css
@@ -0,0 +1,22 @@
+@import "cropperjs/dist/cropper.css";
+
+.cropper {
+ display: flex;
+ column-gap: 10px;
+}
+
+.hidden {
+ display: none;
+}
+
+.editor {
+ flex: 1;
+}
+
+#result {
+ overflow: hidden;
+ width: 256px;
+ height: 256px;
+ max-width: 256px;
+ max-height: 256px;
+}
diff --git a/web_src/css/index.css b/web_src/css/index.css
index 817f6997da..174a4a9cbc 100644
--- a/web_src/css/index.css
+++ b/web_src/css/index.css
@@ -40,6 +40,7 @@
@import "./features/codeeditor.css";
@import "./features/projects.css";
@import "./features/tribute.css";
+@import "./features/cropper.css";
@import "./features/console.css";
@import "./markup/content.css";
diff --git a/web_src/js/features/comp/Cropper.ts b/web_src/js/features/comp/Cropper.ts
new file mode 100644
index 0000000000..5f6bed27cb
--- /dev/null
+++ b/web_src/js/features/comp/Cropper.ts
@@ -0,0 +1,52 @@
+import Cropper from 'cropperjs';
+
+export function initCompCropper() {
+ if (!document.querySelector('#cropper')) {
+ return;
+ }
+
+ let filename;
+ const image = document.querySelector('#image');
+ const result = document.querySelector('#result');
+ const input = document.querySelector('#new-avatar');
+
+ const done = function (url) {
+ image.src = url;
+
+ const cropper = new Cropper(image, {
+ aspectRatio: 1,
+ viewMode: 1,
+ crop() {
+ const canvas = cropper.getCroppedCanvas();
+ result.src = canvas.toDataURL();
+ canvas.toBlob((blob) => {
+ const file = new File([blob], filename, {type: 'image/jpeg', lastModified: Date.now()});
+ const container = new DataTransfer();
+ container.items.add(file);
+ input.files = container.files;
+ });
+ },
+ });
+ document.querySelector('#cropper').classList.remove('hidden');
+ };
+
+ input.addEventListener('change', (e) => {
+ const files = e.target.files;
+
+ let reader;
+ let file;
+ if (files && files.length > 0) {
+ file = files[0];
+ filename = file.name;
+ if (URL) {
+ done(URL.createObjectURL(file));
+ } else if (FileReader) {
+ reader = new FileReader();
+ reader.addEventListener('load', () => {
+ done(reader.result);
+ });
+ reader.readAsDataURL(file);
+ }
+ }
+ });
+}
diff --git a/web_src/js/index.ts b/web_src/js/index.ts
index eeead37333..dcd2549554 100644
--- a/web_src/js/index.ts
+++ b/web_src/js/index.ts
@@ -49,6 +49,7 @@ import {initRepoRelease, initRepoReleaseNew} from './features/repo-release.ts';
import {initRepoEditor} from './features/repo-editor.ts';
import {initCompSearchUserBox} from './features/comp/SearchUserBox.ts';
import {initInstall} from './features/install.ts';
+import {initCompCropper} from './features/comp/Cropper.ts';
import {initCompWebHookEditor} from './features/comp/WebHookEditor.ts';
import {initRepoBranchButton} from './features/repo-branch.ts';
import {initCommonOrganization} from './features/common-organization.ts';
@@ -137,6 +138,7 @@ onDomReady(() => {
initCompSearchUserBox,
initCompWebHookEditor,
+ initCompCropper,
initInstall,