Abstract Form Components

This commit is contained in:
Chuck1sn
2025-06-10 16:28:30 +08:00
parent 17200ec6d1
commit 24f379857a
16 changed files with 1366 additions and 1037 deletions

View File

@@ -1,14 +1,11 @@
<template>
<div class="space-y-4">
<div v-for="(item, index) in items ?? []" :key="index"
<div v-for="(item, index) in items ?? []" :key="getItemKey(item, index)"
class="p-4 bg-white rounded-lg shadow relative border border-gray-100">
<div class="flex justify-between items-start mb-3">
<!-- 标题区域 -->
<div class="flex items-center">
<slot name="checkbox" :item="item" v-if="hasCheckbox"></slot>
<div class="font-medium text-gray-900">
<slot name="title" :item="item"></slot>
</div>
<div class="font-medium text-gray-900">
<slot name="title" :item="item"></slot>
</div>
<!-- 状态区域 -->
<div>
@@ -35,8 +32,38 @@
</template>
<script setup generic="T" lang="ts">
defineProps<{
items: T[] | undefined;
hasCheckbox?: boolean;
import { ref } from 'vue';
/** 通用对象类型 */
type ItemRecord = Record<string, unknown>;
const props = defineProps<{
/** 数据项数组 */
items: T[] | undefined;
/** 数据项ID字段名 */
idField?: string;
/** 数据项唯一键字段名 */
keyField?: string;
}>();
/**
* 获取数据项的唯一键
* @param item 数据项
* @param index 索引
* @returns 唯一键
*/
const getItemKey = (item: T, index: number): string | number => {
if (props.keyField) {
const key = (item as ItemRecord)[props.keyField];
if (key !== undefined) return String(key);
}
if (props.idField) {
const id = (item as ItemRecord)[props.idField];
if (id !== undefined) return String(id);
}
const id = (item as ItemRecord).id;
return id !== undefined ? String(id) : index;
};
</script>

View File

@@ -0,0 +1,101 @@
<template>
<div class="space-y-4">
<div v-for="(item, index) in items ?? []" :key="getItemKey(item, index)"
class="p-4 bg-white rounded-lg shadow relative border border-gray-100">
<div class="flex justify-between items-start mb-3">
<!-- 标题区域 -->
<div class="flex items-center">
<input :id="'mobile-checkbox-' + getItemId(item)" :value="getItemId(item)" type="checkbox"
v-model="checkedItems"
class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded-sm focus:ring-blue-500 focus:ring-2 mr-3">
<div class="font-medium text-gray-900">
<slot name="title" :item="item"></slot>
</div>
</div>
<!-- 状态区域 -->
<div>
<slot name="status" :item="item"></slot>
</div>
</div>
<!-- 内容区域 -->
<div class="text-sm text-gray-600 mb-3 space-y-2">
<slot name="content" :item="item"></slot>
</div>
<!-- 标签/分类区域 -->
<div v-if="$slots.tags" class="flex flex-wrap gap-2 mb-3">
<slot name="tags" :item="item"></slot>
</div>
<!-- 操作按钮区域 -->
<div class="flex justify-between items-center mt-4">
<slot name="actions" :item="item"></slot>
</div>
</div>
</div>
</template>
<script setup generic="T" lang="ts">
import { ref, watch } from 'vue';
/** 通用对象类型 */
type ItemRecord = Record<string, unknown>;
const props = defineProps<{
/** 数据项数组 */
items: T[] | undefined;
/** 数据项ID字段名 */
idField?: string;
/** 数据项唯一键字段名 */
keyField?: string;
/** 选中项的值数组 */
modelValue?: (string | number)[];
}>();
const emit = defineEmits<{
'update:modelValue': [checkedItems: (string | number)[]];
}>();
const checkedItems = ref<(string | number)[]>(props.modelValue || []);
/**
* 获取数据项的唯一键
* @param item 数据项
* @param index 索引
* @returns 唯一键
*/
const getItemKey = (item: T, index: number): string | number => {
if (props.keyField) {
const key = (item as ItemRecord)[props.keyField];
if (key !== undefined) return String(key);
}
const id = getItemId(item);
return id !== undefined ? id : index;
};
/**
* 获取数据项的ID
* @param item 数据项
* @returns ID值
*/
const getItemId = (item: T): string | number => {
if (props.idField) {
return (item as ItemRecord)[props.idField] as string | number;
}
return (item as ItemRecord).id as string | number || item as unknown as string | number;
};
// 监听选中项变化
watch(checkedItems, (newVal) => {
emit('update:modelValue', newVal);
});
// 监听modelValue变化
watch(() => props.modelValue, (newVal) => {
if (newVal) {
checkedItems.value = newVal;
}
}, { deep: true });
</script>

View File

@@ -0,0 +1,235 @@
# 表单布局组件说明
本项目提供了四种表单布局组件和一个通用按钮组件,用于不同场景下的数据展示和交互:
## 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>
```

View File

@@ -0,0 +1,83 @@
<template>
<button :class="[
'flex items-center justify-center gap-x-1 whitespace-nowrap font-medium rounded-lg focus:ring-4 focus:outline-none',
sizeClasses,
colorClasses,
disabled ? 'opacity-50 cursor-not-allowed' : '',
className
]" :disabled="disabled" @click="handleClick" type="button">
<slot name="icon"></slot>
<span>
<slot></slot>
</span>
</button>
</template>
<script setup lang="ts">
import { computed } from 'vue';
export type ButtonVariant = 'primary' | 'secondary' | 'success' | 'danger' | 'warning' | 'info';
export type ButtonSize = 'xs' | 'sm' | 'md' | 'lg';
const props = defineProps<{
/** 按钮变体类型 */
variant?: ButtonVariant;
/** 按钮尺寸 */
size?: ButtonSize;
/** 是否禁用 */
disabled?: boolean;
/** 自定义CSS类名 */
className?: string;
/** 是否为移动端尺寸 */
isMobile?: boolean;
}>();
const emit = defineEmits<{
'click': [event: MouseEvent];
}>();
/** 按钮颜色样式映射 */
const colorClasses = computed(() => {
const variants: Record<ButtonVariant, string> = {
primary: 'text-white bg-blue-700 hover:bg-blue-800 focus:ring-blue-300',
secondary: 'text-gray-900 bg-white border border-gray-300 hover:bg-gray-100 focus:ring-gray-100',
success: 'text-white bg-green-700 hover:bg-green-800 focus:ring-green-300',
danger: 'text-white bg-red-700 hover:bg-red-800 focus:ring-red-300',
warning: 'text-gray-900 bg-yellow-400 hover:bg-yellow-500 focus:ring-yellow-300',
info: 'text-white bg-cyan-700 hover:bg-cyan-800 focus:ring-cyan-300'
};
return variants[props.variant || 'primary'];
});
/** 按钮尺寸样式映射 */
const sizeClasses = computed(() => {
// 移动端尺寸
if (props.isMobile) {
const sizes: Record<ButtonSize, string> = {
xs: 'text-xs px-2 py-1',
sm: 'text-xs px-3 py-1.5',
md: 'text-sm px-3 py-2',
lg: 'text-sm px-4 py-2.5'
};
return sizes[props.size || 'sm'];
}
// PC端尺寸
const sizes: Record<ButtonSize, string> = {
xs: 'text-xs px-3 py-1.5',
sm: 'text-sm px-3 py-2',
md: 'text-sm px-4 py-2.5',
lg: 'text-base px-5 py-3'
};
return sizes[props.size || 'md'];
});
/** 处理点击事件 */
const handleClick = (event: MouseEvent) => {
if (!props.disabled) {
emit('click', event);
}
};
</script>

View File

@@ -0,0 +1,188 @@
<template>
<div class="relative overflow-x-auto shadow-md sm:rounded-lg">
<table class="w-full text-sm text-left rtl:text-right text-gray-500">
<thead class="text-xs text-gray-700 uppercase bg-gray-50">
<tr>
<th v-if="hasCheckbox" scope="col" class="p-4 w-4">
<div class="flex items-center">
<input id="checkbox-all-search" type="checkbox" v-model="allChecked" @change="handleAllCheckedChange"
class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded-sm focus:ring-blue-500 focus:ring-2">
<label for="checkbox-all-search" class="sr-only">checkbox</label>
</div>
</th>
<th v-for="(column, index) in columns" :key="index" scope="col" :class="[
'px-6 py-3',
column.sortable ? 'cursor-pointer' : '',
column.class || ''
]" @click="column.sortable ? handleSortClick(column.field) : null">
<div class="flex items-center">
<span>{{ column.title }}</span>
<slot v-if="column.sortable" name="sort-icon" :field="column.field"></slot>
</div>
</th>
</tr>
</thead>
<tbody>
<tr v-for="(item, rowIndex) in items" :key="getItemKey(item, rowIndex)"
class="bg-white border-b border-gray-200 hover:bg-gray-50">
<td v-if="hasCheckbox" class="w-4 p-4">
<div class="flex items-center">
<input :id="`checkbox-table-search-${rowIndex}`" :value="getItemId(item)" type="checkbox"
v-model="checkedItems"
class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded-sm focus:ring-blue-500 focus:ring-2">
<label :for="`checkbox-table-search-${rowIndex}`" class="sr-only">checkbox</label>
</div>
</td>
<td v-for="(column, colIndex) in columns" :key="colIndex" :class="[
'px-6 py-4',
column.class || '',
colIndex === 0 ? 'font-medium text-gray-900' : ''
]">
<slot :name="column.field" :item="item" :index="rowIndex">
<div class="max-w-sm whitespace-nowrap overflow-hidden text-ellipsis">
{{ getItemValue(item, column.field) }}
</div>
</slot>
</td>
</tr>
</tbody>
</table>
</div>
</template>
<script setup generic="T" lang="ts">
import { defineEmits, ref, watch } from 'vue';
/**
* 表格列配置接口
*/
export interface Column {
/** 列标题 */
title: string;
/** 数据字段名 */
field: string;
/** 是否可排序 */
sortable?: boolean;
/** 自定义CSS类名 */
class?: string;
}
/** 通用对象类型 */
type ItemRecord = Record<string, unknown>;
const props = defineProps<{
/** 数据项数组 */
items: T[];
/** 列配置数组 */
columns: Column[];
/** 是否显示复选框 */
hasCheckbox?: boolean;
/** 数据项ID字段名 */
idField?: string;
/** 数据项唯一键字段名 */
keyField?: string;
/** 选中项的值数组用于v-model绑定 */
modelValue?: (string | number)[];
}>();
const emit = defineEmits<{
'update:modelValue': [checkedItems: (string | number)[]];
'sort': [field: string];
'all-checked-change': [checked: boolean];
}>();
const checkedItems = ref<(string | number)[]>(props.modelValue || []);
const allChecked = ref(false);
/**
* 获取数据项的唯一键
* @param item 数据项
* @param index 索引
* @returns 唯一键
*/
const getItemKey = (item: T, index: number): string | number => {
if (props.keyField) {
const key = (item as ItemRecord)[props.keyField];
if (key !== undefined) return String(key);
}
const id = getItemId(item);
return id !== undefined ? id : index;
};
/**
* 获取数据项的ID
* @param item 数据项
* @returns ID值
*/
const getItemId = (item: T): string | number => {
if (props.idField) {
return (item as ItemRecord)[props.idField] as string | number;
}
return (item as ItemRecord).id as string | number || item as unknown as string | number;
};
/**
* 获取数据项的字段值
* @param item 数据项
* @param field 字段名
* @returns 字段值
*/
const getItemValue = (item: T, field: string): string => {
if (!field) return '';
// 支持嵌套属性访问,如 "user.name"
return String(field.split('.').reduce<unknown>((obj, key) =>
obj && typeof obj === 'object' && key in (obj as Record<string, unknown>)
? (obj as Record<string, unknown>)[key]
: '',
item as ItemRecord));
};
/**
* 处理全选/取消全选
*/
const handleAllCheckedChange = () => {
if (allChecked.value) {
checkedItems.value = props.items.map(getItemId);
} else {
checkedItems.value = [];
}
emit('all-checked-change', allChecked.value);
emit('update:modelValue', checkedItems.value);
};
/**
* 处理排序点击
* @param field 排序字段
*/
const handleSortClick = (field: string) => {
emit('sort', field);
};
// 监听选中项变化
watch(checkedItems, (newVal) => {
emit('update:modelValue', newVal);
// 更新全选状态
if (props.items.length > 0) {
allChecked.value = newVal.length === props.items.length;
} else {
allChecked.value = false;
}
});
// 监听modelValue变化
watch(() => props.modelValue, (newVal) => {
if (newVal) {
checkedItems.value = newVal;
}
}, { deep: true });
// 监听items变化重置选中状态
watch(() => props.items, () => {
if (allChecked.value) {
checkedItems.value = props.items.map(getItemId);
emit('update:modelValue', checkedItems.value);
}
}, { deep: true });
</script>

View File

@@ -1,135 +1,93 @@
<template>
<div class="px-2 sm:px-4 pt-6 sm:rounded-lg">
<div class="mb-4 sm:mb-6 col-span-full">
<Breadcrumbs :names="['部门分配']" />
<h1 class="text-xl sm:text-2xl mb-4 sm:mb-6 font-semibold text-gray-900">部门分配</h1>
<div class="mb-4 col-span-full">
<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="departmentName"
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 />
<form class="w-full sm:max-w-xs">
<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="departmentName"
class="block w-full p-3 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 />
<button type="submit"
class="text-white absolute end-1.5 bottom-1.5 bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-3 py-1.5 sm:px-4 sm:py-2"
@click.prevent="handleSearch">搜索</button>
</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 items-center justify-end gap-2">
<button @click="() => {
if (checkedDepartmentIds.length === 0) {
alertStore.showAlert({
content: '没有选择部门',
level: 'error',
});
} else {
departmentBindModal?.show();
}
}"
class="flex items-center text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-xs sm:text-sm px-3 py-2 sm:px-4 sm:py-2.5 text-center"
type="button">
<TableButton variant="primary" @click="() => {
if (checkedDepartmentIds.length === 0) {
alertStore.showAlert({
content: '没有选择部门',
level: 'error',
});
} else {
departmentBindModal?.show();
}
}">
绑定
</button>
<button @click="() => {
if (checkedDepartmentIds.length === 0) {
alertStore.showAlert({
content: '没有选择部门',
level: 'error',
});
} else {
departmentUnbindModal?.show();
}
}"
class="flex items-center text-white bg-red-700 hover:bg-red-800 focus:ring-4 focus:outline-none focus:ring-red-300 font-medium rounded-lg text-xs sm:text-sm px-3 py-2 sm:px-4 sm:py-2.5 text-center"
type="button">
</TableButton>
<TableButton variant="danger" @click="() => {
if (checkedDepartmentIds.length === 0) {
alertStore.showAlert({
content: '没有选择部门',
level: 'error',
});
} else {
departmentUnbindModal?.show();
}
}">
解绑
</button>
</TableButton>
</div>
</div>
<!-- 移动端卡片布局 -->
<div class="md:hidden space-y-4">
<div v-for="department in departments" :key="department.id" class="p-4 bg-white rounded-lg shadow">
<div class="flex items-center justify-between mb-3">
<div class="md:hidden">
<MobileCardListWithCheckbox :items="departments || []" v-model="checkedDepartmentIds">
<template #title="{ item }">
{{ item.name }}
</template>
<template #status="{ item }">
<div class="flex items-center">
<input :id="'mobile-checkbox-' + department.id" :value="department.id" type="checkbox"
v-model="checkedDepartmentIds"
class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded-sm focus:ring-blue-500 focus:ring-2 mr-3">
<div class="font-medium text-gray-900">{{ department.name }}</div>
<div class="h-2.5 w-2.5 rounded-full me-2" :class="item.isBound ? 'bg-green-500' : 'bg-red-500'"></div>
<span class="text-sm">{{ item.isBound === true ? "已绑定" : "未绑定" }}</span>
</div>
<div class="flex items-center">
<div class="h-2.5 w-2.5 rounded-full me-2" :class="department.isBound ? 'bg-green-500' : 'bg-red-500'">
</div>
<span class="text-sm">{{ department.isBound === true ? "已绑定" : "未绑定" }}</span>
</template>
<template #content="{ item }">
<div>
<p class="text-xs font-medium text-gray-600">上级部门</p>
<p class="text-sm text-gray-900 mt-0.5">{{ !item.parentName ? '无' : item.parentName }}</p>
</div>
</div>
<div>
<p class="text-xs font-medium text-gray-600">上级部门</p>
<p class="text-sm text-gray-900 mt-0.5">{{ !department.parentName ? '无' : department.parentName }}</p>
</div>
</div>
</template>
</MobileCardListWithCheckbox>
</div>
<!-- PC端表格布局 -->
<div class="relative overflow-x-auto shadow-md sm:rounded-lg hidden md:block">
<table class="w-full text-sm text-left rtl:text-right text-gray-500">
<thead class="text-xs text-gray-700 uppercase bg-gray-50">
<tr>
<th scope="col" class="p-4 w-4">
<div class="flex items-center">
<input id="checkbox-all-search" type="checkbox" v-model="allChecked"
class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded-sm focus:ring-blue-500 focus:ring-2">
<label for="checkbox-all-search" class="sr-only">checkbox</label>
</div>
</th>
<th scope="col" class="px-4 py-3">上级部门</th>
<th scope="col" class="px-4 py-3">部门名称</th>
<th scope="col" class="px-4 py-3">绑定状态</th>
</tr>
</thead>
<tbody>
<tr v-for="department in departments" :key="department.id"
class="bg-white border-b border-gray-200 hover:bg-gray-50">
<td class="w-4 p-4">
<div class="flex items-center">
<input :id="'checkbox-table-search-' + department.id" :value="department.id" type="checkbox"
v-model="checkedDepartmentIds"
class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded-sm focus:ring-blue-500 focus:ring-2">
<label :for="'checkbox-table-search-' + department.id" class="sr-only">checkbox</label>
</div>
</td>
<td scope="row" class="px-4 py-3 whitespace-nowrap">
{{ !department.parentName ? '无' : department.parentName }}
</td>
<td scope="row" class="px-4 py-3 whitespace-nowrap font-medium text-gray-900">
{{ department.name }}
</td>
<td class="px-4 py-3 max-w-sm overflow-hidden text-ellipsis">
<div class="flex items-center">
<div class="h-2.5 w-2.5 rounded-full me-2" :class="department.isBound ? 'bg-green-500' : 'bg-red-500'">
</div> {{
department.isBound === true ? "已绑定" : "未绑定" }}
</div>
</td>
</tr>
</tbody>
</table>
<div class="hidden md:block">
<TableFormLayout :items="departments || []" :columns="columns" :hasCheckbox="true" v-model="checkedDepartmentIds"
@all-checked-change="allChecked = $event">
<template #parentName="{ item }">
{{ !item.parentName ? '无' : item.parentName }}
</template>
<template #name="{ item }">
{{ item.name }}
</template>
<template #bindState="{ item }">
<div class="flex items-center">
<div class="h-2.5 w-2.5 rounded-full me-2" :class="item.isBound ? 'bg-green-500' : 'bg-red-500'"></div>
{{ item.isBound === true ? "已绑定" : "未绑定" }}
</div>
</template>
</TableFormLayout>
</div>
<TablePagination :pageChange="handlePageChange" :total="total" />
<BindModal :id="'department-bind-modal'" :closeModal="() => {
@@ -144,8 +102,11 @@
<script setup lang="ts">
import Breadcrumbs from "@/components/Breadcrumbs.vue";
import MobileCardListWithCheckbox from "@/components/MobileCardListWithCheckbox.vue";
import BindModal from "@/components/PopupModal.vue";
import UnModal from "@/components/PopupModal.vue";
import TableButton from "@/components/TableButton.vue";
import TableFormLayout from "@/components/TableFormLayout.vue";
import TablePagination from "@/components/TablePagination.vue";
import { useDepartmentQuery } from "@/composables/department/useDepartmentQuery";
import { Modal, type ModalInterface, initFlowbite } from "flowbite";
@@ -168,6 +129,13 @@ const { total, departments, fetchDepartmentWith } = useDepartmentQuery();
const { bindDepartment, unbindDepartment } = useDepartmentBind();
// 定义表格列配置
const columns = [
{ title: '上级部门', field: 'parentName' },
{ title: '部门名称', field: 'name' },
{ title: '绑定状态', field: 'bindState' }
];
const handleBindDepartmentSubmit = async () => {
await bindDepartment(
Number($route.params.userId),
@@ -247,7 +215,7 @@ const handlePageChange = async (page: number, pageSize: number) => {
watch(allChecked, async () => {
if (allChecked.value) {
checkedDepartmentIds.value = departments.value?.map((d) => d.id!) ?? [];
checkedDepartmentIds.value = departments.value?.map((r) => r.id!) ?? [];
} else {
checkedDepartmentIds.value = [];
}

View File

@@ -1,113 +1,94 @@
<template>
<div class="px-2 sm:px-4 pt-6 sm:rounded-lg">
<div class="mb-4 sm:mb-6 col-span-full">
<Breadcrumbs :names="['绑定权限']" />
<h1 class="text-xl sm:text-2xl mb-4 sm:mb-6 font-semibold text-gray-900">
绑定权限</h1>
<div class="mb-4 col-span-full">
<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="permissionName"
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 />
<form class="w-full sm:max-w-xs">
<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="permissionName"
class="block w-full p-3 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 />
<button type="submit"
class="text-white absolute end-1.5 bottom-1.5 bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-3 py-1.5 sm:px-4 sm:py-2"
@click.prevent="handleSearch">搜索</button>
</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 items-center justify-end gap-2">
<button @click="() => {
if (checkedPermissionIds.length === 0) {
alertStore.showAlert({
content: '没有选择权限',
level: 'error',
});
} else {
permissionBindModal?.show();
}
}"
class="flex items-center text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-xs sm:text-sm px-3 py-2 sm:px-4 sm:py-2.5 text-center"
type="button">
<TableButton variant="primary" @click="() => {
if (checkedPermissionIds.length === 0) {
alertStore.showAlert({
content: '没有选择权限',
level: 'error',
});
} else {
permissionBindModal?.show();
}
}">
绑定
</button>
<button @click="() => {
if (checkedPermissionIds.length === 0) {
alertStore.showAlert({
content: '没有选择权限',
level: 'error',
});
} else {
permissionUnbindModal?.show();
}
}"
class="flex items-center text-white bg-red-700 hover:bg-red-800 focus:ring-4 focus:outline-none focus:ring-red-300 font-medium rounded-lg text-xs sm:text-sm px-3 py-2 sm:px-4 sm:py-2.5 text-center"
type="button">
</TableButton>
<TableButton variant="danger" @click="() => {
if (checkedPermissionIds.length === 0) {
alertStore.showAlert({
content: '没有选择权限',
level: 'error',
});
} else {
permissionUnbindModal?.show();
}
}">
解绑
</button>
</TableButton>
</div>
</div>
<div class="relative overflow-x-auto shadow-md sm:rounded-lg">
<table class="w-full text-sm text-left rtl:text-right text-gray-500">
<thead class="text-xs text-gray-700 uppercase bg-gray-50">
<tr>
<th scope="col" class="p-2 sm:p-4 w-4">
<div class="flex items-center">
<input id="checkbox-all-search" type="checkbox" v-model="allChecked"
class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded-sm focus:ring-blue-500 focus:ring-2">
<label for="checkbox-all-search" class="sr-only">checkbox</label>
</div>
</th>
<th scope="col" class="px-3 py-2 md:px-4 md:py-3 hidden md:table-cell">权限编码</th>
<th scope="col" class="px-3 py-2 md:px-4 md:py-3">权限名称</th>
<th scope="col" class="px-3 py-2 md:px-4 md:py-3">绑定状态</th>
</tr>
</thead>
<tbody>
<tr v-for="permission in permissions" :key="permission.id"
class="bg-white border-b border-gray-200 hover:bg-gray-50">
<td class="w-4 p-2 sm:p-4">
<div class="flex items-center">
<input :id="'checkbox-table-search-' + permission.id" :value="permission.id" type="checkbox"
v-model="checkedPermissionIds"
class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded-sm focus:ring-blue-500 focus:ring-2">
<label :for="'checkbox-table-search-' + permission.id" class="sr-only">checkbox</label>
</div>
</td>
<td scope="row"
class="px-3 py-2 md:px-4 md:py-3 font-medium text-gray-900 whitespace-nowrap hidden md:table-cell">
{{ permission.code }}
</td>
<td scope="row" class="px-3 py-2 md:px-4 md:py-3 font-medium text-gray-900 whitespace-nowrap">
{{ permission.name }}
</td>
<td class="px-3 py-2 md:px-4 md:py-3 max-w-xs sm:max-w-sm overflow-hidden text-ellipsis">
<div class="flex items-center">
<div class="h-2.5 w-2.5 rounded-full me-2" :class="permission.isBound ? 'bg-green-500' : 'bg-red-500'">
</div> {{
permission.isBound === true ? "已绑定" : "未绑定" }}
</div>
</td>
</tr>
</tbody>
</table>
<!-- 移动端卡片布局 -->
<div class="md:hidden">
<MobileCardListWithCheckbox :items="permissions || []" v-model="checkedPermissionIds">
<template #title="{ item }">
{{ item.name }}
</template>
<template #status="{ item }">
<div class="flex items-center">
<div class="h-2.5 w-2.5 rounded-full me-2" :class="item.isBound ? 'bg-green-500' : 'bg-red-500'"></div>
<span class="text-sm">{{ item.isBound === true ? "已绑定" : "未绑定" }}</span>
</div>
</template>
<template #content="{ item }">
<div>
<p class="text-xs font-medium text-gray-600">权限编码</p>
<p class="text-sm text-gray-900 mt-0.5">{{ item.code }}</p>
</div>
</template>
</MobileCardListWithCheckbox>
</div>
<!-- PC端表格布局 -->
<div class="hidden md:block">
<TableFormLayout :items="permissions || []" :columns="columns" :hasCheckbox="true" v-model="checkedPermissionIds"
@all-checked-change="allChecked = $event">
<template #code="{ item }">
{{ item.code }}
</template>
<template #name="{ item }">
{{ item.name }}
</template>
<template #bindState="{ item }">
<div class="flex items-center">
<div class="h-2.5 w-2.5 rounded-full me-2" :class="item.isBound ? 'bg-green-500' : 'bg-red-500'"></div>
{{ item.isBound === true ? "已绑定" : "未绑定" }}
</div>
</template>
</TableFormLayout>
</div>
<TablePagination :pageChange="handlePageChange" :total="total" />
@@ -119,81 +100,15 @@
<UnModal :id="'permission-unbind-modal'" :closeModal="() => {
permissionUnbindModal!.hide();
}" :onSubmit="handleUnbindPermissionSubmit" title="确定解绑选中的权限吗"></UnModal>
<!-- 移动端卡片布局 -->
<div class="md:hidden space-y-4">
<div v-for="permission in permissions" :key="permission.id" class="p-4 bg-white rounded-lg shadow">
<div class="flex items-center justify-between mb-3">
<div class="flex items-center">
<input :id="'mobile-checkbox-' + permission.id" :value="permission.id" type="checkbox"
v-model="checkedPermissionIds"
class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded-sm focus:ring-blue-500 focus:ring-2 mr-3">
<div class="font-medium text-gray-900">{{ permission.name }}</div>
</div>
<div class="flex items-center">
<div class="h-2.5 w-2.5 rounded-full me-2" :class="permission.isBound ? 'bg-green-500' : 'bg-red-500'"></div>
<span class="text-sm">{{ permission.isBound === true ? "已绑定" : "未绑定" }}</span>
</div>
</div>
<div>
<p class="text-xs font-medium text-gray-600">权限编码</p>
<p class="text-sm text-gray-900 mt-0.5">{{ permission.code }}</p>
</div>
</div>
</div>
<!-- PC端表格布局 -->
<div class="relative overflow-x-auto shadow-md sm:rounded-lg hidden md:block">
<table class="w-full text-sm text-left rtl:text-right text-gray-500">
<thead class="text-xs text-gray-700 uppercase bg-gray-50">
<tr>
<th scope="col" class="p-4 w-4">
<div class="flex items-center">
<input id="checkbox-all-search" type="checkbox" v-model="allChecked"
class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded-sm focus:ring-blue-500 focus:ring-2">
<label for="checkbox-all-search" class="sr-only">checkbox</label>
</div>
</th>
<th scope="col" class="px-4 py-3">权限编码</th>
<th scope="col" class="px-4 py-3">权限名称</th>
<th scope="col" class="px-4 py-3">绑定状态</th>
</tr>
</thead>
<tbody>
<tr v-for="permission in permissions" :key="permission.id"
class="bg-white border-b border-gray-200 hover:bg-gray-50">
<td class="w-4 p-4">
<div class="flex items-center">
<input :id="'checkbox-table-search-' + permission.id" :value="permission.id" type="checkbox"
v-model="checkedPermissionIds"
class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded-sm focus:ring-blue-500 focus:ring-2">
<label :for="'checkbox-table-search-' + permission.id" class="sr-only">checkbox</label>
</div>
</td>
<td scope="row" class="px-4 py-3 font-medium text-gray-900 whitespace-nowrap">
{{ permission.code }}
</td>
<td scope="row" class="px-4 py-3 font-medium text-gray-900 whitespace-nowrap">
{{ permission.name }}
</td>
<td class="px-4 py-3 max-w-sm overflow-hidden text-ellipsis">
<div class="flex items-center">
<div class="h-2.5 w-2.5 rounded-full me-2" :class="permission.isBound ? 'bg-green-500' : 'bg-red-500'">
</div> {{
permission.isBound === true ? "已绑定" : "未绑定" }}
</div>
</td>
</tr>
</tbody>
</table>
</div>
</template>
<script setup lang="ts">
import Breadcrumbs from "@/components/Breadcrumbs.vue";
import MobileCardListWithCheckbox from "@/components/MobileCardListWithCheckbox.vue";
import BindModal from "@/components/PopupModal.vue";
import UnModal from "@/components/PopupModal.vue";
import TableButton from "@/components/TableButton.vue";
import TableFormLayout from "@/components/TableFormLayout.vue";
import TablePagination from "@/components/TablePagination.vue";
import { useMobileStyles } from "@/composables/useMobileStyles";
import { RouteName } from "@/router/constants";
@@ -216,6 +131,13 @@ const alertStore = useAlertStore();
const { total, permissions, fetchPermissionsWith } = usePermissionsQuery();
const { bindPermission, unbindPermission } = usePermissionBind();
// 定义表格列配置
const columns = [
{ title: '权限编码', field: 'code' },
{ title: '权限名称', field: 'name' },
{ title: '绑定状态', field: 'bindState' }
];
const handleBindPermissionSubmit = async () => {
await bindPermission({
roleId: Number($route.params.roleId),

View File

@@ -1,126 +1,85 @@
<template>
<div class="px-2 sm:px-4 pt-6 sm:rounded-lg">
<div class="mb-4 sm:mb-6 col-span-full">
<Breadcrumbs :names="['岗位分配']" />
<h1 class="text-xl sm:text-2xl mb-4 sm:mb-6 font-semibold text-gray-900">岗位分配</h1>
<div class="mb-4 col-span-full">
<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="positionName"
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 />
<form class="w-full sm:max-w-xs">
<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="positionName"
class="block w-full p-3 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 />
<button type="submit"
class="text-white absolute end-1.5 bottom-1.5 bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-3 py-1.5 sm:px-4 sm:py-2"
@click.prevent="handleSearch">搜索</button>
</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 items-center justify-end gap-2">
<button @click="() => {
if (checkedPositionIds.length === 0) {
alertStore.showAlert({
content: '没有选择岗位',
level: 'error',
});
} else {
positionBindModal?.show();
}
}"
class="flex items-center text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-xs sm:text-sm px-3 py-2 sm:px-4 sm:py-2.5 text-center"
type="button">
<TableButton variant="primary" @click="() => {
if (checkedPositionIds.length === 0) {
alertStore.showAlert({
content: '没有选择岗位',
level: 'error',
});
} else {
positionBindModal?.show();
}
}">
绑定
</button>
<button @click="() => {
if (checkedPositionIds.length === 0) {
alertStore.showAlert({
content: '没有选择岗位',
level: 'error',
});
} else {
positionUnbindModal?.show();
}
}"
class="flex items-center text-white bg-red-700 hover:bg-red-800 focus:ring-4 focus:outline-none focus:ring-red-300 font-medium rounded-lg text-xs sm:text-sm px-3 py-2 sm:px-4 sm:py-2.5 text-center"
type="button">
</TableButton>
<TableButton variant="danger" @click="() => {
if (checkedPositionIds.length === 0) {
alertStore.showAlert({
content: '没有选择岗位',
level: 'error',
});
} else {
positionUnbindModal?.show();
}
}">
解绑
</button>
</TableButton>
</div>
</div>
<!-- 移动端卡片布局 -->
<div class="md:hidden space-y-4">
<div v-for="position in positions" :key="position.id" class="p-4 bg-white rounded-lg shadow">
<div class="flex items-center justify-between mb-3">
<div class="md:hidden">
<MobileCardListWithCheckbox :items="positions || []" v-model="checkedPositionIds">
<template #title="{ item }">
{{ item.name }}
</template>
<template #status="{ item }">
<div class="flex items-center">
<input :id="'mobile-checkbox-' + position.id" :value="position.id" type="checkbox"
v-model="checkedPositionIds"
class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded-sm focus:ring-blue-500 focus:ring-2 mr-3">
<div class="font-medium text-gray-900">{{ position.name }}</div>
<div class="h-2.5 w-2.5 rounded-full me-2" :class="item.isBound ? 'bg-green-500' : 'bg-red-500'"></div>
<span class="text-sm">{{ item.isBound === true ? "已绑定" : "未绑定" }}</span>
</div>
<div class="flex items-center">
<div class="h-2.5 w-2.5 rounded-full me-2" :class="position.isBound ? 'bg-green-500' : 'bg-red-500'"></div>
<span class="text-sm">{{ position.isBound === true ? "已绑定" : "未绑定" }}</span>
</div>
</div>
</div>
</template>
</MobileCardListWithCheckbox>
</div>
<!-- PC端表格布局 -->
<div class="relative overflow-x-auto shadow-md sm:rounded-lg hidden md:block">
<table class="w-full text-sm text-left rtl:text-right text-gray-500">
<thead class="text-xs text-gray-700 uppercase bg-gray-50">
<tr>
<th scope="col" class="p-4 w-4">
<div class="flex items-center">
<input id="checkbox-all-search" type="checkbox" v-model="allChecked"
class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded-sm focus:ring-blue-500 focus:ring-2">
<label for="checkbox-all-search" class="sr-only">checkbox</label>
</div>
</th>
<th scope="col" class="px-4 py-3">岗位名称</th>
<th scope="col" class="px-4 py-3">绑定状态</th>
</tr>
</thead>
<tbody>
<tr v-for="position in positions" :key="position.id"
class="bg-white border-b border-gray-200 hover:bg-gray-50">
<td class="w-4 p-4">
<div class="flex items-center">
<input :id="'checkbox-table-search-' + position.id" :value="position.id" type="checkbox"
v-model="checkedPositionIds"
class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded-sm focus:ring-blue-500 focus:ring-2">
<label :for="'checkbox-table-search-' + position.id" class="sr-only">checkbox</label>
</div>
</td>
<td scope="row" class="px-4 py-3 font-medium text-gray-900 whitespace-nowrap">
{{ position.name }}
</td>
<td class="px-4 py-3 max-w-sm overflow-hidden text-ellipsis">
<div class="flex items-center">
<div class="h-2.5 w-2.5 rounded-full me-2" :class="position.isBound ? 'bg-green-500' : 'bg-red-500'">
</div> {{
position.isBound === true ? "已绑定" : "未绑定" }}
</div>
</td>
</tr>
</tbody>
</table>
<div class="hidden md:block">
<TableFormLayout :items="positions || []" :columns="columns" :hasCheckbox="true" v-model="checkedPositionIds"
@all-checked-change="allChecked = $event">
<template #name="{ item }">
{{ item.name }}
</template>
<template #bindState="{ item }">
<div class="flex items-center">
<div class="h-2.5 w-2.5 rounded-full me-2" :class="item.isBound ? 'bg-green-500' : 'bg-red-500'"></div>
{{ item.isBound === true ? "已绑定" : "未绑定" }}
</div>
</template>
</TableFormLayout>
</div>
<TablePagination :pageChange="handlePageChange" :total="total" />
@@ -136,8 +95,11 @@
<script setup lang="ts">
import Breadcrumbs from "@/components/Breadcrumbs.vue";
import MobileCardListWithCheckbox from "@/components/MobileCardListWithCheckbox.vue";
import BindModal from "@/components/PopupModal.vue";
import UnModal from "@/components/PopupModal.vue";
import TableButton from "@/components/TableButton.vue";
import TableFormLayout from "@/components/TableFormLayout.vue";
import TablePagination from "@/components/TablePagination.vue";
import { usePositionBind } from "@/composables/position/usePositionBind";
import { usePositionQuery } from "@/composables/position/usePositionQuery";
@@ -162,6 +124,12 @@ const { total, positions, fetchPositionWith } = usePositionQuery();
const { bindPosition, unbindPosition } = usePositionBind();
// 定义表格列配置
const columns = [
{ title: '岗位名称', field: 'name' },
{ title: '绑定状态', field: 'bindState' }
];
const handleBindPositionSubmit = async () => {
await bindPosition(Number($route.params.userId), checkedPositionIds.value);
positionBindModal.value?.hide();

View File

@@ -31,103 +31,73 @@
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 items-center justify-end gap-2">
<button @click="() => {
if (checkedRoleIds.length === 0) {
alertStore.showAlert({
content: '没有选择角色',
level: 'error',
});
} else {
roleBindModal?.show();
}
}"
class="flex items-center 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 text-center"
type="button">
<div class="flex gap-x-2">
<TableButton variant="primary" @click="() => {
if (checkedRoleIds.length === 0) {
alertStore.showAlert({
content: '没有选择角色',
level: 'error',
});
} else {
roleBindModal?.show();
}
}">
绑定
</button>
<button @click="() => {
if (checkedRoleIds.length === 0) {
alertStore.showAlert({
content: '没有选择角色',
level: 'error',
});
} else {
roleUnbindModal?.show();
}
}"
class="flex items-center text-white bg-red-700 hover:bg-red-800 focus:ring-4 focus:outline-none focus:ring-red-300 font-medium rounded-lg text-sm px-4 py-2.5 text-center"
type="button">
</TableButton>
<TableButton variant="danger" @click="() => {
if (checkedRoleIds.length === 0) {
alertStore.showAlert({
content: '没有选择角色',
level: 'error',
});
} else {
roleUnbindModal?.show();
}
}">
解绑
</button>
</TableButton>
</div>
</div>
<!-- 移动端卡片布局 -->
<div class="md:hidden space-y-4">
<div v-for="role in roles" :key="role.id" class="p-4 bg-white rounded-lg shadow">
<div class="flex items-center justify-between mb-3">
<MobileCardListWithCheckbox :items="roles" v-model="checkedRoleIds">
<template #title="{ item }">
{{ item.name }}
</template>
<template #status="{ item }">
<div class="flex items-center">
<input :id="'mobile-checkbox-' + role.id" :value="role.id" type="checkbox" v-model="checkedRoleIds"
class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded-sm focus:ring-blue-500 focus:ring-2 mr-3">
<div class="font-medium text-gray-900">{{ role.name }}</div>
<div class="h-2.5 w-2.5 rounded-full me-2" :class="item.isBound ? 'bg-green-500' : 'bg-red-500'"></div>
<span class="text-sm">{{ item.isBound === true ? "已绑定" : "未绑定" }}</span>
</div>
<div class="flex items-center">
<div class="h-2.5 w-2.5 rounded-full me-2" :class="role.isBound ? 'bg-green-500' : 'bg-red-500'"></div>
<span class="text-sm">{{ role.isBound === true ? "已绑定" : "未绑定" }}</span>
</template>
<template #content="{ item }">
<div>
<p class="text-xs font-medium text-gray-600">角色编码</p>
<p class="text-sm text-gray-900 mt-0.5">{{ item.code }}</p>
</div>
</div>
<div>
<p class="text-xs font-medium text-gray-600">角色编码</p>
<p class="text-sm text-gray-900 mt-0.5">{{ role.code }}</p>
</div>
</div>
</template>
</MobileCardListWithCheckbox>
</div>
<!-- PC端表格布局 -->
<div class="relative overflow-x-auto shadow-md sm:rounded-lg hidden md:block">
<table class="w-full text-sm text-left rtl:text-right text-gray-500">
<thead class="text-xs text-gray-700 uppercase bg-gray-50">
<tr>
<th scope="col" class="p-4 w-4">
<div class="flex items-center">
<input id="checkbox-all-search" type="checkbox" v-model="allChecked"
class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded-sm focus:ring-blue-500 focus:ring-2">
<label for="checkbox-all-search" class="sr-only">checkbox</label>
</div>
</th>
<th scope="col" class="px-4 py-3">角色编码</th>
<th scope="col" class="px-4 py-3">角色名称</th>
<th scope="col" class="px-4 py-3">绑定状态</th>
</tr>
</thead>
<tbody>
<tr v-for="role in roles" :key="role.id" class="bg-white border-b border-gray-200 hover:bg-gray-50">
<td class="w-4 p-4">
<div class="flex items-center">
<input :id="'checkbox-table-search-' + role.id" :value="role.id" type="checkbox"
v-model="checkedRoleIds"
class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded-sm focus:ring-blue-500 focus:ring-2">
<label :for="'checkbox-table-search-' + role.id" class="sr-only">checkbox</label>
</div>
</td>
<td scope="row" class="px-4 py-3 font-medium text-gray-900 whitespace-nowrap">
{{ role.code }}
</td>
<td scope="row" class="px-4 py-3 whitespace-nowrap">
{{ role.name }}
</td>
<td class="px-4 py-3 max-w-xs sm:max-w-sm overflow-hidden text-ellipsis">
<div class="flex items-center">
<div class="h-2.5 w-2.5 rounded-full me-2" :class="role.isBound ? 'bg-green-500' : 'bg-red-500'">
</div> {{
role.isBound === true ? "已绑定" : "未绑定" }}
</div>
</td>
</tr>
</tbody>
</table>
<div class="hidden md:block">
<TableFormLayout :items="roles || []" :columns="columns" :hasCheckbox="true" v-model="checkedRoleIds"
@all-checked-change="allChecked = $event">
<template #code="{ item }">
{{ item.code }}
</template>
<template #name="{ item }">
{{ item.name }}
</template>
<template #bindState="{ item }">
<div class="flex items-center">
<div class="h-2.5 w-2.5 rounded-full me-2" :class="item.isBound ? 'bg-green-500' : 'bg-red-500'">
</div>
{{ item.isBound === true ? "已绑定" : "未绑定" }}
</div>
</template>
</TableFormLayout>
</div>
<TablePagination :pageChange="handlePageChange" :total="total" />
@@ -143,8 +113,11 @@
<script setup lang="ts">
import Breadcrumbs from "@/components/Breadcrumbs.vue";
import MobileCardListWithCheckbox from "@/components/MobileCardListWithCheckbox.vue";
import BindModal from "@/components/PopupModal.vue";
import UnModal from "@/components/PopupModal.vue";
import TableButton from "@/components/TableButton.vue";
import TableFormLayout from "@/components/TableFormLayout.vue";
import TablePagination from "@/components/TablePagination.vue";
import { useRolesQuery } from "@/composables/role/useRolesQuery";
import { useMobileStyles } from "@/composables/useMobileStyles";
@@ -168,6 +141,13 @@ const alertStore = useAlertStore();
const { total, roles, fetchRolesWith } = useRolesQuery();
const { bindRole, unbindRole } = useRoleBind();
// 定义表格列配置
const columns = [
{ title: '角色编码', field: 'code' },
{ title: '角色名称', field: 'name' },
{ title: '绑定状态', field: 'bindState' }
];
const handleBindRoleSubmit = async () => {
await bindRole({
userId: Number($route.params.userId),

View File

@@ -48,93 +48,67 @@
</template>
<template #actions="{ item }">
<div class="flex gap-x-2">
<button @click="handleUpsertDepartmentClick(item)"
class="flex items-center justify-center gap-x-1 text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-xs px-3 py-1.5"
type="button">
<svg class="w-3 h-3" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
<path d="M17.414 2.586a2 2 0 00-2.828 0L7 10.172V13h2.828l7.586-7.586a2 2 0 000-2.828z"></path>
<path fill-rule="evenodd"
d="M2 6a2 2 0 012-2h4a1 1 0 010 2H4v10h10v-4a1 1 0 112 0v4a2 2 0 01-2 2H4a2 2 0 01-2-2V6z"
clip-rule="evenodd"></path>
</svg>
<span>编辑</span>
</button>
<button
class="flex items-center justify-center gap-x-1 bg-red-700 hover:bg-red-800 focus:outline-none focus:ring-red-300 text-white focus:ring-4 font-medium rounded-lg text-xs px-3 py-1.5"
@click="handleDeleteDepartmentClick(item)" type="button">
<svg class="w-3 h-3" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd"
d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z"
clip-rule="evenodd"></path>
</svg>
<span>删除</span>
</button>
<TableButton variant="primary" size="xs" isMobile @click="handleUpsertDepartmentClick(item)">
<template #icon>
<svg class="w-3 h-3" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
<path d="M17.414 2.586a2 2 0 00-2.828 0L7 10.172V13h2.828l7.586-7.586a2 2 0 000-2.828z"></path>
<path fill-rule="evenodd"
d="M2 6a2 2 0 012-2h4a1 1 0 010 2H4v10h10v-4a1 1 0 112 0v4a2 2 0 01-2 2H4a2 2 0 01-2-2V6z"
clip-rule="evenodd"></path>
</svg>
</template>
编辑
</TableButton>
<TableButton variant="danger" size="xs" isMobile @click="handleDeleteDepartmentClick(item)">
<template #icon>
<svg class="w-3 h-3" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd"
d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z"
clip-rule="evenodd"></path>
</svg>
</template>
删除
</TableButton>
</div>
</template>
</MobileCardList>
</div>
<!-- PC端表格布局 -->
<div class="relative overflow-x-auto shadow-md sm:rounded-lg hidden md:block">
<table class="w-full text-sm text-left rtl:text-right text-gray-500">
<thead class="text-xs text-gray-700 uppercase bg-gray-50">
<tr>
<th scope="col" class="p-4">
<div class="flex items-center">
<input id="checkbox-all-search" disabled type="checkbox"
class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded-sm focus:ring-blue-500 focus:ring-2">
<label for="checkbox-all-search" class="sr-only">checkbox</label>
</div>
</th>
<th scope="col" class="px-6 py-3">上级部门</th>
<th scope="col" class="px-6 py-3">部门名称</th>
<th scope="col" class="px-6 py-3">操作</th>
</tr>
</thead>
<tbody>
<tr v-for="department in departments" :key="department.id"
class="bg-white border-b border-gray-200 hover:bg-gray-50">
<td class="w-4 p-4">
<div class="flex items-center">
<input :id="'checkbox-table-search-' + department.id" type="checkbox" disabled
class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded-sm focus:ring-blue-500 focus:ring-2">
<label :for="'checkbox-table-search-' + department.id" class="sr-only">checkbox</label>
</div>
</td>
<td class="px-6 py-4 max-w-sm overflow-hidden text-ellipsis">
{{ !department.parentName ? '无' : department.parentName }}
</td>
<td class="px-6 py-4 font-medium text-gray-900">
{{ department.name }}
</td>
<td class="px-6 py-4 max-w-sm overflow-hidden text-ellipsis">
<div class="flex items-center gap-x-2">
<button @click="handleUpsertDepartmentClick(department)"
class="flex items-center justify-center whitespace-nowrap gap-x-1 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"
type="button">
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
<path d="M17.414 2.586a2 2 0 00-2.828 0L7 10.172V13h2.828l7.586-7.586a2 2 0 000-2.828z"></path>
<path fill-rule="evenodd"
d="M2 6a2 2 0 012-2h4a1 1 0 010 2H4v10h10v-4a1 1 0 112 0v4a2 2 0 01-2 2H4a2 2 0 01-2-2V6z"
clip-rule="evenodd"></path>
</svg>
<span>编辑</span>
</button>
<button
class="flex items-center justify-center whitespace-nowrap gap-x-1 bg-red-700 hover:bg-red-800 focus:ring-red-300 text-white focus:ring-4 font-medium rounded-lg text-sm px-4 py-2.5"
@click="handleDeleteDepartmentClick(department)" type="button">
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd"
d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z"
clip-rule="evenodd"></path>
</svg>
<span>删除</span>
</button>
</div>
</td>
</tr>
</tbody>
</table>
<div class="hidden md:block">
<TableFormLayout :items="departments || []" :columns="columns">
<template #parentName="{ item }">
{{ !item.parentName ? '无' : item.parentName }}
</template>
<template #name="{ item }">
{{ item.name }}
</template>
<template #actions="{ item }">
<div class="flex items-center gap-x-2">
<TableButton variant="primary" @click="handleUpsertDepartmentClick(item)">
<template #icon>
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
<path d="M17.414 2.586a2 2 0 00-2.828 0L7 10.172V13h2.828l7.586-7.586a2 2 0 000-2.828z"></path>
<path fill-rule="evenodd"
d="M2 6a2 2 0 012-2h4a1 1 0 010 2H4v10h10v-4a1 1 0 112 0v4a2 2 0 01-2 2H4a2 2 0 01-2-2V6z"
clip-rule="evenodd"></path>
</svg>
</template>
编辑
</TableButton>
<TableButton variant="danger" @click="handleDeleteDepartmentClick(item)">
<template #icon>
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd"
d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z"
clip-rule="evenodd"></path>
</svg>
</template>
删除
</TableButton>
</div>
</template>
</TableFormLayout>
</div>
<TablePagination :total="total" :pageChange="handlePageChange" />
@@ -156,6 +130,8 @@ import Button from "@/components/Button.vue";
import DepartmentUpsertModal from "@/components/DepartmentUpsertModal.vue";
import MobileCardList from "@/components/MobileCardList.vue";
import DepartmentDeleteModal from "@/components/PopupModal.vue";
import TableButton from "@/components/TableButton.vue";
import TableFormLayout from "@/components/TableFormLayout.vue";
import TablePagination from "@/components/TablePagination.vue";
import type { DepartmentUpsertModel } from "@/types/department";
import { Modal, type ModalInterface, initFlowbite } from "flowbite";
@@ -184,6 +160,13 @@ const { upsertDepartment } = useDepartmentUpsert();
const alertStore = useAlertStore();
// 定义表格列配置
const columns = [
{ title: '上级部门', field: 'parentName' },
{ title: '部门名称', field: 'name' },
{ title: '操作', field: 'actions' }
];
onMounted(async () => {
await fetchDepartmentWith({
name: name.value,

View File

@@ -78,78 +78,48 @@
</div>
<!-- PC端表格布局 -->
<div class="relative overflow-x-auto shadow-md sm:rounded-lg hidden md:block">
<table class="w-full whitespace-nowrap text-sm text-left rtl:text-right text-gray-500">
<thead class="text-xs text-gray-700 uppercase bg-gray-50">
<tr>
<th scope="col" class="p-4">
<div class="flex items-center">
<input id="checkbox-all-search" disabled type="checkbox"
class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded-sm focus:ring-blue-500 focus:ring-2">
<label for="checkbox-all-search" class="sr-only">checkbox</label>
</div>
</th>
<th scope="col" class="px-6 py-3">名称</th>
<th scope="col" class="px-6 py-3">模型名称</th>
<th scope="col" class="px-6 py-3">类型</th>
<th scope="col" class="px-6 py-3 hidden lg:table-cell">apiKey</th>
<th scope="col" class="px-6 py-3 hidden lg:table-cell">url</th>
<th scope="col" class="px-6 py-3">状态</th>
<th scope="col" class="px-6 py-3">优先级</th>
<th scope="col" class="px-6 py-3">操作</th>
</tr>
</thead>
<tbody>
<tr v-for="llm in llms" :key="llm.id" class="bg-white border-b border-gray-200 hover:bg-gray-50">
<td class="w-4 p-4">
<div class="flex items-center">
<input :id="'checkbox-table-search-' + llm.id" type="checkbox" disabled
class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded-sm focus:ring-blue-500 focus:ring-2">
<label :for="'checkbox-table-search-' + llm.id" class="sr-only">checkbox</label>
</div>
</td>
<td class="px-6 py-4 max-w-sm overflow-hidden text-ellipsis font-medium text-gray-900">
{{ llm.name }}
</td>
<td class="px-6 py-4 max-w-sm overflow-hidden text-ellipsis">
{{ llm.modelName }}
</td>
<td class="px-6 py-4 max-w-sm overflow-hidden text-ellipsis">
{{ llm.type === 'CHAT' ? '聊天' : '嵌入' }}
</td>
<td class="px-6 py-4 max-w-sm overflow-hidden text-ellipsis hidden lg:table-cell">
{{ llm.apiKey }}
</td>
<td class="px-6 py-4 max-w-sm overflow-hidden text-ellipsis hidden lg:table-cell">
{{ llm.url }}
</td>
<td class="px-6 py-4 max-w-sm overflow-hidden text-ellipsis">
<div class="flex items-center">
<div class="h-2.5 w-2.5 rounded-full me-2" :class="llm.enable ? 'bg-blue-500' : 'bg-red-500'"></div> {{
llm.enable === true ? "启用" : "禁用" }}
</div>
</td>
<td class="px-6 py-4 max-w-sm overflow-hidden text-ellipsis">
{{ llm.priority }}
</td>
<td class="px-6 py-4">
<div class="flex items-center gap-x-2">
<button @click="handleLlmUpdateClick(llm)"
class="flex items-center justify-center whitespace-nowrap gap-x-1 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"
type="button">
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
<path d="M17.414 2.586a2 2 0 00-2.828 0L7 10.172V13h2.828l7.586-7.586a2 2 0 000-2.828z"></path>
<path fill-rule="evenodd"
d="M2 6a2 2 0 012-2h4a1 1 0 010 2H4v10h10v-4a1 1 0 112 0v4a2 2 0 01-2 2H4a2 2 0 01-2-2V6z"
clip-rule="evenodd"></path>
</svg>
<span>编辑</span>
</button>
</div>
</td>
</tr>
</tbody>
</table>
<div class="hidden md:block">
<TableFormLayout :items="llms || []" :columns="columns">
<template #name="{ item }">
{{ item.name }}
</template>
<template #modelName="{ item }">
{{ item.modelName }}
</template>
<template #type="{ item }">
{{ item.type === 'CHAT' ? '聊天' : '嵌入' }}
</template>
<template #apiKey="{ item }">
{{ item.apiKey }}
</template>
<template #url="{ item }">
{{ item.url }}
</template>
<template #status="{ item }">
<div class="flex items-center">
<div class="h-2.5 w-2.5 rounded-full me-2" :class="item.enable ? 'bg-blue-500' : 'bg-red-500'"></div>
{{ item.enable === true ? "启用" : "禁用" }}
</div>
</template>
<template #priority="{ item }">
{{ item.priority }}
</template>
<template #actions="{ item }">
<div class="flex items-center gap-x-2">
<button @click="handleLlmUpdateClick(item)"
class="flex items-center justify-center whitespace-nowrap gap-x-1 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"
type="button">
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
<path d="M17.414 2.586a2 2 0 00-2.828 0L7 10.172V13h2.828l7.586-7.586a2 2 0 000-2.828z"></path>
<path fill-rule="evenodd"
d="M2 6a2 2 0 012-2h4a1 1 0 010 2H4v10h10v-4a1 1 0 112 0v4a2 2 0 01-2 2H4a2 2 0 01-2-2V6z"
clip-rule="evenodd"></path>
</svg>
<span>编辑</span>
</button>
</div>
</template>
</TableFormLayout>
</div>
<TablePagination :pageChange="handlePageChange" :total="total" />
@@ -164,6 +134,7 @@
import Breadcrumbs from "@/components/Breadcrumbs.vue";
import LlmUpdateModal from "@/components/LlmUpdateModal.vue";
import MobileCardList from "@/components/MobileCardList.vue";
import TableFormLayout from "@/components/TableFormLayout.vue";
import TablePagination from "@/components/TablePagination.vue";
import { useLlmQuery } from "@/composables/ai/useLlmQuery";
import { useLlmUpdate } from "@/composables/ai/useLlmUpdate";
@@ -181,6 +152,18 @@ const { updateLlmConfig } = useLlmUpdate();
const alertStore = useAlertStore();
// 定义表格列配置
const columns = [
{ title: '名称', field: 'name' },
{ title: '模型名称', field: 'modelName' },
{ title: '类型', field: 'type' },
{ title: 'apiKey', field: 'apiKey', class: 'hidden lg:table-cell' },
{ title: 'url', field: 'url', class: 'hidden lg:table-cell' },
{ title: '状态', field: 'status' },
{ title: '优先级', field: 'priority' },
{ title: '操作', field: 'actions' }
];
const handleUpdateModalSubmit = async (llm: components["schemas"]["LlmVm"]) => {
await updateLlmConfig(llm);
llmUpdateModal.value?.hide();

View File

@@ -49,92 +49,67 @@
</template>
<template #actions="{ item }">
<div class="flex gap-x-2">
<button @click="handleUpsertPermissionClick(item)"
class="flex items-center justify-center gap-x-1 text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-xs px-3 py-1.5"
type="button">
<svg class="w-3 h-3" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
<path d="M17.414 2.586a2 2 0 00-2.828 0L7 10.172V13h2.828l7.586-7.586a2 2 0 000-2.828z"></path>
<path fill-rule="evenodd"
d="M2 6a2 2 0 012-2h4a1 1 0 010 2H4v10h10v-4a1 1 0 112 0v4a2 2 0 01-2 2H4a2 2 0 01-2-2V6z"
clip-rule="evenodd"></path>
</svg>
<span>编辑</span>
</button>
<button
class="flex items-center justify-center gap-x-1 bg-red-700 hover:bg-red-800 focus:outline-none focus:ring-red-300 text-white focus:ring-4 font-medium rounded-lg text-xs px-3 py-1.5"
@click="handleDeletePermissionClick(item)" type="button">
<svg class="w-3 h-3" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd"
d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z"
clip-rule="evenodd"></path>
</svg>
<span>删除</span>
</button>
<TableButton variant="primary" size="xs" isMobile @click="handleUpsertPermissionClick(item)">
<template #icon>
<svg class="w-3 h-3" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
<path d="M17.414 2.586a2 2 0 00-2.828 0L7 10.172V13h2.828l7.586-7.586a2 2 0 000-2.828z"></path>
<path fill-rule="evenodd"
d="M2 6a2 2 0 012-2h4a1 1 0 010 2H4v10h10v-4a1 1 0 112 0v4a2 2 0 01-2 2H4a2 2 0 01-2-2V6z"
clip-rule="evenodd"></path>
</svg>
</template>
编辑
</TableButton>
<TableButton variant="danger" size="xs" isMobile @click="handleDeletePermissionClick(item)">
<template #icon>
<svg class="w-3 h-3" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd"
d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z"
clip-rule="evenodd"></path>
</svg>
</template>
删除
</TableButton>
</div>
</template>
</MobileCardList>
</div>
<!-- PC端表格布局 -->
<div class="relative overflow-x-auto shadow-md sm:rounded-lg hidden md:block">
<table class="w-full text-sm text-left rtl:text-right text-gray-500">
<thead class="text-xs text-gray-700 uppercase bg-gray-50">
<tr>
<th scope="col" class="p-4">
<div class="flex items-center">
<input id="checkbox-all-search" disabled type="checkbox"
class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded-sm focus:ring-blue-500 focus:ring-2">
<label for="checkbox-all-search" class="sr-only">checkbox</label>
</div>
</th>
<th scope="col" class="px-6 py-3">权限名称</th>
<th scope="col" class="px-6 py-3">权限编码</th>
<th scope="col" class="px-6 py-3">操作</th>
</tr>
</thead>
<tbody>
<tr v-for="permission in permissions" :key="permission.id"
class="bg-white border-b border-gray-200 hover:bg-gray-50">
<td class="w-4 p-4">
<div class="flex items-center">
<input :id="'checkbox-table-search-' + permission.id" type="checkbox" disabled
class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded-sm focus:ring-blue-500 focus:ring-2">
<label :for="'checkbox-table-search-' + permission.id" class="sr-only">checkbox</label>
</div>
</td>
<td scope="row" class="px-6 py-4 font-medium text-gray-900 whitespace-nowrap">
{{ permission.name }}
</td>
<td class="px-6 py-4 max-w-sm overflow-hidden text-ellipsis">
{{ permission.code }}</td>
<td class="px-6 py-4 max-w-sm overflow-hidden text-ellipsis">
<div class="flex items-center gap-x-2">
<button @click="handleUpsertPermissionClick(permission)"
class="flex items-center justify-center whitespace-nowrap gap-x-1 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"
type="button">
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
<path d="M17.414 2.586a2 2 0 00-2.828 0L7 10.172V13h2.828l7.586-7.586a2 2 0 000-2.828z"></path>
<path fill-rule="evenodd"
d="M2 6a2 2 0 012-2h4a1 1 0 010 2H4v10h10v-4a1 1 0 112 0v4a2 2 0 01-2 2H4a2 2 0 01-2-2V6z"
clip-rule="evenodd"></path>
</svg>
<span>编辑</span>
</button>
<button
class="flex items-center justify-center whitespace-nowrap gap-x-1 bg-red-700 hover:bg-red-800 focus:outline-none focus:ring-red-300 text-white focus:ring-4 font-medium rounded-lg text-sm px-4 py-2.5"
@click="handleDeletePermissionClick(permission)" type="button">
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd"
d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z"
clip-rule="evenodd"></path>
</svg>
<span>删除</span>
</button>
</div>
</td>
</tr>
</tbody>
</table>
<div class="hidden md:block">
<TableFormLayout :items="permissions || []" :columns="columns">
<template #name="{ item }">
{{ item.name }}
</template>
<template #code="{ item }">
{{ item.code }}
</template>
<template #actions="{ item }">
<div class="flex items-center gap-x-2">
<TableButton variant="primary" @click="handleUpsertPermissionClick(item)">
<template #icon>
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
<path d="M17.414 2.586a2 2 0 00-2.828 0L7 10.172V13h2.828l7.586-7.586a2 2 0 000-2.828z"></path>
<path fill-rule="evenodd"
d="M2 6a2 2 0 012-2h4a1 1 0 010 2H4v10h10v-4a1 1 0 112 0v4a2 2 0 01-2 2H4a2 2 0 01-2-2V6z"
clip-rule="evenodd"></path>
</svg>
</template>
编辑
</TableButton>
<TableButton variant="danger" @click="handleDeletePermissionClick(item)">
<template #icon>
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd"
d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z"
clip-rule="evenodd"></path>
</svg>
</template>
删除
</TableButton>
</div>
</template>
</TableFormLayout>
</div>
<TablePagination :pageChange="handlePageChange" :total="total" />
@@ -158,6 +133,8 @@ import usePermissionDelete from "@/composables/permission/usePermissionDelete";
import type { components } from "@/api/types/schema";
import Breadcrumbs from "@/components/Breadcrumbs.vue";
import MobileCardList from "@/components/MobileCardList.vue";
import TableButton from "@/components/TableButton.vue";
import TableFormLayout from "@/components/TableFormLayout.vue";
import TablePagination from "@/components/TablePagination.vue";
import { Modal, type ModalInterface, initFlowbite } from "flowbite";
import { nextTick, onMounted, ref } from "vue";
@@ -177,6 +154,13 @@ const { deletePermission } = usePermissionDelete();
const permissionUpsert = usePermissionUpsert();
const alertStore = useAlertStore();
// 定义表格列配置
const columns = [
{ title: '权限名称', field: 'name' },
{ title: '权限编码', field: 'code' },
{ title: '操作', field: 'actions' }
];
onMounted(async () => {
await fetchPermissionsWith({
name: permissionName.value,

View File

@@ -70,62 +70,37 @@
</div>
<!-- PC端表格布局 -->
<div class="relative overflow-x-auto shadow-md sm:rounded-lg hidden md:block">
<table class="w-full text-sm text-left rtl:text-right text-gray-500">
<thead class="text-xs text-gray-700 uppercase bg-gray-50">
<tr>
<th scope="col" class="p-4">
<div class="flex items-center">
<input id="checkbox-all-search" disabled type="checkbox"
class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded-sm focus:ring-blue-500 focus:ring-2">
<label for="checkbox-all-search" class="sr-only">checkbox</label>
</div>
</th>
<th scope="col" class="px-6 py-3">岗位名称</th>
<th scope="col" class="px-6 py-3">操作</th>
</tr>
</thead>
<tbody>
<tr v-for="position in positions" :key="position.id"
class="bg-white border-b border-gray-200 hover:bg-gray-50">
<td class="w-4 p-4">
<div class="flex items-center">
<input :id="'checkbox-table-search-' + position.id" type="checkbox" disabled
class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded-sm focus:ring-blue-500 focus:ring-2">
<label :for="'checkbox-table-search-' + position.id" class="sr-only">checkbox</label>
</div>
</td>
<td class="px-6 py-4 font-medium text-gray-900">
{{ position.name }}
</td>
<td class="px-6 py-4 max-w-sm overflow-hidden text-ellipsis">
<div class="flex items-center gap-x-2">
<button @click="handleUpsertPositionClick(position)"
class="flex items-center justify-center whitespace-nowrap gap-x-1 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"
type="button">
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
<path d="M17.414 2.586a2 2 0 00-2.828 0L7 10.172V13h2.828l7.586-7.586a2 2 0 000-2.828z"></path>
<path fill-rule="evenodd"
d="M2 6a2 2 0 012-2h4a1 1 0 010 2H4v10h10v-4a1 1 0 112 0v4a2 2 0 01-2 2H4a2 2 0 01-2-2V6z"
clip-rule="evenodd"></path>
</svg>
<span>编辑</span>
</button>
<button
class="flex items-center justify-center whitespace-nowrap gap-x-1 bg-red-700 hover:bg-red-800 focus:ring-red-300 text-white focus:ring-4 font-medium rounded-lg text-sm px-4 py-2.5"
@click="handleDeletePositionClick(position)" type="button">
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd"
d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z"
clip-rule="evenodd"></path>
</svg>
<span>删除</span>
</button>
</div>
</td>
</tr>
</tbody>
</table>
<div class="hidden md:block">
<TableFormLayout :items="positions" :columns="columns">
<template #name="{ item }">
{{ item.name }}
</template>
<template #actions="{ item }">
<div class="flex items-center gap-x-2">
<button @click="handleUpsertPositionClick(item)"
class="flex items-center justify-center whitespace-nowrap gap-x-1 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"
type="button">
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
<path d="M17.414 2.586a2 2 0 00-2.828 0L7 10.172V13h2.828l7.586-7.586a2 2 0 000-2.828z"></path>
<path fill-rule="evenodd"
d="M2 6a2 2 0 012-2h4a1 1 0 010 2H4v10h10v-4a1 1 0 112 0v4a2 2 0 01-2 2H4a2 2 0 01-2-2V6z"
clip-rule="evenodd"></path>
</svg>
<span>编辑</span>
</button>
<button
class="flex items-center justify-center whitespace-nowrap gap-x-1 bg-red-700 hover:bg-red-800 focus:ring-red-300 text-white focus:ring-4 font-medium rounded-lg text-sm px-4 py-2.5"
@click="handleDeletePositionClick(item)" type="button">
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd"
d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z"
clip-rule="evenodd"></path>
</svg>
<span>删除</span>
</button>
</div>
</template>
</TableFormLayout>
</div>
<TablePagination :total="total" :pageChange="handlePageChange" />
</div>
@@ -146,6 +121,7 @@ import Button from "@/components/Button.vue";
import MobileCardList from "@/components/MobileCardList.vue";
import PositionDeleteModal from "@/components/PopupModal.vue";
import PositionUpsertModal from "@/components/PositionUpsertModal.vue";
import TableFormLayout from "@/components/TableFormLayout.vue";
import TablePagination from "@/components/TablePagination.vue";
import usePositionDelete from "@/composables/position/usePositionDelete";
import { usePositionQuery } from "@/composables/position/usePositionQuery";
@@ -169,6 +145,12 @@ const { upsertPosition } = usePositionUpsert();
const alertStore = useAlertStore();
// 定义表格列配置
const columns = [
{ title: '岗位名称', field: 'name' },
{ title: '操作', field: 'actions' }
];
onMounted(async () => {
await fetchAllPositions();
await fetchPositionWith({

View File

@@ -83,74 +83,49 @@
</div>
<!-- PC端表格布局 -->
<div class="relative overflow-x-auto shadow-md sm:rounded-lg hidden md:block">
<table class="w-full text-sm text-left rtl:text-right text-gray-500">
<thead class="text-xs text-gray-700 uppercase bg-gray-50">
<tr>
<th scope="col" class="p-4">
<div class="flex items-center">
<input id="checkbox-all-search" disabled type="checkbox"
class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded-sm focus:ring-blue-500 focus:ring-2">
<label for="checkbox-all-search" class="sr-only">checkbox</label>
</div>
</th>
<th scope="col" class="px-6 py-3">角色名称</th>
<th scope="col" class="px-6 py-3">角色编码</th>
<th scope="col" class="px-6 py-3">分配</th>
<th scope="col" class="px-6 py-3">操作</th>
</tr>
</thead>
<tbody>
<tr v-for="role in roles" :key="role.id" class="bg-white border-b border-gray-200 hover:bg-gray-50">
<td class="w-4 p-4">
<div class="flex items-center">
<input :id="'checkbox-table-search-' + role.id" type="checkbox" disabled
class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded-sm focus:ring-blue-500 focus:ring-2">
<label :for="'checkbox-table-search-' + role.id" class="sr-only">checkbox</label>
</div>
</td>
<td scope="row" class="px-6 py-4 font-medium text-gray-900 whitespace-nowrap">
{{ role.name }}
</td>
<td class="px-6 py-4 max-w-sm overflow-hidden text-ellipsis">
{{ role.code }}</td>
<td class="px-6 py-4 max-w-sm overflow-hidden text-ellipsis">
<div>
<button
class="flex items-center justify-center min-w-min text-gray-900 bg-white border border-gray-300 focus:outline-none hover:bg-gray-100 focus:ring-4 focus:ring-gray-100 font-medium rounded-lg text-sm px-4 py-2.5 whitespace-nowrap"
@click="handleBindPermissionClick(role)" type="button">
<span>分配权限</span>
</button>
</div>
</td>
<td class="px-6 py-4 max-w-sm overflow-hidden text-ellipsis">
<div class="flex items-center gap-x-2">
<button @click="handleUpsertRoleClick(role)"
class="flex items-center justify-center whitespace-nowrap gap-x-1 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"
type="button">
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
<path d="M17.414 2.586a2 2 0 00-2.828 0L7 10.172V13h2.828l7.586-7.586a2 2 0 000-2.828z"></path>
<path fill-rule="evenodd"
d="M2 6a2 2 0 012-2h4a1 1 0 010 2H4v10h10v-4a1 1 0 112 0v4a2 2 0 01-2 2H4a2 2 0 01-2-2V6z"
clip-rule="evenodd"></path>
</svg>
<span>编辑</span>
</button>
<button
class="flex items-center justify-center whitespace-nowrap gap-x-1 bg-red-700 hover:bg-red-800 focus:outline-none focus:ring-red-300 text-white focus:ring-4 font-medium rounded-lg text-sm px-4 py-2.5"
@click="handleDeleteRoleClick(role)" type="button">
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd"
d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z"
clip-rule="evenodd"></path>
</svg>
<span>删除</span>
</button>
</div>
</td>
</tr>
</tbody>
</table>
<div class="hidden md:block">
<TableFormLayout :items="roles || []" :columns="columns">
<template #name="{ item }">
{{ item.name }}
</template>
<template #code="{ item }">
{{ item.code }}
</template>
<template #assign="{ item }">
<div>
<button
class="flex items-center justify-center min-w-min text-gray-900 bg-white border border-gray-300 focus:outline-none hover:bg-gray-100 focus:ring-4 focus:ring-gray-100 font-medium rounded-lg text-sm px-4 py-2.5 whitespace-nowrap"
@click="handleBindPermissionClick(item)" type="button">
<span>分配权限</span>
</button>
</div>
</template>
<template #actions="{ item }">
<div class="flex items-center gap-x-2">
<button @click="handleUpsertRoleClick(item)"
class="flex items-center justify-center whitespace-nowrap gap-x-1 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"
type="button">
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
<path d="M17.414 2.586a2 2 0 00-2.828 0L7 10.172V13h2.828l7.586-7.586a2 2 0 000-2.828z"></path>
<path fill-rule="evenodd"
d="M2 6a2 2 0 012-2h4a1 1 0 010 2H4v10h10v-4a1 1 0 112 0v4a2 2 0 01-2 2H4a2 2 0 01-2-2V6z"
clip-rule="evenodd"></path>
</svg>
<span>编辑</span>
</button>
<button
class="flex items-center justify-center whitespace-nowrap gap-x-1 bg-red-700 hover:bg-red-800 focus:outline-none focus:ring-red-300 text-white focus:ring-4 font-medium rounded-lg text-sm px-4 py-2.5"
@click="handleDeleteRoleClick(item)" type="button">
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd"
d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z"
clip-rule="evenodd"></path>
</svg>
<span>删除</span>
</button>
</div>
</template>
</TableFormLayout>
</div>
<TablePagination :pageChange="handlePageChange" :total="total" />
</div>
@@ -170,6 +145,7 @@ import Button from "@/components/Button.vue";
import MobileCardList from "@/components/MobileCardList.vue";
import RoleDeleteModal from "@/components/PopupModal.vue";
import RoleUpsertModal from "@/components/RoleUpsertModal.vue";
import TableFormLayout from "@/components/TableFormLayout.vue";
import TablePagination from "@/components/TablePagination.vue";
import useRoleDelete from "@/composables/role/useRoleDelete";
import { useRolesQuery } from "@/composables/role/useRolesQuery";
@@ -193,6 +169,15 @@ const { deleteRole } = useRoleDelete();
const alertStore = useAlertStore();
const router = useRouter();
const upsertRole = useRoleUpsert();
// 定义表格列配置
const columns = [
{ title: '角色名称', field: 'name' },
{ title: '角色编码', field: 'code' },
{ title: '分配', field: 'assign' },
{ title: '操作', field: 'actions' }
];
onMounted(async () => {
await fetchRolesWith({
name: roleName.value,

View File

@@ -85,111 +85,67 @@
</div>
<!-- PC端表格布局 -->
<div class="relative overflow-x-auto shadow-md sm:rounded-lg hidden md:block">
<table class="w-full text-sm text-left rtl:text-right text-gray-500">
<thead class="text-xs text-gray-700 uppercase bg-gray-50">
<tr>
<th scope="col" class="p-2 sm:p-4 hidden sm:table-cell">
<div class="flex items-center">
<input id="checkbox-all-search" disabled type="checkbox"
class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded-sm focus:ring-blue-500 focus:ring-2">
<label for="checkbox-all-search" class="sr-only">checkbox</label>
</div>
</th>
<th scope="col" class="px-3 py-2 sm:px-4 md:px-6 sm:py-3">任务</th>
<th scope="col" class="px-3 py-2 sm:px-4 md:px-6 sm:py-3 hidden lg:table-cell">触发器</th>
<th scope="col" class="px-3 py-2 sm:px-4 md:px-6 sm:py-3 hidden lg:table-cell">开始</th>
<th scope="col" class="px-3 py-2 sm:px-4 md:px-6 sm:py-3 hidden lg:table-cell">结束</th>
<th scope="col" class="px-3 py-2 sm:px-4 md:px-6 sm:py-3 hidden md:table-cell">下次执行</th>
<th scope="col" class="px-3 py-2 sm:px-4 md:px-6 sm:py-3 hidden lg:table-cell">上次执行</th>
<th scope="col" class="px-3 py-2 sm:px-4 md:px-6 sm:py-3 hidden md:table-cell">类型</th>
<th scope="col" class="px-3 py-2 sm:px-4 md:px-6 sm:py-3 hidden md:table-cell">Cron</th>
<th scope="col" class="px-3 py-2 sm:px-4 md:px-6 sm:py-3">状态</th>
<th scope="col" class="px-3 py-2 sm:px-4 md:px-6 sm:py-3 hidden sm:table-cell">编辑</th>
<th scope="col" class="px-3 py-2 sm:px-4 md:px-6 sm:py-3">操作</th>
</tr>
</thead>
<tbody>
<tr v-for="job in jobs" :key="job.triggerName" class="bg-white border-b border-gray-200 hover:bg-gray-50">
<td class="w-4 p-2 sm:p-4 hidden sm:table-cell">
<div class="flex items-center">
<input :id="'checkbox-table-search-' + job.triggerName" type="checkbox" disabled
class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded-sm focus:ring-blue-500 focus:ring-2">
<label :for="'checkbox-table-search-' + job.triggerName" class="sr-only">checkbox</label>
</div>
</td>
<td
class="px-3 py-2 sm:px-4 md:px-6 sm:py-4 max-w-[120px] sm:max-w-xs md:max-w-sm overflow-hidden text-ellipsis font-medium text-gray-900 whitespace-nowrap">
{{
`${job.name}:${job.group}` }}</td>
<td
class="px-3 py-2 sm:px-4 md:px-6 sm:py-4 max-w-[120px] sm:max-w-xs md:max-w-sm overflow-hidden text-ellipsis hidden lg:table-cell">
{{
`${job.triggerName}:${job.triggerGroup}` }}
</td>
<td
class="px-3 py-2 sm:px-4 md:px-6 sm:py-4 min-w-[150px] sm:min-w-3xs max-w-sm overflow-hidden text-ellipsis hidden lg:table-cell">
{{
dayjs(job.startTime!).format("llll") }}
</td>
<td
class="px-3 py-2 sm:px-4 md:px-6 sm:py-4 min-w-[150px] sm:min-w-3xs max-w-sm overflow-hidden text-ellipsis hidden lg:table-cell">
{{ job.endTime ?
dayjs(job.endTime).format("llll") : undefined }}</td>
<td
class="px-3 py-2 sm:px-4 md:px-6 sm:py-4 min-w-[150px] sm:min-w-3xs max-w-sm overflow-hidden text-ellipsis hidden md:table-cell">
{{ job.nextFireTime ?
dayjs(job.nextFireTime).format("llll") : undefined}}</td>
<td
class="px-3 py-2 sm:px-4 md:px-6 sm:py-4 min-w-[150px] sm:min-w-3xs max-w-sm overflow-hidden text-ellipsis hidden lg:table-cell">
{{ job.previousFireTime &&
job.previousFireTime
> 0 ? dayjs(job.previousFireTime).format("llll") :
undefined
}}
</td>
<td
class="px-3 py-2 sm:px-4 md:px-6 sm:py-4 max-w-[80px] sm:max-w-xs md:max-w-sm overflow-hidden text-ellipsis hidden md:table-cell">
{{ job.schedulerType }}</td>
<td
class="px-3 py-2 sm:px-4 md:px-6 sm:py-4 max-w-[100px] sm:max-w-xs md:max-w-sm overflow-hidden text-ellipsis hidden md:table-cell">
{{ job.cronExpression }}</td>
<td
class="px-3 py-2 sm:px-4 md:px-6 sm:py-4 max-w-[80px] sm:max-w-xs md:max-w-sm overflow-hidden text-ellipsis">
{{ job.triggerState }}</td>
<td class="px-3 py-2 sm:px-4 md:px-6 sm:py-4 whitespace-nowrap hidden sm:table-cell">
<div class="flex items-center gap-x-2">
<button @click="handleCronUpdateClick(job)" :disabled="job.schedulerType !== 'CRON'"
:class="['flex items-center justify-center gap-x-1 text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-xs sm:text-sm px-3 py-1.5 sm:px-4 sm:py-2.5' , { 'opacity-50 cursor-not-allowed': job.schedulerType !== 'CRON' }]"
type="button">
<svg class="w-3 h-3 sm:w-4 sm:h-4" fill="currentColor" viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg">
<path d="M17.414 2.586a2 2 0 00-2.828 0L7 10.172V13h2.828l7.586-7.586a2 2 0 000-2.828z"></path>
<path fill-rule="evenodd"
d="M2 6a2 2 0 012-2h4a1 1 0 010 2H4v10h10v-4a1 1 0 112 0v4a2 2 0 01-2 2H4a2 2 0 01-2-2V6z"
clip-rule="evenodd"></path>
</svg>
<span>编辑</span>
</button>
</div>
</td>
<td class="px-3 py-2 sm:px-4 md:px-6 sm:py-4 whitespace-nowrap">
<div class="flex flex-col sm:flex-row items-start sm:items-center gap-y-2 sm:gap-y-0 sm:gap-x-2">
<button
:class="['text-white bg-green-700 hover:bg-green-800 focus:ring-green-300 focus:ring-4 focus:outline-none font-medium rounded-lg text-xs sm:text-sm px-3 py-1.5 sm:px-4 sm:py-2.5 text-center']"
@click="handleResumeJobClick(job)" type="button">
<span>恢复</span>
</button>
<button
:class="['bg-red-700 hover:bg-red-800 focus:ring-red-300 text-white focus:ring-4 focus:outline-none font-medium rounded-lg text-xs sm:text-sm px-3 py-1.5 sm:px-4 sm:py-2.5 text-center']"
@click="handlePauseJobClick(job)" type="button">
<span>暂停</span>
</button>
</div>
</td>
</tr>
</tbody>
</table>
<div class="hidden md:block">
<TableFormLayout :items="jobs || []" :columns="columns">
<template #name="{ item }">
{{ `${item.name}:${item.group}` }}
</template>
<template #trigger="{ item }">
{{ `${item.triggerName}:${item.triggerGroup}` }}
</template>
<template #startTime="{ item }">
{{ dayjs(item.startTime!).format("llll") }}
</template>
<template #endTime="{ item }">
{{ item.endTime ? dayjs(item.endTime).format("llll") : undefined }}
</template>
<template #nextFireTime="{ item }">
{{ item.nextFireTime ? dayjs(item.nextFireTime).format("llll") : undefined }}
</template>
<template #previousFireTime="{ item }">
{{ item.previousFireTime && item.previousFireTime > 0 ? dayjs(item.previousFireTime).format("llll") :
undefined }}
</template>
<template #schedulerType="{ item }">
{{ item.schedulerType }}
</template>
<template #cronExpression="{ item }">
{{ item.cronExpression }}
</template>
<template #triggerState="{ item }">
{{ item.triggerState }}
</template>
<template #edit="{ item }">
<div class="flex items-center gap-x-2">
<button @click="handleCronUpdateClick(item)" :disabled="item.schedulerType !== 'CRON'"
:class="['flex items-center justify-center gap-x-1 text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-xs sm:text-sm px-3 py-1.5 sm:px-4 sm:py-2.5' , { 'opacity-50 cursor-not-allowed': item.schedulerType !== 'CRON' }]"
type="button">
<svg class="w-3 h-3 sm:w-4 sm:h-4" fill="currentColor" viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg">
<path d="M17.414 2.586a2 2 0 00-2.828 0L7 10.172V13h2.828l7.586-7.586a2 2 0 000-2.828z"></path>
<path fill-rule="evenodd"
d="M2 6a2 2 0 012-2h4a1 1 0 010 2H4v10h10v-4a1 1 0 112 0v4a2 2 0 01-2 2H4a2 2 0 01-2-2V6z"
clip-rule="evenodd"></path>
</svg>
<span>编辑</span>
</button>
</div>
</template>
<template #actions="{ item }">
<div class="flex flex-col sm:flex-row items-start sm:items-center gap-y-2 sm:gap-y-0 sm:gap-x-2">
<button
:class="['text-white bg-green-700 hover:bg-green-800 focus:ring-green-300 focus:ring-4 focus:outline-none font-medium rounded-lg text-xs sm:text-sm px-3 py-1.5 sm:px-4 sm:py-2.5 text-center']"
@click="handleResumeJobClick(item)" type="button">
<span>恢复</span>
</button>
<button
:class="['bg-red-700 hover:bg-red-800 focus:ring-red-300 text-white focus:ring-4 focus:outline-none font-medium rounded-lg text-xs sm:text-sm px-3 py-1.5 sm:px-4 sm:py-2.5 text-center']"
@click="handlePauseJobClick(item)" type="button">
<span>暂停</span>
</button>
</div>
</template>
</TableFormLayout>
</div>
<TablePagination :pageChange="handlePageChange" :total="total" />
@@ -211,6 +167,7 @@ import Breadcrumbs from "@/components/Breadcrumbs.vue";
import MobileCardList from "@/components/MobileCardList.vue";
import PopupModal from "@/components/PopupModal.vue";
import SchedulerUpdateModal from "@/components/SchedulerUpdateModal.vue";
import TableFormLayout from "@/components/TableFormLayout.vue";
import TablePagination from "@/components/TablePagination.vue";
import { useJobControl } from "@/composables/job/useJobControl";
import { useJobsPaginationQuery } from "@/composables/job/useJobQuery";
@@ -236,6 +193,21 @@ const { resumeTrigger, pauseTrigger } = useJobControl();
const { updateCron } = useJobUpdate();
// 定义表格列配置
const columns = [
{ title: '任务', field: 'name' },
{ title: '触发器', field: 'trigger', class: 'hidden lg:table-cell' },
{ title: '开始', field: 'startTime', class: 'hidden lg:table-cell' },
{ title: '结束', field: 'endTime', class: 'hidden lg:table-cell' },
{ title: '下次执行', field: 'nextFireTime', class: 'hidden md:table-cell' },
{ title: '上次执行', field: 'previousFireTime', class: 'hidden lg:table-cell' },
{ title: '类型', field: 'schedulerType', class: 'hidden md:table-cell' },
{ title: 'Cron', field: 'cronExpression', class: 'hidden md:table-cell' },
{ title: '状态', field: 'triggerState' },
{ title: '编辑', field: 'edit', class: 'hidden sm:table-cell' },
{ title: '操作', field: 'actions' }
];
const handleResumeJobClick = async (
currentJob: components["schemas"]["JobTriggerDto"],
) => {

View File

@@ -97,104 +97,62 @@
</div>
<!-- PC端表格布局 -->
<div class="relative overflow-x-auto shadow-md sm:rounded-lg hidden md:block">
<table class="w-full text-sm text-left rtl:text-right text-gray-500">
<thead class="text-xs text-gray-700 uppercase bg-gray-50">
<tr>
<th scope="col" class="p-4">
<div class="flex items-center">
<input id="checkbox-all-search" disabled type="checkbox"
class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded-sm focus:ring-blue-500 focus:ring-2">
<label for="checkbox-all-search" class="sr-only">checkbox</label>
</div>
</th>
<th scope="col" class="px-6 py-3 cursor-pointer" @click="handleSortClick('username')">
<div class="flex items-center">
<span>用户名</span>
<SortIcon :sortField="getSortField('username')" />
</div>
</th>
<th scope="col" class="px-6 py-3 cursor-pointer" @click="handleSortClick('createTime')">
<div class="flex items-center">
<span>创建时间</span>
<SortIcon :sortField="getSortField('createTime')" />
</div>
</th>
<th scope="col" class="px-6 py-3">状态</th>
<th scope="col" class="px-6 py-3">分配</th>
<th scope="col" class="px-6 py-3">操作</th>
</tr>
</thead>
<tbody>
<tr v-for="user in users" :key="user.id" class="bg-white border-b border-gray-200 hover:bg-gray-50">
<td class="w-4 p-4">
<div class="flex items-center">
<input :id="'checkbox-table-search-' + user.id" type="checkbox" disabled
class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded-sm focus:ring-blue-500 focus:ring-2">
<label :for="'checkbox-table-search-' + user.id" class="sr-only">checkbox</label>
</div>
</td>
<td scope="row"
class="px-6 py-4 font-medium text-gray-900 whitespace-nowrap max-w-sm overflow-hidden text-ellipsis">
{{ user.username }}
</td>
<td class="px-6 py-4 max-w-sm whitespace-nowrap overflow-hidden text-ellipsis">
{{ dayjs(user.createTime).format("llll") }}
</td>
<td class="px-6 py-4 max-w-sm whitespace-nowrap overflow-hidden text-ellipsis">
<div class="flex items-center">
<div class="h-2.5 w-2.5 rounded-full me-2" :class="user.enable ? 'bg-blue-500' : 'bg-red-500'"></div> {{
user.enable === true ? "启用" : "禁用" }}
</div>
</td>
<td class="px-6 py-4 max-w-sm overflow-hidden text-ellipsis">
<div class="flex items-center gap-x-2">
<button
class="text-gray-900 bg-white border whitespace-nowrap border-gray-300 focus:outline-none hover:bg-gray-100 focus:ring-4 focus:ring-gray-100 font-medium rounded-lg text-sm px-4 py-2.5"
@click="handleBindRoleClick(user)" type="button">
分配角色
</button>
<button
class="text-gray-900 bg-white border whitespace-nowrap border-gray-300 focus:outline-none hover:bg-gray-100 focus:ring-4 focus:ring-gray-100 font-medium rounded-lg text-sm px-4 py-2.5"
@click="handleBindPositionClick(user)" type="button">
分配岗位
</button>
<button
class="text-gray-900 bg-white border whitespace-nowrap border-gray-300 focus:outline-none hover:bg-gray-100 focus:ring-4 focus:ring-gray-100 font-medium rounded-lg text-sm px-4 py-2.5"
@click="handleBindDepartmentClick(user)" type="button">
分配部门
</button>
</div>
</td>
<td class="px-6 py-4 max-w-sm overflow-hidden text-ellipsis">
<!-- Edit Modal toggle -->
<div class="flex items-center gap-x-2">
<button @click="handleUpsertUserClick(user)"
class="flex items-center justify-center whitespace-nowrap gap-x-1 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"
type="button">
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
<path d="M17.414 2.586a2 2 0 00-2.828 0L7 10.172V13h2.828l7.586-7.586a2 2 0 000-2.828z"></path>
<path fill-rule="evenodd"
d="M2 6a2 2 0 012-2h4a1 1 0 010 2H4v10h10v-4a1 1 0 112 0v4a2 2 0 01-2 2H4a2 2 0 01-2-2V6z"
clip-rule="evenodd"></path>
</svg>
<span>编辑</span>
</button>
<button
class="flex items-center justify-center whitespace-nowrap gap-x-1 bg-red-700 hover:bg-red-800 focus:ring-red-300 text-white focus:ring-4 focus:outline-none font-medium rounded-lg text-sm px-4 py-2.5"
@click="handleDeleteUserClick(user)" type="button">
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd"
d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z"
clip-rule="evenodd"></path>
</svg>
<span>删除</span>
</button>
</div>
</td>
</tr>
</tbody>
</table>
<div class="hidden md:block">
<TableFormLayout :items="users" :columns="columns" @sort="handleSortClick">
<template #sort-icon="{ field }">
<SortIcon :sortField="getSortField(field)" />
</template>
<template #status="{ item }">
<div class="flex items-center">
<div class="h-2.5 w-2.5 rounded-full me-2" :class="item.enable ? 'bg-blue-500' : 'bg-red-500'"></div>
{{ item.enable === true ? "启用" : "禁用" }}
</div>
</template>
<template #assign="{ item }">
<div class="flex items-center gap-x-2">
<button
class="text-gray-900 bg-white border whitespace-nowrap border-gray-300 focus:outline-none hover:bg-gray-100 focus:ring-4 focus:ring-gray-100 font-medium rounded-lg text-sm px-4 py-2.5"
@click="handleBindRoleClick(item)" type="button">
分配角色
</button>
<button
class="text-gray-900 bg-white border whitespace-nowrap border-gray-300 focus:outline-none hover:bg-gray-100 focus:ring-4 focus:ring-gray-100 font-medium rounded-lg text-sm px-4 py-2.5"
@click="handleBindPositionClick(item)" type="button">
分配岗位
</button>
<button
class="text-gray-900 bg-white border whitespace-nowrap border-gray-300 focus:outline-none hover:bg-gray-100 focus:ring-4 focus:ring-gray-100 font-medium rounded-lg text-sm px-4 py-2.5"
@click="handleBindDepartmentClick(item)" type="button">
分配部门
</button>
</div>
</template>
<template #actions="{ item }">
<div class="flex items-center gap-x-2">
<button @click="handleUpsertUserClick(item)"
class="flex items-center justify-center whitespace-nowrap gap-x-1 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"
type="button">
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
<path d="M17.414 2.586a2 2 0 00-2.828 0L7 10.172V13h2.828l7.586-7.586a2 2 0 000-2.828z"></path>
<path fill-rule="evenodd"
d="M2 6a2 2 0 012-2h4a1 1 0 010 2H4v10h10v-4a1 1 0 112 0v4a2 2 0 01-2 2H4a2 2 0 01-2-2V6z"
clip-rule="evenodd"></path>
</svg>
<span>编辑</span>
</button>
<button
class="flex items-center justify-center whitespace-nowrap gap-x-1 bg-red-700 hover:bg-red-800 focus:ring-red-300 text-white focus:ring-4 focus:outline-none font-medium rounded-lg text-sm px-4 py-2.5"
@click="handleDeleteUserClick(item)" type="button">
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd"
d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z"
clip-rule="evenodd"></path>
</svg>
<span>删除</span>
</button>
</div>
</template>
</TableFormLayout>
</div>
<TablePagination :pageChange="handlePageChange" :total="total" />
</div>
@@ -213,6 +171,7 @@ 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 TableFormLayout from "@/components/TableFormLayout.vue";
import TablePagination from "@/components/TablePagination.vue";
import UserUpsertModal from "@/components/UserUpsertModal.vue";
import { useSort } from "@/composables/sort";
@@ -239,6 +198,15 @@ const userUpsert = useUserUpsert();
const { sortBy, handleSort, getSortField } = useSort();
const alertStore = useAlertStore();
// 定义表格列配置
const columns = [
{ title: '用户名', field: 'username', sortable: true },
{ title: '创建时间', field: 'createTime', sortable: true },
{ title: '状态', field: 'status' },
{ title: '分配', field: 'assign' },
{ title: '操作', field: 'actions' }
];
onMounted(async () => {
await fetchUsersWith({
username: username.value,