text:
<repo_name>YMNNs/bi-front<file_sep>/src/components/about.vue
<template>

                关于视易

“在客户最需要我们的时候，视易为我们提供了最重要的技术解决方案。”

                安装信息

此处展示关于系统的详细信息。

                            版本:
                            {{ config.version }}

                            构建日期:
                            {{ buildDate }}

                                {{ item.name }}:

                                {{ item.version }}

</template>

<file_sep>/src/components/graph/plot.vue
<template>

</template>

<file_sep>/src/components/graph/scatter.vue
<template>

</template>

<file_sep>/tests/e2e/specs/03_data.js
import 'cypress-file-upload'

describe('上传数据：异常处理', () => {
    it('文件格式不匹配', function () {
        cy.login()
        cy.visit('/new_data')
        const path = '../files/co2.xxx'
        cy.get(`#${Cypress.config('dom_map').new_data.select_file}`).attachFile(path)
        cy.contains('您上传的文件存在问题')
    })

    it('文件存在空列名', function () {
        cy.login()
        cy.visit('/new_data')
        const path = '../files/co2_empty_col.csv'
        cy.get(`#${Cypress.config('dom_map').new_data.select_file}`).attachFile(path)
        cy.contains('您上传的文件存在问题')
    })

    it('一个或多个列名长度超过 64 字节', function () {
        cy.login()
        cy.visit('/new_data')
        const path = '../files/co2_long_col.csv'
        cy.get(`#${Cypress.config('dom_map').new_data.select_file}`).attachFile(path)
        cy.contains('您上传的文件存在问题')
    })

    it('一个或多个列名首尾存在空格', function () {
        cy.login()
        cy.visit('/new_data')
        const path = '../files/co2_space_col.csv'
        cy.get(`#${Cypress.config('dom_map').new_data.select_file}`).attachFile(path)
        cy.contains('您上传的文件存在问题')
    })

    it('文件存在重复列名', function () {
        cy.login()
        cy.visit('/new_data')
        const path = '../files/co2_duplicated_col.csv'
        cy.get(`#${Cypress.config('dom_map').new_data.select_file}`).attachFile(path)
        cy.contains('您上传的文件存在问题')
    })

    it('文件内容问题', function () {
        cy.login()
        cy.visit('/new_data')
        const path = '../files/co2_error.csv'
        cy.get(`#${Cypress.config('dom_map').new_data.select_file}`).attachFile(path)
        cy.contains('您上传的文件存在问题')
    })

    it('缺少必填项', function () {
        cy.login()
        cy.visit('/new_data')
        cy.get(`#${Cypress.config('dom_map').new_data.submit}`).click()
        cy.contains('请')
    })

    it('数据名称过长', function () {
        cy.login()
        cy.visit('/new_data')
        cy.get(`#${Cypress.config('dom_map').new_data.name}`)
            .clear()
            .type(Cypress.config('data_name_long'))
            .should('have.value', Cypress.config('data_name_long'))
        cy.contains('数据名称长度上限为 128 字符')
    })

    it('数据描述过长', function () {
        cy.login()
        cy.visit('/new_data')
        cy.get(`#${Cypress.config('dom_map').new_data.description}`)
            .clear()
            .type(Cypress.config('data_description_long'))
            .should('have.value', Cypress.config('data_description_long'))
        cy.contains('描述长度上限为 256 字符')
    })
})

describe('上传数据：正确路径', () => {
    it('上传数据', function () {
        cy.login()
        cy.visit('/new_data')
        const path = '../files/co2.csv'
        cy.get(`#${Cypress.config('dom_map').new_data.select_file}`).attachFile(path)
        cy.get(`#${Cypress.config('dom_map').new_data.description}`)
            .clear()
            .type(Cypress.config('data_description'))
            .should('have.value', Cypress.config('data_description'))
        cy.get(`#${Cypress.config('dom_map').new_data.name}`)
            .clear()
            .type(Cypress.config('data_name'))
            .should('have.value', Cypress.config('data_name'))
        cy.get(`#${Cypress.config('dom_map').new_data.keys_text}`)
            .click()
            .type('year\n')
            .blur()
        cy.get(`#${Cypress.config('dom_map').new_data.keys_number}`)
            .click()
            .type('Cement production\nGas flarinl\nGas fuel\nLiquid fuel\nSolid fuel\n')
            .blur()
        cy.get(`#${Cypress.config('dom_map').new_data.submit}`).click()
        cy.contains('成功')
    })
})

describe('修改表名：正确路径', () => {
    it('修改表名', function () {
        cy.login()
        cy.visit('/data_management')
        cy.url().then((el) => {
            const data_info = {}
            if (el.indexOf(Cypress.config('dev_hostname')) >= 0) {
                // 服务器
                data_info.data_name = Cypress.config('data_name')
            } else {
                data_info.data_name = Cypress.config('mock_data').tables[0].name
            }
            cy.contains(data_info.data_name).click()
            cy.get('.ant-typography-edit').click()
            cy.get('.ant-input').clear().type('xxx\n')
            cy.contains('成功')
            cy.get('.ant-typography-edit').click()
            cy.get('.ant-input')
                .clear()
                .type(data_info.data_name + '\n')
            cy.contains('成功')
        })
    })
})
<file_sep>/src/api/mock/view_dashboard.js
import Mock from 'mockjs'
import log from '@/util/logger'

Mock.mock(process.env.VUE_APP_API_BASE_URL + 'dashboard/view', 'post', (request) => {
    log.mock(request.url, JSON.parse(request.body))
    return {
        status: {
            code: 0,
            message: '仪表盘请求成功',
        },
        data: {
            instruments: [
                {
                    id: 453,
                    index: 0,
                    chart_id: 2,
                    data_id: 2,
                    selected_keys: '[]',
                },
                {
                    id: 455,
                    index: 1,
                    chart_id: 1,
                    data_id: 1,
                    selected_keys: '[]',
                },
            ],
        },
    }
})
<file_sep>/src/components/layout.vue
<template>

                <template v-for="item in menuList" :key="item.path">
                    <template v-if="!item.children">

                            <template #icon>
                                <icon-font :type="item.meta.icon" />
                            </template>
                            {{ item.meta.title }}

                    </template>
                    <template v-else>

                    </template>
                </template>

{{ systemTitle }}

                                            {{ $store.state.nickname ? $store.state.nickname : '未设置昵称' }}
                                            <DownOutlined />

                                        <template #overlay>

                                                    登录为
                                                    {{ $store.state.username }}

                                                    设置

                                                    退出登录

                                        </template>

                    <router-view />

©{{ new Date().getFullYear() }} {{ systemTitle }}

关于

</template>

<file_sep>/src/components/edit_chart.vue
<template>

        <template #title>编辑图表</template>
        <template #extra>
            重置
            保存图表
        </template>

            <template #editableTooltip>名称长度上限为 128 字符</template>

                        <template #title>
                            维度（ <template v-if="limit.text.min !== limit.text.max"
                                >{{ limit.text.min }}-{{ limit.text.max }}</template
                            >
                            <template v-else>{{ limit.text.min }}</template>
                            个）  已过滤
                            </template
                        >

                            <template #icon><FieldStringOutlined /></template>

                        <template #title>
                            指标（ <template v-if="limit.number.min !== limit.number.max"
                                >{{ limit.number.min }}-{{ limit.number.max }}</template
                            >
                            <template v-else>{{ limit.number.min }}</template>
                            个）  已过滤
                            </template
                        >

                            <template #icon><FieldNumberOutlined /></template>

                            <template #title>
                                图表类型  已过滤
                                </template
                            >

                                    <icon-font :type="item.icon_type" />

                                    {{ item.type_name }}

 ['keys_text', 'keys_number'].indexOf(i.key) >= 0).length > 0 &&
                            instrument_count > 0
                        "
                        style="margin-bottom: 16px"
                    >

                            <template #message>您更改了维度和指标</template>
                            <template #description>
                                保存后，与此图表关联的 {{
                                        instrument_count
                                    }} 个仪表盘条目将被自动删除。
                            </template>

                            <template #message>图表无法预览</template>
                            <template #description>

                                    在预览图表之前，请先修复下列问题：

                                            {{ item }}

                                            {{ item }}

                                    维度和指标的数量限制取决于图表类型。

                            </template>

                            <Graph_api
                                :data="raw_data"
                                :columns="columns.number"
                                :number_keys="checkedKeys_number"
                                :text_keys="checkedKeys_text"
                                :type_id="graph_options.type_id"
                                :key="timeStamp"
                                :x-field="graph_options.xField"
                                :y-field="graph_options.yField"
                                :series-field="graph_options.seriesField"
                            />

</template>

<file_sep>/src/api/post/login.js
import axios from '@/request'
import md5 from 'js-md5'
import { randomString } from '@/util'

export function login(login, password, rememberMe) {
    const salt = randomString()
    return axios.post('user/login', {
        login: login,
        password: md5(md5(password) + salt),
        rememberMe: rememberMe,
        salt: salt,
    })
}
<file_sep>/src/api/mock/view_chart.js
import Mock from 'mockjs'
import log from '@/util/logger'

Mock.mock(process.env.VUE_APP_API_BASE_URL + 'chart/view', 'post', (request) => {
    log.mock('chart/view', JSON.parse(request.body))
    const { id } = JSON.parse(request.body)
    switch (id) {
        case 1: {
            return {
                status: {
                    code: 0,
                    message: '请求图表成功',
                },
                data: {
                    chart: {
                        type_id: 1,
                        data_id: 1,
                        chart_name: '豆瓣',
                        last_modified_time: 1623916319,
                        keys_number: ['comment'],
                        keys_text: ['type'],
                        xField: null,
                        yField: null,
                        seriesField: null,
                    },
                },
            }
        }
        case 2: {
            return {
                status: {
                    code: 0,
                    message: '请求图表成功',
                },
                data: {
                    chart: {
                        type_id: 2,
                        data_id: 2,
                        chart_name: '58',
                        last_modified_time: 1623916319,
                        keys_number: ['total_price'],
                        keys_text: ['direction'],
                        xField: null,
                        yField: null,
                        seriesField: null,
                    },
                },
            }
        }
        default: {
            return {
                status: {
                    code: 1,
                    message: '图表id无效',
                },
                data: null,
            }
        }
    }
})
<file_sep>/src/api/post/request_reset_password.js
import axios from '@/request'

export function request_reset_password(email) {
    return axios.post('user/request_reset_password', { email: email })
}
<file_sep>/src/api/post/delete_account.js
import axios from '@/request'
import md5 from 'js-md5'
import { randomString } from '@/util'

export function delete_account(username, password) {
    const salt = randomString()
    return axios.post('user/delete', {
        username: username,
        password: md5(md5(password) + salt),
        salt: salt,
    })
}
<file_sep>/src/components/data_management.vue
<template>

            <template v-slot:extra>
                新建数据
            </template>

                        当前显示
                            {{ tables.length }}
                            个条目（共
                            {{ tables.length }} 个）

                            <template #renderItem="{ item }">

                                        <template #title>
                                            {{ item.name }}

                                        </template>

                                        <template #avatar>

                                                <template #icon>
                                                    <DatabaseTwoTone />
                                                </template>

                                        </template>

                                    <template #actions v-if="false">

                                                操作
                                                <DownOutlined />

                                            <template #overlay>

                                                        删除

                                            </template>

                                    </template>

                                        样例数据

                                        导入数据

                            </template>

</template>

<file_sep>/src/assets/about.md
### 视易

视易 BI 数据分析平台是一个数据可视化和智能分析的数据平台，平台结合当前环境的发展趋势和大环境下整体的需求，主要面对的用户是公司中的业务人员和产品经理等需要对大量数据进行评测和分析后做出工作方向上的决策的人。

#### 可指导行动的数据，驱动实时个性化

如今，仅仅拥有大量的数据是远远不够的。在无 Cookie 的数字世界里，若要满足不断变化的客户期望，企业需要从最相关的数据源中实时提取正确的数据。而且，还要能够在整个组织内广泛访问这些数据。

这就是视易应运而生的原因。具体方法如下。

#### 轻松分析展示整个客户旅程的关联数据

我们提供的分析和数据管理产品可帮助您分解数据，打破组织孤岛。通过将整个客户旅程中的数据拼成一个单一的视图，您可以根据单次互动（甚至是刚刚完成的互动）深入了解客户的行为。这些数据还可用于训练 AI/ML 模型，以针对归因、贡献分析、个性化和测试而优化模型。

您还可以轻松地与需要它们的任何团队或系统共享所有这些数据和分析，以便在整个组织内普及数据访问。

#### 针对每一位客户个性化每个接触点的每次体验

我们可提供由 AI/ML 提供支持的个性化、推荐和用户体验服务。这些都是通过客户档案丰富充实，其中客户档案将会根据来自任何渠道（包括 CRM、Web、移动，AR、聊天等）上的每一次互动实时更新，即使匿名也是如此。

这意味着可以基于相同的实时客户档案对每个渠道上的每项活动进行个性化设置，让您可以无比灵活、不受限制地解锁客户行为和洞察，而不仅限于单一渠道或数据源。

#### 适用于当今和未来的隐私和治理控制

我们可以帮助营销人员和 IT 团队简化其数据管理和激活工作，特别是在涉及隐私和治理等关键问题时。

我们的数据产品可以让您组合使用经过身份验证的数据和匿名数据，而不会对隐私造成影响。使用企业权限可以轻松确保正确使用敏感数据，即使在最矩阵型的组织中也是如此。
<file_sep>/src/api/mock/register.js
import Mock from 'mockjs'
import mock_data from '@/api/mock/mock_data'
import log from '@/util/logger'

Mock.mock(process.env.VUE_APP_API_BASE_URL + 'user/register', 'post', (request) => {
    log.mock(request.url, JSON.parse(request.body))
    const { email, username } = JSON.parse(request.body)
    if (email === mock_data.email) {
        return {
            status: {
                code: 11,
                message: '电子邮件地址已被占用',
            },
        }
    } else if (username === mock_data.username) {
        return {
            status: {
                code: 21,
                message: '用户名已被占用',
            },
        }
    } else {
        return {
            status: {
                code: 0,
                message: '注册成功',
            },
            data: {
                token: mock_data.token,
            },
        }
    }
})
<file_sep>/tests/e2e/specs/04_chart.js
describe('新建图表：错误处理', () => {
    it('图表名称过长', function () {
        cy.login()
        cy.visit('/new_chart')
        cy.get(`#${Cypress.config('dom_map').new_chart.name}`)
            .clear()
            .type(Cypress.config('chart_name_long'))
            .should('have.value', Cypress.config('chart_name_long'))
            .blur()
        cy.contains('图表名称长度上限为 128 字符')
    })
    it('缺少必填项', function () {
        cy.login()
        cy.visit('/new_chart')
        cy.get(`#${Cypress.config('dom_map').new_chart.submit}`).click()
        cy.contains('请')
    })
})

describe('新建图表：正确路径', () => {
    it('新建图表', function () {
        cy.login()
        cy.visit('/new_chart')
        cy.get(`#${Cypress.config('dom_map').new_chart.name}`)
            .clear()
            .type(Cypress.config('chart_name'))
            .should('have.value', Cypress.config('chart_name'))
            .blur()
        cy.url().then((el) => {
            const chart_info = {}
            if (el.indexOf(Cypress.config('dev_hostname')) >= 0) {
                // 服务器
                chart_info.data_name = Cypress.config('data_name')
            } else {
                chart_info.data_name = Cypress.config('mock_data').tables[0].name
            }
            cy.get(`#${Cypress.config('dom_map').new_chart.data}`).click()
            cy.contains(chart_info.data_name).click()
            cy.get(`#${Cypress.config('dom_map').new_chart.chart_type}`).click()
            cy.contains('折线图').click()
            cy.get(`#${Cypress.config('dom_map').new_chart.submit}`).click()
            cy.contains('已创建图表')
        })
    })
})

describe('查看图表', () => {
    it('搜索图表', function () {
        cy.login()
        cy.visit('/chart_management')
        cy.url().then((el) => {
            const chart_info = {}
            if (el.indexOf(Cypress.config('dev_hostname')) >= 0) {
                // 服务器
                chart_info.name = Cypress.config('chart_name')
            } else {
                chart_info.name = Cypress.config('mock_data').charts[0].chart_name
            }
            cy.get(`#${Cypress.config('dom_map').chart_management.search}`)
                .clear()
                .type(chart_info.name + '\n')
            cy.contains(chart_info.name)
            // cy.contains('已过滤')
            // // 清除过滤器
            // cy.get('.anticon-close').click()
            // cy.contains('已过滤').should('not.exist')
        })
    })
})

describe('编辑图表：正确路径', () => {
    it('编辑图表', function () {
        cy.login()
        cy.visit('/chart_management')
        cy.url().then((el) => {
            const chart_info = {}
            if (el.indexOf(Cypress.config('dev_hostname')) >= 0) {
                // 服务器
                chart_info.name = Cypress.config('chart_name')
            } else {
                chart_info.name = Cypress.config('mock_data').charts[0].chart_name
            }
            cy.contains(chart_info.name).click()
            cy.contains('编辑图表')
            if (el.indexOf(Cypress.config('dev_hostname')) >= 0) {
                // 服务器
            } else {
                // 取消勾选
                cy.get('.ant-tree-checkbox-checked').each((i) => {
                    cy.get(i).click()
                })
            }
            // 勾选第一个维度和第一个指标
            cy.get('.ant-tree').each((i) => {
                cy.get(i).children().first().children('.ant-tree-checkbox').click()
            })
            // 更改图表类型
            cy.get(`#${Cypress.config('dom_map').edit_chart.chart_type_prefix}2`).click()
            cy.contains('图表无法预览').should('not.exist')
            cy.get(`#${Cypress.config('dom_map').edit_chart.submit}`).click()
            // cy.contains('您确定要继续吗')
            // // 确认对话框
            // cy.get('.ant-modal-confirm-btns').children('.ant-btn-primary').click()
            cy.contains('图表已保存')
        })
    })
})
<file_sep>/.eslintrc.js
module.exports = {
    root: true,
    env: {
        node: true,
    },
    extends: ['plugin:vue/vue3-essential', 'eslint:recommended', '@vue/prettier'],
    parserOptions: {
        parser: 'babel-eslint',
    },
    rules: {
        indent: ['off', 4], // 设置缩进4字符
        quotes: [2, 'single'], //强制双引号
        semi: [2, 'never'], //语句强制分号结尾
        'vue/v-bind-style': ['error', 'shorthand'], //用":"代替"v-bind:"
        'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
        'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
        // 闭合html标签
        'vue/html-self-closing': [
            'error',
            {
                html: {
                    void: 'always',
                    normal: 'always',
                    component: 'always',
                },
                svg: 'always',
                math: 'always',
            },
        ],
        'vue/script-setup-uses-vars': 0,
        'no-var': 2,
        eqeqeq: 2,
    },
    overrides: [
        {
            files: ['**/__tests__/*.{j,t}s?(x)', '**/tests/unit/**/*.spec.{j,t}s?(x)'],
            env: {
                jest: true,
            },
        },
    ],
}
<file_sep>/src/api/post/validate_email.js
import axios from '@/request'

export function validate_email(email) {
    return axios.post('user/validate_email', { email: email })
}
<file_sep>/src/components/register.vue
<template>

            <template v-if="step === 'register'">

                         {
                                    logger.warn(_error)
                                })
                            "
                        />

                         {
                                    logger.warn(_error)
                                })
                            "
                        />

                         {
                                    logger.warn(_error)
                                })
                            "
                        />

                         {
                                    logger.warn(_error)
                                })
                            "
                        />

                        注册
                        重置
                        <template #help
                            >

                                选择注册来完成注册，选择重置来清空表单以供重新填写。

</template
                        >

            </template>

            <template v-if="step === 'waiting'">

            </template>

            <template v-if="step === 'error'">

                    <template #subTitle>{{ error }}</template>
                    <template #extra>
                        返回
                    </template>

            </template>

            <template v-if="step === 'done'">

                    <template #extra>
                        前往控制台
                    </template>

            </template>

</template>

<file_sep>/src/api/post/all_charts.js
import axios from '@/request'

export function all_charts() {
    return axios.post('chart/all', {}) //data没有也不能不写
}
<file_sep>/src/api/post/activate_email.js
import axios from '@/request'

export function activate_email(activation_token) {
    return axios.post('user/activate_email', {
        activation_token: activation_token,
    })
}
<file_sep>/tests/e2e/support/commands.js
// ***********************************************
// This example commands.js shows you how to
// create various custom commands and overwrite
// existing commands.
//
// For more comprehensive examples of custom
// commands please read more here:
// https://on.cypress.io/custom-commands
// ***********************************************
//
//
// -- This is a parent command --
// Cypress.Commands.add("login", (email, password) => { ... })
//
//
// -- This is a child command --
// Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... })
//
//
// -- This is a dual command --
// Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... })
//
//
// -- This is will overwrite an existing command --
// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... })

/**
 * 登录操作
 * 自动判断环境
 */
Cypress.Commands.add('login', function (password) {
    cy.visit('/login')
    cy.url().then((el) => {
        const login_info = {}
        if (el.indexOf(Cypress.config('dev_hostname')) >= 0) {
            // 服务器
            login_info.username = Cypress.config('valid_username')
            login_info.password = <PASSWORD>('valid_password')
            login_info.nickname = Cypress.config('nickname')
        } else {
            // 本地 mock
            login_info.username = Cypress.config('mock_data').username
            login_info.password = <PASSWORD>('mock_<PASSWORD>').password
            login_info.nickname = Cypress.config('mock_data').nickname
        }
        if (password) {
            login_info.password = <PASSWORD>
        }
        cy.get(`#${Cypress.config('dom_map').login.username}`)
            .clear()
            .type(login_info.username)
            .should('have.value', login_info.username)
        cy.get(`#${Cypress.config('dom_map').login.password}`)
            .clear()
            .type(login_info.password)
            .should('have.value', login_info.password)
        cy.get(`#${Cypress.config('dom_map').login.login}`).click()
        cy.contains(login_info.nickname)
    })
})
<file_sep>/src/components/new_data.vue
<template>

                    新建数据

                        数据会经过测量、收集、报告和分析，因此它可以使用图表、图像或者其他分析工具进行可视化。数据作为一种关于一些已存在的信息或知识的一般概念，会使用一些可以更好地利用或处理的形式进行呈现或编码。

                     onTabChange(key)"
                    >

                                    <template #label>上传文件</template>
                                    <a-upload-dragger
                                        :id="dom_map.new_data.select_file"
                                        v-model:fileList="modelRef_upload.fileList"
                                        name="file"
                                        :multiple="false"
                                        :accept="'.csv'"
                                        :customRequest="handleUpload"
                                        :beforeUpload="beforeUpload"
                                        @change="handleUploadChange"
                                    >

                                            <inbox-outlined />

按这里或拖动文件来上传

                                            大小限制：{{ limit.size / (1024 * 1024) }} MB，支持格式：*.{{
                                                limit.postfix
                                            }}

                                    <template #label>数据名称</template>
                                    <template #help>

默认为上传的文件名，您也可以在此处修改。

                                    </template>
                                     {
                                                logger.warn(_error)
                                            })
                                        "
                                        placeholder="我的新数据"
                                        v-model:value="modelRef_upload.data_name"
                                    />

                                    <template #label>描述</template>
                                    <template #help>

                                            描述将会显示在 数据管理 页面的列表中

                                    </template>
                                     {
                                                logger.warn(_error)
                                            })
                                        "
                                        placeholder="此数据为..."
                                        v-model:value="modelRef_upload.description"
                                        auto-size
                                    />

                                    <template #label>维度</template>
                                    <template #help>

维度一般指自变量。

                                    </template>
                                    <a-select
                                        :id="dom_map.new_data.keys_text"
                                        mode="multiple"
                                        v-model:value="modelRef_upload.keys_text"
                                        style="width: 100%"
                                        placeholder="选择维度列"
                                        @change="setOptions"
                                        :options="keys_text_options"
                                    />

                                    <template #label>指标</template>
                                    <template #help>

指标一般指因变量。

                                    </template>
                                    <a-select
                                        :id="dom_map.new_data.keys_number"
                                        mode="multiple"
                                        v-model:value="modelRef_upload.keys_number"
                                        style="width: 100%"
                                        placeholder="选择指标列"
                                        @change="setOptions"
                                        :options="keys_number_options"
                                    />

                                    完成
                                    取消
                                    <template #help
                                        >

                                            选择完成开始编辑图表，选择取消返回上一页面。

</template
                                    >

</template>

<file_sep>/tests/e2e/specs/06_profile.js
describe('修改用户信息：错误处理', () => {
    it('用户名被占用', function () {
        cy.login()
        cy.visit('/edit_user_profile')
        cy.get(`#${Cypress.config('dom_map').profile.username}`)
            .clear()
            .type(Cypress.config('mock_data').duplicated_username)
            .should('have.value', Cypress.config('mock_data').duplicated_username)
            .blur()
        cy.contains('用户名已被占用')
    })

    it('用户名过长', function () {
        cy.login()
        cy.visit('/edit_user_profile')
        cy.get(`#${Cypress.config('dom_map').profile.username}`)
            .clear()
            .type(Cypress.config('username_long'))
            .should('have.value', Cypress.config('username_long'))
            .blur()
        cy.contains('用户名长度上限为 16 字符')
    })

    it('电子邮件地址被占用', function () {
        cy.login()
        cy.visit('/edit_user_profile')
        cy.get(`#${Cypress.config('dom_map').profile.email}`)
            .clear()
            .type(Cypress.config('mock_data').duplicated_email)
            .should('have.value', Cypress.config('mock_data').duplicated_email)
            .blur()
        cy.contains('电子邮件地址已被占用')
    })

    it('电子邮件格式不正确', function () {
        cy.login()
        cy.visit('/edit_user_profile')
        cy.get(`#${Cypress.config('dom_map').profile.email}`)
            .clear()
            .type(Cypress.config('invalid_email'))
            .should('have.value', Cypress.config('invalid_email'))
            .blur()
        cy.contains('请输入有效的电子邮件地址')
    })

    it('昵称过长', function () {
        cy.login()
        cy.visit('/edit_user_profile')
        cy.get(`#${Cypress.config('dom_map').profile.nickname}`)
            .clear()
            .type(Cypress.config('nickname_long'))
            .should('have.value', Cypress.config('nickname_long'))
            .blur()
        cy.contains('昵称长度上限为 16 字符')
    })

    it('缺少必填信息', function () {
        cy.login()
        cy.visit('/edit_user_profile')
        cy.get(`#${Cypress.config('dom_map').profile.username}`).clear()
        cy.get(`#${Cypress.config('dom_map').profile.email}`).clear()
        cy.get(`#${Cypress.config('dom_map').profile.nickname}`).clear()
        cy.get(`#${Cypress.config('dom_map').profile.update_profile_button}`).click()
        cy.contains('请输入')
    })
})

describe('修改用户信息：正确路径', () => {
    it('修改用户信息', function () {
        cy.login()
        cy.visit('/edit_user_profile')
        cy.get(`#${Cypress.config('dom_map').profile.username}`)
            .clear()
            .type(Cypress.config('valid_username_new'))
            .should('have.value', Cypress.config('valid_username_new'))
            .blur()
        cy.get(`#${Cypress.config('dom_map').profile.email}`)
            .clear()
            .type(Cypress.config('email_new'))
            .should('have.value', Cypress.config('email_new'))
            .blur()
        cy.get(`#${Cypress.config('dom_map').profile.nickname}`)
            .clear()
            .type(Cypress.config('nickname_new'))
            .should('have.value', Cypress.config('nickname_new'))
            .blur()
        cy.get(`#${Cypress.config('dom_map').profile.update_profile_button}`).click()
        cy.contains('您的个人信息现已更新。')
        // 再改回去
        cy.get(`#${Cypress.config('dom_map').profile.username}`)
            .clear()
            .type(Cypress.config('valid_username'))
            .should('have.value', Cypress.config('valid_username'))
            .blur()
        cy.get(`#${Cypress.config('dom_map').profile.email}`)
            .clear()
            .type(Cypress.config('email'))
            .should('have.value', Cypress.config('email'))
            .blur()
        cy.get(`#${Cypress.config('dom_map').profile.nickname}`)
            .clear()
            .type(Cypress.config('nickname'))
            .should('have.value', Cypress.config('nickname'))
            .blur()
        cy.get(`#${Cypress.config('dom_map').profile.update_profile_button}`).click()
        cy.contains('您的个人信息现已更新。')
    })
})

describe('修改密码：错误处理', () => {
    it('原密码格式不正确', function () {
        cy.login()
        cy.visit('/edit_user_profile')
        cy.get(`#${Cypress.config('dom_map').profile.current_password}`)
            .clear()
            .type(Cypress.config('password_short'))
            .should('have.value', Cypress.config('password_short'))
            .blur()
        cy.contains('密码长度必须在 8-16 字符之间')
    })

    it('新密码格式不正确', function () {
        cy.login()
        cy.visit('/edit_user_profile')
        cy.get(`#${Cypress.config('dom_map').profile.new_password}`)
            .clear()
            .type(Cypress.config('password_short'))
            .should('have.value', Cypress.config('password_short'))
            .blur()
        cy.contains('密码长度必须在 8-16 字符之间')
    })

    it('旧密码不匹配', function () {
        cy.login()
        cy.visit('/edit_user_profile')
        cy.get(`#${Cypress.config('dom_map').profile.current_password}`)
            .clear()
            .type(Cypress.config('invalid_password'))
            .should('have.value', Cypress.config('invalid_password'))
            .blur()
        cy.get(`#${Cypress.config('dom_map').profile.new_password}`)
            .clear()
            .type(Cypress.config('valid_password'))
            .should('have.value', Cypress.config('valid_password'))
            .blur()
        cy.get(`#${Cypress.config('dom_map').profile.update_password_button}`).click()
        cy.contains('您的修改密码请求出现错误')
    })
})

describe('修改密码：正确路径', () => {
    it('修改密码', function () {
        cy.login()
        cy.visit('/edit_user_profile')
        cy.url().then((el) => {
            const password_info = {}
            if (el.indexOf(Cypress.config('dev_hostname')) >= 0) {
                // 服务器
                password_info.current_password = <PASSWORD>('valid_<PASSWORD>')
            } else {
                password_info.current_password = Cypress.config('mock_data').password
            }
            cy.get(`#${Cypress.config('dom_map').profile.current_password}`)
                .clear()
                .type(password_info.current_password)
                .should('have.value', password_info.current_password)
                .blur()
            cy.get(`#${Cypress.config('dom_map').profile.new_password}`)
                .clear()
                .type(Cypress.config('valid_password'))
                .should('have.value', Cypress.config('valid_password'))
                .blur()
            cy.get(`#${Cypress.config('dom_map').profile.update_password_button}`).click()
            cy.contains('您的个人密码现已更新，请使用新密码重新登录。')
        })
    })
})

describe('删除账户：错误处理', () => {
    it('用户名不匹配', function () {
        cy.login()
        cy.visit('/edit_user_profile')
        cy.get(`#${Cypress.config('dom_map').profile.delete_account_button}`).click()
        cy.url().then((el) => {
            const delete_info = {}
            if (el.indexOf(Cypress.config('dev_hostname')) >= 0) {
                // 服务器
                delete_info.username = Cypress.config('valid_username')
                delete_info.password = <PASSWORD>('valid_<PASSWORD>')
            } else {
                // 本地 mock
                delete_info.username = Cypress.config('mock_data').username
                delete_info.password = <PASSWORD>.config('mock_data').password
            }
            cy.get(`#${Cypress.config('dom_map').profile.delete_username}`)
                .clear()
                .type(Cypress.config('invalid_username'))
                .blur()
            cy.get(`#${Cypress.config('dom_map').profile.delete_confirm}`)
                .clear()
                .type('删除我的账户')
                .blur()
            cy.get(`#${Cypress.config('dom_map').profile.delete_password}`)
                .clear()
                .type(delete_info.password)
                .blur()
            cy.get(`#${Cypress.config('dom_map').profile.confirm_delete_button}`).click()
            cy.contains('您的删除账户请求出现错误')
        })
    })

    it('密码不匹配', function () {
        cy.login()
        cy.visit('/edit_user_profile')
        cy.get(`#${Cypress.config('dom_map').profile.delete_account_button}`).click()
        cy.url().then((el) => {
            const delete_info = {}
            if (el.indexOf(Cypress.config('dev_hostname')) >= 0) {
                // 服务器
                delete_info.username = Cypress.config('valid_username')
                delete_info.password = <PASSWORD>')
            } else {
                // 本地 mock
                delete_info.username = Cypress.config('mock_data').username
                delete_info.password = Cypress.config('mock_data').password
            }
            cy.get(`#${Cypress.config('dom_map').profile.delete_username}`)
                .clear()
                .type(delete_info.username)
                .blur()
            cy.get(`#${Cypress.config('dom_map').profile.delete_confirm}`)
                .clear()
                .type('删除我的账户')
                .blur()
            cy.get(`#${Cypress.config('dom_map').profile.delete_password}`)
                .clear()
                .type(Cypress.config('invalid_password'))
                .blur()
            cy.get(`#${Cypress.config('dom_map').profile.confirm_delete_button}`).click()
            cy.contains('您的删除账户请求出现错误')
        })
    })
})

describe('删除账户：正确路径', () => {
    it('删除账户', function () {
        cy.login()
        cy.visit('/edit_user_profile')
        cy.get(`#${Cypress.config('dom_map').profile.delete_account_button}`).click()
        cy.url().then((el) => {
            const delete_info = {}
            if (el.indexOf(Cypress.config('dev_hostname')) >= 0) {
                // 服务器
                delete_info.username = Cypress.config('valid_username')
                delete_info.password = <PASSWORD>('valid_<PASSWORD>')
            } else {
                // 本地 mock
                delete_info.username = Cypress.config('mock_data').username
                delete_info.password = <PASSWORD>('mock_data').<PASSWORD>
            }
            cy.get(`#${Cypress.config('dom_map').profile.delete_username}`)
                .clear()
                .type(delete_info.username)
                .blur()
            cy.get(`#${Cypress.config('dom_map').profile.delete_confirm}`)
                .clear()
                .type('删除我的账户')
                .blur()
            cy.get(`#${Cypress.config('dom_map').profile.delete_password}`)
                .clear()
                .type(delete_info.password)
                .blur()
            cy.get(`#${Cypress.config('dom_map').profile.confirm_delete_button}`).click()
            cy.contains('已删除，正在前往登录页面')
        })
    })
})
<file_sep>/src/api/mock/create_instrument.js
import Mock from 'mockjs'
import mock_data from '@/api/mock/mock_data'
import log from '@/util/logger'

Mock.mock(process.env.VUE_APP_API_BASE_URL + 'dashboard/add', 'post', (request) => {
    log.mock(request.url, JSON.parse(request.body))
    const { chart_id } = JSON.parse(request.body)
    if (!mock_data.charts.find((i) => i.id === chart_id)) {
        return {
            status: {
                code: 1,
                message: '图表id不存在',
            },
            data: null,
        }
    } else {
        return {
            status: {
                code: 0,
                message: '仪表添加成功',
            },
            data: {
                id: 123,
            },
        }
    }
})
<file_sep>/src/api/post/create_data.js
import axios from '@/request'

export function create_data(columns) {
    return axios.post('data/create', {
        columns: columns,
    })
}
<file_sep>/src/components/chart_management.vue
<template>

            <template v-slot:extra>
                新建图表
            </template>

                        当前显示
                            {{ chartsDisplay.length }}
                            个条目（共
                            {{ charts.length }} 个）
                        已过滤

                        名称
                        最近更新

                    <template #renderItem="{ item }">

                                <template class="ant-card-actions" #actions>
                                    <edit-outlined key="edit" @click="handleEdit(item.id)" />
                                    <a-popconfirm
                                        :title="`此图表与 ${item.relatedDashboardItem} 个仪表盘条目关联。删除此图表后，关联的仪表盘条目将被一并删除且不可恢复。`"
                                        @confirm="handleDelete(item.id)"
                                        v-if="item.relatedDashboardItem > 0"
                                    >
                                        <DeleteOutlined key="delete" />

                                        <DeleteOutlined key="delete" />

                                </template>
                                <template #cover>

                                        <icon-font :type="item.icon_type" />

                                </template>

                                    <template #description>{{ item.type_name }} </template>

                    </template>

</template>

<file_sep>/src/components/dashboard.vue
<template>

        添加仪表
        编辑仪表

您可以将图表附加在仪表盘中以便随时查看，并为仪表指定专属的数据子集。

正在编辑仪表“{{ modelRef.editing_instrument.chart.chart_name }}”

                <template #label>
                    图表
                </template>
                <template #help>

                        您可以在图表管理中 查看 您的图表。

                </template>

            <template v-if="purpose === 'edit'">

                    <template #label>
                          筛选维度：{{ modelRef.col_name }}
                    </template>

            </template>

                <Graph_api
                    v-if="modelRef.chart_id && preview_chart.ready === 'true'"
                    :key="uniqueId()"
                    :type_id="preview_chart.type_id"
                    :series-field="preview_chart.seriesField"
                    :x-field="preview_chart.xField"
                    :y-field="preview_chart.yField"
                    :text_keys="preview_chart.text_keys"
                    :number_keys="preview_chart.number_keys"
                    :data="preview_chart.data_display"
                    :columns="preview_chart.columns"
                />
                <template #help>

预览将要<template v-if="purpose === 'create'">添加</template
                        ><template v-if="purpose === 'edit'">编辑</template>的仪表。

正在载入预览...

                        此图表不能用于仪表盘。因为其信息不完整。请在
                            此处 编辑该图表以解决问题。

选择图表后将显示预览。

                </template>

            取消
            完成
            完成

        <template v-slot:extra>
            编辑

                <template #icon>
                    <PlusOutlined />
                </template>
                添加

            重置
            完成
            取消
        </template>

    <template v-if="incomplete > 0 && !edit">

            <template #message> 此页面有 {{ incomplete }} 个图表存在问题。 </template>
            <template #description>

                    这些图表缺少必要的信息，请前往
                    图表管理
                    中修复相关错误。

            </template>

    </template>

                    当前显示
                        {{ instruments_display.length }}
                        个条目

            <template #renderItem="{ item }">

                        <template #title
                            >
                                <WarningTwoTone twoToneColor="#ff4d4f" style="margin-right: 4px" /> {{ item.chart.chart_name }}</template
                        >
                        <Graph_api
                            :key="item.data_id"
                            :data="dataSources.find((i) => i.id === item.data_id).dataSource"
                            :columns="
                                dataSources
                                    .find((i) => i.id === item.data_id)
                                    .columns.filter((j) => j.type === 'number')
                            "
                            :number_keys="item.chart.keys_number"
                            :text_keys="item.chart.keys_text"
                            :type_id="item.chart.type_id"
                            :x-field="item.chart.xField"
                            :y-field="item.chart.yField"
                            :series-field="item.chart.seriesField"
                        />
                        <template class="ant-card-actions" #actions v-if="edit">
                            <LeftOutlined
                                :id="dom_map.dashboard.left"
                                key="left"
                                v-if="instruments_display.indexOf(item) !== 0"
                                @click="moveLeft(item.index)"
                            />
                            <EditOutlined
                                :id="dom_map.dashboard.edit_instrument"
                                key="edit"
                                v-if="!item.chart.incomplete"
                                @click="showDrawer(item.id)"
                            />
                            <template #title>
                                    您可以通过页面上方的
                                    重置 按钮撤销此操作。
                                </template>
                                <DeleteOutlined :id="dom_map.dashboard.delete" key="delete" />

                            <RightOutlined
                                :id="dom_map.dashboard.right"
                                key="right"
                                v-if="instruments_display.indexOf(item) !== instruments_display.length - 1"
                                @click="moveRight(item.index)"
                            />
                        </template>

            </template>

</template>

<file_sep>/src/components/widget/ConnectionStatus.vue
<template>

            <DisconnectOutlined />

</template>

<file_sep>/src/api/post/user_info.js
import axios from '@/request'

export function user_info() {
    return axios.post('user/info', {}) //data没有也不能不写
}
<file_sep>/src/constant/chart_types.js
import log from '@/util/logger'
export const chart_types = [
    {
        type_id: 1,
        type_name: '折线图',
        description: '使用一条折线的线段显示数据在一个具有顺序性的维度上的变化',
        icon_type: 'icon-jibenzhexiantu',
        component: () => import('@/components/graph/plot'),
        yField: 'value',
        seriesField: 'category',
        limit: {
            number: {
                max: 5,
                min: 1,
            },
            text: {
                max: 1,
                min: 1,
            },
        },
    },
    {
        type_id: 2,
        type_name: '柱状图',
        description: '使用柱形显示维度的数值。横轴显示分类维度，纵轴显示相应的值',
        icon_type: 'icon-duoxilieezhuzhuangtu',
        component: () => import('@/components/graph/column'),
        yField: 'value',
        seriesField: 'category',
        limit: {
            number: {
                max: 5,
                min: 1,
            },
            text: {
                max: 1,
                min: 1,
            },
        },
    },
    {
        type_id: 3,
        type_name: '条形图',
        description: '使用柱形显示维度的数值。纵轴显示分类维度，横轴显示相应的值',
        icon_type: 'icon-duidietiaoxingtu',
        component: () => import('@/components/graph/bar'),
        yField: 'value',
        seriesField: 'category',
        limit: {
            number: {
                max: 5,
                min: 1,
            },
            text: {
                max: 1,
                min: 1,
            },
        },
    },
    {
        type_id: 4,
        type_name: '面积图',
        description: '使用带填充区域的线段显示数据在一个具有顺序性的维度上的变化',
        icon_type: 'icon-duidiemianjitu',
        component: () => import('@/components/graph/area'),
        yField: 'value',
        seriesField: 'category',
        limit: {
            number: {
                max: 5,
                min: 1,
            },
            text: {
                max: 1,
                min: 1,
            },
        },
    },
    {
        type_id: 5,
        type_name: '饼图',
        description: '通过扇形区块的颜色和弧长（角度、面积）来展现数据的分类和占比情况',
        icon_type: 'icon-bingtu',
        component: () => import('@/components/graph/pie'),
        yField: 'value',
        seriesField: 'category',
        limit: {
            number: {
                max: 1,
                min: 1,
            },
            text: {
                max: 1,
                min: 1,
            },
        },
    },
    {
        type_id: 6,
        type_name: '散点图',
        description: '将所有的数据以不同颜色的点的形式展现在平面直角坐标系上',
        icon_type: 'icon-sandiantu',
        component: () => import('@/components/graph/scatter'),
        yField: 'value',
        seriesField: 'category',
        limit: {
            number: {
                max: 2,
                min: 2,
            },
            text: {
                max: 1,
                min: 1,
            },
        },
    },
    {
        type_id: 7,
        type_name: '雷达图',
        description: '将不同系列的多个维度的数据量映射到坐标轴上',
        icon_type: 'icon-leidatu',
        component: () => import('@/components/graph/radar'),
        yField: 'value',
        seriesField: 'category',
        limit: {
            number: {
                max: 5,
                min: 1,
            },
            text: {
                max: 1,
                min: 1,
            },
        },
    },
]

export const prepare_data = (data, columns, number_keys, text_keys) => {
    const label = `计算图表数据（${data.length} 行，${number_keys.length + text_keys.length} 列）`
    log.time(label)
    const _result = []
    const columns_number = columns.filter((i) => i.type === 'number')
    const all_category_value = new Set()
    const all_keys_text_value = new Set()
    data.forEach((i) => {
        number_keys.forEach((number) => {
            const item = {}
            item['value'] = i[number]
            item['category'] = columns_number.find((_i) => _i.key === number).title
            all_category_value.add(item['category']) // 添加到分类集合
            item[text_keys[0]] = i[text_keys[0]] // 只有一个维度列，简化
            all_keys_text_value.add(item[text_keys[0]]) // 添加到维度集合
            _result.push(item)
        })
    })
    const result = []
    // 先遍历分类
    all_category_value.forEach((i) => {
        all_keys_text_value.forEach((j) => {
            const _line = _result.filter((_res) => _res['category'] === i && _res[text_keys[0]] === j)
            const line = {}
            line[text_keys[0]] = j
            line['category'] = i
            // 对指标求和
            line['value'] = _line.reduce((previousValue, currentValue) => {
                return previousValue + currentValue['value']
            }, 0)
            result.push(line)
        })
    })
    log.timeEnd(label)
    return result
}
<file_sep>/src/api/mock/delete_account.js
import Mock from 'mockjs'
import md5 from 'js-md5'
import mock_data from '@/api/mock/mock_data'
import log from '@/util/logger'

Mock.mock(process.env.VUE_APP_API_BASE_URL + 'user/delete', 'post', (request) => {
    log.mock(request.url, JSON.parse(request.body))
    const { username, password, salt } = JSON.parse(request.body)
    if (username === mock_data.username && password === md5(md5(mock_data.password) + salt)) {
        //验证成功
        return {
            status: {
                code: 0,
                message: '用户已注销',
            },
            data: null,
        }
    } else {
        return {
            status: {
                code: 1,
                message: '用户名或密码无效',
            },
            data: null,
        }
    }
})
<file_sep>/src/constant/dom_map.js
/**
 * 控件id表，列举了所有页面的可交互元素id
 * 引用于页面和测试用例
 * 测试中用到，不支持ES6
 */
module.exports = {
    register: {
        email: 'email',
        username: 'username',
        password: '<PASSWORD>',
        nickname: 'nickname',
        register: 'register',
        reset: 'reset',
    },
    login: {
        username: 'username',
        password: '<PASSWORD>',
        rememberMe: 'rememberMe',
        login: 'login',
    },
    new_data: {
        select_file: 'select_file',
        name: 'name',
        description: 'description',
        keys_text: 'keys_text',
        keys_number: 'keys_number',
        submit: 'submit',
    },
    chart_management: {
        search: 'search',
        sort: 'sort',
    },
    new_chart: {
        name: 'name',
        data: 'data',
        chart_type: 'chart_type',
        submit: 'submit',
    },
    edit_chart: {
        submit: 'submit',
        reset: 'reset',
        chart_type_prefix: 'chart_type_',
    },
    dashboard: {
        size: 'size',
        edit: 'edit',
        add: 'add',
        cancel_edit_dashboard: 'cancel_edit_dashboard',
        cancel_add_edit_chart: 'cancel_add_edit_chart',
        reset: 'reset',
        finish_edit_dashboard: 'finish_edit_dashboard',
        finish_add: 'finish_add',
        finish_edit_chart: 'finish_edit_chart',
        select_chart: 'select_chart',
        left: 'left',
        right: 'right',
        delete: 'delete',
        edit_instrument: 'edit_instrument',
        filter_checkbox: 'filter_checkbox',
        select_keys: 'select_keys',
    },
    profile: {
        username: 'username',
        email: 'email',
        resend_link: 'resend_link',
        nickname: 'nickname',
        update_profile_button: 'update_profile_button',
        current_password: '<PASSWORD>',
        new_password: '<PASSWORD>',
        update_password_button: 'update_password_button',
        delete_account_button: 'delete_account_button',
        delete_username: 'delete_username',
        delete_confirm: 'delete_confirm',
        delete_password: '<PASSWORD>',
        confirm_delete_button: 'confirm_delete_button',
    },
    reset_password: {
        email: 'email',
        request_button: 'request_button',
        new_password: '<PASSWORD>',
        reset_button: 'reset_button',
    },
    layout: {
        dropdown: 'dropdown',
        settings: 'settings',
        logout: 'logout',
    },
}
<file_sep>/src/components/login.vue
<template>

                    您必须输入用户名和密码以登录
{{ text.appTitle }}

                        <template #prefix>
                            <user-outlined type="user" />
                        </template>

                        <template #prefix>
                            <lock-outlined />
                        </template>

                    记住我<template v-if="modelRef.rememberMe">（请勿在公共电脑上选中此选项）</template>

                    登录

                注册
                忘记密码？

</template>

<file_sep>/src/api/post/resend_activation_email.js
import axios from '@/request'

export function resend_activation_email() {
    return axios.post('user/resend_activation_email', {}) //data没有也不能不写
}
<file_sep>/src/api/post/table_content.js
import axios from '@/request'

export function table_content(table_id, max_length, offset) {
    return axios.post('data/table_content', {
        table_id: table_id,
        max_length: max_length,
        offset: offset,
    })
}
<file_sep>/src/api/post/view_dashboard.js
import axios from '@/request'

export function view_dashboard() {
    return axios.post('dashboard/view', {})
}
<file_sep>/src/components/data_display.vue
<template>

                <template #editableTooltip>名称长度上限为 128 字符</template>

                        当前显示
                            {{ dataSource.length }}
                            个条目（共
                            {{ pagination.total }} 个）

                    <template #name="{ text, record }">

                                <check-outlined class="editable-cell-icon-check" @click="save(record.key)" />

                                {{ text || ' ' }}
                                <edit-outlined class="editable-cell-icon" @click="edit(record.key)" />

                    </template>

                    <template #operation="{ record }">

                            删除

                    </template>

</template>

<file_sep>/src/api/mock/mock_data.js
/**
 * 测试中用到，不支持ES6
 * @type {{charts: [{chart_name: string, last_modified_time: number, type_id: number, id: number}, {chart_name: string, last_modified_time: number, type_id: number, id: number}], email_verified: string, duplicated_username: string, reset_password_token: string, duplicated_email: string, token: string, activation_token: string, password: string, tables: [{editable: boolean, name: string, description: string, id: number}, {editable: boolean, name: string, description: string, id: number}], user_id: number, nickname: string, dashboard_size: number, email: string, username: string}}
 */
module.exports = {
    username: 'bilyn',
    duplicated_username: 'bbilyn',
    password: '<PASSWORD>',
    email: '<EMAIL>',
    duplicated_email: '<EMAIL>',
    nickname: '本地用户',
    user_id: 23445,
    token: '<PASSWORD>',
    reset_password_token: '<PASSWORD>',
    email_verified: '0',
    activation_token: '<PASSWORD>',
    tables: [
        {
            id: 1,
            name: '豆瓣电影数据',
            editable: false,
            description: '按照豆瓣评分排序展示电影的基本信息',
        },
        {
            id: 2,
            name: '58租房数据',
            editable: true,
            description: '展示58同城租房网站的户型等基本信息',
        },
    ],
    charts: [
        {
            id: 1,
            chart_name: '豆瓣',
            type_id: 1,
            last_modified_time: 1623748361,
        },
        {
            id: 2,
            chart_name: '58',
            type_id: 2,
            last_modified_time: 1623748364,
        },
    ],
    dashboard_size: 2,
}
<file_sep>/src/api/mock/edit_user_profile.js
import Mock from 'mockjs'
import mock_data from '@/api/mock/mock_data'
import log from '@/util/logger'

Mock.mock(process.env.VUE_APP_API_BASE_URL + 'user/modify', 'post', (request) => {
    log.mock(request.url, JSON.parse(request.body))
    const user = JSON.parse(request.body)
    const errors = []
    if (user.nickname) {
        if (user.nickname.length < 3 || user.nickname.length > 16) {
            errors.push({
                key: 'nickname',
                value: '昵称格式不正确',
            })
        }
    }
    if (user.username) {
        if (user.username === mock_data.username) {
            errors.push({
                key: 'username',
                value: '用户名已被占用',
            })
        } else if (user.username.length < 5 || user.username.length > 16) {
            errors.push({
                key: 'username',
                value: '用户名格式不正确',
            })
        }
    }
    if (user.email) {
        if (user.email === mock_data.email) {
            errors.push({
                key: 'email',
                value: '电子邮件地址已被占用',
            })
        } else if (!user.email.match('^\\w+([-+.]\\w+)*@\\w+([-.]\\w+)*\\.\\w+([-.]\\w+)*$')) {
            errors.push({
                key: 'email',
                value: '电子邮件地址格式不正确',
            })
        }
    }
    const code = errors.length
    return {
        status: {
            code: code,
            message: '有' + code + '个字段修改失败',
        },
        data: {
            errors: errors,
        },
    }
})
<file_sep>/src/api/post/change_password.js
import axios from '@/request'

export function change_password(old_password, new_password) {
    return axios.post('user/change_password', {
        old_password: <PASSWORD>,
        new_password: <PASSWORD>,
    })
}
<file_sep>/src/api/mock/create_data.js
import Mock from 'mockjs'
import log from '@/util/logger'

Mock.mock(process.env.VUE_APP_API_BASE_URL + 'data/create', 'post', (request) => {
    log.mock(request.url, JSON.parse(request.body))
    return {
        status: {
            code: 0,
            message: '上传成功',
        },
        data: {
            id: 1,
        },
    }
})
<file_sep>/src/components/graph/radar.vue
<template>

</template>

<file_sep>/src/api/post/get_data_keys.js
import axios from '@/request'

export function get_data_keys(id, keyText) {
    return axios.post('data/keys', {
        id: id,
        keyText: keyText,
    })
}
<file_sep>/tests/e2e/specs/01_register.js
describe('注册：异常处理', () => {
    it('电子邮件地址被占用', function () {
        cy.log(Cypress.config('dev_hostname'))
        cy.visit('/register')
        cy.get(`#${Cypress.config('dom_map').register.email}`)
            .clear()
            .type(Cypress.config('mock_data').duplicated_email)
            .should('have.value', Cypress.config('mock_data').duplicated_email)
            .blur()
        cy.contains('电子邮件地址已被占用')
    })

    it('用户名被占用', function () {
        cy.visit('/register')
        cy.get(`#${Cypress.config('dom_map').register.username}`)
            .clear()
            .type(Cypress.config('mock_data').duplicated_username)
            .should('have.value', Cypress.config('mock_data').duplicated_username)
            .blur()
        cy.contains('用户名已被占用')
    })

    it('用户名过长', function () {
        cy.visit('/register')
        cy.get(`#${Cypress.config('dom_map').register.username}`)
            .clear()
            .type(Cypress.config('username_long'))
            .should('have.value', Cypress.config('username_long'))
            .blur()
        cy.contains('用户名长度上限为 16 字符')
    })

    it('密码格式不正确', function () {
        cy.visit('/register')
        cy.get(`#${Cypress.config('dom_map').register.password}`)
            .clear()
            .type(Cypress.config('password_short'))
            .should('have.value', Cypress.config('password_short'))
            .blur()
        cy.contains('密码长度必须在 8-16 字符之间')
    })

    it('昵称过长', function () {
        cy.visit('/register')
        cy.get(`#${Cypress.config('dom_map').register.nickname}`)
            .clear()
            .type(Cypress.config('nickname_long'))
            .should('have.value', Cypress.config('nickname_long'))
            .blur()
        cy.contains('昵称长度上限为 16 字符')
    })

    it('缺少必填项目', function () {
        cy.visit('/register')
        cy.get(`#${Cypress.config('dom_map').register.register}`).click()
        cy.contains('请输入')
    })

    it('重置表单', function () {
        cy.visit('/register')
        cy.get(`#${Cypress.config('dom_map').register.register}`).click()
        cy.get(`#${Cypress.config('dom_map').register.reset}`).click()
        cy.contains('请输入').should('not.exist')
    })
})

describe('注册：正确路径', function () {
    it('注册', () => {
        cy.visit('/register')
        cy.get(`#${Cypress.config('dom_map').register.email}`)
            .clear()
            .type(Cypress.config('email'))
            .should('have.value', Cypress.config('email'))
            .blur()
        cy.get(`#${Cypress.config('dom_map').register.username}`)
            .clear()
            .type(Cypress.config('valid_username'))
            .should('have.value', Cypress.config('valid_username'))
            .blur()
        cy.get(`#${Cypress.config('dom_map').register.password}`)
            .clear()
            .type(Cypress.config('valid_password'))
            .should('have.value', Cypress.config('valid_password'))
            .blur()
        cy.get(`#${Cypress.config('dom_map').register.nickname}`)
            .clear()
            .type(Cypress.config('nickname'))
            .should('have.value', Cypress.config('nickname'))
            .blur()
        cy.get(`#${Cypress.config('dom_map').register.register}`).click()
        cy.contains('注册完成')
    })
})
<file_sep>/src/api/post/validate_username.js
import axios from '@/request'

export function validate_username(username) {
    return axios.post('user/validate_username', { username: username })
}
<file_sep>/src/components/reset_password.vue
<template>

            <template v-if="step === 'request'">

                         {
                                    logger.warn(_error)
                                })
                            "
                        />

                        发送重置密码邮件

                        <template #help>

                                选择发送重置密码邮件来验证您对账号的所有权，在您完成重置前密码不会被修改。

                        </template>

            </template>

            <template v-if="step === 'reset'">

                         {
                                    logger.warn(_error)
                                })
                            "
                        />

                        重置密码
                        <template #help>

                                选择重置密码来保存新密码，在您完成重置前密码不会被修改。

                        </template>

            </template>

            <template v-if="step === 'error'">

                        您需要修复以下问题：

                        <close-circle-outlined :style="{ color: 'red' }" />
                        {{ message.filter((i) => i.code === code)[0].problem }}
                         i.code === code)[0].link">
                            {{ message.filter((i) => i.code === code)[0].solution }}>

            </template>

            <template v-if="step === 'waiting'">
                <a-result
                    title="已发送电子邮件"
                    :sub-title="`系统已接受您的重置密码请求，请访问您的电子邮箱（${modelRef_request.email.toLowerCase()}），根据邮件内容提示步骤，重置密码。`"
                >
                    <template #extra>
                        返回
                    </template>

            </template>

            <template v-if="step === 'done'">

                    <template #extra>
                        去登录
                    </template>

            </template>

</template>

<file_sep>/tests/e2e/specs/07_reset_password.js
describe('请求重置密码：错误处理', () => {
    it('电子邮件地址未注册', function () {
        cy.visit('/reset_password')
        cy.url().then((el) => {
            if (el.indexOf(Cypress.config('dev_hostname')) >= 0) {
                this.skip()
            }
            cy.get(`#${Cypress.config('dom_map').reset_password.email}`)
                .clear()
                .type(Cypress.config('email'))
            cy.get(`#${Cypress.config('dom_map').reset_password.request_button}`).click()
            cy.contains('未找到此电子邮件地址对应的账户')
        })
    })

    it('电子邮件地址格式不正确', function () {
        cy.visit('/reset_password')
        cy.url().then((el) => {
            if (el.indexOf(Cypress.config('dev_hostname')) >= 0) {
                this.skip()
            }
            cy.get(`#${Cypress.config('dom_map').reset_password.email}`)
                .clear()
                .type(Cypress.config('invalid_email'))
                .blur()
            cy.contains('请输入有效的电子邮件地址')
        })
    })
})

describe('请求重置密码：正确路径', () => {
    it('发送电子邮件', function () {
        cy.visit('/reset_password')
        cy.url().then((el) => {
            if (el.indexOf(Cypress.config('dev_hostname')) >= 0) {
                this.skip()
            }
            cy.get(`#${Cypress.config('dom_map').reset_password.email}`)
                .clear()
                .type(Cypress.config('mock_data').email)
                .blur()
            cy.get(`#${Cypress.config('dom_map').reset_password.request_button}`).click()
            cy.contains('已发送电子邮件')
        })
    })
})

describe('重置密码：错误处理', () => {
    it('错误token', function () {
        cy.visit(`/reset_password?token=${Cypress.config('invalid_token')}`)
        cy.contains('您提供的令牌无效或已过期')
    })

    it('密码格式不正确', function () {
        cy.visit('/login')
        cy.url().then((el) => {
            if (el.indexOf(Cypress.config('dev_hostname')) >= 0) {
                this.skip()
            }
            cy.visit(`/reset_password?token=${Cypress.config('mock_data').reset_password_token}`)
            cy.get(`#${Cypress.config('dom_map').reset_password.new_password}`)
                .clear()
                .type(Cypress.config('password_short'))
                .blur()
            cy.contains('密码长度必须在 8-16 字符之间')
        })
    })
})

describe('重置密码：正确路径', () => {
    it('重置密码', function () {
        cy.visit('/login')
        cy.url().then((el) => {
            if (el.indexOf(Cypress.config('dev_hostname')) >= 0) {
                this.skip()
            }
            cy.visit(`/reset_password?token=${Cypress.config('mock_data').reset_password_token}`)
            cy.get(`#${Cypress.config('dom_map').reset_password.new_password}`)
                .clear()
                .type(Cypress.config('valid_password'))
                .blur()
            cy.get(`#${Cypress.config('dom_map').reset_password.reset_button}`).click()
            cy.contains('您的密码已重置')
        })
    })
})
<file_sep>/src/components/graph/column.vue
<template>

</template>

<file_sep>/src/components/graph/bar.vue
<template>

</template>

<file_sep>/src/App.vue
<template>

        <ConnectionStatus v-if="network" />
        <router-view />

</template>

<file_sep>/src/api/post/register.js
import axios from '@/request'

export function register(email, username, password, nickname) {
    return axios.post('user/register', {
        email: email,
        username: username,
        password: <PASSWORD>,
        nickname: nickname,
    })
}
<file_sep>/src/util/index.js
/**
 * 带错误处理的解析对象
 * @param o JSON字符串
 * @param k key
 * @returns {undefined|*} 解析的对象或undefined
 * @constructor
 */
export function parseOrNull(o, k) {
    try {
        if (!k) return JSON.parse(o)
        else return JSON.parse(o)[k]
    } catch (e) {
        return undefined
    }
}

/**
 * 比较两数组元素是否相同
 * @param array
 * @returns {boolean}
 */
Array.prototype.elementEquals = function (array) {
    return (
        this.length === array.length &&
        this.every((i) => array.indexOf(i) >= 0 && array.every((i) => this.indexOf(i) >= 0))
    )
}

/**
 * 随机字符串
 * @param e 长度
 * @returns {string}
 */
export function randomString(e) {
    e = e || 32
    let t = 'ABCDEFGHJKMNPQRSTWXYZabcdefhijkmnprstwxyz2345678',
        a = t.length,
        n = ''
    for (let i = 0; i < e; i++) n += t.charAt(Math.floor(Math.random() * a))
    return n
}

/**
 * 获取字符串字节长度
 * @returns {number}
 */
String.prototype.getBytesLength = function () {
    let totalLength = 0
    let charCode
    for (let i = 0; i < this.length; i++) {
        charCode = this.charCodeAt(i)
        if (charCode < 0x007f) {
            totalLength++
        } else if (0x0080 <= charCode && charCode <= 0x07ff) {
            totalLength += 2
        } else if (0x0800 <= charCode && charCode <= 0xffff) {
            totalLength += 3
        } else {
            totalLength += 4
        }
    }
    return totalLength
}
<file_sep>/src/api/post/edit_chart.js
import axios from '@/request'

export function edit_chart(id, chart) {
    if (chart.xField) {
        chart.x_Field = chart.xField
    }
    if (chart.yField) {
        chart.y_Field = chart.yField
    }
    return axios.post('chart/edit', { chart_id: id, ...chart })
}
<file_sep>/src/api/mock/reset_password.js
import Mock from 'mockjs'
import mock_data from '@/api/mock/mock_data'
import log from '@/util/logger'

Mock.mock(process.env.VUE_APP_API_BASE_URL + 'user/reset_password', 'post', (request) => {
    log.mock(request.url, JSON.parse(request.body))
    const { reset_password_token, password } = JSON.parse(request.body)
    if (reset_password_token === mock_data.reset_password_token) {
        // token 正确
        if (password.length >= 8 && password.length <= 16) {
            // 密码格式正确
            return {
                status: {
                    code: 0,
                    message: '密码已重置',
                },
                data: null,
            }
        } else {
            // 密码格式错误
            return {
                status: {
                    code: 7,
                    message: '密码格式错误',
                },
                data: null,
            }
        }
    } else {
        // token错误
        return {
            status: {
                code: 1,
                message: '重置密码令牌无效',
            },
            data: null,
        }
    }
})
<file_sep>/src/components/graph/pie.vue
<template>

</template>

<file_sep>/src/api/post/edit_user_profile.js
import axios from '@/request'

export function edit_user_profile(user) {
    return axios.post('user/modify', { ...user })
}
<file_sep>/src/api/post/delete_data.js
import axios from '@/request'

export function delete_data(id) {
    return axios.post('data/delete', {
        id: id,
    })
}
<file_sep>/src/api/post/reset_password.js
import axios from '@/request'

export function reset_password(reset_password_token, password) {
    return axios.post('user/reset_password', {
        reset_password_token: <PASSWORD>_password_token,
        password: <PASSWORD>,
    })
}
<file_sep>/src/api/mock/user_info.js
import Mock from 'mockjs'
import mock_data from '@/api/mock/mock_data'
import log from '@/util/logger'

Mock.mock(process.env.VUE_APP_API_BASE_URL + 'user/info', 'post', (request) => {
    log.mock(request.url, JSON.parse(request.body))
    // 返回用户信息
    return {
        status: {
            code: 0,
            message: '用户信息获取成功',
        },
        data: {
            user: {
                id: mock_data.user_id,
                username: mock_data.username,
                nickname: mock_data.nickname,
                email: mock_data.email,
                emailVerified: mock_data.email_verified,
                status: 'enabled',
                role: 'user',
                last_login: '1623033546',
            },
        },
    }
})
<file_sep>/src/components/error_pages/e404.vue
<template>

            <template #extra>
                找点乐子吧
            </template>

</template>
<file_sep>/src/api/post/all_tables.js
import axios from '@/request'

export function all_tables() {
    return axios.post('data/all_tables', {}) //data没有也不能不写
}
<file_sep>/src/components/graph/graph_api.vue
<template>
    <component
        :is="defineAsyncComponent(chart_meta.component)"
        :xField="xField"
        :yField="yField"
        :seriesField="seriesField"
        :data="data"
        :key="key"
        v-if="ready"
    />
</template>

<file_sep>/src/components/new_chart.vue
<template>

                新建图表

                        图表是从数据到几何标记对象的图形属性的一个映射，此外图形中还可能包含数据的统计变换，最后绘制在某个特定的坐标系中。

