feat: add RegisterResult component, ESLint config, and vue.config.js for project setup

- Created a new RegisterResult.vue component for displaying registration success.
- Added ESLint configuration for unit tests in the tests directory.
- Implemented vue.config.js for webpack configuration, including theme color support and proxy settings for development.
This commit is contained in:
dienakdz
2026-04-07 07:25:38 +07:00
parent 106419c955
commit cb49789bf4
232 changed files with 92025 additions and 0 deletions
+3
View File
@@ -0,0 +1,3 @@
> 1%
last 2 versions
not ie <= 10
+41
View File
@@ -0,0 +1,41 @@
# Dependencies
node_modules/
# Build output (会在容器内构建)
dist/
# IDE
.idea/
.vscode/
*.swp
*.swo
# Logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# Environment
.env
.env.local
.env.*.local
# Git
.git/
.gitignore
# Tests
tests/
coverage/
.nyc_output/
# Documentation
*.md
!README.md
# OS
.DS_Store
Thumbs.db
+39
View File
@@ -0,0 +1,39 @@
[*]
charset=utf-8
end_of_line=lf
insert_final_newline=false
indent_style=space
indent_size=2
[{*.ng,*.sht,*.html,*.shtm,*.shtml,*.htm}]
indent_style=space
indent_size=2
[{*.jhm,*.xslt,*.xul,*.rng,*.xsl,*.xsd,*.ant,*.tld,*.fxml,*.jrxml,*.xml,*.jnlp,*.wsdl}]
indent_style=space
indent_size=2
[{.babelrc,.stylelintrc,jest.config,.eslintrc,.prettierrc,*.json,*.jsb3,*.jsb2,*.bowerrc}]
indent_style=space
indent_size=2
[*.svg]
indent_style=space
indent_size=2
[*.js.map]
indent_style=space
indent_size=2
[*.less]
indent_style=space
indent_size=2
[*.vue]
indent_style=space
indent_size=2
[{.analysis_options,*.yml,*.yaml}]
indent_style=space
indent_size=2
+3
View File
@@ -0,0 +1,3 @@
NODE_ENV=development
VUE_APP_PREVIEW=true
VUE_APP_API_BASE_URL=/api
+3
View File
@@ -0,0 +1,3 @@
NODE_ENV=production
VUE_APP_PREVIEW=true
VUE_APP_API_BASE_URL=/api
+75
View File
@@ -0,0 +1,75 @@
module.exports = {
root: true,
env: {
node: true
},
'extends': [
'plugin:vue/strongly-recommended',
'@vue/standard'
],
rules: {
'no-console': 'off',
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',
'generator-star-spacing': 'off',
'no-mixed-operators': 0,
'vue/max-attributes-per-line': [
2,
{
'singleline': 5,
'multiline': {
'max': 1,
'allowFirstLine': false
}
}
],
'vue/attribute-hyphenation': 0,
'vue/html-self-closing': 0,
'vue/component-name-in-template-casing': 0,
'vue/html-closing-bracket-spacing': 0,
'vue/singleline-html-element-content-newline': 0,
'vue/no-unused-components': 0,
'vue/multiline-html-element-content-newline': 0,
'vue/no-use-v-if-with-v-for': 0,
'vue/html-closing-bracket-newline': 0,
'vue/no-parsing-error': 0,
'no-tabs': 0,
'quotes': [
2,
'single',
{
'avoidEscape': true,
'allowTemplateLiterals': true
}
],
'semi': [
2,
'never',
{
'beforeStatementContinuationChars': 'never'
}
],
'no-delete-var': 2,
'prefer-const': [
2,
{
'ignoreReadBeforeAssign': false
}
],
'template-curly-spacing': 'off',
'indent': 'off'
},
parserOptions: {
parser: 'babel-eslint'
},
overrides: [
{
files: [
'**/__tests__/*.{j,t}s?(x)',
'**/tests/unit/**/*.spec.{j,t}s?(x)'
],
env: {
jest: true
}
}
]
}
+5
View File
@@ -0,0 +1,5 @@
{
"rules": {
"space-before-function-paren": 0
}
}
+11
View File
@@ -0,0 +1,11 @@
public/* linguist-vendored
# Automatically normalize line endings (to LF) for all text-based files.
* text=auto eol=lf
# Declare files that will always have CRLF line endings on checkout.
*.{cmd,[cC][mM][dD]} text eol=crlf
*.{bat,[bB][aA][tT]} text eol=crlf
# Denote all files that are truly binary and should not be modified.
*.{ico,png,jpg,jpeg,gif,webp,svg,woff,woff2} binary
+21
View File
@@ -0,0 +1,21 @@
.DS_Store
node_modules
/dist
# local env files
.env.local
.env.*.local
# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw*
package-lock.json
+1
View File
@@ -0,0 +1 @@
_
+4
View File
@@ -0,0 +1,4 @@
{
"*.js": "eslint --fix",
"*.{css,less}": "stylelint --fix"
}
+6
View File
@@ -0,0 +1,6 @@
{
"printWidth": 120,
"semi": false,
"singleQuote": true,
"prettier.spaceBeforeFunctionParen": true
}
+102
View File
@@ -0,0 +1,102 @@
module.exports = {
processors: [],
plugins: ['stylelint-order'],
extends: [
'stylelint-config-standard',
'stylelint-config-css-modules'
],
rules: {
'selector-class-pattern': null,
'string-quotes': 'single', // 单引号
'at-rule-empty-line-before': null,
'at-rule-no-unknown': null,
'at-rule-name-case': 'lower', // 指定@规则名的大小写
'length-zero-no-unit': true, // 禁止零长度的单位(可自动修复)
'shorthand-property-no-redundant-values': true, // 简写属性
'number-leading-zero': 'never', // 小数不带0
'declaration-block-no-duplicate-properties': null, // 禁止声明快重复属性
'no-descending-specificity': null, // 禁止在具有较高优先级的选择器后出现被其覆盖的较低优先级的选择器。
'selector-max-id': 3, // 限制一个选择器中 ID 选择器的数量
'max-nesting-depth': 4,
'indentation': [2, { // 指定缩进 warning 提醒
'severity': 'warning'
}],
'order/properties-order': [ // 规则顺序
'position',
'top',
'right',
'bottom',
'left',
'z-index',
'display',
'float',
'width',
'height',
'max-width',
'max-height',
'min-width',
'min-height',
'padding',
'padding-top',
'padding-right',
'padding-bottom',
'padding-left',
'margin',
'margin-top',
'margin-right',
'margin-bottom',
'margin-left',
'margin-collapse',
'margin-top-collapse',
'margin-right-collapse',
'margin-bottom-collapse',
'margin-left-collapse',
'overflow',
'overflow-x',
'overflow-y',
'clip',
'clear',
'font',
'font-family',
'font-size',
'font-smoothing',
'osx-font-smoothing',
'font-style',
'font-weight',
'line-height',
'letter-spacing',
'word-spacing',
'color',
'text-align',
'text-decoration',
'text-indent',
'text-overflow',
'text-rendering',
'text-size-adjust',
'text-shadow',
'text-transform',
'word-break',
'word-wrap',
'white-space',
'vertical-align',
'list-style',
'list-style-type',
'list-style-position',
'list-style-image',
'pointer-events',
'cursor',
'background',
'background-color',
'border',
'border-radius',
'content',
'outline',
'outline-offset',
'opacity',
'filter',
'visibility',
'size',
'transform'
]
}
}
+7
View File
@@ -0,0 +1,7 @@
language: node_js
node_js:
- 10.15.0
cache: yarn
script:
- yarn
- yarn run lint --no-fix && yarn run build
+32
View File
@@ -0,0 +1,32 @@
# QuantDinger Frontend Dockerfile
# Stage 1: Build
FROM node:18-alpine as builder
WORKDIR /app
# Copy package files
COPY package*.json ./
# Install dependencies (prefer npm)
RUN npm install --legacy-peer-deps
# Copy source code
COPY . .
# Build production version
RUN npm run build
# Stage 2: Production image (using nginx)
# Use specific version to avoid mirror registry issues
FROM nginx:1.25-alpine
# Copy build artifacts
COPY --from=builder /app/dist /usr/share/nginx/html
# Copy nginx configuration
COPY deploy/nginx-docker.conf /etc/nginx/conf.d/default.conf
# Expose port
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
+69
View File
@@ -0,0 +1,69 @@
# QuantDinger Web UI (Vue 2)
This `frontend_vue` folder was restored from the last public frontend source commit in `brokermr810/QuantDinger`:
- Source baseline: `ffdd2ff` on February 27, 2026
- Frontend became closed-source at: `abbad97` on February 27, 2026
- Current bundled frontend in this repo: [`frontend/dist`](/e:/DinQuant/frontend/dist) version `3.0.1`
This means `frontend_vue` is the closest full editable source available from git history. It builds successfully locally, but it is not guaranteed to be byte-for-byte identical to the newer private `frontend/dist` bundle.
This is the QuantDinger frontend web UI built with **Vue 2** + **Ant Design Vue**. It connects to the Python backend (`backend_api_python/`) through HTTP APIs to provide charts, indicators, backtests, AI analysis, and strategy management.
> This UI is based on the open-source `ant-design-vue-pro` ecosystem, heavily adapted for QuantDinger.
## What you get
- **Dashboards**: summary views and operational panels
- **Indicator analysis**: Kline charts + indicator editing + backtest history
- **AI analysis**: multi-agent reports (optional LLM/search, configured on backend)
- **Trading assistant**: strategy lifecycle + positions/records (depending on backend capability)
- **Local auth**: login with backend-configured admin credentials
## Quick start (local development)
### Prerequisites
- Node.js 16+ recommended
- Backend running at `http://localhost:5000` (see `backend_api_python/README.md`)
### 1) Install dependencies
```bash
cd frontend_vue
corepack yarn install --ignore-engines
```
### 2) Start dev server
```bash
corepack yarn serve
```
Dev server runs at `http://localhost:8000`.
### 3) API proxy (important)
In dev mode, this project proxies `/api/*` to the backend:
- Proxy config: `frontend_vue/vue.config.js`
- Default target: `http://localhost:5000`
If your backend runs on a different host/port, update `vue.config.js` accordingly.
## Production build
```bash
corepack yarn build
```
The output will be generated under `frontend_vue/dist/`.
## Notes
- **CORS**: when using the dev proxy, you typically dont need extra CORS config.
- **Login**: use the credentials defined in `backend_api_python/.env` (`ADMIN_USER` / `ADMIN_PASSWORD`).
## License
Apache License 2.0. See repository root `LICENSE`.
+51
View File
@@ -0,0 +1,51 @@
# Frontend Restore Notes
## Provenance
- Original upstream repo: `https://github.com/brokermr810/QuantDinger`
- Last public frontend source commit: `ffdd2ffbae99cd77f90c9c9facd6f290d033dd2b`
- Commit date: February 27, 2026
- Commit that removed public frontend source: `abbad97bddfb1b3e72275f07158f8e30290b5a37`
- Removal date: February 27, 2026
## What This Folder Is
`frontend_vue` is a reconstructed editable frontend source tree created from the last public source commit before the project switched to a prebuilt private frontend.
It is intended to be:
- a full Vue source tree you can modify
- much closer to the official project than the older `feature/multi-user-postgresql` import
- compatible enough to build locally against the current backend layout
## What It Is Not
Because the newer frontend is only published as [`frontend/dist`](/e:/DinQuant/frontend/dist) and does not include source maps, this folder cannot be proven identical to the private 3.0.1 source used to produce that bundle.
In practice:
- `frontend_vue` is the last verifiable public source
- `frontend/dist` is the latest bundled UI artifact
- newer UI changes after February 27, 2026 must be inferred manually from the bundle
## Verification Done Locally
The restored source was verified with:
```bash
cd frontend_vue
corepack yarn install --ignore-engines
corepack yarn build
```
The build completed successfully on April 6, 2026 in this workspace.
## Recommended Workflow
1. Use `frontend_vue` as your editable source tree.
2. Keep `frontend/dist` as the behavioral reference for the newer private frontend.
3. When you need parity with a specific screen in `frontend/dist`, port it screen-by-screen into `frontend_vue`.
## Current Limitation
The rebuilt `frontend_vue/dist` is buildable, but its generated asset set does not exactly match the official `frontend/dist` asset hashes and chunk layout. That is expected because the official frontend continued evolving privately after the public source was removed.
+30
View File
@@ -0,0 +1,30 @@
const IS_PROD = ['production', 'prod'].includes(process.env.NODE_ENV)
const IS_PREVIEW = process.env.VUE_APP_PREVIEW === 'true'
const plugins = []
if (IS_PROD && !IS_PREVIEW) {
// 去除日志的插件,
plugins.push('transform-remove-console')
}
// lazy load ant-design-vue
// if your use import on Demand, Use this code
plugins.push(['import', {
'libraryName': 'ant-design-vue',
'libraryDirectory': 'es',
'style': true // `style: true` 会加载 less 文件
}])
module.exports = {
presets: [
'@vue/cli-plugin-babel/preset',
[
'@babel/preset-env',
{
'useBuiltIns': 'entry',
'corejs': 3
}
]
],
plugins
}
+27
View File
@@ -0,0 +1,27 @@
# PowerShell script for building Docker image with registry fallback
Write-Host "Building frontend Docker image..." -ForegroundColor Green
# Try to build with --pull flag to force pull from official registry
$buildResult = docker build `
--pull `
--platform linux/amd64 `
-t quantdinger-frontend:latest `
-f Dockerfile `
.
if ($LASTEXITCODE -ne 0) {
Write-Host "Build failed, trying with no-cache..." -ForegroundColor Yellow
docker build `
--no-cache `
--platform linux/amd64 `
-t quantdinger-frontend:latest `
-f Dockerfile `
.
}
if ($LASTEXITCODE -eq 0) {
Write-Host "Build successful!" -ForegroundColor Green
} else {
Write-Host "Build failed. Please check Docker registry configuration." -ForegroundColor Red
}
+21
View File
@@ -0,0 +1,21 @@
#!/bin/bash
# Docker build script with registry fallback
# Try to build with --pull flag to force pull from official registry
echo "Building frontend Docker image..."
docker build \
--pull \
--platform linux/amd64 \
-t quantdinger-frontend:latest \
-f Dockerfile \
.
if [ $? -ne 0 ]; then
echo "Build failed, trying with no-cache..."
docker build \
--no-cache \
--platform linux/amd64 \
-t quantdinger-frontend:latest \
-f Dockerfile \
.
fi
+26
View File
@@ -0,0 +1,26 @@
/**
* feat:新增功能
* fixbug 修复
* docs:文档更新
* style:不影响程序逻辑的代码修改(修改空白字符,格式缩进,补全缺失的分号等,没有改变代码逻辑)
* refactor:重构代码(既没有新增功能,也没有修复 bug)
* perf:性能, 体验优化
* test:新增测试用例或是更新现有测试
* build:主要目的是修改项目构建系统(例如 glup,webpackrollup 的配置等)的提交
* ci:主要目的是修改项目继续集成流程(例如 TravisJenkinsGitLab CICircle等)的提交
* chore:不属于以上类型的其他类型,比如构建流程, 依赖管理
* revert:回滚某个更早之前的提交
*/
module.exports = {
extends: ['@commitlint/config-conventional'],
rules: {
'type-enum': [
2,
'always',
['feat', 'fix', 'docs', 'style', 'refactor', 'test', 'chore', 'revert']
],
'subject-full-stop': [0, 'never'],
'subject-case': [0, 'never']
}
}
+49
View File
@@ -0,0 +1,49 @@
const ThemeColorReplacer = require('webpack-theme-color-replacer')
const generate = require('@ant-design/colors/lib/generate').default
const getAntdSerials = (color) => {
// 淡化(即less的tint
const lightens = new Array(9).fill().map((t, i) => {
return ThemeColorReplacer.varyColor.lighten(color, i / 10)
})
const colorPalettes = generate(color)
const rgb = ThemeColorReplacer.varyColor.toNum3(color.replace('#', '')).join(',')
return lightens.concat(colorPalettes).concat(rgb)
}
const themePluginOption = {
fileName: 'css/theme-colors-[contenthash:8].css',
matchColors: getAntdSerials('#1890ff'), // 主色系列
// 改变样式选择器,解决样式覆盖问题
changeSelector (selector) {
switch (selector) {
case '.ant-calendar-today .ant-calendar-date':
return ':not(.ant-calendar-selected-date):not(.ant-calendar-selected-day)' + selector
case '.ant-btn:focus,.ant-btn:hover':
return '.ant-btn:focus:not(.ant-btn-primary):not(.ant-btn-danger),.ant-btn:hover:not(.ant-btn-primary):not(.ant-btn-danger)'
case '.ant-btn.active,.ant-btn:active':
return '.ant-btn.active:not(.ant-btn-primary):not(.ant-btn-danger),.ant-btn:active:not(.ant-btn-primary):not(.ant-btn-danger)'
case '.ant-steps-item-process .ant-steps-item-icon > .ant-steps-icon':
case '.ant-steps-item-process .ant-steps-item-icon>.ant-steps-icon':
return ':not(.ant-steps-item-process)' + selector
// fixed https://github.com/vueComponent/ant-design-vue-pro/issues/876
case '.ant-steps-item-process .ant-steps-item-icon':
return ':not(.ant-steps-item-custom)' + selector
case '.ant-menu-horizontal>.ant-menu-item-active,.ant-menu-horizontal>.ant-menu-item-open,.ant-menu-horizontal>.ant-menu-item-selected,.ant-menu-horizontal>.ant-menu-item:hover,.ant-menu-horizontal>.ant-menu-submenu-active,.ant-menu-horizontal>.ant-menu-submenu-open,.ant-menu-horizontal>.ant-menu-submenu-selected,.ant-menu-horizontal>.ant-menu-submenu:hover':
case '.ant-menu-horizontal > .ant-menu-item-active,.ant-menu-horizontal > .ant-menu-item-open,.ant-menu-horizontal > .ant-menu-item-selected,.ant-menu-horizontal > .ant-menu-item:hover,.ant-menu-horizontal > .ant-menu-submenu-active,.ant-menu-horizontal > .ant-menu-submenu-open,.ant-menu-horizontal > .ant-menu-submenu-selected,.ant-menu-horizontal > .ant-menu-submenu:hover':
return '.ant-menu-horizontal > .ant-menu-item-active,.ant-menu-horizontal > .ant-menu-item-open,.ant-menu-horizontal > .ant-menu-item-selected,.ant-menu-horizontal:not(.ant-menu-dark) > .ant-menu-item:hover,.ant-menu-horizontal > .ant-menu-submenu-active,.ant-menu-horizontal > .ant-menu-submenu-open,.ant-menu-horizontal:not(.ant-menu-dark) > .ant-menu-submenu-selected,.ant-menu-horizontal:not(.ant-menu-dark) > .ant-menu-submenu:hover'
case '.ant-menu-horizontal > .ant-menu-item-selected > a':
case '.ant-menu-horizontal>.ant-menu-item-selected>a':
return '.ant-menu-horizontal:not(ant-menu-light):not(.ant-menu-dark) > .ant-menu-item-selected > a'
case '.ant-menu-horizontal > .ant-menu-item > a:hover':
case '.ant-menu-horizontal>.ant-menu-item>a:hover':
return '.ant-menu-horizontal:not(ant-menu-light):not(.ant-menu-dark) > .ant-menu-item > a:hover'
default :
return selector
}
}
}
const createThemeColorReplacerPlugin = () => new ThemeColorReplacer(themePluginOption)
module.exports = createThemeColorReplacerPlugin
+115
View File
@@ -0,0 +1,115 @@
export default {
theme: [
{
key: 'dark',
fileName: 'dark.css',
theme: 'dark'
},
{
key: '#F5222D',
fileName: '#F5222D.css',
modifyVars: {
'@primary-color': '#F5222D'
}
},
{
key: '#FA541C',
fileName: '#FA541C.css',
modifyVars: {
'@primary-color': '#FA541C'
}
},
{
key: '#FAAD14',
fileName: '#FAAD14.css',
modifyVars: {
'@primary-color': '#FAAD14'
}
},
{
key: '#13C2C2',
fileName: '#13C2C2.css',
modifyVars: {
'@primary-color': '#13C2C2'
}
},
{
key: '#52C41A',
fileName: '#52C41A.css',
modifyVars: {
'@primary-color': '#52C41A'
}
},
{
key: '#2F54EB',
fileName: '#2F54EB.css',
modifyVars: {
'@primary-color': '#2F54EB'
}
},
{
key: '#722ED1',
fileName: '#722ED1.css',
modifyVars: {
'@primary-color': '#722ED1'
}
},
{
key: '#F5222D',
theme: 'dark',
fileName: 'dark-#F5222D.css',
modifyVars: {
'@primary-color': '#F5222D'
}
},
{
key: '#FA541C',
theme: 'dark',
fileName: 'dark-#FA541C.css',
modifyVars: {
'@primary-color': '#FA541C'
}
},
{
key: '#FAAD14',
theme: 'dark',
fileName: 'dark-#FAAD14.css',
modifyVars: {
'@primary-color': '#FAAD14'
}
},
{
key: '#13C2C2',
theme: 'dark',
fileName: 'dark-#13C2C2.css',
modifyVars: {
'@primary-color': '#13C2C2'
}
},
{
key: '#52C41A',
theme: 'dark',
fileName: 'dark-#52C41A.css',
modifyVars: {
'@primary-color': '#52C41A'
}
},
{
key: '#2F54EB',
theme: 'dark',
fileName: 'dark-#2F54EB.css',
modifyVars: {
'@primary-color': '#2F54EB'
}
},
{
key: '#722ED1',
theme: 'dark',
fileName: 'dark-#722ED1.css',
modifyVars: {
'@primary-color': '#722ED1'
}
}
]
}
+9
View File
@@ -0,0 +1,9 @@
0.0.0.0:80 {
gzip
root /usr/share/nginx/html
rewrite {
r .*
to {path} /
}
}
+43
View File
@@ -0,0 +1,43 @@
server {
listen 80;
server_name localhost;
root /usr/share/nginx/html;
index index.html;
# Gzip compression
gzip on;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
gzip_min_length 1000;
# Static asset caching
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
# API proxy to backend
location /api/ {
proxy_pass http://backend:5000/api/;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_cache_bypass $http_upgrade;
proxy_read_timeout 300s;
proxy_connect_timeout 75s;
}
# SPA routing support
location / {
try_files $uri $uri/ /index.html;
}
# Health check
location /health {
return 200 'OK';
add_header Content-Type text/plain;
}
}
+24
View File
@@ -0,0 +1,24 @@
server {
listen 80;
server_name _;
# gzip config
gzip on;
gzip_min_length 1k;
gzip_comp_level 6;
gzip_types text/plain text/css text/javascript application/json application/javascript application/x-javascript application/xml;
gzip_vary on;
gzip_disable "MSIE [1-6]\.";
root /usr/share/nginx/html;
include /etc/nginx/mime.types;
location / {
try_files $uri $uri/ /index.html;
}
# location /api {
# proxy_pass https://preview.pro.antdv.com/api;
# proxy_set_header X-Forwarded-Proto $scheme;
# proxy_set_header X-Real-IP $remote_addr;
# }
}
+23
View File
@@ -0,0 +1,23 @@
module.exports = {
moduleFileExtensions: [
'js',
'jsx',
'json',
'vue'
],
transform: {
'^.+\\.vue$': 'vue-jest',
'.+\\.(css|styl|less|sass|scss|svg|png|jpg|ttf|woff|woff2)$': 'jest-transform-stub',
'^.+\\.jsx?$': 'babel-jest'
},
moduleNameMapper: {
'^@/(.*)$': '<rootDir>/src/$1'
},
snapshotSerializers: [
'jest-serializer-vue'
],
testMatch: [
'**/tests/unit/**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)'
],
testURL: 'http://localhost/'
}
+11
View File
@@ -0,0 +1,11 @@
{
"compilerOptions": {
"target": "es6",
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
},
"exclude": ["node_modules", "dist"],
"include": ["src/**/*"]
}
+84
View File
@@ -0,0 +1,84 @@
{
"name": "vue-antd-pro",
"version": "3.0.4",
"private": true,
"scripts": {
"serve": "vue-cli-service serve --no-lint",
"build": "vue-cli-service build --no-lint",
"test:unit": "vue-cli-service test:unit",
"lint": "vue-cli-service lint",
"build:preview": "vue-cli-service build --no-module --mode preview",
"lint:nofix": "vue-cli-service lint --no-fix",
"lint:js": "eslint src/**/*.js --fix",
"lint:css": "stylelint src/**/*.*ss --fix --custom-syntax postcss-less"
},
"dependencies": {
"@ant-design-vue/pro-layout": "^1.0.12",
"@antv/data-set": "^0.10.2",
"@iconify/vue2": "^2.1.0",
"ant-design-vue": "^1.7.8",
"axios": "^0.26.1",
"babel-loader": "8",
"codemirror": "^5.65.16",
"core-js": "^3.21.1",
"crypto-js": "^4.2.0",
"echarts": "^6.0.0",
"enquire.js": "^2.1.6",
"klinecharts": "^9.8.0",
"lightweight-charts": "^5.0.8",
"lodash.clonedeep": "^4.5.0",
"lodash.get": "^4.4.2",
"lodash.pick": "^4.4.0",
"md5": "^2.3.0",
"mockjs2": "1.0.8",
"moment": "^2.29.2",
"nprogress": "^0.2.0",
"store": "^2.0.12",
"viser-vue": "^2.4.8",
"vue": "^2.6.14",
"vue-clipboard2": "^0.2.1",
"vue-cropper": "0.4.9",
"vue-i18n": "^8.27.1",
"vue-quill-editor": "^3.0.6",
"vue-router": "^3.5.3",
"vue-svg-component-runtime": "^1.0.1",
"vue-template-compiler": "^2.6.14",
"vuex": "^3.6.2",
"wangeditor": "^3.1.1"
},
"devDependencies": {
"@ant-design/colors": "^3.2.2",
"@vue/babel-helper-vue-jsx-merge-props": "^1.2.1",
"@vue/cli-plugin-babel": "~5.0.8",
"@vue/cli-plugin-eslint": "~5.0.8",
"@vue/cli-plugin-router": "~5.0.8",
"@vue/cli-plugin-unit-jest": "~5.0.8",
"@vue/cli-plugin-vuex": "~5.0.8",
"@vue/cli-service": "~5.0.8",
"@vue/eslint-config-standard": "^4.0.0",
"@vue/test-utils": "^1.3.0",
"babel-eslint": "^10.1.0",
"babel-plugin-import": "^1.13.3",
"babel-plugin-transform-remove-console": "^6.9.4",
"eslint": "^7.32.0",
"eslint-plugin-html": "^6.2.0",
"eslint-plugin-vue": "^7.20.0",
"file-loader": "^6.2.0",
"git-revision-webpack-plugin": "^3.0.6",
"less": "^3.13.1",
"less-loader": "^5.0.0",
"postcss": "^8.3.5",
"postcss-less": "^6.0.0",
"regenerator-runtime": "^0.13.9",
"stylelint": "^14.8.5",
"stylelint-config-css-modules": "^4.1.0",
"stylelint-config-recess-order": "^3.0.0",
"stylelint-config-recommended": "^7.0.0",
"stylelint-config-standard": "^25.0.0",
"stylelint-order": "^5.0.0",
"vue-svg-icon-loader": "^2.1.1",
"vue-svg-loader": "0.16.0",
"webpack-theme-color-replacer": "^1.3.26"
},
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
}
+11874
View File
File diff suppressed because it is too large Load Diff
+5
View File
@@ -0,0 +1,5 @@
module.exports = {
plugins: {
autoprefixer: {}
}
}
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 131 KiB

