feat(user): 新增用户管理功能

- 添加用户列表页面,包含搜索、分页等功能
- 实现用户状态切换和详情查看功能
- 新增用户选择组件,用于选择用户
- 优化表格组件,支持自定义列和操作
- 添加面包屑组件,用于展示导航路径
This commit is contained in:
2025-04-28 17:57:16 +08:00
parent 749d285a0d
commit e0868a10af
17 changed files with 1132 additions and 46 deletions

View File

@@ -0,0 +1,165 @@
<template>
<div class="container-wrapper">
<el-form
ref="ruleFormRef"
:model="formData"
:rules="rules"
label-width="auto"
style="margin-top: 0.5rem; "
>
<ColumnItem title="文章编辑">
<el-row :gutter="20">
<el-col :span="8">
<el-form-item label="标题" prop="title">
<el-input v-model="formData.title" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="价格" prop="price">
<el-input v-model="formData.price" type="number" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="8">
<el-form-item label="图片" prop="coverImg">
<el-upload
class="avatar-uploader"
action="https://run.mocky.io/v3/9d059bf9-4660-45f2-925d-ce80ad6c4d15"
:show-file-list="false"
:on-success="handleAvatarSuccess"
:before-upload="beforeAvatarUpload"
>
<img v-if="formData.coverImg" :src="formData.coverImg" class="avatar" alt=""/>
<el-icon v-else class="avatar-uploader-icon"><Plus /></el-icon>
</el-upload>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="讲师" prop="teacher">
<el-input v-model="formData.teacher" readonly @click="() => userDialogRef.open()" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="20">
<el-form-item label="详情" prop="content">
<RichEdit v-model="formData.content"/>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="20">
<el-form-item label="备注" prop="desc">
<el-input type="textarea" v-model="formData.desc" />
</el-form-item>
</el-col>
</el-row>
</ColumnItem>
</el-form>
<div class="gva-table-box footer-box">
<el-button @click="$router.back()">返回</el-button>
<el-button type="primary" @click="submit">提交</el-button>
</div>
</div>
<!-- 选择人员弹窗-->
<userChoose
ref="userDialogRef"
@getRecipientInfo="getStaffInfo"
title="讲师人员"
/>
</template>
<script setup>
import {onMounted, ref } from 'vue'
import userChoose from '@/components/userChoose/index.vue'
import { useRouter, useRoute} from 'vue-router'
import {detail} from '@/api/goods/index.js'
import ColumnItem from '@/components/columnItem/ColumnItem.vue'
import { ElMessage } from 'element-plus'
import { Plus } from '@element-plus/icons-vue'
import RichEdit from '@/components/richtext/rich-edit.vue'
const router = useRouter()
const route = useRoute()
const userDialogRef = ref()
const ruleFormRef = ref(null), formData = ref({}), rules = ref({
title: [
{ required: true, message: '请输入文章标题', trigger: 'blur' }
],
})
onMounted(() => {
console.log(route.query.id)
if(route.query.id) {
getDetail()
}
})
const handleAvatarSuccess = ( response, uploadFile ) => {
formData.value.coverImg = URL.createObjectURL(uploadFile.raw)
}
const beforeAvatarUpload= (rawFile) => {
if (!['image/jpeg', 'image/png'].includes(rawFile.type)) {
ElMessage.error('请上传图片格式!')
return false
} else if (rawFile.size / 1024 / 1024 > 2) {
ElMessage.error('文件大小不能超过 2MB!')
return false
}
return true
}
async function getDetail() {
const res = await detail(route.query.id)
if(res.code === 0) {
formData.value = res.data
}
}
function getStaffInfo(data) {
console.log(data)
formData.value.teacherId = data.ID
formData.value.teacher = data.nickName
}
function submit() {
console.log('提交')
}
</script>
<style lang="scss" scoped>
.avatar-uploader .el-upload {
border: 1px dashed var(--el-border-color) !important;
border-radius: 6px;
cursor: pointer;
position: relative;
overflow: hidden;
transition: var(--el-transition-duration-fast);
//border: 1px dashed red !important;
}
.avatar-uploader .el-upload:hover {
border-color: var(--el-color-primary);
}
.el-icon.avatar-uploader-icon {
font-size: 28px;
color: #8c939d;
width: 178px;
height: 178px;
text-align: center;
}
:deep{
.el-row {
justify-content: space-around;
}
.el-form{
flex: 1 ;
background: white;
overflow: hidden;
}
}
</style>

View File

