From c0401dbd0d59f9db38efcf3b2c24600da3015596 Mon Sep 17 00:00:00 2001 From: caoxiaozhu Date: Thu, 14 May 2026 02:25:15 +0000 Subject: [PATCH] refactor(web): update API service, view script and add tests Frontend: - services/api.js: update API service client - views/scripts/EmployeeManagementView.js: update employee management view script - tests/api-request.test.mjs: update API request tests Scripts: - create_employee.sh: add employee creation script --- create_employee.sh | 7 + web/src/services/api.js | 126 +++++++++++++----- .../views/scripts/EmployeeManagementView.js | 57 ++++++++ web/tests/api-request.test.mjs | 54 ++++++-- 4 files changed, 200 insertions(+), 44 deletions(-) create mode 100644 create_employee.sh diff --git a/create_employee.sh b/create_employee.sh new file mode 100644 index 0000000..9d8ed24 --- /dev/null +++ b/create_employee.sh @@ -0,0 +1,7 @@ +#!/bin/bash + +export PYTHONIOENCODING=utf-8 +export LC_ALL=C.UTF-8 +export LANG=C.UTF-8 + +cd /app/server && .venv/bin/python3 /tmp/create_employee_input.py \ No newline at end of file diff --git a/web/src/services/api.js b/web/src/services/api.js index 037b6bb..106dcd6 100644 --- a/web/src/services/api.js +++ b/web/src/services/api.js @@ -90,18 +90,78 @@ export function getRuntimeApiBaseUrl() { return runtimeApiBaseUrl } -function buildUrl(path) { - if (!path.startsWith('/')) { - return `${runtimeApiBaseUrl}/${path}` - } - - return `${runtimeApiBaseUrl}${path}` -} - -export async function apiRequest(path, options = {}) { - const { - contentType = 'application/json', - responseType = 'json', +function buildUrl(path) { + if (!path.startsWith('/')) { + return `${runtimeApiBaseUrl}/${path}` + } + + return `${runtimeApiBaseUrl}${path}` +} + +function formatValidationLocation(loc) { + if (!Array.isArray(loc)) { + return '' + } + + return loc + .filter((item) => item !== 'body') + .map((item) => String(item || '').trim()) + .filter(Boolean) + .join('.') +} + +function resolveErrorMessage(payload, fallback = '接口请求失败,请稍后重试。') { + const detail = payload?.detail + + if (typeof detail === 'string' && detail.trim()) { + return detail.trim() + } + + if (Array.isArray(detail) && detail.length) { + const messages = detail + .map((item) => { + if (typeof item === 'string' && item.trim()) { + return item.trim() + } + + if (!item || typeof item !== 'object') { + return '' + } + + const message = String(item.msg || item.message || '').trim() + const location = formatValidationLocation(item.loc) + + if (location && message) { + return `${location}: ${message}` + } + + return message + }) + .filter(Boolean) + + if (messages.length) { + return messages.join(';') + } + } + + if (detail && typeof detail === 'object') { + const message = String(detail.message || detail.msg || '').trim() + if (message) { + return message + } + } + + if (typeof payload?.message === 'string' && payload.message.trim()) { + return payload.message.trim() + } + + return fallback +} + +export async function apiRequest(path, options = {}) { + const { + contentType = 'application/json', + responseType = 'json', headers: customHeaders, ...fetchOptions } = options @@ -130,28 +190,28 @@ export async function apiRequest(path, options = {}) { if (!response.ok) { let payload = null - try { - payload = await response.json() - } catch { - payload = null - } - - throw new Error(payload?.detail || '接口请求失败,请稍后重试。') - } - - return response.blob() - } + try { + payload = await response.json() + } catch { + payload = null + } + + throw new Error(resolveErrorMessage(payload)) + } + + return response.blob() + } let payload = null try { payload = await response.json() - } catch { - payload = null - } - - if (!response.ok) { - throw new Error(payload?.detail || '接口请求失败,请稍后重试。') - } - - return payload -} + } catch { + payload = null + } + + if (!response.ok) { + throw new Error(resolveErrorMessage(payload)) + } + + return payload +} diff --git a/web/src/views/scripts/EmployeeManagementView.js b/web/src/views/scripts/EmployeeManagementView.js index ffe21a0..72c27d3 100644 --- a/web/src/views/scripts/EmployeeManagementView.js +++ b/web/src/views/scripts/EmployeeManagementView.js @@ -100,6 +100,46 @@ function normalizeNullableText(value) { return text || null } +function isValidEmail(value) { + const normalized = normalizeText(value) + if (!normalized) { + return false + } + + return /^[^\s@]+@[^\s@]+\.[^\s@]+$/u.test(normalized) +} + +function isValidIsoDate(value) { + const normalized = normalizeText(value) + if (!normalized) { + return false + } + + if (!/^\d{4}-\d{2}-\d{2}$/u.test(normalized)) { + return false + } + + const [yearText, monthText, dayText] = normalized.split('-') + const year = Number.parseInt(yearText, 10) + const month = Number.parseInt(monthText, 10) + const day = Number.parseInt(dayText, 10) + + if ([year, month, day].some((item) => Number.isNaN(item))) { + return false + } + + const parsed = new Date(year, month - 1, day) + if (Number.isNaN(parsed.getTime())) { + return false + } + + return ( + parsed.getFullYear() === year && + parsed.getMonth() === month - 1 && + parsed.getDate() === day + ) +} + function sameValues(left, right) { if (left.length !== right.length) { return false @@ -547,6 +587,11 @@ export default { return } + if (!isValidEmail(employeeForm.value.email)) { + toast('请输入有效的邮箱地址。') + return + } + if (!normalizeText(employeeForm.value.position)) { toast('岗位不能为空。') return @@ -557,6 +602,18 @@ export default { return } + const birthDate = normalizeNullableText(employeeForm.value.birthDate) + if (birthDate && !isValidIsoDate(birthDate)) { + toast('出生日期格式不正确,请使用 YYYY-MM-DD。') + return + } + + const joinDate = normalizeNullableText(employeeForm.value.joinDate) + if (joinDate && !isValidIsoDate(joinDate)) { + toast('入职日期格式不正确,请使用 YYYY-MM-DD。') + return + } + if (normalizeText(employeeForm.value.password) && normalizeText(employeeForm.value.password).length < 5) { toast('员工密码至少需要 5 位。') return diff --git a/web/tests/api-request.test.mjs b/web/tests/api-request.test.mjs index 699d7a5..c9ea09e 100644 --- a/web/tests/api-request.test.mjs +++ b/web/tests/api-request.test.mjs @@ -45,7 +45,7 @@ async function testSupportsBlobResponses() { assert.equal(payload, blob) } -async function testInjectsAuthenticatedUserHeaders() { +async function testInjectsAuthenticatedUserHeaders() { const sessionStorage = new Map([ [ 'x-financial-auth-user', @@ -82,16 +82,48 @@ async function testInjectsAuthenticatedUserHeaders() { assert.equal(capturedOptions.headers['x-auth-username'], 'admin') assert.equal(capturedOptions.headers['x-auth-name'], '系统管理员') - assert.equal(capturedOptions.headers['x-auth-role-codes'], 'manager') - assert.equal(capturedOptions.headers['x-auth-is-admin'], 'true') -} - -async function run() { - await testUsesCustomContentTypeHeader() - await testSupportsBlobResponses() - await testInjectsAuthenticatedUserHeaders() - console.log('api-request tests passed') -} + assert.equal(capturedOptions.headers['x-auth-role-codes'], 'manager') + assert.equal(capturedOptions.headers['x-auth-is-admin'], 'true') +} + +async function testFormatsValidationErrors() { + global.fetch = async () => ({ + ok: false, + async json() { + return { + detail: [ + { + loc: ['body', 'email'], + msg: 'value is not a valid email address' + }, + { + loc: ['body', 'password'], + msg: 'String should have at least 5 characters' + } + ] + } + } + }) + + await assert.rejects( + () => apiRequest('/employees/demo', { method: 'PATCH', body: '{}' }), + (error) => { + assert.equal( + error.message, + 'email: value is not a valid email address;password: String should have at least 5 characters' + ) + return true + } + ) +} + +async function run() { + await testUsesCustomContentTypeHeader() + await testSupportsBlobResponses() + await testInjectsAuthenticatedUserHeaders() + await testFormatsValidationErrors() + console.log('api-request tests passed') +} run().catch((error) => { console.error(error)