18 Commits

Author SHA1 Message Date
3545b82015 feat: 新增产品兼容数据、产品兼容数据配件类型相关接口
All checks were successful
Gitea Actions Official-website / deploy-dev (push) Successful in 5s
2026-07-08 16:59:47 +08:00
3717a6a34d feat: 添加产品兼容数据表,产品兼容数据配件类型表,产品与兼容性关联表的数据库迁移文件 2026-07-08 16:59:04 +08:00
5beae1b39b Rename migration file and class for consistency
All checks were successful
Gitea Actions Official-website / deploy-dev (push) Successful in 5s
2026-07-07 11:10:14 +08:00
d314cfd12a Fix static method call to instance method in VideoCategory
All checks were successful
Gitea Actions Official-website / deploy-dev (push) Successful in 4s
2026-07-07 11:02:34 +08:00
80291c6a6a refactor: 优化图片迁移脚本
All checks were successful
Gitea Actions Official-website / deploy-dev (push) Successful in 4s
2026-05-09 11:53:07 +08:00
3d7ed2c16b 跳轉调整
All checks were successful
Gitea Actions Official-website / deploy-dev (push) Successful in 5s
2026-05-08 09:30:38 +08:00
ade2c6bea8 添加懒加载
All checks were successful
Gitea Actions Official-website / deploy-dev (push) Successful in 4s
2026-05-06 15:37:45 +08:00
2f4b0f8b76 mobile - topic ssd
All checks were successful
Gitea Actions Official-website / deploy-dev (push) Successful in 4s
2026-05-06 14:39:04 +08:00
9fcdea0061 移动端闪存
All checks were successful
Gitea Actions Official-website / deploy-dev (push) Successful in 4s
2026-05-06 13:37:29 +08:00
d7d8e2aa77 外边距设置
All checks were successful
Gitea Actions Official-website / deploy-dev (push) Successful in 3s
2026-05-06 11:50:06 +08:00
cfe07c9df5 换背景色
All checks were successful
Gitea Actions Official-website / deploy-dev (push) Successful in 4s
2026-05-06 11:48:51 +08:00
5eb49defe1 admin - system 内页
All checks were successful
Gitea Actions Official-website / deploy-dev (push) Successful in 3s
2026-05-06 11:39:11 +08:00
00e8eed191 topic ssd data
All checks were successful
Gitea Actions Official-website / deploy-dev (push) Successful in 5s
2026-05-06 11:34:57 +08:00
aeec3e4f0d 闪存
All checks were successful
Gitea Actions Official-website / deploy-dev (push) Successful in 3s
2026-05-06 10:50:48 +08:00
251a13c368 feat: topic ssd
All checks were successful
Gitea Actions Official-website / deploy-dev (push) Successful in 4s
2026-05-06 10:28:46 +08:00
b75a159d70 修复蒙版遮盖下划线
All checks were successful
Gitea Actions Official-website / deploy-dev (push) Successful in 4s
2026-04-17 12:28:21 +08:00
40d4a41461 优化产品列表右侧图片间距
All checks were successful
Gitea Actions Official-website / deploy-dev (push) Successful in 2s
2026-04-17 10:45:26 +08:00
456b2ede8a 产品列表图片间距
All checks were successful
Gitea Actions Official-website / deploy-dev (push) Successful in 3s
2026-04-17 10:31:33 +08:00
27 changed files with 1504 additions and 269 deletions

View File

