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
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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')
|
||||
@@ -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
|
||||
@@ -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')})
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
-- =============================================================================
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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 1x–125x 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
|
||||
|
||||
@@ -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;"]
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"name": "QuantDinger",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
> 1%
|
||||
last 2 versions
|
||||
not ie <= 10
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
NODE_ENV=production
|
||||
VUE_APP_PREVIEW=false
|
||||
VUE_APP_API_BASE_URL=/api
|
||||
@@ -1,3 +0,0 @@
|
||||
NODE_ENV=development
|
||||
VUE_APP_PREVIEW=true
|
||||
VUE_APP_API_BASE_URL=/api
|
||||
@@ -1,3 +0,0 @@
|
||||
NODE_ENV=production
|
||||
VUE_APP_PREVIEW=true
|
||||
VUE_APP_API_BASE_URL=/api
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"rules": {
|
||||
"space-before-function-paren": 0
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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 +0,0 @@
|
||||
_
|
||||
@@ -1,4 +0,0 @@
|
||||
{
|
||||
"*.js": "eslint --fix",
|
||||
"*.{css,less}": "stylelint --fix"
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"printWidth": 120,
|
||||
"semi": false,
|
||||
"singleQuote": true,
|
||||
"prettier.spaceBeforeFunctionParen": true
|
||||
}
|
||||
@@ -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'
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
language: node_js
|
||||
node_js:
|
||||
- 10.15.0
|
||||
cache: yarn
|
||||
script:
|
||||
- yarn
|
||||
- yarn run lint --no-fix && yarn run build
|
||||
@@ -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;"]
|
||||
@@ -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 don’t 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`.
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
@@ -1,26 +0,0 @@
|
||||
/**
|
||||
* feat:新增功能
|
||||
* fix:bug 修复
|
||||
* docs:文档更新
|
||||
* style:不影响程序逻辑的代码修改(修改空白字符,格式缩进,补全缺失的分号等,没有改变代码逻辑)
|
||||
* refactor:重构代码(既没有新增功能,也没有修复 bug)
|
||||
* perf:性能, 体验优化
|
||||
* test:新增测试用例或是更新现有测试
|
||||
* build:主要目的是修改项目构建系统(例如 glup,webpack,rollup 的配置等)的提交
|
||||
* ci:主要目的是修改项目继续集成流程(例如 Travis,Jenkins,GitLab CI,Circle等)的提交
|
||||
* 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']
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
0.0.0.0:80 {
|
||||
gzip
|
||||
root /usr/share/nginx/html
|
||||
|
||||
rewrite {
|
||||
r .*
|
||||
to {path} /
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
# }
|
||||
}
|
||||
@@ -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/'
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es6",
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["src/*"]
|
||||
}
|
||||
},
|
||||
"exclude": ["node_modules", "dist"],
|
||||
"include": ["src/**/*"]
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
autoprefixer: {}
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 131 KiB |
@@ -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>
|
||||
|
Before Width: | Height: | Size: 9.0 KiB |
|
Before Width: | Height: | Size: 26 KiB |
@@ -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>
|
||||
@@ -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'
|
||||
})
|
||||
}
|
||||
@@ -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')
|
||||
}
|
||||
@@ -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 }
|
||||
})
|
||||
}
|
||||
@@ -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 }
|
||||
})
|
||||
}
|
||||
@@ -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'
|
||||
})
|
||||
}
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
@@ -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'
|
||||
})
|
||||
}
|
||||
@@ -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'
|
||||
})
|
||||
}
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
@@ -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'
|
||||
})
|
||||
}
|
||||
@@ -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'
|
||||
})
|
||||
}
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
@@ -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 |
|
Before Width: | Height: | Size: 103 KiB |
@@ -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 |
|
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%;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||