This commit is contained in:
2025-12-08 15:18:37 +08:00
parent 5ea4f2cfe3
commit a09631551b
15 changed files with 1172 additions and 1502 deletions
BIN
View File
Binary file not shown.
+191
View File
@@ -0,0 +1,191 @@
<template>
<el-dialog
:model-value="visible"
title="选择用户"
width="800px"
class="user-selector-dialog"
append-to-body
@update:model-value="handleVisibleChange"
>
<!-- 搜索栏 -->
<div class="selector-search">
<el-input
v-model="searchParams.key"
placeholder="搜索用户名或ID"
clearable
@keyup.enter="handleSearch"
style="width: 300px; margin-right: 12px"
>
<template #prefix>
<el-icon><Search /></el-icon>
</template>
</el-input>
<el-button type="primary" @click="handleSearch">
<el-icon><Search /></el-icon>
搜索
</el-button>
<el-button @click="handleReset">重置</el-button>
</div>
<!-- 用户表格 -->
<el-table
v-loading="loading"
:data="userList"
highlight-current-row
@current-change="handleCurrentChange"
style="width: 100%; margin-top: 16px"
:height="400"
>
<el-table-column type="index" label="序号" width="60" />
<el-table-column prop="UserId" label="用户ID" width="100" />
<el-table-column prop="UserName" label="用户名" min-width="150" />
<el-table-column prop="Email" label="邮箱" min-width="180" />
</el-table>
<!-- 分页 -->
<el-pagination
v-model:current-page="searchParams.page"
v-model:page-size="searchParams.count"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next, jumper"
:total="total"
@size-change="handleSizeChange"
@current-change="handlePageChange"
background
class="selector-pagination"
/>
<template #footer>
<div class="dialog-footer">
<el-button @click="closeDialog">取消</el-button>
<el-button type="primary" @click="confirmSelection" :disabled="!selectedUser">
确定选择
</el-button>
</div>
</template>
</el-dialog>
</template>
<script setup>
import { ref, reactive, watch } from 'vue'
import { Search } from '@element-plus/icons-vue'
import { getUserList } from '@/api/admin/user'
import { ElMessage } from 'element-plus'
const props = defineProps({
visible: {
type: Boolean,
default: false
}
})
const emit = defineEmits(['update:visible', 'select'])
const loading = ref(false)
const userList = ref([])
const total = ref(0)
const selectedUser = ref(null)
const searchParams = reactive({
key: '',
page: 1,
count: 10
})
// 监听 visible 变化,打开时加载数据
watch(() => props.visible, (newVal) => {
if (newVal) {
selectedUser.value = null
if (userList.value.length === 0) {
fetchUserList()
}
}
})
const handleVisibleChange = (val) => {
emit('update:visible', val)
}
const closeDialog = () => {
emit('update:visible', false)
}
const fetchUserList = async () => {
loading.value = true
try {
const res = await getUserList(searchParams)
if (res.data.code === 200) {
userList.value = res.data.data?.data || []
total.value = res.data.data?.all_count || 0
}
} catch (error) {
console.error('获取用户列表失败:', error)
ElMessage.error('获取用户列表失败')
} finally {
loading.value = false
}
}
const handleSearch = () => {
searchParams.page = 1
fetchUserList()
}
const handleReset = () => {
searchParams.key = ''
searchParams.page = 1
fetchUserList()
}
const handleCurrentChange = (row) => {
selectedUser.value = row
}
const handleSizeChange = (size) => {
searchParams.count = size
fetchUserList()
}
const handlePageChange = (page) => {
searchParams.page = page
fetchUserList()
}
const confirmSelection = () => {
if (!selectedUser.value) {
ElMessage.warning('请选择一个用户')
return
}
emit('select', selectedUser.value)
closeDialog()
}
</script>
<style scoped>
.selector-search {
display: flex;
align-items: center;
padding: 12px 0;
border-bottom: 1px solid #ebeef5;
}
.selector-pagination {
margin-top: 16px;
justify-content: flex-end;
}
:deep(.el-table__row) {
cursor: pointer;
}
:deep(.el-table__row):hover {
background-color: #f5f7fa;
}
:deep(.current-row) {
background-color: var(--el-color-primary-light-8) !important;
color: var(--el-color-primary);
font-weight: bold;
}
</style>
+29 -61
View File
@@ -5,20 +5,20 @@ export const menus = [
icon: 'DataBoard'
},
{
path : '/ticket',
path: '/ticket',
title: '工单处理',
icon: 'DataBoard'
},
{
path:'/user',
path: '/user',
title: '用户管理',
icon: 'User',
children: [
{
{
path: '/user/list',
title: '用户列表'
},
{
{
path: '/user/balance',
title: '用户余额管理'
},
@@ -45,10 +45,7 @@ export const menus = [
path: '/product/group',
title: '商品分组'
},
{
path: '/product/parameter',
title: '商品参数'
}
]
},
{
@@ -75,36 +72,7 @@ export const menus = [
path: '/marketing/voucher',
title: '代金券管理'
},
{
path: '/marketing/user-distribution',
title: '用户分发管理'
},
{
id: 'discount-goods',
title: '商品关联管理',
path: '/marketing/discount-goods',
badge: 'NEW'
},
{
id: 'discount-users',
title: '用户关联管理',
path: '/marketing/discount-users',
badge: 'NEW'
},
{
id: 'user-info',
title: '用户信息管理',
path: '/marketing/user-info',
badge: 'NEW'
},
{
id: 'user-history',
title: '用户使用记录管理',
path: '/marketing/user-history',
badge: 'NEW'
}
]
},
{
@@ -141,9 +109,9 @@ export const menus = [
{ path: '/acs/images/categories', title: '镜像分类' }
]
},
{
path: '/acs/nodes',
title: '节点管理'
{
path: '/acs/nodes',
title: '节点管理'
},
{
path: '/acs/guacamole',
@@ -158,10 +126,10 @@ export const menus = [
]
},
{
path:'/setting',
title:'全局设置管理',
children:[
{path:'/setting/global',title:'全局设置'}
path: '/setting',
title: '全局设置管理',
children: [
{ path: '/setting/global', title: '全局设置' }
]
}
]
@@ -171,31 +139,31 @@ export const menus = [
title: '系统管理',
icon: 'Setting',
children: [
{
path: '/system/permission',
{
path: '/system/permission',
title: '权限管理',
children: [
{ path: '/system/permission/route', title: '路由权限' },
{ path: '/system/permission/admin', title: '管理员权限' }
]
},
{
path: '/system/file',
title: '文件管理'
{
path: '/system/file',
title: '文件管理'
},
{
path: '/system/domain-whitelist',
title: '域名白名单'
{
path: '/system/domain-whitelist',
title: '域名白名单'
},
{
path: '/system/setting-group',
title: '配置组管理'
{
path: '/system/setting-group',
title: '配置组管理'
},
{
path: '/system/setting-list',
title: '配置管理'
{
path: '/system/setting-list',
title: '配置管理'
}
]
}
+8 -48
View File
@@ -230,14 +230,7 @@ const routes = [
title: '商品分组'
}
},
{
path: 'parameter',
name: 'ProductParameter',
component: () => import('../views/product/ProductParameter.vue'),
meta: {
title: '商品参数'
}
}
]
},
// 订单管理路由
@@ -287,49 +280,16 @@ const routes = [
}
},
{
path: 'user-distribution',
name: 'UserDistribution',
component: () => import('../views/marketing/UserVoucher.vue'),
path: 'voucher/:id/manage',
name: 'VoucherManagement',
component: () => import('../views/marketing/VoucherManagement.vue'),
meta: {
title: '用户分发管理'
title: '代金券详情管理',
hidden: true,
activeMenu: '/marketing/voucher'
}
},
{
path: 'discount-goods',
name: 'DiscountGoods',
component: () => import('../views/marketing/DiscountGoods.vue'),
meta: {
title: '商品关联管理',
badge: 'NEW'
}
},
{
path: 'discount-users',
name: 'DiscountUsers',
component: () => import('../views/marketing/DiscountUsers.vue'),
meta: {
title: '用户关联管理',
badge: 'NEW'
}
},
{
path: 'user-info',
name: 'UserInfo',
component: () => import('../views/marketing/VoucherHolders.vue'),
meta: {
title: '用户信息管理',
badge: 'NEW'
}
},
{
path: 'user-history',
name: 'UserHistory',
component: () => import('../views/marketing/VoucherHistory.vue'),
meta: {
title: '用户使用记录管理',
badge: 'NEW'
}
}
]
},
// 活动管理路由
+20 -6
View File
@@ -5,8 +5,8 @@
<!-- 搜索和操作栏 -->
<div class="filter-section">
<div class="filter-content">
<el-form :inline="true" :model="queryParams" class="search-form">
<el-form-item label="代金卷">
<el-form :inline="true" :model="queryParams" class="search-form" v-if="!codeId">
<el-form-item label="代金卷" v-if="!codeId">
<el-select
v-model="queryParams.code_id"
placeholder="请选择代金券"
@@ -71,7 +71,7 @@
>
<el-table-column type="selection" width="55" />
<el-table-column prop="id" label="ID" width="80" />
<el-table-column prop="discountId" label="代金券ID" width="120" />
<el-table-column prop="discountId" label="代金券ID" width="120" v-if="!codeId" />
<el-table-column label="关联对象ID" width="120">
<template #default="{ row }">
{{ row.goodId || row.goodGroupId || '-' }}
@@ -149,7 +149,7 @@
placeholder="请选择代金券"
filterable
clearable
:disabled="dialogType === 'edit'"
:disabled="dialogType === 'edit' || !!codeId"
style="width: 100%"
>
<el-option
@@ -234,7 +234,7 @@
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { ref, reactive, onMounted, watch } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Delete, Search, Plus, Refresh } from '@element-plus/icons-vue'
import {
@@ -249,13 +249,27 @@ import {
getProductGroupList
} from '@/api/admin/product'
const props = defineProps({
codeId: {
type: [String, Number],
default: ''
}
})
// 查询参数
const queryParams = reactive({
code_id: '',
code_id: props.codeId || '',
page: 1,
count: 10
})
watch(() => props.codeId, (newVal) => {
if (newVal) {
queryParams.code_id = newVal
fetchGoodsList()
}
})
// 表单数据
const form = reactive({
id: undefined,
+33 -143
View File
@@ -5,8 +5,8 @@
<!-- 搜索和操作栏 -->
<div class="filter-section">
<div class="filter-content">
<el-form :inline="true" :model="queryParams" class="search-form">
<el-form-item label="代金卷">
<el-form :inline="true" :model="queryParams" class="search-form" v-if="!codeId">
<el-form-item label="代金卷" v-if="!codeId">
<el-select
v-model="queryParams.code_id"
placeholder="请选择代金券"
@@ -68,8 +68,8 @@
>
<el-table-column type="selection" width="55" />
<el-table-column prop="id" label="ID" width="80" />
<el-table-column prop="discountId" label="代金券ID" width="120" />
<el-table-column label="关联对象ID" width="130">
<el-table-column prop="discountId" label="代金券ID" width="120" v-if="!codeId" />
<el-table-column label="关联对象ID" min-width="150">
<template #default="{ row }">
{{ row.userId || row.userGroupId || '-' }}
</template>
@@ -224,82 +224,16 @@
</el-dialog>
<!-- 用户选择弹窗 -->
<el-dialog
v-model="userSelectorVisible"
title="选择用户"
width="800px"
class="user-selector-dialog"
append-to-body
>
<!-- 搜索栏 -->
<div class="selector-search">
<el-input
v-model="userSearchParams.key"
placeholder="搜索用户名或ID"
clearable
@keyup.enter="searchUsers"
style="width: 300px; margin-right: 12px"
>
<template #prefix>
<el-icon><Search /></el-icon>
</template>
</el-input>
<el-button type="primary" @click="searchUsers">
<el-icon><Search /></el-icon>
搜索
</el-button>
<el-button @click="resetUserSearch">重置</el-button>
</div>
<!-- 用户表格 -->
<el-table
v-loading="userSelectorLoading"
:data="userSelectorList"
highlight-current-row
@current-change="handleUserSelectChange"
style="width: 100%; margin-top: 16px"
:height="400"
>
<el-table-column type="index" label="序号" width="60" />
<el-table-column prop="UserId" label="用户ID" width="100" />
<el-table-column prop="UserName" label="用户名" min-width="150" />
<el-table-column prop="Email" label="邮箱" min-width="180" />
<el-table-column label="状态" width="100">
<template #default="{ row }">
<el-tag :type="row.Status === 1 ? 'success' : 'danger'" size="small">
{{ row.Status === 1 ? '正常' : '禁用' }}
</el-tag>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<el-pagination
v-model:current-page="userSearchParams.page"
v-model:page-size="userSearchParams.count"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next, jumper"
:total="userSelectorTotal"
@size-change="handleUserSelectorSizeChange"
@current-change="handleUserSelectorPageChange"
background
class="selector-pagination"
/>
<template #footer>
<div class="dialog-footer">
<el-button @click="userSelectorVisible = false">取消</el-button>
<el-button type="primary" @click="confirmUserSelection" :disabled="!selectedUserTemp">
确定选择
</el-button>
</div>
</template>
</el-dialog>
<UserSelector
v-model:visible="userSelectorVisible"
@select="confirmUserSelection"
/>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { ref, reactive, onMounted, watch } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Delete, Search, Plus, Refresh, User } from '@element-plus/icons-vue'
import {
@@ -313,14 +247,29 @@ import {
getUserList,
getUserGroupList
} from '@/api/admin/user'
import UserSelector from '@/components/UserSelector/index.vue'
const props = defineProps({
codeId: {
type: [String, Number],
default: ''
}
})
// 查询参数
const queryParams = reactive({
code_id: '',
code_id: props.codeId || '',
page: 1,
count: 10
})
watch(() => props.codeId, (newVal) => {
if (newVal) {
queryParams.code_id = newVal
fetchUsersList()
}
})
// 表单数据
const form = reactive({
id: undefined,
@@ -364,15 +313,6 @@ const userGroupOptions = ref([]) // 用户组列表选项
// 用户选择弹窗相关
const userSelectorVisible = ref(false)
const userSelectorLoading = ref(false)
const userSelectorList = ref([])
const userSelectorTotal = ref(0)
const selectedUserTemp = ref(null) // 临时存储选中的用户
const userSearchParams = reactive({
key: '',
page: 1,
count: 10
})
// 格式化日期
const formatDate = (dateStr) => {
@@ -469,70 +409,19 @@ const fetchUserGroupList = async () => {
// 打开用户选择器
const openUserSelector = () => {
userSelectorVisible.value = true
selectedUserTemp.value = null
userSearchParams.key = ''
userSearchParams.page = 1
fetchUserSelectorList()
}
// 获取用户选择器列表
const fetchUserSelectorList = async () => {
userSelectorLoading.value = true
try {
const res = await getUserList(userSearchParams)
console.log('用户选择器列表:', res.data)
if (res.data.code === 200) {
userSelectorList.value = res.data.data?.data || []
userSelectorTotal.value = res.data.data?.all_count || 0
}
} catch (error) {
console.error('获取用户列表失败:', error)
ElMessage.error('获取用户列表失败')
} finally {
userSelectorLoading.value = false
}
}
// 搜索用户
const searchUsers = () => {
userSearchParams.page = 1
fetchUserSelectorList()
}
// 重置用户搜索
const resetUserSearch = () => {
userSearchParams.key = ''
userSearchParams.page = 1
fetchUserSelectorList()
}
// 用户选择变化
const handleUserSelectChange = (row) => {
selectedUserTemp.value = row
}
// 用户选择器分页
const handleUserSelectorSizeChange = (size) => {
userSearchParams.count = size
fetchUserSelectorList()
}
const handleUserSelectorPageChange = (page) => {
userSearchParams.page = page
fetchUserSelectorList()
}
// 确认用户选择
const confirmUserSelection = () => {
if (!selectedUserTemp.value) {
const confirmUserSelection = (user) => {
if (!user) {
ElMessage.warning('请选择一个用户')
return
}
form.selected_user = selectedUserTemp.value.UserId
form.user_id = selectedUserTemp.value.UserId
form.selected_user = user.UserId
form.user_id = user.UserId
// 将选中的用户添加到 userOptions 中(如果不存在)
if (!userOptions.value.find(u => u.UserId === selectedUserTemp.value.UserId)) {
userOptions.value.push(selectedUserTemp.value)
if (!userOptions.value.find(u => u.UserId === user.UserId)) {
userOptions.value.push(user)
}
userSelectorVisible.value = false
ElMessage.success('用户选择成功')
@@ -691,6 +580,7 @@ const handleEdit = (row) => {
})
//点击编辑需要初始化加载用户列表
fetchUserList()
fetchUserGroupList()
}
// 删除用户关联
+85 -33
View File
@@ -2,8 +2,8 @@
<div class="user-voucher-container">
<!-- 搜索和操作栏 -->
<el-card class="filter-container" shadow="never">
<el-form :inline="true" :model="queryParams" class="search-form">
<el-form-item label="代金券">
<el-form :inline="true" :model="queryParams" class="search-form" v-if="!codeId">
<el-form-item label="代金券" v-if="!codeId">
<el-select
v-model="queryParams.code_id"
placeholder="请选择代金券"
@@ -54,37 +54,37 @@
{{ row.Id || row.id }}
</template>
</el-table-column>
<el-table-column label="用户ID" width="100">
<el-table-column label="用户ID" min-width="120">
<template #default="{ row }">
{{ row.UserId || row.userId }}
</template>
</el-table-column>
<el-table-column label="代金券ID" width="100">
<el-table-column label="代金券ID" width="100" v-if="!codeId">
<template #default="{ row }">
{{ row.discountId }}
</template>
</el-table-column>
<el-table-column label="代金券名称" min-width="150">
<el-table-column label="代金券名称" min-width="150" v-if="!codeId">
<template #default="{ row }">
{{ row.discount?.name || '-' }}
</template>
</el-table-column>
<el-table-column label="面额" width="120">
<el-table-column label="面额" min-width="120">
<template #default="{ row }">
<span class="amount">¥{{ row.discount?.amount ? (row.discount.amount / 100).toFixed(2) : '0.00' }}</span>
</template>
</el-table-column>
<el-table-column label="已使用/最大次数" width="150">
<el-table-column label="已使用/最大次数" min-width="150">
<template #default="{ row }">
<el-tag type="info">{{ row.useTimes || 0 }} / {{ row.maxUseTimes || row.discount?.maxTimes || 0 }}</el-tag>
</template>
</el-table-column>
<el-table-column label="过期时间" width="180">
<el-table-column label="过期时间" min-width="180">
<template #default="{ row }">
{{ formatDate(row.expireAt) }}
</template>
</el-table-column>
<el-table-column label="创建时间" width="180">
<el-table-column label="创建时间" min-width="180">
<template #default="{ row }">
{{ formatDate(row.CreatedAt) }}
</template>
@@ -134,7 +134,7 @@
<el-select
v-model="addForm.voucher_id"
placeholder="请选择代金券"
:disabled="addForm.discount_type === 'code'"
:disabled="addForm.discount_type === 'code' || !!codeId"
filterable
clearable
style="width: 100%"
@@ -178,25 +178,22 @@
</el-form-item>
<el-form-item label="用户" prop="user_id">
<el-select
v-model="addForm.user_id"
placeholder="请选择用户"
:disabled="addForm.target_type === 'group'"
filterable
clearable
remote
:remote-method="searchUsers"
:loading="userSearchLoading"
style="width: 100%"
@change="handleUserChange"
>
<el-option
v-for="item in userOptions"
:key="item.UserId"
:label="`${item.UserName} (ID: ${item.UserId})`"
:value="item.UserId"
/>
</el-select>
<div class="user-selector-wrapper">
<div class="selected-user-display" v-if="addForm.user_id">
<el-tag type="primary" closable @close="clearSelectedUser">
{{ getSelectedUserName() }}
</el-tag>
</div>
<el-button
type="primary"
plain
@click="openUserSelector"
style="width: 100%"
>
<el-icon><User /></el-icon>
{{ addForm.user_id ? '重新选择用户' : '选择用户' }}
</el-button>
</div>
</el-form-item>
<el-form-item label="用户组" prop="group_id">
@@ -268,13 +265,19 @@
<el-button type="primary" @click="submitEdit">确定</el-button>
</template>
</el-dialog>
<!-- 用户选择弹窗 -->
<UserSelector
v-model:visible="userSelectorVisible"
@select="confirmUserSelection"
/>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { ref, reactive, onMounted, watch } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Delete, Search, Plus, Refresh } from '@element-plus/icons-vue'
import { Delete, Search, Plus, Refresh, User } from '@element-plus/icons-vue'
import {
getUserVoucherList,
addUserVoucher,
@@ -285,14 +288,29 @@ import {
allocateVoucher
} from '@/api/admin/discount'
import { getUserList, getUserGroupList } from '@/api/admin/user'
import UserSelector from '@/components/UserSelector/index.vue'
const props = defineProps({
codeId: {
type: [String, Number],
default: ''
}
})
// 查询参数
const queryParams = reactive({
code_id: undefined,
code_id: props.codeId || undefined,
page: 1,
count: 10
})
watch(() => props.codeId, (newVal) => {
if (newVal) {
queryParams.code_id = newVal
fetchUserVoucherList()
}
})
// 添加表单
const addForm = reactive({
discount_type: 'coupon', // 优惠类型:coupon-代金券, code-优惠码
@@ -321,6 +339,7 @@ const groupOptions = ref([]) // 用户组选项
const userSearchLoading = ref(false) // 用户搜索加载状态
const submitLoading = ref(false) // 提交加载状态
const dataList = ref([]) // 优惠列表
const userSelectorVisible = ref(false)
// 编辑表单
const editForm = reactive({
@@ -623,6 +642,36 @@ const handleGroupChange = (val) => {
}
}
// 打开用户选择器
const openUserSelector = () => {
userSelectorVisible.value = true
}
// 确认用户选择
const confirmUserSelection = (user) => {
if (!user) {
ElMessage.warning('请选择一个用户')
return
}
addForm.user_id = user.UserId
// 将选中的用户添加到 userOptions 中(如果不存在)
if (!userOptions.value.find(u => u.UserId === user.UserId)) {
userOptions.value.push(user)
}
userSelectorVisible.value = false
}
// 清除选中的用户
const clearSelectedUser = () => {
addForm.user_id = undefined
}
// 获取选中用户的显示名称
const getSelectedUserName = () => {
const user = userOptions.value.find(u => u.UserId === addForm.user_id)
return user ? `${user.UserName} (ID: ${user.UserId})` : `用户ID: ${addForm.user_id}`
}
// 添加用户代金券
const handleAdd = async () => {
addDialogVisible.value = true
@@ -630,7 +679,7 @@ const handleAdd = async () => {
// 重置表单
Object.assign(addForm, {
discount_type: 'coupon',
voucher_id: undefined,
voucher_id: props.codeId || undefined,
code_id: undefined,
target_type: 'user',
user_id: undefined,
@@ -837,6 +886,9 @@ onMounted(() => {
// 加载代金券列表供选择
fetchVoucherListOptions()
fetchDiscountList()
if (queryParams.code_id) {
fetchUserVoucherList()
}
})
</script>
+11 -1
View File
@@ -63,9 +63,10 @@
<el-icon v-else color="#f56c6c" :size="20"><CircleCloseFilled /></el-icon>
</template>
</el-table-column>
<el-table-column label="操作" width="200" fixed="right">
<el-table-column label="操作" width="280" fixed="right">
<template #default="{ row }">
<el-button type="primary" link @click="handleEdit(row)">编辑</el-button>
<el-button type="primary" link @click="handleManage(row)">管理</el-button>
<el-button type="success" link @click="handleView(row)">查看</el-button>
<el-button type="danger" link @click="handleDelete(row)">删除</el-button>
</template>
@@ -166,8 +167,10 @@
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Plus, Delete, Refresh, SuccessFilled, CircleCloseFilled } from '@element-plus/icons-vue'
import {
@@ -180,6 +183,8 @@ import {
import { timeToTimestamp } from '@/utils/tool'
import DiscountDetailDialog from '@/components/marketing/DiscountDetailDialog.vue'
const router = useRouter()
// 查询参数
const queryParams = reactive({
discount_type: 'coupon', // 固定为coupon表示代金券
@@ -312,6 +317,11 @@ const handleEdit = (row) => {
})
}
// 管理代金券
const handleManage = (row) => {
router.push(`/marketing/voucher/${row.id}/manage`)
}
// 查看代金券详情
const handleView = async (row) => {
try {
+42 -144
View File
@@ -46,8 +46,8 @@
<el-table-column prop="user_id" label="用户ID" width="100" />
<el-table-column prop="username" label="用户名" width="150" />
<el-table-column prop="email" label="邮箱" min-width="200" />
<el-table-column prop="discount_id" label="代金券ID" width="120" />
<el-table-column prop="discount_name" label="代金券名称" min-width="180" />
<el-table-column prop="discount_id" label="代金券ID" width="120" v-if="!codeId" />
<el-table-column prop="discount_name" label="代金券名称" min-width="180" v-if="!codeId" />
<el-table-column label="优惠金额" width="120">
<template #default="{ row }">
<span class="amount">¥{{ row.discount_amount ? (row.discount_amount / 100).toFixed(2) : '0.00' }}</span>
@@ -133,74 +133,10 @@
</template>
</el-dialog>
<!-- 用户选择弹窗 -->
<el-dialog
v-model="userSelectorVisible"
title="选择用户"
width="800px"
class="user-selector-dialog"
>
<!-- 搜索栏 -->
<div class="selector-search">
<el-input
v-model="userSearchParams.key"
placeholder="搜索用户名或ID"
clearable
@keyup.enter="searchUsers"
style="width: 300px; margin-right: 12px"
>
<template #prefix>
<el-icon><Search /></el-icon>
</template>
</el-input>
<el-button type="primary" @click="searchUsers">
<el-icon><Search /></el-icon>
搜索
</el-button>
<el-button @click="resetUserSearch">重置</el-button>
</div>
<!-- 用户表格 -->
<el-table
v-loading="userSelectorLoading"
:data="userSelectorList"
highlight-current-row
@current-change="handleUserSelectChange"
style="width: 100%; margin-top: 16px"
:height="400"
>
<el-table-column type="index" label="序号" width="60" />
<el-table-column prop="UserId" label="用户ID" width="100" />
<el-table-column prop="UserName" label="用户名" min-width="150" />
<el-table-column prop="Email" label="邮箱" min-width="180" />
<el-table-column label="状态" width="100">
<template #default="{ row }">
<el-tag :type="row.Status === 1 ? 'success' : 'danger'" size="small">
{{ row.Status === 1 ? '正常' : '禁用' }}
</el-tag>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<el-pagination
v-model:current-page="userSearchParams.page"
v-model:page-size="userSearchParams.count"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next, jumper"
:total="userSelectorTotal"
@size-change="handleUserSelectorSizeChange"
@current-change="handleUserSelectorPageChange"
background
class="selector-pagination"
/>
<template #footer>
<el-button @click="userSelectorVisible = false">取消</el-button>
<el-button type="primary" @click="confirmUserSelection" :disabled="!selectedUserTemp">
确定选择
</el-button>
</template>
</el-dialog>
<UserSelector
v-model:visible="userSelectorVisible"
@select="confirmUserSelection"
/>
<!-- 统计卡片 -->
<el-row :gutter="20" style="margin-top: 20px">
@@ -237,20 +173,36 @@
</template>
<script setup>
import { ref, reactive, onMounted, computed } from 'vue'
import { ref, reactive, onMounted, computed, watch } from 'vue'
import { ElMessage } from 'element-plus'
import { Search, Refresh, Download } from '@element-plus/icons-vue'
import { getUserVoucherHistory, getDiscountCodeList } from '@/api/admin/discount'
import { getUserList } from '@/api/admin/user'
import UserSelector from '@/components/UserSelector/index.vue'
const props = defineProps({
codeId: {
type: [String, Number],
default: ''
}
})
// 查询参数
const queryParams = reactive({
user_id: undefined,
code_id: props.codeId || undefined,
id: '',
page: 1,
count: 10
})
watch(() => props.codeId, (newVal) => {
if (newVal) {
queryParams.code_id = newVal
fetchHistoryList()
}
})
// 状态数据
const loading = ref(false)
const historyList = ref([])
@@ -261,15 +213,6 @@ const currentDetail = ref({})
const discountOptions = ref([])
const selectorType = ref('query')
const userSelectorVisible = ref(false)
const userSelectorList = ref([])
const userSelectorTotal = ref(0)
const userSearchParams = reactive({
key: '',
page: 1,
count: 10
})
const selectedUserTemp = ref(null)
const userSelectorLoading = ref(false)
const UserOptions = ref([])
// 格式化日期
@@ -371,86 +314,41 @@ const resetUserSearch = () => {
// fetchUserSelectorList()
}
// 打开查询用户选择器
const openQueryUserSelector = () => {
selectorType.value = 'query'
userSelectorVisible.value = true
}
// 打开编辑用户选择器
const openEditUserSelector = () => {
selectorType.value = 'edit'
userSelectorVisible.value = true
}
// 确认用户选择
const confirmUserSelection = () => {
if (!selectedUserTemp.value) {
const confirmUserSelection = (user) => {
if (!user) {
ElMessage.warning('请选择一个用户')
return
}
if (selectorType.value === 'query') {
// 查询表单选择
queryParams.user_id = selectedUserTemp.value.UserId
queryParams.user_id = user.UserId
} else {
// 编辑表单选择
editForm.user_id = selectedUserTemp.value.UserId
editForm.user_id = user.UserId
}
// 将选中的用户添加到 UserOptions 中(如果不存在)
if (!UserOptions.value.find(u => u.UserId === selectedUserTemp.value.UserId)) {
UserOptions.value.push(selectedUserTemp.value)
if (!UserOptions.value.find(u => u.UserId === user.UserId)) {
UserOptions.value.push(user)
}
userSelectorVisible.value = false
ElMessage.success('用户选择成功')
}
// 打开查询用户选择器
const openQueryUserSelector = () => {
selectorType.value = 'query'
userSelectorVisible.value = true
selectedUserTemp.value = null
userSearchParams.key = ''
userSearchParams.page = 1
fetchUserSelectorList()
}
// 打开编辑用户选择器
const openEditUserSelector = () => {
selectorType.value = 'edit'
userSelectorVisible.value = true
selectedUserTemp.value = null
userSearchParams.key = ''
userSearchParams.page = 1
fetchUserSelectorList()
}
// 用户选择变化
const handleUserSelectChange = (row) => {
selectedUserTemp.value = row
}
// 搜索用户
const searchUsers = () => {
userSearchParams.page = 1
fetchUserSelectorList()
}
// 用户选择器分页
const handleUserSelectorSizeChange = (size) => {
userSearchParams.count = size
fetchUserSelectorList()
}
const handleUserSelectorPageChange = (page) => {
userSearchParams.page = page
fetchUserSelectorList()
}
// 获取用户选择器列表
const fetchUserSelectorList = async () => {
userSelectorLoading.value = true
try {
const res = await getUserList(userSearchParams)
console.log('用户选择器列表:', res.data)
if (res.data.code === 200) {
userSelectorList.value = res.data.data?.data || []
userSelectorTotal.value = res.data.data?.all_count || 0
}
} catch (error) {
console.error('获取用户列表失败:', error)
ElMessage.error('获取用户列表失败')
} finally {
userSelectorLoading.value = false
}
}
// 查询
const handleQuery = () => {
@@ -461,7 +359,7 @@ const handleQuery = () => {
// 重置查询
const resetQuery = () => {
queryParams.user_id = undefined
queryParams.discount_id = undefined
queryParams.code_id = undefined
queryParams.id = ''
queryParams.page = 1
fetchHistoryList()
+48 -167
View File
@@ -40,51 +40,51 @@
style="width: 100%"
>
<el-table-column prop="Id" label="ID" width="80" />
<el-table-column prop="UserId" label="用户ID" width="100" />
<el-table-column label="代金券ID" width="120">
<el-table-column prop="UserId" label="用户ID" min-width="100" />
<el-table-column label="代金券ID" min-width="110" v-if="!codeId">
<template #default="{ row }">
{{ row.discountId || '-' }}
</template>
</el-table-column>
<el-table-column label="代金券名称" min-width="180">
<el-table-column label="代金券名称" min-width="180" v-if="!codeId" show-overflow-tooltip>
<template #default="{ row }">
{{ row.discount?.name || '-' }}
</template>
</el-table-column>
<el-table-column label="代金券编码" width="150">
<el-table-column label="代金券编码" min-width="150" v-if="!codeId">
<template #default="{ row }">
{{ row.discount?.code || '-' }}
</template>
</el-table-column>
<el-table-column label="面额" width="120">
<el-table-column label="面额" min-width="110">
<template #default="{ row }">
<span class="amount">¥{{ row.discount?.amount ? (row.discount.amount / 100).toFixed(2) : '0.00' }}</span>
</template>
</el-table-column>
<el-table-column prop="useTimes" label="已使用次数" width="120" />
<el-table-column prop="maxUseTimes" label="最大使用次数" width="120" />
<el-table-column label="状态" width="100">
<el-table-column prop="useTimes" label="已使用" min-width="100" />
<el-table-column prop="maxUseTimes" label="最大使用" min-width="100" />
<el-table-column label="状态" min-width="100">
<template #default="{ row }">
<el-tag :type="getStatusType(row)">
<el-tag :type="getStatusType(row)" size="small">
{{ getStatusText(row) }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="过期时间" width="180">
<el-table-column label="过期时间" min-width="160">
<template #default="{ row }">
{{ formatDate(row.expireAt) }}
</template>
</el-table-column>
<el-table-column label="创建时间" width="180">
<el-table-column label="创建时间" min-width="160">
<template #default="{ row }">
{{ formatDate(row.CreatedAt) }}
</template>
</el-table-column>
<el-table-column label="操作" width="200" fixed="right">
<el-table-column label="操作" width="210" fixed="right">
<template #default="{ row }">
<el-button type="primary" link @click="handleView(row)">查看详情</el-button>
<el-button type="warning" link @click="handleEdit(row)">编辑</el-button>
<el-button type="danger" link @click="handleDelete(row)">删除</el-button>
<el-button type="primary" link size="small" @click="handleView(row)">查看</el-button>
<el-button type="warning" link size="small" @click="handleEdit(row)">编辑</el-button>
<el-button type="danger" link size="small" @click="handleDelete(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
@@ -203,81 +203,17 @@
</el-dialog>
<!-- 用户选择弹窗 -->
<el-dialog
v-model="userSelectorVisible"
title="选择用户"
width="800px"
class="user-selector-dialog"
>
<!-- 搜索栏 -->
<div class="selector-search">
<el-input
v-model="userSearchParams.key"
placeholder="搜索用户名或ID"
clearable
@keyup.enter="searchUsers"
style="width: 300px; margin-right: 12px"
>
<template #prefix>
<el-icon><Search /></el-icon>
</template>
</el-input>
<el-button type="primary" @click="searchUsers">
<el-icon><Search /></el-icon>
搜索
</el-button>
<el-button @click="resetUserSearch">重置</el-button>
</div>
<!-- 用户表格 -->
<el-table
v-loading="userSelectorLoading"
:data="userSelectorList"
highlight-current-row
@current-change="handleUserSelectChange"
style="width: 100%; margin-top: 16px"
:height="400"
>
<el-table-column type="index" label="序号" width="60" />
<el-table-column prop="UserId" label="用户ID" width="100" />
<el-table-column prop="UserName" label="用户名" min-width="150" />
<el-table-column prop="Email" label="邮箱" min-width="180" />
<el-table-column label="状态" width="100">
<template #default="{ row }">
<el-tag :type="row.Status === 1 ? 'success' : 'danger'" size="small">
{{ row.Status === 1 ? '正常' : '禁用' }}
</el-tag>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<el-pagination
v-model:current-page="userSearchParams.page"
v-model:page-size="userSearchParams.count"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next, jumper"
:total="userSelectorTotal"
@size-change="handleUserSelectorSizeChange"
@current-change="handleUserSelectorPageChange"
background
class="selector-pagination"
/>
<template #footer>
<el-button @click="userSelectorVisible = false">取消</el-button>
<el-button type="primary" @click="confirmUserSelection" :disabled="!selectedUserTemp">
确定选择
</el-button>
</template>
</el-dialog>
<UserSelector
v-model:visible="userSelectorVisible"
@select="confirmUserSelection"
/>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { ref, reactive, onMounted, watch } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Search, Refresh, Download, Plus, User } from '@element-plus/icons-vue'
import { Search, Refresh, Plus, User } from '@element-plus/icons-vue'
import {
getUserVoucherList,
allocateVoucher,
@@ -285,14 +221,34 @@ import {
deleteUserVoucher,
getDiscountCodeList
} from '@/api/admin/discount'
import { getUserList } from '@/api/admin/user'
import UserSelector from '@/components/UserSelector/index.vue'
const props = defineProps({
codeId: {
type: [String, Number],
default: ''
}
})
// 查询参数
const queryParams = reactive({
user_id: undefined,
code_id: props.codeId || undefined,
page: 1,
count: 10
})
watch(() => props.codeId, (newVal) => {
if (newVal) {
queryParams.code_id = newVal
// 如果有 code_id,尝试刷新列表(取决于 API 是否支持仅按 code_id 查询)
// 如果 API 必须要求 user_id,则这里可能不需要立即刷新,或者提示用户选择用户
if (queryParams.user_id) {
fetchHoldersList()
}
}
})
// 状态数据
const loading = ref(false)
const holdersList = ref([])
@@ -307,16 +263,7 @@ const discountOptions = ref([])
// 用户选择弹窗相关
const userSelectorVisible = ref(false)
const userSelectorLoading = ref(false)
const userSelectorList = ref([])
const userSelectorTotal = ref(0)
const selectedUserTemp = ref(null) // 临时存储选中的用户
const selectorType = ref('query') // 'query' 或 'edit' 用于区分是查询还是编辑
const userSearchParams = reactive({
key: '',
page: 1,
count: 10
})
// 编辑表单
const editForm = reactive({
@@ -448,101 +395,36 @@ const handleExport = () => {
ElMessage.info('导出功能开发中...')
}
// 获取用户列表
const fetchUserList = async () => {
const res = await getUserList({
page: 1,
count: 10000,
key: ''
})
UserOptions.value = res.data.data?.data || []
}
// 打开查询用户选择器
const openQueryUserSelector = () => {
selectorType.value = 'query'
userSelectorVisible.value = true
selectedUserTemp.value = null
userSearchParams.key = ''
userSearchParams.page = 1
fetchUserSelectorList()
}
// 打开编辑用户选择器
const openEditUserSelector = () => {
selectorType.value = 'edit'
userSelectorVisible.value = true
selectedUserTemp.value = null
userSearchParams.key = ''
userSearchParams.page = 1
fetchUserSelectorList()
}
// 获取用户选择器列表
const fetchUserSelectorList = async () => {
userSelectorLoading.value = true
try {
const res = await getUserList(userSearchParams)
console.log('用户选择器列表:', res.data)
if (res.data.code === 200) {
userSelectorList.value = res.data.data?.data || []
userSelectorTotal.value = res.data.data?.all_count || 0
}
} catch (error) {
console.error('获取用户列表失败:', error)
ElMessage.error('获取用户列表失败')
} finally {
userSelectorLoading.value = false
}
}
// 搜索用户
const searchUsers = () => {
userSearchParams.page = 1
fetchUserSelectorList()
}
// 重置用户搜索
const resetUserSearch = () => {
userSearchParams.key = ''
userSearchParams.page = 1
fetchUserSelectorList()
}
// 用户选择变化
const handleUserSelectChange = (row) => {
selectedUserTemp.value = row
}
// 用户选择器分页
const handleUserSelectorSizeChange = (size) => {
userSearchParams.count = size
fetchUserSelectorList()
}
const handleUserSelectorPageChange = (page) => {
userSearchParams.page = page
fetchUserSelectorList()
}
// 确认用户选择
const confirmUserSelection = () => {
if (!selectedUserTemp.value) {
const confirmUserSelection = (user) => {
if (!user) {
ElMessage.warning('请选择一个用户')
return
}
if (selectorType.value === 'query') {
// 查询表单选择
queryParams.user_id = selectedUserTemp.value.UserId
queryParams.user_id = user.UserId
} else {
// 编辑表单选择
editForm.user_id = selectedUserTemp.value.UserId
editForm.user_id = user.UserId
}
// 将选中的用户添加到 UserOptions 中(如果不存在)
if (!UserOptions.value.find(u => u.UserId === selectedUserTemp.value.UserId)) {
UserOptions.value.push(selectedUserTemp.value)
if (!UserOptions.value.find(u => u.UserId === user.UserId)) {
UserOptions.value.push(user)
}
userSelectorVisible.value = false
@@ -692,7 +574,6 @@ const submitEditForm = () => {
// 初始化
onMounted(() => {
fetchUserList()
fetchDiscountList()
if (queryParams.user_id) {
fetchHoldersList()
+69
View File
@@ -0,0 +1,69 @@
<template>
<div class="voucher-management-container">
<div class="header">
<el-page-header @back="goBack">
<template #content>
<span class="text-large font-600 mr-3">代金券管理 (ID: {{ voucherId }})</span>
</template>
</el-page-header>
</div>
<el-card class="mt-4" shadow="never">
<el-tabs v-model="activeTab" type="card">
<el-tab-pane label="用户分发管理" name="user-distribution">
<UserVoucher :code-id="voucherId" />
</el-tab-pane>
<el-tab-pane label="商品关联管理" name="discount-goods">
<DiscountGoods :code-id="voucherId" />
</el-tab-pane>
<el-tab-pane label="用户关联管理" name="discount-users">
<DiscountUsers :code-id="voucherId" />
</el-tab-pane>
<el-tab-pane label="用户信息管理" name="user-info">
<VoucherHolders :code-id="voucherId" />
</el-tab-pane>
<el-tab-pane label="用户使用记录" name="user-history">
<VoucherHistory :code-id="voucherId" />
</el-tab-pane>
</el-tabs>
</el-card>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import UserVoucher from './UserVoucher.vue'
import DiscountGoods from './DiscountGoods.vue'
import DiscountUsers from './DiscountUsers.vue'
import VoucherHolders from './VoucherHolders.vue'
import VoucherHistory from './VoucherHistory.vue'
const route = useRoute()
const router = useRouter()
const activeTab = ref('user-distribution')
const voucherId = computed(() => route.params.id)
const goBack = () => {
router.push('/marketing/voucher')
}
</script>
<style scoped>
.voucher-management-container {
padding: 20px;
}
.header {
margin-bottom: 20px;
}
.mt-4 {
margin-top: 16px;
}
</style>
+429 -3
View File
@@ -99,7 +99,7 @@
<el-table-column label="操作" width="200" fixed="right">
<template #default="{ row }">
<el-button type="primary" link @click="handleEdit(row)">编辑</el-button>
<!-- <el-button type="warning" link @click="handleSpec(row)">规格</el-button> -->
<el-button type="warning" link @click="handleParameter(row)">参数</el-button>
<el-button type="danger" link @click="handleDelete(row)">删除</el-button>
</template>
</el-table-column>
@@ -181,6 +181,152 @@
<el-button type="primary" @click="submitForm">确定</el-button>
</template>
</el-dialog>
<!-- 商品参数列表对话框 -->
<el-dialog
v-model="paramDialogVisible"
title="商品参数管理"
width="900px"
>
<div class="filter-section" style="border: none; padding: 0 0 16px 0;">
<div class="action-bar">
<el-button type="primary" @click="handleAddParameter">
<el-icon><Plus /></el-icon>新增参数
</el-button>
<el-button type="success" @click="fetchParameterList">
<el-icon><Refresh /></el-icon>刷新
</el-button>
</div>
</div>
<el-table
v-loading="paramLoading"
:data="parameterList"
style="width: 100%"
:header-cell-style="{ background: '#fafafa', color: '#333', fontWeight: 600 }"
>
<el-table-column prop="id" label="参数ID" width="100" />
<el-table-column prop="name" label="参数名称" min-width="150" />
<el-table-column prop="type" label="参数类型" width="120">
<template #default="{ row }">
<el-tag :type="getArgTypeTag(row.type)">
{{ getArgTypeText(row.type) }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="250" fixed="right">
<template #default="{ row }">
<div class="action-buttons">
<el-button type="primary" link @click="handleEditParameter(row)">编辑</el-button>
<el-button type="success" link @click="handleViewParamValues(row)">查看参数值</el-button>
<el-button type="danger" link @click="handleDeleteParameter(row)">删除</el-button>
</div>
</template>
</el-table-column>
</el-table>
</el-dialog>
<!-- 商品参数表单对话框 -->
<el-dialog
v-model="paramFormDialogVisible"
:title="paramFormType === 'add' ? '新增商品参数' : '编辑商品参数'"
width="600px"
append-to-body
>
<el-form
ref="paramFormRef"
:model="paramForm"
:rules="paramRules"
label-width="100px"
>
<el-form-item label="参数名称" prop="arg_name">
<el-input v-model="paramForm.arg_name" placeholder="请输入参数名称" />
</el-form-item>
<el-form-item label="参数类型" prop="arg_type">
<el-radio-group v-model="paramForm.arg_type">
<el-radio label="string">字符串</el-radio>
<el-radio label="number">数字</el-radio>
<el-radio label="select">选择</el-radio>
</el-radio-group>
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="paramFormDialogVisible = false">取消</el-button>
<el-button type="primary" @click="submitParamForm">确定</el-button>
</div>
</template>
</el-dialog>
<!-- 参数值管理对话框 -->
<el-dialog
v-model="paramValuesDialogVisible"
title="参数值管理"
width="800px"
append-to-body
>
<div class="values-header">
<span>参数{{ currentParam?.name }}</span>
<el-button type="primary" @click="handleAddParamValue">
<el-icon><Plus /></el-icon>添加参数值
</el-button>
</div>
<el-table
v-loading="paramValuesLoading"
:data="paramValueList"
style="width: 100%; margin-top: 20px"
:header-cell-style="{ background: '#fafafa', color: '#333', fontWeight: 600 }"
>
<el-table-column prop="id" label="值ID" width="100" />
<el-table-column prop="name" label="值名称" min-width="150" />
<el-table-column prop="value" label="值" min-width="150" />
<el-table-column label="价格" width="120">
<template #default="{ row }">
¥{{ (row.price / 100).toFixed(2) }}
</template>
</el-table-column>
<el-table-column label="操作" width="200" fixed="right">
<template #default="{ row }">
<div class="action-buttons">
<el-button type="primary" link @click="handleEditParamValue(row)">编辑</el-button>
<el-button type="danger" link @click="handleDeleteParamValue(row)">删除</el-button>
</div>
</template>
</el-table-column>
</el-table>
</el-dialog>
<!-- 参数值表单对话框 -->
<el-dialog
v-model="paramValueFormDialogVisible"
:title="paramValueFormType === 'add' ? '添加参数值' : '编辑参数值'"
width="500px"
append-to-body
>
<el-form
ref="paramValueFormRef"
:model="paramValueForm"
:rules="paramValueRules"
label-width="100px"
>
<el-form-item label="值名称" prop="attr_name">
<el-input v-model="paramValueForm.attr_name" placeholder="请输入值名称" />
</el-form-item>
<el-form-item label="值" prop="attr_value">
<el-input v-model="paramValueForm.attr_value" placeholder="请输入值" />
</el-form-item>
<el-form-item label="价格(元)" prop="attr_price">
<el-input-number v-model="paramValueForm.attr_price" :min="0" :precision="2" :step="0.01" placeholder="请输入价格" style="width: 100%" />
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="paramValueFormDialogVisible = false">取消</el-button>
<el-button type="primary" @click="submitParamValueForm">确定</el-button>
</div>
</template>
</el-dialog>
</div>
</template>
@@ -189,8 +335,16 @@ import { ref, reactive, onMounted } from 'vue'
import { getFileDetail } from '@/api/admin/file'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Plus, Delete, Search, Refresh } from '@element-plus/icons-vue'
import { getProductList, createProduct, updateProduct, deleteProduct } from '@/api/admin/product'
import { getProductGroupList } from '@/api/admin/product'
import { getProductList, createProduct, updateProduct, deleteProduct, getProductGroupList,
getProductParameterList,
getProductParameterDetail,
createProductParameter,
updateProductParameter,
deleteProductParameter,
addProductParameterValue,
updateProductParameterValue,
deleteProductParameterValue
} from '@/api/admin/product'
// 查询参数
const queryParams = reactive({
@@ -480,6 +634,259 @@ onMounted(() => {
fetchProductList()
fetchGroupList()
})
// ---------------------------------------------------------------------
// 参数管理相关逻辑
// ---------------------------------------------------------------------
// 状态
const paramDialogVisible = ref(false)
const paramLoading = ref(false)
const parameterList = ref([])
const currentProductId = ref(null)
const paramFormDialogVisible = ref(false)
const paramFormType = ref('add')
const paramFormRef = ref(null)
const paramForm = reactive({
arg_id: undefined,
arg_name: '',
arg_type: 'string'
})
const paramRules = {
arg_name: [{ required: true, message: '请输入参数名称', trigger: 'blur' }],
arg_type: [{ required: true, message: '请选择参数类型', trigger: 'change' }]
}
const paramValuesDialogVisible = ref(false)
const paramValuesLoading = ref(false)
const paramValueList = ref([])
const currentParam = ref(null)
const paramValueFormDialogVisible = ref(false)
const paramValueFormType = ref('add')
const paramValueFormRef = ref(null)
const paramValueForm = reactive({
attr_id: undefined,
attr_name: '',
attr_value: '',
attr_price: 0
})
const paramValueRules = {
attr_name: [{ required: true, message: '请输入值名称', trigger: 'blur' }],
attr_value: [{ required: true, message: '请输入值', trigger: 'blur' }],
attr_price: [{ required: true, message: '请输入价格', trigger: 'blur' }]
}
// 打开参数管理
const handleParameter = (row) => {
currentProductId.value = row.id
paramDialogVisible.value = true
fetchParameterList()
}
// 获取参数列表
const fetchParameterList = async () => {
if (!currentProductId.value) return
paramLoading.value = true
try {
const res = await getProductParameterList({ good_id: currentProductId.value })
if (res.data.code === 200) {
parameterList.value = res.data.data || []
}
} catch (error) {
ElMessage.error('获取参数列表失败')
} finally {
paramLoading.value = false
}
}
// 参数类型显示
const getArgTypeText = (type) => {
const typeMap = { 'string': '字符串', 'number': '数字', 'select': '选择' }
return typeMap[type] || '未知'
}
const getArgTypeTag = (type) => {
const tagMap = { 'string': 'primary', 'number': 'success', 'select': 'warning' }
return tagMap[type] || 'info'
}
// 新增参数
const handleAddParameter = () => {
paramFormType.value = 'add'
paramFormDialogVisible.value = true
Object.assign(paramForm, {
arg_id: undefined,
arg_name: '',
arg_type: 'string'
})
paramFormRef.value?.resetFields()
}
// 编辑参数
const handleEditParameter = (row) => {
paramFormType.value = 'edit'
paramFormDialogVisible.value = true
Object.assign(paramForm, {
arg_id: row.id,
arg_name: row.name,
arg_type: row.type
})
}
// 删除参数
const handleDeleteParameter = (row) => {
ElMessageBox.confirm(`确认删除参数 ${row.name} 吗?`, '警告', {
confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning'
}).then(async () => {
try {
const res = await deleteProductParameter({ good_id: currentProductId.value, arg_id: row.id })
if (res.data.code === 200) {
ElMessage.success('删除成功')
fetchParameterList()
}
} catch (error) {
ElMessage.error('删除失败')
}
}).catch(() => {})
}
// 提交参数表单
const submitParamForm = () => {
paramFormRef.value?.validate(async (valid) => {
if (valid) {
try {
const submitData = {
good_id: Number(currentProductId.value),
arg_name: paramForm.arg_name,
arg_type: paramForm.arg_type
}
if (paramFormType.value === 'edit') {
submitData.arg_id = paramForm.arg_id
}
const res = paramFormType.value === 'add'
? await createProductParameter(submitData)
: await updateProductParameter(submitData)
if (res.data.code === 200) {
ElMessage.success(paramFormType.value === 'add' ? '新增成功' : '修改成功')
paramFormDialogVisible.value = false
fetchParameterList()
}
} catch (error) {
ElMessage.error(error.response?.data?.message || '操作失败')
}
}
})
}
// 查看参数值
const handleViewParamValues = (row) => {
currentParam.value = row
paramValuesDialogVisible.value = true
fetchParamValuesList()
}
// 获取参数值列表
const fetchParamValuesList = async () => {
if (!currentProductId.value || !currentParam.value) return
paramValuesLoading.value = true
try {
const res = await getProductParameterDetail({
good_id: currentProductId.value,
arg_id: currentParam.value.id
})
if (res.data.code === 200) {
paramValueList.value = res.data.data.attrs || []
}
} catch (error) {
ElMessage.error('获取参数值列表失败')
} finally {
paramValuesLoading.value = false
}
}
// 添加参数值
const handleAddParamValue = () => {
paramValueFormType.value = 'add'
paramValueFormDialogVisible.value = true
Object.assign(paramValueForm, {
attr_id: undefined,
attr_name: '',
attr_value: '',
attr_price: 0
})
paramValueFormRef.value?.resetFields()
}
// 编辑参数值
const handleEditParamValue = (row) => {
paramValueFormType.value = 'edit'
paramValueFormDialogVisible.value = true
Object.assign(paramValueForm, {
attr_id: row.id,
attr_name: row.name,
attr_value: row.value,
attr_price: row.price / 100
})
}
// 删除参数值
const handleDeleteParamValue = (row) => {
ElMessageBox.confirm(`确认删除参数值 ${row.name} 吗?`, '警告', {
confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning'
}).then(async () => {
try {
const res = await deleteProductParameterValue({
good_id: currentProductId.value,
attr_id: row.id
})
if (res.data.code === 200) {
ElMessage.success('删除成功')
fetchParamValuesList()
}
} catch (error) {
ElMessage.error('删除失败')
}
}).catch(() => {})
}
// 提交参数值表单
const submitParamValueForm = () => {
paramValueFormRef.value?.validate(async (valid) => {
if (valid) {
try {
const submitData = {
good_id: Number(currentProductId.value),
arg_id: Number(currentParam.value.id),
attr_name: paramValueForm.attr_name,
attr_value: paramValueForm.attr_value,
attr_price: paramValueForm.attr_price
}
if (paramValueFormType.value === 'edit') {
submitData.attr_id = paramValueForm.attr_id
}
const res = paramValueFormType.value === 'add'
? await addProductParameterValue(submitData)
: await updateProductParameterValue(submitData)
if (res.data.code === 200) {
ElMessage.success(paramValueFormType.value === 'add' ? '添加成功' : '修改成功')
paramValueFormDialogVisible.value = false
fetchParamValuesList()
}
} catch (error) {
ElMessage.error(error.response?.data?.message || '操作失败')
}
}
})
}
</script>
<style scoped>
@@ -607,5 +1014,24 @@ onMounted(() => {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}
.values-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.action-buttons {
display: flex;
gap: 8px;
align-items: center;
}
.dialog-footer {
display: flex;
justify-content: flex-end;
gap: 12px;
}
</style>
-771
View File
@@ -1,771 +0,0 @@
<template>
<div class="product-parameter-container">
<!-- 主容器 -->
<el-card class="main-container" shadow="never">
<!-- 操作栏 -->
<div class="filter-section">
<div class="filter-content">
<el-form ref="queryFormRef" label-width="80px" :inline="true" :model="queryParams" class="search-form">
<el-form-item label="商品分组">
<el-select
v-model="queryParams.good_group_id"
placeholder="请选择商品分组"
clearable
@change="handleGroupChange"
style="width: 200px"
>
<el-option
v-for="item in groupOptions"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-form-item label="商品">
<el-select
v-model="queryParams.good_id"
placeholder="请先选择商品分组"
clearable
:disabled="!queryParams.good_group_id"
style="width: 200px"
>
<el-option
v-for="item in productOptions"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleQuery">
<el-icon><Search /></el-icon>查询
</el-button>
<el-button @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
<div class="action-bar">
<el-button type="primary" @click="handleAdd">
<el-icon><Plus /></el-icon>新增商品参数
</el-button>
<el-button type="success" @click="fetchParameterList">
<el-icon><Refresh /></el-icon>刷新
</el-button>
</div>
</div>
</div>
<!-- 商品参数列表 -->
<div class="table-section">
<!-- 骨架屏 -->
<div v-if="loading" class="skeleton-container">
<div v-for="i in 5" :key="i" class="skeleton-row">
<div class="skeleton-cell skeleton-id"></div>
<div class="skeleton-cell skeleton-name"></div>
<div class="skeleton-cell skeleton-type"></div>
<div class="skeleton-cell skeleton-action"></div>
</div>
</div>
<el-table
v-else
v-loading="loading"
:data="parameterList"
style="width: 100%"
:header-cell-style="{ background: '#fafafa', color: '#333', fontWeight: 600 }"
>
<el-table-column prop="id" label="参数ID" width="100" />
<el-table-column prop="name" label="参数名称" min-width="200" />
<el-table-column prop="type" label="参数类型" width="120">
<template #default="{ row }">
<el-tag :type="getArgTypeTag(row.type)">
{{ getArgTypeText(row.type) }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="250" fixed="right">
<template #default="{ row }">
<div class="action-buttons">
<el-button type="primary" link @click="handleEdit(row)">编辑</el-button>
<el-button type="success" link @click="handleViewValues(row)">查看参数值</el-button>
<el-button type="danger" link @click="handleDelete(row)">删除</el-button>
</div>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<el-pagination
v-model:current-page="queryParams.page"
v-model:page-size="queryParams.count"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next, jumper"
:total="total"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
background
class="pagination"
/>
</div>
</el-card>
<!-- 商品参数表单对话框 -->
<el-dialog
v-model="dialogVisible"
:title="dialogType === 'add' ? '新增商品参数' : '编辑商品参数'"
width="600px"
append-to-body
>
<el-form
ref="parameterFormRef"
:model="parameterForm"
:rules="parameterRules"
label-width="100px"
>
<el-form-item label="参数名称" prop="arg_name">
<el-input v-model="parameterForm.arg_name" placeholder="请输入参数名称" />
</el-form-item>
<el-form-item label="参数类型" prop="arg_type">
<el-radio-group v-model="parameterForm.arg_type">
<el-radio label="string">字符串</el-radio>
<el-radio label="number">数字</el-radio>
<el-radio label="select">选择</el-radio>
</el-radio-group>
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="submitForm">确定</el-button>
</div>
</template>
</el-dialog>
<!-- 参数值管理对话框 -->
<el-dialog
v-model="valuesDialogVisible"
title="参数值管理"
width="800px"
append-to-body
>
<div class="values-header">
<span>参数{{ currentParameter?.arg_name }}</span>
<el-button type="primary" @click="handleAddValue">
<el-icon><Plus /></el-icon>添加参数值
</el-button>
</div>
<el-table
v-loading="valuesLoading"
:data="valuesList"
style="width: 100%; margin-top: 20px"
:header-cell-style="{ background: '#fafafa', color: '#333', fontWeight: 600 }"
>
<el-table-column prop="id" label="值ID" width="100" />
<el-table-column prop="name" label="值名称" min-width="150" />
<el-table-column prop="value" label="值" min-width="150" />
<el-table-column label="价格" width="120">
<template #default="{ row }">
¥{{ (row.price / 100).toFixed(2) }}
</template>
</el-table-column>
<el-table-column label="操作" width="200" fixed="right">
<template #default="{ row }">
<div class="action-buttons">
<el-button type="primary" link @click="handleEditValue(row)">编辑</el-button>
<el-button type="danger" link @click="handleDeleteValue(row)">删除</el-button>
</div>
</template>
</el-table-column>
</el-table>
</el-dialog>
<!-- 参数值表单对话框 -->
<el-dialog
v-model="valueDialogVisible"
:title="valueDialogType === 'add' ? '添加参数值' : '编辑参数值'"
width="500px"
append-to-body
>
<el-form
ref="valueFormRef"
:model="valueForm"
:rules="valueRules"
label-width="100px"
>
<el-form-item label="值名称" prop="attr_name">
<el-input v-model="valueForm.attr_name" placeholder="请输入值名称" />
</el-form-item>
<el-form-item label="值" prop="attr_value">
<el-input v-model="valueForm.attr_value" placeholder="请输入值" />
</el-form-item>
<el-form-item label="价格(元)" prop="attr_price">
<el-input-number v-model="valueForm.attr_price" :min="0" :precision="2" :step="0.01" placeholder="请输入价格" style="width: 100%" />
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="valueDialogVisible = false">取消</el-button>
<el-button type="primary" @click="submitValueForm">确定</el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Plus, Search, Refresh } from '@element-plus/icons-vue'
import {
getProductParameterList,
getProductParameterDetail,
createProductParameter,
updateProductParameter,
deleteProductParameter,
addProductParameterValue,
updateProductParameterValue,
deleteProductParameterValue,
getProductList,
getProductGroupList
} from '@/api/admin/product'
// 查询参数
const queryParams = reactive({
good_group_id: undefined, // 商品分组ID
good_id: undefined, // 商品ID
page: 1,
count: 10
})
// 下拉选项数据
const groupOptions = ref([]) // 商品分组选项
const productOptions = ref([]) // 商品选项
const queryFormRef = ref(null)
// 商品参数表单
const parameterForm = reactive({
good_id: undefined,
arg_id: undefined,
arg_name: '',
arg_type: 'string'
})
const parameterRules = {
arg_name: [
{ required: true, message: '请输入参数名称', trigger: 'blur' }
],
arg_type: [
{ required: true, message: '请选择参数类型', trigger: 'change' }
]
}
// 参数值表单
const valueForm = reactive({
good_id: undefined,
arg_id: undefined,
attr_id: undefined,
attr_name: '',
attr_value: '',
attr_price: 0
})
const valueRules = {
attr_name: [
{ required: true, message: '请输入值名称', trigger: 'blur' }
],
attr_value: [
{ required: true, message: '请输入值', trigger: 'blur' }
],
attr_price: [
{ required: true, message: '请输入价格', trigger: 'blur' }
]
}
// 状态数据
const loading = ref(false)
const valuesLoading = ref(false)
const parameterList = ref([])
const valuesList = ref([])
const total = ref(0)
const dialogVisible = ref(false)
const valuesDialogVisible = ref(false)
const valueDialogVisible = ref(false)
const dialogType = ref('add')
const valueDialogType = ref('add')
const currentParameter = ref(null)
const parameterFormRef = ref(null)
const valueFormRef = ref(null)
// 获取商品分组列表
const fetchGroupList = async () => {
try {
const res = await getProductGroupList({ page: 1, count: 100 })
console.log('商品分组列表:', res.data)
if (res.data.code === 200) {
groupOptions.value = res.data.data.data || []
}
} catch (error) {
console.error('获取商品分组列表失败:', error)
ElMessage.error('获取商品分组列表失败')
}
}
// 获取商品列表(根据分组ID
const fetchProductList = async (groupId) => {
try {
const res = await getProductList({ good_group_id: groupId, page: 1, count: 100 })
console.log('商品列表:', res.data)
if (res.data.code === 200) {
productOptions.value = res.data.data.data || []
productOptions.value = productOptions.value.filter(item => item.delete == false)
}
} catch (error) {
console.error('获取商品列表失败:', error)
ElMessage.error('获取商品列表失败')
}
}
// 商品分组改变时
const handleGroupChange = (groupId) => {
// 清空商品选择
queryParams.good_id = undefined
productOptions.value = []
if (groupId) {
// 获取该分组下的商品列表
fetchProductList(groupId)
}
}
// 获取商品参数列表
const fetchParameterList = async () => {
// 如果没有选择商品ID,不查询
if (!queryParams.good_id) {
ElMessage.warning('请先选择商品')
return
}
loading.value = true
try {
const res = await getProductParameterList({ good_id: queryParams.good_id })
console.log('商品参数列表:', res.data)
if (res.data.code === 200) {
parameterList.value = res.data.data || []
total.value = res.data.data.length || 0
}
} catch (error) {
console.error('获取商品参数列表失败:', error)
ElMessage.error('获取商品参数列表失败')
} finally {
loading.value = false
}
}
// 查询
const handleQuery = () => {
queryParams.page = 1
fetchParameterList()
}
// 重置查询
const resetQuery = () => {
queryParams.good_group_id = undefined
queryParams.good_id = undefined
queryParams.page = 1
productOptions.value = []
parameterList.value = []
total.value = 0
}
// 获取参数值列表
const fetchValuesList = async (goodId, argId) => {
valuesLoading.value = true
console.log('goodId', goodId)
console.log('argId', argId)
try {
const res = await getProductParameterDetail({ good_id: goodId, arg_id: argId })
console.log('参数值列表:', res.data)
if (res.data.code === 200) {
valuesList.value = res.data.data.attrs || []
}
} catch (error) {
console.error('获取参数值列表失败:', error)
ElMessage.error('获取参数值列表失败')
} finally {
valuesLoading.value = false
}
}
// 获取参数类型文本
const getArgTypeText = (type) => {
const typeMap = {
'string': '字符串',
'number': '数字',
'select': '选择'
}
return typeMap[type] || '未知'
}
// 获取参数类型标签颜色
const getArgTypeTag = (type) => {
const tagMap = {
'string': 'primary',
'number': 'success',
'select': 'warning'
}
return tagMap[type] || 'info'
}
// 分页
const handleSizeChange = (size) => {
queryParams.count = size
fetchParameterList()
}
const handleCurrentChange = (page) => {
queryParams.page = page
fetchParameterList()
}
// 新增商品参数
const handleAdd = () => {
if (!queryParams.good_id) {
ElMessage.warning('请先选择商品')
return
}
dialogType.value = 'add'
dialogVisible.value = true
Object.assign(parameterForm, {
good_id: queryParams.good_id,
arg_id: undefined,
arg_name: '',
arg_type: 'string'
})
parameterFormRef.value?.resetFields()
}
// 编辑商品参数
const handleEdit = (row) => {
dialogType.value = 'edit'
dialogVisible.value = true
Object.assign(parameterForm, {
good_id: queryParams.good_id,
arg_id: row.id,
arg_name: row.name,
arg_type: row.type
})
}
// 查看参数值
const handleViewValues = (row) => {
currentParameter.value = row
valuesDialogVisible.value = true
fetchValuesList(queryParams.good_id, row.id)
}
// 添加参数值
const handleAddValue = () => {
valueDialogType.value = 'add'
console.log('currentParameter', currentParameter.value)
valueDialogVisible.value = true
Object.assign(valueForm, {
good_id: queryParams.good_id,
arg_id: currentParameter.value.id,
attr_id: undefined,
attr_name: '',
attr_value: '',
attr_price: 0
})
valueFormRef.value?.resetFields()
}
// 编辑参数值
const handleEditValue = (row) => {
valueDialogType.value = 'edit'
valueDialogVisible.value = true
Object.assign(valueForm, {
good_id: queryParams.good_id,
arg_id: currentParameter.value.id,
attr_id: row.id,
attr_name: row.name,
attr_value: row.value,
attr_price: row.price / 100 // 分转元
})
}
// 删除参数值
const handleDeleteValue = (row) => {
ElMessageBox.confirm(`确认删除参数值 ${row.name} 吗?`, '警告', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(async () => {
try {
const res = await deleteProductParameterValue({
good_id: queryParams.good_id,
attr_id: row.id
})
console.log('删除参数值响应:', res.data)
if (res.data.code === 200) {
ElMessage.success('删除成功')
fetchValuesList(queryParams.good_id, currentParameter.value.id)
}
} catch (error) {
console.error('删除失败:', error)
ElMessage.error(error.response?.data?.message || '删除失败')
}
}).catch(() => {})
}
// 删除商品参数
const handleDelete = (row) => {
ElMessageBox.confirm(`确认删除商品参数 ${row.name} 吗?`, '警告', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(async () => {
try {
const res = await deleteProductParameter({
good_id: queryParams.good_id,
arg_id: row.id
})
console.log('删除参数响应:', res.data)
if (res.data.code === 200) {
ElMessage.success('删除成功')
fetchParameterList()
}
} catch (error) {
console.error('删除失败:', error)
ElMessage.error(error.response?.data?.message || '删除失败')
}
}).catch(() => {})
}
// 提交参数表单
const submitForm = () => {
parameterFormRef.value?.validate(async (valid) => {
if (valid) {
try {
const submitData = {
good_id: Number(parameterForm.good_id),
arg_name: parameterForm.arg_name,
arg_type: parameterForm.arg_type
}
if (dialogType.value === 'edit') {
submitData.arg_id = parameterForm.arg_id
}
console.log('提交参数数据:', submitData)
let res
if (dialogType.value === 'add') {
res = await createProductParameter(submitData)
} else {
res = await updateProductParameter(submitData)
}
console.log('提交参数响应:', res.data)
if (res.data.code === 200) {
ElMessage.success(dialogType.value === 'add' ? '新增成功' : '修改成功')
dialogVisible.value = false
fetchParameterList()
}
} catch (error) {
console.error('操作失败:', error)
ElMessage.error(error.response?.data?.message || '操作失败')
}
}
})
}
// 提交参数值表单
const submitValueForm = () => {
valueFormRef.value?.validate(async (valid) => {
if (valid) {
try {
const submitData = {
good_id: Number(valueForm.good_id),
arg_id: Number(valueForm.arg_id),
attr_name: valueForm.attr_name,
attr_value: valueForm.attr_value,
attr_price: valueForm.attr_price // 元转分
}
if (valueDialogType.value === 'edit') {
submitData.attr_id = valueForm.attr_id
}
console.log('提交参数值数据:', submitData)
let res
if (valueDialogType.value === 'add') {
res = await addProductParameterValue(submitData)
} else {
res = await updateProductParameterValue(submitData)
}
console.log('提交参数值响应:', res.data)
if (res.data.code === 200) {
ElMessage.success(valueDialogType.value === 'add' ? '添加成功' : '修改成功')
valueDialogVisible.value = false
fetchValuesList(queryParams.good_id, currentParameter.value.id)
}
} catch (error) {
console.error('操作失败:', error)
ElMessage.error(error.response?.data?.message || '操作失败')
}
}
})
}
// 初始化
onMounted(() => {
// 初始化时只获取商品分组列表
fetchGroupList()
})
</script>
<style scoped>
.product-parameter-container {
padding: 0;
}
.main-container {
border: 1px solid #e1e8ed;
background: #ffffff;
}
.filter-section {
padding: 0;
border-bottom: 1px solid #e1e8ed;
background: #fafbfc;
}
.filter-content {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 20px;
gap: 20px;
flex-wrap: wrap;
}
.search-form {
margin: 0;
flex: 1;
display: flex;
align-items: center;
gap: 12px;
}
.search-form :deep(.el-form-item) {
margin-bottom: 0;
}
.action-bar {
display: flex;
gap: 12px;
flex-shrink: 0;
}
.table-section {
padding: 0;
}
.action-buttons {
display: flex;
gap: 8px;
align-items: center;
}
.values-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.pagination {
margin-top: 20px;
padding: 16px 20px;
border-top: 1px solid #e1e8ed;
background: #fafbfc;
justify-content: flex-end;
}
.dialog-footer {
display: flex;
justify-content: flex-end;
gap: 12px;
padding: 0;
}
/* 表格样式优化 */
:deep(.el-table) {
border: none;
color: #2c3e50;
}
:deep(.el-table__header) {
background: #f8f9fa;
}
:deep(.el-table th) {
background: #f8f9fa !important;
border-bottom: 2px solid #e1e8ed;
color: #2c3e50;
font-weight: 600;
font-size: 13px;
}
:deep(.el-table td) {
border-bottom: 1px solid #f0f2f5;
color: #34495e;
}
:deep(.el-table tr:hover > td) {
background-color: #f8f9fa !important;
}
:deep(.el-card__body) {
padding: 0;
}
/* 骨架屏样式 */
.skeleton-container {
padding: 20px;
}
.skeleton-row {
display: flex;
align-items: center;
padding: 16px 0;
border-bottom: 1px solid #f0f0f0;
gap: 16px;
}
.skeleton-row:last-child {
border-bottom: none;
}
.skeleton-cell {
height: 20px;
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
background-size: 200% 100%;
animation: skeleton-loading 1.5s ease-in-out infinite;
border-radius: 4px;
}
.skeleton-id { width: 100px; }
.skeleton-name { width: 200px; }
.skeleton-type { width: 120px; }
.skeleton-action { width: 250px; height: 32px; }
@keyframes skeleton-loading {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}
</style>
+134 -103
View File
@@ -33,7 +33,7 @@
@click="selectTicket(ticket)"
>
<div class="ticket-avatar">
<el-avatar :size="40">{{ ticket.username.charAt(0) }}</el-avatar>
<el-avatar :size="40" :src="ticket.avatar">{{ ticket.username.charAt(0) }}</el-avatar>
</div>
<div class="ticket-content">
<div class="ticket-top">
@@ -96,8 +96,8 @@
:class="['message-item', message.isAdmin ? 'message-admin' : message.isSystem ? 'message-system' : 'message-user']"
>
<div class="message-avatar" v-if="!message.isAdmin && !message.isSystem">
<el-avatar :size="36" :src="getUserAvatar(message.userId)">
{{ currentTicket.username.charAt(0) }}
<el-avatar :size="36" :src="message.avatar">
{{ message.userId === currentTicket.userId ? currentTicket.username.charAt(0) : 'U' }}
</el-avatar>
</div>
<div class="message-content">
@@ -117,7 +117,7 @@
<div class="message-time">{{ formatMessageTime(message.time) }}</div>
</div>
<div class="message-avatar" v-if="message.isAdmin && !message.isSystem">
<el-avatar :size="36" :src="getUserAvatar(message.userId || 1)">A</el-avatar>
<el-avatar :size="36" :src="message.avatar">A</el-avatar>
</div>
</div>
</div>
@@ -204,21 +204,24 @@ import { ElMessage, ElMessageBox } from 'element-plus'
import { Search, Plus, Loading } from '@element-plus/icons-vue'
import { useRoute, useRouter } from 'vue-router'
import {
getTickerList,
getTickerList,
getTicketDetail,
replyTicket,
closeTicket,
getUserAvatar,
getFileImage,
parseFilesToImages
parseFilesToImages,
getTicketCount
} from '@/api/ticket'
import notificationSound from '@/assets/7.wav'
import { useUserStore } from '@/store/userStore'
// 路由相关
const route = useRoute()
const router = useRouter()
// 管理员ID列表(客服ID
const adminUserIds = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] // 假设这些ID是客服ID
// 用户 store
const userStore = useUserStore()
// 头像
const adminAvatar = ref('')
@@ -271,6 +274,11 @@ const stats = reactive({
isLoadingStats: false
})
// 上一次的待处理数量,用于判断是否有新工单
const previousPendingCount = ref(0)
// 音频对象
const audio = new Audio(notificationSound)
// 快捷回复选项
const quickReplies = ref([
{ title: '您好,有什么可以帮助您的?', content: '您好,有什么可以帮助您的?' },
@@ -327,8 +335,9 @@ const fetchTicketList = async (append = false) => {
const mappedTickets = tickets.map(item => ({
id: item.work_id,
title: item.name,
username: `用户${item.user_id}`, // 用户名,真实环境可能需要获取用户信息
userId: item.user_id,
username: item.user?.userName || `用户${item.user?.userId || 'Unknown'}`,
userId: item.user?.userId,
avatar: item.user?.coverUrl || '',
createTime: new Date(item.created_at).toLocaleString(),
lastReplyTime: new Date(item.update_time).toLocaleString(),
status: convertStatusToString(item.status),
@@ -368,44 +377,35 @@ const fetchTicketList = async (append = false) => {
}
}
// 获取单个状态的工单数量
const fetchStatusStat = async (status) => {
try {
// 将状态字符串转换为API所需的状态值
let statusValue = '';
if (status === 'pending') statusValue = '0';
else if (status === 'processing') statusValue = '1';
else if (status === 'replied') statusValue = '2';
else if (status === 'completed') statusValue = '3';
const res = await getTickerList(10, 1, statusValue) // 只请求一条数据,但获取总数
if (res.code === 200) {
if (status === '') {
stats.total = res.data.all_count
} else {
stats[status] = res.data.all_count
}
} else {
console.error(`获取${status || '全部'}工单统计失败:`, res.message)
}
} catch (error) {
console.error(`获取${status || '全部'}工单统计出错:`, error)
}
}
// 获取所有状态的工单数量
const fetchAllStats = async () => {
stats.isLoadingStats = true
try {
// 并行获取各个状态的工单数量
await Promise.all([
fetchStatusStat(''), // 获取全部工单数量
fetchStatusStat('pending'), // 待处理
fetchStatusStat('processing'), // 处理中
fetchStatusStat('replied'), // 已回复
fetchStatusStat('completed') // 已完成
])
const res = await getTicketCount()
if (res.code === 200) {
const data = res.data
// 检查是否有新工单(待处理数量增加)
if (data.wait_count > previousPendingCount.value && previousPendingCount.value !== 0) {
try {
audio.play().catch(e => console.error('播放提示音失败:', e))
} catch (e) {
console.error('播放提示音出错:', e)
}
}
// 更新上一次的数量
previousPendingCount.value = data.wait_count
stats.total = data.all_count
stats.pending = data.wait_count
stats.replied = data.reply_count
stats.completed = data.close_count
// 计算处理中的数量:总数 - 待处理 - 已回复 - 已完成
stats.processing = data.all_count - data.wait_count - data.reply_count - data.close_count
} else {
console.error('获取工单统计失败:', res.message)
}
} catch (error) {
console.error('获取工单统计数据出错:', error)
} finally {
@@ -413,18 +413,12 @@ const fetchAllStats = async () => {
}
}
// 刷新当前分类的统计数据(用于定时刷新,减少请求
// 刷新统计数据(用于定时刷新)
const fetchCurrentStatusStat = async () => {
try {
// 只获取当前选中分类的统计数据
await fetchStatusStat(activeStatus.value)
// 同时获取全部工单数量(因为顶部显示需要)
await fetchStatusStat('')
} catch (error) {
console.error('获取当前分类统计数据出错:', error)
}
await fetchAllStats()
}
// 加载更多工单
const loadMoreTickets = () => {
if (!hasMore.value || isLoading.value) return
@@ -476,9 +470,9 @@ const filteredTickets = computed(() => {
})
})
// 判断是否是客服
// 判断是否是当前登录的管理员
const isAdmin = (userId) => {
return adminUserIds.includes(userId)
return userId === userStore.userInfo?.user_id
}
// 状态转换
@@ -540,25 +534,25 @@ const fetchTicketMessages = async (workId) => {
}
// 处理消息列表
if (detail.Content && detail.Content.length > 0) {
// 使用Promise.all一次性处理所有消息和图片
const messagesPromises = detail.Content.map(async (msg) => {
const isAdminMsg = isAdmin(msg.UserId)
const images = await parseFilesToImages(msg.Flies)
if (detail.content && detail.content.length > 0) {
// 处理所有消息
const messages = detail.content.map((msg) => {
const isAdminMsg = isAdmin(msg.user?.userId)
// 从 flies 数组中提取图片 URL
const images = msg.flies ? msg.flies.map(file => file.url) : []
return {
id: msg.Id,
content: msg.Content !== 'empty' ? msg.Content : null,
id: msg.id,
content: msg.content !== 'empty' ? msg.content : null,
images: images,
time: new Date(msg.CreatedAt).toLocaleString(),
time: new Date().toLocaleString(), // API 没有返回时间,使用当前时间
isAdmin: isAdminMsg,
isSystem: false,
userId: msg.UserId
userId: msg.user?.userId,
avatar: msg.user?.coverUrl || ''
}
})
// 等待所有消息处理完成
const messages = await Promise.all(messagesPromises)
currentMessages.value = messages
}
} else {
@@ -589,23 +583,29 @@ const sendMessage = async () => {
const fileIds = []
try {
// 添加一个临时的"正在发送"消息
// 保存输入内容
const inputMsg = messageInput.value.trim()
const inputImages = [...selectedImages.value]
// 清空输入和已选图片
messageInput.value = ''
selectedImages.value = []
// 立即添加消息到界面(不显示 loading)
const tempMsg = {
content: messageInput.value.trim() || null,
images: selectedImages.value.length > 0 ? [...selectedImages.value] : null,
id: Date.now(), // 临时 ID
content: inputMsg || null,
images: inputImages.length > 0 ? inputImages : [],
time: new Date().toLocaleString(),
isAdmin: true,
isLoading: true,
isSystem: false,
userId: userStore.userInfo?.user_id,
avatar: userStore.userInfo?.cover_url || '',
isTempMessage: true
}
currentMessages.value.push(tempMsg)
// 清空输入和已选图片
const inputMsg = messageInput.value
messageInput.value = ''
selectedImages.value = []
// 滚动到底部
await nextTick()
scrollToBottom()
@@ -633,6 +633,7 @@ const sendMessage = async () => {
// 恢复输入内容
messageInput.value = inputMsg
selectedImages.value = inputImages
ElMessage.error(res.message || '发送失败')
}
@@ -838,8 +839,9 @@ const refreshTicketList = async () => {
const mappedTickets = tickets.map(item => ({
id: item.work_id,
title: item.name,
username: `用户${item.user_id}`,
userId: item.user_id,
username: item.user?.userName || `用户${item.user?.userId || 'Unknown'}`,
userId: item.user?.userId,
avatar: item.user?.coverUrl || '',
createTime: new Date(item.created_at).toLocaleString(),
lastReplyTime: new Date(item.update_time).toLocaleString(),
status: convertStatusToString(item.status),
@@ -874,6 +876,39 @@ const refreshTicketList = async () => {
}
}
// 辅助函数:去除 URL 中的查询参数
const normalizeUrl = (url) => {
if (!url) return ''
return url.split('?')[0]
}
// 辅助函数:比较两个消息数组是否相同(忽略 URL 查询参数)
const areMessagesEqual = (messages1, messages2) => {
if (messages1.length !== messages2.length) return false
for (let i = 0; i < messages1.length; i++) {
const msg1 = messages1[i]
const msg2 = messages2[i]
// 比较消息 ID 和内容
if (msg1.id !== msg2.id || msg1.content !== msg2.content) return false
// 比较图片数量
if ((msg1.images?.length || 0) !== (msg2.images?.length || 0)) return false
// 比较图片 URL(去除查询参数)
if (msg1.images && msg2.images) {
for (let j = 0; j < msg1.images.length; j++) {
if (normalizeUrl(msg1.images[j]) !== normalizeUrl(msg2.images[j])) {
return false
}
}
}
}
return true
}
// 静默刷新聊天记录(不显示loading状态)
const refreshTicketMessages = async (workId) => {
try {
@@ -886,37 +921,33 @@ const refreshTicketMessages = async (workId) => {
if (currentTicket.value) {
// 只有非待处理状态才直接更新,待处理状态保持不变,等待回复后再更新
if (currentTicket.value.status !== 'pending') {
currentTicket.value.status = convertStatusToString(detail.Status)
currentTicket.value.status = convertStatusToString(detail.status)
}
}
// 处理消息列表
if (detail.Content && detail.Content.length > 0) {
// 检查是否有新消息
const lastMsgId = currentMessages.value.length > 0 ?
currentMessages.value[currentMessages.value.length - 1].id : 0;
const hasNewMessage = detail.Content.some(msg => msg.Id > lastMsgId);
if (hasNewMessage) {
// 有新消息时才更新
const messagesPromises = detail.Content.map(async (msg) => {
const isAdminMsg = isAdmin(msg.UserId)
const images = await parseFilesToImages(msg.Flies)
return {
id: msg.Id,
content: msg.Content !== 'empty' ? msg.Content : null,
images: images,
time: new Date(msg.CreatedAt).toLocaleString(),
isAdmin: isAdminMsg,
isSystem: false,
userId: msg.UserId
}
})
if (detail.content && detail.content.length > 0) {
// 构建新消息列表
const newMessages = detail.content.map((msg) => {
const isAdminMsg = isAdmin(msg.user?.userId)
// 从 flies 数组中提取图片 URL
const images = msg.flies ? msg.flies.map(file => file.url) : []
// 等待所有消息处理完成
const messages = await Promise.all(messagesPromises)
currentMessages.value = messages
return {
id: msg.id,
content: msg.content !== 'empty' ? msg.content : null,
images: images,
time: new Date().toLocaleString(), // API 没有返回时间,使用当前时间
isAdmin: isAdminMsg,
isSystem: false,
userId: msg.user?.userId,
avatar: msg.user?.coverUrl || ''
}
})
// 只有在消息真正发生变化时才更新(忽略 URL 查询参数的变化)
if (!areMessagesEqual(currentMessages.value, newMessages)) {
currentMessages.value = newMessages
// 如果有新消息,滚动到底部
nextTick(() => {
+73 -22
View File
@@ -304,23 +304,37 @@
</el-dialog>
<!-- Token 展示对话框 -->
<el-dialog v-model="tokenDialogVisible" title="模拟登录 Token" width="600px" append-to-body>
<el-dialog v-model="tokenDialogVisible" title="模拟登录" width="450px" append-to-body>
<div class="token-container">
<el-alert type="success" :closable="false" show-icon class="token-alert">
<template #default>
<span>Token 生成成功请妥善保管</span>
</template>
</el-alert>
<div class="token-box">
<el-input v-model="loginToken" type="textarea" :rows="4" readonly class="token-input" />
<div class="token-actions">
<el-button type="primary" link :icon="CopyDocument" @click="copyToken">复制</el-button>
</div>
</div>
<el-form label-width="100px">
<el-form-item label="选择环境">
<el-select v-model="selectedEnvironment" placeholder="请选择登录环境" size="large" style="width: 100%">
<el-option label="正式环境" value="production">
<div class="env-option">
<span>正式环境</span>
<span class="env-url">www.007yjs.com</span>
</div>
</el-option>
<el-option label="测试环境" value="test">
<div class="env-option">
<span>测试环境</span>
<span class="env-url">apiserver.s1f.ren</span>
</div>
</el-option>
<el-option label="本地环境" value="local">
<div class="env-option">
<span>本地环境</span>
<span class="env-url">localhost:5173</span>
</div>
</el-option>
</el-select>
</el-form-item>
</el-form>
</div>
<template #footer>
<div class="dialog-footer">
<el-button @click="tokenDialogVisible = false">关闭</el-button>
<el-button @click="tokenDialogVisible = false">取消</el-button>
<el-button type="primary" @click="confirmJump" :disabled="!selectedEnvironment">确认跳转</el-button>
</div>
</template>
</el-dialog>
@@ -364,7 +378,7 @@ import AvatarSelector from '@/components/admin/AvatarSelector.vue'
import {
ArrowLeft, Refresh, Edit as EditIcon, Delete, Wallet, Avatar, Lock,
UserFilled, Document, Clock, List, Switch, User, Camera, Upload,
UploadFilled, Key, CopyDocument
UploadFilled, Key, Monitor, Setting
} from '@element-plus/icons-vue'
import { getUserGroupList } from '@/api/admin/user'
import { getFileDetail, getFileList, getFile, uploadFile } from '@/api/admin/file'
@@ -465,6 +479,15 @@ const realnameForm = reactive({
// Token 展示相关
const tokenDialogVisible = ref(false)
const loginToken = ref('')
const loginExpire = ref('')
const selectedEnvironment = ref('')
// 环境配置
const environments = {
production: 'https://www.007yjs.com',
test: 'https://apiserver.s1f.ren',
local: 'http://localhost:5173'
}
// 管理员权限管理相关
const adminDialogVisible = ref(false)
@@ -729,8 +752,10 @@ const handleSimulateLogin = async () => {
const res = await mockUserLogin({ user_id: userInfo.value.UserId })
if (res.data.code === 200) {
loginToken.value = res.data.data.token || ''
loginExpire.value = res.data.data.expire_time || ''
selectedEnvironment.value = ''
tokenDialogVisible.value = true
ElMessage.success('模拟登录成功')
//ElMessage.success('模拟登录成功')
} else {
ElMessage.error(res.data.message || '模拟登录失败')
}
@@ -739,14 +764,18 @@ const handleSimulateLogin = async () => {
}
}
// 复制 Token
const copyToken = async () => {
try {
await navigator.clipboard.writeText(loginToken.value)
ElMessage.success('Token 已复制到剪贴板')
} catch (err) {
ElMessage.error('复制失败,请手动复制')
// 确认跳转
const confirmJump = () => {
if (!selectedEnvironment.value) {
ElMessage.warning('请选择登录环境')
return
}
const baseUrl = environments[selectedEnvironment.value]
const url = `${baseUrl}/token-login?token=${loginToken.value}&expire=${loginExpire.value}`
window.open(url, '_blank')
const envName = selectedEnvironment.value === 'production' ? '正式' : selectedEnvironment.value === 'test' ? '测试' : '本地'
ElMessage.success(`正在跳转到${envName}环境`)
tokenDialogVisible.value = false
}
// 获取实名状态文本
@@ -1166,6 +1195,28 @@ onActivated(() => {
font-size: 13px;
}
/* Token Dialog */
.token-container {
padding: 20px 0;
}
.env-option {
display: flex;
align-items: center;
justify-content: space-between;
padding: 4px 0;
}
.env-option span:first-child {
font-size: 14px;
color: #303133;
}
.env-url {
font-size: 12px;
color: #909399;
}
/* Responsive */
@media (max-width: 1024px) {
.detail-grid {