feat(admin): enrich admin panel fields; add user index constraint and customer service
- admin controller: add updatedAt to interview/resume selects; add orderCount, todayOrders, totalRevenue to overview - admin.vue: enrich all tabs with more fields - overview: order cards (count, revenue) - users: wxOpenid, email, createdAt, interviewCount, vipExpireAt, role badge - interviews: user email, updatedAt, summary preview - orders: title, type, channel, paidAt, wxTransactionId, refund info - resumes: user email, updatedAt - share: sharer phone, shareCode, isActive, visitorId(IP), creditedAt - admins: email, createdAt - user.schema: add unique indexes on phone/wxOpenid/email; pre-save hook requiring at least one contact method - user/about: add WeChat contact button (open-type=contact) for customer service
This commit is contained in:
@@ -48,7 +48,7 @@ export class AdminController {
|
|||||||
const [
|
const [
|
||||||
userCount, interviewCount, todayUsers, todayInterviews,
|
userCount, interviewCount, todayUsers, todayInterviews,
|
||||||
resumeCount, paidDownloadCount,
|
resumeCount, paidDownloadCount,
|
||||||
planStats,
|
planStats, orderCount, todayOrders, totalRevenue,
|
||||||
] = await Promise.all([
|
] = await Promise.all([
|
||||||
this.userModel.countDocuments().exec(),
|
this.userModel.countDocuments().exec(),
|
||||||
this.interviewModel.countDocuments().exec(),
|
this.interviewModel.countDocuments().exec(),
|
||||||
@@ -59,12 +59,19 @@ export class AdminController {
|
|||||||
this.userModel.aggregate([
|
this.userModel.aggregate([
|
||||||
{ $group: { _id: '$plan', count: { $sum: 1 } } },
|
{ $group: { _id: '$plan', count: { $sum: 1 } } },
|
||||||
]).exec(),
|
]).exec(),
|
||||||
|
this.orderModel.countDocuments().exec(),
|
||||||
|
this.orderModel.countDocuments({ status: 'success', createdAt: { $gte: new Date(Date.now() - 24 * 60 * 60 * 1000) } }).exec(),
|
||||||
|
this.orderModel.aggregate([
|
||||||
|
{ $match: { status: 'success' } },
|
||||||
|
{ $group: { _id: null, total: { $sum: '$amount' } } },
|
||||||
|
]).exec(),
|
||||||
])
|
])
|
||||||
const planBreakdown: Record<string, number> = {}
|
const planBreakdown: Record<string, number> = {}
|
||||||
planStats.forEach(p => { planBreakdown[p._id || 'free'] = p.count })
|
planStats.forEach(p => { planBreakdown[p._id || 'free'] = p.count })
|
||||||
return {
|
return {
|
||||||
userCount, interviewCount, todayUsers, todayInterviews,
|
userCount, interviewCount, todayUsers, todayInterviews,
|
||||||
resumeCount, paidDownloadCount,
|
resumeCount, paidDownloadCount, orderCount, todayOrders,
|
||||||
|
totalRevenue: totalRevenue[0]?.total || 0,
|
||||||
planBreakdown,
|
planBreakdown,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -95,8 +102,8 @@ export class AdminController {
|
|||||||
this.interviewModel.find()
|
this.interviewModel.find()
|
||||||
.sort({ createdAt: -1 })
|
.sort({ createdAt: -1 })
|
||||||
.skip(skip).limit(+limit)
|
.skip(skip).limit(+limit)
|
||||||
.populate('userId', 'phone nickname')
|
.populate('userId', 'phone nickname email wxOpenid')
|
||||||
.select('position status totalScore questionCount fillerScore fillerDensity summary createdAt')
|
.select('position status totalScore questionCount fillerScore fillerDensity summary createdAt updatedAt')
|
||||||
.lean().exec(),
|
.lean().exec(),
|
||||||
this.interviewModel.countDocuments().exec(),
|
this.interviewModel.countDocuments().exec(),
|
||||||
])
|
])
|
||||||
@@ -217,8 +224,8 @@ export class AdminController {
|
|||||||
this.resumeModel.find()
|
this.resumeModel.find()
|
||||||
.sort({ createdAt: -1 })
|
.sort({ createdAt: -1 })
|
||||||
.skip(skip).limit(+limit)
|
.skip(skip).limit(+limit)
|
||||||
.populate('userId', 'phone nickname')
|
.populate('userId', 'phone nickname email')
|
||||||
.select('title targetPosition version paidDownload createdAt')
|
.select('title targetPosition version paidDownload createdAt updatedAt contentHash')
|
||||||
.lean().exec(),
|
.lean().exec(),
|
||||||
this.resumeModel.countDocuments().exec(),
|
this.resumeModel.countDocuments().exec(),
|
||||||
])
|
])
|
||||||
|
|||||||
@@ -5,10 +5,10 @@ export type UserDocument = User & Document
|
|||||||
|
|
||||||
@Schema({ timestamps: true })
|
@Schema({ timestamps: true })
|
||||||
export class User {
|
export class User {
|
||||||
@Prop({ sparse: true })
|
@Prop({ unique: true, sparse: true })
|
||||||
phone?: string
|
phone?: string
|
||||||
|
|
||||||
@Prop({ sparse: true })
|
@Prop({ unique: true, sparse: true })
|
||||||
wxOpenid?: string
|
wxOpenid?: string
|
||||||
|
|
||||||
@Prop({ default: '' })
|
@Prop({ default: '' })
|
||||||
@@ -60,7 +60,7 @@ export class User {
|
|||||||
@Prop({ default: false })
|
@Prop({ default: false })
|
||||||
isSystemAdmin: boolean
|
isSystemAdmin: boolean
|
||||||
|
|
||||||
@Prop({ sparse: true })
|
@Prop({ unique: true, sparse: true })
|
||||||
email?: string
|
email?: string
|
||||||
|
|
||||||
@Prop({ default: '', select: false })
|
@Prop({ default: '', select: false })
|
||||||
@@ -68,3 +68,10 @@ export class User {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const UserSchema = SchemaFactory.createForClass(User)
|
export const UserSchema = SchemaFactory.createForClass(User)
|
||||||
|
|
||||||
|
UserSchema.pre('save', function (next) {
|
||||||
|
if (!this.phone && !this.wxOpenid && !this.email) {
|
||||||
|
return next(new Error('用户必须至少有一个联系方式(手机号/微信/邮箱)'))
|
||||||
|
}
|
||||||
|
next()
|
||||||
|
})
|
||||||
@@ -17,6 +17,15 @@
|
|||||||
<text class="info-value">contact@yuzhiran.com</text>
|
<text class="info-value">contact@yuzhiran.com</text>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
|
<!-- #ifdef MP-WEIXIN -->
|
||||||
|
<button class="contact-btn" open-type="contact">
|
||||||
|
<view class="contact-btn-inner">
|
||||||
|
<text class="contact-icon">💬</text>
|
||||||
|
<text class="contact-text">联系在线客服</text>
|
||||||
|
</view>
|
||||||
|
</button>
|
||||||
|
<!-- #endif -->
|
||||||
|
|
||||||
<view class="link-section">
|
<view class="link-section">
|
||||||
<view class="link-item" @click="goAgreement">
|
<view class="link-item" @click="goAgreement">
|
||||||
<text class="link-text">用户协议</text>
|
<text class="link-text">用户协议</text>
|
||||||
@@ -59,4 +68,10 @@ const goPrivacy = () => uni.navigateTo({ url: '/pages/privacy/privacy' })
|
|||||||
.disclaimer { margin-top: 40rpx; background: #FFF8E1; border-radius: var(--radius-md); padding: 24rpx; }
|
.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-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; }
|
.disclaimer-text { font-size: 22rpx; color: var(--color-text-secondary); line-height: 1.8; }
|
||||||
|
|
||||||
|
.contact-btn { width: 100%; background: #FFF; border: none; border-radius: var(--radius-md); padding: 0; margin-bottom: 12rpx; }
|
||||||
|
.contact-btn-inner { display: flex; align-items: center; gap: 16rpx; padding: 24rpx 30rpx; }
|
||||||
|
.contact-btn:active { opacity: 0.7; }
|
||||||
|
.contact-icon { font-size: 28rpx; }
|
||||||
|
.contact-text { font-size: 26rpx; color: var(--color-text); }
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -45,6 +45,23 @@
|
|||||||
<text class="stat-sub">付费下载 {{ overview.paidDownloadCount ?? 0 }}</text>
|
<text class="stat-sub">付费下载 {{ overview.paidDownloadCount ?? 0 }}</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
<view class="stat-cards" style="margin-top:12rpx">
|
||||||
|
<view class="stat-card">
|
||||||
|
<text class="stat-num">{{ overview.orderCount ?? 0 }}</text>
|
||||||
|
<text class="stat-label">总订单</text>
|
||||||
|
<text class="stat-sub">今日 +{{ overview.todayOrders ?? 0 }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="stat-card">
|
||||||
|
<text class="stat-num">{{ overview.totalRevenue ? '¥' + (overview.totalRevenue / 100).toFixed(1) : '¥0' }}</text>
|
||||||
|
<text class="stat-label">总营收</text>
|
||||||
|
<text class="stat-sub">已支付订单合计</text>
|
||||||
|
</view>
|
||||||
|
<view class="stat-card" v-if="!(overview.orderCount)">
|
||||||
|
<text class="stat-num">--</text>
|
||||||
|
<text class="stat-label">--</text>
|
||||||
|
<text class="stat-sub" />
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
<view class="plan-cards">
|
<view class="plan-cards">
|
||||||
<view class="plan-card" v-for="(cnt, plan) in overview.planBreakdown" :key="plan">
|
<view class="plan-card" v-for="(cnt, plan) in overview.planBreakdown" :key="plan">
|
||||||
<text class="plan-num">{{ cnt }}</text>
|
<text class="plan-num">{{ cnt }}</text>
|
||||||
@@ -64,11 +81,22 @@
|
|||||||
<view class="user-main">
|
<view class="user-main">
|
||||||
<text class="user-phone">{{ u.phone || '--' }}</text>
|
<text class="user-phone">{{ u.phone || '--' }}</text>
|
||||||
<text class="user-name">{{ u.nickname || '--' }}</text>
|
<text class="user-name">{{ u.nickname || '--' }}</text>
|
||||||
|
<text class="user-badge-role" v-if="u.role === 'admin'">管理</text>
|
||||||
</view>
|
</view>
|
||||||
<view class="user-badges">
|
<view class="user-meta-row">
|
||||||
<text class="user-plan" :class="{ vip: u.plan === 'growth' || u.plan === 'sprint' }">{{ u.plan === 'growth' || u.plan === 'sprint' ? '会员' : '免费' }}</text>
|
<text class="meta-tag email" v-if="u.email">{{ u.email }}</text>
|
||||||
<text class="user-credit">引力值:{{ u.gravity ?? 0 }}</text>
|
<text class="meta-tag" v-if="u.wxOpenid">openid:{{ u.wxOpenid.slice(0,12) }}..</text>
|
||||||
<text class="user-credit share" v-if="u.shareCredits > 0">分享:{{ u.shareCredits }}</text>
|
</view>
|
||||||
|
<view class="user-meta-row">
|
||||||
|
<text class="meta-tag">引力:{{ u.gravity ?? 0 }}</text>
|
||||||
|
<text class="meta-tag">面试:{{ u.interviewCount ?? 0 }}次</text>
|
||||||
|
<text class="user-plan" :class="{ vip: u.plan === 'growth' || u.plan === 'sprint' }">{{ u.plan === 'growth' || u.plan === 'sprint' ? u.plan==='sprint'?'冲刺':'会员' : '免费' }}</text>
|
||||||
|
<text class="meta-tag share" v-if="u.shareCredits > 0">分享:{{ u.shareCredits }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="user-meta-row time-row">
|
||||||
|
<text class="time-label">注册:{{ u.createdAt?.slice(0,16).replace('T',' ') }}</text>
|
||||||
|
<text class="time-label" v-if="u.vipExpireAt">到期:{{ u.vipExpireAt?.slice(0,10) }}</text>
|
||||||
|
<text class="time-label" v-if="u.sprintExpireAt">冲刺到期:{{ u.sprintExpireAt?.slice(0,10) }}</text>
|
||||||
</view>
|
</view>
|
||||||
<view class="user-actions">
|
<view class="user-actions">
|
||||||
<text class="user-action-btn" v-if="u.plan === 'free'" @click="setVip(u._id)">设为会员</text>
|
<text class="user-action-btn" v-if="u.plan === 'free'" @click="setVip(u._id)">设为会员</text>
|
||||||
@@ -93,14 +121,19 @@
|
|||||||
<view class="iv-main">
|
<view class="iv-main">
|
||||||
<text class="iv-pos">{{ iv.position }}</text>
|
<text class="iv-pos">{{ iv.position }}</text>
|
||||||
<text class="iv-user">{{ iv.userId?.phone || iv.userId?.nickname || '--' }}</text>
|
<text class="iv-user">{{ iv.userId?.phone || iv.userId?.nickname || '--' }}</text>
|
||||||
|
<text class="iv-user email" v-if="iv.userId?.email">{{ iv.userId.email }}</text>
|
||||||
</view>
|
</view>
|
||||||
<view class="iv-meta">
|
<view class="iv-meta">
|
||||||
<text class="iv-status" :class="{ done: iv.status === 'completed' }">{{ iv.status === 'completed' ? '已完成' : '进行中' }}</text>
|
<text class="iv-status" :class="{ done: iv.status === 'completed' }">{{ iv.status === 'completed' ? '已完成' : '进行中' }}</text>
|
||||||
<text class="iv-tag">{{ iv.questionCount || 0 }}题</text>
|
<text class="iv-tag">{{ iv.questionCount || 0 }}题</text>
|
||||||
<text class="iv-tag score">得分 {{ iv.totalScore ?? '-' }}</text>
|
<text class="iv-tag score">得分 {{ iv.totalScore ?? '-' }}</text>
|
||||||
<text class="iv-tag filler" v-if="iv.fillerScore != null && iv.fillerScore > 0">语分析 {{ iv.fillerScore }}/{{ iv.fillerDensity ?? '-' }}</text>
|
<text class="iv-tag filler" v-if="iv.fillerScore != null && iv.fillerScore > 0">语气 {{ iv.fillerScore }}/{{ iv.fillerDensity ?? '-' }}</text>
|
||||||
</view>
|
</view>
|
||||||
<text class="iv-time">{{ iv.createdAt?.slice(0,16).replace('T',' ') }}</text>
|
<view class="iv-meta">
|
||||||
|
<text class="time-label">开始:{{ iv.createdAt?.slice(0,16).replace('T',' ') }}</text>
|
||||||
|
<text class="time-label" v-if="iv.updatedAt && iv.updatedAt !== iv.createdAt">更新:{{ iv.updatedAt?.slice(0,16).replace('T',' ') }}</text>
|
||||||
|
</view>
|
||||||
|
<text class="iv-summary" v-if="iv.summary">{{ iv.summary.slice(0,60) }}{{ iv.summary.length > 60 ? '...' : '' }}</text>
|
||||||
</view>
|
</view>
|
||||||
<text class="load-more" v-if="ivTotal > interviews.length" @click="loadMoreInterviews">加载更多</text>
|
<text class="load-more" v-if="ivTotal > interviews.length" @click="loadMoreInterviews">加载更多</text>
|
||||||
<text class="empty-text" v-if="interviews.length === 0 && !ivLoading">暂无面试记录</text>
|
<text class="empty-text" v-if="interviews.length === 0 && !ivLoading">暂无面试记录</text>
|
||||||
@@ -119,13 +152,17 @@
|
|||||||
<view class="resume-main">
|
<view class="resume-main">
|
||||||
<text class="resume-title">{{ r.title }}</text>
|
<text class="resume-title">{{ r.title }}</text>
|
||||||
<text class="resume-user">{{ r.userId?.phone || r.userId?.nickname || '--' }}</text>
|
<text class="resume-user">{{ r.userId?.phone || r.userId?.nickname || '--' }}</text>
|
||||||
|
<text class="resume-user email" v-if="r.userId?.email">{{ r.userId.email }}</text>
|
||||||
</view>
|
</view>
|
||||||
<view class="resume-meta">
|
<view class="resume-meta">
|
||||||
<text class="resume-tag">v{{ r.version }}</text>
|
<text class="resume-tag">v{{ r.version }}</text>
|
||||||
<text class="resume-tag" v-if="r.targetPosition">{{ r.targetPosition }}</text>
|
<text class="resume-tag" v-if="r.targetPosition">{{ r.targetPosition }}</text>
|
||||||
<text class="resume-tag paid" v-if="r.paidDownload">付费下载</text>
|
<text class="resume-tag paid" v-if="r.paidDownload">付费下载</text>
|
||||||
</view>
|
</view>
|
||||||
<text class="resume-time">{{ r.createdAt?.slice(0,10) }}</text>
|
<view class="resume-meta time-row">
|
||||||
|
<text class="time-label">创建:{{ r.createdAt?.slice(0,16).replace('T',' ') }}</text>
|
||||||
|
<text class="time-label" v-if="r.updatedAt && r.updatedAt !== r.createdAt">更新:{{ r.updatedAt?.slice(0,16).replace('T',' ') }}</text>
|
||||||
|
</view>
|
||||||
<view class="resume-actions">
|
<view class="resume-actions">
|
||||||
<text class="admin-action-btn del" @click="deleteResume(r._id, r.title)">删除</text>
|
<text class="admin-action-btn del" @click="deleteResume(r._id, r.title)">删除</text>
|
||||||
</view>
|
</view>
|
||||||
@@ -148,22 +185,37 @@
|
|||||||
<view class="order-list" v-if="!orderLoading">
|
<view class="order-list" v-if="!orderLoading">
|
||||||
<view class="order-row" v-for="o in orders" :key="o._id">
|
<view class="order-row" v-for="o in orders" :key="o._id">
|
||||||
<view class="order-info">
|
<view class="order-info">
|
||||||
<text class="order-id">订单号: {{ o.outTradeNo }}</text>
|
<text class="order-id">{{ o.outTradeNo }}</text>
|
||||||
<text class="order-user">用户: {{ o.userPhone || o.userId.slice(-6) }}</text>
|
<view class="order-status rp" :class="o.status === 'success' ? 'paid' : o.status === 'refunded' ? 'refund' : 'pend'">
|
||||||
</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' ? '已退款' : '待支付' }}
|
{{ o.status === 'success' ? '已支付' : o.status === 'refunded' ? '已退款' : '待支付' }}
|
||||||
</view>
|
</view>
|
||||||
<text class="order-time">{{ o.createdAt ? o.createdAt.slice(0,16).replace('T',' ') : '--' }}</text>
|
</view>
|
||||||
<view class="order-actions">
|
<view class="order-meta-row">
|
||||||
|
<text class="order-title">{{ o.title || '--' }}</text>
|
||||||
|
<text class="order-user">用户: {{ o.userPhone || o.userId?.slice(-6) }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="order-meta-row">
|
||||||
|
<text class="order-amount">¥{{ (o.amount / 100).toFixed(1) }}</text>
|
||||||
|
<text class="meta-tag">类型:{{ o.type || '--' }}</text>
|
||||||
|
<text class="meta-tag">渠道:{{ o.channel || '--' }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="order-meta-row time-row">
|
||||||
|
<text class="time-label">创建:{{ o.createdAt?.slice(0,16).replace('T',' ') }}</text>
|
||||||
|
<text class="time-label" v-if="o.paidAt">支付:{{ o.paidAt?.slice(0,16).replace('T',' ') }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="order-meta-row" v-if="o.wxTransactionId">
|
||||||
|
<text class="time-label">微信单号:{{ o.wxTransactionId }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="order-meta-row" v-if="o.status === 'refunded'">
|
||||||
|
<text class="time-label refund-label">退款:¥{{ (o.refundAmount/100).toFixed(1) }} {{ o.refundedAt?.slice(0,16).replace('T',' ') }}</text>
|
||||||
|
<text class="time-label" v-if="o.refundReason">原因:{{ o.refundReason }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="order-actions-bar">
|
||||||
<text class="sync-btn" v-if="o.status === 'pending'" @click="syncOrder(o.outTradeNo)">同步</text>
|
<text class="sync-btn" v-if="o.status === 'pending'" @click="syncOrder(o.outTradeNo)">同步</text>
|
||||||
<text class="refund-btn" v-if="o.status === 'success'" @click="openRefundModal(o)">退款</text>
|
<text class="refund-btn" v-if="o.status === 'success'" @click="openRefundModal(o)">退款</text>
|
||||||
<text class="sync-btn" v-if="o.status === 'refunded'" @click="queryRefund(o.outTradeNo)">查询</text>
|
<text class="sync-btn" v-if="o.status === 'refunded'" @click="queryRefund(o.outTradeNo)">查询</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
|
||||||
<text class="load-more" v-if="ordersTotal > orders.length" @click="loadMoreOrders">加载更多</text>
|
<text class="load-more" v-if="ordersTotal > orders.length" @click="loadMoreOrders">加载更多</text>
|
||||||
<text class="empty-text" v-if="orders.length === 0 && !orderLoading">暂无订单</text>
|
<text class="empty-text" v-if="orders.length === 0 && !orderLoading">暂无订单</text>
|
||||||
</view>
|
</view>
|
||||||
@@ -283,13 +335,19 @@
|
|||||||
<view class="share-row" v-for="r in shareRecords" :key="r.shareCode">
|
<view class="share-row" v-for="r in shareRecords" :key="r.shareCode">
|
||||||
<view class="share-main">
|
<view class="share-main">
|
||||||
<text class="share-title">{{ r.title }}</text>
|
<text class="share-title">{{ r.title }}</text>
|
||||||
<text class="share-meta">{{ r.sharer?.nickname || '--' }} · {{ r.type }}</text>
|
<text class="share-meta">{{ r.sharer?.phone || r.sharer?.nickname || '--' }} · {{ r.type }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="share-meta-row">
|
||||||
|
<text class="meta-tag">code:{{ r.shareCode }}</text>
|
||||||
|
<text class="meta-tag" :class="r.isActive ? 'badge-done' : 'badge-pend'">{{ r.isActive ? '启用' : '停用' }}</text>
|
||||||
</view>
|
</view>
|
||||||
<view class="share-stats">
|
<view class="share-stats">
|
||||||
<text>访问 {{ r.visitCount }}</text>
|
<text>访问 {{ r.visitCount }}</text>
|
||||||
<text class="share-credited">有效 {{ r.creditedCount }}</text>
|
<text class="share-credited">有效 {{ r.creditedCount }}</text>
|
||||||
</view>
|
</view>
|
||||||
<text class="share-time">{{ r.createdAt?.slice(0,16).replace('T',' ') }}</text>
|
<view class="share-meta-row time-row">
|
||||||
|
<text class="time-label">创建:{{ r.createdAt?.slice(0,16).replace('T',' ') }}</text>
|
||||||
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
<text class="loading-text" v-if="shareLoading">加载中...</text>
|
<text class="loading-text" v-if="shareLoading">加载中...</text>
|
||||||
@@ -299,13 +357,17 @@
|
|||||||
<view class="share-list" v-if="!shareLoading">
|
<view class="share-list" v-if="!shareLoading">
|
||||||
<view class="share-row" v-for="(v, i) in shareVisitors" :key="i">
|
<view class="share-row" v-for="(v, i) in shareVisitors" :key="i">
|
||||||
<view class="share-main">
|
<view class="share-main">
|
||||||
<text>分享者: {{ v.sharer?.nickname || '--' }}</text>
|
<text>分享者:{{ v.sharer?.phone || v.sharer?.nickname || '--' }}</text>
|
||||||
<text class="share-meta">访客: {{ v.visitor?.nickname || '匿名' }}</text>
|
<text class="share-meta">访客:{{ v.visitor?.phone || v.visitor?.nickname || '匿名' }}</text>
|
||||||
</view>
|
</view>
|
||||||
<view class="share-stats">
|
<view class="share-meta-row">
|
||||||
<text class="badge" :class="v.credited ? 'badge-done' : 'badge-pend'">{{ v.credited ? '已积分' : '未积分' }}</text>
|
<text class="meta-tag">IP:{{ v.visitorId || '--' }}</text>
|
||||||
|
<text class="meta-tag" :class="v.credited ? 'badge-done' : 'badge-pend'">{{ v.credited ? '已积分' : '未积分' }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="share-meta-row time-row">
|
||||||
|
<text class="time-label">访问:{{ v.createdAt?.slice(0,16).replace('T',' ') }}</text>
|
||||||
|
<text class="time-label" v-if="v.creditedAt">积分:{{ v.creditedAt?.slice(0,16).replace('T',' ') }}</text>
|
||||||
</view>
|
</view>
|
||||||
<text class="share-time">{{ v.createdAt?.slice(0,16).replace('T',' ') }}</text>
|
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
<text class="loading-text" v-if="shareLoading">加载中...</text>
|
<text class="loading-text" v-if="shareLoading">加载中...</text>
|
||||||
@@ -429,7 +491,9 @@
|
|||||||
<view class="admin-row" v-for="a in adminList" :key="a._id">
|
<view class="admin-row" v-for="a in adminList" :key="a._id">
|
||||||
<text class="admin-phone">{{ a.phone || '--' }}</text>
|
<text class="admin-phone">{{ a.phone || '--' }}</text>
|
||||||
<text class="admin-name">{{ a.nickname || '--' }}</text>
|
<text class="admin-name">{{ a.nickname || '--' }}</text>
|
||||||
|
<text class="admin-email" v-if="a.email">{{ a.email }}</text>
|
||||||
<text class="admin-badge" v-if="a.isSystemAdmin">系统</text>
|
<text class="admin-badge" v-if="a.isSystemAdmin">系统</text>
|
||||||
|
<text class="time-label" style="margin-left:auto">设置:{{ a.createdAt?.slice(0,10) }}</text>
|
||||||
</view>
|
</view>
|
||||||
<text class="empty-text" v-if="adminList.length === 0">暂无管理员</text>
|
<text class="empty-text" v-if="adminList.length === 0">暂无管理员</text>
|
||||||
</view>
|
</view>
|
||||||
@@ -438,6 +502,7 @@
|
|||||||
<view class="admin-row">
|
<view class="admin-row">
|
||||||
<text class="admin-phone">{{ searchResult.phone || '--' }}</text>
|
<text class="admin-phone">{{ searchResult.phone || '--' }}</text>
|
||||||
<text class="admin-name">{{ searchResult.nickname || '--' }}</text>
|
<text class="admin-name">{{ searchResult.nickname || '--' }}</text>
|
||||||
|
<text class="admin-email" v-if="searchResult.email">{{ searchResult.email }}</text>
|
||||||
<text class="admin-set-btn" v-if="searchResult.role !== 'admin'" @click="setAdmin(searchResult._id)">设为管理员</text>
|
<text class="admin-set-btn" v-if="searchResult.role !== 'admin'" @click="setAdmin(searchResult._id)">设为管理员</text>
|
||||||
<text class="admin-set-btn done" v-else>已是管理员</text>
|
<text class="admin-set-btn done" v-else>已是管理员</text>
|
||||||
</view>
|
</view>
|
||||||
@@ -1091,4 +1156,27 @@ onMounted(() => { doVerify() })
|
|||||||
.admin-action-btn { font-size: 20rpx; padding: 4rpx 16rpx; border-radius: var(--radius-round); cursor: pointer; }
|
.admin-action-btn { font-size: 20rpx; padding: 4rpx 16rpx; border-radius: var(--radius-round); cursor: pointer; }
|
||||||
.admin-action-btn.del { color: #EF4444; border: 2rpx solid #EF4444; }
|
.admin-action-btn.del { color: #EF4444; border: 2rpx solid #EF4444; }
|
||||||
.resume-actions { display: flex; gap: 8rpx; align-items: center; }
|
.resume-actions { display: flex; gap: 8rpx; align-items: center; }
|
||||||
|
|
||||||
|
/* ─── 新增字段样式 ───── */
|
||||||
|
.user-meta-row { display: flex; flex-wrap: wrap; gap: 6rpx; margin-bottom: 6rpx; }
|
||||||
|
.meta-tag { font-size: 18rpx; background: #F3F4F6; color: var(--color-text-tertiary); padding: 2rpx 10rpx; border-radius: var(--radius-round); }
|
||||||
|
.meta-tag.email { background: #EEF2FF; color: var(--color-primary); }
|
||||||
|
.meta-tag.share { background: #FFF7ED; color: #D97706; }
|
||||||
|
.meta-tag.badge-done { background: #ECFDF5; color: #059669; }
|
||||||
|
.meta-tag.badge-pend { background: #FEF3C7; color: #D97706; }
|
||||||
|
.time-row { display: flex; flex-wrap: wrap; gap: 12rpx; }
|
||||||
|
.time-label { font-size: 18rpx; color: #9CA3AF; }
|
||||||
|
.iv-summary { font-size: 18rpx; color: #6B7280; margin-top: 4rpx; line-height: 1.4; display: block; }
|
||||||
|
.iv-user.email { font-size: 18rpx; color: #6B7280; }
|
||||||
|
.user-badge-role { font-size: 18rpx; background: #FEF3C7; color: #D97706; padding: 0 10rpx; border-radius: var(--radius-round); font-weight: 500; }
|
||||||
|
.share-meta-row { display: flex; gap: 6rpx; margin-top: 4rpx; }
|
||||||
|
.share-meta-row.time-row { gap: 12rpx; }
|
||||||
|
.order-meta-row { display: flex; flex-wrap: wrap; gap: 8rpx; margin-bottom: 4rpx; }
|
||||||
|
.order-meta-row.time-row { gap: 12rpx; }
|
||||||
|
.order-title { font-size: 22rpx; font-weight: 500; color: var(--color-text); }
|
||||||
|
.order-status.rp { font-size: 18rpx; display: inline-block; }
|
||||||
|
.refund-label { color: #EF4444 !important; }
|
||||||
|
.order-actions-bar { display: flex; gap: 8rpx; margin-top: 6rpx; }
|
||||||
|
.admin-email { font-size: 20rpx; color: #6B7280; }
|
||||||
|
.resume-user.email { font-size: 18rpx; color: #6B7280; }
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -97,6 +97,13 @@
|
|||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
<view class="menu-group">
|
<view class="menu-group">
|
||||||
|
<!-- #ifdef MP-WEIXIN -->
|
||||||
|
<button class="menu-item contact-btn" open-type="contact">
|
||||||
|
<view class="menu-icon-wrap wrap-gray"><text class="menu-icon">💬</text></view>
|
||||||
|
<text class="menu-text">联系客服</text>
|
||||||
|
<text class="menu-arrow">›</text>
|
||||||
|
</button>
|
||||||
|
<!-- #endif -->
|
||||||
<view class="menu-item" @click="goAbout">
|
<view class="menu-item" @click="goAbout">
|
||||||
<view class="menu-icon-wrap wrap-gray"><text class="menu-icon">ℹ️</text></view>
|
<view class="menu-icon-wrap wrap-gray"><text class="menu-icon">ℹ️</text></view>
|
||||||
<text class="menu-text">关于</text>
|
<text class="menu-text">关于</text>
|
||||||
@@ -322,6 +329,9 @@ const doLogout = () => {
|
|||||||
.menu-area { padding: 0 32rpx 32rpx; margin-top: 8rpx; }
|
.menu-area { padding: 0 32rpx 32rpx; margin-top: 8rpx; }
|
||||||
.menu-group { background: #FFFFFF; border-radius: var(--radius-lg); overflow: hidden; margin-bottom: 24rpx; box-shadow: var(--shadow-sm); }
|
.menu-group { background: #FFFFFF; border-radius: var(--radius-lg); overflow: hidden; margin-bottom: 24rpx; box-shadow: var(--shadow-sm); }
|
||||||
.menu-item { display: flex; align-items: center; padding: 28rpx 32rpx; border-bottom: 1rpx solid var(--color-border); }
|
.menu-item { display: flex; align-items: center; padding: 28rpx 32rpx; border-bottom: 1rpx solid var(--color-border); }
|
||||||
|
.contact-btn { width: 100%; background: transparent; border: none; border-radius: 0; padding: 0; margin: 0; line-height: inherit; font-size: inherit; text-align: left; }
|
||||||
|
.contact-btn::after { border: none; }
|
||||||
|
.contact-btn:active { background: #F9FAFB; }
|
||||||
.menu-item:last-child { border-bottom: none; }
|
.menu-item:last-child { border-bottom: none; }
|
||||||
.menu-item:active { background: #F9FAFB; }
|
.menu-item:active { background: #F9FAFB; }
|
||||||
.menu-icon-wrap { width: 60rpx; height: 60rpx; border-radius: var(--radius-md); display: flex; align-items: center; justify-content: center; margin-right: 20rpx; flex-shrink: 0; }
|
.menu-icon-wrap { width: 60rpx; height: 60rpx; border-radius: var(--radius-md); display: flex; align-items: center; justify-content: center; margin-right: 20rpx; flex-shrink: 0; }
|
||||||
|
|||||||
Reference in New Issue
Block a user