10 Days, 2 Cities: Explore Sichuan - Tibet with Ease: A Journey through Letong Ancient Town, Cuopu Valley, Ranwu Lake, Midoi Glacier, Basum Tso, Potala Palace, and Jokhang Temple

10-Day Chengdu to Lhasa Discovery | Dive Deep into the Beauty of Sichuan & Tibet

$1,922.00
Date
Date
Quantity
Expert Local Guides
Compact and Rich Schedules
Selected Well-located Hotels
Customize Your Tour

Description

10 Days, 2 Cities: Explore Sichuan - Tibet with Ease: A Journey through Letong Ancient Town, Cuopu Valley, Ranwu Lake, Midoi Glacier, Basum Tso, Potala Palace, and Jokhang Temple

Tour Prices/per person
Group Sizes Total Price (USD)
3 guests 1922

*Please make your reservation at least one month in advance to ensure availability.
*Prices are per person on twin sharing.
A single room surcharge applies if a traveler stays alone in a separate room at the designated hotel for the entire trip.(279 USD/per person)

✨Tour Highlights

Worry-Free Travel Services

  • Chengdu airport/train station pick-up & drop-off

  • 1-night stay at a 4-star hotel in Chengdu

  • Travel insurance up to ¥800,000 per person

Immersive Tibetan Cultural Experience

  • Visit traditional Tibetan villages: welcome hada, butter tea, and zanba making

  • Tibetan-style travel photoshoot: 3 retouched photos + 5 HD originals (with simple makeup)

  • Taste of Tibet: 9 breakfasts + 1 specialty meal (Lulang Stone Pot Chicken)

  • Tibetan-style blessing ritual: toss Lungta under snow-capped mountains

Thoughtful Travel Essentials Provided

  • Portable oxygen, first-aid kit, oximeter, Rhodiola tea, neck pillow, thermos, and more

Signature + Off-the-Beaten-Path Destinations

  • ★ Litang Letong Ancient Town: micro museums of Kham culture

  • ★ Cuopu Valley: a hidden paradise with turquoise "jelly lake"

  • ★ Midui Glacier & Ranwu Lake: raw and breathtaking nature

  • ★ Basom Lake: stunning turquoise waters

  • ★ Potala Palace & Jokhang Temple: iconic Tibetan Buddhist landmarks

  • ★ Hidden gems: Zheduo Mountain, Sky Road 18 Bends, Kazila Mountain, Dongda Pass, Nujiang 72 Bends, Lulang Town, Mount Namcha Barwa, and more

Professional English-speaking Tour Leader
Guided support throughout the entire journey


10-Day Itinerary

+ Day 1: Arrival in Chengdu

Accommodation: Complimentary 4-star hotel (twin room)
Meals: Not included
Details:
Arrive in Chengdu from around the world and check in to the hotel arranged by “Play G318.” Today is your free arrival day — no scheduled activities.
Suggested Activities:
Explore Chengdu at your own pace. Stroll through the stylish Chunxi Road and visit the ancient Daci Temple, or immerse yourself in local life at Jinli Ancient Street and Kuanzhai Alley.
Reminders:
1. Stay safe during your free time. Travel insurance is not included today.
2. Check-in is available after 2:00 PM; show your name at the front desk. A deposit may be required.
3. Your tour leader will contact you by 9:00 PM with next day departure details. If not, contact customer support.

+ Day 2: Chengdu – Kangding – Zheduo Mountain – Xinduqiao – Yajiang

Altitude: 200m → 4,298m → 2,600m
Highlights: Zheduo Mountain, Xinduqiao “Paradise for Photographers”
Distance: 420 km / ~7 hrs
Accommodation: Twin room in Yajiang
Meals: Breakfast included

Details:
Depart Chengdu early, passing through Ya’an and the Erlang Mountain Tunnel, arriving at Kangding — capital of Ganzi Tibetan Autonomous Prefecture. Continue over Zheduo Mountain Pass (4,298m) to reach Xinduqiao, famed for its epic highland landscapes. Enjoy sweeping views of grasslands, creeks, Tibetan villages, and grazing yaks before heading to Yajiang for the night.

Tips:
1. Depending on time and road conditions, we may take either the S434 (Red Sea Lake) or G318 route.
2. Avoid bathing for the first 3 days to reduce risk of altitude sickness.
3. Tibetan areas may experience temporary power/water cuts — bring wet wipes and a flashlight just in case.

+ Day 3: Yajiang – 18 Bends – Litang (Letong Ancient Town) – Maoya Grassland – Sister Lakes – Cuopu Valley Area

Altitude: 2,600m → 4,014m → 3,300m
Highlights: Hometown of influencer Ding Zhen (Litang), Letong Ancient Town, Sister Lakes
Distance: 250 km / ~6 hrs
Accommodation: Twin room near Cuopu Valley
Meals: Breakfast included

Details:
Drive the winding 18 Bends and cross Kazila Mountain Pass (4,718m) to reach Litang, one of the world’s highest towns.
Explore Letong Ancient Town, home to over 4,000 traditional Tibetan houses, and visit:
318 Memory Museum
Kham People Museum
Renkang Mansion (home to 13 living Buddhas)
Tsangyang Gyatso Museum
Changtso Monastery – the largest Yellow Sect monastery in the Kham region

Continue past Wuliang River and Rabbit Mountain, through Haizi Mountain Nature Reserve, with a final stop at the beautiful Sister Lakes before arriving at Cuopu Valley.
Tips:
1. You’ll be heading into higher altitudes — portable oxygen will be provided if needed.
2. Accommodation may be in Cuopu Valley or Batang depending on availability.

+ Day 4: Cuopu Valley – Mangkang

Altitude: 3,300m → 4,100m → 3,800m
Highlights: Hidden gem of Sichuan – Cuopu Valley
Distance: 155 km / ~4.5 hrs
Accommodation: Twin room in Mangkang
Meals: Breakfast included

Details:
Today, explore the stunning Cuopu Valley, a secluded highland paradise with snowy peaks, grasslands, forests, lakes, and temples. Hop on the eco-bus and visit:
Yasuo Monastery (Nyingma Sect)
Hot Spring Zone – where you can boil eggs in 85°C geothermal springs
Zhangde Grassland and the sacred Zajin Jiabo Mountain
Cuopu Lake, Cuopu Monastery, and other hidden lakes

After sightseeing, cross Jinsha River Bridge and officially enter Tibet, arriving in Mangkang by evening.

+ Day 5: Mangkang – Dongda Mountain – Zogang – 72 Bends – Baxoi

Altitude: 3,800m → 5,008m → 3,260m
Highlights: Dongda Mountain, 72 Bends Road, Bangda Grasslands
Distance: 360 km / ~8 hrs
Accommodation: Twin room in Baxoi
Meals: Breakfast included

Details:
Start your high-altitude drive by crossing Lawu Mountain (4,376m) and Dongda Mountain Pass (5,008m) — one of the highest on this route.
Follow the beautiful Yuqu River to Bangda Grassland, then descend the dizzying 72 Hairpin Turns (aka Tianlu 72 Bends) from Yela Mountain for incredible photo ops of terraced fields and villages far below.
Continue past Nujiang River Bridge to Baxoi for an overnight stay.

Tips:
1. Speed is strictly regulated in Tibet — patience and flexibility are appreciated.
2. This will be a long travel day — pack snacks and water.

+Day 6: Baxoi – Ranwu Lake – Midui Glacier – Bomi

Altitude: 3,260m → 3,850m → 2,750m
Highlights: Anjula Mountain Pass, Ranwu Lake, Midui Glacier
Distance: 230 km / ~6 hrs
Accommodation: Twin room in Bomi
Meals: Breakfast included

Details:
Depart Baxoi in the morning and cross Anjula Mountain Pass (4,468m), enjoying stunning views of snow-capped peaks, forests, and farmlands.
Visit Ranwu Lake, a famous glacial lake on the Sichuan–Tibet Highway known for its calm reflections and alpine scenery.
Continue to Midui Glacier, one of the most beautiful maritime glaciers in Tibet, formed by two massive ice waterfalls. The surrounding landscapes — flower-filled meadows, dense forests, and traditional Tibetan villages — are surreal.

Arrive in Bomi for overnight stay.

+ Day 7: Bomi – Guxiang Lake – Lulang – Tibetan Village Experience – Sejila Mountain – Nyingchi

Altitude: 2,750m → 3,700m → 2,900m
Highlights: Guxiang Lake, Lulang Town, Tibetan culture experience, Mt. Namcha Barwa
Distance: 235 km / ~6 hrs
Accommodation: Twin room in Nyingchi
Meals: Breakfast & lunch included (Lulang Stone Pot Chicken)

Details:
Head out after breakfast and stop by the serene Guxiang Lake, a perfect photo spot framed by snowy mountains and soft sand shores.
Pass through Tongmai, once known as a treacherous section of the highway, now made safe by modern bridges.
Arrive at Lulang, a picturesque town with alpine meadows, snow peaks, Tibetan architecture, and grazing yaks.
Enjoy a local specialty: Lulang Stone Pot Chicken, made with Tibetan herbs and free-range chicken.
Visit a Tibetan village where you’ll be welcomed with a hada, then try butter tea, tsampa-making, and optional horse riding or archery in full traditional dress.
Cross the misty Sejila Mountain Pass (4,702m) for views of the majestic Mt. Namcha Barwa, then descend into the lush Niyang River Valley and arrive in Nyingchi.

+ Day 8: Nyingchi – Basum Tso – Lhasa

Altitude: 2,900m → 3,480m → 3,650m
Highlights: Basum Tso Lake, scenic drive to Lhasa
Distance: 460 km / ~6.5 hrs
Accommodation: 4-star hotel in Lhasa
Meals: Breakfast included

Details:
Travel along the scenic Niyang River and the high-standard Linzhi–Lhasa Highway.
Make a detour to visit Basum Tso Lake, a turquoise alpine lake shaped like a crescent moon and surrounded by snow peaks.
Take a boat or bridge to the central island and explore Tsodzong Monastery, a 1,500-year-old Nyingma temple.
After your visit, continue to Lhasa, the spiritual heart of Tibet.

+ Day 9: Potala Palace – Jokhang Temple – Barkhor Street

Altitude: 3,650m
Highlights: Potala Palace, Jokhang Temple, Barkhor Street
Distance: ~20 km / ~0.5 hrs driving
Accommodation: 4-star hotel in Lhasa
Meals: Breakfast included

Details:
In the morning, visit the iconic Potala Palace, the highest ancient palace in the world and former residence of the Dalai Lama.
Then explore the Jokhang Temple, the spiritual center of Tibetan Buddhism, dating back over 1,300 years.
Stroll through Barkhor Street, the bustling pilgrimage route and market encircling the temple. This sacred path is walked clockwise by local devotees.
Enjoy some free time to shop for souvenirs and soak up the spiritual atmosphere.

+ Day 10: Departure from Lhasa

Altitude: 3,650m → 3,600m
Highlights: Free time in Lhasa, airport transfer
Distance: ~60 km / ~1.5 hrs
Accommodation: Not included
Meals: Breakfast included

