Merge branch 'dev'

This commit is contained in:
2025-12-31 16:55:53 +08:00
36 changed files with 4380 additions and 35 deletions

View File

@@ -107,13 +107,6 @@ abstract class Common extends BaseController
'status' => 1
])
->where('status', '=', 1)
->where(function($query) {
// 临时代码,移动端暂时不显示 "AI PC"
if (request()->from == 'mobile') {
$table_name = SysNavigationItemModel::getTable();
$query->whereNotIn($table_name . ".id", [77, 78]);
}
})
->order(['sort' => 'asc', 'id' => 'asc'])
->select();
if ($nav->isEmpty()) {

View File

@@ -5,7 +5,6 @@ namespace app\index\controller;
use app\index\model\SysBannerModel;
use think\facade\View;
use think\Request;
class TopicLaptop extends Common
{
@@ -96,7 +95,7 @@ class TopicLaptop extends Common
// 产品检测介绍
$data['product_testing'] = data_get($banners_map, 'BANNER_693a2ff4629bd')?->items->toArray();
// 网页脚注
$data['webpage_footnotes'] = data_get($banners_map, 'BANNER_693a30e9e4572')?->items->first()?->toArray();
$data['webpage_footnotes'] = data_get($banners_map, 'BANNER_693a30e9e4572')?->items->first()?->toArray();
}
View::assign('data', $data);

View File

@@ -92,9 +92,9 @@ return [
],
'attachment/video' => [
'软件和驱动程序' => 'Software and Drivers',
'您的浏览器不支持 video 标签。' => 'Your browser does not support HTML5 video.',
'您的浏览器不支持 video 标签。' => 'Your browser does not support HTML5 video.',
],
// 联系我们批量购买页面
'contactus/bulkbuy' => [
'批量购买' => 'Bulk Buy',
@@ -219,4 +219,21 @@ return [
'联系我们' => 'Contact US',
'目录' => 'Content'
],
];
// 笔记本专题 - 首页
'topiclaptop/index' => [
'CineBench R23 多核跑分' => 'Outperforms Ryzen 5 & Intel i5',
'*此跑分为ORICO实验室测定所得请以实际使用为准' => '*Data measured by ORICO Lab. Actual performance may vary.',
'3DMARK Time Spy显卡得分' => 'Handles Office & Gaming with Ease',
'肯辛通锁孔' => 'Kensington <br/> Lock Slot',
'千兆网口' => 'Gigabit <br/> Ethernet',
'USB-A<br/>(5Gbps)' => 'USB-A <br/> (5Gbps)',
'3.5mm<br/>耳麦合一' => '3.5mm <br/> Combo Audio',
'TF口3.0' => 'TF 3.0 <br/> Card Slot',
'全功能<br/>USB-C' => 'All-in-One <br/> USB-C',
"接口大满贯" => "Full-Featured Ports",
"酷睿i5-12450H" => "Core i5-12450H",
"锐龙9 6900HX" => "Ryzen9 6900HX",
"标配多种接口,会议室连接电脑、</br>U盘传输文件、TF卡读取等全都轻松搞定" => "Versatile Ports for Easy Connectivity. Effortlessly</br> link to projectors, U disks, TF cards, and more.",
],
];

File diff suppressed because it is too large Load Diff

View File

@@ -18,7 +18,6 @@
<link rel="stylesheet" href="__CSS__/topic_laptop/ips.css">
<link rel="stylesheet" href="__CSS__/topic_laptop/rgb.css">
<link rel="stylesheet" href="__CSS__/topic_laptop/bly.css">
<link rel="stylesheet" href="__CSS__/topic_laptop/bly.css">
<link rel="stylesheet" href="__CSS__/topic_laptop/qb.css">
<link rel="stylesheet" href="__CSS__/topic_laptop/xh.css">
<link rel="stylesheet" href="__CSS__/topic_laptop/jk.css">
@@ -58,15 +57,11 @@
{volist name="data.top_focus_images" id="tfi"}
<div class="swiper-slide auto-swiper-slide">
<div class="swiper-container-texts" style="color:#fff">
<div class="swiper-container-texts-t">{$tfi.title}</div>
<div class="swiper-container-texts-p">{$tfi.desc||raw|htmlspecialchars_decode}</div>
<div class="swiper-container-texts-t">{$tfi.title}</div>
<div class="swiper-container-texts-p">{$tfi.desc|raw|htmlspecialchars_decode}</div>
<div class="swiper-container-texts-img">
<a href="{$tfi.link}">
{eq name=":cookie('think_lang')" value="en-us"}
<img src="__IMAGES__/topic_laptop/eljgd.png" alt="" >
{else/}
<img src="__IMAGES__/topic_laptop/ljgd.png" alt="" >
{/eq}
<a href="{$tfi.link}">
<img src="{$tfi.extra_image}" alt="" >
</a>
</div>
</div>
@@ -1045,14 +1040,14 @@
let videoTip = null;
// 初始化视频提示
const initVideoTip = () => {
if (!fsBox || videoTip) return;
videoTip = document.createElement('div');
videoTip.className = 'video-tip';
videoTip.innerText = '点击播放视频';
fsBox.appendChild(videoTip);
videoTip.style.display = 'none';
};
// const initVideoTip = () => {
// if (!fsBox || videoTip) return;
// videoTip = document.createElement('div');
// videoTip.className = 'video-tip';
// videoTip.innerText = '点击播放视频';
// fsBox.appendChild(videoTip);
// videoTip.style.display = 'none';
// };
// 停止所有视频
function stopAllVideos ()
@@ -1145,7 +1140,7 @@
};
// 初始化
initVideoTip();
// initVideoTip();
window.addEventListener('scroll', handleVideoScroll, { passive: true });
window.addEventListener('resize', handleVideoResize);
// 页面加载完成后尝试播放

View File

@@ -0,0 +1,40 @@
.amd-box {
width: 100%;
position: relative;
max-width: 6.82rem;
margin: 0 auto;
margin-top: -0.86rem;
z-index: 1;
}
.amd-img-header,
.amd-img-main,
.amd-img-footer {
width: 100%;
max-width: 6.82rem;
}
.amd-img-main {
display: flex;
justify-content: space-between;
}
.amd-img-main-left,
.amd-img-main-right {
flex: 1;
max-width: 3.36rem;
display: block;
}
.amd-img-main-left img,
.amd-img-main-right img {
width: 3.36rem;
display: block;
margin-top: 0.1rem;
}
.amd-img-header img {
width: 6.82rem;
display: block;
}
.amd-img-footer img {
width: 6.82rem;
margin-top: 0.1rem;
display: block;
}

View File

@@ -0,0 +1,50 @@
.bly {
margin: 0 0.34rem;
aspect-ratio: 682/338;
/* max-height: 6.97rem; */
padding-top: 1.5rem;
}
.bly .ba-slider .handle:after {
position: absolute;
top: 50%;
/* 1. 缩小宽高从48px改为32px可根据需求再调比如28px/30px */
width: 0.32rem;
height: 0.32rem;
/* 2. 同步调整margin宽高的一半保证居中48px对应-24px32px对应-16px */
margin: -0.16rem 0 0 -0.16rem;
content: '';
display: flex;
align-items: center;
justify-content: center;
background: #fff
url('https://dev.ow.f2b211.com/static/index/pc/images/ba-arrow.png')
/* 3. 缩小背景箭头从22px改为16px匹配整体尺寸 */ center center /
0.16rem 0.16rem no-repeat;
border: 1px solid #fff;
border-radius: 50%;
transition: all 0.3s ease;
transform: scale(1);
z-index: 5;
box-shadow: none;
}
.bly .ba-slider .handle.ba-draggable:after {
transform: scale(0.8);
}
.bly-t {
font-size: 0.48rem;
color: #fff;
text-align: center;
width: 100%;
}
.bly-p {
font-size: 0.20rem;
color: #cbcfd8;
text-align: center;
width: 100%;
padding-top: 0.37rem;
padding-bottom: 0.56rem;
font-family: 'HarmonyOS-Light';
line-height: 1.5;
}

View File

@@ -0,0 +1,50 @@
.cpu {
margin-top: 1.5rem;
width: 100%;
aspect-ratio: 750/882;
position: relative;
}
.cpu-main {
width: 100%;
aspect-ratio: 750/882;
}
.cpu-texts-t {
color: #fff;
font-size: 0.48rem;
line-height: 1;
text-align: center;
padding-top: 0.19rem;
}
.cpu-texts-p {
color: #cbcfd8;
font-size: 0.20rem;
margin-top: 0.37rem;
line-height: 1;
text-align: center;
}
.cpu-footer {
position: absolute;
bottom: 0;
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
}
.cpu-footer-img {
display: flex;
gap: 0.1rem;
}
.cpu-footer img {
width: 100%;
max-width: 1.48rem;
display: block;
}
.cpu-footer-desc {
font-size: 0.16rem;
color: #cbcfd8;
text-align: center;
margin-top: 0.38rem;
margin-bottom: 0.51rem;
width: 100%;
}

View File

@@ -0,0 +1,59 @@
.endurance {
position: relative;
width: 100%;
margin-top: 1.78rem;
aspect-ratio: 750/778;
}
.endurance-texts {
width: 100%;
display: flex;
flex-direction: column;
justify-content: center;
position: absolute;
top: -0.4rem;
}
.endurance-t {
width: 100%;
font-size: 0.5rem;
color: #fff;
text-align: center;
}
.endurance-t span:nth-child(1) {
font-size: 0.4rem;
line-height: 1;
}
.endurance-t span:nth-child(2) {
font-size: 0.48rem;
margin-left: 0.11rem;
line-height: 1;
}
.endurance-p {
font-size: 0.20rem;
color: #cbcfd8;
text-align: center;
padding-top: 0.37rem;
font-family: 'HarmonyOS-Light';
}
.endurance-img {
width: 100%;
position: absolute;
bottom: 0.62rem;
display: flex;
justify-content: center;
gap: 0.1rem;
}
.endurance-img img {
width: 2.46rem;
display: block;
}
.endurance-footer-p {
color: #ffffff;
font-size: 0.18rem;
position: absolute;
bottom: 0;
text-align: center;
left: 50%;
transform: translateX(-50%);
width: 100%;
}

