feat: 替换用户管理模块的模拟数据为真实接口,并优化额度设置功能

This commit is contained in:
Wei_佳 2025-11-20 14:52:15 +08:00
parent 7c64f0c76a
commit 2ff5421c27
7 changed files with 134 additions and 284 deletions

74
menu_init.sql Normal file
View File

@ -0,0 +1,74 @@
-- 完整菜单初始化SQL
-- 创建时间: 2025-11-20
-- 说明: 包含所有新增的菜单项和权限分配
-- ========================================
-- 1. 工作台菜单
-- ========================================
INSERT INTO menu (id, name, menu_type, icon, path, "order", parent_id, is_hidden, component, keepalive, redirect, created_at, updated_at)
VALUES
(22, '工作台', 'menu', 'carbon:dashboard', '/workbench', 1, 0, 0, '/workbench', 1, NULL, datetime('now'), datetime('now'));
-- ========================================
-- 2. 交易管理菜单
-- ========================================
-- 插入一级目录:交易管理
INSERT INTO menu (id, name, menu_type, icon, path, "order", parent_id, is_hidden, component, keepalive, redirect, created_at, updated_at)
VALUES
(16, '交易管理', 'catalog', 'carbon:receipt', '/transaction', 3, 0, 0, 'Layout', 0, '/transaction/invoice', datetime('now'), datetime('now'));
-- 插入二级菜单:开票记录
INSERT INTO menu (id, name, menu_type, icon, path, "order", parent_id, is_hidden, component, keepalive, redirect, created_at, updated_at)
VALUES
(17, '开票记录', 'menu', 'carbon:document', 'invoice', 1, 16, 0, '/transaction/invoice', 0, NULL, datetime('now'), datetime('now'));
-- ========================================
-- 3. 估值管理菜单
-- ========================================
-- 插入一级目录:估值管理
INSERT INTO menu (id, name, menu_type, icon, path, "order", parent_id, is_hidden, component, keepalive, redirect, created_at, updated_at)
VALUES
(18, '估值管理', 'catalog', 'carbon:calculator', '/valuation', 4, 0, 0, 'Layout', 0, '/valuation/audit', datetime('now'), datetime('now'));
-- 插入二级菜单:审核列表
INSERT INTO menu (id, name, menu_type, icon, path, "order", parent_id, is_hidden, component, keepalive, redirect, created_at, updated_at)
VALUES
(19, '审核列表', 'menu', 'carbon:task-approved', 'audit', 1, 18, 0, '/valuation/audit', 0, NULL, datetime('now'), datetime('now'));
-- ========================================
-- 4. 用户管理菜单
-- ========================================
-- 插入一级目录:用户管理
INSERT INTO menu (id, name, menu_type, icon, path, "order", parent_id, is_hidden, component, keepalive, redirect, created_at, updated_at)
VALUES
(20, '用户管理', 'catalog', 'carbon:user-multiple', '/user-management', 5, 0, 0, 'Layout', 0, '/user-management/user-list', datetime('now'), datetime('now'));
-- 插入二级菜单:用户列表
INSERT INTO menu (id, name, menu_type, icon, path, "order", parent_id, is_hidden, component, keepalive, redirect, created_at, updated_at)
VALUES
(21, '用户列表', 'menu', 'carbon:user', 'user-list', 1, 20, 0, '/user-management/user-list', 0, NULL, datetime('now'), datetime('now'));
-- ========================================
-- 角色权限分配
-- ========================================
-- 为管理员角色(role_id=1)分配所有菜单权限
INSERT INTO role_menu (role_id, menu_id)
VALUES
(1, 22), -- 工作台
(1, 16), -- 交易管理
(1, 17), -- 开票记录
(1, 18), -- 估值管理
(1, 19), -- 审核列表
(1, 20), -- 用户管理
(1, 21); -- 用户列表
-- 为普通用户角色(role_id=2)分配基础菜单权限
INSERT INTO role_menu (role_id, menu_id)
VALUES
(2, 22), -- 工作台
(2, 16), -- 交易管理
(2, 17), -- 开票记录
(2, 18), -- 估值管理
(2, 19); -- 审核列表
-- 注意:普通用户不分配用户管理权限

