feat:增加主包(tabbar)、system(系统管理)、infra(基础设施)、bpm(工作流程)的页面

This commit is contained in:
YunaiV
2025-12-12 19:16:46 +08:00
parent c14e0d04d0
commit cc94b2a5f7
97 changed files with 12198 additions and 81 deletions

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>