@@ -9,9 +9,13 @@ if (!function_exists('xlsx_reader')) {
* @param array $keys_map 列 -> 键 例:['A' => 'name', 'B' => 'age', 'C' => 'sex']
* @return array
*/
function xlsx_reader(string $file, int $initial_row = 1, array $keys_map = []): array
function xlsx_reader(string $file, int $initial_row = 1, array $keys_map = [], bool $read_only = false): array
{
$reader = \PhpOffice\PhpSpreadsheet\IOFactory::createReader('Xlsx');
if ($read_only) {
// 只读模式,不读取公式,样式等等,减少内存消耗
$reader->setReadDataOnly(true);
}
// 读取文件
$spreadsheet = $reader->load($file);
@@ -42,6 +46,54 @@ if (!function_exists('xlsx_reader')) {
}
}
if (!function_exists('xlsx_stream_reader')) {
/**
* 读取Excel数据
* @param string $file Excel文件路径
* @param int $initial_row 初始行
* @param array $keys_map 列 -> 键 例:['A' => 'name', 'B' => 'age', 'C' => 'sex']
* @return Generator
*/
function xlsx_stream_reader(string $file, int $initial_row = 1, array $keys_map = [], bool $read_only = false): Generator
{
$reader = \PhpOffice\PhpSpreadsheet\IOFactory::createReader('Xlsx');
if ($read_only) {
// 只读模式,不读取公式,样式等等,减少内存消耗
$reader->setReadDataOnly(true);
}
// 读取文件
$spreadsheet = $reader->load($file);
// 获取活动sheet
$sheet = $spreadsheet->getActiveSheet();
// 获取最大行
$max_row = $sheet->getHighestRow();
// 获取最大列
$max_col = $sheet->getHighestColumn();
$xlsx_row = [];
$first_col = ord('A');
$last_col = ord($max_col);
for ($row = $initial_row; $row <= $max_row; $row++) {
for ($col = $first_col; $col <= $last_col; $col++) {
$key = $col_chr = chr($col);
if (!empty($keys_map)) {
$key = $keys_map[$col_chr] ?? $col_chr;
}
$xlsx_row[$key] = $sheet->getCell($col_chr . $row)->getValue();
}
yield $xlsx_row;
}
// 主动销毁对象以释放内存
$spreadsheet->disconnectWorksheets();
unset($spreadsheet);
}
}
if (!function_exists('xlsx_writer')) {
/**
* 构建Excel数据

View File

@@ -0,0 +1,312 @@
<?php
declare (strict_types = 1);
namespace app\admin\controller\v1;
use app\admin\model\v1\ProductCompatModel;
use app\admin\model\v1\ProductCompatPartsModel;
use app\admin\validate\v1\ProductCompatValidate;
class ProductCompat
{
/**
* 产品兼容性数据列表
*/
public function index()
{
$params = request()->param([
'label_name',
'model',
'page/d' => 1,
'size/d' => 10
]);
$list = ProductCompatModel::withoutField([
'language_id',
'deleted_at'
])
->with(['parts' => function ($query) {
$query->field(['id', 'name']);
}])
->language(request()->lang_id)
->withSearch(['label_name', 'model'], [
'label_name' => $params['label_name'] ?? null,
'model' => $params['model'] ?? null,
])
->order(['updated_at' => 'desc'])
->paginate([
'list_rows' => $params['size'],
'page' => $params['page'],
])
?->bindAttr('parts', ['part_name' => 'name'])
?->hidden(['parts']);
return success("获取成功", $list);
}
/**
* 导入产品兼容性数据
*/
public function import()
{
// 获取上传文件
$file = request()->file('file');
if (empty($file)) {
return error('请上传文件');
}
$lang_id = request()->lang_id;
// 读取文件
$keys_map = [
'A' => 'label_name',
'B' => 'part_name',
'C' => 'brand_name',
'D' => 'level_name',
'E' => 'class_name',
'F' => 'series_name',
'G' => 'model',
'H' => 'capacity',
'I' => 'interface_type',
'J' => 'compat_status',
];
$chunk = 500; // 每批次处理的条数
$items = 0; // 已处理的条数
$xlsx_data = [];
$xlsx_reader = xlsx_stream_reader($file->getRealPath(), 2, $keys_map, true);
\think\facade\Db::startTrans();
try {
foreach ($xlsx_reader as $row) {
$items++;
$row['seq_no'] = $items; // 记录行序号,防止后续顺序打乱
$xlsx_data[] = $row;
if ($items % $chunk == 0) {
// 每500条为一批次进行处理
$this->handleImport($xlsx_data, $lang_id);
$xlsx_data = [];
}
}
if (!empty($xlsx_data)) {
// 处理剩余的
$this->handleImport($xlsx_data, $lang_id);
}
} catch (\Throwable $th) {
\think\facade\Db::rollback();
return error($th->getMessage());
}
\think\facade\Db::commit();
return success('操作成功');
}
private function handleImport(array $xlsx_data, int $lang_id): void
{
$compat_datas = $this->matchExistCompatData($xlsx_data, $lang_id);
// 校验数据并组装sql语句
$raw_sql = $this->validateAndBuildSql($compat_datas);
if (false === \think\facade\Db::execute($raw_sql)) {
throw new \Exception(sprintf('第【%s】行执行导入 SQL 失败', implode(',', array_column($xlsx_data, 'seq_no'))));
}
}
private function matchExistCompatData(array $datas, int $lang_id)
{
$models = array_column($datas, 'model');
$compat_map = ProductCompatModel::language($lang_id)
->modelName($models)
->column('id', 'model');
$part_names = array_unique(array_column($datas, 'part_name'));
$part_map = ProductCompatPartsModel::language($lang_id)
->partName($part_names)
->column('id', 'name');
// 匹配已存在数据的主键、配件id
foreach ($datas as &$d) {
$d['id'] = $compat_map[$d['model']] ?? null;
$d['language_id'] = $lang_id;
$d['part_id'] = $part_map[$d['part_name']] ?? 0;
}
unset($d);
return $datas;
}
private function validateAndBuildSql(array $compat_datas)
{
$sql_values = [];
$validate = new ProductCompatValidate;
foreach ($compat_datas as $compat) {
// 校验数据
$scene = is_null($compat['id']) ? 'add' : 'edit';
if (!$validate->scene($scene)->check($compat)) {
throw new \Exception(sprintf('第【%s】行%s', $compat['seq_no'], $validate->getError()));
}
// 组装 sql values
$sql_values[] = $this->buildSqlValue($compat);
}
return $this->buildRawSql($sql_values);
}
private function buildSqlValue(array $compat)
{
$pdo = \think\facade\Db::connect()->getPdo();
return sprintf(
'(%d, %d, %d, %s, %s, %s, %s, %s, %s, %s, %s, %s)',
$compat['id'],
$compat['language_id'],
$compat['part_id'],
is_string($compat['label_name']) ? $pdo->quote($compat['label_name']) : $compat['label_name'],
is_string($compat['brand_name']) ? $pdo->quote($compat['brand_name']) : $compat['brand_name'],
is_string($compat['level_name']) ? $pdo->quote($compat['level_name']) : $compat['level_name'],
is_string($compat['class_name']) ? $pdo->quote($compat['class_name']) : $compat['class_name'],
is_string($compat['series_name']) ? $pdo->quote($compat['series_name']) : $compat['series_name'],
is_string($compat['model']) ? $pdo->quote($compat['model']) : $compat['model'],
is_string($compat['capacity']) ? $pdo->quote($compat['capacity']) : $compat['capacity'],
is_string($compat['interface_type']) ? $pdo->quote($compat['interface_type']) : $compat['interface_type'],
is_string($compat['compat_status']) ? $pdo->quote($compat['compat_status']) : $compat['compat_status']
);
}
private function buildRawSql(array $values)
{
return sprintf(
'INSERT INTO %s (
`id`,
`language_id`,
`part_id`,
`label_name`,
`brand_name`,
`level_name`,
`class_name`,
`series_name`,
`model`,
`capacity`,
`interface_type`,
`compat_status`
) VALUES %s
ON DUPLICATE KEY UPDATE
`part_id` = VALUES(`part_id`),
`label_name` = VALUES(`label_name`),
`brand_name` = VALUES(`brand_name`),
`level_name` = VALUES(`level_name`),
`class_name` = VALUES(`class_name`),
`series_name` = VALUES(`series_name`),
`model` = VALUES(`model`),
`capacity` = VALUES(`capacity`),
`interface_type` = VALUES(`interface_type`),
`compat_status` = VALUES(`compat_status`)
',
(new ProductCompatModel)->getTable(),
implode(',', $values)
);
}
/**
* 产品兼容性数据添加
*/
public function save()
{
$post = request()->post([
'part_id',
'label_name',
'brand_name',
'level_name',
'class_name',
'series_name',
'model',
'capacity',
'interface_type',
'compat_status',
'sort',
'disabled'
]);
$data = array_merge($post, ['language_id' => request()->lang_id]);
// 校验输入
$validate = new ProductCompatValidate;
if (!$validate->scene('add')->check($data)) {
return error($validate->getError());
}
$compat = ProductCompatModel::create($data);
if ($compat->isEmpty()) {
return error('操作失败');
}
return success('操作成功');
}
/**
* 产品兼容性数据详细
*/
public function read()
{
$id = request()->param('id');
$data = ProductCompatModel::withoutField(['language_id', 'deleted_at'])
->bypk($id)
->find();
if (empty($data)) {
return error('获取失败');
}
return success("获取成功", $data);
}
/**
* 产品兼容性数据更新
*/
public function update()
{
$id = request()->param('id');
$put = request()->put([
'part_id',
'label_name',
'brand_name',
'level_name',
'class_name',
'series_name',
'model',
'capacity',
'interface_type',
'compat_status',
'sort',
'disabled'
]);
$data = array_merge($put, ['id' => $id, 'language_id' => request()->lang_id]);
// 校验输入
$validate = new ProductCompatValidate;
if (!$validate->scene('edit')->check($data)) {
return error($validate->getError());
}
$compat = ProductCompatModel::bypk($id)->find();
if (empty($compat)) {
return error('请确认操作对象是否正确');
}
if (!$compat->save($data)) {
return error('操作失败');
}
return success('操作成功');
}
/**
* 产品兼容性数据删除
*/
public function delete()
{
$id = request()->param('id');
$data = ProductCompatModel::bypk($id)->find();
if (empty($data)) {
return error("请确认操作对象是否正确");
}
if (!$data->delete()) {
return error("操作失败");
}
return success("操作成功");
}
}

