feat: 企业版合作模式(后端、管理端、小程序)
- API:合作模式配置、用户选择、企业版入口与迁移 SQL - 管理端:合作方式管理、企业/用户侧设置与 CSV 导出等 - 小程序:合作模式弹窗与结果/简历等页对接;开发脚本小调整 Made-with: Cursor
This commit is contained in:
166
admin/src/views/admin/CooperationChoices.vue
Normal file
166
admin/src/views/admin/CooperationChoices.vue
Normal file
@@ -0,0 +1,166 @@
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<div class="page-header">
|
||||
<div class="header-left">
|
||||
<h2>合作意向</h2>
|
||||
<p class="subtitle">用户在企业版完成三项测评并选择合作模式后的记录;可导出为 CSV 用 Excel 打开</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-alert
|
||||
v-if="!canUse"
|
||||
type="warning"
|
||||
:closable="false"
|
||||
show-icon
|
||||
title="当前账号未绑定企业,无数据范围"
|
||||
/>
|
||||
|
||||
<template v-else>
|
||||
<div class="toolbar">
|
||||
<el-input
|
||||
v-model="keyword"
|
||||
placeholder="搜索微信昵称、手机号、模式代码"
|
||||
clearable
|
||||
class="search-input"
|
||||
@keyup.enter="onSearch"
|
||||
/>
|
||||
<el-button type="primary" @click="onSearch" :loading="loading">查询</el-button>
|
||||
<el-button :loading="exporting" @click="doExport">导出 CSV</el-button>
|
||||
</div>
|
||||
|
||||
<el-table v-loading="loading" :data="list" border stripe class="data-table" empty-text="暂无选择记录">
|
||||
<el-table-column prop="userId" label="用户ID" width="88" />
|
||||
<el-table-column prop="nickname" label="微信昵称" min-width="120" show-overflow-tooltip />
|
||||
<el-table-column prop="phone" label="手机号" width="120" show-overflow-tooltip />
|
||||
<el-table-column prop="modeCode" label="模式代码" width="120" />
|
||||
<el-table-column prop="modeTitle" label="模式标题" min-width="120" show-overflow-tooltip />
|
||||
<el-table-column prop="chosenAtText" label="选择时间" width="170" />
|
||||
<el-table-column prop="updatedAtText" label="更新时间" width="170" />
|
||||
</el-table>
|
||||
|
||||
<div class="pager-wrap" v-if="total > 0">
|
||||
<el-pagination
|
||||
v-model:current-page="page"
|
||||
v-model:page-size="pageSize"
|
||||
:total="total"
|
||||
:page-sizes="[20, 50, 100]"
|
||||
layout="total, sizes, prev, pager, next"
|
||||
@current-change="load"
|
||||
@size-change="load"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { request } from '@/utils/request'
|
||||
import { downloadCsvGet } from '@/utils/downloadCsv'
|
||||
import { getAdminRole } from '@/utils/authStorage'
|
||||
|
||||
const canUse = computed(() => {
|
||||
const r = getAdminRole()
|
||||
return r === 'admin' || r === 'enterprise_admin'
|
||||
})
|
||||
|
||||
const loading = ref(false)
|
||||
const exporting = ref(false)
|
||||
const list = ref<any[]>([])
|
||||
const total = ref(0)
|
||||
const page = ref(1)
|
||||
const pageSize = ref(20)
|
||||
const keyword = ref('')
|
||||
|
||||
const load = async () => {
|
||||
if (!canUse.value) return
|
||||
loading.value = true
|
||||
try {
|
||||
const res: any = await request.get('/admin/enterprise/cooperation-choices', {
|
||||
params: {
|
||||
page: page.value,
|
||||
pageSize: pageSize.value,
|
||||
keyword: keyword.value.trim() || undefined
|
||||
}
|
||||
})
|
||||
if (res.code === 200) {
|
||||
list.value = res.data?.list || []
|
||||
total.value = res.data?.total ?? 0
|
||||
}
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.message || '加载失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const onSearch = () => {
|
||||
page.value = 1
|
||||
load()
|
||||
}
|
||||
|
||||
const doExport = async () => {
|
||||
if (!canUse.value) return
|
||||
exporting.value = true
|
||||
try {
|
||||
const q: Record<string, string> = {}
|
||||
const k = keyword.value.trim()
|
||||
if (k) q.keyword = k
|
||||
await downloadCsvGet(
|
||||
'admin/enterprise/cooperation-choices/export',
|
||||
`cooperation-choices.csv`,
|
||||
q
|
||||
)
|
||||
ElMessage.success('已开始下载')
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.message || '导出失败')
|
||||
} finally {
|
||||
exporting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (canUse.value) load()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.page-container {
|
||||
padding: 24px;
|
||||
min-height: calc(100vh - 64px);
|
||||
}
|
||||
.page-header {
|
||||
margin-bottom: 20px;
|
||||
.header-left h2 {
|
||||
margin: 0 0 4px 0;
|
||||
font-size: 22px;
|
||||
font-weight: 700;
|
||||
color: #111827;
|
||||
}
|
||||
.subtitle {
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
color: #6b7280;
|
||||
}
|
||||
}
|
||||
.toolbar {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.search-input {
|
||||
width: 280px;
|
||||
max-width: 100%;
|
||||
}
|
||||
.data-table {
|
||||
width: 100%;
|
||||
}
|
||||
.pager-wrap {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 16px;
|
||||
}
|
||||
</style>
|
||||
@@ -62,6 +62,71 @@
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div v-if="isEnterpriseAdmin()" class="coop-embed-section" v-loading="coopLoading">
|
||||
<div class="cunkebao-embed-header">
|
||||
<h4 class="cunkebao-embed-title">合作模式</h4>
|
||||
<p class="hint-muted cunkebao-embed-desc">
|
||||
用户在本企业版完成简历、面相与 MBTI 后,可择一合作意向;与超管侧配置一致,仅影响本企业。代码为小写英文、数字、下划线(1–32 位),已保存的代码不可改。
|
||||
</p>
|
||||
</div>
|
||||
<div class="coop-embed-toolbar">
|
||||
<el-button type="primary" link @click="addCoopRow">+ 新增合作模式</el-button>
|
||||
</div>
|
||||
<el-table
|
||||
v-if="coopModes.length"
|
||||
:data="coopModes"
|
||||
border
|
||||
size="small"
|
||||
class="w-full"
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-table-column label="代码" width="160">
|
||||
<template #default="{ row }">
|
||||
<el-input
|
||||
v-model="row.code"
|
||||
size="small"
|
||||
placeholder="如 partner_project"
|
||||
:disabled="row.codeLocked"
|
||||
maxlength="32"
|
||||
@blur="() => normalizeSettingsCoopCode(row)"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="启用" width="78" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-switch v-model="row.enabled" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="排序" width="104" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-input-number v-model="row.sortOrder" :min="0" :max="9999" size="small" controls-position="right" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="标题" min-width="120">
|
||||
<template #default="{ row }">
|
||||
<el-input v-model="row.title" size="small" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="说明" min-width="160">
|
||||
<template #default="{ row }">
|
||||
<el-input v-model="row.description" type="textarea" :rows="2" size="small" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="72" align="center" fixed="right">
|
||||
<template #default="{ $index }">
|
||||
<el-button type="danger" link size="small" @click="removeCoopRow($index)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<p v-else class="hint-muted">暂无配置,可点击「新增合作模式」添加。</p>
|
||||
<div class="save-actions">
|
||||
<el-button type="primary" class="save-btn" :loading="coopSaving" @click="saveCooperationModes">
|
||||
保存合作模式
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template v-if="canConfigureCunkebaoKeys()">
|
||||
<div class="cunkebao-embed-section" v-loading="cunkebaoLoading">
|
||||
<div class="cunkebao-embed-header">
|
||||
@@ -274,6 +339,117 @@ const cunkebaoUnified = reactive({
|
||||
const cunkebaoLoading = ref(false)
|
||||
const cunkebaoSaving = ref(false)
|
||||
|
||||
/** 企业管理员:合作模式 */
|
||||
type CoopModeRow = {
|
||||
code: string
|
||||
title: string
|
||||
description: string
|
||||
sortOrder: number
|
||||
enabled: boolean
|
||||
codeLocked: boolean
|
||||
}
|
||||
|
||||
const coopModes = ref<CoopModeRow[]>([])
|
||||
const coopLoading = ref(false)
|
||||
const coopSaving = ref(false)
|
||||
|
||||
const COOP_CODE_RE = /^[a-z0-9_]{1,32}$/
|
||||
|
||||
const normalizeSettingsCoopCode = (row: CoopModeRow) => {
|
||||
row.code = String(row.code || '')
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9_]/g, '')
|
||||
}
|
||||
|
||||
const addCoopRow = () => {
|
||||
const maxSort = coopModes.value.reduce((m, r) => Math.max(m, Number(r.sortOrder) || 0), 0)
|
||||
coopModes.value.push({
|
||||
code: '',
|
||||
title: '',
|
||||
description: '',
|
||||
sortOrder: maxSort + 10,
|
||||
enabled: true,
|
||||
codeLocked: false
|
||||
})
|
||||
}
|
||||
|
||||
const removeCoopRow = (index: number) => {
|
||||
coopModes.value = coopModes.value.filter((_, i) => i !== index)
|
||||
}
|
||||
|
||||
const loadCooperationModes = async () => {
|
||||
if (!isEnterpriseAdmin()) return
|
||||
coopLoading.value = true
|
||||
try {
|
||||
const res: any = await request.get('/admin/enterprise/cooperation-modes')
|
||||
const list = res?.data?.list ?? []
|
||||
coopModes.value = Array.isArray(list)
|
||||
? list.map((row: any) => ({
|
||||
code: String(row.code || ''),
|
||||
title: String(row.title || ''),
|
||||
description: String(row.description || ''),
|
||||
sortOrder: Number(row.sortOrder) || 0,
|
||||
enabled: !!row.enabled,
|
||||
codeLocked: true
|
||||
}))
|
||||
: []
|
||||
} catch (e: any) {
|
||||
coopModes.value = []
|
||||
ElMessage.error(e?.message || '加载合作模式失败')
|
||||
} finally {
|
||||
coopLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const saveCooperationModes = async () => {
|
||||
if (!isEnterpriseAdmin()) return
|
||||
const seen = new Set<string>()
|
||||
const modes: Record<string, unknown>[] = []
|
||||
for (const r of coopModes.value) {
|
||||
const code = String(r.code || '')
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9_]/g, '')
|
||||
if (!code) {
|
||||
ElMessage.error('每行需填写模式代码,或先删除空行再保存')
|
||||
return
|
||||
}
|
||||
if (!COOP_CODE_RE.test(code)) {
|
||||
ElMessage.error(`模式代码不合法:${code}(仅 1–32 位小写字母、数字、下划线)`)
|
||||
return
|
||||
}
|
||||
if (seen.has(code)) {
|
||||
ElMessage.error(`模式代码重复:${code}`)
|
||||
return
|
||||
}
|
||||
seen.add(code)
|
||||
modes.push({
|
||||
modeCode: code,
|
||||
enabled: r.enabled,
|
||||
sortOrder: r.sortOrder,
|
||||
title: r.title,
|
||||
description: r.description
|
||||
})
|
||||
}
|
||||
if (modes.length === 0) {
|
||||
ElMessage.error('请至少保留一条合作模式')
|
||||
return
|
||||
}
|
||||
coopSaving.value = true
|
||||
try {
|
||||
const res: any = await request.put('/admin/enterprise/cooperation-modes', { modes })
|
||||
if (res.code === 200) {
|
||||
ElMessage.success('合作模式已保存')
|
||||
await loadCooperationModes()
|
||||
}
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.message || '保存失败')
|
||||
} finally {
|
||||
coopSaving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const loadCunkebaoKeys = async () => {
|
||||
if (!canConfigureCunkebaoKeys()) return
|
||||
cunkebaoLoading.value = true
|
||||
@@ -410,6 +586,7 @@ watch(
|
||||
if (tab === 'features') {
|
||||
if (isEnterpriseAdmin()) {
|
||||
loadAdminPermissions()
|
||||
loadCooperationModes()
|
||||
}
|
||||
if (canConfigureCunkebaoKeys()) {
|
||||
loadCunkebaoKeys()
|
||||
@@ -427,6 +604,7 @@ onMounted(() => {
|
||||
if (activeTab.value === 'features') {
|
||||
if (isEnterpriseAdmin()) {
|
||||
loadAdminPermissions()
|
||||
loadCooperationModes()
|
||||
}
|
||||
if (canConfigureCunkebaoKeys()) {
|
||||
loadCunkebaoKeys()
|
||||
@@ -469,6 +647,16 @@ onMounted(() => {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.coop-embed-section {
|
||||
margin-top: 28px;
|
||||
padding-top: 24px;
|
||||
border-top: 1px solid #e5e7eb;
|
||||
}
|
||||
|
||||
.coop-embed-toolbar {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.cunkebao-embed-section {
|
||||
margin-top: 28px;
|
||||
padding-top: 24px;
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="profile-summary profile-summary--five">
|
||||
<div class="profile-summary profile-summary--six">
|
||||
<div class="summary-card">
|
||||
<div class="summary-ic ic-blue">👥</div>
|
||||
<div class="summary-body">
|
||||
@@ -61,6 +61,16 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="summary-card">
|
||||
<div class="summary-ic ic-coop">🤝</div>
|
||||
<div class="summary-body">
|
||||
<div class="summary-label">已选合作意向</div>
|
||||
<div class="summary-value">
|
||||
{{ profileStats.cooperationCount }}
|
||||
<span class="summary-sub">/ {{ pageUserCount }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 搜索栏 -->
|
||||
@@ -149,6 +159,20 @@
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="合作意向" min-width="150" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
<div v-if="row.cooperationModeTitle || row.cooperationModeCode" class="coop-cell">
|
||||
<span class="coop-title">{{ row.cooperationModeTitle || row.cooperationModeCode || '—' }}</span>
|
||||
<span
|
||||
v-if="row.cooperationModeCode && row.cooperationModeTitle && row.cooperationModeCode !== row.cooperationModeTitle"
|
||||
class="coop-code"
|
||||
>{{ row.cooperationModeCode }}</span>
|
||||
<div v-if="row.cooperationChosenAt" class="coop-time">{{ formatDate(row.cooperationChosenAt) }}</div>
|
||||
</div>
|
||||
<span v-else class="coop-empty">—</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="测评次数 · 最近活跃" width="180">
|
||||
<template #default="{ row }">
|
||||
<div class="activity-cell">
|
||||
@@ -751,6 +775,7 @@ const profileStats = computed(() => {
|
||||
let mbtiCount = 0
|
||||
let sbtiCount = 0
|
||||
let anyTestCount = 0
|
||||
let cooperationCount = 0
|
||||
for (const u of list) {
|
||||
const hasFace = !!(
|
||||
u.faceMbtiType ||
|
||||
@@ -767,8 +792,9 @@ const profileStats = computed(() => {
|
||||
if (hasMbti) mbtiCount++
|
||||
if (hasSbti) sbtiCount++
|
||||
if (hasFace || hasMbti || hasSbti || hasDisc || hasPdp) anyTestCount++
|
||||
if (u.cooperationModeCode || u.cooperationModeTitle || u.cooperationChosenAt) cooperationCount++
|
||||
}
|
||||
return { faceCount, mbtiCount, sbtiCount, anyTestCount }
|
||||
return { faceCount, mbtiCount, sbtiCount, anyTestCount, cooperationCount }
|
||||
})
|
||||
|
||||
async function loadUsers() {
|
||||
@@ -1242,6 +1268,16 @@ onMounted(() => {
|
||||
}
|
||||
}
|
||||
|
||||
&.profile-summary--six {
|
||||
grid-template-columns: repeat(6, minmax(0, 1fr));
|
||||
@media (max-width: 1280px) {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
}
|
||||
@media (max-width: 900px) {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
@@ -1278,6 +1314,7 @@ onMounted(() => {
|
||||
&.ic-indigo { background: #eef2ff; }
|
||||
&.ic-violet { background: #f5f3ff; }
|
||||
&.ic-amber { background: #fffbeb; }
|
||||
&.ic-coop { background: #f0fdf4; }
|
||||
}
|
||||
|
||||
.summary-body {
|
||||
@@ -1380,6 +1417,29 @@ onMounted(() => {
|
||||
}
|
||||
}
|
||||
|
||||
.coop-cell {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
line-height: 1.35;
|
||||
}
|
||||
.coop-title {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: #111827;
|
||||
}
|
||||
.coop-code {
|
||||
font-size: 12px;
|
||||
color: #6b7280;
|
||||
}
|
||||
.coop-time {
|
||||
font-size: 12px;
|
||||
color: #9ca3af;
|
||||
}
|
||||
.coop-empty {
|
||||
color: #d1d5db;
|
||||
}
|
||||
|
||||
.user-info-cell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
193
admin/src/views/superadmin/CooperationChoicesPanel.vue
Normal file
193
admin/src/views/superadmin/CooperationChoicesPanel.vue
Normal file
@@ -0,0 +1,193 @@
|
||||
<template>
|
||||
<div class="ccp-wrap">
|
||||
<p class="ccp-hint">
|
||||
先选择要查看的企业,再加载或导出该企业在小程序中记录的用户合作意向(与表 <code>mbti_user_cooperation_choices</code> 一致)。
|
||||
</p>
|
||||
|
||||
<div class="ccp-toolbar">
|
||||
<el-select
|
||||
v-model="enterpriseId"
|
||||
filterable
|
||||
clearable
|
||||
placeholder="选择企业"
|
||||
class="ccp-select"
|
||||
:loading="entLoading"
|
||||
@visible-change="onEntDropdown"
|
||||
>
|
||||
<el-option v-for="e in entOptions" :key="e.id" :label="e.label" :value="e.id" />
|
||||
</el-select>
|
||||
<el-input
|
||||
v-model="keyword"
|
||||
placeholder="搜索昵称/手机/模式代码"
|
||||
clearable
|
||||
class="ccp-search"
|
||||
@keyup.enter="onSearch"
|
||||
/>
|
||||
<el-button type="primary" :disabled="!enterpriseId" :loading="loading" @click="onSearch">查询</el-button>
|
||||
<el-button :disabled="!enterpriseId" :loading="exporting" @click="doExport">导出 CSV</el-button>
|
||||
</div>
|
||||
|
||||
<el-table v-loading="loading" :data="list" border stripe class="ccp-table" empty-text="暂无数据或无权限">
|
||||
<el-table-column prop="enterpriseId" label="企业ID" width="88" />
|
||||
<el-table-column prop="enterpriseName" label="企业名称" min-width="140" show-overflow-tooltip />
|
||||
<el-table-column prop="userId" label="用户ID" width="88" />
|
||||
<el-table-column prop="nickname" label="微信昵称" min-width="120" show-overflow-tooltip />
|
||||
<el-table-column prop="phone" label="手机号" width="120" show-overflow-tooltip />
|
||||
<el-table-column prop="modeCode" label="模式代码" width="120" />
|
||||
<el-table-column prop="modeTitle" label="模式标题" min-width="120" show-overflow-tooltip />
|
||||
<el-table-column prop="chosenAtText" label="选择时间" width="170" />
|
||||
<el-table-column prop="updatedAtText" label="更新时间" width="170" />
|
||||
</el-table>
|
||||
|
||||
<div class="pager-wrap" v-if="enterpriseId && total > 0">
|
||||
<el-pagination
|
||||
v-model:current-page="page"
|
||||
v-model:page-size="pageSize"
|
||||
:total="total"
|
||||
:page-sizes="[20, 50, 100]"
|
||||
layout="total, sizes, prev, pager, next"
|
||||
@current-change="load"
|
||||
@size-change="load"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { request } from '@/utils/request'
|
||||
import { downloadCsvGet } from '@/utils/downloadCsv'
|
||||
|
||||
const entLoading = ref(false)
|
||||
const entOptions = ref<{ id: number; label: string }[]>([])
|
||||
|
||||
const enterpriseId = ref<number | undefined>(undefined)
|
||||
const keyword = ref('')
|
||||
const loading = ref(false)
|
||||
const exporting = ref(false)
|
||||
const list = ref<any[]>([])
|
||||
const total = ref(0)
|
||||
const page = ref(1)
|
||||
const pageSize = ref(20)
|
||||
|
||||
const loadEntOptions = async () => {
|
||||
if (entOptions.value.length > 0) return
|
||||
entLoading.value = true
|
||||
try {
|
||||
const res: any = await request.get('/superadmin/enterprises', { params: { page: 1, pageSize: 500 } })
|
||||
const raw = res?.data?.list ?? res?.data?.data?.list
|
||||
const rows = Array.isArray(raw) ? raw : []
|
||||
entOptions.value = rows.map((r: any) => ({
|
||||
id: Number(r.id),
|
||||
label: `${r.name || '未命名'} (#${r.id})`
|
||||
}))
|
||||
} catch {
|
||||
entOptions.value = []
|
||||
} finally {
|
||||
entLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const onEntDropdown = (open: boolean) => {
|
||||
if (open) loadEntOptions()
|
||||
}
|
||||
|
||||
const load = async () => {
|
||||
const eid = enterpriseId.value
|
||||
if (!eid) {
|
||||
list.value = []
|
||||
total.value = 0
|
||||
return
|
||||
}
|
||||
loading.value = true
|
||||
try {
|
||||
const res: any = await request.get(`/superadmin/enterprises/${eid}/cooperation-choices`, {
|
||||
params: {
|
||||
page: page.value,
|
||||
pageSize: pageSize.value,
|
||||
keyword: keyword.value.trim() || undefined
|
||||
}
|
||||
})
|
||||
if (res.code === 200) {
|
||||
list.value = res.data?.list || []
|
||||
total.value = res.data?.total ?? 0
|
||||
}
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.message || '加载失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const onSearch = () => {
|
||||
page.value = 1
|
||||
load()
|
||||
}
|
||||
|
||||
const doExport = async () => {
|
||||
const eid = enterpriseId.value
|
||||
if (!eid) return
|
||||
exporting.value = true
|
||||
try {
|
||||
const q: Record<string, string> = {}
|
||||
const k = keyword.value.trim()
|
||||
if (k) q.keyword = k
|
||||
await downloadCsvGet(
|
||||
`superadmin/enterprises/${eid}/cooperation-choices/export`,
|
||||
`cooperation-choices-e${eid}.csv`,
|
||||
q
|
||||
)
|
||||
ElMessage.success('已开始下载')
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.message || '导出失败')
|
||||
} finally {
|
||||
exporting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
watch(enterpriseId, () => {
|
||||
page.value = 1
|
||||
load()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.ccp-wrap {
|
||||
max-width: 1200px;
|
||||
}
|
||||
.ccp-hint {
|
||||
font-size: 13px;
|
||||
color: #6b7280;
|
||||
line-height: 1.5;
|
||||
margin: 0 0 16px 0;
|
||||
code {
|
||||
font-size: 12px;
|
||||
background: #f3f4f6;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
.ccp-toolbar {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.ccp-select {
|
||||
min-width: 280px;
|
||||
}
|
||||
.ccp-search {
|
||||
width: 240px;
|
||||
max-width: 100%;
|
||||
}
|
||||
.ccp-table {
|
||||
width: 100%;
|
||||
}
|
||||
.pager-wrap {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 16px;
|
||||
}
|
||||
</style>
|
||||
@@ -27,6 +27,7 @@
|
||||
<Enterprises v-if="activeTab === 'companies'" embedded />
|
||||
<Users v-if="activeTab === 'users'" :key="usersRefreshKey" embedded />
|
||||
<SoulArticles v-if="activeTab === 'soulArticles'" />
|
||||
<CooperationChoicesPanel v-if="activeTab === 'cooperation'" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -39,10 +40,11 @@ import { request } from '@/utils/request'
|
||||
import Enterprises from './Enterprises.vue'
|
||||
import Users from './Users.vue'
|
||||
import SoulArticles from './SoulArticles.vue'
|
||||
import CooperationChoicesPanel from './CooperationChoicesPanel.vue'
|
||||
import SaPageHeader from '@/components/superadmin/SaPageHeader.vue'
|
||||
import SaTabs from '@/components/superadmin/SaTabs.vue'
|
||||
|
||||
const TAB_IDS = ['companies', 'users', 'soulArticles'] as const
|
||||
const TAB_IDS = ['companies', 'users', 'soulArticles', 'cooperation'] as const
|
||||
type TabId = (typeof TAB_IDS)[number]
|
||||
|
||||
function isTabId(s: string): s is TabId {
|
||||
@@ -96,7 +98,8 @@ async function runOrphanMigrate() {
|
||||
const innerTabs: { label: string; value: TabId }[] = [
|
||||
{ label: '企业列表', value: 'companies' },
|
||||
{ label: '用户总览', value: 'users' },
|
||||
{ label: '引流文章', value: 'soulArticles' }
|
||||
{ label: '引流文章', value: 'soulArticles' },
|
||||
{ label: '合作意向', value: 'cooperation' }
|
||||
]
|
||||
|
||||
function queryWithoutTab(): Record<string, string> {
|
||||
|
||||
@@ -265,7 +265,7 @@
|
||||
<el-dialog
|
||||
v-model="showEditDialog"
|
||||
title="编辑企业"
|
||||
width="440px"
|
||||
width="min(720px, 96vw)"
|
||||
class="custom-dialog"
|
||||
:show-close="true"
|
||||
align-center
|
||||
@@ -331,6 +331,62 @@
|
||||
</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="合作模式(企业版小程序)">
|
||||
<div v-loading="editCoopLoading" class="coop-edit-block">
|
||||
<p class="perm-form-hint">
|
||||
用户在该企业完成简历、面相与 MBTI 后将弹出选择;关闭项不会对用户展示。代码为小写英文、数字、下划线(1–32 位),保存后不可改。
|
||||
</p>
|
||||
<div class="coop-edit-toolbar">
|
||||
<el-button type="primary" link @click="addEditCoopRow">+ 新增合作模式</el-button>
|
||||
</div>
|
||||
<el-table v-if="editCoopModes.length" :data="editCoopModes" border size="small" class="coop-edit-table">
|
||||
<el-table-column label="代码" width="160">
|
||||
<template #default="{ row }">
|
||||
<el-input
|
||||
v-model="row.code"
|
||||
size="small"
|
||||
placeholder="如 partner_project"
|
||||
:disabled="row.codeLocked"
|
||||
maxlength="32"
|
||||
@blur="() => normalizeCoopCode(row)"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="启用" width="78" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-switch v-model="row.enabled" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="排序" width="104" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-input-number v-model="row.sortOrder" :min="0" :max="9999" size="small" controls-position="right" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="标题" min-width="120">
|
||||
<template #default="{ row }">
|
||||
<el-input v-model="row.title" size="small" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="说明" min-width="160">
|
||||
<template #default="{ row }">
|
||||
<el-input v-model="row.description" type="textarea" :rows="2" size="small" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="72" align="center" fixed="right">
|
||||
<template #default="{ $index }">
|
||||
<el-button type="danger" link size="small" @click="removeEditCoopRow($index)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<p v-else class="hint-muted">暂无配置,可点击「新增合作模式」添加;保存后未包含的旧记录将从数据库删除。</p>
|
||||
<div class="coop-edit-actions">
|
||||
<el-button type="primary" plain size="small" :loading="editCoopSaving" @click="saveEditCooperationModes">
|
||||
保存合作模式
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
@@ -808,6 +864,119 @@ const inviteQrcodeEnterpriseB64 = ref('')
|
||||
const inviteQrcodePersonalB64 = ref('')
|
||||
const inviteQrcodeError = ref('')
|
||||
|
||||
/** 编辑弹窗:合作模式(GET/PUT enterprises/:id/cooperation-modes) */
|
||||
type EditCoopRow = {
|
||||
code: string
|
||||
title: string
|
||||
description: string
|
||||
sortOrder: number
|
||||
enabled: boolean
|
||||
/** 服务端已有记录为 true,保存后代码不可改 */
|
||||
codeLocked: boolean
|
||||
}
|
||||
|
||||
const editCoopModes = ref<EditCoopRow[]>([])
|
||||
const editCoopLoading = ref(false)
|
||||
const editCoopSaving = ref(false)
|
||||
|
||||
const normalizeCoopCode = (row: EditCoopRow) => {
|
||||
row.code = String(row.code || '')
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9_]/g, '')
|
||||
}
|
||||
|
||||
const addEditCoopRow = () => {
|
||||
const maxSort = editCoopModes.value.reduce((m, r) => Math.max(m, Number(r.sortOrder) || 0), 0)
|
||||
editCoopModes.value.push({
|
||||
code: '',
|
||||
title: '',
|
||||
description: '',
|
||||
sortOrder: maxSort + 10,
|
||||
enabled: true,
|
||||
codeLocked: false
|
||||
})
|
||||
}
|
||||
|
||||
const removeEditCoopRow = (index: number) => {
|
||||
editCoopModes.value = editCoopModes.value.filter((_, i) => i !== index)
|
||||
}
|
||||
|
||||
const loadEditCooperationModes = async () => {
|
||||
const id = currentEditId.value
|
||||
if (!id) return
|
||||
editCoopLoading.value = true
|
||||
try {
|
||||
const res: any = await request.get(`/enterprises/${id}/cooperation-modes`)
|
||||
const list = res?.data?.list ?? []
|
||||
editCoopModes.value = Array.isArray(list)
|
||||
? list.map((row: any) => ({
|
||||
code: String(row.code || ''),
|
||||
title: String(row.title || ''),
|
||||
description: String(row.description || ''),
|
||||
sortOrder: Number(row.sortOrder) || 0,
|
||||
enabled: !!row.enabled,
|
||||
codeLocked: true
|
||||
}))
|
||||
: []
|
||||
} catch (e: any) {
|
||||
editCoopModes.value = []
|
||||
ElMessage.error(e?.message || '加载合作模式失败')
|
||||
} finally {
|
||||
editCoopLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const saveEditCooperationModes = async () => {
|
||||
const id = currentEditId.value
|
||||
if (!id) return
|
||||
const COOP_CODE_RE = /^[a-z0-9_]{1,32}$/
|
||||
const seen = new Set<string>()
|
||||
const modes: Record<string, unknown>[] = []
|
||||
for (const r of editCoopModes.value) {
|
||||
const code = String(r.code || '')
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9_]/g, '')
|
||||
if (!code) {
|
||||
ElMessage.error('每行需填写模式代码,或先删除空行再保存')
|
||||
return
|
||||
}
|
||||
if (!COOP_CODE_RE.test(code)) {
|
||||
ElMessage.error(`模式代码不合法:${code}(仅 1–32 位小写字母、数字、下划线)`)
|
||||
return
|
||||
}
|
||||
if (seen.has(code)) {
|
||||
ElMessage.error(`模式代码重复:${code}`)
|
||||
return
|
||||
}
|
||||
seen.add(code)
|
||||
modes.push({
|
||||
modeCode: code,
|
||||
enabled: r.enabled,
|
||||
sortOrder: r.sortOrder,
|
||||
title: r.title,
|
||||
description: r.description
|
||||
})
|
||||
}
|
||||
if (modes.length === 0) {
|
||||
ElMessage.error('请至少保留一条合作模式')
|
||||
return
|
||||
}
|
||||
editCoopSaving.value = true
|
||||
try {
|
||||
const res: any = await request.put(`/enterprises/${id}/cooperation-modes`, { modes })
|
||||
if (res.code === 200) {
|
||||
ElMessage.success('合作模式已保存')
|
||||
await loadEditCooperationModes()
|
||||
}
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.message || '保存失败')
|
||||
} finally {
|
||||
editCoopSaving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const resetInviteQrcodeDialog = () => {
|
||||
inviteDialogEnterprise.value = null
|
||||
inviteQrcodeEnterpriseB64.value = ''
|
||||
@@ -1248,6 +1417,7 @@ const handleEditDialogOpen = async () => {
|
||||
|
||||
console.log('表单数据已填充:', editEnterprise)
|
||||
pendingEditData.value = null
|
||||
await loadEditCooperationModes()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2200,6 +2370,10 @@ watch([searchTerm, statusFilter], () => {
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.coop-edit-toolbar {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
/* 权限开关组(编辑 / 创建弹窗) */
|
||||
.perm-switch-group {
|
||||
display: flex;
|
||||
|
||||
Reference in New Issue
Block a user