refactor: 重构用户列表字段结构和组件化弹窗

- 调整用户列表字段,包含ID、手机号、微信号、注册时间、备注、剩余体验次数
- 更新mock数据,匹配新的字段结构
- 将次数设置弹窗拆分为独立组件LimitSettingModal
- 优化搜索功能,支持手机号和微信号搜索
- 简化主页面代码,提高可维护性
This commit is contained in:
Wei_佳 2025-11-13 16:33:35 +08:00
parent 85ced0bf36
commit 8b47f76965
3 changed files with 266 additions and 246 deletions

View File

@ -44,68 +44,68 @@ export default {
// Mock 数据 // Mock 数据
const mockUsers = [ const mockUsers = [
{ {
id: 1, id: 11111111,
phone: '13800138001', phone: '15021982682',
email: 'zhang.wei@example.com', wechat: 'f1498480844',
created_at: '2024-01-15T10:30:00Z', created_at: '2024-01-15T10:30:00Z',
last_login: '2024-11-13T09:15:00Z', notes: '测试用户1',
is_active: true remaining_count: 1
}, },
{ {
id: 2, id: 11111112,
phone: '13800138002', phone: '13800138002',
email: 'li.ming@example.com', wechat: 'wx_limming2024',
created_at: '2024-02-20T14:20:00Z', created_at: '2024-02-20T14:20:00Z',
last_login: '2024-11-12T16:45:00Z', notes: '付费用户',
is_active: true remaining_count: 5
}, },
{ {
id: 3, id: 11111113,
phone: '13800138003', phone: '13800138003',
email: 'wang.fang@example.com', wechat: null,
created_at: '2024-03-10T08:45:00Z', created_at: '2024-03-10T08:45:00Z',
last_login: null, notes: null,
is_active: false remaining_count: 0
}, },
{ {
id: 4, id: 11111114,
phone: '13800138004', phone: '13800138004',
email: 'chen.jun@example.com', wechat: 'chenjun_vip',
created_at: '2024-04-05T11:30:00Z', created_at: '2024-04-05T11:30:00Z',
last_login: '2024-11-10T13:20:00Z', notes: 'VIP用户',
is_active: true remaining_count: 10
}, },
{ {
id: 5, id: 11111115,
phone: '13800138005', phone: '13800138005',
email: 'liu.xia@example.com', wechat: 'liuxia888',
created_at: '2024-05-12T16:15:00Z', created_at: '2024-05-12T16:15:00Z',
last_login: '2024-11-11T10:30:00Z', notes: '体验用户',
is_active: true remaining_count: 3
}, },
{ {
id: 6, id: 11111116,
phone: '13800138006', phone: '13800138006',
email: null, wechat: null,
created_at: '2024-06-18T09:00:00Z', created_at: '2024-06-18T09:00:00Z',
last_login: null, notes: '新注册用户',
is_active: false remaining_count: 2
}, },
{ {
id: 7, id: 11111117,
phone: '13800138007', phone: '13800138007',
email: 'zhao.lei@example.com', wechat: 'zhaolei2024',
created_at: '2024-07-22T12:45:00Z', created_at: '2024-07-22T12:45:00Z',
last_login: '2024-11-13T08:30:00Z', notes: null,
is_active: true remaining_count: 0
}, },
{ {
id: 8, id: 11111118,
phone: '13800138008', phone: '13800138008',
email: 'sun.mei@example.com', wechat: 'sunmei_user',
created_at: '2024-08-30T15:20:00Z', created_at: '2024-08-30T15:20:00Z',
last_login: '2024-11-09T14:15:00Z', notes: '活跃用户',
is_active: true remaining_count: 7
} }
] ]
@ -119,10 +119,10 @@ export default {
) )
} }
// 邮箱搜索 // 微信号搜索
if (params.email) { if (params.wechat) {
filteredUsers = filteredUsers.filter(user => filteredUsers = filteredUsers.filter(user =>
user.email && user.email.includes(params.email) user.wechat && user.wechat.includes(params.wechat)
) )
} }

View File

