diff --git a/frontend/src/views/MyDiary.vue b/frontend/src/views/MyDiary.vue index c0f0715..3f5180f 100644 --- a/frontend/src/views/MyDiary.vue +++ b/frontend/src/views/MyDiary.vue @@ -113,48 +113,76 @@
-

🏷️ 品牌设置

+

分享配方卡片时,二维码、背景图、Logo 会自动展示在卡片上

-
- - -
+ +
+ +
+ +

卡片右上角展示

+
+ + 点击上传 +
+ + +
-
- - -
- QR + +
+ +

铺满整张卡片,半透明

+
+ + 点击上传 +
+ + +
+ + +
+ +

卡片底部居中水印

+
+ + 点击上传 +
+ +
+
- -
- - 📲 点击上传二维码图片 + + +
+ + +
- -
上传后将显示在配方卡片右下角
-
- -
- - 点击上传Logo + +
+ +
+
+
+
doTERRA · RECIPE CARD
+
配方名称
+
薰衣草 · 乳香 · 茶树
+
+ + +
💰 ¥12.50
+
{{ brandName }}
-
-
- -
- - 点击上传背景图 -
- -
+
@@ -241,6 +269,7 @@ const brandQrUrl = ref('') const brandQrImage = ref('') const brandLogo = ref('') const brandBg = ref('') +const brandAlign = ref('center') const logoInput = ref(null) const bgInput = ref(null) const qrInput = ref(null) @@ -362,6 +391,7 @@ async function loadBrandSettings() { brandQrImage.value = data.qr_code || '' brandLogo.value = data.brand_logo || '' brandBg.value = data.brand_bg || '' + brandAlign.value = data.brand_align || 'center' } } catch { // no brand settings yet @@ -374,7 +404,7 @@ async function saveBrandSettings() { method: 'PUT', body: JSON.stringify({ brand_name: brandName.value, - qr_url: brandQrUrl.value, + brand_align: brandAlign.value, }), }) } catch { @@ -397,11 +427,43 @@ function readFileAsBase64(file) { }) } +// Compress image if too large +function compressImage(base64, maxSize = 500000) { + return new Promise((resolve) => { + if (base64.length <= maxSize) { resolve(base64); return } + const img = new Image() + img.onload = () => { + const canvas = document.createElement('canvas') + let w = img.width, h = img.height + // Scale down if very large + const maxDim = 800 + if (w > maxDim || h > maxDim) { + const ratio = Math.min(maxDim / w, maxDim / h) + w = Math.round(w * ratio) + h = Math.round(h * ratio) + } + canvas.width = w + canvas.height = h + canvas.getContext('2d').drawImage(img, 0, 0, w, h) + let quality = 0.8 + let result = canvas.toDataURL('image/jpeg', quality) + while (result.length > maxSize && quality > 0.2) { + quality -= 0.1 + result = canvas.toDataURL('image/jpeg', quality) + } + resolve(result) + } + img.src = base64 + }) +} + async function handleUpload(type, event) { const file = event.target.files[0] if (!file) return try { - const base64 = await readFileAsBase64(file) + let base64 = await readFileAsBase64(file) + const maxSize = type === 'bg' ? 1000000 : 500000 + base64 = await compressImage(base64, maxSize) const fieldMap = { logo: 'brand_logo', bg: 'brand_bg', qr: 'qr_code' } const field = fieldMap[type] if (!field) return @@ -414,9 +476,31 @@ async function handleUpload(type, event) { else if (type === 'bg') brandBg.value = base64 else if (type === 'qr') brandQrImage.value = base64 ui.showToast('上传成功') + } else { + const err = await res.json().catch(() => ({})) + ui.showToast(err.detail || '上传失败') } + } catch (e) { + ui.showToast('上传失败: ' + (e.message || '')) + } + // Reset input so same file can be re-selected + event.target.value = '' +} + +async function clearBrandImage(type) { + const fieldMap = { logo: 'brand_logo', bg: 'brand_bg', qr: 'qr_code' } + const field = fieldMap[type] + try { + await api('/api/brand', { + method: 'PUT', + body: JSON.stringify({ [field]: null }), + }) + if (type === 'logo') brandLogo.value = '' + else if (type === 'bg') brandBg.value = '' + else if (type === 'qr') brandQrImage.value = '' + ui.showToast('已清除') } catch { - ui.showToast('上传失败') + ui.showToast('清除失败') } } @@ -853,6 +937,115 @@ async function applyBusiness() { color: #b0aab5; } +/* Upload box (matching initial commit style) */ +.upload-box { + width: 100px; + height: 100px; + border: 2px dashed var(--border, #e0d4c0); + border-radius: 12px; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + overflow: hidden; + background: white; + transition: border-color 0.15s; +} +.upload-box:hover { border-color: var(--sage, #7a9e7e); } +.upload-box-img { width: 100%; height: 100%; object-fit: cover; } +.upload-box-hint { font-size: 12px; color: var(--text-light, #9a8570); } +.btn-clear { + margin-top: 6px; + font-size: 11px; + background: none; + border: 1px solid var(--border); + border-radius: 6px; + padding: 2px 8px; + cursor: pointer; + color: var(--text-light); +} +.btn-clear:hover { border-color: #c0392b; color: #c0392b; } +.btn-align { + font-size: 11px; + padding: 3px 10px; + border: 1.5px solid var(--border); + border-radius: 6px; + background: white; + cursor: pointer; + color: var(--text-mid); +} +.btn-align.active { + background: var(--sage-mist); + border-color: var(--sage); + color: var(--sage-dark); +} + +/* Card preview mini */ +.card-preview-mini { + position: relative; + width: 280px; + height: 180px; + background: linear-gradient(145deg, #faf7f0, #f5ede0); + border-radius: 14px; + border: 1px solid #e0ccaa; + overflow: hidden; + font-family: 'Noto Serif SC', serif; + padding: 16px; +} +.card-preview-bg { + position: absolute; + inset: 0; + background-size: cover; + background-position: center; + opacity: 0.15; +} +.card-preview-content { position: relative; z-index: 1; } +.card-preview-qr { + position: absolute; + top: 10px; + right: 10px; + width: 40px; + height: 40px; + object-fit: cover; + border-radius: 4px; + z-index: 1; +} +.card-preview-logo { + position: absolute; + bottom: 40px; + left: 50%; + transform: translateX(-50%); + height: 20px; + width: auto; + object-fit: contain; + z-index: 1; +} +.card-preview-cost { + position: absolute; + bottom: 12px; + left: 16px; + right: 16px; + height: 24px; + background: linear-gradient(135deg, var(--sage), #5a7d5e); + border-radius: 6px; + display: flex; + align-items: center; + justify-content: center; + color: white; + font-size: 10px; + z-index: 1; +} +.card-preview-brand { + position: absolute; + bottom: 40px; + left: 16px; + right: 16px; + font-size: 9px; + color: var(--text-light); + z-index: 1; + white-space: pre-line; +} + .hint-text { font-size: 13px; color: #6b6375;