信眾牌位範本,活動總疏文
This commit is contained in:
@@ -1287,12 +1287,19 @@ public class FollowerController : ApiController
|
||||
if (q.num>0)
|
||||
{//表示要更新
|
||||
var qry = _db.followers_tablet_template.AsQueryable();
|
||||
|
||||
qry = qry.Where(x => x.num == q.num);
|
||||
var d=qry.FirstOrDefault();
|
||||
d.name = q.name;
|
||||
d.style_id = q.style_id;
|
||||
d.shuwen = q.shuwen;
|
||||
d.tablet = q.tablet;
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
_db.followers_tablet_template.Add(q);
|
||||
}
|
||||
_db.SaveChanges();
|
||||
|
||||
return Ok();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,7 +94,8 @@ public class familyMembersController : ApiController
|
||||
return Ok(familyMember);
|
||||
}
|
||||
|
||||
// GET api/familymembers/follower/5
|
||||
// GET api/familymembers/follower/5
|
||||
[HttpGet]
|
||||
[Route("api/familymembers/follower/{followerId}")]
|
||||
public IHttpActionResult GetByFollower(int followerId)
|
||||
{
|
||||
|
||||
@@ -37,7 +37,9 @@ public class shuwensController: BaseApiController
|
||||
x.act_num,
|
||||
x.shuwen_type,
|
||||
x.shuwen_num,
|
||||
x.contents
|
||||
x.contents,
|
||||
x.paper_size,
|
||||
x.direction
|
||||
}).FirstOrDefault()
|
||||
});
|
||||
}
|
||||
@@ -78,4 +80,55 @@ public class shuwensController: BaseApiController
|
||||
}).ToList()
|
||||
});
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Route("api/shuwens/SaveActShuwen")]
|
||||
public IHttpActionResult SaveActShuwen([FromBody] Model.activity_shuwen s)
|
||||
{
|
||||
var qry = _db.activity_shuwen.AsQueryable();
|
||||
qry=qry.Where(x => x.act_num == s.act_num && x.shuwen_type == s.shuwen_type);
|
||||
var d=qry.FirstOrDefault();
|
||||
if (d != null)
|
||||
{
|
||||
d.contents = s.contents;
|
||||
d.direction = s.direction;
|
||||
d.paper_size = s.paper_size;
|
||||
d.reg_time=DateTime.Now;
|
||||
}
|
||||
else
|
||||
{
|
||||
s.reg_time = DateTime.Now;
|
||||
_db.activity_shuwen.Add(s);
|
||||
}
|
||||
|
||||
_db.SaveChanges();
|
||||
return Ok();
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Route("api/shuwens/SaveAsShuwenTemplate")]
|
||||
public IHttpActionResult SaveAsShuwenTemplate([FromBody] Model.shuwen_temp t)
|
||||
{
|
||||
if (t.num > 0)
|
||||
{//跑更新
|
||||
var qry=_db.shuwen_temp.AsQueryable();
|
||||
qry = qry.Where(x => x.num == t.num);
|
||||
var d=qry.FirstOrDefault();
|
||||
if (d != null)
|
||||
{
|
||||
d.contents = t.contents;
|
||||
d.name = t.name;
|
||||
d.reg_time = DateTime.Now;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
t.reg_time=DateTime.Now;
|
||||
_db.shuwen_temp.Add(t);
|
||||
}
|
||||
_db.SaveChanges();
|
||||
|
||||
return Ok();
|
||||
}
|
||||
|
||||
}
|
||||
+137
-16
@@ -51,7 +51,7 @@
|
||||
max="80"
|
||||
v-model.number="globalMargin"
|
||||
@input="applyGlobalMargin" />
|
||||
<button @click.event="adjustAllMargins(1)">+</button>
|
||||
<button @click.prevent="adjustAllMargins(1)">+</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -62,7 +62,10 @@
|
||||
<hr />
|
||||
|
||||
<button class="action-btn save-btn" @click.prevent="saveData">
|
||||
1. 儲存至資料庫
|
||||
1. 儲存總疏文
|
||||
</button>
|
||||
<button class="action-btn save-btn" @click.prevent="saveAsTempData">
|
||||
1.1. 另存成範本
|
||||
</button>
|
||||
<button class="action-btn print-btn" @click.prevent="triggerPrint">
|
||||
2. 產生 PDF / 列印
|
||||
@@ -228,13 +231,45 @@ module.exports = {
|
||||
methods: {
|
||||
changeShuwenType() {
|
||||
console.log("changeShuwenType:", this.shuwenType)
|
||||
// 防呆:如果切換回「請選擇」,就清空畫面
|
||||
if (!this.shuwenType) {
|
||||
this.clearContent();
|
||||
this.defaultTemplate = "";
|
||||
return;
|
||||
}
|
||||
//去資料庫抓回他的消災或超薦的總疏文
|
||||
axios.post(HTTP_HOST + 'api/shuwens/GetActShuwen', { act_num: Number(this.num), shuwen_type: this.shuwenType })
|
||||
.then(response => {
|
||||
console.log(response)
|
||||
if (response && response.data && response.data.data) {
|
||||
let shuwenContent = response.data.data.contents
|
||||
document.getElementById("editor").innerHTML = `${shuwenContent}`;
|
||||
let data = response.data.data;
|
||||
|
||||
// 1. 帶入疏文內容
|
||||
if (data.contents) {
|
||||
document.getElementById("editor").innerHTML = data.contents;
|
||||
}
|
||||
|
||||
// 2. 帶入紙張大小與方向 (對應 DB 欄位 paper_size 與 direction)
|
||||
if (data.paper_size) {
|
||||
this.paperSize = data.paper_size;
|
||||
}
|
||||
if (data.direction) {
|
||||
this.paperOrientation = data.direction;
|
||||
}
|
||||
|
||||
// 3. 同步樣板下拉選單 (如果之前存檔有綁定樣板)
|
||||
if (data.shuwen_num) {
|
||||
this.defaultTemplate = data.shuwen_num;
|
||||
} else {
|
||||
this.defaultTemplate = "";
|
||||
}
|
||||
|
||||
// 4. 呼叫更新 Layout,讓白紙區塊實際變形
|
||||
this.updateLayout();
|
||||
} else {
|
||||
// 如果該活動還沒有存過這個類型的總疏文,就給予預設空白
|
||||
this.clearContent();
|
||||
this.defaultTemplate = "";
|
||||
}
|
||||
|
||||
})
|
||||
@@ -352,19 +387,105 @@ module.exports = {
|
||||
this.globalMargin = newVal;
|
||||
this.applyGlobalMargin();
|
||||
},
|
||||
saveAsTempData() {
|
||||
// 1. 隱藏選單與取消選取狀態,避免將選取的樣式存進去
|
||||
this.isMenuVisible = false;
|
||||
if (this.currentTargetLine) {
|
||||
this.currentTargetLine.classList.remove('selected-line');
|
||||
}
|
||||
|
||||
saveData() {
|
||||
this.isMenuVisible = false;
|
||||
if (this.currentTargetLine) {
|
||||
this.currentTargetLine.classList.remove('selected-line');
|
||||
}
|
||||
console.log(this.num, this.shuwenType, this.defaultTemplate);
|
||||
const editor = this.$refs.editorRef;
|
||||
if (editor) {
|
||||
console.log(editor.innerHTML);
|
||||
alert('已成功擷取 HTML 結構至 Console!');
|
||||
}
|
||||
},
|
||||
// 2. 檢核:必須要有疏文類型
|
||||
if (!this.shuwenType) {
|
||||
alert("請先選擇【疏文類型】(消災/超薦)!");
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. 取得範本名稱 (使用原生 prompt,您也可以後續在 UI 加上 input)
|
||||
const templateName = prompt("請輸入新範本名稱:");
|
||||
if (!templateName || templateName.trim() === "") {
|
||||
return; // 使用者按取消或未輸入
|
||||
}
|
||||
|
||||
// 4. 取得編輯器內的 HTML 內容
|
||||
const editor = this.$refs.editorRef;
|
||||
const htmlContent = editor ? editor.innerHTML : '';
|
||||
|
||||
// 5. 組合 API Payload (對應 dbo.shuwen_temp 資料表)
|
||||
// 註:num 為 IDENTITY(1,1) 自動遞增,不需傳入
|
||||
// 註:reg_time 通常由後端 DB 的 GETDATE() 或程式產生,此處不強制傳入
|
||||
const payload = {
|
||||
shuwen_type: this.shuwenType,
|
||||
name: templateName.trim(),
|
||||
contents: htmlContent
|
||||
};
|
||||
|
||||
console.log('準備另存疏文範本:', payload);
|
||||
|
||||
// 6. 發送 API 請求 (請依據您的實際路由進行修改)
|
||||
axios.post(HTTP_HOST + 'api/shuwens/SaveAsShuwenTemplate', payload)
|
||||
.then(response => {
|
||||
if (response.status==200) {
|
||||
alert('另存範本成功!');
|
||||
// 重新抓取範本清單,讓下拉選單即時更新
|
||||
this.initData();
|
||||
} else {
|
||||
alert('存檔失敗');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('另存範本發生錯誤:', error);
|
||||
alert('存檔發生錯誤');
|
||||
});
|
||||
},
|
||||
|
||||
saveData() {
|
||||
// 1. 隱藏選單與取消選取狀態
|
||||
this.isMenuVisible = false;
|
||||
if (this.currentTargetLine) {
|
||||
this.currentTargetLine.classList.remove('selected-line');
|
||||
}
|
||||
|
||||
// 2. 檢核:確認活動編號與疏文類型
|
||||
if (!this.num) {
|
||||
alert("缺少活動編號 (act_num),無法儲存!");
|
||||
return;
|
||||
}
|
||||
if (!this.shuwenType) {
|
||||
alert("請先選擇【疏文類型】(消災/超薦)!");
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. 取得編輯器內的 HTML 內容
|
||||
const editor = this.$refs.editorRef;
|
||||
const htmlContent = editor ? editor.innerHTML : '';
|
||||
|
||||
// 4. 組合 API Payload (對應 dbo.activity_shuwen 資料表)
|
||||
// 註:act_num 與 shuwen_type 為 PK 複合主鍵
|
||||
const payload = {
|
||||
act_num: Number(this.num),
|
||||
shuwen_num: this.defaultTemplate ? Number(this.defaultTemplate) : null,
|
||||
shuwen_type: this.shuwenType,
|
||||
contents: htmlContent,
|
||||
paper_size: this.paperSize,
|
||||
direction: this.paperOrientation
|
||||
};
|
||||
|
||||
console.log('準備儲存活動總疏文:', payload);
|
||||
|
||||
// 5. 發送 API 請求 (請依據您的實際路由進行修改)
|
||||
axios.post(HTTP_HOST + 'api/shuwens/SaveActShuwen', payload)
|
||||
.then(response => {
|
||||
if (response.status==200) {
|
||||
alert('總疏文儲存成功!');
|
||||
} else {
|
||||
alert('存檔失敗');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('儲存活動疏文發生錯誤:', error);
|
||||
alert('存檔發生錯誤');
|
||||
});
|
||||
},
|
||||
|
||||
triggerPrint() {
|
||||
//window.print();
|
||||
|
||||
@@ -59,7 +59,6 @@
|
||||
background-size: 100% 100%;
|
||||
}
|
||||
|
||||
/* 牌位外框視覺提示 */
|
||||
.tablet-element {
|
||||
cursor: pointer;
|
||||
border: 1px solid transparent;
|
||||
@@ -84,10 +83,10 @@
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
width: 30vw;
|
||||
height:80vh;
|
||||
height: 80vh;
|
||||
left: 50px;
|
||||
top: 50px;
|
||||
overflow:auto;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
#line-floating-menu h4 {
|
||||
@@ -215,7 +214,7 @@
|
||||
}
|
||||
|
||||
#line-floating-menu {
|
||||
display: none !important;
|
||||
display: !important;
|
||||
}
|
||||
|
||||
#preview-container {
|
||||
@@ -250,23 +249,23 @@
|
||||
}
|
||||
}
|
||||
</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 class="col-2 d-flex align-items-end gap-2">
|
||||
<button class="btn btn-outline-secondary" id="btn_new_template">新增範本</button>
|
||||
<button class="btn btn-primary" id="btn_save">儲存範本</button>
|
||||
</div>
|
||||
<div class="col-5">
|
||||
<div class="col-4">
|
||||
<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;">
|
||||
@@ -277,14 +276,10 @@
|
||||
</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-right" class="editor"></div>
|
||||
<div id="editor-left" class="editor printArea"></div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -292,13 +287,11 @@
|
||||
<!-- 浮動設定面板 -->
|
||||
<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>
|
||||
@@ -317,7 +310,7 @@
|
||||
<ul id="selectedItem" class="item-list"></ul>
|
||||
</div>
|
||||
|
||||
<div class="control-group" style="height:50%;">
|
||||
<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>
|
||||
@@ -329,24 +322,19 @@
|
||||
<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: "",
|
||||
shuwenList: [],
|
||||
|
||||
selectedElement: null,
|
||||
selectStyle: "",
|
||||
tabletTempNameList: [],
|
||||
@@ -361,103 +349,96 @@
|
||||
],
|
||||
|
||||
async init() {
|
||||
this.getTabletStyles();
|
||||
this.follower_id = typeof VueApp !== 'undefined' ? VueApp.follower_id : "";
|
||||
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;
|
||||
}
|
||||
});
|
||||
|
||||
await Promise.all([
|
||||
this.getTabletStyles(),
|
||||
this.getFamilies(),
|
||||
this.getTemplateNameList(),
|
||||
this.getData()
|
||||
]);
|
||||
},
|
||||
|
||||
// ================= 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 getFamilies() {
|
||||
let res = await axios.post('/api/follower/GetFollowerFamilies', { num: this.follower_id });
|
||||
if (res.status === 200 && res.data.list) this.familyMembers = res.data.list;
|
||||
},
|
||||
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);
|
||||
}
|
||||
});
|
||||
let res = await axios.post('/api/tablet/GetTabletStyleList', {});
|
||||
if (res.status === 200 && res.data.list) {
|
||||
this.allStyle = res.data.list.filter(x => x.styleID !== "000001");
|
||||
$('.styleSelector').empty().append('<option value="">請選擇版型</option>');
|
||||
this.allStyle.forEach(x => $('.styleSelector').append(`<option value="${x.styleID}">${x.name}</option>`));
|
||||
this.papers = res.data.papers || [];
|
||||
}
|
||||
},
|
||||
async getData() {
|
||||
this.properTitle = []; this.leftProperTitle = []; this.rightProperTitle = []; this.alive = [];
|
||||
//let param = [{ order_no: this.tabletItem.order_no, num: this.tabletItem.num }];
|
||||
// 參數建議統一為 num 或 follower_num,依您後端 API 實際定義為主
|
||||
let res = await axios.post('/api/follower/GetTabletTemplateList', { num: this.follower_id });
|
||||
|
||||
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);
|
||||
}
|
||||
// 🌟 修正:確保 res.data.list 存在,再對其進行 forEach
|
||||
if (res.status === 200 && res.data.list) {
|
||||
res.data.list.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 = [];
|
||||
let res = await axios.post('/api/follower/GetTabletTemplateList', { num: this.follower_id });
|
||||
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>`));
|
||||
}
|
||||
},
|
||||
|
||||
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. 動態渲染排版 =================
|
||||
// 🌟 輔助函式:統一萃取名稱,避免重複寫 if-else
|
||||
getExtractedNames(arr) {
|
||||
if (!arr || arr.length === 0) return ["請選擇"];
|
||||
return arr.map(x => typeof x === 'string' ? x : x.fam_name);
|
||||
},
|
||||
|
||||
// 🌟 輔助函式:統一根據 elementID 產生對應 HTML,解決 renderEditorLeft 與 rerenderPart 的冗餘
|
||||
generateElementHtml(d) {
|
||||
let pTitleNames = this.getExtractedNames(this.properTitle);
|
||||
let aliveNames = this.getExtractedNames(this.alive);
|
||||
let leftNames = this.getExtractedNames(this.leftProperTitle);
|
||||
let rightNames = this.getExtractedNames(this.rightProperTitle);
|
||||
let addressStr = typeof this.address === 'string' ? this.address : (this.address ? this.address.fam_name : "");
|
||||
if (!addressStr) addressStr = "請選擇";
|
||||
|
||||
if (d.elementID === 'title1') return this.buildNameList(pTitleNames, d);
|
||||
if (d.elementID === 'titletriangle') return this.buildRosterList(pTitleNames, d);
|
||||
if (d.elementID === 'combined' || d.elementID === 'tricombined') return this.buildCombinedList(pTitleNames, d);
|
||||
if (d.elementID === 'alive') return this.buildLiveList(aliveNames, d);
|
||||
if (d.elementID === 'lefttitle') return this.buildNormalWithSpace(leftNames, d, 'lefttitle');
|
||||
if (d.elementID === 'righttitle') return this.buildNormalWithSpace(rightNames, d, 'righttitle');
|
||||
if (d.elementID === 'address') return `<div class='address' style='width:${d.width}px; height:${d.height}px; font-size:${d.fontSize}px; font-family:${d.fontFamily};'>${addressStr}</div>`;
|
||||
return "";
|
||||
},
|
||||
|
||||
renderEditorLeft() {
|
||||
$('#editor-left').empty();
|
||||
|
||||
let style = this.allStyle.find(y => y.styleID == this.selectStyle);
|
||||
if (!style) return;
|
||||
|
||||
@@ -465,45 +446,13 @@
|
||||
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' });
|
||||
$('#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>`;
|
||||
}
|
||||
$('#editor-right').css({ width: '297mm', height: Number(size.height) + 'mm' });
|
||||
|
||||
(style.details || []).forEach(d => {
|
||||
if (d.isActive !== "hidden") {
|
||||
let html = this.generateElementHtml(d);
|
||||
if (html) {
|
||||
let wrapper = $(`<div class="tablet-element vertical-text"></div>`).css({
|
||||
position: 'absolute', left: d.startX + 'mm', top: d.startY + 'mm',
|
||||
@@ -515,7 +464,60 @@
|
||||
});
|
||||
},
|
||||
|
||||
// 完美修正版正名演算
|
||||
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 d = (style.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();
|
||||
this.selectedElement.append(this.generateElementHtml(d));
|
||||
},
|
||||
|
||||
renderEditorRight() {
|
||||
let $editorRight = $('#editor-right').empty();
|
||||
$editorRight.css({ "writing-mode": "vertical-rl", "text-orientation": "mixed", "padding": "40px 20px 20px 20px", "display": "block" });
|
||||
|
||||
this.shuwenList.forEach((name, idx) => {
|
||||
if (!name || name === "請選擇") return;
|
||||
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", "position": "relative"
|
||||
}).text(name);
|
||||
|
||||
let $controls = $(`
|
||||
<div class="no-print" style="position: absolute; top: -30px; left: 50%; transform: translateX(-50%); writing-mode: horizontal-tb; display: flex; gap: 4px; z-index: 10;">
|
||||
<button class="btn-shuwen-up" data-idx="${idx}" style="padding: 0 4px; font-size: 12px; cursor: pointer; border: 1px solid #ccc; border-radius: 4px; background: #fff;" title="往右移">↑</button>
|
||||
<button class="btn-shuwen-down" data-idx="${idx}" style="padding: 0 4px; font-size: 12px; cursor: pointer; border: 1px solid #ccc; border-radius: 4px; background: #fff;" title="往左移">↓</button>
|
||||
</div>
|
||||
`);
|
||||
$div.append($controls);
|
||||
$editorRight.append($div);
|
||||
});
|
||||
|
||||
$editorRight.off('click', '.btn-shuwen-up').on('click', '.btn-shuwen-up', (e) => {
|
||||
e.preventDefault(); let idx = $(e.currentTarget).data('idx');
|
||||
if (idx > 0) { [this.shuwenList[idx - 1], this.shuwenList[idx]] = [this.shuwenList[idx], this.shuwenList[idx - 1]]; this.renderEditorRight(); }
|
||||
});
|
||||
|
||||
$editorRight.off('click', '.btn-shuwen-down').on('click', '.btn-shuwen-down', (e) => {
|
||||
e.preventDefault(); let idx = $(e.currentTarget).data('idx');
|
||||
if (idx < this.shuwenList.length - 1) { [this.shuwenList[idx], this.shuwenList[idx + 1]] = [this.shuwenList[idx + 1], this.shuwenList[idx]]; this.renderEditorRight(); }
|
||||
});
|
||||
},
|
||||
|
||||
// ================= 3. 排版引擎 =================
|
||||
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;
|
||||
@@ -523,13 +525,11 @@
|
||||
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] + "暨";
|
||||
}
|
||||
|
||||
if (h<500&&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)));
|
||||
let l = name.length, isShort = (testNames.length > 1) && ((l * 40) <= (th + (v_gap / 2)));
|
||||
return { name: names[i], l: l, isShort: isShort, isMulti: false, size: 0 };
|
||||
});
|
||||
|
||||
@@ -541,7 +541,6 @@
|
||||
|
||||
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) {
|
||||
@@ -581,7 +580,6 @@
|
||||
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` });
|
||||
@@ -593,12 +591,9 @@
|
||||
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);
|
||||
let w = parseFloat(d.width), h = parseFloat(d.height), th = parseFloat(d.textHeight) || h, v_gap = parseFloat(d.textSpan) || 0, h_gap = 5;
|
||||
let shrinkLimit = (h < 300) ? 8 : ((h < 500) ? 6 : 4), 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;
|
||||
@@ -625,7 +620,46 @@
|
||||
return $namelist;
|
||||
},
|
||||
|
||||
// ================= 3. 浮動選單與互動綁定 =================
|
||||
buildRosterList(names, d) {
|
||||
const mid = names.length === 1 ? 1 : Math.floor(names.length / 2);
|
||||
const top = names.slice(0, mid), bot = names.slice(mid);
|
||||
let baseSize = parseFloat(d.fontSize), 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;
|
||||
},
|
||||
|
||||
buildCombinedList(names, d) {
|
||||
let baseFontSize = parseFloat(d.fontSize), parts = names.join("\n").split('\n');
|
||||
return $(`
|
||||
<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>`);
|
||||
},
|
||||
|
||||
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;
|
||||
},
|
||||
|
||||
// ================= 4. UI 互動與事件綁定 =================
|
||||
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(); });
|
||||
},
|
||||
|
||||
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; });
|
||||
@@ -639,7 +673,6 @@
|
||||
}
|
||||
});
|
||||
|
||||
// 點擊牌位區塊
|
||||
$('#editor-left').on('click', '.tablet-element', e => {
|
||||
e.preventDefault(); e.stopPropagation();
|
||||
this.selectedElement = $(e.currentTarget);
|
||||
@@ -649,28 +682,22 @@
|
||||
$('#line-floating-menu').show();
|
||||
});
|
||||
|
||||
// 字體大小拉桿
|
||||
$('input[type="range"]').on('input', e => {
|
||||
let val = parseInt($(e.target).val());
|
||||
$('#lbl-fontSize').text(val);
|
||||
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")
|
||||
));
|
||||
let style = this.allStyle.find(y => y.styleID == this.selectStyle);
|
||||
if (!style) return;
|
||||
let d = (style.details || []).find(y => (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();
|
||||
$("#selectedItem").empty(); $("#canselectItem").empty();
|
||||
let currentData = [], editLabel = "未定義區塊";
|
||||
|
||||
let isChaojian = (this.item_type === 'B' || (typeof VueApp !== 'undefined' && VueApp.item_type === 'B') || (this.tabletItem && this.tabletItem.item_type === 'B')) ||
|
||||
(this.allStyle.find(y => y.styleID == this.selectStyle)?.name || "").includes("超薦");
|
||||
let isChaojian = (typeof VueApp !== 'undefined' && VueApp.item_type === 'B') || (this.allStyle.find(y => y.styleID == this.selectStyle)?.name || "").includes("超薦");
|
||||
let requireDeceased = isChaojian;
|
||||
|
||||
if (element.hasClass("liveList")) { editLabel = "陽上"; currentData = this.alive; requireDeceased = false; }
|
||||
@@ -681,28 +708,20 @@
|
||||
|
||||
$("#current-edit-label").text("目前編輯:" + editLabel);
|
||||
|
||||
// 🌟 從物件中取出 fam_name 顯示
|
||||
currentData.forEach((item, index) => {
|
||||
let displayName = typeof item === 'string' ? item : item.fam_name;
|
||||
$("#selectedItem").append(`
|
||||
<li><span class="selected">${displayName}</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>`);
|
||||
$("#selectedItem").append(`<li><span class="selected">${displayName}</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; }
|
||||
|
||||
// 🌟 從已選陣列取出名稱來做比對
|
||||
let currentNames = currentData.map(x => typeof x === 'string' ? x : x.fam_name);
|
||||
let filterKeyword = $('#filter_txt').val().trim() || "";
|
||||
|
||||
this.familyMembers.forEach(f => {
|
||||
let isDead = (f.deceased === true || f.deceased === "true" || f.deceased === "True");
|
||||
if (isDead === requireDeceased && !currentNames.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;">+</span></li>`);
|
||||
$("#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;">+</span></li>`);
|
||||
}
|
||||
});
|
||||
this.bindListEvents();
|
||||
@@ -710,85 +729,101 @@
|
||||
|
||||
bindListEvents() {
|
||||
let self = this;
|
||||
|
||||
$("#canselectItem").off("click", ".itemAppend").on("click", ".itemAppend", function (e) {
|
||||
e.preventDefault();
|
||||
let name = $(this).siblings(".unselect").text();
|
||||
let fam = self.familyMembers.find(f => f.fam_name === name);
|
||||
// 🌟 從親友名單帶入時,組裝成完整物件
|
||||
let obj = {
|
||||
IsShuWen: false, deceased: fam ? (fam.deceased === true || fam.deceased === "True") : false,
|
||||
fam_gender: "", fam_name: name, fam_file: "", nospace: true, num: fam ? fam.num : 0, option_break: false
|
||||
};
|
||||
self.modifyDataAndRerender("add", obj);
|
||||
e.preventDefault(); let name = $(this).siblings(".unselect").text(); let fam = self.familyMembers.find(f => f.fam_name === name);
|
||||
self.modifyDataAndRerender("add", { IsShuWen: false, deceased: fam ? (fam.deceased === true || fam.deceased === "True") : false, fam_gender: "", fam_name: name, fam_file: "", nospace: true, num: fam ? fam.num : 0, option_break: false });
|
||||
});
|
||||
|
||||
$("#selectedItem").off("click", ".itemRemove").on("click", ".itemRemove", function (e) {
|
||||
e.preventDefault(); self.modifyDataAndRerender("remove", $(this).data("idx"));
|
||||
});
|
||||
$("#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"));
|
||||
});
|
||||
$("#selectedItem").off("click", ".itemRemove").on("click", ".itemRemove", function (e) { e.preventDefault(); self.modifyDataAndRerender("remove", $(this).data("idx")); });
|
||||
$("#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) {
|
||||
// 🌟 手動輸入時,產生帶有 num: 0 的物件
|
||||
let obj = {
|
||||
IsShuWen: false, deceased: false, fam_gender: "",
|
||||
fam_name: val, fam_file: "", nospace: true, num: 0, option_break: false
|
||||
};
|
||||
self.modifyDataAndRerender("add", obj);
|
||||
$('#custom_txt').val('');
|
||||
}
|
||||
if (val) { self.modifyDataAndRerender("add", { IsShuWen: false, deceased: false, fam_gender: "", fam_name: val, fam_file: "", nospace: true, num: 0, option_break: false }); $('#custom_txt').val(''); }
|
||||
});
|
||||
|
||||
$('#filter_txt').off("keyup").on('keyup', () => {
|
||||
if (this.selectedElement) this.renderItemMenu(this.selectedElement.children().first());
|
||||
});
|
||||
$('#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();
|
||||
this.selectStyle = $(e.target).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("範本載入失敗!");
|
||||
}
|
||||
this.selectedTemplate = this.tabletTempNameList.find(x => x.num == template_num);
|
||||
if (!this.selectedTemplate) return;
|
||||
|
||||
if (this.selectedTemplate.tablet) {
|
||||
let tablet = JSON.parse(this.selectedTemplate.tablet);
|
||||
this.properTitle = tablet.mid_items || []; this.alive = tablet.left_items || []; this.leftProperTitle = tablet.mid_left || []; this.rightProperTitle = tablet.mid_right || []; this.address = tablet.addr_items || "";
|
||||
} else { this.properTitle = []; this.alive = []; this.leftProperTitle = []; this.rightProperTitle = []; this.address = ""; }
|
||||
|
||||
if (this.selectedTemplate.shuwen && this.selectedTemplate.shuwen !== "[]") this.shuwenList = JSON.parse(this.selectedTemplate.shuwen);
|
||||
else this.shuwenList = [...this.getExtractedNames(this.properTitle), ...this.getExtractedNames(this.leftProperTitle), ...this.getExtractedNames(this.rightProperTitle)];
|
||||
|
||||
this.selectStyle = this.selectedTemplate.style_id; $('.styleSelector').val(this.selectStyle);
|
||||
$('#template_name_txt').val(this.selectedTemplate.name || "");
|
||||
this.renderEditorLeft(); this.renderEditorRight();
|
||||
if (this.selectedElement) this.renderItemMenu(this.selectedElement.children().first());
|
||||
} catch (error) { alert("範本載入失敗!資料格式可能有誤。"); }
|
||||
});
|
||||
|
||||
$('#btn_save').off('click').on('click', async function (e) {
|
||||
e.preventDefault();
|
||||
let tablet = { mid_items: self.properTitle, left_items: self.alive, mid_left: self.leftProperTitle, mid_right: self.rightProperTitle, addr_items: self.address, style: self.selectStyle }
|
||||
let thenum = $("#templateSel").val();
|
||||
let data = { num: thenum == "" ? "0" : thenum, name: $("#template_name_txt").val(), follower_num: self.follower_id, style_id: $(".styleSelector").val(), tablet: JSON.stringify(tablet), shuwen: JSON.stringify(self.shuwenList) }
|
||||
await axios.post('/api/follower/SaveTabletTemplate', data).then(res => {
|
||||
if (res.status == 200) {
|
||||
|
||||
//alert("儲存成功!");
|
||||
VueApp.snackbar.show = true;
|
||||
VueApp.snackbar.message = "儲存成功";
|
||||
self.getTemplateNameList();
|
||||
|
||||
// (可選) 如果您希望存檔後,下拉選單保持在剛存好的那個範本,
|
||||
// 可以在這裡把 thenum 塞回去(前提是 API 有回傳新 ID 或原本就是更新舊檔)
|
||||
if (thenum !== "") {
|
||||
$("#templateSel").val(thenum);
|
||||
}
|
||||
} else {
|
||||
VueApp.snackbar.show = true;
|
||||
VueApp.snackbar.message = "儲存失敗";
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
// ==========================================
|
||||
// 🌟 3. 儲存按鈕事件 (含範本資料)
|
||||
// 🌟 4. 新增範本按鈕事件 (清空所有資料)
|
||||
// ==========================================
|
||||
$('#btn_save').off('click').on('click', async function (e) {
|
||||
$('#btn_new_template').off('click').on('click', (e) => {
|
||||
e.preventDefault();
|
||||
let tablet = {
|
||||
mid_items: self.properTitle,
|
||||
left_items: self.alive,
|
||||
mid_left: self.leftProperTitle,
|
||||
mid_right: self.rightProperTitle,
|
||||
addr_items: self.address,
|
||||
style: self.selectStyle,
|
||||
|
||||
// 1. 清空頂端的選單與名稱
|
||||
$("#templateSel").val("");
|
||||
$("#template_name_txt").val("");
|
||||
|
||||
// 2. 清空所有名單資料與狀態
|
||||
this.selectedTemplate = {};
|
||||
this.properTitle = [];
|
||||
this.alive = [];
|
||||
this.leftProperTitle = [];
|
||||
this.rightProperTitle = [];
|
||||
this.address = "";
|
||||
this.shuwenList = [];
|
||||
|
||||
// 3. 重新渲染畫面 (這時會印出空的牌位區塊)
|
||||
this.renderEditorLeft();
|
||||
this.renderEditorRight();
|
||||
|
||||
// 4. 如果浮動選單目前開著,也要同步清空選單裡的「已選」清單
|
||||
if (this.selectedElement) {
|
||||
let target = this.selectedElement.children().first();
|
||||
this.renderItemMenu(target);
|
||||
}
|
||||
let data = {
|
||||
num: 0, name: $("#template_name_txt").val(), follower_num: VueApp.follower_id,
|
||||
style_id: $(".styleSelector").val(), tablet: JSON.stringify(tablet), shuwen: ""
|
||||
}
|
||||
console.log("btn_save:", data);
|
||||
});
|
||||
},
|
||||
|
||||
@@ -802,197 +837,26 @@
|
||||
else if (target.hasClass("righttitle")) targetArr = this.rightProperTitle;
|
||||
else if (target.hasClass("address")) {
|
||||
if (action === "add") this.address = typeof payload === 'string' ? payload : payload.fam_name;
|
||||
else if (action === "remove") this.address = "";
|
||||
targetArr = null;
|
||||
else if (action === "remove") this.address = ""; targetArr = null;
|
||||
}
|
||||
|
||||
if (targetArr) {
|
||||
if (action === "add") {
|
||||
// 🌟 檢查是否已經存在 (比對物件的 fam_name)
|
||||
if (targetArr.some(x => x.fam_name === payload.fam_name)) { alert("名稱已存在!"); return; }
|
||||
targetArr.push(payload);
|
||||
}
|
||||
else if (action === "remove") { targetArr.splice(payload, 1); } // 🌟 刪除改用 index
|
||||
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]];
|
||||
if (targetArr !== this.alive && targetArr !== null) this.shuwenList.push(payload.fam_name);
|
||||
} else if (action === "remove") {
|
||||
if (targetArr !== this.alive && targetArr !== null) {
|
||||
let nameToRemove = typeof targetArr[payload] === 'string' ? targetArr[payload] : targetArr[payload].fam_name;
|
||||
let sIdx = this.shuwenList.indexOf(nameToRemove); if (sIdx > -1) this.shuwenList.splice(sIdx, 1);
|
||||
}
|
||||
targetArr.splice(payload, 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();
|
||||
if (this.renderEditorRight) 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 = "";
|
||||
|
||||
// 🌟 在傳入排版引擎前,統一 map 成字串陣列
|
||||
let pTitleNames = this.properTitle.map(x => typeof x === 'string' ? x : x.fam_name);
|
||||
let aliveNames = this.alive.map(x => typeof x === 'string' ? x : x.fam_name);
|
||||
let leftNames = this.leftProperTitle.map(x => typeof x === 'string' ? x : x.fam_name);
|
||||
let rightNames = this.rightProperTitle.map(x => typeof x === 'string' ? x : x.fam_name);
|
||||
|
||||
if (target.hasClass("nameList")) html = this.buildNameList(pTitleNames, d);
|
||||
else if (target.hasClass("rosterList")) html = this.buildRosterList(pTitleNames, d);
|
||||
else if (target.hasClass("ancestor-wrapper")) html = this.buildCombinedList(pTitleNames, d);
|
||||
else if (target.hasClass("liveList")) html = this.buildLiveList(aliveNames, d);
|
||||
else if (target.hasClass("lefttitle")) html = this.buildNormalWithSpace(leftNames, d, 'lefttitle');
|
||||
else if (target.hasClass("righttitle")) html = this.buildNormalWithSpace(rightNames, 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();
|
||||
$editorRight.css({
|
||||
"writing-mode": "vertical-rl", "text-orientation": "mixed", "padding": "20px", "display": "block"
|
||||
});
|
||||
|
||||
// 🌟 同樣從物件萃取 fam_name
|
||||
let syncNames = [
|
||||
...this.properTitle.map(x => x.fam_name),
|
||||
...this.leftProperTitle.map(x => x.fam_name),
|
||||
...this.rightProperTitle.map(x => x.fam_name)
|
||||
];
|
||||
|
||||
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);
|
||||
});
|
||||
},
|
||||
this.renderItemMenu(target); this.rerenderPart(); this.renderEditorRight();
|
||||
}
|
||||
};
|
||||
|
||||
// ================= 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>
|
||||
$(() => { designer.init(); });
|
||||
</script>
|
||||
|
||||
@@ -1010,7 +1010,8 @@
|
||||
|
||||
try {
|
||||
// 嘗試獲取家族成員數據
|
||||
const response = await axios.get(HTTP_HOST + 'api/familymembers/follower/' + this.follower_id);
|
||||
console.log("哩來哩來:",this.follower_id)
|
||||
const response = await axios.get( '/api/familymembers/follower/' + this.follower_id);
|
||||
familyMembers = response.data;
|
||||
} catch (error) {
|
||||
console.warn('error:', error);
|
||||
|
||||
@@ -502,7 +502,7 @@
|
||||
}
|
||||
let long_s = Number(d.fontSize);
|
||||
let short_s = Number(d.fontSize);
|
||||
if (mid.length==1&&mid[0].includes("暨無始劫以來累世冤親債主")) {
|
||||
if (Number(d.height)<500&&mid.length == 1 && mid[0].includes("暨無始劫以來累世冤親債主")) {
|
||||
mid = mid[0].split("暨")
|
||||
mid[0] = mid[0] + "暨";
|
||||
console.log("mid:",mid)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user