@ -0,0 +1,208 @@
<script setup>
import { ref, watch } from 'vue'
import {
NModal,
NButton,
NSelect,
NInput,
NDivider,
} from 'naive-ui'
// Props
const props = defineProps({
visible: {
type: Boolean,
default: false
},
userData: {
type: Object,
default: () => ({})
}
})
// Emits
const emit = defineEmits(['update:visible', 'save'])
//
const limitForm = ref({
remainingCount: 0,
type: '免费体验',
experienceCount: 1,
notes: ''
})
//
const typeOptions = [
{ label: '免费体验', value: '免费体验' },
{ label: '付费用户', value: '付费用户' },
{ label: 'VIP用户', value: 'VIP用户' }
]
//
watch(() => props.userData, (newData) => {
if (newData && Object.keys(newData).length > 0) {
limitForm.value = {
remainingCount: newData.remaining_count || 0,
type: newData.user_type || '免费体验',
experienceCount: newData.experience_count || 1,
notes: newData.notes || ''
}
}
}, { immediate: true })
//
function handleExperienceCountChange(delta) {
const newCount = limitForm.value.experienceCount + delta
if (newCount >= 0) {
limitForm.value.experienceCount = newCount
}
}
//
function handleSave() {
const data = {
user_id: props.userData.id,
...limitForm.value
}
emit('save', data)
}
//
function handleCancel() {
emit('update:visible', false)
//
limitForm.value = {
remainingCount: 0,
type: '免费体验',
experienceCount: 1,
notes: ''
}
}
</script>
<template>
<NModal
:show="visible"
preset="card"
title="估值设置"
style="width: 500px;"
:bordered="false"
size="huge"
role="dialog"
aria-modal="true"
@update:show="$emit('update:visible', $event)"
>
<div class="limit-setting-form">
<!-- 剩余估值次数 -->
<div class="form-row">
<span class="label">剩余估值次数</span>
<span class="value">{{ limitForm.remainingCount }}</span>
</div>
<NDivider style="margin: 16px 0;" />
<!-- 类型选择 -->
<div class="form-row">
<span class="label">类型</span>
<NSelect
v-model:value="limitForm.type"
:options="typeOptions"
style="width: 120px;"
/>
</div>
<!-- 体验次数 -->
<div class="form-row">
<span class="label">体验次数</span>
<div class="count-control">
<NButton
size="small"
@click="handleExperienceCountChange(-1)"
:disabled="limitForm.experienceCount <= 0"
>
-
</NButton>
<span class="count-value">{{ limitForm.experienceCount }}</span>
<NButton
size="small"
@click="handleExperienceCountChange(1)"
>
+
</NButton>
</div>
</div>
<!-- 备注 -->
<div class="form-row notes-row">
<span class="label">备注</span>
</div>
<NInput
v-model:value="limitForm.notes"
type="textarea"
placeholder="请输入备注信息"
:rows="4"
style="margin-top: 8px;"
/>
<!-- 操作按钮 -->
<div class="action-buttons">
<NButton @click="handleCancel">
取消
</NButton>
<NButton type="primary" @click="handleSave">
确定
</NButton>
</div>
</div>
</NModal>
</template>
<style scoped>
.limit-setting-form {
padding: 16px 0;
}
.form-row {
display: flex;
align-items: center;
margin-bottom: 16px;
}
.notes-row {
margin-bottom: 8px;
}
.label {
font-size: 14px;
color: #333;
min-width: 100px;
margin-right: 12px;
}
.value {
font-size: 14px;
color: #666;
}
.count-control {
display: flex;
align-items: center;
gap: 12px;
}
.count-value {
font-size: 16px;
font-weight: 500;
min-width: 20px;
text-align: center;
}
.action-buttons {
display: flex;
justify-content: flex-end;
gap: 12px;
margin-top: 24px;
padding-top: 16px;
border-top: 1px solid #f0f0f0;
}
</style>

View File

