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
+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); }