mirror of
https://github.com/tencentmusic/supersonic.git
synced 2025-12-20 06:34:55 +00:00
first commit
This commit is contained in:
11
webapp/packages/supersonic-fe/src/access.ts
Normal file
11
webapp/packages/supersonic-fe/src/access.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { ROUTE_AUTH_CODES } from '../config/routes';
|
||||
|
||||
export default function access({ authCodes }: { authCodes: string[] }) {
|
||||
return Object.keys(ROUTE_AUTH_CODES).reduce((result, key) => {
|
||||
const data = { ...result };
|
||||
const code = ROUTE_AUTH_CODES[key];
|
||||
const codes = authCodes || [];
|
||||
data[code] = codes.includes(code);
|
||||
return data;
|
||||
}, {});
|
||||
}
|
||||
160
webapp/packages/supersonic-fe/src/app.tsx
Normal file
160
webapp/packages/supersonic-fe/src/app.tsx
Normal file
@@ -0,0 +1,160 @@
|
||||
import type { Settings as LayoutSettings } from '@ant-design/pro-layout';
|
||||
import { Spin, Space } from 'antd';
|
||||
import ScaleLoader from 'react-spinners/ScaleLoader';
|
||||
import { history } from 'umi';
|
||||
import type { RunTimeLayoutConfig } from 'umi';
|
||||
import RightContent from '@/components/RightContent';
|
||||
import S2Icon, { ICON } from '@/components/S2Icon';
|
||||
import qs from 'qs';
|
||||
import { queryCurrentUser } from './services/user';
|
||||
import { queryToken } from './services/login';
|
||||
import defaultSettings from '../config/defaultSettings';
|
||||
import settings from '../config/themeSettings';
|
||||
import { deleteUrlQuery } from './utils/utils';
|
||||
import { AUTH_TOKEN_KEY, FROM_URL_KEY } from '@/common/constants';
|
||||
import 'supersonic-chat-sdk/dist/index.css';
|
||||
import { setToken as setChatSdkToken } from 'supersonic-chat-sdk';
|
||||
export { request } from './services/request';
|
||||
import { ROUTE_AUTH_CODES } from '../config/routes';
|
||||
|
||||
const TOKEN_KEY = AUTH_TOKEN_KEY;
|
||||
|
||||
const replaceRoute = '/';
|
||||
|
||||
const getRuningEnv = async () => {
|
||||
try {
|
||||
// const response = await fetch(`supersonic.config.json`);
|
||||
// const config = await response.json();
|
||||
} catch (error) {
|
||||
console.warn('无法获取配置文件: 运行时环境将以semantic启动');
|
||||
}
|
||||
};
|
||||
|
||||
Spin.setDefaultIndicator(
|
||||
<ScaleLoader color={settings['primary-color']} height={25} width={2} radius={2} margin={2} />,
|
||||
);
|
||||
|
||||
export const initialStateConfig = {
|
||||
loading: (
|
||||
<Spin wrapperClassName="initialLoading">
|
||||
<div className="loadingPlaceholder" />
|
||||
</Spin>
|
||||
),
|
||||
};
|
||||
|
||||
const getToken = async () => {
|
||||
let { search } = window.location;
|
||||
if (search.length > 0) {
|
||||
search = search.slice(1);
|
||||
}
|
||||
const data = qs.parse(search);
|
||||
if (data.code) {
|
||||
try {
|
||||
const fromUrl = localStorage.getItem(FROM_URL_KEY);
|
||||
const res = await queryToken(data.code as string);
|
||||
localStorage.setItem(TOKEN_KEY, res.data.authToken);
|
||||
const newUrl = deleteUrlQuery(window.location.href, 'code');
|
||||
window.location.href = fromUrl || newUrl;
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const getAuthCodes = () => {
|
||||
const { RUN_TYPE, APP_TARGET } = process.env;
|
||||
if (RUN_TYPE === 'local') {
|
||||
return location.host.includes('9080')
|
||||
? [ROUTE_AUTH_CODES.CHAT, ROUTE_AUTH_CODES.CHAT_SETTING]
|
||||
: [ROUTE_AUTH_CODES.SEMANTIC];
|
||||
}
|
||||
if (APP_TARGET === 'inner') {
|
||||
return [ROUTE_AUTH_CODES.CHAT_SETTING, ROUTE_AUTH_CODES.SEMANTIC];
|
||||
}
|
||||
return [ROUTE_AUTH_CODES.CHAT, ROUTE_AUTH_CODES.CHAT_SETTING, ROUTE_AUTH_CODES.SEMANTIC];
|
||||
};
|
||||
|
||||
export async function getInitialState(): Promise<{
|
||||
settings?: LayoutSettings;
|
||||
currentUser?: API.CurrentUser;
|
||||
fetchUserInfo?: () => Promise<API.CurrentUser | undefined>;
|
||||
codeList?: string[];
|
||||
authCodes?: string[];
|
||||
}> {
|
||||
await getRuningEnv();
|
||||
const fetchUserInfo = async () => {
|
||||
try {
|
||||
const { code, data } = await queryCurrentUser();
|
||||
if (code === 200) {
|
||||
return { ...data, staffName: data.staffName || data.name };
|
||||
}
|
||||
} catch (error) {}
|
||||
return undefined;
|
||||
};
|
||||
const { query } = history.location as any;
|
||||
const currentToken = query[TOKEN_KEY] || localStorage.getItem(TOKEN_KEY);
|
||||
|
||||
if (window.location.host.includes('tmeoa') && !currentToken) {
|
||||
await getToken();
|
||||
}
|
||||
|
||||
setChatSdkToken(localStorage.getItem(AUTH_TOKEN_KEY) || '');
|
||||
|
||||
const currentUser = await fetchUserInfo();
|
||||
|
||||
if (currentUser) {
|
||||
localStorage.setItem('user', currentUser.staffName);
|
||||
if (currentUser.orgName) {
|
||||
localStorage.setItem('organization', currentUser.orgName);
|
||||
}
|
||||
}
|
||||
|
||||
const authCodes = getAuthCodes();
|
||||
|
||||
return {
|
||||
fetchUserInfo,
|
||||
currentUser,
|
||||
settings: defaultSettings,
|
||||
authCodes,
|
||||
};
|
||||
}
|
||||
|
||||
export const layout: RunTimeLayoutConfig = (params) => {
|
||||
const { initialState } = params as any;
|
||||
return {
|
||||
onMenuHeaderClick: (e) => {
|
||||
e.preventDefault();
|
||||
history.push(replaceRoute);
|
||||
},
|
||||
logo: (
|
||||
<Space>
|
||||
<S2Icon
|
||||
icon={ICON.iconlogobiaoshi}
|
||||
size={30}
|
||||
color="#fff"
|
||||
style={{ display: 'inline-block', marginTop: 8 }}
|
||||
/>
|
||||
<div className="logo">超音数(SuperSonic)</div>
|
||||
</Space>
|
||||
),
|
||||
contentStyle: { ...(initialState?.contentStyle || {}) },
|
||||
rightContentRender: () => <RightContent />,
|
||||
disableContentMargin: true,
|
||||
onPageChange: (location: any) => {
|
||||
const { pathname } = location;
|
||||
const { RUN_TYPE, APP_TARGET } = process.env;
|
||||
if (
|
||||
(RUN_TYPE === 'local' && !window.location.host.includes('9080') && pathname === '/chat') ||
|
||||
(APP_TARGET === 'inner' && pathname === '/chat')
|
||||
) {
|
||||
history.push('/semanticModel');
|
||||
}
|
||||
},
|
||||
menuHeaderRender: undefined,
|
||||
childrenRender: (dom) => {
|
||||
return dom;
|
||||
},
|
||||
openKeys: false,
|
||||
...initialState?.settings,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,6 @@
|
||||
@import '~antd/lib/style/themes/default';
|
||||
|
||||
// main
|
||||
@primary: #225ace;
|
||||
@body-bg: #f0f2f5;
|
||||
@border-radius-base: '4px';
|
||||
1
webapp/packages/supersonic-fe/src/assets/logo.svg
Normal file
1
webapp/packages/supersonic-fe/src/assets/logo.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 44 18" class="design-iconfont" width="128" height="128"><path d="M24.7272727,4.26325641e-14 L33.5127273,17.4545455 L26.5345455,17.4545455 L21.1236364,6.70181818 L24.7272727,4.26325641e-14 Z M17.52,4.26325641e-14 L21.1236364,6.70181818 L15.7127273,17.4545455 L8.73090909,17.4545455 L17.52,4.26325641e-14 Z M41.5890909,12.6945455 L43.9818182,17.4545455 L35.0909091,17.4545455 L32.6981818,12.6945455 L41.5890909,12.6945455 Z M12.68,6.32 L7.08,17.4545455 L0.498181818,17.4545455 L6.09818182,6.32 L12.68,6.32 Z M38.4145455,6.32 L40.9090909,11.2727273 L32.0181818,11.2727273 L29.5272727,6.32 L38.4145455,6.32 Z M15.7890909,0.141818182 L13.3963636,4.89818182 L-3.55271368e-14,4.89818182 L2.39272727,0.141818182 L15.7890909,0.141818182 Z M35.2690909,0.141818182 L37.6654545,4.89818182 L28.7745455,4.89818182 L26.3818182,0.141818182 L35.2690909,0.141818182 Z" fill-rule="evenodd" fill="#ffffff"></path></svg>
|
||||
|
After Width: | Height: | Size: 953 B |
0
webapp/packages/supersonic-fe/src/common/config.ts
Normal file
0
webapp/packages/supersonic-fe/src/common/config.ts
Normal file
28
webapp/packages/supersonic-fe/src/common/constants.ts
Normal file
28
webapp/packages/supersonic-fe/src/common/constants.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
// 登陆 token key
|
||||
export const AUTH_TOKEN_KEY = 'SUPERSONIC_TOKEN';
|
||||
|
||||
// 记录上次访问页面
|
||||
export const FROM_URL_KEY = 'FROM_URL';
|
||||
|
||||
export const PRIMARY_COLOR = '#f87653';
|
||||
export const CHART_BLUE_COLOR = '#446dff';
|
||||
export const CHAT_BLUE = '#1b4aef';
|
||||
export const CHART_SECONDARY_COLOR = 'rgba(153, 153, 153, 0.3)';
|
||||
|
||||
export enum NumericUnit {
|
||||
None = '无',
|
||||
TenThousand = '万',
|
||||
EnTenThousand = 'w',
|
||||
OneHundredMillion = '亿',
|
||||
Thousand = 'k',
|
||||
Million = 'M',
|
||||
Giga = 'G',
|
||||
}
|
||||
|
||||
export const DEFAULT_CONVERSATION_NAME = '新问答对话';
|
||||
|
||||
export const PAGE_TITLE = '问答对话';
|
||||
|
||||
export const WEB_TITLE = '问答对话 - 超音数';
|
||||
|
||||
export const PLACE_HOLDER = '请输入您的问题';
|
||||
@@ -0,0 +1,16 @@
|
||||
import { Space } from 'antd';
|
||||
export interface IProps {
|
||||
title: string;
|
||||
subTitle?: string;
|
||||
}
|
||||
|
||||
const FormItemTitle: React.FC<IProps> = ({ title, subTitle }) => {
|
||||
return (
|
||||
<Space direction="vertical" size={2}>
|
||||
<span>{title}</span>
|
||||
{subTitle && <span style={{ fontSize: '12px', color: '#6a6a6a' }}>{subTitle}</span>}
|
||||
</Space>
|
||||
);
|
||||
};
|
||||
|
||||
export default FormItemTitle;
|
||||
@@ -0,0 +1,39 @@
|
||||
.normalState {
|
||||
position: static;
|
||||
height: 100%;
|
||||
|
||||
.backNormal {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.maxState {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
z-index: 999;
|
||||
|
||||
.innerWrap {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.backNormal {
|
||||
display: block;
|
||||
height: 30px;
|
||||
padding-right: 20px;
|
||||
color: #02a7f0;
|
||||
font-size: 22px;
|
||||
line-height: 30px;
|
||||
text-align: right;
|
||||
|
||||
.fullscreenExitIcon {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export const formLayout: any = {
|
||||
// labelCol: { span: 13 },
|
||||
// wrapperCol: { span: 13 },
|
||||
layout: 'vertical',
|
||||
};
|
||||
@@ -0,0 +1,39 @@
|
||||
.normalState {
|
||||
position: static;
|
||||
height: 100%;
|
||||
|
||||
.backNormal {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.maxState {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
z-index: 999;
|
||||
|
||||
.innerWrap {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.backNormal {
|
||||
display: block;
|
||||
height: 30px;
|
||||
padding-right: 20px;
|
||||
color: #02a7f0;
|
||||
font-size: 22px;
|
||||
line-height: 30px;
|
||||
text-align: right;
|
||||
|
||||
.fullscreenExitIcon {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
import type { ReactNode, FC } from 'react';
|
||||
import { useEffect } from 'react';
|
||||
import { useImperativeHandle, useState } from 'react';
|
||||
import { FullscreenExitOutlined } from '@ant-design/icons';
|
||||
import styles from './index.less';
|
||||
|
||||
export interface IProps {
|
||||
children: ReactNode;
|
||||
maxRef?: any;
|
||||
top?: string;
|
||||
isFullScreen: boolean;
|
||||
triggerBackToNormal: () => void;
|
||||
}
|
||||
|
||||
const FullScreen: FC<IProps> = ({
|
||||
children,
|
||||
maxRef,
|
||||
top = '50px',
|
||||
isFullScreen,
|
||||
triggerBackToNormal,
|
||||
}) => {
|
||||
const [wrapCls, setWrapCls] = useState(styles.normalState);
|
||||
const changeToMax = () => {
|
||||
setWrapCls(styles.maxState);
|
||||
};
|
||||
|
||||
const changeToNormal = () => {
|
||||
setWrapCls(styles.normalState);
|
||||
};
|
||||
|
||||
const handleBackToNormal = () => {
|
||||
if (typeof triggerBackToNormal === 'function') {
|
||||
triggerBackToNormal();
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (isFullScreen) {
|
||||
changeToMax();
|
||||
} else {
|
||||
changeToNormal();
|
||||
}
|
||||
}, [isFullScreen]);
|
||||
|
||||
useImperativeHandle(maxRef, () => ({
|
||||
changeToMax,
|
||||
changeToNormal,
|
||||
}));
|
||||
|
||||
return (
|
||||
<div className={wrapCls} style={wrapCls === styles.maxState ? { paddingTop: top } : {}}>
|
||||
<div
|
||||
className={styles.innerWrap}
|
||||
style={wrapCls === styles.maxState ? { top } : { height: '100%' }}
|
||||
>
|
||||
<div className={styles.backNormal}>
|
||||
<FullscreenExitOutlined
|
||||
className={styles.fullscreenExitIcon}
|
||||
title="退出全屏"
|
||||
onClick={handleBackToNormal}
|
||||
/>
|
||||
</div>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default FullScreen;
|
||||
@@ -0,0 +1,16 @@
|
||||
@import '~antd/es/style/themes/default.less';
|
||||
|
||||
.container > * {
|
||||
background-color: @popover-bg;
|
||||
border-radius: 4px;
|
||||
box-shadow: @shadow-1-down;
|
||||
}
|
||||
|
||||
@media screen and (max-width: @screen-xs) {
|
||||
.container {
|
||||
width: 100% !important;
|
||||
}
|
||||
.container > * {
|
||||
border-radius: 0 !important;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import type { DropDownProps } from 'antd/es/dropdown';
|
||||
import { Dropdown } from 'antd';
|
||||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import styles from './index.less';
|
||||
|
||||
export type HeaderDropdownProps = {
|
||||
overlayClassName?: string;
|
||||
overlay: React.ReactNode | (() => React.ReactNode) | any;
|
||||
placement?: 'bottomLeft' | 'bottomRight' | 'topLeft' | 'topCenter' | 'topRight' | 'bottomCenter';
|
||||
} & Omit<DropDownProps, 'overlay'>;
|
||||
|
||||
const HeaderDropdown: React.FC<HeaderDropdownProps> = ({ overlayClassName: cls, ...restProps }) => (
|
||||
<Dropdown overlayClassName={classNames(styles.container, cls)} {...restProps} />
|
||||
);
|
||||
|
||||
export default HeaderDropdown;
|
||||
@@ -0,0 +1,8 @@
|
||||
import { createFromIconfontCN } from '@ant-design/icons';
|
||||
import defaultSettings from '../../../config/defaultSettings';
|
||||
|
||||
const IconFont = createFromIconfontCN({
|
||||
scriptUrl: defaultSettings.iconfontUrl,
|
||||
});
|
||||
|
||||
export default IconFont;
|
||||
@@ -0,0 +1,62 @@
|
||||
import React from 'react';
|
||||
import { LogoutOutlined } from '@ant-design/icons';
|
||||
import { Menu } from 'antd';
|
||||
import { useModel } from 'umi';
|
||||
import HeaderDropdown from '../HeaderDropdown';
|
||||
import styles from './index.less';
|
||||
import TMEAvatar from '../TMEAvatar';
|
||||
import cx from 'classnames';
|
||||
import { AUTH_TOKEN_KEY } from '@/common/constants';
|
||||
import { history } from 'umi';
|
||||
|
||||
export type GlobalHeaderRightProps = {
|
||||
menu?: boolean;
|
||||
onClickLogin?: () => void;
|
||||
};
|
||||
|
||||
/**
|
||||
* 退出登录
|
||||
* 并返回到首页
|
||||
*/
|
||||
const loginOut = async () => {
|
||||
localStorage.removeItem(AUTH_TOKEN_KEY);
|
||||
history.push('/login');
|
||||
window.location.reload();
|
||||
};
|
||||
|
||||
const { APP_TARGET } = process.env;
|
||||
|
||||
const AvatarDropdown: React.FC<GlobalHeaderRightProps> = () => {
|
||||
const { initialState = {}, setInitialState } = useModel('@@initialState');
|
||||
|
||||
const onMenuClick = (event: any) => {
|
||||
const { key } = event;
|
||||
if (key === 'logout' && initialState) {
|
||||
loginOut().then(() => {
|
||||
setInitialState({ ...initialState, currentUser: undefined });
|
||||
});
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
const { currentUser = {} } = initialState as any;
|
||||
console.log(currentUser, 'currentUser');
|
||||
const menuHeaderDropdown = (
|
||||
<Menu className={styles.menu} selectedKeys={[]} onClick={onMenuClick}>
|
||||
<Menu.Item key="logout">
|
||||
<LogoutOutlined />
|
||||
退出登录
|
||||
</Menu.Item>
|
||||
</Menu>
|
||||
);
|
||||
return (
|
||||
<HeaderDropdown overlay={menuHeaderDropdown} disabled={APP_TARGET === 'inner'}>
|
||||
<span className={`${styles.action} ${styles.account}`}>
|
||||
<TMEAvatar className={styles.avatar} size="small" staffName={currentUser.staffName} />
|
||||
<span className={cx(styles.name, 'anticon')}>{currentUser.staffName}</span>
|
||||
</span>
|
||||
</HeaderDropdown>
|
||||
);
|
||||
};
|
||||
|
||||
export default AvatarDropdown;
|
||||
@@ -0,0 +1,105 @@
|
||||
@import '~antd/es/style/themes/default.less';
|
||||
|
||||
@pro-header-hover-bg: rgba(0, 0, 0, 0.025);
|
||||
|
||||
.menu {
|
||||
:global(.anticon) {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
:global(.ant-dropdown-menu-item) {
|
||||
min-width: 160px;
|
||||
}
|
||||
}
|
||||
|
||||
.right {
|
||||
display: flex;
|
||||
float: right;
|
||||
height: 48px;
|
||||
margin-left: auto;
|
||||
overflow: hidden;
|
||||
|
||||
.action {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 48px;
|
||||
padding: 0 12px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
|
||||
>span {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: @pro-header-hover-bg;
|
||||
}
|
||||
|
||||
&:global(.opened) {
|
||||
background: @pro-header-hover-bg;
|
||||
}
|
||||
}
|
||||
|
||||
.search {
|
||||
padding: 0 12px;
|
||||
|
||||
&:hover {
|
||||
background: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
.account {
|
||||
.avatar {
|
||||
margin-right: 8px;
|
||||
color: @primary-color;
|
||||
vertical-align: top;
|
||||
background: rgba(255, 255, 255, 0.85);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dark {
|
||||
.action {
|
||||
.download {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.menuName {
|
||||
margin-left: 5px;
|
||||
color: #fff;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: #296df3;
|
||||
}
|
||||
|
||||
&:global(.opened) {
|
||||
background: #252a3d;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.actionIcon {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.tooltip {
|
||||
padding-top: 0 !important;
|
||||
font-size: 12px !important;
|
||||
|
||||
:global {
|
||||
.ant-tooltip-arrow {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.ant-tooltip-inner {
|
||||
min-height: 0 !important;
|
||||
padding: 3px 6px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import { Space } from 'antd';
|
||||
import React from 'react';
|
||||
import { useModel } from 'umi';
|
||||
import Avatar from './AvatarDropdown';
|
||||
|
||||
import styles from './index.less';
|
||||
import cx from 'classnames';
|
||||
|
||||
export type SiderTheme = 'light' | 'dark';
|
||||
|
||||
const GlobalHeaderRight: React.FC = () => {
|
||||
const { initialState } = useModel('@@initialState');
|
||||
|
||||
if (!initialState || !initialState.settings) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { navTheme, layout } = initialState.settings;
|
||||
let className = styles.right;
|
||||
|
||||
if ((navTheme === 'dark' && layout === 'top') || layout === 'mix') {
|
||||
className = cx(styles.right, styles.dark);
|
||||
}
|
||||
|
||||
function handleLogin() {}
|
||||
|
||||
return (
|
||||
<Space className={className}>
|
||||
<Avatar onClickLogin={handleLogin} />
|
||||
</Space>
|
||||
);
|
||||
};
|
||||
export default GlobalHeaderRight;
|
||||
340
webapp/packages/supersonic-fe/src/components/S2Icon/iconfont.css
Normal file
340
webapp/packages/supersonic-fe/src/components/S2Icon/iconfont.css
Normal file
@@ -0,0 +1,340 @@
|
||||
@font-face {
|
||||
font-family: "iconfont"; /* Project id 2436113 */
|
||||
src: url('iconfont.woff2?t=1659425018463') format('woff2'),
|
||||
url('iconfont.woff?t=1659425018463') format('woff'),
|
||||
url('iconfont.ttf?t=1659425018463') format('truetype'),
|
||||
url('iconfont.svg?t=1659425018463#iconfont') format('svg');
|
||||
}
|
||||
|
||||
.iconfont {
|
||||
font-family: "iconfont" !important;
|
||||
font-size: 16px;
|
||||
font-style: normal;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
.iconbaobiaokanban:before {
|
||||
content: "\e66b";
|
||||
}
|
||||
|
||||
.iconkanban:before {
|
||||
content: "\e638";
|
||||
}
|
||||
|
||||
.iconyunyingkanban:before {
|
||||
content: "\e608";
|
||||
}
|
||||
|
||||
.iconshujukanban1:before {
|
||||
content: "\eb66";
|
||||
}
|
||||
|
||||
.iconjingqingqidai01:before {
|
||||
content: "\e607";
|
||||
}
|
||||
|
||||
.icontouzi:before {
|
||||
content: "\e67a";
|
||||
}
|
||||
|
||||
.iconriqi:before {
|
||||
content: "\e609";
|
||||
}
|
||||
|
||||
.iconyinleren_:before {
|
||||
content: "\e606";
|
||||
}
|
||||
|
||||
.icondapan:before {
|
||||
content: "\e668";
|
||||
}
|
||||
|
||||
.iconbangdan:before {
|
||||
content: "\e669";
|
||||
}
|
||||
|
||||
.iconshujuwajue:before {
|
||||
content: "\e667";
|
||||
}
|
||||
|
||||
.iconshoucang1:before {
|
||||
content: "\e600";
|
||||
}
|
||||
|
||||
.icontianjiazhibiao:before {
|
||||
content: "\e632";
|
||||
}
|
||||
|
||||
.icontianjiafenzu:before {
|
||||
content: "\e666";
|
||||
}
|
||||
|
||||
.iconyouxiajiaogouxuan:before {
|
||||
content: "\e8b7";
|
||||
}
|
||||
|
||||
.iconxiaoshouzhibiaoshezhi:before {
|
||||
content: "\e665";
|
||||
}
|
||||
|
||||
.iconyingyongbiaoge:before {
|
||||
content: "\e6ae";
|
||||
}
|
||||
|
||||
.iconzhibiao:before {
|
||||
content: "\e66a";
|
||||
}
|
||||
|
||||
.iconsearch:before {
|
||||
content: "\e7c9";
|
||||
}
|
||||
|
||||
.iconfactory-color:before {
|
||||
content: "\e69d";
|
||||
}
|
||||
|
||||
.iconportray-color:before {
|
||||
content: "\e69e";
|
||||
}
|
||||
|
||||
.iconvisualize-color:before {
|
||||
content: "\e69f";
|
||||
}
|
||||
|
||||
.iconamount-color:before {
|
||||
content: "\e68f";
|
||||
}
|
||||
|
||||
.iconapi-color:before {
|
||||
content: "\e690";
|
||||
}
|
||||
|
||||
.iconcontent-color:before {
|
||||
content: "\e691";
|
||||
}
|
||||
|
||||
.iconbox-color:before {
|
||||
content: "\e692";
|
||||
}
|
||||
|
||||
.iconchat-color:before {
|
||||
content: "\e693";
|
||||
}
|
||||
|
||||
.iconclient-color:before {
|
||||
content: "\e694";
|
||||
}
|
||||
|
||||
.icondata-process:before {
|
||||
content: "\e695";
|
||||
}
|
||||
|
||||
.iconbi-color:before {
|
||||
content: "\e696";
|
||||
}
|
||||
|
||||
.iconfiled-color:before {
|
||||
content: "\e697";
|
||||
}
|
||||
|
||||
.iconinvoking-color:before {
|
||||
content: "\e698";
|
||||
}
|
||||
|
||||
.iconissue-color:before {
|
||||
content: "\e699";
|
||||
}
|
||||
|
||||
.iconplatform-color:before {
|
||||
content: "\e69a";
|
||||
}
|
||||
|
||||
.iconfile-color:before {
|
||||
content: "\e69b";
|
||||
}
|
||||
|
||||
.iconname-color:before {
|
||||
content: "\e69c";
|
||||
}
|
||||
|
||||
.icondraft:before {
|
||||
content: "\e605";
|
||||
}
|
||||
|
||||
.iconunknown:before {
|
||||
content: "\e604";
|
||||
}
|
||||
|
||||
.iconnormal:before {
|
||||
content: "\e603";
|
||||
}
|
||||
|
||||
.iconfreezed:before {
|
||||
content: "\e602";
|
||||
}
|
||||
|
||||
.iconlogowenzi:before {
|
||||
content: "\e660";
|
||||
}
|
||||
|
||||
.iconlogobiaoshi:before {
|
||||
content: "\e664";
|
||||
}
|
||||
|
||||
.iconchaoyinshuxitonglogo:before {
|
||||
content: "\e663";
|
||||
}
|
||||
|
||||
.iconzanwuquanxiandianjishenqing_1:before {
|
||||
content: "\e662";
|
||||
}
|
||||
|
||||
.iconqingchuangjianmuluhuokanban:before {
|
||||
content: "\e661";
|
||||
}
|
||||
|
||||
.iconzichan:before {
|
||||
content: "\e65f";
|
||||
}
|
||||
|
||||
.iconhangweifenxi:before {
|
||||
content: "\e65e";
|
||||
}
|
||||
|
||||
.iconshujuzichan:before {
|
||||
content: "\e65d";
|
||||
}
|
||||
|
||||
.iconshujukanban:before {
|
||||
content: "\e659";
|
||||
}
|
||||
|
||||
.iconshujujieru:before {
|
||||
content: "\e65a";
|
||||
}
|
||||
|
||||
.iconshujutansuo:before {
|
||||
content: "\e65b";
|
||||
}
|
||||
|
||||
.iconminjiefenxi:before {
|
||||
content: "\e65c";
|
||||
}
|
||||
|
||||
.iconyanfagongju:before {
|
||||
content: "\e658";
|
||||
}
|
||||
|
||||
.iconshujuanquan:before {
|
||||
content: "\e614";
|
||||
}
|
||||
|
||||
.iconCE:before {
|
||||
content: "\e601";
|
||||
}
|
||||
|
||||
.iconkanbantu-shuaxin:before {
|
||||
content: "\e657";
|
||||
}
|
||||
|
||||
.icondaohang-sousuo:before {
|
||||
content: "\e63e";
|
||||
}
|
||||
|
||||
.icondaohang-bangzhu:before {
|
||||
content: "\e63f";
|
||||
}
|
||||
|
||||
.iconkanbantu-fenxiang:before {
|
||||
content: "\e640";
|
||||
}
|
||||
|
||||
.iconquanju-riqi:before {
|
||||
content: "\e641";
|
||||
}
|
||||
|
||||
.icondaohang-shezhi:before {
|
||||
content: "\e642";
|
||||
}
|
||||
|
||||
.icondaohang-zichangouwuche:before {
|
||||
content: "\e643";
|
||||
}
|
||||
|
||||
.iconquanju-xiazai:before {
|
||||
content: "\e644";
|
||||
}
|
||||
|
||||
.iconkanbantu-quanping:before {
|
||||
content: "\e645";
|
||||
}
|
||||
|
||||
.iconshujuzichan-yewushujuzichan:before {
|
||||
content: "\e646";
|
||||
}
|
||||
|
||||
.iconshujukanban-tianjiakanban:before {
|
||||
content: "\e647";
|
||||
}
|
||||
|
||||
.iconqingkong:before {
|
||||
content: "\e648";
|
||||
}
|
||||
|
||||
.iconshujuzichan-jishushujuzichan:before {
|
||||
content: "\e649";
|
||||
}
|
||||
|
||||
.iconshujuzichan-zichanfaxian:before {
|
||||
content: "\e64a";
|
||||
}
|
||||
|
||||
.icontishi-beizhu1:before {
|
||||
content: "\e64b";
|
||||
}
|
||||
|
||||
.iconshujukanban-tianjiamulu:before {
|
||||
content: "\e64c";
|
||||
}
|
||||
|
||||
.icontubiao-zhuzhuangtu:before {
|
||||
content: "\e64d";
|
||||
}
|
||||
|
||||
.icondaohang-xiaoxitishi:before {
|
||||
content: "\e64e";
|
||||
}
|
||||
|
||||
.icontubiao-bingtu:before {
|
||||
content: "\e64f";
|
||||
}
|
||||
|
||||
.icontishi-beizhu2:before {
|
||||
content: "\e650";
|
||||
}
|
||||
|
||||
.iconshezhi-quanxianshezhi:before {
|
||||
content: "\e651";
|
||||
}
|
||||
|
||||
.iconhangweifenxi-mokuaifenxi:before {
|
||||
content: "\e652";
|
||||
}
|
||||
|
||||
.icontubiao-loudoutu:before {
|
||||
content: "\e653";
|
||||
}
|
||||
|
||||
.icontubiao-zhexiantu:before {
|
||||
content: "\e654";
|
||||
}
|
||||
|
||||
.icontubiao-biaoge:before {
|
||||
content: "\e655";
|
||||
}
|
||||
|
||||
.iconhangweifenxi-baobiaoliebiao:before {
|
||||
content: "\e656";
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,576 @@
|
||||
{
|
||||
"id": "2436113",
|
||||
"name": "SuperSonic 超音数系统官方图标库",
|
||||
"font_family": "iconfont",
|
||||
"css_prefix_text": "icon",
|
||||
"description": "TME数据中台产品",
|
||||
"glyphs": [
|
||||
{
|
||||
"icon_id": "22419559",
|
||||
"name": "报表看板",
|
||||
"font_class": "baobiaokanban",
|
||||
"unicode": "e66b",
|
||||
"unicode_decimal": 58987
|
||||
},
|
||||
{
|
||||
"icon_id": "17815082",
|
||||
"name": "看板",
|
||||
"font_class": "kanban",
|
||||
"unicode": "e638",
|
||||
"unicode_decimal": 58936
|
||||
},
|
||||
{
|
||||
"icon_id": "29598913",
|
||||
"name": "运营看板",
|
||||
"font_class": "yunyingkanban",
|
||||
"unicode": "e608",
|
||||
"unicode_decimal": 58888
|
||||
},
|
||||
{
|
||||
"icon_id": "3868281",
|
||||
"name": "数据看板",
|
||||
"font_class": "shujukanban1",
|
||||
"unicode": "eb66",
|
||||
"unicode_decimal": 60262
|
||||
},
|
||||
{
|
||||
"icon_id": "580614",
|
||||
"name": "敬请期待",
|
||||
"font_class": "jingqingqidai01",
|
||||
"unicode": "e607",
|
||||
"unicode_decimal": 58887
|
||||
},
|
||||
{
|
||||
"icon_id": "845846",
|
||||
"name": "投资",
|
||||
"font_class": "touzi",
|
||||
"unicode": "e67a",
|
||||
"unicode_decimal": 59002
|
||||
},
|
||||
{
|
||||
"icon_id": "7294994",
|
||||
"name": "日期",
|
||||
"font_class": "riqi",
|
||||
"unicode": "e609",
|
||||
"unicode_decimal": 58889
|
||||
},
|
||||
{
|
||||
"icon_id": "2029508",
|
||||
"name": "音乐人_16",
|
||||
"font_class": "yinleren_",
|
||||
"unicode": "e606",
|
||||
"unicode_decimal": 58886
|
||||
},
|
||||
{
|
||||
"icon_id": "9504044",
|
||||
"name": "大盘",
|
||||
"font_class": "dapan",
|
||||
"unicode": "e668",
|
||||
"unicode_decimal": 58984
|
||||
},
|
||||
{
|
||||
"icon_id": "26652200",
|
||||
"name": "榜单",
|
||||
"font_class": "bangdan",
|
||||
"unicode": "e669",
|
||||
"unicode_decimal": 58985
|
||||
},
|
||||
{
|
||||
"icon_id": "4313898",
|
||||
"name": "数据挖掘",
|
||||
"font_class": "shujuwajue",
|
||||
"unicode": "e667",
|
||||
"unicode_decimal": 58983
|
||||
},
|
||||
{
|
||||
"icon_id": "12694976",
|
||||
"name": "KHCFDC_收藏",
|
||||
"font_class": "shoucang1",
|
||||
"unicode": "e600",
|
||||
"unicode_decimal": 58880
|
||||
},
|
||||
{
|
||||
"icon_id": "10690834",
|
||||
"name": "添加指标",
|
||||
"font_class": "tianjiazhibiao",
|
||||
"unicode": "e632",
|
||||
"unicode_decimal": 58930
|
||||
},
|
||||
{
|
||||
"icon_id": "21030367",
|
||||
"name": "添加维度",
|
||||
"font_class": "tianjiafenzu",
|
||||
"unicode": "e666",
|
||||
"unicode_decimal": 58982
|
||||
},
|
||||
{
|
||||
"icon_id": "1727419",
|
||||
"name": "203右下角勾选",
|
||||
"font_class": "youxiajiaogouxuan",
|
||||
"unicode": "e8b7",
|
||||
"unicode_decimal": 59575
|
||||
},
|
||||
{
|
||||
"icon_id": "2462841",
|
||||
"name": "销售指标设置",
|
||||
"font_class": "xiaoshouzhibiaoshezhi",
|
||||
"unicode": "e665",
|
||||
"unicode_decimal": 58981
|
||||
},
|
||||
{
|
||||
"icon_id": "8097138",
|
||||
"name": "应用表格",
|
||||
"font_class": "yingyongbiaoge",
|
||||
"unicode": "e6ae",
|
||||
"unicode_decimal": 59054
|
||||
},
|
||||
{
|
||||
"icon_id": "12331689",
|
||||
"name": "指标",
|
||||
"font_class": "zhibiao",
|
||||
"unicode": "e66a",
|
||||
"unicode_decimal": 58986
|
||||
},
|
||||
{
|
||||
"icon_id": "6242439",
|
||||
"name": "search",
|
||||
"font_class": "search",
|
||||
"unicode": "e7c9",
|
||||
"unicode_decimal": 59337
|
||||
},
|
||||
{
|
||||
"icon_id": "25630410",
|
||||
"name": "factory-color",
|
||||
"font_class": "factory-color",
|
||||
"unicode": "e69d",
|
||||
"unicode_decimal": 59037
|
||||
},
|
||||
{
|
||||
"icon_id": "25630419",
|
||||
"name": "portray-color",
|
||||
"font_class": "portray-color",
|
||||
"unicode": "e69e",
|
||||
"unicode_decimal": 59038
|
||||
},
|
||||
{
|
||||
"icon_id": "25630420",
|
||||
"name": "visualize-color",
|
||||
"font_class": "visualize-color",
|
||||
"unicode": "e69f",
|
||||
"unicode_decimal": 59039
|
||||
},
|
||||
{
|
||||
"icon_id": "25630396",
|
||||
"name": "amount-color",
|
||||
"font_class": "amount-color",
|
||||
"unicode": "e68f",
|
||||
"unicode_decimal": 59023
|
||||
},
|
||||
{
|
||||
"icon_id": "25630397",
|
||||
"name": "api-color",
|
||||
"font_class": "api-color",
|
||||
"unicode": "e690",
|
||||
"unicode_decimal": 59024
|
||||
},
|
||||
{
|
||||
"icon_id": "25630398",
|
||||
"name": "content-color",
|
||||
"font_class": "content-color",
|
||||
"unicode": "e691",
|
||||
"unicode_decimal": 59025
|
||||
},
|
||||
{
|
||||
"icon_id": "25630399",
|
||||
"name": "box-color",
|
||||
"font_class": "box-color",
|
||||
"unicode": "e692",
|
||||
"unicode_decimal": 59026
|
||||
},
|
||||
{
|
||||
"icon_id": "25630400",
|
||||
"name": "chat-color",
|
||||
"font_class": "chat-color",
|
||||
"unicode": "e693",
|
||||
"unicode_decimal": 59027
|
||||
},
|
||||
{
|
||||
"icon_id": "25630401",
|
||||
"name": "client-color",
|
||||
"font_class": "client-color",
|
||||
"unicode": "e694",
|
||||
"unicode_decimal": 59028
|
||||
},
|
||||
{
|
||||
"icon_id": "25630402",
|
||||
"name": "data-process",
|
||||
"font_class": "data-process",
|
||||
"unicode": "e695",
|
||||
"unicode_decimal": 59029
|
||||
},
|
||||
{
|
||||
"icon_id": "25630403",
|
||||
"name": "bi-color",
|
||||
"font_class": "bi-color",
|
||||
"unicode": "e696",
|
||||
"unicode_decimal": 59030
|
||||
},
|
||||
{
|
||||
"icon_id": "25630404",
|
||||
"name": "filed-color",
|
||||
"font_class": "filed-color",
|
||||
"unicode": "e697",
|
||||
"unicode_decimal": 59031
|
||||
},
|
||||
{
|
||||
"icon_id": "25630405",
|
||||
"name": "invoking-color",
|
||||
"font_class": "invoking-color",
|
||||
"unicode": "e698",
|
||||
"unicode_decimal": 59032
|
||||
},
|
||||
{
|
||||
"icon_id": "25630406",
|
||||
"name": "issue-color",
|
||||
"font_class": "issue-color",
|
||||
"unicode": "e699",
|
||||
"unicode_decimal": 59033
|
||||
},
|
||||
{
|
||||
"icon_id": "25630407",
|
||||
"name": "platform-color",
|
||||
"font_class": "platform-color",
|
||||
"unicode": "e69a",
|
||||
"unicode_decimal": 59034
|
||||
},
|
||||
{
|
||||
"icon_id": "25630408",
|
||||
"name": "file-color",
|
||||
"font_class": "file-color",
|
||||
"unicode": "e69b",
|
||||
"unicode_decimal": 59035
|
||||
},
|
||||
{
|
||||
"icon_id": "25630409",
|
||||
"name": "name-color",
|
||||
"font_class": "name-color",
|
||||
"unicode": "e69c",
|
||||
"unicode_decimal": 59036
|
||||
},
|
||||
{
|
||||
"icon_id": "21480366",
|
||||
"name": "icon-task-status-draft",
|
||||
"font_class": "draft",
|
||||
"unicode": "e605",
|
||||
"unicode_decimal": 58885
|
||||
},
|
||||
{
|
||||
"icon_id": "21480363",
|
||||
"name": "icon-task-status-system-freeze",
|
||||
"font_class": "unknown",
|
||||
"unicode": "e604",
|
||||
"unicode_decimal": 58884
|
||||
},
|
||||
{
|
||||
"icon_id": "21480360",
|
||||
"name": "icon-task-status-normal",
|
||||
"font_class": "normal",
|
||||
"unicode": "e603",
|
||||
"unicode_decimal": 58883
|
||||
},
|
||||
{
|
||||
"icon_id": "21480337",
|
||||
"name": "icon-task-status-freezed",
|
||||
"font_class": "freezed",
|
||||
"unicode": "e602",
|
||||
"unicode_decimal": 58882
|
||||
},
|
||||
{
|
||||
"icon_id": "20901515",
|
||||
"name": "logo文字",
|
||||
"font_class": "logowenzi",
|
||||
"unicode": "e660",
|
||||
"unicode_decimal": 58976
|
||||
},
|
||||
{
|
||||
"icon_id": "20901503",
|
||||
"name": "logo标识",
|
||||
"font_class": "logobiaoshi",
|
||||
"unicode": "e664",
|
||||
"unicode_decimal": 58980
|
||||
},
|
||||
{
|
||||
"icon_id": "20897340",
|
||||
"name": "超音数系统logo",
|
||||
"font_class": "chaoyinshuxitonglogo",
|
||||
"unicode": "e663",
|
||||
"unicode_decimal": 58979
|
||||
},
|
||||
{
|
||||
"icon_id": "20852244",
|
||||
"name": "暂无权限点击申请",
|
||||
"font_class": "zanwuquanxiandianjishenqing_1",
|
||||
"unicode": "e662",
|
||||
"unicode_decimal": 58978
|
||||
},
|
||||
{
|
||||
"icon_id": "20851776",
|
||||
"name": "请创建目录或看板",
|
||||
"font_class": "qingchuangjianmuluhuokanban",
|
||||
"unicode": "e661",
|
||||
"unicode_decimal": 58977
|
||||
},
|
||||
{
|
||||
"icon_id": "20830143",
|
||||
"name": "资产",
|
||||
"font_class": "zichan",
|
||||
"unicode": "e65f",
|
||||
"unicode_decimal": 58975
|
||||
},
|
||||
{
|
||||
"icon_id": "20829646",
|
||||
"name": "行为分析",
|
||||
"font_class": "hangweifenxi",
|
||||
"unicode": "e65e",
|
||||
"unicode_decimal": 58974
|
||||
},
|
||||
{
|
||||
"icon_id": "20829640",
|
||||
"name": "数据资产",
|
||||
"font_class": "shujuzichan",
|
||||
"unicode": "e65d",
|
||||
"unicode_decimal": 58973
|
||||
},
|
||||
{
|
||||
"icon_id": "20829629",
|
||||
"name": "数据看板",
|
||||
"font_class": "shujukanban",
|
||||
"unicode": "e659",
|
||||
"unicode_decimal": 58969
|
||||
},
|
||||
{
|
||||
"icon_id": "20829630",
|
||||
"name": "数据接入",
|
||||
"font_class": "shujujieru",
|
||||
"unicode": "e65a",
|
||||
"unicode_decimal": 58970
|
||||
},
|
||||
{
|
||||
"icon_id": "20829631",
|
||||
"name": "数据探索",
|
||||
"font_class": "shujutansuo",
|
||||
"unicode": "e65b",
|
||||
"unicode_decimal": 58971
|
||||
},
|
||||
{
|
||||
"icon_id": "20829633",
|
||||
"name": "敏捷分析",
|
||||
"font_class": "minjiefenxi",
|
||||
"unicode": "e65c",
|
||||
"unicode_decimal": 58972
|
||||
},
|
||||
{
|
||||
"icon_id": "19149997",
|
||||
"name": "研发工具",
|
||||
"font_class": "yanfagongju",
|
||||
"unicode": "e658",
|
||||
"unicode_decimal": 58968
|
||||
},
|
||||
{
|
||||
"icon_id": "3977827",
|
||||
"name": "数据安全",
|
||||
"font_class": "shujuanquan",
|
||||
"unicode": "e614",
|
||||
"unicode_decimal": 58900
|
||||
},
|
||||
{
|
||||
"icon_id": "20782797",
|
||||
"name": "CE",
|
||||
"font_class": "CE",
|
||||
"unicode": "e601",
|
||||
"unicode_decimal": 58881
|
||||
},
|
||||
{
|
||||
"icon_id": "20624066",
|
||||
"name": "看板图-刷新",
|
||||
"font_class": "kanbantu-shuaxin",
|
||||
"unicode": "e657",
|
||||
"unicode_decimal": 58967
|
||||
},
|
||||
{
|
||||
"icon_id": "20623681",
|
||||
"name": "导航-搜索",
|
||||
"font_class": "daohang-sousuo",
|
||||
"unicode": "e63e",
|
||||
"unicode_decimal": 58942
|
||||
},
|
||||
{
|
||||
"icon_id": "20623682",
|
||||
"name": "导航-帮助",
|
||||
"font_class": "daohang-bangzhu",
|
||||
"unicode": "e63f",
|
||||
"unicode_decimal": 58943
|
||||
},
|
||||
{
|
||||
"icon_id": "20623683",
|
||||
"name": "看板图-分享",
|
||||
"font_class": "kanbantu-fenxiang",
|
||||
"unicode": "e640",
|
||||
"unicode_decimal": 58944
|
||||
},
|
||||
{
|
||||
"icon_id": "20623684",
|
||||
"name": "全局-日期",
|
||||
"font_class": "quanju-riqi",
|
||||
"unicode": "e641",
|
||||
"unicode_decimal": 58945
|
||||
},
|
||||
{
|
||||
"icon_id": "20623685",
|
||||
"name": "导航-设置",
|
||||
"font_class": "daohang-shezhi",
|
||||
"unicode": "e642",
|
||||
"unicode_decimal": 58946
|
||||
},
|
||||
{
|
||||
"icon_id": "20623686",
|
||||
"name": "导航-资产购物车",
|
||||
"font_class": "daohang-zichangouwuche",
|
||||
"unicode": "e643",
|
||||
"unicode_decimal": 58947
|
||||
},
|
||||
{
|
||||
"icon_id": "20623687",
|
||||
"name": "全局-下载",
|
||||
"font_class": "quanju-xiazai",
|
||||
"unicode": "e644",
|
||||
"unicode_decimal": 58948
|
||||
},
|
||||
{
|
||||
"icon_id": "20623688",
|
||||
"name": "看板图-全屏",
|
||||
"font_class": "kanbantu-quanping",
|
||||
"unicode": "e645",
|
||||
"unicode_decimal": 58949
|
||||
},
|
||||
{
|
||||
"icon_id": "20623689",
|
||||
"name": "数据资产-业务数据资产",
|
||||
"font_class": "shujuzichan-yewushujuzichan",
|
||||
"unicode": "e646",
|
||||
"unicode_decimal": 58950
|
||||
},
|
||||
{
|
||||
"icon_id": "20623690",
|
||||
"name": "数据看板-添加看板",
|
||||
"font_class": "shujukanban-tianjiakanban",
|
||||
"unicode": "e647",
|
||||
"unicode_decimal": 58951
|
||||
},
|
||||
{
|
||||
"icon_id": "20623691",
|
||||
"name": "清空",
|
||||
"font_class": "qingkong",
|
||||
"unicode": "e648",
|
||||
"unicode_decimal": 58952
|
||||
},
|
||||
{
|
||||
"icon_id": "20623692",
|
||||
"name": "数据资产-技术数据资产",
|
||||
"font_class": "shujuzichan-jishushujuzichan",
|
||||
"unicode": "e649",
|
||||
"unicode_decimal": 58953
|
||||
},
|
||||
{
|
||||
"icon_id": "20623693",
|
||||
"name": "数据资产-资产发现",
|
||||
"font_class": "shujuzichan-zichanfaxian",
|
||||
"unicode": "e64a",
|
||||
"unicode_decimal": 58954
|
||||
},
|
||||
{
|
||||
"icon_id": "20623695",
|
||||
"name": "提示-备注1",
|
||||
"font_class": "tishi-beizhu1",
|
||||
"unicode": "e64b",
|
||||
"unicode_decimal": 58955
|
||||
},
|
||||
{
|
||||
"icon_id": "20623696",
|
||||
"name": "数据看板-添加目录",
|
||||
"font_class": "shujukanban-tianjiamulu",
|
||||
"unicode": "e64c",
|
||||
"unicode_decimal": 58956
|
||||
},
|
||||
{
|
||||
"icon_id": "20623697",
|
||||
"name": "图表-柱状图",
|
||||
"font_class": "tubiao-zhuzhuangtu",
|
||||
"unicode": "e64d",
|
||||
"unicode_decimal": 58957
|
||||
},
|
||||
{
|
||||
"icon_id": "20623698",
|
||||
"name": "导航-消息提示",
|
||||
"font_class": "daohang-xiaoxitishi",
|
||||
"unicode": "e64e",
|
||||
"unicode_decimal": 58958
|
||||
},
|
||||
{
|
||||
"icon_id": "20623699",
|
||||
"name": "图表-饼图",
|
||||
"font_class": "tubiao-bingtu",
|
||||
"unicode": "e64f",
|
||||
"unicode_decimal": 58959
|
||||
},
|
||||
{
|
||||
"icon_id": "20623700",
|
||||
"name": "提示-备注2",
|
||||
"font_class": "tishi-beizhu2",
|
||||
"unicode": "e650",
|
||||
"unicode_decimal": 58960
|
||||
},
|
||||
{
|
||||
"icon_id": "20623701",
|
||||
"name": "设置-权限设置",
|
||||
"font_class": "shezhi-quanxianshezhi",
|
||||
"unicode": "e651",
|
||||
"unicode_decimal": 58961
|
||||
},
|
||||
{
|
||||
"icon_id": "20623702",
|
||||
"name": "行为分析-模块分析",
|
||||
"font_class": "hangweifenxi-mokuaifenxi",
|
||||
"unicode": "e652",
|
||||
"unicode_decimal": 58962
|
||||
},
|
||||
{
|
||||
"icon_id": "20623703",
|
||||
"name": "图表-漏斗图",
|
||||
"font_class": "tubiao-loudoutu",
|
||||
"unicode": "e653",
|
||||
"unicode_decimal": 58963
|
||||
},
|
||||
{
|
||||
"icon_id": "20623704",
|
||||
"name": "图表-折线图",
|
||||
"font_class": "tubiao-zhexiantu",
|
||||
"unicode": "e654",
|
||||
"unicode_decimal": 58964
|
||||
},
|
||||
{
|
||||
"icon_id": "20623705",
|
||||
"name": "图表-表格",
|
||||
"font_class": "tubiao-biaoge",
|
||||
"unicode": "e655",
|
||||
"unicode_decimal": 58965
|
||||
},
|
||||
{
|
||||
"icon_id": "20623706",
|
||||
"name": "行为分析-报表列表",
|
||||
"font_class": "hangweifenxi-baobiaoliebiao",
|
||||
"unicode": "e656",
|
||||
"unicode_decimal": 58966
|
||||
}
|
||||
]
|
||||
}
|
||||
181
webapp/packages/supersonic-fe/src/components/S2Icon/iconfont.svg
Normal file
181
webapp/packages/supersonic-fe/src/components/S2Icon/iconfont.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 97 KiB |
BIN
webapp/packages/supersonic-fe/src/components/S2Icon/iconfont.ttf
Normal file
BIN
webapp/packages/supersonic-fe/src/components/S2Icon/iconfont.ttf
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,3 @@
|
||||
.s2icon {
|
||||
line-height: 1;
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import type { CSSProperties, FC } from 'react';
|
||||
import cx from 'classnames';
|
||||
import iconfont from './iconfont.css';
|
||||
import styles from './index.less';
|
||||
|
||||
export interface S2IconProps {
|
||||
icon: string;
|
||||
color?: string;
|
||||
size?: string | number;
|
||||
style?: CSSProperties;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const S2Icon: FC<S2IconProps> = ({ color, size, icon, style, className }) => {
|
||||
return (
|
||||
<span
|
||||
className={cx(styles.s2icon, iconfont.iconfont, icon, className)}
|
||||
style={{ color, fontSize: size, ...style }}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const ICON = iconfont;
|
||||
|
||||
export const AssetIcon = <S2Icon icon={ICON.iconzichan} />;
|
||||
|
||||
export default S2Icon;
|
||||
@@ -0,0 +1,156 @@
|
||||
import { Avatar, TreeSelect, Tag } from 'antd';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { getDepartmentTree, getUserByDeptid } from './service';
|
||||
import TMEAvatar from '@/components/TMEAvatar';
|
||||
|
||||
type Props = {
|
||||
type: 'selectedPerson' | 'selectedDepartment';
|
||||
value?: any;
|
||||
onChange?: (value: boolean) => void;
|
||||
treeSelectProps?: Record<string, any>;
|
||||
};
|
||||
|
||||
const isDisableCheckbox = (name: string, type: string) => {
|
||||
const isPersonNode = name.includes('(');
|
||||
if (type === 'selectedPerson') {
|
||||
return !isPersonNode;
|
||||
}
|
||||
if (type === 'selectedDepartment') {
|
||||
if (isPersonNode) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
// 转化树结构
|
||||
export function changeTreeData(treeData: any = [], type: string) {
|
||||
return treeData.map((item: any) => {
|
||||
return {
|
||||
title: item.name,
|
||||
value: item.key,
|
||||
key: item.key,
|
||||
isLeaf: !!item.emplid,
|
||||
children: item?.subDepartments ? changeTreeData(item.subDepartments, type) : [],
|
||||
disableCheckbox: isDisableCheckbox(item.name, type),
|
||||
checkable: !isDisableCheckbox(item.name, type),
|
||||
icon: item.name.includes('(') && (
|
||||
<Avatar size={18} shape="square" src={`${item.avatarImg}`} alt="avatar" />
|
||||
),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
const SelectPartner: React.FC<Props> = ({
|
||||
type = 'selectedPerson',
|
||||
value,
|
||||
onChange,
|
||||
treeSelectProps = {},
|
||||
}) => {
|
||||
const [treeData, setTreeData] = useState([]);
|
||||
|
||||
const getDetpartment = async () => {
|
||||
const res = await getDepartmentTree();
|
||||
const data = changeTreeData(res.data, type);
|
||||
setTreeData(data);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
getDetpartment();
|
||||
}, []);
|
||||
|
||||
const updateTreeData = (list: any, key: any, children: any) => {
|
||||
return list.map((node: any) => {
|
||||
if (node.key === key) {
|
||||
let childrenData = node.children;
|
||||
if (node.children && !node.children.find((item: any) => item?.key === children[0]?.key)) {
|
||||
childrenData = [...children, ...node.children];
|
||||
}
|
||||
return { ...node, children: childrenData };
|
||||
}
|
||||
if (node.children.length !== 0) {
|
||||
return { ...node, children: updateTreeData(node.children, key, children) };
|
||||
}
|
||||
return node;
|
||||
});
|
||||
};
|
||||
const onLoadData = (target: any) => {
|
||||
const { key } = target;
|
||||
const loadData = async () => {
|
||||
const childData = await getUserByDeptid(key);
|
||||
if (childData.data.length === 0) {
|
||||
return;
|
||||
}
|
||||
setTimeout(() => {
|
||||
setTreeData((origin) => updateTreeData(origin, key, changeTreeData(childData.data, type)));
|
||||
}, 300);
|
||||
};
|
||||
return new Promise<void>((resolve) => {
|
||||
loadData().then(() => {
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const handleChange = (newValue: any) => {
|
||||
onChange?.(newValue);
|
||||
};
|
||||
const tagRender = (props: any) => {
|
||||
const { label } = props;
|
||||
const onPreventMouseDown = (event: React.MouseEvent<HTMLSpanElement>) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
};
|
||||
const enEname = label.split('(')[0];
|
||||
return (
|
||||
<Tag
|
||||
onMouseDown={onPreventMouseDown}
|
||||
closable={true}
|
||||
onClose={() => {
|
||||
const { value: propsValue } = props;
|
||||
const newValue = value.filter((code: string) => {
|
||||
return code !== propsValue;
|
||||
});
|
||||
onChange?.(newValue);
|
||||
}}
|
||||
style={{ marginRight: 3, marginBottom: 3 }}
|
||||
>
|
||||
{type === 'selectedPerson' && <TMEAvatar size="small" staffName={enEname} />}
|
||||
<span
|
||||
style={{
|
||||
position: 'relative',
|
||||
top: '2px',
|
||||
left: '5px',
|
||||
}}
|
||||
>
|
||||
{label}
|
||||
</span>
|
||||
</Tag>
|
||||
);
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<TreeSelect
|
||||
showSearch
|
||||
style={{ width: '100%' }}
|
||||
value={value}
|
||||
loadData={onLoadData}
|
||||
dropdownStyle={{ maxHeight: 800, overflow: 'auto' }}
|
||||
allowClear
|
||||
multiple
|
||||
onChange={handleChange}
|
||||
treeCheckable={true}
|
||||
treeIcon={true}
|
||||
treeData={treeData}
|
||||
tagRender={tagRender}
|
||||
treeNodeFilterProp={'title'}
|
||||
listHeight={500}
|
||||
showCheckedStrategy={TreeSelect.SHOW_PARENT}
|
||||
{...treeSelectProps}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default SelectPartner;
|
||||
@@ -0,0 +1,13 @@
|
||||
import { request } from 'umi';
|
||||
|
||||
export async function getDepartmentTree() {
|
||||
return request<any>('/api/tpp/getDetpartmentTree', {
|
||||
method: 'GET',
|
||||
});
|
||||
}
|
||||
|
||||
export async function getUserByDeptid(id: any) {
|
||||
return request<any>(`/api/tpp/getUserByDeptid/${id}`, {
|
||||
method: 'GET',
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
.userAvatar {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.userText {
|
||||
margin-left: 10px;
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
import { useState } from 'react';
|
||||
import type { FC } from 'react';
|
||||
import { Select, message } from 'antd';
|
||||
import type { UserItem } from './service';
|
||||
import { getAllUser } from './service';
|
||||
|
||||
import styles from './index.less';
|
||||
import { useFetchDataEffect } from '@/utils/curd';
|
||||
import TMEAvatar from '../TMEAvatar';
|
||||
|
||||
interface Props {
|
||||
value?: string[];
|
||||
placeholder?: string;
|
||||
isMultiple?: boolean;
|
||||
onChange?: (owners: string | string[]) => void;
|
||||
}
|
||||
|
||||
const SelectTMEPerson: FC<Props> = ({ placeholder, value, isMultiple = true, onChange }) => {
|
||||
const [userList, setUserList] = useState<UserItem[]>([]);
|
||||
|
||||
useFetchDataEffect(
|
||||
{
|
||||
fetcher: async () => {
|
||||
const res = await getAllUser();
|
||||
if (res.code !== 200) {
|
||||
message.error(res.msg);
|
||||
throw new Error(res.msg);
|
||||
}
|
||||
return res.data || [];
|
||||
},
|
||||
updater: (list) => {
|
||||
const users = list.map((item: UserItem) => {
|
||||
const { enName, chName, name } = item;
|
||||
return {
|
||||
...item,
|
||||
enName: enName || name,
|
||||
chName: chName || name,
|
||||
};
|
||||
});
|
||||
setUserList(users);
|
||||
},
|
||||
cleanup: () => {
|
||||
setUserList([]);
|
||||
},
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
return (
|
||||
<Select
|
||||
value={value}
|
||||
placeholder={placeholder ?? '请选择用户名'}
|
||||
mode={isMultiple ? 'multiple' : undefined}
|
||||
allowClear
|
||||
showSearch
|
||||
onChange={onChange}
|
||||
>
|
||||
{userList.map((item) => {
|
||||
return (
|
||||
<Select.Option key={item.enName} value={item.enName}>
|
||||
<TMEAvatar size="small" staffName={item.enName} />
|
||||
<span className={styles.userText}>{item.displayName}</span>
|
||||
</Select.Option>
|
||||
);
|
||||
})}
|
||||
</Select>
|
||||
);
|
||||
};
|
||||
|
||||
export default SelectTMEPerson;
|
||||
@@ -0,0 +1,19 @@
|
||||
import request from 'umi-request';
|
||||
|
||||
export type UserItem = {
|
||||
enName?: string;
|
||||
displayName: string;
|
||||
chName?: string;
|
||||
name?: string;
|
||||
email: string;
|
||||
};
|
||||
export type GetAllUserRes = Result<UserItem[]>;
|
||||
|
||||
// 获取所有用户
|
||||
export async function getAllUser(): Promise<GetAllUserRes> {
|
||||
const { APP_TARGET } = process.env;
|
||||
if (APP_TARGET === 'inner') {
|
||||
return request.get('/api/oa/user/all');
|
||||
}
|
||||
return request.get(`${process.env.AUTH_API_BASE_URL}user/getUserList`);
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
@borderColor: #eee;
|
||||
|
||||
.sqlEditor {
|
||||
min-width: 0;
|
||||
height: 100%;
|
||||
border: solid 1px @borderColor;
|
||||
|
||||
:global {
|
||||
.ace_editor {
|
||||
font-family: 'Menlo', 'Monaco', 'Ubuntu Mono', 'Consolas', 'source-code-pro' !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.fullScreenBtnBox {
|
||||
display: flex;
|
||||
justify-content: end;
|
||||
}
|
||||
282
webapp/packages/supersonic-fe/src/components/SqlEditor/index.tsx
Normal file
282
webapp/packages/supersonic-fe/src/components/SqlEditor/index.tsx
Normal file
@@ -0,0 +1,282 @@
|
||||
/* eslint-disable */
|
||||
|
||||
import React, { useRef, useEffect, useCallback, useState, useMemo } from 'react';
|
||||
import AceEditor, { IAceOptions } from 'react-ace';
|
||||
import languageTools from 'ace-builds/src-min-noconflict/ext-language_tools';
|
||||
import 'ace-builds/src-min-noconflict/ext-searchbox';
|
||||
import 'ace-builds/src-min-noconflict/theme-sqlserver';
|
||||
import 'ace-builds/src-min-noconflict/theme-monokai';
|
||||
import 'ace-builds/src-min-noconflict/mode-sql';
|
||||
import ReactAce, { IAceEditorProps } from 'react-ace/lib/ace';
|
||||
import { Typography } from 'antd';
|
||||
import { debounce } from 'lodash';
|
||||
import FullScreen from '../FullScreen';
|
||||
import styles from './index.less';
|
||||
|
||||
type TMode = 'sql' | 'mysql' | 'sqlserver';
|
||||
|
||||
enum EHintMeta {
|
||||
table = 'table',
|
||||
variable = 'variable',
|
||||
column = 'column',
|
||||
}
|
||||
const DEFAULT_FONT_SIZE = '14px';
|
||||
// const THEME_DEFAULT = 'sqlserver';
|
||||
const MODE_DEFAULT = 'sql';
|
||||
// const HEIGHT_DEFAULT = '300px';
|
||||
const HEIGHT_DEFAULT = '100%';
|
||||
const EDITOR_OPTIONS: IAceOptions = {
|
||||
behavioursEnabled: true,
|
||||
enableSnippets: false,
|
||||
enableBasicAutocompletion: true,
|
||||
enableLiveAutocompletion: true,
|
||||
autoScrollEditorIntoView: true,
|
||||
wrap: true,
|
||||
useWorker: false,
|
||||
};
|
||||
export interface ISqlEditorProps {
|
||||
hints?: { [name: string]: string[] };
|
||||
value?: string;
|
||||
height?: string;
|
||||
/**
|
||||
* 需引入对应的包 'ace-builds/src-min-noconflict/mode-${mode}'
|
||||
*/
|
||||
mode?: TMode;
|
||||
/**
|
||||
* 需引入对应的包 'ace-builds/src-min-noconflict/theme-${theme}'
|
||||
*/
|
||||
// theme?: TTheme;
|
||||
isRightTheme?: boolean;
|
||||
editorConfig?: IAceEditorProps;
|
||||
sizeChanged?: number;
|
||||
fullScreenBtnVisible?: boolean;
|
||||
onSqlChange?: (sql: string) => void;
|
||||
onChange?: (sql: string) => void;
|
||||
onSelect?: (sql: string) => void;
|
||||
onCmdEnter?: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Editor Component
|
||||
* @param props ISqlEditorProps
|
||||
*/
|
||||
function SqlEditor(props: ISqlEditorProps) {
|
||||
const refEditor = useRef<ReactAce>();
|
||||
const {
|
||||
hints = {},
|
||||
value,
|
||||
height = HEIGHT_DEFAULT,
|
||||
mode = MODE_DEFAULT,
|
||||
isRightTheme = false,
|
||||
sizeChanged,
|
||||
editorConfig,
|
||||
fullScreenBtnVisible = true,
|
||||
onSqlChange,
|
||||
onChange,
|
||||
onSelect,
|
||||
onCmdEnter,
|
||||
} = props;
|
||||
const resize = useCallback(
|
||||
debounce(() => {
|
||||
refEditor.current?.editor.resize();
|
||||
}, 300),
|
||||
[],
|
||||
);
|
||||
|
||||
const change = useCallback((sql: string) => {
|
||||
onSqlChange?.(sql);
|
||||
onChange?.(sql);
|
||||
}, []);
|
||||
|
||||
const selectionChange = useCallback(
|
||||
debounce((selection: any) => {
|
||||
const rawSelectedQueryText: any = refEditor.current?.editor.session.doc.getTextRange(
|
||||
selection.getRange(),
|
||||
);
|
||||
const selectedQueryText = rawSelectedQueryText?.length > 1 ? rawSelectedQueryText : null;
|
||||
onSelect?.(selectedQueryText);
|
||||
}, 300),
|
||||
[],
|
||||
);
|
||||
|
||||
const commands = useMemo(
|
||||
() => [
|
||||
{
|
||||
name: 'execute',
|
||||
bindKey: { win: 'Ctrl-Enter', mac: 'Command-Enter' },
|
||||
exec: onCmdEnter,
|
||||
},
|
||||
],
|
||||
[],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
resize();
|
||||
}, [sizeChanged, height]);
|
||||
|
||||
useEffect(() => {
|
||||
setHintsPopover(hints);
|
||||
}, [hints]);
|
||||
|
||||
const [isSqlIdeFullScreen, setIsSqlIdeFullScreen] = useState<boolean>(false);
|
||||
|
||||
const handleNormalScreenSqlIde = () => {
|
||||
setIsSqlIdeFullScreen(false);
|
||||
// setSqlEditorHeight(getDefaultSqlEditorHeight(screenSize));
|
||||
};
|
||||
return (
|
||||
<div className={styles.sqlEditor} style={{ height }}>
|
||||
<FullScreen
|
||||
isFullScreen={isSqlIdeFullScreen}
|
||||
top={`${0}px`}
|
||||
triggerBackToNormal={handleNormalScreenSqlIde}
|
||||
>
|
||||
<AceEditor
|
||||
ref={refEditor}
|
||||
name="aceEditor"
|
||||
width="100%"
|
||||
height="100%"
|
||||
fontSize={DEFAULT_FONT_SIZE}
|
||||
mode={mode}
|
||||
theme={isRightTheme ? 'sqlserver' : 'monokai'}
|
||||
value={value}
|
||||
showPrintMargin={false}
|
||||
highlightActiveLine={true}
|
||||
setOptions={EDITOR_OPTIONS}
|
||||
commands={commands as any}
|
||||
onChange={change}
|
||||
onSelectionChange={selectionChange}
|
||||
// autoScrollEditorIntoView={true}
|
||||
{...editorConfig}
|
||||
/>
|
||||
</FullScreen>
|
||||
{fullScreenBtnVisible && (
|
||||
<span
|
||||
className={styles.fullScreenBtnBox}
|
||||
onClick={() => {
|
||||
setIsSqlIdeFullScreen(true);
|
||||
}}
|
||||
>
|
||||
<Typography.Link>全屏查看</Typography.Link>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface ICompleters {
|
||||
value: string;
|
||||
name?: string;
|
||||
caption?: string;
|
||||
meta?: string;
|
||||
type?: string;
|
||||
score?: number;
|
||||
}
|
||||
|
||||
function setHintsPopover(hints: ISqlEditorProps['hints']) {
|
||||
const {
|
||||
textCompleter,
|
||||
keyWordCompleter,
|
||||
// snippetCompleter,
|
||||
setCompleters,
|
||||
} = languageTools;
|
||||
const customHintsCompleter = {
|
||||
identifierRegexps: [/[a-zA-Z_0-9.\-\u00A2-\uFFFF]/],
|
||||
getCompletions: (editor, session, pos, prefix, callback) => {
|
||||
const { tableKeywords, tableColumnKeywords, variableKeywords, columns } =
|
||||
formatCompleterFromHints(hints);
|
||||
if (prefix[prefix.length - 1] === '.') {
|
||||
const tableName = prefix.substring(0, prefix.length - 1);
|
||||
const AliasTableColumnKeywords = genAliasTableColumnKeywords(editor, tableName, hints);
|
||||
const hintList = tableKeywords.concat(
|
||||
variableKeywords,
|
||||
AliasTableColumnKeywords,
|
||||
tableColumnKeywords[tableName] || [],
|
||||
);
|
||||
return callback(null, hintList);
|
||||
}
|
||||
callback(null, tableKeywords.concat(variableKeywords, columns));
|
||||
},
|
||||
};
|
||||
const completers = [
|
||||
textCompleter,
|
||||
keyWordCompleter,
|
||||
// snippetCompleter,
|
||||
customHintsCompleter,
|
||||
];
|
||||
setCompleters(completers);
|
||||
}
|
||||
|
||||
function formatCompleterFromHints(hints: ISqlEditorProps['hints']) {
|
||||
const variableKeywords: ICompleters[] = [];
|
||||
const tableKeywords: ICompleters[] = [];
|
||||
const tableColumnKeywords: { [tableName: string]: ICompleters[] } = {};
|
||||
const columns: ICompleters[] = [];
|
||||
let score = 1000;
|
||||
Object.keys(hints).forEach((key) => {
|
||||
const meta: EHintMeta = isVariable(key) as any;
|
||||
if (!meta) {
|
||||
const { columnWithTableName, column } = genTableColumnKeywords(hints[key], key);
|
||||
tableColumnKeywords[key] = columnWithTableName;
|
||||
columns.push(...column);
|
||||
tableKeywords.push({
|
||||
name: key,
|
||||
value: key,
|
||||
score: score--,
|
||||
meta: isTable(),
|
||||
});
|
||||
} else {
|
||||
variableKeywords.push({ score: score--, value: key, meta });
|
||||
}
|
||||
});
|
||||
|
||||
return { tableKeywords, tableColumnKeywords, variableKeywords, columns };
|
||||
}
|
||||
|
||||
function genTableColumnKeywords(table: string[], tableName: string) {
|
||||
let score = 100;
|
||||
const columnWithTableName: ICompleters[] = [];
|
||||
const column: ICompleters[] = [];
|
||||
table.forEach((columnVal) => {
|
||||
const basis = { score: score--, meta: isColumn() };
|
||||
columnWithTableName.push({
|
||||
caption: `${tableName}.${columnVal}`,
|
||||
name: `${tableName}.${columnVal}`,
|
||||
value: `${tableName}.${columnVal}`,
|
||||
...basis,
|
||||
});
|
||||
column.push({ value: columnVal, name: columnVal, ...basis });
|
||||
});
|
||||
return { columnWithTableName, column };
|
||||
}
|
||||
|
||||
function genAliasTableColumnKeywords(
|
||||
editor,
|
||||
aliasTableName: string,
|
||||
hints: ISqlEditorProps['hints'],
|
||||
) {
|
||||
const content = editor.getSession().getValue();
|
||||
const tableName = Object.keys(hints).find((tableName) => {
|
||||
const reg = new RegExp(`.+${tableName}\\s*(as|AS)?(?=\\s+${aliasTableName}\\s*)`, 'im');
|
||||
return reg.test(content);
|
||||
});
|
||||
if (!tableName) {
|
||||
return [];
|
||||
}
|
||||
const { columnWithTableName } = genTableColumnKeywords(hints[tableName], aliasTableName);
|
||||
return columnWithTableName;
|
||||
}
|
||||
|
||||
function isVariable(key: string) {
|
||||
return key.startsWith('$') && key.endsWith('$') && EHintMeta.variable;
|
||||
}
|
||||
|
||||
function isTable(key?: string) {
|
||||
return EHintMeta.table;
|
||||
}
|
||||
|
||||
function isColumn(key?: string) {
|
||||
return EHintMeta.column;
|
||||
}
|
||||
|
||||
export default SqlEditor;
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 1.6 KiB |
@@ -0,0 +1,15 @@
|
||||
import type { FC } from 'react';
|
||||
import { Avatar } from 'antd';
|
||||
import type { AvatarProps } from 'antd';
|
||||
import avatarIcon from './assets/avatar.gif';
|
||||
|
||||
interface Props extends AvatarProps {
|
||||
staffName?: string;
|
||||
avatarImg?: string;
|
||||
}
|
||||
|
||||
const TMEAvatar: FC<Props> = ({ avatarImg, ...restProps }) => (
|
||||
<Avatar src={`${avatarImg}`} alt="avatar" icon={<img src={avatarIcon} />} {...restProps} />
|
||||
);
|
||||
|
||||
export default TMEAvatar;
|
||||
42
webapp/packages/supersonic-fe/src/enum/index.ts
Normal file
42
webapp/packages/supersonic-fe/src/enum/index.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
export * from './models/base';
|
||||
type ObjToArrayParams = Record<string, string>;
|
||||
|
||||
const keyTypeTran = {
|
||||
string: String,
|
||||
number: Number,
|
||||
};
|
||||
/**
|
||||
* obj转成value,label的数组
|
||||
* @param _obj
|
||||
*/
|
||||
export const objToArray = (_obj: ObjToArrayParams, keyType: string = 'string') => {
|
||||
return Object.keys(_obj).map((key) => {
|
||||
return {
|
||||
value: keyTypeTran[keyType](key),
|
||||
label: _obj[key],
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
type EnumToArrayItem = {
|
||||
value: number;
|
||||
label: string;
|
||||
showSelect?: boolean;
|
||||
};
|
||||
export type EnumToArrayParams = Record<string, EnumToArrayItem>;
|
||||
|
||||
export const enumToArray = (_obj: EnumToArrayParams) => {
|
||||
return Object.keys(_obj).map((key) => {
|
||||
return _obj[key];
|
||||
});
|
||||
};
|
||||
|
||||
// 枚举类转出的key value列表转key value对象
|
||||
export const enumArrayTrans = (_array: EnumToArrayItem[]) => {
|
||||
const returnObj = {};
|
||||
_array.map((item) => {
|
||||
returnObj[item.value] = item.label;
|
||||
return item;
|
||||
});
|
||||
return returnObj;
|
||||
};
|
||||
48
webapp/packages/supersonic-fe/src/enum/models/base.ts
Normal file
48
webapp/packages/supersonic-fe/src/enum/models/base.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
export const EnumTransDbType = {
|
||||
mysql: 'mysql',
|
||||
tdw: 'tdw',
|
||||
clickhouse: 'clickhouse',
|
||||
kafka: 'kafka',
|
||||
binlog: 'binlog',
|
||||
hbase: 'hbase',
|
||||
kugou_datahub: 'kugou_datahub',
|
||||
aiting_datahub: 'aiting_datahub',
|
||||
http: 'http',
|
||||
};
|
||||
|
||||
export const EnumTransModelType = {
|
||||
edit: '编辑',
|
||||
add: '新增',
|
||||
};
|
||||
|
||||
export const EnumDescSensitivity = {
|
||||
low: {
|
||||
value: 1,
|
||||
label: '低',
|
||||
},
|
||||
middle: {
|
||||
value: 2,
|
||||
label: '中',
|
||||
},
|
||||
height: {
|
||||
value: 3,
|
||||
label: '高',
|
||||
},
|
||||
};
|
||||
|
||||
export const EnumDbTypeOwnKeys = {
|
||||
mysql: ['ip', 'port', 'dbName', 'username', 'password'],
|
||||
clickhouse: ['ip', 'port', 'dbName', 'username', 'password'],
|
||||
tdw: ['dbName', 'username', 'password'],
|
||||
kafka: ['bootstrap', 'dbName', 'username', 'password'],
|
||||
binlog: ['ip', 'port', 'dbName', 'username', 'password'],
|
||||
hbase: ['config'],
|
||||
kugou_datahub: ['config'],
|
||||
aiting_datahub: ['config'],
|
||||
http: ['url'],
|
||||
};
|
||||
|
||||
export enum EnumDashboardType {
|
||||
DIR = 0, // 目录
|
||||
DASHBOARD = 1, // 看板
|
||||
}
|
||||
217
webapp/packages/supersonic-fe/src/global.less
Normal file
217
webapp/packages/supersonic-fe/src/global.less
Normal file
@@ -0,0 +1,217 @@
|
||||
@import '~antd/es/style/themes/default.less';
|
||||
|
||||
:root:root {
|
||||
--primary-color: #f87653;
|
||||
--blue: #296df3;
|
||||
--deep-blue: #446dff;
|
||||
--chat-blue: #1b4aef;
|
||||
--body-background: #f7fafa;
|
||||
--deep-background: #f0f0f0;
|
||||
--light-background: #f5f5f5;
|
||||
--component-background: #fff;
|
||||
--header-color: #edf2f2;
|
||||
--text-color: #181a1a;
|
||||
--text-color-secondary: #3d4242;
|
||||
--text-color-third: #626a6a;
|
||||
--text-color-fourth: #889191;
|
||||
--text-color-fifth: #afb6b6;
|
||||
--text-color-six: #a3a4a6;
|
||||
--text-color-fifth-4: hsla(180, 5%, 70%, 0.4);
|
||||
--tooltip-max-width: 350px;
|
||||
--success-color: #52c41a;
|
||||
--processing-color: #ff2442;
|
||||
--error-color: #ff4d4f;
|
||||
--highlight-color: #ff4d4f;
|
||||
}
|
||||
|
||||
html,
|
||||
body,
|
||||
#root {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.colorWeak {
|
||||
filter: invert(80%);
|
||||
}
|
||||
|
||||
.ant-layout {
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
canvas {
|
||||
display: block;
|
||||
}
|
||||
|
||||
body {
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
ul,
|
||||
ol {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
@media (max-width: @screen-xs) {
|
||||
.ant-table {
|
||||
width: 100%;
|
||||
overflow-x: auto;
|
||||
|
||||
&-thead>tr,
|
||||
&-tbody>tr {
|
||||
|
||||
>th,
|
||||
>td {
|
||||
white-space: pre;
|
||||
|
||||
>span {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 兼容IE11
|
||||
@media screen and(-ms-high-contrast: active),
|
||||
(-ms-high-contrast: none) {
|
||||
body .ant-design-pro>.ant-layout {
|
||||
min-height: 100vh;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-card-body {
|
||||
padding: 24px !important;
|
||||
}
|
||||
|
||||
.ant-pro-page-container-children-content {
|
||||
margin: 12px 12px 0 !important;
|
||||
}
|
||||
|
||||
.ant-page-header {
|
||||
padding-bottom: 10px !important;
|
||||
}
|
||||
|
||||
.ant-spin-spinning {
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
justify-content: center !important;
|
||||
}
|
||||
|
||||
.ant-table-selection-extra {
|
||||
.ant-dropdown-trigger {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
.initialLoading {
|
||||
.ant-spin-spinning {
|
||||
max-height: none !important;
|
||||
}
|
||||
|
||||
.loadingPlaceholder {
|
||||
height: 100vh;
|
||||
}
|
||||
}
|
||||
|
||||
.ellipsis {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.ant-menu-dark.ant-menu-horizontal>.ant-menu-item,
|
||||
.ant-menu-dark.ant-menu-horizontal>.ant-menu-submenu {
|
||||
font-weight: bold;
|
||||
font-size: 14px;
|
||||
|
||||
&>span>a,
|
||||
&>a {
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-pro-top-nav-header-logo h1 {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.ant-layout-header {
|
||||
background: linear-gradient(to right, #153d8f, #0a276d);
|
||||
background-color: rgba(0, 0, 0, 0.2);
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
|
||||
|
||||
.customizeHeader {
|
||||
background-color: rgba(0, 0, 0, 0.2);
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.ant-pro-top-nav-header-main-left {
|
||||
min-width: 100px !important;
|
||||
}
|
||||
|
||||
.ant-pro-top-nav-header-logo {
|
||||
min-width: 100px !important;
|
||||
}
|
||||
|
||||
|
||||
.link {
|
||||
color: #296df3;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.closeTab {
|
||||
position: relative;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
// opacity: 0;
|
||||
}
|
||||
|
||||
.closeTab::before,
|
||||
.closeTab::after {
|
||||
position: absolute;
|
||||
top: -2px;
|
||||
left: 0;
|
||||
width: 1px;
|
||||
height: 10px;
|
||||
background-color: rgb(50, 50, 50);
|
||||
content: ' ';
|
||||
}
|
||||
|
||||
.closeTab::before {
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
|
||||
.closeTab::after {
|
||||
transform: rotate(-45deg);
|
||||
}
|
||||
|
||||
.dot {
|
||||
float: right;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background: #bfbfbf;
|
||||
border-radius: 100%;
|
||||
}
|
||||
|
||||
.bdWrapper {
|
||||
margin: -24px;
|
||||
.ant-layout-sider {
|
||||
top: 48px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.logo {
|
||||
position: relative;
|
||||
padding-bottom: 5px;
|
||||
color: #fff;
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
padding-right: 50px;
|
||||
}
|
||||
|
||||
.ant-notification-topRight {
|
||||
right: 240px !important;
|
||||
}
|
||||
85
webapp/packages/supersonic-fe/src/global.tsx
Normal file
85
webapp/packages/supersonic-fe/src/global.tsx
Normal file
@@ -0,0 +1,85 @@
|
||||
import { Button, message, notification } from 'antd';
|
||||
|
||||
import React from 'react';
|
||||
import { useIntl } from 'umi';
|
||||
import defaultSettings from '../config/defaultSettings';
|
||||
|
||||
const { pwa } = defaultSettings;
|
||||
const isHttps = document.location.protocol === 'https:';
|
||||
|
||||
// if pwa is true
|
||||
if (pwa) {
|
||||
// Notify user if offline now
|
||||
window.addEventListener('sw.offline', () => {
|
||||
message.warning(useIntl().formatMessage({ id: 'app.pwa.offline' }));
|
||||
});
|
||||
|
||||
// Pop up a prompt on the page asking the user if they want to use the latest version
|
||||
window.addEventListener('sw.updated', (event: Event) => {
|
||||
const e = event as CustomEvent;
|
||||
const reloadSW = async () => {
|
||||
// Check if there is sw whose state is waiting in ServiceWorkerRegistration
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerRegistration
|
||||
const worker = e.detail && e.detail.waiting;
|
||||
if (!worker) {
|
||||
return true;
|
||||
}
|
||||
// Send skip-waiting event to waiting SW with MessageChannel
|
||||
await new Promise((resolve, reject) => {
|
||||
const channel = new MessageChannel();
|
||||
channel.port1.onmessage = (msgEvent) => {
|
||||
if (msgEvent.data.error) {
|
||||
reject(msgEvent.data.error);
|
||||
} else {
|
||||
resolve(msgEvent.data);
|
||||
}
|
||||
};
|
||||
worker.postMessage({ type: 'skip-waiting' }, [channel.port2]);
|
||||
});
|
||||
// Refresh current page to use the updated HTML and other assets after SW has skiped waiting
|
||||
window.location.reload(true);
|
||||
return true;
|
||||
};
|
||||
const key = `open${Date.now()}`;
|
||||
const btn = (
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={() => {
|
||||
notification.close(key);
|
||||
reloadSW();
|
||||
}}
|
||||
>
|
||||
{useIntl().formatMessage({ id: 'app.pwa.serviceworker.updated.ok' })}
|
||||
</Button>
|
||||
);
|
||||
notification.open({
|
||||
message: useIntl().formatMessage({ id: 'app.pwa.serviceworker.updated' }),
|
||||
description: useIntl().formatMessage({ id: 'app.pwa.serviceworker.updated.hint' }),
|
||||
btn,
|
||||
key,
|
||||
onClose: async () => null,
|
||||
});
|
||||
});
|
||||
} else if ('serviceWorker' in navigator && isHttps) {
|
||||
// unregister service worker
|
||||
const { serviceWorker } = navigator;
|
||||
if (serviceWorker.getRegistrations) {
|
||||
serviceWorker.getRegistrations().then((sws) => {
|
||||
sws.forEach((sw) => {
|
||||
sw.unregister();
|
||||
});
|
||||
});
|
||||
}
|
||||
serviceWorker.getRegistration().then((sw) => {
|
||||
if (sw) sw.unregister();
|
||||
});
|
||||
|
||||
// remove all caches
|
||||
if (window.caches && window.caches.keys) {
|
||||
caches.keys().then((keys) => {
|
||||
keys.forEach((key) => {
|
||||
caches.delete(key);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
12
webapp/packages/supersonic-fe/src/hooks/useMounted.ts
Normal file
12
webapp/packages/supersonic-fe/src/hooks/useMounted.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { useEffect, useRef } from 'react';
|
||||
|
||||
export const useMounted = () => {
|
||||
const mountedRef = useRef(false);
|
||||
useEffect(() => {
|
||||
mountedRef.current = true;
|
||||
return () => {
|
||||
mountedRef.current = false;
|
||||
};
|
||||
}, []);
|
||||
return () => mountedRef.current;
|
||||
};
|
||||
24
webapp/packages/supersonic-fe/src/locales/zh-CN.ts
Normal file
24
webapp/packages/supersonic-fe/src/locales/zh-CN.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import component from './zh-CN/component';
|
||||
import globalHeader from './zh-CN/globalHeader';
|
||||
import menu from './zh-CN/menu';
|
||||
import pwa from './zh-CN/pwa';
|
||||
import settingDrawer from './zh-CN/settingDrawer';
|
||||
import settings from './zh-CN/settings';
|
||||
import pages from './zh-CN/pages';
|
||||
|
||||
export default {
|
||||
'navBar.lang': '语言',
|
||||
'layout.user.link.help': '帮助',
|
||||
'layout.user.link.privacy': '隐私',
|
||||
'layout.user.link.terms': '条款',
|
||||
'app.preview.down.block': '下载此页面到本地项目',
|
||||
'app.welcome.link.fetch-blocks': '获取全部区块',
|
||||
'app.welcome.link.block-list': '基于 block 开发,快速构建标准页面',
|
||||
...pages,
|
||||
...globalHeader,
|
||||
...menu,
|
||||
...settingDrawer,
|
||||
...settings,
|
||||
...pwa,
|
||||
...component,
|
||||
};
|
||||
@@ -0,0 +1,5 @@
|
||||
export default {
|
||||
'component.tagSelect.expand': '展开',
|
||||
'component.tagSelect.collapse': '收起',
|
||||
'component.tagSelect.all': '全部',
|
||||
};
|
||||
@@ -0,0 +1,17 @@
|
||||
export default {
|
||||
'component.globalHeader.search': '站内搜索',
|
||||
'component.globalHeader.search.example1': '搜索提示一',
|
||||
'component.globalHeader.search.example2': '搜索提示二',
|
||||
'component.globalHeader.search.example3': '搜索提示三',
|
||||
'component.globalHeader.help': '使用文档',
|
||||
'component.globalHeader.notification': '通知',
|
||||
'component.globalHeader.notification.empty': '你已查看所有通知',
|
||||
'component.globalHeader.message': '消息',
|
||||
'component.globalHeader.message.empty': '您已读完所有消息',
|
||||
'component.globalHeader.event': '待办',
|
||||
'component.globalHeader.event.empty': '你已完成所有待办',
|
||||
'component.noticeIcon.clear': '清空',
|
||||
'component.noticeIcon.cleared': '清空了',
|
||||
'component.noticeIcon.empty': '暂无数据',
|
||||
'component.noticeIcon.view-more': '查看更多',
|
||||
};
|
||||
14
webapp/packages/supersonic-fe/src/locales/zh-CN/menu.ts
Normal file
14
webapp/packages/supersonic-fe/src/locales/zh-CN/menu.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
export default {
|
||||
'menu.welcome': '欢迎',
|
||||
'menu.result': '结果页',
|
||||
'menu.result.success': '成功页',
|
||||
'menu.result.fail': '失败页',
|
||||
'menu.exception': '异常页',
|
||||
'menu.exception.not-permission': '403',
|
||||
'menu.exception.not-find': '404',
|
||||
'menu.exception.server-error': '500',
|
||||
'menu.semanticModel': '语义建模',
|
||||
'menu.chatSetting': '问答设置',
|
||||
'menu.login': '登录',
|
||||
'menu.chat': '问答对话',
|
||||
};
|
||||
65
webapp/packages/supersonic-fe/src/locales/zh-CN/pages.ts
Normal file
65
webapp/packages/supersonic-fe/src/locales/zh-CN/pages.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
export default {
|
||||
'pages.layouts.userLayout.title': 'Ant Design 是西湖区最具影响力的 Web 设计规范',
|
||||
'pages.login.accountLogin.tab': '账户密码登录',
|
||||
'pages.login.accountLogin.errorMessage': '错误的用户名和密码(admin/ant.design)',
|
||||
'pages.login.username.placeholder': '用户名: admin or user',
|
||||
'pages.login.username.required': '用户名是必填项!',
|
||||
'pages.login.password.placeholder': '密码: ant.design',
|
||||
'pages.login.password.required': '密码是必填项!',
|
||||
'pages.login.phoneLogin.tab': '手机号登录',
|
||||
'pages.login.phoneLogin.errorMessage': '验证码错误',
|
||||
'pages.login.phoneNumber.placeholder': '请输入手机号!',
|
||||
'pages.login.phoneNumber.required': '手机号是必填项!',
|
||||
'pages.login.phoneNumber.invalid': '不合法的手机号!',
|
||||
'pages.login.captcha.placeholder': '请输入验证码!',
|
||||
'pages.login.captcha.required': '验证码是必填项!',
|
||||
'pages.login.phoneLogin.getVerificationCode': '获取验证码',
|
||||
'pages.getCaptchaSecondText': '秒后重新获取',
|
||||
'pages.login.rememberMe': '自动登录',
|
||||
'pages.login.forgotPassword': '忘记密码 ?',
|
||||
'pages.login.submit': '登录',
|
||||
'pages.login.loginWith': '其他登录方式 :',
|
||||
'pages.login.registerAccount': '注册账户',
|
||||
'pages.welcome.advancedComponent': '高级表格',
|
||||
'pages.welcome.link': '欢迎使用',
|
||||
'pages.welcome.advancedLayout': '高级布局',
|
||||
'pages.welcome.alertMessage': '更快更强的重型组件,已经发布。',
|
||||
'pages.admin.subPage.title': ' 这个页面只有 admin 权限才能查看',
|
||||
'pages.admin.subPage.alertMessage': 'umi ui 现已发布,欢迎使用 npm run ui 启动体验。',
|
||||
'pages.searchTable.createForm.newRule': '新建规则',
|
||||
'pages.searchTable.updateForm.ruleConfig': '规则配置',
|
||||
'pages.searchTable.updateForm.basicConfig': '基本信息',
|
||||
'pages.searchTable.updateForm.ruleName.nameLabel': '规则名称',
|
||||
'pages.searchTable.updateForm.ruleName.nameRules': '请输入规则名称!',
|
||||
'pages.searchTable.updateForm.ruleDesc.descLabel': '规则描述',
|
||||
'pages.searchTable.updateForm.ruleDesc.descPlaceholder': '请输入至少五个字符',
|
||||
'pages.searchTable.updateForm.ruleDesc.descRules': '请输入至少五个字符的规则描述!',
|
||||
'pages.searchTable.updateForm.ruleProps.title': '配置规则属性',
|
||||
'pages.searchTable.updateForm.object': '监控对象',
|
||||
'pages.searchTable.updateForm.ruleProps.templateLabel': '规则模板',
|
||||
'pages.searchTable.updateForm.ruleProps.typeLabel': '规则类型',
|
||||
'pages.searchTable.updateForm.schedulingPeriod.title': '设定调度周期',
|
||||
'pages.searchTable.updateForm.schedulingPeriod.timeLabel': '开始时间',
|
||||
'pages.searchTable.updateForm.schedulingPeriod.timeRules': '请选择开始时间!',
|
||||
'pages.searchTable.titleDesc': '描述',
|
||||
'pages.searchTable.ruleName': '规则名称为必填项',
|
||||
'pages.searchTable.titleCallNo': '服务调用次数',
|
||||
'pages.searchTable.titleStatus': '状态',
|
||||
'pages.searchTable.nameStatus.default': '关闭',
|
||||
'pages.searchTable.nameStatus.running': '运行中',
|
||||
'pages.searchTable.nameStatus.online': '已上线',
|
||||
'pages.searchTable.nameStatus.abnormal': '异常',
|
||||
'pages.searchTable.titleUpdatedAt': '上次调度时间',
|
||||
'pages.searchTable.exception': '请输入异常原因!',
|
||||
'pages.searchTable.titleOption': '操作',
|
||||
'pages.searchTable.config': '配置',
|
||||
'pages.searchTable.subscribeAlert': '订阅警报',
|
||||
'pages.searchTable.title': '查询表格',
|
||||
'pages.searchTable.new': '新建',
|
||||
'pages.searchTable.chosen': '已选择',
|
||||
'pages.searchTable.item': '项',
|
||||
'pages.searchTable.totalServiceCalls': '服务调用次数总计',
|
||||
'pages.searchTable.tenThousand': '万',
|
||||
'pages.searchTable.batchDeletion': '批量删除',
|
||||
'pages.searchTable.batchApproval': '批量审批',
|
||||
};
|
||||
6
webapp/packages/supersonic-fe/src/locales/zh-CN/pwa.ts
Normal file
6
webapp/packages/supersonic-fe/src/locales/zh-CN/pwa.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export default {
|
||||
'app.pwa.offline': '当前处于离线状态',
|
||||
'app.pwa.serviceworker.updated': '有新内容',
|
||||
'app.pwa.serviceworker.updated.hint': '请点击“刷新”按钮或者手动刷新页面',
|
||||
'app.pwa.serviceworker.updated.ok': '刷新',
|
||||
};
|
||||
@@ -0,0 +1,31 @@
|
||||
export default {
|
||||
'app.setting.pagestyle': '整体风格设置',
|
||||
'app.setting.pagestyle.dark': '暗色菜单风格',
|
||||
'app.setting.pagestyle.light': '亮色菜单风格',
|
||||
'app.setting.content-width': '内容区域宽度',
|
||||
'app.setting.content-width.fixed': '定宽',
|
||||
'app.setting.content-width.fluid': '流式',
|
||||
'app.setting.themecolor': '主题色',
|
||||
'app.setting.themecolor.dust': '薄暮',
|
||||
'app.setting.themecolor.volcano': '火山',
|
||||
'app.setting.themecolor.sunset': '日暮',
|
||||
'app.setting.themecolor.cyan': '明青',
|
||||
'app.setting.themecolor.green': '极光绿',
|
||||
'app.setting.themecolor.daybreak': '拂晓蓝(默认)',
|
||||
'app.setting.themecolor.geekblue': '极客蓝',
|
||||
'app.setting.themecolor.purple': '酱紫',
|
||||
'app.setting.navigationmode': '导航模式',
|
||||
'app.setting.sidemenu': '侧边菜单布局',
|
||||
'app.setting.topmenu': '顶部菜单布局',
|
||||
'app.setting.fixedheader': '固定 Header',
|
||||
'app.setting.fixedsidebar': '固定侧边菜单',
|
||||
'app.setting.fixedsidebar.hint': '侧边菜单布局时可配置',
|
||||
'app.setting.hideheader': '下滑时隐藏 Header',
|
||||
'app.setting.hideheader.hint': '固定 Header 时可配置',
|
||||
'app.setting.othersettings': '其他设置',
|
||||
'app.setting.weakmode': '色弱模式',
|
||||
'app.setting.copy': '拷贝设置',
|
||||
'app.setting.copyinfo': '拷贝成功,请到 src/defaultSettings.js 中替换默认配置',
|
||||
'app.setting.production.hint':
|
||||
'配置栏只在开发环境用于预览,生产环境不会展现,请拷贝后手动修改配置文件',
|
||||
};
|
||||
55
webapp/packages/supersonic-fe/src/locales/zh-CN/settings.ts
Normal file
55
webapp/packages/supersonic-fe/src/locales/zh-CN/settings.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
export default {
|
||||
'app.settings.menuMap.basic': '基本设置',
|
||||
'app.settings.menuMap.security': '安全设置',
|
||||
'app.settings.menuMap.binding': '账号绑定',
|
||||
'app.settings.menuMap.notification': '新消息通知',
|
||||
'app.settings.basic.avatar': '头像',
|
||||
'app.settings.basic.change-avatar': '更换头像',
|
||||
'app.settings.basic.email': '邮箱',
|
||||
'app.settings.basic.email-message': '请输入您的邮箱!',
|
||||
'app.settings.basic.nickname': '昵称',
|
||||
'app.settings.basic.nickname-message': '请输入您的昵称!',
|
||||
'app.settings.basic.profile': '个人简介',
|
||||
'app.settings.basic.profile-message': '请输入个人简介!',
|
||||
'app.settings.basic.profile-placeholder': '个人简介',
|
||||
'app.settings.basic.country': '国家/地区',
|
||||
'app.settings.basic.country-message': '请输入您的国家或地区!',
|
||||
'app.settings.basic.geographic': '所在省市',
|
||||
'app.settings.basic.geographic-message': '请输入您的所在省市!',
|
||||
'app.settings.basic.address': '街道地址',
|
||||
'app.settings.basic.address-message': '请输入您的街道地址!',
|
||||
'app.settings.basic.phone': '联系电话',
|
||||
'app.settings.basic.phone-message': '请输入您的联系电话!',
|
||||
'app.settings.basic.update': '更新基本信息',
|
||||
'app.settings.security.strong': '强',
|
||||
'app.settings.security.medium': '中',
|
||||
'app.settings.security.weak': '弱',
|
||||
'app.settings.security.password': '账户密码',
|
||||
'app.settings.security.password-description': '当前密码强度',
|
||||
'app.settings.security.phone': '密保手机',
|
||||
'app.settings.security.phone-description': '已绑定手机',
|
||||
'app.settings.security.question': '密保问题',
|
||||
'app.settings.security.question-description': '未设置密保问题,密保问题可有效保护账户安全',
|
||||
'app.settings.security.email': '备用邮箱',
|
||||
'app.settings.security.email-description': '已绑定邮箱',
|
||||
'app.settings.security.mfa': 'MFA 设备',
|
||||
'app.settings.security.mfa-description': '未绑定 MFA 设备,绑定后,可以进行二次确认',
|
||||
'app.settings.security.modify': '修改',
|
||||
'app.settings.security.set': '设置',
|
||||
'app.settings.security.bind': '绑定',
|
||||
'app.settings.binding.taobao': '绑定淘宝',
|
||||
'app.settings.binding.taobao-description': '当前未绑定淘宝账号',
|
||||
'app.settings.binding.alipay': '绑定支付宝',
|
||||
'app.settings.binding.alipay-description': '当前未绑定支付宝账号',
|
||||
'app.settings.binding.dingding': '绑定钉钉',
|
||||
'app.settings.binding.dingding-description': '当前未绑定钉钉账号',
|
||||
'app.settings.binding.bind': '绑定',
|
||||
'app.settings.notification.password': '账户密码',
|
||||
'app.settings.notification.password-description': '其他用户的消息将以站内信的形式通知',
|
||||
'app.settings.notification.messages': '系统消息',
|
||||
'app.settings.notification.messages-description': '系统消息将以站内信的形式通知',
|
||||
'app.settings.notification.todo': '待办任务',
|
||||
'app.settings.notification.todo-description': '待办任务将以站内信的形式通知',
|
||||
'app.settings.open': '开',
|
||||
'app.settings.close': '关',
|
||||
};
|
||||
22
webapp/packages/supersonic-fe/src/manifest.json
Normal file
22
webapp/packages/supersonic-fe/src/manifest.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"name": "Ant Design Pro",
|
||||
"short_name": "Ant Design Pro",
|
||||
"display": "standalone",
|
||||
"start_url": "./?utm_source=homescreen",
|
||||
"theme_color": "#002140",
|
||||
"background_color": "#001529",
|
||||
"icons": [
|
||||
{
|
||||
"src": "icons/icon-192x192.png",
|
||||
"sizes": "192x192"
|
||||
},
|
||||
{
|
||||
"src": "icons/icon-128x128.png",
|
||||
"sizes": "128x128"
|
||||
},
|
||||
{
|
||||
"src": "icons/icon-512x512.png",
|
||||
"sizes": "512x512"
|
||||
}
|
||||
]
|
||||
}
|
||||
18
webapp/packages/supersonic-fe/src/pages/401.tsx
Normal file
18
webapp/packages/supersonic-fe/src/pages/401.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import { Button, Result } from 'antd';
|
||||
import React from 'react';
|
||||
import { history } from 'umi';
|
||||
|
||||
const NoAuthPage: React.FC = () => (
|
||||
<Result
|
||||
status="403"
|
||||
title="当前页面无权限"
|
||||
subTitle={1 ? '请联系项目管理员 jerryjzhang 开通权限' : '请申请加入自己业务的项目'}
|
||||
extra={
|
||||
<Button type="primary" onClick={() => history.push('/homepage')}>
|
||||
回到首页
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
||||
export default NoAuthPage;
|
||||
18
webapp/packages/supersonic-fe/src/pages/404.tsx
Normal file
18
webapp/packages/supersonic-fe/src/pages/404.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import { Button, Result } from 'antd';
|
||||
import React from 'react';
|
||||
import { history } from 'umi';
|
||||
|
||||
const NoFoundPage: React.FC = () => (
|
||||
<Result
|
||||
status="404"
|
||||
title="404"
|
||||
subTitle="Sorry, the page you visited does not exist."
|
||||
extra={
|
||||
<Button type="primary" onClick={() => history.push('/homepage')}>
|
||||
Back Home
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
||||
export default NoFoundPage;
|
||||
@@ -0,0 +1,270 @@
|
||||
import IconFont from '@/components/IconFont';
|
||||
import { getTextWidth, groupByColumn, isMobile } from '@/utils/utils';
|
||||
import { AutoComplete, Select, Tag } from 'antd';
|
||||
import classNames from 'classnames';
|
||||
import { debounce } from 'lodash';
|
||||
import { forwardRef, useCallback, useEffect, useImperativeHandle, useRef, useState } from 'react';
|
||||
import type { ForwardRefRenderFunction } from 'react';
|
||||
import { searchRecommend } from 'supersonic-chat-sdk';
|
||||
import { SemanticTypeEnum, SEMANTIC_TYPE_MAP } from '../constants';
|
||||
import styles from './style.less';
|
||||
import { PLACE_HOLDER } from '@/common/constants';
|
||||
|
||||
type Props = {
|
||||
inputMsg: string;
|
||||
chatId?: number;
|
||||
onInputMsgChange: (value: string) => void;
|
||||
onSendMsg: (msg: string, domainId?: number) => void;
|
||||
};
|
||||
|
||||
const { OptGroup, Option } = Select;
|
||||
let isPinyin = false;
|
||||
let isSelect = false;
|
||||
|
||||
const compositionStartEvent = () => {
|
||||
isPinyin = true;
|
||||
};
|
||||
|
||||
const compositionEndEvent = () => {
|
||||
isPinyin = false;
|
||||
};
|
||||
|
||||
const ChatFooter: ForwardRefRenderFunction<any, Props> = (
|
||||
{ inputMsg, chatId, onInputMsgChange, onSendMsg },
|
||||
ref,
|
||||
) => {
|
||||
const [stepOptions, setStepOptions] = useState<Record<string, any[]>>({});
|
||||
const [open, setOpen] = useState(false);
|
||||
const [focused, setFocused] = useState(false);
|
||||
const inputRef = useRef<any>();
|
||||
const fetchRef = useRef(0);
|
||||
|
||||
const inputFocus = () => {
|
||||
inputRef.current?.focus();
|
||||
};
|
||||
|
||||
const inputBlur = () => {
|
||||
inputRef.current?.blur();
|
||||
};
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
inputFocus,
|
||||
inputBlur,
|
||||
}));
|
||||
|
||||
const initEvents = () => {
|
||||
const autoCompleteEl = document.getElementById('chatInput');
|
||||
autoCompleteEl!.addEventListener('compositionstart', compositionStartEvent);
|
||||
autoCompleteEl!.addEventListener('compositionend', compositionEndEvent);
|
||||
};
|
||||
|
||||
const removeEvents = () => {
|
||||
const autoCompleteEl = document.getElementById('chatInput');
|
||||
if (autoCompleteEl) {
|
||||
autoCompleteEl.removeEventListener('compositionstart', compositionStartEvent);
|
||||
autoCompleteEl.removeEventListener('compositionend', compositionEndEvent);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
initEvents();
|
||||
return () => {
|
||||
removeEvents();
|
||||
};
|
||||
}, []);
|
||||
|
||||
const debounceGetWordsFunc = useCallback(() => {
|
||||
const getAssociateWords = async (msg: string, chatId?: number) => {
|
||||
if (isPinyin) {
|
||||
return;
|
||||
}
|
||||
fetchRef.current += 1;
|
||||
const fetchId = fetchRef.current;
|
||||
const res = await searchRecommend(msg, chatId);
|
||||
if (fetchId !== fetchRef.current) {
|
||||
return;
|
||||
}
|
||||
const recommends = msg ? res.data.data || [] : [];
|
||||
const stepOptionList = recommends.map((item: any) => item.subRecommend);
|
||||
|
||||
if (stepOptionList.length > 0 && stepOptionList.every((item: any) => item !== null)) {
|
||||
const data = groupByColumn(recommends, 'domainName');
|
||||
const optionsData =
|
||||
isMobile && recommends.length > 6
|
||||
? Object.keys(data)
|
||||
.slice(0, 4)
|
||||
.reduce((result, key) => {
|
||||
result[key] = data[key].slice(
|
||||
0,
|
||||
Object.keys(data).length > 2 ? 2 : Object.keys(data).length > 1 ? 3 : 6,
|
||||
);
|
||||
return result;
|
||||
}, {})
|
||||
: data;
|
||||
setStepOptions(optionsData);
|
||||
} else {
|
||||
setStepOptions({});
|
||||
}
|
||||
|
||||
setOpen(recommends.length > 0);
|
||||
};
|
||||
return debounce(getAssociateWords, 20);
|
||||
}, []);
|
||||
|
||||
const [debounceGetWords] = useState<any>(debounceGetWordsFunc);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isSelect) {
|
||||
debounceGetWords(inputMsg, chatId);
|
||||
} else {
|
||||
isSelect = false;
|
||||
}
|
||||
if (!inputMsg) {
|
||||
setStepOptions({});
|
||||
}
|
||||
}, [inputMsg]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!focused) {
|
||||
setOpen(false);
|
||||
}
|
||||
}, [focused]);
|
||||
|
||||
useEffect(() => {
|
||||
const autoCompleteDropdown = document.querySelector(
|
||||
`.${styles.autoCompleteDropdown}`,
|
||||
) as HTMLElement;
|
||||
if (!autoCompleteDropdown) {
|
||||
return;
|
||||
}
|
||||
const textWidth = getTextWidth(inputMsg);
|
||||
if (Object.keys(stepOptions).length > 0) {
|
||||
autoCompleteDropdown.style.marginLeft = `${textWidth}px`;
|
||||
}
|
||||
}, [stepOptions]);
|
||||
|
||||
const sendMsg = (value: string) => {
|
||||
const option = Object.keys(stepOptions)
|
||||
.reduce((result: any[], item) => {
|
||||
result = result.concat(stepOptions[item]);
|
||||
return result;
|
||||
}, [])
|
||||
.find((item) =>
|
||||
Object.keys(stepOptions).length === 1
|
||||
? item.recommend === value
|
||||
: `${item.domainName || ''}${item.recommend}` === value,
|
||||
);
|
||||
if (option && isSelect) {
|
||||
onSendMsg(option.recommend, option.domainId);
|
||||
} else {
|
||||
onSendMsg(value);
|
||||
}
|
||||
};
|
||||
|
||||
const autoCompleteDropdownClass = classNames(styles.autoCompleteDropdown, {
|
||||
[styles.external]: true,
|
||||
[styles.mobile]: isMobile,
|
||||
});
|
||||
|
||||
const onSelect = (value: string) => {
|
||||
isSelect = true;
|
||||
sendMsg(value);
|
||||
setOpen(false);
|
||||
setTimeout(() => {
|
||||
isSelect = false;
|
||||
}, 200);
|
||||
};
|
||||
|
||||
const chatFooterClass = classNames(styles.chatFooter, {
|
||||
[styles.mobile]: isMobile,
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={chatFooterClass}>
|
||||
<div className={styles.composer}>
|
||||
<div className={styles.composerInputWrapper}>
|
||||
<AutoComplete
|
||||
className={styles.composerInput}
|
||||
placeholder={PLACE_HOLDER}
|
||||
value={inputMsg}
|
||||
onChange={onInputMsgChange}
|
||||
onSelect={onSelect}
|
||||
autoFocus={!isMobile}
|
||||
backfill
|
||||
ref={inputRef}
|
||||
id="chatInput"
|
||||
onKeyDown={(e) => {
|
||||
if ((e.code === 'Enter' || e.code === 'NumpadEnter') && !isSelect) {
|
||||
const chatInputEl: any = document.getElementById('chatInput');
|
||||
sendMsg(chatInputEl.value);
|
||||
setOpen(false);
|
||||
}
|
||||
}}
|
||||
onFocus={() => {
|
||||
setFocused(true);
|
||||
}}
|
||||
onBlur={() => {
|
||||
setFocused(false);
|
||||
}}
|
||||
dropdownClassName={autoCompleteDropdownClass}
|
||||
listHeight={500}
|
||||
allowClear
|
||||
open={open}
|
||||
getPopupContainer={isMobile ? (triggerNode) => triggerNode.parentNode : undefined}
|
||||
>
|
||||
{Object.keys(stepOptions).map((key) => {
|
||||
return (
|
||||
<OptGroup key={key} label={key}>
|
||||
{stepOptions[key].map((option) => (
|
||||
<Option
|
||||
key={`${option.recommend}${option.domainName ? `_${option.domainName}` : ''}`}
|
||||
value={
|
||||
Object.keys(stepOptions).length === 1
|
||||
? option.recommend
|
||||
: `${option.domainName || ''}${option.recommend}`
|
||||
}
|
||||
className={styles.searchOption}
|
||||
>
|
||||
<div className={styles.optionContent}>
|
||||
{option.schemaElementType && (
|
||||
<Tag
|
||||
className={styles.semanticType}
|
||||
color={
|
||||
option.schemaElementType === SemanticTypeEnum.DIMENSION ||
|
||||
option.schemaElementType === SemanticTypeEnum.DOMAIN
|
||||
? 'blue'
|
||||
: option.schemaElementType === SemanticTypeEnum.VALUE
|
||||
? 'geekblue'
|
||||
: 'orange'
|
||||
}
|
||||
>
|
||||
{SEMANTIC_TYPE_MAP[option.schemaElementType] ||
|
||||
option.schemaElementType ||
|
||||
'维度'}
|
||||
</Tag>
|
||||
)}
|
||||
{option.subRecommend}
|
||||
</div>
|
||||
</Option>
|
||||
))}
|
||||
</OptGroup>
|
||||
);
|
||||
})}
|
||||
</AutoComplete>
|
||||
<div
|
||||
className={classNames(styles.sendBtn, {
|
||||
[styles.sendBtnActive]: inputMsg?.length > 0,
|
||||
})}
|
||||
onClick={() => {
|
||||
sendMsg(inputMsg);
|
||||
}}
|
||||
>
|
||||
<IconFont type="icon-ios-send" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default forwardRef(ChatFooter);
|
||||
@@ -0,0 +1,153 @@
|
||||
.chatFooter {
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-top: 6px;
|
||||
margin-right: 20px;
|
||||
margin-bottom: 40px;
|
||||
|
||||
.composer {
|
||||
display: flex;
|
||||
height: 46px;
|
||||
|
||||
.composerInputWrapper {
|
||||
flex: 1;
|
||||
|
||||
.composerInput {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
:global {
|
||||
.ant-select-selector {
|
||||
box-sizing: border-box;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
color: rgba(0, 0, 0, 0.87);
|
||||
font-size: 16px;
|
||||
word-break: break-all;
|
||||
background: #fff;
|
||||
border: 0;
|
||||
border-radius: 24px;
|
||||
box-shadow: rgba(0, 0, 0, 0.07) 0 -0.5px 0, rgba(0, 0, 0, 0.1) 0 0 18px;
|
||||
transition: border-color 0.15s ease-in-out;
|
||||
resize: none;
|
||||
|
||||
.ant-select-selection-search-input {
|
||||
height: 100% !important;
|
||||
padding: 0 20px;
|
||||
}
|
||||
|
||||
.ant-select-selection-search {
|
||||
right: 0 !important;
|
||||
left: 0 !important;
|
||||
}
|
||||
|
||||
.ant-select-selection-placeholder {
|
||||
padding-left: 10px !important;
|
||||
line-height: 45px;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-select-clear {
|
||||
right: auto;
|
||||
left: 500px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin-top: -8px;
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:global {
|
||||
.ant-select-focused {
|
||||
.ant-select-selector {
|
||||
box-shadow: rgb(74, 114, 245) 0 0 3px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.sendBtn {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
right: 6px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
color: #fff;
|
||||
font-size: 20px;
|
||||
background-color: rgb(184, 184, 191);
|
||||
border: unset;
|
||||
border-radius: 50%;
|
||||
transform: translateY(-50%);
|
||||
transition: background-color 0.3s ease 0s;
|
||||
|
||||
&.sendBtnActive {
|
||||
background-color: var(--chat-blue);
|
||||
}
|
||||
}
|
||||
|
||||
&.mobile {
|
||||
height: 40px;
|
||||
margin: 12px;
|
||||
margin-bottom: 20px;
|
||||
|
||||
.composer {
|
||||
height: 40px;
|
||||
|
||||
:global {
|
||||
.ant-select-selector {
|
||||
font-size: 14px !important;
|
||||
}
|
||||
|
||||
.ant-select-selection-placeholder {
|
||||
line-height: 39px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.searchOption {
|
||||
padding: 6px 20px;
|
||||
color: #212121;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.mobile {
|
||||
.searchOption {
|
||||
min-height: 26px;
|
||||
padding: 2px 12px;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.domain {
|
||||
margin-top: 2px;
|
||||
color: var(--text-color-fourth);
|
||||
font-size: 13px;
|
||||
line-height: 12px;
|
||||
}
|
||||
|
||||
.autoCompleteDropdown {
|
||||
left: 285px !important;
|
||||
width: fit-content !important;
|
||||
min-width: 50px !important;
|
||||
border-radius: 6px;
|
||||
|
||||
&.external {
|
||||
left: 226px !important;
|
||||
}
|
||||
|
||||
&.mobile {
|
||||
left: 20px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.semanticType {
|
||||
margin-right: 10px;
|
||||
}
|
||||
203
webapp/packages/supersonic-fe/src/pages/Chat/Conversation.tsx
Normal file
203
webapp/packages/supersonic-fe/src/pages/Chat/Conversation.tsx
Normal file
@@ -0,0 +1,203 @@
|
||||
import IconFont from '@/components/IconFont';
|
||||
import { Dropdown, Menu, message } from 'antd';
|
||||
import classNames from 'classnames';
|
||||
import {
|
||||
useEffect,
|
||||
useState,
|
||||
forwardRef,
|
||||
ForwardRefRenderFunction,
|
||||
useImperativeHandle,
|
||||
} from 'react';
|
||||
import { useLocation } from 'umi';
|
||||
import ConversationHistory from './components/ConversationHistory';
|
||||
import ConversationModal from './components/ConversationModal';
|
||||
import { deleteConversation, getAllConversations, saveConversation } from './service';
|
||||
import styles from './style.less';
|
||||
import { ConversationDetailType } from './type';
|
||||
|
||||
type Props = {
|
||||
currentConversation?: ConversationDetailType;
|
||||
onSelectConversation: (conversation: ConversationDetailType, name?: string) => void;
|
||||
};
|
||||
|
||||
const Conversation: ForwardRefRenderFunction<any, Props> = (
|
||||
{ currentConversation, onSelectConversation },
|
||||
ref,
|
||||
) => {
|
||||
const location = useLocation();
|
||||
const { q, cid } = (location as any).query;
|
||||
const [originConversations, setOriginConversations] = useState<ConversationDetailType[]>([]);
|
||||
const [conversations, setConversations] = useState<ConversationDetailType[]>([]);
|
||||
const [editModalVisible, setEditModalVisible] = useState(false);
|
||||
const [editConversation, setEditConversation] = useState<ConversationDetailType>();
|
||||
const [historyVisible, setHistoryVisible] = useState(false);
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
updateData,
|
||||
onAddConversation,
|
||||
}));
|
||||
|
||||
const updateData = async () => {
|
||||
const { data } = await getAllConversations();
|
||||
const conversationList = (data || []).slice(0, 5);
|
||||
setOriginConversations(data || []);
|
||||
setConversations(conversationList);
|
||||
return conversationList;
|
||||
};
|
||||
|
||||
const initData = async () => {
|
||||
const data = await updateData();
|
||||
if (data.length > 0) {
|
||||
const chatId = localStorage.getItem('CONVERSATION_ID') || cid;
|
||||
if (chatId) {
|
||||
const conversation = data.find((item: any) => item.chatId === +chatId);
|
||||
if (conversation) {
|
||||
onSelectConversation(conversation);
|
||||
} else {
|
||||
onSelectConversation(data[0]);
|
||||
}
|
||||
} else {
|
||||
onSelectConversation(data[0]);
|
||||
}
|
||||
} else {
|
||||
onAddConversation();
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (q && cid === undefined) {
|
||||
onAddConversation(q);
|
||||
} else {
|
||||
initData();
|
||||
}
|
||||
}, [q]);
|
||||
|
||||
const addConversation = async (name?: string) => {
|
||||
await saveConversation(name || '新问答对话');
|
||||
return updateData();
|
||||
};
|
||||
|
||||
const onDeleteConversation = async (id: number) => {
|
||||
await deleteConversation(id);
|
||||
initData();
|
||||
};
|
||||
|
||||
const onAddConversation = async (name?: string) => {
|
||||
const data = await addConversation(name);
|
||||
onSelectConversation(data[0], name);
|
||||
};
|
||||
|
||||
const onOperate = (key: string, conversation: ConversationDetailType) => {
|
||||
if (key === 'editName') {
|
||||
setEditConversation(conversation);
|
||||
setEditModalVisible(true);
|
||||
} else if (key === 'delete') {
|
||||
onDeleteConversation(conversation.chatId);
|
||||
}
|
||||
};
|
||||
|
||||
const onNewChat = () => {
|
||||
onAddConversation('新问答对话');
|
||||
};
|
||||
|
||||
const onShowHistory = () => {
|
||||
setHistoryVisible(true);
|
||||
};
|
||||
|
||||
const onShare = () => {
|
||||
message.info('正在开发中,敬请期待');
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.conversation}>
|
||||
<div className={styles.leftSection}>
|
||||
<div className={styles.conversationList}>
|
||||
{conversations.map((item) => {
|
||||
const conversationItemClass = classNames(styles.conversationItem, {
|
||||
[styles.activeConversationItem]: currentConversation?.chatId === item.chatId,
|
||||
});
|
||||
return (
|
||||
<Dropdown
|
||||
key={item.chatId}
|
||||
overlay={
|
||||
<Menu
|
||||
items={[
|
||||
{ label: '修改对话名称', key: 'editName' },
|
||||
{ label: '删除', key: 'delete' },
|
||||
]}
|
||||
onClick={({ key }) => {
|
||||
onOperate(key, item);
|
||||
}}
|
||||
/>
|
||||
}
|
||||
trigger={['contextMenu']}
|
||||
>
|
||||
<div
|
||||
key={item.chatId}
|
||||
className={conversationItemClass}
|
||||
onClick={() => {
|
||||
onSelectConversation(item);
|
||||
}}
|
||||
>
|
||||
<div className={styles.conversationItemContent}>
|
||||
<IconFont type="icon-chat1" className={styles.conversationIcon} />
|
||||
<div className={styles.conversationContent} title={item.chatName}>
|
||||
{item.chatName}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Dropdown>
|
||||
);
|
||||
})}
|
||||
<div className={styles.conversationItem} onClick={onShowHistory}>
|
||||
<div className={styles.conversationItemContent}>
|
||||
<IconFont
|
||||
type="icon-more2"
|
||||
className={`${styles.conversationIcon} ${styles.historyIcon}`}
|
||||
/>
|
||||
<div className={styles.conversationContent}>查看更多对话</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.operateSection}>
|
||||
<div className={styles.operateItem} onClick={onNewChat}>
|
||||
<IconFont type="icon-add" className={`${styles.operateIcon} ${styles.addIcon}`} />
|
||||
<div className={styles.operateLabel}>新建对话</div>
|
||||
</div>
|
||||
<div className={styles.operateItem} onClick={onShare}>
|
||||
<IconFont
|
||||
type="icon-fenxiang2"
|
||||
className={`${styles.operateIcon} ${styles.shareIcon}`}
|
||||
/>
|
||||
<div className={styles.operateLabel}>分享</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{historyVisible && (
|
||||
<ConversationHistory
|
||||
conversations={originConversations}
|
||||
onSelectConversation={(conversation) => {
|
||||
onSelectConversation(conversation);
|
||||
setHistoryVisible(false);
|
||||
}}
|
||||
onClose={() => {
|
||||
setHistoryVisible(false);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<ConversationModal
|
||||
visible={editModalVisible}
|
||||
editConversation={editConversation}
|
||||
onClose={() => {
|
||||
setEditModalVisible(false);
|
||||
}}
|
||||
onFinish={() => {
|
||||
setEditModalVisible(false);
|
||||
updateData();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default forwardRef(Conversation);
|
||||
@@ -0,0 +1,89 @@
|
||||
import Text from './components/Text';
|
||||
import { memo, useCallback, useEffect } from 'react';
|
||||
import { isEqual } from 'lodash';
|
||||
import styles from './style.less';
|
||||
import { connect, Dispatch } from 'umi';
|
||||
import { ChatItem } from 'supersonic-chat-sdk';
|
||||
import type { MsgDataType } from 'supersonic-chat-sdk';
|
||||
import { MessageItem, MessageTypeEnum } from './type';
|
||||
|
||||
type Props = {
|
||||
id: string;
|
||||
chatId: number;
|
||||
messageList: MessageItem[];
|
||||
dispatch: Dispatch;
|
||||
onClickMessageContainer: () => void;
|
||||
onMsgDataLoaded: (data: MsgDataType) => void;
|
||||
onSelectSuggestion: (value: string) => void;
|
||||
onUpdateMessageScroll: () => void;
|
||||
};
|
||||
|
||||
const MessageContainer: React.FC<Props> = ({
|
||||
id,
|
||||
chatId,
|
||||
messageList,
|
||||
dispatch,
|
||||
onClickMessageContainer,
|
||||
onMsgDataLoaded,
|
||||
onSelectSuggestion,
|
||||
onUpdateMessageScroll,
|
||||
}) => {
|
||||
const onWindowResize = useCallback(() => {
|
||||
dispatch({
|
||||
type: 'windowResize/setTriggerResize',
|
||||
payload: true,
|
||||
});
|
||||
setTimeout(() => {
|
||||
dispatch({
|
||||
type: 'windowResize/setTriggerResize',
|
||||
payload: false,
|
||||
});
|
||||
}, 0);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener('resize', onWindowResize);
|
||||
return () => {
|
||||
window.removeEventListener('resize', onWindowResize);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div id={id} className={styles.messageContainer} onClick={onClickMessageContainer}>
|
||||
<div className={styles.messageList}>
|
||||
{messageList.map((msgItem: MessageItem, index: number) => {
|
||||
return (
|
||||
<div key={`${msgItem.id}`} id={`${msgItem.id}`} className={styles.messageItem}>
|
||||
{msgItem.type === MessageTypeEnum.TEXT && <Text position="left" data={msgItem.msg} />}
|
||||
{msgItem.type === MessageTypeEnum.QUESTION && (
|
||||
<>
|
||||
<Text position="right" data={msgItem.msg} quote={msgItem.quote} />
|
||||
<ChatItem
|
||||
msg={msgItem.msg || ''}
|
||||
msgData={msgItem.msgData}
|
||||
conversationId={chatId}
|
||||
classId={msgItem.domainId}
|
||||
isLastMessage={index === messageList.length - 1}
|
||||
onLastMsgDataLoaded={onMsgDataLoaded}
|
||||
onSelectSuggestion={onSelectSuggestion}
|
||||
onUpdateMessageScroll={onUpdateMessageScroll}
|
||||
suggestionEnable
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
function areEqual(prevProps: Props, nextProps: Props) {
|
||||
if (prevProps.id === nextProps.id && isEqual(prevProps.messageList, nextProps.messageList)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export default connect()(memo(MessageContainer, areEqual));
|
||||
@@ -0,0 +1,56 @@
|
||||
import moment from 'moment';
|
||||
import styles from './style.less';
|
||||
import type { ChatContextType } from 'supersonic-chat-sdk';
|
||||
|
||||
type Props = {
|
||||
chatContext: ChatContextType;
|
||||
};
|
||||
|
||||
const Context: React.FC<Props> = ({ chatContext }) => {
|
||||
const { domainName, metrics, dateInfo, filters } = chatContext;
|
||||
|
||||
return (
|
||||
<div className={styles.context}>
|
||||
<div className={styles.title}>相关信息</div>
|
||||
<div className={styles.content}>
|
||||
<div className={styles.field}>
|
||||
<span className={styles.fieldName}>主题域:</span>
|
||||
<span className={styles.fieldValue}>{domainName}</span>
|
||||
</div>
|
||||
{dateInfo && (
|
||||
<div className={styles.field}>
|
||||
<span className={styles.fieldName}>时间范围:</span>
|
||||
<span className={styles.fieldValue}>
|
||||
{dateInfo.text ||
|
||||
`近${moment(dateInfo.endDate).diff(moment(dateInfo.startDate), 'days') + 1}天`}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{metrics && metrics.length > 0 && (
|
||||
<div className={styles.field}>
|
||||
<span className={styles.fieldName}>指标:</span>
|
||||
<span className={styles.fieldValue}>
|
||||
{metrics.map((metric) => metric.name).join('、')}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{filters && filters.length > 0 && (
|
||||
<div className={styles.filterSection}>
|
||||
<div className={styles.fieldName}>筛选条件:</div>
|
||||
<div className={styles.filterValues}>
|
||||
{filters.map((filter) => {
|
||||
return (
|
||||
<div className={styles.filterItem} key={filter.name}>
|
||||
{filter.name}:{filter.value}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Context;
|
||||
@@ -0,0 +1,70 @@
|
||||
.context {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.title {
|
||||
margin-bottom: 22px;
|
||||
color: var(--text-color);
|
||||
font-size: 16px;
|
||||
line-height: 24px;
|
||||
}
|
||||
|
||||
.desc {
|
||||
max-height: 350px;
|
||||
margin-top: 10px;
|
||||
margin-bottom: 10px;
|
||||
overflow-y: auto;
|
||||
color: var(--text-color-third);
|
||||
font-size: 13px;
|
||||
line-height: 22px;
|
||||
}
|
||||
|
||||
.field {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
|
||||
&.columnLayout {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
margin-right: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.filterSection {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.fieldName {
|
||||
margin-right: 6px;
|
||||
color: var(--text-color);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.fieldValue {
|
||||
color: var(--text-color);
|
||||
|
||||
&.switchField {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.filterValues {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 10px;
|
||||
font-size: 13px;
|
||||
column-gap: 4px;
|
||||
row-gap: 4px;
|
||||
|
||||
.filterItem {
|
||||
padding: 2px 12px;
|
||||
color: var(--text-color-secondary);
|
||||
background-color: var(--body-background);
|
||||
border-radius: 13px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import { getFormattedValueData } from '@/utils/utils';
|
||||
import moment from 'moment';
|
||||
import React from 'react';
|
||||
import styles from './style.less';
|
||||
import type { EntityInfoType, MsgDataType } from 'supersonic-chat-sdk';
|
||||
|
||||
type Props = {
|
||||
currentEntity: MsgDataType;
|
||||
};
|
||||
|
||||
const Introduction: React.FC<Props> = ({ currentEntity }) => {
|
||||
const { entityInfo } = currentEntity;
|
||||
const { dimensions, metrics } = entityInfo || ({} as EntityInfoType);
|
||||
|
||||
return (
|
||||
<div className={styles.introduction}>
|
||||
{dimensions
|
||||
?.filter((dimension) => !dimension.bizName.includes('photo'))
|
||||
.map((dimension) => {
|
||||
return (
|
||||
<div className={styles.field} key={dimension.name}>
|
||||
<span className={styles.fieldName}>{dimension.name}:</span>
|
||||
<span className={styles.fieldValue}>
|
||||
{dimension.bizName.includes('publish_time')
|
||||
? moment(dimension.value).format('YYYY-MM-DD')
|
||||
: dimension.value}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{metrics?.map((metric) => (
|
||||
<div className={styles.field} key={metric.name}>
|
||||
<span className={styles.fieldName}>{metric.name}:</span>
|
||||
<span className={styles.fieldValue}>{getFormattedValueData(metric.value)}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Introduction;
|
||||
@@ -0,0 +1,63 @@
|
||||
.introduction {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding-bottom: 4px;
|
||||
|
||||
.title {
|
||||
margin-bottom: 22px;
|
||||
color: var(--text-color);
|
||||
font-size: 16px;
|
||||
line-height: 24px;
|
||||
}
|
||||
|
||||
.desc {
|
||||
max-height: 350px;
|
||||
margin-top: 10px;
|
||||
margin-bottom: 10px;
|
||||
overflow-y: auto;
|
||||
color: var(--text-color-third);
|
||||
font-size: 13px;
|
||||
line-height: 22px;
|
||||
}
|
||||
|
||||
.field {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
|
||||
&.columnLayout {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
margin-right: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.fieldName {
|
||||
margin-right: 6px;
|
||||
color: var(--text-color);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.fieldValue {
|
||||
color: var(--text-color);
|
||||
|
||||
&.switchField {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&.dimensionFieldValue {
|
||||
max-width: 90px;
|
||||
// white-space: nowrap;
|
||||
}
|
||||
|
||||
&.mainNameFieldValue {
|
||||
max-width: 90px;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import classNames from 'classnames';
|
||||
import Context from './Context';
|
||||
import Introduction from './Introduction';
|
||||
import styles from './style.less';
|
||||
import type { MsgDataType } from 'supersonic-chat-sdk';
|
||||
|
||||
type Props = {
|
||||
currentEntity?: MsgDataType;
|
||||
};
|
||||
|
||||
const RightSection: React.FC<Props> = ({ currentEntity }) => {
|
||||
const rightSectionClass = classNames(styles.rightSection, {
|
||||
[styles.external]: true,
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={rightSectionClass}>
|
||||
{currentEntity && (
|
||||
<div className={styles.entityInfo}>
|
||||
{currentEntity?.chatContext && <Context chatContext={currentEntity.chatContext} />}
|
||||
<Introduction currentEntity={currentEntity} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default RightSection;
|
||||
@@ -0,0 +1,19 @@
|
||||
.rightSection {
|
||||
width: 225px;
|
||||
height: calc(100vh - 48px);
|
||||
padding-right: 10px;
|
||||
padding-bottom: 10px;
|
||||
padding-left: 20px;
|
||||
overflow-y: auto;
|
||||
|
||||
.entityInfo {
|
||||
margin-top: 30px;
|
||||
|
||||
.topInfo {
|
||||
margin-bottom: 20px;
|
||||
color: var(--text-color-third);
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import { CloseOutlined } from '@ant-design/icons';
|
||||
import moment from 'moment';
|
||||
import type { ConversationDetailType } from '../../type';
|
||||
import styles from './style.less';
|
||||
|
||||
type Props = {
|
||||
conversations: ConversationDetailType[];
|
||||
onSelectConversation: (conversation: ConversationDetailType) => void;
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
const ConversationHistory: React.FC<Props> = ({ conversations, onSelectConversation, onClose }) => {
|
||||
return (
|
||||
<div className={styles.conversationHistory}>
|
||||
<div className={styles.header}>
|
||||
<div className={styles.headerTitle}>历史记录</div>
|
||||
<CloseOutlined className={styles.headerClose} onClick={onClose} />
|
||||
</div>
|
||||
<div className={styles.conversationContent}>
|
||||
{conversations.slice(0, 1000).map((conversation) => {
|
||||
return (
|
||||
<div
|
||||
key={conversation.chatId}
|
||||
className={styles.conversationItem}
|
||||
onClick={() => {
|
||||
onSelectConversation(conversation);
|
||||
}}
|
||||
>
|
||||
<div className={styles.conversationName} title={conversation.chatName}>
|
||||
{conversation.chatName}
|
||||
</div>
|
||||
<div className={styles.conversationTime}>
|
||||
更新时间:{moment(conversation.lastTime).format('YYYY-MM-DD')}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ConversationHistory;
|
||||
@@ -0,0 +1,64 @@
|
||||
.conversationHistory {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 10;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 215px;
|
||||
height: calc(100vh - 48px);
|
||||
overflow: hidden;
|
||||
background: #f3f3f7;
|
||||
border-right: 1px solid var(--border-color-base);
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 50px;
|
||||
padding: 0 16px;
|
||||
border-bottom: 1px solid #e8e8e8;
|
||||
.headerTitle {
|
||||
color: var(--text-color);
|
||||
font-weight: 500;
|
||||
font-size: 16px;
|
||||
}
|
||||
.headerClose {
|
||||
color: var(--text-color-third);
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
color: var(--chat-blue);
|
||||
}
|
||||
}
|
||||
}
|
||||
.conversationContent {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
.conversationItem {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
padding: 8px 16px;
|
||||
border-bottom: 1px solid var(--border-color-base-bg-5);
|
||||
cursor: pointer;
|
||||
row-gap: 2px;
|
||||
|
||||
&:hover {
|
||||
background: var(--light-blue-background);
|
||||
}
|
||||
.conversationName {
|
||||
width: 170px;
|
||||
overflow: hidden;
|
||||
color: var(--text-color);
|
||||
font-size: 14px;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.conversationTime {
|
||||
color: var(--text-color-third);
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
import { Form, Input, Modal } from 'antd';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { updateConversationName } from '../../service';
|
||||
import type { ConversationDetailType } from '../../type';
|
||||
|
||||
const FormItem = Form.Item;
|
||||
|
||||
type Props = {
|
||||
visible: boolean;
|
||||
editConversation?: ConversationDetailType;
|
||||
onClose: () => void;
|
||||
onFinish: (conversationName: string) => void;
|
||||
};
|
||||
|
||||
const layout = {
|
||||
labelCol: { span: 6 },
|
||||
wrapperCol: { span: 18 },
|
||||
};
|
||||
|
||||
const ConversationModal: React.FC<Props> = ({ visible, editConversation, onClose, onFinish }) => {
|
||||
const [form] = Form.useForm();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const conversationNameInputRef = useRef<any>();
|
||||
|
||||
useEffect(() => {
|
||||
if (visible) {
|
||||
form.setFieldsValue({ conversationName: editConversation!.chatName });
|
||||
setTimeout(() => {
|
||||
conversationNameInputRef.current.focus({
|
||||
cursor: 'all',
|
||||
});
|
||||
}, 0);
|
||||
}
|
||||
}, [visible]);
|
||||
|
||||
const onConfirm = async () => {
|
||||
const values = await form.validateFields();
|
||||
setLoading(true);
|
||||
await updateConversationName(values.conversationName, editConversation!.chatId);
|
||||
setLoading(false);
|
||||
onFinish(values.conversationName);
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title="修改问答对话名称"
|
||||
visible={visible}
|
||||
onCancel={onClose}
|
||||
onOk={onConfirm}
|
||||
confirmLoading={loading}
|
||||
>
|
||||
<Form {...layout} form={form}>
|
||||
<FormItem name="conversationName" label="名称" rules={[{ required: true }]}>
|
||||
<Input
|
||||
placeholder="请输入问答对话名称"
|
||||
ref={conversationNameInputRef}
|
||||
onPressEnter={onConfirm}
|
||||
/>
|
||||
</FormItem>
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default ConversationModal;
|
||||
@@ -0,0 +1,35 @@
|
||||
import classNames from 'classnames';
|
||||
import styles from './style.less';
|
||||
|
||||
type Props = {
|
||||
position: 'left' | 'right';
|
||||
bubbleClassName?: string;
|
||||
aggregator?: string;
|
||||
noTime?: boolean;
|
||||
};
|
||||
|
||||
const Message: React.FC<Props> = ({ position, children, bubbleClassName }) => {
|
||||
const messageClass = classNames(styles.message, {
|
||||
[styles.left]: position === 'left',
|
||||
[styles.right]: position === 'right',
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={messageClass}>
|
||||
<div className={styles.messageContent}>
|
||||
<div className={styles.messageBody}>
|
||||
<div
|
||||
className={`${styles.bubble}${bubbleClassName ? ` ${bubbleClassName}` : ''}`}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Message;
|
||||
@@ -0,0 +1,19 @@
|
||||
import Message from './Message';
|
||||
import styles from './style.less';
|
||||
|
||||
type Props = {
|
||||
position: 'left' | 'right';
|
||||
data: any;
|
||||
quote?: string;
|
||||
};
|
||||
|
||||
const Text: React.FC<Props> = ({ position, data, quote }) => {
|
||||
return (
|
||||
<Message position={position} bubbleClassName={styles.textBubble}>
|
||||
{position === 'right' && quote && <div className={styles.quote}>{quote}</div>}
|
||||
<div className={styles.text}>{data}</div>
|
||||
</Message>
|
||||
);
|
||||
};
|
||||
|
||||
export default Text;
|
||||
@@ -0,0 +1,19 @@
|
||||
import { CHAT_BLUE } from '@/common/constants';
|
||||
import { Spin } from 'antd';
|
||||
import BeatLoader from 'react-spinners/BeatLoader';
|
||||
import Message from './Message';
|
||||
import styles from './style.less';
|
||||
|
||||
const Typing = () => {
|
||||
return (
|
||||
<Message position="left" bubbleClassName={styles.typingBubble}>
|
||||
<Spin
|
||||
spinning={true}
|
||||
indicator={<BeatLoader color={CHAT_BLUE} size={10} />}
|
||||
className={styles.typing}
|
||||
/>
|
||||
</Message>
|
||||
);
|
||||
};
|
||||
|
||||
export default Typing;
|
||||
@@ -0,0 +1,277 @@
|
||||
.message {
|
||||
.messageContent {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
|
||||
.messageBody {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.bubble {
|
||||
box-sizing: border-box;
|
||||
min-width: 1px;
|
||||
max-width: 100%;
|
||||
padding: 8px 16px 10px;
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
border: 1px solid transparent;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.14), 0 0 2px rgba(0, 0, 0, 0.12);
|
||||
|
||||
.text {
|
||||
line-height: 1.5;
|
||||
white-space: pre-wrap;
|
||||
overflow-wrap: break-word;
|
||||
user-select: text;
|
||||
}
|
||||
|
||||
.textMsg {
|
||||
padding: 12px 0 5px;
|
||||
}
|
||||
|
||||
.topBar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
max-width: 100%;
|
||||
padding: 4px 0 8px;
|
||||
overflow-x: auto;
|
||||
color: var(--text-color);
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
white-space: nowrap;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.03);
|
||||
|
||||
.messageTitleWrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.messageTitle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: var(--text-color);
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.right {
|
||||
.messageContent {
|
||||
flex-direction: row-reverse;
|
||||
|
||||
.bubble {
|
||||
float: right;
|
||||
box-sizing: border-box;
|
||||
padding: 8px 16px;
|
||||
color: #fff;
|
||||
font-size: 16px;
|
||||
background: linear-gradient(81.62deg, #2870ea 8.72%, var(--chat-blue) 85.01%);
|
||||
border: 1px solid transparent;
|
||||
border-radius: 12px 4px 12px 12px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.14), 0 0 2px rgba(0, 0, 0, 0.12);
|
||||
|
||||
.text {
|
||||
&::selection {
|
||||
background: #1ba1f7;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.textBubble {
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
.listenerSex {
|
||||
padding-bottom: 24px;
|
||||
}
|
||||
|
||||
.listenerArea {
|
||||
padding-top: 24px;
|
||||
padding-bottom: 12px;
|
||||
}
|
||||
|
||||
.typing {
|
||||
width: 100%;
|
||||
padding: 0 5px;
|
||||
|
||||
:global {
|
||||
.ant-spin-dot {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.messageEntityName {
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
}
|
||||
|
||||
.messageAvatar {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.dataHolder {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.subTitle {
|
||||
margin-left: 20px;
|
||||
color: var(--text-color-third);
|
||||
font-weight: normal;
|
||||
font-size: 12px;
|
||||
|
||||
.subTitleValue {
|
||||
margin-left: 6px;
|
||||
color: var(--text-color);
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
.avatarPopover {
|
||||
:global {
|
||||
.ant-popover-inner-content {
|
||||
padding: 3px 4px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.moreOption {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-top: 10px;
|
||||
color: var(--text-color-fourth);
|
||||
font-size: 12px;
|
||||
|
||||
.selectOthers {
|
||||
color: var(--text-color);
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
}
|
||||
|
||||
.indicators {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-left: 12px;
|
||||
column-gap: 12px;
|
||||
|
||||
.indicator {
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.contentName {
|
||||
max-width: 350px;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.aggregatorIndicator {
|
||||
color: var(--text-color);
|
||||
font-weight: 500;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.entityId {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-left: 12px;
|
||||
column-gap: 4px;
|
||||
|
||||
.idTitle {
|
||||
color: var(--text-color-fourth);
|
||||
font-size: 12px;
|
||||
}
|
||||
.idValue {
|
||||
color: var(--text-color-fourth);
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.typingBubble {
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
.quote {
|
||||
margin-bottom: 4px;
|
||||
padding: 0 4px 0 6px;
|
||||
color: var(--border-color-base);
|
||||
font-size: 13px;
|
||||
border-left: 4px solid var(--border-color-base);
|
||||
border-top-left-radius: 2px;
|
||||
border-bottom-left-radius: 2px;
|
||||
}
|
||||
|
||||
.filterSection {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: var(--text-color-secondary);
|
||||
font-weight: normal;
|
||||
font-size: 13px;
|
||||
.filterItem {
|
||||
padding: 2px 12px;
|
||||
color: var(--text-color-secondary);
|
||||
background-color: #edf2f2;
|
||||
border-radius: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
.noPermissionTip {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.tip {
|
||||
margin-left: 6px;
|
||||
color: var(--text-color-third);
|
||||
}
|
||||
|
||||
.infoBar {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
margin-top: 20px;
|
||||
column-gap: 20px;
|
||||
}
|
||||
|
||||
.mainEntityInfo {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
font-size: 13px;
|
||||
column-gap: 20px;
|
||||
|
||||
.infoItem {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
.infoName {
|
||||
color: var(--text-color-fourth);
|
||||
}
|
||||
|
||||
.infoValue {
|
||||
color: var(--text-color-secondary);
|
||||
}
|
||||
}
|
||||
}
|
||||
28
webapp/packages/supersonic-fe/src/pages/Chat/constants.ts
Normal file
28
webapp/packages/supersonic-fe/src/pages/Chat/constants.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
export const THEME_COLOR_LIST = [
|
||||
'#3369FF',
|
||||
'#36D2B8',
|
||||
'#DB8D76',
|
||||
'#47B359',
|
||||
'#8545E6',
|
||||
'#E0B18B',
|
||||
'#7258F3',
|
||||
'#0095FF',
|
||||
'#52CC8F',
|
||||
'#6675FF',
|
||||
'#CC516E',
|
||||
'#5CA9E6',
|
||||
];
|
||||
|
||||
export enum SemanticTypeEnum {
|
||||
DOMAIN = 'DOMAIN',
|
||||
DIMENSION = 'DIMENSION',
|
||||
METRIC = 'METRIC',
|
||||
VALUE = 'VALUE',
|
||||
}
|
||||
|
||||
export const SEMANTIC_TYPE_MAP = {
|
||||
[SemanticTypeEnum.DOMAIN]: '主题域',
|
||||
[SemanticTypeEnum.DIMENSION]: '维度',
|
||||
[SemanticTypeEnum.METRIC]: '指标',
|
||||
[SemanticTypeEnum.VALUE]: '维度值',
|
||||
};
|
||||
238
webapp/packages/supersonic-fe/src/pages/Chat/index.tsx
Normal file
238
webapp/packages/supersonic-fe/src/pages/Chat/index.tsx
Normal file
@@ -0,0 +1,238 @@
|
||||
import { updateMessageContainerScroll, isMobile, uuid } from '@/utils/utils';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { Helmet } from 'umi';
|
||||
import MessageContainer from './MessageContainer';
|
||||
import styles from './style.less';
|
||||
import { ConversationDetailType, MessageItem, MessageTypeEnum } from './type';
|
||||
import { updateConversationName } from './service';
|
||||
import { useThrottleFn } from 'ahooks';
|
||||
import Conversation from './Conversation';
|
||||
import RightSection from './RightSection';
|
||||
import ChatFooter from './ChatFooter';
|
||||
import classNames from 'classnames';
|
||||
import { DEFAULT_CONVERSATION_NAME, WEB_TITLE } from '@/common/constants';
|
||||
import { HistoryMsgItemType, MsgDataType, getHistoryMsg, queryContext } from 'supersonic-chat-sdk';
|
||||
import { getConversationContext } from './utils';
|
||||
|
||||
const Chat = () => {
|
||||
const [messageList, setMessageList] = useState<MessageItem[]>([]);
|
||||
const [inputMsg, setInputMsg] = useState('');
|
||||
const [pageNo, setPageNo] = useState(1);
|
||||
const [hasNextPage, setHasNextPage] = useState(false);
|
||||
const [historyInited, setHistoryInited] = useState(false);
|
||||
const [currentConversation, setCurrentConversation] = useState<
|
||||
ConversationDetailType | undefined
|
||||
>(isMobile ? { chatId: 0, chatName: '问答对话' } : undefined);
|
||||
const [currentEntity, setCurrentEntity] = useState<MsgDataType>();
|
||||
const conversationRef = useRef<any>();
|
||||
const chatFooterRef = useRef<any>();
|
||||
|
||||
const sendHelloRsp = () => {
|
||||
setMessageList([
|
||||
{
|
||||
id: uuid(),
|
||||
type: MessageTypeEnum.TEXT,
|
||||
msg: '您好,请问有什么我能帮您吗?',
|
||||
},
|
||||
]);
|
||||
};
|
||||
|
||||
const updateHistoryMsg = async (page: number) => {
|
||||
const res = await getHistoryMsg(page, currentConversation!.chatId);
|
||||
const { hasNextPage, list } = res.data.data;
|
||||
setMessageList([
|
||||
...list.map((item: HistoryMsgItemType) => ({
|
||||
id: item.questionId,
|
||||
type: MessageTypeEnum.QUESTION,
|
||||
msg: item.queryText,
|
||||
msgData: item.queryResponse,
|
||||
})),
|
||||
...(page === 1 ? [] : messageList),
|
||||
]);
|
||||
setHasNextPage(hasNextPage);
|
||||
if (page === 1) {
|
||||
if (list.length === 0) {
|
||||
sendHelloRsp();
|
||||
} else {
|
||||
setCurrentEntity(list[list.length - 1].queryResponse);
|
||||
}
|
||||
updateMessageContainerScroll();
|
||||
setHistoryInited(true);
|
||||
}
|
||||
if (page > 1) {
|
||||
const msgEle = document.getElementById(`${messageList[0]?.id}`);
|
||||
msgEle?.scrollIntoView();
|
||||
}
|
||||
};
|
||||
|
||||
const { run: handleScroll } = useThrottleFn(
|
||||
(e) => {
|
||||
if (e.target.scrollTop === 0 && hasNextPage) {
|
||||
updateHistoryMsg(pageNo + 1);
|
||||
setPageNo(pageNo + 1);
|
||||
}
|
||||
},
|
||||
{
|
||||
leading: true,
|
||||
trailing: true,
|
||||
wait: 200,
|
||||
},
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (historyInited) {
|
||||
const messageContainerEle = document.getElementById('messageContainer');
|
||||
messageContainerEle?.addEventListener('scroll', handleScroll);
|
||||
}
|
||||
return () => {
|
||||
const messageContainerEle = document.getElementById('messageContainer');
|
||||
messageContainerEle?.removeEventListener('scroll', handleScroll);
|
||||
};
|
||||
}, [historyInited]);
|
||||
|
||||
const inputFocus = () => {
|
||||
if (!isMobile) {
|
||||
chatFooterRef.current?.inputFocus();
|
||||
}
|
||||
};
|
||||
|
||||
const inputBlur = () => {
|
||||
chatFooterRef.current?.inputBlur();
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!currentConversation) {
|
||||
return;
|
||||
}
|
||||
setCurrentEntity(undefined);
|
||||
const { initMsg, domainId } = currentConversation;
|
||||
if (initMsg) {
|
||||
inputFocus();
|
||||
if (initMsg === DEFAULT_CONVERSATION_NAME) {
|
||||
sendHelloRsp();
|
||||
return;
|
||||
}
|
||||
onSendMsg(currentConversation.initMsg, [], domainId, true);
|
||||
return;
|
||||
}
|
||||
updateHistoryMsg(1);
|
||||
setPageNo(1);
|
||||
}, [currentConversation]);
|
||||
|
||||
const modifyConversationName = async (name: string) => {
|
||||
await updateConversationName(name, currentConversation!.chatId);
|
||||
conversationRef?.current?.updateData();
|
||||
window.history.replaceState('', '', `?q=${name}&cid=${currentConversation!.chatId}`);
|
||||
};
|
||||
|
||||
const onSendMsg = async (
|
||||
msg?: string,
|
||||
list?: MessageItem[],
|
||||
domainId?: number,
|
||||
firstMsg?: boolean,
|
||||
) => {
|
||||
const currentMsg = msg || inputMsg;
|
||||
if (currentMsg.trim() === '') {
|
||||
setInputMsg('');
|
||||
return;
|
||||
}
|
||||
let quote = '';
|
||||
if (currentEntity && !firstMsg) {
|
||||
const { data } = await queryContext(currentMsg, currentConversation!.chatId);
|
||||
if (data.code === 200 && data.data.domainId === currentEntity.chatContext?.domainId) {
|
||||
quote = getConversationContext(data.data);
|
||||
}
|
||||
}
|
||||
setMessageList([
|
||||
...(list || messageList),
|
||||
{ id: uuid(), msg: currentMsg, domainId, type: MessageTypeEnum.QUESTION, quote },
|
||||
]);
|
||||
updateMessageContainerScroll();
|
||||
setInputMsg('');
|
||||
modifyConversationName(currentMsg);
|
||||
};
|
||||
|
||||
const onInputMsgChange = (value: string) => {
|
||||
const inputMsgValue = value || '';
|
||||
setInputMsg(inputMsgValue);
|
||||
};
|
||||
|
||||
const saveConversationToLocal = (conversation: ConversationDetailType) => {
|
||||
if (conversation) {
|
||||
if (conversation.chatId !== -1) {
|
||||
localStorage.setItem('CONVERSATION_ID', `${conversation.chatId}`);
|
||||
}
|
||||
} else {
|
||||
localStorage.removeItem('CONVERSATION_ID');
|
||||
}
|
||||
};
|
||||
|
||||
const onSelectConversation = (conversation: ConversationDetailType, name?: string) => {
|
||||
window.history.replaceState('', '', `?q=${conversation.chatName}&cid=${conversation.chatId}`);
|
||||
setCurrentConversation({
|
||||
...conversation,
|
||||
initMsg: name,
|
||||
});
|
||||
saveConversationToLocal(conversation);
|
||||
};
|
||||
|
||||
const onMsgDataLoaded = (data: MsgDataType) => {
|
||||
setCurrentEntity(data);
|
||||
updateMessageContainerScroll();
|
||||
};
|
||||
|
||||
const chatClass = classNames(styles.chat, {
|
||||
[styles.external]: true,
|
||||
[styles.mobile]: isMobile,
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={chatClass}>
|
||||
<Helmet title={WEB_TITLE} />
|
||||
<div className={styles.topSection} />
|
||||
<div className={styles.chatSection}>
|
||||
{!isMobile && (
|
||||
<Conversation
|
||||
currentConversation={currentConversation}
|
||||
onSelectConversation={onSelectConversation}
|
||||
ref={conversationRef}
|
||||
/>
|
||||
)}
|
||||
<div className={styles.chatApp}>
|
||||
{currentConversation && (
|
||||
<div className={styles.chatBody}>
|
||||
<div className={styles.chatContent}>
|
||||
<MessageContainer
|
||||
id="messageContainer"
|
||||
messageList={messageList}
|
||||
chatId={currentConversation?.chatId}
|
||||
onClickMessageContainer={() => {
|
||||
inputFocus();
|
||||
}}
|
||||
onMsgDataLoaded={onMsgDataLoaded}
|
||||
onSelectSuggestion={onSendMsg}
|
||||
onUpdateMessageScroll={updateMessageContainerScroll}
|
||||
/>
|
||||
<ChatFooter
|
||||
inputMsg={inputMsg}
|
||||
chatId={currentConversation?.chatId}
|
||||
onInputMsgChange={onInputMsgChange}
|
||||
onSendMsg={(msg: string, domainId?: number) => {
|
||||
onSendMsg(msg, messageList, domainId);
|
||||
if (isMobile) {
|
||||
inputBlur();
|
||||
}
|
||||
}}
|
||||
ref={chatFooterRef}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{!isMobile && <RightSection currentEntity={currentEntity} />}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Chat;
|
||||
22
webapp/packages/supersonic-fe/src/pages/Chat/service.ts
Normal file
22
webapp/packages/supersonic-fe/src/pages/Chat/service.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { request } from 'umi';
|
||||
|
||||
const prefix = '/api';
|
||||
|
||||
export function saveConversation(chatName: string) {
|
||||
return request<Result<any>>(`${prefix}/chat/manage/save?chatName=${chatName}`, { method: 'POST' });
|
||||
}
|
||||
|
||||
export function updateConversationName(chatName: string, chatId: number = 0) {
|
||||
return request<Result<any>>(
|
||||
`${prefix}/chat/manage/updateChatName?chatName=${chatName}&chatId=${chatId}`,
|
||||
{ method: 'POST' },
|
||||
);
|
||||
}
|
||||
|
||||
export function deleteConversation(chatId: number) {
|
||||
return request<Result<any>>(`${prefix}/chat/manage/delete?chatId=${chatId}`, { method: 'POST' });
|
||||
}
|
||||
|
||||
export function getAllConversations() {
|
||||
return request<Result<any>>(`${prefix}/chat/manage/getAll`);
|
||||
}
|
||||
579
webapp/packages/supersonic-fe/src/pages/Chat/style.less
Normal file
579
webapp/packages/supersonic-fe/src/pages/Chat/style.less
Normal file
@@ -0,0 +1,579 @@
|
||||
.chat {
|
||||
height: calc(100vh - 48px) !important;
|
||||
overflow-y: hidden;
|
||||
background: linear-gradient(180deg, rgba(23, 74, 228, 0) 29.44%, rgba(23, 74, 228, 0.06) 100%),
|
||||
linear-gradient(90deg, #f3f3f7 0%, #f3f3f7 20%, #ebf0f9 60%, #f3f3f7 80%, #f3f3f7 100%);
|
||||
|
||||
&.external {
|
||||
.chatApp {
|
||||
width: calc(100vw - 450px) !important;
|
||||
height: calc(100vh - 58px) !important;
|
||||
}
|
||||
}
|
||||
|
||||
&.mobile {
|
||||
height: 100vh !important;
|
||||
|
||||
.chatSection {
|
||||
// height: 100vh !important;
|
||||
height: 100% !important;
|
||||
}
|
||||
|
||||
.conversation {
|
||||
// height: 100vh !important;
|
||||
height: 100% !important;
|
||||
}
|
||||
|
||||
.chatApp {
|
||||
width: 100vw !important;
|
||||
// height: 100vh !important;
|
||||
height: 100% !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.chatSection {
|
||||
display: flex;
|
||||
height: calc(100vh - 48px) !important;
|
||||
overflow-y: hidden;
|
||||
}
|
||||
|
||||
.chatBody {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.conversation {
|
||||
position: relative;
|
||||
width: 225px;
|
||||
height: calc(100vh - 48px);
|
||||
|
||||
.leftSection {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.chatApp {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: calc(100vw - 510px);
|
||||
height: calc(100vh - 58px) !important;
|
||||
margin-top: 10px;
|
||||
color: rgba(0, 0, 0, 0.87);
|
||||
|
||||
.emptyHolder {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.navBar {
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 40px;
|
||||
padding: 0 10px;
|
||||
background: rgb(243 243 243);
|
||||
border-bottom: 1px solid rgb(228, 228, 228);
|
||||
|
||||
.conversationNameWrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.conversationName {
|
||||
padding: 4px 12px;
|
||||
color: var(--text-color-third) !important;
|
||||
font-size: 14px !important;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
|
||||
.editIcon {
|
||||
margin-left: 10px;
|
||||
color: var(--text-color-fourth);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(0, 0, 0, 0.03);
|
||||
}
|
||||
}
|
||||
|
||||
.divider {
|
||||
width: 1px;
|
||||
height: 16px;
|
||||
margin-right: 4px;
|
||||
margin-left: 12px;
|
||||
background-color: var(--text-color-fourth);
|
||||
}
|
||||
}
|
||||
|
||||
.conversationInput {
|
||||
width: 300px;
|
||||
color: var(--text-color-third) !important;
|
||||
font-size: 14px !important;
|
||||
cursor: default !important;
|
||||
}
|
||||
}
|
||||
|
||||
.chatBody {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
|
||||
.chatContent {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
|
||||
.messageContainer {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
overflow-x: hidden;
|
||||
overflow-y: scroll;
|
||||
|
||||
.messageList {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 0 20px 90px 4px;
|
||||
row-gap: 20px;
|
||||
|
||||
.messageItem {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
row-gap: 20px;
|
||||
}
|
||||
|
||||
&.reportLoading {
|
||||
position: absolute;
|
||||
bottom: 10000px;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mobile {
|
||||
.messageList {
|
||||
padding: 0 12px 20px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.keyword {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.messageItem {
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.messageTime {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-top: 20px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.modules {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 40px;
|
||||
padding: 8px 12px;
|
||||
overflow: hidden;
|
||||
background: rgb(243, 243, 243);
|
||||
border-top: 1px solid rgb(228, 228, 228);
|
||||
|
||||
.moduleType {
|
||||
width: 80px;
|
||||
margin-right: 12px;
|
||||
|
||||
:global {
|
||||
.ant-select-selection-item {
|
||||
font-size: 13px !important;
|
||||
}
|
||||
.ant-select-selection-item,
|
||||
.ant-select-single:not(.ant-select-customize-input) .ant-select-selector::after {
|
||||
line-height: 28px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.moduleSelect {
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
color: rgba(0, 0, 0, 0.87);
|
||||
word-break: break-all;
|
||||
border: 0;
|
||||
border-radius: 20px;
|
||||
|
||||
:global {
|
||||
.ant-select-selector {
|
||||
height: 30px !important;
|
||||
border: 1px solid var(--primary-color) !important;
|
||||
border-radius: 20px !important;
|
||||
}
|
||||
|
||||
.ant-select-arrow {
|
||||
margin-top: -4px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.example {
|
||||
margin-right: 4px;
|
||||
// margin-left: 16px;
|
||||
color: var(--text-color-secondary);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
:global {
|
||||
button[ant-click-animating-without-extra-node]::after {
|
||||
border: 0 none;
|
||||
opacity: 0;
|
||||
animation: none 0 ease 0 1 normal;
|
||||
}
|
||||
|
||||
.iconBtn {
|
||||
color: rgba(0, 0, 0, 0.4) !important;
|
||||
background: transparent !important;
|
||||
border: 0 !important;
|
||||
box-shadow: none !important;
|
||||
|
||||
.scrollerControlIcon {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: rgba(0, 0, 0, 0.05) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.modulesInner {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
overflow-x: scroll;
|
||||
overflow-y: hidden;
|
||||
scroll-behavior: smooth;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.moduleItem {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-left: 8px;
|
||||
padding: 4px 11px;
|
||||
color: var(--text-color);
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
line-height: 1.43;
|
||||
white-space: nowrap;
|
||||
background: #fff;
|
||||
border: 1px solid #fff;
|
||||
border-radius: 20px;
|
||||
cursor: pointer;
|
||||
transition: 0.15s ease-in-out;
|
||||
|
||||
&:hover {
|
||||
background: rgba(0, 0, 0, 0.05);
|
||||
background-clip: padding-box;
|
||||
border-color: rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
margin-left: 0 !important;
|
||||
}
|
||||
|
||||
&.activeModuleItem {
|
||||
color: var(--primary-color) !important;
|
||||
border-color: var(--primary-color) !important;
|
||||
}
|
||||
|
||||
&.cmdItem {
|
||||
font-weight: normal;
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.optGroupBar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
// width: 530px;
|
||||
|
||||
&.recentSearchBar {
|
||||
padding-top: 2px;
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
|
||||
.optGroupTitle {
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.recentSearch {
|
||||
color: #999;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.clearSearch {
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.recentSearchOption {
|
||||
padding-left: 12px !important;
|
||||
|
||||
.optionItem {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 483px;
|
||||
|
||||
.removeRecentMsg {
|
||||
display: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
&:hover {
|
||||
.removeRecentMsg {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.conversationList {
|
||||
padding-top: 20px;
|
||||
.conversationItem {
|
||||
padding-left: 16px;
|
||||
cursor: pointer;
|
||||
|
||||
.conversationItemContent {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 12px 0;
|
||||
color: var(--text-color-third);
|
||||
|
||||
.conversationIcon {
|
||||
margin-right: 10px;
|
||||
color: var(--text-color-fourth);
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.conversationContent {
|
||||
width: 160px;
|
||||
overflow: hidden;
|
||||
color: var(--text-color-third);
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
|
||||
&.activeConversationItem,
|
||||
&:hover {
|
||||
.conversationContent {
|
||||
color: var(--chat-blue);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.addConversation {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-left: 12px;
|
||||
color: var(--text-color-third);
|
||||
column-gap: 4px;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
}
|
||||
|
||||
.loadingWords {
|
||||
padding: 40px 1px;
|
||||
}
|
||||
|
||||
.associateWordsOption {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
column-gap: 10px;
|
||||
|
||||
.optionContent {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-width: 450px;
|
||||
column-gap: 10px;
|
||||
}
|
||||
|
||||
.indicatorItem {
|
||||
min-width: 180px;
|
||||
.indicatorLabel {
|
||||
color: var(--text-color-fourth);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.indicatorValue {
|
||||
margin-left: 4px;
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.collapseBtn {
|
||||
margin: 0 10px;
|
||||
color: var(--text-color-third);
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
}
|
||||
|
||||
.autoCompleteDropdown {
|
||||
width: 650px !important;
|
||||
min-width: 650px !important;
|
||||
border-radius: 10px;
|
||||
|
||||
:global {
|
||||
.ant-select-item {
|
||||
min-height: 36px !important;
|
||||
line-height: 26px !important;
|
||||
|
||||
&:not(:first-child):hover {
|
||||
background: #f5f5f5 !important;
|
||||
}
|
||||
}
|
||||
|
||||
// .ant-select-item-option-active:not(.ant-select-item-option-disabled) {
|
||||
// background-color: #fff;
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
.recommendItemTitle {
|
||||
margin-right: 14px;
|
||||
padding: 4px 12px;
|
||||
background-color: var(--deep-background);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.refeshQuestions {
|
||||
cursor: pointer;
|
||||
|
||||
.reloadIcon {
|
||||
margin-right: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.recommendQuestions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 54px;
|
||||
height: 100%;
|
||||
color: var(--text-color-fourth);
|
||||
font-size: 18px;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
.currentTool {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0 24px 0 2px;
|
||||
color: var(--chat-blue);
|
||||
font-weight: 500;
|
||||
font-size: 16px;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
|
||||
.removeTool {
|
||||
position: absolute;
|
||||
top: 14px;
|
||||
right: 6px;
|
||||
color: var(--text-color-fifth);
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: var(--chat-blue);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.associateOption {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.associateOptionAvatar {
|
||||
width: 32px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.optionContent {
|
||||
min-width: 330px;
|
||||
}
|
||||
|
||||
.optionIndicator {
|
||||
min-width: 120px;
|
||||
margin-left: 4px;
|
||||
color: var(--text-color-fourth);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.operateSection {
|
||||
margin-top: 20px;
|
||||
padding-left: 15px;
|
||||
}
|
||||
|
||||
.operateItem {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 10px 0;
|
||||
cursor: pointer;
|
||||
|
||||
.operateIcon {
|
||||
margin-right: 10px;
|
||||
color: var(--text-color-fourth);
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.operateLabel {
|
||||
color: var(--text-color-third);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.operateLabel {
|
||||
color: var(--chat-blue);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.messageLoading {
|
||||
margin-top: 30px;
|
||||
}
|
||||
34
webapp/packages/supersonic-fe/src/pages/Chat/type.ts
Normal file
34
webapp/packages/supersonic-fe/src/pages/Chat/type.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { MsgDataType } from 'supersonic-chat-sdk';
|
||||
|
||||
export enum MessageTypeEnum {
|
||||
TEXT = 'text', // 指标文本
|
||||
QUESTION = 'question',
|
||||
TAG = 'tag', // 标签
|
||||
SUGGESTION = 'suggestion', // 建议
|
||||
NO_PERMISSION = 'no_permission', // 无权限
|
||||
SEMANTIC_DETAIL = 'semantic_detail', // 语义指标/维度等信息详情
|
||||
}
|
||||
|
||||
export type MessageItem = {
|
||||
id: string | number;
|
||||
type?: MessageTypeEnum;
|
||||
msg?: string;
|
||||
domainId?: number;
|
||||
msgData?: MsgDataType;
|
||||
quote?: string;
|
||||
};
|
||||
|
||||
export type ConversationDetailType = {
|
||||
chatId: number;
|
||||
chatName: string;
|
||||
createTime?: string;
|
||||
creator?: string;
|
||||
lastQuestion?: string;
|
||||
lastTime?: string;
|
||||
initMsg?: string;
|
||||
domainId?: number;
|
||||
};
|
||||
|
||||
export enum MessageModeEnum {
|
||||
INTERPRET = 'interpret',
|
||||
}
|
||||
16
webapp/packages/supersonic-fe/src/pages/Chat/utils.ts
Normal file
16
webapp/packages/supersonic-fe/src/pages/Chat/utils.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { ChatContextType } from 'supersonic-chat-sdk';
|
||||
import moment from 'moment';
|
||||
|
||||
export function getConversationContext(chatContext: ChatContextType) {
|
||||
if (!chatContext) return '';
|
||||
const { domainName, metrics, dateInfo } = chatContext;
|
||||
// const dimensionStr =
|
||||
// dimensions?.length > 0 ? dimensions.map((dimension) => dimension.name).join('、') : '';
|
||||
const timeStr =
|
||||
dateInfo?.text ||
|
||||
`近${moment(dateInfo?.endDate).diff(moment(dateInfo?.startDate), 'days') + 1}天`;
|
||||
|
||||
return `${domainName}${
|
||||
metrics?.length > 0 ? `${timeStr}${metrics.map((metric) => metric.name).join('、')}` : ''
|
||||
}`;
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Form, Button, Modal, Input } from 'antd';
|
||||
import type { RegisterFormDetail } from './types';
|
||||
|
||||
export type RegisterFormProps = {
|
||||
onCancel: () => void;
|
||||
onSubmit: (values: RegisterFormDetail) => Promise<any>;
|
||||
createModalVisible: boolean;
|
||||
};
|
||||
|
||||
const formLayout = {
|
||||
labelCol: { span: 4 },
|
||||
wrapperCol: { span: 18 },
|
||||
};
|
||||
|
||||
const { Item } = Form;
|
||||
|
||||
const ForgetPwdForm: React.FC<RegisterFormProps> = (props) => {
|
||||
const [formVals, setFormVals] = useState<Partial<RegisterFormDetail>>({
|
||||
email: '', // 邮箱
|
||||
});
|
||||
const [saveLoading, setSaveLoading] = useState(false);
|
||||
const [form] = Form.useForm();
|
||||
|
||||
const { onSubmit: handleUpdate, onCancel, createModalVisible } = props;
|
||||
|
||||
const handleSubmit = async () => {
|
||||
const fieldsValue = await form.validateFields();
|
||||
setFormVals({ ...formVals, ...fieldsValue });
|
||||
setSaveLoading(true);
|
||||
const formValues = {
|
||||
...formVals,
|
||||
...fieldsValue,
|
||||
};
|
||||
try {
|
||||
await handleUpdate(formValues);
|
||||
setSaveLoading(false);
|
||||
} catch (error) {
|
||||
setSaveLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const renderFooter = () => {
|
||||
return (
|
||||
<>
|
||||
<Button onClick={onCancel}>取消</Button>
|
||||
<Button type="primary" loading={saveLoading} onClick={handleSubmit}>
|
||||
发送邮件
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
width={600}
|
||||
bodyStyle={{ padding: '32px 40px 48px' }}
|
||||
destroyOnClose
|
||||
title="忘记密码"
|
||||
open={createModalVisible}
|
||||
footer={renderFooter()}
|
||||
onCancel={onCancel}
|
||||
>
|
||||
<Form
|
||||
{...formLayout}
|
||||
form={form}
|
||||
initialValues={{
|
||||
...formVals,
|
||||
}}
|
||||
>
|
||||
<Item name="email" rules={[{ required: true }]} label="邮箱地址">
|
||||
<Input size="large" type="email" placeholder="请输入邮箱地址" />
|
||||
</Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default ForgetPwdForm;
|
||||
@@ -0,0 +1,87 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Form, Button, Modal, Input } from 'antd';
|
||||
|
||||
import type { RegisterFormDetail } from './types';
|
||||
|
||||
export type RegisterFormProps = {
|
||||
onCancel: () => void;
|
||||
onSubmit: (values: RegisterFormDetail) => Promise<any>;
|
||||
createModalVisible: boolean;
|
||||
};
|
||||
|
||||
const formLayout = {
|
||||
labelCol: { span: 4 },
|
||||
wrapperCol: { span: 18 },
|
||||
};
|
||||
|
||||
const { Item } = Form;
|
||||
|
||||
const RegisterForm: React.FC<RegisterFormProps> = (props) => {
|
||||
const [formVals, setFormVals] = useState<Partial<RegisterFormDetail>>({
|
||||
name: '', // 名称
|
||||
password: '', // 密码
|
||||
});
|
||||
const [saveLoading, setSaveLoading] = useState(false);
|
||||
const [form] = Form.useForm();
|
||||
|
||||
const { onSubmit: handleUpdate, onCancel, createModalVisible } = props;
|
||||
|
||||
const handleSubmit = async () => {
|
||||
const fieldsValue = await form.validateFields();
|
||||
setFormVals({ ...formVals, ...fieldsValue });
|
||||
setSaveLoading(true);
|
||||
const formValus = {
|
||||
...formVals,
|
||||
...fieldsValue,
|
||||
};
|
||||
try {
|
||||
await handleUpdate(formValus);
|
||||
setSaveLoading(false);
|
||||
} catch (error) {
|
||||
setSaveLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const renderFooter = () => {
|
||||
return (
|
||||
<>
|
||||
<Button onClick={onCancel}>取消</Button>
|
||||
<Button type="primary" loading={saveLoading} onClick={handleSubmit}>
|
||||
完成注册
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
width={600}
|
||||
bodyStyle={{ padding: '32px 40px 48px' }}
|
||||
destroyOnClose
|
||||
title="用户注册"
|
||||
open={createModalVisible}
|
||||
footer={renderFooter()}
|
||||
onCancel={onCancel}
|
||||
>
|
||||
<Form
|
||||
{...formLayout}
|
||||
form={form}
|
||||
initialValues={{
|
||||
...formVals,
|
||||
}}
|
||||
>
|
||||
<Item name="name" rules={[{ required: true }]} label="账号">
|
||||
<Input size="large" placeholder="请输入账号" />
|
||||
</Item>
|
||||
<Item name="password" rules={[{ required: true, min: 6, max: 10 }]} label="密码">
|
||||
<Input size="large" type="password" placeholder="请输入密码" />
|
||||
</Item>
|
||||
{/* <Item name="email" rules={[{ required: true, type: 'email' }]} label="邮箱地址">
|
||||
<Input size="large" type="email" placeholder="请输入邮箱地址" />
|
||||
</Item> */}
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default RegisterForm;
|
||||
@@ -0,0 +1,9 @@
|
||||
export type RegisterFormDetail = {
|
||||
password: string; // 密码
|
||||
name: string; // 用户名
|
||||
};
|
||||
|
||||
export type ResetPasswordFormDetail = {
|
||||
password: string;
|
||||
passwordConfirm: string;
|
||||
};
|
||||
137
webapp/packages/supersonic-fe/src/pages/Login/index.tsx
Normal file
137
webapp/packages/supersonic-fe/src/pages/Login/index.tsx
Normal file
@@ -0,0 +1,137 @@
|
||||
// import type { FC } from 'react';
|
||||
import styles from './style.less';
|
||||
import { Button, Form, Input, message, Space } from 'antd';
|
||||
import { LockOutlined, UserOutlined } from '@ant-design/icons';
|
||||
import RegisterForm from './components/RegisterForm';
|
||||
// import ForgetPwdForm from './components/ForgetPwdForm';
|
||||
import S2Icon, { ICON } from '@/components/S2Icon';
|
||||
import React, { useState } from 'react';
|
||||
import { useForm } from 'antd/lib/form/Form';
|
||||
import type { RegisterFormDetail } from './components/types';
|
||||
import { postUserLogin, userRegister } from './services';
|
||||
import { AUTH_TOKEN_KEY } from '@/common/constants';
|
||||
import { queryCurrentUser } from '@/services/user';
|
||||
import { history, useModel } from 'umi';
|
||||
import { setToken as setChatSdkToken } from 'supersonic-chat-sdk';
|
||||
|
||||
const { Item } = Form;
|
||||
const LoginPage: React.FC = () => {
|
||||
const [createModalVisible, setCreateModalVisible] = useState<boolean>(false);
|
||||
// const [forgetModalVisible, setForgetModalVisible] = useState<boolean>(false);
|
||||
const [form] = useForm();
|
||||
const { initialState = {}, setInitialState } = useModel('@@initialState');
|
||||
// 通过用户信息进行登录
|
||||
const loginDone = async (values: RegisterFormDetail) => {
|
||||
const { code, data, msg } = await postUserLogin(values);
|
||||
if (code === 200) {
|
||||
localStorage.setItem(AUTH_TOKEN_KEY, data);
|
||||
setChatSdkToken(data || '');
|
||||
const { code: queryUserCode, data: queryUserData } = await queryCurrentUser();
|
||||
if (queryUserCode === 200) {
|
||||
const currentUser = {
|
||||
...queryUserData,
|
||||
staffName: queryUserData.staffName || queryUserData.name,
|
||||
};
|
||||
setInitialState({ ...initialState, currentUser });
|
||||
}
|
||||
history.push('/');
|
||||
return;
|
||||
}
|
||||
message.success(msg);
|
||||
};
|
||||
|
||||
// 处理登录按钮响应
|
||||
const handleLogin = async () => {
|
||||
const { validateFields } = form;
|
||||
const content = await validateFields();
|
||||
await loginDone(content);
|
||||
};
|
||||
|
||||
// 处理注册弹窗确定按钮
|
||||
const handleRegister = async (values: RegisterFormDetail) => {
|
||||
const { code } = await userRegister({ ...values });
|
||||
if (code === 200) {
|
||||
message.success('注册成功');
|
||||
setCreateModalVisible(false);
|
||||
// 注册完自动帮用户登录
|
||||
await loginDone(values);
|
||||
}
|
||||
};
|
||||
|
||||
// 相应注册按钮
|
||||
const handleRegisterBtn = () => {
|
||||
setCreateModalVisible(true);
|
||||
};
|
||||
|
||||
// // 忘记密码弹窗确定响应
|
||||
// const handleForgetPwd = async (values: RegisterFormDetail) => {
|
||||
// await getUserForgetPwd({ ...values });
|
||||
// message.success('发送邮件成功,请在收到邮件后进入邮件链接进行密码重置');
|
||||
// setForgetModalVisible(false);
|
||||
// };
|
||||
|
||||
// // 响应忘记密码按钮
|
||||
// const handleForgetPwdBtn = () => {
|
||||
// setForgetModalVisible(true);
|
||||
// };
|
||||
|
||||
return (
|
||||
<div className={styles.loginWarp}>
|
||||
<div className={styles.content}>
|
||||
<div className={styles.formContent}>
|
||||
<div className={styles.formBox}>
|
||||
<Form form={form} labelCol={{ span: 6 }} colon={false}>
|
||||
<div className={styles.loginMain}>
|
||||
<h3 className={styles.title}>
|
||||
<Space>
|
||||
<S2Icon
|
||||
icon={ICON.iconlogobiaoshi}
|
||||
size={30}
|
||||
color="#296DF3"
|
||||
style={{ display: 'inline-block', marginTop: 8 }}
|
||||
/>
|
||||
<div>超音数(SuperSonic)</div>
|
||||
</Space>
|
||||
</h3>
|
||||
<Item name="name" rules={[{ required: true }]} label="">
|
||||
<Input size="large" placeholder="用户名: admin" prefix={<UserOutlined />} />
|
||||
</Item>
|
||||
<Item name="password" rules={[{ required: true }]} label="">
|
||||
<Input
|
||||
size="large"
|
||||
type="password"
|
||||
placeholder="密码: admin"
|
||||
onPressEnter={handleLogin}
|
||||
prefix={<LockOutlined />}
|
||||
/>
|
||||
</Item>
|
||||
|
||||
<Button className={styles.signInBtn} type="primary" onClick={handleLogin}>
|
||||
登录
|
||||
</Button>
|
||||
|
||||
<div className={styles.tool}>
|
||||
<Button className={styles.button} onClick={handleRegisterBtn}>
|
||||
注册
|
||||
</Button>
|
||||
{/* <Button className={styles.button} type="link" onClick={handleForgetPwdBtn}>
|
||||
忘记密码
|
||||
</Button> */}
|
||||
</div>
|
||||
</div>
|
||||
</Form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<RegisterForm
|
||||
onCancel={() => {
|
||||
setCreateModalVisible(false);
|
||||
}}
|
||||
onSubmit={handleRegister}
|
||||
createModalVisible={createModalVisible}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default LoginPage;
|
||||
29
webapp/packages/supersonic-fe/src/pages/Login/services.ts
Normal file
29
webapp/packages/supersonic-fe/src/pages/Login/services.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import request from 'umi-request';
|
||||
|
||||
export interface PostUserLoginRes {
|
||||
code: string; // 返回编码
|
||||
msg: string; // 返回消息
|
||||
data: string;
|
||||
traceId: string;
|
||||
}
|
||||
|
||||
export interface PostUserRegesiterRes {
|
||||
code: string; // 返回编码
|
||||
msg: string; // 返回消息
|
||||
data: never;
|
||||
traceId: string;
|
||||
}
|
||||
|
||||
export function userRegister(data: any): Promise<any> {
|
||||
return request(`${process.env.AUTH_API_BASE_URL}user/register`, {
|
||||
method: 'POST',
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
export function postUserLogin(data: any): Promise<any> {
|
||||
return request(`${process.env.AUTH_API_BASE_URL}user/login`, {
|
||||
method: 'POST',
|
||||
data,
|
||||
});
|
||||
}
|
||||
61
webapp/packages/supersonic-fe/src/pages/Login/style.less
Normal file
61
webapp/packages/supersonic-fe/src/pages/Login/style.less
Normal file
@@ -0,0 +1,61 @@
|
||||
@import '~@/assets/css/variable.less';
|
||||
|
||||
.loginWarp {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
overflow: auto;
|
||||
.content {
|
||||
flex: 1;
|
||||
padding: 32px 0 24px;
|
||||
.formContent {
|
||||
display: flex;
|
||||
flex: 1 1;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
padding: 132px 0 24px;
|
||||
overflow: auto;
|
||||
background: inherit;
|
||||
.formBox {
|
||||
min-width: 480px;
|
||||
max-width: 500px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.loginMain {
|
||||
// max-width: 480px;
|
||||
// min-height: 200px;
|
||||
margin: 120px auto auto;
|
||||
padding: 20px;
|
||||
background: #fff;
|
||||
border-radius: 5px;
|
||||
box-shadow: 0 0 10px 2px #eee;
|
||||
}
|
||||
|
||||
.title {
|
||||
margin-bottom: 20px;
|
||||
font-size: 24px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.input {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.signInBtn {
|
||||
width: 100%;
|
||||
margin: 20px 0;
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
.tool {
|
||||
display: flex;
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
|
||||
.button {
|
||||
margin-left: 10px;
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
import { Tabs } from 'antd';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { connect, Helmet } from 'umi';
|
||||
import ProjectListTree from './components/ProjectList';
|
||||
import EntitySection from './components/Entity/EntitySection';
|
||||
import styles from './components/style.less';
|
||||
import type { StateType } from './model';
|
||||
import { RightOutlined, LeftOutlined } from '@ant-design/icons';
|
||||
import SplitPane from 'react-split-pane';
|
||||
import Pane from 'react-split-pane/lib/Pane';
|
||||
import type { Dispatch } from 'umi';
|
||||
|
||||
const { TabPane } = Tabs;
|
||||
|
||||
type Props = {
|
||||
domainManger: StateType;
|
||||
dispatch: Dispatch;
|
||||
};
|
||||
|
||||
const DEFAULT_LEFT_SIZE = '300px';
|
||||
|
||||
const ChatSetting: React.FC<Props> = ({ domainManger, dispatch }) => {
|
||||
window.RUNNING_ENV = 'chat';
|
||||
const [collapsed, setCollapsed] = useState(false);
|
||||
const [leftSize, setLeftSize] = useState('');
|
||||
const { selectDomainId, selectDomainName } = domainManger;
|
||||
useEffect(() => {
|
||||
const semanticLeftCollapsed = localStorage.getItem('semanticLeftCollapsed');
|
||||
const semanticLeftSize =
|
||||
semanticLeftCollapsed === 'true' ? '0px' : localStorage.getItem('semanticLeftSize');
|
||||
setCollapsed(semanticLeftCollapsed === 'true');
|
||||
setLeftSize(semanticLeftSize || DEFAULT_LEFT_SIZE);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectDomainId) {
|
||||
dispatch({
|
||||
type: 'domainManger/queryDimensionList',
|
||||
payload: {
|
||||
domainId: selectDomainId,
|
||||
},
|
||||
});
|
||||
dispatch({
|
||||
type: 'domainManger/queryMetricList',
|
||||
payload: {
|
||||
domainId: selectDomainId,
|
||||
},
|
||||
});
|
||||
}
|
||||
}, [selectDomainId]);
|
||||
|
||||
const onCollapse = () => {
|
||||
const collapsedValue = !collapsed;
|
||||
setCollapsed(collapsedValue);
|
||||
localStorage.setItem('semanticLeftCollapsed', String(collapsedValue));
|
||||
const semanticLeftSize = collapsedValue ? '0px' : localStorage.getItem('semanticLeftSize');
|
||||
const sizeValue = parseInt(semanticLeftSize || '0');
|
||||
if (!collapsedValue && sizeValue <= 10) {
|
||||
setLeftSize(DEFAULT_LEFT_SIZE);
|
||||
localStorage.setItem('semanticLeftSize', DEFAULT_LEFT_SIZE);
|
||||
} else {
|
||||
setLeftSize(semanticLeftSize || DEFAULT_LEFT_SIZE);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const width = document.getElementById('tab');
|
||||
const switchWarpper: any = document.getElementById('switch');
|
||||
if (width && switchWarpper) {
|
||||
switchWarpper.style.width = width.offsetWidth * 0.77 + 'px';
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={styles.projectBody}>
|
||||
<Helmet title={'问答设置-超音数'} />
|
||||
<SplitPane
|
||||
split="vertical"
|
||||
onChange={(size) => {
|
||||
localStorage.setItem('semanticLeftSize', size[0]);
|
||||
setLeftSize(size[0]);
|
||||
}}
|
||||
>
|
||||
<Pane initialSize={leftSize || DEFAULT_LEFT_SIZE}>
|
||||
<div className={styles.menu}>
|
||||
<ProjectListTree createDomainBtnVisible={false} queryService="chat" />
|
||||
</div>
|
||||
</Pane>
|
||||
|
||||
<div className={styles.projectManger}>
|
||||
<div className={styles.collapseLeftBtn} onClick={onCollapse}>
|
||||
{collapsed ? <RightOutlined /> : <LeftOutlined />}
|
||||
</div>
|
||||
<h2 className={styles.title}>
|
||||
{selectDomainName ? `选择的主题域:${selectDomainName}` : '主题域信息'}
|
||||
</h2>
|
||||
{selectDomainId ? (
|
||||
<>
|
||||
<Tabs className={styles.tab} defaultActiveKey="chatSetting" destroyInactiveTabPane>
|
||||
<TabPane className={styles.tabPane} tab="问答设置" key="chatSetting">
|
||||
<EntitySection />
|
||||
</TabPane>
|
||||
</Tabs>
|
||||
</>
|
||||
) : (
|
||||
<h2 className={styles.mainTip}>请选择项目</h2>
|
||||
)}
|
||||
</div>
|
||||
</SplitPane>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default connect(({ domainManger }: { domainManger: StateType }) => ({
|
||||
domainManger,
|
||||
}))(ChatSetting);
|
||||
@@ -0,0 +1,38 @@
|
||||
import React from 'react';
|
||||
import { Form, Input, Spin } from 'antd';
|
||||
import type { FormInstance } from 'antd/lib/form';
|
||||
|
||||
const FormItem = Form.Item;
|
||||
const { TextArea } = Input;
|
||||
|
||||
type Props = {
|
||||
isEdit?: boolean;
|
||||
form: FormInstance<any>;
|
||||
tableLoading?: boolean;
|
||||
};
|
||||
|
||||
const DataSourceBasicForm: React.FC<Props> = ({ isEdit, tableLoading = false }) => {
|
||||
return (
|
||||
<Spin spinning={tableLoading}>
|
||||
<FormItem
|
||||
name="name"
|
||||
label="数据源中文名"
|
||||
rules={[{ required: true, message: '请输入数据源中文名' }]}
|
||||
>
|
||||
<Input placeholder="名称不可重复" />
|
||||
</FormItem>
|
||||
<FormItem
|
||||
name="bizName"
|
||||
label="数据源英文名"
|
||||
rules={[{ required: true, message: '请输入数据源英文名' }]}
|
||||
>
|
||||
<Input placeholder="名称不可重复" disabled={isEdit} />
|
||||
</FormItem>
|
||||
<FormItem name="description" label="数据源描述">
|
||||
<TextArea placeholder="请输入数据源描述" />
|
||||
</FormItem>
|
||||
</Spin>
|
||||
);
|
||||
};
|
||||
|
||||
export default DataSourceBasicForm;
|
||||
@@ -0,0 +1,291 @@
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { Form, Button, Modal, Steps, message } from 'antd';
|
||||
import BasicInfoForm from './DataSourceBasicForm';
|
||||
import FieldForm from './DataSourceFieldForm';
|
||||
import { formLayout } from '@/components/FormHelper/utils';
|
||||
import { EnumDataSourceType } from '../constants';
|
||||
import type { DataInstanceItem, FieldItem, SaveDataSetForm } from '../data';
|
||||
import styles from '../style.less';
|
||||
import { createDatasource, updateDatasource } from '../../service';
|
||||
|
||||
export type CreateFormProps = {
|
||||
createModalVisible: boolean;
|
||||
sql: string;
|
||||
domainId: number;
|
||||
dataSourceItem: DataInstanceItem | any;
|
||||
onCancel?: () => void;
|
||||
onSubmit?: (dataSourceInfo: any) => void;
|
||||
scriptColumns: any[];
|
||||
};
|
||||
const { Step } = Steps;
|
||||
|
||||
const initFormVal = {
|
||||
name: '', // 数据源名称
|
||||
bizName: '', // 数据源英文名
|
||||
description: '', // 数据源描述
|
||||
};
|
||||
|
||||
const DataSourceCreateForm: React.FC<CreateFormProps> = ({
|
||||
onCancel,
|
||||
createModalVisible,
|
||||
domainId,
|
||||
scriptColumns,
|
||||
sql,
|
||||
onSubmit,
|
||||
dataSourceItem,
|
||||
}) => {
|
||||
const isEdit = !!dataSourceItem?.id;
|
||||
const [fields, setFields] = useState<FieldItem[]>([]);
|
||||
const [currentStep, setCurrentStep] = useState(0);
|
||||
const [saveLoading, setSaveLoading] = useState(false);
|
||||
const formValRef = useRef(initFormVal as any);
|
||||
const [form] = Form.useForm();
|
||||
const updateFormVal = (val: SaveDataSetForm) => {
|
||||
formValRef.current = val;
|
||||
};
|
||||
|
||||
const forward = () => setCurrentStep(currentStep + 1);
|
||||
const backward = () => setCurrentStep(currentStep - 1);
|
||||
|
||||
const getFieldsClassify = (fieldsList: FieldItem[]) => {
|
||||
const classify = fieldsList.reduce(
|
||||
(fieldsClassify, item: FieldItem) => {
|
||||
const {
|
||||
type,
|
||||
bizName,
|
||||
timeGranularity,
|
||||
agg,
|
||||
isCreateDimension,
|
||||
name,
|
||||
isCreateMetric,
|
||||
dateFormat,
|
||||
} = item;
|
||||
switch (type) {
|
||||
case EnumDataSourceType.CATEGORICAL:
|
||||
fieldsClassify.dimensions.push({
|
||||
bizName,
|
||||
type,
|
||||
isCreateDimension,
|
||||
name,
|
||||
});
|
||||
break;
|
||||
case EnumDataSourceType.TIME:
|
||||
fieldsClassify.dimensions.push({
|
||||
bizName,
|
||||
type,
|
||||
isCreateDimension,
|
||||
name,
|
||||
dateFormat,
|
||||
typeParams: {
|
||||
isPrimary: true,
|
||||
timeGranularity,
|
||||
},
|
||||
});
|
||||
break;
|
||||
case EnumDataSourceType.FOREIGN:
|
||||
case EnumDataSourceType.PRIMARY:
|
||||
fieldsClassify.identifiers.push({
|
||||
bizName,
|
||||
name,
|
||||
type,
|
||||
});
|
||||
break;
|
||||
case EnumDataSourceType.MEASURES:
|
||||
fieldsClassify.measures.push({
|
||||
bizName,
|
||||
type,
|
||||
agg,
|
||||
name,
|
||||
isCreateMetric,
|
||||
});
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return fieldsClassify;
|
||||
},
|
||||
{
|
||||
identifiers: [],
|
||||
dimensions: [],
|
||||
measures: [],
|
||||
} as any,
|
||||
);
|
||||
return classify;
|
||||
};
|
||||
const handleNext = async () => {
|
||||
const fieldsValue = await form.validateFields();
|
||||
|
||||
const fieldsClassify = getFieldsClassify(fields);
|
||||
const submitForm = {
|
||||
...formValRef.current,
|
||||
...fieldsValue,
|
||||
...fieldsClassify,
|
||||
};
|
||||
updateFormVal(submitForm);
|
||||
if (currentStep < 1) {
|
||||
forward();
|
||||
} else {
|
||||
setSaveLoading(true);
|
||||
const queryParams = {
|
||||
...submitForm,
|
||||
sqlQuery: sql,
|
||||
databaseId: dataSourceItem.databaseId,
|
||||
queryType: 'sql_query',
|
||||
domainId,
|
||||
};
|
||||
const queryDatasource = isEdit ? updateDatasource : createDatasource;
|
||||
const { code, msg, data } = await queryDatasource(queryParams);
|
||||
setSaveLoading(false);
|
||||
if (code === 200) {
|
||||
message.success('保存数据源成功!');
|
||||
onSubmit?.({
|
||||
...queryParams,
|
||||
...data,
|
||||
resData: data,
|
||||
});
|
||||
return;
|
||||
}
|
||||
message.error(msg);
|
||||
}
|
||||
};
|
||||
|
||||
const initFields = (fieldsClassifyList: any[]) => {
|
||||
const columnFields: any[] = scriptColumns.map((item: any) => {
|
||||
const { type, nameEn } = item;
|
||||
const oldItem = fieldsClassifyList.find((oItem) => oItem.bizName === item.nameEn) || {};
|
||||
return {
|
||||
...oldItem,
|
||||
bizName: nameEn,
|
||||
// name,
|
||||
sqlType: type,
|
||||
};
|
||||
});
|
||||
setFields(columnFields || []);
|
||||
};
|
||||
|
||||
const formatterMeasures = (measuresList: any[] = []) => {
|
||||
return measuresList.map((measures: any) => {
|
||||
return {
|
||||
...measures,
|
||||
type: EnumDataSourceType.MEASURES,
|
||||
};
|
||||
});
|
||||
};
|
||||
const formatterDimensions = (dimensionsList: any[] = []) => {
|
||||
return dimensionsList.map((dimension: any) => {
|
||||
const { typeParams } = dimension;
|
||||
return {
|
||||
...dimension,
|
||||
timeGranularity: typeParams?.timeGranularity || '',
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
const initData = () => {
|
||||
const { id, name, bizName, description, datasourceDetail } = dataSourceItem as any;
|
||||
const initValue = {
|
||||
id,
|
||||
name,
|
||||
bizName,
|
||||
description,
|
||||
};
|
||||
const editInitFormVal = {
|
||||
...formValRef.current,
|
||||
...initValue,
|
||||
};
|
||||
updateFormVal(editInitFormVal);
|
||||
form.setFieldsValue(initValue);
|
||||
const { dimensions, identifiers, measures } = datasourceDetail;
|
||||
const formatFields = [
|
||||
...formatterDimensions(dimensions || []),
|
||||
...(identifiers || []),
|
||||
...formatterMeasures(measures || []),
|
||||
];
|
||||
initFields(formatFields);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (isEdit) {
|
||||
initData();
|
||||
} else {
|
||||
initFields([]);
|
||||
}
|
||||
}, [dataSourceItem]);
|
||||
|
||||
const handleFieldChange = (fieldName: string, data: any) => {
|
||||
const result = fields.map((field) => {
|
||||
if (field.bizName === fieldName) {
|
||||
return {
|
||||
...field,
|
||||
...data,
|
||||
};
|
||||
}
|
||||
return {
|
||||
...field,
|
||||
};
|
||||
});
|
||||
setFields(result);
|
||||
};
|
||||
|
||||
const renderContent = () => {
|
||||
if (currentStep === 1) {
|
||||
return <FieldForm fields={fields} onFieldChange={handleFieldChange} />;
|
||||
}
|
||||
return <BasicInfoForm form={form} isEdit={isEdit} />;
|
||||
};
|
||||
|
||||
const renderFooter = () => {
|
||||
if (currentStep === 1) {
|
||||
return (
|
||||
<>
|
||||
<Button style={{ float: 'left' }} onClick={backward}>
|
||||
上一步
|
||||
</Button>
|
||||
<Button onClick={onCancel}>取消</Button>
|
||||
<Button type="primary" loading={saveLoading} onClick={handleNext}>
|
||||
完成
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<Button onClick={onCancel}>取消</Button>
|
||||
<Button type="primary" onClick={handleNext}>
|
||||
下一步
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
forceRender
|
||||
width={1300}
|
||||
bodyStyle={{ padding: '32px 40px 48px' }}
|
||||
destroyOnClose
|
||||
title={`${isEdit ? '编辑' : '新建'}数据源`}
|
||||
maskClosable={false}
|
||||
open={createModalVisible}
|
||||
footer={renderFooter()}
|
||||
onCancel={onCancel}
|
||||
>
|
||||
<Steps style={{ marginBottom: 28 }} size="small" current={currentStep}>
|
||||
<Step title="基本信息" />
|
||||
<Step title="字段信息" />
|
||||
</Steps>
|
||||
<Form
|
||||
{...formLayout}
|
||||
form={form}
|
||||
initialValues={{
|
||||
...formValRef.current,
|
||||
}}
|
||||
className={styles.form}
|
||||
>
|
||||
{renderContent()}
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default DataSourceCreateForm;
|
||||
@@ -0,0 +1,196 @@
|
||||
import React from 'react';
|
||||
import { Table, Select, Checkbox, Input } from 'antd';
|
||||
import type { FieldItem } from '../data';
|
||||
import { isUndefined } from 'lodash';
|
||||
import { TYPE_OPTIONS, DATE_FORMATTER, AGG_OPTIONS, EnumDataSourceType } from '../constants';
|
||||
|
||||
type Props = {
|
||||
fields: FieldItem[];
|
||||
onFieldChange: (fieldName: string, data: Partial<FieldItem>) => void;
|
||||
};
|
||||
|
||||
const { Option } = Select;
|
||||
|
||||
const FieldForm: React.FC<Props> = ({ fields, onFieldChange }) => {
|
||||
const handleFieldChange = (record: FieldItem, fieldName: string, value: any) => {
|
||||
onFieldChange(record.bizName, {
|
||||
...record,
|
||||
[fieldName]: value,
|
||||
});
|
||||
};
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: '字段名称',
|
||||
dataIndex: 'bizName',
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
title: '数据类型',
|
||||
dataIndex: 'sqlType',
|
||||
width: 80,
|
||||
},
|
||||
{
|
||||
title: '字段类型',
|
||||
dataIndex: 'type',
|
||||
width: 100,
|
||||
render: (_: any, record: FieldItem) => {
|
||||
const type = fields.find((field) => field.bizName === record.bizName)?.type;
|
||||
return (
|
||||
<Select
|
||||
placeholder="字段类型"
|
||||
value={type}
|
||||
onChange={(value) => {
|
||||
let defaultParams = {};
|
||||
if (value === EnumDataSourceType.MEASURES) {
|
||||
defaultParams = {
|
||||
agg: AGG_OPTIONS[0].value,
|
||||
};
|
||||
} else if (value === EnumDataSourceType.TIME) {
|
||||
defaultParams = {
|
||||
dateFormat: DATE_FORMATTER[0],
|
||||
timeGranularity: 'day',
|
||||
};
|
||||
} else {
|
||||
defaultParams = {
|
||||
agg: undefined,
|
||||
dateFormat: undefined,
|
||||
timeGranularity: undefined,
|
||||
};
|
||||
}
|
||||
// handleFieldChange(record, 'type', value);
|
||||
onFieldChange(record.bizName, {
|
||||
...record,
|
||||
type: value,
|
||||
...defaultParams,
|
||||
});
|
||||
}}
|
||||
style={{ width: '100%' }}
|
||||
>
|
||||
{TYPE_OPTIONS.map((item) => (
|
||||
<Option key={item.label} value={item.value}>
|
||||
{item.label}
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '扩展配置',
|
||||
dataIndex: 'extender',
|
||||
width: 100,
|
||||
render: (_: any, record: FieldItem) => {
|
||||
const { type } = record;
|
||||
if (type === EnumDataSourceType.MEASURES) {
|
||||
const agg = fields.find((field) => field.bizName === record.bizName)?.agg;
|
||||
return (
|
||||
<Select
|
||||
placeholder="度量算子"
|
||||
value={agg}
|
||||
onChange={(value) => {
|
||||
handleFieldChange(record, 'agg', value);
|
||||
}}
|
||||
defaultValue={AGG_OPTIONS[0].value}
|
||||
style={{ width: '100%' }}
|
||||
>
|
||||
{AGG_OPTIONS.map((item) => (
|
||||
<Option key={item.value} value={item.value}>
|
||||
{item.label}
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
);
|
||||
}
|
||||
if (type === EnumDataSourceType.TIME) {
|
||||
const dateFormat = fields.find((field) => field.bizName === record.bizName)?.dateFormat;
|
||||
return (
|
||||
<Select
|
||||
placeholder="时间格式"
|
||||
value={dateFormat}
|
||||
onChange={(value) => {
|
||||
handleFieldChange(record, 'dateFormat', value);
|
||||
}}
|
||||
defaultValue={DATE_FORMATTER[0]}
|
||||
style={{ width: '100%' }}
|
||||
>
|
||||
{DATE_FORMATTER.map((item) => (
|
||||
<Option key={item} value={item}>
|
||||
{item}
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
);
|
||||
}
|
||||
return <></>;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '快速创建',
|
||||
dataIndex: 'fastCreate',
|
||||
width: 100,
|
||||
render: (_: any, record: FieldItem) => {
|
||||
const { type, name } = record;
|
||||
if (
|
||||
[
|
||||
EnumDataSourceType.PRIMARY,
|
||||
EnumDataSourceType.FOREIGN,
|
||||
EnumDataSourceType.CATEGORICAL,
|
||||
EnumDataSourceType.TIME,
|
||||
EnumDataSourceType.MEASURES,
|
||||
].includes(type as EnumDataSourceType)
|
||||
) {
|
||||
const isCreateName = [EnumDataSourceType.CATEGORICAL, EnumDataSourceType.TIME].includes(
|
||||
type as EnumDataSourceType,
|
||||
)
|
||||
? 'isCreateDimension'
|
||||
: 'isCreateMetric';
|
||||
const editState = !isUndefined(record[isCreateName]) ? !!record[isCreateName] : true;
|
||||
return (
|
||||
<Checkbox
|
||||
checked={editState}
|
||||
onChange={(e) => {
|
||||
const value = e.target.checked ? 1 : 0;
|
||||
if (!value) {
|
||||
onFieldChange(record.bizName, {
|
||||
...record,
|
||||
name: '',
|
||||
[isCreateName]: value,
|
||||
});
|
||||
} else {
|
||||
handleFieldChange(record, isCreateName, value);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Input
|
||||
value={name}
|
||||
disabled={!editState}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
handleFieldChange(record, 'name', value);
|
||||
}}
|
||||
placeholder="请输入中文名"
|
||||
/>
|
||||
</Checkbox>
|
||||
);
|
||||
}
|
||||
return <></>;
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<Table<FieldItem>
|
||||
dataSource={fields}
|
||||
columns={columns}
|
||||
className="fields-table"
|
||||
rowKey="bizName"
|
||||
pagination={false}
|
||||
scroll={{ y: 500 }}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default FieldForm;
|
||||
@@ -0,0 +1,530 @@
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import { Button, Table, message, Tooltip, Space, Dropdown } from 'antd';
|
||||
import SplitPane from 'react-split-pane';
|
||||
import Pane from 'react-split-pane/lib/Pane';
|
||||
import sqlFormatter from 'sql-formatter';
|
||||
import {
|
||||
FullscreenOutlined,
|
||||
WarningOutlined,
|
||||
EditOutlined,
|
||||
PlayCircleTwoTone,
|
||||
SwapOutlined,
|
||||
PlayCircleOutlined,
|
||||
CloudServerOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { isFunction } from 'lodash';
|
||||
import FullScreen from '@/components/FullScreen';
|
||||
import SqlEditor from '@/components/SqlEditor';
|
||||
import type { TaskResultParams, TaskResultItem, DataInstanceItem, TaskResultColumn } from '../data';
|
||||
import { excuteSql } from '../service';
|
||||
import { getDatabaseByDomainId } from '../../service';
|
||||
import DataSourceCreateForm from './DataSourceCreateForm';
|
||||
import styles from '../style.less';
|
||||
|
||||
import 'ace-builds/src-min-noconflict/ext-searchbox';
|
||||
import 'ace-builds/src-min-noconflict/theme-sqlserver';
|
||||
import 'ace-builds/src-min-noconflict/theme-monokai';
|
||||
import 'ace-builds/src-min-noconflict/mode-sql';
|
||||
|
||||
type IProps = {
|
||||
oprType: 'add' | 'edit';
|
||||
dataSourceItem: DataInstanceItem;
|
||||
domainId: number;
|
||||
onUpdateSql?: (sql: string) => void;
|
||||
sql?: string;
|
||||
onSubmitSuccess?: (dataSourceInfo: any) => void;
|
||||
onJdbcSourceChange?: (jdbcId: number) => void;
|
||||
};
|
||||
|
||||
type ResultTableItem = Record<string, any>;
|
||||
|
||||
type ResultColItem = {
|
||||
key: string;
|
||||
title: string;
|
||||
dataIndex: string;
|
||||
};
|
||||
|
||||
type ScreenSize = 'small' | 'middle' | 'large';
|
||||
|
||||
type JdbcSourceItems = {
|
||||
label: string;
|
||||
key: number;
|
||||
};
|
||||
|
||||
const SqlDetail: React.FC<IProps> = ({
|
||||
dataSourceItem,
|
||||
onSubmitSuccess,
|
||||
domainId,
|
||||
sql = '',
|
||||
onUpdateSql,
|
||||
onJdbcSourceChange,
|
||||
}) => {
|
||||
const [resultTable, setResultTable] = useState<ResultTableItem[]>([]);
|
||||
const [resultTableLoading, setResultTableLoading] = useState(false);
|
||||
const [resultCols, setResultCols] = useState<ResultColItem[]>([]);
|
||||
const [pagination, setPagination] = useState({
|
||||
current: 1,
|
||||
pageSize: 20,
|
||||
total: 0,
|
||||
});
|
||||
const [jdbcSourceItems, setJdbcSourceItems] = useState<JdbcSourceItems[]>([]);
|
||||
const [dataSourceModalVisible, setDataSourceModalVisible] = useState(false);
|
||||
|
||||
const [tableScroll, setTableScroll] = useState({
|
||||
scrollToFirstRowOnChange: true,
|
||||
x: '100%',
|
||||
y: 200,
|
||||
});
|
||||
|
||||
// const [dataSourceResult, setDataSourceResult] = useState<any>({});
|
||||
|
||||
const [runState, setRunState] = useState<boolean | undefined>();
|
||||
|
||||
const [taskLog, setTaskLog] = useState('');
|
||||
const [isSqlExcLocked, setIsSqlExcLocked] = useState(false);
|
||||
const [screenSize, setScreenSize] = useState<ScreenSize>('middle');
|
||||
|
||||
const [isSqlIdeFullScreen, setIsSqlIdeFullScreen] = useState<boolean>(false);
|
||||
const [isSqlResFullScreen, setIsSqlResFullScreen] = useState<boolean>(false);
|
||||
|
||||
// const [sqlParams, setSqlParams] = useState<SqlParamsItem[]>([]);
|
||||
const resultInnerWrap = useRef<HTMLDivElement>();
|
||||
|
||||
const [editorSize, setEditorSize] = useState<number>(0);
|
||||
const DEFAULT_FULLSCREEN_TOP = 0;
|
||||
|
||||
const [partialSql, setPartialSql] = useState('');
|
||||
const [isPartial, setIsPartial] = useState(false);
|
||||
const [isRight, setIsRight] = useState(false);
|
||||
|
||||
const [scriptColumns, setScriptColumns] = useState<any[]>([]);
|
||||
// const [jdbcSourceName, setJdbcSourceName] = useState<string>(() => {
|
||||
// const sourceId = dataSourceItem.databaseId;
|
||||
// if (sourceId) {
|
||||
// const target: any = jdbcSourceItems.filter((item: any) => {
|
||||
// return item.key === Number(sourceId);
|
||||
// })[0];
|
||||
// if (target) {
|
||||
// return target.label;
|
||||
// }
|
||||
// }
|
||||
// return 'ClickHouse';
|
||||
// });
|
||||
|
||||
const queryDatabaseConfig = async () => {
|
||||
const { code, data } = await getDatabaseByDomainId(domainId);
|
||||
if (code === 200) {
|
||||
setJdbcSourceItems([
|
||||
{
|
||||
label: data?.name,
|
||||
key: data?.id,
|
||||
},
|
||||
]);
|
||||
onJdbcSourceChange?.(data?.id && Number(data?.id));
|
||||
return;
|
||||
}
|
||||
message.error('数据库配置获取错误');
|
||||
};
|
||||
|
||||
function creatCalcItem(key: string, data: string) {
|
||||
const line = document.createElement('div'); // 需要每条数据一行,这样避免数据换行的时候获得的宽度不准确
|
||||
const child = document.createElement('span');
|
||||
child.classList.add(`resultCalcItem_${key}`);
|
||||
child.innerText = data;
|
||||
line.appendChild(child);
|
||||
return line;
|
||||
}
|
||||
|
||||
// 计算每列的宽度,通过容器插入文档中动态得到该列数据(包括表头)的最长宽度,设为列宽度,保证每列的数据都能一行展示完
|
||||
function getKeyWidthMap(list: TaskResultItem[]): TaskResultItem {
|
||||
const widthMap = {};
|
||||
const container = document.createElement('div');
|
||||
container.id = 'resultCalcWrap';
|
||||
container.style.position = 'fixed';
|
||||
container.style.left = '-99999px';
|
||||
container.style.top = '-99999px';
|
||||
container.style.width = '19999px';
|
||||
container.style.fontSize = '12px';
|
||||
list.forEach((item, index) => {
|
||||
if (index === 0) {
|
||||
Object.keys(item).forEach((key, keyIndex) => {
|
||||
// 因为key可能存在一些特殊字符,导致querySelectorAll获取的时候报错,所以用keyIndex(而不用key)拼接className
|
||||
container.appendChild(creatCalcItem(`${keyIndex}`, key));
|
||||
container.appendChild(creatCalcItem(`${keyIndex}`, `${item[key]}`));
|
||||
});
|
||||
} else {
|
||||
Object.keys(item).forEach((key, keyIndex) => {
|
||||
container.appendChild(creatCalcItem(`${keyIndex}`, `${item[key]}`));
|
||||
});
|
||||
}
|
||||
});
|
||||
document.body.appendChild(container);
|
||||
Object.keys(list[0]).forEach((key, keyIndex) => {
|
||||
// 因为key可能存在一些特殊字符,导致querySelectorAll获取的时候报错,所以用keyIndex(而不用key)拼接className
|
||||
const widthArr = Array.from(container.querySelectorAll(`.resultCalcItem_${keyIndex}`)).map(
|
||||
(node: any) => node.offsetWidth,
|
||||
);
|
||||
widthMap[key] = Math.max(...widthArr);
|
||||
});
|
||||
document.body.removeChild(container);
|
||||
return widthMap;
|
||||
}
|
||||
|
||||
const updateResultCols = (list: TaskResultItem[], columns: TaskResultColumn[]) => {
|
||||
if (list.length) {
|
||||
const widthMap = getKeyWidthMap(list);
|
||||
const cols = columns.map(({ nameEn }) => {
|
||||
return {
|
||||
key: nameEn,
|
||||
title: nameEn,
|
||||
dataIndex: nameEn,
|
||||
width: `${(widthMap[nameEn] as number) + 22}px`, // 字宽度 + 20px(比左右padding宽几像素,作为一个buffer值)
|
||||
};
|
||||
});
|
||||
setResultCols(cols);
|
||||
}
|
||||
};
|
||||
|
||||
const fetchTaskResult = (params: TaskResultParams) => {
|
||||
setResultTable(
|
||||
params.resultList.map((item, index) => {
|
||||
return {
|
||||
...item,
|
||||
index,
|
||||
};
|
||||
}),
|
||||
);
|
||||
setPagination({
|
||||
current: 1,
|
||||
pageSize: 20,
|
||||
total: params.resultList.length,
|
||||
});
|
||||
setScriptColumns(params.columns);
|
||||
updateResultCols(params.resultList, params.columns);
|
||||
};
|
||||
|
||||
const changePaging = (paging: Pagination) => {
|
||||
setPagination({
|
||||
...pagination,
|
||||
...paging,
|
||||
});
|
||||
};
|
||||
|
||||
const onSqlChange = (sqlString: string) => {
|
||||
if (onUpdateSql && isFunction(onUpdateSql)) {
|
||||
onUpdateSql(sqlString);
|
||||
}
|
||||
};
|
||||
|
||||
const formatSQL = () => {
|
||||
const sqlvalue = sqlFormatter.format(sql);
|
||||
if (onUpdateSql && isFunction(onUpdateSql)) {
|
||||
onUpdateSql(sqlvalue);
|
||||
}
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
sql = sqlvalue;
|
||||
};
|
||||
|
||||
const separateSql = async (value: string) => {
|
||||
setResultTableLoading(true);
|
||||
const { code, data, msg } = await excuteSql({
|
||||
sql: value,
|
||||
domainId,
|
||||
});
|
||||
setResultTableLoading(false);
|
||||
if (code === 200) {
|
||||
// setDataSourceResult(data);
|
||||
fetchTaskResult(data);
|
||||
setRunState(true);
|
||||
} else {
|
||||
setRunState(false);
|
||||
setTaskLog(msg);
|
||||
}
|
||||
};
|
||||
|
||||
const onSelect = (value: string) => {
|
||||
if (value) {
|
||||
setIsPartial(true);
|
||||
setPartialSql(value);
|
||||
} else {
|
||||
setIsPartial(false);
|
||||
}
|
||||
};
|
||||
|
||||
const excuteScript = () => {
|
||||
if (!sql) {
|
||||
return message.error('SQL查询语句不可以为空!');
|
||||
}
|
||||
if (isSqlExcLocked) {
|
||||
return message.warning('请间隔5s再重新执行!');
|
||||
}
|
||||
const waitTime = 5000;
|
||||
setIsSqlExcLocked(true); // 加锁,5s后再解锁
|
||||
setTimeout(() => {
|
||||
setIsSqlExcLocked(false);
|
||||
}, waitTime);
|
||||
|
||||
return isPartial ? separateSql(partialSql) : separateSql(sql);
|
||||
};
|
||||
|
||||
const showDataSetModal = () => {
|
||||
setDataSourceModalVisible(true);
|
||||
};
|
||||
|
||||
const startCreatDataSource = async () => {
|
||||
showDataSetModal();
|
||||
};
|
||||
|
||||
const updateNormalResScroll = () => {
|
||||
const node = resultInnerWrap?.current;
|
||||
if (node) {
|
||||
setTableScroll({
|
||||
scrollToFirstRowOnChange: true,
|
||||
x: '100%',
|
||||
y: node.clientHeight - 120,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const updateFullScreenResScroll = () => {
|
||||
const windowHeight = window.innerHeight;
|
||||
const paginationHeight = 96;
|
||||
setTableScroll({
|
||||
scrollToFirstRowOnChange: true,
|
||||
x: '100%',
|
||||
y: windowHeight - DEFAULT_FULLSCREEN_TOP - paginationHeight - 30, // 30为退出全屏按钮的高度
|
||||
});
|
||||
};
|
||||
|
||||
const handleFullScreenSqlIde = () => {
|
||||
setIsSqlIdeFullScreen(true);
|
||||
};
|
||||
|
||||
const handleNormalScreenSqlIde = () => {
|
||||
setIsSqlIdeFullScreen(false);
|
||||
};
|
||||
|
||||
const handleFullScreenSqlResult = () => {
|
||||
setIsSqlResFullScreen(true);
|
||||
};
|
||||
|
||||
const handleNormalScreenSqlResult = () => {
|
||||
setIsSqlResFullScreen(false);
|
||||
};
|
||||
|
||||
const handleThemeChange = () => {
|
||||
setIsRight(!isRight);
|
||||
};
|
||||
|
||||
const renderResult = () => {
|
||||
if (runState === false) {
|
||||
return (
|
||||
<>
|
||||
{
|
||||
<div className={styles.taskFailed}>
|
||||
<WarningOutlined className={styles.resultFailIcon} />
|
||||
任务执行失败
|
||||
</div>
|
||||
}
|
||||
<div
|
||||
className={styles.sqlResultLog}
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: taskLog.replace(/\r\n/g, '<br/>').replace(/\t/g, ' '),
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if (runState) {
|
||||
return (
|
||||
<>
|
||||
<div className={styles.detail} />
|
||||
<Table<TaskResultItem>
|
||||
loading={resultTableLoading}
|
||||
dataSource={resultTable}
|
||||
columns={resultCols}
|
||||
onChange={changePaging}
|
||||
pagination={pagination}
|
||||
scroll={tableScroll}
|
||||
className={styles.resultTable}
|
||||
rowClassName="resultTableRow"
|
||||
rowKey="index"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
return <div className={styles.sqlResultContent}>请点击左侧任务列表查看执行详情</div>;
|
||||
};
|
||||
|
||||
// 更新任务结果列表的高度,使其撑满容器
|
||||
useEffect(() => {
|
||||
if (isSqlResFullScreen) {
|
||||
updateFullScreenResScroll();
|
||||
} else {
|
||||
updateNormalResScroll();
|
||||
}
|
||||
}, [resultTable, isSqlResFullScreen]);
|
||||
|
||||
useEffect(() => {
|
||||
queryDatabaseConfig();
|
||||
const windowHeight = window.innerHeight;
|
||||
let size: ScreenSize = 'small';
|
||||
if (windowHeight > 1100) {
|
||||
size = 'large';
|
||||
} else if (windowHeight > 850) {
|
||||
size = 'middle';
|
||||
}
|
||||
setScreenSize(size);
|
||||
}, []);
|
||||
|
||||
const exploreEditorSize = localStorage.getItem('exploreEditorSize');
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={styles.sqlOprBar}>
|
||||
<div className={styles.sqlOprBarLeftBox}>
|
||||
<Tooltip title="数据类型">
|
||||
<Dropdown
|
||||
menu={{
|
||||
items: jdbcSourceItems,
|
||||
onClick: (e) => {
|
||||
const value = e.key;
|
||||
const target: any = jdbcSourceItems.filter((item: any) => {
|
||||
return item.key === Number(value);
|
||||
})[0];
|
||||
if (target) {
|
||||
// setJdbcSourceName(target.label);
|
||||
onJdbcSourceChange?.(Number(value));
|
||||
}
|
||||
},
|
||||
}}
|
||||
placement="bottom"
|
||||
>
|
||||
<Button style={{ marginRight: '15px', minWidth: '140px' }}>
|
||||
<Space>
|
||||
<CloudServerOutlined className={styles.sqlOprIcon} style={{ marginRight: 0 }} />
|
||||
<span>{jdbcSourceItems[0]?.label}</span>
|
||||
</Space>
|
||||
</Button>
|
||||
</Dropdown>
|
||||
</Tooltip>
|
||||
<Tooltip title="全屏">
|
||||
<FullscreenOutlined className={styles.sqlOprIcon} onClick={handleFullScreenSqlIde} />
|
||||
</Tooltip>
|
||||
<Tooltip title="格式化SQL语句">
|
||||
<EditOutlined className={styles.sqlOprIcon} onClick={formatSQL} />
|
||||
</Tooltip>
|
||||
<Tooltip title="改变主题">
|
||||
<SwapOutlined className={styles.sqlOprIcon} onClick={handleThemeChange} />
|
||||
</Tooltip>
|
||||
<Tooltip title="执行脚本">
|
||||
<Button
|
||||
style={{
|
||||
lineHeight: '24px',
|
||||
top: '3px',
|
||||
position: 'relative',
|
||||
}}
|
||||
type="primary"
|
||||
shape="round"
|
||||
icon={
|
||||
isPartial ? '' : isSqlExcLocked ? <PlayCircleOutlined /> : <PlayCircleTwoTone />
|
||||
}
|
||||
size={'small'}
|
||||
className={
|
||||
isSqlExcLocked ? `${styles.disableIcon} ${styles.sqlOprIcon}` : styles.sqlOprBtn
|
||||
}
|
||||
onClick={excuteScript}
|
||||
>
|
||||
{isPartial ? '部分运行' : '运行'}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
<SplitPane
|
||||
split="horizontal"
|
||||
onChange={(size) => {
|
||||
setEditorSize(size);
|
||||
localStorage.setItem('exploreEditorSize', size[0]);
|
||||
}}
|
||||
>
|
||||
<Pane initialSize={exploreEditorSize || '500px'}>
|
||||
<div className={styles.sqlMain}>
|
||||
<div className={styles.sqlEditorWrapper}>
|
||||
<FullScreen
|
||||
isFullScreen={isSqlIdeFullScreen}
|
||||
top={`${DEFAULT_FULLSCREEN_TOP}px`}
|
||||
triggerBackToNormal={handleNormalScreenSqlIde}
|
||||
>
|
||||
<SqlEditor
|
||||
value={sql}
|
||||
// height={sqlEditorHeight}
|
||||
// theme="monokai"
|
||||
isRightTheme={isRight}
|
||||
sizeChanged={editorSize}
|
||||
onSqlChange={onSqlChange}
|
||||
onSelect={onSelect}
|
||||
/>
|
||||
</FullScreen>
|
||||
</div>
|
||||
</div>
|
||||
</Pane>
|
||||
<div className={`${styles.sqlBottmWrap} ${screenSize}`}>
|
||||
<div className={styles.sqlResultWrap}>
|
||||
<div className={styles.sqlToolBar}>
|
||||
{
|
||||
<Button
|
||||
className={styles.sqlToolBtn}
|
||||
type="primary"
|
||||
onClick={startCreatDataSource}
|
||||
disabled={!runState}
|
||||
>
|
||||
生成数据源
|
||||
</Button>
|
||||
}
|
||||
<Button
|
||||
className={styles.sqlToolBtn}
|
||||
type="primary"
|
||||
onClick={handleFullScreenSqlResult}
|
||||
disabled={!runState}
|
||||
>
|
||||
全屏查看
|
||||
</Button>
|
||||
</div>
|
||||
<div
|
||||
className={styles.sqlResultPane}
|
||||
ref={resultInnerWrap as React.MutableRefObject<HTMLDivElement | null>}
|
||||
>
|
||||
<FullScreen
|
||||
isFullScreen={isSqlResFullScreen}
|
||||
top={`${DEFAULT_FULLSCREEN_TOP}px`}
|
||||
triggerBackToNormal={handleNormalScreenSqlResult}
|
||||
>
|
||||
{renderResult()}
|
||||
</FullScreen>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</SplitPane>
|
||||
|
||||
{dataSourceModalVisible && (
|
||||
<DataSourceCreateForm
|
||||
sql={sql}
|
||||
domainId={domainId}
|
||||
dataSourceItem={dataSourceItem}
|
||||
scriptColumns={scriptColumns}
|
||||
onCancel={() => {
|
||||
setDataSourceModalVisible(false);
|
||||
}}
|
||||
onSubmit={(dataSourceInfo: any) => {
|
||||
setDataSourceModalVisible(false);
|
||||
onSubmitSuccess?.(dataSourceInfo);
|
||||
}}
|
||||
createModalVisible={dataSourceModalVisible}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default SqlDetail;
|
||||
@@ -0,0 +1,126 @@
|
||||
import React, { useState, useRef, useEffect } from 'react';
|
||||
import { Tabs } from 'antd';
|
||||
import SqlDetail from './SqlDetail';
|
||||
import type { SqlItem } from '../data';
|
||||
|
||||
import styles from '../style.less';
|
||||
|
||||
type Panes = {
|
||||
title: string;
|
||||
key: string;
|
||||
type: 'add' | 'edit';
|
||||
scriptId?: number;
|
||||
sql?: string;
|
||||
sqlInfo?: SqlItem;
|
||||
isSave?: boolean; // 暂存提示保存
|
||||
};
|
||||
|
||||
type TableRef = {
|
||||
current?: {
|
||||
fetchSqlList: () => void;
|
||||
upDateActiveItem: (key: any) => void;
|
||||
};
|
||||
};
|
||||
|
||||
type Props = {
|
||||
initialValues: any;
|
||||
domainId: number;
|
||||
onSubmitSuccess?: (dataSourceInfo: any) => void;
|
||||
};
|
||||
|
||||
const { TabPane } = Tabs;
|
||||
const LIST_KEY = 'list';
|
||||
|
||||
const SqlSide: React.FC<Props> = ({ initialValues, domainId, onSubmitSuccess }) => {
|
||||
const defaultPanes: Panes[] = [
|
||||
{
|
||||
key: '数据源查询',
|
||||
title: initialValues?.name || '数据源查询',
|
||||
type: 'add',
|
||||
isSave: true,
|
||||
},
|
||||
];
|
||||
|
||||
const [activeKey, setActiveKey] = useState('数据源查询');
|
||||
const [panes, setPanes] = useState<Panes[]>(defaultPanes);
|
||||
const tableRef: TableRef = useRef();
|
||||
const panesRef = useRef<Panes[]>(defaultPanes);
|
||||
|
||||
const [dataSourceItem, setDataSourceItem] = useState<any>(initialValues || {});
|
||||
|
||||
const updatePane = (list: Panes[]) => {
|
||||
setPanes(list);
|
||||
panesRef.current = list;
|
||||
};
|
||||
|
||||
// 更新脚本内容
|
||||
const updateTabSql = (sql: string, targetKey: string) => {
|
||||
const newPanes = panesRef.current.slice();
|
||||
const index = newPanes.findIndex((item) => item.key === targetKey);
|
||||
const targetItem = newPanes[index];
|
||||
newPanes.splice(index, 1, {
|
||||
...targetItem,
|
||||
sql,
|
||||
isSave: false,
|
||||
});
|
||||
updatePane(newPanes);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (initialValues) {
|
||||
updateTabSql(initialValues?.datasourceDetail?.sqlQuery || '', '数据源查询');
|
||||
}
|
||||
}, [initialValues]);
|
||||
|
||||
const onChange = (key: string) => {
|
||||
setActiveKey(key);
|
||||
tableRef?.current?.upDateActiveItem(key);
|
||||
if (key === LIST_KEY) {
|
||||
tableRef?.current?.fetchSqlList();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={styles.outside}>
|
||||
<Tabs
|
||||
type="editable-card"
|
||||
hideAdd={true}
|
||||
activeKey={activeKey}
|
||||
onChange={onChange}
|
||||
className={styles.middleArea}
|
||||
>
|
||||
{panes.map((pane) => {
|
||||
return (
|
||||
<TabPane
|
||||
tab={<div className={styles.paneName}>{pane.title}</div>}
|
||||
closable={false}
|
||||
key={pane.key}
|
||||
>
|
||||
<SqlDetail
|
||||
onSubmitSuccess={onSubmitSuccess}
|
||||
dataSourceItem={dataSourceItem}
|
||||
oprType={pane.type}
|
||||
domainId={domainId}
|
||||
onUpdateSql={(sql: string) => {
|
||||
updateTabSql(sql, pane.key);
|
||||
}}
|
||||
onJdbcSourceChange={(databaseId) => {
|
||||
setDataSourceItem({
|
||||
...dataSourceItem,
|
||||
databaseId,
|
||||
});
|
||||
}}
|
||||
sql={pane.sql}
|
||||
/>
|
||||
</TabPane>
|
||||
);
|
||||
})}
|
||||
</Tabs>
|
||||
</div>
|
||||
{/* </SplitPane> */}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default SqlSide;
|
||||
@@ -0,0 +1,67 @@
|
||||
export const EDITOR_HEIGHT_MAP = new Map([
|
||||
['small', '250px'],
|
||||
['middle', '300px'],
|
||||
['large', '400px'],
|
||||
]);
|
||||
|
||||
export enum EnumDataSourceType {
|
||||
CATEGORICAL = 'categorical',
|
||||
TIME = 'time',
|
||||
MEASURES = 'measures',
|
||||
PRIMARY = 'primary',
|
||||
FOREIGN = 'foreign',
|
||||
}
|
||||
|
||||
export const TYPE_OPTIONS = [
|
||||
{
|
||||
label: '维度',
|
||||
value: EnumDataSourceType.CATEGORICAL,
|
||||
},
|
||||
{
|
||||
label: '日期',
|
||||
value: EnumDataSourceType.TIME,
|
||||
},
|
||||
{
|
||||
label: '度量',
|
||||
value: EnumDataSourceType.MEASURES,
|
||||
},
|
||||
{
|
||||
label: '主键',
|
||||
value: EnumDataSourceType.PRIMARY,
|
||||
},
|
||||
{
|
||||
label: '外键',
|
||||
value: EnumDataSourceType.FOREIGN,
|
||||
},
|
||||
];
|
||||
|
||||
export const AGG_OPTIONS = [
|
||||
{
|
||||
label: 'sum',
|
||||
value: 'sum',
|
||||
},
|
||||
{
|
||||
label: 'max',
|
||||
value: 'max',
|
||||
},
|
||||
{
|
||||
label: 'min',
|
||||
value: 'min',
|
||||
},
|
||||
{
|
||||
label: 'avg',
|
||||
value: 'avg',
|
||||
},
|
||||
{
|
||||
label: 'count',
|
||||
value: 'count',
|
||||
},
|
||||
{
|
||||
label: 'count_distinct',
|
||||
value: 'count_distinct',
|
||||
},
|
||||
];
|
||||
|
||||
export const DATE_OPTIONS = ['day', 'week', 'month'];
|
||||
|
||||
export const DATE_FORMATTER = ['YYYY-MM-DD', 'YYYYMMDD', 'YYYY-MM', 'YYYYMM'];
|
||||
@@ -0,0 +1,68 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import SplitPane from 'react-split-pane';
|
||||
import SqlSide from './components/SqlSide';
|
||||
import Pane from 'react-split-pane/lib/Pane';
|
||||
import styles from './style.less';
|
||||
import { RightOutlined, LeftOutlined } from '@ant-design/icons';
|
||||
|
||||
type Props = {
|
||||
initialValues: any;
|
||||
domainId: number;
|
||||
onSubmitSuccess?: (dataSourceInfo: any) => void;
|
||||
};
|
||||
|
||||
const DEFAULT_RIGHT_SIZE = '300px';
|
||||
|
||||
const DataExploreView: React.FC<Props> = ({ initialValues, domainId, onSubmitSuccess }) => {
|
||||
const [collapsed, setCollapsed] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const exploreRightCollapsed = localStorage.getItem('exploreRightCollapsed');
|
||||
setCollapsed(exploreRightCollapsed === 'true');
|
||||
}, []);
|
||||
|
||||
const onCollapse = () => {
|
||||
const collapsedValue = !collapsed;
|
||||
setCollapsed(collapsedValue);
|
||||
localStorage.setItem('exploreRightCollapsed', String(collapsedValue));
|
||||
const exploreRightSize = collapsedValue ? '0px' : localStorage.getItem('exploreRightSize');
|
||||
const sizeValue = parseInt(exploreRightSize || '0');
|
||||
if (!collapsedValue && sizeValue <= 10) {
|
||||
localStorage.setItem('exploreRightSize', DEFAULT_RIGHT_SIZE);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`${styles.pageContainer} ${
|
||||
window.location.hash.includes('external') ? styles.externalPageContainer : ''
|
||||
}`}
|
||||
>
|
||||
<div className={styles.main}>
|
||||
<SplitPane
|
||||
split="vertical"
|
||||
onChange={(size) => {
|
||||
localStorage.setItem('exploreRightSize', size[1]);
|
||||
}}
|
||||
>
|
||||
<div className={styles.rightListSide}>
|
||||
{false && (
|
||||
<div className={styles.collapseRightBtn} onClick={onCollapse}>
|
||||
{collapsed ? <LeftOutlined /> : <RightOutlined />}
|
||||
</div>
|
||||
)}
|
||||
<SqlSide
|
||||
initialValues={initialValues}
|
||||
domainId={domainId}
|
||||
onSubmitSuccess={onSubmitSuccess}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Pane initialSize={0} />
|
||||
</SplitPane>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DataExploreView;
|
||||
@@ -0,0 +1,12 @@
|
||||
import request from 'umi-request';
|
||||
|
||||
type ExcuteSqlParams = {
|
||||
sql: string;
|
||||
domainId: number;
|
||||
};
|
||||
|
||||
// 执行脚本
|
||||
export async function excuteSql(params: ExcuteSqlParams) {
|
||||
const data = { ...params };
|
||||
return request.post(`${process.env.API_BASE_URL}database/executeSql`, { data });
|
||||
}
|
||||
@@ -0,0 +1,759 @@
|
||||
@borderColor: #eee;
|
||||
@activeColor: #a0c5e8;
|
||||
@hoverColor: #dee4e9;
|
||||
|
||||
.pageContainer {
|
||||
position: absolute;
|
||||
top: 55px;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
// margin: -24px;
|
||||
background: #fff;
|
||||
|
||||
&.externalPageContainer {
|
||||
margin: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.searchBar {
|
||||
:global {
|
||||
.ant-form-item-label {
|
||||
width: 70px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.main {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
display: flex;
|
||||
:global {
|
||||
.ant-tabs {
|
||||
height: 100% !important;
|
||||
.ant-tabs-content {
|
||||
height: 100% !important;
|
||||
.ant-tabs-tabpane {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.rightSide {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
min-width: 250px;
|
||||
height: 100%;
|
||||
margin-left: 4px;
|
||||
padding: 10px;
|
||||
overflow: hidden;
|
||||
:global {
|
||||
.ant-form-item {
|
||||
margin-bottom: 6px;
|
||||
|
||||
.ant-form-item-label {
|
||||
width: 70px;
|
||||
}
|
||||
|
||||
.ant-form-item-control {
|
||||
min-width: 100px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.rightListSide {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
// padding: 10px 10px 0;
|
||||
background-color: #fff;
|
||||
// 去掉标签间距
|
||||
:global {
|
||||
.ant-tabs-card.ant-tabs-top > .ant-tabs-nav .ant-tabs-tab + .ant-tabs-tab,
|
||||
.ant-tabs-card.ant-tabs-bottom > .ant-tabs-nav .ant-tabs-tab + .ant-tabs-tab,
|
||||
.ant-tabs-card.ant-tabs-top > div > .ant-tabs-nav .ant-tabs-tab + .ant-tabs-tab,
|
||||
.ant-tabs-card.ant-tabs-bottom > div > .ant-tabs-nav .ant-tabs-tab + .ant-tabs-tab {
|
||||
margin-left: 0;
|
||||
}
|
||||
.ant-tabs > .ant-tabs-nav .ant-tabs-nav-add,
|
||||
.ant-tabs > div > .ant-tabs-nav .ant-tabs-nav-add {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.leftListSide {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
// padding: 10px 10px 0;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.tableTotal {
|
||||
margin: 0 2px;
|
||||
color: #296df3;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.tableDetaildrawer {
|
||||
:global {
|
||||
.ant-drawer-header {
|
||||
padding: 10px 45px 10px 10px;
|
||||
}
|
||||
|
||||
.ant-drawer-close {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.ant-drawer-body {
|
||||
padding: 0 10px 10px;
|
||||
}
|
||||
|
||||
.ant-tabs-top > .ant-tabs-nav {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tableDetailTable {
|
||||
:global {
|
||||
.ant-table-cell,
|
||||
.resultTableRow > td {
|
||||
padding: 8px;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.sqlEditor {
|
||||
min-width: 0;
|
||||
height: 100%;
|
||||
border: solid 1px @borderColor;
|
||||
|
||||
:global {
|
||||
.ace_editor {
|
||||
font-family: 'Menlo', 'Monaco', 'Ubuntu Mono', 'Consolas', 'source-code-pro' !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.sqlOprBar {
|
||||
margin-top: -10px;
|
||||
padding: 5px;
|
||||
display: flex;
|
||||
.sqlOprBarLeftBox {
|
||||
flex: 1 1 200px;
|
||||
}
|
||||
.sqlOprBarRightBox {
|
||||
flex: 0 1 210px;
|
||||
}
|
||||
:global {
|
||||
.ant-btn-round.ant-btn-sm {
|
||||
font-size: 12px;
|
||||
}
|
||||
.ant-btn-primary {
|
||||
color: #fff;
|
||||
background: #02a7f0;
|
||||
border-color: #02a7f0;
|
||||
}
|
||||
.ant-segmented-item-selected {
|
||||
color: #fff;
|
||||
background: #02a7f0;
|
||||
border-color: #02a7f0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.sqlOprIcon {
|
||||
margin-right: 30px;
|
||||
color: #02a7f0;
|
||||
font-size: 22px;
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
&:active {
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
|
||||
.sqlOprBtn {
|
||||
margin-right: 30px;
|
||||
vertical-align: super !important;
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
&:active {
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
|
||||
.sqlOprSwitch {
|
||||
// vertical-align: super !important;
|
||||
float: right;
|
||||
margin-right: 10px !important;
|
||||
}
|
||||
|
||||
:global {
|
||||
.is-sql-full-select {
|
||||
background-color: #02a7f0;
|
||||
}
|
||||
.cjjWdp:hover {
|
||||
z-index: 10;
|
||||
}
|
||||
}
|
||||
|
||||
.sqlMain {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
height: 100%;
|
||||
|
||||
.sqlEditorWrapper {
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.sqlParams {
|
||||
width: 20%;
|
||||
height: 100% !important;
|
||||
overflow: auto;
|
||||
}
|
||||
.hideSqlParams {
|
||||
width: 0;
|
||||
height: 100% !important;
|
||||
overflow: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.sqlParamsBody {
|
||||
.header {
|
||||
display: flex;
|
||||
padding: 10px;
|
||||
font-weight: bold;
|
||||
|
||||
.title {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-right: 10px !important;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.paramsList {
|
||||
.paramsItem {
|
||||
display: flex;
|
||||
padding: 10px;
|
||||
:global {
|
||||
.ant-list-item-action {
|
||||
margin-left: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.name {
|
||||
flex: 1;
|
||||
width: 80%;
|
||||
overflow: hidden;
|
||||
font-size: 12px;
|
||||
text-overflow: ellipsis;
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.icon {
|
||||
// display: none;
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// .paramsItem:hover {
|
||||
// .icon {
|
||||
// display: inline-block;
|
||||
// margin-left: 8px;
|
||||
// cursor: pointer;
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
.disableIcon {
|
||||
vertical-align: super !important;
|
||||
// color: rgba(0, 10, 36, 0.25);
|
||||
background: #7d7f80 !important;
|
||||
border-color: #7d7f80 !important;
|
||||
:global {
|
||||
.anticon .anticon-play-circle {
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
cursor: not-allowed;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.sqlTaskListWrap {
|
||||
position: relative;
|
||||
width: 262px;
|
||||
border-top: 0 !important;
|
||||
border-radius: 0;
|
||||
|
||||
:global {
|
||||
.ant-card-head {
|
||||
min-height: 20px;
|
||||
}
|
||||
|
||||
.ant-card-head-title {
|
||||
padding: 8px 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.sqlTaskList {
|
||||
position: absolute !important;
|
||||
top: 42px;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.sqlBottmWrap {
|
||||
// position: absolute;
|
||||
// top: 484px;
|
||||
// right: 0;
|
||||
// bottom: 0;
|
||||
// left: 0;
|
||||
display: flex;
|
||||
height: 100%;
|
||||
// padding: 0 10px;
|
||||
|
||||
&:global(.small) {
|
||||
top: 334px;
|
||||
}
|
||||
|
||||
&:global(.middle) {
|
||||
top: 384px;
|
||||
}
|
||||
}
|
||||
|
||||
.sqlResultWrap {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
overflow: auto;
|
||||
border: solid 1px @borderColor;
|
||||
border-top: 0;
|
||||
border-left: 0;
|
||||
}
|
||||
|
||||
.sqlToolBar {
|
||||
display: flex;
|
||||
flex-direction: row-reverse;
|
||||
align-items: center;
|
||||
height: 41px;
|
||||
padding: 5px 0;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.sqlResultPane {
|
||||
flex: 1;
|
||||
border-top: solid 1px @borderColor;
|
||||
}
|
||||
|
||||
.sqlToolBtn {
|
||||
margin-right: 15px;
|
||||
}
|
||||
.runScriptBtn {
|
||||
margin-right: 15px;
|
||||
background-color: #e87954;
|
||||
border-color: #e87954;
|
||||
&:hover{
|
||||
border-color: #f89878;
|
||||
background: #f89878;
|
||||
}
|
||||
&:focus {
|
||||
border-color: #f89878;
|
||||
background: #f89878;
|
||||
}
|
||||
}
|
||||
|
||||
.taskFailed {
|
||||
padding: 20px 20px 0 20px;
|
||||
}
|
||||
|
||||
.sqlResultContent {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
width: 100%;
|
||||
color: rgba(0, 0, 0, 0.25);
|
||||
font-size: 16px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.sqlResultLog {
|
||||
padding: 20px;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.tableList {
|
||||
position: absolute !important;
|
||||
top: 160px;
|
||||
right: 0;
|
||||
bottom: 26px;
|
||||
left: 0;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
border-bottom: solid 1px @borderColor;
|
||||
}
|
||||
|
||||
.tablePage {
|
||||
position: absolute !important;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
z-index: 1;
|
||||
width: 100%;
|
||||
min-width: 250px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.tableListItem {
|
||||
width: 88%;
|
||||
overflow: hidden;
|
||||
font-size: 12px;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.tableItem {
|
||||
&:global(.ant-list-item) {
|
||||
padding: 6px 0 6px 6px;
|
||||
}
|
||||
|
||||
:global(.ant-list-item-action) {
|
||||
margin-left: 12px !important;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: @hoverColor;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
&:global(.active) {
|
||||
background: @activeColor;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.taskIcon {
|
||||
margin-right: 10px;
|
||||
color: #1890ff;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.taskSuccessIcon {
|
||||
.taskIcon();
|
||||
|
||||
color: #67c23a;
|
||||
}
|
||||
|
||||
.taskFailIcon {
|
||||
.taskIcon();
|
||||
|
||||
color: #f56c6c;
|
||||
}
|
||||
|
||||
.resultFailIcon {
|
||||
margin-right: 8px;
|
||||
color: #f56c6c;
|
||||
}
|
||||
|
||||
.taskItem {
|
||||
padding: 10px 8px !important;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
|
||||
&:global(.ant-list-item) {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: @hoverColor;
|
||||
}
|
||||
}
|
||||
|
||||
.activeTask {
|
||||
background: @activeColor;
|
||||
}
|
||||
|
||||
.resultTable {
|
||||
width: 100%;
|
||||
|
||||
:global {
|
||||
.ant-table-body {
|
||||
width: 100%;
|
||||
// max-height: none !important;
|
||||
overflow: auto !important;
|
||||
}
|
||||
|
||||
.ant-table-cell,
|
||||
.resultTableRow > td {
|
||||
padding: 8px;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.taskLogWrap {
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.siteTagPlus {
|
||||
background: #fff;
|
||||
border-style: dashed;
|
||||
}
|
||||
|
||||
.editTag {
|
||||
margin-bottom: 5px;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.tagInput {
|
||||
width: 78px;
|
||||
margin-right: 8px;
|
||||
vertical-align: top;
|
||||
}
|
||||
.outside {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
}
|
||||
.collapseRightBtn {
|
||||
position: absolute;
|
||||
top: calc(50% + 50px);
|
||||
right: 0;
|
||||
z-index: 100;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 70px;
|
||||
color: #fff;
|
||||
font-size: 12px;
|
||||
background-color: rgba(40, 46, 54, 0.2);
|
||||
border-radius: 24px 0 0 24px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.collapseLeftBtn {
|
||||
position: absolute;
|
||||
top: calc(50% + 45px);
|
||||
left: 0;
|
||||
z-index: 100;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 70px;
|
||||
color: #fff;
|
||||
font-size: 12px;
|
||||
background-color: rgba(40, 46, 54, 0.2);
|
||||
border-radius: 0 24px 24px 0;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.detail {
|
||||
.titleCollapse {
|
||||
float: right;
|
||||
padding-right: 18px;
|
||||
color: #1890ff;
|
||||
line-height: 35px;
|
||||
text-align: right;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.tableTitle {
|
||||
display: inline-block;
|
||||
width: 85%;
|
||||
margin-left: 15px;
|
||||
overflow: hidden;
|
||||
line-height: 35px;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
:global {
|
||||
.ant-divider-horizontal {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.search {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.middleArea {
|
||||
:global {
|
||||
.ant-tabs-nav .ant-tabs-tab {
|
||||
border: none;
|
||||
// background: #d9d9d96e;
|
||||
border-right: 1px solid #f0f0f0;
|
||||
border-radius: 0 !important;
|
||||
}
|
||||
.ant-tabs-nav-add {
|
||||
border-radius: 0 !important;
|
||||
}
|
||||
|
||||
.ant-tabs-tab {
|
||||
.ant-tabs-tab-remove {
|
||||
.closeTab {
|
||||
opacity: 0;
|
||||
}
|
||||
.dot {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
.ant-tabs-tab:hover {
|
||||
.ant-tabs-tab-remove {
|
||||
.closeTab {
|
||||
opacity: 1 !important;
|
||||
}
|
||||
.dot {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.menu {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
height: 100%;
|
||||
padding: 5px;
|
||||
overflow: hidden;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
:global {
|
||||
.ant-form {
|
||||
margin: -2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.menuList {
|
||||
position: absolute !important;
|
||||
top: 95px;
|
||||
right: 0;
|
||||
bottom: 26px;
|
||||
left: 0;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
border-bottom: solid 1px @borderColor;
|
||||
.menuItem {
|
||||
&:global(.ant-list-item) {
|
||||
padding: 6px 0 6px 14px;
|
||||
}
|
||||
|
||||
:global(.ant-list-item-action) {
|
||||
margin-left: 12px !important;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: @hoverColor;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
.icon {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
&:global(.active) {
|
||||
background: @activeColor;
|
||||
}
|
||||
.menuListItem {
|
||||
width: 90%;
|
||||
overflow: hidden;
|
||||
font-size: 12px;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
.icon {
|
||||
display: none;
|
||||
margin-right: 15px !important;
|
||||
cursor: pointer;
|
||||
}
|
||||
.menuIcon {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.scriptFile {
|
||||
width: 100%;
|
||||
margin: 10px;
|
||||
overflow: hidden;
|
||||
font-size: 14px;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
.icon {
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.sqlScriptName {
|
||||
width: 93% !important;
|
||||
margin: 14px 0 0 14px !important;
|
||||
}
|
||||
|
||||
.fileIcon {
|
||||
width: 20px !important;
|
||||
height: 20px !important;
|
||||
padding-top: 2px !important;
|
||||
padding-right: 5px !important;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.itemName {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.paneName {
|
||||
width: 100px;
|
||||
overflow: hidden;
|
||||
font-size: 12px !important;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.titleIcon {
|
||||
width: 16px !important;
|
||||
height: 16px !important;
|
||||
margin: 0 3px 4px;
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
import { Tabs } from 'antd';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { connect, Helmet } from 'umi';
|
||||
import ProjectListTree from './components/ProjectList';
|
||||
import ClassDataSourceTable from './components/ClassDataSourceTable';
|
||||
import ClassDimensionTable from './components/ClassDimensionTable';
|
||||
import ClassMetricTable from './components/ClassMetricTable';
|
||||
import PermissionSection from './components/Permission/PermissionSection';
|
||||
import DatabaseSection from './components/Database/DatabaseSection';
|
||||
import styles from './components/style.less';
|
||||
import type { StateType } from './model';
|
||||
import { RightOutlined, LeftOutlined } from '@ant-design/icons';
|
||||
import SemanticFlow from './SemanticFlows';
|
||||
// import SemanticGraph from './SemanticGraph';
|
||||
import SplitPane from 'react-split-pane';
|
||||
import Pane from 'react-split-pane/lib/Pane';
|
||||
import type { Dispatch } from 'umi';
|
||||
|
||||
const { TabPane } = Tabs;
|
||||
|
||||
type Props = {
|
||||
domainManger: StateType;
|
||||
dispatch: Dispatch;
|
||||
};
|
||||
|
||||
const DEFAULT_LEFT_SIZE = '300px';
|
||||
|
||||
const DomainManger: React.FC<Props> = ({ domainManger, dispatch }) => {
|
||||
const [collapsed, setCollapsed] = useState(false);
|
||||
const [leftSize, setLeftSize] = useState('');
|
||||
const { selectDomainId, selectDomainName } = domainManger;
|
||||
useEffect(() => {
|
||||
const semanticLeftCollapsed = localStorage.getItem('semanticLeftCollapsed');
|
||||
const semanticLeftSize =
|
||||
semanticLeftCollapsed === 'true' ? '0px' : localStorage.getItem('semanticLeftSize');
|
||||
setCollapsed(semanticLeftCollapsed === 'true');
|
||||
setLeftSize(semanticLeftSize || DEFAULT_LEFT_SIZE);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectDomainId) {
|
||||
dispatch({
|
||||
type: 'domainManger/queryDimensionList',
|
||||
payload: {
|
||||
domainId: selectDomainId,
|
||||
},
|
||||
});
|
||||
dispatch({
|
||||
type: 'domainManger/queryMetricList',
|
||||
payload: {
|
||||
domainId: selectDomainId,
|
||||
},
|
||||
});
|
||||
}
|
||||
}, [selectDomainId]);
|
||||
|
||||
const onCollapse = () => {
|
||||
const collapsedValue = !collapsed;
|
||||
setCollapsed(collapsedValue);
|
||||
localStorage.setItem('semanticLeftCollapsed', String(collapsedValue));
|
||||
const semanticLeftSize = collapsedValue ? '0px' : localStorage.getItem('semanticLeftSize');
|
||||
const sizeValue = parseInt(semanticLeftSize || '0');
|
||||
if (!collapsedValue && sizeValue <= 10) {
|
||||
setLeftSize(DEFAULT_LEFT_SIZE);
|
||||
localStorage.setItem('semanticLeftSize', DEFAULT_LEFT_SIZE);
|
||||
} else {
|
||||
setLeftSize(semanticLeftSize || DEFAULT_LEFT_SIZE);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const width = document.getElementById('tab');
|
||||
const switchWarpper: any = document.getElementById('switch');
|
||||
if (width && switchWarpper) {
|
||||
switchWarpper.style.width = width.offsetWidth * 0.77 + 'px';
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={styles.projectBody}>
|
||||
<Helmet title={'语义建模-超音数'} />
|
||||
<SplitPane
|
||||
split="vertical"
|
||||
onChange={(size) => {
|
||||
localStorage.setItem('semanticLeftSize', size[0]);
|
||||
setLeftSize(size[0]);
|
||||
}}
|
||||
>
|
||||
<Pane initialSize={leftSize || DEFAULT_LEFT_SIZE}>
|
||||
<div className={styles.menu}>
|
||||
<ProjectListTree />
|
||||
</div>
|
||||
</Pane>
|
||||
|
||||
<div className={styles.projectManger}>
|
||||
<div className={styles.collapseLeftBtn} onClick={onCollapse}>
|
||||
{collapsed ? <RightOutlined /> : <LeftOutlined />}
|
||||
</div>
|
||||
<h2 className={styles.title}>
|
||||
{selectDomainName ? `选择的主题域:${selectDomainName}` : '主题域信息'}
|
||||
</h2>
|
||||
{selectDomainId ? (
|
||||
<>
|
||||
<Tabs className={styles.tab} defaultActiveKey="xflow" destroyInactiveTabPane>
|
||||
{/* <TabPane className={styles.tabPane} tab="关系可视化" key="graph">
|
||||
<div style={{ width: '100%', height: 'calc(100vh - 200px)' }}>
|
||||
<SemanticGraph domainId={selectDomainId} />
|
||||
</div>
|
||||
</TabPane> */}
|
||||
<TabPane className={styles.tabPane} tab="可视化建模" key="xflow">
|
||||
<div style={{ width: '100%', height: 'calc(100vh - 200px)' }}>
|
||||
<SemanticFlow />
|
||||
</div>
|
||||
</TabPane>
|
||||
<TabPane className={styles.tabPane} tab="数据库" key="dataBase">
|
||||
<DatabaseSection />
|
||||
</TabPane>
|
||||
<TabPane className={styles.tabPane} tab="数据源" key="dataSource">
|
||||
<ClassDataSourceTable />
|
||||
</TabPane>
|
||||
<TabPane className={styles.tabPane} tab="维度" key="dimenstion">
|
||||
<ClassDimensionTable key={selectDomainId} />
|
||||
</TabPane>
|
||||
<TabPane className={styles.tabPane} tab="指标" key="metric">
|
||||
<ClassMetricTable />
|
||||
</TabPane>
|
||||
<TabPane className={styles.tabPane} tab="权限管理" key="permissonSetting">
|
||||
<PermissionSection />
|
||||
</TabPane>
|
||||
</Tabs>
|
||||
</>
|
||||
) : (
|
||||
<h2 className={styles.mainTip}>请选择项目</h2>
|
||||
)}
|
||||
</div>
|
||||
</SplitPane>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default connect(({ domainManger }: { domainManger: StateType }) => ({
|
||||
domainManger,
|
||||
}))(DomainManger);
|
||||
@@ -0,0 +1,209 @@
|
||||
import type { HookHub, ICmdHooks as IHooks, NsGraph } from '@antv/xflow';
|
||||
import { Deferred, ManaSyringe } from '@antv/xflow';
|
||||
import { Modal, ConfigProvider } from 'antd';
|
||||
import type { IArgsBase, ICommandHandler } from '@antv/xflow';
|
||||
import { ICommandContextProvider } from '@antv/xflow';
|
||||
import { DATASOURCE_NODE_RENDER_ID } from '../constant';
|
||||
import { CustomCommands } from './constants';
|
||||
|
||||
import 'antd/es/modal/style/index.css';
|
||||
|
||||
export namespace NsConfirmModalCmd {
|
||||
/** Command: 用于注册named factory */
|
||||
// eslint-disable-next-line
|
||||
export const command = CustomCommands.SHOW_CONFIRM_MODAL;
|
||||
/** hook name */
|
||||
// eslint-disable-next-line
|
||||
export const hookKey = 'confirmModal';
|
||||
/** hook 参数类型 */
|
||||
export interface IArgs extends IArgsBase {
|
||||
nodeConfig: NsGraph.INodeConfig;
|
||||
confirmModalCallBack: IConfirmModalService;
|
||||
}
|
||||
export interface IConfirmModalService {
|
||||
(): Promise<any>;
|
||||
}
|
||||
/** hook handler 返回类型 */
|
||||
export type IResult = any;
|
||||
/** hooks 类型 */
|
||||
export interface ICmdHooks extends IHooks {
|
||||
confirmModal: HookHub<IArgs, IResult>;
|
||||
}
|
||||
}
|
||||
|
||||
const deleteDataSourceConfirmNode = (name: string) => {
|
||||
return (
|
||||
<>
|
||||
数据源<span style={{ color: '#296DF3', fontWeight: 'bold' }}>{name}</span>
|
||||
将被删除,是否确认?
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
// prettier-ignore
|
||||
type ICommand = ICommandHandler<NsConfirmModalCmd.IArgs, NsConfirmModalCmd.IResult, NsConfirmModalCmd.ICmdHooks>;
|
||||
|
||||
@ManaSyringe.injectable()
|
||||
/** 部署画布数据 */
|
||||
export class ConfirmModalCommand implements ICommand {
|
||||
/** api */
|
||||
@ManaSyringe.inject(ICommandContextProvider) contextProvider!: ICommand['contextProvider'];
|
||||
|
||||
/** 执行Cmd */
|
||||
execute = async () => {
|
||||
const ctx = this.contextProvider();
|
||||
const { args } = ctx.getArgs();
|
||||
const hooks = ctx.getHooks();
|
||||
await hooks.confirmModal.call(args, async (confirmArgs: NsConfirmModalCmd.IArgs) => {
|
||||
const { nodeConfig, confirmModalCallBack } = confirmArgs;
|
||||
const { renderKey, label } = nodeConfig;
|
||||
if (!nodeConfig.modalProps?.modalContent) {
|
||||
let modalContent = <></>;
|
||||
if (renderKey === DATASOURCE_NODE_RENDER_ID) {
|
||||
modalContent = deleteDataSourceConfirmNode(label!);
|
||||
}
|
||||
nodeConfig.modalProps = {
|
||||
...(nodeConfig.modalProps || {}),
|
||||
modalContent,
|
||||
};
|
||||
}
|
||||
const getAppContext: IGetAppCtx = () => {
|
||||
return {
|
||||
confirmModalCallBack,
|
||||
};
|
||||
};
|
||||
|
||||
const x6Graph = await ctx.getX6Graph();
|
||||
const cell = x6Graph.getCellById(nodeConfig.id);
|
||||
|
||||
if (!cell || !cell.isNode()) {
|
||||
throw new Error(`${nodeConfig.id} is not a valid node`);
|
||||
}
|
||||
/** 通过modal 获取 new name */
|
||||
await showModal(nodeConfig, getAppContext);
|
||||
|
||||
return;
|
||||
});
|
||||
|
||||
// ctx.setResult(result);
|
||||
return this;
|
||||
};
|
||||
|
||||
/** undo cmd */
|
||||
undo = async () => {
|
||||
if (this.isUndoable()) {
|
||||
const ctx = this.contextProvider();
|
||||
ctx.undo();
|
||||
}
|
||||
return this;
|
||||
};
|
||||
|
||||
/** redo cmd */
|
||||
redo = async () => {
|
||||
if (!this.isUndoable()) {
|
||||
await this.execute();
|
||||
}
|
||||
return this;
|
||||
};
|
||||
|
||||
isUndoable(): boolean {
|
||||
const ctx = this.contextProvider();
|
||||
return ctx.isUndoable();
|
||||
}
|
||||
}
|
||||
|
||||
export interface IGetAppCtx {
|
||||
(): {
|
||||
confirmModalCallBack: NsConfirmModalCmd.IConfirmModalService;
|
||||
};
|
||||
}
|
||||
|
||||
export type IModalInstance = ReturnType<typeof Modal.confirm>;
|
||||
|
||||
function showModal(node: NsGraph.INodeConfig, getAppContext: IGetAppCtx) {
|
||||
/** showModal 返回一个Promise */
|
||||
const defer = new Deferred<string | void>();
|
||||
const modalTitle = node.modalProps?.title;
|
||||
const modalContent = node.modalProps?.modalContent;
|
||||
/** modal确认保存逻辑 */
|
||||
class ModalCache {
|
||||
static modal: IModalInstance;
|
||||
}
|
||||
|
||||
/** modal确认保存逻辑 */
|
||||
const onOk = async () => {
|
||||
const { modal } = ModalCache;
|
||||
const appContext = getAppContext();
|
||||
const { confirmModalCallBack } = appContext;
|
||||
try {
|
||||
modal.update({ okButtonProps: { loading: true } });
|
||||
|
||||
/** 执行 confirm回调*/
|
||||
if (confirmModalCallBack) {
|
||||
await confirmModalCallBack();
|
||||
}
|
||||
/** 更新成功后,关闭modal */
|
||||
onHide();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
/** 如果resolve空字符串则不更新 */
|
||||
modal.update({ okButtonProps: { loading: false } });
|
||||
}
|
||||
};
|
||||
|
||||
/** modal销毁逻辑 */
|
||||
const onHide = () => {
|
||||
modal.destroy();
|
||||
ModalCache.modal = null as any;
|
||||
container.destroy();
|
||||
};
|
||||
|
||||
/** modal内容 */
|
||||
const ModalContent = () => {
|
||||
return (
|
||||
<div>
|
||||
<ConfigProvider>{modalContent}</ConfigProvider>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
/** 创建modal dom容器 */
|
||||
const container = createContainer();
|
||||
/** 创建modal */
|
||||
const modal = Modal.confirm({
|
||||
title: modalTitle,
|
||||
content: <ModalContent />,
|
||||
getContainer: () => {
|
||||
return container.element;
|
||||
},
|
||||
okButtonProps: {
|
||||
onClick: (e) => {
|
||||
e.stopPropagation();
|
||||
onOk();
|
||||
},
|
||||
},
|
||||
onCancel: () => {
|
||||
onHide();
|
||||
},
|
||||
afterClose: () => {
|
||||
onHide();
|
||||
},
|
||||
});
|
||||
|
||||
/** 缓存modal实例 */
|
||||
ModalCache.modal = modal;
|
||||
|
||||
/** showModal 返回一个Promise,用于await */
|
||||
return defer.promise;
|
||||
}
|
||||
|
||||
const createContainer = () => {
|
||||
const div = document.createElement('div');
|
||||
div.classList.add('xflow-modal-container');
|
||||
window.document.body.append(div);
|
||||
return {
|
||||
element: div,
|
||||
destroy: () => {
|
||||
window.document.body.removeChild(div);
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,89 @@
|
||||
import type {
|
||||
NsGraphCmd,
|
||||
ICmdHooks as IHooks,
|
||||
NsGraph,
|
||||
IArgsBase,
|
||||
ICommandHandler,
|
||||
HookHub,
|
||||
} from '@antv/xflow';
|
||||
import { XFlowGraphCommands, ManaSyringe } from '@antv/xflow';
|
||||
import { ICommandContextProvider } from '@antv/xflow';
|
||||
import { CustomCommands } from './constants';
|
||||
|
||||
// prettier-ignore
|
||||
type ICommand = ICommandHandler<NsDeployDagCmd.IArgs, NsDeployDagCmd.IResult, NsDeployDagCmd.ICmdHooks>;
|
||||
|
||||
export namespace NsDeployDagCmd {
|
||||
/** Command: 用于注册named factory */
|
||||
// eslint-disable-next-line
|
||||
export const command = CustomCommands.DEPLOY_SERVICE;
|
||||
/** hook name */
|
||||
// eslint-disable-next-line
|
||||
export const hookKey = 'deployDag';
|
||||
/** hook 参数类型 */
|
||||
export interface IArgs extends IArgsBase {
|
||||
deployDagService: IDeployDagService;
|
||||
}
|
||||
export interface IDeployDagService {
|
||||
(meta: NsGraph.IGraphMeta, data: NsGraph.IGraphData): Promise<{ success: boolean }>;
|
||||
}
|
||||
/** hook handler 返回类型 */
|
||||
export interface IResult {
|
||||
success: boolean;
|
||||
}
|
||||
/** hooks 类型 */
|
||||
export interface ICmdHooks extends IHooks {
|
||||
deployDag: HookHub<IArgs, IResult>;
|
||||
}
|
||||
}
|
||||
|
||||
@ManaSyringe.injectable()
|
||||
/** 部署画布数据 */
|
||||
export class DeployDagCommand implements ICommand {
|
||||
/** api */
|
||||
@ManaSyringe.inject(ICommandContextProvider) contextProvider!: ICommand['contextProvider'];
|
||||
|
||||
/** 执行Cmd */
|
||||
execute = async () => {
|
||||
const ctx = this.contextProvider();
|
||||
const { args } = ctx.getArgs();
|
||||
const hooks = ctx.getHooks();
|
||||
const result = await hooks.deployDag.call(args, async (handlerArgs) => {
|
||||
const { commandService, deployDagService } = handlerArgs;
|
||||
/** 执行Command */
|
||||
await commandService!.executeCommand<NsGraphCmd.SaveGraphData.IArgs>(
|
||||
XFlowGraphCommands.SAVE_GRAPH_DATA.id,
|
||||
{
|
||||
saveGraphDataService: async (meta, graph) => {
|
||||
await deployDagService(meta, graph);
|
||||
},
|
||||
},
|
||||
);
|
||||
return { success: true };
|
||||
});
|
||||
ctx.setResult(result);
|
||||
return this;
|
||||
};
|
||||
|
||||
/** undo cmd */
|
||||
undo = async () => {
|
||||
if (this.isUndoable()) {
|
||||
const ctx = this.contextProvider();
|
||||
ctx.undo();
|
||||
}
|
||||
return this;
|
||||
};
|
||||
|
||||
/** redo cmd */
|
||||
redo = async () => {
|
||||
if (!this.isUndoable()) {
|
||||
await this.execute();
|
||||
}
|
||||
return this;
|
||||
};
|
||||
|
||||
isUndoable(): boolean {
|
||||
const ctx = this.contextProvider();
|
||||
return ctx.isUndoable();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,255 @@
|
||||
import type { HookHub, ICmdHooks as IHooks, NsGraph, IModelService } from '@antv/xflow';
|
||||
import { Deferred, ManaSyringe } from '@antv/xflow';
|
||||
import type { FormInstance } from 'antd';
|
||||
import { Modal, Form, Input, ConfigProvider } from 'antd';
|
||||
|
||||
import type { IArgsBase, ICommandHandler, IGraphCommandService } from '@antv/xflow';
|
||||
import { ICommandContextProvider } from '@antv/xflow';
|
||||
|
||||
import { CustomCommands } from './constants';
|
||||
|
||||
import 'antd/es/modal/style/index.css';
|
||||
|
||||
// prettier-ignore
|
||||
type ICommand = ICommandHandler<NsRenameNodeCmd.IArgs, NsRenameNodeCmd.IResult, NsRenameNodeCmd.ICmdHooks>;
|
||||
|
||||
export namespace NsRenameNodeCmd {
|
||||
/** Command: 用于注册named factory */
|
||||
// eslint-disable-next-line
|
||||
export const command = CustomCommands.SHOW_RENAME_MODAL;
|
||||
/** hook name */
|
||||
// eslint-disable-next-line
|
||||
export const hookKey = 'renameNode';
|
||||
/** hook 参数类型 */
|
||||
export interface IArgs extends IArgsBase {
|
||||
nodeConfig: NsGraph.INodeConfig;
|
||||
updateNodeNameService: IUpdateNodeNameService;
|
||||
}
|
||||
export interface IUpdateNodeNameService {
|
||||
(newName: string, nodeConfig: NsGraph.INodeConfig, meta: NsGraph.IGraphMeta): Promise<{
|
||||
err: string | null;
|
||||
nodeName: string;
|
||||
}>;
|
||||
}
|
||||
/** hook handler 返回类型 */
|
||||
export interface IResult {
|
||||
err: string | null;
|
||||
preNodeName?: string;
|
||||
currentNodeName?: string;
|
||||
}
|
||||
/** hooks 类型 */
|
||||
export interface ICmdHooks extends IHooks {
|
||||
renameNode: HookHub<IArgs, IResult>;
|
||||
}
|
||||
}
|
||||
|
||||
@ManaSyringe.injectable()
|
||||
/** 部署画布数据 */
|
||||
// prettier-ignore
|
||||
export class RenameNodeCommand implements ICommand {
|
||||
/** api */
|
||||
@ManaSyringe.inject(ICommandContextProvider) contextProvider!: ICommand['contextProvider'];
|
||||
|
||||
/** 执行Cmd */
|
||||
execute = async () => {
|
||||
const ctx = this.contextProvider();
|
||||
// const app = useXFlowApp();
|
||||
const { args } = ctx.getArgs();
|
||||
const hooks = ctx.getHooks();
|
||||
const result = await hooks.renameNode.call(args, async (args) => {
|
||||
const { nodeConfig, graphMeta, commandService, modelService, updateNodeNameService } = args;
|
||||
const preNodeName = nodeConfig.label;
|
||||
|
||||
const getAppContext: IGetAppCtx = () => {
|
||||
return {
|
||||
graphMeta,
|
||||
commandService,
|
||||
modelService,
|
||||
updateNodeNameService,
|
||||
};
|
||||
};
|
||||
|
||||
const x6Graph = await ctx.getX6Graph();
|
||||
const cell = x6Graph.getCellById(nodeConfig.id);
|
||||
const nodes = x6Graph.getNodes();
|
||||
const edges = x6Graph.getEdges();
|
||||
nodes.forEach((node) => {
|
||||
if (node !== cell) {
|
||||
x6Graph.removeCell(node);
|
||||
}
|
||||
});
|
||||
edges.forEach((edge) => {
|
||||
x6Graph.removeEdge(edge);
|
||||
});
|
||||
if (!cell || !cell.isNode()) {
|
||||
throw new Error(`${nodeConfig.id} is not a valid node`);
|
||||
}
|
||||
/** 通过modal 获取 new name */
|
||||
const newName = await showModal(nodeConfig, getAppContext);
|
||||
/** 更新 node name */
|
||||
if (newName) {
|
||||
const cellData = cell.getData<NsGraph.INodeConfig>();
|
||||
|
||||
cell.setData({ ...cellData, label: newName } as NsGraph.INodeConfig);
|
||||
return { err: null, preNodeName, currentNodeName: newName };
|
||||
}
|
||||
return { err: null, preNodeName, currentNodeName: '' };
|
||||
});
|
||||
|
||||
ctx.setResult(result);
|
||||
return this;
|
||||
};
|
||||
|
||||
/** undo cmd */
|
||||
undo = async () => {
|
||||
if (this.isUndoable()) {
|
||||
const ctx = this.contextProvider();
|
||||
ctx.undo();
|
||||
}
|
||||
return this;
|
||||
};
|
||||
|
||||
/** redo cmd */
|
||||
redo = async () => {
|
||||
if (!this.isUndoable()) {
|
||||
await this.execute();
|
||||
}
|
||||
return this;
|
||||
};
|
||||
|
||||
isUndoable(): boolean {
|
||||
const ctx = this.contextProvider();
|
||||
return ctx.isUndoable();
|
||||
}
|
||||
}
|
||||
|
||||
export interface IGetAppCtx {
|
||||
(): {
|
||||
graphMeta: NsGraph.IGraphMeta;
|
||||
commandService: IGraphCommandService;
|
||||
modelService: IModelService;
|
||||
updateNodeNameService: NsRenameNodeCmd.IUpdateNodeNameService;
|
||||
};
|
||||
}
|
||||
|
||||
export type IModalInstance = ReturnType<typeof Modal.confirm>;
|
||||
export interface IFormProps {
|
||||
newNodeName: string;
|
||||
}
|
||||
|
||||
const layout = {
|
||||
labelCol: { span: 5 },
|
||||
wrapperCol: { span: 19 },
|
||||
};
|
||||
|
||||
function showModal(node: NsGraph.INodeConfig, getAppContext: IGetAppCtx) {
|
||||
/** showModal 返回一个Promise */
|
||||
const defer = new Deferred<string | void>();
|
||||
|
||||
/** modal确认保存逻辑 */
|
||||
class ModalCache {
|
||||
static modal: IModalInstance;
|
||||
static form: FormInstance<IFormProps>;
|
||||
}
|
||||
|
||||
/** modal确认保存逻辑 */
|
||||
const onOk = async () => {
|
||||
const { form, modal } = ModalCache;
|
||||
const appContext = getAppContext();
|
||||
const { updateNodeNameService, graphMeta } = appContext;
|
||||
try {
|
||||
modal.update({ okButtonProps: { loading: true } });
|
||||
await form.validateFields();
|
||||
const values = await form.getFieldsValue();
|
||||
const newName: string = values.newNodeName;
|
||||
/** 执行 backend service */
|
||||
if (updateNodeNameService) {
|
||||
const { err, nodeName } = await updateNodeNameService(newName, node, graphMeta);
|
||||
if (err) {
|
||||
throw new Error(err);
|
||||
}
|
||||
defer.resolve(nodeName);
|
||||
}
|
||||
/** 更新成功后,关闭modal */
|
||||
onHide();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
/** 如果resolve空字符串则不更新 */
|
||||
modal.update({ okButtonProps: { loading: false } });
|
||||
}
|
||||
};
|
||||
|
||||
/** modal销毁逻辑 */
|
||||
const onHide = () => {
|
||||
modal.destroy();
|
||||
ModalCache.form = null as any;
|
||||
ModalCache.modal = null as any;
|
||||
container.destroy();
|
||||
};
|
||||
|
||||
/** modal内容 */
|
||||
const ModalContent = () => {
|
||||
const [form] = Form.useForm<IFormProps>();
|
||||
/** 缓存form实例 */
|
||||
ModalCache.form = form;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<ConfigProvider>
|
||||
<Form form={form} {...layout} initialValues={{ newNodeName: node.label }}>
|
||||
<Form.Item
|
||||
name="newNodeName"
|
||||
label="节点名"
|
||||
rules={[
|
||||
{ required: true, message: '请输入新节点名' },
|
||||
{ min: 3, message: '节点名不能少于3个字符' },
|
||||
]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</ConfigProvider>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
/** 创建modal dom容器 */
|
||||
const container = createContainer();
|
||||
/** 创建modal */
|
||||
const modal = Modal.confirm({
|
||||
title: '重命名',
|
||||
content: <ModalContent />,
|
||||
getContainer: () => {
|
||||
return container.element;
|
||||
},
|
||||
okButtonProps: {
|
||||
onClick: (e) => {
|
||||
e.stopPropagation();
|
||||
onOk();
|
||||
},
|
||||
},
|
||||
onCancel: () => {
|
||||
onHide();
|
||||
},
|
||||
afterClose: () => {
|
||||
onHide();
|
||||
},
|
||||
});
|
||||
|
||||
/** 缓存modal实例 */
|
||||
ModalCache.modal = modal;
|
||||
|
||||
/** showModal 返回一个Promise,用于await */
|
||||
return defer.promise;
|
||||
}
|
||||
|
||||
const createContainer = () => {
|
||||
const div = document.createElement('div');
|
||||
div.classList.add('xflow-modal-container');
|
||||
window.document.body.append(div);
|
||||
return {
|
||||
element: div,
|
||||
destroy: () => {
|
||||
window.document.body.removeChild(div);
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,88 @@
|
||||
import type {
|
||||
ICmdHooks as IHooks,
|
||||
NsGraph,
|
||||
IArgsBase,
|
||||
ICommandHandler,
|
||||
HookHub,
|
||||
} from '@antv/xflow';
|
||||
import { ManaSyringe } from '@antv/xflow';
|
||||
import { ICommandContextProvider } from '@antv/xflow';
|
||||
import { CustomCommands } from './constants';
|
||||
import { getDatasourceRelaList } from '../../service';
|
||||
|
||||
// prettier-ignore
|
||||
type ICommand = ICommandHandler<NsDataSourceRelationCmd.IArgs, NsDataSourceRelationCmd.IResult, NsDataSourceRelationCmd.ICmdHooks>;
|
||||
|
||||
export namespace NsDataSourceRelationCmd {
|
||||
/** Command: 用于注册named factory */
|
||||
// eslint-disable-next-line
|
||||
export const command = CustomCommands.DATASOURCE_RELATION;
|
||||
/** hook name */
|
||||
// eslint-disable-next-line
|
||||
export const hookKey = 'dataSourceRelation';
|
||||
/** hook 参数类型 */
|
||||
export interface IArgs extends IArgsBase {
|
||||
dataSourceRelationService: IDataSourceRelationService;
|
||||
}
|
||||
export interface IDataSourceRelationService {
|
||||
(meta: NsGraph.IGraphMeta, data: NsGraph.IGraphData): Promise<{ success: boolean }>;
|
||||
}
|
||||
/** hook handler 返回类型 */
|
||||
export type IResult = any[] | undefined;
|
||||
/** hooks 类型 */
|
||||
export interface ICmdHooks extends IHooks {
|
||||
dataSourceRelation: HookHub<IArgs, IResult>;
|
||||
}
|
||||
}
|
||||
|
||||
@ManaSyringe.injectable()
|
||||
/** 部署画布数据 */
|
||||
export class DataSourceRelationCommand implements ICommand {
|
||||
/** api */
|
||||
@ManaSyringe.inject(ICommandContextProvider) contextProvider!: ICommand['contextProvider'];
|
||||
|
||||
/** 执行Cmd */
|
||||
execute = async () => {
|
||||
const ctx = this.contextProvider();
|
||||
const { args } = ctx.getArgs();
|
||||
const hooks = ctx.getHooks();
|
||||
const graphMeta = await ctx.getGraphMeta();
|
||||
|
||||
const domainId = graphMeta?.meta?.domainManger?.selectDomainId;
|
||||
if (!domainId) {
|
||||
return this;
|
||||
}
|
||||
const result = await hooks.dataSourceRelation.call(args, async () => {
|
||||
const { code, data } = await getDatasourceRelaList(domainId);
|
||||
if (code === 200) {
|
||||
return data;
|
||||
}
|
||||
return [];
|
||||
});
|
||||
ctx.setResult(result);
|
||||
ctx.setGlobal('dataSourceRelationList', result);
|
||||
return this;
|
||||
};
|
||||
|
||||
/** undo cmd */
|
||||
undo = async () => {
|
||||
if (this.isUndoable()) {
|
||||
const ctx = this.contextProvider();
|
||||
ctx.undo();
|
||||
}
|
||||
return this;
|
||||
};
|
||||
|
||||
/** redo cmd */
|
||||
redo = async () => {
|
||||
if (!this.isUndoable()) {
|
||||
await this.execute();
|
||||
}
|
||||
return this;
|
||||
};
|
||||
|
||||
isUndoable(): boolean {
|
||||
const ctx = this.contextProvider();
|
||||
return ctx.isUndoable();
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user