up
This commit is contained in:
@@ -165,6 +165,64 @@ const navItems = computed(() => [
|
||||
{{ t("message.login_or_register") }}
|
||||
</RouterLink>
|
||||
</div>
|
||||
|
||||
<!-- Mobile user avatar (only when logged in) -->
|
||||
<div v-if="userStore.isLoggedIn" class="ml-3 md:hidden">
|
||||
<button
|
||||
class="rounded-md p-1.5 text-gray-500 hover:bg-gray-100 hover:text-gray-700 dark:hover:bg-dk-card dark:hover:text-dk-text"
|
||||
@click="userDropdownOpen = !userDropdownOpen"
|
||||
>
|
||||
<img
|
||||
:src="userStore.avatarUrl"
|
||||
class="h-7 w-7 rounded-full object-cover"
|
||||
alt="avatar"
|
||||
/>
|
||||
</button>
|
||||
<Transition
|
||||
enter-active-class="transition duration-100 ease-out"
|
||||
enter-from-class="transform scale-95 opacity-0"
|
||||
enter-to-class="transform scale-100 opacity-100"
|
||||
leave-active-class="transition duration-75 ease-in"
|
||||
leave-from-class="transform scale-100 opacity-100"
|
||||
leave-to-class="transform scale-95 opacity-0"
|
||||
>
|
||||
<div
|
||||
v-if="userDropdownOpen"
|
||||
class="absolute right-4 z-50 mt-1 w-48 rounded-lg border border-gray-200 bg-white py-1 shadow-lg dark:border-dk-muted dark:bg-dk-card"
|
||||
>
|
||||
<div class="px-4 py-2 text-sm text-gray-600 dark:text-dk-subtle">
|
||||
<div class="flex items-center gap-2">
|
||||
<img
|
||||
:src="userStore.avatarUrl"
|
||||
class="h-8 w-8 rounded-full object-cover"
|
||||
alt="avatar"
|
||||
/>
|
||||
<div class="truncate">{{ userStore.user?.Name || "" }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<hr class="my-1 border-gray-200 dark:border-dk-muted" />
|
||||
<RouterLink
|
||||
to="/settings/account"
|
||||
class="flex items-center gap-2 px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:text-dk-subtle dark:hover:bg-dk-muted"
|
||||
@click="userDropdownOpen = false"
|
||||
>
|
||||
<IconSettings :size="16" />
|
||||
{{ t("message.user_settings") }}
|
||||
</RouterLink>
|
||||
<hr class="my-1 border-gray-200 dark:border-dk-muted" />
|
||||
<button
|
||||
class="flex w-full items-center gap-2 px-4 py-2 text-sm text-red-600 hover:bg-red-50 dark:text-red-400 dark:hover:bg-red-900/20"
|
||||
@click="
|
||||
handleLogout();
|
||||
userDropdownOpen = false;
|
||||
"
|
||||
>
|
||||
<IconLogout :size="16" />
|
||||
{{ t("message.logout") }}
|
||||
</button>
|
||||
</div>
|
||||
</Transition>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Mobile menu -->
|
||||
@@ -208,27 +266,25 @@ const navItems = computed(() => [
|
||||
<IconMoon v-if="!isDark" :size="20" />
|
||||
<IconSun v-else :size="20" />
|
||||
</button>
|
||||
<div class="ml-auto">
|
||||
<RouterLink
|
||||
v-if="!userStore.isLoggedIn"
|
||||
to="/login"
|
||||
class="rounded-md bg-blue-600 px-3 py-1.5 text-sm font-medium text-white hover:bg-blue-700"
|
||||
@click="mobileMenuOpen = false"
|
||||
>
|
||||
{{ t("message.login_or_register") }}
|
||||
</RouterLink>
|
||||
<button
|
||||
v-else
|
||||
class="rounded-md px-3 py-1.5 text-sm text-red-600 hover:bg-red-50 dark:text-red-400"
|
||||
@click="
|
||||
handleLogout();
|
||||
mobileMenuOpen = false;
|
||||
"
|
||||
>
|
||||
{{ t("message.logout") }}
|
||||
</button>
|
||||
<div class="ml-auto">
|
||||
<RouterLink
|
||||
v-if="!userStore.isLoggedIn"
|
||||
to="/login"
|
||||
class="rounded-md bg-blue-600 px-3 py-1.5 text-sm font-medium text-white hover:bg-blue-700"
|
||||
@click="mobileMenuOpen = false"
|
||||
>
|
||||
{{ t("message.login_or_register") }}
|
||||
</RouterLink>
|
||||
<div v-else class="flex items-center gap-2 text-sm text-gray-600 dark:text-dk-subtle">
|
||||
<img
|
||||
:src="userStore.avatarUrl"
|
||||
class="h-6 w-6 rounded-full object-cover"
|
||||
alt="avatar"
|
||||
/>
|
||||
<span class="truncate">{{ userStore.user?.Name || "" }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</header>
|
||||
|
||||
@@ -11,7 +11,7 @@ var cor_size_height = 300;
|
||||
const is_have_URL = ref(false);
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => { initCropper(reader.result); };
|
||||
const emit = defineEmits(['crop_to_canvas'])
|
||||
const emit = defineEmits(['crop-data-url', 'crop-error'])
|
||||
onMounted(() => {
|
||||
cro_sele.value.$change(0, 0, cor_size_width, cor_size_height);
|
||||
cro_canv.value.style.width = cor_size_width.toString() + "px";
|
||||
@@ -32,7 +32,159 @@ function openFilePicker() {
|
||||
fileInput.click();
|
||||
}
|
||||
function getsele() {
|
||||
cro_canv.value.$toCanvas().then((a) => { emit('crop_to_canvas',a) });
|
||||
console.log('getsele called, checking elements:', {
|
||||
cro_canv: cro_canv.value,
|
||||
cro_sele: cro_sele.value,
|
||||
cro_imag: cro_imag.value,
|
||||
})
|
||||
|
||||
// 使用async函数处理异步裁剪逻辑
|
||||
const cropImage = async () => {
|
||||
try {
|
||||
console.log('Starting crop process')
|
||||
|
||||
// 方法1: 尝试调用原生的toCanvas方法(如果可用)
|
||||
if (cro_canv.value && typeof cro_canv.value.$toCanvas === 'function') {
|
||||
console.log('Using $toCanvas method')
|
||||
try {
|
||||
const result = await cro_canv.value.$toCanvas()
|
||||
console.log('$toCanvas result:', result ? 'Received data URL' : 'Empty result')
|
||||
if (result) {
|
||||
emit('crop-data-url', result)
|
||||
return // 成功,结束函数
|
||||
}
|
||||
console.log('$toCanvas returned empty, falling back to manual crop')
|
||||
} catch (error) {
|
||||
console.warn('$toCanvas failed, using manual crop:', error.message)
|
||||
}
|
||||
}
|
||||
|
||||
// 方法2: 使用手动裁剪方法
|
||||
console.log('Falling back to manual crop method')
|
||||
|
||||
// 获取图像元素
|
||||
const img = cro_imag.value
|
||||
if (!img) {
|
||||
console.error('No image element found')
|
||||
emit('crop-error', '未找到图像')
|
||||
return
|
||||
}
|
||||
|
||||
// 等待图像加载完成
|
||||
if (!img.complete) {
|
||||
console.log('Waiting for image to load...')
|
||||
await new Promise((resolve) => {
|
||||
img.onload = resolve
|
||||
img.onerror = resolve // 即使加载失败也继续
|
||||
// 设置超时,防止无限等待
|
||||
setTimeout(resolve, 3000)
|
||||
})
|
||||
}
|
||||
|
||||
if (!img.complete || img.naturalWidth === 0) {
|
||||
console.error('Image failed to load or has 0 dimensions')
|
||||
emit('crop-error', '图像加载失败')
|
||||
return
|
||||
}
|
||||
|
||||
console.log('Image loaded successfully:', img.naturalWidth, 'x', img.naturalHeight)
|
||||
|
||||
// 获取canvas的尺寸
|
||||
const canvasRect = cro_canv.value?.getBoundingClientRect?.() || {
|
||||
width: cor_size_width,
|
||||
height: cor_size_height,
|
||||
left: 0,
|
||||
top: 0
|
||||
}
|
||||
|
||||
console.log('Canvas rectangle:', canvasRect)
|
||||
|
||||
// 获取选择区域
|
||||
let selectionRect = null
|
||||
if (cro_sele.value && typeof cro_sele.value.$getRect === 'function') {
|
||||
selectionRect = cro_sele.value.$getRect()
|
||||
console.log('Selection rect:', selectionRect)
|
||||
}
|
||||
|
||||
// 验证选择区域
|
||||
if (!selectionRect || !selectionRect.width || !selectionRect.height) {
|
||||
selectionRect = {
|
||||
left: 0,
|
||||
top: 0,
|
||||
width: canvasRect.width,
|
||||
height: canvasRect.height
|
||||
}
|
||||
console.log('Using entire canvas as selection:', selectionRect)
|
||||
}
|
||||
|
||||
// 创建输出canvas
|
||||
const outputCanvas = document.createElement('canvas')
|
||||
outputCanvas.width = selectionRect.width
|
||||
outputCanvas.height = selectionRect.height
|
||||
const ctx = outputCanvas.getContext('2d')
|
||||
|
||||
// 设置白色背景
|
||||
ctx.fillStyle = '#ffffff'
|
||||
ctx.fillRect(0, 0, outputCanvas.width, outputCanvas.height)
|
||||
|
||||
// 计算图像在canvas中的显示方式
|
||||
const imgAspect = img.naturalWidth / img.naturalHeight
|
||||
const canvasAspect = canvasRect.width / canvasRect.height
|
||||
|
||||
let drawWidth, drawHeight, drawX, drawY
|
||||
|
||||
if (imgAspect > canvasAspect) {
|
||||
// 图像更宽,高度适配
|
||||
drawHeight = canvasRect.height
|
||||
drawWidth = canvasRect.height * imgAspect
|
||||
drawX = (canvasRect.width - drawWidth) / 2
|
||||
drawY = 0
|
||||
} else {
|
||||
// 图像更高,宽度适配
|
||||
drawWidth = canvasRect.width
|
||||
drawHeight = canvasRect.width / imgAspect
|
||||
drawX = 0
|
||||
drawY = (canvasRect.height - drawHeight) / 2
|
||||
}
|
||||
|
||||
console.log('Image display info:', { drawX, drawY, drawWidth, drawHeight })
|
||||
|
||||
// 计算裁剪区域(将选择区域转换到图像坐标)
|
||||
const cropX = Math.max(0, (selectionRect.left - drawX) / drawWidth * img.naturalWidth)
|
||||
const cropY = Math.max(0, (selectionRect.top - drawY) / drawHeight * img.naturalHeight)
|
||||
const cropWidth = Math.min(
|
||||
(selectionRect.width / drawWidth) * img.naturalWidth,
|
||||
img.naturalWidth - cropX
|
||||
)
|
||||
const cropHeight = Math.min(
|
||||
(selectionRect.height / drawHeight) * img.naturalHeight,
|
||||
img.naturalHeight - cropY
|
||||
)
|
||||
|
||||
console.log('Final crop coordinates (image space):', {
|
||||
cropX, cropY, cropWidth, cropHeight
|
||||
})
|
||||
|
||||
// 执行裁剪
|
||||
ctx.drawImage(
|
||||
img,
|
||||
cropX, cropY, cropWidth, cropHeight, // 源图像裁剪区域
|
||||
0, 0, outputCanvas.width, outputCanvas.height // 目标canvas区域
|
||||
)
|
||||
|
||||
// 生成data URL(JPEG格式,质量0.9)
|
||||
const dataUrl = outputCanvas.toDataURL('image/jpeg', 0.9)
|
||||
console.log('Generated crop data URL, length:', dataUrl.length)
|
||||
emit('crop-data-url', dataUrl)
|
||||
|
||||
} catch (error) {
|
||||
console.error('Crop process error:', error)
|
||||
emit('crop-error', '裁剪过程中发生错误:' + error.message)
|
||||
}
|
||||
}
|
||||
|
||||
// 执行裁剪
|
||||
cropImage()
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -182,6 +182,7 @@
|
||||
"my_account": "My Account",
|
||||
"profile_information": "Profile Information",
|
||||
"profile_picture": "Profile Picture",
|
||||
"account_information": "Account Information",
|
||||
"change_avatar": "Change Avatar",
|
||||
"change_email": "Change Email",
|
||||
"name": "Name",
|
||||
|
||||
@@ -182,6 +182,7 @@
|
||||
"my_account": "我的账户",
|
||||
"profile_information": "个人信息",
|
||||
"profile_picture": "个人头像",
|
||||
"account_information": "账户信息",
|
||||
"change_avatar": "更改头像",
|
||||
"change_email": "更改邮箱",
|
||||
"name": "姓名",
|
||||
|
||||
Reference in New Issue
Block a user