feat: 会员数据分析页与俱乐部新建总会员入口

实现会员充值记录筛选表格与汇总;俱乐部视图增加新建总会员按钮并对接 hy-bundle-create、hy-recharge-records 接口。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
XingQue
2026-07-05 18:27:51 +08:00
parent 16b04e3905
commit 288d001579
3 changed files with 568 additions and 11 deletions

View File

@@ -308,6 +308,8 @@ export const JITUAN_API = {
memberEnable: '/jituan/houtai/hthysj',
memberDisable: '/jituan/houtai/hthyxj',
memberCardUpload: '/jituan/houtai/hy-card-upload',
memberRechargeRecords: '/jituan/houtai/hy-recharge-records',
memberBundleCreate: '/jituan/houtai/hy-bundle-create',
identityTagList: '/jituan/houtai/identity-tag/list',
identityTagManage: '/jituan/houtai/identity-tag/manage',
identityTagBindList: '/jituan/houtai/identity-tag/bind-list',

View File

@@ -1,8 +1,410 @@
<template>
<div class="member-data-page">
<ClubScopeHint />
<div class="filter-card">
<el-form :inline="true" :model="filters" class="filter-form" @submit.prevent="search">
<el-form-item label="订单ID">
<el-input v-model="filters.dingdan_id" clearable placeholder="订单ID" style="width: 160px" />
</el-form-item>
<el-form-item label="购买人ID">
<el-input v-model="filters.yonghuid" clearable placeholder="打手ID" style="width: 120px" />
</el-form-item>
<el-form-item label="购买人昵称">
<el-input v-model="filters.buyer_keyword" clearable placeholder="昵称关键词" style="width: 140px" />
</el-form-item>
<el-form-item label="会员类型">
<el-select v-model="filters.huiyuan_id" clearable filterable placeholder="全部" style="width: 180px">
<el-option
v-for="item in huiyuanOptions"
:key="item.huiyuan_id"
:label="`${item.jieshao}${item.is_bundle ? ' [总]' : ''}`"
:value="item.huiyuan_id"
/>
</el-select>
</el-form-item>
<el-form-item label="支付状态">
<el-select v-model="filters.zhuangtai" clearable placeholder="全部" style="width: 120px">
<el-option label="已支付" :value="3" />
<el-option label="待支付" :value="9" />
</el-select>
</el-form-item>
<el-form-item label="订单类型">
<el-select v-model="filters.is_trial" clearable placeholder="全部" style="width: 120px">
<el-option label="正式会员" :value="false" />
<el-option label="体验会员" :value="true" />
</el-select>
</el-form-item>
<el-form-item label="总会员单">
<el-select v-model="filters.is_bundle" clearable placeholder="全部" style="width: 120px">
<el-option label="是" :value="true" />
<el-option label="否" :value="false" />
</el-select>
</el-form-item>
<el-form-item label="已分红">
<el-select v-model="filters.has_fenhong" clearable placeholder="全部" style="width: 120px">
<el-option label="已分红" :value="true" />
<el-option label="未分红" :value="false" />
</el-select>
</el-form-item>
<el-form-item label="管事ID">
<el-input v-model="filters.guanshi_id" clearable placeholder="管事ID" style="width: 120px" />
</el-form-item>
<el-form-item label="组长ID">
<el-input v-model="filters.zuzhang_id" clearable placeholder="组长ID" style="width: 120px" />
</el-form-item>
<el-form-item label="金额区间">
<el-input-number v-model="filters.jine_min" :min="0" :precision="2" controls-position="right" placeholder="最低" style="width: 120px" />
<span class="range-sep">-</span>
<el-input-number v-model="filters.jine_max" :min="0" :precision="2" controls-position="right" placeholder="最高" style="width: 120px" />
</el-form-item>
<el-form-item label="购买时间">
<el-date-picker
v-model="dateRange"
type="datetimerange"
range-separator=""
start-placeholder="开始"
end-placeholder="结束"
value-format="YYYY-MM-DD HH:mm:ss"
style="width: 360px"
/>
</el-form-item>
<el-form-item label="关键词">
<el-input v-model="filters.keyword" clearable placeholder="订单/用户/说明" style="width: 160px" />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="search" :loading="loading">查询</el-button>
<el-button @click="resetFilters">重置</el-button>
</el-form-item>
</el-form>
</div>
<div class="summary-grid">
<div class="summary-card">
<div class="s-label">记录总数</div>
<div class="s-value">{{ summary.total_count }}</div>
</div>
<div class="summary-card">
<div class="s-label">已支付笔数</div>
<div class="s-value">{{ summary.paid_count }}</div>
</div>
<div class="summary-card">
<div class="s-label">待支付笔数</div>
<div class="s-value">{{ summary.pending_count }}</div>
</div>
<div class="summary-card">
<div class="s-label">已付金额合计</div>
<div class="s-value income">¥{{ formatMoney(summary.paid_jine) }}</div>
</div>
<div class="summary-card">
<div class="s-label">管事分红合计</div>
<div class="s-value">¥{{ formatMoney(summary.guanshi_fenhong_total) }}</div>
</div>
<div class="summary-card">
<div class="s-label">组长分红合计</div>
<div class="s-value">¥{{ formatMoney(summary.zuzhang_fenhong_total) }}</div>
</div>
<div class="summary-card">
<div class="s-label">体验单数</div>
<div class="s-value">{{ summary.trial_count }}</div>
</div>
</div>
<div class="table-card" v-loading="loading">
<el-table :data="list" stripe border style="width: 100%" row-key="dingdan_id">
<el-table-column type="expand">
<template #default="{ row }">
<div class="expand-panel" v-if="row.fenhong">
<div class="expand-title">分红明细</div>
<div class="expand-grid">
<div><span class="k">管事</span>{{ row.fenhong.guanshi_nicheng }} ({{ row.fenhong.guanshi_id }})</div>
<div><span class="k">管事分红</span>¥{{ formatMoney(row.fenhong.guanshi_fenhong) }}</div>
<div><span class="k">组长</span>{{ row.fenhong.zuzhang_nicheng || '-' }} ({{ row.fenhong.zuzhang_id || '-' }})</div>
<div><span class="k">组长分红</span>¥{{ formatMoney(row.fenhong.zuzhang_fenhong) }}</div>
<div><span class="k">第几次正式购买</span>{{ row.fenhong.formal_cishu ?? row.formal_cishu ?? '-' }}</div>
<div><span class="k">分红时间</span>{{ formatDate(row.fenhong.fenhong_time) }}</div>
<div><span class="k">体验分红</span>{{ row.fenhong.is_trial ? '是' : '否' }}</div>
<div><span class="k">说明</span>{{ row.fenhong.shuoming || '-' }}</div>
</div>
</div>
<div v-else class="expand-panel empty-fenhong">暂无分红记录可能未支付或尚未履约</div>
</template>
</el-table-column>
<el-table-column prop="dingdan_id" label="订单ID" min-width="180" show-overflow-tooltip />
<el-table-column prop="CreateTime" label="购买时间" min-width="170">
<template #default="{ row }">{{ formatDate(row.CreateTime) }}</template>
</el-table-column>
<el-table-column label="购买人" min-width="140">
<template #default="{ row }">
<div>{{ row.buyer_nicheng }}</div>
<div class="sub-id">{{ row.yonghuid }}</div>
</template>
</el-table-column>
<el-table-column label="会员类型" min-width="140">
<template #default="{ row }">
<div>
{{ row.huiyuan_name }}
<el-tag v-if="row.is_bundle" size="small" type="success"></el-tag>
<el-tag v-if="row.is_trial" size="small" type="warning">体验</el-tag>
</div>
<div class="sub-id">{{ row.huiyuan_id }}</div>
</template>
</el-table-column>
<el-table-column prop="jine" label="付款金额" min-width="100">
<template #default="{ row }">¥{{ formatMoney(row.jine) }}</template>
</el-table-column>
<el-table-column prop="purchase_days" label="天数" width="70" />
<el-table-column prop="zhuangtai_label" label="状态" width="90">
<template #default="{ row }">
<el-tag :type="row.zhuangtai === 3 ? 'success' : 'info'" size="small">{{ row.zhuangtai_label }}</el-tag>
</template>
</el-table-column>
<el-table-column label="正式第N次" width="100">
<template #default="{ row }">{{ row.formal_cishu ?? '-' }}</template>
</el-table-column>
<el-table-column label="管事分红" min-width="100">
<template #default="{ row }">
<span v-if="row.fenhong">¥{{ formatMoney(row.fenhong.guanshi_fenhong) }}</span>
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column label="组长分红" min-width="100">
<template #default="{ row }">
<span v-if="row.fenhong">¥{{ formatMoney(row.fenhong.zuzhang_fenhong) }}</span>
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column prop="club_id" label="俱乐部" width="80" />
<el-table-column prop="shuoming" label="说明" min-width="120" show-overflow-tooltip />
</el-table>
<div class="pager">
<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, jumper"
@current-change="fetchData"
@size-change="onSizeChange"
/>
</div>
</div>
</div>
</template>
<script>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
import request from '@/utils/request'
import { JITUAN_API } from '@/utils/club-context'
import ClubScopeHint from '@/components/ClubScopeHint.vue'
const username = localStorage.getItem('username') || ''
const loading = ref(false)
const list = ref([])
const total = ref(0)
const page = ref(1)
const pageSize = ref(20)
const huiyuanOptions = ref([])
const dateRange = ref([])
const filters = reactive({
dingdan_id: '',
yonghuid: '',
buyer_keyword: '',
huiyuan_id: '',
zhuangtai: '',
is_trial: '',
is_bundle: '',
has_fenhong: '',
guanshi_id: '',
zuzhang_id: '',
jine_min: null,
jine_max: null,
keyword: '',
})
const summary = reactive({
total_count: 0,
paid_count: 0,
pending_count: 0,
paid_jine: 0,
guanshi_fenhong_total: 0,
zuzhang_fenhong_total: 0,
trial_count: 0,
})
const formatMoney = (val) => {
if (val === undefined || val === null || val === '') return '0.00'
return Number(val).toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })
}
const formatDate = (val) => {
if (!val) return '-'
return String(val).replace('T', ' ').slice(0, 19)
}
const buildPayload = () => {
const payload = {
username,
page: page.value,
page_size: pageSize.value,
}
Object.entries(filters).forEach(([key, val]) => {
if (val !== '' && val !== null && val !== undefined) {
payload[key] = val
}
})
if (dateRange.value?.length === 2) {
payload.date_from = dateRange.value[0]
payload.date_to = dateRange.value[1]
}
return payload
}
const fetchData = async () => {
if (!username) {
ElMessage.error('未登录,请重新登录')
return
}
loading.value = true
try {
const res = await request.post(JITUAN_API.memberRechargeRecords, buildPayload())
if (res.code === 0) {
const data = res.data || {}
list.value = data.list || []
total.value = data.total || 0
huiyuanOptions.value = data.filter_options?.huiyuan_list || []
Object.assign(summary, data.summary || {})
} else {
ElMessage.error(res.msg || '查询失败')
}
} catch (e) {
console.error(e)
ElMessage.error('查询失败')
} finally {
loading.value = false
}
}
const search = () => {
page.value = 1
fetchData()
}
const resetFilters = () => {
Object.assign(filters, {
dingdan_id: '',
yonghuid: '',
buyer_keyword: '',
huiyuan_id: '',
zhuangtai: '',
is_trial: '',
is_bundle: '',
has_fenhong: '',
guanshi_id: '',
zuzhang_id: '',
jine_min: null,
jine_max: null,
keyword: '',
})
dateRange.value = []
search()
}
const onSizeChange = () => {
page.value = 1
fetchData()
}
onMounted(fetchData)
</script>
<style>
</style>
<style scoped>
.member-data-page {
padding: 20px;
min-height: 100vh;
color: #c0e0ff;
}
.filter-card,
.table-card {
background: rgba(18, 25, 35, 0.85);
border: 1px solid rgba(64, 158, 255, 0.15);
border-radius: 12px;
padding: 16px;
margin-bottom: 16px;
}
.filter-form :deep(.el-form-item__label) {
color: #9ec5ff;
}
.range-sep {
margin: 0 8px;
color: #9ec5ff;
}
.summary-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
gap: 12px;
margin-bottom: 16px;
}
.summary-card {
background: rgba(18, 25, 35, 0.85);
border: 1px solid rgba(64, 158, 255, 0.15);
border-radius: 10px;
padding: 14px;
}
.s-label {
font-size: 12px;
color: #8fb8e8;
margin-bottom: 6px;
}
.s-value {
font-size: 20px;
font-weight: 600;
color: #e6f3ff;
}
.s-value.income {
color: #67c23a;
}
.sub-id {
font-size: 12px;
color: #8fb8e8;
}
.pager {
display: flex;
justify-content: flex-end;
margin-top: 16px;
}
.expand-panel {
padding: 12px 20px;
background: rgba(10, 16, 24, 0.6);
}
.expand-title {
font-weight: 600;
margin-bottom: 10px;
color: #9ec5ff;
}
.expand-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
gap: 8px 16px;
font-size: 13px;
}
.expand-grid .k {
color: #8fb8e8;
margin-right: 8px;
}
.empty-fenhong {
color: #8fb8e8;
}
.table-card :deep(.el-table) {
--el-table-bg-color: transparent;
--el-table-tr-bg-color: rgba(12, 18, 28, 0.5);
--el-table-header-bg-color: rgba(20, 30, 45, 0.9);
--el-table-row-hover-bg-color: rgba(64, 158, 255, 0.08);
--el-table-text-color: #d8ebff;
--el-table-header-text-color: #9ec5ff;
--el-table-border-color: rgba(64, 158, 255, 0.12);
}
</style>

