Merge branch 'develop' of github.com:Tencent/tdesign-vue-next-starter into site

This commit is contained in:
Uyarn 2023-07-18 18:35:56 +08:00
commit cfe9beaa7e
22 changed files with 402 additions and 160 deletions

View File

@ -19,7 +19,7 @@
### 项目简介 ### 项目简介
TDesign Vue Next Starter 是一个基于 TDesign使用 `Vue3`、`Vite2`、`Pinia`、`TypeScript` 开发,可进行个性化主题配置,旨在提供项目开箱即用的、配置式的中后台项目。 TDesign Vue Next Starter 是一个基于 TDesign使用 `Vue3`、`Vite`、`Pinia`、`TypeScript` 开发,可进行个性化主题配置,旨在提供项目开箱即用的、配置式的中后台项目。
<p> <p>
<a href="http://tdesign.tencent.com/starter/vue-next/">在线预览</a> <a href="http://tdesign.tencent.com/starter/vue-next/">在线预览</a>

View File

@ -17,7 +17,7 @@
English | [简体中文](./README-zh_CN.md) English | [简体中文](./README-zh_CN.md)
### Introduction ### Introduction
TDesign Vue Next Starter is a TDesign-based developed with `Vue 3`, `Vite 3+`, `Pinia`, `TypeScript`. It can be customized theme configuration, and aims to provide project out-of-the-box, configuration-style middle and background projects. TDesign Vue Next Starter is a TDesign-based developed with `Vue 3`, `Vite`, `Pinia`, `TypeScript`. It can be customized theme configuration, and aims to provide project out-of-the-box, configuration-style middle and background projects.
<p> <p>
<a href="http://tdesign.tencent.com/starter/vue-next/">Live Preview</a> <a href="http://tdesign.tencent.com/starter/vue-next/">Live Preview</a>

View File

