@jjlmoya/utils-motor 1.13.0 → 1.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/category/index.ts +2 -1
- package/src/entries.ts +4 -1
- package/src/tests/locale_completeness.test.ts +1 -1
- package/src/tests/mfe_assets_contract.test.ts +6 -1
- package/src/tests/registry_contract.test.ts +23 -0
- package/src/tests/tool_validation.test.ts +1 -1
- package/src/tool/vehicleTotalCostPerKilometreCalculator/bibliography.astro +6 -0
- package/src/tool/vehicleTotalCostPerKilometreCalculator/bibliography.ts +12 -0
- package/src/tool/vehicleTotalCostPerKilometreCalculator/component.astro +97 -0
- package/src/tool/vehicleTotalCostPerKilometreCalculator/controller.ts +198 -0
- package/src/tool/vehicleTotalCostPerKilometreCalculator/dom-views.ts +47 -0
- package/src/tool/vehicleTotalCostPerKilometreCalculator/entry.ts +30 -0
- package/src/tool/vehicleTotalCostPerKilometreCalculator/evaluator.ts +14 -0
- package/src/tool/vehicleTotalCostPerKilometreCalculator/i18n/de.ts +30 -0
- package/src/tool/vehicleTotalCostPerKilometreCalculator/i18n/en.ts +48 -0
- package/src/tool/vehicleTotalCostPerKilometreCalculator/i18n/es.ts +30 -0
- package/src/tool/vehicleTotalCostPerKilometreCalculator/i18n/factory.ts +102 -0
- package/src/tool/vehicleTotalCostPerKilometreCalculator/i18n/fr.ts +30 -0
- package/src/tool/vehicleTotalCostPerKilometreCalculator/i18n/id.ts +30 -0
- package/src/tool/vehicleTotalCostPerKilometreCalculator/i18n/it.ts +30 -0
- package/src/tool/vehicleTotalCostPerKilometreCalculator/i18n/ja.ts +30 -0
- package/src/tool/vehicleTotalCostPerKilometreCalculator/i18n/ko.ts +30 -0
- package/src/tool/vehicleTotalCostPerKilometreCalculator/i18n/nl.ts +30 -0
- package/src/tool/vehicleTotalCostPerKilometreCalculator/i18n/pl.ts +30 -0
- package/src/tool/vehicleTotalCostPerKilometreCalculator/i18n/pt.ts +30 -0
- package/src/tool/vehicleTotalCostPerKilometreCalculator/i18n/ru.ts +30 -0
- package/src/tool/vehicleTotalCostPerKilometreCalculator/i18n/sv.ts +30 -0
- package/src/tool/vehicleTotalCostPerKilometreCalculator/i18n/tr.ts +30 -0
- package/src/tool/vehicleTotalCostPerKilometreCalculator/i18n/zh.ts +30 -0
- package/src/tool/vehicleTotalCostPerKilometreCalculator/index.ts +10 -0
- package/src/tool/vehicleTotalCostPerKilometreCalculator/logic.test.ts +63 -0
- package/src/tool/vehicleTotalCostPerKilometreCalculator/logic.ts +132 -0
- package/src/tool/vehicleTotalCostPerKilometreCalculator/seo.astro +15 -0
- package/src/tool/vehicleTotalCostPerKilometreCalculator/storage.ts +38 -0
- package/src/tool/vehicleTotalCostPerKilometreCalculator/ui.ts +54 -0
- package/src/tool/vehicleTotalCostPerKilometreCalculator/vehicle-total-cost-per-kilometre-calculator.css +417 -0
- package/src/tools.ts +4 -1
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { FAQItem, HowToStep } from '../../../types';
|
|
2
|
+
import { createVehicleCostContent, createVehicleCostSeo } from './factory';
|
|
3
|
+
import type { VehicleTotalCostPerKilometreCalculatorUI } from '../ui';
|
|
4
|
+
|
|
5
|
+
const ui: VehicleTotalCostPerKilometreCalculatorUI = {
|
|
6
|
+
presetLabel: 'Börja med ett realistiskt mönster', presetCity: 'Mest stad', presetBalanced: 'Blandad körning', presetLongDistance: 'Lång körning', resetLabel: 'Återställ värden',
|
|
7
|
+
primaryScenarioLabel: 'Ditt fordon', comparisonScenarioLabel: 'Andra scenariot', distanceLabel: 'Sträcka per år', distanceHint: 'Använd den sträcka du tror att du kör på ett år', fuelUseLabel: 'Bränsleförbrukning', fuelUseHint: 'Fordonets genomsnitt i liter per 100 km', fuelPriceLabel: 'Bränslepris', fuelPriceHint: 'Pris per liter', insuranceLabel: 'Försäkring per år', insuranceHint: 'Din faktiska årspremie', maintenanceLabel: 'Underhåll per år', maintenanceHint: 'Service, reparationer, däck och kontroller', taxLabel: 'Skatt och avgifter per år', taxHint: 'Fordonsskatt, registrering och återkommande avgifter', depreciationLabel: 'Värdeminskning per år', depreciationHint: 'Din uppskattning av värdet som försvinner under ett år', currencyLabel: 'Valuta', compareLabel: 'Jämför ett annat scenario', compareOnLabel: 'Jämförelse på', compareOffLabel: 'Lägg till jämförelse', removeComparisonLabel: 'Dölj jämförelse', resultLabel: 'Kostnadsvägen', resultTitle: 'Vad varje kilometer verkligen kostar', resultSceneLabel: 'En väg visar fasta ägandekostnader och bränsle fram till kostnaden per kilometer', costPerKmLabel: 'Kostnad per kilometer', monthlyCostLabel: 'Genomsnittlig månad', annualCostLabel: 'Helt år', fuelCostLabel: 'Bränsle per år', fixedCostLabel: 'Fasta kostnader och ägande', statusBalanced: 'Kostnaderna fördelas över vägen', statusFixedHeavy: 'Ägandekostnaderna väger tyngst', statusRunningHeavy: 'Bränslet väger tyngst', validationError: 'Ange en positiv årlig sträcka och icke-negativa kostnader.', comparisonTitle: 'Jämförelse av scenarier', comparisonPrimaryCheaper: 'Ditt fordon är billigare med', comparisonSecondaryCheaper: 'Det andra scenariot är billigare med', comparisonSame: 'Scenarierna kostar ungefär lika mycket', fixedLaneLabel: 'Fast', fuelLaneLabel: 'Bränsle', totalLaneLabel: 'Totalt', formulaLabel: 'Beräkning', formulaText: 'Årskostnad = årlig sträcka × förbrukning ÷ 100 × bränslepris + försäkring + underhåll + skatt + värdeminskning. Kostnad per kilometer = årskostnad ÷ årlig sträcka.', inputUnitDistance: 'km', inputUnitFuelUse: 'L / 100 km', inputUnitFuelPrice: '/ L', inputUnitAnnualCost: '/ år', outputUnitDistance: 'km',
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
const faq: FAQItem[] = [
|
|
11
|
+
{ question: 'Vad ingår i kalkylatorn för kostnad per kilometer?', answer: 'Den kombinerar bränsle, försäkring, underhåll, skatter och avgifter samt den uppskattade årliga värdeminskningen. Du anger dina egna värden.' },
|
|
12
|
+
{ question: 'Varför blir resultatet högre än i en bränslekalkylator?', answer: 'Bränsle är bara den rörliga delen. Försäkring, reparationer, årsavgifter och värdeminskning ska också fördelas på varje kilometer.' },
|
|
13
|
+
{ question: 'Hur uppskattar jag värdeminskningen?', answer: 'Dra det förväntade värdet vid årets slut från startvärdet. Testa ett lågt och ett högt scenario om andrahandsvärdet är osäkert.' },
|
|
14
|
+
{ question: 'Kan jag jämföra två fordon?', answer: 'Ja. Aktivera jämförelsen och använd samma årliga sträcka, period och valuta.' },
|
|
15
|
+
{ question: 'Förutsäger resultatet den exakta kostnaden för en resa?', answer: 'Nej. Det är en planeringsuppskattning. Priser, reparationer, sträcka och andrahandsvärde kan förändras.' },
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
const howTo: HowToStep[] = [
|
|
19
|
+
{ name: 'Ange årlig sträcka', text: 'Skriv in hur många kilometer du tror att du kör på ett år.' },
|
|
20
|
+
{ name: 'Lägg till bränsle', text: 'Ange förbrukning per 100 kilometer och pris per liter.' },
|
|
21
|
+
{ name: 'Lägg till ägandekostnader', text: 'Ange försäkring, underhåll, skatt, avgifter och värdeminskning.' },
|
|
22
|
+
{ name: 'Läs eller jämför', text: 'Använd kostnaden per kilometer och kontrollera fasta kostnader mot bränsle.' },
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
export const content = createVehicleCostContent({
|
|
26
|
+
locale: 'sv', slug: 'kalkylator-total-fordonkostnad-per-kilometer', title: 'Kalkylator för total fordonskostnad per kilometer', description: 'Beräkna den verkliga kostnaden per kilometer med bränsle, ägandekostnader och årlig körsträcka.', faqTitle: 'Vanliga frågor', bibliographyTitle: 'Bibliografiska källor', ui, faq, howTo,
|
|
27
|
+
seo: createVehicleCostSeo({
|
|
28
|
+
introTitle: 'Se kostnaden som döljer sig bakom varje kilometer', introA: 'Ett bränslepris räcker inte för att visa vad ett fordon faktiskt kostar. Försäkring, service, däck, skatt och värdeminskning påverkar också beslutet. Kalkylatorn fördelar de årliga kostnaderna över den sträcka du väntar dig att köra och visar dem bredvid bränslekostnaden.', introB: 'Ange dina egna siffror i stället för ett allmänt genomsnitt. Huvudresultatet är kostnaden per kilometer, med månadssumma, årssumma och en visuell uppdelning mellan fasta kostnader och bränsle.', costsTitle: 'Vilka kostnader ska tas med', costsText: 'Bränslet förändras med sträckan. Försäkring, underhåll, avgifter och värdeminskning anges som årskostnader. En kort årlig sträcka gör fasta kostnader större per kilometer.', costsHeaders: ['Indata', 'Så används den', 'Kontrollera'], costsRows: [['Årlig sträcka', 'Delar årssumman på kilometer', 'Använd en realistisk sträcka'], ['Förbrukning och pris', 'Skapar den rörliga bränslekostnaden', 'Använd samma grund i båda scenarierna'], ['Försäkring, underhåll och skatt', 'Lägger till återkommande kostnader', 'Glöm inte däck och kontroller'], ['Värdeminskning', 'Lägger till årlig avskrivning', 'Testa flera andrahandsvärden']], readTitle: 'Läs kostnadsvägen', readText: 'Den fasta delen visar kostnader som inte ökar direkt för varje kilometer. Bränsledelen visar det distansberoende bidraget. Slutpunkten är den totala kostnaden per kilometer.', list: ['Testa en försiktig och en optimistisk värdeminskning.', 'Jämför fordon med samma årliga sträcka och valuta.', 'Byt efter några månader ut uppskattningar mot egna uppgifter.', 'Kontrollera månads- och årssumman mot din verkliga budget.'], decisionTitle: 'Jämför antaganden för att fatta beslut', decisionText: 'En jämförelse fungerar bara när scenarierna använder samma period och regler. Ändra en sak i taget när du undersöker ett köp eller en vanlig rutt.', tipTitle: 'Det är en planeringsmodell, inte en offert', tipText: 'Kalkylatorn hämtar inte marknadspriser, intygar inte ersättningar och känner inte till framtida andrahandsvärden. Den gör dina antaganden tydliga.'
|
|
29
|
+
}),
|
|
30
|
+
});
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { FAQItem, HowToStep } from '../../../types';
|
|
2
|
+
import { createVehicleCostContent, createVehicleCostSeo } from './factory';
|
|
3
|
+
import type { VehicleTotalCostPerKilometreCalculatorUI } from '../ui';
|
|
4
|
+
|
|
5
|
+
const ui: VehicleTotalCostPerKilometreCalculatorUI = {
|
|
6
|
+
presetLabel: 'Gerçekçi bir profille başlayın', presetCity: 'Çoğunlukla şehir', presetBalanced: 'Karma sürüş', presetLongDistance: 'Uzun mesafe', resetLabel: 'Değerleri sıfırla',
|
|
7
|
+
primaryScenarioLabel: 'Aracınız', comparisonScenarioLabel: 'İkinci senaryo', distanceLabel: 'Yıllık mesafe', distanceHint: 'Bir yılda gitmeyi beklediğiniz mesafeyi kullanın', fuelUseLabel: 'Yakıt tüketimi', fuelUseHint: 'Aracınızın 100 km başına litre cinsinden ortalaması', fuelPriceLabel: 'Yakıt fiyatı', fuelPriceHint: 'Litre başına ödenen fiyat', insuranceLabel: 'Yıllık sigorta', insuranceHint: 'Gerçek yıllık priminiz', maintenanceLabel: 'Yıllık bakım', maintenanceHint: 'Servis, onarım, lastik ve kontroller', taxLabel: 'Yıllık vergi ve ücretler', taxHint: 'Motorlu taşıt vergisi, tescil ve düzenli ücretler', depreciationLabel: 'Yıllık değer kaybı', depreciationHint: 'Aracın bir yılda kaybettiği tahmini değer', currencyLabel: 'Para birimi', compareLabel: 'Başka senaryoyla karşılaştır', compareOnLabel: 'Karşılaştırma açık', compareOffLabel: 'Karşılaştırma ekle', removeComparisonLabel: 'Karşılaştırmayı gizle', resultLabel: 'Maliyet yolu', resultTitle: 'Her kilometrenin gerçek maliyeti', resultSceneLabel: 'Bir yol, sabit sahiplik maliyetlerini ve yakıt maliyetlerini kilometre başı maliyete bağlar', costPerKmLabel: 'Kilometre başı maliyet', monthlyCostLabel: 'Aylık ortalama', annualCostLabel: 'Tam yıl', fuelCostLabel: 'Yıllık yakıt', fixedCostLabel: 'Sabit ve sahiplik maliyetleri', statusBalanced: 'Maliyetler yola dengeli dağılıyor', statusFixedHeavy: 'Sahiplik maliyetleri daha büyük paya sahip', statusRunningHeavy: 'Yakıt maliyetleri daha büyük paya sahip', validationError: 'Pozitif bir yıllık mesafe ve negatif olmayan maliyetler girin.', comparisonTitle: 'Senaryo karşılaştırması', comparisonPrimaryCheaper: 'Aracınız şu kadar daha ucuz', comparisonSecondaryCheaper: 'İkinci senaryo şu kadar daha ucuz', comparisonSame: 'İki senaryonun maliyeti yaklaşık aynı', fixedLaneLabel: 'Sabit', fuelLaneLabel: 'Yakıt', totalLaneLabel: 'Toplam', formulaLabel: 'Hesaplama', formulaText: 'Yıllık maliyet = yıllık mesafe × tüketim ÷ 100 × yakıt fiyatı + sigorta + bakım + vergiler + değer kaybı. Kilometre başı maliyet = yıllık maliyet ÷ yıllık mesafe.', inputUnitDistance: 'km', inputUnitFuelUse: 'L / 100 km', inputUnitFuelPrice: '/ L', inputUnitAnnualCost: '/ yıl', outputUnitDistance: 'km',
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
const faq: FAQItem[] = [
|
|
11
|
+
{ question: 'Bu kilometre başı maliyet hesaplayıcısı neleri içerir?', answer: 'Yakıtı; sigorta, bakım, vergi ve ücretler ile aracın yıl içinde kaybetmesini beklediğiniz değeri birleştirir. Kendi verilerinizi girersiniz.' },
|
|
12
|
+
{ question: 'Sonuç neden yakıt hesaplayıcısından daha yüksek?', answer: 'Yakıt yalnızca değişken bölümdür. Sigorta, onarım, yıllık ücretler ve amortisman da her kilometreye paylaştırılır.' },
|
|
13
|
+
{ question: 'Değer kaybını nasıl tahmin etmeliyim?', answer: 'Yıl sonunda beklenen değeri başlangıç değerinden çıkarın. İkinci el değeri belirsizse düşük ve yüksek bir senaryo deneyin.' },
|
|
14
|
+
{ question: 'İki aracı karşılaştırabilir miyim?', answer: 'Evet. Karşılaştırmayı açın ve iki senaryoda aynı yıllık mesafeyi, dönemi ve para birimini kullanın.' },
|
|
15
|
+
{ question: 'Sonuç gelecekteki yolculuğun kesin maliyetini tahmin eder mi?', answer: 'Hayır. Bu bir planlama tahminidir. Fiyatlar, onarımlar, mesafe ve yeniden satış değeri değişebilir.' },
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
const howTo: HowToStep[] = [
|
|
19
|
+
{ name: 'Yıllık mesafeyi belirleyin', text: 'Bir yılda gitmeyi beklediğiniz kilometreyi girin.' },
|
|
20
|
+
{ name: 'Yakıt varsayımlarını ekleyin', text: '100 kilometrede tüketimi ve litre fiyatını yazın.' },
|
|
21
|
+
{ name: 'Sahiplik maliyetlerini ekleyin', text: 'Sigorta, bakım, vergi, ücretler ve değer kaybını ekleyin.' },
|
|
22
|
+
{ name: 'Sonucu okuyun veya karşılaştırın', text: 'Kilometre başı maliyeti kullanın ve sabit maliyet ile yakıt payını inceleyin.' },
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
export const content = createVehicleCostContent({
|
|
26
|
+
locale: 'tr', slug: 'arac-kilometre-basina-toplam-maliyet-hesaplayici', title: 'Araç Kilometre Başına Toplam Maliyet Hesaplayıcısı', description: 'Yakıt, sahiplik maliyetleri ve yıllık mesafeyi birleştirerek aracın gerçek kilometre başı maliyetini hesaplayın.', faqTitle: 'Sık sorulan sorular', bibliographyTitle: 'Bibliyografik kaynaklar', ui, faq, howTo,
|
|
27
|
+
seo: createVehicleCostSeo({
|
|
28
|
+
introTitle: 'Her kilometrenin arkasındaki gizli maliyeti görün', introA: 'Tek başına yakıt fiyatı bir aracı kullanmanın gerçek maliyetini göstermez. Sigorta, bakım, lastikler, vergiler ve aracın kaybettiği değer de karara dahildir. Bu hesaplayıcı yıllık maliyetleri beklenen mesafeye dağıtır ve yakıt faturasıyla birlikte gösterir.', introB: 'Genel bir ortalamaya güvenmek yerine kendi değerlerinizi girin. Ana sonuç kilometre başı maliyettir; aylık ve yıllık toplamlar ile sabit maliyet ve yakıtın görsel dağılımı da verilir.', costsTitle: 'Tahmine hangi maliyetleri eklemeli', costsText: 'Yakıt mesafeyle değişir. Sigorta, bakım, ücretler ve değer kaybı yıllık tutarlar olarak girilir. Yıllık mesafe düşükse sabit maliyet kilometre başına daha ağır olur.', costsHeaders: ['Girdi', 'Hesaplayıcıdaki kullanımı', 'Kontrol edilecek'], costsRows: [['Yıllık mesafe', 'Yıllık toplamı kilometrelere böler', 'Gerçekçi bir mesafe kullanın'], ['Tüketim ve fiyat', 'Değişken yakıt maliyetini oluşturur', 'İki senaryoda aynı temeli kullanın'], ['Sigorta, bakım ve vergiler', 'Düzenli yıllık maliyetleri ekler', 'Lastikleri ve kontrolleri unutmayın'], ['Değer kaybı', 'Yıllık amortisman ekler', 'Birden fazla yeniden satış değeri deneyin']], readTitle: 'Maliyet yolunu okuyun', readText: 'Sabit bölüm her kilometreyle doğrudan artmayan maliyetleri gösterir. Yakıt bölümü mesafeye duyarlı kısmı gösterir. Son nokta karşılaştırılacak tüm kilometre başı maliyettir.', list: ['Belirsiz yeniden satış değeri için temkinli ve iyimser iki tahmin deneyin.', 'Araçları aynı yıllık mesafe ve para birimiyle karşılaştırın.', 'Birkaç ay sonra tahminleri kendi kayıtlarınızla değiştirin.', 'Aylık ve yıllık toplamların gerçek bütçenize uyup uymadığını kontrol edin.'], decisionTitle: 'Karar vermek için varsayımları karşılaştırın', decisionText: 'Karşılaştırma, iki senaryo aynı sahiplik dönemini ve kuralları kullandığında anlamlıdır. Bir satın alma veya düzenli rota incelerken her seferinde tek bir varsayımı değiştirin.', tipTitle: 'Bu bir planlama modelidir, teklif değildir', tipText: 'Hesaplayıcı piyasa fiyatlarına bakmaz, yasal giderleri onaylamaz ve gelecekteki yeniden satış değerini bilemez. Kendi varsayımlarınızı şeffaflaştırır.'
|
|
29
|
+
}),
|
|
30
|
+
});
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { FAQItem, HowToStep } from '../../../types';
|
|
2
|
+
import { createVehicleCostContent, createVehicleCostSeo } from './factory';
|
|
3
|
+
import type { VehicleTotalCostPerKilometreCalculatorUI } from '../ui';
|
|
4
|
+
|
|
5
|
+
const ui: VehicleTotalCostPerKilometreCalculatorUI = {
|
|
6
|
+
presetLabel: '从真实模式开始', presetCity: '主要城市道路', presetBalanced: '混合驾驶', presetLongDistance: '长途驾驶', resetLabel: '重置数值',
|
|
7
|
+
primaryScenarioLabel: '你的车辆', comparisonScenarioLabel: '第二个方案', distanceLabel: '每年行驶距离', distanceHint: '输入预计一年行驶的距离', fuelUseLabel: '燃油消耗', fuelUseHint: '车辆平均每100公里的升数', fuelPriceLabel: '燃油价格', fuelPriceHint: '每升支付的价格', insuranceLabel: '每年保险', insuranceHint: '实际年度保费', maintenanceLabel: '每年维护', maintenanceHint: '保养、维修、轮胎和检查', taxLabel: '每年税费', taxHint: '道路税、登记费和固定费用', depreciationLabel: '每年价值损失', depreciationHint: '估计车辆一年中损失的价值', currencyLabel: '货币', compareLabel: '比较另一个方案', compareOnLabel: '比较已开启', compareOffLabel: '添加比较', removeComparisonLabel: '隐藏比较', resultLabel: '成本之路', resultTitle: '每一公里真正的成本', resultSceneLabel: '道路展示固定拥有成本和燃油成本如何汇合为每公里成本', costPerKmLabel: '每公里成本', monthlyCostLabel: '月平均', annualCostLabel: '全年合计', fuelCostLabel: '每年燃油', fixedCostLabel: '固定和拥有成本', statusBalanced: '成本均匀分布在道路上', statusFixedHeavy: '拥有成本占主要部分', statusRunningHeavy: '燃油成本占主要部分', validationError: '请输入正数的年度距离,并且成本不能为负数。', comparisonTitle: '方案比较', comparisonPrimaryCheaper: '你的车辆便宜', comparisonSecondaryCheaper: '第二个方案便宜', comparisonSame: '两个方案的成本大致相同', fixedLaneLabel: '固定', fuelLaneLabel: '燃油', totalLaneLabel: '合计', formulaLabel: '计算方式', formulaText: '年度成本 = 年度距离 × 燃油消耗 ÷ 100 × 燃油价格 + 保险 + 维护 + 税费 + 价值损失。每公里成本 = 年度成本 ÷ 年度距离。', inputUnitDistance: 'km', inputUnitFuelUse: 'L / 100 km', inputUnitFuelPrice: '/ L', inputUnitAnnualCost: '/ 年', outputUnitDistance: 'km',
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
const faq: FAQItem[] = [
|
|
11
|
+
{ question: '这款每公里成本计算器包含哪些费用?', answer: '它将燃油、保险、维护、税费以及车辆一年中的预计价值损失合并计算。你可以输入自己的车辆数据。' },
|
|
12
|
+
{ question: '为什么结果高于燃油成本计算器?', answer: '燃油只是随距离变化的部分。保险、维修、年度费用和折旧也需要分摊到每公里。' },
|
|
13
|
+
{ question: '应该如何估计折旧?', answer: '用起始价值减去一年后的预计价值。如果转售价值不确定,可以分别测试低值和高值方案。' },
|
|
14
|
+
{ question: '可以比较两辆车吗?', answer: '可以。开启比较,并为两个方案使用相同的年度距离、周期和货币。' },
|
|
15
|
+
{ question: '结果会预测未来旅程的准确费用吗?', answer: '不会。这是规划估算。价格、维修、距离和转售价值都可能变化。' },
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
const howTo: HowToStep[] = [
|
|
19
|
+
{ name: '设定年度距离', text: '输入预计一年行驶的公里数。' },
|
|
20
|
+
{ name: '添加燃油假设', text: '输入每100公里的消耗量和每升价格。' },
|
|
21
|
+
{ name: '添加拥有成本', text: '输入保险、维护、税费和年度价值损失。' },
|
|
22
|
+
{ name: '阅读或比较结果', text: '使用每公里成本作为主要比较值,并检查固定成本与燃油成本的比例。' },
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
export const content = createVehicleCostContent({
|
|
26
|
+
locale: 'zh', slug: 'vehicle-total-cost-per-kilometre-calculator', title: '车辆每公里总成本计算器', description: '结合燃油、拥有成本和年度距离,计算车辆每公里的真实使用成本。', faqTitle: '常见问题', bibliographyTitle: '参考资料', ui, faq, howTo,
|
|
27
|
+
seo: createVehicleCostSeo({
|
|
28
|
+
introTitle: '看见每一公里背后的隐藏成本', introA: '燃油价格本身无法说明使用车辆的真实成本。保险、保养、轮胎、税费以及车辆拥有期间损失的价值也会影响决策。本计算器把年度成本按预计行驶距离分摊,并和燃油账单放在一起。', introB: '输入自己的车辆数据,而不是依赖通用平均值。主要结果是每公里成本,同时提供月度和年度合计,以及固定拥有成本和燃油成本的可视化拆分。', costsTitle: '估算中应包括哪些成本', costsText: '燃油会随着距离变化,而保险、维护、费用和折旧以年度金额输入。年度距离较短时,固定成本会让每公里价格更高。', costsHeaders: ['输入', '计算方式', '需要检查'], costsRows: [['年度距离', '把年度总额分摊到每公里', '使用合理的距离'], ['消耗量和价格', '生成可变燃油成本', '两个方案使用相同基础'], ['保险、维护和税费', '加入固定年度成本', '不要忘记轮胎和检查'], ['价值损失', '加入年度折旧估算', '测试多个转售价值']], readTitle: '如何阅读成本之路', readText: '固定部分展示不会随每公里直接增加的费用。燃油部分展示对距离敏感的费用。终点数值就是用于比较的完整每公里成本。', list: ['当转售价值不确定时,测试保守和乐观两种折旧。', '使用相同年度距离和货币比较车辆。', '几个月后用真实记录替换估算值。', '检查月度和年度合计是否符合真实预算。'], decisionTitle: '比较假设,帮助做决定', decisionText: '只有当两个方案使用相同的拥有周期和规则时,比较才有意义。研究购车或固定路线时,一次只改变一个假设。', tipTitle: '这是规划模型,不是报价', tipText: '计算器不会查询市场价格,不会证明法律费用,也不知道未来转售价值。它的作用是让你的假设透明可比。'
|
|
29
|
+
}),
|
|
30
|
+
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { vehicleTotalCostPerKilometreCalculator } from './entry';
|
|
2
|
+
|
|
3
|
+
export * from './entry';
|
|
4
|
+
|
|
5
|
+
export const VEHICLE_TOTAL_COST_PER_KILOMETRE_CALCULATOR_TOOL = {
|
|
6
|
+
entry: vehicleTotalCostPerKilometreCalculator,
|
|
7
|
+
Component: () => import('./component.astro'),
|
|
8
|
+
SEOComponent: () => import('./seo.astro'),
|
|
9
|
+
BibliographyComponent: () => import('./bibliography.astro'),
|
|
10
|
+
};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { calculateVehicleCost, compareVehicleCosts, convertCurrencyAmount, convertVehicleCostInput, validateVehicleCostInput, type VehicleCostInput } from './logic';
|
|
3
|
+
|
|
4
|
+
const example: VehicleCostInput = {
|
|
5
|
+
annualKm: 15000,
|
|
6
|
+
fuelUsePer100Km: 6,
|
|
7
|
+
fuelPrice: 1.8,
|
|
8
|
+
insurance: 600,
|
|
9
|
+
maintenance: 500,
|
|
10
|
+
tax: 200,
|
|
11
|
+
depreciation: 2400,
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
describe('vehicle total cost per kilometre logic', () => {
|
|
15
|
+
it('calculates fuel, fixed and total costs', () => {
|
|
16
|
+
const result = calculateVehicleCost(example);
|
|
17
|
+
expect(result).not.toBeNull();
|
|
18
|
+
expect(result?.annualFuelLitres).toBe(900);
|
|
19
|
+
expect(result?.annualFuelCost).toBe(1620);
|
|
20
|
+
expect(result?.fixedCost).toBe(3700);
|
|
21
|
+
expect(result?.annualCost).toBe(5320);
|
|
22
|
+
expect(result?.monthlyCost).toBeCloseTo(443.3333);
|
|
23
|
+
expect(result?.costPerKm).toBeCloseTo(0.3546667);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('rejects zero distance and negative costs', () => {
|
|
27
|
+
expect(validateVehicleCostInput({ ...example, annualKm: 0 }).valid).toBe(false);
|
|
28
|
+
expect(validateVehicleCostInput({ ...example, tax: -1 }).valid).toBe(false);
|
|
29
|
+
expect(calculateVehicleCost({ ...example, fuelPrice: Number.NaN })).toBeNull();
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('handles a zero cost scenario without dividing by zero', () => {
|
|
33
|
+
const result = calculateVehicleCost({ annualKm: 1000, fuelUsePer100Km: 0, fuelPrice: 0, insurance: 0, maintenance: 0, tax: 0, depreciation: 0 });
|
|
34
|
+
expect(result?.annualCost).toBe(0);
|
|
35
|
+
expect(result?.costPerKm).toBe(0);
|
|
36
|
+
expect(result?.fixedShare).toBe(0);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('identifies the cheaper scenario by cost per kilometre', () => {
|
|
40
|
+
const cheaper = compareVehicleCosts(example, { ...example, annualKm: 20000 });
|
|
41
|
+
expect(cheaper?.cheaper).toBe('secondary');
|
|
42
|
+
expect(cheaper?.differencePerKm).toBeLessThan(0);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('returns same when the costs are effectively equal', () => {
|
|
46
|
+
const same = compareVehicleCosts(example, { ...example });
|
|
47
|
+
expect(same?.cheaper).toBe('same');
|
|
48
|
+
expect(same?.differencePerKm).toBe(0);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('converts monetary inputs while keeping distance and consumption unchanged', () => {
|
|
52
|
+
const converted = convertVehicleCostInput(example, 'EUR', 'JPY');
|
|
53
|
+
expect(converted.annualKm).toBe(example.annualKm);
|
|
54
|
+
expect(converted.fuelUsePer100Km).toBe(example.fuelUsePer100Km);
|
|
55
|
+
expect(converted.fuelPrice).toBeCloseTo(convertCurrencyAmount(example.fuelPrice, 'EUR', 'JPY'));
|
|
56
|
+
expect(converted.insurance).toBeCloseTo(111552);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('uses the shared currency ratios for rubles and yuan', () => {
|
|
60
|
+
expect(convertCurrencyAmount(100, 'EUR', 'RUB')).toBeCloseTo(9673);
|
|
61
|
+
expect(convertCurrencyAmount(100, 'EUR', 'CNY')).toBeCloseTo(782.51);
|
|
62
|
+
});
|
|
63
|
+
});
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
export interface VehicleCostInput {
|
|
2
|
+
annualKm: number;
|
|
3
|
+
fuelUsePer100Km: number;
|
|
4
|
+
fuelPrice: number;
|
|
5
|
+
insurance: number;
|
|
6
|
+
maintenance: number;
|
|
7
|
+
tax: number;
|
|
8
|
+
depreciation: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface CurrencyOption {
|
|
12
|
+
code: string;
|
|
13
|
+
symbol: string;
|
|
14
|
+
name: string;
|
|
15
|
+
unitsPerEur: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const CURRENCY_OPTIONS = [
|
|
19
|
+
{ code: 'EUR', symbol: '€', name: 'Euro', unitsPerEur: 1 },
|
|
20
|
+
{ code: 'USD', symbol: '$', name: 'US dollar', unitsPerEur: 1.1643 },
|
|
21
|
+
{ code: 'GBP', symbol: '£', name: 'Pound sterling', unitsPerEur: 0.8572 },
|
|
22
|
+
{ code: 'JPY', symbol: '¥', name: 'Japanese yen', unitsPerEur: 185.92 },
|
|
23
|
+
{ code: 'CNY', symbol: 'CN¥', name: 'Chinese yuan', unitsPerEur: 7.8251 },
|
|
24
|
+
{ code: 'RUB', symbol: '₽', name: 'Russian ruble', unitsPerEur: 96.73 },
|
|
25
|
+
{ code: 'INR', symbol: '₹', name: 'Indian rupee', unitsPerEur: 111.0585 },
|
|
26
|
+
{ code: 'BRL', symbol: 'R$', name: 'Brazilian real', unitsPerEur: 6.0126 },
|
|
27
|
+
{ code: 'MXN', symbol: 'MX$', name: 'Mexican peso', unitsPerEur: 19.7327 },
|
|
28
|
+
{ code: 'CAD', symbol: 'CA$', name: 'Canadian dollar', unitsPerEur: 1.613 },
|
|
29
|
+
{ code: 'AUD', symbol: 'A$', name: 'Australian dollar', unitsPerEur: 1.6183 },
|
|
30
|
+
{ code: 'CHF', symbol: 'CHF', name: 'Swiss franc', unitsPerEur: 0.9364 },
|
|
31
|
+
{ code: 'SEK', symbol: 'kr', name: 'Swedish krona', unitsPerEur: 11.0885 },
|
|
32
|
+
{ code: 'PLN', symbol: 'zł', name: 'Polish zloty', unitsPerEur: 4.3365 },
|
|
33
|
+
{ code: 'TRY', symbol: '₺', name: 'Turkish lira', unitsPerEur: 56.1718 },
|
|
34
|
+
{ code: 'KRW', symbol: '₩', name: 'South Korean won', unitsPerEur: 1600.39 },
|
|
35
|
+
{ code: 'IDR', symbol: 'Rp', name: 'Indonesian rupiah', unitsPerEur: 20628.08 },
|
|
36
|
+
] as const;
|
|
37
|
+
|
|
38
|
+
export type CurrencyCode = typeof CURRENCY_OPTIONS[number]['code'];
|
|
39
|
+
|
|
40
|
+
export const DEFAULT_CURRENCY: CurrencyCode = 'EUR';
|
|
41
|
+
|
|
42
|
+
export function isCurrencyCode(value: unknown): value is CurrencyCode {
|
|
43
|
+
return typeof value === 'string' && CURRENCY_OPTIONS.some((option) => option.code === value);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function getCurrencyOption(code: CurrencyCode): CurrencyOption {
|
|
47
|
+
return CURRENCY_OPTIONS.find((option) => option.code === code) ?? CURRENCY_OPTIONS[0]!;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function convertCurrencyAmount(amount: number, from: CurrencyCode, to: CurrencyCode): number {
|
|
51
|
+
const fromFactor = getCurrencyOption(from).unitsPerEur;
|
|
52
|
+
const toFactor = getCurrencyOption(to).unitsPerEur;
|
|
53
|
+
return (amount / fromFactor) * toFactor;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function convertVehicleCostInput(input: VehicleCostInput, from: CurrencyCode, to: CurrencyCode): VehicleCostInput {
|
|
57
|
+
if (from === to) return { ...input };
|
|
58
|
+
return {
|
|
59
|
+
...input,
|
|
60
|
+
fuelPrice: convertCurrencyAmount(input.fuelPrice, from, to),
|
|
61
|
+
insurance: convertCurrencyAmount(input.insurance, from, to),
|
|
62
|
+
maintenance: convertCurrencyAmount(input.maintenance, from, to),
|
|
63
|
+
tax: convertCurrencyAmount(input.tax, from, to),
|
|
64
|
+
depreciation: convertCurrencyAmount(input.depreciation, from, to),
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface VehicleCostResult {
|
|
69
|
+
annualFuelLitres: number;
|
|
70
|
+
annualFuelCost: number;
|
|
71
|
+
fixedCost: number;
|
|
72
|
+
annualCost: number;
|
|
73
|
+
monthlyCost: number;
|
|
74
|
+
costPerKm: number;
|
|
75
|
+
fixedShare: number;
|
|
76
|
+
runningShare: number;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export interface VehicleCostComparison {
|
|
80
|
+
primary: VehicleCostResult;
|
|
81
|
+
secondary: VehicleCostResult;
|
|
82
|
+
differencePerKm: number;
|
|
83
|
+
cheaper: 'primary' | 'secondary' | 'same';
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export interface VehicleCostValidation {
|
|
87
|
+
valid: boolean;
|
|
88
|
+
errors: string[];
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const nonNegativeFields: Array<keyof VehicleCostInput> = ['fuelUsePer100Km', 'fuelPrice', 'insurance', 'maintenance', 'tax', 'depreciation'];
|
|
92
|
+
|
|
93
|
+
export function validateVehicleCostInput(input: VehicleCostInput): VehicleCostValidation {
|
|
94
|
+
const errors: string[] = [];
|
|
95
|
+
if (!Number.isFinite(input.annualKm) || input.annualKm <= 0) errors.push('annualKm');
|
|
96
|
+
nonNegativeFields.forEach((field) => {
|
|
97
|
+
if (!Number.isFinite(input[field]) || input[field] < 0) errors.push(field);
|
|
98
|
+
});
|
|
99
|
+
return { valid: errors.length === 0, errors };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function calculateVehicleCost(input: VehicleCostInput): VehicleCostResult | null {
|
|
103
|
+
if (!validateVehicleCostInput(input).valid) return null;
|
|
104
|
+
const annualFuelLitres = input.annualKm * input.fuelUsePer100Km / 100;
|
|
105
|
+
const annualFuelCost = annualFuelLitres * input.fuelPrice;
|
|
106
|
+
const fixedCost = input.insurance + input.maintenance + input.tax + input.depreciation;
|
|
107
|
+
const annualCost = fixedCost + annualFuelCost;
|
|
108
|
+
return {
|
|
109
|
+
annualFuelLitres,
|
|
110
|
+
annualFuelCost,
|
|
111
|
+
fixedCost,
|
|
112
|
+
annualCost,
|
|
113
|
+
monthlyCost: annualCost / 12,
|
|
114
|
+
costPerKm: annualCost / input.annualKm,
|
|
115
|
+
fixedShare: annualCost === 0 ? 0 : fixedCost / annualCost,
|
|
116
|
+
runningShare: annualCost === 0 ? 0 : annualFuelCost / annualCost,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function compareVehicleCosts(primaryInput: VehicleCostInput, secondaryInput: VehicleCostInput): VehicleCostComparison | null {
|
|
121
|
+
const primary = calculateVehicleCost(primaryInput);
|
|
122
|
+
const secondary = calculateVehicleCost(secondaryInput);
|
|
123
|
+
if (!primary || !secondary) return null;
|
|
124
|
+
const differencePerKm = secondary.costPerKm - primary.costPerKm;
|
|
125
|
+
const cheaper = getCheaperScenario(differencePerKm);
|
|
126
|
+
return { primary, secondary, differencePerKm, cheaper };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function getCheaperScenario(differencePerKm: number): 'primary' | 'secondary' | 'same' {
|
|
130
|
+
if (Math.abs(differencePerKm) < 0.0001) return 'same';
|
|
131
|
+
return differencePerKm > 0 ? 'primary' : 'secondary';
|
|
132
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { SEORenderer } from '@jjlmoya/utils-shared';
|
|
3
|
+
import { vehicleTotalCostPerKilometreCalculator } from './entry';
|
|
4
|
+
import type { KnownLocale } from '../../types';
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
locale?: KnownLocale;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const { locale = 'en' } = Astro.props;
|
|
11
|
+
const loader = vehicleTotalCostPerKilometreCalculator.i18n[locale] ?? vehicleTotalCostPerKilometreCalculator.i18n.en;
|
|
12
|
+
const content = await loader?.();
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
{content && <SEORenderer content={{ locale, sections: content.seo }} />}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { DEFAULT_CURRENCY, isCurrencyCode, type CurrencyCode, type VehicleCostInput } from './logic';
|
|
2
|
+
|
|
3
|
+
export interface VehicleCostStorageState {
|
|
4
|
+
primary: VehicleCostInput;
|
|
5
|
+
secondary: VehicleCostInput;
|
|
6
|
+
comparisonEnabled: boolean;
|
|
7
|
+
currency: CurrencyCode;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function normalizeCurrency(value: unknown, fallback: CurrencyCode): CurrencyCode {
|
|
11
|
+
if (isCurrencyCode(value)) return value;
|
|
12
|
+
const legacySymbols: Record<string, CurrencyCode> = { '€': 'EUR', '$': 'EUR', '£': 'EUR' };
|
|
13
|
+
return typeof value === 'string' && legacySymbols[value] ? legacySymbols[value] : fallback;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function loadVehicleCostState(key: string, fallback: VehicleCostStorageState): VehicleCostStorageState {
|
|
17
|
+
if (typeof window === 'undefined') return fallback;
|
|
18
|
+
try {
|
|
19
|
+
const raw = window.localStorage.getItem(key);
|
|
20
|
+
if (!raw) return fallback;
|
|
21
|
+
const parsed = JSON.parse(raw) as Partial<VehicleCostStorageState>;
|
|
22
|
+
if (!parsed.primary || !parsed.secondary) return fallback;
|
|
23
|
+
return {
|
|
24
|
+
...fallback,
|
|
25
|
+
...parsed,
|
|
26
|
+
currency: normalizeCurrency(parsed.currency, fallback.currency ?? DEFAULT_CURRENCY),
|
|
27
|
+
};
|
|
28
|
+
} catch {
|
|
29
|
+
return fallback;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function saveVehicleCostState(key: string, state: VehicleCostStorageState): void {
|
|
34
|
+
if (typeof window === 'undefined') return;
|
|
35
|
+
try {
|
|
36
|
+
window.localStorage.setItem(key, JSON.stringify(state));
|
|
37
|
+
} catch {}
|
|
38
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
export interface VehicleTotalCostPerKilometreCalculatorUI extends Record<string, string> {
|
|
2
|
+
presetLabel: string;
|
|
3
|
+
presetCity: string;
|
|
4
|
+
presetBalanced: string;
|
|
5
|
+
presetLongDistance: string;
|
|
6
|
+
resetLabel: string;
|
|
7
|
+
primaryScenarioLabel: string;
|
|
8
|
+
comparisonScenarioLabel: string;
|
|
9
|
+
distanceLabel: string;
|
|
10
|
+
distanceHint: string;
|
|
11
|
+
fuelUseLabel: string;
|
|
12
|
+
fuelUseHint: string;
|
|
13
|
+
fuelPriceLabel: string;
|
|
14
|
+
fuelPriceHint: string;
|
|
15
|
+
insuranceLabel: string;
|
|
16
|
+
insuranceHint: string;
|
|
17
|
+
maintenanceLabel: string;
|
|
18
|
+
maintenanceHint: string;
|
|
19
|
+
taxLabel: string;
|
|
20
|
+
taxHint: string;
|
|
21
|
+
depreciationLabel: string;
|
|
22
|
+
depreciationHint: string;
|
|
23
|
+
currencyLabel: string;
|
|
24
|
+
compareLabel: string;
|
|
25
|
+
compareOnLabel: string;
|
|
26
|
+
compareOffLabel: string;
|
|
27
|
+
removeComparisonLabel: string;
|
|
28
|
+
resultLabel: string;
|
|
29
|
+
resultTitle: string;
|
|
30
|
+
resultSceneLabel: string;
|
|
31
|
+
costPerKmLabel: string;
|
|
32
|
+
monthlyCostLabel: string;
|
|
33
|
+
annualCostLabel: string;
|
|
34
|
+
fuelCostLabel: string;
|
|
35
|
+
fixedCostLabel: string;
|
|
36
|
+
statusBalanced: string;
|
|
37
|
+
statusFixedHeavy: string;
|
|
38
|
+
statusRunningHeavy: string;
|
|
39
|
+
validationError: string;
|
|
40
|
+
comparisonTitle: string;
|
|
41
|
+
comparisonPrimaryCheaper: string;
|
|
42
|
+
comparisonSecondaryCheaper: string;
|
|
43
|
+
comparisonSame: string;
|
|
44
|
+
fixedLaneLabel: string;
|
|
45
|
+
fuelLaneLabel: string;
|
|
46
|
+
totalLaneLabel: string;
|
|
47
|
+
formulaLabel: string;
|
|
48
|
+
formulaText: string;
|
|
49
|
+
inputUnitDistance: string;
|
|
50
|
+
inputUnitFuelUse: string;
|
|
51
|
+
inputUnitFuelPrice: string;
|
|
52
|
+
inputUnitAnnualCost: string;
|
|
53
|
+
outputUnitDistance: string;
|
|
54
|
+
}
|