>} data
+ * @param {string} filename
+ * @param {string} sheetName
+ */
+export async function writeAoaToExcel(data, filename, sheetName = 'Sheet1') {
+ const workbook = new ExcelJS.Workbook()
+ const worksheet = workbook.addWorksheet(sheetName)
+ data.forEach((row) => worksheet.addRow(row))
+
+ const buffer = await workbook.xlsx.writeBuffer()
+ downloadBuffer(buffer, filename)
+}
diff --git a/src/utils/geometry.js b/src/utils/geometry.js
index 691ede9..b6056de 100644
--- a/src/utils/geometry.js
+++ b/src/utils/geometry.js
@@ -33,9 +33,8 @@ export function calculateBearing(x1, y1, x2, y2) {
if (azimuth < 0) azimuth += 360;
// 计算象限角
- let quadrant = '';
const angle = Math.abs(Math.atan2(dx, dy) * 180 / Math.PI);
-
+ let quadrant;
if (dx >= 0 && dy >= 0) {
quadrant = `N ${angle.toFixed(2)}° E`;
} else if (dx >= 0 && dy < 0) {
@@ -51,44 +50,3 @@ export function calculateBearing(x1, y1, x2, y2) {
quadrant
};
}
-
-/**
- * 计算多边形面积(Shoelace公式)
- * @param {Array<{x: number, y: number}>} points - 顶点坐标数组
- * @returns {number} 面积
- */
-export function calculatePolygonArea(points) {
- if (points.length < 3) return 0;
-
- let area = 0;
- for (let i = 0; i < points.length; i++) {
- const j = (i + 1) % points.length;
- area += points[i].x * points[j].y;
- area -= points[j].x * points[i].y;
- }
-
- return Math.abs(area / 2);
-}
-
-/**
- * 度分秒转十进制度(角度)
- */
-export function dmsAngleToDecimal(degrees, minutes, seconds) {
- return degrees + minutes / 60 + seconds / 3600;
-}
-
-/**
- * 十进制度转度分秒(角度)
- */
-export function decimalAngleToDms(decimal) {
- const degrees = Math.floor(decimal);
- const minutesDecimal = (decimal - degrees) * 60;
- const minutes = Math.floor(minutesDecimal);
- const seconds = (minutesDecimal - minutes) * 60;
-
- return {
- degrees,
- minutes,
- seconds: parseFloat(seconds.toFixed(6))
- };
-}
diff --git a/src/utils/geometry.test.js b/src/utils/geometry.test.js
new file mode 100644
index 0000000..0641f82
--- /dev/null
+++ b/src/utils/geometry.test.js
@@ -0,0 +1,42 @@
+import { describe, it, expect } from 'vitest'
+import { calculateBearing, degToRad, radToDeg } from '../utils/geometry.js'
+
+describe('degToRad / radToDeg', () => {
+ it('converts 180 degrees to PI radians', () => {
+ expect(degToRad(180)).toBeCloseTo(Math.PI, 10)
+ })
+
+ it('converts PI radians to 180 degrees', () => {
+ expect(radToDeg(Math.PI)).toBeCloseTo(180, 10)
+ })
+})
+
+describe('calculateBearing', () => {
+ it('returns northeast quadrant', () => {
+ const result = calculateBearing(0, 0, 100, 100)
+ expect(result.azimuth).toBeCloseTo(45, 1)
+ expect(result.quadrant).toContain('N')
+ expect(result.quadrant).toContain('E')
+ })
+
+ it('returns southeast quadrant', () => {
+ const result = calculateBearing(0, 0, 100, -100)
+ expect(result.azimuth).toBeCloseTo(135, 1)
+ expect(result.quadrant).toContain('S')
+ expect(result.quadrant).toContain('E')
+ })
+
+ it('returns southwest quadrant', () => {
+ const result = calculateBearing(0, 0, -100, -100)
+ expect(result.azimuth).toBeCloseTo(225, 1)
+ expect(result.quadrant).toContain('S')
+ expect(result.quadrant).toContain('W')
+ })
+
+ it('returns northwest quadrant', () => {
+ const result = calculateBearing(0, 0, -100, 100)
+ expect(result.azimuth).toBeCloseTo(315, 1)
+ expect(result.quadrant).toContain('N')
+ expect(result.quadrant).toContain('W')
+ })
+})
diff --git a/src/utils/proj.test.js b/src/utils/proj.test.js
new file mode 100644
index 0000000..3176d3d
--- /dev/null
+++ b/src/utils/proj.test.js
@@ -0,0 +1,53 @@
+import { describe, it, expect } from 'vitest'
+import {
+ getCentralMeridian,
+ projectToPlane,
+ unprojectToLngLat
+} from '../utils/proj.js'
+
+describe('getCentralMeridian', () => {
+ it('computes 3-degree zone', () => {
+ const { zone, meridian } = getCentralMeridian(108.5, 3)
+ expect(zone).toBe(36)
+ expect(meridian).toBe(108)
+ })
+
+ it('computes 6-degree zone', () => {
+ const { zone, meridian } = getCentralMeridian(108.5, 6)
+ expect(zone).toBe(19)
+ expect(meridian).toBe(111)
+ })
+})
+
+describe('projectToPlane / unprojectToLngLat', () => {
+ it('round-trips without zone prefix', () => {
+ const lng = 108.55972199
+ const lat = 22.85808956
+ const projected = projectToPlane(lng, lat, 3, false)
+ const back = unprojectToLngLat(
+ projected.easting,
+ projected.northing,
+ 3,
+ false,
+ projected.zone
+ )
+ expect(back.lng).toBeCloseTo(lng, 6)
+ expect(back.lat).toBeCloseTo(lat, 6)
+ })
+
+ it('round-trips with zone prefix', () => {
+ const lng = 116.397451
+ const lat = 39.909235
+ const projected = projectToPlane(lng, lat, 3, true)
+ expect(String(Math.floor(projected.easting)).length).toBeGreaterThanOrEqual(7)
+ const back = unprojectToLngLat(
+ projected.easting,
+ projected.northing,
+ 3,
+ true
+ )
+ expect(back.lng).toBeCloseTo(lng, 6)
+ expect(back.lat).toBeCloseTo(lat, 6)
+ expect(back.zone).toBe(projected.zone)
+ })
+})
diff --git a/src/components/AmapSearchTool.vue b/src/views/AmapSearchTool.vue
similarity index 97%
rename from src/components/AmapSearchTool.vue
rename to src/views/AmapSearchTool.vue
index 445f05d..2eb8a43 100644
--- a/src/components/AmapSearchTool.vue
+++ b/src/views/AmapSearchTool.vue
@@ -146,8 +146,9 @@
diff --git a/src/components/CoordinatePlotter.vue b/src/views/CoordinatePlotter.vue
similarity index 90%
rename from src/components/CoordinatePlotter.vue
rename to src/views/CoordinatePlotter.vue
index 3cab464..4e1d0a8 100644
--- a/src/components/CoordinatePlotter.vue
+++ b/src/views/CoordinatePlotter.vue
@@ -2,7 +2,7 @@
@@ -47,7 +47,7 @@
:class="['tab-btn', { active: activeTab === tab.key }]"
@click="activeTab = tab.key"
>
- {{ tab.icon }} {{ tab.label }}
+ {{ tab.label }}
@@ -147,11 +147,11 @@