@@ -1,11 +1,132 @@
<template>
<div class="container">
文章管理
<div >
<div class="searchForm">
<searchForm
:search="ARTICLE_SEARCH_CONFIG"
@searchData="searchData"
@resetData="resetData"
class="search-box searchForm"
/>
</div>
<div class="gva-table-box">
<div class="gva-btn-list">
<el-button type="primary" icon="plus" @click="openDialog()">新增</el-button>
</div>
<Content
@changePage="changePage"
:total="total"
v-model:tabloading="tableLoading"
v-model:currentPage="queryParams.page"
v-model:pageSize="queryParams.pageSize"
:data="tableData"
:config="ARTICLE_TABLE_CONFIG"
>
<template #status="{ row }">
<el-tag :type="tag(row.status).extend">{{ tag(row.status).label }}</el-tag>
</template>
<template #coverImg="{ row }">
<el-image
style="width: 100px; height: 100px"
:src="row.coverImg"
:zoom-rate="1.2"
:max-scale="7"
:min-scale="0.2"
:preview-src-list="[row.coverImg]"
show-progress
:initial-index="4"
fit="cover"
/>
</template>
<template #CreatedAt="{ row }">
{{ formatDate(row.CreatedAt) }}
</template>
<template #UpdatedAt="{ row }">
{{ formatDate(row.UpdatedAt) }}
</template>
<template #operate="{ row }">
<el-button type="primary" link class="table-button" @click="handleDetail(row)">
<el-icon style="margin-right: 5px"><InfoFilled /></el-icon>查看
</el-button>
<el-button type="primary" link icon="edit" class="table-button" @click="handleEdit(row)">编辑</el-button>
<el-button type="primary" link icon="delete" @click="handleDelete(row)">删除</el-button>
</template>
</Content>
</div>
</div>
</template>
<script setup>
import searchForm from '@/components/searchForm/index.vue'
import Content from '@/components/content/index.vue'
import {list} from '@/api/goods/index.js'
import { ref, onMounted } from 'vue'
import {formatDate} from '@/utils/format'
import { ARTICLE_SEARCH_CONFIG, ARTICLE_TABLE_CONFIG } from '@/config'
import { InfoFilled } from '@element-plus/icons-vue'
import { useRouter, useRoute} from 'vue-router'
const router = useRouter()
const route = useRoute()
const tableLoading = ref(true), tableData = ref([{status:1}])
const queryParams = ref({
page: 1,
pageSize: 10,
orderId: '',
startTime: '',
endTime: ''
}), total = ref(0)
// const EMPTY_STR = '- -'
const tag = (status) => {
return userStatus.find((item) => item.value === status+'') || { type: '', label: EMPTY_STR }
}
const searchData = (data) => {
if (data.times) {
data.startTime = data.times[0]
data.endTime = data.times[1]
}
queryParams.value.page = 1
queryParams.value = { ...queryParams.value, ...data }
delete queryParams.value.times
getList()
}
const resetData = () => {
queryParams.value = {
page: 1,
pageSize: 10,
}
getList()
}
onMounted(() => {
getList()
})
async function getList() {
const res = await list(queryParams.value)
if(res.code === 0) {
tableData.value = res.data.list
total.value = res.data.total
}
}
function handleDetail(row) {
}
function handleEdit(row) {
console.log(row)
router.push({
name: 'edit',
query: {
id: row.ID
}
})
}
function handleDelete(row) {
}
function changePage(data) {
queryParams.value.pageNum = data
getList()
}
</script>
<style lang="scss" scoped>
</style>
</style>

View File

