Commit 7638a8c7 authored by wuhao's avatar wuhao 🎯

ader

parent 8c144095
No preview for this file type
# Ant Design Pro
本项目使用[Ant Design Pro](https://pro.ant.design)进行初始化。以下是如何使用的快速指南。
## 环境准备
安装 `node_modules`:
```bash
npm install
```
or
```bash
yarn
```
## Provided Scripts
Ant Design Pro提供了一些有用的脚本来帮助您快速启动和构建web项目,代码风格检查和测试。
`package.json`中提供的脚本。修改或添加其他脚本是安全的:
### 启动项目
```bash
npm start
```
### 构建项目
```bash
npm run build
```
### 检查代码风格
```bash
npm run lint
```
### 你也可以使用脚本自动修复一些lint错误:
```bash
npm run lint:fix
```
### 测试代码
```bash
npm test
```
## More
[github](https://github.com/ant-design/ant-design-pro).
// https://umijs.org/config/
import { defineConfig } from '@umijs/max';
import env from './env';
import proxy from './proxy';
import routes from './routes';
const { REACT_APP_ENV = 'dev', NODE_ENV = 'development' }: any = process.env;
export default defineConfig({
/**
* @name 开启 hash 模式
* @description 让 build 之后的产物包含 hash 后缀。通常用于增量发布和避免浏览器加载缓存。
* @doc https://umijs.org/docs/api/config#hash
*/
hash: true,
history: {
type: 'hash',
},
publicPath: NODE_ENV === 'production' ? './' : '/',
/**
* @name 兼容性设置
* @description 设置 ie11 不一定完美兼容,需要检查自己使用的所有依赖
* @doc https://umijs.org/docs/api/config#targets
*/
// targets: {
// ie: 11,
// },
/**
* @name 路由的配置,不在路由中引入的文件不会编译
* @description 只支持 path,component,routes,redirect,wrappers,title 的配置
* @doc https://umijs.org/docs/guides/routes
*/
// umi routes: https://umijs.org/docs/routing
routes,
/**
* @name 主题的配置
* @description 虽然叫主题,但是其实只是 less 的变量设置
* @doc antd的主题设置 https://ant.design/docs/react/customize-theme-cn
* @doc umi 的theme 配置 https://umijs.org/docs/api/config#theme
*/
theme: {
// 如果不想要 configProvide 动态设置主题需要把这个设置为 default
// 只有设置为 variable, 才能使用 configProvide 动态设置主色调
'root-entry-name': 'variable',
},
/**
* @name moment 的国际化配置
* @description 如果对国际化没有要求,打开之后能减少js的包大小
* @doc https://umijs.org/docs/api/config#ignoremomentlocale
*/
ignoreMomentLocale: true,
/**
* @name 代理配置
* @description 可以让你的本地服务器代理到你的服务器上,这样你就可以访问服务器的数据了
* @see 要注意以下 代理只能在本地开发时使用,build 之后就无法使用了。
* @doc 代理介绍 https://umijs.org/docs/guides/proxy
* @doc 代理配置 https://umijs.org/docs/api/config#proxy
*/
proxy: proxy[REACT_APP_ENV as keyof typeof proxy],
/**
* @name 快速热更新配置
* @description 一个不错的热更新组件,更新时可以保留 state
*/
fastRefresh: true,
//============== 以下都是max的插件配置 ===============
/**
* @name 数据流插件
* @@doc https://umijs.org/docs/max/data-flow
*/
model: {},
/**
* 一个全局的初始数据流,可以用它在插件之间共享数据
* @description 可以用来存放一些全局的数据,比如用户信息,或者一些全局的状态,全局初始状态在整个 Umi 项目的最开始创建。
* @doc https://umijs.org/docs/max/data-flow#%E5%85%A8%E5%B1%80%E5%88%9D%E5%A7%8B%E7%8A%B6%E6%80%81
*/
initialState: {},
/**
* @name layout 插件
* @doc https://umijs.org/docs/max/layout-menu
*/
locale: {
// default zh-CN
default: 'zh-CN',
antd: true,
// default true, when it is true, will use `navigator.language` overwrite default
baseNavigator: true,
},
/**
* @name antd 插件
* @description 内置了 babel import 插件
* @doc https://umijs.org/docs/max/antd#antd
*/
antd: {},
/**
* @name 网络请求配置
* @description 它基于 axios 和 ahooks 的 useRequest 提供了一套统一的网络请求和错误处理方案。
* @doc https://umijs.org/docs/max/request
*/
request: {},
/**
* @name 权限插件
* @description 基于 initialState 的权限插件,必须先打开 initialState
* @doc https://umijs.org/docs/max/access
*/
access: {},
//================ pro 插件配置 =================
presets: ['umi-presets-pro'],
mfsu: {},
requestRecord: {},
define: env[REACT_APP_ENV as keyof typeof env],
title: '精密测量虚拟仿真实训平台',
});
import { Settings as LayoutSettings } from '@ant-design/pro-components';
/**
* @name
*/
const Settings: LayoutSettings & {
pwa?: boolean;
logo?: string;
} = {
navTheme: 'light',
// 拂晓蓝
colorPrimary: '#1890ff',
layout: 'mix',
contentWidth: 'Fluid',
fixedHeader: false,
fixSiderbar: true,
colorWeak: false,
title: 'Ant Design Pro',
pwa: false,
logo: 'https://gw.alipayobjects.com/zos/rmsportal/KDpgvguMpGfqaHPjicRK.svg',
iconfontUrl: '',
};
export default Settings;
export default {
dev:{
"REACT_APP_URL":"/vstp", // http://tasks-dev.nangaoyun.com
"DEFAULT_HEAD_IMG":"./assets/images/avatars/avatar_21.jpg",
"DEFAULT_404_IMG":"./assets/illustrations/illustration_404.svg"
},
test:{
"REACT_APP_URL":"http://118.25.178.150:7001"
},
prod:{
"REACT_APP_URL":"http://tasks.nangaoyun.com"
}
}
This diff is collapsed.
/**
* @name 代理的配置
* @see 在生产环境 代理是无法生效的,所以这里没有生产环境的配置
* -------------------------------
* The agent cannot take effect in the production environment
* so there is no configuration of the production environment
* For details, please see
* https://pro.ant.design/docs/deploy
*
* @doc https://umijs.org/docs/guides/proxy
*/
export default {
dev: {
"/vstp/": {
target: "http://192.168.40.117:8044/vstp",
changeOrigin: true,
pathRewrite: { "^/vstp": "" },
},
},
/**
* @name 详细的代理配置
* @doc https://github.com/chimurai/http-proxy-middleware
*/
test: {
// localhost:8000/api/** -> https://preview.pro.ant.design/api/**
"/api/": {
target: "https://proapi.azurewebsites.net",
changeOrigin: true,
pathRewrite: { "^": "" },
},
},
pre: {
"/api/": {
target: "your pre url",
changeOrigin: true,
pathRewrite: { "^": "" },
},
},
};
/**
* @name umi 的路由配置
* @description 只支持 path,component,routes,redirect,wrappers,name,icon 的配置
* @param path path 只支持两种占位符配置,第一种是动态参数 :id 的形式,第二种是 * 通配符,通配符只能出现路由字符串的最后。
* @param component 配置 location 和 path 匹配后用于渲染的 React 组件路径。可以是绝对路径,也可以是相对路径,如果是相对路径,会从 src/pages 开始找起。
* @param routes 配置子路由,通常在需要为多个路径增加 layout 组件时使用。
* @param redirect 配置路由跳转
* @param wrappers 配置路由组件的包装组件,通过包装组件可以为当前的路由组件组合进更多的功能。 比如,可以用于路由级别的权限校验
* @param name 配置路由的标题,默认读取国际化文件 menu.ts 中 menu.xxxx 的值,如配置 name 为 login,则读取 menu.ts 中 menu.login 的取值作为标题
* @param icon 配置路由的图标,取值参考 https://ant.design/components/icon-cn, 注意去除风格后缀和大小写,如想要配置图标为 <StepBackwardOutlined /> 则取值应为 stepBackward 或 StepBackward,如想要配置图标为 <UserOutlined /> 则取值应为 user 或者 User
* @doc https://umijs.org/docs/guides/routes
*/
export default [
{
path: "/",
layout: "./layouts/index",
routes: [
{
path: "/",
redirect: "/work",
},
{
path: "/user",
component: "@/layouts/login/index",
routes: [
{
path: "/user",
redirect: "/user/login",
},
{
name: "登录",
path: "/user/login",
component: "./user/login",
},
{
name: "注册",
path: "/user/signup",
component: "./user/signup",
},
],
},
{
path: "/work",
name: "欢迎使用",
icon: "smile",
component: "@/layouts/dashboard/index",
routes: [
{
path: "/work",
redirect: "/work/homepage",
},
{
name: "个人主页",
path: "/work/homepage",
component: "./homepage",
},
{
name: "个人中心",
path: "/work/usercenter",
component: "./usercenter",
},
{
name: "组织管理",
path: "/work/organization",
component: "./organization",
},
{
name: "教师管理",
path: "/work/teacher",
component: "./teacher",
},
{
name: "学生管理",
path: "/work/student",
component: "./student",
},
{
name: "班级管理",
path: "/work/class",
component: "./class",
},
{
name: "模型管理",
path: "/work/model",
component: "./model",
},
{
name: "平台日志",
path: "/work/logs",
component: "./logs",
},
{
name: "课程管理",
path: "/work/lessons",
component: "./lessons",
},
{
name: "备课",
path: "/work/dolessons/:id",
component: "./dolessons",
},
],
},
{
path: "/share/:id",
name: "项目详情",
component: "./share",
},
{
path: "/dashboard",
name: "管理平台",
component: "./dashboard",
routes: [
{
path: "/dashboard",
redirect: "/dashboard/user",
},
{
name: "用户",
path: "/dashboard/user",
component: "./dashboard/user",
},
{
name: "组织",
path: "/dashboard/org",
component: "./dashboard/org",
},
],
},
],
},
{
path: "*",
layout: false,
component: "./404",
},
];
ssh root@49.235.82.163 "rm -rf /opt/react-reveal-ppt/jmcl/*"
echo 'deleted /opt/react-reveal-ppt/jmcl'
scp -r ./dist/* root@49.235.82.163:/opt/react-reveal-ppt/jmcl/
echo 'sync complete'
\ No newline at end of file
ssh root@118.25.178.150 "rm -rf /usr/share/nginx/html/tasks/*"
echo 'deleted /usr/share/nginx/html/tasks/*'
scp -r ./dist/* root@118.25.178.150:/usr/share/nginx/html/tasks/
echo 'sync complete'
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + React</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>
import { configUmiAlias, createConfig } from '@umijs/max/test';
export default async () => {
const config = await configUmiAlias({
...createConfig({
target: 'browser',
}),
});
return {
...config,
testEnvironmentOptions: {
...(config?.testEnvironmentOptions || {}),
url: 'http://localhost:8000',
},
setupFiles: [...(config.setupFiles || []), './tests/setupTests.jsx'],
globals: {
...config.globals,
localStorage: null,
},
};
};
{
"compilerOptions": {
"jsx": "react-jsx",
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
}
}
{
"name": "react",
"name": "ant-design-pro",
"version": "6.0.0",
"private": true,
"version": "0.0.0",
"type": "module",
"description": "An out-of-box UI solution for enterprise applications",
"scripts": {
"dev": "vite",
"": "",
"build": "vite build",
"preview": "vite preview"
"analyze": "cross-env ANALYZE=1 max build",
"build": "cross-env REACT_APP_ENV=prod max build",
"build:dev": "cross-env REACT_APP_ENV=dev max build",
"build:test": "cross-env REACT_APP_ENV=test max build",
"deploy": "npm run build && npm run gh-pages",
"dev": "npm run start:dev",
"dev:test": "cross-env REACT_APP_ENV=test MOCK=none UMI_ENV=dev max dev",
"prod": "cross-env REACT_APP_ENV=prod MOCK=none UMI_ENV=dev max dev",
"gh-pages": "gh-pages -d dist",
"i18n-remove": "pro i18n-remove --locale=zh-CN --write",
"postinstall": "max setup",
"jest": "jest",
"lint": "npm run lint:js && npm run lint:prettier && npm run tsc",
"lint-staged": "lint-staged",
"lint-staged:js": "eslint --ext .js,.jsx,.ts,.tsx ",
"lint:fix": "eslint --fix --cache --ext .js,.jsx,.ts,.tsx --format=pretty ./src ",
"lint:js": "eslint --cache --ext .js,.jsx,.ts,.tsx --format=pretty ./src",
"lint:prettier": "prettier -c --write \"**/**.{js,jsx,tsx,ts,less,md,json}\" --end-of-line auto",
"openapi": "max openapi",
"prettier": "prettier -c --write \"**/**.{js,jsx,tsx,ts,less,md,json}\"",
"preview": "npm run build && max preview --port 8000",
"record": "cross-env NODE_ENV=development REACT_APP_ENV=test max record --scene=login",
"serve": "umi-serve",
"start": "cross-env UMI_ENV=dev max dev",
"start:dev": "cross-env REACT_APP_ENV=dev MOCK=none UMI_ENV=dev max dev",
"start:no-mock": "cross-env MOCK=none UMI_ENV=dev max dev",
"start:pre": "cross-env REACT_APP_ENV=pre UMI_ENV=dev max dev",
"test": "jest",
"test:coverage": "npm run jest -- --coverage",
"test:update": "npm run jest -- -u",
"tsc": "tsc --noEmit"
},
"lint-staged": {
"**/*.{js,jsx,ts,tsx}": "npm run lint-staged:js",
"**/*.{js,jsx,tsx,ts,less,md,json}": [
"prettier --write"
]
},
"browserslist": [
"> 1%",
"last 2 versions",
"not ie <= 10"
],
"resolutions": {
"react-error-overlay": "6.0.9"
},
"dependencies": {
"@ant-design/icons": "^5.0.1",
"@ant-design/pro-components": "^2.4.4",
"@ant-design/use-emotion-css": "1.0.4",
"@emotion/react": "^11.10.6",
"@emotion/styled": "^11.10.6",
"@faker-js/faker": "^7.6.0",
......@@ -23,42 +65,72 @@
"@mui/material": "^5.11.16",
"@mui/styled-engine-sc": "^5.11.11",
"@reduxjs/toolkit": "^1.9.3",
"@umijs/route-utils": "^2.1.3",
"ahooks": "^3.7.6",
"antd": "^5.4.2",
"antd-img-crop": "^4.12.2",
"apexcharts": "^3.37.0",
"braft-editor": "^2.3.9",
"change-case": "^4.1.2",
"classnames": "^2.3.2",
"crypto-js": "^4.1.1",
"date-fns": "^2.29.3",
"dayjs": "^1.11.7",
"gantt-task-react": "^0.3.9",
"history": "^5.3.0",
"lodash": "^4.17.21",
"moment": "^2.29.4",
"numeral": "^2.0.6",
"omit.js": "^2.0.2",
"prop-types": "^15.8.1",
"rc-menu": "^9.6.4",
"rc-util": "^5.24.4",
"react": "^18.2.0",
"react-apexcharts": "^1.4.0",
"react-beautiful-dnd": "^13.1.1",
"react-color": "^2.19.3",
"react-custom-scrollbars": "^4.2.1",
"react-dev-inspector": "^1.8.1",
"react-dom": "^18.2.0",
"react-draggable": "^4.4.5",
"react-helmet-async": "^1.3.0",
"react-hook-form": "^7.43.1",
"react-redux": "^8.0.5",
"react-resizable": "^3.0.5",
"react-router-dom": "^6.9.0",
"react-scripts": "^5.0.1",
"redux": "^4.2.1",
"react-reveal": "^1.2.2",
"simplebar-react": "^3.2.1",
"socket.io-client": "^2.2.1",
"styled-components": "^5.3.9",
"umi-request": "^1.4.0",
"web-vitals": "^3.1.1"
},
"devDependencies": {
"@rollup/plugin-terser": "^0.4.1",
"@types/react": "^18.0.28",
"@types/react-dom": "^18.0.11",
"@vitejs/plugin-react": "^3.1.0",
"less": "^4.1.3",
"less-loader": "^11.1.0",
"vite": "^4.2.0",
"vite-plugin-html": "^3.2.0"
"@ant-design/pro-cli": "^2.1.0",
"@testing-library/react": "^13.4.0",
"@types/classnames": "^2.3.1",
"@types/express": "^4.17.14",
"@types/history": "^4.7.11",
"@types/jest": "^29.2.1",
"@types/lodash": "^4.14.186",
"@types/react": "^17.0.0",
"@types/react-dom": "^17.0.0",
"@types/react-helmet": "^6.1.5",
"@umijs/lint": "^4.0.34",
"@umijs/max": "^4.0.33",
"cross-env": "^7.0.3",
"eslint": "^8.0.0",
"express": "^4.18.2",
"gh-pages": "^3.2.0",
"jest": "^29.2.2",
"jest-environment-jsdom": "^29.2.2",
"lint-staged": "^10.0.0",
"mockjs": "^1.1.0",
"prettier": "^2.7.1",
"react-error-overlay": "^6.0.11",
"swagger-ui-dist": "^4.14.2",
"ts-node": "^10.9.1",
"typescript": "^4.8.4",
"umi-presets-pro": "^2.0.0"
},
"engines": {
"node": ">=12.0.0"
}
}
This diff is collapsed.
preview.pro.ant.design
\ No newline at end of file
import store from "@/store";
import { Provider, useSelector } from "react-redux";
import CssBaseline from "@mui/material/CssBaseline";
import Router from "./router";
import { HelmetProvider } from "react-helmet-async";
import React from "react";
import "./App.less";
import Slide from "@mui/material/Slide";
import { useDispatch } from "react-redux";
import { changemessage } from "@/store/message";
import ThemeProvider from "./theme";
import Snackbar from "@mui/material/Snackbar";
import MuiAlert from "@mui/material/Alert";
import { AlertTitle } from "@mui/material";
/* eslint-disable @typescript-eslint/no-unused-vars */
import { history } from "@umijs/max";
import { errorConfig } from "./requestErrorConfig";
import { doFetch } from "./utils/doFetch";
const loginPath = "/user/login";
const Alert = React.forwardRef(function Alert(props, ref) {
return <MuiAlert elevation={6} ref={ref} variant="filled" {...props} />;
});
function Dom() {
const dispatch = useDispatch();
const message = useSelector((state) => state.message);
const handleClose = (event) => {
dispatch(
changemessage({
...message,
/**
* @see https://umijs.org/zh-CN/plugins/plugin-initial-state
* */
export async function getInitialState() {
const fetchUserInfo = async () => {
try {
const msg = await doFetch({
url: "/system/me",
});
return msg.data.data;
} catch (error) {
history.push(loginPath);
}
return undefined;
};
// 如果不是登录页面,执行
const { location } = history;
if (!location.pathname.includes("user")) {
const currentUser = await fetchUserInfo();
localStorage.setItem("ID", currentUser?.id);
return {
fetchUserInfo,
currentUser,
activeUserIdList: [],
vs: false,
nav: 280,
message: {
open: false,
})
);
snackbar: {
autoHideDuration: 2000,
anchorOrigin: {
vertical: "bottom",
horizontal: "right",
},
},
type: "success",
content: "",
},
};
}
return {
fetchUserInfo,
activeUserIdList: [],
vs: false,
message: {
open: false,
snackbar: {
autoHideDuration: 2000,
anchorOrigin: {
vertical: "bottom",
horizontal: "right",
},
},
type: "success",
content: "",
},
nav: 280,
};
// 自定义主题
// const { palette } = useTheme();
// const theme = useMemo(() => {
// return createTheme(curthemeconfig);
// }, [curthemeconfig]);
return (
<ThemeProvider>
{/* <div style={{ position: "fixed", right: 36, top: 36 }}>
<IconButton
onClick={() => {
if (curthemeconfig.palette.mode == "dark") {
dispatch(changetheme("lightTheme"));
} else {
dispatch(changetheme("darkTheme"));
}
}}
>
{curthemeconfig.palette.mode !== "dark" ? (
<WbSunnyIcon style={{ color: "#ff9900" }}></WbSunnyIcon>
) : (
<DarkModeIcon></DarkModeIcon>
)}
</IconButton>
</div> */}
<CssBaseline />
<Snackbar
open={message?.open}
TransitionComponent={(props) => <Slide {...props} direction="left" />}
{...message?.snackbar}
onClose={handleClose}
>
<Alert
severity={message?.type}
onClose={handleClose}
{...message.alert}
>
{message?.title ? <AlertTitle>{message?.title}</AlertTitle> : null}
{message?.content}
</Alert>
</Snackbar>
<Router />
</ThemeProvider>
);
}
// 路由&theme+mui
function App() {
return (
<HelmetProvider>
<Provider store={store}>
<Dom></Dom>
</Provider>
</HelmetProvider>
);
}
export default App;
/**
* @name request 配置,可以配置错误处理
* 它基于 axios 和 ahooks 的 useRequest 提供了一套统一的网络请求和错误处理方案。
* @doc https://umijs.org/docs/max/request#配置
*/
export const request = {
...errorConfig,
};
#root {
margin: 0 auto;
//color: rgba(209, 233, 252, 0.347);
}
.react-resizable {
position: relative;
}
.table-cell {
display: -webkit-box;
//width: 100%;
overflow: hidden;
text-overflow: ellipsis;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
}
.react-resizable-handle {
position: absolute;
right: -5px;
bottom: 0;
z-index: 999;
width: 10px;
height: 100%;
background: transparent;
cursor: col-resize;
}
.anticon {
font-size: 14px !important;
color: #637381 !important;
}
.ant-select {
.anticon-close-circle {
font-size: 14px !important;
margin-top: -2px;
margin-left: -2px;
}
}
.ant-pro-card {
border-radius: 12px !important;
}
.ant-pro-card-body {
padding-inline: 0 !important;
}
.ant-pro-table-list-toolbar-container {
padding-inline: 8px;
}
.ant-pro-table-list-toolbar-right {
flex: 1 !important;
flex-direction: row !important;
align-items: center !important;
justify-content: space-between !important;
}
.ant-pro-table-list-toolbar-left {
flex: 0;
}
.ant-table-pagination.ant-pagination {
padding-inline: 8px;
}
//清除圆角
.ant-table-wrapper
.ant-table-container
table
> thead
> tr:first-child
> *:last-child,
.ant-table-wrapper
.ant-table-container
table
> thead
> tr:first-child
> *:first-child {
border-radius: 0 !important;
}
//清楚arrow
.ant-popover-arrow {
display: none !important;
}
......@@ -33,7 +33,7 @@ const PRODUCT_COLOR = ['#00AB55', '#000000', '#FFFFFF', '#FFC0CB', '#FF4842', '#
// ----------------------------------------------------------------------
const products = [...Array(24)].map((_, index) => {
const products = [...Array(5)].map((_, index) => {
const setIndex = index + 1;
return {
......
/**
* @see https://umijs.org/zh-CN/plugins/plugin-access
* */
export default function access(initialState: { currentUser?: API.CurrentUser } | undefined) {
const { currentUser } = initialState ?? {};
return {
canAdmin: currentUser && currentUser.access === 'admin',
};
}
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>
\ No newline at end of file
/* eslint-disable react-hooks/exhaustive-deps */
/* eslint-disable react-hooks/rules-of-hooks */
import React, { useEffect, useRef, useState, memo, useMemo } from "react";
import { doFetch } from "@/utils/doFetch";
import { ProTable } from "@ant-design/pro-components";
import Resizecell from "./Resizecell";
import { Tooltip } from "@mui/material";
import { doFetch } from "@/utils/doFetch";
import { useAsyncEffect } from "ahooks";
import { memo, useEffect, useRef, useState } from "react";
import Resizecell from "./Resizecell";
let handlEmptyChild = (tree = []) => {
const newtree = tree.map((item) => {
......@@ -23,7 +23,7 @@ let handlEmptyChild = (tree = []) => {
return newtree;
};
const Mtable = (props) => {
const AutoTable = (props) => {
const {
actionRef, //表格动作
formRef, //表单Ref
......@@ -35,7 +35,8 @@ const Mtable = (props) => {
pageSize, //修改默认pageSize
pagination, //分页设置
x, //横向滚动
resizeable = true,
resizeable = false,
rerendered = true,
} = props;
const actionRefs = actionRef ?? useRef(),
......@@ -46,7 +47,8 @@ const Mtable = (props) => {
const [columnes, setcolumnes] = useState(
columns?.filter?.((it) => it.valueType != "split") ?? []
);
const [newparames, setnewparams] = useState({});
const [paramconfig, setparamconfig] = useState({});
//调用接口
const request = async (params, sort, filter) => {
......@@ -64,7 +66,7 @@ const Mtable = (props) => {
}
const result = await doFetch({ url: path, params: newparams });
//分页结果
let data = result?.data?.page?.list,
let data = result?.data?.page?.records,
success = true,
total = result?.data?.page?.total;
//不带分页获取结果
......@@ -79,7 +81,7 @@ const Mtable = (props) => {
};
};
function changeColumns(allcol = {}) {
useEffect(() => {
setcolumnes((s) => {
return s
.filter((it) => it.valueType != "split")
......@@ -89,13 +91,6 @@ const Mtable = (props) => {
if (it.valueType == "option") {
curkey = "option";
}
let itemwidth = allcol[curkey]?.width
? allcol[curkey].width
: it.width
? it.width
: resizeable
? 160
: "auto";
let options = {};
if (
["select", "treeSelect", "radio", "checkbox", "cascader"].includes(
......@@ -107,16 +102,23 @@ const Mtable = (props) => {
fieldProps: {
...it?.fieldProps,
options: [...it.options],
dropdownMatchSelectWidth: 200,
},
};
} else if (it.options) {
options = {
params: newparames,
request: async (params) => {
fieldProps: {
...it?.fieldProps,
dropdownMatchSelectWidth: 200,
showSearch: true,
},
params: paramconfig[curkey],
request: async (parames) => {
delete parames?.keyWords;
if (Object.keys(it?.options).includes("linkParams")) {
let list = await doFetch({
url: it?.options?.path,
params: newparames,
params: { ...parames, isAll: 1 },
});
const res = list.data.dataList;
return it.valueType == "treeSelect"
......@@ -160,7 +162,6 @@ const Mtable = (props) => {
}
options = {
...options,
width: itemwidth,
};
delete it.formItemProps;
......@@ -168,49 +169,18 @@ const Mtable = (props) => {
...it,
...options,
onHeaderCell: (column) => ({
width: column.width ?? itemwidth,
width: column.width,
onResize: handleResize(index),
onResizeStop: handleResizeStop(index),
}),
};
});
});
}
//初始化操作数据
const initDrage = async () => {
if (!path) return;
//allcol 默认状态设置 valueColumns 为columns全列设置
let allcol = {};
columns
.filter((it) => it.valueType != "split")
.map((it, i) => {
if (it.valueType == "option") {
allcol.option = {
order: columns.length - 1,
show: true,
fixed: "right",
};
} else {
allcol[it.key ?? it.dataIndex] = {
order: i,
show: true,
};
}
});
setvalueColumns(allcol);
return allcol;
};
}, [paramconfig]);
//调用重新渲染表格
useAsyncEffect(async () => {
let allcol = {};
if (resizeable) {
allcol = await initDrage();
}
changeColumns(allcol);
actionRefs?.current?.reload();
}, [columns, extraparams, path ]);
rerendered && actionRefs?.current?.reload();
}, [path, columns, extraparams]);
//缩放表格
const handleResize =
......@@ -235,7 +205,6 @@ const Mtable = (props) => {
setvalueColumns((s) => {
let submitdata = { ...s } ?? {},
curkey = columnes[index]?.key ?? columnes[index]?.dataIndex;
console.log(curkey, size.width);
if (!curkey) return;
submitdata[curkey] = submitdata[curkey] ?? {};
submitdata[curkey].width = parseInt(size.width);
......@@ -265,27 +234,49 @@ const Mtable = (props) => {
},
}
: {};
return (
<ProTable
{...props}
{...components}
size={size}
onSubmit={(params) => {
let newparams = {};
columns.map((it, i) => {
if (
it?.options?.linkParams &&
Object.keys(it?.options?.linkParams).includes(
Object.keys(params)[0]
)
) {
for (let dataindex in it?.options?.linkParams) {
newparams[dataindex] =
formRefs?.current?.getFieldValue?.(dataindex);
form={{
onValuesChange: (changedValues, values) => {
let curchangekey = Object.keys(changedValues)[0];
let newparams = {},
resetkeys = [];
columns.map((it, i) => {
const { linkParams } = it?.options ?? {};
if (linkParams && Object.keys(linkParams).includes(curchangekey)) {
for (let dataindex in linkParams) {
let linkkey = "";
if (!linkParams[dataindex]) {
linkkey = dataindex;
} else {
linkkey = linkParams[dataindex];
}
newparams[linkkey] =
formRefs?.current?.getFieldValue?.(dataindex);
resetkeys.push(it?.key);
}
}
});
const resetkey = resetkeys?.filter((it) => it !== curchangekey)?.[0];
if (resetkey) {
formRefs?.current?.setFieldsValue({ [resetkey]: "" });
}
});
setnewparams(newparams);
if (Object?.keys?.(newparams)?.length > 0) {
setparamconfig((s) => ({
...s,
[resetkey]: newparams,
}));
}
},
}}
onSubmit={(params) => {
// formRef?.current?
}}
onSizeChange={(size) => {
localStorage.setItem("size", size); //设置全局表格规格缓存
......@@ -298,23 +289,20 @@ const Mtable = (props) => {
rowKey={rowKey ?? "id"} //表格每行数据的key
dateFormatter="string"
request={request}
pagination={
ifspagination
? false
: {
showTotal: (total, range) => <span>{total}</span>,
showQuickJumper: true,
showSizeChanger: true,
pageSizeOptions: [5, 10, 15, 30, 50, 100, 200],
defaultPageSize: pageSize || 15,
}
}
pagination={{
size: !ifspagination ? "default" : "small",
showTotal: (total, range) => <span>{total}</span>,
showQuickJumper: true,
showSizeChanger: true,
pageSizeOptions: [5, 10, 15, 30, 50, 100, 200],
defaultPageSize: pageSize || 15,
}}
search={{
filterType: "light", //轻量模式
placement: 'bottomLeft',
placement: "bottomLeft",
}}
/>
);
};
export default memo(Mtable);
export default memo(AutoTable);
/* eslint-disable react-hooks/exhaustive-deps */
import { Grid } from 'antd';
import BraftEditor from 'braft-editor';
import 'braft-editor/dist/index.css';
import { memo, useMemo } from 'react';
const { useBreakpoint } = Grid;
function EditorItem({ value, onChange, height, serverURL, style, bordered }) {
const screens = useBreakpoint();
let UploadFn = (param) => {
const xhr = new XMLHttpRequest();
const token = localStorage.getItem('TOKENES');
const fd = new FormData();
const successFn = (response) => {
// 假设服务端直接返回文件上传后的地址
// 上传成功后调用param.success并传入上传后的文件地址
param.success({
url: xhr.responseText ? JSON.parse(xhr.responseText)?.url : null,
meta: {
id: parseInt(Math.random() * 100000),
title: param.file.name,
alt: param.file.name,
loop: true, // 指定音视频是否循环播放
autoPlay: true, // 指定音视频是否自动播放
controls: true, // 指定音视频是否显示控制栏
poster: 'http://xxx/xx.png', // 指定视频播放器的封面
},
});
};
const progressFn = (event) => {
// 上传进度发生变化时调用param.progress
param.progress((event.loaded / event.total) * 100);
};
const errorFn = (response) => {
// 上传发生错误时调用param.error
param.error({
msg: '上传失败',
});
};
xhr.upload.addEventListener('progress', progressFn, false);
xhr.addEventListener('load', successFn, false);
xhr.addEventListener('error', errorFn, false);
xhr.addEventListener('abort', errorFn, false);
fd.append('file', param.file);
xhr.open('POST', serverURL ? serverURL : `${REACT_APP_URL}/webtool/upload`, true);
xhr.setRequestHeader('Authorization', token);
xhr.send(fd);
};
const controls = useMemo(() => {
if (screens.md) {
return [
'undo',
'redo',
'separator',
'font-size',
'line-height',
'letter-spacing',
'separator',
'text-color',
'bold',
'italic',
'underline',
'strike-through',
'separator',
'superscript',
'subscript',
'remove-styles',
'emoji',
'separator',
'text-indent',
'text-align',
'separator',
'headings',
'list-ul',
'list-ol',
'blockquote',
'code',
'separator',
'link',
'separator',
'hr',
'separator',
'media',
'separator',
'clear',
];
} else {
return [
'undo',
'redo',
'separator',
'font-size',
'line-height',
'separator',
'text-color',
'separator',
'text-align',
'separator',
'list-ul',
'list-ol',
'separator',
'link',
'separator',
'hr',
'separator',
'media',
'separator',
'clear',
];
}
}, [screens]);
return (
<div
style={{
...style,
border: '#ddd solid 1px',
height: height ? height : 400,
overflow: 'hidden',
borderRadius: 12,
}}
>
<BraftEditor
controls={controls}
media={{ uploadFn: UploadFn }}
value={BraftEditor.createEditorState(value)}
onChange={(val) => {
onChange(val);
}}
/>
</div>
);
}
export default memo(EditorItem);
'use strict';
import { useState } from 'react';
import { TwitterPicker } from 'react-color';
import reactCSS from 'reactcss';
let ColorPicker = ({ color, handleChange }) => {
const [displayColorPicker, setdisplayColorPicker] = useState();
let handleClick = () => {
setdisplayColorPicker(!displayColorPicker);
},
handleClose = () => {
setdisplayColorPicker(false);
};
const styles = reactCSS({
default: {
color: {
width: '14px',
height: '14px',
borderRadius: '14px',
background: color,
},
swatch: {
cursor: 'pointer',
},
popover: {
position: 'absolute',
zIndex: '2',
left: -7,
top: 38,
},
cover: {
position: 'fixed',
top: '0px',
right: '0px',
bottom: '0px',
left: '0px',
},
},
});
return (
<div style={{marginRight:4,position:"relative"}}>
<div style={styles.swatch} onClick={handleClick}>
<div style={styles.color} />
</div>
{displayColorPicker ? (
<div style={styles.popover}>
<div style={styles.cover} onClick={handleClose} />
<TwitterPicker color={color} onChangeComplete={handleChange} />
</div>
) : null}
</div>
);
};
export default ColorPicker;
import { useEffect, useState } from 'react';
import { PlusOutlined } from '@ant-design/icons';
import { Input, message, Space, Tag, theme, Tooltip } from 'antd';
import { useRef } from 'react';
import ColorPicker from './colorpicker';
const Tagadder = ({ value = [{ color: '#13c2c2', text: '123' }], onChange, max }) => {
const { token } = theme.useToken();
const [inputVisible, setInputVisible] = useState(false);
const [inputValue, setInputValue] = useState('');
const [editInputIndex, setEditInputIndex] = useState(-1);
const [editInputValue, setEditInputValue] = useState('');
const inputRef = useRef(null);
const editInputRef = useRef(null);
useEffect(() => {
if (inputVisible) {
inputRef.current?.focus();
}
}, [inputVisible]);
useEffect(() => {
editInputRef.current?.focus();
}, [inputValue]);
const handleClose = (removedTag) => {
const newTags = value.filter((tag) => tag.text !== removedTag);
onChange(newTags);
};
const showInput = () => {
setInputVisible(true);
};
const handleInputChange = (e) => {
setInputValue(e.target.value);
};
const handleInputConfirm = () => {
if (!inputValue || inputValue.replace(/\s/g, '') === '') {
message.destroy();
message.error('标签不可为空字符!');
setInputVisible(false);
return;
}
if (inputValue && value?.map((it) => it.text).indexOf(inputValue) === -1) {
onChange([
...value,
{
color: '#13c2c2',
text: inputValue,
tag_name: inputValue,
},
]);
} else {
if (inputValue) message.warning('已存在的标签名!');
}
setInputVisible(false);
setInputValue('');
};
const handleEditInputChange = (e) => {
setEditInputValue(e.target.value);
};
const handleEditInputConfirm = () => {
if (!editInputValue || editInputValue.replace(/\s/g, '') === '') {
message.destroy();
message.error('标签不可为空字符!');
return;
}
if (
editInputValue &&
value
?.filter((it, i) => i !== editInputIndex)
.map((it) => it.text)
.indexOf(editInputValue) === -1
) {
const newTags = [...value];
newTags[editInputIndex] = {
...newTags[editInputIndex],
text: editInputValue,
};
onChange(newTags);
setEditInputIndex(-1);
setInputValue('');
} else {
message.warning('已存在的标签名!');
}
};
const tagInputStyle = {
width: 78,
verticalAlign: 'top',
};
const tagPlusStyle = {
background: token.colorBgContainer,
borderStyle: 'dashed',
};
return (
<Space size={[0, 8]} wrap style={{ paddingTop: 4, justifyContent: 'flex-start' }}>
{value.map((tag, index) => {
if (editInputIndex === index) {
return (
<Input
ref={editInputRef}
key={tag}
size="small"
style={tagInputStyle}
value={editInputValue}
onChange={handleEditInputChange}
onBlur={handleEditInputConfirm}
onPressEnter={handleEditInputConfirm}
/>
);
}
const isLongTag = tag?.text?.length > 20;
const tagElem = (
<Tag
key={tag.text}
closable
style={{
userSelect: 'none',
display: 'flex',
alignItems: 'center',
}}
onClose={() => handleClose(tag.text)}
>
<ColorPicker
color={tag.color}
handleChange={(val) => {
let newval = JSON.parse(JSON.stringify(value));
newval = newval?.map((it, i) => {
if (index === i) {
it.color = val.hex;
}
return it;
});
onChange(newval);
}}
></ColorPicker>
<span
onDoubleClick={(e) => {
setEditInputIndex(index);
setEditInputValue(tag.text);
e.preventDefault();
}}
>
{isLongTag ? `${tag.text.slice(0, 20)}...` : tag.text}
</span>
</Tag>
);
return isLongTag ? (
<Tooltip title={tag.text} key={tag.text}>
{tagElem}
</Tooltip>
) : (
tagElem
);
})}
{inputVisible ? (
<Input
ref={inputRef}
type="text"
size="small"
style={tagInputStyle}
value={inputValue}
onChange={handleInputChange}
onBlur={handleInputConfirm}
onPressEnter={handleInputConfirm}
/>
) : (
value.length < max &&
editInputIndex === -1 && (
<Tag style={tagPlusStyle} onClick={showInput}>
<PlusOutlined /> 新建标签
</Tag>
)
)}
</Space>
);
};
export default Tagadder;
This diff is collapsed.
import { Modal } from 'antd';
import { useRef, useState } from 'react';
import Draggable from 'react-draggable';
const DragModal = (props) => {
const [disabled, setDisabled] = useState(false);
const [bounds, setBounds] = useState({
left: 0,
top: 0,
bottom: 0,
right: 0,
});
const draggleRef = useRef(null);
const onStart = (_event, uiData) => {
const { clientWidth, clientHeight } = window.document.documentElement;
const targetRect = draggleRef?.current?.getBoundingClientRect();
if (!targetRect) {
return;
}
setBounds({
left: -targetRect.left + uiData.x,
right: clientWidth - (targetRect.right - uiData.x),
top: -targetRect.top + uiData.y,
bottom: clientHeight - (targetRect.bottom - uiData.y),
});
};
return (
<Modal
{...props}
footer={false}
destroyOnClose
>
{props?.children}
</Modal>
);
};
export default DragModal;
import * as React from "react";
import { SendOutlined } from "@ant-design/icons";
import LoadingButton from "@mui/lab/LoadingButton";
import Button from "@mui/material/Button";
import Dialog from "@mui/material/Dialog";
import DialogActions from "@mui/material/DialogActions";
import DialogContent from "@mui/material/DialogContent";
import DialogContentText from "@mui/material/DialogContentText";
import DialogTitle from "@mui/material/DialogTitle";
import Paper from "@mui/material/Paper";
import Slide from "@mui/material/Slide";
import React from "react";
import Draggable from "react-draggable";
const Transition = React.forwardRef(function Transition(props, ref) {
return <Slide direction="up" ref={ref} {...props} />;
});
function PaperComponent(props) {
return (
<Draggable
......@@ -19,58 +25,63 @@ function PaperComponent(props) {
);
}
export default function DraggableDialog({ children }) {
const [open, setOpen] = React.useState(false);
export default function DraggableDialog({
children,
dialogprops,
handleClose,
loading,
formdom,
maxWidth,
}) {
const formRef = React.useRef();
const handleClickOpen = () => {
setOpen(true);
};
const handleClose = () => {
setOpen(false);
};
return (
<div>
<Button variant="outlined" onClick={handleClickOpen}>
Open draggable dialog
</Button>
<Dialog
maxWidth="md"
open={open}
onClose={handleClose}
PaperComponent={PaperComponent}
aria-labelledby="draggable-dialog-title"
TransitionComponent={Transition}
maxWidth={maxWidth ?? "xs"}
fullWidth
{...dialogprops}
//keepMounted
>
<DialogTitle style={{ cursor: "move" }} id="draggable-dialog-title">
Subscribe
{dialogprops?.title}
</DialogTitle>
<DialogContent>
{React.cloneElement(children, { submitter: false, formRef })}
{children &&
React.cloneElement(children, { submitter: false, formRef })}
{formdom &&
React.cloneElement(formdom, { submitter: false, formRef })}
</DialogContent>
<DialogActions>
<Button
type="reset"
key="rest"
onClick={() => {
formRef?.current?.resetFields();
}}
>
重置
</Button>
<Button
type="submit"
key="submit"
variant="contained"
onClick={() => () => {
formRef?.current?.submit();
}}
>
提交
</Button>
</DialogActions>
{dialogprops?.footer === false ? null : (
<DialogActions>
<Button
type="reset"
key="rest"
onClick={() => {
formRef?.current?.resetFields();
}}
>
重置
</Button>
<LoadingButton
type="submit"
key="submit"
variant="contained"
loading={loading}
loadingPosition="start"
startIcon={<SendOutlined />}
onClick={() => {
console.log(formRef?.current?.submit());
}}
>
提交
</LoadingButton>
</DialogActions>
)}
</Dialog>
</div>
);
......
import { GithubOutlined } from '@ant-design/icons';
import { DefaultFooter } from '@ant-design/pro-components';
import { useIntl } from '@umijs/max';
import React from 'react';
const Footer: React.FC = () => {
const intl = useIntl();
const defaultMessage = intl.formatMessage({
id: 'app.copyright.produced',
defaultMessage: '江苏南高智能装备创新中心有限公司技术部出品',
});
const currentYear = new Date().getFullYear();
return (
<DefaultFooter
style={{
background: 'none',
}}
copyright={`${currentYear} ${defaultMessage}`}
links={[
{
key: '精密测量虚拟仿真实训平台',
title: '精密测量虚拟仿真实训平台',
href: 'https://gitee.com/wyuers/utools_props',
blankTarget: true,
},
{
key: 'wyuer',
title: <GithubOutlined />,
href: 'https://gitee.com/wyuers',
blankTarget: true,
},
{
key: 'Ant Design',
title: 'Ant Design',
href: 'https://ant.design',
blankTarget: true,
},
]}
/>
);
};
export default Footer;
import { doFetch } from '@/utils/doFetch';
import { Avatar, message, Upload } from 'antd';
const beforeUpload = (file) => {
const isJpgOrPng = file.type === 'image/jpeg' || file.type === 'image/png';
if (!isJpgOrPng) {
message.error('You can only upload JPG/PNG file!');
}
const isLt2M = file.size / 1024 / 1024 < 2;
if (!isLt2M) {
message.error('Image must smaller than 2MB!');
}
return isJpgOrPng && isLt2M;
};
const Header = ({ currentUser, run }) => {
const imageUrl = currentUser?.head_url ?? null;
const handleChange = (info) => {
if (info.file.status === 'done') {
doFetch({
url: `/webtool/v1/user/${currentUser?.id}`,
params: { head_url: info?.file?.response?.url ?? null },
method: 'PUT',
}).then((res) => {
if (res?.code === 0) {
run();
}
});
}
};
const uploadButton = (
<Avatar size={45} style={{ flexShrink: 0 }}>
{currentUser?.user_name?.charAt(0)}
</Avatar>
);
const token = localStorage.getItem('TOKENES');
return (
<Upload
name="avatar"
listType="picture-card"
className="avatar-uploader"
showUploadList={false}
action={`${REACT_APP_URL}/webtool/upload`}
headers={{
Authorization: token,
}}
beforeUpload={beforeUpload}
onChange={handleChange}
>
{imageUrl ? <Avatar size={45} style={{ flexShrink: 0 }} src={imageUrl} /> : uploadButton}
</Upload>
);
};
export default Header;
import { Dropdown } from 'antd';
import type { DropDownProps } from 'antd/es/dropdown';
import React from 'react';
import { useEmotionCss } from '@ant-design/use-emotion-css';
import classNames from 'classnames';
export type HeaderDropdownProps = {
overlayClassName?: string;
placement?: 'bottomLeft' | 'bottomRight' | 'topLeft' | 'topCenter' | 'topRight' | 'bottomCenter';
} & Omit<DropDownProps, 'overlay'>;
const HeaderDropdown: React.FC<HeaderDropdownProps> = ({ overlayClassName: cls, ...restProps }) => {
const className = useEmotionCss(({ token }) => {
return {
[`@media screen and (max-width: ${token.screenXS})`]: {
width: '100%',
},
};
});
return (
<Dropdown
overlayClassName={classNames(className, cls)}
getPopupContainer={(target) => target.parentElement || document.body}
{...restProps}
/>
);
};
export default HeaderDropdown;
import React from 'react';
function highlightText(originalText, keyword) {
const regex = new RegExp(`(${keyword})`, 'gi');
return originalText.replace(regex, "<span style='color:#ff4800'>$1</span>");
}
function Highlighter({ children, keyword }) {
const html = keyword ? highlightText(children, keyword) : children;
const handleClick = (e) => {
console.log('====================================');
console.log(e.target.src);
console.log('====================================');
console.log('====================================');
console.log(e);
console.log('====================================');
};
if (typeof children === 'string') {
return (
<div onClick={handleClick} className="limit" dangerouslySetInnerHTML={{ __html: html }} />
);
}
const clonedChildren = React.Children.map(children, (child) => {
if (typeof child === 'string') {
return (
<div onClick={handleClick} className="limit" dangerouslySetInnerHTML={{ __html: html }} />
);
}
return child;
});
return <div>{clonedChildren}</div>;
}
export default Highlighter;
......@@ -4,7 +4,7 @@ import { createFromIconfontCN } from '@ant-design/icons';
import font from './font/iconfont'
const IconFont = createFromIconfontCN({
scriptUrl: font,
scriptUrl: '//at.alicdn.com/t/c/font_4010447_84s57xv2oi7.js',
});
export default IconFont;
import { Button, Stack, Typography } from "@mui/material";
import { useState } from "react";
import DraggableDialog from "../DraggableDialog";
import InitForm from "../InitForm"
function ImportExcel() {
const [dialogprops, setdialogprops] = useState();
return (
<>
<Stack direction={"row"} gap={1}>
<Button
variant="outlined"
onClick={() => {
setdialogprops({
open: true,
title: "导入",
});
}}
>
导入
</Button>
<DraggableDialog
dialogprops={dialogprops}
handleClose={()=>{
setdialogprops(s=>({
...s,
open:false
}))
}}
formdom={
<InitForm
style={{ marginTop: 12, marginBottom: -18 }}
fields={[
{
title: "",
dataIndex: "file",
key: "file",
valueType: "uploadDragger",
colProps: {
span: 24,
},
},
]}
></InitForm>
}
>
<Stack direction={"column"}>
<Stack
direction={"row"}
gap={1}
alignItems={"center"}
justifyContent={"space-between"}
>
<Typography variant={"b"} color={"#999999"}>
*请先下载模板文件
</Typography>
<Button variant="text">模板文件</Button>
</Stack>
</Stack>
</DraggableDialog>
</Stack>
</>
);
}
export default ImportExcel;
......@@ -4,7 +4,6 @@ import BraftEditor from 'braft-editor';
import React, { useState, useMemo, useEffect } from 'react';
import dayjs from 'dayjs';
const prefix = import.meta.env.VITE_APP_URL;
export default function EditorItem({
value,
......@@ -54,7 +53,7 @@ export default function EditorItem({
'POST',
serverURL
? serverURL
: prefix + '/file/upload',
: REACT_APP_URL + '/file/upload',
true,
);
xhr.send(fd);
......
......@@ -36,7 +36,6 @@ import {
} from "@ant-design/icons";
import EditTable from "./EditTable";
import EditorItem from "./EditorItem";
const prefix = import.meta.env.VITE_APP_URL;
const { Image, Form, Upload, Col, Dropdown, Menu, Tabs } = Antd;
......@@ -456,7 +455,7 @@ function NolinkSelect({ item, colProps }) {
return (
<>
<ProFormSelect
fieldProps={item.fieldProps}
fieldProps={{...item.fieldProps,dropdownMatchSelectWidth:200}}
formItemProps={item.formItemProps}
name={item.key ?? item.dataIndex}
colProps={item.colProps ?? colProps}
......@@ -1052,7 +1051,7 @@ function UploadBtn({ item, colProps }) {
<ProFormUploadButton
fieldProps={{
...item?.fieldProps,
action: prefix + "/file/upload",
action: REACT_APP_URL + "/file/upload",
onPreview: (file) => {
let url = "";
if (file.response) {
......@@ -1090,7 +1089,7 @@ function UploadBtn({ item, colProps }) {
function UploadImg({ value, onChange, fieldProps }) {
const [image, setImage] = useState({});
let token = "18e1081d54f57af2fdeac1964cc981e7";
let token = localStorage.getItem("TOKENES");
function beforeUpload(file) {
const isJpgOrPng =
......@@ -1107,7 +1106,7 @@ function UploadImg({ value, onChange, fieldProps }) {
// maxCount 最大数量
const defaultconfig = {
name: "file",
action: prefix + "/file/upload",
action: REACT_APP_URL + "/file/upload",
accept: ".jpg,.png,.jpeg",
listType: "picture-card",
beforeUpload: beforeUpload,
......@@ -1224,7 +1223,7 @@ function UploadDragger({ item, colProps }) {
<ProFormUploadDragger
fieldProps={{
...item?.fieldProps,
action: prefix + "/file/upload",
action: REACT_APP_URL + "/file/upload",
onPreview: (file) => {
let url = "";
if (file.response) {
......
import React, { createElement, memo, useRef } from "react";
import { ProForm, ProFormDependency } from "@ant-design/pro-components";
import { doFetch } from "@/utils/doFetch";
import "./index.less";
import FormItems from "./FormItems";
import { ProForm, ProFormDependency } from "@ant-design/pro-components";
import { Button } from "@mui/material";
import { createElement, memo, useRef } from "react";
import FormItems from "./FormItems";
import "./index.less";
function upperCase(str) {
const newStr = str.slice(0, 1).toUpperCase() + str.slice(1);
......@@ -85,11 +85,12 @@ let FormRender = memo(({ fields = [], colProps, proformRef }) => {
function InitForm({
formRef,
onFinish = (vals) => {
console.log(vals);
onFinish = (vals,extra) => {
console.log(vals,extra);
},
formKey,
params = {},
style = {},
detailpath = "",
defaultFormValue = {},
submitter,
......@@ -104,7 +105,7 @@ function InitForm({
return (
<ProForm
style={{ overflow: "hidden" }}
style={{ ...style, overflow: "hidden" }}
formRef={proformRef}
onFinish={onFinish}
formKey={formKey ?? parseInt(Math.random() * 1000000)}
......
.title {
position: relative;
width: 100%;
margin-bottom: 8px;
margin-bottom: 20px;
padding-left: 16px;
color: #000000;
font-weight: bolder;
......
/* eslint-disable eqeqeq */
import { useModel } from '@umijs/max';
import { Image } from 'antd';
import { useRef,useState } from 'react';
function Limit({ content, style={} }) {
const [preview, setpreview] = useState({
visible: false,
});
const containerRef = useRef();
const handleClick = (e) => {
if (e.target.tagName !== 'IMG') {
return;
}
let srcarr = [],
current = 0;
if (containerRef?.current) {
const imgTags = containerRef.current.querySelectorAll('img');
srcarr = Array.from(imgTags)?.map((el, i) => {
if (el.src === e.target.src) {
current = i + 1;
}
return el.src;
});
}
setpreview((s) => ({
current: current,
urls: srcarr,
visible: true,
}));
};
return (
<div style={style}>
<div
style={{
display: 'none',
}}
>
<Image.PreviewGroup
preview={{
visible: preview?.visible,
current: preview?.current-1,
onVisibleChange: (vis) =>
setpreview((s) => ({
...s,
visible: vis,
})),
}}
>
{preview?.urls?.map?.((it) => (
<Image src={it} key={it} />
))}
</Image.PreviewGroup>
</div>
<div
onClick={handleClick}
ref={containerRef}
dangerouslySetInnerHTML={{ __html: content }}
className="limit"
></div>
</div>
);
}
export default Limit;
import { Button } from "@mui/material";
import { Popconfirm } from "antd";
function PremButton(props) {
const { children, btn, pop, access } = props;
let accesses = access ? ["havePrem"].includes(access) : true;
//配置按钮权限接口
return pop ? (
<Popconfirm
{...pop}
disabled={pop?.disabled || !accesses}
placement="bottomRight"
>
<Button
{...btn}
disabled={btn?.disabled || !accesses}
sx={{ minWidth: 44 }}
>
{children}
</Button>
</Popconfirm>
) : (
<Button
{...btn}
disabled={btn?.disabled || !accesses}
sx={{ minWidth: 44 }}
>
{children}
</Button>
);
}
export default PremButton;
import PropTypes from "prop-types";
// @mui
import {
Box,
Card,
colors,
IconButton,
Stack,
Tooltip,
Typography,
} from "@mui/material";
import { styled } from "@mui/material/styles";
// utils
// components
import IconFont from "@/components/IconFont";
import Label from "@/components/label";
import difftime from "@/utils/difftime";
import CheckIcon from "@mui/icons-material/Check";
import DeleteIcon from "@mui/icons-material/Delete";
import EditIcon from "@mui/icons-material/Edit";
import { history } from "@umijs/max";
import dayjs from "dayjs";
import { useState } from "react";
// ----------------------------------------------------------------------
const StyledProductImg = styled("img")({
top: 0,
width: "92%",
height: "92%",
marginTop: "4%",
objectFit: "cover",
position: "absolute",
});
// ----------------------------------------------------------------------
ShopProductCard.propTypes = {
product: PropTypes.object,
};
export default function ShopProductCard({ product, remove, edit, publish }) {
const { courseName, picUrl, createTime, statusName, type } = product;
const [confirm, setconfirm] = useState(false);
const [shut, setshut] = useState(false);
return (
<Card sx={{ borderRadius: 3 }} className="hovered" id="jikl">
<Box sx={{ pt: "66%", position: "relative" }} className="center">
{statusName && (
<Label
variant="filled"
color={(statusName === "sale" && "error") || "info"}
sx={{
zIndex: 9,
top: 20,
left: 20,
position: "absolute",
textTransform: "uppercase",
}}
>
{statusName}
</Label>
)}
<StyledProductImg
alt={courseName}
src={picUrl ?? DEFAULT_404_IMG}
sx={{ borderRadius: 2 }}
/>
<Box
className="actionhover"
sx={{ borderRadius: 2, overflow: "hidden" }}
>
<Box className="masker"></Box>
<Box className="edit">
<Tooltip title="编辑">
<IconButton
onClick={() => {
edit(product);
}}
>
<EditIcon
style={{ fontSize: 20, color: colors.blue[200] }}
></EditIcon>
</IconButton>
</Tooltip>
<Tooltip title={confirm ? "确认删除" : "删除"}>
{confirm ? (
<IconButton
disabled={confirm === "1"}
onClick={() => {
remove(product);
}}
>
<CheckIcon
style={{
fontSize: 20,
color:
confirm === "1" ? colors.grey[500] : colors.green[500],
}}
></CheckIcon>
</IconButton>
) : (
<IconButton
onClick={() => {
setconfirm("1");
setTimeout(() => {
setconfirm(true);
}, 1000);
setTimeout(() => {
setconfirm(false);
}, 3000);
}}
>
<DeleteIcon
style={{ fontSize: 20, color: colors.red[500] }}
></DeleteIcon>
</IconButton>
)}
</Tooltip>
</Box>
<Stack
direction="row"
alignItems="center"
justifyContent="space-around"
width={"100%"}
className="stackani"
>
<Tooltip title="备课">
<IconButton
onClick={() => {
history.push("/work/dolessons/" + product.id);
}}
>
<IconFont
type="icon-beike"
style={{ fontSize: 20, color: "#ffffff" }}
></IconFont>
</IconButton>
</Tooltip>
<Tooltip title="授权">
<IconButton>
<IconFont
type="icon-shouquanguanli"
style={{ fontSize: 20, color: "#ffffff" }}
></IconFont>
</IconButton>
</Tooltip>
{type == 3 ? null : (
<Tooltip title={type == 1 ? "发布" : type == 2 ? "取消发布" : ""}>
<IconButton
onClick={() => {
publish(product);
}}
>
{type == 1 ? (
<IconFont
type="icon-fabu"
style={{ fontSize: 20, color: "#ffffff" }}
></IconFont>
) : (
<IconFont
type="icon-undo"
style={{ fontSize: 20, color: "#ffffff" }}
></IconFont>
)}
</IconButton>
</Tooltip>
)}
<Tooltip title="复制创建">
<IconButton>
<IconFont
type="icon-fuzhi"
style={{ fontSize: 20, color: "#ffffff" }}
></IconFont>
</IconButton>
</Tooltip>
{type == 2 && (
<Tooltip title={shut ? "确认关闭" : "关闭"}>
{shut ? (
<IconButton
disabled={shut === "1"}
onClick={() => {
publish(product, { type: 3 });
}}
>
<CheckIcon
style={{
fontSize: 20,
color:
shut === "1" ? colors.grey[500] : colors.green[500],
}}
></CheckIcon>
</IconButton>
) : (
<IconButton
onClick={() => {
setshut("1");
setTimeout(() => {
setshut(true);
}, 1000);
setTimeout(() => {
setshut(false);
}, 3000);
}}
>
<IconFont
type="icon-guanbi"
style={{ fontSize: 20, color: "#ffffff" }}
></IconFont>
</IconButton>
)}
</Tooltip>
)}
</Stack>
</Box>
</Box>
<Stack spacing={2} sx={{ p: 2 }}>
<Stack
direction={"row"}
justifyContent={"space-between"}
width={"100%"}
overflow={"hidden"}
alignItems={"center"}
>
<Typography variant="subtitle2" noWrap>
{courseName}
</Typography>
<Box width={60} textAlign={"right"} flexShrink={0}>
<Tooltip title={createTime}>
<Typography
component="span"
variant="body2"
sx={{
color: "text.disabled",
}}
>
{difftime(dayjs(), dayjs(createTime))}
</Typography>
</Tooltip>
</Box>
</Stack>
</Stack>
</Card>
);
}
import PropTypes from "prop-types";
// @mui
import {
Box,
Card,
IconButton,
Link,
Skeleton,
Stack,
Tooltip,
Typography,
} from "@mui/material";
import { styled } from "@mui/material/styles";
// utils
import { fCurrency } from "@/utils/formatNumber";
// components
import IconFont from "@/components/IconFont";
// ----------------------------------------------------------------------
const StyledProductImg = styled("img")({
top: 0,
width: "92%",
height: "92%",
marginTop: "4%",
objectFit: "cover",
position: "absolute",
});
// ----------------------------------------------------------------------
ShopProductLoadingCard.propTypes = {
product: PropTypes.object,
};
export default function ShopProductLoadingCard({ product, loading }) {
const { courseName, picUrl, price, status, priceSale } = product;
return (
<Card sx={{ borderRadius: 3 }}>
<Box sx={{ pt: "66%", position: "relative" }} className="center">
<Skeleton
variant="rectangular"
width={"92%"}
height={"92%"}
sx={{ position: "absolute",top: '8%',borderRadius: 2 }}
animation="wave"
/>
</Box>
<Stack spacing={2} sx={{ p: 2 }}>
<Skeleton animation="wave"/>
</Stack>
</Card>
);
}
/* eslint-disable eqeqeq */
import IconFont from '@/components/IconFont';
import Limit from '@/components/Limit';
import {
CloseOutlined,
EditFilled,
LoadingOutlined,
MessageFilled,
} from '@ant-design/icons';
import { Avatar, Popconfirm, Tooltip } from 'antd';
import dayjs from 'dayjs';
import { useEffect, useRef, useState } from 'react';
import ReplyChildCard from './ReplyChildCard';
function difftime(start, end) {
const diffInMs = start.diff(end);
if (diffInMs < 60 * 60 * 1000) {
// 时间差小于 1 小时,转为分钟
const diffInMinutes = Math.round(diffInMs / (60 * 1000));
return diffInMinutes + '分钟前';
} else {
// 时间差大于等于 1 小时,继续判断是否超过 24 小时
const diffInHours = Math.round(diffInMs / (60 * 60 * 1000));
if (diffInHours >= 24) {
// 时间差超过 24 小时,转为天
const diffInDays = Math.round(diffInHours / 24);
return diffInDays + '天前';
} else {
// 时间差在 1 小时到 24 小时之间,直接输出小时数
return diffInHours + '小时前';
}
}
}
const emojiarr = ['👍', '👎', '😆', '😊', '😢', '😭', '😂', '🎉', '❤️', '🚀', '👀'];
function qc(arr) {
let tempArr = [];
let obj = {};
let resultArr = [];
arr.forEach((v) => {
if (!tempArr.includes(v)) {
tempArr.push(v);
}
});
arr.forEach((v) => {
if (obj[v]) {
obj[v]++;
} else {
obj[v] = 1;
}
});
tempArr.forEach((v) => {
resultArr.push({
name: v,
num: obj[v],
});
});
return resultArr;
}
const ReplyCard = ({
user = {},
reply,
bereply_user = {},
otherusers = [],
setdoreply,
id,
doreply,
currentUser,
drawer,
run,
loading,
info,
formRef,
children,
created_at,
curitem,
other,
}) => {
const [curclickid, setcurclickid] = useState();
const intoRef = useRef();
useEffect(() => {
if (curitem?.msg_id == id) {
intoRef?.current?.scrollIntoView({
behavior: 'smooth',
block: 'start',
inline: 'start',
});
}
}, [curitem?.msg_id]);
return (
<div
style={{
overflow: 'hidden',
display: 'flex',
transition: 'all 0.2s',
borderTop: '1px solid #f0f0f0',
padding: '12px 12px 0 12px',
backgroundColor: doreply?.curid === id ? 'rgba(0, 132, 255, 0.1)' : 'transparent',
}}
ref={intoRef}
>
<div className="info" style={{ flexShrink: 0 }}>
<Tooltip title={user.user_name}>
<Avatar size="large" src={user?.head_url || null}>
{user?.head_url ? null : user?.user_name?.charAt(0)}
</Avatar>
</Tooltip>
</div>
<div style={{ flex: 1 }}>
<div className="spread" style={{ height: 40, marginBottom: 12 }}>
<div className="center">
<span style={{ color: curitem?.msg_id == id ? '#ff4800' : '#333333' }}>
回复&nbsp;<b>{bereply_user?.user_name}</b>
</span>
<div className="center" style={{ gap: 8, paddingLeft: 12 }}>
{qc(info?.map?.((it) => it?.type) ?? [])?.map((it) => {
return (
<div
className="emojis"
key={it?.name}
onClick={() => {
setcurclickid(id);
if (loading && curclickid == id) return;
run({
url: `/webtool/v1/msg/${id}`,
params: { emoji: it?.name },
method: 'PUT',
});
}}
>
<span>{emojiarr[it?.name]}</span>
<span>{it?.num}</span>
</div>
);
})}
</div>
</div>
<div
className="center"
style={{
gap: 6,
display: drawer?.val == 'edit' ? 'none' : 'flex',
position: 'relative',
}}
id={`container${id}`}
>
<div
className="sorts hoveremoji"
style={{ position: 'relative', justifyContent: 'flex-end' }}
>
<div className="center" style={{ position: 'absolute', right: 30, width: 264 }}>
{emojiarr?.map((it, i) => {
return (
<div
className="emoji"
key={i}
onClick={() => {
setcurclickid(id);
if (loading && curclickid == id) return;
run({
url: `/webtool/v1/msg/${id}`,
params: { emoji: i.toString() },
method: 'PUT',
});
}}
>
{it}
</div>
);
})}
</div>
<div className="center" style={{ width: 30, height: 30 }}>
<IconFont type="icon-xiaolian" style={{ fontSize: 16 }} />
</div>
</div>
<div
className="sorts"
onClick={async (e) => {
e.stopPropagation();
await setdoreply({
...user,
curid: id,
type: 'reply',
});
setTimeout(() => {
intoRef?.current?.scrollIntoView({
behavior: 'smooth',
block: 'start',
inline: 'start',
});
}, 300);
}}
>
<MessageFilled style={{ color: 'rgb(24, 144, 255)' }} />
</div>
{currentUser?.id == user?.id ? (
<div
className="sorts"
id="container"
onClick={async (e) => {
e.stopPropagation();
await setdoreply({
...bereply_user,
curid: id,
type: 'edit1',
other:
other
?.split(',')
?.filter((it) => it !== '')
?.map((it) => parseInt(it)) ?? [],
});
formRef?.current?.setFieldsValue({ reply });
setTimeout(() => {
intoRef?.current?.scrollIntoView({
behavior: 'smooth',
block: 'start',
inline: 'start',
});
}, 300);
}}
>
{loading && curclickid == id ? (
<LoadingOutlined />
) : (
<EditFilled style={{ color: 'green' }} />
)}
</div>
) : null}
{currentUser?.id == user?.id ? (
<Popconfirm
title={'确认删除'}
placement="left"
getPopupContainer={() => document.getElementById(`container${id}`)}
onConfirm={(e) => {
e.stopPropagation();
setcurclickid(id);
run({ url: `/webtool/v1/msg/${id}`, params: {}, method: 'DELETE' });
}}
onCancel={(e) => {
e.stopPropagation();
}}
>
<div
className="sorts"
onClick={(e) => {
e.stopPropagation();
}}
>
{loading && curclickid == id ? (
<LoadingOutlined />
) : (
<CloseOutlined style={{ color: '#ff4800' }} />
)}
</div>
</Popconfirm>
) : null}
</div>
</div>
<div
className="limit"
style={{
padding: '0 12px 16px 12px',
border: '1px solid #ddd',
borderRadius: 8,
marginBottom: 12,
}}
>
<Limit content={reply}></Limit>
<div className="spread" style={{ color: '#666', fontSize: 12 }}>
<span>{difftime(dayjs(), dayjs(created_at))}</span>
<span>
<b style={{ display: otherusers && otherusers?.length > 0 ? 'inline' : 'none' }}>
@{' '}
</b>
{otherusers?.map?.((it) => (
<span key={it?.user_name} style={{ padding: '0 4px' }}>
{it?.user_name}
</span>
))}
</span>
</div>
</div>
<div style={{ marginBottom: 20, borderRadius: 8 }}>
{children?.map((it) => {
return (
<ReplyChildCard
key={it?.id}
{...it}
drawer={drawer}
doreply={doreply}
run={run}
loading={loading}
currentUser={currentUser}
formRef={formRef}
curitem={curitem}
setdoreply={(val) => {
setdoreply(val);
}}
/>
);
})}
</div>
</div>
</div>
);
};
export default ReplyCard
/* eslint-disable eqeqeq */
import IconFont from '@/components/IconFont';
import Limit from '@/components/Limit';
import {
CloseOutlined,
EditFilled,
LoadingOutlined,
MessageFilled,
} from '@ant-design/icons';
import { Avatar, Popconfirm, Tooltip } from 'antd';
import dayjs from 'dayjs';
import { useEffect, useRef, useState } from 'react';
function difftime(start, end) {
const diffInMs = start.diff(end);
if (diffInMs < 60 * 60 * 1000) {
// 时间差小于 1 小时,转为分钟
const diffInMinutes = Math.round(diffInMs / (60 * 1000));
return diffInMinutes + '分钟前';
} else {
// 时间差大于等于 1 小时,继续判断是否超过 24 小时
const diffInHours = Math.round(diffInMs / (60 * 60 * 1000));
if (diffInHours >= 24) {
// 时间差超过 24 小时,转为天
const diffInDays = Math.round(diffInHours / 24);
return diffInDays + '天前';
} else {
// 时间差在 1 小时到 24 小时之间,直接输出小时数
return diffInHours + '小时前';
}
}
}
const emojiarr = ['👍', '👎', '😆', '😊', '😢', '😭', '😂', '🎉', '❤️', '🚀', '👀'];
function qc(arr) {
let tempArr = [];
let obj = {};
let resultArr = [];
arr.forEach((v) => {
if (!tempArr.includes(v)) {
tempArr.push(v);
}
});
arr.forEach((v) => {
if (obj[v]) {
obj[v]++;
} else {
obj[v] = 1;
}
});
tempArr.forEach((v) => {
resultArr.push({
name: v,
num: obj[v],
});
});
return resultArr;
}
const ReplyChildCard = ({
user = {},
reply,
bereply_user = {},
otherusers = [],
setdoreply,
id,
msg_id,
doreply,
currentUser,
drawer,
run,
loading,
info,
formRef,
children,
created_at,
curitem,
other,
}) => {
const [curclickid, setcurclickid] = useState();
const intoRef = useRef();
useEffect(() => {
if (curitem?.msg_id == id) {
intoRef?.current?.scrollIntoView({
behavior: 'smooth',
block: 'start',
inline: 'start',
});
}
}, [curitem?.msg_id]);
return (
<div
style={{
overflow: 'hidden',
display: 'flex',
transition: 'all 0.2s',
borderBottom: '1px solid #f0f0f0',
padding: '12px 12px 0 12px',
backgroundColor: 'transparent',
}}
ref={intoRef}
>
<div style={{ flex: 1 }}>
<div className="spread" style={{ height: 40, marginBottom: 0 }}>
<div className="center">
<Tooltip title={user.user_name}>
<Avatar size="small" src={user?.head_url || null}>
{user?.head_url ? null : user?.user_name?.charAt(0)}
</Avatar>
</Tooltip>
<span style={{ color: curitem?.msg_id == id ? '#ff4800' : '#333333', textIndent: 12 }}>
回复&nbsp;<b>{bereply_user?.user_name}</b>
</span>
<div className="center" style={{ gap: 8, paddingLeft: 12 }}>
{qc(info?.map?.((it) => it?.type) ?? [])?.map((it) => {
return (
<div
className="emojis"
key={it?.name}
onClick={() => {
setcurclickid(id);
if (loading && curclickid == id) return;
run({
url: `/webtool/v1/msg/${id}`,
params: { emoji: it?.name },
method: 'PUT',
});
}}
>
<span>{emojiarr[it?.name]}</span>
<span>{it?.num}</span>
</div>
);
})}
</div>
</div>
<div
className="center"
style={{
gap: 6,
display: drawer?.val == 'edit' ? 'none' : 'flex',
position: 'relative',
}}
id={`container${id}`}
>
<div
className="sorts hoveremoji"
style={{ position: 'relative', justifyContent: 'flex-end' }}
>
<div className="center" style={{ position: 'absolute', right: 30, width: 264 }}>
{emojiarr?.map((it, i) => {
return (
<div
className="emoji"
key={i}
onClick={() => {
setcurclickid(id);
if (loading && curclickid == id) return;
run({
url: `/webtool/v1/msg/${id}`,
params: { emoji: i.toString() },
method: 'PUT',
});
}}
>
{it}
</div>
);
})}
</div>
<div className="center" style={{ width: 30, height: 30 }}>
<IconFont type="icon-xiaolian" style={{ fontSize: 16 }} />
</div>
</div>
<div
className="sorts"
onClick={async (e) => {
e.stopPropagation();
await setdoreply({
...user,
curid: msg_id,
curitemid: id,
type: 'reply',
});
setTimeout(() => {
intoRef?.current?.scrollIntoView({
behavior: 'smooth',
block: 'start',
inline: 'start',
});
}, 300);
}}
>
<MessageFilled style={{ color: 'rgb(24, 144, 255)' }} />
</div>
{currentUser?.id == user?.id ? (
<div
className="sorts"
onClick={async (e) => {
e.stopPropagation();
await setdoreply({
...bereply_user,
curid: msg_id, // 父级消息id
curitemid: id, //本身消息id
type: 'edit2',
other:
other
?.split(',')
?.filter((it) => it !== '')
?.map((it) => parseInt(it)) ?? [],
});
formRef?.current?.setFieldsValue({ reply });
setTimeout(() => {
intoRef?.current?.scrollIntoView({
behavior: 'smooth',
block: 'start',
inline: 'start',
});
}, 300);
}}
>
{loading && curclickid == id ? (
<LoadingOutlined />
) : (
<EditFilled style={{ color: 'green' }} />
)}
</div>
) : null}
{currentUser?.id == user?.id ? (
<Popconfirm
title={'确认删除'}
placement="left"
getPopupContainer={() => document.getElementById(`container${id}`)}
onConfirm={(e) => {
e.stopPropagation();
setcurclickid(id);
run({ url: `/webtool/v1/msg/${id}`, params: {}, method: 'DELETE' });
}}
onCancel={(e) => {
e.stopPropagation();
}}
>
<div
className="sorts"
onClick={(e) => {
e.stopPropagation();
}}
>
{loading && curclickid == id ? (
<LoadingOutlined />
) : (
<CloseOutlined style={{ color: '#ff4800' }} />
)}
</div>
</Popconfirm>
) : null}
</div>
</div>
<div
className="limit"
style={{
borderRadius: 8,
marginBottom: 12,
paddingBottom: 10,
}}
>
<Limit content={reply}></Limit>
<div className="spread" style={{ color: '#666', fontSize: 12 }}>
<span>{difftime(dayjs(), dayjs(created_at))}</span>
<span>
<b style={{ display: otherusers && otherusers?.length > 0 ? 'inline' : 'none' }}>
@{' '}
</b>
{otherusers?.map?.((it) => (
<span key={it?.user_name} style={{ padding: '0 4px' }}>
{it?.user_name}
</span>
))}
</span>
</div>
</div>
</div>
</div>
);
};
export default ReplyChildCard
/* eslint-disable eqeqeq */
import { AddReply } from '@/components/DragModal/formdoms';
import ReplyCard from '@/components/Reply/ReplyCard';
import { doFetch, getFetch } from '@/utils/doFetch';
import { LoadingOutlined, SendOutlined } from '@ant-design/icons';
import { useModel, useParams } from '@umijs/max';
import { useRequest } from 'ahooks';
import { Avatar, Drawer, message, Select, Tooltip } from 'antd';
import { useEffect, useRef, useState } from 'react';
function Reply({ drawer, userList, children, extra, full }) {
const { id } = useParams();
const [doreply, setdoreply] = useState(false);
const {
initialState: { currentUser, curitem },
setInitialState,
} = useModel('@@initialState');
const formRef = useRef();
const [submitdata, setsubmitdata] = useState({
project_id: parseInt(id),
item_id: null,
bereply_userid: drawer?.userInfo?.id,
other: [],
});
useEffect(() => {
setsubmitdata((s) => ({
...s,
project_id: parseInt(id),
bereply_userid: drawer?.userInfo?.id,
other: drawer?.open ? doreply?.other ?? [] : [],
}));
}, [drawer?.userInfo?.id, id, doreply?.other, drawer?.open]);
const allmsg = useRequest(
async () => {
if (!drawer.open) return;
let res = await getFetch({ url: '/webtool/v1/allmsg', params: { item_id: drawer?.id ?? 0 } });
return res?.data;
},
{
refreshDeps: [drawer?.id, drawer?.open, curitem],
},
);
const { run, loading } = useRequest(
(params) => {
return doFetch(params);
},
{
manual: true,
debounceWait: 600,
onSuccess: (res) => {
allmsg?.refresh();
if (res?.code === 0) {
formRef.current.setFieldsValue({ reply: '' });
setdoreply(false);
setsubmitdata(s=>({
...s,
other:[]
}))
}
},
},
);
return (
<Drawer
maskStyle={{
backgroundColor: 'transparent',
}}
contentWrapperStyle={full ? {} : { maxWidth: 1280 }}
placement="right"
extra={extra(setdoreply)}
width={full?"100vw":"100%"}
destroyOnClose
{...drawer}
closable={drawer?.closable}
>
<div
onClick={() => {
setdoreply(false);
}}
>
<div style={{ padding: 18 }}>{children}</div>
{allmsg?.data?.map((it) => {
return (
<ReplyCard
key={it?.id}
{...it}
drawer={drawer}
doreply={doreply}
run={run}
loading={loading}
currentUser={currentUser}
curitem={curitem}
formRef={formRef}
setdoreply={(val) => {
setdoreply(val);
}}
/>
);
})}
<div style={{ height: doreply ? 490 : 66 }}></div>
</div>
{drawer?.val === 'reply' && currentUser?.user_name && (
<div
style={{
position: 'fixed',
bottom: 0,
backgroundColor: '#fff',
padding: '12px',
borderTop: '1px solid #ddd',
width:full? drawer?.bp?'calc(100vw - 24px)':"calc(70vw - 24px)" :"auto"
}}
onClick={() => {
if (doreply === false) {
setdoreply(drawer?.userInfo);
}
}}
>
<div
style={{
height: doreply ? 460 : 40,
overflow: 'hidden',
display: 'flex',
transition: 'all 0.2s',
width: '100%',
flexDirection: 'column',
}}
>
<div className="center">
<div className="info">
<Tooltip title={currentUser.user_name}>
<Avatar size="large" src={currentUser?.head_url || null}>
{currentUser?.head_url ? null : currentUser.user_name?.charAt(0)}
</Avatar>
</Tooltip>
</div>
<div className="spread" style={{ flex: 1 }}>
<div
style={{
position: 'absolute',
top: 0,
left: 0,
right: 0,
margin: 'auto',
width: 54,
height: 3,
borderRadius: 6,
backgroundColor: '#ccc',
}}
></div>
<div className="centerl" style={{ flex: 1 }}>
<span>
回复 <b>{doreply?.user_name ?? '-'}</b>
</span>
<b style={{ padding: '0 12px', fontSize: 16 }}>@</b>
<Select
mode="multiple"
allowClear
value={submitdata?.other}
onChange={(val) => {
setsubmitdata((s) => ({
...s,
other: val,
}));
}}
placeholder="请选择"
options={userList
?.filter((it) => it.id !== currentUser?.id && it?.id !== doreply?.id)
?.map((it) => {
return {
label: it?.user_name,
value: it?.id,
};
})}
style={{ width: '120px' }}
maxTagCount={1}
></Select>
</div>
<div
className="sorts"
style={{
width: 66,
height: 32,
backgroundColor: 'rgb(24, 144, 255)',
color: '#fff',
}}
onClick={async (e) => {
e.stopPropagation();
if (loading || !doreply) {
setdoreply(drawer?.userInfo);
return;
}
const reply = formRef.current.getFieldFormatValue('reply');
const div = document.createElement('div');
div.innerHTML = reply;
if (div.innerText === '' || div.innerText.replace(/\s*/g, '') === '') {
message.warning('请至少回复一句话!');
return;
}
let extra = {};
if (doreply?.curid) {
extra = { msg_id: doreply?.curid };
}
// 设置提及人员
const other = submitdata?.other?.toString() ?? '';
if (doreply?.type == 'edit2') {
run({
url: '/webtool/v1/msg/' + doreply?.curitemid,
params: {
...submitdata,
...extra,
other,
item_id: drawer?.id,
reply,
},
method: 'PUT',
});
return;
}
if (doreply?.type == 'edit1') {
run({
url: '/webtool/v1/msg/' + doreply?.curid,
params: {
...submitdata,
other,
item_id: drawer?.id,
reply,
},
method: 'PUT',
});
return;
}
run({
url: '/webtool/v1/msg',
params: {
...submitdata,
...extra,
other,
item_id: drawer?.id,
reply,
bereply_userid: doreply?.id,
},
});
}}
>
<b>回复</b>
&nbsp;
{loading ? (
<LoadingOutlined></LoadingOutlined>
) : (
<SendOutlined style={{ color: '#fff' }} />
)}
</div>
</div>
</div>
<div style={{ padding: "0 4px",flex:1,marginTop:-12 }}>
<AddReply formRef={formRef}></AddReply>
</div>
</div>
</div>
)}
</Drawer>
);
}
export default Reply;
This diff is collapsed.
This diff is collapsed.
import { ProDescriptions } from "@ant-design/pro-components";
import "./index.less";
function SplitDesc({columns = [], dataSource}) {
return columns?.map?.((it, i) => {
if (Array.isArray(it)) {
return <ProDescriptions dataSource={dataSource} columns={it} key={i} />;
} else {
return (
<div className="title" style={{ borderWidth: i == 0 ? 0 : 1 }}>
{it.title}
</div>
);
}
});
}
export default SplitDesc;
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
export { default } from './Logo';
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment