-
+
@@ -706,9 +687,9 @@
@@ -719,13 +700,13 @@
-
+
@@ -736,9 +717,9 @@
@@ -749,13 +730,13 @@
-
+
@@ -766,9 +747,9 @@
@@ -779,13 +760,13 @@
-
+
@@ -796,9 +777,9 @@
@@ -809,13 +790,13 @@
-
+
@@ -825,18 +806,18 @@
-
+
@@ -862,7 +843,7 @@
@@ -919,7 +898,7 @@
网站首页
-
+
@@ -939,7 +918,7 @@
公司简介
-
+
@@ -949,7 +928,7 @@
资质荣誉
-
+
@@ -959,7 +938,7 @@
品质技术
-
+
@@ -969,7 +948,7 @@
发展历程
-
+
@@ -979,7 +958,7 @@
店面展示
-
+
@@ -1001,7 +980,7 @@
礼盒系列
-
+
@@ -1011,7 +990,7 @@
华阳金标海参
-
+
@@ -1021,7 +1000,7 @@
生鲜系列
-
+
@@ -1031,7 +1010,7 @@
无骨系列
-
+
@@ -1041,7 +1020,7 @@
调味系列
-
+
@@ -1051,7 +1030,7 @@
裹粉系列
-
+
@@ -1061,7 +1040,7 @@
休闲即食系列
-
+
@@ -1083,7 +1062,7 @@
七道鲜文化
-
+
@@ -1093,7 +1072,7 @@
节庆文化
-
+
@@ -1103,7 +1082,7 @@
礼品文化
-
+
@@ -1113,7 +1092,7 @@
企业文化
-
+
@@ -1123,7 +1102,7 @@
历史文化
-
+
@@ -1133,7 +1112,7 @@
产品文化
-
+
@@ -1143,7 +1122,7 @@
...
-
+
@@ -1165,7 +1144,7 @@
华阳新闻
-
+
@@ -1175,7 +1154,7 @@
行业动态
-
+
@@ -1185,68 +1164,25 @@
媒体报道
-
+
-
-
-
-
-
-
- -
-
-
-
-
- -
-
-
-
-
- -
-
-
-
-
-
-
-
+
-
-
+
+
@@ -1288,9 +1224,9 @@
-
+
营业执照及SEO链接: 营业执照
-
+
@@ -1305,8 +1241,8 @@
您可以查看其他栏目或返回 首页
-
-
+
+
@@ -1347,7 +1283,7 @@
SVG图标库请自行添加图标,用div包起来,并命名使用
-
+
-
© SAF-Team 版权所有
盗用有风险,使用需谨慎,加入SAF获取更多技术支持
-
+
© SAF-Team 版权所有
盗用有风险,使用需谨慎,加入SAF获取更多技术支持
+
@@ -1422,32 +1358,32 @@
-
-
-
+
+
+
-
+
@@ -1478,4 +1414,3 @@
-
diff --git a/frontend/src/generated/quality-page-body.html b/frontend/src/generated/quality-page-body.html
index 5cfdf5b..57c0513 100644
--- a/frontend/src/generated/quality-page-body.html
+++ b/frontend/src/generated/quality-page-body.html
@@ -2,21 +2,21 @@
-
-->
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
-
-
-
-
-
-
-
-
+
+
+
-
+
-
+
-
+
-
+
-
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
+
-
+
@@ -238,7 +219,7 @@
-
+
@@ -259,8 +240,8 @@
@@ -922,7 +901,7 @@
公司简介
-
+
@@ -932,7 +911,7 @@
资质荣誉
-
+
@@ -942,7 +921,7 @@
品质技术
-
+
@@ -952,7 +931,7 @@
发展历程
-
+
@@ -962,7 +941,7 @@
店面展示
-
+
@@ -984,7 +963,7 @@
礼盒系列
-
+
@@ -994,7 +973,7 @@
华阳金标海参
-
+
@@ -1004,7 +983,7 @@
生鲜系列
-
+
@@ -1014,7 +993,7 @@
无骨系列
-
+
@@ -1024,7 +1003,7 @@
调味系列
-
+
@@ -1034,7 +1013,7 @@
裹粉系列
-
+
@@ -1044,7 +1023,7 @@
休闲即食系列
-
+
@@ -1066,7 +1045,7 @@
七道鲜文化
-
+
@@ -1076,7 +1055,7 @@
节庆文化
-
+
@@ -1086,7 +1065,7 @@
礼品文化
-
+
@@ -1096,7 +1075,7 @@
企业文化
-
+
@@ -1106,7 +1085,7 @@
历史文化
-
+
@@ -1116,7 +1095,7 @@
产品文化
-
+
@@ -1126,7 +1105,7 @@
...
-
+
@@ -1148,7 +1127,7 @@
华阳新闻
-
+
@@ -1158,7 +1137,7 @@
行业动态
-
+
@@ -1168,61 +1147,18 @@
媒体报道
-
+
-
-
-
-
-
-
- -
-
-
-
-
- -
-
-
-
-
- -
-
-
-
-
-
-
-
+
@@ -1232,14 +1168,14 @@
联系方式
-
+
-
-
+
+
@@ -1281,9 +1217,9 @@
-
+
营业执照及SEO链接: 营业执照
-
+
@@ -1298,8 +1234,8 @@
您可以查看其他栏目或返回 首页
-
-
+
+
@@ -1340,7 +1276,7 @@
SVG图标库请自行添加图标,用div包起来,并命名使用
-
+
-
© SAF-Team 版权所有
盗用有风险,使用需谨慎,加入SAF获取更多技术支持
-
+
© SAF-Team 版权所有
盗用有风险,使用需谨慎,加入SAF获取更多技术支持
+
@@ -1415,32 +1351,32 @@
-
-
-
+
+
+
-
+
diff --git a/frontend/src/generated/store-page-body.html b/frontend/src/generated/store-page-body.html
index 3059004..f8d5039 100644
--- a/frontend/src/generated/store-page-body.html
+++ b/frontend/src/generated/store-page-body.html
@@ -2,21 +2,21 @@
-
-->
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
-
-
-
-
-
-
-
-
+
+
+
-
+
-
+
-
+
-
+
-
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
+
-
+
@@ -238,7 +219,7 @@
-
+
@@ -259,8 +240,8 @@
@@ -417,12 +398,12 @@
@@ -602,12 +583,12 @@
-
-
-
+
+
+
-
+
© 2026 Baidu - GS(2025)4125号 - 甲测资字11111342 - 京ICP证030173号 - Data © 百度智图
@@ -627,109 +608,109 @@
-
+
-
+

-
+
-
+

-
+
-
+

-
+
-
+

-
+
-
+

-
+
-
+

-
+
-
+

-
+
-
+

-
+
-
+

-
+
-
+

-
+
-
+

-
+
-
+

-
+
-
+

-
+
-
+

-
+
-
-
-
-
-
+
+
+
+
+
-
-
-
+
+
+
-
+
@@ -756,7 +737,7 @@
@@ -813,7 +792,7 @@
网站首页
-
+
@@ -833,7 +812,7 @@
公司简介
-
+
@@ -843,7 +822,7 @@
资质荣誉
-
+
@@ -853,7 +832,7 @@
品质技术
-
+
@@ -863,7 +842,7 @@
发展历程
-
+
@@ -873,7 +852,7 @@
店面展示
-
+
@@ -895,7 +874,7 @@
礼盒系列
-
+
@@ -905,7 +884,7 @@
华阳金标海参
-
+
@@ -915,7 +894,7 @@
生鲜系列
-
+
@@ -925,7 +904,7 @@
无骨系列
-
+
@@ -935,7 +914,7 @@
调味系列
-
+
@@ -945,7 +924,7 @@
裹粉系列
-
+
@@ -955,7 +934,7 @@
休闲即食系列
-
+
@@ -977,7 +956,7 @@
七道鲜文化
-
+
@@ -987,7 +966,7 @@
节庆文化
-
+
@@ -997,7 +976,7 @@
礼品文化
-
+
@@ -1007,7 +986,7 @@
企业文化
-
+
@@ -1017,7 +996,7 @@
历史文化
-
+
@@ -1027,7 +1006,7 @@
产品文化
-
+
@@ -1037,7 +1016,7 @@
...
-
+
@@ -1059,7 +1038,7 @@
华阳新闻
-
+
@@ -1069,7 +1048,7 @@
行业动态
-
+
@@ -1079,61 +1058,18 @@
媒体报道
-
+
-
-
-
-
-
-
- -
-
-
-
-
- -
-
-
-
-
- -
-
-
-
-
-
-
-
+
@@ -1143,14 +1079,14 @@
联系方式
-
+
-
-
+
+
@@ -1192,9 +1128,9 @@
-
+
营业执照及SEO链接: 营业执照
-
+
@@ -1209,8 +1145,8 @@
您可以查看其他栏目或返回 首页
-
-
+
+
@@ -1251,7 +1187,7 @@
SVG图标库请自行添加图标,用div包起来,并命名使用
-
+
-
© SAF-Team 版权所有
盗用有风险,使用需谨慎,加入SAF获取更多技术支持
-
+
© SAF-Team 版权所有
盗用有风险,使用需谨慎,加入SAF获取更多技术支持
+
@@ -1326,32 +1262,32 @@
-
-
-
+
+
+
-
+
@@ -1381,4 +1317,4 @@
-
\ No newline at end of file
+
diff --git a/frontend/src/i18n/index.js b/frontend/src/i18n/index.js
new file mode 100644
index 0000000..5d9b9d8
--- /dev/null
+++ b/frontend/src/i18n/index.js
@@ -0,0 +1,24 @@
+import { createI18n } from 'vue-i18n'
+import zh from '../locales/zh.json'
+import en from '../locales/en.json'
+import ja from '../locales/ja.json'
+
+export const DEFAULT_LOCALE = 'zh_cn'
+export const FALLBACK_LOCALE = 'en_us'
+export const SUPPORTED_LOCALES = ['zh_cn', 'en_us', 'ja_jp']
+
+const i18n = createI18n({
+ legacy: false,
+ globalInjection: true,
+ locale: DEFAULT_LOCALE,
+ fallbackLocale: FALLBACK_LOCALE,
+ missingWarn: false,
+ fallbackWarn: false,
+ messages: {
+ zh_cn: zh,
+ en_us: en,
+ ja_jp: ja
+ }
+})
+
+export default i18n
diff --git a/frontend/src/locales/en.json b/frontend/src/locales/en.json
new file mode 100644
index 0000000..073cae7
--- /dev/null
+++ b/frontend/src/locales/en.json
@@ -0,0 +1,174 @@
+{
+ "common": {
+ "title": {
+ "site_name": "Dalian Huayang Foods Co., Ltd.",
+ "site_home": "Home",
+ "about": "About",
+ "product": "Products",
+ "product_detail": "Product Details",
+ "culture_nav": "Culture",
+ "contact": "Contact",
+ "company_intro": "Company Profile",
+ "honor": "Qualifications & Honors",
+ "tech": "Quality & Tech",
+ "timeline": "Timeline",
+ "store": "Store Showcase",
+ "store_location": "Store Location",
+ "mall": "Huayang Mall"
+ },
+ "button": {
+ "submit": "Submit"
+ },
+ "placeholder": {
+ "name": "Your name",
+ "phone": "Phone number",
+ "email": "Email address",
+ "message": "Your message"
+ }
+ }
+ ,
+ "home": {
+ "section3": {
+ "main": {
+ "title": "Featured Products"
+ },
+ "more": "Learn more >"
+ },
+ "section4": {
+ "main": {
+ "title": "Culture"
+ },
+ "more": "Learn more >",
+ "cards": {
+ "select": {
+ "badge": "Select",
+ "desc": "Deep-sea sourcing"
+ },
+ "freeze": {
+ "badge": "Freeze",
+ "desc": "Rapid fresh-freezing"
+ },
+ "test": {
+ "badge": "Test",
+ "desc": "Quality inspection"
+ },
+ "preserve": {
+ "badge": "Preserve",
+ "desc": "Freshness technology"
+ },
+ "craft": {
+ "badge": "Craft",
+ "desc": "Signature processing"
+ },
+ "deliver": {
+ "badge": "Deliver",
+ "desc": "Cold-chain delivery"
+ },
+ "restore": {
+ "badge": "Serve",
+ "desc": "Ocean flavor restored"
+ }
+ }
+ }
+ }
+ ,
+ "ui": {
+ "menu": "Menu",
+ "back_home": "Back to Home",
+ "loading": {
+ "submitting": "Submitting...",
+ "building": "Content is being updated."
+ },
+ "form": {
+ "required_tip": "Please complete the required fields.",
+ "contact_required": "Please fill in name, phone and message.",
+ "inquiry_required": "Please fill in name, phone and inquiry message."
+ },
+ "status": {
+ "submit_ok_contact": "Submitted successfully. We will contact you soon.",
+ "submit_ok_message": "Message submitted successfully. We will contact you soon.",
+ "submit_ok_message_short": "Message submitted successfully. We will contact you soon.",
+ "submit_fail_retry": "Submission failed. Please try again later."
+ },
+ "network": {
+ "network_error": "Network error",
+ "request_failed": "Request failed"
+ },
+ "empty": {
+ "recommend_products": "No recommended products.",
+ "products": "No products.",
+ "product_desc": "No description."
+ },
+ "culture": {
+ "category_load_failed": "Failed to load categories. Please try again later.",
+ "no_categories": "No culture categories. Please add them in admin.",
+ "tag_load_failed": "Failed to load tags. Please try again later.",
+ "no_tags": "No content in this category. Please add tags in admin.",
+ "content_load_failed": "Failed to load content.",
+ "content_not_found": "Content not found or removed.",
+ "back_list": "Back to Culture List",
+ "read_count": "Views"
+ },
+ "product": {
+ "related": "Related Products",
+ "actions": {
+ "contact": "Contact Us",
+ "detail": "Details"
+ },
+ "operate": "Product actions",
+ "contact_card": "Contact",
+ "phone": "Phone",
+ "email": "Email",
+ "address": "Address",
+ "back_list": "Back to Product List",
+ "inquiry_hint_prefix": "For",
+ "inquiry": "product inquiry",
+ "inquiry_hint_before_link": "please fill out the form below, or",
+ "inquiry_hint_after_link": ".",
+ "qualifications": "Certificates",
+ "price_prefix": "Reference price",
+ "rich": {
+ "watermark_brand": "Huayang Culture",
+ "scientific_name_label": "Scientific name",
+ "latin_name_subcaption": "Latin name",
+ "country_origin_label": "Country of origin",
+ "country_origin_subcaption": "Country of origin",
+ "salmon_line_zh": "Frozen Pink Salmon Series",
+ "salmon_line_en_caps": "FROZEN PINK SALMON SERIES",
+ "salmon_wordmark": "PINK SALMON",
+ "salmon_line_ja": "Pink salmon (Karafuto masu)",
+ "salmon_origin_zh": "Russia, United States",
+ "salmon_origin_en": "RUSSIA, US"
+ }
+ },
+ "offline": {
+ "title_404": "404 Error!",
+ "subtitle_not_found": "Page not found!",
+ "desc_backend_offline": "Sorry, the service is unavailable. Please start the backend service, then refresh or go back to Home.",
+ "refresh_retry": "Retry"
+ },
+ "contact": {
+ "loading_contacts": "Loading contacts...",
+ "no_contacts": "No contact information available.",
+ "submit_message": "Send Message"
+ },
+ "company": {
+ "store_loading_or_empty": "Store data is loading or unavailable.",
+ "no_quality_content": "No quality & tech content. Please add it in admin."
+ },
+ "loading_page": "Loading...",
+ "search": {
+ "prompt_keyword": "Enter a keyword:"
+ }
+ },
+ "contact": {
+ "label_hotline": "Business Hotline:",
+ "label_email": "E-mail:",
+ "label_address": "Address:",
+ "page": {
+ "online_title": "Online Message",
+ "online_hint": "Leave your comments or suggestions here—we will reply as soon as possible.",
+ "location_title": "Our Location"
+ }
+ }
+}
diff --git a/frontend/src/locales/ja.json b/frontend/src/locales/ja.json
new file mode 100644
index 0000000..a6e7a53
--- /dev/null
+++ b/frontend/src/locales/ja.json
@@ -0,0 +1,174 @@
+{
+ "common": {
+ "title": {
+ "site_name": "大連華陽食品有限公司",
+ "site_home": "ホーム",
+ "about": "会社案内",
+ "product": "製品",
+ "product_detail": "製品詳細",
+ "culture_nav": "企業文化",
+ "contact": "お問い合わせ",
+ "company_intro": "会社概要",
+ "honor": "資格・栄誉",
+ "tech": "品質・技術",
+ "timeline": "沿革",
+ "store": "店舗展示",
+ "store_location": "店舗所在地",
+ "mall": "華陽モール"
+ },
+ "button": {
+ "submit": "送信"
+ },
+ "placeholder": {
+ "name": "お名前を入力",
+ "phone": "電話番号を入力",
+ "email": "メールアドレスを入力",
+ "message": "メッセージを入力"
+ }
+ }
+ ,
+ "home": {
+ "section3": {
+ "main": {
+ "title": "製品紹介"
+ },
+ "more": "詳しく見る >"
+ },
+ "section4": {
+ "main": {
+ "title": "企業文化"
+ },
+ "more": "詳しく見る >",
+ "cards": {
+ "select": {
+ "badge": "選ぶ",
+ "desc": "深海素材を厳選"
+ },
+ "freeze": {
+ "badge": "凍結",
+ "desc": "急速冷凍加工"
+ },
+ "test": {
+ "badge": "検査",
+ "desc": "鮮度と品質を確認"
+ },
+ "preserve": {
+ "badge": "保鮮",
+ "desc": "鮮度保持技術"
+ },
+ "craft": {
+ "badge": "加工",
+ "desc": "独自の加工技術"
+ },
+ "deliver": {
+ "badge": "配送",
+ "desc": "一貫したコールドチェーン"
+ },
+ "restore": {
+ "badge": "再現",
+ "desc": "海の旨みを届ける"
+ }
+ }
+ }
+ }
+ ,
+ "ui": {
+ "menu": "メニュー",
+ "back_home": "ホームへ戻る",
+ "loading": {
+ "submitting": "送信中...",
+ "building": "コンテンツ準備中"
+ },
+ "form": {
+ "required_tip": "必須項目をすべて入力してください。",
+ "contact_required": "お名前・電話番号・メッセージ内容を入力してください。",
+ "inquiry_required": "お名前・電話番号・ご要望内容を入力してください。"
+ },
+ "status": {
+ "submit_ok_contact": "送信に成功しました。近日中にご連絡いたします。",
+ "submit_ok_message": "メッセージ送信に成功しました。近日中にご連絡いたします。",
+ "submit_ok_message_short": "メッセージ送信に成功しました。近日中にご連絡いたします。",
+ "submit_fail_retry": "送信に失敗しました。しばらくしてから再度お試しください。"
+ },
+ "network": {
+ "network_error": "ネットワークエラー",
+ "request_failed": "リクエストに失敗しました"
+ },
+ "empty": {
+ "recommend_products": "おすすめ商品はありません。",
+ "products": "商品はありません。",
+ "product_desc": "詳細説明はありません。"
+ },
+ "culture": {
+ "category_load_failed": "カテゴリの読み込みに失敗しました。しばらくしてから再度お試しください。",
+ "no_categories": "カテゴリがありません。管理画面で追加してください。",
+ "tag_load_failed": "タグの読み込みに失敗しました。しばらくしてから再度お試しください。",
+ "no_tags": "このカテゴリには内容がありません。管理画面で追加してください。",
+ "content_load_failed": "内容の読み込みに失敗しました。",
+ "content_not_found": "該当する内容がないか、非公開になっています。",
+ "back_list": "一覧へ戻る",
+ "read_count": "閲覧数"
+ },
+ "product": {
+ "related": "関連商品",
+ "actions": {
+ "contact": "お問い合わせ",
+ "detail": "詳細"
+ },
+ "operate": "商品操作",
+ "contact_card": "連絡先",
+ "phone": "電話",
+ "email": "メール",
+ "address": "住所",
+ "back_list": "商品一覧へ戻る",
+ "inquiry_hint_prefix": "",
+ "inquiry": "商品に関するお問い合わせ",
+ "inquiry_hint_before_link": "は本ページ下部のフォームに入力するか、",
+ "inquiry_hint_after_link": "へお問い合わせください。",
+ "qualifications": "認証・資格",
+ "price_prefix": "参考価格",
+ "rich": {
+ "watermark_brand": "華陽文化",
+ "scientific_name_label": "学名",
+ "latin_name_subcaption": "ラテン名",
+ "country_origin_label": "原産国",
+ "country_origin_subcaption": "COUNTRY OF ORIGIN",
+ "salmon_line_zh": "冷凍ピンクサーモンシリーズ",
+ "salmon_line_en_caps": "FROZEN PINK SALMON SERIES",
+ "salmon_wordmark": "PINK SALMON",
+ "salmon_line_ja": "カラフトマス",
+ "salmon_origin_zh": "ロシア、アメリカ",
+ "salmon_origin_en": "RUSSIA, US"
+ }
+ },
+ "offline": {
+ "title_404": "404 エラー",
+ "subtitle_not_found": "ページが見つかりません。",
+ "desc_backend_offline": "申し訳ありません。現在サービスをご利用いただけません。バックエンドサービスを起動してから更新するか、ホームへ戻ってください。",
+ "refresh_retry": "再試行"
+ },
+ "contact": {
+ "loading_contacts": "連絡先を読み込み中...",
+ "no_contacts": "連絡先情報を取得できませんでした。",
+ "submit_message": "メッセージ送信"
+ },
+ "company": {
+ "store_loading_or_empty": "店舗データを読み込み中、またはデータがありません。",
+ "no_quality_content": "品質・技術の内容がありません。管理画面で追加してください。"
+ },
+ "loading_page": "読み込み中...",
+ "search": {
+ "prompt_keyword": "検索キーワードを入力してください:"
+ }
+ },
+ "contact": {
+ "label_hotline": "ビジネスホットライン:",
+ "label_email": "E-mail:",
+ "label_address": "住所:",
+ "page": {
+ "online_title": "オンラインでのお問い合わせ",
+ "online_hint": "ご意見・ご感想をお寄せください。追ってご返信いたします。",
+ "location_title": "所在地"
+ }
+ }
+}
diff --git a/frontend/src/locales/zh.json b/frontend/src/locales/zh.json
new file mode 100644
index 0000000..21ae5a1
--- /dev/null
+++ b/frontend/src/locales/zh.json
@@ -0,0 +1,174 @@
+{
+ "common": {
+ "title": {
+ "site_name": "大连华阳食品有限公司",
+ "site_home": "首页",
+ "about": "关于我们",
+ "product": "产品中心",
+ "product_detail": "产品详情",
+ "culture_nav": "华阳文化",
+ "contact": "联系我们",
+ "company_intro": "公司简介",
+ "honor": "资质荣誉",
+ "tech": "品质技术",
+ "timeline": "发展历程",
+ "store": "店面展示",
+ "store_location": "店面位置",
+ "mall": "华阳商城"
+ },
+ "button": {
+ "submit": "提交"
+ },
+ "placeholder": {
+ "name": "请输入姓名",
+ "phone": "请输入电话",
+ "email": "请输入邮箱",
+ "message": "请输入留言"
+ }
+ }
+ ,
+ "home": {
+ "section3": {
+ "main": {
+ "title": "产品展示"
+ },
+ "more": "了解更多 >"
+ },
+ "section4": {
+ "main": {
+ "title": "华阳文化"
+ },
+ "more": "了解更多 >",
+ "cards": {
+ "select": {
+ "badge": "选",
+ "desc": "深海鲜冻海珍"
+ },
+ "freeze": {
+ "badge": "留",
+ "desc": "急速冻鲜加工"
+ },
+ "test": {
+ "badge": "测",
+ "desc": "保鲜无双检测"
+ },
+ "preserve": {
+ "badge": "藏",
+ "desc": "超临界鲜冻术"
+ },
+ "craft": {
+ "badge": "制",
+ "desc": "独家秘制工艺"
+ },
+ "deliver": {
+ "badge": "送",
+ "desc": "全程冷链配送"
+ },
+ "restore": {
+ "badge": "还",
+ "desc": "还原深海鲜肴"
+ }
+ }
+ }
+ }
+ ,
+ "ui": {
+ "menu": "菜单",
+ "back_home": "返回首页",
+ "loading": {
+ "submitting": "提交中...",
+ "building": "内容建设中"
+ },
+ "form": {
+ "required_tip": "请完整填写必填项",
+ "contact_required": "请填写姓名、电话和留言内容",
+ "inquiry_required": "请完整填写姓名、电话和需求信息"
+ },
+ "status": {
+ "submit_ok_contact": "提交成功,我们将尽快联系您!",
+ "submit_ok_message": "留言提交成功,我们会尽快与您联系!",
+ "submit_ok_message_short": "留言提交成功,我们会尽快与您联系",
+ "submit_fail_retry": "提交失败,请稍后重试"
+ },
+ "network": {
+ "network_error": "网络错误",
+ "request_failed": "请求失败"
+ },
+ "empty": {
+ "recommend_products": "暂无推荐产品",
+ "products": "暂无产品",
+ "product_desc": "暂无详细描述"
+ },
+ "culture": {
+ "category_load_failed": "分类加载失败,请稍后重试",
+ "no_categories": "暂无文化分类,请在管理端「华阳文化页」中添加",
+ "tag_load_failed": "标签加载失败,请稍后重试",
+ "no_tags": "该分类下暂无内容,请在管理端添加文化标签",
+ "content_load_failed": "内容加载失败",
+ "content_not_found": "暂无该内容或已下架",
+ "back_list": "返回文化列表",
+ "read_count": "阅读量"
+ },
+ "product": {
+ "related": "相关产品",
+ "actions": {
+ "contact": "联系我们",
+ "detail": "产品详情"
+ },
+ "operate": "产品操作",
+ "contact_card": "联系方式",
+ "phone": "电话",
+ "email": "邮箱",
+ "address": "地址",
+ "back_list": "返回产品列表",
+ "inquiry_hint_prefix": "需要",
+ "inquiry": "产品咨询",
+ "inquiry_hint_before_link": "请填写本页下方表单,或",
+ "inquiry_hint_after_link": "。",
+ "qualifications": "资质与认证",
+ "price_prefix": "参考价格",
+ "rich": {
+ "watermark_brand": "华阳文化",
+ "scientific_name_label": "学名",
+ "latin_name_subcaption": "LATIN NAME",
+ "country_origin_label": "原产国",
+ "country_origin_subcaption": "COUNTRY OF ORIGIN",
+ "salmon_line_zh": "冻粉红鲑鱼系列",
+ "salmon_line_en_caps": "FROZEN PINK SALMON SERIES",
+ "salmon_wordmark": "PINK SALMON",
+ "salmon_line_ja": "カラフトマス",
+ "salmon_origin_zh": "俄罗斯,美国",
+ "salmon_origin_en": "RUSSIA, US"
+ }
+ },
+ "offline": {
+ "title_404": "404错误!",
+ "subtitle_not_found": "找不到网页!",
+ "desc_backend_offline": "对不起,当前服务暂不可用。请先启动后端服务,再刷新或返回首页。",
+ "refresh_retry": "刷新重试"
+ },
+ "contact": {
+ "loading_contacts": "联系方式加载中...",
+ "no_contacts": "暂未获取到联系方式。",
+ "submit_message": "提交留言"
+ },
+ "company": {
+ "store_loading_or_empty": "店面数据加载中或暂无数据",
+ "no_quality_content": "暂无品质技术相关内容,请在后台「品质技术」菜单添加。"
+ },
+ "loading_page": "页面加载中...",
+ "search": {
+ "prompt_keyword": "请输入搜索关键词:"
+ }
+ },
+ "contact": {
+ "label_hotline": "商务热线:",
+ "label_email": "E-mail:",
+ "label_address": "地址:",
+ "page": {
+ "online_title": "在线留言",
+ "online_hint": "您可以在这里留下意见和建议,我们会及时回复。",
+ "location_title": "我们的位置"
+ }
+ }
+}
diff --git a/frontend/src/main.js b/frontend/src/main.js
index 2425c0f..7ec9506 100644
--- a/frontend/src/main.js
+++ b/frontend/src/main.js
@@ -1,5 +1,8 @@
import { createApp } from 'vue'
import './style.css'
import App from './App.vue'
+import i18n from './i18n'
+import { initLanguage } from './api/language'
-createApp(App).mount('#app')
+initLanguage()
+createApp(App).use(i18n).mount('#app')
diff --git a/frontend/src/page-config-loader.js b/frontend/src/page-config-loader.js
new file mode 100644
index 0000000..35b902d
--- /dev/null
+++ b/frontend/src/page-config-loader.js
@@ -0,0 +1,138 @@
+const PAGE_META = {
+ home: {
+ title: 'Home',
+ pageId: '7fe175cc-1fde-4aef-8722-6cf9e35aa31d',
+ sourceKey: 'home'
+ },
+ about: {
+ title: 'About',
+ pageId: '0f314d44-bcb7-4dea-b818-1abd55ce0152',
+ sourceKey: 'about'
+ },
+ history: {
+ title: 'History',
+ pageId: '5fa12ca5-7145-40f4-b43c-346ffb358bd0',
+ sourceKey: 'history'
+ },
+ product: {
+ title: 'Product',
+ pageId: 'ca42b3fa-2fc5-4217-a099-272a787889b1',
+ sourceKey: 'product'
+ },
+ culture: {
+ title: 'Culture',
+ pageId: '9132410e-4e93-479d-ae6d-5ea1d5b6e50f',
+ sourceKey: 'culture'
+ },
+ contact: {
+ title: 'Contact',
+ pageId: 'contact-page',
+ sourceKey: 'contact'
+ }
+}
+
+const PAGE_SOURCE_LOADERS = {
+ home: async () => {
+ const [bodyRaw, assets, inlineScriptsRaw] = await Promise.all([
+ import('./generated/page-body.html?raw'),
+ import('./generated/assets.json'),
+ import('./generated/inline-scripts.txt?raw')
+ ])
+
+ return {
+ bodyRaw: bodyRaw.default,
+ assets: assets.default,
+ inlineScriptsRaw: inlineScriptsRaw.default
+ }
+ },
+ about: async () => {
+ const [bodyRaw, assets, inlineScriptsRaw] = await Promise.all([
+ import('./generated/about-page-body.html?raw'),
+ import('./generated/about-assets.json'),
+ import('./generated/about-inline-scripts.txt?raw')
+ ])
+
+ return {
+ bodyRaw: bodyRaw.default,
+ assets: assets.default,
+ inlineScriptsRaw: inlineScriptsRaw.default
+ }
+ },
+ history: async () => {
+ const [bodyRaw, assets, inlineScriptsRaw] = await Promise.all([
+ import('./generated/history-page-body.html?raw'),
+ import('./generated/history-assets.json'),
+ import('./generated/history-inline-scripts.txt?raw')
+ ])
+
+ return {
+ bodyRaw: bodyRaw.default,
+ assets: assets.default,
+ inlineScriptsRaw: inlineScriptsRaw.default
+ }
+ },
+ product: async () => {
+ const [bodyRaw, assets, inlineScriptsRaw] = await Promise.all([
+ import('./generated/product-page-body.html?raw'),
+ import('./generated/product-assets.json'),
+ import('./generated/product-inline-scripts.txt?raw')
+ ])
+
+ return {
+ bodyRaw: bodyRaw.default,
+ assets: assets.default,
+ inlineScriptsRaw: inlineScriptsRaw.default
+ }
+ },
+ culture: async () => {
+ const [bodyRaw, assets, inlineScriptsRaw] = await Promise.all([
+ import('./generated/culture-page-body.html?raw'),
+ import('./generated/culture-assets.json'),
+ import('./generated/culture-inline-scripts.txt?raw')
+ ])
+
+ return {
+ bodyRaw: bodyRaw.default,
+ assets: assets.default,
+ inlineScriptsRaw: inlineScriptsRaw.default
+ }
+ },
+ contact: async () => {
+ const [bodyRaw, assets, inlineScriptsRaw] = await Promise.all([
+ import('./generated/contact-page-body.html?raw'),
+ import('./generated/contact-assets.json'),
+ import('./generated/contact-inline-scripts.txt?raw')
+ ])
+
+ return {
+ bodyRaw: bodyRaw.default,
+ assets: assets.default,
+ inlineScriptsRaw: inlineScriptsRaw.default
+ }
+ }
+}
+
+const pageSourceCache = new Map()
+
+export function hasPageConfig(pageKey) {
+ return Object.prototype.hasOwnProperty.call(PAGE_META, pageKey)
+}
+
+export async function loadPageConfig(pageKey) {
+ const safePageKey = hasPageConfig(pageKey) ? pageKey : 'home'
+ const pageMeta = PAGE_META[safePageKey]
+ const sourceKey = pageMeta.sourceKey
+
+ if (!pageSourceCache.has(sourceKey)) {
+ const loader = PAGE_SOURCE_LOADERS[sourceKey] || PAGE_SOURCE_LOADERS.home
+ pageSourceCache.set(sourceKey, loader())
+ }
+
+ const pageSource = await pageSourceCache.get(sourceKey)
+ return {
+ ...pageMeta,
+ bodyRaw: pageSource.bodyRaw,
+ assets: pageSource.assets,
+ inlineScriptsRaw: pageSource.inlineScriptsRaw
+ }
+}
diff --git a/frontend/src/style.css b/frontend/src/style.css
index d1b1b52..ac92db7 100644
--- a/frontend/src/style.css
+++ b/frontend/src/style.css
@@ -6,6 +6,24 @@ body,
min-height: 100%;
}
+/*
+ * 仅 Logo/微信/Menu 所在条 .leftNav:占位层常 width:100% 且盖在 .topbar(中英日)之上,
+ * 但不改 z-index、不把 Menu 改成 fixed,避免顶栏跑版。
+ * 容器不接收命中,子元素恢复命中 → 空白处点击穿透到下层中英日。
+ */
+.haiho-page .header .leftNav {
+ pointer-events: none;
+}
+
+.haiho-page .header .leftNav * {
+ pointer-events: auto;
+}
+
+.haiho-page .header .topbar .user01 span[data-lang] {
+ pointer-events: auto;
+ cursor: pointer;
+}
+
/* 移动端顶栏:归档导航 CSS 把 .p_iconBox 置于 top:40% 大侧栏坐标下,在 75px 顶栏内不可见;线条在浅底上也不明显 */
@media (max-width: 768px) {
.haiho-page .header {
@@ -29,6 +47,18 @@ body,
z-index: 1;
}
+ .haiho-page .header .topbar .user01 {
+ pointer-events: auto;
+ position: relative;
+ z-index: 50;
+ }
+
+ .haiho-page .header .topbar .user01 span[data-lang] {
+ pointer-events: auto;
+ position: relative;
+ z-index: 2;
+ }
+
.haiho-page .header .p_iconBox {
position: absolute !important;
right: 10px !important;
@@ -134,6 +164,12 @@ body[data-pageid='ca42b3fa-2fc5-4217-a099-272a787889b1'] .haiho-page #c_navigati
.haiho-page .saf-fullscreen > .swiper > .swiper-wrapper {
height: 100%;
+ will-change: transform;
+}
+
+.haiho-page .saf-fullscreen > .swiper:not(.swiper-horizontal) > .swiper-wrapper {
+ display: block !important;
+ transform: translate3d(0, 0, 0);
}
.haiho-page .saf-fullscreen > .swiper > .swiper-wrapper > .swiper-slide {
@@ -142,6 +178,11 @@ body[data-pageid='ca42b3fa-2fc5-4217-a099-272a787889b1'] .haiho-page #c_navigati
overflow: hidden;
}
+body.hy-home-fullpage-transitioning .haiho-page .huayang-honor-marquee-track,
+body.hy-home-fullpage-transitioning .haiho-page .huayang-store-showcase-marquee__track {
+ animation-play-state: paused !important;
+}
+
.haiho-page .e_bannerA-6 .swiper-container,
.haiho-page .e_bannerA-6 .swiper-wrapper,
.haiho-page .e_bannerA-6 .swiper-slide {
@@ -316,53 +357,12 @@ body[data-pageid='ca42b3fa-2fc5-4217-a099-272a787889b1'] .haiho-page #c_navigati
}
.haiho-page .haiho-product-detail-layout {
- display: flex;
- flex-direction: row;
- align-items: flex-start;
- gap: 24px;
-}
-
-.haiho-page .haiho-product-detail-sidebar {
- flex: 0 0 240px;
- position: sticky;
- top: 88px;
-}
-
-.haiho-page .haiho-product-detail-sidecard {
- background: #fff;
- border: 1px solid #e2e8f0;
- border-radius: 10px;
- padding: 20px 16px;
- font-size: 14px;
- color: #334155;
- line-height: 1.6;
-}
-
-.haiho-page .haiho-product-detail-sidecard__title {
- margin: 0 0 16px;
- font-size: 16px;
- font-weight: 600;
- color: #0f172a;
- padding-bottom: 10px;
- border-bottom: 1px solid #ebeef5;
-}
-
-.haiho-page .haiho-product-detail-sidecard__block {
- margin: 0 0 12px;
-}
-
-.haiho-page .haiho-product-detail-sidecard a {
- color: #e60012;
- text-decoration: none;
-}
-
-.haiho-page .haiho-product-detail-sidecard a:hover {
- text-decoration: underline;
+ display: block;
}
.haiho-page .haiho-product-detail-back {
display: inline-block;
- margin-top: 16px;
+ margin-bottom: 18px;
padding: 8px 14px;
border-radius: 8px;
border: 1px solid #cbd5e1;
@@ -432,32 +432,6 @@ body[data-pageid='ca42b3fa-2fc5-4217-a099-272a787889b1'] .haiho-page #c_navigati
font-weight: 700;
}
-.haiho-page .haiho-product-detail-meta {
- margin: 20px 0 24px;
- padding: 16px 0;
- border-top: 1px solid #ebeef5;
- border-bottom: 1px solid #ebeef5;
- text-align: center;
-}
-
-.haiho-page .haiho-product-detail-meta__link {
- color: #e60012;
- font-size: 18px;
- font-weight: 600;
- text-decoration: none;
-}
-
-.haiho-page .haiho-product-detail-meta__link:hover {
- text-decoration: underline;
-}
-
-.haiho-page .haiho-product-detail-meta__hr {
- border: none;
- border-top: 1px solid #e2e8f0;
- margin: 14px auto;
- max-width: 280px;
-}
-
.haiho-page .haiho-product-detail-desc {
line-height: 1.85;
font-size: 15px;
@@ -571,37 +545,6 @@ body[data-pageid='ca42b3fa-2fc5-4217-a099-272a787889b1'] .haiho-page #c_navigati
overflow: hidden;
}
-.haiho-page .haiho-product-detail-inquiry-hint {
- margin: 28px 0 0;
- padding: 14px 16px;
- background: #f8fafc;
- border-radius: 8px;
- font-size: 14px;
- color: #64748b;
- border: 1px solid #e2e8f0;
-}
-
-.haiho-page .haiho-product-detail-inquiry-hint a {
- color: #2563eb;
- text-decoration: none;
-}
-
-.haiho-page .haiho-product-detail-inquiry-hint a:hover {
- text-decoration: underline;
-}
-
-@media (max-width: 900px) {
- .haiho-page .haiho-product-detail-layout {
- flex-direction: column;
- }
-
- .haiho-page .haiho-product-detail-sidebar {
- flex: none;
- width: 100%;
- position: static;
- }
-}
-
/* Product detail image swiper */
.haiho-page .product-detail-swiper .swiper {
width: 100%;
@@ -643,20 +586,21 @@ body[data-pageid='ca42b3fa-2fc5-4217-a099-272a787889b1'] .haiho-page #c_navigati
}
/*
- * 首页「华阳七道鲜」.industry-list
+ * 首页「华阳文化」.industry-list
* 归档 CSS 依赖 legacy JS 为子项切换 .now / .sible 宽度;SPA 关闭 ENABLE_LEGACY_RUNTIME 后失效。
* 以下用 flex + :has(> div:hover) 还原悬停展开,子项数量变化时仍按比例分配。
*/
@media screen and (min-width: 769px) {
- .haiho-page #c_static_001_P_33409-1715331797067 .industry-list {
+ .haiho-page [id^='c_static_001_P_33409-'] .industry-list {
display: flex !important;
flex-direction: row;
flex-wrap: nowrap !important;
align-items: stretch;
box-sizing: border-box;
+ overflow: hidden;
}
- .haiho-page #c_static_001_P_33409-1715331797067 .industry-list > div {
+ .haiho-page [id^='c_static_001_P_33409-'] .industry-list > div {
float: none !important;
width: auto !important;
flex: 1 1 0 !important;
@@ -668,14 +612,14 @@ body[data-pageid='ca42b3fa-2fc5-4217-a099-272a787889b1'] .haiho-page #c_navigati
transition: flex 0.45s cubic-bezier(0.4, 0, 0.2, 1) !important;
}
- .haiho-page #c_static_001_P_33409-1715331797067 .industry-list > div .e_image-6 {
+ .haiho-page [id^='c_static_001_P_33409-'] .industry-list > div .e_image-6 {
position: relative;
flex: 1 1 auto;
min-height: 0;
overflow: hidden;
}
- .haiho-page #c_static_001_P_33409-1715331797067 .industry-list > div .e_image-6 img {
+ .haiho-page [id^='c_static_001_P_33409-'] .industry-list > div .e_image-6 img {
width: 100% !important;
height: 100% !important;
min-height: 0 !important;
@@ -688,20 +632,20 @@ body[data-pageid='ca42b3fa-2fc5-4217-a099-272a787889b1'] .haiho-page #c_navigati
}
@media screen and (min-width: 769px) and (hover: hover) and (pointer: fine) {
- .haiho-page #c_static_001_P_33409-1715331797067 .industry-list:has(> div:hover) > div:not(:hover) {
+ .haiho-page [id^='c_static_001_P_33409-'] .industry-list:has(> div:hover) > div:not(:hover) {
flex: 0.52 1 0 !important;
}
- .haiho-page #c_static_001_P_33409-1715331797067 .industry-list > div:hover {
+ .haiho-page [id^='c_static_001_P_33409-'] .industry-list > div:hover {
flex: 3.2 1 0 !important;
}
- .haiho-page #c_static_001_P_33409-1715331797067 .industry-list > div:hover .e_image-6 img {
+ .haiho-page [id^='c_static_001_P_33409-'] .industry-list > div:hover .e_image-6 img {
opacity: 1 !important;
transform: scale(1.07) !important;
}
- .haiho-page #c_static_001_P_33409-1715331797067 .industry-list > div:hover .e_image-6:after {
+ .haiho-page [id^='c_static_001_P_33409-'] .industry-list > div:hover .e_image-6:after {
opacity: 0.05 !important;
}
}
@@ -845,12 +789,6 @@ body[data-pageid='about-honor-page'] .haiho-page #c_static_001_P_4578-1694141444
box-sizing: border-box;
}
-.haiho-page #c_static_001_P_4578-16941414442160 .huayang-honor-card-link {
- display: block;
- color: inherit;
- text-decoration: none;
-}
-
.haiho-page #c_static_001_P_4578-16941414442160 .huayang-honor-card-inner {
padding: 12px 12px 16px;
}
@@ -880,8 +818,7 @@ body[data-pageid='about-honor-page'] .haiho-page #c_static_001_P_4578-1694141444
text-align: center;
}
-.haiho-page #c_static_001_P_4578-16941414442160 .huayang-honor-card-type,
-.haiho-page #c_static_001_P_4578-16941414442160 .huayang-honor-card-desc {
+.haiho-page #c_static_001_P_4578-16941414442160 .huayang-honor-card-type {
margin: 6px 0 0;
font-size: 12px;
line-height: 1.4;
@@ -1513,248 +1450,12 @@ body[data-pageid='3dab96dd-d8d5-45e3-af9a-ff735532e65c']
min-width: 0;
}
-/* 新闻详情封面:与列表图一致的小图规格,避免超大图 */
-.haiho-page .news-detail-container .news-cover-thumb {
- text-align: left;
- margin-bottom: 16px;
-}
-.haiho-page .news-detail-container .news-cover-thumb img {
- width: 180px;
- height: 110px;
- object-fit: cover;
- border-radius: 8px;
- display: block;
-}
-
-/* 新闻动态正文内图片:限制尺寸(含 Quill 写入的内联宽高) */
-.haiho-page .news-rich-text img,
-.haiho-page .e_richText-40.news-rich-text img,
-.haiho-page .news-detail-container .e_richText-40 img {
- max-width: min(100%, 720px) !important;
- width: auto !important;
- height: auto !important;
- object-fit: contain;
- display: block;
- margin: 12px auto;
- border-radius: 8px;
- box-sizing: border-box;
-}
-
-.haiho-page .news-rich-text p,
-.haiho-page .e_richText-40.news-rich-text p {
- overflow-wrap: break-word;
-}
-
-/* 新闻详情富文本:正文内嵌视频(上传 mp4)与外链 iframe(YouTube 等) */
-.haiho-page .news-rich-text video,
-.haiho-page .news-rich-text iframe.ql-video {
- max-width: 100%;
- display: block;
- margin: 12px auto;
- border-radius: 8px;
-}
-
-/* 新闻中心分类 Tab:接口注入后当前分类高亮(bindNewsCategoryTabs) */
-.haiho-page .e_loop-3 .p_loopitem.huayang-news-tab--active .e_text-4 a {
- color: #cf261d;
- font-weight: 600;
-}
-
-/* 新闻列表(injectNewsList):左封面 + 中标题 + 右日期,封面约 16:9 */
-.haiho-page .p_list .huayang-news-list-item {
- margin-bottom: 12px;
-}
-.haiho-page .p_list .huayang-news-list-item:last-child {
- margin-bottom: 0;
-}
-
-.haiho-page .huayang-news-list-item__inner {
- box-sizing: border-box;
- display: flex;
- flex-direction: row;
- align-items: center;
- gap: 16px;
- width: 100%;
- padding: 12px 16px;
- background: #fff;
- border: 1px solid #e8eef4;
- border-radius: 10px;
-}
-
-.haiho-page .huayang-news-list-item__thumb {
- flex: 0 0 220px;
- width: 220px;
- max-width: 26%;
- aspect-ratio: 16 / 9;
- border-radius: 8px;
- overflow: hidden;
- display: block;
- flex-shrink: 0;
-}
-
-.haiho-page .huayang-news-list-item__thumb img {
- width: 100%;
- height: 100%;
- object-fit: cover;
- display: block;
-}
-
-.haiho-page .huayang-news-list-item__main {
- flex: 1;
- min-width: 0;
-}
-
-.haiho-page .huayang-news-list-item__title {
- font-size: 17px;
- font-weight: 600;
- color: #1f2937;
- text-decoration: none;
- line-height: 1.5;
- display: block;
-}
-
-.haiho-page .huayang-news-list-item__title:hover {
- color: #cf261d;
-}
-
-/* 新闻列表底部分页(归档 HTML .p_page + injectNewsList 注入) */
-.haiho-page .e_loop-16 .p_page.huayang-news-pagination .page_con,
-.haiho-page .e_loop-20 .p_page.huayang-news-pagination .page_con {
- display: flex;
- justify-content: center;
- align-items: center;
- gap: 8px;
- margin-top: 16px;
- flex-wrap: wrap;
-}
-
-.haiho-page .e_loop-16 .p_page.huayang-news-pagination button.page_a,
-.haiho-page .e_loop-20 .p_page.huayang-news-pagination button.page_a {
- box-sizing: border-box;
- min-width: 36px;
- height: 36px;
- padding: 0 10px;
- margin: 0;
- border: 1px solid #d1d5db;
- border-radius: 6px;
- background: #fff;
- color: #374151;
- font-size: 14px;
- line-height: 1;
- font-family: inherit;
- cursor: pointer;
- display: inline-flex;
- align-items: center;
- justify-content: center;
- transition: background 0.15s ease, border-color 0.15s ease, color 0.15s ease;
-}
-
-.haiho-page .e_loop-16 .p_page.huayang-news-pagination button.page_a.page_num.current,
-.haiho-page .e_loop-20 .p_page.huayang-news-pagination button.page_a.page_num.current {
- background: #cf261d;
- border-color: #cf261d;
- color: #fff;
-}
-
-.haiho-page .e_loop-16 .p_page.huayang-news-pagination button.page_a:disabled,
-.haiho-page .e_loop-20 .p_page.huayang-news-pagination button.page_a:disabled {
- background: #f3f4f6;
- border-color: #e5e7eb;
- color: #9ca3af;
- cursor: not-allowed;
-}
-
-/* 独立新闻页 NewsPage.vue 分页(与上同款) */
-.haiho-page .huayang-news-pager,
-.huayang-news-pager {
- display: flex;
- justify-content: center;
- align-items: center;
- gap: 8px;
- margin-top: 16px;
- flex-wrap: wrap;
-}
-
-.haiho-page .huayang-news-pager__btn,
-.huayang-news-pager__btn {
- box-sizing: border-box;
- min-width: 36px;
- height: 36px;
- padding: 0 10px;
- margin: 0;
- border: 1px solid #d1d5db;
- border-radius: 6px;
- background: #fff;
- color: #374151;
- font-size: 14px;
- line-height: 1;
- font-family: inherit;
- cursor: pointer;
- display: inline-flex;
- align-items: center;
- justify-content: center;
- transition: background 0.15s ease, border-color 0.15s ease, color 0.15s ease;
-}
-
-.haiho-page .huayang-news-pager__btn.is-active,
-.huayang-news-pager__btn.is-active {
- background: #cf261d;
- border-color: #cf261d;
- color: #fff;
-}
-
-.haiho-page .huayang-news-pager__btn:disabled,
-.huayang-news-pager__btn:disabled {
- background: #f3f4f6;
- border-color: #e5e7eb;
- color: #9ca3af;
- cursor: not-allowed;
-}
-
-.haiho-page .huayang-news-list-item__date {
- flex: 0 0 auto;
- display: flex;
- flex-direction: column;
- align-items: flex-end;
- justify-content: center;
- text-align: right;
- padding-left: 8px;
-}
-
-.haiho-page .huayang-news-list-item__ym {
- font-size: 14px;
- color: #64748b;
- margin-top: 0;
-}
-
-@media (max-width: 768px) {
- .haiho-page .huayang-news-list-item__inner {
- flex-wrap: wrap;
- align-items: flex-start;
- }
-
- .haiho-page .huayang-news-list-item__thumb {
- flex: 0 0 120px;
- width: 120px;
- max-width: 38%;
- }
-
- .haiho-page .huayang-news-list-item__main {
- flex: 1;
- min-width: 0;
- }
-
- .haiho-page .huayang-news-list-item__date {
- width: 100%;
- flex-direction: row;
- align-items: baseline;
- justify-content: flex-end;
- gap: 8px;
- padding-left: 0;
- margin-top: 8px;
- }
-
- .haiho-page .huayang-news-list-item__ym {
- margin-top: 0;
- }
+/* 页脚红条:隐藏「留资」表单(白底输入框 + 红色提交按钮)以及上方文案 e_text-92/93 */
+.haiho-page .e_container-74 .e_form-94.s_form_layout1,
+.haiho-page .s_tmpl_footer .e_form-94.s_form_layout1,
+.haiho-page .e_container-74 .e_text-92,
+.haiho-page .e_container-74 .e_text-93,
+.haiho-page .s_tmpl_footer .e_text-92,
+.haiho-page .s_tmpl_footer .e_text-93 {
+ display: none !important;
}
diff --git a/frontend/src/utils/asset.js b/frontend/src/utils/asset.js
index 24b26b3..20a998d 100644
--- a/frontend/src/utils/asset.js
+++ b/frontend/src/utils/asset.js
@@ -1,16 +1,25 @@
/**
- * 统一资源路径解析工具
- * 逻辑优先级:
- * 1. 项目静态资源 (/assets/) -> 直接返回
- * 2. 完整 URL 且为 /profile 资源 -> 若指向本机开发地址则改为同源 /profile,避免正式站仍请求 localhost
- * 3. 其它完整 URL -> 原样返回
- * 4. 相对路径:去掉 /dev-api 等若依前缀后,统一为 /profile/... 供 Nginx/Vite 代理
+ * 统一资源路径解析(门户各模块共用)
+ * - 多图逗号分隔时取第一张
+ * - 去掉若依接口前缀 /dev-api、/prod-api、/stage-api
+ * - 若库中存的是 http://localhost(或 127.0.0.1 等)/profile|/assets|/images|/resource/...,
+ * 转为同源路径,避免正式环境仍请求本机开发端口导致 404
+ * - 相对路径由 Vite 开发代理或 Nginx 反代到后端
*/
const LOCAL_HOST_SET = new Set(['localhost', '127.0.0.1', '0.0.0.0', '[::1]'])
-const PROFILE_UPLOAD_CACHE_BUSTER = `cb=${Date.now().toString(36)}`
+const RESOURCE_BASE_URL = String(import.meta.env.VITE_RESOURCE_BASE_URL || '').replace(/\/+$/, '')
+const FORCE_IMAGE_CACHE_BUSTER =
+ String(import.meta.env.VITE_FORCE_IMAGE_CACHE_BUSTER ?? 'false').toLowerCase() === 'true'
+const PROFILE_UPLOAD_CACHE_BUSTER_VALUE = String(
+ import.meta.env.VITE_IMAGE_CACHE_BUSTER || Date.now().toString(36)
+)
+const PROFILE_UPLOAD_CACHE_BUSTER = `cb=${encodeURIComponent(PROFILE_UPLOAD_CACHE_BUSTER_VALUE)}`
+export const BLANK_IMAGE_DATA_URI =
+ 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///ywAAAAAAQABAAACAUwAOw=='
function shouldBypassCache() {
if (typeof window === 'undefined') return false
+ if (FORCE_IMAGE_CACHE_BUSTER) return true
return LOCAL_HOST_SET.has(String(window.location.hostname || '').toLowerCase())
}
@@ -21,32 +30,102 @@ function appendProfileUploadCacheBuster(input) {
return input.includes('?') ? `${input}&${PROFILE_UPLOAD_CACHE_BUSTER}` : `${input}?${PROFILE_UPLOAD_CACHE_BUSTER}`
}
+function resolveBackendResource(path) {
+ const value = appendProfileUploadCacheBuster(path)
+ if (!RESOURCE_BASE_URL || !/^\/(?:profile|images|resource)\//i.test(value)) return value
+ return `${RESOURCE_BASE_URL}${value}`
+}
+
export function resolveImageUrl(url) {
if (!url) return ''
+ const first = String(url).split(',')[0].trim().replace(/\\/g, '/')
+ if (!first) return ''
+ if (/^(?:data|blob):/i.test(first)) return first
- const firstUrl = url.split(',')[0].trim()
- if (!firstUrl) return ''
-
- if (firstUrl.startsWith('/assets/')) return firstUrl
-
- if (/^https?:\/\//i.test(firstUrl)) {
+ if (/^https?:\/\//i.test(first)) {
try {
- const u = new URL(firstUrl)
- if (u.pathname.startsWith('/profile')) {
- const h = u.hostname
- if (h === 'localhost' || h === '127.0.0.1' || h === '0.0.0.0') {
- return appendProfileUploadCacheBuster(u.pathname + (u.search || ''))
- }
+ const u = new URL(first)
+ const pathOnly = u.pathname + (u.search || '')
+ const h = String(u.hostname || '').toLowerCase()
+ const loopback = LOCAL_HOST_SET.has(h)
+ if (loopback && u.pathname.startsWith('/assets')) {
+ return BLANK_IMAGE_DATA_URI
+ }
+ if (
+ loopback &&
+ (u.pathname.startsWith('/profile') ||
+ u.pathname.startsWith('/images') ||
+ u.pathname.startsWith('/resource'))
+ ) {
+ return resolveBackendResource(pathOnly)
}
} catch {
/* ignore */
}
- return appendProfileUploadCacheBuster(firstUrl)
+ return appendProfileUploadCacheBuster(first)
}
- let p = firstUrl.startsWith('/') ? firstUrl : `/${firstUrl}`
- p = p.replace(/^\/(?:dev-api|prod-api|stage-api)(?=\/)/, '')
- const cleanPath = p.startsWith('/') ? p.slice(1) : p
- const resolved = cleanPath.startsWith('profile/') ? `/${cleanPath}` : `/profile/${cleanPath}`
- return appendProfileUploadCacheBuster(resolved)
+ let path = first.startsWith('/') ? first : `/${first}`
+ path = path.replace(/^\/(?:dev-api|prod-api|stage-api)(?=\/)/i, '')
+
+ if (path.startsWith('/assets/')) {
+ return BLANK_IMAGE_DATA_URI
+ }
+ if (
+ path.startsWith('/profile/') ||
+ path.startsWith('/images/') ||
+ path.startsWith('/resource/')
+ ) {
+ return resolveBackendResource(path)
+ }
+ if (path.startsWith('/')) {
+ return appendProfileUploadCacheBuster(path)
+ }
+ return appendProfileUploadCacheBuster(first)
+}
+
+function escapeAttr(value) {
+ return String(value || '')
+ .replace(/&/g, '&')
+ .replace(/"/g, '"')
+ .replace(//g, '>')
+}
+
+function resolveSrcset(value) {
+ return String(value || '')
+ .split(',')
+ .map((part) => {
+ const pieces = part.trim().split(/\s+/).filter(Boolean)
+ if (!pieces.length) return ''
+ const resolved = resolveImageUrl(pieces[0])
+ if (!resolved) return ''
+ return [resolved, ...pieces.slice(1)].join(' ')
+ })
+ .filter(Boolean)
+ .join(', ')
+}
+
+export function resolveRichImageHtml(html) {
+ if (html == null || html === '') return ''
+ return String(html)
+ .replace(
+ /\s(src|data-src|data-original|lazy)=("([^"]*)"|'([^']*)'|([^\s>]+))/gi,
+ (full, attr, _raw, dqValue, sqValue, bareValue) => {
+ const rawValue = dqValue ?? sqValue ?? bareValue ?? ''
+ const resolved = resolveImageUrl(rawValue)
+ return resolved ? ` ${attr}="${escapeAttr(resolved)}"` : full
+ }
+ )
+ .replace(
+ /\ssrcset=("([^"]*)"|'([^']*)')/gi,
+ (full, _raw, dqValue, sqValue) => {
+ const resolved = resolveSrcset(dqValue ?? sqValue ?? '')
+ return resolved ? ` srcset="${escapeAttr(resolved)}"` : full
+ }
+ )
+ .replace(/url\(\s*(['"]?)([^'")]+)\1\s*\)/gi, (full, _quote, rawUrl) => {
+ const resolved = resolveImageUrl(rawUrl)
+ return resolved ? `url("${escapeAttr(resolved)}")` : full
+ })
}
diff --git a/frontend/src/utils/asyncCache.js b/frontend/src/utils/asyncCache.js
new file mode 100644
index 0000000..2471121
--- /dev/null
+++ b/frontend/src/utils/asyncCache.js
@@ -0,0 +1,63 @@
+const asyncCacheStore = new Map()
+
+export async function remember(key, loader, options = {}) {
+ const ttlMs = Number(options.ttlMs) > 0 ? Number(options.ttlMs) : 30000
+ const cacheEmpty = options.cacheEmpty !== false
+ const now = Date.now()
+ const hit = asyncCacheStore.get(key)
+
+ if (hit) {
+ if (hit.promise) return hit.promise
+ if (hit.expiresAt > now) return hit.value
+ }
+
+ const promise = Promise.resolve().then(loader)
+ asyncCacheStore.set(key, {
+ value: hit?.value,
+ expiresAt: hit?.expiresAt || 0,
+ promise
+ })
+
+ try {
+ const value = await promise
+ const shouldCache =
+ cacheEmpty ||
+ !(
+ value == null ||
+ value === '' ||
+ (Array.isArray(value) && value.length === 0)
+ )
+
+ if (shouldCache) {
+ asyncCacheStore.set(key, {
+ value,
+ expiresAt: Date.now() + ttlMs,
+ promise: null
+ })
+ } else {
+ asyncCacheStore.delete(key)
+ }
+
+ return value
+ } catch (error) {
+ asyncCacheStore.delete(key)
+ throw error
+ }
+}
+
+export function forget(key) {
+ asyncCacheStore.delete(key)
+}
+
+export function clearByPrefix(prefix = '') {
+ if (!prefix) {
+ asyncCacheStore.clear()
+ return
+ }
+
+ for (const key of asyncCacheStore.keys()) {
+ if (String(key).startsWith(prefix)) {
+ asyncCacheStore.delete(key)
+ }
+ }
+}
diff --git a/frontend/src/utils/imageLoading.js b/frontend/src/utils/imageLoading.js
new file mode 100644
index 0000000..2a8afeb
--- /dev/null
+++ b/frontend/src/utils/imageLoading.js
@@ -0,0 +1,68 @@
+/** 1×1 透明 GIF,归档占位图注入前使用,避免请求不存在的 /assets/ 文件 */
+export const ARCHIVE_IMG_PLACEHOLDER =
+ 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///ywAAAAAAQABAAACAUwAOw=='
+
+/**
+ * 将归档 HTML 中 eager 的 img src 改为 lazy 延迟加载(配合 setupLegacyLazyImageFallback)
+ */
+export function deferArchiveImages(html) {
+ if (!html) return html
+ return html.replace(/
![]()
]*)>/gi, (full, inner) => {
+ if (/\blazy\s*=/i.test(inner)) return full
+ const srcMatch = inner.match(/\bsrc\s*=\s*(["'])([^"']+)\1/i)
+ if (!srcMatch) return full
+ const src = srcMatch[2].trim()
+ if (!src || src.startsWith('data:')) return full
+ const quote = srcMatch[1]
+ const withoutSrc = inner.replace(srcMatch[0], '').trim()
+ const spacer = withoutSrc ? ` ${withoutSrc}` : ''
+ return `

`
+ })
+}
+
+/** 去掉归档 HTML 内联 style 中的 background url,避免首屏请求无效静态图 */
+export function neutralizeArchiveInlineBackgrounds(html) {
+ if (!html) return html
+ return html.replace(/\bstyle\s*=\s*(["'])([\s\S]*?)\1/gi, (full, quote, style) => {
+ if (!/url\s*\(/i.test(style)) return full
+ const cleaned = style
+ .replace(/background-image\s*:\s*url\([^)]*\)\s*;?/gi, '')
+ .replace(/background\s*:\s*[^;]*url\([^)]*\)[^;]*;?/gi, '')
+ .replace(/;\s*;/g, ';')
+ .trim()
+ if (cleaned === style.trim()) return full
+ if (!cleaned) return ''
+ return `style=${quote}${cleaned}${quote}`
+ })
+}
+
+/** @param {'high'|'lazy'} mode */
+export function applyImageLoadPriority(img, mode = 'lazy') {
+ if (!img) return
+ img.decoding = 'async'
+ if (mode === 'high') {
+ img.loading = 'eager'
+ img.setAttribute('fetchpriority', 'high')
+ } else {
+ img.loading = 'lazy'
+ img.removeAttribute('fetchpriority')
+ }
+}
+
+/** 区块进入视口后再应用全屏背景,减少首屏外大图请求 */
+export function observeDeferredBackground(target, applyFn, rootMargin = '120px 0px') {
+ if (!target || typeof applyFn !== 'function') return
+ if (!('IntersectionObserver' in window)) {
+ applyFn()
+ return
+ }
+ const observer = new IntersectionObserver(
+ (entries) => {
+ if (!entries.some((e) => e.isIntersecting)) return
+ applyFn()
+ observer.disconnect()
+ },
+ { rootMargin }
+ )
+ observer.observe(target)
+}
diff --git a/frontend/src/utils/swiperRuntime.js b/frontend/src/utils/swiperRuntime.js
new file mode 100644
index 0000000..72ed923
--- /dev/null
+++ b/frontend/src/utils/swiperRuntime.js
@@ -0,0 +1,51 @@
+let swiperClassPromise = null
+let swiperStylesPromise = null
+
+export function ensureSwiperStyles() {
+ if (typeof document !== 'undefined' && document.documentElement.dataset.haihoSwiperStyles === '1') {
+ return Promise.resolve()
+ }
+
+ if (!swiperStylesPromise) {
+ swiperStylesPromise = import('swiper/css/bundle')
+ .then(() => {
+ if (typeof document !== 'undefined') {
+ document.documentElement.dataset.haihoSwiperStyles = '1'
+ }
+ })
+ .catch((error) => {
+ swiperStylesPromise = null
+ throw error
+ })
+ }
+
+ return swiperStylesPromise
+}
+
+export function ensureSwiperClass() {
+ if (typeof window !== 'undefined' && window.Swiper) {
+ return Promise.resolve(window.Swiper)
+ }
+
+ if (!swiperClassPromise) {
+ swiperClassPromise = import('swiper/bundle')
+ .then((mod) => {
+ const SwiperClass = mod?.default || mod?.Swiper || null
+ if (SwiperClass && typeof window !== 'undefined' && !window.Swiper) {
+ window.Swiper = SwiperClass
+ }
+ return SwiperClass
+ })
+ .catch((error) => {
+ swiperClassPromise = null
+ throw error
+ })
+ }
+
+ return swiperClassPromise
+}
+
+export async function ensureSwiperRuntime() {
+ const [SwiperClass] = await Promise.all([ensureSwiperClass(), ensureSwiperStyles()])
+ return SwiperClass
+}
diff --git a/frontend/vite.config.js b/frontend/vite.config.js
index 80432b8..dde2d8f 100644
--- a/frontend/vite.config.js
+++ b/frontend/vite.config.js
@@ -4,7 +4,7 @@ import vue from '@vitejs/plugin-vue'
// https://vite.dev/config/
export default defineConfig(({ mode }) => {
const env = loadEnv(mode, process.cwd(), '')
- const proxyTarget = env.VITE_DEV_PROXY_TARGET || 'http://localhost:8080'
+ const proxyTarget = env.VITE_DEV_PROXY_TARGET || 'http://localhost:7003'
const staticOnlyMode = mode === 'development' && env.VITE_STATIC_ONLY !== 'false'
const proxy = staticOnlyMode
? undefined
@@ -35,6 +35,7 @@ export default defineConfig(({ mode }) => {
chunkSizeWarningLimit: 3072
},
server: {
+ port: Number(env.VITE_PORT || 7002),
proxy,
host: '0.0.0.0'
},
diff --git a/qc b/qc
deleted file mode 100644
index b833c99..0000000
--- a/qc
+++ /dev/null
@@ -1 +0,0 @@
-MySQL80