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>
|
<button class="btn-return" @click="goBackToRecipe">← 返回配方卡片</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="section-card">
|
<div class="section-card">
|
||||||
<h4>🏷️ 品牌设置</h4>
|
<p style="font-size:13px;color:var(--text-light);margin-bottom:16px">分享配方卡片时,二维码、背景图、Logo 会自动展示在卡片上</p>
|
||||||
|
|
||||||
<div class="form-group">
|
<!-- Three upload areas side by side -->
|
||||||
<label>品牌名称</label>
|
<div style="display:flex;gap:20px;flex-wrap:wrap;margin-bottom:16px">
|
||||||
<input v-model="brandName" class="form-input" placeholder="您的品牌名称" @blur="saveBrandSettings" />
|
<!-- QR Code -->
|
||||||
</div>
|
<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">
|
<!-- Background -->
|
||||||
<label>二维码链接</label>
|
<div>
|
||||||
<input v-model="brandQrUrl" class="form-input" placeholder="https://..." @blur="saveBrandSettings" />
|
<label class="form-label">🖼 背景图</label>
|
||||||
<div v-if="brandQrUrl" class="qr-preview">
|
<p style="font-size:11px;color:var(--text-light);margin-bottom:6px">铺满整张卡片,半透明</p>
|
||||||
<img :src="'https://api.qrserver.com/v1/create-qr-code/?size=120x120&data=' + encodeURIComponent(brandQrUrl)" alt="QR" class="qr-img" />
|
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Brand name -->
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>我的二维码图片</label>
|
<label class="form-label">品牌名称或标语(显示在二维码下方)</label>
|
||||||
<div class="upload-area" @click="triggerUpload('qr')">
|
<textarea v-model="brandName" class="form-control" rows="2" placeholder="扫码申请成为优惠顾客 我的精油小屋" style="max-width:350px;font-size:13px" @blur="saveBrandSettings"></textarea>
|
||||||
<img v-if="brandQrImage" :src="brandQrImage" class="upload-preview qr-upload-preview" />
|
<div style="display:flex;gap:6px;margin-top:6px">
|
||||||
<span v-else class="upload-hint">📲 点击上传二维码图片</span>
|
<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>
|
</div>
|
||||||
<input ref="qrInput" type="file" accept="image/*" style="display:none" @change="handleUpload('qr', $event)" />
|
|
||||||
<div class="field-hint">上传后将显示在配方卡片右下角</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<!-- Card Preview -->
|
||||||
<label>品牌Logo</label>
|
<div style="margin-bottom:16px">
|
||||||
<div class="upload-area" @click="triggerUpload('logo')">
|
<label class="form-label">📋 配方卡片预览</label>
|
||||||
<img v-if="brandLogo" :src="brandLogo" class="upload-preview" />
|
<div class="card-preview-mini">
|
||||||
<span v-else class="upload-hint">点击上传Logo</span>
|
<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>
|
</div>
|
||||||
<input ref="logoInput" type="file" accept="image/*" style="display:none" @change="handleUpload('logo', $event)" />
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<button class="btn btn-primary" @click="saveBrandSettings">💾 保存品牌设置</button>
|
||||||
<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>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -241,6 +269,7 @@ const brandQrUrl = ref('')
|
|||||||
const brandQrImage = ref('')
|
const brandQrImage = ref('')
|
||||||
const brandLogo = ref('')
|
const brandLogo = ref('')
|
||||||
const brandBg = ref('')
|
const brandBg = ref('')
|
||||||
|
const brandAlign = ref('center')
|
||||||
const logoInput = ref(null)
|
const logoInput = ref(null)
|
||||||
const bgInput = ref(null)
|
const bgInput = ref(null)
|
||||||
const qrInput = ref(null)
|
const qrInput = ref(null)
|
||||||
@@ -362,6 +391,7 @@ async function loadBrandSettings() {
|
|||||||
brandQrImage.value = data.qr_code || ''
|
brandQrImage.value = data.qr_code || ''
|
||||||
brandLogo.value = data.brand_logo || ''
|
brandLogo.value = data.brand_logo || ''
|
||||||
brandBg.value = data.brand_bg || ''
|
brandBg.value = data.brand_bg || ''
|
||||||
|
brandAlign.value = data.brand_align || 'center'
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// no brand settings yet
|
// no brand settings yet
|
||||||
@@ -374,7 +404,7 @@ async function saveBrandSettings() {
|
|||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
brand_name: brandName.value,
|
brand_name: brandName.value,
|
||||||
qr_url: brandQrUrl.value,
|
brand_align: brandAlign.value,
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
} catch {
|
} 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) {
|
async function handleUpload(type, event) {
|
||||||
const file = event.target.files[0]
|
const file = event.target.files[0]
|
||||||
if (!file) return
|
if (!file) return
|
||||||
try {
|
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 fieldMap = { logo: 'brand_logo', bg: 'brand_bg', qr: 'qr_code' }
|
||||||
const field = fieldMap[type]
|
const field = fieldMap[type]
|
||||||
if (!field) return
|
if (!field) return
|
||||||
@@ -414,9 +476,31 @@ async function handleUpload(type, event) {
|
|||||||
else if (type === 'bg') brandBg.value = base64
|
else if (type === 'bg') brandBg.value = base64
|
||||||
else if (type === 'qr') brandQrImage.value = base64
|
else if (type === 'qr') brandQrImage.value = base64
|
||||||
ui.showToast('上传成功')
|
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 {
|
} catch {
|
||||||
ui.showToast('上传失败')
|
ui.showToast('清除失败')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -853,6 +937,115 @@ async function applyBusiness() {
|
|||||||
color: #b0aab5;
|
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 {
|
.hint-text {
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
color: #6b6375;
|
color: #6b6375;
|
||||||
|
|||||||
Reference in New Issue
Block a user