Files
17168ERP/web/admin/print/TabletDesign.aspx
T
2026-07-17 17:40:17 +08:00

1438 lines
70 KiB
Plaintext
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.
<%@ Page Title="" Language="C#" MasterPageFile="~/admin/Templates/TBS5ADM001/MasterPage.master" AutoEventWireup="true" CodeFile="TabletDesign.aspx.cs" Inherits="admin_print_TabletDesign" %>
<%@ Register Src="~/admin/_uc/alert.ascx" TagPrefix="uc1" TagName="alert" %>
<asp:Content ID="Content1" ContentPlaceHolderID="page_header" runat="Server">
<style>
.canvas-area {
min-height: 100vw !important;
}
.limit-text {
display: inline-block; /* 將 span 轉換為區塊,允許設定寬度 */
width: 100px; /* 設定可視寬度 */
overflow: hidden; /* 將超過容器的文字隱藏 */
white-space: nowrap; /* 強制文字不換行 */
text-overflow: ellipsis;/* 讓超出部分的文字顯示為省略號 (...) */
word-break: break-all;
overflow-wrap: break-word;
font-weight:900;
font-size:large;
}
.custom-menu {
position: fixed; /* 關鍵:使用 fixed 定位 */
background: white;
border: 1px solid #ccc;
list-style: none;
padding: 5px 0;
box-shadow: 2px 2px 5px rgba(0,0,0,0.2);
z-index: 999;
}
</style>
</asp:Content>
<asp:Content ID="Content2" ContentPlaceHolderID="page_nav" runat="Server">
</asp:Content>
<asp:Content ID="Content3" ContentPlaceHolderID="ContentPlaceHolder1" runat="Server">
<div id="content" class="container-lg">
<v-tabs
v-model="tab"
background-color="primary"
centered
dark
icons-and-text>
<v-tabs-slider></v-tabs-slider>
<v-tab href="#版型">
版型
<v-icon>mdi-book-open-outline</v-icon>
</v-tab>
<v-tab href="#內容尺寸">
內容尺寸
<v-icon>mdi-bowl-mix-outline</v-icon>
</v-tab>
<v-tab href="#列印尺寸">
列印尺寸
<v-icon>mdi-dome-light</v-icon>
</v-tab>
<v-tab href="#列印模式">
列印模式
<v-icon>mdi-weather-cloudy</v-icon>
</v-tab>
<v-tab href="#底圖">
底圖
<v-icon>mdi-account-circle-outline</v-icon>
</v-tab>
<v-tab href="#牌位顯示項目">
牌位顯示項目
<v-icon>mdi-flower</v-icon>
</v-tab>
<v-tab href="#存檔">
存檔
<v-icon>mdi-flower</v-icon>
</v-tab>
</v-tabs>
<v-tabs-items v-model="tab">
<v-tab-item
value="版型">
<v-card >
<v-card-text>
<!-- 使用 v-tabs 並設定 fixed-tabs 或 center-active -->
<v-row>
<v-col cols="12" class="pa-0">
<v-tabs
class="fixed-bottom-tabs w-100"
bg-color="surface"
color="primary"
align-tabs="start"
center-active
show-arrows
v-resize="onTabsResize"
>
<v-tab v-for="item in allStyle" :key="item.styleID" :value="item.styleID" class="d-flex flex-column py-2">
<button tytpe="button" class="btn btn-info" @click.prevent="changeStyle(item.styleID)"
@contextmenu.prevent="openStyleMenu($event,item.styleID)">
<span class=" limit-text mt-1">{{ item.name }}</span></button>
</v-tab>
</v-tabs>
</v-col>
</v-row>
<v-menu
absolute
offset-y
:position-x="styleMenuX"
:position-y="styleMenuY"
v-model="showStyleMenu"
>
<v-list>
<v-list-item
:key="showStyleMenu">
<v-list-item-title>
<button class="btn btn-primary" @click.prevent="removeStyle()">刪除</button>
</v-list-item-title>
</v-list-item>
</v-list>
</v-menu>
<%--<ul class="dropdown-menu style-menu">
</ul>--%>
</v-card-text>
</v-card>
</v-tab-item>
<v-tab-item
value="內容尺寸">
<v-card flat>
<v-card-text>
<div class="form-floating mb-3">
<v-row>
<v-col cols="12" class="pa-0">
<v-tabs
class="fixed-bottom-tabs w-100"
bg-color="surface"
color="primary"
align-tabs="start"
center-active
>
<v-tab v-for="item in allSize" :key="item.id" :value="item.id" class="d-flex flex-column py-2">
<button tytpe="button" class="btn btn-info" @click.prevent="changePaper1(item)"><span class=" limit-text mt-1">{{ item.name }}</span></button>
</v-tab>
</v-tabs>
</v-col>
</v-row>
</div>
</v-card-text>
</v-card>
</v-tab-item>
<v-tab-item
value="列印尺寸">
<v-card flat>
<v-card-text>
<v-row>
<v-col cols="12" class="pa-0">
<v-tabs
class="fixed-bottom-tabs w-100"
bg-color="surface"
color="primary"
align-tabs="start"
center-active
>
<v-tab v-for="item in allSize" :key="item.id" :value="item.id" class="d-flex flex-column py-2">
<button tytpe="button" class="btn btn-info" @click.prevent="changePrintSize(item.id)"><span class=" limit-text mt-1">{{ item.name }}</span></button>
</v-tab>
</v-tabs>
</v-col>
</v-row>
</v-card-text>
</v-card>
</v-tab-item>
<v-tab-item
value="列印模式">
<v-card flat>
<v-card-text>
<v-tabs
class="fixed-bottom-tabs w-100"
bg-color="surface"
color="primary"
align-tabs="start"
center-active
>
<v-tab key="combine" value="combine" class="d-flex flex-column py-2">
<button tytpe="button" class="btn btn-info" @click.prevent="changePrintMode('combine')"><span class=" limit-text mt-1">合併</span></button>
</v-tab>
<v-tab key="origin" value="origin" class="d-flex flex-column py-2">
<button tytpe="button" class="btn btn-info" @click.prevent="changePrintMode('origin')"><span class=" limit-text mt-1">原尺寸</span></button>
</v-tab>
</v-tabs>
</v-card-text>
</v-card>
</v-tab-item>
<v-tab-item
value="底圖">
<v-card flat>
<v-card-text>
<v-tabs
class="fixed-bottom-tabs w-100"
bg-color="surface"
color="primary"
align-tabs="start"
center-active
>
<v-tab v-for="item in bg" :key="item.name" :value="item.name" class="d-flex flex-column py-2">
<button tytpe="button" class="btn btn-info" @click.prevent="changeBg(item)"><span class=" limit-text mt-1">{{ item.name }}</span></button>
</v-tab>
</v-tabs>
</v-card-text>
</v-card>
</v-tab-item>
<v-tab-item
value="牌位顯示項目">
<v-card flat>
<v-card-text>
<v-tabs
class="fixed-bottom-tabs w-100"
bg-color="surface"
color="primary"
align-tabs="start"
center-active
>
<v-tab key="address" value="address" class="d-flex flex-column py-2">
<button tytpe="button" class="btn btn-info" @click.prevent="displayItem('address')"><span class=" limit-text mt-1">地址欄</span></button>
</v-tab>
<v-tab key="title1" value="title1" class="d-flex flex-column py-2">
<button tytpe="button" class="btn btn-info" @click.prevent="displayItem('title1')"><span class=" limit-text mt-1">牌位正名</span></button>
</v-tab>
<v-tab key="lefttitle" value="lefttitle" class="d-flex flex-column py-2">
<button tytpe="button" class="btn btn-info" @click.prevent="displayItem('lefttitle')"><span class=" limit-text mt-1">左正名</span></button>
</v-tab>
<v-tab key="righttitle" value="righttitle" class="d-flex flex-column py-2">
<button tytpe="button" class="btn btn-info" @click.prevent="displayItem('righttitle')"><span class=" limit-text mt-1">右正名</span></button>
</v-tab>
<v-tab key="titletriangle" value="titletriangle" class="d-flex flex-column py-2">
<button tytpe="button" class="btn btn-info" @click.prevent="displayItem('titletriangle')"><span class=" limit-text mt-1">品字名單</span></button>
</v-tab>
<v-tab key="combined" value="combined" class="d-flex flex-column py-2">
<button tytpe="button" class="btn btn-info" @click.prevent="displayItem('combined')"><span class=" limit-text mt-1">雙姓合併置中</span></button>
</v-tab>
<v-tab key="alive" value="alive" class="d-flex flex-column py-2">
<button tytpe="button" class="btn btn-info" @click.prevent="displayItem('alive')"><span class=" limit-text mt-1">陽上報恩</span></button>
</v-tab>
</v-tabs>
</v-card-text>
</v-card>
</v-tab-item>
<v-tab-item
value="存檔">
<v-card flat>
<v-card-text>
<span class="btn btn-sm btn-outline-info w-100 mb-2" onclick="saveStyle()">存檔</span>
</v-card-text>
</v-card>
</v-tab-item>
</v-tabs-items>
</div>
<div class="d-flex overflow-hidden" style="height: 1920px">
<div class="p-2 text-white floting-box" style="width: 250px; left: 20px; top: 80px;">
<%-- <h6 class="border-bottom pb-2">設計工具箱</h6>--%>
<div class="input-group mb-3">
<v-list flat disabled style="width: 250px;">
<v-subheader>型版屬性</v-subheader>
<v-list-item-group color="primary">
<v-list-item >
<v-list-item-content>
<v-list-item-title>型版名稱:</v-list-item-title>
<input type="text" v-model="styleName" />
</v-list-item-content>
</v-list-item>
<v-list-item >
<v-list-item-content>
<v-list-item-title>內容尺寸:{{getParseName(paperSize,"content")}}</v-list-item-title>
</v-list-item-content>
</v-list-item>
<v-list-item >
<v-list-item-content>
<v-list-item-title >列印尺寸:{{ getParseName(printSize,"print") }}</v-list-item-title>
</v-list-item-content>
</v-list-item>
<v-list-item >
<v-list-item-content>
<v-list-item-title >列印模式:{{ getParseName(printMode,"mode") }}</v-list-item-title>
</v-list-item-content>
</v-list-item>
<v-list-item >
<v-list-item-content>
<v-list-item-title> 底圖:{{ backendInp }}</v-list-item-title>
</v-list-item-content>
</v-list-item>
</v-list-item-group>
</v-list>
</div>
</div>
<div id="printArea" style="width: 100vw; height: 100vh">
<div class="canvas-area flex-grow-1 overflow-auto d-flex flex-column align-items-center position-relative" id="canvas">
<div class="tablet-paper">
</div>
</div>
</div>
<div id="attr-box" class="d-none p-2 bg-dark text-whit floting-box" style="top: 80px; right: 20px">
<div class="form-floating mb-3">
<textarea id="inp-text" class="form-control form-control-sm mb-2" rows="5"></textarea>
<label class="small" for="inp-text">內容文字</label>
</div>
<div class="form-floating mb-3">
<input type="number" id="inp-size" class="form-control form-control-sm mb-2">
<label class="small" for="inp-size">字體大小 (pt)</label>
</div>
<div class="row">
<div class="col-6">
<div class="form-floating mb-3">
<input type="number" id="xPosition" class="form-control form-control-sm mb-2" onchange="changePosition()">
<label class="small" for="xPosition">X</label>
</div>
</div>
<div class="col-6">
<div class="form-floating mb-3">
<input type="number" id="yPosition" class="form-control form-control-sm mb-2" onchange="changePosition()">
<label class="small" for="yPosition">Y</label>
</div>
</div>
</div>
<div class="row">
<div class="col-6">
<div class="form-floating mb-3">
<input type="number" id="width" class="form-control form-control-sm mb-2">
<label class="small" for="width">寬度</label>
</div>
</div>
<div class="col-6">
<div class="form-floating mb-3">
<input type="number" id="height" class="form-control form-control-sm mb-2">
<label class="small" for="height">高度</label>
</div>
</div>
</div>
<div class="row">
<div class="col-6">
<div class="form-floating mb-3">
<input type="number" id="textWidth" class="form-control form-control-sm mb-2">
<label class="small" for="textWidth">文字寬度</label>
</div>
</div>
<div class="col-6">
<div class="form-floating mb-3">
<input type="number" id="textHeight" class="form-control form-control-sm mb-2">
<label class="small" for="textHeight">文字高度</label>
</div>
</div>
</div>
<div class="row">
<div class="col-6">
<div class="form-floating mb-3">
<input type="number" id="textSpan" class="form-control form-control-sm mb-2">
<label class="small" for="textSpan">文字間距</label>
</div>
</div>
</div>
</div>
</div>
<div class="position-fixed top-50 start-50 p-3" style="z-index: 10001">
<div id="liveToast" class="toast hide " role="alert" aria-live="assertive"
data-bs-delay="3000" aria-atomic="true"
style="color: white; background-color: dimgray; opacity: 1; font-size: 24px;">
<div class="toast-header">
<strong class="me-auto">提示訊息</strong>
<button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="Close"></button>
</div>
<div class="toast-body" id="toast_body">
</div>
</div>
</div>
<v-snackbar
v-model="snackbar.show"
timeout="2000">
{{ snackbar.text }}
<template v-slot:action="{ attrs }">
<v-btn
text
v-bind="attrs"
@click="snackbar.show = false"
>
關閉
</v-btn>
</template>
</v-snackbar>
</asp:Content>
<asp:Content ID="Content4" ContentPlaceHolderID="offCanvasRight" runat="Server">
</asp:Content>
<asp:Content ID="Content5" ContentPlaceHolderID="footer_script" runat="Server">
<script>
let VueApp = new Vue({
el: '#app',
vuetify: new Vuetify(vuetify_options),
data() {
return {
snackbar: {
show: false,
text: "",
},
tab: "型版",
value:"",
elements: [],
activeId: null,
styleID: null,
paper: { width: "100", height: "272" },
rosterLimit: 10,
allSize: [
],
bg: [
{ name: "超薦1", path: "../../admin/print/html/tablet-1_new.svg" },
{ name: "超薦2", path: "../../admin/print/html/tablet-1B_new.svg" },
{ name: "消災1", path: "../../admin/print/html/tablet-2.svg" },
{ name: "消災2", path: "../../admin/print/html/tablet-2B.svg" },
{ name: "消災3", path: "../../admin/print/html/tablet-2C.svg" },
{ name: "消災4", path: "../../admin/print/html/tablet-2D.svg" }
],
allStyle: [
],
tabletElements: [
],
orientation: "portrait",
toast: null,
paperSize: "",
backendInp: "",
printSize: "",
printMode: "",
perpage: "",
//rosterLimit: "",
newPaper: {
paperName: "",
paperWidth: "",
paperHeight:""
},
styleName: "",
showStyleMenu: false,
styleMenuX: 0,
styleMenuY: 0,
styleMenuStyle:""
}
},
computed: {
},
mounted() {
this.init()
},
methods: {
onTabsResize(e) {
console.log(e)
// 當畫面變動時,呼叫 Vuetify 內部的呼叫方法重新計算寬度
//if (this.$refs.myTabs) {
// this.$refs.myTabs.onResize();
//}
},
getParseName(id, classes) {
if (id != null) {
if (classes === "style") {
let data = this.allStyle.find(x => x.styleID === id)
return data?.name
} else if (classes === "content") {
let data = this.allSize.find(x => x.id === id)
return data?.name
} else if (classes === "print") {
let data = this.allSize.find(x => x.id === id)
return data?.name
} else if (classes === "mode") {
return id === "origin" ? "原尺寸" : "合併";
}
}
return ""
},
init() {
//$(".tablet-element").draggable({});
this.getPaperSize();
this.getElements();
this.getStyles();
this.bindEvents();
this.bindBackend();
this.bg.forEach(x => {
$("#backendInp").append("<option value='" + x.name + "'>" + x.name + "</option>")
});
//$("#paperOrientation").html("<span>直向</span>")
this.toast = new bootstrap.Toast($('#liveToast'));
},
openStyleMenu(e, id) {
this.showStyleMenu = true;
this.styleMenuX = e.clientX;
this.styleMenuY = e.clientY;
this.styleMenuStyle = `top:${e.clientY}px,left:${e.clientX}px`
this.styleID=id
},
closeStyleMenu() {
this.showStyleMenu = false;
},
print() {
let self = this;
let s = this.allStyle.find(x => x.styleID == this.styleID)
let size = this.allSize.find(x => x.id = s.paperSize)
let w = window.open('', '_blank');
if (!w) {
$("#toast_body").html(`${"請允許瀏覽器開啟彈出式視窗!"}`)
self.toast.show();
return;
}
// 1. 抓取您原本網頁定義的 HTTP_HOST (您的全域變數),如果沒有則抓取當前網址
let hostUrl = typeof HTTP_HOST !== 'undefined' ? HTTP_HOST : (window.location.protocol + "//" + window.location.host);
if (!hostUrl.endsWith('/')) hostUrl += '/';
// 2. 抓出畫布完整的 HTML
let canvasHtml = $("#printArea").html();
// 3. 【關鍵修復】把所有的 "../../" 替換成完整的網址,解決底圖破圖問題!
canvasHtml = canvasHtml.replace(/\.\.\/\.\.\//g, hostUrl);
// 4. 組合列印視窗的 HTML
let htmlContent = `
<!DOCTYPE html>
<html>
<head>
<title>預覽列印 - 牌位設計</title>
<meta charset="utf-8">
<style>
/* 名單金字塔佈局容器 */
.roster-container {
width: 100%; height: 100%;
writing-mode: vertical-rl; /* 直書 */
display: flex;
/*flex-direction: column;*/ /* 雖然是直書,但物理上我們是將「上層區」和「下層區」垂直堆疊 */
flex-direction: row; /* 上下分層 (Top / Bottom) */
align-items: center; /* 左右置中對齊 */
justify-content: center;
gap: 20px; /* 這是「上層」跟「下層」之間的距離,可以設大一點 */
}
.roster-row {
display: flex;
flex-direction: row-reverse; /* 直書由右至左,所以 Row 要反向或依需求調整 */
justify-content: center;
gap: 8px; /* 名字之間的間距 */
margin-left: 10px; /* 上下排之間的間距 (因為是直書,margin-left 是物理上的左邊/下方) */
}
.name-group {
display: flex;
flex-direction: column; /* ★★★ 關鍵:這讓名字左右並排 ★★★ */
justify-content: center;
align-items: center;
/* gap 由 HTML 動態綁定 */
}
.roster-name {
text-orientation: upright;
/*font-weight: bold;*/
white-space: nowrap;
line-height: 1.2;
font-family: ${style.fontFamily};
/* 確保名字本身不會佔據過多寬度導致間距看起來很大 */
width: fit-content;
}
.vertical-text {
writing-mode: vertical-rl !important;
-webkit-writing-mode: vertical-rl !important;
text-orientation: mixed !important;
}
/* 【修復底圖沒出現】強制印出背景設定 */
* {
-webkit-print-color-adjust: exact !important;
print-color-adjust: exact !important;
color-adjust: exact !important;
}
/* 【修復排版沒照設定】覆寫原本 HTML 裡的 fixed,並防止 Bootstrap 洗掉 absolute */
.tablet-paper {
position: relative !important;
margin: 0 auto !important;
page-break-after: always !important;
box-shadow: none !important;
width: var(--page-w);
height: var(--page-h);
}
.tablet-element {
position: absolute !important; /* 絕對不能被 Bootstrap 覆蓋為 static */
color: black !important; /* 確保文字是黑色的 */
}
// .ancestor-wrapper {
// /* 重置為橫向流,這樣 column 才會是真正的上下堆疊 */
// writing-mode: horizontal-tb;
// display: flex;
// flex-direction: column;
// align-items: center; /* 水平置中 */
// justify-content: flex-start;
// width: fit-content;
//}
.main-name {
line-height: 1.2;
/* 保持大字 */
}
.sub-text {
font-size: 0.6em; /* 縮小字體 */
line-height: 1.2;
margin-top: 4px; /* 與上方林張的間距 */
white-space: nowrap; /* 避免自動換行 */
writing-mode:vertical-rl
}
/* 列印時的紙張大小與頁面歸零 */
@media print {
@page {
/* 動態套用您設定的紙張寬高 */
size: ${size.width}mm ${size.height}mm portrait;
margin: 0;
}
body, html {
margin: 0 !important;
padding: 0 !important;
}
body{
--page-w: ${size.width}mm;
--page-h: ${size.height}mm;
--font-max: 22pt;
--bg-padding: 0;
}
}
</style>
</head>
<body>
${canvasHtml}
</body>
</html>
`;
w.document.write(htmlContent);
w.document.close();
// 5. 將等待時間稍微拉長至 1.2 秒,確保大張的 SVG 底圖與外部字體(如標楷體)下載完畢
setTimeout(() => {
w.focus();
w.print();
}, 1200);
},
changeOrientation() {
this.orientation = (this.orientation === "portrait" ? "land" : "portrait");
$("#paperOrientation").empty();
let ori = this.orientation == "portrait" ? "直向" : "橫向";
$("#paperOrientation").html("<span>" + ori + "</span>");
let paperSize = $("#paperSize").val()
let size = this.allSize.find(x => x.name == paperSize)
this.paper.width = this.orientation == "portrait" ? size.width : size.height;
this.paper.height = this.orientation == "portrait" ? size.height : size.width;
$(".tablet-paper").css({
"background-color": "white",
width: this.paper.width + "mm",
height: this.paper.height + "mm",
"background-repeat": "no-repeat!important",
});
},
async getPaperSize() {
let self = this
self.allSize.length = 0;
$("#paperSize").html("")
$("#printSize").html("")
await axios
.post(HTTP_HOST + 'api/tablet/GetPaperSize', {})
.then(response => {
//if (response.result=="Y") {
if (response.status == "200") {
let data = response.data;
if (data.result == "Y") {
data.data.forEach(x => {
self.allSize.push({ name: x.paperName, id: x.paperID, width: x.width, height: x.height })
});
}
}
});
$("#paperSize").append("<option value='newsize' >新增尺寸</option>")
this.allSize.forEach(x => {
$("#paperSize").append("<option value='" + x.id + "'>" + x.name + "(" + x.width + "*" + x.height + ")</option>")
$("#printSize").append("<option value='" + x.id + "'>" + x.name + "(" + x.width + "*" + x.height + ")</option>");
})
},
async getElements() {
let self = this
await axios
.post(HTTP_HOST + 'api/tablet/GetTabletElement', {})
.then(response => {
console.log(response)
//if (response.result=="Y") {
if (response.status == "200") {
let data = response.data;
console.log("data:", data)
if (data.result == "Y") {
data.data.forEach(x => {
self.tabletElements.push(x);
});
}
}
});
},
async getStyles() {
let self = this
self.allSize.length = 0
$(".style-menu").html("")
await axios
.post(HTTP_HOST + 'api/tablet/GetStyleData', {})
.then(response => {
//if (response.result=="Y") {
if (response.status == "200") {
let data = response.data;
if (data.result == "Y") {
data.data.forEach(x => {
self.allStyle.push(x);
$(".style-menu").append("<li><span class='dropdown-item style-item' data-value='" + x.styleID + "'>" + x.name + "</span></li>");
});
this.bindDropdown();
}
}
//}
});
},
bindDropdown() {
let self = this;
$(".style-menu li").on("click", function (e) {
$(".style-menu li").removeClass('active');
$(this).addClass('active');
$("#styleName").val(e.target.textContent);
self.styleID = e.target.getAttribute("data-value")
self.changeStyle(e.target.getAttribute("data-value"));
let style = self.allStyle.find(x => x.styleID == e.target.getAttribute("data-value"));
if (style) {
let size = self.allSize.find(x => x.id == style.paperSize);
if (size) {
self.paper.width = size.width;
self.paper.height = size.height;
} else {
self.paper.width = 100;
self.paper.height = 272;
}
} else {
self.paper.width = 100;
self.paper.height = 272;
}
});
},
async changeStyle(id) {//切換版型,抓回明細
let self = this;
this.elements.length = 0;
let obj = this.elements
let s = this.allStyle.find(x => x.styleID == id)
let size = this.allSize.find(x => x.id == s.paperSize)
//this.showStyleMenu = true;
this.paper.width = size.width;
this.paper.height = size.height;
this.rosterLimit = s.rosterLimit;
this.paperSize = s.paperSize
this.backendInp = s.backendImg
this.printSize = s.printSize
this.printMode = s.printMode
this.perpage = s.perpage
this.styleID = id
//this.rosterLimit = s.rosterLimit
//$("#paperSize").val(s.paperSize);
//$("#backendInp").val(s.backendImg);
//$("#printSize").val(s.printSize);
//$("#printMode").val(s.printMode);
//$("#perpage").val(s.printPageCount);
//$("#rosterLimit").val(s.rosterLimit);
let img = this.bg.find(x => x.name == s.backendImg);
$(".tablet-paper").css({
'background-image': 'url(' + (img ? img.path : "") + ')', 'background-size': '100% 100%',
width: (size ? size.width : self.paper.width) + "mm",
height: (size ? size.height : self.paper.height) + "mm",
})
await axios
.post(HTTP_HOST + 'api/tablet/GetStyleDetailData', { styleID: id })
.then(response => {
if (response.status == "200") {
let data = response.data;
if (data.result == "Y") {
let details = data.data;
$(".tablet-paper").empty()
details.forEach(el => {
let te = self.tabletElements.find(x => {
return x.elementID == el.elementID
});
let config = {
id: el.elementID,
type: te.elementType,
text: te.sampleContent.replaceAll("\\n", "\n"),
x: el.startX, y: el.startY,
style: {
fontSize: el.fontSize,
fontFamily: el.fontFamily,
isVertical: true,
letterSpacing: 5, lineHeight: 1.5,
visibility: el.isActive,
},
width: el.width,
height: el.height,
textWidth: el.textWidth,
textHeight: el.textHeight,
breakLen: el.breakLen,
backendInp: el.backendImg,
textSpan: el.textSpan
};
obj.push(config)
})
this.render();
}
}
});
},
changeBg(item) {
this.backendInp=item.name
let path = this.bg.find(el => el.name == item.name)//$("#backendInp").val()
$(".tablet-paper").css({ 'background-image': 'url(' + path.path + ')', 'background-size': '100% 100%' })
},
changePrintSize(id) {
this.printSize=id
},
changePosition() {
//this.activeId = id;
const el = this.elements.find(x => x.id === this.activeId);
el.x = $('#xPosition').val();
el.y = $("#yPosition").val();
$(`.tablet-element[id="${this.activeId}"]`).css({ left: el.x + "mm", top: el.y + "mm" });
},
changePaper1(val) {
//$("#paperSize").val(val)
this.paperSize = val.id
console.log("changePaper1:",val)
this.changePaper()
},
changePaper() {
let paperSize = this.paperSize//$("#paperSize").val()
let self = this;
if (paperSize == "newsize") {
$("#sizeDiv").attr("style", "display:");
} else {
// console.log("paperSize:", paperSize);
$("#sizeDiv").attr("style", "display:none;");
let size = this.allSize.find(x => x.id == paperSize)
$(".tablet-paper").css({
"background-color": "white",
width: (size ? size.width : self.paper.width) + "mm",
height: (size ? size.height : self.paper.height) + "mm",
});
}
},
async removePaper() {
let self = this;
let paperSize =this.paperSize //$("#paperSize").val()
//console.log("paperSize:",paperSize);
if (paperSize == "newsize" || paperSize == "" || paperSize == null || paperSize == undefined) {
$("#toast_body").html("請選擇要刪除的尺寸")
self.toast.show();
return false
}
let ps = { paperSize: paperSize }
await axios
.post(HTTP_HOST + 'api/tablet/DelPaperSize', ps)
.then(response => {
if (response.status == "200") {
//if (response.data.result == "Y") {
$("#toast_body").html(`${response.data.message}`)
self.toast.show();
//}
if (response.data.result == "Y") {
self.getPaperSize()
//$("#paperSize").val("")
this.paperSize=""
this.changePaper()
}
}
});
},
async removeStyle() {
let self = this;
let sID = this.styleID
//console.log("styleID:", sID);
if (sID == "000001" || sID == null || sID == undefined) {
//$("#toast_body").html("請選擇要刪除的版型")
//self.toast.show();
this.snackbar.text = "請選擇要刪除的版型"
this.snackbar.show=true
return false
}
let ps = { styleID: sID }
await axios
.post(HTTP_HOST + 'api/tablet/DelTabletStyle', ps)
.then(response => {
if (response.status == "200") {
//if (response.data.result == "Y") {
$("#toast_body").html(`${response.data.message}`)
self.toast.show();
//}
if (response.data.result == "Y") {
self.getStyles()
}
}
});
},
async savePaperSize() {
let ps = {
paperName: this.newPaper.paperName, //$("#paperName").val(),
width: this.newPaper.paperWidth,//$("#paperWidth").val(),
height: this.newPaper.paperHeight//$("#paperHeight").val()
}
let self = this;
//console.log(ps);
await axios
.post(HTTP_HOST + 'api/tablet/SavePaperSize', ps)
.then(response => {
if (response.status == "200") {
if (response.data.result == "Y") {
$("#toast_body").html(`${"ok"}`)
self.toast.show();
self.getPaperSize()
//$("#paperSize").val("")
self.paperSize=""
self.changePaper()
}
}
});
},
changeRosterLimit() {
//this.rosterLimit = parseInt(this.rosterLimit) //parseInt($("#rosterLimit").val());
},
changePrintMode() {
},
bindEvents() {
// 文字變更連動渲染
//$('#rosterLimit').on('input', (e) =>this.)
//console.log("OOQ");
$('#inp-text').on('input', (e) => this.updateActive('text', $(e.target).val()));
$('#inp-size').on('input', (e) => this.updateActive('fontSize', $(e.target).val()));
$('#width').on('input', (e) => this.updateActive('width', $(e.target).val()))
$('#2offset').on('input', (e) => this.updateActive('2offset', $(e.target).val()))
$('#3offset').on('input', (e) => this.updateActive('3offset', $(e.target).val()))
$('#4offset').on('input', (e) => this.updateActive('4offset', $(e.target).val()))
$('#breakLen').on('input', (e) => this.updateActive('breakLen', $(e.target).val()))
$('#width').on('input', (e) => this.updateActive('width', $(e.target).val()))
$('#height').on('input', (e) => this.updateActive('height', $(e.target).val()))
$('#textWidth').on('input', (e) => this.updateActive('textWidth', $(e.target).val()))
$('#textHeight').on('input', (e) => this.updateActive('textHeight', $(e.target).val()))
$('#textSpan').on('input', (e) => this.updateActive('textSpan', $(e.target).val()))
$(document).on('mousedown', '.tablet-element', (e) => {
e.stopPropagation();
this.select($(e.currentTarget).attr('id'));
});
},
loadConfig() {
$.ajax({
type: "POST", url: "TabletDesigner.aspx/GetConfig", contentType: "application/json",
success: (res) => {
const data = JSON.parse(res.d);
this.elements = data.elements;
this.render();
}
});
},
// 處理分頁渲染邏輯
render() {
$(".tablet-paper").empty();
//const $canvas = $('#canvas').empty();
//const roster = this.elements.find(e => e.type === 'roster');
let style = this.allStyle.find(x => x.styleID == this.styleID);
let size = this.allSize.find(x => x.id == style.paperSize);
//console.log(this.paper, size);
//for (let p = 0; p < pages; p++) {
// const $paper = $('<div class="tablet-paper"></div>')
// .css({ "background-color": "red", width: this.paper.width + 'mm', height: this.paper.height + 'mm' });
let $paper = $(".tablet-paper").css({
"background-color": "white",
width: (size ? size.width : this.paper.width) + 'mm',
height: (size ? size.height : this.paper.height) + 'mm',
position: "absolute"
});
//const slice = names.slice(p * this.rosterLimit, (p + 1) * this.rosterLimit);
//const roster = this.elements.find(e => e.id === this.styleID);
let self = this;
this.elements.forEach(el => {
const names = el.text.split('\n').filter(s => s.trim());
const pages = Math.max(1, Math.ceil(names.length / self.rosterLimit));
const slice = names.slice(0 * self.rosterLimit, (0 + 1) * self.rosterLimit);
$paper.append(self.createEl(el, slice));
});
//$canvas.append($paper);
//}
//let s = this.allStyle.find(x => x.styleID == this.styleID);
//let d = this.elements.filter(x => x.styleID == this.styleID);
//console.log(s,d);
this.makeDraggable();
},
createEl(el, slice) {
let html = '';
// 1. 智慧名單渲染
if (el.type === 'roster') {
html = this.renderRoster(slice, el);
}
// 2. 正名合併置中
else if (el.type === 'combined-center') {
const parts = el.text.split('\n');
html = `<div class="ancestor-wrapper" style="width:${el.width}px !important;height:${el.height}px !important;" >
<span class="main-name" >${parts[0] || ''}</span>
<span class="sub-text">${parts[1] || ''}</span>
</div>`;
}
// 3. 地址數字轉橫排
else if (el.id === 'address') {
html = el.text.replace(/(\d+)/g, '<span class="tate-chu-yoko">$1</span>');
}
else if (el.id === 'title1') {
html = this.renderNameList(slice, el);
}
else if (el.id === 'alive') {
console.log("createEl:", slice, el)
html = this.renderLiveList(slice, el);
}
else {
html = el.text;
}
return $(`<div class="tablet-element vertical-text ${this.activeId === el.id ? 'selected' : ''}" id="${el.id}"></div>`)
.css({
position: "absolute", left: el.x + "mm", top: el.y + "mm", fontSize: el.style.fontSize + 'pt', fontFamily: el.style.fontFamily, "z-index": 9999, visibility: el.style.visibility
//position: "absolute", left: el.startX + "mm", top: el.startY + "mm", fontSize: el.fontSize + 'pt', fontFamily: el.fontFamily, "z-index": 9999, visibility: el.isActive
})
.html(html);
},
renderNameList(names, el) {
let $namelist = $(`<div class='nameList'></div>`).css({
"writing-mode": "vertical rl",
display: "flex",
"flex-direction": "row",
"flex-wrap": "wrap",
margin: "auto",
width: `${el.width}px`,
height: `${el.height}px`,
border: "0px solid #ccc",
padding: "1px",
"font-family": el.fontFamily,
//"letter-spacing": `${el.textSpan}px`,
"column-gap": `${el.textSpan}px`,
"align-items": "center",
});
//
//"row-gap": "1px",
let self = this;
names.forEach(n => {
$namelist.append(self.renderNameSpan(n, el))
})
return $namelist;
},
renderNameSpan(name, el) {
return $(`<span>${name}</span>`).css({
display: "block",
"min-height": `${el.textHeight}px`,
"max-height": `${el.height}px`,
width: `${el.textWidth}px`,
"text-align": "justify",
"text-align-last": "justify",
"margin-left": "5px",
"text-justify": "inter-character",
});// "margin-bottom": `${el.textSpan}px`,
},
renderLiveList(names, el) {
console.log("renderLiveList:", names);
//$("#imp-text").updateActive("text",names)
let $namelist = $(`<div class='liveList'></div>`).css({
"writing-mode": "vertical rl",
display: "flex",
"flex-direction": "row",
"flex-wrap": "wrap",
margin: "auto",
width: `${el.width}px`,
height: `${el.height}px`,
border: "0px solid #ccc",
padding: "1px",
"font-family": "BiauKai",
//"letter-spacing": `${el.textSpan}px`,
"column-gap": `${el.textSpan}px`,
"align-items": "center",
});
// "column-gap": `${el.textSpan}px`,
//"row-gap": "1px",
let self = this;
names.forEach(n => {
$namelist.append(self.renderLiveSpan(n, el))
})
return $namelist;
},
renderLiveSpan(name, el) {
console.log("renderLiveSpan:", name, el
);
return $(`<span>${name}</span>`).css({
display: "block",
"min-height": `${el.textHeight}px`,
"max-height": `${el.height}px`,
width: `${el.textWidth}px`,
"text-align": "justify",
"text-align-last": "justify",
"margin-left": "5px",
"text-justify": "inter-character",
});//"margin-bottom": `${el.textSpan}px`,
},
// 品字佈局邏輯:一上二下
renderRoster(names, el) {
if (!names.length) return '';
const mid = names.length === 1 ? 1 : Math.floor(names.length / 2);
const top = names.slice(0, mid);
const bot = names.slice(mid);
const size = this.autoScale(names, el.fontSize);
let h = $(`<div class=""></div>`).css(
{
width: el.width, height: el.height,
"writing-mode": "vertical-rl", /* 直書 */
display: "flex",
"flex-direction": "row", /* 上下分層 (Top / Bottom) */
"align-items": "center", /* 左右置中對齊 */
"justify-content": "start",
gap: `${el.textSpan}px`,
});
h.append(this.renderNameGroups(top, el));
if (bot.length) h.append(this.renderNameGroups(bot, el));
return h
},
renderNameGroups(items, el) {
let g = $(`<div ></div>`).css({
"display": "flex",
"flex-direction": "column",
"justify-content": "center",
"height": el.textHeight,
})
items.forEach(x => {
g.append($(`<div class="" style="font-size:${el.style.fontSize}pt;letter-spacing: 10px;text-align: justify; text-align-last: justify;">${x}</div>`))
})
return g;
},
autoScale(names, base) {
const max = Math.max(...names.map(n => n.length), 0);
return max > 5 ? Math.max(base * (5 / max), base * 0.6) : base;
},
getPosInMm(px) {
return parseFloat((px * 25.4 / 96).toFixed(2));
},
// 拖動後同步所有頁面的位置
makeDraggable() {
const self = this;
$(".tablet-element").draggable({
start(e, ui) {
console.log("gogogogo");
},
stop(e, ui) {
const id = $(this).attr('id');
const el = self.elements.find(x => x.id === id);
el.x = self.getPosInMm(ui.position.left);
el.y = self.getPosInMm(ui.position.top);
//console.log(id,ui.position.left, ui.position.top,ui.offset.left,ui.offset.top);
$(`.tablet-element[id="${id}"]`).css({ left: el.startX + "mm", top: el.startY + "mm" });
}
});
},
select(id) {
console.log("QQ:", id);
this.activeId = id;
console.log("QQ:", this.elements);
const el = this.elements.find(x => x.id === id);
const el1 = this.elements.find(x => x.id === "titletriangle");
$('#attr-box').removeClass('d-none');
$('#inp-text').val(el.text);
$('#inp-size').val(el.style.fontSize);
$('#xPosition').val(el.x);
$("#yPosition").val(el.y);
$('#2offset').val(el.twoOffset);
$("#3offset").val(el.threeOffset);
$("#4offset").val(el.fourOffset);
$("#breakLen").val(el.breakLen);
$("#width").val(el.width);
$("#height").val(el.height);
$("#textWidth").val(el.textWidth);
$("#textHeight").val(el.textHeight);
$("#textSpan").val(el.textSpan);
//this.render();
},
updateActive(key, val) {
console.log("updateActive:", key, val)
const el = this.elements.find(x => x.id === this.activeId);
if (key === 'fontSize') el.style.fontSize = parseFloat(val);
else if (key === 'width') el.width = parseFloat(val);
else if (key === 'breakLen') el.breakLen = parseInt(val);
else if (key === '2offset') el.twoOffset = parseFloat(val);
else if (key === '3offset') el.threeOffset = parseFloat(val);
else if (key === '4offset') el.fourOffset = parseFloat(val);
else if (key === 'height') el.height = parseFloat(val);
else el[key] = val;
if (el.type == "roster") {
const names = el.text.split('\n').filter(s => s.trim());
$(`.tablet-element[id="${this.activeId}"]`).css({
position: "absolute", left: el.startX + "mm", top: el.startY + "mm", fontSize: el.style.fontSize + 'pt', fontFamily: el.fontFamily, "z-index": 9999, visibility: el.style.isActive
}).html(this.renderRoster(names.slice(0 * this.rosterLimit, (0 + 1) * this.rosterLimit), el));
} else if (el.type == "combined-center") {
console.log("combined-center");
const parts = el.text.split('\n');
html = `<div class="ancestor-wrapper" style='width:${el.width}px !important;height:${el.height}px !important' >
<span class="main-name" >${parts[0] || ''}</span>
<span class="sub-text">${parts[1] || ''}</span>
</div>`;
$(`.tablet-element[id="${this.activeId}"]`).css({
position: "absolute", left: el.startX + "mm", top: el.startY + "mm",
fontSize: el.style.fontSize + 'pt', fontFamily: el.fontFamily,
"z-index": 9999, visibility: el.style.isActive,
}).html(html);
} else if (this.activeId === "title1") {
let names = el.text.split('\n').filter(s => s.trim());
let slice = names.slice(0 * this.rosterLimit, (0 + 1) * this.rosterLimit);
html = this.renderNameList(slice, el);
$(`.tablet-element[id="${this.activeId}"]`).css({
position: "absolute", left: el.x + "mm", top: el.y + "mm", fontSize: el.style.fontSize + 'pt', fontFamily: el.style.fontFamily, "z-index": 9999, visibility: el.style.visibility
}).html(html);
} else if (this.activeId === "alive") {
let names = el.text.split('\n').filter(s => s.trim());
let slice = names.slice(0 * this.rosterLimit, (0 + 1) * this.rosterLimit);
html = this.renderLiveList(slice, el);
$(`.tablet-element[id="${this.activeId}"]`).css({
position: "absolute", left: el.x + "mm", top: el.y + "mm", fontSize: el.style.fontSize + 'pt', fontFamily: el.style.fontFamily, "z-index": 9999, visibility: el.style.visibility
}).html(html);
} else if (this.activeId === "righttitle") {
$(`.tablet-element[id="${this.activeId}"]`).css({
position: "absolute", left: el.x + "mm", top: el.y + "mm", fontSize: el.style.fontSize + 'pt',
fontFamily: el.style.fontFamily, "z-index": 9999, visibility: el.style.visibility,
width: el.width, height: el.height
}).text(el.text);
} else if (this.activeId === "lefttitle") {
$(`.tablet-element[id="${this.activeId}"]`).css({
position: "absolute", left: el.x + "mm", top: el.y + "mm", fontSize: el.style.fontSize + 'pt',
fontFamily: el.style.fontFamily, "z-index": 9999, visibility: el.style.visibility,
width: el.width, height: el.height
}).text(el.text);
} else if (this.activeId === "address") {
$(`.tablet-element[id="${this.activeId}"]`).css({
position: "absolute", left: el.x + "mm", top: el.y + "mm", fontSize: el.style.fontSize + 'pt',
fontFamily: el.style.fontFamily, "z-index": 9999, visibility: el.style.visibility,
width: el.width, height: el.height
}).text(el.text);
} else {
$(`.tablet-element[id="${this.activeId}"]`).css({
position: "absolute", left: el.x + "mm", top: el.y + "mm", fontSize: el.style.fontSize + 'pt', fontFamily: el.style.fontFamily, "z-index": 9999, visibility: el.style.visibility
}).text(el.text);
}
//this.render();
},
displayItem(id) {
console.log("displayItem:", this.elements, id);
let el = this.elements.find(x => x.id === id);
el.style.visibility = el.style.visibility === "hidden" ? "" : "hidden";
//要判斷如果所在位置超出範圍,要拉回來放
console.log("displayItem:", this.paper, el)
if (el.x > this.paper.width) {
el.x = this.paper.width / 2
}
if (el.y > this.paper.height) {
el.y = this.paper.height / 2
}
$("#" + id).css({
position: "absolute", left: el.x + "mm", top: el.y + "mm", fontSize: el.style.fontSize + 'pt', fontFamily: el.style.fontFamily, "z-index": 9999, visibility: el.style.visibility
})
},
addMulti(prefix, count) {
for (let i = 1; i <= count; i++) {
const id = `${prefix}-${Date.now()}-${i}`;
this.elements.push({
id, type: 'title', text: `正名${i}`, x: 100 - (i * 30), y: 80,
style: { fontSize: 24, fontFamily: 'DFKai-sb', isVertical: true }
});
}
//this.render();
},
addCombined() {
this.stopPropagation();
this.elements.push({
id: 'combined-' + Date.now(), type: 'combined-center', text: '林張\n氏歷代祖先', x: 130, y: 80,
style: { fontSize: 24, fontFamily: 'DFKai-sb', isVertical: true }
});
//this.render();
},
addElement() {
//console.log("QQQ");
},
wrapImageInSvg(imageBase64) {
// 建立一個簡單的 SVG 字串,將圖片鋪滿
// preserveAspectRatio="none" 確保圖片會拉伸填滿 SVG 容器
const svgString = `
<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" preserveAspectRatio="none">
<image href="${imageBase64}" x="0" y="0" width="100%" height="100%" preserveAspectRatio="none" />
</svg>
`.trim();
// 轉成 Data URI 格式
// 注意:SVG 內容若包含特殊字元需編碼,但 base64 圖片通常是安全的
// 為了保險,我們使用 encodeURIComponent 對 svgString 編碼
return `data:image/svg+xml;utf8,${encodeURIComponent(svgString)}`;
},
clickBackend() {
$("#backendInp").click();
},
bindBackend() {
$("#backendInp").on('change', function (e) {
const input = e.target;
if (input.files && input.files[0]) {
const file = input.files[0];
// 1. 先讀取檔案轉成 Base64
const reader = new FileReader();
reader.onload = (e) => {
const base64Image = e.target.result; // 這會是 data:image/png;base64,xxxx...
// 2. 將 Base64 PNG 包裝成 SVG
//const svgDataUri = this.wrapImageInSvg(base64Image);
// 3. 設定到底圖
//this.service.setBackground(svgDataUri);
$(".tablet-paper").css({ 'background-image': 'url(' + base64Image + ')', 'background-size': '100% 100%' })
};
reader.readAsDataURL(file);
}
})
},
async saveStyle() {
let self = this;
let detail = [
];
this.elements.forEach((el) => {
detail.push({
elementID: el.id, startX: el.x.toString(), startY: el.y.toString(), fontSize: el.style.fontSize, fontFamily: el.style.fontFamily,
breakLen: el.breakLen, twoOffset: el.twoOffset, threeOffset: el.threeOffset, fourOffset: el.fourOffset,
isActive: el.style.visibility, width: el.width, height: el.height, textWidth: el.textWidth, textHeight: el.textHeight, textSpan: el.textSpan
});
});
let sID = this.styleID
if (sID == "000001") {
this.styleID = ""
}
let master = {
styleID: this.styleID, styleName: this.styleName, paperSize: this.paperSize,//$("#paperSize").val(),
backendImg: this.backendInp, printSize: this.printSize, printMode: this.printMode,
orientation:"", printPageCount:this.perPage, rosterLimit: 8,
detail: detail
}
console.log(master);
let path = "SavDegignerData";
if (this.styleID != null && this.styleID != "") {
path = "UpdateDegignerData"
}
await axios
.post(HTTP_HOST + `api/tablet/${path}`, master)
.then(response => {
if (response.status == "200") {
if (response.data.result == "Y") {
$("#toast_body").html(`${"ok"}`)
self.toast.show();
}
}
});
}
}
})
//$(() => Designer.init());
</script>
</asp:Content>