600 lines
12 KiB
Vue
600 lines
12 KiB
Vue
|
|
<!--
|
|||
|
|
页面开发示例代码
|
|||
|
|
展示项目中页面组件的编写规范和最佳实践
|
|||
|
|
-->
|
|||
|
|
|
|||
|
|
<script lang="ts" setup>
|
|||
|
|
/**
|
|||
|
|
* 导入顺序规范:
|
|||
|
|
* 1. Vue 框架相关
|
|||
|
|
* 2. 第三方库
|
|||
|
|
* 3. 工具函数
|
|||
|
|
* 4. API 接口
|
|||
|
|
* 5. 状态管理
|
|||
|
|
* 6. 本地组件
|
|||
|
|
*/
|
|||
|
|
import { ref, reactive, computed, onMounted, onShow, watch } from 'vue';
|
|||
|
|
import { getUserList, getRoleList, createUser, updateUser, deleteUser, type User } from '@/api/user';
|
|||
|
|
import { useUserStore } from '@/store/user';
|
|||
|
|
import { debounce } from '@/utils/common';
|
|||
|
|
import UserForm from './components/UserForm.vue';
|
|||
|
|
|
|||
|
|
// ✅ 页面配置(使用 definePage 宏)
|
|||
|
|
definePage({
|
|||
|
|
style: {
|
|||
|
|
navigationBarTitleText: '用户管理',
|
|||
|
|
// 可以添加其他页面配置
|
|||
|
|
},
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 响应式数据定义
|
|||
|
|
*/
|
|||
|
|
|
|||
|
|
// 加载状态
|
|||
|
|
const loading = ref(false);
|
|||
|
|
|
|||
|
|
// 用户列表
|
|||
|
|
const userList = ref<User[]>([]);
|
|||
|
|
|
|||
|
|
// 搜索参数
|
|||
|
|
const searchParams = reactive({
|
|||
|
|
keyword: '',
|
|||
|
|
status: '',
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 分页参数
|
|||
|
|
const pagination = reactive({
|
|||
|
|
current: 1,
|
|||
|
|
size: 10,
|
|||
|
|
total: 0,
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 选中的用户列表
|
|||
|
|
const selectedUsers = ref<number[]>([]);
|
|||
|
|
|
|||
|
|
// 是否显示表单弹窗
|
|||
|
|
const showUserForm = ref(false);
|
|||
|
|
|
|||
|
|
// 表单模式(add/edit)
|
|||
|
|
const formMode = ref<'add' | 'edit'>('add');
|
|||
|
|
|
|||
|
|
// 当前编辑的用户
|
|||
|
|
const currentUser = ref<User | null>(null);
|
|||
|
|
|
|||
|
|
// 角色列表(用于用户表单)
|
|||
|
|
const roleList = ref([]);
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 计算属性
|
|||
|
|
*/
|
|||
|
|
|
|||
|
|
// 表格选中状态
|
|||
|
|
const isAllSelected = computed(() => {
|
|||
|
|
return userList.value.length > 0 && selectedUsers.value.length === userList.value.length;
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
const isIndeterminate = computed(() => {
|
|||
|
|
return selectedUsers.value.length > 0 && selectedUsers.value.length < userList.value.length;
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 方法定义
|
|||
|
|
*/
|
|||
|
|
|
|||
|
|
// ✅ 初始化数据
|
|||
|
|
/**
|
|||
|
|
* 初始化页面数据
|
|||
|
|
*/
|
|||
|
|
const initData = async () => {
|
|||
|
|
await Promise.all([getUserData(), getRoleData()]);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// ✅ 获取用户列表
|
|||
|
|
/**
|
|||
|
|
* 获取用户列表数据
|
|||
|
|
*/
|
|||
|
|
const getUserData = async () => {
|
|||
|
|
loading.value = true;
|
|||
|
|
try {
|
|||
|
|
const result = await getUserList({
|
|||
|
|
...searchParams,
|
|||
|
|
page: pagination.current,
|
|||
|
|
size: pagination.size,
|
|||
|
|
});
|
|||
|
|
userList.value = result.data;
|
|||
|
|
pagination.total = result.total;
|
|||
|
|
// 清空选中状态
|
|||
|
|
selectedUsers.value = [];
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error('获取用户列表失败:', error);
|
|||
|
|
uni.showToast({
|
|||
|
|
title: '获取数据失败',
|
|||
|
|
icon: 'none',
|
|||
|
|
});
|
|||
|
|
} finally {
|
|||
|
|
loading.value = false;
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// ✅ 获取角色列表
|
|||
|
|
/**
|
|||
|
|
* 获取角色列表(用于用户表单)
|
|||
|
|
*/
|
|||
|
|
const getRoleData = async () => {
|
|||
|
|
try {
|
|||
|
|
const result = await getRoleList();
|
|||
|
|
roleList.value = result;
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error('获取角色列表失败:', error);
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// ✅ 防抖搜索
|
|||
|
|
/**
|
|||
|
|
* 防抖搜索处理
|
|||
|
|
*/
|
|||
|
|
const handleSearch = debounce(() => {
|
|||
|
|
pagination.current = 1; // 重置页码
|
|||
|
|
getUserData();
|
|||
|
|
}, 500);
|
|||
|
|
|
|||
|
|
// ✅ 搜索框输入
|
|||
|
|
/**
|
|||
|
|
* 搜索框输入处理
|
|||
|
|
* @param value 输入值
|
|||
|
|
*/
|
|||
|
|
const handleSearchInput = (value: string) => {
|
|||
|
|
searchParams.keyword = value;
|
|||
|
|
handleSearch();
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// ✅ 清空搜索
|
|||
|
|
/**
|
|||
|
|
* 清空搜索条件
|
|||
|
|
*/
|
|||
|
|
const handleSearchClear = () => {
|
|||
|
|
searchParams.keyword = '';
|
|||
|
|
handleSearch();
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// ✅ 分页改变
|
|||
|
|
/**
|
|||
|
|
* 分页改变处理
|
|||
|
|
* @param page 页码
|
|||
|
|
*/
|
|||
|
|
const handlePageChange = (page: number) => {
|
|||
|
|
pagination.current = page;
|
|||
|
|
getUserData();
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// ✅ 添加用户
|
|||
|
|
/**
|
|||
|
|
* 打开添加用户弹窗
|
|||
|
|
*/
|
|||
|
|
const handleAddUser = () => {
|
|||
|
|
formMode.value = 'add';
|
|||
|
|
currentUser.value = null;
|
|||
|
|
showUserForm.value = true;
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// ✅ 编辑用户
|
|||
|
|
/**
|
|||
|
|
* 打开编辑用户弹窗
|
|||
|
|
* @param user 用户信息
|
|||
|
|
*/
|
|||
|
|
const handleEditUser = (user: User) => {
|
|||
|
|
formMode.value = 'edit';
|
|||
|
|
currentUser.value = { ...user };
|
|||
|
|
showUserForm.value = true;
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// ✅ 删除用户
|
|||
|
|
/**
|
|||
|
|
* 删除用户确认
|
|||
|
|
* @param user 用户信息
|
|||
|
|
*/
|
|||
|
|
const handleDeleteUser = (user: User) => {
|
|||
|
|
uni.showModal({
|
|||
|
|
title: '确认删除',
|
|||
|
|
content: `确定要删除用户"${user.nickname}"吗?`,
|
|||
|
|
success: async (res) => {
|
|||
|
|
if (res.confirm) {
|
|||
|
|
await confirmDelete(user.id);
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
});
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// ✅ 确认删除
|
|||
|
|
/**
|
|||
|
|
* 确认删除用户
|
|||
|
|
* @param id 用户ID
|
|||
|
|
*/
|
|||
|
|
const confirmDelete = async (id: number) => {
|
|||
|
|
try {
|
|||
|
|
await deleteUser(id);
|
|||
|
|
uni.showToast({
|
|||
|
|
title: '删除成功',
|
|||
|
|
icon: 'success',
|
|||
|
|
});
|
|||
|
|
// 刷新列表
|
|||
|
|
getUserData();
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error('删除用户失败:', error);
|
|||
|
|
uni.showToast({
|
|||
|
|
title: '删除失败',
|
|||
|
|
icon: 'none',
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// ✅ 批量删除
|
|||
|
|
/**
|
|||
|
|
* 批量删除用户
|
|||
|
|
*/
|
|||
|
|
const handleBatchDelete = () => {
|
|||
|
|
if (selectedUsers.value.length === 0) {
|
|||
|
|
uni.showToast({
|
|||
|
|
title: '请选择要删除的用户',
|
|||
|
|
icon: 'none',
|
|||
|
|
});
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
uni.showModal({
|
|||
|
|
title: '确认删除',
|
|||
|
|
content: `确定要删除选中的 ${selectedUsers.value.length} 个用户吗?`,
|
|||
|
|
success: async (res) => {
|
|||
|
|
if (res.confirm) {
|
|||
|
|
try {
|
|||
|
|
// 这里应该调用批量删除 API
|
|||
|
|
// await batchDeleteUsers(selectedUsers.value);
|
|||
|
|
uni.showToast({
|
|||
|
|
title: '批量删除成功',
|
|||
|
|
icon: 'success',
|
|||
|
|
});
|
|||
|
|
getUserData();
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error('批量删除失败:', error);
|
|||
|
|
uni.showToast({
|
|||
|
|
title: '删除失败',
|
|||
|
|
icon: 'none',
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
});
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// ✅ 全选/取消全选
|
|||
|
|
/**
|
|||
|
|
* 表格全选切换
|
|||
|
|
* @param checked 是否选中
|
|||
|
|
*/
|
|||
|
|
const handleSelectAll = (checked: boolean) => {
|
|||
|
|
if (checked) {
|
|||
|
|
selectedUsers.value = userList.value.map((user) => user.id);
|
|||
|
|
} else {
|
|||
|
|
selectedUsers.value = [];
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// ✅ 单选切换
|
|||
|
|
/**
|
|||
|
|
* 单个用户选择切换
|
|||
|
|
* @param user 用户信息
|
|||
|
|
*/
|
|||
|
|
const handleSelectItem = (user: User) => {
|
|||
|
|
const index = selectedUsers.value.indexOf(user.id);
|
|||
|
|
if (index === -1) {
|
|||
|
|
selectedUsers.value.push(user.id);
|
|||
|
|
} else {
|
|||
|
|
selectedUsers.value.splice(index, 1);
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// ✅ 表单提交成功
|
|||
|
|
/**
|
|||
|
|
* 用户表单提交成功回调
|
|||
|
|
*/
|
|||
|
|
const handleFormSuccess = () => {
|
|||
|
|
showUserForm.value = false;
|
|||
|
|
currentUser.value = null;
|
|||
|
|
// 刷新列表
|
|||
|
|
getUserData();
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// ✅ 状态切换
|
|||
|
|
/**
|
|||
|
|
* 切换用户状态
|
|||
|
|
* @param user 用户信息
|
|||
|
|
*/
|
|||
|
|
const handleToggleStatus = async (user: User) => {
|
|||
|
|
try {
|
|||
|
|
await updateUser({
|
|||
|
|
id: user.id,
|
|||
|
|
status: user.status === 0 ? 1 : 0,
|
|||
|
|
});
|
|||
|
|
uni.showToast({
|
|||
|
|
title: '状态更新成功',
|
|||
|
|
icon: 'success',
|
|||
|
|
});
|
|||
|
|
// 刷新列表
|
|||
|
|
getUserData();
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error('更新状态失败:', error);
|
|||
|
|
uni.showToast({
|
|||
|
|
title: '操作失败',
|
|||
|
|
icon: 'none',
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 生命周期钩子
|
|||
|
|
*/
|
|||
|
|
onMounted(() => {
|
|||
|
|
initData();
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
onShow(() => {
|
|||
|
|
// 页面显示时刷新数据(可选)
|
|||
|
|
});
|
|||
|
|
</script>
|
|||
|
|
|
|||
|
|
<template>
|
|||
|
|
<view class="user-manage-page">
|
|||
|
|
<!-- ✅ 搜索栏 -->
|
|||
|
|
<view class="search-bar">
|
|||
|
|
<view class="search-input">
|
|||
|
|
<input
|
|||
|
|
v-model="searchParams.keyword"
|
|||
|
|
placeholder="请输入用户名或昵称"
|
|||
|
|
@input="handleSearchInput"
|
|||
|
|
@clear="handleSearchClear"
|
|||
|
|
/>
|
|||
|
|
</view>
|
|||
|
|
<picker
|
|||
|
|
:value="searchParams.status"
|
|||
|
|
@change="handleStatusChange"
|
|||
|
|
:range="[{label:'全部', value:''}, {label:'正常', value:0}, {label:'禁用', value:1}]"
|
|||
|
|
range-key="label"
|
|||
|
|
>
|
|||
|
|
<view class="status-picker">
|
|||
|
|
<text>{{ searchParams.status === '' ? '全部' : searchParams.status === 0 ? '正常' : '禁用' }}</text>
|
|||
|
|
<text class="arrow">▼</text>
|
|||
|
|
</view>
|
|||
|
|
</picker>
|
|||
|
|
</view>
|
|||
|
|
|
|||
|
|
<!-- ✅ 操作栏 -->
|
|||
|
|
<view class="action-bar">
|
|||
|
|
<button class="add-btn" @click="handleAddUser">添加用户</button>
|
|||
|
|
<button
|
|||
|
|
class="delete-btn"
|
|||
|
|
:disabled="selectedUsers.length === 0"
|
|||
|
|
@click="handleBatchDelete"
|
|||
|
|
>
|
|||
|
|
批量删除 ({{ selectedUsers.length }})
|
|||
|
|
</button>
|
|||
|
|
</view>
|
|||
|
|
|
|||
|
|
<!-- ✅ 用户表格 -->
|
|||
|
|
<view class="user-table">
|
|||
|
|
<view class="table-header">
|
|||
|
|
<checkbox
|
|||
|
|
:checked="isAllSelected"
|
|||
|
|
:indeterminate="isIndeterminate"
|
|||
|
|
@tap="handleSelectAll"
|
|||
|
|
/>
|
|||
|
|
<text class="th">用户名</text>
|
|||
|
|
<text class="th">昵称</text>
|
|||
|
|
<text class="th">状态</text>
|
|||
|
|
<text class="th">操作</text>
|
|||
|
|
</view>
|
|||
|
|
|
|||
|
|
<view class="table-body" v-if="!loading && userList.length > 0">
|
|||
|
|
<view
|
|||
|
|
v-for="user in userList"
|
|||
|
|
:key="user.id"
|
|||
|
|
class="table-row"
|
|||
|
|
>
|
|||
|
|
<checkbox
|
|||
|
|
:checked="selectedUsers.includes(user.id)"
|
|||
|
|
@tap="handleSelectItem(user)"
|
|||
|
|
/>
|
|||
|
|
<text class="td">{{ user.username }}</text>
|
|||
|
|
<text class="td">{{ user.nickname }}</text>
|
|||
|
|
<view class="td">
|
|||
|
|
<text
|
|||
|
|
class="status-badge"
|
|||
|
|
:class="{ 'status-active': user.status === 0 }"
|
|||
|
|
@tap="handleToggleStatus(user)"
|
|||
|
|
>
|
|||
|
|
{{ user.status === 0 ? '正常' : '禁用' }}
|
|||
|
|
</text>
|
|||
|
|
</view>
|
|||
|
|
<view class="td action-buttons">
|
|||
|
|
<button class="edit-btn" @tap="handleEditUser(user)">编辑</button>
|
|||
|
|
<button class="delete-btn" @tap="handleDeleteUser(user)">删除</button>
|
|||
|
|
</view>
|
|||
|
|
</view>
|
|||
|
|
</view>
|
|||
|
|
|
|||
|
|
<!-- ✅ 加载中 -->
|
|||
|
|
<view class="loading-container" v-if="loading">
|
|||
|
|
<text>加载中...</text>
|
|||
|
|
</view>
|
|||
|
|
|
|||
|
|
<!-- ✅ 空数据 -->
|
|||
|
|
<view class="empty-container" v-if="!loading && userList.length === 0">
|
|||
|
|
<text>暂无数据</text>
|
|||
|
|
</view>
|
|||
|
|
</view>
|
|||
|
|
|
|||
|
|
<!-- ✅ 分页 -->
|
|||
|
|
<view class="pagination" v-if="pagination.total > 0">
|
|||
|
|
<text>共 {{ pagination.total }} 条记录</text>
|
|||
|
|
<!-- 这里可以使用 uni-app 的分页组件 -->
|
|||
|
|
</view>
|
|||
|
|
|
|||
|
|
<!-- ✅ 用户表单弹窗 -->
|
|||
|
|
<user-form
|
|||
|
|
v-model="showUserForm"
|
|||
|
|
:mode="formMode"
|
|||
|
|
:user="currentUser"
|
|||
|
|
:role-list="roleList"
|
|||
|
|
@success="handleFormSuccess"
|
|||
|
|
/>
|
|||
|
|
</view>
|
|||
|
|
</template>
|
|||
|
|
|
|||
|
|
<style scoped lang="scss">
|
|||
|
|
.user-manage-page {
|
|||
|
|
padding: 20rpx;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* 搜索栏 */
|
|||
|
|
.search-bar {
|
|||
|
|
display: flex;
|
|||
|
|
gap: 20rpx;
|
|||
|
|
margin-bottom: 20rpx;
|
|||
|
|
|
|||
|
|
.search-input {
|
|||
|
|
flex: 1;
|
|||
|
|
padding: 10rpx 20rpx;
|
|||
|
|
background: #f5f5f5;
|
|||
|
|
border-radius: 8rpx;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.status-picker {
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
gap: 10rpx;
|
|||
|
|
padding: 10rpx 20rpx;
|
|||
|
|
background: #f5f5f5;
|
|||
|
|
border-radius: 8rpx;
|
|||
|
|
|
|||
|
|
.arrow {
|
|||
|
|
font-size: 20rpx;
|
|||
|
|
color: #999;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* 操作栏 */
|
|||
|
|
.action-bar {
|
|||
|
|
display: flex;
|
|||
|
|
gap: 20rpx;
|
|||
|
|
margin-bottom: 20rpx;
|
|||
|
|
|
|||
|
|
.add-btn,
|
|||
|
|
.delete-btn {
|
|||
|
|
padding: 20rpx 40rpx;
|
|||
|
|
border-radius: 8rpx;
|
|||
|
|
font-size: 28rpx;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.add-btn {
|
|||
|
|
background: #4a90ff;
|
|||
|
|
color: white;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.delete-btn {
|
|||
|
|
background: #f56c6c;
|
|||
|
|
color: white;
|
|||
|
|
|
|||
|
|
&[disabled] {
|
|||
|
|
background: #ccc;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* 表格 */
|
|||
|
|
.user-table {
|
|||
|
|
background: white;
|
|||
|
|
border-radius: 8rpx;
|
|||
|
|
overflow: hidden;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.table-header {
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
padding: 20rpx;
|
|||
|
|
background: #f5f5f5;
|
|||
|
|
font-weight: 600;
|
|||
|
|
|
|||
|
|
.th {
|
|||
|
|
flex: 1;
|
|||
|
|
margin-left: 20rpx;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.table-row {
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
padding: 20rpx;
|
|||
|
|
border-bottom: 1rpx solid #f0f0f0;
|
|||
|
|
|
|||
|
|
&:last-child {
|
|||
|
|
border-bottom: none;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.td {
|
|||
|
|
flex: 1;
|
|||
|
|
margin-left: 20rpx;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.status-badge {
|
|||
|
|
padding: 8rpx 16rpx;
|
|||
|
|
border-radius: 4rpx;
|
|||
|
|
font-size: 24rpx;
|
|||
|
|
background: #f56c6c;
|
|||
|
|
color: white;
|
|||
|
|
|
|||
|
|
&.status-active {
|
|||
|
|
background: #67c23a;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.action-buttons {
|
|||
|
|
display: flex;
|
|||
|
|
gap: 10rpx;
|
|||
|
|
|
|||
|
|
button {
|
|||
|
|
padding: 8rpx 16rpx;
|
|||
|
|
border-radius: 4rpx;
|
|||
|
|
font-size: 24rpx;
|
|||
|
|
|
|||
|
|
&.edit-btn {
|
|||
|
|
background: #4a90ff;
|
|||
|
|
color: white;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
&.delete-btn {
|
|||
|
|
background: #f56c6c;
|
|||
|
|
color: white;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* 空状态 */
|
|||
|
|
.loading-container,
|
|||
|
|
.empty-container {
|
|||
|
|
display: flex;
|
|||
|
|
justify-content: center;
|
|||
|
|
align-items: center;
|
|||
|
|
padding: 100rpx;
|
|||
|
|
color: #999;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* 分页 */
|
|||
|
|
.pagination {
|
|||
|
|
margin-top: 20rpx;
|
|||
|
|
text-align: center;
|
|||
|
|
color: #666;
|
|||
|
|
}
|
|||
|
|
</style>
|