View File

@@ -0,0 +1,133 @@
<?php
declare (strict_types = 1);
namespace app\admin\controller\v1;
use app\admin\model\v1\ProductCompatPartsModel;
use app\admin\validate\v1\ProductCompatPartsValidate;
use think\Request;
class ProductCompatParts
{
/**
* 兼容配件列表
*/
public function index()
{
$params = request()->param([
'name',
'page/d' => 1,
'size/d' => 10
]);
$list = ProductCompatPartsModel::withoutField([
'language_id',
'deleted_at'
])
->language(request()->lang_id)
->withSearch('name', [
'name' => $params['name']??null
])
->order(['id' => 'desc'])
->paginate([
'list_rows' => $params['size'],
'page' => $params['page'],
]);
return success('获取成功', $list);
}
/**
* 添加兼容配件
*/
public function save()
{
$post = request()->post([
'name',
'sort',
'disabled'
]);
$data = array_merge($post, ['language_id' => request()->lang_id]);
// 校验输入
$validate = new ProductCompatPartsValidate;
if (!$validate->scene('add')->check($data)) {
return error($validate->getError());
}
$parts = ProductCompatPartsModel::create($data);
if ($parts->isEmpty()) {
return error('操作失败');
}
return success('操作成功');
}
/**
* 兼容配件详细
*/
public function read()
{
$id = request()->param('id');
$parts = ProductCompatPartsModel::withoutField([
'language_id',
'deleted_at'
])
->bypk($id)
->find();
if (empty($parts)) {
return error('获取失败');
}
return success('获取成功', $parts);
}
/**
* 兼容配件更新
*/
public function update()
{
$id = request()->param('id');
$put = request()->put([
'name',
'sort',
'disabled'
]);
$data = array_merge($put, ['id' => $id, 'language_id' => request()->lang_id]);
$validate = new ProductCompatPartsValidate;
if (!$validate->scene('edit')->check($data)) {
return error($validate->getError());
}
$parts = ProductCompatPartsModel::bypk($id)->find();
if (empty($parts)) {
return error('请确认操作对象是否正确');
}
if (!$parts->save($put)) {
return error('操作失败');
}
return success('操作成功');
}
/**
* 兼容配件删除
*/
public function delete()
{
$id = request()->param('id');
$parts = ProductCompatPartsModel::bypk($id)->find();
if (empty($parts)) {
return error('请确认操作对象是否正确');
}
if (!$parts->delete()) {
return error("操作失败");
}
return success("操作成功");
}
}

