Merge remote-tracking branch 'yudao/v5-next' into v5-next-tmp

pull/66/head
puhui999 2025-04-04 10:43:13 +08:00
commit 53d2d33ab0
67 changed files with 3660 additions and 1061 deletions

View File

@ -2,6 +2,7 @@ import type { PageResult } from '@vben/request';
import { requestClient } from '#/api/request';
// TODO @puhui999代码风格的统一
export namespace SystemMailAccountApi {
export interface MailAccountVO {
id: number;

View File

@ -2,6 +2,7 @@ import type { PageResult } from '@vben/request';
import { requestClient } from '#/api/request';
// TODO @puhui999代码风格的统一
export namespace SystemMailLogApi {
export interface MailLogVO {
id: number;

View File

@ -2,6 +2,7 @@ import type { PageResult } from '@vben/request';
import { requestClient } from '#/api/request';
// TODO @puhui999代码风格的统一
export namespace SystemMailTemplateApi {
export interface MailTemplateVO {
id: number;

View File

@ -1,10 +1,10 @@
import type { PageResult } from '@vben/request';
import type { PageParam, PageResult } from '@vben/request';
import { requestClient } from '#/api/request';
export namespace SystemSmsChannelApi {
/** 短信渠道信息 */
export interface SmsChannelVO {
export interface SmsChannel {
id?: number;
code: string;
status: number;
@ -18,8 +18,8 @@ export namespace SystemSmsChannelApi {
}
/** 查询短信渠道列表 */
export function getSmsChannelPage(params: any) {
return requestClient.get<PageResult<SystemSmsChannelApi.SmsChannelVO>>(
export function getSmsChannelPage(params: PageParam) {
return requestClient.get<PageResult<SystemSmsChannelApi.SmsChannel>>(
'/system/sms-channel/page',
{ params },
);
@ -27,25 +27,21 @@ export function getSmsChannelPage(params: any) {
/** 获得短信渠道精简列表 */
export function getSimpleSmsChannelList() {
return requestClient.get<SystemSmsChannelApi.SmsChannelVO[]>(
'/system/sms-channel/simple-list',
);
return requestClient.get<SystemSmsChannelApi.SmsChannel[]>('/system/sms-channel/simple-list');
}
/** 查询短信渠道详情 */
export function getSmsChannel(id: number) {
return requestClient.get<SystemSmsChannelApi.SmsChannelVO>(
`/system/sms-channel/get?id=${id}`,
);
return requestClient.get<SystemSmsChannelApi.SmsChannel>(`/system/sms-channel/get?id=${id}`);
}
/** 新增短信渠道 */
export function createSmsChannel(data: SystemSmsChannelApi.SmsChannelVO) {
export function createSmsChannel(data: SystemSmsChannelApi.SmsChannel) {
return requestClient.post('/system/sms-channel/create', data);
}
/** 修改短信渠道 */
export function updateSmsChannel(data: SystemSmsChannelApi.SmsChannelVO) {
export function updateSmsChannel(data: SystemSmsChannelApi.SmsChannel) {
return requestClient.put('/system/sms-channel/update', data);
}

View File

@ -1,4 +1,4 @@
import type { PageResult } from '@vben/request';
import type { PageParam, PageResult } from '@vben/request';
import { requestClient } from '#/api/request';
@ -32,11 +32,8 @@ export namespace SystemSmsLogApi {
}
/** 查询短信日志列表 */
export function getSmsLogPage(params: any) {
return requestClient.get<PageResult<SystemSmsLogApi.SmsLogVO>>(
'/system/sms-log/page',
{ params },
);
export function getSmsLogPage(params: PageParam) {
return requestClient.get<PageResult<SystemSmsLogApi.SmsLogVO>>('/system/sms-log/page', { params });
}
/** 导出短信日志 */

View File

@ -4,7 +4,7 @@ import { requestClient } from '#/api/request';
export namespace SystemSmsTemplateApi {
/** 短信模板信息 */
export interface SmsTemplateVO {
export interface SmsTemplate {
id?: number;
type?: number;
status: number;
@ -20,7 +20,7 @@ export namespace SystemSmsTemplateApi {
}
/** 发送短信请求 */
export interface SendSmsReqVO {
export interface SmsSendReqVO {
mobile: string;
templateCode: string;
templateParams: Record<string, any>;
@ -29,7 +29,7 @@ export namespace SystemSmsTemplateApi {
/** 查询短信模板列表 */
export function getSmsTemplatePage(params: any) {
return requestClient.get<PageResult<SystemSmsTemplateApi.SmsTemplateVO>>(
return requestClient.get<PageResult<SystemSmsTemplateApi.SmsTemplate>>(
'/system/sms-template/page',
{ params },
);
@ -37,18 +37,16 @@ export function getSmsTemplatePage(params: any) {
/** 查询短信模板详情 */
export function getSmsTemplate(id: number) {
return requestClient.get<SystemSmsTemplateApi.SmsTemplateVO>(
`/system/sms-template/get?id=${id}`,
);
return requestClient.get<SystemSmsTemplateApi.SmsTemplate>(`/system/sms-template/get?id=${id}`);
}
/** 新增短信模板 */
export function createSmsTemplate(data: SystemSmsTemplateApi.SmsTemplateVO) {
export function createSmsTemplate(data: SystemSmsTemplateApi.SmsTemplate) {
return requestClient.post('/system/sms-template/create', data);
}
/** 修改短信模板 */
export function updateSmsTemplate(data: SystemSmsTemplateApi.SmsTemplateVO) {
export function updateSmsTemplate(data: SystemSmsTemplateApi.SmsTemplate) {
return requestClient.put('/system/sms-template/update', data);
}
@ -59,12 +57,10 @@ export function deleteSmsTemplate(id: number) {
/** 导出短信模板 */
export function exportSmsTemplate(params: any) {
return requestClient.download('/system/sms-template/export-excel', {
params,
});
return requestClient.download('/system/sms-template/export-excel', { params });
}
/** 发送短信 */
export function sendSms(data: SystemSmsTemplateApi.SendSmsReqVO) {
export function sendSms(data: SystemSmsTemplateApi.SmsSendReqVO) {
return requestClient.post('/system/sms-template/send-sms', data);
}

View File

@ -0,0 +1,51 @@
import type { PageParam, PageResult } from '@vben/request';
import { requestClient } from '#/api/request';
export namespace SystemTenantPackageApi {
/** 租户套餐信息 */
export interface SystemTenantPackage {
id: number;
name: string;
status: number;
remark: string;
creator: string;
updater: string;
updateTime: string;
menuIds: number[];
createTime: Date;
}
}
/** 租户套餐列表 */
export function getTenantPackagePage(params: PageParam) {
return requestClient.get<PageResult<SystemTenantPackageApi.SystemTenantPackage>>(
'/system/tenant-package/page',
{ params }
);
}
/** 查询租户套餐详情 */
export function getTenantPackage(id: number) {
return requestClient.get(`/system/tenant-package/get?id=${id}`);
}
/** 新增租户套餐 */
export function createTenantPackage(data: SystemTenantPackageApi.SystemTenantPackage) {
return requestClient.post('/system/tenant-package/create', data);
}
/** 修改租户套餐 */
export function updateTenantPackage(data: SystemTenantPackageApi.SystemTenantPackage) {
return requestClient.put('/system/tenant-package/update', data);
}
/** 删除租户套餐 */
export function deleteTenantPackage(id: number) {
return requestClient.delete(`/system/tenant-package/delete?id=${id}`);
}
/** 获取租户套餐精简信息列表 */
export function getTenantPackageList() {
return requestClient.get<SystemTenantPackageApi.SystemTenantPackage[]>('/system/tenant-package/get-simple-list');
}

View File

@ -0,0 +1,55 @@
import type { PageParam, PageResult } from '@vben/request';
import { requestClient } from '#/api/request';
export namespace SystemTenantApi {
/** 租户信息 */
export interface SystemTenant {
id?: number;
name: string;
packageId: number;
contactName: string;
contactMobile: string;
accountCount: number;
expireTime: Date;
website: string;
status: number;
}
}
/** 租户列表 */
export function getTenantPage(params: PageParam) {
return requestClient.get<PageResult<SystemTenantApi.SystemTenant>>('/system/tenant/page', { params });
}
/** 获取租户精简信息列表 */
export function getSimpleTenantList() {
return requestClient.get<SystemTenantApi.SystemTenant[]>('/system/tenant/simple-list');
}
/** 查询租户详情 */
export function getTenant(id: number) {
return requestClient.get<SystemTenantApi.SystemTenant>(`/system/tenant/get?id=${id}`,);
}
/** 新增租户 */
export function createTenant(data: SystemTenantApi.SystemTenant) {
return requestClient.post('/system/tenant/create', data);
}
/** 修改租户 */
export function updateTenant(data: SystemTenantApi.SystemTenant) {
return requestClient.put('/system/tenant/update', data);
}
/** 删除租户 */
export function deleteTenant(id: number) {
return requestClient.delete(`/system/tenant/delete?id=${id}`);
}
/** 导出租户 */
export function exportTenant(params: any) {
return requestClient.download('/system/tenant/export-excel', {
params,
});
}

View File

@ -47,7 +47,7 @@ const [Modal, modalApi] = useVbenModal({
modalApi.lock(false);
}
},
async onOpenChange(isOpen) {
async onOpenChange(isOpen: boolean) {
if (!isOpen) {
return;
}

View File

@ -47,7 +47,7 @@ const [Modal, modalApi] = useVbenModal({
modalApi.lock(false);
}
},
async onOpenChange(isOpen) {
async onOpenChange(isOpen: boolean) {
if (!isOpen) {
return;
}

View File

@ -47,7 +47,7 @@ const [Modal, modalApi] = useVbenModal({
modalApi.lock(false);
}
},
async onOpenChange(isOpen) {
async onOpenChange(isOpen: boolean) {
if (!isOpen) {
return;
}

View File

@ -59,7 +59,7 @@ const [Modal, modalApi] = useVbenModal({
modalApi.lock(false);
}
},
async onOpenChange(isOpen) {
async onOpenChange(isOpen: boolean) {
if (!isOpen) {
return;
}

View File

@ -54,7 +54,7 @@ const [Modal, modalApi] = useVbenModal({
modalApi.lock(false);
}
},
async onOpenChange(isOpen) {
async onOpenChange(isOpen: boolean) {
if (!isOpen) {
return;
}

View File

@ -47,7 +47,7 @@ const [Modal, modalApi] = useVbenModal({
modalApi.lock(false);
}
},
async onOpenChange(isOpen) {
async onOpenChange(isOpen: boolean) {
if (!isOpen) {
return;
}

View File

@ -11,7 +11,6 @@ export function useFormSchema(): VbenFormSchema[] {
return [
{
fieldName: 'id',
label: 'id',
component: 'Input',
dependencies: {
triggerFields: [''],
@ -22,6 +21,9 @@ export function useFormSchema(): VbenFormSchema[] {
fieldName: 'signature',
label: '短信签名',
component: 'Input',
componentProps: {
placeholder: '请输入短信签名',
},
rules: 'required',
},
{
@ -29,8 +31,9 @@ export function useFormSchema(): VbenFormSchema[] {
label: '渠道编码',
component: 'Select',
componentProps: {
class: 'w-full',
options: getDictOptions(DICT_TYPE.SYSTEM_SMS_CHANNEL_CODE, 'string'),
class: 'w-full',
placeholder: '请选择短信渠道',
},
rules: 'required',
},
@ -49,22 +52,34 @@ export function useFormSchema(): VbenFormSchema[] {
fieldName: 'remark',
label: '备注',
component: 'Textarea',
componentProps: {
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',
}
},
];
}
@ -76,6 +91,10 @@ export function useGridFormSchema(): VbenFormSchema[] {
fieldName: 'signature',
label: '短信签名',
component: 'Input',
componentProps: {
allowClear: true,
placeholder: '请输入短信签名',
}
},
{
fieldName: 'code',
@ -84,6 +103,7 @@ export function useGridFormSchema(): VbenFormSchema[] {
componentProps: {
allowClear: true,
options: getDictOptions(DICT_TYPE.SYSTEM_SMS_CHANNEL_CODE, 'string'),
placeholder: '请选择短信渠道',
},
},
{
@ -96,6 +116,7 @@ export function useGridFormSchema(): VbenFormSchema[] {
},
},
{
// TODO @芋艿:怎么解决范围检索
fieldName: 'createTime',
label: '创建时间',
component: 'RangePicker',
@ -107,7 +128,7 @@ export function useGridFormSchema(): VbenFormSchema[] {
}
/** 列表的字段 */
export function useGridColumns<T = SystemSmsChannelApi.SmsChannelVO>(
export function useGridColumns<T = SystemSmsChannelApi.SmsChannel>(
onActionClick: OnActionClickFn<T>,
): VxeTableGridOptions['columns'] {
return [

View File

@ -1,26 +1,18 @@
<script lang="ts" setup>
import type {
OnActionClickParams,
VxeTableGridOptions,
} from '#/adapter/vxe-table';
import type { OnActionClickParams, VxeTableGridOptions } from '#/adapter/vxe-table';
import type { SystemSmsChannelApi } from '#/api/system/sms/channel';
import { Page, useVbenModal } from '@vben/common-ui';
import { Download, Plus } from '@vben/icons';
import { Button, message } from 'ant-design-vue';
import { Download, Plus } from '@vben/icons';
import Form from './modules/form.vue';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import {
deleteSmsChannel,
exportSmsChannel,
getSmsChannelPage,
} from '#/api/system/sms/channel';
import { $t } from '#/locales';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { getSmsChannelPage, deleteSmsChannel, exportSmsChannel } from '#/api/system/sms/channel';
import { downloadByData } from '#/utils/download';
import { useGridColumns, useGridFormSchema } from './data';
import Form from './modules/form.vue';
const [FormModal, formModalApi] = useVbenModal({
connectedComponent: Form,
@ -44,12 +36,12 @@ function onCreate() {
}
/** 编辑短信渠道 */
function onEdit(row: SystemSmsChannelApi.SmsChannelVO) {
function onEdit(row: SystemSmsChannelApi.SmsChannel) {
formModalApi.setData(row).open();
}
/** 删除短信渠道 */
async function onDelete(row: SystemSmsChannelApi.SmsChannelVO) {
async function onDelete(row: SystemSmsChannelApi.SmsChannel) {
const hideLoading = message.loading({
content: $t('ui.actionMessage.deleting', [row.signature]),
duration: 0,
@ -71,16 +63,16 @@ async function onDelete(row: SystemSmsChannelApi.SmsChannelVO) {
function onActionClick({
code,
row,
}: OnActionClickParams<SystemSmsChannelApi.SmsChannelVO>) {
}: OnActionClickParams<SystemSmsChannelApi.SmsChannel>) {
switch (code) {
case 'delete': {
onDelete(row);
break;
}
case 'edit': {
onEdit(row);
break;
}
case 'delete': {
onDelete(row);
break;
}
}
}
@ -110,7 +102,7 @@ const [Grid, gridApi] = useVbenVxeGrid({
refresh: { code: 'query' },
search: true,
},
} as VxeTableGridOptions<SystemSmsChannelApi.SmsChannelVO>,
} as VxeTableGridOptions<SystemSmsChannelApi.SmsChannel>,
});
</script>

View File

@ -1,24 +1,18 @@
<script lang="ts" setup>
import type { SystemSmsChannelApi } from '#/api/system/sms/channel';
import { computed, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { message } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import {
createSmsChannel,
getSmsChannel,
updateSmsChannel,
} from '#/api/system/sms/channel';
import { $t } from '#/locales';
import { computed, ref } from 'vue';
import { useVbenForm } from '#/adapter/form';
import { getSmsChannel, createSmsChannel, updateSmsChannel } from '#/api/system/sms/channel';
import { useFormSchema } from '../data';
const emit = defineEmits(['success']);
const formData = ref<SystemSmsChannelApi.SmsChannelVO>();
const formData = ref<SystemSmsChannelApi.SmsChannel>();
const getTitle = computed(() => {
return formData.value?.id
? $t('ui.actionTitle.edit', ['短信渠道'])
@ -29,6 +23,9 @@ const [Form, formApi] = useVbenForm({
layout: 'horizontal',
schema: useFormSchema(),
showDefaultActions: false,
commonConfig: {
labelWidth: 120
}
});
const [Modal, modalApi] = useVbenModal({
@ -39,12 +36,9 @@ const [Modal, modalApi] = useVbenModal({
}
modalApi.lock();
//
const data =
(await formApi.getValues()) as SystemSmsChannelApi.SmsChannelVO;
const data = (await formApi.getValues()) as SystemSmsChannelApi.SmsChannel;
try {
await (formData.value?.id
? updateSmsChannel(data)
: createSmsChannel(data));
await (formData.value?.id ? updateSmsChannel(data) : createSmsChannel(data));
//
await modalApi.close();
emit('success');
@ -56,12 +50,12 @@ const [Modal, modalApi] = useVbenModal({
modalApi.lock(false);
}
},
async onOpenChange(isOpen) {
async onOpenChange(isOpen: boolean) {
if (!isOpen) {
return;
}
//
const data = modalApi.getData<SystemSmsChannelApi.SmsChannelVO>();
const data = modalApi.getData<SystemSmsChannelApi.SmsChannel>();
if (!data || !data.id) {
return;
}

View File

@ -12,6 +12,10 @@ export function useGridFormSchema(): VbenFormSchema[] {
fieldName: 'mobile',
label: '手机号',
component: 'Input',
componentProps: {
allowClear: true,
placeholder: '请输入手机号',
}
},
{
fieldName: 'channelId',
@ -22,23 +26,30 @@ export function useGridFormSchema(): VbenFormSchema[] {
labelField: 'signature',
valueField: 'id',
allowClear: true,
placeholder: '请选择短信渠道',
},
},
{
fieldName: 'templateId',
label: '模板编号',
component: 'Input',
componentProps: {
allowClear: true,
placeholder: '请输入模板编号',
}
},
{
fieldName: 'sendStatus',
label: '发送状态',
component: 'Select',
componentProps: {
allowClear: true,
options: getDictOptions(DICT_TYPE.SYSTEM_SMS_SEND_STATUS, 'number'),
allowClear: true,
placeholder: '请选择发送状态',
},
},
{
// TODO @芋艿:怎么解决范围检索
fieldName: 'sendTime',
label: '发送时间',
component: 'RangePicker',
@ -51,11 +62,13 @@ export function useGridFormSchema(): VbenFormSchema[] {
label: '接收状态',
component: 'Select',
componentProps: {
allowClear: true,
options: getDictOptions(DICT_TYPE.SYSTEM_SMS_RECEIVE_STATUS, 'number'),
allowClear: true,
placeholder: '请选择接收状态',
},
},
{
// TODO @芋艿:怎么解决范围检索
fieldName: 'receiveTime',
label: '接收时间',
component: 'RangePicker',

View File

@ -1,22 +1,18 @@
<script lang="ts" setup>
import type {
OnActionClickParams,
VxeTableGridOptions,
} from '#/adapter/vxe-table';
import type { OnActionClickParams, VxeTableGridOptions } from '#/adapter/vxe-table';
import type { SystemSmsLogApi } from '#/api/system/sms/log';
import { Page, useVbenModal } from '@vben/common-ui';
import { Download } from '@vben/icons';
import { Button } from 'ant-design-vue';
import Form from './modules/form.vue';
import { $t } from '#/locales';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { exportSmsLog, getSmsLogPage } from '#/api/system/sms/log';
import { $t } from '#/locales';
import { downloadByData } from '#/utils/download';
import { useGridColumns, useGridFormSchema } from './data';
import Form from './modules/form.vue';
const [FormModal, formModalApi] = useVbenModal({
connectedComponent: Form,

View File

@ -1,17 +1,14 @@
<script lang="ts" setup>
import type { SystemSmsLogApi } from '#/api/system/sms/log';
import { computed, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { ref } from 'vue';
const formData = ref<SystemSmsLogApi.SmsLogVO>();
const getTitle = computed(() => {
return '短信日志详情';
});
const [Modal, modalApi] = useVbenModal({
async onOpenChange(isOpen) {
async onOpenChange(isOpen: boolean) {
if (!isOpen) {
return;
}
@ -30,8 +27,9 @@ const [Modal, modalApi] = useVbenModal({
});
</script>
<!-- TODO @puhui999https://ant-design.antgroup.com/components/descriptions-cn -->
<template>
<Modal :title="getTitle">
<Modal title="短信日志详情">
<div class="p-4">
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
<div class="form-item">
@ -39,6 +37,7 @@ const [Modal, modalApi] = useVbenModal({
<div>{{ formData?.id }}</div>
</div>
<div class="form-item">
<!-- TODO @puhui格式不对 -->
<div class="form-label">创建时间</div>
<div>{{ formData?.createTime }}</div>
</div>
@ -67,6 +66,7 @@ const [Modal, modalApi] = useVbenModal({
<div class="mt-4 grid grid-cols-1 gap-4 md:grid-cols-2">
<div class="form-item">
<!-- TODO @puhui格式不对 -->
<div class="form-label">发送状态</div>
<div>{{ formData?.sendStatus }}</div>
</div>
@ -83,6 +83,7 @@ const [Modal, modalApi] = useVbenModal({
<div>{{ formData?.apiSendMsg }}</div>
</div>
<div class="form-item">
<!-- TODO @puhui格式不对 -->
<div class="form-label">接收状态</div>
<div>{{ formData?.receiveStatus }}</div>
</div>
@ -91,19 +92,19 @@ const [Modal, modalApi] = useVbenModal({
<div>{{ formData?.receiveTime }}</div>
</div>
<div class="form-item">
<div class="form-label">API接收编码</div>
<div class="form-label">API 接收编码</div>
<div>{{ formData?.apiReceiveCode }}</div>
</div>
<div class="form-item">
<div class="form-label">API接收消息</div>
<div class="form-label">API 接收消息</div>
<div>{{ formData?.apiReceiveMsg }}</div>
</div>
<div class="form-item">
<div class="form-label">API请求ID</div>
<div class="form-label">API 请求 ID</div>
<div>{{ formData?.apiRequestId }}</div>
</div>
<div class="form-item">
<div class="form-label">API序列号</div>
<div class="form-label">API 序列号</div>
<div>{{ formData?.apiSerialNo }}</div>
</div>
</div>

View File

@ -12,7 +12,6 @@ export function useFormSchema(): VbenFormSchema[] {
return [
{
fieldName: 'id',
label: 'id',
component: 'Input',
dependencies: {
triggerFields: [''],
@ -24,8 +23,9 @@ export function useFormSchema(): VbenFormSchema[] {
label: '短信类型',
component: 'Select',
componentProps: {
class: 'w-full',
options: getDictOptions(DICT_TYPE.SYSTEM_SMS_TEMPLATE_TYPE, 'number'),
class: 'w-full',
placeholder: '请选择短信类型',
},
rules: 'required',
},
@ -33,12 +33,18 @@ export function useFormSchema(): VbenFormSchema[] {
fieldName: 'name',
label: '模板名称',
component: 'Input',
componentProps: {
placeholder: '请输入模板名称',
},
rules: 'required',
},
{
fieldName: 'code',
label: '模板编码',
component: 'Input',
componentProps: {
placeholder: '请输入模板编码',
},
rules: 'required',
},
{
@ -50,6 +56,7 @@ export function useFormSchema(): VbenFormSchema[] {
class: 'w-full',
labelField: 'signature',
valueField: 'id',
placeholder: '请选择短信渠道',
},
rules: 'required',
},
@ -68,17 +75,27 @@ export function useFormSchema(): VbenFormSchema[] {
fieldName: 'content',
label: '模板内容',
component: 'Textarea',
componentProps: {
placeholder: '请输入模板内容',
},
rules: 'required',
},
{
fieldName: 'apiTemplateId',
label: '短信 API 的模板编号',
component: 'Input',
componentProps: {
placeholder: '请输入短信 API 的模板编号',
},
rules: 'required',
},
{
fieldName: 'remark',
label: '备注',
component: 'Textarea',
componentProps: {
placeholder: '请输入备注',
}
},
];
}
@ -91,8 +108,9 @@ export function useGridFormSchema(): VbenFormSchema[] {
label: '短信类型',
component: 'Select',
componentProps: {
allowClear: true,
options: getDictOptions(DICT_TYPE.SYSTEM_SMS_TEMPLATE_TYPE, 'number'),
allowClear: true,
placeholder: '请选择短信类型',
},
},
{
@ -100,19 +118,28 @@ export function useGridFormSchema(): VbenFormSchema[] {
label: '开启状态',
component: 'Select',
componentProps: {
allowClear: true,
options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
allowClear: true,
placeholder: '请选择开启状态',
},
},
{
fieldName: 'code',
label: '模板编码',
component: 'Input',
componentProps: {
allowClear: true,
placeholder: '请输入模板编码',
}
},
{
fieldName: 'name',
label: '模板名称',
component: 'Input',
componentProps: {
allowClear: true,
placeholder: '请输入模板名称',
}
},
{
fieldName: 'channelId',
@ -123,8 +150,10 @@ export function useGridFormSchema(): VbenFormSchema[] {
labelField: 'signature',
valueField: 'id',
allowClear: true,
placeholder: '请选择短信渠道',
},
},
// TODO @芋艿:范围检索的处理
{
fieldName: 'createTime',
label: '创建时间',
@ -143,6 +172,9 @@ export function useSendSmsFormSchema(): VbenFormSchema[] {
fieldName: 'mobile',
label: '手机号码',
component: 'Input',
componentProps: {
placeholder: '请输入手机号码',
},
rules: 'required',
},
{
@ -158,7 +190,7 @@ export function useSendSmsFormSchema(): VbenFormSchema[] {
}
/** 列表的字段 */
export function useGridColumns<T = SystemSmsTemplateApi.SmsTemplateVO>(
export function useGridColumns<T = SystemSmsTemplateApi.SmsTemplate>(
onActionClick: OnActionClickFn<T>,
): VxeTableGridOptions['columns'] {
return [
@ -228,7 +260,7 @@ export function useGridColumns<T = SystemSmsTemplateApi.SmsTemplateVO>(
{
field: 'operation',
title: '操作',
minWidth: 300,
minWidth: 180,
align: 'center',
fixed: 'right',
cellRender: {

View File

@ -1,27 +1,19 @@
<script lang="ts" setup>
import type {
OnActionClickParams,
VxeTableGridOptions,
} from '#/adapter/vxe-table';
import type { OnActionClickParams, VxeTableGridOptions } from '#/adapter/vxe-table';
import type { SystemSmsTemplateApi } from '#/api/system/sms/template';
import { Page, useVbenModal } from '@vben/common-ui';
import { Download, Plus } from '@vben/icons';
import { Button, message } from 'ant-design-vue';
import Form from './modules/form.vue';
import SendForm from './modules/send-form.vue';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import {
deleteSmsTemplate,
exportSmsTemplate,
getSmsTemplatePage,
} from '#/api/system/sms/template';
import { $t } from '#/locales';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { deleteSmsTemplate, exportSmsTemplate, getSmsTemplatePage } from '#/api/system/sms/template';
import { downloadByData } from '#/utils/download';
import { useGridColumns, useGridFormSchema } from './data';
import Form from './modules/form.vue';
import SendForm from './modules/send-form.vue';
const [FormModal, formModalApi] = useVbenModal({
connectedComponent: Form,
@ -50,17 +42,17 @@ function onCreate() {
}
/** 编辑短信模板 */
function onEdit(row: SystemSmsTemplateApi.SmsTemplateVO) {
function onEdit(row: SystemSmsTemplateApi.SmsTemplate) {
formModalApi.setData(row).open();
}
/** 发送测试短信 */
function onSend(row: SystemSmsTemplateApi.SmsTemplateVO) {
function onSend(row: SystemSmsTemplateApi.SmsTemplate) {
sendModalApi.setData(row).open();
}
/** 删除短信模板 */
async function onDelete(row: SystemSmsTemplateApi.SmsTemplateVO) {
async function onDelete(row: SystemSmsTemplateApi.SmsTemplate) {
const hideLoading = message.loading({
content: $t('ui.actionMessage.deleting', [row.name]),
duration: 0,
@ -82,16 +74,16 @@ async function onDelete(row: SystemSmsTemplateApi.SmsTemplateVO) {
function onActionClick({
code,
row,
}: OnActionClickParams<SystemSmsTemplateApi.SmsTemplateVO>) {
}: OnActionClickParams<SystemSmsTemplateApi.SmsTemplate>) {
switch (code) {
case 'delete': {
onDelete(row);
break;
}
case 'edit': {
onEdit(row);
break;
}
case 'delete': {
onDelete(row);
break;
}
case 'sms-send': {
onSend(row);
break;
@ -125,7 +117,7 @@ const [Grid, gridApi] = useVbenVxeGrid({
refresh: { code: 'query' },
search: true,
},
} as VxeTableGridOptions<SystemSmsTemplateApi.SmsTemplateVO>,
} as VxeTableGridOptions<SystemSmsTemplateApi.SmsTemplate>,
});
</script>

View File

@ -1,24 +1,18 @@
<script lang="ts" setup>
import type { SystemSmsTemplateApi } from '#/api/system/sms/template';
import { computed, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { message } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import {
createSmsTemplate,
getSmsTemplate,
updateSmsTemplate,
} from '#/api/system/sms/template';
import { $t } from '#/locales';
import { computed, ref } from 'vue';
import { useVbenForm } from '#/adapter/form';
import { createSmsTemplate, getSmsTemplate, updateSmsTemplate } from '#/api/system/sms/template';
import { useFormSchema } from '../data';
const emit = defineEmits(['success']);
const formData = ref<SystemSmsTemplateApi.SmsTemplateVO>();
const formData = ref<SystemSmsTemplateApi.SmsTemplate>();
const getTitle = computed(() => {
return formData.value?.id
? $t('ui.actionTitle.edit', ['短信模板'])
@ -29,6 +23,9 @@ const [Form, formApi] = useVbenForm({
layout: 'horizontal',
schema: useFormSchema(),
showDefaultActions: false,
commonConfig: {
labelWidth: 140
}
});
const [Modal, modalApi] = useVbenModal({
@ -40,7 +37,7 @@ const [Modal, modalApi] = useVbenModal({
modalApi.lock();
//
const data =
(await formApi.getValues()) as SystemSmsTemplateApi.SmsTemplateVO;
(await formApi.getValues()) as SystemSmsTemplateApi.SmsTemplate;
try {
await (formData.value?.id
? updateSmsTemplate(data)
@ -56,12 +53,12 @@ const [Modal, modalApi] = useVbenModal({
modalApi.lock(false);
}
},
async onOpenChange(isOpen) {
async onOpenChange(isOpen: boolean) {
if (!isOpen) {
return;
}
//
const data = modalApi.getData<SystemSmsTemplateApi.SmsTemplateVO>();
const data = modalApi.getData<SystemSmsTemplateApi.SmsTemplate>();
if (!data || !data.id) {
return;
}

View File

@ -1,24 +1,19 @@
<script lang="ts" setup>
import type { SystemSmsTemplateApi } from '#/api/system/sms/template';
import { computed, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { ref } from 'vue';
import { message } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import { sendSms } from '#/api/system/sms/template';
import { $t } from '#/locales';
import { useSendSmsFormSchema } from '../data';
const emit = defineEmits(['success']);
const templateData = ref<SystemSmsTemplateApi.SmsTemplateVO>();
const getTitle = computed(() => {
return $t('ui.actionTitle.send', ['短信']);
});
const templateData = ref<SystemSmsTemplateApi.SmsTemplate>();
// TODO @puhui999
//
const buildSchema = () => {
const schema = useSendSmsFormSchema();
@ -63,7 +58,7 @@ const [Modal, modalApi] = useVbenModal({
}
//
const data: SystemSmsTemplateApi.SendSmsReqVO = {
const data: SystemSmsTemplateApi.SmsSendReqVO = {
mobile: values.mobile,
templateCode: templateData.value?.code || '',
templateParams: paramsObj,
@ -84,12 +79,12 @@ const [Modal, modalApi] = useVbenModal({
modalApi.lock(false);
}
},
async onOpenChange(isOpen) {
async onOpenChange(isOpen: boolean) {
if (!isOpen) {
return;
}
//
const data = modalApi.getData<SystemSmsTemplateApi.SmsTemplateVO>();
const data = modalApi.getData<SystemSmsTemplateApi.SmsTemplate>();
if (!data) {
return;
}
@ -103,7 +98,7 @@ const [Modal, modalApi] = useVbenModal({
</script>
<template>
<Modal :title="getTitle">
<Modal title="发送短信">
<Form class="mx-4" />
</Modal>
</template>

View File

@ -0,0 +1,239 @@
import type { VbenFormSchema } from '#/adapter/form';
import type { OnActionClickFn, VxeTableGridOptions } from '#/adapter/vxe-table';
import type { SystemTenantApi } from '#/api/system/tenant';
import { z } from '#/adapter/form';
import { getTenantPackageList } from '#/api/system/tenant-package';
import { CommonStatusEnum } from '#/utils/constants';
import { DICT_TYPE, getDictOptions } from '#/utils/dict';
/** 新增/修改的表单 */
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(),
class: 'w-full',
labelField: 'name',
valueField: 'id',
placeholder: '请选择租户套餐',
},
rules: 'required',
},
{
fieldName: 'contactName',
label: '联系人',
component: 'Input',
rules: 'required',
},
{
fieldName: 'contactMobile',
label: '联系手机',
component: 'Input',
},
{
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',
componentProps: {
class: 'w-full',
placeholder: '请输入账号额度',
},
rules: 'required',
},
{
label: '过期时间',
fieldName: 'expireTime',
component: 'DatePicker',
componentProps: {
format: 'YYYY-MM-DD',
valueFormat: 'x',
class: 'w-full',
},
rules: 'required',
},
{
label: '绑定域名',
fieldName: 'website',
component: 'Input',
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),
},
];
}
/** 列表的搜索表单 */
export function useGridFormSchema(): VbenFormSchema[] {
return [
{
fieldName: 'name',
label: '租户名',
component: 'Input',
componentProps: {
allowClear: true,
},
},
{
fieldName: 'contactName',
label: '联系人',
component: 'Input',
componentProps: {
allowClear: true,
},
},
{
fieldName: 'contactMobile',
label: '联系手机',
component: 'Input',
componentProps: {
allowClear: true,
},
},
{
fieldName: 'status',
label: '状态',
component: 'Select',
componentProps: {
allowClear: true,
options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
},
},
{
fieldName: 'createTime',
label: '创建时间',
component: 'RangePicker',
componentProps: {
allowClear: true,
},
},
];
}
/** 列表的字段 */
const tenantPackageList = await getTenantPackageList();
export function useGridColumns<T = SystemTenantApi.SystemTenant>(onActionClick: OnActionClickFn<T>): VxeTableGridOptions['columns'] {
return [
{
field: 'id',
title: '租户编号',
minWidth: 100,
},
{
field: 'name',
title: '租户名',
minWidth: 180,
},
{
field: 'packageId',
title: '租户套餐',
minWidth: 180,
formatter: (row) => {
const packageId = row.cellValue;
return packageId === 0 ? '系统租户' : tenantPackageList.find((tenantPackage) => tenantPackage.id === packageId)?.name || '-';
},
},
{
field: 'contactName',
title: '联系人',
minWidth: 100,
},
{
field: 'contactMobile',
title: '联系手机',
minWidth: 180,
},
{
field: 'accountCount',
title: '账号额度',
minWidth: 100,
},
{
field: 'expireTime',
title: '过期时间',
minWidth: 180,
formatter: 'formatDateTime',
},
{
field: 'website',
title: '绑定域名',
minWidth: 180,
},
{
field: 'status',
title: '租户状态',
minWidth: 100,
cellRender: {
name: 'CellDict',
props: { type: DICT_TYPE.COMMON_STATUS },
},
},
{
field: 'createTime',
title: '创建时间',
minWidth: 180,
formatter: 'formatDateTime',
},
{
field: 'operation',
title: '操作',
minWidth: 130,
align: 'center',
fixed: 'right',
cellRender: {
attrs: {
nameField: 'name',
nameTitle: '租户',
onClick: onActionClick,
},
name: 'CellOperation',
},
},
];
}

View File

@ -0,0 +1,125 @@
<script lang="ts" setup>
import type { OnActionClickParams, VxeTableGridOptions } from '#/adapter/vxe-table';
import type { SystemTenantApi } from '#/api/system/tenant';
import { Page, useVbenModal } from '@vben/common-ui';
import { Button, message } from 'ant-design-vue';
import { Plus, Download } from '@vben/icons';
import Form from './modules/form.vue';
import { $t } from '#/locales';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { getTenantPage, deleteTenant, exportTenant } from '#/api/system/tenant';
import { downloadByData } from '#/utils/download';
import { useGridColumns, useGridFormSchema } from './data';
const [FormModal, formModalApi] = useVbenModal({
connectedComponent: Form,
destroyOnClose: true,
});
/** 刷新表格 */
function onRefresh() {
gridApi.query();
}
/** 导出表格 */
async function onExport() {
const data = await exportTenant(await gridApi.formApi.getValues());
downloadByData(data, '租户.xls');
}
/** 创建租户 */
function onCreate() {
formModalApi.setData(null).open();
}
/** 编辑租户 */
function onEdit(row: SystemTenantApi.SystemTenant) {
formModalApi.setData(row).open();
}
/** 删除租户 */
async function onDelete(row: SystemTenantApi.SystemTenant) {
const hideLoading = message.loading({
content: $t('ui.actionMessage.deleting', [row.name]),
duration: 0,
key: 'action_process_msg',
});
try {
await deleteTenant(row.id as number);
message.success({
content: $t('ui.actionMessage.deleteSuccess', [row.name]),
key: 'action_process_msg',
});
onRefresh();
} catch {
hideLoading();
}
}
/** 表格操作按钮的回调函数 */
function onActionClick({
code,
row,
}: OnActionClickParams<SystemTenantApi.SystemTenant>) {
switch (code) {
case 'edit': {
onEdit(row);
break;
}
case 'delete': {
onDelete(row);
break;
}
}
}
const [Grid, gridApi] = useVbenVxeGrid({
formOptions: {
schema: useGridFormSchema(),
// TODO @
},
gridOptions: {
columns: useGridColumns(onActionClick),
height: 'auto',
keepSource: true,
proxyConfig: {
ajax: {
query: async ({ page }, formValues) => {
return await getTenantPage({
pageNo: page.currentPage,
pageSize: page.pageSize,
...formValues,
});
},
},
},
rowConfig: {
keyField: 'id',
},
toolbarConfig: {
refresh: { code: 'query' },
search: true,
},
} as VxeTableGridOptions<SystemTenantApi.SystemTenant>,
});
</script>
<template>
<Page auto-content-height>
<FormModal @success="onRefresh" />
<Grid table-title="">
<template #toolbar-tools>
<Button type="primary" @click="onCreate">
<Plus class="size-5" />
{{ $t('ui.actionTitle.create', ['租户']) }}
</Button>
<Button type="primary" class="ml-2" @click="onExport">
<Download class="size-5" />
{{ $t('ui.actionTitle.export') }}
</Button>
</template>
</Grid>
</Page>
</template>

View File

@ -0,0 +1,74 @@
<script lang="ts" setup>
import type { SystemTenantApi } from '#/api/system/tenant';
import { useVbenModal } from '@vben/common-ui';
import { message } from 'ant-design-vue';
import { $t } from '#/locales';
import { computed, ref } from 'vue';
import { useVbenForm } from '#/adapter/form';
import { createTenant, getTenant, updateTenant } from '#/api/system/tenant';
import { useFormSchema } from '../data';
const emit = defineEmits(['success']);
const formData = ref<SystemTenantApi.SystemTenant>();
const getTitle = computed(() => {
return formData.value
? $t('ui.actionTitle.edit', ['租户'])
: $t('ui.actionTitle.create', ['租户']);
});
const [Form, formApi] = useVbenForm({
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.SystemTenant;
try {
await (formData.value ? updateTenant(data) : createTenant(data));
//
await modalApi.close();
emit('success');
message.success({
content: $t('ui.actionMessage.operationSuccess'),
key: 'action_process_msg',
});
} finally {
modalApi.lock(false);
}
},
async onOpenChange(isOpen: boolean) {
if (!isOpen) {
return;
}
//
const data = modalApi.getData<SystemTenantApi.SystemTenant>();
if (!data || !data.id) {
return;
}
modalApi.lock();
try {
formData.value = await getTenant(data.id as number);
// values
await formApi.setValues(formData.value);
} finally {
modalApi.lock(false);
}
},
});
</script>
<template>
<Modal :title="getTitle">
<Form class="mx-4" />
</Modal>
</template>

View File

@ -0,0 +1,139 @@
import type { VbenFormSchema } from '#/adapter/form';
import type { OnActionClickFn, VxeTableGridOptions } from '#/adapter/vxe-table';
import type { SystemTenantPackageApi } from '#/api/system/tenant-package';
import { z } from '#/adapter/form';
import { CommonStatusEnum } from '#/utils/constants';
import { DICT_TYPE, getDictOptions } from '#/utils/dict';
/** 新增/修改的表单 */
export function useFormSchema(): VbenFormSchema[] {
return [
{
fieldName: 'id',
component: 'Input',
dependencies: {
triggerFields: [''],
show: () => false,
},
},
{
fieldName: 'name',
label: '套餐名称',
component: 'Input',
componentProps: {
placeholder: '请输入套餐名称',
},
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: 'Textarea',
componentProps: {
placeholder: '请输入备注',
}
},
];
}
/** 列表的搜索表单 */
export function useGridFormSchema(): VbenFormSchema[] {
return [
{
fieldName: 'name',
label: '套餐名称',
component: 'Input',
componentProps: {
allowClear: true,
placeholder: '请输入套餐名称',
},
},
{
fieldName: 'status',
label: '状态',
component: 'Select',
componentProps: {
options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
allowClear: true,
placeholder: '请选择状态',
},
},
{
fieldName: 'createTime',
label: '创建时间',
component: 'RangePicker',
componentProps: {
allowClear: true,
},
},
];
}
/** 列表的字段 */
export function useGridColumns<T = SystemTenantPackageApi.SystemTenantPackage>(onActionClick: OnActionClickFn<T>): VxeTableGridOptions['columns'] {
return [
{
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',
},
{
field: 'operation',
title: '操作',
minWidth: 130,
align: 'center',
fixed: 'right',
cellRender: {
attrs: {
nameField: 'name',
nameTitle: '套餐',
onClick: onActionClick,
},
name: 'CellOperation',
},
},
];
}

View File

@ -0,0 +1,111 @@
<script lang="ts" setup>
import type { OnActionClickParams, VxeTableGridOptions } from '#/adapter/vxe-table';
import type { SystemTenantPackageApi } from '#/api/system/tenant-package';
import { Page, useVbenModal } from '@vben/common-ui';
import { Button, message } from 'ant-design-vue';
import Form from './modules/form.vue';
import { $t } from '#/locales';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { deleteTenantPackage, getTenantPackagePage } from '#/api/system/tenant-package';
import { useGridColumns, useGridFormSchema } from './data';
const [FormModal, formModalApi] = useVbenModal({
connectedComponent: Form,
destroyOnClose: true,
});
/** 刷新表格 */
function onRefresh() {
gridApi.query();
}
/** 创建租户套餐 */
function onCreate() {
formModalApi.setData(null).open();
}
/** 编辑租户套餐 */
function onEdit(row: SystemTenantPackageApi.SystemTenantPackage) {
formModalApi.setData(row).open();
}
/** 删除租户套餐 */
async function onDelete(row: SystemTenantPackageApi.SystemTenantPackage) {
const hideLoading = message.loading({
content: $t('ui.actionMessage.deleting', [row.name]),
duration: 0,
key: 'action_process_msg',
});
try {
await deleteTenantPackage(row.id as number);
message.success({
content: $t('ui.actionMessage.deleteSuccess', [row.name]),
key: 'action_process_msg',
});
onRefresh();
} catch {
hideLoading();
}
}
/** 表格操作按钮的回调函数 */
function onActionClick({ code, row }: OnActionClickParams<SystemTenantPackageApi.SystemTenantPackage>) {
switch (code) {
case 'edit': {
onEdit(row);
break;
}
case 'delete': {
onDelete(row);
break;
}
}
}
const [Grid, gridApi] = useVbenVxeGrid({
formOptions: {
schema: useGridFormSchema(),
// TODO @
},
gridOptions: {
columns: useGridColumns(onActionClick),
height: 'auto',
keepSource: true,
proxyConfig: {
ajax: {
query: async ({ page }, formValues) => {
return await getTenantPackagePage({
pageNo: page.currentPage,
pageSize: page.pageSize,
...formValues,
});
},
},
},
rowConfig: {
keyField: 'id',
},
toolbarConfig: {
refresh: { code: 'query' },
search: true,
},
} as VxeTableGridOptions<SystemTenantPackageApi.SystemTenantPackage>,
});
</script>
<template>
<Page auto-content-height>
<FormModal @success="onRefresh" />
<Grid table-title="">
<template #toolbar-tools>
<Button type="primary" @click="onCreate">
<Plus class="size-5" />
{{ $t('ui.actionTitle.create', ['套餐']) }}
</Button>
</template>
</Grid>
</Page>
</template>

View File

@ -0,0 +1,133 @@
<script lang="ts" setup>
import type { SystemDeptApi } from '#/api/system/dept';
import type { SystemTenantPackageApi } from '#/api/system/tenant-package';
import { useVbenModal, VbenTree } from '@vben/common-ui';
import { Checkbox, message } from 'ant-design-vue';
import { computed, ref } from 'vue';
import { $t } from '#/locales';
import { useVbenForm } from '#/adapter/form';
import { getMenuList } from '#/api/system/menu';
import { createTenantPackage, getTenantPackage, updateTenantPackage } from '#/api/system/tenant-package';
import { handleTree } from '#/utils/tree';
import { useFormSchema } from '../data';
const emit = defineEmits(['success']);
const formData = ref<SystemTenantPackageApi.SystemTenantPackage>();
const getTitle = computed(() => {
return formData.value ? $t('ui.actionTitle.edit', ['套餐']) : $t('ui.actionTitle.create', ['套餐']);
});
const menuTree = ref<SystemDeptApi.SystemDept[]>([]); //
const menuLoading = ref(false); //
const isAllSelected = ref(false); //
const isExpanded = ref(false); //
const expandedKeys = ref<number[]>([]); //
const [Form, formApi] = useVbenForm({
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.SystemTenantPackage;
try {
await (formData.value ? updateTenantPackage(data) : createTenantPackage(data));
//
await modalApi.close();
emit('success');
message.success({
content: $t('ui.actionMessage.operationSuccess'),
key: 'action_process_msg',
});
} finally {
modalApi.lock(false);
}
},
async onOpenChange(isOpen: boolean) {
//
await loadMenuTree();
if (!isOpen) {
return;
}
const data = modalApi.getData<SystemTenantPackageApi.SystemTenantPackage>();
if (!data || !data.id) {
return;
}
modalApi.lock();
try {
formData.value = await getTenantPackage(data.id as number);
await formApi.setValues(data);
} finally {
modalApi.lock(false);
}
},
});
/** 加载菜单树 */
async function loadMenuTree() {
menuLoading.value = true;
try {
const data = await getMenuList();
menuTree.value = handleTree(data);
} finally {
menuLoading.value = false;
}
}
/** 全选/全不选 */
function toggleSelectAll() {
isAllSelected.value = !isAllSelected.value;
if (isAllSelected.value) {
const allIds = getAllNodeIds(menuTree.value);
formApi.setFieldValue('menuIds', allIds);
} else {
formApi.setFieldValue('menuIds', []);
}
}
/** 展开/折叠所有节点 */
function toggleExpandAll() {
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">
<Form class="mx-6">
<template #menuIds="slotProps">
<Spin :spinning="menuLoading" class="w-full">
<!-- TODO @芋艿可优化使用 antd tree原因是更原生 -->
<VbenTree :tree-data="menuTree" multiple bordered :expanded="expandedKeys" v-bind="slotProps" value-field="id" label-field="name" />
</Spin>
</template>
</Form>
<template #prepend-footer>
<div class="flex flex-auto items-center">
<Checkbox :checked="isAllSelected" @change="toggleSelectAll"> </Checkbox>
<Checkbox :checked="isExpanded" @change="toggleExpandAll"> </Checkbox>
</div>
</template>
</Modal>
</template>

View File

@ -103,17 +103,26 @@ export function useGridFormSchema(): VbenFormSchema[] {
fieldName: 'username',
label: '用户名称',
component: 'Input',
componentProps: {
placeholder: '请输入用户名称',
allowClear: true,
},
},
{
fieldName: 'mobile',
label: '手机号码',
component: 'Input',
componentProps: {
placeholder: '请输入手机号码',
allowClear: true,
},
},
{
fieldName: 'status',
label: '状态',
component: 'Select',
componentProps: {
placeholder: '请输入用户状态',
allowClear: true,
options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
},
@ -124,6 +133,9 @@ export function useGridFormSchema(): VbenFormSchema[] {
component: 'RangePicker',
componentProps: {
allowClear: true,
showTime: true,
format: 'YYYY-MM-DD HH:mm:ss',
placeholder: ['开始日期', '结束日期'],
},
},
];
@ -150,7 +162,7 @@ export function useGridColumns<T = SystemUserApi.SystemUser>(
minWidth: 120,
},
{
field: 'deptId',
field: 'deptName',
title: '部门',
minWidth: 120,
},
@ -159,13 +171,18 @@ export function useGridColumns<T = SystemUserApi.SystemUser>(
title: '手机号码',
minWidth: 120,
},
// TODO @芋艿switch 的接入
{
field: 'status',
title: '状态',
minWidth: 100,
align: 'center',
cellRender: {
name: 'CellDict',
props: { type: DICT_TYPE.COMMON_STATUS },
name: 'CellSwitch',
props: {
activeValue: 0,
inactiveValue: 1
},
},
},
{
@ -177,16 +194,28 @@ export function useGridColumns<T = SystemUserApi.SystemUser>(
{
field: 'operation',
title: '操作',
minWidth: 180,
align: 'center',
minWidth: 160,
fixed: 'right',
align: 'center',
cellRender: {
attrs: {
nameField: 'username',
nameTitle: '用户',
nameField: 'name',
nameTitle: '角色',
onClick: onActionClick,
},
name: 'CellOperation',
options: [
'edit', // 默认的编辑按钮
'delete', // 默认的删除按钮
{
code: 'assign-data-permission',
text: '数据权限',
},
{
code: 'assign-menu',
text: '菜单权限',
}
],
},
},
];

View File

@ -47,7 +47,7 @@ const [Modal, modalApi] = useVbenModal({
modalApi.lock(false);
}
},
async onOpenChange(isOpen) {
async onOpenChange(isOpen: boolean) {
if (!isOpen) {
return;
}

View File

@ -168,6 +168,10 @@ function sidebarComponents(): DefaultTheme.SidebarItem[] {
link: 'common-ui/vben-api-component',
text: 'ApiComponent Api组件包装器',
},
{
link: 'common-ui/vben-alert',
text: 'Alert 轻量提示框',
},
{
link: 'common-ui/vben-modal',
text: 'Modal 模态框',

View File

@ -0,0 +1,111 @@
---
outline: deep
---
# Vben Alert 轻量提示框
框架提供的一些用于轻量提示的弹窗仅使用js代码即可快速动态创建提示而不需要在template写任何代码。
::: info 应用场景
Alert提供的功能与Modal类似但只适用于简单应用场景。例如临时性、动态地弹出模态确认框、输入框等。如果对弹窗有更复杂的需求请使用VbenModal
:::
::: tip README
下方示例代码中的,存在一些主题色未适配、样式缺失的问题,这些问题只在文档内会出现,实际使用并不会有这些问题,可忽略,不必纠结。
:::
## 基础用法
使用 `alert` 创建只有一个确认按钮的提示框。
<DemoPreview dir="demos/vben-alert/alert" />
使用 `confirm` 创建有确认和取消按钮的提示框。
<DemoPreview dir="demos/vben-alert/confirm" />
使用 `prompt` 创建有确认和取消按钮、接受用户输入的提示框。
<DemoPreview dir="demos/vben-alert/prompt" />
## 类型说明
```ts
/** 预置的图标类型 */
export type IconType = 'error' | 'info' | 'question' | 'success' | 'warning';
export type BeforeCloseScope = {
/** 是否为点击确认按钮触发的关闭 */
isConfirm: boolean;
};
export type AlertProps = {
/** 关闭前的回调如果返回false则终止关闭 */
beforeClose?: (
scope: BeforeCloseScope,
) => boolean | Promise<boolean | undefined> | undefined;
/** 边框 */
bordered?: boolean;
/** 取消按钮的标题 */
cancelText?: string;
/** 是否居中显示 */
centered?: boolean;
/** 确认按钮的标题 */
confirmText?: string;
/** 弹窗容器的额外样式 */
containerClass?: string;
/** 弹窗提示内容 */
content: Component | string;
/** 弹窗内容的额外样式 */
contentClass?: string;
/** 弹窗的图标(在标题的前面) */
icon?: Component | IconType;
/** 是否显示取消按钮 */
showCancel?: boolean;
/** 弹窗标题 */
title?: string;
};
/**
* 函数签名
* alert和confirm的函数签名相同。
* confirm默认会显示取消按钮而alert默认只有一个按钮
* */
export function alert(options: AlertProps): Promise<void>;
export function alert(
message: string,
options?: Partial<AlertProps>,
): Promise<void>;
export function alert(
message: string,
title?: string,
options?: Partial<AlertProps>,
): Promise<void>;
/**
* 弹出输入框的函数签名。
* beforeClose的参数会传入用户当前输入的值
* component指定接受用户输入的组件默认为Input
* componentProps 为输入组件设置的属性数据
* defaultValue 默认的值
* modelPropName 输入组件的值属性名称。默认为modelValue
*/
export async function prompt<T = any>(
options: Omit<AlertProps, 'beforeClose'> & {
beforeClose?: (
scope: BeforeCloseScope & {
/** 输入组件的当前值 */
value: T;
},
) => boolean | Promise<boolean | undefined> | undefined;
component?: Component;
componentProps?: Recordable<any>;
defaultValue?: T;
modelPropName?: string;
},
): Promise<T | undefined>;
```

View File

@ -123,6 +123,10 @@ function fetchApi(): Promise<Record<string, any>> {
:::
## 并发和缓存
有些场景下可能需要使用多个ApiComponent它们使用了相同的远程数据源例如用在可编辑的表格中。如果直接将请求后端接口的函数传递给api属性则每一个实例都会访问一次接口这会造成资源浪费是完全没有必要的。Tanstack Query提供了并发控制、缓存、重试等诸多特性我们可以将接口请求函数用useQuery包装一下再传递给ApiComponent这样的话无论页面有多少个使用相同数据源的ApiComponent实例都只会发起一次远程请求。演示效果请参考 [Playground vue-query](https://www.vben.pro/#/demos/features/vue-query),具体代码请查看项目文件[concurrency-caching](https://github.com/vbenjs/vue-vben-admin/blob/main/playground/src/views/demos/features/vue-query/concurrency-caching.vue)
## API
### Props
@ -147,3 +151,10 @@ function fetchApi(): Promise<Record<string, any>> {
| options | 直接传入选项数据也作为api返回空数据时的后备数据 | `OptionsItem[]` | - |
| visibleEvent | 触发重新请求数据的事件名 | `string` | - |
| loadingSlot | 目标组件的插槽名称,用来显示一个"加载中"的图标 | `string` | - |
### Methods
| 方法 | 描述 | 类型 | 版本要求 |
| --- | --- | --- | --- |
| getComponentRef | 获取被包装的组件的实例 | ()=>T | >5.5.4 |
| updateParam | 设置接口请求参数将与params属性合并 | (newParams: Record<string, any>)=>void | >5.5.4 |

View File

@ -318,7 +318,7 @@ useVbenForm 返回的第二个参数,是一个对象,包含了一些表单
| collapsed | 是否折叠,在`showCollapseButton`为`true`时生效 | `boolean` | `false` |
| collapseTriggerResize | 折叠时,触发`resize`事件 | `boolean` | `false` |
| collapsedRows | 折叠时保持的行数 | `number` | `1` |
| fieldMappingTime | 用于将表单内的数组值映射成 2 个字段 | `[string, [string, string],Nullable<string>\|[string,string]\|((any,string)=>any)?][]` | - |
| fieldMappingTime | 用于将表单内的数组值映射成 2 个字段 | `[string, [string, string],Nullable<string>\|[string,string]\|((any,string)=>any)?][]` | - |
| commonConfig | 表单项的通用配置,每个配置都会传递到每个表单项,表单项可覆盖 | `FormCommonConfig` | - |
| schema | 表单项的每一项配置 | `FormSchema[]` | - |
| submitOnEnter | 按下回车健时提交表单 | `boolean` | false |

View File

@ -0,0 +1,31 @@
<script lang="ts" setup>
import { h } from 'vue';
import { alert, VbenButton } from '@vben/common-ui';
import { Empty } from 'ant-design-vue';
function showAlert() {
alert('This is an alert message');
}
function showIconAlert() {
alert({
content: 'This is an alert message with icon',
icon: 'success',
});
}
function showCustomAlert() {
alert({
content: h(Empty, { description: '什么都没有' }),
});
}
</script>
<template>
<div class="flex gap-4">
<VbenButton @click="showAlert">Alert</VbenButton>
<VbenButton @click="showIconAlert">Alert With Icon</VbenButton>
<VbenButton @click="showCustomAlert">Alert With Custom Content</VbenButton>
</div>
</template>

View File

@ -0,0 +1,42 @@
<script lang="ts" setup>
import { alert, confirm, VbenButton } from '@vben/common-ui';
function showConfirm() {
confirm('This is an alert message')
.then(() => {
alert('Confirmed');
})
.catch(() => {
alert('Canceled');
});
}
function showIconConfirm() {
confirm({
content: 'This is an alert message with icon',
icon: 'success',
});
}
function showAsyncConfirm() {
confirm({
beforeClose({ isConfirm }) {
if (isConfirm) {
// false
return new Promise((resolve) => setTimeout(resolve, 2000));
}
},
content: 'This is an alert message with async confirm',
icon: 'success',
}).then(() => {
alert('Confirmed');
});
}
</script>
<template>
<div class="flex gap-4">
<VbenButton @click="showConfirm">Confirm</VbenButton>
<VbenButton @click="showIconConfirm">Confirm With Icon</VbenButton>
<VbenButton @click="showAsyncConfirm">Async Confirm</VbenButton>
</div>
</template>

View File

@ -0,0 +1,41 @@
<script lang="ts" setup>
import { alert, prompt, VbenButton } from '@vben/common-ui';
import { VbenSelect } from '@vben-core/shadcn-ui';
function showPrompt() {
prompt({
content: '请输入一些东西',
})
.then((val) => {
alert(`已收到你的输入:${val}`);
})
.catch(() => {
alert('Canceled');
});
}
function showSelectPrompt() {
prompt({
component: VbenSelect,
componentProps: {
options: [
{ label: 'Option 1', value: 'option1' },
{ label: 'Option 2', value: 'option2' },
{ label: 'Option 3', value: 'option3' },
],
placeholder: '请选择',
},
content: 'This is an alert message with icon',
icon: 'question',
}).then((val) => {
alert(`你选择的是${val}`);
});
}
</script>
<template>
<div class="flex gap-4">
<VbenButton @click="showPrompt">Prompt</VbenButton>
<VbenButton @click="showSelectPrompt">Confirm With Select</VbenButton>
</div>
</template>

View File

@ -150,6 +150,73 @@ To run the `docs` application:
pnpm dev:docs
```
### Distinguishing Build Environments
In actual business development, multiple environments are usually distinguished during the build process, such as the test environment `test` and the production environment `build`.
At this point, you can modify three files and add corresponding script configurations to distinguish between production environments.
Take the addition of the test environment `test` to `@vben/web-antd` as an example:
- `apps\web-antd\package.json`
```json
"scripts": {
"build:prod": "pnpm vite build --mode production",
"build:test": "pnpm vite build --mode test",
"build:analyze": "pnpm vite build --mode analyze",
"dev": "pnpm vite --mode development",
"preview": "vite preview",
"typecheck": "vue-tsc --noEmit --skipLibCheck"
}
```
Add the command `"build:test"` and change the original `"build"` to `"build:prod"` to avoid building packages for two environments simultaneously.
- `package.json`
```json
"scripts": {
"build": "cross-env NODE_OPTIONS=--max-old-space-size=8192 turbo build",
"build:analyze": "turbo build:analyze",
"build:antd": "pnpm run build --filter=@vben/web-antd",
"build-test:antd": "pnpm run build --filter=@vben/web-antd build:test",
······
}
```
Add the command to build the test environment in the root directory `package.json`.
- `turbo.json`
```json
"tasks": {
"build": {
"dependsOn": ["^build"],
"outputs": [
"dist/**",
"dist.zip",
".vitepress/dist.zip",
".vitepress/dist/**"
]
},
"build-test:antd": {
"dependsOn": ["@vben/web-antd#build:test"],
"outputs": ["dist/**"]
},
"@vben/web-antd#build:test": {
"dependsOn": ["^build"],
"outputs": ["dist/**"]
},
······
```
Add the relevant dependent commands in `turbo.json`.
## Public Static Resources
If you need to use public static resources in the project, such as images, static HTML, etc., and you want to directly import them in the development process through `src="/xxx.png"`.

View File

@ -150,6 +150,73 @@ pnpm dev:ele
pnpm dev:docs
```
## 区分构建环境
在实际的业务开发中,通常会在构建时区分多种环境,如测试环境`test`、生产环境`build`等。
此时可以修改三个文件,在其中增加对应的脚本配置来达到区分生产环境的效果。
以`@vben/web-antd`添加测试环境`test`为例:
- `apps\web-antd\package.json`
```json
"scripts": {
"build:prod": "pnpm vite build --mode production",
"build:test": "pnpm vite build --mode test",
"build:analyze": "pnpm vite build --mode analyze",
"dev": "pnpm vite --mode development",
"preview": "vite preview",
"typecheck": "vue-tsc --noEmit --skipLibCheck"
},
```
增加命令`"build:test"`, 并将原`"build"`改为`"build:prod"`以避免同时构建两个环境的包。
- `package.json`
```json
"scripts": {
"build": "cross-env NODE_OPTIONS=--max-old-space-size=8192 turbo build",
"build:analyze": "turbo build:analyze",
"build:antd": "pnpm run build --filter=@vben/web-antd",
"build-test:antd": "pnpm run build --filter=@vben/web-antd build:test",
······
}
```
在根目录`package.json`中加入构建测试环境的命令
- `turbo.json`
```json
"tasks": {
"build": {
"dependsOn": ["^build"],
"outputs": [
"dist/**",
"dist.zip",
".vitepress/dist.zip",
".vitepress/dist/**"
]
},
"build-test:antd": {
"dependsOn": ["@vben/web-antd#build:test"],
"outputs": ["dist/**"]
},
"@vben/web-antd#build:test": {
"dependsOn": ["^build"],
"outputs": ["dist/**"]
},
······
```
在`turbo.json`中加入相关依赖的命令
## 公共静态资源
项目中需要使用到的公共静态资源图片、静态HTML等需要在开发中通过 `src="/xxx.png"` 直接引入的。

View File

@ -46,3 +46,47 @@ async function getVersionTag() {
}
}
```
## 替换为第三方库检查更新方式
如果需要通过其他方式检查更新例如使用其他版本控制方式chunkHash、version.json、使用`Web Worker`在后台轮询更新、自定义检查更新时机不使用轮询你可以通过JS库`version-polling`来实现。
```bash
pnpm add version-polling
```
以`apps/web-antd`项目为例,在项目入口文件`main.ts`或者`app.vue`添加以下代码
```ts
import { h } from 'vue';
import { Button, notification } from 'ant-design-vue';
import { createVersionPolling } from 'version-polling';
createVersionPolling({
silent: import.meta.env.MODE === 'development', // 开发环境下不检测
onUpdate: (self) => {
const key = `open${Date.now()}`;
notification.info({
message: '提示',
description: '检测到网页有更新, 是否刷新页面加载最新版本?',
btn: () =>
h(
Button,
{
type: 'primary',
size: 'small',
onClick: () => {
notification.close(key);
self.onRefresh();
},
},
{ default: () => '刷新' },
),
key,
duration: null,
placement: 'bottomRight',
});
},
});
```

View File

@ -99,7 +99,7 @@
"node": ">=20.10.0",
"pnpm": ">=9.12.0"
},
"packageManager": "pnpm@9.15.7",
"packageManager": "pnpm@9.15.9",
"pnpm": {
"peerDependencyRules": {
"allowedVersions": {

View File

@ -15,8 +15,10 @@ export {
ChevronsLeft,
ChevronsRight,
Circle,
CircleAlert,
CircleCheckBig,
CircleHelp,
CircleX,
Copy,
CornerDownLeft,
Ellipsis,

View File

@ -6,6 +6,7 @@ export const messages: Record<Locale, Record<string, string>> = {
collapse: 'Collapse',
confirm: 'Confirm',
expand: 'Expand',
prompt: 'Prompt',
reset: 'Reset',
submit: 'Submit',
},
@ -14,6 +15,7 @@ export const messages: Record<Locale, Record<string, string>> = {
collapse: '收起',
confirm: '确认',
expand: '展开',
prompt: '提示',
reset: '重置',
submit: '提交',
},

View File

@ -0,0 +1,207 @@
import type { Component } from 'vue';
import type { Recordable } from '@vben-core/typings';
import type { AlertProps, BeforeCloseScope } from './alert';
import { h, ref, render } from 'vue';
import { useSimpleLocale } from '@vben-core/composables';
import { Input } from '@vben-core/shadcn-ui';
import { isFunction, isString } from '@vben-core/shared/utils';
import Alert from './alert.vue';
const alerts = ref<Array<{ container: HTMLElement; instance: Component }>>([]);
const { $t } = useSimpleLocale();
export function vbenAlert(options: AlertProps): Promise<void>;
export function vbenAlert(
message: string,
options?: Partial<AlertProps>,
): Promise<void>;
export function vbenAlert(
message: string,
title?: string,
options?: Partial<AlertProps>,
): Promise<void>;
export function vbenAlert(
arg0: AlertProps | string,
arg1?: Partial<AlertProps> | string,
arg2?: Partial<AlertProps>,
): Promise<void> {
return new Promise((resolve, reject) => {
const options: AlertProps = isString(arg0)
? {
content: arg0,
}
: { ...arg0 };
if (arg1) {
if (isString(arg1)) {
options.title = arg1;
} else if (!isString(arg1)) {
// 如果第二个参数是对象,则合并到选项中
Object.assign(options, arg1);
}
}
if (arg2 && !isString(arg2)) {
Object.assign(options, arg2);
}
// 创建容器元素
const container = document.createElement('div');
document.body.append(container);
// 创建一个引用,用于在回调中访问实例
const alertRef = { container, instance: null as any };
const props: AlertProps & Recordable<any> = {
onClosed: (isConfirm: boolean) => {
// 移除组件实例以及创建的所有dom恢复页面到打开前的状态
// 从alerts数组中移除该实例
alerts.value = alerts.value.filter((item) => item !== alertRef);
// 从DOM中移除容器
render(null, container);
if (container.parentNode) {
container.remove();
}
// 解析 Promise传递用户操作结果
if (isConfirm) {
resolve();
} else {
reject(new Error('dialog cancelled'));
}
},
...options,
open: true,
title: options.title ?? $t.value('prompt'),
};
// 创建Alert组件的VNode
const vnode = h(Alert, props);
// 渲染组件到容器
render(vnode, container);
// 保存组件实例引用
alertRef.instance = vnode.component?.proxy as Component;
// 将实例和容器添加到alerts数组中
alerts.value.push(alertRef);
});
}
export function vbenConfirm(options: AlertProps): Promise<void>;
export function vbenConfirm(
message: string,
options?: Partial<AlertProps>,
): Promise<void>;
export function vbenConfirm(
message: string,
title?: string,
options?: Partial<AlertProps>,
): Promise<void>;
export function vbenConfirm(
arg0: AlertProps | string,
arg1?: Partial<AlertProps> | string,
arg2?: Partial<AlertProps>,
): Promise<void> {
const defaultProps: Partial<AlertProps> = {
showCancel: true,
};
if (!arg1) {
return isString(arg0)
? vbenAlert(arg0, defaultProps)
: vbenAlert({ ...defaultProps, ...arg0 });
} else if (!arg2) {
return isString(arg1)
? vbenAlert(arg0 as string, arg1, defaultProps)
: vbenAlert(arg0 as string, { ...defaultProps, ...arg1 });
}
return vbenAlert(arg0 as string, arg1 as string, {
...defaultProps,
...arg2,
});
}
export async function vbenPrompt<T = any>(
options: Omit<AlertProps, 'beforeClose'> & {
beforeClose?: (scope: {
isConfirm: boolean;
value: T | undefined;
}) => boolean | Promise<boolean | undefined> | undefined;
component?: Component;
componentProps?: Recordable<any>;
defaultValue?: T;
modelPropName?: string;
},
): Promise<T | undefined> {
const {
component: _component,
componentProps: _componentProps,
content,
defaultValue,
modelPropName: _modelPropName,
...delegated
} = options;
const contents: Component[] = [];
const modelValue = ref<T | undefined>(defaultValue);
if (isString(content)) {
contents.push(h('span', content));
} else {
contents.push(content);
}
const componentProps = _componentProps || {};
const modelPropName = _modelPropName || 'modelValue';
componentProps[modelPropName] = modelValue.value;
componentProps[`onUpdate:${modelPropName}`] = (val: any) => {
modelValue.value = val;
};
const componentRef = h(_component || Input, componentProps);
contents.push(componentRef);
const props: AlertProps & Recordable<any> = {
...delegated,
async beforeClose(scope: BeforeCloseScope) {
if (delegated.beforeClose) {
return await delegated.beforeClose({
...scope,
value: modelValue.value,
});
}
},
content: h(
'div',
{ class: 'flex flex-col gap-2' },
{ default: () => contents },
),
onOpened() {
// 组件挂载完成后,自动聚焦到输入组件
if (
componentRef.component?.exposed &&
isFunction(componentRef.component.exposed.focus)
) {
componentRef.component.exposed.focus();
} else if (componentRef.el && isFunction(componentRef.el.focus)) {
componentRef.el.focus();
}
},
};
await vbenConfirm(props);
return modelValue.value;
}
export function clearAllAlerts() {
alerts.value.forEach((alert) => {
// 从DOM中移除容器
render(null, alert.container);
if (alert.container.parentNode) {
alert.container.remove();
}
});
alerts.value = [];
}

View File

@ -0,0 +1,34 @@
import type { Component } from 'vue';
export type IconType = 'error' | 'info' | 'question' | 'success' | 'warning';
export type BeforeCloseScope = {
isConfirm: boolean;
};
export type AlertProps = {
/** 关闭前的回调如果返回false则终止关闭 */
beforeClose?: (
scope: BeforeCloseScope,
) => boolean | Promise<boolean | undefined> | undefined;
/** 边框 */
bordered?: boolean;
/** 取消按钮的标题 */
cancelText?: string;
/** 是否居中显示 */
centered?: boolean;
/** 确认按钮的标题 */
confirmText?: string;
/** 弹窗容器的额外样式 */
containerClass?: string;
/** 弹窗提示内容 */
content: Component | string;
/** 弹窗内容的额外样式 */
contentClass?: string;
/** 弹窗的图标(在标题的前面) */
icon?: Component | IconType;
/** 是否显示取消按钮 */
showCancel?: boolean;
/** 弹窗标题 */
title?: string;
};

View File

@ -0,0 +1,182 @@
<script lang="ts" setup>
import type { Component } from 'vue';
import type { AlertProps } from './alert';
import { computed, h, nextTick, ref, watch } from 'vue';
import { useSimpleLocale } from '@vben-core/composables';
import {
CircleAlert,
CircleCheckBig,
CircleHelp,
CircleX,
Info,
X,
} from '@vben-core/icons';
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogTitle,
VbenButton,
VbenLoading,
VbenRenderContent,
} from '@vben-core/shadcn-ui';
import { globalShareState } from '@vben-core/shared/global-state';
import { cn } from '@vben-core/shared/utils';
const props = withDefaults(defineProps<AlertProps>(), {
bordered: true,
centered: true,
containerClass: 'w-[520px]',
});
const emits = defineEmits(['closed', 'confirm', 'opened']);
const open = defineModel<boolean>('open', { default: false });
const { $t } = useSimpleLocale();
const components = globalShareState.getComponents();
const isConfirm = ref(false);
watch(open, async (val) => {
await nextTick();
if (val) {
isConfirm.value = false;
} else {
emits('closed', isConfirm.value);
}
});
const getIconRender = computed(() => {
let iconRender: Component | null = null;
if (props.icon) {
if (typeof props.icon === 'string') {
switch (props.icon) {
case 'error': {
iconRender = h(CircleX, {
style: { color: 'hsl(var(--destructive))' },
});
break;
}
case 'info': {
iconRender = h(Info, { style: { color: 'hsl(var(--info))' } });
break;
}
case 'question': {
iconRender = CircleHelp;
break;
}
case 'success': {
iconRender = h(CircleCheckBig, {
style: { color: 'hsl(var(--success))' },
});
break;
}
case 'warning': {
iconRender = h(CircleAlert, {
style: { color: 'hsl(var(--warning))' },
});
break;
}
default: {
iconRender = null;
break;
}
}
}
} else {
iconRender = props.icon ?? null;
}
return iconRender;
});
function handleConfirm() {
isConfirm.value = true;
emits('confirm');
}
function handleCancel() {
isConfirm.value = false;
}
const loading = ref(false);
async function handleOpenChange(val: boolean) {
if (!val && props.beforeClose) {
loading.value = true;
try {
const res = await props.beforeClose({ isConfirm: isConfirm.value });
if (res !== false) {
open.value = false;
}
} finally {
loading.value = false;
}
} else {
open.value = val;
}
}
</script>
<template>
<AlertDialog :open="open" @update:open="handleOpenChange">
<AlertDialogContent
:open="open"
:centered="centered"
@opened="emits('opened')"
:class="
cn(
containerClass,
'left-0 right-0 top-[10vh] mx-auto flex max-h-[80%] flex-col p-0 duration-300 sm:rounded-[var(--radius)] md:w-[520px] md:max-w-[80%]',
{
'border-border border': bordered,
'shadow-3xl': !bordered,
'top-1/2 !-translate-y-1/2': centered,
},
)
"
>
<div :class="cn('relative flex-1 overflow-y-auto p-3', contentClass)">
<AlertDialogTitle v-if="title">
<div class="flex items-center">
<component :is="getIconRender" class="mr-2" />
<span class="flex-auto">{{ $t(title) }}</span>
<AlertDialogCancel v-if="showCancel">
<VbenButton
variant="ghost"
size="icon"
class="rounded-full"
:disabled="loading"
@click="handleCancel"
>
<X class="text-muted-foreground size-4" />
</VbenButton>
</AlertDialogCancel>
</div>
</AlertDialogTitle>
<AlertDialogDescription>
<div class="m-4 mb-6 min-h-[30px]">
<VbenRenderContent :content="content" render-br />
</div>
<VbenLoading v-if="loading" :spinning="loading" />
</AlertDialogDescription>
<div class="flex justify-end gap-x-2">
<AlertDialogCancel v-if="showCancel" :disabled="loading">
<component
:is="components.DefaultButton || VbenButton"
variant="ghost"
@click="handleCancel"
>
{{ cancelText || $t('cancel') }}
</component>
</AlertDialogCancel>
<AlertDialogAction>
<component
:is="components.PrimaryButton || VbenButton"
:loading="loading"
@click="handleConfirm"
>
{{ confirmText || $t('confirm') }}
</component>
</AlertDialogAction>
</div>
</div>
</AlertDialogContent>
</AlertDialog>
</template>

View File

@ -0,0 +1,9 @@
export * from './alert';
export { default as Alert } from './alert.vue';
export {
vbenAlert as alert,
clearAllAlerts,
vbenConfirm as confirm,
vbenPrompt as prompt,
} from './AlertBuilder';

View File

@ -1,2 +1,3 @@
export * from './alert';
export * from './drawer';
export * from './modal';

View File

@ -3,7 +3,7 @@ import type { Component, PropType } from 'vue';
import { defineComponent, h } from 'vue';
import { isFunction, isObject } from '@vben-core/shared/utils';
import { isFunction, isObject, isString } from '@vben-core/shared/utils';
export default defineComponent({
name: 'RenderContent',
@ -14,6 +14,10 @@ export default defineComponent({
| undefined,
type: [Object, String, Function],
},
renderBr: {
default: false,
type: Boolean,
},
},
setup(props, { attrs, slots }) {
return () => {
@ -24,7 +28,20 @@ export default defineComponent({
(isObject(props.content) || isFunction(props.content)) &&
props.content !== null;
if (!isComponent) {
return props.content;
if (props.renderBr && isString(props.content)) {
const lines = props.content.split('\n');
const result = [];
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
result.push(h('span', { key: i }, line));
if (i < lines.length - 1) {
result.push(h('br'));
}
}
return result;
} else {
return props.content;
}
}
return h(props.content as never, {
...attrs,

View File

@ -0,0 +1,16 @@
<script setup lang="ts">
import type { AlertDialogEmits, AlertDialogProps } from 'radix-vue';
import { AlertDialogRoot, useForwardPropsEmits } from 'radix-vue';
const props = defineProps<AlertDialogProps>();
const emits = defineEmits<AlertDialogEmits>();
const forwarded = useForwardPropsEmits(props, emits);
</script>
<template>
<AlertDialogRoot v-bind="forwarded">
<slot></slot>
</AlertDialogRoot>
</template>

View File

@ -0,0 +1,13 @@
<script setup lang="ts">
import type { AlertDialogActionProps } from 'radix-vue';
import { AlertDialogAction } from 'radix-vue';
const props = defineProps<AlertDialogActionProps>();
</script>
<template>
<AlertDialogAction v-bind="props">
<slot></slot>
</AlertDialogAction>
</template>

View File

@ -0,0 +1,13 @@
<script setup lang="ts">
import type { AlertDialogCancelProps } from 'radix-vue';
import { AlertDialogCancel } from 'radix-vue';
const props = defineProps<AlertDialogCancelProps>();
</script>
<template>
<AlertDialogCancel v-bind="props">
<slot></slot>
</AlertDialogCancel>
</template>

View File

@ -0,0 +1,91 @@
<script setup lang="ts">
import type {
AlertDialogContentEmits,
AlertDialogContentProps,
} from 'radix-vue';
import type { ClassType } from '@vben-core/typings';
import { computed, ref } from 'vue';
import { cn } from '@vben-core/shared/utils';
import {
AlertDialogContent,
AlertDialogPortal,
useForwardPropsEmits,
} from 'radix-vue';
import AlertDialogOverlay from './AlertDialogOverlay.vue';
const props = withDefaults(
defineProps<
AlertDialogContentProps & {
centered?: boolean;
class?: ClassType;
modal?: boolean;
open?: boolean;
overlayBlur?: number;
zIndex?: number;
}
>(),
{ modal: true },
);
const emits = defineEmits<
AlertDialogContentEmits & { close: []; closed: []; opened: [] }
>();
const delegatedProps = computed(() => {
const { class: _, modal: _modal, open: _open, ...delegated } = props;
return delegated;
});
const forwarded = useForwardPropsEmits(delegatedProps, emits);
const contentRef = ref<InstanceType<typeof AlertDialogContent> | null>(null);
function onAnimationEnd(event: AnimationEvent) {
// contentRef opened/closed
if (event.target === contentRef.value?.$el) {
if (props.open) {
emits('opened');
} else {
emits('closed');
}
}
}
defineExpose({
getContentRef: () => contentRef.value,
});
</script>
<template>
<AlertDialogPortal>
<Transition name="fade">
<AlertDialogOverlay
v-if="open && modal"
:style="{
...(zIndex ? { zIndex } : {}),
position: 'fixed',
backdropFilter:
overlayBlur && overlayBlur > 0 ? `blur(${overlayBlur}px)` : 'none',
}"
@click="() => emits('close')"
/>
</Transition>
<AlertDialogContent
ref="contentRef"
:style="{ ...(zIndex ? { zIndex } : {}), position: 'fixed' }"
@animationend="onAnimationEnd"
v-bind="forwarded"
:class="
cn(
'z-popup bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-top-[48%] w-full p-6 shadow-lg outline-none sm:rounded-xl',
props.class,
)
"
>
<slot></slot>
</AlertDialogContent>
</AlertDialogPortal>
</template>

View File

@ -0,0 +1,28 @@
<script lang="ts" setup>
import type { AlertDialogDescriptionProps } from 'radix-vue';
import { computed } from 'vue';
import { cn } from '@vben-core/shared/utils';
import { AlertDialogDescription, useForwardProps } from 'radix-vue';
const props = defineProps<AlertDialogDescriptionProps & { class?: any }>();
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props;
return delegated;
});
const forwardedProps = useForwardProps(delegatedProps);
</script>
<template>
<AlertDialogDescription
v-bind="forwardedProps"
:class="cn('text-muted-foreground text-sm', props.class)"
>
<slot></slot>
</AlertDialogDescription>
</template>

View File

@ -0,0 +1,8 @@
<script setup lang="ts">
import { useScrollLock } from '@vben-core/composables';
useScrollLock();
</script>
<template>
<div class="bg-overlay z-popup inset-0"></div>
</template>

View File

@ -0,0 +1,30 @@
<script setup lang="ts">
import type { AlertDialogTitleProps } from 'radix-vue';
import { computed } from 'vue';
import { cn } from '@vben-core/shared/utils';
import { AlertDialogTitle, useForwardProps } from 'radix-vue';
const props = defineProps<AlertDialogTitleProps & { class?: any }>();
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props;
return delegated;
});
const forwardedProps = useForwardProps(delegatedProps);
</script>
<template>
<AlertDialogTitle
v-bind="forwardedProps"
:class="
cn('text-lg font-semibold leading-none tracking-tight', props.class)
"
>
<slot></slot>
</AlertDialogTitle>
</template>

View File

@ -0,0 +1,6 @@
export { default as AlertDialog } from './AlertDialog.vue';
export { default as AlertDialogAction } from './AlertDialogAction.vue';
export { default as AlertDialogCancel } from './AlertDialogCancel.vue';
export { default as AlertDialogContent } from './AlertDialogContent.vue';
export { default as AlertDialogDescription } from './AlertDialogDescription.vue';
export { default as AlertDialogTitle } from './AlertDialogTitle.vue';

View File

@ -1,4 +1,5 @@
export * from './accordion';
export * from './alert-dialog';
export * from './avatar';
export * from './badge';
export * from './breadcrumb';

View File

@ -102,3 +102,11 @@
.vxe-tools--operate:not(:has(button)) {
margin-left: 0;
}
.vxe-grid--layout-header-wrapper {
overflow: visible;
}
.vxe-grid--layout-body-content-wrapper {
overflow: hidden;
}

View File

@ -34,7 +34,7 @@ async function generateRoutesByBackend(
return [...options.routes, ...routes];
} catch (error) {
console.error(error);
return [];
throw error;
}
}

View File

@ -0,0 +1,61 @@
<script lang="ts" setup>
import type { Recordable } from '@vben/types';
import { useQuery } from '@tanstack/vue-query';
import { useVbenForm } from '#/adapter/form';
import { getMenuList } from '#/api';
const queryKey = ['demo', 'api', 'options'];
const count = 4;
const { dataUpdatedAt, promise: fetchDataFn } = useQuery({
//
experimental_prefetchInRender: true,
//
queryFn: getMenuList,
queryKey,
// always
refetchOnMount: 'always',
//
staleTime: 1000 * 60 * 5,
});
async function fetchOptions() {
return await fetchDataFn.value;
}
const schema = [];
for (let i = 0; i < count; i++) {
schema.push({
component: 'ApiSelect',
componentProps: {
api: fetchOptions,
class: 'w-full',
filterOption: (input: string, option: Recordable<any>) => {
return option.label.toLowerCase().includes(input.toLowerCase());
},
labelField: 'name',
showSearch: true,
valueField: 'id',
},
fieldName: `field${i}`,
label: `Select ${i}`,
});
}
const [Form] = useVbenForm({
schema,
showDefaultActions: false,
});
</script>
<template>
<div>
<div class="mb-2 flex gap-2">
<div>以下{{ count }}个组件共用一个数据源</div>
<div>缓存更新时间{{ new Date(dataUpdatedAt).toLocaleString() }}</div>
</div>
<Form />
</div>
</template>

View File

@ -1,11 +1,15 @@
<script setup lang="ts">
import { Page } from '@vben/common-ui';
import { Card } from 'ant-design-vue';
import { refAutoReset } from '@vueuse/core';
import { Button, Card, Empty } from 'ant-design-vue';
import ConcurrencyCaching from './concurrency-caching.vue';
import InfiniteQueries from './infinite-queries.vue';
import PaginatedQueries from './paginated-queries.vue';
import QueryRetries from './query-retries.vue';
const showCaching = refAutoReset(true, 1000);
</script>
<template>
@ -20,6 +24,17 @@ import QueryRetries from './query-retries.vue';
<Card title="错误重试">
<QueryRetries />
</Card>
<Card
title="并发和缓存"
v-spinning="!showCaching"
:body-style="{ minHeight: '330px' }"
>
<template #extra>
<Button @click="showCaching = false">重新加载</Button>
</template>
<ConcurrencyCaching v-if="showCaching" />
<Empty v-else description="正在加载..." />
</Card>
</div>
</Page>
</template>

View File

@ -1,7 +1,16 @@
<script lang="ts" setup>
import { Page, useVbenModal } from '@vben/common-ui';
import { onBeforeUnmount } from 'vue';
import { Button, Card, Flex } from 'ant-design-vue';
import {
alert,
clearAllAlerts,
confirm,
Page,
prompt,
useVbenModal,
} from '@vben/common-ui';
import { Button, Card, Flex, message } from 'ant-design-vue';
import DocButton from '../doc-button.vue';
import AutoHeightDemo from './auto-height-demo.vue';
@ -103,6 +112,62 @@ function openFormModal() {
})
.open();
}
function openAlert() {
alert({
content: '这是一个弹窗',
icon: 'success',
}).then(() => {
message.info('用户关闭了弹窗');
});
}
onBeforeUnmount(() => {
//
clearAllAlerts();
});
function openConfirm() {
confirm({
beforeClose({ isConfirm }) {
if (!isConfirm) return;
//
return new Promise((resolve) => {
setTimeout(() => {
resolve(true);
}, 1000);
});
},
content: '这是一个确认弹窗',
icon: 'question',
})
.then(() => {
message.success('用户确认了操作');
})
.catch(() => {
message.error('用户取消了操作');
});
}
async function openPrompt() {
prompt<string>({
async beforeClose({ isConfirm, value }) {
if (isConfirm && value === '芝士') {
message.error('不能吃芝士');
return false;
}
},
componentProps: { placeholder: '不能吃芝士...' },
content: '中午吃了什么?',
icon: 'question',
})
.then((res) => {
message.success(`用户输入了:${res}`);
})
.catch(() => {
message.error('用户取消了输入');
});
}
</script>
<template>
@ -195,6 +260,14 @@ function openFormModal() {
<Button type="primary" @click="openBlurModal"></Button>
</template>
</Card>
<Card class="w-[300px]" title="轻量提示弹窗">
<p>通过快捷方法创建动态提示弹窗适合一些轻量的提示和确认输入等</p>
<template #actions>
<Button type="primary" @click="openAlert">Alert</Button>
<Button type="primary" @click="openConfirm">Confirm</Button>
<Button type="primary" @click="openPrompt">Prompt</Button>
</template>
</Card>
</Flex>
</Page>
</template>

File diff suppressed because it is too large Load Diff

View File

@ -21,22 +21,22 @@ catalog:
'@commitlint/cli': ^19.8.0
'@commitlint/config-conventional': ^19.8.0
'@ctrl/tinycolor': ^4.1.0
'@eslint/js': ^9.22.0
'@eslint/js': ^9.23.0
'@faker-js/faker': ^9.6.0
'@iconify/json': ^2.2.314
'@iconify/json': ^2.2.323
'@iconify/tailwind': ^1.2.0
'@iconify/vue': ^4.3.0
'@intlify/core-base': ^11.1.2
'@intlify/unplugin-vue-i18n': ^6.0.3
'@intlify/unplugin-vue-i18n': ^6.0.5
'@jspm/generator': ^2.5.1
'@manypkg/get-packages': ^2.2.2
'@nolebase/vitepress-plugin-git-changelog': ^2.15.0
'@playwright/test': ^1.51.0
'@pnpm/workspace.read-manifest': ^1000.1.1
'@nolebase/vitepress-plugin-git-changelog': ^2.15.1
'@playwright/test': ^1.51.1
'@pnpm/workspace.read-manifest': ^1000.1.2
'@stylistic/stylelint-plugin': ^3.1.2
'@tailwindcss/nesting': 0.0.0-insiders.565cd3e
'@tailwindcss/typography': ^0.5.16
'@tanstack/vue-query': ^5.67.2
'@tanstack/vue-query': ^5.71.1
'@tanstack/vue-store': ^0.7.0
'@types/archiver': ^6.0.3
'@types/eslint': ^9.6.1
@ -45,20 +45,20 @@ catalog:
'@types/lodash.clonedeep': ^4.5.9
'@types/lodash.get': ^4.4.9
'@types/lodash.isequal': ^4.5.8
'@types/lodash.set': ^4.3.2
'@types/node': ^22.13.10
'@types/lodash.set': ^4.3.9
'@types/node': ^22.13.17
'@types/nprogress': ^0.2.3
'@types/postcss-import': ^14.0.3
'@types/qrcode': ^1.5.5
'@types/qs': ^6.9.18
'@types/sortablejs': ^1.15.8
'@types/crypto-js': ^4.2.2
'@typescript-eslint/eslint-plugin': ^8.26.0
'@typescript-eslint/parser': ^8.26.0
'@typescript-eslint/eslint-plugin': ^8.29.0
'@typescript-eslint/parser': ^8.29.0
'@vee-validate/zod': ^4.15.0
'@vite-pwa/vitepress': ^0.5.3
'@vitejs/plugin-vue': ^5.2.1
'@vitejs/plugin-vue-jsx': ^4.1.1
'@vite-pwa/vitepress': ^0.5.4
'@vitejs/plugin-vue': ^5.2.3
'@vitejs/plugin-vue-jsx': ^4.1.2
'@vue/reactivity': ^3.5.13
'@vue/shared': ^3.5.13
'@vue/test-utils': ^2.4.6
@ -67,8 +67,8 @@ catalog:
'@vueuse/integrations': ^12.8.2
ant-design-vue: ^4.2.6
archiver: ^7.0.1
autoprefixer: ^10.4.20
axios: ^1.8.2
autoprefixer: ^10.4.21
axios: ^1.8.4
axios-mock-adapter: ^2.1.0
cac: ^6.7.14
chalk: ^5.4.1
@ -77,10 +77,10 @@ catalog:
class-variance-authority: ^0.7.1
clsx: ^2.1.1
commitlint-plugin-function-rules: ^4.0.1
consola: ^3.4.0
consola: ^3.4.2
cross-env: ^7.0.3
crypto-js: ^4.2.0
cspell: ^8.17.5
cspell: ^8.18.1
cssnano: ^7.0.6
cz-git: ^1.11.1
czg: ^1.11.1
@ -89,18 +89,18 @@ catalog:
depcheck: ^1.4.7
dotenv: ^16.4.7
echarts: ^5.6.0
element-plus: ^2.9.6
eslint: ^9.22.0
element-plus: ^2.9.7
eslint: ^9.23.0
eslint-config-turbo: ^2.4.4
eslint-plugin-command: ^0.2.7
eslint-plugin-eslint-comments: ^3.2.0
eslint-plugin-import-x: ^4.6.1
eslint-plugin-jsdoc: ^50.6.3
eslint-plugin-jsonc: ^2.19.1
eslint-plugin-n: ^17.16.2
eslint-plugin-import-x: ^4.10.0
eslint-plugin-jsdoc: ^50.6.9
eslint-plugin-jsonc: ^2.20.0
eslint-plugin-n: ^17.17.0
eslint-plugin-no-only-tests: ^3.3.0
eslint-plugin-perfectionist: ^4.10.0
eslint-plugin-prettier: ^5.2.3
eslint-plugin-perfectionist: ^4.11.0
eslint-plugin-prettier: ^5.2.5
eslint-plugin-regexp: ^2.7.0
eslint-plugin-unicorn: ^56.0.1
eslint-plugin-unused-imports: ^4.1.4
@ -117,7 +117,7 @@ catalog:
is-ci: ^4.1.0
jsonc-eslint-parser: ^2.4.0
jsonwebtoken: ^9.0.2
lint-staged: ^15.4.3
lint-staged: ^15.5.0
lodash.clonedeep: ^4.5.0
lodash.get: ^4.4.2
lodash.set: ^4.3.2
@ -125,13 +125,13 @@ catalog:
lucide-vue-next: ^0.469.0
medium-zoom: ^1.1.0
naive-ui: ^2.41.0
nitropack: ^2.11.6
nitropack: ^2.11.8
nprogress: ^0.2.0
ora: ^8.2.0
pinia: ^2.3.1
pinia-plugin-persistedstate: ^4.2.0
pkg-types: ^1.3.1
playwright: ^1.51.0
playwright: ^1.51.1
postcss: ^8.5.3
postcss-antd-fixes: ^0.2.0
postcss-html: ^1.8.0
@ -146,11 +146,11 @@ catalog:
radix-vue: ^1.9.17
resolve.exports: ^2.0.3
rimraf: ^6.0.1
rollup: ^4.35.0
rollup: ^4.39.0
rollup-plugin-visualizer: ^5.14.0
sass: ^1.85.1
sass: ^1.86.1
sortablejs: ^1.15.6
stylelint: ^16.15.0
stylelint: ^16.17.0
stylelint-config-recess-order: ^5.1.1
stylelint-config-recommended: ^14.0.1
stylelint-config-recommended-scss: ^14.1.0
@ -165,29 +165,29 @@ catalog:
theme-colors: ^0.1.0
tippy.js: ^6.2.5
turbo: ^2.4.4
typescript: ^5.7.3
typescript: ^5.8.2
unbuild: ^3.5.0
unplugin-element-plus: ^0.9.1
vee-validate: ^4.15.0
vite: ^6.2.1
vite: ^6.2.4
vite-plugin-compression: ^0.5.1
vite-plugin-dts: ^4.5.3
vite-plugin-html: ^3.2.2
vite-plugin-lazy-import: ^1.0.7
vite-plugin-pwa: ^0.21.1
vite-plugin-pwa: ^0.21.2
vite-plugin-vue-devtools: ^7.7.2
vitepress: ^1.6.3
vitepress-plugin-group-icons: ^1.3.6
vitepress-plugin-group-icons: ^1.3.8
vitest: ^2.1.9
vue: ^3.5.13
vue-eslint-parser: ^9.4.3
vue-i18n: ^11.1.2
vue-json-viewer: ^3.0.4
vue-router: ^4.5.0
vue-tippy: ^6.6.0
vue-tippy: ^6.7.0
vue-tsc: 2.1.10
vxe-pc-ui: ^4.4.8
vxe-table: 4.10.0
vxe-pc-ui: ^4.5.11
vxe-table: ^4.12.5
watermark-js-plus: ^1.5.8
zod: ^3.24.2
zod-defaults: ^0.1.3