From 10f78b7cff5770aedfa6a7ac15b71c30e9e518ae Mon Sep 17 00:00:00 2001 From: qianpw <2233607957@qq.com> Date: Thu, 25 Sep 2025 17:51:05 +0800 Subject: [PATCH 1/4] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E4=BF=A1?= =?UTF-8?q?=E6=81=AF=E6=8F=90=E7=A4=BAhooks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/hooks/antd/useMessage.ts | 131 +++++++++++++++++++++++++++++++++++ 1 file changed, 131 insertions(+) create mode 100644 src/hooks/antd/useMessage.ts diff --git a/src/hooks/antd/useMessage.ts b/src/hooks/antd/useMessage.ts new file mode 100644 index 0000000..c32335f --- /dev/null +++ b/src/hooks/antd/useMessage.ts @@ -0,0 +1,131 @@ +// src/hooks/antd/useMessage.ts +import { Modal, message, notification } from 'antd'; + +export const useMessage = () => { + return { + // 消息提示 + info(content: string) { + message.info(content); + }, + // 错误消息 + error(content: string) { + message.error(content); + }, + // 成功消息 + success(content: string) { + message.success(content); + }, + // 警告消息 + warning(content: string) { + message.warning(content); + }, + // 弹出提示 + alert(content: string) { + Modal.info({ + title: '提示', + content: content, + okText: '确定', + }); + }, + // 错误提示 + alertError(content: string) { + Modal.error({ + title: '提示', + content: content, + okText: '确定', + }); + }, + // 成功提示 + alertSuccess(content: string) { + Modal.success({ + title: '提示', + content: content, + okText: '确定', + }); + }, + // 警告提示 + alertWarning(content: string) { + Modal.warning({ + title: '提示', + content: content, + okText: '确定', + }); + }, + // 通知提示 + notify(content: string) { + notification.info({ + message: content, + }); + }, + // 错误通知 + notifyError(content: string) { + notification.error({ + message: content, + }); + }, + // 成功通知 + notifySuccess(content: string) { + notification.success({ + message: content, + }); + }, + // 警告通知 + notifyWarning(content: string) { + notification.warning({ + message: content, + }); + }, + // 确认窗体 + confirm(content: string, tip?: string) { + return new Promise((resolve) => { + Modal.confirm({ + title: tip ? tip : '提示', + content: content, + okText: '确定', + cancelText: '取消', + onOk: () => resolve(true), + onCancel: () => resolve(false), + }); + }); + }, + // 删除窗体 + delConfirm(content?: string, tip?: string) { + return new Promise((resolve) => { + Modal.confirm({ + title: tip ? tip : '提示', + content: content ? content : '是否确认删除?', + okText: '确定', + cancelText: '取消', + onOk: () => resolve(true), + onCancel: () => resolve(false), + }); + }); + }, + // 导出窗体 + exportConfirm(content?: string, tip?: string) { + return new Promise((resolve) => { + Modal.confirm({ + title: tip ? tip : '提示', + content: content ? content : '是否确认导出?', + okText: '确定', + cancelText: '取消', + onOk: () => resolve(true), + onCancel: () => resolve(false), + }); + }); + }, + // 提交内容 + prompt(content: string, tip: string) { + return new Promise((resolve) => { + Modal.confirm({ + title: tip, + content: content, + okText: '确定', + cancelText: '取消', + onOk: () => resolve(true), + onCancel: () => resolve(false), + }); + }); + }, + }; +}; From 9c510636c5f7257371ca7f504e13d3043231e6af Mon Sep 17 00:00:00 2001 From: qianpw <2233607957@qq.com> Date: Thu, 25 Sep 2025 17:51:46 +0800 Subject: [PATCH 2/4] =?UTF-8?q?feat:=20=E5=AE=8C=E5=96=84=E8=8F=9C?= =?UTF-8?q?=E5=8D=95=E7=AE=A1=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/system/menu/config.tsx | 271 ++++++++++++++++++++++++++++--- src/pages/system/menu/index.tsx | 36 +++- 2 files changed, 278 insertions(+), 29 deletions(-) diff --git a/src/pages/system/menu/config.tsx b/src/pages/system/menu/config.tsx index 8018186..a1d591b 100644 --- a/src/pages/system/menu/config.tsx +++ b/src/pages/system/menu/config.tsx @@ -1,10 +1,13 @@ // src/pages/system/menu/config.tsx -import type { - ProColumns, - ProFormColumnsType, +import { + type ProColumns, + type ProFormColumnsType, + ProFormRadio, + ProFormText, } from '@ant-design/pro-components'; import { Switch } from 'antd'; -import type { MenuVO } from '@/services/system/menu'; +import { getSimpleMenusList, type MenuVO } from '@/services/system/menu'; +import { handleTree } from '@/utils/tree'; export const baseMenuColumns: ProColumns[] = [ { @@ -16,6 +19,7 @@ export const baseMenuColumns: ProColumns[] = [ title: '图标', dataIndex: 'icon', width: 60, + hideInSearch: true, render: (_, record: MenuVO) => ( {record.icon ? : '—'} @@ -26,21 +30,25 @@ export const baseMenuColumns: ProColumns[] = [ title: '排序', dataIndex: 'sort', width: 80, + hideInSearch: true, }, { title: '权限标识', dataIndex: 'permission', width: 150, + hideInSearch: true, }, { title: '组件路径', dataIndex: 'component', width: 150, + hideInSearch: true, }, { title: '组件名称', dataIndex: 'componentName', width: 150, + hideInSearch: true, }, { title: '状态', @@ -56,6 +64,22 @@ export const baseMenuColumns: ProColumns[] = [ ]; export const formColumns = (_type: string): ProFormColumnsType[] => [ + { + title: '上级菜单', + dataIndex: 'parentId', + valueType: 'treeSelect', + request: async () => { + const res = await getSimpleMenusList(); + console.log(res); + const menu: Tree = { id: 0, name: '主类目', children: [] }; + menu.children = handleTree(res); + return [menu]; + }, + fieldProps: { + fieldNames: { label: 'name', value: 'id', children: 'children' }, + placeholder: '请选择上级菜单', + }, + }, { title: '菜单名称', dataIndex: 'name', @@ -69,39 +93,105 @@ export const formColumns = (_type: string): ProFormColumnsType[] => [ }, }, { - title: '图标', - dataIndex: 'icon', - valueType: 'select', + title: '菜单类型', + dataIndex: 'type', + valueType: 'radioButton', fieldProps: { - placeholder: '请选择图标', + placeholder: '请选择菜单类型', options: [ - { label: '商品', value: 'icon-product' }, - { label: '系统', value: 'icon-system' }, - { label: '用户', value: 'icon-user' }, - { label: '设置', value: 'icon-setting' }, + { label: '目录', value: 1 }, + { label: '菜单', value: 2 }, + { label: '按钮', value: 3 }, + ], + }, + formItemProps: { + rules: [ + { + required: true, + message: '请输入菜单类型', + }, ], }, }, { - title: '排序', - dataIndex: 'sort', - valueType: 'digit', + title: '菜单图标', + dataIndex: 'icon', fieldProps: { - placeholder: '请输入排序值', + placeholder: '请选择图标', + }, + dependencies: ['type'], + renderFormItem: (_schema, _config, form) => { + const type = form.getFieldValue('type'); + if (type === 3) return null; + return ( + + ); }, }, { - title: '权限标识', - dataIndex: 'permission', + title: '路由地址', + dataIndex: 'path', + tooltip: + '访问的路由地址,如:`user`。如需外网地址时,则以 `http(s)://` 开头', fieldProps: { - placeholder: '请输入权限标识', + placeholder: '请输入路由地址', + }, + formItemProps: { + rules: [ + { + required: true, + message: '请输入菜单路径', + }, + ], + }, + dependencies: ['type'], + renderFormItem: (_schema, _config, form) => { + const type = form.getFieldValue('type'); + if (type === 3) return null; + return ( + + ); }, }, { - title: '组件路径', + title: '组件地址', dataIndex: 'component', fieldProps: { - placeholder: '请输入组件路径', + placeholder: '请输入组件地址', + }, + formItemProps: { + rules: [ + { + required: true, + message: '请输入组件地址', + }, + ], + }, + dependencies: ['type'], + renderFormItem: (_schema, _config, form) => { + const type = form.getFieldValue('type'); + if (type !== 2) return null; + return ( + + ); }, }, { @@ -110,11 +200,68 @@ export const formColumns = (_type: string): ProFormColumnsType[] => [ fieldProps: { placeholder: '请输入组件名称', }, + dependencies: ['type'], + renderFormItem: (_schema, _config, form) => { + const type = form.getFieldValue('type'); + if (type !== 2) return null; + return ( + + ); + }, }, { - title: '状态', + title: '权限标识', + dataIndex: 'permission', + tooltip: + "Controller 方法上的权限字符,如:@PreAuthorize(`@ss.hasPermission('system:user:list')`)", + fieldProps: { + placeholder: '请输入权限标识', + }, + dependencies: ['type'], + renderFormItem: (_schema, _config, form) => { + const type = form.getFieldValue('type'); + if (type === 1) return null; + return ( + + ); + }, + }, + { + title: '显示排序', + dataIndex: 'sort', + valueType: 'digit', + fieldProps: { + style: { + width: '100%', + }, + placeholder: '请输入排序值', + }, + formItemProps: { + rules: [ + { + required: true, + message: '请输入排序值', + }, + ], + }, + }, + + { + title: '菜单状态', dataIndex: 'status', - valueType: 'select', + valueType: 'radio', fieldProps: { options: [ { label: '启用', value: 1 }, @@ -122,4 +269,82 @@ export const formColumns = (_type: string): ProFormColumnsType[] => [ ], }, }, + { + title: '显示状态', + dataIndex: 'visible', + valueType: 'radio', + tooltip: '选择隐藏时,路由将不会出现在侧边栏,但仍然可以访问', + fieldProps: { + options: [ + { label: '显示', value: true }, + { label: '隐藏', value: false }, + ], + }, + dependencies: ['type'], + renderFormItem: (_schema, _config, form) => { + const type = form.getFieldValue('type'); + if (type === 3) return null; + return ( + + ); + }, + }, + { + title: '总是显示', + dataIndex: 'alwaysShow', + valueType: 'radio', + tooltip: '选择不是时,当该菜单只有一个子菜单时,不展示自己,直接展示子菜单', + fieldProps: { + options: [ + { label: '总是', value: true }, + { label: '不是', value: false }, + ], + }, + dependencies: ['type'], + renderFormItem: (_schema, _config, form) => { + const type = form.getFieldValue('type'); + if (type === 3) return null; + return ( + + ); + }, + }, + { + dataIndex: 'keepAlive', + title: '缓存状态', + valueType: 'radio', + tooltip: '选择缓存时,则会被 `keep-alive` 缓存,必须填写「组件名称」字段', + fieldProps: { + options: [ + { label: '总是', value: true }, + { label: '不是', value: false }, + ], + }, + dependencies: ['type'], + renderFormItem: (_schema, _config, form) => { + const type = form.getFieldValue('type'); + if (type === 3) return null; + return ( + + ); + }, + }, ]; diff --git a/src/pages/system/menu/index.tsx b/src/pages/system/menu/index.tsx index 383a9cc..9ea4eeb 100644 --- a/src/pages/system/menu/index.tsx +++ b/src/pages/system/menu/index.tsx @@ -1,8 +1,9 @@ // src/pages/system/menu/index.tsx -import { PlusOutlined } from '@ant-design/icons'; +import { PlusOutlined, ReloadOutlined } from '@ant-design/icons'; import type { ActionType, ProColumns } from '@ant-design/pro-components'; -import { Popconfirm } from 'antd'; +import { useModel } from '@umijs/max'; +import { Modal, Popconfirm } from 'antd'; import React, { useCallback, useRef, useState } from 'react'; import ConfigurableDrawerForm, { type ConfigurableDrawerFormRef, @@ -10,6 +11,8 @@ import ConfigurableDrawerForm, { import EnhancedProTable from '@/components/EnhancedProTable'; import type { ToolbarAction } from '@/components/EnhancedProTable/types'; import { formStatusType } from '@/constants'; +import { useMessage } from '@/hooks/antd/useMessage'; +import { CACHE_KEY, useCache } from '@/hooks/web/useCache'; import { createMenu, deleteMenu, @@ -24,10 +27,11 @@ import { baseMenuColumns, formColumns } from './config'; const SystemMenu = () => { const configurableDrawerRef = useRef(null); const tableRef = useRef(null); - + const { wsCache } = useCache(); + const message = useMessage(); // 消息弹窗 const [type, setType] = useState<'create' | 'update'>('create'); const [id, setId] = useState(0); - + const { initialState, setInitialState } = useModel('@@initialState'); const handleEdit = (record: MenuVO) => { setType('update'); setId(record.id); @@ -47,7 +51,19 @@ const SystemMenu = () => { }; const handleAdd = () => { setType('create'); - configurableDrawerRef.current?.open({}); + configurableDrawerRef.current?.open({ type: 1 }); + }; + + const handleReload = async () => { + try { + await message.confirm('即将更新缓存刷新浏览器!', '刷新菜单缓存'); + // 清空,从而触发刷新 + // wsCache.delete(CACHE_KEY.USER); + // wsCache.delete(CACHE_KEY.ROLE_ROUTERS); + await initialState?.fetchUserInfo?.(); + // 刷新浏览器 + location.reload(); + } catch {} }; const handleSubmit = useCallback( @@ -74,6 +90,13 @@ const SystemMenu = () => { icon: , onClick: handleAdd, }, + { + key: 'reload', + label: '刷新菜单缓存', + type: 'primary', + icon: , + onClick: handleReload, + }, ]; const actionColumns: ProColumns = { @@ -108,7 +131,7 @@ const SystemMenu = () => { columns={columns} request={onFetch} toolbarActions={toolbarActions} - headerTitle="租户列表" + headerTitle="菜单管理" showIndex={false} showSelection={false} /> @@ -118,6 +141,7 @@ const SystemMenu = () => { title={formStatusType[type]} columns={formColumns(type)} onSubmit={handleSubmit} + width={900} /> ); From b4d3535b919223dc894e955ce503ef5895da6575 Mon Sep 17 00:00:00 2001 From: qianpw <2233607957@qq.com> Date: Sat, 27 Sep 2025 15:52:40 +0800 Subject: [PATCH 3/4] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E5=AD=97?= =?UTF-8?q?=E5=85=B8=E6=96=B9=E6=B3=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app.tsx | 34 +- src/components/XTag/index.tsx | 89 ++++++ src/hooks/stores/dict.ts | 111 +++++++ src/pages/system/dict/config.tsx | 18 +- src/pages/system/dict/data/config.tsx | 6 +- src/pages/system/tenant/list/config.tsx | 24 +- src/services/prod/prod-manager/index.ts | 210 +++++++++++++ src/utils/color.ts | 174 +++++++++++ src/utils/common.ts | 396 ++++++++++++++++++++++++ src/utils/dict.ts | 263 ++++++++++++++++ 10 files changed, 1290 insertions(+), 35 deletions(-) create mode 100644 src/components/XTag/index.tsx create mode 100644 src/hooks/stores/dict.ts create mode 100644 src/services/prod/prod-manager/index.ts create mode 100644 src/utils/color.ts create mode 100644 src/utils/common.ts create mode 100644 src/utils/dict.ts diff --git a/src/app.tsx b/src/app.tsx index e216283..765c404 100644 --- a/src/app.tsx +++ b/src/app.tsx @@ -1,15 +1,9 @@ import type { Settings as LayoutSettings } from '@ant-design/pro-components'; import { SettingDrawer } from '@ant-design/pro-components'; import type { RequestConfig, RunTimeLayoutConfig } from '@umijs/max'; -import { history, Link, Navigate } from '@umijs/max'; -import { Modal, Spin } from 'antd'; -import React, { - Children, - Component, - createContext, - JSX, - Suspense, -} from 'react'; +import { history, Navigate } from '@umijs/max'; +import { Spin } from 'antd'; +import React from 'react'; import { AvatarDropdown, AvatarName, @@ -18,17 +12,14 @@ import { SelectLang, } from '@/components'; import { getInfo } from '@/services/login'; -import type { TokenType, UserInfoVO, UserVO } from '@/services/login/types'; +import type { UserInfoVO } from '@/services/login/types'; import defaultSettings from '../config/defaultSettings'; import { errorConfig } from './requestErrorConfig'; import '@ant-design/v5-patch-for-react-19'; -import { getAccessToken, getRefreshToken, getTenantId } from '@/utils/auth'; -import { - transformBackendMenuToFlatRoutes, - transformMenuToRoutes, -} from '@/utils/menuUtils'; + +import { useDictStore } from '@/hooks/stores/dict'; +import { getAccessToken, getTenantId } from '@/utils/auth'; import { CACHE_KEY, useCache } from './hooks/web/useCache'; -import { MenuVO } from './services/system/menu'; const isDev = process.env.NODE_ENV === 'development'; const isDevOrTest = isDev || process.env.CI; @@ -46,6 +37,7 @@ export async function getInitialState(): Promise<{ fetchUserInfo?: () => Promise; }> { const { wsCache } = useCache(); + const dictStore = useDictStore(); const fetchUserInfo = async () => { // history.push(loginPath); try { @@ -57,6 +49,10 @@ export async function getInitialState(): Promise<{ wsCache.set(CACHE_KEY.USER, data); wsCache.set(CACHE_KEY.ROLE_ROUTERS, data.menus); + if (!dictStore.getIsSetDict) { + await dictStore.setDictMap(); + } + // 转换菜单格式 return data; @@ -198,7 +194,7 @@ export const request: RequestConfig = { }; // 如果有token,则添加Authorization头 if (token) { - headers['Authorization'] = `Bearer ${getAccessToken()}`; + headers.Authorization = `Bearer ${getAccessToken()}`; } return { url, options: { ...options, headers } }; }, @@ -241,7 +237,7 @@ export function patchClientRoutes({ routes }: { routes: any }) { const parentId = routes[routerIndex].id; if (globalMenus) { - routes[routerIndex]['routes'].push(...loopMenuItem(globalMenus, parentId)); + routes[routerIndex].routes.push(...loopMenuItem(globalMenus, parentId)); } } @@ -308,7 +304,7 @@ const findFirstLeafRoute = (menuItem: any, parent = '/'): string | null => { // 递归查找第一个叶子节点 for (const child of menuItem.children) { - const leafRoute = findFirstLeafRoute(child, menuItem.path + '/'); + const leafRoute = findFirstLeafRoute(child, `${menuItem.path}/`); if (leafRoute) { return leafRoute; } diff --git a/src/components/XTag/index.tsx b/src/components/XTag/index.tsx new file mode 100644 index 0000000..12a6bfd --- /dev/null +++ b/src/components/XTag/index.tsx @@ -0,0 +1,89 @@ +import type { TagProps } from 'antd'; +import { Tag } from 'antd'; +import React, { useMemo } from 'react'; +import { isHexColor } from '@/utils/color'; +import { isArray, isBoolean, isNumber, isString } from '@/utils/common'; +import type { DictDataType } from '@/utils/dict'; +import { getDictOptions } from '@/utils/dict'; + +interface DictTagProps { + type: string; + value: string | number | boolean | Array; + // 字符串分隔符 只有当 value 传入值为字符串时有效 + separator?: string; + // 每个 tag 之间的间隔,默认为 5px + gutter?: string; +} + +const DictTag: React.FC = ({ + type, + value, + separator = ',', + gutter = '5px', +}) => { + const valueArr = useMemo(() => { + // 1. 是 Number 类型和 Boolean 类型的情况 + if (isNumber(value) || isBoolean(value)) { + return [String(value)]; + } + // 2. 是字符串(进一步判断是否有包含分隔符号 -> separator) + else if (isString(value)) { + return value.split(separator); + } + // 3. 数组 + else if (isArray(value)) { + return value.map(String); + } + return []; + }, [value, separator]); + + // 解决自定义字典标签值为零时标签不渲染的问题 + if (!type || value === undefined || value === null || value === '') { + return null; + } + + const dictOptions = getDictOptions(type); + + return ( +
+ {dictOptions + .filter((dict: DictDataType) => valueArr.includes(String(dict.value))) + .map((dict: DictDataType) => { + // 处理 tag 类型 + let colorType: TagProps['color']; + if (dict.colorType !== 'primary' && dict.colorType !== 'default') { + colorType = dict.colorType || undefined; + } + + // 处理自定义颜色 + const customColor = + dict?.cssClass && isHexColor(dict?.cssClass) + ? dict?.cssClass + : undefined; + + return ( + + {dict.label} + + ); + })} +
+ ); +}; + +export default DictTag; diff --git a/src/hooks/stores/dict.ts b/src/hooks/stores/dict.ts new file mode 100644 index 0000000..938f4c5 --- /dev/null +++ b/src/hooks/stores/dict.ts @@ -0,0 +1,111 @@ +import { CACHE_KEY, useCache } from '@/hooks/web/useCache'; +import type { DictDataVO } from '@/services/system/dict/dict.data'; +import { getSimpleDictDataList } from '@/services/system/dict/dict.data'; + +export interface DictValueType { + value: any; + label: string; + clorType?: string; + cssClass?: string; +} + +export interface DictTypeType { + dictType: string; + dictValue: DictValueType[]; +} + +export interface DictState { + dictMap: Record; + isSetDict: boolean; +} + +export const useDictStore = () => { + const { wsCache } = useCache(); + const state: DictState = { + dictMap: {}, + isSetDict: false, + }; + + const getDictMap = (): Recordable => { + const dictMap = wsCache.get(CACHE_KEY.DICT_CACHE); + if (dictMap) { + state.dictMap = dictMap; + } + return state.dictMap; + }; + + const getIsSetDict = (): boolean => { + return state.isSetDict; + }; + + const setDictMap = async () => { + const dictMap = wsCache.get(CACHE_KEY.DICT_CACHE); + if (dictMap) { + state.dictMap = dictMap; + state.isSetDict = true; + } else { + const res = await getSimpleDictDataList(); + // 设置数据 + const dictDataMap: Record = {}; + + res.forEach((dictData: DictDataVO) => { + // 获得 dictType 层级 + if (!dictDataMap[dictData.dictType]) { + dictDataMap[dictData.dictType] = []; + } + + // 处理 dictValue 层级 + dictDataMap[dictData.dictType].push({ + value: dictData.value, + label: dictData.label, + colorType: dictData.colorType, + cssClass: dictData.cssClass, + }); + }); + state.dictMap = dictDataMap; + state.isSetDict = true; + wsCache.set(CACHE_KEY.DICT_CACHE, dictDataMap, { exp: 60 }); // 60 秒 过期 + } + }; + + const getDictByType = (type: string) => { + if (!state.isSetDict) { + setDictMap(); + } + return state.dictMap[type]; + }; + + const resetDict = async () => { + wsCache.delete(CACHE_KEY.DICT_CACHE); + const res = await getSimpleDictDataList(); + // 设置数据 + const dictDataMap: Record = {}; + + res.forEach((dictData: DictDataVO) => { + // 获得 dictType 层级 + if (!dictDataMap[dictData.dictType]) { + dictDataMap[dictData.dictType] = []; + } + + // 处理 dictValue 层级 + dictDataMap[dictData.dictType].push({ + value: dictData.value, + label: dictData.label, + colorType: dictData.colorType, + cssClass: dictData.cssClass, + }); + }); + state.dictMap = dictDataMap; + state.isSetDict = true; + wsCache.set(CACHE_KEY.DICT_CACHE, dictDataMap, { exp: 60 }); // 60 秒 过期 + }; + + return { + state, + getDictMap, + getIsSetDict, + setDictMap, + getDictByType, + resetDict, + }; +}; diff --git a/src/pages/system/dict/config.tsx b/src/pages/system/dict/config.tsx index 3e90129..bf05638 100644 --- a/src/pages/system/dict/config.tsx +++ b/src/pages/system/dict/config.tsx @@ -2,10 +2,12 @@ import type { ProColumns, ProFormColumnsType, } from '@ant-design/pro-components'; -import { Tag } from 'antd'; + import dayjs from 'dayjs'; +import DictTag from '@/components/XTag'; import { tenantStatus } from '@/constants'; import type { DictTypeVO } from '@/services/system/dict/dict.type'; +import { DICT_TYPE } from '@/utils/dict'; export const baseDictTypeColumns: ProColumns[] = [ { @@ -29,14 +31,12 @@ export const baseDictTypeColumns: ProColumns[] = [ title: '状态', dataIndex: 'status', width: 100, - render: (_, record) => ( - - {record.status === 1 ? '正常' : '禁用'} - - ), + valueType: 'select', + render: (_, record) => { + return ( + + ); + }, }, { title: '备注', diff --git a/src/pages/system/dict/data/config.tsx b/src/pages/system/dict/data/config.tsx index 2eeb93c..30bf79d 100644 --- a/src/pages/system/dict/data/config.tsx +++ b/src/pages/system/dict/data/config.tsx @@ -47,6 +47,7 @@ export const baseDictDataColumns: ProColumns[] = [ }, { title: 'CSS Class', + dataIndex: 'cssClass', width: 120, }, @@ -133,11 +134,11 @@ export const formColumns = (_type: string): ProFormColumnsType[] => [ options: [ { label: '启用', - value: 1, + value: 0, }, { label: '禁用', - value: 0, + value: 1, }, ], }, @@ -181,6 +182,7 @@ export const formColumns = (_type: string): ProFormColumnsType[] => [ { title: 'CSS Class', dataIndex: 'cssClass', + tooltip: '输入rgb颜色值或#16进制颜色值', }, { title: '备注', diff --git a/src/pages/system/tenant/list/config.tsx b/src/pages/system/tenant/list/config.tsx index 3284ef2..8c05f4a 100644 --- a/src/pages/system/tenant/list/config.tsx +++ b/src/pages/system/tenant/list/config.tsx @@ -2,10 +2,10 @@ import type { ProColumns, ProFormColumnsType, } from '@ant-design/pro-components'; -import { DatePicker, Modal, Popconfirm } from 'antd'; -import { FormInstance } from 'antd/lib'; +import { Tag } from 'antd'; + import dayjs from 'dayjs'; -import { deleteTenant, type TenantVO } from '@/services/system/tenant/list'; +import type { TenantVO } from '@/services/system/tenant/list'; import { getTenantPackageList } from '@/services/system/tenant/package'; export const baseTenantColumns: ProColumns[] = [ @@ -29,8 +29,22 @@ export const baseTenantColumns: ProColumns[] = [ request: async () => { const packageList: { id: number; name: string }[] = await getTenantPackageList(); - packageList.map((item) => ({ label: item.name, value: item.id })); - return packageList.map((item) => ({ label: item.name, value: item.id })); + const newData = packageList.map((item) => ({ + label: item.name, + value: item.id, + })); + const defData = [{ value: 0, label: '系统' }]; + return [...defData, ...newData]; + }, + render: (dom, record) => { + return ( + + {dom} + + ); }, // valueEnum: { // all: { text: "全部", status: "Default" }, diff --git a/src/services/prod/prod-manager/index.ts b/src/services/prod/prod-manager/index.ts new file mode 100644 index 0000000..e5c8ee7 --- /dev/null +++ b/src/services/prod/prod-manager/index.ts @@ -0,0 +1,210 @@ +export interface Prod extends PageParam { + /** + * 商品简称 + */ + abbreviation?: string; + /** + * 是否特殊日期(节假日周末什么的)0关1开 + */ + additionalFeeSwitch?: number; + /** + * 是否特殊时段0关1开 + */ + additionalSwitch?: number; + /** + * 品牌 + */ + brand?: string; + /** + * 简要描述,卖点等 + */ + brief?: string; + /** + * 商品分类 + */ + categoryId?: number; + /** + * 详细描述 + */ + content?: string; + /** + * 创建时间 + */ + createTime?: string; + /** + * 创建者,目前使用 SysUser 的 id 编号 + * + * 使用 String 类型的原因是,未来可能会存在非数值的情况,留好拓展性。 + */ + creator?: string; + /** + * 是否删除 + */ + deleted?: number; + /** + * 是否紧急响应服务0关1开 + */ + emergencySwitch?: number; + /** + * 商品轮播图片,以,分割 + */ + imgs?: string; + /** + * 是否置灰0否1是 + */ + isProhibit?: number; + /** + * 关键词 + */ + keyword?: string; + /** + * 是否接单上线0关1开 + */ + orderLimitSwitch?: number; + /** + * 商品主图 + */ + pic?: string; + /** + * 审核备注 + */ + processNotes?: string; + /** + * 产品ID + */ + prodId?: number; + /** + * 商品名称 + */ + prodName?: string; + /** + * 商品编号 + */ + prodNumber?: string; + /** + * 是否开启区域0关1开 + */ + regionSwitch?: number; + /** + * 是否预约0关1开 + */ + reservationSwitch?: number; + /** + * seo搜索 + */ + seoSearch?: string; + /** + * seo标题 + */ + seoShortName?: string; + /** + * 分享话术 + */ + shareContent?: string; + /** + * 分享图 + */ + shareImage?: string; + /** + * 店铺id + */ + shopId?: number; + /** + * 销量 + */ + soldNum?: number; + /** + * 默认是1,正常状态(出售中), 0:下架(仓库中) 2:待审核 + */ + status?: number; + /** + * 标签 + */ + tag?: string[]; + /** + * 展示的权重 + */ + top?: number; + /** + * 更新者,目前使用 SysUser 的 id 编号 + * + * 使用 String 类型的原因是,未来可能会存在非数值的情况,留好拓展性。 + */ + updater?: string; + /** + * 最后更新时间 + */ + updateTime?: string; + /** + * 版本 乐观锁 + */ + version?: number; + /** + * 视频 + */ + video?: string; + /** + * 是否开启体重配置0关1开 + */ + weightSwitch?: number; + /** + * 商品轮播图片,以,分割 + */ + whiteImg?: string; + [property: string]: any; +} + +export interface SkuList { + skuName: string; //规格名称 + basePrice?: number; //基准价格 * + price?: number; //"当前价" + isSpecs?: number; //默认规格 + stocksFlg?: number; //库存类型 + status?: number; //状态 + stocks?: number; //可售数量 + isShelf?: number; //上/下架 + skuId?: number; + isExist?: number; + propIds?: string; + deleteTime?: number; //删除时间 + remainingDays?: number; //保留时间 + id?: number; + // 添加索引签名,允许动态属性 + [key: string]: string | number | undefined; +} +export interface SkuPropValues { + id?: number; + propValue: string; + valueId: number; + state: 0 | 1; // 0禁用1启用 + isExist: 0 | 1; //是否新增 0否1是 + sort: number; + isExpire: 0 | 1 /**是否失效0否1是**/; +} +export interface SkuConfig { + propName: string; //规格名字 + propId: number; + id?: number; + isExist?: number; // //是否新增 0否1是 + prodPropValues: SkuPropValues[]; //规格值 +} + +export interface ProdDetail { + prodName: string; //商品名称 + abbreviation?: string; //商品简称 + brief?: string; //商品概述 + prodNumber?: string; //商品编码 + brand?: string; //品牌 + shopId?: string; //商品所有权 + categoryId?: number; //关联类目 + tag?: string[]; //商品标签 + skuList: SkuList[]; + prodPropSaveReqVO: SkuConfig; +} + +export interface ProdReq extends PageParam { + name?: string; + status?: 0 | 1 | 2; //状态 1,正常状态(出售中), 0:下架(仓库中) 2:待审核 + prodName?: string; // 商品名称 + createTime?: Date; //创建时间 +} diff --git a/src/utils/color.ts b/src/utils/color.ts new file mode 100644 index 0000000..e3097fd --- /dev/null +++ b/src/utils/color.ts @@ -0,0 +1,174 @@ +/** + * 判断是否 十六进制颜色值. + * 输入形式可为 #fff000 #f00 + * + * @param String color 十六进制颜色值 + * @return Boolean + */ +export const isHexColor = (color: string) => { + const reg = /^#([0-9a-fA-F]{3}|[0-9a-fA-f]{6})$/; + return reg.test(color); +}; + +/** + * RGB 颜色值转换为 十六进制颜色值. + * r, g, 和 b 需要在 [0, 255] 范围内 + * + * @return String 类似#ff00ff + * @param r + * @param g + * @param b + */ +export const rgbToHex = (r: number, g: number, b: number) => { + // tslint:disable-next-line:no-bitwise + const hex = ((r << 16) | (g << 8) | b).toString(16); + return `#${new Array(Math.abs(hex.length - 7)).join('0')}${hex}`; +}; + +/** + * Transform a HEX color to its RGB representation + * @param {string} hex The color to transform + * @returns The RGB representation of the passed color + */ +export const hexToRGB = (hex: string, opacity?: number) => { + let sHex = hex.toLowerCase(); + if (isHexColor(hex)) { + if (sHex.length === 4) { + let sColorNew = '#'; + for (let i = 1; i < 4; i += 1) { + sColorNew += sHex.slice(i, i + 1).concat(sHex.slice(i, i + 1)); + } + sHex = sColorNew; + } + const sColorChange: number[] = []; + for (let i = 1; i < 7; i += 2) { + sColorChange.push(parseInt(`0x${sHex.slice(i, i + 2)}`, 10)); + } + return opacity + ? `RGBA(${sColorChange.join(',')},${opacity})` + : `RGB(${sColorChange.join(',')})`; + } + return sHex; +}; + +export const colorIsDark = (color: string) => { + if (!isHexColor(color)) return; + const [r, g, b] = hexToRGB(color) + .replace(/(?:\(|\)|rgb|RGB)*/g, '') + .split(',') + .map((item) => Number(item)); + return r * 0.299 + g * 0.578 + b * 0.114 < 192; +}; + +/** + * Darkens a HEX color given the passed percentage + * @param {string} color The color to process + * @param {number} amount The amount to change the color by + * @returns {string} The HEX representation of the processed color + */ +export const darken = (color: string, amount: number) => { + color = color.indexOf('#') >= 0 ? color.substring(1, color.length) : color; + amount = Math.trunc((255 * amount) / 100); + return `#${subtractLight(color.substring(0, 2), amount)}${subtractLight( + color.substring(2, 4), + amount, + )}${subtractLight(color.substring(4, 6), amount)}`; +}; + +/** + * Lightens a 6 char HEX color according to the passed percentage + * @param {string} color The color to change + * @param {number} amount The amount to change the color by + * @returns {string} The processed color represented as HEX + */ +export const lighten = (color: string, amount: number) => { + color = color.indexOf('#') >= 0 ? color.substring(1, color.length) : color; + amount = Math.trunc((255 * amount) / 100); + return `#${addLight(color.substring(0, 2), amount)}${addLight( + color.substring(2, 4), + amount, + )}${addLight(color.substring(4, 6), amount)}`; +}; + +/* Suma el porcentaje indicado a un color (RR, GG o BB) hexadecimal para aclararlo */ +/** + * Sums the passed percentage to the R, G or B of a HEX color + * @param {string} color The color to change + * @param {number} amount The amount to change the color by + * @returns {string} The processed part of the color + */ +const addLight = (color: string, amount: number) => { + const cc = parseInt(color, 16) + amount; + const c = cc > 255 ? 255 : cc; + return c.toString(16).length > 1 ? c.toString(16) : `0${c.toString(16)}`; +}; + +/** + * Calculates luminance of an rgb color + * @param {number} r red + * @param {number} g green + * @param {number} b blue + */ +const luminanace = (r: number, g: number, b: number) => { + const a = [r, g, b].map((v) => { + v /= 255; + return v <= 0.03928 ? v / 12.92 : ((v + 0.055) / 1.055) ** 2.4; + }); + return a[0] * 0.2126 + a[1] * 0.7152 + a[2] * 0.0722; +}; + +/** + * Calculates contrast between two rgb colors + * @param {string} rgb1 rgb color 1 + * @param {string} rgb2 rgb color 2 + */ +const contrast = (rgb1: string[], rgb2: number[]) => { + return ( + (luminanace(~~rgb1[0], ~~rgb1[1], ~~rgb1[2]) + 0.05) / + (luminanace(rgb2[0], rgb2[1], rgb2[2]) + 0.05) + ); +}; + +/** + * Determines what the best text color is (black or white) based con the contrast with the background + * @param hexColor - Last selected color by the user + */ +export const calculateBestTextColor = (hexColor: string) => { + const rgbColor = hexToRGB(hexColor.substring(1)); + const contrastWithBlack = contrast(rgbColor.split(','), [0, 0, 0]); + + return contrastWithBlack >= 12 ? '#000000' : '#FFFFFF'; +}; + +/** + * Subtracts the indicated percentage to the R, G or B of a HEX color + * @param {string} color The color to change + * @param {number} amount The amount to change the color by + * @returns {string} The processed part of the color + */ +const subtractLight = (color: string, amount: number) => { + const cc = parseInt(color, 16) - amount; + const c = cc < 0 ? 0 : cc; + return c.toString(16).length > 1 ? c.toString(16) : `0${c.toString(16)}`; +}; + +// 预设颜色 +export const PREDEFINE_COLORS = [ + '#ff4500', + '#ff8c00', + '#ffd700', + '#90ee90', + '#00ced1', + '#1e90ff', + '#c71585', + '#409EFF', + '#909399', + '#C0C4CC', + '#b7390b', + '#ff7800', + '#fad400', + '#5b8c5f', + '#00babd', + '#1f73c3', + '#711f57', +]; diff --git a/src/utils/common.ts b/src/utils/common.ts new file mode 100644 index 0000000..473d61e --- /dev/null +++ b/src/utils/common.ts @@ -0,0 +1,396 @@ +import type { SkuConfig, SkuList } from '@/services/prod/prod-manager'; + +/** + * @param str 需要转驼峰的下划线字符串 + * @returns 字符串驼峰 + */ +export const underlineToHump = (str: string): string => { + if (!str) return ''; + return str.replace(/-(\w)/g, (_, letter: string) => { + return letter.toUpperCase(); + }); +}; + +export const setCssVar = ( + prop: string, + val: any, + dom = document.documentElement, +) => { + dom.style.setProperty(prop, val); +}; + +/** + * @param str 需要转下划线的驼峰字符串 + * @returns 字符串下划线 + */ +export const humpToUnderline = (str: string): string => { + return str.replace(/([A-Z])/g, '-$1').toLowerCase(); +}; + +// copy to vben-admin + +const objectToString = Object.prototype.toString; + +export const is = (val: unknown, type: string) => { + return objectToString.call(val) === `[object ${type}]`; +}; + +export const isDef = (val?: T): val is T => { + return typeof val !== 'undefined'; +}; + +export const isUnDef = (val?: T): val is T => { + return !isDef(val); +}; + +export const isObject = (val: any): val is Record => { + return val !== null && is(val, 'Object'); +}; + +export const isEmpty = (val: any): boolean => { + if (val === null || val === undefined || typeof val === 'undefined') { + return true; + } + if (isArray(val) || isString(val)) { + return val.length === 0; + } + + if (val instanceof Map || val instanceof Set) { + return val.size === 0; + } + + if (isObject(val)) { + return Object.keys(val).length === 0; + } + + return false; +}; + +export const isDate = (val: unknown): val is Date => { + return is(val, 'Date'); +}; + +export const isNull = (val: unknown): val is null => { + return val === null; +}; + +export const isNullAndUnDef = (val: unknown): val is null | undefined => { + return isUnDef(val) && isNull(val); +}; + +export const isNullOrUnDef = (val: unknown): val is null | undefined => { + return isUnDef(val) || isNull(val); +}; + +export const isNumber = (val: unknown): val is number => { + return is(val, 'Number'); +}; + +export const isPromise = (val: unknown): val is Promise => { + return ( + is(val, 'Promise') && + isObject(val) && + isFunction(val.then) && + isFunction(val.catch) + ); +}; + +export const isString = (val: unknown): val is string => { + return is(val, 'String'); +}; + +export const isFunction = (val: unknown): val is (...args: any[]) => any => { + return typeof val === 'function'; +}; + +export const isBoolean = (val: unknown): val is boolean => { + return is(val, 'Boolean'); +}; + +export const isRegExp = (val: unknown): val is RegExp => { + return is(val, 'RegExp'); +}; + +export const isArray = (val: any): val is Array => { + return val && Array.isArray(val); +}; + +export const isWindow = (val: any): val is Window => { + return typeof window !== 'undefined' && is(val, 'Window'); +}; + +export const isElement = (val: unknown): val is Element => { + return isObject(val) && !!val.tagName; +}; + +export const isMap = (val: unknown): val is Map => { + return is(val, 'Map'); +}; + +export const isServer = typeof window === 'undefined'; + +export const isClient = !isServer; + +export const isUrl = (path: string): boolean => { + // fix:修复hash路由无法跳转的问题 + const reg = + /(((^https?:(?:\/\/)?)(?:[-:&=+$,\w]+@)?[A-Za-z0-9.-]+(?::\d+)?|(?:www.|[-:&=+$,\w]+@)[A-Za-z0-9.-]+)((?:\/[+~%#/.\w-_]*)?\??(?:[-+=&%@.\w_]*)#?(?:[\w]*))?)$/; + return reg.test(path); +}; + +export const isDark = (): boolean => { + return window.matchMedia('(prefers-color-scheme: dark)').matches; +}; + +// 是否是图片链接 +export const isImgPath = (path: string): boolean => { + return /(https?:\/\/|data:image\/).*?\.(png|jpg|jpeg|gif|svg|webp|ico)/gi.test( + path, + ); +}; + +export const isEmptyVal = (val: any): boolean => { + return val === '' || val === null || val === undefined; +}; +export const isExternal = (path: string): boolean => { + return /https?/.test(path); +}; + +/** + * 查找数组对象的某个下标 + * @param {Array} ary 查找的数组 + * @param {Functon} fn 判断的方法 + */ +// eslint-disable-next-line +export const findIndex = (ary: Array, fn: Fn): number => { + if (ary.findIndex) { + return ary.findIndex(fn); + } + let index = -1; + ary.some((item: T, i: number, ary: Array) => { + const ret: T = fn(item, i, ary); + if (ret) { + index = i; + return true; + } + return false; + }); + return index; +}; + +// 笛卡尔积函数 + +function cartesianProduct(...arrays: T[][]): T[][] { + return arrays.reduce( + (acc, curr) => { + const result: T[][] = []; + acc.forEach((a) => { + curr.forEach((c) => { + result.push([...a, c]); + }); + }); + return result; + }, + [[]] as T[][], + ); +} + +export function generateSkuTableData(data: SkuConfig[]): SkuList[] { + // 过滤掉prodPropValues为空的配置 + const validData = data.filter((config) => config.prodPropValues.length > 0); + + // 如果所有配置都被过滤掉了,返回空数组 + if (validData.length === 0) { + return []; + } + + const propValueArrays = validData.map((config) => config.prodPropValues); + const combinations = cartesianProduct<{ + propValue: string; + valueId?: number; + state?: number; + isExist?: number; + }>(...propValueArrays); + + return combinations.map((combination, index) => { + const skuItem: SkuList = { + skuName: combination.map((item) => item.propValue).join(','), + basePrice: 0, + isSpecs: index === 0 ? 1 : 0, + status: 1, + stocks: 0, + isShelf: 1, + // propIds: combination.map((item) => item.valueId).join(",") + }; + + // 只添加有效配置的动态属性 + validData.forEach((config, configIndex) => { + (skuItem as any)[config.propName] = combination[configIndex].propValue; + }); + + return skuItem; + }); +} + +// 根据删除的SKU规格更新data状态 +function updateDataByDeletedSku( + data: SkuConfig[], + deletedSkuName: string, +): SkuConfig[] { + // 解析删除的SKU名称,获取各个属性值 + const deletedValues = deletedSkuName.split(',').map((v) => v.trim()); + + // 深拷贝data以避免修改原数据 + const updatedData = JSON.parse(JSON.stringify(data)) as SkuConfig[]; + + // 遍历每个属性配置 + updatedData.forEach((config, configIndex) => { + if (configIndex < deletedValues.length) { + const targetValue = deletedValues[configIndex]; + + // 找到对应的属性值并设置为失效 + config.prodPropValues.forEach( + (propValue: { + propValue: string; + state?: number; + isExist?: number; + }) => { + if (propValue.propValue === targetValue) { + propValue.state = 0; // 设置为失效 + propValue.isExist = 0; // 设置为不存在 + } + }, + ); + } + }); + + return updatedData; +} + +// 批量处理多个删除的SKU +function _updateDataByDeletedSkus( + data: SkuConfig[], + deletedSkuNames: string[], +): SkuConfig[] { + let updatedData = JSON.parse(JSON.stringify(data)) as SkuConfig[]; + + deletedSkuNames.forEach((skuName) => { + updatedData = updateDataByDeletedSku(updatedData, skuName); + }); + + return updatedData; +} + +// 根据属性名和属性值直接设置失效状态 +function _updateDataByPropValue( + data: SkuConfig[], + propName: string, + propValue: string, + state: 0 | 1 = 0, +): SkuConfig[] { + const updatedData = JSON.parse(JSON.stringify(data)) as SkuConfig[]; + + const targetConfig = updatedData.find( + (config) => config.propName === propName, + ); + if (targetConfig) { + const targetPropValue = targetConfig.prodPropValues.find( + (pv: { + propValue: string; + state?: number; + isExist?: number; + isExpire?: number; + }) => pv.propValue === propValue, + ); + if (targetPropValue) { + targetPropValue.state = state; + targetPropValue.isExist = state; + } + } + + return updatedData; +} + +// 重新生成有效的SKU列表(过滤掉失效的属性值) +export function generateValidSkuTableData(data: SkuConfig[]): SkuList[] { + console.log(data); + // 过滤出有效的属性值 + const validData = data + .map((config) => ({ + ...config, + prodPropValues: config.prodPropValues.filter( + (pv: { isExpire: number }) => pv.isExpire === 0, + ), + })) + .filter((config) => config.prodPropValues.length > 0); + if (validData.length === 0) { + return []; + } + + const propValueArrays = validData.map((config) => config.prodPropValues); + const combinations = cartesianProduct<{ + propValue: string; + valueId?: number; + state?: number; + isExist?: number; + isExpire?: number; + }>(...propValueArrays); + + return combinations.map((combination) => { + const skuItem: SkuList = { + skuName: combination.map((item) => item.propValue).join(','), + properties: combination.map((item) => item.propValue).join(','), + basePrice: 0, + isSpecs: 0, + status: 1, + stocks: 0, + stocksFlg: 1, + isShelf: 1, + isExist: combination.some((item) => item.isExist) ? 1 : 0, + }; + + validData.forEach((config, configIndex) => { + (skuItem as any)[config.propName] = combination[configIndex].propValue; + }); + + return skuItem; + }); +} + +export const generateUUID = () => { + if (typeof crypto === 'object') { + if (typeof crypto.randomUUID === 'function') { + return crypto.randomUUID(); + } + if ( + typeof crypto.getRandomValues === 'function' && + typeof Uint8Array === 'function' + ) { + const callback = (c: any) => { + const num = Number(c); + return ( + num ^ + (crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (num / 4))) + ).toString(16); + }; + return '10000000-1000-4000-8000-100000000000'.replace(/[018]/g, callback); + } + } + let timestamp = Date.now(); + let performanceNow = + (typeof performance !== 'undefined' && + performance.now && + performance.now() * 1000) || + 0; + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => { + let random = Math.random() * 16; + if (timestamp > 0) { + random = ((timestamp + random) % 16) | 0; + timestamp = Math.floor(timestamp / 16); + } else { + random = ((performanceNow + random) % 16) | 0; + performanceNow = Math.floor(performanceNow / 16); + } + return (c === 'x' ? random : (random & 0x3) | 0x8).toString(16); + }); +}; diff --git a/src/utils/dict.ts b/src/utils/dict.ts new file mode 100644 index 0000000..bcfa2f8 --- /dev/null +++ b/src/utils/dict.ts @@ -0,0 +1,263 @@ +/** + * 数据字典工具类 + */ +import { useDictStore } from '@/hooks/stores/dict'; + +export type AntDesignInfoType = + | 'default' + | 'primary' + | 'success' + | 'info' + | 'warning' + | 'danger' + | undefined; + +/** + * 获取 dictType 对应的数据字典数组 + * + * @param dictType 数据类型 + * @returns {*|Array} 数据字典数组 + */ +export interface DictDataType { + dictType: string; + label: string; + value: string | number | boolean; + colorType: AntDesignInfoType; + cssClass: string; +} + +export interface NumberDictDataType extends DictDataType { + value: number; +} + +export interface StringDictDataType extends DictDataType { + value: string; +} + +export const getDictOptions = (dictType: string) => { + const dictStore = useDictStore(); + return dictStore.getDictByType(dictType) || []; +}; + +export const getIntDictOptions = (dictType: string): NumberDictDataType[] => { + // 获得通用的 DictDataType 列表 + const dictOptions: DictDataType[] = getDictOptions(dictType); + // 转换成 number 类型的 NumberDictDataType 类型 + // why 需要特殊转换:避免 IDEA 在 v-for="dict in getIntDictOptions(...)" 时,el-option 的 key 会告警 + const dictOption: NumberDictDataType[] = []; + dictOptions.forEach((dict: DictDataType) => { + dictOption.push({ + ...dict, + value: parseInt(`${dict.value}`, 10), + }); + }); + return dictOption; +}; + +export const getStrDictOptions = (dictType: string) => { + // 获得通用的 DictDataType 列表 + const dictOptions: DictDataType[] = getDictOptions(dictType); + // 转换成 string 类型的 StringDictDataType 类型 + // why 需要特殊转换:避免 IDEA 在 v-for="dict in getStrDictOptions(...)" 时,el-option 的 key 会告警 + const dictOption: StringDictDataType[] = []; + dictOptions.forEach((dict: DictDataType) => { + dictOption.push({ + ...dict, + value: `${dict.value}`, + }); + }); + return dictOption; +}; + +export const getBoolDictOptions = (dictType: string) => { + const dictOption: DictDataType[] = []; + const dictOptions: DictDataType[] = getDictOptions(dictType); + dictOptions.forEach((dict: DictDataType) => { + dictOption.push({ + ...dict, + value: `${dict.value}` === 'true', + }); + }); + return dictOption; +}; + +/** + * 获取指定字典类型的指定值对应的字典对象 + * @param dictType 字典类型 + * @param value 字典值 + * @return DictDataType 字典对象 + */ +export const getDictObj = ( + dictType: string, + value: any, +): DictDataType | undefined => { + const dictOptions: DictDataType[] = getDictOptions(dictType); + for (const dict of dictOptions) { + if (dict.value === `${value}`) { + return dict; + } + } + return undefined; +}; + +/** + * 获得字典数据的文本展示 + * + * @param dictType 字典类型 + * @param value 字典数据的值 + * @return 字典名称 + */ +export const getDictLabel = (dictType: string, value: any): string => { + const dictOptions: DictDataType[] = getDictOptions(dictType); + let dictLabel = ''; + dictOptions.forEach((dict: DictDataType) => { + if (dict.value === `${value}`) { + dictLabel = dict.label; + } + }); + return dictLabel; +}; + +export enum DICT_TYPE { + USER_TYPE = 'user_type', + COMMON_STATUS = 'common_status', + TERMINAL = 'terminal', // 终端 + DATE_INTERVAL = 'date_interval', // 数据间隔 + + // ========== 产品 模块 ========== + PROD_STATUS = 'prod_status', + CATEGORY_STATUS = 'category_status', + // ========== SYSTEM 模块 ========== + SYSTEM_USER_SEX = 'system_user_sex', + SYSTEM_MENU_TYPE = 'system_menu_type', + SYSTEM_ROLE_TYPE = 'system_role_type', + SYSTEM_DATA_SCOPE = 'system_data_scope', + SYSTEM_NOTICE_TYPE = 'system_notice_type', + SYSTEM_LOGIN_TYPE = 'system_login_type', + SYSTEM_LOGIN_RESULT = 'system_login_result', + SYSTEM_SMS_CHANNEL_CODE = 'system_sms_channel_code', + SYSTEM_SMS_TEMPLATE_TYPE = 'system_sms_template_type', + SYSTEM_SMS_SEND_STATUS = 'system_sms_send_status', + SYSTEM_SMS_RECEIVE_STATUS = 'system_sms_receive_status', + SYSTEM_OAUTH2_GRANT_TYPE = 'system_oauth2_grant_type', + SYSTEM_MAIL_SEND_STATUS = 'system_mail_send_status', + SYSTEM_NOTIFY_TEMPLATE_TYPE = 'system_notify_template_type', + SYSTEM_SOCIAL_TYPE = 'system_social_type', + + // ========== INFRA 模块 ========== + INFRA_BOOLEAN_STRING = 'infra_boolean_string', + INFRA_JOB_STATUS = 'infra_job_status', + INFRA_JOB_LOG_STATUS = 'infra_job_log_status', + INFRA_API_ERROR_LOG_PROCESS_STATUS = 'infra_api_error_log_process_status', + INFRA_CONFIG_TYPE = 'infra_config_type', + INFRA_CODEGEN_TEMPLATE_TYPE = 'infra_codegen_template_type', + INFRA_CODEGEN_FRONT_TYPE = 'infra_codegen_front_type', + INFRA_CODEGEN_SCENE = 'infra_codegen_scene', + INFRA_FILE_STORAGE = 'infra_file_storage', + INFRA_OPERATE_TYPE = 'infra_operate_type', + + // ========== BPM 模块 ========== + BPM_MODEL_TYPE = 'bpm_model_type', + BPM_MODEL_FORM_TYPE = 'bpm_model_form_type', + BPM_TASK_CANDIDATE_STRATEGY = 'bpm_task_candidate_strategy', + BPM_PROCESS_INSTANCE_STATUS = 'bpm_process_instance_status', + BPM_TASK_STATUS = 'bpm_task_status', + BPM_OA_LEAVE_TYPE = 'bpm_oa_leave_type', + BPM_PROCESS_LISTENER_TYPE = 'bpm_process_listener_type', + BPM_PROCESS_LISTENER_VALUE_TYPE = 'bpm_process_listener_value_type', + + // ========== PAY 模块 ========== + PAY_CHANNEL_CODE = 'pay_channel_code', // 支付渠道编码类型 + PAY_ORDER_STATUS = 'pay_order_status', // 商户支付订单状态 + PAY_REFUND_STATUS = 'pay_refund_status', // 退款订单状态 + PAY_NOTIFY_STATUS = 'pay_notify_status', // 商户支付回调状态 + PAY_NOTIFY_TYPE = 'pay_notify_type', // 商户支付回调状态 + PAY_TRANSFER_STATUS = 'pay_transfer_status', // 转账订单状态 + PAY_TRANSFER_TYPE = 'pay_transfer_type', // 转账订单状态 + + // ========== MP 模块 ========== + MP_AUTO_REPLY_REQUEST_MATCH = 'mp_auto_reply_request_match', // 自动回复请求匹配类型 + MP_MESSAGE_TYPE = 'mp_message_type', // 消息类型 + + // ========== Member 会员模块 ========== + MEMBER_POINT_BIZ_TYPE = 'member_point_biz_type', // 积分的业务类型 + MEMBER_EXPERIENCE_BIZ_TYPE = 'member_experience_biz_type', // 会员经验业务类型 + + // ========== MALL - 商品模块 ========== + PRODUCT_SPU_STATUS = 'product_spu_status', //商品状态 + + // ========== MALL - 交易模块 ========== + EXPRESS_CHARGE_MODE = 'trade_delivery_express_charge_mode', //快递的计费方式 + TRADE_AFTER_SALE_STATUS = 'trade_after_sale_status', // 售后 - 状态 + TRADE_AFTER_SALE_WAY = 'trade_after_sale_way', // 售后 - 方式 + TRADE_AFTER_SALE_TYPE = 'trade_after_sale_type', // 售后 - 类型 + TRADE_ORDER_TYPE = 'trade_order_type', // 订单 - 类型 + TRADE_ORDER_STATUS = 'trade_order_status', // 订单 - 状态 + TRADE_ORDER_ITEM_AFTER_SALE_STATUS = 'trade_order_item_after_sale_status', // 订单项 - 售后状态 + TRADE_DELIVERY_TYPE = 'trade_delivery_type', // 配送方式 + BROKERAGE_ENABLED_CONDITION = 'brokerage_enabled_condition', // 分佣模式 + BROKERAGE_BIND_MODE = 'brokerage_bind_mode', // 分销关系绑定模式 + BROKERAGE_BANK_NAME = 'brokerage_bank_name', // 佣金提现银行 + BROKERAGE_WITHDRAW_TYPE = 'brokerage_withdraw_type', // 佣金提现类型 + BROKERAGE_RECORD_BIZ_TYPE = 'brokerage_record_biz_type', // 佣金业务类型 + BROKERAGE_RECORD_STATUS = 'brokerage_record_status', // 佣金状态 + BROKERAGE_WITHDRAW_STATUS = 'brokerage_withdraw_status', // 佣金提现状态 + + // ========== MALL - 营销模块 ========== + PROMOTION_DISCOUNT_TYPE = 'promotion_discount_type', // 优惠类型 + PROMOTION_PRODUCT_SCOPE = 'promotion_product_scope', // 营销的商品范围 + PROMOTION_COUPON_TEMPLATE_VALIDITY_TYPE = 'promotion_coupon_template_validity_type', // 优惠劵模板的有限期类型 + PROMOTION_COUPON_STATUS = 'promotion_coupon_status', // 优惠劵的状态 + PROMOTION_COUPON_TAKE_TYPE = 'promotion_coupon_take_type', // 优惠劵的领取方式 + PROMOTION_CONDITION_TYPE = 'promotion_condition_type', // 营销的条件类型枚举 + PROMOTION_BARGAIN_RECORD_STATUS = 'promotion_bargain_record_status', // 砍价记录的状态 + PROMOTION_COMBINATION_RECORD_STATUS = 'promotion_combination_record_status', // 拼团记录的状态 + PROMOTION_BANNER_POSITION = 'promotion_banner_position', // banner 定位 + + // ========== CRM - 客户管理模块 ========== + CRM_AUDIT_STATUS = 'crm_audit_status', // CRM 审批状态 + CRM_BIZ_TYPE = 'crm_biz_type', // CRM 业务类型 + CRM_BUSINESS_END_STATUS_TYPE = 'crm_business_end_status_type', // CRM 商机结束状态类型 + CRM_RECEIVABLE_RETURN_TYPE = 'crm_receivable_return_type', // CRM 回款的还款方式 + CRM_CUSTOMER_INDUSTRY = 'crm_customer_industry', // CRM 客户所属行业 + CRM_CUSTOMER_LEVEL = 'crm_customer_level', // CRM 客户级别 + CRM_CUSTOMER_SOURCE = 'crm_customer_source', // CRM 客户来源 + CRM_PRODUCT_STATUS = 'crm_product_status', // CRM 商品状态 + CRM_PERMISSION_LEVEL = 'crm_permission_level', // CRM 数据权限的级别 + CRM_PRODUCT_UNIT = 'crm_product_unit', // CRM 产品单位 + CRM_FOLLOW_UP_TYPE = 'crm_follow_up_type', // CRM 跟进方式 + + // ========== ERP - 企业资源计划模块 ========== + ERP_AUDIT_STATUS = 'erp_audit_status', // ERP 审批状态 + ERP_STOCK_RECORD_BIZ_TYPE = 'erp_stock_record_biz_type', // 库存明细的业务类型 + + // ========== AI - 人工智能模块 ========== + AI_PLATFORM = 'ai_platform', // AI 平台 + AI_MODEL_TYPE = 'ai_model_type', // AI 模型类型 + AI_IMAGE_STATUS = 'ai_image_status', // AI 图片状态 + AI_MUSIC_STATUS = 'ai_music_status', // AI 音乐状态 + AI_GENERATE_MODE = 'ai_generate_mode', // AI 生成模式 + AI_WRITE_TYPE = 'ai_write_type', // AI 写作类型 + AI_WRITE_LENGTH = 'ai_write_length', // AI 写作长度 + AI_WRITE_FORMAT = 'ai_write_format', // AI 写作格式 + AI_WRITE_TONE = 'ai_write_tone', // AI 写作语气 + AI_WRITE_LANGUAGE = 'ai_write_language', // AI 写作语言 + + // ========== IOT - 物联网模块 ========== + IOT_NET_TYPE = 'iot_net_type', // IOT 联网方式 + IOT_VALIDATE_TYPE = 'iot_validate_type', // IOT 数据校验级别 + IOT_PRODUCT_STATUS = 'iot_product_status', // IOT 产品状态 + IOT_PRODUCT_DEVICE_TYPE = 'iot_product_device_type', // IOT 产品设备类型 + IOT_DATA_FORMAT = 'iot_data_format', // IOT 数据格式 + IOT_PROTOCOL_TYPE = 'iot_protocol_type', // IOT 接入网关协议 + IOT_DEVICE_STATE = 'iot_device_state', // IOT 设备状态 + IOT_THING_MODEL_TYPE = 'iot_thing_model_type', // IOT 产品功能类型 + IOT_DATA_TYPE = 'iot_data_type', // IOT 数据类型 + IOT_THING_MODEL_UNIT = 'iot_thing_model_unit', // IOT 物模型单位 + IOT_RW_TYPE = 'iot_rw_type', // IOT 读写类型 + IOT_PLUGIN_DEPLOY_TYPE = 'iot_plugin_deploy_type', // IOT 插件部署类型 + IOT_PLUGIN_STATUS = 'iot_plugin_status', // IOT 插件状态 + IOT_PLUGIN_TYPE = 'iot_plugin_type', // IOT 插件类型 + IOT_DATA_BRIDGE_DIRECTION_ENUM = 'iot_data_bridge_direction_enum', // 桥梁方向 + IOT_DATA_BRIDGE_TYPE_ENUM = 'iot_data_bridge_type_enum', // 桥梁类型 +} From 2821e864dab06031eee53b3da4579d4ad96d50e9 Mon Sep 17 00:00:00 2001 From: qianpw <2233607957@qq.com> Date: Fri, 17 Oct 2025 16:50:13 +0800 Subject: [PATCH 4/4] =?UTF-8?q?feat:=20add=E5=AD=97=E5=85=B8=E7=AE=A1?= =?UTF-8?q?=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/proxy.ts | 3 ++- src/components/XTag/index.tsx | 12 +++++++---- src/constants/index.ts | 7 +++++++ src/pages/ai/sample-tag/index.tsx | 2 +- src/pages/system/dict/data/config.tsx | 18 ++++++++++------ src/pages/system/menu/config.tsx | 30 +++++++++++++++++++++------ 6 files changed, 54 insertions(+), 18 deletions(-) diff --git a/config/proxy.ts b/config/proxy.ts index e1a47c0..8a70bb6 100644 --- a/config/proxy.ts +++ b/config/proxy.ts @@ -15,7 +15,8 @@ export default { // localhost:8000/api/** -> https://preview.pro.ant.design/api/** '/admin-api/': { // 要代理的地址 - target: 'http://114.132.60.20:48080', + // target: 'http://114.132.60.20:48080', + target: 'http://192.168.1.231:48080', // 配置了这个可以从 http 代理到 https // 依赖 origin 的功能可能需要这个,比如 cookie changeOrigin: true, diff --git a/src/components/XTag/index.tsx b/src/components/XTag/index.tsx index 12a6bfd..b86e189 100644 --- a/src/components/XTag/index.tsx +++ b/src/components/XTag/index.tsx @@ -58,10 +58,11 @@ const DictTag: React.FC = ({ .filter((dict: DictDataType) => valueArr.includes(String(dict.value))) .map((dict: DictDataType) => { // 处理 tag 类型 - let colorType: TagProps['color']; - if (dict.colorType !== 'primary' && dict.colorType !== 'default') { - colorType = dict.colorType || undefined; - } + const colorType: TagProps['color'] = dict.colorType || undefined; + + // if (dict.colorType !== "primary" && dict.colorType !== "default") { + // colorType = dict.colorType || undefined; + // } // 处理自定义颜色 const customColor = @@ -69,6 +70,9 @@ const DictTag: React.FC = ({ ? dict?.cssClass : undefined; + console.log(customColor, 'customColor'); + console.log(colorType, 'colorType'); + return ( [] = [ title: '字典标签', dataIndex: 'label', width: 120, + render: (_, record) => ( + {record.label} + ), }, { title: '字典键值', @@ -44,6 +47,9 @@ export const baseDictDataColumns: ProColumns[] = [ title: '颜色类型', dataIndex: 'colorType', width: 80, + render: (_, record) => ( + {record.colorType} + ), }, { title: 'CSS Class', @@ -157,23 +163,23 @@ export const formColumns = (_type: string): ProFormColumnsType[] => [ fieldProps: { options: [ { - label: '默认(waiting)', - value: 'waiting', + label: 默认(default), + value: 'default', }, { - label: '成功(success)', + label: 成功(success), value: 'success', }, { - label: '信息(processing)', + label: 信息(processing), value: 'processing', }, { - label: '失败(error)', + label: 失败(error), value: 'error', }, { - label: '警告(warning)', + label: 警告(warning), value: 'warning', }, ], diff --git a/src/pages/system/menu/config.tsx b/src/pages/system/menu/config.tsx index a1d591b..9ec61f1 100644 --- a/src/pages/system/menu/config.tsx +++ b/src/pages/system/menu/config.tsx @@ -6,9 +6,23 @@ import { ProFormText, } from '@ant-design/pro-components'; import { Switch } from 'antd'; -import { getSimpleMenusList, type MenuVO } from '@/services/system/menu'; +import { CommonStatusEnum } from '@/constants'; +import { useMessage } from '@/hooks/antd/useMessage'; +import { + getSimpleMenusList, + type MenuVO, + updateMenu, +} from '@/services/system/menu'; import { handleTree } from '@/utils/tree'; +const handleStatus = async (record: MenuVO) => { + const message = useMessage(); // 消息弹窗 + const text = record.status === CommonStatusEnum.ENABLE ? '停用' : '启用'; + await message.confirm(`确认要"${text}""${record.name}"菜单吗?`); + const status = record.status === 0 ? 1 : 0; + await updateMenu({ ...record, status }); +}; + export const baseMenuColumns: ProColumns[] = [ { title: '菜单名称', @@ -54,10 +68,13 @@ export const baseMenuColumns: ProColumns[] = [ title: '状态', dataIndex: 'status', width: 80, - render: (_, record: MenuVO) => ( + render: (_dom, record: MenuVO, _: any, action: any) => ( { + await handleStatus(record); + action?.reload(); + }} + checked={record.status === 0} /> ), }, @@ -204,6 +221,7 @@ export const formColumns = (_type: string): ProFormColumnsType[] => [ renderFormItem: (_schema, _config, form) => { const type = form.getFieldValue('type'); if (type !== 2) return null; + return ( [ valueType: 'radio', fieldProps: { options: [ - { label: '启用', value: 1 }, - { label: '禁用', value: 0 }, + { label: '启用', value: 0 }, + { label: '禁用', value: 1 }, ], }, },