View File

@@ -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')
],
]
]
];
}

View File

@@ -151,7 +151,7 @@ class VideoCategory
$validate = new VideoCategoryValidate;
if (!$validate->scene('sort')->check(['id' => $id,'sort' => $sort])) {
return error($validate::getError());
return error($validate->getError());
}
$category = VideoCategoryModel::bypk($id)->find();

View File

@@ -0,0 +1,55 @@
<?php
declare (strict_types = 1);
namespace app\admin\model\v1;
use app\common\model\ProductCompatBaseModel;
use think\model\concern\SoftDelete;
/**
* @mixin \think\Model
*/
class ProductCompatModel extends ProductCompatBaseModel
{
// 启用软件删除
use SoftDelete;
// 软件删除时间字段
protected $deleteTime = 'deleted_at';
// 关联配件类型
public function parts()
{
return $this->hasOne(ProductCompatPartsModel::class, 'id', 'part_id');
}
// 按所属语言查询
public function scopeLanguage(\think\db\Query $query, int $value)
{
$query->where('language_id', $value);
}
// 按型号查询
public function scopeModelName(\think\db\Query $query, string|array $value)
{
if (is_array($value)) {
$query->where('model', 'in', $value);
return;
}
$query->where('model', '=', $value);
}
// 按标签名称搜索
public function searchLabelNameAttr(\think\db\Query $query, string|null $value, array $data)
{
if (is_null($value)) return;
$query->where('label_name', 'like', '%' . $value . '%');
}
// 按型号搜索
public function searchModelAttr(\think\db\Query $query, string|null $value, array $data)
{
if (is_null($value)) return;
$query->where('model', 'like', '%' . $value . '%');
}
}

View File

