Replace xlsx with exceljs, unify CRS/DMS logic, move pages to views with shared map/toast composables, and add Vitest/ESLint/Prettier plus docs. Co-authored-by: Cursor <cursoragent@cursor.com>
169 lines
5.4 KiB
Vue
169 lines
5.4 KiB
Vue
<template>
|
||
<div class="container">
|
||
<div class="tool-header">
|
||
<router-link to="/" class="back-link">← 返回首页</router-link>
|
||
<h2>📊 角度转换工具</h2>
|
||
<p>度分秒、十进制度数、弧度之间的转换</p>
|
||
</div>
|
||
|
||
<div class="card">
|
||
<div class="grid grid-3">
|
||
<!-- DMS Input -->
|
||
<div>
|
||
<h3 class="card-title">度分秒</h3>
|
||
<div class="input-group">
|
||
<label class="input-label">度</label>
|
||
<input type="number" v-model.number="dms.degrees" class="input" @input="fromDms">
|
||
</div>
|
||
<div class="input-group">
|
||
<label class="input-label">分</label>
|
||
<input type="number" v-model.number="dms.minutes" class="input" @input="fromDms" min="0"
|
||
max="59">
|
||
</div>
|
||
<div class="input-group">
|
||
<label class="input-label">秒</label>
|
||
<input type="number" v-model.number="dms.seconds" class="input" @input="fromDms" min="0"
|
||
max="59" step="0.01">
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Decimal Input -->
|
||
<div>
|
||
<h3 class="card-title">十进制度数</h3>
|
||
<div class="input-group">
|
||
<label class="input-label">度数</label>
|
||
<input type="number" v-model.number="decimal" class="input" @input="fromDecimal" step="0.000001"
|
||
placeholder="例:45.5">
|
||
</div>
|
||
<div class="conversion-info">
|
||
<p class="hint">当前角度的其他表示形式会自动同步</p>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Radian Input -->
|
||
<div>
|
||
<h3 class="card-title">弧度</h3>
|
||
<div class="input-group">
|
||
<label class="input-label">弧度值</label>
|
||
<input type="number" v-model.number="radian" class="input" @input="fromRadian" step="0.000001"
|
||
placeholder="例:0.7854">
|
||
</div>
|
||
<div class="conversion-info">
|
||
<p class="hint">π ≈ 3.14159</p>
|
||
<p class="hint">180° = π 弧度</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="result mt-xl">
|
||
<div class="result-label">完整转换结果</div>
|
||
<div class="conversion-results">
|
||
<div class="conversion-item">
|
||
<strong>度分秒:</strong>
|
||
<span>{{ dms.degrees }}° {{ dms.minutes }}' {{ dms.seconds.toFixed(2) }}"</span>
|
||
</div>
|
||
<div class="conversion-item">
|
||
<strong>十进制:</strong>
|
||
<span>{{ decimal.toFixed(6) }}°</span>
|
||
</div>
|
||
<div class="conversion-item">
|
||
<strong>弧度:</strong>
|
||
<span>{{ radian.toFixed(6) }} rad</span>
|
||
</div>
|
||
<div class="conversion-item">
|
||
<strong>π表示:</strong>
|
||
<span>{{ (radian / Math.PI).toFixed(4) }}π</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { ref } from 'vue'
|
||
import { dmsToDecimal, decimalToDms } from '../utils/coordinate'
|
||
|
||
const dms = ref({ degrees: 45, minutes: 30, seconds: 0 })
|
||
const decimal = ref(45.5)
|
||
const radian = ref(0.7941)
|
||
|
||
const fromDms = () => {
|
||
decimal.value = dmsToDecimal(dms.value.degrees, dms.value.minutes, dms.value.seconds)
|
||
radian.value = decimal.value * Math.PI / 180
|
||
}
|
||
|
||
const fromDecimal = () => {
|
||
const converted = decimalToDms(decimal.value)
|
||
dms.value = converted
|
||
radian.value = decimal.value * Math.PI / 180
|
||
}
|
||
|
||
const fromRadian = () => {
|
||
decimal.value = radian.value * 180 / Math.PI
|
||
const converted = decimalToDms(decimal.value)
|
||
dms.value = converted
|
||
}
|
||
|
||
// Initialize
|
||
fromDms()
|
||
</script>
|
||
|
||
<style scoped>
|
||
.tool-header {
|
||
text-align: center;
|
||
margin-bottom: var(--spacing-xl);
|
||
}
|
||
|
||
.back-link {
|
||
display: inline-block;
|
||
color: var(--text-secondary);
|
||
text-decoration: none;
|
||
margin-bottom: var(--spacing-md);
|
||
transition: color var(--transition-base);
|
||
}
|
||
|
||
.back-link:hover {
|
||
color: var(--primary);
|
||
}
|
||
|
||
.conversion-info {
|
||
margin-top: var(--spacing-lg);
|
||
padding: var(--spacing-md);
|
||
background: var(--bg-secondary);
|
||
border-radius: var(--border-radius-sm);
|
||
}
|
||
|
||
.hint {
|
||
font-size: var(--font-size-sm);
|
||
color: var(--text-tertiary);
|
||
margin-bottom: var(--spacing-xs);
|
||
}
|
||
|
||
.conversion-results {
|
||
display: grid;
|
||
gap: var(--spacing-md);
|
||
margin-top: var(--spacing-md);
|
||
}
|
||
|
||
.conversion-item {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
padding: var(--spacing-sm) var(--spacing-md);
|
||
background: var(--bg-secondary);
|
||
border-radius: var(--border-radius-sm);
|
||
font-size: var(--font-size-base);
|
||
}
|
||
|
||
.conversion-item span {
|
||
color: var(--primary);
|
||
font-weight: 600;
|
||
}
|
||
|
||
@media (max-width: 768px) {
|
||
.grid-3 {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
}
|
||
</style>
|