@ -1,6 +1,6 @@
{ {
"name": "@tencent/tdesign-vue-next-starter", "name": "@tencent/tdesign-vue-next-starter",
"version": "0.7.6", "version": "0.8.0",
"scripts": { "scripts": {
"dev:mock": "vite --open --mode mock", "dev:mock": "vite --open --mode mock",
"dev": "vite --open --mode development", "dev": "vite --open --mode development",
@ -12,70 +12,69 @@
"lint": "eslint --ext .vue,.js,.jsx,.ts,.tsx ./ --max-warnings 0", "lint": "eslint --ext .vue,.js,.jsx,.ts,.tsx ./ --max-warnings 0",
"lint:fix": "eslint --ext .vue,.js,jsx,.ts,.tsx ./ --max-warnings 0 --fix", "lint:fix": "eslint --ext .vue,.js,jsx,.ts,.tsx ./ --max-warnings 0 --fix",
"stylelint": "stylelint src/**/*.{html,vue,sass,less}", "stylelint": "stylelint src/**/*.{html,vue,sass,less}",
"stylelint:fix": "stylelint --fix src/**/*.{html,vue,vss,sass,less}", "stylelint:fix": "stylelint --fix src/**/*.{html,vue,css,sass,less}",
"prepare": "husky install", "prepare": "husky install",
"site:preview": "npm run build && cp -r dist _site", "site:preview": "npm run build && cp -r dist _site",
"test": "echo \"no test specified,work in process\"", "test": "echo \"no test specified,work in process\"",
"test:coverage": "echo \"no test:coverage specified,work in process\"" "test:coverage": "echo \"no test:coverage specified,work in process\""
}, },
"dependencies": { "dependencies": {
"axios": "^1.3.4", "axios": "^1.4.0",
"dayjs": "^1.11.7", "dayjs": "^1.11.9",
"echarts": "5.1.2", "echarts": "5.1.2",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"nprogress": "^0.2.0", "nprogress": "^0.2.0",
"pinia": "^2.0.33", "pinia": "^2.1.4",
"pinia-plugin-persistedstate": "^3.1.0", "pinia-plugin-persistedstate": "^3.1.0",
"qrcode.vue": "^3.3.4", "qrcode.vue": "^3.4.0",
"qs": "^6.11.1", "qs": "^6.11.2",
"tdesign-icons-vue-next": "^0.1.11", "tdesign-icons-vue-next": "^0.1.11",
"tdesign-site-components": "^0.12.9", "tdesign-site-components": "^0.12.9",
"tdesign-theme-generator": "^1.0.5", "tdesign-theme-generator": "^1.0.5",
"tdesign-vue-next": "^1.3.4", "tdesign-vue-next": "^1.3.10",
"tvision-color": "^1.6.0", "tvision-color": "^1.6.0",
"vue": "^3.2.47", "vue": "^3.3.4",
"vue-clipboard3": "^2.0.0", "vue-clipboard3": "^2.0.0",
"vue-router": "~4.1.6" "vue-router": "~4.2.3"
}, },
"devDependencies": { "devDependencies": {
"@commitlint/cli": "^17.3.0", "@commitlint/cli": "^17.6.6",
"@commitlint/config-conventional": "^17.3.0", "@commitlint/config-conventional": "^17.6.6",
"@types/echarts": "^4.9.16", "@types/echarts": "^4.9.18",
"@types/lodash": "^4.14.191", "@types/lodash": "^4.14.195",
"@types/nprogress": "^0.2.0", "@types/nprogress": "^0.2.0",
"@types/qs": "^6.9.7", "@types/qs": "^6.9.7",
"@typescript-eslint/eslint-plugin": "^5.47.1", "@typescript-eslint/eslint-plugin": "^5.61.0",
"@typescript-eslint/parser": "^5.47.1", "@typescript-eslint/parser": "^5.61.0",
"@vitejs/plugin-vue": "^3.2.0", "@vitejs/plugin-vue": "^4.2.3",
"@vitejs/plugin-vue-jsx": "^1.3.10", "@vitejs/plugin-vue-jsx": "^3.0.1",
"@vue/compiler-sfc": "^3.2.47", "@vue/compiler-sfc": "^3.3.4",
"@vue/eslint-config-typescript": "^11.0.2", "@vue/eslint-config-typescript": "^11.0.3",
"commitizen": "^4.2.4", "commitizen": "^4.3.0",
"cz-conventional-changelog": "^3.3.0", "cz-conventional-changelog": "^3.3.0",
"eslint": "^8.30.0", "eslint": "^8.44.0",
"eslint-config-airbnb-base": "^15.0.0", "eslint-config-airbnb-base": "^15.0.0",
"eslint-config-prettier": "^8.5.0", "eslint-config-prettier": "^8.8.0",
"eslint-plugin-import": "^2.26.0", "eslint-plugin-import": "^2.27.5",
"eslint-plugin-prettier": "^4.2.1", "eslint-plugin-prettier": "^4.2.1",
"eslint-plugin-simple-import-sort": "^10.0.0", "eslint-plugin-simple-import-sort": "^10.0.0",
"eslint-plugin-vue": "^9.8.0", "eslint-plugin-vue": "^9.15.1",
"eslint-plugin-vue-scoped-css": "^2.2.0", "eslint-plugin-vue-scoped-css": "^2.5.0",
"husky": "^8.0.2", "husky": "^8.0.3",
"less": "^4.1.3", "less": "^4.1.3",
"lint-staged": "^13.1.0", "lint-staged": "^13.2.3",
"mockjs": "^1.1.0", "mockjs": "^1.1.0",
"postcss-html": "^1.5.0", "postcss-html": "^1.5.0",
"postcss-less": "^6.0.0", "postcss-less": "^6.0.0",
"prettier": "^2.8.1", "prettier": "^2.8.8",
"stylelint": "~14.9.1", "stylelint": "~15.10.0",
"stylelint-config-prettier": "~9.0.4", "stylelint-config-standard": "^34.0.0",
"stylelint-less": "1.0.6", "stylelint-order": "~6.0.3",
"stylelint-order": "~6.0.1", "typescript": "~5.1.6",
"typescript": "~4.9.5", "vite": "^4.3.9",
"vite": "^3.2.5", "vite-plugin-mock": "^3.0.0",
"vite-plugin-mock": "^2.9.6",
"vite-svg-loader": "^4.0.0", "vite-svg-loader": "^4.0.0",
"vue-tsc": "^1.2.0" "vue-tsc": "^1.8.4"
}, },
"config": { "config": {
"commitizen": { "commitizen": {
@ -87,7 +86,7 @@
"prettier --write", "prettier --write",
"npm run lint:fix" "npm run lint:fix"
], ],
"*.{html,vue,vss,sass,less}": [ "*.{html,vue,css,sass,less}": [
"npm run stylelint:fix" "npm run stylelint:fix"
] ]
} }

View File

@ -1,6 +1,6 @@
<template> <template>
<router-view v-if="!isRefreshing" v-slot="{ Component }"> <router-view v-if="!isRefreshing" v-slot="{ Component }">
<transition name="fade" mode="out-in"> <transition name="fade">
<keep-alive :include="aliveViews"> <keep-alive :include="aliveViews">
<component :is="Component" /> <component :is="Component" />
</keep-alive> </keep-alive>
@ -53,6 +53,7 @@ const isRefreshing = computed(() => {
.fade-enter-active { .fade-enter-active {
transition: opacity @anim-duration-slow @anim-time-fn-easing; transition: opacity @anim-duration-slow @anim-time-fn-easing;
} }
.fade-enter, .fade-enter,
.fade-leave-to { .fade-leave-to {
opacity: 0; opacity: 0;

View File

@ -206,7 +206,6 @@ watchEffect(() => {
position: fixed; position: fixed;
bottom: 200px; bottom: 200px;
right: 0; right: 0;
transition: transform 0.3s cubic-bezier(0.7, 0.3, 0.1, 1), visibility 0.3s cubic-bezier(0.7, 0.3, 0.1, 1);
height: 40px; height: 40px;
width: 40px; width: 40px;
border-radius: 20px 0 0 20px; border-radius: 20px 0 0 20px;
@ -252,9 +251,9 @@ watchEffect(() => {
.setting-group-title { .setting-group-title {
font-size: 14px; font-size: 14px;
line-height: 22px; line-height: 22px;
margin: 32px 0 24px 0; margin: 32px 0 24px;
text-align: left; text-align: left;
font-family: PingFang SC; font-family: 'PingFang SC', var(--td-font-family);
font-style: normal; font-style: normal;
font-weight: 500; font-weight: 500;
color: var(--td-text-color-primary); color: var(--td-text-color-primary);
@ -283,6 +282,7 @@ watchEffect(() => {
.setting-container { .setting-container {
padding-bottom: 100px; padding-bottom: 100px;
} }
.t-radio-group.t-size-m { .t-radio-group.t-size-m {
min-height: 32px; min-height: 32px;
width: 100%; width: 100%;
@ -306,6 +306,7 @@ watchEffect(() => {
padding: 8px; padding: 8px;
border-radius: var(--td-radius-default); border-radius: var(--td-radius-default);
border: 2px solid var(--td-component-border); border: 2px solid var(--td-component-border);
> .t-radio-button__label { > .t-radio-button__label {
display: inline-flex; display: inline-flex;
} }

View File

@ -15,7 +15,6 @@
</template> </template>
<div <div
id="monitorContainer" id="monitorContainer"
ref="monitorContainer"
class="dashboard-chart-container" class="dashboard-chart-container"
:style="{ width: '100%', height: `${resizeTime * 326}px` }" :style="{ width: '100%', height: `${resizeTime * 326}px` }"
/> />
@ -25,7 +24,6 @@
<t-card title="销售渠道" :subtitle="currentMonth" class="dashboard-chart-card" :bordered="false"> <t-card title="销售渠道" :subtitle="currentMonth" class="dashboard-chart-card" :bordered="false">
<div <div
id="countContainer" id="countContainer"
ref="countContainer"
:style="{ width: `${resizeTime * 326}px`, height: `${resizeTime * 326}px`, margin: '0 auto' }" :style="{ width: `${resizeTime * 326}px`, height: `${resizeTime * 326}px`, margin: '0 auto' }"
class="dashboard-chart-container" class="dashboard-chart-container"
/> />

View File

@ -17,12 +17,7 @@
@change="onStokeDataChange" @change="onStokeDataChange"
/> />
</template> </template>
<div <div id="stokeContainer" style="width: 100%; height: 351px" class="dashboard-chart-container"></div>
id="stokeContainer"
ref="stokeContainer"
style="width: 100%; height: 351px"
class="dashboard-chart-container"
></div>
</t-card> </t-card>
</t-col> </t-col>
<t-col :xs="12" :xl="3"> <t-col :xs="12" :xl="3">

View File

@ -20,7 +20,7 @@
</span> </span>
</template> </template>
<template #operation="slotProps"> <template #operation="slotProps">
<a class="t-button-link" @click="rehandleClickOp(slotProps)">详情</a> <t-link theme="primary" @click="rehandleClickOp(slotProps)">详情</t-link>
</template> </template>
</t-table> </t-table>
</t-card> </t-card>
@ -43,7 +43,7 @@
<trend :type="row.growUp > 0 ? 'up' : 'down'" :describe="Math.abs(row.growUp)" /> <trend :type="row.growUp > 0 ? 'up' : 'down'" :describe="Math.abs(row.growUp)" />
</template> </template>
<template #operation="slotProps"> <template #operation="slotProps">
<a class="t-button-link" @click="rehandleClickOp(slotProps)">详情</a> <t-link theme="primary" @click="rehandleClickOp(slotProps)">详情</t-link>
</template> </template>
</t-table> </t-table>
</t-card> </t-card>

View File

@ -87,8 +87,10 @@
</template> </template>
<template #op="slotProps"> <template #op="slotProps">
<a :class="prefix + '-link'" @click="listClick()">管理</a> <t-space>
<a :class="prefix + '-link'" @click="deleteClickOp(slotProps)">删除</a> <t-link theme="primary" @click="listClick()">管理</t-link>
<t-link theme="danger" @click="deleteClickOp(slotProps)">删除</t-link>
</t-space>
</template> </template>
<template #op-column> <template #op-column>
@ -128,7 +130,6 @@ export default {
import { onMounted, ref } from 'vue'; import { onMounted, ref } from 'vue';
import { getPurchaseList } from '@/api/detail'; import { getPurchaseList } from '@/api/detail';
import { prefix } from '@/config/global';
import Product from './components/Product.vue'; import Product from './components/Product.vue';
import { BASE_INFO_DATA, PRODUCT_LIST, TABLE_COLUMNS_DATA as columns } from './constants'; import { BASE_INFO_DATA, PRODUCT_LIST, TABLE_COLUMNS_DATA as columns } from './constants';
@ -188,5 +189,5 @@ const onConfirm = () => {
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>
@import url('./index.less'); @import './index.less';
</style> </style>

View File

@ -40,8 +40,10 @@
</span> </span>
</template> </template>
<template #op="slotProps"> <template #op="slotProps">
<a :class="prefix + '-link'" @click="listClick()">管理</a> <t-space>
<a :class="prefix + '-link'" @click="deleteClickOp(slotProps)">删除</a> <t-link theme="primary" @click="listClick()">管理</t-link>
<t-link theme="danger" @click="deleteClickOp(slotProps)">删除</t-link>
</t-space>
</template> </template>
<template #op-column> <template #op-column>
<t-icon name="descending-order" /> <t-icon name="descending-order" />
@ -84,7 +86,6 @@ import { CanvasRenderer } from 'echarts/renderers';
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'; import { computed, onMounted, onUnmounted, ref, watch } from 'vue';
import { getProjectList } from '@/api/detail'; import { getProjectList } from '@/api/detail';
import { prefix } from '@/config/global';
import { useSettingStore } from '@/store'; import { useSettingStore } from '@/store';
import { changeChartsTheme } from '@/utils/color'; import { changeChartsTheme } from '@/utils/color';
@ -197,7 +198,7 @@ const deleteClickOp = (e: { rowIndex: number }) => {
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>
@import url('../base/index.less'); @import '../base/index.less';
.detail-deploy { .detail-deploy {
:deep(.t-card) { :deep(.t-card) {

View File

@ -51,8 +51,10 @@
</template> </template>
<template #op="slotProps"> <template #op="slotProps">
<a class="t-button-link" @click="handleClickDetail()">详情</a> <t-space>
<a class="t-button-link" @click="handleClickDelete(slotProps)">删除</a> <t-link theme="primary" @click="handleClickDetail()">详情</t-link>
<t-link theme="danger" @click="handleClickDelete(slotProps)">删除</t-link>
</t-space>
</template> </template>
</t-table> </t-table>
</t-card> </t-card>

View File

@ -22,6 +22,7 @@
class="form-item-content" class="form-item-content"
:options="CONTRACT_STATUS_OPTIONS" :options="CONTRACT_STATUS_OPTIONS"
placeholder="请选择合同状态" placeholder="请选择合同状态"
clearable
/> />
</t-form-item> </t-form-item>
</t-col> </t-col>
@ -43,6 +44,7 @@
class="form-item-content" class="form-item-content"
:options="CONTRACT_TYPE_OPTIONS" :options="CONTRACT_TYPE_OPTIONS"
placeholder="请选择合同类型" placeholder="请选择合同类型"
clearable
/> />
</t-form-item> </t-form-item>
</t-col> </t-col>
@ -90,8 +92,10 @@
</p> </p>
</template> </template>
<template #op="slotProps"> <template #op="slotProps">
<a class="t-button-link" @click="rehandleClickOp(slotProps)">管理</a> <t-space>
<a class="t-button-link" @click="handleClickDelete(slotProps)">删除</a> <t-link theme="primary" @click="rehandleClickOp(slotProps)">管理</t-link>
<t-link theme="danger" @click="handleClickDelete(slotProps)">删除</t-link>
</t-space>
</template> </template>
</t-table> </t-table>
<t-dialog <t-dialog
@ -120,6 +124,13 @@ import {
} from '@/constants'; } from '@/constants';
import { useSettingStore } from '@/store'; import { useSettingStore } from '@/store';
interface FormData {
name: string;
no: string;
status?: number;
type: string;
}
const store = useSettingStore(); const store = useSettingStore();
const COLUMNS: PrimaryTableCol[] = [ const COLUMNS: PrimaryTableCol[] = [
@ -168,11 +179,10 @@ const COLUMNS: PrimaryTableCol[] = [
const searchForm = { const searchForm = {
name: '', name: '',
no: '', no: '',
status: typeof CONTRACT_STATUS,
type: '', type: '',
}; };
const formData = ref({ ...searchForm }); const formData = ref<FormData>({ ...searchForm });
const rowKey = 'index'; const rowKey = 'index';
const verticalAlign = 'top' as const; const verticalAlign = 'top' as const;
const hover = true; const hover = true;
@ -242,6 +252,7 @@ const onReset = (val: unknown) => {
}; };
const onSubmit = (val: unknown) => { const onSubmit = (val: unknown) => {
console.log(val); console.log(val);
console.log(formData.value);
}; };
const rehandlePageChange = (pageInfo: PageInfo, newDataSource: TableRowData[]) => { const rehandlePageChange = (pageInfo: PageInfo, newDataSource: TableRowData[]) => {
console.log('分页变化', pageInfo, newDataSource); console.log('分页变化', pageInfo, newDataSource);
@ -281,6 +292,7 @@ const headerAffixedTop = computed(
display: flex; display: flex;
justify-content: flex-end; justify-content: flex-end;
align-items: center; align-items: center;
.expand { .expand {
.t-button__text { .t-button__text {
display: flex; display: flex;

View File

@ -37,9 +37,13 @@ export const usePermissionStore = defineStore('permission', {
} }
}, },
async restoreRoutes() { async restoreRoutes() {
this.removeRoutes.forEach((item: RouteRecordRaw) => { // 不需要在此额外调用initRoutes更新侧边导肮内容在登录后asyncRoutes为空会调用
router.addRoute(item); this.asyncRoutes.forEach((item: RouteRecordRaw) => {
if (item.name) {
router.removeRoute(item.name);
}
}); });
this.asyncRoutes = [];
}, },
}, },
}); });

View File

@ -45,7 +45,8 @@
} }
.t-menu--dark .t-menu__operations .t-icon { .t-menu--dark .t-menu__operations .t-icon {
color: rgba(255, 255, 255, 0.55); color: rgba(255, 255, 255, 55%);
&:hover { &:hover {
cursor: pointer; cursor: pointer;
} }
@ -81,6 +82,7 @@
&-layout { &-layout {
height: calc(100vh - var(--td-comp-size-xxxl)); height: calc(100vh - var(--td-comp-size-xxxl));
overflow-y: scroll; overflow-y: scroll;
&-tabs-nav { &-tabs-nav {
max-width: 100%; max-width: 100%;
position: fixed; position: fixed;
@ -153,6 +155,7 @@
align-items: center; align-items: center;
justify-content: center; justify-content: center;
width: 100%; width: 100%;
&:hover { &:hover {
cursor: pointer; cursor: pointer;
} }

View File

@ -1,8 +1,8 @@
// 对部分样式进行重置 // 对部分样式进行重置
body { body {
color: var(--td-text-color-secondary); color: var(--td-text-color-secondary);
font-family: -apple-system, BlinkMacSystemFont, var(--td-font-family);
font: var(--td-font-body-medium); font: var(--td-font-body-medium);
font-family: -apple-system, BlinkMacSystemFont, var(--td-font-family);
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
padding: 0; padding: 0;
margin: 0; margin: 0;
@ -37,34 +37,3 @@ p {
box-sizing: border-box; box-sizing: border-box;
} }
.t-button-link,
a {
color: var(--td-brand-color);
text-decoration: none;
cursor: pointer;
transition: color @anim-duration-base @anim-time-fn-easing;
&:hover {
color: var(--td-brand-color-hover);
}
&:active {
color: var(--td-brand-color-active);
}
&--active {
color: var(--td-brand-color-active);
}
&:focus {
text-decoration: none;
}
}
.t-button-link {
margin-right: var(--td-comp-margin-xxl);
&:last-child {
margin-right: 0;
}
}

75
src/types/axios.d.ts vendored
View File

@ -1,18 +1,89 @@
import { AxiosRequestConfig } from 'axios'; import type { AxiosRequestConfig } from 'axios';
/**
* Axios请求配置
*/
export interface RequestOptions { export interface RequestOptions {
/**
*
*
* : http://www.baidu.com/api
*/
apiUrl?: string; apiUrl?: string;
/**
*
*
* : http://www.baidu.com/api
* urlPrefix: 'api'
*/
isJoinPrefix?: boolean; isJoinPrefix?: boolean;
/**
*
*/
urlPrefix?: string; urlPrefix?: string;
/**
* POST请求的时候添加参数到Url中
*/
joinParamsToUrl?: boolean; joinParamsToUrl?: boolean;
/**
*
*/
formatDate?: boolean; formatDate?: boolean;
/**
*
*/
isTransformResponse?: boolean; isTransformResponse?: boolean;
/**
*
*
* : 需要获取响应头时使用该属性
*/
isReturnNativeResponse?: boolean; isReturnNativeResponse?: boolean;
ignoreRepeatRequest?: boolean; /**
*
*
*
*
*
*/
ignoreCancelToken?: boolean;
/**
*
*/
joinTime?: boolean; joinTime?: boolean;
/**
* Token
*/
withToken?: boolean; withToken?: boolean;
/**
*
*/
retry?: { retry?: {
/**
*
*/
count: number; count: number;
/**
*
*
* 单位: 毫秒
*/
delay: number;
};
/**
*
*
* 单位: 毫秒
*/
throttle?: {
delay: number;
};
/**
*
*
* 单位: 毫秒
*/
debounce?: {
delay: number; delay: number;
}; };
} }

View File

@ -1,6 +1,15 @@
import axios, { AxiosError, AxiosInstance, AxiosRequestConfig, AxiosResponse, InternalAxiosRequestConfig } from 'axios'; import axios, {
AxiosError,
AxiosInstance,
AxiosRequestConfig,
AxiosRequestHeaders,
AxiosResponse,
InternalAxiosRequestConfig,
} from 'axios';
import cloneDeep from 'lodash/cloneDeep'; import cloneDeep from 'lodash/cloneDeep';
import debounce from 'lodash/debounce';
import isFunction from 'lodash/isFunction'; import isFunction from 'lodash/isFunction';
import throttle from 'lodash/throttle';
import { stringify } from 'qs'; import { stringify } from 'qs';
import { ContentTypeEnum } from '@/constants'; import { ContentTypeEnum } from '@/constants';
@ -9,12 +18,20 @@ import { AxiosRequestConfigRetry, RequestOptions, Result } from '@/types/axios';
import { AxiosCanceler } from './AxiosCancel'; import { AxiosCanceler } from './AxiosCancel';
import { CreateAxiosOptions } from './AxiosTransform'; import { CreateAxiosOptions } from './AxiosTransform';
// Axios模块 /**
* Axios
*/
export class VAxios { export class VAxios {
// axios句柄 /**
* Axios实例句柄
* @private
*/
private instance: AxiosInstance; private instance: AxiosInstance;
// axios选项 /**
* Axios配置
* @private
*/
private readonly options: CreateAxiosOptions; private readonly options: CreateAxiosOptions;
constructor(options: CreateAxiosOptions) { constructor(options: CreateAxiosOptions) {
@ -23,57 +40,71 @@ export class VAxios {
this.setupInterceptors(); this.setupInterceptors();
} }
// 创建axios句柄 /**
* Axios实例
* @param config
* @private
*/
private createAxios(config: CreateAxiosOptions): void { private createAxios(config: CreateAxiosOptions): void {
this.instance = axios.create(config); this.instance = axios.create(config);
} }
// 获取数据处理 /**
*
* @private
*/
private getTransform() { private getTransform() {
const { transform } = this.options; const { transform } = this.options;
return transform; return transform;
} }
// 获取句柄 /**
* Axios实例
*/
getAxios(): AxiosInstance { getAxios(): AxiosInstance {
return this.instance; return this.instance;
} }
// 配置 axios /**
* Axios
* @param config
*/
configAxios(config: CreateAxiosOptions) { configAxios(config: CreateAxiosOptions) {
if (!this.instance) { if (!this.instance) return;
return;
}
this.createAxios(config); this.createAxios(config);
} }
// 设置通用头信息 /**
*
* @param headers
*/
setHeader(headers: Record<string, string>): void { setHeader(headers: Record<string, string>): void {
if (!this.instance) { if (!this.instance) return;
return;
}
Object.assign(this.instance.defaults.headers, headers); Object.assign(this.instance.defaults.headers, headers);
} }
// 设置拦截器 /**
*
* @private
*/
private setupInterceptors() { private setupInterceptors() {
const transform = this.getTransform(); const transform = this.getTransform();
if (!transform) { if (!transform) return;
return;
}
const { requestInterceptors, requestInterceptorsCatch, responseInterceptors, responseInterceptorsCatch } = const { requestInterceptors, requestInterceptorsCatch, responseInterceptors, responseInterceptorsCatch } =
transform; transform;
const axiosCanceler = new AxiosCanceler(); const axiosCanceler = new AxiosCanceler();
// 请求配置处理 // 请求拦截器
this.instance.interceptors.request.use((config: InternalAxiosRequestConfig) => { this.instance.interceptors.request.use((config: InternalAxiosRequestConfig) => {
// 如果忽略取消令牌,则不会取消重复的请求
// @ts-ignore // @ts-ignore
const { ignoreRepeatRequest } = config.requestOptions; const { ignoreCancelToken } = config.requestOptions;
const ignoreRepeat = ignoreRepeatRequest ?? this.options.requestOptions?.ignoreRepeatRequest; const ignoreCancel = ignoreCancelToken ?? this.options.requestOptions?.ignoreCancelToken;
if (!ignoreRepeat) axiosCanceler.addPending(config); if (!ignoreCancel) axiosCanceler.addPending(config);
if (requestInterceptors && isFunction(requestInterceptors)) { if (requestInterceptors && isFunction(requestInterceptors)) {
config = requestInterceptors(config, this.options); config = requestInterceptors(config, this.options) as InternalAxiosRequestConfig;
} }
return config; return config;
@ -99,9 +130,12 @@ export class VAxios {
} }
} }
// 支持Form Data /**
* FormData
* @param config
*/
supportFormData(config: AxiosRequestConfig) { supportFormData(config: AxiosRequestConfig) {
const headers = config.headers || this.options.headers; const headers = config.headers || (this.options.headers as AxiosRequestHeaders);
const contentType = headers?.['Content-Type'] || headers?.['content-type']; const contentType = headers?.['Content-Type'] || headers?.['content-type'];
if ( if (
@ -118,6 +152,24 @@ export class VAxios {
}; };
} }
/**
* params
* @param config
*/
supportParamsStringify(config: AxiosRequestConfig) {
const headers = config.headers || this.options.headers;
const contentType = headers?.['Content-Type'] || headers?.['content-type'];
if (contentType === ContentTypeEnum.FormURLEncoded || !Reflect.has(config, 'params')) {
return config;
}
return {
...config,
paramsSerializer: (params: any) => stringify(params, { arrayFormat: 'brackets' }),
};
}
get<T = any>(config: AxiosRequestConfig, options?: RequestOptions): Promise<T> { get<T = any>(config: AxiosRequestConfig, options?: RequestOptions): Promise<T> {
return this.request({ ...config, method: 'GET' }, options); return this.request({ ...config, method: 'GET' }, options);
} }
@ -138,8 +190,62 @@ export class VAxios {
return this.request({ ...config, method: 'PATCH' }, options); return this.request({ ...config, method: 'PATCH' }, options);
} }
// 请求 /**
async request<T = any>(config: AxiosRequestConfigRetry, options?: RequestOptions): Promise<T> { *
* @param key key
* @param file
* @param config
* @param options
*/
upload<T = any>(key: string, file: File, config: AxiosRequestConfig, options?: RequestOptions): Promise<T> {
const params: FormData = config.params ?? new FormData();
params.append(key, file);
return this.request(
{
...config,
method: 'POST',
headers: {
'Content-Type': ContentTypeEnum.FormData,
},
params,
},
options,
);
}
/**
*
* @param config
* @param options
*/
request<T = any>(config: AxiosRequestConfigRetry, options?: RequestOptions): Promise<T> {
const { requestOptions } = this.options;
if (requestOptions.throttle !== undefined && requestOptions.debounce !== undefined) {
throw new Error('throttle and debounce cannot be set at the same time');
}
if (requestOptions.throttle && requestOptions.throttle.delay !== 0) {
return new Promise((resolve) => {
throttle(() => resolve(this.synthesisRequest(config, options)), requestOptions.throttle.delay);
});
}
if (requestOptions.debounce && requestOptions.debounce.delay !== 0) {
return new Promise((resolve) => {
debounce(() => resolve(this.synthesisRequest(config, options)), requestOptions.debounce.delay);
});
}
return this.synthesisRequest(config, options);
}
/**
*
* @private
*/
private async synthesisRequest<T = any>(config: AxiosRequestConfigRetry, options?: RequestOptions): Promise<T> {
let conf: CreateAxiosOptions = cloneDeep(config); let conf: CreateAxiosOptions = cloneDeep(config);
const transform = this.getTransform(); const transform = this.getTransform();
@ -154,6 +260,8 @@ export class VAxios {
conf.requestOptions = opt; conf.requestOptions = opt;
conf = this.supportFormData(conf); conf = this.supportFormData(conf);
// 支持params数组参数格式化因axios默认的toFormData即为brackets方式无需配置paramsSerializer为qs有需要可解除注释参数参考qs文档
// conf = this.supportParamsStringify(conf);
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
this.instance this.instance

View File

@ -5,10 +5,20 @@ import isFunction from 'lodash/isFunction';
// 存储请求与取消令牌的键值对列表 // 存储请求与取消令牌的键值对列表
let pendingMap = new Map<string, Canceler>(); let pendingMap = new Map<string, Canceler>();
/**
* Url
* @param config
*/
export const getPendingUrl = (config: AxiosRequestConfig) => [config.method, config.url].join('&'); export const getPendingUrl = (config: AxiosRequestConfig) => [config.method, config.url].join('&');
/**
* @description
*/
export class AxiosCanceler { export class AxiosCanceler {
// 添加请求到列表 /**
*
* @param config
*/
addPending(config: AxiosRequestConfig) { addPending(config: AxiosRequestConfig) {
this.removePending(config); this.removePending(config);
const url = getPendingUrl(config); const url = getPendingUrl(config);
@ -22,7 +32,9 @@ export class AxiosCanceler {
}); });
} }
// 清空所有请求 /**
*
*/
removeAllPending() { removeAllPending() {
pendingMap.forEach((cancel) => { pendingMap.forEach((cancel) => {
if (cancel && isFunction(cancel)) cancel(); if (cancel && isFunction(cancel)) cancel();
@ -30,7 +42,10 @@ export class AxiosCanceler {
pendingMap.clear(); pendingMap.clear();
} }
// 移除某个请求 /**
*
* @param config
*/
removePending(config: AxiosRequestConfig) { removePending(config: AxiosRequestConfig) {
const url = getPendingUrl(config); const url = getPendingUrl(config);
@ -43,6 +58,9 @@ export class AxiosCanceler {
} }
} }
/**
*
*/
reset() { reset() {
pendingMap = new Map<string, Canceler>(); pendingMap = new Map<string, Canceler>();
} }

View File

@ -1,38 +1,64 @@
import type { AxiosInstance, AxiosRequestConfig, AxiosResponse, InternalAxiosRequestConfig } from 'axios'; import type { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios';
import { AxiosError } from 'axios'; import { AxiosError } from 'axios';
import type { RequestOptions, Result } from '@/types/axios'; import type { RequestOptions, Result } from '@/types/axios';
// 创建Axios选项 /**
* @description Axios实例配置
*/
export interface CreateAxiosOptions extends AxiosRequestConfig { export interface CreateAxiosOptions extends AxiosRequestConfig {
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication#authentication_schemes /**
*
*
* https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication#authentication_schemes
*/
authenticationScheme?: string; authenticationScheme?: string;
// 数据处理 /**
*
*/
transform?: AxiosTransform; transform?: AxiosTransform;
// 请求选项 /**
*
*/
requestOptions?: RequestOptions; requestOptions?: RequestOptions;
} }
// Axios 数据处理 /**
* Axios请求数据处理
*/
export abstract class AxiosTransform { export abstract class AxiosTransform {
// 请求前Hook /**
*
*/
beforeRequestHook?: (config: AxiosRequestConfig, options: RequestOptions) => AxiosRequestConfig; beforeRequestHook?: (config: AxiosRequestConfig, options: RequestOptions) => AxiosRequestConfig;
// 转换前Hook /**
transformRequestHook?: (res: AxiosResponse<Result>, options: RequestOptions) => any; *
*/
transformRequestHook?: <T = any>(res: AxiosResponse<Result>, options: RequestOptions) => T;
// 请求失败处理 /**
requestCatchHook?: (e: Error | AxiosError, options: RequestOptions) => Promise<any>; *
*/
requestCatchHook?: <T = any>(e: Error | AxiosError, options: RequestOptions) => Promise<T>;
// 请求前的拦截器 /**
requestInterceptors?: (config: AxiosRequestConfig, options: CreateAxiosOptions) => InternalAxiosRequestConfig; *
*/
requestInterceptors?: (config: AxiosRequestConfig, options: CreateAxiosOptions) => AxiosRequestConfig;
// 请求后的拦截器 /**
*
*/
responseInterceptors?: (res: AxiosResponse) => AxiosResponse; responseInterceptors?: (res: AxiosResponse) => AxiosResponse;
// 请求前的拦截器错误处理 /**
*
*/
requestInterceptorsCatch?: (error: AxiosError) => void; requestInterceptorsCatch?: (error: AxiosError) => void;
// 请求后的拦截器错误处理 /**
*
*/
responseInterceptorsCatch?: (error: AxiosError, instance: AxiosInstance) => void; responseInterceptorsCatch?: (error: AxiosError, instance: AxiosInstance) => void;
} }

View File

@ -1,5 +1,5 @@
// axios配置 可自行根据项目进行更改,只需更改该文件即可,其他文件可以不动 // axios配置 可自行根据项目进行更改,只需更改该文件即可,其他文件可以不动
import type { AxiosInstance, InternalAxiosRequestConfig } from 'axios'; import type { AxiosInstance } from 'axios';
import isString from 'lodash/isString'; import isString from 'lodash/isString';
import merge from 'lodash/merge'; import merge from 'lodash/merge';
@ -23,7 +23,7 @@ const transform: AxiosTransform = {
// 如果204无内容直接返回 // 如果204无内容直接返回
const method = res.config.method?.toLowerCase(); const method = res.config.method?.toLowerCase();
if (res.status === 204 || method === 'put' || method === 'patch') { if (res.status === 204 && ['put', 'patch', 'delete'].includes(method)) {
return res; return res;
} }
@ -122,7 +122,7 @@ const transform: AxiosTransform = {
? `${options.authenticationScheme} ${token}` ? `${options.authenticationScheme} ${token}`
: token; : token;
} }
return config as InternalAxiosRequestConfig; return config;
}, },
// 响应拦截器处理 // 响应拦截器处理
@ -186,8 +186,10 @@ function createAxios(opt?: Partial<CreateAxiosOptions>) {
formatDate: true, formatDate: true,
// 是否加入时间戳 // 是否加入时间戳
joinTime: true, joinTime: true,
// 忽略重复请求 // 是否忽略请求取消令牌
ignoreRepeatRequest: true, // 如果启用,则重复请求时不进行处理
// 如果禁用,则重复请求时会取消当前请求
ignoreCancelToken: true,
// 是否携带token // 是否携带token
withToken: true, withToken: true,
// 重试 // 重试

View File

@ -1,7 +1,38 @@
module.exports = { module.exports = {
defaultSeverity: 'error', defaultSeverity: 'error',
extends: ['stylelint-config-prettier'], extends: ['stylelint-config-standard'],
plugins: ['stylelint-less'], rules: {
'no-duplicate-selectors': null,
'block-no-empty': null,
'selector-class-pattern': null,
'declaration-block-no-redundant-longhand-properties': [true, { ignoreShorthands: ['/flex/'] }],
'custom-property-pattern': null,
'keyframes-name-pattern': null,
'no-empty-source': null,
'font-family-no-missing-generic-family-keyword': [
true,
{
ignoreFontFamilies: ['PingFangSC-Regular', 'PingFangSC-Medium', 't'],
},
],
'unit-no-unknown': [true, { ignoreUnits: ['rpx'] }],
'function-url-quotes': null,
'max-line-length': null,
'at-rule-empty-line-before': ['always', { ignore: ['after-comment'] }],
'declaration-colon-newline-after': null,
'no-descending-specificity': null,
'selector-type-no-unknown': null,
'color-function-notation': 'legacy',
'value-keyword-case': null,
'property-no-unknown': [true, { checkPrefixed: true }],
'import-notation': 'string',
'selector-pseudo-class-no-unknown': [
true,
{
ignorePseudoClasses: ['deep'],
},
],
},
overrides: [ overrides: [
{ {
files: ['**/*.html', '**/*.vue'], files: ['**/*.html', '**/*.vue'],

View File

@ -35,7 +35,7 @@ export default ({ mode }: ConfigEnv): UserConfig => {
vueJsx(), vueJsx(),
viteMockServe({ viteMockServe({
mockPath: 'mock', mockPath: 'mock',
localEnabled: true, enable: true,
}), }),
svgLoader(), svgLoader(),
], ],