feat: add search panel
This commit is contained in:
4
frontend/superadmin/dist/index.html
vendored
4
frontend/superadmin/dist/index.html
vendored
@@ -7,8 +7,8 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Sakai Vue</title>
|
<title>Sakai Vue</title>
|
||||||
<link href="https://fonts.cdnfonts.com/css/lato" rel="stylesheet">
|
<link href="https://fonts.cdnfonts.com/css/lato" rel="stylesheet">
|
||||||
<script type="module" crossorigin src="./assets/index-CurkpZlu.js"></script>
|
<script type="module" crossorigin src="./assets/index-PcBKlZrK.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="./assets/index-BK0XSEUV.css">
|
<link rel="stylesheet" crossorigin href="./assets/index-BDK8BdlV.css">
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
24
frontend/superadmin/src/components/SearchField.vue
Normal file
24
frontend/superadmin/src/components/SearchField.vue
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
<script setup>
|
||||||
|
const props = defineProps({
|
||||||
|
label: { type: String, required: true },
|
||||||
|
forId: { type: String, default: '' },
|
||||||
|
colClass: { type: String, default: 'col-span-12 md:col-span-6 lg:col-span-4' },
|
||||||
|
labelClass: { type: String, default: 'w-28 text-right' }
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div :class="props.colClass">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<label v-if="props.forId" :for="props.forId"
|
||||||
|
class="text-sm font-medium text-surface-900 dark:text-surface-0" :class="props.labelClass">
|
||||||
|
{{ props.label }}
|
||||||
|
</label>
|
||||||
|
<span v-else class="text-sm font-medium text-surface-900 dark:text-surface-0" :class="props.labelClass">{{
|
||||||
|
props.label }}</span>
|
||||||
|
<div class="flex-1 min-w-0">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
44
frontend/superadmin/src/components/SearchPanel.vue
Normal file
44
frontend/superadmin/src/components/SearchPanel.vue
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
<script setup>
|
||||||
|
import { Comment, computed, ref, useSlots } from 'vue';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
collapsedCount: { type: Number, default: 3 },
|
||||||
|
initialExpanded: { type: Boolean, default: false },
|
||||||
|
loading: { type: Boolean, default: false }
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['search', 'reset']);
|
||||||
|
|
||||||
|
const slots = useSlots();
|
||||||
|
const expanded = ref(props.initialExpanded);
|
||||||
|
|
||||||
|
const fieldNodes = computed(() => {
|
||||||
|
const nodes = slots.default ? slots.default() : [];
|
||||||
|
return (nodes || []).filter((n) => n && n.type !== Comment);
|
||||||
|
});
|
||||||
|
|
||||||
|
const canToggle = computed(() => fieldNodes.value.length > props.collapsedCount);
|
||||||
|
const visibleFields = computed(() => (expanded.value ? fieldNodes.value : fieldNodes.value.slice(0, props.collapsedCount)));
|
||||||
|
|
||||||
|
function onToggle() {
|
||||||
|
expanded.value = !expanded.value;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="mb-4 p-4 rounded border border-surface-200 dark:border-surface-700 bg-surface-0 dark:bg-surface-900">
|
||||||
|
<div class="flex flex-col lg:flex-row lg:items-end gap-4 lg:gap-6">
|
||||||
|
<div class="flex-1 grid grid-cols-12 gap-x-6 gap-y-4 items-end">
|
||||||
|
<template v-for="(node, idx) in visibleFields" :key="idx">
|
||||||
|
<component :is="node" />
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center justify-end gap-2 lg:flex-none">
|
||||||
|
<Button label="查询" icon="pi pi-search" :loading="props.loading" @click="emit('search')" />
|
||||||
|
<Button label="重置" icon="pi pi-refresh" severity="secondary" outlined :disabled="props.loading" @click="emit('reset')" />
|
||||||
|
<Button v-if="canToggle" :label="expanded ? '收起' : '展开'" :icon="expanded ? 'pi pi-angle-up' : 'pi pi-angle-down'" iconPos="right" severity="secondary" text :disabled="props.loading" @click="onToggle" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { TenantService } from '@/service/TenantService';
|
import { TenantService } from '@/service/TenantService';
|
||||||
|
import SearchField from '@/components/SearchField.vue';
|
||||||
|
import SearchPanel from '@/components/SearchPanel.vue';
|
||||||
import { useToast } from 'primevue/usetoast';
|
import { useToast } from 'primevue/usetoast';
|
||||||
import { onMounted, ref } from 'vue';
|
import { onMounted, ref } from 'vue';
|
||||||
|
|
||||||
@@ -198,21 +200,20 @@ onMounted(() => {
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="flex flex-wrap items-center justify-between gap-3 mb-4">
|
<div class="flex items-center justify-between mb-4">
|
||||||
<div class="flex items-center gap-2">
|
<h4 class="m-0">租户列表</h4>
|
||||||
<h4 class="m-0">租户列表</h4>
|
</div>
|
||||||
</div>
|
|
||||||
<div class="flex flex-wrap items-center gap-2">
|
<SearchPanel :loading="loading" @search="onSearch" @reset="onReset">
|
||||||
|
<SearchField label="名称 / Code">
|
||||||
<IconField>
|
<IconField>
|
||||||
<InputIcon>
|
<InputIcon>
|
||||||
<i class="pi pi-search" />
|
<i class="pi pi-search" />
|
||||||
</InputIcon>
|
</InputIcon>
|
||||||
<InputText v-model="keyword" placeholder="名称 / Code" @keyup.enter="onSearch" />
|
<InputText v-model="keyword" placeholder="请输入" class="w-full" @keyup.enter="onSearch" />
|
||||||
</IconField>
|
</IconField>
|
||||||
<Button label="查询" icon="pi pi-search" severity="secondary" @click="onSearch" />
|
</SearchField>
|
||||||
<Button label="重置" icon="pi pi-refresh" severity="secondary" @click="onReset" />
|
</SearchPanel>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<DataTable
|
<DataTable
|
||||||
:value="tenants"
|
:value="tenants"
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
|
import SearchField from '@/components/SearchField.vue';
|
||||||
|
import SearchPanel from '@/components/SearchPanel.vue';
|
||||||
import StatisticsStrip from '@/components/StatisticsStrip.vue';
|
import StatisticsStrip from '@/components/StatisticsStrip.vue';
|
||||||
import { UserService } from '@/service/UserService';
|
import { UserService } from '@/service/UserService';
|
||||||
import { useToast } from 'primevue/usetoast';
|
import { useToast } from 'primevue/usetoast';
|
||||||
@@ -202,48 +204,34 @@ onMounted(() => {
|
|||||||
<div>
|
<div>
|
||||||
<StatisticsStrip :items="statisticsItems" containerClass="card mb-4" />
|
<StatisticsStrip :items="statisticsItems" containerClass="card mb-4" />
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="flex flex-wrap items-center justify-between gap-3 mb-4">
|
<div class="flex items-center justify-between mb-4">
|
||||||
<div class="flex items-center gap-2">
|
<h4 class="m-0">用户列表</h4>
|
||||||
<h4 class="m-0">用户列表</h4>
|
</div>
|
||||||
</div>
|
|
||||||
<div class="flex flex-wrap items-center gap-2">
|
<SearchPanel :loading="loading" @search="onSearch" @reset="onReset">
|
||||||
|
<SearchField label="用户名">
|
||||||
<IconField>
|
<IconField>
|
||||||
<InputIcon>
|
<InputIcon>
|
||||||
<i class="pi pi-search" />
|
<i class="pi pi-search" />
|
||||||
</InputIcon>
|
</InputIcon>
|
||||||
<InputText v-model="username" placeholder="用户名" @keyup.enter="onSearch" />
|
<InputText v-model="username" placeholder="请输入" class="w-full" @keyup.enter="onSearch" />
|
||||||
</IconField>
|
</IconField>
|
||||||
<Button label="查询" icon="pi pi-search" severity="secondary" @click="onSearch" />
|
</SearchField>
|
||||||
<Button label="重置" icon="pi pi-refresh" severity="secondary" @click="onReset" />
|
</SearchPanel>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<DataTable
|
<DataTable :value="users" dataKey="id" :loading="loading" lazy :paginator="true" :rows="rows"
|
||||||
:value="users"
|
:totalRecords="totalRecords" :first="(page - 1) * rows" :rowsPerPageOptions="[10, 20, 50, 100]"
|
||||||
dataKey="id"
|
sortMode="single" :sortField="sortField" :sortOrder="sortOrder" @page="onPage" @sort="onSort"
|
||||||
:loading="loading"
|
|
||||||
lazy
|
|
||||||
:paginator="true"
|
|
||||||
:rows="rows"
|
|
||||||
:totalRecords="totalRecords"
|
|
||||||
:first="(page - 1) * rows"
|
|
||||||
:rowsPerPageOptions="[10, 20, 50, 100]"
|
|
||||||
sortMode="single"
|
|
||||||
:sortField="sortField"
|
|
||||||
:sortOrder="sortOrder"
|
|
||||||
@page="onPage"
|
|
||||||
@sort="onSort"
|
|
||||||
currentPageReportTemplate="显示第 {first} - {last} 条,共 {totalRecords} 条"
|
currentPageReportTemplate="显示第 {first} - {last} 条,共 {totalRecords} 条"
|
||||||
paginatorTemplate="FirstPageLink PrevPageLink PageLinks NextPageLink LastPageLink CurrentPageReport RowsPerPageDropdown"
|
paginatorTemplate="FirstPageLink PrevPageLink PageLinks NextPageLink LastPageLink CurrentPageReport RowsPerPageDropdown"
|
||||||
scrollable
|
scrollable scrollHeight="flex" responsiveLayout="scroll">
|
||||||
scrollHeight="flex"
|
|
||||||
responsiveLayout="scroll"
|
|
||||||
>
|
|
||||||
<Column field="id" header="ID" sortable style="min-width: 6rem" />
|
<Column field="id" header="ID" sortable style="min-width: 6rem" />
|
||||||
<Column field="username" header="用户名" sortable style="min-width: 14rem" />
|
<Column field="username" header="用户名" sortable style="min-width: 14rem" />
|
||||||
<Column field="status" header="状态" sortable style="min-width: 10rem">
|
<Column field="status" header="状态" sortable style="min-width: 10rem">
|
||||||
<template #body="{ data }">
|
<template #body="{ data }">
|
||||||
<Tag :value="data.status_description || data.status || '-'" :severity="getStatusSeverity(data.status)" class="cursor-pointer" @click="openStatusDialog(data)" />
|
<Tag :value="data.status_description || data.status || '-'"
|
||||||
|
:severity="getStatusSeverity(data.status)" class="cursor-pointer"
|
||||||
|
@click="openStatusDialog(data)" />
|
||||||
</template>
|
</template>
|
||||||
</Column>
|
</Column>
|
||||||
<Column field="roles" header="角色" style="min-width: 16rem">
|
<Column field="roles" header="角色" style="min-width: 16rem">
|
||||||
@@ -282,12 +270,15 @@ onMounted(() => {
|
|||||||
<div class="flex flex-col gap-4">
|
<div class="flex flex-col gap-4">
|
||||||
<div>
|
<div>
|
||||||
<label class="block font-medium mb-2">用户状态</label>
|
<label class="block font-medium mb-2">用户状态</label>
|
||||||
<Select v-model="statusValue" :options="statusOptions" optionLabel="label" optionValue="value" placeholder="选择状态" :disabled="statusLoading" fluid />
|
<Select v-model="statusValue" :options="statusOptions" optionLabel="label" optionValue="value"
|
||||||
|
placeholder="选择状态" :disabled="statusLoading" fluid />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<Button label="取消" icon="pi pi-times" text @click="statusDialogVisible = false" :disabled="statusLoading" />
|
<Button label="取消" icon="pi pi-times" text @click="statusDialogVisible = false"
|
||||||
<Button label="确认" icon="pi pi-check" @click="confirmUpdateStatus" :loading="statusLoading" :disabled="!statusValue" />
|
:disabled="statusLoading" />
|
||||||
|
<Button label="确认" icon="pi pi-check" @click="confirmUpdateStatus" :loading="statusLoading"
|
||||||
|
:disabled="!statusValue" />
|
||||||
</template>
|
</template>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user