first commit

This commit is contained in:
jerryjzhang
2023-06-12 18:44:01 +08:00
commit dc4fc69b57
879 changed files with 573090 additions and 0 deletions

View 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

View File

@@ -0,0 +1,8 @@
/lambda/
/scripts
/config
.history
public
dist
.umi
mock

View 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,
},
};

View 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

View 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

View File

@@ -0,0 +1,5 @@
const fabric = require('@umijs/fabric');
module.exports = {
...fabric.prettier,
};

View File

@@ -0,0 +1,5 @@
const fabric = require('@umijs/fabric');
module.exports = {
...fabric.stylelint,
};

View 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}`);

View 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

View 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

View 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

View 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中挂载裸露的全局变量还需要配置eslintts相关配置才能导致在使用中不会飘红冗余较高这里挂在进程环境下
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'],
},
});

View 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;

View 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,
},
},
};

View 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;

View 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;

View 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,
},
};

View File

@@ -0,0 +1,10 @@
{
"compilerOptions": {
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
}
}

View 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
}

View File

@@ -0,0 +1 @@
preview.pro.ant.design

Binary file not shown.

After

Width:  |  Height:  |  Size: 551 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 199 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

View 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

View 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

View File

@@ -0,0 +1,3 @@
{
"env": "semantic"
}

View 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;
}, {});
}

View 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,
};
};

View File

@@ -0,0 +1,6 @@
@import '~antd/lib/style/themes/default';
// main
@primary: #225ace;
@body-bg: #f0f2f5;
@border-radius-base: '4px';

View 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

View 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 = '请输入您的问题';

View File

@@ -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;

View File

@@ -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;
}
}
}

View File

@@ -0,0 +1,5 @@
export const formLayout: any = {
// labelCol: { span: 13 },
// wrapperCol: { span: 13 },
layout: 'vertical',
};

View File

@@ -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;
}
}
}

View File

@@ -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;

View File

@@ -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;
}
}

View File

@@ -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;

View File

@@ -0,0 +1,8 @@
import { createFromIconfontCN } from '@ant-design/icons';
import defaultSettings from '../../../config/defaultSettings';
const IconFont = createFromIconfontCN({
scriptUrl: defaultSettings.iconfontUrl,
});
export default IconFont;

View File

@@ -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;

View File

@@ -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;
}
}
}

View File

@@ -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;

View File

@@ -0,0 +1,340 @@
@font-face {
font-family: "iconfont"; /* Project id 2436113 */
src: url('iconfont.woff2?t=1659425018463') format('woff2'),
url('iconfont.woff?t=1659425018463') format('woff'),
url('iconfont.ttf?t=1659425018463') format('truetype'),
url('iconfont.svg?t=1659425018463#iconfont') format('svg');
}
.iconfont {
font-family: "iconfont" !important;
font-size: 16px;
font-style: normal;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.iconbaobiaokanban:before {
content: "\e66b";
}
.iconkanban:before {
content: "\e638";
}
.iconyunyingkanban:before {
content: "\e608";
}
.iconshujukanban1:before {
content: "\eb66";
}
.iconjingqingqidai01:before {
content: "\e607";
}
.icontouzi:before {
content: "\e67a";
}
.iconriqi:before {
content: "\e609";
}
.iconyinleren_:before {
content: "\e606";
}
.icondapan:before {
content: "\e668";
}
.iconbangdan:before {
content: "\e669";
}
.iconshujuwajue:before {
content: "\e667";
}
.iconshoucang1:before {
content: "\e600";
}
.icontianjiazhibiao:before {
content: "\e632";
}
.icontianjiafenzu:before {
content: "\e666";
}
.iconyouxiajiaogouxuan:before {
content: "\e8b7";
}
.iconxiaoshouzhibiaoshezhi:before {
content: "\e665";
}
.iconyingyongbiaoge:before {
content: "\e6ae";
}
.iconzhibiao:before {
content: "\e66a";
}
.iconsearch:before {
content: "\e7c9";
}
.iconfactory-color:before {
content: "\e69d";
}
.iconportray-color:before {
content: "\e69e";
}
.iconvisualize-color:before {
content: "\e69f";
}
.iconamount-color:before {
content: "\e68f";
}
.iconapi-color:before {
content: "\e690";
}
.iconcontent-color:before {
content: "\e691";
}
.iconbox-color:before {
content: "\e692";
}
.iconchat-color:before {
content: "\e693";
}
.iconclient-color:before {
content: "\e694";
}
.icondata-process:before {
content: "\e695";
}
.iconbi-color:before {
content: "\e696";
}
.iconfiled-color:before {
content: "\e697";
}
.iconinvoking-color:before {
content: "\e698";
}
.iconissue-color:before {
content: "\e699";
}
.iconplatform-color:before {
content: "\e69a";
}
.iconfile-color:before {
content: "\e69b";
}
.iconname-color:before {
content: "\e69c";
}
.icondraft:before {
content: "\e605";
}
.iconunknown:before {
content: "\e604";
}
.iconnormal:before {
content: "\e603";
}
.iconfreezed:before {
content: "\e602";
}
.iconlogowenzi:before {
content: "\e660";
}
.iconlogobiaoshi:before {
content: "\e664";
}
.iconchaoyinshuxitonglogo:before {
content: "\e663";
}
.iconzanwuquanxiandianjishenqing_1:before {
content: "\e662";
}
.iconqingchuangjianmuluhuokanban:before {
content: "\e661";
}
.iconzichan:before {
content: "\e65f";
}
.iconhangweifenxi:before {
content: "\e65e";
}
.iconshujuzichan:before {
content: "\e65d";
}
.iconshujukanban:before {
content: "\e659";
}
.iconshujujieru:before {
content: "\e65a";
}
.iconshujutansuo:before {
content: "\e65b";
}
.iconminjiefenxi:before {
content: "\e65c";
}
.iconyanfagongju:before {
content: "\e658";
}
.iconshujuanquan:before {
content: "\e614";
}
.iconCE:before {
content: "\e601";
}
.iconkanbantu-shuaxin:before {
content: "\e657";
}
.icondaohang-sousuo:before {
content: "\e63e";
}
.icondaohang-bangzhu:before {
content: "\e63f";
}
.iconkanbantu-fenxiang:before {
content: "\e640";
}
.iconquanju-riqi:before {
content: "\e641";
}
.icondaohang-shezhi:before {
content: "\e642";
}
.icondaohang-zichangouwuche:before {
content: "\e643";
}
.iconquanju-xiazai:before {
content: "\e644";
}
.iconkanbantu-quanping:before {
content: "\e645";
}
.iconshujuzichan-yewushujuzichan:before {
content: "\e646";
}
.iconshujukanban-tianjiakanban:before {
content: "\e647";
}
.iconqingkong:before {
content: "\e648";
}
.iconshujuzichan-jishushujuzichan:before {
content: "\e649";
}
.iconshujuzichan-zichanfaxian:before {
content: "\e64a";
}
.icontishi-beizhu1:before {
content: "\e64b";
}
.iconshujukanban-tianjiamulu:before {
content: "\e64c";
}
.icontubiao-zhuzhuangtu:before {
content: "\e64d";
}
.icondaohang-xiaoxitishi:before {
content: "\e64e";
}
.icontubiao-bingtu:before {
content: "\e64f";
}
.icontishi-beizhu2:before {
content: "\e650";
}
.iconshezhi-quanxianshezhi:before {
content: "\e651";
}
.iconhangweifenxi-mokuaifenxi:before {
content: "\e652";
}
.icontubiao-loudoutu:before {
content: "\e653";
}
.icontubiao-zhexiantu:before {
content: "\e654";
}
.icontubiao-biaoge:before {
content: "\e655";
}
.iconhangweifenxi-baobiaoliebiao:before {
content: "\e656";
}

File diff suppressed because one or more lines are too long

View File

@@ -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
}
]
}

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 97 KiB

View File

@@ -0,0 +1,3 @@
.s2icon {
line-height: 1;
}

View File

@@ -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;

View File

@@ -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;

View File

@@ -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',
});
}

View File

@@ -0,0 +1,9 @@
.userAvatar {
width: 24px;
height: 24px;
border-radius: 50%;
}
.userText {
margin-left: 10px;
}

View File

@@ -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;

View File

@@ -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`);
}