@@ -0,0 +1,48 @@
<?php
declare (strict_types = 1);
namespace app\admin\model\v1;
use app\common\model\ProductCompatPartsBaseModel;
use think\model\concern\SoftDelete;
/**
* @mixin \think\Model
*/
class ProductCompatPartsModel extends ProductCompatPartsBaseModel
{
// 启用软件删除
use SoftDelete;
// 软件删除时间字段
protected $deleteTime = 'deleted_at';
// 状态获取器
public function getStatusTextAttr($value, array $data)
{
$status = ['启用', '禁用'];
return $status[$data['status']];
}
// 按所属语言查询
public function scopeLanguage(\think\db\Query $query, int $value)
{
$query->where('language_id', $value);
}
// 按类型名称查询
public function scopePartName(\think\db\Query $query, string|array $value)
{
if (is_array($value)) {
$query->where('name', 'in', $value);
return;
}
$query->where('name', '=', $value);
}
// 查询配件类型名称搜索
public function searchNameAttr(\think\db\Query $query, string|null $value, array $data)
{
if (is_null($value)) return;
$query->where('name', 'like', '%' . $value . '%');
}
}

View File

@@ -0,0 +1,14 @@
<?php
declare (strict_types = 1);
namespace app\admin\model\v1;
use app\common\model\ProductCompatRelBaseModel;
/**
* @mixin \think\Model
*/
class ProductCompatRelModel extends ProductCompatRelBaseModel
{
//
}

View File

@@ -366,6 +366,45 @@ Route::group('v1', function () {
// 产品回收站删除
Route::delete('delete/:id', 'ProductTrash/delete');
});
// 产品兼容性数据
Route::group('compat', function () {
// 兼容性数据列表
Route::get('index', 'ProductCompat/index');
// 兼容性数据详情
Route::get('read/:id', 'ProductCompat/read');
// 兼容性数据导入
Route::post('import', 'ProductCompat/import');
// 兼容性数据添加
Route::post('save', 'ProductCompat/save');
// 兼容性数据更新
Route::put('update/:id', 'ProductCompat/update');
// 兼容性数据删除
Route::delete('delete/:id', 'ProductCompat/delete');
// 配件类型管理
Route::group('parts', function () {
// 配件列表
Route::get('index', 'ProductCompatParts/index');
// 配件详情
Route::get('read/:id', 'ProductCompatParts/read');
// 配件添加
Route::post('save', 'ProductCompatParts/save');
// 配件更新
Route::put('update/:id', 'ProductCompatParts/update');
// 配件删除
Route::delete('delete/:id', 'ProductCompatParts/delete');
});
});
});
// 附件(下载管理)

View File

@@ -0,0 +1,46 @@
<?php
declare (strict_types = 1);
namespace app\admin\validate\v1;
use think\Validate;
class ProductCompatPartsValidate extends Validate
{
/**
* 定义验证规则
* 格式:'字段名' => ['规则1','规则2'...]
*
* @var array
*/
protected $rule = [
'language_id' => 'require|integer',
'name' => 'max:128'
];
/**
* 定义错误信息
* 格式:'字段名.规则名' => '错误信息'
*
* @var array
*/
protected $message = [
'id.require' => 'ID不能为空',
'id.integer' => 'ID必须为整数',
'language_id.require' => '语言ID不能为空',
'language_id.integer' => '语言ID必须是整数',
'name.max' => '类型名称不能超过:rule个字符'
];
// 新增场景
protected function sceneAdd()
{
return $this->remove("id", "require|integer");
}
// 编辑场景
protected function sceneEdit()
{
return $this->append("id", "require|integer");
}
}

View File

@@ -0,0 +1,66 @@
<?php
declare (strict_types = 1);
namespace app\admin\validate\v1;
use think\Validate;
class ProductCompatValidate extends Validate
{
/**
* 定义验证规则
* 格式:'字段名' => ['规则1','规则2'...]
*
* @var array
*/
protected $rule = [
'language_id' => 'require|integer',
'part_id' => 'require|integer',
'label_name' =>'max: 128',
'brand_name' => 'max:128',
'level_name' => 'max:32',
'class_name' => 'max:128',
'series_name' => 'max:128',
'model' => 'require|max:128',
'capacity' => 'max:32',
'interface_type' => 'max:64',
'compat_status' => 'max:64'
];
/**
* 定义错误信息
* 格式:'字段名.规则名' => '错误信息'
*
* @var array
*/
protected $message = [
'id.require' => 'ID不能为空',
'id.integer' => 'ID必须为整数',
'language_id.require' => '语言ID必须',
'language_id.integer' => '语言ID必须为整数',
'part_id.require' => '零件ID必须',
'part_id.integer' => '零件ID必须为整数',
'label_name.max' => '标签名称不能超过:rule个字符',
'brand_name.max' => '品牌名称不能超过:rule个字符',
'level_name.max' => '等级名称不能超过:rule个字符',
'class_name.max' => '类型名称不能超过:rule个字符',
'series_name.max' => '系列名称不能超过:rule个字符',
'model.require' => '型号必须',
'model.max' => '型号不能超过:rule个字符',
'capacity.max' => '容量不能超过:rule个字符',
'interface_type.max' => '接口类型不能超过:rule个字符',
'compat_status.max' => '兼容状态不能超过:rule个字符',
];
// 新增场景
protected function sceneAdd()
{
return $this->remove("id", "require|integer");
}
// 编辑场景
protected function sceneEdit()
{
return $this->append("id", "require|integer");
}
}

