Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a3d2398f83 | |||
| 80291c6a6a | |||
| 1ff3d28ff7 | |||
| 3d7ed2c16b | |||
| ade2c6bea8 | |||
| 2f4b0f8b76 | |||
| 9fcdea0061 | |||
| d7d8e2aa77 | |||
| cfe07c9df5 | |||
| 5eb49defe1 | |||
| 00e8eed191 | |||
| aeec3e4f0d | |||
| 251a13c368 | |||
| 73a51fcffc | |||
| b75a159d70 |
@@ -366,6 +366,18 @@ class System
|
||||
'url' => (string)url('/index/topic/laptop/index')
|
||||
],
|
||||
]
|
||||
],
|
||||
[
|
||||
'id' => 9,
|
||||
'name' => '闪存(SSD)专题',
|
||||
'url' => '',
|
||||
'children' => [
|
||||
[
|
||||
'id' => 101,
|
||||
'name' => '首页',
|
||||
'url' => (string)url('/index/topic/ssd/index')
|
||||
],
|
||||
]
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
52
app/index/controller/TopicSsd.php
Normal file
52
app/index/controller/TopicSsd.php
Normal file
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
declare (strict_types = 1);
|
||||
|
||||
namespace app\index\controller;
|
||||
|
||||
use app\index\model\SysBannerModel;
|
||||
use think\facade\View;
|
||||
|
||||
/**
|
||||
* 专题 - SSD
|
||||
*/
|
||||
class TopicSsd extends Common
|
||||
{
|
||||
/**
|
||||
* 专题 - SSD首页
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
$banners = SysBannerModel::with([
|
||||
'items' => function ($query) {
|
||||
$query->withoutField(['sort', 'created_at', 'updated_at', 'deleted_at'])
|
||||
->order(['sort' => 'asc', 'id' => 'desc'])
|
||||
->enabled(true);
|
||||
}
|
||||
])
|
||||
->atPlatform(request()->from)
|
||||
->uniqueLabel([
|
||||
'BANNER_69faaf8582967', // 专题 - 闪存(SSD)首页 - 焦点图
|
||||
'BANNER_69fab1bed8f71', // 专题 - 闪存(SSD)首页 - 产品
|
||||
])
|
||||
->language($this->lang_id)
|
||||
->enabled(true)
|
||||
->order(['sort' => 'asc', 'id' => 'desc'])
|
||||
->select();
|
||||
|
||||
$data = [];
|
||||
if (!$banners->isEmpty()) {
|
||||
$banners_map = [];
|
||||
foreach ($banners as $banner) {
|
||||
$banners_map[$banner->unique_label] = $banner;
|
||||
}
|
||||
|
||||
// 焦点图轮播图
|
||||
$data['top_focus_images'] = data_get($banners_map, 'BANNER_69faaf8582967')?->items->toArray();
|
||||
// 产品
|
||||
$data['products'] = data_get($banners_map, 'BANNER_69fab1bed8f71')?->items->toArray();
|
||||
}
|
||||
View::assign('data', $data);
|
||||
|
||||
return View::fetch('index');
|
||||
}
|
||||
}
|
||||
@@ -119,6 +119,12 @@ Route::group('topic', function () {
|
||||
// 专题 - 笔记本电脑首页
|
||||
Route::get('index', 'TopicLaptop/index');
|
||||
});
|
||||
|
||||
// 专题 - SSD
|
||||
Route::group('ssd', function() {
|
||||
// 专题 - SSD首页
|
||||
Route::get('index', 'TopicSsd/index');
|
||||
});
|
||||
});
|
||||
|
||||
// 数据迁移
|
||||
|
||||
43
app/index/view/mobile/topic_ssd/index.html
Normal file
43
app/index/view/mobile/topic_ssd/index.html
Normal file
@@ -0,0 +1,43 @@
|
||||
{extend name="public/base" /}
|
||||
{block name="style"}
|
||||
<link rel="stylesheet" href="__CSS__/topic_laptop/header.css">
|
||||
<link rel="stylesheet" href="__CSS__/topic_laptop/footer.css">
|
||||
<link rel="stylesheet" href="__CSS__/topic_ssd/index.css">
|
||||
<!-- 将rem适配JS移到这里,确保优先执行 -->
|
||||
<script type="text/javascript">
|
||||
(function (doc, win)
|
||||
{
|
||||
var docEl = doc.documentElement;
|
||||
var resizeEvt = 'orientationchange' in window ? 'orientationchange' : 'resize';
|
||||
|
||||
function setRootFontSize ()
|
||||
{
|
||||
var clientWidth = docEl.clientWidth;
|
||||
if (!clientWidth) return;
|
||||
var fontSize = clientWidth / 7.5;
|
||||
// 直接修改内联样式,优先级最高
|
||||
docEl.setAttribute('style', 'font-size: ' + fontSize + 'px !important;');
|
||||
}
|
||||
|
||||
setRootFontSize();
|
||||
win.addEventListener(resizeEvt, setRootFontSize);
|
||||
doc.addEventListener('DOMContentLoaded', setRootFontSize);
|
||||
})(document, window);
|
||||
</script>
|
||||
{/block}
|
||||
{block name="main"}
|
||||
<div class="m-sc-main" style="margin-top:42px;">
|
||||
{volist name="data.top_focus_images" id="item"}
|
||||
<a href="{$item.link}" class="m-sc-mt20">
|
||||
<img src="{$item.image}" alt="" class="m-sc-main-img" loading="lazy">
|
||||
</a>
|
||||
{/volist}
|
||||
<div class="m-sc-main-imgs m-sc-mt20 m-sc-mb34">
|
||||
{volist name="data.products" id="item"}
|
||||
<a href="{$item.link}" class="">
|
||||
<img src="{$item.image}" alt="" class="m-sc-main-img1" loading="lazy">
|
||||
</a>
|
||||
{/volist}
|
||||
</div>
|
||||
</div>
|
||||
{/block}
|
||||
@@ -350,7 +350,6 @@
|
||||
if (mhk && mhk.style.display !== 'block') {
|
||||
// 保存当前滚动位置
|
||||
scrollTopPosition = window.pageYOffset || document.documentElement.scrollTop;
|
||||
|
||||
mhk.style.display = 'block';
|
||||
|
||||
// 禁止滚动,保持位置
|
||||
@@ -377,76 +376,18 @@
|
||||
hasNavOpen = true;
|
||||
}
|
||||
});
|
||||
|
||||
if (!isBuyOpen && !isSearchOpen && !isLangOpen && !hasNavOpen) {
|
||||
mhk.style.display = 'none';
|
||||
|
||||
// 恢复滚动
|
||||
body.style.position = '';
|
||||
body.style.top = '';
|
||||
body.style.left = '';
|
||||
body.style.right = '';
|
||||
body.style.width = '';
|
||||
|
||||
// 恢复滚动位置
|
||||
window.scrollTo(0, scrollTopPosition);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ========== header-dropdown 导航下拉菜单打开时显示蒙版 ==========
|
||||
function initNavDropdownOverlay() {
|
||||
const navItems = document.querySelectorAll('.header-nav-item');
|
||||
let hideTimer = null;
|
||||
|
||||
navItems.forEach(item => {
|
||||
const dropdown = item.querySelector('.header-dropdown');
|
||||
if (!dropdown) return;
|
||||
|
||||
// 鼠标移入导航项
|
||||
item.addEventListener('mouseenter', () => {
|
||||
if (hideTimer) clearTimeout(hideTimer);
|
||||
openOverlay();
|
||||
});
|
||||
|
||||
// 鼠标移出导航项
|
||||
item.addEventListener('mouseleave', () => {
|
||||
hideTimer = setTimeout(() => {
|
||||
const isHoveringDropdown = dropdown && dropdown.matches(':hover');
|
||||
if (!isHoveringDropdown) {
|
||||
const isBuyOpen = buyDropdown && buyDropdown.classList.contains('show');
|
||||
const isSearchOpen = searchDropdown && searchDropdown.classList.contains('show');
|
||||
const isLangOpen = langDropdown && langDropdown.classList.contains('show');
|
||||
if (!isBuyOpen && !isSearchOpen && !isLangOpen) {
|
||||
closeOverlay();
|
||||
}
|
||||
}
|
||||
}, 100);
|
||||
});
|
||||
|
||||
// 鼠标移入下拉菜单
|
||||
dropdown.addEventListener('mouseenter', () => {
|
||||
if (hideTimer) clearTimeout(hideTimer);
|
||||
openOverlay();
|
||||
});
|
||||
|
||||
// 鼠标移出下拉菜单
|
||||
dropdown.addEventListener('mouseleave', () => {
|
||||
hideTimer = setTimeout(() => {
|
||||
const isHoveringNav = item && item.matches(':hover');
|
||||
if (!isHoveringNav) {
|
||||
const isBuyOpen = buyDropdown && buyDropdown.classList.contains('show');
|
||||
const isSearchOpen = searchDropdown && searchDropdown.classList.contains('show');
|
||||
const isLangOpen = langDropdown && langDropdown.classList.contains('show');
|
||||
if (!isBuyOpen && !isSearchOpen && !isLangOpen) {
|
||||
closeOverlay();
|
||||
}
|
||||
}
|
||||
}, 100);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 关闭所有导航下拉菜单
|
||||
function closeAllNavDropdowns() {
|
||||
document.querySelectorAll('.header-dropdown').forEach(dropdown => {
|
||||
@@ -604,15 +545,13 @@
|
||||
setupDropdownObserver(searchDropdown);
|
||||
setupDropdownObserver(langDropdown);
|
||||
|
||||
// ========== header-dropdown JS 控制版本(带防抖) ==========
|
||||
function initHeaderDropdown() {
|
||||
const navItems = document.querySelectorAll('.header-nav-item');
|
||||
let hoverTimer = null;
|
||||
let leaveTimer = null;
|
||||
let currentOpenDropdown = null;
|
||||
// ========== 监听下拉菜单打开状态,自动控制蒙版 ==========
|
||||
|
||||
// 获取所有下拉菜单并设置初始状态
|
||||
function initHeaderDropdown() {
|
||||
// 获取所有下拉菜单
|
||||
const allDropdowns = document.querySelectorAll('.header-dropdown');
|
||||
|
||||
// 设置初始样式
|
||||
allDropdowns.forEach(dropdown => {
|
||||
dropdown.style.opacity = '0';
|
||||
dropdown.style.transform = 'translateY(-1.25em)';
|
||||
@@ -620,115 +559,110 @@
|
||||
dropdown.style.transition = 'opacity 0.3s ease, transform 0.3s ease';
|
||||
});
|
||||
|
||||
// 显示下拉菜单
|
||||
function showDropdown(dropdown) {
|
||||
if (!dropdown) return;
|
||||
// 关闭当前打开的
|
||||
if (currentOpenDropdown && currentOpenDropdown !== dropdown) {
|
||||
hideDropdown(currentOpenDropdown);
|
||||
}
|
||||
dropdown.style.opacity = '1';
|
||||
dropdown.style.transform = 'translateY(0)';
|
||||
dropdown.style.pointerEvents = 'auto';
|
||||
currentOpenDropdown = dropdown;
|
||||
}
|
||||
|
||||
// 隐藏下拉菜单
|
||||
function hideDropdown(dropdown) {
|
||||
if (!dropdown) return;
|
||||
dropdown.style.opacity = '0';
|
||||
dropdown.style.transform = 'translateY(-1.25em)';
|
||||
dropdown.style.pointerEvents = 'none';
|
||||
if (currentOpenDropdown === dropdown) {
|
||||
currentOpenDropdown = null;
|
||||
}
|
||||
}
|
||||
|
||||
// 隐藏所有下拉菜单
|
||||
function hideAllDropdowns() {
|
||||
allDropdowns.forEach(dropdown => {
|
||||
dropdown.style.opacity = '0';
|
||||
dropdown.style.transform = 'translateY(-1.25em)';
|
||||
dropdown.style.pointerEvents = 'none';
|
||||
// 监听单个下拉菜单的样式变化
|
||||
function observeDropdown(dropdown) {
|
||||
const observer = new MutationObserver(() => {
|
||||
const isOpen = dropdown.style.opacity === '1';
|
||||
if (isOpen) {
|
||||
openOverlay();
|
||||
} else {
|
||||
let anyOpen = false;
|
||||
allDropdowns.forEach(d => {
|
||||
if (d.style.opacity === '1') anyOpen = true;
|
||||
});
|
||||
if (!anyOpen) {
|
||||
closeOverlay();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
observer.observe(dropdown, {
|
||||
attributes: true,
|
||||
attributeFilter: ['style']
|
||||
});
|
||||
currentOpenDropdown = null;
|
||||
}
|
||||
|
||||
// 为每个下拉菜单添加监听
|
||||
allDropdowns.forEach(dropdown => {
|
||||
observeDropdown(dropdown);
|
||||
});
|
||||
|
||||
// 鼠标悬停控制下拉菜单显示/隐藏
|
||||
const navItems = document.querySelectorAll('.header-nav-item');
|
||||
|
||||
navItems.forEach(item => {
|
||||
const dropdown = item.querySelector('.header-dropdown');
|
||||
if (!dropdown) return;
|
||||
|
||||
// 鼠标移入导航项
|
||||
let timer = null;
|
||||
let isHovering = false; // 增加悬停状态标记
|
||||
|
||||
// 显示下拉菜单的函数
|
||||
const showDropdown = () => {
|
||||
if (timer) clearTimeout(timer);
|
||||
if (dropdown.style.opacity === '1') return;
|
||||
dropdown.style.opacity = '1';
|
||||
dropdown.style.transform = 'translateY(0)';
|
||||
dropdown.style.pointerEvents = 'auto';
|
||||
};
|
||||
|
||||
// 隐藏下拉菜单的函数
|
||||
const hideDropdown = () => {
|
||||
if (timer) clearTimeout(timer);
|
||||
if (dropdown.style.opacity === '0') return;
|
||||
dropdown.style.opacity = '0';
|
||||
dropdown.style.transform = 'translateY(-1.25em)';
|
||||
dropdown.style.pointerEvents = 'none';
|
||||
};
|
||||
|
||||
item.addEventListener('mouseenter', () => {
|
||||
// 清除离开定时器
|
||||
if (leaveTimer) clearTimeout(leaveTimer);
|
||||
if (hoverTimer) clearTimeout(hoverTimer);
|
||||
isHovering = true;
|
||||
clearTimeout(timer);
|
||||
showDropdown();
|
||||
});
|
||||
|
||||
// 防抖:延迟100ms再显示,避免频繁触发
|
||||
hoverTimer = setTimeout(() => {
|
||||
// 关闭其他弹窗(搜索、语言、购买)
|
||||
if (searchDropdown) searchDropdown.classList.remove('show');
|
||||
if (langDropdown) langDropdown.classList.remove('show');
|
||||
if (buyDropdown) buyDropdown.classList.remove('show');
|
||||
item.addEventListener('mouseleave', (e) => {
|
||||
isHovering = false;
|
||||
// 检查鼠标是否移到了下拉菜单上
|
||||
const relatedTarget = e.relatedTarget;
|
||||
const isMovingToDropdown = dropdown.contains(relatedTarget);
|
||||
|
||||
showDropdown(dropdown);
|
||||
// 打开蒙版层
|
||||
if (typeof openOverlay === 'function') openOverlay();
|
||||
if (isMovingToDropdown) {
|
||||
// 如果移向下拉菜单,不清除,等待下拉菜单的 mouseenter
|
||||
return;
|
||||
}
|
||||
|
||||
timer = setTimeout(() => {
|
||||
if (!isHovering && !dropdown.matches(':hover')) {
|
||||
hideDropdown();
|
||||
}
|
||||
}, 100);
|
||||
});
|
||||
|
||||
// 鼠标移出导航项
|
||||
item.addEventListener('mouseleave', () => {
|
||||
// 清除移入定时器
|
||||
if (hoverTimer) clearTimeout(hoverTimer);
|
||||
|
||||
// 延迟300ms再隐藏,给用户移动到下拉菜单的时间
|
||||
leaveTimer = setTimeout(() => {
|
||||
// 检查鼠标是否在下拉菜单上
|
||||
const isHoveringDropdown = dropdown.matches(':hover');
|
||||
if (!isHoveringDropdown) {
|
||||
hideDropdown(dropdown);
|
||||
// 关闭蒙版层
|
||||
if (typeof closeOverlay === 'function') closeOverlay();
|
||||
}
|
||||
}, 300);
|
||||
});
|
||||
|
||||
// 鼠标移入下拉菜单
|
||||
dropdown.addEventListener('mouseenter', () => {
|
||||
if (leaveTimer) clearTimeout(leaveTimer);
|
||||
showDropdown(dropdown);
|
||||
isHovering = true;
|
||||
clearTimeout(timer);
|
||||
showDropdown();
|
||||
});
|
||||
|
||||
// 鼠标移出下拉菜单
|
||||
dropdown.addEventListener('mouseleave', () => {
|
||||
leaveTimer = setTimeout(() => {
|
||||
const isHoveringNav = item.matches(':hover');
|
||||
if (!isHoveringNav) {
|
||||
hideDropdown(dropdown);
|
||||
// 关闭蒙版层
|
||||
if (typeof closeOverlay === 'function') closeOverlay();
|
||||
dropdown.addEventListener('mouseleave', (e) => {
|
||||
isHovering = false;
|
||||
// 检查鼠标是否移回了导航项
|
||||
const relatedTarget = e.relatedTarget;
|
||||
const isMovingToNavItem = item.contains(relatedTarget);
|
||||
|
||||
if (isMovingToNavItem) {
|
||||
return;
|
||||
}
|
||||
|
||||
timer = setTimeout(() => {
|
||||
if (!isHovering && !item.matches(':hover')) {
|
||||
hideDropdown();
|
||||
}
|
||||
}, 200);
|
||||
}, 100);
|
||||
});
|
||||
});
|
||||
|
||||
// 点击其他地方关闭所有下拉菜单
|
||||
document.addEventListener('click', (e) => {
|
||||
if (!e.target.closest('.header-nav-item')) {
|
||||
hideAllDropdowns();
|
||||
if (typeof closeOverlay === 'function') closeOverlay();
|
||||
}
|
||||
});
|
||||
|
||||
// 暴露方法供外部调用
|
||||
window.headerDropdown = {
|
||||
hideAll: hideAllDropdowns,
|
||||
show: showDropdown,
|
||||
hide: hideDropdown
|
||||
};
|
||||
}
|
||||
|
||||
// 初始化(在 DOM 加载完成后执行)
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', initHeaderDropdown);
|
||||
@@ -750,8 +684,8 @@
|
||||
}
|
||||
});
|
||||
|
||||
// 初始化导航下拉菜单蒙版层
|
||||
initNavDropdownOverlay();
|
||||
// // 初始化导航下拉菜单蒙版层
|
||||
// initNavDropdownOverlay();
|
||||
|
||||
// 初始化渲染
|
||||
renderHistory();
|
||||
|
||||
44
app/index/view/pc/topic_ssd/index.html
Normal file
44
app/index/view/pc/topic_ssd/index.html
Normal file
@@ -0,0 +1,44 @@
|
||||
{extend name="public/base" /}
|
||||
{block name="style"}
|
||||
<link rel="stylesheet" href="__CSS__/topic_laptop/header.css">
|
||||
<link rel="stylesheet" href="__CSS__/topic_laptop/footer.css">
|
||||
<link rel="stylesheet" href="__CSS__/topic_ssd/index.css">
|
||||
<script type="text/javascript">
|
||||
(function (doc, win)
|
||||
{
|
||||
var docEl = doc.documentElement;
|
||||
var resizeEvt = 'orientationchange' in window ? 'orientationchange' : 'resize';
|
||||
var designWidth = 2560;
|
||||
var designRemPx = 100;
|
||||
function setRootFontSize ()
|
||||
{
|
||||
var clientWidth = docEl.clientWidth;
|
||||
if (!clientWidth) return;
|
||||
var fontSize = (clientWidth / designWidth) * designRemPx;
|
||||
fontSize = Math.max(fontSize, 20);
|
||||
fontSize = Math.min(fontSize, designRemPx);
|
||||
docEl.style.fontSize = fontSize + 'px';
|
||||
}
|
||||
|
||||
setRootFontSize();
|
||||
win.addEventListener(resizeEvt, setRootFontSize);
|
||||
doc.addEventListener('DOMContentLoaded', setRootFontSize);
|
||||
})(document, window);
|
||||
</script>
|
||||
{/block}
|
||||
{block name="main"}
|
||||
<div class="sc-main">
|
||||
{volist name="data.top_focus_images" id="item"}
|
||||
<a href="{$item.link}" class="sc-mt20">
|
||||
<img src="{$item.image}" alt="" class="sc-main-img" loading="lazy">
|
||||
</a>
|
||||
{/volist}
|
||||
<div class="sc-main-imgs sc-mt20 sc-mb-78">
|
||||
{volist name="data.products" id="item"}
|
||||
<a href="{$item.link}" class="">
|
||||
<img src="{$item.image}" alt="" class="sc-main-img1" loading="lazy">
|
||||
</a>
|
||||
{/volist}
|
||||
</div>
|
||||
</div>
|
||||
{/block}
|
||||
65
public/static/index/mobile/css/topic_ssd/index.css
Normal file
65
public/static/index/mobile/css/topic_ssd/index.css
Normal file
@@ -0,0 +1,65 @@
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
html {
|
||||
width: 100% !important;
|
||||
overflow-x: hidden;
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
max-width: 100vw !important;
|
||||
}
|
||||
body {
|
||||
width: 100%;
|
||||
background: #fff;
|
||||
overflow-x: hidden;
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
max-width: 100vw !important;
|
||||
}
|
||||
.m-sc-main {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
display: block;
|
||||
}
|
||||
|
||||
img {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.m-sc-main-img {
|
||||
width: 100%;
|
||||
/* max-width: 2560px; */
|
||||
}
|
||||
|
||||
.m-sc-mt20 {
|
||||
margin-top: 0.1rem;
|
||||
}
|
||||
.m-sc-mt20:first-child {
|
||||
margin-top: 0 !important;
|
||||
}
|
||||
.m-sc-mb34 {
|
||||
margin-bottom: 0.34rem;
|
||||
}
|
||||
.m-sc-main-imgs {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 0.1rem;
|
||||
list-style: none;
|
||||
/* 去除列表样式(如果有) */
|
||||
}
|
||||
|
||||
.m-sc-main-imgs a {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.m-sc-main-img1 {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
/* 保持比例 */
|
||||
display: block;
|
||||
}
|
||||
@@ -97,10 +97,11 @@ a {
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 0.125em;
|
||||
height: 2px;
|
||||
background-color: #004bfa;
|
||||
transform: scaleX(0);
|
||||
transition: transform 0.2s ease;
|
||||
z-index: 999;
|
||||
}
|
||||
|
||||
.header-nav-title:hover::after,
|
||||
@@ -147,12 +148,7 @@ a {
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
/* .header-nav-btn img {
|
||||
width: 1.5em;
|
||||
max-width: 17px;
|
||||
max-height: 17px;
|
||||
height: 1.5em;
|
||||
} */
|
||||
|
||||
|
||||
.header-nav-btn:hover {
|
||||
color: #004bfa;
|
||||
@@ -269,7 +265,7 @@ a {
|
||||
left: 0;
|
||||
right: 0;
|
||||
background-color: #fff;
|
||||
z-index: 999;
|
||||
z-index: 99;
|
||||
border-top: none;
|
||||
overflow: hidden;
|
||||
padding-bottom: 2.375em;
|
||||
|
||||
68
public/static/index/pc/css/topic_ssd/index.css
Normal file
68
public/static/index/pc/css/topic_ssd/index.css
Normal file
@@ -0,0 +1,68 @@
|
||||
|
||||
html {
|
||||
width: 100%;
|
||||
}
|
||||
body {
|
||||
/* width: 100vw;
|
||||
height: 100vh; */
|
||||
background: #fff;
|
||||
overflow-x: hidden;
|
||||
scroll-behavior: smooth !important;
|
||||
-webkit-overflow-scrolling: touch !important;
|
||||
}
|
||||
/* 当视口宽度大于1920px时生效 */
|
||||
@media screen and (min-width: 1920px) {
|
||||
/* 这里写你的样式 */
|
||||
body {
|
||||
max-width:100% !important;
|
||||
width: 100vw !important;
|
||||
}
|
||||
|
||||
}
|
||||
.sc-main {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
max-width: 2560px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
display: block;
|
||||
}
|
||||
|
||||
img {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.sc-main-img {
|
||||
width: 100%;
|
||||
max-width: 2560px;
|
||||
}
|
||||
.sc-mt20:first-child {
|
||||
margin-top: 0 !important;
|
||||
}
|
||||
.sc-mt20 {
|
||||
margin-top: 0.2rem;
|
||||
}
|
||||
.sc-mb-78 {
|
||||
margin-bottom: 0.78rem;
|
||||
}
|
||||
.sc-main-imgs {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 0.2rem;
|
||||
list-style: none;
|
||||
/* 去除列表样式(如果有) */
|
||||
}
|
||||
|
||||
.sc-main-imgs a {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.sc-main-img1 {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
/* 保持比例 */
|
||||
display: block;
|
||||
}
|
||||
@@ -147,46 +147,6 @@ class ImageMigrator:
|
||||
# 确保paramiko已导入
|
||||
ensure_paramiko()
|
||||
|
||||
# 连接目标服务器(必须为SSH)
|
||||
if self.target_config.is_local():
|
||||
print("错误: 目标服务器必须为SSH服务器,不能是本地目录")
|
||||
return False
|
||||
|
||||
self.target_client = paramiko.SSHClient()
|
||||
self.target_client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
|
||||
if self.verbose:
|
||||
print(
|
||||
f"连接到目标服务器: {self.target_config.host}:{self.target_config.port}"
|
||||
)
|
||||
|
||||
if self.target_config.key_file:
|
||||
key = paramiko.RSAKey.from_private_key_file(self.target_config.key_file)
|
||||
|
||||
self.target_client.connect(
|
||||
hostname=self.target_config.host,
|
||||
port=self.target_config.port,
|
||||
username=self.target_config.username,
|
||||
pkey=key,
|
||||
)
|
||||
else:
|
||||
self.target_client.connect(
|
||||
hostname=self.target_config.host,
|
||||
port=self.target_config.port,
|
||||
username=self.target_config.username,
|
||||
password=self.target_config.password,
|
||||
)
|
||||
|
||||
self.target_sftp = self.target_client.open_sftp()
|
||||
|
||||
# 检查SFTP服务器的工作目录
|
||||
if self.verbose:
|
||||
try:
|
||||
cwd = self.target_sftp.getcwd()
|
||||
print(f"DEBUG: 目标SFTP服务器当前工作目录: {cwd}")
|
||||
except Exception as e:
|
||||
print(f"DEBUG: 无法获取目标SFTP服务器工作目录: {e}")
|
||||
|
||||
# 连接源服务器(如果是SSH类型)
|
||||
if not self.source_config.is_local():
|
||||
self.source_client = paramiko.SSHClient()
|
||||
@@ -218,11 +178,61 @@ class ImageMigrator:
|
||||
|
||||
self.source_sftp = self.source_client.open_sftp()
|
||||
|
||||
# 检查SFTP服务器的工作目录
|
||||
if self.verbose:
|
||||
try:
|
||||
cwd = self.source_sftp.getcwd()
|
||||
print(f"DEBUG: 源SFTP服务器当前工作目录: {cwd}")
|
||||
except Exception as e:
|
||||
print(f"DEBUG: 无法获取源SFTP服务器工作目录: {e}")
|
||||
|
||||
# 连接目标服务器(如果为SSH)
|
||||
if not self.target_config.is_local():
|
||||
self.target_client = paramiko.SSHClient()
|
||||
self.target_client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
|
||||
if self.verbose:
|
||||
print(
|
||||
f"连接到目标服务器: {self.target_config.host}:{self.target_config.port}"
|
||||
)
|
||||
|
||||
if self.target_config.key_file:
|
||||
key = paramiko.RSAKey.from_private_key_file(
|
||||
self.target_config.key_file
|
||||
)
|
||||
|
||||
self.target_client.connect(
|
||||
hostname=self.target_config.host,
|
||||
port=self.target_config.port,
|
||||
username=self.target_config.username,
|
||||
pkey=key,
|
||||
)
|
||||
else:
|
||||
self.target_client.connect(
|
||||
hostname=self.target_config.host,
|
||||
port=self.target_config.port,
|
||||
username=self.target_config.username,
|
||||
password=self.target_config.password,
|
||||
)
|
||||
|
||||
self.target_sftp = self.target_client.open_sftp()
|
||||
|
||||
# 检查SFTP服务器的工作目录
|
||||
if self.verbose:
|
||||
try:
|
||||
cwd = self.target_sftp.getcwd()
|
||||
print(f"DEBUG: 目标SFTP服务器当前工作目录: {cwd}")
|
||||
except Exception as e:
|
||||
print(f"DEBUG: 无法获取目标SFTP服务器工作目录: {e}")
|
||||
|
||||
if self.verbose:
|
||||
source_type = (
|
||||
"本地目录" if self.source_config.is_local() else "SSH服务器"
|
||||
)
|
||||
print(f"连接成功! 源: {source_type}, 目标: SSH服务器")
|
||||
target_type = (
|
||||
"本地目录" if self.target_config.is_local() else "SSH服务器"
|
||||
)
|
||||
print(f"连接成功! 源: {source_type}, 目标: {target_type}")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
@@ -718,7 +728,7 @@ def read_image_paths(image_list_file: str) -> List[str]:
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="从本地目录或SSH服务器迁移图片到远程SSH服务器,保持相对路径结构",
|
||||
description="本地目录之间迁移图片,本地目录或SSH服务器迁移图片到远程SSH服务器,保持相对路径结构",
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
epilog="""
|
||||
示例:
|
||||
@@ -756,8 +766,7 @@ def main():
|
||||
parser.add_argument(
|
||||
"--source-type",
|
||||
choices=["local", "ssh"],
|
||||
default="local",
|
||||
help="源服务器类型: local(本地目录) 或 ssh(SSH服务器),默认: local",
|
||||
help="源服务器类型: local(本地目录) 或 ssh(SSH服务器)",
|
||||
)
|
||||
parser.add_argument("--source-host", help="源服务器地址(仅SSH类型需要)")
|
||||
parser.add_argument(
|
||||
@@ -772,6 +781,11 @@ def main():
|
||||
parser.add_argument("--source-dir", help="源服务器图片基础目录")
|
||||
|
||||
# 目标服务器选项
|
||||
parser.add_argument(
|
||||
"--target-type",
|
||||
choices=["local", "ssh"],
|
||||
help="目标服务器类型: local(本地目录) 或 ssh(SSH服务器)",
|
||||
)
|
||||
parser.add_argument("--target-host", help="目标服务器地址")
|
||||
parser.add_argument(
|
||||
"--target-port", type=int, default=22, help="目标服务器端口(默认: 22)"
|
||||
@@ -859,23 +873,38 @@ def main():
|
||||
|
||||
# 构建目标服务器配置
|
||||
target_config_data = config.get("target", {})
|
||||
target_config_data["type"] = "ssh" # 目标必须是SSH
|
||||
|
||||
ssh_config = target_config_data.get("ssh", {})
|
||||
|
||||
# 命令行参数覆盖配置文件
|
||||
if args.target_host:
|
||||
ssh_config["host"] = args.target_host
|
||||
if args.target_port:
|
||||
ssh_config["port"] = args.target_port
|
||||
if args.target_user:
|
||||
ssh_config["username"] = args.target_user
|
||||
if args.target_password:
|
||||
ssh_config["password"] = args.target_password
|
||||
if args.target_key:
|
||||
ssh_config["key_file"] = args.target_key
|
||||
if args.target_type:
|
||||
target_config_data["type"] = args.target_type
|
||||
|
||||
target_config_data["ssh"] = ssh_config
|
||||
# 命令行参数覆盖配置文件
|
||||
if args.target_type == "ssh" or target_config_data.get("type") == "ssh":
|
||||
ssh_config = target_config_data.get("ssh", {})
|
||||
|
||||
if args.target_host:
|
||||
ssh_config["host"] = args.target_host
|
||||
if args.target_port:
|
||||
ssh_config["port"] = args.target_port
|
||||
if args.target_user:
|
||||
ssh_config["username"] = args.target_user
|
||||
if args.target_password:
|
||||
ssh_config["password"] = args.target_password
|
||||
if args.target_key:
|
||||
ssh_config["key_file"] = args.target_key
|
||||
|
||||
target_config_data["ssh"] = ssh_config
|
||||
|
||||
# 检查必要的目标SSH参数
|
||||
if not ssh_config.get("host"):
|
||||
print("错误: 必须指定目标服务器地址 (--target-host)")
|
||||
sys.exit(1)
|
||||
if not ssh_config.get("username"):
|
||||
print("错误: 必须指定目标服务器用户名 (--target-user)")
|
||||
sys.exit(1)
|
||||
else:
|
||||
# 本地类型,不需要SSH配置
|
||||
target_config_data.pop("ssh", None)
|
||||
|
||||
# 设置基础目录
|
||||
if args.target_dir:
|
||||
@@ -884,14 +913,6 @@ def main():
|
||||
print("错误: 必须指定目标服务器图片基础目录 (--target-dir)")
|
||||
sys.exit(1)
|
||||
|
||||
# 检查必要的目标SSH参数
|
||||
if not ssh_config.get("host"):
|
||||
print("错误: 必须指定目标服务器地址 (--target-host)")
|
||||
sys.exit(1)
|
||||
if not ssh_config.get("username"):
|
||||
print("错误: 必须指定目标服务器用户名 (--target-user)")
|
||||
sys.exit(1)
|
||||
|
||||
# 创建服务器配置对象
|
||||
try:
|
||||
source_config = ServerConfig.from_dict(source_config_data)
|
||||
|
||||
Reference in New Issue
Block a user