View File

@@ -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;
}

View File

@@ -0,0 +1,282 @@
/* eslint-disable */
import React, { useRef, useEffect, useCallback, useState, useMemo } from 'react';
import AceEditor, { IAceOptions } from 'react-ace';
import languageTools from 'ace-builds/src-min-noconflict/ext-language_tools';
import 'ace-builds/src-min-noconflict/ext-searchbox';
import 'ace-builds/src-min-noconflict/theme-sqlserver';
import 'ace-builds/src-min-noconflict/theme-monokai';
import 'ace-builds/src-min-noconflict/mode-sql';
import ReactAce, { IAceEditorProps } from 'react-ace/lib/ace';
import { Typography } from 'antd';
import { debounce } from 'lodash';
import FullScreen from '../FullScreen';
import styles from './index.less';
type TMode = 'sql' | 'mysql' | 'sqlserver';
enum EHintMeta {
table = 'table',
variable = 'variable',
column = 'column',
}
const DEFAULT_FONT_SIZE = '14px';
// const THEME_DEFAULT = 'sqlserver';
const MODE_DEFAULT = 'sql';
// const HEIGHT_DEFAULT = '300px';
const HEIGHT_DEFAULT = '100%';
const EDITOR_OPTIONS: IAceOptions = {
behavioursEnabled: true,
enableSnippets: false,
enableBasicAutocompletion: true,
enableLiveAutocompletion: true,
autoScrollEditorIntoView: true,
wrap: true,
useWorker: false,
};
export interface ISqlEditorProps {
hints?: { [name: string]: string[] };
value?: string;
height?: string;
/**
* 需引入对应的包 'ace-builds/src-min-noconflict/mode-${mode}'
*/
mode?: TMode;
/**
* 需引入对应的包 'ace-builds/src-min-noconflict/theme-${theme}'
*/
// theme?: TTheme;
isRightTheme?: boolean;
editorConfig?: IAceEditorProps;
sizeChanged?: number;
fullScreenBtnVisible?: boolean;
onSqlChange?: (sql: string) => void;
onChange?: (sql: string) => void;
onSelect?: (sql: string) => void;
onCmdEnter?: () => void;
}
/**
* Editor Component
* @param props ISqlEditorProps
*/
function SqlEditor(props: ISqlEditorProps) {
const refEditor = useRef<ReactAce>();
const {
hints = {},
value,
height = HEIGHT_DEFAULT,
mode = MODE_DEFAULT,
isRightTheme = false,
sizeChanged,
editorConfig,
fullScreenBtnVisible = true,
onSqlChange,
onChange,
onSelect,
onCmdEnter,
} = props;
const resize = useCallback(
debounce(() => {
refEditor.current?.editor.resize();
}, 300),
[],
);
const change = useCallback((sql: string) => {
onSqlChange?.(sql);
onChange?.(sql);
}, []);
const selectionChange = useCallback(
debounce((selection: any) => {
const rawSelectedQueryText: any = refEditor.current?.editor.session.doc.getTextRange(
selection.getRange(),
);
const selectedQueryText = rawSelectedQueryText?.length > 1 ? rawSelectedQueryText : null;
onSelect?.(selectedQueryText);
}, 300),
[],
);
const commands = useMemo(
() => [
{
name: 'execute',
bindKey: { win: 'Ctrl-Enter', mac: 'Command-Enter' },
exec: onCmdEnter,
},
],
[],
);
useEffect(() => {
resize();
}, [sizeChanged, height]);
useEffect(() => {
setHintsPopover(hints);
}, [hints]);
const [isSqlIdeFullScreen, setIsSqlIdeFullScreen] = useState<boolean>(false);
const handleNormalScreenSqlIde = () => {
setIsSqlIdeFullScreen(false);
// setSqlEditorHeight(getDefaultSqlEditorHeight(screenSize));
};
return (
<div className={styles.sqlEditor} style={{ height }}>
<FullScreen
isFullScreen={isSqlIdeFullScreen}
top={`${0}px`}
triggerBackToNormal={handleNormalScreenSqlIde}
>
<AceEditor
ref={refEditor}
name="aceEditor"
width="100%"
height="100%"
fontSize={DEFAULT_FONT_SIZE}
mode={mode}
theme={isRightTheme ? 'sqlserver' : 'monokai'}
value={value}
showPrintMargin={false}
highlightActiveLine={true}
setOptions={EDITOR_OPTIONS}
commands={commands as any}
onChange={change}
onSelectionChange={selectionChange}
// autoScrollEditorIntoView={true}
{...editorConfig}
/>
</FullScreen>
{fullScreenBtnVisible && (
<span
className={styles.fullScreenBtnBox}
onClick={() => {
setIsSqlIdeFullScreen(true);
}}
>
<Typography.Link></Typography.Link>
</span>
)}
</div>
);
}
interface ICompleters {
value: string;
name?: string;
caption?: string;
meta?: string;
type?: string;
score?: number;
}
function setHintsPopover(hints: ISqlEditorProps['hints']) {
const {
textCompleter,
keyWordCompleter,
// snippetCompleter,
setCompleters,
} = languageTools;
const customHintsCompleter = {
identifierRegexps: [/[a-zA-Z_0-9.\-\u00A2-\uFFFF]/],
getCompletions: (editor, session, pos, prefix, callback) => {
const { tableKeywords, tableColumnKeywords, variableKeywords, columns } =
formatCompleterFromHints(hints);
if (prefix[prefix.length - 1] === '.') {
const tableName = prefix.substring(0, prefix.length - 1);
const AliasTableColumnKeywords = genAliasTableColumnKeywords(editor, tableName, hints);
const hintList = tableKeywords.concat(
variableKeywords,
AliasTableColumnKeywords,
tableColumnKeywords[tableName] || [],
);
return callback(null, hintList);
}
callback(null, tableKeywords.concat(variableKeywords, columns));
},
};
const completers = [
textCompleter,
keyWordCompleter,
// snippetCompleter,
customHintsCompleter,
];
setCompleters(completers);
}
function formatCompleterFromHints(hints: ISqlEditorProps['hints']) {
const variableKeywords: ICompleters[] = [];
const tableKeywords: ICompleters[] = [];
const tableColumnKeywords: { [tableName: string]: ICompleters[] } = {};
const columns: ICompleters[] = [];
let score = 1000;
Object.keys(hints).forEach((key) => {
const meta: EHintMeta = isVariable(key) as any;
if (!meta) {
const { columnWithTableName, column } = genTableColumnKeywords(hints[key], key);
tableColumnKeywords[key] = columnWithTableName;
columns.push(...column);
tableKeywords.push({
name: key,
value: key,
score: score--,
meta: isTable(),
});
} else {
variableKeywords.push({ score: score--, value: key, meta });
}
});
return { tableKeywords, tableColumnKeywords, variableKeywords, columns };
}
function genTableColumnKeywords(table: string[], tableName: string) {
let score = 100;
const columnWithTableName: ICompleters[] = [];
const column: ICompleters[] = [];
table.forEach((columnVal) => {
const basis = { score: score--, meta: isColumn() };
columnWithTableName.push({
caption: `${tableName}.${columnVal}`,
name: `${tableName}.${columnVal}`,
value: `${tableName}.${columnVal}`,
...basis,
});
column.push({ value: columnVal, name: columnVal, ...basis });
});
return { columnWithTableName, column };
}
function genAliasTableColumnKeywords(
editor,
aliasTableName: string,
hints: ISqlEditorProps['hints'],
) {
const content = editor.getSession().getValue();
const tableName = Object.keys(hints).find((tableName) => {
const reg = new RegExp(`.+${tableName}\\s*(as|AS)?(?=\\s+${aliasTableName}\\s*)`, 'im');
return reg.test(content);
});
if (!tableName) {
return [];
}
const { columnWithTableName } = genTableColumnKeywords(hints[tableName], aliasTableName);
return columnWithTableName;
}
function isVariable(key: string) {
return key.startsWith('$') && key.endsWith('$') && EHintMeta.variable;
}
function isTable(key?: string) {
return EHintMeta.table;
}
function isColumn(key?: string) {
return EHintMeta.column;
}
export default SqlEditor;

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@@ -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;

