fix: c
Some checks failed
CI / Test (ubuntu-latest) (push) Has been cancelled
CI / Test (windows-latest) (push) Has been cancelled
CI / Lint (ubuntu-latest) (push) Has been cancelled
CI / Lint (windows-latest) (push) Has been cancelled
CI / Check (ubuntu-latest) (push) Has been cancelled
CI / Check (windows-latest) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
Deploy Website on push / Deploy Push Playground Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Docs Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Antd Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Element Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Naive Ftp (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
CI / CI OK (push) Has been cancelled
Deploy Website on push / Rerun on failure (push) Has been cancelled
Lock Threads / action (push) Has been cancelled
Issue Close Require / close-issues (push) Has been cancelled
Close stale issues / stale (push) Has been cancelled

This commit is contained in:
2025-12-24 14:42:57 +08:00
parent 08e79c60e7
commit 7743cdc29a
49 changed files with 2179 additions and 4575 deletions

View File

@@ -25,6 +25,37 @@ export namespace FeatureApi {
api_id?: string;
name?: string;
}
export interface FeatureExampleItem {
id: number;
feature_id: number;
api_id: string;
data: string;
create_time: string;
update_time: string;
}
export interface ConfigFeatureExampleRequest {
feature_id: number;
data: string;
}
export interface ConfigFeatureExampleResponse {
success: boolean;
}
export interface GetFeatureExampleRequest {
feature_id: number;
}
export interface GetFeatureExampleResponse {
id: number;
feature_id: number;
api_id: string;
data: string;
create_time: string;
update_time: string;
}
}
/**
@@ -72,10 +103,35 @@ async function deleteFeature(id: number) {
return requestClient.delete<{ success: boolean }>(`/feature/delete/${id}`);
}
/**
* 配置功能示例数据
* @param data 示例数据配置
*/
async function configFeatureExample(
data: FeatureApi.ConfigFeatureExampleRequest,
) {
return requestClient.post<FeatureApi.ConfigFeatureExampleResponse>(
'/feature/config-example',
data,
);
}
/**
* 获取功能示例数据
* @param featureId 功能ID
*/
async function getFeatureExample(featureId: number) {
return requestClient.get<FeatureApi.GetFeatureExampleResponse>(
`/feature/example/${featureId}`,
);
}
export {
configFeatureExample,
createFeature,
deleteFeature,
getFeatureDetail,
getFeatureExample,
getFeatureList,
updateFeature,
};

View File

@@ -0,0 +1,165 @@
import type { Recordable } from '@vben/types';
import { requestClient } from '#/api/request';
export namespace SystemApiApi {
export interface SystemApiItem {
id: number;
role_id?: number;
api_id?: number;
api_name: string;
api_code: string;
method: string;
url: string;
status: 0 | 1;
description?: string;
create_time?: string;
update_time?: string;
}
export interface SystemApi {
list: SystemApiItem[];
total: number;
}
export interface SystemApiAllResponse {
items: SystemApiItem[];
}
export interface SystemRoleApiResponse {
items: null | SystemApiItem[];
}
export interface RoleApiItem {
id: number;
role_id: number;
api_id: number;
api_name: string;
api_code: string;
method: string;
url: string;
status: 0 | 1;
description?: string;
}
export interface RoleApi {
list: RoleApiItem[];
}
}
/**
* 获取API列表数据
*/
async function getApiList(params: Recordable<any>) {
return requestClient.get<SystemApiApi.SystemApi>('/api/list', {
params,
});
}
/**
* 获取API详情
* @param id API ID
*/
async function getApiDetail(id: number) {
return requestClient.get<SystemApiApi.SystemApiItem>(`/api/detail/${id}`);
}
/**
* 创建API
* @param data API数据
*/
async function createApi(
data: Omit<SystemApiApi.SystemApiItem, 'create_time' | 'id' | 'update_time'>,
) {
return requestClient.post('/api/create', data);
}
/**
* 更新API
* @param id API ID
* @param data API数据
*/
async function updateApi(
id: number,
data: Omit<SystemApiApi.SystemApiItem, 'create_time' | 'id' | 'update_time'>,
) {
return requestClient.put(`/api/update/${id}`, data);
}
/**
* 删除API
* @param id API ID
*/
async function deleteApi(id: number) {
return requestClient.delete(`/api/delete/${id}`);
}
/**
* 批量更新API状态
* @param data.ids API ID数组
* @param data.status 状态值
*/
async function batchUpdateApiStatus(data: { ids: number[]; status: 0 | 1 }) {
return requestClient.put('/api/batch-update-status', data);
}
/**
* 获取角色API权限列表
* @param roleId 角色ID
*/
async function getRoleApiList(roleId: number) {
return requestClient.get<SystemApiApi.SystemRoleApiResponse>(
`/role/${roleId}/api/list`,
);
}
/**
* 分配角色API权限
* @param data.api_ids API ID数组
* @param data.role_id 角色ID
*/
async function assignRoleApi(data: { api_ids: number[]; role_id: number }) {
return requestClient.post('/role/api/assign', data);
}
/**
* 移除角色API权限
* @param data.api_ids API ID数组
* @param data.role_id 角色ID
*/
async function removeRoleApi(data: { api_ids: number[]; role_id: number }) {
return requestClient.post('/role/api/remove', data);
}
/**
* 更新角色API权限全量更新
* @param data.api_ids API ID数组
* @param data.role_id 角色ID
*/
async function updateRoleApi(data: { api_ids: number[]; role_id: number }) {
return requestClient.put('/role/api/update', data);
}
/**
* 获取所有API列表用于权限分配
* @param params.status 状态过滤
*/
async function getAllApiList(params?: { status?: number }) {
return requestClient.get<SystemApiApi.SystemApiAllResponse>('/api/all', {
params,
});
}
export {
assignRoleApi,
batchUpdateApiStatus,
createApi,
deleteApi,
getAllApiList,
getApiDetail,
getApiList,
getRoleApiList,
removeRoleApi,
updateApi,
updateRoleApi,
};

