Files
resume-agent/frontend/src/components/ResumePhoneSelector.vue
T
2026-07-20 14:48:41 +08:00

215 lines
5.9 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import { computed, ref, watch } from 'vue'
import type { ComponentSubmission } from '../types/resumeAgent'
import {
booleanValue,
initialValue,
optionList,
stringValue,
} from '../utils/componentData'
import FormCard from './shared/FormCard.vue'
const props = withDefaults(
defineProps<{
data: Record<string, unknown>
value?: unknown
readOnly?: boolean
pending?: boolean
inputOnly?: boolean
}>(),
{ value: undefined, readOnly: false, pending: false, inputOnly: false },
)
const emit = defineEmits<{ submit: [submission: ComponentSubmission] }>()
const accountOptions = computed(() => {
if (props.inputOnly) return []
const explicit = optionList(props.data.options ?? props.data.phones ?? props.data.choices)
if (explicit.length) return explicit
return [
...(booleanValue(props.data.has_account_phone)
? [{
value: 'account',
label: stringValue(props.data.masked_phone, '使用登录手机号'),
description: '将登录手机号作为简历联系电话',
}]
: []),
{ value: 'manual', label: '输入其他手机号', description: '仅校验格式,不发送验证码' },
]
})
const sourceValue = computed(() =>
typeof props.value === 'string' ? props.value : initialValue(props.data),
)
const selected = ref(sourceValue.value)
const manualPhone = ref(props.inputOnly ? sourceValue.value : '')
const validationError = ref('')
watch(sourceValue, (next) => {
selected.value = next
if (props.inputOnly) manualPhone.value = next
})
const resolvedValue = computed(() => (props.inputOnly ? manualPhone.value.trim() : selected.value))
const resolvedLabel = computed(
() => accountOptions.value.find((option) => option.value === sourceValue.value)?.label || sourceValue.value,
)
function selectPhone(value: string) {
selected.value = value
validationError.value = ''
}
function submit() {
if (!props.inputOnly) {
if (!selected.value) {
validationError.value = '请选择手机号来源。'
return
}
const source = ['manual', 'other'].includes(selected.value) ? 'manual' : 'account'
emit('submit', {
event: 'select',
payload: { value: source, source },
})
return
}
const phone = resolvedValue.value.trim()
if (!/^1[3-9]\d{9}$/.test(phone)) {
validationError.value = '手机号格式不正确,请检查后重试。'
return
}
emit('submit', {
event: 'submit',
payload: { value: phone, phone },
})
}
</script>
<template>
<FormCard
eyebrow="简历定位"
:title="stringValue(data.title, inputOnly ? '输入其他简历手机号' : '选择简历联系电话')"
:description="stringValue(data.description, '可以使用登录手机号,也可以填写其他中国大陆手机号。')"
:read-only="readOnly"
:pending="pending"
>
<div v-if="readOnly" class="read-only-value">{{ resolvedLabel || '手机号已记录' }}</div>
<form v-else novalidate @submit.prevent="submit">
<ul v-if="!inputOnly" class="phone-list">
<li v-for="phone in accountOptions" :key="phone.value">
<button
class="phone-option"
:class="{ 'phone-option--selected': selected === phone.value }"
type="button"
:disabled="pending || phone.disabled"
:aria-pressed="selected === phone.value"
@click="selectPhone(phone.value)"
>
<span class="phone-option__avatar" aria-hidden="true">{{ phone.label.slice(-2) }}</span>
<span>
<strong>{{ phone.label }}</strong>
<small>{{ phone.description || '已有简历记录' }}</small>
</span>
</button>
</li>
</ul>
<div v-if="inputOnly" class="field-group">
<label class="field-label" for="resume-phone">手机号</label>
<input
id="resume-phone"
v-model="manualPhone"
class="text-input"
type="tel"
inputmode="tel"
autocomplete="tel"
:placeholder="stringValue(data.placeholder, '例如:13800138000')"
:disabled="pending"
:aria-invalid="Boolean(validationError)"
aria-describedby="resume-phone-help resume-phone-error"
@input="validationError = ''"
/>
<p id="resume-phone-help" class="field-hint">仅校验 11 位中国大陆手机号格式不发送验证码</p>
<p v-if="validationError" id="resume-phone-error" class="validation-error" role="alert">
{{ validationError }}
</p>
</div>
<div class="component-actions">
<button class="primary-button" type="submit" :disabled="!resolvedValue || pending">
继续
</button>
</div>
</form>
</FormCard>
</template>
<style scoped>
.phone-list {
display: grid;
gap: 9px;
margin: 0;
padding: 0;
list-style: none;
}
.phone-option {
display: flex;
width: 100%;
min-height: 70px;
align-items: center;
gap: 12px;
padding: 11px 13px;
border: 1px solid var(--line);
border-radius: 16px;
color: var(--ink);
background: #f9fcfb;
text-align: left;
transition: border-color 160ms ease, background 160ms ease, transform 160ms ease;
}
.phone-option:hover:not(:disabled) {
transform: translateY(-1px);
border-color: #91cbc7;
}
.phone-option--selected {
border-color: var(--brand);
background: var(--brand-soft);
}
.phone-option__avatar {
display: grid;
width: 42px;
height: 42px;
flex: none;
place-items: center;
border-radius: 13px;
color: var(--brand-dark);
background: #fff;
font-family: ui-monospace, "SFMono-Regular", Consolas, monospace;
font-size: 11px;
font-weight: 800;
}
.phone-option strong,
.phone-option small {
display: block;
}
.phone-option strong {
font-size: 14px;
}
.phone-option small {
margin-top: 3px;
color: var(--ink-faint);
font-size: 11px;
}
.validation-error {
margin: 0;
color: var(--danger);
font-size: 12px;
}
</style>