diff --git a/apps/web-ele/src/adapter/component/index.ts b/apps/web-ele/src/adapter/component/index.ts index e2f533cf..470caaa7 100644 --- a/apps/web-ele/src/adapter/component/index.ts +++ b/apps/web-ele/src/adapter/component/index.ts @@ -21,6 +21,8 @@ import { $t } from '@vben/locales'; import { ElNotification } from 'element-plus'; +import { FileUpload, ImageUpload } from '#/components/upload'; + const ElButton = defineAsyncComponent(() => Promise.all([ import('element-plus/es/components/button/index'), @@ -167,7 +169,9 @@ export type ComponentType = | 'CheckboxGroup' | 'DatePicker' | 'Divider' + | 'FileUpload' | 'IconPicker' + | 'ImageUpload' | 'Input' | 'InputNumber' | 'RadioGroup' @@ -315,6 +319,8 @@ async function initComponentAdapter() { }, TreeSelect: withDefaultPlaceholder(ElTreeSelect, 'select'), Upload: ElUpload, + FileUpload, + ImageUpload, }; // 将组件注册到全局共享状态中 diff --git a/apps/web-ele/src/adapter/vxe-table.ts b/apps/web-ele/src/adapter/vxe-table.ts index 44b31eae..82239f04 100644 --- a/apps/web-ele/src/adapter/vxe-table.ts +++ b/apps/web-ele/src/adapter/vxe-table.ts @@ -1,8 +1,20 @@ +import type { Recordable } from '@vben/types'; + import { h } from 'vue'; -import { setupVbenVxeTable, useVbenVxeGrid } from '@vben/plugins/vxe-table'; +import { IconifyIcon } from '@vben/icons'; +import { $te } from '@vben/locales'; +import { + AsyncComponents, + setupVbenVxeTable, + useVbenVxeGrid, +} from '@vben/plugins/vxe-table'; +import { isFunction, isString } from '@vben/utils'; -import { ElButton, ElImage } from 'element-plus'; +import { ElButton, ElImage, ElPopconfirm, ElSwitch } from 'element-plus'; + +import { DictTag } from '#/components/dict-tag'; +import { $t } from '#/locales'; import { useVbenForm } from './form'; @@ -20,6 +32,17 @@ setupVbenVxeTable({ // 全局禁用vxe-table的表单配置,使用formOptions enabled: false, }, + toolbarConfig: { + import: false, // 是否导入 + export: false, // 是否导出 + refresh: true, // 是否刷新 + print: false, // 是否打印 + zoom: true, // 是否缩放 + custom: true, // 是否自定义配置 + }, + customConfig: { + mode: 'modal', + }, proxyConfig: { autoLoad: true, response: { @@ -30,6 +53,12 @@ setupVbenVxeTable({ showActiveMsg: true, showResponseMsg: false, }, + pagerConfig: { + enabled: true, + }, + sortConfig: { + multiple: true, + }, round: true, showOverflow: true, size: 'small', @@ -57,12 +86,209 @@ setupVbenVxeTable({ }, }); - // 这里可以自行扩展 vxe-table 的全局配置,比如自定义格式化 - // vxeUI.formats.add + // 表格配置项可以用 cellRender: { name: 'CellDict', props:{dictType: ''} }, + vxeUI.renderer.add('CellDict', { + renderTableDefault(renderOpts, params) { + const { props } = renderOpts; + const { column, row } = params; + if (!props) { + return ''; + } + // 使用 DictTag 组件替代原来的实现 + return h(DictTag, { + type: props.type, + value: row[column.field]?.toString(), + }); + }, + }); + + // 表格配置项可以用 cellRender: { name: 'CellSwitch', props: { beforeChange: () => {} } }, + vxeUI.renderer.add('CellSwitch', { + renderTableDefault({ attrs, props }, { column, row }) { + const loadingKey = `__loading_${column.field}`; + const finallyProps = { + activeText: $t('common.enabled'), + inactiveText: $t('common.disabled'), + activeValue: 1, + inactiveValue: 0, + ...props, + modelValue: row[column.field], + loading: row[loadingKey] ?? false, + 'onUpdate:modelValue': onChange, + }; + + async function onChange(newVal: any) { + row[loadingKey] = true; + try { + const result = await attrs?.beforeChange?.(newVal, row); + if (result !== false) { + row[column.field] = newVal; + } + } finally { + row[loadingKey] = false; + } + } + + return h(ElSwitch, finallyProps); + }, + }); + + // 注册表格的操作按钮渲染器 cellRender: { name: 'CellOperation', options: ['edit', 'delete'] } + vxeUI.renderer.add('CellOperation', { + renderTableDefault({ attrs, options, props }, { column, row }) { + const defaultProps = { size: 'small', type: 'primary', ...props }; + let align = 'end'; + switch (column.align) { + case 'center': { + align = 'center'; + break; + } + case 'left': { + align = 'start'; + break; + } + default: { + align = 'end'; + break; + } + } + const presets: Recordable> = { + delete: { + type: 'danger', + text: $t('common.delete'), + }, + edit: { + text: $t('common.edit'), + }, + }; + const operations: Array> = ( + options || ['edit', 'delete'] + ) + .map((opt) => { + if (isString(opt)) { + return presets[opt] + ? { code: opt, ...presets[opt], ...defaultProps } + : { + code: opt, + text: $te(`common.${opt}`) ? $t(`common.${opt}`) : opt, + ...defaultProps, + }; + } else { + return { ...defaultProps, ...presets[opt.code], ...opt }; + } + }) + .map((opt) => { + const optBtn: Recordable = {}; + Object.keys(opt).forEach((key) => { + optBtn[key] = isFunction(opt[key]) ? opt[key](row) : opt[key]; + }); + return optBtn; + }) + .filter((opt) => opt.show !== false); + + function renderBtn(opt: Recordable, listen = true) { + return h( + ElButton, + { + ...props, + ...opt, + icon: undefined, + onClick: listen + ? () => + attrs?.onClick?.({ + code: opt.code, + row, + }) + : undefined, + }, + { + default: () => { + const content = []; + if (opt.icon) { + content.push( + h(IconifyIcon, { class: 'size-5', icon: opt.icon }), + ); + } + content.push(opt.text); + return content; + }, + }, + ); + } + + function renderConfirm(opt: Recordable) { + return h( + ElPopconfirm, + { + title: $t('ui.actionTitle.delete', [attrs?.nameTitle || '']), + width: 'auto', + 'popper-class': 'popper-top-left', + onConfirm: () => { + attrs?.onClick?.({ + code: opt.code, + row, + }); + }, + }, + { + reference: () => renderBtn({ ...opt }, false), + default: () => + h( + 'div', + { class: 'truncate' }, + $t('ui.actionMessage.deleteConfirm', [ + row[attrs?.nameField || 'name'], + ]), + ), + }, + ); + } + + const btns = operations.map((opt) => + opt.code === 'delete' ? renderConfirm(opt) : renderBtn(opt), + ); + return h( + 'div', + { + class: 'flex table-operations', + style: { justifyContent: align }, + }, + btns, + ); + }, + }); + + // 添加数量格式化,例如金额 + vxeUI.formats.add('formatAmount', { + cellFormatMethod({ cellValue }, digits = 2) { + if (cellValue === null || cellValue === undefined) { + return ''; + } + if (isString(cellValue)) { + cellValue = Number.parseFloat(cellValue); + } + // 如果非 number,则直接返回空串 + if (Number.isNaN(cellValue)) { + return ''; + } + return cellValue.toFixed(digits); + }, + }); }, useVbenForm, }); export { useVbenVxeGrid }; +const [VxeTable, VxeColumn, VxeToolbar] = AsyncComponents; +export { VxeColumn, VxeTable, VxeToolbar }; + +// 导出操作按钮的回调函数类型 +export type OnActionClickParams> = { + code: string; + row: T; +}; +export type OnActionClickFn> = ( + params: OnActionClickParams, +) => void; export type * from '@vben/plugins/vxe-table'; diff --git a/apps/web-ele/src/components/dict-tag/dict-tag.vue b/apps/web-ele/src/components/dict-tag/dict-tag.vue new file mode 100644 index 00000000..ec1ac136 --- /dev/null +++ b/apps/web-ele/src/components/dict-tag/dict-tag.vue @@ -0,0 +1,84 @@ + + + diff --git a/apps/web-ele/src/components/dict-tag/index.ts b/apps/web-ele/src/components/dict-tag/index.ts new file mode 100644 index 00000000..881265a3 --- /dev/null +++ b/apps/web-ele/src/components/dict-tag/index.ts @@ -0,0 +1 @@ +export { default as DictTag } from './dict-tag.vue'; diff --git a/apps/web-ele/src/components/doc-alert/doc-alert.vue b/apps/web-ele/src/components/doc-alert/doc-alert.vue new file mode 100644 index 00000000..20590d6f --- /dev/null +++ b/apps/web-ele/src/components/doc-alert/doc-alert.vue @@ -0,0 +1,37 @@ + + + diff --git a/apps/web-ele/src/components/doc-alert/index.ts b/apps/web-ele/src/components/doc-alert/index.ts new file mode 100644 index 00000000..21335153 --- /dev/null +++ b/apps/web-ele/src/components/doc-alert/index.ts @@ -0,0 +1 @@ +export { default as DocAlert } from './doc-alert.vue'; diff --git a/apps/web-ele/src/components/upload/file-upload.vue b/apps/web-ele/src/components/upload/file-upload.vue new file mode 100644 index 00000000..f50e20bd --- /dev/null +++ b/apps/web-ele/src/components/upload/file-upload.vue @@ -0,0 +1,256 @@ + + + diff --git a/apps/web-ele/src/components/upload/helper.ts b/apps/web-ele/src/components/upload/helper.ts new file mode 100644 index 00000000..a7a67639 --- /dev/null +++ b/apps/web-ele/src/components/upload/helper.ts @@ -0,0 +1,20 @@ +export function checkFileType(file: File, accepts: string[]) { + if (!accepts || accepts.length === 0) { + return true; + } + const newTypes = accepts.join('|'); + const reg = new RegExp(`${String.raw`\.(` + newTypes})$`, 'i'); + return reg.test(file.name); +} + +/** + * 默认图片类型 + */ +export const defaultImageAccepts = ['jpg', 'jpeg', 'png', 'gif', 'webp']; + +export function checkImgType( + file: File, + accepts: string[] = defaultImageAccepts, +) { + return checkFileType(file, accepts); +} diff --git a/apps/web-ele/src/components/upload/image-upload.vue b/apps/web-ele/src/components/upload/image-upload.vue new file mode 100644 index 00000000..174df55f --- /dev/null +++ b/apps/web-ele/src/components/upload/image-upload.vue @@ -0,0 +1,265 @@ + + + + + diff --git a/apps/web-ele/src/components/upload/index.ts b/apps/web-ele/src/components/upload/index.ts new file mode 100644 index 00000000..a66b2fca --- /dev/null +++ b/apps/web-ele/src/components/upload/index.ts @@ -0,0 +1,2 @@ +export { default as FileUpload } from './file-upload.vue'; +export { default as ImageUpload } from './image-upload.vue'; diff --git a/apps/web-ele/src/components/upload/typing.ts b/apps/web-ele/src/components/upload/typing.ts new file mode 100644 index 00000000..97ed4774 --- /dev/null +++ b/apps/web-ele/src/components/upload/typing.ts @@ -0,0 +1,46 @@ +import type { UploadStatus } from 'element-plus'; + +export type UploadListType = 'picture' | 'picture-card' | 'text'; + +export type UploadStatus = 'error' | 'removed' | 'success' | 'uploading'; + +export enum UploadResultStatus { + DONE = 'success', + ERROR = 'error', + REMOVED = 'removed', + SUCCESS = 'success', + UPLOADING = 'uploading', +} + +export interface CustomUploadFile { + uid: number; + name: string; + status: UploadStatus; + url?: string; + response?: any; + percentage?: number; + size?: number; + raw?: File; +} + +export function convertToUploadStatus( + status: UploadResultStatus, +): UploadStatus { + switch (status) { + case UploadResultStatus.DONE: { + return 'success'; + } + case UploadResultStatus.ERROR: { + return 'error'; + } + case UploadResultStatus.REMOVED: { + return 'removed'; + } + case UploadResultStatus.UPLOADING: { + return 'uploading'; + } + default: { + return 'success'; + } + } +} diff --git a/apps/web-ele/src/components/upload/use-upload.ts b/apps/web-ele/src/components/upload/use-upload.ts new file mode 100644 index 00000000..9e3ca13f --- /dev/null +++ b/apps/web-ele/src/components/upload/use-upload.ts @@ -0,0 +1,158 @@ +import type { Ref } from 'vue'; + +import type { AxiosProgressEvent, InfraFileApi } from '#/api/infra/file'; + +import { computed, unref } from 'vue'; + +import { useAppConfig } from '@vben/hooks'; +import { $t } from '@vben/locales'; + +// import CryptoJS from 'crypto-js'; +import { createFile, getFilePresignedUrl, uploadFile } from '#/api/infra/file'; +import { baseRequestClient } from '#/api/request'; + +const { apiURL } = useAppConfig(import.meta.env, import.meta.env.PROD); + +/** + * 上传类型 + */ +enum UPLOAD_TYPE { + // 客户端直接上传(只支持S3服务) + CLIENT = 'client', + // 客户端发送到后端上传 + SERVER = 'server', +} + +export function useUploadType({ + acceptRef, + helpTextRef, + maxNumberRef, + maxSizeRef, +}: { + acceptRef: Ref; + helpTextRef: Ref; + maxNumberRef: Ref; + maxSizeRef: Ref; +}) { + // 文件类型限制 + const getAccept = computed(() => { + const accept = unref(acceptRef); + if (accept && accept.length > 0) { + return accept; + } + return []; + }); + const getStringAccept = computed(() => { + return unref(getAccept) + .map((item) => { + return item.indexOf('/') > 0 || item.startsWith('.') + ? item + : `.${item}`; + }) + .join(','); + }); + + // 支持jpg、jpeg、png格式,不超过2M,最多可选择10张图片,。 + const getHelpText = computed(() => { + const helpText = unref(helpTextRef); + if (helpText) { + return helpText; + } + const helpTexts: string[] = []; + + const accept = unref(acceptRef); + if (accept.length > 0) { + helpTexts.push($t('ui.upload.accept', [accept.join(',')])); + } + + const maxSize = unref(maxSizeRef); + if (maxSize) { + helpTexts.push($t('ui.upload.maxSize', [maxSize])); + } + + const maxNumber = unref(maxNumberRef); + if (maxNumber && maxNumber !== Infinity) { + helpTexts.push($t('ui.upload.maxNumber', [maxNumber])); + } + return helpTexts.join(','); + }); + return { getAccept, getStringAccept, getHelpText }; +} + +// TODO @芋艿:目前保持和 admin-vue3 一致,后续可能重构 +export const useUpload = (directory?: string) => { + // 后端上传地址 + const uploadUrl = getUploadUrl(); + // 是否使用前端直连上传 + const isClientUpload = + UPLOAD_TYPE.CLIENT === import.meta.env.VITE_UPLOAD_TYPE; + // 重写ElUpload上传方法 + const httpRequest = async ( + file: File, + onUploadProgress?: AxiosProgressEvent, + ) => { + // 模式一:前端上传 + if (isClientUpload) { + // 1.1 生成文件名称 + const fileName = await generateFileName(file); + // 1.2 获取文件预签名地址 + const presignedInfo = await getFilePresignedUrl(fileName, directory); + // 1.3 上传文件 + return baseRequestClient + .put(presignedInfo.uploadUrl, file, { + headers: { + 'Content-Type': file.type, + }, + }) + .then(() => { + // 1.4. 记录文件信息到后端(异步) + createFile0(presignedInfo, file); + // 通知成功,数据格式保持与后端上传的返回结果一致 + return { url: presignedInfo.url }; + }); + } else { + // 模式二:后端上传 + return uploadFile({ file, directory }, onUploadProgress); + } + }; + + return { + uploadUrl, + httpRequest, + }; +}; + +/** + * 获得上传 URL + */ +export const getUploadUrl = (): string => { + return `${apiURL}/infra/file/upload`; +}; + +/** + * 创建文件信息 + * + * @param vo 文件预签名信息 + * @param file 文件 + */ +function createFile0(vo: InfraFileApi.FilePresignedUrlRespVO, file: File) { + const fileVO = { + configId: vo.configId, + url: vo.url, + path: vo.path, + name: file.name, + type: file.type, + size: file.size, + }; + createFile(fileVO); + return fileVO; +} + +/** + * 生成文件名称 + * + * @param file 要上传的文件 + */ +async function generateFileName(file: File) { + return file.name; +} diff --git a/apps/web-ele/src/views/system/tenant/data.ts b/apps/web-ele/src/views/system/tenant/data.ts new file mode 100644 index 00000000..16c16541 --- /dev/null +++ b/apps/web-ele/src/views/system/tenant/data.ts @@ -0,0 +1,255 @@ +import type { VbenFormSchema } from '#/adapter/form'; +import type { OnActionClickFn, VxeTableGridOptions } from '#/adapter/vxe-table'; +import type { SystemTenantApi } from '#/api/system/tenant'; + +import { useAccess } from '@vben/access'; + +import { z } from '#/adapter/form'; +import { getTenantPackageList } from '#/api/system/tenant-package'; +import { + CommonStatusEnum, + DICT_TYPE, + getDictOptions, + getRangePickerDefaultProps, +} from '#/utils'; + +const { hasAccessByCodes } = useAccess(); + +/** 新增/修改的表单 */ +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: '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: { + ...getRangePickerDefaultProps(), + allowClear: true, + }, + }, + ]; +} + +/** 列表的字段 */ +export function useGridColumns( + onActionClick: OnActionClickFn, + getPackageName?: (packageId: number) => string | undefined, +): VxeTableGridOptions['columns'] { + return [ + { + field: 'id', + title: '租户编号', + minWidth: 100, + }, + { + field: 'name', + title: '租户名', + minWidth: 180, + }, + { + field: 'packageId', + title: '租户套餐', + minWidth: 180, + formatter: (row: { cellValue: number }) => { + return getPackageName?.(row.cellValue) || '-'; + }, + }, + { + 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', + options: [ + { + code: 'edit', + show: hasAccessByCodes(['system:tenant:update']), + }, + { + code: 'delete', + show: hasAccessByCodes(['system:tenant:delete']), + }, + ], + }, + }, + ]; +} diff --git a/apps/web-ele/src/views/system/tenant/index.vue b/apps/web-ele/src/views/system/tenant/index.vue new file mode 100644 index 00000000..0fbbce71 --- /dev/null +++ b/apps/web-ele/src/views/system/tenant/index.vue @@ -0,0 +1,160 @@ + + diff --git a/apps/web-ele/src/views/system/tenant/modules/form.vue b/apps/web-ele/src/views/system/tenant/modules/form.vue new file mode 100644 index 00000000..3e787306 --- /dev/null +++ b/apps/web-ele/src/views/system/tenant/modules/form.vue @@ -0,0 +1,81 @@ + + diff --git a/apps/web-ele/src/views/system/tenantPackage/data.ts b/apps/web-ele/src/views/system/tenantPackage/data.ts new file mode 100644 index 00000000..5f1bcf9f --- /dev/null +++ b/apps/web-ele/src/views/system/tenantPackage/data.ts @@ -0,0 +1,154 @@ +import type { VbenFormSchema } from '#/adapter/form'; +import type { OnActionClickFn, VxeTableGridOptions } from '#/adapter/vxe-table'; +import type { SystemTenantPackageApi } from '#/api/system/tenant-package'; + +import { useAccess } from '@vben/access'; + +import { z } from '#/adapter/form'; +import { + CommonStatusEnum, + DICT_TYPE, + getDictOptions, + getRangePickerDefaultProps, +} from '#/utils'; + +const { hasAccessByCodes } = useAccess(); + +/** 新增/修改的表单 */ +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: 'Textarea', + }, + ]; +} + +/** 列表的搜索表单 */ +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: { + ...getRangePickerDefaultProps(), + allowClear: true, + }, + }, + ]; +} + +/** 列表的字段 */ +export function useGridColumns( + onActionClick: OnActionClickFn, +): 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', + options: [ + { + code: 'edit', + show: hasAccessByCodes(['system:tenant-package:update']), + }, + { + code: 'delete', + show: hasAccessByCodes(['system:tenant-package:delete']), + }, + ], + }, + }, + ]; +} diff --git a/apps/web-ele/src/views/system/tenantPackage/index.vue b/apps/web-ele/src/views/system/tenantPackage/index.vue new file mode 100644 index 00000000..239b95d9 --- /dev/null +++ b/apps/web-ele/src/views/system/tenantPackage/index.vue @@ -0,0 +1,129 @@ + + + diff --git a/apps/web-ele/src/views/system/tenantPackage/modules/form.vue b/apps/web-ele/src/views/system/tenantPackage/modules/form.vue new file mode 100644 index 00000000..010d13cc --- /dev/null +++ b/apps/web-ele/src/views/system/tenantPackage/modules/form.vue @@ -0,0 +1,162 @@ + + +