refactor(api): 统一前端API调用使用apiFetch并优化错误处理

refactor: 替换直接fetch调用为apiFetch以统一处理错误和响应
fix(server): 改进QR码验证的错误消息和密码哈希处理
This commit is contained in:
sudomarcma
2025-06-26 11:45:14 +08:00
parent e563f17283
commit 5e3015ba4f
8 changed files with 184 additions and 198 deletions
+31 -51
View File
@@ -223,6 +223,8 @@
<script setup>
import { ref, onMounted, computed } from 'vue'
// CORRECT: Import the apiFetch wrapper
import { apiFetch } from '@/api.js'
// --- STATE ---
const searchQuery = ref('')
@@ -305,42 +307,30 @@ const calendarGrid = computed(() => {
})
const handleSearch = async () => {
// SCENARIO 1: User presses Enter on a highlighted item in the list
if (highlightedIndex.value >= 0 && searchResults.value[highlightedIndex.value]) {
selectWorker(searchResults.value[highlightedIndex.value])
}
// SCENARIO 2: User presses Enter in an empty search box
else if (searchQuery.value === '') {
} else if (searchQuery.value === '') {
await handleSelectAll()
searchResults.value = []
highlightedIndex.value = -1
}
// SCENARIO 3: User types a query and presses Enter for the first time
else {
// This fetches the list of workers so you can navigate it
} else {
await fetchWorkers()
}
}
const fetchWorkers = async (selectAll = false) => {
// ... this function's internal logic remains the same
try {
const res = await fetch(
`${import.meta.env.VITE_API_BASE_URL}/api/managers/workers?search=${searchQuery.value}&limit=1000`,
)
if (res.ok) {
const data = await res.json()
if (selectAll) {
data.workers.forEach((worker) => {
if (!selectedWorkers.value.some((w) => w.id === worker.id)) {
selectedWorkers.value.push(worker)
}
})
} else {
searchResults.value = data.workers
// Set highlighted index to the first item, or -1 if no results
highlightedIndex.value = data.workers.length > 0 ? 0 : -1
}
// CORRECT: Use apiFetch and get data directly
const data = await apiFetch(`/api/managers/workers?search=${searchQuery.value}&limit=1000`)
if (selectAll) {
data.workers.forEach((worker) => {
if (!selectedWorkers.value.some((w) => w.id === worker.id)) {
selectedWorkers.value.push(worker)
}
})
} else {
searchResults.value = data.workers
highlightedIndex.value = data.workers.length > 0 ? 0 : -1
}
} catch (err) {
console.error('Failed to search workers', err)
@@ -361,11 +351,10 @@ const navigateResults = (direction) => {
}
}
const handleSelectAll = async () => {
await fetchWorkers(true) // This function already fetches and selects all workers
isSelectAllActive.value = true // Activate the summary view
await fetchWorkers(true)
isSelectAllActive.value = true
}
// vvv ADD THIS NEW METHOD vvv
const clearAllSelection = () => {
selectedWorkers.value = []
isSelectAllActive.value = false
@@ -375,14 +364,13 @@ const selectWorker = (worker) => {
if (!selectedWorkers.value.some((w) => w.id === worker.id)) {
selectedWorkers.value.push(worker)
}
isSelectAllActive.value = false // Turn off "Select All" mode
clearSearch() // Clear the search box and results after selection
isSelectAllActive.value = false
clearSearch()
}
// vvv MODIFY THIS METHOD vvv
const removeWorker = (workerId) => {
selectedWorkers.value = selectedWorkers.value.filter((w) => w.id !== workerId)
isSelectAllActive.value = false // Deactivate summary view when removing individually
isSelectAllActive.value = false
}
const generateReport = async () => {
@@ -396,16 +384,14 @@ const generateReport = async () => {
overtimeReport.value = null
const workerIds = selectedWorkers.value.map((w) => w.id).join(',')
const url = `${import.meta.env.VITE_API_BASE_URL}/api/managers/attendance-records?workerIds=${workerIds}&startDate=${filters.value.startDate}&endDate=${filters.value.endDate}`
const url = `/api/managers/attendance-records?workerIds=${workerIds}&startDate=${filters.value.startDate}&endDate=${filters.value.endDate}`
try {
const res = await fetch(url)
if (!res.ok) throw new Error('Failed to fetch attendance records')
const fetchedRecords = await res.json()
// CORRECT: Use apiFetch to get records directly
const fetchedRecords = await apiFetch(url)
reportData.value = fetchedRecords
// --- OT CALCULATION FOR UI SUMMARY ---
// OT CALCULATION LOGIC REMAINS THE SAME
const otResults = {}
const hourlyRate = monthlySalary.value / 26 / 8
@@ -433,7 +419,7 @@ const generateReport = async () => {
const otPay = hours * hourlyRate * factor
dailyBreakdown.push({ hours, otPay })
}
clockInTime = null // Reset for the next session
clockInTime = null
}
}
@@ -451,7 +437,7 @@ const generateReport = async () => {
}
}
// --- UPDATED CSV EXPORT METHOD ---
// CSV EXPORT LOGIC REMAINS THE SAME
const exportOtSummaryCsv = () => {
if (reportData.value.length === 0) return
@@ -488,7 +474,7 @@ const exportOtSummaryCsv = () => {
if (!clockInRecord) {
clockInRecord = record
}
row.push('', '') // Push empty cells for hours and wage on clock-in
row.push('', '')
allRows.push(row.join(','))
} else if (record.event_type === 'clock_out') {
if (clockInRecord) {
@@ -503,7 +489,7 @@ const exportOtSummaryCsv = () => {
? overtimeSettings.value.publicHolidayFactor
: overtimeSettings.value.restDayFactor
const otPay = hours * hourlyRate * factor
workerTotalWage += otPay // Accumulate wage for summary
workerTotalWage += otPay
row.push(hours.toFixed(2), otPay.toFixed(2))
} else {
@@ -517,25 +503,19 @@ const exportOtSummaryCsv = () => {
}
}
// Add summary row for the worker
const summaryRow = [`"${worker.full_name} Total"`, '', '', '', '', workerTotalWage.toFixed(2)]
allRows.push(summaryRow.join(','))
// Add empty row for separation
allRows.push('')
}
// Remove the last empty row
if (allRows.length > 0) {
allRows.pop()
}
// --- CHANGED SECTION FOR FILENAME ---
// Create a formatted timestamp for the filename
const now = new Date()
const timestamp = now.toISOString().slice(0, 19).replace('T', '_').replace(/:/g, '-')
const fileName = `${timestamp}_report.csv` // e.g., 2025-06-16_16-46-25_report.csv
// --- END OF CHANGED SECTION ---
const fileName = `${timestamp}_report.csv`
let csvContent = headers.join(',') + '\n' + allRows.join('\n')
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' })
@@ -549,7 +529,7 @@ const exportOtSummaryCsv = () => {
document.body.removeChild(link)
}
// --- CALENDAR METHODS ---
// CALENDAR METHODS REMAIN THE SAME
const changeMonth = (offset) => {
const newDate = new Date(calendarDate.value)
newDate.setMonth(newDate.getMonth() + offset)
@@ -576,7 +556,7 @@ onMounted(() => {
</script>
<style scoped>
/* Add these media queries */
/* STYLES REMAIN UNCHANGED */
@media (max-width: 768px) {
.attendance-reporting-layout {
flex-direction: column;
+32 -36
View File
@@ -44,7 +44,6 @@
<tr v-for="qr in qrCodes" :key="qr.id">
<td>{{ qr.name }}</td>
<td>
<!-- FIX #1: Use qr.is_active instead of qr.isActive -->
<span class="status-badge" :class="qr.is_active ? 'active' : 'inactive'">
{{ qr.is_active ? 'Active' : 'Inactive' }}
</span>
@@ -57,7 +56,6 @@
>
<span></span> Download
</button>
<!-- FIX #1: Use qr.is_active instead of qr.isActive -->
<button @click="toggleQrStatus(qr)" class="button-secondary">
{{ qr.is_active ? 'Deactivate' : 'Activate' }}
</button>
@@ -75,6 +73,7 @@
import { ref, onMounted, nextTick } from 'vue'
import { useRouter } from 'vue-router'
import QRCode from 'qrcode'
import { apiFetch } from '@/api.js'
const router = useRouter()
const qrCodes = ref([])
@@ -93,8 +92,9 @@ onMounted(() => {
const fetchQrCodes = async () => {
try {
const res = await fetch(`${import.meta.env.VITE_API_BASE_URL}/api/managers/qr-codes`)
qrCodes.value = await res.json()
// CORRECT: Get the data directly from apiFetch
const data = await apiFetch('/api/managers/qr-codes')
qrCodes.value = data
} catch (err) {
console.error('Failed to fetch QR codes:', err)
}
@@ -103,26 +103,25 @@ const fetchQrCodes = async () => {
const addQrCode = async () => {
if (!newQrName.value) return
try {
const res = await fetch(`${import.meta.env.VITE_API_BASE_URL}/api/managers/qr-codes`, {
// CORRECT: Get the new QR object directly
const newQr = await apiFetch('/api/managers/qr-codes', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: newQrName.value }),
})
if (res.ok) {
const newQr = await res.json()
qrCodes.value.unshift(newQr)
newlyGeneratedQr.value = newQr
newQrName.value = ''
await nextTick()
QRCode.toCanvas(
newQrCanvas.value,
newQr.id,
{ width: 220, margin: 2, color: { dark: '#050505', light: '#FFFFFF' } },
(error) => {
if (error) console.error(error)
},
)
}
qrCodes.value.unshift(newQr)
newlyGeneratedQr.value = newQr
newQrName.value = ''
await nextTick()
QRCode.toCanvas(
newQrCanvas.value,
newQr.id,
{ width: 220, margin: 2, color: { dark: '#050505', light: '#FFFFFF' } },
(error) => {
if (error) console.error(error)
},
)
} catch (err) {
console.error('Failed to add QR code:', err)
}
@@ -130,19 +129,16 @@ const addQrCode = async () => {
const toggleQrStatus = async (qr) => {
try {
const res = await fetch(`${import.meta.env.VITE_API_BASE_URL}/api/managers/qr-codes/${qr.id}`, {
// CORRECT: No need to check response, catch block will handle errors
await apiFetch(`/api/managers/qr-codes/${qr.id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
// Send the opposite of the current status
body: JSON.stringify({ isActive: !qr.is_active }),
})
if (res.ok) {
// FIX #2: Instead of replacing the object, just update the property.
// This preserves the 'name' and other properties of the object.
const index = qrCodes.value.findIndex((q) => q.id === qr.id)
if (index !== -1) {
qrCodes.value[index].is_active = !qrCodes.value[index].is_active
}
// Update status locally on success
const index = qrCodes.value.findIndex((q) => q.id === qr.id)
if (index !== -1) {
qrCodes.value[index].is_active = !qrCodes.value[index].is_active
}
} catch (err) {
console.error('Failed to update QR status:', err)
@@ -154,12 +150,12 @@ const deleteQrCode = async (id) => {
return
}
try {
const res = await fetch(`${import.meta.env.VITE_API_BASE_URL}/api/managers/qr-codes/${id}`, {
// CORRECT: No need to check response, just await the call
await apiFetch(`/api/managers/qr-codes/${id}`, {
method: 'DELETE',
})
if (res.ok) {
qrCodes.value = qrCodes.value.filter((qr) => qr.id !== id)
}
// Filter out the deleted QR code on success
qrCodes.value = qrCodes.value.filter((qr) => qr.id !== id)
} catch (err) {
console.error('Failed to delete QR code:', err)
}
@@ -187,7 +183,7 @@ const downloadQrCode = async (qr) => {
</script>
<style scoped>
/* Styles remain the same */
/* STYLES REMAIN UNCHANGED */
.qr-management-container {
display: flex;
flex-direction: column;