219 lines
9.5 KiB
Vue
219 lines
9.5 KiB
Vue
|
|
<template>
|
|||
|
|
<div class="design-panel">
|
|||
|
|
<!-- 背景色 -->
|
|||
|
|
<section>
|
|||
|
|
<h4>背景色</h4>
|
|||
|
|
<p class="hint">整副牌默认用这个颜色,个别牌可在下面单独覆盖</p>
|
|||
|
|
<div class="row">
|
|||
|
|
<input type="color" :value="design.background_color" @input="setBgColor($event.target.value)" />
|
|||
|
|
<input type="text" :value="design.background_color" @change="setBgColor($event.target.value)" />
|
|||
|
|
<button @click="uploadBg" class="mini">上传背景图</button>
|
|||
|
|
</div>
|
|||
|
|
<input ref="bgFileInput" type="file" accept="image/*" @change="onBgFile" hidden />
|
|||
|
|
<div v-if="design.background_image" class="row">
|
|||
|
|
<img :src="design.background_image" class="thumb" />
|
|||
|
|
<button @click="clearBgImage" class="mini">清除</button>
|
|||
|
|
</div>
|
|||
|
|
</section>
|
|||
|
|
|
|||
|
|
<!-- 边框 -->
|
|||
|
|
<section>
|
|||
|
|
<h4>边框</h4>
|
|||
|
|
<div class="row">
|
|||
|
|
<label class="mini-label">颜色</label>
|
|||
|
|
<input type="color" :value="design.border_color" @input="setBorder('border_color', $event.target.value)" />
|
|||
|
|
<label class="mini-label">粗细</label>
|
|||
|
|
<input type="number" min="0" max="40" :value="design.border_width"
|
|||
|
|
@change="setBorder('border_width', parseInt($event.target.value) || 0)" />
|
|||
|
|
</div>
|
|||
|
|
</section>
|
|||
|
|
|
|||
|
|
<!-- 花色符号 -->
|
|||
|
|
<section>
|
|||
|
|
<h4>花色符号</h4>
|
|||
|
|
<p class="hint">用系统字体显示一个 Unicode 符号;想用图片可在右侧素材库上传后切换</p>
|
|||
|
|
<div v-for="s in suits" :key="s" class="suit-row">
|
|||
|
|
<div class="suit-label">{{ suitSymbol(s) }} {{ suitLabel(s) }}</div>
|
|||
|
|
<input type="color" :value="design.suit_symbols?.[s]?.color || '#000000'"
|
|||
|
|
@input="setSuit(s, 'color', $event.target.value)" />
|
|||
|
|
<select :value="design.suit_symbols?.[s]?.type || 'text'"
|
|||
|
|
@change="setSuit(s, 'type', $event.target.value)">
|
|||
|
|
<option value="text">字体符号</option>
|
|||
|
|
<option value="image" :disabled="!design.suit_symbols?.[s]?.asset_id">图片素材</option>
|
|||
|
|
</select>
|
|||
|
|
</div>
|
|||
|
|
</section>
|
|||
|
|
|
|||
|
|
<!-- 尺寸 -->
|
|||
|
|
<section>
|
|||
|
|
<h4>大小比例</h4>
|
|||
|
|
<label class="row">
|
|||
|
|
<span>角标</span>
|
|||
|
|
<input type="range" min="0.08" max="0.20" step="0.01"
|
|||
|
|
:value="design.corner_size_ratio"
|
|||
|
|
@input="setDesign('corner_size_ratio', parseFloat($event.target.value))" />
|
|||
|
|
<span class="val">{{ Math.round(design.corner_size_ratio * 100) }}%</span>
|
|||
|
|
</label>
|
|||
|
|
<label class="row">
|
|||
|
|
<span>中心花色</span>
|
|||
|
|
<input type="range" min="0.08" max="0.24" step="0.01"
|
|||
|
|
:value="design.pip_size_ratio"
|
|||
|
|
@input="setDesign('pip_size_ratio', parseFloat($event.target.value))" />
|
|||
|
|
<span class="val">{{ Math.round(design.pip_size_ratio * 100) }}%</span>
|
|||
|
|
</label>
|
|||
|
|
</section>
|
|||
|
|
|
|||
|
|
<!-- 单牌覆盖 -->
|
|||
|
|
<section v-if="canHaveOverride">
|
|||
|
|
<h4>本牌特殊设置</h4>
|
|||
|
|
<p class="hint">只对当前选中的牌生效</p>
|
|||
|
|
<div class="row">
|
|||
|
|
<button @click="setOverrideBg" class="mini">设置独立背景色</button>
|
|||
|
|
<button v-if="hasOverride" @click="clearOverride" class="mini ghost">清除</button>
|
|||
|
|
</div>
|
|||
|
|
<div v-if="override?.background_color" class="row">
|
|||
|
|
<input type="color" :value="override.background_color"
|
|||
|
|
@input="patchOverride('background_color', $event.target.value)" />
|
|||
|
|
<input type="text" :value="override.background_color"
|
|||
|
|
@change="patchOverride('background_color', $event.target.value)" />
|
|||
|
|
</div>
|
|||
|
|
</section>
|
|||
|
|
|
|||
|
|
<!-- 数字牌花色位置微调 -->
|
|||
|
|
<section v-if="isNumberCard">
|
|||
|
|
<h4>数字牌花色位置微调</h4>
|
|||
|
|
<p class="hint">拖动滑块微调每个花色的位置(占整张牌的比例,0 = 默认)</p>
|
|||
|
|
<div class="rank-tabs">
|
|||
|
|
<button v-for="r in [1,2,3,4,5,6,7,8,9,10]" :key="r"
|
|||
|
|
:class="{ active: selectedRank === r }"
|
|||
|
|
@click="selectedRank = r">{{ r === 1 ? 'A' : r }}</button>
|
|||
|
|
</div>
|
|||
|
|
<div v-for="(pos, i) in positions" :key="i" class="pip-row">
|
|||
|
|
<div class="pip-label">#{{ i + 1 }}</div>
|
|||
|
|
<label class="mini-label">dx</label>
|
|||
|
|
<input type="range" min="-0.05" max="0.05" step="0.005"
|
|||
|
|
:value="overrideFor(i).dx" @input="setOffset(i, 'dx', $event.target.value)" />
|
|||
|
|
<label class="mini-label">dy</label>
|
|||
|
|
<input type="range" min="-0.05" max="0.05" step="0.005"
|
|||
|
|
:value="overrideFor(i).dy" @input="setOffset(i, 'dy', $event.target.value)" />
|
|||
|
|
<label class="mini-label">缩放</label>
|
|||
|
|
<input type="range" min="0.6" max="1.4" step="0.05"
|
|||
|
|
:value="overrideFor(i).scale" @input="setOffset(i, 'scale', $event.target.value)" />
|
|||
|
|
</div>
|
|||
|
|
<button @click="resetLayout" class="mini ghost">重置本点数布局</button>
|
|||
|
|
</section>
|
|||
|
|
</div>
|
|||
|
|
</template>
|
|||
|
|
|
|||
|
|
<script setup>
|
|||
|
|
import { ref, computed, watch } from 'vue'
|
|||
|
|
import { useProjectStore } from '@/stores/projectStore'
|
|||
|
|
import { SUITS, SUIT_TEXT, LAYOUT_POSITIONS, isJoker } from '@/utils/cardLayout'
|
|||
|
|
|
|||
|
|
const store = useProjectStore()
|
|||
|
|
const design = computed(() => store.effectiveDesign)
|
|||
|
|
const override = computed(() => {
|
|||
|
|
if (!store.project || !store.currentCard) return null
|
|||
|
|
return (store.project.card_overrides || {})[store.currentCard] || null
|
|||
|
|
})
|
|||
|
|
const hasOverride = computed(() => !!override.value)
|
|||
|
|
|
|||
|
|
const suits = SUITS
|
|||
|
|
const suitSymbol = (s) => SUIT_TEXT[s]
|
|||
|
|
const suitLabel = (s) => ({ spade: '黑桃', heart: '红桃', club: '梅花', diamond: '方块' })[s]
|
|||
|
|
|
|||
|
|
const bgFileInput = ref(null)
|
|||
|
|
function uploadBg() { bgFileInput.value?.click() }
|
|||
|
|
function onBgFile(e) {
|
|||
|
|
const f = e.target.files[0]
|
|||
|
|
if (!f) return
|
|||
|
|
const reader = new FileReader()
|
|||
|
|
reader.onload = (ev) => {
|
|||
|
|
// 这里只把 dataURL 临时存到 design;正式应上传到后端
|
|||
|
|
store.patchDesign('background_image', ev.target.result)
|
|||
|
|
}
|
|||
|
|
reader.readAsDataURL(f)
|
|||
|
|
}
|
|||
|
|
function clearBgImage() { store.patchDesign('background_image', null) }
|
|||
|
|
|
|||
|
|
function setBgColor(v) { store.patchDesign('background_color', v) }
|
|||
|
|
function setBorder(path, v) { store.patchDesign(path, v) }
|
|||
|
|
function setSuit(suit, path, v) { store.patchDesign(`suit_symbols.${suit}.${path}`, v) }
|
|||
|
|
function setDesign(path, v) { store.patchDesign(path, v) }
|
|||
|
|
|
|||
|
|
const canHaveOverride = computed(() => {
|
|||
|
|
return !isJoker(store.currentCard) && store.currentCard !== 'back'
|
|||
|
|
})
|
|||
|
|
function setOverrideBg() {
|
|||
|
|
if (!canHaveOverride.value) return
|
|||
|
|
store.patchCardOverride(store.currentCard, 'background_color', design.value.background_color)
|
|||
|
|
}
|
|||
|
|
function patchOverride(path, v) {
|
|||
|
|
store.patchCardOverride(store.currentCard, path, v)
|
|||
|
|
}
|
|||
|
|
function clearOverride() {
|
|||
|
|
store.clearCardOverride(store.currentCard)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const isNumberCard = computed(() => {
|
|||
|
|
if (!store.currentCard || !store.currentCard.includes('-')) return false
|
|||
|
|
const r = store.currentCard.split('-')[1]
|
|||
|
|
return /^[0-9]+$/.test(r) || r === 'A'
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
const selectedRank = ref(1)
|
|||
|
|
watch(() => store.currentCard, () => {
|
|||
|
|
if (isNumberCard.value) {
|
|||
|
|
const r = store.currentCard.split('-')[1]
|
|||
|
|
selectedRank.value = r === 'A' ? 1 : parseInt(r)
|
|||
|
|
}
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
const positions = computed(() => LAYOUT_POSITIONS[selectedRank.value] || LAYOUT_POSITIONS[1])
|
|||
|
|
|
|||
|
|
function overrideFor(i) {
|
|||
|
|
const list = (store.project?.number_layout || {})[String(selectedRank.value)] || []
|
|||
|
|
const o = list[i] || {}
|
|||
|
|
return {
|
|||
|
|
dx: Number(o.dx) || 0,
|
|||
|
|
dy: Number(o.dy) || 0,
|
|||
|
|
scale: Number(o.scale) || 1,
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
function setOffset(i, key, val) {
|
|||
|
|
store.patchNumberLayout(String(selectedRank.value), i, { [key]: parseFloat(val) })
|
|||
|
|
}
|
|||
|
|
function resetLayout() {
|
|||
|
|
store.resetNumberLayout(String(selectedRank.value))
|
|||
|
|
}
|
|||
|
|
</script>
|
|||
|
|
|
|||
|
|
<style scoped>
|
|||
|
|
.design-panel section { margin-bottom: 18px; padding-bottom: 14px; border-bottom: 1px solid #16213e; }
|
|||
|
|
.design-panel h4 { margin: 0 0 6px 0; font-size: 13px; color: #ccc; }
|
|||
|
|
.hint { font-size: 11px; color: #777; margin: 0 0 8px 0; line-height: 1.4; }
|
|||
|
|
.row { display: flex; align-items: center; gap: 6px; margin-bottom: 6px; }
|
|||
|
|
.row span:first-child { font-size: 12px; color: #aaa; min-width: 56px; }
|
|||
|
|
.row .val { font-size: 11px; color: #888; min-width: 32px; text-align: right; }
|
|||
|
|
input[type="color"] { width: 28px; height: 24px; border: none; border-radius: 3px; cursor: pointer; background: transparent; }
|
|||
|
|
input[type="text"] { flex: 1; background: #16213e; border: 1px solid #16213e; color: #eee; padding: 4px 6px; border-radius: 3px; font-size: 12px; }
|
|||
|
|
input[type="number"] { width: 50px; background: #16213e; border: 1px solid #16213e; color: #eee; padding: 4px; border-radius: 3px; font-size: 12px; }
|
|||
|
|
input[type="range"] { flex: 1; }
|
|||
|
|
select { background: #16213e; border: 1px solid #16213e; color: #eee; padding: 4px; border-radius: 3px; font-size: 12px; }
|
|||
|
|
.mini { background: #16213e; color: #aaa; padding: 4px 8px; font-size: 11px; border-radius: 3px; }
|
|||
|
|
.mini.ghost { background: transparent; border: 1px solid #555; }
|
|||
|
|
.mini-label { font-size: 11px; color: #888; min-width: 24px; }
|
|||
|
|
.thumb { width: 60px; height: 30px; object-fit: cover; border-radius: 3px; }
|
|||
|
|
|
|||
|
|
.suit-row { display: flex; align-items: center; gap: 6px; margin-bottom: 6px; }
|
|||
|
|
.suit-label { font-size: 12px; min-width: 70px; color: #ccc; }
|
|||
|
|
.suit-row select { flex: 1; }
|
|||
|
|
|
|||
|
|
.rank-tabs { display: grid; grid-template-columns: repeat(5, 1fr); gap: 4px; margin-bottom: 10px; }
|
|||
|
|
.rank-tabs button { background: #16213e; color: #aaa; padding: 4px; font-size: 11px; border-radius: 3px; }
|
|||
|
|
.rank-tabs button.active { background: #e94560; color: white; }
|
|||
|
|
|
|||
|
|
.pip-row { display: flex; align-items: center; gap: 4px; margin-bottom: 4px; padding: 4px; background: #16213e; border-radius: 4px; }
|
|||
|
|
.pip-label { font-size: 11px; min-width: 24px; color: #888; }
|
|||
|
|
</style>
|