feat: naive system init
This commit is contained in:
@@ -145,14 +145,12 @@ setupVbenVxeTable({
|
|||||||
const loadingKey = `__loading_${column.field}`;
|
const loadingKey = `__loading_${column.field}`;
|
||||||
const finallyProps = {
|
const finallyProps = {
|
||||||
inlinePrompt: true,
|
inlinePrompt: true,
|
||||||
activeText: $t('common.enabled'),
|
checkedValue: 0,
|
||||||
inactiveText: $t('common.disabled'),
|
uncheckedValue: 1,
|
||||||
activeValue: 1,
|
|
||||||
inactiveValue: 0,
|
|
||||||
...props,
|
...props,
|
||||||
modelValue: row[column.field],
|
value: row[column.field],
|
||||||
loading: row[loadingKey] ?? false,
|
loading: row[loadingKey] ?? false,
|
||||||
'onUpdate:modelValue': onChange,
|
'onUpdate:value': onChange,
|
||||||
};
|
};
|
||||||
|
|
||||||
async function onChange(newVal: any) {
|
async function onChange(newVal: any) {
|
||||||
@@ -167,7 +165,10 @@ setupVbenVxeTable({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return h(NSwitch, finallyProps);
|
return h(NSwitch, finallyProps, {
|
||||||
|
checked: $t('common.enabled'),
|
||||||
|
unchecked: $t('common.disabled'),
|
||||||
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -206,4 +207,5 @@ setupVbenVxeTable({
|
|||||||
|
|
||||||
export { useVbenVxeGrid };
|
export { useVbenVxeGrid };
|
||||||
|
|
||||||
|
export * from '#/components/table-action';
|
||||||
export type * from '@vben/plugins/vxe-table';
|
export type * from '@vben/plugins/vxe-table';
|
||||||
|
|||||||
79
apps/web-naive/src/components/description/description.vue
Normal file
79
apps/web-naive/src/components/description/description.vue
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
<script lang="tsx">
|
||||||
|
import type { DescriptionsProps } from 'naive-ui';
|
||||||
|
|
||||||
|
import type { PropType } from 'vue';
|
||||||
|
|
||||||
|
import type { DescriptionItemSchema, DescriptionsOptions } from './typing';
|
||||||
|
|
||||||
|
import { defineComponent } from 'vue';
|
||||||
|
|
||||||
|
import { get } from '@vben/utils';
|
||||||
|
|
||||||
|
import { NDescriptions, NDescriptionsItem } from 'naive-ui';
|
||||||
|
|
||||||
|
/** 对 Descriptions 进行二次封装 */
|
||||||
|
const Description = defineComponent({
|
||||||
|
name: 'Descriptions',
|
||||||
|
props: {
|
||||||
|
data: {
|
||||||
|
type: Object as PropType<Record<string, any>>,
|
||||||
|
default: () => ({}),
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
type: Array as PropType<DescriptionItemSchema[]>,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
// Descriptions 原生 props
|
||||||
|
componentProps: {
|
||||||
|
type: Object as PropType<DescriptionsProps>,
|
||||||
|
default: () => ({}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
setup(props: DescriptionsOptions) {
|
||||||
|
// TODO @xingyu:每个 field 的 slot 的考虑
|
||||||
|
// TODO @xingyu:from 5.0:extra: () => getSlot(slots, 'extra')
|
||||||
|
/** 过滤掉不需要展示的 */
|
||||||
|
const shouldShowItem = (item: DescriptionItemSchema) => {
|
||||||
|
if (item.hidden === undefined) return true;
|
||||||
|
return typeof item.hidden === 'function'
|
||||||
|
? !item.hidden(props.data)
|
||||||
|
: !item.hidden;
|
||||||
|
};
|
||||||
|
/** 渲染内容 */
|
||||||
|
const renderContent = (item: DescriptionItemSchema) => {
|
||||||
|
if (item.content) {
|
||||||
|
return typeof item.content === 'function'
|
||||||
|
? item.content(props.data)
|
||||||
|
: item.content;
|
||||||
|
}
|
||||||
|
return item.field ? get(props.data, item.field) : null;
|
||||||
|
};
|
||||||
|
|
||||||
|
return () => (
|
||||||
|
<NDescriptions
|
||||||
|
{...props}
|
||||||
|
bordered={props.componentProps?.bordered}
|
||||||
|
column={props.componentProps?.column}
|
||||||
|
size={props.componentProps?.size}
|
||||||
|
title={props.componentProps?.title}
|
||||||
|
>
|
||||||
|
{props.schema?.filter(shouldShowItem).map((item) => (
|
||||||
|
<NDescriptionsItem
|
||||||
|
contentStyle={item.contentStyle}
|
||||||
|
key={item.field || String(item.label)}
|
||||||
|
label={item.label as string}
|
||||||
|
labelStyle={item.labelStyle}
|
||||||
|
span={item.span}
|
||||||
|
>
|
||||||
|
{renderContent(item)}
|
||||||
|
</NDescriptionsItem>
|
||||||
|
))}
|
||||||
|
</NDescriptions>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// TODO @xingyu:from 5.0:emits: ['register'] 事件
|
||||||
|
export default Description;
|
||||||
|
</script>
|
||||||
3
apps/web-naive/src/components/description/index.ts
Normal file
3
apps/web-naive/src/components/description/index.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export { default as Description } from './description.vue';
|
||||||
|
export * from './typing';
|
||||||
|
export { useDescription } from './use-description';
|
||||||
27
apps/web-naive/src/components/description/typing.ts
Normal file
27
apps/web-naive/src/components/description/typing.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import type { DescriptionsProps } from 'naive-ui';
|
||||||
|
|
||||||
|
import type { CSSProperties, VNode } from 'vue';
|
||||||
|
|
||||||
|
// TODO @xingyu:【content】这个纠结下;1)vben2.0 是 render;https://doc.vvbin.cn/components/desc.html#usage 2)
|
||||||
|
// TODO @xingyu:vben2.0 还有 sapn【done】、labelMinWidth、contentMinWidth
|
||||||
|
// TODO @xingyu:【hidden】这个纠结下;1)vben2.0 是 show;
|
||||||
|
export interface DescriptionItemSchema {
|
||||||
|
label: string | VNode; // 内容的描述
|
||||||
|
field?: string; // 对应 data 中的字段名
|
||||||
|
content?: ((data: any) => string | VNode) | string | VNode; // 自定义需要展示的内容,比如说 dict-tag
|
||||||
|
span?: number; // 包含列的数量
|
||||||
|
labelStyle?: CSSProperties; // 自定义标签样式
|
||||||
|
contentStyle?: CSSProperties; // 自定义内容样式
|
||||||
|
hidden?: ((data: any) => boolean) | boolean; // 是否显示
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO @xingyu:vben2.0 还有 title【done】、bordered【done】d、useCollapse、collapseOptions
|
||||||
|
// TODO @xingyu:from 5.0:bordered 默认为 true
|
||||||
|
// TODO @xingyu:from 5.0:column 默认为 lg: 3, md: 3, sm: 2, xl: 3, xs: 1, xxl: 4
|
||||||
|
// TODO @xingyu:from 5.0:size 默认为 small;有 'default', 'middle', 'small', undefined
|
||||||
|
// TODO @xingyu:from 5.0:useCollapse 默认为 true
|
||||||
|
export interface DescriptionsOptions {
|
||||||
|
data?: Record<string, any>; // 数据
|
||||||
|
schema?: DescriptionItemSchema[]; // 描述项配置
|
||||||
|
componentProps?: DescriptionsProps; // antd Descriptions 组件参数
|
||||||
|
}
|
||||||
71
apps/web-naive/src/components/description/use-description.ts
Normal file
71
apps/web-naive/src/components/description/use-description.ts
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
import type { DescriptionsOptions } from './typing';
|
||||||
|
|
||||||
|
import { defineComponent, h, isReactive, reactive, watch } from 'vue';
|
||||||
|
|
||||||
|
import Description from './description.vue';
|
||||||
|
|
||||||
|
/** 描述列表 api 定义 */
|
||||||
|
class DescriptionApi {
|
||||||
|
private state = reactive<Record<string, any>>({});
|
||||||
|
|
||||||
|
constructor(options: DescriptionsOptions) {
|
||||||
|
this.state = { ...options };
|
||||||
|
}
|
||||||
|
|
||||||
|
getState(): DescriptionsOptions {
|
||||||
|
return this.state as DescriptionsOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO @xingyu:【setState】纠结下:1)vben2.0 是 data https://doc.vvbin.cn/components/desc.html#usage;
|
||||||
|
setState(newState: Partial<DescriptionsOptions>) {
|
||||||
|
this.state = { ...this.state, ...newState };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ExtendedDescriptionApi = DescriptionApi;
|
||||||
|
|
||||||
|
export function useDescription(options: DescriptionsOptions) {
|
||||||
|
const IS_REACTIVE = isReactive(options);
|
||||||
|
const api = new DescriptionApi(options);
|
||||||
|
// 扩展 API
|
||||||
|
const extendedApi: ExtendedDescriptionApi = api as never;
|
||||||
|
const Desc = defineComponent({
|
||||||
|
name: 'UseDescription',
|
||||||
|
inheritAttrs: false,
|
||||||
|
setup(_, { attrs, slots }) {
|
||||||
|
// 合并props和attrs到state
|
||||||
|
api.setState({ ...attrs });
|
||||||
|
|
||||||
|
return () =>
|
||||||
|
h(
|
||||||
|
Description,
|
||||||
|
{
|
||||||
|
...api.getState(),
|
||||||
|
...attrs,
|
||||||
|
},
|
||||||
|
slots,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// 响应式支持
|
||||||
|
if (IS_REACTIVE) {
|
||||||
|
watch(
|
||||||
|
() => options.schema,
|
||||||
|
(newSchema) => {
|
||||||
|
api.setState({ schema: newSchema });
|
||||||
|
},
|
||||||
|
{ immediate: true, deep: true },
|
||||||
|
);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => options.data,
|
||||||
|
(newData) => {
|
||||||
|
api.setState({ data: newData });
|
||||||
|
},
|
||||||
|
{ immediate: true, deep: true },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [Desc, extendedApi] as const;
|
||||||
|
}
|
||||||
13
apps/web-naive/src/components/table-action/icons.ts
Normal file
13
apps/web-naive/src/components/table-action/icons.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
export const ACTION_ICON = {
|
||||||
|
DOWNLOAD: 'lucide:download',
|
||||||
|
UPLOAD: 'lucide:upload',
|
||||||
|
ADD: 'lucide:plus',
|
||||||
|
EDIT: 'lucide:edit',
|
||||||
|
DELETE: 'lucide:trash-2',
|
||||||
|
REFRESH: 'lucide:refresh-cw',
|
||||||
|
SEARCH: 'lucide:search',
|
||||||
|
FILTER: 'lucide:filter',
|
||||||
|
MORE: 'lucide:ellipsis-vertical',
|
||||||
|
VIEW: 'lucide:eye',
|
||||||
|
COPY: 'lucide:copy',
|
||||||
|
};
|
||||||
4
apps/web-naive/src/components/table-action/index.ts
Normal file
4
apps/web-naive/src/components/table-action/index.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
export * from './icons';
|
||||||
|
|
||||||
|
export { default as TableAction } from './table-action.vue';
|
||||||
|
export * from './typing';
|
||||||
221
apps/web-naive/src/components/table-action/table-action.vue
Normal file
221
apps/web-naive/src/components/table-action/table-action.vue
Normal file
@@ -0,0 +1,221 @@
|
|||||||
|
<!-- add by 星语:参考 vben2 的方式,增加 TableAction 组件 -->
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { DropdownMixedOption } from 'naive-ui/es/dropdown/src/interface';
|
||||||
|
|
||||||
|
import type { PropType } from 'vue';
|
||||||
|
|
||||||
|
import type { ActionItem, PopConfirm } from './typing';
|
||||||
|
|
||||||
|
import { computed, unref, watch } from 'vue';
|
||||||
|
|
||||||
|
import { useAccess } from '@vben/access';
|
||||||
|
import { IconifyIcon } from '@vben/icons';
|
||||||
|
import { isBoolean, isFunction } from '@vben/utils';
|
||||||
|
|
||||||
|
import { NButton, NDropdown, NPopconfirm, NSpace } from 'naive-ui';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
actions: {
|
||||||
|
type: Array as PropType<ActionItem[]>,
|
||||||
|
default() {
|
||||||
|
return [];
|
||||||
|
},
|
||||||
|
},
|
||||||
|
dropDownActions: {
|
||||||
|
type: Array as PropType<ActionItem[]>,
|
||||||
|
default() {
|
||||||
|
return [];
|
||||||
|
},
|
||||||
|
},
|
||||||
|
divider: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { hasAccessByCodes } = useAccess();
|
||||||
|
|
||||||
|
/** 检查是否显示 */
|
||||||
|
function isIfShow(action: ActionItem): boolean {
|
||||||
|
const ifShow = action.ifShow;
|
||||||
|
let isIfShow = true;
|
||||||
|
if (isBoolean(ifShow)) {
|
||||||
|
isIfShow = ifShow;
|
||||||
|
}
|
||||||
|
if (isFunction(ifShow)) {
|
||||||
|
isIfShow = ifShow(action);
|
||||||
|
}
|
||||||
|
if (isIfShow) {
|
||||||
|
isIfShow =
|
||||||
|
hasAccessByCodes(action.auth || []) || (action.auth || []).length === 0;
|
||||||
|
}
|
||||||
|
return isIfShow;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 处理按钮 actions */
|
||||||
|
const getActions = computed(() => {
|
||||||
|
const actions = props.actions || [];
|
||||||
|
return actions.filter((action: ActionItem) => isIfShow(action));
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 处理下拉菜单 actions */
|
||||||
|
const getDropdownList = computed((): DropdownMixedOption[] => {
|
||||||
|
const dropDownActions = props.dropDownActions || [];
|
||||||
|
return dropDownActions
|
||||||
|
.filter((action: ActionItem) => isIfShow(action))
|
||||||
|
.map((action: ActionItem, index: number) => ({
|
||||||
|
label: action.label || '',
|
||||||
|
onClick: () => action.onClick?.(),
|
||||||
|
key: getActionKey(action, index),
|
||||||
|
disabled: action.disabled,
|
||||||
|
divider: action.divider || false,
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
/** Space 组件的 size */
|
||||||
|
const spaceSize = computed(() => {
|
||||||
|
const actions = unref(getActions);
|
||||||
|
return actions?.some((item: ActionItem) => item.type === 'primary') ? 4 : 8;
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 获取 PopConfirm 属性 */
|
||||||
|
function getPopConfirmProps(popConfirm: PopConfirm) {
|
||||||
|
if (!popConfirm) return {};
|
||||||
|
|
||||||
|
const attrs: Record<string, any> = {};
|
||||||
|
|
||||||
|
// 复制基本属性,排除函数
|
||||||
|
Object.keys(popConfirm).forEach((key) => {
|
||||||
|
if (key !== 'confirm' && key !== 'cancel' && key !== 'icon') {
|
||||||
|
attrs[key] = popConfirm[key as keyof PopConfirm];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 单独处理事件函数
|
||||||
|
if (popConfirm.confirm && isFunction(popConfirm.confirm)) {
|
||||||
|
attrs.positiveConfirm = popConfirm.confirm;
|
||||||
|
}
|
||||||
|
if (popConfirm.cancel && isFunction(popConfirm.cancel)) {
|
||||||
|
attrs.negativeCancel = popConfirm.cancel;
|
||||||
|
}
|
||||||
|
if (popConfirm.okText) {
|
||||||
|
attrs.positiveText = popConfirm.okText;
|
||||||
|
}
|
||||||
|
if (popConfirm.cancelText) {
|
||||||
|
attrs.negativeText = popConfirm.cancelText;
|
||||||
|
}
|
||||||
|
|
||||||
|
return attrs;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获取 Button 属性 */
|
||||||
|
function getButtonProps(action: ActionItem) {
|
||||||
|
return {
|
||||||
|
type: action.type || 'primary',
|
||||||
|
quaternary: action.quaternary || false,
|
||||||
|
text: action.text || false,
|
||||||
|
disabled: action.disabled,
|
||||||
|
loading: action.loading,
|
||||||
|
size: action.size,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// /** 获取 Tooltip 属性 */
|
||||||
|
// function getTooltipProps(tooltip: any | string) {
|
||||||
|
// if (!tooltip) return {};
|
||||||
|
// return typeof tooltip === 'string' ? { title: tooltip } : { ...tooltip };
|
||||||
|
// }
|
||||||
|
|
||||||
|
/** 处理菜单点击 */
|
||||||
|
function handleMenuClick(key: number) {
|
||||||
|
const action = getDropdownList.value.find((item) => item.key === key);
|
||||||
|
if (action && action.onClick && isFunction(action.onClick)) {
|
||||||
|
action.onClick();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 生成稳定的 key */
|
||||||
|
function getActionKey(action: ActionItem, index: number) {
|
||||||
|
return `${action.label || ''}-${action.type || ''}-${index}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 处理按钮点击 */
|
||||||
|
function handleButtonClick(action: ActionItem) {
|
||||||
|
if (action.onClick && isFunction(action.onClick)) {
|
||||||
|
action.onClick();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handlePopconfirmClick(popconfirm: PopConfirm, type: string) {
|
||||||
|
if (type === 'positive') {
|
||||||
|
popconfirm.confirm();
|
||||||
|
} else if (type === 'negative' && popconfirm.cancel) {
|
||||||
|
popconfirm.cancel();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 监听props变化,强制重新计算 */
|
||||||
|
watch(
|
||||||
|
() => [props.actions, props.dropDownActions],
|
||||||
|
() => {
|
||||||
|
// 这里不需要额外处理,computed会自动重新计算
|
||||||
|
},
|
||||||
|
{ deep: true },
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="flex">
|
||||||
|
<NSpace :size="spaceSize">
|
||||||
|
<template
|
||||||
|
v-for="(action, index) in getActions"
|
||||||
|
:key="getActionKey(action, index)"
|
||||||
|
>
|
||||||
|
<NPopconfirm
|
||||||
|
v-if="action.popConfirm"
|
||||||
|
v-bind="getPopConfirmProps(action.popConfirm)"
|
||||||
|
@positive-click="handlePopconfirmClick(action.popConfirm, 'positive')"
|
||||||
|
@negative-click="handlePopconfirmClick(action.popConfirm, 'negative')"
|
||||||
|
>
|
||||||
|
<template v-if="action.popConfirm.icon" #icon>
|
||||||
|
<IconifyIcon :icon="action.popConfirm.icon" />
|
||||||
|
</template>
|
||||||
|
<template #trigger>
|
||||||
|
<NButton v-bind="getButtonProps(action)">
|
||||||
|
<template v-if="action.icon" #icon>
|
||||||
|
<IconifyIcon :icon="action.icon" />
|
||||||
|
</template>
|
||||||
|
{{ action.label }}
|
||||||
|
</NButton>
|
||||||
|
</template>
|
||||||
|
<span>{{ getPopConfirmProps(action.popConfirm).title }}</span>
|
||||||
|
</NPopconfirm>
|
||||||
|
<NButton
|
||||||
|
v-else
|
||||||
|
v-bind="getButtonProps(action)"
|
||||||
|
@click="handleButtonClick(action)"
|
||||||
|
>
|
||||||
|
<template v-if="action.icon" #icon>
|
||||||
|
<IconifyIcon :icon="action.icon" />
|
||||||
|
</template>
|
||||||
|
{{ action.label }}
|
||||||
|
</NButton>
|
||||||
|
</template>
|
||||||
|
</NSpace>
|
||||||
|
|
||||||
|
<NDropdown
|
||||||
|
v-if="getDropdownList.length > 0"
|
||||||
|
trigger="click"
|
||||||
|
:options="getDropdownList"
|
||||||
|
:show-arrow="true"
|
||||||
|
@select="handleMenuClick"
|
||||||
|
>
|
||||||
|
<NButton type="primary" text>
|
||||||
|
{{ $t('page.action.more') }}
|
||||||
|
<template #icon>
|
||||||
|
<IconifyIcon icon="lucide:ellipsis-vertical" />
|
||||||
|
</template>
|
||||||
|
</NButton>
|
||||||
|
</NDropdown>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
28
apps/web-naive/src/components/table-action/typing.ts
Normal file
28
apps/web-naive/src/components/table-action/typing.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import type { ButtonProps } from 'naive-ui/es/button/src/Button';
|
||||||
|
import type { TooltipProps } from 'naive-ui/es/tooltip/src/Tooltip';
|
||||||
|
|
||||||
|
export interface PopConfirm {
|
||||||
|
title: string;
|
||||||
|
okText?: string;
|
||||||
|
cancelText?: string;
|
||||||
|
confirm: () => void;
|
||||||
|
cancel?: () => void;
|
||||||
|
icon?: string;
|
||||||
|
disabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ActionItem extends ButtonProps {
|
||||||
|
onClick?: () => void;
|
||||||
|
type?: ButtonProps['type'];
|
||||||
|
label?: string;
|
||||||
|
color?: 'error' | 'success' | 'warning';
|
||||||
|
icon?: string;
|
||||||
|
popConfirm?: PopConfirm;
|
||||||
|
disabled?: boolean;
|
||||||
|
divider?: boolean;
|
||||||
|
// 权限编码控制是否显示
|
||||||
|
auth?: string[];
|
||||||
|
// 业务控制是否显示
|
||||||
|
ifShow?: ((action: ActionItem) => boolean) | boolean;
|
||||||
|
tooltip?: string | TooltipProps;
|
||||||
|
}
|
||||||
1
apps/web-naive/src/utils/index.ts
Normal file
1
apps/web-naive/src/utils/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from './rangePickerProps';
|
||||||
53
apps/web-naive/src/utils/rangePickerProps.ts
Normal file
53
apps/web-naive/src/utils/rangePickerProps.ts
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import dayjs from 'dayjs';
|
||||||
|
|
||||||
|
import { $t } from '#/locales';
|
||||||
|
|
||||||
|
/** 时间段选择器拓展 */
|
||||||
|
export function getRangePickerDefaultProps() {
|
||||||
|
return {
|
||||||
|
startPlaceholder: $t('utils.rangePicker.beginTime'),
|
||||||
|
endPlaceholder: $t('utils.rangePicker.endTime'),
|
||||||
|
type: 'datetimerange',
|
||||||
|
// TODO
|
||||||
|
format: 'YYYY-MM-dd HH:mm:ss',
|
||||||
|
valueFormat: 'YYYY-MM-dd HH:mm:ss',
|
||||||
|
defaultTime: ['00:00:00', '23:59:59'],
|
||||||
|
shortcuts: {
|
||||||
|
[$t('utils.rangePicker.today')]: () =>
|
||||||
|
[
|
||||||
|
dayjs().startOf('day').toDate(),
|
||||||
|
dayjs().endOf('day').toDate(),
|
||||||
|
] as const,
|
||||||
|
[$t('utils.rangePicker.last7Days')]: () =>
|
||||||
|
[
|
||||||
|
dayjs().subtract(7, 'day').startOf('day').toDate(),
|
||||||
|
dayjs().endOf('day').toDate(),
|
||||||
|
] as const,
|
||||||
|
[$t('utils.rangePicker.last30Days')]: () =>
|
||||||
|
[
|
||||||
|
dayjs().subtract(30, 'day').startOf('day').toDate(),
|
||||||
|
dayjs().endOf('day').toDate(),
|
||||||
|
] as const,
|
||||||
|
[$t('utils.rangePicker.yesterday')]: () =>
|
||||||
|
[
|
||||||
|
dayjs().subtract(1, 'day').startOf('day').toDate(),
|
||||||
|
dayjs().subtract(1, 'day').endOf('day').toDate(),
|
||||||
|
] as const,
|
||||||
|
[$t('utils.rangePicker.thisWeek')]: () =>
|
||||||
|
[
|
||||||
|
dayjs().startOf('week').toDate(),
|
||||||
|
dayjs().endOf('day').toDate(),
|
||||||
|
] as const,
|
||||||
|
[$t('utils.rangePicker.thisMonth')]: () =>
|
||||||
|
[
|
||||||
|
dayjs().startOf('month').toDate(),
|
||||||
|
dayjs().endOf('day').toDate(),
|
||||||
|
] as const,
|
||||||
|
[$t('utils.rangePicker.lastWeek')]: () =>
|
||||||
|
[
|
||||||
|
dayjs().subtract(1, 'week').startOf('day').toDate(),
|
||||||
|
dayjs().endOf('day').toDate(),
|
||||||
|
] as const,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
48
apps/web-naive/src/views/system/area/data.ts
Normal file
48
apps/web-naive/src/views/system/area/data.ts
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import type { VbenFormSchema } from '#/adapter/form';
|
||||||
|
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||||
|
import type { SystemAreaApi } from '#/api/system/area';
|
||||||
|
|
||||||
|
import { z } from '#/adapter/form';
|
||||||
|
|
||||||
|
/** 查询 IP 的表单 */
|
||||||
|
export function useFormSchema(): VbenFormSchema[] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
fieldName: 'ip',
|
||||||
|
label: 'IP 地址',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入 IP 地址',
|
||||||
|
},
|
||||||
|
rules: z.string().ip({ message: '请输入正确的 IP 地址' }),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'result',
|
||||||
|
label: '地址',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '展示查询 IP 结果',
|
||||||
|
readonly: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 列表的字段 */
|
||||||
|
export function useGridColumns(): VxeTableGridOptions<SystemAreaApi.Area>['columns'] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
field: 'id',
|
||||||
|
title: '地区编码',
|
||||||
|
minWidth: 120,
|
||||||
|
align: 'left',
|
||||||
|
fixed: 'left',
|
||||||
|
treeNode: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'name',
|
||||||
|
title: '地区名称',
|
||||||
|
minWidth: 200,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
79
apps/web-naive/src/views/system/area/index.vue
Normal file
79
apps/web-naive/src/views/system/area/index.vue
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||||
|
|
||||||
|
import { DocAlert, Page, useVbenModal } from '@vben/common-ui';
|
||||||
|
|
||||||
|
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||||
|
import { getAreaTree } from '#/api/system/area';
|
||||||
|
|
||||||
|
import { useGridColumns } from './data';
|
||||||
|
import Form from './modules/form.vue';
|
||||||
|
|
||||||
|
const [FormModal, formModalApi] = useVbenModal({
|
||||||
|
connectedComponent: Form,
|
||||||
|
destroyOnClose: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 刷新表格 */
|
||||||
|
function handleRefresh() {
|
||||||
|
gridApi.query();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 查询 IP */
|
||||||
|
function handleQueryIp() {
|
||||||
|
formModalApi.setData(null).open();
|
||||||
|
}
|
||||||
|
|
||||||
|
const [Grid, gridApi] = useVbenVxeGrid({
|
||||||
|
gridOptions: {
|
||||||
|
columns: useGridColumns(),
|
||||||
|
height: 'auto',
|
||||||
|
keepSource: true,
|
||||||
|
pagerConfig: {
|
||||||
|
enabled: false,
|
||||||
|
},
|
||||||
|
proxyConfig: {
|
||||||
|
ajax: {
|
||||||
|
query: async () => {
|
||||||
|
return await getAreaTree();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
rowConfig: {
|
||||||
|
keyField: 'id',
|
||||||
|
isHover: true,
|
||||||
|
},
|
||||||
|
toolbarConfig: {
|
||||||
|
refresh: true,
|
||||||
|
},
|
||||||
|
treeConfig: {
|
||||||
|
rowField: 'id',
|
||||||
|
reserve: true,
|
||||||
|
},
|
||||||
|
} as VxeTableGridOptions,
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Page auto-content-height>
|
||||||
|
<template #doc>
|
||||||
|
<DocAlert title="地区 & IP" url="https://doc.iocoder.cn/area-and-ip/" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<FormModal @success="handleRefresh" />
|
||||||
|
<Grid table-title="地区列表">
|
||||||
|
<template #toolbar-tools>
|
||||||
|
<TableAction
|
||||||
|
:actions="[
|
||||||
|
{
|
||||||
|
label: 'IP 查询',
|
||||||
|
type: 'primary',
|
||||||
|
icon: ACTION_ICON.SEARCH,
|
||||||
|
onClick: handleQueryIp,
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</Grid>
|
||||||
|
</Page>
|
||||||
|
</template>
|
||||||
49
apps/web-naive/src/views/system/area/modules/form.vue
Normal file
49
apps/web-naive/src/views/system/area/modules/form.vue
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { useVbenModal } from '@vben/common-ui';
|
||||||
|
|
||||||
|
import { useVbenForm } from '#/adapter/form';
|
||||||
|
import { message } from '#/adapter/naive';
|
||||||
|
import { getAreaByIp } from '#/api/system/area';
|
||||||
|
import { $t } from '#/locales';
|
||||||
|
|
||||||
|
import { useFormSchema } from '../data';
|
||||||
|
|
||||||
|
const [Form, { setFieldValue, validate, getValues }] = useVbenForm({
|
||||||
|
commonConfig: {
|
||||||
|
componentProps: {
|
||||||
|
class: 'w-full',
|
||||||
|
},
|
||||||
|
formItemClass: 'col-span-2',
|
||||||
|
labelWidth: 80,
|
||||||
|
},
|
||||||
|
layout: 'horizontal',
|
||||||
|
schema: useFormSchema(),
|
||||||
|
showDefaultActions: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const [Modal, modalApi] = useVbenModal({
|
||||||
|
async onConfirm() {
|
||||||
|
const { valid } = await validate();
|
||||||
|
if (!valid) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
modalApi.lock();
|
||||||
|
// 提交表单
|
||||||
|
const data = await getValues();
|
||||||
|
try {
|
||||||
|
const result = await getAreaByIp(data.ip);
|
||||||
|
// 设置结果
|
||||||
|
await setFieldValue('result', result);
|
||||||
|
message.success($t('ui.actionMessage.operationSuccess'));
|
||||||
|
} finally {
|
||||||
|
modalApi.unlock();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Modal title="IP 查询">
|
||||||
|
<Form class="mx-4" />
|
||||||
|
</Modal>
|
||||||
|
</template>
|
||||||
165
apps/web-naive/src/views/system/dept/data.ts
Normal file
165
apps/web-naive/src/views/system/dept/data.ts
Normal file
@@ -0,0 +1,165 @@
|
|||||||
|
import type { VbenFormSchema } from '#/adapter/form';
|
||||||
|
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||||
|
import type { SystemDeptApi } from '#/api/system/dept';
|
||||||
|
import type { SystemUserApi } from '#/api/system/user';
|
||||||
|
|
||||||
|
import { CommonStatusEnum, DICT_TYPE } from '@vben/constants';
|
||||||
|
import { getDictOptions } from '@vben/hooks';
|
||||||
|
import { handleTree } from '@vben/utils';
|
||||||
|
|
||||||
|
import { z } from '#/adapter/form';
|
||||||
|
import { getDeptList } from '#/api/system/dept';
|
||||||
|
import { getSimpleUserList } from '#/api/system/user';
|
||||||
|
|
||||||
|
let userList: SystemUserApi.User[] = [];
|
||||||
|
async function getUserData() {
|
||||||
|
userList = await getSimpleUserList();
|
||||||
|
}
|
||||||
|
|
||||||
|
getUserData();
|
||||||
|
|
||||||
|
/** 新增/修改的表单 */
|
||||||
|
export function useFormSchema(): VbenFormSchema[] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
fieldName: 'id',
|
||||||
|
component: 'Input',
|
||||||
|
dependencies: {
|
||||||
|
triggerFields: [''],
|
||||||
|
show: () => false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'parentId',
|
||||||
|
label: '上级部门',
|
||||||
|
component: 'ApiTreeSelect',
|
||||||
|
componentProps: {
|
||||||
|
clearable: true,
|
||||||
|
api: async () => {
|
||||||
|
const data = await getDeptList();
|
||||||
|
data.unshift({
|
||||||
|
id: 0,
|
||||||
|
name: '顶级部门',
|
||||||
|
});
|
||||||
|
return handleTree(data);
|
||||||
|
},
|
||||||
|
labelField: 'name',
|
||||||
|
valueField: 'id',
|
||||||
|
childrenField: 'children',
|
||||||
|
placeholder: '请选择上级部门',
|
||||||
|
treeDefaultExpandAll: true,
|
||||||
|
},
|
||||||
|
rules: 'selectRequired',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'name',
|
||||||
|
label: '部门名称',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入部门名称',
|
||||||
|
},
|
||||||
|
rules: 'required',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'sort',
|
||||||
|
label: '显示顺序',
|
||||||
|
component: 'InputNumber',
|
||||||
|
componentProps: {
|
||||||
|
min: 0,
|
||||||
|
placeholder: '请输入显示顺序',
|
||||||
|
},
|
||||||
|
rules: 'required',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'leaderUserId',
|
||||||
|
label: '负责人',
|
||||||
|
component: 'ApiSelect',
|
||||||
|
componentProps: {
|
||||||
|
api: getSimpleUserList,
|
||||||
|
labelField: 'nickname',
|
||||||
|
valueField: 'id',
|
||||||
|
placeholder: '请选择负责人',
|
||||||
|
clearable: true,
|
||||||
|
},
|
||||||
|
rules: z.number().optional(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'phone',
|
||||||
|
label: '联系电话',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
maxLength: 11,
|
||||||
|
placeholder: '请输入联系电话',
|
||||||
|
},
|
||||||
|
rules: 'mobileRequired',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'email',
|
||||||
|
label: '邮箱',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入邮箱',
|
||||||
|
},
|
||||||
|
rules: z.string().email('邮箱格式不正确').or(z.literal('')).optional(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'status',
|
||||||
|
label: '状态',
|
||||||
|
component: 'RadioGroup',
|
||||||
|
componentProps: {
|
||||||
|
options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
|
||||||
|
buttonStyle: 'solid',
|
||||||
|
optionType: 'button',
|
||||||
|
},
|
||||||
|
rules: z.number().default(CommonStatusEnum.ENABLE),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 列表的字段 */
|
||||||
|
export function useGridColumns(): VxeTableGridOptions<SystemDeptApi.Dept>['columns'] {
|
||||||
|
return [
|
||||||
|
{ type: 'checkbox', width: 40 },
|
||||||
|
{
|
||||||
|
field: 'name',
|
||||||
|
title: '部门名称',
|
||||||
|
minWidth: 150,
|
||||||
|
align: 'left',
|
||||||
|
fixed: 'left',
|
||||||
|
treeNode: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'leaderUserId',
|
||||||
|
title: '负责人',
|
||||||
|
minWidth: 150,
|
||||||
|
formatter: ({ cellValue }) =>
|
||||||
|
userList.find((user) => user.id === cellValue)?.nickname || '-',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'sort',
|
||||||
|
title: '显示顺序',
|
||||||
|
minWidth: 100,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'status',
|
||||||
|
title: '部门状态',
|
||||||
|
minWidth: 100,
|
||||||
|
cellRender: {
|
||||||
|
name: 'CellDict',
|
||||||
|
props: { type: DICT_TYPE.COMMON_STATUS },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'createTime',
|
||||||
|
title: '创建时间',
|
||||||
|
minWidth: 180,
|
||||||
|
formatter: 'formatDateTime',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '操作',
|
||||||
|
width: 260,
|
||||||
|
fixed: 'right',
|
||||||
|
slots: { default: 'actions' },
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
195
apps/web-naive/src/views/system/dept/index.vue
Normal file
195
apps/web-naive/src/views/system/dept/index.vue
Normal file
@@ -0,0 +1,195 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||||
|
import type { SystemDeptApi } from '#/api/system/dept';
|
||||||
|
|
||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
import { confirm, Page, useVbenModal } from '@vben/common-ui';
|
||||||
|
import { isEmpty } from '@vben/utils';
|
||||||
|
|
||||||
|
import { message } from '#/adapter/naive';
|
||||||
|
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||||
|
import { deleteDept, deleteDeptList, getDeptList } from '#/api/system/dept';
|
||||||
|
import { $t } from '#/locales';
|
||||||
|
|
||||||
|
import { useGridColumns } from './data';
|
||||||
|
import Form from './modules/form.vue';
|
||||||
|
|
||||||
|
const [FormModal, formModalApi] = useVbenModal({
|
||||||
|
connectedComponent: Form,
|
||||||
|
destroyOnClose: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 切换树形展开/收缩状态 */
|
||||||
|
const isExpanded = ref(true);
|
||||||
|
function handleExpand() {
|
||||||
|
isExpanded.value = !isExpanded.value;
|
||||||
|
gridApi.grid.setAllTreeExpand(isExpanded.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 刷新表格 */
|
||||||
|
function handleRefresh() {
|
||||||
|
gridApi.query();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 创建部门 */
|
||||||
|
function handleCreate() {
|
||||||
|
formModalApi.setData(null).open();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 添加下级部门 */
|
||||||
|
function handleAppend(row: SystemDeptApi.Dept) {
|
||||||
|
formModalApi.setData({ parentId: row.id }).open();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 编辑部门 */
|
||||||
|
function handleEdit(row: SystemDeptApi.Dept) {
|
||||||
|
formModalApi.setData(row).open();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 删除部门 */
|
||||||
|
async function handleDelete(row: SystemDeptApi.Dept) {
|
||||||
|
const hideLoading = message.loading(
|
||||||
|
$t('ui.actionMessage.deleting', [row.name]),
|
||||||
|
{
|
||||||
|
duration: 0,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
try {
|
||||||
|
await deleteDept(row.id!);
|
||||||
|
message.success($t('ui.actionMessage.deleteSuccess', [row.name]));
|
||||||
|
handleRefresh();
|
||||||
|
} finally {
|
||||||
|
hideLoading.destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 批量删除部门 */
|
||||||
|
async function handleDeleteBatch() {
|
||||||
|
await confirm($t('ui.actionMessage.deleteBatchConfirm'));
|
||||||
|
const hideLoading = message.loading($t('ui.actionMessage.deletingBatch'), {
|
||||||
|
duration: 0,
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
await deleteDeptList(checkedIds.value);
|
||||||
|
checkedIds.value = [];
|
||||||
|
message.success($t('ui.actionMessage.deleteSuccess'));
|
||||||
|
handleRefresh();
|
||||||
|
} finally {
|
||||||
|
hideLoading.destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const checkedIds = ref<number[]>([]);
|
||||||
|
function handleRowCheckboxChange({
|
||||||
|
records,
|
||||||
|
}: {
|
||||||
|
records: SystemDeptApi.Dept[];
|
||||||
|
}) {
|
||||||
|
checkedIds.value = records.map((item) => item.id!);
|
||||||
|
}
|
||||||
|
|
||||||
|
const [Grid, gridApi] = useVbenVxeGrid({
|
||||||
|
gridOptions: {
|
||||||
|
columns: useGridColumns(),
|
||||||
|
height: 'auto',
|
||||||
|
pagerConfig: {
|
||||||
|
enabled: false,
|
||||||
|
},
|
||||||
|
proxyConfig: {
|
||||||
|
ajax: {
|
||||||
|
query: async () => {
|
||||||
|
return await getDeptList();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
rowConfig: {
|
||||||
|
keyField: 'id',
|
||||||
|
isHover: true,
|
||||||
|
},
|
||||||
|
toolbarConfig: {
|
||||||
|
refresh: true,
|
||||||
|
search: true,
|
||||||
|
},
|
||||||
|
treeConfig: {
|
||||||
|
parentField: 'parentId',
|
||||||
|
rowField: 'id',
|
||||||
|
transform: true,
|
||||||
|
expandAll: true,
|
||||||
|
reserve: true,
|
||||||
|
},
|
||||||
|
} as VxeTableGridOptions<SystemDeptApi.Dept>,
|
||||||
|
gridEvents: {
|
||||||
|
checkboxAll: handleRowCheckboxChange,
|
||||||
|
checkboxChange: handleRowCheckboxChange,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Page auto-content-height>
|
||||||
|
<FormModal @success="handleRefresh" />
|
||||||
|
<Grid table-title="部门列表">
|
||||||
|
<template #toolbar-tools>
|
||||||
|
<TableAction
|
||||||
|
:actions="[
|
||||||
|
{
|
||||||
|
label: $t('ui.actionTitle.create', ['部门']),
|
||||||
|
type: 'primary',
|
||||||
|
icon: ACTION_ICON.ADD,
|
||||||
|
auth: ['system:dept:create'],
|
||||||
|
onClick: handleCreate,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: isExpanded ? '收缩' : '展开',
|
||||||
|
type: 'primary',
|
||||||
|
onClick: handleExpand,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: $t('ui.actionTitle.deleteBatch'),
|
||||||
|
type: 'error',
|
||||||
|
icon: ACTION_ICON.DELETE,
|
||||||
|
auth: ['system:dept:delete'],
|
||||||
|
disabled: isEmpty(checkedIds),
|
||||||
|
onClick: handleDeleteBatch,
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<template #actions="{ row }">
|
||||||
|
<TableAction
|
||||||
|
:actions="[
|
||||||
|
{
|
||||||
|
label: '新增下级',
|
||||||
|
type: 'primary',
|
||||||
|
text: true,
|
||||||
|
icon: ACTION_ICON.ADD,
|
||||||
|
auth: ['system:dept:create'],
|
||||||
|
onClick: handleAppend.bind(null, row),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: $t('common.edit'),
|
||||||
|
type: 'primary',
|
||||||
|
text: true,
|
||||||
|
icon: ACTION_ICON.EDIT,
|
||||||
|
auth: ['system:dept:update'],
|
||||||
|
onClick: handleEdit.bind(null, row),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: $t('common.delete'),
|
||||||
|
type: 'error',
|
||||||
|
text: true,
|
||||||
|
icon: ACTION_ICON.DELETE,
|
||||||
|
auth: ['system:dept:delete'],
|
||||||
|
disabled: row.children && row.children.length > 0,
|
||||||
|
popConfirm: {
|
||||||
|
title: $t('ui.actionMessage.deleteConfirm', [row.name]),
|
||||||
|
confirm: handleDelete.bind(null, row),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</Grid>
|
||||||
|
</Page>
|
||||||
|
</template>
|
||||||
83
apps/web-naive/src/views/system/dept/modules/form.vue
Normal file
83
apps/web-naive/src/views/system/dept/modules/form.vue
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { SystemDeptApi } from '#/api/system/dept';
|
||||||
|
|
||||||
|
import { computed, ref } from 'vue';
|
||||||
|
|
||||||
|
import { useVbenModal } from '@vben/common-ui';
|
||||||
|
|
||||||
|
import { useVbenForm } from '#/adapter/form';
|
||||||
|
import { message } from '#/adapter/naive';
|
||||||
|
import { createDept, getDept, updateDept } from '#/api/system/dept';
|
||||||
|
import { $t } from '#/locales';
|
||||||
|
|
||||||
|
import { useFormSchema } from '../data';
|
||||||
|
|
||||||
|
const emit = defineEmits(['success']);
|
||||||
|
const formData = ref<SystemDeptApi.Dept>();
|
||||||
|
const getTitle = computed(() => {
|
||||||
|
return formData.value?.id
|
||||||
|
? $t('ui.actionTitle.edit', ['部门'])
|
||||||
|
: $t('ui.actionTitle.create', ['部门']);
|
||||||
|
});
|
||||||
|
|
||||||
|
const [Form, formApi] = useVbenForm({
|
||||||
|
commonConfig: {
|
||||||
|
componentProps: {
|
||||||
|
class: 'w-full',
|
||||||
|
},
|
||||||
|
formItemClass: 'col-span-2',
|
||||||
|
labelWidth: 80,
|
||||||
|
},
|
||||||
|
layout: 'horizontal',
|
||||||
|
schema: useFormSchema(),
|
||||||
|
showDefaultActions: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const [Modal, modalApi] = useVbenModal({
|
||||||
|
async onConfirm() {
|
||||||
|
const { valid } = await formApi.validate();
|
||||||
|
if (!valid) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
modalApi.lock();
|
||||||
|
// 提交表单
|
||||||
|
const data = (await formApi.getValues()) as SystemDeptApi.Dept;
|
||||||
|
try {
|
||||||
|
await (formData.value?.id ? updateDept(data) : createDept(data));
|
||||||
|
// 关闭并提示
|
||||||
|
await modalApi.close();
|
||||||
|
emit('success');
|
||||||
|
message.success($t('ui.actionMessage.operationSuccess'));
|
||||||
|
} finally {
|
||||||
|
modalApi.unlock();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async onOpenChange(isOpen: boolean) {
|
||||||
|
if (!isOpen) {
|
||||||
|
formData.value = undefined;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 加载数据
|
||||||
|
const data = modalApi.getData<SystemDeptApi.Dept>();
|
||||||
|
if (!data || !data.id) {
|
||||||
|
// 设置上级
|
||||||
|
await formApi.setValues(data);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
modalApi.lock();
|
||||||
|
try {
|
||||||
|
formData.value = await getDept(data.id);
|
||||||
|
// 设置到 values
|
||||||
|
await formApi.setValues(formData.value);
|
||||||
|
} finally {
|
||||||
|
modalApi.unlock();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Modal :title="getTitle">
|
||||||
|
<Form class="mx-4" />
|
||||||
|
</Modal>
|
||||||
|
</template>
|
||||||
355
apps/web-naive/src/views/system/dict/data.ts
Normal file
355
apps/web-naive/src/views/system/dict/data.ts
Normal file
@@ -0,0 +1,355 @@
|
|||||||
|
import type { VbenFormSchema } from '#/adapter/form';
|
||||||
|
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||||
|
|
||||||
|
import { CommonStatusEnum, DICT_TYPE } from '@vben/constants';
|
||||||
|
import { getDictOptions } from '@vben/hooks';
|
||||||
|
|
||||||
|
import { z } from '#/adapter/form';
|
||||||
|
import { getSimpleDictTypeList } from '#/api/system/dict/type';
|
||||||
|
|
||||||
|
// ============================== 字典类型 ==============================
|
||||||
|
|
||||||
|
/** 类型新增/修改的表单 */
|
||||||
|
export function useTypeFormSchema(): VbenFormSchema[] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
fieldName: 'id',
|
||||||
|
component: 'Input',
|
||||||
|
dependencies: {
|
||||||
|
triggerFields: [''],
|
||||||
|
show: () => false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'name',
|
||||||
|
label: '字典名称',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入字典名称',
|
||||||
|
},
|
||||||
|
rules: 'required',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'type',
|
||||||
|
label: '字典类型',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: (values) => {
|
||||||
|
return {
|
||||||
|
placeholder: '请输入字典类型',
|
||||||
|
disabled: !!values.id,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
rules: 'required',
|
||||||
|
dependencies: {
|
||||||
|
triggerFields: [''],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'status',
|
||||||
|
label: '状态',
|
||||||
|
component: 'RadioGroup',
|
||||||
|
componentProps: {
|
||||||
|
options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
|
||||||
|
buttonStyle: 'solid',
|
||||||
|
optionType: 'button',
|
||||||
|
},
|
||||||
|
rules: z.number().default(CommonStatusEnum.ENABLE),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'remark',
|
||||||
|
label: '备注',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
type: 'textarea',
|
||||||
|
placeholder: '请输入备注',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 类型列表的搜索表单 */
|
||||||
|
export function useTypeGridFormSchema(): VbenFormSchema[] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
fieldName: 'name',
|
||||||
|
label: '字典名称',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入字典名称',
|
||||||
|
clearable: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'type',
|
||||||
|
label: '字典类型',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入字典类型',
|
||||||
|
clearable: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'status',
|
||||||
|
label: '状态',
|
||||||
|
component: 'Select',
|
||||||
|
componentProps: {
|
||||||
|
options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
|
||||||
|
placeholder: '请选择状态',
|
||||||
|
clearable: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 类型列表的字段 */
|
||||||
|
export function useTypeGridColumns(): VxeTableGridOptions['columns'] {
|
||||||
|
return [
|
||||||
|
{ type: 'checkbox', width: 40 },
|
||||||
|
{
|
||||||
|
field: 'id',
|
||||||
|
title: '字典编号',
|
||||||
|
minWidth: 100,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'name',
|
||||||
|
title: '字典名称',
|
||||||
|
minWidth: 200,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'type',
|
||||||
|
title: '字典类型',
|
||||||
|
minWidth: 220,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'status',
|
||||||
|
title: '状态',
|
||||||
|
minWidth: 120,
|
||||||
|
cellRender: {
|
||||||
|
name: 'CellDict',
|
||||||
|
props: { type: DICT_TYPE.COMMON_STATUS },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'remark',
|
||||||
|
title: '备注',
|
||||||
|
minWidth: 180,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'createTime',
|
||||||
|
title: '创建时间',
|
||||||
|
minWidth: 180,
|
||||||
|
formatter: 'formatDateTime',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '操作',
|
||||||
|
minWidth: 140,
|
||||||
|
fixed: 'right',
|
||||||
|
slots: { default: 'actions' },
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================== 字典数据 ==============================
|
||||||
|
|
||||||
|
// TODO @芋艿:后续针对 antd,增加
|
||||||
|
/**
|
||||||
|
* 颜色选项
|
||||||
|
*/
|
||||||
|
const colorOptions = [
|
||||||
|
{ value: '', label: '无' },
|
||||||
|
{ value: 'processing', label: '主要' },
|
||||||
|
{ value: 'success', label: '成功' },
|
||||||
|
{ value: 'default', label: '默认' },
|
||||||
|
{ value: 'warning', label: '警告' },
|
||||||
|
{ value: 'error', label: '危险' },
|
||||||
|
{ value: 'pink', label: 'pink' },
|
||||||
|
{ value: 'red', label: 'red' },
|
||||||
|
{ value: 'orange', label: 'orange' },
|
||||||
|
{ value: 'green', label: 'green' },
|
||||||
|
{ value: 'cyan', label: 'cyan' },
|
||||||
|
{ value: 'blue', label: 'blue' },
|
||||||
|
{ value: 'purple', label: 'purple' },
|
||||||
|
];
|
||||||
|
|
||||||
|
/** 数据新增/修改的表单 */
|
||||||
|
export function useDataFormSchema(): VbenFormSchema[] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
fieldName: 'id',
|
||||||
|
component: 'Input',
|
||||||
|
dependencies: {
|
||||||
|
triggerFields: [''],
|
||||||
|
show: () => false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'dictType',
|
||||||
|
label: '字典类型',
|
||||||
|
component: 'ApiSelect',
|
||||||
|
componentProps: (values) => {
|
||||||
|
return {
|
||||||
|
api: getSimpleDictTypeList,
|
||||||
|
placeholder: '请输入字典类型',
|
||||||
|
labelField: 'name',
|
||||||
|
valueField: 'type',
|
||||||
|
disabled: !!values.id,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
rules: 'required',
|
||||||
|
dependencies: {
|
||||||
|
triggerFields: [''],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'label',
|
||||||
|
label: '数据标签',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入数据标签',
|
||||||
|
},
|
||||||
|
rules: 'required',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'value',
|
||||||
|
label: '数据键值',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入数据键值',
|
||||||
|
},
|
||||||
|
rules: 'required',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'sort',
|
||||||
|
label: '显示排序',
|
||||||
|
component: 'InputNumber',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入显示排序',
|
||||||
|
},
|
||||||
|
rules: 'required',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'status',
|
||||||
|
label: '状态',
|
||||||
|
component: 'RadioGroup',
|
||||||
|
componentProps: {
|
||||||
|
options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
|
||||||
|
placeholder: '请选择状态',
|
||||||
|
buttonStyle: 'solid',
|
||||||
|
optionType: 'button',
|
||||||
|
},
|
||||||
|
rules: z.number().default(CommonStatusEnum.ENABLE),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'colorType',
|
||||||
|
label: '颜色类型',
|
||||||
|
component: 'Select',
|
||||||
|
componentProps: {
|
||||||
|
options: colorOptions,
|
||||||
|
placeholder: '请选择颜色类型',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'cssClass',
|
||||||
|
label: 'CSS Class',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入 CSS Class',
|
||||||
|
},
|
||||||
|
help: '输入 hex 模式的颜色, 例如 #108ee9',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'remark',
|
||||||
|
label: '备注',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
type: 'textarea',
|
||||||
|
placeholder: '请输入备注',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 字典数据列表搜索表单 */
|
||||||
|
export function useDataGridFormSchema(): VbenFormSchema[] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
fieldName: 'label',
|
||||||
|
label: '字典标签',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入字典标签',
|
||||||
|
clearable: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'status',
|
||||||
|
label: '状态',
|
||||||
|
component: 'Select',
|
||||||
|
componentProps: {
|
||||||
|
options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
|
||||||
|
placeholder: '请选择状态',
|
||||||
|
clearable: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 字典数据表格列 */
|
||||||
|
export function useDataGridColumns(): VxeTableGridOptions['columns'] {
|
||||||
|
return [
|
||||||
|
{ type: 'checkbox', width: 40 },
|
||||||
|
{
|
||||||
|
field: 'id',
|
||||||
|
title: '字典编码',
|
||||||
|
minWidth: 100,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'label',
|
||||||
|
title: '字典标签',
|
||||||
|
minWidth: 180,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'value',
|
||||||
|
title: '字典键值',
|
||||||
|
minWidth: 100,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'sort',
|
||||||
|
title: '字典排序',
|
||||||
|
minWidth: 100,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'status',
|
||||||
|
title: '状态',
|
||||||
|
minWidth: 100,
|
||||||
|
cellRender: {
|
||||||
|
name: 'CellDict',
|
||||||
|
props: { type: DICT_TYPE.COMMON_STATUS },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'colorType',
|
||||||
|
title: '颜色类型',
|
||||||
|
minWidth: 120,
|
||||||
|
slots: { default: 'colorType' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'cssClass',
|
||||||
|
title: 'CSS Class',
|
||||||
|
minWidth: 120,
|
||||||
|
slots: { default: 'cssClass' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '创建时间',
|
||||||
|
field: 'createTime',
|
||||||
|
minWidth: 180,
|
||||||
|
formatter: 'formatDateTime',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '操作',
|
||||||
|
minWidth: 140,
|
||||||
|
fixed: 'right',
|
||||||
|
slots: { default: 'actions' },
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
33
apps/web-naive/src/views/system/dict/index.vue
Normal file
33
apps/web-naive/src/views/system/dict/index.vue
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
import { DocAlert, Page } from '@vben/common-ui';
|
||||||
|
|
||||||
|
import DataGrid from './modules/data-grid.vue';
|
||||||
|
import TypeGrid from './modules/type-grid.vue';
|
||||||
|
|
||||||
|
const searchDictType = ref<string>(); // 搜索的字典类型
|
||||||
|
|
||||||
|
function handleDictTypeSelect(dictType: string) {
|
||||||
|
searchDictType.value = dictType;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Page auto-content-height>
|
||||||
|
<template #doc>
|
||||||
|
<DocAlert title="字典管理" url="https://doc.iocoder.cn/system-dict/" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div class="flex h-full">
|
||||||
|
<!-- 左侧字典类型列表 -->
|
||||||
|
<div class="w-1/2 pr-3">
|
||||||
|
<TypeGrid @select="handleDictTypeSelect" />
|
||||||
|
</div>
|
||||||
|
<!-- 右侧字典数据列表 -->
|
||||||
|
<div class="w-1/2">
|
||||||
|
<DataGrid :dict-type="searchDictType" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Page>
|
||||||
|
</template>
|
||||||
89
apps/web-naive/src/views/system/dict/modules/data-form.vue
Normal file
89
apps/web-naive/src/views/system/dict/modules/data-form.vue
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { SystemDictDataApi } from '#/api/system/dict/data';
|
||||||
|
|
||||||
|
import { computed, ref } from 'vue';
|
||||||
|
|
||||||
|
import { useVbenModal } from '@vben/common-ui';
|
||||||
|
|
||||||
|
import { useVbenForm } from '#/adapter/form';
|
||||||
|
import { message } from '#/adapter/naive';
|
||||||
|
import {
|
||||||
|
createDictData,
|
||||||
|
getDictData,
|
||||||
|
updateDictData,
|
||||||
|
} from '#/api/system/dict/data';
|
||||||
|
import { $t } from '#/locales';
|
||||||
|
|
||||||
|
import { useDataFormSchema } from '../data';
|
||||||
|
|
||||||
|
defineOptions({ name: 'SystemDictDataForm' });
|
||||||
|
|
||||||
|
const emit = defineEmits(['success']);
|
||||||
|
const formData = ref<SystemDictDataApi.DictData>();
|
||||||
|
const getTitle = computed(() => {
|
||||||
|
return formData.value?.id
|
||||||
|
? $t('ui.actionTitle.edit', ['字典数据'])
|
||||||
|
: $t('ui.actionTitle.create', ['字典数据']);
|
||||||
|
});
|
||||||
|
|
||||||
|
const [Form, formApi] = useVbenForm({
|
||||||
|
commonConfig: {
|
||||||
|
componentProps: {
|
||||||
|
class: 'w-full',
|
||||||
|
},
|
||||||
|
formItemClass: 'col-span-2',
|
||||||
|
labelWidth: 90,
|
||||||
|
},
|
||||||
|
layout: 'horizontal',
|
||||||
|
schema: useDataFormSchema(),
|
||||||
|
showDefaultActions: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const [Modal, modalApi] = useVbenModal({
|
||||||
|
async onConfirm() {
|
||||||
|
const { valid } = await formApi.validate();
|
||||||
|
if (!valid) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
modalApi.lock();
|
||||||
|
// 提交表单
|
||||||
|
const data = (await formApi.getValues()) as SystemDictDataApi.DictData;
|
||||||
|
try {
|
||||||
|
await (formData.value?.id ? updateDictData(data) : createDictData(data));
|
||||||
|
// 关闭并提示
|
||||||
|
await modalApi.close();
|
||||||
|
emit('success');
|
||||||
|
message.success($t('ui.actionMessage.operationSuccess'));
|
||||||
|
} finally {
|
||||||
|
modalApi.unlock();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async onOpenChange(isOpen: boolean) {
|
||||||
|
if (!isOpen) {
|
||||||
|
formData.value = undefined;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 加载数据
|
||||||
|
const data = modalApi.getData<SystemDictDataApi.DictData>();
|
||||||
|
if (!data || !data.id) {
|
||||||
|
// 设置 dictType
|
||||||
|
await formApi.setValues(data);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
modalApi.lock();
|
||||||
|
try {
|
||||||
|
formData.value = await getDictData(data.id);
|
||||||
|
// 设置到 values
|
||||||
|
await formApi.setValues(formData.value);
|
||||||
|
} finally {
|
||||||
|
modalApi.unlock();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Modal :title="getTitle">
|
||||||
|
<Form class="mx-4" />
|
||||||
|
</Modal>
|
||||||
|
</template>
|
||||||
208
apps/web-naive/src/views/system/dict/modules/data-grid.vue
Normal file
208
apps/web-naive/src/views/system/dict/modules/data-grid.vue
Normal file
@@ -0,0 +1,208 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||||
|
import type { SystemDictDataApi } from '#/api/system/dict/data';
|
||||||
|
|
||||||
|
import { ref, watch } from 'vue';
|
||||||
|
|
||||||
|
import { confirm, useVbenModal } from '@vben/common-ui';
|
||||||
|
import { downloadFileFromBlobPart, isEmpty } from '@vben/utils';
|
||||||
|
|
||||||
|
import { message } from '#/adapter/naive';
|
||||||
|
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||||
|
import {
|
||||||
|
deleteDictData,
|
||||||
|
deleteDictDataList,
|
||||||
|
exportDictData,
|
||||||
|
getDictDataPage,
|
||||||
|
} from '#/api/system/dict/data';
|
||||||
|
import { $t } from '#/locales';
|
||||||
|
|
||||||
|
import { useDataGridColumns, useDataGridFormSchema } from '../data';
|
||||||
|
import DataForm from './data-form.vue';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
dictType: {
|
||||||
|
type: String,
|
||||||
|
default: undefined,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const [DataFormModal, dataFormModalApi] = useVbenModal({
|
||||||
|
connectedComponent: DataForm,
|
||||||
|
destroyOnClose: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 刷新表格 */
|
||||||
|
function handleRefresh() {
|
||||||
|
gridApi.query();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 导出表格 */
|
||||||
|
async function handleExport() {
|
||||||
|
const data = await exportDictData(await gridApi.formApi.getValues());
|
||||||
|
downloadFileFromBlobPart({ fileName: '字典数据.xls', source: data });
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 创建字典数据 */
|
||||||
|
function handleCreate() {
|
||||||
|
dataFormModalApi.setData({ dictType: props.dictType }).open();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 编辑字典数据 */
|
||||||
|
function handleEdit(row: SystemDictDataApi.DictData) {
|
||||||
|
dataFormModalApi.setData(row).open();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 删除字典数据 */
|
||||||
|
async function handleDelete(row: SystemDictDataApi.DictData) {
|
||||||
|
const hideLoading = message.loading(
|
||||||
|
$t('ui.actionMessage.deleting', [row.label]),
|
||||||
|
{ duration: 0 },
|
||||||
|
);
|
||||||
|
try {
|
||||||
|
await deleteDictData(row.id!);
|
||||||
|
message.success($t('ui.actionMessage.deleteSuccess', [row.label]));
|
||||||
|
handleRefresh();
|
||||||
|
} finally {
|
||||||
|
hideLoading.destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 批量删除字典数据 */
|
||||||
|
async function handleDeleteBatch() {
|
||||||
|
await confirm($t('ui.actionMessage.deleteBatchConfirm'));
|
||||||
|
const hideLoading = message.loading($t('ui.actionMessage.deleting'), {
|
||||||
|
duration: 0,
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
await deleteDictDataList(checkedIds.value);
|
||||||
|
checkedIds.value = [];
|
||||||
|
message.success($t('ui.actionMessage.deleteSuccess'));
|
||||||
|
handleRefresh();
|
||||||
|
} finally {
|
||||||
|
hideLoading.destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const checkedIds = ref<number[]>([]);
|
||||||
|
function handleRowCheckboxChange({
|
||||||
|
records,
|
||||||
|
}: {
|
||||||
|
records: SystemDictDataApi.DictData[];
|
||||||
|
}) {
|
||||||
|
checkedIds.value = records.map((item) => item.id!);
|
||||||
|
}
|
||||||
|
|
||||||
|
const [Grid, gridApi] = useVbenVxeGrid({
|
||||||
|
formOptions: {
|
||||||
|
schema: useDataGridFormSchema(),
|
||||||
|
},
|
||||||
|
gridOptions: {
|
||||||
|
columns: useDataGridColumns(),
|
||||||
|
height: 'auto',
|
||||||
|
keepSource: true,
|
||||||
|
proxyConfig: {
|
||||||
|
ajax: {
|
||||||
|
query: async ({ page }, formValues) => {
|
||||||
|
return await getDictDataPage({
|
||||||
|
pageNo: page.currentPage,
|
||||||
|
pageSize: page.pageSize,
|
||||||
|
dictType: props.dictType,
|
||||||
|
...formValues,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
rowConfig: {
|
||||||
|
keyField: 'id',
|
||||||
|
isHover: true,
|
||||||
|
},
|
||||||
|
toolbarConfig: {
|
||||||
|
refresh: true,
|
||||||
|
search: true,
|
||||||
|
},
|
||||||
|
} as VxeTableGridOptions<SystemDictDataApi.DictData>,
|
||||||
|
gridEvents: {
|
||||||
|
checkboxAll: handleRowCheckboxChange,
|
||||||
|
checkboxChange: handleRowCheckboxChange,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 监听 dictType 变化,重新查询 */
|
||||||
|
watch(
|
||||||
|
() => props.dictType,
|
||||||
|
() => {
|
||||||
|
if (props.dictType) {
|
||||||
|
handleRefresh();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="flex h-full flex-col">
|
||||||
|
<DataFormModal @success="handleRefresh" />
|
||||||
|
|
||||||
|
<Grid table-title="字典数据列表">
|
||||||
|
<template #toolbar-tools>
|
||||||
|
<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,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: $t('ui.actionTitle.deleteBatch'),
|
||||||
|
type: 'error',
|
||||||
|
icon: ACTION_ICON.DELETE,
|
||||||
|
disabled: isEmpty(checkedIds),
|
||||||
|
auth: ['system:dict:delete'],
|
||||||
|
onClick: handleDeleteBatch,
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<template #colorType="{ row }">
|
||||||
|
<Tag :color="row.colorType">{{ row.colorType }}</Tag>
|
||||||
|
</template>
|
||||||
|
<template #cssClass="{ row }">
|
||||||
|
<Tag :color="row.cssClass">{{ row.cssClass }}</Tag>
|
||||||
|
</template>
|
||||||
|
<template #actions="{ row }">
|
||||||
|
<TableAction
|
||||||
|
:actions="[
|
||||||
|
{
|
||||||
|
label: $t('common.edit'),
|
||||||
|
type: 'primary',
|
||||||
|
text: true,
|
||||||
|
icon: ACTION_ICON.EDIT,
|
||||||
|
auth: ['system:dict:update'],
|
||||||
|
onClick: handleEdit.bind(null, row),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: $t('common.delete'),
|
||||||
|
type: 'error',
|
||||||
|
text: 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>
|
||||||
|
</template>
|
||||||
85
apps/web-naive/src/views/system/dict/modules/type-form.vue
Normal file
85
apps/web-naive/src/views/system/dict/modules/type-form.vue
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { SystemDictTypeApi } from '#/api/system/dict/type';
|
||||||
|
|
||||||
|
import { computed, ref } from 'vue';
|
||||||
|
|
||||||
|
import { useVbenModal } from '@vben/common-ui';
|
||||||
|
|
||||||
|
import { useVbenForm } from '#/adapter/form';
|
||||||
|
import { message } from '#/adapter/naive';
|
||||||
|
import {
|
||||||
|
createDictType,
|
||||||
|
getDictType,
|
||||||
|
updateDictType,
|
||||||
|
} from '#/api/system/dict/type';
|
||||||
|
import { $t } from '#/locales';
|
||||||
|
|
||||||
|
import { useTypeFormSchema } from '../data';
|
||||||
|
|
||||||
|
const emit = defineEmits(['success']);
|
||||||
|
const formData = ref<SystemDictTypeApi.DictType>();
|
||||||
|
const getTitle = computed(() => {
|
||||||
|
return formData.value?.id
|
||||||
|
? $t('ui.actionTitle.edit', ['字典类型'])
|
||||||
|
: $t('ui.actionTitle.create', ['字典类型']);
|
||||||
|
});
|
||||||
|
|
||||||
|
const [Form, formApi] = useVbenForm({
|
||||||
|
commonConfig: {
|
||||||
|
componentProps: {
|
||||||
|
class: 'w-full',
|
||||||
|
},
|
||||||
|
formItemClass: 'col-span-2',
|
||||||
|
labelWidth: 80,
|
||||||
|
},
|
||||||
|
layout: 'horizontal',
|
||||||
|
schema: useTypeFormSchema(),
|
||||||
|
showDefaultActions: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const [Modal, modalApi] = useVbenModal({
|
||||||
|
async onConfirm() {
|
||||||
|
const { valid } = await formApi.validate();
|
||||||
|
if (!valid) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
modalApi.lock();
|
||||||
|
// 提交表单
|
||||||
|
const data = (await formApi.getValues()) as SystemDictTypeApi.DictType;
|
||||||
|
try {
|
||||||
|
await (formData.value?.id ? updateDictType(data) : createDictType(data));
|
||||||
|
// 关闭并提示
|
||||||
|
await modalApi.close();
|
||||||
|
emit('success');
|
||||||
|
message.success($t('ui.actionMessage.operationSuccess'));
|
||||||
|
} finally {
|
||||||
|
modalApi.unlock();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async onOpenChange(isOpen: boolean) {
|
||||||
|
if (!isOpen) {
|
||||||
|
formData.value = undefined;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 加载数据
|
||||||
|
const data = modalApi.getData<SystemDictTypeApi.DictType>();
|
||||||
|
if (!data || !data.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
modalApi.lock();
|
||||||
|
try {
|
||||||
|
formData.value = await getDictType(data.id);
|
||||||
|
// 设置到 values
|
||||||
|
await formApi.setValues(formData.value);
|
||||||
|
} finally {
|
||||||
|
modalApi.unlock();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Modal :title="getTitle">
|
||||||
|
<Form class="mx-4" />
|
||||||
|
</Modal>
|
||||||
|
</template>
|
||||||
189
apps/web-naive/src/views/system/dict/modules/type-grid.vue
Normal file
189
apps/web-naive/src/views/system/dict/modules/type-grid.vue
Normal file
@@ -0,0 +1,189 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||||
|
import type { SystemDictTypeApi } from '#/api/system/dict/type';
|
||||||
|
|
||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
import { confirm, useVbenModal } from '@vben/common-ui';
|
||||||
|
import { downloadFileFromBlobPart, isEmpty } from '@vben/utils';
|
||||||
|
|
||||||
|
import { message } from '#/adapter/naive';
|
||||||
|
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||||
|
import {
|
||||||
|
deleteDictType,
|
||||||
|
deleteDictTypeList,
|
||||||
|
exportDictType,
|
||||||
|
getDictTypePage,
|
||||||
|
} from '#/api/system/dict/type';
|
||||||
|
import { $t } from '#/locales';
|
||||||
|
|
||||||
|
import { useTypeGridColumns, useTypeGridFormSchema } from '../data';
|
||||||
|
import TypeForm from './type-form.vue';
|
||||||
|
|
||||||
|
const emit = defineEmits(['select']);
|
||||||
|
|
||||||
|
const [TypeFormModal, typeFormModalApi] = useVbenModal({
|
||||||
|
connectedComponent: TypeForm,
|
||||||
|
destroyOnClose: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 刷新表格 */
|
||||||
|
function handleRefresh() {
|
||||||
|
gridApi.query();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 导出表格 */
|
||||||
|
async function handleExport() {
|
||||||
|
const data = await exportDictType(await gridApi.formApi.getValues());
|
||||||
|
downloadFileFromBlobPart({ fileName: '字典类型.xls', source: data });
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 创建字典类型 */
|
||||||
|
function handleCreate() {
|
||||||
|
typeFormModalApi.setData(null).open();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 编辑字典类型 */
|
||||||
|
function handleEdit(row: SystemDictTypeApi.DictType) {
|
||||||
|
typeFormModalApi.setData(row).open();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 删除字典类型 */
|
||||||
|
async function handleDelete(row: SystemDictTypeApi.DictType) {
|
||||||
|
const hideLoading = message.loading(
|
||||||
|
$t('ui.actionMessage.deleting', [row.name]),
|
||||||
|
{ duration: 0 },
|
||||||
|
);
|
||||||
|
try {
|
||||||
|
await deleteDictType(row.id!);
|
||||||
|
message.success($t('ui.actionMessage.deleteSuccess', [row.name]));
|
||||||
|
handleRefresh();
|
||||||
|
} finally {
|
||||||
|
hideLoading.destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 批量删除字典类型 */
|
||||||
|
async function handleDeleteBatch() {
|
||||||
|
await confirm($t('ui.actionMessage.deleteBatchConfirm'));
|
||||||
|
const hideLoading = message.loading($t('ui.actionMessage.deletingBatch'), {
|
||||||
|
duration: 0,
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
await deleteDictTypeList(checkedIds.value);
|
||||||
|
checkedIds.value = [];
|
||||||
|
message.success($t('ui.actionMessage.deleteSuccess'), { duration: 0 });
|
||||||
|
handleRefresh();
|
||||||
|
} finally {
|
||||||
|
hideLoading.destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const checkedIds = ref<number[]>([]);
|
||||||
|
function handleRowCheckboxChange({
|
||||||
|
records,
|
||||||
|
}: {
|
||||||
|
records: SystemDictTypeApi.DictType[];
|
||||||
|
}) {
|
||||||
|
checkedIds.value = records.map((item) => item.id!);
|
||||||
|
}
|
||||||
|
|
||||||
|
const [Grid, gridApi] = useVbenVxeGrid({
|
||||||
|
formOptions: {
|
||||||
|
schema: useTypeGridFormSchema(),
|
||||||
|
},
|
||||||
|
gridOptions: {
|
||||||
|
columns: useTypeGridColumns(),
|
||||||
|
height: 'auto',
|
||||||
|
keepSource: true,
|
||||||
|
proxyConfig: {
|
||||||
|
ajax: {
|
||||||
|
query: async ({ page }, formValues) => {
|
||||||
|
return await getDictTypePage({
|
||||||
|
pageNo: page.currentPage,
|
||||||
|
pageSize: page.pageSize,
|
||||||
|
...formValues,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
rowConfig: {
|
||||||
|
keyField: 'id',
|
||||||
|
isCurrent: true,
|
||||||
|
isHover: true,
|
||||||
|
},
|
||||||
|
toolbarConfig: {
|
||||||
|
refresh: true,
|
||||||
|
search: true,
|
||||||
|
},
|
||||||
|
} as VxeTableGridOptions<SystemDictTypeApi.DictType>,
|
||||||
|
gridEvents: {
|
||||||
|
cellClick: ({ row }: { row: SystemDictTypeApi.DictType }) => {
|
||||||
|
emit('select', row.type);
|
||||||
|
},
|
||||||
|
checkboxAll: handleRowCheckboxChange,
|
||||||
|
checkboxChange: handleRowCheckboxChange,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="h-full">
|
||||||
|
<TypeFormModal @success="handleRefresh" />
|
||||||
|
<Grid table-title="字典类型列表">
|
||||||
|
<template #toolbar-tools>
|
||||||
|
<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,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: $t('ui.actionTitle.deleteBatch'),
|
||||||
|
type: 'error',
|
||||||
|
icon: ACTION_ICON.DELETE,
|
||||||
|
disabled: isEmpty(checkedIds),
|
||||||
|
auth: ['system:dict:delete'],
|
||||||
|
onClick: handleDeleteBatch,
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<template #actions="{ row }">
|
||||||
|
<TableAction
|
||||||
|
:actions="[
|
||||||
|
{
|
||||||
|
label: $t('common.edit'),
|
||||||
|
type: 'primary',
|
||||||
|
text: true,
|
||||||
|
icon: ACTION_ICON.EDIT,
|
||||||
|
auth: ['system:dict:update'],
|
||||||
|
onClick: handleEdit.bind(null, row),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: $t('common.delete'),
|
||||||
|
type: 'error',
|
||||||
|
text: 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>
|
||||||
|
</template>
|
||||||
150
apps/web-naive/src/views/system/loginlog/data.ts
Normal file
150
apps/web-naive/src/views/system/loginlog/data.ts
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
import type { VbenFormSchema } from '#/adapter/form';
|
||||||
|
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||||
|
import type { SystemLoginLogApi } from '#/api/system/login-log';
|
||||||
|
import type { DescriptionItemSchema } from '#/components/description';
|
||||||
|
|
||||||
|
import { h } from 'vue';
|
||||||
|
|
||||||
|
import { DICT_TYPE } from '@vben/constants';
|
||||||
|
import { formatDateTime } from '@vben/utils';
|
||||||
|
|
||||||
|
import { DictTag } from '#/components/dict-tag';
|
||||||
|
import { getRangePickerDefaultProps } from '#/utils';
|
||||||
|
|
||||||
|
/** 列表的搜索表单 */
|
||||||
|
export function useGridFormSchema(): VbenFormSchema[] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
fieldName: 'username',
|
||||||
|
label: '用户名称',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
clearable: true,
|
||||||
|
placeholder: '请输入用户名称',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'userIp',
|
||||||
|
label: '登录地址',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
clearable: true,
|
||||||
|
placeholder: '请输入登录地址',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'createTime',
|
||||||
|
label: '登录时间',
|
||||||
|
component: 'DatePicker',
|
||||||
|
componentProps: {
|
||||||
|
...getRangePickerDefaultProps(),
|
||||||
|
clearable: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 列表的字段 */
|
||||||
|
export function useGridColumns(): VxeTableGridOptions['columns'] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
field: 'id',
|
||||||
|
title: '日志编号',
|
||||||
|
minWidth: 100,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'logType',
|
||||||
|
title: '操作类型',
|
||||||
|
minWidth: 120,
|
||||||
|
cellRender: {
|
||||||
|
name: 'CellDict',
|
||||||
|
props: { type: DICT_TYPE.SYSTEM_LOGIN_TYPE },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'username',
|
||||||
|
title: '用户名称',
|
||||||
|
minWidth: 180,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'userIp',
|
||||||
|
title: '登录地址',
|
||||||
|
minWidth: 180,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'userAgent',
|
||||||
|
title: '浏览器',
|
||||||
|
minWidth: 200,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'result',
|
||||||
|
title: '登录结果',
|
||||||
|
minWidth: 120,
|
||||||
|
cellRender: {
|
||||||
|
name: 'CellDict',
|
||||||
|
props: { type: DICT_TYPE.SYSTEM_LOGIN_RESULT },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'createTime',
|
||||||
|
title: '登录日期',
|
||||||
|
minWidth: 180,
|
||||||
|
formatter: 'formatDateTime',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '操作',
|
||||||
|
width: 120,
|
||||||
|
fixed: 'right',
|
||||||
|
slots: { default: 'actions' },
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 详情页的字段 */
|
||||||
|
export function useDetailSchema(): DescriptionItemSchema[] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
field: 'id',
|
||||||
|
label: '日志编号',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'logType',
|
||||||
|
label: '操作类型',
|
||||||
|
content: (data: SystemLoginLogApi.LoginLog) => {
|
||||||
|
return h(DictTag, {
|
||||||
|
type: DICT_TYPE.SYSTEM_LOGIN_TYPE,
|
||||||
|
value: data?.logType,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'username',
|
||||||
|
label: '用户名称',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'userIp',
|
||||||
|
label: '登录地址',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'userAgent',
|
||||||
|
label: '浏览器',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'result',
|
||||||
|
label: '登录结果',
|
||||||
|
content: (data: SystemLoginLogApi.LoginLog) => {
|
||||||
|
return h(DictTag, {
|
||||||
|
type: DICT_TYPE.SYSTEM_LOGIN_RESULT,
|
||||||
|
value: data?.result,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'createTime',
|
||||||
|
label: '登录日期',
|
||||||
|
content: (data: SystemLoginLogApi.LoginLog) => {
|
||||||
|
return formatDateTime(data?.createTime || '') as string;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
104
apps/web-naive/src/views/system/loginlog/index.vue
Normal file
104
apps/web-naive/src/views/system/loginlog/index.vue
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||||
|
import type { SystemLoginLogApi } from '#/api/system/login-log';
|
||||||
|
|
||||||
|
import { DocAlert, Page, useVbenModal } from '@vben/common-ui';
|
||||||
|
import { downloadFileFromBlobPart } from '@vben/utils';
|
||||||
|
|
||||||
|
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||||
|
import { exportLoginLog, getLoginLogPage } from '#/api/system/login-log';
|
||||||
|
import { $t } from '#/locales';
|
||||||
|
|
||||||
|
import { useGridColumns, useGridFormSchema } from './data';
|
||||||
|
import Detail from './modules/detail.vue';
|
||||||
|
|
||||||
|
const [DetailModal, detailModalApi] = useVbenModal({
|
||||||
|
connectedComponent: Detail,
|
||||||
|
destroyOnClose: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 刷新表格 */
|
||||||
|
function handleRefresh() {
|
||||||
|
gridApi.query();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 导出表格 */
|
||||||
|
async function handleExport() {
|
||||||
|
const data = await exportLoginLog(await gridApi.formApi.getValues());
|
||||||
|
downloadFileFromBlobPart({ fileName: '登录日志.xls', source: data });
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 查看登录日志详情 */
|
||||||
|
function handleDetail(row: SystemLoginLogApi.LoginLog) {
|
||||||
|
detailModalApi.setData(row).open();
|
||||||
|
}
|
||||||
|
|
||||||
|
const [Grid, gridApi] = useVbenVxeGrid({
|
||||||
|
formOptions: {
|
||||||
|
schema: useGridFormSchema(),
|
||||||
|
},
|
||||||
|
gridOptions: {
|
||||||
|
columns: useGridColumns(),
|
||||||
|
height: 'auto',
|
||||||
|
keepSource: true,
|
||||||
|
proxyConfig: {
|
||||||
|
ajax: {
|
||||||
|
query: async ({ page }, formValues) => {
|
||||||
|
return await getLoginLogPage({
|
||||||
|
pageNo: page.currentPage,
|
||||||
|
pageSize: page.pageSize,
|
||||||
|
...formValues,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
rowConfig: {
|
||||||
|
keyField: 'id',
|
||||||
|
isHover: true,
|
||||||
|
},
|
||||||
|
toolbarConfig: {
|
||||||
|
refresh: true,
|
||||||
|
search: true,
|
||||||
|
},
|
||||||
|
} as VxeTableGridOptions<SystemLoginLogApi.LoginLog>,
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Page auto-content-height>
|
||||||
|
<template #doc>
|
||||||
|
<DocAlert title="系统日志" url="https://doc.iocoder.cn/system-log/" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<DetailModal @success="handleRefresh" />
|
||||||
|
<Grid table-title="登录日志列表">
|
||||||
|
<template #toolbar-tools>
|
||||||
|
<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: 'primary',
|
||||||
|
text: true,
|
||||||
|
icon: ACTION_ICON.VIEW,
|
||||||
|
auth: ['system:login-log:query'],
|
||||||
|
onClick: handleDetail.bind(null, row),
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</Grid>
|
||||||
|
</Page>
|
||||||
|
</template>
|
||||||
53
apps/web-naive/src/views/system/loginlog/modules/detail.vue
Normal file
53
apps/web-naive/src/views/system/loginlog/modules/detail.vue
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { SystemLoginLogApi } from '#/api/system/login-log';
|
||||||
|
|
||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
import { useVbenModal } from '@vben/common-ui';
|
||||||
|
|
||||||
|
import { useDescription } from '#/components/description';
|
||||||
|
|
||||||
|
import { useDetailSchema } from '../data';
|
||||||
|
|
||||||
|
const formData = ref<SystemLoginLogApi.LoginLog>();
|
||||||
|
|
||||||
|
const [Descriptions] = useDescription({
|
||||||
|
componentProps: {
|
||||||
|
bordered: true,
|
||||||
|
column: 1,
|
||||||
|
contentClass: 'mx-4',
|
||||||
|
},
|
||||||
|
schema: useDetailSchema(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const [Modal, modalApi] = useVbenModal({
|
||||||
|
async onOpenChange(isOpen: boolean) {
|
||||||
|
if (!isOpen) {
|
||||||
|
formData.value = undefined;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 加载数据
|
||||||
|
const data = modalApi.getData<SystemLoginLogApi.LoginLog>();
|
||||||
|
if (!data || !data.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
modalApi.lock();
|
||||||
|
try {
|
||||||
|
formData.value = data;
|
||||||
|
} finally {
|
||||||
|
modalApi.unlock();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Modal
|
||||||
|
title="登录日志详情"
|
||||||
|
class="w-1/2"
|
||||||
|
:show-cancel-button="false"
|
||||||
|
:show-confirm-button="false"
|
||||||
|
>
|
||||||
|
<Descriptions :data="formData" />
|
||||||
|
</Modal>
|
||||||
|
</template>
|
||||||
185
apps/web-naive/src/views/system/mail/account/data.ts
Normal file
185
apps/web-naive/src/views/system/mail/account/data.ts
Normal file
@@ -0,0 +1,185 @@
|
|||||||
|
import type { VbenFormSchema } from '#/adapter/form';
|
||||||
|
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||||
|
|
||||||
|
import { DICT_TYPE } from '@vben/constants';
|
||||||
|
import { getDictOptions } from '@vben/hooks';
|
||||||
|
|
||||||
|
import { z } from '#/adapter/form';
|
||||||
|
|
||||||
|
/** 新增/修改的表单 */
|
||||||
|
export function useFormSchema(): VbenFormSchema[] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
fieldName: 'id',
|
||||||
|
component: 'Input',
|
||||||
|
dependencies: {
|
||||||
|
triggerFields: [''],
|
||||||
|
show: () => false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'mail',
|
||||||
|
label: '邮箱',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入邮箱',
|
||||||
|
},
|
||||||
|
rules: 'required',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'username',
|
||||||
|
label: '用户名',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入用户名',
|
||||||
|
},
|
||||||
|
rules: 'required',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'password',
|
||||||
|
label: '密码',
|
||||||
|
component: 'InputPassword',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入密码',
|
||||||
|
},
|
||||||
|
rules: 'required',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'host',
|
||||||
|
label: 'SMTP 服务器域名',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入 SMTP 服务器域名',
|
||||||
|
},
|
||||||
|
rules: 'required',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'port',
|
||||||
|
label: 'SMTP 服务器端口',
|
||||||
|
component: 'InputNumber',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入 SMTP 服务器端口',
|
||||||
|
min: 0,
|
||||||
|
max: 65_535,
|
||||||
|
},
|
||||||
|
rules: 'required',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'sslEnable',
|
||||||
|
label: '是否开启 SSL',
|
||||||
|
component: 'RadioGroup',
|
||||||
|
componentProps: {
|
||||||
|
options: getDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING, 'boolean'),
|
||||||
|
buttonStyle: 'solid',
|
||||||
|
optionType: 'button',
|
||||||
|
},
|
||||||
|
rules: z.boolean().default(true),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'starttlsEnable',
|
||||||
|
label: '是否开启 STARTTLS',
|
||||||
|
component: 'RadioGroup',
|
||||||
|
componentProps: {
|
||||||
|
options: getDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING, 'boolean'),
|
||||||
|
buttonStyle: 'solid',
|
||||||
|
optionType: 'button',
|
||||||
|
},
|
||||||
|
rules: z.boolean().default(false),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'remark',
|
||||||
|
label: '备注',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
type: 'textarea',
|
||||||
|
placeholder: '请输入备注',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 列表的搜索表单 */
|
||||||
|
export function useGridFormSchema(): VbenFormSchema[] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
fieldName: 'mail',
|
||||||
|
label: '邮箱',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入邮箱',
|
||||||
|
clearable: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'username',
|
||||||
|
label: '用户名',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入用户名',
|
||||||
|
clearable: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 列表的字段 */
|
||||||
|
export function useGridColumns(): VxeTableGridOptions['columns'] {
|
||||||
|
return [
|
||||||
|
{ type: 'checkbox', width: 40 },
|
||||||
|
{
|
||||||
|
field: 'id',
|
||||||
|
title: '编号',
|
||||||
|
minWidth: 100,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'mail',
|
||||||
|
title: '邮箱',
|
||||||
|
minWidth: 160,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'username',
|
||||||
|
title: '用户名',
|
||||||
|
minWidth: 160,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'host',
|
||||||
|
title: 'SMTP 服务器域名',
|
||||||
|
minWidth: 150,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'port',
|
||||||
|
title: 'SMTP 服务器端口',
|
||||||
|
minWidth: 130,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'sslEnable',
|
||||||
|
title: '是否开启 SSL',
|
||||||
|
minWidth: 120,
|
||||||
|
cellRender: {
|
||||||
|
name: 'CellDict',
|
||||||
|
props: { type: DICT_TYPE.INFRA_BOOLEAN_STRING },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'starttlsEnable',
|
||||||
|
title: '是否开启 STARTTLS',
|
||||||
|
minWidth: 145,
|
||||||
|
cellRender: {
|
||||||
|
name: 'CellDict',
|
||||||
|
props: { type: DICT_TYPE.INFRA_BOOLEAN_STRING },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'createTime',
|
||||||
|
title: '创建时间',
|
||||||
|
minWidth: 180,
|
||||||
|
formatter: 'formatDateTime',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '操作',
|
||||||
|
width: 130,
|
||||||
|
fixed: 'right',
|
||||||
|
slots: { default: 'actions' },
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
172
apps/web-naive/src/views/system/mail/account/index.vue
Normal file
172
apps/web-naive/src/views/system/mail/account/index.vue
Normal file
@@ -0,0 +1,172 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||||
|
import type { SystemMailAccountApi } from '#/api/system/mail/account';
|
||||||
|
|
||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
import { confirm, DocAlert, Page, useVbenModal } from '@vben/common-ui';
|
||||||
|
import { isEmpty } from '@vben/utils';
|
||||||
|
|
||||||
|
import { message } from '#/adapter/naive';
|
||||||
|
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||||
|
import {
|
||||||
|
deleteMailAccount,
|
||||||
|
deleteMailAccountList,
|
||||||
|
getMailAccountPage,
|
||||||
|
} from '#/api/system/mail/account';
|
||||||
|
import { $t } from '#/locales';
|
||||||
|
|
||||||
|
import { useGridColumns, useGridFormSchema } from './data';
|
||||||
|
import Form from './modules/form.vue';
|
||||||
|
|
||||||
|
const [FormModal, formModalApi] = useVbenModal({
|
||||||
|
connectedComponent: Form,
|
||||||
|
destroyOnClose: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 刷新表格 */
|
||||||
|
function handleRefresh() {
|
||||||
|
gridApi.query();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 创建邮箱账号 */
|
||||||
|
function handleCreate() {
|
||||||
|
formModalApi.setData(null).open();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 编辑邮箱账号 */
|
||||||
|
function handleEdit(row: SystemMailAccountApi.MailAccount) {
|
||||||
|
formModalApi.setData(row).open();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 删除邮箱账号 */
|
||||||
|
async function handleDelete(row: SystemMailAccountApi.MailAccount) {
|
||||||
|
const hideLoading = message.loading(
|
||||||
|
$t('ui.actionMessage.deleting', [row.mail]),
|
||||||
|
{ duration: 0 },
|
||||||
|
);
|
||||||
|
try {
|
||||||
|
await deleteMailAccount(row.id!);
|
||||||
|
message.success($t('ui.actionMessage.deleteSuccess', [row.mail]));
|
||||||
|
handleRefresh();
|
||||||
|
} finally {
|
||||||
|
hideLoading.destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 批量删除邮箱账号 */
|
||||||
|
async function handleDeleteBatch() {
|
||||||
|
await confirm($t('ui.actionMessage.deleteBatchConfirm'));
|
||||||
|
const hideLoading = message.loading($t('ui.actionMessage.deletingBatch'), {
|
||||||
|
duration: 0,
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
await deleteMailAccountList(checkedIds.value);
|
||||||
|
checkedIds.value = [];
|
||||||
|
message.success($t('ui.actionMessage.deleteSuccess'), { duration: 0 });
|
||||||
|
handleRefresh();
|
||||||
|
} finally {
|
||||||
|
hideLoading.destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const checkedIds = ref<number[]>([]);
|
||||||
|
function handleRowCheckboxChange({
|
||||||
|
records,
|
||||||
|
}: {
|
||||||
|
records: SystemMailAccountApi.MailAccount[];
|
||||||
|
}) {
|
||||||
|
checkedIds.value = records.map((item) => item.id!);
|
||||||
|
}
|
||||||
|
|
||||||
|
const [Grid, gridApi] = useVbenVxeGrid({
|
||||||
|
formOptions: {
|
||||||
|
schema: useGridFormSchema(),
|
||||||
|
},
|
||||||
|
gridOptions: {
|
||||||
|
columns: useGridColumns(),
|
||||||
|
height: 'auto',
|
||||||
|
keepSource: true,
|
||||||
|
proxyConfig: {
|
||||||
|
ajax: {
|
||||||
|
query: async ({ page }, formValues) => {
|
||||||
|
return await getMailAccountPage({
|
||||||
|
pageNo: page.currentPage,
|
||||||
|
pageSize: page.pageSize,
|
||||||
|
...formValues,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
rowConfig: {
|
||||||
|
keyField: 'id',
|
||||||
|
isHover: true,
|
||||||
|
},
|
||||||
|
toolbarConfig: {
|
||||||
|
refresh: true,
|
||||||
|
search: true,
|
||||||
|
},
|
||||||
|
} as VxeTableGridOptions<SystemMailAccountApi.MailAccount>,
|
||||||
|
gridEvents: {
|
||||||
|
checkboxAll: handleRowCheckboxChange,
|
||||||
|
checkboxChange: handleRowCheckboxChange,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<Page auto-content-height>
|
||||||
|
<template #doc>
|
||||||
|
<DocAlert title="邮件配置" url="https://doc.iocoder.cn/mail" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<FormModal @success="handleRefresh" />
|
||||||
|
<Grid table-title="邮箱账号列表">
|
||||||
|
<template #toolbar-tools>
|
||||||
|
<TableAction
|
||||||
|
:actions="[
|
||||||
|
{
|
||||||
|
label: $t('ui.actionTitle.create', ['邮箱账号']),
|
||||||
|
type: 'primary',
|
||||||
|
icon: ACTION_ICON.ADD,
|
||||||
|
auth: ['system:mail-account:create'],
|
||||||
|
onClick: handleCreate,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: $t('ui.actionTitle.deleteBatch'),
|
||||||
|
type: 'error',
|
||||||
|
icon: ACTION_ICON.DELETE,
|
||||||
|
auth: ['system:mail-account:delete'],
|
||||||
|
disabled: isEmpty(checkedIds),
|
||||||
|
onClick: handleDeleteBatch,
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<template #actions="{ row }">
|
||||||
|
<TableAction
|
||||||
|
:actions="[
|
||||||
|
{
|
||||||
|
label: $t('common.edit'),
|
||||||
|
type: 'primary',
|
||||||
|
text: true,
|
||||||
|
icon: ACTION_ICON.EDIT,
|
||||||
|
auth: ['system:mail-account:update'],
|
||||||
|
onClick: handleEdit.bind(null, row),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: $t('common.delete'),
|
||||||
|
type: 'error',
|
||||||
|
text: true,
|
||||||
|
icon: ACTION_ICON.DELETE,
|
||||||
|
auth: ['system:mail-account:delete'],
|
||||||
|
popConfirm: {
|
||||||
|
title: $t('ui.actionMessage.deleteConfirm', [row.mail]),
|
||||||
|
confirm: handleDelete.bind(null, row),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</Grid>
|
||||||
|
</Page>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { SystemMailAccountApi } from '#/api/system/mail/account';
|
||||||
|
|
||||||
|
import { computed, ref } from 'vue';
|
||||||
|
|
||||||
|
import { useVbenModal } from '@vben/common-ui';
|
||||||
|
|
||||||
|
import { useVbenForm } from '#/adapter/form';
|
||||||
|
import { message } from '#/adapter/naive';
|
||||||
|
import {
|
||||||
|
createMailAccount,
|
||||||
|
getMailAccount,
|
||||||
|
updateMailAccount,
|
||||||
|
} from '#/api/system/mail/account';
|
||||||
|
import { $t } from '#/locales';
|
||||||
|
|
||||||
|
import { useFormSchema } from '../data';
|
||||||
|
|
||||||
|
const emit = defineEmits(['success']);
|
||||||
|
const formData = ref<SystemMailAccountApi.MailAccount>();
|
||||||
|
const getTitle = computed(() => {
|
||||||
|
return formData.value?.id
|
||||||
|
? $t('ui.actionTitle.edit', ['邮箱账号'])
|
||||||
|
: $t('ui.actionTitle.create', ['邮箱账号']);
|
||||||
|
});
|
||||||
|
|
||||||
|
const [Form, formApi] = useVbenForm({
|
||||||
|
commonConfig: {
|
||||||
|
componentProps: {
|
||||||
|
class: 'w-full',
|
||||||
|
},
|
||||||
|
formItemClass: 'col-span-2',
|
||||||
|
labelWidth: 140,
|
||||||
|
},
|
||||||
|
layout: 'horizontal',
|
||||||
|
schema: useFormSchema(),
|
||||||
|
showDefaultActions: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const [Modal, modalApi] = useVbenModal({
|
||||||
|
async onConfirm() {
|
||||||
|
const { valid } = await formApi.validate();
|
||||||
|
if (!valid) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
modalApi.lock();
|
||||||
|
// 提交表单
|
||||||
|
const data =
|
||||||
|
(await formApi.getValues()) as SystemMailAccountApi.MailAccount;
|
||||||
|
try {
|
||||||
|
await (formData.value?.id
|
||||||
|
? updateMailAccount(data)
|
||||||
|
: createMailAccount(data));
|
||||||
|
// 关闭并提示
|
||||||
|
await modalApi.close();
|
||||||
|
emit('success');
|
||||||
|
message.success($t('ui.actionMessage.operationSuccess'));
|
||||||
|
} finally {
|
||||||
|
modalApi.unlock();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async onOpenChange(isOpen: boolean) {
|
||||||
|
if (!isOpen) {
|
||||||
|
formData.value = undefined;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 加载数据
|
||||||
|
const data = modalApi.getData<SystemMailAccountApi.MailAccount>();
|
||||||
|
if (!data || !data.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
modalApi.lock();
|
||||||
|
try {
|
||||||
|
formData.value = await getMailAccount(data.id);
|
||||||
|
// 设置到 values
|
||||||
|
await formApi.setValues(formData.value);
|
||||||
|
} finally {
|
||||||
|
modalApi.unlock();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Modal :title="getTitle" class="w-1/3">
|
||||||
|
<Form class="mx-4" />
|
||||||
|
</Modal>
|
||||||
|
</template>
|
||||||
262
apps/web-naive/src/views/system/mail/log/data.ts
Normal file
262
apps/web-naive/src/views/system/mail/log/data.ts
Normal file
@@ -0,0 +1,262 @@
|
|||||||
|
import type { VbenFormSchema } from '#/adapter/form';
|
||||||
|
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||||
|
import type { SystemMailLogApi } from '#/api/system/mail/log';
|
||||||
|
import type { DescriptionItemSchema } from '#/components/description';
|
||||||
|
|
||||||
|
import { h } from 'vue';
|
||||||
|
|
||||||
|
import { DICT_TYPE } from '@vben/constants';
|
||||||
|
import { getDictOptions } from '@vben/hooks';
|
||||||
|
import { formatDateTime } from '@vben/utils';
|
||||||
|
|
||||||
|
import { getSimpleMailAccountList } from '#/api/system/mail/account';
|
||||||
|
import { DictTag } from '#/components/dict-tag';
|
||||||
|
import { getRangePickerDefaultProps } from '#/utils';
|
||||||
|
|
||||||
|
/** 列表的搜索表单 */
|
||||||
|
export function useGridFormSchema(): VbenFormSchema[] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
fieldName: 'sendTime',
|
||||||
|
label: '发送时间',
|
||||||
|
component: 'DatePicker',
|
||||||
|
componentProps: {
|
||||||
|
...getRangePickerDefaultProps(),
|
||||||
|
clearable: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'userId',
|
||||||
|
label: '用户编号',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
clearable: true,
|
||||||
|
placeholder: '请输入用户编号',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'userType',
|
||||||
|
label: '用户类型',
|
||||||
|
component: 'Select',
|
||||||
|
componentProps: {
|
||||||
|
options: getDictOptions(DICT_TYPE.USER_TYPE, 'number'),
|
||||||
|
clearable: true,
|
||||||
|
placeholder: '请选择用户类型',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'sendStatus',
|
||||||
|
label: '发送状态',
|
||||||
|
component: 'Select',
|
||||||
|
componentProps: {
|
||||||
|
options: getDictOptions(DICT_TYPE.SYSTEM_MAIL_SEND_STATUS, 'number'),
|
||||||
|
clearable: true,
|
||||||
|
placeholder: '请选择发送状态',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'accountId',
|
||||||
|
label: '邮箱账号',
|
||||||
|
component: 'ApiSelect',
|
||||||
|
componentProps: {
|
||||||
|
api: async () => await getSimpleMailAccountList(),
|
||||||
|
labelField: 'mail',
|
||||||
|
valueField: 'id',
|
||||||
|
clearable: true,
|
||||||
|
placeholder: '请选择邮箱账号',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'templateId',
|
||||||
|
label: '模板编号',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
clearable: true,
|
||||||
|
placeholder: '请输入模板编号',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 列表的字段 */
|
||||||
|
export function useGridColumns(): VxeTableGridOptions['columns'] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
field: 'id',
|
||||||
|
title: '编号',
|
||||||
|
minWidth: 100,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'sendTime',
|
||||||
|
title: '发送时间',
|
||||||
|
minWidth: 180,
|
||||||
|
formatter: 'formatDateTime',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'userType',
|
||||||
|
title: '接收用户',
|
||||||
|
minWidth: 150,
|
||||||
|
slots: { default: 'userInfo' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'toMails',
|
||||||
|
title: '接收信息',
|
||||||
|
minWidth: 300,
|
||||||
|
formatter: ({ row }) => {
|
||||||
|
const lines: string[] = [];
|
||||||
|
if (row.toMails && row.toMails.length > 0) {
|
||||||
|
lines.push(`收件:${row.toMails.join('、')}`);
|
||||||
|
}
|
||||||
|
if (row.ccMails && row.ccMails.length > 0) {
|
||||||
|
lines.push(`抄送:${row.ccMails.join('、')}`);
|
||||||
|
}
|
||||||
|
if (row.bccMails && row.bccMails.length > 0) {
|
||||||
|
lines.push(`密送:${row.bccMails.join('、')}`);
|
||||||
|
}
|
||||||
|
return lines.join('\n');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'templateTitle',
|
||||||
|
title: '邮件标题',
|
||||||
|
minWidth: 120,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'templateContent',
|
||||||
|
title: '邮件内容',
|
||||||
|
minWidth: 300,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'fromMail',
|
||||||
|
title: '发送邮箱',
|
||||||
|
minWidth: 120,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'sendStatus',
|
||||||
|
title: '发送状态',
|
||||||
|
minWidth: 120,
|
||||||
|
cellRender: {
|
||||||
|
name: 'CellDict',
|
||||||
|
props: { type: DICT_TYPE.SYSTEM_MAIL_SEND_STATUS },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'templateCode',
|
||||||
|
title: '模板编码',
|
||||||
|
minWidth: 120,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '操作',
|
||||||
|
width: 80,
|
||||||
|
fixed: 'right',
|
||||||
|
slots: { default: 'actions' },
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 详情页的字段 */
|
||||||
|
export function useDetailSchema(): DescriptionItemSchema[] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
field: 'id',
|
||||||
|
label: '编号',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'createTime',
|
||||||
|
label: '创建时间',
|
||||||
|
content: (data: SystemMailLogApi.MailLog) => {
|
||||||
|
return formatDateTime(data?.createTime || '') as string;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'fromMail',
|
||||||
|
label: '发送邮箱',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'userId',
|
||||||
|
label: '接收用户',
|
||||||
|
content: (data: SystemMailLogApi.MailLog) => {
|
||||||
|
if (data?.userType && data?.userId) {
|
||||||
|
return h('div', [
|
||||||
|
h(DictTag, {
|
||||||
|
type: DICT_TYPE.USER_TYPE,
|
||||||
|
value: data.userType,
|
||||||
|
}),
|
||||||
|
` (${data.userId})`,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
return '无';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'toMails',
|
||||||
|
label: '接收信息',
|
||||||
|
content: (data: SystemMailLogApi.MailLog) => {
|
||||||
|
const lines: string[] = [];
|
||||||
|
if (data?.toMails && data.toMails.length > 0) {
|
||||||
|
lines.push(`收件:${data.toMails.join('、')}`);
|
||||||
|
}
|
||||||
|
if (data?.ccMails && data.ccMails.length > 0) {
|
||||||
|
lines.push(`抄送:${data.ccMails.join('、')}`);
|
||||||
|
}
|
||||||
|
if (data?.bccMails && data.bccMails.length > 0) {
|
||||||
|
lines.push(`密送:${data.bccMails.join('、')}`);
|
||||||
|
}
|
||||||
|
return h(
|
||||||
|
'div',
|
||||||
|
{
|
||||||
|
style: { whiteSpace: 'pre-line' },
|
||||||
|
},
|
||||||
|
lines.join('\n'),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'templateId',
|
||||||
|
label: '模板编号',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'templateCode',
|
||||||
|
label: '模板编码',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'templateTitle',
|
||||||
|
label: '邮件标题',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'templateContent',
|
||||||
|
label: '邮件内容',
|
||||||
|
span: 2,
|
||||||
|
content: (data: SystemMailLogApi.MailLog) => {
|
||||||
|
return h('div', {
|
||||||
|
innerHTML: data?.templateContent || '',
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'sendStatus',
|
||||||
|
label: '发送状态',
|
||||||
|
content: (data: SystemMailLogApi.MailLog) => {
|
||||||
|
return h(DictTag, {
|
||||||
|
type: DICT_TYPE.SYSTEM_MAIL_SEND_STATUS,
|
||||||
|
value: data?.sendStatus,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'sendTime',
|
||||||
|
label: '发送时间',
|
||||||
|
content: (data: SystemMailLogApi.MailLog) => {
|
||||||
|
return formatDateTime(data?.sendTime || '') as string;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'sendMessageId',
|
||||||
|
label: '发送消息编号',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'sendException',
|
||||||
|
label: '发送异常',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
91
apps/web-naive/src/views/system/mail/log/index.vue
Normal file
91
apps/web-naive/src/views/system/mail/log/index.vue
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||||
|
import type { SystemMailLogApi } from '#/api/system/mail/log';
|
||||||
|
|
||||||
|
import { DocAlert, Page, useVbenModal } from '@vben/common-ui';
|
||||||
|
import { DICT_TYPE } from '@vben/constants';
|
||||||
|
|
||||||
|
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||||
|
import { getMailLogPage } from '#/api/system/mail/log';
|
||||||
|
import { DictTag } from '#/components/dict-tag';
|
||||||
|
import { $t } from '#/locales';
|
||||||
|
|
||||||
|
import { useGridColumns, useGridFormSchema } from './data';
|
||||||
|
import Detail from './modules/detail.vue';
|
||||||
|
|
||||||
|
const [DetailModal, detailModalApi] = useVbenModal({
|
||||||
|
connectedComponent: Detail,
|
||||||
|
destroyOnClose: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 刷新表格 */
|
||||||
|
function handleRefresh() {
|
||||||
|
gridApi.query();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 查看邮件日志 */
|
||||||
|
function handleDetail(row: SystemMailLogApi.MailLog) {
|
||||||
|
detailModalApi.setData(row).open();
|
||||||
|
}
|
||||||
|
|
||||||
|
const [Grid, gridApi] = useVbenVxeGrid({
|
||||||
|
formOptions: {
|
||||||
|
schema: useGridFormSchema(),
|
||||||
|
},
|
||||||
|
gridOptions: {
|
||||||
|
columns: useGridColumns(),
|
||||||
|
height: 'auto',
|
||||||
|
keepSource: true,
|
||||||
|
proxyConfig: {
|
||||||
|
ajax: {
|
||||||
|
query: async ({ page }, formValues) => {
|
||||||
|
return await getMailLogPage({
|
||||||
|
pageNo: page.currentPage,
|
||||||
|
pageSize: page.pageSize,
|
||||||
|
...formValues,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
rowConfig: {
|
||||||
|
keyField: 'id',
|
||||||
|
isHover: true,
|
||||||
|
},
|
||||||
|
toolbarConfig: {
|
||||||
|
refresh: true,
|
||||||
|
search: true,
|
||||||
|
},
|
||||||
|
} as VxeTableGridOptions<SystemMailLogApi.MailLog>,
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<Page auto-content-height>
|
||||||
|
<template #doc>
|
||||||
|
<DocAlert title="邮件配置" url="https://doc.iocoder.cn/mail" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<DetailModal @success="handleRefresh" />
|
||||||
|
<Grid table-title="邮件日志列表">
|
||||||
|
<template #userInfo="{ row }">
|
||||||
|
<div v-if="row.userType && row.userId" class="flex items-center gap-1">
|
||||||
|
<DictTag :type="DICT_TYPE.USER_TYPE" :value="row.userType" />
|
||||||
|
<span>({{ row.userId }})</span>
|
||||||
|
</div>
|
||||||
|
<div v-else>-</div>
|
||||||
|
</template>
|
||||||
|
<template #actions="{ row }">
|
||||||
|
<TableAction
|
||||||
|
:actions="[
|
||||||
|
{
|
||||||
|
label: $t('common.detail'),
|
||||||
|
type: 'primary',
|
||||||
|
icon: ACTION_ICON.VIEW,
|
||||||
|
auth: ['system:mail-log:query'],
|
||||||
|
onClick: handleDetail.bind(null, row),
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</Grid>
|
||||||
|
</Page>
|
||||||
|
</template>
|
||||||
53
apps/web-naive/src/views/system/mail/log/modules/detail.vue
Normal file
53
apps/web-naive/src/views/system/mail/log/modules/detail.vue
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { SystemMailLogApi } from '#/api/system/mail/log';
|
||||||
|
|
||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
import { useVbenModal } from '@vben/common-ui';
|
||||||
|
|
||||||
|
import { useDescription } from '#/components/description';
|
||||||
|
|
||||||
|
import { useDetailSchema } from '../data';
|
||||||
|
|
||||||
|
const formData = ref<SystemMailLogApi.MailLog>();
|
||||||
|
|
||||||
|
const [Descriptions] = useDescription({
|
||||||
|
componentProps: {
|
||||||
|
bordered: true,
|
||||||
|
column: 2,
|
||||||
|
contentClass: 'mx-4',
|
||||||
|
},
|
||||||
|
schema: useDetailSchema(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const [Modal, modalApi] = useVbenModal({
|
||||||
|
async onOpenChange(isOpen: boolean) {
|
||||||
|
if (!isOpen) {
|
||||||
|
formData.value = undefined;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 加载数据
|
||||||
|
const data = modalApi.getData<SystemMailLogApi.MailLog>();
|
||||||
|
if (!data || !data.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
modalApi.lock();
|
||||||
|
try {
|
||||||
|
formData.value = data;
|
||||||
|
} finally {
|
||||||
|
modalApi.unlock();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Modal
|
||||||
|
title="邮件日志详情"
|
||||||
|
class="w-[1280px]"
|
||||||
|
:show-cancel-button="false"
|
||||||
|
:show-confirm-button="false"
|
||||||
|
>
|
||||||
|
<Descriptions :data="formData" />
|
||||||
|
</Modal>
|
||||||
|
</template>
|
||||||
267
apps/web-naive/src/views/system/mail/template/data.ts
Normal file
267
apps/web-naive/src/views/system/mail/template/data.ts
Normal file
@@ -0,0 +1,267 @@
|
|||||||
|
import type { VbenFormSchema } from '#/adapter/form';
|
||||||
|
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||||
|
|
||||||
|
import { CommonStatusEnum, DICT_TYPE } from '@vben/constants';
|
||||||
|
import { getDictOptions } from '@vben/hooks';
|
||||||
|
|
||||||
|
import { z } from '#/adapter/form';
|
||||||
|
import { getSimpleMailAccountList } from '#/api/system/mail/account';
|
||||||
|
import { getRangePickerDefaultProps } from '#/utils';
|
||||||
|
|
||||||
|
/** 新增/修改的表单 */
|
||||||
|
export function useFormSchema(): VbenFormSchema[] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
fieldName: 'id',
|
||||||
|
component: 'Input',
|
||||||
|
dependencies: {
|
||||||
|
triggerFields: [''],
|
||||||
|
show: () => false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'name',
|
||||||
|
label: '模板名称',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入模板名称',
|
||||||
|
},
|
||||||
|
rules: 'required',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'code',
|
||||||
|
label: '模板编码',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入模板编码',
|
||||||
|
},
|
||||||
|
rules: 'required',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'accountId',
|
||||||
|
label: '邮箱账号',
|
||||||
|
component: 'ApiSelect',
|
||||||
|
componentProps: {
|
||||||
|
api: async () => await getSimpleMailAccountList(),
|
||||||
|
labelField: 'mail',
|
||||||
|
valueField: 'id',
|
||||||
|
placeholder: '请选择邮箱账号',
|
||||||
|
},
|
||||||
|
rules: 'required',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'nickname',
|
||||||
|
label: '发送人名称',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入发送人名称',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'title',
|
||||||
|
label: '模板标题',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入模板标题',
|
||||||
|
},
|
||||||
|
rules: 'required',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'content',
|
||||||
|
label: '模板内容',
|
||||||
|
component: 'RichTextarea',
|
||||||
|
rules: 'required',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'status',
|
||||||
|
label: '开启状态',
|
||||||
|
component: 'RadioGroup',
|
||||||
|
componentProps: {
|
||||||
|
options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
|
||||||
|
buttonStyle: 'solid',
|
||||||
|
optionType: 'button',
|
||||||
|
},
|
||||||
|
rules: z.number().default(CommonStatusEnum.ENABLE),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'remark',
|
||||||
|
label: '备注',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
type: 'textarea',
|
||||||
|
placeholder: '请输入备注',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 发送邮件表单 */
|
||||||
|
export function useSendMailFormSchema(): VbenFormSchema[] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
fieldName: 'templateParams',
|
||||||
|
label: '模板参数',
|
||||||
|
component: 'Input',
|
||||||
|
dependencies: {
|
||||||
|
triggerFields: [''],
|
||||||
|
show: () => false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'content',
|
||||||
|
label: '模板内容',
|
||||||
|
component: 'RichTextarea',
|
||||||
|
componentProps: {
|
||||||
|
options: {
|
||||||
|
readonly: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'toMails',
|
||||||
|
label: '收件邮箱',
|
||||||
|
component: 'Select',
|
||||||
|
componentProps: {
|
||||||
|
mode: 'tags',
|
||||||
|
clearable: true,
|
||||||
|
placeholder: '请输入收件邮箱,按 Enter 添加',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'ccMails',
|
||||||
|
label: '抄送邮箱',
|
||||||
|
component: 'Select',
|
||||||
|
componentProps: {
|
||||||
|
mode: 'tags',
|
||||||
|
clearable: true,
|
||||||
|
placeholder: '请输入抄送邮箱,按 Enter 添加',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'bccMails',
|
||||||
|
label: '密送邮箱',
|
||||||
|
component: 'Select',
|
||||||
|
componentProps: {
|
||||||
|
mode: 'tags',
|
||||||
|
clearable: true,
|
||||||
|
placeholder: '请输入密送邮箱,按 Enter 添加',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 列表的搜索表单 */
|
||||||
|
export function useGridFormSchema(): VbenFormSchema[] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
fieldName: 'status',
|
||||||
|
label: '开启状态',
|
||||||
|
component: 'Select',
|
||||||
|
componentProps: {
|
||||||
|
options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
|
||||||
|
clearable: true,
|
||||||
|
placeholder: '请选择开启状态',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'code',
|
||||||
|
label: '模板编码',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
clearable: true,
|
||||||
|
placeholder: '请输入模板编码',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'name',
|
||||||
|
label: '模板名称',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
clearable: true,
|
||||||
|
placeholder: '请输入模板名称',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'accountId',
|
||||||
|
label: '邮箱账号',
|
||||||
|
component: 'ApiSelect',
|
||||||
|
componentProps: {
|
||||||
|
api: async () => await getSimpleMailAccountList(),
|
||||||
|
labelField: 'mail',
|
||||||
|
valueField: 'id',
|
||||||
|
clearable: true,
|
||||||
|
placeholder: '请选择邮箱账号',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'createTime',
|
||||||
|
label: '创建时间',
|
||||||
|
component: 'DatePicker',
|
||||||
|
componentProps: {
|
||||||
|
...getRangePickerDefaultProps(),
|
||||||
|
clearable: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 列表的字段 */
|
||||||
|
export function useGridColumns(
|
||||||
|
getAccountMail?: (accountId: number) => string | undefined,
|
||||||
|
): VxeTableGridOptions['columns'] {
|
||||||
|
return [
|
||||||
|
{ type: 'checkbox', width: 40 },
|
||||||
|
{
|
||||||
|
field: 'id',
|
||||||
|
title: '编号',
|
||||||
|
minWidth: 100,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'code',
|
||||||
|
title: '模板编码',
|
||||||
|
minWidth: 120,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'name',
|
||||||
|
title: '模板名称',
|
||||||
|
minWidth: 120,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'title',
|
||||||
|
title: '模板标题',
|
||||||
|
minWidth: 120,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'accountId',
|
||||||
|
title: '邮箱账号',
|
||||||
|
minWidth: 120,
|
||||||
|
formatter: ({ cellValue }) => getAccountMail?.(cellValue) || '-',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'nickname',
|
||||||
|
title: '发送人名称',
|
||||||
|
minWidth: 120,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'status',
|
||||||
|
title: '开启状态',
|
||||||
|
minWidth: 100,
|
||||||
|
cellRender: {
|
||||||
|
name: 'CellDict',
|
||||||
|
props: { type: DICT_TYPE.COMMON_STATUS },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'createTime',
|
||||||
|
title: '创建时间',
|
||||||
|
minWidth: 180,
|
||||||
|
formatter: 'formatDateTime',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '操作',
|
||||||
|
width: 220,
|
||||||
|
fixed: 'right',
|
||||||
|
slots: { default: 'actions' },
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
205
apps/web-naive/src/views/system/mail/template/index.vue
Normal file
205
apps/web-naive/src/views/system/mail/template/index.vue
Normal file
@@ -0,0 +1,205 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
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 { confirm, DocAlert, Page, useVbenModal } from '@vben/common-ui';
|
||||||
|
import { isEmpty } from '@vben/utils';
|
||||||
|
|
||||||
|
import { message } from '#/adapter/naive';
|
||||||
|
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||||
|
import { getSimpleMailAccountList } from '#/api/system/mail/account';
|
||||||
|
import {
|
||||||
|
deleteMailTemplate,
|
||||||
|
deleteMailTemplateList,
|
||||||
|
getMailTemplatePage,
|
||||||
|
} from '#/api/system/mail/template';
|
||||||
|
import { $t } from '#/locales';
|
||||||
|
|
||||||
|
import { useGridColumns, useGridFormSchema } from './data';
|
||||||
|
import Form from './modules/form.vue';
|
||||||
|
import SendForm from './modules/send-form.vue';
|
||||||
|
|
||||||
|
const [FormModal, formModalApi] = useVbenModal({
|
||||||
|
connectedComponent: Form,
|
||||||
|
destroyOnClose: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const [SendModal, sendModalApi] = useVbenModal({
|
||||||
|
connectedComponent: SendForm,
|
||||||
|
destroyOnClose: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 获取邮箱账号 */
|
||||||
|
const accountList = ref<SystemMailAccountApi.MailAccount[]>([]);
|
||||||
|
function getAccountMail(accountId: number) {
|
||||||
|
return accountList.value.find((account) => account.id === accountId)?.mail;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 刷新表格 */
|
||||||
|
function handleRefresh() {
|
||||||
|
gridApi.query();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 创建邮件模板 */
|
||||||
|
function handleCreate() {
|
||||||
|
formModalApi.setData(null).open();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 编辑邮件模板 */
|
||||||
|
function handleEdit(row: SystemMailTemplateApi.MailTemplate) {
|
||||||
|
formModalApi.setData(row).open();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 发送测试邮件 */
|
||||||
|
function handleSend(row: SystemMailTemplateApi.MailTemplate) {
|
||||||
|
sendModalApi.setData(row).open();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 删除邮件模板 */
|
||||||
|
async function handleDelete(row: SystemMailTemplateApi.MailTemplate) {
|
||||||
|
const hideLoading = message.loading(
|
||||||
|
$t('ui.actionMessage.deleting', [row.name]),
|
||||||
|
{ duration: 0 },
|
||||||
|
);
|
||||||
|
try {
|
||||||
|
await deleteMailTemplate(row.id!);
|
||||||
|
message.success($t('ui.actionMessage.deleteSuccess', [row.name]));
|
||||||
|
handleRefresh();
|
||||||
|
} finally {
|
||||||
|
hideLoading.destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 批量删除邮件模板 */
|
||||||
|
async function handleDeleteBatch() {
|
||||||
|
await confirm($t('ui.actionMessage.deleteBatchConfirm'));
|
||||||
|
const hideLoading = message.loading($t('ui.actionMessage.deletingBatch'), {
|
||||||
|
duration: 0,
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
await deleteMailTemplateList(checkedIds.value);
|
||||||
|
checkedIds.value = [];
|
||||||
|
message.success($t('ui.actionMessage.deleteSuccess'), { duration: 0 });
|
||||||
|
handleRefresh();
|
||||||
|
} finally {
|
||||||
|
hideLoading.destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const checkedIds = ref<number[]>([]);
|
||||||
|
function handleRowCheckboxChange({
|
||||||
|
records,
|
||||||
|
}: {
|
||||||
|
records: SystemMailTemplateApi.MailTemplate[];
|
||||||
|
}) {
|
||||||
|
checkedIds.value = records.map((item) => item.id!);
|
||||||
|
}
|
||||||
|
|
||||||
|
const [Grid, gridApi] = useVbenVxeGrid({
|
||||||
|
formOptions: {
|
||||||
|
schema: useGridFormSchema(),
|
||||||
|
},
|
||||||
|
gridOptions: {
|
||||||
|
columns: useGridColumns(getAccountMail),
|
||||||
|
height: 'auto',
|
||||||
|
keepSource: true,
|
||||||
|
proxyConfig: {
|
||||||
|
ajax: {
|
||||||
|
query: async ({ page }, formValues) => {
|
||||||
|
return await getMailTemplatePage({
|
||||||
|
pageNo: page.currentPage,
|
||||||
|
pageSize: page.pageSize,
|
||||||
|
...formValues,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
rowConfig: {
|
||||||
|
keyField: 'id',
|
||||||
|
isHover: true,
|
||||||
|
},
|
||||||
|
toolbarConfig: {
|
||||||
|
refresh: true,
|
||||||
|
search: true,
|
||||||
|
},
|
||||||
|
} as VxeTableGridOptions<SystemMailTemplateApi.MailTemplate>,
|
||||||
|
gridEvents: {
|
||||||
|
checkboxAll: handleRowCheckboxChange,
|
||||||
|
checkboxChange: handleRowCheckboxChange,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 初始化 */
|
||||||
|
onMounted(async () => {
|
||||||
|
accountList.value = await getSimpleMailAccountList();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<Page auto-content-height>
|
||||||
|
<template #doc>
|
||||||
|
<DocAlert title="邮件配置" url="https://doc.iocoder.cn/mail" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<FormModal @success="handleRefresh" />
|
||||||
|
<SendModal />
|
||||||
|
<Grid table-title="邮件模板列表">
|
||||||
|
<template #toolbar-tools>
|
||||||
|
<TableAction
|
||||||
|
:actions="[
|
||||||
|
{
|
||||||
|
label: $t('ui.actionTitle.create', ['邮件模板']),
|
||||||
|
type: 'primary',
|
||||||
|
icon: ACTION_ICON.ADD,
|
||||||
|
auth: ['system:mail-template:create'],
|
||||||
|
onClick: handleCreate,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: $t('ui.actionTitle.deleteBatch'),
|
||||||
|
type: 'error',
|
||||||
|
icon: ACTION_ICON.DELETE,
|
||||||
|
auth: ['system:mail-template:delete'],
|
||||||
|
disabled: isEmpty(checkedIds),
|
||||||
|
onClick: handleDeleteBatch,
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<template #actions="{ row }">
|
||||||
|
<TableAction
|
||||||
|
:actions="[
|
||||||
|
{
|
||||||
|
label: $t('common.edit'),
|
||||||
|
type: 'primary',
|
||||||
|
text: true,
|
||||||
|
icon: ACTION_ICON.EDIT,
|
||||||
|
auth: ['system:mail-template:update'],
|
||||||
|
onClick: handleEdit.bind(null, row),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '测试',
|
||||||
|
type: 'primary',
|
||||||
|
text: true,
|
||||||
|
icon: ACTION_ICON.VIEW,
|
||||||
|
auth: ['system:mail-template:send-mail'],
|
||||||
|
onClick: handleSend.bind(null, row),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: $t('common.delete'),
|
||||||
|
type: 'error',
|
||||||
|
text: 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>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { SystemMailTemplateApi } from '#/api/system/mail/template';
|
||||||
|
|
||||||
|
import { computed, ref } from 'vue';
|
||||||
|
|
||||||
|
import { useVbenModal } from '@vben/common-ui';
|
||||||
|
|
||||||
|
import { useVbenForm } from '#/adapter/form';
|
||||||
|
import { message } from '#/adapter/naive';
|
||||||
|
import {
|
||||||
|
createMailTemplate,
|
||||||
|
getMailTemplate,
|
||||||
|
updateMailTemplate,
|
||||||
|
} from '#/api/system/mail/template';
|
||||||
|
import { $t } from '#/locales';
|
||||||
|
|
||||||
|
import { useFormSchema } from '../data';
|
||||||
|
|
||||||
|
const emit = defineEmits(['success']);
|
||||||
|
const formData = ref<SystemMailTemplateApi.MailTemplate>();
|
||||||
|
const getTitle = computed(() => {
|
||||||
|
return formData.value?.id
|
||||||
|
? $t('ui.actionTitle.edit', ['邮件模板'])
|
||||||
|
: $t('ui.actionTitle.create', ['邮件模板']);
|
||||||
|
});
|
||||||
|
|
||||||
|
const [Form, formApi] = useVbenForm({
|
||||||
|
commonConfig: {
|
||||||
|
componentProps: {
|
||||||
|
class: 'w-full',
|
||||||
|
},
|
||||||
|
formItemClass: 'col-span-2',
|
||||||
|
labelWidth: 80,
|
||||||
|
},
|
||||||
|
layout: 'horizontal',
|
||||||
|
schema: useFormSchema(),
|
||||||
|
showDefaultActions: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const [Modal, modalApi] = useVbenModal({
|
||||||
|
async onConfirm() {
|
||||||
|
const { valid } = await formApi.validate();
|
||||||
|
if (!valid) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
modalApi.lock();
|
||||||
|
// 提交表单
|
||||||
|
const data =
|
||||||
|
(await formApi.getValues()) as SystemMailTemplateApi.MailTemplate;
|
||||||
|
try {
|
||||||
|
await (formData.value?.id
|
||||||
|
? updateMailTemplate(data)
|
||||||
|
: createMailTemplate(data));
|
||||||
|
// 关闭并提示
|
||||||
|
await modalApi.close();
|
||||||
|
emit('success');
|
||||||
|
message.success($t('ui.actionMessage.operationSuccess'));
|
||||||
|
} finally {
|
||||||
|
modalApi.unlock();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async onOpenChange(isOpen: boolean) {
|
||||||
|
if (!isOpen) {
|
||||||
|
formData.value = undefined;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 加载数据
|
||||||
|
const data = modalApi.getData<SystemMailTemplateApi.MailTemplate>();
|
||||||
|
if (!data || !data.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
modalApi.lock();
|
||||||
|
try {
|
||||||
|
formData.value = await getMailTemplate(data.id);
|
||||||
|
// 设置到 values
|
||||||
|
await formApi.setValues(formData.value);
|
||||||
|
} finally {
|
||||||
|
modalApi.unlock();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Modal :title="getTitle" class="w-1/3">
|
||||||
|
<Form class="mx-4" />
|
||||||
|
</Modal>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { SystemMailTemplateApi } from '#/api/system/mail/template';
|
||||||
|
|
||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
import { useVbenModal } from '@vben/common-ui';
|
||||||
|
|
||||||
|
import { useVbenForm } from '#/adapter/form';
|
||||||
|
import { message } from '#/adapter/naive';
|
||||||
|
import { sendMail } from '#/api/system/mail/template';
|
||||||
|
|
||||||
|
import { useSendMailFormSchema } from '../data';
|
||||||
|
|
||||||
|
const emit = defineEmits(['success']);
|
||||||
|
const formData = ref<SystemMailTemplateApi.MailTemplate>();
|
||||||
|
|
||||||
|
const [Form, formApi] = useVbenForm({
|
||||||
|
commonConfig: {
|
||||||
|
componentProps: {
|
||||||
|
class: 'w-full',
|
||||||
|
},
|
||||||
|
formItemClass: 'col-span-2',
|
||||||
|
labelWidth: 80,
|
||||||
|
},
|
||||||
|
layout: 'horizontal',
|
||||||
|
showDefaultActions: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const [Modal, modalApi] = useVbenModal({
|
||||||
|
async onConfirm() {
|
||||||
|
const { valid } = await formApi.validate();
|
||||||
|
if (!valid) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
modalApi.lock();
|
||||||
|
// 构建发送请求
|
||||||
|
const values = await formApi.getValues();
|
||||||
|
const paramsObj: Record<string, string> = {};
|
||||||
|
if (formData.value?.params) {
|
||||||
|
formData.value.params.forEach((param: string) => {
|
||||||
|
paramsObj[param] = values[`param_${param}`];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const data: SystemMailTemplateApi.MailSendReqVO = {
|
||||||
|
toMails: values.toMails,
|
||||||
|
ccMails: values.ccMails,
|
||||||
|
bccMails: values.bccMails,
|
||||||
|
templateCode: formData.value?.code || '',
|
||||||
|
templateParams: paramsObj,
|
||||||
|
};
|
||||||
|
|
||||||
|
// 提交表单
|
||||||
|
try {
|
||||||
|
await sendMail(data);
|
||||||
|
// 关闭并提示
|
||||||
|
await modalApi.close();
|
||||||
|
emit('success');
|
||||||
|
message.success('邮件发送成功');
|
||||||
|
} finally {
|
||||||
|
modalApi.unlock();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async onOpenChange(isOpen: boolean) {
|
||||||
|
if (!isOpen) {
|
||||||
|
formData.value = undefined;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 获取数据
|
||||||
|
const data = modalApi.getData<SystemMailTemplateApi.MailTemplate>();
|
||||||
|
if (!data) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
formData.value = data;
|
||||||
|
// 更新 form schema
|
||||||
|
const schema = buildFormSchema();
|
||||||
|
formApi.setState({ schema });
|
||||||
|
// 设置到 values
|
||||||
|
await formApi.setValues({
|
||||||
|
content: data.content,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 动态构建表单 schema */
|
||||||
|
function buildFormSchema() {
|
||||||
|
const schema = useSendMailFormSchema();
|
||||||
|
if (formData.value?.params) {
|
||||||
|
formData.value.params?.forEach((param: string) => {
|
||||||
|
schema.push({
|
||||||
|
fieldName: `param_${param}`,
|
||||||
|
label: `参数 ${param}`,
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: `请输入参数 ${param}`,
|
||||||
|
},
|
||||||
|
rules: 'required',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return schema;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Modal title="测试发送邮件" class="w-1/3">
|
||||||
|
<Form class="mx-4" />
|
||||||
|
</Modal>
|
||||||
|
</template>
|
||||||
324
apps/web-naive/src/views/system/menu/data.ts
Normal file
324
apps/web-naive/src/views/system/menu/data.ts
Normal file
@@ -0,0 +1,324 @@
|
|||||||
|
import type { Recordable } from '@vben/types';
|
||||||
|
|
||||||
|
import type { VbenFormSchema } from '#/adapter/form';
|
||||||
|
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||||
|
import type { SystemMenuApi } from '#/api/system/menu';
|
||||||
|
|
||||||
|
import { h } from 'vue';
|
||||||
|
|
||||||
|
import {
|
||||||
|
CommonStatusEnum,
|
||||||
|
DICT_TYPE,
|
||||||
|
SystemMenuTypeEnum,
|
||||||
|
} from '@vben/constants';
|
||||||
|
import { getDictOptions } from '@vben/hooks';
|
||||||
|
import { IconifyIcon } from '@vben/icons';
|
||||||
|
import { handleTree, isHttpUrl } from '@vben/utils';
|
||||||
|
|
||||||
|
import { z } from '#/adapter/form';
|
||||||
|
import { getMenuList } from '#/api/system/menu';
|
||||||
|
import { $t } from '#/locales';
|
||||||
|
import { componentKeys } from '#/router/routes';
|
||||||
|
|
||||||
|
/** 新增/修改的表单 */
|
||||||
|
export function useFormSchema(): VbenFormSchema[] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
component: 'Input',
|
||||||
|
fieldName: 'id',
|
||||||
|
dependencies: {
|
||||||
|
triggerFields: [''],
|
||||||
|
show: () => false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'parentId',
|
||||||
|
label: '上级菜单',
|
||||||
|
component: 'ApiTreeSelect',
|
||||||
|
componentProps: {
|
||||||
|
clearable: true,
|
||||||
|
api: async () => {
|
||||||
|
const data = await getMenuList();
|
||||||
|
data.unshift({
|
||||||
|
id: 0,
|
||||||
|
name: '顶级部门',
|
||||||
|
} as SystemMenuApi.Menu);
|
||||||
|
return handleTree(data);
|
||||||
|
},
|
||||||
|
labelField: 'name',
|
||||||
|
valueField: 'id',
|
||||||
|
childrenField: 'children',
|
||||||
|
placeholder: '请选择上级菜单',
|
||||||
|
filterTreeNode(input: string, node: Recordable<any>) {
|
||||||
|
if (!input || input.length === 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const name: string = node.label ?? '';
|
||||||
|
if (!name) return false;
|
||||||
|
return name.includes(input) || $t(name).includes(input);
|
||||||
|
},
|
||||||
|
showSearch: true,
|
||||||
|
treeDefaultExpandedKeys: [0],
|
||||||
|
},
|
||||||
|
rules: 'selectRequired',
|
||||||
|
renderComponentContent() {
|
||||||
|
return {
|
||||||
|
title({ label, icon }: { icon: string; label: string }) {
|
||||||
|
const components = [];
|
||||||
|
if (!label) return '';
|
||||||
|
if (icon) {
|
||||||
|
components.push(h(IconifyIcon, { class: 'size-4', icon }));
|
||||||
|
}
|
||||||
|
components.push(h('span', { class: '' }, $t(label || '')));
|
||||||
|
return h('div', { class: 'flex items-center gap-1' }, components);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'name',
|
||||||
|
label: '菜单名称',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入菜单名称',
|
||||||
|
},
|
||||||
|
rules: 'required',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'type',
|
||||||
|
label: '菜单类型',
|
||||||
|
component: 'RadioGroup',
|
||||||
|
componentProps: {
|
||||||
|
options: getDictOptions(DICT_TYPE.SYSTEM_MENU_TYPE, 'number'),
|
||||||
|
buttonStyle: 'solid',
|
||||||
|
optionType: 'button',
|
||||||
|
},
|
||||||
|
rules: z.number().default(SystemMenuTypeEnum.DIR),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'icon',
|
||||||
|
label: '菜单图标',
|
||||||
|
component: 'IconPicker',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请选择菜单图标',
|
||||||
|
prefix: 'carbon',
|
||||||
|
},
|
||||||
|
rules: 'required',
|
||||||
|
dependencies: {
|
||||||
|
triggerFields: ['type'],
|
||||||
|
show: (values) => {
|
||||||
|
return [SystemMenuTypeEnum.DIR, SystemMenuTypeEnum.MENU].includes(
|
||||||
|
values.type,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'path',
|
||||||
|
label: '路由地址',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入路由地址',
|
||||||
|
},
|
||||||
|
rules: z.string(),
|
||||||
|
help: '访问的路由地址,如:`user`。如需外网地址时,则以 `http(s)://` 开头',
|
||||||
|
dependencies: {
|
||||||
|
triggerFields: ['type', 'parentId'],
|
||||||
|
show: (values) => {
|
||||||
|
return [SystemMenuTypeEnum.DIR, SystemMenuTypeEnum.MENU].includes(
|
||||||
|
values.type,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
rules: (values) => {
|
||||||
|
const schema = z.string().min(1, '路由地址不能为空');
|
||||||
|
if (isHttpUrl(values.path)) {
|
||||||
|
return schema;
|
||||||
|
}
|
||||||
|
if (values.parentId === 0) {
|
||||||
|
return schema.refine(
|
||||||
|
(path) => path.charAt(0) === '/',
|
||||||
|
'路径必须以 / 开头',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return schema.refine(
|
||||||
|
(path) => path.charAt(0) !== '/',
|
||||||
|
'路径不能以 / 开头',
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'component',
|
||||||
|
label: '组件地址',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入组件地址',
|
||||||
|
},
|
||||||
|
dependencies: {
|
||||||
|
triggerFields: ['type'],
|
||||||
|
show: (values) => {
|
||||||
|
return [SystemMenuTypeEnum.MENU].includes(values.type);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'componentName',
|
||||||
|
label: '组件名称',
|
||||||
|
component: 'AutoComplete',
|
||||||
|
componentProps: {
|
||||||
|
clearable: true,
|
||||||
|
filterOption(input: string, option: { value: string }) {
|
||||||
|
return option.value.toLowerCase().includes(input.toLowerCase());
|
||||||
|
},
|
||||||
|
placeholder: '请选择组件名称',
|
||||||
|
options: componentKeys.map((v) => ({ value: v })),
|
||||||
|
},
|
||||||
|
dependencies: {
|
||||||
|
triggerFields: ['type'],
|
||||||
|
show: (values) => {
|
||||||
|
return [SystemMenuTypeEnum.MENU].includes(values.type);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'permission',
|
||||||
|
label: '权限标识',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入菜单描述',
|
||||||
|
},
|
||||||
|
dependencies: {
|
||||||
|
show: (values) => {
|
||||||
|
return [SystemMenuTypeEnum.BUTTON, SystemMenuTypeEnum.MENU].includes(
|
||||||
|
values.type,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
triggerFields: ['type'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'sort',
|
||||||
|
label: '显示顺序',
|
||||||
|
component: 'InputNumber',
|
||||||
|
componentProps: {
|
||||||
|
min: 0,
|
||||||
|
placeholder: '请输入显示顺序',
|
||||||
|
},
|
||||||
|
rules: 'required',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'status',
|
||||||
|
label: '菜单状态',
|
||||||
|
component: 'RadioGroup',
|
||||||
|
componentProps: {
|
||||||
|
options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
|
||||||
|
buttonStyle: 'solid',
|
||||||
|
optionType: 'button',
|
||||||
|
},
|
||||||
|
rules: z.number().default(CommonStatusEnum.ENABLE),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'alwaysShow',
|
||||||
|
label: '总是显示',
|
||||||
|
component: 'RadioGroup',
|
||||||
|
componentProps: {
|
||||||
|
options: [
|
||||||
|
{ label: '总是', value: true },
|
||||||
|
{ label: '不是', value: false },
|
||||||
|
],
|
||||||
|
buttonStyle: 'solid',
|
||||||
|
optionType: 'button',
|
||||||
|
},
|
||||||
|
rules: 'required',
|
||||||
|
defaultValue: true,
|
||||||
|
help: '选择不是时,当该菜单只有一个子菜单时,不展示自己,直接展示子菜单',
|
||||||
|
dependencies: {
|
||||||
|
triggerFields: ['type'],
|
||||||
|
show: (values) => {
|
||||||
|
return [SystemMenuTypeEnum.MENU].includes(values.type);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'keepAlive',
|
||||||
|
label: '缓存状态',
|
||||||
|
component: 'RadioGroup',
|
||||||
|
componentProps: {
|
||||||
|
options: [
|
||||||
|
{ label: '缓存', value: true },
|
||||||
|
{ label: '不缓存', value: false },
|
||||||
|
],
|
||||||
|
buttonStyle: 'solid',
|
||||||
|
optionType: 'button',
|
||||||
|
},
|
||||||
|
rules: 'required',
|
||||||
|
defaultValue: true,
|
||||||
|
help: '选择缓存时,则会被 `keep-alive` 缓存,必须填写「组件名称」字段',
|
||||||
|
dependencies: {
|
||||||
|
triggerFields: ['type'],
|
||||||
|
show: (values) => {
|
||||||
|
return [SystemMenuTypeEnum.MENU].includes(values.type);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 列表的字段 */
|
||||||
|
export function useGridColumns(): VxeTableGridOptions<SystemMenuApi.Menu>['columns'] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
field: 'name',
|
||||||
|
title: '菜单名称',
|
||||||
|
minWidth: 250,
|
||||||
|
align: 'left',
|
||||||
|
fixed: 'left',
|
||||||
|
slots: { default: 'name' },
|
||||||
|
treeNode: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'type',
|
||||||
|
title: '菜单类型',
|
||||||
|
minWidth: 100,
|
||||||
|
cellRender: {
|
||||||
|
name: 'CellDict',
|
||||||
|
props: { type: DICT_TYPE.SYSTEM_MENU_TYPE },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'sort',
|
||||||
|
title: '显示排序',
|
||||||
|
minWidth: 100,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'permission',
|
||||||
|
title: '权限标识',
|
||||||
|
minWidth: 200,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'path',
|
||||||
|
title: '组件路径',
|
||||||
|
minWidth: 200,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'componentName',
|
||||||
|
title: '组件名称',
|
||||||
|
minWidth: 200,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'status',
|
||||||
|
title: '状态',
|
||||||
|
minWidth: 100,
|
||||||
|
cellRender: {
|
||||||
|
name: 'CellDict',
|
||||||
|
props: { type: DICT_TYPE.COMMON_STATUS },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '操作',
|
||||||
|
width: 220,
|
||||||
|
fixed: 'right',
|
||||||
|
slots: { default: 'actions' },
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
181
apps/web-naive/src/views/system/menu/index.vue
Normal file
181
apps/web-naive/src/views/system/menu/index.vue
Normal file
@@ -0,0 +1,181 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||||
|
import type { SystemMenuApi } from '#/api/system/menu';
|
||||||
|
|
||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
import { DocAlert, Page, useVbenModal } from '@vben/common-ui';
|
||||||
|
import { SystemMenuTypeEnum } from '@vben/constants';
|
||||||
|
import { IconifyIcon } from '@vben/icons';
|
||||||
|
|
||||||
|
import { message } from '#/adapter/naive';
|
||||||
|
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||||
|
import { deleteMenu, getMenuList } from '#/api/system/menu';
|
||||||
|
import { $t } from '#/locales';
|
||||||
|
|
||||||
|
import { useGridColumns } from './data';
|
||||||
|
import Form from './modules/form.vue';
|
||||||
|
|
||||||
|
const [FormModal, formModalApi] = useVbenModal({
|
||||||
|
connectedComponent: Form,
|
||||||
|
destroyOnClose: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 刷新表格 */
|
||||||
|
function handleRefresh() {
|
||||||
|
gridApi.query();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 创建菜单 */
|
||||||
|
function handleCreate() {
|
||||||
|
formModalApi.setData(null).open();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 添加下级菜单 */
|
||||||
|
function handleAppend(row: SystemMenuApi.Menu) {
|
||||||
|
formModalApi.setData({ parentId: row.id }).open();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 编辑菜单 */
|
||||||
|
function handleEdit(row: SystemMenuApi.Menu) {
|
||||||
|
formModalApi.setData(row).open();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 删除菜单 */
|
||||||
|
async function handleDelete(row: SystemMenuApi.Menu) {
|
||||||
|
const hideLoading = message.loading(
|
||||||
|
$t('ui.actionMessage.deleting', [row.name]),
|
||||||
|
{ duration: 0 },
|
||||||
|
);
|
||||||
|
try {
|
||||||
|
await deleteMenu(row.id!);
|
||||||
|
message.success($t('ui.actionMessage.deleteSuccess', [row.name]));
|
||||||
|
handleRefresh();
|
||||||
|
} finally {
|
||||||
|
hideLoading.destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 切换树形展开/收缩状态 */
|
||||||
|
const isExpanded = ref(false);
|
||||||
|
function handleExpand() {
|
||||||
|
isExpanded.value = !isExpanded.value;
|
||||||
|
gridApi.grid.setAllTreeExpand(isExpanded.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
const [Grid, gridApi] = useVbenVxeGrid({
|
||||||
|
gridOptions: {
|
||||||
|
columns: useGridColumns(),
|
||||||
|
height: 'auto',
|
||||||
|
keepSource: true,
|
||||||
|
pagerConfig: {
|
||||||
|
enabled: false,
|
||||||
|
},
|
||||||
|
proxyConfig: {
|
||||||
|
ajax: {
|
||||||
|
query: async (_params) => {
|
||||||
|
return await getMenuList();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
rowConfig: {
|
||||||
|
keyField: 'id',
|
||||||
|
isHover: true,
|
||||||
|
},
|
||||||
|
toolbarConfig: {
|
||||||
|
refresh: true,
|
||||||
|
},
|
||||||
|
treeConfig: {
|
||||||
|
parentField: 'parentId',
|
||||||
|
rowField: 'id',
|
||||||
|
transform: true,
|
||||||
|
reserve: true,
|
||||||
|
},
|
||||||
|
} as VxeTableGridOptions<SystemMenuApi.Menu>,
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Page auto-content-height>
|
||||||
|
<template #doc>
|
||||||
|
<DocAlert
|
||||||
|
title="功能权限"
|
||||||
|
url="https://doc.iocoder.cn/resource-permission"
|
||||||
|
/>
|
||||||
|
<DocAlert title="菜单路由" url="https://doc.iocoder.cn/vue3/route/" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<FormModal @success="handleRefresh" />
|
||||||
|
<Grid table-title="菜单列表">
|
||||||
|
<template #toolbar-tools>
|
||||||
|
<TableAction
|
||||||
|
:actions="[
|
||||||
|
{
|
||||||
|
label: $t('ui.actionTitle.create', ['菜单']),
|
||||||
|
type: 'primary',
|
||||||
|
icon: ACTION_ICON.ADD,
|
||||||
|
auth: ['system:menu:create'],
|
||||||
|
onClick: handleCreate,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: isExpanded ? '收缩' : '展开',
|
||||||
|
type: 'primary',
|
||||||
|
onClick: handleExpand,
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<template #name="{ row }">
|
||||||
|
<div class="flex w-full items-center gap-1">
|
||||||
|
<div class="size-5 flex-shrink-0">
|
||||||
|
<IconifyIcon
|
||||||
|
v-if="row.type === SystemMenuTypeEnum.BUTTON"
|
||||||
|
icon="carbon:square-outline"
|
||||||
|
class="size-full"
|
||||||
|
/>
|
||||||
|
<IconifyIcon
|
||||||
|
v-else-if="row.icon"
|
||||||
|
:icon="row.icon || 'carbon:circle-dash'"
|
||||||
|
class="size-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<span class="flex-auto">{{ $t(row.name) }}</span>
|
||||||
|
<div class="items-center justify-end"></div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #actions="{ row }">
|
||||||
|
<TableAction
|
||||||
|
:actions="[
|
||||||
|
{
|
||||||
|
label: '新增下级',
|
||||||
|
type: 'primary',
|
||||||
|
text: true,
|
||||||
|
icon: ACTION_ICON.ADD,
|
||||||
|
auth: ['system:menu:create'],
|
||||||
|
onClick: handleAppend.bind(null, row),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: $t('common.edit'),
|
||||||
|
type: 'primary',
|
||||||
|
text: true,
|
||||||
|
icon: ACTION_ICON.EDIT,
|
||||||
|
auth: ['system:menu:update'],
|
||||||
|
onClick: handleEdit.bind(null, row),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: $t('common.delete'),
|
||||||
|
type: 'error',
|
||||||
|
text: 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>
|
||||||
83
apps/web-naive/src/views/system/menu/modules/form.vue
Normal file
83
apps/web-naive/src/views/system/menu/modules/form.vue
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { SystemMenuApi } from '#/api/system/menu';
|
||||||
|
|
||||||
|
import { computed, ref } from 'vue';
|
||||||
|
|
||||||
|
import { useVbenModal } from '@vben/common-ui';
|
||||||
|
|
||||||
|
import { useVbenForm } from '#/adapter/form';
|
||||||
|
import { message } from '#/adapter/naive';
|
||||||
|
import { createMenu, getMenu, updateMenu } from '#/api/system/menu';
|
||||||
|
import { $t } from '#/locales';
|
||||||
|
|
||||||
|
import { useFormSchema } from '../data';
|
||||||
|
|
||||||
|
const emit = defineEmits(['success']);
|
||||||
|
const formData = ref<SystemMenuApi.Menu>();
|
||||||
|
const getTitle = computed(() =>
|
||||||
|
formData.value?.id
|
||||||
|
? $t('ui.actionTitle.edit', ['菜单'])
|
||||||
|
: $t('ui.actionTitle.create', ['菜单']),
|
||||||
|
);
|
||||||
|
|
||||||
|
const [Form, formApi] = useVbenForm({
|
||||||
|
commonConfig: {
|
||||||
|
componentProps: {
|
||||||
|
class: 'w-full',
|
||||||
|
},
|
||||||
|
formItemClass: 'col-span-2',
|
||||||
|
labelWidth: 100,
|
||||||
|
},
|
||||||
|
layout: 'horizontal',
|
||||||
|
schema: useFormSchema(),
|
||||||
|
showDefaultActions: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const [Modal, modalApi] = useVbenModal({
|
||||||
|
async onConfirm() {
|
||||||
|
const { valid } = await formApi.validate();
|
||||||
|
if (!valid) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
modalApi.lock();
|
||||||
|
// 提交表单
|
||||||
|
const data = (await formApi.getValues()) as SystemMenuApi.Menu;
|
||||||
|
try {
|
||||||
|
await (formData.value?.id ? updateMenu(data) : createMenu(data));
|
||||||
|
// 关闭并提示
|
||||||
|
await modalApi.close();
|
||||||
|
emit('success');
|
||||||
|
message.success($t('ui.actionMessage.operationSuccess'));
|
||||||
|
} finally {
|
||||||
|
modalApi.unlock();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async onOpenChange(isOpen: boolean) {
|
||||||
|
if (!isOpen) {
|
||||||
|
formData.value = undefined;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 加载数据
|
||||||
|
const data = modalApi.getData<SystemMenuApi.Menu>();
|
||||||
|
if (!data || !data.id) {
|
||||||
|
// 设置上级
|
||||||
|
await formApi.setValues(data);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
modalApi.lock();
|
||||||
|
try {
|
||||||
|
formData.value = await getMenu(data.id);
|
||||||
|
// 设置到 values
|
||||||
|
await formApi.setValues(formData.value);
|
||||||
|
} finally {
|
||||||
|
modalApi.unlock();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Modal class="w-2/5" :title="getTitle">
|
||||||
|
<Form class="mx-4" />
|
||||||
|
</Modal>
|
||||||
|
</template>
|
||||||
136
apps/web-naive/src/views/system/notice/data.ts
Normal file
136
apps/web-naive/src/views/system/notice/data.ts
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
import type { VbenFormSchema } from '#/adapter/form';
|
||||||
|
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||||
|
|
||||||
|
import { CommonStatusEnum, DICT_TYPE } from '@vben/constants';
|
||||||
|
import { getDictOptions } from '@vben/hooks';
|
||||||
|
|
||||||
|
import { z } from '#/adapter/form';
|
||||||
|
|
||||||
|
/** 新增/修改的表单 */
|
||||||
|
export function useFormSchema(): VbenFormSchema[] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
fieldName: 'id',
|
||||||
|
component: 'Input',
|
||||||
|
dependencies: {
|
||||||
|
triggerFields: [''],
|
||||||
|
show: () => false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'title',
|
||||||
|
label: '公告标题',
|
||||||
|
component: 'Input',
|
||||||
|
rules: 'required',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'type',
|
||||||
|
label: '公告类型',
|
||||||
|
component: 'RadioGroup',
|
||||||
|
componentProps: {
|
||||||
|
options: getDictOptions(DICT_TYPE.SYSTEM_NOTICE_TYPE, 'number'),
|
||||||
|
buttonStyle: 'solid',
|
||||||
|
optionType: 'button',
|
||||||
|
},
|
||||||
|
rules: 'required',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'content',
|
||||||
|
label: '公告内容',
|
||||||
|
component: 'RichTextarea',
|
||||||
|
rules: 'required',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'status',
|
||||||
|
label: '公告状态',
|
||||||
|
component: 'RadioGroup',
|
||||||
|
componentProps: {
|
||||||
|
options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
|
||||||
|
buttonStyle: 'solid',
|
||||||
|
optionType: 'button',
|
||||||
|
},
|
||||||
|
rules: z.number().default(CommonStatusEnum.ENABLE),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'remark',
|
||||||
|
label: '备注',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
type: 'textarea',
|
||||||
|
placeholder: '请输入备注',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 列表的搜索表单 */
|
||||||
|
export function useGridFormSchema(): VbenFormSchema[] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
fieldName: 'title',
|
||||||
|
label: '公告标题',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入公告标题',
|
||||||
|
clearable: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'status',
|
||||||
|
label: '公告状态',
|
||||||
|
component: 'Select',
|
||||||
|
componentProps: {
|
||||||
|
options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
|
||||||
|
placeholder: '请选择公告状态',
|
||||||
|
clearable: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 列表的字段 */
|
||||||
|
export function useGridColumns(): VxeTableGridOptions['columns'] {
|
||||||
|
return [
|
||||||
|
{ type: 'checkbox', width: 40 },
|
||||||
|
{
|
||||||
|
field: 'id',
|
||||||
|
title: '公告编号',
|
||||||
|
minWidth: 100,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'title',
|
||||||
|
title: '公告标题',
|
||||||
|
minWidth: 200,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'type',
|
||||||
|
title: '公告类型',
|
||||||
|
minWidth: 100,
|
||||||
|
cellRender: {
|
||||||
|
name: 'CellDict',
|
||||||
|
props: { type: DICT_TYPE.SYSTEM_NOTICE_TYPE },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'status',
|
||||||
|
title: '公告状态',
|
||||||
|
minWidth: 100,
|
||||||
|
cellRender: {
|
||||||
|
name: 'CellDict',
|
||||||
|
props: { type: DICT_TYPE.COMMON_STATUS },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'createTime',
|
||||||
|
title: '创建时间',
|
||||||
|
minWidth: 180,
|
||||||
|
formatter: 'formatDateTime',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '操作',
|
||||||
|
width: 220,
|
||||||
|
fixed: 'right',
|
||||||
|
slots: { default: 'actions' },
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
189
apps/web-naive/src/views/system/notice/index.vue
Normal file
189
apps/web-naive/src/views/system/notice/index.vue
Normal file
@@ -0,0 +1,189 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||||
|
import type { SystemNoticeApi } from '#/api/system/notice';
|
||||||
|
|
||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
import { confirm, Page, useVbenModal } from '@vben/common-ui';
|
||||||
|
import { isEmpty } from '@vben/utils';
|
||||||
|
|
||||||
|
import { message } from '#/adapter/naive';
|
||||||
|
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||||
|
import {
|
||||||
|
deleteNotice,
|
||||||
|
deleteNoticeList,
|
||||||
|
getNoticePage,
|
||||||
|
pushNotice,
|
||||||
|
} from '#/api/system/notice';
|
||||||
|
import { $t } from '#/locales';
|
||||||
|
|
||||||
|
import { useGridColumns, useGridFormSchema } from './data';
|
||||||
|
import Form from './modules/form.vue';
|
||||||
|
|
||||||
|
const [FormModal, formModalApi] = useVbenModal({
|
||||||
|
connectedComponent: Form,
|
||||||
|
destroyOnClose: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 刷新表格 */
|
||||||
|
function handleRefresh() {
|
||||||
|
gridApi.query();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 创建公告 */
|
||||||
|
function handleCreate() {
|
||||||
|
formModalApi.setData(null).open();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 编辑公告 */
|
||||||
|
function handleEdit(row: SystemNoticeApi.Notice) {
|
||||||
|
formModalApi.setData(row).open();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 删除公告 */
|
||||||
|
async function handleDelete(row: SystemNoticeApi.Notice) {
|
||||||
|
const hideLoading = message.loading(
|
||||||
|
$t('ui.actionMessage.deleting', [row.title]),
|
||||||
|
{ duration: 0 },
|
||||||
|
);
|
||||||
|
try {
|
||||||
|
await deleteNotice(row.id!);
|
||||||
|
message.success($t('ui.actionMessage.deleteSuccess', [row.title]));
|
||||||
|
handleRefresh();
|
||||||
|
} finally {
|
||||||
|
hideLoading.destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 批量删除公告 */
|
||||||
|
async function handleDeleteBatch() {
|
||||||
|
await confirm($t('ui.actionMessage.deleteBatchConfirm'));
|
||||||
|
const hideLoading = message.loading($t('ui.actionMessage.deletingBatch'), {
|
||||||
|
duration: 0,
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
await deleteNoticeList(checkedIds.value);
|
||||||
|
checkedIds.value = [];
|
||||||
|
message.success($t('ui.actionMessage.deleteSuccess'));
|
||||||
|
handleRefresh();
|
||||||
|
} finally {
|
||||||
|
hideLoading.destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const checkedIds = ref<number[]>([]);
|
||||||
|
function handleRowCheckboxChange({
|
||||||
|
records,
|
||||||
|
}: {
|
||||||
|
records: SystemNoticeApi.Notice[];
|
||||||
|
}) {
|
||||||
|
checkedIds.value = records.map((item) => item.id!);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 推送公告 */
|
||||||
|
async function handlePush(row: SystemNoticeApi.Notice) {
|
||||||
|
const hideLoading = message.loading('正在推送中...');
|
||||||
|
try {
|
||||||
|
await pushNotice(row.id!);
|
||||||
|
message.success($t('ui.actionMessage.operationSuccess'));
|
||||||
|
} finally {
|
||||||
|
hideLoading.destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const [Grid, gridApi] = useVbenVxeGrid({
|
||||||
|
formOptions: {
|
||||||
|
schema: useGridFormSchema(),
|
||||||
|
},
|
||||||
|
gridOptions: {
|
||||||
|
columns: useGridColumns(),
|
||||||
|
height: 'auto',
|
||||||
|
keepSource: true,
|
||||||
|
proxyConfig: {
|
||||||
|
ajax: {
|
||||||
|
query: async ({ page }, formValues) => {
|
||||||
|
return await getNoticePage({
|
||||||
|
pageNo: page.currentPage,
|
||||||
|
pageSize: page.pageSize,
|
||||||
|
...formValues,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
rowConfig: {
|
||||||
|
keyField: 'id',
|
||||||
|
isHover: true,
|
||||||
|
},
|
||||||
|
toolbarConfig: {
|
||||||
|
refresh: true,
|
||||||
|
search: true,
|
||||||
|
},
|
||||||
|
} as VxeTableGridOptions<SystemNoticeApi.Notice>,
|
||||||
|
gridEvents: {
|
||||||
|
checkboxAll: handleRowCheckboxChange,
|
||||||
|
checkboxChange: handleRowCheckboxChange,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Page auto-content-height>
|
||||||
|
<FormModal @success="handleRefresh" />
|
||||||
|
<Grid table-title="公告列表">
|
||||||
|
<template #toolbar-tools>
|
||||||
|
<TableAction
|
||||||
|
:actions="[
|
||||||
|
{
|
||||||
|
label: $t('ui.actionTitle.create', ['公告']),
|
||||||
|
type: 'primary',
|
||||||
|
icon: ACTION_ICON.ADD,
|
||||||
|
auth: ['system:notice:create'],
|
||||||
|
onClick: handleCreate,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: $t('ui.actionTitle.deleteBatch'),
|
||||||
|
type: 'error',
|
||||||
|
icon: ACTION_ICON.DELETE,
|
||||||
|
auth: ['system:notice:delete'],
|
||||||
|
disabled: isEmpty(checkedIds),
|
||||||
|
onClick: handleDeleteBatch,
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<template #actions="{ row }">
|
||||||
|
<TableAction
|
||||||
|
:actions="[
|
||||||
|
{
|
||||||
|
label: $t('common.edit'),
|
||||||
|
type: 'primary',
|
||||||
|
text: true,
|
||||||
|
icon: ACTION_ICON.EDIT,
|
||||||
|
auth: ['system:notice:update'],
|
||||||
|
onClick: handleEdit.bind(null, row),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '推送',
|
||||||
|
type: 'primary',
|
||||||
|
text: true,
|
||||||
|
icon: ACTION_ICON.ADD,
|
||||||
|
auth: ['system:notice:update'],
|
||||||
|
onClick: handlePush.bind(null, row),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: $t('common.delete'),
|
||||||
|
type: 'error',
|
||||||
|
text: true,
|
||||||
|
icon: ACTION_ICON.DELETE,
|
||||||
|
auth: ['system:notice:delete'],
|
||||||
|
popConfirm: {
|
||||||
|
title: $t('ui.actionMessage.deleteConfirm', [row.title]),
|
||||||
|
confirm: handleDelete.bind(null, row),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</Grid>
|
||||||
|
</Page>
|
||||||
|
</template>
|
||||||
81
apps/web-naive/src/views/system/notice/modules/form.vue
Normal file
81
apps/web-naive/src/views/system/notice/modules/form.vue
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { SystemNoticeApi } from '#/api/system/notice';
|
||||||
|
|
||||||
|
import { computed, ref } from 'vue';
|
||||||
|
|
||||||
|
import { useVbenModal } from '@vben/common-ui';
|
||||||
|
|
||||||
|
import { useVbenForm } from '#/adapter/form';
|
||||||
|
import { message } from '#/adapter/naive';
|
||||||
|
import { createNotice, getNotice, updateNotice } from '#/api/system/notice';
|
||||||
|
import { $t } from '#/locales';
|
||||||
|
|
||||||
|
import { useFormSchema } from '../data';
|
||||||
|
|
||||||
|
const emit = defineEmits(['success']);
|
||||||
|
const formData = ref<SystemNoticeApi.Notice>();
|
||||||
|
const getTitle = computed(() => {
|
||||||
|
return formData.value?.id
|
||||||
|
? $t('ui.actionTitle.edit', ['公告'])
|
||||||
|
: $t('ui.actionTitle.create', ['公告']);
|
||||||
|
});
|
||||||
|
|
||||||
|
const [Form, formApi] = useVbenForm({
|
||||||
|
commonConfig: {
|
||||||
|
componentProps: {
|
||||||
|
class: 'w-full',
|
||||||
|
},
|
||||||
|
formItemClass: 'col-span-2',
|
||||||
|
labelWidth: 80,
|
||||||
|
},
|
||||||
|
layout: 'horizontal',
|
||||||
|
schema: useFormSchema(),
|
||||||
|
showDefaultActions: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const [Modal, modalApi] = useVbenModal({
|
||||||
|
async onConfirm() {
|
||||||
|
const { valid } = await formApi.validate();
|
||||||
|
if (!valid) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
modalApi.lock();
|
||||||
|
// 提交表单
|
||||||
|
const data = (await formApi.getValues()) as SystemNoticeApi.Notice;
|
||||||
|
try {
|
||||||
|
await (formData.value?.id ? updateNotice(data) : createNotice(data));
|
||||||
|
// 关闭并提示
|
||||||
|
await modalApi.close();
|
||||||
|
emit('success');
|
||||||
|
message.success($t('ui.actionMessage.operationSuccess'));
|
||||||
|
} finally {
|
||||||
|
modalApi.unlock();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async onOpenChange(isOpen: boolean) {
|
||||||
|
if (!isOpen) {
|
||||||
|
formData.value = undefined;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 加载数据
|
||||||
|
const data = modalApi.getData<SystemNoticeApi.Notice>();
|
||||||
|
if (!data || !data.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
modalApi.lock();
|
||||||
|
try {
|
||||||
|
formData.value = await getNotice(data.id);
|
||||||
|
// 设置到 values
|
||||||
|
await formApi.setValues(formData.value);
|
||||||
|
} finally {
|
||||||
|
modalApi.unlock();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Modal :title="getTitle" class="w-1/2">
|
||||||
|
<Form class="mx-4" />
|
||||||
|
</Modal>
|
||||||
|
</template>
|
||||||
242
apps/web-naive/src/views/system/notify/message/data.ts
Normal file
242
apps/web-naive/src/views/system/notify/message/data.ts
Normal file
@@ -0,0 +1,242 @@
|
|||||||
|
import type { VbenFormSchema } from '#/adapter/form';
|
||||||
|
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||||
|
import type { SystemNotifyMessageApi } from '#/api/system/notify/message';
|
||||||
|
import type { DescriptionItemSchema } from '#/components/description';
|
||||||
|
|
||||||
|
import { h } from 'vue';
|
||||||
|
|
||||||
|
import { DICT_TYPE } from '@vben/constants';
|
||||||
|
import { getDictOptions } from '@vben/hooks';
|
||||||
|
import { formatDateTime } from '@vben/utils';
|
||||||
|
|
||||||
|
import { DictTag } from '#/components/dict-tag';
|
||||||
|
import { getRangePickerDefaultProps } from '#/utils';
|
||||||
|
|
||||||
|
/** 列表的搜索表单 */
|
||||||
|
export function useGridFormSchema(): VbenFormSchema[] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
fieldName: 'userId',
|
||||||
|
label: '用户编号',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
clearable: true,
|
||||||
|
placeholder: '请输入用户编号',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'userType',
|
||||||
|
label: '用户类型',
|
||||||
|
component: 'Select',
|
||||||
|
componentProps: {
|
||||||
|
clearable: true,
|
||||||
|
options: getDictOptions(DICT_TYPE.USER_TYPE, 'number'),
|
||||||
|
placeholder: '请选择用户类型',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'templateCode',
|
||||||
|
label: '模板编码',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
clearable: true,
|
||||||
|
placeholder: '请输入模板编码',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'templateType',
|
||||||
|
label: '模版类型',
|
||||||
|
component: 'Select',
|
||||||
|
componentProps: {
|
||||||
|
options: getDictOptions(
|
||||||
|
DICT_TYPE.SYSTEM_NOTIFY_TEMPLATE_TYPE,
|
||||||
|
'number',
|
||||||
|
),
|
||||||
|
clearable: true,
|
||||||
|
placeholder: '请选择模版类型',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'createTime',
|
||||||
|
label: '创建时间',
|
||||||
|
component: 'DatePicker',
|
||||||
|
componentProps: {
|
||||||
|
...getRangePickerDefaultProps(),
|
||||||
|
clearable: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 列表的字段 */
|
||||||
|
export function useGridColumns(): VxeTableGridOptions['columns'] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
field: 'id',
|
||||||
|
title: '编号',
|
||||||
|
minWidth: 100,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'userType',
|
||||||
|
title: '用户类型',
|
||||||
|
minWidth: 120,
|
||||||
|
cellRender: {
|
||||||
|
name: 'CellDict',
|
||||||
|
props: { type: DICT_TYPE.USER_TYPE },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'userId',
|
||||||
|
title: '用户编号',
|
||||||
|
minWidth: 100,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'templateCode',
|
||||||
|
title: '模板编码',
|
||||||
|
minWidth: 120,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'templateNickname',
|
||||||
|
title: '发送人名称',
|
||||||
|
minWidth: 180,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'templateContent',
|
||||||
|
title: '模版内容',
|
||||||
|
minWidth: 200,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'templateParams',
|
||||||
|
title: '模版参数',
|
||||||
|
minWidth: 180,
|
||||||
|
formatter: ({ cellValue }) => {
|
||||||
|
try {
|
||||||
|
return JSON.stringify(cellValue);
|
||||||
|
} catch {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'templateType',
|
||||||
|
title: '模版类型',
|
||||||
|
minWidth: 120,
|
||||||
|
cellRender: {
|
||||||
|
name: 'CellDict',
|
||||||
|
props: { type: DICT_TYPE.SYSTEM_NOTIFY_TEMPLATE_TYPE },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'readStatus',
|
||||||
|
title: '是否已读',
|
||||||
|
minWidth: 100,
|
||||||
|
cellRender: {
|
||||||
|
name: 'CellDict',
|
||||||
|
props: { type: DICT_TYPE.INFRA_BOOLEAN_STRING },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'readTime',
|
||||||
|
title: '阅读时间',
|
||||||
|
minWidth: 180,
|
||||||
|
formatter: 'formatDateTime',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'createTime',
|
||||||
|
title: '创建时间',
|
||||||
|
minWidth: 180,
|
||||||
|
formatter: 'formatDateTime',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '操作',
|
||||||
|
width: 80,
|
||||||
|
fixed: 'right',
|
||||||
|
slots: { default: 'actions' },
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 详情页的字段 */
|
||||||
|
export function useDetailSchema(): DescriptionItemSchema[] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
field: 'id',
|
||||||
|
label: '编号',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'userType',
|
||||||
|
label: '用户类型',
|
||||||
|
content: (data: SystemNotifyMessageApi.NotifyMessage) => {
|
||||||
|
return h(DictTag, {
|
||||||
|
type: DICT_TYPE.USER_TYPE,
|
||||||
|
value: data?.userType,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'userId',
|
||||||
|
label: '用户编号',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'templateId',
|
||||||
|
label: '模版编号',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'templateCode',
|
||||||
|
label: '模板编码',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'templateNickname',
|
||||||
|
label: '发送人名称',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'templateContent',
|
||||||
|
label: '模版内容',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'templateParams',
|
||||||
|
label: '模版参数',
|
||||||
|
content: (data: SystemNotifyMessageApi.NotifyMessage) => {
|
||||||
|
try {
|
||||||
|
return JSON.stringify(data?.templateParams);
|
||||||
|
} catch {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'templateType',
|
||||||
|
label: '模版类型',
|
||||||
|
content: (data: SystemNotifyMessageApi.NotifyMessage) => {
|
||||||
|
return h(DictTag, {
|
||||||
|
type: DICT_TYPE.SYSTEM_NOTIFY_TEMPLATE_TYPE,
|
||||||
|
value: data?.templateType,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'readStatus',
|
||||||
|
label: '是否已读',
|
||||||
|
content: (data: SystemNotifyMessageApi.NotifyMessage) => {
|
||||||
|
return h(DictTag, {
|
||||||
|
type: DICT_TYPE.INFRA_BOOLEAN_STRING,
|
||||||
|
value: data?.readStatus,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'readTime',
|
||||||
|
label: '阅读时间',
|
||||||
|
content: (data: SystemNotifyMessageApi.NotifyMessage) => {
|
||||||
|
return formatDateTime(data?.readTime || '') as string;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'createTime',
|
||||||
|
label: '创建时间',
|
||||||
|
content: (data: SystemNotifyMessageApi.NotifyMessage) => {
|
||||||
|
return formatDateTime(data?.createTime || '') as string;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
84
apps/web-naive/src/views/system/notify/message/index.vue
Normal file
84
apps/web-naive/src/views/system/notify/message/index.vue
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||||
|
import type { SystemNotifyMessageApi } from '#/api/system/notify/message';
|
||||||
|
|
||||||
|
import { DocAlert, Page, useVbenModal } from '@vben/common-ui';
|
||||||
|
|
||||||
|
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||||
|
import { getNotifyMessagePage } from '#/api/system/notify/message';
|
||||||
|
import { $t } from '#/locales';
|
||||||
|
|
||||||
|
import { useGridColumns, useGridFormSchema } from './data';
|
||||||
|
import Detail from './modules/detail.vue';
|
||||||
|
|
||||||
|
const [DetailModal, detailModalApi] = useVbenModal({
|
||||||
|
connectedComponent: Detail,
|
||||||
|
destroyOnClose: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 刷新表格 */
|
||||||
|
function handleRefresh() {
|
||||||
|
gridApi.query();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 查看站内信详情 */
|
||||||
|
function handleDetail(row: SystemNotifyMessageApi.NotifyMessage) {
|
||||||
|
detailModalApi.setData(row).open();
|
||||||
|
}
|
||||||
|
|
||||||
|
const [Grid, gridApi] = useVbenVxeGrid({
|
||||||
|
formOptions: {
|
||||||
|
schema: useGridFormSchema(),
|
||||||
|
},
|
||||||
|
gridOptions: {
|
||||||
|
columns: useGridColumns(),
|
||||||
|
height: 'auto',
|
||||||
|
keepSource: true,
|
||||||
|
proxyConfig: {
|
||||||
|
ajax: {
|
||||||
|
query: async ({ page }, formValues) => {
|
||||||
|
return await getNotifyMessagePage({
|
||||||
|
pageNo: page.currentPage,
|
||||||
|
pageSize: page.pageSize,
|
||||||
|
...formValues,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
rowConfig: {
|
||||||
|
keyField: 'id',
|
||||||
|
isHover: true,
|
||||||
|
},
|
||||||
|
toolbarConfig: {
|
||||||
|
refresh: true,
|
||||||
|
search: true,
|
||||||
|
},
|
||||||
|
} as VxeTableGridOptions<SystemNotifyMessageApi.NotifyMessage>,
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Page auto-content-height>
|
||||||
|
<template #doc>
|
||||||
|
<DocAlert title="站内信" url="https://doc.iocoder.cn/notify/" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<DetailModal @success="handleRefresh" />
|
||||||
|
<Grid table-title="站内信列表">
|
||||||
|
<template #actions="{ row }">
|
||||||
|
<TableAction
|
||||||
|
:actions="[
|
||||||
|
{
|
||||||
|
label: $t('common.detail'),
|
||||||
|
type: 'primary',
|
||||||
|
text: true,
|
||||||
|
icon: ACTION_ICON.VIEW,
|
||||||
|
auth: ['system:notify-message:query'],
|
||||||
|
onClick: handleDetail.bind(null, row),
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</Grid>
|
||||||
|
</Page>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { SystemNotifyMessageApi } from '#/api/system/notify/message';
|
||||||
|
|
||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
import { useVbenModal } from '@vben/common-ui';
|
||||||
|
|
||||||
|
import { useDescription } from '#/components/description';
|
||||||
|
|
||||||
|
import { useDetailSchema } from '../data';
|
||||||
|
|
||||||
|
const formData = ref<SystemNotifyMessageApi.NotifyMessage>();
|
||||||
|
|
||||||
|
const [Descriptions] = useDescription({
|
||||||
|
componentProps: {
|
||||||
|
bordered: true,
|
||||||
|
column: 1,
|
||||||
|
contentClass: 'mx-4',
|
||||||
|
},
|
||||||
|
schema: useDetailSchema(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const [Modal, modalApi] = useVbenModal({
|
||||||
|
async onOpenChange(isOpen: boolean) {
|
||||||
|
if (!isOpen) {
|
||||||
|
formData.value = undefined;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 加载数据
|
||||||
|
const data = modalApi.getData<SystemNotifyMessageApi.NotifyMessage>();
|
||||||
|
if (!data || !data.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
modalApi.lock();
|
||||||
|
try {
|
||||||
|
formData.value = data;
|
||||||
|
} finally {
|
||||||
|
modalApi.unlock();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Modal
|
||||||
|
title="站内信详情"
|
||||||
|
class="w-1/3"
|
||||||
|
:show-cancel-button="false"
|
||||||
|
:show-confirm-button="false"
|
||||||
|
>
|
||||||
|
<Descriptions :data="formData" />
|
||||||
|
</Modal>
|
||||||
|
</template>
|
||||||
142
apps/web-naive/src/views/system/notify/my/data.ts
Normal file
142
apps/web-naive/src/views/system/notify/my/data.ts
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
import type { VbenFormSchema } from '#/adapter/form';
|
||||||
|
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||||
|
import type { SystemNotifyMessageApi } from '#/api/system/notify/message';
|
||||||
|
import type { DescriptionItemSchema } from '#/components/description';
|
||||||
|
|
||||||
|
import { h } from 'vue';
|
||||||
|
|
||||||
|
import { DICT_TYPE } from '@vben/constants';
|
||||||
|
import { getDictOptions } from '@vben/hooks';
|
||||||
|
import { formatDateTime } from '@vben/utils';
|
||||||
|
|
||||||
|
import { DictTag } from '#/components/dict-tag';
|
||||||
|
import { getRangePickerDefaultProps } from '#/utils';
|
||||||
|
|
||||||
|
/** 列表的搜索表单 */
|
||||||
|
export function useGridFormSchema(): VbenFormSchema[] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
fieldName: 'readStatus',
|
||||||
|
label: '是否已读',
|
||||||
|
component: 'Select',
|
||||||
|
componentProps: {
|
||||||
|
options: getDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING, 'boolean'),
|
||||||
|
clearable: true,
|
||||||
|
placeholder: '请选择是否已读',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'createTime',
|
||||||
|
label: '发送时间',
|
||||||
|
component: 'DatePicker',
|
||||||
|
componentProps: {
|
||||||
|
...getRangePickerDefaultProps(),
|
||||||
|
clearable: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 列表的字段 */
|
||||||
|
export function useGridColumns(): VxeTableGridOptions['columns'] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
title: '',
|
||||||
|
width: 40,
|
||||||
|
type: 'checkbox',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'templateNickname',
|
||||||
|
title: '发送人',
|
||||||
|
minWidth: 180,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'createTime',
|
||||||
|
title: '发送时间',
|
||||||
|
minWidth: 180,
|
||||||
|
formatter: 'formatDateTime',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'templateType',
|
||||||
|
title: '类型',
|
||||||
|
minWidth: 120,
|
||||||
|
cellRender: {
|
||||||
|
name: 'CellDict',
|
||||||
|
props: { type: DICT_TYPE.SYSTEM_NOTIFY_TEMPLATE_TYPE },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'templateContent',
|
||||||
|
title: '消息内容',
|
||||||
|
minWidth: 300,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'readStatus',
|
||||||
|
title: '是否已读',
|
||||||
|
minWidth: 100,
|
||||||
|
cellRender: {
|
||||||
|
name: 'CellDict',
|
||||||
|
props: { type: DICT_TYPE.INFRA_BOOLEAN_STRING },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'readTime',
|
||||||
|
title: '阅读时间',
|
||||||
|
minWidth: 180,
|
||||||
|
formatter: 'formatDateTime',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '操作',
|
||||||
|
width: 130,
|
||||||
|
fixed: 'right',
|
||||||
|
slots: { default: 'actions' },
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useDetailSchema(): DescriptionItemSchema[] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
field: 'templateNickname',
|
||||||
|
label: '发送人',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'createTime',
|
||||||
|
label: '发送时间',
|
||||||
|
content: (data: SystemNotifyMessageApi.NotifyMessage) => {
|
||||||
|
return formatDateTime(data?.createTime || '') as string;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'templateType',
|
||||||
|
label: '消息类型',
|
||||||
|
content: (data: SystemNotifyMessageApi.NotifyMessage) => {
|
||||||
|
return h(DictTag, {
|
||||||
|
type: DICT_TYPE.SYSTEM_NOTIFY_TEMPLATE_TYPE,
|
||||||
|
value: data?.templateType,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'readStatus',
|
||||||
|
label: '是否已读',
|
||||||
|
content: (data: SystemNotifyMessageApi.NotifyMessage) => {
|
||||||
|
return h(DictTag, {
|
||||||
|
type: DICT_TYPE.INFRA_BOOLEAN_STRING,
|
||||||
|
value: data?.readStatus,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'readTime',
|
||||||
|
label: '阅读时间',
|
||||||
|
content: (data: SystemNotifyMessageApi.NotifyMessage) => {
|
||||||
|
return formatDateTime(data?.readTime || '') as string;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'templateContent',
|
||||||
|
label: '消息内容',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
190
apps/web-naive/src/views/system/notify/my/index.vue
Normal file
190
apps/web-naive/src/views/system/notify/my/index.vue
Normal file
@@ -0,0 +1,190 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||||
|
import type { SystemNotifyMessageApi } from '#/api/system/notify/message';
|
||||||
|
|
||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
import { DocAlert, Page, useVbenModal } from '@vben/common-ui';
|
||||||
|
import { isEmpty } from '@vben/utils';
|
||||||
|
|
||||||
|
import { message } from '#/adapter/naive';
|
||||||
|
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||||
|
import {
|
||||||
|
getMyNotifyMessagePage,
|
||||||
|
updateAllNotifyMessageRead,
|
||||||
|
updateNotifyMessageRead,
|
||||||
|
} from '#/api/system/notify/message';
|
||||||
|
|
||||||
|
import { useGridColumns, useGridFormSchema } from './data';
|
||||||
|
import Detail from './modules/detail.vue';
|
||||||
|
|
||||||
|
const [DetailModal, detailModalApi] = useVbenModal({
|
||||||
|
connectedComponent: Detail,
|
||||||
|
destroyOnClose: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 刷新表格 */
|
||||||
|
function handleRefresh() {
|
||||||
|
gridApi.query();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 查看站内信详情 */
|
||||||
|
function handleDetail(row: SystemNotifyMessageApi.NotifyMessage) {
|
||||||
|
detailModalApi.setData(row).open();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 标记一条站内信已读 */
|
||||||
|
async function handleRead(row: SystemNotifyMessageApi.NotifyMessage) {
|
||||||
|
const hideLoading = message.loading('正在标记已读...', {
|
||||||
|
duration: 0,
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
await updateNotifyMessageRead([row.id]);
|
||||||
|
message.success('标记已读成功');
|
||||||
|
handleRefresh();
|
||||||
|
// 打开详情
|
||||||
|
handleDetail(row);
|
||||||
|
} finally {
|
||||||
|
hideLoading.destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 标记选中的站内信为已读 */
|
||||||
|
async function handleMarkRead() {
|
||||||
|
const rows = gridApi.grid.getCheckboxRecords();
|
||||||
|
if (!rows || rows.length === 0) {
|
||||||
|
message.warning('请选择需要标记的站内信');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ids = rows.map((row: SystemNotifyMessageApi.NotifyMessage) => row.id);
|
||||||
|
const hideLoading = message.loading('正在标记已读...', {
|
||||||
|
duration: 0,
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
await updateNotifyMessageRead(ids);
|
||||||
|
checkedIds.value = [];
|
||||||
|
message.success('标记已读成功');
|
||||||
|
await gridApi.grid.setAllCheckboxRow(false);
|
||||||
|
handleRefresh();
|
||||||
|
} finally {
|
||||||
|
hideLoading.destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const checkedIds = ref<number[]>([]);
|
||||||
|
function handleRowCheckboxChange({
|
||||||
|
records,
|
||||||
|
}: {
|
||||||
|
records: SystemNotifyMessageApi.NotifyMessage[];
|
||||||
|
}) {
|
||||||
|
checkedIds.value = records.map((item) => item.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 标记所有站内信为已读 */
|
||||||
|
async function handleMarkAllRead() {
|
||||||
|
const hideLoading = message.loading('正在标记全部已读...', {
|
||||||
|
duration: 0,
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
await updateAllNotifyMessageRead();
|
||||||
|
message.success('全部标记已读成功');
|
||||||
|
checkedIds.value = [];
|
||||||
|
await gridApi.grid.setAllCheckboxRow(false);
|
||||||
|
handleRefresh();
|
||||||
|
} finally {
|
||||||
|
hideLoading.destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const [Grid, gridApi] = useVbenVxeGrid({
|
||||||
|
formOptions: {
|
||||||
|
schema: useGridFormSchema(),
|
||||||
|
},
|
||||||
|
gridOptions: {
|
||||||
|
columns: useGridColumns(),
|
||||||
|
height: 'auto',
|
||||||
|
keepSource: true,
|
||||||
|
proxyConfig: {
|
||||||
|
ajax: {
|
||||||
|
query: async ({ page }, formValues) => {
|
||||||
|
return await getMyNotifyMessagePage({
|
||||||
|
pageNo: page.currentPage,
|
||||||
|
pageSize: page.pageSize,
|
||||||
|
...formValues,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
rowConfig: {
|
||||||
|
keyField: 'id',
|
||||||
|
isHover: true,
|
||||||
|
},
|
||||||
|
toolbarConfig: {
|
||||||
|
refresh: true,
|
||||||
|
search: true,
|
||||||
|
},
|
||||||
|
checkboxConfig: {
|
||||||
|
checkMethod: (params: { row: SystemNotifyMessageApi.NotifyMessage }) =>
|
||||||
|
!params.row.readStatus,
|
||||||
|
highlight: true,
|
||||||
|
},
|
||||||
|
} as VxeTableGridOptions<SystemNotifyMessageApi.NotifyMessage>,
|
||||||
|
gridEvents: {
|
||||||
|
checkboxAll: handleRowCheckboxChange,
|
||||||
|
checkboxChange: handleRowCheckboxChange,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<Page auto-content-height>
|
||||||
|
<template #doc>
|
||||||
|
<DocAlert title="站内信配置" url="https://doc.iocoder.cn/notify/" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<DetailModal @success="handleRefresh" />
|
||||||
|
<Grid table-title="我的站内信">
|
||||||
|
<template #toolbar-tools>
|
||||||
|
<TableAction
|
||||||
|
:actions="[
|
||||||
|
{
|
||||||
|
label: '标记已读',
|
||||||
|
type: 'primary',
|
||||||
|
icon: ACTION_ICON.ADD,
|
||||||
|
disabled: isEmpty(checkedIds),
|
||||||
|
onClick: handleMarkRead,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '全部已读',
|
||||||
|
type: 'primary',
|
||||||
|
icon: ACTION_ICON.ADD,
|
||||||
|
onClick: handleMarkAllRead,
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<template #actions="{ row }">
|
||||||
|
<TableAction
|
||||||
|
:actions="[
|
||||||
|
{
|
||||||
|
label: '查看',
|
||||||
|
type: 'primary',
|
||||||
|
text: true,
|
||||||
|
ifShow: row.readStatus,
|
||||||
|
icon: ACTION_ICON.VIEW,
|
||||||
|
onClick: handleDetail.bind(null, row),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '已读',
|
||||||
|
type: 'primary',
|
||||||
|
text: true,
|
||||||
|
ifShow: !row.readStatus,
|
||||||
|
icon: ACTION_ICON.ADD,
|
||||||
|
onClick: handleRead.bind(null, row),
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</Grid>
|
||||||
|
</Page>
|
||||||
|
</template>
|
||||||
53
apps/web-naive/src/views/system/notify/my/modules/detail.vue
Normal file
53
apps/web-naive/src/views/system/notify/my/modules/detail.vue
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { SystemNotifyMessageApi } from '#/api/system/notify/message';
|
||||||
|
|
||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
import { useVbenModal } from '@vben/common-ui';
|
||||||
|
|
||||||
|
import { useDescription } from '#/components/description';
|
||||||
|
|
||||||
|
import { useDetailSchema } from '../data';
|
||||||
|
|
||||||
|
const formData = ref<SystemNotifyMessageApi.NotifyMessage>();
|
||||||
|
|
||||||
|
const [Descriptions] = useDescription({
|
||||||
|
componentProps: {
|
||||||
|
bordered: true,
|
||||||
|
column: 1,
|
||||||
|
contentClass: 'mx-4',
|
||||||
|
},
|
||||||
|
schema: useDetailSchema(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const [Modal, modalApi] = useVbenModal({
|
||||||
|
async onOpenChange(isOpen: boolean) {
|
||||||
|
if (!isOpen) {
|
||||||
|
formData.value = undefined;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 加载数据
|
||||||
|
const data = modalApi.getData<SystemNotifyMessageApi.NotifyMessage>();
|
||||||
|
if (!data || !data.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
modalApi.lock();
|
||||||
|
try {
|
||||||
|
formData.value = data;
|
||||||
|
} finally {
|
||||||
|
modalApi.unlock();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Modal
|
||||||
|
title="站内信详情"
|
||||||
|
class="w-1/3"
|
||||||
|
:show-cancel-button="false"
|
||||||
|
:show-confirm-button="false"
|
||||||
|
>
|
||||||
|
<Descriptions :data="formData" />
|
||||||
|
</Modal>
|
||||||
|
</template>
|
||||||
291
apps/web-naive/src/views/system/notify/template/data.ts
Normal file
291
apps/web-naive/src/views/system/notify/template/data.ts
Normal file
@@ -0,0 +1,291 @@
|
|||||||
|
import type { VbenFormSchema } from '#/adapter/form';
|
||||||
|
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||||
|
|
||||||
|
import { CommonStatusEnum, DICT_TYPE, UserTypeEnum } from '@vben/constants';
|
||||||
|
import { getDictOptions } from '@vben/hooks';
|
||||||
|
|
||||||
|
import { z } from '#/adapter/form';
|
||||||
|
import { getSimpleUserList } from '#/api/system/user';
|
||||||
|
import { getRangePickerDefaultProps } from '#/utils';
|
||||||
|
|
||||||
|
/** 新增/修改的表单 */
|
||||||
|
export function useFormSchema(): VbenFormSchema[] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
fieldName: 'id',
|
||||||
|
component: 'Input',
|
||||||
|
dependencies: {
|
||||||
|
triggerFields: [''],
|
||||||
|
show: () => false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'name',
|
||||||
|
label: '模板名称',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入模板名称',
|
||||||
|
},
|
||||||
|
rules: 'required',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'code',
|
||||||
|
label: '模板编码',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入模板编码',
|
||||||
|
},
|
||||||
|
rules: 'required',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'nickname',
|
||||||
|
label: '发送人名称',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入发送人名称',
|
||||||
|
},
|
||||||
|
rules: 'required',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'content',
|
||||||
|
label: '模板内容',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
type: 'textarea',
|
||||||
|
placeholder: '请输入模板内容',
|
||||||
|
},
|
||||||
|
rules: 'required',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'type',
|
||||||
|
label: '模板类型',
|
||||||
|
component: 'Select',
|
||||||
|
componentProps: {
|
||||||
|
options: getDictOptions(
|
||||||
|
DICT_TYPE.SYSTEM_NOTIFY_TEMPLATE_TYPE,
|
||||||
|
'number',
|
||||||
|
),
|
||||||
|
placeholder: '请选择模板类型',
|
||||||
|
},
|
||||||
|
rules: 'required',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'status',
|
||||||
|
label: '状态',
|
||||||
|
component: 'RadioGroup',
|
||||||
|
componentProps: {
|
||||||
|
options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
|
||||||
|
buttonStyle: 'solid',
|
||||||
|
optionType: 'button',
|
||||||
|
},
|
||||||
|
rules: z.number().default(CommonStatusEnum.ENABLE),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'remark',
|
||||||
|
label: '备注',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
type: 'textarea',
|
||||||
|
placeholder: '请输入备注',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 列表的搜索表单 */
|
||||||
|
export function useGridFormSchema(): VbenFormSchema[] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
fieldName: 'name',
|
||||||
|
label: '模板名称',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
clearable: true,
|
||||||
|
placeholder: '请输入模板名称',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'code',
|
||||||
|
label: '模板编码',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
clearable: true,
|
||||||
|
placeholder: '请输入模板编码',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'status',
|
||||||
|
label: '状态',
|
||||||
|
component: 'Select',
|
||||||
|
componentProps: {
|
||||||
|
options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
|
||||||
|
clearable: true,
|
||||||
|
placeholder: '请选择状态',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'type',
|
||||||
|
label: '模板类型',
|
||||||
|
component: 'Select',
|
||||||
|
componentProps: {
|
||||||
|
options: getDictOptions(
|
||||||
|
DICT_TYPE.SYSTEM_NOTIFY_TEMPLATE_TYPE,
|
||||||
|
'number',
|
||||||
|
),
|
||||||
|
clearable: true,
|
||||||
|
placeholder: '请选择模板类型',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'createTime',
|
||||||
|
label: '创建时间',
|
||||||
|
component: 'DatePicker',
|
||||||
|
componentProps: {
|
||||||
|
...getRangePickerDefaultProps(),
|
||||||
|
clearable: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 发送站内信表单 */
|
||||||
|
export function useSendNotifyFormSchema(): VbenFormSchema[] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
fieldName: 'content',
|
||||||
|
label: '模板内容',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
type: 'textarea',
|
||||||
|
disabled: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'templateCode',
|
||||||
|
label: '模板编码',
|
||||||
|
component: 'Input',
|
||||||
|
dependencies: {
|
||||||
|
triggerFields: [''],
|
||||||
|
show: () => false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'userType',
|
||||||
|
label: '用户类型',
|
||||||
|
component: 'RadioGroup',
|
||||||
|
componentProps: {
|
||||||
|
options: getDictOptions(DICT_TYPE.USER_TYPE, 'number'),
|
||||||
|
},
|
||||||
|
rules: z.number().default(UserTypeEnum.MEMBER),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'userId',
|
||||||
|
label: '接收人 ID',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入用户编号',
|
||||||
|
},
|
||||||
|
dependencies: {
|
||||||
|
show(values) {
|
||||||
|
return values.userType === UserTypeEnum.MEMBER;
|
||||||
|
},
|
||||||
|
triggerFields: ['userType'],
|
||||||
|
},
|
||||||
|
rules: 'required',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'userId',
|
||||||
|
label: '接收人',
|
||||||
|
component: 'ApiSelect',
|
||||||
|
componentProps: {
|
||||||
|
api: getSimpleUserList,
|
||||||
|
labelField: 'nickname',
|
||||||
|
valueField: 'id',
|
||||||
|
placeholder: '请选择接收人',
|
||||||
|
},
|
||||||
|
dependencies: {
|
||||||
|
show(values) {
|
||||||
|
return values.userType === UserTypeEnum.ADMIN;
|
||||||
|
},
|
||||||
|
triggerFields: ['userType'],
|
||||||
|
},
|
||||||
|
rules: 'required',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'templateParams',
|
||||||
|
label: '模板参数',
|
||||||
|
component: 'Input',
|
||||||
|
dependencies: {
|
||||||
|
triggerFields: [''],
|
||||||
|
show: () => false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 列表的字段 */
|
||||||
|
export function useGridColumns(): VxeTableGridOptions['columns'] {
|
||||||
|
return [
|
||||||
|
{ type: 'checkbox', width: 40 },
|
||||||
|
{
|
||||||
|
field: 'id',
|
||||||
|
title: '编号',
|
||||||
|
minWidth: 100,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'name',
|
||||||
|
title: '模板名称',
|
||||||
|
minWidth: 120,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'code',
|
||||||
|
title: '模板编码',
|
||||||
|
minWidth: 120,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'nickname',
|
||||||
|
title: '发送人名称',
|
||||||
|
minWidth: 120,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'content',
|
||||||
|
title: '模板内容',
|
||||||
|
minWidth: 200,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'type',
|
||||||
|
title: '模板类型',
|
||||||
|
minWidth: 120,
|
||||||
|
cellRender: {
|
||||||
|
name: 'CellDict',
|
||||||
|
props: { type: DICT_TYPE.SYSTEM_NOTIFY_TEMPLATE_TYPE },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'status',
|
||||||
|
title: '状态',
|
||||||
|
minWidth: 100,
|
||||||
|
cellRender: {
|
||||||
|
name: 'CellDict',
|
||||||
|
props: { type: DICT_TYPE.COMMON_STATUS },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'remark',
|
||||||
|
title: '备注',
|
||||||
|
minWidth: 120,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'createTime',
|
||||||
|
title: '创建时间',
|
||||||
|
minWidth: 180,
|
||||||
|
formatter: 'formatDateTime',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '操作',
|
||||||
|
width: 220,
|
||||||
|
fixed: 'right',
|
||||||
|
slots: { default: 'actions' },
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
209
apps/web-naive/src/views/system/notify/template/index.vue
Normal file
209
apps/web-naive/src/views/system/notify/template/index.vue
Normal file
@@ -0,0 +1,209 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||||
|
import type { SystemNotifyTemplateApi } from '#/api/system/notify/template';
|
||||||
|
|
||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
import { confirm, DocAlert, Page, useVbenModal } from '@vben/common-ui';
|
||||||
|
import { downloadFileFromBlobPart, isEmpty } from '@vben/utils';
|
||||||
|
|
||||||
|
import { message } from '#/adapter/naive';
|
||||||
|
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||||
|
import {
|
||||||
|
deleteNotifyTemplate,
|
||||||
|
deleteNotifyTemplateList,
|
||||||
|
exportNotifyTemplate,
|
||||||
|
getNotifyTemplatePage,
|
||||||
|
} from '#/api/system/notify/template';
|
||||||
|
import { $t } from '#/locales';
|
||||||
|
|
||||||
|
import { useGridColumns, useGridFormSchema } from './data';
|
||||||
|
import Form from './modules/form.vue';
|
||||||
|
import SendForm from './modules/send-form.vue';
|
||||||
|
|
||||||
|
const [FormModal, formModalApi] = useVbenModal({
|
||||||
|
connectedComponent: Form,
|
||||||
|
destroyOnClose: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const [SendModal, sendModalApi] = useVbenModal({
|
||||||
|
connectedComponent: SendForm,
|
||||||
|
destroyOnClose: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 刷新表格 */
|
||||||
|
function handleRefresh() {
|
||||||
|
gridApi.query();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 导出表格 */
|
||||||
|
async function handleExport() {
|
||||||
|
const data = await exportNotifyTemplate(await gridApi.formApi.getValues());
|
||||||
|
downloadFileFromBlobPart({ fileName: '站内信模板.xls', source: data });
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 创建站内信模板 */
|
||||||
|
function handleCreate() {
|
||||||
|
formModalApi.setData(null).open();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 编辑站内信模板 */
|
||||||
|
function handleEdit(row: SystemNotifyTemplateApi.NotifyTemplate) {
|
||||||
|
formModalApi.setData(row).open();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 发送测试站内信 */
|
||||||
|
function handleSend(row: SystemNotifyTemplateApi.NotifyTemplate) {
|
||||||
|
sendModalApi.setData(row).open();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 删除站内信模板 */
|
||||||
|
async function handleDelete(row: SystemNotifyTemplateApi.NotifyTemplate) {
|
||||||
|
const hideLoading = message.loading(
|
||||||
|
$t('ui.actionMessage.deleting', [row.name]),
|
||||||
|
{
|
||||||
|
duration: 0,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
try {
|
||||||
|
await deleteNotifyTemplate(row.id!);
|
||||||
|
message.success($t('ui.actionMessage.deleteSuccess', [row.name]));
|
||||||
|
handleRefresh();
|
||||||
|
} finally {
|
||||||
|
hideLoading.destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 批量删除站内信模板 */
|
||||||
|
async function handleDeleteBatch() {
|
||||||
|
await confirm($t('ui.actionMessage.deleteBatchConfirm'));
|
||||||
|
const hideLoading = message.loading($t('ui.actionMessage.deletingBatch'), {
|
||||||
|
duration: 0,
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
await deleteNotifyTemplateList(checkedIds.value);
|
||||||
|
checkedIds.value = [];
|
||||||
|
message.success($t('ui.actionMessage.deleteSuccess'));
|
||||||
|
handleRefresh();
|
||||||
|
} finally {
|
||||||
|
hideLoading.destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const checkedIds = ref<number[]>([]);
|
||||||
|
function handleRowCheckboxChange({
|
||||||
|
records,
|
||||||
|
}: {
|
||||||
|
records: SystemNotifyTemplateApi.NotifyTemplate[];
|
||||||
|
}) {
|
||||||
|
checkedIds.value = records.map((item) => item.id!);
|
||||||
|
}
|
||||||
|
|
||||||
|
const [Grid, gridApi] = useVbenVxeGrid({
|
||||||
|
formOptions: {
|
||||||
|
schema: useGridFormSchema(),
|
||||||
|
},
|
||||||
|
gridOptions: {
|
||||||
|
columns: useGridColumns(),
|
||||||
|
height: 'auto',
|
||||||
|
keepSource: true,
|
||||||
|
proxyConfig: {
|
||||||
|
ajax: {
|
||||||
|
query: async ({ page }, formValues) => {
|
||||||
|
return await getNotifyTemplatePage({
|
||||||
|
pageNo: page.currentPage,
|
||||||
|
pageSize: page.pageSize,
|
||||||
|
...formValues,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
rowConfig: {
|
||||||
|
keyField: 'id',
|
||||||
|
isHover: true,
|
||||||
|
},
|
||||||
|
toolbarConfig: {
|
||||||
|
refresh: true,
|
||||||
|
search: true,
|
||||||
|
},
|
||||||
|
} as VxeTableGridOptions<SystemNotifyTemplateApi.NotifyTemplate>,
|
||||||
|
gridEvents: {
|
||||||
|
checkboxAll: handleRowCheckboxChange,
|
||||||
|
checkboxChange: handleRowCheckboxChange,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Page auto-content-height>
|
||||||
|
<template #doc>
|
||||||
|
<DocAlert title="站内信" url="https://doc.iocoder.cn/notify/" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<FormModal @success="handleRefresh" />
|
||||||
|
<SendModal />
|
||||||
|
<Grid table-title="站内信模板列表">
|
||||||
|
<template #toolbar-tools>
|
||||||
|
<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,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: $t('ui.actionTitle.deleteBatch'),
|
||||||
|
type: 'error',
|
||||||
|
icon: ACTION_ICON.DELETE,
|
||||||
|
disabled: isEmpty(checkedIds),
|
||||||
|
auth: ['system:notify-template:delete'],
|
||||||
|
onClick: handleDeleteBatch,
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<template #actions="{ row }">
|
||||||
|
<TableAction
|
||||||
|
:actions="[
|
||||||
|
{
|
||||||
|
label: $t('common.edit'),
|
||||||
|
type: 'primary',
|
||||||
|
text: true,
|
||||||
|
icon: ACTION_ICON.EDIT,
|
||||||
|
auth: ['system:notify-template:update'],
|
||||||
|
onClick: handleEdit.bind(null, row),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '测试',
|
||||||
|
type: 'primary',
|
||||||
|
text: true,
|
||||||
|
icon: ACTION_ICON.VIEW,
|
||||||
|
auth: ['system:notify-template:send-notify'],
|
||||||
|
onClick: handleSend.bind(null, row),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: $t('common.delete'),
|
||||||
|
type: 'error',
|
||||||
|
text: 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>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { SystemNotifyTemplateApi } from '#/api/system/notify/template';
|
||||||
|
|
||||||
|
import { computed, ref } from 'vue';
|
||||||
|
|
||||||
|
import { useVbenModal } from '@vben/common-ui';
|
||||||
|
|
||||||
|
import { useVbenForm } from '#/adapter/form';
|
||||||
|
import { message } from '#/adapter/naive';
|
||||||
|
import {
|
||||||
|
createNotifyTemplate,
|
||||||
|
getNotifyTemplate,
|
||||||
|
updateNotifyTemplate,
|
||||||
|
} from '#/api/system/notify/template';
|
||||||
|
import { $t } from '#/locales';
|
||||||
|
|
||||||
|
import { useFormSchema } from '../data';
|
||||||
|
|
||||||
|
const emit = defineEmits(['success']);
|
||||||
|
const formData = ref<SystemNotifyTemplateApi.NotifyTemplate>();
|
||||||
|
const getTitle = computed(() => {
|
||||||
|
return formData.value?.id
|
||||||
|
? $t('ui.actionTitle.edit', ['站内信模板'])
|
||||||
|
: $t('ui.actionTitle.create', ['站内信模板']);
|
||||||
|
});
|
||||||
|
|
||||||
|
const [Form, formApi] = useVbenForm({
|
||||||
|
commonConfig: {
|
||||||
|
componentProps: {
|
||||||
|
class: 'w-full',
|
||||||
|
},
|
||||||
|
formItemClass: 'col-span-2',
|
||||||
|
labelWidth: 80,
|
||||||
|
},
|
||||||
|
layout: 'horizontal',
|
||||||
|
schema: useFormSchema(),
|
||||||
|
showDefaultActions: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const [Modal, modalApi] = useVbenModal({
|
||||||
|
async onConfirm() {
|
||||||
|
const { valid } = await formApi.validate();
|
||||||
|
if (!valid) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
modalApi.lock();
|
||||||
|
// 提交表单
|
||||||
|
const data =
|
||||||
|
(await formApi.getValues()) as SystemNotifyTemplateApi.NotifyTemplate;
|
||||||
|
try {
|
||||||
|
await (formData.value?.id
|
||||||
|
? updateNotifyTemplate(data)
|
||||||
|
: createNotifyTemplate(data));
|
||||||
|
// 关闭并提示
|
||||||
|
await modalApi.close();
|
||||||
|
emit('success');
|
||||||
|
message.success($t('ui.actionMessage.operationSuccess'));
|
||||||
|
} finally {
|
||||||
|
modalApi.unlock();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async onOpenChange(isOpen: boolean) {
|
||||||
|
if (!isOpen) {
|
||||||
|
formData.value = undefined;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 加载数据
|
||||||
|
const data = modalApi.getData<SystemNotifyTemplateApi.NotifyTemplate>();
|
||||||
|
if (!data || !data.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
modalApi.lock();
|
||||||
|
try {
|
||||||
|
formData.value = await getNotifyTemplate(data.id);
|
||||||
|
// 设置到 values
|
||||||
|
await formApi.setValues(formData.value);
|
||||||
|
} finally {
|
||||||
|
modalApi.unlock();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Modal :title="getTitle">
|
||||||
|
<Form class="mx-4" />
|
||||||
|
</Modal>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { SystemNotifyTemplateApi } from '#/api/system/notify/template';
|
||||||
|
|
||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
import { useVbenModal } from '@vben/common-ui';
|
||||||
|
|
||||||
|
import { useVbenForm } from '#/adapter/form';
|
||||||
|
import { message } from '#/adapter/naive';
|
||||||
|
import { sendNotify } from '#/api/system/notify/template';
|
||||||
|
import { $t } from '#/locales';
|
||||||
|
|
||||||
|
import { useSendNotifyFormSchema } from '../data';
|
||||||
|
|
||||||
|
const emit = defineEmits(['success']);
|
||||||
|
const formData = ref<SystemNotifyTemplateApi.NotifyTemplate>();
|
||||||
|
|
||||||
|
const [Form, formApi] = useVbenForm({
|
||||||
|
commonConfig: {
|
||||||
|
componentProps: {
|
||||||
|
class: 'w-full',
|
||||||
|
},
|
||||||
|
formItemClass: 'col-span-2',
|
||||||
|
labelWidth: 120,
|
||||||
|
},
|
||||||
|
layout: 'horizontal',
|
||||||
|
showDefaultActions: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const [Modal, modalApi] = useVbenModal({
|
||||||
|
async onConfirm() {
|
||||||
|
const { valid } = await formApi.validate();
|
||||||
|
if (!valid) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
modalApi.lock();
|
||||||
|
// 构建发送请求
|
||||||
|
const values = await formApi.getValues();
|
||||||
|
const paramsObj: Record<string, string> = {};
|
||||||
|
if (formData.value?.params) {
|
||||||
|
formData.value.params.forEach((param) => {
|
||||||
|
paramsObj[param] = values[`param_${param}`];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const data: SystemNotifyTemplateApi.NotifySendReqVO = {
|
||||||
|
userId: values.userId,
|
||||||
|
userType: values.userType,
|
||||||
|
templateCode: formData.value?.code || '',
|
||||||
|
templateParams: paramsObj,
|
||||||
|
};
|
||||||
|
|
||||||
|
// 提交表单
|
||||||
|
try {
|
||||||
|
await sendNotify(data);
|
||||||
|
// 关闭并提示
|
||||||
|
await modalApi.close();
|
||||||
|
emit('success');
|
||||||
|
message.success($t('ui.actionMessage.operationSuccess'));
|
||||||
|
} catch (error) {
|
||||||
|
console.error('发送站内信失败', error);
|
||||||
|
} finally {
|
||||||
|
modalApi.unlock();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async onOpenChange(isOpen: boolean) {
|
||||||
|
if (!isOpen) {
|
||||||
|
formData.value = undefined;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 获取数据
|
||||||
|
const data = modalApi.getData<SystemNotifyTemplateApi.NotifyTemplate>();
|
||||||
|
if (!data || !data.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
formData.value = data;
|
||||||
|
// 更新 form schema
|
||||||
|
const schema = buildFormSchema();
|
||||||
|
formApi.setState({ schema });
|
||||||
|
// 设置到 values
|
||||||
|
await formApi.setValues({
|
||||||
|
content: data.content,
|
||||||
|
templateCode: data.code,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 动态构建表单 schema */
|
||||||
|
function buildFormSchema() {
|
||||||
|
const schema = useSendNotifyFormSchema();
|
||||||
|
if (formData.value?.params) {
|
||||||
|
formData.value.params.forEach((param: string) => {
|
||||||
|
schema.push({
|
||||||
|
fieldName: `param_${param}`,
|
||||||
|
label: `参数 ${param}`,
|
||||||
|
component: 'Input',
|
||||||
|
rules: 'required',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: `请输入参数 ${param}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return schema;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Modal title="测试发送站内信">
|
||||||
|
<Form class="mx-4" />
|
||||||
|
</Modal>
|
||||||
|
</template>
|
||||||
265
apps/web-naive/src/views/system/oauth2/client/data.ts
Normal file
265
apps/web-naive/src/views/system/oauth2/client/data.ts
Normal file
@@ -0,0 +1,265 @@
|
|||||||
|
import type { VbenFormSchema } from '#/adapter/form';
|
||||||
|
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||||
|
|
||||||
|
import { CommonStatusEnum, DICT_TYPE } from '@vben/constants';
|
||||||
|
import { getDictOptions } from '@vben/hooks';
|
||||||
|
|
||||||
|
import { z } from '#/adapter/form';
|
||||||
|
|
||||||
|
/** 新增/修改的表单 */
|
||||||
|
export function useFormSchema(): VbenFormSchema[] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
fieldName: 'id',
|
||||||
|
component: 'Input',
|
||||||
|
dependencies: {
|
||||||
|
triggerFields: [''],
|
||||||
|
show: () => false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'clientId',
|
||||||
|
label: '客户端编号',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入客户端编号',
|
||||||
|
},
|
||||||
|
rules: 'required',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'secret',
|
||||||
|
label: '客户端密钥',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入客户端密钥',
|
||||||
|
},
|
||||||
|
rules: 'required',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'name',
|
||||||
|
label: '应用名',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入应用名',
|
||||||
|
},
|
||||||
|
rules: 'required',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'logo',
|
||||||
|
label: '应用图标',
|
||||||
|
component: 'ImageUpload',
|
||||||
|
rules: 'required',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'description',
|
||||||
|
label: '应用描述',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
type: 'textarea',
|
||||||
|
placeholder: '请输入应用描述',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'status',
|
||||||
|
label: '状态',
|
||||||
|
component: 'RadioGroup',
|
||||||
|
componentProps: {
|
||||||
|
options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
|
||||||
|
buttonStyle: 'solid',
|
||||||
|
optionType: 'button',
|
||||||
|
},
|
||||||
|
rules: z.number().default(CommonStatusEnum.ENABLE),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'accessTokenValiditySeconds',
|
||||||
|
label: '访问令牌的有效期',
|
||||||
|
component: 'InputNumber',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入访问令牌的有效期,单位:秒',
|
||||||
|
min: 0,
|
||||||
|
},
|
||||||
|
rules: 'required',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'refreshTokenValiditySeconds',
|
||||||
|
label: '刷新令牌的有效期',
|
||||||
|
component: 'InputNumber',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入刷新令牌的有效期,单位:秒',
|
||||||
|
min: 0,
|
||||||
|
},
|
||||||
|
rules: 'required',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'authorizedGrantTypes',
|
||||||
|
label: '授权类型',
|
||||||
|
component: 'Select',
|
||||||
|
componentProps: {
|
||||||
|
options: getDictOptions(DICT_TYPE.SYSTEM_OAUTH2_GRANT_TYPE),
|
||||||
|
mode: 'multiple',
|
||||||
|
placeholder: '请输入授权类型',
|
||||||
|
},
|
||||||
|
rules: 'required',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'scopes',
|
||||||
|
label: '授权范围',
|
||||||
|
component: 'Select',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入授权范围',
|
||||||
|
mode: 'tags',
|
||||||
|
clearable: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'autoApproveScopes',
|
||||||
|
label: '自动授权范围',
|
||||||
|
component: 'Select',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入自动授权范围',
|
||||||
|
mode: 'multiple',
|
||||||
|
},
|
||||||
|
dependencies: {
|
||||||
|
triggerFields: ['scopes'],
|
||||||
|
componentProps: (values) => ({
|
||||||
|
options: values.scopes
|
||||||
|
? values.scopes.map((scope: string) => ({
|
||||||
|
label: scope,
|
||||||
|
value: scope,
|
||||||
|
}))
|
||||||
|
: [],
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'redirectUris',
|
||||||
|
label: '可重定向的 URI 地址',
|
||||||
|
component: 'Select',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入可重定向的 URI 地址',
|
||||||
|
mode: 'tags',
|
||||||
|
},
|
||||||
|
rules: 'required',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'authorities',
|
||||||
|
label: '权限',
|
||||||
|
component: 'Select',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入权限',
|
||||||
|
mode: 'tags',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'resourceIds',
|
||||||
|
label: '资源',
|
||||||
|
component: 'Select',
|
||||||
|
componentProps: {
|
||||||
|
mode: 'tags',
|
||||||
|
placeholder: '请输入资源',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'additionalInformation',
|
||||||
|
label: '附加信息',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
type: 'textarea',
|
||||||
|
placeholder: '请输入附加信息,JSON 格式数据',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 列表的搜索表单 */
|
||||||
|
export function useGridFormSchema(): VbenFormSchema[] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
fieldName: 'name',
|
||||||
|
label: '应用名',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入应用名',
|
||||||
|
clearable: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'status',
|
||||||
|
label: '状态',
|
||||||
|
component: 'Select',
|
||||||
|
componentProps: {
|
||||||
|
options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
|
||||||
|
clearable: true,
|
||||||
|
placeholder: '请输入状态',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 列表的字段 */
|
||||||
|
export function useGridColumns(): VxeTableGridOptions['columns'] {
|
||||||
|
return [
|
||||||
|
{ type: 'checkbox', width: 40 },
|
||||||
|
{
|
||||||
|
field: 'clientId',
|
||||||
|
title: '客户端编号',
|
||||||
|
minWidth: 120,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'secret',
|
||||||
|
title: '客户端密钥',
|
||||||
|
minWidth: 120,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'name',
|
||||||
|
title: '应用名',
|
||||||
|
minWidth: 120,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'logo',
|
||||||
|
title: '应用图标',
|
||||||
|
minWidth: 100,
|
||||||
|
cellRender: {
|
||||||
|
name: 'CellImage',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'status',
|
||||||
|
title: '状态',
|
||||||
|
minWidth: 80,
|
||||||
|
cellRender: {
|
||||||
|
name: 'CellDict',
|
||||||
|
props: { type: DICT_TYPE.COMMON_STATUS },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'accessTokenValiditySeconds',
|
||||||
|
title: '访问令牌的有效期',
|
||||||
|
minWidth: 150,
|
||||||
|
formatter: ({ cellValue }) => `${cellValue} 秒`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'refreshTokenValiditySeconds',
|
||||||
|
title: '刷新令牌的有效期',
|
||||||
|
minWidth: 150,
|
||||||
|
formatter: ({ cellValue }) => `${cellValue} 秒`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'authorizedGrantTypes',
|
||||||
|
title: '授权类型',
|
||||||
|
minWidth: 100,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'createTime',
|
||||||
|
title: '创建时间',
|
||||||
|
minWidth: 180,
|
||||||
|
formatter: 'formatDateTime',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '操作',
|
||||||
|
width: 130,
|
||||||
|
fixed: 'right',
|
||||||
|
slots: { default: 'actions' },
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
176
apps/web-naive/src/views/system/oauth2/client/index.vue
Normal file
176
apps/web-naive/src/views/system/oauth2/client/index.vue
Normal file
@@ -0,0 +1,176 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||||
|
import type { SystemOAuth2ClientApi } from '#/api/system/oauth2/client';
|
||||||
|
|
||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
import { confirm, DocAlert, Page, useVbenModal } from '@vben/common-ui';
|
||||||
|
import { isEmpty } from '@vben/utils';
|
||||||
|
|
||||||
|
import { message } from '#/adapter/naive';
|
||||||
|
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||||
|
import {
|
||||||
|
deleteOAuth2Client,
|
||||||
|
deleteOAuth2ClientList,
|
||||||
|
getOAuth2ClientPage,
|
||||||
|
} from '#/api/system/oauth2/client';
|
||||||
|
import { $t } from '#/locales';
|
||||||
|
|
||||||
|
import { useGridColumns, useGridFormSchema } from './data';
|
||||||
|
import Form from './modules/form.vue';
|
||||||
|
|
||||||
|
const [FormModal, formModalApi] = useVbenModal({
|
||||||
|
connectedComponent: Form,
|
||||||
|
destroyOnClose: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 刷新表格 */
|
||||||
|
function handleRefresh() {
|
||||||
|
gridApi.query();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 创建 OAuth2 客户端 */
|
||||||
|
function handleCreate() {
|
||||||
|
formModalApi.setData(null).open();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 编辑 OAuth2 客户端 */
|
||||||
|
function handleEdit(row: SystemOAuth2ClientApi.OAuth2Client) {
|
||||||
|
formModalApi.setData(row).open();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 删除 OAuth2 客户端 */
|
||||||
|
async function handleDelete(row: SystemOAuth2ClientApi.OAuth2Client) {
|
||||||
|
const hideLoading = message.loading(
|
||||||
|
$t('ui.actionMessage.deleting', [row.name]),
|
||||||
|
{ duration: 0 },
|
||||||
|
);
|
||||||
|
try {
|
||||||
|
await deleteOAuth2Client(row.id!);
|
||||||
|
message.success($t('ui.actionMessage.deleteSuccess', [row.name]));
|
||||||
|
handleRefresh();
|
||||||
|
} finally {
|
||||||
|
hideLoading.destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 批量删除 OAuth2 客户端 */
|
||||||
|
async function handleDeleteBatch() {
|
||||||
|
await confirm($t('ui.actionMessage.deleteBatchConfirm'));
|
||||||
|
const hideLoading = message.loading($t('ui.actionMessage.deletingBatch'), {
|
||||||
|
duration: 0,
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
await deleteOAuth2ClientList(checkedIds.value);
|
||||||
|
checkedIds.value = [];
|
||||||
|
message.success($t('ui.actionMessage.deleteSuccess'));
|
||||||
|
handleRefresh();
|
||||||
|
} finally {
|
||||||
|
hideLoading.destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const checkedIds = ref<number[]>([]);
|
||||||
|
function handleRowCheckboxChange({
|
||||||
|
records,
|
||||||
|
}: {
|
||||||
|
records: SystemOAuth2ClientApi.OAuth2Client[];
|
||||||
|
}) {
|
||||||
|
checkedIds.value = records.map((item) => item.id!);
|
||||||
|
}
|
||||||
|
|
||||||
|
const [Grid, gridApi] = useVbenVxeGrid({
|
||||||
|
formOptions: {
|
||||||
|
schema: useGridFormSchema(),
|
||||||
|
},
|
||||||
|
gridOptions: {
|
||||||
|
columns: useGridColumns(),
|
||||||
|
height: 'auto',
|
||||||
|
keepSource: true,
|
||||||
|
proxyConfig: {
|
||||||
|
ajax: {
|
||||||
|
query: async ({ page }, formValues) => {
|
||||||
|
return await getOAuth2ClientPage({
|
||||||
|
pageNo: page.currentPage,
|
||||||
|
pageSize: page.pageSize,
|
||||||
|
...formValues,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
rowConfig: {
|
||||||
|
keyField: 'id',
|
||||||
|
isHover: true,
|
||||||
|
},
|
||||||
|
toolbarConfig: {
|
||||||
|
refresh: true,
|
||||||
|
search: true,
|
||||||
|
},
|
||||||
|
} as VxeTableGridOptions<SystemOAuth2ClientApi.OAuth2Client>,
|
||||||
|
gridEvents: {
|
||||||
|
checkboxAll: handleRowCheckboxChange,
|
||||||
|
checkboxChange: handleRowCheckboxChange,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Page auto-content-height>
|
||||||
|
<template #doc>
|
||||||
|
<DocAlert
|
||||||
|
title="OAuth 2.0(SSO 单点登录)"
|
||||||
|
url="https://doc.iocoder.cn/oauth2/"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<FormModal @success="handleRefresh" />
|
||||||
|
<Grid table-title="OAuth2 客户端列表">
|
||||||
|
<template #toolbar-tools>
|
||||||
|
<TableAction
|
||||||
|
:actions="[
|
||||||
|
{
|
||||||
|
label: $t('ui.actionTitle.create', [' OAuth2.0 客户端']),
|
||||||
|
type: 'primary',
|
||||||
|
icon: ACTION_ICON.ADD,
|
||||||
|
auth: ['system:oauth2-client:create'],
|
||||||
|
onClick: handleCreate,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: $t('ui.actionTitle.deleteBatch'),
|
||||||
|
type: 'error',
|
||||||
|
icon: ACTION_ICON.DELETE,
|
||||||
|
auth: ['system:oauth2-client:delete'],
|
||||||
|
disabled: isEmpty(checkedIds),
|
||||||
|
onClick: handleDeleteBatch,
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<template #actions="{ row }">
|
||||||
|
<TableAction
|
||||||
|
:actions="[
|
||||||
|
{
|
||||||
|
label: $t('common.edit'),
|
||||||
|
type: 'primary',
|
||||||
|
text: true,
|
||||||
|
icon: ACTION_ICON.EDIT,
|
||||||
|
auth: ['system:oauth2-client:update'],
|
||||||
|
onClick: handleEdit.bind(null, row),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: $t('common.delete'),
|
||||||
|
type: 'error',
|
||||||
|
text: 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>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { SystemOAuth2ClientApi } from '#/api/system/oauth2/client';
|
||||||
|
|
||||||
|
import { computed, ref } from 'vue';
|
||||||
|
|
||||||
|
import { useVbenModal } from '@vben/common-ui';
|
||||||
|
|
||||||
|
import { useVbenForm } from '#/adapter/form';
|
||||||
|
import { message } from '#/adapter/naive';
|
||||||
|
import {
|
||||||
|
createOAuth2Client,
|
||||||
|
getOAuth2Client,
|
||||||
|
updateOAuth2Client,
|
||||||
|
} from '#/api/system/oauth2/client';
|
||||||
|
import { $t } from '#/locales';
|
||||||
|
|
||||||
|
import { useFormSchema } from '../data';
|
||||||
|
|
||||||
|
const emit = defineEmits(['success']);
|
||||||
|
const formData = ref<SystemOAuth2ClientApi.OAuth2Client>();
|
||||||
|
const getTitle = computed(() => {
|
||||||
|
return formData.value?.id
|
||||||
|
? $t('ui.actionTitle.edit', [' OAuth2.0 客户端'])
|
||||||
|
: $t('ui.actionTitle.create', [' OAuth2.0 客户端']);
|
||||||
|
});
|
||||||
|
|
||||||
|
const [Form, formApi] = useVbenForm({
|
||||||
|
commonConfig: {
|
||||||
|
componentProps: {
|
||||||
|
class: 'w-full',
|
||||||
|
},
|
||||||
|
labelWidth: 140,
|
||||||
|
},
|
||||||
|
wrapperClass: 'grid-cols-2',
|
||||||
|
layout: 'horizontal',
|
||||||
|
schema: useFormSchema(),
|
||||||
|
showDefaultActions: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const [Modal, modalApi] = useVbenModal({
|
||||||
|
async onConfirm() {
|
||||||
|
const { valid } = await formApi.validate();
|
||||||
|
if (!valid) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
modalApi.lock();
|
||||||
|
// 提交表单
|
||||||
|
const data =
|
||||||
|
(await formApi.getValues()) as SystemOAuth2ClientApi.OAuth2Client;
|
||||||
|
try {
|
||||||
|
await (formData.value?.id
|
||||||
|
? updateOAuth2Client(data)
|
||||||
|
: createOAuth2Client(data));
|
||||||
|
// 关闭并提示
|
||||||
|
await modalApi.close();
|
||||||
|
emit('success');
|
||||||
|
message.success($t('ui.actionMessage.operationSuccess'));
|
||||||
|
} finally {
|
||||||
|
modalApi.unlock();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async onOpenChange(isOpen: boolean) {
|
||||||
|
if (!isOpen) {
|
||||||
|
formData.value = undefined;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 加载数据
|
||||||
|
const data = modalApi.getData<SystemOAuth2ClientApi.OAuth2Client>();
|
||||||
|
if (!data || !data.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
modalApi.lock();
|
||||||
|
try {
|
||||||
|
formData.value = await getOAuth2Client(data.id);
|
||||||
|
// 设置到 values
|
||||||
|
await formApi.setValues(formData.value);
|
||||||
|
} finally {
|
||||||
|
modalApi.unlock();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Modal class="w-1/2" :title="getTitle">
|
||||||
|
<Form class="mx-4" />
|
||||||
|
</Modal>
|
||||||
|
</template>
|
||||||
93
apps/web-naive/src/views/system/oauth2/token/data.ts
Normal file
93
apps/web-naive/src/views/system/oauth2/token/data.ts
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
import type { VbenFormSchema } from '#/adapter/form';
|
||||||
|
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||||
|
|
||||||
|
import { DICT_TYPE } from '@vben/constants';
|
||||||
|
import { getDictOptions } from '@vben/hooks';
|
||||||
|
|
||||||
|
/** 列表的搜索表单 */
|
||||||
|
export function useGridFormSchema(): VbenFormSchema[] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
fieldName: 'userId',
|
||||||
|
label: '用户编号',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入用户编号',
|
||||||
|
clearable: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'userType',
|
||||||
|
label: '用户类型',
|
||||||
|
component: 'Select',
|
||||||
|
componentProps: {
|
||||||
|
options: getDictOptions(DICT_TYPE.USER_TYPE, 'number'),
|
||||||
|
placeholder: '请选择用户类型',
|
||||||
|
clearable: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'clientId',
|
||||||
|
label: '客户端编号',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入客户端编号',
|
||||||
|
clearable: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 列表的字段 */
|
||||||
|
export function useGridColumns(): VxeTableGridOptions['columns'] {
|
||||||
|
return [
|
||||||
|
{ type: 'checkbox', width: 40 },
|
||||||
|
{
|
||||||
|
field: 'accessToken',
|
||||||
|
title: '访问令牌',
|
||||||
|
minWidth: 300,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'refreshToken',
|
||||||
|
title: '刷新令牌',
|
||||||
|
minWidth: 300,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'userId',
|
||||||
|
title: '用户编号',
|
||||||
|
minWidth: 100,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'userType',
|
||||||
|
title: '用户类型',
|
||||||
|
minWidth: 100,
|
||||||
|
cellRender: {
|
||||||
|
name: 'CellDict',
|
||||||
|
props: { type: DICT_TYPE.USER_TYPE },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'clientId',
|
||||||
|
title: '客户端编号',
|
||||||
|
minWidth: 120,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'expiresTime',
|
||||||
|
title: '过期时间',
|
||||||
|
minWidth: 180,
|
||||||
|
formatter: 'formatDateTime',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'createTime',
|
||||||
|
title: '创建时间',
|
||||||
|
minWidth: 180,
|
||||||
|
formatter: 'formatDateTime',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '操作',
|
||||||
|
width: 80,
|
||||||
|
fixed: 'right',
|
||||||
|
slots: { default: 'actions' },
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
145
apps/web-naive/src/views/system/oauth2/token/index.vue
Normal file
145
apps/web-naive/src/views/system/oauth2/token/index.vue
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||||
|
import type { SystemOAuth2TokenApi } from '#/api/system/oauth2/token';
|
||||||
|
|
||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
import { confirm, DocAlert, Page } from '@vben/common-ui';
|
||||||
|
import { isEmpty } from '@vben/utils';
|
||||||
|
|
||||||
|
import { message } from '#/adapter/naive';
|
||||||
|
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||||
|
import {
|
||||||
|
deleteOAuth2Token,
|
||||||
|
getOAuth2TokenPage,
|
||||||
|
} from '#/api/system/oauth2/token';
|
||||||
|
import { $t } from '#/locales';
|
||||||
|
|
||||||
|
import { useGridColumns, useGridFormSchema } from './data';
|
||||||
|
|
||||||
|
/** 刷新表格 */
|
||||||
|
function handleRefresh() {
|
||||||
|
gridApi.query();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 删除 OAuth2 令牌 */
|
||||||
|
async function handleDelete(row: SystemOAuth2TokenApi.OAuth2Token) {
|
||||||
|
const hideLoading = message.loading(
|
||||||
|
$t('ui.actionMessage.deleting', ['令牌']),
|
||||||
|
{
|
||||||
|
duration: 0,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
try {
|
||||||
|
await deleteOAuth2Token(row.accessToken);
|
||||||
|
message.success($t('ui.actionMessage.deleteSuccess', ['令牌']));
|
||||||
|
handleRefresh();
|
||||||
|
} finally {
|
||||||
|
hideLoading.destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 批量删除 OAuth2 令牌 */
|
||||||
|
async function handleDeleteBatch() {
|
||||||
|
await confirm($t('ui.actionMessage.deleteBatchConfirm'));
|
||||||
|
const hideLoading = message.loading($t('ui.actionMessage.deletingBatch'), {
|
||||||
|
duration: 0,
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
await deleteOAuth2Token(checkedIds.value.join(','));
|
||||||
|
checkedIds.value = [];
|
||||||
|
message.success($t('ui.actionMessage.deleteSuccess'));
|
||||||
|
handleRefresh();
|
||||||
|
} finally {
|
||||||
|
hideLoading.destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const checkedIds = ref<string[]>([]);
|
||||||
|
function handleRowCheckboxChange({
|
||||||
|
records,
|
||||||
|
}: {
|
||||||
|
records: SystemOAuth2TokenApi.OAuth2Token[];
|
||||||
|
}) {
|
||||||
|
checkedIds.value = records.map((item) => item.accessToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
const [Grid, gridApi] = useVbenVxeGrid({
|
||||||
|
formOptions: {
|
||||||
|
schema: useGridFormSchema(),
|
||||||
|
},
|
||||||
|
gridOptions: {
|
||||||
|
columns: useGridColumns(),
|
||||||
|
height: 'auto',
|
||||||
|
keepSource: true,
|
||||||
|
proxyConfig: {
|
||||||
|
ajax: {
|
||||||
|
query: async ({ page }, formValues) => {
|
||||||
|
return await getOAuth2TokenPage({
|
||||||
|
pageNo: page.currentPage,
|
||||||
|
pageSize: page.pageSize,
|
||||||
|
...formValues,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
rowConfig: {
|
||||||
|
keyField: 'accessToken',
|
||||||
|
isHover: true,
|
||||||
|
},
|
||||||
|
toolbarConfig: {
|
||||||
|
refresh: true,
|
||||||
|
search: true,
|
||||||
|
},
|
||||||
|
} as VxeTableGridOptions<SystemOAuth2TokenApi.OAuth2Token>,
|
||||||
|
gridEvents: {
|
||||||
|
checkboxAll: handleRowCheckboxChange,
|
||||||
|
checkboxChange: handleRowCheckboxChange,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Page auto-content-height>
|
||||||
|
<template #doc>
|
||||||
|
<DocAlert
|
||||||
|
title="OAuth 2.0(SSO 单点登录)"
|
||||||
|
url="https://doc.iocoder.cn/oauth2/"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<Grid table-title="令牌列表">
|
||||||
|
<template #toolbar-tools>
|
||||||
|
<TableAction
|
||||||
|
:actions="[
|
||||||
|
{
|
||||||
|
label: $t('ui.actionTitle.deleteBatch'),
|
||||||
|
type: 'error',
|
||||||
|
icon: ACTION_ICON.DELETE,
|
||||||
|
auth: ['system:oauth2-token:delete'],
|
||||||
|
disabled: isEmpty(checkedIds),
|
||||||
|
onClick: handleDeleteBatch,
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<template #actions="{ row }">
|
||||||
|
<TableAction
|
||||||
|
:actions="[
|
||||||
|
{
|
||||||
|
label: $t('common.delete'),
|
||||||
|
type: 'primary',
|
||||||
|
text: true,
|
||||||
|
icon: ACTION_ICON.DELETE,
|
||||||
|
auth: ['system:oauth2-token:delete'],
|
||||||
|
popConfirm: {
|
||||||
|
title: $t('ui.actionMessage.deleteConfirm', ['令牌']),
|
||||||
|
confirm: handleDelete.bind(null, row),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</Grid>
|
||||||
|
</Page>
|
||||||
|
</template>
|
||||||
195
apps/web-naive/src/views/system/operatelog/data.ts
Normal file
195
apps/web-naive/src/views/system/operatelog/data.ts
Normal file
@@ -0,0 +1,195 @@
|
|||||||
|
import type { VbenFormSchema } from '#/adapter/form';
|
||||||
|
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||||
|
import type { SystemOperateLogApi } from '#/api/system/operate-log';
|
||||||
|
import type { DescriptionItemSchema } from '#/components/description';
|
||||||
|
|
||||||
|
import { formatDateTime } from '@vben/utils';
|
||||||
|
|
||||||
|
import { getSimpleUserList } from '#/api/system/user';
|
||||||
|
import { getRangePickerDefaultProps } from '#/utils';
|
||||||
|
|
||||||
|
/** 列表的搜索表单 */
|
||||||
|
export function useGridFormSchema(): VbenFormSchema[] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
fieldName: 'userId',
|
||||||
|
label: '操作人',
|
||||||
|
component: 'ApiSelect',
|
||||||
|
componentProps: {
|
||||||
|
api: getSimpleUserList,
|
||||||
|
fieldNames: {
|
||||||
|
label: 'nickname',
|
||||||
|
value: 'id',
|
||||||
|
},
|
||||||
|
clearable: true,
|
||||||
|
placeholder: '请选择操作人员',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'type',
|
||||||
|
label: '操作模块',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
clearable: true,
|
||||||
|
placeholder: '请输入操作模块',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'subType',
|
||||||
|
label: '操作名',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
clearable: true,
|
||||||
|
placeholder: '请输入操作名',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'action',
|
||||||
|
label: '操作内容',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
clearable: true,
|
||||||
|
placeholder: '请输入操作内容',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'createTime',
|
||||||
|
label: '操作时间',
|
||||||
|
component: 'DatePicker',
|
||||||
|
componentProps: {
|
||||||
|
...getRangePickerDefaultProps(),
|
||||||
|
clearable: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'bizId',
|
||||||
|
label: '业务编号',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
clearable: true,
|
||||||
|
placeholder: '请输入业务编号',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 列表的字段 */
|
||||||
|
export function useGridColumns(): VxeTableGridOptions['columns'] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
field: 'id',
|
||||||
|
title: '日志编号',
|
||||||
|
minWidth: 100,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'userName',
|
||||||
|
title: '操作人',
|
||||||
|
minWidth: 120,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'type',
|
||||||
|
title: '操作模块',
|
||||||
|
minWidth: 120,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'subType',
|
||||||
|
title: '操作名',
|
||||||
|
minWidth: 160,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'action',
|
||||||
|
title: '操作内容',
|
||||||
|
minWidth: 200,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'createTime',
|
||||||
|
title: '操作时间',
|
||||||
|
minWidth: 180,
|
||||||
|
formatter: 'formatDateTime',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'bizId',
|
||||||
|
title: '业务编号',
|
||||||
|
minWidth: 120,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'userIp',
|
||||||
|
title: '操作 IP',
|
||||||
|
minWidth: 120,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '操作',
|
||||||
|
width: 80,
|
||||||
|
fixed: 'right',
|
||||||
|
slots: { default: 'actions' },
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 详情页的字段 */
|
||||||
|
export function useDetailSchema(): DescriptionItemSchema[] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
field: 'id',
|
||||||
|
label: '日志编号',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'traceId',
|
||||||
|
label: '链路追踪',
|
||||||
|
hidden: (data: SystemOperateLogApi.OperateLog) => !data?.traceId,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'userId',
|
||||||
|
label: '操作人编号',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'userName',
|
||||||
|
label: '操作人名字',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'userIp',
|
||||||
|
label: '操作人 IP',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'userAgent',
|
||||||
|
label: '操作人 UA',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'type',
|
||||||
|
label: '操作模块',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'subType',
|
||||||
|
label: '操作名',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'action',
|
||||||
|
label: '操作内容',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'extra',
|
||||||
|
label: '操作拓展参数',
|
||||||
|
hidden: (data: SystemOperateLogApi.OperateLog) => !data?.extra,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '请求 URL',
|
||||||
|
content: (data: SystemOperateLogApi.OperateLog) => {
|
||||||
|
if (data?.requestMethod && data?.requestUrl) {
|
||||||
|
return `${data.requestMethod} ${data.requestUrl}`;
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'createTime',
|
||||||
|
label: '操作时间',
|
||||||
|
content: (data: SystemOperateLogApi.OperateLog) => {
|
||||||
|
return formatDateTime(data?.createTime || '') as string;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'bizId',
|
||||||
|
label: '业务编号',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
104
apps/web-naive/src/views/system/operatelog/index.vue
Normal file
104
apps/web-naive/src/views/system/operatelog/index.vue
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||||
|
import type { SystemOperateLogApi } from '#/api/system/operate-log';
|
||||||
|
|
||||||
|
import { DocAlert, Page, useVbenModal } from '@vben/common-ui';
|
||||||
|
import { downloadFileFromBlobPart } from '@vben/utils';
|
||||||
|
|
||||||
|
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||||
|
import { exportOperateLog, getOperateLogPage } from '#/api/system/operate-log';
|
||||||
|
import { $t } from '#/locales';
|
||||||
|
|
||||||
|
import { useGridColumns, useGridFormSchema } from './data';
|
||||||
|
import Detail from './modules/detail.vue';
|
||||||
|
|
||||||
|
const [DetailModal, detailModalApi] = useVbenModal({
|
||||||
|
connectedComponent: Detail,
|
||||||
|
destroyOnClose: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 刷新表格 */
|
||||||
|
function handleRefresh() {
|
||||||
|
gridApi.query();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 导出表格 */
|
||||||
|
async function handleExport() {
|
||||||
|
const data = await exportOperateLog(await gridApi.formApi.getValues());
|
||||||
|
downloadFileFromBlobPart({ fileName: '操作日志.xls', source: data });
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 查看操作日志详情 */
|
||||||
|
function handleDetail(row: SystemOperateLogApi.OperateLog) {
|
||||||
|
detailModalApi.setData(row).open();
|
||||||
|
}
|
||||||
|
|
||||||
|
const [Grid, gridApi] = useVbenVxeGrid({
|
||||||
|
formOptions: {
|
||||||
|
schema: useGridFormSchema(),
|
||||||
|
},
|
||||||
|
gridOptions: {
|
||||||
|
columns: useGridColumns(),
|
||||||
|
height: 'auto',
|
||||||
|
keepSource: true,
|
||||||
|
proxyConfig: {
|
||||||
|
ajax: {
|
||||||
|
query: async ({ page }, formValues) => {
|
||||||
|
return await getOperateLogPage({
|
||||||
|
pageNo: page.currentPage,
|
||||||
|
pageSize: page.pageSize,
|
||||||
|
...formValues,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
rowConfig: {
|
||||||
|
keyField: 'id',
|
||||||
|
isHover: true,
|
||||||
|
},
|
||||||
|
toolbarConfig: {
|
||||||
|
refresh: true,
|
||||||
|
search: true,
|
||||||
|
},
|
||||||
|
} as VxeTableGridOptions<SystemOperateLogApi.OperateLog>,
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Page auto-content-height>
|
||||||
|
<template #doc>
|
||||||
|
<DocAlert title="系统日志" url="https://doc.iocoder.cn/system-log/" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<DetailModal @success="handleRefresh" />
|
||||||
|
<Grid table-title="操作日志列表">
|
||||||
|
<template #toolbar-tools>
|
||||||
|
<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: 'primary',
|
||||||
|
text: true,
|
||||||
|
icon: ACTION_ICON.VIEW,
|
||||||
|
auth: ['system:operate-log:query'],
|
||||||
|
onClick: handleDetail.bind(null, row),
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</Grid>
|
||||||
|
</Page>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { SystemOperateLogApi } from '#/api/system/operate-log';
|
||||||
|
|
||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
import { useVbenModal } from '@vben/common-ui';
|
||||||
|
|
||||||
|
import { useDescription } from '#/components/description';
|
||||||
|
|
||||||
|
import { useDetailSchema } from '../data';
|
||||||
|
|
||||||
|
const formData = ref<SystemOperateLogApi.OperateLog>();
|
||||||
|
|
||||||
|
const [Descriptions] = useDescription({
|
||||||
|
componentProps: {
|
||||||
|
bordered: true,
|
||||||
|
column: 1,
|
||||||
|
contentClass: 'mx-4',
|
||||||
|
},
|
||||||
|
schema: useDetailSchema(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const [Modal, modalApi] = useVbenModal({
|
||||||
|
async onOpenChange(isOpen: boolean) {
|
||||||
|
if (!isOpen) {
|
||||||
|
formData.value = undefined;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 加载数据
|
||||||
|
const data = modalApi.getData<SystemOperateLogApi.OperateLog>();
|
||||||
|
if (!data || !data.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
modalApi.lock();
|
||||||
|
try {
|
||||||
|
formData.value = data;
|
||||||
|
} finally {
|
||||||
|
modalApi.unlock();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Modal
|
||||||
|
title="操作日志详情"
|
||||||
|
class="w-1/2"
|
||||||
|
:show-cancel-button="false"
|
||||||
|
:show-confirm-button="false"
|
||||||
|
>
|
||||||
|
<Descriptions :data="formData" />
|
||||||
|
</Modal>
|
||||||
|
</template>
|
||||||
148
apps/web-naive/src/views/system/post/data.ts
Normal file
148
apps/web-naive/src/views/system/post/data.ts
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
import type { VbenFormSchema } from '#/adapter/form';
|
||||||
|
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||||
|
|
||||||
|
import { CommonStatusEnum, DICT_TYPE } from '@vben/constants';
|
||||||
|
import { getDictOptions } from '@vben/hooks';
|
||||||
|
|
||||||
|
import { z } from '#/adapter/form';
|
||||||
|
|
||||||
|
/** 新增/修改的表单 */
|
||||||
|
export function useFormSchema(): VbenFormSchema[] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
component: 'Input',
|
||||||
|
fieldName: 'id',
|
||||||
|
dependencies: {
|
||||||
|
triggerFields: [''],
|
||||||
|
show: () => false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: 'Input',
|
||||||
|
fieldName: 'name',
|
||||||
|
label: '岗位名称',
|
||||||
|
rules: 'required',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: 'Input',
|
||||||
|
fieldName: 'code',
|
||||||
|
label: '岗位编码',
|
||||||
|
rules: 'required',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'sort',
|
||||||
|
label: '显示顺序',
|
||||||
|
component: 'InputNumber',
|
||||||
|
componentProps: {
|
||||||
|
min: 0,
|
||||||
|
},
|
||||||
|
rules: 'required',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'status',
|
||||||
|
label: '岗位状态',
|
||||||
|
component: 'RadioGroup',
|
||||||
|
componentProps: {
|
||||||
|
options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
|
||||||
|
buttonStyle: 'solid',
|
||||||
|
optionType: 'button',
|
||||||
|
},
|
||||||
|
rules: z.number().default(CommonStatusEnum.ENABLE),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'remark',
|
||||||
|
label: '岗位备注',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
type: 'textarea',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 列表的搜索表单 */
|
||||||
|
export function useGridFormSchema(): VbenFormSchema[] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
fieldName: 'name',
|
||||||
|
label: '岗位名称',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入岗位名称',
|
||||||
|
clearable: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'code',
|
||||||
|
label: '岗位编码',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入岗位编码',
|
||||||
|
clearable: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'status',
|
||||||
|
label: '岗位状态',
|
||||||
|
component: 'Select',
|
||||||
|
componentProps: {
|
||||||
|
options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
|
||||||
|
placeholder: '请选择岗位状态',
|
||||||
|
clearable: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 列表的字段 */
|
||||||
|
export function useGridColumns(): VxeTableGridOptions['columns'] {
|
||||||
|
return [
|
||||||
|
{ type: 'checkbox', width: 40 },
|
||||||
|
{
|
||||||
|
field: 'id',
|
||||||
|
title: '岗位编号',
|
||||||
|
minWidth: 200,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'name',
|
||||||
|
title: '岗位名称',
|
||||||
|
minWidth: 200,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'code',
|
||||||
|
title: '岗位编码',
|
||||||
|
minWidth: 200,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'sort',
|
||||||
|
title: '显示顺序',
|
||||||
|
minWidth: 100,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'remark',
|
||||||
|
title: '岗位备注',
|
||||||
|
minWidth: 200,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'status',
|
||||||
|
title: '岗位状态',
|
||||||
|
minWidth: 100,
|
||||||
|
cellRender: {
|
||||||
|
name: 'CellDict',
|
||||||
|
props: { type: DICT_TYPE.COMMON_STATUS },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'createTime',
|
||||||
|
title: '创建时间',
|
||||||
|
minWidth: 180,
|
||||||
|
formatter: 'formatDateTime',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '操作',
|
||||||
|
width: 130,
|
||||||
|
fixed: 'right',
|
||||||
|
slots: { default: 'actions' },
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
183
apps/web-naive/src/views/system/post/index.vue
Normal file
183
apps/web-naive/src/views/system/post/index.vue
Normal file
@@ -0,0 +1,183 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||||
|
import type { SystemPostApi } from '#/api/system/post';
|
||||||
|
|
||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
import { confirm, Page, useVbenModal } from '@vben/common-ui';
|
||||||
|
import { downloadFileFromBlobPart, isEmpty } from '@vben/utils';
|
||||||
|
|
||||||
|
import { message } from '#/adapter/naive';
|
||||||
|
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||||
|
import {
|
||||||
|
deletePost,
|
||||||
|
deletePostList,
|
||||||
|
exportPost,
|
||||||
|
getPostPage,
|
||||||
|
} from '#/api/system/post';
|
||||||
|
import { $t } from '#/locales';
|
||||||
|
|
||||||
|
import { useGridColumns, useGridFormSchema } from './data';
|
||||||
|
import Form from './modules/form.vue';
|
||||||
|
|
||||||
|
const [FormModal, formModalApi] = useVbenModal({
|
||||||
|
connectedComponent: Form,
|
||||||
|
destroyOnClose: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 刷新表格 */
|
||||||
|
function handleRefresh() {
|
||||||
|
gridApi.query();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 导出表格 */
|
||||||
|
async function handleExport() {
|
||||||
|
const data = await exportPost(await gridApi.formApi.getValues());
|
||||||
|
downloadFileFromBlobPart({ fileName: '岗位.xls', source: data });
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 创建岗位 */
|
||||||
|
function handleCreate() {
|
||||||
|
formModalApi.setData(null).open();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 编辑岗位 */
|
||||||
|
function handleEdit(row: SystemPostApi.Post) {
|
||||||
|
formModalApi.setData(row).open();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 删除岗位 */
|
||||||
|
async function handleDelete(row: SystemPostApi.Post) {
|
||||||
|
const hideLoading = message.loading(
|
||||||
|
$t('ui.actionMessage.deleting', [row.name]),
|
||||||
|
{ duration: 0 },
|
||||||
|
);
|
||||||
|
try {
|
||||||
|
await deletePost(row.id!);
|
||||||
|
message.success($t('ui.actionMessage.deleteSuccess', [row.name]));
|
||||||
|
handleRefresh();
|
||||||
|
} finally {
|
||||||
|
hideLoading.destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 批量删除岗位 */
|
||||||
|
async function handleDeleteBatch() {
|
||||||
|
await confirm($t('ui.actionMessage.deleteBatchConfirm'));
|
||||||
|
const hideLoading = message.loading($t('ui.actionMessage.deletingBatch'), {
|
||||||
|
duration: 0,
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
await deletePostList(checkedIds.value);
|
||||||
|
checkedIds.value = [];
|
||||||
|
message.success($t('ui.actionMessage.deleteSuccess'));
|
||||||
|
handleRefresh();
|
||||||
|
} finally {
|
||||||
|
hideLoading.destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const checkedIds = ref<number[]>([]);
|
||||||
|
function handleRowCheckboxChange({
|
||||||
|
records,
|
||||||
|
}: {
|
||||||
|
records: SystemPostApi.Post[];
|
||||||
|
}) {
|
||||||
|
checkedIds.value = records.map((item) => item.id!);
|
||||||
|
}
|
||||||
|
|
||||||
|
const [Grid, gridApi] = useVbenVxeGrid({
|
||||||
|
formOptions: {
|
||||||
|
schema: useGridFormSchema(),
|
||||||
|
},
|
||||||
|
gridOptions: {
|
||||||
|
columns: useGridColumns(),
|
||||||
|
height: 'auto',
|
||||||
|
keepSource: true,
|
||||||
|
proxyConfig: {
|
||||||
|
ajax: {
|
||||||
|
query: async ({ page }, formValues) => {
|
||||||
|
return await getPostPage({
|
||||||
|
pageNo: page.currentPage,
|
||||||
|
pageSize: page.pageSize,
|
||||||
|
...formValues,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
rowConfig: {
|
||||||
|
keyField: 'id',
|
||||||
|
isHover: true,
|
||||||
|
},
|
||||||
|
toolbarConfig: {
|
||||||
|
refresh: true,
|
||||||
|
search: true,
|
||||||
|
},
|
||||||
|
} as VxeTableGridOptions<SystemPostApi.Post>,
|
||||||
|
gridEvents: {
|
||||||
|
checkboxAll: handleRowCheckboxChange,
|
||||||
|
checkboxChange: handleRowCheckboxChange,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Page auto-content-height>
|
||||||
|
<FormModal @success="handleRefresh" />
|
||||||
|
<Grid table-title="岗位列表">
|
||||||
|
<template #toolbar-tools>
|
||||||
|
<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,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: $t('ui.actionTitle.deleteBatch'),
|
||||||
|
type: 'error',
|
||||||
|
icon: ACTION_ICON.DELETE,
|
||||||
|
auth: ['system:post:delete'],
|
||||||
|
disabled: isEmpty(checkedIds),
|
||||||
|
onClick: handleDeleteBatch,
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<template #actions="{ row }">
|
||||||
|
<TableAction
|
||||||
|
:actions="[
|
||||||
|
{
|
||||||
|
label: $t('common.edit'),
|
||||||
|
type: 'primary',
|
||||||
|
text: true,
|
||||||
|
icon: ACTION_ICON.EDIT,
|
||||||
|
auth: ['system:post:update'],
|
||||||
|
onClick: handleEdit.bind(null, row),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: $t('common.delete'),
|
||||||
|
type: 'error',
|
||||||
|
text: 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>
|
||||||
|
</template>
|
||||||
81
apps/web-naive/src/views/system/post/modules/form.vue
Normal file
81
apps/web-naive/src/views/system/post/modules/form.vue
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { SystemPostApi } from '#/api/system/post';
|
||||||
|
|
||||||
|
import { computed, ref } from 'vue';
|
||||||
|
|
||||||
|
import { useVbenModal } from '@vben/common-ui';
|
||||||
|
|
||||||
|
import { useVbenForm } from '#/adapter/form';
|
||||||
|
import { message } from '#/adapter/naive';
|
||||||
|
import { createPost, getPost, updatePost } from '#/api/system/post';
|
||||||
|
import { $t } from '#/locales';
|
||||||
|
|
||||||
|
import { useFormSchema } from '../data';
|
||||||
|
|
||||||
|
const emit = defineEmits(['success']);
|
||||||
|
const formData = ref<SystemPostApi.Post>();
|
||||||
|
const getTitle = computed(() => {
|
||||||
|
return formData.value?.id
|
||||||
|
? $t('ui.actionTitle.edit', ['岗位'])
|
||||||
|
: $t('ui.actionTitle.create', ['岗位']);
|
||||||
|
});
|
||||||
|
|
||||||
|
const [Form, formApi] = useVbenForm({
|
||||||
|
commonConfig: {
|
||||||
|
componentProps: {
|
||||||
|
class: 'w-full',
|
||||||
|
},
|
||||||
|
formItemClass: 'col-span-2',
|
||||||
|
labelWidth: 80,
|
||||||
|
},
|
||||||
|
layout: 'horizontal',
|
||||||
|
schema: useFormSchema(),
|
||||||
|
showDefaultActions: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const [Modal, modalApi] = useVbenModal({
|
||||||
|
async onConfirm() {
|
||||||
|
const { valid } = await formApi.validate();
|
||||||
|
if (!valid) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
modalApi.lock();
|
||||||
|
// 提交表单
|
||||||
|
const data = (await formApi.getValues()) as SystemPostApi.Post;
|
||||||
|
try {
|
||||||
|
await (formData.value?.id ? updatePost(data) : createPost(data));
|
||||||
|
// 关闭并提示
|
||||||
|
await modalApi.close();
|
||||||
|
emit('success');
|
||||||
|
message.success($t('ui.actionMessage.operationSuccess'));
|
||||||
|
} finally {
|
||||||
|
modalApi.unlock();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async onOpenChange(isOpen: boolean) {
|
||||||
|
if (!isOpen) {
|
||||||
|
formData.value = undefined;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 加载数据
|
||||||
|
const data = modalApi.getData<SystemPostApi.Post>();
|
||||||
|
if (!data || !data.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
modalApi.lock();
|
||||||
|
try {
|
||||||
|
formData.value = await getPost(data.id);
|
||||||
|
// 设置到 values
|
||||||
|
await formApi.setValues(formData.value);
|
||||||
|
} finally {
|
||||||
|
modalApi.unlock();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Modal class="w-[600px]" :title="getTitle">
|
||||||
|
<Form class="mx-4" />
|
||||||
|
</Modal>
|
||||||
|
</template>
|
||||||
258
apps/web-naive/src/views/system/role/data.ts
Normal file
258
apps/web-naive/src/views/system/role/data.ts
Normal file
@@ -0,0 +1,258 @@
|
|||||||
|
import type { VbenFormSchema } from '#/adapter/form';
|
||||||
|
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||||
|
|
||||||
|
import {
|
||||||
|
CommonStatusEnum,
|
||||||
|
DICT_TYPE,
|
||||||
|
SystemDataScopeEnum,
|
||||||
|
} from '@vben/constants';
|
||||||
|
import { getDictOptions } from '@vben/hooks';
|
||||||
|
|
||||||
|
import { z } from '#/adapter/form';
|
||||||
|
import { getRangePickerDefaultProps } from '#/utils';
|
||||||
|
|
||||||
|
/** 新增/修改的表单 */
|
||||||
|
export function useFormSchema(): VbenFormSchema[] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
fieldName: 'id',
|
||||||
|
component: 'Input',
|
||||||
|
dependencies: {
|
||||||
|
triggerFields: [''],
|
||||||
|
show: () => false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'name',
|
||||||
|
label: '角色名称',
|
||||||
|
component: 'Input',
|
||||||
|
rules: 'required',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'code',
|
||||||
|
label: '角色标识',
|
||||||
|
component: 'Input',
|
||||||
|
rules: 'required',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'sort',
|
||||||
|
label: '显示顺序',
|
||||||
|
component: 'InputNumber',
|
||||||
|
componentProps: {
|
||||||
|
min: 0,
|
||||||
|
placeholder: '请输入显示顺序',
|
||||||
|
},
|
||||||
|
rules: 'required',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'status',
|
||||||
|
label: '角色状态',
|
||||||
|
component: 'RadioGroup',
|
||||||
|
componentProps: {
|
||||||
|
options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
|
||||||
|
buttonStyle: 'solid',
|
||||||
|
optionType: 'button',
|
||||||
|
},
|
||||||
|
rules: z.number().default(CommonStatusEnum.ENABLE),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'remark',
|
||||||
|
label: '角色备注',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
type: 'textarea',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 分配数据权限的表单 */
|
||||||
|
export function useAssignDataPermissionFormSchema(): VbenFormSchema[] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
component: 'Input',
|
||||||
|
fieldName: 'id',
|
||||||
|
dependencies: {
|
||||||
|
triggerFields: [''],
|
||||||
|
show: () => false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'name',
|
||||||
|
label: '角色名称',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
disabled: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: 'Input',
|
||||||
|
fieldName: 'code',
|
||||||
|
label: '角色标识',
|
||||||
|
componentProps: {
|
||||||
|
disabled: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: 'Select',
|
||||||
|
fieldName: 'dataScope',
|
||||||
|
label: '权限范围',
|
||||||
|
componentProps: {
|
||||||
|
options: getDictOptions(DICT_TYPE.SYSTEM_DATA_SCOPE, 'number'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'dataScopeDeptIds',
|
||||||
|
label: '部门范围',
|
||||||
|
component: 'Input',
|
||||||
|
formItemClass: 'items-start',
|
||||||
|
dependencies: {
|
||||||
|
triggerFields: ['dataScope'],
|
||||||
|
show: (values) => {
|
||||||
|
return values.dataScope === SystemDataScopeEnum.DEPT_CUSTOM;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 分配菜单的表单 */
|
||||||
|
export function useAssignMenuFormSchema(): VbenFormSchema[] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
fieldName: 'id',
|
||||||
|
component: 'Input',
|
||||||
|
dependencies: {
|
||||||
|
triggerFields: [''],
|
||||||
|
show: () => false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'name',
|
||||||
|
label: '角色名称',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
disabled: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'code',
|
||||||
|
label: '角色标识',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
disabled: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'menuIds',
|
||||||
|
label: '菜单权限',
|
||||||
|
component: 'Input',
|
||||||
|
formItemClass: 'items-start',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 列表的搜索表单 */
|
||||||
|
export function useGridFormSchema(): VbenFormSchema[] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
fieldName: 'name',
|
||||||
|
label: '角色名称',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入角色名称',
|
||||||
|
clearable: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'code',
|
||||||
|
label: '角色标识',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入角色标识',
|
||||||
|
clearable: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'status',
|
||||||
|
label: '角色状态',
|
||||||
|
component: 'Select',
|
||||||
|
componentProps: {
|
||||||
|
options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
|
||||||
|
placeholder: '请选择角色状态',
|
||||||
|
clearable: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'createTime',
|
||||||
|
label: '创建时间',
|
||||||
|
component: 'DatePicker',
|
||||||
|
componentProps: {
|
||||||
|
...getRangePickerDefaultProps(),
|
||||||
|
clearable: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 列表的字段 */
|
||||||
|
export function useGridColumns(): VxeTableGridOptions['columns'] {
|
||||||
|
return [
|
||||||
|
{ type: 'checkbox', width: 40 },
|
||||||
|
{
|
||||||
|
field: 'id',
|
||||||
|
title: '角色编号',
|
||||||
|
minWidth: 100,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'name',
|
||||||
|
title: '角色名称',
|
||||||
|
minWidth: 200,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'type',
|
||||||
|
title: '角色类型',
|
||||||
|
minWidth: 100,
|
||||||
|
cellRender: {
|
||||||
|
name: 'CellDict',
|
||||||
|
props: { type: DICT_TYPE.SYSTEM_ROLE_TYPE },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'code',
|
||||||
|
title: '角色标识',
|
||||||
|
minWidth: 200,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'sort',
|
||||||
|
title: '显示顺序',
|
||||||
|
minWidth: 100,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'remark',
|
||||||
|
title: '角色备注',
|
||||||
|
minWidth: 100,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'status',
|
||||||
|
title: '角色状态',
|
||||||
|
minWidth: 100,
|
||||||
|
cellRender: {
|
||||||
|
name: 'CellDict',
|
||||||
|
props: { type: DICT_TYPE.COMMON_STATUS },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'createTime',
|
||||||
|
title: '创建时间',
|
||||||
|
minWidth: 180,
|
||||||
|
formatter: 'formatDateTime',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '操作',
|
||||||
|
width: 240,
|
||||||
|
fixed: 'right',
|
||||||
|
slots: { default: 'actions' },
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
232
apps/web-naive/src/views/system/role/index.vue
Normal file
232
apps/web-naive/src/views/system/role/index.vue
Normal file
@@ -0,0 +1,232 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||||
|
import type { SystemRoleApi } from '#/api/system/role';
|
||||||
|
|
||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
import { confirm, DocAlert, Page, useVbenModal } from '@vben/common-ui';
|
||||||
|
import { downloadFileFromBlobPart, isEmpty } from '@vben/utils';
|
||||||
|
|
||||||
|
import { message } from '#/adapter/naive';
|
||||||
|
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||||
|
import {
|
||||||
|
deleteRole,
|
||||||
|
deleteRoleList,
|
||||||
|
exportRole,
|
||||||
|
getRolePage,
|
||||||
|
} from '#/api/system/role';
|
||||||
|
import { $t } from '#/locales';
|
||||||
|
|
||||||
|
import { useGridColumns, useGridFormSchema } from './data';
|
||||||
|
import AssignDataPermissionForm from './modules/assign-data-permission-form.vue';
|
||||||
|
import AssignMenuForm from './modules/assign-menu-form.vue';
|
||||||
|
import Form from './modules/form.vue';
|
||||||
|
|
||||||
|
const [FormModal, formModalApi] = useVbenModal({
|
||||||
|
connectedComponent: Form,
|
||||||
|
destroyOnClose: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const [AssignDataPermissionFormModel, assignDataPermissionFormApi] =
|
||||||
|
useVbenModal({
|
||||||
|
connectedComponent: AssignDataPermissionForm,
|
||||||
|
destroyOnClose: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const [AssignMenuFormModel, assignMenuFormApi] = useVbenModal({
|
||||||
|
connectedComponent: AssignMenuForm,
|
||||||
|
destroyOnClose: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 刷新表格 */
|
||||||
|
function handleRefresh() {
|
||||||
|
gridApi.query();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 导出表格 */
|
||||||
|
async function handleExport() {
|
||||||
|
const data = await exportRole(await gridApi.formApi.getValues());
|
||||||
|
downloadFileFromBlobPart({ fileName: '角色.xls', source: data });
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 创建角色 */
|
||||||
|
function handleCreate() {
|
||||||
|
formModalApi.setData(null).open();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 编辑角色 */
|
||||||
|
function handleEdit(row: SystemRoleApi.Role) {
|
||||||
|
formModalApi.setData(row).open();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 删除角色 */
|
||||||
|
async function handleDelete(row: SystemRoleApi.Role) {
|
||||||
|
const hideLoading = message.loading(
|
||||||
|
$t('ui.actionMessage.deleting', [row.name]),
|
||||||
|
{ duration: 0 },
|
||||||
|
);
|
||||||
|
try {
|
||||||
|
await deleteRole(row.id!);
|
||||||
|
message.success($t('ui.actionMessage.deleteSuccess', [row.name]));
|
||||||
|
handleRefresh();
|
||||||
|
} finally {
|
||||||
|
hideLoading.destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 批量删除角色 */
|
||||||
|
async function handleDeleteBatch() {
|
||||||
|
await confirm($t('ui.actionMessage.deleteBatchConfirm'));
|
||||||
|
const hideLoading = message.loading($t('ui.actionMessage.deletingBatch'), {
|
||||||
|
duration: 0,
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
await deleteRoleList(checkedIds.value);
|
||||||
|
checkedIds.value = [];
|
||||||
|
message.success($t('ui.actionMessage.deleteSuccess'));
|
||||||
|
handleRefresh();
|
||||||
|
} finally {
|
||||||
|
hideLoading.destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const checkedIds = ref<number[]>([]);
|
||||||
|
function handleRowCheckboxChange({
|
||||||
|
records,
|
||||||
|
}: {
|
||||||
|
records: SystemRoleApi.Role[];
|
||||||
|
}) {
|
||||||
|
checkedIds.value = records.map((item) => item.id!);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 分配角色的数据权限 */
|
||||||
|
function handleAssignDataPermission(row: SystemRoleApi.Role) {
|
||||||
|
assignDataPermissionFormApi.setData(row).open();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 分配角色的菜单权限 */
|
||||||
|
function handleAssignMenu(row: SystemRoleApi.Role) {
|
||||||
|
assignMenuFormApi.setData(row).open();
|
||||||
|
}
|
||||||
|
|
||||||
|
const [Grid, gridApi] = useVbenVxeGrid({
|
||||||
|
formOptions: {
|
||||||
|
schema: useGridFormSchema(),
|
||||||
|
},
|
||||||
|
gridOptions: {
|
||||||
|
columns: useGridColumns(),
|
||||||
|
height: 'auto',
|
||||||
|
keepSource: true,
|
||||||
|
proxyConfig: {
|
||||||
|
ajax: {
|
||||||
|
query: async ({ page }, formValues) => {
|
||||||
|
return await getRolePage({
|
||||||
|
pageNo: page.currentPage,
|
||||||
|
pageSize: page.pageSize,
|
||||||
|
...formValues,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
rowConfig: {
|
||||||
|
keyField: 'id',
|
||||||
|
isHover: true,
|
||||||
|
},
|
||||||
|
toolbarConfig: {
|
||||||
|
refresh: true,
|
||||||
|
search: true,
|
||||||
|
},
|
||||||
|
} as VxeTableGridOptions<SystemRoleApi.Role>,
|
||||||
|
gridEvents: {
|
||||||
|
checkboxAll: handleRowCheckboxChange,
|
||||||
|
checkboxChange: handleRowCheckboxChange,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Page auto-content-height>
|
||||||
|
<template #doc>
|
||||||
|
<DocAlert
|
||||||
|
title="功能权限"
|
||||||
|
url="https://doc.iocoder.cn/resource-permission"
|
||||||
|
/>
|
||||||
|
<DocAlert title="数据权限" url="https://doc.iocoder.cn/data-permission" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<FormModal @success="handleRefresh" />
|
||||||
|
<AssignDataPermissionFormModel @success="handleRefresh" />
|
||||||
|
<AssignMenuFormModel @success="handleRefresh" />
|
||||||
|
<Grid table-title="角色列表">
|
||||||
|
<template #toolbar-tools>
|
||||||
|
<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,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: $t('ui.actionTitle.deleteBatch'),
|
||||||
|
type: 'error',
|
||||||
|
icon: ACTION_ICON.DELETE,
|
||||||
|
disabled: isEmpty(checkedIds),
|
||||||
|
auth: ['system:role:delete'],
|
||||||
|
onClick: handleDeleteBatch,
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<template #actions="{ row }">
|
||||||
|
<TableAction
|
||||||
|
:actions="[
|
||||||
|
{
|
||||||
|
label: $t('common.edit'),
|
||||||
|
type: 'primary',
|
||||||
|
text: true,
|
||||||
|
icon: ACTION_ICON.EDIT,
|
||||||
|
auth: ['system:role:update'],
|
||||||
|
onClick: handleEdit.bind(null, row),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: $t('common.delete'),
|
||||||
|
type: 'error',
|
||||||
|
text: true,
|
||||||
|
icon: ACTION_ICON.DELETE,
|
||||||
|
auth: ['system:role:delete'],
|
||||||
|
popConfirm: {
|
||||||
|
title: $t('ui.actionMessage.deleteConfirm', [row.name]),
|
||||||
|
confirm: handleDelete.bind(null, row),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
:drop-down-actions="[
|
||||||
|
{
|
||||||
|
label: '数据权限',
|
||||||
|
type: 'primary',
|
||||||
|
text: true,
|
||||||
|
auth: ['system:permission:assign-role-data-scope'],
|
||||||
|
onClick: handleAssignDataPermission.bind(null, row),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '菜单权限',
|
||||||
|
type: 'primary',
|
||||||
|
text: true,
|
||||||
|
auth: ['system:permission:assign-role-menu'],
|
||||||
|
onClick: handleAssignMenu.bind(null, row),
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</Grid>
|
||||||
|
</Page>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,166 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { SystemDeptApi } from '#/api/system/dept';
|
||||||
|
import type { SystemRoleApi } from '#/api/system/role';
|
||||||
|
|
||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
import { Tree, useVbenModal } from '@vben/common-ui';
|
||||||
|
import { SystemDataScopeEnum } from '@vben/constants';
|
||||||
|
import { handleTree } from '@vben/utils';
|
||||||
|
|
||||||
|
import { NCheckbox, NSpin } from 'naive-ui';
|
||||||
|
|
||||||
|
import { useVbenForm } from '#/adapter/form';
|
||||||
|
import { message } from '#/adapter/naive';
|
||||||
|
import { getDeptList } from '#/api/system/dept';
|
||||||
|
import { assignRoleDataScope } from '#/api/system/permission';
|
||||||
|
import { getRole } from '#/api/system/role';
|
||||||
|
import { $t } from '#/locales';
|
||||||
|
|
||||||
|
import { useAssignDataPermissionFormSchema } from '../data';
|
||||||
|
|
||||||
|
const emit = defineEmits(['success']);
|
||||||
|
|
||||||
|
const deptTree = ref<SystemDeptApi.Dept[]>([]); // 部门树
|
||||||
|
const deptLoading = ref(false); // 加载部门列表
|
||||||
|
const isAllSelected = ref(false); // 全选状态
|
||||||
|
const isExpanded = ref(false); // 展开状态
|
||||||
|
const isCheckStrictly = ref(true); // 父子联动状态
|
||||||
|
const expandedKeys = ref<number[]>([]); // 展开的节点
|
||||||
|
|
||||||
|
const [Form, formApi] = useVbenForm({
|
||||||
|
commonConfig: {
|
||||||
|
componentProps: {
|
||||||
|
class: 'w-full',
|
||||||
|
},
|
||||||
|
formItemClass: 'col-span-2',
|
||||||
|
labelWidth: 80,
|
||||||
|
},
|
||||||
|
layout: 'horizontal',
|
||||||
|
schema: useAssignDataPermissionFormSchema(),
|
||||||
|
showDefaultActions: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const [Modal, modalApi] = useVbenModal({
|
||||||
|
async onConfirm() {
|
||||||
|
const { valid } = await formApi.validate();
|
||||||
|
if (!valid) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
modalApi.lock();
|
||||||
|
const data = await formApi.getValues();
|
||||||
|
try {
|
||||||
|
await assignRoleDataScope({
|
||||||
|
roleId: data.id,
|
||||||
|
dataScope: data.dataScope,
|
||||||
|
dataScopeDeptIds:
|
||||||
|
data.dataScope === SystemDataScopeEnum.DEPT_CUSTOM
|
||||||
|
? data.dataScopeDeptIds
|
||||||
|
: undefined,
|
||||||
|
});
|
||||||
|
await modalApi.close();
|
||||||
|
emit('success');
|
||||||
|
message.success($t('ui.actionMessage.operationSuccess'));
|
||||||
|
} finally {
|
||||||
|
modalApi.unlock();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async onOpenChange(isOpen: boolean) {
|
||||||
|
if (!isOpen) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const data = modalApi.getData<SystemRoleApi.Role>();
|
||||||
|
if (!data || !data.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
modalApi.lock();
|
||||||
|
try {
|
||||||
|
// 加载部门列表
|
||||||
|
await loadDeptTree();
|
||||||
|
handleExpandAll();
|
||||||
|
// 设置表单值,一定要在加载树之后
|
||||||
|
await formApi.setValues(await getRole(data.id));
|
||||||
|
} finally {
|
||||||
|
modalApi.unlock();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 加载部门树 */
|
||||||
|
async function loadDeptTree() {
|
||||||
|
deptLoading.value = true;
|
||||||
|
try {
|
||||||
|
const data = await getDeptList();
|
||||||
|
deptTree.value = handleTree(data) as SystemDeptApi.Dept[];
|
||||||
|
} finally {
|
||||||
|
deptLoading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 全选/全不选 */
|
||||||
|
function handleSelectAll() {
|
||||||
|
isAllSelected.value = !isAllSelected.value;
|
||||||
|
if (isAllSelected.value) {
|
||||||
|
const allIds = getAllNodeIds(deptTree.value);
|
||||||
|
formApi.setFieldValue('dataScopeDeptIds', allIds);
|
||||||
|
} else {
|
||||||
|
formApi.setFieldValue('dataScopeDeptIds', []);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 展开/折叠所有节点 */
|
||||||
|
function handleExpandAll() {
|
||||||
|
isExpanded.value = !isExpanded.value;
|
||||||
|
expandedKeys.value = isExpanded.value ? getAllNodeIds(deptTree.value) : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 切换父子联动 */
|
||||||
|
function handleCheckStrictly() {
|
||||||
|
isCheckStrictly.value = !isCheckStrictly.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 递归获取所有节点 ID */
|
||||||
|
function getAllNodeIds(nodes: any[], ids: number[] = []): number[] {
|
||||||
|
nodes.forEach((node: any) => {
|
||||||
|
ids.push(node.id);
|
||||||
|
if (node.children && node.children.length > 0) {
|
||||||
|
getAllNodeIds(node.children, ids);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return ids;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Modal title="数据权限" class="w-2/5">
|
||||||
|
<Form class="mx-4">
|
||||||
|
<template #dataScopeDeptIds="slotProps">
|
||||||
|
<NSpin :show="deptLoading" class="w-full">
|
||||||
|
<Tree
|
||||||
|
:tree-data="deptTree"
|
||||||
|
multiple
|
||||||
|
bordered
|
||||||
|
:default-expanded-keys="expandedKeys"
|
||||||
|
v-bind="slotProps"
|
||||||
|
:check-strictly="!isCheckStrictly"
|
||||||
|
value-field="id"
|
||||||
|
label-field="name"
|
||||||
|
/>
|
||||||
|
</NSpin>
|
||||||
|
</template>
|
||||||
|
</Form>
|
||||||
|
<template #prepend-footer>
|
||||||
|
<div class="flex flex-auto items-center">
|
||||||
|
<NCheckbox :checked="isAllSelected" @change="handleSelectAll">
|
||||||
|
全选
|
||||||
|
</NCheckbox>
|
||||||
|
<NCheckbox :checked="isExpanded" @change="handleExpandAll">
|
||||||
|
全部展开
|
||||||
|
</NCheckbox>
|
||||||
|
<NCheckbox :checked="isCheckStrictly" @change="handleCheckStrictly">
|
||||||
|
父子联动
|
||||||
|
</NCheckbox>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Modal>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,171 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { Recordable } from '@vben/types';
|
||||||
|
|
||||||
|
import type { SystemMenuApi } from '#/api/system/menu';
|
||||||
|
import type { SystemRoleApi } from '#/api/system/role';
|
||||||
|
|
||||||
|
import { nextTick, ref } from 'vue';
|
||||||
|
|
||||||
|
import { Tree, useVbenModal } from '@vben/common-ui';
|
||||||
|
import { SystemMenuTypeEnum } from '@vben/constants';
|
||||||
|
import { handleTree } from '@vben/utils';
|
||||||
|
|
||||||
|
import { NCheckbox, NSpin } from 'naive-ui';
|
||||||
|
|
||||||
|
import { useVbenForm } from '#/adapter/form';
|
||||||
|
import { message } from '#/adapter/naive';
|
||||||
|
import { getSimpleMenusList } from '#/api/system/menu';
|
||||||
|
import { assignRoleMenu, getRoleMenuList } from '#/api/system/permission';
|
||||||
|
import { $t } from '#/locales';
|
||||||
|
|
||||||
|
import { useAssignMenuFormSchema } from '../data';
|
||||||
|
|
||||||
|
const emit = defineEmits(['success']);
|
||||||
|
|
||||||
|
const menuTree = ref<SystemMenuApi.Menu[]>([]); // 菜单树
|
||||||
|
const menuLoading = ref(false); // 加载菜单列表
|
||||||
|
const isAllSelected = ref(false); // 全选状态
|
||||||
|
const isExpanded = ref(false); // 展开状态
|
||||||
|
const expandedKeys = ref<number[]>([]); // 展开的节点
|
||||||
|
|
||||||
|
const [Form, formApi] = useVbenForm({
|
||||||
|
commonConfig: {
|
||||||
|
componentProps: {
|
||||||
|
class: 'w-full',
|
||||||
|
},
|
||||||
|
formItemClass: 'col-span-2',
|
||||||
|
labelWidth: 80,
|
||||||
|
},
|
||||||
|
layout: 'horizontal',
|
||||||
|
schema: useAssignMenuFormSchema(),
|
||||||
|
showDefaultActions: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const [Modal, modalApi] = useVbenModal({
|
||||||
|
async onConfirm() {
|
||||||
|
const { valid } = await formApi.validate();
|
||||||
|
if (!valid) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
modalApi.lock();
|
||||||
|
// 提交表单
|
||||||
|
const data = await formApi.getValues();
|
||||||
|
try {
|
||||||
|
await assignRoleMenu({
|
||||||
|
roleId: data.id,
|
||||||
|
menuIds: data.menuIds,
|
||||||
|
});
|
||||||
|
// 关闭并提示
|
||||||
|
await modalApi.close();
|
||||||
|
emit('success');
|
||||||
|
message.success($t('ui.actionMessage.operationSuccess'));
|
||||||
|
} finally {
|
||||||
|
modalApi.unlock();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async onOpenChange(isOpen: boolean) {
|
||||||
|
if (!isOpen) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 加载菜单列表
|
||||||
|
await loadMenuTree();
|
||||||
|
const data = modalApi.getData<SystemRoleApi.Role>();
|
||||||
|
if (!data || !data.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
modalApi.lock();
|
||||||
|
try {
|
||||||
|
// 加载角色菜单
|
||||||
|
const menuIds = await getRoleMenuList(data.id);
|
||||||
|
await formApi.setFieldValue('menuIds', menuIds);
|
||||||
|
|
||||||
|
await formApi.setValues(data);
|
||||||
|
} finally {
|
||||||
|
await nextTick(); // 菜单过多,渲染较慢,需要等下一次事件循环
|
||||||
|
modalApi.unlock();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 加载菜单树 */
|
||||||
|
async function loadMenuTree() {
|
||||||
|
menuLoading.value = true;
|
||||||
|
try {
|
||||||
|
const data = await getSimpleMenusList();
|
||||||
|
menuTree.value = handleTree(data) as SystemMenuApi.Menu[];
|
||||||
|
} finally {
|
||||||
|
menuLoading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 全选/全不选 */
|
||||||
|
function handleSelectAll() {
|
||||||
|
isAllSelected.value = !isAllSelected.value;
|
||||||
|
if (isAllSelected.value) {
|
||||||
|
const allIds = getAllNodeIds(menuTree.value);
|
||||||
|
formApi.setFieldValue('menuIds', allIds);
|
||||||
|
} else {
|
||||||
|
formApi.setFieldValue('menuIds', []);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 展开/折叠所有节点 */
|
||||||
|
function handleExpandAll() {
|
||||||
|
isExpanded.value = !isExpanded.value;
|
||||||
|
expandedKeys.value = isExpanded.value ? getAllNodeIds(menuTree.value) : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 递归获取所有节点 ID */
|
||||||
|
function getAllNodeIds(nodes: any[], ids: number[] = []): number[] {
|
||||||
|
nodes.forEach((node: any) => {
|
||||||
|
ids.push(node.id);
|
||||||
|
if (node.children && node.children.length > 0) {
|
||||||
|
getAllNodeIds(node.children, ids);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return ids;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getNodeClass(node: Recordable<any>) {
|
||||||
|
const classes: string[] = [];
|
||||||
|
if (node.value?.type === SystemMenuTypeEnum.BUTTON) {
|
||||||
|
classes.push('inline-flex');
|
||||||
|
if (node.index % 3 >= 1) {
|
||||||
|
classes.push('!pl-0');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return classes.join(' ');
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Modal title="菜单权限" class="w-2/5">
|
||||||
|
<Form class="mx-4">
|
||||||
|
<template #menuIds="slotProps">
|
||||||
|
<NSpin :show="menuLoading" class="w-full">
|
||||||
|
<Tree
|
||||||
|
:tree-data="menuTree"
|
||||||
|
multiple
|
||||||
|
bordered
|
||||||
|
:default-expanded-keys="expandedKeys"
|
||||||
|
:get-node-class="getNodeClass"
|
||||||
|
v-bind="slotProps"
|
||||||
|
value-field="id"
|
||||||
|
label-field="name"
|
||||||
|
/>
|
||||||
|
</NSpin>
|
||||||
|
</template>
|
||||||
|
</Form>
|
||||||
|
<template #prepend-footer>
|
||||||
|
<div class="flex flex-auto items-center">
|
||||||
|
<NCheckbox :checked="isAllSelected" @change="handleSelectAll">
|
||||||
|
全选
|
||||||
|
</NCheckbox>
|
||||||
|
<NCheckbox :checked="isExpanded" @change="handleExpandAll">
|
||||||
|
全部展开
|
||||||
|
</NCheckbox>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Modal>
|
||||||
|
</template>
|
||||||
81
apps/web-naive/src/views/system/role/modules/form.vue
Normal file
81
apps/web-naive/src/views/system/role/modules/form.vue
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { SystemRoleApi } from '#/api/system/role';
|
||||||
|
|
||||||
|
import { computed, ref } from 'vue';
|
||||||
|
|
||||||
|
import { useVbenModal } from '@vben/common-ui';
|
||||||
|
|
||||||
|
import { useVbenForm } from '#/adapter/form';
|
||||||
|
import { message } from '#/adapter/naive';
|
||||||
|
import { createRole, getRole, updateRole } from '#/api/system/role';
|
||||||
|
import { $t } from '#/locales';
|
||||||
|
|
||||||
|
import { useFormSchema } from '../data';
|
||||||
|
|
||||||
|
const emit = defineEmits(['success']);
|
||||||
|
const formData = ref<SystemRoleApi.Role>();
|
||||||
|
const getTitle = computed(() => {
|
||||||
|
return formData.value?.id
|
||||||
|
? $t('ui.actionTitle.edit', ['角色'])
|
||||||
|
: $t('ui.actionTitle.create', ['角色']);
|
||||||
|
});
|
||||||
|
|
||||||
|
const [Form, formApi] = useVbenForm({
|
||||||
|
commonConfig: {
|
||||||
|
componentProps: {
|
||||||
|
class: 'w-full',
|
||||||
|
},
|
||||||
|
formItemClass: 'col-span-2',
|
||||||
|
labelWidth: 80,
|
||||||
|
},
|
||||||
|
layout: 'horizontal',
|
||||||
|
schema: useFormSchema(),
|
||||||
|
showDefaultActions: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const [Modal, modalApi] = useVbenModal({
|
||||||
|
async onConfirm() {
|
||||||
|
const { valid } = await formApi.validate();
|
||||||
|
if (!valid) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
modalApi.lock();
|
||||||
|
// 提交表单
|
||||||
|
const data = (await formApi.getValues()) as SystemRoleApi.Role;
|
||||||
|
try {
|
||||||
|
await (formData.value?.id ? updateRole(data) : createRole(data));
|
||||||
|
// 关闭并提示
|
||||||
|
await modalApi.close();
|
||||||
|
emit('success');
|
||||||
|
message.success($t('ui.actionMessage.operationSuccess'));
|
||||||
|
} finally {
|
||||||
|
modalApi.unlock();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async onOpenChange(isOpen: boolean) {
|
||||||
|
if (!isOpen) {
|
||||||
|
formData.value = undefined;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 加载数据
|
||||||
|
const data = modalApi.getData<SystemRoleApi.Role>();
|
||||||
|
if (!data || !data.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
modalApi.lock();
|
||||||
|
try {
|
||||||
|
formData.value = await getRole(data.id);
|
||||||
|
// 设置到 values
|
||||||
|
await formApi.setValues(formData.value);
|
||||||
|
} finally {
|
||||||
|
modalApi.unlock();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Modal :title="getTitle">
|
||||||
|
<Form class="mx-4" />
|
||||||
|
</Modal>
|
||||||
|
</template>
|
||||||
196
apps/web-naive/src/views/system/sms/channel/data.ts
Normal file
196
apps/web-naive/src/views/system/sms/channel/data.ts
Normal file
@@ -0,0 +1,196 @@
|
|||||||
|
import type { VbenFormSchema } from '#/adapter/form';
|
||||||
|
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||||
|
|
||||||
|
import { CommonStatusEnum, DICT_TYPE } from '@vben/constants';
|
||||||
|
import { getDictOptions } from '@vben/hooks';
|
||||||
|
|
||||||
|
import { z } from '#/adapter/form';
|
||||||
|
import { getRangePickerDefaultProps } from '#/utils';
|
||||||
|
|
||||||
|
/** 新增/修改的表单 */
|
||||||
|
export function useFormSchema(): VbenFormSchema[] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
fieldName: 'id',
|
||||||
|
component: 'Input',
|
||||||
|
dependencies: {
|
||||||
|
triggerFields: [''],
|
||||||
|
show: () => false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'signature',
|
||||||
|
label: '短信签名',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入短信签名',
|
||||||
|
},
|
||||||
|
rules: 'required',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'code',
|
||||||
|
label: '渠道编码',
|
||||||
|
component: 'Select',
|
||||||
|
componentProps: {
|
||||||
|
options: getDictOptions(DICT_TYPE.SYSTEM_SMS_CHANNEL_CODE, 'string'),
|
||||||
|
placeholder: '请选择短信渠道',
|
||||||
|
},
|
||||||
|
rules: 'required',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'status',
|
||||||
|
label: '启用状态',
|
||||||
|
component: 'RadioGroup',
|
||||||
|
componentProps: {
|
||||||
|
options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
|
||||||
|
buttonStyle: 'solid',
|
||||||
|
optionType: 'button',
|
||||||
|
},
|
||||||
|
rules: z.number().default(CommonStatusEnum.ENABLE),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'remark',
|
||||||
|
label: '备注',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
type: 'textarea',
|
||||||
|
placeholder: '请输入备注',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'apiKey',
|
||||||
|
label: '短信 API 的账号',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入短信 API 的账号',
|
||||||
|
},
|
||||||
|
rules: 'required',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'apiSecret',
|
||||||
|
label: '短信 API 的密钥',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入短信 API 的密钥',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'callbackUrl',
|
||||||
|
label: '短信发送回调 URL',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入短信发送回调 URL',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 列表的搜索表单 */
|
||||||
|
export function useGridFormSchema(): VbenFormSchema[] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
fieldName: 'signature',
|
||||||
|
label: '短信签名',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
clearable: true,
|
||||||
|
placeholder: '请输入短信签名',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'code',
|
||||||
|
label: '渠道编码',
|
||||||
|
component: 'Select',
|
||||||
|
componentProps: {
|
||||||
|
clearable: true,
|
||||||
|
options: getDictOptions(DICT_TYPE.SYSTEM_SMS_CHANNEL_CODE, 'string'),
|
||||||
|
placeholder: '请选择短信渠道',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'status',
|
||||||
|
label: '状态',
|
||||||
|
component: 'Select',
|
||||||
|
componentProps: {
|
||||||
|
clearable: true,
|
||||||
|
options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'createTime',
|
||||||
|
label: '创建时间',
|
||||||
|
component: 'DatePicker',
|
||||||
|
componentProps: {
|
||||||
|
...getRangePickerDefaultProps(),
|
||||||
|
clearable: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 列表的字段 */
|
||||||
|
export function useGridColumns(): VxeTableGridOptions['columns'] {
|
||||||
|
return [
|
||||||
|
{ type: 'checkbox', width: 40 },
|
||||||
|
{
|
||||||
|
field: 'id',
|
||||||
|
title: '编号',
|
||||||
|
minWidth: 100,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'signature',
|
||||||
|
title: '短信签名',
|
||||||
|
minWidth: 120,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'code',
|
||||||
|
title: '渠道编码',
|
||||||
|
minWidth: 120,
|
||||||
|
cellRender: {
|
||||||
|
name: 'CellDict',
|
||||||
|
props: { type: DICT_TYPE.SYSTEM_SMS_CHANNEL_CODE },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'status',
|
||||||
|
title: '启用状态',
|
||||||
|
minWidth: 100,
|
||||||
|
cellRender: {
|
||||||
|
name: 'CellDict',
|
||||||
|
props: { type: DICT_TYPE.COMMON_STATUS },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'apiKey',
|
||||||
|
title: '短信 API 的账号',
|
||||||
|
minWidth: 180,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'apiSecret',
|
||||||
|
title: '短信 API 的密钥',
|
||||||
|
minWidth: 180,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'callbackUrl',
|
||||||
|
title: '短信发送回调 URL',
|
||||||
|
minWidth: 180,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'createTime',
|
||||||
|
title: '创建时间',
|
||||||
|
minWidth: 180,
|
||||||
|
formatter: 'formatDateTime',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'remark',
|
||||||
|
title: '备注',
|
||||||
|
minWidth: 120,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '操作',
|
||||||
|
width: 220,
|
||||||
|
fixed: 'right',
|
||||||
|
slots: { default: 'actions' },
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
173
apps/web-naive/src/views/system/sms/channel/index.vue
Normal file
173
apps/web-naive/src/views/system/sms/channel/index.vue
Normal file
@@ -0,0 +1,173 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||||
|
import type { SystemSmsChannelApi } from '#/api/system/sms/channel';
|
||||||
|
|
||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
import { confirm, DocAlert, Page, useVbenModal } from '@vben/common-ui';
|
||||||
|
import { isEmpty } from '@vben/utils';
|
||||||
|
|
||||||
|
import { message } from '#/adapter/naive';
|
||||||
|
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||||
|
import {
|
||||||
|
deleteSmsChannel,
|
||||||
|
deleteSmsChannelList,
|
||||||
|
getSmsChannelPage,
|
||||||
|
} from '#/api/system/sms/channel';
|
||||||
|
import { $t } from '#/locales';
|
||||||
|
|
||||||
|
import { useGridColumns, useGridFormSchema } from './data';
|
||||||
|
import Form from './modules/form.vue';
|
||||||
|
|
||||||
|
const [FormModal, formModalApi] = useVbenModal({
|
||||||
|
connectedComponent: Form,
|
||||||
|
destroyOnClose: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 刷新表格 */
|
||||||
|
function handleRefresh() {
|
||||||
|
gridApi.query();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 创建短信渠道 */
|
||||||
|
function handleCreate() {
|
||||||
|
formModalApi.setData(null).open();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 编辑短信渠道 */
|
||||||
|
function handleEdit(row: SystemSmsChannelApi.SmsChannel) {
|
||||||
|
formModalApi.setData(row).open();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 删除短信渠道 */
|
||||||
|
async function handleDelete(row: SystemSmsChannelApi.SmsChannel) {
|
||||||
|
const hideLoading = message.loading(
|
||||||
|
$t('ui.actionMessage.deleting', [row.signature]),
|
||||||
|
{ duration: 0 },
|
||||||
|
);
|
||||||
|
try {
|
||||||
|
await deleteSmsChannel(row.id!);
|
||||||
|
message.success($t('ui.actionMessage.deleteSuccess', [row.signature]));
|
||||||
|
handleRefresh();
|
||||||
|
} finally {
|
||||||
|
hideLoading.destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 批量删除短信渠道 */
|
||||||
|
async function handleDeleteBatch() {
|
||||||
|
await confirm($t('ui.actionMessage.deleteBatchConfirm'));
|
||||||
|
const hideLoading = message.loading($t('ui.actionMessage.deletingBatch'), {
|
||||||
|
duration: 0,
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
await deleteSmsChannelList(checkedIds.value);
|
||||||
|
checkedIds.value = [];
|
||||||
|
message.success($t('ui.actionMessage.deleteSuccess'));
|
||||||
|
handleRefresh();
|
||||||
|
} finally {
|
||||||
|
hideLoading.destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const checkedIds = ref<number[]>([]);
|
||||||
|
function handleRowCheckboxChange({
|
||||||
|
records,
|
||||||
|
}: {
|
||||||
|
records: SystemSmsChannelApi.SmsChannel[];
|
||||||
|
}) {
|
||||||
|
checkedIds.value = records.map((item) => item.id!);
|
||||||
|
}
|
||||||
|
|
||||||
|
const [Grid, gridApi] = useVbenVxeGrid({
|
||||||
|
formOptions: {
|
||||||
|
schema: useGridFormSchema(),
|
||||||
|
},
|
||||||
|
gridOptions: {
|
||||||
|
columns: useGridColumns(),
|
||||||
|
height: 'auto',
|
||||||
|
keepSource: true,
|
||||||
|
proxyConfig: {
|
||||||
|
ajax: {
|
||||||
|
query: async ({ page }, formValues) => {
|
||||||
|
return await getSmsChannelPage({
|
||||||
|
pageNo: page.currentPage,
|
||||||
|
pageSize: page.pageSize,
|
||||||
|
...formValues,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
rowConfig: {
|
||||||
|
keyField: 'id',
|
||||||
|
isHover: true,
|
||||||
|
},
|
||||||
|
toolbarConfig: {
|
||||||
|
refresh: true,
|
||||||
|
search: true,
|
||||||
|
},
|
||||||
|
} as VxeTableGridOptions<SystemSmsChannelApi.SmsChannel>,
|
||||||
|
gridEvents: {
|
||||||
|
checkboxAll: handleRowCheckboxChange,
|
||||||
|
checkboxChange: handleRowCheckboxChange,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Page auto-content-height>
|
||||||
|
<template #doc>
|
||||||
|
<DocAlert title="短信配置" url="https://doc.iocoder.cn/sms/" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<FormModal @success="handleRefresh" />
|
||||||
|
<Grid table-title="短信渠道列表">
|
||||||
|
<template #toolbar-tools>
|
||||||
|
<TableAction
|
||||||
|
:actions="[
|
||||||
|
{
|
||||||
|
label: $t('ui.actionTitle.create', ['短信渠道']),
|
||||||
|
type: 'primary',
|
||||||
|
icon: ACTION_ICON.ADD,
|
||||||
|
auth: ['system:sms-channel:create'],
|
||||||
|
onClick: handleCreate,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: $t('ui.actionTitle.deleteBatch'),
|
||||||
|
type: 'error',
|
||||||
|
icon: ACTION_ICON.DELETE,
|
||||||
|
disabled: isEmpty(checkedIds),
|
||||||
|
auth: ['system:sms-channel:delete'],
|
||||||
|
onClick: handleDeleteBatch,
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<template #actions="{ row }">
|
||||||
|
<TableAction
|
||||||
|
:actions="[
|
||||||
|
{
|
||||||
|
label: $t('common.edit'),
|
||||||
|
type: 'primary',
|
||||||
|
text: true,
|
||||||
|
icon: ACTION_ICON.EDIT,
|
||||||
|
auth: ['system:sms-channel:update'],
|
||||||
|
onClick: handleEdit.bind(null, row),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: $t('common.delete'),
|
||||||
|
type: 'error',
|
||||||
|
text: true,
|
||||||
|
icon: ACTION_ICON.DELETE,
|
||||||
|
auth: ['system:sms-channel:delete'],
|
||||||
|
popConfirm: {
|
||||||
|
title: $t('ui.actionMessage.deleteConfirm', [row.signature]),
|
||||||
|
confirm: handleDelete.bind(null, row),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</Grid>
|
||||||
|
</Page>
|
||||||
|
</template>
|
||||||
87
apps/web-naive/src/views/system/sms/channel/modules/form.vue
Normal file
87
apps/web-naive/src/views/system/sms/channel/modules/form.vue
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { SystemSmsChannelApi } from '#/api/system/sms/channel';
|
||||||
|
|
||||||
|
import { computed, ref } from 'vue';
|
||||||
|
|
||||||
|
import { useVbenModal } from '@vben/common-ui';
|
||||||
|
|
||||||
|
import { useVbenForm } from '#/adapter/form';
|
||||||
|
import { message } from '#/adapter/naive';
|
||||||
|
import {
|
||||||
|
createSmsChannel,
|
||||||
|
getSmsChannel,
|
||||||
|
updateSmsChannel,
|
||||||
|
} from '#/api/system/sms/channel';
|
||||||
|
import { $t } from '#/locales';
|
||||||
|
|
||||||
|
import { useFormSchema } from '../data';
|
||||||
|
|
||||||
|
const emit = defineEmits(['success']);
|
||||||
|
const formData = ref<SystemSmsChannelApi.SmsChannel>();
|
||||||
|
const getTitle = computed(() => {
|
||||||
|
return formData.value?.id
|
||||||
|
? $t('ui.actionTitle.edit', ['短信渠道'])
|
||||||
|
: $t('ui.actionTitle.create', ['短信渠道']);
|
||||||
|
});
|
||||||
|
|
||||||
|
const [Form, formApi] = useVbenForm({
|
||||||
|
commonConfig: {
|
||||||
|
componentProps: {
|
||||||
|
class: 'w-full',
|
||||||
|
},
|
||||||
|
formItemClass: 'col-span-2',
|
||||||
|
labelWidth: 120,
|
||||||
|
},
|
||||||
|
layout: 'horizontal',
|
||||||
|
schema: useFormSchema(),
|
||||||
|
showDefaultActions: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const [Modal, modalApi] = useVbenModal({
|
||||||
|
async onConfirm() {
|
||||||
|
const { valid } = await formApi.validate();
|
||||||
|
if (!valid) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
modalApi.lock();
|
||||||
|
// 提交表单
|
||||||
|
const data = (await formApi.getValues()) as SystemSmsChannelApi.SmsChannel;
|
||||||
|
try {
|
||||||
|
await (formData.value?.id
|
||||||
|
? updateSmsChannel(data)
|
||||||
|
: createSmsChannel(data));
|
||||||
|
// 关闭并提示
|
||||||
|
await modalApi.close();
|
||||||
|
emit('success');
|
||||||
|
message.success($t('ui.actionMessage.operationSuccess'));
|
||||||
|
} finally {
|
||||||
|
modalApi.unlock();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async onOpenChange(isOpen: boolean) {
|
||||||
|
if (!isOpen) {
|
||||||
|
formData.value = undefined;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 加载数据
|
||||||
|
const data = modalApi.getData<SystemSmsChannelApi.SmsChannel>();
|
||||||
|
if (!data || !data.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
modalApi.lock();
|
||||||
|
try {
|
||||||
|
formData.value = await getSmsChannel(data.id);
|
||||||
|
// 设置到 values
|
||||||
|
await formApi.setValues(formData.value);
|
||||||
|
} finally {
|
||||||
|
modalApi.unlock();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Modal :title="getTitle">
|
||||||
|
<Form class="mx-4" />
|
||||||
|
</Modal>
|
||||||
|
</template>
|
||||||
278
apps/web-naive/src/views/system/sms/log/data.ts
Normal file
278
apps/web-naive/src/views/system/sms/log/data.ts
Normal file
@@ -0,0 +1,278 @@
|
|||||||
|
import type { VbenFormSchema } from '#/adapter/form';
|
||||||
|
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||||
|
import type { SystemSmsLogApi } from '#/api/system/sms/log';
|
||||||
|
import type { DescriptionItemSchema } from '#/components/description';
|
||||||
|
|
||||||
|
import { h } from 'vue';
|
||||||
|
|
||||||
|
import { DICT_TYPE } from '@vben/constants';
|
||||||
|
import { getDictOptions } from '@vben/hooks';
|
||||||
|
import { formatDateTime } from '@vben/utils';
|
||||||
|
|
||||||
|
import { getSimpleSmsChannelList } from '#/api/system/sms/channel';
|
||||||
|
import { DictTag } from '#/components/dict-tag';
|
||||||
|
import { getRangePickerDefaultProps } from '#/utils';
|
||||||
|
|
||||||
|
/** 列表的搜索表单 */
|
||||||
|
export function useGridFormSchema(): VbenFormSchema[] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
fieldName: 'mobile',
|
||||||
|
label: '手机号',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
clearable: true,
|
||||||
|
placeholder: '请输入手机号',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'channelId',
|
||||||
|
label: '短信渠道',
|
||||||
|
component: 'ApiSelect',
|
||||||
|
componentProps: {
|
||||||
|
api: async () => await getSimpleSmsChannelList(),
|
||||||
|
labelField: 'signature',
|
||||||
|
valueField: 'id',
|
||||||
|
clearable: true,
|
||||||
|
placeholder: '请选择短信渠道',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'templateId',
|
||||||
|
label: '模板编号',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
clearable: true,
|
||||||
|
placeholder: '请输入模板编号',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'sendStatus',
|
||||||
|
label: '发送状态',
|
||||||
|
component: 'Select',
|
||||||
|
componentProps: {
|
||||||
|
options: getDictOptions(DICT_TYPE.SYSTEM_SMS_SEND_STATUS, 'number'),
|
||||||
|
clearable: true,
|
||||||
|
placeholder: '请选择发送状态',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'sendTime',
|
||||||
|
label: '发送时间',
|
||||||
|
component: 'DatePicker',
|
||||||
|
componentProps: {
|
||||||
|
...getRangePickerDefaultProps(),
|
||||||
|
clearable: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'receiveStatus',
|
||||||
|
label: '接收状态',
|
||||||
|
component: 'Select',
|
||||||
|
componentProps: {
|
||||||
|
options: getDictOptions(DICT_TYPE.SYSTEM_SMS_RECEIVE_STATUS, 'number'),
|
||||||
|
clearable: true,
|
||||||
|
placeholder: '请选择接收状态',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'receiveTime',
|
||||||
|
label: '接收时间',
|
||||||
|
component: 'DatePicker',
|
||||||
|
componentProps: {
|
||||||
|
...getRangePickerDefaultProps(),
|
||||||
|
clearable: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 列表的字段 */
|
||||||
|
export function useGridColumns(): VxeTableGridOptions['columns'] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
field: 'id',
|
||||||
|
title: '编号',
|
||||||
|
minWidth: 100,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'createTime',
|
||||||
|
title: '创建时间',
|
||||||
|
minWidth: 180,
|
||||||
|
formatter: 'formatDateTime',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'mobile',
|
||||||
|
title: '手机号',
|
||||||
|
minWidth: 120,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'templateContent',
|
||||||
|
title: '短信内容',
|
||||||
|
minWidth: 300,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'sendStatus',
|
||||||
|
title: '发送状态',
|
||||||
|
minWidth: 120,
|
||||||
|
cellRender: {
|
||||||
|
name: 'CellDict',
|
||||||
|
props: { type: DICT_TYPE.SYSTEM_SMS_SEND_STATUS },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'sendTime',
|
||||||
|
title: '发送时间',
|
||||||
|
minWidth: 180,
|
||||||
|
formatter: 'formatDateTime',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'receiveStatus',
|
||||||
|
title: '接收状态',
|
||||||
|
minWidth: 120,
|
||||||
|
cellRender: {
|
||||||
|
name: 'CellDict',
|
||||||
|
props: { type: DICT_TYPE.SYSTEM_SMS_RECEIVE_STATUS },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'receiveTime',
|
||||||
|
title: '接收时间',
|
||||||
|
minWidth: 180,
|
||||||
|
formatter: 'formatDateTime',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'channelCode',
|
||||||
|
title: '短信渠道',
|
||||||
|
minWidth: 120,
|
||||||
|
cellRender: {
|
||||||
|
name: 'CellDict',
|
||||||
|
props: { type: DICT_TYPE.SYSTEM_SMS_CHANNEL_CODE },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'templateId',
|
||||||
|
title: '模板编号',
|
||||||
|
minWidth: 100,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'templateType',
|
||||||
|
title: '短信类型',
|
||||||
|
minWidth: 100,
|
||||||
|
cellRender: {
|
||||||
|
name: 'CellDict',
|
||||||
|
props: { type: DICT_TYPE.SYSTEM_SMS_TEMPLATE_TYPE },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'createTime',
|
||||||
|
title: '创建时间',
|
||||||
|
minWidth: 180,
|
||||||
|
formatter: 'formatDateTime',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '操作',
|
||||||
|
width: 80,
|
||||||
|
fixed: 'right',
|
||||||
|
slots: { default: 'actions' },
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 详情页的字段 */
|
||||||
|
export function useDetailSchema(): DescriptionItemSchema[] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
field: 'createTime',
|
||||||
|
label: '创建时间',
|
||||||
|
content: (data: SystemSmsLogApi.SmsLog) => {
|
||||||
|
return formatDateTime(data?.createTime || '') as string;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'mobile',
|
||||||
|
label: '手机号',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'channelCode',
|
||||||
|
label: '短信渠道',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'templateId',
|
||||||
|
label: '模板编号',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'templateType',
|
||||||
|
label: '模板类型',
|
||||||
|
content: (data: SystemSmsLogApi.SmsLog) => {
|
||||||
|
return h(DictTag, {
|
||||||
|
type: DICT_TYPE.SYSTEM_SMS_TEMPLATE_TYPE,
|
||||||
|
value: data?.templateType,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'templateContent',
|
||||||
|
label: '短信内容',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'sendStatus',
|
||||||
|
label: '发送状态',
|
||||||
|
content: (data: SystemSmsLogApi.SmsLog) => {
|
||||||
|
return h(DictTag, {
|
||||||
|
type: DICT_TYPE.SYSTEM_SMS_SEND_STATUS,
|
||||||
|
value: data?.sendStatus,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'sendTime',
|
||||||
|
label: '发送时间',
|
||||||
|
content: (data: SystemSmsLogApi.SmsLog) => {
|
||||||
|
return formatDateTime(data?.sendTime || '') as string;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'apiSendCode',
|
||||||
|
label: 'API 发送编码',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'apiSendMsg',
|
||||||
|
label: 'API 发送消息',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'receiveStatus',
|
||||||
|
label: '接收状态',
|
||||||
|
content: (data: SystemSmsLogApi.SmsLog) => {
|
||||||
|
return h(DictTag, {
|
||||||
|
type: DICT_TYPE.SYSTEM_SMS_RECEIVE_STATUS,
|
||||||
|
value: data?.receiveStatus,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'receiveTime',
|
||||||
|
label: '接收时间',
|
||||||
|
content: (data: SystemSmsLogApi.SmsLog) => {
|
||||||
|
return formatDateTime(data?.receiveTime || '') as string;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'apiReceiveCode',
|
||||||
|
label: 'API 接收编码',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'apiReceiveMsg',
|
||||||
|
label: 'API 接收消息',
|
||||||
|
span: 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'apiRequestId',
|
||||||
|
label: 'API 请求 ID',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'apiSerialNo',
|
||||||
|
label: 'API 序列号',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
104
apps/web-naive/src/views/system/sms/log/index.vue
Normal file
104
apps/web-naive/src/views/system/sms/log/index.vue
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||||
|
import type { SystemSmsLogApi } from '#/api/system/sms/log';
|
||||||
|
|
||||||
|
import { DocAlert, Page, useVbenModal } from '@vben/common-ui';
|
||||||
|
import { downloadFileFromBlobPart } from '@vben/utils';
|
||||||
|
|
||||||
|
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||||
|
import { exportSmsLog, getSmsLogPage } from '#/api/system/sms/log';
|
||||||
|
import { $t } from '#/locales';
|
||||||
|
|
||||||
|
import { useGridColumns, useGridFormSchema } from './data';
|
||||||
|
import Detail from './modules/detail.vue';
|
||||||
|
|
||||||
|
const [DetailModal, detailModalApi] = useVbenModal({
|
||||||
|
connectedComponent: Detail,
|
||||||
|
destroyOnClose: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 刷新表格 */
|
||||||
|
function handleRefresh() {
|
||||||
|
gridApi.query();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 导出表格 */
|
||||||
|
async function handleExport() {
|
||||||
|
const data = await exportSmsLog(await gridApi.formApi.getValues());
|
||||||
|
downloadFileFromBlobPart({ fileName: '短信日志.xls', source: data });
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 查看短信日志详情 */
|
||||||
|
function handleDetail(row: SystemSmsLogApi.SmsLog) {
|
||||||
|
detailModalApi.setData(row).open();
|
||||||
|
}
|
||||||
|
|
||||||
|
const [Grid, gridApi] = useVbenVxeGrid({
|
||||||
|
formOptions: {
|
||||||
|
schema: useGridFormSchema(),
|
||||||
|
},
|
||||||
|
gridOptions: {
|
||||||
|
columns: useGridColumns(),
|
||||||
|
height: 'auto',
|
||||||
|
keepSource: true,
|
||||||
|
proxyConfig: {
|
||||||
|
ajax: {
|
||||||
|
query: async ({ page }, formValues) => {
|
||||||
|
return await getSmsLogPage({
|
||||||
|
pageNo: page.currentPage,
|
||||||
|
pageSize: page.pageSize,
|
||||||
|
...formValues,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
rowConfig: {
|
||||||
|
keyField: 'id',
|
||||||
|
isHover: true,
|
||||||
|
},
|
||||||
|
toolbarConfig: {
|
||||||
|
refresh: true,
|
||||||
|
search: true,
|
||||||
|
},
|
||||||
|
} as VxeTableGridOptions<SystemSmsLogApi.SmsLog>,
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Page auto-content-height>
|
||||||
|
<template #doc>
|
||||||
|
<DocAlert title="短信配置" url="https://doc.iocoder.cn/sms/" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<DetailModal @success="handleRefresh" />
|
||||||
|
<Grid table-title="短信日志列表">
|
||||||
|
<template #toolbar-tools>
|
||||||
|
<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: 'primary',
|
||||||
|
text: true,
|
||||||
|
icon: ACTION_ICON.VIEW,
|
||||||
|
auth: ['system:sms-log:query'],
|
||||||
|
onClick: handleDetail.bind(null, row),
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</Grid>
|
||||||
|
</Page>
|
||||||
|
</template>
|
||||||
53
apps/web-naive/src/views/system/sms/log/modules/detail.vue
Normal file
53
apps/web-naive/src/views/system/sms/log/modules/detail.vue
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { SystemSmsLogApi } from '#/api/system/sms/log';
|
||||||
|
|
||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
import { useVbenModal } from '@vben/common-ui';
|
||||||
|
|
||||||
|
import { useDescription } from '#/components/description';
|
||||||
|
|
||||||
|
import { useDetailSchema } from '../data';
|
||||||
|
|
||||||
|
const formData = ref<SystemSmsLogApi.SmsLog>();
|
||||||
|
|
||||||
|
const [Descriptions] = useDescription({
|
||||||
|
componentProps: {
|
||||||
|
bordered: true,
|
||||||
|
column: 2,
|
||||||
|
contentClass: 'mx-4',
|
||||||
|
},
|
||||||
|
schema: useDetailSchema(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const [Modal, modalApi] = useVbenModal({
|
||||||
|
async onOpenChange(isOpen: boolean) {
|
||||||
|
if (!isOpen) {
|
||||||
|
formData.value = undefined;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 加载数据
|
||||||
|
const data = modalApi.getData<SystemSmsLogApi.SmsLog>();
|
||||||
|
if (!data || !data.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
modalApi.lock();
|
||||||
|
try {
|
||||||
|
formData.value = data;
|
||||||
|
} finally {
|
||||||
|
modalApi.unlock();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Modal
|
||||||
|
title="短信日志详情"
|
||||||
|
class="w-[1280px]"
|
||||||
|
:show-cancel-button="false"
|
||||||
|
:show-confirm-button="false"
|
||||||
|
>
|
||||||
|
<Descriptions :data="formData" />
|
||||||
|
</Modal>
|
||||||
|
</template>
|
||||||
277
apps/web-naive/src/views/system/sms/template/data.ts
Normal file
277
apps/web-naive/src/views/system/sms/template/data.ts
Normal file
@@ -0,0 +1,277 @@
|
|||||||
|
import type { VbenFormSchema } from '#/adapter/form';
|
||||||
|
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||||
|
|
||||||
|
import { CommonStatusEnum, DICT_TYPE } from '@vben/constants';
|
||||||
|
import { getDictOptions } from '@vben/hooks';
|
||||||
|
|
||||||
|
import { z } from '#/adapter/form';
|
||||||
|
import { getSimpleSmsChannelList } from '#/api/system/sms/channel';
|
||||||
|
import { getRangePickerDefaultProps } from '#/utils';
|
||||||
|
|
||||||
|
/** 新增/修改的表单 */
|
||||||
|
export function useFormSchema(): VbenFormSchema[] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
fieldName: 'id',
|
||||||
|
component: 'Input',
|
||||||
|
dependencies: {
|
||||||
|
triggerFields: [''],
|
||||||
|
show: () => false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'type',
|
||||||
|
label: '短信类型',
|
||||||
|
component: 'Select',
|
||||||
|
componentProps: {
|
||||||
|
options: getDictOptions(DICT_TYPE.SYSTEM_SMS_TEMPLATE_TYPE, 'number'),
|
||||||
|
placeholder: '请选择短信类型',
|
||||||
|
},
|
||||||
|
rules: 'required',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'name',
|
||||||
|
label: '模板名称',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入模板名称',
|
||||||
|
},
|
||||||
|
rules: 'required',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'code',
|
||||||
|
label: '模板编码',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入模板编码',
|
||||||
|
},
|
||||||
|
rules: 'required',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'channelId',
|
||||||
|
label: '短信渠道',
|
||||||
|
component: 'ApiSelect',
|
||||||
|
componentProps: {
|
||||||
|
api: async () => await getSimpleSmsChannelList(),
|
||||||
|
labelField: 'signature',
|
||||||
|
valueField: 'id',
|
||||||
|
placeholder: '请选择短信渠道',
|
||||||
|
},
|
||||||
|
rules: 'required',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'status',
|
||||||
|
label: '开启状态',
|
||||||
|
component: 'RadioGroup',
|
||||||
|
componentProps: {
|
||||||
|
options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
|
||||||
|
buttonStyle: 'solid',
|
||||||
|
optionType: 'button',
|
||||||
|
},
|
||||||
|
rules: z.number().default(CommonStatusEnum.ENABLE),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'content',
|
||||||
|
label: '模板内容',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
type: 'textarea',
|
||||||
|
placeholder: '请输入模板内容',
|
||||||
|
rows: 4,
|
||||||
|
},
|
||||||
|
rules: 'required',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'apiTemplateId',
|
||||||
|
label: '短信 API 的模板编号',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入短信 API 的模板编号',
|
||||||
|
},
|
||||||
|
rules: 'required',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'remark',
|
||||||
|
label: '备注',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
type: 'textarea',
|
||||||
|
placeholder: '请输入备注',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 列表的搜索表单 */
|
||||||
|
export function useGridFormSchema(): VbenFormSchema[] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
fieldName: 'type',
|
||||||
|
label: '短信类型',
|
||||||
|
component: 'Select',
|
||||||
|
componentProps: {
|
||||||
|
options: getDictOptions(DICT_TYPE.SYSTEM_SMS_TEMPLATE_TYPE, 'number'),
|
||||||
|
clearable: true,
|
||||||
|
placeholder: '请选择短信类型',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'status',
|
||||||
|
label: '开启状态',
|
||||||
|
component: 'Select',
|
||||||
|
componentProps: {
|
||||||
|
options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
|
||||||
|
clearable: true,
|
||||||
|
placeholder: '请选择开启状态',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'code',
|
||||||
|
label: '模板编码',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
clearable: true,
|
||||||
|
placeholder: '请输入模板编码',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'name',
|
||||||
|
label: '模板名称',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
clearable: true,
|
||||||
|
placeholder: '请输入模板名称',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'channelId',
|
||||||
|
label: '短信渠道',
|
||||||
|
component: 'ApiSelect',
|
||||||
|
componentProps: {
|
||||||
|
api: async () => await getSimpleSmsChannelList(),
|
||||||
|
labelField: 'signature',
|
||||||
|
valueField: 'id',
|
||||||
|
clearable: true,
|
||||||
|
placeholder: '请选择短信渠道',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'createTime',
|
||||||
|
label: '创建时间',
|
||||||
|
component: 'DatePicker',
|
||||||
|
componentProps: {
|
||||||
|
...getRangePickerDefaultProps(),
|
||||||
|
clearable: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 发送短信表单 */
|
||||||
|
export function useSendSmsFormSchema(): VbenFormSchema[] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
fieldName: 'content',
|
||||||
|
label: '模板内容',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
type: 'textarea',
|
||||||
|
disabled: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'mobile',
|
||||||
|
label: '手机号码',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入手机号码',
|
||||||
|
},
|
||||||
|
rules: 'required',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'templateParams',
|
||||||
|
label: '模板参数',
|
||||||
|
component: 'Input',
|
||||||
|
dependencies: {
|
||||||
|
triggerFields: [''],
|
||||||
|
show: () => false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 列表的字段 */
|
||||||
|
export function useGridColumns(): VxeTableGridOptions['columns'] {
|
||||||
|
return [
|
||||||
|
{ type: 'checkbox', width: 40 },
|
||||||
|
{
|
||||||
|
field: 'id',
|
||||||
|
title: '编号',
|
||||||
|
minWidth: 100,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'type',
|
||||||
|
title: '短信类型',
|
||||||
|
minWidth: 120,
|
||||||
|
cellRender: {
|
||||||
|
name: 'CellDict',
|
||||||
|
props: { type: DICT_TYPE.SYSTEM_SMS_TEMPLATE_TYPE },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'name',
|
||||||
|
title: '模板名称',
|
||||||
|
minWidth: 120,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'code',
|
||||||
|
title: '模板编码',
|
||||||
|
minWidth: 120,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'content',
|
||||||
|
title: '模板内容',
|
||||||
|
minWidth: 200,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'status',
|
||||||
|
title: '开启状态',
|
||||||
|
minWidth: 100,
|
||||||
|
cellRender: {
|
||||||
|
name: 'CellDict',
|
||||||
|
props: { type: DICT_TYPE.COMMON_STATUS },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'apiTemplateId',
|
||||||
|
title: '短信 API 的模板编号',
|
||||||
|
minWidth: 180,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'channelCode',
|
||||||
|
title: '短信渠道',
|
||||||
|
minWidth: 100,
|
||||||
|
cellRender: {
|
||||||
|
name: 'CellDict',
|
||||||
|
props: { type: DICT_TYPE.SYSTEM_SMS_CHANNEL_CODE },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'createTime',
|
||||||
|
title: '创建时间',
|
||||||
|
minWidth: 180,
|
||||||
|
formatter: 'formatDateTime',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'remark',
|
||||||
|
title: '备注',
|
||||||
|
minWidth: 120,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '操作',
|
||||||
|
width: 220,
|
||||||
|
fixed: 'right',
|
||||||
|
slots: { default: 'actions' },
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
209
apps/web-naive/src/views/system/sms/template/index.vue
Normal file
209
apps/web-naive/src/views/system/sms/template/index.vue
Normal file
@@ -0,0 +1,209 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||||
|
import type { SystemSmsTemplateApi } from '#/api/system/sms/template';
|
||||||
|
|
||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
import { confirm, DocAlert, Page, useVbenModal } from '@vben/common-ui';
|
||||||
|
import { downloadFileFromBlobPart, isEmpty } from '@vben/utils';
|
||||||
|
|
||||||
|
import { message } from '#/adapter/naive';
|
||||||
|
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||||
|
import {
|
||||||
|
deleteSmsTemplate,
|
||||||
|
deleteSmsTemplateList,
|
||||||
|
exportSmsTemplate,
|
||||||
|
getSmsTemplatePage,
|
||||||
|
} from '#/api/system/sms/template';
|
||||||
|
import { $t } from '#/locales';
|
||||||
|
|
||||||
|
import { useGridColumns, useGridFormSchema } from './data';
|
||||||
|
import Form from './modules/form.vue';
|
||||||
|
import SendForm from './modules/send-form.vue';
|
||||||
|
|
||||||
|
const [FormModal, formModalApi] = useVbenModal({
|
||||||
|
connectedComponent: Form,
|
||||||
|
destroyOnClose: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const [SendModal, sendModalApi] = useVbenModal({
|
||||||
|
connectedComponent: SendForm,
|
||||||
|
destroyOnClose: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 刷新表格 */
|
||||||
|
function handleRefresh() {
|
||||||
|
gridApi.query();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 导出表格 */
|
||||||
|
async function handleExport() {
|
||||||
|
const data = await exportSmsTemplate(await gridApi.formApi.getValues());
|
||||||
|
downloadFileFromBlobPart({ fileName: '短信模板.xls', source: data });
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 创建短信模板 */
|
||||||
|
function handleCreate() {
|
||||||
|
formModalApi.setData(null).open();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 编辑短信模板 */
|
||||||
|
function handleEdit(row: SystemSmsTemplateApi.SmsTemplate) {
|
||||||
|
formModalApi.setData(row).open();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 发送测试短信 */
|
||||||
|
function handleSend(row: SystemSmsTemplateApi.SmsTemplate) {
|
||||||
|
sendModalApi.setData(row).open();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 删除短信模板 */
|
||||||
|
async function handleDelete(row: SystemSmsTemplateApi.SmsTemplate) {
|
||||||
|
const hideLoading = message.loading(
|
||||||
|
$t('ui.actionMessage.deleting', [row.name]),
|
||||||
|
{
|
||||||
|
duration: 0,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
try {
|
||||||
|
await deleteSmsTemplate(row.id!);
|
||||||
|
message.success($t('ui.actionMessage.deleteSuccess', [row.name]));
|
||||||
|
handleRefresh();
|
||||||
|
} finally {
|
||||||
|
hideLoading.destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 批量删除短信模板 */
|
||||||
|
async function handleDeleteBatch() {
|
||||||
|
await confirm($t('ui.actionMessage.deleteBatchConfirm'));
|
||||||
|
const hideLoading = message.loading($t('ui.actionMessage.deletingBatch'), {
|
||||||
|
duration: 0,
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
await deleteSmsTemplateList(checkedIds.value);
|
||||||
|
checkedIds.value = [];
|
||||||
|
message.success($t('ui.actionMessage.deleteSuccess'));
|
||||||
|
handleRefresh();
|
||||||
|
} finally {
|
||||||
|
hideLoading.destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const checkedIds = ref<number[]>([]);
|
||||||
|
function handleRowCheckboxChange({
|
||||||
|
records,
|
||||||
|
}: {
|
||||||
|
records: SystemSmsTemplateApi.SmsTemplate[];
|
||||||
|
}) {
|
||||||
|
checkedIds.value = records.map((item) => item.id!);
|
||||||
|
}
|
||||||
|
|
||||||
|
const [Grid, gridApi] = useVbenVxeGrid({
|
||||||
|
formOptions: {
|
||||||
|
schema: useGridFormSchema(),
|
||||||
|
},
|
||||||
|
gridOptions: {
|
||||||
|
columns: useGridColumns(),
|
||||||
|
height: 'auto',
|
||||||
|
keepSource: true,
|
||||||
|
proxyConfig: {
|
||||||
|
ajax: {
|
||||||
|
query: async ({ page }, formValues) => {
|
||||||
|
return await getSmsTemplatePage({
|
||||||
|
pageNo: page.currentPage,
|
||||||
|
pageSize: page.pageSize,
|
||||||
|
...formValues,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
rowConfig: {
|
||||||
|
keyField: 'id',
|
||||||
|
isHover: true,
|
||||||
|
},
|
||||||
|
toolbarConfig: {
|
||||||
|
refresh: true,
|
||||||
|
search: true,
|
||||||
|
},
|
||||||
|
} as VxeTableGridOptions<SystemSmsTemplateApi.SmsTemplate>,
|
||||||
|
gridEvents: {
|
||||||
|
checkboxAll: handleRowCheckboxChange,
|
||||||
|
checkboxChange: handleRowCheckboxChange,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Page auto-content-height>
|
||||||
|
<template #doc>
|
||||||
|
<DocAlert title="短信配置" url="https://doc.iocoder.cn/sms/" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<FormModal @success="handleRefresh" />
|
||||||
|
<SendModal />
|
||||||
|
<Grid table-title="短信模板列表">
|
||||||
|
<template #toolbar-tools>
|
||||||
|
<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,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: $t('ui.actionTitle.deleteBatch'),
|
||||||
|
type: 'error',
|
||||||
|
icon: ACTION_ICON.DELETE,
|
||||||
|
disabled: isEmpty(checkedIds),
|
||||||
|
auth: ['system:sms-template:delete'],
|
||||||
|
onClick: handleDeleteBatch,
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<template #actions="{ row }">
|
||||||
|
<TableAction
|
||||||
|
:actions="[
|
||||||
|
{
|
||||||
|
label: $t('common.edit'),
|
||||||
|
type: 'primary',
|
||||||
|
text: true,
|
||||||
|
icon: ACTION_ICON.EDIT,
|
||||||
|
auth: ['system:sms-template:update'],
|
||||||
|
onClick: handleEdit.bind(null, row),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '测试',
|
||||||
|
type: 'primary',
|
||||||
|
text: true,
|
||||||
|
icon: ACTION_ICON.VIEW,
|
||||||
|
auth: ['system:sms-template:send-sms'],
|
||||||
|
onClick: handleSend.bind(null, row),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: $t('common.delete'),
|
||||||
|
type: 'error',
|
||||||
|
text: 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>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { SystemSmsTemplateApi } from '#/api/system/sms/template';
|
||||||
|
|
||||||
|
import { computed, ref } from 'vue';
|
||||||
|
|
||||||
|
import { useVbenModal } from '@vben/common-ui';
|
||||||
|
|
||||||
|
import { useVbenForm } from '#/adapter/form';
|
||||||
|
import { message } from '#/adapter/naive';
|
||||||
|
import {
|
||||||
|
createSmsTemplate,
|
||||||
|
getSmsTemplate,
|
||||||
|
updateSmsTemplate,
|
||||||
|
} from '#/api/system/sms/template';
|
||||||
|
import { $t } from '#/locales';
|
||||||
|
|
||||||
|
import { useFormSchema } from '../data';
|
||||||
|
|
||||||
|
const emit = defineEmits(['success']);
|
||||||
|
const formData = ref<SystemSmsTemplateApi.SmsTemplate>();
|
||||||
|
const getTitle = computed(() => {
|
||||||
|
return formData.value?.id
|
||||||
|
? $t('ui.actionTitle.edit', ['短信模板'])
|
||||||
|
: $t('ui.actionTitle.create', ['短信模板']);
|
||||||
|
});
|
||||||
|
|
||||||
|
const [Form, formApi] = useVbenForm({
|
||||||
|
commonConfig: {
|
||||||
|
componentProps: {
|
||||||
|
class: 'w-full',
|
||||||
|
},
|
||||||
|
formItemClass: 'col-span-2',
|
||||||
|
labelWidth: 80,
|
||||||
|
},
|
||||||
|
layout: 'horizontal',
|
||||||
|
schema: useFormSchema(),
|
||||||
|
showDefaultActions: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const [Modal, modalApi] = useVbenModal({
|
||||||
|
async onConfirm() {
|
||||||
|
const { valid } = await formApi.validate();
|
||||||
|
if (!valid) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
modalApi.lock();
|
||||||
|
// 提交表单
|
||||||
|
const data =
|
||||||
|
(await formApi.getValues()) as SystemSmsTemplateApi.SmsTemplate;
|
||||||
|
try {
|
||||||
|
await (formData.value?.id
|
||||||
|
? updateSmsTemplate(data)
|
||||||
|
: createSmsTemplate(data));
|
||||||
|
// 关闭并提示
|
||||||
|
await modalApi.close();
|
||||||
|
emit('success');
|
||||||
|
message.success($t('ui.actionMessage.operationSuccess'));
|
||||||
|
} finally {
|
||||||
|
modalApi.unlock();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async onOpenChange(isOpen: boolean) {
|
||||||
|
if (!isOpen) {
|
||||||
|
formData.value = undefined;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 加载数据
|
||||||
|
const data = modalApi.getData<SystemSmsTemplateApi.SmsTemplate>();
|
||||||
|
if (!data || !data.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
modalApi.lock();
|
||||||
|
try {
|
||||||
|
formData.value = await getSmsTemplate(data.id);
|
||||||
|
// 设置到 values
|
||||||
|
await formApi.setValues(formData.value);
|
||||||
|
} finally {
|
||||||
|
modalApi.unlock();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Modal :title="getTitle">
|
||||||
|
<Form class="mx-4" />
|
||||||
|
</Modal>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { SystemSmsTemplateApi } from '#/api/system/sms/template';
|
||||||
|
|
||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
import { useVbenModal } from '@vben/common-ui';
|
||||||
|
|
||||||
|
import { useVbenForm } from '#/adapter/form';
|
||||||
|
import { message } from '#/adapter/naive';
|
||||||
|
import { sendSms } from '#/api/system/sms/template';
|
||||||
|
|
||||||
|
import { useSendSmsFormSchema } from '../data';
|
||||||
|
|
||||||
|
const emit = defineEmits(['success']);
|
||||||
|
const formData = ref<SystemSmsTemplateApi.SmsTemplate>();
|
||||||
|
|
||||||
|
const [Form, formApi] = useVbenForm({
|
||||||
|
commonConfig: {
|
||||||
|
componentProps: {
|
||||||
|
class: 'w-full',
|
||||||
|
},
|
||||||
|
formItemClass: 'col-span-2',
|
||||||
|
labelWidth: 80,
|
||||||
|
},
|
||||||
|
layout: 'horizontal',
|
||||||
|
showDefaultActions: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const [Modal, modalApi] = useVbenModal({
|
||||||
|
async onConfirm() {
|
||||||
|
const { valid } = await formApi.validate();
|
||||||
|
if (!valid) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
modalApi.lock();
|
||||||
|
// 构建发送请求
|
||||||
|
const values = await formApi.getValues();
|
||||||
|
const paramsObj: Record<string, string> = {};
|
||||||
|
if (formData.value?.params) {
|
||||||
|
formData.value.params.forEach((param) => {
|
||||||
|
paramsObj[param] = values[`param_${param}`];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const data: SystemSmsTemplateApi.SmsSendReqVO = {
|
||||||
|
mobile: values.mobile,
|
||||||
|
templateCode: formData.value?.code || '',
|
||||||
|
templateParams: paramsObj,
|
||||||
|
};
|
||||||
|
|
||||||
|
// 提交表单
|
||||||
|
try {
|
||||||
|
await sendSms(data);
|
||||||
|
// 关闭并提示
|
||||||
|
await modalApi.close();
|
||||||
|
emit('success');
|
||||||
|
message.success('短信发送成功');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('发送短信失败', error);
|
||||||
|
} finally {
|
||||||
|
modalApi.unlock();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async onOpenChange(isOpen: boolean) {
|
||||||
|
if (!isOpen) {
|
||||||
|
formData.value = undefined;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 获取数据
|
||||||
|
const data = modalApi.getData<SystemSmsTemplateApi.SmsTemplate>();
|
||||||
|
if (!data) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
formData.value = data;
|
||||||
|
// 更新 form schema
|
||||||
|
const schema = buildFormSchema();
|
||||||
|
formApi.setState({ schema });
|
||||||
|
// 设置到 values
|
||||||
|
await formApi.setValues({
|
||||||
|
content: data.content,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 动态构建表单 schema */
|
||||||
|
function buildFormSchema() {
|
||||||
|
const schema = useSendSmsFormSchema();
|
||||||
|
if (formData.value?.params) {
|
||||||
|
formData.value.params.forEach((param) => {
|
||||||
|
schema.push({
|
||||||
|
fieldName: `param_${param}`,
|
||||||
|
label: `参数 ${param}`,
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: `请输入参数 ${param}`,
|
||||||
|
},
|
||||||
|
rules: 'required',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return schema;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Modal class="w-1/3" title="发送短信">
|
||||||
|
<Form class="mx-4" />
|
||||||
|
</Modal>
|
||||||
|
</template>
|
||||||
211
apps/web-naive/src/views/system/social/client/data.ts
Normal file
211
apps/web-naive/src/views/system/social/client/data.ts
Normal file
@@ -0,0 +1,211 @@
|
|||||||
|
import type { VbenFormSchema } from '#/adapter/form';
|
||||||
|
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||||
|
|
||||||
|
import {
|
||||||
|
CommonStatusEnum,
|
||||||
|
DICT_TYPE,
|
||||||
|
SystemUserSocialTypeEnum,
|
||||||
|
} from '@vben/constants';
|
||||||
|
import { getDictOptions } from '@vben/hooks';
|
||||||
|
|
||||||
|
import { z } from '#/adapter/form';
|
||||||
|
|
||||||
|
/** 新增/修改的表单 */
|
||||||
|
export function useFormSchema(): VbenFormSchema[] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
fieldName: 'id',
|
||||||
|
component: 'Input',
|
||||||
|
dependencies: {
|
||||||
|
triggerFields: [''],
|
||||||
|
show: () => false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'name',
|
||||||
|
label: '应用名',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入应用名',
|
||||||
|
},
|
||||||
|
rules: 'required',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'socialType',
|
||||||
|
label: '社交平台',
|
||||||
|
component: 'Select',
|
||||||
|
componentProps: {
|
||||||
|
options: getDictOptions(DICT_TYPE.SYSTEM_SOCIAL_TYPE, 'number'),
|
||||||
|
},
|
||||||
|
rules: 'required',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'userType',
|
||||||
|
label: '用户类型',
|
||||||
|
component: 'RadioGroup',
|
||||||
|
componentProps: {
|
||||||
|
options: getDictOptions(DICT_TYPE.USER_TYPE, 'number'),
|
||||||
|
buttonStyle: 'solid',
|
||||||
|
optionType: 'button',
|
||||||
|
},
|
||||||
|
rules: 'required',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'clientId',
|
||||||
|
label: '客户端编号',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入客户端编号,对应各平台的 appKey',
|
||||||
|
},
|
||||||
|
rules: 'required',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'clientSecret',
|
||||||
|
label: '客户端密钥',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入客户端密钥,对应各平台的 appSecret',
|
||||||
|
},
|
||||||
|
rules: 'required',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'agentId',
|
||||||
|
label: 'agentId',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '授权方的网页应用 ID,有则填',
|
||||||
|
},
|
||||||
|
dependencies: {
|
||||||
|
triggerFields: ['socialType'],
|
||||||
|
show: (values) =>
|
||||||
|
values.socialType === SystemUserSocialTypeEnum.WECHAT_ENTERPRISE.type,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'status',
|
||||||
|
label: '状态',
|
||||||
|
component: 'RadioGroup',
|
||||||
|
componentProps: {
|
||||||
|
options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
|
||||||
|
buttonStyle: 'solid',
|
||||||
|
optionType: 'button',
|
||||||
|
},
|
||||||
|
rules: z.number().default(CommonStatusEnum.ENABLE),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 列表的搜索表单 */
|
||||||
|
export function useGridFormSchema(): VbenFormSchema[] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
fieldName: 'name',
|
||||||
|
label: '应用名',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入应用名',
|
||||||
|
clearable: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'socialType',
|
||||||
|
label: '社交平台',
|
||||||
|
component: 'Select',
|
||||||
|
componentProps: {
|
||||||
|
options: getDictOptions(DICT_TYPE.SYSTEM_SOCIAL_TYPE, 'number'),
|
||||||
|
placeholder: '请选择社交平台',
|
||||||
|
clearable: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'userType',
|
||||||
|
label: '用户类型',
|
||||||
|
component: 'Select',
|
||||||
|
componentProps: {
|
||||||
|
options: getDictOptions(DICT_TYPE.USER_TYPE, 'number'),
|
||||||
|
placeholder: '请选择用户类型',
|
||||||
|
clearable: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'clientId',
|
||||||
|
label: '客户端编号',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入客户端编号',
|
||||||
|
clearable: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'status',
|
||||||
|
label: '状态',
|
||||||
|
component: 'Select',
|
||||||
|
componentProps: {
|
||||||
|
options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
|
||||||
|
placeholder: '请选择状态',
|
||||||
|
clearable: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 列表的字段 */
|
||||||
|
export function useGridColumns(): VxeTableGridOptions['columns'] {
|
||||||
|
return [
|
||||||
|
{ type: 'checkbox', width: 40 },
|
||||||
|
{
|
||||||
|
field: 'id',
|
||||||
|
title: '编号',
|
||||||
|
minWidth: 100,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'name',
|
||||||
|
title: '应用名',
|
||||||
|
minWidth: 120,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'socialType',
|
||||||
|
title: '社交平台',
|
||||||
|
minWidth: 100,
|
||||||
|
cellRender: {
|
||||||
|
name: 'CellDict',
|
||||||
|
props: { type: DICT_TYPE.SYSTEM_SOCIAL_TYPE },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'userType',
|
||||||
|
title: '用户类型',
|
||||||
|
minWidth: 100,
|
||||||
|
cellRender: {
|
||||||
|
name: 'CellDict',
|
||||||
|
props: { type: DICT_TYPE.USER_TYPE },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'clientId',
|
||||||
|
title: '客户端编号',
|
||||||
|
minWidth: 180,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'status',
|
||||||
|
title: '状态',
|
||||||
|
minWidth: 100,
|
||||||
|
cellRender: {
|
||||||
|
name: 'CellDict',
|
||||||
|
props: { type: DICT_TYPE.COMMON_STATUS },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'createTime',
|
||||||
|
title: '创建时间',
|
||||||
|
minWidth: 180,
|
||||||
|
formatter: 'formatDateTime',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '操作',
|
||||||
|
width: 220,
|
||||||
|
fixed: 'right',
|
||||||
|
slots: { default: 'actions' },
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
174
apps/web-naive/src/views/system/social/client/index.vue
Normal file
174
apps/web-naive/src/views/system/social/client/index.vue
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||||
|
import type { SystemSocialClientApi } from '#/api/system/social/client';
|
||||||
|
|
||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
import { confirm, DocAlert, Page, useVbenModal } from '@vben/common-ui';
|
||||||
|
import { isEmpty } from '@vben/utils';
|
||||||
|
|
||||||
|
import { message } from '#/adapter/naive';
|
||||||
|
|
||||||
|
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||||
|
import {
|
||||||
|
deleteSocialClient,
|
||||||
|
deleteSocialClientList,
|
||||||
|
getSocialClientPage,
|
||||||
|
} from '#/api/system/social/client';
|
||||||
|
import { $t } from '#/locales';
|
||||||
|
|
||||||
|
import { useGridColumns, useGridFormSchema } from './data';
|
||||||
|
import Form from './modules/form.vue';
|
||||||
|
|
||||||
|
const [FormModal, formModalApi] = useVbenModal({
|
||||||
|
connectedComponent: Form,
|
||||||
|
destroyOnClose: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 刷新表格 */
|
||||||
|
function handleRefresh() {
|
||||||
|
gridApi.query();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 创建社交客户端 */
|
||||||
|
function handleCreate() {
|
||||||
|
formModalApi.setData(null).open();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 编辑社交客户端 */
|
||||||
|
function handleEdit(row: SystemSocialClientApi.SocialClient) {
|
||||||
|
formModalApi.setData(row).open();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 删除社交客户端 */
|
||||||
|
async function handleDelete(row: SystemSocialClientApi.SocialClient) {
|
||||||
|
const hideLoading = message.loading({
|
||||||
|
content: $t('ui.actionMessage.deleting', [row.name]),
|
||||||
|
duration: 0,
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
await deleteSocialClient(row.id!);
|
||||||
|
message.success($t('ui.actionMessage.deleteSuccess', [row.name]));
|
||||||
|
handleRefresh();
|
||||||
|
} finally {
|
||||||
|
hideLoading.destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 批量删除社交客户端 */
|
||||||
|
async function handleDeleteBatch() {
|
||||||
|
await confirm($t('ui.actionMessage.deleteBatchConfirm'));
|
||||||
|
const hideLoading = message.loading({
|
||||||
|
content: $t('ui.actionMessage.deletingBatch'),
|
||||||
|
duration: 0,
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
await deleteSocialClientList(checkedIds.value);
|
||||||
|
checkedIds.value = [];
|
||||||
|
message.success($t('ui.actionMessage.deleteSuccess'));
|
||||||
|
handleRefresh();
|
||||||
|
} finally {
|
||||||
|
hideLoading.destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const checkedIds = ref<number[]>([]);
|
||||||
|
function handleRowCheckboxChange({
|
||||||
|
records,
|
||||||
|
}: {
|
||||||
|
records: SystemSocialClientApi.SocialClient[];
|
||||||
|
}) {
|
||||||
|
checkedIds.value = records.map((item) => item.id!);
|
||||||
|
}
|
||||||
|
|
||||||
|
const [Grid, gridApi] = useVbenVxeGrid({
|
||||||
|
formOptions: {
|
||||||
|
schema: useGridFormSchema(),
|
||||||
|
},
|
||||||
|
gridOptions: {
|
||||||
|
columns: useGridColumns(),
|
||||||
|
height: 'auto',
|
||||||
|
keepSource: true,
|
||||||
|
proxyConfig: {
|
||||||
|
ajax: {
|
||||||
|
query: async ({ page }, formValues) => {
|
||||||
|
return await getSocialClientPage({
|
||||||
|
pageNo: page.currentPage,
|
||||||
|
pageSize: page.pageSize,
|
||||||
|
...formValues,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
rowConfig: {
|
||||||
|
keyField: 'id',
|
||||||
|
isHover: true,
|
||||||
|
},
|
||||||
|
toolbarConfig: {
|
||||||
|
refresh: true,
|
||||||
|
search: true,
|
||||||
|
},
|
||||||
|
} as VxeTableGridOptions<SystemSocialClientApi.SocialClient>,
|
||||||
|
gridEvents: {
|
||||||
|
checkboxAll: handleRowCheckboxChange,
|
||||||
|
checkboxChange: handleRowCheckboxChange,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Page auto-content-height>
|
||||||
|
<template #doc>
|
||||||
|
<DocAlert title="三方登录" url="https://doc.iocoder.cn/social-user/" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<FormModal @success="handleRefresh" />
|
||||||
|
<Grid table-title="社交客户端列表">
|
||||||
|
<template #toolbar-tools>
|
||||||
|
<TableAction
|
||||||
|
:actions="[
|
||||||
|
{
|
||||||
|
label: $t('ui.actionTitle.create', ['社交客户端']),
|
||||||
|
type: 'primary',
|
||||||
|
icon: ACTION_ICON.ADD,
|
||||||
|
auth: ['system:social-client:create'],
|
||||||
|
onClick: handleCreate,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: $t('ui.actionTitle.deleteBatch'),
|
||||||
|
type: 'error',
|
||||||
|
icon: ACTION_ICON.DELETE,
|
||||||
|
auth: ['system:social-client:delete'],
|
||||||
|
disabled: isEmpty(checkedIds),
|
||||||
|
onClick: handleDeleteBatch,
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<template #actions="{ row }">
|
||||||
|
<TableAction
|
||||||
|
:actions="[
|
||||||
|
{
|
||||||
|
label: $t('common.edit'),
|
||||||
|
type: 'primary',
|
||||||
|
icon: ACTION_ICON.EDIT,
|
||||||
|
auth: ['system:social-client:update'],
|
||||||
|
onClick: handleEdit.bind(null, row),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: $t('common.delete'),
|
||||||
|
type: 'error',
|
||||||
|
text: 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>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { SystemSocialClientApi } from '#/api/system/social/client';
|
||||||
|
|
||||||
|
import { computed, ref } from 'vue';
|
||||||
|
|
||||||
|
import { useVbenModal } from '@vben/common-ui';
|
||||||
|
|
||||||
|
import { useVbenForm } from '#/adapter/form';
|
||||||
|
import { message } from '#/adapter/naive';
|
||||||
|
import {
|
||||||
|
createSocialClient,
|
||||||
|
getSocialClient,
|
||||||
|
updateSocialClient,
|
||||||
|
} from '#/api/system/social/client';
|
||||||
|
import { $t } from '#/locales';
|
||||||
|
|
||||||
|
import { useFormSchema } from '../data';
|
||||||
|
|
||||||
|
const emit = defineEmits(['success']);
|
||||||
|
const formData = ref<SystemSocialClientApi.SocialClient>();
|
||||||
|
const getTitle = computed(() => {
|
||||||
|
return formData.value?.id
|
||||||
|
? $t('ui.actionTitle.edit', ['社交客户端'])
|
||||||
|
: $t('ui.actionTitle.create', ['社交客户端']);
|
||||||
|
});
|
||||||
|
|
||||||
|
const [Form, formApi] = useVbenForm({
|
||||||
|
commonConfig: {
|
||||||
|
componentProps: {
|
||||||
|
class: 'w-full',
|
||||||
|
},
|
||||||
|
formItemClass: 'col-span-2',
|
||||||
|
labelWidth: 80,
|
||||||
|
},
|
||||||
|
layout: 'horizontal',
|
||||||
|
schema: useFormSchema(),
|
||||||
|
showDefaultActions: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const [Modal, modalApi] = useVbenModal({
|
||||||
|
async onConfirm() {
|
||||||
|
const { valid } = await formApi.validate();
|
||||||
|
if (!valid) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
modalApi.lock();
|
||||||
|
// 提交表单
|
||||||
|
const data =
|
||||||
|
(await formApi.getValues()) as SystemSocialClientApi.SocialClient;
|
||||||
|
try {
|
||||||
|
await (formData.value?.id
|
||||||
|
? updateSocialClient(data)
|
||||||
|
: createSocialClient(data));
|
||||||
|
// 关闭并提示
|
||||||
|
await modalApi.close();
|
||||||
|
emit('success');
|
||||||
|
message.success($t('ui.actionMessage.operationSuccess'));
|
||||||
|
} finally {
|
||||||
|
modalApi.unlock();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async onOpenChange(isOpen: boolean) {
|
||||||
|
if (!isOpen) {
|
||||||
|
formData.value = undefined;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 加载数据
|
||||||
|
const data = modalApi.getData<SystemSocialClientApi.SocialClient>();
|
||||||
|
if (!data || !data.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
modalApi.lock();
|
||||||
|
try {
|
||||||
|
formData.value = await getSocialClient(data.id);
|
||||||
|
// 设置到 values
|
||||||
|
await formApi.setValues(formData.value);
|
||||||
|
} finally {
|
||||||
|
modalApi.unlock();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Modal :title="getTitle">
|
||||||
|
<Form class="mx-4" />
|
||||||
|
</Modal>
|
||||||
|
</template>
|
||||||
164
apps/web-naive/src/views/system/social/user/data.ts
Normal file
164
apps/web-naive/src/views/system/social/user/data.ts
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
import type { VbenFormSchema } from '#/adapter/form';
|
||||||
|
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||||
|
import type { SystemSocialUserApi } from '#/api/system/social/user';
|
||||||
|
import type { DescriptionItemSchema } from '#/components/description';
|
||||||
|
|
||||||
|
import { h } from 'vue';
|
||||||
|
|
||||||
|
import { DICT_TYPE } from '@vben/constants';
|
||||||
|
import { getDictOptions } from '@vben/hooks';
|
||||||
|
|
||||||
|
import { DictTag } from '#/components/dict-tag';
|
||||||
|
import { getRangePickerDefaultProps } from '#/utils';
|
||||||
|
|
||||||
|
/** 列表的搜索表单 */
|
||||||
|
export function useGridFormSchema(): VbenFormSchema[] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
fieldName: 'type',
|
||||||
|
label: '社交平台',
|
||||||
|
component: 'Select',
|
||||||
|
componentProps: {
|
||||||
|
options: getDictOptions(DICT_TYPE.SYSTEM_SOCIAL_TYPE, 'number'),
|
||||||
|
placeholder: '请选择社交平台',
|
||||||
|
clearable: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'nickname',
|
||||||
|
label: '用户昵称',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入用户昵称',
|
||||||
|
clearable: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'openid',
|
||||||
|
label: '社交 openid',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入社交 openid',
|
||||||
|
clearable: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'createTime',
|
||||||
|
label: '创建时间',
|
||||||
|
component: 'DatePicker',
|
||||||
|
componentProps: {
|
||||||
|
...getRangePickerDefaultProps(),
|
||||||
|
clearable: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 列表的字段 */
|
||||||
|
export function useGridColumns(): VxeTableGridOptions['columns'] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
field: 'type',
|
||||||
|
title: '社交平台',
|
||||||
|
minWidth: 100,
|
||||||
|
cellRender: {
|
||||||
|
name: 'CellDict',
|
||||||
|
props: { type: DICT_TYPE.SYSTEM_SOCIAL_TYPE },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'openid',
|
||||||
|
title: '社交 openid',
|
||||||
|
minWidth: 180,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'nickname',
|
||||||
|
title: '用户昵称',
|
||||||
|
minWidth: 120,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'avatar',
|
||||||
|
title: '用户头像',
|
||||||
|
minWidth: 100,
|
||||||
|
cellRender: {
|
||||||
|
name: 'CellImage',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'createTime',
|
||||||
|
title: '创建时间',
|
||||||
|
minWidth: 180,
|
||||||
|
formatter: 'formatDateTime',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'updateTime',
|
||||||
|
title: '更新时间',
|
||||||
|
minWidth: 180,
|
||||||
|
formatter: 'formatDateTime',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '操作',
|
||||||
|
width: 120,
|
||||||
|
fixed: 'right',
|
||||||
|
slots: { default: 'actions' },
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 详情页的字段 */
|
||||||
|
export function useDetailSchema(): DescriptionItemSchema[] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
field: 'type',
|
||||||
|
label: '社交平台',
|
||||||
|
content: (data: SystemSocialUserApi.SocialUser) => {
|
||||||
|
return h(DictTag, {
|
||||||
|
type: DICT_TYPE.SYSTEM_SOCIAL_TYPE,
|
||||||
|
value: data?.type,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'nickname',
|
||||||
|
label: '用户昵称',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'avatar',
|
||||||
|
label: '用户头像',
|
||||||
|
// TODO @芋艿:使用 antd 的 Image 组件
|
||||||
|
content: (data: SystemSocialUserApi.SocialUser) => {
|
||||||
|
if (data?.avatar) {
|
||||||
|
return h('img', {
|
||||||
|
src: data.avatar,
|
||||||
|
style: 'width: 30px; height: 30px; cursor: pointer;',
|
||||||
|
onClick: () => {
|
||||||
|
// 可以添加图片预览功能
|
||||||
|
window.open(data.avatar, '_blank');
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return '无';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'token',
|
||||||
|
label: '社交 token',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'rawTokenInfo',
|
||||||
|
label: '原始 Token 数据',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'rawUserInfo',
|
||||||
|
label: '原始 User 数据',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'code',
|
||||||
|
label: '最后一次的认证 code',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'state',
|
||||||
|
label: '最后一次的认证 state',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
78
apps/web-naive/src/views/system/social/user/index.vue
Normal file
78
apps/web-naive/src/views/system/social/user/index.vue
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||||
|
import type { SystemSocialUserApi } from '#/api/system/social/user';
|
||||||
|
|
||||||
|
import { DocAlert, Page, useVbenModal } from '@vben/common-ui';
|
||||||
|
|
||||||
|
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||||
|
import { getSocialUserPage } from '#/api/system/social/user';
|
||||||
|
import { $t } from '#/locales';
|
||||||
|
|
||||||
|
import { useGridColumns, useGridFormSchema } from './data';
|
||||||
|
import Detail from './modules/detail.vue';
|
||||||
|
|
||||||
|
const [DetailModal, detailModalApi] = useVbenModal({
|
||||||
|
connectedComponent: Detail,
|
||||||
|
destroyOnClose: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 查看详情 */
|
||||||
|
function handleDetail(row: SystemSocialUserApi.SocialUser) {
|
||||||
|
detailModalApi.setData(row).open();
|
||||||
|
}
|
||||||
|
|
||||||
|
const [Grid] = useVbenVxeGrid({
|
||||||
|
formOptions: {
|
||||||
|
schema: useGridFormSchema(),
|
||||||
|
},
|
||||||
|
gridOptions: {
|
||||||
|
columns: useGridColumns(),
|
||||||
|
height: 'auto',
|
||||||
|
keepSource: true,
|
||||||
|
proxyConfig: {
|
||||||
|
ajax: {
|
||||||
|
query: async ({ page }, formValues) => {
|
||||||
|
return await getSocialUserPage({
|
||||||
|
pageNo: page.currentPage,
|
||||||
|
pageSize: page.pageSize,
|
||||||
|
...formValues,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
rowConfig: {
|
||||||
|
keyField: 'id',
|
||||||
|
isHover: true,
|
||||||
|
},
|
||||||
|
toolbarConfig: {
|
||||||
|
refresh: true,
|
||||||
|
search: true,
|
||||||
|
},
|
||||||
|
} as VxeTableGridOptions<SystemSocialUserApi.SocialUser>,
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Page auto-content-height>
|
||||||
|
<template #doc>
|
||||||
|
<DocAlert title="三方登录" url="https://doc.iocoder.cn/social-user/" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<DetailModal />
|
||||||
|
<Grid table-title="社交用户列表">
|
||||||
|
<template #actions="{ row }">
|
||||||
|
<TableAction
|
||||||
|
:actions="[
|
||||||
|
{
|
||||||
|
label: $t('common.detail'),
|
||||||
|
type: 'primary',
|
||||||
|
icon: ACTION_ICON.VIEW,
|
||||||
|
auth: ['system:social-user:query'],
|
||||||
|
onClick: handleDetail.bind(null, row),
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</Grid>
|
||||||
|
</Page>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { SystemSocialUserApi } from '#/api/system/social/user';
|
||||||
|
|
||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
import { useVbenModal } from '@vben/common-ui';
|
||||||
|
|
||||||
|
import { getSocialUser } from '#/api/system/social/user';
|
||||||
|
import { useDescription } from '#/components/description';
|
||||||
|
import { $t } from '#/locales';
|
||||||
|
|
||||||
|
import { useDetailSchema } from '../data';
|
||||||
|
|
||||||
|
const formData = ref<SystemSocialUserApi.SocialUser>();
|
||||||
|
|
||||||
|
const [Descriptions] = useDescription({
|
||||||
|
componentProps: {
|
||||||
|
bordered: true,
|
||||||
|
column: 1,
|
||||||
|
size: 'middle',
|
||||||
|
class: 'mx-4',
|
||||||
|
labelStyle: { width: '185px' },
|
||||||
|
},
|
||||||
|
schema: useDetailSchema(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const [Modal, modalApi] = useVbenModal({
|
||||||
|
title: $t('ui.actionTitle.detail'),
|
||||||
|
async onOpenChange(isOpen: boolean) {
|
||||||
|
if (!isOpen) {
|
||||||
|
formData.value = undefined;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 加载数据
|
||||||
|
const data = modalApi.getData();
|
||||||
|
if (!data || !data.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
modalApi.lock();
|
||||||
|
try {
|
||||||
|
formData.value = await getSocialUser(data.id);
|
||||||
|
} finally {
|
||||||
|
modalApi.unlock();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Modal
|
||||||
|
title="社交用户详情"
|
||||||
|
class="w-1/2"
|
||||||
|
:show-cancel-button="false"
|
||||||
|
:show-confirm-button="false"
|
||||||
|
>
|
||||||
|
<Descriptions :data="formData" />
|
||||||
|
</Modal>
|
||||||
|
</template>
|
||||||
247
apps/web-naive/src/views/system/tenant/data.ts
Normal file
247
apps/web-naive/src/views/system/tenant/data.ts
Normal file
@@ -0,0 +1,247 @@
|
|||||||
|
import type { VbenFormSchema } from '#/adapter/form';
|
||||||
|
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||||
|
import type { SystemTenantPackageApi } from '#/api/system/tenant-package';
|
||||||
|
|
||||||
|
import { CommonStatusEnum, DICT_TYPE } from '@vben/constants';
|
||||||
|
import { getDictOptions } from '@vben/hooks';
|
||||||
|
|
||||||
|
import { z } from '#/adapter/form';
|
||||||
|
import { getTenantPackageList } from '#/api/system/tenant-package';
|
||||||
|
import { getRangePickerDefaultProps } from '#/utils';
|
||||||
|
|
||||||
|
// TODO @xingyu:这个不用 ref 么?
|
||||||
|
let tenantPackageList: SystemTenantPackageApi.TenantPackage[] = [];
|
||||||
|
|
||||||
|
async function getTenantPackageData() {
|
||||||
|
tenantPackageList = await getTenantPackageList();
|
||||||
|
}
|
||||||
|
|
||||||
|
getTenantPackageData();
|
||||||
|
|
||||||
|
/** 新增/修改的表单 */
|
||||||
|
export function useFormSchema(): VbenFormSchema[] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
fieldName: 'id',
|
||||||
|
component: 'Input',
|
||||||
|
dependencies: {
|
||||||
|
triggerFields: [''],
|
||||||
|
show: () => false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'name',
|
||||||
|
label: '租户名称',
|
||||||
|
component: 'Input',
|
||||||
|
rules: 'required',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'packageId',
|
||||||
|
label: '租户套餐',
|
||||||
|
component: 'ApiSelect',
|
||||||
|
componentProps: {
|
||||||
|
api: () => getTenantPackageList(),
|
||||||
|
labelField: 'name',
|
||||||
|
valueField: 'id',
|
||||||
|
placeholder: '请选择租户套餐',
|
||||||
|
},
|
||||||
|
rules: 'required',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'contactName',
|
||||||
|
label: '联系人',
|
||||||
|
component: 'Input',
|
||||||
|
rules: 'required',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'contactMobile',
|
||||||
|
label: '联系手机',
|
||||||
|
component: 'Input',
|
||||||
|
rules: 'mobile',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '用户名称',
|
||||||
|
fieldName: 'username',
|
||||||
|
component: 'Input',
|
||||||
|
rules: 'required',
|
||||||
|
dependencies: {
|
||||||
|
triggerFields: ['id'],
|
||||||
|
show: (values) => !values.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '用户密码',
|
||||||
|
fieldName: 'password',
|
||||||
|
component: 'InputPassword',
|
||||||
|
rules: 'required',
|
||||||
|
dependencies: {
|
||||||
|
triggerFields: ['id'],
|
||||||
|
show: (values) => !values.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '账号额度',
|
||||||
|
fieldName: 'accountCount',
|
||||||
|
component: 'InputNumber',
|
||||||
|
rules: 'required',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '过期时间',
|
||||||
|
fieldName: 'expireTime',
|
||||||
|
component: 'DatePicker',
|
||||||
|
componentProps: {
|
||||||
|
format: 'YYYY-MM-DD',
|
||||||
|
valueFormat: 'x',
|
||||||
|
placeholder: '请选择过期时间',
|
||||||
|
},
|
||||||
|
rules: 'required',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '绑定域名',
|
||||||
|
fieldName: 'websites',
|
||||||
|
component: 'Select',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入绑定域名',
|
||||||
|
mode: 'tags',
|
||||||
|
clearable: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'status',
|
||||||
|
label: '租户状态',
|
||||||
|
component: 'RadioGroup',
|
||||||
|
componentProps: {
|
||||||
|
options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
|
||||||
|
buttonStyle: 'solid',
|
||||||
|
optionType: 'button',
|
||||||
|
},
|
||||||
|
rules: z.number().default(CommonStatusEnum.ENABLE),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 列表的搜索表单 */
|
||||||
|
export function useGridFormSchema(): VbenFormSchema[] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
fieldName: 'name',
|
||||||
|
label: '租户名',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入租户名',
|
||||||
|
clearable: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'contactName',
|
||||||
|
label: '联系人',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入联系人',
|
||||||
|
clearable: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'contactMobile',
|
||||||
|
label: '联系手机',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入联系手机',
|
||||||
|
clearable: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'status',
|
||||||
|
label: '状态',
|
||||||
|
component: 'Select',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请选择状态',
|
||||||
|
clearable: true,
|
||||||
|
options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'createTime',
|
||||||
|
label: '创建时间',
|
||||||
|
component: 'DatePicker',
|
||||||
|
componentProps: {
|
||||||
|
...getRangePickerDefaultProps(),
|
||||||
|
clearable: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 列表的字段 */
|
||||||
|
export function useGridColumns(): VxeTableGridOptions['columns'] {
|
||||||
|
return [
|
||||||
|
{ type: 'checkbox', width: 40 },
|
||||||
|
{
|
||||||
|
field: 'id',
|
||||||
|
title: '租户编号',
|
||||||
|
minWidth: 100,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'name',
|
||||||
|
title: '租户名',
|
||||||
|
minWidth: 180,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'packageId',
|
||||||
|
title: '租户套餐',
|
||||||
|
minWidth: 180,
|
||||||
|
formatter: ({ cellValue }) => {
|
||||||
|
return cellValue === 0
|
||||||
|
? '系统租户'
|
||||||
|
: tenantPackageList.find((pkg) => pkg.id === cellValue)?.name || '-';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'contactName',
|
||||||
|
title: '联系人',
|
||||||
|
minWidth: 100,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'contactMobile',
|
||||||
|
title: '联系手机',
|
||||||
|
minWidth: 180,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'accountCount',
|
||||||
|
title: '账号额度',
|
||||||
|
minWidth: 100,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'expireTime',
|
||||||
|
title: '过期时间',
|
||||||
|
minWidth: 180,
|
||||||
|
formatter: 'formatDateTime',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'websites',
|
||||||
|
title: '绑定域名',
|
||||||
|
minWidth: 180,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'status',
|
||||||
|
title: '租户状态',
|
||||||
|
minWidth: 100,
|
||||||
|
cellRender: {
|
||||||
|
name: 'CellDict',
|
||||||
|
props: { type: DICT_TYPE.COMMON_STATUS },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'createTime',
|
||||||
|
title: '创建时间',
|
||||||
|
minWidth: 180,
|
||||||
|
formatter: 'formatDateTime',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '操作',
|
||||||
|
width: 130,
|
||||||
|
fixed: 'right',
|
||||||
|
slots: { default: 'actions' },
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
185
apps/web-naive/src/views/system/tenant/index.vue
Normal file
185
apps/web-naive/src/views/system/tenant/index.vue
Normal file
@@ -0,0 +1,185 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||||
|
import type { SystemTenantApi } from '#/api/system/tenant';
|
||||||
|
|
||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
import { confirm, DocAlert, Page, useVbenModal } from '@vben/common-ui';
|
||||||
|
import { downloadFileFromBlobPart, isEmpty } from '@vben/utils';
|
||||||
|
|
||||||
|
import { message } from '#/adapter/naive';
|
||||||
|
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||||
|
import {
|
||||||
|
deleteTenant,
|
||||||
|
deleteTenantList,
|
||||||
|
exportTenant,
|
||||||
|
getTenantPage,
|
||||||
|
} from '#/api/system/tenant';
|
||||||
|
import { $t } from '#/locales';
|
||||||
|
|
||||||
|
import { useGridColumns, useGridFormSchema } from './data';
|
||||||
|
import Form from './modules/form.vue';
|
||||||
|
|
||||||
|
const [FormModal, formModalApi] = useVbenModal({
|
||||||
|
connectedComponent: Form,
|
||||||
|
destroyOnClose: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 刷新表格 */
|
||||||
|
function handleRefresh() {
|
||||||
|
gridApi.query();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 导出表格 */
|
||||||
|
async function handleExport() {
|
||||||
|
const data = await exportTenant(await gridApi.formApi.getValues());
|
||||||
|
downloadFileFromBlobPart({ fileName: '租户.xls', source: data });
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 创建租户 */
|
||||||
|
function handleCreate() {
|
||||||
|
formModalApi.setData(null).open();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 编辑租户 */
|
||||||
|
function handleEdit(row: SystemTenantApi.Tenant) {
|
||||||
|
formModalApi.setData(row).open();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 删除租户 */
|
||||||
|
async function handleDelete(row: SystemTenantApi.Tenant) {
|
||||||
|
const hideLoading = message.loading(
|
||||||
|
$t('ui.actionMessage.deleting', [row.name]),
|
||||||
|
{ duration: 0 },
|
||||||
|
);
|
||||||
|
try {
|
||||||
|
await deleteTenant(row.id!);
|
||||||
|
message.success($t('ui.actionMessage.deleteSuccess', [row.name]));
|
||||||
|
handleRefresh();
|
||||||
|
} finally {
|
||||||
|
hideLoading.destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 批量删除租户 */
|
||||||
|
async function handleDeleteBatch() {
|
||||||
|
await confirm($t('ui.actionMessage.deleteBatchConfirm'));
|
||||||
|
const hideLoading = message.loading($t('ui.actionMessage.deletingBatch'), {
|
||||||
|
duration: 0,
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
await deleteTenantList(checkedIds.value);
|
||||||
|
checkedIds.value = [];
|
||||||
|
message.success($t('ui.actionMessage.deleteSuccess'));
|
||||||
|
handleRefresh();
|
||||||
|
} finally {
|
||||||
|
hideLoading.destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const checkedIds = ref<number[]>([]);
|
||||||
|
function handleRowCheckboxChange({
|
||||||
|
records,
|
||||||
|
}: {
|
||||||
|
records: SystemTenantApi.Tenant[];
|
||||||
|
}) {
|
||||||
|
checkedIds.value = records.map((item) => item.id!);
|
||||||
|
}
|
||||||
|
|
||||||
|
const [Grid, gridApi] = useVbenVxeGrid({
|
||||||
|
formOptions: {
|
||||||
|
schema: useGridFormSchema(),
|
||||||
|
},
|
||||||
|
gridOptions: {
|
||||||
|
columns: useGridColumns(),
|
||||||
|
height: 'auto',
|
||||||
|
keepSource: true,
|
||||||
|
proxyConfig: {
|
||||||
|
ajax: {
|
||||||
|
query: async ({ page }, formValues) => {
|
||||||
|
return await getTenantPage({
|
||||||
|
pageNo: page.currentPage,
|
||||||
|
pageSize: page.pageSize,
|
||||||
|
...formValues,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
rowConfig: {
|
||||||
|
keyField: 'id',
|
||||||
|
isHover: true,
|
||||||
|
},
|
||||||
|
toolbarConfig: {
|
||||||
|
refresh: true,
|
||||||
|
search: true,
|
||||||
|
},
|
||||||
|
} as VxeTableGridOptions<SystemTenantApi.Tenant>,
|
||||||
|
gridEvents: {
|
||||||
|
checkboxAll: handleRowCheckboxChange,
|
||||||
|
checkboxChange: handleRowCheckboxChange,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<Page auto-content-height>
|
||||||
|
<template #doc>
|
||||||
|
<DocAlert title="SaaS 多租户" url="https://doc.iocoder.cn/saas-tenant/" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<FormModal @success="handleRefresh" />
|
||||||
|
<Grid table-title="租户列表">
|
||||||
|
<template #toolbar-tools>
|
||||||
|
<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,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: $t('ui.actionTitle.deleteBatch'),
|
||||||
|
type: 'error',
|
||||||
|
icon: ACTION_ICON.DELETE,
|
||||||
|
disabled: isEmpty(checkedIds),
|
||||||
|
auth: ['system:tenant:delete'],
|
||||||
|
onClick: handleDeleteBatch,
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<template #actions="{ row }">
|
||||||
|
<TableAction
|
||||||
|
:actions="[
|
||||||
|
{
|
||||||
|
label: $t('common.edit'),
|
||||||
|
type: 'primary',
|
||||||
|
icon: ACTION_ICON.EDIT,
|
||||||
|
auth: ['system:tenant:update'],
|
||||||
|
onClick: handleEdit.bind(null, row),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: $t('common.delete'),
|
||||||
|
type: 'error',
|
||||||
|
text: true,
|
||||||
|
icon: ACTION_ICON.DELETE,
|
||||||
|
auth: ['system:tenant:delete'],
|
||||||
|
popConfirm: {
|
||||||
|
title: $t('ui.actionMessage.deleteConfirm', [row.name]),
|
||||||
|
confirm: handleDelete.bind(null, row),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</Grid>
|
||||||
|
</Page>
|
||||||
|
</template>
|
||||||
80
apps/web-naive/src/views/system/tenant/modules/form.vue
Normal file
80
apps/web-naive/src/views/system/tenant/modules/form.vue
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { SystemTenantApi } from '#/api/system/tenant';
|
||||||
|
|
||||||
|
import { computed, ref } from 'vue';
|
||||||
|
|
||||||
|
import { useVbenModal } from '@vben/common-ui';
|
||||||
|
|
||||||
|
import { message } from '#/adapter/naive';
|
||||||
|
|
||||||
|
import { useVbenForm } from '#/adapter/form';
|
||||||
|
import { createTenant, getTenant, updateTenant } from '#/api/system/tenant';
|
||||||
|
import { $t } from '#/locales';
|
||||||
|
|
||||||
|
import { useFormSchema } from '../data';
|
||||||
|
|
||||||
|
const emit = defineEmits(['success']);
|
||||||
|
const formData = ref<SystemTenantApi.Tenant>();
|
||||||
|
const getTitle = computed(() => {
|
||||||
|
return formData.value
|
||||||
|
? $t('ui.actionTitle.edit', ['租户'])
|
||||||
|
: $t('ui.actionTitle.create', ['租户']);
|
||||||
|
});
|
||||||
|
|
||||||
|
const [Form, formApi] = useVbenForm({
|
||||||
|
commonConfig: {
|
||||||
|
componentProps: {
|
||||||
|
class: 'w-full',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wrapperClass: 'grid-cols-1',
|
||||||
|
layout: 'horizontal',
|
||||||
|
schema: useFormSchema(),
|
||||||
|
showDefaultActions: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const [Modal, modalApi] = useVbenModal({
|
||||||
|
async onConfirm() {
|
||||||
|
const { valid } = await formApi.validate();
|
||||||
|
if (!valid) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
modalApi.lock();
|
||||||
|
// 提交表单
|
||||||
|
const data = (await formApi.getValues()) as SystemTenantApi.Tenant;
|
||||||
|
try {
|
||||||
|
await (formData.value ? updateTenant(data) : createTenant(data));
|
||||||
|
// 关闭并提示
|
||||||
|
await modalApi.close();
|
||||||
|
emit('success');
|
||||||
|
message.success($t('ui.actionMessage.operationSuccess'));
|
||||||
|
} finally {
|
||||||
|
modalApi.unlock();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async onOpenChange(isOpen: boolean) {
|
||||||
|
if (!isOpen) {
|
||||||
|
formData.value = undefined;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 加载数据
|
||||||
|
const data = modalApi.getData<SystemTenantApi.Tenant>();
|
||||||
|
if (!data || !data.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
modalApi.lock();
|
||||||
|
try {
|
||||||
|
formData.value = await getTenant(data.id);
|
||||||
|
// 设置到 values
|
||||||
|
await formApi.setValues(formData.value);
|
||||||
|
} finally {
|
||||||
|
modalApi.unlock();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<Modal :title="getTitle">
|
||||||
|
<Form class="mx-4" />
|
||||||
|
</Modal>
|
||||||
|
</template>
|
||||||
130
apps/web-naive/src/views/system/tenantPackage/data.ts
Normal file
130
apps/web-naive/src/views/system/tenantPackage/data.ts
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
import type { VbenFormSchema } from '#/adapter/form';
|
||||||
|
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||||
|
|
||||||
|
import { CommonStatusEnum, DICT_TYPE } from '@vben/constants';
|
||||||
|
import { getDictOptions } from '@vben/hooks';
|
||||||
|
|
||||||
|
import { z } from '#/adapter/form';
|
||||||
|
import { getRangePickerDefaultProps } from '#/utils';
|
||||||
|
|
||||||
|
/** 新增/修改的表单 */
|
||||||
|
export function useFormSchema(): VbenFormSchema[] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
fieldName: 'id',
|
||||||
|
component: 'Input',
|
||||||
|
dependencies: {
|
||||||
|
triggerFields: [''],
|
||||||
|
show: () => false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'name',
|
||||||
|
label: '套餐名称',
|
||||||
|
component: 'Input',
|
||||||
|
rules: 'required',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'menuIds',
|
||||||
|
label: '菜单权限',
|
||||||
|
component: 'Input',
|
||||||
|
formItemClass: 'items-start',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'status',
|
||||||
|
label: '状态',
|
||||||
|
component: 'RadioGroup',
|
||||||
|
componentProps: {
|
||||||
|
options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
|
||||||
|
buttonStyle: 'solid',
|
||||||
|
optionType: 'button',
|
||||||
|
},
|
||||||
|
rules: z.number().default(CommonStatusEnum.ENABLE),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'remark',
|
||||||
|
label: '备注',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
type: 'textarea',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 列表的搜索表单 */
|
||||||
|
export function useGridFormSchema(): VbenFormSchema[] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
fieldName: 'name',
|
||||||
|
label: '套餐名称',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
clearable: true,
|
||||||
|
placeholder: '请输入套餐名称',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'status',
|
||||||
|
label: '状态',
|
||||||
|
component: 'Select',
|
||||||
|
componentProps: {
|
||||||
|
options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
|
||||||
|
clearable: true,
|
||||||
|
placeholder: '请选择状态',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'createTime',
|
||||||
|
label: '创建时间',
|
||||||
|
component: 'DatePicker',
|
||||||
|
componentProps: {
|
||||||
|
...getRangePickerDefaultProps(),
|
||||||
|
clearable: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 列表的字段 */
|
||||||
|
export function useGridColumns(): VxeTableGridOptions['columns'] {
|
||||||
|
return [
|
||||||
|
{ type: 'checkbox', width: 40 },
|
||||||
|
{
|
||||||
|
field: 'id',
|
||||||
|
title: '套餐编号',
|
||||||
|
minWidth: 100,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'name',
|
||||||
|
title: '套餐名称',
|
||||||
|
minWidth: 180,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'status',
|
||||||
|
title: '状态',
|
||||||
|
minWidth: 100,
|
||||||
|
cellRender: {
|
||||||
|
name: 'CellDict',
|
||||||
|
props: { type: DICT_TYPE.COMMON_STATUS },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'remark',
|
||||||
|
title: '备注',
|
||||||
|
minWidth: 200,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'createTime',
|
||||||
|
title: '创建时间',
|
||||||
|
minWidth: 180,
|
||||||
|
formatter: 'formatDateTime',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '操作',
|
||||||
|
width: 220,
|
||||||
|
fixed: 'right',
|
||||||
|
slots: { default: 'actions' },
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
172
apps/web-naive/src/views/system/tenantPackage/index.vue
Normal file
172
apps/web-naive/src/views/system/tenantPackage/index.vue
Normal file
@@ -0,0 +1,172 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||||
|
import type { SystemTenantPackageApi } from '#/api/system/tenant-package';
|
||||||
|
|
||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
import { confirm, DocAlert, Page, useVbenModal } from '@vben/common-ui';
|
||||||
|
import { isEmpty } from '@vben/utils';
|
||||||
|
|
||||||
|
import { message } from '#/adapter/naive';
|
||||||
|
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||||
|
import {
|
||||||
|
deleteTenantPackage,
|
||||||
|
deleteTenantPackageList,
|
||||||
|
getTenantPackagePage,
|
||||||
|
} from '#/api/system/tenant-package';
|
||||||
|
import { $t } from '#/locales';
|
||||||
|
|
||||||
|
import { useGridColumns, useGridFormSchema } from './data';
|
||||||
|
import Form from './modules/form.vue';
|
||||||
|
|
||||||
|
const [FormModal, formModalApi] = useVbenModal({
|
||||||
|
connectedComponent: Form,
|
||||||
|
destroyOnClose: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 刷新表格 */
|
||||||
|
function handleRefresh() {
|
||||||
|
gridApi.query();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 创建租户套餐 */
|
||||||
|
function handleCreate() {
|
||||||
|
formModalApi.setData(null).open();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 编辑租户套餐 */
|
||||||
|
function handleEdit(row: SystemTenantPackageApi.TenantPackage) {
|
||||||
|
formModalApi.setData(row).open();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 删除租户套餐 */
|
||||||
|
async function handleDelete(row: SystemTenantPackageApi.TenantPackage) {
|
||||||
|
const hideLoading = message.loading(
|
||||||
|
$t('ui.actionMessage.deleting', [row.name]),
|
||||||
|
{ duration: 0 },
|
||||||
|
);
|
||||||
|
try {
|
||||||
|
await deleteTenantPackage(row.id!);
|
||||||
|
message.success($t('ui.actionMessage.deleteSuccess', [row.name]));
|
||||||
|
handleRefresh();
|
||||||
|
} finally {
|
||||||
|
hideLoading.destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 批量删除租户套餐 */
|
||||||
|
async function handleDeleteBatch() {
|
||||||
|
await confirm($t('ui.actionMessage.deleteBatchConfirm'));
|
||||||
|
const hideLoading = message.loading($t('ui.actionMessage.deletingBatch'), {
|
||||||
|
duration: 0,
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
await deleteTenantPackageList(checkedIds.value);
|
||||||
|
checkedIds.value = [];
|
||||||
|
message.success($t('ui.actionMessage.deleteSuccess'));
|
||||||
|
handleRefresh();
|
||||||
|
} finally {
|
||||||
|
hideLoading.destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const checkedIds = ref<number[]>([]);
|
||||||
|
function handleRowCheckboxChange({
|
||||||
|
records,
|
||||||
|
}: {
|
||||||
|
records: SystemTenantPackageApi.TenantPackage[];
|
||||||
|
}) {
|
||||||
|
checkedIds.value = records.map((item) => item.id!);
|
||||||
|
}
|
||||||
|
|
||||||
|
const [Grid, gridApi] = useVbenVxeGrid({
|
||||||
|
formOptions: {
|
||||||
|
schema: useGridFormSchema(),
|
||||||
|
},
|
||||||
|
gridOptions: {
|
||||||
|
columns: useGridColumns(),
|
||||||
|
height: 'auto',
|
||||||
|
keepSource: true,
|
||||||
|
proxyConfig: {
|
||||||
|
ajax: {
|
||||||
|
query: async ({ page }, formValues) => {
|
||||||
|
return await getTenantPackagePage({
|
||||||
|
pageNo: page.currentPage,
|
||||||
|
pageSize: page.pageSize,
|
||||||
|
...formValues,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
rowConfig: {
|
||||||
|
keyField: 'id',
|
||||||
|
isHover: true,
|
||||||
|
},
|
||||||
|
toolbarConfig: {
|
||||||
|
refresh: true,
|
||||||
|
search: true,
|
||||||
|
},
|
||||||
|
} as VxeTableGridOptions<SystemTenantPackageApi.TenantPackage>,
|
||||||
|
gridEvents: {
|
||||||
|
checkboxAll: handleRowCheckboxChange,
|
||||||
|
checkboxChange: handleRowCheckboxChange,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Page auto-content-height>
|
||||||
|
<template #doc>
|
||||||
|
<DocAlert title="SaaS 多租户" url="https://doc.iocoder.cn/saas-tenant/" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<FormModal @success="handleRefresh" />
|
||||||
|
<Grid table-title="租户套餐列表">
|
||||||
|
<template #toolbar-tools>
|
||||||
|
<TableAction
|
||||||
|
:actions="[
|
||||||
|
{
|
||||||
|
label: $t('ui.actionTitle.create', ['套餐']),
|
||||||
|
type: 'primary',
|
||||||
|
icon: ACTION_ICON.ADD,
|
||||||
|
auth: ['system:tenant-package:create'],
|
||||||
|
onClick: handleCreate,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: $t('ui.actionTitle.deleteBatch'),
|
||||||
|
type: 'error',
|
||||||
|
icon: ACTION_ICON.DELETE,
|
||||||
|
auth: ['system:tenant-package:delete'],
|
||||||
|
disabled: isEmpty(checkedIds),
|
||||||
|
onClick: handleDeleteBatch,
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<template #actions="{ row }">
|
||||||
|
<TableAction
|
||||||
|
:actions="[
|
||||||
|
{
|
||||||
|
label: $t('common.edit'),
|
||||||
|
type: 'primary',
|
||||||
|
icon: ACTION_ICON.EDIT,
|
||||||
|
auth: ['system:tenant-package:update'],
|
||||||
|
onClick: handleEdit.bind(null, row),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: $t('common.delete'),
|
||||||
|
type: 'error',
|
||||||
|
text: true,
|
||||||
|
icon: ACTION_ICON.DELETE,
|
||||||
|
auth: ['system:tenant-package:delete'],
|
||||||
|
popConfirm: {
|
||||||
|
title: $t('ui.actionMessage.deleteConfirm', [row.name]),
|
||||||
|
confirm: handleDelete.bind(null, row),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</Grid>
|
||||||
|
</Page>
|
||||||
|
</template>
|
||||||
163
apps/web-naive/src/views/system/tenantPackage/modules/form.vue
Normal file
163
apps/web-naive/src/views/system/tenantPackage/modules/form.vue
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { SystemMenuApi } from '#/api/system/menu';
|
||||||
|
import type { SystemTenantPackageApi } from '#/api/system/tenant-package';
|
||||||
|
|
||||||
|
import { computed, ref } from 'vue';
|
||||||
|
|
||||||
|
import { Tree, useVbenModal } from '@vben/common-ui';
|
||||||
|
import { handleTree } from '@vben/utils';
|
||||||
|
|
||||||
|
import { NCheckbox, NSpin } from 'naive-ui';
|
||||||
|
|
||||||
|
import { useVbenForm } from '#/adapter/form';
|
||||||
|
import { message } from '#/adapter/naive';
|
||||||
|
import { getMenuList } from '#/api/system/menu';
|
||||||
|
import {
|
||||||
|
createTenantPackage,
|
||||||
|
getTenantPackage,
|
||||||
|
updateTenantPackage,
|
||||||
|
} from '#/api/system/tenant-package';
|
||||||
|
import { $t } from '#/locales';
|
||||||
|
|
||||||
|
import { useFormSchema } from '../data';
|
||||||
|
|
||||||
|
const emit = defineEmits(['success']);
|
||||||
|
const formData = ref<SystemTenantPackageApi.TenantPackage>();
|
||||||
|
const getTitle = computed(() => {
|
||||||
|
return formData.value
|
||||||
|
? $t('ui.actionTitle.edit', ['套餐'])
|
||||||
|
: $t('ui.actionTitle.create', ['套餐']);
|
||||||
|
});
|
||||||
|
const menuTree = ref<SystemMenuApi.Menu[]>([]); // 菜单树
|
||||||
|
const menuLoading = ref(false); // 加载菜单列表
|
||||||
|
const isAllSelected = ref(false); // 全选状态
|
||||||
|
const isExpanded = ref(false); // 展开状态
|
||||||
|
const expandedKeys = ref<number[]>([]); // 展开的节点
|
||||||
|
|
||||||
|
const [Form, formApi] = useVbenForm({
|
||||||
|
commonConfig: {
|
||||||
|
componentProps: {
|
||||||
|
class: 'w-full',
|
||||||
|
},
|
||||||
|
formItemClass: 'col-span-2',
|
||||||
|
labelWidth: 80,
|
||||||
|
},
|
||||||
|
layout: 'horizontal',
|
||||||
|
schema: useFormSchema(),
|
||||||
|
showDefaultActions: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const [Modal, modalApi] = useVbenModal({
|
||||||
|
async onConfirm() {
|
||||||
|
const { valid } = await formApi.validate();
|
||||||
|
if (!valid) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
modalApi.lock();
|
||||||
|
// 提交表单
|
||||||
|
const data =
|
||||||
|
(await formApi.getValues()) as SystemTenantPackageApi.TenantPackage;
|
||||||
|
try {
|
||||||
|
await (formData.value
|
||||||
|
? updateTenantPackage(data)
|
||||||
|
: createTenantPackage(data));
|
||||||
|
// 关闭并提示
|
||||||
|
await modalApi.close();
|
||||||
|
emit('success');
|
||||||
|
message.success($t('ui.actionMessage.operationSuccess'));
|
||||||
|
} finally {
|
||||||
|
modalApi.unlock();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async onOpenChange(isOpen: boolean) {
|
||||||
|
if (!isOpen) {
|
||||||
|
formData.value = undefined;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 加载菜单列表
|
||||||
|
await loadMenuTree();
|
||||||
|
// 加载数据
|
||||||
|
const data = modalApi.getData<SystemTenantPackageApi.TenantPackage>();
|
||||||
|
if (!data || !data.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
modalApi.lock();
|
||||||
|
try {
|
||||||
|
formData.value = await getTenantPackage(data.id);
|
||||||
|
await formApi.setValues(data);
|
||||||
|
} finally {
|
||||||
|
modalApi.unlock();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 加载菜单树 */
|
||||||
|
async function loadMenuTree() {
|
||||||
|
menuLoading.value = true;
|
||||||
|
try {
|
||||||
|
const data = await getMenuList();
|
||||||
|
menuTree.value = handleTree(data) as SystemMenuApi.Menu[];
|
||||||
|
} finally {
|
||||||
|
menuLoading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 全选/全不选 */
|
||||||
|
function handleSelectAll() {
|
||||||
|
isAllSelected.value = !isAllSelected.value;
|
||||||
|
if (isAllSelected.value) {
|
||||||
|
const allIds = getAllNodeIds(menuTree.value);
|
||||||
|
formApi.setFieldValue('menuIds', allIds);
|
||||||
|
} else {
|
||||||
|
formApi.setFieldValue('menuIds', []);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 展开/折叠所有节点 */
|
||||||
|
function handleExpandAll() {
|
||||||
|
isExpanded.value = !isExpanded.value;
|
||||||
|
expandedKeys.value = isExpanded.value ? getAllNodeIds(menuTree.value) : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 递归获取所有节点 ID */
|
||||||
|
function getAllNodeIds(nodes: any[], ids: number[] = []): number[] {
|
||||||
|
nodes.forEach((node: any) => {
|
||||||
|
ids.push(node.id);
|
||||||
|
if (node.children && node.children.length > 0) {
|
||||||
|
getAllNodeIds(node.children, ids);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return ids;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Modal :title="getTitle" class="w-2/5">
|
||||||
|
<Form class="mx-6">
|
||||||
|
<template #menuIds="slotProps">
|
||||||
|
<NSpin :show="menuLoading" content-class="w-full">
|
||||||
|
<Tree
|
||||||
|
class="max-h-96 overflow-y-auto"
|
||||||
|
:tree-data="menuTree"
|
||||||
|
multiple
|
||||||
|
bordered
|
||||||
|
:default-expanded-keys="expandedKeys"
|
||||||
|
v-bind="slotProps"
|
||||||
|
value-field="id"
|
||||||
|
label-field="name"
|
||||||
|
/>
|
||||||
|
</NSpin>
|
||||||
|
</template>
|
||||||
|
</Form>
|
||||||
|
<template #prepend-footer>
|
||||||
|
<div class="flex flex-auto items-center">
|
||||||
|
<NCheckbox :checked="isAllSelected" @change="handleSelectAll">
|
||||||
|
全选
|
||||||
|
</NCheckbox>
|
||||||
|
<NCheckbox :checked="isExpanded" @change="handleExpandAll">
|
||||||
|
全部展开
|
||||||
|
</NCheckbox>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Modal>
|
||||||
|
</template>
|
||||||
347
apps/web-naive/src/views/system/user/data.ts
Normal file
347
apps/web-naive/src/views/system/user/data.ts
Normal file
@@ -0,0 +1,347 @@
|
|||||||
|
import type { VbenFormSchema } from '#/adapter/form';
|
||||||
|
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||||
|
import type { SystemUserApi } from '#/api/system/user';
|
||||||
|
|
||||||
|
import { CommonStatusEnum, DICT_TYPE } from '@vben/constants';
|
||||||
|
import { getDictOptions } from '@vben/hooks';
|
||||||
|
import { $t } from '@vben/locales';
|
||||||
|
import { handleTree } from '@vben/utils';
|
||||||
|
|
||||||
|
import { z } from '#/adapter/form';
|
||||||
|
import { getDeptList } from '#/api/system/dept';
|
||||||
|
import { getSimplePostList } from '#/api/system/post';
|
||||||
|
import { getSimpleRoleList } from '#/api/system/role';
|
||||||
|
import { getRangePickerDefaultProps } from '#/utils';
|
||||||
|
|
||||||
|
/** 新增/修改的表单 */
|
||||||
|
export function useFormSchema(): VbenFormSchema[] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
component: 'Input',
|
||||||
|
fieldName: 'id',
|
||||||
|
dependencies: {
|
||||||
|
triggerFields: [''],
|
||||||
|
show: () => false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'username',
|
||||||
|
label: '用户名称',
|
||||||
|
component: 'Input',
|
||||||
|
rules: 'required',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '用户密码',
|
||||||
|
fieldName: 'password',
|
||||||
|
component: 'InputPassword',
|
||||||
|
rules: 'required',
|
||||||
|
dependencies: {
|
||||||
|
triggerFields: ['id'],
|
||||||
|
show: (values) => !values.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'nickname',
|
||||||
|
label: '用户昵称',
|
||||||
|
component: 'Input',
|
||||||
|
rules: 'required',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'deptId',
|
||||||
|
label: '归属部门',
|
||||||
|
component: 'ApiTreeSelect',
|
||||||
|
componentProps: {
|
||||||
|
api: async () => {
|
||||||
|
const data = await getDeptList();
|
||||||
|
return handleTree(data);
|
||||||
|
},
|
||||||
|
labelField: 'name',
|
||||||
|
valueField: 'id',
|
||||||
|
childrenField: 'children',
|
||||||
|
placeholder: '请选择归属部门',
|
||||||
|
treeDefaultExpandAll: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'postIds',
|
||||||
|
label: '岗位',
|
||||||
|
component: 'ApiSelect',
|
||||||
|
componentProps: {
|
||||||
|
api: getSimplePostList,
|
||||||
|
labelField: 'name',
|
||||||
|
valueField: 'id',
|
||||||
|
mode: 'multiple',
|
||||||
|
placeholder: '请选择岗位',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'email',
|
||||||
|
label: '邮箱',
|
||||||
|
component: 'Input',
|
||||||
|
rules: z.string().email('邮箱格式不正确').or(z.literal('')).optional(),
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入邮箱',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'mobile',
|
||||||
|
label: '手机号码',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入手机号码',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'sex',
|
||||||
|
label: '用户性别',
|
||||||
|
component: 'RadioGroup',
|
||||||
|
componentProps: {
|
||||||
|
options: getDictOptions(DICT_TYPE.SYSTEM_USER_SEX, 'number'),
|
||||||
|
buttonStyle: 'solid',
|
||||||
|
optionType: 'button',
|
||||||
|
},
|
||||||
|
rules: z.number().default(1),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'status',
|
||||||
|
label: '用户状态',
|
||||||
|
component: 'RadioGroup',
|
||||||
|
componentProps: {
|
||||||
|
options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
|
||||||
|
buttonStyle: 'solid',
|
||||||
|
optionType: 'button',
|
||||||
|
},
|
||||||
|
rules: z.number().default(CommonStatusEnum.ENABLE),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'remark',
|
||||||
|
label: '备注',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
type: 'textarea',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 重置密码的表单 */
|
||||||
|
export function useResetPasswordFormSchema(): VbenFormSchema[] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
component: 'Input',
|
||||||
|
fieldName: 'id',
|
||||||
|
dependencies: {
|
||||||
|
triggerFields: [''],
|
||||||
|
show: () => false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: 'VbenInputPassword',
|
||||||
|
componentProps: {
|
||||||
|
passwordStrength: true,
|
||||||
|
placeholder: '请输入新密码',
|
||||||
|
},
|
||||||
|
dependencies: {
|
||||||
|
rules(values) {
|
||||||
|
return z
|
||||||
|
.string({ message: '请输入新密码' })
|
||||||
|
.min(5, '密码长度不能少于 5 个字符')
|
||||||
|
.max(20, '密码长度不能超过 20 个字符')
|
||||||
|
.refine(
|
||||||
|
(value) => value !== values.oldPassword,
|
||||||
|
'新旧密码不能相同',
|
||||||
|
);
|
||||||
|
},
|
||||||
|
triggerFields: ['newPassword', 'oldPassword'],
|
||||||
|
},
|
||||||
|
fieldName: 'newPassword',
|
||||||
|
label: '新密码',
|
||||||
|
rules: 'required',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: 'VbenInputPassword',
|
||||||
|
componentProps: {
|
||||||
|
passwordStrength: true,
|
||||||
|
placeholder: $t('authentication.confirmPassword'),
|
||||||
|
},
|
||||||
|
dependencies: {
|
||||||
|
rules(values) {
|
||||||
|
return z
|
||||||
|
.string({ message: '请输入确认密码' })
|
||||||
|
.min(5, '密码长度不能少于 5 个字符')
|
||||||
|
.max(20, '密码长度不能超过 20 个字符')
|
||||||
|
.refine(
|
||||||
|
(value) => value === values.newPassword,
|
||||||
|
'新密码和确认密码不一致',
|
||||||
|
);
|
||||||
|
},
|
||||||
|
triggerFields: ['newPassword', 'confirmPassword'],
|
||||||
|
},
|
||||||
|
fieldName: 'confirmPassword',
|
||||||
|
label: '确认密码',
|
||||||
|
rules: 'required',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 分配角色的表单 */
|
||||||
|
export function useAssignRoleFormSchema(): VbenFormSchema[] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
component: 'Input',
|
||||||
|
fieldName: 'id',
|
||||||
|
dependencies: {
|
||||||
|
triggerFields: [''],
|
||||||
|
show: () => false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'username',
|
||||||
|
label: '用户名称',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
disabled: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'nickname',
|
||||||
|
label: '用户昵称',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
disabled: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'roleIds',
|
||||||
|
label: '角色',
|
||||||
|
component: 'ApiSelect',
|
||||||
|
componentProps: {
|
||||||
|
api: getSimpleRoleList,
|
||||||
|
labelField: 'name',
|
||||||
|
valueField: 'id',
|
||||||
|
mode: 'multiple',
|
||||||
|
placeholder: '请选择角色',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 用户导入的表单 */
|
||||||
|
export function useImportFormSchema(): VbenFormSchema[] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
fieldName: 'file',
|
||||||
|
label: '用户数据',
|
||||||
|
component: 'Upload',
|
||||||
|
rules: 'required',
|
||||||
|
help: '仅允许导入 xls、xlsx 格式文件',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'updateSupport',
|
||||||
|
label: '是否覆盖',
|
||||||
|
component: 'Switch',
|
||||||
|
componentProps: {
|
||||||
|
checkedChildren: '是',
|
||||||
|
unCheckedChildren: '否',
|
||||||
|
},
|
||||||
|
rules: z.boolean().default(false),
|
||||||
|
help: '是否更新已经存在的用户数据',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 列表的搜索表单 */
|
||||||
|
export function useGridFormSchema(): VbenFormSchema[] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
fieldName: 'username',
|
||||||
|
label: '用户名称',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入用户名称',
|
||||||
|
clearable: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'mobile',
|
||||||
|
label: '手机号码',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入手机号码',
|
||||||
|
clearable: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'createTime',
|
||||||
|
label: '创建时间',
|
||||||
|
component: 'DatePicker',
|
||||||
|
componentProps: {
|
||||||
|
...getRangePickerDefaultProps(),
|
||||||
|
clearable: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 列表的字段 */
|
||||||
|
export function useGridColumns(
|
||||||
|
onStatusChange?: (
|
||||||
|
newStatus: number,
|
||||||
|
row: SystemUserApi.User,
|
||||||
|
) => PromiseLike<boolean | undefined>,
|
||||||
|
): VxeTableGridOptions['columns'] {
|
||||||
|
return [
|
||||||
|
{ type: 'checkbox', width: 40 },
|
||||||
|
{
|
||||||
|
field: 'id',
|
||||||
|
title: '用户编号',
|
||||||
|
minWidth: 100,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'username',
|
||||||
|
title: '用户名称',
|
||||||
|
minWidth: 120,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'nickname',
|
||||||
|
title: '用户昵称',
|
||||||
|
minWidth: 120,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'deptName',
|
||||||
|
title: '部门',
|
||||||
|
minWidth: 120,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'mobile',
|
||||||
|
title: '手机号码',
|
||||||
|
minWidth: 120,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'status',
|
||||||
|
title: '状态',
|
||||||
|
minWidth: 100,
|
||||||
|
align: 'center',
|
||||||
|
cellRender: {
|
||||||
|
attrs: { beforeChange: onStatusChange },
|
||||||
|
name: 'CellSwitch',
|
||||||
|
props: {
|
||||||
|
checkedValue: CommonStatusEnum.ENABLE,
|
||||||
|
unCheckedValue: CommonStatusEnum.DISABLE,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'createTime',
|
||||||
|
title: '创建时间',
|
||||||
|
minWidth: 180,
|
||||||
|
formatter: 'formatDateTime',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '操作',
|
||||||
|
width: 180,
|
||||||
|
fixed: 'right',
|
||||||
|
slots: { default: 'actions' },
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
302
apps/web-naive/src/views/system/user/index.vue
Normal file
302
apps/web-naive/src/views/system/user/index.vue
Normal file
@@ -0,0 +1,302 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||||
|
import type { SystemDeptApi } from '#/api/system/dept';
|
||||||
|
import type { SystemUserApi } from '#/api/system/user';
|
||||||
|
|
||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
import { confirm, DocAlert, Page, useVbenModal } from '@vben/common-ui';
|
||||||
|
import { DICT_TYPE } from '@vben/constants';
|
||||||
|
import { getDictLabel } from '@vben/hooks';
|
||||||
|
import { downloadFileFromBlobPart, isEmpty } from '@vben/utils';
|
||||||
|
|
||||||
|
import { NCard } from 'naive-ui';
|
||||||
|
|
||||||
|
import { message } from '#/adapter/naive';
|
||||||
|
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||||
|
import {
|
||||||
|
deleteUser,
|
||||||
|
deleteUserList,
|
||||||
|
exportUser,
|
||||||
|
getUserPage,
|
||||||
|
updateUserStatus,
|
||||||
|
} from '#/api/system/user';
|
||||||
|
import { $t } from '#/locales';
|
||||||
|
|
||||||
|
import { useGridColumns, useGridFormSchema } from './data';
|
||||||
|
import AssignRoleForm from './modules/assign-role-form.vue';
|
||||||
|
import DeptTree from './modules/dept-tree.vue';
|
||||||
|
import Form from './modules/form.vue';
|
||||||
|
import ImportForm from './modules/import-form.vue';
|
||||||
|
import ResetPasswordForm from './modules/reset-password-form.vue';
|
||||||
|
|
||||||
|
const [FormModal, formModalApi] = useVbenModal({
|
||||||
|
connectedComponent: Form,
|
||||||
|
destroyOnClose: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const [ResetPasswordModal, resetPasswordModalApi] = useVbenModal({
|
||||||
|
connectedComponent: ResetPasswordForm,
|
||||||
|
destroyOnClose: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const [AssignRoleModal, assignRoleModalApi] = useVbenModal({
|
||||||
|
connectedComponent: AssignRoleForm,
|
||||||
|
destroyOnClose: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const [ImportModal, importModalApi] = useVbenModal({
|
||||||
|
connectedComponent: ImportForm,
|
||||||
|
destroyOnClose: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 刷新表格 */
|
||||||
|
function handleRefresh() {
|
||||||
|
gridApi.query();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 导出表格 */
|
||||||
|
async function handleExport() {
|
||||||
|
const data = await exportUser(await gridApi.formApi.getValues());
|
||||||
|
downloadFileFromBlobPart({ fileName: '用户.xls', source: data });
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 选择部门 */
|
||||||
|
const searchDeptId = ref<number | undefined>(undefined);
|
||||||
|
async function handleDeptSelect(dept: SystemDeptApi.Dept) {
|
||||||
|
searchDeptId.value = dept.id;
|
||||||
|
handleRefresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 创建用户 */
|
||||||
|
function handleCreate() {
|
||||||
|
formModalApi.setData(null).open();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 导入用户 */
|
||||||
|
function handleImport() {
|
||||||
|
importModalApi.open();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 编辑用户 */
|
||||||
|
function handleEdit(row: SystemUserApi.User) {
|
||||||
|
formModalApi.setData(row).open();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 删除用户 */
|
||||||
|
async function handleDelete(row: SystemUserApi.User) {
|
||||||
|
const hideLoading = message.loading(
|
||||||
|
$t('ui.actionMessage.deleting', [row.username]),
|
||||||
|
{ duration: 0 },
|
||||||
|
);
|
||||||
|
try {
|
||||||
|
await deleteUser(row.id!);
|
||||||
|
message.success($t('ui.actionMessage.deleteSuccess', [row.username]));
|
||||||
|
handleRefresh();
|
||||||
|
} finally {
|
||||||
|
hideLoading.destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 批量删除用户 */
|
||||||
|
async function handleDeleteBatch() {
|
||||||
|
await confirm($t('ui.actionMessage.deleteBatchConfirm'));
|
||||||
|
const hideLoading = message.loading($t('ui.actionMessage.deletingBatch'), {
|
||||||
|
duration: 0,
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
await deleteUserList(checkedIds.value);
|
||||||
|
checkedIds.value = [];
|
||||||
|
message.success($t('ui.actionMessage.deleteSuccess'));
|
||||||
|
handleRefresh();
|
||||||
|
} finally {
|
||||||
|
hideLoading.destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const checkedIds = ref<number[]>([]);
|
||||||
|
function handleRowCheckboxChange({
|
||||||
|
records,
|
||||||
|
}: {
|
||||||
|
records: SystemUserApi.User[];
|
||||||
|
}) {
|
||||||
|
checkedIds.value = records.map((item) => item.id!);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 重置密码 */
|
||||||
|
function handleResetPassword(row: SystemUserApi.User) {
|
||||||
|
resetPasswordModalApi.setData(row).open();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 分配角色 */
|
||||||
|
function handleAssignRole(row: SystemUserApi.User) {
|
||||||
|
assignRoleModalApi.setData(row).open();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 更新用户状态 */
|
||||||
|
async function handleStatusChange(
|
||||||
|
newStatus: number,
|
||||||
|
row: SystemUserApi.User,
|
||||||
|
): Promise<boolean | undefined> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
confirm({
|
||||||
|
content: `你要将${row.username}的状态切换为【${getDictLabel(DICT_TYPE.COMMON_STATUS, newStatus)}】吗?`,
|
||||||
|
})
|
||||||
|
.then(async () => {
|
||||||
|
// 更新用户状态
|
||||||
|
const res = await updateUserStatus(row.id!, newStatus);
|
||||||
|
if (res) {
|
||||||
|
// 提示并返回成功
|
||||||
|
message.success($t('ui.actionMessage.operationSuccess'));
|
||||||
|
resolve(true);
|
||||||
|
} else {
|
||||||
|
reject(new Error('更新失败'));
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
reject(new Error('取消操作'));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const [Grid, gridApi] = useVbenVxeGrid({
|
||||||
|
formOptions: {
|
||||||
|
schema: useGridFormSchema(),
|
||||||
|
},
|
||||||
|
gridOptions: {
|
||||||
|
columns: useGridColumns(handleStatusChange),
|
||||||
|
height: 'auto',
|
||||||
|
keepSource: true,
|
||||||
|
proxyConfig: {
|
||||||
|
ajax: {
|
||||||
|
query: async ({ page }, formValues) => {
|
||||||
|
return await getUserPage({
|
||||||
|
pageNo: page.currentPage,
|
||||||
|
pageSize: page.pageSize,
|
||||||
|
...formValues,
|
||||||
|
deptId: searchDeptId.value,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
rowConfig: {
|
||||||
|
keyField: 'id',
|
||||||
|
isHover: true,
|
||||||
|
},
|
||||||
|
toolbarConfig: {
|
||||||
|
refresh: true,
|
||||||
|
search: true,
|
||||||
|
},
|
||||||
|
} as VxeTableGridOptions<SystemUserApi.User>,
|
||||||
|
gridEvents: {
|
||||||
|
checkboxAll: handleRowCheckboxChange,
|
||||||
|
checkboxChange: handleRowCheckboxChange,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Page auto-content-height>
|
||||||
|
<template #doc>
|
||||||
|
<DocAlert title="用户体系" url="https://doc.iocoder.cn/user-center/" />
|
||||||
|
<DocAlert title="三方登陆" url="https://doc.iocoder.cn/social-user/" />
|
||||||
|
<DocAlert
|
||||||
|
title="Excel 导入导出"
|
||||||
|
url="https://doc.iocoder.cn/excel-import-and-export/"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<FormModal @success="handleRefresh" />
|
||||||
|
<ResetPasswordModal @success="handleRefresh" />
|
||||||
|
<AssignRoleModal @success="handleRefresh" />
|
||||||
|
<ImportModal @success="handleRefresh" />
|
||||||
|
|
||||||
|
<div class="flex h-full w-full">
|
||||||
|
<!-- 左侧部门树 -->
|
||||||
|
<NCard class="mr-4 h-full w-1/6">
|
||||||
|
<DeptTree @select="handleDeptSelect" />
|
||||||
|
</NCard>
|
||||||
|
<!-- 右侧用户列表 -->
|
||||||
|
<div class="w-5/6">
|
||||||
|
<Grid table-title="用户列表">
|
||||||
|
<template #toolbar-tools>
|
||||||
|
<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,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: $t('ui.actionTitle.deleteBatch'),
|
||||||
|
type: 'error',
|
||||||
|
icon: ACTION_ICON.DELETE,
|
||||||
|
disabled: isEmpty(checkedIds),
|
||||||
|
auth: ['system:user:delete'],
|
||||||
|
onClick: handleDeleteBatch,
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<template #actions="{ row }">
|
||||||
|
<TableAction
|
||||||
|
:actions="[
|
||||||
|
{
|
||||||
|
label: $t('common.edit'),
|
||||||
|
type: 'primary',
|
||||||
|
text: true,
|
||||||
|
icon: ACTION_ICON.EDIT,
|
||||||
|
auth: ['system:user:update'],
|
||||||
|
onClick: handleEdit.bind(null, row),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: $t('common.delete'),
|
||||||
|
type: 'error',
|
||||||
|
text: true,
|
||||||
|
icon: ACTION_ICON.DELETE,
|
||||||
|
auth: ['system:user:delete'],
|
||||||
|
popConfirm: {
|
||||||
|
title: $t('ui.actionMessage.deleteConfirm', [row.username]),
|
||||||
|
confirm: handleDelete.bind(null, row),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
:drop-down-actions="[
|
||||||
|
{
|
||||||
|
label: '分配角色',
|
||||||
|
type: 'primary',
|
||||||
|
text: true,
|
||||||
|
auth: ['system:permission:assign-user-role'],
|
||||||
|
onClick: handleAssignRole.bind(null, row),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '重置密码',
|
||||||
|
type: 'primary',
|
||||||
|
text: true,
|
||||||
|
auth: ['system:user:update-password'],
|
||||||
|
onClick: handleResetPassword.bind(null, row),
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</Grid>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Page>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { SystemUserApi } from '#/api/system/user';
|
||||||
|
|
||||||
|
import { useVbenModal } from '@vben/common-ui';
|
||||||
|
|
||||||
|
import { useVbenForm } from '#/adapter/form';
|
||||||
|
import { message } from '#/adapter/naive';
|
||||||
|
import { assignUserRole, getUserRoleList } from '#/api/system/permission';
|
||||||
|
import { $t } from '#/locales';
|
||||||
|
|
||||||
|
import { useAssignRoleFormSchema } from '../data';
|
||||||
|
|
||||||
|
const emit = defineEmits(['success']);
|
||||||
|
const [Form, formApi] = useVbenForm({
|
||||||
|
commonConfig: {
|
||||||
|
componentProps: {
|
||||||
|
class: 'w-full',
|
||||||
|
},
|
||||||
|
formItemClass: 'col-span-2',
|
||||||
|
labelWidth: 80,
|
||||||
|
},
|
||||||
|
layout: 'horizontal',
|
||||||
|
schema: useAssignRoleFormSchema(),
|
||||||
|
showDefaultActions: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const [Modal, modalApi] = useVbenModal({
|
||||||
|
async onConfirm() {
|
||||||
|
const { valid } = await formApi.validate();
|
||||||
|
if (!valid) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
modalApi.lock();
|
||||||
|
// 提交表单
|
||||||
|
const values = await formApi.getValues();
|
||||||
|
try {
|
||||||
|
await assignUserRole({
|
||||||
|
userId: values.id,
|
||||||
|
roleIds: values.roleIds,
|
||||||
|
});
|
||||||
|
// 关闭并提示
|
||||||
|
await modalApi.close();
|
||||||
|
emit('success');
|
||||||
|
message.success($t('ui.actionMessage.operationSuccess'));
|
||||||
|
} finally {
|
||||||
|
modalApi.unlock();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async onOpenChange(isOpen: boolean) {
|
||||||
|
if (!isOpen) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 加载数据
|
||||||
|
const data = modalApi.getData<SystemUserApi.User>();
|
||||||
|
if (!data || !data.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
modalApi.lock();
|
||||||
|
try {
|
||||||
|
const roleIds = await getUserRoleList(data.id);
|
||||||
|
// 设置到 values
|
||||||
|
await formApi.setValues({
|
||||||
|
...data,
|
||||||
|
roleIds,
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
modalApi.unlock();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Modal title="分配角色">
|
||||||
|
<Form class="mx-4" />
|
||||||
|
</Modal>
|
||||||
|
</template>
|
||||||
90
apps/web-naive/src/views/system/user/modules/dept-tree.vue
Normal file
90
apps/web-naive/src/views/system/user/modules/dept-tree.vue
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { TreeOption, TreeOverrideNodeClickBehaviorReturn } from 'naive-ui';
|
||||||
|
|
||||||
|
import type { SystemDeptApi } from '#/api/system/dept';
|
||||||
|
|
||||||
|
import { onMounted, ref } from 'vue';
|
||||||
|
|
||||||
|
import { IconifyIcon } from '@vben/icons';
|
||||||
|
import { handleTree } from '@vben/utils';
|
||||||
|
|
||||||
|
import { NInput, NSpin, NTree } from 'naive-ui';
|
||||||
|
|
||||||
|
import { getSimpleDeptList } from '#/api/system/dept';
|
||||||
|
|
||||||
|
const emit = defineEmits(['select']);
|
||||||
|
const deptList = ref<SystemDeptApi.Dept[]>([]); // 部门列表
|
||||||
|
const deptTree = ref<any[]>([]); // 部门树
|
||||||
|
const expandedKeys = ref<number[]>([]); // 展开的节点
|
||||||
|
const loading = ref(false); // 加载状态
|
||||||
|
const searchValue = ref(''); // 搜索值
|
||||||
|
|
||||||
|
/** 处理搜索逻辑 */
|
||||||
|
function handleSearch(e: any) {
|
||||||
|
const value = e.target.value;
|
||||||
|
searchValue.value = value;
|
||||||
|
const filteredList = value
|
||||||
|
? deptList.value.filter((item) =>
|
||||||
|
item.name.toLowerCase().includes(value.toLowerCase()),
|
||||||
|
)
|
||||||
|
: deptList.value;
|
||||||
|
deptTree.value = handleTree(filteredList);
|
||||||
|
// 展开所有节点
|
||||||
|
expandedKeys.value = deptTree.value.map((node) => node.id!);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 选中部门 */
|
||||||
|
function handleSelect({
|
||||||
|
option,
|
||||||
|
}: {
|
||||||
|
option: TreeOption;
|
||||||
|
}): TreeOverrideNodeClickBehaviorReturn {
|
||||||
|
emit('select', option);
|
||||||
|
return 'default';
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 初始化 */
|
||||||
|
onMounted(async () => {
|
||||||
|
try {
|
||||||
|
loading.value = true;
|
||||||
|
const data = await getSimpleDeptList();
|
||||||
|
deptList.value = data;
|
||||||
|
deptTree.value = handleTree(data);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取部门数据失败', error);
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<NInput
|
||||||
|
placeholder="搜索部门"
|
||||||
|
allow-clear
|
||||||
|
v-model:value="searchValue"
|
||||||
|
@change="handleSearch"
|
||||||
|
class="w-full"
|
||||||
|
>
|
||||||
|
<template #prefix>
|
||||||
|
<IconifyIcon icon="lucide:search" class="size-4" />
|
||||||
|
</template>
|
||||||
|
</NInput>
|
||||||
|
<NSpin :show="loading" class="w-full">
|
||||||
|
<NTree
|
||||||
|
v-if="deptTree.length > 0"
|
||||||
|
class="pt-2"
|
||||||
|
:data="deptTree"
|
||||||
|
:default-expand-all="true"
|
||||||
|
key-field="id"
|
||||||
|
label-field="name"
|
||||||
|
children-field="children"
|
||||||
|
:override-default-node-click-behavior="handleSelect"
|
||||||
|
/>
|
||||||
|
<div v-else-if="!loading" class="py-4 text-center text-gray-500">
|
||||||
|
暂无数据
|
||||||
|
</div>
|
||||||
|
</NSpin>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
81
apps/web-naive/src/views/system/user/modules/form.vue
Normal file
81
apps/web-naive/src/views/system/user/modules/form.vue
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { SystemUserApi } from '#/api/system/user';
|
||||||
|
|
||||||
|
import { computed, ref } from 'vue';
|
||||||
|
|
||||||
|
import { useVbenModal } from '@vben/common-ui';
|
||||||
|
|
||||||
|
import { useVbenForm } from '#/adapter/form';
|
||||||
|
import { message } from '#/adapter/naive';
|
||||||
|
import { createUser, getUser, updateUser } from '#/api/system/user';
|
||||||
|
import { $t } from '#/locales';
|
||||||
|
|
||||||
|
import { useFormSchema } from '../data';
|
||||||
|
|
||||||
|
const emit = defineEmits(['success']);
|
||||||
|
const formData = ref<SystemUserApi.User>();
|
||||||
|
const getTitle = computed(() => {
|
||||||
|
return formData.value?.id
|
||||||
|
? $t('ui.actionTitle.edit', ['用户'])
|
||||||
|
: $t('ui.actionTitle.create', ['用户']);
|
||||||
|
});
|
||||||
|
|
||||||
|
const [Form, formApi] = useVbenForm({
|
||||||
|
commonConfig: {
|
||||||
|
componentProps: {
|
||||||
|
class: 'w-full',
|
||||||
|
},
|
||||||
|
formItemClass: 'col-span-2',
|
||||||
|
labelWidth: 80,
|
||||||
|
},
|
||||||
|
layout: 'horizontal',
|
||||||
|
schema: useFormSchema(),
|
||||||
|
showDefaultActions: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const [Modal, modalApi] = useVbenModal({
|
||||||
|
async onConfirm() {
|
||||||
|
const { valid } = await formApi.validate();
|
||||||
|
if (!valid) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
modalApi.lock();
|
||||||
|
// 提交表单
|
||||||
|
const data = (await formApi.getValues()) as SystemUserApi.User;
|
||||||
|
try {
|
||||||
|
await (formData.value?.id ? updateUser(data) : createUser(data));
|
||||||
|
// 关闭并提示
|
||||||
|
await modalApi.close();
|
||||||
|
emit('success');
|
||||||
|
message.success($t('ui.actionMessage.operationSuccess'));
|
||||||
|
} finally {
|
||||||
|
modalApi.unlock();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async onOpenChange(isOpen: boolean) {
|
||||||
|
if (!isOpen) {
|
||||||
|
formData.value = undefined;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 加载数据
|
||||||
|
const data = modalApi.getData<SystemUserApi.User>();
|
||||||
|
if (!data || !data.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
modalApi.lock();
|
||||||
|
try {
|
||||||
|
formData.value = await getUser(data.id);
|
||||||
|
// 设置到 values
|
||||||
|
await formApi.setValues(formData.value);
|
||||||
|
} finally {
|
||||||
|
modalApi.unlock();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Modal :title="getTitle" class="w-1/3">
|
||||||
|
<Form class="mx-4" />
|
||||||
|
</Modal>
|
||||||
|
</template>
|
||||||
84
apps/web-naive/src/views/system/user/modules/import-form.vue
Normal file
84
apps/web-naive/src/views/system/user/modules/import-form.vue
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { UploadFileInfo } from 'naive-ui';
|
||||||
|
|
||||||
|
import { useVbenModal } from '@vben/common-ui';
|
||||||
|
import { downloadFileFromBlobPart } from '@vben/utils';
|
||||||
|
|
||||||
|
import { NButton, NUpload } from 'naive-ui';
|
||||||
|
|
||||||
|
import { useVbenForm } from '#/adapter/form';
|
||||||
|
import { message } from '#/adapter/naive';
|
||||||
|
import { importUser, importUserTemplate } from '#/api/system/user';
|
||||||
|
import { $t } from '#/locales';
|
||||||
|
|
||||||
|
import { useImportFormSchema } from '../data';
|
||||||
|
|
||||||
|
const emit = defineEmits(['success']);
|
||||||
|
|
||||||
|
const [Form, formApi] = useVbenForm({
|
||||||
|
commonConfig: {
|
||||||
|
formItemClass: 'col-span-2',
|
||||||
|
labelWidth: 120,
|
||||||
|
},
|
||||||
|
layout: 'horizontal',
|
||||||
|
schema: useImportFormSchema(),
|
||||||
|
showDefaultActions: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const [Modal, modalApi] = useVbenModal({
|
||||||
|
async onConfirm() {
|
||||||
|
const { valid } = await formApi.validate();
|
||||||
|
if (!valid) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
modalApi.lock();
|
||||||
|
// 提交表单
|
||||||
|
const data = await formApi.getValues();
|
||||||
|
try {
|
||||||
|
await importUser(data.file, data.updateSupport);
|
||||||
|
// 关闭并提示
|
||||||
|
await modalApi.close();
|
||||||
|
emit('success');
|
||||||
|
message.success($t('ui.actionMessage.operationSuccess'));
|
||||||
|
} finally {
|
||||||
|
modalApi.unlock();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 上传前 */
|
||||||
|
function beforeUpload(file: UploadFileInfo) {
|
||||||
|
formApi.setFieldValue('file', file);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 下载模版 */
|
||||||
|
async function handleDownload() {
|
||||||
|
const data = await importUserTemplate();
|
||||||
|
downloadFileFromBlobPart({ fileName: '用户导入模板.xls', source: data });
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Modal title="导入用户" class="w-1/3">
|
||||||
|
<Form class="mx-4">
|
||||||
|
<template #file>
|
||||||
|
<div class="w-full">
|
||||||
|
<NUpload
|
||||||
|
:show-file-list="false"
|
||||||
|
:max-count="1"
|
||||||
|
accept=".xls,.xlsx"
|
||||||
|
:before-upload="beforeUpload"
|
||||||
|
>
|
||||||
|
<NButton type="primary"> 选择 Excel 文件 </NButton>
|
||||||
|
</NUpload>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Form>
|
||||||
|
<template #prepend-footer>
|
||||||
|
<div class="flex flex-auto items-center">
|
||||||
|
<NButton @click="handleDownload"> 下载导入模板 </NButton>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Modal>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { SystemUserApi } from '#/api/system/user';
|
||||||
|
|
||||||
|
import { useVbenModal } from '@vben/common-ui';
|
||||||
|
|
||||||
|
import { useVbenForm } from '#/adapter/form';
|
||||||
|
import { message } from '#/adapter/naive';
|
||||||
|
import { resetUserPassword } from '#/api/system/user';
|
||||||
|
import { $t } from '#/locales';
|
||||||
|
|
||||||
|
import { useResetPasswordFormSchema } from '../data';
|
||||||
|
|
||||||
|
const emit = defineEmits(['success']);
|
||||||
|
const [Form, formApi] = useVbenForm({
|
||||||
|
commonConfig: {
|
||||||
|
componentProps: {
|
||||||
|
class: 'w-full',
|
||||||
|
},
|
||||||
|
formItemClass: 'col-span-2',
|
||||||
|
labelWidth: 80,
|
||||||
|
},
|
||||||
|
layout: 'horizontal',
|
||||||
|
schema: useResetPasswordFormSchema(),
|
||||||
|
showDefaultActions: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const [Modal, modalApi] = useVbenModal({
|
||||||
|
async onConfirm() {
|
||||||
|
const { valid } = await formApi.validate();
|
||||||
|
if (!valid) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
modalApi.lock();
|
||||||
|
// 提交表单
|
||||||
|
const data = await formApi.getValues();
|
||||||
|
try {
|
||||||
|
await resetUserPassword(data.id, data.newPassword);
|
||||||
|
// 关闭并提示
|
||||||
|
await modalApi.close();
|
||||||
|
emit('success');
|
||||||
|
message.success($t('ui.actionMessage.operationSuccess'));
|
||||||
|
} finally {
|
||||||
|
modalApi.unlock();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async onOpenChange(isOpen: boolean) {
|
||||||
|
if (!isOpen) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 加载数据
|
||||||
|
const data = modalApi.getData<SystemUserApi.User>();
|
||||||
|
if (!data || !data.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 设置到 values
|
||||||
|
await formApi.setValues(data);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Modal title="重置密码">
|
||||||
|
<Form class="mx-4" />
|
||||||
|
</Modal>
|
||||||
|
</template>
|
||||||
Reference in New Issue
Block a user