@ -9,18 +9,13 @@ import {
NSwitch, NSwitch,
NTag, NTag,
NPopconfirm, NPopconfirm,
NModal,
NCard,
NSelect,
NInputNumber,
NText,
NDivider,
} from 'naive-ui' } from 'naive-ui'
import CommonPage from '@/components/page/CommonPage.vue' import CommonPage from '@/components/page/CommonPage.vue'
import QueryBarItem from '@/components/query-bar/QueryBarItem.vue' import QueryBarItem from '@/components/query-bar/QueryBarItem.vue'
import CrudModal from '@/components/table/CrudModal.vue' import CrudModal from '@/components/table/CrudModal.vue'
import CrudTable from '@/components/table/CrudTable.vue' import CrudTable from '@/components/table/CrudTable.vue'
import LimitSettingModal from '@/components/user-management/LimitSettingModal.vue'
import { formatDate, renderIcon } from '@/utils' import { formatDate, renderIcon } from '@/utils'
import { useCRUD } from '@/composables' import { useCRUD } from '@/composables'
@ -35,21 +30,8 @@ const vPermission = resolveDirective('permission')
// //
const limitModalVisible = ref(false) const limitModalVisible = ref(false)
const limitForm = ref({
remainingCount: 0,
type: '免费体验',
experienceCount: 1,
notes: ''
})
const currentUser = ref(null) const currentUser = ref(null)
//
const typeOptions = [
{ label: '免费体验', value: '免费体验' },
{ label: '付费用户', value: '付费用户' },
{ label: 'VIP用户', value: 'VIP用户' }
]
const { const {
modalVisible, modalVisible,
modalTitle, modalTitle,
@ -90,13 +72,13 @@ const columns = [
ellipsis: { tooltip: true }, ellipsis: { tooltip: true },
}, },
{ {
title: '邮箱', title: '微信号',
key: 'email', key: 'wechat',
width: 120, width: 120,
align: 'center', align: 'center',
ellipsis: { tooltip: true }, ellipsis: { tooltip: true },
render(row) { render(row) {
return row.email || '-' return row.wechat || '-'
}, },
}, },
{ {
@ -106,48 +88,26 @@ const columns = [
width: 160, width: 160,
ellipsis: { tooltip: true }, ellipsis: { tooltip: true },
render(row) { render(row) {
return h( return row.created_at ? formatDate(row.created_at) : '-'
NButton,
{ size: 'small', type: 'text', ghost: true },
{
default: () => (row.created_at ? formatDate(row.created_at) : '-'),
icon: renderIcon('mdi:calendar', { size: 16 }),
}
)
}, },
}, },
{ {
title: '最后登录', title: '备注',
key: 'last_login', key: 'notes',
align: 'center', align: 'center',
width: 160, width: 120,
ellipsis: { tooltip: true }, ellipsis: { tooltip: true },
render(row) { render(row) {
return h( return row.notes || '-'
NButton,
{ size: 'small', type: 'text', ghost: true },
{
default: () => (row.last_login ? formatDate(row.last_login) : '未登录'),
icon: renderIcon('mdi:login', { size: 16 }),
}
)
}, },
}, },
{ {
title: '状态', title: '剩余体验次数',
key: 'is_active', key: 'remaining_count',
width: 80, width: 120,
align: 'center', align: 'center',
render(row) { render(row) {
return h(NSwitch, { return row.remaining_count || 0
size: 'small',
rubberBand: false,
value: row.is_active,
loading: !!row.publishing,
checkedValue: true,
uncheckedValue: false,
onUpdateValue: () => handleUpdateStatus(row),
})
}, },
}, },
{ {
@ -204,24 +164,13 @@ function handleViewDetail(row) {
// //
function handleSetLimit(row) { function handleSetLimit(row) {
currentUser.value = row currentUser.value = row
//
limitForm.value = {
remainingCount: row.remaining_count || 0,
type: row.user_type || '免费体验',
experienceCount: row.experience_count || 1,
notes: row.notes || ''
}
limitModalVisible.value = true limitModalVisible.value = true
} }
// //
async function handleSaveLimitSetting() { async function handleSaveLimitSetting(data) {
try { try {
// API // API
const data = {
user_id: currentUser.value.id,
...limitForm.value
}
// await api.setUserLimit(data) // await api.setUserLimit(data)
$message.success('次数设置保存成功') $message.success('次数设置保存成功')
limitModalVisible.value = false limitModalVisible.value = false
@ -231,26 +180,6 @@ async function handleSaveLimitSetting() {
} }
} }
//
function handleCancelLimitSetting() {
limitModalVisible.value = false
limitForm.value = {
remainingCount: 0,
type: '免费体验',
experienceCount: 1,
notes: ''
}
currentUser.value = null
}
//
function handleExperienceCountChange(delta) {
const newCount = limitForm.value.experienceCount + delta
if (newCount >= 0) {
limitForm.value.experienceCount = newCount
}
}
const validateForm = { const validateForm = {
phone: [ phone: [
{ {
@ -298,12 +227,12 @@ const validateForm = {
@keypress.enter="$table?.handleSearch()" @keypress.enter="$table?.handleSearch()"
/> />
</QueryBarItem> </QueryBarItem>
<QueryBarItem label="邮箱" :label-width="50"> <QueryBarItem label="微信号" :label-width="50">
<NInput <NInput
v-model:value="queryItems.email" v-model:value="queryItems.wechat"
clearable clearable
type="text" type="text"
placeholder="请输入邮箱" placeholder="请输入微信号"
@keypress.enter="$table?.handleSearch()" @keypress.enter="$table?.handleSearch()"
/> />
</QueryBarItem> </QueryBarItem>
@ -337,128 +266,11 @@ const validateForm = {
</CrudModal> </CrudModal>
<!-- 次数设置弹窗 --> <!-- 次数设置弹窗 -->
<NModal <LimitSettingModal
v-model:show="limitModalVisible" v-model:visible="limitModalVisible"
preset="card" :user-data="currentUser"
title="估值设置" @save="handleSaveLimitSetting"
style="width: 500px;" />
:bordered="false"
size="huge"
role="dialog"
aria-modal="true"
>
<div class="limit-setting-form">
<!-- 剩余估值次数 -->
<div class="form-row">
<span class="label">剩余估值次数</span>
<span class="value">{{ limitForm.remainingCount }}</span>
</div>
<NDivider style="margin: 16px 0;" />
<!-- 类型选择 -->
<div class="form-row">
<span class="label">类型</span>
<NSelect
v-model:value="limitForm.type"
:options="typeOptions"
style="width: 120px;"
/>
</div>
<!-- 体验次数 -->
<div class="form-row">
<span class="label">体验次数</span>
<div class="count-control">
<NButton
size="small"
@click="handleExperienceCountChange(-1)"
:disabled="limitForm.experienceCount <= 0"
>
-
</NButton>
<span class="count-value">{{ limitForm.experienceCount }}</span>
<NButton
size="small"
@click="handleExperienceCountChange(1)"
>
+
</NButton>
</div>
</div>
<!-- 备注 -->
<div class="form-row notes-row">
<span class="label">备注</span>
</div>
<NInput
v-model:value="limitForm.notes"
type="textarea"
placeholder="请输入备注信息"
:rows="4"
style="margin-top: 8px;"
/>
<!-- 操作按钮 -->
<div class="action-buttons">
<NButton @click="handleCancelLimitSetting">
取消
</NButton>
<NButton type="primary" @click="handleSaveLimitSetting">
确定
</NButton>
</div>
</div>
</NModal>
</CommonPage> </CommonPage>
</template> </template>
<style scoped>
.limit-setting-form {
padding: 16px 0;
}
.form-row {
display: flex;
align-items: center;
margin-bottom: 16px;
}
.notes-row {
margin-bottom: 8px;
}
.label {
font-size: 14px;
color: #333;
min-width: 100px;
margin-right: 12px;
}
.value {
font-size: 14px;
color: #666;
}
.count-control {
display: flex;
align-items: center;
gap: 12px;
}
.count-value {
font-size: 16px;
font-weight: 500;
min-width: 20px;
text-align: center;
}
.action-buttons {
display: flex;
justify-content: flex-end;
gap: 12px;
margin-top: 24px;
padding-top: 16px;
border-top: 1px solid #f0f0f0;
}
</style>