fix: 品牌设置页重写,匹配原版设计
Some checks failed
PR Preview / teardown-preview (pull_request) Has been skipped
Test / unit-test (push) Successful in 5s
Test / build-check (push) Successful in 4s
PR Preview / test (pull_request) Successful in 5s
PR Preview / deploy-preview (pull_request) Successful in 15s
Test / e2e-test (push) Failing after 1m27s
Some checks failed
PR Preview / teardown-preview (pull_request) Has been skipped
Test / unit-test (push) Successful in 5s
Test / build-check (push) Successful in 4s
PR Preview / test (pull_request) Successful in 5s
PR Preview / deploy-preview (pull_request) Successful in 15s
Test / e2e-test (push) Failing after 1m27s
- 三个上传框横排(二维码/背景图/Logo),100x100虚线框 - 点击上传,有预览,有清除按钮 - 上传前自动压缩图片(QR/Logo≤500KB,背景≤1MB) - 品牌名称+对齐方式(靠左/居中/靠右) - 配方卡片迷你预览(280x180,展示QR/Logo/背景/品牌名效果) - 加载时读取brand_align - 上传失败显示具体错误 - 重置file input允许重复选择同一文件 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -113,48 +113,76 @@
|
||||
<button class="btn-return" @click="goBackToRecipe">← 返回配方卡片</button>
|
||||
</div>
|
||||
<div class="section-card">
|
||||
<h4>🏷️ 品牌设置</h4>
|
||||
<p style="font-size:13px;color:var(--text-light);margin-bottom:16px">分享配方卡片时,二维码、背景图、Logo 会自动展示在卡片上</p>
|
||||
|
||||
<div class="form-group">
|
||||
<label>品牌名称</label>
|
||||
<input v-model="brandName" class="form-input" placeholder="您的品牌名称" @blur="saveBrandSettings" />
|
||||
</div>
|
||||
<!-- Three upload areas side by side -->
|
||||
<div style="display:flex;gap:20px;flex-wrap:wrap;margin-bottom:16px">
|
||||
<!-- QR Code -->
|
||||
<div>
|
||||
<label class="form-label">📱 二维码</label>
|
||||
<p style="font-size:11px;color:var(--text-light);margin-bottom:6px">卡片右上角展示</p>
|
||||
<div class="upload-box" @click="triggerUpload('qr')">
|
||||
<img v-if="brandQrImage" :src="brandQrImage" class="upload-box-img" />
|
||||
<span v-else class="upload-box-hint">点击上传</span>
|
||||
</div>
|
||||
<input ref="qrInput" type="file" accept="image/*" style="display:none" @change="handleUpload('qr', $event)" />
|
||||
<button v-if="brandQrImage" class="btn-clear" @click="clearBrandImage('qr')">清除</button>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>二维码链接</label>
|
||||
<input v-model="brandQrUrl" class="form-input" placeholder="https://..." @blur="saveBrandSettings" />
|
||||
<div v-if="brandQrUrl" class="qr-preview">
|
||||
<img :src="'https://api.qrserver.com/v1/create-qr-code/?size=120x120&data=' + encodeURIComponent(brandQrUrl)" alt="QR" class="qr-img" />
|
||||
<!-- Background -->
|
||||
<div>
|
||||
<label class="form-label">🖼 背景图</label>
|
||||
<p style="font-size:11px;color:var(--text-light);margin-bottom:6px">铺满整张卡片,半透明</p>
|
||||
<div class="upload-box" @click="triggerUpload('bg')">
|
||||
<img v-if="brandBg" :src="brandBg" class="upload-box-img" />
|
||||
<span v-else class="upload-box-hint">点击上传</span>
|
||||
</div>
|
||||
<input ref="bgInput" type="file" accept="image/*" style="display:none" @change="handleUpload('bg', $event)" />
|
||||
<button v-if="brandBg" class="btn-clear" @click="clearBrandImage('bg')">清除</button>
|
||||
</div>
|
||||
|
||||
<!-- Logo -->
|
||||
<div>
|
||||
<label class="form-label">🏷 Logo</label>
|
||||
<p style="font-size:11px;color:var(--text-light);margin-bottom:6px">卡片底部居中水印</p>
|
||||
<div class="upload-box" @click="triggerUpload('logo')">
|
||||
<img v-if="brandLogo" :src="brandLogo" class="upload-box-img" />
|
||||
<span v-else class="upload-box-hint">点击上传</span>
|
||||
</div>
|
||||
<input ref="logoInput" type="file" accept="image/*" style="display:none" @change="handleUpload('logo', $event)" />
|
||||
<button v-if="brandLogo" class="btn-clear" @click="clearBrandImage('logo')">清除</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Brand name -->
|
||||
<div class="form-group">
|
||||
<label>我的二维码图片</label>
|
||||
<div class="upload-area" @click="triggerUpload('qr')">
|
||||
<img v-if="brandQrImage" :src="brandQrImage" class="upload-preview qr-upload-preview" />
|
||||
<span v-else class="upload-hint">📲 点击上传二维码图片</span>
|
||||
<label class="form-label">品牌名称或标语(显示在二维码下方)</label>
|
||||
<textarea v-model="brandName" class="form-control" rows="2" placeholder="扫码申请成为优惠顾客 我的精油小屋" style="max-width:350px;font-size:13px" @blur="saveBrandSettings"></textarea>
|
||||
<div style="display:flex;gap:6px;margin-top:6px">
|
||||
<button class="btn-align" :class="{ active: brandAlign === 'left' }" @click="brandAlign='left'; saveBrandSettings()">靠左</button>
|
||||
<button class="btn-align" :class="{ active: brandAlign === 'center' }" @click="brandAlign='center'; saveBrandSettings()">居中</button>
|
||||
<button class="btn-align" :class="{ active: brandAlign === 'right' }" @click="brandAlign='right'; saveBrandSettings()">靠右</button>
|
||||
</div>
|
||||
<input ref="qrInput" type="file" accept="image/*" style="display:none" @change="handleUpload('qr', $event)" />
|
||||
<div class="field-hint">上传后将显示在配方卡片右下角</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>品牌Logo</label>
|
||||
<div class="upload-area" @click="triggerUpload('logo')">
|
||||
<img v-if="brandLogo" :src="brandLogo" class="upload-preview" />
|
||||
<span v-else class="upload-hint">点击上传Logo</span>
|
||||
<!-- Card Preview -->
|
||||
<div style="margin-bottom:16px">
|
||||
<label class="form-label">📋 配方卡片预览</label>
|
||||
<div class="card-preview-mini">
|
||||
<div v-if="brandBg" class="card-preview-bg" :style="{ backgroundImage: 'url(' + brandBg + ')' }"></div>
|
||||
<div class="card-preview-content">
|
||||
<div style="font-size:8px;letter-spacing:2px;color:var(--sage);margin-bottom:4px">doTERRA · RECIPE CARD</div>
|
||||
<div style="font-size:14px;font-weight:700;color:var(--text-dark)">配方名称</div>
|
||||
<div style="font-size:10px;color:var(--text-light);margin-top:2px">薰衣草 · 乳香 · 茶树</div>
|
||||
</div>
|
||||
<img v-if="brandQrImage" :src="brandQrImage" class="card-preview-qr" />
|
||||
<img v-if="brandLogo" :src="brandLogo" class="card-preview-logo" />
|
||||
<div class="card-preview-cost">💰 ¥12.50</div>
|
||||
<div v-if="brandName" class="card-preview-brand" :style="{ textAlign: brandAlign }">{{ brandName }}</div>
|
||||
</div>
|
||||
<input ref="logoInput" type="file" accept="image/*" style="display:none" @change="handleUpload('logo', $event)" />
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>卡片背景</label>
|
||||
<div class="upload-area" @click="triggerUpload('bg')">
|
||||
<img v-if="brandBg" :src="brandBg" class="upload-preview wide" />
|
||||
<span v-else class="upload-hint">点击上传背景图</span>
|
||||
</div>
|
||||
<input ref="bgInput" type="file" accept="image/*" style="display:none" @change="handleUpload('bg', $event)" />
|
||||
</div>
|
||||
<button class="btn btn-primary" @click="saveBrandSettings">💾 保存品牌设置</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user