mirror of
https://github.com/fastapiadmin/FastapiAdmin.git
synced 2026-10-11 20:05:44 +00:00
feat: 重构前端项目结构并优化代码
refactor: 迁移前端资源文件至web目录 feat: 新增多种图标资源 style: 统一代码风格和格式化配置 docs: 更新README和文档说明 chore: 更新依赖和配置文件 fix: 修复部分类型定义和枚举 perf: 优化路由和组件加载逻辑
This commit is contained in:
@@ -0,0 +1,177 @@
|
||||
<!-- 部门树 -->
|
||||
<template>
|
||||
<el-card shadow="hover">
|
||||
<div class="dept-tree-toolbar">
|
||||
<el-input v-model="deptName" placeholder="部门名称" class="dept-tree-search">
|
||||
<template #prefix>
|
||||
<el-icon>
|
||||
<Search />
|
||||
</el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
<el-button
|
||||
type="primary"
|
||||
link
|
||||
size="small"
|
||||
class="dept-tree-expand-btn"
|
||||
@click="toggleTreeExpandAll"
|
||||
>
|
||||
<el-icon :size="14">
|
||||
<Switch />
|
||||
</el-icon>
|
||||
<span>{{ treeExpanded ? '收起' : '展开' }}</span>
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<el-tree
|
||||
ref="deptTreeRef"
|
||||
class="mt-2"
|
||||
node-key="value"
|
||||
:data="deptOptions"
|
||||
:props="{ children: 'children', label: 'label', disabled: 'disabled' }"
|
||||
:expand-on-click-node="false"
|
||||
:filter-node-method="handleFilter"
|
||||
default-expand-all
|
||||
@node-click="handleNodeClick"
|
||||
>
|
||||
<template #empty>
|
||||
<el-empty :image-size="80" description="暂无数据" />
|
||||
</template>
|
||||
</el-tree>
|
||||
</el-card>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Switch } from '@element-plus/icons-vue';
|
||||
import DeptAPI, { DeptPageQuery } from '@/api/module_system/dept';
|
||||
import { formatTree } from '@/utils/common';
|
||||
import type { FilterNodeMethodFunction, TreeInstance } from 'element-plus';
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: [String, Number],
|
||||
default: undefined,
|
||||
},
|
||||
});
|
||||
|
||||
const deptOptions = ref<OptionType[]>([]); // 部门列表
|
||||
const deptTreeRef = ref<TreeInstance>(); // 部门树
|
||||
const deptName = ref(); // 部门名称
|
||||
/** 与「全部展开」状态同步,用于按钮提示(手动展开单节点后可能不完全一致) */
|
||||
const treeExpanded = ref(true);
|
||||
|
||||
const emits = defineEmits(['node-click', 'update:modelValue']);
|
||||
|
||||
const deptId = useVModel(props, 'modelValue', emits);
|
||||
|
||||
watch(deptName, (val) => {
|
||||
deptTreeRef.value?.filter(val);
|
||||
});
|
||||
|
||||
type TreeStoreNode = {
|
||||
childNodes?: TreeStoreNode[];
|
||||
expand: () => void;
|
||||
collapse: () => void;
|
||||
};
|
||||
|
||||
function getTreeRoot() {
|
||||
const tree = deptTreeRef.value as TreeInstance & { store?: { root: TreeStoreNode } };
|
||||
return tree?.store?.root;
|
||||
}
|
||||
|
||||
/** 展开整棵树 */
|
||||
function expandAllTreeNodes() {
|
||||
const root = getTreeRoot();
|
||||
if (!root) return;
|
||||
const walk = (node: TreeStoreNode) => {
|
||||
node.childNodes?.forEach((child) => {
|
||||
if (child.childNodes?.length) {
|
||||
child.expand();
|
||||
walk(child);
|
||||
}
|
||||
});
|
||||
};
|
||||
walk(root);
|
||||
}
|
||||
|
||||
/** 收起整棵树 */
|
||||
function collapseAllTreeNodes() {
|
||||
const root = getTreeRoot();
|
||||
if (!root) return;
|
||||
const walk = (node: TreeStoreNode) => {
|
||||
node.childNodes?.forEach((child) => {
|
||||
walk(child);
|
||||
child.collapse();
|
||||
});
|
||||
};
|
||||
walk(root);
|
||||
}
|
||||
|
||||
function toggleTreeExpandAll() {
|
||||
if (treeExpanded.value) {
|
||||
collapseAllTreeNodes();
|
||||
treeExpanded.value = false;
|
||||
} else {
|
||||
expandAllTreeNodes();
|
||||
treeExpanded.value = true;
|
||||
}
|
||||
}
|
||||
|
||||
interface Tree {
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* 部门筛选
|
||||
*/
|
||||
const handleFilter: FilterNodeMethodFunction = (value: string, data: Tree) => {
|
||||
if (!value) return true;
|
||||
return data.label.includes(value);
|
||||
};
|
||||
|
||||
/** 部门树节点 Click */
|
||||
function handleNodeClick(data: { [key: string]: any }) {
|
||||
deptId.value = data.value;
|
||||
emits('node-click');
|
||||
}
|
||||
|
||||
const queryFormData = reactive<DeptPageQuery>({
|
||||
name: undefined,
|
||||
status: undefined,
|
||||
created_time: undefined,
|
||||
});
|
||||
|
||||
const loading = ref(true);
|
||||
|
||||
onBeforeMount(() => {
|
||||
loading.value = true;
|
||||
DeptAPI.listDept(queryFormData)
|
||||
.then((response) => {
|
||||
deptOptions.value = formatTree(response.data.data);
|
||||
})
|
||||
.finally(() => {
|
||||
loading.value = false;
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.dept-tree-toolbar {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
align-items: center;
|
||||
|
||||
.dept-tree-search {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.dept-tree-expand-btn {
|
||||
flex-shrink: 0;
|
||||
height: auto;
|
||||
min-height: unset;
|
||||
padding: 0 4px;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,132 @@
|
||||
<!-- 列表选择器 -->
|
||||
<template>
|
||||
<table-select
|
||||
:text="text"
|
||||
:select-config="selectConfig"
|
||||
@confirm-click="handleConfirm"
|
||||
@clear-click="handleClearSelection"
|
||||
>
|
||||
<template #status="scope">
|
||||
<el-tag :type="scope.row[scope.prop] === '0' ? 'success' : 'danger'">
|
||||
{{ scope.row[scope.prop] === '0' ? '启用' : '停用' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</table-select>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { ISelectConfig } from '@/components/TableSelect/index.vue';
|
||||
import UserAPI from '@/api/module_system/user';
|
||||
|
||||
// 父组件双向绑定的值(选中用户ID)
|
||||
const props = defineProps<{ modelValue?: number }>();
|
||||
|
||||
// 事件向上派发,支持父组件监听和 v-model(仅回传选中用户ID)
|
||||
const emit = defineEmits<{
|
||||
confirmClick: [data: IUser[]];
|
||||
'update:modelValue': [val: number | undefined];
|
||||
}>();
|
||||
|
||||
const selectConfig: ISelectConfig = {
|
||||
pk: 'id',
|
||||
width: '167.5px', // 与搜索表单其他输入宽度一致
|
||||
placeholder: '请选择用户',
|
||||
popover: {
|
||||
width: 720, // 弹出层宽度,提升可用性(约 720px)
|
||||
},
|
||||
formItems: [
|
||||
{
|
||||
type: 'select',
|
||||
label: '状态',
|
||||
prop: 'status',
|
||||
initialValue: '0',
|
||||
attrs: {
|
||||
placeholder: '全部',
|
||||
clearable: true,
|
||||
style: {
|
||||
width: '140px',
|
||||
},
|
||||
},
|
||||
options: [
|
||||
{ label: '启用', value: '0' },
|
||||
{ label: '停用', value: '1' },
|
||||
],
|
||||
},
|
||||
],
|
||||
indexAction(params) {
|
||||
// 映射查询参数到后端接口
|
||||
const query: any = { ...params };
|
||||
// 清理空字符串/空值,避免后端 422
|
||||
Object.keys(query).forEach((k) => {
|
||||
const v = query[k];
|
||||
if (v === '' || v === null || v === undefined) {
|
||||
delete query[k];
|
||||
}
|
||||
});
|
||||
// 规范化状态为布尔值
|
||||
if (typeof query.status === 'string') {
|
||||
if (query.status === 'true') query.status = true;
|
||||
else if (query.status === 'false') query.status = false;
|
||||
}
|
||||
// 请求用户分页列表并适配 TableSelect 需要的结构
|
||||
return UserAPI.listUser(query).then((res: any) => {
|
||||
return {
|
||||
total: res.data.data.total,
|
||||
list: res.data.data.items,
|
||||
};
|
||||
});
|
||||
},
|
||||
tableColumns: [
|
||||
{ type: 'selection', width: 50, align: 'center' },
|
||||
{ label: '编号', align: 'center', prop: 'id', width: 100 },
|
||||
{ label: '账号', align: 'center', prop: 'username' },
|
||||
{ label: '用户名', align: 'center', prop: 'name', width: 120 },
|
||||
{
|
||||
label: '状态',
|
||||
align: 'center',
|
||||
prop: 'status',
|
||||
templet: 'custom',
|
||||
slotName: 'status',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
interface IUser {
|
||||
id: number;
|
||||
username: string;
|
||||
name: string;
|
||||
mobile?: string;
|
||||
gender?: string;
|
||||
avatar?: string;
|
||||
email?: string | null;
|
||||
status?: string;
|
||||
dept_name?: string;
|
||||
role_names?: string[];
|
||||
created_time?: string;
|
||||
}
|
||||
const selectedUser = ref<IUser>();
|
||||
function handleConfirm(data: IUser[]) {
|
||||
selectedUser.value = data[0];
|
||||
const id = selectedUser.value?.id;
|
||||
emit('update:modelValue', id);
|
||||
emit('confirmClick', data);
|
||||
}
|
||||
function handleClearSelection() {
|
||||
selectedUser.value = undefined;
|
||||
emit('update:modelValue', undefined);
|
||||
}
|
||||
|
||||
// 当父组件重置 v-model(modelValue)为 undefined 时,清空本地显示
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(val) => {
|
||||
if (val === undefined || val === null) {
|
||||
selectedUser.value = undefined;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const text = computed(() => {
|
||||
return selectedUser.value ? `${selectedUser.value.username} - ${selectedUser.value.name}` : '';
|
||||
});
|
||||
</script>
|
||||
Reference in New Issue
Block a user