You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

726 lines
19 KiB
Vue

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<template>
<view class="push-message-page">
<TopDivBar @back="backToIndex" />
<view class="page-body">
<view class="tabs-bar">
<tn-tabs
v-model="currentTabIndex"
:bottom-shadow="false"
:bar="false"
height="40px"
>
<TnTabsItem
v-for="(item, index) in tabsData"
:key="index"
:title="item.text"
@click="currentTabIndex = index"
/>
</tn-tabs>
</view>
<!-- 语音 -->
<view v-show="currentTabIndex === 0" class="tab-panel">
<view class="field">
<text class="field-label">要呼叫的票号</text>
<input
v-model="voiceForm.ticketNo"
class="field-input"
placeholder="请输入要呼叫的票号"
/>
</view>
<view class="field">
<text class="field-label">窗口</text>
<input
v-model="voiceForm.windowNo"
class="field-input"
placeholder="请输入窗口号或窗口名称"
/>
</view>
<view class="actions">
<tn-button
type="primary"
:loading="voiceSending"
@click="onVoiceSend"
>
发送呼叫
</tn-button>
</view>
</view>
<!-- 信息屏 -->
<view v-show="currentTabIndex === 1" class="tab-panel">
<view class="field">
<text class="field-label">推送目标</text>
<TnCheckboxGroup v-model="screenForm.targets" class="checkbox-row">
<TnCheckbox label="sync" size="lg" :border="true"
>同步屏</TnCheckbox
>
<TnCheckbox label="composite" size="lg" :border="true"
>综合屏</TnCheckbox
>
</TnCheckboxGroup>
</view>
<view class="field">
<text class="field-label">推送内容</text>
<textarea
v-model="screenForm.content"
class="field-textarea"
placeholder="请输入要推送到信息屏的内容"
:maxlength="500"
/>
</view>
<view class="actions">
<tn-button
type="primary"
:loading="screenSending"
@click="onScreenPush"
>
推送至信息屏
</tn-button>
</view>
</view>
<!-- 短信 -->
<view v-show="currentTabIndex === 2" class="tab-panel">
<view class="field">
<text class="field-label">目标手机号</text>
<input
v-model="smsForm.phone"
class="field-input"
type="number"
maxlength="11"
placeholder="请输入目标手机号码"
/>
</view>
<view class="field">
<text class="field-label">短信内容</text>
<textarea
v-model="smsForm.content"
class="field-textarea"
placeholder="请输入短信内容"
:maxlength="500"
/>
</view>
<view class="actions">
<tn-button type="primary" :loading="smsSending" @click="onSmsSend">
发送短信
</tn-button>
</view>
</view>
<!-- 小票模板 -->
<view
v-show="currentTabIndex === 3"
class="tab-panel ticket-template-panel"
>
<view v-if="templateLoading" class="template-loading"
>模板加载中...</view
>
<view
v-else-if="!printTemplateForm.sections.length"
class="template-loading"
>
暂无模板数据,请点击重新加载
</view>
<view v-else class="ticket-template-layout">
<view class="ticket-editor">
<view class="section-list">
<view
v-for="(section, index) in printTemplateForm.sections"
:key="index"
class="section-card"
>
<view class="section-head">
<text class="section-type">{{
getSectionTypeLabel(section)
}}</text>
</view>
<template v-if="section.type === 'text'">
<view
v-if="isPlaceholderTextSection(section)"
class="section-field"
>
<text class="section-label">占位文本</text>
<text class="section-readonly">{{ section.content }}</text>
</view>
<view v-else class="section-field">
<text class="section-label">文本内容</text>
<input
v-model="section.content"
class="field-input section-input"
placeholder="请输入文本内容"
/>
</view>
<view class="section-field">
<text class="section-label">字号</text>
<view class="option-row">
<view
v-for="size in fontSizeOptions"
:key="size.value"
class="option-chip"
:class="{ active: section.fontSize === size.value }"
@click="section.fontSize = size.value"
>
{{ size.label }}
</view>
</view>
</view>
<view class="section-field">
<text class="section-label">对齐</text>
<view class="option-row">
<view
v-for="align in alignOptions"
:key="align.value"
class="option-chip"
:class="{ active: section.align === align.value }"
@click="section.align = align.value"
>
{{ align.label }}
</view>
</view>
</view>
<view
v-if="section.bold !== undefined"
class="section-field checkbox-field"
>
<TnCheckbox v-model="section.bold" size="lg" :border="true"
>加粗</TnCheckbox
>
</view>
</template>
<template v-else-if="section.type === 'field'">
<view class="section-field">
<text class="section-label">字段标识</text>
<text class="section-readonly">{{ section.key }}</text>
</view>
<view class="section-field">
<text class="section-label">显示标签</text>
<input
v-model="section.label"
class="field-input section-input"
placeholder="如:业务"
/>
</view>
<view class="section-field">
<text class="section-label">后缀</text>
<input
v-model="section.suffix"
class="field-input section-input"
placeholder="如: 人"
/>
</view>
<view class="section-field checkbox-field">
<TnCheckbox v-model="section.show" size="lg" :border="true"
>显示该字段</TnCheckbox
>
</view>
</template>
<view
v-else-if="section.type === 'line'"
class="section-line-tip"
>
分隔线(不可编辑)
</view>
</view>
</view>
</view>
<view class="ticket-preview-wrap">
<text class="preview-label">预览效果</text>
<view class="ticket-preview-ticket">
<rich-text :nodes="printPreviewHtml" />
</view>
<text class="preview-tip"
>预览数据为示例,实际打印时按取号结果填充</text
>
</view>
</view>
<view class="actions template-actions">
<tn-button :loading="templateLoading" @click="loadPrintTemplate"
>重新加载</tn-button
>
<tn-button
type="primary"
:loading="templateSaving"
@click="onSavePrintTemplate"
>
保存模板
</tn-button>
</view>
</view>
</view>
</view>
</template>
<script setup>
import { getPrintTemplate, savePrintTemplate } from "@/api/print.js";
import TopDivBar from "@/components/TopDivBar.vue";
import TnCheckboxGroup from "@/uni_modules/tuniaoui-vue3/components/checkbox/src/checkbox-group.vue";
import TnCheckbox from "@/uni_modules/tuniaoui-vue3/components/checkbox/src/checkbox.vue";
import TnTabsItem from "@/uni_modules/tuniaoui-vue3/components/tabs/src/tabs-item.vue";
import TnTabs from "@/uni_modules/tuniaoui-vue3/components/tabs/src/tabs.vue";
import {
buildPrintPreviewData,
buildPrintTemplateSaveBody,
getDefaultPrintTemplate,
getMinimalPrintTemplateForTest,
isPlaceholderTextSection,
parsePrintTemplate,
PRINT_SAMPLE_DATA,
renderPrintTemplateHtml,
} from "@/utils/printTemplate.js";
import { computed, reactive, ref, watch } from "vue";
const DEFAULT_SMS_TEMPLATE =
"尊敬的纳税人:【办税服务厅】提醒您尽快办理相关业务。如有疑问请致电大厅咨询。感谢您的配合!";
const PRINT_TEMPLATE_TAB_INDEX = 3;
// 测试开关true 时保存极小 sections验证是否因数据过大导致签名失败
const USE_MINIMAL_SECTIONS_FOR_SAVE_TEST = true;
const currentTabIndex = ref(0);
const tabsData = [
{ text: "语音" },
{ text: "信息屏" },
{ text: "短信" },
// { text: "小票模板" },
];
const fontSizeOptions = [
{ label: "小", value: 1 },
{ label: "中", value: 2 },
{ label: "大", value: 3 },
];
const alignOptions = [
{ label: "左", value: "left" },
{ label: "中", value: "center" },
{ label: "右", value: "right" },
];
const printTemplateForm = reactive({
cutPaper: true,
sections: [],
});
const templateLoading = ref(false);
const templateSaving = ref(false);
const printPreviewHtml = computed(() => {
const previewData = buildPrintPreviewData(
printTemplateForm,
PRINT_SAMPLE_DATA,
);
return renderPrintTemplateHtml(printTemplateForm, previewData);
});
const getSectionTypeLabel = (section) => {
if (section.type === "text") return "文本";
if (section.type === "field") return "字段";
if (section.type === "line") return "分隔线";
return section.type || "未知";
};
const applyPrintTemplate = (data) => {
const template = parsePrintTemplate(data);
if (!template.sections.length) {
return false;
}
printTemplateForm.cutPaper = template.cutPaper;
printTemplateForm.sections = JSON.parse(JSON.stringify(template.sections));
return true;
};
const loadPrintTemplate = async () => {
templateLoading.value = true;
try {
const data = await getPrintTemplate();
console.log("[print-template] getPrintTemplate 原始返回:", data);
console.log("[print-template] 返回详情:", JSON.stringify(data, null, 2));
const applied = applyPrintTemplate(data);
console.log(
"[print-template] 解析结果 applied:",
applied,
"sections:",
printTemplateForm.sections.length,
);
if (!applied) {
applyPrintTemplate(getDefaultPrintTemplate());
uni.showToast({
title: "模板解析失败,已使用默认模板",
icon: "none",
});
}
} catch (error) {
console.log("[print-template] 获取打印模板失败:", error);
applyPrintTemplate(getDefaultPrintTemplate());
uni.showToast({
title: "加载失败,已使用默认模板",
icon: "none",
});
} finally {
templateLoading.value = false;
}
};
const onSavePrintTemplate = async () => {
if (!printTemplateForm.sections.length) {
uni.showToast({ title: "模板区块不能为空", icon: "none" });
return;
}
templateSaving.value = true;
try {
const saveBody = USE_MINIMAL_SECTIONS_FOR_SAVE_TEST
? getMinimalPrintTemplateForTest()
: buildPrintTemplateSaveBody(printTemplateForm);
console.log(
"[print-template] 保存请求体(测试模式:",
USE_MINIMAL_SECTIONS_FOR_SAVE_TEST,
"):",
saveBody,
);
await savePrintTemplate(saveBody);
uni.showToast({
title: USE_MINIMAL_SECTIONS_FOR_SAVE_TEST ? "测试保存成功" : "保存成功",
icon: "success",
});
} catch (error) {
console.log("保存打印模板失败:", error);
} finally {
templateSaving.value = false;
}
};
watch(currentTabIndex, (index) => {
if (Number(index) === PRINT_TEMPLATE_TAB_INDEX) {
loadPrintTemplate();
}
});
const voiceForm = reactive({
ticketNo: "",
windowNo: "",
});
const voiceSending = ref(false);
const screenForm = reactive({
targets: [],
content: "",
});
const screenSending = ref(false);
const smsForm = reactive({
phone: "",
content: DEFAULT_SMS_TEMPLATE,
});
const smsSending = ref(false);
const backToIndex = () => {
uni.navigateBack({
fail: () => {
uni.navigateTo({ url: "/pages/index/index" });
},
});
};
const onVoiceSend = () => {
const ticket = voiceForm.ticketNo.trim();
const win = voiceForm.windowNo.trim();
if (!ticket) {
uni.showToast({ title: "请输入要呼叫的票号", icon: "none" });
return;
}
if (!win) {
uni.showToast({ title: "请输入窗口", icon: "none" });
return;
}
uni.showModal({
title: "确认发送",
content: `将呼叫票号「${ticket}」至窗口「${win}」,是否继续?`,
success: (res) => {
if (!res.confirm) return;
voiceSending.value = true;
try {
console.log("[voice-call]", { ticketNo: ticket, windowNo: win });
uni.showToast({ title: "呼叫指令已提交", icon: "success" });
} finally {
voiceSending.value = false;
}
},
});
};
const onScreenPush = () => {
if (!screenForm.targets.length) {
uni.showToast({ title: "请至少选择一种信息屏", icon: "none" });
return;
}
if (!screenForm.content.trim()) {
uni.showToast({ title: "请输入推送内容", icon: "none" });
return;
}
uni.showModal({
title: "确认推送",
content: "确定将内容推送到已选信息屏?",
success: (res) => {
if (!res.confirm) return;
screenSending.value = true;
try {
console.log("[screen-push]", {
targets: screenForm.targets,
content: screenForm.content,
});
uni.showToast({ title: "推送已提交", icon: "success" });
} finally {
screenSending.value = false;
}
},
});
};
const onSmsSend = () => {
const phone = smsForm.phone.trim();
if (!phone) {
uni.showToast({ title: "请输入手机号码", icon: "none" });
return;
}
if (!/^1\d{10}$/.test(phone)) {
uni.showToast({ title: "请输入11位有效手机号", icon: "none" });
return;
}
if (!smsForm.content.trim()) {
uni.showToast({ title: "请输入短信内容", icon: "none" });
return;
}
uni.showModal({
title: "确认发送",
content: `将向 ${phone} 发送短信,是否继续?`,
success: (res) => {
if (!res.confirm) return;
smsSending.value = true;
try {
console.log("[sms-send]", { phone, content: smsForm.content });
uni.showToast({ title: "短信已提交", icon: "success" });
} finally {
smsSending.value = false;
}
},
});
};
</script>
<style lang="scss" scoped>
.push-message-page {
min-height: 100vh;
background-color: #f5f7fa;
}
.page-body {
padding: calc(6vh + 64px) 24px 24px;
max-width: 720px;
margin: 0 auto;
}
.ticket-template-panel {
max-width: none;
}
.ticket-template-layout {
display: flex;
gap: 16px;
align-items: flex-start;
flex-wrap: wrap;
}
.ticket-editor {
flex: 1;
min-width: 320px;
}
.ticket-preview-wrap {
width: 300px;
flex-shrink: 0;
}
.preview-label {
display: block;
font-size: 14px;
color: #374151;
font-weight: 500;
margin-bottom: 8px;
}
.ticket-preview-ticket {
background: #fff;
border: 1px dashed #d1d5db;
border-radius: 8px;
overflow: hidden;
min-height: 280px;
}
.preview-tip {
display: block;
margin-top: 8px;
font-size: 12px;
color: #9ca3af;
line-height: 1.5;
}
.template-loading {
padding: 40px 0;
text-align: center;
color: #6b7280;
font-size: 14px;
}
.section-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.section-card {
border: 1px solid #e5e7eb;
border-radius: 8px;
padding: 12px;
background: #fafafa;
}
.section-head {
margin-bottom: 10px;
}
.section-type {
font-size: 13px;
font-weight: 600;
color: #2563eb;
}
.section-field {
margin-bottom: 10px;
}
.section-label {
display: block;
font-size: 13px;
color: #6b7280;
margin-bottom: 6px;
}
.section-input {
height: 40px;
}
.section-readonly {
display: block;
font-size: 14px;
color: #111827;
padding: 8px 0;
}
.section-line-tip {
font-size: 13px;
color: #9ca3af;
}
.option-row {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.option-chip {
min-width: 48px;
height: 32px;
padding: 0 12px;
border: 1px solid #d1d5db;
border-radius: 6px;
display: flex;
align-items: center;
justify-content: center;
font-size: 13px;
color: #374151;
background: #fff;
}
.option-chip.active {
border-color: #2563eb;
color: #2563eb;
background: #eff6ff;
}
.checkbox-field {
margin-bottom: 0;
}
.template-actions {
display: flex;
gap: 12px;
flex-wrap: wrap;
}
.tabs-bar {
background: #fff;
border-radius: 12px;
padding: 8px 12px;
margin-bottom: 16px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06);
}
.tab-panel {
background: #fff;
border-radius: 12px;
padding: 20px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06);
}
.field {
margin-bottom: 20px;
}
.field-label {
display: block;
font-size: 14px;
color: #374151;
margin-bottom: 8px;
font-weight: 500;
}
.field-input {
width: 100%;
height: 44px;
border: 1px solid #e5e7eb;
border-radius: 8px;
padding: 0 12px;
font-size: 15px;
box-sizing: border-box;
background: #fafafa;
}
.field-textarea {
width: 100%;
min-height: 140px;
border: 1px solid #e5e7eb;
border-radius: 8px;
padding: 12px;
font-size: 15px;
box-sizing: border-box;
background: #fafafa;
}
.checkbox-row {
display: flex;
flex-wrap: wrap;
gap: 16px;
align-items: center;
}
.actions {
margin-top: 8px;
}
</style>