Compare commits

..

No commits in common. "6606e100c99dc819afaf55f37295cd67787efc1a" and "f40c01b078c35ad1577af8393ca7d3cf0ebfb430" have entirely different histories.

7 changed files with 76 additions and 437 deletions

View File

@ -1,7 +1,7 @@
{ {
"manifest_version": 3, "manifest_version": 3,
"name": "渠道应用开发部知识库", "name": "渠道应用开发部知识库",
"version": "2.0.1", "version": "2.0.0",
"description": "通过关键词快速搜索内部文档", "description": "通过关键词快速搜索内部文档",
"permissions": ["storage"], "permissions": ["storage"],
"host_permissions": [ "host_permissions": [

View File

@ -1,4 +1,3 @@
<!-- popup.html -->
<!DOCTYPE html> <!DOCTYPE html>
<html lang="zh-CN"> <html lang="zh-CN">
<head> <head>
@ -19,7 +18,6 @@
<div class="user-avatar" id="userAvatar">👤</div> <div class="user-avatar" id="userAvatar">👤</div>
<div class="user-dropdown hidden" id="userDropdown"> <div class="user-dropdown hidden" id="userDropdown">
<div id="userName">未登录</div> <div id="userName">未登录</div>
<div id="settingsBtn" class="logout-option">设置</div>
<div id="logoutBtn" class="logout-option">退出登录</div> <div id="logoutBtn" class="logout-option">退出登录</div>
</div> </div>
</div> </div>
@ -57,12 +55,12 @@
</div> </div>
</div> </div>
<!-- 脚本加载顺序不可错乱 --> <!-- JS模块加载顺序非常重要请保持此顺序 -->
<script src="../scripts/config.js"></script>
<script src="../scripts/utils.js"></script> <script src="../scripts/utils.js"></script>
<script src="../scripts/auth.js"></script> <script src="../scripts/auth.js"></script>
<script src="../scripts/llm.js"></script> <script src="../scripts/llm.js"></script>
<script src="../scripts/popup.js"></script> <script src="../scripts/popup.js"></script>
<!-- 新增 marked.js CDN -->
<script src="../scripts/markdown-min.js"></script> <script src="../scripts/markdown-min.js"></script>
</body> </body>
</html> </html>

View File

@ -1,152 +0,0 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<title>插件设置</title>
<style>
body {
font-family: "Microsoft YaHei", sans-serif;
padding: 20px 30px;
max-width: 1000px;
margin: auto;
background-color: #f7f8fc;
color: #333;
}
h2 {
margin-top: 0;
}
.back-link {
display: inline-block;
margin-bottom: 20px;
font-size: 14px;
color: #2560de;
cursor: pointer;
}
.section {
margin-bottom: 30px;
background: #fff;
border: 1px solid #dfe4ed;
border-radius: 8px;
box-shadow: 0 2px 6px rgba(0,0,0,0.04);
padding: 20px 24px;
}
.section h3 {
margin: 0 0 10px;
font-size: 17px;
color: #1e3a8a;
}
.form-row {
margin-bottom: 14px;
}
.form-row label {
display: block;
font-weight: bold;
font-size: 13px;
margin-bottom: 4px;
}
.form-row input {
width: 100%;
padding: 8px;
font-size: 13px;
border: 1px solid #ccc;
border-radius: 5px;
box-sizing: border-box;
}
.api-block {
border-top: 1px solid #e3e7f1;
margin-top: 25px;
padding-top: 18px;
}
.api-name {
font-weight: bold;
font-size: 15px;
margin-bottom: 8px;
color: #2560de;
padding: 4px 8px;
background: #eef3fc;
border-left: 4px solid #2560de;
border-radius: 4px;
}
.param-group {
margin-top: 12px;
padding-left: 12px;
border-left: 3px solid #e0e8f8;
margin-bottom: 14px;
}
.param-group h4 {
font-size: 13px;
margin-bottom: 8px;
color: #444;
}
.btn-row {
display: flex;
justify-content: flex-end;
gap: 10px;
margin-top: 20px;
}
button {
padding: 8px 18px;
font-size: 14px;
border-radius: 5px;
cursor: pointer;
border: 1px solid #ccc;
}
#saveBtn {
background-color: #2560de;
color: white;
border: none;
}
#resetBtn {
background: #f8f8f8;
}
.desc {
font-size: 13px;
color: #666;
margin-top: 4px;
}
</style>
</head>
<body>
<div class="back-link" id="backToPopup">← 返回主界面</div>
<h2>插件设置</h2>
<div class="section">
<h3>通用设置</h3>
<div class="form-row">
<label for="tokenExpireHours">Token 有效期(小时,最大 168</label>
<input id="tokenExpireHours" type="number" min="1" max="168" />
<div class="desc">超出有效期将要求重新登录</div>
</div>
</div>
<div class="section" id="endpointSection">
<h3>接口配置</h3>
<!-- 动态插入接口配置 -->
</div>
<div class="btn-row">
<button id="resetBtn">恢复默认</button>
<button id="saveBtn">保存设置</button>
</div>
<script src="../scripts/config.js"></script>
<script src="../scripts/settings.js"></script>
</body>
</html>

View File

@ -1,99 +0,0 @@
const defaultConfig = {
tokenExpireHours: 24,
apiConfig: {
esSearch: {
name: "ES 搜索接口",
url: "http://app.wisdompulse.cn/search",
requestParams: [
{ label: "关键词组", field: "keywordGroups" },
{ label: "页码", field: "page" },
{ label: "分页大小", field: "size" }
],
responseParams: [
{ label: "结果字段", field: "results" },
{ label: "总条数字段", field: "total" },
{ label: "分页大小字段", field: "size" }
]
},
materialPublicUrl: {
name: "材料公开地址",
url: "http://share.wisdompulse.cn/public",
requestParams: [],
responseParams: []
},
login: {
name: "登录接口",
url: "http://app.wisdompulse.cn/api/v1/user/login",
requestParams: [
{ label: "用户名字段", field: "username" },
{ label: "密码字段", field: "password" }
],
responseParams: [
{ label: "Token 字段", field: "token" },
{ label: "用户名字段", field: "username" }
]
},
filePath: {
name: "获取文件路径接口",
url: "http://app.wisdompulse.cn/search/file-path",
requestParams: [
{ label: "文件名字段", field: "filename" }
],
responseParams: [
{ label: "路径字段", field: "filepath" }
]
},
llmToken: {
name: "获取 AnythingLLM Token 接口",
url: "http://llm.wisdompulse.cn/api/request-token",
requestParams: [
{ label: "用户名字段", field: "username" }
],
responseParams: [
{ label: "Token 字段", field: "token" }
]
},
workspaces: {
name: "获取工作区接口",
url: "http://llm.wisdompulse.cn/api/workspaces",
requestParams: [],
responseParams: [
{ label: "工作区数组字段", field: "workspaces" }
]
},
llmChat: {
name: "指定工作区对话接口",
url: "http://llm.wisdompulse.cn/api/workspace/${workId}/stream-chat",
requestParams: [
{ label: "问题字段", field: "prompt" },
{ label: "历史字段", field: "messages" }
],
responseParams: []
}
}
};
function loadMergedConfig() {
return new Promise((resolve) => {
chrome.storage.local.get(["customConfig"], (res) => {
const merged = JSON.parse(JSON.stringify(defaultConfig));
if (res.customConfig && typeof res.customConfig === "object") {
merged.tokenExpireHours = res.customConfig.tokenExpireHours || merged.tokenExpireHours;
for (const key in merged.apiConfig) {
const override = res.customConfig.apiConfig?.[key];
if (override) {
merged.apiConfig[key].url = override.url || merged.apiConfig[key].url;
merged.apiConfig[key].requestParams.forEach((param, i) => {
if (override.requestParams?.[i]?.field) param.field = override.requestParams[i].field;
});
merged.apiConfig[key].responseParams.forEach((param, i) => {
if (override.responseParams?.[i]?.field) param.field = override.responseParams[i].field;
});
}
}
}
resolve(merged);
});
});
}

View File

@ -1,8 +1,10 @@
document.addEventListener("DOMContentLoaded", async () => { document.addEventListener("DOMContentLoaded", async () => {
log("DOMContentLoaded - 页面加载完成");
const panel = document.getElementById("draggablePanel"); const panel = document.getElementById("draggablePanel");
const header = document.querySelector(".header"); const header = document.querySelector(".header");
if (window.innerWidth > 900 && window.innerHeight > 700) { if (window.innerWidth > 900 && window.innerHeight > 700) {
log("启用全屏模式");
document.body.classList.add("fullscreen"); document.body.classList.add("fullscreen");
if (panel) { if (panel) {
panel.style.left = ""; panel.style.left = "";
@ -10,6 +12,7 @@ document.addEventListener("DOMContentLoaded", async () => {
panel.style.position = "fixed"; panel.style.position = "fixed";
} }
} else if (panel && header) { } else if (panel && header) {
log("启用可拖拽模式");
let dragging = false, offsetX = 0, offsetY = 0; let dragging = false, offsetX = 0, offsetY = 0;
header.addEventListener("mousedown", (e) => { header.addEventListener("mousedown", (e) => {
dragging = true; dragging = true;
@ -29,6 +32,7 @@ document.addEventListener("DOMContentLoaded", async () => {
}); });
} }
// ====== 元素选择 ======
const input = document.getElementById("searchInput"); const input = document.getElementById("searchInput");
const searchBtn = document.getElementById("searchBtn"); const searchBtn = document.getElementById("searchBtn");
const clearBtn = document.getElementById("clearHistoryBtn"); const clearBtn = document.getElementById("clearHistoryBtn");
@ -44,20 +48,22 @@ document.addEventListener("DOMContentLoaded", async () => {
const userAvatar = document.getElementById("userAvatar"); const userAvatar = document.getElementById("userAvatar");
const userDropdown = document.getElementById("userDropdown"); const userDropdown = document.getElementById("userDropdown");
const openTabBtn = document.getElementById("openTabBtn"); const openTabBtn = document.getElementById("openTabBtn");
const settingsBtn = document.getElementById("settingsBtn");
const tabEs = document.getElementById("tabEs"); const tabEs = document.getElementById("tabEs");
const tabAi = document.getElementById("tabAi"); const tabAi = document.getElementById("tabAi");
const tabContentEs = document.getElementById("tabContentEs"); const tabContentEs = document.getElementById("tabContentEs");
const tabContentAi = document.getElementById("tabContentAi"); const tabContentAi = document.getElementById("tabContentAi");
// ====== 标签切换 ======
tabEs.onclick = () => { tabEs.onclick = () => {
log("切换到 ES 结果页");
tabEs.classList.add("active"); tabEs.classList.add("active");
tabAi.classList.remove("active"); tabAi.classList.remove("active");
tabContentEs.classList.add("active"); tabContentEs.classList.add("active");
tabContentAi.classList.remove("active"); tabContentAi.classList.remove("active");
}; };
tabAi.onclick = () => { tabAi.onclick = () => {
log("切换到 LLM 结果页");
tabEs.classList.remove("active"); tabEs.classList.remove("active");
tabAi.classList.add("active"); tabAi.classList.add("active");
tabContentEs.classList.remove("active"); tabContentEs.classList.remove("active");
@ -67,22 +73,35 @@ document.addEventListener("DOMContentLoaded", async () => {
}, 100); }, 100);
}; };
const isNewTab = new URLSearchParams(window.location.search).get("from") === "newtab"; function getQueryParam(name) {
const url = new URL(window.location.href);
return url.searchParams.get(name);
}
const isNewTab = getQueryParam("from") === "newtab";
let token = null; let token = null;
let currentPage = 1; let currentPage = 1;
let totalPages = 1; let totalPages = 1;
let lastKeywords = []; let lastKeywords = [];
let config = await loadMergedConfig();
const auth = await loadToken(); const auth = await loadToken();
if (!auth) return handleLogout(); if (!auth) {
log("用户未认证,执行登出");
return handleLogout();
}
token = auth.token; token = auth.token;
userNameLabel.textContent = auth.username; userNameLabel.textContent = auth.username;
log("用户认证成功,用户名:", auth.username);
async function triggerSearch() { async function triggerSearch() {
const raw = input?.value?.trim(); const raw = input?.value?.trim();
if (!token || !raw) return; if (!token || !raw) {
log("取消搜索token 丢失或关键词为空");
return;
}
log("执行 triggerSearch关键词:", raw, "是否新标签页:", isNewTab);
updateHistory(raw); updateHistory(raw);
renderHistoryTags(true); renderHistoryTags(true);
if (!isNewTab) { if (!isNewTab) {
@ -108,6 +127,7 @@ document.addEventListener("DOMContentLoaded", async () => {
} }
function clearResults() { function clearResults() {
log("清空结果和历史区域");
resultBox.innerHTML = ""; resultBox.innerHTML = "";
llmResultContent.innerHTML = ""; llmResultContent.innerHTML = "";
llmResultContainer.classList.add("hidden"); llmResultContainer.classList.add("hidden");
@ -135,20 +155,18 @@ document.addEventListener("DOMContentLoaded", async () => {
async function fetchResults(rawInput, page) { async function fetchResults(rawInput, page) {
currentPage = page; currentPage = page;
lastKeywords = rawInput; lastKeywords = rawInput;
log("调用 fetchResults关键词:", rawInput, "页码:", page);
showLoading(); showLoading();
const endpoint = config.apiConfig.esSearch;
const [kwField, pageField, sizeField] = endpoint.requestParams.map(p => p.field);
const [resultsField, totalField, sizeField2] = endpoint.responseParams.map(p => p.field);
const keywordGroups = parseKeywordGroups(rawInput); const keywordGroups = parseKeywordGroups(rawInput);
const requestBody = { const requestBody = {
[kwField]: keywordGroups, keywordGroups,
[pageField]: page, page: currentPage,
[sizeField]: 10 size: 10
}; };
try { try {
const response = await fetch(endpoint.url, { const response = await fetch("http://app.wisdompulse.cn/search", {
method: "POST", method: "POST",
headers: { headers: {
"Authorization": `${token}`, "Authorization": `${token}`,
@ -157,30 +175,31 @@ document.addEventListener("DOMContentLoaded", async () => {
body: JSON.stringify(requestBody) body: JSON.stringify(requestBody)
}); });
if (response.status === 401) return handleLogout(); log("搜索接口响应状态:", response.status);
const data = await response.json();
const results = data[resultsField];
const total = data[totalField];
const pageSize = data[sizeField2] || 10;
if (response.status === 401) {
log("token 已失效,执行登出");
return handleLogout();
}
const data = await response.json();
resultBox.innerHTML = ""; resultBox.innerHTML = "";
if (!results || results.length === 0) { if (!data.results || data.results.length === 0) {
log("无搜索结果");
resultBox.innerHTML = "<li>未找到相关文档</li>"; resultBox.innerHTML = "<li>未找到相关文档</li>";
return; return;
} }
totalPages = Math.ceil(total / pageSize); totalPages = Math.ceil(data.total / data.size);
const fileBaseUrl = config.apiConfig.materialPublicUrl.url;
results.forEach(doc => { data.results.forEach(doc => {
const li = document.createElement("li"); const li = document.createElement("li");
const filename = doc.filename || doc.title || "未知文件"; const path = doc.filepath.replace(/^import-data\//, "").replace(/\.md$/, ".html");
const filepath = doc.filepath?.replace(/^import-data\//, "").replace(/\.md$/, ".html") || "";
const link = document.createElement("a"); const link = document.createElement("a");
link.href = `${fileBaseUrl}/${filepath || filename}`; link.href = `http://share.wisdompulse.cn/public/${path}`;
link.target = "_blank"; link.target = "_blank";
link.textContent = filename; link.textContent = doc.filename;
const snippet = document.createElement("div"); const snippet = document.createElement("div");
snippet.className = "summary"; snippet.className = "summary";
@ -193,11 +212,13 @@ document.addEventListener("DOMContentLoaded", async () => {
renderPagination(); renderPagination();
} catch (err) { } catch (err) {
log("搜索请求异常:", err);
resultBox.innerHTML = "<li>搜索失败,请检查网络或服务状态。</li>"; resultBox.innerHTML = "<li>搜索失败,请检查网络或服务状态。</li>";
} }
} }
function updateHistory(keyword) { function updateHistory(keyword) {
log("更新搜索历史:", keyword);
chrome.storage.local.get(["searchHistory"], (res) => { chrome.storage.local.get(["searchHistory"], (res) => {
let history = res.searchHistory || []; let history = res.searchHistory || [];
history = [keyword, ...history.filter(item => item !== keyword)]; history = [keyword, ...history.filter(item => item !== keyword)];
@ -207,6 +228,7 @@ document.addEventListener("DOMContentLoaded", async () => {
} }
function renderHistoryTags(show = false) { function renderHistoryTags(show = false) {
log("渲染历史标签,是否显示:", show);
chrome.storage.local.get(["searchHistory"], (res) => { chrome.storage.local.get(["searchHistory"], (res) => {
const history = res.searchHistory || []; const history = res.searchHistory || [];
historyBox.innerHTML = ""; historyBox.innerHTML = "";
@ -231,7 +253,11 @@ document.addEventListener("DOMContentLoaded", async () => {
}); });
} }
searchBtn.addEventListener("click", triggerSearch); // ====== DOM 事件绑定 ======
searchBtn.addEventListener("click", () => {
log("点击搜索按钮");
triggerSearch();
});
input.addEventListener("keydown", e => { input.addEventListener("keydown", e => {
if (e.key === "Enter") { if (e.key === "Enter") {
e.preventDefault(); e.preventDefault();
@ -240,19 +266,25 @@ document.addEventListener("DOMContentLoaded", async () => {
}); });
input.addEventListener("focus", () => renderHistoryTags(true)); input.addEventListener("focus", () => renderHistoryTags(true));
input.addEventListener("input", () => { input.addEventListener("input", () => {
if (!input.value.trim()) clearResults(); if (!input.value.trim()) {
log("输入清空,清除结果");
clearResults();
}
}); });
input.addEventListener("blur", () => setTimeout(() => historyWrapper.classList.add("hidden"), 200)); input.addEventListener("blur", () => setTimeout(() => historyWrapper.classList.add("hidden"), 200));
clearBtn.addEventListener("click", () => { clearBtn.addEventListener("click", () => {
log("点击清空历史按钮");
chrome.storage.local.remove("searchHistory", () => renderHistoryTags()); chrome.storage.local.remove("searchHistory", () => renderHistoryTags());
}); });
logoutBtn?.addEventListener("click", () => handleLogout()); logoutBtn?.addEventListener("click", () => {
userAvatar?.addEventListener("click", () => { log("点击退出登录按钮");
userDropdown.classList.toggle("hidden"); handleLogout();
}); });
settingsBtn?.addEventListener("click", () => {
chrome.tabs.create({ url: chrome.runtime.getURL("pages/settings.html") }); userAvatar?.addEventListener("click", () => {
log("点击用户头像,切换菜单");
userDropdown.classList.toggle("hidden");
}); });
window.addEventListener("click", (e) => { window.addEventListener("click", (e) => {
@ -265,6 +297,7 @@ document.addEventListener("DOMContentLoaded", async () => {
const val = llmInput.value.trim(); const val = llmInput.value.trim();
if (!val) return; if (!val) return;
llmInput.value = ""; llmInput.value = "";
log("发送 LLM 问题:", val);
fetchLLMAnswer(val, llmResultContainer, llmResultContent); fetchLLMAnswer(val, llmResultContainer, llmResultContent);
}); });
@ -275,16 +308,18 @@ document.addEventListener("DOMContentLoaded", async () => {
} }
}); });
openTabBtn.addEventListener("click", () => { openTabBtn.onclick = () => {
const val = input?.value?.trim(); const val = input?.value?.trim();
const encoded = encodeURIComponent(val || ""); const encoded = encodeURIComponent(val || "");
const url = chrome.runtime.getURL("pages/popup.html?keywords=" + encoded + "&from=newtab"); const url = chrome.runtime.getURL("pages/popup.html?keywords=" + encoded + "&from=newtab");
chrome.tabs.create({ url }); log("点击新标签页按钮,跳转到:", url);
}); chrome.tabs?.create({ url });
};
const autoKeywords = new URLSearchParams(location.search).get("keywords"); const autoKeywords = getQueryParam("keywords");
if (autoKeywords) { if (autoKeywords) {
input.value = decodeURIComponent(autoKeywords); input.value = decodeURIComponent(autoKeywords);
log("检测到 URL 参数 keywords自动搜索:", input.value);
triggerSearch(); triggerSearch();
} }

View File

@ -1,125 +0,0 @@
// scripts/settings.js
function getMergedConfig(callback) {
chrome.storage.local.get(["customConfig"], (res) => {
const merged = JSON.parse(JSON.stringify(defaultConfig));
if (res.customConfig && typeof res.customConfig === "object") {
merged.tokenExpireHours = res.customConfig.tokenExpireHours || merged.tokenExpireHours;
for (const key in merged.apiConfig) {
const override = res.customConfig.apiConfig?.[key];
if (override) {
merged.apiConfig[key].url = override.url || merged.apiConfig[key].url;
merged.apiConfig[key].requestParams.forEach((param, i) => {
if (override.requestParams?.[i]?.field) param.field = override.requestParams[i].field;
});
merged.apiConfig[key].responseParams.forEach((param, i) => {
if (override.responseParams?.[i]?.field) param.field = override.responseParams[i].field;
});
}
}
}
callback(merged);
});
}
function renderConfig(config) {
document.getElementById("tokenExpireHours").value = config.tokenExpireHours;
const container = document.getElementById("endpointSection");
container.innerHTML = "";
for (const [key, endpoint] of Object.entries(config.apiConfig)) {
const block = document.createElement("div");
block.className = "api-block";
block.innerHTML += `<div class="api-name">【${endpoint.name}】</div>`;
const urlRow = document.createElement("div");
urlRow.className = "form-row";
urlRow.innerHTML = `
<label>接口地址</label>
<input data-key="${key}" data-type="url" value="${endpoint.url}" />
`;
block.appendChild(urlRow);
const reqGroup = document.createElement("div");
reqGroup.className = "param-group";
reqGroup.innerHTML = `<h4>请求参数</h4>`;
endpoint.requestParams.forEach((param, index) => {
const row = document.createElement("div");
row.className = "form-row";
row.innerHTML = `
<label>${param.label}</label>
<input data-key="${key}" data-type="requestParams" data-index="${index}" value="${param.field}" />
`;
reqGroup.appendChild(row);
});
block.appendChild(reqGroup);
const resGroup = document.createElement("div");
resGroup.className = "param-group";
resGroup.innerHTML = `<h4>响应字段</h4>`;
endpoint.responseParams.forEach((param, index) => {
const row = document.createElement("div");
row.className = "form-row";
row.innerHTML = `
<label>${param.label}</label>
<input data-key="${key}" data-type="responseParams" data-index="${index}" value="${param.field}" />
`;
resGroup.appendChild(row);
});
block.appendChild(resGroup);
container.appendChild(block);
}
}
function collectUserConfig() {
const tokenExpireHours = Math.min(168, parseInt(document.getElementById("tokenExpireHours").value) || 24);
const apiConfig = {};
const inputs = document.querySelectorAll("input[data-key]");
for (const input of inputs) {
const key = input.dataset.key;
const type = input.dataset.type;
const index = input.dataset.index;
if (!apiConfig[key]) {
apiConfig[key] = { requestParams: [], responseParams: [] };
}
if (type === "url") {
apiConfig[key].url = input.value.trim();
} else if (type === "requestParams") {
apiConfig[key].requestParams[index] = { field: input.value.trim() };
} else if (type === "responseParams") {
apiConfig[key].responseParams[index] = { field: input.value.trim() };
}
}
return { tokenExpireHours, apiConfig };
}
document.getElementById("saveBtn").addEventListener("click", () => {
const newConfig = collectUserConfig();
chrome.storage.local.set({ customConfig: newConfig }, () => {
alert("设置已保存!");
});
});
document.getElementById("resetBtn").addEventListener("click", () => {
chrome.storage.local.remove("customConfig", () => {
alert("已恢复默认设置,刷新页面以生效");
location.reload();
});
});
document.addEventListener("DOMContentLoaded", () => {
getMergedConfig(renderConfig);
const backBtn = document.getElementById("backToPopup");
backBtn.addEventListener("click", () => {
const popupUrl = chrome.runtime.getURL("pages/popup.html");
chrome.tabs.create({ url: popupUrl });
});
});

View File

@ -203,21 +203,3 @@ h1 {
background: #eaf1fb; background: #eaf1fb;
color: #1e40af; color: #1e40af;
} }
body.fullscreen .draggable-panel {
position: fixed;
display: flex;
flex-direction: column;
height: 94vh;
max-height: 98vh;
overflow: hidden;
}
body.fullscreen .container {
flex: 1 1 auto;
overflow-y: auto;
padding: 0 24px 30px;
box-sizing: border-box;
display: flex;
flex-direction: column;
}