Merge remote-tracking branch 'yudao/dev' into dev-new

# Conflicts:
#	apps/web-antd/src/api/infra/codegen/index.ts
#	apps/web-antd/src/views/infra/codegen/data.ts
pull/111/head
puhui999 2025-05-20 14:13:14 +08:00
commit 074eb0e83d
96 changed files with 2168 additions and 1856 deletions

View File

@ -300,4 +300,5 @@ export type OnActionClickParams<T = Recordable<any>> = {
export type OnActionClickFn<T = Recordable<any>> = (
params: OnActionClickParams<T>,
) => void;
export * from '#/components/table-action';
export type * from '@vben/plugins/vxe-table';

View File

@ -47,6 +47,7 @@ export function deleteDemo01Contact(id: number) {
}
// 批量删除示例联系人
// TODO @puhui999注释风格哈。
export function deleteDemo01ContactByIds(ids: number[]) {
return requestClient.delete(
`/infra/demo01-contact/delete-batch?ids=${ids.join(',')}`,

View File

@ -0,0 +1,161 @@
// TODO @
<script lang="ts" setup>
import type { DataNode } from 'ant-design-vue/es/tree';
import type { SystemDeptApi } from '#/api/system/dept';
import { defineProps, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { handleTree } from '@vben/utils';
import { Button, Card, Col, Row, Tree } from 'ant-design-vue';
import { getSimpleDeptList } from '#/api/system/dept';
defineOptions({ name: 'DeptSelectModal' });
const props = withDefaults(
defineProps<{
//
cancelText?: string;
// checkable
checkStrictly?: boolean;
//
confirmText?: string;
//
multiple?: boolean;
//
title?: string;
}>(),
{
cancelText: '取消',
checkStrictly: false,
confirmText: '确认',
multiple: true,
title: '部门选择',
},
);
const emit = defineEmits<{
confirm: [deptList: SystemDeptApi.Dept[]];
}>();
//
const [Modal, modalApi] = useVbenModal({
title: props.title,
async onOpenChange(isOpen: boolean) {
if (!isOpen) {
resetData();
return;
}
modalApi.setState({ loading: true });
try {
deptData.value = await getSimpleDeptList();
deptTree.value = handleTree(deptData.value) as DataNode[];
} finally {
modalApi.setState({ loading: false });
}
},
destroyOnClose: true,
});
type checkedKeys = number[] | { checked: number[]; halfChecked: number[] };
//
const deptTree = ref<DataNode[]>([]);
// ID
const selectedDeptIds = ref<checkedKeys>([]);
//
const deptData = ref<SystemDeptApi.Dept[]>([]);
/** 打开对话框 */
const open = async (selectedList?: SystemDeptApi.Dept[]) => {
modalApi.open();
// //
if (selectedList?.length) {
const selectedIds = selectedList
.map((dept) => dept.id)
.filter((id): id is number => id !== undefined);
selectedDeptIds.value = props.checkStrictly
? {
checked: selectedIds,
halfChecked: [],
}
: selectedIds;
}
};
/** 处理选中状态变化 */
const handleCheck = () => {
if (!props.multiple) {
//
if (Array.isArray(selectedDeptIds.value)) {
const lastSelectedId =
selectedDeptIds.value[selectedDeptIds.value.length - 1];
if (lastSelectedId) {
selectedDeptIds.value = [lastSelectedId];
}
} else {
// checkStrictly true selectedDeptIds
const checked = selectedDeptIds.value.checked || [];
if (checked.length > 0) {
const lastSelectedId = checked[checked.length - 1];
selectedDeptIds.value = {
checked: [lastSelectedId!],
halfChecked: [],
};
}
}
}
};
/** 提交选择 */
const handleConfirm = async () => {
// ID
const selectedIds: number[] = Array.isArray(selectedDeptIds.value)
? selectedDeptIds.value
: selectedDeptIds.value.checked || [];
const deptArray = deptData.value.filter((dept) =>
selectedIds.includes(dept.id!),
);
//
await modalApi.close();
emit('confirm', deptArray);
};
const handleCancel = () => {
modalApi.close();
};
/** 重置数据 */
const resetData = () => {
deptTree.value = [];
selectedDeptIds.value = [];
};
/** 提供 open 方法,用于打开对话框 */
defineExpose({ open });
</script>
<template>
<Modal>
<Row class="h-full">
<Col :span="24">
<Card class="h-full">
<Tree
:tree-data="deptTree"
v-if="deptTree.length > 0"
v-model:checked-keys="selectedDeptIds"
:checkable="true"
:check-strictly="checkStrictly"
:field-names="{ title: 'name', key: 'id' }"
:default-expand-all="true"
@check="handleCheck"
/>
</Card>
</Col>
</Row>
<template #footer>
<Button @click="handleCancel">{{ cancelText }}</Button>
<Button type="primary" @click="handleConfirm">{{ confirmText }}</Button>
</template>
</Modal>
</template>

View File

@ -0,0 +1 @@
export { default as DeptSelectModal } from './dept-select-modal.vue';

View File

@ -0,0 +1,12 @@
export const ACTION_ICON = {
DOWNLOAD: 'lucide:download',
UPLOAD: 'lucide:upload',
ADD: 'lucide:plus',
EDIT: 'lucide:edit',
DELETE: 'lucide:trash',
REFRESH: 'lucide:refresh-cw',
SEARCH: 'lucide:search',
FILTER: 'lucide:filter',
MORE: 'lucide:ellipsis-vertical',
VIEW: 'lucide:eye',
};

View File

@ -1,2 +1,4 @@
export * from './icons';
export { default as TableAction } from './table-action.vue';
export * from './typing';

View File

@ -1,7 +1,5 @@
<!-- add by 星语参考 vben2 的方式增加 TableAction 组件 -->
<script setup lang="ts">
import type { ButtonType } from 'ant-design-vue/es/button';
import type { PropType } from 'vue';
import type { ActionItem, PopConfirm } from './typing';
@ -69,8 +67,7 @@ const getActions = computed(() => {
.map((action) => {
const { popConfirm } = action;
return {
// getPopupContainer: document.body,
type: 'link' as ButtonType,
type: action.type || 'link',
...action,
...popConfirm,
onConfirm: popConfirm?.confirm,
@ -135,7 +132,7 @@ function handleMenuClick(e: any) {
</script>
<template>
<div class="m-table-action">
<div class="table-actions">
<Space
:size="
getActions?.some((item: ActionItem) => item.type === 'link') ? 0 : 8
@ -184,10 +181,10 @@ function handleMenuClick(e: any) {
<Dropdown v-if="getDropdownList.length > 0" :trigger="['hover']">
<slot name="more">
<Button size="small" type="link">
<Button :type="getDropdownList[0].type">
<template #icon>
{{ $t('page.action.more') }}
<IconifyIcon class="icon-more" icon="ant-design:more-outlined" />
<IconifyIcon icon="lucide:ellipsis-vertical" />
</template>
</Button>
</slot>
@ -230,8 +227,8 @@ function handleMenuClick(e: any) {
</div>
</template>
<style lang="scss">
.m-table-action {
.ant-btn {
.table-actions {
.ant-btn-link {
padding: 4px;
margin-left: 0;
}

View File

@ -1,4 +1,7 @@
import type { ButtonProps } from 'ant-design-vue/es/button/buttonTypes';
import type {
ButtonProps,
ButtonType,
} from 'ant-design-vue/es/button/buttonTypes';
import type { TooltipProps } from 'ant-design-vue/es/tooltip/Tooltip';
export interface PopConfirm {
@ -13,6 +16,7 @@ export interface PopConfirm {
export interface ActionItem extends ButtonProps {
onClick?: () => void;
type?: ButtonType;
label?: string;
color?: 'error' | 'success' | 'warning';
icon?: string;

View File

@ -1,6 +1,7 @@
<script setup lang="ts">
import type { BpmCategoryApi } from '#/api/bpm/category';
import type { BpmProcessDefinitionApi } from '#/api/bpm/definition';
import type { BpmFormApi } from '#/api/bpm/form';
import type { SystemDeptApi } from '#/api/system/dept';
import type { SystemUserApi } from '#/api/system/user';
@ -12,10 +13,11 @@ import { useTabs } from '@vben/hooks';
import { ArrowLeft } from '@vben/icons';
import { useUserStore } from '@vben/stores';
import { Button, message } from 'ant-design-vue';
import { Button, Card, message } from 'ant-design-vue';
import { getCategorySimpleList } from '#/api/bpm/category';
import { getProcessDefinition } from '#/api/bpm/definition';
import { getFormSimpleList } from '#/api/bpm/form';
import {
createModel,
deployModel,
@ -26,10 +28,12 @@ import { getSimpleDeptList } from '#/api/system/dept';
import { getSimpleUserList } from '#/api/system/user';
import BasicInfo from './modules/basic-info.vue';
import FormDesign from './modules/form-design.vue';
defineOptions({ name: 'BpmModelCreate' });
// TODO apps 使 @utils/constant.ts @
// TODO @jason/Users/yunai/Java/yudao-ui-admin-vben-v5/apps/web-antd/src/utils/constants.ts apps
const BpmModelType = {
BPMN: 10, // BPMN
SIMPLE: 20, //
@ -62,7 +66,9 @@ const route = useRoute();
const userStore = useUserStore();
//
const basicInfoRef = ref();
const basicInfoRef = ref<InstanceType<typeof BasicInfo>>();
//
const formDesignRef = ref<InstanceType<typeof FormDesign>>();
/** 步骤校验函数 */
const validateBasic = async () => {
@ -71,7 +77,7 @@ const validateBasic = async () => {
/** 表单设计校验 */
const validateForm = async () => {
// TODO
await formDesignRef.value?.validate();
};
/** 流程设计校验 */
@ -132,7 +138,7 @@ provide('processData', processData);
provide('modelData', formData);
//
// const formList = ref([])
const formList = ref<BpmFormApi.FormVO[]>([]);
const categoryList = ref<BpmCategoryApi.CategoryVO[]>([]);
const userList = ref<SystemUserApi.User[]>([]);
const deptList = ref<SystemDeptApi.Dept[]>([]);
@ -187,8 +193,8 @@ const initData = async () => {
formData.value.managerUserIds.push(userStore.userInfo?.userId);
}
// TODO
// formList.value = await getFormSimpleList()
//
formList.value = await getFormSimpleList();
categoryList.value = await getCategorySimpleList();
//
userList.value = await getSimpleUserList();
@ -393,9 +399,8 @@ onMounted(async () => {
/** 添加组件卸载前的清理 */
onBeforeUnmount(() => {
//
basicInfoRef.value = null;
// TODO
// formDesignRef.value = null;
basicInfoRef.value = undefined;
formDesignRef.value = undefined;
// processDesignRef.value = null;
});
</script>
@ -467,27 +472,34 @@ onBeforeUnmount(() => {
</Button>
</div>
</div>
<!-- 主体内容 -->
<div class="mt-[50px]">
<!-- 第一步基本信息 -->
<div v-if="currentStep === 0" class="mx-auto w-[560px]">
<BasicInfo
v-model="formData"
:category-list="categoryList"
:user-list="userList"
:dept-list="deptList"
ref="basicInfoRef"
/>
<Card :body-style="{ padding: '10px' }" class="mb-4">
<div class="mt-[50px]">
<!-- 第一步基本信息 -->
<div v-if="currentStep === 0" class="mx-auto w-4/6">
<BasicInfo
v-model="formData"
:category-list="categoryList"
:user-list="userList"
:dept-list="deptList"
ref="basicInfoRef"
/>
</div>
<!-- 第二步表单设计 -->
<div v-show="currentStep === 1" class="mx-auto w-4/6">
<FormDesign
v-model="formData"
:form-list="formList"
ref="formDesignRef"
/>
</div>
<!-- 第三步流程设计 TODO -->
<!-- 第四步更多设置 TODO -->
<div v-show="currentStep === 3" class="mx-auto w-4/6"></div>
</div>
<!-- 第二步表单设计 TODO -->
<!-- 第三步流程设计 TODO -->
<!-- 第四步更多设置 TODO -->
<div v-show="currentStep === 3" class="mx-auto w-[700px]"></div>
</div>
</Card>
</div>
</Page>
</template>

View File

@ -2,13 +2,15 @@
import type { Rule } from 'ant-design-vue/es/form';
import type { SelectValue } from 'ant-design-vue/es/select';
import type { PropType } from 'vue';
import type { BpmCategoryApi } from '#/api/bpm/category';
import type { SystemDeptApi } from '#/api/system/dept';
import type { SystemUserApi } from '#/api/system/user';
import { ref, watch } from 'vue';
import { IconifyIcon, Plus, ShieldQuestion, X } from '@vben/icons';
import { CircleHelp, IconifyIcon, Plus, X } from '@vben/icons';
import {
Avatar,
@ -20,18 +22,25 @@ import {
Tooltip,
} from 'ant-design-vue';
import { DeptSelectModal } from '#/components/dept-select-modal';
import { ImageUpload } from '#/components/upload';
import { UserSelectModal } from '#/components/user-select-modal';
import { DICT_TYPE, getBoolDictOptions, getIntDictOptions } from '#/utils';
const props = withDefaults(
defineProps<{
categoryList: BpmCategoryApi.CategoryVO[];
deptList: SystemDeptApi.Dept[];
userList: SystemUserApi.User[];
}>(),
{},
);
const props = defineProps({
categoryList: {
type: Array as PropType<BpmCategoryApi.CategoryVO[]>,
required: true,
},
userList: {
type: Array as PropType<SystemUserApi.User[]>,
required: true,
},
deptList: {
type: Array as PropType<SystemDeptApi.Dept[]>,
required: true,
},
});
//
const formRef = ref();
@ -45,6 +54,7 @@ const selectedStartDepts = ref<SystemDeptApi.Dept[]>([]);
//
const selectedManagerUsers = ref<SystemUserApi.User[]>([]);
const userSelectFormRef = ref();
const deptSelectFormRef = ref();
const currentSelectType = ref<'manager' | 'start'>('start');
//
const selectedUsers = ref<number[]>();
@ -55,6 +65,7 @@ const rules: Record<string, Rule[]> = {
category: [{ required: true, message: '流程分类不能为空', trigger: 'blur' }],
type: [{ required: true, message: '流程类型不能为空', trigger: 'blur' }],
visible: [{ required: true, message: '是否可见不能为空', trigger: 'blur' }],
// TODO
managerUserIds: [
{ required: true, message: '流程管理员不能为空', trigger: 'blur' },
],
@ -99,8 +110,14 @@ const openStartUserSelect = () => {
/** 打开部门选择 */
const openStartDeptSelect = () => {
// TODO
console.warn('部门选择功能暂未实现');
deptSelectFormRef.value.open(selectedStartDepts.value);
};
/** 处理部门选择确认 */
const handleDeptSelectConfirm = (depts: SystemDeptApi.Dept[]) => {
modelData.value = {
...modelData.value,
startDeptIds: depts.map((d) => d.id),
};
};
/** 打开管理员选择 */
@ -203,9 +220,7 @@ const validate = async () => {
await formRef.value?.validate();
};
defineExpose({
validate,
});
defineExpose({ validate });
</script>
<template>
@ -213,8 +228,8 @@ defineExpose({
ref="formRef"
:model="modelData"
:rules="rules"
:label-col="{ span: 6 }"
:wrapper-col="{ span: 18 }"
:label-col="{ span: 4 }"
:wrapper-col="{ span: 20 }"
class="mt-5"
>
<Form.Item label="流程标识" name="key" class="mb-5">
@ -231,7 +246,7 @@ defineExpose({
"
placement="top"
>
<ShieldQuestion class="ml-1 text-gray-500" />
<CircleHelp class="ml-1 size-5 text-gray-900" />
</Tooltip>
</div>
</Form.Item>
@ -343,7 +358,7 @@ defineExpose({
:key="dept.id"
class="relative flex h-9 items-center rounded-full bg-gray-100 pr-2"
>
<IconifyIcon icon="mdi:building-outline" class="size-5" />
<IconifyIcon icon="ep:office-building" class="size-6 px-1" />
{{ dept.name }}
<X
class="ml-2 size-4 cursor-pointer text-gray-400 hover:text-red-500"
@ -408,6 +423,13 @@ defineExpose({
@closed="handleUserSelectClosed"
@cancel="handleUserSelectCancel"
/>
<!-- 部门选择对话框 -->
<DeptSelectModal
ref="deptSelectFormRef"
title="发起人部门选择"
:check-strictly="true"
@confirm="handleDeptSelectConfirm"
/>
</template>
<style lang="scss" scoped>

View File

@ -0,0 +1,187 @@
<script lang="ts" setup>
import type { Rule } from 'ant-design-vue/es/form';
import type { BpmFormApi } from '#/api/bpm/form';
import { ref, watch } from 'vue';
import { CircleHelp } from '@vben/icons';
import FormCreate from '@form-create/ant-design-vue';
import {
Form,
FormItem,
Input,
Radio,
RadioGroup,
Select,
SelectOption,
Tooltip,
} from 'ant-design-vue';
import { getFormDetail } from '#/api/bpm/form';
import {
BpmModelFormType,
DICT_TYPE,
getDictOptions,
setConfAndFields2,
} from '#/utils';
const props = defineProps({
formList: {
type: Array<BpmFormApi.FormVO>,
required: true,
},
});
const formRef = ref();
//
const modelData = defineModel<any>();
//
const formPreview = ref({
formData: {} as any,
rule: [],
option: {
submitBtn: false,
resetBtn: false,
formData: {},
},
});
/** 监听表单ID变化加载表单数据 */
watch(
() => modelData.value.formId,
async (newFormId) => {
if (newFormId && modelData.value.formType === BpmModelFormType.NORMAL) {
const data = await getFormDetail(newFormId);
setConfAndFields2(formPreview.value, data.conf, data.fields);
//
formPreview.value.rule.forEach((item: any) => {
item.props = { ...item.props, disabled: true };
});
} else {
formPreview.value.rule = [];
}
},
{ immediate: true },
);
const rules: Record<string, Rule[]> = {
formType: [{ required: true, message: '表单类型不能为空', trigger: 'blur' }],
formId: [{ required: true, message: '流程表单不能为空', trigger: 'blur' }],
formCustomCreatePath: [
{ required: true, message: '表单提交路由不能为空', trigger: 'blur' },
],
formCustomViewPath: [
{ required: true, message: '表单查看地址不能为空', trigger: 'blur' },
],
};
/** 表单校验 */
const validate = async () => {
await formRef.value?.validate();
};
defineExpose({ validate });
</script>
<template>
<Form
ref="formRef"
:model="modelData"
:rules="rules"
:label-col="{ span: 4 }"
:wrapper-col="{ span: 20 }"
class="mt-5"
>
<FormItem label="表单类型" name="formType" class="mb-5">
<RadioGroup v-model:value="modelData.formType">
<Radio
v-for="dict in getDictOptions(
DICT_TYPE.BPM_MODEL_FORM_TYPE,
'number',
)"
:key="dict.value as string"
:value="dict.value"
>
{{ dict.label }}
</Radio>
</RadioGroup>
</FormItem>
<FormItem
v-if="modelData.formType === BpmModelFormType.NORMAL"
label="流程表单"
name="formId"
class="mb-5"
>
<Select v-model:value="modelData.formId" clearable>
<SelectOption
v-for="form in props.formList"
:key="form.id"
:value="form.id"
>
{{ form.name }}
</SelectOption>
>
</Select>
</FormItem>
<FormItem
v-if="modelData.formType === BpmModelFormType.CUSTOM"
label="表单提交路由"
name="formCustomCreatePath"
class="mb-5"
>
<div class="flex items-center">
<Input
v-model:value="modelData.formCustomCreatePath"
placeholder="请输入表单提交路由"
/>
<Tooltip
title="自定义表单的提交路径,使用 Vue 的路由地址, 例如说: bpm/oa/leave/create.vue"
placement="top"
>
<CircleHelp class="ml-1 size-5 text-gray-900" />
</Tooltip>
</div>
</FormItem>
<FormItem
v-if="modelData.formType === BpmModelFormType.CUSTOM"
label="表单查看地址"
name="formCustomViewPath"
class="mb-5"
>
<div class="flex items-center">
<Input
v-model:value="modelData.formCustomViewPath"
placeholder="请输入表单查看的组件地址"
/>
<Tooltip
title="自定义表单的查看组件地址,使用 Vue 的组件地址例如说bpm/oa/leave/detail.vue"
placement="top"
>
<CircleHelp class="ml-1 size-5 text-gray-900" />
</Tooltip>
</div>
</FormItem>
<!-- 表单预览 -->
<div
v-if="
modelData.formType === BpmModelFormType.NORMAL &&
modelData.formId &&
formPreview.rule.length > 0
"
class="mb-5 mt-7 rounded-sm border border-solid border-gray-200 p-5"
>
<div class="mb-[15px] flex items-center">
<div class="mr-[10px] h-[15px] w-[4px] bg-[#1890ff]"></div>
<span class="text-[15px] font-bold">表单预览</span>
</div>
<FormCreate
v-model:api="formPreview.formData"
:rule="formPreview.rule"
:option="formPreview.option"
/>
</div>
</Form>
</template>

View File

@ -162,6 +162,7 @@ const handleCategorySortSubmit = async () => {
<template>
<Page auto-content-height>
<!-- TODO @jaosn没头像的图标展示文字头像哈 -->
<!-- 流程分类表单弹窗 -->
<CategoryFormModal @success="getList" />
<Card

View File

@ -151,6 +151,7 @@ export function useGridColumns(
minWidth: 200,
align: 'center',
fixed: 'right',
// TODO @puhui999headerAlign 要使用 headerAlign: 'center' 么?看着现在分成了 align 和 headerAlign 两种
headerAlign: 'center',
showOverflow: false,
cellRender: {

View File

@ -61,6 +61,7 @@ async function onDelete(row: Demo01ContactApi.Demo01Contact) {
}
}
// TODO @puhui999:1/** */ deleteIds 2showDeleteBatchBtn disabled
const deleteIds = ref<number[]>([]); // ID
const showDeleteBatchBtn = computed(() => isEmpty(deleteIds.value));
function setDeleteIds({

View File

@ -24,7 +24,7 @@ function onRefresh() {
}
/** 查询 IP */
function onQueryIp() {
function handleQueryIp() {
formModalApi.setData(null).open();
}
@ -66,7 +66,7 @@ const [Grid, gridApi] = useVbenVxeGrid({
<FormModal @success="onRefresh" />
<Grid table-title="">
<template #toolbar-tools>
<Button type="primary" @click="onQueryIp">
<Button type="primary" @click="handleQueryIp">
<Search class="size-5" />
IP 查询
</Button>

View File

@ -1,8 +1,7 @@
import type { VbenFormSchema } from '#/adapter/form';
import type { OnActionClickFn, VxeTableGridOptions } from '#/adapter/vxe-table';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { SystemDeptApi } from '#/api/system/dept';
import { useAccess } from '@vben/access';
import { handleTree } from '@vben/utils';
import { z } from '#/adapter/form';
@ -10,8 +9,6 @@ import { getDeptList } from '#/api/system/dept';
import { getSimpleUserList } from '#/api/system/user';
import { CommonStatusEnum, DICT_TYPE, getDictOptions } from '#/utils';
const { hasAccessByCodes } = useAccess();
/** 新增/修改的表单 */
export function useFormSchema(): VbenFormSchema[] {
return [
@ -113,7 +110,6 @@ export function useFormSchema(): VbenFormSchema[] {
/** 列表的字段 */
export function useGridColumns(
onActionClick?: OnActionClickFn<SystemDeptApi.Dept>,
getLeaderName?: (userId: number) => string | undefined,
): VxeTableGridOptions<SystemDeptApi.Dept>['columns'] {
return [
@ -154,39 +150,10 @@ export function useGridColumns(
formatter: 'formatDateTime',
},
{
field: 'operation',
title: '操作',
minWidth: 200,
align: 'right',
width: 220,
fixed: 'right',
headerAlign: 'center',
showOverflow: false,
cellRender: {
attrs: {
nameField: 'name',
nameTitle: '部门',
onClick: onActionClick,
},
name: 'CellOperation',
options: [
{
code: 'append',
text: '新增下级',
show: hasAccessByCodes(['system:dept:create']),
},
{
code: 'edit',
show: hasAccessByCodes(['system:dept:update']),
},
{
code: 'delete',
show: hasAccessByCodes(['system:dept:delete']),
disabled: (row: SystemDeptApi.Dept) => {
return !!(row.children && row.children.length > 0);
},
},
],
},
slots: { default: 'actions' },
},
];
}

View File

@ -1,19 +1,15 @@
<script lang="ts" setup>
import type {
OnActionClickParams,
VxeTableGridOptions,
} from '#/adapter/vxe-table';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { SystemDeptApi } from '#/api/system/dept';
import type { SystemUserApi } from '#/api/system/user';
import { onMounted, ref } from 'vue';
import { Page, useVbenModal } from '@vben/common-ui';
import { Plus } from '@vben/icons';
import { Button, message } from 'ant-design-vue';
import { message } from 'ant-design-vue';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import { deleteDept, getDeptList } from '#/api/system/dept';
import { getSimpleUserList } from '#/api/system/user';
import { $t } from '#/locales';
@ -29,9 +25,9 @@ const [FormModal, formModalApi] = useVbenModal({
const userList = ref<SystemUserApi.User[]>([]);
/** 获取负责人名称 */
const getLeaderName = (userId: number) => {
function getLeaderName(userId: number) {
return userList.value.find((user) => user.id === userId)?.nickname;
};
}
/** 刷新表格 */
function onRefresh() {
@ -46,57 +42,41 @@ function toggleExpand() {
}
/** 创建部门 */
function onCreate() {
function handleCreate() {
formModalApi.setData(null).open();
}
/** 添加下级部门 */
function onAppend(row: SystemDeptApi.Dept) {
function handleAppend(row: SystemDeptApi.Dept) {
formModalApi.setData({ parentId: row.id }).open();
}
/** 编辑部门 */
function onEdit(row: SystemDeptApi.Dept) {
function handleEdit(row: SystemDeptApi.Dept) {
formModalApi.setData(row).open();
}
/** 删除部门 */
async function onDelete(row: SystemDeptApi.Dept) {
async function handleDelete(row: SystemDeptApi.Dept) {
const hideLoading = message.loading({
content: $t('ui.actionMessage.deleting', [row.name]),
duration: 0,
key: 'action_process_msg',
key: 'action_key_msg',
});
try {
await deleteDept(row.id as number);
message.success($t('ui.actionMessage.deleteSuccess', [row.name]));
message.success({
content: $t('ui.actionMessage.deleteSuccess', [row.name]),
key: 'action_key_msg',
});
onRefresh();
} catch {
} finally {
hideLoading();
}
}
/** 表格操作按钮的回调函数 */
function onActionClick({ code, row }: OnActionClickParams<SystemDeptApi.Dept>) {
switch (code) {
case 'append': {
onAppend(row);
break;
}
case 'delete': {
onDelete(row);
break;
}
case 'edit': {
onEdit(row);
break;
}
}
}
const [Grid, gridApi] = useVbenVxeGrid({
gridOptions: {
columns: useGridColumns(onActionClick, getLeaderName),
columns: useGridColumns(getLeaderName),
height: 'auto',
keepSource: true,
pagerConfig: {
@ -136,17 +116,54 @@ onMounted(async () => {
<FormModal @success="onRefresh" />
<Grid table-title="">
<template #toolbar-tools>
<Button
type="primary"
@click="onCreate"
v-access:code="['system:dept:create']"
>
<Plus class="size-5" />
{{ $t('ui.actionTitle.create', ['部门']) }}
</Button>
<Button class="ml-2" @click="toggleExpand">
{{ isExpanded ? '收缩' : '展开' }}
</Button>
<TableAction
:actions="[
{
label: $t('ui.actionTitle.create', ['菜单']),
type: 'primary',
icon: ACTION_ICON.ADD,
auth: ['system:dept:create'],
onClick: handleCreate,
},
{
label: isExpanded ? '收缩' : '展开',
type: 'primary',
onClick: toggleExpand,
},
]"
/>
</template>
<template #actions="{ row }">
<TableAction
:actions="[
{
label: '新增下级',
type: 'link',
icon: ACTION_ICON.ADD,
auth: ['system:menu:create'],
onClick: handleAppend.bind(null, row),
},
{
label: $t('common.edit'),
type: 'link',
icon: ACTION_ICON.EDIT,
auth: ['system:menu:update'],
onClick: handleEdit.bind(null, row),
},
{
label: $t('common.delete'),
type: 'link',
danger: true,
icon: ACTION_ICON.DELETE,
auth: ['system:menu:delete'],
disabled: !!(row.children && row.children.length > 0),
popConfirm: {
title: $t('ui.actionMessage.deleteConfirm', [row.name]),
confirm: handleDelete.bind(null, row),
},
},
]"
/>
</template>
</Grid>
</Page>

View File

@ -1,16 +1,10 @@
import type { VbenFormSchema } from '#/adapter/form';
import type { OnActionClickFn, VxeTableGridOptions } from '#/adapter/vxe-table';
import type { SystemDictDataApi } from '#/api/system/dict/data';
import type { SystemDictTypeApi } from '#/api/system/dict/type';
import { useAccess } from '@vben/access';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import { z } from '#/adapter/form';
import { getSimpleDictTypeList } from '#/api/system/dict/type';
import { CommonStatusEnum, DICT_TYPE, getDictOptions } from '#/utils';
const { hasAccessByCodes } = useAccess();
// ============================== 字典类型 ==============================
/** 类型新增/修改的表单 */
@ -96,9 +90,7 @@ export function useTypeGridFormSchema(): VbenFormSchema[] {
}
/** 类型列表的字段 */
export function useTypeGridColumns<T = SystemDictTypeApi.DictType>(
onActionClick: OnActionClickFn<T>,
): VxeTableGridOptions['columns'] {
export function useTypeGridColumns(): VxeTableGridOptions['columns'] {
return [
{
field: 'id',
@ -136,29 +128,10 @@ export function useTypeGridColumns<T = SystemDictTypeApi.DictType>(
formatter: 'formatDateTime',
},
{
minWidth: 120,
title: '操作',
field: 'operation',
width: 160,
fixed: 'right',
align: 'center',
cellRender: {
attrs: {
nameField: 'type',
nameTitle: '字典类型',
onClick: onActionClick,
},
name: 'CellOperation',
options: [
{
code: 'edit',
show: hasAccessByCodes(['system:dict:update']),
},
{
code: 'delete',
show: hasAccessByCodes(['system:dict:delete']),
},
],
},
slots: { default: 'actions' },
},
];
}
@ -310,9 +283,7 @@ export function useDataGridFormSchema(): VbenFormSchema[] {
/**
*
*/
export function useDataGridColumns<T = SystemDictDataApi.DictData>(
onActionClick: OnActionClickFn<T>,
): VxeTableGridOptions['columns'] {
export function useDataGridColumns(): VxeTableGridOptions['columns'] {
return [
{
field: 'id',
@ -360,29 +331,10 @@ export function useDataGridColumns<T = SystemDictDataApi.DictData>(
formatter: 'formatDateTime',
},
{
minWidth: 120,
title: '操作',
field: 'operation',
width: 160,
fixed: 'right',
align: 'center',
cellRender: {
attrs: {
nameField: 'label',
nameTitle: '字典数据',
onClick: onActionClick,
},
name: 'CellOperation',
options: [
{
code: 'edit',
show: hasAccessByCodes(['system:dict:update']),
},
{
code: 'delete',
show: hasAccessByCodes(['system:dict:delete']),
},
],
},
slots: { default: 'actions' },
},
];
}

View File

@ -10,7 +10,7 @@ import TypeGrid from './modules/type-grid.vue';
const searchDictType = ref<string>(); //
function onDictTypeSelect(dictType: string) {
function handleDictTypeSelect(dictType: string) {
searchDictType.value = dictType;
}
</script>
@ -24,7 +24,7 @@ function onDictTypeSelect(dictType: string) {
<div class="flex h-full">
<!-- 左侧字典类型列表 -->
<div class="w-1/2 pr-3">
<TypeGrid @select="onDictTypeSelect" />
<TypeGrid @select="handleDictTypeSelect" />
</div>
<!-- 右侧字典数据列表 -->
<div class="w-1/2">

View File

@ -1,19 +1,15 @@
<script lang="ts" setup>
import type {
OnActionClickParams,
VxeTableGridOptions,
} from '#/adapter/vxe-table';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { SystemDictDataApi } from '#/api/system/dict/data';
import { watch } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { Download, Plus } from '@vben/icons';
import { downloadFileFromBlobPart } from '@vben/utils';
import { Button, message } from 'ant-design-vue';
import { message } from 'ant-design-vue';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import {
deleteDictData,
exportDictData,
@ -42,33 +38,32 @@ function onRefresh() {
}
/** 导出表格 */
async function onExport() {
async function handleExport() {
const data = await exportDictData(await gridApi.formApi.getValues());
downloadFileFromBlobPart({ fileName: '字典数据.xls', source: data });
}
/** 创建字典数据 */
function onCreate() {
function handleCreate() {
dataFormModalApi.setData({ dictType: props.dictType }).open();
}
/** 编辑字典数据 */
function onEdit(row: any) {
function handleEdit(row: SystemDictDataApi.DictData) {
dataFormModalApi.setData(row).open();
}
/** 删除字典数据 */
async function onDelete(row: any) {
async function handleDelete(row: SystemDictDataApi.DictData) {
const hideLoading = message.loading({
content: $t('common.processing'),
duration: 0,
key: 'process_message',
content: $t('ui.actionMessage.deleting', [row.label]),
key: 'action_key_msg',
});
try {
await deleteDictData(row.id);
await deleteDictData(row.id as number);
message.success({
content: $t('common.operationSuccess'),
key: 'process_message',
content: $t('ui.actionMessage.deleteSuccess', [row.label]),
key: 'action_key_msg',
});
onRefresh();
} finally {
@ -76,26 +71,12 @@ async function onDelete(row: any) {
}
}
/** 表格操作按钮回调 */
function onActionClick({ code, row }: OnActionClickParams) {
switch (code) {
case 'delete': {
onDelete(row);
break;
}
case 'edit': {
onEdit(row);
break;
}
}
}
const [Grid, gridApi] = useVbenVxeGrid({
formOptions: {
schema: useDataGridFormSchema(),
},
gridOptions: {
columns: useDataGridColumns(onActionClick),
columns: useDataGridColumns(),
height: 'auto',
keepSource: true,
proxyConfig: {
@ -137,23 +118,48 @@ watch(
<Grid table-title="">
<template #toolbar-tools>
<Button
type="primary"
@click="onCreate"
v-access:code="['system:dict:create']"
>
<Plus class="size-5" />
{{ $t('ui.actionTitle.create', ['字典数据']) }}
</Button>
<Button
type="primary"
class="ml-2"
@click="onExport"
v-access:code="['system:dict:export']"
>
<Download class="size-5" />
{{ $t('ui.actionTitle.export') }}
</Button>
<TableAction
:actions="[
{
label: $t('ui.actionTitle.create', ['字典数据']),
type: 'primary',
icon: ACTION_ICON.ADD,
auth: ['system:dict:create'],
onClick: handleCreate,
},
{
label: $t('ui.actionTitle.export'),
type: 'primary',
icon: ACTION_ICON.DOWNLOAD,
auth: ['system:dict:export'],
onClick: handleExport,
},
]"
/>
</template>
<template #actions="{ row }">
<TableAction
:actions="[
{
label: $t('common.edit'),
type: 'link',
icon: ACTION_ICON.EDIT,
auth: ['system:dict:update'],
onClick: handleEdit.bind(null, row),
},
{
label: $t('common.delete'),
type: 'link',
danger: true,
icon: ACTION_ICON.DELETE,
auth: ['system:dict:delete'],
popConfirm: {
title: $t('ui.actionMessage.deleteConfirm', [row.label]),
confirm: handleDelete.bind(null, row),
},
},
]"
/>
</template>
</Grid>
</div>

View File

@ -1,18 +1,16 @@
<script lang="ts" setup>
import type {
OnActionClickParams,
VxeGridListeners,
VxeTableGridOptions,
} from '#/adapter/vxe-table';
import type { SystemDictTypeApi } from '#/api/system/dict/type';
import { useVbenModal } from '@vben/common-ui';
import { Download, Plus } from '@vben/icons';
import { downloadFileFromBlobPart } from '@vben/utils';
import { Button, message } from 'ant-design-vue';
import { message } from 'ant-design-vue';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import {
deleteDictType,
exportDictType,
@ -36,33 +34,32 @@ function onRefresh() {
}
/** 导出表格 */
async function onExport() {
async function handleExport() {
const data = await exportDictType(await gridApi.formApi.getValues());
downloadFileFromBlobPart({ fileName: '字典类型.xls', source: data });
}
/** 创建字典类型 */
function onCreate() {
function handleCreate() {
typeFormModalApi.setData(null).open();
}
/** 编辑字典类型 */
function onEdit(row: any) {
function handleEdit(row: any) {
typeFormModalApi.setData(row).open();
}
/** 删除字典类型 */
async function onDelete(row: SystemDictTypeApi.DictType) {
async function handleDelete(row: SystemDictTypeApi.DictType) {
const hideLoading = message.loading({
content: $t('common.processing'),
duration: 0,
key: 'process_message',
content: $t('ui.actionMessage.deleting', [row.name]),
key: 'action_key_msg',
});
try {
await deleteDictType(row.id as number);
message.success({
content: $t('common.operationSuccess'),
key: 'process_message',
content: $t('ui.actionMessage.deleteSuccess', [row.name]),
key: 'action_key_msg',
});
onRefresh();
} finally {
@ -70,23 +67,6 @@ async function onDelete(row: SystemDictTypeApi.DictType) {
}
}
/** 表格操作按钮回调 */
function onActionClick({
code,
row,
}: OnActionClickParams<SystemDictTypeApi.DictType>) {
switch (code) {
case 'delete': {
onDelete(row);
break;
}
case 'edit': {
onEdit(row);
break;
}
}
}
/** 表格事件 */
const gridEvents: VxeGridListeners<SystemDictTypeApi.DictType> = {
cellClick: ({ row }) => {
@ -99,7 +79,7 @@ const [Grid, gridApi] = useVbenVxeGrid({
schema: useTypeGridFormSchema(),
},
gridOptions: {
columns: useTypeGridColumns(onActionClick),
columns: useTypeGridColumns(),
height: 'auto',
keepSource: true,
proxyConfig: {
@ -132,23 +112,48 @@ const [Grid, gridApi] = useVbenVxeGrid({
<Grid table-title="">
<template #toolbar-tools>
<Button
type="primary"
@click="onCreate"
v-access:code="['system:dict:create']"
>
<Plus class="size-5" />
{{ $t('ui.actionTitle.create', ['字典类型']) }}
</Button>
<Button
type="primary"
class="ml-2"
@click="onExport"
v-access:code="['system:dict:export']"
>
<Download class="size-5" />
{{ $t('ui.actionTitle.export') }}
</Button>
<TableAction
:actions="[
{
label: $t('ui.actionTitle.create', ['字典类型']),
type: 'primary',
icon: ACTION_ICON.ADD,
auth: ['system:dict:create'],
onClick: handleCreate,
},
{
label: $t('ui.actionTitle.export'),
type: 'primary',
icon: ACTION_ICON.DOWNLOAD,
auth: ['system:dict:export'],
onClick: handleExport,
},
]"
/>
</template>
<template #actions="{ row }">
<TableAction
:actions="[
{
label: $t('common.edit'),
type: 'link',
icon: ACTION_ICON.EDIT,
auth: ['system:dict:update'],
onClick: handleEdit.bind(null, row),
},
{
label: $t('common.delete'),
type: 'link',
danger: true,
icon: ACTION_ICON.DELETE,
auth: ['system:dict:delete'],
popConfirm: {
title: $t('ui.actionMessage.deleteConfirm', [row.name]),
confirm: handleDelete.bind(null, row),
},
},
]"
/>
</template>
</Grid>
</div>

View File

@ -1,13 +1,8 @@
import type { VbenFormSchema } from '#/adapter/form';
import type { OnActionClickFn, VxeTableGridOptions } from '#/adapter/vxe-table';
import type { SystemLoginLogApi } from '#/api/system/login-log';
import { useAccess } from '@vben/access';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import { DICT_TYPE, getRangePickerDefaultProps } from '#/utils';
const { hasAccessByCodes } = useAccess();
/** 列表的搜索表单 */
export function useGridFormSchema(): VbenFormSchema[] {
return [
@ -42,9 +37,7 @@ export function useGridFormSchema(): VbenFormSchema[] {
}
/** 列表的字段 */
export function useGridColumns<T = SystemLoginLogApi.LoginLog>(
onActionClick: OnActionClickFn<T>,
): VxeTableGridOptions['columns'] {
export function useGridColumns(): VxeTableGridOptions['columns'] {
return [
{
field: 'id',
@ -91,26 +84,10 @@ export function useGridColumns<T = SystemLoginLogApi.LoginLog>(
formatter: 'formatDateTime',
},
{
field: 'operation',
title: '操作',
minWidth: 120,
align: 'center',
width: 80,
fixed: 'right',
cellRender: {
attrs: {
nameField: 'username',
nameTitle: '登录日志',
onClick: onActionClick,
},
name: 'CellOperation',
options: [
{
code: 'detail',
text: '详情',
show: hasAccessByCodes(['system:login-log:query']),
},
],
},
slots: { default: 'actions' },
},
];
}

View File

@ -1,17 +1,11 @@
<script lang="ts" setup>
import type {
OnActionClickParams,
VxeTableGridOptions,
} from '#/adapter/vxe-table';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { SystemLoginLogApi } from '#/api/system/login-log';
import { Page, useVbenModal } from '@vben/common-ui';
import { Download } from '@vben/icons';
import { downloadFileFromBlobPart } from '@vben/utils';
import { Button } from 'ant-design-vue';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import { exportLoginLog, getLoginLogPage } from '#/api/system/login-log';
import { DocAlert } from '#/components/doc-alert';
import { $t } from '#/locales';
@ -30,35 +24,22 @@ function onRefresh() {
}
/** 导出表格 */
async function onExport() {
async function handleExport() {
const data = await exportLoginLog(await gridApi.formApi.getValues());
downloadFileFromBlobPart({ fileName: '登录日志.xls', source: data });
}
/** 查看登录日志详情 */
function onDetail(row: SystemLoginLogApi.LoginLog) {
function handleDetail(row: SystemLoginLogApi.LoginLog) {
detailModalApi.setData(row).open();
}
/** 表格操作按钮的回调函数 */
function onActionClick({
code,
row,
}: OnActionClickParams<SystemLoginLogApi.LoginLog>) {
switch (code) {
case 'detail': {
onDetail(row);
break;
}
}
}
const [Grid, gridApi] = useVbenVxeGrid({
formOptions: {
schema: useGridFormSchema(),
},
gridOptions: {
columns: useGridColumns(onActionClick),
columns: useGridColumns(),
height: 'auto',
keepSource: true,
proxyConfig: {
@ -92,15 +73,30 @@ const [Grid, gridApi] = useVbenVxeGrid({
<DetailModal @success="onRefresh" />
<Grid table-title="">
<template #toolbar-tools>
<Button
type="primary"
class="ml-2"
@click="onExport"
v-access:code="['system:login-log:export']"
>
<Download class="size-5" />
{{ $t('ui.actionTitle.export') }}
</Button>
<TableAction
:actions="[
{
label: $t('ui.actionTitle.export'),
type: 'primary',
icon: ACTION_ICON.DOWNLOAD,
auth: ['system:login-log:export'],
onClick: handleExport,
},
]"
/>
</template>
<template #actions="{ row }">
<TableAction
:actions="[
{
label: $t('common.detail'),
type: 'link',
icon: ACTION_ICON.VIEW,
auth: ['system:login-log:query'],
onClick: handleDetail.bind(null, row),
},
]"
/>
</template>
</Grid>
</Page>

View File

@ -1,14 +1,9 @@
import type { VbenFormSchema } from '#/adapter/form';
import type { OnActionClickFn, VxeTableGridOptions } from '#/adapter/vxe-table';
import type { SystemMailAccountApi } from '#/api/system/mail/account';
import { useAccess } from '@vben/access';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import { z } from '#/adapter/form';
import { DICT_TYPE, getDictOptions } from '#/utils';
const { hasAccessByCodes } = useAccess();
/** 新增/修改的表单 */
export function useFormSchema(): VbenFormSchema[] {
return [
@ -125,9 +120,7 @@ export function useGridFormSchema(): VbenFormSchema[] {
}
/** 列表的字段 */
export function useGridColumns<T = SystemMailAccountApi.MailAccount>(
onActionClick: OnActionClickFn<T>,
): VxeTableGridOptions['columns'] {
export function useGridColumns(): VxeTableGridOptions['columns'] {
return [
{
field: 'id',
@ -179,29 +172,10 @@ export function useGridColumns<T = SystemMailAccountApi.MailAccount>(
formatter: 'formatDateTime',
},
{
field: 'operation',
title: '操作',
minWidth: 130,
align: 'center',
width: 130,
fixed: 'right',
cellRender: {
attrs: {
nameField: 'mail',
nameTitle: '邮箱账号',
onClick: onActionClick,
},
name: 'CellOperation',
options: [
{
code: 'edit',
show: hasAccessByCodes(['system:mail-account:update']),
},
{
code: 'delete',
show: hasAccessByCodes(['system:mail-account:delete']),
},
],
},
slots: { default: 'actions' },
},
];
}

View File

@ -1,16 +1,12 @@
<script lang="ts" setup>
import type {
OnActionClickParams,
VxeTableGridOptions,
} from '#/adapter/vxe-table';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { SystemMailAccountApi } from '#/api/system/mail/account';
import { Page, useVbenModal } from '@vben/common-ui';
import { Plus } from '@vben/icons';
import { Button, message } from 'ant-design-vue';
import { message } from 'ant-design-vue';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import {
deleteMailAccount,
getMailAccountPage,
@ -32,54 +28,39 @@ function onRefresh() {
}
/** 创建邮箱账号 */
function onCreate() {
function handleCreate() {
formModalApi.setData(null).open();
}
/** 编辑邮箱账号 */
function onEdit(row: SystemMailAccountApi.MailAccount) {
function handleEdit(row: SystemMailAccountApi.MailAccount) {
formModalApi.setData(row).open();
}
/** 删除邮箱账号 */
async function onDelete(row: SystemMailAccountApi.MailAccount) {
async function handleDelete(row: SystemMailAccountApi.MailAccount) {
const hideLoading = message.loading({
content: $t('ui.actionMessage.deleting', [row.mail]),
duration: 0,
key: 'action_process_msg',
key: 'action_key_msg',
});
try {
await deleteMailAccount(row.id as number);
message.success($t('ui.actionMessage.deleteSuccess', [row.mail]));
message.success({
content: $t('ui.actionMessage.deleteSuccess', [row.mail]),
key: 'action_key_msg',
});
onRefresh();
} finally {
hideLoading();
}
}
/** 表格操作按钮的回调函数 */
function onActionClick({
code,
row,
}: OnActionClickParams<SystemMailAccountApi.MailAccount>) {
switch (code) {
case 'delete': {
onDelete(row);
break;
}
case 'edit': {
onEdit(row);
break;
}
}
}
const [Grid, gridApi] = useVbenVxeGrid({
formOptions: {
schema: useGridFormSchema(),
},
gridOptions: {
columns: useGridColumns(onActionClick),
columns: useGridColumns(),
height: 'auto',
keepSource: true,
proxyConfig: {
@ -112,14 +93,41 @@ const [Grid, gridApi] = useVbenVxeGrid({
<FormModal @success="onRefresh" />
<Grid table-title="">
<template #toolbar-tools>
<Button
type="primary"
@click="onCreate"
v-access:code="['system:mail-account:create']"
>
<Plus class="size-5" />
{{ $t('ui.actionTitle.create', ['邮箱账号']) }}
</Button>
<TableAction
:actions="[
{
label: $t('ui.actionTitle.create', ['邮箱账号']),
type: 'primary',
icon: ACTION_ICON.ADD,
auth: ['system:mail-account:create'],
onClick: handleCreate,
},
]"
/>
</template>
<template #actions="{ row }">
<TableAction
:actions="[
{
label: $t('common.edit'),
type: 'link',
icon: ACTION_ICON.EDIT,
auth: ['system:mail-account:update'],
onClick: handleEdit.bind(null, row),
},
{
label: $t('common.delete'),
type: 'link',
danger: true,
icon: ACTION_ICON.DELETE,
auth: ['system:mail-account:delete'],
popConfirm: {
title: $t('ui.actionMessage.deleteConfirm', [row.name]),
confirm: handleDelete.bind(null, row),
},
},
]"
/>
</template>
</Grid>
</Page>

View File

@ -1,14 +1,9 @@
import type { VbenFormSchema } from '#/adapter/form';
import type { OnActionClickFn, VxeTableGridOptions } from '#/adapter/vxe-table';
import type { SystemMailLogApi } from '#/api/system/mail/log';
import { useAccess } from '@vben/access';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import { getSimpleMailAccountList } from '#/api/system/mail/account';
import { DICT_TYPE, getDictOptions, getRangePickerDefaultProps } from '#/utils';
const { hasAccessByCodes } = useAccess();
/** 列表的搜索表单 */
export function useGridFormSchema(): VbenFormSchema[] {
return [
@ -75,9 +70,7 @@ export function useGridFormSchema(): VbenFormSchema[] {
}
/** 列表的字段 */
export function useGridColumns<T = SystemMailLogApi.MailLog>(
onActionClick: OnActionClickFn<T>,
): VxeTableGridOptions['columns'] {
export function useGridColumns(): VxeTableGridOptions['columns'] {
return [
{
field: 'id',
@ -125,26 +118,10 @@ export function useGridColumns<T = SystemMailLogApi.MailLog>(
minWidth: 120,
},
{
field: 'operation',
title: '操作',
minWidth: 80,
align: 'center',
width: 80,
fixed: 'right',
cellRender: {
attrs: {
nameField: 'toMail',
nameTitle: '邮件日志',
onClick: onActionClick,
},
name: 'CellOperation',
options: [
{
code: 'detail',
text: '查看',
show: hasAccessByCodes(['system:mail-log:query']),
},
],
},
slots: { default: 'actions' },
},
];
}

View File

@ -1,13 +1,10 @@
<script lang="ts" setup>
import type {
OnActionClickParams,
VxeTableGridOptions,
} from '#/adapter/vxe-table';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { SystemMailLogApi } from '#/api/system/mail/log';
import { Page, useVbenModal } from '@vben/common-ui';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import { getMailLogPage } from '#/api/system/mail/log';
import { DocAlert } from '#/components/doc-alert';
@ -25,29 +22,16 @@ function onRefresh() {
}
/** 查看邮件日志 */
function onDetail(row: SystemMailLogApi.MailLog) {
function handleDetail(row: SystemMailLogApi.MailLog) {
detailModalApi.setData(row).open();
}
/** 表格操作按钮的回调函数 */
function onActionClick({
code,
row,
}: OnActionClickParams<SystemMailLogApi.MailLog>) {
switch (code) {
case 'detail': {
onDetail(row);
break;
}
}
}
const [Grid, gridApi] = useVbenVxeGrid({
formOptions: {
schema: useGridFormSchema(),
},
gridOptions: {
columns: useGridColumns(onActionClick),
columns: useGridColumns(),
height: 'auto',
keepSource: true,
proxyConfig: {
@ -79,7 +63,19 @@ const [Grid, gridApi] = useVbenVxeGrid({
<DetailModal @success="onRefresh" />
<Grid table-title="">
<template #toolbar-tools> </template>
<template #actions="{ row }">
<TableAction
:actions="[
{
label: $t('common.detail'),
type: 'link',
icon: ACTION_ICON.VIEW,
auth: ['system:mail-log:query'],
onClick: handleDetail.bind(null, row),
},
]"
/>
</template>
</Grid>
</Page>
</template>

View File

@ -1,8 +1,5 @@
import type { VbenFormSchema } from '#/adapter/form';
import type { OnActionClickFn, VxeTableGridOptions } from '#/adapter/vxe-table';
import type { SystemMailTemplateApi } from '#/api/system/mail/template';
import { useAccess } from '@vben/access';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import { z } from '#/adapter/form';
import { getSimpleMailAccountList } from '#/api/system/mail/account';
@ -13,8 +10,6 @@ import {
getRangePickerDefaultProps,
} from '#/utils';
const { hasAccessByCodes } = useAccess();
/** 新增/修改的表单 */
export function useFormSchema(): VbenFormSchema[] {
return [
@ -193,8 +188,7 @@ export function useGridFormSchema(): VbenFormSchema[] {
}
/** 列表的字段 */
export function useGridColumns<T = SystemMailTemplateApi.MailTemplate>(
onActionClick: OnActionClickFn<T>,
export function useGridColumns(
getAccountMail?: (accountId: number) => string | undefined,
): VxeTableGridOptions['columns'] {
return [
@ -245,34 +239,10 @@ export function useGridColumns<T = SystemMailTemplateApi.MailTemplate>(
formatter: 'formatDateTime',
},
{
field: 'operation',
title: '操作',
minWidth: 150,
align: 'center',
width: 220,
fixed: 'right',
cellRender: {
attrs: {
nameField: 'name',
nameTitle: '邮件模板',
onClick: onActionClick,
},
name: 'CellOperation',
options: [
{
code: 'edit',
show: hasAccessByCodes(['system:mail-template:update']),
},
{
code: 'delete',
show: hasAccessByCodes(['system:mail-template:delete']),
},
{
code: 'send',
text: '测试',
show: hasAccessByCodes(['system:mail-template:send-mail']),
},
],
},
slots: { default: 'actions' },
},
];
}

View File

@ -1,19 +1,15 @@
<script lang="ts" setup>
import type {
OnActionClickParams,
VxeTableGridOptions,
} from '#/adapter/vxe-table';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { SystemMailAccountApi } from '#/api/system/mail/account';
import type { SystemMailTemplateApi } from '#/api/system/mail/template';
import { onMounted, ref } from 'vue';
import { Page, useVbenModal } from '@vben/common-ui';
import { Plus } from '@vben/icons';
import { Button, message } from 'ant-design-vue';
import { message } from 'ant-design-vue';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import { getSimpleMailAccountList } from '#/api/system/mail/account';
import {
deleteMailTemplate,
@ -28,11 +24,6 @@ import SendForm from './modules/send-form.vue';
const accountList = ref<SystemMailAccountApi.MailAccount[]>([]);
/** 获取邮箱账号 */
const getAccountMail = (accountId: number) => {
return accountList.value.find((account) => account.id === accountId)?.mail;
};
const [FormModal, formModalApi] = useVbenModal({
connectedComponent: Form,
destroyOnClose: true,
@ -49,55 +40,38 @@ function onRefresh() {
}
/** 创建邮件模板 */
function onCreate() {
function handleCreate() {
formModalApi.setData(null).open();
}
/** 编辑邮件模板 */
function onEdit(row: SystemMailTemplateApi.MailTemplate) {
function handleEdit(row: SystemMailTemplateApi.MailTemplate) {
formModalApi.setData(row).open();
}
/** 发送测试邮件 */
function onSend(row: SystemMailTemplateApi.MailTemplate) {
function handleSend(row: SystemMailTemplateApi.MailTemplate) {
sendModalApi.setData(row).open();
}
/** 删除邮件模板 */
async function onDelete(row: SystemMailTemplateApi.MailTemplate) {
const hideLoading = message.loading({
async function handleDelete(row: SystemMailTemplateApi.MailTemplate) {
message.loading({
content: $t('ui.actionMessage.deleting', [row.name]),
duration: 0,
key: 'action_process_msg',
});
try {
await deleteMailTemplate(row.id as number);
message.success($t('ui.actionMessage.deleteSuccess', [row.name]));
onRefresh();
} finally {
hideLoading();
}
await deleteMailTemplate(row.id as number);
message.success({
content: $t('ui.actionMessage.deleteSuccess', [row.name]),
key: 'action_key_msg',
});
onRefresh();
}
/** 表格操作按钮的回调函数 */
function onActionClick({
code,
row,
}: OnActionClickParams<SystemMailTemplateApi.MailTemplate>) {
switch (code) {
case 'delete': {
onDelete(row);
break;
}
case 'edit': {
onEdit(row);
break;
}
case 'send': {
onSend(row);
break;
}
}
/** 获取邮箱账号 */
function getAccountMail(accountId: number) {
return accountList.value.find((account) => account.id === accountId)?.mail;
}
const [Grid, gridApi] = useVbenVxeGrid({
@ -105,7 +79,7 @@ const [Grid, gridApi] = useVbenVxeGrid({
schema: useGridFormSchema(),
},
gridOptions: {
columns: useGridColumns(onActionClick, getAccountMail),
columns: useGridColumns(getAccountMail),
height: 'auto',
keepSource: true,
proxyConfig: {
@ -144,14 +118,48 @@ onMounted(async () => {
<SendModal />
<Grid table-title="">
<template #toolbar-tools>
<Button
type="primary"
@click="onCreate"
v-access:code="['system:mail-template:create']"
>
<Plus class="size-5" />
{{ $t('ui.actionTitle.create', ['邮件模板']) }}
</Button>
<TableAction
:actions="[
{
label: $t('ui.actionTitle.create', ['邮件模板']),
type: 'primary',
icon: ACTION_ICON.ADD,
auth: ['system:mail-template:create'],
onClick: handleCreate,
},
]"
/>
</template>
<template #actions="{ row }">
<TableAction
:actions="[
{
label: $t('common.edit'),
type: 'link',
icon: ACTION_ICON.EDIT,
auth: ['system:mail-template:update'],
onClick: handleEdit.bind(null, row),
},
{
label: '测试',
type: 'link',
icon: ACTION_ICON.EDIT,
auth: ['system:mail-template:send-mail'],
onClick: handleSend.bind(null, row),
},
{
label: $t('common.delete'),
type: 'link',
danger: true,
icon: ACTION_ICON.DELETE,
auth: ['system:mail-template:delete'],
popConfirm: {
title: $t('ui.actionMessage.deleteConfirm', [row.name]),
confirm: handleDelete.bind(null, row),
},
},
]"
/>
</template>
</Grid>
</Page>

View File

@ -83,7 +83,7 @@ const [Modal, modalApi] = useVbenModal({
});
/** 动态构建表单 schema */
const buildFormSchema = () => {
function buildFormSchema() {
const schema = useSendMailFormSchema();
if (formData.value?.params) {
formData.value.params?.forEach((param: string) => {
@ -99,7 +99,7 @@ const buildFormSchema = () => {
});
}
return schema;
};
}
</script>
<template>

View File

@ -1,12 +1,11 @@
import type { Recordable } from '@vben/types';
import type { VbenFormSchema } from '#/adapter/form';
import type { OnActionClickFn, VxeTableGridOptions } from '#/adapter/vxe-table';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { SystemMenuApi } from '#/api/system/menu';
import { h } from 'vue';
import { useAccess } from '@vben/access';
import { IconifyIcon } from '@vben/icons';
import { handleTree, isHttpUrl } from '@vben/utils';
@ -21,8 +20,6 @@ import {
SystemMenuTypeEnum,
} from '#/utils';
const { hasAccessByCodes } = useAccess();
/** 新增/修改的表单 */
export function useFormSchema(): VbenFormSchema[] {
return [
@ -269,9 +266,7 @@ export function useFormSchema(): VbenFormSchema[] {
}
/** 列表的字段 */
export function useGridColumns(
onActionClick: OnActionClickFn<SystemMenuApi.Menu>,
): VxeTableGridOptions<SystemMenuApi.Menu>['columns'] {
export function useGridColumns(): VxeTableGridOptions<SystemMenuApi.Menu>['columns'] {
return [
{
field: 'name',
@ -321,34 +316,10 @@ export function useGridColumns(
},
},
{
field: 'operation',
title: '操作',
minWidth: 200,
width: 220,
fixed: 'right',
align: 'center',
showOverflow: false,
cellRender: {
attrs: {
nameField: 'name',
onClick: onActionClick,
},
name: 'CellOperation',
options: [
{
code: 'append',
text: '新增下级',
show: hasAccessByCodes(['system:menu:create']),
},
{
code: 'edit',
show: hasAccessByCodes(['system:menu:update']),
},
{
code: 'delete',
show: hasAccessByCodes(['system:menu:delete']),
},
],
},
slots: { default: 'actions' },
},
];
}

View File

@ -1,18 +1,15 @@
<script lang="ts" setup>
import type {
OnActionClickParams,
VxeTableGridOptions,
} from '#/adapter/vxe-table';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { SystemMenuApi } from '#/api/system/menu';
import { ref } from 'vue';
import { Page, useVbenModal } from '@vben/common-ui';
import { IconifyIcon, Plus } from '@vben/icons';
import { IconifyIcon } from '@vben/icons';
import { Button, message } from 'ant-design-vue';
import { message } from 'ant-design-vue';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import { deleteMenu, getMenuList } from '#/api/system/menu';
import { DocAlert } from '#/components/doc-alert';
import { $t } from '#/locales';
@ -32,54 +29,38 @@ function onRefresh() {
}
/** 创建菜单 */
function onCreate() {
function handleCreate() {
formModalApi.setData({}).open();
}
/** 添加下级菜单 */
function onAppend(row: SystemMenuApi.Menu) {
function handleAppend(row: SystemMenuApi.Menu) {
formModalApi.setData({ pid: row.id }).open();
}
/** 编辑菜单 */
function onEdit(row: SystemMenuApi.Menu) {
function handleEdit(row: SystemMenuApi.Menu) {
formModalApi.setData(row).open();
}
/** 删除菜单 */
async function onDelete(row: SystemMenuApi.Menu) {
async function handleDelete(row: SystemMenuApi.Menu) {
const hideLoading = message.loading({
content: $t('ui.actionMessage.deleting', [row.name]),
duration: 0,
key: 'action_process_msg',
key: 'action_key_msg',
});
try {
await deleteMenu(row.id as number);
message.success($t('ui.actionMessage.deleteSuccess', [row.name]));
message.success({
content: $t('ui.actionMessage.deleteSuccess', [row.name]),
key: 'action_key_msg',
});
onRefresh();
} catch {
} finally {
hideLoading();
}
}
/** 表格操作按钮的回调函数 */
function onActionClick({ code, row }: OnActionClickParams<SystemMenuApi.Menu>) {
switch (code) {
case 'append': {
onAppend(row);
break;
}
case 'delete': {
onDelete(row);
break;
}
case 'edit': {
onEdit(row);
break;
}
}
}
/** 切换树形展开/收缩状态 */
const isExpanded = ref(false);
function toggleExpand() {
@ -89,7 +70,7 @@ function toggleExpand() {
const [Grid, gridApi] = useVbenVxeGrid({
gridOptions: {
columns: useGridColumns(onActionClick),
columns: useGridColumns(),
height: 'auto',
keepSource: true,
pagerConfig: {
@ -131,17 +112,22 @@ const [Grid, gridApi] = useVbenVxeGrid({
<FormModal @success="onRefresh" />
<Grid>
<template #toolbar-tools>
<Button
type="primary"
@click="onCreate"
v-access:code="['system:menu:create']"
>
<Plus class="size-5" />
{{ $t('ui.actionTitle.create', ['菜单']) }}
</Button>
<Button class="ml-2" @click="toggleExpand">
{{ isExpanded ? '收缩' : '展开' }}
</Button>
<TableAction
:actions="[
{
label: $t('ui.actionTitle.create', ['菜单']),
type: 'primary',
icon: ACTION_ICON.ADD,
auth: ['system:menu:create'],
onClick: handleCreate,
},
{
label: isExpanded ? '收缩' : '展开',
type: 'primary',
onClick: toggleExpand,
},
]"
/>
</template>
<template #name="{ row }">
<div class="flex w-full items-center gap-1">
@ -161,6 +147,37 @@ const [Grid, gridApi] = useVbenVxeGrid({
<div class="items-center justify-end"></div>
</div>
</template>
<template #actions="{ row }">
<TableAction
:actions="[
{
label: '新增下级',
type: 'link',
icon: ACTION_ICON.ADD,
auth: ['system:menu:create'],
onClick: handleAppend.bind(null, row),
},
{
label: $t('common.edit'),
type: 'link',
icon: ACTION_ICON.EDIT,
auth: ['system:menu:update'],
onClick: handleEdit.bind(null, row),
},
{
label: $t('common.delete'),
type: 'link',
danger: true,
icon: ACTION_ICON.DELETE,
auth: ['system:menu:delete'],
popConfirm: {
title: $t('ui.actionMessage.deleteConfirm', [row.name]),
confirm: handleDelete.bind(null, row),
},
},
]"
/>
</template>
</Grid>
</Page>
</template>

View File

@ -1,14 +1,9 @@
import type { VbenFormSchema } from '#/adapter/form';
import type { OnActionClickFn, VxeTableGridOptions } from '#/adapter/vxe-table';
import type { SystemNoticeApi } from '#/api/system/notice';
import { useAccess } from '@vben/access';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import { z } from '#/adapter/form';
import { CommonStatusEnum, DICT_TYPE, getDictOptions } from '#/utils';
const { hasAccessByCodes } = useAccess();
/** 新增/修改的表单 */
export function useFormSchema(): VbenFormSchema[] {
return [
@ -91,9 +86,7 @@ export function useGridFormSchema(): VbenFormSchema[] {
}
/** 列表的字段 */
export function useGridColumns<T = SystemNoticeApi.Notice>(
onActionClick: OnActionClickFn<T>,
): VxeTableGridOptions['columns'] {
export function useGridColumns(): VxeTableGridOptions['columns'] {
return [
{
field: 'id',
@ -130,34 +123,10 @@ export function useGridColumns<T = SystemNoticeApi.Notice>(
formatter: 'formatDateTime',
},
{
field: 'operation',
title: '操作',
minWidth: 180,
align: 'center',
width: 220,
fixed: 'right',
cellRender: {
attrs: {
nameField: 'title',
nameTitle: '公告',
onClick: onActionClick,
},
name: 'CellOperation',
options: [
{
code: 'edit',
show: hasAccessByCodes(['system:notice:update']),
},
{
code: 'push',
text: '推送',
show: hasAccessByCodes(['system:notice:update']),
},
{
code: 'delete',
show: hasAccessByCodes(['system:notice:delete']),
},
],
},
slots: { default: 'actions' },
},
];
}

View File

@ -1,16 +1,12 @@
<script lang="ts" setup>
import type {
OnActionClickParams,
VxeTableGridOptions,
} from '#/adapter/vxe-table';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { SystemNoticeApi } from '#/api/system/notice';
import { Page, useVbenModal } from '@vben/common-ui';
import { Plus } from '@vben/icons';
import { Button, message } from 'ant-design-vue';
import { message } from 'ant-design-vue';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import { deleteNotice, getNoticePage, pushNotice } from '#/api/system/notice';
import { $t } from '#/locales';
@ -28,73 +24,56 @@ function onRefresh() {
}
/** 创建公告 */
function onCreate() {
function handleCreate() {
formModalApi.setData(null).open();
}
/** 编辑公告 */
function onEdit(row: SystemNoticeApi.Notice) {
function handleEdit(row: SystemNoticeApi.Notice) {
formModalApi.setData(row).open();
}
/** 删除公告 */
async function onDelete(row: SystemNoticeApi.Notice) {
async function handleDelete(row: SystemNoticeApi.Notice) {
const hideLoading = message.loading({
content: $t('ui.actionMessage.deleting', [row.title]),
duration: 0,
key: 'action_process_msg',
key: 'action_key_msg',
});
try {
await deleteNotice(row.id as number);
message.success($t('ui.actionMessage.deleteSuccess', [row.title]));
message.success({
content: $t('ui.actionMessage.deleteSuccess', [row.title]),
key: 'action_key_msg',
});
onRefresh();
} catch {
} finally {
hideLoading();
}
}
/** 推送公告 */
async function onPush(row: SystemNoticeApi.Notice) {
async function handlePush(row: SystemNoticeApi.Notice) {
const hideLoading = message.loading({
content: $t('ui.actionMessage.processing', ['推送']),
duration: 0,
key: 'action_process_msg',
});
try {
await pushNotice(row.id as number);
message.success($t('ui.actionMessage.operationSuccess'));
} catch {
message.success({
content: $t('ui.actionMessage.operationSuccess'),
key: 'action_key_msg',
});
} finally {
hideLoading();
}
}
/** 表格操作按钮的回调函数 */
function onActionClick({
code,
row,
}: OnActionClickParams<SystemNoticeApi.Notice>) {
switch (code) {
case 'delete': {
onDelete(row);
break;
}
case 'edit': {
onEdit(row);
break;
}
case 'push': {
onPush(row);
break;
}
}
}
const [Grid, gridApi] = useVbenVxeGrid({
formOptions: {
schema: useGridFormSchema(),
},
gridOptions: {
columns: useGridColumns(onActionClick),
columns: useGridColumns(),
height: 'auto',
keepSource: true,
proxyConfig: {
@ -124,14 +103,48 @@ const [Grid, gridApi] = useVbenVxeGrid({
<FormModal @success="onRefresh" />
<Grid table-title="">
<template #toolbar-tools>
<Button
type="primary"
@click="onCreate"
v-access:code="['system:notice:create']"
>
<Plus class="size-5" />
{{ $t('ui.actionTitle.create', ['公告']) }}
</Button>
<TableAction
:actions="[
{
label: $t('ui.actionTitle.create', ['公告']),
type: 'primary',
icon: ACTION_ICON.ADD,
auth: ['system:notice:create'],
onClick: handleCreate,
},
]"
/>
</template>
<template #actions="{ row }">
<TableAction
:actions="[
{
label: $t('common.edit'),
type: 'link',
icon: ACTION_ICON.EDIT,
auth: ['system:notice:update'],
onClick: handleEdit.bind(null, row),
},
{
label: '推送',
type: 'link',
icon: ACTION_ICON.ADD,
auth: ['system:notice:update'],
onClick: handlePush.bind(null, row),
},
{
label: $t('common.delete'),
type: 'link',
danger: true,
icon: ACTION_ICON.DELETE,
auth: ['system:notice:delete'],
popConfirm: {
title: $t('ui.actionMessage.deleteConfirm', [row.name]),
confirm: handleDelete.bind(null, row),
},
},
]"
/>
</template>
</Grid>
</Page>

View File

@ -1,13 +1,8 @@
import type { VbenFormSchema } from '#/adapter/form';
import type { OnActionClickFn, VxeTableGridOptions } from '#/adapter/vxe-table';
import type { SystemNotifyMessageApi } from '#/api/system/notify/message';
import { useAccess } from '@vben/access';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import { DICT_TYPE, getDictOptions, getRangePickerDefaultProps } from '#/utils';
const { hasAccessByCodes } = useAccess();
/** 列表的搜索表单 */
export function useGridFormSchema(): VbenFormSchema[] {
return [
@ -65,9 +60,7 @@ export function useGridFormSchema(): VbenFormSchema[] {
}
/** 列表的字段 */
export function useGridColumns<T = SystemNotifyMessageApi.NotifyMessage>(
onActionClick: OnActionClickFn<T>,
): VxeTableGridOptions['columns'] {
export function useGridColumns(): VxeTableGridOptions['columns'] {
return [
{
field: 'id',
@ -146,26 +139,10 @@ export function useGridColumns<T = SystemNotifyMessageApi.NotifyMessage>(
formatter: 'formatDateTime',
},
{
field: 'operation',
title: '操作',
minWidth: 180,
align: 'center',
width: 80,
fixed: 'right',
cellRender: {
attrs: {
nameField: 'id',
nameTitle: '站内信',
onClick: onActionClick,
},
name: 'CellOperation',
options: [
{
code: 'detail',
text: '详情',
show: hasAccessByCodes(['system:notify-message:query']),
},
],
},
slots: { default: 'actions' },
},
];
}

View File

@ -1,13 +1,10 @@
<script lang="ts" setup>
import type {
OnActionClickParams,
VxeTableGridOptions,
} from '#/adapter/vxe-table';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { SystemNotifyMessageApi } from '#/api/system/notify/message';
import { Page, useVbenModal } from '@vben/common-ui';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import { getNotifyMessagePage } from '#/api/system/notify/message';
import { DocAlert } from '#/components/doc-alert';
@ -25,29 +22,16 @@ function onRefresh() {
}
/** 查看站内信详情 */
function onDetail(row: SystemNotifyMessageApi.NotifyMessage) {
function handleDetail(row: SystemNotifyMessageApi.NotifyMessage) {
detailModalApi.setData(row).open();
}
/** 表格操作按钮的回调函数 */
function onActionClick({
code,
row,
}: OnActionClickParams<SystemNotifyMessageApi.NotifyMessage>) {
switch (code) {
case 'detail': {
onDetail(row);
break;
}
}
}
const [Grid, gridApi] = useVbenVxeGrid({
formOptions: {
schema: useGridFormSchema(),
},
gridOptions: {
columns: useGridColumns(onActionClick),
columns: useGridColumns(),
height: 'auto',
keepSource: true,
proxyConfig: {
@ -79,6 +63,20 @@ const [Grid, gridApi] = useVbenVxeGrid({
</template>
<DetailModal @success="onRefresh" />
<Grid table-title="" />
<Grid table-title="">
<template #actions="{ row }">
<TableAction
:actions="[
{
label: $t('common.detail'),
type: 'link',
icon: ACTION_ICON.VIEW,
auth: ['system:notify-message:query'],
onClick: handleDetail.bind(null, row),
},
]"
/>
</template>
</Grid>
</Page>
</template>

View File

@ -1,6 +1,5 @@
import type { VbenFormSchema } from '#/adapter/form';
import type { OnActionClickFn, VxeTableGridOptions } from '#/adapter/vxe-table';
import type { SystemNotifyMessageApi } from '#/api/system/notify/message';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { DescriptionItemSchema } from '#/components/description';
import { h } from 'vue';
@ -36,9 +35,7 @@ export function useGridFormSchema(): VbenFormSchema[] {
}
/** 列表的字段 */
export function useGridColumns<T = SystemNotifyMessageApi.NotifyMessage>(
onActionClick: OnActionClickFn<T>,
): VxeTableGridOptions['columns'] {
export function useGridColumns(): VxeTableGridOptions['columns'] {
return [
{
title: '',
@ -86,31 +83,10 @@ export function useGridColumns<T = SystemNotifyMessageApi.NotifyMessage>(
formatter: 'formatDateTime',
},
{
field: 'operation',
title: '操作',
minWidth: 180,
align: 'center',
width: 130,
fixed: 'right',
cellRender: {
attrs: {
nameField: 'id',
nameTitle: '站内信',
onClick: onActionClick,
},
name: 'CellOperation',
options: [
{
code: 'detail',
text: '查看',
show: (row: any) => row.readStatus,
},
{
code: 'read',
text: '已读',
show: (row: any) => !row.readStatus,
},
],
},
slots: { default: 'actions' },
},
];
}

View File

@ -1,16 +1,12 @@
<script lang="ts" setup>
import type {
OnActionClickParams,
VxeTableGridOptions,
} from '#/adapter/vxe-table';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { SystemNotifyMessageApi } from '#/api/system/notify/message';
import { Page, useVbenModal } from '@vben/common-ui';
import { MdiCheckboxMarkedCircleOutline } from '@vben/icons';
import { Button, message } from 'ant-design-vue';
import { message } from 'ant-design-vue';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import {
getMyNotifyMessagePage,
updateAllNotifyMessageRead,
@ -32,12 +28,12 @@ function onRefresh() {
}
/** 查看站内信详情 */
function onDetail(row: SystemNotifyMessageApi.NotifyMessage) {
function handleDetail(row: SystemNotifyMessageApi.NotifyMessage) {
detailModalApi.setData(row).open();
}
/** 标记一条站内信已读 */
async function onRead(row: SystemNotifyMessageApi.NotifyMessage) {
async function handleRead(row: SystemNotifyMessageApi.NotifyMessage) {
message.loading({
content: '正在标记已读...',
duration: 0,
@ -46,15 +42,18 @@ async function onRead(row: SystemNotifyMessageApi.NotifyMessage) {
//
await updateNotifyMessageRead([row.id]);
//
message.success('标记已读成功');
message.success({
content: '标记已读成功',
key: 'action_process_msg',
});
onRefresh();
//
onDetail(row);
handleDetail(row);
}
/** 标记选中的站内信为已读 */
async function onMarkRead() {
async function handleMarkRead() {
const rows = gridApi.grid.getCheckboxRecords();
if (!rows || rows.length === 0) {
message.warning('请选择需要标记的站内信');
@ -62,48 +61,43 @@ async function onMarkRead() {
}
const ids = rows.map((row: SystemNotifyMessageApi.NotifyMessage) => row.id);
message.loading({
const hideLoading = message.loading({
content: '正在标记已读...',
duration: 0,
key: 'action_process_msg',
key: 'action_key_msg',
});
//
await updateNotifyMessageRead(ids);
//
message.success('标记已读成功');
await gridApi.grid.setAllCheckboxRow(false);
onRefresh();
try {
//
await updateNotifyMessageRead(ids);
//
message.success({
content: '标记已读成功',
key: 'action_key_msg',
});
await gridApi.grid.setAllCheckboxRow(false);
onRefresh();
} finally {
hideLoading();
}
}
/** 标记所有站内信为已读 */
async function onMarkAllRead() {
message.loading({
async function handleMarkAllRead() {
const hideLoading = message.loading({
content: '正在标记全部已读...',
duration: 0,
key: 'action_process_msg',
key: 'action_key_msg',
});
//
await updateAllNotifyMessageRead();
//
message.success('全部标记已读成功');
await gridApi.grid.setAllCheckboxRow(false);
onRefresh();
}
/** 表格操作按钮的回调函数 */
function onActionClick({
code,
row,
}: OnActionClickParams<SystemNotifyMessageApi.NotifyMessage>) {
switch (code) {
case 'detail': {
onDetail(row);
break;
}
case 'read': {
onRead(row);
break;
}
try {
//
await updateAllNotifyMessageRead();
//
message.success({
content: '全部标记已读成功',
key: 'action_key_msg',
});
await gridApi.grid.setAllCheckboxRow(false);
onRefresh();
} finally {
hideLoading();
}
}
@ -112,7 +106,7 @@ const [Grid, gridApi] = useVbenVxeGrid({
schema: useGridFormSchema(),
},
gridOptions: {
columns: useGridColumns(onActionClick),
columns: useGridColumns(),
height: 'auto',
keepSource: true,
proxyConfig: {
@ -150,14 +144,42 @@ const [Grid, gridApi] = useVbenVxeGrid({
<DetailModal @success="onRefresh" />
<Grid table-title="">
<template #toolbar-tools>
<Button type="primary" @click="onMarkRead">
<MdiCheckboxMarkedCircleOutline />
标记已读
</Button>
<Button type="primary" class="ml-2" @click="onMarkAllRead">
<MdiCheckboxMarkedCircleOutline />
全部已读
</Button>
<TableAction
:actions="[
{
label: '标记已读',
type: 'primary',
icon: 'mdi:checkbox-marked-circle-outline',
onClick: handleMarkRead,
},
{
label: '全部已读',
type: 'primary',
icon: 'mdi:checkbox-marked-circle-outline',
onClick: handleMarkAllRead,
},
]"
/>
</template>
<template #actions="{ row }">
<TableAction
:actions="[
{
label: '查看',
type: 'link',
ifShow: row.readStatus,
icon: ACTION_ICON.VIEW,
onClick: handleDetail.bind(null, row),
},
{
label: '已读',
type: 'link',
ifShow: !row.readStatus,
icon: ACTION_ICON.DELETE,
onClick: handleRead.bind(null, row),
},
]"
/>
</template>
</Grid>
</Page>

View File

@ -1,8 +1,5 @@
import type { VbenFormSchema } from '#/adapter/form';
import type { OnActionClickFn, VxeTableGridOptions } from '#/adapter/vxe-table';
import type { SystemNotifyTemplateApi } from '#/api/system/notify/template';
import { useAccess } from '@vben/access';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import { z } from '#/adapter/form';
import { getSimpleUserList } from '#/api/system/user';
@ -14,8 +11,6 @@ import {
UserTypeEnum,
} from '#/utils';
const { hasAccessByCodes } = useAccess();
/** 新增/修改的表单 */
export function useFormSchema(): VbenFormSchema[] {
return [
@ -229,9 +224,7 @@ export function useSendNotifyFormSchema(): VbenFormSchema[] {
}
/** 列表的字段 */
export function useGridColumns<T = SystemNotifyTemplateApi.NotifyTemplate>(
onActionClick: OnActionClickFn<T>,
): VxeTableGridOptions['columns'] {
export function useGridColumns(): VxeTableGridOptions['columns'] {
return [
{
field: 'id',
@ -288,34 +281,10 @@ export function useGridColumns<T = SystemNotifyTemplateApi.NotifyTemplate>(
formatter: 'formatDateTime',
},
{
field: 'operation',
title: '操作',
minWidth: 180,
align: 'center',
width: 220,
fixed: 'right',
cellRender: {
attrs: {
nameField: 'name',
nameTitle: '站内信模板',
onClick: onActionClick,
},
name: 'CellOperation',
options: [
{
code: 'edit',
show: hasAccessByCodes(['system:notify-template:update']),
},
{
code: 'send',
text: '测试',
show: hasAccessByCodes(['system:notify-template:send-notify']),
},
{
code: 'delete',
show: hasAccessByCodes(['system:notify-template:delete']),
},
],
},
slots: { default: 'actions' },
},
];
}

View File

@ -1,17 +1,13 @@
<script lang="ts" setup>
import type {
OnActionClickParams,
VxeTableGridOptions,
} from '#/adapter/vxe-table';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { SystemNotifyTemplateApi } from '#/api/system/notify/template';
import { Page, useVbenModal } from '@vben/common-ui';
import { Download, Plus } from '@vben/icons';
import { downloadFileFromBlobPart } from '@vben/utils';
import { Button, message } from 'ant-design-vue';
import { message } from 'ant-design-vue';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import {
deleteNotifyTemplate,
exportNotifyTemplate,
@ -40,69 +36,50 @@ function onRefresh() {
}
/** 导出表格 */
async function onExport() {
async function handleExport() {
const data = await exportNotifyTemplate(await gridApi.formApi.getValues());
downloadFileFromBlobPart({ fileName: '站内信模板.xls', source: data });
}
/** 创建站内信模板 */
function onCreate() {
function handleCreate() {
formModalApi.setData(null).open();
}
/** 编辑站内信模板 */
function onEdit(row: SystemNotifyTemplateApi.NotifyTemplate) {
function handleEdit(row: SystemNotifyTemplateApi.NotifyTemplate) {
formModalApi.setData(row).open();
}
/** 发送测试站内信 */
function onSend(row: SystemNotifyTemplateApi.NotifyTemplate) {
function handleSend(row: SystemNotifyTemplateApi.NotifyTemplate) {
sendModalApi.setData(row).open();
}
/** 删除站内信模板 */
async function onDelete(row: SystemNotifyTemplateApi.NotifyTemplate) {
async function handleDelete(row: SystemNotifyTemplateApi.NotifyTemplate) {
const hideLoading = message.loading({
content: $t('ui.actionMessage.deleting', [row.name]),
duration: 0,
key: 'action_process_msg',
key: 'action_key_msg',
});
try {
await deleteNotifyTemplate(row.id as number);
message.success($t('ui.actionMessage.deleteSuccess', [row.name]));
message.success({
content: $t('ui.actionMessage.deleteSuccess', [row.name]),
key: 'action_key_msg',
});
onRefresh();
} finally {
hideLoading();
}
}
/** 表格操作按钮的回调函数 */
function onActionClick({
code,
row,
}: OnActionClickParams<SystemNotifyTemplateApi.NotifyTemplate>) {
switch (code) {
case 'delete': {
onDelete(row);
break;
}
case 'edit': {
onEdit(row);
break;
}
case 'send': {
onSend(row);
break;
}
}
}
const [Grid, gridApi] = useVbenVxeGrid({
formOptions: {
schema: useGridFormSchema(),
},
gridOptions: {
columns: useGridColumns(onActionClick),
columns: useGridColumns(),
height: 'auto',
keepSource: true,
proxyConfig: {
@ -137,23 +114,55 @@ const [Grid, gridApi] = useVbenVxeGrid({
<SendModal />
<Grid table-title="">
<template #toolbar-tools>
<Button
type="primary"
@click="onCreate"
v-access:code="['system:notify-template:create']"
>
<Plus class="size-5" />
{{ $t('ui.actionTitle.create', ['站内信模板']) }}
</Button>
<Button
type="primary"
class="ml-2"
@click="onExport"
v-access:code="['system:notify-template:export']"
>
<Download class="size-5" />
{{ $t('ui.actionTitle.export') }}
</Button>
<TableAction
:actions="[
{
label: $t('ui.actionTitle.create', ['短信渠道']),
type: 'primary',
icon: ACTION_ICON.ADD,
auth: ['system:notify-template:create'],
onClick: handleCreate,
},
{
label: $t('ui.actionTitle.export'),
type: 'primary',
icon: ACTION_ICON.DOWNLOAD,
auth: ['system:notify-template:export'],
onClick: handleExport,
},
]"
/>
</template>
<template #actions="{ row }">
<TableAction
:actions="[
{
label: $t('common.edit'),
type: 'link',
icon: ACTION_ICON.EDIT,
auth: ['system:notify-template:update'],
onClick: handleEdit.bind(null, row),
},
{
label: '测试',
type: 'link',
icon: ACTION_ICON.ADD,
auth: ['system:notify-template:send-notify'],
onClick: handleSend.bind(null, row),
},
{
label: $t('common.delete'),
type: 'link',
danger: true,
icon: ACTION_ICON.DELETE,
auth: ['system:notify-template:delete'],
popConfirm: {
title: $t('ui.actionMessage.deleteConfirm', [row.name]),
confirm: handleDelete.bind(null, row),
},
},
]"
/>
</template>
</Grid>
</Page>

View File

@ -86,7 +86,7 @@ const [Modal, modalApi] = useVbenModal({
});
/** 动态构建表单 schema */
const buildFormSchema = () => {
function buildFormSchema() {
const schema = useSendNotifyFormSchema();
if (formData.value?.params) {
formData.value.params.forEach((param: string) => {
@ -102,7 +102,7 @@ const buildFormSchema = () => {
});
}
return schema;
};
}
</script>
<template>

View File

@ -1,14 +1,9 @@
import type { VbenFormSchema } from '#/adapter/form';
import type { OnActionClickFn, VxeTableGridOptions } from '#/adapter/vxe-table';
import type { SystemOAuth2ClientApi } from '#/api/system/oauth2/client';
import { useAccess } from '@vben/access';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import { z } from '#/adapter/form';
import { CommonStatusEnum, DICT_TYPE, getDictOptions } from '#/utils';
const { hasAccessByCodes } = useAccess();
/** 新增/修改的表单 */
export function useFormSchema(): VbenFormSchema[] {
return [
@ -191,9 +186,7 @@ export function useGridFormSchema(): VbenFormSchema[] {
}
/** 列表的字段 */
export function useGridColumns<T = SystemOAuth2ClientApi.OAuth2Client>(
onActionClick: OnActionClickFn<T>,
): VxeTableGridOptions['columns'] {
export function useGridColumns(): VxeTableGridOptions['columns'] {
return [
{
field: 'clientId',
@ -251,29 +244,10 @@ export function useGridColumns<T = SystemOAuth2ClientApi.OAuth2Client>(
formatter: 'formatDateTime',
},
{
field: 'operation',
title: '操作',
minWidth: 130,
align: 'center',
width: 130,
fixed: 'right',
cellRender: {
attrs: {
nameField: 'name',
nameTitle: 'OAuth2 客户端',
onClick: onActionClick,
},
name: 'CellOperation',
options: [
{
code: 'edit',
show: hasAccessByCodes(['system:oauth2-client:update']),
},
{
code: 'delete',
show: hasAccessByCodes(['system:oauth2-client:delete']),
},
],
},
slots: { default: 'actions' },
},
];
}

View File

@ -1,16 +1,12 @@
<script lang="ts" setup>
import type {
OnActionClickParams,
VxeTableGridOptions,
} from '#/adapter/vxe-table';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { SystemOAuth2ClientApi } from '#/api/system/oauth2/client';
import { Page, useVbenModal } from '@vben/common-ui';
import { Plus } from '@vben/icons';
import { Button, message } from 'ant-design-vue';
import { message } from 'ant-design-vue';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import {
deleteOAuth2Client,
getOAuth2ClientPage,
@ -32,54 +28,39 @@ function onRefresh() {
}
/** 创建 OAuth2 客户端 */
function onCreate() {
function handleCreate() {
formModalApi.setData(null).open();
}
/** 编辑 OAuth2 客户端 */
function onEdit(row: SystemOAuth2ClientApi.OAuth2Client) {
function handleEdit(row: SystemOAuth2ClientApi.OAuth2Client) {
formModalApi.setData(row).open();
}
/** 删除 OAuth2 客户端 */
async function onDelete(row: SystemOAuth2ClientApi.OAuth2Client) {
async function handleDelete(row: SystemOAuth2ClientApi.OAuth2Client) {
const hideLoading = message.loading({
content: $t('ui.actionMessage.deleting', [row.name]),
duration: 0,
key: 'action_process_msg',
key: 'action_key_msg',
});
try {
await deleteOAuth2Client(row.id as number);
message.success($t('ui.actionMessage.deleteSuccess', [row.name]));
message.success({
content: $t('ui.actionMessage.deleteSuccess', [row.name]),
key: 'action_key_msg',
});
onRefresh();
} catch {
} finally {
hideLoading();
}
}
/** 表格操作按钮的回调函数 */
function onActionClick({
code,
row,
}: OnActionClickParams<SystemOAuth2ClientApi.OAuth2Client>) {
switch (code) {
case 'delete': {
onDelete(row);
break;
}
case 'edit': {
onEdit(row);
break;
}
}
}
const [Grid, gridApi] = useVbenVxeGrid({
formOptions: {
schema: useGridFormSchema(),
},
gridOptions: {
columns: useGridColumns(onActionClick),
columns: useGridColumns(),
height: 'auto',
keepSource: true,
proxyConfig: {
@ -116,14 +97,41 @@ const [Grid, gridApi] = useVbenVxeGrid({
<FormModal @success="onRefresh" />
<Grid table-title="OAuth2 ">
<template #toolbar-tools>
<Button
type="primary"
@click="onCreate"
v-access:code="['system:oauth2-client:create']"
>
<Plus class="size-5" />
{{ $t('ui.actionTitle.create', [' OAuth2.0 客户端']) }}
</Button>
<TableAction
:actions="[
{
label: $t('ui.actionTitle.create', [' OAuth2.0 客户端']),
type: 'primary',
icon: ACTION_ICON.ADD,
auth: ['system:oauth2-client:create'],
onClick: handleCreate,
},
]"
/>
</template>
<template #actions="{ row }">
<TableAction
:actions="[
{
label: $t('common.edit'),
type: 'link',
icon: ACTION_ICON.EDIT,
auth: ['system:oauth2-client:update'],
onClick: handleEdit.bind(null, row),
},
{
label: $t('common.delete'),
type: 'link',
danger: true,
icon: ACTION_ICON.DELETE,
auth: ['system:oauth2-client:delete'],
popConfirm: {
title: $t('ui.actionMessage.deleteConfirm', [row.name]),
confirm: handleDelete.bind(null, row),
},
},
]"
/>
</template>
</Grid>
</Page>

View File

@ -1,13 +1,8 @@
import type { VbenFormSchema } from '#/adapter/form';
import type { OnActionClickFn, VxeTableGridOptions } from '#/adapter/vxe-table';
import type { SystemOAuth2TokenApi } from '#/api/system/oauth2/token';
import { useAccess } from '@vben/access';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import { DICT_TYPE, getDictOptions } from '#/utils';
const { hasAccessByCodes } = useAccess();
/** 列表的搜索表单 */
export function useGridFormSchema(): VbenFormSchema[] {
return [
@ -40,9 +35,7 @@ export function useGridFormSchema(): VbenFormSchema[] {
}
/** 列表的字段 */
export function useGridColumns<T = SystemOAuth2TokenApi.OAuth2Token>(
onActionClick: OnActionClickFn<T>,
): VxeTableGridOptions['columns'] {
export function useGridColumns(): VxeTableGridOptions['columns'] {
return [
{
field: 'accessToken',
@ -86,26 +79,10 @@ export function useGridColumns<T = SystemOAuth2TokenApi.OAuth2Token>(
formatter: 'formatDateTime',
},
{
field: 'operation',
title: '操作',
minWidth: 100,
align: 'center',
width: 80,
fixed: 'right',
cellRender: {
attrs: {
nameField: 'accessToken',
nameTitle: 'OAuth2 令牌',
onClick: onActionClick,
},
name: 'CellOperation',
options: [
{
code: 'delete',
text: '强退',
show: hasAccessByCodes(['system:oauth2-token:delete']),
},
],
},
slots: { default: 'actions' },
},
];
}

View File

@ -1,15 +1,12 @@
<script lang="ts" setup>
import type {
OnActionClickParams,
VxeTableGridOptions,
} from '#/adapter/vxe-table';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { SystemOAuth2TokenApi } from '#/api/system/oauth2/token';
import { Page } from '@vben/common-ui';
import { message } from 'ant-design-vue';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import {
deleteOAuth2Token,
getOAuth2TokenPage,
@ -25,40 +22,29 @@ function onRefresh() {
}
/** 删除 OAuth2 令牌 */
async function onDelete(row: SystemOAuth2TokenApi.OAuth2Token) {
async function handleDelete(row: SystemOAuth2TokenApi.OAuth2Token) {
const hideLoading = message.loading({
content: $t('ui.actionMessage.deleting', ['令牌']),
duration: 0,
key: 'action_process_msg',
key: 'action_key_msg',
});
try {
await deleteOAuth2Token(row.accessToken);
message.success($t('ui.actionMessage.operationSuccess'));
message.success({
content: $t('ui.actionMessage.deleteSuccess', ['令牌']),
key: 'action_key_msg',
});
onRefresh();
} catch {
} finally {
hideLoading();
}
}
/** 表格操作按钮的回调函数 */
function onActionClick({
code,
row,
}: OnActionClickParams<SystemOAuth2TokenApi.OAuth2Token>) {
switch (code) {
case 'delete': {
onDelete(row);
break;
}
}
}
const [Grid, gridApi] = useVbenVxeGrid({
formOptions: {
schema: useGridFormSchema(),
},
gridOptions: {
columns: useGridColumns(onActionClick),
columns: useGridColumns(),
height: 'auto',
keepSource: true,
proxyConfig: {
@ -92,6 +78,24 @@ const [Grid, gridApi] = useVbenVxeGrid({
/>
</template>
<Grid table-title="" />
<Grid table-title="">
<template #actions="{ row }">
<TableAction
:actions="[
{
label: '强退',
type: 'link',
danger: true,
icon: ACTION_ICON.DELETE,
auth: ['system:oauth2-token:delete'],
popConfirm: {
title: $t('ui.actionMessage.deleteConfirm', [row.name]),
confirm: handleDelete.bind(null, row),
},
},
]"
/>
</template>
</Grid>
</Page>
</template>

View File

@ -1,14 +1,9 @@
import type { VbenFormSchema } from '#/adapter/form';
import type { OnActionClickFn, VxeTableGridOptions } from '#/adapter/vxe-table';
import type { SystemOperateLogApi } from '#/api/system/operate-log';
import { useAccess } from '@vben/access';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import { getSimpleUserList } from '#/api/system/user';
import { getRangePickerDefaultProps } from '#/utils';
const { hasAccessByCodes } = useAccess();
/** 列表的搜索表单 */
export function useGridFormSchema(): VbenFormSchema[] {
return [
@ -75,9 +70,7 @@ export function useGridFormSchema(): VbenFormSchema[] {
}
/** 列表的字段 */
export function useGridColumns<T = SystemOperateLogApi.OperateLog>(
onActionClick: OnActionClickFn<T>,
): VxeTableGridOptions['columns'] {
export function useGridColumns(): VxeTableGridOptions['columns'] {
return [
{
field: 'id',
@ -121,26 +114,10 @@ export function useGridColumns<T = SystemOperateLogApi.OperateLog>(
minWidth: 120,
},
{
field: 'operation',
title: '操作',
minWidth: 120,
align: 'center',
width: 80,
fixed: 'right',
cellRender: {
attrs: {
nameField: 'action',
nameTitle: '操作日志',
onClick: onActionClick,
},
name: 'CellOperation',
options: [
{
code: 'detail',
text: '详情',
show: hasAccessByCodes(['system:operate-log:query']),
},
],
},
slots: { default: 'actions' },
},
];
}

View File

@ -1,17 +1,11 @@
<script lang="ts" setup>
import type {
OnActionClickParams,
VxeTableGridOptions,
} from '#/adapter/vxe-table';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { SystemOperateLogApi } from '#/api/system/operate-log';
import { Page, useVbenModal } from '@vben/common-ui';
import { Download } from '@vben/icons';
import { downloadFileFromBlobPart } from '@vben/utils';
import { Button } from 'ant-design-vue';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import { exportOperateLog, getOperateLogPage } from '#/api/system/operate-log';
import { DocAlert } from '#/components/doc-alert';
import { $t } from '#/locales';
@ -30,35 +24,22 @@ function onRefresh() {
}
/** 导出表格 */
async function onExport() {
async function handleExport() {
const data = await exportOperateLog(await gridApi.formApi.getValues());
downloadFileFromBlobPart({ fileName: '操作日志.xls', source: data });
}
/** 查看操作日志详情 */
function onDetail(row: SystemOperateLogApi.OperateLog) {
function handleDetail(row: SystemOperateLogApi.OperateLog) {
detailModalApi.setData(row).open();
}
/** 表格操作按钮的回调函数 */
function onActionClick({
code,
row,
}: OnActionClickParams<SystemOperateLogApi.OperateLog>) {
switch (code) {
case 'detail': {
onDetail(row);
break;
}
}
}
const [Grid, gridApi] = useVbenVxeGrid({
formOptions: {
schema: useGridFormSchema(),
},
gridOptions: {
columns: useGridColumns(onActionClick),
columns: useGridColumns(),
height: 'auto',
keepSource: true,
proxyConfig: {
@ -92,15 +73,30 @@ const [Grid, gridApi] = useVbenVxeGrid({
<DetailModal @success="onRefresh" />
<Grid table-title="">
<template #toolbar-tools>
<Button
type="primary"
class="ml-2"
@click="onExport"
v-access:code="['system:operate-log:export']"
>
<Download class="size-5" />
{{ $t('ui.actionTitle.export') }}
</Button>
<TableAction
:actions="[
{
label: $t('ui.actionTitle.export'),
type: 'primary',
icon: ACTION_ICON.DOWNLOAD,
auth: ['system:operate-log:export'],
onClick: handleExport,
},
]"
/>
</template>
<template #actions="{ row }">
<TableAction
:actions="[
{
label: $t('common.detail'),
type: 'link',
icon: ACTION_ICON.VIEW,
auth: ['system:operate-log:query'],
onClick: handleDetail.bind(null, row),
},
]"
/>
</template>
</Grid>
</Page>

View File

@ -1,14 +1,9 @@
import type { VbenFormSchema } from '#/adapter/form';
import type { OnActionClickFn, VxeTableGridOptions } from '#/adapter/vxe-table';
import type { SystemPostApi } from '#/api/system/post';
import { useAccess } from '@vben/access';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import { z } from '#/adapter/form';
import { CommonStatusEnum, DICT_TYPE, getDictOptions } from '#/utils';
const { hasAccessByCodes } = useAccess();
/** 新增/修改的表单 */
export function useFormSchema(): VbenFormSchema[] {
return [
@ -86,9 +81,7 @@ export function useGridFormSchema(): VbenFormSchema[] {
}
/** 列表的字段 */
export function useGridColumns<T = SystemPostApi.Post>(
onActionClick: OnActionClickFn<T>,
): VxeTableGridOptions['columns'] {
export function useGridColumns(): VxeTableGridOptions['columns'] {
return [
{
field: 'id',
@ -131,29 +124,10 @@ export function useGridColumns<T = SystemPostApi.Post>(
formatter: 'formatDateTime',
},
{
field: 'operation',
title: '操作',
minWidth: 130,
align: 'center',
width: 130,
fixed: 'right',
cellRender: {
attrs: {
nameField: 'name',
nameTitle: '岗位',
onClick: onActionClick,
},
name: 'CellOperation',
options: [
{
code: 'edit',
show: hasAccessByCodes(['system:post:update']),
},
{
code: 'delete',
show: hasAccessByCodes(['system:post:delete']),
},
],
},
slots: { default: 'actions' },
},
];
}

View File

@ -1,17 +1,13 @@
<script lang="ts" setup>
import type {
OnActionClickParams,
VxeTableGridOptions,
} from '#/adapter/vxe-table';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { SystemPostApi } from '#/api/system/post';
import { Page, useVbenModal } from '@vben/common-ui';
import { Download, Plus } from '@vben/icons';
import { downloadFileFromBlobPart } from '@vben/utils';
import { Button, message } from 'ant-design-vue';
import { message } from 'ant-design-vue';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import { deletePost, exportPost, getPostPage } from '#/api/system/post';
import { $t } from '#/locales';
@ -29,57 +25,45 @@ function onRefresh() {
}
/** 导出表格 */
async function onExport() {
async function handleExport() {
const data = await exportPost(await gridApi.formApi.getValues());
downloadFileFromBlobPart({ fileName: '岗位.xls', source: data });
}
/** 创建岗位 */
function onCreate() {
function handleCreate() {
formModalApi.setData(null).open();
}
/** 编辑岗位 */
function onEdit(row: SystemPostApi.Post) {
function handleEdit(row: SystemPostApi.Post) {
formModalApi.setData(row).open();
}
/** 删除岗位 */
async function onDelete(row: SystemPostApi.Post) {
async function handleDelete(row: SystemPostApi.Post) {
const hideLoading = message.loading({
content: $t('ui.actionMessage.deleting', [row.name]),
duration: 0,
key: 'action_process_msg',
key: 'action_key_msg',
});
try {
await deletePost(row.id as number);
message.success($t('ui.actionMessage.deleteSuccess', [row.name]));
message.success({
content: $t('ui.actionMessage.deleteSuccess', [row.name]),
key: 'action_key_msg',
});
onRefresh();
} catch {
} finally {
hideLoading();
}
}
/** 表格操作按钮的回调函数 */
function onActionClick({ code, row }: OnActionClickParams<SystemPostApi.Post>) {
switch (code) {
case 'delete': {
onDelete(row);
break;
}
case 'edit': {
onEdit(row);
break;
}
}
}
const [Grid, gridApi] = useVbenVxeGrid({
formOptions: {
schema: useGridFormSchema(),
},
gridOptions: {
columns: useGridColumns(onActionClick),
columns: useGridColumns(),
height: 'auto',
keepSource: true,
proxyConfig: {
@ -109,23 +93,48 @@ const [Grid, gridApi] = useVbenVxeGrid({
<FormModal @success="onRefresh" />
<Grid table-title="">
<template #toolbar-tools>
<Button
type="primary"
@click="onCreate"
v-access:code="['system:post:create']"
>
<Plus class="size-5" />
{{ $t('ui.actionTitle.create', ['岗位']) }}
</Button>
<Button
type="primary"
class="ml-2"
@click="onExport"
v-access:code="['system:post:export']"
>
<Download class="size-5" />
{{ $t('ui.actionTitle.export') }}
</Button>
<TableAction
:actions="[
{
label: $t('ui.actionTitle.create', ['短信渠道']),
type: 'primary',
icon: ACTION_ICON.ADD,
auth: ['system:post:create'],
onClick: handleCreate,
},
{
label: $t('ui.actionTitle.export'),
type: 'primary',
icon: ACTION_ICON.DOWNLOAD,
auth: ['system:post:export'],
onClick: handleExport,
},
]"
/>
</template>
<template #actions="{ row }">
<TableAction
:actions="[
{
label: $t('common.edit'),
type: 'link',
icon: ACTION_ICON.EDIT,
auth: ['system:post:update'],
onClick: handleEdit.bind(null, row),
},
{
label: $t('common.delete'),
type: 'link',
danger: true,
icon: ACTION_ICON.DELETE,
auth: ['system:post:delete'],
popConfirm: {
title: $t('ui.actionMessage.deleteConfirm', [row.name]),
confirm: handleDelete.bind(null, row),
},
},
]"
/>
</template>
</Grid>
</Page>

View File

@ -3,15 +3,13 @@ import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { SystemRoleApi } from '#/api/system/role';
import { Page, useVbenModal } from '@vben/common-ui';
import { Download, Plus } from '@vben/icons';
import { downloadFileFromBlobPart } from '@vben/utils';
import { Button, message } from 'ant-design-vue';
import { message } from 'ant-design-vue';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import { deleteRole, exportRole, getRolePage } from '#/api/system/role';
import { DocAlert } from '#/components/doc-alert';
import { TableAction } from '#/components/table-action';
import { $t } from '#/locales';
import { useGridColumns, useGridFormSchema } from './data';
@ -41,45 +39,46 @@ function onRefresh() {
}
/** 导出表格 */
async function onExport() {
async function handleExport() {
const data = await exportRole(await gridApi.formApi.getValues());
downloadFileFromBlobPart({ fileName: '角色.xls', source: data });
}
/** 编辑角色 */
function onEdit(row: SystemRoleApi.Role) {
function handleEdit(row: SystemRoleApi.Role) {
formModalApi.setData(row).open();
}
/** 创建角色 */
function onCreate() {
function handleCreate() {
formModalApi.setData(null).open();
}
/** 删除角色 */
async function onDelete(row: SystemRoleApi.Role) {
async function handleDelete(row: SystemRoleApi.Role) {
const hideLoading = message.loading({
content: $t('ui.actionMessage.deleting', [row.name]),
duration: 0,
key: 'action_process_msg',
key: 'action_key_msg',
});
try {
await deleteRole(row.id as number);
hideLoading();
message.success($t('ui.actionMessage.deleteSuccess', [row.name]));
message.success({
content: $t('ui.actionMessage.deleteSuccess', [row.name]),
key: 'action_key_msg',
});
onRefresh();
} catch {
} finally {
hideLoading();
}
}
/** 分配角色的数据权限 */
function onAssignDataPermission(row: SystemRoleApi.Role) {
function handleAssignDataPermission(row: SystemRoleApi.Role) {
assignDataPermissionFormApi.setData(row).open();
}
/** 分配角色的菜单权限 */
function onAssignMenu(row: SystemRoleApi.Role) {
function handleAssignMenu(row: SystemRoleApi.Role) {
assignMenuFormApi.setData(row).open();
}
@ -128,23 +127,24 @@ const [Grid, gridApi] = useVbenVxeGrid({
<AssignMenuFormModel @success="onRefresh" />
<Grid table-title="">
<template #toolbar-tools>
<Button
type="primary"
@click="onCreate"
v-access:code="['system:role:create']"
>
<Plus class="size-5" />
{{ $t('ui.actionTitle.create', ['角色']) }}
</Button>
<Button
type="primary"
class="ml-2"
@click="onExport"
v-access:code="['system:role:export']"
>
<Download class="size-5" />
{{ $t('ui.actionTitle.export') }}
</Button>
<TableAction
:actions="[
{
label: $t('ui.actionTitle.create', ['角色']),
type: 'primary',
icon: ACTION_ICON.ADD,
auth: ['system:role:create'],
onClick: handleCreate,
},
{
label: $t('ui.actionTitle.export'),
type: 'primary',
icon: ACTION_ICON.DOWNLOAD,
auth: ['system:role:export'],
onClick: handleExport,
},
]"
/>
</template>
<template #actions="{ row }">
<TableAction
@ -152,19 +152,19 @@ const [Grid, gridApi] = useVbenVxeGrid({
{
label: $t('common.edit'),
type: 'link',
icon: 'ant-design:edit-outlined',
icon: ACTION_ICON.EDIT,
auth: ['system:role:update'],
onClick: onEdit.bind(null, row),
onClick: handleEdit.bind(null, row),
},
{
label: $t('common.delete'),
type: 'link',
danger: true,
icon: 'ant-design:delete-outlined',
icon: ACTION_ICON.DELETE,
auth: ['system:role:delete'],
popConfirm: {
title: $t('ui.actionMessage.deleteConfirm', [row.name]),
confirm: onDelete.bind(null, row),
confirm: handleDelete.bind(null, row),
},
},
]"
@ -173,13 +173,13 @@ const [Grid, gridApi] = useVbenVxeGrid({
label: '数据权限',
type: 'link',
auth: ['system:permission:assign-role-data-scope'],
onClick: onAssignDataPermission.bind(null, row),
onClick: handleAssignDataPermission.bind(null, row),
},
{
label: '菜单权限',
type: 'link',
auth: ['system:permission:assign-role-menu'],
onClick: onAssignMenu.bind(null, row),
onClick: handleAssignMenu.bind(null, row),
},
]"
/>

View File

@ -1,8 +1,5 @@
import type { VbenFormSchema } from '#/adapter/form';
import type { OnActionClickFn, VxeTableGridOptions } from '#/adapter/vxe-table';
import type { SystemSmsChannelApi } from '#/api/system/sms/channel';
import { useAccess } from '@vben/access';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import { z } from '#/adapter/form';
import {
@ -12,8 +9,6 @@ import {
getRangePickerDefaultProps,
} from '#/utils';
const { hasAccessByCodes } = useAccess();
/** 新增/修改的表单 */
export function useFormSchema(): VbenFormSchema[] {
return [
@ -135,9 +130,7 @@ export function useGridFormSchema(): VbenFormSchema[] {
}
/** 列表的字段 */
export function useGridColumns<T = SystemSmsChannelApi.SmsChannel>(
onActionClick: OnActionClickFn<T>,
): VxeTableGridOptions['columns'] {
export function useGridColumns(): VxeTableGridOptions['columns'] {
return [
{
field: 'id',
@ -194,29 +187,10 @@ export function useGridColumns<T = SystemSmsChannelApi.SmsChannel>(
formatter: 'formatDateTime',
},
{
field: 'operation',
title: '操作',
minWidth: 180,
align: 'center',
width: 130,
fixed: 'right',
cellRender: {
attrs: {
nameField: 'signature',
nameTitle: '短信渠道',
onClick: onActionClick,
},
name: 'CellOperation',
options: [
{
code: 'edit',
show: hasAccessByCodes(['system:sms-channel:update']),
},
{
code: 'delete',
show: hasAccessByCodes(['system:sms-channel:delete']),
},
],
},
slots: { default: 'actions' },
},
];
}

View File

@ -1,17 +1,13 @@
<script lang="ts" setup>
import type {
OnActionClickParams,
VxeTableGridOptions,
} from '#/adapter/vxe-table';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { SystemSmsChannelApi } from '#/api/system/sms/channel';
import { Page, useVbenModal } from '@vben/common-ui';
import { Download, Plus } from '@vben/icons';
import { downloadFileFromBlobPart } from '@vben/utils';
import { Button, message } from 'ant-design-vue';
import { message } from 'ant-design-vue';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import {
deleteSmsChannel,
exportSmsChannel,
@ -34,60 +30,45 @@ function onRefresh() {
}
/** 导出表格 */
async function onExport() {
async function handleExport() {
const data = await exportSmsChannel(await gridApi.formApi.getValues());
downloadFileFromBlobPart({ fileName: '短信渠道.xls', source: data });
}
/** 创建短信渠道 */
function onCreate() {
function handleCreate() {
formModalApi.setData(null).open();
}
/** 编辑短信渠道 */
function onEdit(row: SystemSmsChannelApi.SmsChannel) {
function handleEdit(row: SystemSmsChannelApi.SmsChannel) {
formModalApi.setData(row).open();
}
/** 删除短信渠道 */
async function onDelete(row: SystemSmsChannelApi.SmsChannel) {
async function handleDelete(row: SystemSmsChannelApi.SmsChannel) {
const hideLoading = message.loading({
content: $t('ui.actionMessage.deleting', [row.signature]),
duration: 0,
key: 'action_process_msg',
key: 'action_key_msg',
});
try {
await deleteSmsChannel(row.id as number);
message.success($t('ui.actionMessage.deleteSuccess', [row.signature]));
message.success({
content: $t('ui.actionMessage.deleteSuccess', [row.signature]),
key: 'action_key_msg',
});
onRefresh();
} catch {
} finally {
hideLoading();
}
}
/** 表格操作按钮的回调函数 */
function onActionClick({
code,
row,
}: OnActionClickParams<SystemSmsChannelApi.SmsChannel>) {
switch (code) {
case 'delete': {
onDelete(row);
break;
}
case 'edit': {
onEdit(row);
break;
}
}
}
const [Grid, gridApi] = useVbenVxeGrid({
formOptions: {
schema: useGridFormSchema(),
},
gridOptions: {
columns: useGridColumns(onActionClick),
columns: useGridColumns(),
height: 'auto',
keepSource: true,
proxyConfig: {
@ -121,23 +102,48 @@ const [Grid, gridApi] = useVbenVxeGrid({
<FormModal @success="onRefresh" />
<Grid table-title="">
<template #toolbar-tools>
<Button
type="primary"
@click="onCreate"
v-access:code="['system:sms-channel:create']"
>
<Plus class="size-5" />
{{ $t('ui.actionTitle.create', ['短信渠道']) }}
</Button>
<Button
type="primary"
class="ml-2"
@click="onExport"
v-access:code="['system:sms-channel:export']"
>
<Download class="size-5" />
{{ $t('ui.actionTitle.export') }}
</Button>
<TableAction
:actions="[
{
label: $t('ui.actionTitle.create', ['短信渠道']),
type: 'primary',
icon: ACTION_ICON.ADD,
auth: ['system:sms-channel:create'],
onClick: handleCreate,
},
{
label: $t('ui.actionTitle.export'),
type: 'primary',
icon: ACTION_ICON.DOWNLOAD,
auth: ['system:sms-channel:export'],
onClick: handleExport,
},
]"
/>
</template>
<template #actions="{ row }">
<TableAction
:actions="[
{
label: $t('common.edit'),
type: 'link',
icon: ACTION_ICON.EDIT,
auth: ['system:sms-channel:update'],
onClick: handleEdit.bind(null, row),
},
{
label: $t('common.delete'),
type: 'link',
danger: true,
icon: ACTION_ICON.DELETE,
auth: ['system:sms-channel:delete'],
popConfirm: {
title: $t('ui.actionMessage.deleteConfirm', [row.name]),
confirm: handleDelete.bind(null, row),
},
},
]"
/>
</template>
</Grid>
</Page>

View File

@ -1,14 +1,9 @@
import type { VbenFormSchema } from '#/adapter/form';
import type { OnActionClickFn, VxeTableGridOptions } from '#/adapter/vxe-table';
import type { SystemSmsLogApi } from '#/api/system/sms/log';
import { useAccess } from '@vben/access';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import { getSimpleSmsChannelList } from '#/api/system/sms/channel';
import { DICT_TYPE, getDictOptions, getRangePickerDefaultProps } from '#/utils';
const { hasAccessByCodes } = useAccess();
/** 列表的搜索表单 */
export function useGridFormSchema(): VbenFormSchema[] {
return [
@ -84,9 +79,7 @@ export function useGridFormSchema(): VbenFormSchema[] {
}
/** 列表的字段 */
export function useGridColumns<T = SystemSmsLogApi.SmsLog>(
onActionClick: OnActionClickFn<T>,
): VxeTableGridOptions['columns'] {
export function useGridColumns(): VxeTableGridOptions['columns'] {
return [
{
field: 'id',
@ -163,26 +156,10 @@ export function useGridColumns<T = SystemSmsLogApi.SmsLog>(
},
},
{
field: 'operation',
title: '操作',
minWidth: 120,
align: 'center',
width: 80,
fixed: 'right',
cellRender: {
attrs: {
nameField: 'mobile',
nameTitle: '短信日志',
onClick: onActionClick,
},
name: 'CellOperation',
options: [
{
code: 'detail',
text: '详情',
show: hasAccessByCodes(['system:sms-log:query']),
},
],
},
slots: { default: 'actions' },
},
];
}

View File

@ -1,17 +1,11 @@
<script lang="ts" setup>
import type {
OnActionClickParams,
VxeTableGridOptions,
} from '#/adapter/vxe-table';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { SystemSmsLogApi } from '#/api/system/sms/log';
import { Page, useVbenModal } from '@vben/common-ui';
import { Download } from '@vben/icons';
import { downloadFileFromBlobPart } from '@vben/utils';
import { Button } from 'ant-design-vue';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import { exportSmsLog, getSmsLogPage } from '#/api/system/sms/log';
import { DocAlert } from '#/components/doc-alert';
import { $t } from '#/locales';
@ -30,35 +24,22 @@ function onRefresh() {
}
/** 导出表格 */
async function onExport() {
async function handleExport() {
const data = await exportSmsLog(await gridApi.formApi.getValues());
downloadFileFromBlobPart({ fileName: '短信日志.xls', source: data });
}
/** 查看短信日志详情 */
function onDetail(row: SystemSmsLogApi.SmsLog) {
function handleDetail(row: SystemSmsLogApi.SmsLog) {
detailModalApi.setData(row).open();
}
/** 表格操作按钮的回调函数 */
function onActionClick({
code,
row,
}: OnActionClickParams<SystemSmsLogApi.SmsLog>) {
switch (code) {
case 'detail': {
onDetail(row);
break;
}
}
}
const [Grid, gridApi] = useVbenVxeGrid({
formOptions: {
schema: useGridFormSchema(),
},
gridOptions: {
columns: useGridColumns(onActionClick),
columns: useGridColumns(),
height: 'auto',
keepSource: true,
proxyConfig: {
@ -92,15 +73,30 @@ const [Grid, gridApi] = useVbenVxeGrid({
<DetailModal @success="onRefresh" />
<Grid table-title="">
<template #toolbar-tools>
<Button
type="primary"
class="ml-2"
@click="onExport"
v-access:code="['system:sms-log:export']"
>
<Download class="size-5" />
{{ $t('ui.actionTitle.export') }}
</Button>
<TableAction
:actions="[
{
label: $t('ui.actionTitle.export'),
type: 'primary',
icon: ACTION_ICON.DOWNLOAD,
auth: ['system:sms-log:export'],
onClick: handleExport,
},
]"
/>
</template>
<template #actions="{ row }">
<TableAction
:actions="[
{
label: $t('common.detail'),
type: 'link',
icon: ACTION_ICON.VIEW,
auth: ['system:sms-log:query'],
onClick: handleDetail.bind(null, row),
},
]"
/>
</template>
</Grid>
</Page>

View File

@ -1,8 +1,5 @@
import type { VbenFormSchema } from '#/adapter/form';
import type { OnActionClickFn, VxeTableGridOptions } from '#/adapter/vxe-table';
import type { SystemSmsTemplateApi } from '#/api/system/sms/template';
import { useAccess } from '@vben/access';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import { z } from '#/adapter/form';
import { getSimpleSmsChannelList } from '#/api/system/sms/channel';
@ -13,8 +10,6 @@ import {
getRangePickerDefaultProps,
} from '#/utils';
const { hasAccessByCodes } = useAccess();
/** 新增/修改的表单 */
export function useFormSchema(): VbenFormSchema[] {
return [
@ -204,9 +199,7 @@ export function useSendSmsFormSchema(): VbenFormSchema[] {
}
/** 列表的字段 */
export function useGridColumns<T = SystemSmsTemplateApi.SmsTemplate>(
onActionClick: OnActionClickFn<T>,
): VxeTableGridOptions['columns'] {
export function useGridColumns(): VxeTableGridOptions['columns'] {
return [
{
field: 'id',
@ -272,34 +265,10 @@ export function useGridColumns<T = SystemSmsTemplateApi.SmsTemplate>(
formatter: 'formatDateTime',
},
{
field: 'operation',
title: '操作',
minWidth: 180,
align: 'center',
width: 220,
fixed: 'right',
cellRender: {
attrs: {
nameField: 'name',
nameTitle: '短信模板',
onClick: onActionClick,
},
name: 'CellOperation',
options: [
{
code: 'edit',
show: hasAccessByCodes(['system:sms-template:update']),
},
{
code: 'delete',
show: hasAccessByCodes(['system:sms-template:delete']),
},
{
code: 'sms-send',
text: '发送短信',
show: hasAccessByCodes(['system:sms-template:send-sms']),
},
],
},
slots: { default: 'actions' },
},
];
}

View File

@ -1,17 +1,13 @@
<script lang="ts" setup>
import type {
OnActionClickParams,
VxeTableGridOptions,
} from '#/adapter/vxe-table';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { SystemSmsTemplateApi } from '#/api/system/sms/template';
import { Page, useVbenModal } from '@vben/common-ui';
import { Download, Plus } from '@vben/icons';
import { downloadFileFromBlobPart } from '@vben/utils';
import { Button, message } from 'ant-design-vue';
import { message } from 'ant-design-vue';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import {
deleteSmsTemplate,
exportSmsTemplate,
@ -40,69 +36,50 @@ function onRefresh() {
}
/** 导出表格 */
async function onExport() {
async function handleExport() {
const data = await exportSmsTemplate(await gridApi.formApi.getValues());
downloadFileFromBlobPart({ fileName: '短信模板.xls', source: data });
}
/** 创建短信模板 */
function onCreate() {
function handleCreate() {
formModalApi.setData(null).open();
}
/** 编辑短信模板 */
function onEdit(row: SystemSmsTemplateApi.SmsTemplate) {
function handleEdit(row: SystemSmsTemplateApi.SmsTemplate) {
formModalApi.setData(row).open();
}
/** 发送测试短信 */
function onSend(row: SystemSmsTemplateApi.SmsTemplate) {
function handleSend(row: SystemSmsTemplateApi.SmsTemplate) {
sendModalApi.setData(row).open();
}
/** 删除短信模板 */
async function onDelete(row: SystemSmsTemplateApi.SmsTemplate) {
async function handleDelete(row: SystemSmsTemplateApi.SmsTemplate) {
const hideLoading = message.loading({
content: $t('ui.actionMessage.deleting', [row.name]),
duration: 0,
key: 'action_process_msg',
key: 'action_key_msg',
});
try {
await deleteSmsTemplate(row.id as number);
message.success($t('ui.actionMessage.deleteSuccess', [row.name]));
message.success({
content: $t('ui.actionMessage.deleteSuccess', [row.name]),
key: 'action_key_msg',
});
onRefresh();
} finally {
hideLoading();
}
}
/** 表格操作按钮的回调函数 */
function onActionClick({
code,
row,
}: OnActionClickParams<SystemSmsTemplateApi.SmsTemplate>) {
switch (code) {
case 'delete': {
onDelete(row);
break;
}
case 'edit': {
onEdit(row);
break;
}
case 'sms-send': {
onSend(row);
break;
}
}
}
const [Grid, gridApi] = useVbenVxeGrid({
formOptions: {
schema: useGridFormSchema(),
},
gridOptions: {
columns: useGridColumns(onActionClick),
columns: useGridColumns(),
height: 'auto',
keepSource: true,
proxyConfig: {
@ -137,23 +114,55 @@ const [Grid, gridApi] = useVbenVxeGrid({
<SendModal />
<Grid table-title="">
<template #toolbar-tools>
<Button
type="primary"
@click="onCreate"
v-access:code="['system:sms-template:create']"
>
<Plus class="size-5" />
{{ $t('ui.actionTitle.create', ['短信模板']) }}
</Button>
<Button
type="primary"
class="ml-2"
@click="onExport"
v-access:code="['system:sms-template:export']"
>
<Download class="size-5" />
{{ $t('ui.actionTitle.export') }}
</Button>
<TableAction
:actions="[
{
label: $t('ui.actionTitle.create', ['短信模板']),
type: 'primary',
icon: ACTION_ICON.ADD,
auth: ['system:sms-template:create'],
onClick: handleCreate,
},
{
label: $t('ui.actionTitle.export'),
type: 'primary',
icon: ACTION_ICON.DOWNLOAD,
auth: ['system:sms-template:export'],
onClick: handleExport,
},
]"
/>
</template>
<template #actions="{ row }">
<TableAction
:actions="[
{
label: $t('common.edit'),
type: 'link',
icon: ACTION_ICON.EDIT,
auth: ['system:sms-template:update'],
onClick: handleEdit.bind(null, row),
},
{
label: '发送短信',
type: 'link',
icon: ACTION_ICON.ADD,
auth: ['system:sms-template:send-sms'],
onClick: handleSend.bind(null, row),
},
{
label: $t('common.delete'),
type: 'link',
danger: true,
icon: ACTION_ICON.DELETE,
auth: ['system:sms-template:delete'],
popConfirm: {
title: $t('ui.actionMessage.deleteConfirm', [row.name]),
confirm: handleDelete.bind(null, row),
},
},
]"
/>
</template>
</Grid>
</Page>

View File

@ -1,8 +1,5 @@
import type { VbenFormSchema } from '#/adapter/form';
import type { OnActionClickFn, VxeTableGridOptions } from '#/adapter/vxe-table';
import type { SystemSocialClientApi } from '#/api/system/social/client';
import { useAccess } from '@vben/access';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import { z } from '#/adapter/form';
import {
@ -12,8 +9,6 @@ import {
SystemUserSocialTypeEnum,
} from '#/utils';
const { hasAccessByCodes } = useAccess();
/** 新增/修改的表单 */
export function useFormSchema(): VbenFormSchema[] {
return [
@ -152,9 +147,7 @@ export function useGridFormSchema(): VbenFormSchema[] {
}
/** 列表的字段 */
export function useGridColumns<T = SystemSocialClientApi.SocialClient>(
onActionClick: OnActionClickFn<T>,
): VxeTableGridOptions['columns'] {
export function useGridColumns(): VxeTableGridOptions['columns'] {
return [
{
field: 'id',
@ -205,29 +198,10 @@ export function useGridColumns<T = SystemSocialClientApi.SocialClient>(
formatter: 'formatDateTime',
},
{
field: 'operation',
title: '操作',
minWidth: 130,
align: 'center',
width: 130,
fixed: 'right',
cellRender: {
attrs: {
nameField: 'name',
nameTitle: '社交客户端',
onClick: onActionClick,
},
name: 'CellOperation',
options: [
{
code: 'edit',
show: hasAccessByCodes(['system:social-client:update']),
},
{
code: 'delete',
show: hasAccessByCodes(['system:social-client:delete']),
},
],
},
slots: { default: 'actions' },
},
];
}

View File

@ -1,16 +1,12 @@
<script lang="ts" setup>
import type {
OnActionClickParams,
VxeTableGridOptions,
} from '#/adapter/vxe-table';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { SystemSocialClientApi } from '#/api/system/social/client';
import { Page, useVbenModal } from '@vben/common-ui';
import { Plus } from '@vben/icons';
import { Button, message } from 'ant-design-vue';
import { message } from 'ant-design-vue';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import {
deleteSocialClient,
getSocialClientPage,
@ -32,54 +28,39 @@ function onRefresh() {
}
/** 创建社交客户端 */
function onCreate() {
function handleCreate() {
formModalApi.setData(null).open();
}
/** 编辑社交客户端 */
function onEdit(row: SystemSocialClientApi.SocialClient) {
function handleEdit(row: SystemSocialClientApi.SocialClient) {
formModalApi.setData(row).open();
}
/** 删除社交客户端 */
async function onDelete(row: SystemSocialClientApi.SocialClient) {
async function handleDelete(row: SystemSocialClientApi.SocialClient) {
const hideLoading = message.loading({
content: $t('ui.actionMessage.deleting', [row.name]),
duration: 0,
key: 'action_process_msg',
key: 'action_key_msg',
});
try {
await deleteSocialClient(row.id as number);
message.success($t('ui.actionMessage.deleteSuccess', [row.name]));
message.success({
content: $t('ui.actionMessage.deleteSuccess', [row.name]),
key: 'action_key_msg',
});
onRefresh();
} catch {
} finally {
hideLoading();
}
}
/** 表格操作按钮的回调函数 */
function onActionClick({
code,
row,
}: OnActionClickParams<SystemSocialClientApi.SocialClient>) {
switch (code) {
case 'delete': {
onDelete(row);
break;
}
case 'edit': {
onEdit(row);
break;
}
}
}
const [Grid, gridApi] = useVbenVxeGrid({
formOptions: {
schema: useGridFormSchema(),
},
gridOptions: {
columns: useGridColumns(onActionClick),
columns: useGridColumns(),
height: 'auto',
keepSource: true,
proxyConfig: {
@ -113,14 +94,41 @@ const [Grid, gridApi] = useVbenVxeGrid({
<FormModal @success="onRefresh" />
<Grid table-title="">
<template #toolbar-tools>
<Button
type="primary"
@click="onCreate"
v-access:code="['system:social-client:create']"
>
<Plus class="size-5" />
{{ $t('ui.actionTitle.create', ['社交客户端']) }}
</Button>
<TableAction
:actions="[
{
label: $t('ui.actionTitle.create', ['社交客户端']),
type: 'primary',
icon: ACTION_ICON.ADD,
auth: ['system:social-client:create'],
onClick: handleCreate,
},
]"
/>
</template>
<template #actions="{ row }">
<TableAction
:actions="[
{
label: $t('common.edit'),
type: 'link',
icon: ACTION_ICON.EDIT,
auth: ['system:social-client:update'],
onClick: handleEdit.bind(null, row),
},
{
label: $t('common.delete'),
type: 'link',
danger: true,
icon: ACTION_ICON.DELETE,
auth: ['system:social-client:delete'],
popConfirm: {
title: $t('ui.actionMessage.deleteConfirm', [row.name]),
confirm: handleDelete.bind(null, row),
},
},
]"
/>
</template>
</Grid>
</Page>

View File

@ -1,13 +1,8 @@
import type { VbenFormSchema } from '#/adapter/form';
import type { OnActionClickFn, VxeTableGridOptions } from '#/adapter/vxe-table';
import type { SystemSocialUserApi } from '#/api/system/social/user';
import { useAccess } from '@vben/access';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import { DICT_TYPE, getDictOptions, getRangePickerDefaultProps } from '#/utils';
const { hasAccessByCodes } = useAccess();
/** 列表的搜索表单 */
export function useGridFormSchema(): VbenFormSchema[] {
return [
@ -52,9 +47,7 @@ export function useGridFormSchema(): VbenFormSchema[] {
}
/** 列表的字段 */
export function useGridColumns<T = SystemSocialUserApi.SocialUser>(
onActionClick: OnActionClickFn<T>,
): VxeTableGridOptions['columns'] {
export function useGridColumns(): VxeTableGridOptions['columns'] {
return [
{
field: 'type',
@ -96,26 +89,10 @@ export function useGridColumns<T = SystemSocialUserApi.SocialUser>(
formatter: 'formatDateTime',
},
{
field: 'operation',
title: '操作',
minWidth: 100,
align: 'center',
width: 80,
fixed: 'right',
cellRender: {
attrs: {
nameField: 'nickname',
nameTitle: '社交用户',
onClick: onActionClick,
},
name: 'CellOperation',
options: [
{
code: 'detail',
text: '详情',
show: hasAccessByCodes(['system:social-user:query']),
},
],
},
slots: { default: 'actions' },
},
];
}

View File

@ -1,13 +1,10 @@
<script lang="ts" setup>
import type {
OnActionClickParams,
VxeTableGridOptions,
} from '#/adapter/vxe-table';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { SystemSocialUserApi } from '#/api/system/social/user';
import { Page, useVbenModal } from '@vben/common-ui';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import { getSocialUserPage } from '#/api/system/social/user';
import { DocAlert } from '#/components/doc-alert';
@ -19,35 +16,17 @@ const [DetailModal, detailModalApi] = useVbenModal({
destroyOnClose: true,
});
/** 刷新表格 */
// function onRefresh() {
// gridApi.query();
// }
/** 查看详情 */
function onDetail(row: SystemSocialUserApi.SocialUser) {
function handleDetail(row: SystemSocialUserApi.SocialUser) {
detailModalApi.setData(row).open();
}
/** 表格操作按钮的回调函数 */
function onActionClick({
code,
row,
}: OnActionClickParams<SystemSocialUserApi.SocialUser>) {
switch (code) {
case 'detail': {
onDetail(row);
break;
}
}
}
const [Grid] = useVbenVxeGrid({
formOptions: {
schema: useGridFormSchema(),
},
gridOptions: {
columns: useGridColumns(onActionClick),
columns: useGridColumns(),
height: 'auto',
keepSource: true,
proxyConfig: {
@ -79,6 +58,20 @@ const [Grid] = useVbenVxeGrid({
</template>
<DetailModal />
<Grid table-title="" />
<Grid table-title="">
<template #actions="{ row }">
<TableAction
:actions="[
{
label: $t('common.detail'),
type: 'link',
icon: ACTION_ICON.VIEW,
auth: ['system:social-user:query'],
onClick: handleDetail.bind(null, row),
},
]"
/>
</template>
</Grid>
</Page>
</template>

View File

@ -6,12 +6,11 @@ import type { SystemTenantPackageApi } from '#/api/system/tenant-package';
import { onMounted, ref } from 'vue';
import { Page, useVbenModal } from '@vben/common-ui';
import { Download, Plus } from '@vben/icons';
import { downloadFileFromBlobPart } from '@vben/utils';
import { Button, message } from 'ant-design-vue';
import { message } from 'ant-design-vue';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import { deleteTenant, exportTenant, getTenantPage } from '#/api/system/tenant';
import { getTenantPackageList } from '#/api/system/tenant-package';
import { DocAlert } from '#/components/doc-alert';
@ -41,33 +40,35 @@ function onRefresh() {
}
/** 导出表格 */
async function onExport() {
async function handleExport() {
const data = await exportTenant(await gridApi.formApi.getValues());
downloadFileFromBlobPart({ fileName: '租户.xls', source: data });
}
/** 创建租户 */
function onCreate() {
function handleCreate() {
formModalApi.setData(null).open();
}
/** 编辑租户 */
function onEdit(row: SystemTenantApi.Tenant) {
function handleEdit(row: SystemTenantApi.Tenant) {
formModalApi.setData(row).open();
}
/** 删除租户 */
async function onDelete(row: SystemTenantApi.Tenant) {
async function handleDelete(row: SystemTenantApi.Tenant) {
const hideLoading = message.loading({
content: $t('ui.actionMessage.deleting', [row.name]),
duration: 0,
key: 'action_process_msg',
key: 'action_key_msg',
});
try {
await deleteTenant(row.id as number);
message.success($t('ui.actionMessage.deleteSuccess', [row.name]));
message.success({
content: $t('ui.actionMessage.deleteSuccess', [row.name]),
key: 'action_key_msg',
});
onRefresh();
} catch {
} finally {
hideLoading();
}
}
@ -78,8 +79,6 @@ const [Grid, gridApi] = useVbenVxeGrid({
},
gridOptions: {
columns: useGridColumns(getPackageName),
height: 'auto',
keepSource: true,
proxyConfig: {
ajax: {
query: async ({ page }, formValues) => {
@ -115,23 +114,24 @@ onMounted(async () => {
<FormModal @success="onRefresh" />
<Grid table-title="">
<template #toolbar-tools>
<Button
type="primary"
@click="onCreate"
v-access:code="['system:tenant:create']"
>
<Plus class="size-5" />
{{ $t('ui.actionTitle.create', ['租户']) }}
</Button>
<Button
type="primary"
class="ml-2"
@click="onExport"
v-access:code="['system:tenant:export']"
>
<Download class="size-5" />
{{ $t('ui.actionTitle.export') }}
</Button>
<TableAction
:actions="[
{
label: $t('ui.actionTitle.create', ['租户']),
type: 'primary',
icon: ACTION_ICON.ADD,
auth: ['system:tenant:create'],
onClick: handleCreate,
},
{
label: $t('ui.actionTitle.export'),
type: 'primary',
icon: ACTION_ICON.DOWNLOAD,
auth: ['system:tenant:export'],
onClick: handleExport,
},
]"
/>
</template>
<template #actions="{ row }">
<TableAction
@ -139,19 +139,19 @@ onMounted(async () => {
{
label: $t('common.edit'),
type: 'link',
icon: 'ant-design:edit-outlined',
icon: ACTION_ICON.EDIT,
auth: ['system:role:update'],
onClick: onEdit.bind(null, row),
onClick: handleEdit.bind(null, row),
},
{
label: $t('common.delete'),
type: 'link',
danger: true,
icon: 'ant-design:delete-outlined',
icon: ACTION_ICON.DELETE,
auth: ['system:role:delete'],
popConfirm: {
title: $t('ui.actionMessage.deleteConfirm', [row.name]),
confirm: onDelete.bind(null, row),
confirm: handleDelete.bind(null, row),
},
},
]"

View File

@ -3,11 +3,10 @@ import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { SystemTenantPackageApi } from '#/api/system/tenant-package';
import { Page, useVbenModal } from '@vben/common-ui';
import { Plus } from '@vben/icons';
import { Button, message } from 'ant-design-vue';
import { message } from 'ant-design-vue';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import {
deleteTenantPackage,
getTenantPackagePage,
@ -29,27 +28,29 @@ function onRefresh() {
}
/** 创建租户套餐 */
function onCreate() {
function handleCreate() {
formModalApi.setData(null).open();
}
/** 编辑租户套餐 */
function onEdit(row: SystemTenantPackageApi.TenantPackage) {
function handleEdit(row: SystemTenantPackageApi.TenantPackage) {
formModalApi.setData(row).open();
}
/** 删除租户套餐 */
async function onDelete(row: SystemTenantPackageApi.TenantPackage) {
async function handleDelete(row: SystemTenantPackageApi.TenantPackage) {
const hideLoading = message.loading({
content: $t('ui.actionMessage.deleting', [row.name]),
duration: 0,
key: 'action_process_msg',
key: 'action_key_msg',
});
try {
await deleteTenantPackage(row.id as number);
message.success($t('ui.actionMessage.deleteSuccess', [row.name]));
message.success({
content: $t('ui.actionMessage.deleteSuccess', [row.name]),
key: 'action_key_msg',
});
onRefresh();
} catch {
} finally {
hideLoading();
}
}
@ -94,14 +95,17 @@ const [Grid, gridApi] = useVbenVxeGrid({
<FormModal @success="onRefresh" />
<Grid table-title="">
<template #toolbar-tools>
<Button
type="primary"
@click="onCreate"
v-access:code="['system:tenant-package:create']"
>
<Plus class="size-5" />
{{ $t('ui.actionTitle.create', ['套餐']) }}
</Button>
<TableAction
:actions="[
{
label: $t('ui.actionTitle.create', ['套餐']),
type: 'primary',
icon: ACTION_ICON.ADD,
auth: ['system:tenant-package:create'],
onClick: handleCreate,
},
]"
/>
</template>
<template #actions="{ row }">
<TableAction
@ -109,19 +113,19 @@ const [Grid, gridApi] = useVbenVxeGrid({
{
label: $t('common.edit'),
type: 'link',
icon: 'ant-design:edit-outlined',
icon: ACTION_ICON.EDIT,
auth: ['system:role:update'],
onClick: onEdit.bind(null, row),
onClick: handleEdit.bind(null, row),
},
{
label: $t('common.delete'),
type: 'link',
danger: true,
icon: 'ant-design:delete-outlined',
icon: ACTION_ICON.DELETE,
auth: ['system:role:delete'],
popConfirm: {
title: $t('ui.actionMessage.deleteConfirm', [row.name]),
confirm: onDelete.bind(null, row),
confirm: handleDelete.bind(null, row),
},
},
]"

View File

@ -312,7 +312,7 @@ export function useGridColumns<T = SystemUserApi.User>(
},
{
title: '操作',
width: 160,
width: 180,
fixed: 'right',
slots: { default: 'actions' },
},

View File

@ -6,12 +6,11 @@ import type { SystemUserApi } from '#/api/system/user';
import { ref } from 'vue';
import { confirm, Page, useVbenModal } from '@vben/common-ui';
import { Download, Plus, Upload } from '@vben/icons';
import { downloadFileFromBlobPart } from '@vben/utils';
import { Button, message } from 'ant-design-vue';
import { message } from 'ant-design-vue';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import {
deleteUser,
exportUser,
@ -19,7 +18,6 @@ import {
updateUserStatus,
} from '#/api/system/user';
import { DocAlert } from '#/components/doc-alert';
import { TableAction } from '#/components/table-action';
import { $t } from '#/locales';
import { DICT_TYPE, getDictLabel } from '#/utils';
@ -56,61 +54,64 @@ function onRefresh() {
}
/** 导出表格 */
async function onExport() {
async function handleExport() {
const data = await exportUser(await gridApi.formApi.getValues());
downloadFileFromBlobPart({ fileName: '用户.xls', source: data });
}
/** 选择部门 */
const searchDeptId = ref<number | undefined>(undefined);
async function onDeptSelect(dept: SystemDeptApi.Dept) {
async function handleDeptSelect(dept: SystemDeptApi.Dept) {
searchDeptId.value = dept.id;
onRefresh();
}
/** 创建用户 */
function onCreate() {
function handleCreate() {
formModalApi.setData(null).open();
}
/** 导入用户 */
function onImport() {
function handleImport() {
importModalApi.open();
}
/** 编辑用户 */
function onEdit(row: SystemUserApi.User) {
function handleEdit(row: SystemUserApi.User) {
formModalApi.setData(row).open();
}
/** 删除用户 */
async function onDelete(row: SystemUserApi.User) {
async function handleDelete(row: SystemUserApi.User) {
const hideLoading = message.loading({
content: $t('ui.actionMessage.deleting', [row.username]),
duration: 0,
key: 'action_process_msg',
key: 'action_key_msg',
});
try {
await deleteUser(row.id as number);
message.success($t('ui.actionMessage.deleteSuccess', [row.username]));
message.success({
content: $t('ui.actionMessage.deleteSuccess', [row.username]),
key: 'action_key_msg',
});
onRefresh();
} catch {
} finally {
hideLoading();
}
}
/** 重置密码 */
function onResetPassword(row: SystemUserApi.User) {
function handleResetPassword(row: SystemUserApi.User) {
resetPasswordModalApi.setData(row).open();
}
/** 分配角色 */
function onAssignRole(row: SystemUserApi.User) {
function handleAssignRole(row: SystemUserApi.User) {
assignRoleModalApi.setData(row).open();
}
/** 更新用户状态 */
async function onStatusChange(
async function handleStatusChange(
newStatus: number,
row: SystemUserApi.User,
): Promise<boolean | undefined> {
@ -140,7 +141,7 @@ const [Grid, gridApi] = useVbenVxeGrid({
schema: useGridFormSchema(),
},
gridOptions: {
columns: useGridColumns(onStatusChange),
columns: useGridColumns(handleStatusChange),
height: 'auto',
keepSource: true,
proxyConfig: {
@ -185,38 +186,37 @@ const [Grid, gridApi] = useVbenVxeGrid({
<div class="flex h-full w-full">
<!-- 左侧部门树 -->
<div class="h-full w-1/6 pr-4">
<DeptTree @select="onDeptSelect" />
<DeptTree @select="handleDeptSelect" />
</div>
<!-- 右侧用户列表 -->
<div class="w-5/6">
<Grid table-title="">
<template #toolbar-tools>
<Button
type="primary"
@click="onCreate"
v-access:code="['system:user:create']"
>
<Plus class="size-5" />
{{ $t('ui.actionTitle.create', ['用户']) }}
</Button>
<Button
type="primary"
class="ml-2"
@click="onExport"
v-access:code="['system:user:export']"
>
<Download class="size-5" />
{{ $t('ui.actionTitle.export') }}
</Button>
<Button
type="primary"
class="ml-2"
@click="onImport"
v-access:code="['system:user:import']"
>
<Upload class="size-5" />
{{ $t('ui.actionTitle.import', ['用户']) }}
</Button>
<TableAction
:actions="[
{
label: $t('ui.actionTitle.create', ['用户']),
type: 'primary',
icon: ACTION_ICON.ADD,
auth: ['system:user:create'],
onClick: handleCreate,
},
{
label: $t('ui.actionTitle.export'),
type: 'primary',
icon: ACTION_ICON.DOWNLOAD,
auth: ['system:user:export'],
onClick: handleExport,
},
{
label: $t('ui.actionTitle.import', ['用户']),
type: 'primary',
icon: ACTION_ICON.UPLOAD,
auth: ['system:user:import'],
onClick: handleImport,
},
]"
/>
</template>
<template #actions="{ row }">
<TableAction
@ -224,34 +224,34 @@ const [Grid, gridApi] = useVbenVxeGrid({
{
label: $t('common.edit'),
type: 'link',
icon: 'ant-design:edit-outlined',
icon: ACTION_ICON.EDIT,
auth: ['system:user:update'],
onClick: onEdit.bind(null, row),
onClick: handleEdit.bind(null, row),
},
{
label: $t('common.delete'),
type: 'link',
danger: true,
icon: 'ant-design:delete-outlined',
icon: ACTION_ICON.DELETE,
auth: ['system:user:delete'],
popConfirm: {
title: $t('ui.actionMessage.deleteConfirm', [row.name]),
confirm: onDelete.bind(null, row),
confirm: handleDelete.bind(null, row),
},
},
]"
:drop-down-actions="[
{
label: '数据权限',
label: '分配角色',
type: 'link',
auth: ['system:permission:assign-user-role'],
onClick: onAssignRole.bind(null, row),
onClick: handleAssignRole.bind(null, row),
},
{
label: '菜单权限',
label: '重置密码',
type: 'link',
auth: ['system:user:update-password'],
onClick: onResetPassword.bind(null, row),
onClick: handleResetPassword.bind(null, row),
},
]"
/>

View File

@ -55,7 +55,7 @@ function beforeUpload(file: FileType) {
}
/** 下载模版 */
async function onDownload() {
async function handleDownload() {
const data = await importUserTemplate();
downloadFileFromBlobPart({ fileName: '用户导入模板.xls', source: data });
}
@ -78,7 +78,7 @@ async function onDownload() {
</Form>
<template #prepend-footer>
<div class="flex flex-auto items-center">
<Button @click="onDownload"> </Button>
<Button @click="handleDownload"> </Button>
</div>
</template>
</Modal>

View File

@ -186,6 +186,12 @@ const defaultPreferences: Preferences = {
colorWeakMode: false,
compact: false,
contentCompact: 'wide',
contentCompactWidth: 1200,
contentPadding: 0,
contentPaddingBottom: 0,
contentPaddingLeft: 0,
contentPaddingRight: 0,
contentPaddingTop: 0,
defaultAvatar:
'https://unpkg.com/@vbenjs/static-source@0.1.7/source/avatar-v1.webp',
defaultHomePath: '/analytics',
@ -200,6 +206,7 @@ const defaultPreferences: Preferences = {
name: 'Vben Admin',
preferencesButtonPosition: 'auto',
watermark: false,
zIndex: 200,
},
breadcrumb: {
enable: true,
@ -220,9 +227,11 @@ const defaultPreferences: Preferences = {
footer: {
enable: false,
fixed: false,
height: 32,
},
header: {
enable: true,
height: 50,
hidden: false,
menuAlign: 'start',
mode: 'fixed',
@ -248,11 +257,14 @@ const defaultPreferences: Preferences = {
collapsed: false,
collapsedButton: true,
collapsedShowTitle: false,
collapseWidth: 60,
enable: true,
expandOnHover: true,
extraCollapse: false,
extraCollapsedWidth: 60,
fixedButton: true,
hidden: false,
mixedWidth: 80,
width: 224,
},
tabbar: {
@ -319,6 +331,18 @@ interface AppPreferences {
compact: boolean;
/** Whether to enable content compact mode */
contentCompact: ContentCompactType;
/** Content compact width */
contentCompactWidth: number;
/** Content padding */
contentPadding: number;
/** Content bottom padding */
contentPaddingBottom: number;
/** Content left padding */
contentPaddingLeft: number;
/** Content right padding */
contentPaddingRight: number;
/** Content top padding */
contentPaddingTop: number;
// /** Default application avatar */
defaultAvatar: string;
/** Default homepage path */
@ -349,6 +373,8 @@ interface AppPreferences {
* @zh_CN Whether to enable watermark
*/
watermark: boolean;
/** z-index */
zIndex: number;
}
interface BreadcrumbPreferences {
/** Whether breadcrumbs are enabled */
@ -385,11 +411,15 @@ interface FooterPreferences {
enable: boolean;
/** Whether the footer is fixed */
fixed: boolean;
/** Footer height */
height: number;
}
interface HeaderPreferences {
/** Whether the header is enabled */
enable: boolean;
/** Header height */
height: number;
/** Whether the header is hidden, css-hidden */
hidden: boolean;
/** Header menu alignment */
@ -422,16 +452,22 @@ interface SidebarPreferences {
collapsedButton: boolean;
/** Whether to show title when sidebar is collapsed */
collapsedShowTitle: boolean;
/** Sidebar collapse width */
collapseWidth: number;
/** Whether the sidebar is visible */
enable: boolean;
/** Menu auto-expand state */
expandOnHover: boolean;
/** Whether the sidebar extension area is collapsed */
extraCollapse: boolean;
/** Sidebar extension area collapse width */
extraCollapsedWidth: number;
/** Whether the sidebar fixed button is visible */
fixedButton: boolean;
/** Whether the sidebar is hidden - css */
hidden: boolean;
/** Mixed sidebar width */
mixedWidth: number;
/** Sidebar width */
width: number;
}

View File

@ -214,7 +214,7 @@ server {
使用 nginx 处理项目部署后的跨域问题
1. 配置前端项目接口地址,在项目目录下的``.env.production`文件中配置:
1. 配置前端项目接口地址,在项目目录下的`.env.production`文件中配置:
```bash
VITE_GLOB_API_URL=/api

View File

@ -339,6 +339,10 @@ interface RouteMeta {
| 'success'
| 'warning'
| string;
/**
* 路由的完整路径作为key默认true
*/
fullPathKey?: boolean;
/**
* 当前路由的子级在菜单中不展现
* @default false
@ -502,6 +506,13 @@ interface RouteMeta {
用于配置页面的徽标颜色。
### fullPathKey
- 类型:`boolean`
- 默认值:`true`
是否将路由的完整路径作为tab key默认true
### activePath
- 类型:`string`
@ -602,3 +613,32 @@ const { refresh } = useRefresh();
refresh();
</script>
```
## 标签页与路由控制
在某些场景下需要单个路由打开多个标签页或者修改路由的query不打开新的标签页
每个标签页Tab使用唯一的key标识设置Tab key有三种方式优先级由高到低
- 使用路由query参数pageKey
```vue
<script setup lang="ts">
import { useRouter } from 'vue-router';
// 跳转路由
const router = useRouter();
router.push({
path: 'path',
query: {
pageKey: 'key',
},
});
```
- 路由的完整路径作为key
`meta` 属性中的 `fullPathKey`不为false则使用路由`fullPath`作为key
- 路由的path作为key
`meta` 属性中的 `fullPathKey`为false则使用路由`path`作为key

View File

@ -185,6 +185,12 @@ const defaultPreferences: Preferences = {
colorWeakMode: false,
compact: false,
contentCompact: 'wide',
contentCompactWidth: 1200,
contentPadding: 0,
contentPaddingBottom: 0,
contentPaddingLeft: 0,
contentPaddingRight: 0,
contentPaddingTop: 0,
defaultAvatar:
'https://unpkg.com/@vbenjs/static-source@0.1.7/source/avatar-v1.webp',
defaultHomePath: '/analytics',
@ -199,6 +205,7 @@ const defaultPreferences: Preferences = {
name: 'Vben Admin',
preferencesButtonPosition: 'auto',
watermark: false,
zIndex: 200,
},
breadcrumb: {
enable: true,
@ -219,9 +226,11 @@ const defaultPreferences: Preferences = {
footer: {
enable: false,
fixed: false,
height: 32,
},
header: {
enable: true,
height: 50,
hidden: false,
menuAlign: 'start',
mode: 'fixed',
@ -247,11 +256,14 @@ const defaultPreferences: Preferences = {
collapsed: false,
collapsedButton: true,
collapsedShowTitle: false,
collapseWidth: 60,
enable: true,
expandOnHover: true,
extraCollapse: false,
extraCollapsedWidth: 60,
fixedButton: true,
hidden: false,
mixedWidth: 80,
width: 224,
},
tabbar: {
@ -318,6 +330,18 @@ interface AppPreferences {
compact: boolean;
/** 是否开启内容紧凑模式 */
contentCompact: ContentCompactType;
/** 内容紧凑宽度 */
contentCompactWidth: number;
/** 内容内边距 */
contentPadding: number;
/** 内容底部内边距 */
contentPaddingBottom: number;
/** 内容左侧内边距 */
contentPaddingLeft: number;
/** 内容右侧内边距 */
contentPaddingRight: number;
/** 内容顶部内边距 */
contentPaddingTop: number;
// /** 应用默认头像 */
defaultAvatar: string;
/** 默认首页地址 */
@ -348,6 +372,8 @@ interface AppPreferences {
* @zh_CN 是否开启水印
*/
watermark: boolean;
/** z-index */
zIndex: number;
}
interface BreadcrumbPreferences {
@ -385,11 +411,15 @@ interface FooterPreferences {
enable: boolean;
/** 底栏是否固定 */
fixed: boolean;
/** 底栏高度 */
height: number;
}
interface HeaderPreferences {
/** 顶栏是否启用 */
enable: boolean;
/** 顶栏高度 */
height: number;
/** 顶栏是否隐藏,css-隐藏 */
hidden: boolean;
/** 顶栏菜单位置 */
@ -423,16 +453,22 @@ interface SidebarPreferences {
collapsedButton: boolean;
/** 侧边栏折叠时是否显示title */
collapsedShowTitle: boolean;
/** 侧边栏折叠宽度 */
collapseWidth: number;
/** 侧边栏是否可见 */
enable: boolean;
/** 菜单自动展开状态 */
expandOnHover: boolean;
/** 侧边栏扩展区域是否折叠 */
extraCollapse: boolean;
/** 侧边栏扩展区域折叠宽度 */
extraCollapsedWidth: number;
/** 侧边栏固定按钮是否可见 */
fixedButton: boolean;
/** 侧边栏是否隐藏 - css */
hidden: boolean;
/** 混合侧边栏宽度 */
mixedWidth: number;
/** 侧边栏宽度 */
width: number;
}

View File

@ -12,7 +12,7 @@
"license": "MIT",
"type": "module",
"scripts": {
"stub": "pnpm unbuild"
"stub": "pnpm unbuild --stub"
},
"files": [
"dist"

View File

@ -1,3 +1,8 @@
import type { RouteLocationNormalized } from 'vue-router';
export type TabDefinition = RouteLocationNormalized;
export interface TabDefinition extends RouteLocationNormalized {
/**
* key
*/
key?: string;
}

View File

@ -43,6 +43,10 @@ interface RouteMeta {
| 'success'
| 'warning'
| string;
/**
* keytrue
*/
fullPathKey?: boolean;
/**
*
* @default false

View File

@ -10,6 +10,12 @@ exports[`defaultPreferences immutability test > should not modify the config obj
"colorWeakMode": false,
"compact": false,
"contentCompact": "wide",
"contentCompactWidth": 1200,
"contentPadding": 0,
"contentPaddingBottom": 0,
"contentPaddingLeft": 0,
"contentPaddingRight": 0,
"contentPaddingTop": 0,
"defaultAvatar": "https://unpkg.com/@vbenjs/static-source@0.1.7/source/avatar-v1.webp",
"defaultHomePath": "/analytics",
"dynamicTitle": true,
@ -23,6 +29,7 @@ exports[`defaultPreferences immutability test > should not modify the config obj
"name": "Vben Admin",
"preferencesButtonPosition": "auto",
"watermark": false,
"zIndex": 200,
},
"breadcrumb": {
"enable": true,
@ -43,9 +50,11 @@ exports[`defaultPreferences immutability test > should not modify the config obj
"footer": {
"enable": false,
"fixed": false,
"height": 32,
},
"header": {
"enable": true,
"height": 50,
"hidden": false,
"menuAlign": "start",
"mode": "fixed",
@ -68,14 +77,17 @@ exports[`defaultPreferences immutability test > should not modify the config obj
},
"sidebar": {
"autoActivateChild": false,
"collapseWidth": 60,
"collapsed": false,
"collapsedButton": true,
"collapsedShowTitle": false,
"enable": true,
"expandOnHover": true,
"extraCollapse": false,
"extraCollapsedWidth": 60,
"fixedButton": true,
"hidden": false,
"mixedWidth": 80,
"width": 224,
},
"tabbar": {

View File

@ -9,6 +9,12 @@ const defaultPreferences: Preferences = {
colorWeakMode: false,
compact: false,
contentCompact: 'wide',
contentCompactWidth: 1200,
contentPadding: 0,
contentPaddingBottom: 0,
contentPaddingLeft: 0,
contentPaddingRight: 0,
contentPaddingTop: 0,
defaultAvatar:
'https://unpkg.com/@vbenjs/static-source@0.1.7/source/avatar-v1.webp',
defaultHomePath: '/analytics',
@ -23,6 +29,7 @@ const defaultPreferences: Preferences = {
name: 'Vben Admin',
preferencesButtonPosition: 'auto',
watermark: false,
zIndex: 200,
},
breadcrumb: {
enable: true,
@ -43,13 +50,16 @@ const defaultPreferences: Preferences = {
footer: {
enable: false,
fixed: false,
height: 32,
},
header: {
enable: true,
height: 50,
hidden: false,
menuAlign: 'start',
mode: 'fixed',
},
logo: {
enable: true,
source: 'https://unpkg.com/@vbenjs/static-source@0.1.7/source/logo-v1.webp',
@ -71,11 +81,14 @@ const defaultPreferences: Preferences = {
collapsed: false,
collapsedButton: true,
collapsedShowTitle: false,
collapseWidth: 60,
enable: true,
expandOnHover: true,
extraCollapse: false,
extraCollapsedWidth: 60,
fixedButton: true,
hidden: false,
mixedWidth: 80,
width: 224,
},
tabbar: {

View File

@ -33,6 +33,18 @@ interface AppPreferences {
compact: boolean;
/** 是否开启内容紧凑模式 */
contentCompact: ContentCompactType;
/** 内容紧凑宽度 */
contentCompactWidth: number;
/** 内容内边距 */
contentPadding: number;
/** 内容底部内边距 */
contentPaddingBottom: number;
/** 内容左侧内边距 */
contentPaddingLeft: number;
/** 内容右侧内边距 */
contentPaddingRight: number;
/** 内容顶部内边距 */
contentPaddingTop: number;
// /** 应用默认头像 */
defaultAvatar: string;
/** 默认首页地址 */
@ -63,6 +75,8 @@ interface AppPreferences {
* @zh_CN
*/
watermark: boolean;
/** z-index */
zIndex: number;
}
interface BreadcrumbPreferences {
@ -100,11 +114,15 @@ interface FooterPreferences {
enable: boolean;
/** 底栏是否固定 */
fixed: boolean;
/** 底栏高度 */
height: number;
}
interface HeaderPreferences {
/** 顶栏是否启用 */
enable: boolean;
/** 顶栏高度 */
height: number;
/** 顶栏是否隐藏,css-隐藏 */
hidden: boolean;
/** 顶栏菜单位置 */
@ -138,16 +156,22 @@ interface SidebarPreferences {
collapsedButton: boolean;
/** 侧边栏折叠时是否显示title */
collapsedShowTitle: boolean;
/** 侧边栏折叠宽度 */
collapseWidth: number;
/** 侧边栏是否可见 */
enable: boolean;
/** 菜单自动展开状态 */
expandOnHover: boolean;
/** 侧边栏扩展区域是否折叠 */
extraCollapse: boolean;
/** 侧边栏扩展区域折叠宽度 */
extraCollapsedWidth: number;
/** 侧边栏固定按钮是否可见 */
fixedButton: boolean;
/** 侧边栏是否隐藏 - css */
hidden: boolean;
/** 混合侧边栏宽度 */
mixedWidth: number;
/** 侧边栏宽度 */
width: number;
}

View File

@ -36,7 +36,7 @@ export interface VbenButtonGroupProps
btnClass?: any;
gap?: number;
multiple?: boolean;
options?: { label: CustomRenderType; value: ValueType }[];
options?: { [key: string]: any; label: CustomRenderType; value: ValueType }[];
showIcon?: boolean;
size?: 'large' | 'middle' | 'small';
}

View File

@ -119,7 +119,7 @@ async function onBtnClick(value: ValueType) {
<CircleCheckBig v-else-if="innerValue.includes(btn.value)" />
<Circle v-else />
</div>
<slot name="option" :label="btn.label" :value="btn.value">
<slot name="option" :label="btn.label" :value="btn.value" :data="btn">
<VbenRenderContent :content="btn.label" />
</slot>
</Button>
@ -127,6 +127,9 @@ async function onBtnClick(value: ValueType) {
</template>
<style lang="scss" scoped>
.vben-check-button-group {
display: flex;
flex-wrap: wrap;
&:deep(.size-large) button {
.icon-wrapper {
margin-right: 0.3rem;
@ -159,5 +162,16 @@ async function onBtnClick(value: ValueType) {
}
}
}
&.no-gap > :deep(button):nth-of-type(1) {
border-right-width: 0;
}
&.no-gap {
:deep(button + button) {
margin-right: -1px;
border-left-width: 1px;
}
}
}
</style>

View File

@ -224,15 +224,20 @@ defineExpose({
:class="
cn('cursor-pointer', getNodeClass?.(item), {
'data-[selected]:bg-accent': !multiple,
'cursor-not-allowed': disabled,
})
"
v-bind="
Object.assign(item.bind, {
onfocus: disabled ? 'this.blur()' : undefined,
})
"
v-bind="item.bind"
@select="
(event) => {
if (event.detail.originalEvent.type === 'click') {
event.preventDefault();
}
onSelect(item, event.detail.isSelected);
!disabled && onSelect(item, event.detail.isSelected);
}
"
@toggle="
@ -240,7 +245,7 @@ defineExpose({
if (event.detail.originalEvent.type === 'click') {
event.preventDefault();
}
onToggle(item);
!disabled && onToggle(item);
}
"
class="tree-node focus:ring-grass8 my-0.5 flex items-center rounded px-2 py-1 outline-none focus:ring-2"
@ -262,10 +267,11 @@ defineExpose({
<Checkbox
v-if="multiple"
:checked="isSelected"
:disabled="disabled"
:indeterminate="isIndeterminate"
@click="
() => {
handleSelect();
!disabled && handleSelect();
// onSelect(item, !isSelected);
}
"
@ -276,7 +282,7 @@ defineExpose({
(_event) => {
// $event.stopPropagation();
// $event.preventDefault();
handleSelect();
!disabled && handleSelect();
// onSelect(item, !isSelected);
}
"

View File

@ -40,14 +40,14 @@ const style = computed(() => {
const tabsView = computed(() => {
return props.tabs.map((tab) => {
const { fullPath, meta, name, path } = tab || {};
const { fullPath, meta, name, path, key } = tab || {};
const { affixTab, icon, newTabTitle, tabClosable, title } = meta || {};
return {
affixTab: !!affixTab,
closable: Reflect.has(meta, 'tabClosable') ? !!tabClosable : true,
fullPath,
icon: icon as string,
key: fullPath || path,
key,
meta,
name,
path,

View File

@ -47,14 +47,14 @@ const typeWithClass = computed(() => {
const tabsView = computed(() => {
return props.tabs.map((tab) => {
const { fullPath, meta, name, path } = tab || {};
const { fullPath, meta, name, path, key } = tab || {};
const { affixTab, icon, newTabTitle, tabClosable, title } = meta || {};
return {
affixTab: !!affixTab,
closable: Reflect.has(meta, 'tabClosable') ? !!tabClosable : true,
fullPath,
icon: icon as string,
key: fullPath || path,
key,
meta,
name,
path,

View File

@ -193,67 +193,107 @@ export function useElementPlusDesignTokens() {
'--el-border-radius-base': getCssVariableValue('--radius', false),
'--el-color-danger': getCssVariableValue('--destructive-500'),
'--el-color-danger-dark-2': getCssVariableValue('--destructive'),
'--el-color-danger-light-3': getCssVariableValue('--destructive-400'),
'--el-color-danger-light-5': getCssVariableValue('--destructive-300'),
'--el-color-danger-light-7': getCssVariableValue('--destructive-200'),
'--el-color-danger-dark-2': isDark.value
? getCssVariableValue('--destructive-400')
: getCssVariableValue('--destructive-600'),
'--el-color-danger-light-3': isDark.value
? getCssVariableValue('--destructive-600')
: getCssVariableValue('--destructive-400'),
'--el-color-danger-light-5': isDark.value
? getCssVariableValue('--destructive-700')
: getCssVariableValue('--destructive-300'),
'--el-color-danger-light-7': isDark.value
? getCssVariableValue('--destructive-800')
: getCssVariableValue('--destructive-200'),
'--el-color-danger-light-8': isDark.value
? border
? getCssVariableValue('--destructive-900')
: getCssVariableValue('--destructive-100'),
'--el-color-danger-light-9': isDark.value
? accent
? getCssVariableValue('--destructive-950')
: getCssVariableValue('--destructive-50'),
'--el-color-error': getCssVariableValue('--destructive-500'),
'--el-color-error-dark-2': getCssVariableValue('--destructive'),
'--el-color-error-light-3': getCssVariableValue('--destructive-400'),
'--el-color-error-light-5': getCssVariableValue('--destructive-300'),
'--el-color-error-light-7': getCssVariableValue('--destructive-200'),
'--el-color-error-dark-2': isDark.value
? getCssVariableValue('--destructive-400')
: getCssVariableValue('--destructive-600'),
'--el-color-error-light-3': isDark.value
? getCssVariableValue('--destructive-600')
: getCssVariableValue('--destructive-400'),
'--el-color-error-light-5': isDark.value
? getCssVariableValue('--destructive-700')
: getCssVariableValue('--destructive-300'),
'--el-color-error-light-7': isDark.value
? getCssVariableValue('--destructive-800')
: getCssVariableValue('--destructive-200'),
'--el-color-error-light-8': isDark.value
? border
? getCssVariableValue('--destructive-900')
: getCssVariableValue('--destructive-100'),
'--el-color-error-light-9': isDark.value
? accent
? getCssVariableValue('--destructive-950')
: getCssVariableValue('--destructive-50'),
'--el-color-info-light-5': border,
'--el-color-info-light-8': border,
'--el-color-info-light-9': getCssVariableValue('--info'), // getCssVariableValue('--secondary'),
'--el-color-primary': getCssVariableValue('--primary-500'),
'--el-color-primary-dark-2': getCssVariableValue('--primary'),
'--el-color-primary-light-3': getCssVariableValue('--primary-400'),
'--el-color-primary-light-5': getCssVariableValue('--primary-300'),
'--el-color-primary-dark-2': isDark.value
? getCssVariableValue('--primary-400')
: getCssVariableValue('--primary-600'),
'--el-color-primary-light-3': isDark.value
? getCssVariableValue('--primary-600')
: getCssVariableValue('--primary-400'),
'--el-color-primary-light-5': isDark.value
? getCssVariableValue('--primary-700')
: getCssVariableValue('--primary-300'),
'--el-color-primary-light-7': isDark.value
? border
? getCssVariableValue('--primary-800')
: getCssVariableValue('--primary-200'),
'--el-color-primary-light-8': isDark.value
? border
? getCssVariableValue('--primary-900')
: getCssVariableValue('--primary-100'),
'--el-color-primary-light-9': isDark.value
? accent
? getCssVariableValue('--primary-950')
: getCssVariableValue('--primary-50'),
'--el-color-success': getCssVariableValue('--success-500'),
'--el-color-success-dark-2': getCssVariableValue('--success'),
'--el-color-success-light-3': getCssVariableValue('--success-400'),
'--el-color-success-light-5': getCssVariableValue('--success-300'),
'--el-color-success-light-7': getCssVariableValue('--success-200'),
'--el-color-success-dark-2': isDark.value
? getCssVariableValue('--success-400')
: getCssVariableValue('--success-600'),
'--el-color-success-light-3': isDark.value
? getCssVariableValue('--success-600')
: getCssVariableValue('--success-400'),
'--el-color-success-light-5': isDark.value
? getCssVariableValue('--success-700')
: getCssVariableValue('--success-300'),
'--el-color-success-light-7': isDark.value
? getCssVariableValue('--success-800')
: getCssVariableValue('--success-200'),
'--el-color-success-light-8': isDark.value
? border
? getCssVariableValue('--success-900')
: getCssVariableValue('--success-100'),
'--el-color-success-light-9': isDark.value
? accent
? getCssVariableValue('--success-950')
: getCssVariableValue('--success-50'),
'--el-color-warning': getCssVariableValue('--warning-500'),
'--el-color-warning-dark-2': getCssVariableValue('--warning'),
'--el-color-warning-light-3': getCssVariableValue('--warning-400'),
'--el-color-warning-light-5': getCssVariableValue('--warning-300'),
'--el-color-warning-light-7': getCssVariableValue('--warning-200'),
'--el-color-warning-dark-2': isDark.value
? getCssVariableValue('--warning-400')
: getCssVariableValue('--warning-600'),
'--el-color-warning-light-3': isDark.value
? getCssVariableValue('--warning-600')
: getCssVariableValue('--warning-400'),
'--el-color-warning-light-5': isDark.value
? getCssVariableValue('--warning-700')
: getCssVariableValue('--warning-300'),
'--el-color-warning-light-7': isDark.value
? getCssVariableValue('--warning-800')
: getCssVariableValue('--warning-200'),
'--el-color-warning-light-8': isDark.value
? border
? getCssVariableValue('--warning-900')
: getCssVariableValue('--warning-100'),
'--el-color-warning-light-9': isDark.value
? accent
? getCssVariableValue('--warning-950')
: getCssVariableValue('--warning-50'),
'--el-fill-color': getCssVariableValue('--accent'),

View File

@ -1,3 +1,4 @@
import type { ComputedRef } from 'vue';
import type { RouteLocationNormalized } from 'vue-router';
import { useRoute, useRouter } from 'vue-router';
@ -53,7 +54,24 @@ export function useTabs() {
await tabbarStore.closeTabByKey(key, router);
}
async function setTabTitle(title: string) {
/**
*
*
* @description
* @description ,
*
* @param title -
* - :
* - : ComputedRef
*
* @example
* // 静态标题
* setTabTitle('标签页')
*
* // 动态标题(多语言)
* setTabTitle(computed(() => t('page.title')))
*/
async function setTabTitle(title: ComputedRef<string> | string) {
tabbarStore.setUpdateTime();
await tabbarStore.setTabTitle(route, title);
}

View File

@ -38,7 +38,7 @@ const { authPanelCenter, authPanelLeft, authPanelRight, isDark } =
<template>
<div
:class="[isDark]"
:class="[isDark ? 'dark' : '']"
class="flex min-h-full flex-1 select-none overflow-x-hidden"
>
<template v-if="toolbar">

View File

@ -1 +1,2 @@
export { default as AuthPageLayout } from './authentication.vue';
export * from './types';

View File

@ -9,7 +9,7 @@ import { computed } from 'vue';
import { RouterView } from 'vue-router';
import { preferences, usePreferences } from '@vben/preferences';
import { storeToRefs, useTabbarStore } from '@vben/stores';
import { getTabKey, storeToRefs, useTabbarStore } from '@vben/stores';
import { IFrameRouterView } from '../../iframe';
@ -115,13 +115,13 @@ function transformComponent(
:is="transformComponent(Component, route)"
v-if="renderRouteView"
v-show="!route.meta.iframeSrc"
:key="route.fullPath"
:key="getTabKey(route)"
/>
</KeepAlive>
<component
:is="Component"
v-else-if="renderRouteView"
:key="route.fullPath"
:key="getTabKey(route)"
/>
</Transition>
<template v-else>
@ -134,13 +134,13 @@ function transformComponent(
:is="transformComponent(Component, route)"
v-if="renderRouteView"
v-show="!route.meta.iframeSrc"
:key="route.fullPath"
:key="getTabKey(route)"
/>
</KeepAlive>
<component
:is="Component"
v-else-if="renderRouteView"
:key="route.fullPath"
:key="getTabKey(route)"
/>
</template>
</RouterView>

View File

@ -180,8 +180,16 @@ const headerSlots = computed(() => {
<VbenAdminLayout
v-model:sidebar-extra-visible="sidebarExtraVisible"
:content-compact="preferences.app.contentCompact"
:content-compact-width="preferences.app.contentCompactWidth"
:content-padding="preferences.app.contentPadding"
:content-padding-bottom="preferences.app.contentPaddingBottom"
:content-padding-left="preferences.app.contentPaddingLeft"
:content-padding-right="preferences.app.contentPaddingRight"
:content-padding-top="preferences.app.contentPaddingTop"
:footer-enable="preferences.footer.enable"
:footer-fixed="preferences.footer.fixed"
:footer-height="preferences.footer.height"
:header-height="preferences.header.height"
:header-hidden="preferences.header.hidden"
:header-mode="preferences.header.mode"
:header-theme="headerTheme"
@ -196,11 +204,15 @@ const headerSlots = computed(() => {
:sidebar-fixed-button="preferences.sidebar.fixedButton"
:sidebar-expand-on-hover="preferences.sidebar.expandOnHover"
:sidebar-extra-collapse="preferences.sidebar.extraCollapse"
:sidebar-extra-collapsed-width="preferences.sidebar.extraCollapsedWidth"
:sidebar-hidden="preferences.sidebar.hidden"
:sidebar-mixed-width="preferences.sidebar.mixedWidth"
:sidebar-theme="sidebarTheme"
:sidebar-width="preferences.sidebar.width"
:side-collapse-width="preferences.sidebar.collapseWidth"
:tabbar-enable="preferences.tabbar.enable"
:tabbar-height="preferences.tabbar.height"
:z-index="preferences.app.zIndex"
@side-mouse-leave="handleSideMouseLeave"
@toggle-sidebar="toggleSidebar"
@update:sidebar-collapse="

View File

@ -140,7 +140,10 @@ function useMixedMenu() {
watch(
() => route.path,
(path) => {
const currentPath = (route?.meta?.activePath as string) ?? path;
const currentPath = route?.meta?.activePath ?? route?.meta?.link ?? path;
if (willOpenedByWindow(currentPath)) {
return;
}
calcSideMenus(currentPath);
if (rootMenuPath.value)
defaultSubMap.set(rootMenuPath.value, currentPath);

View File

@ -30,7 +30,7 @@ const {
} = useTabbar();
const menus = computed(() => {
const tab = tabbarStore.getTabByPath(currentActive.value);
const tab = tabbarStore.getTabByKey(currentActive.value);
const menus = createContextMenus(tab);
return menus.map((item) => {
return {

View File

@ -22,7 +22,7 @@ import {
X,
} from '@vben/icons';
import { $t, useI18n } from '@vben/locales';
import { useAccessStore, useTabbarStore } from '@vben/stores';
import { getTabKey, useAccessStore, useTabbarStore } from '@vben/stores';
import { filterTree } from '@vben/utils';
export function useTabbar() {
@ -44,8 +44,11 @@ export function useTabbar() {
toggleTabPin,
} = useTabs();
/**
* tabkey
*/
const currentActive = computed(() => {
return route.fullPath;
return getTabKey(route);
});
const { locale } = useI18n();
@ -73,7 +76,8 @@ export function useTabbar() {
// 点击tab,跳转路由
const handleClick = (key: string) => {
router.push(key);
const { fullPath, path } = tabbarStore.getTabByKey(key);
router.push(fullPath || path);
};
// 关闭tab
@ -100,7 +104,7 @@ export function useTabbar() {
);
watch(
() => route.path,
() => route.fullPath,
() => {
const meta = route.matched?.[route.matched.length - 1]?.meta;
tabbarStore.addTab({

View File

@ -17,6 +17,7 @@
"edit": "Edit",
"delete": "Delete",
"create": "Create",
"detail": "Detail",
"yes": "Yes",
"no": "No",
"showSearchPanel": "Show search panel",

View File

@ -25,6 +25,7 @@
"deleteConfirm": "Are you sure to delete {0}?",
"deleting": "Deleting {0} ...",
"deleteSuccess": "{0} deleted successfully",
"deleteFailed": "{0} deleted failed",
"operationSuccess": "Operation succeeded",
"operationFailed": "Operation failed",
"importSuccess": "Import succeeded",

View File

@ -17,6 +17,7 @@
"edit": "修改",
"delete": "删除",
"create": "新增",
"detail": "详情",
"yes": "是",
"no": "否",
"showSearchPanel": "显示搜索面板",

View File

@ -25,6 +25,7 @@
"deleteConfirm": "确定删除 {0} 吗?",
"deleting": "正在删除 {0} ...",
"deleteSuccess": "{0} 删除成功",
"deleteFailed": "{0} 删除失败",
"operationSuccess": "操作成功",
"operationFailed": "操作失败",
"importSuccess": "导入成功",

View File

@ -22,12 +22,13 @@ describe('useAccessStore', () => {
const tab: any = {
fullPath: '/home',
meta: {},
key: '/home',
name: 'Home',
path: '/home',
};
store.addTab(tab);
const addNewTab = store.addTab(tab);
expect(store.tabs.length).toBe(1);
expect(store.tabs[0]).toEqual(tab);
expect(store.tabs[0]).toEqual(addNewTab);
});
it('adds a new tab if it does not exist', () => {
@ -38,20 +39,22 @@ describe('useAccessStore', () => {
name: 'New',
path: '/new',
};
store.addTab(newTab);
expect(store.tabs).toContainEqual(newTab);
const addNewTab = store.addTab(newTab);
expect(store.tabs).toContainEqual(addNewTab);
});
it('updates an existing tab instead of adding a new one', () => {
const store = useTabbarStore();
const initialTab: any = {
fullPath: '/existing',
meta: {},
meta: {
fullPathKey: false,
},
name: 'Existing',
path: '/existing',
query: {},
};
store.tabs.push(initialTab);
store.addTab(initialTab);
const updatedTab = { ...initialTab, query: { id: '1' } };
store.addTab(updatedTab);
expect(store.tabs.length).toBe(1);
@ -60,9 +63,12 @@ describe('useAccessStore', () => {
it('closes all tabs', async () => {
const store = useTabbarStore();
store.tabs = [
{ fullPath: '/home', meta: {}, name: 'Home', path: '/home' },
] as any;
store.addTab({
fullPath: '/home',
meta: {},
name: 'Home',
path: '/home',
} as any);
router.replace = vi.fn();
await store.closeAllTabs(router);
@ -157,7 +163,7 @@ describe('useAccessStore', () => {
path: '/contact',
} as any);
await store._bulkCloseByPaths(['/home', '/contact']);
await store._bulkCloseByKeys(['/home', '/contact']);
expect(store.tabs).toHaveLength(1);
expect(store.tabs[0]?.name).toBe('About');
@ -183,9 +189,8 @@ describe('useAccessStore', () => {
name: 'Contact',
path: '/contact',
};
store.addTab(targetTab);
await store.closeLeftTabs(targetTab);
const addTargetTab = store.addTab(targetTab);
await store.closeLeftTabs(addTargetTab);
expect(store.tabs).toHaveLength(1);
expect(store.tabs[0]?.name).toBe('Contact');
@ -205,7 +210,7 @@ describe('useAccessStore', () => {
name: 'About',
path: '/about',
};
store.addTab(targetTab);
const addTargetTab = store.addTab(targetTab);
store.addTab({
fullPath: '/contact',
meta: {},
@ -213,7 +218,7 @@ describe('useAccessStore', () => {
path: '/contact',
} as any);
await store.closeOtherTabs(targetTab);
await store.closeOtherTabs(addTargetTab);
expect(store.tabs).toHaveLength(1);
expect(store.tabs[0]?.name).toBe('About');
@ -227,7 +232,7 @@ describe('useAccessStore', () => {
name: 'Home',
path: '/home',
};
store.addTab(targetTab);
const addTargetTab = store.addTab(targetTab);
store.addTab({
fullPath: '/about',
meta: {},
@ -241,7 +246,7 @@ describe('useAccessStore', () => {
path: '/contact',
} as any);
await store.closeRightTabs(targetTab);
await store.closeRightTabs(addTargetTab);
expect(store.tabs).toHaveLength(1);
expect(store.tabs[0]?.name).toBe('Home');

View File

@ -1,4 +1,9 @@
import type { Router, RouteRecordNormalized } from 'vue-router';
import type { ComputedRef } from 'vue';
import type {
RouteLocationNormalized,
Router,
RouteRecordNormalized,
} from 'vue-router';
import type { TabDefinition } from '@vben-core/typings';
@ -52,23 +57,23 @@ export const useTabbarStore = defineStore('core-tabbar', {
/**
* Close tabs in bulk
*/
async _bulkCloseByPaths(paths: string[]) {
this.tabs = this.tabs.filter((item) => {
return !paths.includes(getTabPath(item));
});
async _bulkCloseByKeys(keys: string[]) {
const keySet = new Set(keys);
this.tabs = this.tabs.filter(
(item) => !keySet.has(getTabKeyFromTab(item)),
);
this.updateCacheTabs();
await this.updateCacheTabs();
},
/**
* @zh_CN
* @param tab
*/
_close(tab: TabDefinition) {
const { fullPath } = tab;
if (isAffixTab(tab)) {
return;
}
const index = this.tabs.findIndex((item) => item.fullPath === fullPath);
const index = this.tabs.findIndex((item) => equalTab(item, tab));
index !== -1 && this.tabs.splice(index, 1);
},
/**
@ -101,14 +106,17 @@ export const useTabbarStore = defineStore('core-tabbar', {
* @zh_CN
* @param routeTab
*/
addTab(routeTab: TabDefinition) {
const tab = cloneTab(routeTab);
addTab(routeTab: TabDefinition): TabDefinition {
let tab = cloneTab(routeTab);
if (!tab.key) {
tab.key = getTabKey(routeTab);
}
if (!isTabShown(tab)) {
return;
return tab;
}
const tabIndex = this.tabs.findIndex((tab) => {
return getTabPath(tab) === getTabPath(routeTab);
const tabIndex = this.tabs.findIndex((item) => {
return equalTab(item, tab);
});
if (tabIndex === -1) {
@ -154,10 +162,11 @@ export const useTabbarStore = defineStore('core-tabbar', {
mergedTab.meta.newTabTitle = curMeta.newTabTitle;
}
}
tab = mergedTab;
this.tabs.splice(tabIndex, 1, mergedTab);
}
this.updateCacheTabs();
return tab;
},
/**
* @zh_CN
@ -173,65 +182,63 @@ export const useTabbarStore = defineStore('core-tabbar', {
* @param tab
*/
async closeLeftTabs(tab: TabDefinition) {
const index = this.tabs.findIndex(
(item) => getTabPath(item) === getTabPath(tab),
);
const index = this.tabs.findIndex((item) => equalTab(item, tab));
if (index < 1) {
return;
}
const leftTabs = this.tabs.slice(0, index);
const paths: string[] = [];
const keys: string[] = [];
for (const item of leftTabs) {
if (!isAffixTab(item)) {
paths.push(getTabPath(item));
keys.push(item.key as string);
}
}
await this._bulkCloseByPaths(paths);
await this._bulkCloseByKeys(keys);
},
/**
* @zh_CN
* @param tab
*/
async closeOtherTabs(tab: TabDefinition) {
const closePaths = this.tabs.map((item) => getTabPath(item));
const closeKeys = this.tabs.map((item) => getTabKeyFromTab(item));
const paths: string[] = [];
const keys: string[] = [];
for (const path of closePaths) {
if (path !== tab.fullPath) {
const closeTab = this.tabs.find((item) => getTabPath(item) === path);
for (const key of closeKeys) {
if (key !== tab.key) {
const closeTab = this.tabs.find(
(item) => getTabKeyFromTab(item) === key,
);
if (!closeTab) {
continue;
}
if (!isAffixTab(closeTab)) {
paths.push(getTabPath(closeTab));
keys.push(closeTab.key as string);
}
}
}
await this._bulkCloseByPaths(paths);
await this._bulkCloseByKeys(keys);
},
/**
* @zh_CN
* @param tab
*/
async closeRightTabs(tab: TabDefinition) {
const index = this.tabs.findIndex(
(item) => getTabPath(item) === getTabPath(tab),
);
const index = this.tabs.findIndex((item) => equalTab(item, tab));
if (index !== -1 && index < this.tabs.length - 1) {
const rightTabs = this.tabs.slice(index + 1);
const paths: string[] = [];
const keys: string[] = [];
for (const item of rightTabs) {
if (!isAffixTab(item)) {
paths.push(getTabPath(item));
keys.push(item.key as string);
}
}
await this._bulkCloseByPaths(paths);
await this._bulkCloseByKeys(keys);
}
},
@ -242,15 +249,14 @@ export const useTabbarStore = defineStore('core-tabbar', {
*/
async closeTab(tab: TabDefinition, router: Router) {
const { currentRoute } = router;
// 关闭不是激活选项卡
if (getTabPath(currentRoute.value) !== getTabPath(tab)) {
if (getTabKey(currentRoute.value) !== getTabKeyFromTab(tab)) {
this._close(tab);
this.updateCacheTabs();
return;
}
const index = this.getTabs.findIndex(
(item) => getTabPath(item) === getTabPath(currentRoute.value),
(item) => getTabKeyFromTab(item) === getTabKey(currentRoute.value),
);
const before = this.getTabs[index - 1];
@ -277,7 +283,7 @@ export const useTabbarStore = defineStore('core-tabbar', {
async closeTabByKey(key: string, router: Router) {
const originKey = decodeURIComponent(key);
const index = this.tabs.findIndex(
(item) => getTabPath(item) === originKey,
(item) => getTabKeyFromTab(item) === originKey,
);
if (index === -1) {
return;
@ -290,12 +296,12 @@ export const useTabbarStore = defineStore('core-tabbar', {
},
/**
*
* @param path
* tabkeytab
* @param key
*/
getTabByPath(path: string) {
getTabByKey(key: string) {
return this.getTabs.find(
(item) => getTabPath(item) === path,
(item) => getTabKeyFromTab(item) === key,
) as TabDefinition;
},
/**
@ -311,22 +317,19 @@ export const useTabbarStore = defineStore('core-tabbar', {
* @param tab
*/
async pinTab(tab: TabDefinition) {
const index = this.tabs.findIndex(
(item) => getTabPath(item) === getTabPath(tab),
);
if (index !== -1) {
const oldTab = this.tabs[index];
tab.meta.affixTab = true;
tab.meta.title = oldTab?.meta?.title as string;
// this.addTab(tab);
this.tabs.splice(index, 1, tab);
const index = this.tabs.findIndex((item) => equalTab(item, tab));
if (index === -1) {
return;
}
const oldTab = this.tabs[index];
tab.meta.affixTab = true;
tab.meta.title = oldTab?.meta?.title as string;
// this.addTab(tab);
this.tabs.splice(index, 1, tab);
// 过滤固定tabs后面更改affixTabOrder的值的话可能会有问题目前行464排序affixTabs没有设置值
const affixTabs = this.tabs.filter((tab) => isAffixTab(tab));
// 获得固定tabs的index
const newIndex = affixTabs.findIndex(
(item) => getTabPath(item) === getTabPath(tab),
);
const newIndex = affixTabs.findIndex((item) => equalTab(item, tab));
// 交换位置重新排序
await this.sortTabs(index, newIndex);
},
@ -371,9 +374,7 @@ export const useTabbarStore = defineStore('core-tabbar', {
if (tab?.meta?.newTabTitle) {
return;
}
const findTab = this.tabs.find(
(item) => getTabPath(item) === getTabPath(tab),
);
const findTab = this.tabs.find((item) => equalTab(item, tab));
if (findTab) {
findTab.meta.newTabTitle = undefined;
await this.updateCacheTabs();
@ -401,13 +402,24 @@ export const useTabbarStore = defineStore('core-tabbar', {
/**
* @zh_CN
* @param tab
* @param title
*
* @zh_CN
* @zh_CN ,
* @zh_CN
*
* @param {TabDefinition} tab -
* @param {ComputedRef<string> | string} title - ,
*
* @example
* // 设置静态标题
* setTabTitle(tab, '新标签页');
*
* @example
* // 设置动态标题
* setTabTitle(tab, computed(() => t('common.dashboard')));
*/
async setTabTitle(tab: TabDefinition, title: string) {
const findTab = this.tabs.find(
(item) => getTabPath(item) === getTabPath(tab),
);
async setTabTitle(tab: TabDefinition, title: ComputedRef<string> | string) {
const findTab = this.tabs.find((item) => equalTab(item, tab));
if (findTab) {
findTab.meta.newTabTitle = title;
@ -448,17 +460,15 @@ export const useTabbarStore = defineStore('core-tabbar', {
* @param tab
*/
async unpinTab(tab: TabDefinition) {
const index = this.tabs.findIndex(
(item) => getTabPath(item) === getTabPath(tab),
);
if (index !== -1) {
const oldTab = this.tabs[index];
tab.meta.affixTab = false;
tab.meta.title = oldTab?.meta?.title as string;
// this.addTab(tab);
this.tabs.splice(index, 1, tab);
const index = this.tabs.findIndex((item) => equalTab(item, tab));
if (index === -1) {
return;
}
const oldTab = this.tabs[index];
tab.meta.affixTab = false;
tab.meta.title = oldTab?.meta?.title as string;
// this.addTab(tab);
this.tabs.splice(index, 1, tab);
// 过滤固定tabs后面更改affixTabOrder的值的话可能会有问题目前行464排序affixTabs没有设置值
const affixTabs = this.tabs.filter((tab) => isAffixTab(tab));
// 获得固定tabs的index,使用固定tabs的下一个位置也就是活动tabs的第一个位置
@ -591,11 +601,49 @@ function isTabShown(tab: TabDefinition) {
}
/**
* @zh_CN
* routetabkey
* @param tab
*/
function getTabPath(tab: RouteRecordNormalized | TabDefinition) {
return decodeURIComponent((tab as TabDefinition).fullPath || tab.path);
function getTabKey(tab: RouteLocationNormalized | RouteRecordNormalized) {
const {
fullPath,
path,
meta: { fullPathKey } = {},
query = {},
} = tab as RouteLocationNormalized;
// pageKey可能是数组查询参数重复时可能出现
const pageKey = Array.isArray(query.pageKey)
? query.pageKey[0]
: query.pageKey;
let rawKey;
if (pageKey) {
rawKey = pageKey;
} else {
rawKey = fullPathKey === false ? path : (fullPath ?? path);
}
try {
return decodeURIComponent(rawKey);
} catch {
return rawKey;
}
}
/**
* tabtabkey
* tabkey,routekey
* @param tab
*/
function getTabKeyFromTab(tab: TabDefinition): string {
return tab.key ?? getTabKey(tab);
}
/**
* tab
* @param a
* @param b
*/
function equalTab(a: TabDefinition, b: TabDefinition) {
return getTabKeyFromTab(a) === getTabKeyFromTab(b);
}
function routeToTab(route: RouteRecordNormalized) {
@ -603,5 +651,8 @@ function routeToTab(route: RouteRecordNormalized) {
meta: route.meta,
name: route.name,
path: route.path,
key: getTabKey(route),
} as TabDefinition;
}
export { getTabKey };

View File

@ -19,7 +19,7 @@ const checkValue = ref(['a', 'b']);
const options = [
{ label: '选项1', value: 'a' },
{ label: '选项2', value: 'b' },
{ label: '选项2', value: 'b', num: 999 },
{ label: '选项3', value: 'c' },
{ label: '选项4', value: 'd' },
{ label: '选项5', value: 'e' },
@ -168,10 +168,11 @@ function onBtnClick(value: any) {
:options="options"
v-bind="compProps"
>
<template #option="{ label, value }">
<template #option="{ label, value, data }">
<div class="flex items-center">
<span>{{ label }}</span>
<span class="ml-2 text-gray-400">{{ value }}</span>
<span v-if="data.num" class="white ml-2">{{ data.num }}</span>
</div>
</template>
</VbenCheckButtonGroup>