|
|
<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>
|