diff --git a/api/auth.ts b/api/auth.ts new file mode 100644 index 0000000..877bd2b --- /dev/null +++ b/api/auth.ts @@ -0,0 +1,113 @@ +let apiBaseUrl = 'http://192.168.2.76:8000/api' + +// #ifdef H5 +apiBaseUrl = '/backend-api' +// #endif + +export const API_BASE_URL = apiBaseUrl + +export type SendCodePayload = { + phone: string + scene: 'register' | 'login' | 'reset' +} + +export type SendCodeResponse = { + message: string +} + +export type LoginCodePayload = { + phone: string + code: string +} + +export type BackendUser = { + id: number + username: string + phone: string + real_name: string + role_type: string + institution: string | null + department: string | null +} + +export type LoginResponse = { + message: string + user?: Partial & Record + tokens: { + access: string + refresh: string + } +} + +export class ApiRequestError extends Error { + code?: string + statusCode?: number + + constructor(message: string, code?: string, statusCode?: number) { + super(message) + this.name = 'ApiRequestError' + this.code = code + this.statusCode = statusCode + } +} + +function readErrorMessage(data: unknown, fallback: string) { + if (data && typeof data === 'object') { + const payload = data as Record + const message = payload.message || payload.detail || payload.error + if (typeof message === 'string' && message.trim()) return message + } + return fallback +} + +function request(url: string, data: unknown): Promise { + return new Promise((resolve, reject) => { + uni.request({ + url: `${API_BASE_URL}${url}`, + method: 'POST', + timeout: 10000, + header: { + 'Content-Type': 'application/json' + }, + data, + success: response => { + if (response.statusCode >= 200 && response.statusCode < 300) { + resolve(response.data as T) + return + } + const payload = response.data as Record | undefined + const code = typeof payload?.code === 'string' ? payload.code : undefined + reject(new ApiRequestError(readErrorMessage(response.data, `请求失败(${response.statusCode})`), code, response.statusCode)) + }, + fail: error => { + reject(new ApiRequestError(error.errMsg || '无法连接服务')) + } + }) + }) +} + +function isLoginResponse(data: unknown): data is LoginResponse { + if (!data || typeof data !== 'object') return false + const payload = data as Partial + const tokens = payload.tokens as Partial | undefined + + return Boolean( + payload.tokens && + typeof tokens?.access === 'string' && + typeof tokens?.refresh === 'string' + ) +} + +export function sendLoginCode(phone: string, scene: SendCodePayload['scene'] = 'login'): Promise { + return request('/user/auth/send-code/', { + phone, + scene + }) +} + +export function loginWithCode(payload: LoginCodePayload): Promise { + return request('/user/auth/login-code/', payload).then(response => { + if (isLoginResponse(response)) return response + throw new Error('登录接口返回数据格式异常') + }) +} diff --git a/manifest.json b/manifest.json index 381f8bb..4d1491d 100644 --- a/manifest.json +++ b/manifest.json @@ -68,5 +68,18 @@ "uniStatistics" : { "enable" : false }, + "h5" : { + "devServer" : { + "proxy" : { + "/backend-api" : { + "target" : "http://192.168.2.76:8000", + "changeOrigin" : true, + "pathRewrite" : { + "^/backend-api" : "/api" + } + } + } + } + }, "vueVersion" : "3" } diff --git a/pages/config/config.vue b/pages/config/config.vue index e8df469..0c00620 100644 --- a/pages/config/config.vue +++ b/pages/config/config.vue @@ -114,9 +114,10 @@ type OptionGroup = keyof ConfigOptions type SaveState = 'idle' | 'saved' type LoginUser = { - id?: string + id?: string | number phone?: string institutionId?: string + institution?: string | null } const form = reactive({ @@ -229,9 +230,9 @@ function chooseOption(option: ConfigOption) { function handleSubmit() { const user = (uni.getStorageSync('clinical-thinking-user') || {}) as LoginUser const payload: ClinicalConfigPayload = { - userId: user.id || 'mock-user-guest', + userId: user.id ? String(user.id) : 'mock-user-guest', phone: user.phone || '', - institutionId: user.institutionId || '', + institutionId: user.institutionId || user.institution || '', department: form.department, title: form.title, experience: form.experience, diff --git a/pages/index/index.vue b/pages/index/index.vue index 49e5715..881039b 100644 --- a/pages/index/index.vue +++ b/pages/index/index.vue @@ -11,117 +11,130 @@ @open-profile="showProfilePage = true" /> - - - 临床思维训练 - 提升临床决策能力的高效平台 - - - - - 手机号码 - - +86 - - - + + + + 临床思维训练 + 提升临床决策能力的高效平台 - - 验证码 - - - + + + 手机号码 + + +86 + - + + + + + 所属机构 + + + {{ selectedInstitution ? selectedInstitution.name : '选择医院或医学院校' }} + + + + + + * 未注册手机号登录时将自动创建账号 + + - - 所属机构 - - - {{ selectedInstitution ? selectedInstitution.name : '选择医院或医学院校' }} - - - - - - * 未注册手机号登录时将自动创建账号 - - - - - - - - - 我已阅读并同意 - 《用户服务协议》 - - 《隐私保护政策》 - - - - - - {{ toastMessage }} - - - - - 选择所属机构 - 关闭 - - - - - {{ institution.name }} - {{ institution.city }} · {{ institution.typeName }} + + + + + 我已阅读并同意 + 《用户服务协议》 + + 《隐私保护政策》 - - + + + + {{ toastMessage }} + + + + + 选择所属机构 + 关闭 + + + + + {{ institution.name }} + {{ institution.city }} · {{ institution.typeName }} + + + + + - @@ -452,33 +473,17 @@ page { min-width: 0; } -.shield-icon { - position: relative; +.verified-icon { flex: 0 0 auto; - box-sizing: border-box; - width: 22px; - height: 24px; + width: 28px; + height: 28px; margin-right: 12px; - border: 3px solid #727783; - border-radius: 7px 7px 10px 10px; -} - -.shield-icon::after { - content: ''; - position: absolute; - left: 5px; - top: 5px; - width: 8px; - height: 5px; - border-left: 2px solid #727783; - border-bottom: 2px solid #727783; - transform: rotate(-45deg); + background: center / 24px 24px no-repeat url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='28' height='28' viewBox='0 0 24 24' fill='none' stroke='%23727783' stroke-width='2.3' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M12 3l7 3v5c0 4.5-3 7.8-7 10-4-2.2-7-5.5-7-10V6l7-3z'/%3E%3Cpath d='M9 12l2 2 4-4'/%3E%3C/svg%3E"); } .code-button { box-sizing: border-box; flex: 0 0 auto; - width: 124px; height: 56px; padding: 0 16px; border: 1px solid #00478d; @@ -507,6 +512,7 @@ page { } .select-wrap { + position: relative; justify-content: space-between; } @@ -525,24 +531,12 @@ page { color: rgba(114, 119, 131, 0.6); } -.expand-icon { - position: relative; +.select-arrow { flex: 0 0 auto; - width: 24px; - height: 24px; + width: 28px; + height: 28px; margin-left: 12px; -} - -.expand-icon::before { - content: ''; - position: absolute; - left: 6px; - top: 7px; - width: 10px; - height: 10px; - border-right: 2px solid #727783; - border-bottom: 2px solid #727783; - transform: rotate(45deg); + background: center / 28px 28px no-repeat url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='28' height='28' viewBox='0 0 24 24' fill='none' stroke='%23727783' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M7 8l5 5 5-5'/%3E%3Cpath d='M7 12l5 5 5-5'/%3E%3C/svg%3E"); } .hint { @@ -556,10 +550,13 @@ page { letter-spacing: 0; } +.login-area { + padding-top: 16px; +} + .login-button { width: 100%; height: 56px; - margin-top: 16px; border-radius: 8px; background: #00478d; box-shadow: 0 4px 12px rgba(0, 71, 141, 0.2); diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..217d89d --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,15 @@ +import { defineConfig } from 'vite' +import uni from '@dcloudio/vite-plugin-uni' + +export default defineConfig({ + plugins: [uni()], + server: { + proxy: { + '/backend-api': { + target: 'http://192.168.2.76:8000', + changeOrigin: true, + rewrite: path => path.replace(/^\/backend-api/, '/api') + } + } + } +})