View File

@@ -0,0 +1,161 @@
.footer-imgs {
width: 6.8rem;
margin: 0 auto;
margin-top: 1.5rem;
display: flex;
justify-content: space-between;
flex-wrap: wrap;
}
.footer-imgs img {
width: 2.22rem;
}
.footer-imgs img:nth-child(4),
.footer-imgs img:nth-child(5),
.footer-imgs img:nth-child(6) {
margin-top: 0.09rem;
}
.footer-texts {
width: 6.8rem;
margin: 1.5rem 0.35rem;
/* font-size: 0.16rem; */
color: #909399;
/* white-space: normal; */
/* margin-top: 1.5rem;
margin-bottom: 1.5rem; */
box-sizing: border-box;
}
.footer-texts p {
width: 6.8rem;
font-size: 0.16rem;
padding-left: 0.1rem;
text-indent: -0.2rem;
/* line-height: 1.5;
margin-bottom: 0.1rem; */
font-family: 'HarmonyOS-Light';
letter-spacing: 1px;
}
.footer-texts-en {
width: 6.8rem;
margin: 1.5rem 0.35rem;
/* font-size: 0.16rem; */
color: #909399;
/* white-space: normal; */
/* margin-top: 1.5rem;
margin-bottom: 1.5rem; */
box-sizing: border-box;
}
.footer-texts-en p {
width: 6.8rem;
font-size: 0.16rem;
padding-left: 0.1rem;
text-indent: -0.1rem;
/* line-height: 1.5;
margin-bottom: 0.1rem; */
font-family: 'HarmonyOS-Light';
/* letter-spacing: 1px; */
}
.oircoEgapp-foot .logo-white img {
margin: 0 auto;
}
.oircoEgapp-foot .m_footer {
display: flex;
/* flex-direction: row; */
align-items: center;
justify-content: center;
margin:0 4%;
padding-top: 0 !important;
}
/* .oircoEgapp-foot .m_footer .left,
.oircoEgapp-foot .foot-con {
display: flex;
flex-direction: row;
align-items: center;
} */
.oircoEgapp-foot .m_footer .right {
flex:1;
width: 50% !important;
display: flex !important;
justify-content: end !important;
/* max-width: 55%; */
}
.oircoEgapp-foot .m_footer .left {
flex:1 !important;
/* max-width: 40%;
justify-content: end;
margin-right: 4%; */
width: 50% !important;
display: flex;
justify-content: space-between;
}
.oircoEgapp-foot .foot-con span {
width: auto;
padding: 0 0.625rem;
}
.oircoEgapp-foot .foot-cate .clearfix li h3 {
margin-bottom: 10px;
}
.oircoEgapp-foot .foot-cate .clearfix li p,
.oircoEgapp-foot .foot-cate .clearfix li p a {
font-size: 12px;
}
.logo-white {
text-align: center;
padding: 1rem 0 0;
display: flex;
align-items: center;
}
.foot-cate {
padding: 0.16rem;
}
.foot-cate h3 {
font-size: 0.28rem;
}
.foot-cate li {
padding: 0.16rem 0;
min-height: 276px;
}
.foot-cate li p {
line-height: 40px;
}
.m_footer .right {
float: right;
width: 57%;
text-align: left;
}
.foot-con span {
font-size:14px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
-webkit-line-clamp: 1;
width: 100%;
height: auto;
cursor: pointer;
font-family: 'HarmonyOS-Medium';
}
.oircoEgapp-foot .foot-con span {
width: auto;
padding: 0 0.125rem;
}
.m_footer .left a{
display: flex;
align-items: center;
}
.m_footer .left img {
width: 30px;
padding-right:0 !important;
padding-top: 0;
}
/* .oircoEgapp-foot .m_footer .right {
max-width: 50%;
} */
.m_footer .right {
padding: 0 !important;
}

View File

@@ -0,0 +1,69 @@
.fs {
width: 100%;
}
.fs-box {
width: 100%;
aspect-ratio: 16/9;
}
.fs-img,
.fs-video {
backface-visibility: hidden; /* 开启硬件加速 */
transform: translateZ(0); /* 硬件加速 */
}
.fs-box img {
width: 100%;
}
.fs-box-img {
display: flex;
flex-wrap: wrap;
}
.fs-h-img {
width: 100%;
display: flex;
justify-content: center;
gap: 0.11rem;
}
.fs-h-img img {
width: 3.35rem;
aspect-ratio: 335/95;
}
.fs-b-img {
width: 100%;
display: flex;
justify-content: center;
margin-top: 0.1rem;
aspect-ratio: 681/122;
}
.fs-b-img img {
width: 6.81rem;
}
.fs-ts {
font-size: 0.18rem;
text-align: center;
color: #cbcfd8;
padding: 0.4rem 0;
}
.dl {
width: 100%;
padding-top: 1.01rem;
}
.dl-t {
color: #fff;
font-size: 0.48rem;
width: 100%;
text-align: center;
line-height: 1.5;
}
.dl-p {
color: #cbcfd8;
font-size: 0.20rem;
width: 100%;
text-align: center;
padding-top: 0.37rem;
/* padding-bottom: 0.37rem; */
line-height: 1.5;
font-family: 'HarmonyOS-Light';
}

View File

@@ -0,0 +1,44 @@
.gpu {
width: 100%;
position: relative;
box-sizing: border-box;
}
.gpu-texts {
color: #fff;
font-size: 0.48rem;
margin: 0 0.34rem;
}
.gpu-texts-t {
line-height: 1.5;
}
.gpu-texts-p {
font-size: 0.20rem;
color: #cbcfd8;
line-height: 1.5;
margin-top: 0.37rem;
}
.gpu-main-img {
width: 100%;
aspect-ratio: 750/780;
}
.gpu-amd-img {
width: 100%;
}
.gpu-amd-img img {
max-width: 2.26rem;
width: 100%;
margin-top: 2.39rem;
margin-left: 0.45rem;
}
.gpu-footer-imgs {
display: flex;
position: absolute;
bottom: 0.61rem;
gap: 0.1rem;
left: 50%;
transform: translateX(-50%);
}
.gpu-footer-imgs img {
max-width: 1.63rem;
}

View File

@@ -0,0 +1,233 @@
.oircoEgapp-head {
display: flex;
flex-direction: column;
width: 100%;
height: auto;
position: fixed;
top: 0;
z-index: 1000;
width: -webkit-fill-available;
}
.oircoEgapp-head .headtop {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
padding: 0 0.16rem;
height:60px;
background: #fff;
}
.oircoEgapp-head .headtop .headerlogimg {
max-width: 140px;
min-width: 123px;
height: auto !important;
display: block;
/* height: 2.25rem; */
}
.oircoEgapp-head .top-menu {
background-color: #fff;
overflow: hidden;
width: 100%;
color: #000;
display: none;
}
.oircoEgapp-head .top-menu .it-ct {
font-family: "HarmonyOS-SemiBold";
font-weight: bold;
}
.oircoEgapp-head .top-menu .it-ct .it-1 {
padding: 0.16rem 3.5% 0.2rem;
border-bottom: 1px solid #e5e5e5;
font-size: 14px;
position: relative;
}
.oircoEgapp-head .top-menu .it-ct .it-1-more {
display: flex;
flex-direction: row;
justify-content: space-between;
padding-bottom: 1.5%;
}
.oircoEgapp-head .top-menu .it-ct .it-1-more i {
font-weight: bold;
}
.oircoEgapp-head .top-menu .it-ct .it-1-2 {
padding-left: 0.16rem;
font-size: 13px;
line-height: 1.8;
font-weight: 100;
font-weight: bold;
display: none;
}
.oircoEgapp-head .top-menu .it-ct .it-1-2 a {
color: #666;
}
.cursor_p span {
font-size: 22px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
-webkit-line-clamp: 1;
width: 100%;
/* height: 1.2rem; */
padding-left: 0.2rem;
}
.cursor_p span:hover {
font-size:22px;
white-space: normal;
width: 100%;
overflow: inherit;
line-height: 1.5;
}
/* .cursor_p {
height: 0.38rem !important;
} */
.img-responsive {
display: flex !important;
justify-content: center !important;
align-items: center !important;
}
.img-responsive span {
display: flex !important;
align-items: center !important;
}
.action-sheet {
border-top-left-radius: 10px;
border-top-right-radius: 10px;
display: none;
background: #fff;
width: 100%;
position: fixed;
bottom: 0;
z-index: 11111;
left: 0;
}
.menu-name {
text-indent: 0.2rem;
font-size: 14px;
font-weight: 700;
color: #000;
}
.action-sheet ul li {
margin: 0 10px;
color: #333;
font-size: 12px;
text-align: left;
border-bottom: 1px solid #eee;
padding: 12px 14px;
}
.action-sheet ul li a {
color: #333;
display: flex;
flex-direction: row;
align-items: center;
}
.action-sheet ul li img {
margin-right: 14px;
}
.menu-title {
display: flex;
align-items: center;
margin: 0 10px;
justify-content: space-between;
background: #fff;
border-top-left-radius: 6px;
border-top-right-radius:6px;
padding: 5px 0;
}
.close-icon {
width: 24px;
display: flex;
align-items: center;
}
.close-icon img {
width: 24px !important;
}
.title-text {
padding-top: 0.24rem;
color: #333;
font-size: 14px;
}
.title-text p {
line-height: 2;
}
.title-text p a {
color: #989898;
}
.title-text p a:hover {
color: #989898;
}
.marsk-container {
background: rgba(0, 0, 0, 0.8);
display: none;
position: absolute;
position: fixed;
top: 0;
right: 0;
left: 0;
bottom: 0px;
z-index: 9999;
}
.marsk-container-detail {
background: rgba(0, 0, 0, 0.8);
display: none;
position: absolute;
position: fixed;
top: 0;
right: 0;
left: 0;
bottom: 0px;
z-index: 9999;
}
.search-in {
margin-top:0.8rem;
}
.search-in input {
width: 70% !important;
height: 0.8rem;
background: #ffffff;
border: 1px solid #d6d6d6;
opacity: 1;
border-radius: 20px;
padding: 0 14px;
font-size: 14px;
}
::-webkit-input-placeholder {
color: #989898;
}
.search-button {
border: none;
font-size: 14px;
color: #fff;
position: absolute;
right: 10px;
cursor: pointer;
background: #0060ff;
height:0.7rem;
border-radius: 20px;
padding: 0 14px;
width: 22%;
}
.search-in form{
display: flex;
flex-direction: row;
align-items: center;
}
.search-in .title-text p a{
font-size: 14px;
}
/*头部搜索弹窗*/
.popup-quick {
width: 86%;
background-color: #fff;
z-index: 9999;
box-shadow: 0px 2px 5px rgba(255, 255, 255, 0.2);
color: #333;
padding: 14px;
border-radius: 5px;
margin: 14px auto 0;
position: relative;
margin-top: 1rem;
}