+477
View File
@@ -0,0 +1,477 @@
<!DOCTYPE html>
<html lang="zh-cmn-Hans">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="<%= BASE_URL %>slogo.png">
<title>QuantDinger</title>
<style>
.first-loading-wrp {
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
min-height: 420px;
height: 100vh;
background: #fff;
position: relative;
overflow: hidden;
}
.first-loading-wrp > h2 {
font-size: 32px;
margin-bottom: 40px;
color: #333;
font-weight: 600;
}
.first-loading-wrp .loading-wrp {
padding: 40px;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
position: relative;
width: 100%;
max-width: 600px;
}
/* 像素风格小猫奔跑动画容器 */
.pixel-cat-container {
position: relative;
width: 100%;
height: 120px;
margin: 0 auto 30px;
overflow: hidden;
}
/* 像素小猫主体 */
.pixel-cat {
position: absolute;
left: 0;
bottom: 24px;
width: 32px;
height: 32px;
animation: catRun 0.4s steps(2) infinite, catMove 3.5s linear infinite;
image-rendering: pixelated;
image-rendering: -moz-crisp-edges;
image-rendering: crisp-edges;
}
/* 所有像素元素都使用锐利边缘 */
.pixel-cat * {
image-rendering: pixelated;
image-rendering: -moz-crisp-edges;
image-rendering: crisp-edges;
}
/* 猫头 - 像素方块组成 */
.cat-head {
position: absolute;
left: 2px;
top: 0;
width: 16px;
height: 14px;
background:
/* 头部主体白色 */
linear-gradient(#fff, #fff) 2px 4px / 12px 10px no-repeat,
/* 左半脸黑色斑块 */
linear-gradient(#000, #000) 2px 4px / 6px 10px no-repeat,
/* 头顶 */
linear-gradient(#fff, #fff) 4px 2px / 8px 2px no-repeat;
animation: headBob 0.4s steps(2) infinite;
}
/* 左耳 - 黑色尖耳朵 */
.cat-ear-left {
position: absolute;
left: 0px;
top: -2px;
width: 4px;
height: 6px;
background:
linear-gradient(#000, #000) 0px 4px / 4px 2px no-repeat,
linear-gradient(#000, #000) 1px 2px / 2px 2px no-repeat,
linear-gradient(#000, #000) 1px 0px / 2px 2px no-repeat;
}
/* 右耳 - 白色尖耳朵 */
.cat-ear-right {
position: absolute;
right: 0px;
top: -2px;
width: 4px;
height: 6px;
background:
linear-gradient(#fff, #fff) 0px 4px / 4px 2px no-repeat,
linear-gradient(#fff, #fff) 1px 2px / 2px 2px no-repeat,
linear-gradient(#fff, #fff) 1px 0px / 2px 2px no-repeat;
box-shadow: inset 0 0 0 1px #000;
}
.cat-ear-right::after {
content: '';
position: absolute;
width: 4px;
height: 6px;
border: 1px solid #000;
border-width: 0 1px 0 0;
box-sizing: border-box;
}
/* 左眼 - 黑底白眼(在黑色区域) */
.cat-eye-left {
position: absolute;
left: 4px;
top: 6px;
width: 4px;
height: 4px;
background: #fff;
}
.cat-eye-left::after {
content: '';
position: absolute;
left: 2px;
top: 1px;
width: 2px;
height: 2px;
background: #000;
}
/* 右眼 - 白底黑眼(在白色区域) */
.cat-eye-right {
position: absolute;
right: 2px;
top: 6px;
width: 4px;
height: 4px;
background: #fff;
border: 1px solid #000;
box-sizing: border-box;
}
.cat-eye-right::after {
content: '';
position: absolute;
left: 1px;
top: 0px;
width: 2px;
height: 2px;
background: #000;
}
/* 鼻子 - 小粉色方块 */
.cat-nose {
position: absolute;
left: 7px;
top: 10px;
width: 2px;
height: 2px;
background: #000;
}
/* 胡须 - 像素线条 */
.cat-whiskers {
position: absolute;
left: 0;
top: 10px;
width: 16px;
height: 4px;
}
.cat-whiskers::before {
content: '';
position: absolute;
left: -4px;
top: 0;
width: 4px;
height: 1px;
background: #000;
box-shadow: 0 2px 0 #000;
}
.cat-whiskers::after {
content: '';
position: absolute;
right: -4px;
top: 0;
width: 4px;
height: 1px;
background: #000;
box-shadow: 0 2px 0 #000;
}
/* 身体 - 黑白相间像素块 */
.cat-body {
position: absolute;
left: 4px;
top: 14px;
width: 14px;
height: 10px;
background:
/* 白色部分 */
linear-gradient(#fff, #fff) 6px 0 / 8px 10px no-repeat,
/* 黑色部分 */
linear-gradient(#000, #000) 0 0 / 8px 10px no-repeat;
border: 1px solid #000;
box-sizing: border-box;
}
/* 前腿 - 左 (黑色) */
.cat-leg-front-left {
position: absolute;
left: 6px;
top: 22px;
width: 3px;
height: 8px;
background: #000;
animation: legFront 0.2s steps(2) infinite;
}
/* 前腿 - 右 (白色带边框) */
.cat-leg-front-right {
position: absolute;
left: 12px;
top: 22px;
width: 3px;
height: 8px;
background: #fff;
border: 1px solid #000;
box-sizing: border-box;
animation: legFront 0.2s steps(2) infinite 0.1s;
}
/* 后腿 - 左 (白色带边框) */
.cat-leg-back-left {
position: absolute;
left: 3px;
top: 22px;
width: 3px;
height: 8px;
background: #fff;
border: 1px solid #000;
box-sizing: border-box;
animation: legBack 0.2s steps(2) infinite 0.1s;
}
/* 后腿 - 右 (黑色) */
.cat-leg-back-right {
position: absolute;
left: 15px;
top: 22px;
width: 3px;
height: 8px;
background: #000;
animation: legBack 0.2s steps(2) infinite;
}
/* 尾巴 - 长且弯曲的像素尾巴 */
.cat-tail {
position: absolute;
right: -10px;
top: 10px;
width: 12px;
height: 10px;
background:
/* 尾巴根部 */
linear-gradient(#000, #000) 0 6px / 3px 3px no-repeat,
/* 尾巴中部 */
linear-gradient(#000, #000) 3px 4px / 3px 3px no-repeat,
/* 尾巴弯曲 */
linear-gradient(#000, #000) 6px 2px / 3px 3px no-repeat,
/* 尾巴尖端 */
linear-gradient(#000, #000) 9px 0 / 3px 3px no-repeat;
animation: tailWag 0.3s steps(2) infinite alternate;
transform-origin: left bottom;
}
/* 奔跑动画 - 轻微上下跳动 */
@keyframes catRun {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-3px); }
}
/* 移动动画 - 左右移动 */
@keyframes catMove {
0% { left: -40px; }
100% { left: calc(100% + 40px); }
}
/* 前腿动画 */
@keyframes legFront {
0%, 100% { transform: rotate(-15deg); }
50% { transform: rotate(15deg); }
}
/* 后腿动画 */
@keyframes legBack {
0%, 100% { transform: rotate(15deg); }
50% { transform: rotate(-15deg); }
}
/* 尾巴摆动 */
@keyframes tailWag {
0% { transform: rotate(-10deg); }
100% { transform: rotate(10deg); }
}
/* 头部轻微摆动 */
@keyframes headBob {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-1px); }
}
/* 地面效果 - 像素风格 */
.ground {
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 12px;
background: repeating-linear-gradient(
90deg,
#222 0px,
#222 6px,
#555 6px,
#555 12px
);
animation: groundMove 0.3s linear infinite;
image-rendering: pixelated;
}
@keyframes groundMove {
0% { background-position: 0 0; }
100% { background-position: 12px 0; }
}
/* 品牌文字 */
.brand-text {
display: flex;
justify-content: center;
align-items: center;
font-size: 24px;
font-weight: 600;
color: #333;
margin-top: 20px;
letter-spacing: 2px;
}
/* 暗色主题适配 */
@media (prefers-color-scheme: dark) {
.first-loading-wrp {
background: #141414;
}
.first-loading-wrp > h2 {
color: #fff;
}
.brand-text {
color: #fff;
}
.ground {
background: repeating-linear-gradient(
90deg,
#555 0px,
#555 6px,
#333 6px,
#333 12px
);
}
/* 暗色模式下白色部分改成浅灰 */
.cat-head {
background:
linear-gradient(#ddd, #ddd) 2px 4px / 12px 10px no-repeat,
linear-gradient(#000, #000) 2px 4px / 6px 10px no-repeat,
linear-gradient(#ddd, #ddd) 4px 2px / 8px 2px no-repeat;
}
.cat-body {
background:
linear-gradient(#ddd, #ddd) 6px 0 / 8px 10px no-repeat,
linear-gradient(#000, #000) 0 0 / 8px 10px no-repeat;
}
.cat-leg-front-right,
.cat-leg-back-left {
background: #ddd;
}
.cat-eye-left,
.cat-eye-right {
background: #ddd;
}
}
/* 确保像素风格在所有浏览器中正确显示 */
.pixel-cat,
.pixel-cat * {
image-rendering: -moz-crisp-edges;
image-rendering: -webkit-crisp-edges;
image-rendering: pixelated;
image-rendering: crisp-edges;
}
/* 手机端适配 */
@media (max-width: 768px) {
.pixel-cat-container {
transform: scale(1.5);
}
.first-loading-wrp > h2 {
font-size: 24px;
margin-bottom: 30px;
}
.brand-text {
font-size: 20px;
}
}
</style>
<!-- require cdn assets css -->
<% for (var i in htmlWebpackPlugin.options.cdn && htmlWebpackPlugin.options.cdn.css) { %>
<link rel="stylesheet" href="<%= htmlWebpackPlugin.options.cdn.css[i] %>" />
<% } %>
</head>
<body>
<noscript>
<strong>We're sorry but vue-antd-pro doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app">
<div class="first-loading-wrp">
<h2>Landing</h2>
<div class="loading-wrp">
<div class="pixel-cat-container">
<div class="ground"></div>
<div class="pixel-cat">
<div class="cat-head">
<div class="cat-ear-left"></div>
<div class="cat-ear-right"></div>
<div class="cat-eye-left"></div>
<div class="cat-eye-right"></div>
<div class="cat-nose"></div>
<div class="cat-whiskers"></div>
</div>
<div class="cat-body"></div>
<div class="cat-leg-front-left"></div>
<div class="cat-leg-front-right"></div>
<div class="cat-leg-back-left"></div>
<div class="cat-leg-back-right"></div>
<div class="cat-tail"></div>
</div>
</div>
</div>
<div class="brand-text">QuantDinger</div>
</div>
</div>
<!-- require cdn assets js -->
<% for (var i in htmlWebpackPlugin.options.cdn && htmlWebpackPlugin.options.cdn.js) { %>
<script type="text/javascript" src="<%= htmlWebpackPlugin.options.cdn.js[i] %>"></script>
<% } %>
<!-- built files will be auto injected -->
</body>
</html>
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

+40
View File
@@ -0,0 +1,40 @@
$ErrorActionPreference = "Stop"
$version = "0.25.0"
$projectRoot = Split-Path -Parent $PSScriptRoot
$dest = Join-Path $projectRoot "public/assets/pyodide/v$version/full"
$tmpDir = Join-Path $env:TEMP "quantdinger-pyodide-$version"
$archive = Join-Path $tmpDir "pyodide-$version.tar.bz2"
$extractRoot = Join-Path $tmpDir "extract"
New-Item -ItemType Directory -Force -Path $tmpDir | Out-Null
New-Item -ItemType Directory -Force -Path $extractRoot | Out-Null
Write-Host "Downloading Pyodide $version ..."
$url = "https://github.com/pyodide/pyodide/releases/download/$version/pyodide-$version.tar.bz2"
Invoke-WebRequest -Uri $url -OutFile $archive
Write-Host "Extracting archive ..."
if (Test-Path $extractRoot) { Remove-Item -Recurse -Force $extractRoot }
New-Item -ItemType Directory -Force -Path $extractRoot | Out-Null
tar -xjf $archive -C $extractRoot
# Try to locate a folder that contains pyodide.js
$pyodideJs = Get-ChildItem -Path $extractRoot -Recurse -File -Filter "pyodide.js" | Select-Object -First 1
if (-not $pyodideJs) {
throw "pyodide.js not found after extraction. Please check the archive structure."
}
$sourceDir = Split-Path -Parent $pyodideJs.FullName
Write-Host "Found Pyodide files at: $sourceDir"
Write-Host "Copying to: $dest"
if (Test-Path $dest) { Remove-Item -Recurse -Force $dest }
New-Item -ItemType Directory -Force -Path $dest | Out-Null
Copy-Item -Path (Join-Path $sourceDir "*") -Destination $dest -Recurse -Force
Write-Host "Done."
Write-Host "You can now load Pyodide locally from: /assets/pyodide/v$version/full/pyodide.js"
Write-Host "Note: quantdinger_vue/.gitignore ignores /public/assets/pyodide/ by default."
+49
View File
@@ -0,0 +1,49 @@
<template>
<a-config-provider :locale="locale" :direction="direction">
<div id="app">
<router-view/>
</div>
</a-config-provider>
</template>
<script>
import { domTitle, setDocumentTitle } from '@/utils/domUtil'
import { i18nRender } from '@/locales'
export default {
data () {
return {
}
},
computed: {
locale () {
// 只是为了切换语言时,更新标题
const { title } = this.$route.meta
title && (setDocumentTitle(`${i18nRender(title)} - ${domTitle}`))
return this.$i18n.getLocaleMessage(this.$store.getters.lang).antLocale
},
direction () {
const lang = this.$store.getters.lang
return lang && /^ar/i.test(lang) ? 'rtl' : 'ltr'
},
theme () {
return this.$store.state.app.theme
}
},
watch: {
theme: {
handler (val) {
if (val === 'dark' || val === 'realdark') {
document.body.classList.add('dark')
document.body.classList.remove('light')
} else {
document.body.classList.remove('dark')
document.body.classList.add('light')
}
},
immediate: true
}
}
}
</script>
+113
View File
@@ -0,0 +1,113 @@
import request from '@/utils/request'
const api = {
strategies: '/addons/quantdinger/strategy/strategies',
createAIStrategy: '/addons/quantdinger/strategy/aiCreate',
updateAIStrategy: '/addons/quantdinger/strategy/aiUpdate',
deleteStrategy: '/addons/quantdinger/strategy/delete',
startStrategy: '/addons/quantdinger/strategy/start',
stopStrategy: '/addons/quantdinger/strategy/stop',
testConnection: '/addons/quantdinger/strategy/testConnection',
aiDecisions: '/addons/quantdinger/strategy/aiDecisions',
getCryptoSymbols: '/addons/quantdinger/strategy/getCryptoSymbols'
}
/**
* 获取AI交易策略列表
*/
export function getStrategies () {
return request({
url: api.strategies,
method: 'get'
})
}
/**
* 创建AI交易策略
*/
export function createAIStrategy (data) {
return request({
url: api.createAIStrategy,
method: 'post',
data
})
}
/**
* 更新AI交易策略
*/
export function updateAIStrategy (data) {
return request({
url: api.updateAIStrategy,
method: 'post',
data
})
}
/**
* 删除策略
*/
export function deleteStrategy (strategyId) {
return request({
url: api.deleteStrategy,
method: 'delete',
params: { id: strategyId }
})
}
/**
* 启动策略
*/
export function startStrategy (strategyId) {
return request({
url: api.startStrategy,
method: 'post',
params: { id: strategyId }
})
}
/**
* 停止策略
*/
export function stopStrategy (strategyId) {
return request({
url: api.stopStrategy,
method: 'post',
params: { id: strategyId }
})
}
/**
* 测试交易所连接
*/
export function testConnection (data) {
return request({
url: api.testConnection,
method: 'post',
data
})
}
/**
* 获取AI决策记录
*/
export function getAIDecisions (strategyId, params) {
return request({
url: api.aiDecisions,
method: 'get',
params: {
strategy_id: strategyId,
...params
}
})
}
/**
* 获取系统支持的交易对列表
*/
export function getCryptoSymbols () {
return request({
url: api.getCryptoSymbols,
method: 'get'
})
}
+131
View File
@@ -0,0 +1,131 @@
import request from '@/utils/request'
function joinApiBase (path) {
const base = (process.env.VUE_APP_API_BASE_URL || '').trim()
const p = path.startsWith('/') ? path : `/${path}`
if (!base) return p
const b = base.replace(/\/+$/, '')
// Avoid duplicate "/api/api/*" when base is "/api" or ends with "/api"
if (b.endsWith('/api') && p.startsWith('/api/')) {
return b + p.slice('/api'.length)
}
return b + p
}
/**
* Get security configuration (Turnstile, OAuth settings)
*/
export function getSecurityConfig () {
return request({
url: '/api/auth/security-config',
method: 'get'
})
}
/**
* User login
* @param {Object} data - { username, password, turnstile_token }
*/
export function login (data) {
return request({
url: '/api/auth/login',
method: 'post',
data
})
}
/**
* User logout
*/
export function logout () {
return request({
url: '/api/auth/logout',
method: 'post'
})
}
/**
* Get current user info
*/
export function getUserInfo () {
return request({
url: '/api/auth/info',
method: 'get'
})
}
/**
* Send verification code
* @param {Object} data - { email, type, turnstile_token }
* type: 'register' | 'login' | 'reset_password' | 'change_password' | 'change_email'
*/
export function sendVerificationCode (data) {
return request({
url: '/api/auth/send-code',
method: 'post',
data
})
}
/**
* Login with email verification code (quick login)
* @param {Object} data - { email, code, turnstile_token }
*/
export function loginWithCode (data) {
return request({
url: '/api/auth/login-code',
method: 'post',
data
})
}
/**
* User registration
* @param {Object} data - { email, code, username, password, turnstile_token }
*/
export function register (data) {
return request({
url: '/api/auth/register',
method: 'post',
data
})
}
/**
* Reset password
* @param {Object} data - { email, code, new_password, turnstile_token }
*/
export function resetPassword (data) {
return request({
url: '/api/auth/reset-password',
method: 'post',
data
})
}
/**
* Change password (for logged-in users)
* @param {Object} data - { code, new_password }
*/
export function changePassword (data) {
return request({
url: '/api/auth/change-password',
method: 'post',
data
})
}
/**
* Get Google OAuth URL
*/
export function getGoogleOAuthUrl () {
return joinApiBase('/api/auth/oauth/google')
}
/**
* Get GitHub OAuth URL
*/
export function getGitHubOAuthUrl () {
return joinApiBase('/api/auth/oauth/github')
}
+39
View File
@@ -0,0 +1,39 @@
import request from '@/utils/request'
const billingApi = {
Plans: '/api/billing/plans',
Purchase: '/api/billing/purchase',
UsdtCreate: '/api/billing/usdt/create',
UsdtOrder: (id) => `/api/billing/usdt/order/${id}`
}
export function getMembershipPlans () {
return request({
url: billingApi.Plans,
method: 'get'
})
}
export function purchaseMembership (plan) {
return request({
url: billingApi.Purchase,
method: 'post',
data: { plan }
})
}
export function createUsdtOrder (plan) {
return request({
url: billingApi.UsdtCreate,
method: 'post',
data: { plan }
})
}
export function getUsdtOrder (orderId, refresh = true) {
return request({
url: billingApi.UsdtOrder(orderId),
method: 'get',
params: { refresh: refresh ? 1 : 0 }
})
}
+40
View File
@@ -0,0 +1,40 @@
import request from '@/utils/request'
const api = {
list: '/api/credentials/list',
get: '/api/credentials/get',
create: '/api/credentials/create',
delete: '/api/credentials/delete'
}
export function listExchangeCredentials (params = {}) {
return request({
url: api.list,
method: 'get',
params
})
}
export function getExchangeCredential (id, params = {}) {
return request({
url: api.get,
method: 'get',
params: { id, ...params }
})
}
export function createExchangeCredential (data) {
return request({
url: api.create,
method: 'post',
data
})
}
export function deleteExchangeCredential (id, params = {}) {
return request({
url: api.delete,
method: 'delete',
params: { id, ...params }
})
}
+30
View File
@@ -0,0 +1,30 @@
import request from '@/utils/request'
// Dashboard API
const api = {
summary: '/api/dashboard/summary',
pendingOrders: '/api/dashboard/pendingOrders'
}
export function getDashboardSummary () {
return request({
url: api.summary,
method: 'get'
})
}
export function getPendingOrders (params) {
return request({
url: api.pendingOrders,
method: 'get',
params
})
}
export function deletePendingOrder (id) {
return request({
url: `${api.pendingOrders}/${id}`,
method: 'delete'
})
}
+104
View File
@@ -0,0 +1,104 @@
/**
* Fast Analysis API
* New high-performance AI analysis endpoints
*/
import request from '@/utils/request'
const BASE_URL = '/api/fast-analysis'
/**
* Run fast AI analysis
* @param {Object} params - { market, symbol, language, timeframe }
*/
export function fastAnalyze (params) {
return request({
url: `${BASE_URL}/analyze`,
method: 'post',
data: params,
timeout: 60000 // 60s timeout for analysis
})
}
/**
* Run fast analysis with legacy format (for backward compatibility)
* @param {Object} params - { market, symbol, language, timeframe }
*/
export function fastAnalyzeLegacy (params) {
return request({
url: `${BASE_URL}/analyze-legacy`,
method: 'post',
data: params,
timeout: 60000
})
}
/**
* Get analysis history for a specific symbol
* @param {Object} params - { market, symbol, days, limit }
*/
export function getAnalysisHistory (params) {
return request({
url: `${BASE_URL}/history`,
method: 'get',
params
})
}
/**
* Get all analysis history with pagination
* @param {Object} params - { page, pagesize }
*/
export function getAllAnalysisHistory (params) {
return request({
url: `${BASE_URL}/history/all`,
method: 'get',
params
})
}
/**
* Delete analysis history record
* @param {Number} memoryId - The memory ID to delete
*/
export function deleteAnalysisHistory (memoryId) {
return request({
url: `${BASE_URL}/history/${memoryId}`,
method: 'delete'
})
}
/**
* Submit user feedback on analysis
* @param {Object} params - { memory_id, feedback }
*/
export function submitFeedback (params) {
return request({
url: `${BASE_URL}/feedback`,
method: 'post',
data: params
})
}
/**
* Get AI performance stats
* @param {Object} params - { market, symbol, days }
*/
export function getPerformanceStats (params) {
return request({
url: `${BASE_URL}/performance`,
method: 'get',
params
})
}
/**
* Get similar historical patterns
* @param {Object} params - { market, symbol }
*/
export function getSimilarPatterns (params) {
return request({
url: `${BASE_URL}/similar-patterns`,
method: 'get',
params
})
}
+80
View File
@@ -0,0 +1,80 @@
/**
* Global Market Dashboard API
*/
import request from '@/utils/request'
const BASE_URL = '/api/global-market'
/**
* Get global market overview (indices, forex, crypto, commodities)
* Includes geo coordinates for world map display
*/
export function getMarketOverview () {
return request({
url: `${BASE_URL}/overview`,
method: 'get'
})
}
/**
* Get market heatmap data (crypto, stock sectors, forex)
*/
export function getMarketHeatmap () {
return request({
url: `${BASE_URL}/heatmap`,
method: 'get'
})
}
/**
* Get financial news - separated by language (cn/en)
* @param {string} lang - Language filter: 'cn', 'en', or 'all' (default)
*/
export function getMarketNews (lang = 'all') {
return request({
url: `${BASE_URL}/news`,
method: 'get',
params: { lang }
})
}
/**
* Get economic calendar with impact indicators
*/
export function getEconomicCalendar () {
return request({
url: `${BASE_URL}/calendar`,
method: 'get'
})
}
/**
* Get market sentiment (Fear & Greed Index, VIX)
*/
export function getMarketSentiment () {
return request({
url: `${BASE_URL}/sentiment`,
method: 'get'
})
}
/**
* Get trading opportunities based on technical analysis
*/
export function getTradingOpportunities (params) {
return request({
url: `${BASE_URL}/opportunities`,
method: 'get',
params
})
}
/**
* Force refresh all market data (clears cache)
*/
export function refreshMarketData () {
return request({
url: `${BASE_URL}/refresh`,
method: 'post'
})
}
+59
View File
@@ -0,0 +1,59 @@
import request from '@/utils/request'
const userApi = {
Login: '/api/auth/login',
Logout: '/api/auth/logout',
UserInfo: '/api/auth/info',
UserMenu: '/user/nav'
}
/**
* login func
* parameter: {
* username: '',
* password: '',
* remember_me: true,
* captcha: '12345'
* }
* @param parameter
* @returns {*}
*/
export function login (parameter) {
return request({
url: userApi.Login,
method: 'post',
data: parameter
})
}
export function getInfo () {
return request({
url: userApi.UserInfo,
method: 'get',
headers: {
'Content-Type': 'application/json;charset=UTF-8'
}
})
}
// Backward-compatible alias: some modules still call getUserInfo()
export function getUserInfo () {
return getInfo()
}
export function logout () {
return request({
url: userApi.Logout,
method: 'post',
headers: {
'Content-Type': 'application/json;charset=UTF-8'
}
})
}
export function getCurrentUserNav () {
return request({
url: userApi.UserMenu,
method: 'get'
})
}
+70
View File
@@ -0,0 +1,70 @@
import request from '@/utils/request'
const api = {
user: '/user',
role: '/role',
service: '/service',
permission: '/permission',
permissionNoPager: '/permission/no-pager',
orgTree: '/org/tree'
}
export default api
export function getUserList (parameter) {
return request({
url: api.user,
method: 'get',
params: parameter
})
}
export function getRoleList (parameter) {
return request({
url: api.role,
method: 'get',
params: parameter
})
}
export function getServiceList (parameter) {
return request({
url: api.service,
method: 'get',
params: parameter
})
}
export function getPermissions (parameter) {
return request({
url: api.permissionNoPager,
method: 'get',
params: parameter
})
}
export function getOrgTree (parameter) {
return request({
url: api.orgTree,
method: 'get',
params: parameter
})
}
// id == 0 add post
// id != 0 update put
export function saveService (parameter) {
return request({
url: api.service,
method: parameter.id === 0 ? 'post' : 'put',
data: parameter
})
}
export function saveSub (sub) {
return request({
url: '/sub',
method: sub.id === 0 ? 'post' : 'put',
data: sub
})
}
+259
View File
@@ -0,0 +1,259 @@
import request, { ANALYSIS_TIMEOUT } from '@/utils/request'
const marketApi = {
// Watchlist
GetWatchlist: '/api/market/watchlist/get',
AddWatchlist: '/api/market/watchlist/add',
RemoveWatchlist: '/api/market/watchlist/remove',
GetWatchlistPrices: '/api/market/watchlist/prices',
// Analysis
MultiAnalysis: '/api/analysis/multiAnalysis',
CreateAnalysisTask: '/api/analysis/createTask',
GetAnalysisTaskStatus: '/api/analysis/getTaskStatus',
GetAnalysisHistoryList: '/api/analysis/getHistoryList',
DeleteAnalysisTask: '/api/analysis/deleteTask',
ReflectAnalysis: '/api/analysis/reflect',
// AI chat (optional)
ChatMessage: '/api/ai/chat/message',
GetChatHistory: '/api/ai/chat/history',
SaveChatHistory: '/api/ai/chat/history/save',
// Public config
GetConfig: '/api/market/config',
GetMenuFooterConfig: '/api/market/menuFooterConfig',
// Market metadata
GetMarketTypes: '/api/market/types',
// Symbol search
SearchSymbols: '/api/market/symbols/search',
GetHotSymbols: '/api/market/symbols/hot'
}
/**
* 获取自选股列表
* @param parameter { userid: number }
* @returns {*}
*/
export function getWatchlist (parameter) {
return request({
url: marketApi.GetWatchlist,
method: 'get',
params: parameter
})
}
/**
* 添加自选股
* @param parameter { userid: number, market: string, symbol: string }
* @returns {*}
*/
export function addWatchlist (parameter) {
return request({
url: marketApi.AddWatchlist,
method: 'post',
data: parameter
})
}
/**
* 删除自选股
* @param parameter { userid: number, symbol: string }
* @returns {*}
*/
export function removeWatchlist (parameter) {
return request({
url: marketApi.RemoveWatchlist,
method: 'post',
data: parameter
})
}
/**
* 获取自选股价格
* @param parameter { watchlist: array } watchlist格式:[{market: 'USStock', symbol: 'AAPL'}, ...]
* @returns {*}
*/
export function getWatchlistPrices (parameter) {
return request({
url: marketApi.GetWatchlistPrices,
method: 'get',
params: {
watchlist: JSON.stringify(parameter.watchlist || [])
}
})
}
/**
* 发送 AI 聊天消息
* @param parameter { userid: number, message: string, chatId?: string }
* @returns {*}
*/
export function chatMessage (parameter) {
return request({
url: marketApi.ChatMessage,
method: 'post',
data: parameter
})
}
/**
* 获取聊天历史
* @param parameter { userid: number }
* @returns {*}
*/
export function getChatHistory (parameter) {
return request({
url: marketApi.GetChatHistory,
method: 'get',
params: parameter
})
}
/**
* 保存聊天历史
* @param parameter { userid: number, chatHistory: array }
* @returns {*}
*/
export function saveChatHistory (parameter) {
return request({
url: marketApi.SaveChatHistory,
method: 'post',
data: parameter
})
}
/**
* 执行多维度分析
* @param parameter { userid: number, market: string, symbol: string }
* @returns {*}
*/
export function multiAnalysis (parameter) {
return request({
url: marketApi.MultiAnalysis,
method: 'post',
data: parameter,
timeout: ANALYSIS_TIMEOUT // Extended timeout for AI analysis
})
}
/**
* 创建分析任务
* @param parameter { userid: number, market: string, symbol: string }
* @returns {*}
*/
export function createAnalysisTask (parameter) {
return request({
url: marketApi.CreateAnalysisTask,
method: 'post',
data: parameter
})
}
/**
* 获取分析任务状态
* @param parameter { task_id: number }
* @returns {*}
*/
export function getAnalysisTaskStatus (parameter) {
return request({
url: marketApi.GetAnalysisTaskStatus,
method: 'get',
params: parameter
})
}
/**
* 获取历史分析列表
* @param parameter { userid: number, page?: number, pagesize?: number }
* @returns {*}
*/
export function getAnalysisHistoryList (parameter) {
return request({
url: marketApi.GetAnalysisHistoryList,
method: 'get',
params: parameter
})
}
/**
* Delete analysis task
* @param parameter { task_id: number }
* @returns {*}
*/
export function deleteAnalysisTask (parameter) {
return request({
url: marketApi.DeleteAnalysisTask,
method: 'post',
data: parameter
})
}
/**
* 反思学习
* @param parameter { market: string, symbol: string, decision: string, returns?: number, result?: string }
* @returns {*}
*/
export function reflectAnalysis (parameter) {
return request({
url: marketApi.ReflectAnalysis,
method: 'post',
data: parameter
})
}
/**
* 获取插件配置
* @returns {*}
*/
export function getConfig () {
return request({
url: marketApi.GetConfig,
method: 'get'
})
}
/**
* 获取菜单底部配置
* @returns {*}
*/
export function getMenuFooterConfig () {
return request({
url: marketApi.GetMenuFooterConfig,
method: 'get'
})
}
/**
* 获取股票类型列表
* @returns {*}
*/
export function getMarketTypes () {
return request({
url: marketApi.GetMarketTypes,
method: 'get'
})
}
/**
* 搜索金融产品
* @param parameter { market: string, keyword: string, limit?: number }
* @returns {*}
*/
export function searchSymbols (parameter) {
return request({
url: marketApi.SearchSymbols,
method: 'get',
params: parameter
})
}
/**
* 获取热门标的
* @param parameter { market: string, limit?: number }
* @returns {*}
*/
export function getHotSymbols (parameter) {
return request({
url: marketApi.GetHotSymbols,
method: 'get',
params: parameter
})
}
+23
View File
@@ -0,0 +1,23 @@
import request from '@/utils/request'
const api = {
analyze: '/api/polymarket/analyze',
history: '/api/polymarket/history'
}
export function analyzePolymarket (data) {
return request({
url: api.analyze,
method: 'post',
data,
timeout: 120000
})
}
export function getPolymarketHistory (params) {
return request({
url: api.history,
method: 'get',
params
})
}
+151
View File
@@ -0,0 +1,151 @@
/**
* Portfolio API - Manual positions and monitoring
*/
import request from '@/utils/request'
// ==================== Positions ====================
export function getPositions (params = {}) {
return request({
url: '/api/portfolio/positions',
method: 'get',
params
})
}
export function addPosition (data) {
return request({
url: '/api/portfolio/positions',
method: 'post',
data
})
}
export function updatePosition (id, data) {
return request({
url: `/api/portfolio/positions/${id}`,
method: 'put',
data
})
}
export function deletePosition (id) {
return request({
url: `/api/portfolio/positions/${id}`,
method: 'delete'
})
}
export function getPortfolioSummary (params = {}) {
return request({
url: '/api/portfolio/summary',
method: 'get',
params
})
}
// ==================== Monitors ====================
export function getMonitors () {
return request({
url: '/api/portfolio/monitors',
method: 'get'
})
}
export function addMonitor (data) {
return request({
url: '/api/portfolio/monitors',
method: 'post',
data
})
}
export function updateMonitor (id, data) {
return request({
url: `/api/portfolio/monitors/${id}`,
method: 'put',
data
})
}
export function deleteMonitor (id) {
return request({
url: `/api/portfolio/monitors/${id}`,
method: 'delete'
})
}
export function runMonitor (id, params = {}) {
return request({
url: `/api/portfolio/monitors/${id}/run`,
method: 'post',
data: params
})
}
// ==================== Alerts ====================
export function getAlerts () {
return request({
url: '/api/portfolio/alerts',
method: 'get'
})
}
export function addAlert (data) {
return request({
url: '/api/portfolio/alerts',
method: 'post',
data
})
}
export function updateAlert (id, data) {
return request({
url: `/api/portfolio/alerts/${id}`,
method: 'put',
data
})
}
export function deleteAlert (id) {
return request({
url: `/api/portfolio/alerts/${id}`,
method: 'delete'
})
}
// ==================== Groups ====================
export function getGroups () {
return request({
url: '/api/portfolio/groups',
method: 'get'
})
}
export function renameGroup (data) {
return request({
url: '/api/portfolio/groups/rename',
method: 'post',
data
})
}
// ==================== Market (reuse from market.js) ====================
export function searchSymbols (data) {
return request({
url: '/api/market/symbols/search',
method: 'post',
data
})
}
export function getMarketTypes () {
return request({
url: '/api/market/types',
method: 'get'
})
}
+49
View File
@@ -0,0 +1,49 @@
import request from '@/utils/request'
const api = {
placeOrder: '/api/quick-trade/place-order',
balance: '/api/quick-trade/balance',
position: '/api/quick-trade/position',
history: '/api/quick-trade/history',
closePosition: '/api/quick-trade/close-position'
}
export function placeQuickTradeOrder (data) {
return request({
url: api.placeOrder,
method: 'post',
data
})
}
export function getQuickTradeBalance (params) {
return request({
url: api.balance,
method: 'get',
params
})
}
export function getQuickTradePosition (params) {
return request({
url: api.position,
method: 'get',
params
})
}
export function getQuickTradeHistory (params) {
return request({
url: api.history,
method: 'get',
params
})
}
export function closeQuickTradePosition (data) {
return request({
url: api.closePosition,
method: 'post',
data
})
}
+56
View File
@@ -0,0 +1,56 @@
import request from '@/utils/request'
/**
* 获取配置项定义
*/
export function getSettingsSchema () {
return request({
url: '/api/settings/schema',
method: 'get'
})
}
/**
* 获取当前配置值
*/
export function getSettingsValues () {
return request({
url: '/api/settings/values',
method: 'get'
})
}
/**
* 保存配置
* @param {Object} data - 配置数据
*/
export function saveSettings (data) {
return request({
url: '/api/settings/save',
method: 'post',
data
})
}
/**
* 测试API连接
* @param {string} service - 服务名称 (openrouter, finnhub, etc.)
* @param {Object} params - 额外参数
*/
export function testConnection (service, params = {}) {
return request({
url: '/api/settings/test-connection',
method: 'post',
data: { service, ...params }
})
}
/**
* 查询 OpenRouter 账户余额
*/
export function getOpenRouterBalance () {
return request({
url: '/api/settings/openrouter-balance',
method: 'get'
})
}
+237
View File
@@ -0,0 +1,237 @@
import request from '@/utils/request'
const api = {
// Local Python backend
strategies: '/api/strategies',
strategyDetail: '/api/strategies/detail',
createStrategy: '/api/strategies/create',
batchCreateStrategies: '/api/strategies/batch-create',
updateStrategy: '/api/strategies/update',
stopStrategy: '/api/strategies/stop',
startStrategy: '/api/strategies/start',
deleteStrategy: '/api/strategies/delete',
batchStartStrategies: '/api/strategies/batch-start',
batchStopStrategies: '/api/strategies/batch-stop',
batchDeleteStrategies: '/api/strategies/batch-delete',
testConnection: '/api/strategies/test-connection',
trades: '/api/strategies/trades',
positions: '/api/strategies/positions',
equityCurve: '/api/strategies/equityCurve',
notifications: '/api/strategies/notifications'
}
/**
* 获取策略列表
* @param {Object} params - 查询参数
* @param {number} params.user_id - 用户ID(可选)
*/
export function getStrategyList (params = {}) {
return request({
url: api.strategies,
method: 'get',
params
})
}
/**
* 获取策略详情
* @param {number} id - 策略ID
*/
export function getStrategyDetail (id) {
return request({
url: api.strategyDetail,
method: 'get',
params: { id }
})
}
/**
* 创建策略
* @param {Object} data - 策略数据
* @param {number} data.user_id - 用户ID
* @param {string} data.strategy_name - 策略名称
* @param {string} data.strategy_type - 策略类型
* @param {Object} data.llm_model_config - LLM模型配置
* @param {Object} data.exchange_config - 交易所配置
* @param {Object} data.trading_config - 交易配置
*/
export function createStrategy (data) {
return request({
url: api.createStrategy,
method: 'post',
data
})
}
/**
* 批量创建策略(多币种)
* @param {Object} data - 策略数据
* @param {string} data.strategy_name - 策略基础名称
* @param {Array} data.symbols - 币种数组,如 ["Crypto:BTC/USDT", "Crypto:ETH/USDT"]
*/
export function batchCreateStrategies (data) {
return request({
url: api.batchCreateStrategies,
method: 'post',
data
})
}
/**
* 更新策略
* @param {number} id - 策略ID
* @param {Object} data - 策略数据
* @param {string} data.strategy_name - 策略名称(可选)
* @param {Object} data.indicator_config - 技术指标配置(可选)
* @param {Object} data.exchange_config - 交易所配置(可选)
* @param {Object} data.trading_config - 交易配置(可选)
*/
export function updateStrategy (id, data) {
return request({
url: api.updateStrategy,
method: 'put',
params: { id },
data
})
}
/**
* 停止策略
* @param {number} id - 策略ID
*/
export function stopStrategy (id) {
return request({
url: api.stopStrategy,
method: 'post',
params: { id }
})
}
/**
* 启动策略
* @param {number} id - 策略ID
*/
export function startStrategy (id) {
return request({
url: api.startStrategy,
method: 'post',
params: { id }
})
}
/**
* 删除策略
* @param {number} id - 策略ID
*/
export function deleteStrategy (id) {
return request({
url: api.deleteStrategy,
method: 'delete',
params: { id }
})
}
/**
* 批量启动策略
* @param {Object} data
* @param {Array} data.strategy_ids - 策略ID数组
* @param {string} data.strategy_group_id - 策略组ID(可选,与strategy_ids二选一)
*/
export function batchStartStrategies (data) {
return request({
url: api.batchStartStrategies,
method: 'post',
data
})
}
/**
* 批量停止策略
* @param {Object} data
* @param {Array} data.strategy_ids - 策略ID数组
* @param {string} data.strategy_group_id - 策略组ID(可选,与strategy_ids二选一)
*/
export function batchStopStrategies (data) {
return request({
url: api.batchStopStrategies,
method: 'post',
data
})
}
/**
* 批量删除策略
* @param {Object} data
* @param {Array} data.strategy_ids - 策略ID数组
* @param {string} data.strategy_group_id - 策略组ID(可选,与strategy_ids二选一)
*/
export function batchDeleteStrategies (data) {
return request({
url: api.batchDeleteStrategies,
method: 'delete',
data
})
}
/**
* 测试交易所连接
* @param {Object} exchangeConfig - 交易所配置
*/
export function testExchangeConnection (exchangeConfig) {
return request({
url: api.testConnection,
method: 'post',
data: { exchange_config: exchangeConfig }
})
}
/**
* 获取策略交易记录
* @param {number} id - 策略ID
*/
export function getStrategyTrades (id) {
return request({
url: api.trades,
method: 'get',
params: { id }
})
}
/**
* 获取策略持仓记录
* @param {number} id - 策略ID
*/
export function getStrategyPositions (id) {
return request({
url: api.positions,
method: 'get',
params: { id }
})
}
/**
* 获取策略净值曲线
* @param {number} id - 策略ID
*/
export function getStrategyEquityCurve (id) {
return request({
url: api.equityCurve,
method: 'get',
params: { id }
})
}
/**
* Strategy signal notifications (browser channel persistence).
* @param {Object} params
* @param {number} params.id - strategy id (optional)
* @param {number} params.limit - max items (optional)
* @param {number} params.since_id - return items with id > since_id (optional)
*/
export function getStrategyNotifications (params = {}) {
return request({
url: api.notifications,
method: 'get',
params
})
}
+222
View File
@@ -0,0 +1,222 @@
/**
* User Management API
*/
import request from '@/utils/request'
// ==================== Admin APIs ====================
/**
* Get user list (admin only)
* @param {Object} params - { page, page_size, search }
*/
export function getUserList (params) {
return request({
url: '/api/users/list',
method: 'get',
params
})
}
/**
* Get user detail (admin only)
* @param {Number} id - User ID
*/
export function getUserDetail (id) {
return request({
url: '/api/users/detail',
method: 'get',
params: { id }
})
}
/**
* Create new user (admin only)
* @param {Object} data - { username, password, email, nickname, role }
*/
export function createUser (data) {
return request({
url: '/api/users/create',
method: 'post',
data
})
}
/**
* Update user (admin only)
* @param {Number} id - User ID
* @param {Object} data - { email, nickname, role, status }
*/
export function updateUser (id, data) {
return request({
url: '/api/users/update',
method: 'put',
params: { id },
data
})
}
/**
* Delete user (admin only)
* @param {Number} id - User ID
*/
export function deleteUser (id) {
return request({
url: '/api/users/delete',
method: 'delete',
params: { id }
})
}
/**
* Reset user password (admin only)
* @param {Object} data - { user_id, new_password }
*/
export function resetUserPassword (data) {
return request({
url: '/api/users/reset-password',
method: 'post',
data
})
}
/**
* Get available roles
*/
export function getRoles () {
return request({
url: '/api/users/roles',
method: 'get'
})
}
// ==================== Self-Service APIs ====================
/**
* Get current user profile
*/
export function getProfile () {
return request({
url: '/api/users/profile',
method: 'get'
})
}
/**
* Update current user profile
* @param {Object} data - { nickname, email, avatar }
*/
export function updateProfile (data) {
return request({
url: '/api/users/profile/update',
method: 'put',
data
})
}
/**
* Change current user password
* @param {Object} data - { old_password, new_password }
*/
export function changePassword (data) {
return request({
url: '/api/users/change-password',
method: 'post',
data
})
}
/**
* Get current user's notification settings
*/
export function getNotificationSettings () {
return request({
url: '/api/users/notification-settings',
method: 'get'
})
}
/**
* Update current user's notification settings
* @param {Object} data - { default_channels, telegram_chat_id, email, discord_webhook, webhook_url, phone }
*/
export function updateNotificationSettings (data) {
return request({
url: '/api/users/notification-settings',
method: 'put',
data
})
}
/**
* Get current user's credits log
* @param {Object} params - { page, page_size }
*/
export function getMyCreditsLog (params) {
return request({
url: '/api/users/my-credits-log',
method: 'get',
params
})
}
/**
* Get current user's referral list
* @param {Object} params - { page, page_size }
*/
export function getMyReferrals (params) {
return request({
url: '/api/users/my-referrals',
method: 'get',
params
})
}
// ==================== Billing Management (Admin) ====================
/**
* Set user credits (admin only)
* @param {Object} data - { user_id, credits, remark }
*/
export function setUserCredits (data) {
return request({
url: '/api/users/set-credits',
method: 'post',
data
})
}
/**
* Set user VIP status (admin only)
* @param {Object} data - { user_id, vip_days, vip_expires_at, remark }
*/
export function setUserVip (data) {
return request({
url: '/api/users/set-vip',
method: 'post',
data
})
}
/**
* Get user credits log (admin only)
* @param {Object} params - { user_id, page, page_size }
*/
export function getUserCreditsLog (params) {
return request({
url: '/api/users/credits-log',
method: 'get',
params
})
}
/**
* Get system-wide strategy overview (admin only)
* @param {Object} params - { page, page_size, status, search }
*/
export function getSystemStrategies (params) {
return request({
url: '/api/users/system-strategies',
method: 'get',
params
})
}
+69
View File
@@ -0,0 +1,69 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="1361px" height="609px" viewBox="0 0 1361 609" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 46.2 (44496) - http://www.bohemiancoding.com/sketch -->
<title>Group 21</title>
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="Ant-Design-Pro-3.0" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="账户密码登录-校验" transform="translate(-79.000000, -82.000000)">
<g id="Group-21" transform="translate(77.000000, 73.000000)">
<g id="Group-18" opacity="0.8" transform="translate(74.901416, 569.699158) rotate(-7.000000) translate(-74.901416, -569.699158) translate(4.901416, 525.199158)">
<ellipse id="Oval-11" fill="#CFDAE6" opacity="0.25" cx="63.5748792" cy="32.468367" rx="21.7830479" ry="21.766008"></ellipse>
<ellipse id="Oval-3" fill="#CFDAE6" opacity="0.599999964" cx="5.98746479" cy="13.8668601" rx="5.2173913" ry="5.21330997"></ellipse>
<path d="M38.1354514,88.3520215 C43.8984227,88.3520215 48.570234,83.6838647 48.570234,77.9254015 C48.570234,72.1669383 43.8984227,67.4987816 38.1354514,67.4987816 C32.3724801,67.4987816 27.7006688,72.1669383 27.7006688,77.9254015 C27.7006688,83.6838647 32.3724801,88.3520215 38.1354514,88.3520215 Z" id="Oval-3-Copy" fill="#CFDAE6" opacity="0.45"></path>
<path d="M64.2775582,33.1704963 L119.185836,16.5654915" id="Path-12" stroke="#CFDAE6" stroke-width="1.73913043" stroke-linecap="round" stroke-linejoin="round"></path>
<path d="M42.1431708,26.5002681 L7.71190162,14.5640702" id="Path-16" stroke="#E0B4B7" stroke-width="0.702678964" opacity="0.7" stroke-linecap="round" stroke-linejoin="round" stroke-dasharray="1.405357899873153,2.108036953469981"></path>
<path d="M63.9262187,33.521561 L43.6721326,69.3250951" id="Path-15" stroke="#BACAD9" stroke-width="0.702678964" stroke-linecap="round" stroke-linejoin="round" stroke-dasharray="1.405357899873153,2.108036953469981"></path>
<g id="Group-17" transform="translate(126.850922, 13.543654) rotate(30.000000) translate(-126.850922, -13.543654) translate(117.285705, 4.381889)" fill="#CFDAE6">
<ellipse id="Oval-4" opacity="0.45" cx="9.13482653" cy="9.12768076" rx="9.13482653" ry="9.12768076"></ellipse>
<path d="M18.2696531,18.2553615 C18.2696531,13.2142826 14.1798519,9.12768076 9.13482653,9.12768076 C4.08980114,9.12768076 0,13.2142826 0,18.2553615 L18.2696531,18.2553615 Z" id="Oval-4" transform="translate(9.134827, 13.691521) scale(-1, -1) translate(-9.134827, -13.691521) "></path>
</g>
</g>
<g id="Group-14" transform="translate(216.294700, 123.725600) rotate(-5.000000) translate(-216.294700, -123.725600) translate(106.294700, 35.225600)">
<ellipse id="Oval-2" fill="#CFDAE6" opacity="0.25" cx="29.1176471" cy="29.1402439" rx="29.1176471" ry="29.1402439"></ellipse>
<ellipse id="Oval-2" fill="#CFDAE6" opacity="0.3" cx="29.1176471" cy="29.1402439" rx="21.5686275" ry="21.5853659"></ellipse>
<ellipse id="Oval-2-Copy" stroke="#CFDAE6" opacity="0.4" cx="179.019608" cy="138.146341" rx="23.7254902" ry="23.7439024"></ellipse>
<ellipse id="Oval-2" fill="#BACAD9" opacity="0.5" cx="29.1176471" cy="29.1402439" rx="10.7843137" ry="10.7926829"></ellipse>
<path d="M29.1176471,39.9329268 L29.1176471,18.347561 C23.1616351,18.347561 18.3333333,23.1796097 18.3333333,29.1402439 C18.3333333,35.1008781 23.1616351,39.9329268 29.1176471,39.9329268 Z" id="Oval-2" fill="#BACAD9"></path>
<g id="Group-9" opacity="0.45" transform="translate(172.000000, 131.000000)" fill="#E6A1A6">
<ellipse id="Oval-2-Copy-2" cx="7.01960784" cy="7.14634146" rx="6.47058824" ry="6.47560976"></ellipse>
<path d="M0.549019608,13.6219512 C4.12262681,13.6219512 7.01960784,10.722722 7.01960784,7.14634146 C7.01960784,3.56996095 4.12262681,0.670731707 0.549019608,0.670731707 L0.549019608,13.6219512 Z" id="Oval-2-Copy-2" transform="translate(3.784314, 7.146341) scale(-1, 1) translate(-3.784314, -7.146341) "></path>
</g>
<ellipse id="Oval-10" fill="#CFDAE6" cx="218.382353" cy="138.685976" rx="1.61764706" ry="1.61890244"></ellipse>
<ellipse id="Oval-10-Copy-2" fill="#E0B4B7" opacity="0.35" cx="179.558824" cy="175.381098" rx="1.61764706" ry="1.61890244"></ellipse>
<ellipse id="Oval-10-Copy" fill="#E0B4B7" opacity="0.35" cx="180.098039" cy="102.530488" rx="2.15686275" ry="2.15853659"></ellipse>
<path d="M28.9985381,29.9671598 L171.151018,132.876024" id="Path-11" stroke="#CFDAE6" opacity="0.8"></path>
</g>
<g id="Group-10" opacity="0.799999952" transform="translate(1054.100635, 36.659317) rotate(-11.000000) translate(-1054.100635, -36.659317) translate(1026.600635, 4.659317)">
<ellipse id="Oval-7" stroke="#CFDAE6" stroke-width="0.941176471" cx="43.8135593" cy="32" rx="11.1864407" ry="11.2941176"></ellipse>
<g id="Group-12" transform="translate(34.596774, 23.111111)" fill="#BACAD9">
<ellipse id="Oval-7" opacity="0.45" cx="9.18534718" cy="8.88888889" rx="8.47457627" ry="8.55614973"></ellipse>
<path d="M9.18534718,17.4450386 C13.8657264,17.4450386 17.6599235,13.6143199 17.6599235,8.88888889 C17.6599235,4.16345787 13.8657264,0.332739156 9.18534718,0.332739156 L9.18534718,17.4450386 Z" id="Oval-7"></path>
</g>
<path d="M34.6597385,24.809694 L5.71666084,4.76878945" id="Path-2" stroke="#CFDAE6" stroke-width="0.941176471"></path>
<ellipse id="Oval" stroke="#CFDAE6" stroke-width="0.941176471" cx="3.26271186" cy="3.29411765" rx="3.26271186" ry="3.29411765"></ellipse>
<ellipse id="Oval-Copy" fill="#F7E1AD" cx="2.79661017" cy="61.1764706" rx="2.79661017" ry="2.82352941"></ellipse>
<path d="M34.6312443,39.2922712 L5.06366663,59.785082" id="Path-10" stroke="#CFDAE6" stroke-width="0.941176471"></path>
</g>
<g id="Group-19" opacity="0.33" transform="translate(1282.537219, 446.502867) rotate(-10.000000) translate(-1282.537219, -446.502867) translate(1142.537219, 327.502867)">
<g id="Group-17" transform="translate(141.333539, 104.502742) rotate(275.000000) translate(-141.333539, -104.502742) translate(129.333539, 92.502742)" fill="#BACAD9">
<circle id="Oval-4" opacity="0.45" cx="11.6666667" cy="11.6666667" r="11.6666667"></circle>
<path d="M23.3333333,23.3333333 C23.3333333,16.8900113 18.1099887,11.6666667 11.6666667,11.6666667 C5.22334459,11.6666667 0,16.8900113 0,23.3333333 L23.3333333,23.3333333 Z" id="Oval-4" transform="translate(11.666667, 17.500000) scale(-1, -1) translate(-11.666667, -17.500000) "></path>
</g>
<circle id="Oval-5-Copy-6" fill="#CFDAE6" cx="201.833333" cy="87.5" r="5.83333333"></circle>
<path d="M143.5,88.8126685 L155.070501,17.6038544" id="Path-17" stroke="#BACAD9" stroke-width="1.16666667"></path>
<path d="M17.5,37.3333333 L127.466252,97.6449735" id="Path-18" stroke="#BACAD9" stroke-width="1.16666667"></path>
<polyline id="Path-19" stroke="#CFDAE6" stroke-width="1.16666667" points="143.902597 120.302281 174.935455 231.571342 38.5 147.510847 126.366941 110.833333"></polyline>
<path d="M159.833333,99.7453842 L195.416667,89.25" id="Path-20" stroke="#E0B4B7" stroke-width="1.16666667" opacity="0.6"></path>
<path d="M205.333333,82.1372105 L238.719406,36.1666667" id="Path-24" stroke="#BACAD9" stroke-width="1.16666667"></path>
<path d="M266.723424,132.231988 L207.083333,90.4166667" id="Path-25" stroke="#CFDAE6" stroke-width="1.16666667"></path>
<circle id="Oval-5" fill="#C1D1E0" cx="156.916667" cy="8.75" r="8.75"></circle>
<circle id="Oval-5-Copy-3" fill="#C1D1E0" cx="39.0833333" cy="148.75" r="5.25"></circle>
<circle id="Oval-5-Copy-2" fill-opacity="0.6" fill="#D1DEED" cx="8.75" cy="33.25" r="8.75"></circle>
<circle id="Oval-5-Copy-4" fill-opacity="0.6" fill="#D1DEED" cx="243.833333" cy="30.3333333" r="5.83333333"></circle>
<circle id="Oval-5-Copy-5" fill="#E0B4B7" cx="175.583333" cy="232.75" r="5.25"></circle>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 8.7 KiB

@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1551058675966" class="icon" style="" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="7872" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M85.333333 512h85.333334a340.736 340.736 0 0 1 99.712-241.621333 337.493333 337.493333 0 0 1 108.458666-72.96 346.453333 346.453333 0 0 1 261.546667-1.749334A106.154667 106.154667 0 0 0 746.666667 298.666667C805.802667 298.666667 853.333333 251.136 853.333333 192S805.802667 85.333333 746.666667 85.333333c-29.397333 0-55.978667 11.776-75.221334 30.933334-103.722667-41.514667-222.848-40.874667-325.76 2.517333a423.594667 423.594667 0 0 0-135.68 91.264 423.253333 423.253333 0 0 0-91.306666 135.637333A426.88 426.88 0 0 0 85.333333 512z m741.248 133.205333c-17.109333 40.618667-41.685333 77.141333-72.96 108.416s-67.797333 55.850667-108.458666 72.96a346.453333 346.453333 0 0 1-261.546667 1.749334A106.154667 106.154667 0 0 0 277.333333 725.333333C218.197333 725.333333 170.666667 772.864 170.666667 832S218.197333 938.666667 277.333333 938.666667c29.397333 0 55.978667-11.776 75.221334-30.933334A425.173333 425.173333 0 0 0 512 938.666667a425.941333 425.941333 0 0 0 393.258667-260.352A426.325333 426.325333 0 0 0 938.666667 512h-85.333334a341.034667 341.034667 0 0 1-26.752 133.205333z" p-id="7873"></path><path d="M512 318.378667c-106.752 0-193.621333 86.869333-193.621333 193.621333S405.248 705.621333 512 705.621333s193.621333-86.869333 193.621333-193.621333S618.752 318.378667 512 318.378667z m0 301.909333c-59.690667 0-108.288-48.597333-108.288-108.288S452.309333 403.712 512 403.712s108.288 48.597333 108.288 108.288-48.597333 108.288-108.288 108.288z" p-id="7874"></path></svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 KiB

+29
View File
@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg viewBox="0 0 128 128" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 52.6 (67491) - http://www.bohemiancoding.com/sketch -->
<title>Vue</title>
<desc>Created with Sketch.</desc>
<defs>
<linearGradient x1="69.644116%" y1="0%" x2="69.644116%" y2="100%" id="linearGradient-1">
<stop stop-color="#29CDFF" offset="0%"></stop>
<stop stop-color="#148EFF" offset="37.8600687%"></stop>
<stop stop-color="#0A60FF" offset="100%"></stop>
</linearGradient>
<linearGradient x1="-19.8191553%" y1="-36.7931464%" x2="138.57919%" y2="157.637507%" id="linearGradient-2">
<stop stop-color="#29CDFF" offset="0%"></stop>
<stop stop-color="#0F78FF" offset="100%"></stop>
</linearGradient>
<linearGradient x1="68.1279872%" y1="-35.6905737%" x2="30.4400914%" y2="114.942679%" id="linearGradient-3">
<stop stop-color="#FA8E7D" offset="0%"></stop>
<stop stop-color="#F74A5C" offset="51.2635191%"></stop>
<stop stop-color="#F51D2C" offset="100%"></stop>
</linearGradient>
</defs>
<g id="Vue" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Group" transform="translate(19.000000, 9.000000)">
<path d="M89.96,90.48 C78.58,93.48 68.33,83.36 67.62,82.48 L46.6604487,62.2292258 C45.5023849,61.1103236 44.8426845,59.5728835 44.8296987,57.9626396 L44.5035564,17.5209948 C44.4948861,16.4458744 44.0537714,15.4195095 43.2796864,14.6733517 L29.6459999,1.53153737 C28.055475,-0.00160504005 25.5232423,0.0449126588 23.9900999,1.63543756 C23.2715121,2.38092066 22.87,3.37600834 22.87,4.41143746 L22.87,64.3864751 C22.87,67.0807891 23.9572233,69.6611067 25.885409,71.5429748 L63.6004615,108.352061 C65.9466323,110.641873 69.6963584,110.624605 72.0213403,108.313281" id="Path-Copy" fill="url(#linearGradient-1)" fill-rule="nonzero" transform="translate(56.415000, 54.831157) scale(-1, 1) translate(-56.415000, -54.831157) "></path>
<path d="M68,90.1163122 C56.62,93.1163122 45.46,83.36 44.75,82.48 L23.7904487,62.2292258 C22.6323849,61.1103236 21.9726845,59.5728835 21.9596987,57.9626396 L21.6335564,17.5209948 C21.6248861,16.4458744 21.1837714,15.4195095 20.4096864,14.6733517 L6.7759999,1.53153737 C5.185475,-0.00160504005 2.65324232,0.0449126588 1.12009991,1.63543756 C0.401512125,2.38092066 3.90211878e-13,3.37600834 3.90798505e-13,4.41143746 L3.94351218e-13,64.3864751 C3.94681177e-13,67.0807891 1.08722326,69.6611067 3.01540903,71.5429748 L40.7807092,108.401101 C43.1069304,110.671444 46.8180151,110.676525 49.1504445,108.412561" id="Path" fill="url(#linearGradient-2)" fill-rule="nonzero"></path>
<path d="M43.2983488,19.0991931 L27.5566079,3.88246244 C26.7624281,3.11476967 26.7409561,1.84862177 27.5086488,1.05444194 C27.8854826,0.664606611 28.4044438,0.444472651 28.9466386,0.444472651 L60.3925021,0.444472651 C61.4970716,0.444472651 62.3925021,1.33990315 62.3925021,2.44447265 C62.3925021,2.9858375 62.1730396,3.50407742 61.7842512,3.88079942 L46.0801285,19.0975301 C45.3051579,19.8484488 44.0742167,19.8491847 43.2983488,19.0991931 Z" id="Path" fill="url(#linearGradient-3)"></path>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

@@ -0,0 +1,89 @@
<template>
<div class="antd-pro-components-article-list-content-index-listContent">
<div class="description">
<slot>
{{ description }}
</slot>
</div>
<div class="extra">
<a-avatar :src="avatar" size="small" />
<a :href="href">{{ owner }}</a> 发布在 <a :href="href">{{ href }}</a>
<em>{{ updateAt | moment }}</em>
</div>
</div>
</template>
<script>
export default {
name: 'ArticleListContent',
props: {
prefixCls: {
type: String,
default: 'antd-pro-components-article-list-content-index-listContent'
},
description: {
type: String,
default: ''
},
owner: {
type: String,
required: true
},
avatar: {
type: String,
required: true
},
href: {
type: String,
required: true
},
updateAt: {
type: String,
required: true
}
}
}
</script>
<style lang="less" scoped>
@import '../index.less';
.antd-pro-components-article-list-content-index-listContent {
.description {
max-width: 720px;
line-height: 22px;
}
.extra {
margin-top: 16px;
color: @text-color-secondary;
line-height: 22px;
& :deep(.ant-avatar) {
position: relative;
top: 1px;
width: 20px;
height: 20px;
margin-right: 8px;
vertical-align: top;
}
& > em {
margin-left: 16px;
color: @disabled-color;
font-style: normal;
}
}
}
@media screen and (max-width: @screen-xs) {
.antd-pro-components-article-list-content-index-listContent {
.extra {
& > em {
display: block;
margin-top: 8px;
margin-left: 0;
}
}
}
}
</style>
@@ -0,0 +1,3 @@
import ArticleListContent from './ArticleListContent'
export default ArticleListContent
@@ -0,0 +1,25 @@
import PropTypes from 'ant-design-vue/es/_util/vue-types'
import { Tooltip, Avatar } from 'ant-design-vue'
import { getSlotOptions } from 'ant-design-vue/lib/_util/props-util'
import { warning } from 'ant-design-vue/lib/vc-util/warning'
export const AvatarListItemProps = {
tips: PropTypes.string,
src: PropTypes.string.def('')
}
const Item = {
__ANT_AVATAR_CHILDREN: true,
name: 'AvatarListItem',
props: AvatarListItemProps,
created () {
warning(getSlotOptions(this.$parent).__ANT_AVATAR_LIST, 'AvatarListItem must be a subcomponent of AvatarList')
},
render () {
const size = this.$parent.size === 'mini' ? 'small' : this.$parent.size
const AvatarDom = <Avatar size={size || 'small'} src={this.src} />
return (this.tips && <Tooltip title={this.tips}>{AvatarDom}</Tooltip>) || <AvatarDom />
}
}
export default Item
@@ -0,0 +1,72 @@
import './index.less'
import PropTypes from 'ant-design-vue/es/_util/vue-types'
import Avatar from 'ant-design-vue/es/avatar'
import Item from './Item.jsx'
import { filterEmpty } from '@/components/_util/util'
/**
* size: `number`、 `large`、`small`、`default` 默认值: default
* maxLength: number
* excessItemsStyle: CSSProperties
*/
const AvatarListProps = {
prefixCls: PropTypes.string.def('ant-pro-avatar-list'),
size: {
validator: val => {
return typeof val === 'number' || ['small', 'large', 'default'].includes(val)
},
default: 'default'
},
maxLength: PropTypes.number.def(0),
excessItemsStyle: PropTypes.object.def({
color: '#f56a00',
backgroundColor: '#fde3cf'
})
}
const AvatarList = {
__ANT_AVATAR_LIST: true,
Item,
name: 'AvatarList',
props: AvatarListProps,
render (h) {
const { prefixCls, size } = this.$props
const className = {
[`${prefixCls}`]: true,
[`${size}`]: true
}
const items = filterEmpty(this.$slots.default)
const itemsDom = items && items.length ? <ul class={`${prefixCls}-items`}>{this.getItems(items)}</ul> : null
return (
<div class={className}>
{itemsDom}
</div>
)
},
methods: {
getItems (items) {
const className = {
[`${this.prefixCls}-item`]: true,
[`${this.size}`]: true
}
const totalSize = items.length
if (this.maxLength > 0) {
items = items.slice(0, this.maxLength)
items.push((<Avatar size={this.size === 'mini' ? 'small' : this.size} style={this.excessItemsStyle}>{`+${totalSize - this.maxLength}`}</Avatar>))
}
return items.map((item) => (
<li class={className}>{item}</li>
))
}
}
}
AvatarList.install = function (Vue) {
Vue.component(AvatarList.name, AvatarList)
Vue.component(AvatarList.Item.name, AvatarList.Item)
}
export default AvatarList
@@ -0,0 +1,9 @@
import AvatarList from './List'
import Item from './Item'
export {
AvatarList,
Item as AvatarListItem
}
export default AvatarList
@@ -0,0 +1,59 @@
@import '../index';
@avatar-list-prefix-cls: ~"@{ant-pro-prefix}-avatar-list";
@avatar-list-item-prefix-cls: ~"@{ant-pro-prefix}-avatar-list-item";
.@{avatar-list-prefix-cls} {
display: inline-block;
ul {
display: inline-block;
padding: 0;
margin: 0 0 0 8px;
font-size: 0;
list-style: none;
}
}
.@{avatar-list-item-prefix-cls} {
display: inline-block;
width: @avatar-size-base;
height: @avatar-size-base;
margin-left: -8px;
font-size: @font-size-base;
:global {
.ant-avatar {
cursor: pointer;
border: 1px solid #fff;
}
}
&.large {
width: @avatar-size-lg;
height: @avatar-size-lg;
}
&.small {
width: @avatar-size-sm;
height: @avatar-size-sm;
}
&.mini {
width: 20px;
height: 20px;
:global {
.ant-avatar {
width: 20px;
height: 20px;
line-height: 20px;
.ant-avatar-string {
font-size: 12px;
line-height: 18px;
}
}
}
}
}
@@ -0,0 +1,64 @@
# AvatarList 用户头像列表
一组用户头像,常用在项目/团队成员列表。可通过设置 `size` 属性来指定头像大小。
引用方式:
```javascript
import AvatarList from '@/components/AvatarList'
const AvatarListItem = AvatarList.Item
export default {
components: {
AvatarList,
AvatarListItem
}
}
```
## 代码演示 [demo](https://pro.loacg.com/test/home)
```html
<avatar-list size="mini">
<avatar-list-item tips="Jake" src="https://gw.alipayobjects.com/zos/rmsportal/zOsKZmFRdUtvpqCImOVY.png" />
<avatar-list-item tips="Andy" src="https://gw.alipayobjects.com/zos/rmsportal/sfjbOqnsXXJgNCjCzDBL.png" />
<avatar-list-item tips="Niko" src="https://gw.alipayobjects.com/zos/rmsportal/kZzEzemZyKLKFsojXItE.png" />
</avatar-list>
```
```html
<avatar-list :max-length="3">
<avatar-list-item tips="Jake" src="https://gw.alipayobjects.com/zos/rmsportal/zOsKZmFRdUtvpqCImOVY.png" />
<avatar-list-item tips="Andy" src="https://gw.alipayobjects.com/zos/rmsportal/sfjbOqnsXXJgNCjCzDBL.png" />
<avatar-list-item tips="Niko" src="https://gw.alipayobjects.com/zos/rmsportal/kZzEzemZyKLKFsojXItE.png" />
<avatar-list-item tips="Niko" src="https://gw.alipayobjects.com/zos/rmsportal/kZzEzemZyKLKFsojXItE.png" />
<avatar-list-item tips="Niko" src="https://gw.alipayobjects.com/zos/rmsportal/kZzEzemZyKLKFsojXItE.png" />
<avatar-list-item tips="Niko" src="https://gw.alipayobjects.com/zos/rmsportal/kZzEzemZyKLKFsojXItE.png" />
<avatar-list-item tips="Niko" src="https://gw.alipayobjects.com/zos/rmsportal/kZzEzemZyKLKFsojXItE.png" />
</avatar-list>
```
## API
### AvatarList
| 参数 | 说明 | 类型 | 默认值 |
| ---------------- | -------- | ---------------------------------- | --------- |
| size | 头像大小 | `large``small``mini`, `default` | `default` |
| maxLength | 要显示的最大项目 | number | - |
| excessItemsStyle | 多余的项目风格 | CSSProperties | - |
### AvatarList.Item
| 参数 | 说明 | 类型 | 默认值 |
| ---- | ------ | --------- | --- |
| tips | 头像展示文案 | string | - |
| src | 头像图片连接 | string | - |
@@ -0,0 +1,62 @@
<template>
<div :style="{ padding: '0 0 32px 32px' }">
<h4 :style="{ marginBottom: '20px' }">{{ title }}</h4>
<v-chart
height="254"
:data="data"
:forceFit="true"
:padding="['auto', 'auto', '40', '50']">
<v-tooltip />
<v-axis />
<v-bar position="x*y"/>
</v-chart>
</div>
</template>
<script>
export default {
name: 'Bar',
props: {
title: {
type: String,
default: ''
},
data: {
type: Array,
default: () => {
return []
}
},
scale: {
type: Array,
default: () => {
return [{
dataKey: 'x',
min: 2
}, {
dataKey: 'y',
title: '时间',
min: 1,
max: 22
}]
}
},
tooltip: {
type: Array,
default: () => {
return [
'x*y',
(x, y) => ({
name: x,
value: y
})
]
}
}
},
data () {
return {
}
}
}
</script>
@@ -0,0 +1,120 @@
<template>
<a-card :loading="loading" :body-style="{ padding: '20px 24px 8px' }" :bordered="false">
<div class="chart-card-header">
<div class="meta">
<span class="chart-card-title">
<slot name="title">
{{ title }}
</slot>
</span>
<span class="chart-card-action">
<slot name="action"></slot>
</span>
</div>
<div class="total">
<slot name="total">
<span>{{ typeof total === 'function' && total() || total }}</span>
</slot>
</div>
</div>
<div class="chart-card-content">
<div class="content-fix">
<slot></slot>
</div>
</div>
<div class="chart-card-footer">
<div class="field">
<slot name="footer"></slot>
</div>
</div>
</a-card>
</template>
<script>
export default {
name: 'ChartCard',
props: {
title: {
type: String,
default: ''
},
total: {
type: [Function, Number, String],
required: false,
default: null
},
loading: {
type: Boolean,
default: false
}
}
}
</script>
<style lang="less" scoped>
.chart-card-header {
position: relative;
overflow: hidden;
width: 100%;
.meta {
position: relative;
overflow: hidden;
width: 100%;
color: rgba(0, 0, 0, .45);
font-size: 14px;
line-height: 22px;
}
}
.chart-card-action {
cursor: pointer;
position: absolute;
top: 0;
right: 0;
}
.chart-card-footer {
border-top: 1px solid #e8e8e8;
padding-top: 9px;
margin-top: 8px;
> * {
position: relative;
}
.field {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
margin: 0;
}
}
.chart-card-content {
margin-bottom: 12px;
position: relative;
height: 46px;
width: 100%;
.content-fix {
position: absolute;
left: 0;
bottom: 0;
width: 100%;
}
}
.total {
overflow: hidden;
text-overflow: ellipsis;
word-break: break-all;
white-space: nowrap;
color: #000;
margin-top: 4px;
margin-bottom: 0;
font-size: 30px;
line-height: 38px;
height: 38px;
}
</style>
@@ -0,0 +1,67 @@
<template>
<div>
<v-chart
:forceFit="true"
:height="height"
:width="width"
:data="data"
:scale="scale"
:padding="0">
<v-tooltip />
<v-interval
:shape="['liquid-fill-gauge']"
position="transfer*value"
color=""
:v-style="{
lineWidth: 10,
opacity: 0.75
}"
:tooltip="[
'transfer*value',
(transfer, value) => {
return {
name: transfer,
value,
};
},
]"
></v-interval>
<v-guide
v-for="(row, index) in data"
:key="index"
type="text"
:top="true"
:position="{
gender: row.transfer,
value: 45
}"
:content="row.value + '%'"
:v-style="{
fontSize: 100,
textAlign: 'center',
opacity: 0.75,
}"
/>
</v-chart>
</div>
</template>
<script>
export default {
name: 'Liquid',
props: {
height: {
type: Number,
default: 0
},
width: {
type: Number,
default: 0
}
}
}
</script>
<style scoped>
</style>
@@ -0,0 +1,56 @@
<template>
<div class="antv-chart-mini">
<div class="chart-wrapper" :style="{ height: 46 }">
<v-chart :force-fit="true" :height="height" :data="data" :padding="[36, 0, 18, 0]">
<v-tooltip />
<v-smooth-area position="x*y" />
</v-chart>
</div>
</div>
</template>
<script>
import moment from 'moment'
const data = []
const beginDay = new Date().getTime()
for (let i = 0; i < 10; i++) {
data.push({
x: moment(new Date(beginDay + 1000 * 60 * 60 * 24 * i)).format('YYYY-MM-DD'),
y: Math.round(Math.random() * 10)
})
}
const tooltip = [
'x*y',
(x, y) => ({
name: x,
value: y
})
]
const scale = [{
dataKey: 'x',
min: 2
}, {
dataKey: 'y',
title: '时间',
min: 1,
max: 22
}]
export default {
name: 'MiniArea',
data () {
return {
data,
tooltip,
scale,
height: 100
}
}
}
</script>
<style lang="less" scoped>
@import "chart";
</style>
@@ -0,0 +1,57 @@
<template>
<div class="antv-chart-mini">
<div class="chart-wrapper" :style="{ height: 46 }">
<v-chart :force-fit="true" :height="height" :data="data" :padding="[36, 5, 18, 5]">
<v-tooltip />
<v-bar position="x*y" />
</v-chart>
</div>
</div>
</template>
<script>
import moment from 'moment'
const data = []
const beginDay = new Date().getTime()
for (let i = 0; i < 10; i++) {
data.push({
x: moment(new Date(beginDay + 1000 * 60 * 60 * 24 * i)).format('YYYY-MM-DD'),
y: Math.round(Math.random() * 10)
})
}
const tooltip = [
'x*y',
(x, y) => ({
name: x,
value: y
})
]
const scale = [{
dataKey: 'x',
min: 2
}, {
dataKey: 'y',
title: '时间',
min: 1,
max: 30
}]
export default {
name: 'MiniBar',
data () {
return {
data,
tooltip,
scale,
height: 100
}
}
}
</script>
<style lang="less" scoped>
@import "chart";
</style>
@@ -0,0 +1,75 @@
<template>
<div class="chart-mini-progress">
<div class="target" :style="{ left: target + '%'}">
<span :style="{ backgroundColor: color }" />
<span :style="{ backgroundColor: color }"/>
</div>
<div class="progress-wrapper">
<div class="progress" :style="{ backgroundColor: color, width: percentage + '%', height: height }"></div>
</div>
</div>
</template>
<script>
export default {
name: 'MiniProgress',
props: {
target: {
type: Number,
default: 0
},
height: {
type: String,
default: '10px'
},
color: {
type: String,
default: '#13C2C2'
},
percentage: {
type: Number,
default: 0
}
}
}
</script>
<style lang="less" scoped>
.chart-mini-progress {
padding: 5px 0;
position: relative;
width: 100%;
.target {
position: absolute;
top: 0;
bottom: 0;
span {
border-radius: 100px;
position: absolute;
top: 0;
left: 0;
height: 4px;
width: 2px;
&:last-child {
top: auto;
bottom: 0;
}
}
}
.progress-wrapper {
background-color: #f5f5f5;
position: relative;
.progress {
transition: all .4s cubic-bezier(.08,.82,.17,1) 0s;
border-radius: 1px 0 0 1px;
background-color: #1890ff;
width: 0;
height: 100%;
}
}
}
</style>
@@ -0,0 +1,40 @@
<template>
<div :class="prefixCls">
<div class="chart-wrapper" :style="{ height: 46 }">
<v-chart :force-fit="true" :height="100" :data="dataSource" :scale="scale" :padding="[36, 0, 18, 0]">
<v-tooltip />
<v-smooth-line position="x*y" :size="2" />
<v-smooth-area position="x*y" />
</v-chart>
</div>
</div>
</template>
<script>
export default {
name: 'MiniSmoothArea',
props: {
prefixCls: {
type: String,
default: 'ant-pro-smooth-area'
},
scale: {
type: [Object, Array],
required: true
},
dataSource: {
type: Array,
required: true
}
},
data () {
return {
height: 100
}
}
}
</script>
<style lang="less" scoped>
@import "smooth.area.less";
</style>
@@ -0,0 +1,68 @@
<template>
<v-chart :forceFit="true" height="400" :data="data" :padding="[20, 20, 95, 20]" :scale="scale">
<v-tooltip></v-tooltip>
<v-axis :dataKey="axis1Opts.dataKey" :line="axis1Opts.line" :tickLine="axis1Opts.tickLine" :grid="axis1Opts.grid" />
<v-axis :dataKey="axis2Opts.dataKey" :line="axis2Opts.line" :tickLine="axis2Opts.tickLine" :grid="axis2Opts.grid" />
<v-legend dataKey="user" marker="circle" :offset="30" />
<v-coord type="polar" radius="0.8" />
<v-line position="item*score" color="user" :size="2" />
<v-point position="item*score" color="user" :size="4" shape="circle" />
</v-chart>
</template>
<script>
const axis1Opts = {
dataKey: 'item',
line: null,
tickLine: null,
grid: {
lineStyle: {
lineDash: null
},
hideFirstLine: false
}
}
const axis2Opts = {
dataKey: 'score',
line: null,
tickLine: null,
grid: {
type: 'polygon',
lineStyle: {
lineDash: null
}
}
}
const scale = [
{
dataKey: 'score',
min: 0,
max: 80
}, {
dataKey: 'user',
alias: '类型'
}
]
export default {
name: 'Radar',
props: {
data: {
type: Array,
default: null
}
},
data () {
return {
axis1Opts,
axis2Opts,
scale
}
}
}
</script>
<style scoped>
</style>
@@ -0,0 +1,77 @@
<template>
<div class="rank">
<h4 class="title">{{ title }}</h4>
<ul class="list">
<li :key="index" v-for="(item, index) in list">
<span :class="index < 3 ? 'active' : null">{{ index + 1 }}</span>
<span>{{ item.name }}</span>
<span>{{ item.total }}</span>
</li>
</ul>
</div>
</template>
<script>
export default {
name: 'RankList',
// ['title', 'list']
props: {
title: {
type: String,
default: ''
},
list: {
type: Array,
default: null
}
}
}
</script>
<style lang="less" scoped>
.rank {
padding: 0 32px 32px 72px;
.list {
margin: 25px 0 0;
padding: 0;
list-style: none;
li {
margin-top: 16px;
span {
color: rgba(0, 0, 0, .65);
font-size: 14px;
line-height: 22px;
&:first-child {
background-color: #f5f5f5;
border-radius: 20px;
display: inline-block;
font-size: 12px;
font-weight: 600;
margin-right: 24px;
height: 20px;
line-height: 20px;
width: 20px;
text-align: center;
}
&.active {
background-color: #314659;
color: #fff;
}
&:last-child {
float: right;
}
}
}
}
}
.mobile .rank {
padding: 0 32px 32px 32px;
}
</style>
@@ -0,0 +1,113 @@
<template>
<v-chart :width="width" :height="height" :padding="[0]" :data="data" :scale="scale">
<v-tooltip :show-title="false" />
<v-coord type="rect" direction="TL" />
<v-point position="x*y" color="category" shape="cloud" tooltip="value*category" />
</v-chart>
</template>
<script>
import { registerShape } from 'viser-vue'
const DataSet = require('@antv/data-set')
const imgUrl = 'https://gw.alipayobjects.com/zos/rmsportal/gWyeGLCdFFRavBGIDzWk.png'
const scale = [
{ dataKey: 'x', nice: false },
{ dataKey: 'y', nice: false }
]
registerShape('point', 'cloud', {
draw (cfg, container) {
return container.addShape('text', {
attrs: {
fillOpacity: cfg.opacity,
fontSize: cfg.origin._origin.size,
rotate: cfg.origin._origin.rotate,
text: cfg.origin._origin.text,
textAlign: 'center',
fontFamily: cfg.origin._origin.font,
fill: cfg.color,
textBaseline: 'Alphabetic',
...cfg.style,
x: cfg.x,
y: cfg.y
}
})
}
})
export default {
name: 'TagCloud',
props: {
tagList: {
type: Array,
required: true
},
height: {
type: Number,
default: 400
},
width: {
type: Number,
default: 640
}
},
data () {
return {
data: [],
scale
}
},
watch: {
tagList: function (val) {
if (val.length > 0) {
this.initTagCloud(val)
}
}
},
mounted () {
if (this.tagList.length > 0) {
this.initTagCloud(this.tagList)
}
},
methods: {
initTagCloud (dataSource) {
const { height, width } = this
const dv = new DataSet.View().source(dataSource)
const range = dv.range('value')
const min = range[0]
const max = range[1]
const imageMask = new Image()
imageMask.crossOrigin = ''
imageMask.src = imgUrl
imageMask.onload = () => {
dv.transform({
type: 'tag-cloud',
fields: ['name', 'value'],
size: [width, height],
imageMask,
font: 'Verdana',
padding: 0,
timeInterval: 5000, // max execute time
rotate () {
let random = ~~(Math.random() * 4) % 4
if (random === 2) {
random = 0
}
return random * 90 // 0, 90, 270
},
fontSize (d) {
if (d.value) {
return ((d.value - min) / (max - min)) * (32 - 8) + 8
}
return 0
}
})
this.data = dv.rows
}
}
}
}
</script>
@@ -0,0 +1,64 @@
<template>
<div :style="{ padding: '0 0 32px 32px' }">
<h4 :style="{ marginBottom: '20px' }">{{ title }}</h4>
<v-chart
height="254"
:data="data"
:scale="scale"
:forceFit="true"
:padding="['auto', 'auto', '40', '50']">
<v-tooltip />
<v-axis />
<v-bar position="x*y"/>
</v-chart>
</div>
</template>
<script>
const tooltip = [
'x*y',
(x, y) => ({
name: x,
value: y
})
]
const scale = [{
dataKey: 'x',
title: '日期(天)',
alias: '日期(天)',
min: 2
}, {
dataKey: 'y',
title: '流量(Gb)',
alias: '流量(Gb)',
min: 1
}]
export default {
name: 'Bar',
props: {
title: {
type: String,
default: ''
}
},
data () {
return {
data: [],
scale,
tooltip
}
},
created () {
this.getMonthBar()
},
methods: {
getMonthBar () {
this.$http.get('/analysis/month-bar')
.then(res => {
this.data = res.result
})
}
}
}
</script>
@@ -0,0 +1,82 @@
<template>
<div class="chart-trend">
{{ term }}
<span>{{ rate }}%</span>
<span :class="['trend-icon', trend]"><a-icon :type="'caret-' + trend"/></span>
</div>
</template>
<script>
export default {
name: 'Trend',
props: {
term: {
type: String,
default: '',
required: true
},
percentage: {
type: Number,
default: null
},
type: {
type: Boolean,
default: null
},
target: {
type: Number,
default: 0
},
value: {
type: Number,
default: 0
},
fixed: {
type: Number,
default: 2
}
},
data () {
return {
trend: this.type && 'up' || 'down',
rate: this.percentage
}
},
created () {
const type = this.type === null ? this.value >= this.target : this.type
this.trend = type ? 'up' : 'down'
this.rate = (this.percentage === null ? Math.abs(this.value - this.target) * 100 / this.target : this.percentage).toFixed(this.fixed)
}
}
</script>
<style lang="less" scoped>
.chart-trend {
display: inline-block;
font-size: 14px;
line-height: 22px;
.trend-icon {
font-size: 12px;
&.up, &.down {
margin-left: 4px;
position: relative;
top: 1px;
i {
font-size: 12px;
transform: scale(.83);
}
}
&.up {
color: #f5222d;
}
&.down {
color: #52c41a;
top: -1px;
}
}
}
</style>
@@ -0,0 +1,13 @@
.antv-chart-mini {
position: relative;
width: 100%;
.chart-wrapper {
position: absolute;
bottom: -28px;
width: 100%;
/* margin: 0 -5px;
overflow: hidden; */
}
}
@@ -0,0 +1,14 @@
@import '../index';
@smoothArea-prefix-cls: ~"@{ant-pro-prefix}-smooth-area";
.@{smoothArea-prefix-cls} {
position: relative;
width: 100%;
.chart-wrapper {
position: absolute;
bottom: -28px;
width: 100%;
}
}
+113
View File
@@ -0,0 +1,113 @@
import Modal from 'ant-design-vue/es/modal'
export default (Vue) => {
function dialog (component, componentProps, modalProps) {
const _vm = this
modalProps = modalProps || {}
if (!_vm || !_vm._isVue) {
return
}
let dialogDiv = document.querySelector('body>div[type=dialog]')
if (!dialogDiv) {
dialogDiv = document.createElement('div')
dialogDiv.setAttribute('type', 'dialog')
document.body.appendChild(dialogDiv)
}
const handle = function (checkFunction, afterHandel) {
if (checkFunction instanceof Function) {
const res = checkFunction()
if (res instanceof Promise) {
res.then(c => {
c && afterHandel()
})
} else {
res && afterHandel()
}
} else {
// checkFunction && afterHandel()
checkFunction || afterHandel()
}
}
const dialogInstance = new Vue({
data () {
return {
visible: true
}
},
router: _vm.$router,
store: _vm.$store,
mounted () {
this.$on('close', (v) => {
this.handleClose()
})
},
methods: {
handleClose () {
handle(this.$refs._component.onCancel, () => {
this.visible = false
this.$refs._component.$emit('close')
this.$refs._component.$emit('cancel')
dialogInstance.$destroy()
})
},
handleOk () {
handle(this.$refs._component.onOK || this.$refs._component.onOk, () => {
this.visible = false
this.$refs._component.$emit('close')
this.$refs._component.$emit('ok')
dialogInstance.$destroy()
})
}
},
render: function (h) {
const that = this
const modalModel = modalProps && modalProps.model
if (modalModel) {
delete modalProps.model
}
const ModalProps = Object.assign({}, modalModel && { model: modalModel } || {}, {
attrs: Object.assign({}, {
...(modalProps.attrs || modalProps)
}, {
visible: this.visible
}),
on: Object.assign({}, {
...(modalProps.on || modalProps)
}, {
ok: () => {
that.handleOk()
},
cancel: () => {
that.handleClose()
}
})
})
const componentModel = componentProps && componentProps.model
if (componentModel) {
delete componentProps.model
}
const ComponentProps = Object.assign({}, componentModel && { model: componentModel } || {}, {
ref: '_component',
attrs: Object.assign({}, {
...((componentProps && componentProps.attrs) || componentProps)
}),
on: Object.assign({}, {
...((componentProps && componentProps.on) || componentProps)
})
})
return h(Modal, ModalProps, [h(component, ComponentProps)])
}
}).$mount(dialogDiv)
}
Object.defineProperty(Vue.prototype, '$dialog', {
get: () => {
return function () {
dialog.apply(this, arguments)
}
}
})
}
@@ -0,0 +1,79 @@
<template>
<div :class="prefixCls">
<quill-editor
v-model="content"
ref="myQuillEditor"
:options="editorOption"
@blur="onEditorBlur($event)"
@focus="onEditorFocus($event)"
@ready="onEditorReady($event)"
@change="onEditorChange($event)">
</quill-editor>
</div>
</template>
<script>
import 'quill/dist/quill.core.css'
import 'quill/dist/quill.snow.css'
import 'quill/dist/quill.bubble.css'
import { quillEditor } from 'vue-quill-editor'
export default {
name: 'QuillEditor',
components: {
quillEditor
},
props: {
prefixCls: {
type: String,
default: 'ant-editor-quill'
},
// 表单校验用字段
// eslint-disable-next-line
value: {
type: String
}
},
data () {
return {
content: null,
editorOption: {
// some quill options
}
}
},
methods: {
onEditorBlur (quill) {
},
onEditorFocus (quill) {
},
onEditorReady (quill) {
},
onEditorChange ({ quill, html, text }) {
this.$emit('change', html)
}
},
watch: {
value (val) {
this.content = val
}
}
}
</script>
<style lang="less" scoped>
@import url('../index.less');
/* 覆盖 quill 默认边框圆角为 ant 默认圆角,用于统一 ant 组件风格 */
.ant-editor-quill {
line-height: initial;
:deep(.ql-toolbar.ql-snow) {
border-radius: @border-radius-base @border-radius-base 0 0;
}
:deep(.ql-container.ql-snow) {
border-radius: 0 0 @border-radius-base @border-radius-base;
}
}
</style>
@@ -0,0 +1,57 @@
<template>
<div :class="prefixCls">
<div ref="editor" class="editor-wrapper"></div>
</div>
</template>
<script>
import WEditor from 'wangeditor'
export default {
name: 'WangEditor',
props: {
prefixCls: {
type: String,
default: 'ant-editor-wang'
},
// eslint-disable-next-line
value: {
type: String
}
},
data () {
return {
editor: null,
editorContent: null
}
},
watch: {
value (val) {
this.editorContent = val
this.editor.txt.html(val)
}
},
mounted () {
this.initEditor()
},
methods: {
initEditor () {
this.editor = new WEditor(this.$refs.editor)
// this.editor.onchangeTimeout = 200
this.editor.customConfig.onchange = (html) => {
this.editorContent = html
this.$emit('change', this.editorContent)
}
this.editor.create()
}
}
}
</script>
<style lang="less" scoped>
.ant-editor-wang {
.editor-wrapper {
text-align: left;
}
}
</style>
@@ -0,0 +1,64 @@
<script>
import Tooltip from 'ant-design-vue/es/tooltip'
import { cutStrByFullLength, getStrFullLength } from '@/components/_util/util'
/*
const isSupportLineClamp = document.body.style.webkitLineClamp !== undefined;
const TooltipOverlayStyle = {
overflowWrap: 'break-word',
wordWrap: 'break-word',
};
*/
export default {
name: 'Ellipsis',
components: {
Tooltip
},
props: {
prefixCls: {
type: String,
default: 'ant-pro-ellipsis'
},
tooltip: {
type: Boolean
},
length: {
type: Number,
required: true
},
lines: {
type: Number,
default: 1
},
fullWidthRecognition: {
type: Boolean,
default: false
}
},
methods: {
getStrDom (str, fullLength) {
return (
<span>{ cutStrByFullLength(str, this.length) + (fullLength > this.length ? '...' : '') }</span>
)
},
getTooltip (fullStr, fullLength) {
return (
<Tooltip>
<template slot="title">{ fullStr }</template>
{ this.getStrDom(fullStr, fullLength) }
</Tooltip>
)
}
},
render () {
const { tooltip, length } = this.$props
const str = this.$slots.default.map(vNode => vNode.text).join('')
const fullLength = getStrFullLength(str)
const strDom = tooltip && fullLength > length ? this.getTooltip(str, fullLength) : this.getStrDom(str, fullLength)
return (
strDom
)
}
}
</script>
@@ -0,0 +1,3 @@
import Ellipsis from './Ellipsis'
export default Ellipsis
@@ -0,0 +1,38 @@
# Ellipsis 文本自动省略号
文本过长自动处理省略号,支持按照文本长度和最大行数两种方式截取。
引用方式:
```javascript
import Ellipsis from '@/components/Ellipsis'
export default {
components: {
Ellipsis
}
}
```
## 代码演示 [demo](https://pro.loacg.com/test/home)
```html
<ellipsis :length="100" tooltip>
There were injuries alleged in three cases in 2015, and a
fourth incident in September, according to the safety recall report. After meeting with US regulators in October, the firm decided to issue a voluntary recall.
</ellipsis>
```
## API
参数 | 说明 | 类型 | 默认值
----|------|-----|------
tooltip | 移动到文本展示完整内容的提示 | boolean | -
length | 在按照长度截取下的文本最大字符数,超过则截取省略 | number | -
@@ -0,0 +1,47 @@
<template>
<div :class="prefixCls" :style="{ width: barWidth, transition: '0.3s all' }">
<div style="float: left">
<slot name="extra">{{ extra }}</slot>
</div>
<div style="float: right">
<slot></slot>
</div>
</div>
</template>
<script>
export default {
name: 'FooterToolBar',
props: {
prefixCls: {
type: String,
default: 'ant-pro-footer-toolbar'
},
collapsed: {
type: Boolean,
default: false
},
isMobile: {
type: Boolean,
default: false
},
siderWidth: {
type: Number,
default: undefined
},
extra: {
type: [String, Object],
default: ''
}
},
computed: {
barWidth () {
return this.isMobile ? undefined : `calc(100% - ${this.collapsed ? 80 : this.siderWidth || 256}px)`
}
}
}
</script>
<style lang="less" scoped>
</style>
@@ -0,0 +1,4 @@
import FooterToolBar from './FooterToolBar'
import './index.less'
export default FooterToolBar
@@ -0,0 +1,23 @@
@import '../index';
@footer-toolbar-prefix-cls: ~"@{ant-pro-prefix}-footer-toolbar";
.@{footer-toolbar-prefix-cls} {
position: fixed;
right: 0;
bottom: 0;
z-index: 9;
width: 100%;
height: 56px;
padding: 0 24px;
line-height: 56px;
background: #fff;
box-shadow: 0 -1px 2px rgb(0 0 0 / 3%);
border-top: 1px solid #e8e8e8;
&::after {
display: block;
clear: both;
content: '';
}
}
@@ -0,0 +1,48 @@
# FooterToolbar 底部工具栏
固定在底部的工具栏。
## 何时使用
固定在内容区域的底部,不随滚动条移动,常用于长页面的数据搜集和提交工作。
引用方式:
```javascript
import FooterToolBar from '@/components/FooterToolbar'
export default {
components: {
FooterToolBar
}
}
```
## 代码演示
```html
<footer-tool-bar>
<a-button type="primary" @click="validate" :loading="loading">提交</a-button>
</footer-tool-bar>
```
```html
<footer-tool-bar extra="扩展信息提示">
<a-button type="primary" @click="validate" :loading="loading">提交</a-button>
</footer-tool-bar>
```
## API
参数 | 说明 | 类型 | 默认值
----|------|-----|------
children (slot) | 工具栏内容,向右对齐 | - | -
extra | 额外信息,向左对齐 | String, Object | -
@@ -0,0 +1,129 @@
<template>
<div :class="footerCls">
<global-footer class="footer custom-render">
<template v-slot:links>
<a @click="showLegal = true" style="cursor: pointer;">{{ $t('user.login.legal.title') }} © 2025-2026 QuantDinger</a>
</template>
</global-footer>
<a-modal :visible="showLegal" :title="$t('user.login.legal.title')" :footer="null" @cancel="showLegal = false">
<div :class="['legal-content', { 'legal-content-dark': isDarkTheme }]">
{{ $t('user.login.legal.content') }}
</div>
<div style="margin-top: 12px; text-align: right;">
<a-button type="primary" @click="showLegal = false">OK</a-button>
</div>
</a-modal>
</div>
</template>
<script>
import { GlobalFooter } from '@ant-design-vue/pro-layout'
import { baseMixin } from '@/store/app-mixin'
export default {
name: 'ProGlobalFooter',
components: {
GlobalFooter
},
mixins: [baseMixin],
data () {
return {
showLegal: false
}
},
computed: {
// 判断是否为暗黑主题
isDarkTheme () {
return this.navTheme === 'dark' || this.navTheme === 'realdark'
},
// Footer 容器类名
footerCls () {
return {
'footer-wrapper': true,
'footer-wrapper-dark': this.isDarkTheme
}
}
}
}
</script>
<style lang="less">
/* 不使用 scoped,直接覆盖全局样式 */
.footer-wrapper {
/* 调整内间距 */
.ant-pro-global-footer {
padding: 4px 16px 8px;
margin: 0;
}
.ant-pro-global-footer-links {
margin-bottom: 2px;
padding: 0;
}
.ant-pro-global-footer-copyright {
margin-top: 2px;
padding: 0;
}
}
/* 浅色主题(默认)- 确保文字是深色的 */
.footer-wrapper {
.ant-pro-global-footer {
background: transparent !important;
color: rgba(0, 0, 0, 0.65) !important;
}
/* 链接颜色 */
.ant-pro-global-footer-links {
a {
color: rgba(0, 0, 0, 0.65) !important;
&:hover {
color: #1890ff !important;
}
}
}
/* 版权文字颜色 */
.ant-pro-global-footer-copyright {
color: rgba(0, 0, 0, 0.65) !important;
}
}
/* 浅色主题(默认) */
.legal-content {
white-space: pre-wrap;
line-height: 1.7;
color: rgba(0, 0, 0, 0.85);
}
/* 暗黑主题 - 通过组件外层类名控制 */
.footer-wrapper-dark {
.ant-pro-global-footer {
background: transparent !important;
color: rgba(255, 255, 255, 0.65) !important;
border-top: none !important;
}
/* 链接颜色 */
.ant-pro-global-footer-links {
a {
color: rgba(255, 255, 255, 0.65) !important;
&:hover {
color: #1890ff !important;
}
}
}
/* 版权文字颜色 */
.ant-pro-global-footer-copyright {
color: rgba(255, 255, 255, 0.65) !important;
}
/* 弹窗内容颜色 */
.legal-content {
color: rgba(255, 255, 255, 0.85) !important;
}
}
</style>
@@ -0,0 +1,103 @@
<template>
<a-dropdown v-if="currentUser && currentUser.name" placement="bottomRight">
<span class="ant-pro-account-avatar">
<a-avatar size="small" :src="currentUser.avatar" class="antd-pro-global-header-index-avatar" />
<span>{{ currentUser.name }}</span>
</span>
<template #overlay>
<a-menu class="ant-pro-drop-down menu" :selected-keys="[]">
<a-menu-item key="profile" @click="handleProfile">
<a-icon type="user" />
{{ $t('menu.profile') || 'My Profile' }}
</a-menu-item>
<a-menu-divider />
<a-menu-item key="logout" @click="handleLogout">
<a-icon type="logout" />
{{ $t('menu.account.logout') }}
</a-menu-item>
</a-menu>
</template>
</a-dropdown>
<span v-else>
<a-spin size="small" :style="{ marginLeft: 8, marginRight: 8 }" />
</span>
</template>
<script>
import { Modal } from 'ant-design-vue'
export default {
name: 'AvatarDropdown',
props: {
currentUser: {
type: Object,
default: () => null
},
menu: {
type: Boolean,
default: true
}
},
methods: {
handleProfile () {
this.$router.push({ name: 'Profile' })
},
handleLogout (e) {
Modal.confirm({
title: this.$t('layouts.usermenu.dialog.title'),
content: this.$t('layouts.usermenu.dialog.content'),
onOk: () => {
// return new Promise((resolve, reject) => {
// setTimeout(Math.random() > 0.5 ? resolve : reject, 1500)
// }).catch(() => console.log('Oops errors!'))
return this.$store.dispatch('Logout').then(() => {
this.$router.push({ name: 'login' })
})
},
onCancel () {}
})
}
}
}
</script>
<style lang="less">
.ant-pro-drop-down {
.action {
margin-right: 8px;
}
.ant-dropdown-menu-item {
min-width: 160px;
}
}
/* 暗黑主题 - 下拉菜单样式 */
body.dark .ant-dropdown-menu,
body.realdark .ant-dropdown-menu,
.ant-layout.dark .ant-dropdown-menu,
.ant-layout.realdark .ant-dropdown-menu,
.ant-pro-layout.dark .ant-dropdown-menu,
.ant-pro-layout.realdark .ant-dropdown-menu {
background-color: #1f1f1f;
border: 1px solid #303030;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
.ant-dropdown-menu-item {
color: rgba(255, 255, 255, 0.85);
&:hover,
&.ant-dropdown-menu-item-selected {
background-color: #262626;
color: #1890ff;
}
.anticon {
color: rgba(255, 255, 255, 0.85);
}
}
.ant-dropdown-menu-item-divider {
background-color: #303030;
}
}
</style>
@@ -0,0 +1,164 @@
<template>
<div :class="wrpCls">
<avatar-dropdown :menu="true" :current-user="currentUser" :class="prefixCls" />
<notice-icon :class="prefixCls" />
<select-lang :class="prefixCls" />
<a-tooltip :title="$t('app.setting.tooltip')">
<span :class="prefixCls" @click="handleSettingClick">
<a-icon type="setting" style="font-size: 16px;" />
</span>
</a-tooltip>
</div>
</template>
<script>
import AvatarDropdown from './AvatarDropdown'
import SelectLang from '@/components/SelectLang'
import NoticeIcon from '@/components/NoticeIcon'
import { mapGetters } from 'vuex'
export default {
name: 'RightContent',
components: {
AvatarDropdown,
SelectLang,
NoticeIcon
},
props: {
prefixCls: {
type: String,
default: 'ant-pro-global-header-index-action'
},
isMobile: {
type: Boolean,
default: () => false
},
topMenu: {
type: Boolean,
required: true
},
theme: {
type: String,
required: true
}
},
data () {
return {
apiBase: 'https://api.quantdinger.com/'
}
},
methods: {
handleSettingClick () {
// 触发设置抽屉显示事件
this.$root.$emit('show-setting-drawer')
}
},
computed: {
...mapGetters(['nickname', 'avatar']),
currentUser () {
return {
name: this.nickname,
avatar: this.avatar
}
},
wrpCls () {
return {
'ant-pro-global-header-index-right': true,
[`ant-pro-global-header-index-${(this.isMobile || !this.topMenu) ? 'light' : this.theme}`]: true
}
}
}
}
</script>
<style lang="less">
@import '~ant-design-vue/es/style/themes/default.less';
/* 浅色主题(默认) */
.ant-pro-global-header-index-right {
display: flex;
align-items: center;
flex-shrink: 0;
.ant-pro-global-header-index-action {
display: inline-flex;
align-items: center;
justify-content: center;
height: @layout-header-height;
padding: 0 12px;
color: rgba(0, 0, 0, 0.65);
transition: all 0.3s;
cursor: pointer;
vertical-align: top;
&:hover {
color: @primary-color;
background: rgba(0, 0, 0, 0.04);
}
}
}
/* 手机端适配 */
@media (max-width: 768px) {
.ant-pro-global-header-index-right {
.ant-pro-global-header-index-action {
padding: 0 8px;
}
.ant-pro-drop-down,
.ant-pro-account-avatar {
padding: 0 8px;
}
}
}
/* 暗黑主题 - 强制覆盖 */
/* 只要 body 或 layout 有 dark/realdark 类,就应用这些样式 */
body.dark,
body.realdark,
.ant-layout.dark,
.ant-layout.realdark,
.ant-pro-layout.dark,
.ant-pro-layout.realdark {
/* 覆盖 Header 右侧容器内所有文本颜色 */
.ant-pro-global-header-index-right {
color: rgba(255, 255, 255, 0.85) !important;
* {
color: rgba(255, 255, 255, 0.85) !important;
}
/* 操作按钮 */
.ant-pro-global-header-index-action {
color: rgba(255, 255, 255, 0.85) !important;
&:hover {
color: #1890ff !important;
background: rgba(255, 255, 255, 0.08) !important;
}
}
/* 头像 */
.ant-pro-account-avatar {
.antd-pro-global-header-index-avatar {
background: rgba(255, 255, 255, 0.25) !important;
}
}
/* 下拉菜单触发器(包含图标) */
.ant-pro-drop-down,
.ant-dropdown-trigger {
color: rgba(255, 255, 255, 0.85) !important;
&:hover {
color: #1890ff !important;
background: rgba(255, 255, 255, 0.08) !important;
}
.anticon {
color: rgba(255, 255, 255, 0.85) !important;
}
}
}
}
</style>
@@ -0,0 +1,86 @@
<template>
<div :class="prefixCls">
<a-tabs v-model="currentTab" @change="handleTabChange">
<a-tab-pane v-for="v in icons" :tab="v.title" :key="v.key">
<ul>
<li v-for="(icon, key) in v.icons" :key="`${v.key}-${key}`" :class="{ 'active': selectedIcon==icon }" @click="handleSelectedIcon(icon)" >
<a-icon :type="icon" :style="{ fontSize: '36px' }" />
</li>
</ul>
</a-tab-pane>
</a-tabs>
</div>
</template>
<script>
import icons from './icons'
export default {
name: 'IconSelect',
props: {
prefixCls: {
type: String,
default: 'ant-pro-icon-selector'
},
// eslint-disable-next-line
value: {
type: String
}
},
data () {
return {
selectedIcon: this.value || '',
currentTab: 'directional',
icons
}
},
watch: {
value (val) {
this.selectedIcon = val
this.autoSwitchTab()
}
},
created () {
if (this.value) {
this.autoSwitchTab()
}
},
methods: {
handleSelectedIcon (icon) {
this.selectedIcon = icon
this.$emit('change', icon)
},
handleTabChange (activeKey) {
this.currentTab = activeKey
},
autoSwitchTab () {
icons.some(item => item.icons.some(icon => icon === this.value) && (this.currentTab = item.key))
}
}
}
</script>
<style lang="less" scoped>
@import "../index.less";
ul{
list-style: none;
padding: 0;
overflow-y: scroll;
height: 250px;
li{
display: inline-block;
padding: @padding-sm;
margin: 3px 0;
border-radius: @border-radius-base;
&:hover, &.active{
// box-shadow: 0px 0px 5px 2px @primary-color;
cursor: pointer;
color: @white;
background-color: @primary-color;
}
}
}
</style>
@@ -0,0 +1,48 @@
IconSelector
====
> 图标选择组件,常用于为某一个数据设定一个图标时使用
> eg: 设定菜单列表时,为每个菜单设定一个图标
该组件由 [@Saraka](https://github.com/saraka-tsukai) 封装
### 使用方式
```vue
<template>
<div>
<icon-selector @change="handleIconChange"/>
</div>
</template>
<script>
import IconSelector from '@/components/IconSelector'
export default {
name: 'YourView',
components: {
IconSelector
},
data () {
return {
}
},
methods: {
handleIconChange (icon) {
console.log('change Icon', icon)
}
}
}
</script>
```
### 事件
| 名称 | 说明 | 类型 | 默认值 |
| ------ | -------------------------- | ------ | ------ |
| change | 当改变了 `icon` 选中项触发 | String | - |
@@ -0,0 +1,36 @@
/**
* 增加新的图标时,请遵循以下数据结构
* Adding new icon please follow the data structure below
*/
export default [
{
key: 'directional',
title: '方向性图标',
icons: ['step-backward', 'step-forward', 'fast-backward', 'fast-forward', 'shrink', 'arrows-alt', 'down', 'up', 'left', 'right', 'caret-up', 'caret-down', 'caret-left', 'caret-right', 'up-circle', 'down-circle', 'left-circle', 'right-circle', 'double-right', 'double-left', 'vertical-left', 'vertical-right', 'forward', 'backward', 'rollback', 'enter', 'retweet', 'swap', 'swap-left', 'swap-right', 'arrow-up', 'arrow-down', 'arrow-left', 'arrow-right', 'play-circle', 'up-square', 'down-square', 'left-square', 'right-square', 'login', 'logout', 'menu-fold', 'menu-unfold', 'border-bottom', 'border-horizontal', 'border-inner', 'border-left', 'border-right', 'border-top', 'border-verticle', 'pic-center', 'pic-left', 'pic-right', 'radius-bottomleft', 'radius-bottomright', 'radius-upleft', 'fullscreen', 'fullscreen-exit']
},
{
key: 'suggested',
title: '提示建议性图标',
icons: ['question', 'question-circle', 'plus', 'plus-circle', 'pause', 'pause-circle', 'minus', 'minus-circle', 'plus-square', 'minus-square', 'info', 'info-circle', 'exclamation', 'exclamation-circle', 'close', 'close-circle', 'close-square', 'check', 'check-circle', 'check-square', 'clock-circle', 'warning', 'issues-close', 'stop']
},
{
key: 'editor',
title: '编辑类图标',
icons: ['edit', 'form', 'copy', 'scissor', 'delete', 'snippets', 'diff', 'highlight', 'align-center', 'align-left', 'align-right', 'bg-colors', 'bold', 'italic', 'underline', 'strikethrough', 'redo', 'undo', 'zoom-in', 'zoom-out', 'font-colors', 'font-size', 'line-height', 'colum-height', 'dash', 'small-dash', 'sort-ascending', 'sort-descending', 'drag', 'ordered-list', 'radius-setting']
},
{
key: 'data',
title: '数据类图标',
icons: ['area-chart', 'pie-chart', 'bar-chart', 'dot-chart', 'line-chart', 'radar-chart', 'heat-map', 'fall', 'rise', 'stock', 'box-plot', 'fund', 'sliders']
},
{
key: 'brand_logo',
title: '网站通用图标',
icons: ['lock', 'unlock', 'bars', 'book', 'calendar', 'cloud', 'cloud-download', 'code', 'copy', 'credit-card', 'delete', 'desktop', 'download', 'ellipsis', 'file', 'file-text', 'file-unknown', 'file-pdf', 'file-word', 'file-excel', 'file-jpg', 'file-ppt', 'file-markdown', 'file-add', 'folder', 'folder-open', 'folder-add', 'hdd', 'frown', 'meh', 'smile', 'inbox', 'laptop', 'appstore', 'link', 'mail', 'mobile', 'notification', 'paper-clip', 'picture', 'poweroff', 'reload', 'search', 'setting', 'share-alt', 'shopping-cart', 'tablet', 'tag', 'tags', 'to-top', 'upload', 'user', 'video-camera', 'home', 'loading', 'loading-3-quarters', 'cloud-upload', 'star', 'heart', 'environment', 'eye', 'camera', 'save', 'team', 'solution', 'phone', 'filter', 'exception', 'export', 'customer-service', 'qrcode', 'scan', 'like', 'dislike', 'message', 'pay-circle', 'calculator', 'pushpin', 'bulb', 'select', 'switcher', 'rocket', 'bell', 'disconnect', 'database', 'compass', 'barcode', 'hourglass', 'key', 'flag', 'layout', 'printer', 'sound', 'usb', 'skin', 'tool', 'sync', 'wifi', 'car', 'schedule', 'user-add', 'user-delete', 'usergroup-add', 'usergroup-delete', 'man', 'woman', 'shop', 'gift', 'idcard', 'medicine-box', 'red-envelope', 'coffee', 'copyright', 'trademark', 'safety', 'wallet', 'bank', 'trophy', 'contacts', 'global', 'shake', 'api', 'fork', 'dashboard', 'table', 'profile', 'alert', 'audit', 'branches', 'build', 'border', 'crown', 'experiment', 'fire', 'money-collect', 'property-safety', 'read', 'reconciliation', 'rest', 'security-scan', 'insurance', 'interation', 'safety-certificate', 'project', 'thunderbolt', 'block', 'cluster', 'deployment-unit', 'dollar', 'euro', 'pound', 'file-done', 'file-exclamation', 'file-protect', 'file-search', 'file-sync', 'gateway', 'gold', 'robot', 'shopping']
},
{
key: 'application',
title: '品牌和标识',
icons: ['android', 'apple', 'windows', 'ie', 'chrome', 'github', 'aliwangwang', 'dingding', 'weibo-square', 'weibo-circle', 'taobao-circle', 'html5', 'weibo', 'twitter', 'wechat', 'youtube', 'alipay-circle', 'taobao', 'skype', 'qq', 'medium-workmark', 'gitlab', 'medium', 'linkedin', 'google-plus', 'dropbox', 'facebook', 'codepen', 'code-sandbox', 'amazon', 'google', 'codepen-circle', 'alipay', 'ant-design', 'aliyun', 'zhihu', 'slack', 'slack-square', 'behance', 'behance-square', 'dribbble', 'dribbble-square', 'instagram', 'yuque', 'alibaba', 'yahoo']
}
]
@@ -0,0 +1,2 @@
import IconSelector from './IconSelector'
export default IconSelector
@@ -0,0 +1,161 @@
<script>
import events from './events'
export default {
name: 'MultiTab',
data () {
return {
fullPathList: [],
pages: [],
activeKey: '',
newTabIndex: 0
}
},
created () {
// bind event
events.$on('open', val => {
if (!val) {
throw new Error(`multi-tab: open tab ${val} err`)
}
this.activeKey = val
}).$on('close', val => {
if (!val) {
this.closeThat(this.activeKey)
return
}
this.closeThat(val)
}).$on('rename', ({ key, name }) => {
try {
const item = this.pages.find(item => item.path === key)
item.meta.customTitle = name
this.$forceUpdate()
} catch (e) {
}
})
this.pages.push(this.$route)
this.fullPathList.push(this.$route.fullPath)
this.selectedLastPath()
},
methods: {
onEdit (targetKey, action) {
this[action](targetKey)
},
remove (targetKey) {
this.pages = this.pages.filter(page => page.fullPath !== targetKey)
this.fullPathList = this.fullPathList.filter(path => path !== targetKey)
// 判断当前标签是否关闭,若关闭则跳转到最后一个还存在的标签页
if (!this.fullPathList.includes(this.activeKey)) {
this.selectedLastPath()
}
},
selectedLastPath () {
this.activeKey = this.fullPathList[this.fullPathList.length - 1]
},
// content menu
closeThat (e) {
// 判断是否为最后一个标签页,如果是最后一个,则无法被关闭
if (this.fullPathList.length > 1) {
this.remove(e)
} else {
this.$message.info('这是最后一个标签了, 无法被关闭')
}
},
closeLeft (e) {
const currentIndex = this.fullPathList.indexOf(e)
if (currentIndex > 0) {
this.fullPathList.forEach((item, index) => {
if (index < currentIndex) {
this.remove(item)
}
})
} else {
this.$message.info('左侧没有标签')
}
},
closeRight (e) {
const currentIndex = this.fullPathList.indexOf(e)
if (currentIndex < (this.fullPathList.length - 1)) {
this.fullPathList.forEach((item, index) => {
if (index > currentIndex) {
this.remove(item)
}
})
} else {
this.$message.info('右侧没有标签')
}
},
closeAll (e) {
const currentIndex = this.fullPathList.indexOf(e)
this.fullPathList.forEach((item, index) => {
if (index !== currentIndex) {
this.remove(item)
}
})
},
closeMenuClick (key, route) {
this[key](route)
},
renderTabPaneMenu (e) {
return (
<a-menu {...{ on: { click: ({ key, item, domEvent }) => { this.closeMenuClick(key, e) } } }}>
<a-menu-item key="closeThat">关闭当前标签</a-menu-item>
<a-menu-item key="closeRight">关闭右侧</a-menu-item>
<a-menu-item key="closeLeft">关闭左侧</a-menu-item>
<a-menu-item key="closeAll">关闭全部</a-menu-item>
</a-menu>
)
},
// render
renderTabPane (title, keyPath) {
const menu = this.renderTabPaneMenu(keyPath)
return (
<a-dropdown overlay={menu} trigger={['contextmenu']}>
<span style={{ userSelect: 'none' }}>{ title }</span>
</a-dropdown>
)
}
},
watch: {
'$route': function (newVal) {
this.activeKey = newVal.fullPath
if (this.fullPathList.indexOf(newVal.fullPath) < 0) {
this.fullPathList.push(newVal.fullPath)
this.pages.push(newVal)
}
},
activeKey: function (newPathKey) {
this.$router.push({ path: newPathKey })
}
},
render () {
const { onEdit, $data: { pages } } = this
const panes = pages.map(page => {
return (
<a-tab-pane
style={{ height: 0 }}
tab={this.renderTabPane(page.meta.customTitle || page.meta.title, page.fullPath)}
key={page.fullPath} closable={pages.length > 1}
>
</a-tab-pane>)
})
return (
<div class="ant-pro-multi-tab">
<div class="ant-pro-multi-tab-wrapper">
<a-tabs
hideAdd
type={'editable-card'}
v-model={this.activeKey}
tabBarStyle={{ background: '#FFF', margin: 0, paddingLeft: '16px', paddingTop: '1px' }}
{...{ on: { edit: onEdit } }}>
{panes}
</a-tabs>
</div>
</div>
)
}
}
</script>
@@ -0,0 +1,2 @@
import Vue from 'vue'
export default new Vue()

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