feat(admin): 双邀请太阳码与自动拉取;企业/个人 Invite API;用户头像字段;题库工具与抖音测试页调整
Made-with: Cursor
This commit is contained in:
@@ -110,15 +110,24 @@
|
||||
<div class="panel-head row">
|
||||
<div>
|
||||
<h2 class="panel-title">邀请小程序码</h2>
|
||||
<p class="panel-desc">员工 / 客户扫码进入企业测评</p>
|
||||
<p class="panel-desc">企业版进企业测评,个人版进小程序首页</p>
|
||||
</div>
|
||||
<el-button size="small" type="primary" @click="loadInviteQrcode" :loading="inviteLoading">
|
||||
{{ inviteQrcode ? '刷新' : '生成' }}
|
||||
{{ inviteQrcodeEnterprise || inviteQrcodePersonal ? '刷新' : '生成' }}
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="invite-body">
|
||||
<img v-if="inviteQrcode" :src="inviteQrcode" alt="邀请码" class="invite-img" />
|
||||
<span v-else class="invite-placeholder">点击生成</span>
|
||||
<template v-if="inviteQrcodeEnterprise || inviteQrcodePersonal">
|
||||
<div v-if="inviteQrcodeEnterprise" class="invite-card">
|
||||
<span class="invite-label">企业版</span>
|
||||
<img :src="inviteQrcodeEnterprise" alt="企业版太阳码" class="invite-img" />
|
||||
</div>
|
||||
<div v-if="inviteQrcodePersonal" class="invite-card">
|
||||
<span class="invite-label">个人版</span>
|
||||
<img :src="inviteQrcodePersonal" alt="个人版太阳码" class="invite-img" />
|
||||
</div>
|
||||
</template>
|
||||
<span v-else class="invite-placeholder">{{ inviteLoadError || '进入页面将自动加载,也可点击生成' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
@@ -189,7 +198,9 @@ const faceSubtypeHints = ref<{
|
||||
|
||||
const loading = ref(false)
|
||||
const inviteLoading = ref(false)
|
||||
const inviteQrcode = ref<string>('')
|
||||
const inviteQrcodeEnterprise = ref<string>('')
|
||||
const inviteQrcodePersonal = ref<string>('')
|
||||
const inviteLoadError = ref<string>('')
|
||||
|
||||
/** 侧栏表格最大高度:单屏内滚动,不撑开整页 */
|
||||
const tableMaxH = 220
|
||||
@@ -381,21 +392,32 @@ const loadData = async () => {
|
||||
|
||||
onMounted(() => {
|
||||
loadData()
|
||||
loadInviteQrcode()
|
||||
})
|
||||
|
||||
const loadInviteQrcode = async () => {
|
||||
if (inviteLoading.value) return
|
||||
inviteLoading.value = true
|
||||
inviteLoadError.value = ''
|
||||
try {
|
||||
const res: any = await request.get('/admin/invite/qrcode')
|
||||
const qrcode = res?.data?.qrcode
|
||||
if (qrcode && typeof qrcode === 'string') {
|
||||
inviteQrcode.value = qrcode
|
||||
} else {
|
||||
ElMessage.error(res?.message || res?.msg || '生成失败,请确认企业绑定')
|
||||
const d = res?.data
|
||||
const ent = d?.enterprise?.qrcode ?? d?.qrcode
|
||||
const per = d?.personal?.qrcode
|
||||
if (typeof ent === 'string' && ent) inviteQrcodeEnterprise.value = ent
|
||||
else inviteQrcodeEnterprise.value = ''
|
||||
if (typeof per === 'string' && per) inviteQrcodePersonal.value = per
|
||||
else inviteQrcodePersonal.value = ''
|
||||
|
||||
if (!inviteQrcodeEnterprise.value && !inviteQrcodePersonal.value) {
|
||||
const msg = res?.message || res?.msg || '生成失败,请确认企业绑定'
|
||||
inviteLoadError.value = msg
|
||||
ElMessage.error(msg)
|
||||
}
|
||||
} catch (error: any) {
|
||||
ElMessage.error(error?.message || '生成失败')
|
||||
const msg = error?.message || '生成失败'
|
||||
inviteLoadError.value = msg
|
||||
ElMessage.error(msg)
|
||||
} finally {
|
||||
inviteLoading.value = false
|
||||
}
|
||||
@@ -807,11 +829,26 @@ const loadInviteQrcode = async () => {
|
||||
|
||||
.invite-body {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
align-items: flex-start;
|
||||
justify-content: center;
|
||||
gap: 16px;
|
||||
min-height: 112px;
|
||||
}
|
||||
|
||||
.invite-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.invite-label {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #374151;
|
||||
}
|
||||
|
||||
.invite-img {
|
||||
width: 112px;
|
||||
height: 112px;
|
||||
|
||||
@@ -35,7 +35,20 @@
|
||||
<el-table-column label="用户信息" min-width="180">
|
||||
<template #default="{ row }">
|
||||
<div class="user-info-cell">
|
||||
<div class="user-avatar">{{ (row.username || '?')[0] }}</div>
|
||||
<img
|
||||
v-if="displayAvatarUrl(row.avatar)"
|
||||
:src="displayAvatarUrl(row.avatar)"
|
||||
class="user-avatar user-avatar-img"
|
||||
referrerpolicy="no-referrer"
|
||||
alt=""
|
||||
/>
|
||||
<div
|
||||
v-else
|
||||
class="user-avatar user-avatar-letter"
|
||||
:style="{ backgroundColor: avatarBgColor(row) }"
|
||||
>
|
||||
{{ avatarLetter(row) }}
|
||||
</div>
|
||||
<div class="user-details">
|
||||
<div class="username">{{ row.username || '未设置昵称' }}</div>
|
||||
<div class="openid-line">{{ row.openid || '—' }}</div>
|
||||
@@ -685,6 +698,27 @@ async function loadUsers() {
|
||||
}
|
||||
}
|
||||
|
||||
/** 有效头像地址(接口字段 avatar,过滤空白) */
|
||||
function displayAvatarUrl(avatar: string | null | undefined) {
|
||||
const u = avatar != null ? String(avatar).trim() : ''
|
||||
return u || ''
|
||||
}
|
||||
|
||||
/** 无头像时文字占位:昵称首字 */
|
||||
function avatarLetter(row: { username?: string; nickname?: string }) {
|
||||
const name = (row?.username || row?.nickname || '?').trim()
|
||||
const ch = name.charAt(0) || '?'
|
||||
return /[a-zA-Z]/.test(ch) ? ch.toUpperCase() : ch
|
||||
}
|
||||
|
||||
const AVATAR_PALETTE = ['#6366f1', '#8b5cf6', '#ec4899', '#f43f5e', '#14b8a6', '#0ea5e9', '#3b82f6', '#eab308']
|
||||
function avatarBgColor(row: { username?: string; nickname?: string }) {
|
||||
const name = (row?.username || row?.nickname || '?').trim()
|
||||
let hash = 0
|
||||
for (let i = 0; i < name.length; i++) hash += name.charCodeAt(i)
|
||||
return AVATAR_PALETTE[Math.abs(hash) % AVATAR_PALETTE.length]
|
||||
}
|
||||
|
||||
function formatPhone(phone: string) {
|
||||
if (!phone) return '-'
|
||||
if (phone.length === 11) return phone.substring(0, 3) + '****' + phone.substring(7)
|
||||
@@ -1072,6 +1106,16 @@ onMounted(() => {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.user-avatar-img {
|
||||
object-fit: cover;
|
||||
border: 1px solid #e5e7eb;
|
||||
background: #f3f4f6;
|
||||
}
|
||||
|
||||
.user-avatar-letter {
|
||||
background-color: #7c3aed;
|
||||
color: #fff;
|
||||
display: flex;
|
||||
@@ -1079,7 +1123,6 @@ onMounted(() => {
|
||||
justify-content: center;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.user-details {
|
||||
|
||||
Reference in New Issue
Block a user