Files
17168ERP/web/admin/follower/tablet_template.html
T
2026-08-26 17:46:49 +08:00

1034 lines
48 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<style>
/* ================= 基礎排版與重設 ================= */
.print-preview-app {
font-family: "Microsoft JhengHei", DFKai-sb, sans-serif;
margin: 0;
background: #f8f9fa;
display: flex;
gap: 24px;
height: 100vh;
overflow: hidden;
box-sizing: border-box;
padding: 24px;
}
#preview-container {
display: flex;
flex-grow: 1;
background: #e2e8f0;
padding: 32px;
overflow: auto;
position: relative;
border-radius: 12px;
justify-content: center;
align-items: flex-start;
}
#paper-preview {
background: #fff;
padding: 20mm;
box-sizing: border-box;
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.08);
width: 210mm;
height: 297mm;
display: flex;
flex-direction: column;
position: relative;
transform-origin: top center;
}
.dual-editor-wrapper {
display: flex;
flex-direction: row-reverse;
width: 100%;
height: 100%;
gap: 40px;
justify-content: space-between;
}
.editor {
display: flex;
outline: none;
box-sizing: border-box;
border: 1px dashed #cbd5e1;
overflow: hidden;
width: 105mm;
height: 297mm;
position: relative;
background-size: 100% 100%;
}
/* 牌位外框視覺提示 */
.tablet-element {
cursor: pointer;
border: 1px solid transparent;
transition: border 0.2s;
}
.tablet-element:hover {
border: 1px dashed #94a3b8;
}
/* ================= 浮動選單樣式 ================= */
#line-floating-menu {
position: fixed;
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(8px);
border: 1px solid #e2e8f0;
padding: 16px;
border-radius: 10px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
z-index: 99999;
display: flex;
flex-direction: column;
gap: 12px;
width: 30vw;
height:80vh;
left: 50px;
top: 50px;
overflow:auto;
}
#line-floating-menu h4 {
margin: 0;
color: #4a5568;
font-size: 12px;
cursor: move;
background: #f8fafc;
padding: 8px;
border-radius: 6px;
text-align: center;
user-select: none;
border: 1px solid #edf2f7;
}
.control-group {
display: flex;
flex-direction: column;
gap: 8px;
font-size: 13px;
font-weight: 500;
color: #4a5568;
}
.adjust-row {
display: flex;
align-items: center;
gap: 8px;
}
.adjust-row button {
padding: 4px 12px;
border: 1px solid #e2e8f0;
background: #fff;
color: #4a5568;
cursor: pointer;
border-radius: 6px;
font-weight: 600;
}
.adjust-row button:hover {
background: #f1f5f9;
}
.adjust-row input[type="range"] {
flex-grow: 1;
padding: 0;
}
/* ================= 名單 UI 與排版樣式 ================= */
.item-list {
list-style: none;
padding: 0;
margin: 0;
max-height: 120px;
overflow-y: auto;
border: 1px solid #e2e8f0;
border-radius: 6px;
background: #fff;
}
.item-list li {
display: flex;
justify-content: space-between;
align-items: center;
padding: 4px 8px;
border-bottom: 1px solid #edf2f7;
font-size: 13px;
color: #4a5568;
}
.list-actions {
display: flex;
gap: 4px;
}
.list-actions button, .item-list button {
padding: 2px 6px;
background: #f8fafc;
border: 1px solid #cbd5e1;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
color: #4a5568;
line-height: 1;
}
.list-actions button:hover, .item-list button:hover {
background: #e2e8f0;
}
/* 直書排版容器 */
.vertical-text {
writing-mode: vertical-rl;
text-orientation: mixed;
}
.nameList, .liveList {
writing-mode: vertical-rl;
display: flex;
flex-direction: row;
flex-wrap: wrap;
align-content: center;
align-items: center;
margin: auto;
border: 0px solid transparent;
}
.nameSpan, .liveSpan {
display: block;
line-height: 1.1;
text-align: justify;
text-align-last: justify;
text-justify: inter-character;
word-break: break-all;
white-space: pre-line;
}
/* 列印設定 */
@media print {
.print-preview-app {
padding: 0;
height: auto;
overflow: visible;
}
#line-floating-menu {
display: none !important;
}
#preview-container {
background: #fff;
padding: 0;
margin: 0;
position: absolute;
left: 0;
top: 0;
}
#paper-preview {
width: 100% !important;
height: 100% !important;
box-shadow: none;
display: block !important;
padding-top: 30mm !important;
padding-right: 30mm !important;
padding-bottom: 0 !important;
padding-left: 0 !important;
border: none;
}
.editor {
height: auto !important;
max-height: 100% !important;
border: none;
}
.no-print {
display: none !important;
}
}
</style>
<!-- 頂端控制列 (依據 editorNewOne_2.html) -->
<div class="row mt-2 no-print" style="padding: 0 24px; background: white; z-index: 10;">
<div class="col-3">
範本設定:
<select id="templateSel" class="form-select">
<option value="">請選擇範本</option>
<!-- 這裡的選項可透過 API 動態載入 -->
</select>
</div>
<div class="col-3">
範本名稱:
<input type="text" id="template_name_txt" class="form-control" placeholder="" />
</div>
<div class="col-2">
<button class="btn btn-primary" id="btn_save">儲存牌位</button>
</div>
<div class="col-5">
<div style="display: flex; align-items: center; padding: 8px; background-color: #f5f5f5; border-radius: 4px;">
<span style="margin-right: 8px; cursor:pointer;" id="zoomOut">➖</span>
<input type="range" id="scaleSlider" min="0.0" max="1.0" step="0.05" value="0.5" style="flex-grow: 1; cursor: pointer;">
<span style="margin-left: 8px; cursor:pointer;" id="zoomIn">➕</span>
<span id="scaleSize" style="margin-left: 12px; min-width: 45px; font-weight: bold; font-size: 14px; text-align: right;">50%</span>
</div>
</div>
</div>
<div class="print-preview-app">
<!-- 預覽容器 -->
<div id="preview-container">
<div id="paper-preview" style="transform: scale(0.5);">
<div class="dual-editor-wrapper">
<!-- 右側編輯區 -->
<div id="editor-right" class="editor">123</div>
<!-- 左側編輯區 (動態產生內容) -->
<div id="editor-left" class="editor printArea"></div>
</div>
</div>
<!-- 浮動設定面板 -->
<div id="line-floating-menu" class="no-print" style="display: ;">
<h4 id="menu-header">✥ 拖拉此處移動面板</h4>
<!-- 🌟 新增:浮動選單內的版型選擇 -->
<div class="control-group">
<label>版型設定</label>
<div class="adjust-row">
<select class="styleSelector" style="width: 100%; padding: 6px; font-size: 13px; border: 1px solid #e2e8f0; border-radius: 6px; outline: none; background-color: #f8fafc;">
<option value="">請選擇版型</option>
</select>
</div>
</div>
<hr style="margin: 15px 0;">
<div class="control-group">
<label style="color:#d63031; font-weight:bold;" id="current-edit-label">目前編輯:請點擊牌位區塊</label>
</div>
<div class="control-group" style="height:50%;">
<label>已選</label>
<div class="adjust-row">
<input type="text" id="custom_txt" placeholder="請輸入名稱" style="padding:4px; font-size:12px; width: 100%;">
<button id="btn_add_custom" style="width: 32px; padding: 4px;">+</button>
</div>
<ul id="selectedItem" class="item-list"></ul>
</div>
<div class="control-group" style="height:50%;">
<label>可選</label>
<input type="text" id="filter_txt" placeholder="篩選條件..." style="padding:4px; font-size:12px;">
<ul id="canselectItem" class="item-list"></ul>
</div>
</div>
</div>
</div>
<script>
const designer = {
zoomScale: 0.5,
tabletItem: {},
familyMembers: [],
allStyle: [],
allStyleDetails: [],
allSize: [],
tabletElement: [],
papers: [],
elements:[],
http_host: "",
follower_id: "",
follower_num:"",
// 動態名單資料
properTitle: [],
leftProperTitle: [],
rightProperTitle: [],
alive: [],
address: "",
currentTarget: "",
selectedElement: null,
selectStyle: "",
tabletTempNameList: [],
selectedTemplate: {},
bg: [
{ name: "超薦1", path: "../print/html/tablet-1_new.svg" },
{ name: "超薦2", path: "../print/html/tablet-1B_new.svg" },
{ name: "消災1", path: "../print/html/tablet-2.svg" },
{ name: "消災2", path: "../print/html/tablet-2B.svg" },
{ name: "消災3", path: "../print/html/tablet-2C.svg" },
{ name: "消災4", path: "../print/html/tablet-2D.svg" }
],
async init() {
this.getTabletStyles();
this.bindZoom();
this.bindFloatingMenu();
this.bindListEvents();
this.follower_id = VueApp.follower_id;
//console.log(this.follower_num,":",VueApp.follower_id);
this.getFamilies();
this.getTemplateNameList();
},
//抓親友資料
async getFamilies() {
await axios.post('/api/follower/GetFollowerFamilies', { num: VueApp.follower_id }).then(res => {
if (res.status == 200 && res.data.list) {
this.familyMembers = res.data.list;
}
});
},
// ================= 1. API 資料撈取 =================
//async getAllStyleDetails() {
// await axios.post('/api/tablet/GetStyleDetailData', {}).then(res => {
// if (res.status == 200 && res.data.result == "Y") this.allStyleDetails = res.data.data;
// });
//},
//async getPaperSize() {
// await axios.post('/api/tablet/GetPaperSize', {}).then(res => {
// if (res.status == 200) this.allSize = res.data.data;
// });
//},
//async getTabletElement() {
// await axios.post('/api/tablet/GetTabletElement', {}).then(res => {
// if (res.status == 200) this.tabletElement = res.data.data;
// });
//},
async getTabletStyles() {
await axios.post('/api/tablet/GetTabletStyleList', {}).then(res => {
if (res.status == 200 && res.data.list) {
this.allStyle = [];
// 🌟 改用 Class
$('.styleSelector').empty().append('<option value="">請選擇版型</option>');
res.data.list.forEach(x => {
if (x.styleID !== "000001") {
this.allStyle.push(x);
// 🌟 改用 Class
$('.styleSelector').append(`<option value="${x.styleID}">${x.name}</option>`);
}
});
this.papers = res.data.papers;
this.elements = res.data.elements;
//console.log("getTabletStyles:",this.allStyle, this.papers, this.elements);
}
});
},
async getData() {
this.properTitle = []; this.leftProperTitle = []; this.rightProperTitle = []; this.alive = [];
//let param = [{ order_no: this.tabletItem.order_no, num: this.tabletItem.num }];
await axios.post('/api/follower/GetTabletTemplateList', { follower_num:VueApp.follower_id}).then(res => {
if (res.status == 200) {
res.data.forEach(x => {
if (x.style_id) {
this.selectStyle = x.style_id;
// 🌟 載入資料時同步兩邊的下拉選單
$(".styleSelector").val(x.style_id);
}
if (x.tablet) {
let tablet = JSON.parse(x.tablet);
if (tablet.mid_items) this.properTitle = tablet.mid_items;
if (tablet.left_items) this.alive = tablet.left_items;
if (tablet.mid_left) this.leftProperTitle = tablet.mid_left;
if (tablet.mid_right) this.rightProperTitle = tablet.mid_right;
this.address = tablet.addr_items || x.address || "";
}
});
this.renderEditorLeft(); // 資料撈完後開始畫畫面
this.renderEditorRight();
}
});
},
async getTemplateNameList() {//抓範本名稱做下拉選項
$("#templateSel").empty().append('<option value="">請選擇範本</option>');;
this.tabletTempNameList.length = 0;
await axios.post('/api/follower/GetTabletTemplateNameList', { num: VueApp.follower_id }).then(res => {
if (res.status == 200 && res.data.list) {
this.tabletTempNameList = res.data.list;
this.tabletTempNameList.forEach(x => {
$('#templateSel').append(`<option value="${x.num}">${x.name}</option>`);
});
}
});
},
// ================= 2. 動態渲染排版 =================
renderEditorLeft() {
$('#editor-left').empty();
let style = this.allStyle.find(y => y.styleID == this.selectStyle);
if (!style) return;
let size = this.papers.find(y => y.paperID == style.paperSize);
let printSize = this.papers.find(y => y.paperID == style.printSize);
let img = this.bg.find(y => y.name == style.backendImg);
// 設定外框與底圖
$('#paper-preview').css({ width: (Number(printSize.width) * 2) + 'mm', height: (Number(printSize.height)+20) + 'mm' });
$('#editor-left').css({ width: Number(size.width) + 'mm', height: Number(size.height) + 'mm', backgroundImage: `url(${img.path})` });
// 🌟 3. 設定右側 (#editor-right) 的尺寸:寬度永遠鎖定 A4 橫向 (297mm),高度跟著版型
$('#editor-right').css({
width: '297mm',
height: Number(size.height) + 'mm'
});
let details = style.details; //this.elements.filter(y => y.styleID == style.styleID);
// 依據版型設定動態產生內部區塊
// 依據版型設定動態產生內部區塊
details.forEach(d => {
if (d.isActive != "hidden") {
let html = "";
let currentList = [];
if (d.elementID === 'title1') {
currentList = this.properTitle.length > 0 ? this.properTitle : ["請選擇"];
html = this.buildNameList(currentList, d);
} else if (d.elementID === 'titletriangle') { // 🌟 加入品字佈局
currentList = this.properTitle.length > 0 ? this.properTitle : ["請選擇"];
html = this.buildRosterList(currentList, d);
} else if (d.elementID === 'combined' || d.elementID === 'tricombined') { // 🌟 加入合併佈局
currentList = this.properTitle.length > 0 ? this.properTitle : ["請選擇"];
html = this.buildCombinedList(currentList, d);
} else if (d.elementID === 'alive') {
currentList = this.alive.length > 0 ? this.alive : ["請選擇"];
html = this.buildLiveList(currentList, d);
} else if (d.elementID === 'lefttitle') {
currentList = this.leftProperTitle.length > 0 ? this.leftProperTitle : ["請選擇"];
html = this.buildNormalWithSpace(currentList, d, 'lefttitle');
} else if (d.elementID === 'righttitle') {
currentList = this.rightProperTitle.length > 0 ? this.rightProperTitle : ["請選擇"];
html = this.buildNormalWithSpace(currentList, d, 'righttitle');
} else if (d.elementID === 'address') {
html = `<div class='address' style='width:${d.width}px; height:${d.height}px; font-size:${d.fontSize}px; font-family:${d.fontFamily};'>${this.address || "請選擇"}</div>`;
}
if (html) {
let wrapper = $(`<div class="tablet-element vertical-text"></div>`).css({
position: 'absolute', left: d.startX + 'mm', top: d.startY + 'mm',
fontFamily: d.fontFamily, fontWeight: 'bold', zIndex: 9999, visibility: d.isActive
}).html(html);
$('#editor-left').append(wrapper);
}
}
});
},
// 完美修正版正名演算
buildNameList(names, d) {
let w = parseFloat(d.width), h = parseFloat(d.height), th = parseFloat(d.textHeight) || h;
let v_gap = parseFloat(d.textSpan) || 0, h_gap = 5;
let shrinkLimit = (h < 500) ? 8 : ((h < 300) ? 12 : 6);
let long_s = parseFloat(d.fontSize), short_s = parseFloat(d.fontSize);
let testNames = [...names];
if (testNames.length === 1 && testNames[0].includes("暨無始劫以來累世冤親債主")) {
testNames[0] = testNames[0].split("暨")[0] + "暨";
}
let items = testNames.map((name, i) => {
let l = name.length;
let isShort = (testNames.length > 1) && ((l * 40) <= (th + (v_gap / 2)));
return { name: names[i], l: l, isShort: isShort, isMulti: false, size: 0 };
});
let max_short_effective = shrinkLimit, min_long_limit = parseFloat(d.fontSize);
items.forEach(item => {
if (item.isShort) { let ms = Math.floor(th / (item.l * 1.333)); if (ms > max_short_effective) max_short_effective = ms; }
else { let mh = Math.floor((h - 10) / (item.l * 1.333)); if (mh < min_long_limit) min_long_limit = mh; }
});
if (short_s > max_short_effective) short_s = max_short_effective;
if (long_s > min_long_limit) long_s = min_long_limit;
let sizes = [], shrink_turn = 'long';
while (true) {
let totalWidth = 0, current_col_h = 0, current_col_w = 0, cols_width = [], any_wrapped = false;
for (let i = 0; i < items.length; i++) {
let item = items[i], s = item.isShort ? short_s : long_s;
if (item.isShort) { let ms = Math.floor(th / (item.l * 1.333)); if (s > ms) s = ms; }
item.size = s; let size_px = s * 1.333;
if (item.isShort) {
let item_h = Math.max(th, item.l * size_px);
if (current_col_h === 0) { current_col_h = item_h; current_col_w = size_px; }
else if ((current_col_h + v_gap + item_h) <= h) { current_col_h += (v_gap + item_h); if (size_px > current_col_w) current_col_w = size_px; }
else { cols_width.push(current_col_w); current_col_h = item_h; current_col_w = size_px; }
} else {
if (current_col_h > 0) { cols_width.push(current_col_w); current_col_h = 0; current_col_w = 0; }
let item_w = size_px;
if ((item.l * size_px) > (h - 10)) {
item.isMulti = true; any_wrapped = true;
item_w = Math.ceil(item.l / Math.max(1, Math.floor((h - 10) / size_px))) * (size_px * 1.1);
} else { item.isMulti = false; }
cols_width.push(item_w);
}
}
if (current_col_h > 0) cols_width.push(current_col_w);
totalWidth = cols_width.reduce((a, b) => a + b, 0) + (cols_width.length > 1 ? (cols_width.length - 1) * h_gap : 0);
if (totalWidth <= w && (!any_wrapped || long_s === shrinkLimit)) {
sizes = items.map(x => { let fs = x.size; if (x.isShort) { let ms = Math.floor(th / (x.l * 1.333)); if (fs > ms) fs = ms; } return { name: x.name, size: fs, isMulti: x.isMulti, isShort: x.isShort }; });
break;
}
if (any_wrapped && long_s > shrinkLimit) long_s--;
else {
let hasLong = items.some(x => !x.isShort), hasShort = items.some(x => x.isShort);
if (hasLong && hasShort) { if (shrink_turn === 'long') { if (long_s > shrinkLimit) { long_s--; shrink_turn = 'short'; } else if (short_s > shrinkLimit) { short_s--; shrink_turn = 'long'; } else break; } else { if (short_s > shrinkLimit) { short_s--; shrink_turn = 'long'; } else if (long_s > shrinkLimit) { long_s--; shrink_turn = 'short'; } else break; } }
else if (hasLong) { if (long_s > shrinkLimit) long_s--; else break; } else { if (short_s > shrinkLimit) short_s--; else break; }
}
}
if (sizes.length === 0) sizes = names.map(name => ({ name: name, size: shrinkLimit, isMulti: false, isShort: true }));
let $namelist = $(`<div class='nameList'></div>`).css({ width: `${w}px`, height: `${h}px`, "column-gap": `${v_gap}px` });
sizes.forEach(item => {
let minH = item.isShort ? `${th}px` : `${h}px`;
let alignStyle = item.isMulti || (!item.isMulti && !item.isShort) ? "start" : "justify";
$namelist.append($(`<span class="nameSpan" style="font-size: ${item.size}pt; min-height: ${minH}; max-height: ${h}px; text-align: ${alignStyle}; text-align-last: ${alignStyle}; margin-left: 5px;">${item.name}</span>`));
});
return $namelist;
},
// 完美修正版陽上演算
buildLiveList(names, d) {
let w = parseFloat(d.width), h = parseFloat(d.height), th = parseFloat(d.textHeight) || h;
let v_gap = parseFloat(d.textSpan) || 0, h_gap = 5;
let shrinkLimit = (h < 300) ? 8 : ((h < 500) ? 6 : 4);
let s = parseFloat(d.fontSize);
while (s >= shrinkLimit) {
let size_px = s * 1.333, current_col_h = 0, current_col_w = 0, cols_width = [], any_wrapped = false;
for (let i = 0; i < names.length; i++) {
let l = names[i].length, item_h = Math.max(th, l * size_px);
if (item_h > (h - 5)) {
any_wrapped = true;
if (current_col_h > 0) { cols_width.push(current_col_w); current_col_h = 0; current_col_w = 0; }
cols_width.push(Math.ceil(l / Math.max(1, Math.floor((h - 5) / size_px))) * (size_px * 1.1));
} else {
if (current_col_h === 0) { current_col_h = item_h; current_col_w = size_px; }
else if (current_col_h + v_gap + item_h <= h) { current_col_h += v_gap + item_h; if (size_px > current_col_w) current_col_w = size_px; }
else { cols_width.push(current_col_w); current_col_h = item_h; current_col_w = size_px; }
}
}
if (current_col_h > 0) cols_width.push(current_col_w);
if ((cols_width.reduce((a, b) => a + b, 0) + (cols_width.length > 1 ? (cols_width.length - 1) * h_gap : 0)) <= w && (!any_wrapped || s === shrinkLimit)) break;
s -= 1;
}
if (s < shrinkLimit) s = shrinkLimit;
let $namelist = $(`<div class='liveList'></div>`).css({ width: `${w}px`, height: `${h}px`, "column-gap": `${v_gap}px` });
names.forEach(name => $namelist.append($(`<span class="liveSpan" style="font-size: ${s}pt; min-height: ${th}px; max-height: ${h}px; margin-left: 0px;">${name}</span>`)));
return $namelist;
},
// ================= 3. 浮動選單與互動綁定 =================
bindFloatingMenu() {
let isDragging = false, startX = 0, startY = 0;
$('#menu-header').on('mousedown', e => { isDragging = true; startX = e.clientX - $('#line-floating-menu').offset().left; startY = e.clientY - $('#line-floating-menu').offset().top; });
$(document).on('mousemove', e => { if (isDragging) $('#line-floating-menu').css({ left: (e.clientX - startX) + 'px', top: (e.clientY - startY) + 'px', bottom: 'auto' }); }).on('mouseup', () => isDragging = false);
$('#preview-container').on('click', e => {
if (e.target.id === 'preview-container' || e.target.id === 'paper-preview') {
$('#line-floating-menu').hide();
$('#editor-left .tablet-element').css('border', '1px solid transparent');
this.selectedElement = null;
}
});
// 點擊牌位區塊
$('#editor-left').on('click', '.tablet-element', e => {
e.preventDefault(); e.stopPropagation();
this.selectedElement = $(e.currentTarget);
$('#editor-left .tablet-element').css('border', '1px solid transparent');
this.selectedElement.css('border', '1px dashed #ff7675');
this.renderItemMenu(this.selectedElement.children().first());
$('#line-floating-menu').show();
});
// 字體大小拉桿
$('input[type="range"]').on('input', e => {
let val = parseInt($(e.target).val());
$('#lbl-fontSize').text(val);
if (this.selectedElement) {
let target = this.selectedElement.children().first();
let d = this.allStyleDetails.find(y => y.styleID == this.selectStyle && (
(target.hasClass("nameList") && y.elementID == "title1") ||
(target.hasClass("liveList") && y.elementID == "alive")
));
if (d) { d.fontSize = val; this.rerenderPart(); }
}
});
},
renderItemMenu(element) {
$("#selectedItem").empty();
$("#canselectItem").empty();
let currentData = [], editLabel = "未定義區塊";
// 🌟 1. 判斷是否為超薦牌位 (利用 item_type === 'B' 或是依據底圖名稱防呆)
let isChaojian = false;
if (this.item_type === 'B' || (typeof VueApp !== 'undefined' && VueApp.item_type === 'B') || (this.tabletItem && this.tabletItem.item_type === 'B')) {
isChaojian = true;
} else {
let style = this.allStyle.find(y => y.styleID == this.selectStyle);
if (style && style.name && style.name.includes("超薦")) {
isChaojian = true;
}
}
// 預設生死條件:超薦要死者(true),消災要活者(false)
let requireDeceased = isChaojian;
// 🌟 2. 判斷當前編輯區塊
if (element.hasClass("liveList")) {
editLabel = "陽上";
currentData = this.alive;
requireDeceased = false; // 🚨 陽上永遠只能選活者
}
else if (element.hasClass("nameList") || element.hasClass("rosterList") || element.hasClass("ancestor-wrapper")) {
editLabel = "正名";
currentData = this.properTitle;
}
else if (element.hasClass("lefttitle")) {
editLabel = "左正名";
currentData = this.leftProperTitle;
}
else if (element.hasClass("righttitle")) {
editLabel = "右正名";
currentData = this.rightProperTitle;
}
else if (element.hasClass("address")) {
editLabel = "地址";
currentData = this.address ? [this.address] : [];
}
$("#current-edit-label").text("目前編輯:" + editLabel);
// 渲染已選
currentData.forEach((x, index) => {
$("#selectedItem").append(`
<li><span class="selected">${x}</span><div class="list-actions">
<button class="itemUp" data-idx="${index}">↑</button><button class="itemDown" data-idx="${index}">↓</button><button class="itemRemove" data-idx="${index}">-</button>
</div></li>`);
});
// 地址不需要出現親友選單
if (element.hasClass("address")) {
this.bindListEvents();
return;
}
// 🌟 3. 渲染可選 (嚴格過濾生死狀態)
let filterKeyword = $('#filter_txt').val().trim() || "";
this.familyMembers.forEach(f => {
// 判斷該親友的生死狀態 (相容 boolean 與字串)
let isDead = (f.deceased === true || f.deceased === "true" || f.deceased === "True");
// 條件成立:生死符合要求 + 尚未加入 + 關鍵字相符
if (isDead === requireDeceased && !currentData.includes(f.fam_name) && f.fam_name.includes(filterKeyword)) {
$("#canselectItem").append(`
<li>
<span style="opacity:0.5">卍</span>
<span class="unselect">${f.fam_name}</span>
<span>
<i class="rounded-pill rounded-3 ${(f && f.fam_title) ? 'p-1' : ''}" style="background-color:lightgray">
${f.fam_title ? f.fam_title : ""}
</i>
</span>
<span class='itemAppend' style="float:right;cursor:pointer;">&#43;</span>
</li>
`);
}
});
this.bindListEvents();
},
bindListEvents() {
let self = this;
// 🌟 修正1:加上 .off() 解除舊綁定,防止「幽靈疊加點擊」!
// 否則您每點一次,它會因為 renderItemMenu 重新綁定,導致一次加好幾個名字甚至當掉
$("#canselectItem").off("click", ".itemAppend").on("click", ".itemAppend", function (e) {
e.preventDefault();
self.modifyDataAndRerender("add", $(this).siblings(".unselect").text());
});
$("#selectedItem").off("click", ".itemRemove").on("click", ".itemRemove", function (e) {
e.preventDefault();
self.modifyDataAndRerender("remove", $(this).closest("li").find(".selected").text());
});
$("#selectedItem").off("click", ".itemUp").on("click", ".itemUp", function (e) {
e.preventDefault();
self.modifyDataAndRerender("up", $(this).data("idx"));
});
$("#selectedItem").off("click", ".itemDown").on("click", ".itemDown", function (e) {
e.preventDefault();
self.modifyDataAndRerender("down", $(this).data("idx"));
});
$('#btn_add_custom').off("click").on('click', function (e) {
e.preventDefault();
let val = $('#custom_txt').val().trim();
if (val) { self.modifyDataAndRerender("add", val); $('#custom_txt').val(''); }
});
$('#filter_txt').off("keyup").on('keyup', () => {
if (this.selectedElement) this.renderItemMenu(this.selectedElement.children().first());
});
$('.styleSelector').off("change").on('change', (e) => {
let val = $(e.target).val();
$('.styleSelector').val(val); // 雙向同步
this.selectStyle = val;
this.renderEditorLeft();
});
$('#templateSel').off('change').on('change', async (e) => {
let template_num = $(e.target).val();
if (!template_num) return;
//抓回範本資料
try {
let val = $(e.target).val();
this.selectedTemplate = this.tabletTempNameList.find(x=>x.num===template_num);
} catch (error) {
console.error("範本載入失敗:", error);
alert("範本載入失敗!");
}
});
// ==========================================
// 🌟 3. 儲存按鈕事件 (含範本資料)
// ==========================================
$('#btn_save').off('click').on('click', async function (e) {
e.preventDefault();
//let tablet = {
// mid_items: designer.family_deceased_N_selected,
// left_items: designer.family_deceased_Y_selected,
// style: designer.selectStyle.style,
// mid_left: designer.family_left_title,
// mid_right: designer.family_right_title,
// addr_items: designer.family_address
//}
let data = {
num: 0, name: $("#template_name_txt").val(), follower_num: VueApp.follower_id,
style_id: $(".styleSelector").val(),tablet:"",shuwen:""
}
console.log("btn_save:", data);
});
},
modifyDataAndRerender(action, payload) {
if (!this.selectedElement) return;
let target = this.selectedElement.children().first(), targetArr = null;
if (target.hasClass("liveList")) targetArr = this.alive;
else if (target.hasClass("nameList")) targetArr = this.properTitle;
else if (target.hasClass("lefttitle")) targetArr = this.leftProperTitle;
else if (target.hasClass("righttitle")) targetArr = this.rightProperTitle;
else if (target.hasClass("address")) {
if (action === "add") this.address = payload; else if (action === "remove") this.address = "";
targetArr = this.address ? [this.address] : [];
}
if (targetArr) {
if (action === "add" && !targetArr.includes(payload)) targetArr.push(payload);
else if (action === "add" && targetArr.includes(payload)) { alert("名稱已存在!"); return; }
else if (action === "remove") { let idx = targetArr.indexOf(payload); if (idx > -1) targetArr.splice(idx, 1); }
else if (action === "up" && payload > 0) [targetArr[payload - 1], targetArr[payload]] = [targetArr[payload], targetArr[payload - 1]];
else if (action === "down" && payload < targetArr.length - 1) [targetArr[payload], targetArr[payload + 1]] = [targetArr[payload + 1], targetArr[payload]];
}
this.renderItemMenu(target);
this.rerenderPart();
this.renderEditorRight();
},
rerenderPart() {
if (!this.selectedElement) return;
let target = this.selectedElement.children().first();
let style = this.allStyle.find(y => y.styleID == this.selectStyle);
if (!style) return;
let details = style.details || this.allStyleDetails.filter(y => y.styleID == this.selectStyle);
// 🌟 加入尋找設定檔的判斷
let d = details.find(y => (
(target.hasClass("nameList") && y.elementID == "title1") ||
(target.hasClass("rosterList") && y.elementID == "titletriangle") ||
(target.hasClass("ancestor-wrapper") && (y.elementID == "combined" || y.elementID == "tricombined")) ||
(target.hasClass("liveList") && y.elementID == "alive") ||
(target.hasClass("lefttitle") && y.elementID == "lefttitle") ||
(target.hasClass("righttitle") && y.elementID == "righttitle") ||
(target.hasClass("address") && y.elementID == "address")
));
if (!d) return;
this.selectedElement.empty();
let html = "";
// 🌟 重新導向對應的排版引擎
if (target.hasClass("nameList")) html = this.buildNameList(this.properTitle, d);
else if (target.hasClass("rosterList")) html = this.buildRosterList(this.properTitle, d);
else if (target.hasClass("ancestor-wrapper")) html = this.buildCombinedList(this.properTitle, d);
else if (target.hasClass("liveList")) html = this.buildLiveList(this.alive, d);
else if (target.hasClass("lefttitle")) html = this.buildNormalWithSpace(this.leftProperTitle, d, 'lefttitle');
else if (target.hasClass("righttitle")) html = this.buildNormalWithSpace(this.rightProperTitle, d, 'righttitle');
else if (target.hasClass("address")) html = `<div class='address' style='width:${d.width}px; height:${d.height}px; font-size:${d.fontSize}px; font-family:${d.fontFamily};'>${this.address}</div>`;
this.selectedElement.append(html);
},
// 縮放控制
bindZoom() {
let updateZoom = () => { $('#scaleSize').html(Math.floor(this.zoomScale * 100) + "%"); $('#scaleSlider').val(this.zoomScale); $('#paper-preview').css("transform", `scale(${this.zoomScale})`); };
$('#scaleSlider').on("input", e => { this.zoomScale = parseFloat($(e.target).val()); updateZoom(); });
$('#zoomOut').on("click", () => { this.zoomScale = Math.max(0.1, this.zoomScale - 0.05); updateZoom(); });
$('#zoomIn').on("click", () => { this.zoomScale = Math.min(2.0, this.zoomScale + 0.05); updateZoom(); });
},
// 🌟 1. 品字排版引擎 (titletriangle)
buildRosterList(names, d) {
if (!names.length) names = ["請選擇"];
const mid = names.length === 1 ? 1 : Math.floor(names.length / 2);
const top = names.slice(0, mid);
const bot = names.slice(mid);
// 計算自動縮小字體
let baseSize = parseFloat(d.fontSize);
const maxLen = Math.max(...names.map(n => n.length), 0);
let finalSize = maxLen > 5 ? Math.max(baseSize * (5 / maxLen), baseSize * 0.6) : baseSize;
let $wrapper = $(`<div class="rosterList"></div>`).css({
width: `${d.width}px`, height: `${d.height}px`,
"writing-mode": "vertical-rl",
"display": "flex",
"flex-direction": "row",
"align-items": "center",
"justify-content": "start",
"gap": "20px"
});
let renderGroup = (items) => {
let $g = $(`<div class="rosterspan"></div>`).css({
"display": "flex",
"flex-direction": "column",
"justify-content": "center",
"height": (parseFloat(d.textHeight) || parseFloat(d.height)) + 'px',
"white-space": "pre-line"
});
items.forEach(x => {
$g.append($(`<div style="letter-spacing: 10px; text-align: justify; text-align-last: justify; font-size: ${finalSize}px; font-family: ${d.fontFamily};">${x}</div>`));
});
return $g;
};
$wrapper.append(renderGroup(top));
if (bot.length) $wrapper.append(renderGroup(bot));
return $wrapper;
},
// 🌟 2. 合併多姓氏排版引擎 (combined)
buildCombinedList(names, d) {
if (!names.length) names = ["請選擇"];
let baseFontSize = parseFloat(d.fontSize);
// 取前兩筆作為 main-name 與 sub-text
const parts = names.join("\n").split('\n');
// 因為這個元素需要水平包覆垂直,所以把 CSS 直接寫進 inline-style
let html = `
<div class="ancestor-wrapper" style="width:${d.width}px !important; height:${d.height}px !important; display: flex; flex-direction: column; align-items: center; justify-content: flex-start; writing-mode: horizontal-tb; font-family: ${d.fontFamily};">
<span class="main-name" style="line-height: 1.2; font-size: ${baseFontSize}px;">${parts[0] || ''}</span>
<span class="sub-text" style="font-size: ${baseFontSize}px; line-height: 1.2; margin-top: 4px; white-space: normal; writing-mode: vertical-rl;">${parts[1] || ''}</span>
</div>`;
return $(html);
},
// 🌟 完美還原左右正名的換行與間距排版
buildNormalWithSpace(names, d, className) {
let $div = $(`<div class='${className}'></div>`).css({
"width": `${d.width}px`,
"height": `${d.height}px`,
"writing-mode": "vertical-rl",
"display": "flex",
"flex-direction": "row",
"flex-wrap": "wrap",
"column-gap": `${d.textSpan || 0}px`,
"align-items": "flex-start",
"justify-content": "start",
"font-size": `${d.fontSize}px`,
"font-family": d.fontFamily
});
names.forEach(x => {
$div.append($(`<div>${x}</div>`).css({
"margin-bottom": "10px",
"margin-left": "5px",
"text-justify": "inter-character",
"white-space": "pre-line"
}));
});
return $div;
},
// 🌟 處理右側 Editor 同步渲染 (強制一行一個)
renderEditorRight() {
let $editorRight = $('#editor-right');
$editorRight.empty();
// 1. 設定為直書模式,拔除 flex,恢復自然的區塊(Block)換行特性
$editorRight.css({
"writing-mode": "vertical-rl",
"text-orientation": "mixed",
"padding": "20px",
"display": "block" // 🌟 關鍵:改用 block,讓每個子元素絕對獨佔一整行
});
// 2. 收集所有要同步的名單
let syncNames = [
...this.properTitle,
...this.leftProperTitle,
...this.rightProperTitle
];
// 3. 渲染名單
syncNames.forEach(name => {
let $div = $('<div></div>').css({
"font-size": "16pt",
"font-family": "DFKai-sb",
"line-height": "1.2",
"letter-spacing": "2px",
"white-space": "pre-line",
"margin-left": "25px" // 🌟 設定行距 (在直書模式中,左邊就是下一行的位置)
}).text(name);
$editorRight.append($div);
});
},
};
// ================= 4. 啟動與 Message 接聽 =================
//$(window).on("message", function (event) {
// let e = event.originalEvent;
// if (e.origin === window.location.origin || e.origin === window.parent.location.origin) {
// const data = e.data;
// console.log("template load:",data)
// //if (data.tabletItem) {
// // designer.tabletItem = data.tabletItem;
// // if (data.familyMembers) designer.familyMembers = data.familyMembers;
// // designer.getData();
// //}
// }
//});
$(() => {
designer.init();
});
</script>