Files
17168ERP/web/admin/print/TabletDesign.aspx
T
2026-07-29 20:59:49 +08:00

1899 lines
91 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 scoped >
.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;
}
/* 全域 Chrome / Edge / Safari 捲軸優化 */
::-webkit-scrollbar {
width: 6px;
height: 6px;
}
::-webkit-scrollbar-track {
background: #f1f1f1;
}
::-webkit-scrollbar-thumb {
background: #ccc;
border-radius: 3px;
}
::-webkit-scrollbar-thumb:hover {
background: #999;
}
/* 全域 Firefox 捲軸優化 */
html, body, * {
scrollbar-width: thin;
scrollbar-color: #ccc #f1f1f1;
}
</style>
</asp:Content>
<asp:Content ID="Content2" ContentPlaceHolderID="page_nav" runat="Server">
</asp:Content>
<asp:Content ID="Content3" ContentPlaceHolderID="ContentPlaceHolder1" runat="Server">
<div class="d-flex overflow-hidden" style="height: 1920px">
<div class="p-2 text-white floting-box" style="width: 350px; left: 20px; top: 80px;">
<%-- <h6 class="border-bottom pb-2">設計工具箱</h6>--%>
<v-tabs
v-model="tab"
background-color="primary"
icon-and-text
dark
vertical>
<%--<v-tabs-slider></v-tabs-slider>--%>
<v-tab href="#版型">
版型
<v-icon left>mdi-book-open-outline</v-icon>
</v-tab>
<v-tab href="#內容尺寸">
內容尺寸
<v-icon left>mdi-bowl-mix-outline</v-icon>
</v-tab>
<v-tab href="#列印尺寸">
列印尺寸
<v-icon left>mdi-dome-light</v-icon>
</v-tab>
<v-tab href="#列印模式">
列印模式
<v-icon left>mdi-weather-cloudy</v-icon>
</v-tab>
<v-tab href="#底圖">
底圖
<v-icon left>mdi-account-circle-outline</v-icon>
</v-tab>
<v-tab href="#牌位顯示項目">
牌位顯示項目
<v-icon left>mdi-flower</v-icon>
</v-tab>
<v-tab href="#存檔">
存檔
<v-icon left>mdi-flower</v-icon>
</v-tab>
<v-tab-item value="版型">
<v-card flat height="504px" class="d-flex flex-column">
<v-card-text class="overflow-y-auto flex-grow-1">
<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
vertical
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" :title="item.name">{{ 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>
</v-card-text>
</v-card>
</v-tab-item>
<v-tab-item value="內容尺寸">
<v-card flat height="504px" class="d-flex flex-column">
<v-card-text class="overflow-y-auto flex-grow-1">
<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
vertical
v-resize="onTabsResize"
>
<v-tab key="000001" value="000001" class="d-flex flex-column py-2">
<button
type="button"
class="btn btn-info"
@click.prevent="newPaperSize"
>
新增尺寸
</button>
</v-tab>
<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)"
@contextmenu.prevent="openPaperMenu($event,item.id)"
>
<span class="limit-text mt-1" :title="item.name">{{ item.name }}</span>
</button>
</v-tab>
</v-tabs>
</v-col>
</v-row>
<v-menu
absolute
offset-y
:position-x="paperMenuX"
:position-y="paperMenuY"
v-model="showPaperMenu"
>
<v-list>
<v-list-item :key="showPaperMenu">
<v-list-item-title>
<button class="btn btn-primary" @click.prevent="removePaper()">
刪除
</button>
</v-list-item-title>
</v-list-item>
</v-list>
</v-menu>
</v-card-text>
</v-card>
</v-tab-item>
<v-tab-item value="列印尺寸">
<v-card flat height="504px" class="d-flex flex-column">
<v-card-text class="overflow-y-auto flex-grow-1">
<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
vertical
v-resize="onTabsResize"
>
<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" :title="item.name">{{ 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 height="504px" class="d-flex flex-column">
<v-card-text class="overflow-y-auto flex-grow-1">
<v-tabs
class="fixed-bottom-tabs w-100"
bg-color="surface"
color="primary"
align-tabs="start"
center-active
vertical
>
<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 height="504px" class="d-flex flex-column">
<v-card-text class="overflow-y-auto flex-grow-1">
<v-tabs
class="fixed-bottom-tabs w-100"
bg-color="surface"
color="primary"
align-tabs="start"
center-active
vertical
>
<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 height="504px" class="d-flex flex-column">
<v-card-text class="overflow-y-auto flex-grow-1">
<v-tabs
class="fixed-bottom-tabs w-100"
bg-color="surface"
color="primary"
align-tabs="start"
center-active
vertical
>
<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 height="504px" class="d-flex flex-column">
<v-card-text class="overflow-y-auto flex-grow-1">
<span
class="btn btn-sm btn-outline-info w-100 mb-2"
@click.prevent="saveStyle"
>存檔</span
>
</v-card-text>
</v-card>
</v-tab-item>
</v-tabs>
<!-- 原生縮放橫軸容器 -->
<div style="display: flex; align-items: center; padding: 8px; background-color: #f5f5f5; border-radius: 4px; margin-bottom: 12px; font-family: sans-serif;">
<span style="margin-right: 8px; font-size: 14px; cursor:pointer" @click.prevent="minusZoomScalee"></span>
<!-- 使用 input type="range",綁定 zoomScale -->
<!-- min="0.2" (20%), max="1.5" (150%), step="0.05" (每次滑動5%) -->
<input type="range"
v-model="zoomScale"
min="0.0"
max="1.0"
step="0.05"
style="flex-grow: 1; cursor: pointer;" id="scaleSlider">
<span style="margin-left: 8px; font-size: 14px; cursor:pointer" @click.prevent="plusZoomScalee"></span>
<!-- 顯示百分比數值 -->
<span id="scaleSize" style="color:black;margin-left: 12px; min-width: 45px; font-weight: bold; font-size: 14px; text-align: right;">
{{ scaleSize }}%
</span>
</div>
<v-card>
<v-card-title>
型版屬性
</v-card-title>
<v-card-text>
型版名稱: <v-text-field type="input" v-model="styleName"></v-text-field><br>
內容尺寸:{{getParseName(paperSize,"content")}}<br>
列印尺寸:{{ getParseName(printSize,"print") }}<br>
列印模式:{{ getParseName(printMode,"mode") }}<br>
底圖:{{ backendInp }}
</v-card-text>
</v-card>
</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" @change.prevent="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" @change.prevent="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>
<v-dialog
v-model="paperDialog"
width="600" height="800" :z-index="3000"
>
<v-card>
<v-card-text>
<v-text-field v-model="newPaper.paperName" label="尺寸名稱"></v-text-field>
<v-text-field v-model="newPaper.paperWidth" label="寬度(mm)"></v-text-field>
<v-text-field v-model="newPaper.paperHeight" label="高度(mm)"></v-text-field>
</v-card-text>
<v-card-actions>
<v-btn
class="ms-auto"
@click="savePaperSize"
>確定</v-btn>
<v-btn
class="ms-auto"
@click="paperDialog = false"
>取消</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<v-dialog
v-model="removeDialog"
width="auto"
:z-index="3000"
>
<v-card>
<v-card-text>
確定刪除?
</v-card-text>
<v-card-actions>
<v-btn
class="ms-auto"
v-if="removeTarget=='paper'"
@click="confirmRemovePaper"
>確定</v-btn>
<v-btn
class="ms-auto"
v-if="removeTarget=='style'"
@click="confirmRemoveStyle"
>確定</v-btn>
<v-btn
class="ms-auto"
@click="removeDialog = false"
>取消</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</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 {
zoomScale: 0.5,
scaleSize: 50,
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,
showPaperMenu: false,
styleMenuX: 0,
styleMenuY: 0,
styleMenuStyle: "",
removeDialog: false,
removeTarget: "",
removeStyleID: "",
removePaperID: "",
paperMenuX: 0,
paperMenuY: 0,
paperDialog:false,
}
},
computed: {
},
mounted() {
this.init()
},
watch: {
zoomScale: {
handler() {
this.scaleSize =Math.floor( Number(this.zoomScale)*100)
$(".tablet-paper").css({ transform: `scale(${this.zoomScale})` });
},
deep: true,
}
},
methods: {
minusZoomScalee() {
this.zoomScale = Number(this.zoomScale) -0.05
//this.scaleSize = this.scaleSize - 5
//$(".tablet-paper").css({ transform:`scale(${this.zoomScale})` });
},
plusZoomScalee() {
this.zoomScale = Number(this.zoomScale) + 0.05
//this.scaleSize = this.scaleSize + 5
//$(".tablet-paper").css({ transform: `scale(${this.zoomScale})` });
},
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
this.removeStyleID = id
//console.log("QQOO_openStyleMenu:",id);
},
closeStyleMenu() {
this.showStyleMenu = false;
},
openPaperMenu(e, id) {
this.paperMenuX = e.clientX
this.paperMenuY = e.clientY
this.showPaperMenu = true
this.removePaperID=id
},
closePaperMenu() {
this.showPaperMenu = 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",
transform: `scale(${this.zoomScale})`, /* 縮小為 50% (改為 0.8 就是 80%,依此類推) */
"transform-origin": "top left", /* 確保縮放的基準點在左上角,防止縮放後位移 */
});
},
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
self.tabletElements.length=0
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;
if (data.result == "Y") {
data.data.forEach(x => {
self.tabletElements.push(x);
});
}
}
});
},
async getStyles() {
let self = this
self.allStyle.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) {//切換版型,抓回明細
if (this.allSize.length === 0) {
this.snackbar.text = "請先至少設定一種尺寸"
this.snackbar.show = true
return
}
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)
if (size == null || size == undefined) {
size = this.allSize[0]
}
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.styleName=s.name
//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);
//let scaleSize = 1
//let documentHeight = document.documentElement.scrollHeight;
//if ((Number(this.paper.height) / 0.265) >= (Number(documentHeight) * 0.8)) {
// scaleSize = (Number(documentHeight) * 0.8)/(Number(this.paper.height) / 0.265)
//}
console.log(scaleSize)
$(".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",
transform: `scale(${this.zoomScale})`, /* 縮小為 50% (改為 0.8 就是 80%,依此類推) */
"transform-origin": "top left", /* 確保縮放的基準點在左上角,防止縮放後位移 */
})
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)
//let scaleSize = 1
//let documentHeight = document.documentElement.scrollHeight;
//if ((Number(size.height) / 0.265) >= (Number(documentHeight) * 0.8)) {
// scaleSize = (Number(documentHeight) * 0.8) / (Number(size.height) / 0.265)
//}
$(".tablet-paper").css({
"background-color": "white",
width: (size ? size.width : self.paper.width) + "mm",
height: (size ? size.height : self.paper.height) + "mm",
transform: `scale(${this.zoomScale})`, /* 縮小為 50% (改為 0.8 就是 80%,依此類推) */
"transform-origin": "top left", /* 確保縮放的基準點在左上角,防止縮放後位移 */
});
}
},
async confirmRemovePaper() {
let self = this;
let paperSize = this.removePaperID //$("#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 }
self.removeDialog=false
await axios
.post(HTTP_HOST + 'api/tablet/DelPaperSize', ps)
.then(response => {
if (response.status == "200") {
this.snackbar.text = `${response.data.message}`;
this.snackbar.show = true
if (response.data.result == "Y") {
self.getPaperSize()
//$("#paperSize").val("")
this.paperSize = ""
this.changePaper()
}
} else {
this.snackbar.text = "存檔失敗";
this.snackbar.show=true
}
this.removeDialog = false;
});
},
async removePaper() {
this.removeDialog = true;
this.removeTarget = "paper";
//console.log("QQOO", this.removeStyle)
},
async confirmRemoveStyle() {
let self = this;
let sID = this.removeStyleID;
//console.log("styleID:", sID);
self.removeDialog = false;
if (sID == "000001" || sID == null || sID == undefined) {
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") {
$("#toast_body").html(`${response.data.message}`)
self.toast.show();
if (response.data.result == "Y") {
self.getStyles()
}
}
});
},
async removeStyle() {
this.removeDialog = true;
this.removeTarget = "style";
//console.log("QQOO",this.removeStyle)
},
async newPaperSize() {
this.paperDialog = true
},
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.allSize.length>0?self.allSize[0].id:""
self.changePaper()
self.paperDialog = false
self.newPaper.paperName = ""
self.newPaper.paperWidth = 0
self.newPaper.paperHeight =0
}
}
});
},
changeRosterLimit() {
//this.rosterLimit = parseInt(this.rosterLimit) //parseInt($("#rosterLimit").val());
},
changePrintMode(printMode) {
this.printMode = printMode
},
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);
//let scaleSize = 1
//let documentHeight = document.documentElement.scrollHeight;
//if ((Number(size.height) / 0.265) >= (Number(documentHeight) * 0.8)) {
// scaleSize = (Number(documentHeight) * 0.8) / (Number(size.height) / 0.265)
//}
//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",
transform: `scale(${this.zoomScale})`, /* 縮小為 50% (改為 0.8 就是 80%,依此類推) */
"transform-origin": "top left", /* 確保縮放的基準點在左上角,防止縮放後位移 */
});
//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 = '';
let fontSize = el.style.fontSize;
// 1. 智慧名單渲染
if (el.type === 'roster') {
let perRow = Math.floor(Number(el.height) / Number(el.textHeight));
let rows = Math.ceil(Number(slice.length) / Number(perRow));//總行數
let rowWidth = Math.floor(Number(el.width) / rows);
let fontSize = Math.floor(rowWidth / 2);
if (fontSize > el.style.fontSize) {
fontSize = el.style.fontSize;
rowWidth = fontSize * 2;
}
html = this.renderRoster(slice, el, rowWidth);
}
// 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') {
//要先去算出需要縮到多小字,根據總寬度跟行數,這樣才會在正中間
//字體大小*2如果大於行數就要縮字體
//let perRow = Math.floor(Number(el.height) / Number(el.textHeight));
//let rows = Math.ceil(Number(slice.length) / Number(perRow));//總行數
//let rowWidth = Math.floor(Number(el.width) / rows);
//fontSize = Math.floor(rowWidth / 2);
////console.log(el.height, el.textHeight, perRow, rows, rowWidth, fontSize)
//if (fontSize > el.style.fontSize) {
// fontSize = el.style.fontSize;
// rowWidth = fontSize * 2;
//}
fontSize = el.style.fontSize;
// 1. 依據 textHeight 與 textSpan 計算一行(垂直列)最多能放幾個名字
let th = Number(el.textHeight) || 0;
let span = Number(el.textSpan) || 0;
let h = Number(el.height);
let w = Number(el.width);
let namesPerCol = Math.floor((h + span) / (th + span));
if (namesPerCol < 1) namesPerCol = 1;
// 2. 計算總共會產生幾列 (cols),並推算出每個名字能分配到的最大欄寬
let cols = Math.ceil(slice.length / namesPerCol);
if (cols < 1) cols = 1;
let colWidth = Math.floor(w / cols);
html = this.renderNameList(slice, el, colWidth);
}
else if (el.id === 'alive') {
//console.log("createEl:", slice, el)
let perRow = Math.floor(Number(el.height) / Number(el.textHeight));
let rows = Math.ceil(Number(slice.length) / Number(perRow));//總行數
let rowWidth = Math.floor(Number(el.width) / rows);
fontSize = Math.floor(rowWidth / 2);
if (fontSize > el.style.fontSize) {
fontSize = el.style.fontSize;
rowWidth = fontSize * 2;
}
html = this.renderLiveList(slice, el, rowWidth);
}
else {
html = el.text;
}
//el.style.fontSize
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: 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);
},
calcIndividualFontSize(name, el, maxW) {
let s = Number(el.style.fontSize);
let maxH = Number(el.textHeight); // 限制在單一名字預設的區塊高度內
let l = Number(name.length);
while (s >= 12) {
let s_px = s * 1.333; // 將 pt 轉換為 px
// 如果單行高度夠放,直接回傳當下大小 (不會過度縮小)
if (l * s_px <= maxH) {
return s;
}
// 單行放不下,模擬多行折疊後的總寬度
let charsPerCol = Math.floor(maxH / s_px);
if (charsPerCol < 1) charsPerCol = 1;
let cols = Math.ceil(l / charsPerCol);
let neededWidth = cols * (s_px * 1.1); // 1.1 為緊密行高
// 如果折疊後的總寬度不超過分配到的橫向欄寬,就是最佳字體
if (neededWidth <= maxW) {
return s;
}
s -= 1; // 否則繼續縮小 1pt
}
return 12; // 極限最小字體
},
countFontSize(name, el, baseFontSize) {
let s = Number(baseFontSize);
let h = Number(el.height);
let w = Number(el.width);
let l = Number(name.length);
// 逐步縮小字體 (最低限制到 4pt)
while (s >= 4) {
// ★ 關鍵修正:將 pt 轉換為實際的像素(px)高度 (1pt ≈ 1.333px)
let s_px = s * 1.333;
// 如果單行的高度夠放
if (l * s_px <= h) {
// 如果原本的字體就放得下回傳 0,否則回傳縮小後的字體大小
return s === Number(baseFontSize) ? 0 : s;
}
// 單行放不下,必須折行
let charsPerCol = Math.floor(h / s_px); // 一行(垂直)最多能塞幾個字
if (charsPerCol < 1) charsPerCol = 1;
let cols = Math.ceil(l / charsPerCol); // 計算總共需要折成幾行
let neededWidth = cols * (s_px * 1.2); // 估算折行後需要的總像素寬度 (1.2為行距緩衝)
// 若折行後的總寬度小於等於容器寬度,代表此字體大小剛好能安全放下
if (neededWidth <= w) {
return s;
}
s -= 1; // 放不下則縮小 1pt 繼續嘗試
}
return 8; // 極限最小字體
},
renderNameList(names, el,rowWidth) {
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, rowWidth))
})
return $namelist;
},
renderNameSpan(name, el, colWidth) {
// 為「這個名字」獨立計算出專屬的最完美字體大小
let indSize = this.calcIndividualFontSize(name, el, colWidth);
// 2. 判斷是否發生了折行 (名字長度 * 單字像素高度 > 分配到的最大高度)
let s_px = indSize * 1.333;
let isMultiLine = (name.length * s_px) > Number(el.textHeight);
// 3. 根據是否折行動態決定對齊方式
// 單行:分散對齊 (justify) / 多行:置中對齊 (center) 或是改為 靠上對齊 (start)
let alignStyle = isMultiLine ? "start" : "justify";
return $(`<span style="font-size: ${indSize}pt;">${name}</span>`).css({
display: "block",
"min-height": `${el.textHeight}px`,
"max-height": `${el.height}px`,
width: "auto",
"line-height": "1.1",
"text-align": alignStyle, // ★ 動態套用對齊方式
"text-align-last": alignStyle, // ★ 動態套用最後一行對齊方式
"margin-left": "5px",
"text-justify": "inter-character",
"word-break": "break-all"
});
//return $(`<span>${name}</span>`).css({
// display: "block",
// "min-height": `${el.textHeight}px`,
// "max-height": `${el.height}px`,
// width: `${rowWidth}px`,
// "text-align": "justify",
// "text-align-last": "justify",
// "margin-left": "5px",
// "text-justify": "inter-character",
//});// "margin-bottom": `${el.textSpan}px`,
},
renderLiveList(names, el, rowWidth) {
//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, rowWidth))
})
return $namelist;
},
renderLiveSpan(name, el, rowWidth) {
//console.log("renderLiveSpan:", name, el );
return $(`<span>${name}</span>`).css({
display: "block",
"min-height": `${el.textHeight}px`,
"max-height": `${el.height}px`,
width: `${rowWidth}px`,
"text-align": "justify",
"text-align-last": "justify",
"margin-left": "5px",
"text-justify": "inter-character",
});//"margin-bottom": `${el.textSpan}px`,
},
// 品字佈局邏輯:一上二下
renderRoster(names, el,rowWidth) {
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());
let perRow = Math.floor(Number(el.height) / Number(el.textHeight));
let rows = Math.ceil(Number(names.length) / Number(perRow));//總行數
let rowWidth = Math.floor(Number(el.width) / rows);
let fontSize = Math.floor(rowWidth / 2);
if (fontSize > el.style.fontSize) {
fontSize = el.style.fontSize;
rowWidth = fontSize * 2;
}
$(`.tablet-element[id="${this.activeId}"]`).css({
position: "absolute", left: el.startX + "mm", top: el.startY + "mm", fontSize: 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, rowWidth));
} 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);
//let perRow = Math.floor(Number(el.height) / Number(el.textHeight));
//let rows = Math.ceil(Number(slice.length) / Number(perRow));//總行數
//let rowWidth = Math.floor(Number(el.width) / rows);
//let fontSize = Math.floor(rowWidth / 2);
//if (fontSize > el.style.fontSize) {
// fontSize = el.style.fontSize;
// rowWidth = fontSize * 2;
//}
// 1. 依據 textHeight 與 textSpan 計算一行(垂直列)最多能放幾個名字
let th = Number(el.textHeight) || 0;
let span = Number(el.textSpan) || 0;
let h = Number(el.height);
let w = Number(el.width);
let namesPerCol = Math.floor((h + span) / (th + span));
if (namesPerCol < 1) namesPerCol = 1;
// 2. 計算總共會產生幾列 (cols),並推算出每個名字能分配到的最大欄寬
let cols = Math.ceil(slice.length / namesPerCol);
if (cols < 1) cols = 1;
let colWidth = Math.floor(w / cols);
//console.log(el.height, el.textHeight, perRow, rows, rowWidth, fontSize)
html = this.renderNameList(slice, el, colWidth);//el.style.fontSize
$(`.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);
let perRow = Math.floor(Number(el.height) / Number(el.textHeight));
let rows = Math.ceil(Number(slice.length) / Number(perRow));//總行數
let rowWidth = Math.floor(Number(el.width) / rows);
let fontSize = Math.floor(rowWidth / 2);
if (fontSize > el.style.fontSize) {
fontSize = el.style.fontSize;
rowWidth = fontSize * 2;
}
html = this.renderLiveList(slice, el, rowWidth);
$(`.tablet-element[id="${this.activeId}"]`).css({
position: "absolute", left: el.x + "mm", top: el.y + "mm", fontSize: 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 = ""
}
if (this.styleName === "新增版型") {
this.snackbar.text = "請輸入新的版型名稱"
this.snackbar.show = true
return
}
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();
this.getStyles();
}
}
});
}
}
})
//$(() => Designer.init());
</script>
</asp:Content>