From 8d29da07804e20b29794ebcdfbffe5411f0e2c7b Mon Sep 17 00:00:00 2001 From: npmrun <1549469775@qq.com> Date: Sun, 19 Apr 2026 00:32:04 +0800 Subject: [PATCH] feat(image-crop): integrate vue-advanced-cropper for avatar and header icon uploads - Added ImageCropModal component for cropping images before upload. - Updated profile page to handle image cropping for avatar and header icon. - Included vue-advanced-cropper as a dependency in package.json and bun.lock. Made-with: Cursor --- app/components/ImageCropModal.vue | 156 ++++++++++++++++++++++++++++++++++++++ app/pages/me/profile/index.vue | 53 +++++++++++-- bun.lock | 9 +++ package.json | 1 + packages/drizzle-pkg/db.sqlite | Bin 143360 -> 147456 bytes 5 files changed, 211 insertions(+), 8 deletions(-) create mode 100644 app/components/ImageCropModal.vue diff --git a/app/components/ImageCropModal.vue b/app/components/ImageCropModal.vue new file mode 100644 index 0000000..3b76d80 --- /dev/null +++ b/app/components/ImageCropModal.vue @@ -0,0 +1,156 @@ + + + + + + + + + {{ variant === 'avatar' ? '头像将显示为圆形,建议把主体放在圆内。' : '顶栏图标为方形显示。' }} + + + + + + + 取消 + + + 确认并上传 + + + + + diff --git a/app/pages/me/profile/index.vue b/app/pages/me/profile/index.vue index e01e975..5638bfa 100644 --- a/app/pages/me/profile/index.vue +++ b/app/pages/me/profile/index.vue @@ -48,6 +48,16 @@ const uploadingHeaderIcon = ref(false) const avatarFileInput = ref(null) const headerIconFileInput = ref(null) +const imageCropOpen = ref(false) +const imageCropFile = ref(null) +const imageCropVariant = ref<'avatar' | 'headerIcon'>('avatar') + +watch(imageCropOpen, (isOpen) => { + if (!isOpen) { + imageCropFile.value = null + } +}) + const BIO_PREVIEW_MAX_CHARS = 200 const bioModalOpen = ref(false) const bioDraft = ref('') @@ -83,13 +93,31 @@ function openHeaderIconPicker() { headerIconFileInput.value?.click() } -async function onAvatarFileChange(ev: Event) { +function onAvatarFileChange(ev: Event) { + const input = ev.target as HTMLInputElement + const file = input.files?.[0] + input.value = '' + if (!file) { + return + } + imageCropVariant.value = 'avatar' + imageCropFile.value = file + imageCropOpen.value = true +} + +function onHeaderIconFileChange(ev: Event) { const input = ev.target as HTMLInputElement const file = input.files?.[0] input.value = '' if (!file) { return } + imageCropVariant.value = 'headerIcon' + imageCropFile.value = file + imageCropOpen.value = true +} + +async function uploadCroppedAvatar(file: File) { uploadingAvatar.value = true try { const form = new FormData() @@ -113,13 +141,7 @@ async function onAvatarFileChange(ev: Event) { } } -async function onHeaderIconFileChange(ev: Event) { - const input = ev.target as HTMLInputElement - const file = input.files?.[0] - input.value = '' - if (!file) { - return - } +async function uploadCroppedHeaderIcon(file: File) { uploadingHeaderIcon.value = true try { const form = new FormData() @@ -143,6 +165,14 @@ async function onHeaderIconFileChange(ev: Event) { } } +async function onImageCropConfirm(file: File) { + if (imageCropVariant.value === 'avatar') { + await uploadCroppedAvatar(file) + } else { + await uploadCroppedHeaderIcon(file) + } +} + async function load() { loading.value = true try { @@ -388,6 +418,13 @@ async function save() { + + `H3+c?r2R25sgsbHDntUR(gwlQtr}_ti?(-8 zf4Ka<)BEb_ec$_hzwi5fpYM0?ht9ow*&|K2K>+~JiiZ~uh)2iKjS>Nc)59;IMkjX+ zpfXA#AOAPL$V=QJcdQ(!+shTZoiv!{XgRK{sv0t)dOR-0rEo-vht!0wMRhHqClZ<( zizMYxBpQwCQB{&;O;3o1Dr%anCXA4-!P&}VU7*`h1=B;JNK%SOVKFWnx`HwEXhe&M zNm+?0vYZrUO)}!Ds7P9qq3U5>N@$5BHV})5vMOm3oIQPDv>dwQUD)VE=K%UIx`HmF zf1)LH9?he3<$&-Oa#19JPFpz2=QmBESTsV?11wF4G{Z1LVI>&X3^f=|hK-=@aWG=U zG&!s$V{$A8XaBON@H;sBy#BA_<)^n?MXlfGRQXij);d2;kt`%_L69WNx^Soo^a;Q& zeu6{#Kdxye2fa88Yo}=(Sla9JPOH9|^*+|;H18ZY$%mJc<+sK~7;uw*Sjc22@F3ZRo{AKHOKvBaBKPqi~L zWLTxm{Gj9wEFCH@&dpr-t+jOOwR|C$8Xv(oO<(NzA;rMWoR20+XH*Rd$ZOkNG2Wm;Ijz)RC_J4UEI<^};9V ziqZxjCyE<|VLg>kWG0Q=&}1r~il>cXAvInwMvR^WcHTluZ)X+`!H$XbBVU|nOe$t#{)pEpn2r?0t@piRX{Ey)6 zW`o;jUS*dl9LlsQx<}0>Zj{+XG2X3fRyO>HZz%8M`k__Y1iMTJda;qCo(2r$0QZvf zJ?9p>LOUQ=X%yjO6y-V&CMwT`;qE)@FyQsii6@Rq{omn5RJoFYF0r0(12i`XxVKRc z{}DgK4dRE|!?nd}4`7P{0@n;0&>=;MMkG~u`+rOp;kN~<{T4%X?e?$pi@#+%Wp&TN*LfN()s#HUmiJJz zU2dv{hx%&uf#?f>zOWPiF*^EygTY%MOP~1=QbLoPUIysb2zoy7bi3bm{oa*jmzjAc zM=#?ius{>}7VDWXY&ZMetF3ck*g@=WHapzwpu_re82ZhudqccltpjZ^VVzzFTdW0+@L8X(gY_X7JDi<}r&Ecc ze0pN!8prTrJe3(5({jJmGgH@@x3X#-Sr0c_NP)Dqb3F_aF4j7@9?HdySpD5JFXSd_ zSYCVNCXcH$uT^~?o|}lzSPKC-Ot{)B%Phe%)MbD!+wosQw|gMvXJIy%Nv6^UZI?`l zZmOF&8f)3>YL0gU!um90ke%qHkAARTt{|9>#lJ5Wj delta 964 zcma)5T}TvB6u$S)%+B1|oqN_=7*}i6%4*H6b=zH6#EhbsO!B2(B6O?STB_C5vU55uDtbuVVxS>VgrJ8gG79P?A(5iMD2iB#q-GF{qK7V=3-^5Ye0=9S_jYyB zuGi8)!2&dm5L#--{8~O(9wBJ@PSbVpx>y8(2`TWi$E=e@%#z{&Gb4d#*z1BR1m7VC zpWy?%gDEqwVI0hL7<8XXQP`?_sckH*=kGvD?lQ z7((z8hHS=Zrq2uq^`jZZ$PMF~MQ!b^>C9eRdFZRd&{SFK7g`Pnv5Qvb=@VI%C6m9d ztoc-^4`o+M;rd-GoD2G$>_YuxmO;3FyHiZyA%chS);4hzghf29#yZZb`kS#Li8#`1 zqvymCgs$br*K^S$6V^AtqPNt29x-wOfe2ZphrLh=gpj+O&d6V-7#$Nw$pz09v}y2k zk-39xBW9VP6Zxd#5>-{jr;b^xm*SJe91&k}Jj`Ipyy)h!ew#mTED7O#W{=WN0&SeF zP_hFl@?|7q1u3_r7gDWY2_k>i@`dpUp0xRl9|8$lqfxwZ)xRvYa?Mx{J2T?gB!X+O zg-zP7K4-?{Fuu?4z#e4W%;KFg2XzP@!8xe2;TkaN#_&CpZ}2UC1utuCDg@#LGZ
+ {{ variant === 'avatar' ? '头像将显示为圆形,建议把主体放在圆内。' : '顶栏图标为方形显示。' }} +