1111
This commit is contained in:
@@ -0,0 +1,657 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, reactive, ref } from "vue";
|
||||
import { ElMessage } from "element-plus";
|
||||
import apiClient from "@/api/client";
|
||||
import type { SystemSettings } from "@/types";
|
||||
import { outputFormatLabelMap, recordingTemplateLabelMap, saveModeLabelMap } from "@/types";
|
||||
|
||||
const loading = ref(false);
|
||||
const saving = ref(false);
|
||||
const testingEmail = ref(false);
|
||||
|
||||
const form = reactive<SystemSettings>({
|
||||
ffmpegPath: "ffmpeg",
|
||||
outputRoot: "records",
|
||||
outputDirectoryTemplate: "{platform}/{yyyy}/{MM}/{dd}/{anchor}",
|
||||
outputFileNameTemplate: "{HHmmss}_{anchor}_{title}_{roomId}{segmentSuffix}",
|
||||
defaultQuality: "origin",
|
||||
defaultOutputFormat: 0,
|
||||
saveMode: 0,
|
||||
recordingTemplate: 0,
|
||||
segmentDurationMinutes: 30,
|
||||
enableAutoReconnect: true,
|
||||
reconnectDelayMaxSeconds: 5,
|
||||
readWriteTimeoutMilliseconds: 15000000,
|
||||
enableDanmakuRecording: true,
|
||||
danmakuIncludeNonChatEvents: true,
|
||||
danmakuMinPollIntervalMilliseconds: 1000,
|
||||
danmakuRetryDelayMaxSeconds: 15,
|
||||
enableBackgroundPolling: true,
|
||||
autoStartRecordingOnLive: true,
|
||||
pollingIntervalSeconds: 60,
|
||||
enableEmailNotification: false,
|
||||
emailSmtpHost: "",
|
||||
emailSmtpPort: 587,
|
||||
emailUseSsl: true,
|
||||
emailUsername: "",
|
||||
emailPassword: "",
|
||||
emailFromAddress: "",
|
||||
emailFromDisplayName: "Live Recorder",
|
||||
emailToAddresses: "",
|
||||
notifyOnLiveStarted: true,
|
||||
notifyOnException: true,
|
||||
emailLiveStartedSubjectTemplate: "[{{appName}}] Live started: {{anchor}} {{title}} ({{roomId}})",
|
||||
emailLiveStartedBodyTemplateHtml: `<div style="font-family: 'Segoe UI', 'PingFang SC', sans-serif; color: #1f2937; line-height: 1.7;">
|
||||
<h2 style="margin: 0 0 16px; color: #3e5f7c;">Live started</h2>
|
||||
<p>The monitored live room is now online.</p>
|
||||
<ul>
|
||||
<li><strong>Platform:</strong> {{platform}}</li>
|
||||
<li><strong>Room ID:</strong> {{roomId}}</li>
|
||||
<li><strong>Title:</strong> {{title}}</li>
|
||||
<li><strong>Anchor:</strong> {{anchor}}</li>
|
||||
<li><strong>Detected At (UTC):</strong> {{detectedAtUtc}}</li>
|
||||
</ul>
|
||||
<p><strong>Source URL:</strong> <a href="{{sourceUrl}}">{{sourceUrl}}</a></p>
|
||||
</div>`,
|
||||
emailExceptionSubjectTemplate: "[{{appName}}] Exception: {{source}}",
|
||||
emailExceptionBodyTemplateHtml: `<div style="font-family: 'Segoe UI', 'PingFang SC', sans-serif; color: #1f2937; line-height: 1.7;">
|
||||
<h2 style="margin: 0 0 16px; color: #8b5e3c;">Exception detected</h2>
|
||||
<p>{{summary}}</p>
|
||||
<ul>
|
||||
<li><strong>Source:</strong> {{source}}</li>
|
||||
<li><strong>Live Room ID:</strong> {{liveRoomId}}</li>
|
||||
<li><strong>Room ID:</strong> {{roomId}}</li>
|
||||
<li><strong>Record Task ID:</strong> {{recordTaskId}}</li>
|
||||
<li><strong>Task Status:</strong> {{taskStatus}}</li>
|
||||
<li><strong>Occurred At (UTC):</strong> {{occurredAtUtc}}</li>
|
||||
</ul>
|
||||
<div style="margin-top: 16px; padding: 12px 14px; border-radius: 8px; background: #f5f5f5; white-space: pre-wrap;">{{detail}}</div>
|
||||
</div>`,
|
||||
douyinUserAgent: "",
|
||||
douyinReferer: "https://live.douyin.com/",
|
||||
douyinCookie: ""
|
||||
});
|
||||
|
||||
const outputTemplateTokens = [
|
||||
"{platform}",
|
||||
"{roomId}",
|
||||
"{anchor}",
|
||||
"{title}",
|
||||
"{yyyy}",
|
||||
"{MM}",
|
||||
"{dd}",
|
||||
"{HHmmss}",
|
||||
"{date}",
|
||||
"{time}",
|
||||
"{fileStem}",
|
||||
"{segmentSuffix}"
|
||||
];
|
||||
|
||||
const emailTemplateTokens = [
|
||||
"{{appName}}",
|
||||
"{{platform}}",
|
||||
"{{roomId}}",
|
||||
"{{title}}",
|
||||
"{{anchor}}",
|
||||
"{{sourceUrl}}",
|
||||
"{{detectedAtUtc}}",
|
||||
"{{source}}",
|
||||
"{{summary}}",
|
||||
"{{detail}}",
|
||||
"{{liveRoomId}}",
|
||||
"{{recordTaskId}}",
|
||||
"{{taskStatus}}",
|
||||
"{{occurredAtUtc}}"
|
||||
];
|
||||
|
||||
const canSendTestEmail = computed(() =>
|
||||
Boolean(form.emailSmtpHost.trim() && form.emailFromAddress.trim() && form.emailToAddresses.trim())
|
||||
);
|
||||
|
||||
const segmentedExamplePath = computed(() => {
|
||||
const extension = form.defaultOutputFormat === 1 ? "ts" : "mp4";
|
||||
return `Douyin/2026/04/15/主播名/221530_主播名_直播标题_123456789_00001.${extension}`;
|
||||
});
|
||||
|
||||
const nestedSegmentedExamplePath = computed(() => {
|
||||
const extension = form.defaultOutputFormat === 1 ? "ts" : "mp4";
|
||||
return `Douyin/2026/04/15/主播名/221530_主播名_直播标题_123456789/221530_主播名_直播标题_123456789_00001.${extension}`;
|
||||
});
|
||||
|
||||
async function loadSettings() {
|
||||
loading.value = true;
|
||||
|
||||
try {
|
||||
const { data } = await apiClient.get<SystemSettings>("/settings");
|
||||
Object.assign(form, data);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function saveSettings() {
|
||||
saving.value = true;
|
||||
|
||||
try {
|
||||
const { data } = await apiClient.put<SystemSettings>("/settings", form);
|
||||
Object.assign(form, data);
|
||||
ElMessage.success("设置已保存");
|
||||
} finally {
|
||||
saving.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function sendTestEmail() {
|
||||
testingEmail.value = true;
|
||||
|
||||
try {
|
||||
await apiClient.post("/settings/test-email", {
|
||||
emailSmtpHost: form.emailSmtpHost,
|
||||
emailSmtpPort: form.emailSmtpPort,
|
||||
emailUseSsl: form.emailUseSsl,
|
||||
emailUsername: form.emailUsername,
|
||||
emailPassword: form.emailPassword,
|
||||
emailFromAddress: form.emailFromAddress,
|
||||
emailFromDisplayName: form.emailFromDisplayName,
|
||||
emailToAddresses: form.emailToAddresses,
|
||||
emailLiveStartedSubjectTemplate: form.emailLiveStartedSubjectTemplate,
|
||||
emailLiveStartedBodyTemplateHtml: form.emailLiveStartedBodyTemplateHtml,
|
||||
emailExceptionSubjectTemplate: form.emailExceptionSubjectTemplate,
|
||||
emailExceptionBodyTemplateHtml: form.emailExceptionBodyTemplateHtml
|
||||
});
|
||||
|
||||
ElMessage.success("测试邮件已发送,请检查收件箱");
|
||||
} catch (error: any) {
|
||||
const message = error?.response?.data?.message ?? error?.message ?? "测试邮件发送失败";
|
||||
ElMessage.error(message);
|
||||
} finally {
|
||||
testingEmail.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(loadSettings);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page-stack">
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h1 class="page-title">系统设置</h1>
|
||||
<p class="page-subtitle">
|
||||
配置录制参数、路径模板、弹幕录制、后台巡检和邮件通知。测试邮件会直接使用当前表单值,不要求先保存。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<el-button type="primary" :loading="saving" @click="saveSettings">保存设置</el-button>
|
||||
</div>
|
||||
|
||||
<div class="settings-grid" v-loading="loading">
|
||||
<el-card class="surface-card settings-card" shadow="never">
|
||||
<h3 class="section-title">录制基础</h3>
|
||||
<p class="section-subtitle">默认清晰度、输出格式、分段时长、ffmpeg 模板和网络容错参数统一在这里维护。</p>
|
||||
|
||||
<el-form label-position="top">
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="ffmpeg 路径">
|
||||
<el-input v-model="form.ffmpegPath" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="录制输出根目录">
|
||||
<el-input v-model="form.outputRoot" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="默认清晰度">
|
||||
<el-input v-model="form.defaultQuality" placeholder="origin / FULL_HD / HD / SD" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="直播保存格式">
|
||||
<el-select v-model="form.defaultOutputFormat">
|
||||
<el-option
|
||||
v-for="(label, value) in outputFormatLabelMap"
|
||||
:key="value"
|
||||
:label="label"
|
||||
:value="Number(value)"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="保存模式">
|
||||
<el-select v-model="form.saveMode">
|
||||
<el-option
|
||||
v-for="(label, value) in saveModeLabelMap"
|
||||
:key="value"
|
||||
:label="label"
|
||||
:value="Number(value)"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="录制模板">
|
||||
<el-select v-model="form.recordingTemplate">
|
||||
<el-option
|
||||
v-for="(label, value) in recordingTemplateLabelMap"
|
||||
:key="value"
|
||||
:label="label"
|
||||
:value="Number(value)"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="分段时长(分钟)">
|
||||
<el-input-number v-model="form.segmentDurationMinutes" :min="1" :max="720" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="最大重连延迟(秒)">
|
||||
<el-input-number v-model="form.reconnectDelayMaxSeconds" :min="1" :max="300" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="读写超时(毫秒)">
|
||||
<el-input-number v-model="form.readWriteTimeoutMilliseconds" :min="1000" :max="60000000" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="启用 ffmpeg 自动重连">
|
||||
<el-switch v-model="form.enableAutoReconnect" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-form>
|
||||
</el-card>
|
||||
|
||||
<el-card class="surface-card settings-card" shadow="never">
|
||||
<h3 class="section-title">弹幕录制</h3>
|
||||
<p class="section-subtitle">控制是否并行录制弹幕 XML、是否保留非聊天事件,以及轮询和失败重连节奏。</p>
|
||||
|
||||
<el-form label-position="top">
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="启用弹幕录制">
|
||||
<el-switch v-model="form.enableDanmakuRecording" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="记录非聊天事件">
|
||||
<el-switch v-model="form.danmakuIncludeNonChatEvents" :disabled="!form.enableDanmakuRecording" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="最小轮询间隔(毫秒)">
|
||||
<el-input-number
|
||||
v-model="form.danmakuMinPollIntervalMilliseconds"
|
||||
:min="100"
|
||||
:max="60000"
|
||||
:disabled="!form.enableDanmakuRecording"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="最大重试退避(秒)">
|
||||
<el-input-number
|
||||
v-model="form.danmakuRetryDelayMaxSeconds"
|
||||
:min="1"
|
||||
:max="300"
|
||||
:disabled="!form.enableDanmakuRecording"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-form>
|
||||
|
||||
<div class="helper-panel">
|
||||
XML 文件会继续与视频同 stem 保存,不额外新增单独的弹幕路径模板。关闭“记录非聊天事件”后,只会保留聊天 <code><d></code> 节点。
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<el-card class="surface-card settings-card" shadow="never">
|
||||
<h3 class="section-title">路径模板</h3>
|
||||
<p class="section-subtitle">目录层级和文件名都支持变量。分段模式不再强制多建一层目录,是否分出子目录完全由模板决定。</p>
|
||||
|
||||
<el-form label-position="top">
|
||||
<el-form-item label="目录模板">
|
||||
<el-input
|
||||
v-model="form.outputDirectoryTemplate"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="{platform}/{yyyy}/{MM}/{dd}/{anchor}"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="文件名模板">
|
||||
<el-input
|
||||
v-model="form.outputFileNameTemplate"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="{HHmmss}_{anchor}_{title}_{roomId}{segmentSuffix}"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<div class="example-box">
|
||||
<div class="example-box__label">示例输出</div>
|
||||
<div class="monospace example-box__value">默认分段:{{ segmentedExamplePath }}</div>
|
||||
<div class="monospace example-box__value">目录模板含 {fileStem}:{{ nestedSegmentedExamplePath }}</div>
|
||||
</div>
|
||||
|
||||
<div class="helper-panel">
|
||||
`{fileStem}` 只用于目录模板,表示按文件模板渲染后的基础文件名;`{segmentSuffix}` 只用于文件模板,分段模式下会渲染为 `_00001` 这类后缀,单文件模式下为空。
|
||||
</div>
|
||||
|
||||
<div class="token-list">
|
||||
<span v-for="token in outputTemplateTokens" :key="token" class="token-chip">{{ token }}</span>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<el-card class="surface-card settings-card" shadow="never">
|
||||
<h3 class="section-title">后台巡检</h3>
|
||||
<p class="section-subtitle">控制是否定时检测直播状态,并在开播后自动创建录制任务。</p>
|
||||
|
||||
<el-form label-position="top">
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="启用后台巡检">
|
||||
<el-switch v-model="form.enableBackgroundPolling" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="检测到开播后自动录制">
|
||||
<el-switch v-model="form.autoStartRecordingOnLive" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="巡检间隔(秒)">
|
||||
<el-input-number v-model="form.pollingIntervalSeconds" :min="10" :max="3600" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-form>
|
||||
</el-card>
|
||||
|
||||
<el-card class="surface-card settings-card settings-grid__full" shadow="never">
|
||||
<h3 class="section-title">邮件通知</h3>
|
||||
<p class="section-subtitle">支持 SMTP、开播提醒和异常提醒 HTML 模板。测试发送会直接使用当前表单中的 SMTP 与模板。</p>
|
||||
|
||||
<el-form label-position="top">
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="6">
|
||||
<el-form-item label="启用邮件通知">
|
||||
<el-switch v-model="form.enableEmailNotification" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-form-item label="启用 SSL">
|
||||
<el-switch v-model="form.emailUseSsl" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-form-item label="开播提醒">
|
||||
<el-switch v-model="form.notifyOnLiveStarted" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-form-item label="异常提醒">
|
||||
<el-switch v-model="form.notifyOnException" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="8">
|
||||
<el-form-item label="SMTP Host">
|
||||
<el-input v-model="form.emailSmtpHost" placeholder="smtp.example.com" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="SMTP Port">
|
||||
<el-input-number v-model="form.emailSmtpPort" :min="1" :max="65535" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="发件人名称">
|
||||
<el-input v-model="form.emailFromDisplayName" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="8">
|
||||
<el-form-item label="SMTP 用户名">
|
||||
<el-input v-model="form.emailUsername" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="SMTP 密码">
|
||||
<el-input v-model="form.emailPassword" type="password" show-password />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="发件地址">
|
||||
<el-input v-model="form.emailFromAddress" placeholder="noreply@example.com" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="24">
|
||||
<el-form-item label="收件人列表">
|
||||
<el-input
|
||||
v-model="form.emailToAddresses"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="多个地址可用逗号、分号或换行分隔"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="24">
|
||||
<div class="template-section">
|
||||
<div class="template-section__header">
|
||||
<div>
|
||||
<h4 class="template-section__title">开播提醒模板</h4>
|
||||
<p class="template-section__subtitle">主题模板输出纯文本,正文模板支持 HTML。</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-form-item label="主题模板">
|
||||
<el-input v-model="form.emailLiveStartedSubjectTemplate" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="HTML 正文模板">
|
||||
<el-input v-model="form.emailLiveStartedBodyTemplateHtml" type="textarea" :rows="10" />
|
||||
</el-form-item>
|
||||
</div>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="24">
|
||||
<div class="template-section">
|
||||
<div class="template-section__header">
|
||||
<div>
|
||||
<h4 class="template-section__title">异常提醒模板</h4>
|
||||
<p class="template-section__subtitle">异常邮件会把错误摘要和任务上下文按变量注入到 HTML 中。</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-form-item label="主题模板">
|
||||
<el-input v-model="form.emailExceptionSubjectTemplate" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="HTML 正文模板">
|
||||
<el-input v-model="form.emailExceptionBodyTemplateHtml" type="textarea" :rows="12" />
|
||||
</el-form-item>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-form>
|
||||
|
||||
<div class="template-help">
|
||||
<div class="template-help__label">可用占位符</div>
|
||||
<div class="token-list">
|
||||
<span v-for="token in emailTemplateTokens" :key="token" class="token-chip">{{ token }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="email-actions">
|
||||
<div class="helper-text">测试发送不会自动保存配置,邮件中会同时渲染“开播提醒示例”和“异常提醒示例”。</div>
|
||||
<el-button :loading="testingEmail" :disabled="!canSendTestEmail" @click="sendTestEmail">测试发送</el-button>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<el-card class="surface-card settings-card settings-grid__full" shadow="never">
|
||||
<h3 class="section-title">Douyin 请求头</h3>
|
||||
<p class="section-subtitle">这些伪装参数会同时用于 Douyin API 请求和 ffmpeg 输入拉流,适合处理直播流读取校验问题。</p>
|
||||
|
||||
<el-form label-position="top">
|
||||
<el-form-item label="Douyin User-Agent">
|
||||
<el-input v-model="form.douyinUserAgent" type="textarea" :rows="3" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="Douyin Referer">
|
||||
<el-input v-model="form.douyinReferer" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="Douyin Cookie">
|
||||
<el-input
|
||||
v-model="form.douyinCookie"
|
||||
type="textarea"
|
||||
:rows="4"
|
||||
placeholder="可粘贴 ttwid、msToken 等 Cookie"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.page-stack {
|
||||
display: grid;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.settings-grid {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1.15fr) minmax(320px, 0.85fr);
|
||||
align-items: start;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.settings-grid__full {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.settings-card :deep(.el-card__body) {
|
||||
padding-top: 20px;
|
||||
}
|
||||
|
||||
.example-box {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
padding: 16px 18px;
|
||||
border-radius: 12px;
|
||||
background: linear-gradient(180deg, rgba(243, 246, 250, 0.92), rgba(255, 255, 255, 0.84));
|
||||
border: 1px solid rgba(91, 110, 129, 0.14);
|
||||
}
|
||||
|
||||
.example-box__label {
|
||||
font-size: 12px;
|
||||
color: #6e7883;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.06em;
|
||||
}
|
||||
|
||||
.example-box__value {
|
||||
font-size: 12px;
|
||||
line-height: 1.7;
|
||||
color: #44505c;
|
||||
}
|
||||
|
||||
.helper-panel {
|
||||
margin-top: 14px;
|
||||
padding: 14px 16px;
|
||||
border-radius: 12px;
|
||||
background: rgba(245, 248, 251, 0.74);
|
||||
border: 1px solid rgba(91, 110, 129, 0.12);
|
||||
color: #627080;
|
||||
font-size: 13px;
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.template-section {
|
||||
padding: 18px 18px 4px;
|
||||
border-radius: 12px;
|
||||
border: 1px solid rgba(91, 110, 129, 0.12);
|
||||
background: linear-gradient(180deg, rgba(248, 250, 252, 0.94), rgba(255, 255, 255, 0.84));
|
||||
}
|
||||
|
||||
.template-section__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.template-section__title {
|
||||
margin: 0;
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.02em;
|
||||
color: #1e2937;
|
||||
}
|
||||
|
||||
.template-section__subtitle {
|
||||
margin: 6px 0 0;
|
||||
font-size: 13px;
|
||||
line-height: 1.7;
|
||||
color: #697586;
|
||||
}
|
||||
|
||||
.template-help {
|
||||
margin-top: 8px;
|
||||
padding: 16px 18px;
|
||||
border-radius: 12px;
|
||||
border: 1px solid rgba(91, 110, 129, 0.12);
|
||||
background: rgba(245, 248, 251, 0.72);
|
||||
}
|
||||
|
||||
.template-help__label {
|
||||
margin-bottom: 10px;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
color: #687381;
|
||||
letter-spacing: 0.06em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.email-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
margin-top: 16px;
|
||||
padding-top: 16px;
|
||||
border-top: 1px solid rgba(91, 110, 129, 0.12);
|
||||
}
|
||||
|
||||
.helper-text {
|
||||
color: #657180;
|
||||
font-size: 13px;
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
@media (max-width: 1100px) {
|
||||
.settings-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.email-actions {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user