page_compare.html 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443
  1. {include file="header" /}
  2. <link rel="stylesheet" href="/assets/xtu/page-compare.css?v=20260625-1420">
  3. {php}
  4. $compareCategory = isset($category) ? $category : getCategory($catid ?? 41);
  5. $productModelId = 2;
  6. $catNames = \addons\cms\model\Category::where('modelid', $productModelId)
  7. ->where('status', 1)
  8. ->order('listorder DESC,id DESC')
  9. ->column('catname', 'id');
  10. $catNames = is_array($catNames) ? $catNames : [];
  11. $catNames = array_filter($catNames, function ($catName, $catId) {
  12. return (int)$catId > 0 && trim((string)$catName) !== '';
  13. }, ARRAY_FILTER_USE_BOTH);
  14. $productCatIds = array_map('intval', array_keys($catNames));
  15. $productCatIds = array_filter($productCatIds, function ($catId) {
  16. return $catId > 0;
  17. });
  18. $productWhere = '`status` = 1';
  19. if (!empty($productCatIds)) {
  20. $productWhere = "`catid` in(" . implode(',', $productCatIds) . ") AND " . $productWhere;
  21. }
  22. $productList = !empty($productCatIds) ? (new \addons\cms\model\Cms())->getList(
  23. $productModelId,
  24. $productWhere,
  25. 1,
  26. '*',
  27. 'listorder DESC,id DESC',
  28. 50,
  29. false
  30. ) : [];
  31. $productList = is_array($productList) ? $productList : [];
  32. $compareText = function ($key, $default) {
  33. return function_exists('cms_t') ? cms_t($key, $default) : $default;
  34. };
  35. foreach ($productList as &$productItem) {
  36. $productItem['cat_label'] = $catNames[(int)($productItem['catid'] ?? 0)] ?? '';
  37. }
  38. unset($productItem);
  39. $productMap = [];
  40. $allowedProductIds = [];
  41. foreach ($productList as $productItem) {
  42. $productId = (int)($productItem['id'] ?? 0);
  43. if ($productId <= 0) {
  44. continue;
  45. }
  46. $productMap[$productId] = $productItem;
  47. $allowedProductIds[$productId] = true;
  48. }
  49. $selectedIds = [];
  50. $requestSelected = request()->param('compare_ids/a', []);
  51. if (!empty($requestSelected) && is_array($requestSelected)) {
  52. foreach ($requestSelected as $selectedId) {
  53. $selectedId = (int)$selectedId;
  54. if ($selectedId > 0 && isset($allowedProductIds[$selectedId]) && !in_array($selectedId, $selectedIds, true)) {
  55. $selectedIds[] = $selectedId;
  56. }
  57. if (count($selectedIds) >= 3) {
  58. break;
  59. }
  60. }
  61. }
  62. if (empty($selectedIds)) {
  63. foreach ($productList as $productItem) {
  64. $selectedId = (int)($productItem['id'] ?? 0);
  65. if ($selectedId <= 0 || in_array($selectedId, $selectedIds, true)) {
  66. continue;
  67. }
  68. $selectedIds[] = $selectedId;
  69. if (count($selectedIds) >= 3) {
  70. break;
  71. }
  72. }
  73. }
  74. $selectedProducts = [];
  75. foreach ($selectedIds as $selectedId) {
  76. if (isset($productMap[$selectedId])) {
  77. $selectedProducts[] = $productMap[$selectedId];
  78. }
  79. }
  80. if (count($selectedProducts) > 3) {
  81. $selectedProducts = array_slice($selectedProducts, 0, 3);
  82. }
  83. while (count($selectedProducts) < 3 && !empty($productList)) {
  84. foreach ($productList as $productItem) {
  85. $exists = false;
  86. foreach ($selectedProducts as $selectedProduct) {
  87. if ((int)$selectedProduct['id'] === (int)$productItem['id']) {
  88. $exists = true;
  89. break;
  90. }
  91. }
  92. if (!$exists) {
  93. $selectedProducts[] = $productItem;
  94. break;
  95. }
  96. }
  97. if (count($selectedProducts) >= count($productList)) {
  98. break;
  99. }
  100. }
  101. $selectedProducts = array_slice($selectedProducts, 0, 3);
  102. $selectedIds = array_map(function ($product) {
  103. return (int)($product['id'] ?? 0);
  104. }, $selectedProducts);
  105. $specWhere = "`catid` = 10 AND `status` = 1";
  106. if (!empty($selectedIds)) {
  107. $specWhere .= " AND `project_id` in(" . implode(',', $selectedIds) . ")";
  108. }
  109. $specRows = (new \addons\cms\model\Cms())->getList(6, $specWhere, 1, '*', 'listorder ASC,id ASC', 500, false);
  110. $specRows = is_array($specRows) ? $specRows : [];
  111. $specFieldMeta = cms_cache('ModelField')[6] ?? [];
  112. $enabledFieldTitles = [];
  113. foreach ($specFieldMeta as $fieldName => $fieldMeta) {
  114. if ((int)($fieldMeta['status'] ?? 0) !== 1) {
  115. continue;
  116. }
  117. $enabledFieldTitles[$fieldName] = trim((string)($fieldMeta['title'] ?? $fieldName));
  118. }
  119. $compareFieldGroups = [
  120. $compareText('compare_group_camera', '相机') => [
  121. ['name' => '__cat_label', 'label' => $compareText('compare_label_category', '所属分类')],
  122. ['name' => '__sj_status', 'label' => $compareText('compare_label_sale_status', '上架状态')],
  123. ],
  124. $compareText('compare_group_general', '通用') => [
  125. ['name' => 'size', 'label' => $compareText('compare_label_size', '尺寸')],
  126. ['name' => 'waterproof', 'label' => $compareText('compare_label_waterproof', '防水')],
  127. ['name' => 'mac', 'label' => $compareText('compare_label_microphone', '麦克风')],
  128. ['name' => 'weight', 'label' => $compareText('compare_label_weight', '重量')],
  129. ['name' => 'conn_method', 'label' => $compareText('compare_label_connection_method', '连接方式')],
  130. ['name' => 'color_ver', 'label' => $compareText('compare_label_color_version', '颜色版本')],
  131. ['name' => 'vertical_shoot', 'label' => $compareText('compare_label_vertical_shoot', '原生竖拍')],
  132. ['name' => 'live_stream', 'label' => $compareText('compare_label_live_stream', '直播功能')],
  133. ],
  134. $compareText('compare_group_display', '显示屏') => [
  135. ['name' => 'rear_screen', 'label' => $compareText('compare_label_rear_screen', '后置显示屏')],
  136. ['name' => 'front_screen', 'label' => $compareText('compare_label_front_screen', '前置显示屏')],
  137. ['name' => 'main_cpu', 'label' => $compareText('compare_label_main_cpu', '主控CPU')],
  138. ['name' => 'image_sensor', 'label' => $compareText('compare_label_image_sensor', '影像传感器')],
  139. ['name' => 'iso_range', 'label' => $compareText('compare_label_iso_range', 'ISO范围')],
  140. ['name' => 'e_shutter_speed', 'label' => $compareText('compare_label_e_shutter_speed', '电子快门速度')],
  141. ['name' => 'normal_video', 'label' => $compareText('compare_label_normal_video', '普通录影')],
  142. ['name' => 'hdr_video', 'label' => $compareText('compare_label_hdr_video', 'HDR录影')],
  143. ['name' => 'night_mode', 'label' => $compareText('compare_label_night_mode', '夜景模式')],
  144. ['name' => 'slowmo_video', 'label' => $compareText('compare_label_slowmo_video', '慢动作录影')],
  145. ['name' => 'raw_photo', 'label' => $compareText('compare_label_raw_photo', 'RAW拍照')],
  146. ['name' => 'time_lapse', 'label' => $compareText('compare_label_time_lapse', '延时摄影')],
  147. ['name' => 'pre_record_time', 'label' => $compareText('compare_label_pre_record_time', '预录时长')],
  148. ['name' => 'horizon_level', 'label' => $compareText('compare_label_horizon_level', '地平线矫正')],
  149. ['name' => 'stabilization', 'label' => $compareText('compare_label_stabilization', '防抖')],
  150. ['name' => 'video_max_bitrate', 'label' => $compareText('compare_label_video_max_bitrate', '视频储存最大码流')],
  151. ['name' => 'file_system', 'label' => $compareText('compare_label_file_system', '支持文件系统')],
  152. ['name' => 'image_format', 'label' => $compareText('compare_label_image_format', '图片格式')],
  153. ['name' => 'video_format', 'label' => $compareText('compare_label_video_format', '视频格式')],
  154. ['name' => 'sd_card_type', 'label' => $compareText('compare_label_sd_card_type', '支持存储卡类型')],
  155. ['name' => 'audio_record', 'label' => $compareText('compare_label_audio_record', '音频录制')],
  156. ],
  157. $compareText('compare_group_battery', '电池') => [
  158. ['name' => 'battery_type', 'label' => $compareText('compare_label_battery_type', '类型')],
  159. ['name' => 'battery_cap', 'fallback' => 'battery_capacity', 'label' => $compareText('compare_label_battery_cap', '容量')],
  160. ['name' => 'battery_energy', 'label' => $compareText('compare_label_battery_energy', '能量')],
  161. ['name' => 'battery_voltage', 'label' => $compareText('compare_label_battery_voltage', '电压')],
  162. ['name' => 'work_temp', 'fallback' => 'operating_temperature', 'label' => $compareText('compare_label_work_temp', '使用环境温度')],
  163. ['name' => 'fast_charge', 'fallback' => 'fast_charging', 'label' => $compareText('compare_label_fast_charge', '是否支持快充')],
  164. ['name' => 'charge_time', 'fallback' => 'charging_time', 'label' => $compareText('compare_label_charge_time', '充电时间')],
  165. ['name' => 'battery_life', 'label' => $compareText('compare_label_battery_life', '续航时间')],
  166. ],
  167. $compareText('compare_group_connectivity', '连接') => [
  168. ['name' => 'wifi_freq', 'fallback' => 'wifi_frequency', 'label' => $compareText('compare_label_wifi_freq', 'Wi-Fi 工作频率')],
  169. ['name' => 'wifi_proto', 'fallback' => 'wifi_protocol', 'label' => $compareText('compare_label_wifi_proto', 'Wi-Fi 协议')],
  170. ['name' => 'bt_freq', 'fallback' => 'bluetooth_frequency', 'label' => $compareText('compare_label_bt_freq', '蓝牙工作频率')],
  171. ['name' => 'bt_proto', 'fallback' => 'bluetooth_protocol', 'label' => $compareText('compare_label_bt_proto', '蓝牙协议')],
  172. ],
  173. $compareText('compare_group_other_features', '其他功能') => [
  174. ['name' => 'bt_setup', 'fallback' => 'bluetooth_setup', 'label' => $compareText('compare_label_bt_setup', '蓝牙配网')],
  175. ['name' => 'rev_charge', 'fallback' => 'reverse_charging', 'label' => $compareText('compare_label_rev_charge', '应急反向充电')],
  176. ['name' => 'voice_ctrl', 'fallback' => 'voice_control', 'label' => $compareText('compare_label_voice_ctrl', '语音控制')],
  177. ['name' => 'quick_record', 'fallback' => 'quick_start_recording', 'label' => $compareText('compare_label_quick_record', '快启(快启录像)')],
  178. ],
  179. ];
  180. $specGroups = [];
  181. $specGroupOrder = [];
  182. foreach ($compareFieldGroups as $groupName => $groupFields) {
  183. $rows = [];
  184. foreach ($groupFields as $fieldDef) {
  185. $fieldName = $fieldDef['name'];
  186. $fallbackFieldName = $fieldDef['fallback'] ?? '';
  187. $fieldLabel = $fieldDef['label'];
  188. $rowValues = [];
  189. if ($fieldName === '__cat_label') {
  190. foreach ($selectedProducts as $product) {
  191. $rowValues[(int)$product['id']] = (string)($product['cat_label'] ?? '-');
  192. }
  193. $rows[] = ['label' => $fieldLabel, 'values' => $rowValues];
  194. continue;
  195. }
  196. if ($fieldName === '__sj_status') {
  197. foreach ($selectedProducts as $product) {
  198. $productId = (int)$product['id'];
  199. $statusText = '';
  200. foreach ($specRows as $specItem) {
  201. if ((int)($specItem['project_id'] ?? 0) !== $productId) {
  202. continue;
  203. }
  204. $statusText = trim((string)($specItem['list'] ?? ''));
  205. break;
  206. }
  207. $rowValues[$productId] = $statusText;
  208. }
  209. $rows[] = ['label' => $fieldLabel, 'values' => $rowValues];
  210. continue;
  211. }
  212. if (!isset($enabledFieldTitles[$fieldName]) && (!$fallbackFieldName || !isset($enabledFieldTitles[$fallbackFieldName]))) {
  213. continue;
  214. }
  215. foreach ($specRows as $specItem) {
  216. $projectId = (int)($specItem['project_id'] ?? 0);
  217. if ($projectId <= 0) {
  218. continue;
  219. }
  220. $rawValue = $specItem[$fieldName] ?? '';
  221. if (($rawValue === '' || $rawValue === null) && $fallbackFieldName !== '') {
  222. $rawValue = $specItem[$fallbackFieldName] ?? '';
  223. }
  224. if (is_array($rawValue)) {
  225. $rawValue = implode(' / ', $rawValue);
  226. }
  227. $rawValue = preg_replace('/<br\s*\/?>/i', "\n", (string)$rawValue);
  228. $value = trim(strip_tags($rawValue));
  229. $rowValues[$projectId] = $value;
  230. }
  231. foreach ($selectedProducts as $product) {
  232. $productId = (int)$product['id'];
  233. if (!isset($rowValues[$productId])) {
  234. $rowValues[$productId] = '';
  235. }
  236. }
  237. $rows[] = ['label' => $fieldLabel, 'values' => $rowValues];
  238. }
  239. if (!empty($rows)) {
  240. $specGroups[$groupName] = $rows;
  241. $specGroupOrder[] = $groupName;
  242. }
  243. }
  244. {/php}
  245. <main class="compare-page">
  246. <section class="compare-hero">
  247. <div class="compare-hero-banner">
  248. <img class="compare-hero-image" src="{:getCategory(41,'image')}" alt="{:getCategory(41,'catname')}">
  249. <div class="compare-hero-minimal">
  250. <div class="compare-shell">
  251. <div class="compare-hero-copy">
  252. <span class="compare-hero-overline">{:cms_t('compare_hero_overline','选购指南')}</span>
  253. <h1>{if getCategory(41,'description')}{:getCategory(41,'description')}{else /}{:getCategory(41,'catname')}{/if}</h1>
  254. </div>
  255. </div>
  256. </div>
  257. </div>
  258. </section>
  259. <section class="compare-main">
  260. <div class="compare-shell">
  261. <form class="compare-toolbar" method="get" action="{:getCategory($catid,'url')}" id="compareToolbarForm">
  262. {volist name="selectedProducts" id="selectedProduct" key="slotIndex"}
  263. <div class="compare-dropdown" data-compare-dropdown>
  264. <input type="hidden" name="compare_ids[]" value="{$selectedProduct.id}" data-compare-input>
  265. <button type="button" class="compare-dropdown-toggle" data-compare-toggle aria-expanded="false">
  266. <span class="compare-dropdown-text">
  267. <strong>{$selectedProduct.title}</strong>
  268. <small>{$selectedProduct.cat_label|default=cms_t('compare_product_series','产品系列')}</small>
  269. </span>
  270. <span class="compare-dropdown-icon"></span>
  271. </button>
  272. <div class="compare-dropdown-menu" data-compare-menu>
  273. {volist name="productList" id="option"}
  274. <button type="button" class="compare-dropdown-item {if $option.id == $selectedProduct.id}is-active{/if}" data-value="{$option.id}" data-label="{$option.title}" data-subtitle="{$option.cat_label|default=cms_t('compare_product_series','产品系列')}">
  275. <span class="compare-dropdown-item-title">{$option.title}</span>
  276. <span class="compare-dropdown-item-subtitle">{$option.cat_label|default=cms_t('compare_product_series','产品系列')}</span>
  277. </button>
  278. {/volist}
  279. </div>
  280. </div>
  281. {/volist}
  282. </form>
  283. <div class="compare-products">
  284. {volist name="selectedProducts" id="product"}
  285. <article class="compare-product-card">
  286. <div class="compare-product-media">
  287. <img src="{$product.thumb|default=$product.thumb2}" alt="{$product.title}">
  288. </div>
  289. <h3 class="compare-product-title">{$product.title}</h3>
  290. <div class="compare-product-subtitle">{$product.title1|default=$product.title2|default=''}</div>
  291. <div class="compare-product-actions">
  292. <a class="compare-buy-btn" href="{$product.url}">{:cms_t('buy_now','立即购买')}</a>
  293. <a class="compare-more-link" href="{$product.url}">{:cms_t('learn_more','了解更多')} <i class="bi bi-chevron-right"></i></a>
  294. </div>
  295. </article>
  296. {/volist}
  297. </div>
  298. <div class="compare-table-wrap">
  299. {if empty($specGroupOrder)}
  300. <div class="compare-empty">{:cms_t('compare_empty_desc','当前还没有匹配到参数数据。请确认参数栏目 ID 10 下的内容已启用,并且每条参数内容的 project_id 正确关联到了产品内容 ID。')}</div>
  301. {else /}
  302. {volist name="specGroupOrder" id="groupName"}
  303. <h2 class="compare-group-title">{$groupName}</h2>
  304. <div class="compare-table-scroll">
  305. <table class="compare-spec-table">
  306. <colgroup>
  307. <col>
  308. <col>
  309. <col>
  310. <col>
  311. </colgroup>
  312. <tbody>
  313. {volist name="specGroups[$groupName]" id="specRow"}
  314. <tr>
  315. <th>{$specRow.label}</th>
  316. {volist name="selectedProducts" id="productForValue"}
  317. <td>{$specRow.values[$productForValue.id]|default=''}</td>
  318. {/volist}
  319. </tr>
  320. {/volist}
  321. </tbody>
  322. </table>
  323. </div>
  324. {/volist}
  325. {/if}
  326. </div>
  327. </div>
  328. </section>
  329. </main>
  330. <script>
  331. document.addEventListener('DOMContentLoaded', function () {
  332. const form = document.getElementById('compareToolbarForm')
  333. const dropdowns = document.querySelectorAll('[data-compare-dropdown]')
  334. let submitTimer = null
  335. let submitting = false
  336. const closeAll = () => {
  337. dropdowns.forEach((dropdown) => {
  338. dropdown.classList.remove('is-open')
  339. const toggle = dropdown.querySelector('[data-compare-toggle]')
  340. if (toggle) toggle.setAttribute('aria-expanded', 'false')
  341. })
  342. }
  343. const markSubmitting = () => {
  344. submitting = true
  345. dropdowns.forEach((dropdown) => {
  346. dropdown.classList.add('is-busy')
  347. const toggle = dropdown.querySelector('[data-compare-toggle]')
  348. if (toggle) toggle.disabled = true
  349. })
  350. }
  351. const scheduleSubmit = () => {
  352. if (!form || submitting) return
  353. if (submitTimer) window.clearTimeout(submitTimer)
  354. submitTimer = window.setTimeout(() => {
  355. markSubmitting()
  356. form.submit()
  357. }, 120)
  358. }
  359. const getCurrentSelectedValues = () => Array.from(dropdowns).map((dropdown) => {
  360. const input = dropdown.querySelector('[data-compare-input]')
  361. return input ? String(input.value || '') : ''
  362. })
  363. dropdowns.forEach((dropdown) => {
  364. const toggle = dropdown.querySelector('[data-compare-toggle]')
  365. const input = dropdown.querySelector('[data-compare-input]')
  366. const items = dropdown.querySelectorAll('.compare-dropdown-item')
  367. if (!toggle || !input) return
  368. toggle.addEventListener('click', function (event) {
  369. event.preventDefault()
  370. if (submitting) return
  371. const isOpen = dropdown.classList.contains('is-open')
  372. closeAll()
  373. if (!isOpen) {
  374. dropdown.classList.add('is-open')
  375. toggle.setAttribute('aria-expanded', 'true')
  376. }
  377. })
  378. items.forEach((item) => {
  379. item.addEventListener('click', function () {
  380. if (submitting) return
  381. const nextValue = String(item.dataset.value || '')
  382. const currentValues = getCurrentSelectedValues()
  383. const currentValue = String(input.value || '')
  384. const duplicateUsed = currentValues.some((value, index) => value === nextValue && dropdowns[index] !== dropdown)
  385. if (duplicateUsed || nextValue === currentValue) {
  386. closeAll()
  387. return
  388. }
  389. input.value = nextValue
  390. const titleNode = toggle.querySelector('strong')
  391. const subtitleNode = toggle.querySelector('small')
  392. if (titleNode) titleNode.textContent = item.dataset.label || ''
  393. if (subtitleNode) subtitleNode.textContent = item.dataset.subtitle || ''
  394. closeAll()
  395. scheduleSubmit()
  396. })
  397. })
  398. })
  399. document.addEventListener('click', function (event) {
  400. if (!event.target.closest('[data-compare-dropdown]')) {
  401. closeAll()
  402. }
  403. })
  404. document.addEventListener('keydown', function (event) {
  405. if (event.key === 'Escape') closeAll()
  406. })
  407. })
  408. </script>
  409. {include file="footer" /}