diff --git a/apps/web-antd/src/api/base.ts b/apps/web-antd/src/api/base.ts new file mode 100644 index 00000000..a8064af0 --- /dev/null +++ b/apps/web-antd/src/api/base.ts @@ -0,0 +1,53 @@ +import { requestClient } from './request'; + +export type ID = number | string; +export type IDS = (number | string)[]; + +export interface BaseEntity { + createBy?: string; + createDept?: string; + createTime?: string; + updateBy?: string; + updateTime?: string; +} + +/** + * 分页查询参数 + * @param pageNum 当前页 + * @param pageSize 每页大小 + * @param orderByColumn 排序字段 + * @param isAsc 是否升序 + */ +export interface PageQuery { + isAsc?: boolean; + orderByColumn?: string; + pageNum?: number; + pageSize?: number; +} + +/** + * @description: contentType + */ +export enum ContentTypeEnum { + // form-data upload + FORM_DATA = 'multipart/form-data;charset=UTF-8', + // form-data qs + FORM_URLENCODED = 'application/x-www-form-urlencoded;charset=UTF-8', + // json + JSON = 'application/json;charset=UTF-8', +} + +/** + * 通用下载接口 封装一层 + * @param url 请求地址 + * @param data 请求参数 + * @returns blob二进制 + */ +export function commonExport(url: string, data: Record) { + return requestClient.post(url, data, { + data, + headers: { 'Content-Type': ContentTypeEnum.FORM_URLENCODED }, + isTransformResponse: false, + responseType: 'blob', + }); +} diff --git a/apps/web-antd/src/api/system/dict/dict-data-model.d.ts b/apps/web-antd/src/api/system/dict/dict-data-model.d.ts new file mode 100644 index 00000000..e46092f3 --- /dev/null +++ b/apps/web-antd/src/api/system/dict/dict-data-model.d.ts @@ -0,0 +1,17 @@ +export interface DictData { + createBy: string; + createTime: string; + cssClass: string; + default: boolean; + dictCode: number; + dictLabel: string; + dictSort: number; + dictType: string; + dictValue: string; + isDefault: string; + listClass: string; + remark: string; + status: string; + updateBy?: any; + updateTime?: any; +} diff --git a/apps/web-antd/src/api/system/dict/dict-data.ts b/apps/web-antd/src/api/system/dict/dict-data.ts new file mode 100644 index 00000000..4180e5eb --- /dev/null +++ b/apps/web-antd/src/api/system/dict/dict-data.ts @@ -0,0 +1,75 @@ +import type { DictData } from './dict-data-model'; + +import type { ID, IDS, PageQuery } from '#/api/base'; + +import { commonExport } from '#/api/base'; +import { requestClient } from '#/api/request'; + +enum Api { + dictDataExport = '/system/dict/data/export', + dictDataList = '/system/dict/data/list', + root = '/system/dict/data', +} + +/** + * 主要是DictTag组件使用 + * @param dictType 字典类型 + * @returns 字典数据 + */ +export function dictDataInfo(dictType: string) { + return requestClient.get(`${Api.root}/type/${dictType}`); +} + +/** + * 字典数据 + * @param params 查询参数 + * @returns 字典数据列表 + */ +export function dictDataList(params?: PageQuery) { + return requestClient.get(Api.dictDataList, { params }); +} + +/** + * 导出字典数据 + * @param data 表单参数 + * @returns blob + */ +export function dictDataExport(data: any) { + return commonExport(Api.dictDataExport, data); +} + +/** + * 删除 + * @param dictIds 字典ID Array + * @returns void + */ +export function dictDataRemove(dictIds: IDS) { + return requestClient.deleteWithMsg(`${Api.root}/${dictIds.join(',')}`); +} + +/** + * 新增 + * @param data 表单参数 + * @returns void + */ +export function dictDataAdd(data: any) { + return requestClient.postWithMsg(Api.root, data); +} + +/** + * 修改 + * @param data 表单参数 + * @returns void + */ +export function dictDataUpdate(data: any) { + return requestClient.putWithMsg(Api.root, data); +} + +/** + * 查询字典数据详细 + * @param dictCode 字典编码 + * @returns 字典数据 + */ +export function dictDetailInfo(dictCode: ID) { + return requestClient.get(`${Api.root}/${dictCode}`); +} diff --git a/apps/web-antd/src/api/system/dict/dict-type-model.d.ts b/apps/web-antd/src/api/system/dict/dict-type-model.d.ts new file mode 100644 index 00000000..b4bfcb80 --- /dev/null +++ b/apps/web-antd/src/api/system/dict/dict-type-model.d.ts @@ -0,0 +1,8 @@ +export interface DictType { + createTime: string; + dictId: number; + dictName: string; + dictType: string; + remark: string; + status: string; +} diff --git a/apps/web-antd/src/api/system/dict/dict-type.ts b/apps/web-antd/src/api/system/dict/dict-type.ts new file mode 100644 index 00000000..23966334 --- /dev/null +++ b/apps/web-antd/src/api/system/dict/dict-type.ts @@ -0,0 +1,84 @@ +import type { DictType } from './dict-type-model'; + +import type { ID, IDS, PageQuery } from '#/api/base'; + +import { commonExport } from '#/api/base'; +import { requestClient } from '#/api/request'; + +enum Api { + dictOptionSelectList = '/system/dict/type/optionselect', + dictTypeExport = '/system/dict/type/export', + dictTypeList = '/system/dict/type/list', + dictTypeRefreshCache = '/system/dict/type/refreshCache', + root = '/system/dict/type', +} + +/** + * 获取字典类型列表 + * @param params 请求参数 + * @returns list + */ +export function dictList(params?: PageQuery) { + return requestClient.get(Api.dictTypeList, { params }); +} + +/** + * 导出字典类型列表 + * @param data 表单参数 + * @returns blob + */ +export function dictExport(data: any) { + return commonExport(Api.dictTypeExport, data); +} + +/** + * 删除字典类型 + * @param dictIds 字典类型id数组 + * @returns void + */ +export function dictTypeRemove(dictIds: IDS) { + return requestClient.deleteWithMsg(`${Api.root}/${dictIds.join(',')}`); +} + +/** + * 刷新字典缓存 + * @returns void + */ +export function refreshDictTypeCache() { + return requestClient.deleteWithMsg(Api.dictTypeRefreshCache); +} + +/** + * 新增 + * @param data 表单参数 + * @returns void + */ +export function dictTypeAdd(data: any) { + return requestClient.postWithMsg(Api.root, data); +} + +/** + * 修改 + * @param data 表单参数 + * @returns void + */ +export function dictTypeUpdate(data: any) { + return requestClient.putWithMsg(Api.root, data); +} + +/** + * 查询详情 + * @param dictId 字典类型id + * @returns 信息 + */ +export function dictTypeInfo(dictId: ID) { + return requestClient.get(`${Api.root}/${dictId}`); +} + +/** + * 下拉框 返回值和list一样 + * @returns options + */ +export function dictOptionSelectList() { + return requestClient.get(Api.dictOptionSelectList); +} diff --git a/apps/web-antd/src/components/Dict/index.ts b/apps/web-antd/src/components/Dict/index.ts new file mode 100644 index 00000000..9dd84d24 --- /dev/null +++ b/apps/web-antd/src/components/Dict/index.ts @@ -0,0 +1,5 @@ +import { withInstall } from '#/utils'; + +import dictTag from './src/index.vue'; + +export const DictTag = withInstall(dictTag); diff --git a/apps/web-antd/src/components/Dict/src/data.tsx b/apps/web-antd/src/components/Dict/src/data.tsx new file mode 100644 index 00000000..68395f8a --- /dev/null +++ b/apps/web-antd/src/components/Dict/src/data.tsx @@ -0,0 +1,44 @@ +import { type VNode } from 'vue'; + +import { Tag } from 'ant-design-vue'; + +interface TagType { + [key: string]: { color: string; label: string }; +} + +export const tagTypes: TagType = { + cyan: { color: 'cyan', label: 'cyan' }, + danger: { color: 'error', label: '危险(danger)' }, + /** 由于和elementUI不同 用于替换颜色 */ + default: { color: 'default', label: '默认(default)' }, + green: { color: 'green', label: 'green' }, + info: { color: 'default', label: '信息(info)' }, + orange: { color: 'orange', label: 'orange' }, + /** 自定义预设 color可以为16进制颜色 */ + pink: { color: 'pink', label: 'pink' }, + primary: { color: 'processing', label: '主要(primary)' }, + purple: { color: 'purple', label: 'purple' }, + red: { color: 'red', label: 'red' }, + success: { color: 'success', label: '成功(success)' }, + warning: { color: 'warning', label: '警告(warning)' }, +}; + +// 字典选择使用 { label: string; value: string }[] +interface Options { + label: string | VNode; + value: string; +} + +export function tagSelectOptions() { + const selectArray: Options[] = []; + Object.keys(tagTypes).forEach((key) => { + if (!tagTypes[key]) return; + const label = tagTypes[key].label; + const color = tagTypes[key].color; + selectArray.push({ + label: {label}, + value: key, + }); + }); + return selectArray; +} diff --git a/apps/web-antd/src/components/Dict/src/index.vue b/apps/web-antd/src/components/Dict/src/index.vue new file mode 100644 index 00000000..25b04794 --- /dev/null +++ b/apps/web-antd/src/components/Dict/src/index.vue @@ -0,0 +1,61 @@ + + + diff --git a/apps/web-antd/src/store/dict.ts b/apps/web-antd/src/store/dict.ts new file mode 100644 index 00000000..b8b195f1 --- /dev/null +++ b/apps/web-antd/src/store/dict.ts @@ -0,0 +1,99 @@ +import type { DictData } from '#/api/system/dict/dict-data-model'; + +import { reactive } from 'vue'; + +import { defineStore } from 'pinia'; + +/** + * antd使用 select和radio通用 + */ +export interface Option { + disabled?: boolean; + label: string; + value: string; +} + +export function dictToOptions(data: DictData[]): Option[] { + return data.map((item) => ({ + label: item.dictLabel, + value: item.dictValue, + })); +} + +export const useDictStore = defineStore('app-dict', () => { + /** + * 一般是dictTag使用 + */ + const dictMap = reactive(new Map()); + /** + * select radio radioButton使用 只能为固定格式(Option) + */ + const dictOptionsMap = reactive(new Map()); + + function getDict(dictName: string): DictData[] { + if (!dictName) return []; + // 没有key 添加一个空数组 + if (!dictMap.has(dictName)) { + dictMap.set(dictName, reactive([])); + } + // 这里拿到的就不可能为空了 + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + return dictMap.get(dictName)!; + } + + function getDictOptions(dictName: string): Option[] { + if (!dictName) return []; + // 没有key 添加一个空数组 + if (!dictOptionsMap.has(dictName)) { + dictOptionsMap.set(dictName, []); + } + // 这里拿到的就不可能为空了 + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + return dictOptionsMap.get(dictName)!; + } + + function resetCache() { + dictMap.clear(); + dictOptionsMap.clear(); + } + + /** + * 核心逻辑 + * + * 不能直接粗暴使用set 会导致之前return的空数组跟现在的数组指向不是同一个地址 数据也就为空了 + * + * 判断是否已经存在key 并且数组长度为0 说明该次要处理的数据是return的空数组 直接push(不修改指向) + * 否则 直接set + * + */ + function setDictInfo(dictName: string, dictValue: DictData[]) { + if (dictMap.has(dictName) && dictMap.get(dictName)?.length === 0) { + dictMap.get(dictName)?.push(...dictValue); + } else { + dictMap.set(dictName, dictValue); + } + if ( + dictOptionsMap.has(dictName) && + dictOptionsMap.get(dictName)?.length === 0 + ) { + dictOptionsMap.get(dictName)?.push(...dictToOptions(dictValue)); + } else { + dictOptionsMap.set(dictName, dictToOptions(dictValue)); + } + } + + function $reset() { + dictMap.clear(); + dictOptionsMap.clear(); + } + + return { + $reset, + dictMap, + dictOptionsMap, + getDict, + getDictOptions, + resetCache, + setDictInfo, + }; +}); diff --git a/apps/web-antd/src/utils/dict.ts b/apps/web-antd/src/utils/dict.ts new file mode 100644 index 00000000..dca5e02a --- /dev/null +++ b/apps/web-antd/src/utils/dict.ts @@ -0,0 +1,55 @@ +import type { DictData } from '#/api/system/dict/dict-data-model'; + +import { dictDataInfo } from '#/api/system/dict/dict-data'; +import { type Option, useDictStore } from '#/store/dict'; +// todo 重复代码的封装 +/** + * 添加一个字典请求状态的缓存 + * + * 主要解决多次请求重复api的问题(不能用abortController 会导致除了第一个其他的获取的全为空) + * 比如在一个页面 index表单 modal drawer总共会请求三次 但是获取的都是一样的数据 + */ +const dictRequestCache = new Map>(); + +export function getDict(dictName: string): DictData[] { + const { getDict, setDictInfo } = useDictStore(); + // 这里拿到 + const dictList = getDict(dictName); + if ( + dictList.length === 0 && // 检查请求状态缓存 + !dictRequestCache.has(dictName) + ) { + dictRequestCache.set( + dictName, + dictDataInfo(dictName).then((resp) => { + // 缓存到store 这样就不用重复获取了 + // 内部处理了push的逻辑 这里不用push + setDictInfo(dictName, resp); + // 移除请求状态缓存 + dictRequestCache.delete(dictName); + }), + ); + } + return dictList; +} + +export function getDictOptions(dictName: string): Option[] { + const { getDictOptions, setDictInfo } = useDictStore(); + const dictOptionList = getDictOptions(dictName); + if ( + dictOptionList.length === 0 && // 检查请求状态缓存 + !dictRequestCache.has(dictName) + ) { + dictRequestCache.set( + dictName, + dictDataInfo(dictName).then((resp) => { + // 缓存到store 这样就不用重复获取了 + // 内部处理了push的逻辑 这里不用push + setDictInfo(dictName, resp); + // 移除请求状态缓存 + dictRequestCache.delete(dictName); + }), + ); + } + return dictOptionList; +} diff --git a/apps/web-antd/src/utils/index.ts b/apps/web-antd/src/utils/index.ts new file mode 100644 index 00000000..0b8248b1 --- /dev/null +++ b/apps/web-antd/src/utils/index.ts @@ -0,0 +1,180 @@ +import type { + RouteLocationNormalized, + RouteRecordNormalized, +} from 'vue-router'; + +import type { App, Component } from 'vue'; +import { unref } from 'vue'; + +import { + intersectionWith, + isArray, + isEqual, + isObject, + mergeWith, + unionWith, +} from 'lodash-es'; + +export const noop = () => {}; + +/** + * @description: Set ui mount node + */ +export function getPopupContainer(node?: HTMLElement): HTMLElement { + return (node?.parentNode as HTMLElement) ?? document.body; +} + +/** + * Add the object as a parameter to the URL + * @param baseUrl url + * @param obj + * @returns {string} + * eg: + * let obj = {a: '3', b: '4'} + * setObjToUrlParams('www.baidu.com', obj) + * ==>www.baidu.com?a=3&b=4 + */ +export function setObjToUrlParams(baseUrl: string, obj: any): string { + let parameters = ''; + for (const key in obj) { + parameters += `${key}=${encodeURIComponent(obj[key])}&`; + } + parameters = parameters.replace(/&$/, ''); + return /\?$/.test(baseUrl) + ? baseUrl + parameters + : baseUrl.replace(/\/?$/, '?') + parameters; +} + +/** + * Recursively merge two objects. + * 递归合并两个对象。 + * + * @param source The source object to merge from. 要合并的源对象。 + * @param target The target object to merge into. 目标对象,合并后结果存放于此。 + * @param mergeArrays How to merge arrays. Default is "replace". + * 如何合并数组。默认为replace。 + * - "union": Union the arrays. 对数组执行并集操作。 + * - "intersection": Intersect the arrays. 对数组执行交集操作。 + * - "concat": Concatenate the arrays. 连接数组。 + * - "replace": Replace the source array with the target array. 用目标数组替换源数组。 + * @returns The merged object. 合并后的对象。 + */ +export function deepMerge< + T extends null | object | undefined, + U extends null | object | undefined, +>( + source: T, + target: U, + mergeArrays: 'concat' | 'intersection' | 'replace' | 'union' = 'replace', +): T & U { + if (!target) { + return source as T & U; + } + if (!source) { + return target as T & U; + } + return mergeWith({}, source, target, (sourceValue, targetValue) => { + if (isArray(targetValue) && isArray(sourceValue)) { + switch (mergeArrays) { + case 'concat': { + return [...sourceValue, ...targetValue]; + } + case 'intersection': { + return intersectionWith(sourceValue, targetValue, isEqual); + } + case 'replace': { + return targetValue; + } + case 'union': { + return unionWith(sourceValue, targetValue, isEqual); + } + default: { + throw new Error( + `Unknown merge array strategy: ${mergeArrays as string}`, + ); + } + } + } + if (isObject(targetValue) && isObject(sourceValue)) { + return deepMerge(sourceValue, targetValue, mergeArrays); + } + }); +} + +export function openWindow( + url: string, + opt?: { + noopener?: boolean; + noreferrer?: boolean; + target?: '_blank' | '_self' | string; + }, +) { + const { noopener = true, noreferrer = true, target = '__blank' } = opt || {}; + const feature: string[] = []; + + noopener && feature.push('noopener=yes'); + noreferrer && feature.push('noreferrer=yes'); + + window.open(url, target, feature.join(',')); +} + +// dynamic use hook props +export function getDynamicProps, U>( + props: T, +): Partial { + const ret: Record = {}; + + Object.keys(props).forEach((key) => { + ret[key] = unref((props as Record)[key]); + }); + + return ret as Partial; +} + +export function getRawRoute( + route: RouteLocationNormalized, +): RouteLocationNormalized { + if (!route) return route; + const { matched, ...opt } = route; + return { + ...opt, + matched: (matched + ? matched.map((item) => ({ + meta: item.meta, + name: item.name, + path: item.path, + })) + : undefined) as RouteRecordNormalized[], + }; +} + +// https://github.com/vant-ui/vant/issues/8302 +interface EventShim { + new (...args: any[]): { + $props: { + onClick?: (...args: any[]) => void; + }; + }; +} + +export type WithInstall = { + install(app: App): void; +} & EventShim & + T; + +export type CustomComponent = { displayName?: string } & Component; + +export const withInstall = ( + component: T, + alias?: string, +) => { + (component as Record).install = (app: App) => { + const compName = component.name || component.displayName; + if (!compName) return; + app.component(compName, component); + if (alias) { + app.config.globalProperties[alias] = component; + } + }; + return component as WithInstall; +}; diff --git a/apps/web-antd/src/views/system/user/index.vue b/apps/web-antd/src/views/system/user/index.vue index 8c44707c..5d833c7b 100644 --- a/apps/web-antd/src/views/system/user/index.vue +++ b/apps/web-antd/src/views/system/user/index.vue @@ -1,7 +1,11 @@