Details:
After breakfast, enjoy free time in Lhasa if your flight is later in the day. Store luggage at the hotel and explore nearby sights or shop around Barkhor Street.
We’ll arrange your airport transfer to Lhasa Gonggar International Airport. Please be ready at least 2.5 hours before your flight.
Your 10-day journey ends here — we wish you a safe flight home!
Reminder:
Please provide your flight details at least 2 days in advance to ensure timely airport pickup.

    Tour 'Wow' Points

    Potala Palace

    The iconic Potala Palace is Tibet’s most famous landmark, standing majestically over Lhasa. Once the winter residence of the Dalai Lama, this grand palace is a masterpiece of Tibetan architecture, rising over 13 stories with over 1,000 rooms. Inside, visitors can explore lavishly decorated halls, golden tombs of past Dalai Lamas, and an incredible collection of Buddhist art and scriptures. The palace also offers stunning views of Lhasa and the surrounding mountains, making it a truly unforgettable experience.

    Jokhang Temple

    Jokhang Temple is the spiritual heart of Tibetan Buddhism and a must-visit site in Lhasa. Built over 1,300 years ago, this UNESCO World Heritage Site is home to a revered statue of Jowo Shakyamuni, believed to be the most sacred image of Buddha in Tibet. Pilgrims from all over the region walk in circles around the temple, spinning prayer wheels and chanting mantras. Inside, visitors can explore beautifully decorated halls filled with golden statues, ancient murals, and the deep scent of burning incense.

    Litang Letong Ancient Town

    Nestled high on the Tibetan Plateau, Letong Ancient Town in Litang is one of the oldest and highest towns in the world, often referred to as the “Town in the Sky.” With its deep Tibetan Buddhist roots, centuries-old architecture, and majestic mountain backdrop, the town offers a rare glimpse into traditional Tibetan life and spirituality.

    Cuopu Valley (Cuopugou)

    Tucked away in the wilds of Western Sichuan, Cuopu Valley is a hidden gem known for its alpine lakes, virgin forests, snow-capped peaks, and diverse wildlife. It’s an ideal destination for nature lovers and hikers seeking untouched Tibetan landscapes and the serenity of high-altitude beauty.

    Midui Glacier

    Midui Glacier is one of the most beautiful glaciers in China, combining snow-covered peaks, lush forests, and ancient Tibetan villages. Located at the junction of oceanic and continental glaciers, it’s both accessible and photogenic—perfect for travelers eager to witness the power and grace of nature up close.

    Ranwu Lake (Ranwu Tso)

    Ranwu Lake is a breathtaking alpine lake surrounded by glaciers and snow mountains. Known for its crystal-clear water that mirrors the sky and peaks, it's a peaceful stop along the Sichuan-Tibet Highway. The changing colors of the lake throughout the day create a surreal, dreamlike atmosphere.

    Mount Namcha Barwa

    Often called “The Father of Tibetan Mountains,” Mount Namcha Barwa is one of the most mysterious and revered peaks in Tibet. Standing at over 7,700 meters, it’s rarely climbed and frequently shrouded in clouds. Its remote location and legendary status make it a bucket-list destination for adventurers and photographers alike.

    The 72 Turns of the Nujiang River

    This thrilling stretch of winding road features 72 sharp switchbacks descending along the cliffs of the Nujiang River Gorge. It’s one of the most dramatic and heart-pounding drives in Tibet—offering jaw-dropping views of valleys, rivers, and towering peaks.

    Required Documents for Travel to Tibet:

    1. Original Passport

    2. Chinese Tourist Visa

    3. Tibet Travel Permit – Processed by the travel agency in advance and sent to the customer via international courier. Registration required at least one month in advance.

    4. Alien Travel Permit – Arranged by the travel agency upon arrival in Lhasa.

    5. Military Permit – Issued by the travel agency.

    6. Border Permit – Issued by the travel agency.

    Visa Types & Additional Requirements:

    • Tourist Visa (L Visa): Passport + Chinese Visa

    • Family Visit Visa (Q Visa): Passport + Chinese Visa + Invitation Letter

    • Work Visa (Z Visa): Passport + Chinese Visa + Work Certification

    • Student Visa (X Visa): Passport + Chinese Visa + Proof of Enrollment

    Price Includes & Price Excludes

    Price Includes Price Excludes
    • Tickets (Entrance Fees):

      • Includes admission to: Letong Ancient Town + sightseeing bus, Cuopu Valley, Midui Glacier, Basum Lake, Potala Palace, Jokhang Temple

    • Single Room Supplement:

      • Price based on double occupancy

      • Solo travelers will be matched with same-gender guests if possible; otherwise, a single supplement applies

      • Please notify us during booking if you prefer not to share a room

      • Supplement may vary depending on tour batch—please check “Departure Batches” or ask our staff

      • Transportation:

        • Small VIP groups of 2–10 people

        • Vehicles arranged based on group size, ensuring one seat per person:

          • 2 pax: 5-seat SUV

          • 3–4 pax: 7-seat business van

          • 5–6 pax: 9-seat business van

          • 7–10 pax: 12–17-seat van

        • Includes: driver, fuel, tolls, parking, and transport from departure to attractions and hotels

        • Excludes: transportation during evening free time

        • Note: Luggage capacity is limited. Please bring a suitcase 24 inches or smaller

      • Meals:

        • Lunch and dinner (except Day 7 specialty meal) are self-paid

        • Breakfasts are complimentary and non-refundable if not used

        • You may dine independently or join others (AA-style)

        • Tour leader meals are included in tour price

      • Accommodation:

        • 9 nights’ accommodation in standard twin rooms by default

        • Solo travelers may share with same-gender guests or pay a single supplement

        • For triple rooms or double beds, please request in advance (especially during holidays or peak periods; if not pre-requested, we may not be able to accommodate)

        • During holidays or large events, hotel costs are non-refundable if guests cancel or change the itinerary

        • For regular days, refunds = room fee - 30% deposit if changes are made (excluding force majeure)

      • Optional or Mandatory Fees:

        • Honghaizi Cleaning Fee: 10–20 CNY/person (optional)

        • Cuopu Valley sightseeing bus: 90 CNY/person (mandatory)

        • Midui Glacier sightseeing bus: 36 CNY/person (mandatory)

        • Basum Lake sightseeing bus: 50 CNY/person (mandatory)

        • Basum Lake boat ride: 180 CNY/person (optional)

      • Special Experiences:

        1. Tibetan costume photography: 3 edited photos + 5 raw images

        2. Tibetan cultural immersion: Khata ceremony, butter tea tasting, Tsampa making

        3. Free Tibet Entry Permit application

        4. One bottled water per day + Tibetan blessing prayer flags

        5. Travel comfort kit: neck pillow, rain poncho (2-piece), portable oxygen

      • Child Car Seats:

        • Children under 5 years old or under 18kg must use a car seat by law

        • Guests must bring their own seats

        • Due to space limitations, an extra vehicle seat fee is required if bringing one

      • Tour Leader:

        • Experienced bilingual guide (Chinese & English), with meals, tickets, and accommodation included

      Long-Distance Travel:

      • Transportation to Chengdu (start) and return from Lhasa (end)

      • Insurance & Service:

        • Travel accident insurance with coverage up to 800,000 CNY (Note:

          • Children under 10: max coverage reduced to 200,000

          • Ages 10–17: max coverage 500,000)

        • Airport pick-up in Chengdu (Shuangliu Airport) for 2+ guests(Chengdu Tianfu International Airport is located far from the city center. Pick-up service from this airport requires an additional fee. Please contact customer service for details.)

        • Airport drop-off in Lhasa (Gonggar Airport) for 2+ guests

        • Vehicle equipped with thermos and Rhodiola tea for altitude sickness

        • First aid kit, oximeter, thermometer to monitor guest health

        ⚠ Note: Taiwanese and non-Chinese passport holders can only claim insurance within Mainland China Tier-3 or higher hospitals. Overseas treatments are not reimbursed.
        Complimentary services are non-refundable and non-transferable if declined or missed.

      • 💰 Tips:

        • Suggested tip: 7 USD per person per day total (for both guide & driver), only if you’re satisfied with the service

        🧾 Other Costs:

        • Personal expenses

        • Invoice not included in quoted price

        ⚠ Force Majeure Disclaimer:
        We are not liable for changes or cancellations due to force majeure, including landslides, natural disasters, road closures, flight delays, government policy changes, wars, epidemics, traffic control, etc.


      🧾Q&A – Regarding Travel

      1. How do I get to the hotel from Tianfu Airport?

      Chengdu Tianfu International Airport is about a 1-hour drive from downtown.

      • A taxi costs approximately CNY 180.

      • You may also take Metro Line 18 and transfer to other lines to reach the hotel (about 1 hour 20 minutes in total).

      • If you would like us to arrange a pickup service, please send your flight number at least 2 days in advance. (Additional fees apply; please consult customer service for details.)


      2. What clothes should I pack for this trip?

      The temperature difference between day and night on the Sichuan–Tibet route is significant.

      • April to June: Wear winter clothing (recommended: fleece-lined windbreaker or down jacket).

      • July to September: Wear spring/autumn clothing and prepare some warmer layers.

      • October to November: Wear winter clothing.

      • A hat and sunscreen are also essential.

      • We suggest bringing a small daypack for personal essentials.


      3. Will I experience altitude sickness on this trip?

      Most people coming from low-altitude regions may experience mild altitude symptoms such as headache or ringing in the ears.

      • These usually ease after 1–2 days.

      • To adapt better: move slowly, avoid running or jumping, and maintain a calm attitude.

      • Each vehicle is equipped with a bottle of oxygen per guest.

      • If you feel discomfort, you can use the oxygen provided.


      4. Will there be shopping stops on this tour?

      No. Our tours are strictly non-shopping and pure leisure travel.

      • If you'd like to buy local products or souvenirs, please evaluate quality carefully.

      • We are not responsible for any issues arising from personal purchases.


      5. Do I need a Tibet Entry Permit?

      Yes. We will assist all travelers with the Tibet Travel Permit free of charge.

      • You must provide a valid passport (at least 6 months before expiry) and a Chinese visa.

      • If you're traveling on a non-tourist visa, additional documentation may be required.

      🧾Travel Notice

      About Registration:

      Most attractions in the Sichuan-Tibet Route, Sichuan, Lhasa, and the Gansu-Qinghai Loop are located at altitudes above 3,000 meters. Rapid ascension to such elevations can cause varying degrees of altitude sickness.

      To cope with altitude sickness, it is important to stay calm and relaxed. Most people adapt within 1-2 days and the symptoms subside. Individuals with hypertension, heart disease, blood diseases, vascular conditions, asthma, or other altitude-related health issues should be cautious when entering high-altitude areas. Healthy individuals should avoid catching a cold before entering high altitudes and stay warm once there. Avoid jumping or running to prevent oxygen deprivation and altitude sickness.

      (1) Symptoms of Altitude Sickness
      Consider it altitude sickness if you experience any of the following: headache, dizziness, nausea, vomiting, palpitations, shortness of breath, chest tightness or pain, insomnia, drowsiness, loss of appetite, bloating, numbness in hands and feet, bluish lips or fingers, swollen eyelids or face, etc.

      (2) How to Prevent
      After arriving at high altitudes, keep warm and avoid bathing for 1-2 days. Drink plenty of hot water. You may also start taking Rhodiola (as tea) about a week before departure.

      (3) How to Treat
      Do not panic. Treat symptoms based on severity. Mild cases usually adapt in 1-2 days with rest, hydration, and warmth. For moderate symptoms, seek oxygen therapy at a local hospital. If the condition worsens, consult a doctor and descend to lower altitudes promptly.

      Eligibility for Registration:

      1. Participants must be aged between 6 and 65. (Minors aged 6–18 must be accompanied by a guardian. Participants aged 65–69 must be accompanied by immediate family and sign a waiver.)

      2. Applicants must be in good health with no acute or chronic illnesses. Participants should be physically fit, mentally healthy, and capable of team cooperation.

      3. Individuals with contagious diseases, cardiovascular or cerebrovascular conditions, respiratory issues, mental disorders, severe anemia, those in recovery from major surgeries, those with limited mobility, and pregnant women are not eligible.

      4. Registrants and those registering on behalf of others must truthfully disclose health conditions. Concealing medical issues is the registrant’s responsibility.

      About Withdrawal Mid-Tour:

      If a participant withdraws due to personal reasons (e.g., fatigue, illness, personal matters), a withdrawal agreement must be signed. Refunds will be minimal and only for unused meals and entrance tickets. Additional expenses are borne by the individual.

      Rules and Conduct:

      1. Be punctual. Wake up, gather, and depart at the designated times. A delay of more than 10 minutes will be considered voluntary withdrawal. Refunds will be issued per the refund policy for unused services. Safety and expenses post-withdrawal are the participant’s responsibility.

      2. Participants who frequently complain or spread negativity may be asked to leave the group. The driver has the right to enforce this if the behavior affects group morale.

      Accommodation Notes:

      1. Due to limited infrastructure in high-altitude regions, most hotels are not rated. Conditions may be basic. Bringing a sleeping bag liner is recommended. Disposable slippers, hairdryers, and toiletries may not be available.

      2. Water and electricity shortages are common. Prepare flashlights and power banks. Bathing may be impossible during outages; bring wet wipes.

      3. Water temperature may be unstable. Test it before showering to avoid burns or colds.

      4. Most hotels have no air conditioning. Electric blankets are usually provided.

      5. Hotel staff may lack professional service awareness. Some hotels may lack elevators or luggage assistance.

      6. If you have specific lodging requirements (e.g., single room, queen bed, triple room), notify us during registration. Changes may not be possible afterward.

      7. Accommodation changes require group consensus. Original bookings are non-refundable. Extra costs are shared by the participant(s).

      Meals:

      Except for meals explicitly included, other meals are not provided. Participants may dine together (AA) or separately. Cuisine varies: Sichuan and Tibet focus on Sichuan-style dishes; the northwest emphasizes lamb and regional flavors. Participants with special preferences can bring their own snacks or dishes.

      Transportation:

      1. Vehicle Breakdown: Long mountainous journeys increase the likelihood of mechanical issues. If breakdowns occur, please be patient. If repairs exceed 2 hours, alternative local transport (e.g., minivans) will be arranged at the company’s expense.

      2. Traffic Jams: Mudslides, rockfalls, and road closures are common during the rainy season. Be prepared with food and warm clothing.

      3. Night Driving: Generally avoided but may be necessary due to conditions. Refusing night travel may incur additional lodging costs.

      4. Unexpected Costs: Prepaid lodging is non-refundable if unoccupied due to delays. New lodging costs are borne by participants. Driver's lodging will be covered by the company.

      5. Long Rides: Bring snacks, water, and games (e.g., cards) to ease the journey.

      Shopping:

      1. No shopping is arranged in this tour; drivers will not recommend shops.

      2. If you decide to shop during free time, do not bargain unless you intend to buy.

      3. Drivers are not allowed to accept product requests or make purchases on behalf of participants.

      4. Vendors at scenic areas are not affiliated with our company. Keep receipts for your purchases. Be vigilant with your belongings. Avoid shopping in crowded or isolated areas.

      Communication:

      1. Mobile signal may be inconsistent in mountainous or desert areas. However, coverage has improved significantly.

      2. China Mobile generally offers the best signal, followed by China Telecom. China Unicom is the least reliable.

      3. Wi-Fi is unstable in about 60% of mountain areas. Prepare accordingly.

      Insurance:

      1. The tour includes travel accident insurance but does not cover illnesses or personal belongings. Please ensure you are medically fit for high-altitude travel.

      2. Take care of personal valuables. Losses are the participant’s responsibility.

      3. If property is lost and you choose to stay behind, a withdrawal agreement must be signed. Refunds only cover unused lodging and tickets. Transport, guide, and insurance fees are non-refundable.

      Force Majeure:

      Organizers reserve the right to adjust the itinerary due to weather, flights, traffic, seasons, or crowds, without reducing major attractions. In case of force majeure (e.g., landslides, heavy rain), no compensation will be provided. Alternative attractions may be arranged; costs will be adjusted accordingly.

      Toilets:

      1. Toilets outside hotels and gas stations often charge 1-3 RMB. Be prepared to pay.

      2. In areas without toilets, bring an umbrella or similar cover and avoid culturally sensitive areas like rivers, mani piles, stupas, etc.

      Respecting Local Culture & Regulations:

      1. Respect local customs, beliefs, and religious practices. Follow local photography rules. Do not photograph military areas, bridges, private homes, temples, or religious events. Unauthorized photography may lead to confiscation of equipment or legal issues.

      2. Locals and animals (e.g., yaks) may charge for photos. Always ask for permission or be ready to pay. Do not photograph if refused.

      3. Photography results depend on weather and lighting. The group will not extend time at locations for photo opportunities.

      Complaints:

      If you have service concerns, please report them to the tour leader or our company promptly. Complaints are only valid if submitted via the "Tour Satisfaction Survey" or "Service Quality Feedback Form" during the tour. Please fill them out carefully.


      🧾
      Activity Gear Checklist

      Category 1: Identification Documents

      • (1) ID Card: Mandatory for Chinese citizens. If you don’t have a valid ID, please obtain a temporary one immediately. Without it, you will not be able to travel.

      • (2) Hong Kong, Macau, and Foreign Nationals: Bring your Home Return Permit (for HK/Macau residents) or a valid passport.

      • (3) Discount Eligibility Documents: Tour guide license, student ID, military ID, soldier ID, disability certificate, senior citizen card, etc.

      ⚠️ Note: IDs must be shown at both accommodations and tourist attractions. Please cooperate with the travel agency. If you cannot check in due to lack of valid ID, you will be solely responsible for any resulting losses.


      Category 2: Clothing, Footwear & Accessories

      (Note: Large temperature fluctuations between morning and evening)

      • (1) High-Altitude Clothing:

        • Oct–May: Mainly winter gear such as down jackets, windbreakers, and fleece layers.

        • Jun–Sep: Autumn wear like T-shirts and fleece jackets.

      • (2) Gobi Desert Clothing (Jun–Oct): Summer wear such as T-shirts, sun-protective clothing, and light jackets.

      • (3) Footwear: Non-slip, waterproof, and warm high-ankle hiking boots are recommended. You may also bring trekking or sports shoes.

        🚫 High heels are not allowed.

      • (4) Accessories: Sunhat, gloves, headscarf/bandana.


      Category 3: Daily Essentials

      • (1) Toiletries & Skincare:

        • Toothbrush, toothpaste, facial cleanser, toner, moisturizer, SPF50+ sunscreen (a must), lip balm, hand cream.

      • (2) Utility Items: Sunglasses, thermos, rain gear (umbrella and raincoat), flashlight or headlamp.


      Category 4: Medications

      It is recommended to carry:

      • Cold medicine, gastrointestinal medicine, motion sickness pills, vitamins, band-aids, essential balm.

      • Oral glucose solution (for quick energy), ibuprofen (for headaches), glucose tablets.

      ⚠️ Note: Effects vary by individual. The list includes only over-the-counter (OTC) suggestions. If you require prescription medication, please consult a doctor and bring your own as needed.


      Category 5: Food & Snacks

      You may bring drinking water, dry food, fruit, and snacks for convenience.

      • (1) Energy: Chocolate, Snickers, beef jerky, etc.

      • (2) Filling: Biscuits, bread, etc.

      • (3) Fruits: Oranges, apples, pears, etc.


      Category 6: Photography & Communication

      • (1) Communication Devices: Mobile phone (Best signal: China Telecom, followed by China Mobile, weakest is China Unicom) and related chargers.

      • (2) Photography Gear: DSLR, telephoto and wide-angle lenses, tripod, camera bag, rain cover, large-capacity memory cards, and chargers.

      • (3) Props: Long dresses, scarves, shawls, and other creative props for photos.


      Category 7: Bags & Luggage

      • (1) Large Bag: Each person is allowed one large backpack (under 70L) or one suitcase (24–26 inches or smaller).

      • (2) Daypack: A small backpack is essential for day trips—carry snacks, water, etc.

        🚫 Shoulder bags or handheld bags are not recommended.

      • (3) Waist Pouch: Suggested for storing valuables.


      Change & Cancellation Policies

      • If the cancellation request is submitted before 00:00, 10 days prior to departure, no cancellation fee will be charged.
      • If the cancellation request is submitted before 00:00, 7 days prior to departure, a 10% fee will be charged.
      • If the cancellation request is submitted before 00:00, 5 days prior to departure, a 30% fee will be charged.
      • If the cancellation request is submitted before 00:00, 3 days prior to departure, a 100% fee will be charged.
      • If a discount is applied, the fee will be calculated as a proportion of the pre-discount price. It will not exceed the amount actually paid.
      • *Partial refund not available
      • *If you need to change the date, please cancel the booking and then rebook.

      Attractions in Sichuan, China

      Huanglongxi Ancient Town Tours | People's Park Tours | Mt. Emei (Emeishan) Tours | Mt. Qingcheng (Qingcheng Shan) Tours | Dujiangyan Tours | Wuhou Temple Tours | Kuan Zhai Alley Tours | Leshan Giant Buddha Tours

      const TAG = 'spz-custom-revue-util'; const DEFAULT_DELAY_TIME = 100; class SpzCustomRevueUtil extends SPZ.BaseElement { constructor(element) { super(element); this.templates_ = SPZServices.templatesForDoc(); } buildCallback = () => { this.action_ = SPZServices.actionServiceForDoc(this.element); this.templates_ = SPZServices.templatesForDoc(this.element); this.xhr_ = SPZServices.xhrFor(this.win); } static deferredMount() { return false; } mountCallback() { } debounceRender(el, thisEl, containerStr) { return this.smoothRender_(el, thisEl, containerStr).then(() => this.attemptToFit_(thisEl)); } smoothRender_(newEl, thisEl, containerStr) { const that = this; that.appendAsUnvisibleContainer_(newEl, thisEl); const components = newEl.querySelectorAll('[layout]'); return Promise.race([ Promise.all( Array.prototype.map.call(components, (e) => SPZ.whenDefined(e).then(() => e.whenBuilt()) ) ), SPZServices.timerFor(that.win).promise(DEFAULT_DELAY_TIME), ]).then(() => { return containerStr !== 'form_' ? thisEl.mutateElement(() => that.quickReplace(thisEl, newEl)) : thisEl.mutateElement(() => that.quickReplaceForm(thisEl, newEl)); }); } quickReplace(thisEl, newEl) { thisEl.container_ && this.toggleVisible_(thisEl.container_); this.toggleVisible_(newEl, true); thisEl.container_ && SPZCore.Dom.removeElement(thisEl.container_); thisEl.container_ = newEl; }; quickReplaceForm(thisEl, newEl) { thisEl.form_ && this.toggleVisible_(thisEl.form_); this.toggleVisible_(newEl, true); const children = thisEl.form_.querySelector('*:not(template)'); children && SPZCore.Dom.removeElement(children); this.toggleVisible_(thisEl.form_, true); thisEl.form_.appendChild(newEl); }; appendAsUnvisibleContainer_(el, thisEl) { this.toggleVisible_(el); thisEl.element.appendChild(el); } attemptToFit_(thisEl) { const fitFunc = () => { thisEl.mutateElement(this.setElementHeight_.bind(thisEl)); }; const container = thisEl.container_ || thisEl.form_; if (container) { const children = container.querySelectorAll('*:not(template)'); const spzChildren = Array.prototype.filter .call(children, SPZUtils.isSpzElement) .filter((e) => !(e.isMount && e.isMount())); spzChildren .map((e) => SPZ.whenDefined(e).then(() => e.whenMounted())) .forEach((p) => p.then(() => fitFunc())); } return fitFunc(); } setElementHeight_() { const targetHeight = (this.container_ || this.form_)?./*OK*/ scrollHeight; const height = this.element./*OK*/ offsetHeight; if (height !== targetHeight) { SPZCore.Dom.setStyles(this.element, { height: `${targetHeight}px`, }); } } toggleVisible_(el, visible = false) { if (!visible) { el.classList.add('i-spzhtml-layout-fill'); SPZCore.Dom.setStyles(el, { 'z-index': -100000, 'opacity': 0, }); } else { el.classList.remove('i-spzhtml-layout-fill'); SPZCore.Dom.setStyles(el, { 'z-index': 'auto', 'opacity': 1, }); } } setMinWidth_() { const targetWidth = this.container_?./*OK*/ scrollWidth; const width = this.element./*OK*/ offsetWidth; if (width !== targetWidth) { SPZCore.Dom.setStyles(this.element, { 'min-width': `${targetWidth}px`, }); } } triggerEvent_ = (name, data) => { const event = SPZUtils.Event.create(this.win, `${TAG}.${name}`, data || {}); this.action_.trigger(this.element, name, event); } isLayoutSupported(layout) { return layout == SPZCore.Layout.CONTAINER; } } SPZ.defineElement(TAG, SpzCustomRevueUtil); const TAG = 'spz-custom-revue-render'; class SPZCustomRevueRender extends SPZ.BaseElement { constructor(element) { super(element); } static deferredMount() { return false; } buildCallback = () => { this.action_ = SPZServices.actionServiceForDoc(this.element); this.templates_ = SPZServices.templatesForDoc(this.element); this.xhr_ = SPZServices.xhrFor(this.win); } mountCallback = () => {} render = (data) => { return this.templates_ .findAndRenderTemplate(this.element, data, null) .then((el) => { if (this.element.children.length > 0) { this.element.children[0].style.display = 'none'; } this.element.appendChild(el); // const utilsEl = document.getElementById('spz_custom_revue_util'); // utilsEl && SPZ.whenApiDefined(utilsEl).then((api) => { // api.debounceRender(el, this); // }); }); } triggerEvent_(name, data) { const event = SPZUtils.Event.create(this.win, `${ TAG }.${ name }`, data || {}); this.action_.trigger(this.element, name, event); } isLayoutSupported(layout) { return layout == SPZCore.Layout.CONTAINER; } } SPZ.defineElement(TAG, SPZCustomRevueRender) const TAG = 'spz-custom-revue-star'; class SPZCustomRevueStar extends SPZ.BaseElement { constructor(element) { super(element); } static deferredMount() { return false; } buildCallback = () => { this.action_ = SPZServices.actionServiceForDoc(this.element); this.templates_ = SPZServices.templatesForDoc(this.element); this.xhr_ = SPZServices.xhrFor(this.win); this.starNum = this.element.getAttribute('starNum'); this.starTotal = this.element.getAttribute('starTotal'); this.showStarText = this.element.getAttribute('showStarText'); this.starColor = this.element.getAttribute('color'); this.interact = this.element.getAttribute('interact'); this.starSize = this.element.getAttribute('starSize') || 14; } mountCallback = () => { this.doRender_({ starTotal: this.starTotal, totalArray: Array.from({ length: Number(this.starTotal) }, (v, k) => k + 1), starNum: this.starNum, showStarText: this.showStarText, starColor: this.starColor, starSize: this.starSize }).then(() => { if (this.interact) { this.addEventListeners_(); } }); } addEventListeners_ = () => { const stars = document.querySelectorAll('.revue-star__star'); stars.forEach(star => { star.addEventListener('click', event => { const starEl = star.closest('.revue-star__star'); const starIndex = Number(starEl.dataset.index); let isHalf = event.offsetX < star.offsetWidth / 2; // rtl if (document.documentElement.getAttribute('dir') === 'rtl') { isHalf = event.offsetX > star.offsetWidth / 2; } const starValue = isHalf ? starIndex - 0.5 : starIndex; this.starClickHandler_({ value: starValue }); }); }); } renderStar = () => { const isRtl = document.documentElement.getAttribute('dir') === 'rtl'; const stars = this.element.querySelectorAll('.revue-star__star'); stars.forEach((star, i) => { const starIndex = i + 1; const starEl = star.querySelector('svg:nth-child(2)'); const isHalf = this.starNum % 1 > 0 && Math.ceil(this.starNum) === starIndex; const isSolid = starIndex <= Math.ceil(this.starNum); starEl.style.display = isSolid ? 'block' : 'none'; if (isHalf) { if (isRtl) { // RTL布局下,如果是半星,显示星星的右半边 starEl.style.clipPath = `polygon(50% 0, 100% 0, 100% 100%, 50% 100%)`; } else { // LTR布局下,如果是半星,显示星星的左半边 starEl.style.clipPath = `polygon(0 0, 50% 0, 50% 100%, 0 100%)`; } } else { starEl.style.clipPath = `polygon(0 0, 100% 0, 100% 100%, 0 100%)` } }); const showCountEle = this.element.querySelector('#revue-star-show-count'); showCountEle && SPZ.whenApiDefined(showCountEle).then((api) => { api.render({ starNum: this.starNum, starTotal: this.starTotal }); }); } doRender_ = (data) => { return this.templates_ .findAndRenderTemplate(this.element, { starSize: this.starSize, ...data }, null) .then((el) => { const children = this.element.querySelector('*:not(template)'); children && SPZCore.Dom.removeElement(children); this.element.appendChild(el); }) .then(() => { this.starNum = data.starNum; this.renderStar(); }); } starClickHandler_ = (event) => { this.starNum = event.value; this.renderStar(); this.triggerEvent_('change', { value: event.value }); } triggerEvent_(name, data) { const event = SPZUtils.Event.create(this.win, `${ TAG }.${ name }`, data || {}); this.action_.trigger(this.element, name, event); } isLayoutSupported(layout) { return layout == SPZCore.Layout.CONTAINER; } } SPZ.defineElement(TAG, SPZCustomRevueStar) const TAG = 'spz-custom-revue-progress'; class SPZCustomRevueProgress extends SPZ.BaseElement { constructor(element) { super(element); } static deferredMount() { return false; } buildCallback = () => { this.action_ = SPZServices.actionServiceForDoc(this.element); this.templates_ = SPZServices.templatesForDoc(this.element); this.xhr_ = SPZServices.xhrFor(this.win); this.isPC = window.innerWidth > (window.breakpoint || 960); this.height = '6px'; this.color = this.element.getAttribute('color') || '#000000'; this.show_percentage = 'false'; this.show_percentage_num = 100; this.count = this.element.getAttribute('count'); this.total = this.element.getAttribute('total'); } mountCallback = () => { this.doRender_({ count: Number(this.count), total: Number(this.total), height: this.height, color: this.color, show_percentage: this.show_percentage, show_percentage_num: this.show_percentage_num }).then(() => { }); } doRender_ = (data) => { return this.templates_ .findAndRenderTemplate(this.element, data, null) .then((el) => { const children = this.element.querySelector('*:not(template)'); children && SPZCore.Dom.removeElement(children); this.element.appendChild(el); }); } triggerEvent_(name, data) { const event = SPZUtils.Event.create(this.win, `${ TAG }.${ name }`, data || {}); this.action_.trigger(this.element, name, event); } isLayoutSupported(layout) { return layout == SPZCore.Layout.CONTAINER; } } SPZ.defineElement(TAG, SPZCustomRevueProgress) const TAG = 'spz-custom-revue-like'; class SPZCustomRevueLike extends SPZ.BaseElement { constructor(element) { super(element); } static deferredMount() { return false; } buildCallback = () => { this.action_ = SPZServices.actionServiceForDoc(this.element); this.templates_ = SPZServices.templatesForDoc(this.element); this.xhr_ = SPZServices.xhrFor(this.win); this.grayColor = this.element.getAttribute('gray_color') || "#BDBDBD"; this.likedColor = this.element.getAttribute('like_color') || "#FFCB44"; this.color = this.grayColor; this.count = this.element.getAttribute('count'); this.revueId = this.element.getAttribute('revue-id'); this.location = this.element.getAttribute('location'); } mountCallback = () => { const likes = sessionStorage.getItem('likes') ? JSON.parse(sessionStorage.getItem('likes')) : []; const like = likes.find(item => item.id === this.revueId); if (like) { this.color = like.like_status === 1 ? this.likedColor : this.grayColor; } // 如果location是modal,则找到相同revue-id的list的元素,拿到其count,存在list count变了,但是modal的count没变的情况 if (this.location === 'modal') { const listElement = document.querySelector(`spz-custom-revue-like[revue-id="${this.revueId}"] .revue-like-count`); if (listElement) { this.count = listElement.getAttribute('data-real-count'); } } this.doRender_({ color: this.color, count: this.count }).then(() => { this.addEventListeners_(); if(this.location === 'list') { // modal数量变更,list同步变更 document.addEventListener('like-clicked', (e) => { if (e.detail.location !== this.location && e.detail.id === this.revueId) { this.color = e.detail.like_status === 1 ? this.likedColor : this.grayColor; this.count = e.detail.count; this.element.querySelector('.revue-like__icon').querySelector('svg').setAttribute('fill', this.color); this.element.querySelector('.revue-like__icon').querySelector('svg').querySelector('path').setAttribute('fill', this.color); this.element.querySelector('.revue-like-count').innerText = this.count > 99 ? '99+' : this.count < 1 ? '' : this.count; this.element.querySelector('.revue-like-count').setAttribute('data-real-count', this.count); if(this.count > 0){ this.element.querySelector('.revue-like-count').classList.remove('hidden'); }else{ this.element.querySelector('.revue-like-count').classList.add('hidden'); } } }); } }); } addEventListeners_ = () => { const icon = this.element.querySelector('.revue-like__icon'); icon.addEventListener('click', (e) => { e.stopPropagation(); const likeStatus = this.color === this.likedColor ? 0 : 1; this.color = this.color === this.likedColor ? this.grayColor : this.likedColor; this.count = likeStatus === 1 ? parseInt(this.count) + 1 : parseInt(this.count) - 1; icon.querySelector('svg').setAttribute('fill', this.color); icon.querySelector('svg').querySelector('path').setAttribute('fill', this.color); this.element.querySelector('.revue-like-count').innerText = this.count > 99 ? '99+' : this.count < 1 ? '' : this.count; this.element.querySelector('.revue-like-count').setAttribute('data-real-count', this.count); if(this.count > 0){ this.element.querySelector('.revue-like-count').classList.remove('hidden'); }else{ this.element.querySelector('.revue-like-count').classList.add('hidden'); } this.postLike(likeStatus); if (this.location === 'modal') { const clickedEvent = new CustomEvent('like-clicked', { detail: { id: this.revueId, like_status: likeStatus, count: this.count, location: this.location } }); document.dispatchEvent(clickedEvent); } }); } setLikeToStorage = (likeToStore) => { if (typeof (Storage) !== 'function') return; const likesInStore = sessionStorage.getItem('likes') ? JSON.parse(sessionStorage.getItem('likes')) : []; const reviewIndex = likesInStore.findIndex(item => item.id === likeToStore.id); if (reviewIndex !== -1) { likesInStore[reviewIndex].like_status = likeToStore.like_status; likesInStore[reviewIndex].count = likeToStore.count; } else { likesInStore.push(likeToStore); } sessionStorage.setItem('likes', JSON.stringify(likesInStore)); } doRender_ = (data) => { return this.templates_ .findAndRenderTemplate(this.element, data, null) .then((el) => { const children = this.element.querySelector('*:not(template)'); children && SPZCore.Dom.removeElement(children); this.element.appendChild(el); }); } postLike = (likeStatus) => { fetch('/api/comment/like', { method: 'POST', headers: { 'Accept': 'application/json', 'Content-Type': 'application/json' }, body: JSON.stringify({ id: this.revueId, status: likeStatus }) }).then((res) => { if (res.status === 200) { this.setLikeToStorage({ id: this.revueId, like_status: likeStatus, count: this.count }); } }); } triggerEvent_(name, data) { const event = SPZUtils.Event.create(this.win, `${ TAG }.${ name }`, data || {}); this.action_.trigger(this.element, name, event); } isLayoutSupported(layout) { return layout == SPZCore.Layout.CONTAINER; } } SPZ.defineElement(TAG, SPZCustomRevueLike) const TAG = 'spz-custom-revue-media'; class SPZCustomRevueMedia extends SPZ.BaseElement { constructor(element) { super(element); } static deferredMount() { return false; } buildCallback = () => { this.action_ = SPZServices.actionServiceForDoc(this.element); this.templates_ = SPZServices.templatesForDoc(this.element); this.xhr_ = SPZServices.xhrFor(this.win); this.imgCover = this.element.getAttribute('img-cover') ?? false; this.pc_layout = this.element.getAttribute('pc-layout') ?? ''; // data-images 格式为 xxxx.png?width=1&height=1,xxxx.png?width=1&height=1 const images = this.element.getAttribute('data-images').split(',') || []; const parsedImages = images.map(image => { return this.mediaParse_(image); }); this.images = parsedImages; this.isPC = window.innerWidth > 960; } mountCallback = () => { this.doRender_({ images: this.images, isPC: this.isPC, imgCover: this.imgCover, pc_layout: this.pc_layout }).then(() => { this.addEventListeners_(); }); } addEventListeners_ = () => { const images = this.element.querySelectorAll('.revue-image-item'); images.forEach((image, index) => { image.addEventListener('click', () => { const carousel = document.querySelector('#revue-image-carousel-render'); carousel && SPZ.whenApiDefined(carousel).then((api) => { const width = this.isPC ? 460 : window.innerWidth * 0.9; const height = this.isPC ? 630 : 500; api.render({ images: this.images, index: index, width: width, height: height }); }); }); }); } doRender_ = (data) => { return this.templates_ .findAndRenderTemplate(this.element, data, null) .then((el) => { const children = this.element.querySelector('*:not(template)'); children && SPZCore.Dom.removeElement(children); this.element.appendChild(el); }); } mediaParse_ = function (url) { var result = {}; try { url.replace(/[?&]+([^=&]+)=([^&]*)/gi, function (str, key, value) { try { result[key] = decodeURIComponent(value); } catch (e) { result[key] = value; } }); result.preview_image = url.split('?')[0]; } catch (e) {}; return result; } triggerEvent_(name, data) { const event = SPZUtils.Event.create(this.win, `${ TAG }.${ name }`, data || {}); this.action_.trigger(this.element, name, event); } isLayoutSupported(layout) { return layout == SPZCore.Layout.CONTAINER; } } SPZ.defineElement(TAG, SPZCustomRevueMedia) const TAG = 'spz-custom-revue-sort'; class SPZCustomRevueSort extends SPZ.BaseElement { constructor(element) { super(element); } static deferredMount() { return false; } triggerEvent_(name, data) { const event = SPZUtils.Event.create(this.win, `${ TAG }.${ name }`, data || {}); this.action_.trigger(this.element, name, event); } isLayoutSupported(layout) { return layout == SPZCore.Layout.CONTAINER; } buildCallback = () => { this.action_ = SPZServices.actionServiceForDoc(this.element); this.templates_ = SPZServices.templatesForDoc(this.element); this.xhr_ = SPZServices.xhrFor(this.win); this.isPC = window.innerWidth > 960; this.width = this.isPC ? `${this.element.getAttribute('width') || 150}px` : '100%'; this.randomStr = Math.random().toString(36).substr(2); this.sectionId = this.element.getAttribute('section-id') || '1760579731641'; this.prefix = this.element.getAttribute('prefix'); } mountCallback = () => { const data = { width: this.width, randomStr: this.randomStr }; this.doRender_(data).then(() => { let revueSortListRender = this.isPC ? this.element.querySelector(`#${this.prefix}-revue-sort-list-render-${this.sectionId}`) : this.element.querySelector(`#${this.prefix}-revue-sort-dropdown-render-${this.sectionId}`); revueSortListRender && SPZ.whenApiDefined(revueSortListRender).then((api) => { api.render(data).then(() => { if (this.isPC) { this.addEventListenersForPC_(); } else { this.addEventListenersForMobile_(); } }); }); }); } doRender_ = (data) => { return this.templates_ .findAndRenderTemplate(this.element, data, null) .then((el) => { const children = this.element.querySelector('*:not(template)'); children && SPZCore.Dom.removeElement(children); this.element.appendChild(el); }); } addEventListenersForPC_ = () => { const revueSelectList = this.element.querySelector('.revue_select_list'); const revueSelectItem = this.element.querySelectorAll('.revue_select_item'); const revueSelectSortIcon = this.element.querySelector(`#${this.prefix}-revue_select_sort_icon-${this.sectionId}`); revueSelectItem.forEach(item => { item.addEventListener('click', () => { const sort = item.getAttribute('data-sort'); const direction = item.getAttribute('data-direction'); this.triggerEvent_('sort', { sort, direction }); this.element.querySelector('.revue_select_label').innerText = item.innerText; revueSelectList.classList.remove('revue_select_list_active'); const revueChecked = this.element.querySelector(`#${this.prefix}-revue_checked`); revueChecked && SPZCore.Dom.removeElement(revueChecked); const revueCheckedClone = revueChecked.cloneNode(true); item.appendChild(revueCheckedClone); const pcDropdownEle = document.querySelector(`#${this.prefix}-revue-sort-pc-dropdown-${this.sectionId}`); if (!revueSelectSortIcon.classList.contains('up_icon')) { return; } revueSelectSortIcon.classList.remove('up_icon'); SPZ.whenApiDefined(pcDropdownEle).then((api) => { api.close(); }); }); }); window.addEventListener('scroll', (e) => { if (!revueSelectSortIcon || !revueSelectSortIcon.classList.contains('up_icon')) { return; } revueSelectSortIcon.classList.remove('up_icon'); SPZ.whenApiDefined(pcDropdownEle).then((api) => { api.close(); }); }); } addEventListenersForMobile_ = () => { const revueSortDropdownRender = document.querySelector(`#${this.prefix}-revue-sort-dropdown-render-${this.sectionId}`); revueSortDropdownRender && SPZ.whenApiDefined(revueSortDropdownRender).then(async (api) => { await api.render(); const revueSortDropdownItem = document.querySelectorAll(`#${this.prefix}-revue-sort-dropdown-${this.sectionId} .revue_sort_dropdown_item`); revueSortDropdownItem.forEach(item => { item.addEventListener('click', () => { const sort = item.getAttribute('data-sort'); const direction = item.getAttribute('data-direction'); revueSortDropdownItem.forEach((_item)=>{_item.classList.remove('selected')}) item.classList.add('selected'); // 抛出事件 this.triggerEvent_('sort', { sort, direction }); // 移除revue_checked元素,复制一个新的到当前选中的元素 const revueChecked = document.querySelector(`#${this.prefix}-revue-sort-dropdown-${this.sectionId} #${this.prefix}-revue_checked`); revueChecked && SPZCore.Dom.removeElement(revueChecked); const revueCheckedClone = revueChecked.cloneNode(true); item.appendChild(revueCheckedClone); const mDropdownEle = document.querySelector(`#${this.prefix}-revue-sort-dropdown-${this.sectionId}`); SPZ.whenApiDefined(mDropdownEle).then((api) => { api.close(); }); }); }); }) } } SPZ.defineElement(TAG, SPZCustomRevueSort) const TAG = 'spz-custom-revue-type'; class SPZCustomRevueType extends SPZ.BaseElement { constructor(element) { super(element); } static deferredMount() { return false; } triggerEvent_(name, data) { const event = SPZUtils.Event.create(this.win, `${ TAG }.${ name }`, data || {}); this.action_.trigger(this.element, name, event); } isLayoutSupported(layout) { return layout == SPZCore.Layout.CONTAINER; } buildCallback = () => { this.action_ = SPZServices.actionServiceForDoc(this.element); this.templates_ = SPZServices.templatesForDoc(this.element); this.xhr_ = SPZServices.xhrFor(this.win); this.isPC = window.innerWidth > 960; this.width = this.isPC ? `${this.element.getAttribute('width') || 150}px` : '100%'; this.randomStr = Math.random().toString(36).substr(2); this.sectionId = this.element.getAttribute('section-id') || '1760579731641'; this.prefix = this.element.getAttribute('prefix'); } mountCallback = () => { } render = (data) => { const renderData = { ...data, width: this.width, randomStr: this.randomStr }; return this.templates_ .findAndRenderTemplate(this.element, renderData, null) .then((el) => { const children = this.element.querySelector('*:not(template)'); children && SPZCore.Dom.removeElement(children); this.element.appendChild(el); }).then(() => { let revueTypeListRender = this.isPC ? this.element.querySelector(`#${this.prefix}-revue-type-list-render-${this.sectionId}`) : this.element.querySelector(`#${this.prefix}-revue-type-dropdown-render-${this.sectionId}`); revueTypeListRender && SPZ.whenApiDefined(revueTypeListRender).then((api) => { api.render(renderData).then(() => { if (this.isPC) { this.addEventListenersForPC_(); } else { this.addEventListenersForMobile_(); } }); }); }); } addEventListenersForPC_ = () => { const revueSelectList = this.element.querySelector('.revue_select_list'); const revueSelectItem = this.element.querySelectorAll('.revue_select_item'); const revueSelectTypeIcon = this.element.querySelector(`#${this.prefix}-revue_select_type_icon-${this.sectionId}`); revueSelectItem.forEach(item => { item.addEventListener('click', () => { const type = item.getAttribute('data-type'); const direction = item.getAttribute('data-direction'); this.triggerEvent_('type', { type, direction }); this.element.querySelector('.revue_select_label').innerText = item.innerText; revueSelectList.classList.remove('revue_select_list_active'); const revueChecked = this.element.querySelector(`#${this.prefix}-revue_checked`); revueChecked && SPZCore.Dom.removeElement(revueChecked); const revueCheckedClone = revueChecked.cloneNode(true); item.appendChild(revueCheckedClone); if (!revueSelectTypeIcon.classList.contains('up_icon')) { return; } const pcDropdownEle = this.element.querySelector(`#${this.prefix}-revue-type-pc-dropdown-${this.sectionId}`); revueSelectTypeIcon.classList.remove('up_icon'); SPZ.whenApiDefined(pcDropdownEle).then((api) => { api.close(); }); }); }); window.addEventListener('scroll', (e) => { if (!revueSelectTypeIcon.classList.contains('up_icon')) { return; } revueSelectTypeIcon.classList.remove('up_icon'); SPZ.whenApiDefined(pcDropdownEle).then((api) => { api.close(); }); }); } addEventListenersForMobile_ = () => { const revueTypeDropdownItem = this.element.querySelectorAll(`#${this.prefix}-revue-type-dropdown-${this.sectionId} .revue_type_dropdown_item`); revueTypeDropdownItem.forEach(item => { item.addEventListener('click', () => { const type = item.getAttribute('data-type'); const direction = item.getAttribute('data-direction'); revueTypeDropdownItem.forEach((_item)=>{_item.classList.remove('selected')}) item.classList.add('selected'); // 抛出事件 this.triggerEvent_('type', { type, direction }); // 移除revue_checked元素,复制一个新的到当前选中的元素 const revueChecked = this.element.querySelector(`#${this.prefix}-revue-type-dropdown-${this.sectionId} #${this.prefix}-revue_checked`); revueChecked && SPZCore.Dom.removeElement(revueChecked); const revueCheckedClone = revueChecked.cloneNode(true); item.appendChild(revueCheckedClone); const mDropdownEle = this.element.querySelector(`#${this.prefix}-revue-type-dropdown-${this.sectionId}`); SPZ.whenApiDefined(mDropdownEle).then((api) => { api.close(); }); }); }); } } SPZ.defineElement(TAG, SPZCustomRevueType) const TAG = 'spz-custom-revue-pagination'; class SPZCustomRevuePagination extends SPZ.BaseElement { constructor(element) { super(element); } static deferredMount() { return false; } buildCallback = () => { this.action_ = SPZServices.actionServiceForDoc(this.element); this.templates_ = SPZServices.templatesForDoc(this.element); this.xhr_ = SPZServices.xhrFor(this.win); this.isPC = window.innerWidth > (window.breakpoint || 960); this.numItems = this.numItems(); this.pageSize = this.pageSize(); } mountCallback = () => { this.doRender_({ numPages: this.numPages(), pageNum: this.currentPageNumber(), useCallback: true }).then(() => { }); } currentPageNumber() { let pageNum = this.element.getAttribute('page-num'); if (pageNum) return parseInt(pageNum); } numPages() { return Math.ceil(this.numItems / this.pageSize); } numItems() { return parseInt(this.element.getAttribute('num-items')); } pageSize() { return parseInt(this.element.getAttribute('page-size')) || 10; } doRender_ = (data) => { return this.templates_ .findAndRenderTemplate(this.element, data, null) .then((el) => { const children = this.element.querySelector('*:not(template)'); children && SPZCore.Dom.removeElement(children); this.element.appendChild(el); }); } triggerEvent_(name, data) { const event = SPZUtils.Event.create(this.win, `${ TAG }.${ name }`, data || {}); this.action_.trigger(this.element, name, event); } isLayoutSupported(layout) { return layout == SPZCore.Layout.CONTAINER; } } SPZ.defineElement(TAG, SPZCustomRevuePagination) const TAG = 'spz-custom-revue-product'; class SpzCustomRevueProduct extends SPZ.BaseElement { constructor(element) { super(element); } static deferredMount() { return false; } triggerEvent_(name, data) { const event = SPZUtils.Event.create(this.win, `${ TAG }.${ name }`, data || {}); this.action_.trigger(this.element, name, event); } isLayoutSupported(layout) { return layout == SPZCore.Layout.CONTAINER; } buildCallback = () => { this.section_id = this.element.getAttribute('section-id'); this.action_ = SPZServices.actionServiceForDoc(this.element); this.templates_ = SPZServices.templatesForDoc(this.element); this.xhr_ = SPZServices.xhrFor(this.win); this.setupAction_(); const url = new URL(window.location.href); this.isPC = window.innerWidth > (window.breakpoint || 960); this.nodata = false; this.firstRender = true; this.commentConfig = {}; this.commentSummary = {}; this.commentList = {}; this.panelId = 'all'; this.sort = 'created_at'; this.direction = 'desc'; this.pageNum = 1; this.pageSize = +window.reviewProductSettings[this.section_id].page_limit; this.pc_layout = window.reviewProductSettings[this.section_id].pc_layout; this.star_least = +window.reviewProductSettings[this.section_id].star_least; this.only_media = window.reviewProductSettings[this.section_id].only_media; this.product_id = window.SHOPLAZZA.meta.page.resource_id; this.isProductPage = '1' == 1; this.isCollectionPage = '1' == 2; this.isCartPage = '1' == 13; this.review_insufficient = window.reviewProductSettings[this.section_id].review_insufficient; // 评论不足类型 this.mini_quantity = window.reviewProductSettings[this.section_id].mini_quantity; // 评论少于一定数量 this.actions = window.reviewProductSettings[this.section_id].actions; // 评论处理方式 this.only_media = window.reviewProductSettings[this.section_id].only_media; // 只显示有图片的评论 this.only_featured = window.reviewProductSettings[this.section_id].only_featured ?? false; // 只显示精选评论 this.display_product_link = window.reviewProductSettings[this.section_id].display_product_link ?? false; // 是否显示商品链接 this.m_loading_type = window.reviewProductSettings[this.section_id].m_loading_type; // 移动端加载方式 this.m_modal_page_limit = window.reviewProductSettings[this.section_id].m_modal_page_limit; // 移动端弹窗加载限制 this.hide_review_section = window.reviewProductSettings[this.section_id].hide_review_section; // 无数据是否隐藏评论组件 this.accent_color = window.reviewProductSettings[this.section_id].accent_color; // 主题色 } mountCallback = () => { this.templates_ .findAndRenderTemplate(this.element, { isPC: this.isPC }, null) .then((el) => { this.element.appendChild(el); this.renderPage(); }) } fetchCommentConfig_ = async () => { const response = await fetch('/api/comment-config'); return response.json(); } fetchCommentSummary_ = async(data) => { const response = await fetch(`/api/v1/comments/summary`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(data) }); return response.json(); } fetchCommentList_ = async(data) => { // const response = await fetch(`/api/comment/list?show_product=1&star_least=${data.star_least}&onlyimg=${data.onlyimg}&limit=${data.limit}&offset=${data.offset}&sort_by=${data.sort_by || 'created_at'}&product_id=${data.productId}&status=1&sort_direction=${data.sort_direction || 'desc'}&show_reply=${data.show_reply}`); const response = await fetch('/api/v1/comments', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(data) }); return response.json(); } fetchThemeConfig_ = async(themeId) => { const response = await fetch(`/api/comment/theme-config?theme_id=${themeId}`); return response.json(); } getCommentConfig = () => { return this.fetchCommentConfig_() } getCommentSummary = (data = {}) => { const fetchData = { star_least: this.star_least, product_ids: this.isProductPage ? 'c0b6a1d4-61f0-4fd7-b5b4-fbb00a7c03a8' : this.isCartPage ? '' : '', collection_id: this.isCollectionPage ? '' : '', filter_type: this.isProductPage ? 'product' : this.isCollectionPage ? 'collection' : 'store', fill_min_threshold: this.review_insufficient === 'less_than' ? this.mini_quantity : undefined, fill_strategy: this.actions === 'all_product' ? 'store' : '', only_media: this.only_media ? this.only_media : this.panelId !== 'all', only_featured: this.only_featured, ...data, } return this.fetchCommentSummary_(fetchData) } getCommentList = (data = {}) => { const fetchData = { show_product: true, filter_type: (this.isProductPage || this.isCartPage) ? 'product' : this.isCollectionPage ? 'collection' : 'store', star_least: this.star_least, show_reply: true, limit: this.pageSize, offset: (this.pageNum - 1) * this.pageSize, only_media: this.only_media ? this.only_media : this.panelId !== 'all', sort_by: this.sort, sort_direction: this.direction, product_ids: this.isProductPage ? 'c0b6a1d4-61f0-4fd7-b5b4-fbb00a7c03a8' : this.isCartPage ? '' : '', collection_id: this.isCollectionPage ? '' : '', only_featured: this.only_featured, fill_strategy: this.actions === 'all_product' ? 'store' : '', fill_min_threshold: this.review_insufficient === 'less_than' ? this.mini_quantity : undefined, ...data, } return this.fetchCommentList_(fetchData) } getPageData = () => { return Promise.all([ this.getCommentConfig(), this.getCommentSummary(), this.getCommentList() ]) } renderPage = async () => { const [commentConfigRes, commentSummaryRes, commentListRes] = await this.getPageData(); let commentConfigData = commentConfigRes.data || {}; let commentSummaryData = commentSummaryRes.data || {}; let commentListData = commentListRes.data || []; this.commentConfig = commentConfigData; this.commentSummary = commentSummaryData; this.commentList = commentListData; this.accent_color = this.accent_color || this.commentConfig.star_color; let lessThanCount = 1; // 评论不足逻辑 if(this.actions === "hide") { // 不展示评论组件 if(this.review_insufficient === 'less_than') { lessThanCount = this.mini_quantity; // 无评论或少于一定数量 } } else if(this.actions === "all_product") { lessThanCount = 1; } if(commentListData.count < lessThanCount) { // 是否隐藏评论组件 if(this.hide_review_section) { this.renderNoData(); return null; } else { this.renderRevueEmpty(); } this.nodata = true; } window.addEventListener('resize', SPZCore.Types.throttle(window, this.onResize, 300)); this.renderPageData([this.commentConfig, this.commentSummary, this.commentList]); } onResize = () => { if(this.nodata) { return; } // 判断是否需要重新渲染 if((this.isPC && window.innerWidth > (window.breakpoint || 960)) || (!this.isPC && window.innerWidth < (window.breakpoint || 960))) { return; } this.isPC = window.innerWidth > (window.breakpoint || 960); this.panelId = 'all'; this.sort = 'created_at'; this.direction = 'desc'; this.pageNum = 1; this.templates_ .findAndRenderTemplate(this.element, { isPC: this.isPC }, null) .then((el) => { const children = this.element.querySelector('*:not(template)'); children && SPZCore.Dom.removeElement(children); this.element.appendChild(el); this.renderPageData([this.commentConfig, this.commentSummary, this.commentList]); }) } renderPageData = (data) => { const [commentConfigData, commentSummaryData, commentListData] = data; // 渲染头部 this.renderHeader_({ starData: commentSummaryData, listData: commentListData, comment_avg_star: commentSummaryData.comment_avg_star, comment_count: commentSummaryData.comment_count, }); // 有评论逻辑 this.renderStarCounts(commentSummaryData); if(this.isPC && this.pc_layout === 'single_column') { this.renderCommentTab({ listData: commentListData, isPC: this.isPC, }, `revue-tab-${this.section_id}`); } else { this.renderList_({ listData: commentListData, config: this.commentConfig, shop_name: window.SHOPLAZZA.shop.shop_name, isPC: this.isPC, star_color: this.accent_color, }); } } renderNoData = () => { const sectionEle = document.querySelector(`#revue-product-compo`); if (sectionEle) { sectionEle.setAttribute('hidden', 'true'); } if(window.top === window.self) { // c端不渲染 return; } // b端渲染 const noDataPlaceholder = document.querySelector(`#revue_no_data_placeholder_${this.section_id}`); if(noDataPlaceholder) { SPZ.whenApiDefined(noDataPlaceholder).then(async (api) => { await api.render(); }); } } renderRevueEmpty = (section) => { const emptyEle = document.querySelector(`#revue_empty-${this.section_id}`); const writeReviewBtn = document.querySelector(`#revue_write_review_btn_single`); if (emptyEle) { emptyEle.classList.remove('hidden'); } if (writeReviewBtn) { writeReviewBtn.classList.remove('hidden'); } const skeletonEle = document.querySelector('#revue_skeleton'); if (skeletonEle) { skeletonEle.classList.add('hidden'); } } renderHeader_ = (data) => { const headerEle = document.querySelector(`#app-review-revue-header-${this.section_id}`); if (headerEle) { SPZ.whenApiDefined(headerEle).then(async (api) => { api.render({ ...data, star_color: this.accent_color, isPC: this.isPC, }); }); } } renderStarCounts = (data, eleId = `revue-summary-${this.section_id}`) => { const ndata = { ...this.commentSummary, star_color: this.accent_color, isPC: this.isPC, ...data, } const summaryEle = document.querySelector(`#${eleId}`); if (summaryEle) { SPZ.whenApiDefined(summaryEle).then((api) => { api.render({ ...ndata, }); }); } } renderCommentTab = (data, eleId) => { const elementId = eleId || `revue-tab-${this.section_id}`; const ndata = { listData: this.commentList, isPC: this.isPC, ...data } const tabEle = document.querySelector(`#${elementId}`); let listId; if (tabEle) { SPZ.whenApiDefined(tabEle).then(async (api) => { await api.render({ ...ndata, // suffix: "list", }); if(eleId) { listId = `revue-comment-list-${this.section_id}_tab`; } this.renderList_({ ...ndata, // suffix: "list", }, listId); }); } } renderList_ = (data, eleId) => { const listEle = document.querySelector(`#revue-comment-list`); if (listEle && !eleId) { SPZ.whenApiDefined(listEle).then(async (api) => { await api.render({ ...data, // suffix: "list", pageSize: this.pageSize, hasmore: data.listData.has_more, }) let nlist = data.listData.list.map(item => { return { ...item, config: this.commentConfig, star_color: this.accent_color, shop_name: window.SHOPLAZZA.shop.shop_name, current_panel: this.panelId, pageNum: this.pageNum, suffix: data.suffix, show_link: this.display_product_link, } }) let hasmore = data.listData.has_more; if(!this.isPC && this.m_loading_type === 'modal') { nlist = nlist.slice(0, this.m_modal_page_limit); hasmore = true; } api.renderList({ ...data, list: nlist, count: this.panelId === 'all' ? data.listData.count : data.listData.image_count, // suffix: "list", hasmore: hasmore, pageSize: this.pageSize }) }) return; } const viewallListEle = document.querySelector(`#${eleId}`); if (viewallListEle) { SPZ.whenApiDefined(viewallListEle).then(async (api) => { await api.render({ ...data, pageSize: this.pageSize, hasmore: data.listData.has_more, }); let nlist = data.listData.list.map(item => { return { ...item, config: this.commentConfig, star_color: this.accent_color, shop_name: window.SHOPLAZZA.shop.shop_name, current_panel: this.panelId, pageNum: this.pageNum, suffix: data.suffix, show_link: this.display_product_link, } }) api.renderList({ ...data, list: nlist, count: this.panelId === 'all' ? data.listData.count : data.listData.image_count, hasmore: data.listData.has_more, pageSize: this.pageSize, }) }); } } renderCommentList = (data, eleId = 'revue-comment-list', renderType = 'list', redo = false) => { const listEle = document.querySelector(`#${eleId}`); if (listEle) { SPZ.whenApiDefined(listEle).then((api) => { let nlist = data.listData.list.map(item => { return { ...item, config: this.commentConfig, star_color: this.accent_color, shop_name: window.SHOPLAZZA.shop.shop_name, current_panel: this.panelId, pageNum: this.pageNum, hasmore: data.listData.has_more, show_link: this.display_product_link, // suffix: data.suffix, } }) if(!this.isPC && this.m_loading_type === 'modal' && renderType === 'list') { nlist = nlist.slice(0, this.m_modal_page_limit); } api.renderList({ count: this.panelId === 'all' ? data.listData.count : data.listData.image_count, list: nlist, // suffix: "list", hasmore: data.listData.has_more, pageSize: this.pageSize }, redo); }); return; } } renderByScrollPagination = async (eleId, renderType) => { this.pageNum = this.pageNum + 1; const params = {} const res = await this.getCommentList(params); this.renderCommentList({ listData: res.data, }, eleId, renderType, false); } setupAction_ = () => { this.registerAction('renderTabChangeList', async (invocation) => { // 兼容 ljs-tab 首次加载会触发 tabchange 事件 if(this.firstRender) { this.firstRender = false; return; } const panelId = invocation.args.data.panelId; const { eleId, renderType } = invocation.args; this.panelId = panelId; this.pageNum = 1; this.modalHasMore = true; const params = { // only_media: panelId !== 'all', } const res = await this.getCommentList(params); this.renderCommentList({ listData: res.data, }, eleId, renderType, true); }); this.registerAction('renderTypeChangeList', async (invocation) => { const { type } = invocation.args.data; const { eleId, renderType } = invocation.args; this.panelId = type; this.pageNum = 1; this.modalHasMore = true; const params = { // only_media: type !== 'all', } const res = await this.getCommentList(params); this.renderCommentList({ listData: res.data, }, eleId, renderType, true); }); this.registerAction('renderSortedList', async(invocation) => { const { sort, direction } = invocation.args.data; const eleId = invocation.args.eleId; const renderType = invocation.args.renderType; this.sort = sort; this.direction = direction; this.pageNum = 1; this.modalHasMore = true; const params = { sort_by: sort, sort_direction: direction, } const res = await this.getCommentList(params); this.renderCommentList({ listData: res.data, }, eleId, renderType, true); }); this.registerAction('renderByPagination', async(invocation) => { const { pageNum, eleId, renderType } = invocation.args; this.pageNum = pageNum; const params = {} const res = await this.getCommentList(params); this.renderCommentList({ listData: res.data, }, `revue-comment-list-${this.section_id}_tab`, 'tab', true); const tabsEle = document.querySelector('#revue-product-compo'); if (tabsEle) { tabsEle.scrollIntoView({ behavior: 'smooth' }); } }); this.registerAction('renderByViewMore', async(invocation) => { const { eleId, renderType } = invocation.args; this.pageNum = this.pageNum + 1; const params = {} const res = await this.getCommentList(params); this.renderCommentList({ listData: res.data, }, eleId, renderType, false); }); this.registerAction('refresh', async(invocation) => { this.panelId = 'all'; this.sort = 'created_at'; this.direction = 'desc'; this.pageNum = 1; this.templates_ .findAndRenderTemplate(this.element, { isPC: this.isPC }, null) .then((el) => { const children = this.element.querySelector('*:not(template)'); children && SPZCore.Dom.removeElement(children); this.element.appendChild(el); this.renderPage(); }); const productEle = document.querySelector(`#revue-viewall-modal-comp`); if (productEle) { SPZ.whenApiDefined(productEle).then(async (api) => { api.refresh(); }); } }); } } SPZ.defineElement(TAG, SpzCustomRevueProduct) (function() { const TAG = 'spz-custom-new-revue'; class SpzCustomNewRevue extends SPZ.BaseElement { constructor(element) { super(element); this.config_ = null; this.loading_ = false; this.accent_color = this.element.getAttribute('accent-color'); this.sectionId = this.element.getAttribute('section-id'); this.prefix = this.element.getAttribute('prefix'); } buildCallback() { this.action_ = SPZServices.actionServiceForDoc(this.element); this.xhr_ = SPZServices.xhrFor(this.win); this.form_ = SPZCore.Dom.scopedQuerySelector( this.element, 'form' ); this.hasShowLengthInputs_ = SPZCore.Dom.scopedQuerySelectorAll( this.form_, '[showlength]' ); [...this.hasShowLengthInputs_].forEach(item => { const countRecordDom = SPZCore.Dom.scopedQuerySelector( this.form_, `#${item.id} ~ div[type="count-record"]` ); if (!countRecordDom) { console.error(`Cannot find count record DOM element for input ${item.id}`); return; } item.addEventListener('input', (e) => { countRecordDom.innerText = `${e.target.value.length}/3000`; }); }); this.setupAction_(); this.getRevueConfigData_(); } setupAction_() { this.registerAction('submitForm', async(invocation) => { if (this.loading_) { return; } this.loading_ = true; const formData = Object.entries(invocation.args.data).reduce((acc, [key, value]) => { if (key === 'star' || key === 'type') { acc[key] = Number(value[0]); } else { acc[key] = value[0]; } return acc; }, {}); try { const data = await fetch('/api/comment', { method: "post", headers: { "Content-Type": "application/json" }, body: JSON.stringify(formData) }).then(res => res.json()); if (data.state === 0) { this.triggerEvent_('submitSuccess', { panelId: 'with_photo', message: '' }); return; } throw new Error(data.msg); } catch(e) { e = await e; this.triggerEvent_('submitError', {data: e.message}); } finally { this.loading_ = false; } }); this.registerAction('renderFormStar', async(invocation) => { this.triggerEvent_('rerenderFormStar', { star_color: this.starColor_ }); }) } mountCallback() { } getRevueConfigData_ = () => { fetch('/api/comment-config') .then(res => res.json()) .then(data => { this.config_ = data.data; // anonymous_permission 是否支持匿名 if (!this.config_.anonymous_permission) { const anonymousInput = this.form_.querySelector(`#${this.prefix}-revue-anonymous-${this.sectionId}`); anonymousInput.value = 'false'; anonymousInput.parentNode.classList.add('hidden', 'anonymous-permission-hidden'); } this.starColor_ = this.config_.star_color; if(this.accent_color && this.accent_color != 'null'){ this.starColor_ = this.accent_color; } // render star // star_color 星星颜色 const starEl = this.form_.querySelector(`#${this.prefix}-revue_write_modal_star-${this.sectionId}`); if (starEl) { SPZ.whenApiDefined(starEl).then((api) => { api.render({ star_color: this.starColor_ }); }); } }); } triggerEvent_(name, data) { const event = SPZUtils.Event.create(this.win, `${ TAG }.${ name }`, data || {}); this.action_.trigger(this.element, name, event); } isLayoutSupported = (layout) => { return layout == SPZCore.Layout.CONTAINER; } } SPZ.defineElement(TAG, SpzCustomNewRevue); })() (function() { const TAG = 'spz-custom-revue-product-info-script'; class SpzCustomRevueProductInfoScript extends SPZ.BaseElement { constructor(element) { super(element); /** @private {!Element} */ this.product_id = null; } async buildCallback() { this.action_ = SPZServices.actionServiceForDoc(this.element); this.product_id = this.getProductId_(); this.triggerEvent_('init', { product_id: this.product_id }); try { const data = await this.getProductInfo_(); if (data?.data?.product) { this.triggerEvent_('finish', data.data.product); } } catch (error) { console.error('Failed to fetch product info:', error); // Handle the error appropriately } } getProductId_ = () => { return window.SHOPLAZZA.meta.page.resource_id; } async getProductInfo_() { if (!this.product_id) { console.error('Product ID is undefined or null'); return null; } try { const response = await fetch(`/api/products/${this.product_id}`); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } return await response.json(); } catch (error) { console.error('Error fetching product info:', error); throw error; // Rethrow to be caught by the caller } } triggerEvent_(name, data) { const event = SPZUtils.Event.create(this.win, `${ TAG }.${ name }`, data || {}); this.action_.trigger(this.element, name, event); } isLayoutSupported = (layout) => { return layout == SPZCore.Layout.LOGIC; } } SPZ.defineElement(TAG, SpzCustomRevueProductInfoScript); })() const TAG = 'spz-custom-revue-star'; class SPZCustomRevueStar extends SPZ.BaseElement { constructor(element) { super(element); } static deferredMount() { return false; } buildCallback = () => { this.action_ = SPZServices.actionServiceForDoc(this.element); this.templates_ = SPZServices.templatesForDoc(this.element); this.xhr_ = SPZServices.xhrFor(this.win); this.starNum = this.element.getAttribute('starNum'); this.starTotal = this.element.getAttribute('starTotal'); this.showStarText = this.element.getAttribute('showStarText'); this.starColor = this.element.getAttribute('color'); this.interact = this.element.getAttribute('interact'); this.starSize = this.element.getAttribute('starSize') || 14; } mountCallback = () => { this.doRender_({ starTotal: this.starTotal, totalArray: Array.from({ length: Number(this.starTotal) }, (v, k) => k + 1), starNum: this.starNum, showStarText: this.showStarText, starColor: this.starColor, starSize: this.starSize }).then(() => { if (this.interact) { this.addEventListeners_(); } }); } addEventListeners_ = () => { const stars = document.querySelectorAll('.revue-star__star'); stars.forEach(star => { star.addEventListener('click', event => { const starEl = star.closest('.revue-star__star'); const starIndex = Number(starEl.dataset.index); let isHalf = event.offsetX < star.offsetWidth / 2; // rtl if (document.documentElement.getAttribute('dir') === 'rtl') { isHalf = event.offsetX > star.offsetWidth / 2; } const starValue = isHalf ? starIndex - 0.5 : starIndex; this.starClickHandler_({ value: starValue }); }); }); } renderStar = () => { const isRtl = document.documentElement.getAttribute('dir') === 'rtl'; const stars = this.element.querySelectorAll('.revue-star__star'); stars.forEach((star, i) => { const starIndex = i + 1; const starEl = star.querySelector('svg:nth-child(2)'); const isHalf = this.starNum % 1 > 0 && Math.ceil(this.starNum) === starIndex; const isSolid = starIndex <= Math.ceil(this.starNum); starEl.style.display = isSolid ? 'block' : 'none'; if (isHalf) { if (isRtl) { // RTL布局下,如果是半星,显示星星的右半边 starEl.style.clipPath = `polygon(50% 0, 100% 0, 100% 100%, 50% 100%)`; } else { // LTR布局下,如果是半星,显示星星的左半边 starEl.style.clipPath = `polygon(0 0, 50% 0, 50% 100%, 0 100%)`; } } else { starEl.style.clipPath = `polygon(0 0, 100% 0, 100% 100%, 0 100%)` } }); const showCountEle = this.element.querySelector('#revue-star-show-count'); showCountEle && SPZ.whenApiDefined(showCountEle).then((api) => { api.render({ starNum: this.starNum, starTotal: this.starTotal }); }); } doRender_ = (data) => { return this.templates_ .findAndRenderTemplate(this.element, { starSize: this.starSize, ...data }, null) .then((el) => { const children = this.element.querySelector('*:not(template)'); children && SPZCore.Dom.removeElement(children); this.element.appendChild(el); }) .then(() => { this.starNum = data.starNum; this.renderStar(); }); } starClickHandler_ = (event) => { this.starNum = event.value; this.renderStar(); this.triggerEvent_('change', { value: event.value }); } triggerEvent_(name, data) { const event = SPZUtils.Event.create(this.win, `${ TAG }.${ name }`, data || {}); this.action_.trigger(this.element, name, event); } isLayoutSupported(layout) { return layout == SPZCore.Layout.CONTAINER; } } SPZ.defineElement(TAG, SPZCustomRevueStar) (function() { const TAG = 'spz-custom-new-revue-files-show'; class SpzCustomNewRevueFilesShow extends SPZ.BaseElement { constructor(element) { super(element); /** @private {!Element} */ this.files_ = [] } buildCallback() { this.action_ = SPZServices.actionServiceForDoc(this.element); this.templates_ = SPZServices.templatesForDoc(this.element); this.setupAction_(); this.element.setAttribute('nums', this.files_.length); } mountCallback() { } setupAction_() { this.registerAction('upload', async(invocation) => { const uploadFileList = invocation.args?.data || []; uploadFileList.forEach(file => { if(this.files_.some(item => item.url === file.url)) return this.files_.push(file); }) this.doRender_(); }); this.registerAction('delete', async(invocation) => { this.files_ = this.files_.filter((_, index) => index !== invocation.args.index); this.doRender_(); this.triggerEvent_('delete', { count: this.files_.length, files: this.files_ }); }); this.registerAction('preview', async(invocation) => { let previewFileData = this.files_[invocation.args.index]; if (previewFileData.type === 'video') { previewFileData = {...this.parseVideoSrc_(previewFileData.url), ...previewFileData}; } this.triggerEvent_('preview', previewFileData); }); this.registerAction('clear', async(invocation) => { this.files_ = []; this.doRender_(); }); } triggerEvent_(name, data) { const event = SPZUtils.Event.create(this.win, `${ TAG }.${ name }`, data || {}); this.action_.trigger(this.element, name, event); } parseVideoSrc_(src) { const url = new URL(src); const params = new URLSearchParams(url.search); return { videoUrl: url.origin + url.pathname, mediaType: params.get('media_type'), vID: params.get('vID'), mp4: params.get('mp4'), hls: params.get('hls') }; } doRender_ = () => { this.triggerEvent_('setInputValue', { data: this.files_ .map(file => { const url = file.type === 'video' ? file.poster : file.url; return `${url}?width=${file.width}&height=${file.height}`; }) .join(',') }); this.element.setAttribute('nums', this.files_.length); return this.templates_ .findAndRenderTemplate(this.element, { files: this.files_ }) .then((el) => { const children = this.element.querySelector('*:not(template)'); children && SPZCore.Dom.removeElement(children); this.element.appendChild(el); }) } isLayoutSupported = (layout) => { return layout == SPZCore.Layout.CONTAINER; } } SPZ.defineElement(TAG, SpzCustomNewRevueFilesShow); })() const TAG = 'spz-custom-revue-header'; class SPZCustomRevueHeader extends SPZ.BaseElement { constructor(element) { super(element); this.showCount = this.element.getAttribute('show-count'); } static deferredMount() { return false; } isLayoutSupported(layout) { return layout == SPZCore.Layout.CONTAINER; } buildCallback() { this.action_ = SPZServices.actionServiceForDoc(this.element); this.templates_ = SPZServices.templatesForDoc(this.element); this.xhr_ = SPZServices.xhrFor(this.win); this.showCount = this.element.getAttribute('show-count'); this.showSummary = this.element.getAttribute('show-summary'); this.showWriteReview = this.element.getAttribute('show-write-review'); this.showType = this.element.getAttribute('show-type') ; this.showSort = this.element.getAttribute('show-sort') ; this.sectionId = this.element.getAttribute('section-id'); this.viewall = this.element.getAttribute('viewall') ?? false; this.prefix = this.element.getAttribute('prefix'); } mountCallback() { } triggerEvent_(name, data) { const event = SPZUtils.Event.create(this.win, `${ TAG }.${ name }`, data || {}); this.action_.trigger(this.element, name, event); } render(data) { const ndata = { ...data, showCount: this.showCount, showSummary: this.showSummary, showWriteReview: this.showWriteReview, showType: this.showType, showSort: this.showSort, } if(this.viewall == 'review'){ ndata.viewall = false } return this.templates_ .findAndRenderTemplate(this.element, ndata, null, true) .then(({el}) => { const children = this.element.querySelector('*:not(template)'); children && SPZCore.Dom.removeElement(children); this.element.appendChild(el); }).then(() => { if(data && Object.keys(data).length > 0) { this.updateRender(data); this.setupSummaryContainerEffects_(data); } }); } updateRender(data) { this.renderStarCounts_(data); this.renderTypeSelect(data); this.renderSortSelect(data); } renderStarCounts_(data) { const renderData = { ...data.starData, star_color: data.star_color, isPC: data.isPC, } const summaryEle = data.isPC ? this.element.querySelector(`#${this.prefix}-revue-summary-${this.sectionId}_header_pc`) : this.element.querySelector(`#${this.prefix}-revue-summary-${this.sectionId}_header`); if(summaryEle) { SPZ.whenApiDefined(summaryEle).then((api) => { api.render(renderData); }); } } renderTypeSelect(data) { const typeSelect = this.element.querySelector(`#${this.prefix}-revue-header-type-${this.sectionId}`); if(typeSelect) { SPZ.whenApiDefined(typeSelect).then((api) => { api.render(data); api.registerAction('headerType_', (invocation) => { this.triggerEvent_('headerType', invocation.args.data); }); }); } } renderSortSelect(data) { const suffix = data.suffix || this.sectionId; const sortSelect = this.element.querySelector(`#${this.prefix}-revue-header-sort-${suffix}`); if(sortSelect) { SPZ.whenApiDefined(sortSelect).then((api) => { api.registerAction('headerSort_', (invocation) => { this.triggerEvent_('headerSort', invocation.args.data); }); }); } } setupSummaryContainerEffects_(data) { if(data.isPC) { this.setupSummaryContainerHover_(); } else { this.setupSummaryContainerTap_(); } } setupSummaryContainerHover_() { const summaryContainer = this.element.querySelector(`#revue-header-summary-container-${this.sectionId}`); const summaryEle = this.element.querySelector(`#${this.prefix}-revue-summary-${this.sectionId}_header_pc`); if (!summaryContainer || !summaryEle) return; let isHovering = false; // 鼠标移入容器时显示summary SPZUtils.Event.listen(summaryContainer, 'mouseenter', () => { isHovering = true; summaryEle.removeAttribute('hidden'); const selectIcon = summaryContainer.querySelector(`#revue-header-summary-icon-${this.sectionId}`); if(selectIcon) { selectIcon.classList.add('up-icon'); } }); // 鼠标移入summary时也保持显示 SPZUtils.Event.listen(summaryEle, 'mouseenter', () => { isHovering = true; }); // 鼠标移出容器时,检查是否还在summary上 SPZUtils.Event.listen(summaryContainer, 'mouseleave', () => { isHovering = false; setTimeout(() => { if (!isHovering) { summaryEle.setAttribute('hidden', 'true'); const selectIcon = summaryContainer.querySelector(`#revue-header-summary-icon-${this.sectionId}`); if(selectIcon) { selectIcon.classList.remove('up-icon'); } } }, 50); }); // 鼠标移出summary时,检查是否还在容器上 SPZUtils.Event.listen(summaryEle, 'mouseleave', () => { isHovering = false; setTimeout(() => { if (!isHovering) { summaryEle.setAttribute('hidden', 'true'); const selectIcon = summaryEle.querySelector(`#revue-header-summary-icon-${this.sectionId}`); if(selectIcon) { selectIcon.classList.remove('up-icon'); } } }, 50); }); } setupSummaryContainerTap_() { const selectIcon = this.element.querySelector(`#revue-header-summary-icon-${this.sectionId}`); const summaryEle = this.element.querySelector(`#${this.prefix}-revue-summary-${this.sectionId}_header`); if(!summaryEle) return; let isTapped = false; // 是否显示summary SPZ.whenApiDefined(summaryEle).then((api) => { api.registerAction('display', () => { if(isTapped) { isTapped = false; summaryEle.removeAttribute('hidden'); selectIcon.classList.add('up-icon'); } else { isTapped = true; summaryEle.setAttribute('hidden', 'true'); selectIcon.classList.remove('up-icon'); } }); }); } } SPZ.defineElement(TAG, SPZCustomRevueHeader); const TAG = 'spz-custom-revue-list'; class SPZCustomRevueList extends SPZ.BaseElement { constructor(element) { super(element); } static deferredMount() { return false; } triggerEvent_(name, data) { const event = SPZUtils.Event.create(this.win, `${ TAG }.${ name }`, data || {}); this.action_.trigger(this.element, name, event); } isLayoutSupported(layout) { return layout == SPZCore.Layout.CONTAINER; } buildCallback = () => { this.element_id = this.element.getAttribute('id'); this.section_id = this.element.getAttribute('section-id'); this.suffix = this.element.getAttribute('suffix'); this.action_ = SPZServices.actionServiceForDoc(this.element); this.templates_ = SPZServices.templatesForDoc(this.element); this.xhr_ = SPZServices.xhrFor(this.win); this.isPC = window.innerWidth > (window.breakpoint || 960); } mountCallback = () => { // this.render({}); this.setAction() } render = (data) => { const ndata = { ...data, pc_layout: window.reviewProductSettings[this.section_id].pc_layout, m_loading_type: window.reviewProductSettings[this.section_id].m_loading_type, container_id: this.element_id, suffix: this.suffix, isProductPage: this.isProductPage, } return this.templates_ .findAndRenderTemplate(this.element, ndata, null) .then((el) => { const children = this.element.querySelector('*:not(template)'); children && SPZCore.Dom.removeElement(children); this.element.appendChild(el); }).then(() => { this.triggerEvent_('finish', {}); this.setupIntersectionObserver(); }); } renderList = (data, redo = false) => { const listEle = document.querySelector(`#revue-list-${this.suffix}`); const viewMoreEle = document.querySelector(`#revue-list-view-more`); const loadingEle = document.querySelector(`#revue-list-scroll-loading`); const viewMoreModal = document.querySelector(`#revue-viewall-modal-comp`); const reachBottomEle = document.querySelector(`#revue-list-reach-bottom-${this.suffix}`); if(viewMoreModal) { SPZ.whenApiDefined(viewMoreModal).then((api) => { api.setMarkScrollTop() }) } if (listEle) { SPZ.whenApiDefined(listEle).then((api) => { api.listRender(data, redo); }); } if(viewMoreEle) { if(data.hasmore) { viewMoreEle.removeAttribute('hidden'); } else { viewMoreEle.setAttribute('hidden', true); } } if (loadingEle) { if(data.hasmore) { loadingEle.removeAttribute('hidden'); } else { loadingEle.setAttribute('hidden', true); } } if (reachBottomEle) { if(data.hasmore) { reachBottomEle.setAttribute('hidden', true); } else { reachBottomEle.removeAttribute('hidden'); } } } setupIntersectionObserver = () => { // 创建 Intersection Observer 实例 const observer = new IntersectionObserver((entries) => { entries.forEach(entry => { if (entry.isIntersecting) { const viewallModal = document.querySelector(`#revue-viewall-modal-comp`); if (viewallModal) { SPZ.whenApiDefined(viewallModal).then((api) => { api.loadMore(); }); } } }); }, { threshold: 0.1 // 当目标元素 10% 进入视区时触发 }); const loadingElement = document.querySelector('.revue-list-scroll-loading'); if (loadingElement) { observer.observe(loadingElement); } } setAction = () => { this.registerAction('checkOverFlow', () => { // 检查普通评论 this.element.querySelectorAll('.revue_text_line_4').forEach(elem => { if (elem.scrollHeight > elem.clientHeight + 10) { elem.classList.add('overflow-text'); } else { elem.classList.remove('overflow-text'); } }); // 检查回复内容 this.element.querySelectorAll('.revue_reply').forEach(elem => { const contentElem = elem.querySelector('.revue_reply_content'); if (contentElem.scrollHeight > contentElem.clientHeight + 10) { elem.classList.add('overflow-text'); } else { elem.classList.remove('overflow-text'); } }); }); } } SPZ.defineElement(TAG, SPZCustomRevueList); const TAG = 'spz-custom-revue-viewall-modal'; class SPZCustomRevueViewallModal extends SPZ.BaseElement { constructor(element) { super(element); } triggerEvent_(name, data) { const event = SPZUtils.Event.create(this.win, `${ TAG }.${ name }`, data || {}); this.action_.trigger(this.element, name, event); } isLayoutSupported(layout) { return layout == SPZCore.Layout.CONTAINER; } buildCallback = () => { this.section_id = this.element.getAttribute('section-id'); this.action_ = SPZServices.actionServiceForDoc(this.element); this.templates_ = SPZServices.templatesForDoc(this.element); this.firstRender = true; this.markScrollTop = 0; this.scrollTop = 0; } mountCallback = () => { this.doRender_(); this.setupAction_(); } doRender_() { this.templates_ .findAndRenderTemplate(this.element, {}) .then((el) => { const children = this.element.querySelector('*:not(template)'); children && SPZCore.Dom.removeElement(children); this.element.appendChild(el); }).then(() => { const viewallModalContentEle = document.querySelector(`#revue-viewall-modal-content-${this.section_id}`); viewallModalContentEle.addEventListener('scroll', () => { this.scrollTop = viewallModalContentEle.scrollTop; }); }) } setupAction_() { this.registerAction('renderTab', async (invocation) => { if(this.firstRender) { this.firstRender = false; const productEle = document.querySelector(`#revue-product-compo`); const summaryEle = document.querySelector(`#revue-summary-${this.section_id}_viewall`); if (productEle) { SPZ.whenApiDefined(productEle).then(async (api) => { api.renderStarCounts({}, `revue-summary-${this.section_id}_viewall`); api.renderCommentTab({ viewall: false, write_review: false, scroll_loading: true }, `revue-tab-${this.section_id}_viewall`); }); } } }); this.registerAction('scrollToLast', async (invocation) => { const viewallModalContentEle = document.querySelector(`#revue-viewall-modal-content-${this.section_id}`); if(viewallModalContentEle) { requestAnimationFrame(() => { viewallModalContentEle.scrollTop = this.markScrollTop; }); } }); } setMarkScrollTop() { this.markScrollTop = this.scrollTop; } refresh() { this.firstRender = true; this.scrollTop = 0; const productEle = document.querySelector(`#revue-viewall-modal-${this.section_id}`); if (productEle) { SPZ.whenApiDefined(productEle).then(async (api) => { api.close(); }); } } loadMore() { const productEle = document.querySelector(`#revue-product-compo`); if (productEle) { SPZ.whenApiDefined(productEle).then(async (api) => { await api.renderByScrollPagination(`revue-comment-list-${this.section_id}_tab`, 'tab'); }); } } } SPZ.defineElement(TAG, SPZCustomRevueViewallModal); let section_id = '1760579731641'; window.reviewProductSettings = {}; const default_settings = { "star_least": "5", "only_featured": false, "only_media": false, "review_insufficient": "no_reviews", "mini_quantity": 5, "actions": "hide", "pc_layout": "single_column", "m_loading_type": "modal", "m_modal_page_limit": "3", "page_limit": 10, "display_product_link": false, "hide_review_section": true, "title": "Reviews", "title_color": "rgba(51, 51, 51, 1)", "primary_color": "rgba(48, 53, 77, 1)", "section_bg_color": "rgba(255, 255, 255, 1)", "background_color_new": "rgba(255, 255, 255, 1)" }; const user_settings = { "description_text": null, "star_least": "5", "only_featured": false, "only_media": false, "review_insufficient": "no_reviews", "mini_quantity": 5, "actions": "hide", "pc_layout": "single_column", "m_loading_type": "modal", "m_modal_page_limit": "3", "comment_page_limit": null, "page_limit": 10, "display_product_link": false, "hide_review_section": true, "title": "Reviews", "accent_color": null, "title_color": "rgba(32, 32, 32, 1)", "text_color": "rgba(32, 32, 32, 1)", "section_bg_color": null, "background_color_new": null }; window.reviewProductSettings[section_id] = Object.assign({}, default_settings, user_settings, { page_limit: user_settings.comment_page_limit || user_settings.page_limit || default_settings.page_limit });