v2.2.1: frontend closed-source + Docker one-click deploy

- Remove frontend source code (now in private repo)
- Add pre-built frontend/dist/ with Nginx serving
- Simplify docker-compose.yml (no Node.js build needed)
- Update README with docs index and Docker deploy guide
- Add admin order list and AI analysis stats tabs
- Add quick trade API routes
- Clean up redundant files (package-lock.json, yarn.lock, .iml)
- Add GitHub Actions workflow for frontend update automation
This commit is contained in:
TIANHE
2026-02-27 19:57:23 +08:00
parent ffdd2ffbae
commit abbad97bdd
250 changed files with 1895 additions and 91621 deletions
+84
View File
@@ -0,0 +1,84 @@
# ======================================================
# Workflow: Update Frontend Build
# ======================================================
# This workflow is triggered manually (or via repository_dispatch)
# from your PRIVATE frontend repo after a new build.
#
# Setup:
# 1. In your PRIVATE frontend repo, create a GitHub Action that:
# - Builds the Vue.js project
# - Uses repository_dispatch to trigger this workflow
# - Or: upload the dist as an artifact and trigger this workflow
#
# 2. Create a GitHub Personal Access Token (PAT) with repo access
# and store it as a secret: FRONTEND_DEPLOY_TOKEN
#
# Alternative: Run manually from the Actions tab.
# ======================================================
name: Update Frontend Build
on:
# Manual trigger from GitHub UI
workflow_dispatch:
inputs:
version:
description: 'Frontend version (e.g. 1.2.0)'
required: false
default: 'latest'
# Triggered by private repo via repository_dispatch
repository_dispatch:
types: [frontend-updated]
jobs:
update-frontend:
runs-on: ubuntu-latest
steps:
- name: Checkout open-source repo
uses: actions/checkout@v4
with:
token: ${{ secrets.FRONTEND_DEPLOY_TOKEN || secrets.GITHUB_TOKEN }}
- name: Download frontend build artifact
# Option A: Download from private repo's latest release
# Replace OWNER/PRIVATE_REPO with your private frontend repo
env:
GH_TOKEN: ${{ secrets.FRONTEND_DEPLOY_TOKEN }}
run: |
echo "Downloading frontend build..."
# Option A: From GitHub Release
# gh release download latest -R OWNER/quantdinger-frontend -p 'dist.tar.gz' -D /tmp
# tar -xzf /tmp/dist.tar.gz -C frontend/dist/
# Option B: From repository_dispatch payload
if [ "${{ github.event.client_payload.artifact_url }}" != "" ]; then
curl -L -H "Authorization: token $GH_TOKEN" \
"${{ github.event.client_payload.artifact_url }}" \
-o /tmp/dist.tar.gz
rm -rf frontend/dist/*
tar -xzf /tmp/dist.tar.gz -C frontend/dist/
else
echo "No artifact URL provided. Please use manual upload or release download."
echo "Skipping download step - assuming dist/ is already updated."
fi
- name: Update VERSION
run: |
VERSION="${{ github.event.inputs.version || github.event.client_payload.version || 'latest' }}"
echo "$VERSION" > frontend/VERSION
echo "Frontend version: $VERSION"
- name: Commit and push
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add frontend/dist/ frontend/VERSION
if git diff --staged --quiet; then
echo "No changes to commit"
else
VERSION=$(cat frontend/VERSION)
git commit -m "chore: update frontend build v${VERSION}"
git push
fi
+73 -431
View File
@@ -1,432 +1,74 @@
# ============================================
# QuantDinger - Open Source Repository
# Frontend is closed-source, only dist/ is tracked.
# ============================================
/quantdinger_vue/.browserslistrc
/quantdinger_vue/.dockerignore
/quantdinger_vue/.editorconfig
/quantdinger_vue/.env
/quantdinger_vue/.env.development
/quantdinger_vue/.env.preview
/quantdinger_vue/tests/unit/.eslintrc.js
/quantdinger_vue/.eslintrc.js
/quantdinger_vue/.eslintrc.json
/quantdinger_vue/.gitattributes
/quantdinger_vue/.husky/.gitignore
/quantdinger_vue/.gitignore
/quantdinger_vue/.lintstagedrc.json
/quantdinger_vue/.prettierrc
/quantdinger_vue/.stylelintrc.js
/quantdinger_vue/.travis.yml
/quantdinger_vue/src/views/exception/403.vue
/quantdinger_vue/src/views/exception/404.vue
/quantdinger_vue/src/views/404.vue
/quantdinger_vue/src/views/exception/500.vue
/quantdinger_vue/src/core/directives/action.js
/quantdinger_vue/src/api/ai-trading.js
/quantdinger_vue/src/views/trading-assistant/components/AIDecisionRecords.vue
/quantdinger_vue/src/config/aiModels.js
/quantdinger_vue/src/store/modules/app.js
/quantdinger_vue/src/App.vue
/quantdinger_vue/src/store/app-mixin.js
/quantdinger_vue/src/locales/lang/ar-SA.js
/quantdinger_vue/src/mock/services/article.js
/quantdinger_vue/src/components/ArticleListContent/ArticleListContent.vue
/quantdinger_vue/src/store/modules/async-router.js
/quantdinger_vue/src/api/auth.js
/quantdinger_vue/src/mock/services/auth.js
/quantdinger_vue/public/avatar2.jpg
/quantdinger_vue/src/components/GlobalHeader/AvatarDropdown.vue
/quantdinger_vue/src/utils/axios.js
/quantdinger_vue/babel.config.js
/quantdinger_vue/src/assets/background.svg
/quantdinger_vue/src/views/indicator-analysis/components/BacktestHistoryDrawer.vue
/quantdinger_vue/src/views/indicator-analysis/components/BacktestModal.vue
/quantdinger_vue/src/views/indicator-analysis/components/BacktestRunViewer.vue
/quantdinger_vue/src/components/Charts/Bar.vue
/quantdinger_vue/src/layouts/BasicLayout.less
/quantdinger_vue/src/layouts/BasicLayout.vue
/quantdinger_vue/src/layouts/BlankLayout.vue
/quantdinger_vue/src/core/bootstrap.js
/quantdinger_vue/src/assets/icons/bx-analyse.svg
/quantdinger_vue/deploy/caddy.conf
/quantdinger_vue/src/components/Other/CarbonAds.vue
/quantdinger_vue/src/components/Charts/chart.less
/quantdinger_vue/src/components/Charts/ChartCard.vue
/quantdinger_vue/src/utils/codeDecrypt.js
/quantdinger_vue/commitlint.config.js
/quantdinger_vue/src/api/credentials.js
/quantdinger_vue/src/api/dashboard.js
/quantdinger_vue/src/locales/lang/de-DE.js
/quantdinger_vue/src/config/defaultSettings.js
/quantdinger_vue/src/store/device-mixin.js
/quantdinger_vue/src/components/Dialog.js
/quantdinger_vue/Dockerfile
/quantdinger_vue/src/utils/domUtil.js
/quantdinger_vue/src/components/Ellipsis/Ellipsis.vue
/quantdinger_vue/src/locales/lang/en-US.js
/quantdinger_vue/src/components/MultiTab/events.js
/quantdinger_vue/src/utils/filter.js
/quantdinger_vue/src/components/FooterToolbar/FooterToolBar.vue
/quantdinger_vue/src/locales/lang/fr-FR.js
/quantdinger_vue/src/router/generator-routers.js
/quantdinger_vue/src/store/getters.js
/quantdinger_vue/src/global.less
/quantdinger_vue/src/components/Search/GlobalSearch.jsx
/quantdinger_vue/src/store/i18n-mixin.js
/quantdinger_vue/src/components/IconSelector/icons.js
/quantdinger_vue/src/core/icons.js
/quantdinger_vue/src/components/IconSelector/IconSelector.vue
/quantdinger_vue/public/index.html
/quantdinger_vue/src/components/ArticleListContent/index.js
/quantdinger_vue/src/components/AvatarList/index.js
/quantdinger_vue/src/components/Ellipsis/index.js
/quantdinger_vue/src/components/FooterToolbar/index.js
/quantdinger_vue/src/components/IconSelector/index.js
/quantdinger_vue/src/components/MultiTab/index.js
/quantdinger_vue/src/components/NoticeIcon/index.js
/quantdinger_vue/src/components/NumberInfo/index.js
/quantdinger_vue/src/components/SettingDrawer/index.js
/quantdinger_vue/src/components/StandardFormRow/index.js
/quantdinger_vue/src/components/Table/index.js
/quantdinger_vue/src/components/Trend/index.js
/quantdinger_vue/src/components/index.js
/quantdinger_vue/src/layouts/index.js
/quantdinger_vue/src/locales/index.js
/quantdinger_vue/src/mock/index.js
/quantdinger_vue/src/router/index.js
/quantdinger_vue/src/store/index.js
/quantdinger_vue/src/components/PageLoading/index.jsx
/quantdinger_vue/src/components/SelectLang/index.jsx
/quantdinger_vue/src/components/TagSelect/index.jsx
/quantdinger_vue/src/components/TextArea/index.jsx
/quantdinger_vue/src/components/AvatarList/index.less
/quantdinger_vue/src/components/FooterToolbar/index.less
/quantdinger_vue/src/components/MultiTab/index.less
/quantdinger_vue/src/components/NumberInfo/index.less
/quantdinger_vue/src/components/Search/index.less
/quantdinger_vue/src/components/SelectLang/index.less
/quantdinger_vue/src/components/Trend/index.less
/quantdinger_vue/src/components/index.less
/quantdinger_vue/src/components/AvatarList/index.md
/quantdinger_vue/src/components/Ellipsis/index.md
/quantdinger_vue/src/components/FooterToolbar/index.md
/quantdinger_vue/src/components/NumberInfo/index.md
/quantdinger_vue/src/components/Trend/index.md
/quantdinger_vue/src/components/GlobalFooter/index.vue
/quantdinger_vue/src/components/Turnstile/index.vue
/quantdinger_vue/src/views/ai-analysis/components/index.vue
/quantdinger_vue/src/views/ai-analysis/index.vue
/quantdinger_vue/src/views/dashboard/index.vue
/quantdinger_vue/src/views/indicator-analysis/index.vue
/quantdinger_vue/src/views/indicator-community/index.vue
/quantdinger_vue/src/views/portfolio/index.vue
/quantdinger_vue/src/views/profile/index.vue
/quantdinger_vue/src/views/settings/index.vue
/quantdinger_vue/src/views/trading-assistant/index.vue
/quantdinger_vue/src/views/user-manage/index.vue
/quantdinger_vue/src/views/indicator-analysis/components/IndicatorEditor.vue
/quantdinger_vue/src/components/AvatarList/Item.jsx
/quantdinger_vue/src/locales/lang/ja-JP.js
/quantdinger_vue/jest.config.js
/quantdinger_vue/jsconfig.json
/quantdinger_vue/src/views/indicator-analysis/components/KlineChart.vue
/quantdinger_vue/src/locales/lang/ko-KR.js
/quantdinger_vue/src/core/lazy_use.js
/quantdinger_vue/src/components/Charts/Liquid.vue
/quantdinger_vue/src/components/AvatarList/List.jsx
/quantdinger_vue/src/api/login.js
/quantdinger_vue/src/views/user/Login.vue
/quantdinger_vue/public/logo.png
/quantdinger_vue/src/assets/logo.png
/quantdinger_vue/src/assets/logo.svg
/quantdinger_vue/src/main.js
/quantdinger_vue/src/api/manage.js
/quantdinger_vue/src/mock/services/manage.js
/quantdinger_vue/src/api/market.js
/quantdinger_vue/src/components/Charts/MiniArea.vue
/quantdinger_vue/src/components/Charts/MiniBar.vue
/quantdinger_vue/src/components/Charts/MiniProgress.vue
/quantdinger_vue/src/components/Charts/MiniSmoothArea.vue
/quantdinger_vue/src/components/MultiTab/MultiTab.vue
/quantdinger_vue/src/store/mutation-types.js
/quantdinger_vue/deploy/nginx.conf
/quantdinger_vue/deploy/nginx-docker.conf
/quantdinger_vue/src/components/NoticeIcon/NoticeIcon.vue
/quantdinger_vue/src/components/NProgress/nprogress.less
/quantdinger_vue/src/components/NumberInfo/NumberInfo.vue
/quantdinger_vue/src/mock/services/other.js
/quantdinger_vue/package.json
/quantdinger_vue/src/layouts/PageView.vue
/quantdinger_vue/src/core/permission/permission.js
/quantdinger_vue/src/permission.js
/quantdinger_vue/config/plugin.config.js
/quantdinger_vue/pnpm-lock.yaml
/quantdinger_vue/src/api/portfolio.js
/quantdinger_vue/src/views/trading-assistant/components/PositionRecords.vue
/quantdinger_vue/postcss.config.js
/quantdinger_vue/src/components/Editor/QuillEditor.vue
/quantdinger_vue/src/components/Charts/Radar.vue
/quantdinger_vue/src/components/Charts/RankList.vue
/quantdinger_vue/src/components/IconSelector/README.md
/quantdinger_vue/src/components/Table/README.md
/quantdinger_vue/src/router/README.md
/quantdinger_vue/README.md
/quantdinger_vue/src/views/user/RegisterResult.vue
/quantdinger_vue/src/utils/request.js
/quantdinger_vue/src/components/GlobalHeader/RightContent.vue
/quantdinger_vue/src/utils/routeConvert.js
/quantdinger_vue/src/config/router.config.js
/quantdinger_vue/src/layouts/RouteView.vue
/quantdinger_vue/src/utils/screenLog.js
/quantdinger_vue/src/components/SettingDrawer/settingConfig.js
/quantdinger_vue/src/components/SettingDrawer/SettingDrawer.vue
/quantdinger_vue/src/components/SettingDrawer/SettingItem.vue
/quantdinger_vue/src/api/settings.js
/quantdinger_vue/public/slogo.png
/quantdinger_vue/src/assets/slogo.png
/quantdinger_vue/src/components/Charts/smooth.area.less
/quantdinger_vue/src/components/StandardFormRow/StandardFormRow.vue
/quantdinger_vue/src/store/modules/static-router.js
/quantdinger_vue/src/api/strategy.js
/quantdinger_vue/src/components/TextArea/style.less
/quantdinger_vue/src/mock/services/tagCloud.js
/quantdinger_vue/src/components/Charts/TagCloud.vue
/quantdinger_vue/src/components/TagSelect/TagSelectOption.jsx
/quantdinger_vue/src/locales/lang/th-TH.js
/quantdinger_vue/src/components/SettingDrawer/themeColor.js
/quantdinger_vue/config/themePluginConfig.js
/quantdinger_vue/src/views/trading-assistant/components/TradingRecords.vue
/quantdinger_vue/src/components/Charts/TransferBar.vue
/quantdinger_vue/src/components/Tree/Tree.jsx
/quantdinger_vue/src/components/Charts/Trend.vue
/quantdinger_vue/src/components/Trend/Trend.vue
/quantdinger_vue/src/components/tools/TwoStepCaptcha.vue
/quantdinger_vue/src/core/use.js
/quantdinger_vue/src/api/user.js
/quantdinger_vue/src/mock/services/user.js
/quantdinger_vue/src/store/modules/user.js
/quantdinger_vue/src/layouts/UserLayout.vue
/quantdinger_vue/src/components/_util/util.js
/quantdinger_vue/src/mock/util.js
/quantdinger_vue/src/utils/util.js
/quantdinger_vue/src/utils/utils.less
/quantdinger_vue/src/locales/lang/vi-VN.js
/quantdinger_vue/vue.config.js
/quantdinger_vue/src/components/Editor/WangEditor.vue
/quantdinger_vue/yarn.lock
/quantdinger_vue/src/locales/lang/zh-CN.js
/quantdinger_vue/src/locales/lang/zh-TW.js
/quantdinger_vue/.browserslistrc
/quantdinger_vue/.dockerignore
/quantdinger_vue/.editorconfig
/quantdinger_vue/.env
/quantdinger_vue/.env.development
/quantdinger_vue/.env.preview
/quantdinger_vue/tests/unit/.eslintrc.js
/quantdinger_vue/.eslintrc.js
/quantdinger_vue/.eslintrc.json
/quantdinger_vue/.gitattributes
/quantdinger_vue/.husky/.gitignore
/quantdinger_vue/.gitignore
/quantdinger_vue/.lintstagedrc.json
/quantdinger_vue/.prettierrc
/quantdinger_vue/.stylelintrc.js
/quantdinger_vue/.travis.yml
/quantdinger_vue/src/views/exception/403.vue
/quantdinger_vue/src/views/exception/404.vue
/quantdinger_vue/src/views/404.vue
/quantdinger_vue/src/views/exception/500.vue
/quantdinger_vue/src/core/directives/action.js
/quantdinger_vue/src/api/ai-trading.js
/quantdinger_vue/src/views/trading-assistant/components/AIDecisionRecords.vue
/quantdinger_vue/src/config/aiModels.js
/quantdinger_vue/src/store/modules/app.js
/quantdinger_vue/src/App.vue
/quantdinger_vue/src/store/app-mixin.js
/quantdinger_vue/src/locales/lang/ar-SA.js
/quantdinger_vue/src/mock/services/article.js
/quantdinger_vue/src/components/ArticleListContent/ArticleListContent.vue
/quantdinger_vue/src/store/modules/async-router.js
/quantdinger_vue/src/api/auth.js
/quantdinger_vue/src/mock/services/auth.js
/quantdinger_vue/public/avatar2.jpg
/quantdinger_vue/src/components/GlobalHeader/AvatarDropdown.vue
/quantdinger_vue/src/utils/axios.js
/quantdinger_vue/babel.config.js
/quantdinger_vue/src/assets/background.svg
/quantdinger_vue/src/views/indicator-analysis/components/BacktestHistoryDrawer.vue
/quantdinger_vue/src/views/indicator-analysis/components/BacktestModal.vue
/quantdinger_vue/src/views/indicator-analysis/components/BacktestRunViewer.vue
/quantdinger_vue/src/components/Charts/Bar.vue
/quantdinger_vue/src/layouts/BasicLayout.less
/quantdinger_vue/src/layouts/BasicLayout.vue
/quantdinger_vue/src/layouts/BlankLayout.vue
/quantdinger_vue/src/core/bootstrap.js
/quantdinger_vue/src/assets/icons/bx-analyse.svg
/quantdinger_vue/deploy/caddy.conf
/quantdinger_vue/src/components/Other/CarbonAds.vue
/quantdinger_vue/src/components/Charts/chart.less
/quantdinger_vue/src/components/Charts/ChartCard.vue
/quantdinger_vue/src/utils/codeDecrypt.js
/quantdinger_vue/commitlint.config.js
/quantdinger_vue/src/api/credentials.js
/quantdinger_vue/src/api/dashboard.js
/quantdinger_vue/src/locales/lang/de-DE.js
/quantdinger_vue/src/config/defaultSettings.js
/quantdinger_vue/src/store/device-mixin.js
/quantdinger_vue/src/components/Dialog.js
/quantdinger_vue/Dockerfile
/quantdinger_vue/src/utils/domUtil.js
/quantdinger_vue/src/components/Ellipsis/Ellipsis.vue
/quantdinger_vue/src/locales/lang/en-US.js
/quantdinger_vue/src/components/MultiTab/events.js
/quantdinger_vue/src/utils/filter.js
/quantdinger_vue/src/components/FooterToolbar/FooterToolBar.vue
/quantdinger_vue/src/locales/lang/fr-FR.js
/quantdinger_vue/src/router/generator-routers.js
/quantdinger_vue/src/store/getters.js
/quantdinger_vue/src/global.less
/quantdinger_vue/src/components/Search/GlobalSearch.jsx
/quantdinger_vue/src/store/i18n-mixin.js
/quantdinger_vue/src/components/IconSelector/icons.js
/quantdinger_vue/src/core/icons.js
/quantdinger_vue/src/components/IconSelector/IconSelector.vue
/quantdinger_vue/public/index.html
/quantdinger_vue/src/components/ArticleListContent/index.js
/quantdinger_vue/src/components/AvatarList/index.js
/quantdinger_vue/src/components/Ellipsis/index.js
/quantdinger_vue/src/components/FooterToolbar/index.js
/quantdinger_vue/src/components/IconSelector/index.js
/quantdinger_vue/src/components/MultiTab/index.js
/quantdinger_vue/src/components/NoticeIcon/index.js
/quantdinger_vue/src/components/NumberInfo/index.js
/quantdinger_vue/src/components/SettingDrawer/index.js
/quantdinger_vue/src/components/StandardFormRow/index.js
/quantdinger_vue/src/components/Table/index.js
/quantdinger_vue/src/components/Trend/index.js
/quantdinger_vue/src/components/index.js
/quantdinger_vue/src/layouts/index.js
/quantdinger_vue/src/locales/index.js
/quantdinger_vue/src/mock/index.js
/quantdinger_vue/src/router/index.js
/quantdinger_vue/src/store/index.js
/quantdinger_vue/src/components/PageLoading/index.jsx
/quantdinger_vue/src/components/SelectLang/index.jsx
/quantdinger_vue/src/components/TagSelect/index.jsx
/quantdinger_vue/src/components/TextArea/index.jsx
/quantdinger_vue/src/components/AvatarList/index.less
/quantdinger_vue/src/components/FooterToolbar/index.less
/quantdinger_vue/src/components/MultiTab/index.less
/quantdinger_vue/src/components/NumberInfo/index.less
/quantdinger_vue/src/components/Search/index.less
/quantdinger_vue/src/components/SelectLang/index.less
/quantdinger_vue/src/components/Trend/index.less
/quantdinger_vue/src/components/index.less
/quantdinger_vue/src/components/AvatarList/index.md
/quantdinger_vue/src/components/Ellipsis/index.md
/quantdinger_vue/src/components/FooterToolbar/index.md
/quantdinger_vue/src/components/NumberInfo/index.md
/quantdinger_vue/src/components/Trend/index.md
/quantdinger_vue/src/components/GlobalFooter/index.vue
/quantdinger_vue/src/components/Turnstile/index.vue
/quantdinger_vue/src/views/ai-analysis/components/index.vue
/quantdinger_vue/src/views/ai-analysis/index.vue
/quantdinger_vue/src/views/dashboard/index.vue
/quantdinger_vue/src/views/indicator-analysis/index.vue
/quantdinger_vue/src/views/indicator-community/index.vue
/quantdinger_vue/src/views/portfolio/index.vue
/quantdinger_vue/src/views/profile/index.vue
/quantdinger_vue/src/views/settings/index.vue
/quantdinger_vue/src/views/trading-assistant/index.vue
/quantdinger_vue/src/views/user-manage/index.vue
/quantdinger_vue/src/views/indicator-analysis/components/IndicatorEditor.vue
/quantdinger_vue/src/components/AvatarList/Item.jsx
/quantdinger_vue/src/locales/lang/ja-JP.js
/quantdinger_vue/jest.config.js
/quantdinger_vue/jsconfig.json
/quantdinger_vue/src/views/indicator-analysis/components/KlineChart.vue
/quantdinger_vue/src/locales/lang/ko-KR.js
/quantdinger_vue/src/core/lazy_use.js
/quantdinger_vue/src/components/Charts/Liquid.vue
/quantdinger_vue/src/components/AvatarList/List.jsx
/quantdinger_vue/src/api/login.js
/quantdinger_vue/src/views/user/Login.vue
/quantdinger_vue/public/logo.png
/quantdinger_vue/src/assets/logo.png
/quantdinger_vue/src/assets/logo.svg
/quantdinger_vue/src/main.js
/quantdinger_vue/src/api/manage.js
/quantdinger_vue/src/mock/services/manage.js
/quantdinger_vue/src/api/market.js
/quantdinger_vue/src/components/Charts/MiniArea.vue
/quantdinger_vue/src/components/Charts/MiniBar.vue
/quantdinger_vue/src/components/Charts/MiniProgress.vue
/quantdinger_vue/src/components/Charts/MiniSmoothArea.vue
/quantdinger_vue/src/components/MultiTab/MultiTab.vue
/quantdinger_vue/src/store/mutation-types.js
/quantdinger_vue/deploy/nginx.conf
/quantdinger_vue/deploy/nginx-docker.conf
/quantdinger_vue/src/components/NoticeIcon/NoticeIcon.vue
/quantdinger_vue/src/components/NProgress/nprogress.less
/quantdinger_vue/src/components/NumberInfo/NumberInfo.vue
/quantdinger_vue/src/mock/services/other.js
/quantdinger_vue/package.json
/quantdinger_vue/src/layouts/PageView.vue
/quantdinger_vue/src/core/permission/permission.js
/quantdinger_vue/src/permission.js
/quantdinger_vue/config/plugin.config.js
/quantdinger_vue/pnpm-lock.yaml
/quantdinger_vue/src/api/portfolio.js
/quantdinger_vue/src/views/trading-assistant/components/PositionRecords.vue
/quantdinger_vue/postcss.config.js
/quantdinger_vue/src/components/Editor/QuillEditor.vue
/quantdinger_vue/src/components/Charts/Radar.vue
/quantdinger_vue/src/components/Charts/RankList.vue
/quantdinger_vue/src/components/IconSelector/README.md
/quantdinger_vue/src/components/Table/README.md
/quantdinger_vue/src/router/README.md
/quantdinger_vue/README.md
/quantdinger_vue/src/views/user/RegisterResult.vue
/quantdinger_vue/src/utils/request.js
/quantdinger_vue/src/components/GlobalHeader/RightContent.vue
/quantdinger_vue/src/utils/routeConvert.js
/quantdinger_vue/src/config/router.config.js
/quantdinger_vue/src/layouts/RouteView.vue
/quantdinger_vue/src/utils/screenLog.js
/quantdinger_vue/src/components/SettingDrawer/settingConfig.js
/quantdinger_vue/src/components/SettingDrawer/SettingDrawer.vue
/quantdinger_vue/src/components/SettingDrawer/SettingItem.vue
/quantdinger_vue/src/api/settings.js
/quantdinger_vue/public/slogo.png
/quantdinger_vue/src/assets/slogo.png
/quantdinger_vue/src/components/Charts/smooth.area.less
/quantdinger_vue/src/components/StandardFormRow/StandardFormRow.vue
/quantdinger_vue/src/store/modules/static-router.js
/quantdinger_vue/src/api/strategy.js
/quantdinger_vue/src/components/TextArea/style.less
/quantdinger_vue/src/mock/services/tagCloud.js
/quantdinger_vue/src/components/Charts/TagCloud.vue
/quantdinger_vue/src/components/TagSelect/TagSelectOption.jsx
/quantdinger_vue/src/locales/lang/th-TH.js
/quantdinger_vue/src/components/SettingDrawer/themeColor.js
/quantdinger_vue/config/themePluginConfig.js
/quantdinger_vue/src/views/trading-assistant/components/TradingRecords.vue
/quantdinger_vue/src/components/Charts/TransferBar.vue
/quantdinger_vue/src/components/Tree/Tree.jsx
/quantdinger_vue/src/components/Charts/Trend.vue
/quantdinger_vue/src/components/Trend/Trend.vue
/quantdinger_vue/src/components/tools/TwoStepCaptcha.vue
/quantdinger_vue/src/core/use.js
/quantdinger_vue/src/api/user.js
/quantdinger_vue/src/mock/services/user.js
/quantdinger_vue/src/store/modules/user.js
/quantdinger_vue/src/layouts/UserLayout.vue
/quantdinger_vue/src/components/_util/util.js
/quantdinger_vue/src/mock/util.js
/quantdinger_vue/src/utils/util.js
/quantdinger_vue/src/utils/utils.less
/quantdinger_vue/src/locales/lang/vi-VN.js
/quantdinger_vue/vue.config.js
/quantdinger_vue/src/components/Editor/WangEditor.vue
/quantdinger_vue/yarn.lock
/quantdinger_vue/src/locales/lang/zh-CN.js
/quantdinger_vue/src/locales/lang/zh-TW.js
/quantdinger_vue/.browserslistrc
# ========================
# Frontend Source (CLOSED SOURCE - NOT tracked)
# ========================
/quantdinger_vue/
# ========================
# Frontend dist - DO track (pre-built)
# ========================
!/frontend/
!/frontend/dist/
!/frontend/dist/**
# ========================
# Environment & Secrets
# ========================
.env
.env.local
.env.*.local
backend_api_python/.env
# ========================
# IDE & OS
# ========================
.idea/
.vscode/
*.swp
*.swo
*~
.DS_Store
Thumbs.db
*.iml
# ========================
# Python
# ========================
__pycache__/
*.py[cod]
*$py.class
*.egg-info/
dist/
build/
*.egg
.eggs/
venv/
.venv/
env/
# ========================
# Node (if any remain)
# ========================
node_modules/
npm-debug.log*
yarn-debug.log*
yarn-error.log*
package-lock.json
yarn.lock
# ========================
# Logs & Runtime Data
# ========================
*.log
backend_api_python/logs/
backend_api_python/data/quantdinger.db
backend_api_python/data/memory/
# ========================
# Docker
# ========================
docker-compose.override.yml
-12
View File
@@ -1,12 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="PYTHON_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$" />
<orderEntry type="jdk" jdkName="Python 3.12 (QuantDinger)" jdkType="Python SDK" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
<component name="TemplatesService">
<option name="TEMPLATE_CONFIGURATION" value="Django" />
</component>
</module>
+184 -89
View File
@@ -27,8 +27,8 @@
<p align="center">
<a href="LICENSE"><img src="https://img.shields.io/badge/License-Apache%202.0-blue.svg?style=flat-square&logo=apache" alt="License"></a>
<img src="https://img.shields.io/badge/Version-2.2.1-orange?style=flat-square" alt="Version">
<img src="https://img.shields.io/badge/Python-3.10+-3776AB?style=flat-square&logo=python&logoColor=white" alt="Python">
<img src="https://img.shields.io/badge/Vue.js-2.x-4FC08D?style=flat-square&logo=vue.js&logoColor=white" alt="Vue">
<img src="https://img.shields.io/badge/Docker-Ready-2496ED?style=flat-square&logo=docker&logoColor=white" alt="Docker">
<img src="https://img.shields.io/github/stars/brokermr810/QuantDinger?style=flat-square&logo=github" alt="Stars">
</p>
@@ -62,12 +62,12 @@ QuantDinger is built for traders, researchers, and engineers who:
### Core Value
- **🔓 Apache 2.0 Open Source (Code)**: Permissive and commercial-friendly
- **🔓 Apache 2.0 Open Source (Backend)**: Permissive and commercial-friendly
- **🐍 Python-Native & Visual**: Write indicators in Python with AI assistance, visualize on built-in K-line charts
- **🤖 AI-Loop Optimization**: AI analyzes backtest results to suggest parameter tuning, forming a closed optimization loop
- **🌍 Universal Market Access**: Crypto (Live), US Stocks (IBKR), Forex (MT5), Futures (Data/Notify)
- **💳 Built-in Monetization**: Membership subscription, credit system, USDT on-chain payment
- **⚡ Docker & Clean Arch**: 4-line command deployment with modern tech stack
- **⚡ Docker One-Click Deploy**: `docker-compose up -d` — zero dependency, zero build, production-ready in 2 minutes
---
@@ -82,17 +82,64 @@ QuantDinger is built for traders, researchers, and engineers who:
---
## 📚 Documentation
## 📚 Documentation Index
### Guides
- [Python Strategy Development Guide](docs/STRATEGY_DEV_GUIDE.md)
- [Interactive Brokers (IBKR) Trading Guide](docs/IBKR_TRADING_GUIDE_EN.md) 🆕
- [MetaTrader 5 (MT5) Trading Guide](docs/MT5_TRADING_GUIDE_EN.md) 🆕
All detailed guides and tutorials are in the [`docs/`](docs/) folder. Click any link below to jump directly.
### Notification Configuration
- [Telegram Notification Setup](docs/NOTIFICATION_TELEGRAM_CONFIG_EN.md)
- [Email (SMTP) Notification Setup](docs/NOTIFICATION_EMAIL_CONFIG_EN.md)
- [SMS (Twilio) Notification Setup](docs/NOTIFICATION_SMS_CONFIG_EN.md)
### 📋 General
| Document | Description |
|----------|-------------|
| [Changelog](docs/CHANGELOG.md) | Version history, new features, bug fixes, and migration notes |
| [Multi-User Setup](docs/multi-user-setup.md) | PostgreSQL-based multi-user deployment guide |
### 🐍 Strategy Development
| Document | Language |
|----------|----------|
| [Strategy Development Guide](docs/STRATEGY_DEV_GUIDE.md) | 🇺🇸 English |
| [策略开发指南](docs/STRATEGY_DEV_GUIDE_CN.md) | 🇨🇳 简体中文 |
| [策略開發指南](docs/STRATEGY_DEV_GUIDE_TW.md) | 🇹🇼 繁體中文 |
| [ストラテジー開発ガイド](docs/STRATEGY_DEV_GUIDE_JA.md) | 🇯🇵 日本語 |
| [전략 개발 가이드](docs/STRATEGY_DEV_GUIDE_KO.md) | 🇰🇷 한국어 |
### 📈 Cross-Sectional Strategy
| Document | Language |
|----------|----------|
| [Cross-Sectional Strategy Guide](docs/CROSS_SECTIONAL_STRATEGY_GUIDE_EN.md) | 🇺🇸 English |
| [截面策略开发指南](docs/CROSS_SECTIONAL_STRATEGY_GUIDE_CN.md) | 🇨🇳 简体中文 |
### 🏦 Broker Integration
| Document | Description |
|----------|-------------|
| [IBKR Trading Guide](docs/IBKR_TRADING_GUIDE_EN.md) | Interactive Brokers (US Stocks) integration |
| [MT5 Trading Guide (EN)](docs/MT5_TRADING_GUIDE_EN.md) | MetaTrader 5 (Forex) integration — English |
| [MT5 交易指南 (CN)](docs/MT5_TRADING_GUIDE_CN.md) | MetaTrader 5 (Forex) 集成指南 — 中文 |
### 🔐 OAuth Configuration
| Document | Language |
|----------|----------|
| [OAuth Configuration (EN)](docs/OAUTH_CONFIG_EN.md) | 🇺🇸 Google & GitHub OAuth setup |
| [OAuth 配置指南 (CN)](docs/OAUTH_CONFIG_CN.md) | 🇨🇳 Google & GitHub OAuth 配置 |
### 🔔 Notification Configuration
| Channel | English | 中文 |
|---------|---------|------|
| **Telegram** | [Setup Guide](docs/NOTIFICATION_TELEGRAM_CONFIG_EN.md) | [配置指南](docs/NOTIFICATION_TELEGRAM_CONFIG_CH.md) |
| **Email (SMTP)** | [Setup Guide](docs/NOTIFICATION_EMAIL_CONFIG_EN.md) | [配置指南](docs/NOTIFICATION_EMAIL_CONFIG_CH.md) |
| **SMS (Twilio)** | [Setup Guide](docs/NOTIFICATION_SMS_CONFIG_EN.md) | [配置指南](docs/NOTIFICATION_SMS_CONFIG_CH.md) |
### 💻 Code Examples
| File | Description |
|------|-------------|
| [docs/examples/](docs/examples/) | Python strategy code examples and templates |
---
## 📸 Visual Tour
@@ -175,13 +222,14 @@ QuantDinger is built for traders, researchers, and engineers who:
*Fast, Accurate, Multi-Market Intelligence.*
- **Fast Analysis Mode**: Single LLM call architecture for quick, accurate analysis
- **AI Trading Opportunities Radar** 🆕: Auto-scans Crypto, US Stocks, and Forex markets every hour, displaying opportunities in a rolling carousel
- **AI Trading Opportunities Radar**: Auto-scans Crypto, US Stocks, and Forex markets every hour, displaying opportunities in a rolling carousel
- **Quick Trade Panel (⚡闪电交易)**: Side-sliding trade panel — see an AI signal or indicator opportunity, click "Trade Now" to instantly place an order without leaving the page. Supports market/limit orders, leverage, TP/SL price, and one-click position close.
- **ATR-Based Trading Levels**: Stop-loss and take-profit recommendations based on technical analysis
- **Analysis Memory**: Store analysis results for history review and continuous learning
- **Strategic Integration**: AI analysis can serve as a "Market Filter" for your strategies
### 4. Membership & Billing System 🆕
*Built-in Monetization for SaaS Deployment.*
### 4. Membership & Billing System
*Built-in Monetization for Deployment.*
- **Subscription Plans**: Monthly / Yearly / Lifetime tiers with configurable pricing
- **Credit System**: Each plan includes credits; lifetime members receive monthly credit bonuses
@@ -193,7 +241,7 @@ QuantDinger is built for traders, researchers, and engineers who:
- **Publish & Share**: Share your Python indicators with the community
- **Credit-Based Purchase**: Buy premium indicators from other users with credits
- **VIP Free Indicators** 🆕: Mark indicators as "VIP Free" — VIP members can use them without spending credits
- **VIP Free Indicators**: Mark indicators as "VIP Free" — VIP members can use them without spending credits
- **Rating & Reviews**: Rate and review purchased indicators
- **Live Performance Tracking**: Real-time performance stats aggregated from backtests and live trades
@@ -320,10 +368,10 @@ Simply configure your preferred provider's API key in `.env`. The system auto-de
### 11. Tech Stack
- **Backend**: Python (Flask) + PostgreSQL + Redis (optional)
- **Frontend**: Vue 2 + Ant Design Vue + KlineCharts/ECharts
- **Frontend**: Pre-built (Ant Design Vue + KlineCharts/ECharts)
- **Payment**: USDT TRC20 on-chain (HD Wallet xpub derivation + TronGrid API)
- **Mobile**: Vue 3 + Capacitor (Android / iOS) — see `QuantDinger-Mobile/`
- **Deployment**: Docker Compose (with PostgreSQL)
- **Deployment**: Docker Compose (one-click, zero build)
- **Current Version**: V2.2.1 ([Changelog](docs/CHANGELOG.md))
---
@@ -381,120 +429,165 @@ All UI elements, error messages, and documentation are fully translated. Languag
---
## 🚀 Quick Start
## 🚀 Quick Start (Docker One-Click Deploy)
### Option 1: Docker (Recommended)
> **Prerequisites**: Docker & Docker Compose installed.
> No Node.js, no Python environment needed — everything runs in containers.
### 1. Clone & Configure
```bash
# 1. Clone & configure
git clone https://github.com/brokermr810/QuantDinger.git
cd QuantDinger
```
### 2. Set Up Environment
```bash
# Copy the environment template
cp backend_api_python/env.example backend_api_python/.env
```
# 2. Edit .env — set your admin password & AI API key
# ADMIN_USER=quantdinger
# ADMIN_PASSWORD=your_password
# OPENROUTER_API_KEY=your_key (optional, for AI features)
> **Windows PowerShell**:
> ```powershell
> Copy-Item backend_api_python\env.example -Destination backend_api_python\.env
> ```
# 3. Start all services
Edit `backend_api_python/.env` with your settings:
```ini
# Required — Change these for production!
ADMIN_USER=quantdinger
ADMIN_PASSWORD=your_secure_password
SECRET_KEY=your_random_secret_key
# Optional — Enable AI features
OPENROUTER_API_KEY=your_openrouter_key
# or
OPENAI_API_KEY=your_openai_key
```
### 3. Launch
```bash
docker-compose up -d --build
```
> **Windows PowerShell**: use `Copy-Item backend_api_python\env.example -Destination backend_api_python\.env` instead of `cp`.
**That's it!** Services will be available at:
**That's it!** 🎉 Wait about 30 seconds for all services to start.
| Service | URL |
|---------|-----|
| Frontend UI | http://localhost:8888 |
| Backend API | http://localhost:5000 |
| **Frontend UI** | http://localhost:8888 |
| Backend API | http://localhost:5000 (internal) |
| PostgreSQL | localhost:5432 (internal) |
Default login: `quantdinger` / `123456` (change in `.env` for production).
#### Common Docker Commands
### Common Docker Commands
```bash
docker-compose ps # View status
docker-compose logs -f backend # View backend logs
docker-compose restart # Restart services
docker-compose up -d --build # Rebuild & restart
docker-compose down # Stop services
docker-compose ps # View service status
docker-compose logs -f backend # View backend logs (real-time)
docker-compose logs -f frontend # View frontend/nginx logs
docker-compose restart backend # Restart backend only
docker-compose up -d --build # Rebuild & restart all
docker-compose down # Stop all services
```
#### Update to Latest Version
### Update to Latest Version
```bash
git pull && docker-compose up -d --build
git pull
docker-compose up -d --build
```
#### Backup & Restore
### Backup & Restore
```bash
# Backup database
docker exec quantdinger-db pg_dump -U quantdinger quantdinger > backup.sql
docker exec quantdinger-db pg_dump -U quantdinger quantdinger > backup_$(date +%Y%m%d).sql
# Restore database
cat backup.sql | docker exec -i quantdinger-db psql -U quantdinger quantdinger
```
---
### Custom Port
### Option 2: Local Development
Create a `.env` file in the project root to override docker-compose defaults:
**Prerequisites**: Python 3.10+, Node.js 16+, PostgreSQL 14+
```bash
# 1. Setup database
sudo -u postgres psql -c "CREATE DATABASE quantdinger; CREATE USER quantdinger WITH ENCRYPTED PASSWORD 'your_password'; GRANT ALL PRIVILEGES ON DATABASE quantdinger TO quantdinger;"
psql -U quantdinger -d quantdinger -f backend_api_python/migrations/init.sql
# 2. Start backend
cd backend_api_python
pip install -r requirements.txt
cp env.example .env # Edit .env with your DATABASE_URL
python run.py # → http://localhost:5000
# 3. Start frontend (in another terminal)
cd quantdinger_vue
npm install
npm run serve # → http://localhost:8000
```ini
FRONTEND_PORT=3000 # Change frontend port (default: 8888)
BACKEND_PORT=127.0.0.1:5001 # Change backend port (default: 5000)
DB_PORT=127.0.0.1:5433 # Change database port (default: 5432)
```
---
### Architecture
## 🏗️ Architecture
```text
┌─────────────────────────────┐
quantdinger_vue
(Vue 2 + Ant Design Vue)
────────────────────────────
│ HTTP (/api/*)
┌─────────────────────────────┐
backend_api_python
(Flask + strategy runtime)
└──────────────┬──────────────┘
├─ PostgreSQL (multi-user, orders, membership)
├─ Redis (optional cache)
├─ TronGrid API (USDT payment verification)
└─ Data providers / LLMs / Exchanges
┌────────────────────────────────────────
Docker Compose
│ ┌──────────────────────────────────┐ │
│ │ frontend (Nginx) │ │
│ │ Pre-built static files │ │
│ │ → :8888 │ │
└──────────────┬───────────────────┘
│ /api/* proxy
│ ▼ │
│ ┌──────────────────────────────────┐
│ │ backend (Python/Flask) │ │
│ │ API + AI + Strategy Runtime │ │
│ │ → :5000 │ │
│ └──────────────┬───────────────────┘ │
│ │ │
│ ┌──────────────▼───────────────────┐ │
│ │ postgres (PostgreSQL 16) │ │
│ │ Users, Orders, Strategies, ... │ │
│ │ → :5432 │ │
│ └──────────────────────────────────┘ │
│ │
│ External connections: │
│ ├─ LLM APIs (OpenRouter/OpenAI/...) │
│ ├─ Exchange APIs (Binance/OKX/...) │
│ ├─ TronGrid API (USDT payment) │
│ └─ Data providers (Yahoo/Finnhub/...) │
└────────────────────────────────────────┘
```
### Repository Layout
```text
.
├─ backend_api_python/ # Flask API + AI + backtest + strategy + billing
─ app/
├─ routes/ # API endpoints (user, billing, indicator, etc.)
│ │ ─ services/ # Business logic (trading, payment, community)
├─ migrations/init.sql # Database schema
├─ env.example # Copy to .env for configuration
└─ run.py # Entrypoint
├─ quantdinger_vue/ # Vue 2 UI (Ant Design Vue)
└─ QuantDinger-Mobile/ # Vue 3 + Capacitor mobile app (optional)
QuantDinger/
├─ backend_api_python/ # 🐍 Backend API (Open Source)
├── app/
├─ routes/ # API endpoints (user, billing, strategy, ...)
├── services/ # Business logic (trading, payment, AI, ...)
│ ├── data_sources/ # Market data providers
│ └── utils/ # Helpers (DB, auth, etc.)
├── migrations/init.sql # Database schema
│ ├── env.example # ⚙️ Configuration template — copy to .env
│ ├── Dockerfile # Backend container image
│ └── run.py # Entrypoint
├── frontend/ # 🎨 Frontend (Pre-built)
│ ├── dist/ # Compiled static files (HTML/JS/CSS)
│ ├── Dockerfile # Nginx container image
│ ├── nginx.conf # Nginx config (SPA + API proxy)
│ └── VERSION # Frontend version tracker
├── docs/ # 📚 Documentation & Guides
│ ├── CHANGELOG.md # Version history
│ ├── screenshots/ # UI screenshots
│ └── *.md # Strategy, broker, notification guides
├── docker-compose.yml # 🐳 One-click deployment
├── LICENSE # Apache License 2.0
├── TRADEMARKS.md # Trademark policy
├── SECURITY.md # Security policy
├── CONTRIBUTING.md # Contribution guide
└── CODE_OF_CONDUCT.md # Code of conduct
```
---
@@ -511,8 +604,8 @@ Use `backend_api_python/env.example` as a template. Key settings:
| **OAuth** | `GOOGLE_CLIENT_ID`, `GITHUB_CLIENT_ID`, etc. |
| **Security** | `TURNSTILE_SITE_KEY`, `ENABLE_REGISTRATION` |
| **Order Execution** | `ORDER_MODE` (market/maker), `MAKER_WAIT_SEC` |
| **Membership** 🆕 | `MEMBERSHIP_MONTHLY_PRICE_USD`, `MEMBERSHIP_MONTHLY_CREDITS`, `MEMBERSHIP_YEARLY_PRICE_USD`, etc. |
| **USDT Payment** 🆕 | `USDT_PAY_ENABLED`, `USDT_TRC20_XPUB`, `TRONGRID_API_KEY`, `USDT_ORDER_EXPIRE_MINUTES` |
| **Membership** | `MEMBERSHIP_MONTHLY_PRICE_USD`, `MEMBERSHIP_MONTHLY_CREDITS`, `MEMBERSHIP_YEARLY_PRICE_USD`, etc. |
| **USDT Payment** | `USDT_PAY_ENABLED`, `USDT_TRC20_XPUB`, `TRONGRID_API_KEY`, `USDT_ORDER_EXPIRE_MINUTES` |
| **Proxy** | `PROXY_PORT` or `PROXY_URL` |
| **Workers** | `ENABLE_PENDING_ORDER_WORKER`, `ENABLE_PORTFOLIO_MONITOR` |
@@ -524,7 +617,7 @@ The backend provides REST endpoints for login, market data, indicators, backtest
- Health: `GET /api/health`
- Auth: `POST /api/user/login`, `GET /api/user/info`
- Billing: `GET /api/billing/plans`, `POST /api/billing/usdt/create-order` 🆕
- Billing: `GET /api/billing/plans`, `POST /api/billing/usdt/create-order`
For the full route list, see `backend_api_python/app/routes/`.
@@ -534,6 +627,8 @@ For the full route list, see `backend_api_python/app/routes/`.
Licensed under the **Apache License 2.0**. See `LICENSE`.
> **Note**: The frontend UI is provided as pre-built files. The backend source code is fully open under Apache 2.0.
---
## 🤝 Community & Support
+26
View File
@@ -74,6 +74,31 @@ def start_pending_order_worker():
logger.error(f"Failed to start pending order worker: {e}")
def start_usdt_order_worker():
"""Start the USDT order background worker.
Periodically scans pending/paid USDT orders and checks on-chain status.
Ensures orders are confirmed even if the user closes the browser after payment.
Only starts if USDT_PAY_ENABLED=true.
"""
import os
if str(os.getenv("USDT_PAY_ENABLED", "False")).lower() not in ("1", "true", "yes"):
logger.info("USDT order worker not started (USDT_PAY_ENABLED is not true).")
return
# Avoid running twice with Flask reloader
debug = os.getenv("PYTHON_API_DEBUG", "false").lower() == "true"
if debug:
if os.environ.get("WERKZEUG_RUN_MAIN") != "true":
return
try:
from app.services.usdt_payment_service import get_usdt_order_worker
get_usdt_order_worker().start()
except Exception as e:
logger.error(f"Failed to start USDT order worker: {e}")
def restore_running_strategies():
"""
Restore running strategies on startup.
@@ -231,6 +256,7 @@ def create_app(config_name='default'):
with app.app_context():
start_pending_order_worker()
start_portfolio_monitor()
start_usdt_order_worker()
restore_running_strategies()
return app
+3 -1
View File
@@ -25,6 +25,7 @@ def register_routes(app: Flask):
from app.routes.community import community_bp
from app.routes.fast_analysis import fast_analysis_bp
from app.routes.billing import billing_bp
from app.routes.quick_trade import quick_trade_bp
app.register_blueprint(health_bp)
app.register_blueprint(auth_bp, url_prefix='/api/auth') # Auth routes
@@ -44,4 +45,5 @@ def register_routes(app: Flask):
app.register_blueprint(global_market_bp, url_prefix='/api/global-market')
app.register_blueprint(community_bp, url_prefix='/api/community')
app.register_blueprint(fast_analysis_bp, url_prefix='/api/fast-analysis')
app.register_blueprint(billing_bp, url_prefix='/api/billing')
app.register_blueprint(billing_bp, url_prefix='/api/billing')
app.register_blueprint(quick_trade_bp, url_prefix='/api/quick-trade')
+62 -20
View File
@@ -6,7 +6,6 @@ Local deployment notes:
- Credentials are stored as plaintext JSON in DB (encrypted_config column kept for compatibility).
"""
import time
import traceback
import json
from flask import Blueprint, request, jsonify, g
@@ -42,7 +41,7 @@ def list_credentials():
"""
SELECT id, user_id, name, exchange_id, api_key_hint, created_at, updated_at
FROM qd_exchange_credentials
WHERE user_id = ?
WHERE user_id = %s
ORDER BY id DESC
""",
(user_id,)
@@ -57,41 +56,84 @@ def list_credentials():
return jsonify({'code': 0, 'msg': str(e), 'data': {'items': []}}), 500
CRYPTO_EXCHANGES = [
'binance', 'okx', 'bitget', 'bybit', 'coinbaseexchange',
'kraken', 'kucoin', 'gate', 'bitfinex', 'deepcoin'
]
@credentials_bp.route('/create', methods=['POST'])
@login_required
def create_credential():
"""Create a new credential for the current user."""
"""Create a new credential for the current user.
Supports crypto exchanges, IBKR (US stocks) and MT5 (Forex).
"""
try:
user_id = g.user_id
data = request.get_json() or {}
name = (data.get('name') or '').strip()
exchange_id = (data.get('exchange_id') or '').strip()
api_key = (data.get('api_key') or '').strip()
secret_key = (data.get('secret_key') or '').strip()
passphrase = (data.get('passphrase') or '').strip()
exchange_id = (data.get('exchange_id') or '').strip().lower()
if not exchange_id:
return jsonify({'code': 0, 'msg': 'Missing exchange_id', 'data': None}), 400
if not api_key or not secret_key:
return jsonify({'code': 0, 'msg': 'Missing api_key/secret_key', 'data': None}), 400
plaintext_config = json.dumps({
'exchange_id': exchange_id,
'api_key': api_key,
'secret_key': secret_key,
'passphrase': passphrase
}, ensure_ascii=False)
config = {'exchange_id': exchange_id}
hint = ''
if exchange_id == 'ibkr':
# Interactive Brokers (US stocks)
config.update({
'ibkr_host': (data.get('ibkr_host') or '127.0.0.1').strip(),
'ibkr_port': int(data.get('ibkr_port') or 7497),
'ibkr_client_id': int(data.get('ibkr_client_id') or 1),
'ibkr_account': (data.get('ibkr_account') or '').strip()
})
hint = f"{config['ibkr_host']}:{config['ibkr_port']}"
elif exchange_id == 'mt5':
# MetaTrader 5 (Forex)
mt5_server = (data.get('mt5_server') or '').strip()
mt5_login = str(data.get('mt5_login') or '').strip()
mt5_password = (data.get('mt5_password') or '').strip()
if not mt5_server or not mt5_login or not mt5_password:
return jsonify({'code': 0, 'msg': 'Missing mt5_server/mt5_login/mt5_password', 'data': None}), 400
config.update({
'mt5_server': mt5_server,
'mt5_login': mt5_login,
'mt5_password': mt5_password,
'mt5_terminal_path': (data.get('mt5_terminal_path') or '').strip()
})
hint = f"{mt5_server}/{mt5_login}"
elif exchange_id in CRYPTO_EXCHANGES:
# Crypto exchanges
api_key = (data.get('api_key') or '').strip()
secret_key = (data.get('secret_key') or '').strip()
if not api_key or not secret_key:
return jsonify({'code': 0, 'msg': 'Missing api_key/secret_key', 'data': None}), 400
config.update({
'api_key': api_key,
'secret_key': secret_key,
'passphrase': (data.get('passphrase') or '').strip(),
'enable_demo_trading': bool(data.get('enable_demo_trading', False))
})
hint = _api_key_hint(api_key)
else:
return jsonify({'code': 0, 'msg': f'Unsupported exchange: {exchange_id}', 'data': None}), 400
plaintext_config = json.dumps(config, ensure_ascii=False)
with get_db_connection() as db:
cur = db.cursor()
cur.execute(
"""
INSERT INTO qd_exchange_credentials (user_id, name, exchange_id, api_key_hint, encrypted_config, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, NOW(), NOW())
VALUES (%s, %s, %s, %s, %s, NOW(), NOW())
RETURNING id
""",
(user_id, name, exchange_id, _api_key_hint(api_key), plaintext_config)
(user_id, name, exchange_id, hint, plaintext_config)
)
new_id = cur.lastrowid
row = cur.fetchone()
new_id = (row or {}).get('id')
db.commit()
cur.close()
@@ -115,7 +157,7 @@ def delete_credential():
with get_db_connection() as db:
cur = db.cursor()
cur.execute(
"DELETE FROM qd_exchange_credentials WHERE id = ? AND user_id = ?",
"DELETE FROM qd_exchange_credentials WHERE id = %s AND user_id = %s",
(cred_id, user_id)
)
db.commit()
@@ -146,7 +188,7 @@ def get_credential():
"""
SELECT id, user_id, name, exchange_id, encrypted_config, api_key_hint, created_at, updated_at
FROM qd_exchange_credentials
WHERE id = ? AND user_id = ?
WHERE id = %s AND user_id = %s
""",
(cred_id, user_id)
)
@@ -0,0 +1,555 @@
"""
Quick Trade API — manual / discretionary order placement.
Allows users to place market or limit orders directly from AI analysis
or indicator analysis pages, without creating a strategy first.
Endpoints:
POST /api/quick-trade/place-order — Place a quick order
GET /api/quick-trade/balance — Get available balance
GET /api/quick-trade/position — Get current position for symbol
GET /api/quick-trade/history — Get quick trade history
"""
from __future__ import annotations
import json
import time
import traceback
import uuid
from typing import Any, Dict
from flask import Blueprint, g, jsonify, request
from app.utils.db import get_db_connection
from app.utils.logger import get_logger
from app.utils.auth import login_required
logger = get_logger(__name__)
quick_trade_bp = Blueprint('quick_trade', __name__)
# ────────── helpers ──────────
def _safe_json(v, default=None):
if v is None:
return default
if isinstance(v, (dict, list)):
return v
try:
return json.loads(v) if isinstance(v, str) else default
except Exception:
return default
def _load_credential(credential_id: int, user_id: int) -> Dict[str, Any]:
"""Load exchange credential JSON for the given user."""
with get_db_connection() as db:
cur = db.cursor()
cur.execute(
"SELECT encrypted_config FROM qd_exchange_credentials WHERE id = %s AND user_id = %s",
(int(credential_id), int(user_id)),
)
row = cur.fetchone() or {}
cur.close()
return _safe_json(row.get("encrypted_config"), {})
def _build_exchange_config(credential_id: int, user_id: int, overrides: Dict[str, Any] = None) -> Dict[str, Any]:
"""Build exchange config from saved credential + overrides."""
base = _load_credential(credential_id, user_id)
if not base:
raise ValueError("Credential not found or access denied")
if overrides:
for k, v in overrides.items():
if v is not None and (not isinstance(v, str) or v.strip()):
base[k] = v
return base
def _create_client(exchange_config: Dict[str, Any], market_type: str = "swap"):
"""Create exchange client from config."""
from app.services.live_trading.factory import create_client
return create_client(exchange_config, market_type=market_type)
def _record_quick_trade(
user_id: int,
credential_id: int,
exchange_id: str,
symbol: str,
side: str,
order_type: str,
amount: float,
price: float,
leverage: int,
market_type: str,
tp_price: float,
sl_price: float,
status: str,
exchange_order_id: str,
filled: float,
avg_price: float,
error_msg: str,
source: str,
raw_result: Dict[str, Any],
):
"""Insert a quick trade record into the database."""
try:
with get_db_connection() as db:
cur = db.cursor()
cur.execute(
"""
INSERT INTO qd_quick_trades
(user_id, credential_id, exchange_id, symbol, side, order_type,
amount, price, leverage, market_type, tp_price, sl_price,
status, exchange_order_id, filled_amount, avg_fill_price,
error_msg, source, raw_result, created_at)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, NOW())
RETURNING id
""",
(
user_id, credential_id, exchange_id, symbol, side, order_type,
amount, price, leverage, market_type, tp_price, sl_price,
status, exchange_order_id, filled, avg_price,
error_msg, source, json.dumps(raw_result or {}),
),
)
row = cur.fetchone()
db.commit()
cur.close()
return (row or {}).get("id")
except Exception as e:
logger.error(f"Failed to record quick trade: {e}")
return None
# ────────── endpoints ──────────
@quick_trade_bp.route('/place-order', methods=['POST'])
@login_required
def place_order():
"""
Place a quick market or limit order.
Body JSON:
credential_id (int) — saved exchange credential ID
symbol (str) — e.g. "BTC/USDT"
side (str) — "buy" or "sell"
order_type (str) — "market" or "limit" (default: market)
amount (float) — order size (USDT quote amount for market buy, or base qty)
price (float) — limit price (required for limit orders)
leverage (int) — leverage multiplier (default: 1)
market_type (str) — "swap" / "spot" (default: swap)
tp_price (float) — take-profit price (optional, for record only)
sl_price (float) — stop-loss price (optional, for record only)
source (str) — "ai_radar" / "ai_analysis" / "indicator" / "manual"
"""
try:
user_id = g.user_id
body = request.get_json(force=True, silent=True) or {}
credential_id = int(body.get("credential_id") or 0)
symbol = str(body.get("symbol") or "").strip()
side = str(body.get("side") or "").strip().lower()
order_type = str(body.get("order_type") or "market").strip().lower()
amount = float(body.get("amount") or 0)
price = float(body.get("price") or 0)
leverage = int(body.get("leverage") or 1)
market_type = str(body.get("market_type") or "swap").strip().lower()
tp_price = float(body.get("tp_price") or 0)
sl_price = float(body.get("sl_price") or 0)
source = str(body.get("source") or "manual").strip()
# ---- validation ----
if not credential_id:
return jsonify({"code": 0, "msg": "Missing credential_id"}), 400
if not symbol:
return jsonify({"code": 0, "msg": "Missing symbol"}), 400
if side not in ("buy", "sell"):
return jsonify({"code": 0, "msg": "side must be 'buy' or 'sell'"}), 400
if amount <= 0:
return jsonify({"code": 0, "msg": "amount must be > 0"}), 400
if order_type == "limit" and price <= 0:
return jsonify({"code": 0, "msg": "price required for limit orders"}), 400
if market_type in ("futures", "future", "perp", "perpetual"):
market_type = "swap"
# ---- build exchange client ----
exchange_config = _build_exchange_config(credential_id, user_id, {
"market_type": market_type,
})
exchange_id = (exchange_config.get("exchange_id") or "").strip().lower()
if not exchange_id:
return jsonify({"code": 0, "msg": "Invalid credential: missing exchange_id"}), 400
client = _create_client(exchange_config, market_type=market_type)
# ---- set leverage (futures only) ----
if market_type != "spot" and leverage > 1:
try:
if hasattr(client, "set_leverage"):
client.set_leverage(symbol=symbol, leverage=leverage)
elif hasattr(client, "set_leverage") and callable(getattr(client, "set_leverage", None)):
client.set_leverage(symbol=symbol, lever=leverage)
except Exception as le:
logger.warning(f"set_leverage failed (non-fatal): {le}")
# ---- place order ----
client_order_id = f"qt_{int(time.time())}_{uuid.uuid4().hex[:8]}"
result = None
if order_type == "market":
result = client.place_market_order(
symbol=symbol,
side=side.upper() if "binance" in exchange_id else side,
**_market_order_kwargs(client, symbol, amount, side, market_type, client_order_id),
)
else:
result = client.place_limit_order(
symbol=symbol,
side=side.upper() if "binance" in exchange_id else side,
**_limit_order_kwargs(client, symbol, amount, price, side, market_type, client_order_id),
)
# ---- extract result ----
exchange_order_id = str(getattr(result, "exchange_order_id", "") or "")
filled = float(getattr(result, "filled", 0) or 0)
avg_fill = float(getattr(result, "avg_price", 0) or 0)
raw = getattr(result, "raw", {}) or {}
# ---- record trade ----
trade_id = _record_quick_trade(
user_id=user_id,
credential_id=credential_id,
exchange_id=exchange_id,
symbol=symbol,
side=side,
order_type=order_type,
amount=amount,
price=price if order_type == "limit" else avg_fill,
leverage=leverage,
market_type=market_type,
tp_price=tp_price,
sl_price=sl_price,
status="filled" if filled > 0 else "submitted",
exchange_order_id=exchange_order_id,
filled=filled,
avg_price=avg_fill,
error_msg="",
source=source,
raw_result=raw,
)
return jsonify({
"code": 1,
"msg": "Order placed successfully",
"data": {
"trade_id": trade_id,
"exchange_order_id": exchange_order_id,
"filled": filled,
"avg_price": avg_fill,
"status": "filled" if filled > 0 else "submitted",
},
})
except Exception as e:
logger.error(f"quick trade failed: {e}")
logger.error(traceback.format_exc())
# Try to record the failure
try:
_record_quick_trade(
user_id=g.user_id,
credential_id=int(body.get("credential_id") or 0),
exchange_id="",
symbol=str(body.get("symbol") or ""),
side=str(body.get("side") or ""),
order_type=str(body.get("order_type") or "market"),
amount=float(body.get("amount") or 0),
price=0,
leverage=int(body.get("leverage") or 1),
market_type=str(body.get("market_type") or "swap"),
tp_price=0,
sl_price=0,
status="failed",
exchange_order_id="",
filled=0,
avg_price=0,
error_msg=str(e)[:500],
source=str(body.get("source") or "manual"),
raw_result={},
)
except Exception:
pass
return jsonify({"code": 0, "msg": str(e)}), 500
def _market_order_kwargs(client, symbol, amount, side, market_type, client_order_id):
"""Build kwargs compatible with any exchange client's place_market_order."""
from app.services.live_trading.binance import BinanceFuturesClient
from app.services.live_trading.binance_spot import BinanceSpotClient
from app.services.live_trading.okx import OkxClient
from app.services.live_trading.bitget import BitgetMixClient
from app.services.live_trading.bybit import BybitClient
if isinstance(client, (BinanceFuturesClient, BinanceSpotClient)):
return {"quantity": amount, "client_order_id": client_order_id}
if isinstance(client, OkxClient):
return {"size": amount, "client_order_id": client_order_id}
if isinstance(client, BitgetMixClient):
return {"size": amount, "client_order_id": client_order_id}
if isinstance(client, BybitClient):
return {"qty": amount, "client_order_id": client_order_id}
# Generic fallback
return {"size": amount, "client_order_id": client_order_id}
def _limit_order_kwargs(client, symbol, amount, price, side, market_type, client_order_id):
"""Build kwargs compatible with any exchange client's place_limit_order."""
from app.services.live_trading.binance import BinanceFuturesClient
from app.services.live_trading.binance_spot import BinanceSpotClient
if isinstance(client, (BinanceFuturesClient, BinanceSpotClient)):
return {"quantity": amount, "price": price, "client_order_id": client_order_id}
# Generic fallback
return {"size": amount, "price": price, "client_order_id": client_order_id}
@quick_trade_bp.route('/balance', methods=['GET'])
@login_required
def get_balance():
"""
Get available balance from exchange.
Query: credential_id (int), market_type (str, default "swap")
"""
try:
user_id = g.user_id
credential_id = request.args.get("credential_id", type=int)
market_type = request.args.get("market_type", "swap").strip().lower()
if not credential_id:
return jsonify({"code": 0, "msg": "Missing credential_id"}), 400
exchange_config = _build_exchange_config(credential_id, user_id, {"market_type": market_type})
exchange_id = (exchange_config.get("exchange_id") or "").strip().lower()
client = _create_client(exchange_config, market_type=market_type)
balance_data = {"available": 0, "total": 0, "currency": "USDT"}
try:
if hasattr(client, "get_balance"):
raw = client.get_balance()
balance_data = _parse_balance(raw, exchange_id, market_type)
elif hasattr(client, "get_account"):
raw = client.get_account()
balance_data = _parse_balance(raw, exchange_id, market_type)
elif hasattr(client, "get_accounts"):
raw = client.get_accounts()
balance_data = _parse_balance(raw, exchange_id, market_type)
except Exception as be:
logger.warning(f"Balance fetch failed: {be}")
balance_data["error"] = str(be)
return jsonify({"code": 1, "msg": "success", "data": balance_data})
except Exception as e:
logger.error(f"get_balance failed: {e}")
return jsonify({"code": 0, "msg": str(e)}), 500
def _parse_balance(raw: Any, exchange_id: str, market_type: str) -> Dict[str, Any]:
"""Best-effort parse balance from various exchange responses."""
result = {"available": 0, "total": 0, "currency": "USDT"}
if not raw:
return result
try:
if isinstance(raw, dict):
# Binance futures
if "availableBalance" in raw:
result["available"] = float(raw.get("availableBalance") or 0)
result["total"] = float(raw.get("totalWalletBalance") or raw.get("totalMarginBalance") or 0)
return result
# Binance spot
if "balances" in raw:
for b in raw.get("balances", []):
if str(b.get("asset") or "").upper() == "USDT":
result["available"] = float(b.get("free") or 0)
result["total"] = float(b.get("free") or 0) + float(b.get("locked") or 0)
return result
return result
# OKX
data = raw.get("data")
if isinstance(data, list) and data:
first = data[0] if isinstance(data[0], dict) else {}
# Account balance
details = first.get("details", [])
if isinstance(details, list):
for d in details:
if str(d.get("ccy") or "").upper() == "USDT":
result["available"] = float(d.get("availBal") or d.get("availEq") or 0)
result["total"] = float(d.get("eq") or d.get("cashBal") or 0)
return result
# Fallback
result["available"] = float(first.get("availBal") or first.get("totalEq") or 0)
result["total"] = float(first.get("totalEq") or 0)
return result
# Bybit
if "result" in raw:
res = raw["result"]
if isinstance(res, dict):
coin_list = res.get("list", [])
if isinstance(coin_list, list):
for acc in coin_list:
coins = acc.get("coin", []) if isinstance(acc, dict) else []
for c in coins:
if str(c.get("coin") or "").upper() == "USDT":
result["available"] = float(c.get("availableToWithdraw") or c.get("walletBalance") or 0)
result["total"] = float(c.get("walletBalance") or 0)
return result
# Fallback: try to find any USDT-like values
if isinstance(raw, dict):
for k, v in raw.items():
if "avail" in str(k).lower() and isinstance(v, (int, float)):
result["available"] = float(v)
if "total" in str(k).lower() and isinstance(v, (int, float)):
result["total"] = float(v)
except Exception as e:
logger.warning(f"_parse_balance error: {e}")
return result
@quick_trade_bp.route('/position', methods=['GET'])
@login_required
def get_position():
"""
Get current position for a symbol from exchange.
Query: credential_id (int), symbol (str), market_type (str)
"""
try:
user_id = g.user_id
credential_id = request.args.get("credential_id", type=int)
symbol = request.args.get("symbol", "").strip()
market_type = request.args.get("market_type", "swap").strip().lower()
if not credential_id or not symbol:
return jsonify({"code": 0, "msg": "Missing credential_id or symbol"}), 400
exchange_config = _build_exchange_config(credential_id, user_id, {"market_type": market_type})
client = _create_client(exchange_config, market_type=market_type)
positions = []
try:
if hasattr(client, "get_positions"):
raw = client.get_positions(symbol=symbol)
positions = _parse_positions(raw)
elif hasattr(client, "get_position"):
raw = client.get_position(symbol=symbol)
positions = _parse_positions(raw)
except Exception as pe:
logger.warning(f"Position fetch failed: {pe}")
return jsonify({"code": 1, "msg": "success", "data": {"positions": positions}})
except Exception as e:
logger.error(f"get_position failed: {e}")
return jsonify({"code": 0, "msg": str(e)}), 500
def _parse_positions(raw: Any) -> list:
"""Best-effort parse positions from exchange response."""
result = []
if not raw:
return result
try:
items = []
if isinstance(raw, list):
items = raw
elif isinstance(raw, dict):
data = raw.get("data") or raw.get("result") or raw.get("positions") or []
if isinstance(data, list):
items = data
elif isinstance(data, dict):
items = data.get("list", []) if "list" in data else [data]
for item in items:
if not isinstance(item, dict):
continue
size = float(item.get("posAmt") or item.get("pos") or item.get("size") or item.get("contracts") or 0)
if abs(size) < 1e-10:
continue
result.append({
"symbol": item.get("symbol") or item.get("instId") or "",
"side": "long" if size > 0 else "short",
"size": abs(size),
"entry_price": float(item.get("entryPrice") or item.get("avgCost") or item.get("avgPx") or 0),
"unrealized_pnl": float(item.get("unRealizedProfit") or item.get("upl") or item.get("unrealisedPnl") or 0),
"leverage": float(item.get("leverage") or 1),
"mark_price": float(item.get("markPrice") or item.get("markPx") or 0),
})
except Exception as e:
logger.warning(f"_parse_positions error: {e}")
return result
@quick_trade_bp.route('/history', methods=['GET'])
@login_required
def get_history():
"""
Get quick trade history for the current user.
Query: limit (int, default 50), offset (int, default 0)
"""
try:
user_id = g.user_id
limit = min(int(request.args.get("limit") or 50), 200)
offset = int(request.args.get("offset") or 0)
with get_db_connection() as db:
cur = db.cursor()
cur.execute(
"""
SELECT id, exchange_id, symbol, side, order_type, amount, price,
leverage, market_type, tp_price, sl_price, status,
exchange_order_id, filled_amount, avg_fill_price,
error_msg, source, created_at
FROM qd_quick_trades
WHERE user_id = %s
ORDER BY created_at DESC
LIMIT %s OFFSET %s
""",
(user_id, limit, offset),
)
rows = cur.fetchall() or []
cur.close()
trades = []
for r in rows:
trades.append({
"id": r.get("id"),
"exchange_id": r.get("exchange_id") or "",
"symbol": r.get("symbol") or "",
"side": r.get("side") or "",
"order_type": r.get("order_type") or "market",
"amount": float(r.get("amount") or 0),
"price": float(r.get("price") or 0),
"leverage": int(r.get("leverage") or 1),
"market_type": r.get("market_type") or "swap",
"tp_price": float(r.get("tp_price") or 0),
"sl_price": float(r.get("sl_price") or 0),
"status": r.get("status") or "",
"exchange_order_id": r.get("exchange_order_id") or "",
"filled_amount": float(r.get("filled_amount") or 0),
"avg_fill_price": float(r.get("avg_fill_price") or 0),
"error_msg": r.get("error_msg") or "",
"source": r.get("source") or "",
"created_at": str(r.get("created_at") or ""),
})
return jsonify({"code": 1, "msg": "success", "data": {"trades": trades}})
except Exception as e:
logger.error(f"get_history failed: {e}")
return jsonify({"code": 0, "msg": str(e)}), 500
+21 -12
View File
@@ -640,7 +640,7 @@ def test_connection():
Test exchange connection.
Request body:
exchange_config: Exchange configuration
exchange_config: Exchange configuration (may contain credential_id or inline keys)
"""
try:
data = request.get_json() or {}
@@ -664,29 +664,38 @@ def test_connection():
logger.error(f"Invalid exchange_config type: {type(exchange_config)}, data: {str(exchange_config)[:200]}")
# Frontend expects HTTP 200 with {code:0} for business failures.
return jsonify({'code': 0, 'msg': 'Invalid exchange config format; please check your payload', 'data': None})
# 验证必要字段
if not exchange_config.get('exchange_id'):
# Resolve credential_id → full config (merges credential keys with any overrides).
# This allows the frontend to send just {credential_id: 5} without raw api_key/secret_key.
from app.services.exchange_execution import resolve_exchange_config
user_id = g.user_id if hasattr(g, 'user_id') else 1
resolved = resolve_exchange_config(exchange_config, user_id=user_id)
# 验证必要字段 (check resolved config after credential merge)
if not resolved.get('exchange_id'):
return jsonify({'code': 0, 'msg': 'Please select an exchange', 'data': None})
api_key = exchange_config.get('api_key', '')
secret_key = exchange_config.get('secret_key', '')
api_key = resolved.get('api_key', '')
secret_key = resolved.get('secret_key', '')
# 详细日志排查
logger.info(f"Testing connection: exchange_id={exchange_config.get('exchange_id')}")
logger.info(f"API Key: {api_key[:5]}... (len={len(api_key)})")
logger.info(f"Secret Key: {secret_key[:5]}... (len={len(secret_key)})")
logger.info(f"Testing connection: exchange_id={resolved.get('exchange_id')}")
if api_key:
logger.info(f"API Key: {api_key[:5]}... (len={len(api_key)})")
if secret_key:
logger.info(f"Secret Key: {secret_key[:5]}... (len={len(secret_key)})")
# 检查是否有特殊字符
if api_key.strip() != api_key:
if api_key and api_key.strip() != api_key:
logger.warning("API key contains leading/trailing whitespace")
if secret_key.strip() != secret_key:
if secret_key and secret_key.strip() != secret_key:
logger.warning("Secret key contains leading/trailing whitespace")
if not api_key or not secret_key:
return jsonify({'code': 0, 'msg': 'Please provide API key and secret key', 'data': None})
result = get_strategy_service().test_exchange_connection(exchange_config)
# Pass the resolved config (with actual keys) to the service
result = get_strategy_service().test_exchange_connection(resolved)
if result['success']:
return jsonify({'code': 1, 'msg': result.get('message') or 'Connection successful', 'data': result.get('data')})
+447
View File
@@ -1071,3 +1071,450 @@ def get_system_strategies():
import traceback
logger.error(traceback.format_exc())
return jsonify({'code': 0, 'msg': str(e), 'data': None}), 500
# ==================== Admin Orders ====================
@user_bp.route('/admin-orders', methods=['GET'])
@login_required
@admin_required
def get_admin_orders():
"""
Get all orders across the system (admin only).
Merges qd_membership_orders and qd_usdt_orders into a unified list.
Query params:
page: int (default 1)
page_size: int (default 20, max 100)
status: str (optional, filter by status: paid/pending/confirmed/expired/all)
search: str (optional, search by username/email)
"""
try:
page = request.args.get('page', 1, type=int)
page_size = request.args.get('page_size', 20, type=int)
status_filter = request.args.get('status', '', type=str).strip().lower()
search = request.args.get('search', '', type=str).strip()
page_size = min(100, max(1, page_size))
offset = (page - 1) * page_size
with get_db_connection() as db:
cur = db.cursor()
# --- USDT Orders (primary) ---
usdt_conditions = []
usdt_params = []
if status_filter and status_filter != 'all':
usdt_conditions.append("o.status = ?")
usdt_params.append(status_filter)
if search:
usdt_conditions.append("(u.username ILIKE ? OR u.email ILIKE ? OR u.nickname ILIKE ?)")
like_val = f"%{search}%"
usdt_params.extend([like_val, like_val, like_val])
usdt_where = ""
if usdt_conditions:
usdt_where = "WHERE " + " AND ".join(usdt_conditions)
# Count
cur.execute(
f"SELECT COUNT(*) as cnt FROM qd_usdt_orders o LEFT JOIN qd_users u ON u.id = o.user_id {usdt_where}",
tuple(usdt_params)
)
usdt_total = cur.fetchone()['cnt']
# --- Membership Orders (mock) ---
mock_conditions = []
mock_params = []
if status_filter and status_filter != 'all':
mock_conditions.append("m.status = ?")
mock_params.append(status_filter)
if search:
mock_conditions.append("(u.username ILIKE ? OR u.email ILIKE ? OR u.nickname ILIKE ?)")
like_val = f"%{search}%"
mock_params.extend([like_val, like_val, like_val])
mock_where = ""
if mock_conditions:
mock_where = "WHERE " + " AND ".join(mock_conditions)
cur.execute(
f"SELECT COUNT(*) as cnt FROM qd_membership_orders m LEFT JOIN qd_users u ON u.id = m.user_id {mock_where}",
tuple(mock_params)
)
mock_total = cur.fetchone()['cnt']
total = usdt_total + mock_total
# Use UNION ALL to merge both tables into one sorted list
# We select a unified schema
union_sql = f"""
SELECT * FROM (
SELECT
o.id,
'usdt' AS order_type,
o.user_id,
u.username,
u.nickname,
u.email AS user_email,
o.plan,
o.amount_usdt AS amount,
'USDT' AS currency,
o.chain,
o.address,
o.tx_hash,
o.status,
o.created_at,
o.paid_at,
o.confirmed_at,
o.expires_at
FROM qd_usdt_orders o
LEFT JOIN qd_users u ON u.id = o.user_id
{usdt_where}
UNION ALL
SELECT
m.id,
'mock' AS order_type,
m.user_id,
u.username,
u.nickname,
u.email AS user_email,
m.plan,
m.price_usd AS amount,
'USD' AS currency,
'' AS chain,
'' AS address,
'' AS tx_hash,
m.status,
m.created_at,
m.paid_at,
NULL AS confirmed_at,
NULL AS expires_at
FROM qd_membership_orders m
LEFT JOIN qd_users u ON u.id = m.user_id
{mock_where}
) AS combined
ORDER BY combined.created_at DESC
LIMIT ? OFFSET ?
"""
all_params = list(usdt_params) + list(mock_params) + [page_size, offset]
cur.execute(union_sql, tuple(all_params))
rows = cur.fetchall() or []
# Summary stats
cur.execute(
f"""SELECT
COUNT(*) AS total_orders,
COALESCE(SUM(CASE WHEN status IN ('paid','confirmed') THEN 1 ELSE 0 END), 0) AS paid_orders,
COALESCE(SUM(CASE WHEN status = 'pending' THEN 1 ELSE 0 END), 0) AS pending_orders,
COALESCE(SUM(CASE WHEN status IN ('expired','cancelled','failed') THEN 1 ELSE 0 END), 0) AS failed_orders,
COALESCE(SUM(CASE WHEN status IN ('paid','confirmed') THEN amount_usdt ELSE 0 END), 0) AS total_revenue
FROM qd_usdt_orders"""
)
summary_row = cur.fetchone() or {}
cur.close()
items = []
for row in rows:
created_at = row.get('created_at')
paid_at = row.get('paid_at')
confirmed_at = row.get('confirmed_at')
expires_at = row.get('expires_at')
if hasattr(created_at, 'isoformat'):
created_at = created_at.isoformat()
if hasattr(paid_at, 'isoformat'):
paid_at = paid_at.isoformat()
if hasattr(confirmed_at, 'isoformat'):
confirmed_at = confirmed_at.isoformat()
if hasattr(expires_at, 'isoformat'):
expires_at = expires_at.isoformat()
items.append({
'id': row['id'],
'order_type': row.get('order_type') or '',
'user_id': row.get('user_id'),
'username': row.get('username') or '',
'nickname': row.get('nickname') or '',
'user_email': row.get('user_email') or '',
'plan': row.get('plan') or '',
'amount': float(row.get('amount') or 0),
'currency': row.get('currency') or '',
'chain': row.get('chain') or '',
'address': row.get('address') or '',
'tx_hash': row.get('tx_hash') or '',
'status': row.get('status') or '',
'created_at': created_at,
'paid_at': paid_at,
'confirmed_at': confirmed_at,
'expires_at': expires_at
})
return jsonify({
'code': 1,
'msg': 'success',
'data': {
'items': items,
'total': total,
'page': page,
'page_size': page_size,
'summary': {
'total_orders': int(summary_row.get('total_orders') or 0),
'paid_orders': int(summary_row.get('paid_orders') or 0),
'pending_orders': int(summary_row.get('pending_orders') or 0),
'failed_orders': int(summary_row.get('failed_orders') or 0),
'total_revenue': round(float(summary_row.get('total_revenue') or 0), 2)
}
}
})
except Exception as e:
logger.error(f"get_admin_orders failed: {e}")
import traceback
logger.error(traceback.format_exc())
return jsonify({'code': 0, 'msg': str(e), 'data': None}), 500
# ==================== Admin AI Analysis Stats ====================
@user_bp.route('/admin-ai-stats', methods=['GET'])
@login_required
@admin_required
def get_admin_ai_stats():
"""
Get AI analysis usage statistics across the system (admin only).
Does NOT expose analysis results, only aggregated counts/stats.
Query params:
page: int (default 1)
page_size: int (default 20, max 100)
search: str (optional, search by username)
"""
try:
page = request.args.get('page', 1, type=int)
page_size = request.args.get('page_size', 20, type=int)
search = request.args.get('search', '', type=str).strip()
page_size = min(100, max(1, page_size))
offset = (page - 1) * page_size
with get_db_connection() as db:
cur = db.cursor()
# --- Overall summary (from qd_analysis_tasks + qd_analysis_memory) ---
cur.execute("""
SELECT
COUNT(*) AS total_tasks,
COUNT(DISTINCT user_id) AS unique_users,
COUNT(DISTINCT symbol) AS unique_symbols,
COUNT(DISTINCT market) AS unique_markets
FROM qd_analysis_tasks
""")
task_summary = cur.fetchone() or {}
memory_summary = {}
try:
cur.execute("""
SELECT
COUNT(*) AS total_memory,
COALESCE(SUM(CASE WHEN was_correct = true THEN 1 ELSE 0 END), 0) AS correct_count,
COALESCE(SUM(CASE WHEN was_correct = false THEN 1 ELSE 0 END), 0) AS incorrect_count,
COALESCE(SUM(CASE WHEN user_feedback = 'helpful' THEN 1 ELSE 0 END), 0) AS helpful_count,
COALESCE(SUM(CASE WHEN user_feedback = 'not_helpful' THEN 1 ELSE 0 END), 0) AS not_helpful_count
FROM qd_analysis_memory
""")
memory_summary = cur.fetchone() or {}
except Exception as mem_err:
logger.warning(f"qd_analysis_memory query failed (table/column may not exist): {mem_err}")
db.rollback()
cur = db.cursor() # re-create cursor after rollback
memory_summary = {}
# --- Per-user stats ---
user_conditions = []
user_params = []
if search:
user_conditions.append("(u.username ILIKE ? OR u.nickname ILIKE ? OR u.email ILIKE ?)")
like_val = f"%{search}%"
user_params.extend([like_val, like_val, like_val])
user_where = ""
if user_conditions:
user_where = "WHERE " + " AND ".join(user_conditions)
# Count distinct users who have analysis records
cur.execute(
f"""
SELECT COUNT(DISTINCT t.user_id) AS cnt
FROM qd_analysis_tasks t
LEFT JOIN qd_users u ON u.id = t.user_id
{user_where}
""",
tuple(user_params)
)
user_total = cur.fetchone()['cnt']
# Get per-user aggregated stats
cur.execute(
f"""
SELECT
t.user_id,
u.username,
u.nickname,
u.email,
COUNT(*) AS analysis_count,
COUNT(DISTINCT t.symbol) AS symbol_count,
COUNT(DISTINCT t.market) AS market_count,
MAX(t.created_at) AS last_analysis_at,
MIN(t.created_at) AS first_analysis_at
FROM qd_analysis_tasks t
LEFT JOIN qd_users u ON u.id = t.user_id
{user_where}
GROUP BY t.user_id, u.username, u.nickname, u.email
ORDER BY analysis_count DESC
LIMIT ? OFFSET ?
""",
tuple(user_params) + (page_size, offset)
)
user_rows = cur.fetchall() or []
# Get per-user analysis_memory stats (correct/helpful counts)
user_ids = [r['user_id'] for r in user_rows if r.get('user_id')]
memory_stats_map = {}
if user_ids:
try:
placeholders = ','.join(['?'] * len(user_ids))
cur.execute(
f"""
SELECT
user_id,
COUNT(*) AS memory_count,
COALESCE(SUM(CASE WHEN was_correct = true THEN 1 ELSE 0 END), 0) AS correct,
COALESCE(SUM(CASE WHEN was_correct = false THEN 1 ELSE 0 END), 0) AS incorrect,
COALESCE(SUM(CASE WHEN user_feedback = 'helpful' THEN 1 ELSE 0 END), 0) AS helpful,
COALESCE(SUM(CASE WHEN user_feedback = 'not_helpful' THEN 1 ELSE 0 END), 0) AS not_helpful
FROM qd_analysis_memory
WHERE user_id IN ({placeholders})
GROUP BY user_id
""",
tuple(user_ids)
)
for row in (cur.fetchall() or []):
memory_stats_map[row['user_id']] = {
'memory_count': row['memory_count'],
'correct': row['correct'],
'incorrect': row['incorrect'],
'helpful': row['helpful'],
'not_helpful': row['not_helpful']
}
except Exception as mem_err:
logger.warning(f"qd_analysis_memory per-user query failed: {mem_err}")
db.rollback()
cur = db.cursor() # re-create cursor after rollback
memory_stats_map = {}
# Get recent analysis records (last 50)
cur.execute(
"""
SELECT
t.id,
t.user_id,
u.username,
u.nickname,
t.market,
t.symbol,
t.model,
t.status,
t.created_at,
t.completed_at
FROM qd_analysis_tasks t
LEFT JOIN qd_users u ON u.id = t.user_id
ORDER BY t.created_at DESC
LIMIT 50
"""
)
recent_rows = cur.fetchall() or []
cur.close()
# Build per-user items
user_items = []
for row in user_rows:
uid = row.get('user_id')
ms = memory_stats_map.get(uid, {})
last_at = row.get('last_analysis_at')
first_at = row.get('first_analysis_at')
if hasattr(last_at, 'isoformat'):
last_at = last_at.isoformat()
if hasattr(first_at, 'isoformat'):
first_at = first_at.isoformat()
user_items.append({
'user_id': uid,
'username': row.get('username') or '',
'nickname': row.get('nickname') or '',
'email': row.get('email') or '',
'analysis_count': row.get('analysis_count') or 0,
'symbol_count': row.get('symbol_count') or 0,
'market_count': row.get('market_count') or 0,
'correct': ms.get('correct', 0),
'incorrect': ms.get('incorrect', 0),
'helpful': ms.get('helpful', 0),
'not_helpful': ms.get('not_helpful', 0),
'last_analysis_at': last_at,
'first_analysis_at': first_at
})
# Build recent records
recent_items = []
for row in recent_rows:
created_at = row.get('created_at')
completed_at = row.get('completed_at')
if hasattr(created_at, 'isoformat'):
created_at = created_at.isoformat()
if hasattr(completed_at, 'isoformat'):
completed_at = completed_at.isoformat()
recent_items.append({
'id': row['id'],
'user_id': row.get('user_id'),
'username': row.get('username') or '',
'nickname': row.get('nickname') or '',
'market': row.get('market') or '',
'symbol': row.get('symbol') or '',
'model': row.get('model') or '',
'status': row.get('status') or '',
'created_at': created_at,
'completed_at': completed_at
})
return jsonify({
'code': 1,
'msg': 'success',
'data': {
'user_stats': user_items,
'user_total': user_total,
'page': page,
'page_size': page_size,
'recent': recent_items,
'summary': {
'total_analyses': int(task_summary.get('total_tasks') or 0),
'unique_users': int(task_summary.get('unique_users') or 0),
'unique_symbols': int(task_summary.get('unique_symbols') or 0),
'unique_markets': int(task_summary.get('unique_markets') or 0),
'total_memory': int(memory_summary.get('total_memory') or 0),
'correct_count': int(memory_summary.get('correct_count') or 0),
'incorrect_count': int(memory_summary.get('incorrect_count') or 0),
'helpful_count': int(memory_summary.get('helpful_count') or 0),
'not_helpful_count': int(memory_summary.get('not_helpful_count') or 0)
}
}
})
except Exception as e:
logger.error(f"get_admin_ai_stats failed: {e}")
import traceback
logger.error(traceback.format_exc())
return jsonify({'code': 0, 'msg': str(e), 'data': None}), 500
@@ -61,7 +61,7 @@ def load_strategy_configs(strategy_id: int) -> Dict[str, Any]:
cur = db.cursor()
cur.execute(
"""
SELECT id, exchange_config, trading_config, market_type, leverage, execution_mode, market_category
SELECT id, user_id, exchange_config, trading_config, market_type, leverage, execution_mode, market_category
FROM qd_strategies_trading
WHERE id = %s
""",
@@ -77,9 +77,11 @@ def load_strategy_configs(strategy_id: int) -> Dict[str, Any]:
leverage = float(row.get("leverage") or trading_config.get("leverage") or exchange_config.get("leverage") or 1.0)
execution_mode = (row.get("execution_mode") or "signal").strip().lower()
market_category = (row.get("market_category") or "Crypto").strip()
user_id = int(row.get("user_id") or 1)
return {
"strategy_id": int(strategy_id),
"user_id": user_id,
"exchange_config": exchange_config if isinstance(exchange_config, dict) else {},
"trading_config": trading_config if isinstance(trading_config, dict) else {},
"market_type": market_type,
@@ -207,7 +207,8 @@ class PendingOrderWorker:
if exec_mode != "live":
logger.debug(f"[PositionSync] Strategy {sid} skipped: execution_mode='{exec_mode}' (needs 'live')")
continue
exchange_config = resolve_exchange_config(sc.get("exchange_config") or {})
sync_user_id = int(sc.get("user_id") or 1)
exchange_config = resolve_exchange_config(sc.get("exchange_config") or {}, user_id=sync_user_id)
safe_cfg = safe_exchange_config_for_log(exchange_config)
market_type = (sc.get("market_type") or exchange_config.get("market_type") or "swap")
market_type = str(market_type or "swap").strip().lower()
@@ -832,7 +833,8 @@ class PendingOrderWorker:
return
cfg = load_strategy_configs(strategy_id)
exchange_config = resolve_exchange_config(cfg.get("exchange_config") or {})
strategy_user_id = int(cfg.get("user_id") or 1)
exchange_config = resolve_exchange_config(cfg.get("exchange_config") or {}, user_id=strategy_user_id)
safe_cfg = safe_exchange_config_for_log(exchange_config)
exchange_id = str(exchange_config.get("exchange_id") or "").strip().lower()
market_category = str(cfg.get("market_category") or "Crypto").strip()
+13 -1
View File
@@ -549,6 +549,12 @@ class StrategyService:
trading_config = payload.get('trading_config') or {}
exchange_config = payload.get('exchange_config') or {}
# When credential_id is present, strip raw API keys to avoid
# storing secrets in the strategy record — they live in qd_exchange_credentials.
if isinstance(exchange_config, dict) and exchange_config.get('credential_id'):
for _secret_key in ('api_key', 'secret_key', 'passphrase', 'apiKey', 'secret', 'password'):
exchange_config.pop(_secret_key, None)
# Strategy group fields
strategy_group_id = payload.get('strategy_group_id') or ''
group_base_name = payload.get('group_base_name') or ''
@@ -779,7 +785,13 @@ class StrategyService:
trading_config = payload.get('trading_config') if payload.get('trading_config') is not None else (existing.get('trading_config') or {})
exchange_config = payload.get('exchange_config') if payload.get('exchange_config') is not None else (existing.get('exchange_config') or {})
ai_model_config = payload.get('ai_model_config') if payload.get('ai_model_config') is not None else (existing.get('ai_model_config') or {})
# When credential_id is present, strip raw API keys to avoid
# storing secrets in the strategy record — they live in qd_exchange_credentials.
if isinstance(exchange_config, dict) and exchange_config.get('credential_id'):
for _secret_key in ('api_key', 'secret_key', 'passphrase', 'apiKey', 'secret', 'password'):
exchange_config.pop(_secret_key, None)
# Handle cross-sectional strategy config updates
if payload.get('cs_strategy_type') is not None:
trading_config['cs_strategy_type'] = payload.get('cs_strategy_type')
@@ -4,14 +4,15 @@ USDT Payment Service (方案B:每单独立地址 + 自动对账)
MVP:
- 只支持 USDT-TRC20
- 使用 XPUB 派生地址服务端只保存 xpub不保存私钥
- 通过 TronGrid API 轮询到账前端轮询订单状态时触发刷新
- 后台 Worker 线程自动轮询链上到账 + 前端轮询双保险
"""
import os
import threading
import time
from datetime import datetime, timezone, timedelta
from decimal import Decimal
from typing import Any, Dict, Optional, Tuple
from typing import Any, Dict, List, Optional, Tuple
import requests
@@ -233,10 +234,13 @@ class UsdtPaymentService:
# -------------------- Chain check --------------------
def _refresh_order_in_tx(self, cur, row: Dict[str, Any]) -> None:
"""Check chain status for a single order and update in the current transaction."""
cfg = self._get_cfg()
status = (row.get("status") or "").lower()
chain = (row.get("chain") or "").upper()
order_id = row.get("id")
# --- Expiry check (only for pending; paid orders should still be confirmed) ---
expires_at = row.get("expires_at")
now = datetime.now(timezone.utc)
if expires_at and isinstance(expires_at, datetime):
@@ -244,7 +248,7 @@ class UsdtPaymentService:
if exp.tzinfo is None:
exp = exp.replace(tzinfo=timezone.utc)
if status == "pending" and exp <= now:
cur.execute("UPDATE qd_usdt_orders SET status = 'expired', updated_at = NOW() WHERE id = ?", (row["id"],))
cur.execute("UPDATE qd_usdt_orders SET status = 'expired', updated_at = NOW() WHERE id = ?", (order_id,))
return
if chain != "TRC20":
@@ -257,6 +261,12 @@ class UsdtPaymentService:
if not address or amount <= 0:
return
# --- For 'paid' status, skip chain query and just check confirm delay ---
if status == "paid":
self._try_confirm_paid_order(cur, row, cfg, now)
return
# --- For 'pending' status, query chain for incoming transfer ---
tx = self._find_trc20_usdt_incoming(address, amount, row.get("created_at"))
if not tx:
return
@@ -265,37 +275,60 @@ class UsdtPaymentService:
paid_at = datetime.now(timezone.utc)
cur.execute(
"UPDATE qd_usdt_orders SET status = 'paid', tx_hash = ?, paid_at = ?, updated_at = NOW() WHERE id = ? AND status = 'pending'",
(tx_hash, paid_at, row["id"]),
(tx_hash, paid_at, order_id),
)
# Confirm after a short delay to reduce reorg/uncle risk (TRON usually stable)
# If already old enough, confirm now.
# Try to confirm immediately if delay is satisfied
confirm_sec = int(cfg.get("confirm_seconds") or 30)
try:
if confirm_sec <= 0:
confirm_sec = 0
# If transaction timestamp is available, use it
tx_ts = tx.get("block_timestamp")
if tx_ts:
tx_time = datetime.fromtimestamp(int(tx_ts) / 1000.0, tz=timezone.utc)
if (now - tx_time).total_seconds() >= confirm_sec:
self._confirm_and_activate_in_tx(cur, row["id"], row.get("user_id"), row.get("plan"), tx_hash)
else:
# no timestamp -> confirm immediately
self._confirm_and_activate_in_tx(cur, row["id"], row.get("user_id"), row.get("plan"), tx_hash)
self._confirm_and_activate_in_tx(cur, order_id, row.get("user_id"), row.get("plan"), tx_hash)
elif confirm_sec <= 0:
self._confirm_and_activate_in_tx(cur, order_id, row.get("user_id"), row.get("plan"), tx_hash)
except Exception:
# do not block
pass
def _try_confirm_paid_order(self, cur, row: Dict[str, Any], cfg: Dict[str, Any], now: datetime) -> None:
"""For orders already in 'paid' status, check if confirm delay is met and activate."""
confirm_sec = int(cfg.get("confirm_seconds") or 30)
paid_at = row.get("paid_at")
if paid_at:
if isinstance(paid_at, str):
try:
paid_at = datetime.fromisoformat(paid_at.replace("Z", "+00:00"))
except Exception:
paid_at = None
if paid_at and paid_at.tzinfo is None:
paid_at = paid_at.replace(tzinfo=timezone.utc)
if paid_at and (now - paid_at).total_seconds() >= confirm_sec:
self._confirm_and_activate_in_tx(cur, row["id"], row.get("user_id"), row.get("plan"), row.get("tx_hash") or "")
return
# Fallback: if paid_at missing but confirm_sec <= 0, confirm now
if confirm_sec <= 0:
self._confirm_and_activate_in_tx(cur, row["id"], row.get("user_id"), row.get("plan"), row.get("tx_hash") or "")
def _confirm_and_activate_in_tx(self, cur, order_id: int, user_id: int, plan: str, tx_hash: str) -> None:
# Mark confirmed if not already
"""Mark order as confirmed and activate membership. Idempotent: skips if already confirmed."""
# --- Idempotency check: re-read current status ---
try:
cur.execute("SELECT status FROM qd_usdt_orders WHERE id = ?", (order_id,))
current = cur.fetchone()
if current and (current.get("status") or "").lower() == "confirmed":
logger.debug(f"USDT order {order_id} already confirmed, skipping activation.")
return
except Exception:
pass
# Mark confirmed
cur.execute(
"UPDATE qd_usdt_orders SET status='confirmed', confirmed_at = NOW(), updated_at = NOW() WHERE id = ? AND status IN ('paid','pending')",
(order_id,),
)
# Activate membership (idempotent-ish: billing_service stacks vip)
# Activate membership
try:
# We use existing membership activation (writes qd_membership_orders + credits logs).
ok, msg, data = self.billing.purchase_membership(int(user_id), str(plan))
logger.info(f"USDT activate membership: order={order_id} user={user_id} plan={plan} ok={ok} msg={msg}")
except Exception as e:
@@ -340,13 +373,9 @@ class UsdtPaymentService:
if min_ts and int(it.get("block_timestamp") or 0) < min_ts:
continue
val = int(it.get("value") or 0)
if val != target:
# Accept payments >= order amount (tolerance for overpayment)
if val < target:
continue
# basic checks
token = it.get("token_info") or {}
if str(token.get("symbol") or "").upper() != "USDT":
# some APIs omit symbol; contract filter should already ensure
pass
return it
except Exception:
continue
@@ -354,8 +383,114 @@ class UsdtPaymentService:
return None
return None
# -------------------- Batch refresh (for worker) --------------------
def refresh_all_active_orders(self) -> int:
"""
Scan all pending/paid USDT orders and refresh their chain status.
Called by the background UsdtOrderWorker.
Returns the number of orders that were updated to 'confirmed' or 'expired'.
"""
updated = 0
try:
with get_db_connection() as db:
cur = db.cursor()
self._ensure_schema_best_effort(cur)
cur.execute(
"""
SELECT id, user_id, plan, chain, amount_usdt, address_index, address, status, tx_hash,
paid_at, confirmed_at, expires_at, created_at, updated_at
FROM qd_usdt_orders
WHERE status IN ('pending', 'paid')
ORDER BY created_at ASC
LIMIT 100
"""
)
rows = cur.fetchall() or []
for row in rows:
old_status = (row.get("status") or "").lower()
try:
self._refresh_order_in_tx(cur, row)
except Exception as e:
logger.debug(f"refresh_all: order {row.get('id')} error: {e}")
continue
# Check if status changed
try:
cur.execute("SELECT status FROM qd_usdt_orders WHERE id = ?", (row["id"],))
new_row = cur.fetchone()
new_status = (new_row.get("status") or "").lower() if new_row else old_status
if new_status != old_status:
updated += 1
logger.info(f"USDT order {row['id']}: {old_status} -> {new_status}")
except Exception:
pass
db.commit()
cur.close()
except Exception as e:
logger.error(f"refresh_all_active_orders error: {e}", exc_info=True)
return updated
# ==================== Background Worker ====================
class UsdtOrderWorker:
"""
Background thread that periodically scans pending/paid USDT orders
and checks on-chain status via TronGrid API.
This ensures that even if the user closes the browser after payment,
the order will still be confirmed and membership activated.
"""
def __init__(self, poll_interval_sec: float = 30.0):
self.poll_interval_sec = float(poll_interval_sec)
self._stop_event = threading.Event()
self._thread: Optional[threading.Thread] = None
self._lock = threading.Lock()
def start(self) -> bool:
with self._lock:
if self._thread and self._thread.is_alive():
return True
self._stop_event.clear()
self._thread = threading.Thread(target=self._run_loop, name="UsdtOrderWorker", daemon=True)
self._thread.start()
logger.info("UsdtOrderWorker started (interval=%ss)", self.poll_interval_sec)
return True
def stop(self):
self._stop_event.set()
if self._thread:
self._thread.join(timeout=5)
logger.info("UsdtOrderWorker stopped")
def _run_loop(self):
# Wait a bit on startup to let the app fully initialize
self._stop_event.wait(timeout=10)
while not self._stop_event.is_set():
try:
svc = get_usdt_payment_service()
cfg = svc._get_cfg()
if cfg["enabled"]:
updated = svc.refresh_all_active_orders()
if updated > 0:
logger.info(f"UsdtOrderWorker: refreshed {updated} orders")
except Exception as e:
logger.error(f"UsdtOrderWorker loop error: {e}", exc_info=True)
self._stop_event.wait(timeout=self.poll_interval_sec)
# ==================== Singletons ====================
_svc = None
_worker = None
def get_usdt_payment_service() -> UsdtPaymentService:
@@ -364,3 +499,10 @@ def get_usdt_payment_service() -> UsdtPaymentService:
_svc = UsdtPaymentService()
return _svc
def get_usdt_order_worker() -> UsdtOrderWorker:
global _worker
if _worker is None:
interval = float(os.getenv("USDT_WORKER_POLL_INTERVAL", "30"))
_worker = UsdtOrderWorker(poll_interval_sec=interval)
return _worker
+2
View File
@@ -347,6 +347,8 @@ TRONGRID_API_KEY=
# Order confirmation delay and expiration
USDT_PAY_CONFIRM_SECONDS=30
USDT_PAY_EXPIRE_MINUTES=30
# Background worker poll interval (seconds) for checking pending USDT orders
USDT_WORKER_POLL_INTERVAL=30
# New user registration bonus credits (新用户注册赠送积分)
CREDITS_REGISTER_BONUS=100
+30
View File
@@ -802,6 +802,36 @@ BEGIN
END IF;
END $$;
-- =============================================================================
-- Quick Trades (manual / discretionary orders from Quick Trade Panel)
-- =============================================================================
CREATE TABLE IF NOT EXISTS qd_quick_trades (
id SERIAL PRIMARY KEY,
user_id INTEGER NOT NULL REFERENCES qd_users(id) ON DELETE CASCADE,
credential_id INTEGER DEFAULT 0,
exchange_id VARCHAR(40) NOT NULL DEFAULT '',
symbol VARCHAR(60) NOT NULL DEFAULT '',
side VARCHAR(10) NOT NULL DEFAULT '', -- buy / sell
order_type VARCHAR(20) NOT NULL DEFAULT 'market', -- market / limit
amount DECIMAL(24, 8) DEFAULT 0,
price DECIMAL(24, 8) DEFAULT 0,
leverage INTEGER DEFAULT 1,
market_type VARCHAR(20) DEFAULT 'swap', -- swap / spot
tp_price DECIMAL(24, 8) DEFAULT 0,
sl_price DECIMAL(24, 8) DEFAULT 0,
status VARCHAR(20) DEFAULT 'submitted', -- submitted / filled / failed / cancelled
exchange_order_id VARCHAR(120) DEFAULT '',
filled_amount DECIMAL(24, 8) DEFAULT 0,
avg_fill_price DECIMAL(24, 8) DEFAULT 0,
error_msg TEXT DEFAULT '',
source VARCHAR(40) DEFAULT 'manual', -- ai_radar / ai_analysis / indicator / manual
raw_result JSONB,
created_at TIMESTAMP DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_quick_trades_user ON qd_quick_trades(user_id);
CREATE INDEX IF NOT EXISTS idx_quick_trades_created ON qd_quick_trades(created_at DESC);
-- =============================================================================
-- Completion Notice
-- =============================================================================
+28 -20
View File
@@ -1,10 +1,15 @@
# QuantDinger Docker Compose
# Quick start: docker-compose up -d
# QuantDinger Docker Compose - One-Click Deployment
# Usage:
# 1. Copy .env.example to .env and edit your settings
# 2. docker-compose up -d
# 3. Open http://localhost:8888
version: '3.8'
services:
# ========================
# PostgreSQL Database
# ========================
postgres:
image: postgres:16-alpine
container_name: quantdinger-db
@@ -13,22 +18,23 @@ services:
POSTGRES_DB: ${POSTGRES_DB:-quantdinger}
POSTGRES_USER: ${POSTGRES_USER:-quantdinger}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-quantdinger123}
TZ: Asia/Shanghai
TZ: ${TZ:-Asia/Shanghai}
volumes:
- postgres_data:/var/lib/postgresql/data
- ./backend_api_python/migrations/init.sql:/docker-entrypoint-initdb.d/01-init.sql
- ./backend_api_python/migrations:/migrations
ports:
- "127.0.0.1:5432:5432"
- "${DB_PORT:-127.0.0.1:5432}:5432"
networks:
- quantdinger-network
healthcheck:
test: ["CMD-SHELL", "pg_isready -U quantdinger -d quantdinger"]
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-quantdinger} -d ${POSTGRES_DB:-quantdinger}"]
interval: 10s
timeout: 5s
retries: 5
# Backend API Service
# ========================
# Backend API (Python/Flask)
# ========================
backend:
build:
context: ./backend_api_python
@@ -39,18 +45,16 @@ services:
postgres:
condition: service_healthy
ports:
- "127.0.0.1:5000:5000"
- "${BACKEND_PORT:-127.0.0.1:5000}:5000"
volumes:
# Persistent logs
- ./backend_api_python/logs:/app/logs
- ./backend_api_python/data:/app/data
# Configuration file (optional, for development)
- ./backend_api_python/.env:/app/.env
- backend_logs:/app/logs
- backend_data:/app/data
# Mount .env for runtime config
- ./backend_api_python/.env:/app/.env:ro
environment:
- PYTHON_API_HOST=0.0.0.0
- PYTHON_API_PORT=5000
- TZ=Asia/Shanghai
# Database connection
- TZ=${TZ:-Asia/Shanghai}
- DATABASE_URL=postgresql://${POSTGRES_USER:-quantdinger}:${POSTGRES_PASSWORD:-quantdinger123}@postgres:5432/${POSTGRES_DB:-quantdinger}
- DB_TYPE=postgresql
networks:
@@ -61,17 +65,17 @@ services:
timeout: 10s
retries: 3
# Frontend Web Service
# ========================
# Frontend (Nginx + Pre-built)
# ========================
frontend:
build:
context: ./quantdinger_vue
context: ./frontend
dockerfile: Dockerfile
# Force pull from official registry to avoid mirror issues
pull: true
container_name: quantdinger-frontend
restart: unless-stopped
ports:
- "8888:80"
- "${FRONTEND_PORT:-8888}:80"
depends_on:
- backend
networks:
@@ -85,6 +89,10 @@ services:
volumes:
postgres_data:
driver: local
backend_logs:
driver: local
backend_data:
driver: local
networks:
quantdinger-network:
+44
View File
@@ -42,11 +42,27 @@ This document records version updates, new features, bug fixes, and database mig
- **Market Order Default**: Changed default order mode to market order for reliable execution
- **Billing Config i18n**: All billing configuration items fully multi-language supported
#### Quick Trade Panel (闪电交易) 🆕
- **Side-Sliding Drawer**: Professional trading panel slides in from the right, allowing instant order placement without leaving the analysis page
- **Multi-Exchange Support**: Select from saved exchange credentials (Binance, OKX, Bitget, Bybit, etc.) with real-time balance display
- **Long/Short Toggle**: Color-coded direction buttons with one-click switching
- **Market / Limit Orders**: Toggle between market and limit order types; limit orders accept a specific price
- **Leverage Slider**: Interactive 1x125x leverage control for futures trading
- **TP/SL Price Setting**: Optional take-profit and stop-loss by **absolute price** (not percentage)
- **Current Position Display**: Shows open position with side, size, entry price, unrealized PnL, and one-click close button
- **Recent Trade History**: Displays last 5 quick trades with status tags
- **AI Radar Integration**: "Trade Now" button on each AI Trading Opportunities card pre-fills symbol, direction, and price
- **Indicator Analysis Integration**: Quick Trade button in chart header and floating ⚡ button pre-fills current symbol and price
- **Auto-Polling**: Balance and position data refresh every 10 seconds
- **Full Dark Theme**: Complete dark mode support for all panel elements
- **Multi-Language**: All labels and messages fully internationalized (zh-CN / en-US)
#### Indicator Market Performance Tracking
- **Live Performance Data**: Fixed aggregation to correctly parse backtest `result_json` and include live trade data
- **Combined Metrics**: Backtest return, live PnL, and win rate now properly displayed on indicator cards
### 🐛 Bug Fixes
- Fixed `quick_trade.py` importing from non-existent `auth_utils` module (corrected to `auth`)
- Fixed "Live Performance" data showing all zeros in Indicator Market (incorrect SQL query referencing non-existent columns)
- Fixed incorrect entry price display in Position Records (was falling back to current price)
- Fixed inaccurate System Overview statistics for running strategies, total capital, and total PnL
@@ -148,6 +164,34 @@ CREATE UNIQUE INDEX IF NOT EXISTS idx_usdt_orders_address_unique ON qd_usdt_orde
CREATE INDEX IF NOT EXISTS idx_usdt_orders_user_id ON qd_usdt_orders(user_id);
CREATE INDEX IF NOT EXISTS idx_usdt_orders_status ON qd_usdt_orders(status);
-- 5. Quick Trades table (manual / discretionary orders from Quick Trade Panel)
CREATE TABLE IF NOT EXISTS qd_quick_trades (
id SERIAL PRIMARY KEY,
user_id INTEGER NOT NULL REFERENCES qd_users(id) ON DELETE CASCADE,
credential_id INTEGER DEFAULT 0,
exchange_id VARCHAR(40) NOT NULL DEFAULT '',
symbol VARCHAR(60) NOT NULL DEFAULT '',
side VARCHAR(10) NOT NULL DEFAULT '', -- buy / sell
order_type VARCHAR(20) NOT NULL DEFAULT 'market', -- market / limit
amount DECIMAL(24, 8) DEFAULT 0,
price DECIMAL(24, 8) DEFAULT 0,
leverage INTEGER DEFAULT 1,
market_type VARCHAR(20) DEFAULT 'swap', -- swap / spot
tp_price DECIMAL(24, 8) DEFAULT 0,
sl_price DECIMAL(24, 8) DEFAULT 0,
status VARCHAR(20) DEFAULT 'submitted', -- submitted / filled / failed / cancelled
exchange_order_id VARCHAR(120) DEFAULT '',
filled_amount DECIMAL(24, 8) DEFAULT 0,
avg_fill_price DECIMAL(24, 8) DEFAULT 0,
error_msg TEXT DEFAULT '',
source VARCHAR(40) DEFAULT 'manual', -- ai_radar / ai_analysis / indicator / manual
raw_result JSONB,
created_at TIMESTAMP DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_quick_trades_user ON qd_quick_trades(user_id);
CREATE INDEX IF NOT EXISTS idx_quick_trades_created ON qd_quick_trades(created_at DESC);
-- Migration Complete
DO $$
BEGIN
+13
View File
@@ -0,0 +1,13 @@
# QuantDinger Frontend - Pre-built Static Files
# No build step needed - dist/ is already compiled.
FROM nginx:1.25-alpine
# Copy pre-built frontend files
COPY dist/ /usr/share/nginx/html/
# Copy nginx configuration
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
+1
View File
@@ -0,0 +1 @@
2.2.1
@@ -4,15 +4,23 @@ server {
root /usr/share/nginx/html;
index index.html;
# Security headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
# Gzip compression
gzip on;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
gzip_vary on;
gzip_min_length 1000;
gzip_comp_level 6;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript image/svg+xml;
# Static asset caching
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
# Static asset caching (hashed filenames = immutable)
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot|map)$ {
expires 1y;
add_header Cache-Control "public, immutable";
access_log off;
}
# API proxy to backend
@@ -28,16 +36,19 @@ server {
proxy_cache_bypass $http_upgrade;
proxy_read_timeout 300s;
proxy_connect_timeout 75s;
# Allow large file uploads (e.g. indicator import)
client_max_body_size 10m;
}
# SPA routing support
# SPA routing support (all routes fall back to index.html)
location / {
try_files $uri $uri/ /index.html;
}
# Health check
# Health check endpoint
location /health {
return 200 'OK';
add_header Content-Type text/plain;
access_log off;
}
}
-6
View File
@@ -1,6 +0,0 @@
{
"name": "QuantDinger",
"lockfileVersion": 3,
"requires": true,
"packages": {}
}
-3
View File
@@ -1,3 +0,0 @@
> 1%
last 2 versions
not ie <= 10
-41
View File
@@ -1,41 +0,0 @@
# 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
@@ -1,39 +0,0 @@
[*]
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
@@ -1,3 +0,0 @@
NODE_ENV=production
VUE_APP_PREVIEW=false
VUE_APP_API_BASE_URL=/api
-3
View File
@@ -1,3 +0,0 @@
NODE_ENV=development
VUE_APP_PREVIEW=true
VUE_APP_API_BASE_URL=/api
-3
View File
@@ -1,3 +0,0 @@
NODE_ENV=production
VUE_APP_PREVIEW=true
VUE_APP_API_BASE_URL=/api
-75
View File
@@ -1,75 +0,0 @@
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
@@ -1,5 +0,0 @@
{
"rules": {
"space-before-function-paren": 0
}
}
-11
View File
@@ -1,11 +0,0 @@
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
@@ -1,21 +0,0 @@
.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
@@ -1 +0,0 @@
_
-4
View File
@@ -1,4 +0,0 @@
{
"*.js": "eslint --fix",
"*.{css,less}": "stylelint --fix"
}
-6
View File
@@ -1,6 +0,0 @@
{
"printWidth": 120,
"semi": false,
"singleQuote": true,
"prettier.spaceBeforeFunctionParen": true
}
-102
View File
@@ -1,102 +0,0 @@
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
@@ -1,7 +0,0 @@
language: node_js
node_js:
- 10.15.0
cache: yarn
script:
- yarn
- yarn run lint --no-fix && yarn run build
-32
View File
@@ -1,32 +0,0 @@
# 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;"]
-62
View File
@@ -1,62 +0,0 @@
# QuantDinger Web UI (Vue 2)
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 quantdinger_vue
npm install
```
### 2) Start dev server
```bash
npm run serve
```
Dev server runs at `http://localhost:8000`.
### 3) API proxy (important)
In dev mode, this project proxies `/api/*` to the backend:
- Proxy config: `quantdinger_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
npm run build
```
The output will be generated under `quantdinger_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`.
-30
View File
@@ -1,30 +0,0 @@
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
@@ -1,27 +0,0 @@
# 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
@@ -1,21 +0,0 @@
#!/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
@@ -1,26 +0,0 @@
/**
* feat新增功能
* fixbug 修复
* docs文档更新
* style不影响程序逻辑的代码修改(修改空白字符格式缩进补全缺失的分号等没有改变代码逻辑)
* refactor重构代码(既没有新增功能也没有修复 bug)
* perf性能, 体验优化
* test新增测试用例或是更新现有测试
* build主要目的是修改项目构建系统(例如 glupwebpackrollup 的配置等)的提交
* 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
@@ -1,49 +0,0 @@
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
@@ -1,115 +0,0 @@
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
@@ -1,9 +0,0 @@
0.0.0.0:80 {
gzip
root /usr/share/nginx/html
rewrite {
r .*
to {path} /
}
}
-24
View File
@@ -1,24 +0,0 @@
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
@@ -1,23 +0,0 @@
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
@@ -1,11 +0,0 @@
{
"compilerOptions": {
"target": "es6",
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
},
"exclude": ["node_modules", "dist"],
"include": ["src/**/*"]
}
-83
View File
@@ -1,83 +0,0 @@
{
"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"
}
}
-5
View File
@@ -1,5 +0,0 @@
module.exports = {
plugins: {
autoprefixer: {}
}
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 131 KiB

-477
View File
@@ -1,477 +0,0 @@
<!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>
Binary file not shown.

Before

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
Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

-49
View File
@@ -1,49 +0,0 @@
<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
@@ -1,113 +0,0 @@
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
@@ -1,131 +0,0 @@
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
@@ -1,39 +0,0 @@
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
@@ -1,40 +0,0 @@
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
@@ -1,30 +0,0 @@
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
@@ -1,104 +0,0 @@
/**
* 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
@@ -1,80 +0,0 @@
/**
* 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
@@ -1,59 +0,0 @@
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
@@ -1,70 +0,0 @@
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
@@ -1,259 +0,0 @@
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
})
}
-151
View File
@@ -1,151 +0,0 @@
/**
* 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'
})
}
-56
View File
@@ -1,56 +0,0 @@
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
@@ -1,237 +0,0 @@
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
@@ -1,222 +0,0 @@
/**
* 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
@@ -1,69 +0,0 @@
<?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>

Before

Width:  |  Height:  |  Size: 8.7 KiB

@@ -1 +0,0 @@
<?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>

Before

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 103 KiB

-29
View File
@@ -1,29 +0,0 @@
<?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>

Before

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

@@ -1,89 +0,0 @@
<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>
@@ -1,3 +0,0 @@
import ArticleListContent from './ArticleListContent'
export default ArticleListContent
@@ -1,25 +0,0 @@
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
@@ -1,72 +0,0 @@
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
@@ -1,9 +0,0 @@
import AvatarList from './List'
import Item from './Item'
export {
AvatarList,
Item as AvatarListItem
}
export default AvatarList
@@ -1,59 +0,0 @@
@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;
}
}
}
}
}
@@ -1,64 +0,0 @@
# 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 | - |
@@ -1,62 +0,0 @@
<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>
@@ -1,120 +0,0 @@
<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>
@@ -1,67 +0,0 @@
<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>
@@ -1,56 +0,0 @@
<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>
@@ -1,57 +0,0 @@
<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>
@@ -1,75 +0,0 @@
<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>
@@ -1,40 +0,0 @@
<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>
@@ -1,68 +0,0 @@
<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>
@@ -1,77 +0,0 @@
<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>
@@ -1,113 +0,0 @@
<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>
@@ -1,64 +0,0 @@
<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>
@@ -1,82 +0,0 @@
<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>
@@ -1,13 +0,0 @@
.antv-chart-mini {
position: relative;
width: 100%;
.chart-wrapper {
position: absolute;
bottom: -28px;
width: 100%;
/* margin: 0 -5px;
overflow: hidden; */
}
}
@@ -1,14 +0,0 @@
@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
@@ -1,113 +0,0 @@
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)
}
}
})
}
@@ -1,79 +0,0 @@
<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>

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