Files
17168ERP/web/admin/activity/shuwen02.vue
T

1019 lines
27 KiB
Vue
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.
<template>
<div class="print-preview-app">
<!-- Vue 2 動態注入列印樣式 -->
<component :is="'style'" id="print-style">{{ printPageStyle }}</component>
<!-- 左側面板容器 (加入收合狀態 class) -->
<div class="left-panel-container no-print" :class="{ 'is-collapsed': isPanelCollapsed }">
<!-- 控制面板主體 -->
<div id="control-panel">
<!-- panel-inner 用於固定內容寬度,避免收合時文字因為寬度變小而折行 -->
<div class="panel-inner">
<div class="control-group">
<label>疏文類型</label>
<select v-model="shuwenType" @change="changeShuwenType">
<option value="">請選擇</option>
<option value="01">消災</option>
<option value="02">超薦</option>
</select>
</div>
<div class="control-group">
<label>疏文樣板</label>
<select v-model="defaultTemplate" @change="changeTemplate">
<option value="">請選擇</option>
<option v-for="item in shuwenList" :key="item.num" :value="item.num">{{ item.name }}</option>
</select>
</div>
<div class="control-group">
<label>紙張大小</label>
<select v-model="paperSize" @change="updateLayout">
<option value="A4">A4</option>
<option value="A3">A3</option>
<option value="B5">B5</option>
</select>
</div>
<div class="control-group">
<label>紙張方向</label>
<select v-model="paperOrientation" @change="updateLayout">
<option value="portrait">直向</option>
<option value="landscape">橫向</option>
</select>
</div>
<div class="control-group">
<label>一次修改全部行距: <span>{{ globalMargin }}</span>px</label>
<div class="adjust-row">
<button @click.prevent="adjustAllMargins(-1)">-</button>
<input type="range"
min="0"
max="80"
v-model.number="globalMargin"
@input="applyGlobalMargin" />
<button @click.event="adjustAllMargins(1)">+</button>
</div>
</div>
<hr />
<p class="hint-text">
💡 提示:在右側文字上【按滑鼠右鍵】觸發設定,面板固定顯示在左下角
</p>
<hr />
<button class="action-btn save-btn" @click.prevent="saveData">
1. 儲存至資料庫
</button>
<button class="action-btn print-btn" @click.prevent="triggerPrint">
2. 產生 PDF / 列印
</button>
<button class="action-btn print-btn" @click.prevent="clearContent">
3. 清除內容
</button>
</div>
</div>
<!-- 收合/展開 按鈕 -->
<button class="collapse-btn" @click.prevent="isPanelCollapsed = !isPanelCollapsed" :title="isPanelCollapsed ? '展開面板' : '收合面板'">
<svg class="arrow-icon" viewBox="0 0 24 24" width="16" height="16" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round">
<polyline points="15 18 9 12 15 6"></polyline>
</svg>
</button>
</div>
<!-- 預覽區塊 -->
<div id="preview-container" @click="hideFloatingMenu">
<div id="paper-preview" :style="paperStyle">
<div
id="editor"
contenteditable="true"
ref="editorRef"
@contextmenu="rightClickLineTrack"
@keydown.enter.prevent="newLine"
>
<div style="font-size: 28px; font-weight: bold; align-items: center; margin-left: 25px;">中元普渡祈福文疏</div>
<div></div>
<div style="font-size: 16px; letter-spacing: 4px; margin-left: 12px;">今逢佳節齋供養,花果盡是素佳餚。</div>
<div style="font-size: 16px; margin-left: 12px;">誠心祈求風調雨順、國泰民安。</div>
<div></div>
<div style="font-size: 14px; align-items: flex-end; margin-left: 30px;">歲次天運民國一一五[YEAR]年七[MONTH]月十五[DAY]日。</div>
</div>
</div>
<!-- 浮動設定面板 -->
<div
id="line-floating-menu"
v-show="isMenuVisible"
:style="menuStyle"
@click.stop
class="no-print"
>
<h4 id="menu-header" @mousedown="startDrag">✥ 拖拉此處移動面板</h4>
<div class="control-group">
<label>字型大小: <span>{{ currentLineState.fontSize }}</span>px</label>
<div class="adjust-row">
<button @click.prevent="stepLineStyle('fontSize', -1)">-</button>
<input
type="range"
min="12"
max="60"
v-model.number="currentLineState.fontSize"
@input="applyLineStyle('fontSize', currentLineState.fontSize + 'px')"
/>
<button @click.prevent="stepLineStyle('fontSize', 1)">+</button>
</div>
</div>
<div class="control-group">
<label>字距: <span>{{ currentLineState.letterSpacing }}</span>px</label>
<div class="adjust-row">
<button @click.prevent="stepLineStyle('letterSpacing', -1)">-</button>
<input
type="range"
min="0"
max="25"
v-model.number="currentLineState.letterSpacing"
@input="applyLineStyle('letterSpacing', currentLineState.letterSpacing + 'px')"
/>
<button @click.prevent="stepLineStyle('letterSpacing', 1)">+</button>
</div>
</div>
<div class="control-group">
<label>行距(與左側): <span>{{ currentLineState.marginLeft }}</span>px</label>
<div class="adjust-row">
<button @click.prevent="stepLineStyle('marginLeft', -1)">-</button>
<input
type="range"
min="0"
max="80"
v-model.number="currentLineState.marginLeft"
@input="applyLineStyle('marginLeft', currentLineState.marginLeft + 'px')"
/>
<button @click.prevent="stepLineStyle('marginLeft', 1)">+</button>
</div>
</div>
<div class="control-group">
<label>垂直位置對齊</label>
<div class="menu-btn-group">
<button
:class="{ active: currentLineState.alignItems === 'flex-start' }"
@click.prevent="applyLineStyle('alignItems', 'flex-start')"
>上</button>
<button
:class="{ active: currentLineState.alignItems === 'center' }"
@click.prevent="applyLineStyle('alignItems', 'center')"
>中</button>
<button
:class="{ active: currentLineState.alignItems === 'flex-end' }"
@click.prevent="applyLineStyle('alignItems', 'flex-end')"
>下</button>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
module.exports = {
name: 'Shuwen02',
props: {
num: { type: String }
},
data() {
return {
isPanelCollapsed: false, // 控制面板收合狀態
paperSize: 'A4',
paperOrientation: 'portrait',
globalMargin: 10,
paperStyle: {
width: '210mm',
height: '297mm'
},
isMenuVisible: false,
menuStyle: {
left: '320px', // 微調初始位置,避免擋住按鈕
top: '',
bottom: '20px'
},
currentLineState: {
fontSize: 16,
letterSpacing: 2,
marginLeft: 10,
alignItems: 'flex-start'
},
shuwenList: [],
defaultTemplate: {},
shuwenType:""
};
},
computed: {
printPageStyle() {
return `@media print { @page { size: ${this.paperSize} ${this.paperOrientation}; margin: 0mm; } }`;
}
},
created() {
this.currentTargetLine = null;
this.isDragging = false;
this.startX = 0;
this.startY = 0;
},
methods: {
changeShuwenType() {
console.log("changeShuwenType:", this.shuwenType)
//去資料庫抓回他的消災或超薦的總疏文
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}`;
}
})
.catch(error => console.log(error))
},
newLine() {
document.execCommand('insertParagraph',false);
},
updateLayout() {
const sizes = {
'A4': { w: 210, h: 297 },
'A3': { w: 297, h: 420 },
'B5': { w: 176, h: 250 }
};
const w = sizes[this.paperSize].w;
const h = sizes[this.paperSize].h;
const finalW = this.paperOrientation === 'portrait' ? w : h;
const finalH = this.paperOrientation === 'portrait' ? h : w;
this.paperStyle.width = `${finalW}mm`;
this.paperStyle.height = `${finalH}mm`;
this.hideFloatingMenu();
this.initData();
},
changeTemplate() {
// console.log("changeTemplate:", this.defaultTemplate)
if (this.defaultTemplate === "") {
this.clearContent();
return
}
let temp = this.shuwenList.find((x) => x.num === this.defaultTemplate);
if (temp) {
document.getElementById("editor").innerHTML = temp.contents;
}
},
rightClickLineTrack(e) {
let target = e.target;
const editor = this.$refs.editorRef;
while (target && target.parentNode !== editor && target !== editor) {
target = target.parentNode;
}
if (!target || target === editor || target.tagName !== 'DIV') return;
if (this.currentTargetLine) {
this.currentTargetLine.classList.remove('selected-line');
}
this.currentTargetLine = target;
this.currentTargetLine.classList.add('selected-line');
const computed = window.getComputedStyle(this.currentTargetLine);
this.currentLineState.fontSize = parseInt(computed.fontSize) || 16;
this.currentLineState.letterSpacing = computed.letterSpacing === 'normal' ? 0 : (parseInt(computed.letterSpacing) || 0);
this.currentLineState.marginLeft = parseInt(computed.marginLeft) || 10;
this.currentLineState.alignItems = computed.alignItems || 'flex-start';
this.isMenuVisible = true;
},
hideFloatingMenu(e) {
if (!e || e.target.id === 'preview-container' || e.target.id === 'paper-preview') {
this.isMenuVisible = false;
if (this.currentTargetLine) {
this.currentTargetLine.classList.remove('selected-line');
this.currentTargetLine = null;
}
}
},
applyLineStyle(property, value) {
if (this.currentTargetLine) {
this.currentTargetLine.style[property] = value;
if (property === 'alignItems') {
this.currentLineState.alignItems = value;
}
}
},
stepLineStyle(property, step) {
let currentValue = this.currentLineState[property];
let min = 0, max = 100;
if (property === 'fontSize') { min = 12; max = 60; }
if (property === 'letterSpacing') { min = 0; max = 25; }
if (property === 'marginLeft') { min = 0; max = 80; }
let newValue = currentValue + step;
if (newValue < min) newValue = min;
if (newValue > max) newValue = max;
this.currentLineState[property] = newValue;
this.applyLineStyle(property, `${newValue}px`);
},
applyGlobalMargin() {
const editor = this.$refs.editorRef;
if (editor) {
const lines = editor.querySelectorAll('div');
lines.forEach(line => {
line.style.marginLeft = `${this.globalMargin}px`;
});
}
},
adjustAllMargins(step) {
let newVal = this.globalMargin + step;
if (newVal < 0) newVal = 0;
if (newVal > 80) newVal = 80;
this.globalMargin = newVal;
this.applyGlobalMargin();
},
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!');
}
},
triggerPrint() {
//window.print();
this.print();
},
startDrag(e) {
this.isDragging = true;
const el = document.getElementById('line-floating-menu');
const rect = el.getBoundingClientRect();
this.startX = e.clientX - rect.left;
this.startY = e.clientY - rect.top;
},
handleDrag(e) {
if (this.isDragging) {
e.preventDefault();
this.menuStyle.left = `${e.clientX - this.startX}px`;
this.menuStyle.top = `${e.clientY - this.startY}px`;
this.menuStyle.bottom = 'auto';
}
},
stopDrag() {
this.isDragging = false;
},
bindEvents() {
document.addEventListener('mousemove', this.handleDrag);
document.addEventListener('mouseup', this.stopDrag);
},
unbindEvents() {
document.removeEventListener('mousemove', this.handleDrag);
document.removeEventListener('mouseup', this.stopDrag);
},
initData() {
axios.post(HTTP_HOST + 'api/shuwens/ListShuwenTemplate')
.then(response => {
console.log(response)
this.shuwenList = response.data.data
})
.catch(error => console.log(error))
},
clearContent() {
document.getElementById("editor").innerHTML = `
<div class="selected-line">
&nbsp; &nbsp;</div>
`;
},
print() {
let w = window.open('', '_blank');
if (!w) {
alert("請允許瀏覽器開啟彈出式視窗!");
return;
}
this.pageStyle = `<style id="pageStyle">
@media print {
@page {
size: ${this.paperSize} ${this.paperOrientation}; margin: 0mm; } };};
margin: 0mm;
}
}
body {
font-family: "Microsoft JhengHei", sans-serif;
margin: 0;
padding: 20px;
background: #f5f5f5;
display: flex;
gap: 20px;
}
#control-panel {
width: 240px;
background: #fff;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
display: flex;
flex-direction: column;
gap: 15px;
height: fit-content;
}
.control-group {
display: flex;
flex-direction: column;
gap: 5px;
font-size: 14px;
}
select,
input[type="range"] {
padding: 6px;
border: 1px solid #ccc;
border-radius: 4px;
}
button.action-btn {
padding: 12px;
background: #007bff;
color: #fff;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 16px;
font-weight: 700;
}
#preview-container {
display: flex;
flex-grow: 1;
background: #ddd;
padding: 20px;
overflow: auto;
position: relative;
}
#paper-preview {
background: #fff;
padding: 20mm;
box-sizing: border-box;
width: auto;
height: auto;
}
#editor {
width: 100%;
height: 100%;
outline: none;
writing-mode: vertical-rl;
text-orient: upright;
}
#editor > div {
min-height: 1em;
display: flex !important;
flex-direction: column;
align-items: flex-start;
margin-left: 10px;
padding: 0 4px;
}
#editor > div:hover {
background: rgba(0, 123, 255, 0.05);
}
#editor > div.selected-line {
background: rgba(0, 123, 255, 0.1) !important;
box-shadow: 0 0 0 1px #ff7675;
}
#line-floating-menu {
display: none;
position: fixed;
bottom: 20px;
left: 280px;
background: #fff0f1;
border: 2px solid #ff7675;
padding: 12px;
border-radius: 8px;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3);
z-index: 99999;
flex-direction: column;
gap: 8px;
width: 210px;
}
#line-floating-menu h4 {
margin: 0;
color: #d63031;
font-size: 14px;
cursor: move;
background: #ffe4e6;
padding: 4px;
border-radius: 4px;
text-align: center;
user-select: none;
}
.menu-btn-group {
display: flex;
gap: 4px;
}
.menu-btn-group button {
flex: 1;
padding: 6px;
border: 1px solid #ccc;
background: #fff;
cursor: pointer;
border-radius: 4px;
font-weight: 700;
}
.menu-btn-group button.active {
background: #28a745;
color: #fff;
border-color: #28a745;
}
.adjust-row {
display: flex;
align-items: center;
gap: 5px;
}
.adjust-row button {
padding: 4px 10px;
border: 1px solid #ccc;
background: #fff;
cursor: pointer;
border-radius: 4px;
font-weight: 700;
user-select: none;
}
.adjust-row button:hover {
background: #eee;
}
.adjust-row input[type="range"] {
flex-grow: 1;
padding: 0;
}
@media print {
#control-panel,
#line-floating-menu {
display: none !important;
}
body,
#preview-container {
background: #fff;
padding: 0;
margin: 0;
}
#paper-preview {
width: 100% !important;
height: 100% !important;
box-shadow: none;
/* 1. 移除原本的 flex 與置中設定,改為一般的區塊顯示 */
display: block !important;
/* 2. 透過 padding 設定您需要的上邊界與右邊界 (數值可自由調整) */
padding-top: 30mm !important; /* 控制上方留白 */
padding-right: 30mm !important; /* 控制右側留白 */
padding-bottom: 0 !important;
padding-left: 0 !important;
/* padding: 0;
display: flex !important;
justify-content: center !important;
align-items: center !important; */
}
#editor {
height: auto !important;
/* width: auto !important; 加入這行,讓寬度貼合內容 */
max-height: 100% !important;
}
}
</style>
`
let canvasHtml = document.getElementById("preview-container").outerHTML; //$("#preview-container").html();
console.log(canvasHtml);
let htmlContent = `
<!DOCTYPE html>
<html>
<head>
<title></title>
<meta charset="utf-8">
${this.pageStyle}
</head>
<body>
${canvasHtml}
</body>
</html>`;
w.document.write(htmlContent);
w.document.close();
w.onload = function () {
// 5. 將等待時間稍微拉長至 1.2 秒,確保大張的 SVG 底圖與外部字體(如標楷體)下載完畢
setTimeout(() => {
w.focus();
w.print();
}, 1200);
}
},
},
mounted() {
this.updateLayout();
this.bindEvents();
document.execCommand("defaultParagraphSeparator",false,'div');
},
beforeDestroy() {
this.unbindEvents();
},
activated() {
this.bindEvents();
},
deactivated() {
this.unbindEvents();
this.isDragging = false;
}
};
</script>
<style>
/* 基礎排版與重設 */
.print-preview-app {
font-family: "Microsoft JhengHei", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
margin: 0;
background: #f8f9fa;
display: flex;
gap: 24px;
height: 100vh;
overflow: hidden;
box-sizing: border-box;
padding: 24px;
}
/* 左側面板容器 (控制收合動畫) */
.left-panel-container {
position: relative;
width: 260px;
height: fit-content;
flex-shrink: 0;
transition: width 0.3s cubic-bezier(0.4, 0, 0.2, 1);
display: flex;
align-items: flex-start;
}
/* 展開/收合 按鈕 */
.collapse-btn {
position: absolute;
right: -14px; /* 讓按鈕跨在面板邊界上 */
top: 32px;
width: 28px;
height: 28px;
background: #fff;
border: 1px solid #e2e8f0;
border-radius: 50%;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
color: #4a5568;
z-index: 10;
transition: all 0.2s ease;
padding: 0;
}
.collapse-btn:hover {
background: #f1f5f9;
transform: scale(1.05);
}
.arrow-icon {
transition: transform 0.3s ease;
}
/* == 收合狀態處理 == */
.left-panel-container.is-collapsed {
width: 0px;
}
.left-panel-container.is-collapsed #control-panel {
padding-left: 0;
padding-right: 0;
border-width: 0;
opacity: 0;
visibility: hidden; /* 防止收合時裡面元素還能被點擊 */
}
/* 收合時箭頭反轉,並把按鈕往右推出避免被隱藏 */
.left-panel-container.is-collapsed .arrow-icon {
transform: rotate(180deg);
}
.left-panel-container.is-collapsed .collapse-btn {
right: -36px;
}
/* 控制面板外框 */
#control-panel {
width: 100%;
background: #ffffff;
padding: 24px;
border-radius: 12px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.05);
border: 1px solid #edf2f7;
overflow: hidden;
box-sizing: border-box;
transition: padding 0.3s, opacity 0.3s, border-width 0.3s;
}
/* 固定內部內容寬度,避免收合過程中文字擠壓換行 */
.panel-inner {
width: 210px; /* 260px - 24px*2(padding) */
display: flex;
flex-direction: column;
gap: 16px;
}
.control-group {
display: flex;
flex-direction: column;
gap: 8px;
font-size: 13px;
font-weight: 500;
color: #4a5568;
}
select, input[type="range"] {
padding: 8px 12px;
border: 1px solid #e2e8f0;
border-radius: 6px;
background-color: #f8fafc;
color: #1a202c;
outline: none;
transition: all 0.2s;
width: 100%;
box-sizing: border-box;
}
select:focus {
border-color: #cbd5e1;
background-color: #ffffff;
}
hr {
border: 0;
height: 1px;
background: #edf2f7;
width: 100%;
margin: 4px 0;
}
.hint-text {
font-size: 12px;
color: #718096;
background: #f7fafc;
padding: 10px;
border-radius: 6px;
margin: 0;
line-height: 1.5;
border: 1px solid #e2e8f0;
}
/* 按鈕樣式 */
button.action-btn {
padding: 12px;
color: #fff;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
font-weight: 600;
transition: opacity 0.2s, transform 0.1s;
width: 100%;
}
button.action-btn:active {
transform: translateY(1px);
}
.save-btn {
background: #2d3748;
}
.save-btn:hover {
background: #4a5568;
}
.print-btn {
background: #0f172a;
}
.print-btn:hover {
background: #334155;
}
/* 預覽容器與紙張 */
#preview-container {
display: flex;
flex-grow: 1;
background: #e2e8f0;
padding: 32px;
overflow: auto;
position: relative;
border-radius: 12px;
justify-content: center;
align-items: flex-start;
}
#paper-preview {
background: #fff;
padding: 20mm;
box-sizing: border-box;
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.08);
transition: width 0.3s, height 0.3s;
}
#editor {
width: 100%;
height: 100%;
outline: none;
writing-mode: vertical-rl;
text-orient: upright;
}
#editor > div {
min-height: 1em;
display: flex !important;
flex-direction: column;
align-items: flex-start;
margin-left: 10px;
padding: 0 4px;
border-radius: 4px;
transition: background 0.1s;
}
#editor > div:hover {
background: #f1f5f9;
}
#editor > div.selected-line {
background: #e2e8f0 !important;
box-shadow: 0 0 0 1px #94a3b8;
}
/* 浮動選單 */
#line-floating-menu {
position: fixed;
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(8px);
border: 1px solid #e2e8f0;
padding: 16px;
border-radius: 10px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
z-index: 99999;
display: flex;
flex-direction: column;
gap: 12px;
width: 220px;
}
#line-floating-menu h4 {
margin: 0;
color: #4a5568;
font-size: 12px;
cursor: move;
background: #f8fafc;
padding: 8px;
border-radius: 6px;
text-align: center;
user-select: none;
border: 1px solid #edf2f7;
}
.menu-btn-group {
display: flex;
gap: 4px;
}
.menu-btn-group button {
flex: 1;
padding: 6px;
border: 1px solid #e2e8f0;
background: #fff;
color: #4a5568;
cursor: pointer;
border-radius: 6px;
font-weight: 600;
font-size: 12px;
transition: all 0.2s;
}
.menu-btn-group button.active {
background: #2d3748;
color: #fff;
border-color: #2d3748;
}
.adjust-row {
display: flex;
align-items: center;
gap: 8px;
}
.adjust-row button {
padding: 4px 12px;
border: 1px solid #e2e8f0;
background: #fff;
color: #4a5568;
cursor: pointer;
border-radius: 6px;
font-weight: 600;
user-select: none;
}
.adjust-row button:hover {
background: #f1f5f9;
}
.adjust-row input[type="range"] {
flex-grow: 1;
padding: 0;
}
/* 列印專屬設定 */
@media print {
.print-preview-app {
padding: 0;
height: auto;
overflow: visible;
}
.left-panel-container,
#line-floating-menu {
display: none !important;
}
#preview-container, #preview-container * {
visibility: visible;
}
#preview-container {
background: #fff;
padding: 0;
margin: 0;
position: absolute;
left:0;
top:0;
}
#paper-preview {
width: 100% !important;
height: 100% !important;
box-shadow: none;
display: block !important;
padding-top: 30mm !important;
padding-right: 30mm !important;
padding-bottom: 0 !important;
padding-left: 0 !important;
border: none;
}
#editor {
height: auto !important;
max-height: 100% !important;
}
#editor > div.selected-line {
background: transparent !important;
box-shadow: none !important;
}
.no-print {
display:none !important;
}
}
</style>