View File

@@ -1,3 +1,4 @@
export * from './api';
export * from './dept';
export * from './menu';
export * from './role';

View File

@@ -51,4 +51,14 @@ async function deleteUser(id: string) {
return requestClient.delete(`/user/delete/${id}`);
}
export { createUser, deleteUser, getUserList, updateUser };
/**
* 重置用户密码
* @param id 用户 ID
* @param data 新密码数据
* @param data.password 新密码
*/
async function resetPassword(id: string, data: { password: string }) {
return requestClient.post(`/reset-password/${id}`, data);
}
export { createUser, deleteUser, getUserList, resetPassword, updateUser };

View File

@@ -61,10 +61,27 @@
"operation": "Operation",
"permissions": "Permissions",
"setPermissions": "Permissions",
"setApiPermissions": "API Permissions",
"roleCode": "Role Code",
"description": "Description"
},
"api": {
"title": "API Management",
"list": "API List",
"name": "API",
"apiName": "API Name",
"apiCode": "API Code",
"method": "Request Method",
"url": "API URL",
"status": "Status",
"description": "Description",
"createTime": "Create Time",
"operation": "Operation",
"permissions": "Permissions",
"setPermissions": "Set Permissions"
},
"user": {
"title": "User Management",
"name": "User",
"list": "User List",
"userName": "Username",
@@ -72,6 +89,11 @@
"status": "Status",
"setPermissions": "Set Permissions",
"createTime": "Create Time",
"operation": "Operation"
"operation": "Operation",
"resetPassword": "Reset Password",
"newPassword": "New Password",
"confirmPassword": "Confirm Password",
"confirmPasswordRequired": "Please confirm password",
"passwordMismatch": "The two passwords do not match"
}
}

View File

@@ -62,11 +62,28 @@
"operation": "操作",
"permissions": "权限",
"setPermissions": "授权",
"setApiPermissions": "API权限",
"roleCode": "角色编号",
"description": "描述"
},
"title": "系统管理",
"api": {
"title": "API管理",
"list": "API列表",
"name": "API",
"apiName": "API名称",
"apiCode": "API编码",
"method": "请求方法",
"url": "API地址",
"status": "状态",
"description": "描述",
"createTime": "创建时间",
"operation": "操作",
"permissions": "权限",
"setPermissions": "设置权限"
},
"user": {
"title": "用户管理",
"name": "用户",
"list": "用户列表",
"userName": "用户名",
@@ -74,6 +91,11 @@
"status": "状态",
"setPermissions": "设置权限",
"createTime": "创建时间",
"operation": "操作"
"operation": "操作",
"resetPassword": "重置密码",
"newPassword": "新密码",
"confirmPassword": "确认密码",
"confirmPasswordRequired": "请确认密码",
"passwordMismatch": "两次输入的密码不一致"
}
}

View File

@@ -0,0 +1,64 @@
import type { RouteRecordRaw } from 'vue-router';
import { $t } from '#/locales';
const routes: RouteRecordRaw[] = [
{
meta: {
icon: 'ion:settings-outline',
order: 9997,
title: $t('system.title'),
},
name: 'System',
path: '/system',
children: [
{
path: '/system/user',
name: 'SystemUser',
meta: {
icon: 'mdi:account',
title: $t('system.user.title'),
},
component: () => import('#/views/system/user/list.vue'),
},
{
path: '/system/role',
name: 'SystemRole',
meta: {
icon: 'mdi:account-group',
title: $t('system.role.title'),
},
component: () => import('#/views/system/role/list.vue'),
},
{
path: '/system/api',
name: 'SystemApi',
meta: {
icon: 'mdi:api',
title: $t('system.api.title'),
},
component: () => import('#/views/system/api/list.vue'),
},
{
path: '/system/menu',
name: 'SystemMenu',
meta: {
icon: 'mdi:menu',
title: $t('system.menu.title'),
},
component: () => import('#/views/system/menu/list.vue'),
},
{
path: '/system/dept',
name: 'SystemDept',
meta: {
icon: 'charm:organisation',
title: $t('system.dept.title'),
},
component: () => import('#/views/system/dept/list.vue'),
},
],
},
];
export default routes;

