Compare commits
22 Commits
v1.3.0
...
d8a584e9b7
| Author | SHA1 | Date | |
|---|---|---|---|
| d8a584e9b7 | |||
| 36261488b4 | |||
| 3545b82015 | |||
| 3717a6a34d | |||
| 5beae1b39b | |||
| d314cfd12a | |||
| 80291c6a6a | |||
| 3d7ed2c16b | |||
| ade2c6bea8 | |||
| 2f4b0f8b76 | |||
| 9fcdea0061 | |||
| d7d8e2aa77 | |||
| cfe07c9df5 | |||
| 5eb49defe1 | |||
| 00e8eed191 | |||
| aeec3e4f0d | |||
| 251a13c368 | |||
| b75a159d70 | |||
| 40d4a41461 | |||
| 456b2ede8a | |||
| 322cfad5d9 | |||
| 9b732b19dc |
@@ -9,9 +9,13 @@ if (!function_exists('xlsx_reader')) {
|
|||||||
* @param array $keys_map 列 -> 键 例:['A' => 'name', 'B' => 'age', 'C' => 'sex']
|
* @param array $keys_map 列 -> 键 例:['A' => 'name', 'B' => 'age', 'C' => 'sex']
|
||||||
* @return array
|
* @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');
|
$reader = \PhpOffice\PhpSpreadsheet\IOFactory::createReader('Xlsx');
|
||||||
|
if ($read_only) {
|
||||||
|
// 只读模式,不读取公式,样式等等,减少内存消耗
|
||||||
|
$reader->setReadDataOnly(true);
|
||||||
|
}
|
||||||
|
|
||||||
// 读取文件
|
// 读取文件
|
||||||
$spreadsheet = $reader->load($file);
|
$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')) {
|
if (!function_exists('xlsx_writer')) {
|
||||||
/**
|
/**
|
||||||
* 构建Excel数据
|
* 构建Excel数据
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ declare (strict_types = 1);
|
|||||||
|
|
||||||
namespace app\admin\controller\v1;
|
namespace app\admin\controller\v1;
|
||||||
|
|
||||||
|
use app\admin\model\v1\ProductCompatRelModel;
|
||||||
use app\admin\model\v1\ProductModel;
|
use app\admin\model\v1\ProductModel;
|
||||||
use app\admin\model\v1\ProductParamsModel;
|
use app\admin\model\v1\ProductParamsModel;
|
||||||
use app\admin\model\v1\ProductRelatedModel;
|
use app\admin\model\v1\ProductRelatedModel;
|
||||||
@@ -119,6 +120,19 @@ class Product
|
|||||||
->bindAttr('product', ['spu'])
|
->bindAttr('product', ['spu'])
|
||||||
->hidden(['product']);
|
->hidden(['product']);
|
||||||
|
|
||||||
|
// 兼容性信息
|
||||||
|
$product->compat = ProductCompatRelModel::withoutField([
|
||||||
|
'created_at',
|
||||||
|
'updated_at',
|
||||||
|
])
|
||||||
|
->with(['compat' => function($query) {
|
||||||
|
$query->field(['id', 'label_name']);
|
||||||
|
}])
|
||||||
|
->productId($product->id)
|
||||||
|
->select()
|
||||||
|
?->bindAttr('compat', ['label_name'])
|
||||||
|
?->hidden(['compat']);
|
||||||
|
|
||||||
return success('获取成功', $product);
|
return success('获取成功', $product);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -144,6 +158,7 @@ class Product
|
|||||||
'params' => '',
|
'params' => '',
|
||||||
'skus' => '',
|
'skus' => '',
|
||||||
'related' => '',
|
'related' => '',
|
||||||
|
'compat' => '',
|
||||||
'seo_title',
|
'seo_title',
|
||||||
'seo_keywords',
|
'seo_keywords',
|
||||||
'seo_desc'
|
'seo_desc'
|
||||||
@@ -249,6 +264,27 @@ class Product
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 更新兼容性信息
|
||||||
|
if (!empty($put['compat'])) {
|
||||||
|
// 删除原先的兼容信息
|
||||||
|
ProductCompatRelModel::productId($id)->delete();
|
||||||
|
|
||||||
|
$compat = [];
|
||||||
|
$rel_compat = json_decode(data_get($put['compat'], 'compat_id') ?? [], true);
|
||||||
|
foreach ($rel_compat as $compat_id) {
|
||||||
|
$compat[] = [
|
||||||
|
'product_id' => $id,
|
||||||
|
'compat_id' => $compat_id,
|
||||||
|
'series_name' => data_get($put['compat'], 'series_name'),
|
||||||
|
'show_series_name' => data_get($put['compat'], 'show_series_name') ?? 0,
|
||||||
|
'remark' => data_get($put['compat'], 'remark')
|
||||||
|
];
|
||||||
|
}
|
||||||
|
if (!empty($compat)) {
|
||||||
|
ProductCompatRelModel::insertAll($compat);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return success('操作成功');
|
return success('操作成功');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
312
app/admin/controller/v1/ProductCompat.php
Normal file
312
app/admin/controller/v1/ProductCompat.php
Normal 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("操作成功");
|
||||||
|
}
|
||||||
|
}
|
||||||
133
app/admin/controller/v1/ProductCompatParts.php
Normal file
133
app/admin/controller/v1/ProductCompatParts.php
Normal 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("操作成功");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -366,6 +366,18 @@ class System
|
|||||||
'url' => (string)url('/index/topic/laptop/index')
|
'url' => (string)url('/index/topic/laptop/index')
|
||||||
],
|
],
|
||||||
]
|
]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'id' => 9,
|
||||||
|
'name' => '闪存(SSD)专题',
|
||||||
|
'url' => '',
|
||||||
|
'children' => [
|
||||||
|
[
|
||||||
|
'id' => 101,
|
||||||
|
'name' => '首页',
|
||||||
|
'url' => (string)url('/index/topic/ssd/index')
|
||||||
|
],
|
||||||
|
]
|
||||||
]
|
]
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -151,7 +151,7 @@ class VideoCategory
|
|||||||
|
|
||||||
$validate = new VideoCategoryValidate;
|
$validate = new VideoCategoryValidate;
|
||||||
if (!$validate->scene('sort')->check(['id' => $id,'sort' => $sort])) {
|
if (!$validate->scene('sort')->check(['id' => $id,'sort' => $sort])) {
|
||||||
return error($validate::getError());
|
return error($validate->getError());
|
||||||
}
|
}
|
||||||
|
|
||||||
$category = VideoCategoryModel::bypk($id)->find();
|
$category = VideoCategoryModel::bypk($id)->find();
|
||||||
|
|||||||
55
app/admin/model/v1/ProductCompatModel.php
Normal file
55
app/admin/model/v1/ProductCompatModel.php
Normal 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 . '%');
|
||||||
|
}
|
||||||
|
}
|
||||||
48
app/admin/model/v1/ProductCompatPartsModel.php
Normal file
48
app/admin/model/v1/ProductCompatPartsModel.php
Normal 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 . '%');
|
||||||
|
}
|
||||||
|
}
|
||||||
24
app/admin/model/v1/ProductCompatRelModel.php
Normal file
24
app/admin/model/v1/ProductCompatRelModel.php
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
<?php
|
||||||
|
declare (strict_types = 1);
|
||||||
|
|
||||||
|
namespace app\admin\model\v1;
|
||||||
|
|
||||||
|
use app\common\model\ProductCompatRelBaseModel;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @mixin \think\Model
|
||||||
|
*/
|
||||||
|
class ProductCompatRelModel extends ProductCompatRelBaseModel
|
||||||
|
{
|
||||||
|
// 关联兼容性数据
|
||||||
|
public function compat()
|
||||||
|
{
|
||||||
|
return $this->hasOne(ProductCompatModel::class, 'id', 'compat_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 按所属产品查询
|
||||||
|
public function scopeProductId(\think\db\Query $query, int $id)
|
||||||
|
{
|
||||||
|
$query->where('product_id', '=', $id);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -366,6 +366,45 @@ Route::group('v1', function () {
|
|||||||
// 产品回收站删除
|
// 产品回收站删除
|
||||||
Route::delete('delete/:id', 'ProductTrash/delete');
|
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');
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// 附件(下载管理)
|
// 附件(下载管理)
|
||||||
|
|||||||
46
app/admin/validate/v1/ProductCompatPartsValidate.php
Normal file
46
app/admin/validate/v1/ProductCompatPartsValidate.php
Normal 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");
|
||||||
|
}
|
||||||
|
}
|
||||||
66
app/admin/validate/v1/ProductCompatValidate.php
Normal file
66
app/admin/validate/v1/ProductCompatValidate.php
Normal 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");
|
||||||
|
}
|
||||||
|
}
|
||||||
37
app/common/model/ProductCompatBaseModel.php
Normal file
37
app/common/model/ProductCompatBaseModel.php
Normal 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",
|
||||||
|
];
|
||||||
|
}
|
||||||
28
app/common/model/ProductCompatPartsBaseModel.php
Normal file
28
app/common/model/ProductCompatPartsBaseModel.php
Normal 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",
|
||||||
|
];
|
||||||
|
}
|
||||||
29
app/common/model/ProductCompatRelBaseModel.php
Normal file
29
app/common/model/ProductCompatRelBaseModel.php
Normal 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",
|
||||||
|
];
|
||||||
|
}
|
||||||
52
app/index/controller/TopicSsd.php
Normal file
52
app/index/controller/TopicSsd.php
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
<?php
|
||||||
|
declare (strict_types = 1);
|
||||||
|
|
||||||
|
namespace app\index\controller;
|
||||||
|
|
||||||
|
use app\index\model\SysBannerModel;
|
||||||
|
use think\facade\View;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 专题 - SSD
|
||||||
|
*/
|
||||||
|
class TopicSsd extends Common
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* 专题 - SSD首页
|
||||||
|
*/
|
||||||
|
public function index()
|
||||||
|
{
|
||||||
|
$banners = SysBannerModel::with([
|
||||||
|
'items' => function ($query) {
|
||||||
|
$query->withoutField(['sort', 'created_at', 'updated_at', 'deleted_at'])
|
||||||
|
->order(['sort' => 'asc', 'id' => 'desc'])
|
||||||
|
->enabled(true);
|
||||||
|
}
|
||||||
|
])
|
||||||
|
->atPlatform(request()->from)
|
||||||
|
->uniqueLabel([
|
||||||
|
'BANNER_69faaf8582967', // 专题 - 闪存(SSD)首页 - 焦点图
|
||||||
|
'BANNER_69fab1bed8f71', // 专题 - 闪存(SSD)首页 - 产品
|
||||||
|
])
|
||||||
|
->language($this->lang_id)
|
||||||
|
->enabled(true)
|
||||||
|
->order(['sort' => 'asc', 'id' => 'desc'])
|
||||||
|
->select();
|
||||||
|
|
||||||
|
$data = [];
|
||||||
|
if (!$banners->isEmpty()) {
|
||||||
|
$banners_map = [];
|
||||||
|
foreach ($banners as $banner) {
|
||||||
|
$banners_map[$banner->unique_label] = $banner;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 焦点图轮播图
|
||||||
|
$data['top_focus_images'] = data_get($banners_map, 'BANNER_69faaf8582967')?->items->toArray();
|
||||||
|
// 产品
|
||||||
|
$data['products'] = data_get($banners_map, 'BANNER_69fab1bed8f71')?->items->toArray();
|
||||||
|
}
|
||||||
|
View::assign('data', $data);
|
||||||
|
|
||||||
|
return View::fetch('index');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -119,6 +119,12 @@ Route::group('topic', function () {
|
|||||||
// 专题 - 笔记本电脑首页
|
// 专题 - 笔记本电脑首页
|
||||||
Route::get('index', 'TopicLaptop/index');
|
Route::get('index', 'TopicLaptop/index');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 专题 - SSD
|
||||||
|
Route::group('ssd', function() {
|
||||||
|
// 专题 - SSD首页
|
||||||
|
Route::get('index', 'TopicSsd/index');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// 数据迁移
|
// 数据迁移
|
||||||
|
|||||||
43
app/index/view/mobile/topic_ssd/index.html
Normal file
43
app/index/view/mobile/topic_ssd/index.html
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
{extend name="public/base" /}
|
||||||
|
{block name="style"}
|
||||||
|
<link rel="stylesheet" href="__CSS__/topic_laptop/header.css">
|
||||||
|
<link rel="stylesheet" href="__CSS__/topic_laptop/footer.css">
|
||||||
|
<link rel="stylesheet" href="__CSS__/topic_ssd/index.css">
|
||||||
|
<!-- 将rem适配JS移到这里,确保优先执行 -->
|
||||||
|
<script type="text/javascript">
|
||||||
|
(function (doc, win)
|
||||||
|
{
|
||||||
|
var docEl = doc.documentElement;
|
||||||
|
var resizeEvt = 'orientationchange' in window ? 'orientationchange' : 'resize';
|
||||||
|
|
||||||
|
function setRootFontSize ()
|
||||||
|
{
|
||||||
|
var clientWidth = docEl.clientWidth;
|
||||||
|
if (!clientWidth) return;
|
||||||
|
var fontSize = clientWidth / 7.5;
|
||||||
|
// 直接修改内联样式,优先级最高
|
||||||
|
docEl.setAttribute('style', 'font-size: ' + fontSize + 'px !important;');
|
||||||
|
}
|
||||||
|
|
||||||
|
setRootFontSize();
|
||||||
|
win.addEventListener(resizeEvt, setRootFontSize);
|
||||||
|
doc.addEventListener('DOMContentLoaded', setRootFontSize);
|
||||||
|
})(document, window);
|
||||||
|
</script>
|
||||||
|
{/block}
|
||||||
|
{block name="main"}
|
||||||
|
<div class="m-sc-main" style="margin-top:42px;">
|
||||||
|
{volist name="data.top_focus_images" id="item"}
|
||||||
|
<a href="{$item.link}" class="m-sc-mt20">
|
||||||
|
<img src="{$item.image}" alt="" class="m-sc-main-img" loading="lazy">
|
||||||
|
</a>
|
||||||
|
{/volist}
|
||||||
|
<div class="m-sc-main-imgs m-sc-mt20 m-sc-mb34">
|
||||||
|
{volist name="data.products" id="item"}
|
||||||
|
<a href="{$item.link}" class="">
|
||||||
|
<img src="{$item.image}" alt="" class="m-sc-main-img1" loading="lazy">
|
||||||
|
</a>
|
||||||
|
{/volist}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/block}
|
||||||
@@ -11,9 +11,10 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="header-nav-items">
|
<div class="header-nav-items">
|
||||||
{if condition="!empty($header_categorys)"}
|
{if condition="!empty($header_categorys)"}
|
||||||
<div class="header-nav-item">
|
<div class="header-nav-item" >
|
||||||
<div class="header-nav-title">{:lang_i18n('产品列表')}</div>
|
<div class="header-nav-title">{:lang_i18n('产品列表')}</div>
|
||||||
<div class="header-dropdown" >
|
<!-- style=" opacity: 1;" -->
|
||||||
|
<div class="header-dropdown" style=" opacity: 1;">
|
||||||
<div class="header-dropdown-tabs">
|
<div class="header-dropdown-tabs">
|
||||||
{volist name="header_categorys" id="vo" key="idx"}
|
{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>
|
<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>
|
||||||
@@ -190,40 +191,41 @@
|
|||||||
</div>
|
</div>
|
||||||
<div style="width: 100vw;height: 64px;"></div>
|
<div style="width: 100vw;height: 64px;"></div>
|
||||||
<div id="mhk"></div>
|
<div id="mhk"></div>
|
||||||
<script>
|
|
||||||
|
|
||||||
// // 获取所有包含商品卡片的元素
|
<script>
|
||||||
|
// 获取所有包含商品卡片的元素
|
||||||
const productCards = document.querySelectorAll('.header-buy-product-card');
|
const productCards = document.querySelectorAll('.header-buy-product-card');
|
||||||
|
|
||||||
productCards.forEach(card => {
|
productCards.forEach(card => {
|
||||||
// 获取卡片内的图片容器和两张图片
|
// 获取卡片内的图片容器和两张图片
|
||||||
const imgContainer = card.querySelector('.header-buy-product-img');
|
const imgContainer = card.querySelector('.header-buy-product-img');
|
||||||
const defaultImg = imgContainer?.querySelector('.header-default-img');
|
const defaultImg = imgContainer?.querySelector('.header-default-img');
|
||||||
const hoverImg = imgContainer?.querySelector('.header-hover-img');
|
const hoverImg = imgContainer?.querySelector('.header-hover-img');
|
||||||
|
|
||||||
// 检查悬浮图是否存在(因为HTML中用了if判断,所以可能有也可能没有这个元素)
|
// 检查悬浮图是否存在(因为HTML中用了if判断,所以可能有也可能没有这个元素)
|
||||||
const hasHoverImg = hoverImg !== null;
|
const hasHoverImg = hoverImg !== null;
|
||||||
|
|
||||||
// 如果没有悬浮图,不需要任何交互,直接返回
|
// 如果没有悬浮图,不需要任何交互,直接返回
|
||||||
if (!hasHoverImg) return;
|
if (!hasHoverImg) return;
|
||||||
|
|
||||||
// 初始状态确保普通图显示,悬浮图隐藏
|
// 初始状态确保普通图显示,悬浮图隐藏
|
||||||
|
defaultImg.style.opacity = '1';
|
||||||
|
hoverImg.style.opacity = '0';
|
||||||
|
|
||||||
|
// 鼠标移入事件:显示悬浮图,隐藏普通图
|
||||||
|
card.addEventListener('mouseenter', () => {
|
||||||
|
defaultImg.style.opacity = '0';
|
||||||
|
hoverImg.style.opacity = '1';
|
||||||
|
});
|
||||||
|
|
||||||
|
// 鼠标移出事件:恢复普通图,隐藏悬浮图
|
||||||
|
card.addEventListener('mouseleave', () => {
|
||||||
defaultImg.style.opacity = '1';
|
defaultImg.style.opacity = '1';
|
||||||
hoverImg.style.opacity = '0';
|
hoverImg.style.opacity = '0';
|
||||||
|
});
|
||||||
// 鼠标移入事件:显示悬浮图,隐藏普通图
|
|
||||||
card.addEventListener('mouseenter', () => {
|
|
||||||
defaultImg.style.opacity = '0';
|
|
||||||
hoverImg.style.opacity = '1';
|
|
||||||
});
|
|
||||||
|
|
||||||
// 鼠标移出事件:恢复普通图,隐藏悬浮图
|
|
||||||
card.addEventListener('mouseleave', () => {
|
|
||||||
defaultImg.style.opacity = '1';
|
|
||||||
hoverImg.style.opacity = '0';
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
//产品列表在hover复位tabs选择
|
|
||||||
|
// 产品列表在hover复位tabs选择
|
||||||
const productNavContainer = document.querySelector('.header-nav-item:first-child');
|
const productNavContainer = document.querySelector('.header-nav-item:first-child');
|
||||||
|
|
||||||
if (productNavContainer) {
|
if (productNavContainer) {
|
||||||
@@ -247,20 +249,15 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const tabItems1 = document.querySelectorAll('.header-tab-item');
|
||||||
|
|
||||||
const tabItems = document.querySelectorAll('.header-tab-item');
|
|
||||||
const tabContents = document.querySelectorAll('.header-tab-content');
|
const tabContents = document.querySelectorAll('.header-tab-content');
|
||||||
|
|
||||||
tabItems.forEach(tab =>
|
tabItems1.forEach(tab => {
|
||||||
{
|
tab.addEventListener('mouseenter', () => {
|
||||||
tab.addEventListener('mouseenter', () =>
|
tabItems1.forEach(item => item.classList.remove('active'));
|
||||||
{
|
|
||||||
tabItems.forEach(item => item.classList.remove('active'));
|
|
||||||
tab.classList.add('active');
|
tab.classList.add('active');
|
||||||
const tabId = tab.getAttribute('data-tab');
|
const tabId = tab.getAttribute('data-tab');
|
||||||
tabContents.forEach(content =>
|
tabContents.forEach(content => {
|
||||||
{
|
|
||||||
content.classList.remove('active');
|
content.classList.remove('active');
|
||||||
if (content.id === tabId) {
|
if (content.id === tabId) {
|
||||||
content.classList.add('active');
|
content.classList.add('active');
|
||||||
@@ -271,33 +268,31 @@
|
|||||||
|
|
||||||
let searchHistory = JSON.parse(localStorage.getItem('searchHistory') || '[]');
|
let searchHistory = JSON.parse(localStorage.getItem('searchHistory') || '[]');
|
||||||
|
|
||||||
function renderHistory ()
|
function renderHistory() {
|
||||||
{
|
|
||||||
const historyList = document.getElementById('historyList');
|
const historyList = document.getElementById('historyList');
|
||||||
const historySection = document.getElementById('searchHistory');
|
const historySection = document.getElementById('searchHistory');
|
||||||
if (searchHistory.length === 0) {
|
if (searchHistory.length === 0) {
|
||||||
historySection.style.display = 'none';
|
if (historySection) historySection.style.display = 'none';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
historySection.style.display = 'block';
|
if (historySection) historySection.style.display = 'block';
|
||||||
historyList.innerHTML = searchHistory.map((item, index) => `
|
if (historyList) {
|
||||||
<div class="header-history-item" data-keyword="${item}">
|
historyList.innerHTML = searchHistory.map((item, index) => `
|
||||||
<span>${item}</span>
|
<div class="header-history-item" data-keyword="${item}">
|
||||||
<span class="header-delete-icon" data-index="${index}">✕</span>
|
<span>${item}</span>
|
||||||
</div>
|
<span class="header-delete-icon" data-index="${index}">✕</span>
|
||||||
`).join('');
|
</div>
|
||||||
document.querySelectorAll('.header-history-item').forEach(item =>
|
`).join('');
|
||||||
{
|
}
|
||||||
|
document.querySelectorAll('.header-history-item').forEach(item => {
|
||||||
const keyword = item.getAttribute('data-keyword');
|
const keyword = item.getAttribute('data-keyword');
|
||||||
const deleteBtn = item.querySelector('.header-delete-icon');
|
const deleteBtn = item.querySelector('.header-delete-icon');
|
||||||
item.addEventListener('click', (e) =>
|
item.addEventListener('click', (e) => {
|
||||||
{
|
|
||||||
if (e.target === deleteBtn) return;
|
if (e.target === deleteBtn) return;
|
||||||
performSearch(keyword);
|
performSearch(keyword);
|
||||||
});
|
});
|
||||||
if (deleteBtn) {
|
if (deleteBtn) {
|
||||||
deleteBtn.addEventListener('click', (e) =>
|
deleteBtn.addEventListener('click', (e) => {
|
||||||
{
|
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
const index = parseInt(deleteBtn.getAttribute('data-index'));
|
const index = parseInt(deleteBtn.getAttribute('data-index'));
|
||||||
searchHistory.splice(index, 1);
|
searchHistory.splice(index, 1);
|
||||||
@@ -308,8 +303,7 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function addToHistory (keyword)
|
function addToHistory(keyword) {
|
||||||
{
|
|
||||||
if (!keyword.trim()) return;
|
if (!keyword.trim()) return;
|
||||||
searchHistory = searchHistory.filter(item => item !== keyword);
|
searchHistory = searchHistory.filter(item => item !== keyword);
|
||||||
searchHistory.unshift(keyword);
|
searchHistory.unshift(keyword);
|
||||||
@@ -320,16 +314,14 @@
|
|||||||
|
|
||||||
const clearHistoryBtn = document.getElementById('clearHistory');
|
const clearHistoryBtn = document.getElementById('clearHistory');
|
||||||
if (clearHistoryBtn) {
|
if (clearHistoryBtn) {
|
||||||
clearHistoryBtn.addEventListener('click', () =>
|
clearHistoryBtn.addEventListener('click', () => {
|
||||||
{
|
|
||||||
searchHistory = [];
|
searchHistory = [];
|
||||||
localStorage.setItem('searchHistory', JSON.stringify(searchHistory));
|
localStorage.setItem('searchHistory', JSON.stringify(searchHistory));
|
||||||
renderHistory();
|
renderHistory();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function performSearch (keyword)
|
function performSearch(keyword) {
|
||||||
{
|
|
||||||
if (keyword.trim()) {
|
if (keyword.trim()) {
|
||||||
addToHistory(keyword);
|
addToHistory(keyword);
|
||||||
window.location.href = "{:url('product/search')}?keywords=" + encodeURIComponent(keyword);
|
window.location.href = "{:url('product/search')}?keywords=" + encodeURIComponent(keyword);
|
||||||
@@ -344,67 +336,119 @@
|
|||||||
const searchSubmit = document.querySelector('.header-search-submit');
|
const searchSubmit = document.querySelector('.header-search-submit');
|
||||||
const langDropdown = document.getElementById('langDropdown');
|
const langDropdown = document.getElementById('langDropdown');
|
||||||
const buyDropdown = document.getElementById('buyDropdown');
|
const buyDropdown = document.getElementById('buyDropdown');
|
||||||
|
const mhk = document.getElementById('mhk');
|
||||||
|
const body = document.body;
|
||||||
|
|
||||||
const langCloseBtn = document.querySelector('.header-lang-dropdown-delete-icon');
|
const langCloseBtn = document.querySelector('.header-lang-dropdown-delete-icon');
|
||||||
const searchCloseBtn = document.querySelector('.header-search-dropdown-delete-icon');
|
const searchCloseBtn = document.querySelector('.header-search-dropdown-delete-icon');
|
||||||
|
|
||||||
|
// 保存滚动位置
|
||||||
|
let scrollTopPosition = 0;
|
||||||
|
|
||||||
|
// 打开蒙版层(禁止滚动,不抖动)
|
||||||
|
function openOverlay() {
|
||||||
|
if (mhk && mhk.style.display !== 'block') {
|
||||||
|
// 保存当前滚动位置
|
||||||
|
scrollTopPosition = window.pageYOffset || document.documentElement.scrollTop;
|
||||||
|
mhk.style.display = 'block';
|
||||||
|
|
||||||
|
// 禁止滚动,保持位置
|
||||||
|
body.style.position = 'fixed';
|
||||||
|
body.style.top = `-${scrollTopPosition}px`;
|
||||||
|
body.style.left = '0';
|
||||||
|
body.style.right = '0';
|
||||||
|
body.style.width = '100%';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 关闭蒙版层(恢复滚动)
|
||||||
|
function closeOverlay() {
|
||||||
|
if (mhk && mhk.style.display === 'block') {
|
||||||
|
const isBuyOpen = buyDropdown && buyDropdown.classList.contains('show');
|
||||||
|
const isSearchOpen = searchDropdown && searchDropdown.classList.contains('show');
|
||||||
|
const isLangOpen = langDropdown && langDropdown.classList.contains('show');
|
||||||
|
|
||||||
|
// 检查是否有任何导航下拉菜单打开
|
||||||
|
let hasNavOpen = false;
|
||||||
|
document.querySelectorAll('.header-dropdown').forEach(dropdown => {
|
||||||
|
const style = window.getComputedStyle(dropdown);
|
||||||
|
if (style.opacity === '1') {
|
||||||
|
hasNavOpen = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
mhk.style.display = 'none';
|
||||||
|
// 恢复滚动
|
||||||
|
body.style.position = '';
|
||||||
|
body.style.top = '';
|
||||||
|
body.style.left = '';
|
||||||
|
body.style.right = '';
|
||||||
|
body.style.width = '';
|
||||||
|
// 恢复滚动位置
|
||||||
|
window.scrollTo(0, scrollTopPosition);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 关闭所有导航下拉菜单
|
||||||
|
function closeAllNavDropdowns() {
|
||||||
|
document.querySelectorAll('.header-dropdown').forEach(dropdown => {
|
||||||
|
dropdown.style.opacity = '';
|
||||||
|
dropdown.style.transform = '';
|
||||||
|
dropdown.style.pointerEvents = '';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// 语言关闭按钮
|
// 语言关闭按钮
|
||||||
if (langCloseBtn && langCloseBtn) {
|
if (langCloseBtn) {
|
||||||
langCloseBtn.addEventListener('click', (e) =>
|
langCloseBtn.addEventListener('click', (e) => {
|
||||||
{
|
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
langDropdown.classList.remove('show');
|
if (langDropdown) langDropdown.classList.remove('show');
|
||||||
|
closeOverlay();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
//搜索关闭
|
|
||||||
if (searchCloseBtn && searchCloseBtn) {
|
|
||||||
searchCloseBtn.addEventListener('click', (e) =>
|
|
||||||
{
|
|
||||||
|
|
||||||
|
// 搜索关闭按钮
|
||||||
|
if (searchCloseBtn) {
|
||||||
|
searchCloseBtn.addEventListener('click', (e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
searchDropdown.classList.remove('show');
|
if (searchDropdown) searchDropdown.classList.remove('show');
|
||||||
|
closeOverlay();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
// 鼠标移入导航项时,关闭购买下拉框
|
|
||||||
|
// 鼠标移入导航项时,关闭其他弹窗
|
||||||
const navItems = document.querySelectorAll('.header-nav-item');
|
const navItems = document.querySelectorAll('.header-nav-item');
|
||||||
navItems.forEach(item =>
|
navItems.forEach(item => {
|
||||||
{
|
item.addEventListener('mouseenter', () => {
|
||||||
|
if (buyDropdown) buyDropdown.classList.remove('show');
|
||||||
item.addEventListener('mouseenter', () =>
|
if (langDropdown) langDropdown.classList.remove('show');
|
||||||
{
|
if (searchDropdown) searchDropdown.classList.remove('show');
|
||||||
|
|
||||||
buyDropdown.classList.remove('show');
|
|
||||||
langDropdown.classList.remove('show');
|
|
||||||
searchDropdown.classList.remove('show');
|
|
||||||
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 搜索按钮点击
|
||||||
if (searchBtn && searchDropdown) {
|
if (searchBtn && searchDropdown) {
|
||||||
searchBtn.addEventListener('click', (e) =>
|
searchBtn.addEventListener('click', (e) => {
|
||||||
{
|
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
if (langDropdown) langDropdown.classList.remove('show');
|
if (langDropdown) langDropdown.classList.remove('show');
|
||||||
if (buyDropdown) buyDropdown.classList.remove('show');
|
if (buyDropdown) buyDropdown.classList.remove('show');
|
||||||
searchDropdown.classList.toggle('show');
|
searchDropdown.classList.toggle('show');
|
||||||
if (searchDropdown.classList.contains('show')) {
|
if (searchDropdown.classList.contains('show')) {
|
||||||
renderHistory();
|
renderHistory();
|
||||||
|
openOverlay();
|
||||||
|
} else {
|
||||||
|
closeOverlay();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (searchSubmit) {
|
if (searchSubmit) {
|
||||||
searchSubmit.addEventListener('click', () =>
|
searchSubmit.addEventListener('click', () => {
|
||||||
{
|
|
||||||
performSearch(searchInput.value);
|
performSearch(searchInput.value);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (searchInput) {
|
if (searchInput) {
|
||||||
searchInput.addEventListener('keypress', (e) =>
|
searchInput.addEventListener('keypress', (e) => {
|
||||||
{
|
|
||||||
if (e.key === 'Enter') {
|
if (e.key === 'Enter') {
|
||||||
performSearch(searchInput.value);
|
performSearch(searchInput.value);
|
||||||
}
|
}
|
||||||
@@ -412,37 +456,44 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
const hotProducts = document.querySelectorAll('.header-hot-product-item');
|
const hotProducts = document.querySelectorAll('.header-hot-product-item');
|
||||||
hotProducts.forEach(product =>
|
hotProducts.forEach(product => {
|
||||||
{
|
product.addEventListener('click', () => {
|
||||||
product.addEventListener('click', () =>
|
|
||||||
{
|
|
||||||
const keyword = product.getAttribute('data-keyword');
|
const keyword = product.getAttribute('data-keyword');
|
||||||
if (keyword) performSearch(keyword);
|
if (keyword) performSearch(keyword);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 语言按钮点击
|
||||||
const langBtn = document.querySelector('.header-lang-wrapper .header-nav-btn');
|
const langBtn = document.querySelector('.header-lang-wrapper .header-nav-btn');
|
||||||
if (langBtn && langDropdown) {
|
if (langBtn && langDropdown) {
|
||||||
langBtn.addEventListener('click', (e) =>
|
langBtn.addEventListener('click', (e) => {
|
||||||
{
|
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
if (searchDropdown) searchDropdown.classList.remove('show');
|
if (searchDropdown) searchDropdown.classList.remove('show');
|
||||||
if (buyDropdown) buyDropdown.classList.remove('show');
|
if (buyDropdown) buyDropdown.classList.remove('show');
|
||||||
langDropdown.classList.toggle('show');
|
langDropdown.classList.toggle('show');
|
||||||
|
if (langDropdown.classList.contains('show')) {
|
||||||
|
openOverlay();
|
||||||
|
} else {
|
||||||
|
closeOverlay();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 购买按钮点击
|
||||||
const buyWrapper = document.querySelector('.header-buy-wrapper');
|
const buyWrapper = document.querySelector('.header-buy-wrapper');
|
||||||
if (buyWrapper && buyDropdown) {
|
if (buyWrapper && buyDropdown) {
|
||||||
buyWrapper.addEventListener('click', () =>
|
buyWrapper.addEventListener('click', () => {
|
||||||
{
|
|
||||||
// buyDropdown.classList.add('show');
|
|
||||||
buyDropdown.classList.toggle('show');
|
buyDropdown.classList.toggle('show');
|
||||||
|
if (buyDropdown.classList.contains('show')) {
|
||||||
|
openOverlay();
|
||||||
|
} else {
|
||||||
|
closeOverlay();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
document.addEventListener('click', (e) =>
|
// 全局点击关闭
|
||||||
{
|
document.addEventListener('click', (e) => {
|
||||||
if (!e.target.closest('.header-search-wrapper')) {
|
if (!e.target.closest('.header-search-wrapper')) {
|
||||||
if (searchDropdown) searchDropdown.classList.remove('show');
|
if (searchDropdown) searchDropdown.classList.remove('show');
|
||||||
}
|
}
|
||||||
@@ -452,438 +503,195 @@
|
|||||||
if (!e.target.closest('.header-buy-wrapper')) {
|
if (!e.target.closest('.header-buy-wrapper')) {
|
||||||
if (buyDropdown) buyDropdown.classList.remove('show');
|
if (buyDropdown) buyDropdown.classList.remove('show');
|
||||||
}
|
}
|
||||||
|
// 关闭所有导航下拉菜单
|
||||||
|
if (!e.target.closest('.header-nav-item')) {
|
||||||
|
closeAllNavDropdowns();
|
||||||
|
closeOverlay();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 阻止弹窗冒泡
|
||||||
if (searchDropdown) searchDropdown.addEventListener('click', (e) => e.stopPropagation());
|
if (searchDropdown) searchDropdown.addEventListener('click', (e) => e.stopPropagation());
|
||||||
if (langDropdown) langDropdown.addEventListener('click', (e) => e.stopPropagation());
|
if (langDropdown) langDropdown.addEventListener('click', (e) => e.stopPropagation());
|
||||||
if (buyDropdown) buyDropdown.addEventListener('click', (e) => e.stopPropagation());
|
if (buyDropdown) buyDropdown.addEventListener('click', (e) => e.stopPropagation());
|
||||||
|
|
||||||
renderHistory();
|
// 蒙版层点击关闭
|
||||||
|
if (mhk) {
|
||||||
// ========== 蒙版层功能(支持所有弹窗,包括导航悬停下拉) ==========
|
mhk.addEventListener('click', () => {
|
||||||
const mhk = document.getElementById('mhk');
|
if (searchDropdown) searchDropdown.classList.remove('show');
|
||||||
const body = document.body;
|
if (langDropdown) langDropdown.classList.remove('show');
|
||||||
|
if (buyDropdown) buyDropdown.classList.remove('show');
|
||||||
// 获取滚动条宽度(防止打开弹窗时页面抖动)
|
closeAllNavDropdowns();
|
||||||
function getScrollbarWidth() {
|
closeOverlay();
|
||||||
const scrollDiv = document.createElement('div');
|
|
||||||
scrollDiv.style.cssText = `
|
|
||||||
width: 100px;
|
|
||||||
height: 100px;
|
|
||||||
overflow: scroll;
|
|
||||||
position: absolute;
|
|
||||||
top: -9999px;
|
|
||||||
`;
|
|
||||||
document.body.appendChild(scrollDiv);
|
|
||||||
const scrollbarWidth = scrollDiv.offsetWidth - scrollDiv.clientWidth;
|
|
||||||
console.log(scrollbarWidth,'=scrollbarWidth=')
|
|
||||||
document.body.removeChild(scrollDiv);
|
|
||||||
|
|
||||||
return scrollbarWidth;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 打开蒙版层的函数
|
|
||||||
function openOverlay() {
|
|
||||||
if (mhk && mhk.style.display !== 'block') {
|
|
||||||
mhk.style.display = 'block';
|
|
||||||
body.style.overflow = 'hidden';
|
|
||||||
body.style.paddingRight = getScrollbarWidth() + 'px';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 关闭蒙版层的函数
|
|
||||||
function closeOverlay() {
|
|
||||||
if (mhk && mhk.style.display === 'block') {
|
|
||||||
// 检查是否还有其他弹窗打开(包括导航下拉菜单)
|
|
||||||
const isBuyOpen = buyDropdown && buyDropdown.classList.contains('show');
|
|
||||||
const isSearchOpen = searchDropdown && searchDropdown.classList.contains('show');
|
|
||||||
const isLangOpen = langDropdown && langDropdown.classList.contains('show');
|
|
||||||
|
|
||||||
// 检查导航下拉菜单是否打开(通过检查是否有 active 或显示状态)
|
|
||||||
const productDropdown = document.querySelector('.header-nav-item:first-child .header-dropdown');
|
|
||||||
const isProductDropdownOpen = productDropdown && window.getComputedStyle(productDropdown).display !== 'none';
|
|
||||||
|
|
||||||
// 检查其他导航下拉菜单
|
|
||||||
let isOtherNavOpen = false;
|
|
||||||
const otherNavItems = document.querySelectorAll('.header-nav-item:not(:first-child)');
|
|
||||||
otherNavItems.forEach(item => {
|
|
||||||
const dropdown = item.querySelector('.header-dropdown');
|
|
||||||
if (dropdown && window.getComputedStyle(dropdown).display !== 'none') {
|
|
||||||
isOtherNavOpen = true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 只有当所有弹窗和导航下拉菜单都关闭时,才关闭蒙版层
|
|
||||||
if (!isBuyOpen && !isSearchOpen && !isLangOpen && !isProductDropdownOpen && !isOtherNavOpen) {
|
|
||||||
mhk.style.display = 'none';
|
|
||||||
body.style.overflow = '';
|
|
||||||
body.style.paddingRight = '';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 强制关闭所有弹窗和蒙版
|
|
||||||
function closeAllDropdownsAndOverlay() {
|
|
||||||
let anyOpen = false;
|
|
||||||
|
|
||||||
if (buyDropdown && buyDropdown.classList.contains('show')) {
|
|
||||||
buyDropdown.classList.remove('show');
|
|
||||||
anyOpen = true;
|
|
||||||
}
|
|
||||||
if (searchDropdown && searchDropdown.classList.contains('show')) {
|
|
||||||
searchDropdown.classList.remove('show');
|
|
||||||
anyOpen = true;
|
|
||||||
}
|
|
||||||
if (langDropdown && langDropdown.classList.contains('show')) {
|
|
||||||
langDropdown.classList.remove('show');
|
|
||||||
anyOpen = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 关闭产品列表导航的下拉菜单(通过移除hover触发的显示)
|
|
||||||
const productNavItem = document.querySelector('.header-nav-item:first-child');
|
|
||||||
if (productNavItem) {
|
|
||||||
const productDropdown = productNavItem.querySelector('.header-dropdown');
|
|
||||||
if (productDropdown && window.getComputedStyle(productDropdown).display !== 'none') {
|
|
||||||
productDropdown.style.display = '';
|
|
||||||
anyOpen = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 关闭其他导航的下拉菜单
|
|
||||||
const otherNavItems = document.querySelectorAll('.header-nav-item:not(:first-child)');
|
|
||||||
otherNavItems.forEach(item => {
|
|
||||||
const dropdown = item.querySelector('.header-dropdown');
|
|
||||||
if (dropdown && window.getComputedStyle(dropdown).display !== 'none') {
|
|
||||||
dropdown.style.display = '';
|
|
||||||
anyOpen = true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (anyOpen) {
|
|
||||||
closeOverlay();
|
|
||||||
} else {
|
|
||||||
closeOverlay();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ========== 监听导航下拉菜单的显示/隐藏 ==========
|
|
||||||
|
|
||||||
// 1. 产品列表导航(第一个导航项)
|
|
||||||
const productNavItem = document.querySelector('.header-nav-item:first-child');
|
|
||||||
if (productNavItem) {
|
|
||||||
const productDropdown = productNavItem.querySelector('.header-dropdown');
|
|
||||||
|
|
||||||
// 监听鼠标进入产品列表导航
|
|
||||||
productNavItem.addEventListener('mouseenter', () => {
|
|
||||||
// 当导航下拉菜单显示时,关闭其他点击弹窗
|
|
||||||
if (searchDropdown && searchDropdown.classList.contains('show')) {
|
|
||||||
searchDropdown.classList.remove('show');
|
|
||||||
}
|
|
||||||
if (langDropdown && langDropdown.classList.contains('show')) {
|
|
||||||
langDropdown.classList.remove('show');
|
|
||||||
}
|
|
||||||
if (buyDropdown && buyDropdown.classList.contains('show')) {
|
|
||||||
buyDropdown.classList.remove('show');
|
|
||||||
}
|
|
||||||
// 打开蒙版层
|
|
||||||
openOverlay();
|
|
||||||
});
|
|
||||||
|
|
||||||
// 监听鼠标离开产品列表导航(延迟关闭,给用户移动到下拉菜单的时间)
|
|
||||||
productNavItem.addEventListener('mouseleave', () => {
|
|
||||||
setTimeout(() => {
|
|
||||||
// 检查鼠标是否在下拉菜单上
|
|
||||||
if (productDropdown) {
|
|
||||||
const isHoveringDropdown = productDropdown.matches(':hover');
|
|
||||||
const isHoveringNav = productNavItem.matches(':hover');
|
|
||||||
if (!isHoveringDropdown && !isHoveringNav) {
|
|
||||||
closeOverlay();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
closeOverlay();
|
|
||||||
}
|
|
||||||
}, 100);
|
|
||||||
});
|
|
||||||
|
|
||||||
// 监听下拉菜单的鼠标离开事件
|
|
||||||
if (productDropdown) {
|
|
||||||
productDropdown.addEventListener('mouseleave', () => {
|
|
||||||
setTimeout(() => {
|
|
||||||
const isHoveringNav = productNavItem.matches(':hover');
|
|
||||||
if (!isHoveringNav) {
|
|
||||||
closeOverlay();
|
|
||||||
}
|
|
||||||
}, 100);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// 2. 其他导航项(有下拉菜单的)
|
// 监听弹窗状态变化关闭蒙版
|
||||||
const otherNavItems = document.querySelectorAll('.header-nav-item:not(:first-child)');
|
function setupDropdownObserver(dropdown) {
|
||||||
otherNavItems.forEach(item => {
|
if (!dropdown) return;
|
||||||
const hasDropdown = item.querySelector('.header-dropdown');
|
const observer = new MutationObserver((mutations) => {
|
||||||
if (hasDropdown) {
|
mutations.forEach((mutation) => {
|
||||||
const dropdown = hasDropdown;
|
if (mutation.attributeName === 'class') {
|
||||||
|
if (!dropdown.classList.contains('show')) {
|
||||||
// 监听鼠标进入导航项
|
|
||||||
item.addEventListener('mouseenter', () => {
|
|
||||||
// 关闭其他点击弹窗
|
|
||||||
if (searchDropdown && searchDropdown.classList.contains('show')) {
|
|
||||||
searchDropdown.classList.remove('show');
|
|
||||||
}
|
|
||||||
if (langDropdown && langDropdown.classList.contains('show')) {
|
|
||||||
langDropdown.classList.remove('show');
|
|
||||||
}
|
|
||||||
if (buyDropdown && buyDropdown.classList.contains('show')) {
|
|
||||||
buyDropdown.classList.remove('show');
|
|
||||||
}
|
|
||||||
// 打开蒙版层
|
|
||||||
openOverlay();
|
|
||||||
});
|
|
||||||
|
|
||||||
// 监听鼠标离开导航项
|
|
||||||
item.addEventListener('mouseleave', () => {
|
|
||||||
setTimeout(() => {
|
|
||||||
if (dropdown) {
|
|
||||||
const isHoveringDropdown = dropdown.matches(':hover');
|
|
||||||
const isHoveringNav = item.matches(':hover');
|
|
||||||
if (!isHoveringDropdown && !isHoveringNav) {
|
|
||||||
closeOverlay();
|
closeOverlay();
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
closeOverlay();
|
|
||||||
}
|
}
|
||||||
}, 100);
|
});
|
||||||
|
});
|
||||||
|
observer.observe(dropdown, { attributes: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
setupDropdownObserver(buyDropdown);
|
||||||
|
setupDropdownObserver(searchDropdown);
|
||||||
|
setupDropdownObserver(langDropdown);
|
||||||
|
|
||||||
|
// ========== 监听下拉菜单打开状态,自动控制蒙版 ==========
|
||||||
|
|
||||||
|
function initHeaderDropdown() {
|
||||||
|
// 获取所有下拉菜单
|
||||||
|
const allDropdowns = document.querySelectorAll('.header-dropdown');
|
||||||
|
|
||||||
|
// 设置初始样式
|
||||||
|
allDropdowns.forEach(dropdown => {
|
||||||
|
dropdown.style.opacity = '0';
|
||||||
|
dropdown.style.transform = 'translateY(-1.25em)';
|
||||||
|
dropdown.style.pointerEvents = 'none';
|
||||||
|
dropdown.style.transition = 'opacity 0.3s ease, transform 0.3s ease';
|
||||||
});
|
});
|
||||||
|
|
||||||
// 监听下拉菜单的鼠标离开
|
// 监听单个下拉菜单的样式变化
|
||||||
if (dropdown) {
|
function observeDropdown(dropdown) {
|
||||||
dropdown.addEventListener('mouseleave', () => {
|
const observer = new MutationObserver(() => {
|
||||||
setTimeout(() => {
|
const isOpen = dropdown.style.opacity === '1';
|
||||||
const isHoveringNav = item.matches(':hover');
|
if (isOpen) {
|
||||||
if (!isHoveringNav) {
|
openOverlay();
|
||||||
|
} else {
|
||||||
|
let anyOpen = false;
|
||||||
|
allDropdowns.forEach(d => {
|
||||||
|
if (d.style.opacity === '1') anyOpen = true;
|
||||||
|
});
|
||||||
|
if (!anyOpen) {
|
||||||
closeOverlay();
|
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', () => {
|
||||||
|
isHovering = true;
|
||||||
|
clearTimeout(timer);
|
||||||
|
showDropdown();
|
||||||
|
});
|
||||||
|
|
||||||
|
item.addEventListener('mouseleave', (e) => {
|
||||||
|
isHovering = false;
|
||||||
|
// 检查鼠标是否移到了下拉菜单上
|
||||||
|
const relatedTarget = e.relatedTarget;
|
||||||
|
const isMovingToDropdown = dropdown.contains(relatedTarget);
|
||||||
|
|
||||||
|
if (isMovingToDropdown) {
|
||||||
|
// 如果移向下拉菜单,不清除,等待下拉菜单的 mouseenter
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
timer = setTimeout(() => {
|
||||||
|
if (!isHovering && !dropdown.matches(':hover')) {
|
||||||
|
hideDropdown();
|
||||||
|
}
|
||||||
}, 100);
|
}, 100);
|
||||||
});
|
});
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// ========== 重新绑定所有点击弹窗事件 ==========
|
dropdown.addEventListener('mouseenter', () => {
|
||||||
|
isHovering = true;
|
||||||
|
clearTimeout(timer);
|
||||||
|
showDropdown();
|
||||||
|
});
|
||||||
|
|
||||||
// 1. 购买弹窗
|
dropdown.addEventListener('mouseleave', (e) => {
|
||||||
const buyWrapperNew = document.querySelector('.header-buy-wrapper');
|
isHovering = false;
|
||||||
if (buyWrapperNew && buyDropdown) {
|
// 检查鼠标是否移回了导航项
|
||||||
const newBuyWrapper = buyWrapperNew.cloneNode(true);
|
const relatedTarget = e.relatedTarget;
|
||||||
buyWrapperNew.parentNode.replaceChild(newBuyWrapper, buyWrapperNew);
|
const isMovingToNavItem = item.contains(relatedTarget);
|
||||||
|
|
||||||
newBuyWrapper.addEventListener('click', (e) => {
|
if (isMovingToNavItem) {
|
||||||
e.stopPropagation();
|
return;
|
||||||
const isOpen = buyDropdown.classList.contains('show');
|
|
||||||
|
|
||||||
if (isOpen) {
|
|
||||||
buyDropdown.classList.remove('show');
|
|
||||||
closeOverlay();
|
|
||||||
} else {
|
|
||||||
// 关闭其他弹窗和导航下拉菜单
|
|
||||||
if (searchDropdown && searchDropdown.classList.contains('show')) {
|
|
||||||
searchDropdown.classList.remove('show');
|
|
||||||
}
|
|
||||||
if (langDropdown && langDropdown.classList.contains('show')) {
|
|
||||||
langDropdown.classList.remove('show');
|
|
||||||
}
|
|
||||||
// 关闭导航下拉菜单
|
|
||||||
closeAllNavDropdowns();
|
|
||||||
|
|
||||||
buyDropdown.classList.add('show');
|
|
||||||
openOverlay();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. 搜索弹窗
|
|
||||||
const searchBtnNew = document.querySelector('.header-search-wrapper .header-nav-btn');
|
|
||||||
if (searchBtnNew && searchDropdown) {
|
|
||||||
const newSearchBtn = searchBtnNew.cloneNode(true);
|
|
||||||
searchBtnNew.parentNode.replaceChild(newSearchBtn, searchBtnNew);
|
|
||||||
|
|
||||||
newSearchBtn.addEventListener('click', (e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
const isOpen = searchDropdown.classList.contains('show');
|
|
||||||
|
|
||||||
if (isOpen) {
|
|
||||||
searchDropdown.classList.remove('show');
|
|
||||||
closeOverlay();
|
|
||||||
} else {
|
|
||||||
// 关闭其他弹窗和导航下拉菜单
|
|
||||||
if (buyDropdown && buyDropdown.classList.contains('show')) {
|
|
||||||
buyDropdown.classList.remove('show');
|
|
||||||
}
|
|
||||||
if (langDropdown && langDropdown.classList.contains('show')) {
|
|
||||||
langDropdown.classList.remove('show');
|
|
||||||
}
|
|
||||||
// 关闭导航下拉菜单
|
|
||||||
closeAllNavDropdowns();
|
|
||||||
|
|
||||||
searchDropdown.classList.add('show');
|
|
||||||
openOverlay();
|
|
||||||
if (typeof renderHistory === 'function') {
|
|
||||||
renderHistory();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. 语言弹窗
|
|
||||||
const langBtnNew = document.querySelector('.header-lang-wrapper .header-nav-btn');
|
|
||||||
if (langBtnNew && langDropdown) {
|
|
||||||
const newLangBtn = langBtnNew.cloneNode(true);
|
|
||||||
langBtnNew.parentNode.replaceChild(newLangBtn, langBtnNew);
|
|
||||||
|
|
||||||
newLangBtn.addEventListener('click', (e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
const isOpen = langDropdown.classList.contains('show');
|
|
||||||
|
|
||||||
if (isOpen) {
|
|
||||||
langDropdown.classList.remove('show');
|
|
||||||
closeOverlay();
|
|
||||||
} else {
|
|
||||||
// 关闭其他弹窗和导航下拉菜单
|
|
||||||
if (buyDropdown && buyDropdown.classList.contains('show')) {
|
|
||||||
buyDropdown.classList.remove('show');
|
|
||||||
}
|
|
||||||
if (searchDropdown && searchDropdown.classList.contains('show')) {
|
|
||||||
searchDropdown.classList.remove('show');
|
|
||||||
}
|
|
||||||
// 关闭导航下拉菜单
|
|
||||||
closeAllNavDropdowns();
|
|
||||||
|
|
||||||
langDropdown.classList.add('show');
|
|
||||||
openOverlay();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 辅助函数:关闭所有导航下拉菜单
|
|
||||||
function closeAllNavDropdowns() {
|
|
||||||
// 关闭产品列表导航下拉菜单
|
|
||||||
const productNavItem = document.querySelector('.header-nav-item:first-child');
|
|
||||||
if (productNavItem) {
|
|
||||||
const productDropdown = productNavItem.querySelector('.header-dropdown');
|
|
||||||
if (productDropdown) {
|
|
||||||
productDropdown.style.display = '';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 关闭其他导航下拉菜单
|
|
||||||
const otherNavItems = document.querySelectorAll('.header-nav-item:not(:first-child)');
|
|
||||||
otherNavItems.forEach(item => {
|
|
||||||
const dropdown = item.querySelector('.header-dropdown');
|
|
||||||
if (dropdown) {
|
|
||||||
dropdown.style.display = '';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// ========== 关闭按钮事件重新绑定 ==========
|
|
||||||
|
|
||||||
// 语言关闭按钮
|
|
||||||
const langCloseBtnNew = document.querySelector('.header-lang-dropdown-delete-icon');
|
|
||||||
if (langCloseBtnNew && langDropdown) {
|
|
||||||
const newLangCloseBtn = langCloseBtnNew.cloneNode(true);
|
|
||||||
langCloseBtnNew.parentNode.replaceChild(newLangCloseBtn, langCloseBtnNew);
|
|
||||||
|
|
||||||
newLangCloseBtn.addEventListener('click', (e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
langDropdown.classList.remove('show');
|
|
||||||
closeOverlay();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 搜索关闭按钮
|
|
||||||
const searchCloseBtnNew = document.querySelector('.header-search-dropdown-delete-icon');
|
|
||||||
if (searchCloseBtnNew && searchDropdown) {
|
|
||||||
const newSearchCloseBtn = searchCloseBtnNew.cloneNode(true);
|
|
||||||
searchCloseBtnNew.parentNode.replaceChild(newSearchCloseBtn, searchCloseBtnNew);
|
|
||||||
|
|
||||||
newSearchCloseBtn.addEventListener('click', (e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
searchDropdown.classList.remove('show');
|
|
||||||
closeOverlay();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// ========== 全局点击事件 ==========
|
|
||||||
document.addEventListener('click', (e) => {
|
|
||||||
// 检查是否点击在购买区域外
|
|
||||||
if (!e.target.closest('.header-buy-wrapper') && !e.target.closest('#buyDropdown')) {
|
|
||||||
if (buyDropdown && buyDropdown.classList.contains('show')) {
|
|
||||||
buyDropdown.classList.remove('show');
|
|
||||||
closeOverlay();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查是否点击在搜索区域外
|
|
||||||
if (!e.target.closest('.header-search-wrapper') && !e.target.closest('#searchDropdown')) {
|
|
||||||
if (searchDropdown && searchDropdown.classList.contains('show')) {
|
|
||||||
searchDropdown.classList.remove('show');
|
|
||||||
closeOverlay();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查是否点击在语言区域外
|
|
||||||
if (!e.target.closest('.header-lang-wrapper') && !e.target.closest('#langDropdown')) {
|
|
||||||
if (langDropdown && langDropdown.classList.contains('show')) {
|
|
||||||
langDropdown.classList.remove('show');
|
|
||||||
closeOverlay();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查是否点击在导航区域外(如果点击在导航外部,关闭导航下拉菜单)
|
|
||||||
if (!e.target.closest('.header-nav-item')) {
|
|
||||||
closeAllNavDropdowns();
|
|
||||||
closeOverlay();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// ========== 蒙版层点击事件 ==========
|
|
||||||
if (mhk) {
|
|
||||||
mhk.addEventListener('click', () => {
|
|
||||||
closeAllDropdownsAndOverlay();
|
|
||||||
closeAllNavDropdowns();
|
|
||||||
closeOverlay();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// ========== 监听弹窗状态变化 ==========
|
|
||||||
function setupDropdownObserver(dropdown, name) {
|
|
||||||
if (!dropdown) return;
|
|
||||||
|
|
||||||
const observer = new MutationObserver((mutations) => {
|
|
||||||
mutations.forEach((mutation) => {
|
|
||||||
if (mutation.attributeName === 'class') {
|
|
||||||
if (!dropdown.classList.contains('show')) {
|
|
||||||
closeOverlay();
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
timer = setTimeout(() => {
|
||||||
|
if (!isHovering && !item.matches(':hover')) {
|
||||||
|
hideDropdown();
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
|
||||||
observer.observe(dropdown, { attributes: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
setupDropdownObserver(buyDropdown, 'buy');
|
|
||||||
setupDropdownObserver(searchDropdown, 'search');
|
|
||||||
setupDropdownObserver(langDropdown, 'lang');
|
|
||||||
|
|
||||||
// ========== 页面卸载时恢复滚动 ==========
|
|
||||||
window.addEventListener('beforeunload', () => {
|
|
||||||
if (body) {
|
|
||||||
body.style.overflow = '';
|
|
||||||
body.style.paddingRight = '';
|
|
||||||
}
|
}
|
||||||
});
|
// 初始化(在 DOM 加载完成后执行)
|
||||||
|
if (document.readyState === 'loading') {
|
||||||
|
document.addEventListener('DOMContentLoaded', initHeaderDropdown);
|
||||||
|
} else {
|
||||||
|
initHeaderDropdown();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// 页面卸载时恢复滚动
|
||||||
|
window.addEventListener('beforeunload', () => {
|
||||||
|
if (body) {
|
||||||
|
body.style.position = '';
|
||||||
|
body.style.top = '';
|
||||||
|
body.style.left = '';
|
||||||
|
body.style.right = '';
|
||||||
|
body.style.width = '';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// // 初始化导航下拉菜单蒙版层
|
||||||
|
// initNavDropdownOverlay();
|
||||||
|
|
||||||
|
// 初始化渲染
|
||||||
|
renderHistory();
|
||||||
|
closeOverlay();
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// 确保蒙版层初始状态正确
|
|
||||||
closeOverlay();
|
|
||||||
</script>
|
</script>
|
||||||
44
app/index/view/pc/topic_ssd/index.html
Normal file
44
app/index/view/pc/topic_ssd/index.html
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
{extend name="public/base" /}
|
||||||
|
{block name="style"}
|
||||||
|
<link rel="stylesheet" href="__CSS__/topic_laptop/header.css">
|
||||||
|
<link rel="stylesheet" href="__CSS__/topic_laptop/footer.css">
|
||||||
|
<link rel="stylesheet" href="__CSS__/topic_ssd/index.css">
|
||||||
|
<script type="text/javascript">
|
||||||
|
(function (doc, win)
|
||||||
|
{
|
||||||
|
var docEl = doc.documentElement;
|
||||||
|
var resizeEvt = 'orientationchange' in window ? 'orientationchange' : 'resize';
|
||||||
|
var designWidth = 2560;
|
||||||
|
var designRemPx = 100;
|
||||||
|
function setRootFontSize ()
|
||||||
|
{
|
||||||
|
var clientWidth = docEl.clientWidth;
|
||||||
|
if (!clientWidth) return;
|
||||||
|
var fontSize = (clientWidth / designWidth) * designRemPx;
|
||||||
|
fontSize = Math.max(fontSize, 20);
|
||||||
|
fontSize = Math.min(fontSize, designRemPx);
|
||||||
|
docEl.style.fontSize = fontSize + 'px';
|
||||||
|
}
|
||||||
|
|
||||||
|
setRootFontSize();
|
||||||
|
win.addEventListener(resizeEvt, setRootFontSize);
|
||||||
|
doc.addEventListener('DOMContentLoaded', setRootFontSize);
|
||||||
|
})(document, window);
|
||||||
|
</script>
|
||||||
|
{/block}
|
||||||
|
{block name="main"}
|
||||||
|
<div class="sc-main">
|
||||||
|
{volist name="data.top_focus_images" id="item"}
|
||||||
|
<a href="{$item.link}" class="sc-mt20">
|
||||||
|
<img src="{$item.image}" alt="" class="sc-main-img" loading="lazy">
|
||||||
|
</a>
|
||||||
|
{/volist}
|
||||||
|
<div class="sc-main-imgs sc-mt20 sc-mb-78">
|
||||||
|
{volist name="data.products" id="item"}
|
||||||
|
<a href="{$item.link}" class="">
|
||||||
|
<img src="{$item.image}" alt="" class="sc-main-img1" loading="lazy">
|
||||||
|
</a>
|
||||||
|
{/volist}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/block}
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
use think\migration\Migrator;
|
use think\migration\Migrator;
|
||||||
use think\migration\db\Column;
|
use think\migration\db\Column;
|
||||||
|
|
||||||
class SysMallStoreEntrance extends Migrator
|
class CreateSysMallStoreEntrance extends Migrator
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* Change Method.
|
* Change Method.
|
||||||
56
database/migrations/20260707030708_create_product_compat.php
Normal file
56
database/migrations/20260707030708_create_product_compat.php
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
65
public/static/index/mobile/css/topic_ssd/index.css
Normal file
65
public/static/index/mobile/css/topic_ssd/index.css
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
html {
|
||||||
|
width: 100% !important;
|
||||||
|
overflow-x: hidden;
|
||||||
|
margin: 0 !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
max-width: 100vw !important;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
width: 100%;
|
||||||
|
background: #fff;
|
||||||
|
overflow-x: hidden;
|
||||||
|
margin: 0 !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
max-width: 100vw !important;
|
||||||
|
}
|
||||||
|
.m-sc-main {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
text-decoration: none;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.m-sc-main-img {
|
||||||
|
width: 100%;
|
||||||
|
/* max-width: 2560px; */
|
||||||
|
}
|
||||||
|
|
||||||
|
.m-sc-mt20 {
|
||||||
|
margin-top: 0.1rem;
|
||||||
|
}
|
||||||
|
.m-sc-mt20:first-child {
|
||||||
|
margin-top: 0 !important;
|
||||||
|
}
|
||||||
|
.m-sc-mb34 {
|
||||||
|
margin-bottom: 0.34rem;
|
||||||
|
}
|
||||||
|
.m-sc-main-imgs {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 0.1rem;
|
||||||
|
list-style: none;
|
||||||
|
/* 去除列表样式(如果有) */
|
||||||
|
}
|
||||||
|
|
||||||
|
.m-sc-main-imgs a {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.m-sc-main-img1 {
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
/* 保持比例 */
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
@@ -4,7 +4,10 @@
|
|||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
font-family: '微软雅黑', sans-serif;
|
font-family: '微软雅黑', sans-serif;
|
||||||
}
|
}
|
||||||
|
html {
|
||||||
|
overflow-y: scroll;
|
||||||
|
scrollbar-gutter: stable ;
|
||||||
|
}
|
||||||
body {
|
body {
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
|
|
||||||
@@ -41,7 +44,7 @@ a {
|
|||||||
.header-nav-bar {
|
.header-nav-bar {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 1520px;
|
max-width: 1520px;
|
||||||
height: 4em;
|
height:64px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
flex-wrap: nowrap;
|
flex-wrap: nowrap;
|
||||||
@@ -94,10 +97,11 @@ a {
|
|||||||
bottom: 0;
|
bottom: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 0.125em;
|
height: 2px;
|
||||||
background-color: #004bfa;
|
background-color: #004bfa;
|
||||||
transform: scaleX(0);
|
transform: scaleX(0);
|
||||||
transition: transform 0.2s ease;
|
transition: transform 0.2s ease;
|
||||||
|
z-index: 999;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-nav-title:hover::after,
|
.header-nav-title:hover::after,
|
||||||
@@ -144,12 +148,7 @@ a {
|
|||||||
transition: color 0.2s;
|
transition: color 0.2s;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* .header-nav-btn img {
|
|
||||||
width: 1.5em;
|
|
||||||
max-width: 17px;
|
|
||||||
max-height: 17px;
|
|
||||||
height: 1.5em;
|
|
||||||
} */
|
|
||||||
|
|
||||||
.header-nav-btn:hover {
|
.header-nav-btn:hover {
|
||||||
color: #004bfa;
|
color: #004bfa;
|
||||||
@@ -256,25 +255,36 @@ a {
|
|||||||
|
|
||||||
/* ========== 以下为原有样式,保持不变 ========== */
|
/* ========== 以下为原有样式,保持不变 ========== */
|
||||||
|
|
||||||
/* 原有的下拉框样式 */
|
/* 原有的下拉框样式 - 带动画效果 */
|
||||||
.header-dropdown {
|
.header-dropdown {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
max-width: 1920px;
|
max-width: 1920px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 4em;
|
top: 64px;
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
background-color: #fff;
|
background-color: #fff;
|
||||||
z-index: 199;
|
z-index: 99;
|
||||||
border-top: none;
|
border-top: none;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
padding-bottom: 2.375em;
|
padding-bottom: 2.375em;
|
||||||
display: none;
|
opacity: 0;
|
||||||
|
transform: translateY(-1.25em);
|
||||||
|
pointer-events: none;
|
||||||
|
transition: opacity 0.3s ease, transform 0.3s ease;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-nav-item:hover .header-dropdown {
|
/* .header-nav-item:hover .header-dropdown {
|
||||||
display: block;
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
pointer-events: auto;
|
||||||
|
} */
|
||||||
|
|
||||||
|
/* 添加动画锁定时的不响应状态 */
|
||||||
|
.header-dropdown.animating-out {
|
||||||
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Tabs切换栏:居中,宽度100%(基于下拉框内容区) */
|
/* Tabs切换栏:居中,宽度100%(基于下拉框内容区) */
|
||||||
@@ -325,7 +335,7 @@ a {
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
max-width: 48%;
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -340,10 +350,7 @@ a {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 单个分类区块 */
|
|
||||||
.header-category-block {
|
|
||||||
/* flex: 1; */
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
.header-category-title {
|
.header-category-title {
|
||||||
@@ -385,13 +392,14 @@ a {
|
|||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
font-size: 16px;
|
||||||
|
max-width: 748px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
.header-product-card {
|
.header-product-card {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
width: calc(50% - 0.75em);
|
width: calc(50% - 0.75em);
|
||||||
max-width: 22.625em;
|
max-width: 22.625em;
|
||||||
flex-shrink: 1;
|
flex-shrink: 1;
|
||||||
@@ -415,12 +423,12 @@ a {
|
|||||||
transform: translateY(-2px)
|
transform: translateY(-2px)
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-product-card:nth-child(1) {
|
/* .header-product-card:nth-child(1) {
|
||||||
margin-right: 1.5em;
|
|
||||||
}
|
} */
|
||||||
|
|
||||||
.header-product-card:nth-child(3) {
|
.header-product-card:nth-child(3) {
|
||||||
margin-right: 1.5em;
|
|
||||||
margin-top: 1.5em;
|
margin-top: 1.5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -428,20 +436,7 @@ a {
|
|||||||
margin-top: 1.5em;
|
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 {
|
.header-product-img {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
aspect-ratio: 22.625 / 12.25;
|
aspect-ratio: 22.625 / 12.25;
|
||||||
@@ -451,7 +446,6 @@ a {
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
color: #999;
|
color: #999;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
/* border-radius: 0.5em; */
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-product-img img {
|
.header-product-img img {
|
||||||
@@ -952,7 +946,7 @@ a {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.header-dropdown-content1 {
|
.header-dropdown-content1 {
|
||||||
padding: 0 12em;
|
padding: 2em 12em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-buy-dropdown {
|
.header-buy-dropdown {
|
||||||
@@ -980,7 +974,7 @@ a {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.header-dropdown-content1 {
|
.header-dropdown-content1 {
|
||||||
padding: 0 10em;
|
padding: 2em 10em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-buy-dropdown {
|
.header-buy-dropdown {
|
||||||
@@ -1008,7 +1002,7 @@ a {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.header-dropdown-content1 {
|
.header-dropdown-content1 {
|
||||||
padding: 0 8em;
|
padding: 2em 8em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-buy-dropdown {
|
.header-buy-dropdown {
|
||||||
@@ -1020,6 +1014,7 @@ a {
|
|||||||
row-gap: 60px;
|
row-gap: 60px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
/* 蒙版层样式 */
|
||||||
#mhk {
|
#mhk {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
@@ -1028,6 +1023,7 @@ a {
|
|||||||
left: 0;
|
left: 0;
|
||||||
backdrop-filter: blur(5px);
|
backdrop-filter: blur(5px);
|
||||||
background-color: rgba(0, 0, 0, 0.5);
|
background-color: rgba(0, 0, 0, 0.5);
|
||||||
z-index: 88;
|
z-index: 90;
|
||||||
display: none;
|
display: none;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|||||||
68
public/static/index/pc/css/topic_ssd/index.css
Normal file
68
public/static/index/pc/css/topic_ssd/index.css
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
|
||||||
|
html {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
/* width: 100vw;
|
||||||
|
height: 100vh; */
|
||||||
|
background: #fff;
|
||||||
|
overflow-x: hidden;
|
||||||
|
scroll-behavior: smooth !important;
|
||||||
|
-webkit-overflow-scrolling: touch !important;
|
||||||
|
}
|
||||||
|
/* 当视口宽度大于1920px时生效 */
|
||||||
|
@media screen and (min-width: 1920px) {
|
||||||
|
/* 这里写你的样式 */
|
||||||
|
body {
|
||||||
|
max-width:100% !important;
|
||||||
|
width: 100vw !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
.sc-main {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
max-width: 2560px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
text-decoration: none;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sc-main-img {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 2560px;
|
||||||
|
}
|
||||||
|
.sc-mt20:first-child {
|
||||||
|
margin-top: 0 !important;
|
||||||
|
}
|
||||||
|
.sc-mt20 {
|
||||||
|
margin-top: 0.2rem;
|
||||||
|
}
|
||||||
|
.sc-mb-78 {
|
||||||
|
margin-bottom: 0.78rem;
|
||||||
|
}
|
||||||
|
.sc-main-imgs {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 0.2rem;
|
||||||
|
list-style: none;
|
||||||
|
/* 去除列表样式(如果有) */
|
||||||
|
}
|
||||||
|
|
||||||
|
.sc-main-imgs a {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sc-main-img1 {
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
/* 保持比例 */
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
@@ -147,46 +147,6 @@ class ImageMigrator:
|
|||||||
# 确保paramiko已导入
|
# 确保paramiko已导入
|
||||||
ensure_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类型)
|
# 连接源服务器(如果是SSH类型)
|
||||||
if not self.source_config.is_local():
|
if not self.source_config.is_local():
|
||||||
self.source_client = paramiko.SSHClient()
|
self.source_client = paramiko.SSHClient()
|
||||||
@@ -218,11 +178,61 @@ class ImageMigrator:
|
|||||||
|
|
||||||
self.source_sftp = self.source_client.open_sftp()
|
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:
|
if self.verbose:
|
||||||
source_type = (
|
source_type = (
|
||||||
"本地目录" if self.source_config.is_local() else "SSH服务器"
|
"本地目录" 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
|
return True
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -718,7 +728,7 @@ def read_image_paths(image_list_file: str) -> List[str]:
|
|||||||
|
|
||||||
def main():
|
def main():
|
||||||
parser = argparse.ArgumentParser(
|
parser = argparse.ArgumentParser(
|
||||||
description="从本地目录或SSH服务器迁移图片到远程SSH服务器,保持相对路径结构",
|
description="本地目录之间迁移图片,本地目录或SSH服务器迁移图片到远程SSH服务器,保持相对路径结构",
|
||||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||||
epilog="""
|
epilog="""
|
||||||
示例:
|
示例:
|
||||||
@@ -756,8 +766,7 @@ def main():
|
|||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--source-type",
|
"--source-type",
|
||||||
choices=["local", "ssh"],
|
choices=["local", "ssh"],
|
||||||
default="local",
|
help="源服务器类型: local(本地目录) 或 ssh(SSH服务器)",
|
||||||
help="源服务器类型: local(本地目录) 或 ssh(SSH服务器),默认: local",
|
|
||||||
)
|
)
|
||||||
parser.add_argument("--source-host", help="源服务器地址(仅SSH类型需要)")
|
parser.add_argument("--source-host", help="源服务器地址(仅SSH类型需要)")
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
@@ -772,6 +781,11 @@ def main():
|
|||||||
parser.add_argument("--source-dir", help="源服务器图片基础目录")
|
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-host", help="目标服务器地址")
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--target-port", type=int, default=22, help="目标服务器端口(默认: 22)"
|
"--target-port", type=int, default=22, help="目标服务器端口(默认: 22)"
|
||||||
@@ -859,23 +873,38 @@ def main():
|
|||||||
|
|
||||||
# 构建目标服务器配置
|
# 构建目标服务器配置
|
||||||
target_config_data = config.get("target", {})
|
target_config_data = config.get("target", {})
|
||||||
target_config_data["type"] = "ssh" # 目标必须是SSH
|
|
||||||
|
|
||||||
ssh_config = target_config_data.get("ssh", {})
|
|
||||||
|
|
||||||
# 命令行参数覆盖配置文件
|
# 命令行参数覆盖配置文件
|
||||||
if args.target_host:
|
if args.target_type:
|
||||||
ssh_config["host"] = args.target_host
|
target_config_data["type"] = args.target_type
|
||||||
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_type == "ssh" or target_config_data.get("type") == "ssh":
|
||||||
|
ssh_config = target_config_data.get("ssh", {})
|
||||||
|
|
||||||
|
if args.target_host:
|
||||||
|
ssh_config["host"] = args.target_host
|
||||||
|
if args.target_port:
|
||||||
|
ssh_config["port"] = args.target_port
|
||||||
|
if args.target_user:
|
||||||
|
ssh_config["username"] = args.target_user
|
||||||
|
if args.target_password:
|
||||||
|
ssh_config["password"] = args.target_password
|
||||||
|
if args.target_key:
|
||||||
|
ssh_config["key_file"] = args.target_key
|
||||||
|
|
||||||
|
target_config_data["ssh"] = ssh_config
|
||||||
|
|
||||||
|
# 检查必要的目标SSH参数
|
||||||
|
if not ssh_config.get("host"):
|
||||||
|
print("错误: 必须指定目标服务器地址 (--target-host)")
|
||||||
|
sys.exit(1)
|
||||||
|
if not ssh_config.get("username"):
|
||||||
|
print("错误: 必须指定目标服务器用户名 (--target-user)")
|
||||||
|
sys.exit(1)
|
||||||
|
else:
|
||||||
|
# 本地类型,不需要SSH配置
|
||||||
|
target_config_data.pop("ssh", None)
|
||||||
|
|
||||||
# 设置基础目录
|
# 设置基础目录
|
||||||
if args.target_dir:
|
if args.target_dir:
|
||||||
@@ -884,14 +913,6 @@ def main():
|
|||||||
print("错误: 必须指定目标服务器图片基础目录 (--target-dir)")
|
print("错误: 必须指定目标服务器图片基础目录 (--target-dir)")
|
||||||
sys.exit(1)
|
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:
|
try:
|
||||||
source_config = ServerConfig.from_dict(source_config_data)
|
source_config = ServerConfig.from_dict(source_config_data)
|
||||||
|
|||||||
Reference in New Issue
Block a user