fix:将填写弹窗修改为选择弹窗
Build and Deploy Vue3 / build (push) Successful in 6m17s
Build and Deploy Vue3 / deploy (push) Successful in 1m25s

This commit is contained in:
2026-01-19 17:02:26 +08:00
parent cae89dd5ad
commit 36271b8bd0
13 changed files with 3171 additions and 25 deletions
+5
View File
@@ -17,7 +17,12 @@ store封装到src/store目录下。
注册侧边栏在/config/menus.js文件中。 注册侧边栏在/config/menus.js文件中。
新添加要求:
在遇到用户id需要填写和修改的弹窗将其修改为可预览样式
关于填写表单为推荐人id的需要使用组件AvatarSelector展示,如果是文件id或者是封面id 的也需要预览展示需要向头像列表组件一样,可以弄个文件组件/api/v1/admin/file/list这个是文件列表接口
规则:
1.只要涉及弹窗添加和修改xxxid类型的就需要生成一个弹窗组件并使用到页面中
## 1. 基础布局规范 ## 1. 基础布局规范
```css ```css
+3 -3
View File
@@ -37,9 +37,9 @@
:height="400" :height="400"
> >
<el-table-column type="index" label="序号" width="60" /> <el-table-column type="index" label="序号" width="60" />
<el-table-column prop="UserId" label="用户ID" width="100" /> <el-table-column prop="user_id" label="用户ID" width="100" />
<el-table-column prop="UserName" label="用户名" min-width="150" /> <el-table-column prop="user_name" label="用户名" min-width="150" />
<el-table-column prop="Email" label="邮箱" min-width="180" /> <el-table-column prop="email" label="邮箱" min-width="180" />
</el-table> </el-table>
@@ -0,0 +1,392 @@
<template>
<el-dialog
v-model="visible"
title="选择优惠码"
width="900px"
append-to-body
@close="handleClose"
>
<div class="discount-code-selector">
<el-tabs v-model="activeTab" @tab-click="handleTabClick">
<!-- 选择优惠码 -->
<el-tab-pane label="选择优惠码" name="selectCode">
<div class="code-list-container">
<!-- 搜索筛选区域 -->
<div class="filter-section">
<el-form :inline="true" :model="searchParams" class="search-form">
<el-form-item label="关键词">
<el-input
v-model="searchParams.key"
placeholder="搜索优惠码名称"
clearable
@keyup.enter="handleSearch"
style="width: 200px"
>
<template #prefix>
<el-icon><Search /></el-icon>
</template>
</el-input>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSearch" :icon="Search">
搜索
</el-button>
<el-button @click="handleReset" :icon="Refresh">
重置
</el-button>
</el-form-item>
</el-form>
</div>
<!-- 优惠码列表表格 -->
<el-table
v-loading="loading"
:data="codeList"
highlight-current-row
@current-change="handleCurrentChange"
style="width: 100%"
:height="350"
:row-class-name="tableRowClassName"
>
<el-table-column type="index" label="序号" width="60" align="center" />
<el-table-column prop="id" label="优惠码ID" width="100" align="center" />
<el-table-column prop="name" label="优惠码名称" min-width="120" show-overflow-tooltip />
<el-table-column prop="code" label="优惠码" width="150" show-overflow-tooltip>
<template #default="{ row }">
<el-tag type="success" effect="plain">{{ row.code }}</el-tag>
</template>
</el-table-column>
<el-table-column label="优惠类型" width="100" align="center">
<template #default="{ row }">
<el-tag :type="row.percentage > 0 ? 'warning' : 'primary'" size="small">
{{ row.percentage > 0 ? '折扣' : '固定金额' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="优惠值" width="100" align="right">
<template #default="{ row }">
<span v-if="row.percentage > 0" class="discount-value">{{ row.percentage }}%</span>
<span v-else class="discount-value">¥{{ (row.amount / 100).toFixed(2) }}</span>
</template>
</el-table-column>
<el-table-column label="最低消费" width="100" align="right">
<template #default="{ row }">
<span v-if="row.minAmount">¥{{ (row.minAmount / 100).toFixed(2) }}</span>
<span v-else>无限制</span>
</template>
</el-table-column>
<el-table-column label="使用次数" width="100" align="center">
<template #default="{ row }">
{{ row.userTimes || 0 }} / {{ row.maxTimes || '∞' }}
</template>
</el-table-column>
<el-table-column label="有效期" width="160" align="center">
<template #default="{ row }">
<span :class="{ 'expired': isExpired(row.endTime) }">
{{ formatDate(row.endTime) }}
</span>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<div class="pagination-container" v-if="total > 0">
<el-pagination
v-model:current-page="searchParams.page"
v-model:page-size="searchParams.count"
:page-sizes="[10, 20, 50, 100]"
:total="total"
layout="total, sizes, prev, pager, next, jumper"
background
@size-change="handleSizeChange"
@current-change="handlePageChange"
/>
</div>
<el-empty v-if="codeList.length === 0 && !loading" description="暂无优惠码数据" />
</div>
</el-tab-pane>
</el-tabs>
</div>
<template #footer>
<div class="dialog-footer">
<el-button @click="handleClose">取消</el-button>
<el-button
type="primary"
@click="handleConfirm"
:disabled="!selectedCode"
>
确定选择
</el-button>
</div>
</template>
</el-dialog>
</template>
<script setup>
import { ref, reactive, watch } from 'vue'
import { ElMessage } from 'element-plus'
import { Search, Refresh } from '@element-plus/icons-vue'
import { getDiscountCodeList } from '@/api/admin/discount'
// Props
const props = defineProps({
modelValue: {
type: Boolean,
default: false
},
// 当前已选中的优惠码ID(用于回显)
currentCodeId: {
type: [String, Number],
default: ''
},
// 类型过滤:discount_code - 优惠码
codeType: {
type: String,
default: 'code'
}
})
// Emits
const emit = defineEmits(['update:modelValue', 'confirm'])
// 响应式数据
const visible = ref(false)
const activeTab = ref('selectCode')
const loading = ref(false)
const codeList = ref([])
const total = ref(0)
const selectedCode = ref(null)
// 搜索参数
const searchParams = reactive({
key: '',
page: 1,
count: 10
})
// 监听 modelValue 变化
watch(() => props.modelValue, (newVal) => {
visible.value = newVal
if (newVal) {
// 重置状态
activeTab.value = 'selectCode'
selectedCode.value = null
searchParams.page = 1
fetchCodeList()
}
})
// 监听 visible 变化
watch(visible, (newVal) => {
emit('update:modelValue', newVal)
})
// 获取优惠码列表
const fetchCodeList = async () => {
loading.value = true
codeList.value = []
try {
const params = {
page: searchParams.page,
count: searchParams.count,
discount_type: props.codeType
}
if (searchParams.key) {
params.key = searchParams.key
}
const res = await getDiscountCodeList(params)
if (res.data.code === 200) {
codeList.value = res.data.data?.data || []
total.value = res.data.data?.all_count || 0
// 如果有当前选中的优惠码ID,自动选中
if (props.currentCodeId) {
const currentCode = codeList.value.find(
code => code.id === props.currentCodeId
)
if (currentCode) {
selectedCode.value = currentCode
}
}
} else {
ElMessage.error(res.data.msg || '获取优惠码列表失败')
}
} catch (error) {
console.error('获取优惠码列表失败:', error)
ElMessage.error('获取优惠码列表失败')
} finally {
loading.value = false
}
}
// 处理标签页切换
const handleTabClick = (tab) => {
if (tab.paneName === 'selectCode') {
fetchCodeList()
}
}
// 搜索
const handleSearch = () => {
searchParams.page = 1
fetchCodeList()
}
// 重置搜索
const handleReset = () => {
searchParams.key = ''
searchParams.page = 1
fetchCodeList()
}
// 分页处理
const handleSizeChange = (size) => {
searchParams.count = size
searchParams.page = 1
fetchCodeList()
}
const handlePageChange = (page) => {
searchParams.page = page
fetchCodeList()
}
// 选择优惠码
const handleCurrentChange = (row) => {
selectedCode.value = row
}
// 表格行样式
const tableRowClassName = ({ row }) => {
if (selectedCode.value && row.id === selectedCode.value.id) {
return 'selected-row'
}
return ''
}
// 关闭对话框
const handleClose = () => {
visible.value = false
selectedCode.value = null
codeList.value = []
searchParams.key = ''
searchParams.page = 1
total.value = 0
}
// 格式化日期
const formatDate = (dateStr) => {
if (!dateStr) return '-'
const date = new Date(dateStr)
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 isExpired = (endTime) => {
if (!endTime) return false
return new Date(endTime) < new Date()
}
// 确认选择
const handleConfirm = () => {
if (selectedCode.value) {
emit('confirm', selectedCode.value)
handleClose()
} else {
ElMessage.warning('请选择一个优惠码')
}
}
</script>
<style scoped>
.discount-code-selector {
min-height: 450px;
}
.code-list-container {
padding: 10px 0;
}
.filter-section {
margin-bottom: 16px;
padding: 16px;
background-color: #f5f7fa;
border-radius: 8px;
}
.search-form {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px;
}
.search-form :deep(.el-form-item) {
margin-bottom: 0;
margin-right: 12px;
}
.discount-value {
color: #e6a23c;
font-weight: 600;
}
.expired {
color: #f56c6c;
}
.pagination-container {
margin-top: 20px;
display: flex;
justify-content: flex-end;
}
.dialog-footer {
display: flex;
justify-content: flex-end;
gap: 10px;
}
/* 表格样式 */
:deep(.el-table__row) {
cursor: pointer;
}
:deep(.el-table__row:hover) {
background-color: #f5f7fa;
}
:deep(.selected-row) {
background-color: var(--el-color-primary-light-9) !important;
}
:deep(.selected-row td) {
background-color: var(--el-color-primary-light-9) !important;
}
:deep(.el-table__body tr.current-row > td) {
background-color: var(--el-color-primary-light-8) !important;
}
/* 标签页样式 */
:deep(.el-tabs__header) {
margin-bottom: 16px;
}
:deep(.el-tabs__item) {
font-size: 15px;
padding: 0 24px;
}
:deep(.el-tabs__item.is-active) {
font-weight: 600;
}
</style>
+385
View File
@@ -0,0 +1,385 @@
<template>
<el-dialog
v-model="visible"
title="选择商品"
width="900px"
append-to-body
@close="handleClose"
>
<div class="product-selector">
<el-tabs v-model="activeTab" @tab-click="handleTabClick">
<!-- 选择商品 -->
<el-tab-pane label="选择商品" name="selectProduct">
<div class="product-list-container">
<!-- 搜索筛选区域 -->
<div class="filter-section">
<el-form :inline="true" :model="searchParams" class="search-form">
<el-form-item label="商品分组">
<el-select
v-model="searchParams.good_group_id"
placeholder="全部分组"
clearable
style="width: 150px"
>
<el-option
v-for="item in groupOptions"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSearch" :icon="Search">
搜索
</el-button>
<el-button @click="handleReset" :icon="Refresh">
重置
</el-button>
</el-form-item>
</el-form>
</div>
<!-- 商品列表表格 -->
<el-table
v-loading="loading"
:data="productList"
highlight-current-row
@current-change="handleCurrentChange"
style="width: 100%"
:height="350"
:row-class-name="tableRowClassName"
>
<el-table-column type="index" label="序号" width="60" align="center" />
<el-table-column prop="id" label="商品ID" width="100" align="center" />
<el-table-column label="商品图片" width="80" align="center">
<template #default="{ row }">
<el-image
:src="row.image || '/logo.svg'"
fit="cover"
style="width: 50px; height: 50px; border-radius: 4px;"
/>
</template>
</el-table-column>
<el-table-column prop="name" label="商品名称" min-width="180" show-overflow-tooltip />
<el-table-column prop="table" label="所属表" width="120" show-overflow-tooltip />
<el-table-column label="价格" width="100" align="right">
<template #default="{ row }">
<span class="price">¥{{ (row.price / 100).toFixed(2) }}</span>
</template>
</el-table-column>
<el-table-column label="库存" width="80" align="center">
<template #default="{ row }">
<el-tag :type="row.inventory > 0 ? 'success' : 'danger'" size="small">
{{ row.inventory }}
</el-tag>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<div class="pagination-container" v-if="total > 0">
<el-pagination
v-model:current-page="searchParams.page"
v-model:page-size="searchParams.count"
:page-sizes="[10, 20, 50, 100]"
:total="total"
layout="total, sizes, prev, pager, next, jumper"
background
@size-change="handleSizeChange"
@current-change="handlePageChange"
/>
</div>
<el-empty v-if="productList.length === 0 && !loading" description="暂无商品数据" />
</div>
</el-tab-pane>
</el-tabs>
</div>
<template #footer>
<div class="dialog-footer">
<el-button @click="handleClose">取消</el-button>
<el-button
type="primary"
@click="handleConfirm"
:disabled="!selectedProduct"
>
确定选择
</el-button>
</div>
</template>
</el-dialog>
</template>
<script setup>
import { ref, reactive, watch } from 'vue'
import { ElMessage } from 'element-plus'
import { Search, Refresh } from '@element-plus/icons-vue'
import { getProductList, getProductGroupList } from '@/api/admin/product'
import { getFileDetail } from '@/api/admin/file'
// Props
const props = defineProps({
modelValue: {
type: Boolean,
default: false
},
// 当前已选中的商品ID(用于回显)
currentProductId: {
type: [String, Number],
default: ''
}
})
// Emits
const emit = defineEmits(['update:modelValue', 'confirm'])
// 响应式数据
const visible = ref(false)
const activeTab = ref('selectProduct')
const loading = ref(false)
const productList = ref([])
const groupOptions = ref([])
const total = ref(0)
const selectedProduct = ref(null)
// 搜索参数
const searchParams = reactive({
good_group_id: '',
page: 1,
count: 10
})
// 监听 modelValue 变化
watch(() => props.modelValue, (newVal) => {
visible.value = newVal
if (newVal) {
// 重置状态
activeTab.value = 'selectProduct'
selectedProduct.value = null
searchParams.page = 1
fetchGroupList()
fetchProductList()
}
})
// 监听 visible 变化
watch(visible, (newVal) => {
emit('update:modelValue', newVal)
})
// 获取商品分组列表
const fetchGroupList = async () => {
try {
const res = await getProductGroupList({ page: 1, count: 100 })
if (res.data.code === 200) {
groupOptions.value = res.data.data.data || []
}
} catch (error) {
console.error('获取分组列表失败:', error)
}
}
// 获取商品列表
const fetchProductList = async () => {
loading.value = true
productList.value = []
try {
const params = {
page: searchParams.page,
count: searchParams.count
}
if (searchParams.good_group_id) {
params.good_group_id = searchParams.good_group_id
}
const res = await getProductList(params)
if (res.data.code === 200) {
const allData = res.data.data.data || []
// 过滤掉已删除的数据
productList.value = allData.filter(item => item.delete == false)
total.value = productList.value.length
// 加载商品图片
for (let i = 0; i < productList.value.length; i++) {
if (productList.value[i].coverId) {
try {
const res2 = await getFileDetail({ file_id: productList.value[i].coverId })
if (res2.data.code === 200) {
productList.value[i].image = res2.data.data.url
}
} catch (error) {
console.error('获取商品图片失败:', error)
}
}
}
// 如果有当前选中的商品ID,自动选中
if (props.currentProductId) {
const currentProduct = productList.value.find(
product => product.id === props.currentProductId
)
if (currentProduct) {
selectedProduct.value = currentProduct
}
}
} else {
ElMessage.error(res.data.msg || '获取商品列表失败')
}
} catch (error) {
console.error('获取商品列表失败:', error)
ElMessage.error('获取商品列表失败')
} finally {
loading.value = false
}
}
// 处理标签页切换
const handleTabClick = (tab) => {
if (tab.paneName === 'selectProduct') {
fetchProductList()
}
}
// 搜索
const handleSearch = () => {
searchParams.page = 1
fetchProductList()
}
// 重置搜索
const handleReset = () => {
searchParams.good_group_id = ''
searchParams.page = 1
fetchProductList()
}
// 分页处理
const handleSizeChange = (size) => {
searchParams.count = size
searchParams.page = 1
fetchProductList()
}
const handlePageChange = (page) => {
searchParams.page = page
fetchProductList()
}
// 选择商品
const handleCurrentChange = (row) => {
selectedProduct.value = row
}
// 表格行样式
const tableRowClassName = ({ row }) => {
if (selectedProduct.value && row.id === selectedProduct.value.id) {
return 'selected-row'
}
return ''
}
// 关闭对话框
const handleClose = () => {
visible.value = false
selectedProduct.value = null
productList.value = []
searchParams.good_group_id = ''
searchParams.page = 1
total.value = 0
}
// 确认选择
const handleConfirm = () => {
if (selectedProduct.value) {
emit('confirm', selectedProduct.value)
handleClose()
} else {
ElMessage.warning('请选择一个商品')
}
}
</script>
<style scoped>
.product-selector {
min-height: 450px;
}
.product-list-container {
padding: 10px 0;
}
.filter-section {
margin-bottom: 16px;
padding: 16px;
background-color: #f5f7fa;
border-radius: 8px;
}
.search-form {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px;
}
.search-form :deep(.el-form-item) {
margin-bottom: 0;
margin-right: 12px;
}
.price {
color: #f56c6c;
font-weight: 600;
}
.pagination-container {
margin-top: 20px;
display: flex;
justify-content: flex-end;
}
.dialog-footer {
display: flex;
justify-content: flex-end;
gap: 10px;
}
/* 表格样式 */
:deep(.el-table__row) {
cursor: pointer;
}
:deep(.el-table__row:hover) {
background-color: #f5f7fa;
}
:deep(.selected-row) {
background-color: var(--el-color-primary-light-9) !important;
}
:deep(.selected-row td) {
background-color: var(--el-color-primary-light-9) !important;
}
:deep(.el-table__body tr.current-row > td) {
background-color: var(--el-color-primary-light-8) !important;
}
/* 标签页样式 */
:deep(.el-tabs__header) {
margin-bottom: 16px;
}
:deep(.el-tabs__item) {
font-size: 15px;
padding: 0 24px;
}
:deep(.el-tabs__item.is-active) {
font-weight: 600;
}
</style>
+408
View File
@@ -0,0 +1,408 @@
<template>
<el-dialog
v-model="visible"
title="选择用户组"
width="900px"
append-to-body
@close="handleClose"
>
<div class="user-group-selector">
<el-tabs v-model="activeTab" @tab-click="handleTabClick">
<!-- 选择用户组 -->
<el-tab-pane label="选择用户组" name="selectGroup">
<div class="group-list-container">
<!-- 搜索筛选区域 -->
<div class="filter-section">
<el-form :inline="true" :model="searchParams" class="search-form">
<el-form-item label="关键词">
<el-input
v-model="searchParams.key"
placeholder="搜索用户组名称"
clearable
@keyup.enter="handleSearch"
style="width: 200px"
>
<template #prefix>
<el-icon><Search /></el-icon>
</template>
</el-input>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSearch" :icon="Search">
搜索
</el-button>
<el-button @click="handleReset" :icon="Refresh">
重置
</el-button>
</el-form-item>
</el-form>
</div>
<!-- 用户组列表表格 -->
<el-table
v-loading="loading"
:data="groupList"
highlight-current-row
@current-change="handleCurrentChange"
style="width: 100%"
:height="350"
:row-class-name="tableRowClassName"
>
<el-table-column type="index" label="序号" width="60" align="center" />
<el-table-column label="组ID" width="100" align="center">
<template #default="{ row }">
{{ row.group_id || row.GroupId || row.id || row.Id }}
</template>
</el-table-column>
<el-table-column label="组名称" min-width="150" show-overflow-tooltip>
<template #default="{ row }">
<span class="group-name">{{ row.group_name || row.name || row.Name }}</span>
</template>
</el-table-column>
<el-table-column label="升级金额" width="120" align="right">
<template #default="{ row }">
<span v-if="row.floor_price || row.FloorPrice" class="price-text">
¥{{ row.floor_price || row.FloorPrice }}
</span>
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column label="下一级组ID" width="100" align="center">
<template #default="{ row }">
{{ row.higher_level_id || row.HigherLevelId || '-' }}
</template>
</el-table-column>
<el-table-column label="类型" width="100" align="center">
<template #default="{ row }">
<el-tag :type="(row.fixed || row.Fixed) ? 'warning' : 'success'" size="small">
{{ (row.fixed || row.Fixed) ? '固定' : '可升级' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="成员数量" width="100" align="center">
<template #default="{ row }">
<el-tag type="info" size="small" effect="plain">
{{ row.member_count || row.MemberCount || 0 }}
</el-tag>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<div class="pagination-container" v-if="total > 0">
<el-pagination
v-model:current-page="searchParams.page"
v-model:page-size="searchParams.count"
:page-sizes="[10, 20, 50, 100]"
:total="total"
layout="total, sizes, prev, pager, next, jumper"
background
@size-change="handleSizeChange"
@current-change="handlePageChange"
/>
</div>
<el-empty v-if="groupList.length === 0 && !loading" description="暂无用户组数据" />
</div>
</el-tab-pane>
</el-tabs>
</div>
<template #footer>
<div class="dialog-footer">
<el-button @click="handleClose">取消</el-button>
<el-button
type="primary"
@click="handleConfirm"
:disabled="!selectedGroup"
>
确定选择
</el-button>
</div>
</template>
</el-dialog>
</template>
<script setup>
import { ref, reactive, watch } from 'vue'
import { ElMessage } from 'element-plus'
import { Search, Refresh } from '@element-plus/icons-vue'
import { getUserGroupList } from '@/api/admin/user'
// Props
const props = defineProps({
modelValue: {
type: Boolean,
default: false
},
// 当前已选中的用户组ID(用于回显)
currentGroupId: {
type: [String, Number],
default: ''
},
// 排除的用户组ID(避免选择自己作为下一级)
excludeGroupId: {
type: [String, Number],
default: ''
}
})
// Emits
const emit = defineEmits(['update:modelValue', 'confirm'])
// 响应式数据
const visible = ref(false)
const activeTab = ref('selectGroup')
const loading = ref(false)
const groupList = ref([])
const total = ref(0)
const selectedGroup = ref(null)
// 搜索参数
const searchParams = reactive({
key: '',
page: 1,
count: 10
})
// 监听 modelValue 变化
watch(() => props.modelValue, (newVal) => {
visible.value = newVal
if (newVal) {
// 重置状态
activeTab.value = 'selectGroup'
selectedGroup.value = null
searchParams.page = 1
fetchGroupList()
}
})
// 监听 visible 变化
watch(visible, (newVal) => {
emit('update:modelValue', newVal)
})
// 获取用户组列表
const fetchGroupList = async () => {
loading.value = true
groupList.value = []
try {
const params = {
page: searchParams.page,
count: searchParams.count
}
const res = await getUserGroupList(params)
if (res.data.code === 200) {
let responseData = res.data?.data || res.data
if (Array.isArray(responseData)) {
groupList.value = responseData
total.value = responseData.length
} else if (responseData.list) {
groupList.value = responseData.list || []
total.value = responseData.total || responseData.all_count || 0
} else if (responseData.data && Array.isArray(responseData.data)) {
groupList.value = responseData.data
total.value = responseData.all_count || responseData.data.length
} else {
groupList.value = []
total.value = 0
}
// 过滤掉排除的用户组
if (props.excludeGroupId) {
groupList.value = groupList.value.filter(item => {
const itemId = item.group_id || item.GroupId || item.id || item.Id
return itemId !== props.excludeGroupId
})
}
// 关键词过滤
if (searchParams.key) {
const keyword = searchParams.key.toLowerCase()
groupList.value = groupList.value.filter(item => {
const name = (item.group_name || item.name || item.Name || '').toLowerCase()
const id = String(item.group_id || item.GroupId || item.id || item.Id)
return name.includes(keyword) || id.includes(keyword)
})
}
// 如果有当前选中的用户组ID,自动选中
if (props.currentGroupId) {
const currentGroup = groupList.value.find(item => {
const itemId = item.group_id || item.GroupId || item.id || item.Id
return itemId === props.currentGroupId
})
if (currentGroup) {
selectedGroup.value = currentGroup
}
}
} else {
ElMessage.error(res.data.msg || '获取用户组列表失败')
}
} catch (error) {
console.error('获取用户组列表失败:', error)
ElMessage.error('获取用户组列表失败')
} finally {
loading.value = false
}
}
// 处理标签页切换
const handleTabClick = (tab) => {
if (tab.paneName === 'selectGroup') {
fetchGroupList()
}
}
// 搜索
const handleSearch = () => {
searchParams.page = 1
fetchGroupList()
}
// 重置搜索
const handleReset = () => {
searchParams.key = ''
searchParams.page = 1
fetchGroupList()
}
// 分页处理
const handleSizeChange = (size) => {
searchParams.count = size
searchParams.page = 1
fetchGroupList()
}
const handlePageChange = (page) => {
searchParams.page = page
fetchGroupList()
}
// 选择用户组
const handleCurrentChange = (row) => {
selectedGroup.value = row
}
// 表格行样式
const tableRowClassName = ({ row }) => {
if (selectedGroup.value) {
const selectedId = selectedGroup.value.group_id || selectedGroup.value.GroupId || selectedGroup.value.id || selectedGroup.value.Id
const rowId = row.group_id || row.GroupId || row.id || row.Id
if (rowId === selectedId) {
return 'selected-row'
}
}
return ''
}
// 关闭对话框
const handleClose = () => {
visible.value = false
selectedGroup.value = null
groupList.value = []
searchParams.key = ''
searchParams.page = 1
total.value = 0
}
// 确认选择
const handleConfirm = () => {
if (selectedGroup.value) {
emit('confirm', selectedGroup.value)
handleClose()
} else {
ElMessage.warning('请选择一个用户组')
}
}
</script>
<style scoped>
.user-group-selector {
min-height: 450px;
}
.group-list-container {
padding: 10px 0;
}
.filter-section {
margin-bottom: 16px;
padding: 16px;
background-color: #f5f7fa;
border-radius: 8px;
}
.search-form {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px;
}
.search-form :deep(.el-form-item) {
margin-bottom: 0;
margin-right: 12px;
}
.group-name {
font-weight: 500;
color: #2c3e50;
}
.price-text {
color: #f56c6c;
font-weight: 500;
}
.pagination-container {
margin-top: 20px;
display: flex;
justify-content: flex-end;
}
.dialog-footer {
display: flex;
justify-content: flex-end;
gap: 10px;
}
/* 表格样式 */
:deep(.el-table__row) {
cursor: pointer;
}
:deep(.el-table__row:hover) {
background-color: #f5f7fa;
}
:deep(.selected-row) {
background-color: var(--el-color-primary-light-9) !important;
}
:deep(.selected-row td) {
background-color: var(--el-color-primary-light-9) !important;
}
:deep(.el-table__body tr.current-row > td) {
background-color: var(--el-color-primary-light-8) !important;
}
/* 标签页样式 */
:deep(.el-tabs__header) {
margin-bottom: 16px;
}
:deep(.el-tabs__item) {
font-size: 15px;
padding: 0 24px;
}
:deep(.el-tabs__item.is-active) {
font-weight: 600;
}
</style>
+567
View File
@@ -0,0 +1,567 @@
<template>
<el-dialog
v-model="visible"
title="选择用户"
width="900px"
append-to-body
@close="handleClose"
>
<div class="user-selector">
<el-tabs v-model="activeTab" @tab-click="handleTabClick">
<!-- 选择用户 -->
<el-tab-pane label="选择用户" name="selectUser">
<div class="user-list-container">
<!-- 搜索筛选区域 -->
<div class="filter-section">
<el-form :inline="true" :model="searchParams" class="search-form">
<el-form-item label="关键词">
<el-input
v-model="searchParams.key"
placeholder="搜索用户名或ID"
clearable
@keyup.enter="handleSearch"
style="width: 200px"
>
<template #prefix>
<el-icon><Search /></el-icon>
</template>
</el-input>
</el-form-item>
<el-form-item label="用户状态">
<el-select
v-model="searchParams.status"
placeholder="全部状态"
clearable
style="width: 120px"
>
<el-option label="正常" value="1" />
<el-option label="禁用" value="0" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSearch" :icon="Search">
搜索
</el-button>
<el-button @click="handleReset" :icon="Refresh">
重置
</el-button>
<el-button type="success" @click="switchToAdd" :icon="Plus">
添加新用户
</el-button>
</el-form-item>
</el-form>
</div>
<!-- 用户列表表格 -->
<el-table
v-loading="loading"
:data="userList"
highlight-current-row
@current-change="handleCurrentChange"
style="width: 100%"
:height="350"
:row-class-name="tableRowClassName"
>
<el-table-column type="index" label="序号" width="60" align="center" />
<el-table-column prop="user_id" label="用户ID" width="100" align="center" />
<el-table-column prop="user_name" label="用户名" min-width="120">
<template #default="{ row }">
<div class="user-info-cell">
<el-avatar :size="32" :src="row.cover">
<el-icon><User /></el-icon>
</el-avatar>
<span class="user-name">{{ row.user_name }}</span>
</div>
</template>
</el-table-column>
<el-table-column prop="email" label="邮箱" min-width="180" show-overflow-tooltip />
<el-table-column prop="phone" label="手机号" width="130" show-overflow-tooltip />
<!-- <el-table-column prop="status" label="状态" width="80" align="center">
<template #default="{ row }">
<el-tag :type="row.status === 1 ? 'success' : 'danger'" size="small">
{{ row.status === 1 ? '正常' : '禁用' }}
</el-tag>
</template>
</el-table-column> -->
<el-table-column prop="created_at" label="注册时间" width="160" show-overflow-tooltip />
</el-table>
<!-- 分页 -->
<div class="pagination-container" v-if="total > 0">
<el-pagination
v-model:current-page="searchParams.page"
v-model:page-size="searchParams.count"
:page-sizes="[10, 20, 50, 100]"
:total="total"
layout="total, sizes, prev, pager, next, jumper"
background
@size-change="handleSizeChange"
@current-change="handlePageChange"
/>
</div>
<el-empty v-if="userList.length === 0 && !loading" description="暂无用户数据" />
</div>
</el-tab-pane>
<!-- 添加用户 -->
<el-tab-pane label="添加用户" name="addUser">
<div class="add-user-section">
<el-form
ref="addFormRef"
:model="addForm"
:rules="addFormRules"
label-width="100px"
class="add-user-form"
>
<el-form-item label="用户名" prop="user_name">
<el-input
v-model="addForm.user_name"
placeholder="请输入用户名"
maxlength="50"
show-word-limit
/>
</el-form-item>
<el-form-item label="邮箱" prop="email">
<el-input
v-model="addForm.email"
placeholder="请输入邮箱地址"
type="email"
/>
</el-form-item>
<el-form-item label="手机号" prop="phone">
<el-input
v-model="addForm.phone"
placeholder="请输入手机号"
maxlength="11"
/>
</el-form-item>
<el-form-item label="密码" prop="password">
<el-input
v-model="addForm.password"
placeholder="请输入密码"
type="password"
show-password
/>
</el-form-item>
<el-form-item label="确认密码" prop="confirmPassword">
<el-input
v-model="addForm.confirmPassword"
placeholder="请再次输入密码"
type="password"
show-password
/>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleAddUser" :loading="addLoading">
<el-icon><Plus /></el-icon>
立即创建
</el-button>
<el-button @click="resetAddForm">
<el-icon><Refresh /></el-icon>
重置表单
</el-button>
</el-form-item>
</el-form>
</div>
</el-tab-pane>
</el-tabs>
</div>
<template #footer>
<div class="dialog-footer">
<el-button @click="handleClose">取消</el-button>
<el-button
type="primary"
@click="handleConfirm"
:disabled="!selectedUser"
v-if="activeTab === 'selectUser'"
>
确定选择
</el-button>
</div>
</template>
</el-dialog>
</template>
<script setup>
import { ref, reactive, watch } from 'vue'
import { ElMessage } from 'element-plus'
import { Search, Refresh, Plus, User } from '@element-plus/icons-vue'
import { getUserList, createTask } from '@/api/admin/user'
// Props
const props = defineProps({
modelValue: {
type: Boolean,
default: false
},
// 当前已选中的用户ID(用于回显)
currentUserId: {
type: [String, Number],
default: ''
}
})
// Emits
const emit = defineEmits(['update:modelValue', 'confirm'])
// 响应式数据
const visible = ref(false)
const activeTab = ref('selectUser')
const loading = ref(false)
const addLoading = ref(false)
const userList = ref([])
const total = ref(0)
const selectedUser = ref(null)
const addFormRef = ref(null)
// 搜索参数
const searchParams = reactive({
key: '',
status: '',
page: 1,
count: 10
})
// 添加用户表单
const addForm = reactive({
user_name: '',
email: '',
phone: '',
password: '',
confirmPassword: ''
})
// 密码确认验证
const validateConfirmPassword = (rule, value, callback) => {
if (value === '') {
callback(new Error('请再次输入密码'))
} else if (value !== addForm.password) {
callback(new Error('两次输入密码不一致'))
} else {
callback()
}
}
// 添加用户表单验证规则
const addFormRules = {
user_name: [
{ required: true, message: '请输入用户名', trigger: 'blur' },
{ min: 2, max: 50, message: '用户名长度在 2 到 50 个字符', trigger: 'blur' }
],
email: [
{ required: true, message: '请输入邮箱地址', trigger: 'blur' },
{ type: 'email', message: '请输入正确的邮箱地址', trigger: 'blur' }
],
phone: [
{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号', trigger: 'blur' }
],
password: [
{ required: true, message: '请输入密码', trigger: 'blur' },
{ min: 6, max: 20, message: '密码长度在 6 到 20 个字符', trigger: 'blur' }
],
confirmPassword: [
{ required: true, message: '请再次输入密码', trigger: 'blur' },
{ validator: validateConfirmPassword, trigger: 'blur' }
]
}
// 监听 modelValue 变化
watch(() => props.modelValue, (newVal) => {
visible.value = newVal
if (newVal) {
// 重置状态
activeTab.value = 'selectUser'
selectedUser.value = null
searchParams.page = 1
fetchUserList()
}
})
// 监听 visible 变化
watch(visible, (newVal) => {
emit('update:modelValue', newVal)
})
// 获取用户列表
const fetchUserList = async () => {
loading.value = true
userList.value = []
try {
const params = {
page: searchParams.page,
count: searchParams.count,
key: searchParams.key || ''
}
const res = await getUserList(params)
if (res.data.code === 200) {
userList.value = res.data.data?.data || []
total.value = res.data.data?.all_count || 0
// 如果有当前选中的用户ID,自动选中
if (props.currentUserId) {
const currentUser = userList.value.find(
user => user.user_id === props.currentUserId
)
if (currentUser) {
selectedUser.value = currentUser
}
}
} else {
ElMessage.error(res.data.msg || '获取用户列表失败')
}
} catch (error) {
console.error('获取用户列表失败:', error)
ElMessage.error('获取用户列表失败')
} finally {
loading.value = false
}
}
// 处理标签页切换
const handleTabClick = (tab) => {
if (tab.paneName === 'selectUser') {
fetchUserList()
}
}
// 搜索
const handleSearch = () => {
searchParams.page = 1
fetchUserList()
}
// 重置搜索
const handleReset = () => {
searchParams.key = ''
searchParams.status = ''
searchParams.page = 1
fetchUserList()
}
// 分页处理
const handleSizeChange = (size) => {
searchParams.count = size
searchParams.page = 1
fetchUserList()
}
const handlePageChange = (page) => {
searchParams.page = page
fetchUserList()
}
// 切换到添加用户标签页
const switchToAdd = () => {
activeTab.value = 'addUser'
}
// 选择用户
const handleCurrentChange = (row) => {
selectedUser.value = row
}
// 表格行样式
const tableRowClassName = ({ row }) => {
if (selectedUser.value && row.user_id === selectedUser.value.user_id) {
return 'selected-row'
}
return ''
}
// 添加用户
const handleAddUser = async () => {
if (!addFormRef.value) return
await addFormRef.value.validate(async (valid) => {
if (!valid) return
addLoading.value = true
try {
const formData = new FormData()
formData.append('user_name', addForm.user_name)
formData.append('email', addForm.email)
if (addForm.phone) {
formData.append('phone', addForm.phone)
}
formData.append('password', addForm.password)
const res = await createTask(formData)
if (res.data.code === 200) {
ElMessage.success('用户创建成功')
// 获取新创建的用户信息
const newUser = res.data.data
// 自动选择新创建的用户
if (newUser) {
selectedUser.value = {
user_id: newUser.user_id || newUser.id,
user_name: newUser.user_name || addForm.user_name,
email: newUser.email || addForm.email,
phone: newUser.phone || addForm.phone,
...newUser
}
// 触发确认事件并关闭弹窗
emit('confirm', selectedUser.value)
handleClose()
} else {
// 如果没有返回用户信息,切换到选择标签页并刷新列表
activeTab.value = 'selectUser'
searchParams.page = 1
await fetchUserList()
}
// 重置表单
resetAddForm()
} else {
ElMessage.error(res.data.msg || '用户创建失败')
}
} catch (error) {
console.error('用户创建失败:', error)
ElMessage.error('用户创建失败')
} finally {
addLoading.value = false
}
})
}
// 重置添加表单
const resetAddForm = () => {
addForm.user_name = ''
addForm.email = ''
addForm.phone = ''
addForm.password = ''
addForm.confirmPassword = ''
addFormRef.value?.resetFields()
}
// 关闭对话框
const handleClose = () => {
visible.value = false
selectedUser.value = null
userList.value = []
searchParams.key = ''
searchParams.status = ''
searchParams.page = 1
total.value = 0
resetAddForm()
}
// 确认选择
const handleConfirm = () => {
if (selectedUser.value) {
emit('confirm', selectedUser.value)
handleClose()
} else {
ElMessage.warning('请选择一个用户')
}
}
</script>
<style scoped>
.user-selector {
min-height: 450px;
}
.user-list-container {
padding: 10px 0;
}
.filter-section {
margin-bottom: 16px;
padding: 16px;
background-color: #f5f7fa;
border-radius: 8px;
}
.search-form {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px;
}
.search-form :deep(.el-form-item) {
margin-bottom: 0;
margin-right: 12px;
}
.user-info-cell {
display: flex;
align-items: center;
gap: 10px;
}
.user-name {
font-weight: 500;
color: #303133;
}
.pagination-container {
margin-top: 20px;
display: flex;
justify-content: flex-end;
}
.add-user-section {
padding: 30px 60px;
}
.add-user-form {
max-width: 500px;
margin: 0 auto;
}
.add-user-form :deep(.el-input) {
width: 100%;
}
.dialog-footer {
display: flex;
justify-content: flex-end;
gap: 10px;
}
/* 表格样式 */
:deep(.el-table__row) {
cursor: pointer;
}
:deep(.el-table__row:hover) {
background-color: #f5f7fa;
}
:deep(.selected-row) {
background-color: var(--el-color-primary-light-9) !important;
}
:deep(.selected-row td) {
background-color: var(--el-color-primary-light-9) !important;
}
:deep(.el-table__body tr.current-row > td) {
background-color: var(--el-color-primary-light-8) !important;
}
/* 标签页样式 */
:deep(.el-tabs__header) {
margin-bottom: 16px;
}
:deep(.el-tabs__item) {
font-size: 15px;
padding: 0 24px;
}
:deep(.el-tabs__item.is-active) {
font-weight: 600;
}
</style>
+389
View File
@@ -0,0 +1,389 @@
<template>
<el-dialog
v-model="visible"
title="选择代金券"
width="900px"
append-to-body
@close="handleClose"
>
<div class="voucher-selector">
<el-tabs v-model="activeTab" @tab-click="handleTabClick">
<!-- 选择代金券 -->
<el-tab-pane label="选择代金券" name="selectVoucher">
<div class="voucher-list-container">
<!-- 搜索筛选区域 -->
<div class="filter-section">
<el-form :inline="true" :model="searchParams" class="search-form">
<el-form-item label="关键词">
<el-input
v-model="searchParams.key"
placeholder="搜索代金券名称"
clearable
@keyup.enter="handleSearch"
style="width: 200px"
>
<template #prefix>
<el-icon><Search /></el-icon>
</template>
</el-input>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSearch" :icon="Search">
搜索
</el-button>
<el-button @click="handleReset" :icon="Refresh">
重置
</el-button>
</el-form-item>
</el-form>
</div>
<!-- 代金券列表表格 -->
<el-table
v-loading="loading"
:data="voucherList"
highlight-current-row
@current-change="handleCurrentChange"
style="width: 100%"
:height="350"
:row-class-name="tableRowClassName"
>
<el-table-column type="index" label="序号" width="60" align="center" />
<el-table-column prop="id" label="代金券ID" width="100" align="center" />
<el-table-column prop="name" label="代金券名称" min-width="120" show-overflow-tooltip />
<el-table-column prop="code" label="代金券码" width="150" show-overflow-tooltip>
<template #default="{ row }">
<el-tag type="warning" effect="plain">{{ row.code }}</el-tag>
</template>
</el-table-column>
<el-table-column label="优惠类型" width="100" align="center">
<template #default="{ row }">
<el-tag :type="row.percentage > 0 ? 'warning' : 'primary'" size="small">
{{ row.percentage > 0 ? '折扣' : '固定金额' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="面值" width="100" align="right">
<template #default="{ row }">
<span v-if="row.percentage > 0" class="voucher-value">{{ row.percentage }}%</span>
<span v-else class="voucher-value">¥{{ (row.amount / 100).toFixed(2) }}</span>
</template>
</el-table-column>
<el-table-column label="最低消费" width="100" align="right">
<template #default="{ row }">
<span v-if="row.minAmount">¥{{ (row.minAmount / 100).toFixed(2) }}</span>
<span v-else>无限制</span>
</template>
</el-table-column>
<el-table-column label="使用次数" width="100" align="center">
<template #default="{ row }">
{{ row.userTimes || 0 }} / {{ row.maxTimes || '∞' }}
</template>
</el-table-column>
<el-table-column label="有效期" width="160" align="center">
<template #default="{ row }">
<span :class="{ 'expired': isExpired(row.endTime) }">
{{ formatDate(row.endTime) }}
</span>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<div class="pagination-container" v-if="total > 0">
<el-pagination
v-model:current-page="searchParams.page"
v-model:page-size="searchParams.count"
:page-sizes="[10, 20, 50, 100]"
:total="total"
layout="total, sizes, prev, pager, next, jumper"
background
@size-change="handleSizeChange"
@current-change="handlePageChange"
/>
</div>
<el-empty v-if="voucherList.length === 0 && !loading" description="暂无代金券数据" />
</div>
</el-tab-pane>
</el-tabs>
</div>
<template #footer>
<div class="dialog-footer">
<el-button @click="handleClose">取消</el-button>
<el-button
type="primary"
@click="handleConfirm"
:disabled="!selectedVoucher"
>
确定选择
</el-button>
</div>
</template>
</el-dialog>
</template>
<script setup>
import { ref, reactive, watch } from 'vue'
import { ElMessage } from 'element-plus'
import { Search, Refresh } from '@element-plus/icons-vue'
import { getDiscountCodeList } from '@/api/admin/discount'
// Props
const props = defineProps({
modelValue: {
type: Boolean,
default: false
},
// 当前已选中的代金券ID(用于回显)
currentVoucherId: {
type: [String, Number],
default: ''
}
})
// Emits
const emit = defineEmits(['update:modelValue', 'confirm'])
// 响应式数据
const visible = ref(false)
const activeTab = ref('selectVoucher')
const loading = ref(false)
const voucherList = ref([])
const total = ref(0)
const selectedVoucher = ref(null)
// 搜索参数
const searchParams = reactive({
key: '',
page: 1,
count: 10
})
// 监听 modelValue 变化
watch(() => props.modelValue, (newVal) => {
visible.value = newVal
if (newVal) {
// 重置状态
activeTab.value = 'selectVoucher'
selectedVoucher.value = null
searchParams.page = 1
fetchVoucherList()
}
})
// 监听 visible 变化
watch(visible, (newVal) => {
emit('update:modelValue', newVal)
})
// 获取代金券列表
const fetchVoucherList = async () => {
loading.value = true
voucherList.value = []
try {
const params = {
page: searchParams.page,
count: searchParams.count,
discount_type: 'coupon' // 代金券类型
}
if (searchParams.key) {
params.key = searchParams.key
}
const res = await getDiscountCodeList(params)
if (res.data.code === 200) {
voucherList.value = res.data.data?.data || []
total.value = res.data.data?.all_count || 0
// 如果有当前选中的代金券ID,自动选中
if (props.currentVoucherId) {
const currentVoucher = voucherList.value.find(
voucher => voucher.id === props.currentVoucherId
)
if (currentVoucher) {
selectedVoucher.value = currentVoucher
}
}
} else {
ElMessage.error(res.data.msg || '获取代金券列表失败')
}
} catch (error) {
console.error('获取代金券列表失败:', error)
ElMessage.error('获取代金券列表失败')
} finally {
loading.value = false
}
}
// 处理标签页切换
const handleTabClick = (tab) => {
if (tab.paneName === 'selectVoucher') {
fetchVoucherList()
}
}
// 搜索
const handleSearch = () => {
searchParams.page = 1
fetchVoucherList()
}
// 重置搜索
const handleReset = () => {
searchParams.key = ''
searchParams.page = 1
fetchVoucherList()
}
// 分页处理
const handleSizeChange = (size) => {
searchParams.count = size
searchParams.page = 1
fetchVoucherList()
}
const handlePageChange = (page) => {
searchParams.page = page
fetchVoucherList()
}
// 选择代金券
const handleCurrentChange = (row) => {
selectedVoucher.value = row
}
// 表格行样式
const tableRowClassName = ({ row }) => {
if (selectedVoucher.value && row.id === selectedVoucher.value.id) {
return 'selected-row'
}
return ''
}
// 关闭对话框
const handleClose = () => {
visible.value = false
selectedVoucher.value = null
voucherList.value = []
searchParams.key = ''
searchParams.page = 1
total.value = 0
}
// 格式化日期
const formatDate = (dateStr) => {
if (!dateStr) return '-'
const date = new Date(dateStr)
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 isExpired = (endTime) => {
if (!endTime) return false
return new Date(endTime) < new Date()
}
// 确认选择
const handleConfirm = () => {
if (selectedVoucher.value) {
emit('confirm', selectedVoucher.value)
handleClose()
} else {
ElMessage.warning('请选择一个代金券')
}
}
</script>
<style scoped>
.voucher-selector {
min-height: 450px;
}
.voucher-list-container {
padding: 10px 0;
}
.filter-section {
margin-bottom: 16px;
padding: 16px;
background-color: #f5f7fa;
border-radius: 8px;
}
.search-form {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px;
}
.search-form :deep(.el-form-item) {
margin-bottom: 0;
margin-right: 12px;
}
.voucher-value {
color: #f56c6c;
font-weight: 600;
font-size: 15px;
}
.expired {
color: #909399;
text-decoration: line-through;
}
.pagination-container {
margin-top: 20px;
display: flex;
justify-content: flex-end;
}
.dialog-footer {
display: flex;
justify-content: flex-end;
gap: 10px;
}
/* 表格样式 */
:deep(.el-table__row) {
cursor: pointer;
}
:deep(.el-table__row:hover) {
background-color: #f5f7fa;
}
:deep(.selected-row) {
background-color: var(--el-color-primary-light-9) !important;
}
:deep(.selected-row td) {
background-color: var(--el-color-primary-light-9) !important;
}
:deep(.el-table__body tr.current-row > td) {
background-color: var(--el-color-primary-light-8) !important;
}
/* 标签页样式 */
:deep(.el-tabs__header) {
margin-bottom: 16px;
}
:deep(.el-tabs__item) {
font-size: 15px;
padding: 0 24px;
}
:deep(.el-tabs__item.is-active) {
font-weight: 600;
}
</style>
+187 -5
View File
@@ -186,10 +186,36 @@
<el-input v-model="orderForm.table" placeholder="请输入所属表" /> <el-input v-model="orderForm.table" placeholder="请输入所属表" />
</el-form-item> </el-form-item>
<el-form-item label="用户ID" prop="user_id"> <el-form-item label="用户ID" prop="user_id">
<el-input-number v-model="orderForm.user_id" :min="1" placeholder="请输入用户ID" style="width: 100%" /> <div class="selector-field">
<div class="selector-info" v-if="selectedUserInfo">
<el-tag type="primary" effect="plain">
ID: {{ orderForm.user_id }} - {{ selectedUserInfo.user_name }}
</el-tag>
</div>
<div class="selector-actions">
<el-button type="primary" @click="userSelectorVisible = true">
<el-icon><User /></el-icon>
{{ orderForm.user_id ? '更换用户' : '选择用户' }}
</el-button>
<el-button v-if="orderForm.user_id" @click="clearUser">清除</el-button>
</div>
</div>
</el-form-item> </el-form-item>
<el-form-item label="商品ID" prop="commodity_id"> <el-form-item label="商品ID" prop="commodity_id">
<el-input-number v-model="orderForm.commodity_id" :min="0" placeholder="请输入商品ID" style="width: 100%" /> <div class="selector-field">
<div class="selector-info" v-if="selectedProductInfo">
<el-tag type="success" effect="plain">
ID: {{ orderForm.commodity_id }} - {{ selectedProductInfo.name }}
</el-tag>
</div>
<div class="selector-actions">
<el-button type="success" @click="productSelectorVisible = true">
<el-icon><ShoppingCart /></el-icon>
{{ orderForm.commodity_id ? '更换商品' : '选择商品' }}
</el-button>
<el-button v-if="orderForm.commodity_id" @click="clearProduct">清除</el-button>
</div>
</div>
</el-form-item> </el-form-item>
<el-form-item label="购买数量" prop="pay_num"> <el-form-item label="购买数量" prop="pay_num">
<el-input-number v-model="orderForm.pay_num" :min="1" placeholder="请输入数量" style="width: 100%" /> <el-input-number v-model="orderForm.pay_num" :min="1" placeholder="请输入数量" style="width: 100%" />
@@ -204,10 +230,36 @@
<el-input-number v-model="orderForm.expire_time" :min="0" placeholder="请输入过期时间(时间戳)" style="width: 100%" /> <el-input-number v-model="orderForm.expire_time" :min="0" placeholder="请输入过期时间(时间戳)" style="width: 100%" />
</el-form-item> </el-form-item>
<el-form-item label="优惠码ID" prop="discount_code_id"> <el-form-item label="优惠码ID" prop="discount_code_id">
<el-input-number v-model="orderForm.discount_code_id" :min="0" placeholder="请输入优惠码ID" style="width: 100%" /> <div class="selector-field">
<div class="selector-info" v-if="selectedDiscountCodeInfo">
<el-tag type="warning" effect="plain">
ID: {{ orderForm.discount_code_id }} - {{ selectedDiscountCodeInfo.name || selectedDiscountCodeInfo.code }}
</el-tag>
</div>
<div class="selector-actions">
<el-button type="warning" @click="discountCodeSelectorVisible = true">
<el-icon><Ticket /></el-icon>
{{ orderForm.discount_code_id ? '更换优惠码' : '选择优惠码' }}
</el-button>
<el-button v-if="orderForm.discount_code_id" @click="clearDiscountCode">清除</el-button>
</div>
</div>
</el-form-item> </el-form-item>
<el-form-item label="代金券ID" prop="coupon_id"> <el-form-item label="代金券ID" prop="coupon_id">
<el-input-number v-model="orderForm.coupon_id" :min="0" placeholder="请输入代金券ID (必填)" style="width: 100%" /> <div class="selector-field">
<div class="selector-info" v-if="selectedVoucherInfo">
<el-tag type="danger" effect="plain">
ID: {{ orderForm.coupon_id }} - {{ selectedVoucherInfo.name || selectedVoucherInfo.code }}
</el-tag>
</div>
<div class="selector-actions">
<el-button type="danger" @click="voucherSelectorVisible = true">
<el-icon><Money /></el-icon>
{{ orderForm.coupon_id ? '更换代金券' : '选择代金券' }}
</el-button>
<el-button v-if="orderForm.coupon_id" @click="clearVoucher">清除</el-button>
</div>
</div>
</el-form-item> </el-form-item>
<el-form-item label="订单状态" prop="state"> <el-form-item label="订单状态" prop="state">
<el-radio-group v-model="orderForm.state"> <el-radio-group v-model="orderForm.state">
@@ -233,14 +285,46 @@
</div> </div>
</template> </template>
</el-dialog> </el-dialog>
<!-- 用户选择器 -->
<UserListSelector
v-model="userSelectorVisible"
:current-user-id="orderForm.user_id"
@confirm="handleUserSelect"
/>
<!-- 商品选择器 -->
<ProductSelector
v-model="productSelectorVisible"
:current-product-id="orderForm.commodity_id"
@confirm="handleProductSelect"
/>
<!-- 优惠码选择器 -->
<DiscountCodeSelector
v-model="discountCodeSelectorVisible"
:current-code-id="orderForm.discount_code_id"
@confirm="handleDiscountCodeSelect"
/>
<!-- 代金券选择器 -->
<VoucherSelector
v-model="voucherSelectorVisible"
:current-voucher-id="orderForm.coupon_id"
@confirm="handleVoucherSelect"
/>
</div> </div>
</template> </template>
<script setup> <script setup>
import { ref, reactive, onMounted } from 'vue' import { ref, reactive, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus' import { ElMessage, ElMessageBox } from 'element-plus'
import { Plus, Delete, Search, Download, Refresh } from '@element-plus/icons-vue' import { Plus, Delete, Search, Download, Refresh, User, ShoppingCart, Ticket, Money } from '@element-plus/icons-vue'
import { getOrderList, getOrderDetail, createOrder, updateOrder, deleteOrder } from '@/api/admin/order' import { getOrderList, getOrderDetail, createOrder, updateOrder, deleteOrder } from '@/api/admin/order'
import UserListSelector from '@/components/admin/UserListSelector.vue'
import ProductSelector from '@/components/admin/ProductSelector.vue'
import DiscountCodeSelector from '@/components/admin/DiscountCodeSelector.vue'
import VoucherSelector from '@/components/admin/VoucherSelector.vue'
// 查询参数 // 查询参数
const queryParams = reactive({ const queryParams = reactive({
@@ -305,6 +389,18 @@ const detailDialogVisible = ref(false)
const dialogType = ref('add') const dialogType = ref('add')
const orderFormRef = ref(null) const orderFormRef = ref(null)
// 选择器弹窗状态
const userSelectorVisible = ref(false)
const productSelectorVisible = ref(false)
const discountCodeSelectorVisible = ref(false)
const voucherSelectorVisible = ref(false)
// 选择的显示信息
const selectedUserInfo = ref(null)
const selectedProductInfo = ref(null)
const selectedDiscountCodeInfo = ref(null)
const selectedVoucherInfo = ref(null)
// 获取订单列表 // 获取订单列表
const fetchOrderList = async () => { const fetchOrderList = async () => {
loading.value = true loading.value = true
@@ -399,6 +495,7 @@ const handleCurrentChange = (page) => {
const handleAdd = () => { const handleAdd = () => {
dialogType.value = 'add' dialogType.value = 'add'
dialogVisible.value = true dialogVisible.value = true
clearAllSelections()
Object.assign(orderForm, { Object.assign(orderForm, {
order_id: undefined, order_id: undefined,
name: '', name: '',
@@ -437,6 +534,8 @@ const handleView = async (row) => {
const handleEdit = (row) => { const handleEdit = (row) => {
dialogType.value = 'edit' dialogType.value = 'edit'
dialogVisible.value = true dialogVisible.value = true
clearAllSelections()
Object.assign(orderForm, { Object.assign(orderForm, {
order_id: row.id, order_id: row.id,
name: row.name, name: row.name,
@@ -454,6 +553,14 @@ const handleEdit = (row) => {
args: row.args || '', args: row.args || '',
note: row.note || '' note: row.note || ''
}) })
// 设置显示信息(只显示ID,名称需要从选择器中获取)
if (row.userId) {
selectedUserInfo.value = { user_id: row.userId, user_name: `用户${row.userId}` }
}
if (row.commodityId) {
selectedProductInfo.value = { id: row.commodityId, name: `商品${row.commodityId}` }
}
} }
// 删除订单 // 删除订单
@@ -542,6 +649,62 @@ const submitForm = () => {
}) })
} }
// 用户选择处理
const handleUserSelect = (user) => {
orderForm.user_id = user.user_id
selectedUserInfo.value = user
}
const clearUser = () => {
orderForm.user_id = undefined
selectedUserInfo.value = null
}
// 商品选择处理
const handleProductSelect = (product) => {
orderForm.commodity_id = product.id
selectedProductInfo.value = product
// 自动填充表名
if (product.table) {
orderForm.table = product.table
}
}
const clearProduct = () => {
orderForm.commodity_id = 0
selectedProductInfo.value = null
}
// 优惠码选择处理
const handleDiscountCodeSelect = (code) => {
orderForm.discount_code_id = code.id
selectedDiscountCodeInfo.value = code
}
const clearDiscountCode = () => {
orderForm.discount_code_id = 0
selectedDiscountCodeInfo.value = null
}
// 代金券选择处理
const handleVoucherSelect = (voucher) => {
orderForm.coupon_id = voucher.id
selectedVoucherInfo.value = voucher
}
const clearVoucher = () => {
orderForm.coupon_id = 0
selectedVoucherInfo.value = null
}
// 清除所有选择信息
const clearAllSelections = () => {
selectedUserInfo.value = null
selectedProductInfo.value = null
selectedDiscountCodeInfo.value = null
selectedVoucherInfo.value = null
}
// 初始化 // 初始化
onMounted(() => { onMounted(() => {
fetchOrderList() fetchOrderList()
@@ -701,4 +864,23 @@ onMounted(() => {
0% { background-position: 200% 0; } 0% { background-position: 200% 0; }
100% { background-position: -200% 0; } 100% { background-position: -200% 0; }
} }
/* 选择器字段样式 */
.selector-field {
display: flex;
flex-direction: column;
gap: 8px;
width: 100%;
}
.selector-info {
display: flex;
align-items: center;
}
.selector-actions {
display: flex;
gap: 8px;
align-items: center;
}
</style> </style>
+87 -2
View File
@@ -163,8 +163,28 @@
<el-input v-model="productForm.content" type="textarea" :rows="4" placeholder="请输入内容" /> <el-input v-model="productForm.content" type="textarea" :rows="4" placeholder="请输入内容" />
</el-form-item> </el-form-item>
<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%" /> <div class="cover-selector">
<div class="cover-preview" v-if="productForm.cover_id && coverPreviewUrl">
<el-image :src="coverPreviewUrl" fit="cover" style="width: 80px; height: 80px; border-radius: 4px;" />
</div>
<div class="cover-actions">
<el-button type="primary" @click="openCoverSelector">
<el-icon><Picture /></el-icon>
{{ productForm.cover_id ? '更换封面' : '选择封面' }}
</el-button>
<el-button v-if="productForm.cover_id" @click="clearCover">清除</el-button>
<span v-if="productForm.cover_id" class="cover-id-text">ID: {{ productForm.cover_id }}</span>
</div>
</div>
</el-form-item> </el-form-item>
<!-- 封面选择器弹窗 -->
<AvatarSelector
v-model="coverSelectorVisible"
:user-id="1"
:current-cover-id="productForm.cover_id"
@confirm="handleCoverSelect"
/>
<el-form-item label="库存控制" prop="inventory_control"> <el-form-item label="库存控制" prop="inventory_control">
<el-switch v-model="productForm.inventory_control" active-text="启用" inactive-text="禁用" /> <el-switch v-model="productForm.inventory_control" active-text="启用" inactive-text="禁用" />
</el-form-item> </el-form-item>
@@ -395,7 +415,8 @@
import { ref, reactive, onMounted, nextTick } from 'vue' import { ref, reactive, onMounted, nextTick } 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, Picture } from '@element-plus/icons-vue'
import AvatarSelector from '@/components/admin/AvatarSelector.vue'
import { getProductList, createProduct, updateProduct, deleteProduct, getProductGroupList, import { getProductList, createProduct, updateProduct, deleteProduct, getProductGroupList,
getProductTagList, getProductTagList,
getProductParameterList, getProductParameterList,
@@ -463,6 +484,10 @@ const dialogVisible = ref(false)
const dialogType = ref('add') const dialogType = ref('add')
const productFormRef = ref(null) const productFormRef = ref(null)
// 封面选择器相关
const coverSelectorVisible = ref(false)
const coverPreviewUrl = ref('')
// 获取商品列表 // 获取商品列表
const fetchProductList = async () => { const fetchProductList = async () => {
loading.value = true loading.value = true
@@ -559,6 +584,7 @@ const handleAdd = () => {
dialogType.value = 'add' dialogType.value = 'add'
dialogVisible.value = true dialogVisible.value = true
coverPreviewUrl.value = '' // 清除封面预览
Object.assign(productForm, { Object.assign(productForm, {
id: undefined, id: undefined,
name: '', name: '',
@@ -599,6 +625,8 @@ const handleEdit = (row) => {
recommend: row.recommend, recommend: row.recommend,
recommend_rebate: row.recommendRebate recommend_rebate: row.recommendRebate
}) })
// 加载封面预览
loadCoverPreview(row.coverId)
} }
// 规格管理 // 规格管理
@@ -713,6 +741,40 @@ const submitForm = () => {
}) })
} }
// 打开封面选择器
const openCoverSelector = () => {
coverSelectorVisible.value = true
}
// 处理封面选择
const handleCoverSelect = async (data) => {
productForm.cover_id = data.cover_id
coverPreviewUrl.value = data.url
}
// 清除封面
const clearCover = () => {
productForm.cover_id = undefined
coverPreviewUrl.value = ''
}
// 加载封面预览
const loadCoverPreview = async (coverId) => {
if (!coverId) {
coverPreviewUrl.value = ''
return
}
try {
const res = await getFileDetail({ file_id: coverId })
if (res.data.code === 200) {
coverPreviewUrl.value = res.data.data.url
}
} catch (error) {
console.error('加载封面预览失败:', error)
coverPreviewUrl.value = ''
}
}
// 初始化 // 初始化
onMounted(() => { onMounted(() => {
fetchProductList() fetchProductList()
@@ -1170,5 +1232,28 @@ const submitParamValueForm = () => {
.text-muted { .text-muted {
color: #c0c4cc; color: #c0c4cc;
} }
/* 封面选择器样式 */
.cover-selector {
display: flex;
align-items: center;
gap: 16px;
}
.cover-preview {
flex-shrink: 0;
}
.cover-actions {
display: flex;
align-items: center;
gap: 12px;
flex-wrap: wrap;
}
.cover-id-text {
color: #909399;
font-size: 13px;
}
</style> </style>
+456 -7
View File
@@ -173,15 +173,116 @@
</div> </div>
<el-empty v-else description="暂无操作记录" :image-size="100" /> <el-empty v-else description="暂无操作记录" :image-size="100" />
</el-tab-pane> </el-tab-pane>
<el-tab-pane label="订单列表" name="3">
<el-table :data="userOrderList" v-loading="orderListLoading" stripe style="width: 100%">
<el-table-column prop="order_id" label="订单ID" width="100" />
<el-table-column prop="good_name" label="商品名称" min-width="150" show-overflow-tooltip />
<el-table-column prop="price" label="金额" width="100">
<template #default="{row}">¥{{ (row.price / 100).toFixed(2) }}</template>
</el-table-column>
<el-table-column prop="status" label="状态" width="100">
<template #default="{row}">
<el-tag :type="getOrderStatusType(row.status)" size="small">{{ getOrderStatusText(row.status) }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="created_at" label="创建时间" width="160">
<template #default="{row}">{{ formatDate(row.created_at) }}</template>
</el-table-column>
<el-table-column label="操作" width="80" fixed="right">
<template #default="scope">
<el-button type="primary" link size="small" @click="handleViewOrder(scope.row)">详情</el-button>
</template>
</el-table-column>
</el-table>
<div class="pagination-wrapper" v-if="orderListTotal > 0">
<el-pagination
v-model:current-page="orderListPage"
v-model:page-size="orderListPageSize"
:total="orderListTotal"
:page-sizes="[10, 20, 50]"
layout="total, prev, pager, next"
@size-change="handleOrderListSizeChange"
@current-change="handleOrderListPageChange"
/>
</div>
<el-empty v-else description="暂无订单记录" :image-size="100" />
</el-tab-pane>
<el-tab-pane label="工单列表" name="4">
<el-table :data="userTicketList" v-loading="ticketListLoading" stripe style="width: 100%">
<el-table-column prop="work_id" label="工单ID" width="100" />
<el-table-column prop="title" label="标题" min-width="150" show-overflow-tooltip />
<el-table-column prop="status" label="状态" width="100">
<template #default="{row}">
<el-tag :type="getTicketStatusType(row.status)" size="small">{{ getTicketStatusText(row.status) }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="created_at" label="创建时间" width="160">
<template #default="{row}">{{ formatDate(row.created_at) }}</template>
</el-table-column>
<el-table-column label="操作" width="80" fixed="right">
<template #default="scope">
<el-button type="primary" link size="small" @click="handleViewTicket(scope.row)">详情</el-button>
</template>
</el-table-column>
</el-table>
<div class="pagination-wrapper" v-if="ticketListTotal > 0">
<el-pagination
v-model:current-page="ticketListPage"
v-model:page-size="ticketListPageSize"
:total="ticketListTotal"
:page-sizes="[10, 20, 50]"
layout="total, prev, pager, next"
@size-change="handleTicketListSizeChange"
@current-change="handleTicketListPageChange"
/>
</div>
<el-empty v-else description="暂无工单记录" :image-size="100" />
</el-tab-pane>
</el-tabs> </el-tabs>
</el-card> </el-card>
</div> </div>
</div> </div>
<!-- 余额管理区域 -->
<el-card class="balance-card" shadow="never" style="margin-top: 24px;">
<template #header>
<div class="card-header">
<span class="title"><el-icon><Wallet /></el-icon> 余额管理</span>
<el-button type="primary" size="small" @click="handleBalanceManage">前往余额管理</el-button>
</div>
</template>
<div class="balance-content">
<div class="balance-overview">
<div class="balance-item">
<div class="balance-label">账户余额</div>
<div class="balance-value primary">¥{{ (userBalance.balance / 100).toFixed(2) }}</div>
</div>
<div class="balance-item">
<div class="balance-label">冻结余额</div>
<div class="balance-value warning">¥{{ (userBalance.frozen_balance / 100).toFixed(2) }}</div>
</div>
<div class="balance-item">
<div class="balance-label">累计充值</div>
<div class="balance-value success">¥{{ (userBalance.total_recharge / 100).toFixed(2) }}</div>
</div>
<div class="balance-item">
<div class="balance-label">累计消费</div>
<div class="balance-value danger">¥{{ (userBalance.total_consume / 100).toFixed(2) }}</div>
</div>
</div>
<el-divider />
<div class="balance-actions">
<el-button type="success" plain @click="handleRecharge">充值</el-button>
<el-button type="warning" plain @click="handleDeduct">扣款</el-button>
<el-button type="info" plain @click="handleViewBalanceHistory">余额记录</el-button>
</div>
</div>
</el-card>
</div> </div>
<!-- 编辑用户对话框 --> <!-- 编辑用户对话框 -->
<el-dialog v-model="editDialogVisible" title="编辑用户信息" width="500px" append-to-body destroy-on-close> <el-dialog v-model="editDialogVisible" title="编辑用户信息" width="600px" append-to-body destroy-on-close>
<el-form ref="editFormRef" :model="editForm" :rules="editRules" label-width="80px"> <el-form ref="editFormRef" :model="editForm" :rules="editRules" label-width="100px">
<el-form-item label="用户名" prop="UserName"> <el-form-item label="用户名" prop="UserName">
<el-input v-model="editForm.UserName" placeholder="请输入用户名" /> <el-input v-model="editForm.UserName" placeholder="请输入用户名" />
</el-form-item> </el-form-item>
@@ -200,8 +301,20 @@
<el-option label="女" value="false" /> <el-option label="女" value="false" />
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item label="推荐人ID"> <el-form-item label="推荐人">
<el-input-number v-model="editForm.RecommendUserId" :min="0" style="width: 100%" /> <div class="recommend-user-selector">
<div class="selected-user" v-if="recommendUserInfo.UserId">
<el-avatar :size="32" :src="recommendUserInfo.Avatar" />
<span class="user-name">{{ recommendUserInfo.UserName }}</span>
<span class="user-id">(ID: {{ recommendUserInfo.UserId }})</span>
<el-button type="danger" link size="small" @click="clearRecommendUser">
<el-icon><Close /></el-icon>
</el-button>
</div>
<el-button v-else type="primary" plain @click="showUserSelectorDialog = true">
<el-icon><User /></el-icon> 选择推荐人
</el-button>
</div>
</el-form-item> </el-form-item>
</el-form> </el-form>
<template #footer> <template #footer>
@@ -212,6 +325,13 @@
</template> </template>
</el-dialog> </el-dialog>
<!-- 用户选择器推荐人选择 -->
<UserListSelector
v-model="showUserSelectorDialog"
:current-user-id="editForm.RecommendUserId"
@confirm="handleRecommendUserSelect"
/>
<!-- 头像选择对话框 --> <!-- 头像选择对话框 -->
<AvatarSelector <AvatarSelector
v-model="showAvatarSelector" v-model="showAvatarSelector"
@@ -375,19 +495,22 @@ import { useRoute, useRouter } from 'vue-router'
import { ElMessage, ElMessageBox } from 'element-plus' import { ElMessage, ElMessageBox } from 'element-plus'
import { baseURL } from '@/utils/request' import { baseURL } from '@/utils/request'
import AvatarSelector from '@/components/admin/AvatarSelector.vue' import AvatarSelector from '@/components/admin/AvatarSelector.vue'
import UserListSelector from '@/components/admin/UserListSelector.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, Monitor, Setting, Close
} from '@element-plus/icons-vue' } from '@element-plus/icons-vue'
import { getUserGroupList } from '@/api/admin/user' import { getUserGroupList, getUserBalanceCount } from '@/api/admin/user'
import { getFileDetail, getFileList, getFile, uploadFile } from '@/api/admin/file' import { getFileDetail } from '@/api/admin/file'
import { import {
getUserInfo, updateUserInfo, updateUserAvatar, updateUserPassword, getUserInfo, updateUserInfo, updateUserAvatar, updateUserPassword,
updateUserGroup, updateUserRealName, getUserLoginRecord, updateUserGroup, updateUserRealName, getUserLoginRecord,
getUserOperationRecord, mockUserLogin, deleteUser, updateUserAdmin getUserOperationRecord, mockUserLogin, deleteUser, updateUserAdmin
} from '@/api/admin/user' } from '@/api/admin/user'
import { getAdminGroupList } from '@/api/admin/group' import { getAdminGroupList } from '@/api/admin/group'
import { getOrderList } from '@/api/admin/order'
import { getTickerList } from '@/api/ticket'
const Edit = EditIcon const Edit = EditIcon
const route = useRoute() const route = useRoute()
@@ -418,6 +541,28 @@ const operationHistoryTotal = ref(0)
const operationHistoryPage = ref(1) const operationHistoryPage = ref(1)
const operationHistoryPageSize = ref(10) const operationHistoryPageSize = ref(10)
// 订单列表相关
const userOrderList = ref([])
const orderListLoading = ref(false)
const orderListTotal = ref(0)
const orderListPage = ref(1)
const orderListPageSize = ref(10)
// 工单列表相关
const userTicketList = ref([])
const ticketListLoading = ref(false)
const ticketListTotal = ref(0)
const ticketListPage = ref(1)
const ticketListPageSize = ref(10)
// 用户余额相关
const userBalance = ref({
balance: 0,
frozen_balance: 0,
total_recharge: 0,
total_consume: 0
})
// 编辑用户相关 // 编辑用户相关
const editDialogVisible = ref(false) const editDialogVisible = ref(false)
const editForm = reactive({ const editForm = reactive({
@@ -455,6 +600,15 @@ const showAvatarSelector = ref(false)
const selectedAvatarId = ref('') const selectedAvatarId = ref('')
const currentAvatarUrl = ref('') const currentAvatarUrl = ref('')
// 推荐人选择相关
const showUserSelectorDialog = ref(false)
const recommendUserInfo = ref({
UserId: '',
UserName: '',
Avatar: ''
})
// 密码管理相关 // 密码管理相关
const passwordDialogVisible = ref(false) const passwordDialogVisible = ref(false)
const passwordForm = reactive({ const passwordForm = reactive({
@@ -509,6 +663,10 @@ const handleTabClick = (tab) => {
fetchLoginHistory() fetchLoginHistory()
} else if (tab.props.name === '2') { } else if (tab.props.name === '2') {
fetchOperationHistory() fetchOperationHistory()
} else if (tab.props.name === '3') {
fetchUserOrderList()
} else if (tab.props.name === '4') {
fetchUserTicketList()
} }
}; };
@@ -567,9 +725,51 @@ const handleEditUser = async () => {
editForm.RecommendUserId = userInfo.value.RecommendUserId || '' editForm.RecommendUserId = userInfo.value.RecommendUserId || ''
editForm.CoverID = userInfo.value.CoverID || '' editForm.CoverID = userInfo.value.CoverID || ''
// 初始化推荐人信息
if (userInfo.value.RecommendUserId) {
await fetchRecommendUserInfo(userInfo.value.RecommendUserId)
} else {
recommendUserInfo.value = { UserId: '', UserName: '', Avatar: '' }
}
editDialogVisible.value = true editDialogVisible.value = true
} }
// 获取推荐人信息
const fetchRecommendUserInfo = async (userId) => {
try {
const res = await getUserInfo({ user_id: userId })
if (res.data.code === 200) {
const user = res.data.data
recommendUserInfo.value = {
UserId: user.UserId || user.user_id,
UserName: user.UserName || user.user_name,
Avatar: user.Avatar || user.cover || ''
}
}
} catch (error) {
console.error('获取推荐人信息失败:', error)
}
}
// 推荐人选择处理
const handleRecommendUserSelect = (user) => {
recommendUserInfo.value = {
UserId: user.user_id || user.UserId,
UserName: user.user_name || user.UserName,
Avatar: user.cover || user.Avatar || ''
}
editForm.RecommendUserId = user.user_id || user.UserId
}
// 清除推荐人
const clearRecommendUser = () => {
recommendUserInfo.value = { UserId: '', UserName: '', Avatar: '' }
editForm.RecommendUserId = ''
}
// 提交编辑表单 // 提交编辑表单
const submitEditForm = async () => { const submitEditForm = async () => {
try { try {
@@ -729,6 +929,144 @@ const handleDeleteOperationRecord = async (record) => {
ElMessage.info('功能开发中') ElMessage.info('功能开发中')
} }
// 获取用户订单列表
const fetchUserOrderList = async () => {
if (!route.query.user_id) return
orderListLoading.value = true
try {
const res = await getOrderList({
user_id: route.query.user_id,
page: orderListPage.value,
count: orderListPageSize.value
})
if (res.data.code === 200) {
userOrderList.value = res.data.data.data || []
orderListTotal.value = res.data.data.all_count || 0
}
} catch (error) {
ElMessage.error('获取订单列表失败')
} finally {
orderListLoading.value = false
}
}
const handleOrderListSizeChange = (size) => {
orderListPageSize.value = size
orderListPage.value = 1
fetchUserOrderList()
}
const handleOrderListPageChange = (page) => {
orderListPage.value = page
fetchUserOrderList()
}
// 获取用户工单列表
const fetchUserTicketList = async () => {
if (!route.query.user_id) return
ticketListLoading.value = true
try {
const res = await getTickerList(ticketListPageSize.value, ticketListPage.value, undefined, undefined, undefined, route.query.user_id)
if (res.code === 200) {
userTicketList.value = res.data.data || []
ticketListTotal.value = res.data.all_count || 0
}
} catch (error) {
ElMessage.error('获取工单列表失败')
} finally {
ticketListLoading.value = false
}
}
const handleTicketListSizeChange = (size) => {
ticketListPageSize.value = size
ticketListPage.value = 1
fetchUserTicketList()
}
const handleTicketListPageChange = (page) => {
ticketListPage.value = page
fetchUserTicketList()
}
// 获取用户余额信息
const fetchUserBalance = async () => {
if (!route.query.user_id) return
try {
const res = await getUserBalanceCount({ user_id: route.query.user_id })
if (res.data.code === 200) {
userBalance.value = {
balance: res.data.data.balance || 0,
frozen_balance: res.data.data.frozen_balance || 0,
total_recharge: res.data.data.total_recharge || 0,
total_consume: res.data.data.total_consume || 0
}
}
} catch (error) {
console.error('获取用户余额失败:', error)
}
}
// 订单状态
const getOrderStatusText = (status) => {
const map = { 0: '待支付', 1: '已支付', 2: '已取消', 3: '已退款', 4: '已完成' }
return map[status] || '未知'
}
const getOrderStatusType = (status) => {
const map = { 0: 'warning', 1: 'success', 2: 'info', 3: 'danger', 4: 'success' }
return map[status] || 'info'
}
// 工单状态
const getTicketStatusText = (status) => {
const map = { 0: '待处理', 1: '处理中', 2: '已回复', 3: '已关闭' }
return map[status] || '未知'
}
const getTicketStatusType = (status) => {
const map = { 0: 'warning', 1: 'primary', 2: 'success', 3: 'info' }
return map[status] || 'info'
}
// 查看订单详情
const handleViewOrder = (row) => {
router.push({
path: '/order/list',
query: { order_id: row.order_id }
})
}
// 查看工单详情
const handleViewTicket = (row) => {
router.push({
path: '/ticket/detail',
query: { work_id: row.work_id }
})
}
// 余额操作
const handleRecharge = () => {
router.push({
path: '/user/balance',
query: { user_id: userInfo.value.UserId, action: 'recharge' }
})
}
const handleDeduct = () => {
router.push({
path: '/user/balance',
query: { user_id: userInfo.value.UserId, action: 'deduct' }
})
}
const handleViewBalanceHistory = () => {
router.push({
path: '/user/balance',
query: { user_id: userInfo.value.UserId }
})
}
// 删除用户 // 删除用户
const handleDeleteUser = async () => { const handleDeleteUser = async () => {
try { try {
@@ -958,8 +1296,11 @@ const loadUserData = async () => {
return; return;
} }
await fetchUserInfo(); await fetchUserInfo();
await fetchUserBalance();
await fetchLoginHistory(); await fetchLoginHistory();
await fetchOperationHistory(); await fetchOperationHistory();
await fetchUserOrderList();
await fetchUserTicketList();
} }
// 初始化 // 初始化
@@ -1201,6 +1542,32 @@ onActivated(() => {
font-size: 13px; font-size: 13px;
} }
/* 推荐人选择器 */
.recommend-user-selector {
width: 100%;
}
.selected-user {
display: flex;
align-items: center;
gap: 10px;
padding: 8px 12px;
background: #f5f7fa;
border-radius: 6px;
border: 1px solid #e4e7ed;
}
.selected-user .user-name {
font-weight: 500;
color: #303133;
}
.selected-user .user-id {
color: #909399;
font-size: 12px;
}
/* Token Dialog */ /* Token Dialog */
.token-container { .token-container {
padding: 20px 0; padding: 20px 0;
@@ -1223,6 +1590,78 @@ onActivated(() => {
color: #909399; color: #909399;
} }
/* Balance Card */
.balance-card {
border-radius: 12px;
border: none;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.03);
}
.balance-card .card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.balance-card .card-header .title {
font-weight: 600;
font-size: 16px;
color: #303133;
display: flex;
align-items: center;
gap: 8px;
}
.balance-content {
padding: 10px 0;
}
.balance-overview {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 20px;
}
.balance-item {
text-align: center;
padding: 16px;
background: #f9fafc;
border-radius: 8px;
}
.balance-label {
font-size: 13px;
color: #909399;
margin-bottom: 8px;
}
.balance-value {
font-size: 24px;
font-weight: 700;
}
.balance-value.primary {
color: #409eff;
}
.balance-value.warning {
color: #e6a23c;
}
.balance-value.success {
color: #67c23a;
}
.balance-value.danger {
color: #f56c6c;
}
.balance-actions {
display: flex;
justify-content: center;
gap: 16px;
}
/* Responsive */ /* Responsive */
@media (max-width: 1024px) { @media (max-width: 1024px) {
.detail-grid { .detail-grid {
@@ -1242,5 +1681,15 @@ onActivated(() => {
padding: 16px; padding: 16px;
border-radius: 8px; border-radius: 8px;
} }
.balance-overview {
grid-template-columns: repeat(2, 1fr);
}
}
@media (max-width: 600px) {
.balance-overview {
grid-template-columns: 1fr;
}
} }
</style> </style>
+75 -7
View File
@@ -141,13 +141,29 @@
<el-input v-model="groupForm.auth" type="textarea" :rows="4" placeholder="请输入权限配置(JSON格式)" /> <el-input v-model="groupForm.auth" type="textarea" :rows="4" placeholder="请输入权限配置(JSON格式)" />
</el-form-item> </el-form-item>
<el-form-item label="下一级用户组ID"> <el-form-item label="下一级用户组ID">
<el-input-number <div class="selector-field">
v-model="groupForm.higher_level_id" <div class="selector-info" v-if="selectedHigherGroupInfo">
:min="0" <el-tag type="primary" effect="plain">
placeholder="请输入下一级用户组ID" ID: {{ groupForm.higher_level_id }} - {{ selectedHigherGroupInfo.group_name || selectedHigherGroupInfo.name }}
style="width: 100%" </el-tag>
/> </div>
<div class="selector-actions">
<el-button type="primary" @click="groupSelectorVisible = true">
<el-icon><Connection /></el-icon>
{{ groupForm.higher_level_id ? '更换用户组' : '选择用户组' }}
</el-button>
<el-button v-if="groupForm.higher_level_id" @click="clearHigherGroup">清除</el-button>
</div>
</div>
</el-form-item> </el-form-item>
<!-- 用户组选择器 -->
<UserGroupSelector
v-model="groupSelectorVisible"
:current-group-id="groupForm.higher_level_id"
:exclude-group-id="groupForm.group_id"
@confirm="handleHigherGroupSelect"
/>
<el-form-item label="升级所需消费金额"> <el-form-item label="升级所需消费金额">
<el-input-number <el-input-number
v-model="groupForm.floor_price" v-model="groupForm.floor_price"
@@ -258,7 +274,7 @@
<script setup> <script setup>
import { ref, reactive, onMounted } from 'vue' import { ref, reactive, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus' import { ElMessage, ElMessageBox } from 'element-plus'
import { Plus, Refresh, Edit, User, Delete } from '@element-plus/icons-vue' import { Plus, Refresh, Edit, User, Delete, Connection } from '@element-plus/icons-vue'
import { import {
getUserGroupList, getUserGroupList,
getUserGroupMemberList, getUserGroupMemberList,
@@ -268,6 +284,7 @@ import {
addUserGroupMember addUserGroupMember
} from '@/api/admin/user' } from '@/api/admin/user'
import { formatTime } from '@/utils/tool' import { formatTime } from '@/utils/tool'
import UserGroupSelector from '@/components/admin/UserGroupSelector.vue'
// 查询参数 // 查询参数
const queryParams = reactive({ const queryParams = reactive({
@@ -327,6 +344,10 @@ const dialogType = ref('add')
const groupFormRef = ref(null) const groupFormRef = ref(null)
const memberFormRef = ref(null) const memberFormRef = ref(null)
// 用户组选择器相关
const groupSelectorVisible = ref(false)
const selectedHigherGroupInfo = ref(null)
// 获取用户组列表 // 获取用户组列表
const fetchGroupList = async () => { const fetchGroupList = async () => {
loading.value = true loading.value = true
@@ -418,6 +439,7 @@ const handleMemberCurrentChange = (page) => {
const handleAdd = () => { const handleAdd = () => {
dialogType.value = 'add' dialogType.value = 'add'
dialogVisible.value = true dialogVisible.value = true
selectedHigherGroupInfo.value = null
Object.assign(groupForm, { Object.assign(groupForm, {
group_id: undefined, group_id: undefined,
name: '', name: '',
@@ -433,6 +455,7 @@ const handleAdd = () => {
const handleEdit = (row) => { const handleEdit = (row) => {
dialogType.value = 'edit' dialogType.value = 'edit'
dialogVisible.value = true dialogVisible.value = true
selectedHigherGroupInfo.value = null
const groupId = row.group_id || row.GroupId || row.id || row.Id const groupId = row.group_id || row.GroupId || row.id || row.Id
const groupName = row.group_name || row.name || row.Name const groupName = row.group_name || row.name || row.Name
@@ -449,6 +472,20 @@ const handleEdit = (row) => {
floor_price: floorPrice, floor_price: floorPrice,
fixed: fixed fixed: fixed
}) })
// 查找下一级用户组信息用于显示
if (higherLevelId) {
const higherGroup = groupList.value.find(item => {
const itemId = item.group_id || item.GroupId || item.id || item.Id
return itemId === higherLevelId
})
if (higherGroup) {
selectedHigherGroupInfo.value = higherGroup
} else {
// 如果在列表中找不到,创建一个简单的对象用于显示
selectedHigherGroupInfo.value = { group_id: higherLevelId, name: `用户组${higherLevelId}` }
}
}
} }
// 查看成员 // 查看成员
@@ -555,6 +592,18 @@ const submitAddMember = () => {
}) })
} }
// 下一级用户组选择处理
const handleHigherGroupSelect = (group) => {
const groupId = group.group_id || group.GroupId || group.id || group.Id
groupForm.higher_level_id = groupId
selectedHigherGroupInfo.value = group
}
const clearHigherGroup = () => {
groupForm.higher_level_id = undefined
selectedHigherGroupInfo.value = null
}
// 初始化 // 初始化
onMounted(() => { onMounted(() => {
fetchGroupList() fetchGroupList()
@@ -697,4 +746,23 @@ onMounted(() => {
0% { background-position: 200% 0; } 0% { background-position: 200% 0; }
100% { background-position: -200% 0; } 100% { background-position: -200% 0; }
} }
/* 选择器字段样式 */
.selector-field {
display: flex;
flex-direction: column;
gap: 8px;
width: 100%;
}
.selector-info {
display: flex;
align-items: center;
}
.selector-actions {
display: flex;
gap: 8px;
align-items: center;
}
</style> </style>
+30 -1
View File
@@ -18,6 +18,20 @@
<el-icon><Refresh /></el-icon>刷新 <el-icon><Refresh /></el-icon>刷新
</el-button> </el-button>
</el-form-item> </el-form-item>
<el-form-item label="用户ID">
<el-input
v-model="jumpUserId"
placeholder="输入用户ID跳转"
clearable
style="width: 150px"
@keyup.enter="handleJumpToUser"
/>
</el-form-item>
<el-form-item>
<el-button type="warning" @click="handleJumpToUser">
<el-icon><Position /></el-icon>跳转
</el-button>
</el-form-item>
</el-form> </el-form>
<div class="action-bar"> <div class="action-bar">
<el-button type="primary" @click="handleAdd"> <el-button type="primary" @click="handleAdd">
@@ -384,7 +398,7 @@
import { ref, reactive, onMounted, computed } from 'vue' import { ref, reactive, onMounted, computed } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { ElMessage, ElMessageBox } from 'element-plus' import { ElMessage, ElMessageBox } from 'element-plus'
import { Plus, Delete, Search, ArrowDown, View, User, Edit, Refresh } from '@element-plus/icons-vue' import { Plus, Delete, Search, ArrowDown, View, User, Edit, Refresh, Position } from '@element-plus/icons-vue'
import AvatarSelector from '@/components/admin/AvatarSelector.vue' import AvatarSelector from '@/components/admin/AvatarSelector.vue'
import { import {
getUserList, getUserList,
@@ -404,6 +418,9 @@ import { closeAllMessage } from '../../utils/message'
const router = useRouter() const router = useRouter()
// 跳转用户ID
const jumpUserId = ref('')
// 查询参数 // 查询参数
const queryParams = reactive({ const queryParams = reactive({
key: '', key: '',
@@ -552,6 +569,18 @@ const resetQuery = () => {
fetchUserList() fetchUserList()
} }
// 跳转到指定用户详情
const handleJumpToUser = () => {
if (!jumpUserId.value || !jumpUserId.value.trim()) {
ElMessage.warning('请输入用户ID')
return
}
router.push({
path: '/user/detail',
query: { user_id: jumpUserId.value.trim() }
})
}
// 选择项变化 // 选择项变化
const handleSelectionChange = (selection) => { const handleSelectionChange = (selection) => {
selectedRows.value = selection selectedRows.value = selection
+187
View File
@@ -0,0 +1,187 @@
✅已完成、⚠️部分完成、❌未完成这样显示
-----------------------------------------------------------------------------------------------需要解决
统计现在的项目结构,接口,页面,css,全局主题
## 一、项目结构 ✅
```
src/
├── api/ # API接口层
│ ├── admin/ # 管理后台API (12个文件)
│ │ ├── activity.js ├── cdn.js ├── discount.js
│ │ ├── file.js ├── group.js ├── order.js
│ │ ├── Permission.js ├── product.js ├── product-test.js
│ │ ├── router.js ├── setting.js └── user.js
│ ├── domain.js ├── groupBuy.js
│ ├── login.js └── ticket.js
├── components/ # 公共组件 (14个)
│ ├── admin/
│ │ ├── AvatarSelector.vue # 头像选择组件
│ │ └── FileSelector.vue # 文件/封面选择组件 ✅新增
│ ├── Charts/ # 图表组件 (4个)
│ ├── layout/ # 布局组件 (4个)
│ ├── marketing/DiscountDetailDialog.vue
│ ├── UserSelector/index.vue # 用户选择组件(推荐人选择)
│ ├── Container.vue ├── MonacoEditor.vue
│ ├── Qrcode.vue └── TextTruncate.vue
├── views/ # 页面视图 (67个)
├── store/ # 状态管理 (2个)
├── router/index.js # 路由配置
├── utils/ # 工具函数
│ └── acs/ # ACS云服务工具 (12个)
├── config/menus.js # 菜单配置
├── assets/ # 静态资源
├── App.vue # 应用入口
├── main.js # 主入口
└── style.css # 全局样式
```
## 二、API接口统计 ✅ (约240+个)
| 模块 | 文件 | 接口数 |
|------|------|--------|
| 登录认证 | login.js | 2 |
| 工单管理 | ticket.js | 12 |
| 域名白名单 | domain.js | 3 |
| 拼团管理 | groupBuy.js | 18 |
| 用户管理 | admin/user.js | 28 |
| 商品管理 | admin/product.js | 19 |
| 订单管理 | admin/order.js | 5 |
| 优惠码/代金券 | admin/discount.js | 21 |
| 权限管理 | admin/Permission.js | 8 |
| 管理员组 | admin/group.js | 6 |
| 配置管理 | admin/setting.js | 11 |
| 文件管理 | admin/file.js | 7 |
| 活动管理 | admin/activity.js | 8 |
| 路由管理 | admin/router.js | 2 |
| CDN管理 | admin/cdn.js | 1 |
| ACS服务器 | utils/acs/server.js | 60+ |
| ACS镜像 | utils/acs/mirror.js | 11 |
| ACS消息 | utils/acs/message.js | 8 |
| ACS审计 | utils/acs/audit.js | 4 |
| ACS远程桌面 | utils/acs/guacamole.js | 5 |
## 三、页面视图统计 ✅ (67个)
| 分类 | 页面 | 数量 |
|------|------|------|
| 基础页面 | Login, Dashboard, NotFound, Redirect, About, Home | 6 |
| 用户管理 | UserList, UserDetail, UserBalance, UserGroup, AdminGroup | 5 |
| 商品管理 | ProductList, ProductGroup | 2 |
| 订单管理 | OrderList | 1 |
| 优惠营销 | DiscountCode, Voucher, VoucherManagement + 6个子页面 | 9 |
| 活动管理 | SigninActivity, GroupBuyActivity, GroupBuyType | 3 |
| 工单管理 | TicketList, TicketDetail, TicketChat | 3 |
| 系统管理 | PermissionRoute/Admin, SystemFile, DomainWhitelist, SettingGroup, Setting, OperationLog, Users | 8 |
| 站点审计 | AllSites, ViolationSites | 2 |
| 全局设置 | GlobalSetting | 1 |
| 个人中心 | UserInfo, ChangePassword | 2 |
| ACS消息 | Announcements, Policies, News | 3 |
| ACS镜像 | VmImages, ContainerImages, ImageCategories, ImageForm, ImageRequests | 5 |
| ACS节点 | Nodes, server, ServerForm, VmDetail, containDetail, containerConsole, containFile + 2组件 | 9 |
| ACS远程桌面 | Guacamole | 1 |
## 四、CSS样式统计 ✅
### style.css 全局样式
| 类型 | 内容 |
|------|------|
| 重置样式 | `* { margin:0; padding:0; box-sizing:border-box; }` |
| 文字颜色 | `.text-primary` `.text-success` `.text-warning` `.text-danger` `.text-info` |
| 文字对齐 | `.text-center` `.text-left` `.text-right` |
| Flex布局 | `.flex` `.flex-column` `.justify-center` `.justify-between` `.items-center` |
| 尺寸 | `.w-full` `.h-full` |
| 间距 | `.mb-10` `.mt-10` `.mr-10` `.ml-10` `.p-10` `.py-10` `.px-10` |
| 响应式 | `.hidden-xs` `.hidden-sm` `.hidden-md` `.hidden-lg` |
### App.vue 全局样式
| 类型 | 内容 |
|------|------|
| 过渡动画 | `.fade-enter-active` `.fade-leave-active` |
| 滚动条 | `::-webkit-scrollbar` 自定义滚动条 |
| 按钮扁平化 | `.el-button--primary/success/danger/default` 无圆角深蓝灰主题 |
| 输入框扁平化 | `.el-input__wrapper` 无圆角边框 |
| 标签扁平化 | `.el-tag--success/danger/info` 无圆角彩色背景 |
| 卡片扁平化 | `.el-card` 无圆角1px边框 |
| 表格扁平化 | `.el-table` 深蓝灰表头 |
| 分页扁平化 | `.el-pagination` 无圆角深蓝灰选中 |
| 下拉菜单 | `.el-dropdown-menu` 无圆角阴影边框 |
| 对话框 | `.el-dialog` 无圆角扁平化头部尾部 |
## 五、全局主题配置 ✅
### 主色调
| 颜色 | 值 | 用途 |
|------|------|------|
| 主色 | `#1890ff` | Element Plus主题色 |
| 深蓝灰 | `#2c3e50` | 按钮主色、表头、激活状态 |
| 次深蓝灰 | `#34495e` | 悬浮状态、文字 |
| 绿色 | `#27ae60` / `#52c41a` | 成功状态 |
| 红色 | `#e74c3c` / `#f5222d` | 危险/错误状态 |
| 橙色 | `#faad14` | 警告状态 |
| 蓝色 | `#3498db` | 链接、信息 |
| 紫色 | `#722ed1` | 转化率图表 |
| 背景灰 | `#f5f7fa` / `#f0f2f5` | 页面背景 |
| 边框灰 | `#e1e8ed` / `#d5d9e0` | 边框分割线 |
### 设计风格
| 特性 | 说明 |
|------|------|
| 圆角 | 全局0圆角扁平化设计 |
| 阴影 | 轻阴影 `0 2px 8px rgba(44,62,80,0.1)` |
| 字体 | Helvetica Neue, PingFang SC, Microsoft YaHei |
| 字号 | 基础14px |
## 六、Store状态管理 ✅
| Store | 功能 |
|-------|------|
| userStore.js | 用户信息存储 `userInfo` `setUserInfo()` |
| tagsViewStore.js | 多标签页管理 `visitedViews` `addVisitedView()` `delVisitedView()` `delOthersViews()` `delAllViews()` `delLeftViews()` `delRightViews()` |
## 七、技术栈 ✅
| 技术 | 版本 |
|------|------|
| Vue | ^3.5.13 |
| Vite | ^6.0.3 |
| Element Plus | ^2.9.1 |
| Vue Router | ^4.5.0 |
| Pinia | ^3.0.2 |
| Axios | ^1.7.9 |
| ECharts | ^5.6.0 |
| VueUse | ^13.1.0 |
| Monaco Editor | ^0.52.2 |
| Xterm.js | ^5.3.0 |
| qrcode | ^1.5.4 |
| dayjs | ^1.11.13 |
-----------------------------------------------------------------------------------------------需要解决
## 表单组件优化 ✅已完成
### 1. FileSelector 文件选择组件 ✅
位置: `src/components/admin/FileSelector.vue`
功能:
- 通用文件/封面选择器
- 支持文件列表分页、搜索
- 支持文件上传
- 支持图片预览
- 支持按文件类型筛选(image/document/video/audio)
- 已选文件信息显示
### 2. UserSelector 用户选择组件 ✅
位置: `src/components/UserSelector/index.vue`
功能:
- 用户列表搜索选择
- 用于推荐人ID等需要选择用户的场景
- 显示用户ID、用户名、邮箱
### 3. UserDetail 编辑表单优化 ✅
- 推荐人ID: 改为使用UserSelector组件,显示用户头像、用户名、ID
- 用户封面: 新增FileSelector组件,支持预览和选择图片
-----------------------------------------------------------------------------------------------需要解决
每次解决后的内容写在-需要解决之间,不要写在外面
我不要你解释,不是我主动告诉你解释需求,那么你就根据问题开始直接编写代码解决问题或者完善功能,问题都是用-需要解决隔开的
对应完成的部分在当前文档记录并且进行标记✅已完成、⚠️部分完成、❌未完成这样显示