View File

@@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
namespace app\common\model;
/**
* @mixin \think\Model
*/
class ProductCompatBaseModel extends BaseModel
{
// 表名
protected $name = "product_compat";
// 主键
protected $pk = "id";
// 字段信息
protected $schema = [
"id" => "int",
"language_id" => "int",
"part_id" => "int",
"label_name" => "string",
"brand_name" => "string",
"level_name" => "string",
"class_name" => "string",
"series_name" => "string",
"model" => "string",
"capacity" => "string",
"interface_type" => "string",
"compat_status" => "string",
"sort" => "int",
"disabled" => "int",
"created_at" => "datetime",
"updated_at" => "datetime",
"deleted_at" => "datetime",
];
}

View File

@@ -0,0 +1,28 @@
<?php
declare (strict_types = 1);
namespace app\common\model;
/**
* @mixin \think\Model
*/
class ProductCompatPartsBaseModel extends BaseModel
{
// 表名
protected $name = "product_compat_parts";
// 主键
protected $pk = "id";
// 字段信息
protected $schema = [
"id" => "int",
"language_id" => "int",
"name" => "string",
"sort" => "int",
"disabled" => "int",
"created_at" => "datetime",
"updated_at" => "datetime",
"deleted_at" => "datetime",
];
}

View File

@@ -0,0 +1,29 @@
<?php
declare (strict_types = 1);
namespace app\common\model;
/**
* @mixin \think\Model
*/
class ProductCompatRelBaseModel extends BaseModel
{
// 表名
protected $name = "product_compat_rel";
// 主键
protected $pk = "id";
// 字段信息
protected $schema = [
"id" => "int",
"language_id" => "int",
"product_id" => "int",
"compat_id" => "int",
"series_name" => "string",
"show_series_name" => "int",
"remark" => "int",
"created_at" => "datetime",
"updated_at" => "datetime",
];
}

View 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');
}
}

View File

@@ -119,6 +119,12 @@ Route::group('topic', function () {
// 专题 - 笔记本电脑首页
Route::get('index', 'TopicLaptop/index');
});
// 专题 - SSD
Route::group('ssd', function() {
// 专题 - SSD首页
Route::get('index', 'TopicSsd/index');
});
});
// 数据迁移

View 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}

View File

@@ -14,7 +14,7 @@
<div class="header-nav-item" >
<div class="header-nav-title">{:lang_i18n('产品列表')}</div>
<!-- style=" opacity: 1;" -->
<div class="header-dropdown" >
<div class="header-dropdown" style=" opacity: 1;">
<div class="header-dropdown-tabs">
{volist name="header_categorys" id="vo" key="idx"}
<a href="{:url('product/category', ['id' => $vo.id])}" class="header-tab-item {if condition="$idx == 1"}active{/if}" data-tab="tag-{$vo.id}">{$vo.name}</a>
@@ -350,7 +350,6 @@
if (mhk && mhk.style.display !== 'block') {
// 保存当前滚动位置
scrollTopPosition = window.pageYOffset || document.documentElement.scrollTop;
mhk.style.display = 'block';
// 禁止滚动,保持位置
@@ -377,75 +376,17 @@
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() {
@@ -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;
});
currentOpenDropdown = null;
if (!anyOpen) {
closeOverlay();
}
}
});
observer.observe(dropdown, {
attributes: true,
attributeFilter: ['style']
});
}
// 为每个下拉菜单添加监听
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();
}
}, 200);
});
});
dropdown.addEventListener('mouseleave', (e) => {
isHovering = false;
// 检查鼠标是否移回了导航项
const relatedTarget = e.relatedTarget;
const isMovingToNavItem = item.contains(relatedTarget);
// 点击其他地方关闭所有下拉菜单
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
};
if (isMovingToNavItem) {
return;
}
timer = setTimeout(() => {
if (!isHovering && !item.matches(':hover')) {
hideDropdown();
}
}, 100);
});
});
}
// 初始化(在 DOM 加载完成后执行)
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initHeaderDropdown);
@@ -750,8 +684,8 @@
}
});
// 初始化导航下拉菜单蒙版层
initNavDropdownOverlay();
// // 初始化导航下拉菜单蒙版层
// initNavDropdownOverlay();
// 初始化渲染
renderHistory();

