feat:增加主包(tabbar)、system(系统管理)、infra(基础设施)、bpm(工作流程)的页面
This commit is contained in:
84
src/pages-system/dept/components/breadcrumb.vue
Normal file
84
src/pages-system/dept/components/breadcrumb.vue
Normal file
@@ -0,0 +1,84 @@
|
||||
<template>
|
||||
<view v-if="breadcrumbList.length > 0" class="bg-white px-24rpx py-16rpx">
|
||||
<scroll-view scroll-x class="whitespace-nowrap">
|
||||
<view class="inline-flex items-center">
|
||||
<view
|
||||
class="flex items-center text-28rpx"
|
||||
:class="breadcrumbList.length > 0 ? 'text-[#1890ff]' : 'text-[#333]'"
|
||||
@click="handleClick(-1)"
|
||||
>
|
||||
<text>全部部门</text>
|
||||
</view>
|
||||
<template v-for="(item, index) in breadcrumbList" :key="item.id">
|
||||
<wd-icon name="arrow-right" size="12px" color="#999" custom-class="mx-8rpx" />
|
||||
<view
|
||||
class="flex items-center text-28rpx"
|
||||
:class="index < breadcrumbList.length - 1 ? 'text-[#1890ff]' : 'text-[#333]'"
|
||||
@click="handleClick(index)"
|
||||
>
|
||||
<text>{{ item.name }}</text>
|
||||
</view>
|
||||
</template>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, watch } from 'vue'
|
||||
|
||||
interface BreadcrumbItem {
|
||||
id: number
|
||||
name: string
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: number
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: number]
|
||||
}>()
|
||||
|
||||
const breadcrumbList = ref<BreadcrumbItem[]>([])
|
||||
|
||||
/** 监听外部值变化 */
|
||||
watch(() => props.modelValue, (val) => {
|
||||
if (val === 0) {
|
||||
breadcrumbList.value = []
|
||||
}
|
||||
})
|
||||
|
||||
/** 点击面包屑 */
|
||||
function handleClick(index: number) {
|
||||
if (index === -1) {
|
||||
// 点击"全部部门"
|
||||
breadcrumbList.value = []
|
||||
emit('update:modelValue', 0)
|
||||
} else if (index < breadcrumbList.value.length - 1) {
|
||||
// 点击中间层级
|
||||
const item = breadcrumbList.value[index]
|
||||
breadcrumbList.value = breadcrumbList.value.slice(0, index + 1)
|
||||
emit('update:modelValue', item.id)
|
||||
}
|
||||
}
|
||||
|
||||
/** 进入子层级 */
|
||||
function enter(item: BreadcrumbItem) {
|
||||
breadcrumbList.value.push(item)
|
||||
emit('update:modelValue', item.id)
|
||||
}
|
||||
|
||||
/** 返回上一层级 */
|
||||
function back(): boolean {
|
||||
if (breadcrumbList.value.length === 0) {
|
||||
return false
|
||||
}
|
||||
breadcrumbList.value.pop()
|
||||
const lastItem = breadcrumbList.value[breadcrumbList.value.length - 1]
|
||||
emit('update:modelValue', lastItem?.id ?? 0)
|
||||
return true
|
||||
}
|
||||
|
||||
defineExpose({ enter, back })
|
||||
</script>
|
||||
152
src/pages-system/dept/detail/index.vue
Normal file
152
src/pages-system/dept/detail/index.vue
Normal file
@@ -0,0 +1,152 @@
|
||||
<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="部门名称" :value="formData?.name || '-'" />
|
||||
<wd-cell title="上级部门" :value="getParentName() || '-'" />
|
||||
<wd-cell title="负责人" :value="getLeaderName() || '-'" />
|
||||
<wd-cell title="联系电话" :value="formData?.phone || '-'" />
|
||||
<wd-cell title="邮箱" :value="formData?.email || '-'" />
|
||||
<wd-cell title="显示顺序" :value="String(formData?.sort ?? '-')" />
|
||||
<wd-cell title="状态">
|
||||
<dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="formData?.status" />
|
||||
</wd-cell>
|
||||
<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 class="flex-1" type="warning" @click="handleEdit">
|
||||
编辑
|
||||
</wd-button>
|
||||
<wd-button class="flex-1" type="error" :loading="deleting" @click="handleDelete">
|
||||
删除
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { Dept } from '@/api/system/dept'
|
||||
import type { User } from '@/api/system/user'
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { useToast } from 'wot-design-uni'
|
||||
import { deleteDept, getDept, getSimpleDeptList } from '@/api/system/dept'
|
||||
import { getSimpleUserList } from '@/api/system/user'
|
||||
import { DICT_TYPE } from '@/utils/constants'
|
||||
import { formatDateTime } from '@/utils/date'
|
||||
|
||||
const props = defineProps<{
|
||||
id: number
|
||||
}>()
|
||||
|
||||
definePage({
|
||||
style: {
|
||||
navigationBarTitleText: '',
|
||||
navigationStyle: 'custom',
|
||||
},
|
||||
})
|
||||
|
||||
const toast = useToast()
|
||||
const formData = ref<Dept>() // 详情数据
|
||||
const deleting = ref(false) // 删除中
|
||||
const deptList = ref<Dept[]>([]) // 部门列表
|
||||
const userList = ref<User[]>([]) // 用户列表
|
||||
|
||||
/** 返回上一页 */
|
||||
function handleBack() {
|
||||
uni.navigateBack()
|
||||
}
|
||||
|
||||
/** 获取上级部门名称 */
|
||||
function getParentName(): string {
|
||||
if (!formData.value?.parentId || formData.value.parentId === 0) {
|
||||
return '顶级部门'
|
||||
}
|
||||
const parent = deptList.value.find(d => d.id === formData.value?.parentId)
|
||||
return parent?.name || '未知'
|
||||
}
|
||||
|
||||
/** 获取负责人名称 */
|
||||
function getLeaderName(): string {
|
||||
if (!formData.value?.leaderUserId) {
|
||||
return '未设置'
|
||||
}
|
||||
const user = userList.value.find(u => u.id === formData.value?.leaderUserId)
|
||||
return user?.nickname || '未知'
|
||||
}
|
||||
|
||||
/** 加载部门详情 */
|
||||
async function getDetail() {
|
||||
if (!props.id) {
|
||||
return
|
||||
}
|
||||
formData.value = await getDept(props.id)
|
||||
}
|
||||
|
||||
/** 编辑部门 */
|
||||
function handleEdit() {
|
||||
uni.navigateTo({
|
||||
url: `/pages-system/dept/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 deleteDept(props.id)
|
||||
toast.success('删除成功')
|
||||
setTimeout(() => {
|
||||
handleBack()
|
||||
}, 500)
|
||||
} finally {
|
||||
deleting.value = false
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(async () => {
|
||||
// 获取部门列表
|
||||
deptList.value = await getSimpleDeptList()
|
||||
// 获取用户列表
|
||||
userList.value = await getSimpleUserList()
|
||||
// 获取详情
|
||||
await 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>
|
||||
258
src/pages-system/dept/form/index.vue
Normal file
258
src/pages-system/dept/form/index.vue
Normal file
@@ -0,0 +1,258 @@
|
||||
<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-cell
|
||||
title="上级部门"
|
||||
title-width="180rpx"
|
||||
prop="parentId"
|
||||
is-link
|
||||
:value="getParentName()"
|
||||
@click="showDeptPicker = true"
|
||||
/>
|
||||
<wd-input
|
||||
v-model="formData.name"
|
||||
label="部门名称"
|
||||
label-width="180rpx"
|
||||
prop="name"
|
||||
clearable
|
||||
placeholder="请输入部门名称"
|
||||
/>
|
||||
<wd-cell title="显示顺序" title-width="180rpx" prop="sort" center>
|
||||
<wd-input-number
|
||||
v-model="formData.sort"
|
||||
:min="0"
|
||||
/>
|
||||
</wd-cell>
|
||||
<wd-cell
|
||||
title="负责人"
|
||||
title-width="180rpx"
|
||||
prop="leaderUserId"
|
||||
is-link
|
||||
:value="getLeaderName()"
|
||||
@click="showUserPicker = true"
|
||||
/>
|
||||
<wd-input
|
||||
v-model="formData.phone"
|
||||
label="联系电话"
|
||||
label-width="180rpx"
|
||||
prop="phone"
|
||||
clearable
|
||||
placeholder="请输入联系电话"
|
||||
/>
|
||||
<wd-input
|
||||
v-model="formData.email"
|
||||
label="邮箱"
|
||||
label-width="180rpx"
|
||||
prop="email"
|
||||
clearable
|
||||
placeholder="请输入邮箱"
|
||||
/>
|
||||
<wd-cell title="状态" title-width="180rpx" prop="status" center>
|
||||
<wd-switch
|
||||
v-model="formData.status"
|
||||
:active-value="CommonStatusEnum.ENABLE"
|
||||
:inactive-value="CommonStatusEnum.DISABLE"
|
||||
/>
|
||||
</wd-cell>
|
||||
</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>
|
||||
|
||||
<!-- 上级部门选择器 -->
|
||||
<wd-picker
|
||||
:model-value="showDeptPicker"
|
||||
:columns="deptPickerColumns"
|
||||
title="选择上级部门"
|
||||
@confirm="handleDeptConfirm"
|
||||
@close="showDeptPicker = false"
|
||||
/>
|
||||
|
||||
<!-- 负责人选择器 -->
|
||||
<wd-picker
|
||||
:model-value="showUserPicker"
|
||||
:columns="userPickerColumns"
|
||||
title="选择负责人"
|
||||
@confirm="handleUserConfirm"
|
||||
@close="showUserPicker = false"
|
||||
/>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { Dept } from '@/api/system/dept'
|
||||
import type { User } from '@/api/system/user'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { useToast } from 'wot-design-uni'
|
||||
import { createDept, getDept, getSimpleDeptList, updateDept } from '@/api/system/dept'
|
||||
import { getSimpleUserList } from '@/api/system/user'
|
||||
import { CommonStatusEnum } from '@/utils/constants'
|
||||
|
||||
const props = defineProps<{
|
||||
id?: number
|
||||
parentId?: number
|
||||
}>()
|
||||
|
||||
definePage({
|
||||
style: {
|
||||
navigationBarTitleText: '',
|
||||
navigationStyle: 'custom',
|
||||
},
|
||||
})
|
||||
|
||||
const toast = useToast()
|
||||
const getTitle = computed(() => props.id ? '编辑部门' : '新增部门')
|
||||
const formLoading = ref(false) // 提交中状态
|
||||
const formData = ref<Dept>({
|
||||
id: undefined,
|
||||
name: '',
|
||||
parentId: props.parentId || 0,
|
||||
sort: 0,
|
||||
status: CommonStatusEnum.ENABLE,
|
||||
leaderUserId: undefined,
|
||||
phone: '',
|
||||
email: '',
|
||||
})
|
||||
const formRules = {
|
||||
parentId: [{ required: true, message: '上级部门不能为空' }],
|
||||
name: [{ required: true, message: '部门名称不能为空' }],
|
||||
sort: [{ required: true, message: '显示顺序不能为空' }],
|
||||
status: [{ required: true, message: '状态不能为空' }],
|
||||
}
|
||||
const formRef = ref()
|
||||
|
||||
const deptList = ref<Dept[]>([]) // 部门列表
|
||||
const userList = ref<User[]>([]) // 用户列表
|
||||
const showDeptPicker = ref(false) // 部门选择器
|
||||
const showUserPicker = ref(false) // 负责人选择器
|
||||
|
||||
/** 部门选择器列 */
|
||||
const deptPickerColumns = computed(() => {
|
||||
const items = [{ label: '顶级部门', value: 0 }]
|
||||
deptList.value.forEach((dept) => {
|
||||
// 编辑时排除自己和子部门
|
||||
if (props.id && dept.id === props.id) {
|
||||
return
|
||||
}
|
||||
items.push({ label: dept.name, value: dept.id! })
|
||||
})
|
||||
return items
|
||||
})
|
||||
|
||||
/** 用户选择器列 */
|
||||
const userPickerColumns = computed(() => {
|
||||
const items = [{ label: '不设置', value: 0 }]
|
||||
userList.value.forEach((user) => {
|
||||
items.push({ label: user.nickname, value: user.id! })
|
||||
})
|
||||
return items
|
||||
})
|
||||
|
||||
/** 获取上级部门名称 */
|
||||
function getParentName(): string {
|
||||
if (!formData.value.parentId || formData.value.parentId === 0) {
|
||||
return '顶级部门'
|
||||
}
|
||||
const parent = deptList.value.find(d => d.id === formData.value.parentId)
|
||||
return parent?.name || '请选择'
|
||||
}
|
||||
|
||||
/** 获取负责人名称 */
|
||||
function getLeaderName(): string {
|
||||
if (!formData.value.leaderUserId) {
|
||||
return '请选择'
|
||||
}
|
||||
const user = userList.value.find(u => u.id === formData.value.leaderUserId)
|
||||
return user?.nickname || '请选择'
|
||||
}
|
||||
|
||||
/** 返回上一页 */
|
||||
function handleBack() {
|
||||
uni.navigateBack()
|
||||
}
|
||||
|
||||
/** 加载部门详情 */
|
||||
async function getDetail() {
|
||||
if (!props.id) {
|
||||
return
|
||||
}
|
||||
formData.value = await getDept(props.id)
|
||||
}
|
||||
|
||||
/** 部门选择确认 */
|
||||
function handleDeptConfirm({ value }: { value: number }) {
|
||||
formData.value.parentId = value
|
||||
}
|
||||
|
||||
/** 负责人选择确认 */
|
||||
function handleUserConfirm({ value }: { value: number }) {
|
||||
formData.value.leaderUserId = value || undefined
|
||||
}
|
||||
|
||||
/** 提交表单 */
|
||||
async function handleSubmit() {
|
||||
const { valid } = await formRef.value.validate()
|
||||
if (!valid) {
|
||||
return
|
||||
}
|
||||
|
||||
formLoading.value = true
|
||||
try {
|
||||
if (props.id) {
|
||||
await updateDept(formData.value)
|
||||
toast.success('修改成功')
|
||||
} else {
|
||||
await createDept(formData.value)
|
||||
toast.success('新增成功')
|
||||
}
|
||||
setTimeout(() => {
|
||||
handleBack()
|
||||
}, 500)
|
||||
} finally {
|
||||
formLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(async () => {
|
||||
// 获取部门列表
|
||||
deptList.value = await getSimpleDeptList()
|
||||
// 获取用户列表
|
||||
userList.value = await getSimpleUserList()
|
||||
// 获取详情
|
||||
await 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>
|
||||
153
src/pages-system/dept/index.vue
Normal file
153
src/pages-system/dept/index.vue
Normal file
@@ -0,0 +1,153 @@
|
||||
<template>
|
||||
<view class="min-h-screen bg-[#f5f5f5]">
|
||||
<!-- 顶部导航栏 -->
|
||||
<wd-navbar
|
||||
title="部门管理"
|
||||
left-arrow placeholder safe-area-inset-top fixed
|
||||
@click-left="handleBack"
|
||||
/>
|
||||
|
||||
<!-- 面包屑导航 -->
|
||||
<Breadcrumb ref="breadcrumbRef" v-model="currentParentId" />
|
||||
|
||||
<!-- 部门列表 -->
|
||||
<view class="p-24rpx">
|
||||
<view
|
||||
v-for="item in currentList"
|
||||
:key="item.id"
|
||||
class="mb-24rpx overflow-hidden rounded-12rpx bg-white shadow-sm"
|
||||
>
|
||||
<!-- 主内容区域:点击进入详情 -->
|
||||
<view class="p-24rpx" @click="handleDetail(item)">
|
||||
<!-- 第一行:名称、状态标签 -->
|
||||
<view class="flex items-center justify-between">
|
||||
<view class="flex items-center">
|
||||
<view class="mr-16rpx h-48rpx w-48rpx flex items-center justify-center rounded-8rpx bg-[#1890ff]">
|
||||
<wd-icon name="folder" size="20px" color="#fff" />
|
||||
</view>
|
||||
<view class="text-32rpx text-[#333] font-semibold">
|
||||
{{ item.name }}
|
||||
</view>
|
||||
</view>
|
||||
<dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="item.status" />
|
||||
</view>
|
||||
<!-- 第二行:负责人、子部门入口 -->
|
||||
<view class="mt-12rpx flex items-center justify-between pl-64rpx">
|
||||
<view class="text-24rpx text-[#999]">
|
||||
负责人:{{ getLeaderName(item.leaderUserId) }}
|
||||
</view>
|
||||
<view
|
||||
v-if="item.children && item.children.length > 0"
|
||||
class="flex items-center"
|
||||
@click.stop="handleEnterChildren(item)"
|
||||
>
|
||||
<text class="text-24rpx text-[#1890ff]">子部门 ({{ item.children.length }})</text>
|
||||
<wd-icon name="arrow-right" size="12px" color="#1890ff" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<view v-if="!loading && currentList.length === 0" class="py-100rpx text-center">
|
||||
<wd-status-tip image="content" tip="暂无部门数据" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 新增按钮 -->
|
||||
<view
|
||||
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 { Dept } from '@/api/system/dept'
|
||||
import type { User } from '@/api/system/user'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { getDeptList } from '@/api/system/dept'
|
||||
import { getSimpleUserList } from '@/api/system/user'
|
||||
import { DICT_TYPE } from '@/utils/constants'
|
||||
import { findChildren, handleTree } from '@/utils/tree'
|
||||
import Breadcrumb from './components/breadcrumb.vue'
|
||||
|
||||
definePage({
|
||||
style: {
|
||||
navigationBarTitleText: '',
|
||||
navigationStyle: 'custom',
|
||||
},
|
||||
})
|
||||
|
||||
const loading = ref(false)
|
||||
const list = ref<Dept[]>([]) // 完整部门列表(树形结构)
|
||||
const userList = ref<User[]>([]) // 用户列表
|
||||
|
||||
const currentParentId = ref(0) // 当前层级的父节点编号
|
||||
const currentList = computed(() => {
|
||||
if (currentParentId.value === 0) {
|
||||
return list.value.filter(item => item.parentId === 0)
|
||||
}
|
||||
return findChildren(list.value, currentParentId.value)
|
||||
}) // 当前层级的部门列表
|
||||
const breadcrumbRef = ref<InstanceType<typeof Breadcrumb>>()
|
||||
|
||||
/** 返回上一页或上一层级 */
|
||||
function handleBack() {
|
||||
if (!breadcrumbRef.value?.back()) {
|
||||
uni.navigateBack()
|
||||
}
|
||||
}
|
||||
|
||||
/** 获取负责人名称 */
|
||||
function getLeaderName(leaderUserId?: number): string {
|
||||
if (!leaderUserId) {
|
||||
return '未设置'
|
||||
}
|
||||
const user = userList.value.find(u => u.id === leaderUserId)
|
||||
return user?.nickname || '未知'
|
||||
}
|
||||
|
||||
/** 进入子部门层级 */
|
||||
function handleEnterChildren(item: Dept) {
|
||||
breadcrumbRef.value?.enter({ id: item.id!, name: item.name })
|
||||
}
|
||||
|
||||
/** 查询部门列表 */
|
||||
async function getList() {
|
||||
loading.value = true
|
||||
try {
|
||||
const data = await getDeptList()
|
||||
list.value = handleTree(data)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 新增部门 */
|
||||
function handleAdd() {
|
||||
uni.navigateTo({
|
||||
url: `/pages-system/dept/form/index?parentId=${currentParentId.value}`,
|
||||
})
|
||||
}
|
||||
|
||||
/** 查看详情 */
|
||||
function handleDetail(item: Dept) {
|
||||
uni.navigateTo({
|
||||
url: `/pages-system/dept/detail/index?id=${item.id}`,
|
||||
})
|
||||
}
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(async () => {
|
||||
// 获取用户列表
|
||||
userList.value = await getSimpleUserList()
|
||||
// 获取部门列表
|
||||
await getList()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
||||
98
src/pages-system/menu/components/breadcrumb.vue
Normal file
98
src/pages-system/menu/components/breadcrumb.vue
Normal file
@@ -0,0 +1,98 @@
|
||||
<template>
|
||||
<view class="bg-white px-24rpx py-16rpx">
|
||||
<scroll-view scroll-x class="whitespace-nowrap">
|
||||
<view class="inline-flex items-center text-28rpx">
|
||||
<template v-for="(item, index) in breadcrumbItems" :key="item.id">
|
||||
<text v-if="index > 0" class="mx-8rpx text-[#999]">/</text>
|
||||
<text
|
||||
:class="index === breadcrumbItems.length - 1 ? 'text-[#333]' : 'text-[#1890ff]'"
|
||||
@click="handleClick(index)"
|
||||
>
|
||||
{{ item.name }}
|
||||
</text>
|
||||
</template>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref, watch } from 'vue'
|
||||
|
||||
export interface BreadcrumbNode {
|
||||
id: number
|
||||
name: string
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
modelValue?: number // 当前父节点编号
|
||||
rootName?: string // 根目录名称
|
||||
}>(), {
|
||||
modelValue: 0,
|
||||
rootName: '根目录',
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: number]
|
||||
back: [] // 返回上一层级事件
|
||||
}>()
|
||||
|
||||
const breadcrumbs = ref<BreadcrumbNode[]>([]) // 面包屑路径(不包含根目录)
|
||||
|
||||
const breadcrumbItems = computed(() => [
|
||||
{ id: 0, name: props.rootName },
|
||||
...breadcrumbs.value,
|
||||
]) // 面包屑显示数据(包含根目录)
|
||||
|
||||
const currentParentId = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (val) => emit('update:modelValue', val),
|
||||
}) // 当前父节点编号
|
||||
|
||||
/** 面包屑点击 */
|
||||
function handleClick(index: number) {
|
||||
if (index === breadcrumbItems.value.length - 1) return // 点击当前层级不处理
|
||||
if (index === 0) {
|
||||
breadcrumbs.value = []
|
||||
currentParentId.value = 0
|
||||
} else {
|
||||
breadcrumbs.value = breadcrumbs.value.slice(0, index)
|
||||
currentParentId.value = breadcrumbs.value[index - 1].id
|
||||
}
|
||||
}
|
||||
|
||||
/** 进入子层级 */
|
||||
function enter(node: BreadcrumbNode) {
|
||||
breadcrumbs.value.push({ id: node.id, name: node.name })
|
||||
currentParentId.value = node.id
|
||||
}
|
||||
|
||||
/** 返回上一层级,返回 true 表示还有上层,false 表示已在根目录 */
|
||||
function back(): boolean {
|
||||
if (breadcrumbs.value.length > 0) {
|
||||
breadcrumbs.value.pop()
|
||||
currentParentId.value = breadcrumbs.value.length > 0
|
||||
? breadcrumbs.value[breadcrumbs.value.length - 1].id
|
||||
: 0
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/** 监听外部 modelValue 变化,重置面包屑(用于外部重置场景) */
|
||||
watch(() => props.modelValue, (val) => {
|
||||
if (val === 0 && breadcrumbs.value.length > 0) {
|
||||
breadcrumbs.value = []
|
||||
}
|
||||
})
|
||||
|
||||
defineExpose({
|
||||
enter,
|
||||
back,
|
||||
breadcrumbs,
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
||||
155
src/pages-system/menu/detail/index.vue
Normal file
155
src/pages-system/menu/detail/index.vue
Normal file
@@ -0,0 +1,155 @@
|
||||
<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="菜单名称" :value="formData?.name || '-'" />
|
||||
<wd-cell title="菜单类型">
|
||||
<dict-tag :type="DICT_TYPE.SYSTEM_MENU_TYPE" :value="formData?.type" />
|
||||
</wd-cell>
|
||||
<wd-cell title="上级菜单" :value="parentMenuName" />
|
||||
<wd-cell title="显示排序" :value="String(formData?.sort ?? '-')" />
|
||||
<wd-cell title="路由地址" :value="formData?.path || '-'" />
|
||||
<wd-cell v-if="formData?.type === SystemMenuTypeEnum.MENU" title="组件路径" :value="formData?.component || '-'" />
|
||||
<wd-cell v-if="formData?.type === SystemMenuTypeEnum.MENU" title="组件名称" :value="formData?.componentName || '-'" />
|
||||
<wd-cell v-if="formData?.type !== SystemMenuTypeEnum.DIR" title="权限标识" :value="formData?.permission || '-'" />
|
||||
<wd-cell title="菜单状态">
|
||||
<dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="formData?.status" />
|
||||
</wd-cell>
|
||||
<wd-cell v-if="formData?.type !== SystemMenuTypeEnum.BUTTON" title="显示状态">
|
||||
<wd-tag v-if="formData?.visible" type="success" plain>
|
||||
显示
|
||||
</wd-tag>
|
||||
<wd-tag v-else type="warning" plain>
|
||||
隐藏
|
||||
</wd-tag>
|
||||
</wd-cell>
|
||||
<wd-cell v-if="formData?.type === SystemMenuTypeEnum.MENU" title="缓存状态">
|
||||
<wd-tag v-if="formData?.keepAlive" type="success" plain>
|
||||
缓存
|
||||
</wd-tag>
|
||||
<wd-tag v-else type="default" plain>
|
||||
不缓存
|
||||
</wd-tag>
|
||||
</wd-cell>
|
||||
<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 class="flex-1" type="warning" @click="handleEdit">
|
||||
编辑
|
||||
</wd-button>
|
||||
<wd-button class="flex-1" type="error" :loading="deleting" @click="handleDelete">
|
||||
删除
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { Menu } from '@/api/system/menu'
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { useToast } from 'wot-design-uni'
|
||||
import { deleteMenu, getMenu, getSimpleMenuList } from '@/api/system/menu'
|
||||
import { DICT_TYPE, SystemMenuTypeEnum } from '@/utils/constants'
|
||||
import { formatDateTime } from '@/utils/date'
|
||||
|
||||
const props = defineProps<{
|
||||
id: number
|
||||
}>()
|
||||
|
||||
definePage({
|
||||
style: {
|
||||
navigationBarTitleText: '',
|
||||
navigationStyle: 'custom',
|
||||
},
|
||||
})
|
||||
|
||||
const toast = useToast()
|
||||
const formData = ref<Menu>() // 详情数据
|
||||
const deleting = ref(false) // 删除中
|
||||
const parentMenuName = ref('-') // 上级菜单名称
|
||||
|
||||
/** 返回上一页 */
|
||||
function handleBack() {
|
||||
uni.navigateBack()
|
||||
}
|
||||
|
||||
/** 加载菜单详情 */
|
||||
async function getDetail() {
|
||||
if (!props.id) {
|
||||
return
|
||||
}
|
||||
formData.value = await getMenu(props.id)
|
||||
// 获取上级菜单名称
|
||||
if (formData.value?.parentId === 0) {
|
||||
parentMenuName.value = '主类目'
|
||||
} else if (formData.value?.parentId) {
|
||||
// TODO @芋艿:后续这里可以优化,由后端返回 menuName;
|
||||
const menuList = await getSimpleMenuList()
|
||||
const parent = menuList.find(item => item.id === formData.value?.parentId)
|
||||
parentMenuName.value = parent?.name || '-'
|
||||
}
|
||||
}
|
||||
|
||||
/** 编辑菜单 */
|
||||
function handleEdit() {
|
||||
uni.navigateTo({
|
||||
url: `/pages-system/menu/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 deleteMenu(props.id)
|
||||
toast.success('删除成功')
|
||||
setTimeout(() => {
|
||||
handleBack()
|
||||
}, 500)
|
||||
} finally {
|
||||
deleting.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>
|
||||
143
src/pages-system/menu/form/components/menu-picker.vue
Normal file
143
src/pages-system/menu/form/components/menu-picker.vue
Normal file
@@ -0,0 +1,143 @@
|
||||
<template>
|
||||
<wd-col-picker
|
||||
v-model="selectedValue"
|
||||
label="上级菜单"
|
||||
label-width="180rpx"
|
||||
:columns="menuColumns"
|
||||
:column-change="handleColumnChange"
|
||||
:display-format="displayFormat"
|
||||
@confirm="handleConfirm"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { Menu } from '@/api/system/menu'
|
||||
import { onMounted, ref, watch } from 'vue'
|
||||
import { getSimpleMenuList } from '@/api/system/menu'
|
||||
import { SystemMenuTypeEnum } from '@/utils/constants'
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue?: number
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modelValue', value: number): void
|
||||
}>()
|
||||
|
||||
const menuList = ref<Menu[]>([])
|
||||
const menuColumns = ref<any[]>([])
|
||||
const selectedValue = ref<number[]>([])
|
||||
|
||||
/** 监听外部值变化,回显选中值 */
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(val) => {
|
||||
if (val !== undefined && val !== 0 && menuList.value.length > 0) {
|
||||
const path = findMenuPath(val)
|
||||
selectedValue.value = path
|
||||
buildColumnsForPath(path)
|
||||
} else {
|
||||
selectedValue.value = [0]
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
/** 加载菜单列表 */
|
||||
async function loadMenuList() {
|
||||
const list = await getSimpleMenuList()
|
||||
// 只保留目录和菜单
|
||||
menuList.value = list.filter(item => item.type !== SystemMenuTypeEnum.BUTTON)
|
||||
// 构建第一列数据(主类目 + 顶级菜单)
|
||||
const topMenus = menuList.value.filter(item => item.parentId === 0)
|
||||
menuColumns.value = [[
|
||||
{ value: 0, label: '主类目' },
|
||||
...topMenus.map(item => ({ value: item.id, label: item.name })),
|
||||
]]
|
||||
// 如果有初始值,回显
|
||||
if (props.modelValue !== undefined && props.modelValue !== 0) {
|
||||
const path = findMenuPath(props.modelValue)
|
||||
selectedValue.value = path
|
||||
buildColumnsForPath(path)
|
||||
} else {
|
||||
selectedValue.value = [0]
|
||||
}
|
||||
}
|
||||
|
||||
/** 查找菜单路径 */
|
||||
function findMenuPath(targetId: number): number[] {
|
||||
if (targetId === 0) {
|
||||
return [0]
|
||||
}
|
||||
const path: number[] = []
|
||||
const findPath = (parentId: number, id: number): boolean => {
|
||||
const items = menuList.value.filter(m => m.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.length > 0 ? path : [0]
|
||||
}
|
||||
|
||||
/** 根据路径构建列数据 */
|
||||
function buildColumnsForPath(path: number[]) {
|
||||
if (path.length === 0 || (path.length === 1 && path[0] === 0)) {
|
||||
return
|
||||
}
|
||||
// 第一列已经有了,从第二列开始构建
|
||||
const columns = [menuColumns.value[0]]
|
||||
for (let i = 0; i < path.length; i++) {
|
||||
const parentId = path[i]
|
||||
if (parentId === 0) {
|
||||
continue
|
||||
}
|
||||
const children = menuList.value.filter(item => item.parentId === parentId)
|
||||
if (children.length > 0) {
|
||||
columns.push(children.map(item => ({ value: item.id, label: item.name })))
|
||||
}
|
||||
}
|
||||
menuColumns.value = columns
|
||||
}
|
||||
|
||||
/** 列变化 */
|
||||
function handleColumnChange({ selectedItem, resolve, finish }: any) {
|
||||
if (selectedItem.value === 0) {
|
||||
// 选择主类目,结束
|
||||
finish()
|
||||
return
|
||||
}
|
||||
const children = menuList.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', 0)
|
||||
}
|
||||
}
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(() => {
|
||||
loadMenuList()
|
||||
})
|
||||
</script>
|
||||
257
src/pages-system/menu/form/index.vue
Normal file
257
src/pages-system/menu/form/index.vue
Normal file
@@ -0,0 +1,257 @@
|
||||
<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 pb-200rpx">
|
||||
<wd-form ref="formRef" :model="formData" :rules="formRules">
|
||||
<wd-cell-group custom-class="cell-group" border>
|
||||
<MenuPicker v-model="formData.parentId" />
|
||||
<wd-cell title="菜单类型" title-width="180rpx" prop="type">
|
||||
<wd-radio-group v-model="formData.type" shape="button" @change="handleTypeChange">
|
||||
<wd-radio v-for="dict in getIntDictOptions(DICT_TYPE.SYSTEM_MENU_TYPE)" :key="dict.value" :value="dict.value">
|
||||
{{ dict.label }}
|
||||
</wd-radio>
|
||||
</wd-radio-group>
|
||||
</wd-cell>
|
||||
<wd-input
|
||||
v-model="formData.name"
|
||||
label="菜单名称"
|
||||
label-width="180rpx"
|
||||
prop="name"
|
||||
clearable
|
||||
placeholder="请输入菜单名称"
|
||||
/>
|
||||
<wd-input
|
||||
v-if="formData.type !== SystemMenuTypeEnum.BUTTON"
|
||||
v-model="formData.icon"
|
||||
label="菜单图标"
|
||||
label-width="180rpx"
|
||||
clearable
|
||||
placeholder="请输入菜单图标"
|
||||
/>
|
||||
<wd-input
|
||||
v-if="formData.type !== SystemMenuTypeEnum.BUTTON"
|
||||
v-model="formData.path"
|
||||
label="路由地址"
|
||||
label-width="180rpx"
|
||||
prop="path"
|
||||
clearable
|
||||
placeholder="请输入路由地址"
|
||||
/>
|
||||
<wd-input
|
||||
v-if="formData.type === SystemMenuTypeEnum.MENU"
|
||||
v-model="formData.component"
|
||||
label="组件路径"
|
||||
label-width="180rpx"
|
||||
clearable
|
||||
placeholder="例如:system/user/index"
|
||||
/>
|
||||
<wd-input
|
||||
v-if="formData.type === SystemMenuTypeEnum.MENU"
|
||||
v-model="formData.componentName"
|
||||
label="组件名称"
|
||||
label-width="180rpx"
|
||||
clearable
|
||||
placeholder="例如:SystemUser"
|
||||
/>
|
||||
<wd-input
|
||||
v-if="formData.type !== SystemMenuTypeEnum.DIR"
|
||||
v-model="formData.permission"
|
||||
label="权限标识"
|
||||
label-width="180rpx"
|
||||
clearable
|
||||
placeholder="请输入权限标识"
|
||||
/>
|
||||
<wd-cell title="显示排序" title-width="180rpx" prop="sort" center>
|
||||
<wd-input-number
|
||||
v-model="formData.sort"
|
||||
:min="0"
|
||||
/>
|
||||
</wd-cell>
|
||||
<wd-cell title="菜单状态" title-width="180rpx" prop="status" center>
|
||||
<wd-switch
|
||||
v-model="formData.status"
|
||||
:active-value="CommonStatusEnum.ENABLE"
|
||||
:inactive-value="CommonStatusEnum.DISABLE"
|
||||
/>
|
||||
</wd-cell>
|
||||
<wd-cell v-if="formData.type !== SystemMenuTypeEnum.BUTTON" title="显示状态" title-width="180rpx" center>
|
||||
<wd-switch
|
||||
v-model="formData.visible"
|
||||
:active-value="true"
|
||||
:inactive-value="false"
|
||||
/>
|
||||
</wd-cell>
|
||||
<wd-cell v-if="formData.type !== SystemMenuTypeEnum.BUTTON" title="总是显示" title-width="180rpx" center>
|
||||
<wd-switch
|
||||
v-model="formData.alwaysShow"
|
||||
:active-value="true"
|
||||
:inactive-value="false"
|
||||
/>
|
||||
</wd-cell>
|
||||
<wd-cell v-if="formData.type === SystemMenuTypeEnum.MENU" title="缓存状态" title-width="180rpx" center>
|
||||
<wd-switch
|
||||
v-model="formData.keepAlive"
|
||||
:active-value="true"
|
||||
:inactive-value="false"
|
||||
/>
|
||||
</wd-cell>
|
||||
</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 { Menu } from '@/api/system/menu'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { useToast } from 'wot-design-uni'
|
||||
import { createMenu, getMenu, updateMenu } from '@/api/system/menu'
|
||||
import { getIntDictOptions } from '@/hooks/useDict'
|
||||
import { CommonStatusEnum, DICT_TYPE, SystemMenuTypeEnum } from '@/utils/constants'
|
||||
import MenuPicker from './components/menu-picker.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
id?: number
|
||||
parentId?: number
|
||||
}>()
|
||||
|
||||
definePage({
|
||||
style: {
|
||||
navigationBarTitleText: '',
|
||||
navigationStyle: 'custom',
|
||||
},
|
||||
})
|
||||
|
||||
const toast = useToast()
|
||||
const getTitle = computed(() => props.id ? '编辑菜单' : '新增菜单')
|
||||
const formLoading = ref(false) // 提交中状态
|
||||
const formData = ref<Menu>({
|
||||
id: undefined,
|
||||
name: '',
|
||||
permission: '',
|
||||
type: SystemMenuTypeEnum.DIR,
|
||||
sort: 0,
|
||||
parentId: 0,
|
||||
path: '',
|
||||
icon: '',
|
||||
component: '',
|
||||
componentName: '',
|
||||
status: CommonStatusEnum.ENABLE,
|
||||
visible: true,
|
||||
keepAlive: true,
|
||||
alwaysShow: true,
|
||||
})
|
||||
const formRules = {
|
||||
name: [{ required: true, message: '菜单名称不能为空' }],
|
||||
type: [{ required: true, message: '菜单类型不能为空' }],
|
||||
sort: [{ required: true, message: '显示排序不能为空' }],
|
||||
status: [{ required: true, message: '状态不能为空' }],
|
||||
}
|
||||
const formRef = ref()
|
||||
|
||||
/** 返回上一页 */
|
||||
function handleBack() {
|
||||
uni.navigateBack()
|
||||
}
|
||||
|
||||
/** 菜单类型变更: */
|
||||
function handleTypeChange() {
|
||||
// 切换类型时,清空不需要的字段
|
||||
if (formData.value.type === SystemMenuTypeEnum.BUTTON) {
|
||||
formData.value.path = ''
|
||||
formData.value.component = ''
|
||||
formData.value.componentName = ''
|
||||
formData.value.icon = ''
|
||||
} else if (formData.value.type === SystemMenuTypeEnum.DIR) {
|
||||
formData.value.component = ''
|
||||
formData.value.componentName = ''
|
||||
formData.value.permission = ''
|
||||
}
|
||||
}
|
||||
|
||||
/** 加载菜单详情 */
|
||||
async function getDetail() {
|
||||
if (!props.id) {
|
||||
// 新增时,设置默认的上级菜单
|
||||
if (props.parentId) {
|
||||
formData.value.parentId = props.parentId
|
||||
}
|
||||
return
|
||||
}
|
||||
formData.value = await getMenu(props.id)
|
||||
}
|
||||
|
||||
/** 提交表单 */
|
||||
async function handleSubmit() {
|
||||
const { valid } = await formRef.value.validate()
|
||||
if (!valid) {
|
||||
return
|
||||
}
|
||||
// 路由地址校验
|
||||
if (formData.value.type !== SystemMenuTypeEnum.BUTTON) {
|
||||
const path = formData.value.path
|
||||
const isExternal = /^(?:https?:|mailto:|tel:)/.test(path)
|
||||
if (!isExternal) {
|
||||
if (formData.value.parentId === 0 && path.charAt(0) !== '/') {
|
||||
toast.error('路径必须以 / 开头')
|
||||
return
|
||||
} else if (formData.value.parentId !== 0 && path.charAt(0) === '/') {
|
||||
toast.error('路径不能以 / 开头')
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
formLoading.value = true
|
||||
try {
|
||||
if (props.id) {
|
||||
await updateMenu(formData.value)
|
||||
toast.success('修改成功')
|
||||
} else {
|
||||
await createMenu(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>
|
||||
180
src/pages-system/menu/index.vue
Normal file
180
src/pages-system/menu/index.vue
Normal file
@@ -0,0 +1,180 @@
|
||||
<template>
|
||||
<view class="min-h-screen bg-[#f5f5f5]">
|
||||
<!-- 顶部导航栏 -->
|
||||
<wd-navbar
|
||||
title="菜单管理"
|
||||
left-arrow placeholder safe-area-inset-top fixed
|
||||
@click-left="handleBack"
|
||||
/>
|
||||
|
||||
<!-- 面包屑导航 -->
|
||||
<Breadcrumb ref="breadcrumbRef" v-model="currentParentId" />
|
||||
|
||||
<!-- 菜单列表 -->
|
||||
<view class="p-24rpx">
|
||||
<view
|
||||
v-for="item in currentList"
|
||||
:key="item.id"
|
||||
class="mb-24rpx overflow-hidden rounded-12rpx bg-white shadow-sm"
|
||||
>
|
||||
<!-- 主内容区域:点击进入详情 -->
|
||||
<view class="p-24rpx" @click="handleDetail(item)">
|
||||
<!-- 第一行:图标、名称、状态标签 -->
|
||||
<view class="flex items-center justify-between">
|
||||
<view class="flex items-center">
|
||||
<view class="mr-16rpx h-48rpx w-48rpx flex items-center justify-center rounded-8rpx" :class="getTypeIconBg(item.type)">
|
||||
<wd-icon :name="getTypeIcon(item.type)" size="20px" color="#fff" />
|
||||
</view>
|
||||
<view class="text-32rpx text-[#333] font-semibold">
|
||||
{{ item.name }}
|
||||
</view>
|
||||
</view>
|
||||
<dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="item.status" />
|
||||
</view>
|
||||
<!-- 第二行:类型描述、子菜单入口 -->
|
||||
<view class="mt-12rpx flex items-center justify-between pl-64rpx">
|
||||
<view class="text-24rpx text-[#999]">
|
||||
{{ getTypeDesc(item) }}
|
||||
</view>
|
||||
<view
|
||||
v-if="item.children && item.children.length > 0"
|
||||
class="flex items-center"
|
||||
@click.stop="handleEnterChildren(item)"
|
||||
>
|
||||
<text class="text-24rpx text-[#1890ff]">子菜单 ({{ item.children.length }})</text>
|
||||
<wd-icon name="arrow-right" size="12px" color="#1890ff" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<view v-if="!loading && currentList.length === 0" class="py-100rpx text-center">
|
||||
<wd-status-tip image="content" tip="暂无菜单数据" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 新增按钮 -->
|
||||
<view
|
||||
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 { Menu } from '@/api/system/menu'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { getMenuList } from '@/api/system/menu'
|
||||
import { DICT_TYPE, SystemMenuTypeEnum } from '@/utils/constants'
|
||||
import { findChildren, handleTree } from '@/utils/tree'
|
||||
import Breadcrumb from './components/breadcrumb.vue'
|
||||
|
||||
definePage({
|
||||
style: {
|
||||
navigationBarTitleText: '',
|
||||
navigationStyle: 'custom',
|
||||
},
|
||||
})
|
||||
|
||||
const loading = ref(false)
|
||||
const list = ref<Menu[]>([]) // 完整菜单列表(树形结构)
|
||||
|
||||
const currentParentId = ref(0) // 当前层级的父节点编号
|
||||
const currentList = computed(() => {
|
||||
if (currentParentId.value === 0) {
|
||||
return list.value.filter(item => item.parentId === 0)
|
||||
}
|
||||
return findChildren(list.value, currentParentId.value)
|
||||
}) // 当前层级的菜单列表
|
||||
const breadcrumbRef = ref<InstanceType<typeof Breadcrumb>>()
|
||||
|
||||
/** 返回上一页或上一层级 */
|
||||
function handleBack() {
|
||||
if (!breadcrumbRef.value?.back()) {
|
||||
uni.navigateBack()
|
||||
}
|
||||
}
|
||||
|
||||
/** 获取菜单类型图标 */
|
||||
function getTypeIcon(type: number): string {
|
||||
switch (type) {
|
||||
case SystemMenuTypeEnum.DIR:
|
||||
return 'folder'
|
||||
case SystemMenuTypeEnum.MENU:
|
||||
return 'read'
|
||||
case SystemMenuTypeEnum.BUTTON:
|
||||
return 'tips'
|
||||
default:
|
||||
return 'folder'
|
||||
}
|
||||
}
|
||||
|
||||
/** 获取菜单类型图标背景色 */
|
||||
function getTypeIconBg(type: number): string {
|
||||
switch (type) {
|
||||
case SystemMenuTypeEnum.DIR:
|
||||
return 'bg-[#1890ff]'
|
||||
case SystemMenuTypeEnum.MENU:
|
||||
return 'bg-[#52c41a]'
|
||||
case SystemMenuTypeEnum.BUTTON:
|
||||
return 'bg-[#faad14]'
|
||||
default:
|
||||
return 'bg-[#1890ff]'
|
||||
}
|
||||
}
|
||||
|
||||
/** 获取菜单类型描述(根据类型展示不同信息) */
|
||||
function getTypeDesc(item: Menu): string {
|
||||
switch (item.type) {
|
||||
case SystemMenuTypeEnum.DIR:
|
||||
return `路由:${item.path}`
|
||||
case SystemMenuTypeEnum.MENU:
|
||||
return `路由:${item.path}`
|
||||
case SystemMenuTypeEnum.BUTTON:
|
||||
return `权限:${item.permission}`
|
||||
default:
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
/** 进入子菜单层级 */
|
||||
function handleEnterChildren(item: Menu) {
|
||||
breadcrumbRef.value?.enter({ id: item.id!, name: item.name })
|
||||
}
|
||||
|
||||
/** 查询菜单列表 */
|
||||
async function getList() {
|
||||
loading.value = true
|
||||
try {
|
||||
const data = await getMenuList()
|
||||
list.value = handleTree(data)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 新增菜单 */
|
||||
function handleAdd() {
|
||||
uni.navigateTo({
|
||||
url: `/pages-system/menu/form/index?parentId=${currentParentId.value}`,
|
||||
})
|
||||
}
|
||||
|
||||
/** 查看详情 */
|
||||
function handleDetail(item: Menu) {
|
||||
uni.navigateTo({
|
||||
url: `/pages-system/menu/detail/index?id=${item.id}`,
|
||||
})
|
||||
}
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(() => {
|
||||
getList()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
||||
116
src/pages-system/post/components/search-form.vue
Normal file
116
src/pages-system/post/components/search-form.vue
Normal file
@@ -0,0 +1,116 @@
|
||||
<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.name"
|
||||
placeholder="请输入岗位名称"
|
||||
clearable
|
||||
/>
|
||||
</view>
|
||||
<view class="mb-24rpx">
|
||||
<view class="mb-12rpx text-28rpx text-[#666]">
|
||||
岗位编码
|
||||
</view>
|
||||
<wd-input
|
||||
v-model="formData.code"
|
||||
placeholder="请输入岗位编码"
|
||||
clearable
|
||||
/>
|
||||
</view>
|
||||
<view class="mb-32rpx">
|
||||
<view class="mb-12rpx text-28rpx text-[#666]">
|
||||
状态
|
||||
</view>
|
||||
<wd-radio-group v-model="formData.status" shape="button" size="medium">
|
||||
<wd-radio :value="-1">
|
||||
全部
|
||||
</wd-radio>
|
||||
<wd-radio :value="0">
|
||||
启用
|
||||
</wd-radio>
|
||||
<wd-radio :value="1">
|
||||
禁用
|
||||
</wd-radio>
|
||||
</wd-radio-group>
|
||||
</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 {
|
||||
name?: string
|
||||
code?: string
|
||||
status: number // -1 表示全部
|
||||
}
|
||||
|
||||
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>({
|
||||
name: undefined,
|
||||
code: undefined,
|
||||
status: -1,
|
||||
})
|
||||
|
||||
/** 监听弹窗打开,同步外部参数 */
|
||||
watch(() => props.modelValue, (val) => {
|
||||
if (val && props.searchParams) {
|
||||
formData.name = props.searchParams.name
|
||||
formData.code = props.searchParams.code
|
||||
formData.status = props.searchParams.status ?? -1
|
||||
}
|
||||
})
|
||||
|
||||
/** 搜索 */
|
||||
function handleSearch() {
|
||||
visible.value = false
|
||||
emit('search', { ...formData })
|
||||
}
|
||||
|
||||
/** 重置 */
|
||||
function handleReset() {
|
||||
formData.name = undefined
|
||||
formData.code = undefined
|
||||
formData.status = -1
|
||||
visible.value = false
|
||||
emit('reset')
|
||||
}
|
||||
</script>
|
||||
123
src/pages-system/post/detail/index.vue
Normal file
123
src/pages-system/post/detail/index.vue
Normal file
@@ -0,0 +1,123 @@
|
||||
<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="岗位名称" :value="formData?.name || '-'" />
|
||||
<wd-cell title="岗位编码" :value="formData?.code || '-'" />
|
||||
<wd-cell title="显示顺序" :value="String(formData?.sort ?? '-')" />
|
||||
<wd-cell title="状态">
|
||||
<dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="formData?.status" />
|
||||
</wd-cell>
|
||||
<wd-cell title="备注" :value="formData?.remark || '-'" />
|
||||
<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 class="flex-1" type="warning" @click="handleEdit">
|
||||
编辑
|
||||
</wd-button>
|
||||
<wd-button class="flex-1" type="error" :loading="deleting" @click="handleDelete">
|
||||
删除
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { Post } from '@/api/system/post'
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { useToast } from 'wot-design-uni'
|
||||
import { deletePost, getPost } from '@/api/system/post'
|
||||
import { DICT_TYPE } from '@/utils/constants'
|
||||
import { formatDateTime } from '@/utils/date'
|
||||
|
||||
const props = defineProps<{
|
||||
id: number
|
||||
}>()
|
||||
|
||||
definePage({
|
||||
style: {
|
||||
navigationBarTitleText: '',
|
||||
navigationStyle: 'custom',
|
||||
},
|
||||
})
|
||||
|
||||
const toast = useToast()
|
||||
const formData = ref<Post>() // 详情数据
|
||||
const deleting = ref(false) // 删除中
|
||||
|
||||
/** 返回上一页 */
|
||||
function handleBack() {
|
||||
uni.navigateBack()
|
||||
}
|
||||
|
||||
/** 加载岗位详情 */
|
||||
async function getDetail() {
|
||||
if (!props.id) {
|
||||
return
|
||||
}
|
||||
formData.value = await getPost(props.id)
|
||||
}
|
||||
|
||||
/** 编辑岗位 */
|
||||
function handleEdit() {
|
||||
uni.navigateTo({
|
||||
url: `/pages-system/post/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 deletePost(props.id)
|
||||
toast.success('删除成功')
|
||||
setTimeout(() => {
|
||||
handleBack()
|
||||
}, 500)
|
||||
} finally {
|
||||
deleting.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>
|
||||
160
src/pages-system/post/form/index.vue
Normal file
160
src/pages-system/post/form/index.vue
Normal file
@@ -0,0 +1,160 @@
|
||||
<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.name"
|
||||
label="岗位名称"
|
||||
label-width="180rpx"
|
||||
prop="name"
|
||||
clearable
|
||||
placeholder="请输入岗位名称"
|
||||
/>
|
||||
<wd-input
|
||||
v-model="formData.code"
|
||||
label="岗位编码"
|
||||
label-width="180rpx"
|
||||
prop="code"
|
||||
clearable
|
||||
placeholder="请输入岗位编码"
|
||||
/>
|
||||
<wd-cell title="显示顺序" title-width="180rpx" prop="sort" center>
|
||||
<wd-input-number
|
||||
v-model="formData.sort"
|
||||
:min="0"
|
||||
/>
|
||||
</wd-cell>
|
||||
<wd-cell title="状态" title-width="180rpx" prop="status" center>
|
||||
<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 { Post } from '@/api/system/post'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { useToast } from 'wot-design-uni'
|
||||
import { createPost, getPost, updatePost } from '@/api/system/post'
|
||||
import { CommonStatusEnum } from '@/utils/constants'
|
||||
|
||||
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<Post>({
|
||||
id: undefined,
|
||||
name: '',
|
||||
code: '',
|
||||
sort: 0,
|
||||
status: CommonStatusEnum.ENABLE,
|
||||
remark: '',
|
||||
})
|
||||
const formRules = {
|
||||
name: [{ required: true, message: '岗位名称不能为空' }],
|
||||
code: [{ required: true, message: '岗位编码不能为空' }],
|
||||
sort: [{ required: true, message: '显示顺序不能为空' }],
|
||||
status: [{ required: true, message: '状态不能为空' }],
|
||||
}
|
||||
const formRef = ref()
|
||||
|
||||
/** 返回上一页 */
|
||||
function handleBack() {
|
||||
uni.navigateBack()
|
||||
}
|
||||
|
||||
/** 加载岗位详情 */
|
||||
async function getDetail() {
|
||||
if (!props.id) {
|
||||
return
|
||||
}
|
||||
formData.value = await getPost(props.id)
|
||||
}
|
||||
|
||||
/** 提交表单 */
|
||||
async function handleSubmit() {
|
||||
const { valid } = await formRef.value.validate()
|
||||
if (!valid) {
|
||||
return
|
||||
}
|
||||
|
||||
formLoading.value = true
|
||||
try {
|
||||
if (props.id) {
|
||||
await updatePost(formData.value)
|
||||
toast.success('修改成功')
|
||||
} else {
|
||||
await createPost(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>
|
||||
171
src/pages-system/post/index.vue
Normal file
171
src/pages-system/post/index.vue
Normal file
@@ -0,0 +1,171 @@
|
||||
<template>
|
||||
<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="p-24rpx">
|
||||
<view class="mb-16rpx flex items-center justify-between">
|
||||
<view class="text-32rpx text-[#333] font-semibold">
|
||||
{{ item.name }}
|
||||
</view>
|
||||
<dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="item.status" />
|
||||
</view>
|
||||
<view class="mb-12rpx flex items-center text-28rpx text-[#666]">
|
||||
<text class="mr-8rpx text-[#999]">岗位编码:</text>
|
||||
<text>{{ item.code }}</text>
|
||||
</view>
|
||||
<view v-if="item.remark" class="mb-12rpx flex items-center text-28rpx text-[#666]">
|
||||
<text class="mr-8rpx text-[#999]">备注:</text>
|
||||
<text class="line-clamp-1">{{ item.remark }}</text>
|
||||
</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"
|
||||
/>
|
||||
|
||||
<!-- 新增按钮 -->
|
||||
<view
|
||||
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 { Post } from '@/api/system/post'
|
||||
import type { LoadMoreState } from '@/http/types'
|
||||
import { onReachBottom } from '@dcloudio/uni-app'
|
||||
import { onMounted, reactive, ref } from 'vue'
|
||||
import { getPostPage } from '@/api/system/post'
|
||||
import { DICT_TYPE } from '@/utils/constants'
|
||||
import SearchForm from './components/search-form.vue'
|
||||
|
||||
definePage({
|
||||
style: {
|
||||
navigationBarTitleText: '',
|
||||
navigationStyle: 'custom',
|
||||
},
|
||||
})
|
||||
|
||||
const total = ref(0) // 列表的总页数
|
||||
const list = ref<Post[]>([]) // 列表的数据
|
||||
const loadMoreState = ref<LoadMoreState>('loading') // 加载更多状态
|
||||
const searchVisible = ref(false) // 搜索弹窗
|
||||
const queryParams = reactive({
|
||||
pageNo: 1,
|
||||
pageSize: 10,
|
||||
name: undefined as string | undefined,
|
||||
code: undefined as string | undefined,
|
||||
status: -1 as number, // -1 表示全部
|
||||
})
|
||||
|
||||
/** 返回上一页 */
|
||||
function handleBack() {
|
||||
uni.navigateBack()
|
||||
}
|
||||
|
||||
/** 查询岗位列表 */
|
||||
async function getList() {
|
||||
loadMoreState.value = 'loading'
|
||||
try {
|
||||
const data = await getPostPage({
|
||||
...queryParams,
|
||||
status: queryParams.status === -1 ? undefined : queryParams.status,
|
||||
})
|
||||
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?: { name?: string, code?: string, status?: number }) {
|
||||
queryParams.name = data?.name
|
||||
queryParams.code = data?.code
|
||||
queryParams.status = data?.status ?? -1
|
||||
queryParams.pageNo = 1
|
||||
list.value = [] // 清空列表
|
||||
getList()
|
||||
}
|
||||
|
||||
/** 重置按钮操作 */
|
||||
function handleReset() {
|
||||
handleQuery()
|
||||
}
|
||||
|
||||
/** 加载更多 */
|
||||
function loadMore() {
|
||||
if (loadMoreState.value === 'finished') {
|
||||
return
|
||||
}
|
||||
queryParams.pageNo++
|
||||
getList()
|
||||
}
|
||||
|
||||
/** 新增岗位 */
|
||||
function handleAdd() {
|
||||
uni.navigateTo({
|
||||
url: '/pages-system/post/form/index',
|
||||
})
|
||||
}
|
||||
|
||||
/** 查看详情 */
|
||||
function handleDetail(item: Post) {
|
||||
uni.navigateTo({
|
||||
url: `/pages-system/post/detail/index?id=${item.id}`,
|
||||
})
|
||||
}
|
||||
|
||||
/** 触底加载更多 */
|
||||
onReachBottom(() => {
|
||||
loadMore()
|
||||
})
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(() => {
|
||||
getList()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
||||
116
src/pages-system/role/components/search-form.vue
Normal file
116
src/pages-system/role/components/search-form.vue
Normal file
@@ -0,0 +1,116 @@
|
||||
<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.name"
|
||||
placeholder="请输入角色名称"
|
||||
clearable
|
||||
/>
|
||||
</view>
|
||||
<view class="mb-24rpx">
|
||||
<view class="mb-12rpx text-28rpx text-[#666]">
|
||||
角色标识
|
||||
</view>
|
||||
<wd-input
|
||||
v-model="formData.code"
|
||||
placeholder="请输入角色标识"
|
||||
clearable
|
||||
/>
|
||||
</view>
|
||||
<view class="mb-32rpx">
|
||||
<view class="mb-12rpx text-28rpx text-[#666]">
|
||||
状态
|
||||
</view>
|
||||
<wd-radio-group v-model="formData.status" shape="button" size="medium">
|
||||
<wd-radio :value="-1">
|
||||
全部
|
||||
</wd-radio>
|
||||
<wd-radio :value="0">
|
||||
启用
|
||||
</wd-radio>
|
||||
<wd-radio :value="1">
|
||||
禁用
|
||||
</wd-radio>
|
||||
</wd-radio-group>
|
||||
</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 {
|
||||
name?: string
|
||||
code?: string
|
||||
status: number // -1 表示全部
|
||||
}
|
||||
|
||||
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>({
|
||||
name: undefined,
|
||||
code: undefined,
|
||||
status: -1,
|
||||
})
|
||||
|
||||
/** 监听弹窗打开,同步外部参数 */
|
||||
watch(() => props.modelValue, (val) => {
|
||||
if (val && props.searchParams) {
|
||||
formData.name = props.searchParams.name
|
||||
formData.code = props.searchParams.code
|
||||
formData.status = props.searchParams.status ?? -1
|
||||
}
|
||||
})
|
||||
|
||||
/** 搜索 */
|
||||
function handleSearch() {
|
||||
visible.value = false
|
||||
emit('search', { ...formData })
|
||||
}
|
||||
|
||||
/** 重置 */
|
||||
function handleReset() {
|
||||
formData.name = undefined
|
||||
formData.code = undefined
|
||||
formData.status = -1
|
||||
visible.value = false
|
||||
emit('reset')
|
||||
}
|
||||
</script>
|
||||
124
src/pages-system/role/detail/index.vue
Normal file
124
src/pages-system/role/detail/index.vue
Normal file
@@ -0,0 +1,124 @@
|
||||
<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="角色名称" :value="formData?.name || '-'" />
|
||||
<wd-cell title="角色标识" :value="formData?.code || '-'" />
|
||||
<wd-cell title="显示顺序" :value="String(formData?.sort ?? '-')" />
|
||||
<wd-cell title="状态">
|
||||
<dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="formData?.status" />
|
||||
</wd-cell>
|
||||
<wd-cell title="备注" :value="formData?.remark || '-'" />
|
||||
<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 class="flex-1" type="warning" @click="handleEdit">
|
||||
编辑
|
||||
</wd-button>
|
||||
<wd-button class="flex-1" type="error" :loading="deleting" @click="handleDelete">
|
||||
删除
|
||||
</wd-button>
|
||||
</view>
|
||||
<!-- TODO @芋艿:1)数据权限;2)菜单权限 -->
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { Role } from '@/api/system/role'
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { useToast } from 'wot-design-uni'
|
||||
import { deleteRole, getRole } from '@/api/system/role'
|
||||
import { DICT_TYPE } from '@/utils/constants'
|
||||
import { formatDateTime } from '@/utils/date'
|
||||
|
||||
const props = defineProps<{
|
||||
id: number
|
||||
}>()
|
||||
|
||||
definePage({
|
||||
style: {
|
||||
navigationBarTitleText: '',
|
||||
navigationStyle: 'custom',
|
||||
},
|
||||
})
|
||||
|
||||
const toast = useToast()
|
||||
const formData = ref<Role>() // 详情数据
|
||||
const deleting = ref(false) // 删除中
|
||||
|
||||
/** 返回上一页 */
|
||||
function handleBack() {
|
||||
uni.navigateBack()
|
||||
}
|
||||
|
||||
/** 加载角色详情 */
|
||||
async function getDetail() {
|
||||
if (!props.id) {
|
||||
return
|
||||
}
|
||||
formData.value = await getRole(props.id)
|
||||
}
|
||||
|
||||
/** 编辑角色 */
|
||||
function handleEdit() {
|
||||
uni.navigateTo({
|
||||
url: `/pages-system/role/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 deleteRole(props.id)
|
||||
toast.success('删除成功')
|
||||
setTimeout(() => {
|
||||
handleBack()
|
||||
}, 500)
|
||||
} finally {
|
||||
deleting.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>
|
||||
161
src/pages-system/role/form/index.vue
Normal file
161
src/pages-system/role/form/index.vue
Normal file
@@ -0,0 +1,161 @@
|
||||
<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.name"
|
||||
label="角色名称"
|
||||
label-width="180rpx"
|
||||
prop="name"
|
||||
clearable
|
||||
placeholder="请输入角色名称"
|
||||
/>
|
||||
<wd-input
|
||||
v-model="formData.code"
|
||||
label="角色标识"
|
||||
label-width="180rpx"
|
||||
prop="code"
|
||||
clearable
|
||||
placeholder="请输入角色标识"
|
||||
/>
|
||||
<wd-cell title="显示顺序" title-width="180rpx" prop="sort" center>
|
||||
<wd-input-number
|
||||
v-model="formData.sort"
|
||||
:min="0"
|
||||
/>
|
||||
</wd-cell>
|
||||
<wd-cell title="状态" title-width="180rpx" prop="status" center>
|
||||
<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 { Role } from '@/api/system/role'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { useToast } from 'wot-design-uni'
|
||||
import { createRole, getRole, updateRole } from '@/api/system/role'
|
||||
import { CommonStatusEnum } from '@/utils/constants'
|
||||
|
||||
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<Role>({
|
||||
id: undefined,
|
||||
name: '',
|
||||
code: '',
|
||||
sort: 0,
|
||||
status: CommonStatusEnum.ENABLE,
|
||||
remark: '',
|
||||
createTime: '',
|
||||
})
|
||||
const formRules = {
|
||||
name: [{ required: true, message: '角色名称不能为空' }],
|
||||
code: [{ required: true, message: '角色标识不能为空' }],
|
||||
sort: [{ required: true, message: '显示顺序不能为空' }],
|
||||
status: [{ required: true, message: '状态不能为空' }],
|
||||
}
|
||||
const formRef = ref()
|
||||
|
||||
/** 返回上一页 */
|
||||
function handleBack() {
|
||||
uni.navigateBack()
|
||||
}
|
||||
|
||||
/** 加载角色详情 */
|
||||
async function getDetail() {
|
||||
if (!props.id) {
|
||||
return
|
||||
}
|
||||
formData.value = await getRole(props.id)
|
||||
}
|
||||
|
||||
/** 提交表单 */
|
||||
async function handleSubmit() {
|
||||
const { valid } = await formRef.value.validate()
|
||||
if (!valid) {
|
||||
return
|
||||
}
|
||||
|
||||
formLoading.value = true
|
||||
try {
|
||||
if (props.id) {
|
||||
await updateRole(formData.value)
|
||||
toast.success('修改成功')
|
||||
} else {
|
||||
await createRole(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>
|
||||
174
src/pages-system/role/index.vue
Normal file
174
src/pages-system/role/index.vue
Normal file
@@ -0,0 +1,174 @@
|
||||
<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="p-24rpx">
|
||||
<view class="mb-16rpx flex items-center justify-between">
|
||||
<view class="text-32rpx text-[#333] font-semibold">
|
||||
{{ item.name }}
|
||||
</view>
|
||||
<dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="item.status" />
|
||||
</view>
|
||||
<view class="mb-12rpx flex items-center text-28rpx text-[#666]">
|
||||
<text class="mr-8rpx text-[#999]">角色标识:</text>
|
||||
<text>{{ item.code }}</text>
|
||||
</view>
|
||||
<view v-if="item.remark" class="mb-12rpx flex items-center text-28rpx text-[#666]">
|
||||
<text class="mr-8rpx text-[#999]">备注:</text>
|
||||
<text class="line-clamp-1">{{ item.remark }}</text>
|
||||
</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
|
||||
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 { Role } from '@/api/system/role'
|
||||
import type { LoadMoreState } from '@/http/types'
|
||||
import { onReachBottom } from '@dcloudio/uni-app'
|
||||
import { onMounted, reactive, ref } from 'vue'
|
||||
import { getRolePage } from '@/api/system/role'
|
||||
import { DICT_TYPE } from '@/utils/constants'
|
||||
import SearchForm from './components/search-form.vue'
|
||||
|
||||
definePage({
|
||||
style: {
|
||||
navigationBarTitleText: '',
|
||||
navigationStyle: 'custom',
|
||||
},
|
||||
})
|
||||
|
||||
const total = ref(0) // 列表的总页数
|
||||
const list = ref<Role[]>([]) // 列表的数据
|
||||
const loadMoreState = ref<LoadMoreState>('loading') // 加载更多状态
|
||||
const searchVisible = ref(false) // 搜索弹窗
|
||||
const queryParams = reactive({
|
||||
pageNo: 1,
|
||||
pageSize: 10,
|
||||
name: undefined as string | undefined,
|
||||
code: undefined as string | undefined,
|
||||
status: -1 as number, // -1 表示全部
|
||||
})
|
||||
|
||||
/** 返回上一页 */
|
||||
function handleBack() {
|
||||
uni.navigateBack()
|
||||
}
|
||||
|
||||
/** 查询角色列表 */
|
||||
async function getList() {
|
||||
loadMoreState.value = 'loading'
|
||||
try {
|
||||
const data = await getRolePage({
|
||||
...queryParams,
|
||||
status: queryParams.status === -1 ? undefined : queryParams.status,
|
||||
})
|
||||
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.name = data?.name
|
||||
queryParams.code = data?.code
|
||||
queryParams.status = data?.status
|
||||
queryParams.pageNo = 1
|
||||
list.value = [] // 清空列表
|
||||
getList()
|
||||
}
|
||||
|
||||
/** 重置按钮操作 */
|
||||
function handleReset() {
|
||||
handleQuery()
|
||||
}
|
||||
|
||||
/** 加载更多 */
|
||||
function loadMore() {
|
||||
if (loadMoreState.value === 'finished') {
|
||||
return
|
||||
}
|
||||
queryParams.pageNo++
|
||||
getList()
|
||||
}
|
||||
|
||||
/** 新增角色 */
|
||||
function handleAdd() {
|
||||
uni.navigateTo({
|
||||
url: '/pages-system/role/form/index',
|
||||
})
|
||||
}
|
||||
|
||||
/** 查看详情 */
|
||||
function handleDetail(item: Role) {
|
||||
uni.navigateTo({
|
||||
url: `/pages-system/role/detail/index?id=${item.id}`,
|
||||
})
|
||||
}
|
||||
|
||||
/** 触底加载更多 */
|
||||
onReachBottom(() => {
|
||||
loadMore()
|
||||
})
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(() => {
|
||||
getList()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
||||
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