View File

@@ -24,7 +24,16 @@
show-icon
class="member-scope-tip"
title="集团汇总视图"
description="可新建全局会员类型;修改售价/体验配置请切换到具体俱乐部。"
description="可新建全局会员类型(含总会员开关);修改售价/体验/装饰图请切换到具体俱乐部。"
/>
<el-alert
v-if="!isAllClubScope"
type="success"
:closable="false"
show-icon
class="member-scope-tip"
title="总会员(包含式)"
description="点击「新建总会员」可一步创建并上架;需先上架至少一个普通会员作为包含项。也可在集团汇总先建类型,再在本俱乐部「上架会员」选用。"
/>
<div class="action-bar">
@@ -34,12 +43,10 @@
@click="openAddDialog"
:icon="Plus"
>新建全局会员类型</el-button>
<el-button
v-else
type="primary"
@click="openCatalogDialog"
:icon="Plus"
>上架会员</el-button>
<template v-else>
<el-button type="success" @click="openBundleDialog">新建总会员</el-button>
<el-button type="primary" @click="openCatalogDialog" :icon="Plus">上架会员</el-button>
</template>
</div>
<div class="member-grid" v-loading="loading">
@@ -241,6 +248,62 @@
</template>
</el-dialog>
<el-dialog
v-model="bundleDialogVisible"
title="新建总会员包含式"
width="640px"
@close="closeBundleDialog"
>
<el-alert
type="info"
:closable="false"
show-icon
class="enable-tip"
title="一步创建并上架"
description="将自动创建全局总会员类型并在当前俱乐部上架请至少选择一个已上架的普通会员作为包含项"
/>
<el-form :model="bundleForm" :rules="bundleRules" ref="bundleFormRef" label-width="130px">
<el-form-item label="会员名称" prop="jieshao">
<el-input v-model="bundleForm.jieshao" maxlength="30" placeholder="全能总会员" />
</el-form-item>
<el-form-item label="正式价格()" prop="jiage">
<el-input-number v-model="bundleForm.jiage" :min="0.01" :precision="2" :step="1" style="width: 100%" />
</el-form-item>
<el-form-item label="正式天数" prop="formal_days">
<el-input-number v-model="bundleForm.formal_days" :min="1" :max="3650" style="width: 100%" />
</el-form-item>
<el-form-item label="管事分成()" prop="guanshifc">
<el-input-number v-model="bundleForm.guanshifc" :min="0" :precision="2" :step="1" style="width: 100%" />
</el-form-item>
<el-form-item label="组长分成()" prop="zuzhangfc">
<el-input-number v-model="bundleForm.zuzhangfc" :min="0" :precision="2" :step="1" style="width: 100%" />
</el-form-item>
<el-form-item label="包含的子会员" prop="included_huiyuan_ids">
<el-select
v-model="bundleForm.included_huiyuan_ids"
multiple
filterable
placeholder="选择本俱乐部已上架的普通会员至少一个"
style="width: 100%"
>
<el-option
v-for="opt in enableSubMemberOptions"
:key="opt.huiyuan_id"
:label="`${opt.jieshao} (${opt.huiyuan_id})`"
:value="opt.huiyuan_id"
/>
</el-select>
</el-form-item>
<el-form-item label="详细规则介绍">
<el-input v-model="bundleForm.jtjieshao" type="textarea" rows="3" placeholder="选填" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="bundleDialogVisible = false">取消</el-button>
<el-button type="primary" @click="submitBundle" :loading="bundling">创建并上架</el-button>
</template>
</el-dialog>
<el-dialog
v-model="catalogDialogVisible"
title="从集团选用会员上架"
@@ -419,9 +482,28 @@ const addForm = ref({
jiage: 0,
guanshifc: 0,
zuzhangfc: 0,
jtjieshao: ''
jtjieshao: '',
is_bundle: false,
})
const bundleDialogVisible = ref(false)
const bundleFormRef = ref(null)
const bundling = ref(false)
const bundleForm = ref({
jieshao: '',
jiage: 0,
formal_days: 30,
guanshifc: 0,
zuzhangfc: 0,
jtjieshao: '',
included_huiyuan_ids: [],
})
const bundleRules = {
jieshao: [{ required: true, message: '请输入会员名称', trigger: 'blur' }],
jiage: [{ required: true, message: '请输入价格', trigger: 'blur' }],
included_huiyuan_ids: [{ type: 'array', required: true, min: 1, message: '请至少选择一个子会员', trigger: 'change' }],
}
const catalogDialogVisible = ref(false)
const catalogLoading = ref(false)
const catalogList = ref([])
@@ -692,6 +774,77 @@ const submitAdd = async () => {
}
}
const openBundleDialog = () => {
if (isAllClubScope.value) {
ElMessage.warning('请切换到具体俱乐部后再新建总会员')
return
}
if (enableSubMemberOptions.value.length === 0) {
ElMessage.warning('请先上架至少一个普通会员,再创建总会员')
return
}
bundleForm.value = {
jieshao: '',
jiage: 0,
formal_days: 30,
guanshifc: 0,
zuzhangfc: 0,
jtjieshao: '',
included_huiyuan_ids: enableSubMemberOptions.value.map((m) => m.huiyuan_id),
}
bundleDialogVisible.value = true
}
const closeBundleDialog = () => {
bundleDialogVisible.value = false
bundleFormRef.value?.resetFields()
}
const submitBundle = async () => {
try {
await bundleFormRef.value?.validate()
} catch {
return
}
const f = bundleForm.value
if (f.jiage <= 0) {
ElMessage.error('正式价格必须大于 0')
return
}
if (!validatePriceSplit(f.jiage, f.guanshifc, f.zuzhangfc, false, 0, 0, 0)) {
return
}
if (!f.included_huiyuan_ids?.length) {
ElMessage.error('请至少选择一个包含的子会员')
return
}
bundling.value = true
try {
const res = await request.post(JITUAN_API.memberBundleCreate, {
username: username.value,
jieshao: f.jieshao,
jiage: f.jiage,
formal_days: f.formal_days,
guanshifc: f.guanshifc,
zuzhangfc: f.zuzhangfc,
jtjieshao: f.jtjieshao || '',
included_huiyuan_ids: f.included_huiyuan_ids,
})
if (res.code === 0) {
ElMessage.success('总会员创建并上架成功')
bundleDialogVisible.value = false
await fetchMemberList()
} else {
ElMessage.error(res.msg || '创建失败')
}
} catch (error) {
console.error(error)
ElMessage.error('创建失败')
} finally {
bundling.value = false
}
}
const openCatalogDialog = async () => {
if (isAllClubScope.value) {
ElMessage.warning('请切换到具体俱乐部后再上架会员')