feat: 登录页密码+验证码双模式 / 首页岗位优化 / 法律页面 / 后端接口完善

- 前端:登录页重构,支持密码登录、验证码登录、注册三种模式
- 前端:首页热门岗位添加「参考示例」标签,去虚构数据
- 前端:面试页顶部优化,岗位名+状态标签展示
- 前端:新增用户协议、隐私政策页面及免责声明
- 后端:新增 POST /api/user/register 注册接口
- 后端:新增 POST /api/user/set-password 设置密码接口
- 后端:修复 user.schema.ts unique 索引导致 null 冲突问题
- 后端:新增 payment-order.schema、positions.schema、site-config.schema
- 后端:package.json 新增 postbuild 脚本自动复制证书
- 管理后台:新增订单管理 Tab
This commit is contained in:
yuzhiran
2026-06-09 15:39:17 +08:00
parent 511f60d0db
commit 37cfdfe93c
27 changed files with 1045 additions and 195 deletions
+2 -2
View File
@@ -3,9 +3,9 @@
"appid": "__UNI__DEV__",
"versionName": "1.0.0",
"versionCode": "100",
"description": "AI 面试模拟 - 先模拟,再面试",
"description": "职引 - 宇之然AI磁场旗下AI模拟面试平台,提供AI面试官模拟练习、简历智能优化、大厂面经题库,助你轻松应对校招面试",
"h5": {
"title": "AI磁场",
"title": "职引 - AI模拟面试 | 宇之然AI磁场",
"router": {
"mode": "hash"
}
+11 -9
View File
@@ -1,19 +1,21 @@
{
"pages": [
{ "path": "pages/index/index", "style": { "navigationBarTitleText": "职引 - 先模拟,再上场" } },
{ "path": "pages/interview/interview", "style": { "navigationBarTitleText": "模拟面试" } },
{ "path": "pages/index/index", "style": { "navigationBarTitleText": "职引 - AI模拟面试" } },
{ "path": "pages/interview/interview", "style": { "navigationBarTitleText": "AI模拟面试" } },
{ "path": "pages/report/report", "style": { "navigationBarTitleText": "面试报告" } },
{ "path": "pages/member/member", "style": { "navigationBarTitleText": "会员中心" } },
{ "path": "pages/progress/progress", "style": { "navigationBarTitleText": "进步轨迹" } },
{ "path": "pages/contribute/contribute", "style": { "navigationBarTitleText": "贡献面经" } },
{ "path": "pages/contribute/contribute", "style": { "navigationBarTitleText": "面经分享" } },
{ "path": "pages/login/login", "style": { "navigationBarTitleText": "登录" } },
{ "path": "pages/history/history", "style": { "navigationBarTitleText": "面试记录" } },
{ "path": "pages/user/user", "style": { "navigationBarTitleText": "我的" } },
{ "path": "pages/resume/resume", "style": { "navigationBarTitleText": "我的简历" } },
{ "path": "pages/resume/resume", "style": { "navigationBarTitleText": "简历优化" } },
{ "path": "pages/internship/internship", "style": { "navigationBarTitleText": "实习搜索" } },
{ "path": "pages/about/about", "style": { "navigationBarTitleText": "关于" } },
{ "path": "pages/about/about", "style": { "navigationBarTitleText": "关于职引" } },
{ "path": "pages/admin/admin", "style": { "navigationBarTitleText": "管理后台" } },
{ "path": "pages/result/result", "style": { "navigationBarTitleText": "优化结果" } }
{ "path": "pages/result/result", "style": { "navigationBarTitleText": "优化结果" } },
{ "path": "pages/agreement/agreement", "style": { "navigationBarTitleText": "用户协议" } },
{ "path": "pages/privacy/privacy", "style": { "navigationBarTitleText": "隐私政策" } }
],
"tabBar": {
"color": "#999999",
@@ -22,14 +24,14 @@
"borderStyle": "black",
"list": [
{ "pagePath": "pages/index/index", "text": "面试", "iconPath": "static/tabbar/home.png", "selectedIconPath": "static/tabbar/home-active.png" },
{ "pagePath": "pages/history/history", "text": "记录", "iconPath": "static/tabbar/history.png", "selectedIconPath": "static/tabbar/history-active.png" },
{ "pagePath": "pages/history/history", "text": "面经", "iconPath": "static/tabbar/history.png", "selectedIconPath": "static/tabbar/history-active.png" },
{ "pagePath": "pages/user/user", "text": "我的", "iconPath": "static/tabbar/user.png", "selectedIconPath": "static/tabbar/user-active.png" }
]
},
"globalStyle": {
"navigationBarTextStyle": "black",
"navigationBarTitleText": "职引",
"navigationBarTitleText": "职引 - AI模拟面试",
"navigationBarBackgroundColor": "#ffffff",
"backgroundColor": "#f5f6f7"
}
}
}
+42 -50
View File
@@ -6,65 +6,57 @@
</view>
<view class="info-section">
<text class="info-label">产品名称</text>
<text class="info-value">职引 · AI 面试模拟</text>
<text class="info-value">职引 · 宇之然AI磁场</text>
</view>
<view class="info-section">
<text class="info-label">开发团队</text>
<text class="info-value">宇之然</text>
</view>
<view class="desc">
<text>职引是一款 AI 驱动的面试模拟工具帮助求职者通过模拟真实面试简历诊断和优化提升面试通过率</text>
<view class="info-section">
<text class="info-label">联系邮箱</text>
<text class="info-value">contact@yuzhiran.com</text>
</view>
<view class="link-section">
<view class="link-item" @click="goAgreement">
<text class="link-text">用户协议</text>
<text class="link-arrow"></text>
</view>
<view class="link-item" @click="goPrivacy">
<text class="link-text">隐私政策</text>
<text class="link-arrow"></text>
</view>
</view>
<view class="disclaimer">
<text class="disclaimer-title"> AI生成内容免责声明</text>
<text class="disclaimer-text">
本平台的模拟面试简历诊断简历优化等功能由人工智能模型生成仅供参考和学习用途AI输出的内容可能存在不准确不完整或过时的情况不构成任何专业建议用户在做出重要决策前请务必结合自身判断核实相关信息宇之然AI磁场不对因使用AI生成内容导致的任何直接或间接损失承担责任
</text>
</view>
</view>
</template>
<script setup lang="ts">
<script setup>
const goAgreement = () => uni.navigateTo({ url: '/pages/agreement/agreement' })
const goPrivacy = () => uni.navigateTo({ url: '/pages/privacy/privacy' })
</script>
<style scoped>
.page {
background: #f5f6f7;
padding: 60rpx 30rpx;
}
.logo-area {
text-align: center;
padding: 80rpx 0;
}
.logo {
font-size: 48rpx;
font-weight: 700;
background: linear-gradient(135deg, #4F46E5, #7C3AED);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
display: block;
margin-bottom: 16rpx;
}
.version {
font-size: 24rpx;
color: #999;
}
.info-section {
background: #fff;
padding: 24rpx 30rpx;
border-radius: 16rpx;
margin-bottom: 16rpx;
display: flex;
justify-content: space-between;
}
.info-label {
font-size: 26rpx;
color: #999;
}
.info-value {
font-size: 26rpx;
color: #333;
}
.desc {
margin-top: 40rpx;
font-size: 24rpx;
color: #999;
line-height: 1.8;
text-align: center;
}
</style>
.page { background: var(--color-bg); min-height: 100vh; padding: 60rpx 32rpx; }
.logo-area { text-align: center; padding: 60rpx 0 40rpx; }
.logo { font-size: 48rpx; font-weight: 800; background: linear-gradient(135deg, var(--color-gradient-start), var(--color-gradient-end)); -webkit-background-clip: text; -webkit-text-fill-color: transparent; display: block; margin-bottom: 12rpx; }
.version { font-size: 24rpx; color: var(--color-text-tertiary); }
.info-section { background: #FFF; padding: 24rpx 30rpx; border-radius: var(--radius-md); margin-bottom: 12rpx; display: flex; justify-content: space-between; align-items: center; }
.info-label { font-size: 26rpx; color: var(--color-text-secondary); }
.info-value { font-size: 26rpx; color: var(--color-text); font-weight: 500; }
.link-section { background: #FFF; border-radius: var(--radius-md); margin-top: 24rpx; overflow: hidden; }
.link-item { display: flex; justify-content: space-between; align-items: center; padding: 28rpx 30rpx; border-bottom: 1rpx solid var(--color-border); }
.link-item:last-child { border-bottom: none; }
.link-item:active { background: var(--color-bg); }
.link-text { font-size: 26rpx; color: var(--color-text); }
.link-arrow { font-size: 32rpx; color: var(--color-text-tertiary); }
.disclaimer { margin-top: 40rpx; background: #FFF8E1; border-radius: var(--radius-md); padding: 24rpx; }
.disclaimer-title { font-size: 24rpx; font-weight: 700; color: #F59E0B; display: block; margin-bottom: 12rpx; }
.disclaimer-text { font-size: 22rpx; color: var(--color-text-secondary); line-height: 1.8; }
</style>
+85
View File
@@ -17,6 +17,7 @@
<text class="tab" :class="{ active: tab === 'overview' }" @click="switchTab('overview')">概览</text>
<text class="tab" :class="{ active: tab === 'users' }" @click="switchTab('users')">用户管理</text>
<text class="tab" :class="{ active: tab === 'interviews' }" @click="switchTab('interviews')">面试记录</text>
<text class="tab" :class="{ active: tab === 'orders' }" @click="switchTab('orders')">订单</text>
<text class="tab" :class="{ active: tab === 'admins' }" @click="switchTab('admins')">管理员</text>
<text class="tab" :class="{ active: tab === 'config' }" @click="switchTab('config')">配置</text>
</view>
@@ -70,6 +71,37 @@
</view>
<!-- 订单 -->
<view v-if="tab === 'orders'" class="section">
<view class="tabs in-tab">
<text class="tab" :class="{ active: orderFilter === '' }" @click="orderFilter='';loadOrders()">全部</text>
<text class="tab" :class="{ active: orderFilter === 'pending' }" @click="orderFilter='pending';loadOrders()">待支付</text>
<text class="tab" :class="{ active: orderFilter === 'success' }" @click="orderFilter='success';loadOrders()">已支付</text>
<text class="tab" :class="{ active: orderFilter === 'refunded' }" @click="orderFilter='refunded';loadOrders()">已退款</text>
</view>
<view class="order-list" v-if="!orderLoading">
<view class="order-row" v-for="o in orders" :key="o._id">
<view class="order-info">
<text class="order-id">订单号: {{ o.outTradeNo }}</text>
<text class="order-user">用户: {{ o.userPhone || o.userId.slice(-6) }}</text>
</view>
<view class="order-meta">
<text class="order-amount">¥{{ (o.amount / 100).toFixed(1) }}</text>
<view class="order-status" :class="o.status === 'success' ? 'paid' : o.status === 'refunded' ? 'refund' : 'pend'">
{{ o.status === 'success' ? '已支付' : o.status === 'refunded' ? '已退款' : '待支付' }}
</view>
<text class="order-time">{{ o.createdAt ? o.createdAt.slice(0,16).replace('T',' ') : '--' }}</text>
<view class="order-actions" v-if="o.status === 'pending'">
<text class="sync-btn" @click="syncOrder(o.outTradeNo)">同步</text>
</view>
</view>
</view>
<text class="load-more" v-if="ordersTotal > orders.length" @click="loadMoreOrders">加载更多</text>
<text class="empty-text" v-if="orders.length === 0 && !orderLoading">暂无订单</text>
</view>
<text class="loading-text" v-if="orderLoading">加载中...</text>
</view>
<!-- 套餐配置 -->
<view v-if="tab === 'config'" class="section">
<view class="config-card" v-if="!cfgLoading">
@@ -140,6 +172,11 @@ const adminList = ref([])
const searchResult = ref(null)
const memberConfig = ref({ interview: { maxRoundsFree: 5, maxRoundsVip: 10, dailyFreeLimit: 3 }, diagnosis: { dailyFreeLimit: 2 }, optimize: { dailyFreeLimit: 2 }, price: { monthly: 2900 } })
const cfgLoading = ref(false)
const orders = ref([])
const ordersTotal = ref(0)
const ordersPage = ref(1)
const orderLoading = ref(false)
const orderFilter = ref('')
const token = () => uni.getStorageSync('token') || ''
@@ -182,6 +219,7 @@ const switchTab = (t) => {
if (t === 'interviews' && interviews.value.length === 0) loadInterviews()
if (t === 'admins' && adminList.value.length === 0) loadAdmins()
if (t === 'config') loadConfig()
if (t === 'orders') loadOrders()
}
const loadUsers = async () => {
@@ -220,6 +258,39 @@ const loadConfig = async () => {
finally { cfgLoading.value = false }
}
const loadOrders = async () => {
orderLoading.value = true
ordersPage.value = 1
try {
let url = '/orders?page=1&limit=20'
if (orderFilter.value) url += '&status=' + orderFilter.value
const res = await apiAdmin(url)
if (res.statusCode === 200) { orders.value = res.data.orders || []; ordersTotal.value = res.data.total || 0 }
} catch(e) { console.error(e) }
finally { orderLoading.value = false }
}
const loadMoreOrders = async () => {
ordersPage.value++
let url = '/orders?page=' + ordersPage.value + '&limit=20'
if (orderFilter.value) url += '&status=' + orderFilter.value
try {
const res = await apiAdmin(url)
if (res.statusCode === 200) orders.value = [...orders.value, ...(res.data.orders || [])]
} catch(e) { console.error(e) }
}
const syncOrder = async (outTradeNo) => {
uni.showToast({ title: '同步中...', icon: 'none' })
try {
const res = await apiAdmin('/order/sync', { method: 'POST', data: { outTradeNo } })
if (res.statusCode === 200) {
uni.showToast({ title: '同步完成', icon: 'success' })
loadOrders()
} else { uni.showToast({ title: '同步失败', icon: 'none' }) }
} catch { uni.showToast({ title: '同步失败', icon: 'none' }) }
}
const loadAdmins = async () => {
try {
const res = await apiAdmin('/admins')
@@ -314,6 +385,20 @@ const setVip = async (targetUserId) => {
.admin-set-btn.done { color: var(--color-success); border-color: var(--color-success); }
.admin-badge { font-size: 18rpx; background: var(--color-primary); color: #FFF; padding: 2rpx 10rpx; border-radius: var(--radius-round); }
.empty-text { text-align: center; padding: 20rpx; color: var(--color-text-tertiary); font-size: 22rpx; display: block; }
.order-list { display: flex; flex-direction: column; gap: 8rpx; }
.order-row { background: #FFF; border-radius: var(--radius-sm); padding: 16rpx; }
.order-info { display: flex; justify-content: space-between; margin-bottom: 8rpx; }
.order-id { font-size: 22rpx; color: var(--color-text); font-weight: 500; }
.order-user { font-size: 20rpx; color: var(--color-text-tertiary); }
.order-meta { display: flex; align-items: center; gap: 12rpx; }
.order-amount { font-size: 28rpx; font-weight: 700; color: var(--color-primary); }
.order-status { font-size: 20rpx; padding: 2rpx 12rpx; border-radius: var(--radius-round); }
.order-status.paid { background: #ECFDF5; color: var(--color-success); }
.order-status.refund { background: #FEF3C7; color: var(--color-warning); }
.order-status.pend { background: #F3F4F6; color: var(--color-text-tertiary); }
.order-time { font-size: 20rpx; color: var(--color-text-tertiary); flex: 1; text-align: right; }
.order-actions { }
.sync-btn { font-size: 20rpx; color: var(--color-primary); padding: 4rpx 12rpx; border: 2rpx solid var(--color-primary); border-radius: var(--radius-round); }
.config-card { background: #FFF; border-radius: var(--radius-sm); padding: 20rpx; margin-bottom: 12rpx; }
.cfg-title { font-size: 24rpx; font-weight: 600; color: var(--color-text); margin-bottom: 12rpx; }
.cfg-row { display: flex; justify-content: space-between; padding: 8rpx 0; font-size: 22rpx; color: var(--color-text-secondary); }
@@ -0,0 +1,44 @@
<template>
<view class="page">
<view class="section">
<text class="title">用户协议</text>
<text class="update">最后更新2026年6月8日</text>
<text class="h2">1. 接受条款</text>
<text class="body">欢迎使用宇之然AI磁场以下简称"本平台"在访问或使用本平台前请仔细阅读本用户协议通过注册登录或使用本平台的任何服务即表示您已阅读理解并同意受本协议约束</text>
<text class="h2">2. 服务说明</text>
<text class="body">本平台提供基于人工智能技术的模拟面试简历诊断与优化面经分享题库练习等求职辅助服务所有AI生成的内容仅供参考不构成专业建议</text>
<text class="h2">3. 用户账户</text>
<text class="body">3.1 您在注册时需提供真实准确的邮箱或手机号信息\n3.2 您对账户下的所有行为负责请妥善保管登录凭证\n3.3 如发现账户被盗用请立即联系我们</text>
<text class="h2">4. 用户行为规范</text>
<text class="body">4.1 不得利用本平台从事任何违法活动\n4.2 不得恶意刷量攻击系统或干扰服务正常运行\n4.3 不得侵犯他人知识产权或隐私权\n4.4 面经分享内容应为真实体验不得捏造或抄袭</text>
<text class="h2">5. 免责声明</text>
<text class="body">5.1 本平台的AI面试简历诊断简历优化等功能输出由人工智能模型生成仅供用户参考不构成任何形式的专业建议\n5.2 用户应结合自身判断核实AI输出的准确性和适用性平台不对因使用AI生成内容导致的任何损失承担责任\n5.3 面经内容由用户自发贡献平台不保证其真实性完整性或时效性</text>
<text class="h2">6. 会员服务</text>
<text class="body">6.1 会员服务为订阅制按月度计费\n6.2 会员生效后费用不予退还但可按剩余天数申请等额延期\n6.3 平台有权在提前通知的情况下调整会员权益和价格</text>
<text class="h2">7. 知识产权</text>
<text class="body">本平台的品牌LOGO软件著作权等知识产权归宇之然所有未经授权不得复制修改或用于商业用途</text>
<text class="h2">8. 协议变更</text>
<text class="body">本平台有权随时修改本协议修改后的协议一经发布即生效如您继续使用服务视为接受修改后的协议</text>
<text class="h2">9. 联系我们</text>
<text class="body">邮箱contact@yuzhiran.com</text>
</view>
</view>
</template>
<style scoped>
.page { background: var(--color-bg); min-height: 100vh; padding: 32rpx; }
.section { background: #FFF; border-radius: var(--radius-lg); padding: 32rpx; }
.title { font-size: 36rpx; font-weight: 800; color: var(--color-text); display: block; margin-bottom: 8rpx; }
.update { font-size: 22rpx; color: var(--color-text-tertiary); display: block; margin-bottom: 32rpx; }
.h2 { font-size: 28rpx; font-weight: 700; color: var(--color-text); display: block; margin-top: 28rpx; margin-bottom: 12rpx; }
.body { font-size: 26rpx; color: var(--color-text-secondary); line-height: 1.8; white-space: pre-wrap; display: block; }
</style>
+25 -10
View File
@@ -74,19 +74,27 @@
<!-- 热门岗位 -->
<view class="section">
<view class="section-header">
<text class="section-title">热门岗位</text>
<view class="section-title-row">
<text class="section-title">热门岗位</text>
<text class="section-tag-demo">参考示例</text>
</view>
<text class="section-desc">点击直接面试</text>
</view>
<view class="position-list card" v-if="!positionsLoading">
<view class="pos-item" v-for="(pos, idx) in hotPositions" :key="idx" @click="startInterview(pos)">
<view class="pos-left">
<view class="pos-rank">{{ idx + 1 }}</view>
<text class="pos-icon">{{ posIcons[idx] || '💼' }}</text>
<view class="pos-body">
<text class="pos-name">{{ pos.name }}</text>
<text class="pos-company">{{ pos.company }}</text>
<view class="pos-meta-row">
<text class="pos-company">{{ pos.company || '参考公司' }}</text>
<text class="pos-salary">{{ pos.salary || '参考薪资' }}</text>
</view>
</view>
</view>
<text class="pos-salary">{{ pos.salary }}</text>
<view class="pos-action">
<text class="pos-action-text">立即模拟</text>
</view>
</view>
</view>
<view class="loading-tip" v-if="positionsLoading">加载岗位中...</view>
@@ -103,6 +111,7 @@ import { api } from '../../config'
const userInfo = ref(null)
const greeting = ref('')
const hotPositions = ref([])
const posIcons = ['💻', '⚙️', '🤖', '📊', '🎨', '🧪', '📱', '🔧']
const positionsLoading = ref(true)
const dailyQuestion = ref(null)
const showAnswer = ref(false)
@@ -175,8 +184,10 @@ const startInterview = (pos) => uni.navigateTo({ url: `/pages/interview/intervie
.section { padding: 32rpx 32rpx 0; }
.section:first-of-type { margin-top: -40rpx; padding-top: 0; }
.section-header { display: flex; justify-content: space-between; align-items: baseline; margin-bottom: 20rpx; }
.section-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20rpx; }
.section-title { font-size: 30rpx; font-weight: 700; color: var(--color-text); }
.section-title-row { display: flex; align-items: center; gap: 12rpx; }
.section-tag-demo { font-size: 18rpx; color: #9CA3AF; background: #F3F4F6; padding: 2rpx 10rpx; border-radius: 6rpx; }
.section-desc { font-size: 22rpx; color: var(--color-primary); }
.feature-list { display: flex; flex-direction: column; gap: 16rpx; }
@@ -217,12 +228,16 @@ const startInterview = (pos) => uni.navigateTo({ url: `/pages/interview/intervie
.position-list { border-radius: var(--radius-lg); overflow: hidden; }
.pos-item { padding: 24rpx 28rpx; border-bottom: 1rpx solid var(--color-border); display: flex; justify-content: space-between; align-items: center; }
.pos-item:last-child { border-bottom: none; }
.pos-left { display: flex; align-items: center; gap: 16rpx; }
.pos-rank { width: 40rpx; height: 40rpx; border-radius: 10rpx; background: #F3F4F6; display: flex; align-items: center; justify-content: center; font-size: 22rpx; font-weight: 700; color: var(--color-primary); flex-shrink: 0; }
.pos-body { display: flex; flex-direction: column; }
.pos-item:active { background: var(--color-bg); }
.pos-left { display: flex; align-items: center; gap: 16rpx; flex: 1; min-width: 0; }
.pos-icon { font-size: 36rpx; width: 56rpx; height: 56rpx; display: flex; align-items: center; justify-content: center; background: #F3F4F6; border-radius: 14rpx; flex-shrink: 0; }
.pos-body { display: flex; flex-direction: column; flex: 1; min-width: 0; }
.pos-name { font-size: 28rpx; font-weight: 600; color: var(--color-text); }
.pos-company { font-size: 22rpx; color: var(--color-text-tertiary); margin-top: 4rpx; }
.pos-salary { font-size: 24rpx; color: var(--color-primary); font-weight: 600; }
.pos-meta-row { display: flex; align-items: center; gap: 10rpx; margin-top: 4rpx; }
.pos-company { font-size: 20rpx; color: var(--color-text-tertiary); }
.pos-salary { font-size: 20rpx; color: var(--color-primary); background: #EEF2FF; padding: 2rpx 10rpx; border-radius: 6rpx; }
.pos-action { flex-shrink: 0; margin-left: 16rpx; }
.pos-action-text { font-size: 22rpx; color: var(--color-primary); font-weight: 600; }
.loading-tip { text-align: center; padding: 40rpx; font-size: 24rpx; color: var(--color-text-tertiary); background: #FFF; border-radius: var(--radius-lg); }
.bottom-spacer { height: 40rpx; }
</style>
+18 -4
View File
@@ -5,6 +5,10 @@
<view class="topbar-inner">
<view class="back-btn" @click="confirmExit"><text class="back-arrow"></text></view>
<view class="topbar-center">
<view class="topbar-pos-row">
<text class="topbar-position">{{ position || 'AI面试' }}</text>
<text class="topbar-status">面试中</text>
</view>
<view class="progress-track" v-if="interviewId">
<view class="progress-fill" :style="{ width: progressPercent + '%' }"></view>
</view>
@@ -43,6 +47,8 @@
<text class="send-icon"></text>
</view>
</view>
<!-- AI 免责提示 -->
<view class="disclaimer-bar" v-if="!isComplete">AI 生成内容仅供参考请核实重要信息</view>
<!-- Complete -->
<view class="complete-bar" v-else>
@@ -56,14 +62,14 @@ import { ref, computed, onMounted, onBeforeUnmount, nextTick } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { api } from '../../config'
const messages = ref([{ role: 'ai', content: '你好!我是 AI 面试官,准备好就开始吧' }])
const messages = ref([{ role: 'ai', content: '你好!我是 AI 面试官,请选择岗位开始模拟面试' }])
const inputText = ref('')
const aiLoading = ref(false)
const interviewId = ref('')
const answeredCount = ref(0)
const isComplete = ref(false)
const scrollToId = ref('')
const position = ref('通用岗位')
const position = ref('')
let timerSeconds = 0
let timerInterval = null
@@ -75,7 +81,11 @@ const formatTime = computed(() => {
const token = computed(() => uni.getStorageSync('token') || '')
onLoad((options) => {
if (options?.position) position.value = decodeURIComponent(options.position)
if (options?.position) {
const pos = decodeURIComponent(options.position)
position.value = pos
messages.value = [{ role: 'ai', content: `你好!我是你的专属 ${pos} 面试官,准备好了就开始吧!` }]
}
})
onMounted(() => { timerInterval = setInterval(() => timerSeconds++, 1000); if (token.value) startInterview() })
@@ -151,9 +161,12 @@ const confirmExit = () => {
}
.back-arrow { font-size: 36rpx; color: #FFFFFF; font-weight: 300; line-height: 1; }
.topbar-center { flex: 1; display: flex; flex-direction: column; gap: 8rpx; }
.topbar-pos-row { display: flex; align-items: center; gap: 10rpx; }
.topbar-position { font-size: 26rpx; color: #FFFFFF; font-weight: 600; }
.topbar-status { font-size: 18rpx; color: #FFFFFF; background: rgba(255,255,255,0.2); padding: 2rpx 12rpx; border-radius: 20rpx; }
.progress-track { height: 6rpx; background: rgba(255,255,255,0.2); border-radius: 3rpx; overflow: hidden; }
.progress-fill { height: 100%; background: #FFFFFF; border-radius: 3rpx; transition: width 0.5s cubic-bezier(0.25, 0.46, 0.45, 0.94); }
.topbar-timer { font-size: 22rpx; color: rgba(255,255,255,0.8); font-variant-numeric: tabular-nums; }
.topbar-timer { font-size: 20rpx; color: rgba(255,255,255,0.7); font-variant-numeric: tabular-nums; }
.topbar-right { width: 60rpx; flex-shrink: 0; }
/* ===== Chat ===== */
@@ -207,4 +220,5 @@ const confirmExit = () => {
/* Complete */
.complete-bar { background: #FFFFFF; padding: 20rpx 24rpx; padding-bottom: calc(20rpx + var(--safe-bottom)); }
.cta-btn { width: 100%; height: 88rpx; line-height: 88rpx; background: linear-gradient(135deg, var(--color-gradient-start), var(--color-gradient-mid)); color: #FFFFFF; border-radius: var(--radius-md); font-size: 28rpx; font-weight: 600; border: none; }
.disclaimer-bar { text-align: center; font-size: 20rpx; color: var(--color-text-tertiary); padding: 8rpx 24rpx; background: #FFFFFF; border-top: 1rpx solid var(--color-border); }
</style>
+269 -63
View File
@@ -8,105 +8,285 @@
</view>
<view class="form-section">
<!-- 登录方式切换 -->
<!-- Tab登录 / 注册 / 微信 -->
<view class="tab-bar">
<text class="tab" :class="{ active: mode === 'email' }" @click="mode='email'">邮箱登录</text>
<text class="tab" :class="{ active: mode === 'wechat' }" @click="mode='wechat'" v-if="isMp">微信登录</text>
<text class="tab" :class="{ active: mainTab === 'login' }" @click="mainTab='login'">登录</text>
<text class="tab" :class="{ active: mainTab === 'register' }" @click="mainTab='register'">注册</text>
<text class="tab" :class="{ active: mainTab === 'wechat' }" @click="mainTab='wechat'" v-if="isMp">微信登录</text>
</view>
<!-- 邮箱登录 -->
<view class="card" v-if="mode === 'email'">
<text class="card-title">邮箱登录</text>
<!-- ========== 登录 ========== -->
<view class="card" v-if="mainTab === 'login'">
<!-- Tab密码 / 验证码 -->
<view class="sub-tab-bar">
<text class="sub-tab" :class="{ active: loginMode === 'password' }" @click="loginMode='password'">密码登录</text>
<text class="sub-tab" :class="{ active: loginMode === 'code' }" @click="loginMode='code'">验证码登录</text>
</view>
<!-- 密码登录 -->
<view v-if="loginMode === 'password'">
<view class="field">
<text class="field-label">邮箱</text>
<input class="input" type="text" v-model="email" placeholder="请输入邮箱" />
</view>
<view class="field">
<text class="field-label">密码</text>
<input class="input" type="password" v-model="password" placeholder="请输入密码" @confirm="doPasswordLogin" />
</view>
<button class="login-btn" :disabled="!canPasswordLogin || pwdLoading" @click="doPasswordLogin">
{{ pwdLoading ? '登录中...' : '登录' }}
</button>
<view class="switch-hint" @click="loginMode='code'">忘记密码使用验证码登录</view>
</view>
<!-- 验证码登录 -->
<view v-else>
<!-- 调试信息发布前删掉 -->
<view class="debug-info" v-if="true">debug: emailSent={{emailSent}} cooldown={{cooldown}}</view>
<view class="field">
<text class="field-label">邮箱</text>
<view class="inline-row">
<input class="input inline-input" type="text" v-model="email" placeholder="请输入邮箱" />
<button class="code-btn" :disabled="cooldown > 0 || !email" @click="sendEmailCode">
{{ cooldown > 0 ? cooldown + 's' : (emailSent ? '重新获取' : '获取验证码') }}
</button>
</view>
</view>
<view class="field" v-if="emailSent">
<text class="field-label">验证码</text>
<input class="input" type="number" maxlength="6" v-model="emailCode" placeholder="请输入6位验证码" />
</view>
<button class="login-btn" :disabled="!emailSent || !emailCode || emailLoading" @click="doEmailLogin">
{{ emailLoading ? '登录中...' : '登录' }}
</button>
<view class="switch-hint" @click="loginMode='password'">已有密码使用密码登录</view>
</view>
</view>
<!-- ========== 注册 ========== -->
<view class="card" v-if="mainTab === 'register'">
<text class="card-title">创建账号</text>
<text class="card-sub">注册后享受 AI 面试模拟服务</text>
<view class="field">
<text class="field-label">邮箱</text>
<input class="input" type="text" v-model="email" placeholder="请输入邮箱" @confirm="sendEmailCode" />
<input class="input" type="text" v-model="email" placeholder="请输入邮箱" />
</view>
<view class="field" v-if="emailSent">
<text class="field-label">验证码</text>
<view class="code-row">
<input class="input code-input" type="number" maxlength="6" v-model="emailCode" placeholder="6位验证码" @confirm="doEmailLogin" />
<button class="code-btn" :disabled="cooldown > 0" @click="sendEmailCode">
{{ cooldown > 0 ? cooldown + 's' : '获取验证码' }}
</button>
</view>
<view class="field">
<text class="field-label">密码</text>
<input class="input" type="password" v-model="password" placeholder="至少6位密码" />
</view>
<button class="login-btn" v-if="!emailSent" @click="sendEmailCode">{{ emailSending ? '发送中...' : '获取验证码' }}</button>
<button class="login-btn" v-else :disabled="!emailCode" @click="doEmailLogin">{{ emailLoading ? '登录中...' : '登录' }}</button>
<view class="field">
<text class="field-label">确认密码</text>
<input class="input" type="password" v-model="confirmPassword" placeholder="再次输入密码" @confirm="doRegister" />
</view>
<button class="login-btn" :disabled="!canRegister || regLoading" @click="doRegister">
{{ regLoading ? '注册中...' : '注册' }}
</button>
<view class="switch-hint" @click="mainTab='login'">已有账号去登录</view>
</view>
<!-- 微信登录仅小程序 -->
<view class="card" v-if="mode === 'wechat' && isMp">
<!-- ========== 微信一键登录 ========== -->
<view class="card" v-if="mainTab === 'wechat' && isMp">
<text class="card-title">微信一键登录</text>
<text class="card-sub">授权后自动创建账号</text>
<button class="login-btn wx-btn" @click="doWxLogin">{{ wxLoading ? '登录中...' : '微信一键登录' }}</button>
</view>
<!-- 法律声明 -->
<view class="legal">
<text class="legal-text">登录即表示同意</text>
<text class="legal-link" @click="goAgreement">用户协议</text>
<text class="legal-text"></text>
<text class="legal-link" @click="goPrivacy">隐私政策</text>
</view>
</view>
<!-- 设置密码弹窗验证码登录后引导 -->
<view class="overlay" v-if="showSetPwd" @click="showSetPwd=false"></view>
<view class="pwd-modal" v-if="showSetPwd">
<text class="modal-title">设置登录密码</text>
<text class="modal-desc">设置密码后下次可直接用密码登录无需等待验证码</text>
<input class="input" type="password" v-model="newPassword" placeholder="至少6位密码" />
<view class="modal-btns">
<text class="modal-btn skip" @click="skipSetPwd">暂不设置</text>
<text class="modal-btn confirm" @click="doSetPassword">确认设置</text>
</view>
</view>
</view>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { ref, computed, onMounted, onBeforeUnmount } from 'vue'
import { api } from '../../config'
const mode = ref('email')
const mainTab = ref('login')
const loginMode = ref('password') // 'password' | 'code'
const isMp = ref(false)
const email = ref('')
const password = ref('')
const confirmPassword = ref('')
const emailCode = ref('')
const emailSent = ref(false)
const emailSending = ref(false)
const emailLoading = ref(false)
const pwdLoading = ref(false)
const regLoading = ref(false)
const wxLoading = ref(false)
const cooldown = ref(0)
let timer = null
// 设置密码弹窗
const showSetPwd = ref(false)
const newPassword = ref('')
const canPasswordLogin = computed(() => email.value.trim() && password.value.length >= 6 && !pwdLoading.value)
const canRegister = computed(() => email.value.trim() && password.value.length >= 6 && password.value === confirmPassword.value && !regLoading.value)
onMounted(() => {
// #ifdef MP-WEIXIN
isMp.value = true
mode.value = 'wechat'
mainTab.value = 'wechat'
// #endif
})
// 邮箱验证码
const sendEmailCode = async () => {
const re = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
if (!re.test(email.value)) { uni.showToast({ title: '请输入正确的邮箱', icon: 'none' }); return }
emailSending.value = true
onBeforeUnmount(() => { if (timer) clearInterval(timer) })
// 辅助
const showToast = (title, icon = 'none') => uni.showToast({ title, icon })
const loginSuccess = (data) => {
uni.setStorageSync('token', data.token)
if (data.user) uni.setStorageSync('userInfo', JSON.stringify(data.user))
showToast('登录成功', 'success')
setTimeout(() => uni.navigateBack(), 500)
}
// ====== 密码登录 ======
const doPasswordLogin = async () => {
if (!canPasswordLogin.value) return
pwdLoading.value = true
try {
const res = await uni.request({ url: api('/user/send-email-code'), method: 'POST', data: { email: email.value } })
if (res.statusCode === 200) {
emailSent.value = true
uni.showToast({ title: '验证码已发送', icon: 'success' })
startCooldown()
} else { uni.showToast({ title: res.data?.message || '发送失败', icon: 'none' }) }
} catch { uni.showToast({ title: '网络错误', icon: 'none' }) }
finally { emailSending.value = false }
const res = await uni.request({
url: api('/user/password-login'), method: 'POST',
header: { 'Content-Type': 'application/json' },
data: { email: email.value.trim(), password: password.value },
})
if (res.statusCode === 200 && res.data?.token) {
loginSuccess(res.data)
} else {
showToast(res.data?.message || '登录失败')
}
} catch { showToast('网络错误') }
finally { pwdLoading.value = false }
}
// ====== 验证码 ======
const sendEmailCode = () => {
if (cooldown.value > 0) { showToast('请稍后再试'); return }
const re = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
if (!re.test(email.value)) { showToast('请输入正确的邮箱'); return }
console.log('[sendEmailCode] 发送中,email:', email.value)
uni.request({
url: api('/user/send-email-code'),
method: 'POST',
header: { 'Content-Type': 'application/json' },
data: { email: email.value },
success: (res) => {
console.log('[sendEmailCode] success res:', JSON.stringify(res))
if (res.statusCode === 200) {
emailSent.value = true
console.log('[sendEmailCode] emailSent 设为 true')
showToast('验证码已发送', 'success')
startCooldown()
} else {
const msg = (res.data && res.data.message) || '发送失败'
showToast(msg)
}
},
fail: (err) => {
console.error('[sendEmailCode] fail:', err)
showToast('网络错误')
}
})
}
const startCooldown = () => {
cooldown.value = 60
if (timer) clearInterval(timer)
timer = setInterval(() => { if (--cooldown.value <= 0) { clearInterval(timer); timer = null } }, 1000)
if (timer) clearTimeout(timer)
const tick = () => {
cooldown.value--
if (cooldown.value <= 0) {
timer = null
return
}
timer = setTimeout(tick, 1000)
}
timer = setTimeout(tick, 1000)
}
// 邮箱登录
const doEmailLogin = async () => {
if (!emailCode.value) return
emailLoading.value = true
try {
const res = await uni.request({ url: api('/user/email-login'), method: 'POST', data: { email: email.value, code: emailCode.value } })
const res = await uni.request({
url: api('/user/email-login'), method: 'POST',
header: { 'Content-Type': 'application/json' },
data: { email: email.value.trim(), code: emailCode.value },
})
if (res.statusCode === 200 && res.data?.token) {
uni.setStorageSync('token', res.data.token)
if (res.data.user) uni.setStorageSync('userInfo', JSON.stringify(res.data.user))
uni.showToast({ title: '登录成功', icon: 'success' })
setTimeout(() => uni.navigateBack(), 500)
} else { uni.showToast({ title: res.data?.message || '登录失败', icon: 'none' }) }
} catch { uni.showToast({ title: '登录失败', icon: 'none' }) }
loginSuccess(res.data)
// 新用户(isNew)且没有密码 → 引导设置密码
if (res.data.isNew || !res.data.hasPassword) {
setTimeout(() => { showSetPwd.value = true; newPassword.value = '' }, 800)
}
} else {
showToast(res.data?.message || '登录失败')
}
} catch { showToast('网络错误') }
finally { emailLoading.value = false }
}
// 微信静默登录
// ====== 注册 ======
const doRegister = async () => {
if (!canRegister.value) return
regLoading.value = true
try {
const res = await uni.request({
url: api('/user/register'), method: 'POST',
header: { 'Content-Type': 'application/json' },
data: { email: email.value.trim(), password: password.value },
})
if (res.statusCode === 200 && res.data?.token) {
loginSuccess(res.data)
} else if (res.statusCode === 409) {
showToast('该邮箱已注册,请直接登录')
mainTab.value = 'login'
} else {
showToast(res.data?.message || '注册失败')
}
} catch { showToast('网络错误') }
finally { regLoading.value = false }
}
// ====== 设置密码 ======
const doSetPassword = async () => {
if (newPassword.value.length < 6) { showToast('密码至少6位'); return }
const token = uni.getStorageSync('token')
try {
const res = await uni.request({
url: api('/user/set-password'), method: 'POST',
header: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' },
data: { password: newPassword.value },
})
if (res.statusCode === 200 || res.statusCode === 201) {
showToast('密码设置成功', 'success')
showSetPwd.value = false
}
} catch { showToast('设置失败') }
}
const skipSetPwd = () => { showSetPwd.value = false }
// ====== 微信登录 ======
const doWxLogin = async () => {
// #ifdef MP-WEIXIN
wxLoading.value = true
@@ -114,17 +294,16 @@ const doWxLogin = async () => {
const { code } = await uni.login()
const res = await uni.request({ url: api('/user/wx-login'), method: 'POST', data: { code } })
if (res.statusCode === 200 && res.data?.token) {
uni.setStorageSync('token', res.data.token)
if (res.data.user) uni.setStorageSync('userInfo', JSON.stringify(res.data.user))
uni.showToast({ title: '登录成功', icon: 'success' })
setTimeout(() => uni.navigateBack(), 500)
} else { uni.showToast({ title: '微信登录失败', icon: 'none' }) }
} catch { uni.showToast({ title: '微信登录失败', icon: 'none' }) }
loginSuccess(res.data)
} else { showToast('微信登录失败') }
} catch { showToast('微信登录失败') }
finally { wxLoading.value = false }
// #endif
}
const wxLoading = ref(false)
// ====== 法律页面 ======
const goAgreement = () => uni.navigateTo({ url: '/pages/agreement/agreement' })
const goPrivacy = () => uni.navigateTo({ url: '/pages/privacy/privacy' })
</script>
<style scoped>
@@ -134,23 +313,50 @@ const wxLoading = ref(false)
.brand-tagline { font-size: 24rpx; color: var(--color-text-tertiary); margin-top: 8rpx; display: block; }
.form-section { padding: 0 32rpx; flex: 1; }
/* Tab */
/* ===== Main Tab ===== */
.tab-bar { display: flex; gap: 0; margin-bottom: 24rpx; background: #FFFFFF; border-radius: var(--radius-md); padding: 4rpx; }
.tab { flex: 1; text-align: center; padding: 16rpx; font-size: 26rpx; color: var(--color-text-secondary); border-radius: var(--radius-sm); }
.tab { flex: 1; text-align: center; padding: 16rpx; font-size: 26rpx; color: var(--color-text-secondary); border-radius: var(--radius-sm); transition: all 0.2s;}
.tab.active { background: var(--color-primary); color: #FFFFFF; font-weight: 600; }
/* Form */
/* ===== Sub Tab ===== */
.sub-tab-bar { display: flex; gap: 0; margin-bottom: 24rpx; background: #F9FAFB; border-radius: var(--radius-sm); padding: 4rpx; }
.sub-tab { flex: 1; text-align: center; padding: 12rpx; font-size: 24rpx; color: var(--color-text-tertiary); border-radius: var(--radius-sm); transition: all 0.2s;}
.sub-tab.active { background: #FFFFFF; color: var(--color-text); font-weight: 600; box-shadow: 0 1rpx 4rpx rgba(0,0,0,0.06); }
/* ===== Card ===== */
.card { background: #FFFFFF; border-radius: var(--radius-lg); padding: 32rpx; box-shadow: var(--shadow-sm); }
.card-title { font-size: 30rpx; font-weight: 700; color: var(--color-text); display: block; }
.card-sub { font-size: 22rpx; color: var(--color-text-tertiary); margin-top: 6rpx; margin-bottom: 24rpx; display: block; }
/* ===== Fields ===== */
.field { margin-bottom: 20rpx; }
.field-label { font-size: 22rpx; color: var(--color-text-secondary); margin-bottom: 8rpx; display: block; }
.input { width: 100%; height: 72rpx; background: #F9FAFB; border: 2rpx solid var(--color-border); border-radius: var(--radius-sm); padding: 0 20rpx; font-size: 26rpx; box-sizing: border-box; }
.code-row { display: flex; gap: 12rpx; align-items: center; }
.code-input { flex: 1; }
.code-btn { height: 72rpx; padding: 0 24rpx; background: #F3F4F6; border: 2rpx solid var(--color-border); border-radius: var(--radius-sm); font-size: 24rpx; color: var(--color-primary); white-space: nowrap; flex-shrink: 0; }
.code-btn:disabled { color: var(--color-text-tertiary); }
.inline-row { display: flex; gap: 12rpx; align-items: center; }
.inline-input { flex: 1; }
.code-btn { height: 72rpx; padding: 0 20rpx; background: var(--color-primary); color: #FFFFFF; border: none; border-radius: var(--radius-sm); font-size: 24rpx; white-space: nowrap; flex-shrink: 0; line-height: 72rpx; }
.code-btn:disabled { background: #D1D5DB; color: #9CA3AF; }
/* ===== Buttons ===== */
.login-btn { width: 100%; height: 80rpx; background: linear-gradient(135deg, var(--color-gradient-start), var(--color-gradient-mid)); color: #FFFFFF; border-radius: var(--radius-md); font-size: 28rpx; font-weight: 600; border: none; margin-top: 16rpx; display: flex; align-items: center; justify-content: center; }
.login-btn:disabled { opacity: 0.5; }
.wx-btn { background: linear-gradient(135deg, #07C160, #06AD56); }
/* ===== Switch Hint ===== */
.switch-hint { text-align: center; font-size: 22rpx; color: var(--color-primary); padding: 20rpx 0 4rpx; }
/* ===== Legal ===== */
.legal { display: flex; justify-content: center; align-items: center; gap: 4rpx; margin-top: 24rpx; flex-wrap: wrap; }
.legal-text { font-size: 22rpx; color: var(--color-text-tertiary); }
.legal-link { font-size: 22rpx; color: var(--color-primary); }
/* ===== Password Modal ===== */
.overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.4); z-index: 100; }
.pwd-modal { position: fixed; left: 32rpx; right: 32rpx; top: 50%; transform: translateY(-50%); background: #FFFFFF; border-radius: var(--radius-lg); padding: 40rpx 32rpx; z-index: 101; }
.modal-title { font-size: 30rpx; font-weight: 700; color: var(--color-text); display: block; text-align: center; }
.modal-desc { font-size: 22rpx; color: var(--color-text-tertiary); text-align: center; display: block; margin: 12rpx 0 24rpx; line-height: 1.5; }
.modal-btns { display: flex; gap: 16rpx; margin-top: 24rpx; }
.modal-btn { flex: 1; text-align: center; padding: 20rpx; font-size: 26rpx; border-radius: var(--radius-sm); }
.modal-btn.skip { background: #F3F4F6; color: var(--color-text-secondary); }
.modal-btn.confirm { background: linear-gradient(135deg, var(--color-gradient-start), var(--color-gradient-mid)); color: #FFFFFF; font-weight: 600; }
</style>
+43
View File
@@ -0,0 +1,43 @@
<template>
<view class="page">
<view class="section">
<text class="title">隐私政策</text>
<text class="update">最后更新2026年6月8日</text>
<text class="body">宇之然AI磁场以下简称"本平台"重视您的隐私本隐私政策说明我们如何收集使用存储和保护您的个人信息</text>
<text class="h2">1. 收集的信息</text>
<text class="body">1.1 账户信息注册时收集的邮箱或手机号\n1.2 个人资料您自愿填写的昵称头像教育背景简历内容等\n1.3 使用数据面试记录答题内容诊断报告操作日志等\n1.4 设备信息设备型号操作系统版本网络环境等仅用于服务优化\n1.5 微信信息当您使用微信登录时我们会获取您的微信OpenID</text>
<text class="h2">2. 信息使用</text>
<text class="body">2.1 提供和优化AI模拟面试简历诊断等核心服务\n2.2 生成个性化面试报告和进步轨迹分析\n2.3 改善服务质量和用户体验\n2.4 发送服务通知如会员到期提醒</text>
<text class="h2">3. 信息存储与保护</text>
<text class="body">3.1 您的数据存储在安全的云服务器上采用加密传输HTTPS和存储\n3.2 我们采取业界标准的安全措施防止数据泄露篡改或丢失\n3.3 您的简历和面试数据不会分享给第三方除非获得您的明确同意或法律要求</text>
<text class="h2">4. AI数据处理说明</text>
<text class="body">4.1 您在面试简历诊断等场景中输入的文本内容会被发送至AI服务商进行处理用于生成AI回复\n4.2 我们不会将您的个人身份信息如手机号邮箱发送给AI服务商\n4.3 AI服务商不会将您的输入数据用于模型训练或其他目的</text>
<text class="h2">5. 数据删除</text>
<text class="body">您可以在我的页面注销账户或发送邮件至 contact@yuzhiran.com 申请删除您的所有数据我们将在15个工作日内处理完成</text>
<text class="h2">6. Cookie与本地存储</text>
<text class="body">本平台使用本地存储localStorage保存您的登录状态和偏好设置不会使用第三方追踪Cookie</text>
<text class="h2">7. 政策更新</text>
<text class="body">我们可能不时更新本隐私政策重大变更会通过应用内通知或邮件告知您</text>
<text class="h2">8. 联系方式</text>
<text class="body">如有任何隐私相关问题请联系contact@yuzhiran.com</text>
</view>
</view>
</template>
<style scoped>
.page { background: var(--color-bg); min-height: 100vh; padding: 32rpx; }
.section { background: #FFF; border-radius: var(--radius-lg); padding: 32rpx; }
.title { font-size: 36rpx; font-weight: 800; color: var(--color-text); display: block; margin-bottom: 8rpx; }
.update { font-size: 22rpx; color: var(--color-text-tertiary); display: block; margin-bottom: 32rpx; }
.h2 { font-size: 28rpx; font-weight: 700; color: var(--color-text); display: block; margin-top: 28rpx; margin-bottom: 12rpx; }
.body { font-size: 26rpx; color: var(--color-text-secondary); line-height: 1.8; white-space: pre-wrap; display: block; }
</style>
+5
View File
@@ -138,6 +138,9 @@
<button class="act-download" @click="downloadResult('txt')">📥 下载为 TXT</button>
<button class="act-download outline" @click="downloadResult('html')">📄 预览 HTML</button>
</view>
<view class="disclaimer" v-if="result">
<text> 以上内容由 AI 生成仅供参考请在提交前自行核实重要信息</text>
</view>
</view>
</view>
</view>
@@ -421,4 +424,6 @@ const goLogin = () => uni.navigateTo({ url: '/pages/login/login' })
.empty-icon { font-size: 64rpx; margin-bottom: 16rpx; opacity: 0.5; }
.empty-title { font-size: 28rpx; font-weight: 600; color: var(--color-text); }
.empty-desc { font-size: 22rpx; color: var(--color-text-tertiary); margin-top: 8rpx; }
.disclaimer { margin-top: 16rpx; padding: 12rpx; background: #FFF8E1; border-radius: var(--radius-sm); }
.disclaimer text { font-size: 20rpx; color: #92400E; line-height: 1.6; }
</style>