View 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}

View File

@@ -3,7 +3,7 @@
use think\migration\Migrator;
use think\migration\db\Column;
class SysMallStoreEntrance extends Migrator
class CreateSysMallStoreEntrance extends Migrator
{
/**
* Change Method.

View File

@@ -0,0 +1,56 @@
<?php
use think\migration\Migrator;
use think\migration\db\Column;
class CreateProductCompat extends Migrator
{
/**
* Change Method.
*
* Write your reversible migrations using this method.
*
* More information on writing migrations is available here:
* http://docs.phinx.org/en/latest/migrations.html#the-abstractmigration-class
*
* The following commands can be used in this method and Phinx will
* automatically reverse them when rolling back:
*
* createTable
* renameTable
* addColumn
* renameColumn
* addIndex
* addForeignKey
*
* Remember to call "create()" or "update()" and NOT "save()" when working
* with the Table class.
*/
public function change()
{
$table = $this->table('product_compat', [
'engine' => 'MyISAM',
'collation' => 'utf8mb4_general_ci',
'comment' => '产品兼容性表'
]);
$table->addColumn('language_id', 'integer', ['signed' => false, 'null' => false, 'comment' => '语言ID'])
->addColumn('part_id', 'integer', ['null' => false, 'comment' => '配件id'])
->addColumn('label_name', 'string', ['limit' => 128, 'null' => false, 'comment' => '标签名称'])
->addColumn('brand_name', 'string', ['limit' => 128, 'null' => true, 'comment' => '品牌'])
->addColumn('level_name', 'string', ['limit' => 32 , 'null' => true, 'comment' => '级别'])
->addColumn('class_name', 'string', ['limit' => 128, 'null' => true, 'comment' => '类别'])
->addColumn('series_name', 'string', ['limit' => 128, 'null' => true, 'comment' => '系列'])
->addColumn('model', 'string', ['limit' => 128, 'null' => false, 'comment' => '型号'])
->addColumn('capacity', 'string', ['limit' => 32, 'null' => true, 'comment' => '容量'])
->addColumn('interface_type', 'string', ['limit' => 64, 'null' => true, 'comment' => '接口类型'])
->addColumn('compat_status', 'string', ['limit' => 64, 'null' => true, 'comment' => '兼容性状态'])
->addColumn('sort', 'integer', ['default' => 0, 'comment' => '排序'])
->addColumn('disabled', 'boolean', ['default' => 0, 'comment' => '是否禁用 0:启用 1:禁用'])
->addColumn('created_at', 'timestamp', ['null' => false, 'default' => 'CURRENT_TIMESTAMP', 'comment' => '创建时间'])
->addColumn('updated_at', 'timestamp', ['null' => false, 'default' => 'CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP', 'comment' => '更新时间'])
->addColumn('deleted_at', 'timestamp', ['null' => true, 'comment' => '删除时间'])
->addIndex(['language_id'], ['name' => 'idx_language_id'])
->create();
}
}

View File

@@ -0,0 +1,47 @@
<?php
use think\migration\Migrator;
use think\migration\db\Column;
class CreateProductCompatParts extends Migrator
{
/**
* Change Method.
*
* Write your reversible migrations using this method.
*
* More information on writing migrations is available here:
* http://docs.phinx.org/en/latest/migrations.html#the-abstractmigration-class
*
* The following commands can be used in this method and Phinx will
* automatically reverse them when rolling back:
*
* createTable
* renameTable
* addColumn
* renameColumn
* addIndex
* addForeignKey
*
* Remember to call "create()" or "update()" and NOT "save()" when working
* with the Table class.
*/
public function change()
{
$table = $this->table('product_compat_parts', [
'engine' => 'MyISAM',
'collation' => 'utf8mb4_general_ci',
'comment' => '产品兼容性-配件类型表'
]);
$table->addColumn('language_id', 'integer', ['signed' => false, 'null' => false, 'comment' => '语言ID'])
->addColumn('name', 'string', ['limit' => 128, 'null' => false, 'comment' => '类型名称'])
->addColumn('sort', 'integer', ['default' => 0, 'comment' => '排序'])
->addColumn('disabled', 'boolean', ['default' => 0, 'comment' => '是否禁用 0:启用 1:禁用'])
->addColumn('created_at', 'timestamp', ['null' => false, 'default' => 'CURRENT_TIMESTAMP', 'comment' => '创建时间'])
->addColumn('updated_at', 'timestamp', ['null' => false, 'default' => 'CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP', 'comment' => '更新时间'])
->addColumn('deleted_at', 'timestamp', ['null' => true, 'comment' => '删除时间'])
->addIndex(['language_id'], ['name' => 'idx_language_id'])
->create();
}
}

