lzq 2025-12-03 17:32:02 +08:00
parent ad0fd2ab96
commit 183b44c83a
20 changed files with 702 additions and 66 deletions

View File

@ -1,5 +1,5 @@
<template> <template>
<el-config-provider :button="buttonConfig" size="default"> <el-config-provider :button="buttonConfig" size="small">
<router-view #="{ Component }"> <router-view #="{ Component }">
<component :is="Component"/> <component :is="Component"/>
</router-view> </router-view>

View File

@ -77,7 +77,7 @@ router.beforeEach((to, from) => {
} else { } else {
return { return {
replace: true, replace: true,
name: 'menus', name: 'role',
} }
} }
} }
@ -108,6 +108,8 @@ export function reloadRouter() {
.map((it) => it.routeName) .map((it) => it.routeName)
routNames.push('menus') routNames.push('menus')
routNames.push('user')
routNames.push('role')
if (Colls.isEmpty(routNames)) { if (Colls.isEmpty(routNames)) {
return false return false

View File

@ -7,6 +7,7 @@
export {} export {}
declare global { declare global {
const EffectScope: typeof import('vue').EffectScope const EffectScope: typeof import('vue').EffectScope
const ElMessage: typeof import('element-plus/es').ElMessage
const acceptHMRUpdate: typeof import('pinia').acceptHMRUpdate const acceptHMRUpdate: typeof import('pinia').acceptHMRUpdate
const computed: typeof import('vue').computed const computed: typeof import('vue').computed
const createApp: typeof import('vue').createApp const createApp: typeof import('vue').createApp

View File

@ -5,7 +5,6 @@
// ------ // ------
// Generated by unplugin-vue-components // Generated by unplugin-vue-components
// Read more: https://github.com/vuejs/core/pull/3399 // Read more: https://github.com/vuejs/core/pull/3399
import { GlobalComponents } from 'vue'
export {} export {}
@ -15,25 +14,35 @@ declare module 'vue' {
ElAside: typeof import('element-plus/es')['ElAside'] ElAside: typeof import('element-plus/es')['ElAside']
ElAvatar: typeof import('element-plus/es')['ElAvatar'] ElAvatar: typeof import('element-plus/es')['ElAvatar']
ElButton: typeof import('element-plus/es')['ElButton'] ElButton: typeof import('element-plus/es')['ElButton']
ElButtonGroup: typeof import('element-plus/es')['ElButtonGroup']
ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
ElCheckboxGroup: typeof import('element-plus/es')['ElCheckboxGroup']
ElConfigProvider: typeof import('element-plus/es')['ElConfigProvider'] ElConfigProvider: typeof import('element-plus/es')['ElConfigProvider']
ElContainer: typeof import('element-plus/es')['ElContainer'] ElContainer: typeof import('element-plus/es')['ElContainer']
ElDialog: typeof import('element-plus/es')['ElDialog'] ElDialog: typeof import('element-plus/es')['ElDialog']
ElDropdown: typeof import('element-plus/es')['ElDropdown'] ElDropdown: typeof import('element-plus/es')['ElDropdown']
ElEmpty: typeof import('element-plus/es')['ElEmpty']
ElForm: typeof import('element-plus/es')['ElForm'] ElForm: typeof import('element-plus/es')['ElForm']
ElFormItem: typeof import('element-plus/es')['ElFormItem'] ElFormItem: typeof import('element-plus/es')['ElFormItem']
ElHeader: typeof import('element-plus/es')['ElHeader'] ElHeader: typeof import('element-plus/es')['ElHeader']
ElIcon: typeof import('element-plus/es')['ElIcon'] ElIcon: typeof import('element-plus/es')['ElIcon']
ElIconFilter: typeof import('@element-plus/icons-vue')['Filter'] ElIconFilter: typeof import('@element-plus/icons-vue')['Filter']
ElIconPicture: typeof import('@element-plus/icons-vue')['Picture']
ElImage: typeof import('element-plus/es')['ElImage']
ElInput: typeof import('element-plus/es')['ElInput'] ElInput: typeof import('element-plus/es')['ElInput']
ElInputNumber: typeof import('element-plus/es')['ElInputNumber'] ElInputNumber: typeof import('element-plus/es')['ElInputNumber']
ElMain: typeof import('element-plus/es')['ElMain'] ElMain: typeof import('element-plus/es')['ElMain']
ElOption: typeof import('element-plus/es')['ElOption'] ElOption: typeof import('element-plus/es')['ElOption']
ElPagination: typeof import('element-plus/es')['ElPagination'] ElPagination: typeof import('element-plus/es')['ElPagination']
ElSelect: typeof import('element-plus/es')['ElSelect'] ElSelect: typeof import('element-plus/es')['ElSelect']
ElSwitch: typeof import('element-plus/es')['ElSwitch']
ElTable: typeof import('element-plus/es')['ElTable'] ElTable: typeof import('element-plus/es')['ElTable']
ElTableColumn: typeof import('element-plus/es')['ElTableColumn'] ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
ElTabPane: typeof import('element-plus/es')['ElTabPane'] ElTabPane: typeof import('element-plus/es')['ElTabPane']
ElTabs: typeof import('element-plus/es')['ElTabs'] ElTabs: typeof import('element-plus/es')['ElTabs']
ElTag: typeof import('element-plus/es')['ElTag']
ElTooltip: typeof import('element-plus/es')['ElTooltip']
ElTransfer: typeof import('element-plus/es')['ElTransfer']
ElTreeSelect: typeof import('element-plus/es')['ElTreeSelect'] ElTreeSelect: typeof import('element-plus/es')['ElTreeSelect']
ElUpload: typeof import('element-plus/es')['ElUpload'] ElUpload: typeof import('element-plus/es')['ElUpload']
RouterLink: typeof import('vue-router')['RouterLink'] RouterLink: typeof import('vue-router')['RouterLink']
@ -49,27 +58,37 @@ declare global {
const ElAside: typeof import('element-plus/es')['ElAside'] const ElAside: typeof import('element-plus/es')['ElAside']
const ElAvatar: typeof import('element-plus/es')['ElAvatar'] const ElAvatar: typeof import('element-plus/es')['ElAvatar']
const ElButton: typeof import('element-plus/es')['ElButton'] const ElButton: typeof import('element-plus/es')['ElButton']
const ElButtonGroup: typeof import('element-plus/es')['ElButtonGroup']
const ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
const ElCheckboxGroup: typeof import('element-plus/es')['ElCheckboxGroup']
const ElConfigProvider: typeof import('element-plus/es')['ElConfigProvider'] const ElConfigProvider: typeof import('element-plus/es')['ElConfigProvider']
const ElContainer: typeof import('element-plus/es')['ElContainer'] const ElContainer: typeof import('element-plus/es')['ElContainer']
const ElDialog: typeof import('element-plus/es')['ElDialog'] const ElDialog: typeof import('element-plus/es')['ElDialog']
const ElDropdown: typeof import('element-plus/es')['ElDropdown'] const ElDropdown: typeof import('element-plus/es')['ElDropdown']
const ElEmpty: typeof import('element-plus/es')['ElEmpty']
const ElForm: typeof import('element-plus/es')['ElForm'] const ElForm: typeof import('element-plus/es')['ElForm']
const ElFormItem: typeof import('element-plus/es')['ElFormItem'] const ElFormItem: typeof import('element-plus/es')['ElFormItem']
const ElHeader: typeof import('element-plus/es')['ElHeader'] const ElHeader: typeof import('element-plus/es')['ElHeader']
const ElIcon: typeof import('element-plus/es')['ElIcon'] const ElIcon: typeof import('element-plus/es')['ElIcon']
const ElIconFilter: typeof import('@element-plus/icons-vue')['Filter'] const ElIconFilter: typeof import('@element-plus/icons-vue')['Filter']
const ElIconPicture: typeof import('@element-plus/icons-vue')['Picture']
const ElImage: typeof import('element-plus/es')['ElImage']
const ElInput: typeof import('element-plus/es')['ElInput'] const ElInput: typeof import('element-plus/es')['ElInput']
const ElInputNumber: typeof import('element-plus/es')['ElInputNumber'] const ElInputNumber: typeof import('element-plus/es')['ElInputNumber']
const ElMain: typeof import('element-plus/es')['ElMain'] const ElMain: typeof import('element-plus/es')['ElMain']
const ElOption: typeof import('element-plus/es')['ElOption'] const ElOption: typeof import('element-plus/es')['ElOption']
const ElPagination: typeof import('element-plus/es')['ElPagination'] const ElPagination: typeof import('element-plus/es')['ElPagination']
const ElSelect: typeof import('element-plus/es')['ElSelect'] const ElSelect: typeof import('element-plus/es')['ElSelect']
const ElSwitch: typeof import('element-plus/es')['ElSwitch']
const ElTable: typeof import('element-plus/es')['ElTable'] const ElTable: typeof import('element-plus/es')['ElTable']
const ElTableColumn: typeof import('element-plus/es')['ElTableColumn'] const ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
const ElTabPane: typeof import('element-plus/es')['ElTabPane'] const ElTabPane: typeof import('element-plus/es')['ElTabPane']
const ElTabs: typeof import('element-plus/es')['ElTabs'] const ElTabs: typeof import('element-plus/es')['ElTabs']
const ElTag: typeof import('element-plus/es')['ElTag']
const ElTooltip: typeof import('element-plus/es')['ElTooltip']
const ElTransfer: typeof import('element-plus/es')['ElTransfer']
const ElTreeSelect: typeof import('element-plus/es')['ElTreeSelect'] const ElTreeSelect: typeof import('element-plus/es')['ElTreeSelect']
const ElUpload: typeof import('element-plus/es')['ElUpload'] const ElUpload: typeof import('element-plus/es')['ElUpload']
const RouterLink: typeof import('vue-router')['RouterLink'] const RouterLink: typeof import('vue-router')['RouterLink']
const RouterView: typeof import('vue-router')['RouterView'] const RouterView: typeof import('vue-router')['RouterView']
} }

4
src/dts/g.d.ts vendored
View File

@ -20,8 +20,8 @@ declare global {
} }
interface PageParam { interface PageParam {
current: number current?: number
size: number size?: number
orders?: string orders?: string
} }

View File

@ -1,11 +1,13 @@
import { import {
ElButton,
ElIcon,
ElMenu, ElMenu,
ElMenuItem, ElMenuItem,
ElMenuItemGroup, ElMenuItemGroup,
ElSubMenu, ElSubMenu,
type MenuItemRegistered, type MenuItemRegistered,
} from 'element-plus' } from 'element-plus'
import { elIcons } from '@/common/element/element.ts'
export interface Menu extends G.TreeNode { export interface Menu extends G.TreeNode {
// Id // Id
@ -72,10 +74,24 @@ export default defineComponent(
} }
return currentNode return currentNode
} }
const isCollapse = ref(false)
return () => (<ElMenu>{{ return () => (<>
default: () => menus.map(renderMenu), <ElMenu collapse={isCollapse.value} style={{height: '100%', overflow: 'auto', '--el-menu-base-level-padding': '10px'}} class={'menus'}>
}}</ElMenu>) {{
default: () => menus.map(renderMenu),
}}
</ElMenu>
<ElButton style={{position: 'absolute', right: 0, bottom: 0, width: '32px', height: '32px'}} onClick={() => {
isCollapse.value = !isCollapse.value
}}>
<ElIcon style={{cursor: 'pointer'}}>
{
isCollapse.value ? <elIcons.Fold/> : <elIcons.Expand/>
}
</ElIcon>
</ElButton>
</>)
}, },
{ {
props: { props: {

View File

@ -102,12 +102,8 @@ onUnmounted(() => {
& > aside { & > aside {
height 100%; height 100%;
width 300px; width auto
position relative
& > ul {
height 100%
overflow auto
}
} }
& > main { & > main {
@ -120,4 +116,10 @@ onUnmounted(() => {
} }
} }
} }
</style>
<style>
.menus:not(.el-menu--collapse) {
width: 230px;
}
</style> </style>

View File

@ -17,6 +17,7 @@ const loginFormIns = ref<FormInstance>()
const loginForm = reactive<LoginTypes.LoginForm>({ const loginForm = reactive<LoginTypes.LoginForm>({
account: '', account: '',
secret: '', secret: '',
clientCode: 0,
}) })
const rules = reactive<FormRules<LoginTypes.LoginForm>>({ const rules = reactive<FormRules<LoginTypes.LoginForm>>({
account: [ account: [

View File

@ -4,6 +4,7 @@ declare global {
interface LoginForm { interface LoginForm {
account: string account: string
secret: string secret: string
clientCode: 0 | 1
} }
interface UserSetting { interface UserSetting {

View File

@ -133,10 +133,10 @@ onMounted(() => {
width 100% width 100%
:deep(.table-header) { :deep(.table-header) {
color black color #454C59
th { th {
background-color #E1E5EB background-color #EDF1F7
font-weight 500 font-weight 500
position relative position relative
@ -151,7 +151,7 @@ onMounted(() => {
top: 50%; top: 50%;
left: 1px; left: 1px;
width: 1px; width: 1px;
background-color: #A6AFB5; background-color: #D3D7DE;
transform: translateY(-50%); transform: translateY(-50%);
content: ""; content: "";
height 50% height 50%

View File

@ -0,0 +1,10 @@
import { get } from '@/common/utils/http-util.ts'
export default {
list(data?: ResourceTypes.SearchResourceParam) {
return get<ResourceTypes.SearchResourceResult[]>('/resource/list', data)
},
listRoleRes(roleId: string) {
return get<ResourceTypes.SearchResourceResult[]>('/resource/list_role_res', {roleId})
},
}

View File

@ -0,0 +1,57 @@
export {}
declare global {
namespace ResourceTypes {
interface SearchResourceParam extends G.PageParam {
// Id
id?: string
// 编号
sn?: string
// 表名称
tableName?: string
// 数据行 Id
dataId?: string
// 备注
memo?: string
keywords?: string
}
interface SearchResourceResult {
// Id
id?: string
// 编号
sn?: string
// 表名称
tableName?: string
// 数据行 Id
dataId?: string
// 备注
memo?: string
}
interface AddResourceParam {
// Id
id?: string
// 编号
sn?: string
// 表名称
tableName?: string
// 数据行 Id
dataId?: string
// 备注
memo?: string
}
interface ModifyResourceParam {
// Id
id?: string
// 编号
sn?: string
// 表名称
tableName?: string
// 数据行 Id
dataId?: string
// 备注
memo?: string
}
}
}

View File

@ -0,0 +1,101 @@
<template>
<ElDialog v-model="showDialog"
:close-on-click-modal="false"
destroy-on-close
width="900">
<ElTransfer
v-model="rightValue"
:button-texts="['解绑', '绑定']"
:data="allRes"
:filter-method="filterMethod"
:props="{
key: 'id',
label: 'roleName',
}"
:titles="['资源列表', '已有资源']"
class="transfer"
filter-placeholder="搜索"
filterable
>
<template #default="{ option }">
<ElTooltip
:content="option.sn"
placement="top">
<span> {{ option.memo }} </span>
</ElTooltip>
</template>
<template #left-empty>
<el-empty :image-size="60" description="暂无数据"/>
</template>
<template #right-empty>
<el-empty :image-size="60" description="暂无数据"/>
</template>
</ElTransfer>
<template #footer>
<ElButton @click="showDialog = false">取消</ElButton>
<ElButton :disabled="rightValue.length <= 0" :loading="submiting" type="primary" @click="submitHandler"></ElButton>
</template>
</ElDialog>
</template>
<script lang="ts" setup>
import RoleApi from '@/pages/sys/role/role-api.ts'
import Strings from '@/common/utils/strings.ts'
import {
ElMessage,
type TransferDataItem,
} from 'element-plus'
import ResourceApi from '@/pages/sys/resource/resource-api.ts'
const showDialog = ref(false)
const submiting = ref(false)
const hadRes = ref<ResourceTypes.SearchResourceResult[]>([])
const allRes = ref<ResourceTypes.SearchResourceResult[]>([])
const rightValue = ref<string[]>([])
const currentUserId = ref<string>('')
function filterMethod(query: string, item: TransferDataItem) {
return Strings.isBlank(query) || (item as ResourceTypes.SearchResourceResult).memo!.includes(query) || (item as ResourceTypes.SearchResourceResult).memo!.includes(query)
}
function submitHandler() {
submiting.value = true
RoleApi.bindRes({
id: currentUserId.value,
res: rightValue.value,
}).then(() => {
ElMessage.success('修改成功')
showDialog.value = false
})
.finally(() => {
submiting.value = false
})
}
defineExpose({
open(roleId: string) {
currentUserId.value = roleId
showDialog.value = true
ResourceApi.listRoleRes(roleId)
.then(res => {
hadRes.value = res.data ?? []
rightValue.value = hadRes.value.map(it => it.id!)
})
ResourceApi.list()
.then(res => {
allRes.value = res.data ?? []
})
},
})
</script>
<style lang="stylus" scoped>
.transfer {
width: 100%;
display: flex;
justify-content: center;
align-items: center;
:deep(.el-transfer-panel) {
flex 1
}
}
</style>

View File

@ -0,0 +1,31 @@
import {
get,
post,
} from '@/common/utils/http-util.ts'
export default {
paging(data: RoleTypes.SearchRoleParam) {
return get<G.PageResult<RoleTypes.SearchRoleResult>>('/role/paging', data)
},
detail(id: string) {
return get<RoleTypes.SearchRoleResult>('/role/detail', {id})
},
add(data: RoleTypes.AddRoleParam) {
return post('/role/add', data)
},
modify(data: RoleTypes.ModifyRoleParam) {
return post('/role/modify', data)
},
del(ids: string[]) {
return post('/role/del', ids)
},
listUserRole(userId: string) {
return get<RoleTypes.SearchRoleResult[]>('/role/list_user_role', {userId})
},
list(data?: RoleTypes.SearchRoleParam) {
return get<RoleTypes.SearchRoleResult[]>('/role/list', data)
},
bindRes(data: RoleTypes.BindResParam) {
return post('/role/bind_res', data)
},
}

54
src/pages/sys/role/role.d.ts vendored 100644
View File

@ -0,0 +1,54 @@
export {}
declare global {
namespace RoleTypes {
interface SearchRoleParam extends G.PageParam {
// Id
id?: string
// 角色代码
roleCode?: string
// 角色名称
roleName?: string
// 备注
memo?: string
}
interface SearchRoleResult {
// Id
id?: string
// 角色代码
roleCode?: string
// 角色名称
roleName?: string
// 备注
memo?: string
}
interface AddRoleParam {
// Id
id?: string
// 角色代码
roleCode?: string
// 角色名称
roleName?: string
// 备注
memo?: string
}
interface ModifyRoleParam {
// Id
id?: string
// 角色代码
roleCode?: string
// 角色名称
roleName?: string
// 备注
memo?: string
}
interface BindResParam {
id?: string
res?: string[]
}
}
}

View File

@ -0,0 +1,97 @@
<template>
<ElDialog v-model="showDialog"
:close-on-click-modal="false"
destroy-on-close
width="700">
<ElTransfer
v-model="rightValue"
:button-texts="['解绑', '绑定']"
:data="allRoles"
:filter-method="filterMethod"
:props="{
key: 'id',
label: 'roleName',
}"
:titles="['角色列表', '已有角色']"
class="transfer"
filter-placeholder="搜索"
filterable
>
<template #default="{ option }">
<ElTooltip
:content="option.roleCode"
placement="top">
<span> {{ option.roleName }} </span>
</ElTooltip>
</template>
<template #left-empty>
<el-empty :image-size="60" description="暂无数据"/>
</template>
<template #right-empty>
<el-empty :image-size="60" description="暂无数据"/>
</template>
</ElTransfer>
<template #footer>
<ElButton @click="showDialog = false">取消</ElButton>
<ElButton :disabled="rightValue.length <= 0" :loading="submiting" type="primary" @click="submitHandler"></ElButton>
</template>
</ElDialog>
</template>
<script lang="ts" setup>
import RoleApi from '@/pages/sys/role/role-api.ts'
import Strings from '@/common/utils/strings.ts'
import {
ElMessage,
type TransferDataItem,
} from 'element-plus'
import UserApi from '@/pages/sys/user/user-api.ts'
const showDialog = ref(false)
const submiting = ref(false)
const hadRoles = ref<RoleTypes.SearchRoleResult[]>([])
const allRoles = ref<RoleTypes.SearchRoleResult[]>([])
const rightValue = ref<string[]>([])
const currentUserId = ref<string>('')
function filterMethod(query: string, item: TransferDataItem) {
return Strings.isBlank(query) || (item as RoleTypes.SearchRoleResult).roleCode!.includes(query) || (item as RoleTypes.SearchRoleResult).roleName!.includes(query)
}
function submitHandler() {
submiting.value = true
UserApi.bindRole({
id: currentUserId.value,
roles: rightValue.value,
}).then(() => {
ElMessage.success('修改成功')
showDialog.value = false
})
.finally(() => {
submiting.value = false
})
}
defineExpose({
open(userId: string) {
currentUserId.value = userId
showDialog.value = true
RoleApi.listUserRole(userId)
.then(res => {
hadRoles.value = res.data ?? []
rightValue.value = hadRoles.value.map(it => it.id!)
})
RoleApi.list()
.then(res => {
allRoles.value = res.data ?? []
})
},
})
</script>
<style lang="stylus" scoped>
.transfer {
width: 100%;
display: flex;
justify-content: center;
align-items: center;
}
</style>

View File

@ -1,22 +1,22 @@
<template> <template>
<Page> <Page>
<ElForm v-show="showSearchForm" inline @submit.prevent="paging"> <ElForm v-show="showSearchForm" inline @submit.prevent="paging">
<ElFormItem label="昵称"> <ElFormItem label="昵称">
<ElInput <ElInput
v-model="searchForm.nickname" v-model="searchForm.nickname"
placeholder="昵称"/> placeholder="昵称"/>
</ElFormItem> </ElFormItem>
<ElFormItem label="头像"> <ElFormItem label="头像">
<ElInput <ElInput
v-model="searchForm.avatar" v-model="searchForm.avatar"
placeholder="头像"/> placeholder="头像"/>
</ElFormItem> </ElFormItem>
<ElFormItem label="邮箱"> <ElFormItem label="邮箱">
<ElInput <ElInput
v-model="searchForm.email" v-model="searchForm.email"
placeholder="邮箱"/> placeholder="邮箱"/>
</ElFormItem> </ElFormItem>
<ElFormItem label="手机号"> <ElFormItem label="手机号">
<ElInput <ElInput
v-model="searchForm.phone" v-model="searchForm.phone"
placeholder="手机号"/> placeholder="手机号"/>
@ -36,22 +36,50 @@
empty-text="暂无数据" empty-text="暂无数据"
header-row-class-name="table-header" header-row-class-name="table-header"
row-key="id"> row-key="id">
<ElTableColumn label="昵称" prop="nickname"/> <ElTableColumn label="昵称" prop="nickname"/>
<ElTableColumn label="头像" prop="avatar" width="60">
<ElTableColumn label="头像" prop="avatar"/> <template #default="{row}">
<ElImage :src="AppApi.fileUrl(row.avatar)" class="avatar">
<ElTableColumn label="邮箱" prop="email"/> <template #error>
<ElIcon>
<ElTableColumn label="手机号" prop="phone"/> <ElIconPicture/>
</ElIcon>
</template>
</ElImage>
</template>
</ElTableColumn>
<ElTableColumn label="联系电话" prop="phone"/>
<ElTableColumn label="登录手机号" prop="account.phone"/>
<ElTableColumn label="用户名" prop="account.username"/>
<ElTableColumn label="微信标识" prop="account.wechatOpenid"/>
<ElTableColumn label="已授权客户端" prop="account.clientCode">
<template #default="{row}">
<ElCheckboxGroup v-model="row.account.clients" :disabled="row.id == '1'" @change="clientChangeHandler($event,row)">
<ElCheckbox :value="0" label="电脑端"/>
<ElCheckbox :value="1" label="微信小程序"/>
</ElCheckboxGroup>
</template>
</ElTableColumn>
<ElTableColumn label="是否禁用" prop="account.disabled">
<template #default="{row}">
<ElSwitch v-model="row.account.disabled" :disabled="row.id == '1'" @change="disabledUserHandler($event,row.id)"/>
</template>
</ElTableColumn>
<ElTableColumn label="注册时间" prop="account.regdate" width="170"/>
<ElTableColumn label="操作" width="180"> <ElTableColumn label="操作" width="180">
<template #default="scope"> <template #default="scope">
<ElButton text type="danger" @click="delHandler(scope)"></ElButton> <!-- <ElButton text type="danger" :loading="deling" @click="delHandler(scope)"></ElButton> -->
<ElButton text type="primary" @click="modifyHandler(scope)"></ElButton> <div class="action-btn">
<ElButton text type="primary" @click="bindRoleHandler(scope)"></ElButton>
<ElButton v-if="scope.row.id != '1'" text type="primary" @click="modifyHandler(scope)"></ElButton>
<ElButton v-if="scope.row.id != '1'" text type="primary" @click="resetPasswd(scope)"></ElButton>
</div>
</template> </template>
</ElTableColumn> </ElTableColumn>
</ElTable> </ElTable>
<UserForm ref="userForm"/> <UserForm ref="userForm" @edit-succ="paging"/>
<BindRole ref="bindRole"/>
</Page> </Page>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
@ -59,40 +87,117 @@ import UserApi from '@/pages/sys/user/user-api.ts'
import Page from '@/components/page/Page.vue' import Page from '@/components/page/Page.vue'
import { elIcons } from '@/common/element/element.ts' import { elIcons } from '@/common/element/element.ts'
import UserForm from '@/pages/sys/user/UserForm.vue' import UserForm from '@/pages/sys/user/UserForm.vue'
import {
type CheckboxValueType,
ElMessage,
} from 'element-plus'
import AppApi from '@/common/app/app-api.ts'
import BindRole from '@/pages/sys/user/BindRole.vue'
const tableData = ref<UserTypes.SearchUserResult[]>([]) const tableData = ref<UserTypes.SearchUserResult[]>([])
const searchForm = reactive<UserTypes.SearchUserParam>({ const searchForm = reactive<UserTypes.SearchUserParam>({
current: 1, current: 1,
size: 20, size: 20,
}) })
const searching = ref(false) const searching = ref(false)
// const deling = ref(false)
const showSearchForm = ref(true) const showSearchForm = ref(true)
const userFormIns = useTemplateRef<InstanceType<typeof UserForm>>('userForm') const userFormIns = useTemplateRef<InstanceType<typeof UserForm>>('userForm')
const bindRoleIns = useTemplateRef<InstanceType<typeof BindRole>>('bindRole')
function showDialog(data?: UserTypes.SearchUserResult) { function showDialog(data?: UserTypes.SearchUserResult) {
userFormIns.value?.open(data) userFormIns.value?.open(data)
} }
function delHandler({row}: { row: UserTypes.SearchUserResult }) {
UserApi.del([ row.id! ]) /* function delHandler({row}: { row: UserTypes.SearchUserResult }) {
} if (row.id == '1') {
ElMessage.error('不能删除管理员')
return
}
deling.value = true
UserApi.del([ row.id! ])
.then(() => {
ElMessage.success('删除成功')
paging()
})
.finally(() => {
deling.value = false
})
} */
function modifyHandler({row}: { row: UserTypes.SearchUserResult }) { function modifyHandler({row}: { row: UserTypes.SearchUserResult }) {
if (row.id == '1') {
ElMessage.error('不能修改管理员')
return
}
showDialog(row) showDialog(row)
} }
function resetPasswd({row}: { row: UserTypes.SearchUserResult }) {
if (row.id == '1') {
ElMessage.error('不能修改管理员')
return
}
UserApi.resetPasswd(row.id!)
.then((res) => {
ElMessage.success(res.msg)
})
}
function bindRoleHandler({row}: { row: UserTypes.SearchUserResult }) {
bindRoleIns.value?.open(row.id!)
}
function addHandler() { function addHandler() {
showDialog() showDialog()
} }
function reset() { function reset() {
Object.assign(searchForm, {}) Object.assign(searchForm, {})
paging() paging()
} }
function clientChangeHandler(clients: CheckboxValueType[], data: UserTypes.SearchUserResult) {
if (data.id == '1') {
ElMessage.error('不能操作管理员')
return
}
searching.value = true
UserApi.bindClient(data.id!, clients as number[])
.then(() => {
ElMessage.success('修改成功')
paging()
})
}
function disabledUserHandler(val: string | number | boolean, id: string) {
if (id == '1') {
ElMessage.error('不能操作管理员')
return
}
searching.value = true
UserApi.disable(id, val as boolean)
.then(() => {
ElMessage.success(val ? '禁用成功' : '启用成功')
paging()
})
}
function paging() { function paging() {
searching.value = true searching.value = true
UserApi.paging(searchForm) UserApi.paging(searchForm)
.then(res => { .then(res => {
tableData.value = res.data?.records ?? [] tableData.value = res.data?.records ?? []
tableData.value.map(it => {
it.account.clients = UserApi.clients(it.account.clientCode!).map(it => it.val)
return it
})
}) })
.finally(() => { .finally(() => {
searching.value = false searching.value = false
}) })
} }
onMounted(() => { onMounted(() => {
paging() paging()
}) })
@ -101,36 +206,64 @@ onMounted(() => {
.table-list { .table-list {
flex 1; flex 1;
width 100% width 100%
:deep(.table-header) { :deep(.table-header) {
color black color #454C59
th { th {
background-color #E1E5EB background-color #EDF1F7
font-weight 500 font-weight 500
position relative position relative
& > div { & > div {
display flex display flex
gap 5px gap 5px
align-items center align-items center
} }
&:not(:first-child) > div::before { &:not(:first-child) > div::before {
position: absolute; position: absolute;
top: 50%; top: 50%;
left: 1px; left: 1px;
width: 1px; width: 1px;
background-color: #A6AFB5; background-color: #D3D7DE;
transform: translateY(-50%); transform: translateY(-50%);
content: ""; content: "";
height 50% height 50%
} }
} }
} }
:deep(.table-cell) { :deep(.table-cell) {
color #2F3540 color #2F3540
} }
.action-btn {
width 100%
display flex
flex-wrap wrap
& > button {
margin 0
}
}
} }
.tool-bar { .tool-bar {
display flex display flex
justify-content space-between justify-content space-between
margin 0 0 20px 0 margin 0 0 20px 0
} }
.avatar {
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
:deep(.el-icon) {
font-size 25px
}
}
</style> </style>

View File

@ -6,29 +6,42 @@
<ElForm :model="userFormData" <ElForm :model="userFormData"
class="sys_user-form" class="sys_user-form"
label-width="auto"> label-width="auto">
<ElFormItem label="昵称"> <ElFormItem label="昵称">
<ElInput <ElInput
v-model="userFormData.nickname" v-model="userFormData.nickname"
:disabled="status === 'view'" :disabled="status === 'view'"
placeholder="昵称"/> placeholder="昵称"/>
</ElFormItem> </ElFormItem>
<ElFormItem label="头像"> <ElFormItem label="头像">
<ElInput <Uploader
v-model="userFormData.avatar" v-model:file="userFormData.avatar"
:disabled="status === 'view'" :disabled="status === 'view'"
placeholder="头像"/> :limit="1"
:multiple="true"
accept="image/*"
class="avatar-uploader"
list-type="picture">
<ElButton>点击上传头像</ElButton>
</Uploader>
</ElFormItem> </ElFormItem>
<ElFormItem label="邮箱"> <ElFormItem label="联系电话">
<ElInput
v-model="userFormData.email"
:disabled="status === 'view'"
placeholder="邮箱"/>
</ElFormItem>
<ElFormItem label="手机号">
<ElInput <ElInput
v-model="userFormData.phone" v-model="userFormData.phone"
:disabled="status === 'view'" :disabled="status === 'view'"
placeholder="手机号"/> placeholder="联系电话"/>
</ElFormItem>
<ElFormItem v-if="status === 'add'" label="用户名">
<ElInput
v-model="userFormData.account.username"
placeholder="用户名"/>
</ElFormItem>
<ElFormItem v-if="status === 'add'" label="密码">
<ElInput
v-model="userFormData.account.secret"
autocomplete="new-password"
placeholder="密码"
show-password
type="password"/>
</ElFormItem> </ElFormItem>
</ElForm> </ElForm>
<template #footer> <template #footer>
@ -41,17 +54,28 @@
import UserApi from '@/pages/sys/user/user-api.ts' import UserApi from '@/pages/sys/user/user-api.ts'
import Strings from '@/common/utils/strings.ts' import Strings from '@/common/utils/strings.ts'
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
import Uploader from '@/components/uploader/Uploader.vue'
const emits = defineEmits([ 'editSucc' ])
const showDialog = ref(false) const showDialog = ref(false)
const submiting = ref(false) const submiting = ref(false)
const status = ref<'add' | 'view' | 'modify'>('add') const status = ref<'add' | 'view' | 'modify'>('add')
const userFormData = reactive<UserTypes.SearchUserResult>({}) const userFormData = reactive<UserTypes.SearchUserResult>({
account: {
username: '',
secret: '',
},
})
function submitHandler() { function submitHandler() {
if (status.value === 'view') return if (status.value === 'view') return
submiting.value = true submiting.value = true
if (userFormData.id != null) { if (userFormData.id != null) {
UserApi.modify(userFormData) UserApi.modify(userFormData)
.then(() => { .then(() => {
ElMessage.success('修改成功') ElMessage.success('修改成功')
emits('editSucc')
showDialog.value = false
}) })
.finally(() => { .finally(() => {
submiting.value = false submiting.value = false
@ -60,30 +84,44 @@ function submitHandler() {
UserApi.add(userFormData) UserApi.add(userFormData)
.then(() => { .then(() => {
ElMessage.success('添加成功') ElMessage.success('添加成功')
emits('editSucc')
showDialog.value = false
}) })
.finally(() => { .finally(() => {
submiting.value = false submiting.value = false
}) })
} }
} }
defineExpose({ defineExpose({
open(data: UserTypes.SearchUserResult = {}) { open(data?: UserTypes.SearchUserResult) {
showDialog.value = true showDialog.value = true
if (!Strings.isBlank(data.id)) { if (data != null && !Strings.isBlank(data.id)) {
status.value = 'modify' status.value = 'modify'
UserApi.detail(data.id!) UserApi.detail(data.id!)
.then(res => { .then(res => {
Object.assign(userFormData, res.data) Object.assign(userFormData, {
...res.data, account: {},
})
}) })
} else { } else {
status.value = 'add' status.value = 'add'
Object.assign(userFormData, {}) Object.assign(userFormData, {
account: {
username: '',
secret: '',
},
})
} }
} },
}) })
</script> </script>
<style lang="stylus" scoped> <style lang="stylus" scoped>
.sys_user-form { .sys_user-form {
padding 20px padding 20px
} }
.avatar-uploader {
width 100%
}
</style> </style>

View File

@ -1,21 +1,57 @@
import { import {
get, get,
post post,
} from '@/common/utils/http-util.ts' } from '@/common/utils/http-util.ts'
const Clients = [ {
txt: '电脑端',
val: 0,
}, {
txt: '微信小程序',
val: 1,
} ]
export default { export default {
paging(data: UserTypes.SearchUserParam) { paging(data: UserTypes.SearchUserParam) {
return get<G.PageResult<UserTypes.SearchUserResult>>('/sys_user/paging', data) return get<G.PageResult<UserTypes.SearchUserResult>>('/user/paging', data)
}, },
detail(id: string) { detail(id: string) {
return get<UserTypes.SearchUserResult>('/sys_user/detail', {id}) return get<UserTypes.SearchUserResult>('/user/detail', {id})
},
disable(id: string, disable: boolean) {
return get('/user/disable', {id, disable})
}, },
add(data: UserTypes.AddUserParam) { add(data: UserTypes.AddUserParam) {
return post('/sys_user/add', data) return post('/user/add', data)
},
bindRole(data: UserTypes.BindRoleParam) {
return post('/user/bind_role', data)
}, },
modify(data: UserTypes.ModifyUserParam) { modify(data: UserTypes.ModifyUserParam) {
return post('/sys_user/modify', data) return post('/user/modify', data)
},
modifyInfo(data: { nickname?: string; avatar?: string; phone?: string }) {
return post('/user/modify_info', data)
},
modifyPasswd(data: { oldPasswd: string; newPasswd: string }) {
return post('/user/modify_passwd', data)
},
resetPasswd(id: string) {
return get('/user/reset_passwd', {id})
}, },
del(ids: string[]) { del(ids: string[]) {
return post('/sys_user/del', ids) return post('/user/del', ids)
},
clients(clientCode: number) {
return Clients.filter(it => {
const mask = 1 << it.val
return (clientCode & mask) === 0
})
},
bindClient(id: string, clients: number[]) {
let clientCode = (1 << Clients.length) - 1
for (let client of clients) {
clientCode = (1 << client) ^ clientCode
}
return get('/user/bind_client', {id, clientCode})
}, },
} }

View File

@ -11,7 +11,33 @@ declare global {
// 手机号 // 手机号
phone?: string phone?: string
} }
interface SearchUserAccountResult {
// Id
id?: string
// 用户 Id
userId?: string
// 用户名
username?: string
// 手机号
phone?: string
// 密码
secret?: string
// 微信 openid
wechatOpenid?: string
// 微信 unionid
wechatUnionid?: string
// 注册时间
regdate?: string
// 允许登录的客户端
clientCode?: number
clients?: number[]
// 是否禁用
disabled?: boolean
}
interface SearchUserResult { interface SearchUserResult {
id?: string
// 昵称 // 昵称
nickname?: string nickname?: string
// 头像 // 头像
@ -20,7 +46,10 @@ declare global {
email?: string email?: string
// 手机号 // 手机号
phone?: string phone?: string
account: SearchUserAccountResult
} }
interface AddUserParam { interface AddUserParam {
// 昵称 // 昵称
nickname?: string nickname?: string
@ -31,6 +60,14 @@ declare global {
// 手机号 // 手机号
phone?: string phone?: string
} }
interface BindRoleParam {
// 昵称
id?: string
// 头像
roles?: string[]
}
interface ModifyUserParam { interface ModifyUserParam {
// 昵称 // 昵称
nickname?: string nickname?: string