1 Commits

Author SHA1 Message Date
wlkjyy 777022632c Merge pull request 'refactor: extract image form to standalone page and implement tags view store' (#11) from qian into master
Build and Deploy Vue3 / build (push) Successful in 2m58s
Build and Deploy Vue3 / deploy (push) Successful in 3m43s
Reviewed-on: https://gitlab.s1f.top/lin/ApiServer-Web-admin_dashboard_pc/pulls/11
2025-11-28 14:17:42 +08:00
17 changed files with 1550 additions and 1342 deletions
-2
View File
@@ -1,5 +1,3 @@
# 管理员后台pc端
# 007UI 后台管理系统 # 007UI 后台管理系统
一个基于Vue 3、Element Plus的现代化后台管理系统模板,采用蓝色扁平化高端设计风格。 一个基于Vue 3、Element Plus的现代化后台管理系统模板,采用蓝色扁平化高端设计风格。
BIN
View File
Binary file not shown.
-191
View File
@@ -1,191 +0,0 @@
<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>
+39 -7
View File
@@ -5,12 +5,12 @@ export const menus = [
icon: 'DataBoard' icon: 'DataBoard'
}, },
{ {
path: '/ticket', path : '/ticket',
title: '工单处理', title: '工单处理',
icon: 'DataBoard' icon: 'DataBoard'
}, },
{ {
path: '/user', path:'/user',
title: '用户管理', title: '用户管理',
icon: 'User', icon: 'User',
children: [ children: [
@@ -45,7 +45,10 @@ export const menus = [
path: '/product/group', path: '/product/group',
title: '商品分组' title: '商品分组'
}, },
{
path: '/product/parameter',
title: '商品参数'
}
] ]
}, },
{ {
@@ -72,7 +75,36 @@ export const menus = [
path: '/marketing/voucher', path: '/marketing/voucher',
title: '代金券管理' 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'
}
] ]
}, },
{ {
@@ -126,10 +158,10 @@ export const menus = [
] ]
}, },
{ {
path: '/setting', path:'/setting',
title: '全局设置管理', title:'全局设置管理',
children: [ children:[
{ path: '/setting/global', title: '全局设置' } {path:'/setting/global',title:'全局设置'}
] ]
} }
] ]
+48 -8
View File
@@ -230,7 +230,14 @@ const routes = [
title: '商品分组' title: '商品分组'
} }
}, },
{
path: 'parameter',
name: 'ProductParameter',
component: () => import('../views/product/ProductParameter.vue'),
meta: {
title: '商品参数'
}
}
] ]
}, },
// 订单管理路由 // 订单管理路由
@@ -280,16 +287,49 @@ const routes = [
} }
}, },
{ {
path: 'voucher/:id/manage', path: 'user-distribution',
name: 'VoucherManagement', name: 'UserDistribution',
component: () => import('../views/marketing/VoucherManagement.vue'), component: () => import('../views/marketing/UserVoucher.vue'),
meta: { 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'
}
}
] ]
}, },
// 活动管理路由 // 活动管理路由
-5
View File
@@ -52,8 +52,3 @@ export function timeToTimestamp(time) {
return Math.floor(timestamp / 1000); // 返回毫秒级时间戳(如 1751107200000 return Math.floor(timestamp / 1000); // 返回毫秒级时间戳(如 1751107200000
} }
export function reducenum(num){
return num / 100
}
+6 -20
View File
@@ -5,8 +5,8 @@
<!-- 搜索和操作栏 --> <!-- 搜索和操作栏 -->
<div class="filter-section"> <div class="filter-section">
<div class="filter-content"> <div class="filter-content">
<el-form :inline="true" :model="queryParams" class="search-form" v-if="!codeId"> <el-form :inline="true" :model="queryParams" class="search-form">
<el-form-item label="代金卷" v-if="!codeId"> <el-form-item label="代金卷">
<el-select <el-select
v-model="queryParams.code_id" v-model="queryParams.code_id"
placeholder="请选择代金券" placeholder="请选择代金券"
@@ -71,7 +71,7 @@
> >
<el-table-column type="selection" width="55" /> <el-table-column type="selection" width="55" />
<el-table-column prop="id" label="ID" width="80" /> <el-table-column prop="id" label="ID" width="80" />
<el-table-column prop="discountId" label="代金券ID" width="120" v-if="!codeId" /> <el-table-column prop="discountId" label="代金券ID" width="120" />
<el-table-column label="关联对象ID" width="120"> <el-table-column label="关联对象ID" width="120">
<template #default="{ row }"> <template #default="{ row }">
{{ row.goodId || row.goodGroupId || '-' }} {{ row.goodId || row.goodGroupId || '-' }}
@@ -149,7 +149,7 @@
placeholder="请选择代金券" placeholder="请选择代金券"
filterable filterable
clearable clearable
:disabled="dialogType === 'edit' || !!codeId" :disabled="dialogType === 'edit'"
style="width: 100%" style="width: 100%"
> >
<el-option <el-option
@@ -234,7 +234,7 @@
</template> </template>
<script setup> <script setup>
import { ref, reactive, onMounted, watch } from 'vue' import { ref, reactive, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus' import { ElMessage, ElMessageBox } from 'element-plus'
import { Delete, Search, Plus, Refresh } from '@element-plus/icons-vue' import { Delete, Search, Plus, Refresh } from '@element-plus/icons-vue'
import { import {
@@ -249,27 +249,13 @@ import {
getProductGroupList getProductGroupList
} from '@/api/admin/product' } from '@/api/admin/product'
const props = defineProps({
codeId: {
type: [String, Number],
default: ''
}
})
// 查询参数 // 查询参数
const queryParams = reactive({ const queryParams = reactive({
code_id: props.codeId || '', code_id: '',
page: 1, page: 1,
count: 10 count: 10
}) })
watch(() => props.codeId, (newVal) => {
if (newVal) {
queryParams.code_id = newVal
fetchGoodsList()
}
})
// 表单数据 // 表单数据
const form = reactive({ const form = reactive({
id: undefined, id: undefined,
+159 -53
View File
@@ -5,8 +5,8 @@
<!-- 搜索和操作栏 --> <!-- 搜索和操作栏 -->
<div class="filter-section"> <div class="filter-section">
<div class="filter-content"> <div class="filter-content">
<el-form :inline="true" :model="queryParams" class="search-form" v-if="!codeId"> <el-form :inline="true" :model="queryParams" class="search-form">
<el-form-item label="代金卷" v-if="!codeId"> <el-form-item label="代金卷">
<el-select <el-select
v-model="queryParams.code_id" v-model="queryParams.code_id"
placeholder="请选择代金券" placeholder="请选择代金券"
@@ -68,20 +68,10 @@
> >
<el-table-column type="selection" width="55" /> <el-table-column type="selection" width="55" />
<el-table-column prop="id" label="ID" width="80" /> <el-table-column prop="id" label="ID" width="80" />
<el-table-column prop="discountId" label="代金券ID" width="120" v-if="!codeId" /> <el-table-column prop="discountId" label="代金券ID" width="120" />
<el-table-column label="用户名" min-width="150"> <el-table-column label="关联对象ID" width="130">
<template #default="{ row }"> <template #default="{ row }">
{{ row?.user?.user_name || '-' }} {{ row.userId || row.userGroupId || '-' }}
</template>
</el-table-column>
<el-table-column label="手机号" min-width="150">
<template #default="{ row }">
{{ row?.user?.phone || '-' }}
</template>
</el-table-column>
<el-table-column label="邮箱" min-width="150">
<template #default="{ row }">
{{ row?.user?.email || '-' }}
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="类型" width="120"> <el-table-column label="类型" width="120">
@@ -91,6 +81,11 @@
</el-tag> </el-tag>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="创建时间" width="180">
<template #default="{ row }">
{{ formatDate(row.CreatedAt) }}
</template>
</el-table-column>
<el-table-column label="操作" width="200" fixed="right"> <el-table-column label="操作" width="200" fixed="right">
<template #default="{ row }"> <template #default="{ row }">
<div class="action-buttons"> <div class="action-buttons">
@@ -229,16 +224,82 @@
</el-dialog> </el-dialog>
<!-- 用户选择弹窗 --> <!-- 用户选择弹窗 -->
<UserSelector <el-dialog
v-model:visible="userSelectorVisible" v-model="userSelectorVisible"
@select="confirmUserSelection" 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>
</div> </div>
</template> </template>
<script setup> <script setup>
import { ref, reactive, onMounted, watch } from 'vue' import { ref, reactive, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus' import { ElMessage, ElMessageBox } from 'element-plus'
import { Delete, Search, Plus, Refresh, User } from '@element-plus/icons-vue' import { Delete, Search, Plus, Refresh, User } from '@element-plus/icons-vue'
import { import {
@@ -252,29 +313,14 @@ import {
getUserList, getUserList,
getUserGroupList getUserGroupList
} from '@/api/admin/user' } from '@/api/admin/user'
import UserSelector from '@/components/UserSelector/index.vue'
const props = defineProps({
codeId: {
type: [String, Number],
default: ''
}
})
// 查询参数 // 查询参数
const queryParams = reactive({ const queryParams = reactive({
code_id: props.codeId || '', code_id: '',
page: 1, page: 1,
count: 10 count: 10
}) })
watch(() => props.codeId, (newVal) => {
if (newVal) {
queryParams.code_id = newVal
fetchUsersList()
}
})
// 表单数据 // 表单数据
const form = reactive({ const form = reactive({
id: undefined, id: undefined,
@@ -318,6 +364,15 @@ const userGroupOptions = ref([]) // 用户组列表选项
// 用户选择弹窗相关 // 用户选择弹窗相关
const userSelectorVisible = ref(false) 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) => { const formatDate = (dateStr) => {
@@ -333,24 +388,25 @@ const formatDate = (dateStr) => {
// 获取用户类型名称(根据行数据) // 获取用户类型名称(根据行数据)
const getUserTypeNameByRow = (row) => { const getUserTypeNameByRow = (row) => {
// userId 不为 0 说明是用户
//通过看是否有user对象参数判断是否为用户还是用户组类型 if (row.userId && row.userId !== 0) {
if(row.user){
return '用户' return '用户'
}else{ }
// userGroupId 不为 0 说明是用户组
if (row.userGroupId && row.userGroupId !== 0) {
return '用户组' return '用户组'
} }
return '-' return '-'
} }
// 获取用户类型标签(根据行数据) // 获取用户类型标签(根据行数据)
const getUserTypeTagByRow = (row) => { const getUserTypeTagByRow = (row) => {
// 用户用蓝色
if (row.userId && row.userId !== 0) {
if(row.user){
return 'primary' return 'primary'
}else{ }
// 用户组用橙色
if (row.userGroupId && row.userGroupId !== 0) {
return 'warning' return 'warning'
} }
return 'info' return 'info'
@@ -413,19 +469,70 @@ const fetchUserGroupList = async () => {
// 打开用户选择器 // 打开用户选择器
const openUserSelector = () => { const openUserSelector = () => {
userSelectorVisible.value = true 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 = (user) => { const confirmUserSelection = () => {
if (!user) { if (!selectedUserTemp.value) {
ElMessage.warning('请选择一个用户') ElMessage.warning('请选择一个用户')
return return
} }
form.selected_user = user.UserId form.selected_user = selectedUserTemp.value.UserId
form.user_id = user.UserId form.user_id = selectedUserTemp.value.UserId
// 将选中的用户添加到 userOptions 中(如果不存在) // 将选中的用户添加到 userOptions 中(如果不存在)
if (!userOptions.value.find(u => u.UserId === user.UserId)) { if (!userOptions.value.find(u => u.UserId === selectedUserTemp.value.UserId)) {
userOptions.value.push(user) userOptions.value.push(selectedUserTemp.value)
} }
userSelectorVisible.value = false userSelectorVisible.value = false
ElMessage.success('用户选择成功') ElMessage.success('用户选择成功')
@@ -584,7 +691,6 @@ const handleEdit = (row) => {
}) })
//点击编辑需要初始化加载用户列表 //点击编辑需要初始化加载用户列表
fetchUserList() fetchUserList()
fetchUserGroupList()
} }
// 删除用户关联 // 删除用户关联
+33 -85
View File
@@ -2,8 +2,8 @@
<div class="user-voucher-container"> <div class="user-voucher-container">
<!-- 搜索和操作栏 --> <!-- 搜索和操作栏 -->
<el-card class="filter-container" shadow="never"> <el-card class="filter-container" shadow="never">
<el-form :inline="true" :model="queryParams" class="search-form" v-if="!codeId"> <el-form :inline="true" :model="queryParams" class="search-form">
<el-form-item label="代金券" v-if="!codeId"> <el-form-item label="代金券">
<el-select <el-select
v-model="queryParams.code_id" v-model="queryParams.code_id"
placeholder="请选择代金券" placeholder="请选择代金券"
@@ -54,37 +54,37 @@
{{ row.Id || row.id }} {{ row.Id || row.id }}
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="用户ID" min-width="120"> <el-table-column label="用户ID" width="100">
<template #default="{ row }"> <template #default="{ row }">
{{ row.UserId || row.userId }} {{ row.UserId || row.userId }}
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="代金券ID" width="100" v-if="!codeId"> <el-table-column label="代金券ID" width="100">
<template #default="{ row }"> <template #default="{ row }">
{{ row.discountId }} {{ row.discountId }}
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="代金券名称" min-width="150" v-if="!codeId"> <el-table-column label="代金券名称" min-width="150">
<template #default="{ row }"> <template #default="{ row }">
{{ row.discount?.name || '-' }} {{ row.discount?.name || '-' }}
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="面额" min-width="120"> <el-table-column label="面额" width="120">
<template #default="{ row }"> <template #default="{ row }">
<span class="amount">¥{{ row.discount?.amount ? (row.discount.amount / 100).toFixed(2) : '0.00' }}</span> <span class="amount">¥{{ row.discount?.amount ? (row.discount.amount / 100).toFixed(2) : '0.00' }}</span>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="已使用/最大次数" min-width="150"> <el-table-column label="已使用/最大次数" width="150">
<template #default="{ row }"> <template #default="{ row }">
<el-tag type="info">{{ row.useTimes || 0 }} / {{ row.maxUseTimes || row.discount?.maxTimes || 0 }}</el-tag> <el-tag type="info">{{ row.useTimes || 0 }} / {{ row.maxUseTimes || row.discount?.maxTimes || 0 }}</el-tag>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="过期时间" min-width="180"> <el-table-column label="过期时间" width="180">
<template #default="{ row }"> <template #default="{ row }">
{{ formatDate(row.expireAt) }} {{ formatDate(row.expireAt) }}
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="创建时间" min-width="180"> <el-table-column label="创建时间" width="180">
<template #default="{ row }"> <template #default="{ row }">
{{ formatDate(row.CreatedAt) }} {{ formatDate(row.CreatedAt) }}
</template> </template>
@@ -134,7 +134,7 @@
<el-select <el-select
v-model="addForm.voucher_id" v-model="addForm.voucher_id"
placeholder="请选择代金券" placeholder="请选择代金券"
:disabled="addForm.discount_type === 'code' || !!codeId" :disabled="addForm.discount_type === 'code'"
filterable filterable
clearable clearable
style="width: 100%" style="width: 100%"
@@ -178,22 +178,25 @@
</el-form-item> </el-form-item>
<el-form-item label="用户" prop="user_id"> <el-form-item label="用户" prop="user_id">
<div class="user-selector-wrapper"> <el-select
<div class="selected-user-display" v-if="addForm.user_id"> v-model="addForm.user_id"
<el-tag type="primary" closable @close="clearSelectedUser"> placeholder="请选择用户"
{{ getSelectedUserName() }} :disabled="addForm.target_type === 'group'"
</el-tag> filterable
</div> clearable
<el-button remote
type="primary" :remote-method="searchUsers"
plain :loading="userSearchLoading"
@click="openUserSelector" style="width: 100%"
style="width: 100%" @change="handleUserChange"
> >
<el-icon><User /></el-icon> <el-option
{{ addForm.user_id ? '重新选择用户' : '选择用户' }} v-for="item in userOptions"
</el-button> :key="item.UserId"
</div> :label="`${item.UserName} (ID: ${item.UserId})`"
:value="item.UserId"
/>
</el-select>
</el-form-item> </el-form-item>
<el-form-item label="用户组" prop="group_id"> <el-form-item label="用户组" prop="group_id">
@@ -265,19 +268,13 @@
<el-button type="primary" @click="submitEdit">确定</el-button> <el-button type="primary" @click="submitEdit">确定</el-button>
</template> </template>
</el-dialog> </el-dialog>
<!-- 用户选择弹窗 -->
<UserSelector
v-model:visible="userSelectorVisible"
@select="confirmUserSelection"
/>
</div> </div>
</template> </template>
<script setup> <script setup>
import { ref, reactive, onMounted, watch } from 'vue' import { ref, reactive, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus' import { ElMessage, ElMessageBox } from 'element-plus'
import { Delete, Search, Plus, Refresh, User } from '@element-plus/icons-vue' import { Delete, Search, Plus, Refresh } from '@element-plus/icons-vue'
import { import {
getUserVoucherList, getUserVoucherList,
addUserVoucher, addUserVoucher,
@@ -288,29 +285,14 @@ import {
allocateVoucher allocateVoucher
} from '@/api/admin/discount' } from '@/api/admin/discount'
import { getUserList, getUserGroupList } from '@/api/admin/user' 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({ const queryParams = reactive({
code_id: props.codeId || undefined, code_id: undefined,
page: 1, page: 1,
count: 10 count: 10
}) })
watch(() => props.codeId, (newVal) => {
if (newVal) {
queryParams.code_id = newVal
fetchUserVoucherList()
}
})
// 添加表单 // 添加表单
const addForm = reactive({ const addForm = reactive({
discount_type: 'coupon', // 优惠类型:coupon-代金券, code-优惠码 discount_type: 'coupon', // 优惠类型:coupon-代金券, code-优惠码
@@ -339,7 +321,6 @@ const groupOptions = ref([]) // 用户组选项
const userSearchLoading = ref(false) // 用户搜索加载状态 const userSearchLoading = ref(false) // 用户搜索加载状态
const submitLoading = ref(false) // 提交加载状态 const submitLoading = ref(false) // 提交加载状态
const dataList = ref([]) // 优惠列表 const dataList = ref([]) // 优惠列表
const userSelectorVisible = ref(false)
// 编辑表单 // 编辑表单
const editForm = reactive({ const editForm = reactive({
@@ -642,36 +623,6 @@ 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 () => { const handleAdd = async () => {
addDialogVisible.value = true addDialogVisible.value = true
@@ -679,7 +630,7 @@ const handleAdd = async () => {
// 重置表单 // 重置表单
Object.assign(addForm, { Object.assign(addForm, {
discount_type: 'coupon', discount_type: 'coupon',
voucher_id: props.codeId || undefined, voucher_id: undefined,
code_id: undefined, code_id: undefined,
target_type: 'user', target_type: 'user',
user_id: undefined, user_id: undefined,
@@ -886,9 +837,6 @@ onMounted(() => {
// 加载代金券列表供选择 // 加载代金券列表供选择
fetchVoucherListOptions() fetchVoucherListOptions()
fetchDiscountList() fetchDiscountList()
if (queryParams.code_id) {
fetchUserVoucherList()
}
}) })
</script> </script>
+1 -11
View File
@@ -63,10 +63,9 @@
<el-icon v-else color="#f56c6c" :size="20"><CircleCloseFilled /></el-icon> <el-icon v-else color="#f56c6c" :size="20"><CircleCloseFilled /></el-icon>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="操作" width="280" fixed="right"> <el-table-column label="操作" width="200" fixed="right">
<template #default="{ row }"> <template #default="{ row }">
<el-button type="primary" link @click="handleEdit(row)">编辑</el-button> <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="success" link @click="handleView(row)">查看</el-button>
<el-button type="danger" link @click="handleDelete(row)">删除</el-button> <el-button type="danger" link @click="handleDelete(row)">删除</el-button>
</template> </template>
@@ -167,10 +166,8 @@
</div> </div>
</template> </template>
<script setup> <script setup>
import { ref, reactive, onMounted } from 'vue' import { ref, reactive, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { ElMessage, ElMessageBox } from 'element-plus' import { ElMessage, ElMessageBox } from 'element-plus'
import { Plus, Delete, Refresh, SuccessFilled, CircleCloseFilled } from '@element-plus/icons-vue' import { Plus, Delete, Refresh, SuccessFilled, CircleCloseFilled } from '@element-plus/icons-vue'
import { import {
@@ -183,8 +180,6 @@ import {
import { timeToTimestamp } from '@/utils/tool' import { timeToTimestamp } from '@/utils/tool'
import DiscountDetailDialog from '@/components/marketing/DiscountDetailDialog.vue' import DiscountDetailDialog from '@/components/marketing/DiscountDetailDialog.vue'
const router = useRouter()
// 查询参数 // 查询参数
const queryParams = reactive({ const queryParams = reactive({
discount_type: 'coupon', // 固定为coupon表示代金券 discount_type: 'coupon', // 固定为coupon表示代金券
@@ -317,11 +312,6 @@ const handleEdit = (row) => {
}) })
} }
// 管理代金券
const handleManage = (row) => {
router.push(`/marketing/voucher/${row.id}/manage`)
}
// 查看代金券详情 // 查看代金券详情
const handleView = async (row) => { const handleView = async (row) => {
try { try {
+144 -42
View File
@@ -46,8 +46,8 @@
<el-table-column prop="user_id" label="用户ID" width="100" /> <el-table-column prop="user_id" label="用户ID" width="100" />
<el-table-column prop="username" label="用户名" width="150" /> <el-table-column prop="username" label="用户名" width="150" />
<el-table-column prop="email" label="邮箱" min-width="200" /> <el-table-column prop="email" label="邮箱" min-width="200" />
<el-table-column prop="discount_id" label="代金券ID" width="120" v-if="!codeId" /> <el-table-column prop="discount_id" label="代金券ID" width="120" />
<el-table-column prop="discount_name" label="代金券名称" min-width="180" v-if="!codeId" /> <el-table-column prop="discount_name" label="代金券名称" min-width="180" />
<el-table-column label="优惠金额" width="120"> <el-table-column label="优惠金额" width="120">
<template #default="{ row }"> <template #default="{ row }">
<span class="amount">¥{{ row.discount_amount ? (row.discount_amount / 100).toFixed(2) : '0.00' }}</span> <span class="amount">¥{{ row.discount_amount ? (row.discount_amount / 100).toFixed(2) : '0.00' }}</span>
@@ -133,10 +133,74 @@
</template> </template>
</el-dialog> </el-dialog>
<!-- 用户选择弹窗 --> <!-- 用户选择弹窗 -->
<UserSelector <el-dialog
v-model:visible="userSelectorVisible" v-model="userSelectorVisible"
@select="confirmUserSelection" 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>
<!-- 统计卡片 --> <!-- 统计卡片 -->
<el-row :gutter="20" style="margin-top: 20px"> <el-row :gutter="20" style="margin-top: 20px">
@@ -173,36 +237,20 @@
</template> </template>
<script setup> <script setup>
import { ref, reactive, onMounted, computed, watch } from 'vue' import { ref, reactive, onMounted, computed } from 'vue'
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
import { Search, Refresh, Download } from '@element-plus/icons-vue' import { Search, Refresh, Download } from '@element-plus/icons-vue'
import { getUserVoucherHistory, getDiscountCodeList } from '@/api/admin/discount' import { getUserVoucherHistory, getDiscountCodeList } from '@/api/admin/discount'
import { getUserList } from '@/api/admin/user' import { getUserList } from '@/api/admin/user'
import UserSelector from '@/components/UserSelector/index.vue'
const props = defineProps({
codeId: {
type: [String, Number],
default: ''
}
})
// 查询参数 // 查询参数
const queryParams = reactive({ const queryParams = reactive({
user_id: undefined, user_id: undefined,
code_id: props.codeId || undefined,
id: '', id: '',
page: 1, page: 1,
count: 10 count: 10
}) })
watch(() => props.codeId, (newVal) => {
if (newVal) {
queryParams.code_id = newVal
fetchHistoryList()
}
})
// 状态数据 // 状态数据
const loading = ref(false) const loading = ref(false)
const historyList = ref([]) const historyList = ref([])
@@ -213,6 +261,15 @@ const currentDetail = ref({})
const discountOptions = ref([]) const discountOptions = ref([])
const selectorType = ref('query') const selectorType = ref('query')
const userSelectorVisible = ref(false) 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([]) const UserOptions = ref([])
// 格式化日期 // 格式化日期
@@ -314,41 +371,86 @@ const resetUserSearch = () => {
// fetchUserSelectorList() // fetchUserSelectorList()
} }
// 打开查询用户选择器
const openQueryUserSelector = () => {
selectorType.value = 'query'
userSelectorVisible.value = true
}
// 打开编辑用户选择器
const openEditUserSelector = () => {
selectorType.value = 'edit'
userSelectorVisible.value = true
}
// 确认用户选择 // 确认用户选择
const confirmUserSelection = (user) => { const confirmUserSelection = () => {
if (!user) { if (!selectedUserTemp.value) {
ElMessage.warning('请选择一个用户') ElMessage.warning('请选择一个用户')
return return
} }
if (selectorType.value === 'query') { if (selectorType.value === 'query') {
// 查询表单选择 // 查询表单选择
queryParams.user_id = user.UserId queryParams.user_id = selectedUserTemp.value.UserId
} else { } else {
// 编辑表单选择 // 编辑表单选择
editForm.user_id = user.UserId editForm.user_id = selectedUserTemp.value.UserId
} }
// 将选中的用户添加到 UserOptions 中(如果不存在) // 将选中的用户添加到 UserOptions 中(如果不存在)
if (!UserOptions.value.find(u => u.UserId === user.UserId)) { if (!UserOptions.value.find(u => u.UserId === selectedUserTemp.value.UserId)) {
UserOptions.value.push(user) UserOptions.value.push(selectedUserTemp.value)
} }
userSelectorVisible.value = false userSelectorVisible.value = false
ElMessage.success('用户选择成功') 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 = () => { const handleQuery = () => {
@@ -359,7 +461,7 @@ const handleQuery = () => {
// 重置查询 // 重置查询
const resetQuery = () => { const resetQuery = () => {
queryParams.user_id = undefined queryParams.user_id = undefined
queryParams.code_id = undefined queryParams.discount_id = undefined
queryParams.id = '' queryParams.id = ''
queryParams.page = 1 queryParams.page = 1
fetchHistoryList() fetchHistoryList()
+167 -48
View File
@@ -40,51 +40,51 @@
style="width: 100%" style="width: 100%"
> >
<el-table-column prop="Id" label="ID" width="80" /> <el-table-column prop="Id" label="ID" width="80" />
<el-table-column prop="UserId" label="用户ID" min-width="100" /> <el-table-column prop="UserId" label="用户ID" width="100" />
<el-table-column label="代金券ID" min-width="110" v-if="!codeId"> <el-table-column label="代金券ID" width="120">
<template #default="{ row }"> <template #default="{ row }">
{{ row.discountId || '-' }} {{ row.discountId || '-' }}
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="代金券名称" min-width="180" v-if="!codeId" show-overflow-tooltip> <el-table-column label="代金券名称" min-width="180">
<template #default="{ row }"> <template #default="{ row }">
{{ row.discount?.name || '-' }} {{ row.discount?.name || '-' }}
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="代金券编码" min-width="150" v-if="!codeId"> <el-table-column label="代金券编码" width="150">
<template #default="{ row }"> <template #default="{ row }">
{{ row.discount?.code || '-' }} {{ row.discount?.code || '-' }}
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="面额" min-width="110"> <el-table-column label="面额" width="120">
<template #default="{ row }"> <template #default="{ row }">
<span class="amount">¥{{ row.discount?.amount ? (row.discount.amount / 100).toFixed(2) : '0.00' }}</span> <span class="amount">¥{{ row.discount?.amount ? (row.discount.amount / 100).toFixed(2) : '0.00' }}</span>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column prop="useTimes" label="已使用" min-width="100" /> <el-table-column prop="useTimes" label="已使用次数" width="120" />
<el-table-column prop="maxUseTimes" label="最大使用" min-width="100" /> <el-table-column prop="maxUseTimes" label="最大使用次数" width="120" />
<el-table-column label="状态" min-width="100"> <el-table-column label="状态" width="100">
<template #default="{ row }"> <template #default="{ row }">
<el-tag :type="getStatusType(row)" size="small"> <el-tag :type="getStatusType(row)">
{{ getStatusText(row) }} {{ getStatusText(row) }}
</el-tag> </el-tag>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="过期时间" min-width="160"> <el-table-column label="过期时间" width="180">
<template #default="{ row }"> <template #default="{ row }">
{{ formatDate(row.expireAt) }} {{ formatDate(row.expireAt) }}
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="创建时间" min-width="160"> <el-table-column label="创建时间" width="180">
<template #default="{ row }"> <template #default="{ row }">
{{ formatDate(row.CreatedAt) }} {{ formatDate(row.CreatedAt) }}
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="操作" width="210" fixed="right"> <el-table-column label="操作" width="200" fixed="right">
<template #default="{ row }"> <template #default="{ row }">
<el-button type="primary" link size="small" @click="handleView(row)">查看</el-button> <el-button type="primary" link @click="handleView(row)">查看详情</el-button>
<el-button type="warning" link size="small" @click="handleEdit(row)">编辑</el-button> <el-button type="warning" link @click="handleEdit(row)">编辑</el-button>
<el-button type="danger" link size="small" @click="handleDelete(row)">删除</el-button> <el-button type="danger" link @click="handleDelete(row)">删除</el-button>
</template> </template>
</el-table-column> </el-table-column>
</el-table> </el-table>
@@ -203,17 +203,81 @@
</el-dialog> </el-dialog>
<!-- 用户选择弹窗 --> <!-- 用户选择弹窗 -->
<UserSelector <el-dialog
v-model:visible="userSelectorVisible" v-model="userSelectorVisible"
@select="confirmUserSelection" 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>
</div> </div>
</template> </template>
<script setup> <script setup>
import { ref, reactive, onMounted, watch } from 'vue' import { ref, reactive, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus' import { ElMessage, ElMessageBox } from 'element-plus'
import { Search, Refresh, Plus, User } from '@element-plus/icons-vue' import { Search, Refresh, Download, Plus, User } from '@element-plus/icons-vue'
import { import {
getUserVoucherList, getUserVoucherList,
allocateVoucher, allocateVoucher,
@@ -221,34 +285,14 @@ import {
deleteUserVoucher, deleteUserVoucher,
getDiscountCodeList getDiscountCodeList
} from '@/api/admin/discount' } from '@/api/admin/discount'
import UserSelector from '@/components/UserSelector/index.vue' import { getUserList } from '@/api/admin/user'
const props = defineProps({
codeId: {
type: [String, Number],
default: ''
}
})
// 查询参数 // 查询参数
const queryParams = reactive({ const queryParams = reactive({
user_id: undefined, user_id: undefined,
code_id: props.codeId || undefined,
page: 1, page: 1,
count: 10 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 loading = ref(false)
const holdersList = ref([]) const holdersList = ref([])
@@ -263,7 +307,16 @@ const discountOptions = ref([])
// 用户选择弹窗相关 // 用户选择弹窗相关
const userSelectorVisible = ref(false) 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 selectorType = ref('query') // 'query' 或 'edit' 用于区分是查询还是编辑
const userSearchParams = reactive({
key: '',
page: 1,
count: 10
})
// 编辑表单 // 编辑表单
const editForm = reactive({ const editForm = reactive({
@@ -395,36 +448,101 @@ const handleExport = () => {
ElMessage.info('导出功能开发中...') ElMessage.info('导出功能开发中...')
} }
// 获取用户列表
const fetchUserList = async () => {
const res = await getUserList({
page: 1,
count: 10000,
key: ''
})
UserOptions.value = res.data.data?.data || []
}
// 打开查询用户选择器 // 打开查询用户选择器
const openQueryUserSelector = () => { const openQueryUserSelector = () => {
selectorType.value = 'query' selectorType.value = 'query'
userSelectorVisible.value = true userSelectorVisible.value = true
selectedUserTemp.value = null
userSearchParams.key = ''
userSearchParams.page = 1
fetchUserSelectorList()
} }
// 打开编辑用户选择器 // 打开编辑用户选择器
const openEditUserSelector = () => { const openEditUserSelector = () => {
selectorType.value = 'edit' selectorType.value = 'edit'
userSelectorVisible.value = true 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 = (user) => { const confirmUserSelection = () => {
if (!user) { if (!selectedUserTemp.value) {
ElMessage.warning('请选择一个用户') ElMessage.warning('请选择一个用户')
return return
} }
if (selectorType.value === 'query') { if (selectorType.value === 'query') {
// 查询表单选择 // 查询表单选择
queryParams.user_id = user.UserId queryParams.user_id = selectedUserTemp.value.UserId
} else { } else {
// 编辑表单选择 // 编辑表单选择
editForm.user_id = user.UserId editForm.user_id = selectedUserTemp.value.UserId
} }
// 将选中的用户添加到 UserOptions 中(如果不存在) // 将选中的用户添加到 UserOptions 中(如果不存在)
if (!UserOptions.value.find(u => u.UserId === user.UserId)) { if (!UserOptions.value.find(u => u.UserId === selectedUserTemp.value.UserId)) {
UserOptions.value.push(user) UserOptions.value.push(selectedUserTemp.value)
} }
userSelectorVisible.value = false userSelectorVisible.value = false
@@ -574,6 +692,7 @@ const submitEditForm = () => {
// 初始化 // 初始化
onMounted(() => { onMounted(() => {
fetchUserList()
fetchDiscountList() fetchDiscountList()
if (queryParams.user_id) { if (queryParams.user_id) {
fetchHoldersList() fetchHoldersList()
-69
View File
@@ -1,69 +0,0 @@
<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>
+10 -440
View File
@@ -82,8 +82,8 @@
</el-table-column> </el-table-column>
<el-table-column label="库存控制" width="100"> <el-table-column label="库存控制" width="100">
<template #default="{ row }"> <template #default="{ row }">
<el-tag :type="row.inventoryControl ? 'success' : 'info'"> <el-tag :type="row.inventory_control ? 'success' : 'info'">
{{ row.inventoryControl ? '已启用' : '未启用' }} {{ row.inventory_control ? '已启用' : '未启用' }}
</el-tag> </el-tag>
</template> </template>
</el-table-column> </el-table-column>
@@ -99,7 +99,7 @@
<el-table-column label="操作" width="200" fixed="right"> <el-table-column label="操作" width="200" fixed="right">
<template #default="{ row }"> <template #default="{ row }">
<el-button type="primary" link @click="handleEdit(row)">编辑</el-button> <el-button type="primary" link @click="handleEdit(row)">编辑</el-button>
<el-button type="warning" link @click="handleParameter(row)">参数</el-button> <!-- <el-button type="warning" link @click="handleSpec(row)">规格</el-button> -->
<el-button type="danger" link @click="handleDelete(row)">删除</el-button> <el-button type="danger" link @click="handleDelete(row)">删除</el-button>
</template> </template>
</el-table-column> </el-table-column>
@@ -125,7 +125,6 @@
v-model="dialogVisible" v-model="dialogVisible"
:title="dialogType === 'add' ? '新增商品' : '编辑商品'" :title="dialogType === 'add' ? '新增商品' : '编辑商品'"
width="700px" width="700px"
style="margin-top: 300px;"
> >
<el-form <el-form
ref="productFormRef" ref="productFormRef"
@@ -155,8 +154,8 @@
<el-form-item label="封面ID" prop="cover_id"> <el-form-item label="封面ID" prop="cover_id">
<el-input-number v-model="productForm.cover_id" :min="0" placeholder="请输入封面ID" style="width: 100%" /> <el-input-number v-model="productForm.cover_id" :min="0" placeholder="请输入封面ID" style="width: 100%" />
</el-form-item> </el-form-item>
<el-form-item label="库存控制" prop="inventoryControl"> <el-form-item label="库存控制" prop="inventory_control">
<el-switch v-model="productForm.inventoryControl" active-text="启用" inactive-text="禁用" /> <el-switch v-model="productForm.inventory_control" active-text="启用" inactive-text="禁用" />
</el-form-item> </el-form-item>
<el-form-item label="库存数量" prop="inventory"> <el-form-item label="库存数量" prop="inventory">
<el-input-number v-model="productForm.inventory" :min="0" placeholder="请输入库存" style="width: 100%" /> <el-input-number v-model="productForm.inventory" :min="0" placeholder="请输入库存" style="width: 100%" />
@@ -182,152 +181,6 @@
<el-button type="primary" @click="submitForm">确定</el-button> <el-button type="primary" @click="submitForm">确定</el-button>
</template> </template>
</el-dialog> </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> </div>
</template> </template>
@@ -336,16 +189,8 @@ import { ref, reactive, onMounted } from 'vue'
import { getFileDetail } from '@/api/admin/file' import { getFileDetail } from '@/api/admin/file'
import { ElMessage, ElMessageBox } from 'element-plus' import { ElMessage, ElMessageBox } from 'element-plus'
import { Plus, Delete, Search, Refresh } from '@element-plus/icons-vue' import { Plus, Delete, Search, Refresh } from '@element-plus/icons-vue'
import { getProductList, createProduct, updateProduct, deleteProduct, getProductGroupList, import { getProductList, createProduct, updateProduct, deleteProduct } from '@/api/admin/product'
getProductParameterList, import { getProductGroupList } from '@/api/admin/product'
getProductParameterDetail,
createProductParameter,
updateProductParameter,
deleteProductParameter,
addProductParameterValue,
updateProductParameterValue,
deleteProductParameterValue
} from '@/api/admin/product'
// 查询参数 // 查询参数
const queryParams = reactive({ const queryParams = reactive({
@@ -499,7 +344,6 @@ const handleAdd = () => {
// 编辑商品 // 编辑商品
const handleEdit = (row) => { const handleEdit = (row) => {
console.log("更新:",row)
dialogType.value = 'edit' dialogType.value = 'edit'
dialogVisible.value = true dialogVisible.value = true
Object.assign(productForm, { Object.assign(productForm, {
@@ -509,7 +353,7 @@ const handleEdit = (row) => {
content: row.content, content: row.content,
cover_id: row.coverId, cover_id: row.coverId,
good_group_id: row.goodGroupId, good_group_id: row.goodGroupId,
inventory_control: row.inventoryControl, inventory_control: row.inventory_control,
inventory: row.inventory, inventory: row.inventory,
price: row.price, price: row.price,
pay_num: row.payNum, pay_num: row.payNum,
@@ -605,12 +449,10 @@ const submitForm = () => {
good_group_id: Number(productForm.good_group_id), // 确保是数字类型 good_group_id: Number(productForm.good_group_id), // 确保是数字类型
cover_id: productForm.cover_id || 0, cover_id: productForm.cover_id || 0,
inventory: productForm.inventory || 0, inventory: productForm.inventory || 0,
price: productForm.price/100 || 0, price: productForm.price || 0,
pay_num: productForm.pay_num || 1, pay_num: productForm.pay_num || 1,
expire_time: productForm.expire_time || 0, expire_time: productForm.expire_time || 0,
recommend_rebate: productForm.recommend_rebate || 0, recommend_rebate: productForm.recommend_rebate || 0
inventory_control:productForm.inventoryControl
} }
console.log('提交的数据:', submitData) // 调试日志 console.log('提交的数据:', submitData) // 调试日志
@@ -638,259 +480,6 @@ onMounted(() => {
fetchProductList() fetchProductList()
fetchGroupList() 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> </script>
<style scoped> <style scoped>
@@ -1018,24 +607,5 @@ const submitParamValueForm = () => {
0% { background-position: 200% 0; } 0% { background-position: 200% 0; }
100% { 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> </style>
+771
View File
@@ -0,0 +1,771 @@
<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>
+122 -260
View File
@@ -33,7 +33,7 @@
@click="selectTicket(ticket)" @click="selectTicket(ticket)"
> >
<div class="ticket-avatar"> <div class="ticket-avatar">
<el-avatar :size="40" :src="ticket.avatar">{{ ticket.username.charAt(0) }}</el-avatar> <el-avatar :size="40">{{ ticket.username.charAt(0) }}</el-avatar>
</div> </div>
<div class="ticket-content"> <div class="ticket-content">
<div class="ticket-top"> <div class="ticket-top">
@@ -96,8 +96,8 @@
:class="['message-item', message.isAdmin ? 'message-admin' : message.isSystem ? 'message-system' : 'message-user']" :class="['message-item', message.isAdmin ? 'message-admin' : message.isSystem ? 'message-system' : 'message-user']"
> >
<div class="message-avatar" v-if="!message.isAdmin && !message.isSystem"> <div class="message-avatar" v-if="!message.isAdmin && !message.isSystem">
<el-avatar :size="36" :src="message.avatar"> <el-avatar :size="36" :src="getUserAvatar(message.userId)">
{{ message.userId === currentTicket.userId ? currentTicket.username.charAt(0) : 'U' }} {{ currentTicket.username.charAt(0) }}
</el-avatar> </el-avatar>
</div> </div>
<div class="message-content"> <div class="message-content">
@@ -117,7 +117,7 @@
<div class="message-time">{{ formatMessageTime(message.time) }}</div> <div class="message-time">{{ formatMessageTime(message.time) }}</div>
</div> </div>
<div class="message-avatar" v-if="message.isAdmin && !message.isSystem"> <div class="message-avatar" v-if="message.isAdmin && !message.isSystem">
<el-avatar :size="36" :src="message.avatar">A</el-avatar> <el-avatar :size="36" :src="getUserAvatar(message.userId || 1)">A</el-avatar>
</div> </div>
</div> </div>
</div> </div>
@@ -210,18 +210,15 @@ import {
closeTicket, closeTicket,
getUserAvatar, getUserAvatar,
getFileImage, getFileImage,
parseFilesToImages, parseFilesToImages
getTicketCount
} from '@/api/ticket' } from '@/api/ticket'
import notificationSound from '@/assets/7.wav'
import { useUserStore } from '@/store/userStore'
// 路由相关 // 路由相关
const route = useRoute() const route = useRoute()
const router = useRouter() const router = useRouter()
// 用户 store // 管理员ID列表(客服ID
const userStore = useUserStore() const adminUserIds = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] // 假设这些ID是客服ID
// 头像 // 头像
const adminAvatar = ref('') const adminAvatar = ref('')
@@ -274,11 +271,6 @@ const stats = reactive({
isLoadingStats: false isLoadingStats: false
}) })
// 上一次的待处理数量,用于判断是否有新工单
const previousPendingCount = ref(0)
// 音频对象
const audio = new Audio(notificationSound)
// 快捷回复选项 // 快捷回复选项
const quickReplies = ref([ const quickReplies = ref([
{ title: '您好,有什么可以帮助您的?', content: '您好,有什么可以帮助您的?' }, { title: '您好,有什么可以帮助您的?', content: '您好,有什么可以帮助您的?' },
@@ -335,9 +327,8 @@ const fetchTicketList = async (append = false) => {
const mappedTickets = tickets.map(item => ({ const mappedTickets = tickets.map(item => ({
id: item.work_id, id: item.work_id,
title: item.name, title: item.name,
username: item.user?.userName || `用户${item.user?.userId || 'Unknown'}`, username: `用户${item.user_id}`, // 用户名,真实环境可能需要获取用户信息
userId: item.user?.userId, userId: item.user_id,
avatar: item.user?.coverUrl || '',
createTime: new Date(item.created_at).toLocaleString(), createTime: new Date(item.created_at).toLocaleString(),
lastReplyTime: new Date(item.update_time).toLocaleString(), lastReplyTime: new Date(item.update_time).toLocaleString(),
status: convertStatusToString(item.status), status: convertStatusToString(item.status),
@@ -377,35 +368,44 @@ 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 () => { const fetchAllStats = async () => {
stats.isLoadingStats = true stats.isLoadingStats = true
try { try {
const res = await getTicketCount() // 并行获取各个状态的工单数量
if (res.code === 200) { await Promise.all([
const data = res.data fetchStatusStat(''), // 获取全部工单数量
fetchStatusStat('pending'), // 待处理
// 检查是否有新工单(待处理数量增加) fetchStatusStat('processing'), // 处理中
if (data.wait_count > previousPendingCount.value && previousPendingCount.value !== 0) { fetchStatusStat('replied'), // 已回复
try { fetchStatusStat('completed') // 已完成
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) { } catch (error) {
console.error('获取工单统计数据出错:', error) console.error('获取工单统计数据出错:', error)
} finally { } finally {
@@ -413,12 +413,18 @@ const fetchAllStats = async () => {
} }
} }
// 刷新统计数据(用于定时刷新) // 刷新当前分类的统计数据(用于定时刷新,减少请求
const fetchCurrentStatusStat = async () => { const fetchCurrentStatusStat = async () => {
await fetchAllStats() try {
// 只获取当前选中分类的统计数据
await fetchStatusStat(activeStatus.value)
// 同时获取全部工单数量(因为顶部显示需要)
await fetchStatusStat('')
} catch (error) {
console.error('获取当前分类统计数据出错:', error)
}
} }
// 加载更多工单 // 加载更多工单
const loadMoreTickets = () => { const loadMoreTickets = () => {
if (!hasMore.value || isLoading.value) return if (!hasMore.value || isLoading.value) return
@@ -470,9 +476,9 @@ const filteredTickets = computed(() => {
}) })
}) })
// 判断是否是当前登录的管理员 // 判断是否是客服
const isAdmin = (userId) => { const isAdmin = (userId) => {
return userId === userStore.userInfo?.user_id return adminUserIds.includes(userId)
} }
// 状态转换 // 状态转换
@@ -534,25 +540,25 @@ const fetchTicketMessages = async (workId) => {
} }
// 处理消息列表 // 处理消息列表
if (detail.content && detail.content.length > 0) { if (detail.Content && detail.Content.length > 0) {
// 处理所有消息 // 使用Promise.all一次性处理所有消息和图片
const messages = detail.content.map((msg) => { const messagesPromises = detail.Content.map(async (msg) => {
const isAdminMsg = isAdmin(msg.user?.userId) const isAdminMsg = isAdmin(msg.UserId)
// 从 flies 数组中提取图片 URL const images = await parseFilesToImages(msg.Flies)
const images = msg.flies ? msg.flies.map(file => file.url) : []
return { return {
id: msg.id, id: msg.Id,
content: msg.content !== 'empty' ? msg.content : null, content: msg.Content !== 'empty' ? msg.Content : null,
images: images, images: images,
time: msg.created_at || msg.updated_at || new Date().toLocaleString(), time: new Date(msg.CreatedAt).toLocaleString(),
isAdmin: isAdminMsg, isAdmin: isAdminMsg,
isSystem: false, isSystem: false,
userId: msg.user?.userId, userId: msg.UserId
avatar: msg.user?.coverUrl || ''
} }
}) })
// 等待所有消息处理完成
const messages = await Promise.all(messagesPromises)
currentMessages.value = messages currentMessages.value = messages
} }
} else { } else {
@@ -583,29 +589,23 @@ const sendMessage = async () => {
const fileIds = [] const fileIds = []
try { try {
// 保存输入内容 // 添加一个临时的"正在发送"消息
const inputMsg = messageInput.value.trim()
const inputImages = [...selectedImages.value]
// 清空输入和已选图片
messageInput.value = ''
selectedImages.value = []
// 立即添加消息到界面(不显示 loading)
const tempMsg = { const tempMsg = {
id: Date.now(), // 临时 ID content: messageInput.value.trim() || null,
content: inputMsg || null, images: selectedImages.value.length > 0 ? [...selectedImages.value] : null,
images: inputImages.length > 0 ? inputImages : [],
time: new Date().toLocaleString(), time: new Date().toLocaleString(),
isAdmin: true, isAdmin: true,
isSystem: false, isLoading: true,
userId: userStore.userInfo?.user_id,
avatar: userStore.userInfo?.cover_url || '',
isTempMessage: true isTempMessage: true
} }
currentMessages.value.push(tempMsg) currentMessages.value.push(tempMsg)
// 清空输入和已选图片
const inputMsg = messageInput.value
messageInput.value = ''
selectedImages.value = []
// 滚动到底部 // 滚动到底部
await nextTick() await nextTick()
scrollToBottom() scrollToBottom()
@@ -633,7 +633,6 @@ const sendMessage = async () => {
// 恢复输入内容 // 恢复输入内容
messageInput.value = inputMsg messageInput.value = inputMsg
selectedImages.value = inputImages
ElMessage.error(res.message || '发送失败') ElMessage.error(res.message || '发送失败')
} }
@@ -730,149 +729,42 @@ const updateTicketStats = () => {
// 格式化消息时间 // 格式化消息时间
const formatMessageTime = (timeStr) => { const formatMessageTime = (timeStr) => {
if (!timeStr) return '' const date = new Date(timeStr)
return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
try {
const date = new Date(timeStr)
if (isNaN(date.getTime())) return ''
// 格式化为 HH:MM
const hours = String(date.getHours()).padStart(2, '0')
const minutes = String(date.getMinutes()).padStart(2, '0')
return `${hours}:${minutes}`
} catch (e) {
console.error('时间格式化失败:', e)
return ''
}
} }
// 格式化列表项时间 // 格式化列表项时间
const formatTime = (timeStr) => { const formatTime = (timeStr) => {
if (!timeStr) return ''; // 空值兜底 const date = new Date(timeStr)
const now = new Date()
const diff = now - date
// 步骤1:解析中文时间字符串(核心适配点) // 今天内的消息只显示时间
let date; if (diff < 24 * 60 * 60 * 1000 && date.getDate() === now.getDate()) {
try { return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
// 先尝试原生解析(兼容ISO格式)
date = new Date(timeStr);
// 若原生解析失败(返回Invalid Date),解析中文格式
if (isNaN(date.getTime())) {
// 正则提取中文时间的年、月、日、时、分、秒
const cnTimeMatch = timeStr.match(
/(\d{4})年(\d{1,2})月(\d{1,2})日\s*(上午|下午)\s*(\d{1,2}):(\d{1,2}):(\d{1,2})/
);
if (cnTimeMatch) {
const [, year, month, day, period, hour, minute, second] = cnTimeMatch;
// 处理下午/上午的小时转换(12小时制转24小时制)
let hour24 = parseInt(hour, 10);
if (period === '下午' && hour24 !== 12) {
hour24 += 12;
}
if (period === '上午' && hour24 === 12) {
hour24 = 0; // 上午12点转为0点
}
// 构造日期(月份从0开始,需-1)
date = new Date(
parseInt(year, 10),
parseInt(month, 10) - 1,
parseInt(day, 10),
hour24,
parseInt(minute, 10),
parseInt(second, 10)
);
} else {
return '无效时间'; // 既不是ISO也不是中文格式
}
}
} catch (e) {
console.error('时间解析失败:', e);
return '无效时间';
} }
const now = new Date(); // 一周内的显示星期几
const dateTime = date.getTime(); if (diff < 7 * 24 * 60 * 60 * 1000) {
const nowTime = now.getTime(); const weekdays = ['周日', '周一', '周二', '周三', '周四', '周五', '周六']
const diff = nowTime - dateTime; return weekdays[date.getDay()]
// 步骤2:判断“今天”(年/月/日完全一致)
const isToday = date.getFullYear() === now.getFullYear() &&
date.getMonth() === now.getMonth() &&
date.getDate() === now.getDate();
if (isToday) {
// 格式化今天的时间(24小时制,补零)
const hour = String(date.getHours()).padStart(2, '0');
const minute = String(date.getMinutes()).padStart(2, '0');
return `${hour}:${minute}`;
} }
// 步骤3:判断“一周内” // 其他显示日期
const oneWeek = 7 * 24 * 60 * 60 * 1000; return date.toLocaleDateString()
if (diff < oneWeek) { }
const weekdays = ['周日', '周一', '周二', '周三', '周四', '周五', '周六'];
return weekdays[date.getDay()];
}
// 步骤4:格式化其他日期(补零,统一格式:YYYY/MM/DD)
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
return `${year}/${month}/${day}`;
};
// 格式化日期显示 // 格式化日期显示
const formatDate = (timeStr) => { const formatDate = (timeStr) => {
console.log("原始时间字符串:", timeStr); const date = new Date(timeStr)
if (!timeStr) return ''; // 空值兜底 const now = new Date()
let date; if (date.toDateString() === now.toDateString()) {
// 1. 先尝试原生解析(兼容ISO等标准格式) return '今天'
date = new Date(timeStr);
// 2. 若原生解析失败,专门解析中文时间格式
if (isNaN(date.getTime())) {
// 正则匹配:xxxx年xx月xx日 上午/下午 xx:xx:xx
const cnTimeReg = /(\d{4})年(\d{1,2})月(\d{1,2})日\s*(上午|下午)\s*(\d{1,2}):(\d{1,2}):(\d{1,2})/;
const match = timeStr.match(cnTimeReg);
if (match) {
const [, year, month, day, period, hour, minute, second] = match;
// 处理12小时制转24小时制(关键适配)
let hour24 = parseInt(hour, 10);
if (period === '下午') {
hour24 = hour24 === 12 ? 12 : hour24 + 12; // 下午12点=12点,下午1-11点+12
} else { // 上午
hour24 = hour24 === 12 ? 0 : hour24; // 上午12点=0点,上午1-11点不变
}
// 手动构造合法的Date对象(月份从0开始,需-1)
date = new Date(
parseInt(year, 10),
parseInt(month, 10) - 1,
parseInt(day, 10),
hour24,
parseInt(minute, 10),
parseInt(second, 10)
);
} else {
return '无效时间'; // 非目标格式,返回兜底
}
} }
const now = new Date(); return date.toLocaleDateString()
// 3. 对比“今天”(按日期维度,忽略时分秒) }
const isToday = date.getFullYear() === now.getFullYear() &&
date.getMonth() === now.getMonth() &&
date.getDate() === now.getDate();
if (isToday) {
return '今天';
}
// 4. 格式化非今天的日期(统一格式,避免环境差异)
const formattedDate = `${date.getFullYear()}/${String(date.getMonth() + 1).padStart(2, '0')}/${String(date.getDate()).padStart(2, '0')}`;
return formattedDate;
};
// 监听显示工单变化,更新消息和滚动 // 监听显示工单变化,更新消息和滚动
watch(currentTicket, (newVal) => { watch(currentTicket, (newVal) => {
@@ -946,9 +838,8 @@ const refreshTicketList = async () => {
const mappedTickets = tickets.map(item => ({ const mappedTickets = tickets.map(item => ({
id: item.work_id, id: item.work_id,
title: item.name, title: item.name,
username: item.user?.userName || `用户${item.user?.userId || 'Unknown'}`, username: `用户${item.user_id}`,
userId: item.user?.userId, userId: item.user_id,
avatar: item.user?.coverUrl || '',
createTime: new Date(item.created_at).toLocaleString(), createTime: new Date(item.created_at).toLocaleString(),
lastReplyTime: new Date(item.update_time).toLocaleString(), lastReplyTime: new Date(item.update_time).toLocaleString(),
status: convertStatusToString(item.status), status: convertStatusToString(item.status),
@@ -983,39 +874,6 @@ 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状态) // 静默刷新聊天记录(不显示loading状态)
const refreshTicketMessages = async (workId) => { const refreshTicketMessages = async (workId) => {
try { try {
@@ -1028,33 +886,37 @@ const refreshTicketMessages = async (workId) => {
if (currentTicket.value) { if (currentTicket.value) {
// 只有非待处理状态才直接更新,待处理状态保持不变,等待回复后再更新 // 只有非待处理状态才直接更新,待处理状态保持不变,等待回复后再更新
if (currentTicket.value.status !== 'pending') { if (currentTicket.value.status !== 'pending') {
currentTicket.value.status = convertStatusToString(detail.status) currentTicket.value.status = convertStatusToString(detail.Status)
} }
} }
// 处理消息列表 // 处理消息列表
if (detail.content && detail.content.length > 0) { if (detail.Content && detail.Content.length > 0) {
// 构建新消息列表 // 检查是否有新消息
const newMessages = detail.content.map((msg) => { const lastMsgId = currentMessages.value.length > 0 ?
const isAdminMsg = isAdmin(msg.user?.userId) currentMessages.value[currentMessages.value.length - 1].id : 0;
// 从 flies 数组中提取图片 URL const hasNewMessage = detail.Content.some(msg => msg.Id > lastMsgId);
const images = msg.flies ? msg.flies.map(file => file.url) : []
return { if (hasNewMessage) {
id: msg.id, // 有新消息时才更新
content: msg.content !== 'empty' ? msg.content : null, const messagesPromises = detail.Content.map(async (msg) => {
images: images, const isAdminMsg = isAdmin(msg.UserId)
time: msg.created_at || msg.updated_at || new Date().toLocaleString(), const images = await parseFilesToImages(msg.Flies)
isAdmin: isAdminMsg,
isSystem: false,
userId: msg.user?.userId,
avatar: msg.user?.coverUrl || ''
}
})
// 只有在消息真正发生变化时才更新(忽略 URL 查询参数的变化) return {
if (!areMessagesEqual(currentMessages.value, newMessages)) { id: msg.Id,
currentMessages.value = newMessages content: msg.Content !== 'empty' ? msg.Content : null,
images: images,
time: new Date(msg.CreatedAt).toLocaleString(),
isAdmin: isAdminMsg,
isSystem: false,
userId: msg.UserId
}
})
// 等待所有消息处理完成
const messages = await Promise.all(messagesPromises)
currentMessages.value = messages
// 如果有新消息,滚动到底部 // 如果有新消息,滚动到底部
nextTick(() => { nextTick(() => {
+22 -73
View File
@@ -304,37 +304,23 @@
</el-dialog> </el-dialog>
<!-- Token 展示对话框 --> <!-- Token 展示对话框 -->
<el-dialog v-model="tokenDialogVisible" title="模拟登录" width="450px" append-to-body> <el-dialog v-model="tokenDialogVisible" title="模拟登录 Token" width="600px" append-to-body>
<div class="token-container"> <div class="token-container">
<el-form label-width="100px"> <el-alert type="success" :closable="false" show-icon class="token-alert">
<el-form-item label="选择环境"> <template #default>
<el-select v-model="selectedEnvironment" placeholder="请选择登录环境" size="large" style="width: 100%"> <span>Token 生成成功请妥善保管</span>
<el-option label="正式环境" value="production"> </template>
<div class="env-option"> </el-alert>
<span>正式环境</span> <div class="token-box">
<span class="env-url">www.007yjs.com</span> <el-input v-model="loginToken" type="textarea" :rows="4" readonly class="token-input" />
</div> <div class="token-actions">
</el-option> <el-button type="primary" link :icon="CopyDocument" @click="copyToken">复制</el-button>
<el-option label="测试环境" value="test"> </div>
<div class="env-option"> </div>
<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> </div>
<template #footer> <template #footer>
<div class="dialog-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> </div>
</template> </template>
</el-dialog> </el-dialog>
@@ -378,7 +364,7 @@ import AvatarSelector from '@/components/admin/AvatarSelector.vue'
import { import {
ArrowLeft, Refresh, Edit as EditIcon, Delete, Wallet, Avatar, Lock, ArrowLeft, Refresh, Edit as EditIcon, Delete, Wallet, Avatar, Lock,
UserFilled, Document, Clock, List, Switch, User, Camera, Upload, UserFilled, Document, Clock, List, Switch, User, Camera, Upload,
UploadFilled, Key, Monitor, Setting UploadFilled, Key, CopyDocument
} from '@element-plus/icons-vue' } from '@element-plus/icons-vue'
import { getUserGroupList } from '@/api/admin/user' import { getUserGroupList } from '@/api/admin/user'
import { getFileDetail, getFileList, getFile, uploadFile } from '@/api/admin/file' import { getFileDetail, getFileList, getFile, uploadFile } from '@/api/admin/file'
@@ -479,15 +465,6 @@ const realnameForm = reactive({
// Token 展示相关 // Token 展示相关
const tokenDialogVisible = ref(false) const tokenDialogVisible = ref(false)
const loginToken = ref('') 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) const adminDialogVisible = ref(false)
@@ -752,10 +729,8 @@ const handleSimulateLogin = async () => {
const res = await mockUserLogin({ user_id: userInfo.value.UserId }) const res = await mockUserLogin({ user_id: userInfo.value.UserId })
if (res.data.code === 200) { if (res.data.code === 200) {
loginToken.value = res.data.data.token || '' loginToken.value = res.data.data.token || ''
loginExpire.value = res.data.data.expire_time || ''
selectedEnvironment.value = ''
tokenDialogVisible.value = true tokenDialogVisible.value = true
//ElMessage.success('模拟登录成功') ElMessage.success('模拟登录成功')
} else { } else {
ElMessage.error(res.data.message || '模拟登录失败') ElMessage.error(res.data.message || '模拟登录失败')
} }
@@ -764,18 +739,14 @@ const handleSimulateLogin = async () => {
} }
} }
// 确认跳转 // 复制 Token
const confirmJump = () => { const copyToken = async () => {
if (!selectedEnvironment.value) { try {
ElMessage.warning('请选择登录环境') await navigator.clipboard.writeText(loginToken.value)
return ElMessage.success('Token 已复制到剪贴板')
} catch (err) {
ElMessage.error('复制失败,请手动复制')
} }
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
} }
// 获取实名状态文本 // 获取实名状态文本
@@ -1195,28 +1166,6 @@ onActivated(() => {
font-size: 13px; 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 */ /* Responsive */
@media (max-width: 1024px) { @media (max-width: 1024px) {
.detail-grid { .detail-grid {