View File

@@ -0,0 +1,42 @@
export * from './models/base';
type ObjToArrayParams = Record<string, string>;
const keyTypeTran = {
string: String,
number: Number,
};
/**
* obj转成valuelabel的数组
* @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;
};

View 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, // 看板
}

View 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;
}

View 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);
});
});
}
}

View 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;
};

View 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,
};

View File

@@ -0,0 +1,5 @@
export default {
'component.tagSelect.expand': '展开',
'component.tagSelect.collapse': '收起',
'component.tagSelect.all': '全部',
};

View File

@@ -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': '查看更多',
};

View 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': '问答对话',
};

View 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': '批量审批',
};

View File

@@ -0,0 +1,6 @@
export default {
'app.pwa.offline': '当前处于离线状态',
'app.pwa.serviceworker.updated': '有新内容',
'app.pwa.serviceworker.updated.hint': '请点击“刷新”按钮或者手动刷新页面',
'app.pwa.serviceworker.updated.ok': '刷新',
};

View File

@@ -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':
'配置栏只在开发环境用于预览,生产环境不会展现,请拷贝后手动修改配置文件',
};

View 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': '关',
};

View 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"
}
]
}

View 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;

View 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;

View File

@@ -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);

View File

@@ -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;
}

View 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);

View File

@@ -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));

View File

@@ -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;

View File

@@ -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;
}
}
}

View File

@@ -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;

View File

@@ -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;
}
}
}

View File

@@ -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;

View File

@@ -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;
}
}
}

View File

@@ -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;

View File

@@ -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;
}
}
}
}

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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);
}
}
}

View 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]: '维度值',
};

View 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;

Some files were not shown because too many files have changed in this diff Show More