View File

@ -5,4 +5,4 @@ VITE_PUBLIC_PATH = '/'
VITE_USE_PROXY = true VITE_USE_PROXY = true
# base api # base api
VITE_BASE_API = '/api/v1' VITE_BASE_API = 'http://139.224.70.152:9990/api/v1'

View File

@ -276,120 +276,6 @@ const mockValuationDetails = valuationRecords.map((record) => ({
...record, ...record,
})) }))
const mockAppUsers = [
{
id: 11111111,
phone: '15021982682',
wechat: 'f1498480844',
created_at: '2024-01-15T10:30:00Z',
notes: '测试用户1',
remaining_count: 1,
user_type: '体验用户',
},
{
id: 11111112,
phone: '13800138002',
wechat: 'wx_limming2024',
created_at: '2024-02-20T14:20:00Z',
notes: '付费用户',
remaining_count: 5,
user_type: '付费用户',
},
{
id: 11111113,
phone: '13800138003',
wechat: null,
created_at: '2024-03-10T08:45:00Z',
notes: null,
remaining_count: 0,
user_type: '体验用户',
},
{
id: 11111114,
phone: '13800138004',
wechat: 'chenjun_vip',
created_at: '2024-04-05T11:30:00Z',
notes: 'VIP用户',
remaining_count: 10,
user_type: 'VIP',
},
{
id: 11111115,
phone: '13800138005',
wechat: 'liuxia888',
created_at: '2024-05-12T16:15:00Z',
notes: '体验用户',
remaining_count: 3,
user_type: '体验用户',
},
{
id: 11111116,
phone: '13800138006',
wechat: null,
created_at: '2024-06-18T09:00:00Z',
notes: '新注册用户',
remaining_count: 2,
user_type: '体验用户',
},
{
id: 11111117,
phone: '13800138007',
wechat: 'zhaolei2024',
created_at: '2024-07-22T12:45:00Z',
notes: null,
remaining_count: 0,
user_type: '体验用户',
},
{
id: 11111118,
phone: '13800138008',
wechat: 'sunmei_user',
created_at: '2024-08-30T15:20:00Z',
notes: '活跃用户',
remaining_count: 7,
user_type: 'VIP',
},
]
const defaultInvoiceHeaders = [
{
company_name: '成都文创科技有限公司',
tax_number: '91510100MA7XYZ1234',
register_address: '四川省成都市高新区天府三街666号',
register_phone: '028-66666666',
bank_name: '招商银行成都分行',
bank_account: '6225 6666 8888 0000',
email: 'finance@scwenchuang.com',
},
{
company_name: '天府文化发展有限公司',
tax_number: '91510100678912345K',
register_address: '四川省成都市武侯区科华北路88号',
register_phone: '028-12345678',
bank_name: '中国工商银行成都分行',
bank_account: '6212 8888 0000 9999',
email: 'invoice@tfculture.com',
},
]
const defaultOperationLogs = [
{
time: '2025-10-31 18:30:30',
operator: 'admin',
records: ['剩余估值次数0 -> 1', '类型:付费估值', '备注:新用户'],
},
{
time: '2025-10-31 18:30:30',
operator: 'admin',
records: ['剩余估值次数2 -> 1', '类型:付费估值', '备注:退款'],
},
{
time: '2025-10-31 18:30:30',
operator: 'admin',
records: ['用户备注111111111111 -> 22222222222222222222'],
},
]
export default { export default {
login: (data) => request.post('/base/access_token', data, { noNeedToken: true }), login: (data) => request.post('/base/access_token', data, { noNeedToken: true }),
getUserInfo: () => request.get('/base/userinfo'), getUserInfo: () => request.get('/base/userinfo'),
@ -430,73 +316,10 @@ export default {
// auditlog // auditlog
getAuditLogList: (params = {}) => request.get('/auditlog/list', { params }), getAuditLogList: (params = {}) => request.get('/auditlog/list', { params }),
// app users (客户端用户管理) - 使用现有的后端接口 // app users (客户端用户管理) - 使用现有的后端接口
getAppUserList: (params = {}) => { getAppUserList: (params = {}) => request.get('/app-user-admin/list', { params }),
// 模拟分页和搜索 updateAppUserQuota: (data = {}) => request.post('/app-user-admin/quota', data),
let filteredUsers = [...mockAppUsers] getAppUserQuotaLogs: ({ user_id, ...params } = {}) =>
request.get(`/app-user-admin/${user_id}/quota-logs`, { params }),
// 手机号搜索
if (params.phone) {
filteredUsers = filteredUsers.filter(user =>
user.phone.includes(params.phone)
)
}
// 微信号搜索
if (params.wechat) {
filteredUsers = filteredUsers.filter(user =>
user.wechat && user.wechat.includes(params.wechat)
)
}
// 注册时间筛选(日期范围)
if (params.created_at && Array.isArray(params.created_at) && params.created_at.length === 2) {
const [startTime, endTime] = params.created_at
filteredUsers = filteredUsers.filter(user => {
if (!user.created_at) return false
const userTime = new Date(user.created_at).getTime()
return userTime >= startTime && userTime <= endTime
})
}
// 分页处理
const page = Number(params.page) || 1
const pageSize = Number(params.page_size) || 10
const startIndex = (page - 1) * pageSize
const endIndex = startIndex + pageSize
const paginatedUsers = filteredUsers.slice(startIndex, endIndex)
// 返回 Promise 模拟异步请求
return new Promise((resolve) => {
setTimeout(() => {
resolve({
data: paginatedUsers,
total: filteredUsers.length,
page,
page_size: pageSize,
})
}, 300)
})
},
getAppUserById: (params = {}) =>
new Promise((resolve) => {
const id = Number(params.id)
const user = mockAppUsers.find((item) => item.id === id) || {}
setTimeout(() => {
resolve({
baseInfo: {
id: user.id,
phone: user.phone,
wechat: user.wechat,
register_time: user.created_at,
notes: user.notes,
remaining_count: user.remaining_count,
user_type: user.user_type || '体验用户',
},
invoiceHeaders: defaultInvoiceHeaders,
operationLogs: defaultOperationLogs,
})
}, 300)
}),
createAppUser: (data = {}) => request.post('/app-user/register', data), createAppUser: (data = {}) => request.post('/app-user/register', data),
updateAppUser: (data = {}) => request.post('/app-user/update', data), updateAppUser: (data = {}) => request.post('/app-user/update', data),
deleteAppUser: (params = {}) => request.delete('/app-user/delete', { params }), deleteAppUser: (params = {}) => request.delete('/app-user/delete', { params }),

View File

@ -1,12 +1,6 @@
<script setup> <script setup>
import { ref, watch } from 'vue' import { computed, ref, watch } from 'vue'
import { import { NModal, NButton, NSelect, NInput, NDivider, NInputNumber } from 'naive-ui'
NModal,
NButton,
NSelect,
NInput,
NDivider,
} from 'naive-ui'
// Props // Props
const props = defineProps({ const props = defineProps({
@ -25,44 +19,37 @@ const emit = defineEmits(['update:visible', 'save'])
// //
const limitForm = ref({ const limitForm = ref({
remainingCount: 0, targetCount: 0,
type: '免费体验', quotaType: '免费体验',
experienceCount: 1, remark: ''
notes: ''
}) })
// //
const typeOptions = [ const typeOptions = [
{ label: '免费体验', value: '免费体验' }, { label: '免费体验', value: '免费体验' },
{ label: '付费用户', value: '付费用户' }, { label: '付费评估', value: '付费评估' }
{ label: 'VIP用户', value: 'VIP用户' }
] ]
const currentRemaining = computed(() => props.userData?.remaining_count ?? 0)
// //
watch(() => props.userData, (newData) => { watch(() => props.userData, (newData) => {
if (newData && Object.keys(newData).length > 0) { if (newData && Object.keys(newData).length > 0) {
limitForm.value = { limitForm.value = {
remainingCount: newData.remaining_count || 0, targetCount: newData.remaining_count || 0,
type: newData.user_type || '免费体验', quotaType: newData.user_type || '免费体验',
experienceCount: newData.experience_count || 1, remark: ''
notes: newData.notes || ''
} }
} }
}, { immediate: true }) }, { immediate: true })
//
function handleExperienceCountChange(delta) {
const newCount = limitForm.value.experienceCount + delta
if (newCount >= 0) {
limitForm.value.experienceCount = newCount
}
}
// //
function handleSave() { function handleSave() {
const data = { const data = {
user_id: props.userData.id, user_id: props.userData.id,
...limitForm.value target_count: Number(limitForm.value.targetCount || 0),
op_type: limitForm.value.quotaType,
remark: limitForm.value.remark
} }
emit('save', data) emit('save', data)
} }
@ -72,10 +59,9 @@ function handleCancel() {
emit('update:visible', false) emit('update:visible', false)
// //
limitForm.value = { limitForm.value = {
remainingCount: 0, targetCount: 0,
type: '免费体验', quotaType: '免费体验',
experienceCount: 1, remark: ''
notes: ''
} }
} }
</script> </script>
@ -96,7 +82,7 @@ function handleCancel() {
<!-- 剩余估值次数 --> <!-- 剩余估值次数 -->
<div class="form-row"> <div class="form-row">
<span class="label">剩余估值次数</span> <span class="label">剩余估值次数</span>
<span class="value">{{ limitForm.remainingCount }}</span> <span class="value">{{ currentRemaining }}</span>
</div> </div>
<NDivider style="margin: 16px 0;" /> <NDivider style="margin: 16px 0;" />
@ -105,31 +91,20 @@ function handleCancel() {
<div class="form-row"> <div class="form-row">
<span class="label">类型</span> <span class="label">类型</span>
<NSelect <NSelect
v-model:value="limitForm.type" v-model:value="limitForm.quotaType"
:options="typeOptions" :options="typeOptions"
style="width: 120px;" style="width: 120px;"
/> />
</div> </div>
<!-- 体验次数 --> <!-- 目标次数 -->
<div class="form-row"> <div class="form-row">
<span class="label">体验次数</span> <span class="label">估值次数</span>
<div class="count-control"> <NInputNumber
<NButton v-model:value="limitForm.targetCount"
size="small" :min="0"
@click="handleExperienceCountChange(-1)" style="width: 160px;"
:disabled="limitForm.experienceCount <= 0" />
>
-
</NButton>
<span class="count-value">{{ limitForm.experienceCount }}</span>
<NButton
size="small"
@click="handleExperienceCountChange(1)"
>
+
</NButton>
</div>
</div> </div>
<!-- 备注 --> <!-- 备注 -->
@ -137,7 +112,7 @@ function handleCancel() {
<span class="label">备注</span> <span class="label">备注</span>
</div> </div>
<NInput <NInput
v-model:value="limitForm.notes" v-model:value="limitForm.remark"
type="textarea" type="textarea"
placeholder="请输入备注信息" placeholder="请输入备注信息"
:rows="4" :rows="4"
@ -184,19 +159,6 @@ function handleCancel() {
color: #666; 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 { .action-buttons {
display: flex; display: flex;
justify-content: flex-end; justify-content: flex-end;

View File

@ -45,17 +45,14 @@ const invoiceColumns = [
] ]
const logColumns = [ const logColumns = [
{ title: '操作时间', key: 'time', width: 160 }, { title: 'ID', key: 'id', width: 80 },
{ title: '操作人', key: 'operator', width: 100 }, { title: '操作人', key: 'operator_name', width: 120 },
{ title: '操作类型', key: 'op_type', width: 120, ellipsis: { tooltip: true } },
{ {
title: '操作记录', title: '备注',
key: 'records', key: 'remark',
render: (row) => ellipsis: { tooltip: true },
h( render: (row) => row.remark || '-',
'div',
{ class: 'log-record' },
row.records?.map((item, idx) => h('div', { key: idx }, item))
),
}, },
] ]
@ -209,10 +206,6 @@ function handleClose() {
padding: 24px 0; padding: 24px 0;
} }
.log-record div + div {
margin-top: 4px;
}
.action-buttons { .action-buttons {
display: flex; display: flex;
justify-content: center; justify-content: center;

View File

@ -1,16 +1,6 @@
<script setup> <script setup>
import { h, onMounted, ref, resolveDirective, withDirectives } from 'vue' import { h, onMounted, ref, resolveDirective, withDirectives } from 'vue'
import { import { NButton, NForm, NFormItem, NInput, NDatePicker } from 'naive-ui'
NButton,
NForm,
NFormItem,
NInput,
NSpace,
NSwitch,
NTag,
NPopconfirm,
NDatePicker,
} 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'
@ -142,7 +132,7 @@ const columns = [
icon: renderIcon('material-symbols:info', { size: 16 }), icon: renderIcon('material-symbols:info', { size: 16 }),
} }
), ),
[[vPermission, 'get/api/v1/app_user/detail']] [[vPermission, 'get/api/v1/app-user-admin/list']]
), ),
withDirectives( withDirectives(
h( h(
@ -158,7 +148,7 @@ const columns = [
icon: renderIcon('material-symbols:settings', { size: 16 }), icon: renderIcon('material-symbols:settings', { size: 16 }),
} }
), ),
[[vPermission, 'post/api/v1/app_user/set_limit']] [[vPermission, 'post/api/v1/app-user-admin/quota']]
), ),
] ]
}, },
@ -170,23 +160,26 @@ async function handleViewDetail(row) {
detailModalVisible.value = true detailModalVisible.value = true
detailLoading.value = true detailLoading.value = true
try { try {
const detail = await api.getAppUserById({ id: row.id }) const { data: logs = [] } = await api.getAppUserQuotaLogs({
const baseInfoFromServer = detail?.baseInfo || {} user_id: row.id,
page: 1,
page_size: 50,
})
userDetail.value = { userDetail.value = {
baseInfo: { baseInfo: {
...baseInfoFromServer,
id: row.id, id: row.id,
phone: row.phone, phone: row.phone,
wechat: row.wechat, wechat: row.wechat,
register_time: row.created_at ? formatDate(row.created_at) : '-', register_time: row.created_at ? formatDate(row.created_at) : '-',
notes: row.notes, notes: row.notes,
remaining_count: row.remaining_count, remaining_count: row.remaining_count,
user_type: row.user_type || '-',
}, },
invoiceHeaders: detail?.invoiceHeaders || [], invoiceHeaders: [],
operationLogs: detail?.operationLogs || [], operationLogs: logs,
} }
} catch (error) { } catch (error) {
$message.error('获取用户详情失败') $message.error(error?.message || '获取用户详情失败')
} finally { } finally {
detailLoading.value = false detailLoading.value = false
} }
@ -201,13 +194,17 @@ function handleSetLimit(row) {
// //
async function handleSaveLimitSetting(data) { async function handleSaveLimitSetting(data) {
try { try {
// API await api.updateAppUserQuota({
// await api.setUserLimit(data) user_id: data.user_id,
target_count: data.target_count,
op_type: data.op_type,
remark: data.remark,
})
$message.success('次数设置保存成功') $message.success('次数设置保存成功')
limitModalVisible.value = false limitModalVisible.value = false
$table.value?.handleSearch() $table.value?.handleSearch()
} catch (error) { } catch (error) {
$message.error('保存失败: ' + error.message) $message.error(error?.message || '保存失败')
} }
} }

View File

@ -5,4 +5,5 @@ VITE_PUBLIC_PATH = '/'
VITE_USE_PROXY = true VITE_USE_PROXY = true
# base api # base api
VITE_BASE_API = 'https://value.cdcee.net/api/v1' VITE_BASE_API = 'http://139.224.70.152:9990/api/v1'
# VITE_BASE_API = 'https://value.cdcee.net/api/v1'