870 lines
19 KiB
Vue
870 lines
19 KiB
Vue
<template>
|
|
<view class="page-root">
|
|
<ProfilePage
|
|
v-if="showProfilePage"
|
|
@open-settings="openSettings"
|
|
@go-home="showProfilePage = false"
|
|
/>
|
|
<ConfigPage
|
|
v-if="showConfigPage"
|
|
v-show="!showProfilePage"
|
|
@open-profile="showProfilePage = true"
|
|
/>
|
|
<view v-if="!showConfigPage && !showProfilePage" class="auth-page">
|
|
<view class="auth-container">
|
|
<view class="header">
|
|
<image
|
|
class="logo"
|
|
mode="aspectFit"
|
|
src="/static/logo.png"
|
|
/>
|
|
<text class="title">临床思维训练</text>
|
|
<text class="subtitle">提升临床决策能力的高效平台</text>
|
|
</view>
|
|
|
|
<view class="form">
|
|
<view class="form-group">
|
|
<text class="label">手机号码</text>
|
|
<view class="input-wrap" :class="{ focused: activeField === 'phone' }">
|
|
<text class="country-code">+86</text>
|
|
<view class="separator"></view>
|
|
<input
|
|
class="input"
|
|
type="number"
|
|
maxlength="11"
|
|
v-model="form.phone"
|
|
placeholder="请输入手机号"
|
|
placeholder-class="placeholder"
|
|
@focus="activeField = 'phone'"
|
|
@blur="activeField = ''"
|
|
@confirm="handleLogin"
|
|
/>
|
|
</view>
|
|
</view>
|
|
|
|
<view class="form-group">
|
|
<text class="label">验证码</text>
|
|
<view class="code-row">
|
|
<view class="input-wrap code-input-wrap" :class="{ focused: activeField === 'code' }">
|
|
<view class="verified-icon" aria-hidden="true"></view>
|
|
<input
|
|
class="input"
|
|
type="number"
|
|
maxlength="6"
|
|
v-model="form.code"
|
|
placeholder="短信验证码"
|
|
placeholder-class="placeholder"
|
|
@focus="activeField = 'code'"
|
|
@blur="activeField = ''"
|
|
@confirm="handleLogin"
|
|
/>
|
|
</view>
|
|
<button
|
|
class="code-button"
|
|
:class="{ disabled: isCounting || sendingCode }"
|
|
:disabled="isCounting || sendingCode"
|
|
@click="handleSendCode"
|
|
>
|
|
{{ codeButtonText }}
|
|
</button>
|
|
</view>
|
|
</view>
|
|
|
|
<view class="form-group">
|
|
<text class="label">所属机构</text>
|
|
<view class="select-wrap" @click="institutionPickerVisible = true">
|
|
<text class="select-text" :class="{ muted: !selectedInstitution }">
|
|
{{ selectedInstitution ? selectedInstitution.name : '选择医院或医学院校' }}
|
|
</text>
|
|
<view class="select-arrow" aria-hidden="true"></view>
|
|
</view>
|
|
</view>
|
|
|
|
<text class="hint">* 未注册手机号登录时将自动创建账号</text>
|
|
|
|
<view class="login-area">
|
|
<button class="login-button" :class="{ loading: submitting }" :disabled="submitting" @click="handleLogin">
|
|
<view v-if="submitting" class="loading-spinner"></view>
|
|
<text v-else>登录 / 注册</text>
|
|
</button>
|
|
</view>
|
|
</view>
|
|
|
|
<view class="footer">
|
|
<view class="agreement" :class="{ shake: agreementShake }" @click="toggleAgreement">
|
|
<view class="checkbox" :class="{ checked: agreed }"></view>
|
|
<view class="agreement-copy">
|
|
<text class="agreement-text">我已阅读并同意 </text>
|
|
<text class="agreement-link" @click.stop="openAgreement('service')">《用户服务协议》</text>
|
|
<text class="agreement-text"> 与 </text>
|
|
<text class="agreement-link" @click.stop="openAgreement('privacy')">《隐私保护政策》</text>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
|
|
<view class="toast" :class="{ visible: toastVisible }">{{ toastMessage }}</view>
|
|
|
|
<view v-if="institutionPickerVisible" class="picker-mask" @click="institutionPickerVisible = false">
|
|
<view class="picker-panel" @click.stop>
|
|
<view class="picker-header">
|
|
<text class="picker-title">选择所属机构</text>
|
|
<text class="picker-close" @click="institutionPickerVisible = false">关闭</text>
|
|
</view>
|
|
<scroll-view class="institution-list" scroll-y>
|
|
<view v-if="institutionLoading" class="institution-empty">机构列表加载中...</view>
|
|
<view v-else-if="institutions.length === 0" class="institution-empty">暂无可选机构</view>
|
|
<view
|
|
v-else
|
|
v-for="institution in institutions"
|
|
:key="institution.id"
|
|
class="institution-item"
|
|
:class="{ active: form.institutionId === institution.id }"
|
|
@click="chooseInstitution(institution)"
|
|
>
|
|
<view class="institution-copy">
|
|
<text class="institution-name">{{ institution.name }}</text>
|
|
<text class="institution-meta">{{ institution.city }} · {{ institution.typeName }}</text>
|
|
</view>
|
|
<view v-if="form.institutionId === institution.id" class="selected-mark"></view>
|
|
</view>
|
|
</scroll-view>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { computed, nextTick, onMounted, onUnmounted, reactive, ref } from 'vue'
|
|
import { ApiRequestError, fetchInstitutions, loginWithCode, sendLoginCode, type InstitutionRecord } from '../../api/auth'
|
|
import ConfigPage from '../config/config.vue'
|
|
import ProfilePage from '../profile/profile.vue'
|
|
|
|
type Institution = {
|
|
id: string
|
|
code: string
|
|
name: string
|
|
city: string
|
|
typeName: string
|
|
}
|
|
|
|
const institutions = ref<Institution[]>([])
|
|
|
|
const form = reactive({
|
|
phone: '',
|
|
code: '',
|
|
institutionId: ''
|
|
})
|
|
|
|
const activeField = ref('')
|
|
const agreed = ref(false)
|
|
const agreementShake = ref(false)
|
|
const sendingCode = ref(false)
|
|
const submitting = ref(false)
|
|
const countdown = ref(0)
|
|
const toastMessage = ref('')
|
|
const toastVisible = ref(false)
|
|
const showConfigPage = ref(false)
|
|
const showProfilePage = ref(false)
|
|
const institutionPickerVisible = ref(false)
|
|
const institutionLoading = ref(false)
|
|
const institutionLoaded = ref(false)
|
|
|
|
let countdownTimer: ReturnType<typeof setInterval> | null = null
|
|
let toastTimer: ReturnType<typeof setTimeout> | null = null
|
|
|
|
const selectedInstitution = computed(() => {
|
|
return institutions.value.find(item => item.id === form.institutionId)
|
|
})
|
|
|
|
const isCounting = computed(() => countdown.value > 0)
|
|
|
|
const codeButtonText = computed(() => {
|
|
if (sendingCode.value) return '发送中...'
|
|
return isCounting.value ? `${countdown.value}s` : '获取验证码'
|
|
})
|
|
|
|
function chooseInstitution(institution: Institution) {
|
|
form.institutionId = institution.id
|
|
institutionPickerVisible.value = false
|
|
}
|
|
|
|
function normalizeInstitution(item: InstitutionRecord): Institution {
|
|
const typeNameMap: Record<string, string> = {
|
|
hospital: '医院',
|
|
college: '医学院校',
|
|
school: '学校',
|
|
clinic: '诊所'
|
|
}
|
|
const region = [item.province, item.city].filter(Boolean).join(' · ')
|
|
return {
|
|
id: String(item.id),
|
|
code: item.code,
|
|
name: item.name,
|
|
city: region || '其他',
|
|
typeName: typeNameMap[item.type] || '机构'
|
|
}
|
|
}
|
|
|
|
async function loadInstitutions() {
|
|
institutionLoading.value = true
|
|
try {
|
|
const result = await fetchInstitutions()
|
|
institutions.value = result.map(normalizeInstitution)
|
|
} catch (error) {
|
|
showToast(error instanceof Error ? error.message : '机构列表加载失败')
|
|
institutions.value = []
|
|
} finally {
|
|
institutionLoading.value = false
|
|
institutionLoaded.value = true
|
|
}
|
|
}
|
|
|
|
function toggleAgreement() {
|
|
agreed.value = !agreed.value
|
|
}
|
|
|
|
async function handleSendCode() {
|
|
if (sendingCode.value || isCounting.value) return
|
|
if (!validatePhone()) return
|
|
|
|
sendingCode.value = true
|
|
try {
|
|
let result
|
|
try {
|
|
result = await sendLoginCode(form.phone)
|
|
} catch (error) {
|
|
if (error instanceof ApiRequestError && error.code === 'AUTH_PHONE_NOT_FOUND') {
|
|
result = await sendLoginCode(form.phone, 'register')
|
|
} else {
|
|
throw error
|
|
}
|
|
}
|
|
showToast(result.message || '验证码已发送')
|
|
startCountdown()
|
|
} catch (error) {
|
|
showToast(error instanceof Error ? error.message : '验证码发送失败')
|
|
} finally {
|
|
sendingCode.value = false
|
|
}
|
|
}
|
|
|
|
onMounted(() => {
|
|
void loadInstitutions()
|
|
})
|
|
|
|
function handleLogin() {
|
|
if (submitting.value) return
|
|
if (!validatePhone()) return
|
|
if (!form.code.trim()) {
|
|
showToast('请输入短信验证码')
|
|
return
|
|
}
|
|
if (!selectedInstitution.value) {
|
|
showToast('请选择所属机构')
|
|
return
|
|
}
|
|
if (!agreed.value) {
|
|
showToast('请阅读并勾选用户协议')
|
|
triggerAgreementShake()
|
|
return
|
|
}
|
|
|
|
submitting.value = true
|
|
loginWithCode({
|
|
phone: form.phone,
|
|
code: form.code,
|
|
institution_code: selectedInstitution.value.code,
|
|
institution_name: selectedInstitution.value.name
|
|
}).then(result => {
|
|
const backendUser = result.user || {}
|
|
const normalizedUser = {
|
|
...backendUser,
|
|
id: backendUser.id ? String(backendUser.id) : '',
|
|
phone: backendUser.phone || form.phone,
|
|
institutionId: selectedInstitution.value?.code || '',
|
|
institutionCode: selectedInstitution.value?.code || '',
|
|
institutionName: selectedInstitution.value?.name || ''
|
|
}
|
|
|
|
uni.setStorageSync('clinical-thinking-user', normalizedUser)
|
|
uni.setStorageSync('clinical-thinking-tokens', result.tokens)
|
|
uni.setStorageSync('clinical-thinking-access-token', result.tokens.access)
|
|
uni.setStorageSync('clinical-thinking-refresh-token', result.tokens.refresh)
|
|
showToast(result.message || '正在进入系统...')
|
|
showConfigPage.value = true
|
|
}).catch(error => {
|
|
showToast(error instanceof Error ? error.message : '登录失败,请稍后重试')
|
|
}).finally(() => {
|
|
submitting.value = false
|
|
})
|
|
}
|
|
|
|
function startCountdown() {
|
|
countdown.value = 60
|
|
clearCountdownTimer()
|
|
countdownTimer = setInterval(() => {
|
|
countdown.value -= 1
|
|
if (countdown.value <= 0) {
|
|
clearCountdownTimer()
|
|
}
|
|
}, 1000)
|
|
}
|
|
|
|
function validatePhone() {
|
|
if (!/^1[3-9]\d{9}$/.test(form.phone)) {
|
|
showToast('请输入正确的手机号')
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
function openAgreement(type: 'service' | 'privacy') {
|
|
uni.showModal({
|
|
title: type === 'service' ? '用户服务协议' : '隐私保护政策',
|
|
content: '这里是前端模拟协议内容,后续可替换为正式协议页面或富文本接口。',
|
|
showCancel: false,
|
|
confirmColor: '#00478d'
|
|
})
|
|
}
|
|
|
|
function openSettings() {
|
|
showProfilePage.value = false
|
|
showConfigPage.value = true
|
|
}
|
|
|
|
function triggerAgreementShake() {
|
|
agreementShake.value = false
|
|
nextTick(() => {
|
|
agreementShake.value = true
|
|
setTimeout(() => {
|
|
agreementShake.value = false
|
|
}, 500)
|
|
})
|
|
}
|
|
|
|
function showToast(message: string) {
|
|
if (toastTimer) clearTimeout(toastTimer)
|
|
toastMessage.value = message
|
|
toastVisible.value = true
|
|
uni.showToast({
|
|
title: message,
|
|
icon: 'none'
|
|
})
|
|
toastTimer = setTimeout(() => {
|
|
toastVisible.value = false
|
|
}, 2500)
|
|
}
|
|
|
|
function clearCountdownTimer() {
|
|
if (countdownTimer) {
|
|
clearInterval(countdownTimer)
|
|
countdownTimer = null
|
|
}
|
|
}
|
|
|
|
onUnmounted(() => {
|
|
clearCountdownTimer()
|
|
if (toastTimer) clearTimeout(toastTimer)
|
|
})
|
|
</script>
|
|
|
|
<style>
|
|
page {
|
|
min-height: 100%;
|
|
background: #f9f9ff;
|
|
}
|
|
|
|
.page-root {
|
|
min-height: 100vh;
|
|
background: #f9f9ff;
|
|
}
|
|
|
|
.auth-page {
|
|
min-height: 100vh;
|
|
background: #f9f9ff;
|
|
color: #191c21;
|
|
font-family: Inter, -apple-system, BlinkMacSystemFont, 'PingFang SC', 'Helvetica Neue', Arial, sans-serif;
|
|
-webkit-tap-highlight-color: transparent;
|
|
}
|
|
|
|
.auth-container {
|
|
box-sizing: border-box;
|
|
width: 100%;
|
|
max-width: 448px;
|
|
min-height: 100vh;
|
|
margin: 0 auto;
|
|
padding: 32px 20px 24px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
|
|
.header {
|
|
margin: 24px 0;
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
text-align: center;
|
|
}
|
|
|
|
.logo {
|
|
width: 96px;
|
|
height: 96px;
|
|
margin-bottom: 16px;
|
|
}
|
|
|
|
.title {
|
|
color: #00478d;
|
|
font-size: 24px;
|
|
line-height: 32px;
|
|
font-weight: 600;
|
|
letter-spacing: 0;
|
|
}
|
|
|
|
.subtitle {
|
|
margin-top: 8px;
|
|
color: #727783;
|
|
font-size: 14px;
|
|
line-height: 20px;
|
|
font-weight: 600;
|
|
letter-spacing: 0;
|
|
}
|
|
|
|
.form {
|
|
flex: 1;
|
|
}
|
|
|
|
.form-group {
|
|
margin-bottom: 16px;
|
|
}
|
|
|
|
.label {
|
|
display: block;
|
|
margin: 0 0 8px 4px;
|
|
color: #424752;
|
|
font-size: 12px;
|
|
line-height: 16px;
|
|
font-weight: 500;
|
|
letter-spacing: 0;
|
|
}
|
|
|
|
.input-wrap,
|
|
.select-wrap {
|
|
box-sizing: border-box;
|
|
width: 100%;
|
|
height: 56px;
|
|
padding: 0 16px;
|
|
border: 1px solid #c2c6d4;
|
|
border-radius: 8px;
|
|
background: #f9f9ff;
|
|
display: flex;
|
|
align-items: center;
|
|
transition: border-color 0.18s ease, box-shadow 0.18s ease, transform 0.18s ease;
|
|
}
|
|
|
|
.input-wrap.focused,
|
|
.select-wrap:active {
|
|
border-color: #00478d;
|
|
box-shadow: 0 0 0 1px #00478d;
|
|
}
|
|
|
|
.country-code {
|
|
margin-right: 4px;
|
|
color: #191c21;
|
|
font-size: 16px;
|
|
line-height: 24px;
|
|
font-weight: 400;
|
|
}
|
|
|
|
.separator {
|
|
width: 1px;
|
|
height: 24px;
|
|
margin: 0 12px;
|
|
background: #c2c6d4;
|
|
}
|
|
|
|
.input {
|
|
flex: 1;
|
|
min-width: 0;
|
|
height: 54px;
|
|
padding: 0;
|
|
border: 0;
|
|
background: transparent;
|
|
color: #191c21;
|
|
font-size: 16px;
|
|
line-height: 24px;
|
|
}
|
|
|
|
.placeholder {
|
|
color: rgba(114, 119, 131, 0.6);
|
|
}
|
|
|
|
.code-row {
|
|
display: flex;
|
|
align-items: center;
|
|
column-gap: 8px;
|
|
}
|
|
|
|
.code-input-wrap {
|
|
flex: 1;
|
|
min-width: 0;
|
|
}
|
|
|
|
.verified-icon {
|
|
flex: 0 0 auto;
|
|
width: 28px;
|
|
height: 28px;
|
|
margin-right: 12px;
|
|
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;
|
|
height: 56px;
|
|
padding: 0 16px;
|
|
border: 1px solid #00478d;
|
|
border-radius: 8px;
|
|
background: #f9f9ff;
|
|
color: #00478d;
|
|
font-size: 14px;
|
|
line-height: 54px;
|
|
font-weight: 600;
|
|
white-space: nowrap;
|
|
transition: opacity 0.18s ease, transform 0.18s ease, background 0.18s ease;
|
|
}
|
|
|
|
.code-button:active,
|
|
.login-button:active {
|
|
transform: scale(0.98);
|
|
}
|
|
|
|
.code-button.disabled {
|
|
opacity: 0.5;
|
|
}
|
|
|
|
.code-button::after,
|
|
.login-button::after {
|
|
border: 0;
|
|
}
|
|
|
|
.select-wrap {
|
|
position: relative;
|
|
justify-content: space-between;
|
|
}
|
|
|
|
.select-text {
|
|
flex: 1;
|
|
min-width: 0;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
color: #191c21;
|
|
font-size: 16px;
|
|
line-height: 24px;
|
|
}
|
|
|
|
.select-text.muted {
|
|
color: rgba(114, 119, 131, 0.6);
|
|
}
|
|
|
|
.select-arrow {
|
|
flex: 0 0 auto;
|
|
width: 28px;
|
|
height: 28px;
|
|
margin-left: 12px;
|
|
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 {
|
|
display: block;
|
|
padding: 4px 0;
|
|
text-align: center;
|
|
color: #727783;
|
|
font-size: 12px;
|
|
line-height: 16px;
|
|
font-weight: 500;
|
|
letter-spacing: 0;
|
|
}
|
|
|
|
.login-area {
|
|
padding-top: 16px;
|
|
}
|
|
|
|
.login-button {
|
|
width: 100%;
|
|
height: 56px;
|
|
border-radius: 8px;
|
|
background: #00478d;
|
|
box-shadow: 0 4px 12px rgba(0, 71, 141, 0.2);
|
|
color: #ffffff;
|
|
font-size: 20px;
|
|
line-height: 56px;
|
|
font-weight: 600;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
transition: background 0.18s ease, transform 0.18s ease, opacity 0.18s ease;
|
|
}
|
|
|
|
.login-button.loading {
|
|
opacity: 0.9;
|
|
}
|
|
|
|
.loading-spinner {
|
|
width: 24px;
|
|
height: 24px;
|
|
border: 3px solid rgba(255, 255, 255, 0.36);
|
|
border-top-color: #ffffff;
|
|
border-radius: 50%;
|
|
animation: spin 1s linear infinite;
|
|
}
|
|
|
|
.footer {
|
|
margin-top: 24px;
|
|
padding-bottom: env(safe-area-inset-bottom);
|
|
display: flex;
|
|
justify-content: center;
|
|
}
|
|
|
|
.agreement {
|
|
max-width: 280px;
|
|
display: flex;
|
|
align-items: flex-start;
|
|
}
|
|
|
|
.checkbox {
|
|
position: relative;
|
|
box-sizing: border-box;
|
|
flex: 0 0 auto;
|
|
width: 16px;
|
|
height: 16px;
|
|
margin-top: 4px;
|
|
margin-right: 8px;
|
|
border: 1px solid #c2c6d4;
|
|
border-radius: 4px;
|
|
background: #ffffff;
|
|
}
|
|
|
|
.checkbox.checked {
|
|
border-color: #00478d;
|
|
background: #00478d;
|
|
}
|
|
|
|
.checkbox.checked::after {
|
|
content: '';
|
|
position: absolute;
|
|
left: 4px;
|
|
top: 1px;
|
|
width: 5px;
|
|
height: 9px;
|
|
border-right: 2px solid #ffffff;
|
|
border-bottom: 2px solid #ffffff;
|
|
transform: rotate(45deg);
|
|
}
|
|
|
|
.agreement-copy {
|
|
flex: 1;
|
|
font-size: 0;
|
|
line-height: 0;
|
|
}
|
|
|
|
.agreement-text,
|
|
.agreement-link {
|
|
font-size: 12px;
|
|
line-height: 20px;
|
|
font-weight: 500;
|
|
letter-spacing: 0;
|
|
}
|
|
|
|
.agreement-text {
|
|
color: #424752;
|
|
}
|
|
|
|
.agreement-link {
|
|
color: #00478d;
|
|
}
|
|
|
|
.toast {
|
|
position: fixed;
|
|
left: 50%;
|
|
bottom: 128px;
|
|
z-index: 100;
|
|
max-width: 320px;
|
|
padding: 12px 24px;
|
|
border-radius: 999px;
|
|
background: #2e3037;
|
|
color: #eff0f8;
|
|
font-size: 14px;
|
|
line-height: 20px;
|
|
font-weight: 600;
|
|
text-align: center;
|
|
pointer-events: none;
|
|
opacity: 0;
|
|
transform: translate(-50%, 16px);
|
|
transition: opacity 0.3s ease, transform 0.3s ease;
|
|
}
|
|
|
|
.toast.visible {
|
|
opacity: 1;
|
|
transform: translate(-50%, 0);
|
|
}
|
|
|
|
.picker-mask {
|
|
position: fixed;
|
|
left: 0;
|
|
right: 0;
|
|
top: 0;
|
|
bottom: 0;
|
|
z-index: 90;
|
|
background: rgba(25, 28, 33, 0.38);
|
|
display: flex;
|
|
align-items: flex-end;
|
|
justify-content: center;
|
|
}
|
|
|
|
.picker-panel {
|
|
box-sizing: border-box;
|
|
width: 100%;
|
|
max-width: 448px;
|
|
max-height: 72vh;
|
|
padding: 16px 20px calc(16px + env(safe-area-inset-bottom));
|
|
border-radius: 16px 16px 0 0;
|
|
background: #ffffff;
|
|
box-shadow: 0 -12px 30px rgba(25, 28, 33, 0.16);
|
|
}
|
|
|
|
.picker-header {
|
|
height: 40px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
}
|
|
|
|
.picker-title {
|
|
color: #191c21;
|
|
font-size: 18px;
|
|
line-height: 28px;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.picker-close {
|
|
color: #00478d;
|
|
font-size: 14px;
|
|
line-height: 20px;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.institution-list {
|
|
max-height: calc(72vh - 72px);
|
|
margin-top: 8px;
|
|
}
|
|
|
|
.institution-empty {
|
|
padding: 20px 4px;
|
|
color: #727783;
|
|
font-size: 14px;
|
|
line-height: 20px;
|
|
text-align: center;
|
|
}
|
|
|
|
.institution-item {
|
|
box-sizing: border-box;
|
|
min-height: 64px;
|
|
padding: 12px 4px;
|
|
border-bottom: 1px solid #ecedf6;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
}
|
|
|
|
.institution-item.active {
|
|
color: #00478d;
|
|
}
|
|
|
|
.institution-copy {
|
|
flex: 1;
|
|
min-width: 0;
|
|
padding-right: 16px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
|
|
.institution-name {
|
|
color: #191c21;
|
|
font-size: 16px;
|
|
line-height: 24px;
|
|
font-weight: 500;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.institution-item.active .institution-name {
|
|
color: #00478d;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.institution-meta {
|
|
margin-top: 2px;
|
|
color: #727783;
|
|
font-size: 12px;
|
|
line-height: 18px;
|
|
}
|
|
|
|
.selected-mark {
|
|
position: relative;
|
|
flex: 0 0 auto;
|
|
width: 18px;
|
|
height: 18px;
|
|
border-radius: 50%;
|
|
background: #00478d;
|
|
}
|
|
|
|
.selected-mark::after {
|
|
content: '';
|
|
position: absolute;
|
|
left: 6px;
|
|
top: 3px;
|
|
width: 5px;
|
|
height: 9px;
|
|
border-right: 2px solid #ffffff;
|
|
border-bottom: 2px solid #ffffff;
|
|
transform: rotate(45deg);
|
|
}
|
|
|
|
.shake {
|
|
animation: shake 0.5s cubic-bezier(.36, .07, .19, .97) both;
|
|
}
|
|
|
|
@keyframes shake {
|
|
10%,
|
|
90% {
|
|
transform: translate3d(-1px, 0, 0);
|
|
}
|
|
20%,
|
|
80% {
|
|
transform: translate3d(2px, 0, 0);
|
|
}
|
|
30%,
|
|
50%,
|
|
70% {
|
|
transform: translate3d(-4px, 0, 0);
|
|
}
|
|
40%,
|
|
60% {
|
|
transform: translate3d(4px, 0, 0);
|
|
}
|
|
}
|
|
|
|
@keyframes spin {
|
|
from {
|
|
transform: rotate(0deg);
|
|
}
|
|
to {
|
|
transform: rotate(360deg);
|
|
}
|
|
}
|
|
</style>
|