Files
youlai-app/src/pages/work/dept/index.vue

364 lines
8.9 KiB
Vue

<template>
<view class="dept-page">
<!-- 搜索栏 -->
<view class="search-bar">
<wd-search
v-model="searchText"
placeholder="搜索部门名称"
hide-cancel
@search="handleSearch"
/>
</view>
<!-- 部门列表 -->
<scroll-view class="dept-list" scroll-y>
<view v-if="filteredDeptList.length > 0" class="dept-tree">
<template v-for="dept in filteredDeptList" :key="dept.id">
<dept-item
:dept="dept"
:level="0"
@click="handleDeptClick"
/>
</template>
</view>
<!-- 空状态 -->
<view v-else class="empty-state">
<wd-icon name="folder-open" size="48" color="#d1d5db" />
<text class="empty-state__text">暂无部门数据</text>
</view>
</scroll-view>
<!-- 底部操作栏 -->
<view class="action-bar">
<wd-button type="primary" block @click="handleAddDept">
<wd-icon name="add" size="16" color="#fff" />
<text>新增部门</text>
</wd-button>
</view>
</view>
</template>
<script setup lang="ts">
/**
* 部门管理页面
*
* 功能说明:
* - 展示部门树形结构
* - 支持搜索部门
* - 新增/编辑/删除部门
*
* 技术要点:
* - 使用 BEM 命名规范
* - 支持暗黑模式
* - 使用 CSS 变量
* - 递归组件实现树形结构
*/
// ============================================================================
// 类型定义
// ============================================================================
interface DeptNode {
id: string
label: string
userCount?: number
children?: DeptNode[]
}
// ============================================================================
// 子组件 - 部门项(支持递归)
// ============================================================================
const DeptItem = defineComponent({
name: "DeptItem",
props: {
dept: { type: Object as PropType<DeptNode>, required: true },
level: { type: Number, default: 0 },
},
emits: ["click"],
setup(props, { emit }) {
const isExpanded = ref(false)
const hasChildren = computed(() => props.dept.children && props.dept.children.length > 0)
function toggleExpand() {
if (hasChildren.value) {
isExpanded.value = !isExpanded.value
}
}
function handleClick() {
emit("click", props.dept)
}
return {
isExpanded,
hasChildren,
toggleExpand,
handleClick,
}
},
template: `
<view class="dept-node">
<view
class="dept-node__item"
:class="{ 'dept-node__item--has-children': hasChildren }"
:style="{ paddingLeft: level * 32 + 16 + 'rpx' }"
>
<view class="dept-node__expand" @click="toggleExpand">
<wd-icon
v-if="hasChildren"
name="arrow-right"
size="14"
class="dept-node__arrow"
:class="{ 'dept-node__arrow--expanded': isExpanded }"
/>
</view>
<view class="dept-node__content" @click="handleClick">
<wd-icon
name="folder"
size="18"
:color="hasChildren ? 'var(--color-primary)' : 'var(--color-text-secondary)'"
/>
<text class="dept-node__name">{{ dept.label }}</text>
</view>
<view class="dept-node__meta" @click="handleClick">
<text class="dept-node__count">{{ dept.userCount || 0 }} 人</text>
<wd-icon name="arrow-right" size="14" color="var(--color-text-secondary)" />
</view>
</view>
<!-- 子节点 -->
<view v-if="hasChildren && isExpanded" class="dept-node__children">
<DeptItem
v-for="child in dept.children"
:key="child.id"
:dept="child"
:level="level + 1"
@click="$emit('click', $event)"
/>
</view>
</view>
`,
})
// ============================================================================
// 响应式数据
// ============================================================================
const searchText = ref("")
// 部门树形数据(示例数据)
const deptList = ref<DeptNode[]>([
{
id: "1",
label: "有来技术",
userCount: 128,
children: [
{
id: "1-1",
label: "研发中心",
userCount: 45,
children: [
{ id: "1-1-1", label: "前端组", userCount: 15 },
{ id: "1-1-2", label: "后端组", userCount: 18 },
{ id: "1-1-3", label: "测试组", userCount: 12 },
],
},
{
id: "1-2",
label: "产品中心",
userCount: 20,
children: [
{ id: "1-2-1", label: "产品组", userCount: 12 },
{ id: "1-2-2", label: "设计组", userCount: 8 },
],
},
{
id: "1-3",
label: "运营中心",
userCount: 35,
},
{
id: "1-4",
label: "行政中心",
userCount: 28,
},
],
},
])
// 过滤后的部门列表
const filteredDeptList = computed(() => {
if (!searchText.value) return deptList.value
function filterDepts(depts: DeptNode[]): DeptNode[] {
return depts.reduce<DeptNode[]>((acc, dept) => {
const matched = dept.label.toLowerCase().includes(searchText.value.toLowerCase())
const filteredChildren = dept.children ? filterDepts(dept.children) : []
if (matched || filteredChildren.length > 0) {
acc.push({
...dept,
children: filteredChildren.length > 0 ? filteredChildren : dept.children,
})
}
return acc
}, [])
}
return filterDepts(deptList.value)
})
// ============================================================================
// 事件处理
// ============================================================================
/** 搜索 */
function handleSearch() {
uni.showToast({ title: `搜索: ${searchText.value}`, icon: "none" })
}
/** 部门点击 */
function handleDeptClick(dept: DeptNode) {
uni.showToast({ title: `点击: ${dept.label}`, icon: "none" })
}
/** 新增部门 */
function handleAddDept() {
uni.showToast({ title: "新增部门", icon: "none" })
}
</script>
<route lang="json">
{
"name": "dept",
"style": { "navigationBarTitleText": "部门管理" }
}
</route>
<style lang="scss" scoped>
// ============================================================================
// 页面容器
// ============================================================================
.dept-page {
display: flex;
flex-direction: column;
min-height: 100vh;
background-color: var(--color-bg-secondary);
}
// ============================================================================
// 搜索栏
// ============================================================================
.search-bar {
padding: 16rpx 24rpx;
background-color: var(--color-bg);
}
// ============================================================================
// 部门列表
// ============================================================================
.dept-list {
flex: 1;
}
.dept-tree {
background-color: var(--color-bg);
}
// ============================================================================
// 部门节点(递归组件样式)
// ============================================================================
.dept-node {
&__item {
display: flex;
align-items: center;
height: 96rpx;
border-bottom: 2rpx solid var(--color-border-light);
&:active {
background-color: var(--color-bg-secondary);
}
}
&__expand {
display: flex;
align-items: center;
justify-content: center;
width: 48rpx;
height: 48rpx;
}
&__arrow {
color: var(--color-text-secondary);
transition: transform 0.2s ease;
&--expanded {
transform: rotate(90deg);
}
}
&__content {
display: flex;
flex: 1;
align-items: center;
gap: 16rpx;
}
&__name {
font-size: 28rpx;
color: var(--color-text);
}
&__meta {
display: flex;
align-items: center;
gap: 8rpx;
padding-right: 24rpx;
}
&__count {
font-size: 24rpx;
color: var(--color-text-secondary);
}
&__children {
background-color: var(--color-bg-secondary);
}
}
// ============================================================================
// 空状态
// ============================================================================
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 100rpx 0;
&__text {
margin-top: 24rpx;
font-size: 28rpx;
color: var(--color-text-secondary);
}
}
// ============================================================================
// 底部操作栏
// ============================================================================
.action-bar {
padding: 24rpx;
padding-bottom: calc(24rpx + env(safe-area-inset-bottom));
background-color: var(--color-bg);
box-shadow: 0 -4rpx 16rpx rgba(0, 0, 0, 0.05);
}
</style>