feat: 企业权限上限与迁移脚本;管理端/超管概览与财务;双端小程序 tab/订单/历史/测评选择等同步

Made-with: Cursor
This commit is contained in:
Ghost
2026-03-30 17:12:59 +08:00
parent f589b5d50c
commit 3af7bb35dd
49 changed files with 2570 additions and 920 deletions

View File

@@ -2,9 +2,7 @@
<div class="dashboard-viewport" v-loading="loading">
<header class="dash-head">
<h1 class="dash-title">数据概览</h1>
<p class="dash-tagline">
企业侧测评闭环小程序面相与 MBTI / DISC / PDP 答题 MySQL 落库 本页看用户与测试趋势拉新邀请码全链路见开发文档小程序全链路功能与接口
</p>
<p class="dash-tagline"></p>
</header>
<div class="dash-kpis">

View File

@@ -1,5 +1,13 @@
<template>
<div class="finance-page" :class="{ 'is-embedded': embedded }">
<!-- 嵌入系统设置企业余额时单独展示操作区独立页仍有完整 page-header -->
<div v-if="embedded" class="embedded-toolbar">
<div class="embedded-toolbar-actions">
<el-button @click="loadAll" :loading="loading">刷新</el-button>
<el-button type="primary" color="#7c3aed" @click="openRechargeDialog">生成充值码</el-button>
</div>
</div>
<div v-if="!embedded" class="page-header">
<div>
<h2>企业余额</h2>
@@ -383,4 +391,17 @@ onMounted(() => {
padding: 0;
min-height: auto;
}
.embedded-toolbar {
display: flex;
justify-content: flex-end;
margin-bottom: 16px;
}
.embedded-toolbar-actions {
display: flex;
flex-wrap: wrap;
gap: 12px;
align-items: center;
}
</style>

View File

@@ -22,6 +22,31 @@
</div>
<div class="tab-content-card" :class="{ 'flat-embed': activeTab === 'finance' }">
<div v-if="activeTab === 'features'" class="tab-content" v-loading="permLoading">
<div class="content-header">
<h3>终端功能开关</h3>
<p class="content-description">
仅可在超管为贵司授权的范围内调整若超管未开放某项如人脸分析此处不显示对应开关
</p>
</div>
<div class="form-section">
<template v-if="visibleAdminPermItems.length === 0">
<p class="hint-muted">当前无超管授权项可配置请联系平台管理员</p>
</template>
<template v-else>
<div class="perm-admin-row" v-for="p in visibleAdminPermItems" :key="p.key">
<span class="perm-admin-label">{{ p.label }}</span>
<el-switch v-model="adminPerms[p.key]" />
</div>
<div class="save-actions">
<el-button type="primary" color="#7c3aed" class="save-btn" @click="saveAdminPermissions" :loading="permSaving">
保存功能开关
</el-button>
</div>
</template>
</div>
</div>
<div v-if="activeTab === 'account'" class="tab-content">
<div class="content-header">
<h3>管理员账号设置</h3>
@@ -82,14 +107,15 @@
</template>
<script setup lang="ts">
import { ref, reactive, onMounted, watch } from 'vue'
import { ref, reactive, onMounted, watch, computed } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { DocumentCopy } from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus'
import { request } from '@/utils/request'
import { getAdminRole } from '@/utils/authStorage'
import Finance from './Finance.vue'
const TAB_IDS = ['account', 'finance'] as const
const TAB_IDS = ['account', 'features', 'finance'] as const
type TabId = (typeof TAB_IDS)[number]
function isTabId(s: string): s is TabId {
@@ -101,15 +127,25 @@ const router = useRouter()
const activeTab = ref<TabId>('account')
const loading = ref(false)
const tabs: { label: string; value: TabId }[] = [
{ label: '账号设置', value: 'account' },
{ label: '企业余额', value: 'finance' }
]
const isEnterpriseAdmin = () => getAdminRole() === 'enterprise_admin'
const tabs = computed(() => {
const rows: { label: string; value: TabId }[] = [{ label: '账号设置', value: 'account' }]
if (isEnterpriseAdmin()) {
rows.push({ label: '功能开关', value: 'features' })
}
rows.push({ label: '企业余额', value: 'finance' })
return rows
})
const applyRouteTab = () => {
const t = route.query.tab
if (typeof t === 'string' && isTabId(t)) {
activeTab.value = t
if (t === 'features' && !isEnterpriseAdmin()) {
activeTab.value = 'account'
} else {
activeTab.value = t
}
} else {
activeTab.value = 'account'
}
@@ -136,6 +172,65 @@ const accountConfig = reactive({
confirmPassword: ''
})
const permItems = [
{ key: 'face', label: '人脸分析' },
{ key: 'mbti', label: 'MBTI' },
{ key: 'pdp', label: 'PDP' },
{ key: 'disc', label: 'DISC' },
{ key: 'distribution', label: '分销推广' }
] as const
const defaultAdminPermissions = () =>
({ face: true, mbti: true, pdp: true, disc: true, distribution: true }) as Record<string, boolean>
const permLoading = ref(false)
const permSaving = ref(false)
const adminPermsCeiling = ref<Record<string, boolean>>(defaultAdminPermissions())
const adminPerms = reactive<Record<string, boolean>>(defaultAdminPermissions())
const visibleAdminPermItems = computed(() =>
permItems.filter((p) => adminPermsCeiling.value[p.key] !== false)
)
const loadAdminPermissions = async () => {
if (!isEnterpriseAdmin()) return
permLoading.value = true
try {
const res: any = await request.get('/admin/enterprise/permissions')
if (res.code === 200 && res.data) {
Object.assign(adminPerms, defaultAdminPermissions(), res.data.permissions || {})
adminPermsCeiling.value = { ...defaultAdminPermissions(), ...(res.data.permissionsCeiling || {}) }
}
} catch (e: any) {
console.error(e)
ElMessage.error(e?.message || '加载功能开关失败')
} finally {
permLoading.value = false
}
}
const saveAdminPermissions = async () => {
if (!isEnterpriseAdmin()) return
permSaving.value = true
try {
const body: Record<string, boolean> = { ...defaultAdminPermissions() }
for (const p of permItems) {
body[p.key] = adminPermsCeiling.value[p.key] === false ? false : !!adminPerms[p.key]
}
const res: any = await request.put('/admin/enterprise/permissions', { permissions: body })
if (res.code === 200) {
ElMessage.success('功能开关已保存')
await loadAdminPermissions()
} else {
ElMessage.error(res.msg || '保存失败')
}
} catch (e: any) {
ElMessage.error(e?.message || '保存失败')
} finally {
permSaving.value = false
}
}
const loadSettings = async () => {
loading.value = true
try {
@@ -186,9 +281,21 @@ watch(
() => applyRouteTab()
)
watch(
() => activeTab.value,
(tab) => {
if (tab === 'features' && isEnterpriseAdmin()) {
loadAdminPermissions()
}
}
)
onMounted(() => {
applyRouteTab()
loadSettings()
if (activeTab.value === 'features' && isEnterpriseAdmin()) {
loadAdminPermissions()
}
})
</script>
@@ -217,6 +324,26 @@ onMounted(() => {
gap: 0;
}
.hint-muted {
font-size: 13px;
color: #6b7280;
margin: 0;
}
.perm-admin-row {
display: flex;
align-items: center;
justify-content: space-between;
max-width: 360px;
padding: 12px 0;
border-bottom: 1px solid #f3f4f6;
}
.perm-admin-label {
font-size: 14px;
color: #374151;
}
.custom-tabs-container {
background-color: #f3f4f6;
padding: 4px;

View File

@@ -106,9 +106,26 @@
</template>
</el-table-column>
<el-table-column label="操作" width="140" fixed="right">
<el-table-column label="超管授权" min-width="200">
<template #default="{ row }">
<div class="perm-tags">
<el-tag v-for="p in permItems" :key="p.key" size="small"
:type="permCeilingVal(row, p.key) !== false ? 'success' : 'info'"
:effect="permCeilingVal(row, p.key) !== false ? 'light' : 'plain'"
class="perm-tag"
>{{ p.label }}</el-tag>
</div>
</template>
</el-table-column>
<el-table-column label="操作" width="176" fixed="right">
<template #default="{ row }">
<div class="action-buttons">
<el-tooltip content="邀请小程序码" placement="top">
<el-button link class="action-invite" @click="openInviteQrcodeDialog(row)">
<el-icon><Picture /></el-icon>
</el-button>
</el-tooltip>
<el-button link @click="handleView(row)"><el-icon><View /></el-icon></el-button>
<el-button link @click="handleEdit(row)"><el-icon><Edit /></el-icon></el-button>
<el-button link type="danger" @click="handleDelete(row)"><el-icon><Delete /></el-icon></el-button>
@@ -222,6 +239,16 @@
</el-form-item>
</el-col>
</el-row>
<el-form-item label="功能授权(超管)" class="mt-12">
<p class="perm-form-hint">决定企业管理员能否在后台为终端用户打开该项关闭后管理端不会出现对应开关</p>
<div class="perm-switch-group">
<div class="perm-switch-item" v-for="p in permItems" :key="p.key">
<span class="perm-label">{{ p.label }}</span>
<el-switch v-model="newEnterprise.permissions[p.key]" />
</div>
</div>
</el-form-item>
</el-form>
<template #footer>
@@ -294,6 +321,16 @@
class="w-full"
/>
</el-form-item>
<el-form-item label="功能授权(超管)">
<p class="perm-form-hint">保存后将收紧企业管理员可调范围对已关闭项终端与企业后台同步关闭</p>
<div class="perm-switch-group">
<div class="perm-switch-item" v-for="p in permItems" :key="p.key">
<span class="perm-label">{{ p.label }}</span>
<el-switch v-model="editEnterprise.permissions[p.key]" />
</div>
</div>
</el-form-item>
</el-form>
<template #footer>
@@ -660,6 +697,54 @@
</div>
</template>
</el-dialog>
<!-- 邀请小程序码按当前行企业 ID -->
<el-dialog
v-model="showInviteQrcodeDialog"
width="440px"
class="custom-dialog invite-qrcode-dialog"
align-center
destroy-on-close
@closed="resetInviteQrcodeDialog"
>
<template #header>
<div class="dialog-header">
<h3 class="dialog-title">邀请小程序码</h3>
<p class="dialog-subtitle">
{{ inviteDialogEnterprise?.name || '企业' }} · 企业版进企业测评个人版进小程序首页
</p>
</div>
</template>
<div v-loading="inviteQrcodeLoading" class="invite-qrcode-dialog-body">
<div
v-if="inviteQrcodeEnterpriseB64 || inviteQrcodePersonalB64"
class="invite-qrcode-pair"
>
<div v-if="inviteQrcodeEnterpriseB64" class="invite-qrcode-card">
<span class="invite-qrcode-label">企业版</span>
<img :src="inviteQrcodeEnterpriseB64" alt="企业版太阳码" class="invite-qrcode-img" />
</div>
<div v-if="inviteQrcodePersonalB64" class="invite-qrcode-card">
<span class="invite-qrcode-label">个人版</span>
<img :src="inviteQrcodePersonalB64" alt="个人版太阳码" class="invite-qrcode-img" />
</div>
</div>
<p v-else class="invite-qrcode-placeholder">{{ inviteQrcodeError || '加载中…' }}</p>
</div>
<template #footer>
<div class="dialog-footer">
<el-button @click="showInviteQrcodeDialog = false" class="cancel-btn">关闭</el-button>
<el-button
type="primary"
color="#3b82f6"
:loading="inviteQrcodeLoading"
@click="loadInviteQrcodeForDialog"
>
刷新
</el-button>
</div>
</template>
</el-dialog>
</div>
</template>
@@ -680,7 +765,8 @@ import {
DataAnalysis,
Document,
Wallet,
Postcard
Postcard,
Picture
} from '@element-plus/icons-vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { request } from '@/utils/request'
@@ -715,6 +801,61 @@ const detailOrderPageSize = ref(10)
const enterpriseDetailTab = ref('overview')
const creating = ref(false)
/** 邀请小程序码(企业列表操作列) */
const showInviteQrcodeDialog = ref(false)
const inviteDialogEnterprise = ref<{ id: number; name: string } | null>(null)
const inviteQrcodeLoading = ref(false)
const inviteQrcodeEnterpriseB64 = ref('')
const inviteQrcodePersonalB64 = ref('')
const inviteQrcodeError = ref('')
const resetInviteQrcodeDialog = () => {
inviteDialogEnterprise.value = null
inviteQrcodeEnterpriseB64.value = ''
inviteQrcodePersonalB64.value = ''
inviteQrcodeError.value = ''
}
const loadInviteQrcodeForDialog = async () => {
const ent = inviteDialogEnterprise.value
if (!ent?.id) return
inviteQrcodeLoading.value = true
inviteQrcodeError.value = ''
try {
const res: any = await request.get('/superadmin/invite/qrcode', {
params: { enterpriseId: ent.id }
})
const d = res?.data
const qEnt = d?.enterprise?.qrcode ?? d?.qrcode
const qPer = d?.personal?.qrcode
inviteQrcodeEnterpriseB64.value = typeof qEnt === 'string' && qEnt ? qEnt : ''
inviteQrcodePersonalB64.value = typeof qPer === 'string' && qPer ? qPer : ''
if (!inviteQrcodeEnterpriseB64.value && !inviteQrcodePersonalB64.value) {
inviteQrcodeError.value = res?.message || res?.msg || '生成失败,请检查小程序配置'
ElMessage.error(inviteQrcodeError.value)
}
} catch (e: any) {
inviteQrcodeError.value = e?.message || '加载失败'
ElMessage.error(inviteQrcodeError.value)
} finally {
inviteQrcodeLoading.value = false
}
}
const openInviteQrcodeDialog = (row: any) => {
inviteDialogEnterprise.value = {
id: Number(row.id),
name: row.name ? String(row.name) : `企业#${row.id}`
}
inviteQrcodeEnterpriseB64.value = ''
inviteQrcodePersonalB64.value = ''
inviteQrcodeError.value = ''
showInviteQrcodeDialog.value = true
nextTick(() => {
loadInviteQrcodeForDialog()
})
}
/** 企业侧栏头像首字 */
const enterpriseAvatarLetter = computed(() => {
const n = viewEnterpriseData.value?.name
@@ -773,6 +914,25 @@ const statusOptions = [
{ label: '已停用', value: 'disabled' }
]
const permItems = [
{ key: 'face', label: '人脸分析' },
{ key: 'mbti', label: 'MBTI' },
{ key: 'pdp', label: 'PDP' },
{ key: 'disc', label: 'DISC' },
{ key: 'distribution', label: '分销' },
]
const defaultPermissions = () => ({ face: true, mbti: true, pdp: true, disc: true, distribution: true })
/** 列表/展示:超管授权上限(兼容未返回 permissionsCeiling 的旧接口) */
const permCeilingVal = (row: Record<string, any>, key: string) => {
const c = row.permissionsCeiling
if (c && typeof c === 'object' && key in c) return c[key]
const p = row.permissions
if (p && typeof p === 'object' && key in p) return p[key]
return true
}
const enterprises = ref<any[]>([])
const newEnterprise = reactive({
@@ -785,7 +945,8 @@ const newEnterprise = reactive({
contactPhone: '',
contactEmail: '',
status: 'operating',
trialExpireAt: '' as string
trialExpireAt: '' as string,
permissions: defaultPermissions() as Record<string, boolean>
})
const editEnterprise = reactive({
@@ -795,7 +956,8 @@ const editEnterprise = reactive({
contactPhone: '',
contactEmail: '',
status: 'operating',
trialExpireAt: ''
trialExpireAt: '',
permissions: defaultPermissions() as Record<string, boolean>
})
const getStatusLabel = (status: string) => {
@@ -925,7 +1087,8 @@ const handleCreateEnterprise = async () => {
contactName: newEnterprise.contactName,
contactPhone: newEnterprise.contactPhone,
contactEmail: newEnterprise.contactEmail,
status: newEnterprise.status
status: newEnterprise.status,
permissions: { ...newEnterprise.permissions }
})
if (response.code === 200) {
@@ -941,7 +1104,8 @@ const handleCreateEnterprise = async () => {
contactPhone: '',
contactEmail: '',
status: 'operating',
trialExpireAt: ''
trialExpireAt: '',
permissions: defaultPermissions()
})
loadEnterprises()
}
@@ -1059,11 +1223,18 @@ const handleEditDialogOpen = async () => {
editEnterprise.contactEmail = ''
editEnterprise.status = 'operating'
editEnterprise.trialExpireAt = ''
editEnterprise.permissions = defaultPermissions()
// 等待一个tick确保清空完成
await nextTick()
// 使用 Object.assign 一次性更新所有字段
const ceilingFromApi =
data.permissionsCeiling && typeof data.permissionsCeiling === 'object'
? { ...defaultPermissions(), ...data.permissionsCeiling }
: data.permissions && typeof data.permissions === 'object'
? { ...defaultPermissions(), ...data.permissions }
: defaultPermissions()
Object.assign(editEnterprise, {
name: data.name || '',
code: data.code || '',
@@ -1071,7 +1242,8 @@ const handleEditDialogOpen = async () => {
contactPhone: data.contactPhone || '',
contactEmail: data.contactEmail || '',
status: data.status || 'operating',
trialExpireAt: trialExpireAtStr
trialExpireAt: trialExpireAtStr,
permissions: ceilingFromApi
})
console.log('表单数据已填充:', editEnterprise)
@@ -1101,7 +1273,8 @@ const handleSaveEdit = async () => {
contactName: editEnterprise.contactName,
contactPhone: editEnterprise.contactPhone,
contactEmail: editEnterprise.contactEmail,
status: editEnterprise.status
status: editEnterprise.status,
permissions: { ...editEnterprise.permissions }
}
// 如果选择试用,添加到期时间(转换为时间戳)
@@ -1336,10 +1509,56 @@ watch([searchTerm, statusFilter], () => {
&.el-button--danger:hover {
color: #ef4444;
}
&.action-invite:hover {
color: #3b82f6;
}
}
}
}
.invite-qrcode-dialog-body {
min-height: 140px;
}
.invite-qrcode-pair {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 20px;
padding: 8px 0;
}
.invite-qrcode-card {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
}
.invite-qrcode-label {
font-size: 12px;
font-weight: 600;
color: #374151;
}
.invite-qrcode-img {
width: 120px;
height: 120px;
border-radius: 8px;
border: 1px solid #e5e7eb;
object-fit: contain;
background: #fff;
}
.invite-qrcode-placeholder {
margin: 0;
font-size: 13px;
color: #9ca3af;
text-align: center;
padding: 24px 12px;
}
.empty-state {
padding: 60px;
text-align: center;
@@ -1962,6 +2181,48 @@ watch([searchTerm, statusFilter], () => {
min-height: auto;
}
/* 权限标签 */
.perm-tags {
display: flex;
flex-wrap: wrap;
gap: 4px;
}
.perm-tag {
font-size: 11px;
border-radius: 4px;
}
.perm-form-hint {
font-size: 12px;
color: #6b7280;
margin: 0 0 10px 0;
line-height: 1.45;
}
/* 权限开关组(编辑 / 创建弹窗) */
.perm-switch-group {
display: flex;
flex-wrap: wrap;
gap: 14px 24px;
}
.perm-switch-item {
display: flex;
align-items: center;
gap: 8px;
}
.perm-label {
font-size: 13px;
color: #374151;
white-space: nowrap;
}
.mt-12 {
margin-top: 12px;
}
@media (max-width: 900px) {
.ud-wrap {
flex-direction: column;

View File

@@ -133,6 +133,50 @@
</div>
</div>
<!-- 邀请小程序码与企业后台一致企业版 + 个人版 -->
<div class="invite-section">
<div class="section-header invite-header">
<div>
<h2 class="section-title">邀请小程序码</h2>
<p class="section-subtitle">企业版进企业测评个人版进小程序首页企业版太阳码对应下方所选企业</p>
</div>
<div class="invite-header-actions">
<el-select
v-model="inviteSelectedEnterpriseId"
placeholder="选择企业(生成企业版码)"
filterable
clearable
style="width: 220px"
size="small"
@change="loadInviteQrcode"
>
<el-option
v-for="opt in inviteEnterpriseOptions"
:key="opt.id"
:label="opt.name"
:value="opt.id"
/>
</el-select>
<el-button type="primary" size="small" :loading="inviteLoading" @click="loadInviteQrcode">
{{ inviteQrcodeEnterprise || inviteQrcodePersonal ? '刷新' : '生成' }}
</el-button>
</div>
</div>
<div class="invite-body">
<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>
<!-- 测试趋势折线图 -->
<div class="trend-section">
<div class="section-header">
@@ -238,6 +282,7 @@ import {
Setting
} from '@element-plus/icons-vue'
import { request } from '@/utils/request'
import { ElMessage } from 'element-plus'
import { use } from 'echarts/core'
import { CanvasRenderer } from 'echarts/renderers'
import { LineChart } from 'echarts/charts'
@@ -272,10 +317,18 @@ const stats = reactive({
interface DynamicItem { icon: string; text: string; time: string }
interface RankingItem { name: string; tests: number; amount: number }
interface InviteEnterpriseOption { id: number; name: string }
const recentDynamics = ref<DynamicItem[]>([])
const enterpriseRanking = ref<RankingItem[]>([])
const inviteLoading = ref(false)
const inviteQrcodeEnterprise = ref<string>('')
const inviteQrcodePersonal = ref<string>('')
const inviteLoadError = ref<string>('')
const inviteEnterpriseOptions = ref<InviteEnterpriseOption[]>([])
const inviteSelectedEnterpriseId = ref<number | null>(null)
type TrendPoint = { date: string; face: number; mbti: number; pdp: number; disc: number; total: number }
const testTrends = ref<TrendPoint[]>([])
@@ -430,11 +483,76 @@ const handleViewAll = () => {
router.push('/superadmin/enterprises')
}
onMounted(() => {
const loadInviteEnterprisesAndSettings = async () => {
try {
const [settingsRes, entRes]: any[] = await Promise.all([
request.get('/superadmin/settings'),
request.get('/superadmin/enterprises', { params: { page: 1, pageSize: 500 } })
])
const list = entRes?.data?.list ?? []
inviteEnterpriseOptions.value = list.map((r: any) => ({
id: Number(r.id),
name: r.name ? String(r.name) : `企业#${r.id}`
}))
const defRaw = settingsRes?.data?.system?.defaultEnterpriseId
const defNum =
defRaw != null && defRaw !== '' && !Number.isNaN(Number(defRaw)) ? Number(defRaw) : null
const idSet = new Set(inviteEnterpriseOptions.value.map(o => o.id))
if (defNum != null && defNum > 0 && idSet.has(defNum)) {
inviteSelectedEnterpriseId.value = defNum
} else if (inviteEnterpriseOptions.value.length > 0) {
inviteSelectedEnterpriseId.value = inviteEnterpriseOptions.value[0].id
} else {
inviteSelectedEnterpriseId.value = null
}
} catch (e) {
console.error('加载邀请码企业列表失败:', e)
inviteEnterpriseOptions.value = []
inviteSelectedEnterpriseId.value = null
}
}
const loadInviteQrcode = async () => {
if (inviteLoading.value) return
inviteLoading.value = true
inviteLoadError.value = ''
try {
const params: Record<string, number> = {}
if (inviteSelectedEnterpriseId.value != null && inviteSelectedEnterpriseId.value > 0) {
params.enterpriseId = inviteSelectedEnterpriseId.value
}
const res: any = await request.get('/superadmin/invite/qrcode', {
params: Object.keys(params).length ? params : undefined
})
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) {
const msg = error?.message || '生成失败'
inviteLoadError.value = msg
ElMessage.error(msg)
} finally {
inviteLoading.value = false
}
}
onMounted(async () => {
loadOverview()
loadRecentDynamics()
loadEnterpriseRanking()
loadTestTrends()
await loadInviteEnterprisesAndSettings()
loadInviteQrcode()
})
</script>
@@ -622,6 +740,68 @@ onMounted(() => {
}
}
.invite-section {
margin-bottom: 28px;
background: #fff;
border-radius: 10px;
padding: 20px 24px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
border: 1px solid #f3f4f6;
.invite-header {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 16px;
flex-wrap: wrap;
margin-bottom: 16px;
}
.invite-header-actions {
display: flex;
align-items: center;
gap: 10px;
flex-shrink: 0;
}
.invite-body {
display: flex;
flex-wrap: wrap;
align-items: flex-start;
justify-content: center;
gap: 16px;
min-height: 112px;
padding: 8px 0;
}
.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;
border-radius: 8px;
border: 1px solid #e5e7eb;
object-fit: contain;
background: #fff;
}
.invite-placeholder {
font-size: 12px;
color: #9ca3af;
}
}
.trend-section {
margin-bottom: 28px;
background: #fff;