add i18n step1
This commit is contained in:
parent
34d553a890
commit
79e3db6078
23 changed files with 2309 additions and 450 deletions
|
|
@ -24,6 +24,7 @@ var (
|
|||
prefixMatchPaths = []string{ // 保持前缀匹配,因为数量少
|
||||
"/js/",
|
||||
"/css/",
|
||||
"/locales",
|
||||
}
|
||||
loginMatchPaths = map[string]struct{}{
|
||||
"/login": {},
|
||||
|
|
|
|||
1094
frontend/css/.style.css
Normal file
1094
frontend/css/.style.css
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -46,6 +46,7 @@ body {
|
|||
color: var(--text-color);
|
||||
line-height: 1.6;
|
||||
overflow: hidden;
|
||||
/* 防止主页面滚动条出现, 内部组件自行管理滚动 */
|
||||
transition: background-color var(--transition-speed), color var(--transition-speed);
|
||||
}
|
||||
|
||||
|
|
@ -53,8 +54,7 @@ body {
|
|||
display: none !important;
|
||||
}
|
||||
|
||||
/* --- 新增: 自定义滚动条样式 --- */
|
||||
/* 适用于 Webkit 内核浏览器 (Chrome, Safari, Edge) */
|
||||
/* --- 自定义滚动条样式 (适用于 Webkit 内核浏览器) --- */
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
|
|
@ -69,18 +69,21 @@ body {
|
|||
border-radius: 10px;
|
||||
border: 2px solid transparent;
|
||||
background-clip: content-box;
|
||||
/* 使边框内缩 */
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background-color: var(--scrollbar-thumb-hover-color);
|
||||
}
|
||||
|
||||
/* 适用于 Firefox */
|
||||
/* --- 自定义滚动条样式 (适用于 Firefox) --- */
|
||||
* {
|
||||
scrollbar-width: thin;
|
||||
/* 使滚动条更窄 */
|
||||
scrollbar-color: var(--scrollbar-thumb-color) var(--scrollbar-track-color);
|
||||
}
|
||||
|
||||
/* --- 登录页样式 --- */
|
||||
.login-page-body {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
|
@ -130,10 +133,12 @@ body {
|
|||
margin-top: 16px;
|
||||
width: 100%;
|
||||
justify-content: space-between;
|
||||
/* 内部元素左右对齐 */
|
||||
padding-left: 24px;
|
||||
padding-right: 24px;
|
||||
}
|
||||
|
||||
/* --- Toast 提示样式 --- */
|
||||
.toast-container {
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
|
|
@ -156,12 +161,14 @@ body {
|
|||
width: 320px;
|
||||
opacity: 0;
|
||||
transform: translateX(100%);
|
||||
/* 初始位置在屏幕外 */
|
||||
transition: opacity 0.3s ease, transform 0.3s ease;
|
||||
}
|
||||
|
||||
.toast.show {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
/* 显示时滑入 */
|
||||
}
|
||||
|
||||
.toast-icon {
|
||||
|
|
@ -195,6 +202,7 @@ body {
|
|||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
/* --- Dialog 确认框样式 --- */
|
||||
#dialog-container {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
|
|
@ -208,6 +216,7 @@ body {
|
|||
background-color: rgba(0, 0, 0, 0.3);
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
/* 默认隐藏 */
|
||||
transition: opacity 0.2s ease, visibility 0.2s;
|
||||
}
|
||||
|
||||
|
|
@ -225,11 +234,13 @@ body {
|
|||
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1);
|
||||
text-align: center;
|
||||
transform: scale(0.95);
|
||||
/* 初始略微缩小 */
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
#dialog-container.active .dialog-box {
|
||||
transform: scale(1);
|
||||
/* 显示时放大 */
|
||||
}
|
||||
|
||||
.dialog-message {
|
||||
|
|
@ -246,8 +257,10 @@ body {
|
|||
|
||||
.dialog-actions .btn {
|
||||
width: auto;
|
||||
/* 按钮宽度自适应 */
|
||||
}
|
||||
|
||||
/* --- Modal 模态框样式 --- */
|
||||
#modal-container {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
|
|
@ -261,6 +274,7 @@ body {
|
|||
background-color: rgba(0, 0, 0, 0.4);
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
/* 默认隐藏 */
|
||||
transition: opacity 0.2s ease, visibility 0.2s;
|
||||
}
|
||||
|
||||
|
|
@ -277,11 +291,13 @@ body {
|
|||
text-align: left;
|
||||
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1);
|
||||
transform: scale(0.95) translateY(-10px);
|
||||
/* 初始略微缩小并上移 */
|
||||
transition: transform 0.25s ease;
|
||||
}
|
||||
|
||||
#modal-container.active .modal-box {
|
||||
transform: scale(1) translateY(0);
|
||||
/* 显示时放大并恢复位置 */
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
|
|
@ -300,7 +316,9 @@ body {
|
|||
.modal-content {
|
||||
padding: 24px;
|
||||
max-height: 60vh;
|
||||
/* 限制内容高度, 避免模态框过高 */
|
||||
overflow-y: auto;
|
||||
/* 内容超出时显示滚动条 */
|
||||
}
|
||||
|
||||
ul.preset-list {
|
||||
|
|
@ -326,6 +344,7 @@ ul.preset-list li p {
|
|||
margin-top: 4px;
|
||||
}
|
||||
|
||||
/* --- 主应用布局 --- */
|
||||
.app-container {
|
||||
display: flex;
|
||||
height: 100vh;
|
||||
|
|
@ -333,16 +352,20 @@ ul.preset-list li p {
|
|||
|
||||
.main-content {
|
||||
flex-grow: 1;
|
||||
/* 占据剩余空间 */
|
||||
padding: 24px 32px;
|
||||
overflow-y: auto;
|
||||
/* 允许内容垂直滚动 */
|
||||
}
|
||||
|
||||
/* --- 视图切换动画 --- */
|
||||
#view-container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.view {
|
||||
animation: fadeIn 0.5s ease;
|
||||
/* 视图切换时的淡入动画 */
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
|
|
@ -357,6 +380,7 @@ ul.preset-list li p {
|
|||
}
|
||||
}
|
||||
|
||||
/* --- 侧边栏 --- */
|
||||
.sidebar {
|
||||
width: var(--sidebar-width);
|
||||
background-color: var(--surface-color);
|
||||
|
|
@ -365,6 +389,7 @@ ul.preset-list li p {
|
|||
flex-direction: column;
|
||||
border-right: 1px solid var(--border-color);
|
||||
flex-shrink: 0;
|
||||
/* 防止侧边栏被挤压 */
|
||||
transition: transform var(--transition-speed) ease, background-color var(--transition-speed), border-color var(--transition-speed);
|
||||
}
|
||||
|
||||
|
|
@ -385,6 +410,7 @@ ul.preset-list li p {
|
|||
|
||||
.sidebar-nav {
|
||||
flex-grow: 1;
|
||||
/* 占据剩余空间 */
|
||||
}
|
||||
|
||||
.sidebar-nav ul li a {
|
||||
|
|
@ -414,6 +440,7 @@ ul.preset-list li p {
|
|||
|
||||
.sidebar-bottom {
|
||||
margin-top: auto;
|
||||
/* 推到底部 */
|
||||
padding-top: 16px;
|
||||
border-top: 1px solid var(--border-color);
|
||||
transition: border-color var(--transition-speed);
|
||||
|
|
@ -536,6 +563,28 @@ input:checked+.slider:before {
|
|||
transition: border-color var(--transition-speed);
|
||||
}
|
||||
|
||||
/* 侧边栏遮罩层 (仅用于移动端) */
|
||||
.sidebar-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
z-index: 199;
|
||||
/* 位于侧边栏下方 */
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
/* 默认隐藏 */
|
||||
transition: opacity 0.3s ease, visibility 0.3s;
|
||||
}
|
||||
|
||||
.sidebar-overlay.is-visible {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
/* --- 主内容区头部样式 --- */
|
||||
.main-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
|
@ -550,9 +599,11 @@ input:checked+.slider:before {
|
|||
|
||||
#menu-toggle-btn {
|
||||
display: none;
|
||||
/* 默认隐藏, 仅在小屏幕显示 */
|
||||
margin-right: 16px;
|
||||
}
|
||||
|
||||
/* --- 卡片面板样式 --- */
|
||||
.card-panel {
|
||||
background-color: var(--surface-color);
|
||||
border-radius: var(--border-radius-large);
|
||||
|
|
@ -574,6 +625,7 @@ input:checked+.slider:before {
|
|||
flex-grow: 1;
|
||||
}
|
||||
|
||||
/* --- 按钮样式 --- */
|
||||
.btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
|
|
@ -586,10 +638,12 @@ input:checked+.slider:before {
|
|||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
width: 100%;
|
||||
/* 默认宽度为100% */
|
||||
}
|
||||
|
||||
.btn:active {
|
||||
transform: scale(0.97);
|
||||
/* 点击时轻微缩小 */
|
||||
}
|
||||
|
||||
.btn:disabled {
|
||||
|
|
@ -598,6 +652,7 @@ input:checked+.slider:before {
|
|||
cursor: not-allowed;
|
||||
border-color: transparent;
|
||||
transform: none;
|
||||
/* 禁用时无动画 */
|
||||
}
|
||||
|
||||
[data-theme="dark"] .btn:disabled {
|
||||
|
|
@ -656,6 +711,7 @@ input:checked+.slider:before {
|
|||
padding: 6px 12px;
|
||||
font-size: 0.875rem;
|
||||
width: auto;
|
||||
/* 小按钮宽度自适应 */
|
||||
}
|
||||
|
||||
.btn-icon {
|
||||
|
|
@ -668,6 +724,7 @@ input:checked+.slider:before {
|
|||
font-size: 1.1rem;
|
||||
border-radius: 50%;
|
||||
width: auto;
|
||||
/* 图标按钮宽度自适应 */
|
||||
}
|
||||
|
||||
.btn-icon:hover {
|
||||
|
|
@ -683,6 +740,7 @@ input:checked+.slider:before {
|
|||
font-weight: 600;
|
||||
padding: 4px 8px;
|
||||
width: auto;
|
||||
/* 链接按钮宽度自适应 */
|
||||
}
|
||||
|
||||
.btn-link:hover {
|
||||
|
|
@ -691,12 +749,15 @@ input:checked+.slider:before {
|
|||
|
||||
.main-header .btn-primary {
|
||||
width: auto;
|
||||
/* 主头部按钮宽度自适应 */
|
||||
}
|
||||
|
||||
.form-actions .btn {
|
||||
width: auto;
|
||||
/* 表单操作按钮宽度自适应 */
|
||||
}
|
||||
|
||||
/* --- 配置列表样式 --- */
|
||||
.config-list-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
|
@ -715,6 +776,7 @@ input:checked+.slider:before {
|
|||
|
||||
.config-item:hover {
|
||||
transform: translateY(-2px);
|
||||
/* 悬停时轻微上浮 */
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
|
|
@ -728,9 +790,11 @@ input:checked+.slider:before {
|
|||
gap: 8px;
|
||||
}
|
||||
|
||||
/* --- 表单样式 --- */
|
||||
.form-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||
/* 响应式网格布局 */
|
||||
gap: 24px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
|
@ -761,14 +825,16 @@ fieldset legend {
|
|||
box-shadow: 0 0 0 2px rgba(79, 70, 229, 0.2);
|
||||
}
|
||||
|
||||
/* 新增: 重置数字输入框的默认样式 */
|
||||
/* 重置数字输入框的默认样式 (移除上下箭头) */
|
||||
input[type="number"] {
|
||||
-moz-appearance: textfield;
|
||||
/* Firefox */
|
||||
}
|
||||
|
||||
input[type="number"]::-webkit-inner-spin-button,
|
||||
input[type="number"]::-webkit-outer-spin-button {
|
||||
-webkit-appearance: none;
|
||||
/* Chrome, Safari */
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
|
|
@ -820,6 +886,7 @@ fieldset {
|
|||
|
||||
.custom-checkbox input {
|
||||
display: none;
|
||||
/* 隐藏原生复选框 */
|
||||
}
|
||||
|
||||
.custom-checkbox .checkmark {
|
||||
|
|
@ -837,6 +904,7 @@ fieldset {
|
|||
|
||||
.custom-checkbox .checkmark::after {
|
||||
content: "\f00c";
|
||||
/* Font Awesome 对勾图标 */
|
||||
font-family: "Font Awesome 6 Free";
|
||||
font-weight: 900;
|
||||
font-size: 12px;
|
||||
|
|
@ -845,20 +913,23 @@ fieldset {
|
|||
top: 3px;
|
||||
left: 3px;
|
||||
transform: scale(0);
|
||||
/* 默认隐藏 */
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
|
||||
.custom-checkbox input:checked+.checkmark::after {
|
||||
transform: scale(1);
|
||||
/* 选中时显示 */
|
||||
}
|
||||
|
||||
.header-entry {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr auto;
|
||||
/* 键、值、删除按钮 */
|
||||
gap: 12px;
|
||||
margin-bottom: 12px;
|
||||
align-items: center;
|
||||
/* 修改: 确保所有元素垂直居中对齐 */
|
||||
/* 确保所有元素垂直居中对齐 */
|
||||
}
|
||||
|
||||
.header-entry input {
|
||||
|
|
@ -870,6 +941,7 @@ fieldset {
|
|||
color: var(--text-color);
|
||||
}
|
||||
|
||||
/* --- 分段控制器样式 --- */
|
||||
.segmented-control {
|
||||
position: relative;
|
||||
display: flex;
|
||||
|
|
@ -883,6 +955,7 @@ fieldset {
|
|||
|
||||
.segmented-control button {
|
||||
flex: 1;
|
||||
/* 均分空间 */
|
||||
padding: 8px 12px;
|
||||
border: none;
|
||||
background-color: transparent;
|
||||
|
|
@ -891,6 +964,7 @@ fieldset {
|
|||
cursor: pointer;
|
||||
transition: color 0.2s ease;
|
||||
z-index: 2;
|
||||
/* 确保按钮在滑块之上 */
|
||||
}
|
||||
|
||||
.segmented-control button:hover {
|
||||
|
|
@ -903,6 +977,7 @@ fieldset {
|
|||
|
||||
[data-theme="dark"] .segmented-control button.active {
|
||||
color: var(--text-color);
|
||||
/* 深色模式下保持文本颜色 */
|
||||
}
|
||||
|
||||
#segmented-control-slider {
|
||||
|
|
@ -913,14 +988,16 @@ fieldset {
|
|||
border-radius: 6px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
transition: transform 0.25s cubic-bezier(0.4, 0, 0.2, 1), width 0.25s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
/* 平滑过渡 */
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
/* --- 自定义选择框样式 --- */
|
||||
.custom-select {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* 修改: 统一 .select-selected 的外观, 使其与 input 一致 */
|
||||
/* 统一 .select-selected 的外观, 使其与 input 文本框一致 */
|
||||
.select-selected {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
|
@ -938,8 +1015,8 @@ fieldset {
|
|||
transition: border-color 0.2s, box-shadow 0.2s, background-color var(--transition-speed);
|
||||
line-height: 1.5;
|
||||
/* 与input的字体和内边距计算出的行高一致 */
|
||||
height: calc(1.5em + 24px + 2px);
|
||||
/* font-size * line-height + padding * 2 + border * 2 */
|
||||
/* 准确计算高度: font-size * line-height + padding-top + padding-bottom + border-top + border-bottom */
|
||||
height: calc(1em * 1.5 + 12px * 2 + 1px * 2);
|
||||
}
|
||||
|
||||
.select-selected.select-arrow-active {
|
||||
|
|
@ -949,6 +1026,7 @@ fieldset {
|
|||
|
||||
.select-selected::after {
|
||||
content: '\f078';
|
||||
/* Font Awesome 向下箭头图标 */
|
||||
font-family: 'Font Awesome 6 Free';
|
||||
font-weight: 900;
|
||||
transition: transform var(--transition-speed) ease;
|
||||
|
|
@ -956,12 +1034,14 @@ fieldset {
|
|||
|
||||
.select-selected.select-arrow-active::after {
|
||||
transform: rotate(180deg);
|
||||
/* 展开时箭头旋转 */
|
||||
}
|
||||
|
||||
.select-items {
|
||||
position: absolute;
|
||||
background-color: var(--surface-color);
|
||||
top: 100%;
|
||||
/* 位于选择框下方 */
|
||||
left: 0;
|
||||
right: 0;
|
||||
border: 1px solid var(--border-color);
|
||||
|
|
@ -971,9 +1051,12 @@ fieldset {
|
|||
margin-top: 8px;
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
/* 选项过多时滚动 */
|
||||
opacity: 0;
|
||||
transform: translateY(-10px);
|
||||
/* 默认略微上移 */
|
||||
visibility: hidden;
|
||||
/* 默认隐藏 */
|
||||
transition: opacity 0.2s ease, transform 0.2s ease, visibility 0.2s;
|
||||
}
|
||||
|
||||
|
|
@ -994,48 +1077,102 @@ fieldset {
|
|||
background-color: var(--bg-color);
|
||||
}
|
||||
|
||||
/* --- 渲染输出面板样式 --- */
|
||||
#rendered-output-panel pre {
|
||||
background-color: var(--bg-color);
|
||||
padding: 16px;
|
||||
border-radius: var(--border-radius-small);
|
||||
overflow-x: auto;
|
||||
/* 允许代码水平滚动 */
|
||||
white-space: pre-wrap;
|
||||
/* 自动换行 */
|
||||
word-break: break-all;
|
||||
/* 单词内断行 */
|
||||
border: 1px solid var(--border-color);
|
||||
transition: background-color var(--transition-speed);
|
||||
}
|
||||
|
||||
/* --- 角落语言切换器样式 --- */
|
||||
.language-switcher-corner {
|
||||
position: fixed;
|
||||
bottom: 20px;
|
||||
right: 20px;
|
||||
z-index: 100;
|
||||
}
|
||||
.language-switcher-corner .btn-icon {
|
||||
background-color: var(--surface-color);
|
||||
border: 1px solid var(--border-color);
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
.language-switcher-corner .btn-icon:hover {
|
||||
background-color: var(--bg-color);
|
||||
border-color: var(--primary-color);
|
||||
}
|
||||
.language-switcher-corner ul {
|
||||
position: absolute;
|
||||
bottom: 60px; /* 在按钮上方 */
|
||||
right: 0;
|
||||
list-style: none;
|
||||
padding: 8px;
|
||||
margin: 0;
|
||||
background-color: var(--surface-color);
|
||||
border-radius: var(--border-radius-small);
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
||||
border: 1px solid var(--border-color);
|
||||
width: 150px;
|
||||
}
|
||||
.language-switcher-corner ul li {
|
||||
padding: 8px 12px;
|
||||
cursor: pointer;
|
||||
border-radius: 6px;
|
||||
font-weight: 500;
|
||||
}
|
||||
.language-switcher-corner ul li:hover {
|
||||
background-color: var(--bg-color);
|
||||
}
|
||||
.language-switcher-corner ul li.active {
|
||||
background-color: var(--primary-color);
|
||||
color: white;
|
||||
}
|
||||
/* --- 响应式设计 (媒体查询) --- */
|
||||
@media (max-width: 992px) {
|
||||
.sidebar {
|
||||
position: fixed;
|
||||
/* 在小屏幕上脱离文档流 */
|
||||
z-index: 200;
|
||||
/* 确保在最上层 */
|
||||
height: 100%;
|
||||
transform: translateX(-100%);
|
||||
/* 默认隐藏在左侧 */
|
||||
box-shadow: 0 0 20px rgba(0, 0, 0, 0.1);
|
||||
/* 添加阴影效果 */
|
||||
}
|
||||
|
||||
.sidebar.is-open {
|
||||
transform: translateX(0);
|
||||
/* 打开时滑入 */
|
||||
}
|
||||
|
||||
#menu-toggle-btn {
|
||||
display: inline-flex;
|
||||
/* 仅在小屏幕上显示菜单切换按钮 */
|
||||
}
|
||||
|
||||
.main-header .btn-text {
|
||||
display: none;
|
||||
/* 在小屏幕上隐藏按钮内的长文本 */
|
||||
}
|
||||
|
||||
.sidebar-nav span,
|
||||
.logout-section span {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.caddy-control-panel .btn span {
|
||||
display: inline;
|
||||
/* 隐藏侧边栏导航和底部区域的文本, 只显示图标 */
|
||||
.main-header .btn-primary .btn-text {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
padding: 16px;
|
||||
/* 调整小屏幕内边距 */
|
||||
}
|
||||
}
|
||||
|
|
@ -4,7 +4,7 @@
|
|||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>全局配置 - CaddyDash</title>
|
||||
<title data-i18n="pages.global.page_title">全局配置 - CaddyDash</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
|
|
@ -14,18 +14,18 @@
|
|||
|
||||
<body>
|
||||
<div class="app-container">
|
||||
<!-- 侧边栏 (保持不变) -->
|
||||
<aside class="sidebar" id="sidebar">
|
||||
<header class="sidebar-header"><i class="fa-solid fa-rocket"></i>
|
||||
<h1>CaddyDash</h1>
|
||||
</header>
|
||||
<nav class="sidebar-nav">
|
||||
<ul>
|
||||
<li><a href="/" data-nav-id="configs"><i class="fa-solid fa-sitemap"></i> <span>站点配置</span></a></li>
|
||||
<li><a href="/" data-nav-id="configs"><i class="fa-solid fa-sitemap"></i> <span
|
||||
data-i18n="nav.configs">站点配置</span></a></li>
|
||||
<li><a href="/global.html" data-nav-id="global"><i class="fa-solid fa-globe"></i>
|
||||
<span>全局配置</span></a></li>
|
||||
<span data-i18n="nav.global">全局配置</span></a></li>
|
||||
<li><a href="/settings.html" data-nav-id="settings"><i class="fa-solid fa-gears"></i>
|
||||
<span>面板设置</span></a></li>
|
||||
<span data-i18n="nav.settings">面板设置</span></a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
<div class="sidebar-bottom">
|
||||
|
|
@ -34,96 +34,98 @@
|
|||
</div>
|
||||
<div class="caddy-control-panel">
|
||||
<div id="caddy-status-indicator" class="caddy-status"><span class="status-dot checking"></span><span
|
||||
class="status-text">检查中...</span></div>
|
||||
class="status-text" data-i18n="status.checking">检查中...</span></div>
|
||||
<div id="caddy-action-button-container"></div>
|
||||
</div>
|
||||
<div class="logout-section"><button id="logout-btn" class="btn btn-secondary"><i
|
||||
class="fa-solid fa-right-from-bracket"></i><span>退出登录</span></button></div>
|
||||
class="fa-solid fa-right-from-bracket"></i><span data-i18n="nav.logout">退出登录</span></button></div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<main class="main-content">
|
||||
<header class="main-header">
|
||||
<button class="btn-icon" id="menu-toggle-btn"><i class="fa-solid fa-bars"></i></button>
|
||||
<h2>全局 Caddyfile 配置</h2>
|
||||
<h2 data-i18n="pages.global.title">全局 Caddyfile 配置</h2>
|
||||
</header>
|
||||
|
||||
<div id="view-container">
|
||||
<section class="card-panel">
|
||||
<p class="text-secondary" style="margin-top:-1rem; margin-bottom: 2rem;">修改这些配置将会重写您的主 Caddyfile 并触发
|
||||
Caddy 重载。</p>
|
||||
<p class="text-secondary" style="margin-top:-1rem; margin-bottom: 2rem;"
|
||||
data-i18n="pages.global.description">修改这些配置将会重写您的主 Caddyfile 并触发 Caddy 重载。</p>
|
||||
|
||||
<form id="global-caddy-form">
|
||||
<fieldset>
|
||||
<legend>通用选项</legend>
|
||||
<legend data-i18n="form.legend_general_options">通用选项</legend>
|
||||
<div class="form-grid">
|
||||
<div class="form-group"><label for="admin_port">管理API端口 (只读)</label><input type="text"
|
||||
<div class="form-group"><label for="admin_port" data-i18n="form.admin_api_port_label">管理API端口 (只读)</label><input type="text"
|
||||
id="admin_port" name="admin_port" value=":2019" readonly></div>
|
||||
<div class="form-group"><label for="http_port">HTTP 端口</label><input type="number"
|
||||
<div class="form-group"><label for="http_port" data-i18n="form.http_port_label">HTTP 端口</label><input type="number"
|
||||
id="http_port" name="http_port" min="1" max="65535"></div>
|
||||
<div class="form-group"><label for="https_port">HTTPS 端口</label><input type="number"
|
||||
<div class="form-group"><label for="https_port" data-i18n="form.https_port_label">HTTPS 端口</label><input type="number"
|
||||
id="https_port" name="https_port" min="1" max="65535"></div>
|
||||
</div>
|
||||
<div class="checkbox-grid">
|
||||
<label class="custom-checkbox"><input type="checkbox" id="debug" name="debug"><span
|
||||
class="checkmark"></span> 启用Debug模式</label>
|
||||
class="checkmark"></span> <span data-i18n="form.enable_debug_mode">启用Debug模式</span></label>
|
||||
<label class="custom-checkbox"><input type="checkbox" id="metrics" name="metrics"><span
|
||||
class="checkmark"></span> 启用Prometheus指标</label>
|
||||
class="checkmark"></span> <span data-i18n="form.enable_prometheus_metrics">启用Prometheus指标</span></label>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<fieldset>
|
||||
<legend>主日志配置</legend>
|
||||
<legend data-i18n="form.legend_main_log_config">主日志配置</legend>
|
||||
<div class="form-grid">
|
||||
<div class="form-group"><label>日志级别</label>
|
||||
<div class="form-group"><label data-i18n="form.log_level_label">日志级别</label>
|
||||
<div id="select-log-level" class="custom-select"></div>
|
||||
</div>
|
||||
<div class="form-group"><label for="log_rotate_size">滚动大小</label><input type="text"
|
||||
id="log_rotate_size" name="log_rotate_size" placeholder="例如: 10MB"></div>
|
||||
<div class="form-group"><label for="log_rotate_keep">保留文件数</label><input type="text"
|
||||
id="log_rotate_keep" name="log_rotate_keep" placeholder="例如: 10"></div>
|
||||
<div class="form-group"><label for="log_rotate_keep_for_time">保留时间</label><input
|
||||
<div class="form-group"><label for="log_rotate_size" data-i18n="form.log_rotate_size_label">滚动大小</label><input type="text"
|
||||
id="log_rotate_size" name="log_rotate_size" placeholder="例如: 10MB"
|
||||
data-i18n-placeholder="form.log_rotate_size_placeholder"></div>
|
||||
<div class="form-group"><label for="log_rotate_keep" data-i18n="form.log_rotate_keep_label">保留文件数</label><input type="text"
|
||||
id="log_rotate_keep" name="log_rotate_keep" placeholder="例如: 10"
|
||||
data-i18n-placeholder="form.log_rotate_keep_placeholder"></div>
|
||||
<div class="form-group"><label for="log_rotate_keep_for_time" data-i18n="form.log_rotate_keep_for_time_label">保留时间</label><input
|
||||
type="text" id="log_rotate_keep_for_time" name="log_rotate_keep_for_time"
|
||||
placeholder="例如: 24h"></div>
|
||||
placeholder="例如: 24h" data-i18n-placeholder="form.log_rotate_keep_for_time_placeholder"></div>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<fieldset>
|
||||
<legend>全局TLS配置 (ACME)</legend>
|
||||
<legend data-i18n="form.legend_global_tls_config">全局TLS配置 (ACME)</legend>
|
||||
<label class="custom-checkbox"><input type="checkbox" id="enable_dns_challenge"
|
||||
name="enable_dns_challenge"><span class="checkmark"></span> 启用全局 DNS
|
||||
Challenge</label>
|
||||
name="enable_dns_challenge"><span class="checkmark"></span> <span data-i18n="form.enable_global_dns_challenge">启用全局 DNS
|
||||
Challenge</span></label>
|
||||
<div id="global-tls-config-group" class="hidden" style="margin-top:16px;">
|
||||
<div class="form-grid">
|
||||
<div class="form-group"><label>DNS 提供商</label>
|
||||
<div class="form-group"><label data-i18n="form.dns_provider_label">DNS 提供商</label>
|
||||
<div id="select-tls-provider" class="custom-select"></div>
|
||||
</div>
|
||||
<div class="form-group"><label for="tls_email">ACME 邮箱</label><input type="email"
|
||||
id="tls_email" name="tls_email" placeholder="用于证书申请和续订通知"></div>
|
||||
<div class="form-group"><label for="tls_token">API Token (或等效凭证)</label><input
|
||||
<div class="form-group"><label for="tls_email" data-i18n="form.acme_email_label">ACME 邮箱</label><input type="email"
|
||||
id="tls_email" name="tls_email" placeholder="用于证书申请和续订通知"
|
||||
data-i18n-placeholder="form.acme_email_placeholder"></div>
|
||||
<div class="form-group"><label for="tls_token" data-i18n="form.api_token_label">API Token (或等效凭证)</label><input
|
||||
type="password" id="tls_token" name="tls_token" autocomplete="new-password">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<!-- 新增: ECH 配置区域 -->
|
||||
<fieldset>
|
||||
<legend>加密客户端问候 (ECH)</legend>
|
||||
<legend data-i18n="form.legend_ech_config">加密客户端问候 (ECH)</legend>
|
||||
<label class="custom-checkbox"><input type="checkbox" id="enable_ech"
|
||||
name="enable_ech"><span class="checkmark"></span> 启用 ECH (实验性功能)</label>
|
||||
name="enable_ech"><span class="checkmark"></span> <span data-i18n="form.enable_ech_experimental">启用 ECH (实验性功能)</span></label>
|
||||
<div id="ech-config-group" class="hidden" style="margin-top:16px;">
|
||||
<div class="form-group">
|
||||
<label for="tls_ech_sni">ECH Outer SNI</label>
|
||||
<label for="tls_ech_sni" data-i18n="form.ech_outer_sni_label">ECH Outer SNI</label>
|
||||
<input type="text" id="tls_ech_sni" name="tls_ech_sni"
|
||||
placeholder="例如: ech.example.com">
|
||||
placeholder="例如: ech.example.com" data-i18n-placeholder="form.ech_outer_sni_placeholder">
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<div class="form-actions">
|
||||
<button type="submit" class="btn btn-primary"><i class="fa-solid fa-save"></i>
|
||||
保存并重载</button>
|
||||
<span data-i18n="pages.global.save_btn">保存并重载</span></button>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
|
|
@ -133,6 +135,7 @@
|
|||
|
||||
<div id="toast-container" class="toast-container"></div>
|
||||
<div id="dialog-container"></div>
|
||||
<div id="modal-container"></div>
|
||||
|
||||
<script type="module" src="js/common.js"></script>
|
||||
<script type="module" src="js/global.js"></script>
|
||||
|
|
|
|||
|
|
@ -1,15 +1,17 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>CaddyDash</title>
|
||||
<title data-i18n="pages.configs.page_title">CaddyDash</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
|
||||
<link rel="stylesheet" href="css/style.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="app-container">
|
||||
<aside class="sidebar" id="sidebar">
|
||||
|
|
@ -19,26 +21,35 @@
|
|||
</header>
|
||||
<nav class="sidebar-nav">
|
||||
<ul>
|
||||
<li><a href="/" data-nav-id="configs" class="active"><i class="fa-solid fa-sitemap"></i> <span>站点配置</span></a></li>
|
||||
<li><a href="/global.html" data-nav-id="global"><i class="fa-solid fa-globe"></i> <span>全局配置</span></a></li>
|
||||
<li><a href="/settings.html" data-nav-id="settings"><i class="fa-solid fa-gears"></i> <span>面板设置</span></a></li>
|
||||
<li><a href="/" data-nav-id="configs" class="active"><i class="fa-solid fa-sitemap"></i> <span
|
||||
data-i18n="nav.configs">站点配置</span></a></li>
|
||||
<li><a href="/global.html" data-nav-id="global"><i class="fa-solid fa-globe"></i> <span
|
||||
data-i18n="nav.global">全局配置</span></a></li>
|
||||
<li><a href="/settings.html" data-nav-id="settings"><i class="fa-solid fa-gears"></i> <span
|
||||
data-i18n="nav.settings">面板设置</span></a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
<div class="sidebar-bottom">
|
||||
<div class="theme-switcher"><i class="fa-solid fa-sun"></i><label class="switch"><input type="checkbox" id="theme-toggle-input"><span class="slider"></span></label><i class="fa-solid fa-moon"></i></div>
|
||||
<div class="theme-switcher"><i class="fa-solid fa-sun"></i><label class="switch"><input type="checkbox"
|
||||
id="theme-toggle-input"><span class="slider"></span></label><i class="fa-solid fa-moon"></i>
|
||||
</div>
|
||||
<div class="caddy-control-panel">
|
||||
<div id="caddy-status-indicator" class="caddy-status"><span class="status-dot checking"></span><span class="status-text">检查中...</span></div>
|
||||
<div id="caddy-status-indicator" class="caddy-status"><span class="status-dot checking"></span><span
|
||||
class="status-text" data-i18n="status.checking">检查中...</span></div>
|
||||
<div id="caddy-action-button-container"></div>
|
||||
</div>
|
||||
<div class="logout-section"><button id="logout-btn" class="btn btn-secondary"><i class="fa-solid fa-right-from-bracket"></i><span>退出登录</span></button></div>
|
||||
<div class="logout-section"><button id="logout-btn" class="btn btn-secondary"><i
|
||||
class="fa-solid fa-right-from-bracket"></i><span
|
||||
data-i18n="nav.logout">退出登录</span></button></div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<main class="main-content">
|
||||
<header class="main-header">
|
||||
<button class="btn-icon" id="menu-toggle-btn"><i class="fa-solid fa-bars"></i></button>
|
||||
<h2>配置管理</h2>
|
||||
<button id="add-new-config-btn" class="btn btn-primary"><i class="fa-solid fa-plus"></i> <span class="btn-text">创建新配置</span></button>
|
||||
<h2 data-i18n="pages.configs.title">站点配置管理</h2>
|
||||
<button id="add-new-config-btn" class="btn btn-primary"><i class="fa-solid fa-plus"></i> <span
|
||||
class="btn-text" data-i18n="pages.configs.add_new_btn">创建新配置</span></button>
|
||||
</header>
|
||||
|
||||
<div id="view-container">
|
||||
|
|
@ -47,104 +58,123 @@
|
|||
</section>
|
||||
<section id="config-form-panel" class="card-panel view hidden">
|
||||
<div class="form-panel-header">
|
||||
<button id="back-to-list-btn" class="btn-icon" title="返回列表"><i class="fa-solid fa-arrow-left"></i></button>
|
||||
<h3 id="form-title">创建新配置</h3>
|
||||
<button id="back-to-list-btn" class="btn-icon" title="返回列表"
|
||||
data-i18n-title="common.back_to_list"><i class="fa-solid fa-arrow-left"></i></button>
|
||||
<h3 id="form-title" data-i18n="pages.configs.form_title_create">创建新配置</h3>
|
||||
</div>
|
||||
<form id="config-form">
|
||||
<input type="hidden" id="original-filename" value="">
|
||||
|
||||
<fieldset>
|
||||
<legend>基础配置</legend>
|
||||
<legend data-i18n="form.legend_basic">基础配置</legend>
|
||||
<div class="form-group">
|
||||
<label for="domain">主域名 (将用作文件名)</label>
|
||||
<label for="domain" data-i18n="form.domain_label">主域名 (将用作文件名)</label>
|
||||
<input type="text" id="domain" name="domain" required>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<fieldset>
|
||||
<legend>服务模式</legend>
|
||||
<legend data-i18n="form.legend_service_mode">服务模式</legend>
|
||||
<div id="service-mode-control" class="segmented-control">
|
||||
<div id="segmented-control-slider"></div>
|
||||
<button type="button" data-mode="none" class="active">无</button>
|
||||
<button type="button" data-mode="reverse_proxy">反向代理</button>
|
||||
<button type="button" data-mode="file_server">文件服务</button>
|
||||
<button type="button" data-mode="none" class="active"
|
||||
data-i18n="form.mode_none">无</button>
|
||||
<button type="button" data-mode="reverse_proxy"
|
||||
data-i18n="form.mode_rp">反向代理</button>
|
||||
<button type="button" data-mode="file_server"
|
||||
data-i18n="form.mode_fs">文件服务</button>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<fieldset id="upstream-fieldset" class="hidden">
|
||||
<legend>反向代理配置</legend>
|
||||
<legend data-i18n="form.legend_rp">反向代理配置</legend>
|
||||
<div class="form-group" id="single-upstream-group">
|
||||
<label for="upstream">上游服务地址</label>
|
||||
<input type="text" id="upstream" name="upstream" placeholder="例如: 127.0.0.1:8080">
|
||||
<label for="upstream" data-i18n="form.upstream_addr_label">上游服务地址</label>
|
||||
<input type="text" id="upstream" name="upstream" placeholder="例如: 127.0.0.1:8080"
|
||||
data-i18n-placeholder="form.upstream_addr_placeholder">
|
||||
</div>
|
||||
<div class="sub-fieldset">
|
||||
<label class="custom-checkbox">
|
||||
<input type="checkbox" id="muti_upstream" name="muti_upstream">
|
||||
<span class="checkmark"></span> 启用多上游负载均衡
|
||||
<span class="checkmark"></span> <span
|
||||
data-i18n="form.enable_multi_upstream">启用多上游负载均衡</span>
|
||||
</label>
|
||||
<div id="multi-upstream-group" class="hidden" style="margin-top: 16px;">
|
||||
<p class="sub-legend">上游服务器列表</p>
|
||||
<p class="sub-legend" data-i18n="form.upstream_servers_label">上游服务器列表</p>
|
||||
<div id="multi-upstream-container"></div>
|
||||
<button type="button" id="add-multi-upstream-btn" class="btn btn-secondary btn-small"><i class="fa-solid fa-plus"></i> 添加上游服务器</button>
|
||||
<button type="button" id="add-multi-upstream-btn"
|
||||
class="btn btn-secondary btn-small"><i class="fa-solid fa-plus"></i> <span
|
||||
data-i18n="form.add_upstream_server_btn">添加上游服务器</span></button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sub-fieldset">
|
||||
<div class="sub-legend-group">
|
||||
<p class="sub-legend">上游请求头 (Upstream Headers)</p>
|
||||
<p class="sub-legend" data-i18n="form.upstream_headers_label">上游请求头 (Upstream
|
||||
Headers)</p>
|
||||
<button type="button" class="btn-link" data-preset-target="upstream">
|
||||
<i class="fa-solid fa-wand-magic-sparkles"></i>
|
||||
<span>从预设填充</span>
|
||||
<span data-i18n="form.fill_from_preset">从预设填充</span>
|
||||
</button>
|
||||
</div>
|
||||
<div id="upstream-headers-container"></div>
|
||||
<button type="button" class="btn btn-secondary btn-small" data-add-target="upstream">
|
||||
<i class="fa-solid fa-plus"></i> 添加请求头
|
||||
<i class="fa-solid fa-plus"></i> <span data-i18n="form.add_header_btn">添加请求头</span>
|
||||
</button>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<fieldset id="fileserver-fieldset" class="hidden">
|
||||
<legend>文件服务配置</legend>
|
||||
<legend data-i18n="form.legend_fs">文件服务配置</legend>
|
||||
<div class="form-group">
|
||||
<label for="file_dir_path">根目录路径</label>
|
||||
<input type="text" id="file_dir_path" name="file_dir_path" placeholder="例如: /srv/www">
|
||||
<label for="file_dir_path" data-i18n="form.fs_root_label">根目录路径</label>
|
||||
<input type="text" id="file_dir_path" name="file_dir_path" placeholder="例如: /srv/www"
|
||||
data-i18n-placeholder="form.fs_root_placeholder">
|
||||
</div>
|
||||
<label class="custom-checkbox">
|
||||
<input type="checkbox" id="enable_browser" name="enable_browser">
|
||||
<span class="checkmark"></span> 启用文件浏览器
|
||||
<span class="checkmark"></span> <span
|
||||
data-i18n="form.enable_fs_browser">启用文件浏览器</span>
|
||||
</label>
|
||||
</fieldset>
|
||||
|
||||
<fieldset id="headers-fieldset">
|
||||
<div class="sub-legend-group">
|
||||
<legend>全局请求头 (Headers)</legend>
|
||||
<legend data-i18n="form.legend_global_headers">全局请求头 (Headers)</legend>
|
||||
<button type="button" class="btn-link" data-preset-target="global">
|
||||
<i class="fa-solid fa-wand-magic-sparkles"></i>
|
||||
<span>从预设填充</span>
|
||||
<span data-i18n="form.fill_from_preset">从预设填充</span>
|
||||
</button>
|
||||
</div>
|
||||
<div id="headers-container"></div>
|
||||
<button type="button" class="btn btn-secondary btn-small" data-add-target="global">
|
||||
<i class="fa-solid fa-plus"></i> 添加请求头
|
||||
<i class="fa-solid fa-plus"></i> <span data-i18n="form.add_header_btn">添加请求头</span>
|
||||
</button>
|
||||
</fieldset>
|
||||
|
||||
<fieldset>
|
||||
<legend>附加功能</legend>
|
||||
<legend data-i18n="form.legend_features">附加功能</legend>
|
||||
<div class="checkbox-grid">
|
||||
<label class="custom-checkbox"><input type="checkbox" id="enable_log" name="enable_log"><span class="checkmark"></span> 启用日志</label>
|
||||
<label class="custom-checkbox"><input type="checkbox" id="enable_error_page" name="enable_error_page"><span class="checkmark"></span> 启用错误页</label>
|
||||
<label class="custom-checkbox"><input type="checkbox" id="enable_encode" name="enable_encode"><span class="checkmark"></span> 启用压缩</label>
|
||||
<label class="custom-checkbox"><input type="checkbox" id="enable_log"
|
||||
name="enable_log"><span class="checkmark"></span> <span
|
||||
data-i18n="form.feature_log">启用日志</span></label>
|
||||
<label class="custom-checkbox"><input type="checkbox" id="enable_error_page"
|
||||
name="enable_error_page"><span class="checkmark"></span> <span
|
||||
data-i18n="form.feature_error_page">启用错误页</span></label>
|
||||
<label class="custom-checkbox"><input type="checkbox" id="enable_encode"
|
||||
name="enable_encode"><span class="checkmark"></span> <span
|
||||
data-i18n="form.feature_encode">启用压缩</span></label>
|
||||
</div>
|
||||
</fieldset>
|
||||
<div class="form-actions">
|
||||
<button type="submit" class="btn btn-primary"><i class="fa-solid fa-save"></i> <span>保存配置</span></button>
|
||||
<button type="button" id="cancel-edit-btn" class="btn btn-secondary">取消</button>
|
||||
<button type="submit" class="btn btn-primary"><i class="fa-solid fa-save"></i> <span
|
||||
data-i18n="common.save">保存配置</span></button>
|
||||
<button type="button" id="cancel-edit-btn" class="btn btn-secondary"
|
||||
data-i18n="common.cancel">取消</button>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
<section id="rendered-output-panel" class="card-panel view hidden">
|
||||
<h3>渲染后的 Caddyfile</h3>
|
||||
<h3 data-i18n="pages.configs.rendered_caddyfile_title">渲染后的 Caddyfile</h3>
|
||||
<pre><code id="rendered-content"></code></pre>
|
||||
</section>
|
||||
</div>
|
||||
|
|
@ -153,8 +183,9 @@
|
|||
|
||||
<div id="toast-container" class="toast-container"></div>
|
||||
<div id="dialog-container"></div>
|
||||
<div id="modal-container"></div> <!-- 新增: 通用模态框容器 -->
|
||||
<div id="modal-container"></div>
|
||||
|
||||
<script type="module" src="js/app.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
|
@ -1,56 +1,52 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>首次设置 - CaddyDash</title>
|
||||
<title data-i18n="pages.init.page_title">首次设置 - CaddyDash</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
|
||||
<link rel="stylesheet" href="css/style.css">
|
||||
<style>
|
||||
#init-form .form-group {
|
||||
text-align: left;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body class="login-page-body">
|
||||
|
||||
<div class="login-container">
|
||||
<header class="login-header">
|
||||
<i class="fa-solid fa-magic-wand-sparkles"></i>
|
||||
<h1>欢迎使用 CaddyDash</h1>
|
||||
<p>请创建您的管理员账户以完成首次设置</p>
|
||||
<h1 data-i18n="pages.init.title">欢迎使用 CaddyDash</h1>
|
||||
<p data-i18n="pages.init.prompt">请创建您的管理员账户以完成首次设置</p>
|
||||
</header>
|
||||
|
||||
<form id="init-form">
|
||||
<div class="form-group">
|
||||
<label for="username">管理员用户名</label>
|
||||
<input type="text" id="username" name="username" autocomplete="username">
|
||||
<label for="username" data-i18n="pages.init.admin_user_label">管理员用户名</label>
|
||||
<input type="text" id="username" name="username" required autocomplete="username">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="password">密码 (至少8位)</label>
|
||||
<input type="password" id="password" name="password" autocomplete="new-password">
|
||||
<label for="password" data-i18n="pages.init.password_label">密码 (至少8位)</label>
|
||||
<input type="password" id="password" name="password" required minlength="8" autocomplete="new-password">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="confirm_password">确认密码</label>
|
||||
<input type="password" id="confirm_password" name="confirm_password" autocomplete="new-password">
|
||||
<label for="confirm_password" data-i18n="pages.init.confirm_password_label">确认密码</label>
|
||||
<input type="password" id="confirm_password" name="confirm_password" required minlength="8" autocomplete="new-password">
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary btn-login">
|
||||
<span>完成设置</span>
|
||||
<span data-i18n="pages.init.setup_btn">完成设置</span>
|
||||
<i class="fa-solid fa-check"></i>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div id="toast-container" class="toast-container"></div>
|
||||
<div class="language-switcher-corner">
|
||||
<button id="lang-switcher-btn" class="btn-icon" data-i18n-title="common.switch_language" title="切换语言">
|
||||
<i class="fa-solid fa-language"></i>
|
||||
</button>
|
||||
<ul id="lang-options-list" class="hidden"></ul>
|
||||
</div>
|
||||
|
||||
<div id="toast-container" class="toast-container"></div>
|
||||
<script type="module" src="js/init.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
|
@ -2,13 +2,12 @@
|
|||
|
||||
import { state } from './state.js';
|
||||
import { api } from './api.js';
|
||||
import { theme, activateNav } from './common.js';
|
||||
import { initializePage } from './common.js';
|
||||
import { t } from './locale.js';
|
||||
import { notification } from './notifications.js';
|
||||
import { initCaddyStatus } from './caddy.js';
|
||||
import { DOMElements, switchView, renderConfigList, addKeyValueInput, addSingleInput, fillForm, showRenderedConfig, updateCaddyStatusView, updateSegmentedControl, updateServiceModeView, updateMultiUpstreamView, createPresetSelectionModal } from './ui.js';
|
||||
|
||||
const POLLING_INTERVAL = 5000;
|
||||
let caddyStatusInterval;
|
||||
// --- 事件处理与逻辑流 (所有 handle* 函数保持不变) ---
|
||||
|
||||
function getFormStateAsString() {
|
||||
const formData = new FormData(DOMElements.configForm);
|
||||
|
|
@ -27,14 +26,18 @@ function getFormStateAsString() {
|
|||
}
|
||||
|
||||
async function attemptExitForm() {
|
||||
if (getFormStateAsString() !== state.initialFormState) {
|
||||
if (await notification.confirm('您有未保存的更改。确定要放弃吗?')) switchView(DOMElements.configListPanel);
|
||||
} else switchView(DOMElements.configListPanel);
|
||||
if (await getFormStateAsString() !== state.initialFormState) {
|
||||
if (await notification.confirm(t('dialogs.unsaved_changes_msg'), t('dialogs.unsaved_changes_title'))) {
|
||||
switchView(DOMElements.configListPanel);
|
||||
}
|
||||
} else {
|
||||
switchView(DOMElements.configListPanel);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleLogout() {
|
||||
if (!await notification.confirm('您确定要退出登录吗?')) return;
|
||||
notification.toast('正在退出...', 'info');
|
||||
if (!await notification.confirm(t('dialogs.logout_msg'))) return;
|
||||
notification.toast(t('toasts.logout_processing'), 'info');
|
||||
setTimeout(() => { window.location.href = `/v0/api/auth/logout`; }, 500);
|
||||
}
|
||||
|
||||
|
|
@ -42,7 +45,7 @@ async function loadAllConfigs() {
|
|||
try {
|
||||
const filenames = await api.get('/config/filenames');
|
||||
renderConfigList(filenames);
|
||||
} catch (error) { if (error.message) notification.toast(`加载配置列表失败: ${error.message}`, 'error'); }
|
||||
} catch (error) { if (error.message) notification.toast(t('toasts.load_configs_error', { error: error.message }), 'error'); }
|
||||
}
|
||||
|
||||
async function handleEditConfig(originalFilename) {
|
||||
|
|
@ -53,22 +56,22 @@ async function handleEditConfig(originalFilename) {
|
|||
]);
|
||||
state.isEditing = true;
|
||||
switchView(DOMElements.configFormPanel);
|
||||
DOMElements.formTitle.textContent = '编辑配置';
|
||||
DOMElements.formTitle.textContent = t('pages.configs.form_title_edit');
|
||||
fillForm(config, originalFilename);
|
||||
showRenderedConfig(rendered, originalFilename);
|
||||
const mode = config.upstream_config?.enable_upstream ? 'reverse_proxy' : (config.file_server_config?.enable_file_server ? 'file_server' : 'none');
|
||||
updateServiceModeView(mode);
|
||||
state.initialFormState = getFormStateAsString();
|
||||
} catch (error) { notification.toast(`加载配置详情失败: ${error.message}`, 'error'); }
|
||||
state.initialFormState = await getFormStateAsString();
|
||||
} catch (error) { notification.toast(t('toasts.load_config_detail_error', { error: error.message }), 'error'); }
|
||||
}
|
||||
|
||||
async function handleDeleteConfig(filename) {
|
||||
if (!await notification.confirm(`确定要删除配置 "${filename}" 吗?`)) return;
|
||||
if (!await notification.confirm(t('dialogs.delete_config_msg', { filename }), t('dialogs.delete_config_title'))) return;
|
||||
try {
|
||||
await api.delete(`/config/file/${filename}`);
|
||||
notification.toast('配置已成功删除。', 'success');
|
||||
notification.toast(t('toasts.delete_success'), 'success');
|
||||
loadAllConfigs();
|
||||
} catch (error) { notification.toast(`删除失败: ${error.message}`, 'error'); }
|
||||
} catch (error) { notification.toast(t('toasts.delete_error', { error: error.message }), 'error'); }
|
||||
}
|
||||
|
||||
async function handleSaveConfig(e) {
|
||||
|
|
@ -76,7 +79,7 @@ async function handleSaveConfig(e) {
|
|||
const formData = new FormData(DOMElements.configForm);
|
||||
const domain = formData.get('domain');
|
||||
if (!domain) {
|
||||
notification.toast('域名不能为空。', 'error');
|
||||
notification.toast(t('toasts.error_domain_empty'), 'error');
|
||||
return;
|
||||
}
|
||||
const getHeadersMap = (keyName, valueName) => {
|
||||
|
|
@ -116,23 +119,23 @@ async function handleSaveConfig(e) {
|
|||
try {
|
||||
const result = await api.put(`/config/file/${domain}`, configData);
|
||||
state.isEditing = false;
|
||||
notification.toast(result.message || '配置已成功保存。', 'success');
|
||||
notification.toast(result.message || t('toasts.save_success'), 'success');
|
||||
setTimeout(() => {
|
||||
switchView(DOMElements.configListPanel);
|
||||
loadAllConfigs();
|
||||
}, 500);
|
||||
} catch (error) { notification.toast(`保存失败: ${error.message}`, 'error'); }
|
||||
} catch (error) { notification.toast(t('toasts.save_error', { error: error.message }), 'error'); }
|
||||
}
|
||||
|
||||
async function openPresetModal(targetType) {
|
||||
const applicablePresets = state.headerPresets.filter(p => p.target === targetType || p.target === 'any');
|
||||
if (applicablePresets.length === 0) {
|
||||
notification.toast(`没有适用于此区域的预设。`, 'info');
|
||||
notification.toast(t('toasts.no_presets_available'), 'info');
|
||||
return;
|
||||
}
|
||||
|
||||
const presetId = await createPresetSelectionModal(applicablePresets);
|
||||
if (!presetId) return; // 用户取消了选择
|
||||
const presetId = await createPresetSelectionModal(applicablePresets, t);
|
||||
if (!presetId) return;
|
||||
|
||||
let targetContainer, keyName, valueName;
|
||||
if (targetType === 'global') {
|
||||
|
|
@ -147,18 +150,17 @@ async function openPresetModal(targetType) {
|
|||
return;
|
||||
}
|
||||
|
||||
// 在获取到presetId后,再执行填充逻辑
|
||||
try {
|
||||
notification.toast('正在加载预设...', 'info', 1000);
|
||||
notification.toast(t('toasts.loading_preset'), 'info', 1000);
|
||||
const preset = await api.get(`/config/headers-presets/${presetId}`);
|
||||
if (!preset.headers) {
|
||||
notification.toast('此预设无数据。', 'info');
|
||||
notification.toast(t('toasts.preset_no_data'), 'info');
|
||||
return;
|
||||
}
|
||||
|
||||
const choice = await notification.confirm('如何填充预设?', '选择填充方式', { confirmText: '追加', cancelText: '替换' });
|
||||
const choice = await notification.confirm(t('dialogs.preset_fill_msg'), t('dialogs.preset_fill_title'), { confirmText: t('common.append'), cancelText: t('common.replace') });
|
||||
|
||||
if (choice === false) { // 用户选择了“替换”
|
||||
if (choice === false) {
|
||||
targetContainer.innerHTML = '';
|
||||
}
|
||||
|
||||
|
|
@ -167,19 +169,16 @@ async function openPresetModal(targetType) {
|
|||
addKeyValueInput(targetContainer, keyName, valueName, key, value);
|
||||
});
|
||||
});
|
||||
notification.toast(`已成功填充预设 "${preset.name}"`, 'success');
|
||||
notification.toast(t('toasts.preset_fill_success', { presetName: preset.name }), 'success');
|
||||
|
||||
} catch (error) {
|
||||
notification.toast(`加载预设失败: ${error.message}`, 'error');
|
||||
notification.toast(t('toasts.load_preset_error', { error: error.message }), 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function init() {
|
||||
theme.init(DOMElements.themeToggleInput);
|
||||
notification.init(DOMElements.toastContainer, DOMElements.dialogContainer, DOMElements.modalContainer);
|
||||
activateNav('configs');
|
||||
initCaddyStatus();
|
||||
|
||||
// --- 初始化与事件绑定 ---
|
||||
function pageInit() {
|
||||
// 这个函数包含所有特定于 app.js 的初始化逻辑
|
||||
loadAllConfigs();
|
||||
|
||||
api.get('/config/headers-presets')
|
||||
|
|
@ -187,16 +186,13 @@ function init() {
|
|||
state.headerPresets = presets || [];
|
||||
})
|
||||
.catch(err => {
|
||||
if (err.message) notification.toast(`加载Header预设失败: ${err.message}`, 'error');
|
||||
if (err.message) notification.toast(t('toasts.load_presets_error', { error: err.message }), 'error');
|
||||
});
|
||||
|
||||
DOMElements.menuToggleBtn.addEventListener('click', () => DOMElements.sidebar.classList.toggle('is-open'));
|
||||
DOMElements.mainContent.addEventListener('click', () => DOMElements.sidebar.classList.remove('is-open'));
|
||||
|
||||
DOMElements.addNewConfigBtn.addEventListener('click', () => {
|
||||
DOMElements.addNewConfigBtn.addEventListener('click', async () => {
|
||||
state.isEditing = false;
|
||||
switchView(DOMElements.configFormPanel);
|
||||
DOMElements.formTitle.textContent = '创建新配置';
|
||||
DOMElements.formTitle.textContent = t('pages.configs.form_title_create');
|
||||
DOMElements.configForm.reset();
|
||||
|
||||
const noneButton = DOMElements.serviceModeControl.querySelector('[data-mode="none"]');
|
||||
|
|
@ -204,7 +200,7 @@ function init() {
|
|||
updateServiceModeView('none');
|
||||
updateMultiUpstreamView(false);
|
||||
|
||||
state.initialFormState = getFormStateAsString();
|
||||
state.initialFormState = await getFormStateAsString();
|
||||
DOMElements.headersContainer.innerHTML = '';
|
||||
DOMElements.upstreamHeadersContainer.innerHTML = '';
|
||||
DOMElements.multiUpstreamContainer.innerHTML = '';
|
||||
|
|
@ -247,7 +243,7 @@ function init() {
|
|||
|
||||
DOMElements.addMultiUpstreamBtn.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
addSingleInput(DOMElements.multiUpstreamContainer, 'upstream_servers', '例如: 127.0.0.1:8081');
|
||||
addSingleInput(DOMElements.multiUpstreamContainer, 'upstream_servers', t('form.upstream_server_placeholder'));
|
||||
});
|
||||
|
||||
DOMElements.serviceModeControl.addEventListener('click', (e) => {
|
||||
|
|
@ -265,4 +261,5 @@ function init() {
|
|||
});
|
||||
}
|
||||
|
||||
init();
|
||||
// 使用通用初始化函数启动页面
|
||||
initializePage({ pageId: 'configs', pageInit: pageInit });
|
||||
|
|
@ -6,6 +6,9 @@ import { notification } from './notifications.js';
|
|||
let caddyStatusInterval;
|
||||
const POLLING_INTERVAL = 5000;
|
||||
|
||||
// 将 t 函数保存在模块作用域内
|
||||
let translate;
|
||||
|
||||
const DOMElements = {
|
||||
caddyStatusIndicator: document.getElementById('caddy-status-indicator'),
|
||||
caddyActionButtonContainer: document.getElementById('caddy-action-button-container'),
|
||||
|
|
@ -24,23 +27,31 @@ function updateCaddyStatusView(status) {
|
|||
const text = DOMElements.caddyStatusIndicator.querySelector('.status-text');
|
||||
const buttonContainer = DOMElements.caddyActionButtonContainer;
|
||||
|
||||
if(!dot || !text || !buttonContainer) return; // 如果元素不存在,则不执行
|
||||
if(!dot || !text || !buttonContainer) return;
|
||||
|
||||
dot.className = 'status-dot';
|
||||
buttonContainer.innerHTML = '';
|
||||
let statusText, dotClass;
|
||||
switch (status) {
|
||||
case 'running':
|
||||
statusText = '运行中'; dotClass = 'running';
|
||||
buttonContainer.appendChild(createButton('重载配置', 'btn-warning', handleReloadCaddy));
|
||||
buttonContainer.appendChild(createButton('停止 Caddy', 'btn-danger', handleStopCaddy));
|
||||
statusText = translate('status.running');
|
||||
dotClass = 'running';
|
||||
buttonContainer.appendChild(createButton(translate('caddy.reload_btn'), 'btn-warning', handleReloadCaddy));
|
||||
buttonContainer.appendChild(createButton(translate('caddy.stop_btn'), 'btn-danger', handleStopCaddy));
|
||||
break;
|
||||
case 'stopped':
|
||||
statusText = '已停止'; dotClass = 'stopped';
|
||||
buttonContainer.appendChild(createButton('启动 Caddy', 'btn-success', handleStartCaddy));
|
||||
statusText = translate('status.stopped');
|
||||
dotClass = 'stopped';
|
||||
buttonContainer.appendChild(createButton(translate('caddy.start_btn'), 'btn-success', handleStartCaddy));
|
||||
break;
|
||||
case 'checking':
|
||||
statusText = translate('status.checking');
|
||||
dotClass = 'checking';
|
||||
break;
|
||||
default:
|
||||
statusText = translate('status.unknown');
|
||||
dotClass = 'error';
|
||||
break;
|
||||
case 'checking': statusText = '检查中...'; dotClass = 'checking'; break;
|
||||
default: statusText = '状态未知'; dotClass = 'error'; break;
|
||||
}
|
||||
text.textContent = statusText;
|
||||
dot.classList.add(dotClass);
|
||||
|
|
@ -59,35 +70,38 @@ async function checkCaddyStatus() {
|
|||
async function handleStartCaddy() {
|
||||
try {
|
||||
const result = await api.post('/caddy/run');
|
||||
notification.toast(result.message || '启动命令已发送。', 'success');
|
||||
notification.toast(result.message || translate('toasts.start_cmd_sent'), 'success');
|
||||
setTimeout(checkCaddyStatus, 500);
|
||||
} catch (error) { notification.toast(`启动失败: ${error.message}`, 'error'); }
|
||||
} catch (error) { notification.toast(translate('toasts.start_error', { error: error.message }), 'error'); }
|
||||
}
|
||||
|
||||
async function handleStopCaddy() {
|
||||
if (!await notification.confirm('您确定要停止 Caddy 实例吗?')) return;
|
||||
if (!await notification.confirm(translate('dialogs.stop_caddy_msg'))) return;
|
||||
try {
|
||||
const result = await api.post('/caddy/stop');
|
||||
notification.toast(result.message || '停止命令已发送。', 'info');
|
||||
notification.toast(result.message || translate('toasts.stop_cmd_sent'), 'info');
|
||||
setTimeout(checkCaddyStatus, 500);
|
||||
} catch(error) { notification.toast(`操作失败: ${error.message}`, 'error'); }
|
||||
} catch(error) { notification.toast(translate('toasts.action_error', { error: error.message }), 'error'); }
|
||||
}
|
||||
|
||||
async function handleReloadCaddy() {
|
||||
if (!await notification.confirm('确定要重载 Caddy 配置吗?')) return;
|
||||
if (!await notification.confirm(translate('dialogs.reload_caddy_msg'))) return;
|
||||
try {
|
||||
const result = await api.post('/caddy/restart');
|
||||
notification.toast(result.message || '重载命令已发送。', 'success');
|
||||
notification.toast(result.message || translate('toasts.reload_sent'), 'success');
|
||||
setTimeout(checkCaddyStatus, 500);
|
||||
} catch(error) { notification.toast(`重载失败: ${error.message}`, 'error'); }
|
||||
} catch(error) { notification.toast(translate('toasts.reload_error', { error: error.message }), 'error'); }
|
||||
}
|
||||
|
||||
export function initCaddyStatus() {
|
||||
// 确保通知模块已经初始化
|
||||
// initCaddyStatus 现在接收 t 函数作为参数
|
||||
export function initCaddyStatus(translator) {
|
||||
// 保存翻译函数以供模块内其他函数使用
|
||||
translate = translator;
|
||||
|
||||
const dialogContainer = document.getElementById('dialog-container');
|
||||
const toastContainer = document.getElementById('toast-container');
|
||||
if (dialogContainer && toastContainer) {
|
||||
notification.init(toastContainer, dialogContainer);
|
||||
notification.init(toastContainer, dialogContainer, null, translate); // 将 t 函数传递给通知模块
|
||||
}
|
||||
|
||||
checkCaddyStatus();
|
||||
|
|
|
|||
|
|
@ -1,5 +1,10 @@
|
|||
// js/common.js - 存放共享模块
|
||||
|
||||
import { initI18n, t } from './locale.js';
|
||||
import { notification } from './notifications.js';
|
||||
import { initCaddyStatus } from './caddy.js';
|
||||
import { initUI } from './ui.js';
|
||||
|
||||
const theme = {
|
||||
init: (toggleElement) => {
|
||||
const storedTheme = localStorage.getItem('theme');
|
||||
|
|
@ -55,5 +60,71 @@ function activateNav(pageId) {
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化移动端侧边栏的开关逻辑
|
||||
*/
|
||||
function initSidebar() {
|
||||
const sidebar = document.getElementById('sidebar');
|
||||
const menuToggleBtn = document.getElementById('menu-toggle-btn');
|
||||
|
||||
// 动态创建并管理遮罩层
|
||||
let overlay = document.querySelector('.sidebar-overlay');
|
||||
if (!overlay) {
|
||||
overlay = document.createElement('div');
|
||||
overlay.className = 'sidebar-overlay';
|
||||
document.body.appendChild(overlay);
|
||||
}
|
||||
|
||||
const openSidebar = () => {
|
||||
if (!sidebar || !overlay) return;
|
||||
sidebar.classList.add('is-open');
|
||||
overlay.classList.add('is-visible');
|
||||
};
|
||||
|
||||
const closeSidebar = () => {
|
||||
if (!sidebar || !overlay) return;
|
||||
sidebar.classList.remove('is-open');
|
||||
overlay.classList.remove('is-visible');
|
||||
};
|
||||
|
||||
if (menuToggleBtn) {
|
||||
menuToggleBtn.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
openSidebar();
|
||||
});
|
||||
}
|
||||
|
||||
overlay.addEventListener('click', closeSidebar);
|
||||
}
|
||||
|
||||
/**
|
||||
* 通用的页面初始化函数
|
||||
* @param {object} options - 初始化选项
|
||||
* @param {string} options.pageId - 当前页面的ID, 用于导航高亮
|
||||
* @param {function} [options.pageInit=null] - 特定于该页面的额外初始化逻辑
|
||||
*/
|
||||
export async function initializePage(options) {
|
||||
// 1. 初始化国际化 (最高优先级)
|
||||
await initI18n();
|
||||
|
||||
initUI(t);
|
||||
|
||||
// 2. 初始化UI模块和通用功能
|
||||
theme.init(document.getElementById('theme-toggle-input'));
|
||||
notification.init(
|
||||
document.getElementById('toast-container'),
|
||||
document.getElementById('dialog-container'),
|
||||
document.getElementById('modal-container')
|
||||
);
|
||||
activateNav(options.pageId);
|
||||
initSidebar();
|
||||
initCaddyStatus(t);
|
||||
|
||||
// 3. 如果有特定页面的初始化逻辑, 则执行它
|
||||
if (options.pageInit && typeof options.pageInit === 'function') {
|
||||
options.pageInit();
|
||||
}
|
||||
}
|
||||
|
||||
// 导出模块
|
||||
export { theme, toast, activateNav };
|
||||
|
|
@ -1,25 +1,21 @@
|
|||
// js/global.js - 全局配置页面的逻辑
|
||||
|
||||
import { theme, activateNav } from './common.js';
|
||||
import { initializePage } from './common.js'; // 导入通用初始化函数
|
||||
import { api } from './api.js';
|
||||
import { notification } from './notifications.js';
|
||||
import { initCaddyStatus } from './caddy.js';
|
||||
import { createCustomSelect } from './ui.js';
|
||||
|
||||
const DOMElements = {
|
||||
globalForm: document.getElementById('global-caddy-form'),
|
||||
themeToggleInput: document.getElementById('theme-toggle-input'),
|
||||
logoutBtn: document.getElementById('logout-btn'),
|
||||
toastContainer: document.getElementById('toast-container'),
|
||||
dialogContainer: document.getElementById('dialog-container'),
|
||||
enableDnsChallengeCheckbox: document.getElementById('enable_dns_challenge'),
|
||||
globalTlsConfigGroup: document.getElementById('global-tls-config-group'),
|
||||
enableEchCheckbox: document.getElementById('enable_ech'),
|
||||
echConfigGroup: document.getElementById('ech-config-group'),
|
||||
};
|
||||
const submitButton = DOMElements.globalForm.querySelector('button[type="submit"]');
|
||||
// submitButton 在 pageInit 中获取, 确保DOM已加载
|
||||
let submitButton;
|
||||
|
||||
// 从表单收集数据, 构建成后端需要的JSON结构
|
||||
function getGlobalConfigFromForm() {
|
||||
const formData = new FormData(DOMElements.globalForm);
|
||||
const enableEch = DOMElements.enableEchCheckbox.checked;
|
||||
|
|
@ -49,7 +45,6 @@ function getGlobalConfigFromForm() {
|
|||
};
|
||||
}
|
||||
|
||||
// 用从API获取的数据填充表单
|
||||
function fillGlobalConfigForm(config) {
|
||||
if (!config) return;
|
||||
|
||||
|
|
@ -100,7 +95,6 @@ async function handleSaveGlobalConfig(e) {
|
|||
submitButton.querySelector('span').textContent = "保存中...";
|
||||
|
||||
try {
|
||||
// 修正: 更新API端点路径
|
||||
const result = await api.put('/global/config', configData);
|
||||
notification.toast(result.message || '全局配置已成功保存,Caddy正在重载...', 'success');
|
||||
} catch (error) {
|
||||
|
|
@ -118,23 +112,19 @@ async function handleLogout() {
|
|||
}
|
||||
}
|
||||
|
||||
function init() {
|
||||
theme.init(DOMElements.themeToggleInput);
|
||||
notification.init(DOMElements.toastContainer, DOMElements.dialogContainer);
|
||||
activateNav('global');
|
||||
initCaddyStatus();
|
||||
// 页面特有的初始化逻辑
|
||||
function pageInit() {
|
||||
// 在这里获取 submitButton, 确保 DOM 已加载
|
||||
submitButton = DOMElements.globalForm.querySelector('button[type="submit"]');
|
||||
|
||||
// 修正: 更新API端点路径
|
||||
api.get('/global/log/levels')
|
||||
.then(levels => createCustomSelect('select-log-level', Object.keys(levels)))
|
||||
.catch(err => notification.toast(`加载日志级别失败: ${err.message}`, 'error'));
|
||||
|
||||
// 修正: 更新API端点路径
|
||||
api.get('/global/tls/providers')
|
||||
.then(providers => createCustomSelect('select-tls-provider', Object.keys(providers)))
|
||||
.catch(err => notification.toast(`加载TLS提供商失败: ${err.message}`, 'error'));
|
||||
|
||||
// 修正: 更新API端点路径
|
||||
api.get('/global/config')
|
||||
.then(config => fillGlobalConfigForm(config))
|
||||
.catch(err => notification.toast(`加载全局配置失败: ${err.message}`, 'error'));
|
||||
|
|
@ -150,4 +140,5 @@ function init() {
|
|||
});
|
||||
}
|
||||
|
||||
init();
|
||||
// 使用通用初始化函数启动页面
|
||||
initializePage({ pageId: 'global', pageInit: pageInit });
|
||||
|
|
@ -1,116 +1,156 @@
|
|||
// js/init.js
|
||||
// js/init.js - 初始化页面的独立逻辑
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
|
||||
const DOMElements = {
|
||||
initForm: document.getElementById('init-form'),
|
||||
initButton: null,
|
||||
toastContainer: document.getElementById('toast-container'),
|
||||
usernameInput: document.getElementById('username'),
|
||||
passwordInput: document.getElementById('password'),
|
||||
confirmPasswordInput: document.getElementById('confirm_password'),
|
||||
langSwitcherBtn: document.getElementById('lang-switcher-btn'),
|
||||
langOptionsList: document.getElementById('lang-options-list'), // 从第一个片段引入
|
||||
};
|
||||
|
||||
const initButton = DOMElements.initForm.querySelector('button[type="submit"]');
|
||||
const INIT_API_URL = '/v0/api/auth/init';
|
||||
const PASSWORD_MIN_LENGTH = 8;
|
||||
const TOAST_DEFAULT_DURATION = 3000;
|
||||
const REDIRECT_DELAY_SUCCESS = 1500;
|
||||
|
||||
const theme = {
|
||||
init: () => {
|
||||
const storedTheme = localStorage.getItem('theme');
|
||||
const systemPrefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
const currentTheme = storedTheme || (systemPrefersDark ? 'dark' : 'light');
|
||||
document.documentElement.dataset.theme = currentTheme;
|
||||
const i18n = {
|
||||
currentLocale: {},
|
||||
currentLang: 'en',
|
||||
// 从第一个片段引入, 使用对象更方便显示语言名称
|
||||
supportedLangs: { 'en': 'English', 'zh-CN': '简体中文' },
|
||||
t: function(key, replacements = {}) {
|
||||
const translation = key.split('.').reduce((obj, k) => obj && obj[k], this.currentLocale) || key;
|
||||
let result = translation;
|
||||
if (typeof result === 'string') {
|
||||
for (const placeholder in replacements) {
|
||||
result = result.replace(`{${placeholder}}`, replacements[placeholder]);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
},
|
||||
applyTranslations: function() {
|
||||
// 优化后的翻译应用逻辑, 优先更新span, 其次更新非空文本节点, 最后直接更新元素文本
|
||||
document.querySelectorAll('[data-i18n]').forEach(el => {
|
||||
const key = el.dataset.i18n;
|
||||
const translation = this.t(key);
|
||||
if (translation !== key) { // 仅当找到翻译时才应用
|
||||
const spanChild = el.querySelector('span');
|
||||
if (spanChild) {
|
||||
spanChild.textContent = translation;
|
||||
} else {
|
||||
// 查找直接的、非空文本节点进行替换
|
||||
const textNode = Array.from(el.childNodes).find(node => node.nodeType === Node.TEXT_NODE && node.textContent.trim().length > 0);
|
||||
if (textNode) {
|
||||
textNode.textContent = translation;
|
||||
} else {
|
||||
// 备用方案: 直接设置元素的textContent
|
||||
el.textContent = translation;
|
||||
}
|
||||
};
|
||||
|
||||
const toast = {
|
||||
_container: null,
|
||||
_icons: { success: 'fa-check-circle', error: 'fa-times-circle', info: 'fa-info-circle' },
|
||||
|
||||
init: (containerElement) => {
|
||||
if (!containerElement) {
|
||||
console.error('Toast container element not found.');
|
||||
return;
|
||||
}
|
||||
toast._container = containerElement;
|
||||
toast._container.addEventListener('click', (e) => {
|
||||
if (e.target.dataset.toastClose !== undefined) {
|
||||
toast._hideToast(e.target.closest('.toast'));
|
||||
}
|
||||
});
|
||||
// 从第一个片段引入, 处理data-i18n-title属性
|
||||
document.querySelectorAll('[data-i18n-title]').forEach(el => {
|
||||
el.title = this.t(el.dataset.i18nTitle);
|
||||
});
|
||||
document.title = this.t('pages.init.page_title');
|
||||
},
|
||||
|
||||
show: (message, type = 'info', duration = TOAST_DEFAULT_DURATION) => {
|
||||
if (!toast._container) {
|
||||
console.error('Toast module not initialized. Container is missing.');
|
||||
return;
|
||||
loadLocale: async function(lang) {
|
||||
try {
|
||||
const response = await fetch(`/locales/${lang}.json`);
|
||||
if (!response.ok) throw new Error('File not found');
|
||||
this.currentLocale = await response.json();
|
||||
this.currentLang = lang;
|
||||
document.documentElement.lang = lang; // 设置HTML语言属性
|
||||
localStorage.setItem('appLanguage', lang); // 从第一个片段引入, 保存到localStorage
|
||||
} catch (e) {
|
||||
console.error(`Could not load locale for ${lang}, using fallback.`, e);
|
||||
this.currentLocale = {};
|
||||
}
|
||||
},
|
||||
init: async function() {
|
||||
// 从第一个片段引入, 优先使用保存的语言, 其次使用浏览器语言
|
||||
const savedLang = localStorage.getItem('appLanguage');
|
||||
const browserLang = navigator.language.startsWith('zh') ? 'zh-CN' : 'en';
|
||||
const langToLoad = savedLang || browserLang;
|
||||
await this.loadLocale(langToLoad);
|
||||
this.applyTranslations();
|
||||
this.populateLangOptions(); // 从第一个片段引入, 初始化语言选项列表
|
||||
},
|
||||
// 从第一个片段引入, 用于动态生成语言选项列表
|
||||
populateLangOptions: function() {
|
||||
// 清空现有选项
|
||||
DOMElements.langOptionsList.innerHTML = '';
|
||||
for (const [code, name] of Object.entries(this.supportedLangs)) {
|
||||
const li = document.createElement('li');
|
||||
li.dataset.lang = code;
|
||||
li.textContent = name;
|
||||
if (code === this.currentLang) {
|
||||
li.classList.add('active'); // 标记当前选中语言
|
||||
}
|
||||
DOMElements.langOptionsList.appendChild(li);
|
||||
}
|
||||
}
|
||||
// 移除 i18n.toggleLanguage, 因为有新的语言选择机制
|
||||
};
|
||||
|
||||
const iconClass = toast._icons[type] || 'fa-info-circle';
|
||||
// 从第二个片段完整引入toast对象
|
||||
const toast = {
|
||||
show: function(message, type = 'info', duration = 3000) {
|
||||
if (!DOMElements.toastContainer) return;
|
||||
const icons = { success: 'fa-check-circle', error: 'fa-times-circle', info: 'fa-info-circle' };
|
||||
const toastElement = document.createElement('div');
|
||||
toastElement.className = `toast ${type}`;
|
||||
toastElement.innerHTML = `
|
||||
<i class="toast-icon fa-solid ${iconClass}"></i>
|
||||
<p class="toast-message">${message}</p>
|
||||
<button class="toast-close" data-toast-close>×</button>
|
||||
`;
|
||||
toast._container.appendChild(toastElement);
|
||||
toastElement.innerHTML = `<i class="toast-icon fa-solid ${icons[type]}"></i><p class="toast-message">${message}</p><button class="toast-close" data-toast-close>×</button>`;
|
||||
DOMElements.toastContainer.appendChild(toastElement);
|
||||
requestAnimationFrame(() => toastElement.classList.add('show'));
|
||||
setTimeout(() => toast._hideToast(toastElement), duration);
|
||||
const timeoutId = setTimeout(() => this._hide(toastElement), duration);
|
||||
toastElement.querySelector('[data-toast-close]').addEventListener('click', () => {
|
||||
clearTimeout(timeoutId);
|
||||
this._hide(toastElement);
|
||||
});
|
||||
},
|
||||
|
||||
_hideToast: (toastElement) => {
|
||||
_hide: function(toastElement) {
|
||||
if (!toastElement) return;
|
||||
toastElement.classList.remove('show');
|
||||
toastElement.addEventListener('transitionend', () => toastElement.remove(), { once: true });
|
||||
}
|
||||
};
|
||||
|
||||
// 从第二个片段完整引入handleInitSubmit函数
|
||||
async function handleInitSubmit(e) {
|
||||
e.preventDefault();
|
||||
const username = DOMElements.initForm.username.value.trim();
|
||||
const password = DOMElements.initForm.password.value.trim();
|
||||
const confirmPassword = DOMElements.initForm.confirm_password.value.trim();
|
||||
|
||||
// 1. 获取并修剪输入值
|
||||
const username = DOMElements.usernameInput.value.trim(); // 获取用户名并去除前后空格
|
||||
const password = DOMElements.passwordInput.value.trim();
|
||||
const confirmPassword = DOMElements.confirmPasswordInput.value.trim();
|
||||
|
||||
// 2. 添加用户名输入框的空值验证
|
||||
if (username === '') {
|
||||
toast.show('管理员用户名不能为空', 'error');
|
||||
DOMElements.usernameInput.focus();
|
||||
toast.show(i18n.t('toasts.error_username_empty'), 'error');
|
||||
DOMElements.initForm.username.focus();
|
||||
return;
|
||||
}
|
||||
// 其他密码验证逻辑不变
|
||||
if (password === '') {
|
||||
toast.show('密码不能为空', 'error');
|
||||
DOMElements.passwordInput.focus();
|
||||
toast.show(i18n.t('toasts.error_password_empty'), 'error');
|
||||
DOMElements.initForm.password.focus();
|
||||
return;
|
||||
}
|
||||
if (confirmPassword === '') {
|
||||
toast.show('确认密码不能为空', 'error');
|
||||
DOMElements.confirmPasswordInput.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
if (password !== confirmPassword) {
|
||||
toast.show('两次输入的密码不匹配', 'error');
|
||||
DOMElements.passwordInput.focus();
|
||||
toast.show(i18n.t('toasts.init_error_mismatch'), 'error');
|
||||
DOMElements.initForm.confirm_password.focus();
|
||||
return;
|
||||
}
|
||||
if (password.length < PASSWORD_MIN_LENGTH) {
|
||||
toast.show(`密码长度至少为 ${PASSWORD_MIN_LENGTH} 位`, 'error');
|
||||
DOMElements.passwordInput.focus();
|
||||
toast.show(i18n.t('toasts.init_error_short', { minLength: PASSWORD_MIN_LENGTH }), 'error');
|
||||
DOMElements.initForm.password.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
DOMElements.initButton.disabled = true;
|
||||
DOMElements.initButton.querySelector('span').textContent = '设置中...';
|
||||
initButton.disabled = true;
|
||||
initButton.querySelector('span').textContent = i18n.t('pages.init.setting_up_btn');
|
||||
|
||||
try {
|
||||
const formData = new FormData(DOMElements.initForm);
|
||||
formData.set('username', username); // 确保发送的是修剪过的用户名
|
||||
formData.set('password', password); // 确保发送的是修剪过的密码
|
||||
formData.delete('confirm_password');
|
||||
const formData = new FormData();
|
||||
formData.append('username', username);
|
||||
formData.append('password', password);
|
||||
|
||||
const response = await fetch(INIT_API_URL, {
|
||||
method: 'POST',
|
||||
|
|
@ -118,32 +158,57 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
toast.show('管理员账户创建成功!正在跳转到登录页面...', 'success');
|
||||
setTimeout(() => { window.location.href = '/login.html'; }, REDIRECT_DELAY_SUCCESS);
|
||||
toast.show(i18n.t('toasts.init_success'), 'success');
|
||||
setTimeout(() => { window.location.href = '/login.html'; }, 1500);
|
||||
} else {
|
||||
throw new Error(result.error || `初始化失败: ${response.status}`);
|
||||
throw new Error(result.error || i18n.t('toasts.init_error_generic'));
|
||||
}
|
||||
} catch (error) {
|
||||
toast.show(error.message, 'error');
|
||||
DOMElements.initButton.disabled = false;
|
||||
DOMElements.initButton.querySelector('span').textContent = '完成设置';
|
||||
initButton.disabled = false;
|
||||
initButton.querySelector('span').textContent = i18n.t('pages.init.setup_btn');
|
||||
}
|
||||
}
|
||||
|
||||
function initApp() {
|
||||
theme.init();
|
||||
async function initApp() {
|
||||
// 主题设置逻辑
|
||||
const storedTheme = localStorage.getItem('theme');
|
||||
const systemPrefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
document.documentElement.dataset.theme = storedTheme || (systemPrefersDark ? 'dark' : 'light');
|
||||
|
||||
// 初始化国际化
|
||||
await i18n.init();
|
||||
|
||||
// 初始化表单事件监听
|
||||
if (DOMElements.initForm) {
|
||||
DOMElements.initButton = DOMElements.initForm.querySelector('button[type="submit"]');
|
||||
|
||||
toast.init(DOMElements.toastContainer);
|
||||
|
||||
DOMElements.initForm.addEventListener('submit', handleInitSubmit);
|
||||
} else {
|
||||
console.error('Init form element not found. Script may not function correctly.');
|
||||
}
|
||||
// 语言切换按钮事件监听 (从第一个片段引入)
|
||||
if (DOMElements.langSwitcherBtn) {
|
||||
DOMElements.langSwitcherBtn.addEventListener('click', (e) => {
|
||||
e.stopPropagation(); // 阻止事件冒泡, 防止立即触发document的点击事件
|
||||
DOMElements.langOptionsList.classList.toggle('hidden');
|
||||
});
|
||||
}
|
||||
// 语言选项列表事件监听 (从第一个片段引入)
|
||||
if (DOMElements.langOptionsList) {
|
||||
DOMElements.langOptionsList.addEventListener('click', async (e) => {
|
||||
const target = e.target.closest('li[data-lang]'); // 查找最近的语言li元素
|
||||
if (target) {
|
||||
await i18n.loadLocale(target.dataset.lang); // 加载新语言
|
||||
i18n.applyTranslations(); // 应用翻译
|
||||
i18n.populateLangOptions(); // 更新语言选项列表的激活状态
|
||||
DOMElements.langOptionsList.classList.add('hidden'); // 隐藏列表
|
||||
}
|
||||
});
|
||||
}
|
||||
// 文档点击事件, 用于点击外部时隐藏语言选项列表 (从第一个片段引入)
|
||||
document.addEventListener('click', () => {
|
||||
if (DOMElements.langOptionsList && !DOMElements.langOptionsList.classList.contains('hidden')) {
|
||||
DOMElements.langOptionsList.classList.add('hidden');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
initApp();
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ const supportedLangs = ['en', 'zh-CN']; // 应用支持的语言列表
|
|||
*/
|
||||
async function loadLocale(lang) {
|
||||
try {
|
||||
const response = await fetch(`/locales/${lang}.json?v=${Date.now()}`);
|
||||
const response = await fetch(`/locales/${lang}.json`);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Language file for ${lang} not found (status: ${response.status}).`);
|
||||
}
|
||||
|
|
@ -51,6 +51,13 @@ function applyTranslationsToDOM() {
|
|||
const translation = t(key);
|
||||
if (translation !== key) el.title = translation;
|
||||
});
|
||||
document.querySelectorAll('[data-i18n-placeholder]').forEach(el => {
|
||||
const key = el.dataset.i18nPlaceholder;
|
||||
const translation = t(key);
|
||||
if (translation !== key && el.placeholder !== undefined) { // 确保元素有placeholder属性
|
||||
el.placeholder = translation;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -107,3 +114,7 @@ export async function setLanguage(lang) {
|
|||
window.location.reload(); // 刷新页面以应用所有翻译是最简单可靠的方式
|
||||
}
|
||||
}
|
||||
|
||||
export function getCurrentLanguage() {
|
||||
return currentLang;
|
||||
}
|
||||
|
|
@ -1,64 +1,139 @@
|
|||
// js/login.js - 登录页面的独立逻辑
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
|
||||
const DOMElements = {
|
||||
loginForm: document.getElementById('login-form'),
|
||||
toastContainer: document.getElementById('toast-container'),
|
||||
langSwitcherBtn: document.getElementById('lang-switcher-btn'),
|
||||
langOptionsList: document.getElementById('lang-options-list'), // 从第一个片段引入
|
||||
};
|
||||
const loginButton = DOMElements.loginForm.querySelector('button[type="submit"]');
|
||||
const LOGIN_API_URL = '/v0/api/auth/login';
|
||||
|
||||
const theme = {
|
||||
init: () => {
|
||||
const storedTheme = localStorage.getItem('theme');
|
||||
const systemPrefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
const currentTheme = storedTheme || (systemPrefersDark ? 'dark' : 'light');
|
||||
document.documentElement.dataset.theme = currentTheme;
|
||||
const i18n = {
|
||||
currentLocale: {},
|
||||
currentLang: 'en',
|
||||
// 从第一个片段引入, 使用对象更方便显示语言名称
|
||||
supportedLangs: { 'en': 'English', 'zh-CN': '简体中文' },
|
||||
t: function(key, replacements = {}) {
|
||||
const translation = key.split('.').reduce((obj, k) => obj && obj[k], this.currentLocale) || key;
|
||||
let result = translation;
|
||||
if (typeof result === 'string') {
|
||||
for (const placeholder in replacements) {
|
||||
result = result.replace(`{${placeholder}}`, replacements[placeholder]);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
},
|
||||
applyTranslations: function() {
|
||||
// 优化后的翻译应用逻辑, 优先更新span, 其次更新非空文本节点, 最后直接更新元素文本
|
||||
document.querySelectorAll('[data-i18n]').forEach(el => {
|
||||
const key = el.dataset.i18n;
|
||||
const translation = this.t(key);
|
||||
if (translation !== key) { // 仅当找到翻译时才应用
|
||||
const spanChild = el.querySelector('span');
|
||||
if (spanChild) {
|
||||
spanChild.textContent = translation;
|
||||
} else {
|
||||
// 查找直接的、非空文本节点进行替换
|
||||
const textNode = Array.from(el.childNodes).find(node => node.nodeType === Node.TEXT_NODE && node.textContent.trim().length > 0);
|
||||
if (textNode) {
|
||||
textNode.textContent = translation;
|
||||
} else {
|
||||
// 备用方案: 直接设置元素的textContent
|
||||
el.textContent = translation;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
// 从第一个片段引入, 处理data-i18n-title属性
|
||||
document.querySelectorAll('[data-i18n-title]').forEach(el => {
|
||||
el.title = this.t(el.dataset.i18nTitle);
|
||||
});
|
||||
document.title = this.t('pages.login.page_title');
|
||||
},
|
||||
loadLocale: async function(lang) {
|
||||
try {
|
||||
const response = await fetch(`/locales/${lang}.json`);
|
||||
if (!response.ok) throw new Error('File not found');
|
||||
this.currentLocale = await response.json();
|
||||
this.currentLang = lang;
|
||||
document.documentElement.lang = lang; // 设置HTML语言属性
|
||||
localStorage.setItem('appLanguage', lang); // 从第一个片段引入, 保存到localStorage
|
||||
} catch (e) {
|
||||
console.error(`Could not load locale for ${lang}, using fallback.`, e);
|
||||
this.currentLocale = {};
|
||||
}
|
||||
},
|
||||
init: async function() {
|
||||
// 从第一个片段引入, 优先使用保存的语言, 其次使用浏览器语言
|
||||
const savedLang = localStorage.getItem('appLanguage');
|
||||
const browserLang = navigator.language.startsWith('zh') ? 'zh-CN' : 'en';
|
||||
const langToLoad = savedLang || browserLang;
|
||||
await this.loadLocale(langToLoad);
|
||||
this.applyTranslations();
|
||||
this.populateLangOptions(); // 从第一个片段引入, 初始化语言选项列表
|
||||
},
|
||||
// 从第一个片段引入, 用于动态生成语言选项列表
|
||||
populateLangOptions: function() {
|
||||
// 清空现有选项
|
||||
DOMElements.langOptionsList.innerHTML = '';
|
||||
for (const [code, name] of Object.entries(this.supportedLangs)) {
|
||||
const li = document.createElement('li');
|
||||
li.dataset.lang = code;
|
||||
li.textContent = name;
|
||||
if (code === this.currentLang) {
|
||||
li.classList.add('active'); // 标记当前选中语言
|
||||
}
|
||||
DOMElements.langOptionsList.appendChild(li);
|
||||
}
|
||||
}
|
||||
// 移除 i18n.toggleLanguage, 因为有新的语言选择机制
|
||||
};
|
||||
|
||||
// 从第二个片段完整引入toast对象
|
||||
const toast = {
|
||||
show: (message, type = 'info', duration = 3000) => {
|
||||
show: function(message, type = 'info', duration = 3000) {
|
||||
if (!DOMElements.toastContainer) return;
|
||||
const icons = { success: 'fa-check-circle', error: 'fa-times-circle', info: 'fa-info-circle' };
|
||||
const iconClass = icons[type] || 'fa-info-circle';
|
||||
const toastElement = document.createElement('div');
|
||||
toastElement.className = `toast ${type}`;
|
||||
toastElement.innerHTML = `<i class="toast-icon fa-solid ${iconClass}"></i><p class="toast-message">${message}</p><button class="toast-close" data-toast-close>×</button>`;
|
||||
toastElement.innerHTML = `<i class="toast-icon fa-solid ${icons[type]}"></i><p class="toast-message">${message}</p><button class="toast-close" data-toast-close>×</button>`;
|
||||
DOMElements.toastContainer.appendChild(toastElement);
|
||||
requestAnimationFrame(() => toastElement.classList.add('show'));
|
||||
const timeoutId = setTimeout(() => hideToast(toastElement), duration);
|
||||
const timeoutId = setTimeout(() => this._hide(toastElement), duration);
|
||||
toastElement.querySelector('[data-toast-close]').addEventListener('click', () => {
|
||||
clearTimeout(timeoutId);
|
||||
hideToast(toastElement);
|
||||
this._hide(toastElement);
|
||||
});
|
||||
}
|
||||
};
|
||||
function hideToast(toastElement) {
|
||||
},
|
||||
_hide: function(toastElement) {
|
||||
if (!toastElement) return;
|
||||
toastElement.classList.remove('show');
|
||||
toastElement.addEventListener('transitionend', () => toastElement.remove(), { once: true });
|
||||
}
|
||||
};
|
||||
|
||||
// 从第二个片段完整引入handleLogin函数
|
||||
async function handleLogin(e) {
|
||||
e.preventDefault();
|
||||
|
||||
// 获取并确认值
|
||||
const username = DOMElements.loginForm.username.value.trim();
|
||||
const password = DOMElements.loginForm.password.value.trim();
|
||||
|
||||
if (username === '') {
|
||||
toast.show('用户名不能为空', 'error');
|
||||
toast.show(i18n.t('toasts.error_username_empty'), 'error');
|
||||
DOMElements.loginForm.username.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
if (password === '') {
|
||||
toast.show('密码不能为空', 'error');
|
||||
toast.show(i18n.t('toasts.error_password_empty'), 'error');
|
||||
DOMElements.loginForm.password.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
loginButton.disabled = true;
|
||||
loginButton.querySelector('span').textContent = '登录中...';
|
||||
loginButton.querySelector('span').textContent = i18n.t('pages.login.logging_in_btn');
|
||||
|
||||
try {
|
||||
const response = await fetch(LOGIN_API_URL, {
|
||||
|
|
@ -67,23 +142,57 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
});
|
||||
const result = await response.json();
|
||||
if (response.ok) {
|
||||
toast.show('登录成功,正在跳转...', 'success');
|
||||
toast.show(i18n.t('toasts.login_success'), 'success');
|
||||
setTimeout(() => { window.location.href = '/'; }, 500);
|
||||
} else {
|
||||
throw new Error(result.error || `登录失败: ${response.status}`);
|
||||
throw new Error(result.error || i18n.t('toasts.login_error_generic'));
|
||||
}
|
||||
} catch (error) {
|
||||
toast.show(error.message, 'error');
|
||||
loginButton.disabled = false;
|
||||
loginButton.querySelector('span').textContent = '登录';
|
||||
loginButton.querySelector('span').textContent = i18n.t('pages.login.login_btn');
|
||||
}
|
||||
}
|
||||
|
||||
function init() {
|
||||
theme.init();
|
||||
async function initApp() {
|
||||
// 主题设置逻辑
|
||||
const storedTheme = localStorage.getItem('theme');
|
||||
const systemPrefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
document.documentElement.dataset.theme = storedTheme || (systemPrefersDark ? 'dark' : 'light');
|
||||
|
||||
// 初始化国际化
|
||||
await i18n.init();
|
||||
|
||||
// 登录表单事件监听
|
||||
if (DOMElements.loginForm) {
|
||||
DOMElements.loginForm.addEventListener('submit', handleLogin);
|
||||
}
|
||||
}
|
||||
init();
|
||||
// 语言切换按钮事件监听 (从第一个片段引入)
|
||||
if (DOMElements.langSwitcherBtn) {
|
||||
DOMElements.langSwitcherBtn.addEventListener('click', (e) => {
|
||||
e.stopPropagation(); // 阻止事件冒泡, 防止立即触发document的点击事件
|
||||
DOMElements.langOptionsList.classList.toggle('hidden');
|
||||
});
|
||||
}
|
||||
// 语言选项列表事件监听 (从第一个片段引入)
|
||||
if (DOMElements.langOptionsList) {
|
||||
DOMElements.langOptionsList.addEventListener('click', async (e) => {
|
||||
const target = e.target.closest('li[data-lang]'); // 查找最近的语言li元素
|
||||
if (target) {
|
||||
await i18n.loadLocale(target.dataset.lang); // 加载新语言
|
||||
i18n.applyTranslations(); // 应用翻译
|
||||
i18n.populateLangOptions(); // 更新语言选项列表的激活状态
|
||||
DOMElements.langOptionsList.classList.add('hidden'); // 隐藏列表
|
||||
}
|
||||
});
|
||||
}
|
||||
// 文档点击事件, 用于点击外部时隐藏语言选项列表 (从第一个片段引入)
|
||||
document.addEventListener('click', () => {
|
||||
if (DOMElements.langOptionsList && !DOMElements.langOptionsList.classList.contains('hidden')) {
|
||||
DOMElements.langOptionsList.classList.add('hidden');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
initApp();
|
||||
});
|
||||
|
|
@ -1,8 +1,9 @@
|
|||
// js/notifications.js - 提供Toast和Dialog两种通知
|
||||
|
||||
// 这个模块在初始化时需要知道容器的DOM元素
|
||||
let toastContainer;
|
||||
let dialogContainer;
|
||||
let modalContainer; // 虽然在此文件中不直接使用, 但 init 中保留以示完整
|
||||
let t; // 模块级翻译函数变量
|
||||
|
||||
function hideToast(toastElement) {
|
||||
if (!toastElement) return;
|
||||
|
|
@ -11,11 +12,14 @@ function hideToast(toastElement) {
|
|||
}
|
||||
|
||||
export const notification = {
|
||||
init: (toastEl, dialogEl) => {
|
||||
init: (toastEl, dialogEl, modalEl, translator) => {
|
||||
toastContainer = toastEl;
|
||||
dialogContainer = dialogEl;
|
||||
modalContainer = modalEl;
|
||||
t = translator; // 保存从外部传入的翻译函数
|
||||
},
|
||||
toast: (message, type = 'info', duration = 3000) => {
|
||||
if (!toastContainer) return;
|
||||
const icons = { success: 'fa-check-circle', error: 'fa-times-circle', info: 'fa-info-circle', warning: 'fa-exclamation-triangle' };
|
||||
const iconClass = icons[type] || 'fa-info-circle';
|
||||
const toastElement = document.createElement('div');
|
||||
|
|
@ -29,28 +33,44 @@ export const notification = {
|
|||
hideToast(toastElement);
|
||||
});
|
||||
},
|
||||
confirm: (message) => {
|
||||
confirm: (message, title = '', options = {}) => {
|
||||
return new Promise(resolve => {
|
||||
if (!dialogContainer || !t) {
|
||||
// 如果模块未初始化, 提供一个浏览器默认的 confirm作为回退
|
||||
console.warn('Notification module not initialized. Falling back to native confirm.');
|
||||
resolve(window.confirm(message));
|
||||
return;
|
||||
}
|
||||
|
||||
// 使用 t 函数翻译按钮文本, 如果 options 中提供了自定义键, 则优先使用
|
||||
const confirmText = options.confirmText || t('dialogs.confirm_btn');
|
||||
const cancelText = options.cancelText || t('dialogs.cancel_btn');
|
||||
|
||||
const dialogHTML = `
|
||||
<div class="dialog-box">
|
||||
${title ? `<h3>${title}</h3>` : ''}
|
||||
<p class="dialog-message">${message}</p>
|
||||
<div class="dialog-actions">
|
||||
<button class="btn btn-secondary" data-action="cancel">取消</button>
|
||||
<button class="btn btn-primary" data-action="confirm">确定</button>
|
||||
<button class="btn btn-secondary" data-action="cancel">${cancelText}</button>
|
||||
<button class="btn btn-primary" data-action="confirm">${confirmText}</button>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
dialogContainer.innerHTML = dialogHTML;
|
||||
dialogContainer.classList.add('active');
|
||||
|
||||
const eventHandler = (e) => {
|
||||
const actionButton = e.target.closest('[data-action]');
|
||||
if (!actionButton) return;
|
||||
closeDialog(actionButton.dataset.action === 'confirm');
|
||||
};
|
||||
|
||||
const closeDialog = (result) => {
|
||||
dialogContainer.removeEventListener('click', eventHandler);
|
||||
dialogContainer.classList.remove('active');
|
||||
setTimeout(() => { dialogContainer.innerHTML = ''; resolve(result); }, 200);
|
||||
};
|
||||
|
||||
dialogContainer.addEventListener('click', eventHandler);
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,18 +1,14 @@
|
|||
// js/settings.js - 设置页面的逻辑
|
||||
|
||||
import { theme, toast, activateNav } from './common.js';
|
||||
import { initCaddyStatus } from './caddy.js'; // 导入 Caddy 状态模块
|
||||
import { notification } from './notifications.js'; // 导入通知模块
|
||||
|
||||
const RESET_PWD_API_URL = '/v0/api/auth/resetpwd';
|
||||
const LOGOUT_API_URL = '/v0/api/auth/logout';
|
||||
import { initializePage } from './common.js';
|
||||
import { api } from './api.js';
|
||||
import { notification } from './notifications.js';
|
||||
import { t, setLanguage, getCurrentLanguage } from './locale.js';
|
||||
import { createCustomSelect } from './ui.js';
|
||||
|
||||
const DOMElements = {
|
||||
resetForm: document.getElementById('reset-password-form'),
|
||||
themeToggleInput: document.getElementById('theme-toggle-input'),
|
||||
logoutBtn: document.getElementById('logout-btn'),
|
||||
toastContainer: document.getElementById('toast-container'),
|
||||
dialogContainer: document.getElementById('dialog-container'),
|
||||
};
|
||||
const resetButton = DOMElements.resetForm.querySelector('button[type="submit"]');
|
||||
|
||||
|
|
@ -20,81 +16,62 @@ async function handleResetPassword(e) {
|
|||
e.preventDefault();
|
||||
const newPassword = DOMElements.resetForm.new_password.value;
|
||||
const confirmPassword = DOMElements.resetForm.confirm_new_password.value;
|
||||
|
||||
//保证字段均不为空, 用户名 密码 新密码
|
||||
const currentPassword = DOMElements.resetForm.old_password.value;
|
||||
const username = DOMElements.resetForm.username.value;
|
||||
|
||||
if (username === '') {
|
||||
toast.show('用户名不能为空', 'error');
|
||||
DOMElements.resetForm.username.focus();
|
||||
if (!username || !currentPassword || !newPassword || !confirmPassword) {
|
||||
notification.toast(t('toasts.error_all_fields_required'), 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentPassword === '') {
|
||||
toast.show('当前密码不能为空', 'error');
|
||||
DOMElements.resetForm.old_password.focus();
|
||||
return;
|
||||
}
|
||||
if (newPassword === '') {
|
||||
notification.toast('新密码不能为空', 'error');
|
||||
DOMElements.resetForm.new_password.focus();
|
||||
return;
|
||||
}
|
||||
if (confirmPassword === '') {
|
||||
notification.toast('确认新密码不能为空', 'error');
|
||||
DOMElements.resetForm.confirm_new_password.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
if (newPassword !== confirmPassword) {
|
||||
notification.toast('新密码与确认密码不匹配', 'error');
|
||||
notification.toast(t('toasts.init_error_mismatch'), 'error');
|
||||
return;
|
||||
}
|
||||
if (newPassword.length < 8) {
|
||||
notification.toast('新密码长度至少为8位', 'error');
|
||||
notification.toast(t('toasts.init_error_short'), 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
resetButton.disabled = true;
|
||||
resetButton.querySelector('span').textContent = '重置中...';
|
||||
|
||||
const formData = new FormData(DOMElements.resetForm);
|
||||
resetButton.querySelector('span').textContent = t('pages.settings.resetting_password_btn');
|
||||
|
||||
try {
|
||||
const response = await fetch(RESET_PWD_API_URL, {
|
||||
method: 'POST',
|
||||
body: new URLSearchParams(formData),
|
||||
});
|
||||
const result = await response.json();
|
||||
if (response.ok) {
|
||||
notification.toast('密码重置成功!请重新登录。', 'success');
|
||||
setTimeout(() => { window.location.href = LOGOUT_API_URL; }, 1500);
|
||||
} else {
|
||||
throw new Error(result.error || '重置密码失败');
|
||||
}
|
||||
const result = await api.post('/auth/resetpwd', new URLSearchParams(new FormData(DOMElements.resetForm)));
|
||||
notification.toast(t('toasts.pwd_reset_success'), 'success');
|
||||
setTimeout(() => { window.location.href = '/v0/api/auth/logout'; }, 1500);
|
||||
} catch (error) {
|
||||
notification.toast(error.message, 'error');
|
||||
notification.toast(`${t('common.error_prefix')}: ${error.message}`, 'error');
|
||||
resetButton.disabled = false;
|
||||
resetButton.querySelector('span').textContent = '重置密码';
|
||||
resetButton.querySelector('span').textContent = t('pages.settings.reset_password_btn');
|
||||
}
|
||||
}
|
||||
|
||||
async function handleLogout() {
|
||||
if (await notification.confirm('您确定要退出登录吗?')) {
|
||||
notification.toast('正在退出...', 'info');
|
||||
setTimeout(() => { window.location.href = LOGOUT_API_URL; }, 500);
|
||||
if (await notification.confirm(t('dialogs.logout_msg'))) {
|
||||
notification.toast(t('toasts.logout_processing'), 'info');
|
||||
setTimeout(() => { window.location.href = '/v0/api/auth/logout'; }, 500);
|
||||
}
|
||||
}
|
||||
|
||||
function init() {
|
||||
theme.init(DOMElements.themeToggleInput);
|
||||
notification.init(DOMElements.toastContainer, DOMElements.dialogContainer);
|
||||
activateNav('settings');
|
||||
initCaddyStatus(); // 初始化通用Caddy状态检查
|
||||
// 页面特有的初始化逻辑
|
||||
function pageInit() {
|
||||
const langOptions = { 'en': 'English', 'zh-CN': '简体中文' };
|
||||
const langSelectOptions = Object.keys(langOptions).map(key => ({ name: langOptions[key], value: key }));
|
||||
|
||||
createCustomSelect('select-language', langSelectOptions, (selectedValue) => {
|
||||
setLanguage(selectedValue);
|
||||
});
|
||||
|
||||
const langSelect = document.getElementById('select-language');
|
||||
if (langSelect) {
|
||||
const currentLangName = langOptions[getCurrentLanguage()];
|
||||
const selectedDiv = langSelect.querySelector('.select-selected');
|
||||
if (selectedDiv) selectedDiv.textContent = currentLangName;
|
||||
}
|
||||
|
||||
DOMElements.resetForm.addEventListener('submit', handleResetPassword);
|
||||
DOMElements.logoutBtn.addEventListener('click', handleLogout);
|
||||
}
|
||||
|
||||
init();
|
||||
// 使用通用初始化函数
|
||||
initializePage({ pageId: 'settings', pageInit: pageInit });
|
||||
|
|
@ -1,5 +1,13 @@
|
|||
// js/ui.js - 管理所有与UI渲染和DOM操作相关的函数
|
||||
|
||||
// 模块级私有变量, 用于存储翻译函数
|
||||
let t;
|
||||
|
||||
// 新增: 初始化函数, 用于接收翻译函数
|
||||
export function initUI(translator) {
|
||||
t = translator;
|
||||
}
|
||||
|
||||
export const DOMElements = {
|
||||
sidebar: document.getElementById('sidebar'),
|
||||
menuToggleBtn: document.getElementById('menu-toggle-btn'),
|
||||
|
|
@ -45,14 +53,14 @@ export function switchView(viewToShow) {
|
|||
export function renderConfigList(filenames) {
|
||||
DOMElements.configListContainer.innerHTML = '';
|
||||
if (!filenames || filenames.length === 0) {
|
||||
DOMElements.configListContainer.innerHTML = '<p>还没有任何配置,请创建一个。</p>';
|
||||
DOMElements.configListContainer.innerHTML = `<p>${t('configs.no_configs')}</p>`;
|
||||
return;
|
||||
}
|
||||
filenames.forEach(filename => {
|
||||
const item = document.createElement('li');
|
||||
item.className = 'config-item';
|
||||
item.dataset.filename = filename;
|
||||
item.innerHTML = `<span class="config-item-name">${filename}</span><div class="config-item-actions"><button class="btn-icon edit-btn" title="编辑"><i class="fa-solid fa-pen-to-square"></i></button><button class="btn-icon delete-btn" title="删除"><i class="fa-solid fa-trash-can"></i></button></div>`;
|
||||
item.innerHTML = `<span class="config-item-name">${filename}</span><div class="config-item-actions"><button class="btn-icon edit-btn" title="${t('common.edit')}"><i class="fa-solid fa-pen-to-square"></i></button><button class="btn-icon delete-btn" title="${t('common.delete')}"><i class="fa-solid fa-trash-can"></i></button></div>`;
|
||||
DOMElements.configListContainer.appendChild(item);
|
||||
});
|
||||
}
|
||||
|
|
@ -61,21 +69,21 @@ export function addKeyValueInput(container, keyName, valueName, key = '', value
|
|||
const div = document.createElement('div');
|
||||
div.className = 'header-entry';
|
||||
div.innerHTML = `
|
||||
<input type="text" name="${keyName}" placeholder="Key" value="${key}">
|
||||
<input type="text" name="${valueName}" placeholder="Value" value="${value}">
|
||||
<button type="button" class="btn-icon" onclick="this.parentElement.remove()" title="移除此条目">
|
||||
<input type="text" name="${keyName}" placeholder="${t('form.key_placeholder')}" value="${key}">
|
||||
<input type="text" name="${valueName}" placeholder="${t('form.value_placeholder')}" value="${value}">
|
||||
<button type="button" class="btn-icon" onclick="this.parentElement.remove()" title="${t('common.remove_item')}">
|
||||
<i class="fa-solid fa-xmark"></i>
|
||||
</button>`;
|
||||
container.appendChild(div);
|
||||
}
|
||||
|
||||
export function addSingleInput(container, inputName, placeholder, value = '') {
|
||||
export function addSingleInput(container, inputName, placeholderKey, value = '') {
|
||||
const div = document.createElement('div');
|
||||
div.className = 'header-entry';
|
||||
div.style.gridTemplateColumns = '1fr auto';
|
||||
div.innerHTML = `
|
||||
<input type="text" name="${inputName}" placeholder="${placeholder}" value="${value}">
|
||||
<button type="button" class="btn-icon" onclick="this.parentElement.remove()" title="移除此上游">
|
||||
<input type="text" name="${inputName}" placeholder="${t(placeholderKey)}" value="${value}">
|
||||
<button type="button" class="btn-icon" onclick="this.parentElement.remove()" title="${t('common.remove_upstream')}">
|
||||
<i class="fa-solid fa-xmark"></i>
|
||||
</button>`;
|
||||
container.appendChild(div);
|
||||
|
|
@ -97,7 +105,7 @@ export function fillForm(config, originalFilename) {
|
|||
DOMElements.multiUpstreamContainer.innerHTML = '';
|
||||
if (upstreamConfig.muti_upstream && upstreamConfig.upstream_servers) {
|
||||
upstreamConfig.upstream_servers.forEach(server => {
|
||||
addSingleInput(DOMElements.multiUpstreamContainer, 'upstream_servers', '例如: 127.0.0.1:8081', server);
|
||||
addSingleInput(DOMElements.multiUpstreamContainer, 'upstream_servers', 'form.upstream_server_placeholder', server);
|
||||
});
|
||||
}
|
||||
DOMElements.upstreamHeadersContainer.innerHTML = '';
|
||||
|
|
@ -137,21 +145,22 @@ export function updateCaddyStatusView(status, handlers) {
|
|||
const dot = DOMElements.caddyStatusIndicator.querySelector('.status-dot');
|
||||
const text = DOMElements.caddyStatusIndicator.querySelector('.status-text');
|
||||
const buttonContainer = DOMElements.caddyActionButtonContainer;
|
||||
if (!dot || !text || !buttonContainer) return;
|
||||
dot.className = 'status-dot';
|
||||
buttonContainer.innerHTML = '';
|
||||
let statusText, dotClass;
|
||||
switch (status) {
|
||||
case 'running':
|
||||
statusText = '运行中'; dotClass = 'running';
|
||||
buttonContainer.appendChild(createButton('重载配置', 'btn-warning', handleReloadCaddy));
|
||||
buttonContainer.appendChild(createButton('停止 Caddy', 'btn-danger', handleStopCaddy));
|
||||
statusText = t('status.running'); dotClass = 'running';
|
||||
buttonContainer.appendChild(createButton(t('caddy.reload_btn'), 'btn-warning', handleReloadCaddy));
|
||||
buttonContainer.appendChild(createButton(t('caddy.stop_btn'), 'btn-danger', handleStopCaddy));
|
||||
break;
|
||||
case 'stopped':
|
||||
statusText = '已停止'; dotClass = 'stopped';
|
||||
buttonContainer.appendChild(createButton('启动 Caddy', 'btn-success', handleStartCaddy));
|
||||
statusText = t('status.stopped'); dotClass = 'stopped';
|
||||
buttonContainer.appendChild(createButton(t('caddy.start_btn'), 'btn-success', handleStartCaddy));
|
||||
break;
|
||||
case 'checking': statusText = '检查中...'; dotClass = 'checking'; break;
|
||||
default: statusText = '状态未知'; dotClass = 'error'; break;
|
||||
case 'checking': statusText = t('status.checking'); dotClass = 'checking'; break;
|
||||
default: statusText = t('status.unknown'); dotClass = 'error'; break;
|
||||
}
|
||||
text.textContent = statusText;
|
||||
dot.classList.add(dotClass);
|
||||
|
|
@ -177,24 +186,21 @@ export function updateSegmentedControl(activeButton) {
|
|||
slider.style.transform = `translateX(${activeButton.offsetLeft}px)`;
|
||||
}
|
||||
|
||||
// 彻底重构: 使用 Promise 并管理自己的事件监听器生命周期
|
||||
export function createPresetSelectionModal(presets) {
|
||||
return new Promise(resolve => {
|
||||
const modalContainer = DOMElements.modalContainer;
|
||||
if (!modalContainer) return resolve(null); // 安全退出
|
||||
|
||||
if (!modalContainer) return resolve(null);
|
||||
const presetItems = presets.map(p => `
|
||||
<li data-preset-id="${p.id}">
|
||||
<strong>${p.name}</strong>
|
||||
<p>${p.description}</p>
|
||||
<strong>${t(p.name_key) || p.name}</strong>
|
||||
<p>${t(p.desc_key) || p.description}</p>
|
||||
</li>
|
||||
`).join('');
|
||||
|
||||
const modalHTML = `
|
||||
<div class="modal-overlay"></div>
|
||||
<div class="modal-box">
|
||||
<header class="modal-header">
|
||||
<h3>从预设填充</h3>
|
||||
<h3>${t('form.fill_from_preset')}</h3>
|
||||
<button class="btn-icon" data-modal-close><i class="fa-solid fa-xmark"></i></button>
|
||||
</header>
|
||||
<div class="modal-content">
|
||||
|
|
@ -202,29 +208,22 @@ export function createPresetSelectionModal(presets) {
|
|||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
modalContainer.innerHTML = modalHTML;
|
||||
requestAnimationFrame(() => modalContainer.classList.add('active'));
|
||||
|
||||
const cleanupAndResolve = (value) => {
|
||||
modalContainer.removeEventListener('click', eventHandler);
|
||||
modalContainer.classList.remove('active');
|
||||
setTimeout(() => {
|
||||
modalContainer.innerHTML = '';
|
||||
resolve(value);
|
||||
}, 300);
|
||||
setTimeout(() => { modalContainer.innerHTML = ''; resolve(value); }, 300);
|
||||
};
|
||||
|
||||
const eventHandler = (e) => {
|
||||
if (e.target.classList.contains('modal-overlay') || e.target.closest('[data-modal-close]')) {
|
||||
cleanupAndResolve(null); // 用户取消
|
||||
cleanupAndResolve(null);
|
||||
}
|
||||
const listItem = e.target.closest('li[data-preset-id]');
|
||||
if (listItem) {
|
||||
cleanupAndResolve(listItem.dataset.presetId); // 用户选择
|
||||
cleanupAndResolve(listItem.dataset.presetId);
|
||||
}
|
||||
};
|
||||
|
||||
modalContainer.addEventListener('click', eventHandler);
|
||||
});
|
||||
}
|
||||
|
|
@ -233,28 +232,30 @@ export function createCustomSelect(containerId, options, onSelect) {
|
|||
const container = document.getElementById(containerId);
|
||||
if (!container) return;
|
||||
|
||||
// 从容器的ID动态生成隐藏input的name属性
|
||||
// 例如, id 'select-log-level' -> name 'log_level'
|
||||
const inputName = container.id.replace('select-', '').replace(/-/g, '_');
|
||||
|
||||
container.innerHTML = `<div class="select-selected"></div><div class="select-items"></div><input type="hidden" name="${inputName}">`;
|
||||
|
||||
const selectedDiv = container.querySelector('.select-selected');
|
||||
const itemsDiv = container.querySelector('.select-items');
|
||||
const hiddenInput = container.querySelector('input[type="hidden"]');
|
||||
itemsDiv.innerHTML = '';
|
||||
|
||||
if (!options || options.length === 0) {
|
||||
selectedDiv.textContent = '无可用选项';
|
||||
selectedDiv.textContent = t('common.no_options');
|
||||
return;
|
||||
}
|
||||
|
||||
options.forEach((option, index) => {
|
||||
const item = document.createElement('div');
|
||||
item.textContent = option;
|
||||
item.dataset.value = option;
|
||||
const optionText = typeof option === 'object' ? option.name : option;
|
||||
const optionValue = typeof option === 'object' ? option.value : option;
|
||||
|
||||
item.textContent = optionText;
|
||||
item.dataset.value = optionValue;
|
||||
|
||||
if (index === 0) {
|
||||
selectedDiv.textContent = option;
|
||||
hiddenInput.value = option;
|
||||
selectedDiv.textContent = optionText;
|
||||
hiddenInput.value = optionValue;
|
||||
}
|
||||
item.addEventListener('click', function(e) {
|
||||
selectedDiv.textContent = this.textContent;
|
||||
|
|
@ -266,6 +267,7 @@ export function createCustomSelect(containerId, options, onSelect) {
|
|||
});
|
||||
itemsDiv.appendChild(item);
|
||||
});
|
||||
|
||||
selectedDiv.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
document.querySelectorAll('.select-items.select-show').forEach(openSelect => {
|
||||
|
|
@ -277,6 +279,7 @@ export function createCustomSelect(containerId, options, onSelect) {
|
|||
itemsDiv.classList.toggle('select-show');
|
||||
selectedDiv.classList.toggle('select-arrow-active');
|
||||
});
|
||||
|
||||
document.addEventListener('click', () => {
|
||||
itemsDiv.classList.remove('select-show');
|
||||
if(selectedDiv) selectedDiv.classList.remove('select-arrow-active');
|
||||
|
|
|
|||
140
frontend/locales/en.json
Normal file
140
frontend/locales/en.json
Normal file
|
|
@ -0,0 +1,140 @@
|
|||
{
|
||||
"nav": {
|
||||
"configs": "Site Configs",
|
||||
"global": "Global Config",
|
||||
"settings": "Panel Settings",
|
||||
"logout": "Logout"
|
||||
},
|
||||
"status": {
|
||||
"checking": "Checking...",
|
||||
"running": "Running",
|
||||
"stopped": "Stopped",
|
||||
"unknown": "Unknown"
|
||||
},
|
||||
"caddy": {
|
||||
"start_btn": "Start Caddy",
|
||||
"stop_btn": "Stop Caddy",
|
||||
"reload_btn": "Reload Config"
|
||||
},
|
||||
"common": {
|
||||
"save": "Save",
|
||||
"cancel": "Cancel",
|
||||
"append": "Append",
|
||||
"replace": "Replace",
|
||||
"back_to_list": "Back to list",
|
||||
"error_prefix": "Error",
|
||||
"loading": "Loading...",
|
||||
"logout": "Logout"
|
||||
},
|
||||
"pages": {
|
||||
"configs": {
|
||||
"page_title": "Site Configs - CaddyDash",
|
||||
"title": "Site Configuration",
|
||||
"add_new_btn": "New Config",
|
||||
"form_title_create": "Create New Config",
|
||||
"form_title_edit": "Edit Config",
|
||||
"listtitle": "Site Configs",
|
||||
"rendered_caddyfile_title": "Rendered Caddyfile"
|
||||
},
|
||||
"global": {
|
||||
"page_title": "Global Config - CaddyDash",
|
||||
"title": "Global Caddyfile Configuration",
|
||||
"description": "Changes here will overwrite your main Caddyfile and trigger a Caddy reload.",
|
||||
"save_btn": "Save and Reload"
|
||||
},
|
||||
"settings": {
|
||||
"page_title": "Panel Settings - CaddyDash",
|
||||
"title": "Panel Settings",
|
||||
"account_security_title": "Account Security",
|
||||
"language_label": "Language",
|
||||
"current_password_label": "Current Password",
|
||||
"new_password_label": "New Password (min. 8 characters)",
|
||||
"confirm_new_password_label": "Confirm New Password",
|
||||
"reset_password_btn": "Reset Password",
|
||||
"resetting_password_btn": "Resetting..."
|
||||
},
|
||||
"login": {
|
||||
"page_title": "Login - CaddyDash",
|
||||
"welcome": "Welcome to CaddyDash",
|
||||
"prompt": "Please enter your credentials to continue",
|
||||
"username_label": "Username",
|
||||
"password_label": "Password",
|
||||
"login_btn": "Login",
|
||||
"logging_in_btn": "Logging in..."
|
||||
},
|
||||
"init": {
|
||||
"page_title": "Initial Setup - CaddyDash",
|
||||
"title": "Welcome to CaddyDash",
|
||||
"prompt": "Please create your administrator account to complete setup",
|
||||
"admin_user_label": "Admin Username",
|
||||
"password_label": "Password (min. 8 characters)",
|
||||
"confirm_password_label": "Confirm Password",
|
||||
"setup_btn": "Complete Setup",
|
||||
"setting_up_btn": "Setting up..."
|
||||
}
|
||||
},
|
||||
"form": {
|
||||
"legend_basic": "Basic Config",
|
||||
"domain_label": "Primary Domain (used as filename)",
|
||||
"legend_service_mode": "Service Mode",
|
||||
"mode_none": "None",
|
||||
"mode_rp": "Reverse Proxy",
|
||||
"mode_fs": "File Server",
|
||||
"legend_rp": "Reverse Proxy Config",
|
||||
"upstream_addr_label": "Upstream Address",
|
||||
"upstream_addr_placeholder": "e.g., 127.0.0.1:8080",
|
||||
"enable_multi_upstream": "Enable Multi-Upstream Load Balancing",
|
||||
"upstream_servers_label": "Upstream Server List",
|
||||
"add_upstream_server_btn": "Add Upstream Server",
|
||||
"upstream_server_placeholder": "e.g., 127.0.0.1:8081",
|
||||
"upstream_headers_label": "Upstream Headers",
|
||||
"fill_from_preset": "Fill from Preset",
|
||||
"add_header_btn": "Add Header",
|
||||
"legend_fs": "File Server Config",
|
||||
"fs_root_label": "Root Directory Path",
|
||||
"fs_root_placeholder": "e.g., /srv/www",
|
||||
"enable_fs_browser": "Enable File Browser",
|
||||
"legend_global_headers": "Global Headers",
|
||||
"legend_features": "Additional Features",
|
||||
"feature_log": "Enable Logging",
|
||||
"feature_error_page": "Enable Custom Error Pages",
|
||||
"feature_encode": "Enable Compression"
|
||||
},
|
||||
"dialogs": {
|
||||
"unsaved_changes_title": "Unsaved Changes",
|
||||
"unsaved_changes_msg": "You have unsaved changes. Are you sure you want to discard them?",
|
||||
"delete_config_title": "Confirm Deletion",
|
||||
"delete_config_msg": "Are you sure you want to delete \"{filename}\"?",
|
||||
"stop_caddy_msg": "Are you sure you want to stop the Caddy instance?",
|
||||
"reload_caddy_msg": "Are you sure you want to reload Caddy config?",
|
||||
"logout_msg": "Are you sure you want to log out?",
|
||||
"preset_fill_title": "Fill Method",
|
||||
"preset_fill_msg": "How would you like to apply the preset?",
|
||||
"confirm_btn": "Confirm",
|
||||
"cancel_btn": "Cancel"
|
||||
},
|
||||
"toasts": {
|
||||
"save_success": "Configuration saved successfully.",
|
||||
"save_error": "Failed to save configuration: {error}",
|
||||
"reload_sent": "Reload command has been sent.",
|
||||
"delete_success": "Configuration deleted successfully.",
|
||||
"delete_error": "Failed to delete configuration: {error}",
|
||||
"load_configs_error": "Failed to load site configs: {error}",
|
||||
"load_config_detail_error": "Failed to load config details: {error}",
|
||||
"error_domain_empty": "Domain name cannot be empty.",
|
||||
"loading_preset": "Loading preset...",
|
||||
"preset_no_data": "This preset contains no data.",
|
||||
"load_preset_error": "Failed to load preset: {error}",
|
||||
"preset_fill_success": "Preset \"{presetName}\" filled successfully.",
|
||||
"no_presets_available": "No applicable presets available for this section.",
|
||||
"logout_processing": "Logging out...",
|
||||
"error_username_empty": "Username cannot be empty.",
|
||||
"error_password_empty": "Password cannot be empty.",
|
||||
"login_success": "Login successful! Redirecting...",
|
||||
"login_error_generic": "Login failed. Please check your credentials.",
|
||||
"init_error_mismatch": "Passwords do not match.",
|
||||
"init_error_short": "Password must be at least {minLength} characters long.",
|
||||
"init_success": "Setup complete! Redirecting to login...",
|
||||
"init_error_generic": "Initialization failed. Please try again."
|
||||
}
|
||||
}
|
||||
140
frontend/locales/zh-CN.json
Normal file
140
frontend/locales/zh-CN.json
Normal file
|
|
@ -0,0 +1,140 @@
|
|||
{
|
||||
"nav": {
|
||||
"configs": "站点配置",
|
||||
"global": "全局配置",
|
||||
"settings": "面板设置",
|
||||
"logout": "退出登录"
|
||||
},
|
||||
"status": {
|
||||
"checking": "检查中...",
|
||||
"running": "运行中",
|
||||
"stopped": "已停止",
|
||||
"unknown": "状态未知"
|
||||
},
|
||||
"caddy": {
|
||||
"start_btn": "启动 Caddy",
|
||||
"stop_btn": "停止 Caddy",
|
||||
"reload_btn": "重载配置"
|
||||
},
|
||||
"common": {
|
||||
"save": "保存",
|
||||
"cancel": "取消",
|
||||
"append": "追加",
|
||||
"replace": "替换",
|
||||
"back_to_list": "返回列表",
|
||||
"error_prefix": "错误",
|
||||
"loading": "加载中...",
|
||||
"logout": "退出登录"
|
||||
},
|
||||
"pages": {
|
||||
"configs": {
|
||||
"page_title": "站点配置 - CaddyDash",
|
||||
"title": "站点配置",
|
||||
"add_new_btn": "创建新配置",
|
||||
"form_title_create": "创建新配置",
|
||||
"form_title_edit": "编辑配置",
|
||||
"listtitle": "站点配置管理",
|
||||
"rendered_caddyfile_title": "渲染后的 Caddyfile"
|
||||
},
|
||||
"global": {
|
||||
"page_title": "全局配置 - CaddyDash",
|
||||
"title": "全局 Caddyfile 配置",
|
||||
"description": "修改这些配置将会重写您的主 Caddyfile 并触发 Caddy 重载",
|
||||
"save_btn": "保存并重载"
|
||||
},
|
||||
"settings": {
|
||||
"page_title": "面板设置 - CaddyDash",
|
||||
"title": "面板设置",
|
||||
"account_security_title": "账户安全",
|
||||
"language_label": "语言",
|
||||
"current_password_label": "当前密码",
|
||||
"new_password_label": "新密码 (至少8位)",
|
||||
"confirm_new_password_label": "确认新密码",
|
||||
"reset_password_btn": "重置密码",
|
||||
"resetting_password_btn": "重置中..."
|
||||
},
|
||||
"login": {
|
||||
"page_title": "登录 - CaddyDash",
|
||||
"welcome": "欢迎使用 CaddyDash",
|
||||
"prompt": "请输入您的凭证以继续",
|
||||
"username_label": "用户名",
|
||||
"password_label": "密码",
|
||||
"login_btn": "登录",
|
||||
"logging_in_btn": "登录中..."
|
||||
},
|
||||
"init": {
|
||||
"page_title": "首次设置 - CaddyDash",
|
||||
"title": "欢迎使用 CaddyDash",
|
||||
"prompt": "请创建您的管理员账户以完成首次设置",
|
||||
"admin_user_label": "管理员用户名",
|
||||
"password_label": "密码 (至少8位)",
|
||||
"confirm_password_label": "确认密码",
|
||||
"setup_btn": "完成设置",
|
||||
"setting_up_btn": "设置中..."
|
||||
}
|
||||
},
|
||||
"form": {
|
||||
"legend_basic": "基础配置",
|
||||
"domain_label": "主域名 (将用作文件名)",
|
||||
"legend_service_mode": "服务模式",
|
||||
"mode_none": "无",
|
||||
"mode_rp": "反向代理",
|
||||
"mode_fs": "文件服务",
|
||||
"legend_rp": "反向代理配置",
|
||||
"upstream_addr_label": "上游服务地址",
|
||||
"upstream_addr_placeholder": "例如: 127.0.0.1:8080",
|
||||
"enable_multi_upstream": "启用多上游负载均衡",
|
||||
"upstream_servers_label": "上游服务器列表",
|
||||
"add_upstream_server_btn": "添加上游服务器",
|
||||
"upstream_server_placeholder": "例如: 127.0.0.1:8081",
|
||||
"upstream_headers_label": "上游请求头 (Upstream Headers)",
|
||||
"fill_from_preset": "从预设填充",
|
||||
"add_header_btn": "添加请求头",
|
||||
"legend_fs": "文件服务配置",
|
||||
"fs_root_label": "根目录路径",
|
||||
"fs_root_placeholder": "例如: /srv/www",
|
||||
"enable_fs_browser": "启用文件浏览器",
|
||||
"legend_global_headers": "全局请求头 (Headers)",
|
||||
"legend_features": "附加功能",
|
||||
"feature_log": "启用日志",
|
||||
"feature_error_page": "启用错误页",
|
||||
"feature_encode": "启用压缩"
|
||||
},
|
||||
"dialogs": {
|
||||
"unsaved_changes_title": "未保存的更改",
|
||||
"unsaved_changes_msg": "您有未保存的更改确定要放弃吗?",
|
||||
"delete_config_title": "确认删除",
|
||||
"delete_config_msg": "您确定要删除 \"{filename}\" 吗?",
|
||||
"stop_caddy_msg": "您确定要停止 Caddy 实例吗?",
|
||||
"reload_caddy_msg": "确定要重载 Caddy 配置吗?",
|
||||
"logout_msg": "您确定要退出登录吗?",
|
||||
"preset_fill_title": "填充方式",
|
||||
"preset_fill_msg": "您希望如何应用这个预设?",
|
||||
"confirm_btn": "确定",
|
||||
"cancel_btn": "取消"
|
||||
},
|
||||
"toasts": {
|
||||
"save_success": "配置已成功保存",
|
||||
"save_error": "保存配置失败: {error}",
|
||||
"reload_sent": "重载命令已发送",
|
||||
"delete_success": "配置已成功删除",
|
||||
"delete_error": "删除配置失败: {error}",
|
||||
"load_configs_error": "加载站点配置列表失败: {error}",
|
||||
"load_config_detail_error": "加载配置详情失败: {error}",
|
||||
"error_domain_empty": "域名不能为空",
|
||||
"loading_preset": "正在加载预设...",
|
||||
"preset_no_data": "此预设无数据",
|
||||
"load_preset_error": "加载预设失败: {error}",
|
||||
"preset_fill_success": "已成功填充预设 \"{presetName}\"",
|
||||
"no_presets_available": "没有适用于此区域的预设",
|
||||
"logout_processing": "正在退出...",
|
||||
"error_username_empty": "用户名不能为空",
|
||||
"error_password_empty": "密码不能为空",
|
||||
"login_success": "登录成功!正在跳转...",
|
||||
"login_error_generic": "登录失败请检查您的凭证",
|
||||
"init_error_mismatch": "两次输入的密码不一致",
|
||||
"init_error_short": "密码至少需要 {minLength} 位字符",
|
||||
"init_success": "设置完成!正在跳转到登录页...",
|
||||
"init_error_generic": "初始化失败,请重试"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,44 +1,56 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>登录 - CaddyDash</title>
|
||||
<title data-i18n="pages.login.page_title">登录 - CaddyDash</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
|
||||
<link rel="stylesheet" href="css/style.css">
|
||||
</head>
|
||||
|
||||
<body class="login-page-body">
|
||||
|
||||
<div class="login-container">
|
||||
<header class="login-header">
|
||||
<i class="fa-solid fa-rocket"></i>
|
||||
<h1>CaddyDash</h1>
|
||||
<p>请输入您的凭证以继续</p>
|
||||
<h1 data-i18n="pages.login.welcome">欢迎使用 CaddyDash</h1>
|
||||
<p data-i18n="pages.login.prompt">请输入您的凭证以继续</p>
|
||||
</header>
|
||||
|
||||
<form id="login-form">
|
||||
<div class="form-group">
|
||||
<label for="username">用户名</label>
|
||||
<label for="username" data-i18n="pages.login.username_label">用户名</label>
|
||||
<input type="text" id="username" name="username" autocomplete="username">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="password">密码</label>
|
||||
<label for="password" data-i18n="pages.login.password_label">密码</label>
|
||||
<input type="password" id="password" name="password" autocomplete="current-password">
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary btn-login">
|
||||
<span>登录</span>
|
||||
<span data-i18n="pages.login.login_btn">登录</span>
|
||||
<i class="fa-solid fa-arrow-right"></i>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- 右上角消息通知容器 -->
|
||||
<div id="toast-container" class="toast-container"></div>
|
||||
<!-- 新增: 语言切换器 -->
|
||||
<div class="language-switcher-corner">
|
||||
<button id="lang-switcher-btn" class="btn-icon" data-i18n-title="common.switch_language" title="切换语言">
|
||||
<i class="fa-solid fa-language"></i>
|
||||
</button>
|
||||
<!-- 语言选项列表, 默认隐藏 -->
|
||||
<ul id="lang-options-list" class="hidden">
|
||||
<!-- JS 动态填充 -->
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<script src="js/login.js"></script>
|
||||
<div id="toast-container" class="toast-container"></div>
|
||||
<script type="module" src="js/login.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
|
@ -22,7 +22,8 @@
|
|||
<nav class="sidebar-nav">
|
||||
<ul>
|
||||
<li><a href="/" data-nav-id="configs"><i class="fa-solid fa-sitemap"></i> <span>配置管理</span></a></li>
|
||||
<li><a href="/global.html" data-nav-id="global"><i class="fa-solid fa-globe"></i> <span>全局配置</span></a></li>
|
||||
<li><a href="/global.html" data-nav-id="global"><i class="fa-solid fa-globe"></i>
|
||||
<span>全局配置</span></a></li>
|
||||
<li><a href="/settings.html" data-nav-id="settings"><i class="fa-solid fa-gears"></i>
|
||||
<span>面板设置</span></a></li>
|
||||
</ul>
|
||||
|
|
@ -48,6 +49,13 @@
|
|||
</header>
|
||||
|
||||
<div id="view-container">
|
||||
<section class="card-panel">
|
||||
<h3 data-i18n="settings_language_title">界面设置</h3>
|
||||
<div class="form-group" style="margin-top: 16px;">
|
||||
<label for="select-language" data-i18n="pages.settings.language_label">语言</label>
|
||||
<div id="select-language" class="custom-select"></div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="card-panel">
|
||||
<h3>账户安全</h3>
|
||||
<form id="reset-password-form" style="margin-top: 24px;">
|
||||
|
|
|
|||
2
go.mod
2
go.mod
|
|
@ -4,9 +4,11 @@ go 1.24.4
|
|||
|
||||
require (
|
||||
github.com/BurntSushi/toml v1.5.0
|
||||
github.com/fenthope/compress v0.0.3
|
||||
github.com/fenthope/record v0.0.3
|
||||
github.com/fenthope/sessions v0.0.1
|
||||
github.com/infinite-iroha/touka v0.2.2
|
||||
github.com/klauspost/compress v1.18.0
|
||||
golang.org/x/crypto v0.39.0
|
||||
modernc.org/sqlite v1.38.0
|
||||
)
|
||||
|
|
|
|||
4
go.sum
4
go.sum
|
|
@ -6,6 +6,8 @@ github.com/WJQSERVER-STUDIO/httpc v0.7.1 h1:D3NlfY52pwKIOSzkdRrLinUynyKELrcPZEO8
|
|||
github.com/WJQSERVER-STUDIO/httpc v0.7.1/go.mod h1:M7KNUZjjhCkzzcg9lBPs9YfkImI+7vqjAyjdA19+joE=
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/fenthope/compress v0.0.3 h1:HerAPZjRwpXzhnC5iunUE0rb1CtcDkAvQHNtKtLH5Ec=
|
||||
github.com/fenthope/compress v0.0.3/go.mod h1:/3+aXXRWs9HOOf7fe1m4UhV04/aHco8YxuxeXJeWlzE=
|
||||
github.com/fenthope/reco v0.0.3 h1:RmnQ0D9a8PWtwOODawitTe4BztTnS9wYwrDbipISNq4=
|
||||
github.com/fenthope/reco v0.0.3/go.mod h1:mDkGLHte5udWTIcjQTxrABRcf56SSdxBOCLgrRDwI/Y=
|
||||
github.com/fenthope/record v0.0.3 h1:v5urgs5LAkLMlljAT/MjW8fWuRHXPnAraTem5ui7rm4=
|
||||
|
|
@ -26,6 +28,8 @@ github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kX
|
|||
github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo=
|
||||
github.com/gorilla/sessions v1.4.0 h1:kpIYOp/oi6MG/p5PgxApU8srsSw9tuFbt46Lt7auzqQ=
|
||||
github.com/gorilla/sessions v1.4.0/go.mod h1:FLWm50oby91+hl7p/wRxDth9bWSuk0qVL2emc7lT5ik=
|
||||
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
||||
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
|
||||
|
|
|
|||
33
main.go
33
main.go
|
|
@ -15,10 +15,12 @@ import (
|
|||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/fenthope/compress"
|
||||
"github.com/fenthope/record"
|
||||
"github.com/fenthope/sessions"
|
||||
"github.com/fenthope/sessions/cookie"
|
||||
"github.com/infinite-iroha/touka"
|
||||
"github.com/klauspost/compress/zstd"
|
||||
_ "modernc.org/sqlite"
|
||||
)
|
||||
|
||||
|
|
@ -117,6 +119,37 @@ func main() {
|
|||
r := touka.Default()
|
||||
r.Use(record.Middleware())
|
||||
|
||||
r.Use(compress.Compression(compress.CompressOptions{
|
||||
// Algorithms: 配置每种压缩算法的级别和是否启用对象池
|
||||
Algorithms: map[string]compress.AlgorithmConfig{
|
||||
compress.EncodingGzip: {
|
||||
Level: -1, // Gzip最高压缩比
|
||||
PoolEnabled: true, // 启用Gzip压缩器的对象池
|
||||
},
|
||||
compress.EncodingDeflate: {
|
||||
Level: -1, // Deflate默认压缩比
|
||||
PoolEnabled: false, // Deflate不启用对象池
|
||||
},
|
||||
compress.EncodingZstd: {
|
||||
Level: int(zstd.SpeedBestCompression), // Zstandard最佳压缩比
|
||||
PoolEnabled: true, // 启用Zstandard压缩器的对象池
|
||||
},
|
||||
},
|
||||
|
||||
// MinContentLength: 响应内容达到此字节数才进行压缩 (例如 1KB)
|
||||
MinContentLength: 512,
|
||||
|
||||
// CompressibleTypes: 只有响应的 Content-Type 匹配此列表中的MIME类型前缀才进行压缩
|
||||
CompressibleTypes: compress.DefaultCompressibleTypes,
|
||||
|
||||
// EncodingPriority: 当客户端接受多种支持的压缩算法时,服务器选择的优先级顺序
|
||||
EncodingPriority: []string{
|
||||
compress.EncodingZstd,
|
||||
compress.EncodingGzip,
|
||||
compress.EncodingDeflate,
|
||||
},
|
||||
}))
|
||||
|
||||
store := cookie.NewStore(sessionKey)
|
||||
store.Options(sessions.Options{
|
||||
Path: "/",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue