Refactor code structure for improved readability and maintainability

This commit is contained in:
dienakdz
2026-04-08 10:00:42 +07:00
parent 68b296117b
commit 103055b3df
218 changed files with 46 additions and 93927 deletions
+5 -5
View File
@@ -1,15 +1,15 @@
import { isIE } from '@/utils/util'
// 本地开发已有完整后端 API,禁用 mock 以避免请求被拦截
// 如需启用 mock,将下面的 false 改为 true
// Local development already has a complete backend API, so mock is disabled
// to avoid intercepting real requests. Set this to true if you need mock data.
const ENABLE_MOCK = false
// 判断环境不是 prod 或者 preview 是 true 时,加载 mock 服务
// Load mock services only outside production, or when preview mode is enabled.
if (ENABLE_MOCK && (process.env.NODE_ENV !== 'production' || process.env.VUE_APP_PREVIEW === 'true')) {
if (isIE()) {
}
// 使用同步加载依赖
// 防止 vuex 中的 GetInfo 早于 mock 运行,导致无法 mock 请求返回结果
// Use synchronous loading so the mock layer is ready before Vuex requests
// run. This prevents early GetInfo calls from bypassing mocked responses.
const Mock = require('mockjs2')
require('./services/auth')
require('./services/user')
+41 -42
View File
@@ -1,8 +1,8 @@
路由/菜单说明
Router and Menu Notes
====
格式和说明
Format and Overview
----
```ecmascript 6
@@ -22,34 +22,32 @@ const routerObject = {
`{ Route }` 对象
`{ Route }` object
| 参数 | 说明 | 类型 | 默认值 |
| -------- | ----------------------------------------- | ------- | ------ |
| hidden | 控制路由是否显示在 sidebar | boolean | false |
| redirect | 重定向地址, 访问这个路由时,自定进行重定向 | string | - |
| name | 路由名称, 必须设置,且不能重名 | string | - |
| meta | 路由元信息(路由附带扩展信息) | object | {} |
| hideChildrenInMenu | 强制菜单显示为Item而不是SubItem(配合 meta.hidden) | boolean | - |
| Field | Description | Type | Default |
| ----- | ----------- | ---- | ------- |
| hidden | Controls whether the route is shown in the sidebar | boolean | false |
| redirect | Redirect target used when the route is accessed | string | - |
| name | Route name. Must be unique and is required | string | - |
| meta | Route metadata (extended route information) | object | {} |
| hideChildrenInMenu | Forces the menu to render as an item instead of a submenu, typically used with `meta.hidden` | boolean | - |
`{ Meta }` 路由元信息对象
`{ Meta }` route metadata object
| 参数 | 说明 | 类型 | 默认值 |
| ------------------- | ------------------------------------------------------------ | ------- | ------ |
| title | 路由标题, 用于显示面包屑, 页面标题 *推荐设置 | string | - |
| icon | 路由在 menu 上显示的图标 | [string,svg] | - |
| keepAlive | 缓存该路由 | boolean | false |
| target | 菜单链接跳转目标(参考 html a 标记) | string | - |
| hidden | 配合`hideChildrenInMenu`使用,用于隐藏菜单时,提供递归到父菜单显示 选中菜单项_(可参考 个人页 配置方式)_ | boolean | false |
| hiddenHeaderContent | *特殊 隐藏 [PageHeader](https://github.com/vueComponent/ant-design-vue-pro/blob/master/src/components/PageHeader/PageHeader.vue#L6) 组件中的页面带的 面包屑和页面标题栏 | boolean | false |
| permission | 与项目提供的权限拦截匹配的权限,如果不匹配,则会被禁止访问该路由页面 | array | [] |
| Field | Description | Type | Default |
| ----- | ----------- | ---- | ------- |
| title | Route title used for breadcrumbs and page titles. Recommended | string | - |
| icon | Icon shown in the menu | [string,svg] | - |
| keepAlive | Whether the route should be cached | boolean | false |
| target | Link target for menu navigation, similar to the HTML `a` tag | string | - |
| hidden | Used together with `hideChildrenInMenu` to keep the correct parent menu item selected when the current menu node is hidden | boolean | false |
| hiddenHeaderContent | Hides the breadcrumb and page header content in the [PageHeader](https://github.com/vueComponent/ant-design-vue-pro/blob/master/src/components/PageHeader/PageHeader.vue#L6) component | boolean | false |
| permission | Permission keys checked by the app permission guard. Routes without a matching permission are blocked | array | [] |
> 路由自定义 `Icon` 请引入自定义 `svg` Icon 文件,然后传递给路由的 `meta.icon` 参数即可
> For a custom route `Icon`, import the corresponding custom `svg` file and pass it through the route `meta.icon` field.
路由构建例子方案1
路由例子
Example Route Configuration
----
```ecmascript 6
@@ -58,7 +56,7 @@ const asyncRouterMap = [
path: '/',
name: 'index',
component: BasicLayout,
meta: { title: '首页' },
meta: { title: 'Home' },
redirect: '/ai-analysis',
children: [
{
@@ -66,51 +64,51 @@ const asyncRouterMap = [
component: RouteView,
name: 'dashboard',
redirect: '/dashboard/workplace',
meta: {title: '仪表盘', icon: 'dashboard', permission: ['dashboard']},
meta: { title: 'Dashboard', icon: 'dashboard', permission: ['dashboard'] },
children: [
{
path: '/ai-analysis',
name: 'Analysis',
component: () => import('@/views/dashboard/Analysis'),
meta: {title: '分析页', permission: ['dashboard']}
meta: { title: 'Analysis', permission: ['dashboard'] }
},
{
path: '/dashboard/monitor',
name: 'Monitor',
hidden: true,
component: () => import('@/views/dashboard/Monitor'),
meta: {title: '监控页', permission: ['dashboard']}
meta: { title: 'Monitor', permission: ['dashboard'] }
},
{
path: '/dashboard/workplace',
name: 'Workplace',
component: () => import('@/views/dashboard/Workplace'),
meta: {title: '工作台', permission: ['dashboard']}
meta: { title: 'Workplace', permission: ['dashboard'] }
}
]
},
// result
// Result pages
{
path: '/result',
name: 'result',
component: PageView,
redirect: '/result/success',
meta: { title: '结果页', icon: 'check-circle-o', permission: [ 'result' ] },
meta: { title: 'Result', icon: 'check-circle-o', permission: [ 'result' ] },
children: [
{
path: '/result/success',
name: 'ResultSuccess',
component: () => import(/* webpackChunkName: "result" */ '@/views/result/Success'),
// 该页面隐藏面包屑和页面标题栏
meta: { title: '成功', hiddenHeaderContent: true, permission: [ 'result' ] }
// This page hides breadcrumbs and the page title bar.
meta: { title: 'Success', hiddenHeaderContent: true, permission: [ 'result' ] }
},
{
path: '/result/fail',
name: 'ResultFail',
component: () => import(/* webpackChunkName: "result" */ '@/views/result/Error'),
// 该页面隐藏面包屑和页面标题栏
meta: { title: '失败', hiddenHeaderContent: true, permission: [ 'result' ] }
// This page hides breadcrumbs and the page title bar.
meta: { title: 'Failure', hiddenHeaderContent: true, permission: [ 'result' ] }
}
]
},
@@ -120,15 +118,16 @@ const asyncRouterMap = [
]
```
> 1. 请注意 `component: () => import('..') ` 方式引入路由的页面组件为 懒加载模式。具体可以看 [Vue 官方文档](https://router.vuejs.org/zh/guide/advanced/lazy-loading.html)
> 2. 增加新的路由应该增加在 '/' (index) 路由的 `children`
> 3. 子路由的父级路由必须有 `router-view` 才能让子路由渲染出来,请仔细查阅 vue-router 文档
> 4. `permission` 可以进行自定义修改,只需要对这个模块进行自定义修改即可 [src/store/modules/permission.js#L10](https://github.com/vueComponent/ant-design-vue-pro/blob/master/src/store/modules/permission.js#L10)
> 1. `component: () => import('..')` uses lazy-loaded route components. See the [Vue Router docs](https://router.vuejs.org/guide/advanced/lazy-loading.html) for details.
> 2. New top-level application routes should usually be added under the `'/'` route `children`.
> 3. A parent route for nested children must expose `router-view`, otherwise child routes cannot render.
> 4. The `permission` system can be customized in the corresponding permission module, for example [src/store/modules/permission.js#L10](https://github.com/vueComponent/ant-design-vue-pro/blob/master/src/store/modules/permission.js#L10).
附权限路由结构:
Permission Route Structure
![权限结构](https://static-2.loacg.com/open/static/github/permissions.png)
![Permission Structure](https://static-2.loacg.com/open/static/github/permissions.png)
第二种前端路由由后端动态生成的设计,可以前往官网文档 https://pro.antdv.com/docs/authority-management 参考
If you prefer backend-driven dynamic route generation, refer to the official documentation:
https://pro.antdv.com/docs/authority-management
-3
View File
@@ -1,3 +0,0 @@
> 1%
last 2 versions
not ie <= 10
-41
View File
@@ -1,41 +0,0 @@
# Dependencies
node_modules/
# Build output (会在容器内构建)
dist/
# IDE
.idea/
.vscode/
*.swp
*.swo
# Logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# Environment
.env
.env.local
.env.*.local
# Git
.git/
.gitignore
# Tests
tests/
coverage/
.nyc_output/
# Documentation
*.md
!README.md
# OS
.DS_Store
Thumbs.db
-39
View File
@@ -1,39 +0,0 @@
[*]
charset=utf-8
end_of_line=lf
insert_final_newline=false
indent_style=space
indent_size=2
[{*.ng,*.sht,*.html,*.shtm,*.shtml,*.htm}]
indent_style=space
indent_size=2
[{*.jhm,*.xslt,*.xul,*.rng,*.xsl,*.xsd,*.ant,*.tld,*.fxml,*.jrxml,*.xml,*.jnlp,*.wsdl}]
indent_style=space
indent_size=2
[{.babelrc,.stylelintrc,jest.config,.eslintrc,.prettierrc,*.json,*.jsb3,*.jsb2,*.bowerrc}]
indent_style=space
indent_size=2
[*.svg]
indent_style=space
indent_size=2
[*.js.map]
indent_style=space
indent_size=2
[*.less]
indent_style=space
indent_size=2
[*.vue]
indent_style=space
indent_size=2
[{.analysis_options,*.yml,*.yaml}]
indent_style=space
indent_size=2
-3
View File
@@ -1,3 +0,0 @@
NODE_ENV=production
VUE_APP_PREVIEW=false
VUE_APP_API_BASE_URL=/api
-3
View File
@@ -1,3 +0,0 @@
NODE_ENV=development
VUE_APP_PREVIEW=true
VUE_APP_API_BASE_URL=/api
-3
View File
@@ -1,3 +0,0 @@
NODE_ENV=production
VUE_APP_PREVIEW=true
VUE_APP_API_BASE_URL=/api
-75
View File
@@ -1,75 +0,0 @@
module.exports = {
root: true,
env: {
node: true
},
'extends': [
'plugin:vue/strongly-recommended',
'@vue/standard'
],
rules: {
'no-console': 'off',
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',
'generator-star-spacing': 'off',
'no-mixed-operators': 0,
'vue/max-attributes-per-line': [
2,
{
'singleline': 5,
'multiline': {
'max': 1,
'allowFirstLine': false
}
}
],
'vue/attribute-hyphenation': 0,
'vue/html-self-closing': 0,
'vue/component-name-in-template-casing': 0,
'vue/html-closing-bracket-spacing': 0,
'vue/singleline-html-element-content-newline': 0,
'vue/no-unused-components': 0,
'vue/multiline-html-element-content-newline': 0,
'vue/no-use-v-if-with-v-for': 0,
'vue/html-closing-bracket-newline': 0,
'vue/no-parsing-error': 0,
'no-tabs': 0,
'quotes': [
2,
'single',
{
'avoidEscape': true,
'allowTemplateLiterals': true
}
],
'semi': [
2,
'never',
{
'beforeStatementContinuationChars': 'never'
}
],
'no-delete-var': 2,
'prefer-const': [
2,
{
'ignoreReadBeforeAssign': false
}
],
'template-curly-spacing': 'off',
'indent': 'off'
},
parserOptions: {
parser: 'babel-eslint'
},
overrides: [
{
files: [
'**/__tests__/*.{j,t}s?(x)',
'**/tests/unit/**/*.spec.{j,t}s?(x)'
],
env: {
jest: true
}
}
]
}
-5
View File
@@ -1,5 +0,0 @@
{
"rules": {
"space-before-function-paren": 0
}
}
-11
View File
@@ -1,11 +0,0 @@
public/* linguist-vendored
# Automatically normalize line endings (to LF) for all text-based files.
* text=auto eol=lf
# Declare files that will always have CRLF line endings on checkout.
*.{cmd,[cC][mM][dD]} text eol=crlf
*.{bat,[bB][aA][tT]} text eol=crlf
# Denote all files that are truly binary and should not be modified.
*.{ico,png,jpg,jpeg,gif,webp,svg,woff,woff2} binary
-26
View File
@@ -1,26 +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
# Pyodide (large offline runtime assets)
# - Download via `scripts/fetch_pyodide.ps1`
# - If you want fully offline distribution, remove this ignore and commit the assets.
/public/assets/pyodide/
-1
View File
@@ -1 +0,0 @@
_
-4
View File
@@ -1,4 +0,0 @@
{
"*.js": "eslint --fix",
"*.{css,less}": "stylelint --fix"
}
-6
View File
@@ -1,6 +0,0 @@
{
"printWidth": 120,
"semi": false,
"singleQuote": true,
"prettier.spaceBeforeFunctionParen": true
}
-102
View File
@@ -1,102 +0,0 @@
module.exports = {
processors: [],
plugins: ['stylelint-order'],
extends: [
'stylelint-config-standard',
'stylelint-config-css-modules'
],
rules: {
'selector-class-pattern': null,
'string-quotes': 'single', // 单引号
'at-rule-empty-line-before': null,
'at-rule-no-unknown': null,
'at-rule-name-case': 'lower', // 指定@规则名的大小写
'length-zero-no-unit': true, // 禁止零长度的单位(可自动修复)
'shorthand-property-no-redundant-values': true, // 简写属性
'number-leading-zero': 'never', // 小数不带0
'declaration-block-no-duplicate-properties': null, // 禁止声明快重复属性
'no-descending-specificity': null, // 禁止在具有较高优先级的选择器后出现被其覆盖的较低优先级的选择器。
'selector-max-id': 3, // 限制一个选择器中 ID 选择器的数量
'max-nesting-depth': 4,
'indentation': [2, { // 指定缩进 warning 提醒
'severity': 'warning'
}],
'order/properties-order': [ // 规则顺序
'position',
'top',
'right',
'bottom',
'left',
'z-index',
'display',
'float',
'width',
'height',
'max-width',
'max-height',
'min-width',
'min-height',
'padding',
'padding-top',
'padding-right',
'padding-bottom',
'padding-left',
'margin',
'margin-top',
'margin-right',
'margin-bottom',
'margin-left',
'margin-collapse',
'margin-top-collapse',
'margin-right-collapse',
'margin-bottom-collapse',
'margin-left-collapse',
'overflow',
'overflow-x',
'overflow-y',
'clip',
'clear',
'font',
'font-family',
'font-size',
'font-smoothing',
'osx-font-smoothing',
'font-style',
'font-weight',
'line-height',
'letter-spacing',
'word-spacing',
'color',
'text-align',
'text-decoration',
'text-indent',
'text-overflow',
'text-rendering',
'text-size-adjust',
'text-shadow',
'text-transform',
'word-break',
'word-wrap',
'white-space',
'vertical-align',
'list-style',
'list-style-type',
'list-style-position',
'list-style-image',
'pointer-events',
'cursor',
'background',
'background-color',
'border',
'border-radius',
'content',
'outline',
'outline-offset',
'opacity',
'filter',
'visibility',
'size',
'transform'
]
}
}
-7
View File
@@ -1,7 +0,0 @@
language: node_js
node_js:
- 10.15.0
cache: yarn
script:
- yarn
- yarn run lint --no-fix && yarn run build
-33
View File
@@ -1,33 +0,0 @@
# QuantDinger Frontend Dockerfile
# Stage 1: Build
FROM node:18-alpine as builder
WORKDIR /app
# Copy package files
COPY package*.json ./
COPY pnpm-lock.yaml* ./
COPY yarn.lock* ./
# 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)
FROM nginx:alpine
# Copy build artifacts
COPY --from=builder /app/dist /usr/share/nginx/html
# Copy nginx configuration
COPY deploy/nginx-docker.conf /etc/nginx/conf.d/default.conf
# Expose port
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
-62
View File
@@ -1,62 +0,0 @@
# QuantDinger Web UI (Vue 2)
This is the QuantDinger frontend web UI built with **Vue 2** + **Ant Design Vue**. It connects to the Python backend (`backend_api_python/`) through HTTP APIs to provide charts, indicators, backtests, AI analysis, and strategy management.
> This UI is based on the open-source `ant-design-vue-pro` ecosystem, heavily adapted for QuantDinger.
## What you get
- **Dashboards**: summary views and operational panels
- **Indicator analysis**: Kline charts + indicator editing + backtest history
- **AI analysis**: multi-agent reports (optional LLM/search, configured on backend)
- **Trading assistant**: strategy lifecycle + positions/records (depending on backend capability)
- **Local auth**: login with backend-configured admin credentials
## Quick start (local development)
### Prerequisites
- Node.js 16+ recommended
- Backend running at `http://localhost:5000` (see `backend_api_python/README.md`)
### 1) Install dependencies
```bash
cd quantdinger_vue
npm install
```
### 2) Start dev server
```bash
npm run serve
```
Dev server runs at `http://localhost:8000`.
### 3) API proxy (important)
In dev mode, this project proxies `/api/*` to the backend:
- Proxy config: `quantdinger_vue/vue.config.js`
- Default target: `http://localhost:5000`
If your backend runs on a different host/port, update `vue.config.js` accordingly.
## Production build
```bash
npm run build
```
The output will be generated under `quantdinger_vue/dist/`.
## Notes
- **CORS**: when using the dev proxy, you typically dont need extra CORS config.
- **Login**: use the credentials defined in `backend_api_python/.env` (`ADMIN_USER` / `ADMIN_PASSWORD`).
## License
Apache License 2.0. See repository root `LICENSE`.
-30
View File
@@ -1,30 +0,0 @@
const IS_PROD = ['production', 'prod'].includes(process.env.NODE_ENV)
const IS_PREVIEW = process.env.VUE_APP_PREVIEW === 'true'
const plugins = []
if (IS_PROD && !IS_PREVIEW) {
// 去除日志的插件,
plugins.push('transform-remove-console')
}
// lazy load ant-design-vue
// if your use import on Demand, Use this code
plugins.push(['import', {
'libraryName': 'ant-design-vue',
'libraryDirectory': 'es',
'style': true // `style: true` 会加载 less 文件
}])
module.exports = {
presets: [
'@vue/cli-plugin-babel/preset',
[
'@babel/preset-env',
{
'useBuiltIns': 'entry',
'corejs': 3
}
]
],
plugins
}
-26
View File
@@ -1,26 +0,0 @@
/**
* feat:新增功能
* fixbug 修复
* docs:文档更新
* style:不影响程序逻辑的代码修改(修改空白字符,格式缩进,补全缺失的分号等,没有改变代码逻辑)
* refactor:重构代码(既没有新增功能,也没有修复 bug)
* perf:性能, 体验优化
* test:新增测试用例或是更新现有测试
* build:主要目的是修改项目构建系统(例如 glup,webpackrollup 的配置等)的提交
* ci:主要目的是修改项目继续集成流程(例如 TravisJenkinsGitLab CICircle等)的提交
* chore:不属于以上类型的其他类型,比如构建流程, 依赖管理
* revert:回滚某个更早之前的提交
*/
module.exports = {
extends: ['@commitlint/config-conventional'],
rules: {
'type-enum': [
2,
'always',
['feat', 'fix', 'docs', 'style', 'refactor', 'test', 'chore', 'revert']
],
'subject-full-stop': [0, 'never'],
'subject-case': [0, 'never']
}
}
-49
View File
@@ -1,49 +0,0 @@
const ThemeColorReplacer = require('webpack-theme-color-replacer')
const generate = require('@ant-design/colors/lib/generate').default
const getAntdSerials = (color) => {
// 淡化(即less的tint
const lightens = new Array(9).fill().map((t, i) => {
return ThemeColorReplacer.varyColor.lighten(color, i / 10)
})
const colorPalettes = generate(color)
const rgb = ThemeColorReplacer.varyColor.toNum3(color.replace('#', '')).join(',')
return lightens.concat(colorPalettes).concat(rgb)
}
const themePluginOption = {
fileName: 'css/theme-colors-[contenthash:8].css',
matchColors: getAntdSerials('#1890ff'), // 主色系列
// 改变样式选择器,解决样式覆盖问题
changeSelector (selector) {
switch (selector) {
case '.ant-calendar-today .ant-calendar-date':
return ':not(.ant-calendar-selected-date):not(.ant-calendar-selected-day)' + selector
case '.ant-btn:focus,.ant-btn:hover':
return '.ant-btn:focus:not(.ant-btn-primary):not(.ant-btn-danger),.ant-btn:hover:not(.ant-btn-primary):not(.ant-btn-danger)'
case '.ant-btn.active,.ant-btn:active':
return '.ant-btn.active:not(.ant-btn-primary):not(.ant-btn-danger),.ant-btn:active:not(.ant-btn-primary):not(.ant-btn-danger)'
case '.ant-steps-item-process .ant-steps-item-icon > .ant-steps-icon':
case '.ant-steps-item-process .ant-steps-item-icon>.ant-steps-icon':
return ':not(.ant-steps-item-process)' + selector
// fixed https://github.com/vueComponent/ant-design-vue-pro/issues/876
case '.ant-steps-item-process .ant-steps-item-icon':
return ':not(.ant-steps-item-custom)' + selector
case '.ant-menu-horizontal>.ant-menu-item-active,.ant-menu-horizontal>.ant-menu-item-open,.ant-menu-horizontal>.ant-menu-item-selected,.ant-menu-horizontal>.ant-menu-item:hover,.ant-menu-horizontal>.ant-menu-submenu-active,.ant-menu-horizontal>.ant-menu-submenu-open,.ant-menu-horizontal>.ant-menu-submenu-selected,.ant-menu-horizontal>.ant-menu-submenu:hover':
case '.ant-menu-horizontal > .ant-menu-item-active,.ant-menu-horizontal > .ant-menu-item-open,.ant-menu-horizontal > .ant-menu-item-selected,.ant-menu-horizontal > .ant-menu-item:hover,.ant-menu-horizontal > .ant-menu-submenu-active,.ant-menu-horizontal > .ant-menu-submenu-open,.ant-menu-horizontal > .ant-menu-submenu-selected,.ant-menu-horizontal > .ant-menu-submenu:hover':
return '.ant-menu-horizontal > .ant-menu-item-active,.ant-menu-horizontal > .ant-menu-item-open,.ant-menu-horizontal > .ant-menu-item-selected,.ant-menu-horizontal:not(.ant-menu-dark) > .ant-menu-item:hover,.ant-menu-horizontal > .ant-menu-submenu-active,.ant-menu-horizontal > .ant-menu-submenu-open,.ant-menu-horizontal:not(.ant-menu-dark) > .ant-menu-submenu-selected,.ant-menu-horizontal:not(.ant-menu-dark) > .ant-menu-submenu:hover'
case '.ant-menu-horizontal > .ant-menu-item-selected > a':
case '.ant-menu-horizontal>.ant-menu-item-selected>a':
return '.ant-menu-horizontal:not(ant-menu-light):not(.ant-menu-dark) > .ant-menu-item-selected > a'
case '.ant-menu-horizontal > .ant-menu-item > a:hover':
case '.ant-menu-horizontal>.ant-menu-item>a:hover':
return '.ant-menu-horizontal:not(ant-menu-light):not(.ant-menu-dark) > .ant-menu-item > a:hover'
default :
return selector
}
}
}
const createThemeColorReplacerPlugin = () => new ThemeColorReplacer(themePluginOption)
module.exports = createThemeColorReplacerPlugin
-115
View File
@@ -1,115 +0,0 @@
export default {
theme: [
{
key: 'dark',
fileName: 'dark.css',
theme: 'dark'
},
{
key: '#F5222D',
fileName: '#F5222D.css',
modifyVars: {
'@primary-color': '#F5222D'
}
},
{
key: '#FA541C',
fileName: '#FA541C.css',
modifyVars: {
'@primary-color': '#FA541C'
}
},
{
key: '#FAAD14',
fileName: '#FAAD14.css',
modifyVars: {
'@primary-color': '#FAAD14'
}
},
{
key: '#13C2C2',
fileName: '#13C2C2.css',
modifyVars: {
'@primary-color': '#13C2C2'
}
},
{
key: '#52C41A',
fileName: '#52C41A.css',
modifyVars: {
'@primary-color': '#52C41A'
}
},
{
key: '#2F54EB',
fileName: '#2F54EB.css',
modifyVars: {
'@primary-color': '#2F54EB'
}
},
{
key: '#722ED1',
fileName: '#722ED1.css',
modifyVars: {
'@primary-color': '#722ED1'
}
},
{
key: '#F5222D',
theme: 'dark',
fileName: 'dark-#F5222D.css',
modifyVars: {
'@primary-color': '#F5222D'
}
},
{
key: '#FA541C',
theme: 'dark',
fileName: 'dark-#FA541C.css',
modifyVars: {
'@primary-color': '#FA541C'
}
},
{
key: '#FAAD14',
theme: 'dark',
fileName: 'dark-#FAAD14.css',
modifyVars: {
'@primary-color': '#FAAD14'
}
},
{
key: '#13C2C2',
theme: 'dark',
fileName: 'dark-#13C2C2.css',
modifyVars: {
'@primary-color': '#13C2C2'
}
},
{
key: '#52C41A',
theme: 'dark',
fileName: 'dark-#52C41A.css',
modifyVars: {
'@primary-color': '#52C41A'
}
},
{
key: '#2F54EB',
theme: 'dark',
fileName: 'dark-#2F54EB.css',
modifyVars: {
'@primary-color': '#2F54EB'
}
},
{
key: '#722ED1',
theme: 'dark',
fileName: 'dark-#722ED1.css',
modifyVars: {
'@primary-color': '#722ED1'
}
}
]
}
-9
View File
@@ -1,9 +0,0 @@
0.0.0.0:80 {
gzip
root /usr/share/nginx/html
rewrite {
r .*
to {path} /
}
}
-43
View File
@@ -1,43 +0,0 @@
server {
listen 80;
server_name localhost;
root /usr/share/nginx/html;
index index.html;
# Gzip compression
gzip on;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
gzip_min_length 1000;
# Static asset caching
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
# API proxy to backend
location /api/ {
proxy_pass http://backend:5000/api/;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_cache_bypass $http_upgrade;
proxy_read_timeout 300s;
proxy_connect_timeout 75s;
}
# SPA routing support
location / {
try_files $uri $uri/ /index.html;
}
# Health check
location /health {
return 200 'OK';
add_header Content-Type text/plain;
}
}
-24
View File
@@ -1,24 +0,0 @@
server {
listen 80;
server_name _;
# gzip config
gzip on;
gzip_min_length 1k;
gzip_comp_level 6;
gzip_types text/plain text/css text/javascript application/json application/javascript application/x-javascript application/xml;
gzip_vary on;
gzip_disable "MSIE [1-6]\.";
root /usr/share/nginx/html;
include /etc/nginx/mime.types;
location / {
try_files $uri $uri/ /index.html;
}
# location /api {
# proxy_pass https://preview.pro.antdv.com/api;
# proxy_set_header X-Forwarded-Proto $scheme;
# proxy_set_header X-Real-IP $remote_addr;
# }
}
-23
View File
@@ -1,23 +0,0 @@
module.exports = {
moduleFileExtensions: [
'js',
'jsx',
'json',
'vue'
],
transform: {
'^.+\\.vue$': 'vue-jest',
'.+\\.(css|styl|less|sass|scss|svg|png|jpg|ttf|woff|woff2)$': 'jest-transform-stub',
'^.+\\.jsx?$': 'babel-jest'
},
moduleNameMapper: {
'^@/(.*)$': '<rootDir>/src/$1'
},
snapshotSerializers: [
'jest-serializer-vue'
],
testMatch: [
'**/tests/unit/**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)'
],
testURL: 'http://localhost/'
}
-11
View File
@@ -1,11 +0,0 @@
{
"compilerOptions": {
"target": "es6",
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
},
"exclude": ["node_modules", "dist"],
"include": ["src/**/*"]
}
-104
View File
@@ -1,104 +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",
"prepare": "husky install"
},
"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",
"@commitlint/cli": "^12.1.4",
"@commitlint/config-conventional": "^12.1.4",
"@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",
"commitizen": "^4.2.4",
"cz-conventional-changelog": "^3.3.0",
"eslint": "^7.0.0",
"eslint-plugin-html": "^5.0.5",
"eslint-plugin-vue": "^5.2.3",
"file-loader": "^6.2.0",
"git-revision-webpack-plugin": "^3.0.6",
"husky": "^6.0.0",
"less": "^3.13.1",
"less-loader": "^5.0.0",
"lint-staged": "^12.5.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"
},
"config": {
"commitizen": {
"path": "./node_modules/cz-conventional-changelog"
}
},
"husky": {
"hooks": {
"commit-msg": "commitlint -E HUSKY_GIT_PARAMS"
}
},
"gitHooks": {
"pre-commit": "lint-staged"
},
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
}
-11874
View File
File diff suppressed because it is too large Load Diff
-5
View File
@@ -1,5 +0,0 @@
module.exports = {
plugins: {
autoprefixer: {}
}
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 131 KiB

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

Before

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

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

Before

Width:  |  Height:  |  Size: 8.7 KiB

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

Before

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 103 KiB

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

Before

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

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

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