提示：图表名称和图表内容可以在稍后修改。

                 onTabChange(key)"
                    >

                                    <template #label>
                                        图表名称
                                    </template>
                                     {
                                                logger.warn(_error)
                                            })
                                        "
                                        placeholder="我的新图表"
                                        v-model:value="modelRef_blank.chart_name"
                                    />

                                    <template #label>
                                        数据集
                                    </template>
                                    <template #help>

                                            您可以在数据管理中 查看 您的数据集。

                                    </template>

                                    <template #label>
                                        图表类型
                                    </template>

                                    <template #help>

                                            {{
                                                chart_type_options.find((i) => i.value === modelRef_blank.type_id)
                                                    .description + '。'
                                            }}

                                            合适的图表类型可以让您事半功倍，我们提供 {{
                                                chart_type_options.length
                                            }} 种图表，总有一种适合你。

                                    </template>

                                    完成
                                    取消
                                    <template #help
                                        >

                                            选择完成开始编辑图表，选择取消返回上一页面。

</template
                                    >

</template>

<file_sep>/docs/API.md
### 通用信息

以下内容为所有 API 通用的信息。其他文章将会引用本文的一个或多个条目。

##### 版本管理规范

严格遵循 [Semantic Versioning 2.0.0](http://semver.org/lang/zh-CN/) 语义化版本规范。

##### 域名

| 变量名称  | URL  | 描述  |
| ------------ | ------------ | ------------ |
| baseUrl_dev  | http://bi.stager.top:8088  |  develop 分支后端请求地址 |
| baseUrl_master  | http://bi.stager.top:8080  |  master 分支后端请求地址 |
| frontUrl_dev  | http://bi-develop.stager.top |  develop 分支前端地址 |
| frontUrl_master | http://bi-master.stager.top  |  master 分支后端地址 |
| frontUrl_develop_verify_email|http://bi-develop.stager.top/verify_email?token= |develop 分支前端验证邮件 URL|
| frontUrl_master_verify_email|http://bi-develop.stager.top/verify_email?token= |master 分支前端验证邮件 URL|
| frontUrl_develop_reset_password|http://bi-develop.stager.top/reset_password?token= |develop 分支前端重置密码 URL|
| frontUrl_master_reset_password|http://bi-master.stager.top/reset_password?token= |master 分支前端重置密码 URL|

##### 输入格式

| 名称  | 类型  |  长度（包含） | 限制  |  错误提示 |
| ------------ | ------------ | ------------ | ------------ | ------------ |
|  用户名 | string  | 1-16  |  不与其他用户重复 | `用户名长度上限为 16 字符`、`用户名已被其他用户占用`  |
|  电子邮件地址 |  string | 无  | 不与其他用户重复  |  `电子邮件地址格式不正确`、`电子邮件地址已被其他用户占用` |
|密码|string|8-16|无|`密码长度应在 8-16 字符之间`|
|昵称|string|1-16|无|`昵称长度上限为 16 字符`|
|数据表名|string|1-128|无|`数据表名长度上限为 128 字符`|
|数据表描述|string|0-256|无|`数据表名长度上限为 256 字符`|
|图表名|string|1-128|无|`图表名长度上限为 128 字符`|

##### 图表类型

|  类型 id | 类型名称  |
| ------------ | ------------ |
| 1  |  折线图 |
| 2  |  柱状图 |
| 3  |  条形图 |
| 4  |  面积图 |
| 5  |  饼图 |
| 6  |  散点图 |
| 7  |  雷达图 |

### 公共请求参数

##### 简要描述

一个完整的 API 请求需要两类请求参数：公共请求参数和接口请求参数。本文将介绍 API 请求需要用到的 1 个公共请求参数。

公共请求参数是每个接口都需要使用到的请求参数，开发者每次发送请求时都需要携带这些公共请求参数和接口参数一起提交。

此参数位于请求头中。

##### 请求方式

- POST

##### 参数

|参数名|必选|类型|说明|
|:----    |:---|:----- |-----   |
|Authorization |是  | |用户令牌   |

### 共通返回

这些返回在所有的接口都有可能返回，它们是所有接口共通的，在此统一说明，下面的接口将不再列出。

共通返回封装在`status`对象中，接口专属返回封装在`data`对象中。

##### 返回示例

```JSON
{
    "status": {
        "code": 0,
        "message": "Success"
    },
    "data": {
    }
}
```

##### 返回参数说明

|参数名|类型|说明|
|:-----  |:-----|-----                           |
|code|number|返回码|
|message|string|返回码描述|

##### 备注

为了避免和接口专属的返回码冲突，此处的返回码均不大于0。

|返回码|描述|
|:-----  |:-----|
|0|操作成功|
|-1|未携带 token|
|-2|token 过期或无效|
|-3|无权使用此接口|
|-4|接口已停用|
|-5|只允许 POST 方法|
|-6|只允许 GET 方法|
|-7|用户已停用|
|-8|字段值数据类型错误|
|-9|value格式不正确|
|-10|未知错误|

### 获取用户信息

##### 简要描述

- 本接口用于获取用户信息。该信息封装在`user`对象中。

##### 请求URL

- ` baseUrl/api/user/info `

##### 请求方式

- POST

##### 参数

以下请求参数列表仅列出了接口请求参数，正式调用时需要加上公共请求参数，见[公共请求参数](#公共请求参数 "公共请求参数")页面

|参数名|必选|类型|说明|
|:----    |:---|:----- |-----   |
|公共参数 |是  | |详见： [公共请求参数](#公共请求参数 "公共请求参数")   |

##### 返回码

以下请求参数列表仅列出了此接口请求响应代码，除此接口请求响应代码外。还包含[共通返回](#共通返回 "共通返回")。

| 返回码  |  描述 |
| ------------ | ------------ |
|   | [共通返回](#共通返回 "共通返回")  |

##### 返回示例

```JSON
{
    "status": {
        "code": 0,
        "message": "用户信息获取成功"
    },
    "data": {
        "user": {
            "id": 123,
            "username": "zhangsan",
            "nickname": "张三",
            "email": "<EMAIL>",
            "email_verified": true,
            "status": "enabled",
            "role": "user",
            "last_login": "1623033546"
        }
    }
}
```

##### 返回参数说明

|参数名|类型|说明|
|:-----  |:-----|-----                           |
|id |number   |用户id  |
|username|string|用户名|
|nickname|string|昵称|
|email|string|电子邮件地址|
|email_verified|boolean|邮箱是否通过验证：`true`: 通过；`false`: 未通过。|
|status|string|用户状态：`enabled`：启用；`disabled`：停用。|
|role|string|用户角色：`user`：用户。|
|last_login|string|上次登录时间：`-1`：首次登录；其他：Unix时间戳|

- [Unix时间戳的使用方法](https://tool.chinaz.com/Tools/unixtime.aspx "Unix时间戳的使用方法")

### 登录

##### 简要描述

- 本接口用于登录。

##### 请求URL

- ` baseUrl/api/user/login `

##### 请求方式

- POST

##### 参数

|参数名|必选|类型|说明|
|:----    |:---|:----- |-----   |
|login |是  |string |用户名或电子邮件地址  |
|password |是  |string |密码（经过加密）  |
|rememberMe |是|boolean|记住登录信息|
|salt|是|string|加密参数|

- 密码加密模式：`md5(md5($password) + $salt)`，例如密码为`<PASSWORD>`，salt为`abcdef`，经过加密的密码即为`<PASSWORD>`
  。其中数据库存储`md5($password)`。

> 登录请求无需传入`token`。

##### 返回码

以下请求参数列表仅列出了此接口请求响应代码，除此接口请求响应代码外。还包含[共通返回](#共通返回 "共通返回")。

| 返回码  |  描述 |
| ------------ | ------------ |
|   | [共通返回](#共通返回 "共通返回")  |
|1|用户不存在|
|2|密码无效|
|3|电子邮件地址未激活|

##### 返回示例

```JSON
{
    "status": {
        "code": 0,
        "message": "登录成功"
    },
    "data": {
        "token": "<KEY>"
    }
}
```

##### 返回参数说明

|参数名|类型|说明|
|:-----  |:-----|-----                           |
|token|string|用户令牌

- 登录失败时`data`对象为空。

### 注册

##### 简要描述

- 本接口用于注册。

##### 请求URL

- ` baseUrl/api/user/register `

##### 请求方式

- POST

##### 参数

|参数名|必选|类型|说明|
|:----    |:---|:----- |-----   |
|email|是|string|电子邮件地址|
|username|是|string|用户名|
|nickname|否|string|昵称|
|password|是|string|密码（明文）|

##### 返回码

以下请求参数列表仅列出了此接口请求响应代码，除此接口请求响应代码外。还包含[共通返回](#共通返回 "共通返回")。

| 返回码  |  描述 |
| ------------ | ------------ |
|   | [共通返回](#共通返回 "共通返回")  |
|1|已禁止用户注册|
|11|电子邮件地址已被占用|
|12|电子邮件地址格式错误|
|21|用户名已被占用|
|22|用户名格式错误|
|32|昵称格式错误（允许重复）|
|42|密码格式错误|

##### 返回示例

```JSON
{
    "status": {
        "code": 0,
        "message": "注册成功"
    },
    "data": {
        "token": "<KEY>"
    }
}
```

##### 返回参数说明

|参数名|类型|说明|
|:-----  |:-----|-----                           |
|token|string|用户令牌
|expire_time|string|`token`过期时间，格式为Unix时间戳|

- [Unix时间戳的使用方法](https://tool.chinaz.com/Tools/unixtime.aspx "Unix时间戳的使用方法")
- 注册失败时`data`对象为空。

### 退出登录

##### 简要描述

- 本接口用于退出登录，退出登录后token将会失效。

##### 请求URL

- ` baseUrl/api/user/logout `

##### 请求方式

- POST

##### 参数

|参数名|必选|类型|说明|
|:----    |:---|:----- |-----   |
|公共参数 |是  | |详见： [公共请求参数](#公共请求参数 "公共请求参数")   |

##### 返回码

以下请求参数列表仅列出了此接口请求响应代码，除此接口请求响应代码外。还包含[共通返回](#共通返回 "共通返回")。

| 返回码  |  描述 |
| ------------ | ------------ |
|   | [共通返回](#共通返回 "共通返回")  |

##### 返回示例

```JSON
{
    "status": {
        "code": 0,
        "message": "成功退出登录"
    },
    "data": null
}
```

### 修改用户信息

##### 简要描述

- 本接口用于修改用户信息。要修改的信息封装在`user`对象中。

##### 请求URL

- ` baseUrl/api/user/modify `

##### 请求方式

- POST

##### 参数

以下请求参数列表仅列出了接口请求参数，正式调用时需要加上公共请求参数，见[公共请求参数](#公共请求参数 "公共请求参数")页面

|参数名|必选|类型|说明|
|:----    |:---|:----- |-----   |
|公共参数 |是  | |详见： [公共请求参数](#公共请求参数 "公共请求参数")   |
| username |否| string| 用户名 |
| nickname |否| string| 昵称 |
| email |否| string| 电子邮件地址 |

##### 请求示例

```JSON
{
    "username": "xxxxxx",
    "nickname": "xxxxx",
    "email": "<EMAIL>"
}
```

##### 返回码

以下请求参数列表仅列出了此接口请求响应代码，除此接口请求响应代码外。还包含[共通返回](#共通返回 "共通返回")。

| 返回码  |  描述 |
| ------------ | ------------ |
|   | [共通返回](#共通返回 "共通返回")  |
| x | 修改失败的字段数量|

- 只要有1个字段修改失败即撤销本次所有修改。

##### 返回示例

```JSON
{
    "status": {
        "code": 2,
        "message": "有2个字段修改失败"
    },
    "data": {
        "errors": [
            {
                "key": "username",
                "value": "用户名格式不正确"
            },
            {
                "key": "email",
                "value": "电子邮件地址已被占用"
            }
        ]
    }
}
```

##### 返回参数说明

|参数|类型|描述|
|:-------|:-------|:-------|
| - errors |object  | 错误列表 |
| key | string| 出现错误的字段名称 |
| value | string| 错误原因 |

### 删除账户

##### 简要描述

- 本接口用于删除账户。

##### 请求URL

- ` baseUrl/api/user/delete `

##### 请求方式

- POST

##### 参数

以下请求参数列表仅列出了接口请求参数，正式调用时需要加上公共请求参数，见[公共请求参数](#公共请求参数 "公共请求参数")页面

|参数名|必选|类型|说明|
|:----    |:---|:----- |-----   |
|公共参数 |是  | |详见： [公共请求参数](#公共请求参数 "公共请求参数")   |
|username |是 |string |用户名|
|password |是  |string |密码（经过加密）|
|salt|是|string|加密参数|

- 密码加密模式：`md5(md5($password) + $salt)`，例如密码为`<PASSWORD>`，salt为`<PASSWORD>`，经过加密的密码即为`<PASSWORD>`
  。其中数据库存储`md5($password)`。

##### 返回码

以下请求参数列表仅列出了此接口请求响应代码，除此接口请求响应代码外。还包含[共通返回](#共通返回 "共通返回")。

| 返回码  |  描述 |
| ------------ | ------------ |
|   | [共通返回](#共通返回 "共通返回")  |
|1|用户名或密码无效|

##### 返回示例

```JSON
{
    "status": {
        "code": 0,
        "message": "用户已注销"
    },
    "data": null
}
```

### 检查可用性

##### 简要描述

- 本接口用于检查用户名、电子邮件地址和重置密码令牌的可用性。

##### 请求URL

- ` baseUrl/api/user/validate_username `
- ` baseUrl/api/user/validate_email `
- ` baseUrl/api/user/validate_reset_password_token`

##### 请求方式

- POST

##### 参数

|参数名|必选|类型|说明|
|:----    |:---|:----- |-----   |
|username|是|string|需要检查的用户名|

|参数名|必选|类型|说明|
|:----    |:---|:----- |-----   |
|email|是|string|需要检查的电子邮件地址|

|参数名|必选|类型|说明|
|:----    |:---|:----- |-----   |
|reset_password_token|是|string|需要检查的重置密码令牌|

##### 返回码

以下请求参数列表仅列出了此接口请求响应代码，除此接口请求响应代码外。还包含[共通返回](#共通返回 "共通返回")。

| 返回码  |  描述 |
| ------------ | ------------ |
|   | [共通返回](#共通返回 "共通返回")  |
| 1  | 用户名已被占用 |
| 2 |用户名格式错误|

| 返回码  |  描述 |
| ------------ | ------------ |
|   | [共通返回](#共通返回 "共通返回")  |
| 1  | 电子邮件地址已被占用 |
| 2 |电子邮件地址格式错误|

| 返回码  |  描述 |
| ------------ | ------------ |
|   | [共通返回](#共通返回 "共通返回")  |
| 1  | 令牌无效或已过期 |

##### 返回示例

```JSON
{
    "status": {
        "code": 0,
        "message": "用户名可用"
    },
    "data": null
}
```

```JSON
{
    "status": {
        "code": 0,
        "message": "电子邮件地址可用"
    },
    "data": null
}
```

```JSON
{
    "status": {
        "code": 0,
        "message": "重置密码令牌可用"
    },
    "data": null
}
```

### 请求重置密码

##### 简要描述

- 本接口用于在未登录的情况下请求重置密码。

##### 请求URL

- ` baseUrl/api/user/request_reset_password `

##### 请求方式

- POST

##### 参数

|参数名|必选|类型|说明|
|:----    |:---|:----- |-----   |
|email|是|string|电子邮件地址|

##### 返回码

以下请求参数列表仅列出了此接口请求响应代码，除此接口请求响应代码外。还包含[共通返回](#共通返回 "共通返回")。

| 返回码  |  描述 |
| ------------ | ------------ |
|   | [共通返回](#共通返回 "共通返回")  |
|2|电子邮件地址未注册|
|3|电子邮件地址对应的账户已停用（弃用）|
|4|禁止此用户重置密码(弃用)|
|5|电子邮件地址格式不正确|
|6|电子邮件地址未验证|

##### 返回示例

```JSON
{
    "status": {
        "code": 0,
        "message": "已发送邮件"
    },
    "data": null
}
```

### 重置密码

##### 简要描述

- 本接口用于重置密码。

##### 请求URL

- ` baseUrl/api/user/reset_password `

##### 请求方式

- POST

##### 参数

|参数名|必选|类型|说明|
|:----    |:---|:----- |-----   |
|reset_password_token|是|string|重置密码令牌|
|password|是|string|密码(明文)|

##### 返回码

以下请求参数列表仅列出了此接口请求响应代码，除此接口请求响应代码外。还包含[共通返回](#共通返回 "共通返回")。

| 返回码  |  描述 |
| ------------ | ------------ |
|   | [共通返回](#共通返回 "共通返回")  |
|1|重置密码令牌无效或已过期|
|7|密码格式错误|

##### 返回示例

```JSON
{
    "status": {
        "code": 0,
        "message": "密码已重置"
    },
    "data": null
}
```

### 修改密码

##### 简要描述

- 本接口用于修改密码。

##### 请求URL

- ` baseUrl/api/user/change_password `

##### 请求方式

- POST

##### 参数

以下请求参数列表仅列出了接口请求参数，正式调用时需要加上公共请求参数，见[公共请求参数](#公共请求参数 "公共请求参数")页面

|参数名|必选|类型|说明|
|:----    |:---|:----- |-----   |
|公共参数 |是  | |详见： [公共请求参数](#公共请求参数 "公共请求参数")   |
|old_password|是|string|旧密码（明文）|
|new_password|是|string|新密码（明文）|

##### 返回码

以下请求参数列表仅列出了此接口请求响应代码，除此接口请求响应代码外。还包含[共通返回](#共通返回 "共通返回")。

| 返回码  |  描述 |
| ------------ | ------------ |
|   | [共通返回](#共通返回 "共通返回")  |
|1|旧密码不匹配|
|2|已禁止该用户修改密码（弃用）|
|3|新密码格式错误|

##### 返回示例

```JSON
{
    "status": {
        "code": 0,
        "message": "密码修改成功"
    },
    "data": null
}
```

### 激活电子邮件地址

##### 简要描述

- 本接口用于激活用户的电子邮件地址。

##### 请求URL

- ` baseUrl/api/user/activate_email `

##### 请求方式

- POST

##### 参数

|参数名|必选|类型|说明|
|:----    |:---|:----- |-----   |
|activation_token | 是  |string| 激活电子邮件地址令牌 |

##### 返回码

以下请求参数列表仅列出了此接口请求响应代码，除此接口请求响应代码外。还包含[共通返回](#共通返回 "共通返回")。

| 返回码  |  描述 |
| ------------ | ------------ |
|   | [共通返回](#共通返回 "共通返回")  |
|1|激活电子邮件地址令牌过期或无效|

##### 返回示例

```JSON
{
    "status": {
        "code": 0,
        "message": "电子邮件地址已激活"
    },
    "data": null
}
```

### 重新发送用于激活用户的邮件

##### 简要描述

- 本接口用于重新发送用于激活用户的邮件。该邮件地址存储在后端。

##### 请求URL

- ` baseUrl/api/user/resend_activation_email`

##### 请求方式

- POST

##### 参数

以下请求参数列表仅列出了接口请求参数，正式调用时需要加上公共请求参数，见[公共请求参数](#公共请求参数 "公共请求参数")页面

|参数名|必选|类型|说明|
|:----    |:---|:----- |-----   |
|公共参数 |是  | |详见： [公共请求参数](#公共请求参数 "公共请求参数")   |

##### 返回码

以下请求参数列表仅列出了此接口请求响应代码，除此接口请求响应代码外。还包含[共通返回](#共通返回 "共通返回")。

| 返回码  |  描述 |
| ------------ | ------------ |
|   | [共通返回](#共通返回 "共通返回")  |
|1|此账户的电子邮件地址已激活|

##### 返回示例

```JSON
{
    "status": {
        "code": 0,
        "message": "邮件发送成功"
    },
    "data": null
}
```

### 请求获取数据库表

##### 简要描述

- 请求数据库中全部表信息

##### 请求URL

- ` baseUrl/api/data/all_tables `

##### 请求方式

- POST

##### 参数

以下请求参数列表仅列出了接口请求参数，正式调用时需要加上公共请求参数，见[公共请求参数](#公共请求参数 "公共请求参数")页面

|参数名|必选|类型|说明|
|:----    |:---|:----- |-----   |
|公共参数 |是  | |详见： [公共请求参数](#公共请求参数 "公共请求参数")   |

##### 返回码

|返回码|描述|
|:----   |:---|
||   见：[共通返回](#共通返回 "共通返回")|

##### 返回示例

```JSON
{
    "status": {
        "code": 0,
        "message": "数据库表信息获取成功"
    },
    "data": {
        "tables": [
            {
                "id": 1,
                "name": "豆瓣电影数据",
                "editable": false,
                "description": "按照豆瓣评分排序展示电影的基本信息"
            },
            {
                "id": 2,
                "name": "58租房数据",
                "editable": true,
                "description": "展示58同城租房网站的户型等基本信息"
            }
        ]
    }
}
```

##### 返回参数说明

|参数名|必选|类型|说明|
|:----    |:---|:----- |-----   |
|id |是  |number |   表id|
|name |是  |string | 表名    |
|editable     |是  |boolean | 该表是否为样例数据    |
|description     |是  |string | 数据集描述    |

### 请求表的内容

##### 简要描述

- 分页请求获取某个数据表的部分内容

##### 请求URL

- ` baseUrl/api/data/table_content `

##### 请求方式

- POST

##### 参数

以下请求参数列表仅列出了接口请求参数，正式调用时需要加上公共请求参数，见[公共请求参数](#公共请求参数 "公共请求参数")页面

|参数名|必选|类型|说明|
|:----    |:---|:----- |-----   |
|公共参数 |是  | |详见： [公共请求参数](#公共请求参数 "公共请求参数")   |
|table_id |是  |number |  所请求表的id|
|max_length |否|number |每页最大数据量|
|offset|否|number|当前页号(从1开始)|

> 当 `max_length` 设置为`-1`时返回全部`dataSource`，设为`0`时不返回`dataSource`。上述两种情况时忽略`offset`

##### 请求示例

```JSON
{
    "table_id": 1,
    "max_length": 10,
    "offset": 1
}
```

##### 返回码

以下请求参数列表仅列出了此接口请求响应代码，除此接口请求响应代码外。还包含共通返回。

|返回码|描述|
|:----   |:---|
||   见：[共通返回](#共通返回 "共通返回")|
|1|该表不存在|

##### 返回示例

```JSON
{
    "status": {
        "code": 0,
        "message": "当前页内容信息获取成功"
    },
    "data": {
        "table": {
            "table_name": "豆瓣电影数据",
            "total": 100,
            "columns": [
                {
                    "title": "电影名称",
                    "dataIndex": "name",
                    "key": "name",
                    "type": "string"
                },
                {
                    "title": "评分",
                    "dataIndex": "rate",
                    "key": "rate",
                    "type": "number"
                },
                {
                    "title": "类型",
                    "dataIndex": "type",
                    "key": "type",
                    "type": "string"
                },
                {
                    "title": "地区",
                    "dataIndex": "country",
                    "key": "country",
                    "type": "string"
                },
                {
                    "title": "上映时间",
                    "dataIndex": "year",
                    "key": "year",
                    "type": "string"
                },
                {
                    "title": "评论数",
                    "dataIndex": "comment",
                    "key": "comment",
                    "type": "number"
                }
            ],
            "dataSource": [
                {
                    "ranking": 1,
                    "name": "肖申克的救赎",
                    "rate": 9.7,
                    "type": "犯罪 剧情",
                    "country": "美国",
                    "year": "1994-9",
                    "comment": 2365224
                },
                {
                    "ranking": 2,
                    "name": "霸王别姬",
                    "rate": 9.6,
                    "type": "爱情 剧情",
                    "country": "中国大陆 中国香港",
                    "year": "1993-7",
                    "comment": 1761207
                },
                {
                    "ranking": 3,
                    "name": "美丽人生",
                    "rate": 9.6,
                    "type": "剧情 喜剧",
                    "country": "意大利",
                    "year": "2020-1",
                    "comment": 1095584
                },
                {
                    "ranking": 4,
                    "name": "控方证人",
                    "rate": 9.6,
                    "type": "剧情 犯罪",
                    "country": "美国",
                    "year": "1957-12",
                    "comment": 371881
                },
                {
                    "ranking": 5,
                    "name": "伊丽莎白",
                    "rate": 9.6,
                    "type": "剧情 歌舞",
                    "country": "奥地利",
                    "year": "2005-12",
                    "comment": 5639
                },
                {
                    "ranking": 6,
                    "name": "阿甘正传",
                    "rate": 9.5,
                    "type": "剧情 爱情",
                    "country": "美国",
                    "year": "1994-6",
                    "comment": 1782286
                },
                {
                    "ranking": 7,
                    "name": "辛德勒的名单",
                    "rate": 9.5,
                    "type": "剧情 爱情",
                    "country": "美国",
                    "year": "1994-6",
                    "comment": 1782286
                },
                {
                    "ranking": 8,
                    "name": "茶馆",
                    "rate": 9.5,
                    "type": "剧情 历史",
                    "country": "中国大陆",
                    "year": "1982",
                    "comment": 68805
                },
                {
                    "ranking": 9,
                    "name": "控方证人",
                    "rate": 9.5,
                    "type": "剧情 犯罪",
                    "country": "美国 英国",
                    "year": "1982-12",
                    "comment": 32436
                },
                {
                    "ranking": 10,
                    "name": "十二怒汉",
                    "rate": 9.5,
                    "type": "剧情 犯罪",
                    "country": "美国",
                    "year": "1954-9",
                    "comment": 19810
                }
            ]
        }
    }
}
```

##### 返回参数说明

|参数名|类型|说明|
|:----    |:----- |-----   |
| table_name |object |   数据表名称|
| total |object |   数据表总数据量|
| columns |object |   列信息|
| - title |string|列名|
| - dataIndex |string|绑定标识|
| - key |string|列标识|
| dataSource |object | 表的内容    |

### 修改数据表名

##### 简要描述

- 请求修改样例数据名称接口

##### 请求URL

- ` baseUrl/api/data/change_table
  `

##### 请求方式

- POST

##### 参数

以下请求参数列表仅列出了接口请求参数，正式调用时需要加上公共请求参数，见[公共请求参数](#公共请求参数 "公共请求参数")页面

|参数名|必选|类型|说明|
|:----    |:---|:----- |-----   |
|公共参数 |是  | |详见： [公共请求参数](#公共请求参数 "公共请求参数")   |
|table_id |是  |number |数据集id   |
|table_name |是  |string | 新数据集名    |

##### 请求示例

```JSON
{
    "table_id": 1,
    "table_name": "豆瓣电影数据1"
}
```

##### 返回码

|返回码|描述|
|:----   |:---|
||   见：[共通返回](#共通返回 "共通返回")|
|1|   表不存在|
|2|   数据表名称不合法|

##### 返回示例

``` JSON
  {
    "status": {
    "code": 0,
    "message": "表名修改成功"
  },
    "data": null
  }
```

### 获取全部图表

##### 简要描述

- 本接口用于查询用户现有的图表、限额和已用数量

##### 请求URL

- ` baseUrl/api/chart/all `

##### 请求方式

- POST

##### 参数

以下请求参数列表仅列出了接口请求参数，正式调用时需要加上公共请求参数，见[公共请求参数](#公共请求参数 "公共请求参数")页面

|参数名|必选|类型|说明|
|:----    |:---|:----- |-----   |
|公共参数 |是  | |详见： [公共请求参数](#公共请求参数 "公共请求参数")   |

##### 返回码

以下请求参数列表仅列出了此接口请求响应代码，除此接口请求响应代码外。还包含[共通返回](#共通返回 "共通返回")。

| 返回码  |  描述 |
| ------------ | ------------ |
|   | [共通返回](#共通返回 "共通返回")  |

##### 返回示例

```JSON
{
    "status": {
        "code": 0,
        "message": "获取全部图表"
    },
    "data": {
        "all_charts": {
            "quota": 10,
            "charts": [
                {
                    "id": 1,
                    "chart_name": "My chart",
                    "type_id": 123,
                    "last_modified_time": 1623748364
                },
                {
                    "id": 2,
                    "chart_name": "My chart2",
                    "type_id": 124,
                    "last_modified_time": 1623748365
                }
            ]
        }
    }
}
```

##### 返回参数说明

|参数|类型|描述|
|:-------|:-------|:-------|
| quota | number| 用户的图表限额 |
| - - charts |object  | 图表对象 |
| id | number| 图表id |
| name | string| 图表名称 |
| type_id | number| 图表类型id |
| last_modified_time | number | 上次修改日期（Unix时间戳） |

### 创建图表

##### 简要描述

- 该接口用于选择图表类型及相关属性并生成

##### 请求URL

- ` baseUrl/api/chart/create `

##### 请求方式

- POST

##### 参数

|参数名|必选|类型|说明|
|:----    |:---|:----- |-----   |
|公共参数 |是  | |详见： [公共请求参数](#公共请求参数 "公共请求参数")   |
| data_id |是| number| 数据表id |
| type_id |是| number| 图表类型id |
| chart_name |是| string| 图表名称 |

##### 请求示例

```JSON
{
    "data_id": 0,
    "type_id": 0,
    "chart_name": "My chart"
}
```

##### 返回码

以下请求参数列表仅列出了此接口请求响应代码，除此接口请求响应代码外。还包含[共通返回](#共通返回 "共通返回")。

| 返回码  |  描述 |
| ------------ | ------------ |
|   | [共通返回](#共通返回 "共通返回")  |
|1|图表名称为空|
|2|图表限额已满|
|3|图表构建失败|

##### 返回示例

```JSON
{
    "status": {
        "code": 0,
        "message": "生成成功"
    },
    "data": {
        "chart_id": "152448854"
    }
}
```

##### 返回参数说明

|参数名|类型|说明|
|:-----  |:-----|-----                           |
|chart_id     |int |图表编号     |

### 编辑图表

##### 简要描述

- 本接口用于修改和保存图表

##### 请求URL

- ` baseUrl/api/chart/edit `

##### 请求方式

- POST

##### 参数

以下请求参数列表仅列出了接口请求参数，正式调用时需要加上公共请求参数，见[公共请求参数](#公共请求参数 "公共请求参数")页面

|参数名|必选|类型|说明|
|:----    |:---|:----- |-----   |
|公共参数 |是  | |详见： [公共请求参数](#公共请求参数 "公共请求参数")   |
|chart_id |是|number|图表id|
| type_id |否| number| 图表类型id |
| chart_name |否| string| 图表名称 |
| keys_number |否|object  | 指标列名 |
| keys_text |否|object  | 维度列名 |
| x_Field |否| string| 自变量 |
| y_Field |否| string| 因变量 |
| seriesField |否| string| 类别 |

##### 请求示例

```JSON
{
    "chart_id": 1,
    "type_id": 0,
    "chart_name": "My chart",
    "keys_number": [
        "rate",
        "comment"
    ],
    "keys_text": [
        "name"
    ],
    "x_Field": "name",
    "y_Field": "value",
    "seriesField": "category"
}
```

##### 返回码

以下请求参数列表仅列出了此接口请求响应代码，除此接口请求响应代码外。还包含[共通返回](#共通返回 "共通返回")。

| 返回码  |  描述 |
| ------------ | ------------ |
|   | [共通返回](#共通返回 "共通返回")  |
|2|非空字段的字段值为空|

##### 返回示例

```JSON
  {
    "status": {
        "code": 0,
        "message": "图表已保存"
    },
    "data": null
}
```

### 删除图表

##### 简要描述

- 本接口用于用户删除已生成的图表

##### 请求URL

- ` baseUrl/api/chart/delete `

##### 请求方式

- POST

##### 参数

|参数名|必选|类型|说明|
|:----    |:---|:----- |-----   |
|公共参数 |是  | |详见： [公共请求参数](#公共请求参数 "公共请求参数")   |
|id|是|number|图表编号|

##### 返回码

以下请求参数列表仅列出了此接口请求响应代码，除此接口请求响应代码外。还包含[共通返回](#共通返回 "共通返回")。

| 返回码  |  描述 |
| ------------ | ------------ |
|   | [共通返回](#共通返回 "共通返回")  |
|1|图表编号不存在|
|2|key键值不符合要求|

##### 返回示例

```JSON
   {
	"status": {
      "code": 0,
      "message": "删除成功"
     },
    "data": null
  }
```

### 向仪表盘添加图表（改逻辑）

##### 简要描述

- 本接口用于向仪表盘添加1个图表

##### 请求URL

- ` baseUrl/api/dashboard/add `

##### 请求方式

- POST

##### 请求示例

```JSON
{
    "chart_id": 123
}
```

##### 参数

|参数名|必选|类型|说明|
|:----    |:---|:----- |-----   |
|公共参数 |是  | |详见： [公共请求参数](#公共请求参数 "公共请求参数")   |
|chart_id|是|number|图表id（用户图表的id）|

##### 返回码

以下请求参数列表仅列出了此接口请求响应代码，除此接口请求响应代码外。还包含[共通返回](#共通返回 "共通返回")。

| 返回码  |  描述 |
| ------------ | ------------ |
|   | [共通返回](#共通返回 "共通返回")  |
|1|图表id不存在|
|2|仪表数量超出限制|

##### 返回示例

```JSON
{
    "status": {
        "code": 0,
        "message": "仪表添加成功"
    },
    "data": {
        "id": 123
    }
}
```

##### 返回参数说明

|参数|类型|描述|
|:-------|:-------|:-------|
| id | number| 仪表id（仪表盘中的id） |

### 编辑仪表盘（2.0更新）

##### 简要描述

- 该接口用于编辑仪表盘内图表的位置和删除仪表盘中的图表

##### 请求URL

- ` baseUrl/api/dashboard/edit `

##### 请求方式

- POST

##### 参数

|参数名|必选|类型|说明|
|:----    |:---|:----- |-----   |
|公共参数 |是  | |详见： [公共请求参数](#公共请求参数 "公共请求参数")   |
| -instruments |是|object  | 图表对象 |
| id |是| number| 仪表id |
| number |是| number| 仪表顺序(从1开始) |
|chart_id|是|number| 图表id |
|data_id|是|number| 仪表所用数据集id |
|select_keys|是|string| 仪表中图表所用维度(从JSON序列化) |

##### 请求示例

```JSON
{
    "instruments": [
        {
            "id": 453,
            "number": 1,
            "chart_id": 2,
            "data_id": 1,
            "select_keys": "[\"南\", \"北\"]"
        },
        {
            "id": 454,
            "number": 2,
            "chart_id": 1,
            "data_id": 2,
            "select_keys": "[]"
        }
    ]
}
```

> 当`selected_keys`为空时默认为选择所有值

##### 返回码

以下请求参数列表仅列出了此接口请求响应代码，除此接口请求响应代码外。还包含[共通返回](#共通返回 "共通返回")。

| 返回码  |  描述 |
| ------------ | ------------ |
|   | [共通返回](#共通返回 "共通返回")  |
|1|仪表id不存在|
|2|图表id不存在|
|3|仪表number重复(前端从1开始按顺序传给后端)|
|4|未发现修改内容（弃用）|
|5|number超出上限|

##### 返回示例

```JSON
{
    "status": {
        "code": 0,
        "message": "修改成功"
    },
    "data": null
}
```

### 查看仪表盘（2.0更新）

##### 简要描述

- 本接口用于查看仪表盘

##### 请求URL

- ` baseUrl/api/dashboard/view `

##### 请求方式

- POST

##### 参数

|参数名|必选|类型|说明|
|:----    |:---|:----- |-----   |
|公共参数 |是  | |详见： [公共请求参数](#公共请求参数 "公共请求参数")   |

##### 返回码

以下请求参数列表仅列出了此接口请求响应代码，除此接口请求响应代码外。还包含[共通返回](#共通返回 "共通返回")。

| 返回码  |  描述 |
| ------------ | ------------ |
|   | [共通返回](#共通返回 "共通返回")  |

##### 返回示例

```JSON
{
    "status": {
        "code": 0,
        "message": "仪表盘请求成功"
    },
    "data": {
        "instruments": [
            {
                "id": 453,
                "index": 0,
                "chart_id": 2,
                "data_id": 1,
                "selected_keys": "[\"南\", \"北\"]"
            },
            {
                "id": 454,
                "index": 2,
                "chart_id": 1,
                "data_id": 2,
                "selected_keys": "[]"
            }
        ]
    }
}
```

##### 返回参数说明

|参数|类型|描述|
|:-------|:-------|:-------|
| - - instruments |object  | 仪表对象 |
| id | number| 仪表id |
| index | number| 仪表顺序（不要求连号，前端会按从小到大排序） |
| chart_id | number| 图表id |
| data_id | number |仪表所用数据集id|
| selected_keys | string | 仪表中图表所用维度（从JSON序列化） |

### 查看图表

##### 简要描述

- 该接口用于查看图表

##### 请求URL

- ` baseUrl/api/chart/view `

##### 请求方式

- POST

##### 参数

|参数名|必选|类型|说明|
|:----    |:---|:----- |-----   |
|公共参数 |是  | |详见： [公共请求参数](#公共请求参数 "公共请求参数")   |
| id |是| number| 图表id |

##### 返回码

以下请求参数列表仅列出了此接口请求响应代码，除此接口请求响应代码外。还包含[共通返回](#共通返回 "共通返回")。

| 返回码  |  描述 |
| ------------ | ------------ |
|   | [共通返回](#共通返回 "共通返回")  |
|1|图表id无效|

##### 返回示例

```JSON
{
    "status": {
        "code": 0,
        "message": "请求图表成功"
    },
    "data": {
        "chart": {
            "type_id": 0,
            "data_id": 0,
            "chart_name": "My chart",
            "last_modified_time": 1623916319,
            "keys_number": [
                "rate",
                "comment"
            ],
            "keys_text": [
                "name"
            ],
            "xField": "name",
            "yField": "value",
            "seriesField": "category"
        }
    }
}
```

##### 返回参数说明

|参数|类型|描述|
|:-------|:-------|:-------|
| - - chart |object  | 图表对象 |
| type_id | number| 图表类型id |
| data_id | number| 数据表id |
| chart_name | string| 图表名称 |
| last_modified_time | number| 上次修改时间（Unix时间戳） |
|keys_number|Object|选中的指标列key|
|keys_text|Object|选中的维度列key|
| xField | string| 自变量名 |
| yField | string| 因变量名 |
| seriesField | string| 分组名 |

### 创建数据集

##### 简要描述

- 将用户上传的数据集（不含数据）导入至个人账号内

##### 请求URL

- ` baseUrl/api/data/create `

##### 请求方式

- POST

##### 参数

以下请求参数列表仅列出了接口请求参数，正式调用时需要加上公共请求参数，见[公共请求参数](#公共请求参数 "公共请求参数")页面

|参数名|必选|类型|说明|
|:----    |:---|:----- |-----   |
|公共参数 |是  | |详见： [公共请求参数](#公共请求参数 "公共请求参数")   |
| columns | 是 | object | 数据表表头 |

##### 请求示例

```JSON
{
    "columns": [
        {
            "title": "ccc",
            "key": "ccc",
            "dataIndex": "ccc",
            "type": "number"
        },
        {
            "title": "ddd",
            "key": "ddd",
            "dataIndex": "ddd",
            "type": "string"
        }
    ]
}
```

##### 返回码

以下请求参数列表仅列出了此接口请求响应代码，除此接口请求响应代码外。还包含共通返回。

|返回码|描述|
|:----   |:---|
||   见：[共通返回](#共通返回 "共通返回")|
|1|存在空列名|
|2|存在重复列名|

##### 返回示例

```JSON
{
    "status": {
        "code": 0,
        "message": "数据集导入成功"
    },
    "data": {
        "id": 1
    }
}
```

##### 返回参数说明

|参数名|类型|说明|
|:----    |:----- |-----   |
| id |number | 数据集主键 |

### 获取所有维度值

##### 简要描述

- 本接口用于某个图表所用数据集某一维度的所有值

##### 请求URL

- ` baseUrl/api/data/keys `

##### 请求方式

- POST

##### 参数

以下请求参数列表仅列出了接口请求参数，正式调用时需要加上公共请求参数，见[公共请求参数](#公共请求参数 "公共请求参数")页面

|参数名|必选|类型|说明|
|:----    |:---|:----- |-----   |
|公共参数 |是  | |详见： [公共请求参数](#公共请求参数 "公共请求参数")   |
|id|是|number|数据集主键|
|keyText|是|string|维度名称|

##### 返回码

以下请求参数列表仅列出了此接口请求响应代码，除此接口请求响应代码外。还包含[共通返回](#共通返回 "共通返回")。

| 返回码  |  描述 |
| ------------ | ------------ |
|   | [共通返回](#共通返回 "共通返回")  |
|1|请求数据集不存在|
|2|请求维度不存在|

##### 返回示例

```JSON
{
    "status": {
        "code": 0,
        "message": "数据维度请求成功"
    },
    "data": {
        "keys": [
            "南",
            "南 北"
        ]
    }
}
```

##### 返回参数说明

|参数|类型|描述|
|:-------|:-------|:-------|
| keys | object | 该维度的所有维度值 |

### 修改仪表盘尺寸

##### 简要描述

- 用以选择仪表盘内每行展示图表个数

##### 请求URL

- ` baseUrl/api/dashboard/change_size
  `

##### 请求方式

- POST

##### 参数

以下请求参数列表仅列出了接口请求参数，正式调用时需要加上公共请求参数，见[公共请求参数](#公共请求参数 "公共请求参数")页面

|参数名|必选|类型|说明|
|:----    |:---|:----- |-----   |
|公共参数 |是  | |详见： [公共请求参数](#公共请求参数 "公共请求参数")   |
|size | 是 |number |每行显示图表个数1-4|

##### 返回码

|返回码|描述|
|:----   |:---|
||   见：[共通返回](#共通返回 "共通返回")|
|1|   每行显示数量不符合限制|

##### 返回示例

```JSON
  {
    "status": {
        "code": 0,
        "message": "仪表盘尺寸修改成功"
    },
    "data": null
}
```

### 获取仪表尺寸

##### 简要描述

- 用以获取当前仪表盘内每行展示图表个数

##### 请求URL

- ` baseUrl/api/dashboard/size
  `

##### 请求方式

- POST

##### 参数

以下请求参数列表仅列出了接口请求参数，正式调用时需要加上公共请求参数，见[公共请求参数](#公共请求参数 "公共请求参数")页面

|参数名|必选|类型|说明|
|:----    |:---|:----- |-----   |
|公共参数 |是  | |详见： [公共请求参数](#公共请求参数 "公共请求参数")   |

##### 返回码

|返回码|描述|
|:----   |:---|
||   见：[共通返回](#共通返回 "共通返回")|

##### 返回示例

```JSON
{
    "status": {
        "code": 0,
        "message": "获取仪表盘尺寸成功"
    },
    "data": {
        "id": 2
    }
}
```

### 更新数据集中数据

##### 简要描述

- 更新用户数据集中数据（覆盖原数据）

##### 请求URL

- ` baseUrl/api/data/update `

##### 请求方式

- POST

##### 参数

以下请求参数列表仅列出了接口请求参数，正式调用时需要加上公共请求参数，见[公共请求参数](#公共请求参数 "公共请求参数")页面

|参数名|必选|类型|说明|
|:----    |:---|:----- |-----   |
|公共参数 |是  | |详见： [公共请求参数](#公共请求参数 "公共请求参数")   |
| id | 是 | number | 数据集主键 |
| name | 是 | string | 数据表名称 |
| description | 否 | string | 数据表描述 |
| dataSource | 是 | object | 数据集内容 |

> 将数据存入数据库时需为数据额外添加主键，且前端所传的`dataSource`中数据全部为string类型，需在后端在返回时将number类型值转换后传给前端

##### 请求示例

```JSON
{
    "id": 1,
    "name": "aaa",
    "description": "bbb",
    "dataSource": [
        {
            "ccc": "ccc1",
            "ddd": "ddd1"
        },
        {
            "ccc": "ccc2",
            "ddd": "ddd2"
        }
    ]
}
```

##### 返回码

以下请求参数列表仅列出了此接口请求响应代码，除此接口请求响应代码外。还包含共通返回。

|返回码|描述|
|:----   |:---|
||   见：[共通返回](#共通返回 "共通返回")|
|1|请求数据集不存在|
|2|插入数据列数不匹配|

##### 返回示例

```JSON
{
    "status": {
        "code": 0,
        "message": "数据集导入成功"
    },
    "data": null
}
```

<file_sep>/src/api/mock/login.js
import Mock from 'mockjs'
import md5 from 'js-md5'
import mock_data from '@/api/mock/mock_data'
import log from '@/util/logger'

Mock.mock(process.env.VUE_APP_API_BASE_URL + 'user/login', 'post', (request) => {
    log.mock(request.url, JSON.parse(request.body))
    const { login, password, salt } = JSON.parse(request.body)
    if (login === mock_data.username && password === md5(md5(mock_data.password) + salt)) {
        //登录成功
        return {
            status: {
                code: 0,
                message: '登录成功',
            },
            data: {
                token: mock_data.token,
            },
        }
    } else {
        // 登录失败
        return {
            status: {
                code: 2,
                message: '密码无效',
            },
            data: null,
        }
    }
})
<file_sep>/README.md
# bi-front

## 项目简介

一个基于 Vue.js, Antd, AntV 在线绘图前端。

特点：

- 完善的用户登录模块
- 动态路由
- 可上传数据并选择绘图维度和指标
- 自动化测试和部署 (GitLab CI)
- 支持 7 种图表（可扩展）
- 多环境变量

## 安装项目

```
npm install
```

> 如果提示安装失败，请运行`npm install -force`

### 运行测试服务器，采用`.env`中定义的设置

```
npm run serve
```

### 编译并采用`.env.develop`中定义的设置

```
npm run build-develop
```

### 编译并采用`.env.master`中定义的设置

```
npm run build-master
```

### 运行端到端测试

```
npm run test:e2e
```

### 附注

环境配置文件简介

```dotenv
NODE_ENV=production
VUE_APP_ENV=master
VUE_APP_TITLE=数据分析平台
VUE_APP_API_BASE_URL=http://bi.stager.top:8080/api/
VUE_APP_SHOW_ERROR=false
VUE_APP_MOCK=false
VUE_APP_LOG_LEVEL=1
```

|名称|值|备注|
|---|---|---|
|`NODE_ENV`| `production / development` |Node 环境变量名称|
|`VUE_APP_ENV`| `master / develop` |Vue 环境变量名称|
|`VUE_APP_TITLE`| `<string>` |项目名称|
|`VUE_APP_API_BASE_URL`| `<string>` |后端 URL|
|`VUE_APP_SHOW_ERROR`| `<boolean>` |显示未知错误信息|
|`VUE_APP_MOCK`| `<boolean>` |启用 Mock 数据|
|`VUE_APP_LOG_LEVEL`| `<int>` |日志等级，参见 `src/util/logger.js`|
<file_sep>/src/api/post/update_data.js
import axios from '@/request'

export function update_data(id, name, description, data) {
    return axios.post('data/update', {
        id: id,
        name: name,
        description: description,
        dataSource: data,
    })
}
<file_sep>/src/router/index.js
import { createRouter, createWebHistory } from 'vue-router'
import store from '@/store'
import { notification } from 'ant-design-vue'
import log from '@/util/logger'
import NProgress from 'nprogress' // 页面加载进度条
import 'nprogress/nprogress.css'

// prettier-ignore
NProgress.configure({
    easing: 'ease',         // 动画方式
    speed: 500,             // 递增进度条的速度
    showSpinner: false,     // 是否显示加载ico
    trickleSpeed: 200,      // 自动递增间隔
    minimum: 0.3,           // 初始化时的最小百分比
})

/**
 * 路由表
 * @type {[{path: string, component: (function(): Promise<*>), hidden: boolean, meta: {title: string}, name: string}, {path: string, component: (function(): Promise<*>), hidden: boolean, meta: {title: string}, name: string}, {path: string, component: (function(): Promise<*>), hidden: boolean, meta: {title: string}, name: string}, {path: string, component: (function(): Promise<*>)}]}
 */
const routes = [
    {
        path: '/login',
        name: 'login',
        component: () => import('@/components/login'),
        meta: { title: '登录' },
        hidden: true,
    },
    {
        path: '/register',
        name: 'register',
        component: () => import('@/components/register'),
        meta: { title: '注册' },
        hidden: true,
    },
    {
        path: '/verify_email',
        name: 'verify_email',
        component: () => import('@/components/verify_email'),
        meta: { title: '激活电子邮件地址' },
        hidden: true,
    },
    {
        path: '/reset_password',
        name: 'reset_password',
        component: () => import('@/components/reset_password'),
        meta: { title: '重置密码' },
        hidden: true,
    },
    {
        path: '/403',
        name: 'e403',
        component: () => import('@/components/error_pages/e403'),
        meta: { title: '403错误' },
        hidden: true,
    },
    {
        path: '/404',
        name: 'e404',
        component: () => import('@/components/error_pages/e404'),
        meta: { title: '404错误' },
        hidden: true,
    },
    {
        path: '/',
        component: () => import('@/components/layout'),
        name: 'layout',
        redirect: '/dashboard',
        meta: { role: ['user'], requireAuth: true },
        children: [
            {
                path: '/edit_user_profile',
                component: () => import('@/components/edit_user_profile'),
                name: 'edit_user_profile',
                meta: {
                    title: '编辑个人信息',
                    role: ['user'],
                    requireAuth: true,
                    hidden: true,
                },
            },
            {
                path: '/new_chart',
                component: () => import('@/components/new_chart'),
                name: 'new_chart',
                meta: {
                    title: '新建图表',
                    role: ['user'],
                    requireAuth: true,
                    hidden: true,
                },
            },
            {
                path: '/new_data',
                component: () => import('@/components/new_data'),
                name: 'new_data',
                meta: {
                    title: '新建数据',
                    role: ['user'],
                    requireAuth: true,
                    hidden: true,
                },
            },
            {
                path: '/dashboard',
                component: () => import('@/components/dashboard'),
                name: 'dashboard',
                meta: {
                    title: '仪表盘',
                    role: ['user'],
                    requireAuth: true,
                    icon: 'icon-dashboard',
                },
            },
            {
                path: '/chart_management',
                component: () => import('@/components/chart_management'),
                name: 'chart_management',
                meta: {
                    title: '图表管理',
                    role: ['user'],
                    requireAuth: true,
                    icon: 'icon-barchart',
                },
            },
            {
                path: '/edit_chart/:id',
                component: () => import('@/components/edit_chart'),
                name: 'edit_chart',
                meta: {
                    title: '编辑图表',
                    role: ['user'],
                    requireAuth: true,
                    hidden: true,
                },
            },
            {
                path: '/data_management',
                component: () => import('@/components/data_management'),
                name: 'data_management',
                meta: {
                    title: '数据管理',
                    role: ['user'],
                    requireAuth: true,
                    icon: 'icon-data',
                },
            },
            {
                path: '/data_display/:id',
                component: () => import('@/components/data_display'),
                name: 'data_display',
                meta: {
                    title: '数据展示',
                    role: ['user'],
                    requireAuth: true,
                    hidden: true,
                },
            },
            {
                path: '/about',
                component: () => import('@/components/about'),
                name: 'about',
                meta: {
                    title: '关于',
                    role: ['user'],
                    requireAuth: true,
                    hidden: true,
                },
            },
        ],
    },
    {
        path: '/:catchAll(.*)',
        redirect: '/404',
    },
]

const router = createRouter({
    history: createWebHistory(),
    routes: routes,
})

/**
 * 路由守卫
 */
router.beforeEach((to, from, next) => {
    NProgress.start()
    if (to.meta.title) {
        // 在router中设置的页面标题
        document.title = to.meta.title + ' > ' + process.env.VUE_APP_TITLE
    }
    const token = store.state.token
    const role = store.state.role
    if (to.meta.requireAuth) {
        if (!token) {
            notification['warning']({
                message: '需要登录',
                description: '正在跳转到登录页面。',
            })
            next('/login')
        }
        if (!to.meta.role || to.meta.role.indexOf(role) >= 0) {
            next()
        } else {
            // 403时清除登录信息
            if (!store.state.role) {
                store.dispatch('UPDATE_USER_INFO')
            }
            next('/403')
        }
    } else {
        if (to.path === '/login' && token && !store.state.logout) {
            next('/')
        } else {
            next()
        }
    }
})

router.afterEach((to, from) => {
    log.info(`Router: '${from.fullPath}' --> '${to.fullPath}'`)
    NProgress.done()
})
export default router
<file_sep>/src/components/error_pages/e403.vue
<template>

            <template #extra>
                放轻松
            </template>

</template>
<file_sep>/src/util/logger.js
/**
 * 带颜色标签的日志
 * YMNNs
 */

/* eslint-disable no-console */

const levels = {
    DEBUG: {
        tag: 'DEBUG',
        background: 'LightGoldenRodYellow',
        foreground: 'Black',
        level: 4,
    },
    MOCK: {
        tag: 'MOCK',
        background: 'LightGoldenRodYellow',
        foreground: 'Black',
        level: 4,
    },
    SUCCESS: {
        tag: 'SUCCESS',
        background: 'Green',
        foreground: 'White',
        level: 3,
    },
    INFO: {
        tag: 'INFO',
        background: 'Gainsboro',
        foreground: 'DimGray',
        level: 3,
    },
    TIMER: {
        tag: 'TIME',
        background: 'Tomato',
        foreground: 'White',
        level: 3,
    },
    FAIL: {
        tag: 'FAIL',
        background: 'Crimson',
        foreground: 'White',
        level: 2,
    },
    WARN: {
        tag: 'WARN',
        background: 'Orange',
        foreground: 'Black',
        level: 2,
    },
    ERROR: {
        tag: 'ERROR',
        background: 'Red',
        foreground: 'White',
        level: 1,
    },
}

import colors from 'colors'
import { values } from 'lodash-es'
colors.enable()

let _level = 2
export default {
    setDefaultLevel: (level) => {
        if ([0, 1, 2, 3, 4].indexOf(level) < 0) {
            _level = 2
        } else {
            _level = level
        }
    },
    success: (text) => {
        if (levels.SUCCESS.level <= _level) {
            console.log(
                `%c ${levels.SUCCESS.tag} `,
                `color: ${levels.SUCCESS.foreground}; background-color: ${levels.SUCCESS.background}`,
                text
            )
        }
    },
    fail: (text) => {
        if (levels.FAIL.level <= _level) {
            console.log(
                `%c ${levels.FAIL.tag} `,
                `color: ${levels.FAIL.foreground}; background-color: ${levels.FAIL.background}`,
                text
            )
        }
    },
    warn: (text) => {
        if (levels.WARN.level <= _level) {
            console.log(
                `%c ${levels.WARN.tag} `,
                `color: ${levels.WARN.foreground}; background-color: ${levels.WARN.background}`,
                text
            )
        }
    },
    debug: (text) => {
        if (levels.DEBUG.level <= _level) {
            console.log(
                `%c ${levels.DEBUG.tag} `,
                `color: ${levels.DEBUG.foreground}; background-color: ${levels.DEBUG.background}`,
                text
            )
        }
    },
    info: (text) => {
        if (levels.INFO.level <= _level) {
            console.log(
                `%c ${levels.INFO.tag} `,
                `color: ${levels.INFO.foreground}; background-color: ${levels.INFO.background}`,
                text
            )
        }
    },
    error: (text) => {
        if (levels.ERROR.level <= _level) {
            console.log(
                `%c ${levels.ERROR.tag} `,
                `color: ${levels.ERROR.foreground}; background-color: ${levels.ERROR.background}`,
                text
            )
        }
    },
    time: (text) => {
        if (levels.TIMER.level <= _level) {
            console.time(` ${levels.TIMER.tag} `.bgWhite + ` ${text}`)
        }
    },
    timeEnd: (text) => {
        if (levels.TIMER.level <= _level) {
            console.timeEnd(` ${levels.TIMER.tag} `.bgWhite + ` ${text}`)
        }
    },
    mock: (url, payload) => {
        if (levels.MOCK.level <= _level) {
            console.log(
                `%c ${levels.MOCK.tag} `,
                `color: ${levels.MOCK.foreground}; background-color: ${levels.MOCK.background}`,
                url,
                payload
            )
        }
    },
    logLevel: () => {
        const l = values(levels)
            .filter((i) => i.level <= _level)
            .reduce((previousValue, currentValue) => {
                return previousValue + `${currentValue.tag} `
            }, '已启用的日志等级: ')
        console.log(
            `%c ${levels.INFO.tag} `,
            `color: ${levels.INFO.foreground}; background-color: ${levels.INFO.background}`,
            l
        )
    },
}
<file_sep>/src/components/edit_user_profile.vue
<template>

                个人信息

此信息将显示在您的个人资料中。

                            <template #label>
                                用户名
                            </template>
                            <template #help>

用户名作为您的登录凭据之一，不会对外公开显示。

                            </template>
                             {
                                        logger.warn(_error)
                                    })
                                "
                                v-model:value="modelRef_profile.username"
                            >
                                <template #addonAfter>
                                    用户 ID: {{ modelRef_profile.id }}
                                </template>

                            <template #label>
                                电子邮件地址
                            </template>
                            <template #help v-if="email_verified">

电子邮件地址用于登录和重置密码。

                            </template>
                            <template #help v-else>

                                    请先点击验证邮件中的链接以激活您的电子邮件地址，这封邮件已经发送到
                                    {{ original_profile.email }}。
重新发送激活邮件

                            </template>
                             {
                                        logger.warn(_error)
                                    })
                                "
                                v-model:value="modelRef_profile.email"
                            />

                            <template #label>
                                昵称
                            </template>
                            <template #help>

输入你的名字，这样你认识的人就能认出你。

                            </template>
                             {
                                        logger.warn(_error)
                                    })
                                "
                                v-model:value="modelRef_profile.nickname"
                            />

                            <template #help>

                                    以上的所有字段都是可选的，可以在任何时候删除。通过填写它们，您就同意我们在您的用户信息出现的任何地方共享这些数据。请参阅我们的隐私声明，了解更多关于我们如何使用这些信息。

                            </template>
                            保存个人信息

                密码

密码更新成功后，您将被重定向到登录页面，您可以使用新密码登录。

                            <template #label>
                                当前密码
                            </template>
                            <template #help>

要更改密码，您必须提供当前密码。

                            </template>
                            <a-input-password
                                :id="dom_map.profile.current_password"
                                autocomplete
                                @blur="
                                    validate_password('<PASSWORD>').catch((_error) => {
                                        logger.warn(_error)
                                    })
                                "
                                v-model:value="modelRef_password.old_password"
                            />

                            <template #label>
                                新密码
                            </template>
                            <template #help>

新密码长度必须在8-16字符之间。

                            </template>
                            <a-input-password
                                :id="dom_map.profile.new_password"
                                autocomplete
                                @blur="
                                    validate_password('<PASSWORD>').catch((_error) => {
                                        logger.warn(_error)
                                    })
                                "
                                v-model:value="modelRef_password.new_password"
                            />

                            更新密码

                删除账户

一旦你删除了你的账户，将无法恢复。

                            <template #help>

                                    在删除账户之前，我们需要确认您对账户的所有权。

                            </template>
                            删除账户

                        <template #title>
                            你确定要这么做吗？
                        </template>

                            <template #message>注意</template>
                            <template #icon><WarningOutlined /></template>

我们将立即删除您所有的数据、图表、仪表盘，以及个人信息。

您的用户名将对{{ app_title }}上的所有人开放。

欲了解更多帮助，请阅读我们的文章“删除您的用户帐户”。

                                <template #label>
                                    您的用户名
                                </template>
                                 {
                                            logger.warn(_error)
                                        })
                                    "
                                    v-model:value="modelRef_delete.username"
                                />

                                <template #label>
                                    在下方输入“删除我的账户”以确认
                                </template>
                                 {
                                            logger.warn(_error)
                                        })
                                    "
                                    v-model:value="modelRef_delete.confirm_text"
                                />

                                <template #label>
                                    确认您的密码
                                </template>
                                 {
                                            logger.warn(_error)
                                        })
                                    "
                                    v-model:value="modelRef_delete.password"
                                />

                                确认并删除我的账户

</template>

<file_sep>/src/main.js
import log from '@/util/logger'
log.setDefaultLevel(Number.parseInt(process.env.VUE_APP_LOG_LEVEL))

import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
import { setupAntd } from '@/antd'

/** [Violation] Added non-passive event listener to a scroll-blocking 'wheel' event.
 * Consider marking event handler as 'passive' to make the page more responsive.
 * See https://www.chromestatus.com/feature/5745543795965952 */
// import 'default-passive-events'

// 本地测试使用假后端
if (process.env.VUE_APP_MOCK === 'true') {
    import('@/api/mock/user_info')
    import('@/api/mock/validate_username')
    import('@/api/mock/validate_email')
    import('@/api/mock/register')
    import('@/api/mock/request_reset_password')
    import('@/api/mock/reset_password')
    import('@/api/mock/validate_reset_password_token')
    import('@/api/mock/login')
    import('@/api/mock/edit_user_profile')
    import('@/api/mock/change_password')
    import('@/api/mock/change_table')
    import('@/api/mock/logout')
    import('@/api/mock/delete_account')
    import('@/api/mock/resend_activation_email')
    import('@/api/mock/activate_email')
    import('@/api/mock/table_content')
    import('@/api/mock/all_charts')
    import('@/api/mock/delete_chart')
    import('@/api/mock/all_tables')
    import('@/api/mock/create_chart')
    import('@/api/mock/view_chart')
    import('@/api/mock/edit_chart')
    import('@/api/mock/view_dashboard')
    import('@/api/mock/edit_dashboard')
    import('@/api/mock/create_instrument')
    import('@/api/mock/create_data')
    import('@/api/mock/delete_data')
    import('@/api/mock/get_dashboard_size')
    import('@/api/mock/change_dashboard_size')
    import('@/api/mock/get_data_keys')
    import('@/api/mock/update_data')
}

const app = createApp(App)

// 已配置按需引入，新组件需要在@/antd/index.js中导入
setupAntd(app)
app.use(store)
app.use(router)
app.mount('#app')

log.success(`Vue.js version ${app.version}`)
log.info(process.env.VUE_APP_TITLE)
log.info(`构建日期: ${process.env.BUILD_TIME}`)
log.logLevel()
<file_sep>/tests/e2e/plugins/index.js
/* eslint-disable arrow-body-style */
// https://docs.cypress.io/guides/guides/plugins-guide.html

// if you need a custom webpack configuration you can uncomment the following import
// and then use the `file:preprocessor` event
// as explained in the cypress docs
// https://docs.cypress.io/api/plugins/preprocessors-api.html#Examples

// /* eslint-disable import/no-extraneous-dependencies, global-require */
// const webpack = require('@cypress/webpack-preprocessor')
// require('dotenv').config()

function randomString(e) {
    e = e || 32
    let t = 'ABCDEFGHJKMNPQRSTWXYZabcdefhijkmnprstwxyz2345678',
        a = t.length,
        n = ''
    for (let i = 0; i < e; i++) n += t.charAt(Math.floor(Math.random() * a))
    return n
}
const _dom_map = require('../../../src/constant/dom_map')
const _mock_data = require('../../../src/api/mock/mock_data')

module.exports = (on, config) => {
    // on('file:preprocessor', webpack({
    //  webpackOptions: require('@vue/cli-service/webpack.config'),
    //  watchOptions: {}
    // }))
    return Object.assign({}, config, {
        fixturesFolder: 'tests/e2e/fixtures',
        integrationFolder: 'tests/e2e/specs',
        screenshotsFolder: 'tests/e2e/screenshots',
        videosFolder: 'tests/e2e/videos',
        supportFile: 'tests/e2e/support/index.js',

        dom_map: _dom_map,
        mock_data: _mock_data,
        dev_hostname: 'bi-develop.stager.top',
        invalid_token: randomString(8),
        valid_username: randomString(8),
        valid_username_new: randomString(8),
        username_long: randomString(20),
        invalid_username: randomString(8),
        valid_password: <PASSWORD>String(8),
        invalid_password: <PASSWORD>String(8),
        password_short: randomString(3),
        nickname: randomString(8),
        nickname_new: randomString(8),
        nickname_long: randomString(20),
        email: randomString(8) + '@' + randomString(8) + '.xx',
        email_new: randomString(8) + '@' + randomString(8) + '.xx',
        invalid_email: randomString(8),
        data_name_long: randomString(129),
        data_name: randomString(10),
        data_description_long: randomString(257),
        data_description: randomString(20),
        chart_name: randomString(20),
        chart_name_long: randomString(129),
    })
}
<file_sep>/src/api/post/create_instrument.js
import axios from '@/request'

export function create_instrument(chart_id) {
    return axios.post('dashboard/add', {
        chart_id: chart_id,
    })
}
<file_sep>/src/api/mock/get_dashboard_size.js
import Mock from 'mockjs'
import mock_data from '@/api/mock/mock_data'
import log from '@/util/logger'

Mock.mock(process.env.VUE_APP_API_BASE_URL + 'dashboard/size', 'post', (request) => {
    log.mock(request.url, JSON.parse(request.body))
    return {
        status: {
            code: 0,
            message: '仪表盘大小请求成功',
        },
        data: {
            id: mock_data.dashboard_size,
        },
    }
})
<file_sep>/src/api/post/change_dashboard_size.js
import axios from '@/request'

export function change_dashboard_size(size) {
    return axios.post('dashboard/change_size', {
        size: size,
    })
}
<file_sep>/src/components/verify_email.vue
<template>

        <template #extra>
                返回控制台
            </template>

        <template #extra>
                返回控制台
            </template>

</template>

<file_sep>/src/util/iconfont.js
export const icon_url = '//at.alicdn.com/t/font_2592763_904m8idq6h.js'

branch_name: refs/heads/master

languages: Markdown Vue JavaScript

repo_language: Markdown

repo_name: YMNNs/bi-front