@@ -9,22 +9,6 @@
/>
</div>
<div class="gva-table-box">
<!-- <el-table :data="tableData" style="width: 100%">-->
<!-- <el-table-column type="index" label="序号" width="120" />-->
<!-- <el-table-column prop="order_no" label="订单号" width="180" />-->
<!-- <el-table-column prop="name" label="商品名称" />-->
<!-- <el-table-column prop="price" label="价格" />-->
<!-- <el-table-column prop="order_type" label="类型" />-->
<!-- <el-table-column prop="status" label="状态" />-->
<!-- <el-table-column prop="CreatedAt" label="创建时间" />-->
<!-- <el-table-column prop="UpdatedAt" label="更新时间" />-->
<!-- <el-table-column label="操作" fixed="right" width="100">-->
<!-- <template #default="scope">-->
<!-- <el-button type="text" @click="handleDelete(scope.row)">删除</el-button>-->
<!-- </template>-->
<!-- </el-table-column>-->
<!-- </el-table>-->
<Content
@changePage="changePage"
:total="total"
@@ -50,10 +34,10 @@
import searchForm from '@/components/searchForm/index.vue'
import Content from '@/components/content/index.vue'
import {list} from '@/api/order'
import { ref, reactive, onMounted } from 'vue'
import { ref, onMounted } from 'vue'
import { ORDER_SEARCH_CONFIG, ORDER_TABLE_CONFIG } from '@/config'
const tableLoading = ref(true), tableData = ref([{ status: 1 }])
const queryParams = reactive({
const tableLoading = ref(true), tableData = ref([])
const queryParams = ref({
page: 1,
pageSize: 10,
orderId: '',
@@ -78,7 +62,6 @@ const searchData = (data) => {
queryParams.value.page = 1
queryParams.value = { ...queryParams.value, ...data }
delete queryParams.value.times
// console.log(queryParams.value)
getList()
}
const resetData = () => {
@@ -92,7 +75,7 @@ onMounted(() => {
getList()
})
async function getList() {
const res = await list(queryParams)
const res = await list(queryParams.value)
if(res.code === 0) {
tableData.value = res.data.list
total.value = res.data.total
@@ -106,10 +89,9 @@ function handleDetail(row) {
}
function changePage(data) {
queryParams.value.pageNum = data
getList()
queryParams.value.pageNum = data
getList()
}
</script>
<style lang="scss" scoped>

21
src/view/user/index.vue Normal file
View File

@@ -0,0 +1,21 @@
<template>
<div>
<router-view v-slot="{ Component }">
<transition mode="out-in" name="el-fade-in-linear">
<keep-alive :include="routerStore.keepAliveRouters">
<component :is="Component" />
</keep-alive>
</transition>
</router-view>
</div>
</template>
<script setup>
import { useRouterStore } from '@/pinia/modules/router'
const routerStore = useRouterStore()
defineOptions({
name: 'UserManage'
})
</script>

View File

@@ -0,0 +1,106 @@
<template>
<div >
<div class="searchForm">
<searchForm
:search="USER_SEARCH_CONFIG"
@searchData="searchData"
@resetData="resetData"
class="search-box searchForm"
/>
</div>
<div class="gva-table-box">
<Content
@changePage="changePage"
:total="total"
v-model:tabloading="tableLoading"
v-model:currentPage="queryParams.page"
v-model:pageSize="queryParams.pageSize"
:data="tableData"
:config="USER_TABLE_CONFIG"
>
<template #status="{ row }">
<!-- <el-tag :type="tag(row.status).extend">{{ tag(row.status).label }}</el-tag>-->
<el-switch v-model="row.status"
active-value="1"
inactive-value="0"
:loading="statusChangeLoading"
:before-change="() => statusChangeBefore(row)" />
</template>
<template #operate="{ row }">
<el-button type="text" @click="handleDetail(row)">详情</el-button>
<!-- <el-button type="text" @click="handleDelete(row)">删除</el-button>-->
</template>
</Content>
</div>
</div>
</template>
<script setup>
import searchForm from '@/components/searchForm/index.vue'
import Content from '@/components/content/index.vue'
import {list} from '@/api/user/index.js'
import { ref, onMounted } from 'vue'
import { USER_SEARCH_CONFIG, USER_TABLE_CONFIG } from '@/config'
const tableLoading = ref(true), tableData = ref([{status:1}])
const queryParams = ref({
page: 1,
pageSize: 10,
orderId: '',
startTime: '',
endTime: ''
}), total = ref(0)
const statusChangeLoading = ref(false)
// const EMPTY_STR = '- -'
// const tag = (status) => {
// return userStatus.find((item) => item.value === status+'') || { type: '', label: EMPTY_STR }
// }
const searchData = (data) => {
if (data.times) {
data.startTime = data.times[0]
data.endTime = data.times[1]
}
queryParams.value.page = 1
queryParams.value = { ...queryParams.value, ...data }
delete queryParams.value.times
getList()
}
const resetData = () => {
queryParams.value = {
page: 1,
pageSize: 10,
}
getList()
}
onMounted(() => {
getList()
})
async function getList() {
const res = await list(queryParams.value)
if(res.code === 0) {
tableData.value = res.data.list
total.value = res.data.total
}
}
function handleDetail(row) {
}
function changePage(data) {
queryParams.value.pageNum = data
getList()
}
function statusChangeBefore(row) {
console.log(row)
statusChangeLoading.value = true
return new Promise(resolve => {
setTimeout(() => {
statusChangeLoading.value = false
return resolve(true)
},500)
})
}
</script>
<style lang="scss" scoped>
</style>