View File

@@ -69,12 +69,26 @@ export function useColumns<T = FeatureApi.FeatureItem>(
nameTitle: '模块',
onClick: onActionClick,
},
options: [
{
code: 'edit',
text: '编辑',
},
{
code: 'delete',
text: '删除',
},
{
code: 'example',
text: '示例配置',
},
],
name: 'CellOperation',
},
field: 'operation',
fixed: 'right',
title: '操作',
width: 130,
width: 180,
},
];
}

View File

@@ -5,7 +5,7 @@ import type {
} from '#/adapter/vxe-table';
import type { FeatureApi } from '#/api/product-manage';
import { Page, useVbenDrawer } from '@vben/common-ui';
import { Page, useVbenDrawer, useVbenModal } from '@vben/common-ui';
import { Plus } from '@vben/icons';
import { Button, message } from 'ant-design-vue';
@@ -14,6 +14,7 @@ import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { deleteFeature, getFeatureList } from '#/api/product-manage';
import { useColumns, useGridFormSchema } from './data';
import ExampleConfig from './modules/example-config.vue';
import Form from './modules/form.vue';
// 表单抽屉
@@ -22,6 +23,12 @@ const [FormDrawer, formDrawerApi] = useVbenDrawer({
destroyOnClose: true,
});
// 示例配置弹窗
const [ExampleConfigModal, exampleConfigModalApi] = useVbenModal({
connectedComponent: ExampleConfig,
destroyOnClose: true,
});
// 表格配置
const [Grid, gridApi] = useVbenVxeGrid({
formOptions: {
@@ -67,6 +74,10 @@ function onActionClick(e: OnActionClickParams<FeatureApi.FeatureItem>) {
onEdit(e.row);
break;
}
case 'example': {
onExampleConfig(e.row);
break;
}
}
}
@@ -104,11 +115,17 @@ function onRefresh() {
function onCreate() {
formDrawerApi.setData({}).open();
}
// 示例配置处理
function onExampleConfig(row: FeatureApi.FeatureItem) {
exampleConfigModalApi.setData(row).open();
}
</script>
<template>
<Page auto-content-height>
<FormDrawer @success="onRefresh" />
<ExampleConfigModal @success="onRefresh" />
<Grid table-title="模块列表">
<template #toolbar-tools>
<Button type="primary" @click="onCreate">

View File

@@ -0,0 +1,151 @@
<script lang="ts" setup>
import type { FeatureApi } from '#/api/product-manage';
import { computed, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { Button, Input, message } from 'ant-design-vue';
import { configFeatureExample, getFeatureExample } from '#/api/product-manage';
const emit = defineEmits(['success']);
const featureData = ref<FeatureApi.FeatureItem>();
const exampleData = ref('');
const loading = ref(false);
const saving = ref(false);
const [Modal, modalApi] = useVbenModal({
async onConfirm() {
if (!featureData.value) return;
saving.value = true;
try {
await configFeatureExample({
feature_id: featureData.value.id,
data: exampleData.value,
});
message.success('示例配置保存成功');
emit('success');
modalApi.close();
} catch (error) {
console.error('保存示例配置失败:', error);
} finally {
saving.value = false;
}
},
onOpenChange(isOpen) {
if (isOpen) {
const data = modalApi.getData<FeatureApi.FeatureItem>();
if (data) {
featureData.value = data;
loadExampleData();
}
} else {
featureData.value = undefined;
exampleData.value = '';
}
},
});
// 加载示例数据
async function loadExampleData() {
if (!featureData.value) return;
loading.value = true;
try {
const response = await getFeatureExample(featureData.value.id);
exampleData.value = response.data || '';
} catch (error) {
console.error('获取示例数据失败:', error);
exampleData.value = '';
} finally {
loading.value = false;
}
}
// 格式化JSON
function formatJson() {
try {
if (exampleData.value) {
const parsed = JSON.parse(exampleData.value);
exampleData.value = JSON.stringify(parsed, null, 2);
}
} catch {
message.error('JSON格式错误无法格式化');
}
}
// 清空数据
function clearData() {
exampleData.value = '';
}
const modalTitle = computed(() => {
return featureData.value
? `示例配置 - ${featureData.value.name}`
: '示例配置';
});
const isJsonValid = computed(() => {
if (!exampleData.value) return true;
try {
JSON.parse(exampleData.value);
return true;
} catch {
return false;
}
});
</script>
<template>
<Modal :title="modalTitle" class="w-[1200px]">
<div class="space-y-4">
<div>
<div class="mb-2 flex items-center justify-between">
<label class="text-sm font-medium">示例数据 (JSON格式)</label>
<div class="space-x-2">
<Button size="small" @click="formatJson">格式化</Button>
<Button size="small" @click="clearData">清空</Button>
</div>
</div>
<Input.TextArea
v-model:value="exampleData"
:loading="loading"
:rows="15"
:status="isJsonValid ? undefined : 'error'"
placeholder="请输入JSON格式的示例数据..."
class="font-mono text-sm"
/>
<div v-if="!isJsonValid" class="mt-1 text-xs text-red-500">
JSON格式错误请检查语法
</div>
</div>
<div class="text-xs text-gray-500">
<p>提示</p>
<ul class="list-disc space-y-1 pl-4">
<li>如果当前功能没有配置示例数据输入框将为空</li>
<li>可以在此输入JSON格式的示例数据</li>
<li>点击"格式化"按钮可以美化JSON格式</li>
<li>保存后该示例数据将用于功能展示</li>
</ul>
</div>
</div>
<template #footer>
<div class="flex justify-end space-x-2">
<Button @click="modalApi.close">取消</Button>
<Button
type="primary"
:loading="saving"
:disabled="!isJsonValid"
@click="modalApi.onConfirm"
>
保存
</Button>
</div>
</template>
</Modal>
</template>

View File

@@ -0,0 +1,178 @@
import type { VbenFormSchema } from '#/adapter/form';
import type { OnActionClickFn, VxeTableGridOptions } from '#/adapter/vxe-table';
import type { SystemApiApi } from '#/api';
import { $t } from '#/locales';
export function useFormSchema(): VbenFormSchema[] {
return [
{
component: 'Input',
fieldName: 'api_name',
label: $t('system.api.apiName'),
rules: 'required',
},
{
component: 'Input',
fieldName: 'api_code',
label: $t('system.api.apiCode'),
rules: 'required',
},
{
component: 'Select',
componentProps: {
allowClear: true,
class: 'w-full',
placeholder: '请选择请求方法',
options: [
{ label: 'GET', value: 'GET' },
{ label: 'POST', value: 'POST' },
{ label: 'PUT', value: 'PUT' },
{ label: 'DELETE', value: 'DELETE' },
],
},
fieldName: 'method',
label: $t('system.api.method'),
rules: 'required',
},
{
component: 'Input',
fieldName: 'url',
label: $t('system.api.url'),
rules: 'required',
},
{
component: 'RadioGroup',
componentProps: {
buttonStyle: 'solid',
options: [
{ label: $t('common.enabled'), value: 1 },
{ label: $t('common.disabled'), value: 0 },
],
optionType: 'button',
},
defaultValue: 1,
fieldName: 'status',
label: $t('system.api.status'),
},
{
component: 'Textarea',
fieldName: 'description',
label: $t('system.api.description'),
},
];
}
export function useGridFormSchema(): VbenFormSchema[] {
return [
{
component: 'Input',
fieldName: 'api_name',
label: $t('system.api.apiName'),
},
{
component: 'Input',
fieldName: 'api_code',
label: $t('system.api.apiCode'),
},
{
component: 'Select',
componentProps: {
allowClear: true,
class: 'w-full',
options: [
{ label: 'GET', value: 'GET' },
{ label: 'POST', value: 'POST' },
{ label: 'PUT', value: 'PUT' },
{ label: 'DELETE', value: 'DELETE' },
],
},
fieldName: 'method',
label: $t('system.api.method'),
},
{
component: 'Select',
componentProps: {
allowClear: true,
class: 'w-full',
options: [
{ label: $t('common.enabled'), value: 1 },
{ label: $t('common.disabled'), value: 0 },
],
},
fieldName: 'status',
label: $t('system.api.status'),
},
{
component: 'RangePicker',
fieldName: 'create_time',
label: $t('system.api.createTime'),
},
];
}
export function useColumns<T = SystemApiApi.SystemApiItem>(
onActionClick: OnActionClickFn<T>,
onStatusChange?: (newStatus: any, row: T) => PromiseLike<boolean | undefined>,
): VxeTableGridOptions['columns'] {
return [
{
field: 'api_name',
title: $t('system.api.apiName'),
width: 200,
},
{
field: 'api_code',
title: $t('system.api.apiCode'),
width: 200,
},
{
field: 'method',
title: $t('system.api.method'),
width: 100,
},
{
field: 'url',
title: $t('system.api.url'),
minWidth: 200,
},
{
cellRender: {
attrs: { beforeChange: onStatusChange },
name: onStatusChange ? 'CellSwitch' : 'CellTag',
},
field: 'status',
title: $t('system.api.status'),
width: 100,
},
{
field: 'description',
minWidth: 150,
title: $t('system.api.description'),
},
{
field: 'create_time',
title: $t('system.api.createTime'),
width: 200,
},
{
align: 'center',
cellRender: {
attrs: {
nameField: 'api_name',
nameTitle: $t('system.api.apiName'),
onClick: onActionClick,
},
name: 'CellOperation',
options: [
'edit', // 默认的编辑按钮
'delete', // 默认的删除按钮
],
},
field: 'operation',
fixed: 'right',
title: $t('system.api.operation'),
width: 130,
},
];
}

View File

@@ -0,0 +1,171 @@
<script lang="ts" setup>
import type { Recordable } from '@vben/types';
import type {
OnActionClickParams,
VxeTableGridOptions,
} from '#/adapter/vxe-table';
import type { SystemApiApi } from '#/api';
import { Page, useVbenDrawer } from '@vben/common-ui';
import { Plus } from '@vben/icons';
import { Button, message, Modal } from 'ant-design-vue';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { deleteApi, getApiList, updateApi } from '#/api';
import { $t } from '#/locales';
import { useColumns, useGridFormSchema } from './data';
import Form from './modules/form.vue';
const [FormDrawer, formDrawerApi] = useVbenDrawer({
connectedComponent: Form,
destroyOnClose: true,
});
const [Grid, gridApi] = useVbenVxeGrid({
formOptions: {
fieldMappingTime: [['create_time', ['startTime', 'endTime']]],
schema: useGridFormSchema(),
submitOnChange: true,
},
gridOptions: {
columns: useColumns(onActionClick, onStatusChange),
height: 'auto',
keepSource: true,
proxyConfig: {
ajax: {
query: async ({ page }, formValues) => {
return await getApiList({
page: page.currentPage,
pageSize: page.pageSize,
...formValues,
});
},
},
props: {
result: 'list',
total: 'total',
},
autoLoad: true,
},
rowConfig: {
keyField: 'id',
},
toolbarConfig: {
custom: true,
export: false,
refresh: { code: 'query' },
search: true,
zoom: true,
},
} as VxeTableGridOptions<SystemApiApi.SystemApiItem>,
});
function onActionClick(e: OnActionClickParams<SystemApiApi.SystemApiItem>) {
switch (e.code) {
case 'delete': {
onDelete(e.row);
break;
}
case 'edit': {
onEdit(e.row);
break;
}
}
}
/**
* 将Antd的Modal.confirm封装为promise方便在异步函数中调用。
* @param content 提示内容
* @param title 提示标题
*/
function confirm(content: string, title: string) {
return new Promise((reslove, reject) => {
Modal.confirm({
content,
onCancel() {
reject(new Error('已取消'));
},
onOk() {
reslove(true);
},
title,
});
});
}
/**
* 状态开关即将改变
* @param newStatus 期望改变的状态值
* @param row 行数据
* @returns 返回false则中止改变返回其他值undefined、true则允许改变
*/
async function onStatusChange(
newStatus: number,
row: SystemApiApi.SystemApiItem,
) {
const status: Recordable<string> = {
0: '禁用',
1: '启用',
};
try {
await confirm(
`你要将${row.api_name}的状态切换为 【${status[newStatus.toString()]}】 吗?`,
`切换状态`,
);
await updateApi(row.id, {
...row,
status: newStatus as 0 | 1,
});
return true;
} catch {
return false;
}
}
function onEdit(row: SystemApiApi.SystemApiItem) {
formDrawerApi.setData(row).open();
}
function onDelete(row: SystemApiApi.SystemApiItem) {
const hideLoading = message.loading({
content: $t('ui.actionMessage.deleting', [row.api_name]),
duration: 0,
key: 'action_process_msg',
});
deleteApi(row.id)
.then(() => {
message.success({
content: $t('ui.actionMessage.deleteSuccess', [row.api_name]),
key: 'action_process_msg',
});
onRefresh();
})
.catch(() => {
hideLoading();
});
}
function onRefresh() {
gridApi.query();
}
function onCreate() {
formDrawerApi.setData({}).open();
}
</script>
<template>
<Page auto-content-height>
<FormDrawer @success="onRefresh" />
<Grid :table-title="$t('system.api.list')">
<template #toolbar-tools>
<Button type="primary" @click="onCreate">
<Plus class="size-5" />
{{ $t('ui.actionTitle.create', [$t('system.api.name')]) }}
</Button>
</template>
</Grid>
</Page>
</template>

View File

@@ -0,0 +1,77 @@
<script lang="ts" setup>
import type { SystemApiApi } from '#/api';
import { computed, ref } from 'vue';
import { useVbenDrawer, useVbenForm } from '@vben/common-ui';
import { createApi, updateApi } from '#/api';
import { $t } from '#/locales';
import { useFormSchema } from '../data';
const emit = defineEmits<{
success: [];
}>();
const formData = ref<SystemApiApi.SystemApiItem>();
const schema = useFormSchema();
const [Form, formApi] = useVbenForm({
commonConfig: {
colon: true,
formItemClass: 'col-span-2 md:col-span-1',
},
schema,
showDefaultActions: false,
wrapperClass: 'grid-cols-2 gap-x-4',
});
const [Drawer, drawerApi] = useVbenDrawer({
onConfirm: onSubmit,
onOpenChange(isOpen) {
if (isOpen) {
const data = drawerApi.getData<SystemApiApi.SystemApiItem>();
if (data) {
formData.value = data;
formApi.setValues(formData.value);
} else {
formApi.resetForm();
}
}
},
});
async function onSubmit() {
const { valid } = await formApi.validate();
if (valid) {
drawerApi.lock();
const data =
await formApi.getValues<
Omit<SystemApiApi.SystemApiItem, 'create_time' | 'id' | 'update_time'>
>();
try {
await (formData.value?.id
? updateApi(formData.value.id, data)
: createApi(data));
drawerApi.close();
emit('success');
} finally {
drawerApi.unlock();
}
}
}
const getDrawerTitle = computed(() =>
formData.value?.id
? $t('ui.actionTitle.edit', [$t('system.api.name')])
: $t('ui.actionTitle.create', [$t('system.api.name')]),
);
</script>
<template>
<Drawer class="w-full max-w-[600px]" :title="getDrawerTitle">
<Form class="mx-4" />
</Drawer>
</template>

View File

@@ -80,7 +80,7 @@ export function useGridFormSchema(): VbenFormSchema[] {
];
}
export function useColumns<T = SystemRoleApi.SystemRole>(
export function useColumns<T = SystemRoleApi.SystemRoleItem>(
onActionClick: OnActionClickFn<T>,
onStatusChange?: (newStatus: any, row: T) => PromiseLike<boolean | undefined>,
): VxeTableGridOptions['columns'] {
@@ -123,11 +123,26 @@ export function useColumns<T = SystemRoleApi.SystemRole>(
onClick: onActionClick,
},
name: 'CellOperation',
options: [
{
code: 'edit',
text: $t('ui.actionTitle.edit', [$t('system.role.name')]),
},
{
code: 'api-permissions',
text: $t('system.role.setApiPermissions'),
},
{
code: 'delete',
text: $t('ui.actionTitle.delete', [$t('system.role.name')]),
danger: true,
},
],
},
field: 'operation',
fixed: 'right',
title: $t('system.role.operation'),
width: 130,
width: 200,
},
];
}

View File

@@ -17,6 +17,7 @@ import { deleteRole, getRoleList, updateRole } from '#/api';
import { $t } from '#/locales';
import { useColumns, useGridFormSchema } from './data';
import ApiPermissions from './modules/api-permissions.vue';
import Form from './modules/form.vue';
const [FormDrawer, formDrawerApi] = useVbenDrawer({
@@ -24,6 +25,11 @@ const [FormDrawer, formDrawerApi] = useVbenDrawer({
destroyOnClose: true,
});
const [ApiPermissionsDrawer, apiPermissionsDrawerApi] = useVbenDrawer({
connectedComponent: ApiPermissions,
destroyOnClose: true,
});
const [Grid, gridApi] = useVbenVxeGrid({
formOptions: {
fieldMappingTime: [['create_time', ['startTime', 'endTime']]],
@@ -56,11 +62,15 @@ const [Grid, gridApi] = useVbenVxeGrid({
search: true,
zoom: true,
},
} as VxeTableGridOptions<SystemRoleApi.SystemRole>,
} as VxeTableGridOptions<SystemRoleApi.SystemRoleItem>,
});
function onActionClick(e: OnActionClickParams<SystemRoleApi.SystemRole>) {
function onActionClick(e: OnActionClickParams<SystemRoleApi.SystemRoleItem>) {
switch (e.code) {
case 'api-permissions': {
onApiPermissions(e.row);
break;
}
case 'delete': {
onDelete(e.row);
break;
@@ -118,11 +128,11 @@ async function onStatusChange(
}
}
function onEdit(row: SystemRoleApi.SystemRole) {
function onEdit(row: SystemRoleApi.SystemRoleItem) {
formDrawerApi.setData(row).open();
}
function onDelete(row: SystemRoleApi.SystemRole) {
function onDelete(row: SystemRoleApi.SystemRoleItem) {
const hideLoading = message.loading({
content: $t('ui.actionMessage.deleting', [row.role_name]),
duration: 0,
@@ -148,10 +158,15 @@ function onRefresh() {
function onCreate() {
formDrawerApi.setData({}).open();
}
function onApiPermissions(row: SystemRoleApi.SystemRoleItem) {
apiPermissionsDrawerApi.setData(row).open();
}
</script>
<template>
<Page auto-content-height>
<FormDrawer @success="onRefresh" />
<ApiPermissionsDrawer @success="onRefresh" />
<Grid :table-title="$t('system.role.list')">
<template #toolbar-tools>
<Button type="primary" @click="onCreate">

View File

@@ -0,0 +1,214 @@
<script lang="ts" setup>
import type { SystemRoleApi } from '#/api';
import { computed, ref } from 'vue';
import { useVbenDrawer } from '@vben/common-ui';
import { Button, Checkbox, message, Spin } from 'ant-design-vue';
import { getAllApiList, getRoleApiList, updateRoleApi } from '#/api';
import { $t } from '#/locales';
const emits = defineEmits(['success']);
const loading = ref(false);
const allApiList = ref<any[]>([]);
const roleApiList = ref<any[]>([]);
const selectedApiIds = ref<number[]>([]);
const formData = ref<SystemRoleApi.SystemRoleItem>();
const roleId = ref<number>();
const [Drawer, drawerApi] = useVbenDrawer({
async onConfirm() {
await onSave();
},
onOpenChange(isOpen) {
if (isOpen) {
const data = drawerApi.getData<SystemRoleApi.SystemRoleItem>();
if (data) {
formData.value = data;
roleId.value = data.id;
fetchRoleApiList();
}
fetchAllApiList();
}
},
});
// 计算已选中的API ID
const selectedApiIdsSet = computed(() => new Set(selectedApiIds.value));
// 获取所有API列表
async function fetchAllApiList() {
try {
loading.value = true;
const response = await getAllApiList({ status: 1 }); // 只获取启用的API
allApiList.value = response.items || [];
} catch (error) {
console.error('获取API列表失败:', error);
message.error('获取API列表失败');
} finally {
loading.value = false;
}
}
// 获取角色已分配的API权限
async function fetchRoleApiList() {
if (!roleId.value) return;
try {
loading.value = true;
const response = await getRoleApiList(roleId.value);
roleApiList.value = response.items || [];
selectedApiIds.value = roleApiList.value.map((item) => item.api_id);
} catch (error) {
console.error('获取角色API权限失败:', error);
message.error('获取角色API权限失败');
} finally {
loading.value = false;
}
}
// 全选/取消全选
function toggleSelectAll(e: any) {
const checked = e.target.checked;
selectedApiIds.value = checked
? allApiList.value.map((api) => api.api_id)
: [];
}
// 判断是否全选
const isAllSelected = computed(() => {
return (
allApiList.value.length > 0 &&
selectedApiIds.value.length === allApiList.value.length
);
});
// 判断是否部分选中
const isIndeterminate = computed(() => {
return (
selectedApiIds.value.length > 0 &&
selectedApiIds.value.length < allApiList.value.length
);
});
// 保存API权限
async function onSave() {
if (!roleId.value) return;
try {
loading.value = true;
await updateRoleApi({
role_id: roleId.value,
api_ids: selectedApiIds.value,
});
message.success('API权限保存成功');
emits('success');
drawerApi.close();
} catch (error) {
console.error('保存API权限失败:', error);
message.error('保存API权限失败');
} finally {
loading.value = false;
}
}
// 重置选择
function onReset() {
selectedApiIds.value = roleApiList.value.map((item) => item.api_id);
}
// 计算抽屉标题
const getDrawerTitle = computed(() => {
return formData.value?.role_name
? `${$t('system.role.setApiPermissions')} - ${formData.value.role_name}`
: $t('system.role.setApiPermissions');
});
</script>
<template>
<Drawer :title="getDrawerTitle">
<div class="p-4">
<div class="mb-4">
<p class="text-sm text-gray-500">
为角色分配API访问权限勾选的API将被允许访问
</p>
</div>
<Spin :spinning="loading">
<div class="space-y-4">
<!-- 全选操作 -->
<div class="flex items-center gap-4 rounded-lg bg-gray-50 p-3">
<Checkbox
:checked="isAllSelected"
:indeterminate="isIndeterminate"
@change="toggleSelectAll"
>
全选
</Checkbox>
<span class="text-sm text-gray-500">
已选择 {{ selectedApiIds.length }} / {{ allApiList.length }} 个API
</span>
</div>
<!-- API列表 -->
<div class="max-h-96 overflow-y-auto rounded-lg border">
<div
v-for="api in allApiList"
:key="api.api_id"
class="flex items-center gap-3 border-b p-3 last:border-b-0 hover:bg-gray-50"
>
<Checkbox
:checked="selectedApiIdsSet.has(api.api_id)"
@change="
(e) => {
if (e.target.checked) {
selectedApiIds.push(api.api_id);
} else {
const index = selectedApiIds.indexOf(api.api_id);
if (index > -1) {
selectedApiIds.splice(index, 1);
}
}
}
"
/>
<div class="flex-1">
<div class="flex items-center gap-2">
<span class="font-medium">{{ api.api_name }}</span>
<span
class="rounded px-2 py-1 text-xs"
:class="{
'bg-blue-100 text-blue-800': api.method === 'GET',
'bg-green-100 text-green-800': api.method === 'POST',
'bg-orange-100 text-orange-800': api.method === 'PUT',
'bg-red-100 text-red-800': api.method === 'DELETE',
}"
>
{{ api.method }}
</span>
</div>
<div class="mt-1 text-sm text-gray-500">
{{ api.url }}
</div>
<div v-if="api.description" class="mt-1 text-xs text-gray-400">
{{ api.description }}
</div>
</div>
</div>
</div>
</div>
</Spin>
<!-- 操作按钮 -->
<div class="mt-6 flex justify-end gap-2">
<Button @click="onReset"> 重置 </Button>
<Button type="primary" @click="onSave" :loading="loading">
保存
</Button>
</div>
</div>
</Drawer>
</template>

View File

@@ -121,12 +121,17 @@ export function useColumns<T = SystemUserApi.SystemUser>(
nameTitle: $t('system.user.userName'),
onClick: onActionClick,
},
options: [
{ code: 'edit', text: '编辑' },
{ code: 'resetPassword', text: '重置密码' },
{ code: 'delete', text: '删除' },
],
name: 'CellOperation',
},
field: 'operation',
fixed: 'right',
title: $t('system.user.operation'),
width: 130,
width: 200,
},
];
}

View File

@@ -18,12 +18,18 @@ import { $t } from '#/locales';
import { useColumns, useGridFormSchema } from './data';
import Form from './modules/form.vue';
import ResetPasswordForm from './modules/reset-password-form.vue';
const [FormDrawer, formDrawerApi] = useVbenDrawer({
connectedComponent: Form,
destroyOnClose: true,
});
const [ResetPasswordDrawer, resetPasswordDrawerApi] = useVbenDrawer({
connectedComponent: ResetPasswordForm,
destroyOnClose: true,
});
const [Grid, gridApi] = useVbenVxeGrid({
formOptions: {
fieldMappingTime: [['create_time', ['startTime', 'endTime']]],
@@ -69,6 +75,10 @@ function onActionClick(e: OnActionClickParams<SystemUserApi.SystemUser>) {
onEdit(e.row);
break;
}
case 'resetPassword': {
onResetPassword(e.row);
break;
}
}
}
@@ -122,6 +132,10 @@ function onEdit(row: SystemUserApi.SystemUser) {
formDrawerApi.setData(row).open();
}
function onResetPassword(row: SystemUserApi.SystemUser) {
resetPasswordDrawerApi.setData(row).open();
}
function onDelete(row: SystemUserApi.SystemUser) {
const hideLoading = message.loading({
content: $t('ui.actionMessage.deleting', [row.username]),
@@ -152,6 +166,7 @@ function onCreate() {
<template>
<Page auto-content-height>
<FormDrawer />
<ResetPasswordDrawer @success="onRefresh" />
<Grid :table-title="$t('system.user.list')">
<template #toolbar-tools>
<Button type="primary" @click="onCreate">

View File

@@ -0,0 +1,83 @@
<script lang="ts" setup>
import type { SystemUserApi } from '#/api/system/user';
import { computed, ref } from 'vue';
import { useVbenDrawer, z } from '@vben/common-ui';
import { useVbenForm } from '#/adapter/form';
import { resetPassword } from '#/api/system/user';
const emits = defineEmits(['success']);
const formData = ref<SystemUserApi.SystemUser>();
const [Form, formApi] = useVbenForm({
schema: [
{
component: 'InputPassword',
fieldName: 'password',
label: '新密码',
rules: z.string().min(1, { message: '请输入新密码' }),
},
{
component: 'InputPassword',
fieldName: 'confirmPassword',
label: '确认密码',
dependencies: {
rules(values) {
const { password } = values;
return z
.string()
.min(1, { message: '请确认密码' })
.refine((value) => value === password, {
message: '两次输入的密码不一致',
});
},
triggerFields: ['password'],
},
},
],
showDefaultActions: false,
});
const id = ref();
const [Drawer, drawerApi] = useVbenDrawer({
async onConfirm() {
const { valid } = await formApi.validate();
if (!valid) return;
const values = await formApi.getValues();
drawerApi.lock();
resetPassword(id.value, { password: values.password })
.then(() => {
emits('success');
drawerApi.close();
})
.catch(() => {
drawerApi.unlock();
});
},
onOpenChange(isOpen) {
if (isOpen) {
const data = drawerApi.getData<SystemUserApi.SystemUser>();
formApi.resetForm();
if (data) {
formData.value = data;
id.value = data.id;
} else {
id.value = undefined;
}
}
},
});
const getDrawerTitle = computed(() => {
return '重置密码';
});
</script>
<template>
<Drawer :title="getDrawerTitle">
<Form />
</Drawer>
</template>