feat: 类目管理
This commit is contained in:
100
src/app.tsx
100
src/app.tsx
@@ -1,34 +1,29 @@
|
||||
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, Navigate } from '@umijs/max';
|
||||
import { Spin } from 'antd';
|
||||
import React from 'react';
|
||||
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 } from "@umijs/max";
|
||||
import {
|
||||
AvatarDropdown,
|
||||
AvatarName,
|
||||
Footer,
|
||||
Question,
|
||||
SelectLang,
|
||||
} from '@/components';
|
||||
import { getInfo } from '@/services/login';
|
||||
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 { useDictStore } from '@/hooks/stores/dict';
|
||||
import { getAccessToken, getTenantId } from '@/utils/auth';
|
||||
import { CACHE_KEY, useCache } from './hooks/web/useCache';
|
||||
import type { MenuVO } from './services/system/menu';
|
||||
import { loopMenuItem } from './utils/menuUtils';
|
||||
} from "@/components";
|
||||
import { getInfo } from "@/services/login";
|
||||
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 { useDictStore } from "@/hooks/stores/dict";
|
||||
import { getAccessToken, getTenantId } from "@/utils/auth";
|
||||
import { CACHE_KEY, useCache } from "./hooks/web/useCache";
|
||||
import type { MenuVO } from "./services/system/menu";
|
||||
import { loopMenuItem } from "./utils/menuUtils";
|
||||
|
||||
const isDev = process.env.NODE_ENV === 'development';
|
||||
const isDev = process.env.NODE_ENV === "development";
|
||||
const isDevOrTest = isDev || process.env.CI;
|
||||
const loginPath = '/user/login';
|
||||
const loginPath = "/user/login";
|
||||
|
||||
// 全局存储菜单数据和路由映射
|
||||
|
||||
const dynamicRoutesAdded = false;
|
||||
// 标记是否已添加动态路由
|
||||
/**
|
||||
* @see https://umijs.org/docs/api/runtime-config#getinitialstate
|
||||
@@ -47,7 +42,7 @@ export async function getInitialState(): Promise<{
|
||||
try {
|
||||
const token = getAccessToken();
|
||||
if (!token) {
|
||||
throw new Error('No token found');
|
||||
throw new Error("No token found");
|
||||
}
|
||||
const data = await getInfo();
|
||||
wsCache.set(CACHE_KEY.USER, data);
|
||||
@@ -69,8 +64,8 @@ export async function getInitialState(): Promise<{
|
||||
const { location } = history;
|
||||
|
||||
if (
|
||||
![loginPath, '/user/register', '/user/register-result'].includes(
|
||||
location.pathname,
|
||||
![loginPath, "/user/register", "/user/register-result"].includes(
|
||||
location.pathname
|
||||
)
|
||||
) {
|
||||
const currentUser = wsCache.get(CACHE_KEY.USER);
|
||||
@@ -127,22 +122,22 @@ export const layout: RunTimeLayoutConfig = ({
|
||||
},
|
||||
bgLayoutImgList: [
|
||||
{
|
||||
src: 'https://mdn.alipayobjects.com/yuyan_qk0oxh/afts/img/D2LWSqNny4sAAAAAAAAAAAAAFl94AQBr',
|
||||
src: "https://mdn.alipayobjects.com/yuyan_qk0oxh/afts/img/D2LWSqNny4sAAAAAAAAAAAAAFl94AQBr",
|
||||
left: 85,
|
||||
bottom: 100,
|
||||
height: '303px',
|
||||
height: "303px",
|
||||
},
|
||||
{
|
||||
src: 'https://mdn.alipayobjects.com/yuyan_qk0oxh/afts/img/C2TWRpJpiC0AAAAAAAAAAAAAFl94AQBr',
|
||||
src: "https://mdn.alipayobjects.com/yuyan_qk0oxh/afts/img/C2TWRpJpiC0AAAAAAAAAAAAAFl94AQBr",
|
||||
bottom: -68,
|
||||
right: -45,
|
||||
height: '303px',
|
||||
height: "303px",
|
||||
},
|
||||
{
|
||||
src: 'https://mdn.alipayobjects.com/yuyan_qk0oxh/afts/img/F6vSTbj8KpYAAAAAAAAAAAAAFl94AQBr',
|
||||
src: "https://mdn.alipayobjects.com/yuyan_qk0oxh/afts/img/F6vSTbj8KpYAAAAAAAAAAAAAFl94AQBr",
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
width: '331px',
|
||||
width: "331px",
|
||||
},
|
||||
],
|
||||
menuHeaderRender: undefined,
|
||||
@@ -180,30 +175,57 @@ export const layout: RunTimeLayoutConfig = ({
|
||||
* @doc https://umijs.org/docs/max/request#配置
|
||||
*/
|
||||
export const request: RequestConfig = {
|
||||
baseURL: isDev ? '' : 'https://proapi.azurewebsites.net',
|
||||
baseURL: isDev ? "" : "https://proapi.azurewebsites.net",
|
||||
...errorConfig,
|
||||
// 添加请求拦截器
|
||||
requestInterceptors: [
|
||||
(url, options) => {
|
||||
// 为所有请求添加 API 前缀
|
||||
if (url && !url.startsWith(process.env.API_PREFIX || '/admin-api')) {
|
||||
url = (process.env.API_PREFIX || '/admin-api') + url;
|
||||
if (url && !url.startsWith(process.env.API_PREFIX || "/admin-api")) {
|
||||
url = (process.env.API_PREFIX || "/admin-api") + url;
|
||||
}
|
||||
// 获取存储在本地的 token 和 tenantId
|
||||
const token = getAccessToken();
|
||||
const tenantId = getTenantId(); // 默认租户ID为1
|
||||
const contentType: string = options.headers?.["Content-Type"] as string;
|
||||
// 设置统一的请求头
|
||||
const headers: Record<string, string> = {
|
||||
...options.headers,
|
||||
Accept: '*',
|
||||
'tenant-id': tenantId,
|
||||
Accept: "*",
|
||||
"tenant-id": tenantId,
|
||||
};
|
||||
// 如果有token,则添加Authorization头
|
||||
if (token) {
|
||||
headers.Authorization = `Bearer ${getAccessToken()}`;
|
||||
headers["Content-Type"] = contentType || "application/json";
|
||||
}
|
||||
return { url, options: { ...options, headers } };
|
||||
},
|
||||
// requestInterceptors: [
|
||||
// (url, options) => {
|
||||
// // 为所有请求添加 API 前缀
|
||||
// if (url && !url.startsWith(process.env.API_PREFIX || "/admin-api")) {
|
||||
// url = (process.env.API_PREFIX || "/admin-api") + url;
|
||||
// }
|
||||
// // 获取存储在本地的 token 和 tenantId
|
||||
// const token = getAccessToken();
|
||||
// const tenantId = getTenantId(); // 默认租户ID为1
|
||||
// console.log("request", options);
|
||||
// // 设置统一的请求头
|
||||
// const contentType: string = options.headers?.["Content-Type"] as string;
|
||||
// const headers: Record<string, string> = {
|
||||
// ...options.headers,
|
||||
// Accept: "*",
|
||||
// "Content-Type": contentType || "application/json",
|
||||
// "tenant-id": tenantId,
|
||||
// };
|
||||
// // 如果有token,则添加Authorization头
|
||||
// if (token) {
|
||||
// headers.Authorization = `Bearer ${getAccessToken()}`;
|
||||
// }
|
||||
// console.log("headers", headers);
|
||||
// return { url, options: { ...options, ...headers } };
|
||||
// },
|
||||
],
|
||||
|
||||
// 添加参数序列化配置
|
||||
@@ -212,7 +234,7 @@ export const request: RequestConfig = {
|
||||
const appendParams = (key: string, value: any) => {
|
||||
if (Array.isArray(value)) {
|
||||
// 特殊处理 createTime 数组,转换为 createTime[0] 和 createTime[1] 格式
|
||||
if (key === 'createTime') {
|
||||
if (key === "createTime") {
|
||||
value.forEach((val, index) => {
|
||||
searchParams.append(`${key}[${index}]`, val);
|
||||
});
|
||||
@@ -250,7 +272,7 @@ export async function patchClientRoutes({ routes }: any) {
|
||||
wsCache.set(CACHE_KEY.ROLE_ROUTERS, data.menus);
|
||||
menus = data.menus;
|
||||
} catch (error) {
|
||||
console.error('获取菜单失败:', error);
|
||||
console.error("获取菜单失败:", error);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -258,7 +280,7 @@ export async function patchClientRoutes({ routes }: any) {
|
||||
if (!menus || menus.length === 0) {
|
||||
return;
|
||||
}
|
||||
const routerIndex = routes.findIndex((item: any) => item.path === '/');
|
||||
const routerIndex = routes.findIndex((item: any) => item.path === "/");
|
||||
const parentId = routes[routerIndex].id;
|
||||
|
||||
if (menus) {
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import type { ProFormColumnsType } from '@ant-design/pro-components';
|
||||
import { BetaSchemaForm } from '@ant-design/pro-components';
|
||||
import { Button, type ColProps, Drawer, Space } from 'antd';
|
||||
import type { FormInstance } from 'antd/lib';
|
||||
import type { ProFormColumnsType } from "@ant-design/pro-components";
|
||||
import { BetaSchemaForm } from "@ant-design/pro-components";
|
||||
import { Button, type ColProps, Drawer, Space } from "antd";
|
||||
import type { FormInstance } from "antd/lib";
|
||||
import React, {
|
||||
Children,
|
||||
cloneElement,
|
||||
forwardRef,
|
||||
useImperativeHandle,
|
||||
} from 'react';
|
||||
} from "react";
|
||||
|
||||
interface ConfigurableDrawerFormProps {
|
||||
title?: string;
|
||||
@@ -17,9 +17,9 @@ interface ConfigurableDrawerFormProps {
|
||||
width?: number | string;
|
||||
labelCol?: ColProps;
|
||||
wrapperCol?: ColProps;
|
||||
footer: React.ReactNode;
|
||||
footer?: React.ReactNode;
|
||||
children?: React.ReactNode;
|
||||
bodyStyle: React.CSSProperties;
|
||||
bodyStyle?: React.CSSProperties;
|
||||
}
|
||||
|
||||
export interface ConfigurableDrawerFormRef {
|
||||
@@ -33,7 +33,7 @@ const ConfigurableDrawerForm = forwardRef<
|
||||
>(
|
||||
(
|
||||
{
|
||||
title = '表单',
|
||||
title = "表单",
|
||||
labelCol = { span: 4 },
|
||||
wrapperCol = { span: 20 },
|
||||
columns,
|
||||
@@ -44,7 +44,7 @@ const ConfigurableDrawerForm = forwardRef<
|
||||
children,
|
||||
bodyStyle = {},
|
||||
},
|
||||
ref,
|
||||
ref
|
||||
) => {
|
||||
const [open, setOpen] = React.useState(false);
|
||||
const [formData, setFormData] = React.useState(initialValues || {});
|
||||
@@ -56,7 +56,7 @@ const ConfigurableDrawerForm = forwardRef<
|
||||
if (data) {
|
||||
setFormData(data);
|
||||
}
|
||||
console.log('open');
|
||||
console.log("open");
|
||||
setOpen(true);
|
||||
},
|
||||
close: () => setOpen(false),
|
||||
@@ -96,12 +96,12 @@ const ConfigurableDrawerForm = forwardRef<
|
||||
title={title}
|
||||
styles={{
|
||||
header: {
|
||||
textAlign: 'left',
|
||||
position: 'relative',
|
||||
textAlign: "left",
|
||||
position: "relative",
|
||||
},
|
||||
body: {
|
||||
background: 'var(--ant-background-color)',
|
||||
padding: 'var(--ant-padding-lg)',
|
||||
background: "var(--ant-background-color)",
|
||||
padding: "var(--ant-padding-lg)",
|
||||
...bodyStyle,
|
||||
},
|
||||
}}
|
||||
@@ -111,7 +111,7 @@ const ConfigurableDrawerForm = forwardRef<
|
||||
onClose={() => setOpen(false)}
|
||||
width={width}
|
||||
footer={
|
||||
<Space style={{ width: '100%', justifyContent: 'end' }}>
|
||||
<Space style={{ width: "100%", justifyContent: "end" }}>
|
||||
{footer ? (
|
||||
footer
|
||||
) : (
|
||||
@@ -142,7 +142,7 @@ const ConfigurableDrawerForm = forwardRef<
|
||||
)}
|
||||
</Drawer>
|
||||
);
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
export default ConfigurableDrawerForm;
|
||||
|
||||
@@ -1,691 +0,0 @@
|
||||
// components/RichEditor/index.tsx
|
||||
|
||||
import { Editor } from '@tinymce/tinymce-react';
|
||||
import { message } from 'antd';
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import type { Editor as TinyMCEEditor } from 'tinymce';
|
||||
import './index.less';
|
||||
|
||||
export interface RichEditorProps {
|
||||
value?: string;
|
||||
onChange?: (content: string) => void;
|
||||
height?: number;
|
||||
placeholder?: string;
|
||||
disabled?: boolean;
|
||||
uploadConfig?: {
|
||||
action: string;
|
||||
headers?: Record<string, string>;
|
||||
maxSize?: number;
|
||||
acceptTypes?: string[];
|
||||
data?: Record<string, any>;
|
||||
};
|
||||
showWordCount?: boolean;
|
||||
maxWords?: number;
|
||||
}
|
||||
|
||||
const RichEditor: React.FC<RichEditorProps> = ({
|
||||
value = '',
|
||||
onChange,
|
||||
height = 400,
|
||||
placeholder = '请输入内容...',
|
||||
disabled = false,
|
||||
showWordCount = true,
|
||||
maxWords,
|
||||
uploadConfig = {
|
||||
action: '/api/upload/image',
|
||||
maxSize: 5,
|
||||
acceptTypes: ['image/jpeg', 'image/png', 'image/gif', 'image/webp'],
|
||||
},
|
||||
}) => {
|
||||
const [uploading, setUploading] = useState(false);
|
||||
const [wordCount, setWordCount] = useState(0);
|
||||
const [charCount, setCharCount] = useState(0);
|
||||
|
||||
// 自定义字数统计函数
|
||||
const countWords = (text: string) => {
|
||||
const plainText = text.replace(/<[^>]*>/g, '');
|
||||
const cleanText = plainText.replace(/\s+/g, ' ').trim();
|
||||
|
||||
if (!cleanText) return { words: 0, characters: 0 };
|
||||
|
||||
const chineseChars = cleanText.match(/[\u4e00-\u9fa5]/g) || [];
|
||||
const englishWords = cleanText.match(/[a-zA-Z]+/g) || [];
|
||||
const numbers = cleanText.match(/\d+/g) || [];
|
||||
|
||||
const words = chineseChars.length + englishWords.length + numbers.length;
|
||||
const characters = cleanText.length;
|
||||
|
||||
return { words, characters };
|
||||
};
|
||||
|
||||
const handleEditorChange = (content: string) => {
|
||||
onChange?.(content);
|
||||
|
||||
if (showWordCount) {
|
||||
const { words, characters } = countWords(content);
|
||||
setWordCount(words);
|
||||
setCharCount(characters);
|
||||
|
||||
if (maxWords && words > maxWords) {
|
||||
message.warning(
|
||||
`内容超出字数限制,当前 ${words} 字,限制 ${maxWords} 字`,
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (showWordCount && value) {
|
||||
const { words, characters } = countWords(value);
|
||||
setWordCount(words);
|
||||
setCharCount(characters);
|
||||
}
|
||||
}, [value, showWordCount]);
|
||||
|
||||
const uploadFile = useCallback(
|
||||
async (file: File): Promise<string> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
|
||||
if (uploadConfig.data) {
|
||||
Object.keys(uploadConfig.data).forEach((key) => {
|
||||
if (uploadConfig.data) formData.append(key, uploadConfig.data[key]);
|
||||
});
|
||||
}
|
||||
|
||||
const xhr = new XMLHttpRequest();
|
||||
|
||||
xhr.upload.addEventListener('progress', (e) => {
|
||||
if (e.lengthComputable) {
|
||||
const percent = Math.round((e.loaded / e.total) * 100);
|
||||
console.log(`上传进度: ${percent}%`);
|
||||
}
|
||||
});
|
||||
|
||||
xhr.addEventListener('load', () => {
|
||||
if (xhr.status === 200) {
|
||||
try {
|
||||
const response = JSON.parse(xhr.responseText);
|
||||
if (response.code === 200 && response.data?.url) {
|
||||
resolve(response.data.url);
|
||||
} else {
|
||||
reject(response.message || '上传失败');
|
||||
}
|
||||
} catch (_error) {
|
||||
reject('响应解析失败');
|
||||
}
|
||||
} else {
|
||||
reject(`上传失败: ${xhr.status}`);
|
||||
}
|
||||
});
|
||||
|
||||
xhr.addEventListener('error', () => {
|
||||
reject('网络错误');
|
||||
});
|
||||
|
||||
if (uploadConfig.headers) {
|
||||
Object.keys(uploadConfig.headers).forEach((key) => {
|
||||
if (uploadConfig.headers)
|
||||
xhr.setRequestHeader(key, uploadConfig.headers[key]);
|
||||
});
|
||||
}
|
||||
|
||||
xhr.open('POST', uploadConfig.action);
|
||||
xhr.send(formData);
|
||||
});
|
||||
},
|
||||
[uploadConfig],
|
||||
);
|
||||
|
||||
const handleImageUpload = useCallback(
|
||||
(blobInfo: any, _: (percent: number) => void): Promise<string> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
setUploading(true);
|
||||
const file = blobInfo.blob();
|
||||
uploadFile(file).then((url) => {
|
||||
resolve(url);
|
||||
message.success('图片上传成功');
|
||||
});
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
message.error(typeof error === 'string' ? error : '上传失败');
|
||||
} finally {
|
||||
setUploading(false);
|
||||
}
|
||||
});
|
||||
},
|
||||
[uploadFile],
|
||||
);
|
||||
|
||||
const handleCustomUpload = useCallback(
|
||||
(editor: TinyMCEEditor) => {
|
||||
const input = document.createElement('input');
|
||||
input.type = 'file';
|
||||
input.accept = uploadConfig.acceptTypes?.join(',') || 'image/*';
|
||||
input.multiple = true;
|
||||
|
||||
const handleFileChange = async (event: Event) => {
|
||||
const target = event.target as HTMLInputElement;
|
||||
const files = Array.from(target.files || []);
|
||||
|
||||
if (files.length === 0) return;
|
||||
|
||||
setUploading(true);
|
||||
|
||||
try {
|
||||
for (const file of files) {
|
||||
const isValidType = uploadConfig.acceptTypes?.includes(file.type);
|
||||
if (!isValidType) {
|
||||
message.error(`文件 ${file.name} 类型不支持`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const isValidSize =
|
||||
file.size / 1024 / 1024 < (uploadConfig.maxSize || 5);
|
||||
if (!isValidSize) {
|
||||
message.error(
|
||||
`文件 ${file.name} 大小超过 ${uploadConfig.maxSize || 5}MB`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
const url = await uploadFile(file);
|
||||
const imgHtml = `<img src="${url}" alt="${file.name}" style="max-width: 100%; height: auto;" />`;
|
||||
editor.insertContent(imgHtml);
|
||||
message.success(`${file.name} 上传成功`);
|
||||
} catch (error) {
|
||||
message.error(`${file.name} 上传失败: ${error}`);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
setUploading(false);
|
||||
}
|
||||
};
|
||||
|
||||
input.addEventListener('change', handleFileChange);
|
||||
input.click();
|
||||
},
|
||||
[uploadConfig, uploadFile],
|
||||
);
|
||||
|
||||
// 全展开的工具栏配置
|
||||
const editorConfig = {
|
||||
height,
|
||||
menubar: 'file edit view insert format tools table help',
|
||||
plugins: [
|
||||
'advlist',
|
||||
'autolink',
|
||||
'lists',
|
||||
'link',
|
||||
'image',
|
||||
'charmap',
|
||||
'preview',
|
||||
'anchor',
|
||||
'searchreplace',
|
||||
'visualblocks',
|
||||
'code',
|
||||
'fullscreen',
|
||||
'insertdatetime',
|
||||
'media',
|
||||
'table',
|
||||
'help',
|
||||
'wordcount',
|
||||
'emoticons',
|
||||
'template',
|
||||
'codesample',
|
||||
'hr',
|
||||
'pagebreak',
|
||||
'nonbreaking',
|
||||
'toc',
|
||||
'imagetools',
|
||||
'textpattern',
|
||||
'noneditable',
|
||||
'quickbars',
|
||||
'accordion',
|
||||
],
|
||||
|
||||
// 修改为全展开的工具栏配置
|
||||
toolbar: [
|
||||
// 第一行:撤销重做 + 格式选择 + 字体
|
||||
'undo redo | blocks fontfamily fontsize',
|
||||
|
||||
// 第二行:文本格式
|
||||
'bold italic underline strikethrough subscript superscript | forecolor backcolor',
|
||||
|
||||
// 第三行:对齐和列表
|
||||
'alignleft aligncenter alignright alignjustify | bullist numlist outdent indent',
|
||||
|
||||
// 第四行:插入功能
|
||||
'link customupload image media table emoticons charmap insertdatetime hr pagebreak',
|
||||
|
||||
// 第五行:高级功能
|
||||
'codesample accordion blockquote | searchreplace visualblocks code',
|
||||
|
||||
// 第六行:其他工具
|
||||
'removeformat | fullscreen preview help',
|
||||
].join(' | '),
|
||||
|
||||
// 设置工具栏模式为换行显示,而不是滑动
|
||||
toolbar_mode: 'wrap' as const, // 改为 wrap 模式,全部展开
|
||||
|
||||
// 工具栏分组,每组之间有分隔符
|
||||
toolbar_groups: {
|
||||
history: { icon: 'undo', tooltip: '历史操作' },
|
||||
formatting: { icon: 'bold', tooltip: '文本格式' },
|
||||
alignment: { icon: 'align-left', tooltip: '对齐方式' },
|
||||
indentation: { icon: 'indent', tooltip: '缩进' },
|
||||
insert: { icon: 'plus', tooltip: '插入' },
|
||||
tools: { icon: 'preferences', tooltip: '工具' },
|
||||
},
|
||||
|
||||
font_family_formats: [
|
||||
'微软雅黑=Microsoft YaHei,Helvetica Neue,PingFang SC,sans-serif',
|
||||
'苹方=PingFang SC,Microsoft YaHei,sans-serif',
|
||||
'宋体=SimSun,serif',
|
||||
'黑体=SimHei,sans-serif',
|
||||
'楷体=KaiTi,serif',
|
||||
'Arial=arial,helvetica,sans-serif',
|
||||
'Times New Roman=times new roman,times,serif',
|
||||
'Courier New=courier new,courier,monospace',
|
||||
].join(';'),
|
||||
|
||||
fontsize_formats:
|
||||
'8px 9px 10px 11px 12px 14px 16px 18px 20px 22px 24px 26px 28px 36px 48px 72px',
|
||||
|
||||
block_formats: [
|
||||
'段落=p',
|
||||
'标题1=h1',
|
||||
'标题2=h2',
|
||||
'标题3=h3',
|
||||
'标题4=h4',
|
||||
'标题5=h5',
|
||||
'标题6=h6',
|
||||
'预格式化=pre',
|
||||
'地址=address',
|
||||
'代码=code',
|
||||
].join(';'),
|
||||
|
||||
// 字数统计配置
|
||||
wordcount_countregex: /[\w\u2019\u4e00-\u9fa5]+/g,
|
||||
wordcount_cleanregex: /[0-9.(),;:!?%#$?\x27\x22_+=\\/-]*/g,
|
||||
|
||||
// 颜色配置
|
||||
color_map: [
|
||||
'000000',
|
||||
'黑色',
|
||||
'993300',
|
||||
'深红色',
|
||||
'333300',
|
||||
'深黄色',
|
||||
'003300',
|
||||
'深绿色',
|
||||
'003366',
|
||||
'深青色',
|
||||
'000080',
|
||||
'深蓝色',
|
||||
'333399',
|
||||
'蓝色',
|
||||
'333333',
|
||||
'深灰色',
|
||||
'800000',
|
||||
'栗色',
|
||||
'FF6600',
|
||||
'橙色',
|
||||
'808000',
|
||||
'橄榄色',
|
||||
'008000',
|
||||
'绿色',
|
||||
'008080',
|
||||
'青色',
|
||||
'0000FF',
|
||||
'蓝色',
|
||||
'666699',
|
||||
'灰蓝色',
|
||||
'808080',
|
||||
'灰色',
|
||||
'FF0000',
|
||||
'红色',
|
||||
'FF9900',
|
||||
'琥珀色',
|
||||
'99CC00',
|
||||
'黄绿色',
|
||||
'339966',
|
||||
'海绿色',
|
||||
'33CCCC',
|
||||
'青绿色',
|
||||
'3366FF',
|
||||
'蓝色',
|
||||
'800080',
|
||||
'紫色',
|
||||
'999999',
|
||||
'中灰色',
|
||||
'FF00FF',
|
||||
'洋红色',
|
||||
'FFCC00',
|
||||
'金色',
|
||||
'FFFF00',
|
||||
'黄色',
|
||||
'00FF00',
|
||||
'酸橙色',
|
||||
'00FFFF',
|
||||
'水蓝色',
|
||||
'00CCFF',
|
||||
'天蓝色',
|
||||
'993366',
|
||||
'红紫色',
|
||||
'FFFFFF',
|
||||
'白色',
|
||||
],
|
||||
|
||||
// 代码高亮配置
|
||||
codesample_languages: [
|
||||
{ text: 'HTML/XML', value: 'markup' },
|
||||
{ text: 'JavaScript', value: 'javascript' },
|
||||
{ text: 'TypeScript', value: 'typescript' },
|
||||
{ text: 'CSS', value: 'css' },
|
||||
{ text: 'SCSS', value: 'scss' },
|
||||
{ text: 'Python', value: 'python' },
|
||||
{ text: 'Java', value: 'java' },
|
||||
{ text: 'C++', value: 'cpp' },
|
||||
{ text: 'C#', value: 'csharp' },
|
||||
{ text: 'PHP', value: 'php' },
|
||||
{ text: 'Ruby', value: 'ruby' },
|
||||
{ text: 'Go', value: 'go' },
|
||||
{ text: 'Rust', value: 'rust' },
|
||||
{ text: 'SQL', value: 'sql' },
|
||||
{ text: 'JSON', value: 'json' },
|
||||
{ text: 'Bash', value: 'bash' },
|
||||
],
|
||||
|
||||
// 表格配置
|
||||
table_default_attributes: {
|
||||
border: '1',
|
||||
},
|
||||
table_default_styles: {
|
||||
'border-collapse': 'collapse',
|
||||
width: '100%',
|
||||
},
|
||||
table_class_list: [
|
||||
{ title: '无样式', value: '' },
|
||||
{ title: '简单表格', value: 'simple-table' },
|
||||
{ title: '条纹表格', value: 'striped-table' },
|
||||
{ title: '边框表格', value: 'bordered-table' },
|
||||
],
|
||||
|
||||
// 链接配置
|
||||
link_default_target: '_blank',
|
||||
link_assume_external_targets: true,
|
||||
link_context_toolbar: true,
|
||||
|
||||
// 图片配置
|
||||
image_advtab: true,
|
||||
image_caption: true,
|
||||
|
||||
// 媒体配置
|
||||
media_live_embeds: true,
|
||||
media_filter_html: false,
|
||||
|
||||
content_style: `
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
margin: 16px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
margin-top: 24px;
|
||||
margin-bottom: 16px;
|
||||
font-weight: 600;
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
h1 { font-size: 2em; color: #1a1a1a; }
|
||||
h2 { font-size: 1.5em; color: #2a2a2a; }
|
||||
h3 { font-size: 1.25em; color: #3a3a3a; }
|
||||
|
||||
p { margin-bottom: 16px; }
|
||||
|
||||
img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||
margin: 8px 0;
|
||||
}
|
||||
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
margin: 16px 0;
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
table td, table th {
|
||||
border: 1px solid #e8e8e8;
|
||||
padding: 12px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
table th {
|
||||
background-color: #f8f9fa;
|
||||
font-weight: 600;
|
||||
color: #495057;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
border-left: 4px solid #1890ff;
|
||||
margin: 16px 0;
|
||||
padding: 8px 16px;
|
||||
background-color: #f6f8fa;
|
||||
color: #666;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
code {
|
||||
background-color: #f1f3f4;
|
||||
padding: 2px 4px;
|
||||
border-radius: 3px;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 0.9em;
|
||||
color: #d73a49;
|
||||
}
|
||||
|
||||
pre {
|
||||
background-color: #f8f8f8;
|
||||
border: 1px solid #e1e4e8;
|
||||
border-radius: 6px;
|
||||
padding: 16px;
|
||||
overflow-x: auto;
|
||||
margin: 16px 0;
|
||||
}
|
||||
|
||||
pre code {
|
||||
background: none;
|
||||
padding: 0;
|
||||
color: inherit;
|
||||
}
|
||||
`,
|
||||
|
||||
placeholder,
|
||||
branding: false,
|
||||
elementpath: true,
|
||||
resize: 'both' as const,
|
||||
statusbar: showWordCount,
|
||||
|
||||
// 图片上传配置
|
||||
images_upload_handler: handleImageUpload,
|
||||
automatic_uploads: true,
|
||||
images_reuse_filename: true,
|
||||
images_upload_url: uploadConfig.action,
|
||||
images_upload_base_path: '',
|
||||
images_upload_credentials: true,
|
||||
|
||||
// 粘贴配置
|
||||
paste_data_images: true,
|
||||
paste_as_text: false,
|
||||
paste_webkit_styles: 'none',
|
||||
paste_merge_formats: true,
|
||||
paste_remove_styles_if_webkit: true,
|
||||
|
||||
// 文件选择器配置
|
||||
file_picker_types: 'image media',
|
||||
file_picker_callback: (
|
||||
callback: (url: string, meta?: any) => void,
|
||||
// value: string,
|
||||
meta: any,
|
||||
) => {
|
||||
if (meta.filetype === 'image') {
|
||||
const input = document.createElement('input');
|
||||
input.type = 'file';
|
||||
input.accept = uploadConfig.acceptTypes?.join(',') || 'image/*';
|
||||
|
||||
const handleChange = async (event: Event) => {
|
||||
const target = event.target as HTMLInputElement;
|
||||
const file = target.files?.[0];
|
||||
|
||||
if (!file) return;
|
||||
|
||||
const isValidType = uploadConfig.acceptTypes?.includes(file.type);
|
||||
const isValidSize =
|
||||
file.size / 1024 / 1024 < (uploadConfig.maxSize || 5);
|
||||
|
||||
if (!isValidType) {
|
||||
message.error('文件类型不支持');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isValidSize) {
|
||||
message.error(`文件大小不能超过 ${uploadConfig.maxSize || 5}MB`);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setUploading(true);
|
||||
const url = await uploadFile(file);
|
||||
callback(url, { alt: file.name });
|
||||
message.success('图片上传成功');
|
||||
} catch (error) {
|
||||
message.error('上传失败');
|
||||
console.error('Upload error:', error);
|
||||
} finally {
|
||||
setUploading(false);
|
||||
}
|
||||
};
|
||||
|
||||
input.addEventListener('change', handleChange);
|
||||
input.click();
|
||||
}
|
||||
},
|
||||
|
||||
// 快速工具栏
|
||||
quickbars_selection_toolbar:
|
||||
'bold italic | quicklink h2 h3 blockquote quickimage quicktable',
|
||||
quickbars_insert_toolbar: 'quickimage quicktable',
|
||||
|
||||
// 文本模式
|
||||
textpattern_patterns: [
|
||||
{ start: '*', end: '*', format: 'italic' },
|
||||
{ start: '**', end: '**', format: 'bold' },
|
||||
{ start: '#', format: 'h1' },
|
||||
{ start: '##', format: 'h2' },
|
||||
{ start: '###', format: 'h3' },
|
||||
{ start: '####', format: 'h4' },
|
||||
{ start: '#####', format: 'h5' },
|
||||
{ start: '######', format: 'h6' },
|
||||
{ start: '1. ', cmd: 'InsertOrderedList' },
|
||||
{ start: '* ', cmd: 'InsertUnorderedList' },
|
||||
{ start: '- ', cmd: 'InsertUnorderedList' },
|
||||
],
|
||||
|
||||
// 初始化设置
|
||||
setup: (editor: TinyMCEEditor) => {
|
||||
// 注册自定义上传按钮
|
||||
editor.ui.registry.addButton('customupload', {
|
||||
text: uploading ? '上传中...' : '上传',
|
||||
icon: 'upload',
|
||||
tooltip: '上传图片(支持多选)',
|
||||
enabled: !disabled && !uploading,
|
||||
onAction: () => {
|
||||
handleCustomUpload(editor);
|
||||
},
|
||||
});
|
||||
|
||||
// 监听内容变化,实时更新字数统计
|
||||
editor.on('input change undo redo', () => {
|
||||
if (showWordCount) {
|
||||
const content = editor.getContent();
|
||||
const { words, characters } = countWords(content);
|
||||
setWordCount(words);
|
||||
setCharCount(characters);
|
||||
}
|
||||
});
|
||||
|
||||
editor.on('init', () => {
|
||||
console.log('编辑器初始化完成');
|
||||
|
||||
// 初始化字数统计
|
||||
if (showWordCount && value) {
|
||||
const { words, characters } = countWords(value);
|
||||
setWordCount(words);
|
||||
setCharCount(characters);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// 其他配置
|
||||
convert_urls: false,
|
||||
remove_script_host: false,
|
||||
relative_urls: false,
|
||||
language: 'zh_CN',
|
||||
directionality: 'ltr' as const,
|
||||
|
||||
// 高级配置
|
||||
extended_valid_elements: 'script[src|async|defer|type|charset]',
|
||||
|
||||
// 性能配置
|
||||
browser_spellcheck: true,
|
||||
contextmenu: 'link image table',
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="rich-editor-wrapper">
|
||||
<Editor
|
||||
value={value}
|
||||
onEditorChange={handleEditorChange}
|
||||
init={editorConfig}
|
||||
disabled={disabled}
|
||||
/>
|
||||
|
||||
{/* 自定义字数统计显示 */}
|
||||
{showWordCount && (
|
||||
<div className="word-count-display">
|
||||
<span className="word-count">
|
||||
字数: {wordCount}
|
||||
{maxWords && (
|
||||
<span className={wordCount > maxWords ? 'over-limit' : ''}>
|
||||
/{maxWords}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
<span className="char-count">字符: {charCount}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{uploading && (
|
||||
<div className="upload-overlay">
|
||||
<div className="upload-spinner">
|
||||
<div className="spinner"></div>
|
||||
<span>上传中...</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default RichEditor;
|
||||
@@ -1,7 +1,8 @@
|
||||
import { Editor } from '@tinymce/tinymce-react';
|
||||
import { message } from 'antd';
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import type { Editor as TinyMCEEditor } from 'tinymce';
|
||||
import { uploadImage } from "@/services/infra/media";
|
||||
import { Editor } from "@tinymce/tinymce-react";
|
||||
import { message } from "antd";
|
||||
import React, { useCallback, useState } from "react";
|
||||
import type { Editor as TinyMCEEditor } from "tinymce";
|
||||
export interface RichEditorProps {
|
||||
value?: string;
|
||||
onChange?: (content: string) => void;
|
||||
@@ -18,18 +19,19 @@ export interface RichEditorProps {
|
||||
showWordCount?: boolean;
|
||||
maxWords?: number;
|
||||
}
|
||||
|
||||
console.log(VITE_BASE_URL);
|
||||
const RichEditor: React.FC<RichEditorProps> = ({
|
||||
// value = "",
|
||||
// onChange,
|
||||
value = "",
|
||||
onChange,
|
||||
// height = 400,
|
||||
// placeholder = "请输入内容...",
|
||||
disabled = false,
|
||||
// showWordCount = true,
|
||||
// maxWords,
|
||||
uploadConfig = {
|
||||
action: '/api/upload/image',
|
||||
maxSize: 5,
|
||||
acceptTypes: ['image/jpeg', 'image/png', 'image/gif', 'image/webp'],
|
||||
acceptTypes: ["image/jpeg", "image/png", "image/gif", "image/webp"],
|
||||
},
|
||||
}) => {
|
||||
const [uploading, setUploading] = useState<boolean>(false);
|
||||
@@ -37,55 +39,21 @@ const RichEditor: React.FC<RichEditorProps> = ({
|
||||
async (file: File): Promise<string> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
|
||||
if (uploadConfig.data) {
|
||||
Object.keys(uploadConfig.data).forEach((key) => {
|
||||
formData.append(key, uploadConfig.data?.[key]);
|
||||
});
|
||||
}
|
||||
|
||||
const xhr = new XMLHttpRequest();
|
||||
|
||||
xhr.upload.addEventListener('progress', (e) => {
|
||||
if (e.lengthComputable) {
|
||||
const percent = Math.round((e.loaded / e.total) * 100);
|
||||
console.log(`上传进度: ${percent}%`);
|
||||
}
|
||||
});
|
||||
|
||||
xhr.addEventListener('load', () => {
|
||||
if (xhr.status === 200) {
|
||||
try {
|
||||
const response = JSON.parse(xhr.responseText);
|
||||
if (response.code === 200 && response.data?.url) {
|
||||
resolve(response.data.url);
|
||||
} else {
|
||||
reject(response.message || '上传失败');
|
||||
}
|
||||
} catch (_error) {
|
||||
reject('响应解析失败');
|
||||
formData.append("file", file);
|
||||
try {
|
||||
uploadImage(formData).then((res) => {
|
||||
if (res) {
|
||||
resolve(res);
|
||||
} else {
|
||||
reject(new Error("上传失败:未返回有效的URL"));
|
||||
}
|
||||
} else {
|
||||
reject(`上传失败: ${xhr.status}`);
|
||||
}
|
||||
});
|
||||
|
||||
xhr.addEventListener('error', () => {
|
||||
reject('网络错误');
|
||||
});
|
||||
|
||||
if (uploadConfig.headers) {
|
||||
Object.keys(uploadConfig.headers).forEach((key) => {
|
||||
xhr.setRequestHeader(key, uploadConfig.headers![key]);
|
||||
});
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
|
||||
xhr.open('POST', uploadConfig.action);
|
||||
xhr.send(formData);
|
||||
});
|
||||
},
|
||||
[uploadConfig],
|
||||
[uploadConfig]
|
||||
);
|
||||
const handleImageUpload = useCallback(
|
||||
(blobInfo: any, _: (percent: number) => void): Promise<string> => {
|
||||
@@ -97,23 +65,23 @@ const RichEditor: React.FC<RichEditorProps> = ({
|
||||
resolve(url);
|
||||
});
|
||||
|
||||
message.success('图片上传成功');
|
||||
message.success("图片上传成功");
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
message.error(typeof error === 'string' ? error : '上传失败');
|
||||
message.error(typeof error === "string" ? error : "上传失败");
|
||||
} finally {
|
||||
setUploading(false);
|
||||
}
|
||||
});
|
||||
},
|
||||
[uploadFile],
|
||||
[uploadFile]
|
||||
);
|
||||
|
||||
const handleCustomUpload = useCallback(
|
||||
(editor: TinyMCEEditor) => {
|
||||
const input = document.createElement('input');
|
||||
input.type = 'file';
|
||||
input.accept = uploadConfig.acceptTypes?.join(',') || 'image/*';
|
||||
const input = document.createElement("input");
|
||||
input.type = "file";
|
||||
input.accept = uploadConfig.acceptTypes?.join(",") || "image/*";
|
||||
input.multiple = true;
|
||||
|
||||
const handleFileChange = async (event: Event) => {
|
||||
@@ -136,7 +104,7 @@ const RichEditor: React.FC<RichEditorProps> = ({
|
||||
file.size / 1024 / 1024 < (uploadConfig.maxSize || 5);
|
||||
if (!isValidSize) {
|
||||
message.error(
|
||||
`文件 ${file.name} 大小超过 ${uploadConfig.maxSize || 5}MB`,
|
||||
`文件 ${file.name} 大小超过 ${uploadConfig.maxSize || 5}MB`
|
||||
);
|
||||
continue;
|
||||
}
|
||||
@@ -155,93 +123,108 @@ const RichEditor: React.FC<RichEditorProps> = ({
|
||||
}
|
||||
};
|
||||
|
||||
input.addEventListener('change', handleFileChange);
|
||||
input.addEventListener("change", handleFileChange);
|
||||
input.click();
|
||||
},
|
||||
[uploadConfig, uploadFile],
|
||||
[uploadConfig, uploadFile]
|
||||
);
|
||||
|
||||
const handleEditorChange = (content: string) => {
|
||||
onChange?.(content);
|
||||
};
|
||||
return (
|
||||
<Editor
|
||||
apiKey="xt51qhzvj8q9mxzxytigtp0nu94v4rz7dska2dbqeilkxo99"
|
||||
// apiKey="xt51qhzvj8q9mxzxytigtp0nu94v4rz7dska2dbqeilkxo99"
|
||||
value={value}
|
||||
onEditorChange={handleEditorChange}
|
||||
api-key="no-api-key"
|
||||
licenseKey="gpl"
|
||||
tinymceScriptSrc="/tinymce/tinymce.min.js"
|
||||
init={{
|
||||
plugins: [
|
||||
// Core editing features
|
||||
'anchor',
|
||||
'autolink',
|
||||
'charmap',
|
||||
'codesample',
|
||||
'emoticons',
|
||||
'link',
|
||||
'lists',
|
||||
'media',
|
||||
'searchreplace',
|
||||
'table',
|
||||
'visualblocks',
|
||||
'wordcount',
|
||||
"anchor",
|
||||
"autolink",
|
||||
"charmap",
|
||||
"codesample",
|
||||
"emoticons",
|
||||
"link",
|
||||
"lists",
|
||||
"media",
|
||||
"searchreplace",
|
||||
"table",
|
||||
"visualblocks",
|
||||
"wordcount",
|
||||
// Your account includes a free trial of TinyMCE premium features
|
||||
// Try the most popular premium features until Oct 8, 2025:
|
||||
'checklist',
|
||||
'mediaembed',
|
||||
'casechange',
|
||||
'formatpainter',
|
||||
'pageembed',
|
||||
'a11ychecker',
|
||||
'tinymcespellchecker',
|
||||
'permanentpen',
|
||||
'powerpaste',
|
||||
'advtable',
|
||||
'advcode',
|
||||
'advtemplate',
|
||||
'ai',
|
||||
'uploadcare',
|
||||
'mentions',
|
||||
'tinycomments',
|
||||
'tableofcontents',
|
||||
'footnotes',
|
||||
'mergetags',
|
||||
'autocorrect',
|
||||
'typography',
|
||||
'inlinecss',
|
||||
'markdown',
|
||||
'importword',
|
||||
'exportword',
|
||||
'exportpdf',
|
||||
// "checklist",
|
||||
// "mediaembed",
|
||||
// "casechange",
|
||||
// "formatpainter",
|
||||
// "pageembed",
|
||||
// "a11ychecker",
|
||||
// "tinymcespellchecker",
|
||||
// "permanentpen",
|
||||
// "powerpaste",
|
||||
// "advtable",
|
||||
// "advcode",
|
||||
// "advtemplate",
|
||||
// "ai",
|
||||
// "uploadcare",
|
||||
// "mentions",
|
||||
// "tinycomments",
|
||||
// "tableofcontents",
|
||||
// "footnotes",
|
||||
// "mergetags",
|
||||
// "autocorrect",
|
||||
// "typography",
|
||||
// "inlinecss",
|
||||
// "markdown",
|
||||
// "importword",
|
||||
// "exportword",
|
||||
// "exportpdf",
|
||||
],
|
||||
|
||||
menubar: false,
|
||||
toolbar:
|
||||
'undo redo | blocks | bold italic forecolor | ' +
|
||||
'alignleft aligncenter | ' +
|
||||
'alignright alignjustify | bullist numlist outdent indent customupload | ' +
|
||||
'removeformat ',
|
||||
tinycomments_mode: 'embedded',
|
||||
tinycomments_author: 'Author name',
|
||||
toolbar_mode: 'sliding',
|
||||
"undo redo | blocks | bold italic forecolor " +
|
||||
"alignleft aligncenter | " +
|
||||
"alignright alignjustify | bullist numlist outdent indent customupload | " +
|
||||
"removeformat ",
|
||||
tinycomments_mode: "embedded",
|
||||
tinycomments_author: "Author name",
|
||||
toolbar_mode: "wrap" as const, // 改为 wrap 模式,全部展开
|
||||
// toolbar_mode: "sliding",
|
||||
mergetags_list: [
|
||||
{ value: 'First.Name', title: 'First Name' },
|
||||
{ value: 'Email', title: 'Email' },
|
||||
{ value: "First.Name", title: "First Name" },
|
||||
{ value: "Email", title: "Email" },
|
||||
],
|
||||
ai_request: (
|
||||
_: any,
|
||||
respondWith: { string: (arg0: () => Promise<never>) => any },
|
||||
respondWith: { string: (arg0: () => Promise<never>) => any }
|
||||
) =>
|
||||
respondWith.string(() =>
|
||||
Promise.reject('See docs to implement AI Assistant'),
|
||||
Promise.reject("See docs to implement AI Assistant")
|
||||
),
|
||||
language: 'zh-CN',
|
||||
language: "zh_CN",
|
||||
|
||||
// 其他配置
|
||||
|
||||
table_class_list: [
|
||||
{ title: "无样式", value: "" },
|
||||
{ title: "简单表格", value: "simple-table" },
|
||||
{ title: "条纹表格", value: "striped-table" },
|
||||
{ title: "边框表格", value: "bordered-table" },
|
||||
],
|
||||
convert_urls: false,
|
||||
remove_script_host: false,
|
||||
uploadcare_public_key: '0ad3671d77f59c5756dd',
|
||||
uploadcare_public_key: "0ad3671d77f59c5756dd",
|
||||
|
||||
setup: (editor: TinyMCEEditor) => {
|
||||
// 注册自定义上传按钮
|
||||
editor.ui.registry.addButton('customupload', {
|
||||
text: uploading ? '上传中...' : '上传',
|
||||
icon: 'upload',
|
||||
tooltip: '上传图片(支持多选)',
|
||||
editor.ui.registry.addButton("customupload", {
|
||||
text: uploading ? "上传中..." : "上传",
|
||||
icon: "upload",
|
||||
tooltip: "上传图片(支持多选)",
|
||||
enabled: !disabled && !uploading,
|
||||
onAction: () => {
|
||||
handleCustomUpload(editor);
|
||||
@@ -252,9 +235,10 @@ const RichEditor: React.FC<RichEditorProps> = ({
|
||||
images_upload_handler: handleImageUpload,
|
||||
// 性能配置
|
||||
browser_spellcheck: true,
|
||||
contextmenu: 'link image table',
|
||||
contextmenu: "link image table",
|
||||
}}
|
||||
initialValue="Welcome to TinyMCE!"
|
||||
|
||||
// onBlur={() =>onChange()}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,49 +1,84 @@
|
||||
import type {
|
||||
ProColumns,
|
||||
ProCoreActionType,
|
||||
ProFormColumnsType,
|
||||
} from '@ant-design/pro-components';
|
||||
import dayjs from 'dayjs';
|
||||
import TagEditor from '@/components/TagEditor';
|
||||
import TinyMCEEditor from '@/components/Tinymce';
|
||||
} from "@ant-design/pro-components";
|
||||
import dayjs from "dayjs";
|
||||
import TagEditor from "@/components/TagEditor";
|
||||
import TinyMCEEditor from "@/components/Tinymce";
|
||||
import {
|
||||
getCategoryList,
|
||||
putCategoryUpdate,
|
||||
} from "@/services/prodApi/category";
|
||||
import { Input, message, Modal, Switch } from "antd";
|
||||
export const baseTenantColumns: ProColumns<API.CategoryDO>[] = [
|
||||
{
|
||||
title: '类目名称',
|
||||
dataIndex: 'categoryName',
|
||||
title: "类目名称",
|
||||
dataIndex: "categoryName",
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
title: '类目ID',
|
||||
dataIndex: 'categoryId',
|
||||
title: "类目ID",
|
||||
dataIndex: "categoryId",
|
||||
},
|
||||
{
|
||||
title: '类目层级',
|
||||
dataIndex: 'grade',
|
||||
title: "类目层级",
|
||||
dataIndex: "grade",
|
||||
hideInSearch: true, // 在搜索表单中隐藏
|
||||
},
|
||||
{
|
||||
title: '父级类目',
|
||||
dataIndex: 'parentName',
|
||||
title: "父级类目",
|
||||
dataIndex: "parentName",
|
||||
|
||||
hideInSearch: true, // 在搜索表单中隐藏
|
||||
},
|
||||
{
|
||||
title: '排序权重',
|
||||
dataIndex: 'sort',
|
||||
title: "排序权重",
|
||||
dataIndex: "sort",
|
||||
hideInSearch: true, // 在搜索表单中隐藏
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
valueType: 'switch',
|
||||
title: "状态",
|
||||
dataIndex: "status",
|
||||
valueType: "switch",
|
||||
hideInSearch: true,
|
||||
render: (
|
||||
_,
|
||||
record: API.CategoryDO,
|
||||
_index: number,
|
||||
action: ProCoreActionType | undefined
|
||||
) => (
|
||||
<Switch
|
||||
checked={record.status === 1}
|
||||
checkedChildren="开启"
|
||||
unCheckedChildren="禁用"
|
||||
onChange={(checked) => {
|
||||
Modal.confirm({
|
||||
title: "确认操作",
|
||||
content: `确认要"${checked ? "启用" : "禁用"}${
|
||||
record.categoryName
|
||||
}"类目吗?`,
|
||||
onOk: async () => {
|
||||
console.log(checked);
|
||||
await putCategoryUpdate({
|
||||
status: checked ? 1 : 0,
|
||||
categoryId: record.categoryId,
|
||||
});
|
||||
message.success("修改成功");
|
||||
action?.reload();
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '创建时间',
|
||||
dataIndex: 'createTime',
|
||||
valueType: 'dateRange',
|
||||
title: "创建时间",
|
||||
dataIndex: "createTime",
|
||||
valueType: "dateRange",
|
||||
hideInSearch: true, // 在搜索表单中隐藏
|
||||
render: (_, record: API.CategoryDO) =>
|
||||
dayjs(record.createTime).format('YYYY-MM-DD HH:mm:ss'),
|
||||
dayjs(record.createTime).format("YYYY-MM-DD HH:mm:ss"),
|
||||
},
|
||||
];
|
||||
|
||||
@@ -52,100 +87,129 @@ export const formColumns = (data: {
|
||||
grade: number;
|
||||
}): ProFormColumnsType[] => [
|
||||
{
|
||||
title: '类目',
|
||||
dataIndex: 'grade',
|
||||
valueType: 'radio',
|
||||
title: "类目",
|
||||
dataIndex: "grade",
|
||||
valueType: "radio",
|
||||
fieldProps: {
|
||||
value: data.grade || 1,
|
||||
options: [
|
||||
{ label: '一级类目', value: 1 },
|
||||
{ label: '二级类目', value: 2 },
|
||||
{ label: '三级类目', value: 3 },
|
||||
{ label: "一级类目", value: 1 },
|
||||
{ label: "二级类目", value: 2 },
|
||||
{ label: "三级类目", value: 3 },
|
||||
],
|
||||
disabled: data.type === 'create',
|
||||
disabled: data.type === "update",
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '类目名称',
|
||||
dataIndex: 'username',
|
||||
title: "类目名称",
|
||||
dataIndex: "categoryName",
|
||||
formItemProps: {
|
||||
rules: [
|
||||
{
|
||||
required: true,
|
||||
message: '请输入用户名',
|
||||
message: "请输入用户名",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '排序权重',
|
||||
dataIndex: 'sort',
|
||||
valueType: 'digit',
|
||||
title: "排序权重",
|
||||
dataIndex: "sort",
|
||||
valueType: "digit",
|
||||
},
|
||||
{
|
||||
title: '类目描述',
|
||||
dataIndex: 'description',
|
||||
valueType: 'textarea',
|
||||
title: "类目描述",
|
||||
dataIndex: "description",
|
||||
valueType: "textarea",
|
||||
renderFormItem: () => {
|
||||
return <TinyMCEEditor />;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '关联父级',
|
||||
dataIndex: 'parentId',
|
||||
valueType: 'select',
|
||||
hideInForm: data.grade - 1 === 0,
|
||||
title: "关联父级",
|
||||
dataIndex: "parentId",
|
||||
valueType: "select",
|
||||
hideInForm: data.grade - 1 <= 0,
|
||||
fieldProps: {
|
||||
fieldNames: { label: "categoryName", value: "categoryId" },
|
||||
},
|
||||
request: async () => {
|
||||
const grade = data.grade ? data.grade - 1 : undefined;
|
||||
const res = await getCategoryList({ grade });
|
||||
return res;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '类目icon',
|
||||
dataIndex: 'icon',
|
||||
title: "类目icon",
|
||||
dataIndex: "icon",
|
||||
},
|
||||
{
|
||||
title: '类目标签',
|
||||
dataIndex: 'tages',
|
||||
title: "类目标签",
|
||||
dataIndex: "tag",
|
||||
renderFormItem: () => {
|
||||
return (
|
||||
<TagEditor
|
||||
placeholder="输入标签名称"
|
||||
maxCount={10}
|
||||
tagProps={{
|
||||
color: 'blue',
|
||||
color: "blue",
|
||||
}}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '类目状态',
|
||||
dataIndex: 'status',
|
||||
hideInForm: data.type === 'create',
|
||||
title: "类目状态",
|
||||
dataIndex: "status",
|
||||
hideInForm: data.type === "create",
|
||||
fieldProps: {
|
||||
disabled: data.type === "update",
|
||||
},
|
||||
renderFormItem: (_schema, _config, form) => {
|
||||
const status = form.getFieldValue("type");
|
||||
return <Input value={status ? "开启" : "禁用"} disabled />;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '类目ID',
|
||||
dataIndex: 'categoryId',
|
||||
hideInForm: data.type === 'create',
|
||||
title: "类目ID",
|
||||
dataIndex: "categoryId",
|
||||
hideInForm: data.type === "create",
|
||||
fieldProps: {
|
||||
disabled: data.type === "update",
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '创建时间',
|
||||
dataIndex: 'createTime',
|
||||
valueType: 'dateTime',
|
||||
hideInForm: data.type === 'create',
|
||||
title: "创建时间",
|
||||
dataIndex: "createTime",
|
||||
valueType: "dateTime",
|
||||
hideInForm: data.type === "create",
|
||||
fieldProps: {
|
||||
disabled: data.type === "update",
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '创建人',
|
||||
dataIndex: 'creator',
|
||||
hideInForm: data.type === 'create',
|
||||
title: "创建人",
|
||||
dataIndex: "creator",
|
||||
hideInForm: data.type === "create",
|
||||
fieldProps: {
|
||||
disabled: data.type === "update",
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '更新时间',
|
||||
dataIndex: 'updateTime',
|
||||
valueType: 'dateTime',
|
||||
hideInForm: data.type === 'create',
|
||||
title: "更新时间",
|
||||
dataIndex: "updateTime",
|
||||
valueType: "dateTime",
|
||||
hideInForm: data.type === "create",
|
||||
fieldProps: {
|
||||
disabled: data.type === "update",
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '更新人',
|
||||
dataIndex: 'updator',
|
||||
hideInForm: data.type === 'create',
|
||||
title: "更新人",
|
||||
dataIndex: "updator",
|
||||
hideInForm: data.type === "create",
|
||||
fieldProps: {
|
||||
disabled: data.type === "update",
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -1,27 +1,32 @@
|
||||
import { PlusOutlined } from '@ant-design/icons';
|
||||
import type { ActionType, ProColumns } from '@ant-design/pro-components';
|
||||
import type { TabsProps } from 'antd';
|
||||
import { Tabs } from 'antd';
|
||||
import { useCallback, useRef, useState } from 'react';
|
||||
import { PlusOutlined } from "@ant-design/icons";
|
||||
import type { ActionType, ProColumns } from "@ant-design/pro-components";
|
||||
import type { TabsProps } from "antd";
|
||||
import { Tabs } from "antd";
|
||||
import { useCallback, useRef, useState } from "react";
|
||||
import ConfigurableDrawerForm, {
|
||||
type ConfigurableDrawerFormRef,
|
||||
} from '@/components/DrawerForm';
|
||||
import EnhancedProTable from '@/components/EnhancedProTable';
|
||||
import type { ToolbarAction } from '@/components/EnhancedProTable/types';
|
||||
import { formStatusType } from '@/constants';
|
||||
import { getCategoryList } from '@/services/prodApi/category';
|
||||
import { baseTenantColumns, formColumns } from './config';
|
||||
} from "@/components/DrawerForm";
|
||||
import EnhancedProTable from "@/components/EnhancedProTable";
|
||||
import type { ToolbarAction } from "@/components/EnhancedProTable/types";
|
||||
import { formStatusType } from "@/constants";
|
||||
import {
|
||||
getCategoryList,
|
||||
postCategoryCreate,
|
||||
putCategoryUpdate,
|
||||
} from "@/services/prodApi/category";
|
||||
import { baseTenantColumns, formColumns } from "./config";
|
||||
|
||||
const ProdCategory = () => {
|
||||
const tableRef = useRef<ActionType>(null);
|
||||
const [type, setType] = useState<'create' | 'update' | 'test'>('create');
|
||||
const [grade, setGrade] = useState<string>('');
|
||||
const [type, setType] = useState<"create" | "update" | "test">("create");
|
||||
const [grade, setGrade] = useState<number>();
|
||||
const configurableDrawerRef = useRef<ConfigurableDrawerFormRef>(null);
|
||||
const [id, setId] = useState<number>(0);
|
||||
const onChange = useCallback(
|
||||
(key: string) => {
|
||||
setGrade(key);
|
||||
setGrade(Number(key));
|
||||
},
|
||||
[grade],
|
||||
[grade]
|
||||
);
|
||||
|
||||
const onFetch = async (params: API.getProductCategoryCategoryListParams) => {
|
||||
@@ -36,37 +41,46 @@ const ProdCategory = () => {
|
||||
};
|
||||
|
||||
const handleAdd = () => {
|
||||
setType('create');
|
||||
configurableDrawerRef.current?.open();
|
||||
setType("create");
|
||||
configurableDrawerRef.current?.open({ grade: grade ? grade : 1 });
|
||||
};
|
||||
|
||||
const toolbarActions: ToolbarAction[] = [
|
||||
{
|
||||
key: 'add',
|
||||
label: '新建',
|
||||
type: 'primary',
|
||||
key: "add",
|
||||
label: "新建",
|
||||
type: "primary",
|
||||
icon: <PlusOutlined />,
|
||||
onClick: handleAdd,
|
||||
},
|
||||
];
|
||||
const handleEdit = async (row: API.CategoryDO) => {
|
||||
setType('update');
|
||||
setType("update");
|
||||
row.categoryId && setId(row.categoryId);
|
||||
configurableDrawerRef.current?.open(row);
|
||||
};
|
||||
const handleSubmit = async (values: API.CategoryDO) => {
|
||||
console.log('values', values);
|
||||
// const success = await handleAdd(values as API.CategoryDO);
|
||||
// if (success) {
|
||||
// handleClose();
|
||||
// }
|
||||
return true;
|
||||
};
|
||||
const handleSubmit = useCallback(
|
||||
async (values: API.CategoryDO) => {
|
||||
if (type === "create") {
|
||||
await postCategoryCreate(values);
|
||||
} else {
|
||||
await putCategoryUpdate({
|
||||
...values,
|
||||
categoryId: id,
|
||||
});
|
||||
}
|
||||
tableRef.current?.reload();
|
||||
return true;
|
||||
},
|
||||
|
||||
[id, type]
|
||||
);
|
||||
|
||||
const actionColumns: ProColumns<API.CategoryDO> = {
|
||||
title: '操作',
|
||||
dataIndex: 'option',
|
||||
valueType: 'option',
|
||||
fixed: 'right',
|
||||
title: "操作",
|
||||
dataIndex: "option",
|
||||
valueType: "option",
|
||||
fixed: "right",
|
||||
width: 120,
|
||||
render: (_text: React.ReactNode, record: API.CategoryDO, _: number) => [
|
||||
<a key="edit" onClick={() => handleEdit(record)}>
|
||||
@@ -96,34 +110,42 @@ const ProdCategory = () => {
|
||||
// width="50vw"
|
||||
columns={formColumns({ grade: Number(grade), type })}
|
||||
onSubmit={handleSubmit}
|
||||
footer={undefined}
|
||||
bodyStyle={{}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const items: TabsProps['items'] = [
|
||||
const items: TabsProps["items"] = [
|
||||
{
|
||||
key: '',
|
||||
label: '全部分类',
|
||||
key: "",
|
||||
label: "全部分类",
|
||||
children: renderChildren(),
|
||||
},
|
||||
{
|
||||
key: '3',
|
||||
label: '三级分类',
|
||||
key: "3",
|
||||
label: "三级分类",
|
||||
children: renderChildren(),
|
||||
},
|
||||
{
|
||||
key: '2',
|
||||
label: '二级分类',
|
||||
key: "2",
|
||||
label: "二级分类",
|
||||
children: renderChildren(),
|
||||
},
|
||||
{
|
||||
key: '1',
|
||||
label: '一级分类',
|
||||
key: "1",
|
||||
label: "一级分类",
|
||||
children: renderChildren(),
|
||||
},
|
||||
];
|
||||
return <Tabs defaultActiveKey={grade} items={items} onChange={onChange} />;
|
||||
return (
|
||||
<Tabs
|
||||
defaultActiveKey={grade as unknown as string}
|
||||
items={items}
|
||||
onChange={onChange}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProdCategory;
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import type { RequestOptions } from '@@/plugin-request/request';
|
||||
import type { RequestConfig } from '@umijs/max';
|
||||
import { request } from '@umijs/max';
|
||||
import { message } from 'antd';
|
||||
import { getAccessToken, getRefreshToken, setToken } from './utils/auth';
|
||||
import type { RequestOptions } from "@@/plugin-request/request";
|
||||
import type { RequestConfig } from "@umijs/max";
|
||||
import { request } from "@umijs/max";
|
||||
import { message } from "antd";
|
||||
import { getAccessToken, getRefreshToken, setToken } from "./utils/auth";
|
||||
|
||||
// const tenantEnable = process.env.VITE_APP_TENANT_ENABLE;
|
||||
// const { result_code, base_url, request_timeout } = config;
|
||||
@@ -27,8 +27,6 @@ interface ResponseStructure {
|
||||
// "刷新令牌已过期", // 使用刷新令牌,刷新获取新的访问令牌时,结果因为过期失败,此时需要忽略。否则,会导致继续 401,无法跳转到登出界面
|
||||
// ];
|
||||
|
||||
import { EventEmitter } from 'events';
|
||||
export const requestEventBus = new EventEmitter();
|
||||
// const errorCode: { [key: string]: string } = {
|
||||
// "400": "请求参数不正确",
|
||||
// "401": "账号未登录",
|
||||
@@ -51,8 +49,8 @@ export const requestEventBus = new EventEmitter();
|
||||
*/
|
||||
|
||||
const refreshToken = async () => {
|
||||
return await request('/system/auth/refresh-token', {
|
||||
method: 'POST',
|
||||
return await request("/system/auth/refresh-token", {
|
||||
method: "POST",
|
||||
params: { refreshToken: getRefreshToken() },
|
||||
});
|
||||
};
|
||||
@@ -95,7 +93,7 @@ export const errorConfig: RequestConfig = {
|
||||
const { success, data, code, msg } = res as unknown as ResponseStructure;
|
||||
if (!success) {
|
||||
const error: any = new Error(msg);
|
||||
error.name = 'BizError';
|
||||
error.name = "BizError";
|
||||
error.info = { code, msg, data };
|
||||
throw error; // 抛出自制的错误
|
||||
}
|
||||
@@ -104,9 +102,9 @@ export const errorConfig: RequestConfig = {
|
||||
errorHandler: async (error: any, opts: any) => {
|
||||
if (opts?.skipErrorHandler) throw error;
|
||||
// 我们的 errorThrower 抛出的错误。
|
||||
console.log('errorHandler', error);
|
||||
console.log("errorHandler", error);
|
||||
const errorInfo: ResponseStructure | undefined = error.info;
|
||||
if (error.name === 'BizError') {
|
||||
if (error.name === "BizError") {
|
||||
if (errorInfo) {
|
||||
const { msg } = errorInfo;
|
||||
message.error(msg);
|
||||
@@ -116,7 +114,7 @@ export const errorConfig: RequestConfig = {
|
||||
// 请求成功发出且服务器也响应了状态码,但状态代码超出了 2xx 的范围
|
||||
message.error(`Response status:${error.response.status}`);
|
||||
} else if (error.request) {
|
||||
message.error('None response! Please retry.');
|
||||
message.error("None response! Please retry.");
|
||||
} else {
|
||||
message.error(`发送请求时出了点问题:${error.msg}`);
|
||||
}
|
||||
@@ -127,7 +125,7 @@ export const errorConfig: RequestConfig = {
|
||||
requestInterceptors: [
|
||||
(config: RequestOptions) => {
|
||||
// 拦截请求配置,进行个性化处理。
|
||||
|
||||
console.log("requestInterceptors", config);
|
||||
return { ...config };
|
||||
},
|
||||
],
|
||||
@@ -145,8 +143,8 @@ export const errorConfig: RequestConfig = {
|
||||
// 未设置状态码则默认成功状态
|
||||
// 二进制数据则直接返回,例如说 Excel 导出
|
||||
if (
|
||||
response.request.responseType === 'blob' ||
|
||||
response.request.responseType === 'arraybuffer'
|
||||
response.request.responseType === "blob" ||
|
||||
response.request.responseType === "arraybuffer"
|
||||
) {
|
||||
// 注意:如果导出的响应为 json,说明可能失败了,不直接返回进行下载
|
||||
// if (response.data.type !== "application/json") {
|
||||
@@ -161,7 +159,7 @@ export const errorConfig: RequestConfig = {
|
||||
// return Promise.reject(msg);
|
||||
// }
|
||||
if (!config.url) {
|
||||
throw new Error('请求URL不能为空');
|
||||
throw new Error("请求URL不能为空");
|
||||
}
|
||||
// 发送请求时出了点问题
|
||||
if (code === 401) {
|
||||
@@ -174,7 +172,7 @@ export const errorConfig: RequestConfig = {
|
||||
// 2. 进行刷新访问令牌
|
||||
try {
|
||||
const refreshTokenRes = await refreshToken();
|
||||
console.log('刷新成功', refreshTokenRes);
|
||||
console.log("刷新成功", refreshTokenRes);
|
||||
// 2.1 刷新成功,则回放队列的请求 + 当前请求
|
||||
setToken(refreshTokenRes);
|
||||
// 发出 token 刷新事件
|
||||
@@ -190,12 +188,12 @@ export const errorConfig: RequestConfig = {
|
||||
}
|
||||
return request(config.url, config);
|
||||
} else {
|
||||
console.log('刷新令牌失败');
|
||||
console.log("刷新令牌失败");
|
||||
//添加到队列,等待刷新获取到新的令牌
|
||||
return new Promise((resolve) => {
|
||||
requestList.push(() => {
|
||||
if (!config.url) {
|
||||
throw new Error('请求URL不能为空');
|
||||
throw new Error("请求URL不能为空");
|
||||
}
|
||||
if (config.headers)
|
||||
config.headers.Authorization = `Bearer ${getAccessToken()}`; // 让每个请求携带自定义token 请根据实际情况自行修改
|
||||
@@ -208,7 +206,7 @@ export const errorConfig: RequestConfig = {
|
||||
return Promise.reject(data);
|
||||
}
|
||||
if (data?.success === false) {
|
||||
message.error('请求失败!');
|
||||
message.error("请求失败!");
|
||||
return Promise.reject(data);
|
||||
}
|
||||
|
||||
|
||||
45
src/services/infra/file/index.ts
Normal file
45
src/services/infra/file/index.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { request } from "@umijs/max";
|
||||
|
||||
export interface FilePageReqVO extends PageParam {
|
||||
path?: string;
|
||||
type?: string;
|
||||
createTime?: Date[];
|
||||
}
|
||||
|
||||
// 文件预签名地址 Response VO
|
||||
export interface FilePresignedUrlRespVO {
|
||||
// 文件配置编号
|
||||
configId: number;
|
||||
// 文件上传 URL
|
||||
uploadUrl: string;
|
||||
// 文件 URL
|
||||
url: string;
|
||||
}
|
||||
|
||||
// 查询文件列表
|
||||
// export const getFilePage = (params: FilePageReqVO) => {
|
||||
// return request.get({ url: "/infra/file/page", params });
|
||||
// };
|
||||
|
||||
// // 删除文件
|
||||
// export const deleteFile = (id: number) => {
|
||||
// return request.delete({ url: "/infra/file/delete?id=" + id });
|
||||
// };
|
||||
|
||||
// // 获取文件预签名地址
|
||||
// export const getFilePresignedUrl = (path: string) => {
|
||||
// return request.get<FilePresignedUrlRespVO>({
|
||||
// url: "/infra/file/presigned-url",
|
||||
// params: { path },
|
||||
// });
|
||||
// };
|
||||
|
||||
// // 创建文件
|
||||
// export const createFile = (data: any) => {
|
||||
// return request.post({ url: "/infra/file/create", data });
|
||||
// };
|
||||
|
||||
// // 上传文件
|
||||
// export const updateFile = (data: any) => {
|
||||
// return request.upload({ url: "/infra/file/upload", data });
|
||||
// };
|
||||
8
src/services/infra/media/index.ts
Normal file
8
src/services/infra/media/index.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { request } from "@umijs/max";
|
||||
|
||||
export async function uploadImage(data: FormData) {
|
||||
return request<string>("/infra/file/upload", {
|
||||
method: "POST",
|
||||
data,
|
||||
});
|
||||
}
|
||||
4
src/types/global.d.ts
vendored
4
src/types/global.d.ts
vendored
@@ -75,4 +75,8 @@ declare global {
|
||||
total: number; // 总量
|
||||
msg: string;
|
||||
}
|
||||
|
||||
declare const VITE_BASE_URL: string;
|
||||
declare const API_URL: string;
|
||||
declare const UMI_ENV: string;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user