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
# 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,
}))
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 {
login: (data) => request.post('/base/access_token', data, { noNeedToken: true }),
getUserInfo: () => request.get('/base/userinfo'),
@ -430,73 +316,10 @@ export default {
// auditlog
getAuditLogList: (params = {}) => request.get('/auditlog/list', { params }),
// app users (客户端用户管理) - 使用现有的后端接口
getAppUserList: (params = {}) => {
// 模拟分页和搜索
let filteredUsers = [...mockAppUsers]
// 手机号搜索
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)
}),
getAppUserList: (params = {}) => request.get('/app-user-admin/list', { params }),
updateAppUserQuota: (data = {}) => request.post('/app-user-admin/quota', data),
getAppUserQuotaLogs: ({ user_id, ...params } = {}) =>
request.get(`/app-user-admin/${user_id}/quota-logs`, { params }),
createAppUser: (data = {}) => request.post('/app-user/register', data),
updateAppUser: (data = {}) => request.post('/app-user/update', data),
deleteAppUser: (params = {}) => request.delete('/app-user/delete', { params }),

View File

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

View File

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

View File

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

View File

@ -5,4 +5,5 @@ VITE_PUBLIC_PATH = '/'
VITE_USE_PROXY = true
# 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'