|
- <template>
- <div class="min-h-screen bg-[#f3f7fb]">
- <!-- ========== Hero (对齐 V2 渐变风格) ========== -->
- <div class="relative overflow-hidden text-white"
- style="background: linear-gradient(135deg, #138cff 0%, #34c3ff 54%, #79e0df 100%)">
- <div class="absolute w-[360px] h-[360px] rounded-full bg-white/20 -right-[110px] -top-[140px]" />
- <div class="absolute w-[240px] h-[240px] rounded-full bg-white/15 right-[86px] -bottom-[130px]" />
- <div class="relative max-w-7xl mx-auto px-4 sm:px-6 py-8 lg:py-10">
- <div class="lg:flex lg:items-end lg:justify-between gap-8">
- <div class="max-w-[460px]">
- <div class="text-[11px] lg:text-xs font-bold tracking-wider text-white/75">AI NAME STUDIO</div>
- <h1 class="mt-3 text-[26px] lg:text-[36px] font-extrabold leading-tight">给宝宝取一个有出处的好名字</h1>
- <p class="mt-3 text-sm lg:text-base text-white/90 leading-relaxed">结合姓氏、性别、诗词典故和音律寓意,生成可直接挑选的名字方案。</p>
- </div>
- <div class="hidden lg:block w-[260px] flex-shrink-0">
- <div class="rounded-3xl border border-white/40 bg-white/20 backdrop-blur p-5 shadow-[0_18px_34px_rgba(0,92,190,0.16)]">
- <div class="text-3xl font-extrabold">{{ previewName }}</div>
- <div class="mt-1.5 text-xs text-white/85">李白《独坐敬亭山》</div>
- </div>
- <div class="mt-3 h-12 rounded-2xl border border-white/40 bg-white/20 backdrop-blur flex items-center justify-between px-4 text-xs text-white/90">
- <span>音律</span><span>寓意</span><span>出处</span>
- </div>
- </div>
- </div>
- </div>
- </div>
-
- <!-- ========== Main Grid ========== -->
- <div class="max-w-7xl mx-auto px-4 sm:px-6 py-6 lg:py-8">
- <div class="lg:grid lg:grid-cols-12 lg:gap-8">
- <!-- ===== LEFT: Form (col-span-7) ===== -->
- <div class="lg:col-span-7 space-y-5 pb-24 lg:pb-0">
- <!-- 1. 基础信息 Card -->
- <div class="bg-white rounded-2xl p-5 lg:p-6 shadow-[0_8px_28px_rgba(34,68,112,0.05)]">
- <div class="flex items-baseline mb-4">
- <h3 class="text-base lg:text-lg font-extrabold text-[#172033]">基础信息</h3>
- <span class="ml-auto text-xs text-[#8a95a6]">必填</span>
- </div>
- <!-- Surname + Gender (一行,对齐 V2 surname-box) -->
- <div class="flex items-stretch gap-3 rounded-2xl bg-[#f6f9fe] p-3.5">
- <div class="flex-1 min-w-0">
- <label class="block text-sm font-bold text-[#172033]">姓氏</label>
- <input v-model="form.surname" type="text" maxlength="5" placeholder="输入姓氏"
- class="w-full mt-1 bg-transparent text-xl lg:text-2xl font-extrabold text-[#172033] placeholder:text-[#a0aabb] focus:outline-none" />
- </div>
- <div class="w-[180px] flex-shrink-0 p-1.5 rounded-[28px] bg-white shadow-[inset_0_0_0_1px_#edf2f7] flex">
- <button v-for="opt in genderOptions" :key="opt.value" type="button"
- class="flex-1 h-10 rounded-[24px] text-sm font-bold transition-all"
- :class="form.sex === opt.value ? 'bg-[#0b8cff] text-white' : 'text-[#7f8896]'"
- @click="form.sex = opt.value">{{ opt.label }}</button>
- </div>
- </div>
-
- <!-- 出生信息开关 -->
- <div class="mt-4 rounded-2xl border-2 transition-colors"
- :class="birthInfoIgnored ? 'bg-[#f7f9fd] border-[#edf2f7]' : 'bg-[#eff8ff] border-[#0b8cff]/20'">
- <div class="flex items-center p-4 cursor-pointer" @click="toggleBirthInfo">
- <div class="flex-1 min-w-0">
- <div class="text-sm font-extrabold text-[#172033]">参考出生信息</div>
- <div class="mt-1 text-xs text-[#7f8896]">{{ birthInfoIgnored ? '已关闭,生成时不参考农历和五行' : '开启后会结合农历和五行倾向' }}</div>
- </div>
- <div class="ml-3 w-[60px] h-7 p-1 rounded-full transition-colors flex-shrink-0"
- :class="!birthInfoIgnored ? 'bg-[#0b8cff]' : 'bg-[#d7deea]'">
- <div class="w-5 h-5 rounded-full bg-white shadow transition-transform"
- :class="!birthInfoIgnored && 'translate-x-7'" />
- </div>
- </div>
- <div v-if="!birthInfoIgnored" class="px-4 pb-4 space-y-3">
- <div class="grid grid-cols-2 gap-3">
- <div class="rounded-2xl bg-[#f6f9fe] p-4 flex items-start justify-between cursor-pointer min-h-[88px]"
- @click="openDatePicker">
- <div class="min-w-0">
- <div class="text-sm font-bold text-[#172033]">出生日期</div>
- <div class="mt-2 text-sm text-[#7f8896]">{{ birthDate || '请选择日期' }}</div>
- </div>
- <span class="ml-2 text-2xl text-[#a0aabb] leading-none flex-shrink-0">›</span>
- </div>
- <div class="rounded-2xl bg-[#f6f9fe] p-4 flex items-start justify-between cursor-pointer min-h-[88px]"
- :class="{ 'opacity-80': birthTimeUnknown }" @click="openTimePicker">
- <div class="min-w-0">
- <div class="text-sm font-bold text-[#172033]">出生时分</div>
- <div class="mt-2 text-sm text-[#7f8896]">{{ birthClockText }}</div>
- </div>
- <span class="ml-2 text-2xl text-[#a0aabb] leading-none flex-shrink-0">›</span>
- </div>
- </div>
- <!-- 农历显示 -->
- <div v-if="birthLunarText" class="rounded-2xl bg-[#eff8ff] p-4 flex items-center">
- <span class="mr-3 px-3 py-0.5 rounded-full bg-white text-[#0b8cff] text-xs font-extrabold flex-shrink-0">农历</span>
- <span class="text-sm font-bold text-[#3a5878] min-w-0">{{ birthLunarText }}</span>
- </div>
- <!-- 不知道具体时间 -->
- <div class="inline-flex items-center rounded-full px-4 py-2 cursor-pointer transition-colors border-2"
- :class="birthTimeUnknown ? 'bg-[#eff8ff] border-[#0b8cff] text-[#0b8cff]' : 'bg-[#f6f9fe] border-transparent text-[#7f8896]'"
- @click="toggleBirthTimeUnknown">
- <span class="w-1.5 h-1.5 mr-2 rounded-full bg-current" />
- <span class="text-xs font-bold">不知道具体时间</span>
- </div>
- </div>
- </div>
- </div>
-
- <!-- 2. 名字出处 Card -->
- <div class="bg-white rounded-2xl p-5 lg:p-6 shadow-[0_8px_28px_rgba(34,68,112,0.05)]">
- <div class="flex items-baseline">
- <h3 class="text-base lg:text-lg font-extrabold text-[#172033]">名字出处</h3>
- <span class="ml-auto text-xs text-[#8a95a6]">选择偏好的文化来源</span>
- </div>
- <div class="mt-4 grid grid-cols-2 lg:grid-cols-3 gap-2.5">
- <button v-for="src in visibleSourceOptions" :key="src.id" type="button"
- class="min-h-[72px] p-3 rounded-2xl border-2 transition-all text-left"
- :class="form.name_source === src.id
- ? 'bg-[#eff8ff] border-[#0b8cff] shadow-[0_8px_20px_rgba(11,140,255,0.1)]'
- : 'bg-[#f7f9fd] border-transparent hover:border-[#e7edf5]'"
- @click="form.name_source = src.id">
- <div class="text-sm font-extrabold text-[#172033] text-center">{{ src.label }}</div>
- <div class="mt-1 text-[11px] text-[#8a95a6] text-center">{{ src.desc }}</div>
- </button>
- <button type="button"
- class="min-h-[72px] p-3 rounded-2xl border-2 bg-white border-[#e7edf5] hover:border-[#0b8cff]/30 transition-all text-left"
- @click="sourceExpanded = !sourceExpanded">
- <div class="text-sm font-extrabold text-[#172033] text-center">{{ sourceExpanded ? '收起' : '更多' }}</div>
- <div class="mt-1 text-[11px] text-[#8a95a6] text-center">{{ sourceExpanded ? '精简显示' : '展开全部' }}</div>
- </button>
- </div>
- </div>
-
- <!-- 3. 名字偏好 Card -->
- <div class="bg-white rounded-2xl p-5 lg:p-6 shadow-[0_8px_28px_rgba(34,68,112,0.05)]">
- <div class="flex items-baseline">
- <h3 class="text-base lg:text-lg font-extrabold text-[#172033]">名字偏好</h3>
- <span class="ml-auto text-xs text-[#8a95a6]">用于控制生成结果的方向</span>
- </div>
-
- <!-- 字数 -->
- <div class="mt-5">
- <div class="text-sm font-bold text-[#172033]">名字字数 <span class="text-xs font-normal text-[#8a95a6]">包含姓氏</span></div>
- <div class="mt-3 flex flex-wrap gap-2.5">
- <button v-for="wc in wordOptions" :key="wc.id" type="button"
- class="h-10 px-5 rounded-full text-sm font-bold transition-all border-2"
- :class="form.words_num === wc.id ? 'bg-[#eff8ff] border-[#0b8cff] text-[#0b8cff]' : 'bg-[#f6f9fe] border-transparent text-[#596579]'"
- @click="form.words_num = wc.id">{{ wc.label }}</button>
- </div>
- </div>
-
- <!-- 是否叠词 -->
- <div class="mt-5">
- <div class="text-sm font-bold text-[#172033]">是否叠词</div>
- <div class="mt-3 flex flex-wrap gap-2.5">
- <button v-for="r in redupOptions" :key="r.id" type="button"
- class="h-10 px-5 rounded-full text-sm font-bold transition-all border-2"
- :class="form.is_redup === r.id ? 'bg-[#eff8ff] border-[#0b8cff] text-[#0b8cff]' : 'bg-[#f6f9fe] border-transparent text-[#596579]'"
- @click="form.is_redup = r.id">{{ r.label }}</button>
- </div>
- </div>
-
- <!-- 指定包含字 -->
- <div class="mt-5 flex items-center rounded-2xl bg-[#f7f9fd] p-4">
- <div class="min-w-0">
- <div class="text-sm font-bold text-[#172033]">指定包含字</div>
- <div class="mt-1 text-xs text-[#8a95a6]">可选,限制 1 个字</div>
- </div>
- <input v-model="form.special_word" type="text" maxlength="1" placeholder="如:辰"
- class="ml-auto w-24 h-12 rounded-2xl bg-white text-center text-lg font-extrabold text-[#172033] placeholder:text-[#a0aabb] focus:outline-none focus:ring-2 focus:ring-[#0b8cff]/30" />
- </div>
-
- <!-- 补充描述 -->
- <div class="mt-4 rounded-2xl bg-[#f7f9fd] p-4">
- <div class="flex items-start justify-between gap-4">
- <div>
- <div class="text-sm font-bold text-[#172033]">补充描述</div>
- <div class="mt-1 text-xs text-[#8a95a6]">额外要求、寓意和期望</div>
- </div>
- <div class="text-xs text-[#9aa5b5] flex-shrink-0">{{ (form.content || '').length }}/200</div>
- </div>
- <textarea v-model="form.content" rows="3" maxlength="200" placeholder="您可以补充您的额外要求,也可以添加您的寓意和期望。200字以内..."
- class="mt-3 w-full bg-transparent text-sm text-[#172033] placeholder:text-[#a0aabb] focus:outline-none resize-none leading-relaxed"></textarea>
- </div>
- </div>
-
- <!-- 4. Promise Card -->
- <div class="bg-white rounded-2xl p-5 lg:p-6 shadow-[0_8px_28px_rgba(34,68,112,0.05)] space-y-4">
- <div class="flex items-center">
- <span class="w-7 h-5 mr-3 rounded-[14px] bg-[#eef6ff] text-[#0b8cff] text-[11px] font-extrabold flex items-center justify-center flex-shrink-0">01</span>
- <span class="text-sm text-[#596579] leading-relaxed">可关闭出生信息参考,关闭后不结合农历和五行</span>
- </div>
- <div class="flex items-center">
- <span class="w-7 h-5 mr-3 rounded-[14px] bg-[#eef6ff] text-[#0b8cff] text-[11px] font-extrabold flex items-center justify-center flex-shrink-0">02</span>
- <span class="text-sm text-[#596579] leading-relaxed">开启后可选择日期和时分,不确定时辰可选"不知道具体时间"</span>
- </div>
- </div>
-
- <!-- Desktop Submit -->
- <div class="hidden lg:flex items-center gap-3 rounded-2xl bg-white p-4 shadow-[0_8px_28px_rgba(34,68,112,0.05)]">
- <div class="flex-1 min-w-0">
- <div class="text-sm font-bold text-[#172033] whitespace-nowrap">{{ balanceText }}</div>
- <div v-if="isBalanceInsufficient" class="mt-1 text-xs text-[#f04438]">点数不足,请前往充值</div>
- </div>
- <button type="button"
- class="px-10 h-14 rounded-full bg-[#0b8cff] hover:bg-[#0a7ce0] active:bg-[#096dca] text-white font-bold text-base shadow-[0_12px_24px_rgba(11,140,255,0.25)] transition-all flex-shrink-0 disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center min-w-[160px]"
- :disabled="!canSubmit || submitting" @click="handleSubmit">
- <UIcon v-if="!submitting" name="i-lucide-zap" class="size-4 mr-1.5" />
- <span>{{ submitting ? '生成中...' : submitText }}</span>
- </button>
- </div>
- </div>
-
- <!-- ===== RIGHT: Result (col-span-5, sticky) ===== -->
- <div class="lg:col-span-5">
- <div class="lg:sticky lg:top-6 space-y-4">
- <div v-if="!resultState.visible" class="bg-white rounded-2xl p-8 lg:p-10 shadow-sm text-center min-h-[300px] flex flex-col items-center justify-center">
- <UIcon name="i-lucide-sparkles" class="size-12 text-[#a0aabb] mb-4" />
- <div class="text-base font-medium text-[#172033] mb-1">等待生成</div>
- <div class="text-sm text-[#a0aabb]">填写左侧信息并点击"开始取名"</div>
- <div class="text-sm text-[#a0aabb]">AI将为您推荐最佳名字方案</div>
- </div>
- <div v-if="resultState.loading" class="bg-white rounded-2xl p-6 lg:p-8 shadow-sm">
- <div class="text-base font-bold text-[#172033] mb-1">{{ questionTitle }}</div>
- <div class="text-sm text-[#7f8896] mb-6">AI 正在分析音律、寓意和出处...</div>
- <div class="rounded-2xl bg-[#f7f9fd] py-10 flex flex-col items-center">
- <div class="flex gap-1.5 mb-4">
- <span class="w-2.5 h-2.5 rounded-full bg-[#0b8cff] animate-pulse-dot" />
- <span class="w-2.5 h-2.5 rounded-full bg-[#0b8cff] animate-pulse-dot2" />
- <span class="w-2.5 h-2.5 rounded-full bg-[#0b8cff] animate-pulse-dot3" />
- </div>
- <div class="text-sm font-bold text-[#172033]">正在整理名字方案</div>
- </div>
- </div>
- <div v-if="resultState.error" class="bg-white rounded-2xl p-6 lg:p-8 shadow-sm text-center py-10">
- <UIcon name="i-lucide-circle-alert" class="size-12 text-red-400 mb-3 mx-auto" />
- <div class="text-base font-medium text-[#f04438] mb-2">生成失败</div>
- <div class="text-sm text-[#7f8896] mb-4">{{ resultState.errorMsg }}</div>
- <UButton color="primary" variant="outline" size="sm" @click="handleSubmit">重试</UButton>
- </div>
- <div v-if="resultState.empty" class="bg-white rounded-2xl p-6 lg:p-8 shadow-sm text-center py-10">
- <UIcon name="i-lucide-search-x" class="size-12 text-[#a0aabb] mb-3 mx-auto" />
- <div class="text-base font-bold text-[#172033] mb-1">本次没有可用名字</div>
- <div class="text-sm text-[#7f8896] max-w-xs mx-auto">{{ result.summary || 'AI 返回的候选名未通过姓名可用性筛选,请调整条件或重新生成。' }}</div>
- <UButton color="primary" size="sm" class="mt-5" @click="handleSubmit">重新生成</UButton>
- </div>
- <template v-if="resultState.success">
- <div v-if="result.summary" class="bg-white rounded-2xl p-5 lg:p-6 shadow-sm">
- <div class="text-sm text-[#596579] leading-relaxed">{{ result.summary }}</div>
- </div>
- <div v-for="(item, idx) in result.names" :key="idx" class="bg-white rounded-2xl p-5 lg:p-6 shadow-sm">
- <div class="flex items-center justify-between mb-2">
- <span class="text-2xl lg:text-3xl font-bold text-[#0b8cff]">{{ item.name }}</span>
- <span v-if="item.score" class="text-lg font-bold text-[#ff8a00]">{{ item.score }}分</span>
- </div>
- <div v-if="item.pinyin" class="text-xs text-[#7f8896] mb-3">{{ item.pinyin }}</div>
- <div class="space-y-2.5">
- <div v-for="field in nameFields" :key="field.key">
- <div v-if="item[field.key]" class="flex items-start gap-2.5">
- <span class="flex-shrink-0 inline-flex items-center justify-center h-[26px] px-2 rounded-full bg-[#eef6ff] text-[#0b8cff] text-[11px] font-medium">{{ field.label }}</span>
- <span class="text-sm text-[#172033] leading-relaxed pt-0.5">{{ item[field.key] }}</span>
- </div>
- </div>
- </div>
- </div>
- <div class="flex gap-3">
- <UButton color="primary" variant="outline" size="lg" class="flex-1 !rounded-full" :loading="submitting" @click="handleSubmit">重新生成</UButton>
- <UButton color="primary" size="lg" class="flex-1 !rounded-full" @click="copyResult">复制结果</UButton>
- </div>
- </template>
- </div>
- </div>
- </div>
- </div>
-
- <!-- ========== Mobile Bottom Bar (fixed) ========== -->
- <div class="lg:hidden fixed left-0 right-0 bottom-0 z-50 bg-white/95 backdrop-blur shadow-[0_-8px_24px_rgba(35,55,90,0.08)]"
- style="padding: 14px 16px calc(14px + env(safe-area-inset-bottom))">
- <div class="flex items-center justify-between mb-2.5 gap-3 text-sm">
- <div class="min-w-0 truncate text-[#7b8797]">{{ balanceText }}</div>
- <button v-if="isBalanceInsufficient" class="text-[#0b8cff] text-xs font-extrabold flex-shrink-0" @click="goRecharge">去充值</button>
- </div>
- <div class="flex gap-3">
- <button class="w-[120px] h-12 rounded-full bg-[#eef6ff] text-[#0b8cff] text-sm font-bold" @click="goHistory">历史记录</button>
- <button class="flex-1 h-12 rounded-full text-white font-bold shadow-[0_12px_24px_rgba(11,140,255,0.2)] transition-opacity"
- :class="(canSubmit && !submitting) ? 'bg-[#0b8cff]' : 'bg-[#0b8cff] opacity-60'"
- :disabled="!canSubmit || submitting" @click="handleSubmit">
- {{ submitting ? '生成中...' : submitText }}
- </button>
- </div>
- </div>
-
- <!-- ========== Date Picker Modal (custom calendar grid) ========== -->
- <Teleport to="body">
- <Transition name="modal">
- <div v-if="dateOpen" class="fixed inset-0 z-[100] flex items-center justify-center p-4">
- <div class="absolute inset-0 bg-black/50" @click="dateOpen = false" />
- <div class="relative bg-white rounded-2xl shadow-2xl w-full max-w-md overflow-hidden">
- <div class="flex items-center justify-between px-5 py-4 border-b border-gray-100">
- <h3 class="text-base font-extrabold text-[#172033]">选择出生日期</h3>
- <button class="text-[#a0aabb] text-2xl leading-none w-8 h-8 flex items-center justify-center rounded-full hover:bg-gray-100" @click="dateOpen = false">×</button>
- </div>
- <div class="p-5">
- <div class="flex items-center justify-between mb-4">
- <button type="button" class="w-9 h-9 rounded-full hover:bg-gray-100 flex items-center justify-center text-[#172033]" @click="prevMonth">
- <UIcon name="i-lucide-chevron-left" class="size-5" />
- </button>
- <div class="text-base font-extrabold text-[#172033]">{{ calendarYear }}年{{ calendarMonth }}月</div>
- <button type="button" class="w-9 h-9 rounded-full hover:bg-gray-100 flex items-center justify-center text-[#172033] disabled:opacity-30 disabled:cursor-not-allowed" :disabled="isNextMonthDisabled" @click="nextMonth">
- <UIcon name="i-lucide-chevron-right" class="size-5" />
- </button>
- </div>
- <div class="grid grid-cols-7 gap-1 text-center text-xs text-[#7f8896] mb-2">
- <div v-for="dow in ['日', '一', '二', '三', '四', '五', '六']" :key="dow" class="h-8 flex items-center justify-center">{{ dow }}</div>
- </div>
- <div class="grid grid-cols-7 gap-1">
- <button v-for="day in calendarDays" :key="day.key" type="button"
- class="aspect-square flex items-center justify-center text-sm rounded-full transition-colors relative"
- :class="{
- 'text-[#cbd5e1]': day.otherMonth && !day.isSelected,
- 'text-[#172033] hover:bg-gray-100': !day.otherMonth && !day.isSelected && !day.isToday && !day.disabled,
- 'ring-1 ring-[#0b8cff] text-[#0b8cff] font-bold': day.isToday && !day.isSelected,
- 'bg-[#0b8cff] text-white font-bold': day.isSelected,
- 'opacity-30 cursor-not-allowed': day.disabled,
- }"
- :disabled="day.disabled"
- @click="selectDay(day)">
- {{ day.day }}
- </button>
- </div>
- <div class="mt-4 text-center text-xs text-[#7f8896]">小贴士:可选过去日期,今日及之前可选</div>
- </div>
- <div class="flex gap-3 px-5 py-4 border-t border-gray-100">
- <button type="button" class="flex-1 h-11 rounded-full border border-gray-200 text-[#596579] font-bold hover:bg-gray-50" @click="dateOpen = false">取消</button>
- <button type="button" class="flex-1 h-11 rounded-full bg-[#0b8cff] text-white font-bold hover:bg-[#0a7ce0]" @click="confirmDate">确定</button>
- </div>
- </div>
- </div>
- </Transition>
- </Teleport>
-
- <!-- ========== Time Picker Modal (UInputTime) ========== -->
- <Teleport to="body">
- <Transition name="modal">
- <div v-if="timeOpen" class="fixed inset-0 z-[100] flex items-center justify-center p-4">
- <div class="absolute inset-0 bg-black/50" @click="timeOpen = false" />
- <div class="relative bg-white rounded-2xl shadow-2xl w-full max-w-md overflow-hidden">
- <div class="flex items-center justify-between px-5 py-4 border-b border-gray-100">
- <h3 class="text-base font-extrabold text-[#172033]">选择出生时分</h3>
- <button class="text-[#a0aabb] text-2xl leading-none w-8 h-8 flex items-center justify-center rounded-full hover:bg-gray-100" @click="timeOpen = false">×</button>
- </div>
- <div class="p-6">
- <UInputTime v-model="tempTimeValue" class="w-full" />
- <div class="mt-4 text-center text-xs text-[#7f8896]">不确定时辰?点下方"不知道具体时间"</div>
- </div>
- <div class="flex gap-3 px-5 py-4 border-t border-gray-100">
- <button type="button" class="flex-1 h-11 rounded-full border border-gray-200 text-[#0b8cff] font-bold hover:bg-gray-50" @click="timeUnknownPick">不知道具体时间</button>
- <button type="button" class="flex-1 h-11 rounded-full bg-[#0b8cff] text-white font-bold hover:bg-[#0a7ce0]" @click="confirmTime">确定</button>
- </div>
- </div>
- </div>
- </Transition>
- </Teleport>
- </div>
- </template>
-
- <script setup>
- import { ref, reactive, computed, watch, onMounted } from 'vue'
- import { Time } from '@internationalized/date'
-
- const _api = useApi()
- const api = _api // _api 是 { get, post, request },别名保持 api.post(...) 调用
- const user = useUser()
- const router = useRouter()
- const toast = useToast()
-
- function showToast(message, color = 'red') {
- toast.add({ title: message, color })
- }
-
- // ==================== Options (对齐 V2) ====================
- const genderOptions = [
- { label: '男孩', value: 1 },
- { label: '女孩', value: 2 },
- ]
- const sourceOptions = [
- { id: '0', label: '不限', desc: '综合推荐' },
- { id: '1', label: '诗经', desc: '温润雅致' },
- { id: '2', label: '楚辞', desc: '浪漫开阔' },
- { id: '3', label: '离骚', desc: '清朗有骨' },
- { id: '4', label: '周易', desc: '稳重含蓄' },
- { id: '5', label: '史记', desc: '大气有典' },
- { id: '6', label: '诗词歌赋', desc: '文采舒展' },
- { id: '7', label: '成语典故', desc: '寓意明确' },
- { id: '8', label: '佛教', desc: '清净平和' },
- { id: '9', label: '道教', desc: '自然洒脱' },
- { id: '10', label: '自然景观', desc: '明亮灵动' },
- ]
- const wordOptions = [
- { id: '0', label: '不限' }, { id: '2', label: '2字' }, { id: '3', label: '3字' },
- { id: '4', label: '4字' }, { id: '5', label: '5字' },
- ]
- const redupOptions = [
- { id: '0', label: '不限' }, { id: '1', label: '是' }, { id: '2', label: '否' },
- ]
- const nameFields = [
- { key: 'source', label: '出处' }, { key: 'wuxing', label: '五行' },
- { key: 'meaning', label: '寓意' }, { key: 'tone', label: '音律' }, { key: 'reason', label: '推荐' },
- ]
-
- // ==================== Form ====================
- const form = reactive({
- surname: '', sex: 0, name_source: '0', words_num: '0',
- is_redup: '0', special_word: '', content: '',
- birth_date: '', birth_clock: '', birth_time_unknown: 1,
- })
- const sourceExpanded = ref(false)
- const visibleSourceOptions = computed(() => {
- if (sourceExpanded.value) return sourceOptions
- const selected = sourceOptions.find(i => i.id === form.name_source)
- const rows = [sourceOptions[0]]
- if (selected && selected.id !== '0') rows.push(selected)
- sourceOptions.slice(1).forEach(i => { if (rows.length < 5 && !rows.some(r => r.id === i.id)) rows.push(i) })
- return rows
- })
-
- // ==================== Birth Info ====================
- const birthInfoIgnored = ref(false)
- const birthDate = ref('')
- const birthClock = ref('')
- const birthTimeUnknown = ref(true)
- const dateOpen = ref(false)
- const timeOpen = ref(false)
- const tempTimeValue = ref(new Time(8, 0))
-
- // Custom calendar grid
- const todayDate = new Date()
- const calendarYear = ref(todayDate.getFullYear())
- const calendarMonth = ref(todayDate.getMonth() + 1)
- const pendingDay = ref(null) // 用户在网格里点击但未确认的日期
-
- const calendarDays = computed(() => {
- const year = calendarYear.value
- const month = calendarMonth.value
- const firstDayOfWeek = new Date(year, month - 1, 1).getDay() // 0=Sun
- const daysInMonth = new Date(year, month, 0).getDate()
- const daysInPrevMonth = new Date(year, month - 1, 0).getDate()
- const todayStr = formatYMD(todayDate)
- const currentPending = pendingDay.value
-
- const days = []
- // 上月填充
- for (let i = firstDayOfWeek - 1; i >= 0; i--) {
- const d = daysInPrevMonth - i
- const dateStr = formatYMD(year, month - 1, d)
- days.push({ key: `prev-${dateStr}`, day: d, otherMonth: true, date: dateStr, disabled: true, isToday: false, isSelected: false })
- }
- // 当月
- for (let d = 1; d <= daysInMonth; d++) {
- const dateStr = formatYMD(year, month, d)
- const isFuture = dateStr > todayStr
- const isSelected = !!currentPending && currentPending === dateStr
- days.push({
- key: `cur-${dateStr}`, day: d, otherMonth: false, date: dateStr,
- disabled: isFuture, isToday: dateStr === todayStr, isSelected,
- })
- }
- // 下月填充至 42 格
- let nextDay = 1
- while (days.length < 42) {
- const dateStr = formatYMD(year, month + 1, nextDay)
- days.push({ key: `next-${dateStr}`, day: nextDay, otherMonth: true, date: dateStr, disabled: true, isToday: false, isSelected: false })
- nextDay++
- }
- return days
- })
-
- const isNextMonthDisabled = computed(() => {
- const today = new Date()
- return calendarYear.value > today.getFullYear() ||
- (calendarYear.value === today.getFullYear() && calendarMonth.value >= today.getMonth() + 1)
- })
-
- function formatYMD(year, month, day) {
- const m = month < 0 ? 12 : month > 12 ? 1 : month
- return `${year}-${String(m).padStart(2, '0')}-${String(day).padStart(2, '0')}`
- }
-
- function prevMonth() {
- if (calendarMonth.value === 1) { calendarMonth.value = 12; calendarYear.value-- }
- else calendarMonth.value--
- }
- function nextMonth() {
- if (isNextMonthDisabled.value) return
- if (calendarMonth.value === 12) { calendarMonth.value = 1; calendarYear.value++ }
- else calendarMonth.value++
- }
- function selectDay(day) {
- if (day.disabled) return
- pendingDay.value = day.date
- }
- function toggleBirthInfo() {
- birthInfoIgnored.value = !birthInfoIgnored.value
- syncBirthFields()
- }
- function toggleBirthTimeUnknown() {
- if (birthInfoIgnored.value) return
- birthTimeUnknown.value = !birthTimeUnknown.value
- if (birthTimeUnknown.value) birthClock.value = ''
- syncBirthFields()
- }
- function openDatePicker() {
- if (birthInfoIgnored.value) return
- if (birthDate.value) {
- const p = birthDate.value.split('-')
- calendarYear.value = +p[0]
- calendarMonth.value = +p[1]
- pendingDay.value = birthDate.value
- } else {
- calendarYear.value = todayDate.getFullYear()
- calendarMonth.value = todayDate.getMonth() + 1
- pendingDay.value = null
- }
- dateOpen.value = true
- }
- function confirmDate() {
- if (pendingDay.value) {
- birthDate.value = pendingDay.value
- syncBirthFields()
- }
- dateOpen.value = false
- }
- function openTimePicker() {
- if (birthInfoIgnored.value) return
- const clock = birthClock.value || '08:00'
- const [h, m] = clock.split(':')
- tempTimeValue.value = new Time(+h, +m)
- timeOpen.value = true
- }
- function confirmTime() {
- const t = tempTimeValue.value
- if (t) {
- birthClock.value = `${String(t.hour).padStart(2, '0')}:${String(t.minute).padStart(2, '0')}`
- birthTimeUnknown.value = false
- syncBirthFields()
- }
- timeOpen.value = false
- }
- function timeUnknownPick() {
- birthTimeUnknown.value = true
- birthClock.value = ''
- syncBirthFields()
- timeOpen.value = false
- }
- function syncBirthFields() {
- if (birthInfoIgnored.value) {
- form.birth_date = ''; form.birth_clock = ''; form.birth_time_unknown = 1
- return
- }
- form.birth_date = birthDate.value || ''
- form.birth_clock = birthTimeUnknown.value ? '' : (birthClock.value || '')
- form.birth_time_unknown = birthTimeUnknown.value ? 1 : 0
- }
-
- const birthClockText = computed(() => {
- if (birthTimeUnknown.value) return '不知道具体时间'
- return birthClock.value || '请选择时分'
- })
- const previewName = '白敬亭'
-
- // 农历
- const birthLunarText = ref('')
- watch(birthDate, async (val) => {
- if (!val) { birthLunarText.value = ''; return }
- try {
- const { Solar } = await import('lunar-javascript')
- const p = val.split('-')
- const lunar = Solar.fromYmd(+p[0], +p[1], +p[2]).getLunar()
- birthLunarText.value = `农历${lunar.getYearInGanZhi()}年 ${lunar.getMonthInChinese()}${lunar.getDayInChinese()}`
- } catch { birthLunarText.value = '' }
- }, { immediate: true })
-
- // ==================== Billing & Submit ====================
- const billing = reactive({ loaded: false, points: 0 })
- const balance = reactive({ points: 0 })
- async function loadBilling() {
- try {
- const res = await api.post('/billing/config', { app_key: 'ai-name' })
- if (res.code === 0 && res.data) {
- billing.points = res.data.quote?.points || res.data.points || 0
- if (res.data.balance) balance.points = res.data.balance.points || res.data.balance.point_balance || 0
- }
- } catch {} finally { billing.loaded = true }
- }
- async function loadBalance() {
- try {
- const res = await api.post('/user/getbalance', {})
- if (res.code === 0 && res.data) balance.points = res.data.points || res.data.point_balance || 0
- } catch {}
- }
- onMounted(() => { loadBilling(); if (user.isLogin.value) loadBalance() })
-
- const submitting = ref(false)
- const questionTitle = ref('')
- const canSubmit = computed(() => !!form.surname && !submitting.value)
- const balanceText = computed(() => {
- const cost = Number(billing.points || 0)
- if (!user.isLogin.value) return cost ? `AI取名 · ${cost}点/次` : 'AI取名'
- return cost ? `余额${balance.points}点 · 预计消耗${cost}点` : `余额${balance.points}点`
- })
- const submitText = computed(() => {
- const cost = Number(billing.points || 0)
- if (!user.isLogin.value) return cost ? `登录后取名 · ${cost}点` : '登录后取名'
- return cost ? `立即取名 · ${cost}点` : '立即取名'
- })
- const isBalanceInsufficient = computed(() => {
- const cost = Number(billing.points || 0)
- return user.isLogin.value && cost > 0 && balance.points >= 0 && balance.points < cost
- })
-
- const resultState = ref({ visible: false, loading: false, error: false, empty: false, success: false, errorMsg: '' })
- const result = ref({ summary: '', names: [] })
-
- async function handleSubmit() {
- if (!user.isLogin.value) { showToast('请先登录'); return router.push('/login') }
- if (!form.surname) { showToast('请输入姓氏'); return }
- if (submitting.value) return
- syncBirthFields()
- submitting.value = true
- resultState.value = { visible: true, loading: true, error: false, empty: false, success: false, errorMsg: '' }
- result.value = { summary: '', names: [] }
- questionTitle.value = `${form.surname}${form.sex === 2 ? '女孩' : form.sex === 1 ? '男孩' : ''}好名字`
-
- const params = {
- surname: form.surname, sex: form.sex, name_source: form.name_source,
- words_num: form.words_num, is_redup: form.is_redup, special_word: form.special_word,
- content: form.content, model: 4,
- birth_date: form.birth_date, birth_clock: form.birth_clock, birth_time_unknown: form.birth_time_unknown,
- }
-
- try {
- const res = await api.post('/ainame/send', params)
- if (res.code === 1011) {
- resultState.value = { ...resultState.value, loading: false, error: true, errorMsg: '点数不足,请充值后重试' }
- return
- }
- if (res.code !== 0) {
- resultState.value = { ...resultState.value, loading: false, error: true, errorMsg: res.msg || '生成失败' }
- return
- }
- const data = res.data || {}
- const parsed = parseResult(data.content || data)
- if (parsed.names && parsed.names.length > 0) {
- result.value = parsed
- resultState.value = { visible: true, loading: false, error: false, empty: false, success: true, errorMsg: '' }
- } else {
- result.value = parsed
- resultState.value = { visible: true, loading: false, error: false, empty: true, success: false, errorMsg: '' }
- }
- } catch (e) {
- resultState.value = { ...resultState.value, loading: false, error: true, errorMsg: e.msg || '网络异常,请重试' }
- } finally { submitting.value = false }
- }
-
- function parseResult(content) {
- if (!content) return { summary: '', names: [] }
- let text = String(content).trim().replace(/^```json\s*/i, '').replace(/^```\s*/i, '').replace(/```$/i, '').trim()
- const line = parseJsonLines(text); if (line.names.length) return line
- const marked = parseMarkedText(text); if (marked.names.length) return marked
- const s = text.indexOf('{'), e = text.lastIndexOf('}')
- if (s >= 0 && e > s) text = text.substring(s, e + 1)
- try { const d = JSON.parse(text); return { summary: d.summary || '', names: Array.isArray(d.names) ? d.names : [] } }
- catch { return { summary: text, names: [] } }
- }
- function parseJsonLines(text) {
- const d = { summary: '', names: [] }
- String(text || '').split(/\r?\n/).forEach(l => { l = l.trim(); if (!l) return; try { const i = JSON.parse(l); if (i.type === 'summary') d.summary = i.content || ''; if (i.type === 'name' && i.data) d.names.push(i.data) } catch {} })
- return d
- }
- function parseMarkedText(text) {
- const d = { summary: '', names: [] }; const v = String(text || '')
- const sm = v.match(/整体建议[::]([\s\S]*?)(?=\n\s*【|$)/); if (sm) d.summary = sm[1].trim()
- const reg = /【([^】]+)】([\s\S]*?)(?=\n\s*【|$)/g; let m
- while ((m = reg.exec(v))) {
- const body = m[2] || '', name = resolveNameBlockTitle(m[1], body)
- d.names.push({ name, pinyin: mf(body, '拼音'), score: parseInt(mf(body, '评分')) || 90, source: mf(body, '出处'), wuxing: mf(body, '五行'), meaning: mf(body, '寓意'), tone: mf(body, '音律'), reason: mf(body, '推荐') })
- }
- d.names = d.names.filter(i => i.name).slice(0, 5); return d
- }
- function resolveNameBlockTitle(title, body) {
- const n = String(title || '').trim(); if (!['姓名', '名字', '候选名'].includes(n)) return n
- const fr = /^(拼音|评分|出处|五行|寓意|音律|推荐)[::]/; const line = String(body || '').split(/\r?\n/).map(i => i.trim()).find(i => i && !fr.test(i))
- return line ? line.replace(/^姓名[::]/, '').trim() : n
- }
- function mf(body, label) { const r = new RegExp(`${label}[::]([^\\n\\r]*)`); const m = String(body || '').match(r); return m ? m[1].trim() : '' }
-
- function copyResult() {
- const names = result.value.names || []
- if (!names.length) { showToast('暂无可复制内容'); return }
- const lines = []
- if (result.value.summary) lines.push(result.value.summary)
- names.forEach(item => {
- lines.push(`${item.name}${item.score ? `(${item.score}分)` : ''}`)
- if (item.pinyin) lines.push(`拼音:${item.pinyin}`)
- if (item.source) lines.push(`出处:${item.source}`)
- if (item.wuxing) lines.push(`五行:${item.wuxing}`)
- if (item.meaning) lines.push(`寓意:${item.meaning}`)
- if (item.tone) lines.push(`音律:${item.tone}`)
- if (item.reason) lines.push(`推荐:${item.reason}`)
- lines.push('')
- })
- navigator.clipboard.writeText(lines.join('\n')).then(() => showToast('复制成功', 'primary')).catch(() => showToast('复制失败'))
- }
- function goRecharge() { router.push('/recharge') }
- function goHistory() { router.push('/ai-name-history') }
- </script>
-
- <style scoped>
- .animate-pulse-dot { animation: dotPulse 1.1s ease-in-out infinite; }
- .animate-pulse-dot2 { animation: dotPulse 1.1s ease-in-out 0.15s infinite; }
- .animate-pulse-dot3 { animation: dotPulse 1.1s ease-in-out 0.3s infinite; }
- @keyframes dotPulse {
- 0%, 100% { opacity: 0.35; transform: scale(0.82); }
- 50% { opacity: 1; transform: scale(1); }
- }
- .modal-enter-active, .modal-leave-active { transition: opacity 0.2s ease; }
- .modal-enter-from, .modal-leave-to { opacity: 0; }
- .modal-enter-active > div:last-child, .modal-leave-active > div:last-child { transition: transform 0.2s ease; }
- .modal-enter-from > div:last-child, .modal-leave-to > div:last-child { transform: scale(0.95); }
- </style>
|