first commit
16
webapp/packages/supersonic-fe/.editorconfig
Normal file
@@ -0,0 +1,16 @@
|
||||
# http://editorconfig.org
|
||||
root = true
|
||||
|
||||
[*]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
end_of_line = lf
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
||||
|
||||
[Makefile]
|
||||
indent_style = tab
|
||||
8
webapp/packages/supersonic-fe/.eslintignore
Normal file
@@ -0,0 +1,8 @@
|
||||
/lambda/
|
||||
/scripts
|
||||
/config
|
||||
.history
|
||||
public
|
||||
dist
|
||||
.umi
|
||||
mock
|
||||
38
webapp/packages/supersonic-fe/.eslintrc.js
Normal file
@@ -0,0 +1,38 @@
|
||||
module.exports = {
|
||||
extends: [require.resolve('@umijs/fabric/dist/eslint')],
|
||||
// globals: {
|
||||
// ANT_DESIGN_PRO_ONLY_DO_NOT_USE_IN_YOUR_PRODUCTION: true,
|
||||
// page: true,
|
||||
// REACT_APP_ENV: true,
|
||||
// },
|
||||
rules: {
|
||||
'spaced-comment': 'off',
|
||||
'@typescript-eslint/no-parameter-properties': 'off',
|
||||
'@typescript-eslint/no-redeclare': 'off',
|
||||
'@typescript-eslint/no-namespace': 'off',
|
||||
'no-param-reassign': 'off',
|
||||
'no-underscore-dangle': 'off',
|
||||
'no-restricted-syntax': 'off',
|
||||
'@typescript-eslint/no-loop-func': 'off',
|
||||
'consistent-type-definitions': 'off',
|
||||
"no-use-before-define": "off",
|
||||
"@typescript-eslint/no-use-before-define": "off",
|
||||
"@typescript-eslint/consistent-type-imports": "off",
|
||||
"no-shadow": "off",
|
||||
"@typescript-eslint/no-shadow": "off",
|
||||
'no-useless-return': 'off',
|
||||
'max-classes-per-file': 'off',
|
||||
'no-return-assign': 'off',
|
||||
'no-continue': 'off',
|
||||
'no-bitwise': 'off',
|
||||
'no-await-in-loop': 'off',
|
||||
'@typescript-eslint/no-unused-expressions': 'off',
|
||||
'global-require': 'off',
|
||||
'no-plusplus': 'off',
|
||||
'import/export': 'off',
|
||||
'react-hooks/exhaustive-deps': 'off',
|
||||
'import/no-extraneous-dependencies': 0,
|
||||
'react-hooks/exhaustive-deps': 0,
|
||||
'no-console': 0,
|
||||
},
|
||||
};
|
||||
44
webapp/packages/supersonic-fe/.gitignore
vendored
Normal file
@@ -0,0 +1,44 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
**/node_modules
|
||||
# roadhog-api-doc ignore
|
||||
/src/utils/request-temp.js
|
||||
_roadhog-api-doc
|
||||
|
||||
# production
|
||||
/dist
|
||||
/supersonic-webapp
|
||||
/.vscode
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
npm-debug.log*
|
||||
yarn-error.log
|
||||
|
||||
/coverage
|
||||
.idea
|
||||
package-lock.json
|
||||
yarn.lock
|
||||
*bak
|
||||
.vscode
|
||||
|
||||
# visual studio code
|
||||
.history
|
||||
*.log
|
||||
functions/*
|
||||
.temp/**
|
||||
|
||||
# umi
|
||||
.umi
|
||||
.umi-production
|
||||
|
||||
# screenshot
|
||||
screenshot
|
||||
.firebase
|
||||
.eslintcache
|
||||
|
||||
build
|
||||
|
||||
/public/version.js
|
||||
|
||||
23
webapp/packages/supersonic-fe/.prettierignore
Normal file
@@ -0,0 +1,23 @@
|
||||
**/*.svg
|
||||
package.json
|
||||
.umi
|
||||
.umi-production
|
||||
/dist
|
||||
.dockerignore
|
||||
.DS_Store
|
||||
.eslintignore
|
||||
*.png
|
||||
*.toml
|
||||
docker
|
||||
.editorconfig
|
||||
Dockerfile*
|
||||
.gitignore
|
||||
.prettierignore
|
||||
LICENSE
|
||||
.eslintcache
|
||||
*.lock
|
||||
yarn-error.log
|
||||
.history
|
||||
CNAME
|
||||
/build
|
||||
/public
|
||||
5
webapp/packages/supersonic-fe/.prettierrc.js
Normal file
@@ -0,0 +1,5 @@
|
||||
const fabric = require('@umijs/fabric');
|
||||
|
||||
module.exports = {
|
||||
...fabric.prettier,
|
||||
};
|
||||
5
webapp/packages/supersonic-fe/.stylelintrc.js
Normal file
@@ -0,0 +1,5 @@
|
||||
const fabric = require('@umijs/fabric');
|
||||
|
||||
module.exports = {
|
||||
...fabric.stylelint,
|
||||
};
|
||||
15
webapp/packages/supersonic-fe/.writeVersion.js
Normal file
@@ -0,0 +1,15 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { execSync } = require('child_process');
|
||||
|
||||
const commitId = execSync('git rev-parse HEAD').toString().trim();
|
||||
|
||||
const file = path.resolve(__dirname, './public/version.js');
|
||||
const data = {
|
||||
commitId: commitId,
|
||||
updateTime: new Date().toString(),
|
||||
};
|
||||
const feVersion = JSON.stringify(data, null, 4);
|
||||
// 异步写入数据到文件
|
||||
fs.writeFile(file, `feVersion=${feVersion}`, { encoding: 'utf8' }, (err) => {});
|
||||
console.log(`成功写入版本文件,版本信息为${feVersion}`);
|
||||
61
webapp/packages/supersonic-fe/README.md
Normal file
@@ -0,0 +1,61 @@
|
||||
# Ant Design Pro
|
||||
|
||||
This project is initialized with [Ant Design Pro](https://pro.ant.design). Follow is the quick guide for how to use.
|
||||
|
||||
## Environment Prepare
|
||||
|
||||
Install `node_modules`:
|
||||
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
or
|
||||
|
||||
```bash
|
||||
yarn
|
||||
```
|
||||
|
||||
## Provided Scripts
|
||||
|
||||
Ant Design Pro provides some useful script to help you quick start and build with web project, code style check and test.
|
||||
|
||||
Scripts provided in `package.json`. It's safe to modify or add additional script:
|
||||
|
||||
### Start project
|
||||
|
||||
```bash
|
||||
npm start
|
||||
```
|
||||
|
||||
### Build project
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
### Check code style
|
||||
|
||||
```bash
|
||||
npm run lint
|
||||
```
|
||||
|
||||
You can also use script to auto fix some lint error:
|
||||
|
||||
```bash
|
||||
npm run lint:fix
|
||||
```
|
||||
|
||||
### Test code
|
||||
|
||||
```bash
|
||||
npm test
|
||||
```
|
||||
|
||||
## More
|
||||
|
||||
You can view full document on our [official website](https://pro.ant.design). And welcome any feedback in our [github](https://github.com/ant-design/ant-design-pro).
|
||||
|
||||
#### 踩坑
|
||||
|
||||
1.antd `Select`组件如果默认不选中时默认值不是`undefeated`,则不显示 placeholder
|
||||
19
webapp/packages/supersonic-fe/coding_build/build_prd.sh
Normal file
@@ -0,0 +1,19 @@
|
||||
#!/bin/bash
|
||||
npm i
|
||||
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "npm i failed"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
npm run build
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "build failed"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
rm -rf dist.zip
|
||||
zip -r dist.zip ./dist/
|
||||
mkdir -p bin
|
||||
mv dist.zip bin/
|
||||
tar czf dist.tar.gz bin/dist.zip
|
||||
19
webapp/packages/supersonic-fe/coding_build/build_pre.sh
Normal file
@@ -0,0 +1,19 @@
|
||||
#!/bin/bash
|
||||
npm i
|
||||
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "npm i failed"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
npm run build:inner
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "build failed"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
rm -rf dist.zip
|
||||
zip -r dist.zip ./dist/
|
||||
mkdir -p bin
|
||||
mv dist.zip bin/
|
||||
tar czf dist.tar.gz bin/dist.zip
|
||||
83
webapp/packages/supersonic-fe/config/config.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
// https://umijs.org/config/
|
||||
import { defineConfig } from 'umi';
|
||||
import defaultSettings from './defaultSettings';
|
||||
import themeSettings from './themeSettings';
|
||||
import proxy from './proxy';
|
||||
import routes from './routes';
|
||||
import moment from 'moment';
|
||||
|
||||
const { REACT_APP_ENV, RUN_TYPE } = process.env;
|
||||
|
||||
const publicPath = '/webapp/';
|
||||
|
||||
export default defineConfig({
|
||||
define: {
|
||||
// 添加这个自定义的环境变量
|
||||
// 'process.env.REACT_APP_ENV': process.env.REACT_APP_ENV, // * REACT_APP_ENV 本地开发环境:dev,测试服:test,正式服:prod
|
||||
'process.env': {
|
||||
...process.env,
|
||||
API_BASE_URL: '/api/semantic/', // 直接在define中挂载裸露的全局变量还需要配置eslint,ts相关配置才能导致在使用中不会飘红,冗余较高,这里挂在进程环境下
|
||||
CHAT_API_BASE_URL: '/api/chat/',
|
||||
AUTH_API_BASE_URL: '/api/auth/',
|
||||
},
|
||||
},
|
||||
metas: [
|
||||
{
|
||||
name: 'app_version',
|
||||
content: moment().format('YYYY-MM-DD HH:mm:ss'),
|
||||
},
|
||||
],
|
||||
devServer: { port: 8002 },
|
||||
hash: true,
|
||||
// history: { type: 'hash' },
|
||||
antd: {},
|
||||
dva: {
|
||||
hmr: true,
|
||||
},
|
||||
layout: {
|
||||
name: '',
|
||||
locale: true,
|
||||
siderWidth: 208,
|
||||
...defaultSettings,
|
||||
},
|
||||
locale: {
|
||||
// default zh-CN
|
||||
default: 'zh-CN',
|
||||
antd: true,
|
||||
// default true, when it is true, will use `navigator.language` overwrite default
|
||||
baseNavigator: false,
|
||||
},
|
||||
// dynamicImport: {
|
||||
// loading: '@ant-design/pro-layout/es/PageLoading',
|
||||
// },
|
||||
targets: {
|
||||
ie: 11,
|
||||
},
|
||||
// umi routes: https://umijs.org/docs/routing
|
||||
routes,
|
||||
// Theme for antd: https://ant.design/docs/react/customize-theme-cn
|
||||
theme: {
|
||||
...themeSettings,
|
||||
},
|
||||
esbuild: {},
|
||||
title: false,
|
||||
ignoreMomentLocale: true,
|
||||
proxy: proxy[REACT_APP_ENV || 'dev'],
|
||||
manifest: {
|
||||
basePath: '/',
|
||||
},
|
||||
base: publicPath,
|
||||
publicPath,
|
||||
outputPath: RUN_TYPE === 'local' ? 'supersonic-webapp' : 'dist',
|
||||
// https://github.com/zthxxx/react-dev-inspector
|
||||
plugins: ['react-dev-inspector/plugins/umi/react-inspector'],
|
||||
inspectorConfig: {
|
||||
// loader options type and docs see below
|
||||
exclude: [],
|
||||
babelPlugins: [],
|
||||
babelOptions: {},
|
||||
},
|
||||
resolve: {
|
||||
includes: ['src/components'],
|
||||
},
|
||||
});
|
||||
26
webapp/packages/supersonic-fe/config/defaultSettings.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { Settings as LayoutSettings } from '@ant-design/pro-layout';
|
||||
|
||||
const Settings: LayoutSettings & {
|
||||
pwa?: boolean;
|
||||
logo?: string;
|
||||
} = {
|
||||
navTheme: 'light',
|
||||
primaryColor: '#296DF3',
|
||||
layout: 'mix',
|
||||
contentWidth: 'Fluid',
|
||||
fixedHeader: false,
|
||||
fixSiderbar: true,
|
||||
colorWeak: false,
|
||||
title: '',
|
||||
pwa: false,
|
||||
// logo: 'https://gw.alipayobjects.com/zos/rmsportal/KDpgvguMpGfqaHPjicRK.svg',
|
||||
iconfontUrl: '//at.alicdn.com/t/c/font_3201979_rncj6jun6k.js',
|
||||
splitMenus: true,
|
||||
menu: {
|
||||
defaultOpenAll: true,
|
||||
autoClose: false,
|
||||
ignoreFlatMenu: true,
|
||||
},
|
||||
};
|
||||
|
||||
export default Settings;
|
||||
16
webapp/packages/supersonic-fe/config/proxy.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
export default {
|
||||
dev: {
|
||||
'/api/chat/': {
|
||||
target: 'http://localhost:9080',
|
||||
changeOrigin: true,
|
||||
},
|
||||
'/api/semantic/': {
|
||||
target: 'http://localhost:9081',
|
||||
changeOrigin: true,
|
||||
},
|
||||
'/api/': {
|
||||
target: 'http://localhost:9080',
|
||||
changeOrigin: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
43
webapp/packages/supersonic-fe/config/routes.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
export const ROUTE_AUTH_CODES = {
|
||||
CHAT: 'chat',
|
||||
CHAT_SETTING: 'chatSetting',
|
||||
SEMANTIC: 'semantic',
|
||||
};
|
||||
|
||||
const ROUTES = [
|
||||
{
|
||||
path: '/chat',
|
||||
name: 'chat',
|
||||
component: './Chat',
|
||||
access: ROUTE_AUTH_CODES.CHAT,
|
||||
},
|
||||
{
|
||||
path: '/chatSetting',
|
||||
name: 'chatSetting',
|
||||
component: './SemanticModel/ChatSetting',
|
||||
access: ROUTE_AUTH_CODES.CHAT_SETTING,
|
||||
},
|
||||
{
|
||||
path: '/semanticModel',
|
||||
name: 'semanticModel',
|
||||
component: './SemanticModel/ProjectManager',
|
||||
access: ROUTE_AUTH_CODES.SEMANTIC,
|
||||
},
|
||||
{
|
||||
path: '/login',
|
||||
name: 'login',
|
||||
layout: false,
|
||||
hideInMenu: true,
|
||||
component: './Login',
|
||||
},
|
||||
{
|
||||
path: '/',
|
||||
redirect: '/chat',
|
||||
},
|
||||
{
|
||||
path: '/401',
|
||||
component: './401',
|
||||
},
|
||||
];
|
||||
|
||||
export default ROUTES;
|
||||
52
webapp/packages/supersonic-fe/config/themeSettings.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
// import defaultSettings from './defaultSettings';
|
||||
|
||||
const constants = {
|
||||
black85: 'rgba(0,10,36,0.85)',
|
||||
black65: 'rgba(0,10,36,0.65)',
|
||||
black45: 'rgba(0,10,36,0.45)',
|
||||
black25: 'rgba(0,10,36,0.25)',
|
||||
};
|
||||
|
||||
const settings = {
|
||||
// 'primary-color': defaultSettings.primaryColor,
|
||||
// Colors
|
||||
'blue-6': '#296DF3',
|
||||
'primary-color': '#296DF3',
|
||||
'green-6': '#26C992',
|
||||
'success-color': '#26C992',
|
||||
'red-5': '#EF4872',
|
||||
'error-color': '#EF4872',
|
||||
'gold-6': '#FFB924',
|
||||
'warning-color': '#FFB924',
|
||||
|
||||
// Color used by default to control hover and active backgrounds and for
|
||||
// alert info backgrounds.
|
||||
'primary-1': '#E3ECFD',
|
||||
'primary-2': '#BED2FB',
|
||||
'primary-3': '#86ACF8',
|
||||
'primary-4': '#6193F6',
|
||||
'primary-5': '#4E86F5',
|
||||
'primary-6': '#296DF3',
|
||||
'primary-7': '#0D57E8',
|
||||
'primary-8': '#0B49C3',
|
||||
'primary-9': '#093B9D',
|
||||
'primary-10': '#062666',
|
||||
|
||||
// Base Scaffolding Variables
|
||||
'heading-color': constants.black85,
|
||||
'text-color': constants.black85,
|
||||
'text-color-secondary': constants.black65,
|
||||
'border-radius-base': '4px',
|
||||
|
||||
// Buttons
|
||||
'btn-padding-horizontal-sm': '8px',
|
||||
'btn-padding-horizontal-base': '16px',
|
||||
'btn-padding-horizontal-lg': '16px',
|
||||
'btn-default-color': constants.black65,
|
||||
'btn-default-border': 'rgba(0,0,0,0.15)',
|
||||
'btn-disable-color': constants.black25,
|
||||
'btn-disable-border': 'rgba(0,10,36,0.15)',
|
||||
'btn-disable-bg': 'rgba(0,10,36,0.04)',
|
||||
};
|
||||
|
||||
export default settings;
|
||||
10
webapp/packages/supersonic-fe/jest.config.js
Normal file
@@ -0,0 +1,10 @@
|
||||
module.exports = {
|
||||
testURL: 'http://localhost:8000',
|
||||
testEnvironment: './tests/PuppeteerEnvironment',
|
||||
verbose: false,
|
||||
extraSetupFiles: ['./tests/setupTests.js'],
|
||||
globals: {
|
||||
ANT_DESIGN_PRO_ONLY_DO_NOT_USE_IN_YOUR_PRODUCTION: false,
|
||||
localStorage: null,
|
||||
},
|
||||
};
|
||||
10
webapp/packages/supersonic-fe/jsconfig.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"emitDecoratorMetadata": true,
|
||||
"experimentalDecorators": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
}
|
||||
}
|
||||
146
webapp/packages/supersonic-fe/package.json
Normal file
@@ -0,0 +1,146 @@
|
||||
{
|
||||
"name": "supersonic-fe",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"description": "data chat",
|
||||
"scripts": {
|
||||
"analyze": "cross-env ANALYZE=1 umi build",
|
||||
"build": "npm run build:os",
|
||||
"build:os": "node .writeVersion.js && cross-env REACT_APP_ENV=prod APP_TARGET=opensource umi build",
|
||||
"build:os-local": "node .writeVersion.js && cross-env REACT_APP_ENV=prod APP_TARGET=opensource RUN_TYPE=local umi build",
|
||||
"build:inner": "node .writeVersion.js && cross-env REACT_APP_ENV=prod APP_TARGET=inner umi build",
|
||||
"build:test": "node .writeVersion.js && cross-env REACT_APP_ENV=test umi build",
|
||||
"deploy": "npm run site && npm run gh-pages",
|
||||
"dev": "npm run start:osdev",
|
||||
"dev:os": "npm run start:osdev",
|
||||
"dev:inner": "npm run start:dev",
|
||||
"gh-pages": "gh-pages -d dist",
|
||||
"i18n-remove": "pro i18n-remove --locale=zh-CN --write",
|
||||
"postinstall": "umi g tmp",
|
||||
"lint": "umi g tmp && npm run lint:js && npm run lint:style && npm run lint:prettier",
|
||||
"lint-staged": "lint-staged",
|
||||
"lint-staged:js": "eslint --ext .js,.jsx,.ts,.tsx ",
|
||||
"lint:fix": "eslint --fix --cache --ext .js,.jsx,.ts,.tsx --format=pretty ./src && npm run lint:style",
|
||||
"lint:js": "eslint --cache --ext .js,.jsx,.ts,.tsx --format=pretty ./src",
|
||||
"lint:prettier": "prettier --check \"src/**/*\" --end-of-line auto",
|
||||
"lint:style": "stylelint --fix \"src/**/*.less\" --syntax less",
|
||||
"precommit": "lint-staged",
|
||||
"prettier": "prettier -c --write \"src/**/*\"",
|
||||
"start": "npm run start:osdev",
|
||||
"start:dev": "cross-env REACT_APP_ENV=dev MOCK=none APP_TARGET=inner umi dev",
|
||||
"start:osdev": "cross-env REACT_APP_ENV=dev PORT=9000 MOCK=none APP_TARGET=opensource umi dev",
|
||||
"start:no-mock": "cross-env MOCK=none umi dev",
|
||||
"start:no-ui": "cross-env UMI_UI=none umi dev",
|
||||
"start:pre": "cross-env REACT_APP_ENV=pre umi dev",
|
||||
"start:test": "cross-env REACT_APP_ENV=test MOCK=none umi dev",
|
||||
"pretest": "node ./tests/beforeTest",
|
||||
"test": "umi test",
|
||||
"test:all": "node ./tests/run-tests.js",
|
||||
"test:component": "umi test ./src/components",
|
||||
"tsc": "tsc --noEmit"
|
||||
},
|
||||
"lint-staged": {
|
||||
"**/*.less": "stylelint --syntax less",
|
||||
"**/*.{js,jsx,ts,tsx}": "npm run lint-staged:js",
|
||||
"**/*.{js,jsx,tsx,ts,less,md,json}": [
|
||||
"prettier --write"
|
||||
]
|
||||
},
|
||||
"browserslist": [
|
||||
"> 1%",
|
||||
"last 2 versions",
|
||||
"not ie <= 10"
|
||||
],
|
||||
"dependencies": {
|
||||
"@ant-design/charts": "^1.3.3",
|
||||
"@ant-design/icons": "^4.7.0",
|
||||
"@ant-design/pro-card": "^1.11.13",
|
||||
"@ant-design/pro-components": "^2.4.4",
|
||||
"@ant-design/pro-descriptions": "^1.0.19",
|
||||
"@ant-design/pro-form": "^1.23.0",
|
||||
"@ant-design/pro-layout": "^6.38.22",
|
||||
"@ant-design/pro-table": "^2.80.6",
|
||||
"@antv/g6": "^4.8.14",
|
||||
"@antv/layout": "^0.3.20",
|
||||
"@antv/xflow": "^1.0.55",
|
||||
"@babel/runtime": "^7.22.5",
|
||||
"supersonic-chat-sdk": "^0.1.6",
|
||||
"@types/numeral": "^2.0.2",
|
||||
"@types/react-draft-wysiwyg": "^1.13.2",
|
||||
"@types/react-syntax-highlighter": "^13.5.0",
|
||||
"@umijs/route-utils": "^1.0.33",
|
||||
"ace-builds": "^1.4.12",
|
||||
"ahooks": "^3.7.7",
|
||||
"antd": "^4.24.8",
|
||||
"classnames": "^2.2.6",
|
||||
"copy-to-clipboard": "^3.3.1",
|
||||
"cross-env": "^7.0.0",
|
||||
"crypto-js": "^4.0.0",
|
||||
"echarts": "^5.0.2",
|
||||
"echarts-for-react": "^3.0.1",
|
||||
"eslint-config-tencent": "^1.0.4",
|
||||
"jsencrypt": "^3.0.1",
|
||||
"lodash": "^4.17.11",
|
||||
"moment": "^2.29.1",
|
||||
"numeral": "^2.0.6",
|
||||
"omit.js": "^2.0.2",
|
||||
"path-to-regexp": "^2.4.0",
|
||||
"qs": "^6.9.0",
|
||||
"react": "^17.0.0",
|
||||
"react-ace": "^9.4.1",
|
||||
"react-dev-inspector": "^1.8.4",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-helmet-async": "^1.0.4",
|
||||
"react-spinners": "^0.10.6",
|
||||
"react-split-pane": "^2.0.3",
|
||||
"react-syntax-highlighter": "^15.4.3",
|
||||
"sql-formatter": "^2.3.3",
|
||||
"umi": "^3.2.14",
|
||||
"umi-request": "^1.0.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@ant-design/pro-cli": "^2.0.2",
|
||||
"@types/classnames": "^2.2.7",
|
||||
"@types/crypto-js": "^4.0.1",
|
||||
"@types/draftjs-to-html": "^0.8.0",
|
||||
"@types/echarts": "^4.9.4",
|
||||
"@types/express": "^4.17.0",
|
||||
"@types/history": "^4.7.2",
|
||||
"@types/jest": "^26.0.0",
|
||||
"@types/lodash": "^4.14.144",
|
||||
"@types/pinyin": "^2.8.3",
|
||||
"@types/qs": "^6.5.3",
|
||||
"@types/react": "^17.0.0",
|
||||
"@types/react-dom": "^17.0.0",
|
||||
"@types/react-helmet": "^6.1.0",
|
||||
"@umijs/fabric": "^2.4.0",
|
||||
"@umijs/plugin-blocks": "^2.0.5",
|
||||
"@umijs/plugin-esbuild": "^1.0.1",
|
||||
"@umijs/preset-ant-design-pro": "^1.2.0",
|
||||
"@umijs/preset-dumi": "^1.1.0-rc.6",
|
||||
"@umijs/preset-react": "^1.7.4",
|
||||
"@umijs/yorkie": "^2.0.3",
|
||||
"carlo": "^0.9.46",
|
||||
"cross-port-killer": "^1.1.1",
|
||||
"detect-installer": "^1.0.1",
|
||||
"eslint": "^7.1.0",
|
||||
"eslint-plugin-chalk": "^1.0.0",
|
||||
"eslint-plugin-import": "^2.27.5",
|
||||
"express": "^4.17.1",
|
||||
"gh-pages": "^3.0.0",
|
||||
"jsdom-global": "^3.0.2",
|
||||
"lint-staged": "^10.0.0",
|
||||
"prettier": "^2.3.1",
|
||||
"pro-download": "1.0.1",
|
||||
"puppeteer-core": "^5.0.0",
|
||||
"stylelint": "^13.0.0",
|
||||
"typescript": "^4.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
},
|
||||
"resolutions": {
|
||||
"@types/react": "17.0.0"
|
||||
},
|
||||
"__npminstall_done": false
|
||||
}
|
||||
1
webapp/packages/supersonic-fe/public/CNAME
Normal file
@@ -0,0 +1 @@
|
||||
preview.pro.ant.design
|
||||
BIN
webapp/packages/supersonic-fe/public/favicon.ico
Normal file
|
After Width: | Height: | Size: 551 B |
BIN
webapp/packages/supersonic-fe/public/home_bg.png
Normal file
|
After Width: | Height: | Size: 199 KiB |
BIN
webapp/packages/supersonic-fe/public/icons/icon-128x128.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
webapp/packages/supersonic-fe/public/icons/icon-192x192.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
webapp/packages/supersonic-fe/public/icons/icon-512x512.png
Normal file
|
After Width: | Height: | Size: 5.0 KiB |
1
webapp/packages/supersonic-fe/public/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="#1890ff"></path></svg>
|
||||
|
After Width: | Height: | Size: 953 B |
5
webapp/packages/supersonic-fe/public/pro_icon.svg
Normal file
@@ -0,0 +1,5 @@
|
||||
<svg width="42" height="42" xmlns="http://www.w3.org/2000/svg">
|
||||
<g>
|
||||
<path fill="#070707" d="m6.717392,13.773912l5.6,0c2.8,0 4.7,1.9 4.7,4.7c0,2.8 -2,4.7 -4.9,4.7l-2.5,0l0,4.3l-2.9,0l0,-13.7zm2.9,2.2l0,4.9l1.9,0c1.6,0 2.6,-0.9 2.6,-2.4c0,-1.6 -0.9,-2.4 -2.6,-2.4l-1.9,0l0,-0.1zm8.9,11.5l2.7,0l0,-5.7c0,-1.4 0.8,-2.3 2.2,-2.3c0.4,0 0.8,0.1 1,0.2l0,-2.4c-0.2,-0.1 -0.5,-0.1 -0.8,-0.1c-1.2,0 -2.1,0.7 -2.4,2l-0.1,0l0,-1.9l-2.7,0l0,10.2l0.1,0zm11.7,0.1c-3.1,0 -5,-2 -5,-5.3c0,-3.3 2,-5.3 5,-5.3s5,2 5,5.3c0,3.4 -1.9,5.3 -5,5.3zm0,-2.1c1.4,0 2.2,-1.1 2.2,-3.2c0,-2 -0.8,-3.2 -2.2,-3.2c-1.4,0 -2.2,1.2 -2.2,3.2c0,2.1 0.8,3.2 2.2,3.2z" class="st0" id="Ant-Design-Pro"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 677 B |
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"env": "semantic"
|
||||
}
|
||||
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
@@ -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
@@ -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
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
@@ -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";
|
||||
}
|
||||
|
||||
@@ -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
|
After Width: | Height: | Size: 97 KiB |
BIN
webapp/packages/supersonic-fe/src/components/S2Icon/iconfont.ttf
Normal file
@@ -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
@@ -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;
|
||||
|
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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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;
|
||||