View File

@@ -0,0 +1,42 @@
* {
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: #000;
overflow-x: hidden;
margin: 0 !important;
padding: 0 !important;
max-width: 100vw !important;
}
.f36{
font-size: 0.36rem !important;
font-family: "HarmonyOS-Medium" !important;
/* line-height: 1 !important; */
}
.f18{
font-size: 0.18rem !important;
line-height: 1.5 !important;
}
.lh1 {
line-height: 1 !important;
}
.f28 {
font-size: 0.28rem !important;
font-family: "HarmonyOS-Medium" !important;
}
.f48 {
font-size: 0.48rem !important;
line-height: 1 !important;
margin-bottom: 0.6rem !important;
font-family: "HarmonyOS-Medium" !important;
}

View File

@@ -0,0 +1,43 @@
.ips {
position: relative;
}
.ips-t {
font-size: 0.48rem;
color: #fff;
line-height: 1.5;
margin-top: 1.5rem;
margin-left: 0.38rem;
margin-right: 0.38rem;
margin-bottom: 0.46rem;
}
.ips-bg {
aspect-ratio: 750/678;
position: relative;
}
.ips-bg p {
font-size: 0.20rem;
color: #cbcfd8;
line-height: 1.5;
margin-left: 0.38rem;
margin-right: 0.38rem;
position: absolute;
top: -0.3rem;
}
.ips-imgs {
width: 6.8rem;
flex-wrap: wrap;
gap: 0.1rem;
position: absolute;
left: 50%;
transform: translateX(-50%);
bottom: -3rem;
display: flex;
}
.ips-imgs img {
width: 3.35rem;
display: block;
}
/* .ips-imgs img:nth-child(3),
.ips-imgs img:nth-child(4) {
margin-top: 0.1rem;
} */

View File

@@ -0,0 +1,37 @@
.m2 {
width: 100%;
margin-top: 1.09rem;
}
.m2-bg {
position: relative;
width: 100%;
aspect-ratio: 750/846;
}
.m2-bg-t1 {
font-size: 0.48rem;
color: #fff;
width: 100%;
text-align: center;
line-height: 1.5;
padding-top: 1.3rem;
}
.m2-bg-p {
color: #CBCFD8ff;
font-size: 0.18rem;
padding-top:0.37rem;
width: 100%;
text-align: center;
}
.m2-img-box {
position: absolute;
bottom: 0.66rem;
width: 100%;
display: flex;
justify-content: center;
gap: 0.11rem;
}
.m2-img-box img {
width: 2.22rem;
}

View File

@@ -0,0 +1,39 @@
.memory {
width: 100%;
}
.memory-t {
font-size: 0.48rem;
color: #fff;
width: 100%;
text-align: center;
padding-bottom: 0.37rem;
line-height: 1.5;
}
.memory-img {
width: 100%;
aspect-ratio: 750/727;
position: relative;
}
.memory-p {
width: 100%;
font-size: 0.20rem;
color: #cbcfd8;
text-align: center;
line-height: 1.5;
}
.memory-footer-img {
position: absolute;
left: 50%;
transform: translateX(-50%);
bottom: -0.74rem;
width: 6.83rem;
}
.memory-footer-img p {
height:0;
}
.memory-footer-img img {
width: 6.83rem;
display: block;
aspect-ratio: 683/148;
}

View File

@@ -0,0 +1,129 @@
.progress {
margin-left: 0.34rem;
margin-right: 0.34rem;
box-sizing: border-box;
}
/* .progress-section {
margin-left: 0.34rem;
margin-right: 0.34rem;
box-sizing: border-box;
} */
.progress-title {
font-size: 0.24rem;
color: #fff;
padding: 0.16rem 0 0.36rem 0;
font-family: 'HarmonyOS-Medium';
}
.progress-item .label {
display: flex;
justify-content: space-between;
align-items: baseline;
line-height: 1;
margin-top: 0.13rem;
margin-bottom: 0.36rem;
}
.progress-item .label .device-name {
flex: 1;
font-family: 'HarmonyOS-Medium';
margin-left: 0.04rem;
}
.progress-item .label .value {
text-align: right;
font-family: 'HarmonyOS-Medium';
margin-right: 0.04rem;
}
.colorLinearGradient {
background: linear-gradient(90deg, #7e51ff, #d5dfff);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
}
.progress-item .label .value.m4-max {
color: #5e5ce6;
}
.progress-bar {
flex: 1;
height: 0.1rem;
max-height: 0.2rem;
min-height: 0.1rem;
border-radius: 0.1rem;
overflow: hidden;
position: relative;
/* 性能优化1开启硬件加速减少重绘 */
transform: translateZ(0);
will-change: transform;
}
.progress-fill {
height: 100%;
border-radius: 0.05rem;
transform: scaleX(0);
transform-origin: left center;
transition: transform 1.2s ease-out;
will-change: transform;
position: relative;
}
/* 保留你所有原始的进度条颜色类 */
.progress-fill.m4-max {
background: linear-gradient(40deg, #7e51ff, #e1d5ff);
}
.progress-fill.m2-max {
background: #bfc5d9;
}
@keyframes shimmer {
100% {
left: 150%;
}
}
.font20 {
font-size: 0.2rem !important;
}
.font18 {
font-size: 0.18rem !important;
}
.colorCBCFD8 {
color: #646778 !important;
}
.progress-p {
color: #cbcfd8;
font-size: 0.18rem;
margin-top: 0.51rem;
margin-bottom: 1.5rem;
text-align: center;
}
.progress-p1 {
color: #cbcfd8;
font-size: 0.18rem;
margin-top: 1.56rem;
text-align: center;
}
.w645 {
width: 6.45rem;
}
.w585 {
width: 5.85rem;
}
.w454 {
width: 4.54rem;
}
.w321 {
width: 3.21rem;
}
.w596 {
width: 5.96rem;
}
.w467 {
width: 4.67rem;
}

View File

@@ -0,0 +1,25 @@
/* .qb {
position: absolute;
top:3rem;
left: 50%;
transform: translateX(-50%);
} */
.qb-t {
width: 100%;
font-size: 0.48rem;
color: #fff;
text-align: center;
padding-top: 1.5rem;
}
.qb-p {
font-size: 0.20rem;
color: #cbcfd8;
text-align: center;
padding-top: 0.37rem;
padding-bottom: 0.34rem;
font-family: 'HarmonyOS-Light';
line-height: 1.5;
}
.qb-bg-img1 {
width: 6.9rem;
}

View File

@@ -0,0 +1,28 @@
.rgb-t {
font-size: 0.48rem;
color: #fff;
width: 100%;
text-align: center;
margin-top: 4.84rem;
margin-bottom: 0.37rem;
line-height: 1.5;
}
.rgb-bg {
aspect-ratio: 750/582;
}
.rgb-p {
font-size: 0.20rem;
color: #cbcfd8;
line-height: 1.5;
text-align: center;
}
.rgb-imgs {
display: flex;
gap: 0.1rem;
margin-top: 0.28rem;
justify-content: center;
}
.rgb-imgs img {
width: 2.17rem;
display: block;
}

View File

@@ -0,0 +1,86 @@
/* 轮播容器 - 核心:基于视口高度自适应 */
.auto-swiper-container {
width: 100%;
overflow: hidden;
position: relative;
z-index: 1;
}
/* 轮播项 - 填充容器高度 */
.auto-swiper-slide {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
/* 图片自适应核心:填充屏幕比例高度,保持比例 */
.auto-swiper-slide img {
width: 100%;
height: 100%;
object-fit: cover; /* 优先填充容器,裁剪超出部分(无拉伸) */
/* object-fit: contain; 可选:完整显示图片,不裁剪(可能留黑边) */
display: block;
}
.swiper-container-texts {
position: absolute;
left: 50%;
top: 1.2rem;
transform: translateX(-50%);
z-index: 10;
width: 100%;
}
.swiper-container-texts-t {
font-size: 0.48rem;
text-align: center;
width: 100%;
line-height: 1;
}
.swiper-container-texts-p {
padding-top: 0.54rem;
font-size: 0.16rem;
/* text-align: center; */
width: 100%;
display: flex;
align-items: center;
/* letter-spacing: px; */
justify-content: center;
}
.swiper-container-texts-p div {
font-family: 'HarmonyOS-Light';
}
.swiper-container-texts-line {
width: 1px;
height: 0.16rem;
background: #fff;
margin: 0 12px;
}
.swiper-container-texts-img {
width: 100%;
display: flex;
justify-content: center;
padding-top: 0.54rem;
cursor: pointer;
}
.swiper-container-texts-img a {
width: 100%;
max-width: 1.18rem;
}
.swiper-container-texts-img img {
width: 100%;
max-width: 1.18rem;
}
.swiper-slide-t {
padding-top: 0.3rem;
font-size: 0.2rem;
text-align: center;
color: #fff;
}
.swiper-slide-p {
font-size: 0.16rem;
color: #cbcfd8;
text-align: center;
margin-top: 0.18rem;
}

View File

@@ -0,0 +1,105 @@
.tabs-container {
width: 6.03rem;
margin: 0 auto;
}
.tabs-header-box {
width: 100%;
display: flex;
justify-content: center;
margin-top: 0.48rem;
}
.tabs-header {
display: inline-flex;
/* 改为inline-flex宽度由子元素决定 */
position: relative;
border-bottom: 1px solid #909399;
margin: 0 auto;
width: 1.75rem;
display: flex;
}
.tab-item {
/* padding: 12px 24px; */
font-size: 0.18rem;
cursor: pointer;
transition: color 0.2s ease;
white-space: nowrap;
color: #cbcfd8;
flex: 1;
text-align: center;
}
.tab-item {
margin-bottom: 0.08rem;
}
.tab-indicator {
position: absolute;
bottom: 0;
height:1px;
background-color: #fff;
transform: translateX(0);
width: auto;
transition: transform 0.3s cubic-bezier(0.25, 0.1, 0.25, 1),
width 0.3s cubic-bezier(0.25, 0.1, 0.25, 1);
will-change: transform, width;
backface-visibility: hidden;
perspective: 1000px;
}
.tab-content {
width: 6.03rem;
width: 6.03rem;
/* min-width: 1280px; */
margin: 0 auto;
}
.content-video {
max-width: 100%;
height: auto;
border-radius: 8px;
}
.tabs-p {
width: 100%;
font-size: 0.18rem;
color: #cbcfd8;
text-align: center;
margin-top: 0.48rem;
display: none;
}
.tabs-p.active {
display: block;
}
.tab-panel {
display: none;
}
.tab-panel-img {
background: #1c1c1e;
width: 5.77rem;
overflow: hidden;
display: flex;
justify-content: flex-end;
border-radius: 0.16rem;
}
.tab-panel-img img {
width: 5.77rem;
}
.tab-panel.active {
display: block;
}
.tab-t {
font-size: 0.48rem;
color: #cbcfd8;
width: 100%;
text-align: center;
margin-top: 1.5rem;
margin-bottom: 0.7rem;
display: none;
}
.tab-t.active {
display: block;
}
/* .tab-ts {
margin-top: 2.97rem;
margin-bottom: 1rem;
} */

View File

@@ -0,0 +1,37 @@
.wifi {
position: relative;
width: 100%;
aspect-ratio: 750/595;
margin-top: 3.45rem;
}
.wifi-texts {
position: absolute;
top: -2.3rem;
left: 0.42rem;
width: 100%;
}
.wifi-t {
width: 100%;
font-size: 0.48rem;
color: #fff;
line-height: 1.5;
}
.wifi-p {
font-size: 0.20rem;
color: #cbcfd8;
padding-top: 0.37rem;
line-height: 1.4;
font-family: 'HarmonyOS-Light';
}
.wifi-img {
width: 100%;
position: absolute;
bottom: -0.22rem;
display: flex;
justify-content: center;
gap: 0.1rem;
}
.wifi-img img {
width: 3.35rem;
display: block;
}

View File

@@ -0,0 +1,9 @@
.window {
width: 100%;
display: flex;
justify-content: center;
margin-top: 1.5rem;
}
.window img {
width: 6.8rem;
}

View File

@@ -0,0 +1,64 @@
.xn-container {
width: 6.8rem;
margin: 0 auto;
box-sizing: border-box;
}
/* 图片容器样式(核心) */
.xn-image-section {
overflow: hidden;
border-radius: 0.16rem;
background: #0d0c10;
border: 1px solid #3f3f45;
width: 6.8rem;
}
.zoom-image {
width: 100%;
height: auto;
display: block;
/* 调整transform过渡时间为2sopacity为1.5s,可按需修改 */
transition: transform 2s cubic-bezier(0.25, 0.1, 0.25, 1), opacity 1.5s ease;
transform: scale(0.8);
/* 初始缩放比例 */
opacity: 0.9;
/* 初始透明度(略暗,放大后变亮) */
}
/* 滚动触发后的放大状态 */
.zoom-image.active {
transform: scale(1.1);
/* 放大5%(官网常用比例,不夸张) */
opacity: 1;
}
.xn-t {
font-size: 0.48rem;
color: #fff;
text-align: center;
margin-top: 1.5rem;
margin-bottom: 0.6rem;
}
.xn-p {
padding-top: 0.6rem;
font-size: 0.16rem;
margin-left: 0.47rem;
}
.xn-p p {
width: 5.84rem;
color: #cbcfd8;
line-height: 1.5;
/* 核心:左内边距控制第二行缩进量 */
padding-left: 0.8rem;
/* 首行向左偏移,抵消左内边距,实现第二行缩进 */
text-indent: -0.8rem;
/* 确保p标签是块级且换行正常 */
display: block;
word-wrap: break-word;
white-space: nowrap;
/* 2. 超出元素宽度的内容隐藏 */
overflow: hidden;
/* 3. 将超出的文本替换为省略号... */
text-overflow: ellipsis;
/* 可选:给元素设置一个固定宽度(确保省略效果生效) */
}

View File

@@ -0,0 +1,122 @@
/* 图片容器100vw宽最小宽度1440px按图片原始比例2560:1857定高 */
.zoom-container {
width: 7.5rem;
height: 6.05rem;
position: relative;
}
/* 图片包裹层:与容器同尺寸,定位参考系,承接缩放变换 */
.img-wrapper {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
overflow: hidden;
/* z-index: -10; */
}
/* 图片:按原始比例填充包裹层,不裁切,初始放大+过渡动画 */
.bg-img {
width: 7.5rem;
height: 6.05rem;
display: block;
transform: scale(1.5);
transition: transform 1.8s ease;
transform-origin: center center;
}
/* 图片缩小后的状态 */
.bg-img.zoom-out {
transform: scale(1);
}
/* 标注样式:关键修正 - 基于容器绝对定位,百分比参考图片原始比例 */
.annotation {
position: absolute;
color: #fff;
font-size: calc(12px + 0.3vw);
opacity: 0;
transform: translateY(calc(10px + 0.5vw));
transition: opacity 0.8s ease, transform 0.8s ease;
pointer-events: none;
z-index: 10;
white-space: nowrap;
/* 关键:标注的定位参考系是容器(与图片同比例),而非缩放后的图片 */
top: 0;
left: 0;
/* 重置默认值依赖内联style的百分比定位 */
}
/* 标注显示状态 */
.annotation.anno-show {
opacity: 1;
transform: translateY(0);
}
.annotation span {
position: relative;
display: inline-block;
/* 确保文字居中对齐 */
text-align: center;
}
/* 标注线条样式:基于文字定位,适配缩放 */
.annotation span::before {
content: '';
position: absolute;
left: 50%;
transform: translateX(-50%);
width: 0.01rem;
height: 0;
bottom: calc(100% + 6px);
background-color: #fff;
transition: height 0.8s ease;
transform-origin: bottom center;
}
/* 向上延伸的标注线条(给需要向上的.annotation加anno-up类 */
.annotation.anno-up span::before {
/* 把线条位置从文字下方,改成文字上方 */
bottom: auto;
top: 0.3rem; /* 定位到文字顶部外 */
/* 线条方向改为向上延伸 */
transform-origin: top center;
}
.annotation.anno-up1 span::before {
/* 把线条位置从文字下方,改成文字上方 */
bottom: auto;
top: 0.5rem; /* 定位到文字顶部外 */
/* 线条方向改为向上延伸 */
transform-origin: top center;
}
/* 标注显示时,设置线条最终高度 */
.annotation.anno-show span::before {
height: 0.57rem;
}
.zoom-t {
width: 100%;
text-align: center;
font-size: 0.48rem;
color: #fff;
padding-top: 1.5rem;
}
.zoom-p {
width: 100%;
text-align: center;
font-size: 0.20rem;
color: #cbcfd8;
position: absolute;
top: 0.37rem;
z-index: 10;
line-height: 1.5;
font-family: 'HarmonyOS-Light';
}
/* 标注延迟类 */
.anno-delay-1 {
transition-delay: 0.8s;
}
.anno-delay-1 span::before {
transition-delay: 0.8s;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

View File

@@ -54,4 +54,23 @@
text-align: center;
color:#cbcfd8;
padding:0.4rem 0;
}
}.dl {
width: 100%;
padding-top: 0.71rem;
}
.dl-t {
color: #fff;
font-size: 0.63rem;
width: 100%;
text-align: center;
}
.dl-p {
color: #cbcfd8;
font-size: clamp(16px, 1vw, 0.22rem);
width: 100%;
text-align: center;
padding-top: 0.43rem;
/* padding-bottom: 0.37rem; */
line-height: 1.8;
font-family: "HarmonyOS-Light";
}

186
scripts/QUICK_START.md Normal file
View File

@@ -0,0 +1,186 @@
# 图片迁移脚本快速开始指南
## 概述
这是一个通过SSH将本地图片迁移到远程服务器的Python脚本。支持保持相对路径结构自动创建目录批量处理图片文件。
## 环境要求
- Python 3.6+
- paramiko库 (SSH客户端库)
## 快速开始
### 1. 安装依赖
```bash
cd /var/www/html/orico-official-website/scripts
pip install -r requirements.txt
```
### 2. 创建配置文件
```bash
# 创建示例配置文件
python image_migrate.py --create-config config.json
```
编辑生成的 `config.json` 文件,填写目标服务器信息:
- 修改 `target.ssh` 部分的目标服务器地址、用户名、密码/密钥
- 设置 `source.base_dir` 为本地图片目录路径
### 3. 准备图片路径列表
创建 `images.txt` 文件,每行一个相对路径:
```txt
uploads/products/2024/01/product1.jpg
uploads/products/2024/01/product2.jpg
assets/images/logo.png
uploads/banners/main-banner.jpg
```
### 4. 执行迁移
```bash
python image_migrate.py --config config.json --input images.txt --verbose
```
## 配置文件示例(本地源)
```json
{
"source": {
"type": "local",
"base_dir": "/var/www/html/orico-official-website/public/uploads"
},
"target": {
"type": "ssh",
"base_dir": "/var/www/html/images",
"ssh": {
"host": "your-server.com",
"port": 22,
"username": "deploy",
"password": "your_password",
"key_file": "/path/to/private/key"
}
}
}
```
## 常用命令
### 基本迁移
```bash
python image_migrate.py --config config.json --input images.txt
```
### 详细输出模式
```bash
python image_migrate.py --config config.json --input images.txt --verbose
```
### 覆盖已存在的文件
```bash
python image_migrate.py --config config.json --input images.txt --overwrite
```
### 直接从命令行指定文件
```bash
python image_migrate.py --config config.json \
"uploads/test1.jpg" \
"uploads/test2.jpg" \
"assets/logo.png"
```
### 不使用配置文件,直接指定参数
```bash
python image_migrate.py \
--source-type local \
--source-dir /path/to/local/images \
--target-host server.example.com \
--target-user username \
--target-dir /remote/path/images \
--input images.txt \
--verbose
```
## 常见场景
### 场景1迁移网站上传目录
```bash
# 配置文件中的源目录设置为:
"source": {
"type": "local",
"base_dir": "/var/www/html/orico-official-website/public/uploads"
}
# 执行迁移
python image_migrate.py --config config.json --input uploads_list.txt
```
### 场景2从数据库导出路径并迁移
```bash
# 从数据库导出图片路径
mysql -u username -p database -e "SELECT image_path FROM products" --skip-column-names > products.txt
# 清理路径(如果需要)
sed -i 's/^\.\///' products.txt
# 迁移图片
python image_migrate.py --config config.json --input products.txt --verbose
```
### 场景3迁移整个目录结构
```bash
# 使用find命令生成所有图片文件列表
find /path/to/local/images -type f \( -name "*.jpg" -o -name "*.png" -o -name "*.gif" -o -name "*.webp" \) | sed 's|^/path/to/local/images/||' > all_images.txt
# 迁移所有图片
python image_migrate.py --config config.json --input all_images.txt
```
## 路径说明
- **相对路径**:相对于配置文件中指定的 `base_dir`
- **示例**:如果 `base_dir``/var/www/html/images`,相对路径 `uploads/test.jpg` 对应完整路径 `/var/www/html/images/uploads/test.jpg`
- **目标路径**:在目标服务器上保持相同的相对路径结构
## 故障排查
### 1. 连接失败
```bash
# 测试SSH连接
ssh -p 22 username@server.example.com
# 检查配置文件中的主机、端口、用户名是否正确
```
### 2. 文件不存在
```bash
# 检查本地文件是否存在
ls -la /var/www/html/orico-official-website/public/uploads/uploads/test.jpg
# 使用详细模式查看完整路径
python image_migrate.py --config config.json --input images.txt --verbose
```
### 3. 权限不足
```bash
# 检查本地文件读取权限
ls -la /path/to/local/images
# 检查目标目录写入权限通过SSH
ssh username@server.example.com "ls -la /remote/path/images"
```
## 注意事项
1. 建议使用SSH密钥认证而非密码更安全
2. 首次使用前,建议用少量文件测试
3. 大文件传输可能需要较长时间,建议分批处理
4. 使用 `--verbose` 参数可查看详细传输信息
5. 脚本会自动创建目标服务器上的目录结构
## 获取帮助
```bash
# 查看完整帮助
python image_migrate.py --help
# 查看详细使用说明
cat README.md
```
---
*快速开始指南更新日期2024年*
*脚本位置:/var/www/html/orico-official-website/scripts/*

415
scripts/README.md Normal file
View File

@@ -0,0 +1,415 @@
# 图片迁移脚本 (Image Migration Script)
从本地目录或SSH服务器迁移图片到远程SSH服务器保持相对路径结构一致。
## 功能特性
-**灵活的源支持**:支持本地目录(`local`)和SSH服务器(`ssh`)作为源
-**远程目标**通过SSH迁移到远程服务器
-**路径保持**:在目标服务器上保持相同的相对路径和文件名
-**配置灵活**:支持配置文件和命令行参数
-**目录自动创建**:递归创建目标目录结构
-**重复文件处理**:默认跳过已存在文件,支持覆盖选项
-**进度统计**:显示详细的传输进度和统计信息
-**错误处理**:完善的连接管理和错误处理
## 环境要求
- Python 3.6+
- paramiko 库用于SSH连接
## 快速开始
### 1. 安装依赖
```bash
cd /var/www/html/orico-official-website/scripts
pip install -r requirements.txt
```
### 2. 创建配置文件
```bash
# 创建示例配置文件
python image_migrate.py --create-config config.json
# 编辑配置文件,填写真实的服务器信息
vim config.json
```
### 3. 准备图片路径列表
创建一个文本文件(如 `images.txt`),每行一个相对路径:
```
uploads/products/2024/01/product1.jpg
uploads/products/2024/01/product2.jpg
assets/images/logo.png
```
### 4. 执行迁移
#### 从本地目录迁移到远程服务器(最常用)
```bash
# 使用配置文件迁移(源为本地目录)
python image_migrate.py --config config.json --input images.txt
# 或直接指定参数迁移(源为本地目录)
python image_migrate.py \
--source-type local --source-dir /path/to/local/images \
--target-host dst.example.com --target-user user2 --target-dir /home/user/images \
--input images.txt --verbose
```
#### 从SSH服务器迁移到另一个SSH服务器
```bash
# 使用配置文件迁移源为SSH服务器
python image_migrate.py --config config.json --input images.txt
# 或直接指定参数迁移源为SSH服务器
python image_migrate.py \
--source-type ssh --source-host src.example.com --source-user user1 --source-dir /var/www/images \
--target-host dst.example.com --target-user user2 --target-dir /home/user/images \
--input images.txt --verbose
```
## 配置文件格式
配置文件为JSON格式支持两种源类型配置
### 本地目录作为源(推荐)
```json
{
"source": {
"type": "local",
"base_dir": "/path/to/local/images"
},
"target": {
"type": "ssh",
"base_dir": "/var/www/html/images",
"ssh": {
"host": "target-server.example.com",
"port": 22,
"username": "username",
"password": "your_password_here",
"key_file": "/path/to/private/key"
}
}
}
```
### SSH服务器作为源
```json
{
"source": {
"type": "ssh",
"base_dir": "/var/www/html/images",
"ssh": {
"host": "source-server.example.com",
"port": 22,
"username": "username",
"password": "your_password_here",
"key_file": "/path/to/private/key"
}
},
"target": {
"type": "ssh",
"base_dir": "/var/www/html/images",
"ssh": {
"host": "target-server.example.com",
"port": 22,
"username": "username",
"password": "your_password_here",
"key_file": "/path/to/private/key"
}
}
}
```
**注意:**
- `type`:可以是 `"local"`(本地目录)或 `"ssh"`SSH服务器
- `base_dir`:图片的基础目录,图片的相对路径将相对于这个目录
- 对于SSH类型需要提供`ssh`配置部分
- `password``key_file` 二选一即可
- 目标服务器**必须**是SSH类型
## 命令行参数
### 主要选项
| 参数 | 说明 |
| ---------------------- | ---------------------- |
| `--config FILE` | 使用配置文件 |
| `--create-config FILE` | 创建示例配置文件 |
| `--input FILE` | 包含图片路径列表的文件 |
| `--verbose`, `-v` | 显示详细输出 |
| `--overwrite` | 覆盖已存在的文件 |
### 源服务器参数(可覆盖配置文件)
| 参数 | 说明 |
| ------------------------ | ------------------------------------------------------------ |
| `--source-type TYPE` | 源类型: `local`(本地目录) 或 `ssh`(SSH服务器),默认: `local` |
| `--source-dir DIR` | **必需** 源服务器图片基础目录 |
| `--source-host HOST` | 源服务器地址仅SSH类型需要 |
| `--source-port PORT` | 源服务器端口仅SSH类型默认: 22 |
| `--source-user USER` | 源服务器用户名仅SSH类型需要 |
| `--source-password PASS` | 源服务器密码仅SSH类型需要 |
| `--source-key FILE` | 源服务器私钥文件路径仅SSH类型需要 |
### 目标服务器参数(可覆盖配置文件)
| 参数 | 说明 |
| ------------------------ | ------------------------------------ |
| `--target-host HOST` | **必需** 目标服务器地址必须是SSH |
| `--target-port PORT` | 目标服务器端口(默认: 22 |
| `--target-user USER` | **必需** 目标服务器用户名 |
| `--target-password PASS` | 目标服务器密码 |
| `--target-key FILE` | 目标服务器私钥文件路径 |
| `--target-dir DIR` | **必需** 目标服务器图片基础目录 |
## 使用示例
### 示例1从本地目录迁移到远程服务器推荐
```bash
# 1. 创建配置文件
python image_migrate.py --create-config myconfig.json
# 2. 编辑配置文件
# 将 source.type 设置为 "local"
# 设置 source.base_dir 为本地目录路径
# 填写目标服务器的SSH信息
# 3. 创建图片路径文件
echo "uploads/test.jpg" > images.txt
echo "assets/logo.png" >> images.txt
# 4. 执行迁移
python image_migrate.py --config myconfig.json --input images.txt
```
### 示例2直接从命令行迁移本地图片
```bash
python image_migrate.py \
--source-type local \
--source-dir /home/user/my_website/images \
--target-host 192.168.1.200 \
--target-user deploy \
--target-key ~/.ssh/id_rsa \
--target-dir /var/www/html/images \
--input images.txt \
--verbose
```
### 示例3从SSH服务器迁移到另一个SSH服务器
```bash
python image_migrate.py \
--source-type ssh \
--source-host old-server.example.com \
--source-user admin \
--source-key ~/.ssh/id_rsa \
--source-dir /var/www/images \
--target-host new-server.example.com \
--target-user deploy \
--target-dir /home/deploy/images \
--input images.txt \
--overwrite
```
### 示例4直接从命令行指定图片路径
```bash
python image_migrate.py --config config.json \
"uploads/product1.jpg" \
"uploads/product2.jpg" \
"assets/logo.png"
```
### 示例5批量迁移数据库导出的图片路径
```bash
# 从数据库导出图片路径(假设每行一个路径)
mysql -e "SELECT image_path FROM products" --skip-column-names > products.txt
# 清理路径(如果需要)
sed -i 's/^\.\///' products.txt
# 迁移图片
python image_migrate.py --config config.json --input products.txt --verbose
```
## 图片路径格式
图片路径应为**相对路径**,相对于配置文件中指定的 `base_dir`
```
# 正确示例(相对路径)
uploads/products/2024/01/product1.jpg
assets/images/logo.png
# 错误示例(绝对路径)
/var/www/html/images/uploads/product1.jpg # 错误!应该使用相对路径
# 在目标服务器上,文件将保存为:
# 目标 base_dir + 相对路径
```
## 工作流程
1. **连接建立**连接到目标SSH服务器源为本地时不需要连接
2. **路径处理**:为每个图片构建完整源路径和目标路径
3. **目录创建**:在目标服务器上创建必要的目录结构
4. **文件检查**:检查源文件是否存在,目标文件是否已存在(跳过)
5. **文件传输**:通过本地临时文件传输(源→本地临时文件→目标)
6. **统计报告**:显示传输结果和统计信息
## 故障排除
### 常见问题
#### 1. 连接失败
```
错误: 连接失败: [Errno 111] Connection refused
```
- 检查目标服务器地址和端口
- 确认SSH服务正在运行
- 检查防火墙设置
#### 2. 认证失败
```
错误: Authentication failed.
```
- 确认用户名和密码正确
- 检查SSH密钥权限chmod 600 ~/.ssh/id_rsa
- 确认密钥已添加到目标服务器的 authorized_keys
#### 3. 源文件不存在
```
错误: 源文件不存在: /path/to/local/images/uploads/test.jpg
```
- 确认相对路径正确
- 确认源基础目录正确
- 使用 `--verbose` 查看完整路径
#### 4. 权限不足
```
错误: Permission denied
```
- 确认用户有读取源文件的权限(本地文件)
- 确认用户有写入目标目录的权限
#### 5. 目录创建失败
```
错误: 无法创建目标目录: /var/www/html/images/uploads
```
- 检查目标目录的写入权限
- 确认目标服务器磁盘空间充足
### 调试技巧
1. **使用详细模式**:添加 `--verbose` 参数查看详细信息
2. **测试连接**手动SSH连接测试目标服务器是否可达
3. **检查路径**:在源目录上确认文件存在
4. **查看日志**:脚本会显示详细的错误信息
## 性能优化
### 大文件传输
- 脚本使用流式传输,适用于大文件
- 临时文件存储在系统临时目录,传输完成后自动清理
### 批量传输
- 对于大量文件,建议分批处理
- 可以使用多个图片路径文件,分别执行迁移
### 网络优化
- 确保到目标服务器的网络连接稳定
- 考虑使用内网传输以提高速度
## 安全注意事项
1. **认证方式**建议使用SSH密钥认证而非密码
2. **权限控制**:配置文件建议设置权限 `chmod 600 config.json`
3. **最小权限**:使用具有最小必要权限的账户执行迁移
4. **本地文件安全**:确保本地目录不包含敏感信息
5. **临时文件清理**:脚本会自动清理临时传输文件
## 扩展和定制
### 修改脚本
- 可以在 `ImageMigrator` 类中添加额外的功能
- 支持断点续传、并行传输等高级功能
### 集成到工作流
- 可以与其他脚本结合使用
- 可以作为CI/CD流程的一部分
### 添加进度条
```python
# 可选:安装 tqdm 库
pip install tqdm
# 在脚本中添加进度条显示
```
## 文件结构
```
scripts/
├── image_migrate.py # 主脚本
├── image_migrate_backup.py # 原始版本备份
├── requirements.txt # Python依赖
├── README.md # 本文档
└── examples/ # 示例文件
├── config.example.json # 配置文件示例
└── images.example.txt # 图片路径示例
```
## 版本更新
### v2.0 更新
- ✅ 新增本地目录(`local`)作为源的支持
- ✅ 默认源类型改为`local`
- ✅ 简化配置文件结构
- ✅ 改进错误处理和临时文件管理
## 许可证
本脚本根据项目需要自由使用和修改。
## 支持
如有问题,请:
1. 检查本文档的故障排除部分
2. 使用 `--verbose` 参数获取详细信息
3. 确认服务器配置正确
---
**提示**:首次使用前,建议在测试服务器上进行小规模测试。

24
scripts/config.json Normal file
View File

@@ -0,0 +1,24 @@
{
"source": {
"type": "local",
"base_dir": "public/storage",
"ssh": {
"host": "source-server.example.com",
"port": 22,
"username": "username",
"password": "your_password_here",
"key_file": ""
}
},
"target": {
"type": "ssh",
"base_dir": "/www/wwwroot/orico-official-website/public/storage",
"ssh": {
"host": "47.91.149.172",
"port": 22,
"username": "root",
"password": "Orico666tx5d",
"key_file": ""
}
}
}

View File

@@ -0,0 +1,24 @@
{
"source": {
"type": "local",
"base_dir": "/var/www/html/orico-official-website/public/storage",
"ssh": {
"host": "source-server.example.com",
"port": 22,
"username": "username",
"password": "your_password_here",
"key_file": "/path/to/private/key"
}
},
"target": {
"type": "ssh",
"base_dir": "/www/wwwroot/orico-official-website/public/storage",
"ssh": {
"host": "47.91.149.172",
"port": 22,
"username": "root",
"password": "Orico666tx5d",
"key_file": ""
}
}
}

View File

@@ -0,0 +1,12 @@
# 图片路径示例文件
# 每行一个相对路径(相对于配置文件中指定的基础目录)
# 空行和以 # 开头的行会被忽略
# 路径使用正斜杠 /即使在Windows系统上
# 注意:这些路径都是相对路径
# 实际文件在源服务器上的完整路径是:源基础目录 + 相对路径
# 例如:如果源基础目录是 /var/www/html/images
# 那么 uploads/products/2024/01/product1.jpg 对应的完整路径是:
# /var/www/html/images/uploads/products/2024/01/product1.jpg
images/article/logo.png

922
scripts/image_migrate.py Executable file
View File

@@ -0,0 +1,922 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
图片迁移脚本 - 通过SSH或本地目录迁移图片到远程服务器
保持相对路径结构一致
"""
import argparse
import json
import os
import shutil
import sys
import tempfile
import time
from typing import Any, Dict, List, Optional, Tuple
# paramiko 导入被延迟到实际需要时
paramiko: Any = None
def ensure_paramiko() -> None:
"""确保paramiko已导入如果未安装则报错"""
global paramiko
if paramiko is None:
try:
import paramiko as paramiko_module
paramiko = paramiko_module
except ImportError:
print("错误: 需要安装paramiko库")
print("请运行: pip install paramiko")
sys.exit(1)
class ServerConfig:
"""服务器配置支持SSH或本地目录"""
def __init__(
self,
config_type: str = "ssh", # "ssh" 或 "local"
host: str = "",
port: int = 22,
username: Optional[str] = None,
password: Optional[str] = None,
key_file: Optional[str] = None,
base_dir: Optional[str] = None,
):
"""
初始化服务器配置
Args:
config_type: 配置类型,'ssh' 表示SSH服务器'local' 表示本地目录
host: 服务器地址对于local类型可为None
port: 服务器端口仅SSH类型
username: 用户名仅SSH类型
password: 密码仅SSH类型
key_file: 私钥文件路径仅SSH类型
base_dir: 基础目录路径
"""
self.config_type = config_type
self.host = host
self.port = port
self.username = username
self.password = password
self.key_file = key_file
self.base_dir = base_dir.rstrip("/") if base_dir else ""
def to_dict(self) -> Dict:
"""转换为字典"""
return {
"type": self.config_type,
"host": self.host,
"port": self.port,
"username": self.username,
"password": self.password,
"key_file": self.key_file,
"base_dir": self.base_dir,
}
@classmethod
def from_dict(cls, data: Dict) -> "ServerConfig":
"""从字典创建"""
ssh_data = data.get("ssh", {})
return cls(
config_type=data.get("type", "ssh"),
host=ssh_data.get("host", "localhost"),
port=ssh_data.get("port", 22),
username=ssh_data.get("username", ""),
password=ssh_data.get("password", ""),
key_file=ssh_data.get("key_file", ""),
base_dir=data.get("base_dir", ""),
)
def is_local(self) -> bool:
"""是否是本地目录配置"""
return self.config_type == "local"
def __str__(self) -> str:
if self.is_local():
return f"LocalDirectory(base_dir={self.base_dir})"
else:
return f"SSHServer(host={self.host}:{self.port}, user={self.username}, base_dir={self.base_dir})"
class ImageMigrator:
"""图片迁移器"""
def __init__(
self,
source_config: ServerConfig,
target_config: ServerConfig,
verbose: bool = False,
overwrite: bool = False,
) -> None:
"""
初始化迁移器
Args:
source_config: 源服务器配置可以是本地目录或SSH服务器
target_config: 目标服务器配置必须为SSH服务器
verbose: 是否显示详细输出
overwrite: 是否覆盖已存在的文件
"""
self.source_config = source_config
self.target_config = target_config
self.verbose = verbose
self.overwrite = overwrite
# SSH客户端连接仅用于SSH类型的服务器
self.source_client: Any = None
self.target_client: Any = None
self.source_sftp: Any = None
self.target_sftp: Any = None
# 统计信息
self.stats = {
"total": 0,
"success": 0,
"failed": 0,
"skipped": 0,
"bytes_transferred": 0,
}
def connect(self) -> bool:
"""连接到服务器"""
try:
# 确保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()
self.source_client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
if self.verbose:
print(
f"连接到源服务器: {self.source_config.host}:{self.source_config.port}"
)
if self.source_config.key_file:
key = paramiko.RSAKey.from_private_key_file(
self.source_config.key_file
)
self.source_client.connect(
hostname=self.source_config.host,
port=self.source_config.port,
username=self.source_config.username,
pkey=key,
)
else:
self.source_client.connect(
hostname=self.source_config.host,
port=self.source_config.port,
username=self.source_config.username,
password=self.source_config.password,
)
self.source_sftp = self.source_client.open_sftp()
if self.verbose:
source_type = (
"本地目录" if self.source_config.is_local() else "SSH服务器"
)
print(f"连接成功! 源: {source_type}, 目标: SSH服务器")
return True
except Exception as e:
print(f"连接失败: {e}")
self.close()
return False
def close(self) -> None:
"""关闭所有连接"""
if self.source_sftp:
self.source_sftp.close()
if self.source_client:
self.source_client.close()
if self.target_sftp:
self.target_sftp.close()
if self.target_client:
self.target_client.close()
if self.verbose:
print("连接已关闭")
def ensure_target_directory(self, remote_dir: str) -> bool:
"""确保目标目录存在(递归创建)"""
try:
if self.verbose:
print(f"DEBUG ensure_target_directory: 检查/创建目录: {remote_dir}")
# 确保路径以斜杠开头(绝对路径)
if not remote_dir.startswith("/"):
remote_dir = "/" + remote_dir
if self.verbose:
print(
f"DEBUG ensure_target_directory: 转换为绝对路径: {remote_dir}"
)
# 首先检查目录是否已经存在
try:
# 尝试列出目录内容
self.target_sftp.listdir(remote_dir)
if self.verbose:
print(f"DEBUG ensure_target_directory: 目录已存在: {remote_dir}")
return True
except (FileNotFoundError, IOError):
# 目录不存在,需要递归创建
if self.verbose:
print(
f"DEBUG ensure_target_directory: 目录不存在,开始递归创建: {remote_dir}"
)
# 分割路径为各个组件,保留空字符串以处理根目录
parts = [p for p in remote_dir.split("/") if p]
# 从根目录开始逐级创建
current_path = ""
for i, part in enumerate(parts):
# 构建当前路径(确保以斜杠开头)
if current_path:
current_path = f"{current_path}/{part}"
else:
current_path = f"/{part}"
# 检查当前路径是否存在
try:
self.target_sftp.listdir(current_path)
if self.verbose:
print(
f"DEBUG ensure_target_directory: 路径已存在: {current_path}"
)
except (FileNotFoundError, IOError):
# 当前路径不存在,尝试创建
if self.verbose:
print(
f"DEBUG ensure_target_directory: 创建目录: {current_path}"
)
try:
self.target_sftp.mkdir(current_path)
if self.verbose:
print(
f"DEBUG ensure_target_directory: 目录创建成功: {current_path}"
)
except Exception as mkdir_e:
# 如果创建失败,可能是权限问题或目录已存在
if self.verbose:
print(
f"DEBUG ensure_target_directory: 创建目录时出错 {current_path}: {mkdir_e}"
)
# 尝试检查目录是否真的不存在
try:
self.target_sftp.listdir(current_path)
if self.verbose:
print(
f"DEBUG ensure_target_directory: 目录实际上已存在: {current_path}"
)
except Exception as listdir_e:
# 目录确实不存在且创建失败
print(f"错误: 无法创建目录 {current_path}: {listdir_e}")
return False
# 最终确认目录是否创建成功
try:
self.target_sftp.listdir(remote_dir)
if self.verbose:
print(
f"DEBUG ensure_target_directory: 目录创建完成: {remote_dir}"
)
return True
except Exception as final_check_e:
if self.verbose:
print(
f"DEBUG ensure_target_directory: 最终检查失败 {remote_dir}: {final_check_e}"
)
return False
except Exception as e:
print(f"创建目录失败 {remote_dir}: {e}")
if self.verbose:
import traceback
traceback.print_exc()
return False
def get_source_file_info(self, source_path: str) -> Tuple[bool, Optional[int]]:
"""
获取源文件信息
Returns:
Tuple[exists: bool, size: Optional[int]]
"""
try:
if self.source_config.is_local():
# 本地文件
if self.verbose:
print(f"DEBUG: 检查本地文件路径: {source_path}")
print(f"DEBUG: 当前工作目录: {os.getcwd()}")
print(f"DEBUG: 绝对路径: {os.path.abspath(source_path)}")
print(f"DEBUG: 文件是否存在: {os.path.exists(source_path)}")
if os.path.exists(source_path):
if self.verbose:
print(f"DEBUG: 文件大小: {os.path.getsize(source_path)} bytes")
return True, os.path.getsize(source_path)
else:
if self.verbose:
print("DEBUG: 文件不存在!")
return False, 0
else:
# SSH远程文件
source_attr = self.source_sftp.stat(source_path)
return True, source_attr.st_size
except Exception as e:
if self.verbose:
print(f"DEBUG: 获取文件信息时出错: {e}")
return False, 0
def read_source_file(self, source_path: str, temp_path: str) -> bool:
"""
读取源文件到临时文件
Returns:
bool: 是否成功
"""
try:
if self.source_config.is_local():
# 从本地文件复制
if self.verbose:
print("DEBUG read_source_file: 开始复制文件")
print(f"DEBUG read_source_file: 源路径: {source_path}")
print(f"DEBUG read_source_file: 临时路径: {temp_path}")
print(
f"DEBUG read_source_file: 源文件是否存在: {os.path.exists(source_path)}"
)
print(
f"DEBUG read_source_file: 源文件绝对路径: {os.path.abspath(source_path)}"
)
shutil.copy2(source_path, temp_path)
if self.verbose:
print("DEBUG read_source_file: 复制完成")
print(
f"DEBUG read_source_file: 临时文件是否存在: {os.path.exists(temp_path)}"
)
if os.path.exists(temp_path):
print(
f"DEBUG read_source_file: 临时文件大小: {os.path.getsize(temp_path)} bytes"
)
return True
else:
# 从SSH服务器下载
self.source_sftp.get(source_path, temp_path)
return True
except Exception as e:
with open("fails.txt", "a", encoding="utf-8") as file:
file.write(f"{source_path}\n")
print(f"读取源文件失败 {source_path}: {e}")
import traceback
traceback.print_exc()
return False
def transfer_file(self, relative_path: str) -> bool:
"""
传输单个文件
Args:
relative_path: 相对路径(相对于基础目录)
Returns:
bool: 是否成功
"""
try:
# 构建完整路径
source_path = f"{self.source_config.base_dir}/{relative_path}"
target_path = f"{self.target_config.base_dir}/{relative_path}"
if self.verbose:
source_type = "本地" if self.source_config.is_local() else "远程"
print(f"传输: {relative_path}")
print(f" 源({source_type}): {source_path}")
print(f" 目标(远程): {target_path}")
# 检查源文件是否存在并获取大小
source_exists, file_size = self.get_source_file_info(source_path)
if not source_exists:
print(f"错误: 源文件不存在: {source_path}")
self.stats["failed"] += 1
return False
# 检查目标文件是否已存在根据overwrite选项处理
try:
self.target_sftp.stat(target_path)
if not self.overwrite:
if self.verbose:
print(f"跳过: 目标文件已存在: {target_path}")
self.stats["skipped"] += 1
return True
else:
if self.verbose:
print(f"覆盖: 目标文件已存在: {target_path}")
except FileNotFoundError:
if self.verbose:
print(f"DEBUG: 目标文件不存在,将创建新文件: {target_path}")
except Exception as e:
if self.verbose:
print(f"DEBUG: 检查目标文件时出错: {e}")
# 确保目标目录存在
target_dir = os.path.dirname(target_path)
if self.verbose:
print(f"DEBUG: 目标目录: {target_dir}")
print(f"DEBUG: 目标路径: {target_path}")
if target_dir and not self.ensure_target_directory(target_dir):
print(f"错误: 无法创建目标目录: {target_dir}")
self.stats["failed"] += 1
return False
# 传输文件
start_time = time.time()
temp_path = None
try:
# 创建临时文件
with tempfile.NamedTemporaryFile(delete=False) as temp_file:
temp_path = temp_file.name
if self.verbose:
print(f"DEBUG: 创建临时文件: {temp_path}")
# 读取源文件到临时文件
if not self.read_source_file(source_path, temp_path):
self.stats["failed"] += 1
return False
# 上传到目标服务器
if self.verbose:
print(f"DEBUG: 开始上传到目标服务器: {temp_path} -> {target_path}")
print(f"DEBUG: 临时文件大小: {os.path.getsize(temp_path)} bytes")
print(f"DEBUG: 临时文件是否存在: {os.path.exists(temp_path)}")
# 尝试列出目标目录内容
try:
target_dir = os.path.dirname(target_path)
dir_list = self.target_sftp.listdir(target_dir)
print(f"DEBUG: 目标目录内容: {dir_list}")
except Exception as e:
print(f"DEBUG: 无法列出目标目录内容 {target_dir}: {e}")
# 尝试检查目录是否存在
try:
self.target_sftp.stat(target_dir)
print(f"DEBUG: 但目录stat成功: {target_dir}")
except Exception as stat_e:
print(f"DEBUG: 目录stat也失败: {stat_e}")
self.target_sftp.put(temp_path, target_path)
if self.verbose:
print("DEBUG: 上传完成")
except Exception as e:
if self.verbose:
print(f"DEBUG: 上传过程中出错: {e}")
import traceback
traceback.print_exc()
raise
finally:
# 清理临时文件
if temp_path and os.path.exists(temp_path):
try:
if self.verbose:
print(f"DEBUG: 清理临时文件: {temp_path}")
os.remove(temp_path)
except Exception as e:
if self.verbose:
print(f"DEBUG: 清理临时文件时出错: {e}")
# 记录统计
transfer_time = time.time() - start_time
speed = (file_size or 0) / transfer_time / 1024 if transfer_time > 0 else 0
if self.verbose:
print(
f" 完成: {file_size} bytes, 耗时: {transfer_time:.2f}s, 速度: {speed:.2f} KB/s"
)
self.stats["success"] += 1
self.stats["bytes_transferred"] += file_size or 0
return True
except Exception as e:
print(f"传输失败 {relative_path}: {e}")
if self.verbose:
import traceback
traceback.print_exc()
self.stats["failed"] += 1
return False
def migrate_images(self, image_paths: List[str]) -> Dict[str, int]:
"""
迁移图片列表
Args:
image_paths: 相对路径列表
Returns:
Dict: 统计信息
"""
if not self.connect():
return self.stats
try:
self.stats["total"] = len(image_paths)
source_type = "本地目录" if self.source_config.is_local() else "SSH服务器"
print(f"开始从{source_type}迁移 {len(image_paths)} 个图片到SSH服务器...")
for i, relative_path in enumerate(image_paths, 1):
# 清理路径
relative_path = relative_path.strip()
if not relative_path:
continue
# 显示进度
print(f"[{i}/{len(image_paths)}] {relative_path}", end="")
# 传输文件
success = self.transfer_file(relative_path)
if success:
print("")
else:
print("")
# 打印统计信息
print("\n" + "=" * 50)
print("迁移完成!")
print(f"总计: {self.stats['total']}")
print(f"成功: {self.stats['success']}")
print(f"失败: {self.stats['failed']}")
print(f"跳过: {self.stats['skipped']}")
print(f"传输字节: {self.stats['bytes_transferred']:,}")
return self.stats
finally:
self.close()
def load_config(config_file: str) -> Dict:
"""加载配置文件"""
try:
with open(config_file, "r", encoding="utf-8") as f:
config = json.load(f)
return config
except Exception as e:
print(f"加载配置文件失败 {config_file}: {e}")
return {}
def save_config(config_file: str, config: Dict):
"""保存配置文件"""
try:
with open(config_file, "w", encoding="utf-8") as f:
json.dump(config, f, indent=2, ensure_ascii=False)
print(f"配置文件已保存: {config_file}")
except Exception as e:
print(f"保存配置文件失败 {config_file}: {e}")
def create_example_config(config_file: str):
"""创建示例配置文件"""
example_config = {
"source": {
"type": "local", # 可以是 "local" 或 "ssh"
"base_dir": "/path/to/local/images",
# 如果是SSH类型需要以下配置
"ssh": {
"host": "source-server.example.com",
"port": 22,
"username": "username",
"password": "your_password_here",
"key_file": "/path/to/private/key",
},
},
"target": {
"type": "ssh", # 目标必须是SSH类型
"base_dir": "/var/www/html/images",
"ssh": {
"host": "target-server.example.com",
"port": 22,
"username": "username",
"password": "your_password_here",
"key_file": "/path/to/private/key",
},
},
}
save_config(config_file, example_config)
print("已创建示例配置文件")
print("注意: 源服务器可以是本地目录(local)或SSH服务器(ssh)")
print(" 目标服务器必须是SSH服务器(ssh)")
def read_image_paths(image_list_file: str) -> List[str]:
"""从文件读取图片路径列表"""
paths = []
not_images = []
try:
with open(image_list_file, "r", encoding="utf-8") as f:
for line in f:
line = line.strip()
if line and not line.startswith("#"):
if not line.endswith((".png", ".jpeg", ".jpg", ".gif", ".webp")):
not_images.append(line)
else:
paths.append(line)
print(f"从文件中读取到 {len(paths)} 个图片路径数据行")
print(f"从文件中读取到 {len(not_images)} 个非图片路径数据行")
except Exception as e:
print(f"读取图片路径文件失败 {image_list_file}: {e}")
return paths
def main():
parser = argparse.ArgumentParser(
description="从本地目录或SSH服务器迁移图片到远程SSH服务器保持相对路径结构",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
示例:
# 使用配置文件迁移(源为本地目录)
python image_migrate.py --config config.json --input images.txt
# 使用配置文件迁移源为SSH服务器
# 配置文件中 source.type="ssh"
# 直接指定参数迁移(源为本地目录)
python image_migrate.py \\
--source-type local --source-dir /path/to/local/images \\
--target-host dst.example.com --target-user user2 --target-dir /home/user/images \\
--input images.txt
# 直接指定参数迁移源为SSH服务器
python image_migrate.py \\
--source-type ssh --source-host src.example.com --source-user user1 --source-dir /var/www/images \\
--target-host dst.example.com --target-user user2 --target-dir /home/user/images \\
--input images.txt
# 创建示例配置文件
python image_migrate.py --create-config config.json
# 从命令行直接指定图片路径
python image_migrate.py --config config.json images/product1.jpg images/product2.jpg
""",
)
# 配置文件选项
parser.add_argument("--config", help="配置文件路径")
parser.add_argument("--create-config", metavar="FILE", help="创建示例配置文件")
# 源服务器选项
parser.add_argument(
"--source-type",
choices=["local", "ssh"],
default="local",
help="源服务器类型: local(本地目录) 或 ssh(SSH服务器),默认: local",
)
parser.add_argument("--source-host", help="源服务器地址仅SSH类型需要")
parser.add_argument(
"--source-port",
type=int,
default=22,
help="源服务器端口仅SSH类型默认: 22",
)
parser.add_argument("--source-user", help="源服务器用户名仅SSH类型需要")
parser.add_argument("--source-password", help="源服务器密码仅SSH类型需要")
parser.add_argument("--source-key", help="源服务器私钥文件路径仅SSH类型需要")
parser.add_argument("--source-dir", help="源服务器图片基础目录")
# 目标服务器选项
parser.add_argument("--target-host", help="目标服务器地址")
parser.add_argument(
"--target-port", type=int, default=22, help="目标服务器端口(默认: 22"
)
parser.add_argument("--target-user", help="目标服务器用户名")
parser.add_argument("--target-password", help="目标服务器密码")
parser.add_argument("--target-key", help="目标服务器私钥文件路径")
parser.add_argument("--target-dir", help="目标服务器图片基础目录")
# 输入选项
parser.add_argument(
"--input", "-i", help="包含图片路径列表的文件(每行一个相对路径)"
)
parser.add_argument("--verbose", "-v", action="store_true", help="显示详细输出")
parser.add_argument("--overwrite", action="store_true", help="覆盖已存在的文件")
# 直接传递图片路径
parser.add_argument("image_paths", nargs="*", help="直接指定图片相对路径")
args = parser.parse_args()
# 创建示例配置文件
if args.create_config:
create_example_config(args.create_config)
return
# 检查必要参数(如果不是创建配置文件)
if not args.config and not (
args.source_dir and args.target_host and args.target_user and args.target_dir
):
print("错误: 必须提供配置文件或指定必要的服务器参数")
print("必要参数: --source-dir, --target-host, --target-user, --target-dir")
parser.print_help()
sys.exit(1)
# 加载配置
config = {}
if args.config:
config = load_config(args.config)
if not config:
print("错误: 无法加载配置文件")
sys.exit(1)
# 构建源服务器配置
source_config_data = config.get("source", {})
# 命令行参数覆盖配置文件
if args.source_type:
source_config_data["type"] = args.source_type
# 如果是SSH类型需要SSH配置
if args.source_type == "ssh" or source_config_data.get("type") == "ssh":
ssh_config = source_config_data.get("ssh", {})
if args.source_host:
ssh_config["host"] = args.source_host
if args.source_port:
ssh_config["port"] = args.source_port
if args.source_user:
ssh_config["username"] = args.source_user
if args.source_password:
ssh_config["password"] = args.source_password
if args.source_key:
ssh_config["key_file"] = args.source_key
source_config_data["ssh"] = ssh_config
# 检查必要的SSH参数
if not ssh_config.get("host"):
print("错误: 源服务器为SSH类型必须指定 --source-host")
sys.exit(1)
if not ssh_config.get("username"):
print("错误: 源服务器为SSH类型必须指定 --source-user")
sys.exit(1)
else:
# 本地类型不需要SSH配置
source_config_data.pop("ssh", None)
# 设置基础目录
if args.source_dir:
source_config_data["base_dir"] = args.source_dir
elif "base_dir" not in source_config_data:
print("错误: 必须指定源服务器图片基础目录 (--source-dir)")
sys.exit(1)
# 构建目标服务器配置
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
target_config_data["ssh"] = ssh_config
# 设置基础目录
if args.target_dir:
target_config_data["base_dir"] = args.target_dir
elif "base_dir" not in target_config_data:
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)
target_config = ServerConfig.from_dict(target_config_data)
if args.verbose:
print(f"源服务器配置: {source_config}")
print(f"目标服务器配置: {target_config}")
except Exception as e:
print(f"创建服务器配置失败: {e}")
sys.exit(1)
# 获取图片路径列表
image_paths = []
# 从文件读取
if args.input:
file_paths = read_image_paths(args.input)
image_paths.extend(file_paths)
# 从命令行参数添加
if args.image_paths:
image_paths.extend(args.image_paths)
if not image_paths:
print("错误: 没有指定要迁移的图片路径")
print("请使用 --input 指定文件或直接在命令行提供路径")
sys.exit(1)
origin_paths = image_paths
# 去重
image_paths = list(set(image_paths))
repeats = len(origin_paths) - len(image_paths)
if repeats > 0:
print(f"从文件中读取到 {repeats} 个重复图片路径数据行")
# 创建并运行迁移器
migrator = ImageMigrator(
source_config=source_config,
target_config=target_config,
verbose=args.verbose,
overwrite=args.overwrite,
)
# 执行迁移
stats = migrator.migrate_images(image_paths)
# 如果有失败,返回错误代码
if stats["failed"] > 0:
sys.exit(1)
if __name__ == "__main__":
main()

2
scripts/images.txt Normal file
View File

@@ -0,0 +1,2 @@
images/banner/20251217/c16f15d090170b9cecaf6b88e02384f3_thumb.png
images/banner/20251217/c16f15d090170b9cecaf6b88e02384f3.png

8
scripts/requirements.txt Normal file
View File

@@ -0,0 +1,8 @@
# Requirements for image migration script
paramiko>=2.12.0
# Optional: for better performance with large files
# Optional: for progress bars
# tqdm>=4.66.0
# Install with: pip install -r requirements.txt