feat(user): 新增用户管理功能
- 添加用户列表页面,包含搜索、分页等功能 - 实现用户状态切换和详情查看功能 - 新增用户选择组件,用于选择用户 - 优化表格组件,支持自定义列和操作 - 添加面包屑组件,用于展示导航路径
This commit is contained in:
parent
749d285a0d
commit
e0868a10af
42
src/api/goods/index.js
Normal file
42
src/api/goods/index.js
Normal file
@ -0,0 +1,42 @@
|
||||
import service from '@/utils/request'
|
||||
|
||||
// @tag goods
|
||||
// @summary 获取商品(文章)列表
|
||||
// @param {object} params
|
||||
// @return {object} data
|
||||
// @router get /user/list
|
||||
export const list = (params) => {
|
||||
return service({
|
||||
url: '/article/list',
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
export const add = (data) => {
|
||||
return service({
|
||||
url: '/article/list',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
export const edit = (data) => {
|
||||
return service({
|
||||
url: '/article/list',
|
||||
method: 'put',
|
||||
data
|
||||
})
|
||||
}
|
||||
export const del = (data) => {
|
||||
return service({
|
||||
url: '/article/list',
|
||||
method: 'delete',
|
||||
data
|
||||
})
|
||||
}
|
||||
export const detail = (id) => {
|
||||
return service({
|
||||
url: '/article/'+ id,
|
||||
method: 'get',
|
||||
})
|
||||
}
|
14
src/api/user/index.js
Normal file
14
src/api/user/index.js
Normal file
@ -0,0 +1,14 @@
|
||||
import service from '@/utils/request'
|
||||
|
||||
// @tag user
|
||||
// @summary 获取用户列表
|
||||
// @param {object} params
|
||||
// @return {object} data
|
||||
// @router get /user/list
|
||||
export const list = (params) => {
|
||||
return service({
|
||||
url: '/user/list',
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
}
|
6
src/assets/icons/bookmark-fill.svg
Normal file
6
src/assets/icons/bookmark-fill.svg
Normal file
@ -0,0 +1,6 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 32 32" width="20" height="20" style="border-color: rgba(0,0,0,0);border-width: bpx;border-style: undefined" filter="none">
|
||||
|
||||
<g>
|
||||
<path d="M6.667 2.667h18.667c0.736 0 1.333 0.597 1.333 1.333v0 25.524c0 0 0 0.001 0 0.001 0 0.368-0.298 0.667-0.667 0.667-0.131 0-0.254-0.038-0.357-0.104l0.003 0.002-9.645-6.049-9.645 6.048c-0.101 0.064-0.223 0.102-0.355 0.102-0.368 0-0.666-0.298-0.667-0.666v-25.524c0-0.736 0.597-1.333 1.333-1.333v0z" fill="rgba(21.93,132.09,251.94,1)"></path>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 599 B |
77
src/components/MyTitle/MyTitle.vue
Normal file
77
src/components/MyTitle/MyTitle.vue
Normal file
@ -0,0 +1,77 @@
|
||||
<template>
|
||||
<div
|
||||
style="
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
box-sizing: border-box;
|
||||
border-bottom: 1px solid #eee;
|
||||
"
|
||||
>
|
||||
<div>
|
||||
<div class="myTitle" :style="isBorder ? {} : { border: 'unset' }">
|
||||
<img :src="sonSvg" alt="" class="icon" :style="iconStyle" v-if="showIcon && sonSvg" />
|
||||
<img
|
||||
src="@/assets/icons/bookmark-fill.svg"
|
||||
alt=""
|
||||
class="icon"
|
||||
:style="iconStyle"
|
||||
v-else-if="showIcon"
|
||||
/>
|
||||
<span class="title">{{ title }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div style="padding-right: 20px">
|
||||
<slot name="right"></slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, inject } from 'vue'
|
||||
const props = defineProps({
|
||||
title: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
isBorder: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
iconStyle: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
showIcon: {
|
||||
type: Boolean,
|
||||
default: () => true
|
||||
}
|
||||
})
|
||||
const sonSvg = ref(inject('svgData'))
|
||||
console.log(sonSvg, 'data')
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.myTitle {
|
||||
width: 100%;
|
||||
height: 60px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
user-select: none;
|
||||
.icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
margin-left: 20px;
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
.title {
|
||||
font-size: 16px;
|
||||
color: #233041;
|
||||
margin-left: 10px;
|
||||
font-family: 'SourceHanSansCN-Medium';
|
||||
}
|
||||
}
|
||||
</style>
|
112
src/components/PmDialog/pm-dialog.vue
Normal file
112
src/components/PmDialog/pm-dialog.vue
Normal file
@ -0,0 +1,112 @@
|
||||
<template>
|
||||
<div class="dialog">
|
||||
<el-dialog
|
||||
class="operate"
|
||||
v-model="computedVis"
|
||||
:title="title"
|
||||
:width="width"
|
||||
@close="handleClose"
|
||||
:lock-scroll="false"
|
||||
:draggable="draggable"
|
||||
>
|
||||
<template #default>
|
||||
<slot></slot>
|
||||
</template>
|
||||
<template #footer>
|
||||
<slot name="footer"></slot>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {computed } from 'vue'
|
||||
const props = defineProps({
|
||||
overlayBg: {
|
||||
type: String,
|
||||
default: 'transparent'
|
||||
},
|
||||
modelValue: {
|
||||
type: Boolean,
|
||||
required: true
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
width: {
|
||||
type: [Number, String],
|
||||
default: () => 870
|
||||
},
|
||||
draggable: {
|
||||
type: Boolean,
|
||||
default: () => false
|
||||
}
|
||||
})
|
||||
const emit = defineEmits(['update:modelValue', 'close'])
|
||||
const computedVis = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (val) => {
|
||||
emit('update:modelValue', val)
|
||||
}
|
||||
})
|
||||
|
||||
function handleClose() {
|
||||
emit('close')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.dialog {
|
||||
:deep(.el-overlay) {
|
||||
background-color: v-bind('props.overlayBg');
|
||||
}
|
||||
:deep(.el-dialog.operate) {
|
||||
padding: 0 !important;
|
||||
margin-top: 15vh !important;
|
||||
border-radius: 6px;
|
||||
.el-dialog__header {
|
||||
padding: 0 0 0 20px;
|
||||
height: 52px;
|
||||
background-color: #f1f3f7;
|
||||
line-height: 52px;
|
||||
box-sizing: border-box;
|
||||
border-top-left-radius: 6px;
|
||||
border-top-right-radius: 6px;
|
||||
border-bottom: 1px solid #dddddd;
|
||||
.el-dialog__headerbtn {
|
||||
width: 52px;
|
||||
height: 52px;
|
||||
}
|
||||
}
|
||||
.el-dialog__body {
|
||||
padding: 20px 30px 0 30px;
|
||||
.download {
|
||||
box-sizing: border-box;
|
||||
width: 260px;
|
||||
height: 40px;
|
||||
background-color: rgba(232, 241, 255, 1);
|
||||
border-radius: 4px;
|
||||
padding: 10px 16px;
|
||||
box-sizing: border-box;
|
||||
line-height: 20px;
|
||||
color: rgba(0, 118, 232, 1);
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
.selectbtn {
|
||||
color: #0076e8;
|
||||
border-color: #0076e8;
|
||||
// cursor: pointer;
|
||||
&:hover {
|
||||
background-color: #0076e8;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
.el-dialog__footer {
|
||||
text-align: center;
|
||||
padding: 0 0 20px 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
39
src/components/columnItem/ColumnItem.vue
Normal file
39
src/components/columnItem/ColumnItem.vue
Normal file
@ -0,0 +1,39 @@
|
||||
<template>
|
||||
<div class="column-item">
|
||||
<MyTitle v-if="!!title" :title="title" :showIcon="showIcon">
|
||||
<template #right>
|
||||
<slot name="right"></slot>
|
||||
</template>
|
||||
</MyTitle>
|
||||
<div class="cont">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, inject } from 'vue'
|
||||
import MyTitle from '../myTitle/MyTitle.vue'
|
||||
const props = defineProps({
|
||||
title: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
showIcon: {
|
||||
type: Boolean,
|
||||
default: () => true
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.column-item {
|
||||
background: #fff;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 20px;
|
||||
|
||||
.cont {
|
||||
padding: 20px;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -28,7 +28,7 @@
|
||||
v-if="config.index"
|
||||
type="index"
|
||||
align="center"
|
||||
width="50px"
|
||||
width="80px"
|
||||
label="序号"
|
||||
/>
|
||||
<template v-for="scheme in config.schemes" :key="scheme.attrs.label">
|
||||
@ -65,9 +65,9 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, onBeforeMount, onMounted, watch } from 'vue'
|
||||
import { computed, onBeforeMount, onMounted, watch, ref } from 'vue'
|
||||
import Sortable from 'sortablejs'
|
||||
import { nanoid } from '@/utils/jsencrypt'
|
||||
// import { nanoid } from '@/utils/jsencrypt'
|
||||
|
||||
const props = defineProps({
|
||||
data: {
|
||||
@ -148,9 +148,9 @@
|
||||
(data) => {
|
||||
// if (props.config.drag) {
|
||||
computedData.value = data.map((item) => {
|
||||
if (!item.nanoId) {
|
||||
item.nanoId = nanoid()
|
||||
}
|
||||
// if (!item.nanoId) {
|
||||
// item.nanoId = nanoid()
|
||||
// }
|
||||
return item
|
||||
})
|
||||
// } else {
|
||||
@ -204,7 +204,7 @@
|
||||
}
|
||||
|
||||
function handleTableCurrentChange(data) {
|
||||
console.log(data, 'data')
|
||||
// console.log(data, 'data')
|
||||
emits('update:checkData', data)
|
||||
}
|
||||
|
||||
@ -234,9 +234,9 @@
|
||||
onBeforeMount(() => {
|
||||
// if (props.config.drag) {
|
||||
computedData.value = props.data.map((item) => {
|
||||
if (!item.nanoId) {
|
||||
item.nanoId = nanoid()
|
||||
}
|
||||
// if (!item.nanoId) {
|
||||
// item.nanoId = nanoid()
|
||||
// }
|
||||
return item
|
||||
})
|
||||
// } else {
|
||||
@ -258,7 +258,7 @@
|
||||
.content {
|
||||
background: #fff;
|
||||
border-radius: 4px;
|
||||
padding: 20px 20px 0 20px;
|
||||
//padding: 20px 20px 0 20px;
|
||||
box-sizing: border-box;
|
||||
:deep(.el-table) {
|
||||
.current-row {
|
||||
|
@ -8,7 +8,7 @@
|
||||
<Editor
|
||||
v-model="valueHtml"
|
||||
class="overflow-y-hidden mt-0.5"
|
||||
style="height: 18rem"
|
||||
style="min-height: 18rem"
|
||||
:default-config="editorConfig"
|
||||
mode="default"
|
||||
@onCreated="handleCreated"
|
||||
|
236
src/components/userChoose/index.vue
Normal file
236
src/components/userChoose/index.vue
Normal file
@ -0,0 +1,236 @@
|
||||
<template>
|
||||
<PmDialog
|
||||
v-model="dialoagVisible"
|
||||
:title="'选择' + title"
|
||||
width="1200px"
|
||||
style="max-height: 50vh"
|
||||
>
|
||||
<div class="container">
|
||||
<!-- <div class="left">-->
|
||||
<!-- <el-input-->
|
||||
<!-- style="width: 80%; margin-bottom: 0.625rem"-->
|
||||
<!-- placeholder="请输入部门名称"-->
|
||||
<!-- v-model="deptName"-->
|
||||
<!-- />-->
|
||||
<!-- <el-scrollbar height="31.25rem">-->
|
||||
<!-- <el-tree-->
|
||||
<!-- ref="deptTreeRef"-->
|
||||
<!-- :data="deptTree"-->
|
||||
<!-- :props="defaultProps"-->
|
||||
<!-- :filter-node-method="filterNode"-->
|
||||
<!-- @node-click="handleNodeClick"-->
|
||||
<!-- value-key="id"-->
|
||||
<!-- placeholder="请选择归属部门"-->
|
||||
<!-- />-->
|
||||
<!-- </el-scrollbar>-->
|
||||
<!-- </div>-->
|
||||
<div class="right">
|
||||
<ColumnItem class="content">
|
||||
<el-form inline >
|
||||
<el-form-item :label="title + '名称'">
|
||||
<el-input placeholder="请输入" v-model="queryParams.nickName" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="searchData">查询</el-button>
|
||||
<el-button @click="resaltData">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</ColumnItem>
|
||||
<ColumnItem class="content">
|
||||
<el-table
|
||||
ref="treeRef"
|
||||
style="height: 50vh; overflow: auto"
|
||||
:data="tabList"
|
||||
v-loading="loading"
|
||||
:highlight-current-row="!props.multiple"
|
||||
@current-change="nodeClick"
|
||||
@selection-change="handleSelectionChange"
|
||||
row-key="ID"
|
||||
>
|
||||
<el-table-column v-if="props.multiple" type="selection" width="55" />
|
||||
<el-table-column type="index" width="50" />
|
||||
<el-table-column prop="nickName" label="用户名称" />
|
||||
<el-table-column prop="userName" label="用户账号" />
|
||||
</el-table>
|
||||
</ColumnItem>
|
||||
<el-pagination
|
||||
class="pagination"
|
||||
v-model:current-page="queryParams.page"
|
||||
background
|
||||
v-model:page-size="queryParams.pageSize"
|
||||
:total="total"
|
||||
@size-change="getStaffList"
|
||||
@current-change="getStaffList"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div style="padding: 10px; display: flex; justify-content: center">
|
||||
<el-button
|
||||
@click="requireDiolag"
|
||||
type="primary"
|
||||
:disabled="props.multiple ? choosedTableRows.length < 1 : !selectDept"
|
||||
>确定</el-button
|
||||
>
|
||||
<el-button @click="dialoagVisible = false">取消</el-button>
|
||||
</div>
|
||||
</PmDialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {onMounted, ref, watch, nextTick } from 'vue'
|
||||
import PmDialog from '@/components/PmDialog/pm-dialog.vue'
|
||||
import ColumnItem from '@/components/columnItem/ColumnItem.vue'
|
||||
import { getUserList } from '@/api/user.js'
|
||||
// import { listUser } from '@/api/system/user'
|
||||
const treeRef = ref()
|
||||
const props = defineProps({
|
||||
multiple: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: '收款人'
|
||||
}
|
||||
})
|
||||
const emit = defineEmits(['getRecipientInfo'])
|
||||
const loading = ref(false)
|
||||
const dialoagVisible = ref(false)
|
||||
const deptName = ref(null)
|
||||
const selectDept = ref(null),
|
||||
choosedTableRows = ref([]),
|
||||
unChoosed = ref([])
|
||||
// 使用Set存储选中ID(更高效的存储方式)
|
||||
const selectedIds = ref(new Set())
|
||||
// const deptTree = ref([])
|
||||
const tabList = ref([])
|
||||
const total = ref(0)
|
||||
// const defaultProps = ref({ value: 'value', label: 'label', children: 'children' })
|
||||
const queryParams = ref({
|
||||
pageSize: 10,
|
||||
page: 1,
|
||||
deptId: null,
|
||||
nickName: ''
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
// getDeptList()
|
||||
getStaffList()
|
||||
})
|
||||
const deptTreeRef = ref()
|
||||
watch(
|
||||
() => deptName.value,
|
||||
(val) => {
|
||||
deptTreeRef.value.filter(val)
|
||||
}
|
||||
)
|
||||
|
||||
function open() {
|
||||
dialoagVisible.value = true
|
||||
}
|
||||
// async function getDeptList() {
|
||||
// deptTree.value = await getDeptTree()
|
||||
// }
|
||||
let depClick = ref(false)
|
||||
function getStaffList() {
|
||||
loading.value = true
|
||||
getUserList(queryParams.value)
|
||||
.then(async (res) => {
|
||||
tabList.value = res.data.list
|
||||
total.value = res.data.total
|
||||
if(props.multiple) {
|
||||
// 下一Tick执行确保DOM更新完成
|
||||
depClick.value = true
|
||||
await nextTick(() => {
|
||||
// 根据存储的ID恢复选中状态
|
||||
tabList.value.forEach((row) => {
|
||||
if (selectedIds.value.has(row.ID)) {
|
||||
treeRef.value.toggleRowSelection(row, true)
|
||||
}
|
||||
})
|
||||
})
|
||||
depClick.value = false
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
loading.value = false
|
||||
})
|
||||
}
|
||||
function searchData() {
|
||||
getStaffList()
|
||||
}
|
||||
function resaltData() {
|
||||
queryParams.value.pageSize = 10
|
||||
queryParams.value.page = 1
|
||||
queryParams.value.nickName = ''
|
||||
getStaffList()
|
||||
}
|
||||
|
||||
// function handleNodeClick(node) {
|
||||
// queryParams.value.deptId = node.id
|
||||
// getStaffList()
|
||||
// }
|
||||
function nodeClick(e) {
|
||||
selectDept.value = e
|
||||
}
|
||||
function handleSelectionChange(selection) {
|
||||
console.log(selection)
|
||||
// 更新选中ID集合
|
||||
selection.forEach((row) => selectedIds.value.add(row.ID))
|
||||
|
||||
// 删除当前页取消选中的ID,前提是非点击部门或分页节点触发
|
||||
if (!depClick.value) {
|
||||
// 找出当前页需要删除的ID
|
||||
unChoosed.value = []
|
||||
if (selection.length > 0) {
|
||||
selection = selection.map((item) => item.ID)
|
||||
tabList.value.forEach((item) => {
|
||||
if (!selection.includes(item.ID)) {
|
||||
unChoosed.value.push(item.ID)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
unChoosed.value = JSON.parse(JSON.stringify(tabList.value)).map((item) => item.ID)
|
||||
}
|
||||
selectedIds.value.forEach((id) => {
|
||||
if (unChoosed.value.includes(id)) {
|
||||
selectedIds.value.delete(id)
|
||||
}
|
||||
})
|
||||
}
|
||||
// 更新已选数据(使用过滤确保有效性)
|
||||
choosedTableRows.value = Array.from(selectedIds.value)
|
||||
.map((id) => [...tabList.value, ...choosedTableRows.value].find((r) => r.ID === id))
|
||||
.filter(Boolean)
|
||||
}
|
||||
// function filterNode(value, data) {
|
||||
// if (!value) return true
|
||||
// return data.label.includes(value)
|
||||
// }
|
||||
function requireDiolag() {
|
||||
emit('getRecipientInfo', props.multiple ? choosedTableRows.value : selectDept.value)
|
||||
dialoagVisible.value = false
|
||||
}
|
||||
defineExpose({
|
||||
open
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.container {
|
||||
display: flex;
|
||||
.left {
|
||||
width: 17.5rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.right {
|
||||
flex: 1;
|
||||
:deep(.content:first-child) {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
:deep(.cont) {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
149
src/config.js
149
src/config.js
@ -1,3 +1,5 @@
|
||||
import {getDict} from '@/utils/dictionary'
|
||||
export let userStatus = await getDict('user-status')
|
||||
export const ORDER_SEARCH_CONFIG = [
|
||||
{
|
||||
type: 'input',
|
||||
@ -16,7 +18,53 @@ export const ORDER_SEARCH_CONFIG = [
|
||||
rangeSeparator: '-'
|
||||
},
|
||||
]
|
||||
// 经费申请列表
|
||||
export const USER_SEARCH_CONFIG = [
|
||||
{
|
||||
type: 'input',
|
||||
prop:'name',
|
||||
label: '用户名称',
|
||||
placeholder: '请输入',
|
||||
},
|
||||
{
|
||||
type: 'select',
|
||||
prop:'status',
|
||||
label: '状态',
|
||||
placeholder: '请选择',
|
||||
children:{
|
||||
list: [
|
||||
{
|
||||
label: '全部',
|
||||
value: ''
|
||||
},
|
||||
...userStatus
|
||||
]
|
||||
},
|
||||
}
|
||||
]
|
||||
export const ARTICLE_SEARCH_CONFIG = [
|
||||
{
|
||||
type: 'input',
|
||||
prop:'name',
|
||||
label: '名称',
|
||||
placeholder: '请输入',
|
||||
},
|
||||
{
|
||||
type: 'select',
|
||||
prop:'status',
|
||||
label: '状态',
|
||||
placeholder: '请选择',
|
||||
children:{
|
||||
list: [
|
||||
{
|
||||
label: '全部',
|
||||
value: ''
|
||||
},
|
||||
...userStatus
|
||||
]
|
||||
},
|
||||
}
|
||||
]
|
||||
// 订单列表
|
||||
export const ORDER_TABLE_CONFIG = {
|
||||
index: true,
|
||||
schemes: [
|
||||
@ -48,6 +96,56 @@ export const ORDER_TABLE_CONFIG = {
|
||||
prop: 'order_type'
|
||||
}
|
||||
},
|
||||
{
|
||||
attrs: {
|
||||
label: '状态',
|
||||
prop: 'status',
|
||||
align: 'center'
|
||||
},
|
||||
slot: 'status'
|
||||
},{
|
||||
attrs: {
|
||||
label: '创建时间',
|
||||
prop: 'CreatedAt',
|
||||
align: 'center'
|
||||
},
|
||||
slot: 'CreatedAt'
|
||||
},{
|
||||
attrs: {
|
||||
label: '更新时间',
|
||||
prop: 'UpdatedAt',
|
||||
align: 'center'
|
||||
},
|
||||
slot: 'UpdatedAt'
|
||||
},
|
||||
{
|
||||
attrs: {
|
||||
label: '操作',
|
||||
prop: 'operate',
|
||||
align: 'center'
|
||||
},
|
||||
slot: 'operate'
|
||||
}
|
||||
]
|
||||
}
|
||||
// 用户列表
|
||||
export const USER_TABLE_CONFIG = {
|
||||
index: true,
|
||||
schemes: [
|
||||
{
|
||||
attrs: {
|
||||
label: 'ID',
|
||||
prop: 'order_no',
|
||||
align: 'center'
|
||||
}
|
||||
},
|
||||
{
|
||||
attrs: {
|
||||
label: '名称',
|
||||
prop: 'name',
|
||||
align: 'center'
|
||||
}
|
||||
},
|
||||
{
|
||||
attrs: {
|
||||
label: '状态',
|
||||
@ -78,3 +176,52 @@ export const ORDER_TABLE_CONFIG = {
|
||||
}
|
||||
]
|
||||
}
|
||||
// 文章列表
|
||||
export const ARTICLE_TABLE_CONFIG = {
|
||||
index: true,
|
||||
schemes: [
|
||||
{
|
||||
attrs: {
|
||||
label: '封面',
|
||||
prop: 'coverImg',
|
||||
align: 'center'
|
||||
},
|
||||
slot: 'coverImg'
|
||||
},
|
||||
{
|
||||
attrs: {
|
||||
label: '名称',
|
||||
prop: 'title',
|
||||
align: 'center'
|
||||
}
|
||||
},{
|
||||
attrs: {
|
||||
label: '价格',
|
||||
prop: 'price',
|
||||
align: 'center'
|
||||
}
|
||||
},{
|
||||
attrs: {
|
||||
label: '创建时间',
|
||||
prop: 'CreatedAt',
|
||||
align: 'center'
|
||||
},
|
||||
slot: 'CreatedAt'
|
||||
},{
|
||||
attrs: {
|
||||
label: '更新时间',
|
||||
prop: 'UpdatedAt',
|
||||
align: 'center'
|
||||
},
|
||||
slot: 'UpdatedAt'
|
||||
},
|
||||
{
|
||||
attrs: {
|
||||
label: '操作',
|
||||
prop: 'operate',
|
||||
align: 'center'
|
||||
},
|
||||
slot: 'operate'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -22,6 +22,9 @@
|
||||
"/src/view/example/index.vue": "Example",
|
||||
"/src/view/example/upload/scanUpload.vue": "scanUpload",
|
||||
"/src/view/example/upload/upload.vue": "Upload",
|
||||
"/src/view/goods/article/edit.vue": "Edit",
|
||||
"/src/view/goods/article/index.vue": "Index",
|
||||
"/src/view/goods/index.vue": "goods",
|
||||
"/src/view/init/index.vue": "Init",
|
||||
"/src/view/layout/aside/asideComponent/asyncSubmenu.vue": "AsyncSubmenu",
|
||||
"/src/view/layout/aside/asideComponent/index.vue": "AsideComponent",
|
||||
@ -40,6 +43,8 @@
|
||||
"/src/view/layout/setting/title.vue": "layoutSettingTitle",
|
||||
"/src/view/layout/tabs/index.vue": "HistoryComponent",
|
||||
"/src/view/login/index.vue": "Login",
|
||||
"/src/view/order/index.vue": "OrderManage",
|
||||
"/src/view/order/order/index.vue": "Index",
|
||||
"/src/view/person/person.vue": "Person",
|
||||
"/src/view/routerHolder.vue": "RouterHolder",
|
||||
"/src/view/superAdmin/api/api.vue": "Api",
|
||||
@ -68,11 +73,9 @@
|
||||
"/src/view/systemTools/installPlugin/index.vue": "Index",
|
||||
"/src/view/systemTools/pubPlug/pubPlug.vue": "PubPlug",
|
||||
"/src/view/systemTools/system/system.vue": "Config",
|
||||
"/src/view/user/index.vue": "UserManage",
|
||||
"/src/view/user/user/index.vue": "Index",
|
||||
"/src/plugin/announcement/form/info.vue": "InfoForm",
|
||||
"/src/plugin/announcement/view/info.vue": "Info",
|
||||
"/src/plugin/email/view/index.vue": "Email",
|
||||
"/src/view/goods/index.vue": "Goods",
|
||||
"/src/view/goods/article/index.vue": "Article",
|
||||
"/src/view/order/index.vue": "OrderManage",
|
||||
"/src/view/order/order/index.vue": "Order"
|
||||
"/src/plugin/email/view/index.vue": "Email"
|
||||
}
|
@ -49,3 +49,18 @@
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
.container-wrapper{
|
||||
min-height: calc(100vh - 12rem);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
position: relative;
|
||||
}
|
||||
.footer-box{
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 1rem 0;
|
||||
position: sticky;
|
||||
bottom: 0;
|
||||
border-top: 1px solid #ededed;
|
||||
}
|
||||
|
165
src/view/goods/article/edit.vue
Normal file
165
src/view/goods/article/edit.vue
Normal 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>
|
@ -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>
|
||||
|
@ -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
21
src/view/user/index.vue
Normal 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>
|
||||
|
106
src/view/user/user/index.vue
Normal file
106
src/view/user/user/index.vue
Normal 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>
|
Loading…
Reference in New Issue
Block a user