mirror of
https://github.com/ccmjga/zhilu-admin
synced 2026-03-14 05:33:42 +08:00
tablefilter
This commit is contained in:
@@ -1,235 +0,0 @@
|
||||
# 表单布局组件说明
|
||||
|
||||
本项目提供了四种表单布局组件和一个通用按钮组件,用于不同场景下的数据展示和交互:
|
||||
|
||||
## 1. TableFormLayout.vue
|
||||
|
||||
PC端表格布局组件,适用于不需要checkbox的普通表格展示。
|
||||
|
||||
### 使用方法
|
||||
|
||||
```vue
|
||||
<TableFormLayout
|
||||
:items="dataItems"
|
||||
:columns="columns"
|
||||
:keyField="'id'"
|
||||
@sort="handleSortClick">
|
||||
<!-- 自定义列内容 -->
|
||||
<template #fieldName="{ item }">
|
||||
{{ item.fieldValue }}
|
||||
</template>
|
||||
<!-- 排序图标 -->
|
||||
<template #sort-icon="{ field }">
|
||||
<SortIcon :sortField="getSortField(field)" />
|
||||
</template>
|
||||
</TableFormLayout>
|
||||
```
|
||||
|
||||
### 属性
|
||||
|
||||
- `items`: 要展示的数据数组
|
||||
- `columns`: 列配置数组,每个列对象包含 `title`、`field`、`sortable`(可选)、`class`(可选)
|
||||
- `idField`: (可选) 指定数据项的ID字段名,默认为 'id'
|
||||
- `keyField`: (可选) 指定数据项的唯一键字段名,用于v-for的key
|
||||
- `hasCheckbox`: (可选) 是否显示复选框,默认为false
|
||||
|
||||
### 事件
|
||||
|
||||
- `sort`: 当点击可排序列时触发,参数为字段名
|
||||
- `update:checkedItems`: 当选中项变化时触发,参数为选中项的ID数组
|
||||
- `all-checked-change`: 当全选/取消全选时触发,参数为是否全选
|
||||
|
||||
## 2. TableFormLayoutWithCheckbox.vue
|
||||
|
||||
PC端表格布局组件,带有checkbox功能,适用于需要多选的表格(如绑定关系管理页面)。
|
||||
|
||||
### 使用方法
|
||||
|
||||
```vue
|
||||
<TableFormLayoutWithCheckbox
|
||||
:items="dataItems"
|
||||
:columns="columns"
|
||||
:keyField="'id'"
|
||||
v-model="checkedIds"
|
||||
@all-checked-change="allChecked = $event">
|
||||
<!-- 自定义列内容 -->
|
||||
<template #fieldName="{ item }">
|
||||
{{ item.fieldValue }}
|
||||
</template>
|
||||
</TableFormLayoutWithCheckbox>
|
||||
```
|
||||
|
||||
### 属性
|
||||
|
||||
- `items`: 要展示的数据数组
|
||||
- `columns`: 列配置数组
|
||||
- `idField`: (可选) 指定数据项的ID字段名,默认为 'id'
|
||||
- `keyField`: (可选) 指定数据项的唯一键字段名,用于v-for的key
|
||||
- `modelValue`: (可选) 选中项的ID数组,支持v-model双向绑定
|
||||
|
||||
### 事件
|
||||
|
||||
- `update:modelValue`: 当选中项变化时触发,参数为选中项的ID数组
|
||||
- `sort`: 当点击可排序列时触发,参数为字段名
|
||||
- `all-checked-change`: 当全选/取消全选时触发,参数为是否全选
|
||||
|
||||
## 3. MobileCardList.vue
|
||||
|
||||
移动端卡片布局组件,适用于不需要checkbox的普通卡片展示。
|
||||
|
||||
### 使用方法
|
||||
|
||||
```vue
|
||||
<MobileCardList
|
||||
:items="dataItems"
|
||||
:keyField="'id'">
|
||||
<!-- 标题区域 -->
|
||||
<template #title="{ item }">
|
||||
{{ item.title }}
|
||||
</template>
|
||||
<!-- 状态区域 -->
|
||||
<template #status="{ item }">
|
||||
<div class="flex items-center">
|
||||
<div class="h-2.5 w-2.5 rounded-full me-2" :class="getStatusClass(item)"></div>
|
||||
<span>{{ getStatusText(item) }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<!-- 内容区域 -->
|
||||
<template #content="{ item }">
|
||||
<div>
|
||||
<p class="text-xs font-medium text-gray-600">标签</p>
|
||||
<p class="text-sm text-gray-900">{{ item.value }}</p>
|
||||
</div>
|
||||
</template>
|
||||
<!-- 操作按钮区域 -->
|
||||
<template #actions="{ item }">
|
||||
<div class="flex gap-x-2">
|
||||
<TableButton variant="primary" size="xs" isMobile @click="handleEdit(item)">
|
||||
<template #icon>
|
||||
<svg class="w-3 h-3" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path d="M17.414 2.586a2 2 0 00-2.828 0L7 10.172V13h2.828l7.586-7.586a2 2 0 000-2.828z"></path>
|
||||
</svg>
|
||||
</template>
|
||||
编辑
|
||||
</TableButton>
|
||||
<TableButton variant="danger" size="xs" isMobile @click="handleDelete(item)">删除</TableButton>
|
||||
</div>
|
||||
</template>
|
||||
</MobileCardList>
|
||||
```
|
||||
|
||||
### 属性
|
||||
|
||||
- `items`: 要展示的数据数组
|
||||
- `idField`: (可选) 指定数据项的ID字段名,默认为 'id'
|
||||
- `keyField`: (可选) 指定数据项的唯一键字段名,用于v-for的key
|
||||
|
||||
### 插槽
|
||||
|
||||
- `title`: 卡片标题
|
||||
- `status`: 状态指示器
|
||||
- `content`: 卡片主要内容
|
||||
- `tags`: (可选) 标签/分类
|
||||
- `actions`: 操作按钮
|
||||
|
||||
## 4. MobileCardListWithCheckbox.vue
|
||||
|
||||
移动端卡片布局组件,带有checkbox功能,适用于需要多选的卡片(如绑定关系管理页面)。
|
||||
|
||||
### 使用方法
|
||||
|
||||
```vue
|
||||
<MobileCardListWithCheckbox
|
||||
:items="dataItems"
|
||||
:keyField="'id'"
|
||||
v-model="checkedIds">
|
||||
<!-- 与MobileCardList用法相同 -->
|
||||
<template #title="{ item }">{{ item.title }}</template>
|
||||
<template #content="{ item }">{{ item.content }}</template>
|
||||
</MobileCardListWithCheckbox>
|
||||
```
|
||||
|
||||
### 属性
|
||||
|
||||
- `items`: 要展示的数据数组
|
||||
- `idField`: (可选) 指定数据项的ID字段名,默认为 'id'
|
||||
- `keyField`: (可选) 指定数据项的唯一键字段名,用于v-for的key
|
||||
- `modelValue`: (可选) 选中项的ID数组,支持v-model双向绑定
|
||||
|
||||
### 事件
|
||||
|
||||
- `update:modelValue`: 当选中项变化时触发,参数为选中项的ID数组
|
||||
|
||||
## 5. TableButton.vue
|
||||
|
||||
通用按钮组件,适用于表格和卡片中的操作按钮。
|
||||
|
||||
### 使用方法
|
||||
|
||||
```vue
|
||||
<!-- 基本用法 -->
|
||||
<TableButton @click="handleClick">默认按钮</TableButton>
|
||||
|
||||
<!-- 带图标 -->
|
||||
<TableButton variant="primary" @click="handleEdit">
|
||||
<template #icon>
|
||||
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path d="M17.414 2.586a2 2 0 00-2.828 0L7 10.172V13h2.828l7.586-7.586a2 2 0 000-2.828z"></path>
|
||||
</svg>
|
||||
</template>
|
||||
编辑
|
||||
</TableButton>
|
||||
|
||||
<!-- 不同变体 -->
|
||||
<TableButton variant="secondary">次要按钮</TableButton>
|
||||
<TableButton variant="success">成功按钮</TableButton>
|
||||
<TableButton variant="danger">危险按钮</TableButton>
|
||||
<TableButton variant="warning">警告按钮</TableButton>
|
||||
<TableButton variant="info">信息按钮</TableButton>
|
||||
|
||||
<!-- 不同尺寸 -->
|
||||
<TableButton size="xs">超小按钮</TableButton>
|
||||
<TableButton size="sm">小按钮</TableButton>
|
||||
<TableButton size="md">中按钮</TableButton>
|
||||
<TableButton size="lg">大按钮</TableButton>
|
||||
|
||||
<!-- 移动端尺寸 -->
|
||||
<TableButton size="sm" isMobile>移动端按钮</TableButton>
|
||||
|
||||
<!-- 禁用状态 -->
|
||||
<TableButton disabled>禁用按钮</TableButton>
|
||||
```
|
||||
|
||||
### 属性
|
||||
|
||||
- `variant`: (可选) 按钮变体类型,可选值为 'primary'、'secondary'、'success'、'danger'、'warning'、'info',默认为 'primary'
|
||||
- `size`: (可选) 按钮尺寸,可选值为 'xs'、'sm'、'md'、'lg',默认为 'md'
|
||||
- `disabled`: (可选) 是否禁用,默认为 false
|
||||
- `className`: (可选) 自定义CSS类名
|
||||
- `isMobile`: (可选) 是否为移动端尺寸,默认为 false
|
||||
|
||||
### 事件
|
||||
|
||||
- `click`: 当按钮被点击时触发
|
||||
|
||||
## 使用建议
|
||||
|
||||
1. 对于普通的数据展示页面(如用户管理、岗位管理等),使用 `TableFormLayout` 和 `MobileCardList`
|
||||
2. 对于需要多选功能的页面(如角色绑定、部门绑定等),使用 `TableFormLayoutWithCheckbox` 和 `MobileCardListWithCheckbox`
|
||||
3. 对于表格和卡片中的操作按钮,使用 `TableButton` 组件
|
||||
4. 根据屏幕尺寸自动切换布局:
|
||||
```vue
|
||||
<!-- 移动端卡片布局 -->
|
||||
<div class="md:hidden">
|
||||
<MobileCardList :items="items" :keyField="'id'">
|
||||
<!-- 插槽内容 -->
|
||||
</MobileCardList>
|
||||
</div>
|
||||
|
||||
<!-- PC端表格布局 -->
|
||||
<div class="hidden md:block">
|
||||
<TableFormLayout :items="items" :columns="columns" :keyField="'id'">
|
||||
<!-- 插槽内容 -->
|
||||
</TableFormLayout>
|
||||
</div>
|
||||
```
|
||||
131
frontend/src/components/TableFilterForm.vue
Normal file
131
frontend/src/components/TableFilterForm.vue
Normal file
@@ -0,0 +1,131 @@
|
||||
<template>
|
||||
<div class="flex flex-col sm:flex-row sm:justify-between sm:items-center mb-4 gap-y-3 sm:gap-y-0">
|
||||
<form class="w-full sm:w-auto flex flex-col xs:flex-row gap-2 xs:gap-3 items-stretch xs:items-center">
|
||||
<template v-for="(filter, index) in filters" :key="index">
|
||||
<!-- 输入框类型 -->
|
||||
<div v-if="filter.type === 'input'" class="flex-grow">
|
||||
<label :for="`filter-input-${index}`" class="mb-2 text-sm font-medium text-gray-900 sr-only">{{ filter.label
|
||||
}}</label>
|
||||
<div class="relative">
|
||||
<div class="absolute inset-y-0 start-0 flex items-center ps-3 pointer-events-none">
|
||||
<svg class="w-4 h-4 text-gray-500" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none"
|
||||
viewBox="0 0 20 20">
|
||||
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="m19 19-4-4m0-7A7 7 0 1 1 1 8a7 7 0 0 1 14 0Z" />
|
||||
</svg>
|
||||
</div>
|
||||
<input type="search" :id="`filter-input-${index}`" v-model="filterValues[filter.name]"
|
||||
class="block w-full p-2.5 ps-10 text-sm text-gray-900 border border-gray-300 rounded-lg bg-gray-50 focus:ring-blue-500 focus:border-blue-500"
|
||||
:placeholder="filter.placeholder || ''" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 日期范围选择器 -->
|
||||
<div v-else-if="filter.type === 'date-range'" class="flex-grow">
|
||||
<VueDatePicker v-model="filterValues[filter.name]" locale="zh-CN" range
|
||||
:format="filter.format || 'yyyy/MM/dd HH:mm:ss - yyy/MM/dd HH:mm:ss'" :placeholder="filter.placeholder"
|
||||
:enable-time-picker="filter.enableTimePicker !== false" :auto-apply="filter.autoApply !== false" />
|
||||
</div>
|
||||
|
||||
<!-- 选择器 -->
|
||||
<div v-else-if="filter.type === 'select'" class="flex-grow">
|
||||
<select :id="`filter-select-${index}`" v-model="filterValues[filter.name]"
|
||||
class="w-full xs:w-auto bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block p-2.5">
|
||||
<option v-for="option in filter.options" :key="String(option.value)" :value="option.value">
|
||||
{{ option.label }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<button type="submit"
|
||||
class="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-4 py-2.5"
|
||||
@click.prevent="handleSearch">
|
||||
搜索
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<!-- 额外操作按钮插槽 -->
|
||||
<div class="w-full sm:w-auto">
|
||||
<slot name="actions"></slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, reactive, watch } from "vue";
|
||||
|
||||
export interface FilterOption {
|
||||
value: string | number | boolean;
|
||||
label: string;
|
||||
}
|
||||
|
||||
export interface FilterItem {
|
||||
type: "input" | "select" | "date-range";
|
||||
name: string;
|
||||
label?: string;
|
||||
placeholder?: string;
|
||||
options?: FilterOption[];
|
||||
format?: string;
|
||||
enableTimePicker?: boolean;
|
||||
autoApply?: boolean;
|
||||
}
|
||||
|
||||
type FilterValues = Record<
|
||||
string,
|
||||
string | number | boolean | Date[] | undefined
|
||||
>;
|
||||
|
||||
const props = defineProps<{
|
||||
filters: FilterItem[];
|
||||
initialValues?: FilterValues;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
search: [values: FilterValues];
|
||||
"update:values": [values: FilterValues];
|
||||
}>();
|
||||
|
||||
// 初始化筛选值
|
||||
const filterValues = reactive<FilterValues>({});
|
||||
|
||||
// 初始化默认值
|
||||
onMounted(() => {
|
||||
// 初始化所有筛选项的默认值
|
||||
for (const filter of props.filters) {
|
||||
if (props.initialValues && props.initialValues[filter.name] !== undefined) {
|
||||
filterValues[filter.name] = props.initialValues[filter.name];
|
||||
} else {
|
||||
// 设置默认值
|
||||
switch (filter.type) {
|
||||
case "input":
|
||||
filterValues[filter.name] = "";
|
||||
break;
|
||||
case "select":
|
||||
filterValues[filter.name] =
|
||||
filter.options && filter.options.length > 0
|
||||
? filter.options[0].value
|
||||
: "";
|
||||
break;
|
||||
case "date-range":
|
||||
filterValues[filter.name] = undefined;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 监听筛选值变化
|
||||
watch(
|
||||
filterValues,
|
||||
(newValues) => {
|
||||
emit("update:values", { ...newValues });
|
||||
},
|
||||
{ deep: true },
|
||||
);
|
||||
|
||||
// 处理搜索
|
||||
const handleSearch = () => {
|
||||
emit("search", { ...filterValues });
|
||||
};
|
||||
</script>
|
||||
@@ -4,60 +4,38 @@
|
||||
<Breadcrumbs :names="['用户管理', '角色分配']" :routes="[{ name: RouteName.USERVIEW }]" />
|
||||
<h1 class="text-xl sm:text-2xl mb-4 sm:mb-6 font-semibold text-gray-900">角色分配</h1>
|
||||
</div>
|
||||
<div class="flex flex-col sm:flex-row sm:justify-between sm:items-center mb-4 gap-y-3 sm:gap-y-0">
|
||||
<form class="w-full sm:w-auto flex flex-col xs:flex-row gap-2 xs:gap-3 items-stretch xs:items-center">
|
||||
<div class="flex-grow">
|
||||
<label for="default-search" class="mb-2 text-sm font-medium text-gray-900 sr-only">Search</label>
|
||||
<div class="relative">
|
||||
<div class="absolute inset-y-0 start-0 flex items-center ps-3 pointer-events-none">
|
||||
<svg class="w-4 h-4 text-gray-500" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none"
|
||||
viewBox="0 0 20 20">
|
||||
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="m19 19-4-4m0-7A7 7 0 1 1 1 8a7 7 0 0 1 14 0Z" />
|
||||
</svg>
|
||||
</div>
|
||||
<input type="search" id="default-search" v-model="roleName"
|
||||
class="block w-full p-2.5 ps-10 text-sm text-gray-900 border border-gray-300 rounded-lg bg-gray-50 focus:ring-blue-500 focus:border-blue-500"
|
||||
placeholder="角色名" required />
|
||||
</div>
|
||||
|
||||
<TableFilterForm :filters="filterConfig" :initialValues="filterValues" @search="handleSearch"
|
||||
@update:values="updateFilterValues">
|
||||
<template #actions>
|
||||
<div class="flex gap-x-2">
|
||||
<TableButton variant="primary" @click="() => {
|
||||
if (checkedRoleIds.length === 0) {
|
||||
alertStore.showAlert({
|
||||
content: '没有选择角色',
|
||||
level: 'error',
|
||||
});
|
||||
} else {
|
||||
roleBindModal?.show();
|
||||
}
|
||||
}">
|
||||
绑定
|
||||
</TableButton>
|
||||
<TableButton variant="danger" @click="() => {
|
||||
if (checkedRoleIds.length === 0) {
|
||||
alertStore.showAlert({
|
||||
content: '没有选择角色',
|
||||
level: 'error',
|
||||
});
|
||||
} else {
|
||||
roleUnbindModal?.show();
|
||||
}
|
||||
}">
|
||||
解绑
|
||||
</TableButton>
|
||||
</div>
|
||||
<select id="countries" v-model="bindState"
|
||||
class="w-full xs:w-auto bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block p-2.5">
|
||||
<option value="BIND">已绑定</option>
|
||||
<option value="UNBIND">未绑定</option>
|
||||
<option value="ALL">全部</option>
|
||||
</select>
|
||||
<button type="submit"
|
||||
class="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-4 py-2.5"
|
||||
@click.prevent="handleSearch">搜索</button>
|
||||
</form>
|
||||
<div class="flex gap-x-2">
|
||||
<TableButton variant="primary" @click="() => {
|
||||
if (checkedRoleIds.length === 0) {
|
||||
alertStore.showAlert({
|
||||
content: '没有选择角色',
|
||||
level: 'error',
|
||||
});
|
||||
} else {
|
||||
roleBindModal?.show();
|
||||
}
|
||||
}">
|
||||
绑定
|
||||
</TableButton>
|
||||
<TableButton variant="danger" @click="() => {
|
||||
if (checkedRoleIds.length === 0) {
|
||||
alertStore.showAlert({
|
||||
content: '没有选择角色',
|
||||
level: 'error',
|
||||
});
|
||||
} else {
|
||||
roleUnbindModal?.show();
|
||||
}
|
||||
}">
|
||||
解绑
|
||||
</TableButton>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</TableFilterForm>
|
||||
|
||||
<!-- 移动端卡片布局 -->
|
||||
<div class="md:hidden space-y-4">
|
||||
@@ -117,26 +95,60 @@ import MobileCardListWithCheckbox from "@/components/MobileCardListWithCheckbox.
|
||||
import BindModal from "@/components/PopupModal.vue";
|
||||
import UnModal from "@/components/PopupModal.vue";
|
||||
import TableButton from "@/components/TableButton.vue";
|
||||
import TableFilterForm from "@/components/TableFilterForm.vue";
|
||||
import type { FilterItem } from "@/components/TableFilterForm.vue";
|
||||
import TableFormLayout from "@/components/TableFormLayout.vue";
|
||||
import TablePagination from "@/components/TablePagination.vue";
|
||||
import { useRolesQuery } from "@/composables/role/useRolesQuery";
|
||||
import { useActionExcStore } from "@/composables/store/useActionExcStore";
|
||||
import { RouteName } from "@/router/constants";
|
||||
import { Modal, type ModalInterface, initFlowbite } from "flowbite";
|
||||
import { onMounted, ref, watch } from "vue";
|
||||
import { onMounted, reactive, ref } from "vue";
|
||||
import { useRoute } from "vue-router";
|
||||
import { useRoleBind } from "../composables/role/useRoleBind";
|
||||
import useAlertStore from "../composables/store/useAlertStore";
|
||||
import { useActionExcStore } from "@/composables/store/useActionExcStore";
|
||||
import VueDatePicker from "@vuepic/vue-datepicker";
|
||||
import "@vuepic/vue-datepicker/dist/main.css";
|
||||
|
||||
const roleName = ref<string>("");
|
||||
const filterConfig: FilterItem[] = [
|
||||
{
|
||||
type: "input",
|
||||
name: "roleName",
|
||||
placeholder: "角色名",
|
||||
},
|
||||
{
|
||||
type: "select",
|
||||
name: "bindState",
|
||||
options: [
|
||||
{ value: "BIND", label: "已绑定" },
|
||||
{ value: "UNBIND", label: "未绑定" },
|
||||
{ value: "ALL", label: "全部" },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const filterValues = reactive<{
|
||||
roleName: string;
|
||||
bindState: "BIND" | "ALL" | "UNBIND";
|
||||
}>({
|
||||
roleName: "",
|
||||
bindState: "ALL",
|
||||
});
|
||||
|
||||
const updateFilterValues = (
|
||||
values: Record<string, string | number | boolean | Date[] | undefined>,
|
||||
) => {
|
||||
if (values.roleName !== undefined) {
|
||||
filterValues.roleName = values.roleName as string;
|
||||
}
|
||||
if (values.bindState !== undefined) {
|
||||
filterValues.bindState = values.bindState as "BIND" | "ALL" | "UNBIND";
|
||||
}
|
||||
};
|
||||
|
||||
const checkedRoleIds = ref<number[]>([]);
|
||||
const roleBindModal = ref<ModalInterface>();
|
||||
const roleUnbindModal = ref<ModalInterface>();
|
||||
const allChecked = ref<boolean>(false);
|
||||
const $route = useRoute();
|
||||
const bindState = ref<"BIND" | "ALL" | "UNBIND">("ALL");
|
||||
|
||||
const alertStore = useAlertStore();
|
||||
const { total, roles, fetchRolesWith } = useRolesQuery();
|
||||
@@ -162,9 +174,9 @@ const handleBindRoleSubmit = async () => {
|
||||
level: "success",
|
||||
});
|
||||
await fetchRolesWith({
|
||||
name: roleName.value,
|
||||
name: filterValues.roleName,
|
||||
userId: Number($route.params.userId),
|
||||
bindState: bindState.value,
|
||||
bindState: filterValues.bindState,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -178,17 +190,17 @@ const handleUnbindRoleSubmit = async () => {
|
||||
level: "success",
|
||||
});
|
||||
await fetchRolesWith({
|
||||
name: roleName.value,
|
||||
name: filterValues.roleName,
|
||||
userId: Number($route.params.userId),
|
||||
bindState: bindState.value,
|
||||
bindState: filterValues.bindState,
|
||||
});
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
await fetchRolesWith({
|
||||
name: roleName.value,
|
||||
name: filterValues.roleName,
|
||||
userId: Number($route.params.userId),
|
||||
bindState: bindState.value,
|
||||
bindState: filterValues.bindState,
|
||||
});
|
||||
initFlowbite();
|
||||
const $bindModalElement: HTMLElement | null =
|
||||
@@ -198,11 +210,9 @@ onMounted(async () => {
|
||||
}
|
||||
const $unbindModalElement: HTMLElement | null =
|
||||
document.querySelector("#role-unbind-modal");
|
||||
roleUnbindModal.value = new Modal(
|
||||
$unbindModalElement,
|
||||
{},
|
||||
{ id: "role-unbind-modal" },
|
||||
);
|
||||
if ($unbindModalElement) {
|
||||
roleUnbindModal.value = new Modal($unbindModalElement, {});
|
||||
}
|
||||
actionExcStore.setCallback((result) => {
|
||||
if (result) {
|
||||
handleSearch();
|
||||
@@ -210,37 +220,29 @@ onMounted(async () => {
|
||||
});
|
||||
});
|
||||
|
||||
const clearCheckedRoleIds = () => {
|
||||
checkedRoleIds.value = [];
|
||||
};
|
||||
|
||||
const handleSearch = async () => {
|
||||
await fetchRolesWith({
|
||||
name: roleName.value,
|
||||
name: filterValues.roleName,
|
||||
userId: Number($route.params.userId),
|
||||
bindState: bindState.value,
|
||||
bindState: filterValues.bindState,
|
||||
});
|
||||
};
|
||||
|
||||
const handlePageChange = async (page: number, pageSize: number) => {
|
||||
await fetchRolesWith(
|
||||
{
|
||||
name: roleName.value,
|
||||
name: filterValues.roleName,
|
||||
userId: Number($route.params.userId),
|
||||
bindState: bindState.value,
|
||||
bindState: filterValues.bindState,
|
||||
},
|
||||
page,
|
||||
pageSize,
|
||||
);
|
||||
};
|
||||
|
||||
watch(allChecked, () => {
|
||||
if (allChecked.value) {
|
||||
checkedRoleIds.value = roles.value?.map((r) => r.id!) ?? [];
|
||||
} else {
|
||||
checkedRoleIds.value = [];
|
||||
}
|
||||
});
|
||||
|
||||
const clearCheckedRoleIds = () => {
|
||||
checkedRoleIds.value = [];
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
|
||||
@@ -4,40 +4,21 @@
|
||||
<Breadcrumbs :names="['用户管理']" />
|
||||
<h1 class="text-xl font-semibold text-gray-900 sm:text-2xl">用户管理</h1>
|
||||
</div>
|
||||
<div class="flex flex-col sm:flex-row sm:justify-between sm:items-center mb-4 gap-y-3 sm:gap-y-0">
|
||||
<form class="w-full sm:w-auto flex flex-col xs:flex-row gap-2 xs:gap-3 items-stretch xs:items-center">
|
||||
<div class="flex-grow">
|
||||
<label for="default-search" class="mb-2 text-sm font-medium text-gray-900 sr-only">Search</label>
|
||||
<div class="relative">
|
||||
<div class="absolute inset-y-0 start-0 flex items-center ps-3 pointer-events-none">
|
||||
<svg class="w-4 h-4 text-gray-500" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none"
|
||||
viewBox="0 0 20 20">
|
||||
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="m19 19-4-4m0-7A7 7 0 1 1 1 8a7 7 0 0 1 14 0Z" />
|
||||
</svg>
|
||||
</div>
|
||||
<input type="search" id="default-search" v-model="username"
|
||||
class="block w-full p-2.5 ps-10 text-sm text-gray-900 border border-gray-300 rounded-lg bg-gray-50 focus:ring-blue-500 focus:border-blue-500"
|
||||
placeholder="用户名" required />
|
||||
</div>
|
||||
</div>
|
||||
<VueDatePicker v-model="dateRange" locale="zh-CN" range format="yyyy/MM/dd HH:mm:ss - yyy/MM/dd HH:mm:ss">
|
||||
</VueDatePicker>
|
||||
<button type="submit"
|
||||
class="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-4 py-2.5"
|
||||
@click.prevent="handleSearch">搜索</button>
|
||||
</form>
|
||||
<!-- Create Modal toggle -->
|
||||
<Button :handleClick="() => handleUpsertUserClick(undefined)" :isLoading="false" :abortable="false"
|
||||
submitContent="新增用户" class="w-full sm:w-auto">
|
||||
<template #icon>
|
||||
<svg class="w-4 h-4 me-2" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none"
|
||||
viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
|
||||
</svg>
|
||||
</template>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<TableFilterForm :filters="filterConfig" :initialValues="filterValues" @search="handleSearch"
|
||||
@update:values="updateFilterValues">
|
||||
<template #actions>
|
||||
<Button :handleClick="() => handleUpsertUserClick(undefined)" :isLoading="false" :abortable="false"
|
||||
submitContent="新增用户" class="w-full sm:w-auto">
|
||||
<template #icon>
|
||||
<svg class="w-4 h-4 me-2" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none"
|
||||
viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
|
||||
</svg>
|
||||
</template>
|
||||
</Button>
|
||||
</template>
|
||||
</TableFilterForm>
|
||||
|
||||
<!-- 移动端卡片布局 -->
|
||||
<div class="md:hidden space-y-4">
|
||||
@@ -178,25 +159,58 @@ import Breadcrumbs from "@/components/Breadcrumbs.vue";
|
||||
import Button from "@/components/Button.vue";
|
||||
import UserDeleteModal from "@/components/PopupModal.vue";
|
||||
import SortIcon from "@/components/SortIcon.vue";
|
||||
import TableFilterForm, {
|
||||
type FilterItem,
|
||||
} from "@/components/TableFilterForm.vue";
|
||||
import TableFormLayout from "@/components/TableFormLayout.vue";
|
||||
import TablePagination from "@/components/TablePagination.vue";
|
||||
import UserUpsertModal from "@/components/UserUpsertModal.vue";
|
||||
import { useSort } from "@/composables/sort";
|
||||
import { useActionExcStore } from "@/composables/store/useActionExcStore";
|
||||
import useUserDelete from "@/composables/user/useUserDelete";
|
||||
import { useUserQuery } from "@/composables/user/useUserQuery";
|
||||
import { RouteName } from "@/router/constants";
|
||||
import type { UserUpsertSubmitModel } from "@/types/user";
|
||||
import { dayjs, formatDate } from "@/utils/dateUtil";
|
||||
import { Modal, type ModalInterface, initFlowbite } from "flowbite";
|
||||
import { nextTick, onMounted, ref } from "vue";
|
||||
import { nextTick, onMounted, reactive, ref } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import type { components } from "../api/types/schema";
|
||||
import useAlertStore from "../composables/store/useAlertStore";
|
||||
import { useUserUpsert } from "../composables/user/useUserUpsert";
|
||||
import { useActionExcStore } from "@/composables/store/useActionExcStore";
|
||||
|
||||
const dateRange = ref<Date[]>();
|
||||
const username = ref<string>("");
|
||||
const filterConfig: FilterItem[] = [
|
||||
{
|
||||
type: "input",
|
||||
name: "username",
|
||||
placeholder: "用户名",
|
||||
},
|
||||
{
|
||||
type: "date-range",
|
||||
name: "dateRange",
|
||||
format: "yyyy/MM/dd HH:mm:ss - yyy/MM/dd HH:mm:ss",
|
||||
},
|
||||
];
|
||||
|
||||
const filterValues = reactive<{
|
||||
username: string;
|
||||
dateRange?: Date[];
|
||||
}>({
|
||||
username: "",
|
||||
dateRange: undefined,
|
||||
});
|
||||
|
||||
const updateFilterValues = (
|
||||
values: Record<string, string | number | boolean | Date[] | undefined>,
|
||||
) => {
|
||||
if (values.username !== undefined) {
|
||||
filterValues.username = values.username as string;
|
||||
}
|
||||
if (values.dateRange !== undefined) {
|
||||
filterValues.dateRange = values.dateRange as Date[];
|
||||
}
|
||||
};
|
||||
|
||||
const selectedUser = ref<components["schemas"]["UserRolePermissionDto"]>();
|
||||
const userUpsertModal = ref<ModalInterface>();
|
||||
const userDeleteModal = ref<ModalInterface>();
|
||||
@@ -218,7 +232,7 @@ const columns = [
|
||||
|
||||
onMounted(async () => {
|
||||
await fetchUsersWith({
|
||||
username: username.value,
|
||||
username: filterValues.username,
|
||||
});
|
||||
initFlowbite();
|
||||
const $upsertModalElement: HTMLElement | null =
|
||||
@@ -247,9 +261,9 @@ const handleUpsertUserSubmit = async (data: UserUpsertSubmitModel) => {
|
||||
});
|
||||
await fetchUsersWith(
|
||||
{
|
||||
username: username.value,
|
||||
startDate: formatDate(dateRange?.value?.[0]),
|
||||
endDate: formatDate(dateRange?.value?.[1]),
|
||||
username: filterValues.username,
|
||||
startDate: formatDate(filterValues.dateRange?.[0]),
|
||||
endDate: formatDate(filterValues.dateRange?.[1]),
|
||||
},
|
||||
1,
|
||||
10,
|
||||
@@ -303,9 +317,9 @@ const handleSortClick = async (field: string) => {
|
||||
handleSort(field);
|
||||
await fetchUsersWith(
|
||||
{
|
||||
username: username.value,
|
||||
startDate: formatDate(dateRange?.value?.[0]),
|
||||
endDate: formatDate(dateRange?.value?.[1]),
|
||||
username: filterValues.username,
|
||||
startDate: formatDate(filterValues.dateRange?.[0]),
|
||||
endDate: formatDate(filterValues.dateRange?.[1]),
|
||||
},
|
||||
1,
|
||||
10,
|
||||
@@ -323,9 +337,9 @@ const handleDeleteUserSubmit = async () => {
|
||||
});
|
||||
await fetchUsersWith(
|
||||
{
|
||||
username: username.value,
|
||||
startDate: formatDate(dateRange?.value?.[0]),
|
||||
endDate: formatDate(dateRange?.value?.[1]),
|
||||
username: filterValues.username,
|
||||
startDate: formatDate(filterValues.dateRange?.[0]),
|
||||
endDate: formatDate(filterValues.dateRange?.[1]),
|
||||
},
|
||||
1,
|
||||
10,
|
||||
@@ -345,9 +359,9 @@ const handleDeleteUserClick = async (
|
||||
const handleSearch = async () => {
|
||||
await fetchUsersWith(
|
||||
{
|
||||
username: username.value,
|
||||
startDate: formatDate(dateRange?.value?.[0]),
|
||||
endDate: formatDate(dateRange?.value?.[1]),
|
||||
username: filterValues.username,
|
||||
startDate: formatDate(filterValues.dateRange?.[0]),
|
||||
endDate: formatDate(filterValues.dateRange?.[1]),
|
||||
},
|
||||
1,
|
||||
10,
|
||||
@@ -358,9 +372,9 @@ const handleSearch = async () => {
|
||||
const handlePageChange = async (page: number, pageSize: number) => {
|
||||
await fetchUsersWith(
|
||||
{
|
||||
username: username.value,
|
||||
startDate: formatDate(dateRange?.value?.[0]),
|
||||
endDate: formatDate(dateRange?.value?.[1]),
|
||||
username: filterValues.username,
|
||||
startDate: formatDate(filterValues.dateRange?.[0]),
|
||||
endDate: formatDate(filterValues.dateRange?.[1]),
|
||||
},
|
||||
page,
|
||||
pageSize,
|
||||
|
||||
Reference in New Issue
Block a user