View File

@@ -0,0 +1,48 @@
<?php
use think\migration\Migrator;
use think\migration\db\Column;
class CreateProductCompatRel extends Migrator
{
/**
* Change Method.
*
* Write your reversible migrations using this method.
*
* More information on writing migrations is available here:
* http://docs.phinx.org/en/latest/migrations.html#the-abstractmigration-class
*
* The following commands can be used in this method and Phinx will
* automatically reverse them when rolling back:
*
* createTable
* renameTable
* addColumn
* renameColumn
* addIndex
* addForeignKey
*
* Remember to call "create()" or "update()" and NOT "save()" when working
* with the Table class.
*/
public function change()
{
$table = $this->table('product_compat_rel', [
'engine' => 'InnoDB',
'collation' => 'utf8mb4_general_ci',
'comment' => '产品兼容性关联表'
]);
$table->addColumn('language_id', 'integer', ['signed' => false, 'null' => false, 'comment' => '语言ID'])
->addColumn('product_id', 'integer', ['null' => false, 'comment' => '产品id'])
->addColumn('compat_id', 'json', ['null' => false, 'comment' => '关联的兼容性数据id'])
->addColumn('series_name', 'string', ['limit' => 128, 'null' => true, 'comment' => '产品系列名'])
->addColumn('show_series_name', 'boolean', ['null' => false, 'default' => 0, 'comment' => '是否显示系列名0=否1=是'])
->addColumn('remark', 'string', ['limit' => 512, 'null' => true, 'comment' => '兼容性说明'])
->addColumn('created_at', 'timestamp', ['null' => false, 'default' => 'CURRENT_TIMESTAMP', 'comment' => '创建时间'])
->addColumn('updated_at', 'timestamp', ['null' => false, 'default' => 'CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP', 'comment' => '更新时间'])
->addIndex(['language_id'], ['name' => 'idx_language_id'])
->create();
}
}

View 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;
}

View File

@@ -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;
@@ -339,7 +335,7 @@ a {
display: flex;
flex: 1;
justify-content: space-between;
max-width: 48%;
}
@@ -354,10 +350,7 @@ a {
}
/* 单个分类区块 */
.header-category-block {
/* flex: 1; */
}
.header-category-title {
@@ -399,7 +392,8 @@ a {
justify-content: space-between;
flex: 1;
min-width: 0;
font-size: 16px;
max-width: 748px;
}
@@ -429,12 +423,12 @@ a {
transform: translateY(-2px)
}
.header-product-card:nth-child(1) {
margin-right: 1.5em;
}
/* .header-product-card:nth-child(1) {
} */
.header-product-card:nth-child(3) {
margin-right: 1.5em;
margin-top: 1.5em;
}
@@ -442,20 +436,7 @@ a {
margin-top: 1.5em;
}
/*.header-product-img {*/
/* width: 22.625em;*/
/* height: 12.25em;*/
/* background-color: #f5f5f5;*/
/* display: flex;*/
/* align-items: center;*/
/* justify-content: center;*/
/* color: #999;*/
/*}*/
/*.header-product-img img {*/
/* width: 22.625em;*/
/* height: 12.25em;*/
/*}*/
.header-product-img {
width: 100%;
aspect-ratio: 22.625 / 12.25;
@@ -465,7 +446,6 @@ a {
justify-content: center;
color: #999;
overflow: hidden;
/* border-radius: 0.5em; */
}
.header-product-img img {

View 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;
}

View File

@@ -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,11 +873,15 @@ def main():
# 构建目标服务器配置
target_config_data = config.get("target", {})
target_config_data["type"] = "ssh" # 目标必须是SSH
ssh_config = target_config_data.get("ssh", {})
# 命令行参数覆盖配置文件
if args.target_type:
target_config_data["type"] = args.target_type
# 命令行参数覆盖配置文件
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:
@@ -877,13 +895,6 @@ def main():
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)")
@@ -891,6 +902,16 @@ def main():
if not ssh_config.get("username"):
print("错误: 必须指定目标服务器用户名 (--target-user)")
sys.exit(1)
else:
# 本地类型不需要SSH配置
target_config_data.pop("ssh", None)
# 设置基础目录
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)
# 创建服务器配置对象
try: