feat:增加主包(tabbar)、system(系统管理)、infra(基础设施)、bpm(工作流程)的页面
This commit is contained in:
96
src/pages-system/user/components/search-form.vue
Normal file
96
src/pages-system/user/components/search-form.vue
Normal file
@@ -0,0 +1,96 @@
|
||||
<template>
|
||||
<wd-popup
|
||||
v-model="visible"
|
||||
position="top"
|
||||
custom-style="border-radius: 0 0 24rpx 24rpx;"
|
||||
safe-area-inset-top
|
||||
@close="visible = false"
|
||||
>
|
||||
<view class="p-32rpx">
|
||||
<view class="mb-24rpx text-32rpx text-[#333] font-semibold">
|
||||
搜索用户
|
||||
</view>
|
||||
<view class="mb-24rpx">
|
||||
<view class="mb-12rpx text-28rpx text-[#666]">
|
||||
用户名称
|
||||
</view>
|
||||
<wd-input
|
||||
v-model="formData.username"
|
||||
placeholder="请输入用户名称"
|
||||
clearable
|
||||
/>
|
||||
</view>
|
||||
<view class="mb-24rpx">
|
||||
<view class="mb-12rpx text-28rpx text-[#666]">
|
||||
用户昵称
|
||||
</view>
|
||||
<wd-input
|
||||
v-model="formData.nickname"
|
||||
placeholder="请输入用户昵称"
|
||||
clearable
|
||||
/>
|
||||
</view>
|
||||
<view class="w-full flex justify-center gap-24rpx">
|
||||
<wd-button class="flex-1" plain @click="handleReset">
|
||||
重置
|
||||
</wd-button>
|
||||
<wd-button class="flex-1" type="primary" @click="handleSearch">
|
||||
搜索
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</wd-popup>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, reactive, watch } from 'vue'
|
||||
|
||||
/** 搜索表单数据 */
|
||||
export interface SearchFormData {
|
||||
username?: string
|
||||
nickname?: string
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: boolean
|
||||
searchParams?: Partial<SearchFormData> // 初始搜索参数
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: boolean]
|
||||
'search': [data: SearchFormData]
|
||||
'reset': []
|
||||
}>()
|
||||
|
||||
const visible = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (val: boolean) => emit('update:modelValue', val),
|
||||
})
|
||||
|
||||
const formData = reactive<SearchFormData>({
|
||||
username: undefined,
|
||||
nickname: undefined,
|
||||
})
|
||||
|
||||
/** 监听弹窗打开,同步外部参数 */
|
||||
watch(() => props.modelValue, (val) => {
|
||||
if (val && props.searchParams) {
|
||||
formData.username = props.searchParams.username
|
||||
formData.nickname = props.searchParams.nickname
|
||||
}
|
||||
})
|
||||
|
||||
/** 搜索 */
|
||||
function handleSearch() {
|
||||
visible.value = false
|
||||
emit('search', { ...formData })
|
||||
}
|
||||
|
||||
/** 重置 */
|
||||
function handleReset() {
|
||||
formData.username = undefined
|
||||
formData.nickname = undefined
|
||||
visible.value = false
|
||||
emit('reset')
|
||||
}
|
||||
</script>
|
||||
106
src/pages-system/user/detail/components/password-form.vue
Normal file
106
src/pages-system/user/detail/components/password-form.vue
Normal file
@@ -0,0 +1,106 @@
|
||||
<template>
|
||||
<wd-popup v-model="visible" position="bottom" custom-style="border-radius: 24rpx 24rpx 0 0;" @close="handleClose">
|
||||
<view class="p-32rpx">
|
||||
<view class="mb-24rpx flex items-center justify-between">
|
||||
<text class="text-32rpx text-[#333] font-semibold">重置密码</text>
|
||||
<wd-icon name="close" size="20px" @click="handleClose" />
|
||||
</view>
|
||||
<wd-form ref="formRef" :model="formData" :rules="formRules">
|
||||
<wd-input
|
||||
v-model="formData.password"
|
||||
label="新密码"
|
||||
label-width="160rpx"
|
||||
prop="password"
|
||||
show-password
|
||||
clearable
|
||||
placeholder="请输入新密码"
|
||||
/>
|
||||
<wd-input
|
||||
v-model="formData.confirmPassword"
|
||||
label="确认密码"
|
||||
label-width="160rpx"
|
||||
prop="confirmPassword"
|
||||
show-password
|
||||
clearable
|
||||
placeholder="请再次输入新密码"
|
||||
/>
|
||||
</wd-form>
|
||||
<view class="mt-32rpx">
|
||||
<wd-button type="primary" block :loading="loading" @click="handleConfirm">
|
||||
确定
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</wd-popup>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { useToast } from 'wot-design-uni'
|
||||
import { resetUserPassword } from '@/api/system/user'
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: boolean
|
||||
userId: number
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: boolean]
|
||||
'success': []
|
||||
}>()
|
||||
|
||||
const toast = useToast()
|
||||
const visible = computed({
|
||||
get: () => props.modelValue,
|
||||
set: val => emit('update:modelValue', val),
|
||||
})
|
||||
const loading = ref(false)
|
||||
const formRef = ref()
|
||||
const formData = ref({
|
||||
password: '',
|
||||
confirmPassword: '',
|
||||
})
|
||||
const formRules = {
|
||||
password: [{ required: true, message: '请输入新密码' }],
|
||||
confirmPassword: [
|
||||
{ required: true, message: '请再次输入新密码' },
|
||||
{
|
||||
required: false,
|
||||
validator: (value: string) => value === formData.value.password,
|
||||
message: '两次输入的密码不一致',
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
/** 监听弹窗打开,重置表单 */
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(val) => {
|
||||
if (val) {
|
||||
formData.value = { password: '', confirmPassword: '' }
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
/** 关闭弹窗 */
|
||||
function handleClose() {
|
||||
visible.value = false
|
||||
}
|
||||
|
||||
/** 确认提交 */
|
||||
async function handleConfirm() {
|
||||
const { valid } = await formRef.value.validate()
|
||||
if (!valid) {
|
||||
return
|
||||
}
|
||||
loading.value = true
|
||||
try {
|
||||
await resetUserPassword(props.userId, formData.value.password)
|
||||
toast.success('密码重置成功')
|
||||
handleClose()
|
||||
emit('success')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
80
src/pages-system/user/detail/components/role-assign-form.vue
Normal file
80
src/pages-system/user/detail/components/role-assign-form.vue
Normal file
@@ -0,0 +1,80 @@
|
||||
<template>
|
||||
<wd-popup v-model="visible" position="bottom" custom-style="border-radius: 24rpx 24rpx 0 0;" @close="handleClose">
|
||||
<view class="p-32rpx">
|
||||
<view class="mb-24rpx flex items-center justify-between">
|
||||
<text class="text-32rpx text-[#333] font-semibold">分配角色</text>
|
||||
<wd-icon name="close" size="20px" @click="handleClose" />
|
||||
</view>
|
||||
<wd-checkbox-group v-model="selectedIds" cell shape="button">
|
||||
<wd-checkbox v-for="item in roleList" :key="item.id" :model-value="item.id">
|
||||
{{ item.name }}
|
||||
</wd-checkbox>
|
||||
</wd-checkbox-group>
|
||||
<view class="mt-32rpx">
|
||||
<wd-button type="primary" block :loading="loading" @click="handleConfirm">
|
||||
确定
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</wd-popup>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { Role } from '@/api/system/role'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { useToast } from 'wot-design-uni'
|
||||
import { getSimpleRoleList } from '@/api/system/role'
|
||||
import { assignUserRole, getUserRoleIds } from '@/api/system/user'
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: boolean
|
||||
userId: number
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: boolean]
|
||||
'success': []
|
||||
}>()
|
||||
|
||||
const toast = useToast()
|
||||
const visible = computed({
|
||||
get: () => props.modelValue,
|
||||
set: val => emit('update:modelValue', val),
|
||||
})
|
||||
const loading = ref(false)
|
||||
const roleList = ref<Role[]>([])
|
||||
const selectedIds = ref<number[]>([])
|
||||
|
||||
/** 监听弹窗打开,加载数据 */
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
async (val) => {
|
||||
if (val) {
|
||||
// 加载角色列表
|
||||
if (roleList.value.length === 0) {
|
||||
roleList.value = await getSimpleRoleList()
|
||||
}
|
||||
// 加载用户已有角色
|
||||
selectedIds.value = await getUserRoleIds(props.userId)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
/** 关闭弹窗 */
|
||||
function handleClose() {
|
||||
visible.value = false
|
||||
}
|
||||
|
||||
/** 确认提交 */
|
||||
async function handleConfirm() {
|
||||
loading.value = true
|
||||
try {
|
||||
await assignUserRole(props.userId, selectedIds.value)
|
||||
toast.success('角色分配成功')
|
||||
handleClose()
|
||||
emit('success')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
207
src/pages-system/user/detail/index.vue
Normal file
207
src/pages-system/user/detail/index.vue
Normal file
@@ -0,0 +1,207 @@
|
||||
<template>
|
||||
<view class="min-h-screen bg-[#f5f5f5]">
|
||||
<!-- 顶部导航栏 -->
|
||||
<wd-navbar
|
||||
title="用户详情"
|
||||
left-arrow placeholder safe-area-inset-top fixed
|
||||
@click-left="handleBack"
|
||||
/>
|
||||
|
||||
<!-- 详情内容 -->
|
||||
<view class="p-24rpx pb-200rpx">
|
||||
<wd-cell-group custom-class="cell-group" border>
|
||||
<wd-cell title="头像">
|
||||
<view v-if="formData?.avatar" class="h-80rpx w-80rpx overflow-hidden rounded-full">
|
||||
<image :src="formData.avatar" class="h-full w-full" mode="aspectFill" />
|
||||
</view>
|
||||
<text v-else>-</text>
|
||||
</wd-cell>
|
||||
<wd-cell title="用户昵称" :value="formData?.nickname || '-'" />
|
||||
<wd-cell title="用户账号" :value="formData?.username || '-'" />
|
||||
<wd-cell title="手机号码" :value="formData?.mobile || '-'" />
|
||||
<wd-cell title="邮箱" :value="formData?.email || '-'" />
|
||||
<wd-cell title="部门" :value="formData?.deptName || '-'" />
|
||||
<wd-cell title="性别">
|
||||
<dict-tag :type="DICT_TYPE.SYSTEM_USER_SEX" :value="formData?.sex" />
|
||||
</wd-cell>
|
||||
<wd-cell title="状态">
|
||||
<dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="formData?.sex" />
|
||||
</wd-cell>
|
||||
<wd-cell title="备注" :value="formData?.remark || '-'" />
|
||||
<wd-cell title="最后登录 IP" :value="formData?.loginIp || '-'" />
|
||||
<wd-cell title="最后登录时间" :value="formatDateTime(formData?.loginDate) || '-'" />
|
||||
<wd-cell title="创建时间" :value="formatDateTime(formData?.createTime) || '-'" />
|
||||
</wd-cell-group>
|
||||
</view>
|
||||
|
||||
<!-- 底部操作按钮 -->
|
||||
<view class="safe-area-inset-bottom fixed bottom-0 left-0 right-0 bg-white p-24rpx">
|
||||
<view class="w-full flex gap-24rpx">
|
||||
<wd-button
|
||||
v-if="hasAccessByCodes(['system:user:update'])"
|
||||
class="flex-1" type="warning" @click="handleEdit"
|
||||
>
|
||||
编辑
|
||||
</wd-button>
|
||||
<wd-button
|
||||
v-if="hasAccessByCodes(['system:user:delete'])"
|
||||
class="flex-1" type="error" :loading="deleting" @click="handleDelete"
|
||||
>
|
||||
删除
|
||||
</wd-button>
|
||||
<wd-button
|
||||
v-if="hasMoreActions"
|
||||
class="flex-1" type="info" @click="moreActionVisible = true"
|
||||
>
|
||||
更多
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 更多操作菜单 -->
|
||||
<wd-action-sheet v-model="moreActionVisible" :actions="moreActions" @select="handleMoreAction" />
|
||||
<!-- 重置密码弹窗 -->
|
||||
<PasswordForm v-model="passwordFormVisible" :user-id="props.id" @success="getDetail" />
|
||||
<!-- 分配角色弹窗 -->
|
||||
<RoleAssignForm v-model="roleAssignFormVisible" :user-id="props.id" @success="getDetail" />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { User } from '@/api/system/user'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { useToast } from 'wot-design-uni'
|
||||
import { deleteUser, getUser, updateUserStatus } from '@/api/system/user'
|
||||
import { useAccess } from '@/hooks/useAccess'
|
||||
import { CommonStatusEnum, DICT_TYPE } from '@/utils/constants'
|
||||
import { formatDateTime } from '@/utils/date'
|
||||
import PasswordForm from './components/password-form.vue'
|
||||
import RoleAssignForm from './components/role-assign-form.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
id: number
|
||||
}>()
|
||||
|
||||
definePage({
|
||||
style: {
|
||||
navigationBarTitleText: '',
|
||||
navigationStyle: 'custom',
|
||||
},
|
||||
})
|
||||
|
||||
const { hasAccessByCodes } = useAccess()
|
||||
const toast = useToast()
|
||||
const formData = ref<User>() // 详情数据
|
||||
const deleting = ref(false) // 删除中
|
||||
const moreActionVisible = ref(false) // 更多操作菜单
|
||||
const passwordFormVisible = ref(false) // 密码表单弹窗
|
||||
const roleAssignFormVisible = ref(false) // 角色分配弹窗
|
||||
const moreActions = computed(() => {
|
||||
const actions = []
|
||||
// 修改状态权限
|
||||
if (hasAccessByCodes(['system:user:update'])) {
|
||||
actions.push({ name: formData.value?.status === 1 ? '禁用用户' : '开启用户', value: 'update-status' })
|
||||
}
|
||||
// 重置密码权限
|
||||
if (hasAccessByCodes(['system:user:update-password'])) {
|
||||
actions.push({ name: '重置密码', value: 'resetPassword' })
|
||||
}
|
||||
// 分配角色权限
|
||||
if (hasAccessByCodes(['system:permission:assign-user-role'])) {
|
||||
actions.push({ name: '分配角色', value: 'assignRole' })
|
||||
}
|
||||
return actions
|
||||
})
|
||||
const hasMoreActions = computed(() => moreActions.value.length > 0)
|
||||
|
||||
/** 返回上一页 */
|
||||
function handleBack() {
|
||||
uni.navigateBack()
|
||||
}
|
||||
|
||||
/** 加载用户详情 */
|
||||
async function getDetail() {
|
||||
if (!props.id) {
|
||||
return
|
||||
}
|
||||
formData.value = await getUser(props.id)
|
||||
}
|
||||
|
||||
/** 编辑用户 */
|
||||
function handleEdit() {
|
||||
uni.navigateTo({
|
||||
url: `/pages-system/user/form/index?id=${props.id}`,
|
||||
})
|
||||
}
|
||||
|
||||
/** 删除用户 */
|
||||
function handleDelete() {
|
||||
if (!props.id) {
|
||||
return
|
||||
}
|
||||
uni.showModal({
|
||||
title: '提示',
|
||||
content: '确定要删除该用户吗?',
|
||||
success: async (res) => {
|
||||
if (!res.confirm) {
|
||||
return
|
||||
}
|
||||
deleting.value = true
|
||||
try {
|
||||
await deleteUser(props.id)
|
||||
toast.success('删除成功')
|
||||
setTimeout(() => {
|
||||
handleBack()
|
||||
}, 500)
|
||||
} finally {
|
||||
deleting.value = false
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/** 更多操作 */
|
||||
function handleMoreAction({ item }: { item: { value: string } }) {
|
||||
if (item.value === 'resetPassword') {
|
||||
passwordFormVisible.value = true
|
||||
} else if (item.value === 'assignRole') {
|
||||
roleAssignFormVisible.value = true
|
||||
} else if (item.value === 'update-status') {
|
||||
handleUpdateStatus()
|
||||
}
|
||||
}
|
||||
|
||||
/** 修改用户状态 */
|
||||
function handleUpdateStatus() {
|
||||
const isDisable = formData.value.status === CommonStatusEnum.DISABLE
|
||||
uni.showModal({
|
||||
title: '提示',
|
||||
content: isDisable ? '确定要禁用该用户吗?' : '确定要开启该用户吗?',
|
||||
success: async (res) => {
|
||||
if (!res.confirm) {
|
||||
return
|
||||
}
|
||||
await updateUserStatus(props.id, formData.value.status === 1 ? 0 : 1)
|
||||
toast.success(isDisable ? '禁用成功' : '开启成功')
|
||||
await getDetail()
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(() => {
|
||||
getDetail()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
:deep(.cell-group) {
|
||||
border-radius: 12rpx;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 3rpx 8rpx rgba(24, 144, 255, 0.06);
|
||||
}
|
||||
|
||||
.safe-area-inset-bottom {
|
||||
padding-bottom: calc(24rpx + env(safe-area-inset-bottom));
|
||||
}
|
||||
</style>
|
||||
129
src/pages-system/user/form/components/dept-picker.vue
Normal file
129
src/pages-system/user/form/components/dept-picker.vue
Normal file
@@ -0,0 +1,129 @@
|
||||
<!-- TODO @芋艿:【优化】看看后续要不要抽成组件! -->
|
||||
<template>
|
||||
<wd-col-picker
|
||||
v-model="selectedValue"
|
||||
label="归属部门"
|
||||
label-width="180rpx"
|
||||
:columns="deptColumns"
|
||||
:column-change="handleColumnChange"
|
||||
:display-format="displayFormat"
|
||||
@confirm="handleConfirm"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { Dept } from '@/api/system/dept'
|
||||
import { onMounted, ref, watch } from 'vue'
|
||||
import { getSimpleDeptList } from '@/api/system/dept'
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue?: number
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modelValue', value: number | undefined): void
|
||||
}>()
|
||||
|
||||
const deptList = ref<Dept[]>([])
|
||||
const deptColumns = ref<any[]>([])
|
||||
const selectedValue = ref<number[]>([])
|
||||
|
||||
/** 监听外部值变化,回显选中值 */
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(val) => {
|
||||
if (val && deptList.value.length > 0) {
|
||||
const path = findDeptPath(val)
|
||||
selectedValue.value = path
|
||||
// 构建列数据以支持回显
|
||||
buildColumnsForPath(path)
|
||||
}
|
||||
else {
|
||||
selectedValue.value = []
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
/** 加载部门列表 */
|
||||
async function loadDeptList() {
|
||||
deptList.value = await getSimpleDeptList()
|
||||
// 构建第一列数据(顶级部门)
|
||||
const topDepts = deptList.value.filter(item => item.parentId === 0)
|
||||
deptColumns.value = [topDepts.map(item => ({ value: item.id, label: item.name }))]
|
||||
// 如果有初始值,回显
|
||||
if (props.modelValue) {
|
||||
const path = findDeptPath(props.modelValue)
|
||||
selectedValue.value = path
|
||||
buildColumnsForPath(path)
|
||||
}
|
||||
}
|
||||
|
||||
/** 查找部门路径 */
|
||||
function findDeptPath(targetId: number): number[] {
|
||||
const path: number[] = []
|
||||
const findPath = (parentId: number, id: number): boolean => {
|
||||
const items = deptList.value.filter(d => d.parentId === parentId)
|
||||
for (const item of items) {
|
||||
if (item.id === id) {
|
||||
path.push(item.id)
|
||||
return true
|
||||
}
|
||||
if (findPath(item.id, id)) {
|
||||
path.unshift(item.id)
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
findPath(0, targetId)
|
||||
return path
|
||||
}
|
||||
|
||||
/** 根据路径构建列数据 */
|
||||
function buildColumnsForPath(path: number[]) {
|
||||
if (path.length === 0) {
|
||||
return
|
||||
}
|
||||
// 第一列已经有了,从第二列开始构建
|
||||
const columns = [deptColumns.value[0]]
|
||||
for (let i = 0; i < path.length - 1; i++) {
|
||||
const parentId = path[i]
|
||||
const children = deptList.value.filter(item => item.parentId === parentId)
|
||||
if (children.length > 0) {
|
||||
columns.push(children.map(item => ({ value: item.id, label: item.name })))
|
||||
}
|
||||
}
|
||||
deptColumns.value = columns
|
||||
}
|
||||
|
||||
/** 列变化 */
|
||||
function handleColumnChange({ selectedItem, resolve, finish }: any) {
|
||||
const children = deptList.value.filter(item => item.parentId === selectedItem.value)
|
||||
if (children.length > 0) {
|
||||
resolve(children.map(item => ({ value: item.id, label: item.name })))
|
||||
}
|
||||
else {
|
||||
finish()
|
||||
}
|
||||
}
|
||||
|
||||
/** 格式化显示 */
|
||||
function displayFormat(selectedItems: any[]) {
|
||||
return selectedItems.map(item => item.label).join('/')
|
||||
}
|
||||
|
||||
/** 确认选择 */
|
||||
function handleConfirm({ value }: { value: number[] }) {
|
||||
if (value && value.length > 0) {
|
||||
emit('update:modelValue', value[value.length - 1])
|
||||
}
|
||||
else {
|
||||
emit('update:modelValue', undefined)
|
||||
}
|
||||
}
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(() => {
|
||||
loadDeptList()
|
||||
})
|
||||
</script>
|
||||
82
src/pages-system/user/form/components/post-picker.vue
Normal file
82
src/pages-system/user/form/components/post-picker.vue
Normal file
@@ -0,0 +1,82 @@
|
||||
<!-- TODO @芋艿:【优化】看看后续要不要抽成组件! -->
|
||||
<template>
|
||||
<!-- 岗位选择单元格 -->
|
||||
<wd-cell title="岗位" title-width="180rpx" is-link @click="popupVisible = true">
|
||||
<view class="text-left">
|
||||
{{ displayText }}
|
||||
</view>
|
||||
</wd-cell>
|
||||
|
||||
<!-- 岗位选择弹窗 -->
|
||||
<wd-popup v-model="popupVisible" position="bottom" custom-style="border-radius: 24rpx 24rpx 0 0;">
|
||||
<view class="p-32rpx">
|
||||
<view class="mb-24rpx flex items-center justify-between">
|
||||
<text class="text-32rpx text-[#333] font-semibold">选择岗位</text>
|
||||
<wd-icon name="close" size="20px" @click="popupVisible = false" />
|
||||
</view>
|
||||
<wd-checkbox-group v-model="selectedIds" cell shape="button">
|
||||
<wd-checkbox v-for="item in postList" :key="item.id" :model-value="item.id">
|
||||
{{ item.name }}
|
||||
</wd-checkbox>
|
||||
</wd-checkbox-group>
|
||||
<view class="mt-32rpx">
|
||||
<wd-button type="primary" block @click="handleConfirm">
|
||||
确定
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</wd-popup>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { Post } from '@/api/system/post'
|
||||
import { computed, onMounted, ref, watch } from 'vue'
|
||||
import { getSimplePostList } from '@/api/system/post'
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue?: number[]
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modelValue', value: number[]): void
|
||||
}>()
|
||||
|
||||
const popupVisible = ref(false)
|
||||
const postList = ref<Post[]>([])
|
||||
const selectedIds = ref<number[]>([])
|
||||
|
||||
const displayText = computed(() => {
|
||||
if (!selectedIds.value || selectedIds.value.length === 0) {
|
||||
return ''
|
||||
}
|
||||
return postList.value
|
||||
.filter(item => selectedIds.value.includes(item.id))
|
||||
.map(item => item.name)
|
||||
.join('、')
|
||||
}) // 显示文本
|
||||
|
||||
/** 监听外部值变化 */
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(val) => {
|
||||
selectedIds.value = val || []
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
/** 加载岗位列表 */
|
||||
async function loadPostList() {
|
||||
postList.value = await getSimplePostList()
|
||||
}
|
||||
|
||||
/** 确认选择 */
|
||||
function handleConfirm() {
|
||||
emit('update:modelValue', selectedIds.value)
|
||||
popupVisible.value = false
|
||||
}
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(() => {
|
||||
loadPostList()
|
||||
})
|
||||
</script>
|
||||
201
src/pages-system/user/form/index.vue
Normal file
201
src/pages-system/user/form/index.vue
Normal file
@@ -0,0 +1,201 @@
|
||||
<template>
|
||||
<view class="min-h-screen bg-[#f5f5f5]">
|
||||
<!-- 顶部导航栏 -->
|
||||
<wd-navbar
|
||||
:title="getTitle"
|
||||
left-arrow placeholder safe-area-inset-top fixed
|
||||
@click-left="handleBack"
|
||||
/>
|
||||
|
||||
<!-- 表单区域 -->
|
||||
<view class="p-24rpx">
|
||||
<wd-form ref="formRef" :model="formData" :rules="formRules">
|
||||
<wd-cell-group custom-class="cell-group" border>
|
||||
<wd-input
|
||||
v-model="formData.username"
|
||||
label="用户名称"
|
||||
label-width="180rpx"
|
||||
prop="username"
|
||||
clearable
|
||||
placeholder="请输入用户名称"
|
||||
/>
|
||||
<wd-input
|
||||
v-if="!props.id"
|
||||
v-model="formData.password"
|
||||
label="用户密码"
|
||||
label-width="180rpx"
|
||||
prop="password"
|
||||
show-password
|
||||
clearable
|
||||
placeholder="请输入用户密码"
|
||||
/>
|
||||
<wd-input
|
||||
v-model="formData.nickname"
|
||||
label="用户昵称"
|
||||
label-width="180rpx"
|
||||
prop="nickname"
|
||||
clearable
|
||||
placeholder="请输入用户昵称"
|
||||
/>
|
||||
<DeptPicker v-model="formData.deptId" />
|
||||
<PostPicker v-model="formData.postIds" />
|
||||
<wd-input
|
||||
v-model="formData.email"
|
||||
label="邮箱"
|
||||
label-width="180rpx"
|
||||
prop="email"
|
||||
clearable
|
||||
placeholder="请输入邮箱"
|
||||
/>
|
||||
<wd-input
|
||||
v-model="formData.mobile"
|
||||
label="手机号码"
|
||||
label-width="180rpx"
|
||||
prop="mobile"
|
||||
clearable
|
||||
placeholder="请输入手机号码"
|
||||
/>
|
||||
<wd-cell title="性别" title-width="180rpx" center prop="sex">
|
||||
<wd-radio-group v-model="formData.sex" shape="button" size="medium">
|
||||
<wd-radio v-for="dict in getIntDictOptions(DICT_TYPE.SYSTEM_USER_SEX)" :key="dict.value" :value="dict.value">
|
||||
{{ dict.label }}
|
||||
</wd-radio>
|
||||
</wd-radio-group>
|
||||
</wd-cell>
|
||||
<wd-cell title="状态" title-width="180rpx" center prop="status">
|
||||
<wd-switch
|
||||
v-model="formData.status"
|
||||
:active-value="CommonStatusEnum.ENABLE"
|
||||
:inactive-value="CommonStatusEnum.DISABLE"
|
||||
/>
|
||||
</wd-cell>
|
||||
<wd-textarea
|
||||
v-model="formData.remark"
|
||||
label="备注"
|
||||
label-width="180rpx"
|
||||
placeholder="请输入备注"
|
||||
:maxlength="200"
|
||||
show-word-limit
|
||||
clearable
|
||||
/>
|
||||
</wd-cell-group>
|
||||
</wd-form>
|
||||
</view>
|
||||
|
||||
<!-- 底部保存按钮 -->
|
||||
<view class="safe-area-inset-bottom fixed bottom-0 left-0 right-0 bg-white p-24rpx">
|
||||
<wd-button
|
||||
type="primary"
|
||||
block
|
||||
:loading="formLoading"
|
||||
@click="handleSubmit"
|
||||
>
|
||||
保存
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { User } from '@/api/system/user'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { useToast } from 'wot-design-uni'
|
||||
import { createUser, getUser, updateUser } from '@/api/system/user'
|
||||
import { getIntDictOptions } from '@/hooks/useDict'
|
||||
import { CommonStatusEnum, DICT_TYPE } from '@/utils/constants'
|
||||
import { isEmail, isMobile } from '@/utils/validator'
|
||||
import DeptPicker from './components/dept-picker.vue'
|
||||
import PostPicker from './components/post-picker.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
id?: number
|
||||
}>()
|
||||
|
||||
definePage({
|
||||
style: {
|
||||
navigationBarTitleText: '',
|
||||
navigationStyle: 'custom',
|
||||
},
|
||||
})
|
||||
|
||||
const toast = useToast()
|
||||
const getTitle = computed(() => props.id ? '编辑用户' : '新增用户')
|
||||
const formLoading = ref(false) // 提交中状态
|
||||
const formData = ref<User>({
|
||||
id: undefined,
|
||||
username: '',
|
||||
nickname: '',
|
||||
password: '',
|
||||
mobile: '',
|
||||
email: '',
|
||||
sex: undefined,
|
||||
deptId: undefined,
|
||||
postIds: [],
|
||||
status: CommonStatusEnum.ENABLE,
|
||||
remark: '',
|
||||
})
|
||||
const formRules = {
|
||||
username: [{ required: true, message: '用户名称不能为空' }],
|
||||
password: [{ required: true, message: '用户密码不能为空' }],
|
||||
nickname: [{ required: true, message: '用户昵称不能为空' }],
|
||||
email: [{ required: false, validator: (value: string) => !value || isEmail(value), message: '请输入正确的邮箱地址' }],
|
||||
mobile: [{ required: false, validator: (value: string) => !value || isMobile(value), message: '请输入正确的手机号码' }],
|
||||
sex: [{ required: true, message: '性别不能为空' }],
|
||||
status: [{ required: true, message: '状态不能为空' }],
|
||||
}
|
||||
const formRef = ref()
|
||||
|
||||
/** 返回上一页 */
|
||||
function handleBack() {
|
||||
uni.navigateBack()
|
||||
}
|
||||
|
||||
/** 加载用户详情 */
|
||||
async function getDetail() {
|
||||
if (!props.id) {
|
||||
return
|
||||
}
|
||||
formData.value = await getUser(props.id)
|
||||
}
|
||||
|
||||
/** 提交表单 */
|
||||
async function handleSubmit() {
|
||||
const { valid } = await formRef.value.validate()
|
||||
if (!valid) {
|
||||
return
|
||||
}
|
||||
|
||||
formLoading.value = true
|
||||
try {
|
||||
if (props.id) {
|
||||
await updateUser(formData.value)
|
||||
toast.success('修改成功')
|
||||
} else {
|
||||
await createUser(formData.value)
|
||||
toast.success('新增成功')
|
||||
}
|
||||
setTimeout(() => {
|
||||
handleBack()
|
||||
}, 500)
|
||||
} finally {
|
||||
formLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(() => {
|
||||
getDetail()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
:deep(.cell-group) {
|
||||
border-radius: 12rpx;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 3rpx 8rpx rgba(24, 144, 255, 0.06);
|
||||
}
|
||||
|
||||
.safe-area-inset-bottom {
|
||||
padding-bottom: calc(24rpx + env(safe-area-inset-bottom));
|
||||
}
|
||||
</style>
|
||||
183
src/pages-system/user/index.vue
Normal file
183
src/pages-system/user/index.vue
Normal file
@@ -0,0 +1,183 @@
|
||||
<template>
|
||||
<!-- TODO @芋艿:【优化:全局样式】后续要全局样式么 -->
|
||||
<view class="min-h-screen bg-[#f5f5f5]">
|
||||
<!-- 顶部导航栏 -->
|
||||
<wd-navbar
|
||||
title="用户管理"
|
||||
left-arrow placeholder safe-area-inset-top fixed
|
||||
@click-left="handleBack"
|
||||
>
|
||||
<template #right>
|
||||
<view class="flex items-center" @click="searchVisible = !searchVisible">
|
||||
<wd-icon name="search" size="20px" />
|
||||
</view>
|
||||
</template>
|
||||
</wd-navbar>
|
||||
|
||||
<!-- 用户列表 -->
|
||||
<view class="p-24rpx">
|
||||
<view
|
||||
v-for="item in list"
|
||||
:key="item.id"
|
||||
class="mb-24rpx overflow-hidden rounded-12rpx bg-white shadow-sm"
|
||||
@click="handleDetail(item)"
|
||||
>
|
||||
<view class="relative p-24rpx">
|
||||
<view class="absolute right-24rpx top-24rpx">
|
||||
<dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="item.status" />
|
||||
</view>
|
||||
<view class="flex items-center gap-16rpx">
|
||||
<view
|
||||
v-if="item.avatar"
|
||||
class="h-80rpx w-80rpx overflow-hidden rounded-full"
|
||||
>
|
||||
<image :src="item.avatar" class="h-full w-full" mode="aspectFill" />
|
||||
</view>
|
||||
<view
|
||||
v-else
|
||||
class="h-80rpx w-80rpx flex items-center justify-center rounded-full bg-[#1890ff] text-32rpx text-white"
|
||||
>
|
||||
{{ item.nickname?.charAt(0) || item.username?.charAt(0) }}
|
||||
</view>
|
||||
<view>
|
||||
<view class="text-32rpx text-[#333] font-semibold">
|
||||
{{ item.nickname || item.username }}
|
||||
</view>
|
||||
<view class="text-24rpx text-[#999]">
|
||||
{{ item.deptName || '未分配部门' }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 加载更多 -->
|
||||
<view v-if="loadMoreState !== 'loading' && list.length === 0" class="py-100rpx text-center">
|
||||
<wd-status-tip image="content" tip="暂无用户数据" />
|
||||
</view>
|
||||
<wd-loadmore
|
||||
v-if="list.length > 0"
|
||||
:state="loadMoreState"
|
||||
@reload="loadMore"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- 搜索弹窗 -->
|
||||
<SearchForm
|
||||
v-model="searchVisible"
|
||||
:search-params="queryParams"
|
||||
@search="handleQuery"
|
||||
@reset="handleReset"
|
||||
/>
|
||||
|
||||
<!-- 新增按钮 -->
|
||||
<!-- TODO @芋艿:【优化:全局样式】后续要全局样式么 -->
|
||||
<view
|
||||
v-if="hasAccessByCodes(['system:user:create'])"
|
||||
class="fixed bottom-100rpx right-32rpx z-10 h-100rpx w-100rpx flex items-center justify-center rounded-full bg-[#1890ff] shadow-lg"
|
||||
@click="handleAdd"
|
||||
>
|
||||
<wd-icon name="add" size="24px" color="#fff" />
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { SearchFormData } from './components/search-form.vue'
|
||||
import type { User } from '@/api/system/user'
|
||||
import type { LoadMoreState } from '@/http/types'
|
||||
import { onReachBottom } from '@dcloudio/uni-app'
|
||||
import { onMounted, reactive, ref } from 'vue'
|
||||
import { getUserPage } from '@/api/system/user'
|
||||
import { useAccess } from '@/hooks/useAccess'
|
||||
import { DICT_TYPE } from '@/utils/constants'
|
||||
import SearchForm from './components/search-form.vue'
|
||||
|
||||
definePage({
|
||||
style: {
|
||||
navigationBarTitleText: '',
|
||||
navigationStyle: 'custom',
|
||||
},
|
||||
})
|
||||
|
||||
const { hasAccessByCodes } = useAccess()
|
||||
const total = ref(0) // 列表的总页数
|
||||
const list = ref<User[]>([]) // 列表的数据
|
||||
const loadMoreState = ref<LoadMoreState>('loading') // 加载更多状态
|
||||
const searchVisible = ref(false) // 搜索弹窗
|
||||
const queryParams = reactive({
|
||||
pageNo: 1,
|
||||
pageSize: 10,
|
||||
username: undefined as string | undefined,
|
||||
nickname: undefined as string | undefined,
|
||||
})
|
||||
|
||||
/** 返回上一页 */
|
||||
function handleBack() {
|
||||
uni.navigateBack()
|
||||
}
|
||||
|
||||
/** 查询用户列表 */
|
||||
async function getList() {
|
||||
loadMoreState.value = 'loading'
|
||||
try {
|
||||
const data = await getUserPage(queryParams)
|
||||
list.value = [...list.value, ...data.list]
|
||||
total.value = data.total
|
||||
loadMoreState.value = list.value.length >= total.value ? 'finished' : 'loading'
|
||||
} catch {
|
||||
queryParams.pageNo = queryParams.pageNo > 1 ? queryParams.pageNo - 1 : 1
|
||||
loadMoreState.value = 'error'
|
||||
}
|
||||
}
|
||||
|
||||
/** 搜索按钮操作 */
|
||||
function handleQuery(data?: SearchFormData) {
|
||||
queryParams.username = data?.username
|
||||
queryParams.nickname = data?.nickname
|
||||
queryParams.pageNo = 1
|
||||
list.value = [] // 清空列表
|
||||
getList()
|
||||
}
|
||||
|
||||
/** 重置按钮操作 */
|
||||
function handleReset() {
|
||||
getList()
|
||||
}
|
||||
|
||||
/** 加载更多 */
|
||||
function loadMore() {
|
||||
if (loadMoreState.value === 'finished') {
|
||||
return
|
||||
}
|
||||
queryParams.pageNo++
|
||||
getList()
|
||||
}
|
||||
|
||||
/** 新增用户 */
|
||||
function handleAdd() {
|
||||
uni.navigateTo({
|
||||
url: '/pages-system/user/form/index',
|
||||
})
|
||||
}
|
||||
|
||||
/** 查看详情 */
|
||||
function handleDetail(item: User) {
|
||||
uni.navigateTo({
|
||||
url: `/pages-system/user/detail/index?id=${item.id}`,
|
||||
})
|
||||
}
|
||||
|
||||
/** 触底加载更多 */
|
||||
onReachBottom(() => {
|
||||
loadMore()
|
||||
})
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(() => {
|
||||
getList()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
||||
Reference in New Issue
Block a user