mirror of
https://github.com/ccmjga/zhilu-admin
synced 2026-03-13 21:27:19 +08:00
Abstract Form Components
This commit is contained in:
@@ -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>
|
||||
|
||||
101
frontend/src/components/MobileCardListWithCheckbox.vue
Normal file
101
frontend/src/components/MobileCardListWithCheckbox.vue
Normal 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>
|
||||
235
frontend/src/components/README.md
Normal file
235
frontend/src/components/README.md
Normal 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>
|
||||
```
|
||||
83
frontend/src/components/TableButton.vue
Normal file
83
frontend/src/components/TableButton.vue
Normal 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>
|
||||
188
frontend/src/components/TableFormLayout.vue
Normal file
188
frontend/src/components/TableFormLayout.vue
Normal 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>
|
||||
@@ -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 = [];
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"],
|
||||
) => {
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user