feat: 首次提交
|
|
@ -0,0 +1,43 @@
|
|||
# ============================================================
|
||||
# 项目配置文件
|
||||
# ============================================================
|
||||
|
||||
# 网站标题
|
||||
VITE_APP_TITLE = 应用管理系统
|
||||
|
||||
# 网站副标题
|
||||
VITE_APP_SUBTITLE = 快速开发web应用的模板工具
|
||||
|
||||
# Axios基本URL
|
||||
VITE_APP_API_BASE_URL = /api
|
||||
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 开发设置
|
||||
# ============================================================
|
||||
|
||||
# API接口地址(开发环境)
|
||||
VITE_API_BASE_URL = http://127.0.0.1:3030
|
||||
|
||||
# API代理地址(开发环境)
|
||||
VITE_API_PROXY_URL = /api
|
||||
|
||||
# API文档地址(开发环境) 备注:需为openapi规范的json文件
|
||||
# VITE_API_DOCS_URL = http://127.0.0.1:3030/openapi-json
|
||||
VITE_API_DOCS_URL = https://petstore.swagger.io/v2/swagger.json
|
||||
|
||||
# 端口号(开发环境)
|
||||
VITE_DEV_PORT = 3020
|
||||
|
||||
# 主机地址(开发环境)
|
||||
VITE_DEV_HOST = 0.0.0.0
|
||||
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 构建设置
|
||||
# ============================================================
|
||||
|
||||
# 构建时加载的文件后缀. 例如:设置为todo则会首先尝试加载index.todo.vue文件,不存在时再加载index.vue文件
|
||||
VITE_BUILD_EXTENSION = todo
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
# 工作流名称,可自定义
|
||||
name: 自动部署
|
||||
|
||||
# 事件监听,决定什么时候触发该工作流内的任务
|
||||
on:
|
||||
# 在master分支推动到github时触发
|
||||
push:
|
||||
branches: [ master ]
|
||||
|
||||
# 任务集合,可包含多个任务
|
||||
jobs:
|
||||
# 任务名称
|
||||
build:
|
||||
# 运行的操作系统
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
# 步骤集合,可包含多个步骤
|
||||
steps:
|
||||
# 单个步骤,没有名称,直接使用一个action
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
# 单个步骤,带有名称,带有参数
|
||||
- name: build and deploy
|
||||
run: |
|
||||
npm install
|
||||
npm run build
|
||||
cd dist
|
||||
git config --global user.name "juetan"
|
||||
git config --global user.email "810335188@qq.com"
|
||||
git init
|
||||
git add -A
|
||||
git commit -m "Build through github action"
|
||||
git push -f "https://${{ github.actor }}:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }}.git" master:gh-pages
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
!.vscode/*.code-snippets
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
registry=https://registry.npmmirror.com/
|
||||
public-hoist-pattern[]=@vue/runtime-core
|
||||
|
|
@ -0,0 +1,98 @@
|
|||
{
|
||||
"New useform": {
|
||||
"prefix": "guseform",
|
||||
"description": "useForm code",
|
||||
"body": [
|
||||
"const ${1:form} = useForm({",
|
||||
" model: {},",
|
||||
" items: [",
|
||||
" {",
|
||||
" field: '${2:name}',",
|
||||
" label: '${3:label}',",
|
||||
" type: '${4:input}',",
|
||||
" },",
|
||||
" ${0}",
|
||||
" ],",
|
||||
" submit: async ({ model }) => {",
|
||||
" console.log(model);",
|
||||
" },",
|
||||
"});"
|
||||
]
|
||||
},
|
||||
"New <Table /> Component": {
|
||||
"scope": "vue,vue-html,html",
|
||||
"prefix": "gtable",
|
||||
"description": "Table Component Code",
|
||||
"body": ["<Table ref=\"${1:table}\" v-bind=\"${2:table}\"></Table>"]
|
||||
},
|
||||
"New Table Column": {
|
||||
"prefix": "gcolumn",
|
||||
"description": "Table Column Code",
|
||||
"body": ["{", " title: \"${1}\",", " dataIndex: \"${2}\",", " ${3}", "},", "${0}"]
|
||||
},
|
||||
"New Item": {
|
||||
"prefix": "gitem",
|
||||
"description": "创建新表单元素",
|
||||
"body": ["{", " field: \"${1}\",", " label: \"${2}\",", " type: \"${3:input}\",", " ${4}", "},", "${0}"]
|
||||
},
|
||||
"New Crud Page": {
|
||||
"prefix": "gusetable",
|
||||
"isFileTemplate": true,
|
||||
"body": [
|
||||
"<template>",
|
||||
" <BreadPage>",
|
||||
" <Table ref=\"tableRef\" v-bind=\"table\"></Table>",
|
||||
" </BreadPage>",
|
||||
"</template>",
|
||||
"",
|
||||
"<script setup lang=\"tsx\">",
|
||||
"const table = useTable({",
|
||||
" data: async (model, paging) => {",
|
||||
" ${1}",
|
||||
" },",
|
||||
" columns: [",
|
||||
" {",
|
||||
" type: 'index'",
|
||||
" },",
|
||||
" ${2}",
|
||||
" ],",
|
||||
" common: {",
|
||||
" model: {",
|
||||
" ${3}",
|
||||
" },",
|
||||
" items: [",
|
||||
" ${4}",
|
||||
" ],",
|
||||
" modalProps: {",
|
||||
" width: 772,",
|
||||
" maskClosable: false,",
|
||||
" },",
|
||||
" formProps: {",
|
||||
" layout: \"vertical\",",
|
||||
" class: \"!grid grid-cols-2 gap-x-3\",",
|
||||
" },",
|
||||
" },",
|
||||
" search: {",
|
||||
" items: [",
|
||||
" ${5}",
|
||||
" ],",
|
||||
" },",
|
||||
" create: {",
|
||||
" title: \"新增${6}\",",
|
||||
" submit: ({ model }) => {",
|
||||
" ${7}",
|
||||
" },",
|
||||
" },",
|
||||
" modify: {",
|
||||
" title: \"修改\",",
|
||||
" submit: ({ model }) => {",
|
||||
" ${8}",
|
||||
" },",
|
||||
" },",
|
||||
"});",
|
||||
"</script>",
|
||||
"",
|
||||
"<style lang=\"less\" scoped></style>"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"recommendations": [
|
||||
"dbaeumer.vscode-eslint",
|
||||
"esbenp.prettier-vscode",
|
||||
"xabikos.JavaScriptSnippets",
|
||||
"Vue.volar",
|
||||
"Vue.vscode-typescript-vue-plugin",
|
||||
"sdras.vue-vscode-snippets",
|
||||
"antfu.iconify"
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
{
|
||||
"New Route": {
|
||||
"description": "生成一个Setup风格的SFC页面(带路由)",
|
||||
"prefix": "groute",
|
||||
"isFileTemplate": true,
|
||||
"body": [
|
||||
"<template>",
|
||||
" <div>",
|
||||
" Page",
|
||||
" </div>",
|
||||
"</template>",
|
||||
"",
|
||||
"<script setup lang=\"ts\">",
|
||||
" ${0}",
|
||||
"</script>",
|
||||
"",
|
||||
"<style lang=\"less\" scoped>",
|
||||
" ",
|
||||
"</style>",
|
||||
"",
|
||||
"<route lang=\"json\">",
|
||||
"{",
|
||||
" \"meta\": {",
|
||||
" \"sort\": ${1:20010},",
|
||||
" \"title\": \"${2:测试页面}\",",
|
||||
" \"icon\": \"icon-park-outline-${3:home}\"",
|
||||
" }",
|
||||
"}",
|
||||
"</route>"
|
||||
]
|
||||
},
|
||||
"New Page": {
|
||||
"description": "生成一个Setup风格的SFC页面",
|
||||
"prefix": "gpage",
|
||||
"isFileTemplate": true,
|
||||
"body": [
|
||||
"<template>",
|
||||
" <div>",
|
||||
" Page",
|
||||
" </div>",
|
||||
"</template>",
|
||||
"",
|
||||
"<script setup lang=\"ts\">",
|
||||
" ${0}",
|
||||
"</script>",
|
||||
"",
|
||||
"<style lang=\"less\" scoped>",
|
||||
" ",
|
||||
"</style>"
|
||||
]
|
||||
},
|
||||
"New Route Meta": {
|
||||
"prefix": "groutemeta",
|
||||
"body": [
|
||||
"<route lang=\"json\">",
|
||||
"{",
|
||||
" \"meta\": {",
|
||||
" \"sort\": ${1:20010},",
|
||||
" \"title\": \"${2:测试页面}\",",
|
||||
" \"icon\": \"icon-park-outline-${3:home}\"",
|
||||
" }",
|
||||
"}",
|
||||
"</route>"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,264 @@
|
|||
## 介绍
|
||||
基于vue3 + vite4 + typescript的B端管理系统起始模板,提供自动导入/路由、轻量CRUD表格组件和API接口自动生成等功能。
|
||||
|
||||
## 功能
|
||||
- 一个文件,自动生成路由/菜单/面包屑
|
||||
- Typescript支持,内置和扩展众多类型定义,文档在手可触
|
||||
- 根据openapi自动生成数据类型、请求函数
|
||||
- 轻量化的封装表单、CRUD表格,开箱即用
|
||||
- 内置VITE插件,输出版本/打包信息,支持根据不同后缀打包
|
||||
- 轻量的字典常量定义助手函数
|
||||
- 常用API/组件自动导入,同时带类型提示
|
||||
- 图标/样式一个类名搞定
|
||||
- 遵循Conventional Changelog规范, 自动生成版本记录文档
|
||||
- 内置常用VsCode代码片段和推荐扩展,提升开发效率
|
||||
|
||||
## 快速开始
|
||||
1. 确保本地安装有如下软件,推荐最新版本。
|
||||
```
|
||||
git
|
||||
node
|
||||
pnpm
|
||||
```
|
||||
备注:pnpm在NodeJS v16+版本可通过 corepack enable 命令开启,低版本请通过 npm install pnpm 命令安装
|
||||
|
||||
2. 拉取模板
|
||||
```
|
||||
npx degit https://github.com/juetan/apptify-admin
|
||||
```
|
||||
|
||||
3. 安装依赖
|
||||
```
|
||||
pnpm install
|
||||
```
|
||||
|
||||
4. 启动项目,默认端口3020。
|
||||
```
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
|
||||
## 开发文档
|
||||
本仓库仅是一个起始模板,具体项目请根据需求改造。
|
||||
|
||||
### 路由菜单
|
||||
基于 [vite-plugin-pages](https://github.com/hannoeru/vite-plugin-pages) 插件。本项目使用`src/pages`作为路由目录,最终生成的路由仅有2级,主要是出于`<keepalive>`缓存的需要,其中:
|
||||
|
||||
| 说明 |
|
||||
| --- |
|
||||
| `src/pages`目录下以`_`开头的文件名/目录名为一级路由,如登陆页面。
|
||||
| `src/pages`其他子目录或`.vue`文件为二级路由,如应用首页。
|
||||
|
||||
左侧菜单数据,将根据上面的二级路由自动生成,如需生成层级只需在对应目录下的index.vue文件中定义如下路由配置:
|
||||
```
|
||||
<route lang="json">
|
||||
{
|
||||
"parentMeta": {
|
||||
// 具体属性查阅 src/types/vue-router.d.ts
|
||||
}
|
||||
}
|
||||
</route>
|
||||
```
|
||||
|
||||
### 文件后缀
|
||||
在 `scripts/vite/plugin.ts` 文件中,内置了一个VITE插件,主要用于输出编译信息以及根据不同文件后缀进行打包。在项目根目录下的`.env`配置文件中,可指定以下属性:
|
||||
```
|
||||
VITE_BUILD_TYPE = my
|
||||
```
|
||||
配置后,构建时将首先尝试加载`index.my.vue`文件,不存在时再加载`index.vue`。默认开发环境下为`dev`, 其他环境为`prod`,这在本地临时开发或根据微差异化打包时非常有用。
|
||||
|
||||
### 图标样式
|
||||
基于 [UnoCSS]() 插件,可使用类似TailwindCSS的原子样式快速开发,同时默认安装`icon-park-outline`图标库,只需引用类名即可得到SVG图标。这在路由菜单等需要动态渲染时非常有用,同时所有样式类和图标类都是按需打包的,示例:
|
||||
```html
|
||||
<i class="text-sm icon-park-outline-home" />
|
||||
```
|
||||
|
||||
### 接口请求
|
||||
基于 [typescript-swagger-api]() 库,根据openapi结构自动生成请求接口和数据类型。生成的内容位于`src/api/service`目录下,默认包含数据类型定义、请求客户端(默认axios)和请求基类三大块内容。如需自定义生成模板,可查阅`scripts/openapi`目录下的模板内容。
|
||||
|
||||
生成的接口类型包含完整的入参和出参类型提示。
|
||||
|
||||
此外,在`src/api/instance/useRequest.ts`中还定义了一个`useRequest`函数,可对请求状态进行管理,示例:
|
||||
```typescript
|
||||
const userState = useRequest(api.user.getUsers)
|
||||
|
||||
// 返回的数据
|
||||
userState.data
|
||||
|
||||
// 是否请求中
|
||||
userState.loading
|
||||
|
||||
// 请求异常
|
||||
userState.error
|
||||
```
|
||||
|
||||
### 字典枚举
|
||||
字典枚举可能包含多种需求,例如根据值获取标签、生成下拉框选项、根据值获取其他内容(如颜色)等。在 `src/config/defineConstants.ts` 文件中,定义了一个简易的字典枚举值助手函数,解决以上问题,且提供类型提示,示例:
|
||||
```typescript
|
||||
const media = defineConstants([
|
||||
{
|
||||
label: '视频',
|
||||
value: 1,
|
||||
enumKey: 'VIDEO',
|
||||
color: 'red'
|
||||
},
|
||||
{
|
||||
label: '图片',
|
||||
value: 2,
|
||||
enumeKey: 'IMAGE',
|
||||
color: 'blue'
|
||||
}
|
||||
])
|
||||
|
||||
// enumKey
|
||||
media.IMAGE // 2
|
||||
|
||||
// 根据value值,获取其他属性值, 第2个参数可选(默认label)
|
||||
media.format(1, 'color') // red
|
||||
|
||||
// 获取某个属性值组成的数组
|
||||
media.each('value') // [1, 2]
|
||||
|
||||
// 根据value值过滤数组,omit同理
|
||||
media.pick(1) // [{ label: '视频', value: 1, enumKey: 'VIDEO' }]
|
||||
|
||||
// 原始传入defineContants的数组,可自定义操作
|
||||
media.items // 可直接作为select的选项
|
||||
```
|
||||
|
||||
### 增删改查
|
||||
在`src/components`目录中,封装了`form`组件和`table`组件,主要用于普通CRUD的实现,这里演示基本的使用方法。
|
||||
```html
|
||||
<template>
|
||||
<Table ref="tableRef" v-bind="table" />
|
||||
</template>
|
||||
<script>
|
||||
import { Table, useTable } from '@/components'
|
||||
|
||||
const table = useTable({
|
||||
// 数据源配置,可以是数组或返回对象的异步函数
|
||||
data: async(search, paging) => {
|
||||
return { data, total }
|
||||
},
|
||||
|
||||
// 表格列配置
|
||||
columns: [
|
||||
{
|
||||
title: '用户名',
|
||||
dataIndex: 'name'
|
||||
}
|
||||
],
|
||||
|
||||
// 分页配置
|
||||
pagination: {
|
||||
showTotal: true,
|
||||
},
|
||||
|
||||
// 查询配置,类型为useForm的入参
|
||||
search: {
|
||||
items: [
|
||||
{
|
||||
field: 'username',
|
||||
label: '用户名',
|
||||
type: 'input'
|
||||
},
|
||||
]
|
||||
},
|
||||
|
||||
// 新增表单弹窗的配置,类型为useFormModal的入参
|
||||
create: {
|
||||
title: '新增用户',
|
||||
items: [
|
||||
{
|
||||
field: 'username',
|
||||
label: '用户名',
|
||||
type: 'input'
|
||||
},
|
||||
],
|
||||
submit: async({ model }) => {
|
||||
return api.xx(model)
|
||||
}
|
||||
},
|
||||
|
||||
// 修改表单弹窗的配置,类型为useFormModal的入参
|
||||
modify: {
|
||||
title: '修改用户',
|
||||
items: [
|
||||
{
|
||||
field: 'username',
|
||||
label: '用户名',
|
||||
type: 'input'
|
||||
},
|
||||
],
|
||||
submit: async({ model }) => {
|
||||
return api.xx(model)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
</script>
|
||||
```
|
||||
|
||||
### 自动导入
|
||||
基于 [unplugin-auto-import]() 和 [unplugin-vue-components]() 插件,主要用于常用API的自动导入,例如vue和vue-router等;以及常用组件的导入,例如arco-design等。示例:
|
||||
```html
|
||||
<template>
|
||||
<!-- ArcoDesign组件,无需导入即可使用且有类型提示 -->
|
||||
<a-table :data="data"></a-table>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
|
||||
// vue的API,无需导入即可使用且有类型提示
|
||||
const data = ref([])
|
||||
|
||||
</script>
|
||||
```
|
||||
如需自定义其他API的自动导入,请查阅`vite.config.ts`文件中的配置。
|
||||
|
||||
### 代码片段
|
||||
基于 VsCode 的 snippet 功能,在`.vscode/components.code-snippets`文件中定义了常用组件和API模板的快捷生成。所有代码片段均以`g`(generate)开头,对于快速CRUD或新建页面等非常有用,示例:
|
||||
```ts
|
||||
// 输入以下内容并回车
|
||||
groutemeta
|
||||
|
||||
// 将生成如下内容
|
||||
<route lang="json">
|
||||
{
|
||||
"meta": {
|
||||
"order": 10020,
|
||||
"titble": "测试页面",
|
||||
"icon": "icon-park-outline-home"
|
||||
}
|
||||
}
|
||||
</route>
|
||||
```
|
||||
|
||||
### 版本记录
|
||||
基于 [release-it]() 库,运行`pnpm release`命令时,将根据你的选择,执行以下操作:
|
||||
- 提升package.json的version版本(遵循semver语义)
|
||||
- 给git打版本标签,例如 v1.0.1(同样遵循semver语义)
|
||||
- 根据符合[Conventional Changelog]规范的git提交信息,在`CHANGELOG.md`文件中生成版本记录。
|
||||
- 自动推送到npm/github/gitlab中
|
||||
|
||||
如需自定义`CHANGELOG.md`的生成模板或进行其他自定义配置,请查阅`/scripts/release`目录下的内容。
|
||||
|
||||
### 状态管理
|
||||
基于 [pinia]() 库,具体使用查阅官方文档即可。
|
||||
|
||||
### 工具类库
|
||||
日常开发难免用到各种工具库,但直接使用的话难免有不符合项目需求的时候。例如dayjs的本地化语言、相对时间插件等都要配置,散落在项目的各个角落并不是个好习惯。
|
||||
|
||||
建议在`src/plugins`进行二次封装后,再在项目中使用,不仅统一调用还便于管理,方便后续的优化升级。
|
||||
|
||||
本项目内置开发中常用的类库,如下:
|
||||
| 库 | 说明
|
||||
| --- | ---
|
||||
| [lodash-es]() | 常用函数集,例如深克隆、防抖、节流等
|
||||
| [axios]() | HTTP请求库
|
||||
| [dayjs]() | 日期时间处理库
|
||||
| [numeral]() | 数值处理库,如数值转时间、数值转文件大小等
|
||||
| [nprogress]() | 进度条
|
||||
| [@vueuse/core]() | 基于Vue Composition API的工具库,响应式存储数据、监听事件等
|
||||
|
||||
## 最后
|
||||
如果你在使用过程中遇到问题,请在issue中提问。
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"hash": "3930064b",
|
||||
"browserHash": "71254bdf",
|
||||
"optimized": {
|
||||
"vue": {
|
||||
"src": "../../../../../../../node_modules/.pnpm/vue@3.3.4/node_modules/vue/dist/vue.runtime.esm-bundler.js",
|
||||
"file": "vue.js",
|
||||
"fileHash": "9e6f557b",
|
||||
"needsInterop": false
|
||||
}
|
||||
},
|
||||
"chunks": {}
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"type": "module"
|
||||
}
|
||||
|
|
@ -0,0 +1,152 @@
|
|||
import { defineConfig } from "vitepress";
|
||||
|
||||
/**
|
||||
* 站点配置
|
||||
* @see https://vitepress.dev/reference/site-config
|
||||
*/
|
||||
export default defineConfig({
|
||||
lang: "zh-CN",
|
||||
title: "绝弹博客",
|
||||
description: "一位前端开发者的博客",
|
||||
/**
|
||||
* 主题配置
|
||||
* @see https://vitepress.dev/reference/default-theme-config
|
||||
*/
|
||||
themeConfig: {
|
||||
logo: "/juetan.jpg",
|
||||
search: {
|
||||
provider: "local",
|
||||
options: {
|
||||
translations: {
|
||||
button: {
|
||||
buttonText: "搜索",
|
||||
buttonAriaLabel: "搜索",
|
||||
},
|
||||
modal: {
|
||||
noResultsText: "没有找到结果",
|
||||
resetButtonTitle: "重置搜索",
|
||||
footer: {
|
||||
selectText: "选择",
|
||||
navigateText: "移动",
|
||||
closeText: "关闭",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
outline: {
|
||||
label: "本篇目录",
|
||||
},
|
||||
nav: [
|
||||
{
|
||||
text: "首页",
|
||||
link: "/",
|
||||
},
|
||||
{
|
||||
text: "前端开发",
|
||||
link: "/front-end/",
|
||||
},
|
||||
{
|
||||
text: "后端开发",
|
||||
items: [
|
||||
{
|
||||
text: "测试1",
|
||||
link: "/test1",
|
||||
},
|
||||
{
|
||||
text: "测试2",
|
||||
link: "/test2",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
text: '日常记录',
|
||||
link: '/'
|
||||
},
|
||||
{
|
||||
text: "开发工具",
|
||||
link: "/dev-tools",
|
||||
},
|
||||
],
|
||||
sidebar: {
|
||||
"/front-end/": [
|
||||
{
|
||||
text: "基础知识",
|
||||
items: [
|
||||
{
|
||||
text: "HTML中的标签有多少个?",
|
||||
link: "/front-end/a",
|
||||
},
|
||||
{
|
||||
text: "Runtime API示例",
|
||||
link: "/front-end/b",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
text: '工具类库',
|
||||
items: [
|
||||
{
|
||||
text: 'Lodash在日常开发中有用的函数'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
text: 'vue',
|
||||
items: [
|
||||
{
|
||||
text: '如何将.vue文件编译成js文件?'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
text: '浏览器',
|
||||
items: [
|
||||
{
|
||||
text: '浏览器Console面板中有用的调试技巧',
|
||||
},
|
||||
{
|
||||
text: '如何利用EJS模板引擎辅助生成代码?',
|
||||
link: '/front-end/ejs-generate-code'
|
||||
},
|
||||
{
|
||||
text: '项目中的字典常量应该如何维护?'
|
||||
},
|
||||
{
|
||||
text: '从new xx()和new xx的区别聊聊JS中操作符的优先级问题',
|
||||
link: '/front-end/js-operator-priority'
|
||||
},
|
||||
{
|
||||
text: 'TailwindCSS中一些有意思的用法和实现'
|
||||
},
|
||||
{
|
||||
text: '函数柯里化是什么如何实现它?'
|
||||
},
|
||||
{
|
||||
text: '写一个VITE插件: 根据配置加载不同后缀的文件'
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
},
|
||||
socialLinks: [
|
||||
{
|
||||
icon: "github",
|
||||
link: "https://github.com/juetan",
|
||||
},
|
||||
],
|
||||
docFooter: {
|
||||
prev: "上一篇",
|
||||
next: "下一篇",
|
||||
},
|
||||
footer: {
|
||||
message: "© 2023 JueTan",
|
||||
copyright: "绝弹博客 版权所有",
|
||||
},
|
||||
},
|
||||
|
||||
markdown: {
|
||||
theme: "github-dark-dimmed",
|
||||
lineNumbers: true,
|
||||
},
|
||||
});
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
import { h } from 'vue'
|
||||
import Theme from 'vitepress/theme'
|
||||
import './style.css'
|
||||
import { EnhanceAppContext } from 'vitepress'
|
||||
|
||||
/**
|
||||
* 自定义主题
|
||||
* @see https://vitepress.dev/guide/custom-theme
|
||||
*/
|
||||
export default {
|
||||
...Theme,
|
||||
Layout: () => {
|
||||
return h(Theme.Layout, null, {
|
||||
// https://vitepress.dev/guide/extending-default-theme#layout-slots
|
||||
})
|
||||
},
|
||||
enhanceApp({ app, router, siteData }: EnhanceAppContext) {
|
||||
// ...
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,251 @@
|
|||
/**
|
||||
* Customize default theme styling by overriding CSS variables:
|
||||
* https://github.com/vuejs/vitepress/blob/main/src/client/theme-default/styles/vars.css
|
||||
*/
|
||||
|
||||
/**
|
||||
* Colors
|
||||
* -------------------------------------------------------------------------- */
|
||||
|
||||
:root {
|
||||
--vp-c-brand: #09f;
|
||||
--vp-c-brand-light: #0099ffe0;
|
||||
--vp-c-brand-lighter: #0099ffa0;
|
||||
--vp-c-brand-lightest: #0099ffc0;
|
||||
--vp-c-brand-dark: #535bf2;
|
||||
--vp-c-brand-darker: #454ce1;
|
||||
--vp-c-brand-dimm: rgba(100, 108, 255, 0.08);
|
||||
--vp-sidebar-bg-color: transparent;
|
||||
--vp-nav-height: 56px;
|
||||
}
|
||||
/* :root {
|
||||
--vp-c-brand: #646cff;
|
||||
--vp-c-brand-light: #747bff;
|
||||
--vp-c-brand-lighter: #9499ff;
|
||||
--vp-c-brand-lightest: #bcc0ff;
|
||||
--vp-c-brand-dark: #535bf2;
|
||||
--vp-c-brand-darker: #454ce1;
|
||||
--vp-c-brand-dimm: rgba(100, 108, 255, 0.08);
|
||||
--vp-sidebar-bg-color: transparent;
|
||||
} */
|
||||
|
||||
/**
|
||||
* Component: Button
|
||||
* -------------------------------------------------------------------------- */
|
||||
|
||||
:root {
|
||||
--vp-button-brand-border: var(--vp-c-brand-light);
|
||||
--vp-button-brand-text: var(--vp-c-white);
|
||||
--vp-button-brand-bg: var(--vp-c-brand);
|
||||
--vp-button-brand-hover-border: var(--vp-c-brand-light);
|
||||
--vp-button-brand-hover-text: var(--vp-c-white);
|
||||
--vp-button-brand-hover-bg: var(--vp-c-brand-light);
|
||||
--vp-button-brand-active-border: var(--vp-c-brand-light);
|
||||
--vp-button-brand-active-text: var(--vp-c-white);
|
||||
--vp-button-brand-active-bg: var(--vp-button-brand-bg);
|
||||
}
|
||||
|
||||
/**
|
||||
* Component: Home
|
||||
* -------------------------------------------------------------------------- */
|
||||
|
||||
:root {
|
||||
--vp-home-hero-name-color: transparent;
|
||||
--vp-home-hero-name-background: -webkit-linear-gradient(120deg, #bd34fe 30%, #41d1ff);
|
||||
|
||||
--vp-home-hero-image-background-image: linear-gradient(-45deg, #bd34fe 50%, #47caff 50%);
|
||||
--vp-home-hero-image-filter: blur(40px);
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
:root {
|
||||
--vp-home-hero-image-filter: blur(56px);
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 960px) {
|
||||
:root {
|
||||
--vp-home-hero-image-filter: blur(72px);
|
||||
}
|
||||
.VPContent .VPDoc {
|
||||
padding: 20px 0 40px;
|
||||
}
|
||||
.VPDoc .container > .content {
|
||||
background: #fff;
|
||||
padding: 28px 24px;
|
||||
box-shadow: 0 0 8px #f1f3f5;
|
||||
border: 1px solid #f1f3f5;
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Component: Custom Block
|
||||
* -------------------------------------------------------------------------- */
|
||||
|
||||
:root {
|
||||
--vp-custom-block-tip-border: var(--vp-c-brand);
|
||||
--vp-custom-block-tip-text: var(--vp-c-brand-darker);
|
||||
--vp-custom-block-tip-bg: var(--vp-c-brand-dimm);
|
||||
}
|
||||
|
||||
.dark {
|
||||
--vp-custom-block-tip-border: var(--vp-c-brand);
|
||||
--vp-custom-block-tip-text: var(--vp-c-brand-lightest);
|
||||
--vp-custom-block-tip-bg: var(--vp-c-brand-dimm);
|
||||
}
|
||||
|
||||
/**
|
||||
* Component: Algolia
|
||||
* -------------------------------------------------------------------------- */
|
||||
|
||||
.DocSearch {
|
||||
--docsearch-primary-color: var(--vp-c-brand) !important;
|
||||
}
|
||||
|
||||
.Layout {
|
||||
background-color: #f1f3f5;
|
||||
}
|
||||
|
||||
#VPContent .VPDoc.has-aside .content-container {
|
||||
max-width: initial;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.VPNav {
|
||||
border-bottom: 1px solid #f1f3f5;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.curtain,
|
||||
.aside-curtain {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#VPContent .aside {
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
.vp-doc div[class*="language-"] {
|
||||
border-radius: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
#VPSidebarNav {
|
||||
margin-top: 20px;
|
||||
background: #fff;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
#VPSidebarNav .group {
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
#app .VPSidebar {
|
||||
width: calc((100% - (var(--vp-layout-max-width) - 64px)) / 2 + var(--vp-sidebar-width) - 20px);
|
||||
padding-right: 24px;
|
||||
}
|
||||
|
||||
#app .aside-container {
|
||||
padding-top: calc(var(--vp-nav-height) + var(--vp-layout-top-height, 0px) + var(--vp-doc-top-height, 0px) + 20px);
|
||||
}
|
||||
|
||||
#app .aside {
|
||||
max-width: 228px;
|
||||
}
|
||||
|
||||
#app .pager-link {
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
#app .VPNavBarTitle.has-sidebar .title {
|
||||
border-bottom-color: transparent;
|
||||
}
|
||||
|
||||
#app .VPMenu {
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
/**
|
||||
* Component: Home
|
||||
* -------------------------------------------------------------------------- */
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 10px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: #f1f1f1;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: #888;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: #555;
|
||||
}
|
||||
|
||||
.content img {
|
||||
border: 1px solid #e1f1f1;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.item {
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.link {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.text {
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
body {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.VPSidebarItem {
|
||||
/* padding: 0 8px; */
|
||||
}
|
||||
|
||||
.items > .VPSidebarItem.level-1 {
|
||||
margin-top: 4px;
|
||||
padding: 0 8px;
|
||||
}
|
||||
|
||||
.items > .VPSidebarItem.level-1.is-active {
|
||||
background: hsl(204 100% 95% / 1);
|
||||
/* padding: 0 8px; */
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.vp-doc tr:nth-child(2n) {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.vp-doc h2 {
|
||||
margin: 32px 0 16px;
|
||||
border-top: none;
|
||||
padding-top: 0;
|
||||
border-bottom: 1px solid var(--vp-c-divider);
|
||||
padding-bottom: 8px;
|
||||
letter-spacing: -0.02em;
|
||||
line-height: 32px;
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.vp-doc a {
|
||||
display: none;
|
||||
}
|
||||
.VPNavBarTitle .title {
|
||||
font-weight: 400;
|
||||
}
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
---
|
||||
outline: deep
|
||||
---
|
||||
|
||||
# Runtime API Examples
|
||||
|
||||
This page demonstrates usage of some of the runtime APIs provided by VitePress.
|
||||
The main `useData()` API can be used to access site, theme, and page data for the current page. It works in both `.md` and `.vue` files:
|
||||
|
||||
```md
|
||||
<script setup>
|
||||
import { useData } from 'vitepress'
|
||||
|
||||
const { theme, page, frontmatter } = useData()
|
||||
</script>
|
||||
|
||||
## Results
|
||||
|
||||
### Theme Data
|
||||
<pre>{{ theme }}</pre>
|
||||
|
||||
### Page Data
|
||||
<pre>{{ page }}</pre>
|
||||
|
||||
### Page Frontmatter
|
||||
<pre>{{ frontmatter }}</pre>
|
||||
```
|
||||
|
||||
<script setup>
|
||||
import { useData } from 'vitepress'
|
||||
|
||||
const { site, theme, page, frontmatter, localeIndex } = useData()
|
||||
</script>
|
||||
|
||||
## 输出结果
|
||||
|
||||
### 主题数据
|
||||
{{localeIndex}}
|
||||
<pre>{{ theme }}</pre>
|
||||
|
||||
### 页面数据
|
||||
<pre>{{ page }}</pre>
|
||||
|
||||
### 页面前置数据
|
||||
<pre>{{ frontmatter }}</pre>
|
||||
|
||||
## 更多
|
||||
|
||||
Check out the documentation for the [full list of runtime APIs](https://vitepress.dev/reference/runtime-api#usedata).
|
||||
|
|
@ -0,0 +1,85 @@
|
|||
# 日常开发中如何利用 EJS 模板引擎辅助生成代码?
|
||||
|
||||
在如今的前端开发中,[EJS](https://ejs.bootcss.com/) 已经是一个过时的开发方案,但其实在辅助开发方面还是用处的。话不多说,接下来以一个例子来感受下。
|
||||
|
||||
## 例子
|
||||
|
||||
在日常开发中,有时候会遇到后端有一些枚举字典的定义,前端也要进行相应的配置。例如在做权限功能时,后端给了我们下面的权限码数据:
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": 1,
|
||||
"code": "access:readAbc",
|
||||
"authName": "读取权限",
|
||||
"level": 1
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"code": "access:writeAbc",
|
||||
"authName": "写入权限"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
我们需要把他做成枚举的形式, 也就是下面的形式:
|
||||
|
||||
```typescript
|
||||
/**
|
||||
* 权限码
|
||||
*/
|
||||
export const enum AuthEnum {
|
||||
/**
|
||||
* 读取权限
|
||||
*/
|
||||
ReadAbc = "access:readAbc",
|
||||
/**
|
||||
* 写入权限
|
||||
*/
|
||||
WriteAbc = "access:writeAbc",
|
||||
}
|
||||
```
|
||||
|
||||
那么如何从这项无聊的工作中解放出来呢,操作也比较简单。
|
||||
|
||||
1\. 首先打开一个能编译 EJS 的在线网站, 例如 [One Compiler](https://onecompiler.com/ejs)。
|
||||

|
||||
|
||||
2\. 根据目标数据结构整理下代码,点击`RUN`按钮即可。
|
||||
|
||||
```typescript
|
||||
<%
|
||||
let items = [
|
||||
{
|
||||
"id": 1,
|
||||
"code": "access:readAbc",
|
||||
"authName": "读取权限",
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"code": "access:writeAbc",
|
||||
"authName": "写入权限",
|
||||
},
|
||||
]
|
||||
items = items.map(item => {
|
||||
let key = item.code.substring(7);
|
||||
key = key.charAt(0).toUpperCase() + key.slice(1)
|
||||
return { ...item, key }
|
||||
})
|
||||
-%>
|
||||
/**
|
||||
* 权限码
|
||||
*/
|
||||
export const enum DaEnum {
|
||||
<% items.forEach((i, index) => { -%>
|
||||
/**
|
||||
* <%= i.authName %>
|
||||
*/
|
||||
<%= i.key %> = '<%= i.code %>',
|
||||
<% }) -%>
|
||||
}
|
||||
```
|
||||
|
||||
## 结语
|
||||
|
||||
以上这是一个简单的例子,但在命令行工具进行页面模板的生成方面可能用得比较多。另外,EJS 的语法也不难,在一些重复性的代码生成方面还是不错的。
|
||||
|
|
@ -0,0 +1 @@
|
|||
前端
|
||||
|
After Width: | Height: | Size: 67 KiB |
|
|
@ -0,0 +1,112 @@
|
|||
# 从new xx()和new xx的区别中,整理JS中操作符的优先级
|
||||
|
||||
|
||||
## 问题
|
||||
最近在工作中写日期格式化时,遇到一个问题,先来看下面的代码:
|
||||
|
||||
```typescript
|
||||
// 写法一
|
||||
new Date().toISOString
|
||||
|
||||
// 写法二
|
||||
new Date.toISOString;
|
||||
```
|
||||
执行如下:
|
||||
|
||||
and-new.png)
|
||||
|
||||
猜测是优先级的问题,然后在MDN找到了[答案](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/Operator_precedence#%E6%B1%87%E6%80%BB%E8%A1%A8),它们确实是两个不同的优先级,如下图:
|
||||
|
||||
-and-new.png)
|
||||
|
||||
以上,方式一中的 `.` 优先级最高,但其左边需要先求值,因而先执行`new Date()`得到实例,再返回实例上的`toISOString`属性;而方式二中的 `. `优先级也是最高,但其左边不用求值,因而可以先执行`Date.toISOString`得到`toISOString`值(Date上不存在该属性,因此该值为undefined),再尝试进行`new toISOString`操作时报错发生。
|
||||
|
||||
## 优先级
|
||||
我印象中的运算符只有四十几个,没想到在MDN里面找到的有六十几个。说实话,上面的运算符以前真没注意过,趁着空闲我决定将这些运算符整理下,整理后总体分为下面的五大类:
|
||||
|
||||
### 一级运算符
|
||||
大部分是一元操作符。
|
||||
|
||||
| 运算符 | 类型 | 说明
|
||||
| :--- | :--- | ---
|
||||
| ( … ) | 分组 | 优先级最高的运算符
|
||||
| … . … | 成员访问 | 静态访问
|
||||
| [] | 需计算的成员访问 | 动态访问
|
||||
| new xx() | new(带参数列表) | 实例化
|
||||
| () | 函数调用 | 函数调用
|
||||
| ?. | 可选链(Optional chaining) | `a?.b`类似于`a === null \|\| a === void 0 ? void 0 : a.b;`
|
||||
| new … | new(无参数列表) | 实例化
|
||||
| … ++ | 后置递增 | 先返回再+1,例如 `let a = 1; const b = a++; // b: 1 a: 2`。比较常见的是在for循环中进行后置递增。
|
||||
| … -- | 后置递减 | 同上
|
||||
| ! … | 逻辑非 (!) |
|
||||
| ~ … | 按位非 (~) |
|
||||
| + … | 一元加法 (+) | 可用于把字符串转换为数值,例如`+'1'`将得到`1`。
|
||||
| - … | 一元减法 (-) | 同上
|
||||
| ++ … | 前置递增 | 与后置递增不同,先执行+1再返回
|
||||
| -- … | 前置递减 | 同上
|
||||
| typeof … | typeof | 返回值只有这几个:string \| number \| boolean \| undefined \| null \| function \| object
|
||||
| void … | void | 比较常见的是使用`void 0`代替undefined,因为在以前undefined是可以作为变量名使用的。
|
||||
| delete … | delete | 删除对象的属性
|
||||
| await … | await | 等待某个promise执行成功
|
||||
|
||||
### 算符运算符
|
||||
| 运算符 | 类型 | 说明
|
||||
| :--- | :--- | ---
|
||||
| … ** … | 幂 (**) |
|
||||
| … * … | 乘法 (*) |
|
||||
| … / … | 除法 (/) |
|
||||
| … % … | 取余 (%) |
|
||||
| … + … | 加法 (+) |
|
||||
| … - … | 减法 (-) |
|
||||
| … << … | 按位左移 (<<) | 通常用于二进制数据的移位, 例如:`(4)<<1` 将得到`8`。过程:十进制`4`转换为二进制`100`, 左移一位得到`1000`,再转换为十进制即为`8`。
|
||||
| … >> … | 按位右移 (>>) | 同上
|
||||
| … >>> … | 无符号右移 (>>>) | 同上
|
||||
| … < … | 小于 (<) | 对于数值,比较大小
|
||||
| … <= … | 小于等于 (<=) |
|
||||
| … > … | 大于 (>) |
|
||||
| … >= … | 大于等于 (>=) |
|
||||
|
||||
### 比较运算符
|
||||
| 运算符 | 类型 | 说明
|
||||
| :--- | :--- | ---
|
||||
| … in … | in | 判断某个属性是否存在于对象上,会顺着原型链进行查找,可以用`Object.prototype.hasOwnProperty.call(obj, 'xx')`进行检测自身的属性是否存在。
|
||||
| … instanceof … | instanceof | 判断右边的对象,是否在左边对象的原型链上。
|
||||
| … == … | 相等 (==) | 左右两边的值可能会先做隐式转换,再进行比较,例如: `'1' == 1 //true`
|
||||
| … != … | 不相等 (!=) | 同上
|
||||
| … === … | 一致/严格相等 (===) | 左右两边的值不做隐式转换,直接比较,例如:`'1' === 1 // false`
|
||||
| … !== … | 不一致/严格不相等 (!==) | 同上
|
||||
|
||||
### 布尔运算符
|
||||
| 运算符 | 类型 | 说明
|
||||
| :--- | :--- | ---
|
||||
| … & … | 按位与 (&) | 常用于对二进制数值进行操作
|
||||
| … ^ … | 按位异或 (^) |
|
||||
| … \| … | 按位或 (\|) |
|
||||
| … && … | 逻辑与 (&&) |
|
||||
| … \|\| … | 逻辑或 (||) |
|
||||
| … ?? … | 空值合并 (??) | 当左边的值不为undefined或null时,返回左边,否则返回右边, 具体代码可能是这样的\n: `a !== null && a !== void 0 ? a : b;` |
|
||||
|
||||
### 赋值运算符
|
||||
| 运算符 | 类型 | 说明
|
||||
| :--- | :--- | ---
|
||||
| … ? … : … | 条件(三元)运算符 | 可以在简单场景中代替if/else使用,不过自从出了??运算符,这个运算符用的比较少
|
||||
| … = … | 赋值 |
|
||||
| … += … |
|
||||
| … -= … |
|
||||
| … **= … |
|
||||
| … *= … |
|
||||
| … /= … |
|
||||
| … %= … |
|
||||
| … <<= … |
|
||||
| … >>= … |
|
||||
| … >>>= … |
|
||||
| … &= … |
|
||||
| … ^= … |
|
||||
| … |= … |
|
||||
| … &&= … |
|
||||
| … ||= … |
|
||||
| … ??= … |
|
||||
| … , … | 逗号 / 序列 | 优先级最低的运算符,由于其会返回最后一个值,在某些简短操作中也会用到,例如:`const map = items.reduce((m, i) => (m[i.id]=i,m), {})`
|
||||
|
||||
## 结语
|
||||
优先级的重要性不言而喻,往后还是要多多温习。
|
||||
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 53 KiB |
|
|
@ -0,0 +1,19 @@
|
|||
---
|
||||
# https://vitepress.dev/reference/default-theme-home-page
|
||||
layout: home
|
||||
|
||||
hero:
|
||||
name: "绝弹博客"
|
||||
text: ""
|
||||
tagline: 一位前端开发者的博客
|
||||
actions:
|
||||
- theme: brand
|
||||
text: 首页
|
||||
link: /markdown-examples
|
||||
- theme: alt
|
||||
text: 测试页面
|
||||
link: /api-examples
|
||||
image:
|
||||
src: /assets/td.svg
|
||||
alt: page
|
||||
---
|
||||
|
|
@ -0,0 +1,85 @@
|
|||
# Markdown语法示例
|
||||
|
||||
This page demonstrates some of the built-in markdown extensions provided by VitePress.
|
||||
|
||||
## Syntax Highlighting
|
||||
|
||||
VitePress provides Syntax Highlighting powered by [Shiki](https://github.com/shikijs/shiki), with additional features like line-highlighting:
|
||||
|
||||
**Input**
|
||||
|
||||
````
|
||||
```js{4}
|
||||
export default {
|
||||
data () {
|
||||
return {
|
||||
msg: 'Highlighted!'
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
````
|
||||
|
||||
**Output**
|
||||
|
||||
```js{4}
|
||||
export default {
|
||||
data () {
|
||||
return {
|
||||
msg: 'Highlighted!'
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Custom Containers
|
||||
|
||||
**Input**
|
||||
|
||||
```md
|
||||
::: info
|
||||
This is an info box.
|
||||
:::
|
||||
|
||||
::: tip
|
||||
This is a tip.
|
||||
:::
|
||||
|
||||
::: warning
|
||||
This is a warning.
|
||||
:::
|
||||
|
||||
::: danger
|
||||
This is a dangerous warning.
|
||||
:::
|
||||
|
||||
::: details
|
||||
This is a details block.
|
||||
:::
|
||||
```
|
||||
|
||||
**Output**
|
||||
|
||||
::: info
|
||||
This is an info box.
|
||||
:::
|
||||
|
||||
::: tip
|
||||
This is a tip.
|
||||
:::
|
||||
|
||||
::: warning
|
||||
This is a warning.
|
||||
:::
|
||||
|
||||
::: danger
|
||||
This is a dangerous warning.
|
||||
:::
|
||||
|
||||
::: details
|
||||
This is a details block.
|
||||
:::
|
||||
|
||||
## More
|
||||
|
||||
Check out the documentation for the [full list of markdown extensions](https://vitepress.dev/guide/markdown).
|
||||
|
After Width: | Height: | Size: 79 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 4.9 KiB |
|
|
@ -0,0 +1,60 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="./favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>%VITE_APP_TITLE% - %VITE_APP_SUBTITLE%</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app" class="dark:bg-slate-900 dark:text-slate-200">
|
||||
<style>
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
font-size: 14px;
|
||||
font-family: Inter, "-apple-system", BlinkMacSystemFont, "PingFang SC", "Hiragino Sans GB", "noto sans",
|
||||
"Microsoft YaHei", "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||
}
|
||||
#app {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.loading {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.loading-image {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
}
|
||||
.loading-title {
|
||||
margin: 0;
|
||||
margin-top: 20px;
|
||||
font-size: 22px;
|
||||
font-weight: 400;
|
||||
line-height: 1;
|
||||
}
|
||||
.loading-tip {
|
||||
margin-top: 12px;
|
||||
line-height: 1;
|
||||
}
|
||||
</style>
|
||||
<div class="loading">
|
||||
<img src="/assets/loading.svg" alt="loading" class="loading-image" />
|
||||
<h1 class="loading-title">欢迎访问%VITE_APP_TITLE%</h1>
|
||||
<div class="loading-tip">正在加载中, 请稍后...</div>
|
||||
</div>
|
||||
</div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
{
|
||||
"name": "common",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vue-tsc && vite build",
|
||||
"preview": "vite preview",
|
||||
"new": "cd ./scripts/plop && plop",
|
||||
"api": "tsx ./scripts/openapi/index.ts",
|
||||
"release": "release-it --config ./scripts/release/index.cjs",
|
||||
"docs:dev": "vitepress dev docs",
|
||||
"docs:build": "vitepress build docs",
|
||||
"docs:preview": "vitepress preview docs"
|
||||
},
|
||||
"prettier": {
|
||||
"tabWidth": 2,
|
||||
"printWidth": 120
|
||||
},
|
||||
"devDependencies": {
|
||||
"@arco-design/web-vue": "^2.44.4",
|
||||
"@iconify-json/icon-park-outline": "^1.1.10",
|
||||
"@release-it/conventional-changelog": "^5.1.1",
|
||||
"@types/lodash-es": "^4.17.7",
|
||||
"@types/nprogress": "^0.2.0",
|
||||
"@vitejs/plugin-vue": "^4.0.0",
|
||||
"@vitejs/plugin-vue-jsx": "^3.0.0",
|
||||
"@vueuse/core": "^9.13.0",
|
||||
"axios": "^1.3.6",
|
||||
"dayjs": "^1.11.7",
|
||||
"less": "^4.1.3",
|
||||
"lodash-es": "^4.17.21",
|
||||
"nprogress": "^0.2.0",
|
||||
"numeral": "^2.0.6",
|
||||
"pinia": "^2.0.33",
|
||||
"plop": "^3.1.2",
|
||||
"release-it": "^15.10.1",
|
||||
"swagger-typescript-api": "^12.0.4",
|
||||
"tsx": "^3.12.6",
|
||||
"typescript": "^4.6.4",
|
||||
"unocss": "^0.49.4",
|
||||
"unplugin-auto-import": "^0.13.0",
|
||||
"unplugin-icons": "^0.15.2",
|
||||
"unplugin-vue-components": "^0.23.0",
|
||||
"vite": "^4.3.9",
|
||||
"vite-plugin-mock": "^3.0.0",
|
||||
"vite-plugin-pages": "^0.28.0",
|
||||
"vite-plugin-style-import": "^2.0.0",
|
||||
"vitepress": "1.0.0-beta.1",
|
||||
"vue": "^3.3.4",
|
||||
"vue-router": "^4.1.6",
|
||||
"vue-tsc": "^1.0.9"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="margin: auto; background: rgb(255, 255, 255); display: block; shape-rendering: auto;" width="200px" height="200px" viewBox="0 0 100 100" preserveAspectRatio="xMidYMid">
|
||||
<g transform="translate(50 50)">
|
||||
<g>
|
||||
<animateTransform attributeName="transform" type="rotate" values="0;45" keyTimes="0;1" dur="0.2s" repeatCount="indefinite"></animateTransform><path d="M29.491524206117255 -5.5 L37.491524206117255 -5.5 L37.491524206117255 5.5 L29.491524206117255 5.5 A30 30 0 0 1 24.742744050198738 16.964569457146712 L24.742744050198738 16.964569457146712 L30.399598299691117 22.621423706639092 L22.621423706639096 30.399598299691114 L16.964569457146716 24.742744050198734 A30 30 0 0 1 5.5 29.491524206117255 L5.5 29.491524206117255 L5.5 37.491524206117255 L-5.499999999999997 37.491524206117255 L-5.499999999999997 29.491524206117255 A30 30 0 0 1 -16.964569457146705 24.742744050198738 L-16.964569457146705 24.742744050198738 L-22.621423706639085 30.399598299691117 L-30.399598299691117 22.621423706639092 L-24.742744050198738 16.964569457146712 A30 30 0 0 1 -29.491524206117255 5.500000000000009 L-29.491524206117255 5.500000000000009 L-37.491524206117255 5.50000000000001 L-37.491524206117255 -5.500000000000001 L-29.491524206117255 -5.500000000000002 A30 30 0 0 1 -24.742744050198738 -16.964569457146705 L-24.742744050198738 -16.964569457146705 L-30.399598299691117 -22.621423706639085 L-22.621423706639092 -30.399598299691117 L-16.964569457146712 -24.742744050198738 A30 30 0 0 1 -5.500000000000011 -29.491524206117255 L-5.500000000000011 -29.491524206117255 L-5.500000000000012 -37.491524206117255 L5.499999999999998 -37.491524206117255 L5.5 -29.491524206117255 A30 30 0 0 1 16.964569457146702 -24.74274405019874 L16.964569457146702 -24.74274405019874 L22.62142370663908 -30.39959829969112 L30.399598299691117 -22.6214237066391 L24.742744050198738 -16.964569457146716 A30 30 0 0 1 29.491524206117255 -5.500000000000013 M0 -20A20 20 0 1 0 0 20 A20 20 0 1 0 0 -20" fill="#09f"></path></g></g>
|
||||
<!-- [ldio] generated by https://loading.io/ --></svg>
|
||||
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
|
@ -0,0 +1,63 @@
|
|||
import path from "path";
|
||||
import { generateApi } from "swagger-typescript-api";
|
||||
import { loadEnv } from "vite";
|
||||
import { fileURLToPath } from "url";
|
||||
|
||||
const __dirname = path.join(fileURLToPath(new URL(import.meta.url)), "..");
|
||||
const env = loadEnv("development", process.cwd());
|
||||
|
||||
const run = async () => {
|
||||
const output = await generateApi({
|
||||
url: env.VITE_API_DOCS_URL,
|
||||
templates: path.resolve(__dirname, "./template"),
|
||||
output: path.resolve(process.cwd(), "src/api/service"),
|
||||
name: "index.ts",
|
||||
singleHttpClient: false,
|
||||
httpClientType: "axios",
|
||||
unwrapResponseData: false,
|
||||
moduleNameIndex: 1,
|
||||
moduleNameFirstTag: true,
|
||||
cleanOutput: true,
|
||||
// generateRouteTypes: true,
|
||||
extractRequestParams: true,
|
||||
modular: false,
|
||||
prettier: {
|
||||
printWidth: 120,
|
||||
tabWidth: 2,
|
||||
trailingComma: "all",
|
||||
parser: "typescript",
|
||||
},
|
||||
});
|
||||
// const { configuration, getTemplate, renderTemplate, createFile } = output
|
||||
// const { config } = configuration
|
||||
// const { templateInfos } = config
|
||||
// const templateMap = templateInfos.reduce((acc, { fileName, name }) => ({
|
||||
// ...acc,
|
||||
// [name]: getTemplate({ fileName, name }),
|
||||
// }),
|
||||
// {});
|
||||
// const files = [
|
||||
// {
|
||||
// path: config.output,
|
||||
// fileName: 'dataContracts.ts',
|
||||
// content: renderTemplate(templateMap.dataContracts, configuration),
|
||||
// },
|
||||
// {
|
||||
// path: config.output,
|
||||
// fileName: 'httpClient.ts',
|
||||
// content: renderTemplate(templateMap.httpClient, configuration),
|
||||
// },
|
||||
// {
|
||||
// path: config.output,
|
||||
// fileName: 'apiClient.ts',
|
||||
// content: renderTemplate(templateMap.api, configuration),
|
||||
// }
|
||||
// ]
|
||||
// for (const file of files) {
|
||||
// createFile(file)
|
||||
// }
|
||||
debugger
|
||||
return output;
|
||||
};
|
||||
|
||||
run();
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
# 修改
|
||||
- procedure-call.ejs 添加return
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
<%
|
||||
const { apiConfig, routes, utils, config } = it;
|
||||
const { info, servers, externalDocs } = apiConfig;
|
||||
const { _, require, formatDescription } = utils;
|
||||
|
||||
const server = (servers && servers[0]) || { url: "" };
|
||||
|
||||
const descriptionLines = _.compact([
|
||||
`@title ${info.title || "No title"}`,
|
||||
info.version && `@version ${info.version}`,
|
||||
info.license && `@license ${_.compact([
|
||||
info.license.name,
|
||||
info.license.url && `(${info.license.url})`,
|
||||
]).join(" ")}`,
|
||||
info.termsOfService && `@termsOfService ${info.termsOfService}`,
|
||||
server.url && `@baseUrl ${server.url}`,
|
||||
externalDocs.url && `@externalDocs ${externalDocs.url}`,
|
||||
info.contact && `@contact ${_.compact([
|
||||
info.contact.name,
|
||||
info.contact.email && `<${info.contact.email}>`,
|
||||
info.contact.url && `(${info.contact.url})`,
|
||||
]).join(" ")}`,
|
||||
info.description && " ",
|
||||
info.description && _.replace(formatDescription(info.description), /\n/g, "\n * "),
|
||||
]);
|
||||
|
||||
%>
|
||||
|
||||
<% if (config.httpClientType === config.constants.HTTP_CLIENT.AXIOS) { %> import { AxiosRequestConfig, AxiosResponse } from "axios"; <% } %>
|
||||
|
||||
<% if (descriptionLines.length) { %>
|
||||
/**
|
||||
<% descriptionLines.forEach((descriptionLine) => { %>
|
||||
* <%~ descriptionLine %>
|
||||
|
||||
<% }) %>
|
||||
*/
|
||||
<% } %>
|
||||
export class <%~ config.apiClassName %><SecurityDataType extends unknown><% if (!config.singleHttpClient) { %> extends HttpClient<SecurityDataType> <% } %> {
|
||||
|
||||
<% if(config.singleHttpClient) { %>
|
||||
http: HttpClient<SecurityDataType>;
|
||||
|
||||
constructor (http: HttpClient<SecurityDataType>) {
|
||||
this.http = http;
|
||||
}
|
||||
<% } %>
|
||||
|
||||
|
||||
<% routes.outOfModule && routes.outOfModule.forEach((route) => { %>
|
||||
|
||||
<%~ includeFile('./procedure-call.ejs', { ...it, route }) %>
|
||||
|
||||
<% }) %>
|
||||
|
||||
<% routes.combined && routes.combined.forEach(({ routes = [], moduleName }) => { %>
|
||||
<%~ moduleName %> = {
|
||||
<% routes.forEach((route) => { %>
|
||||
|
||||
<%~ includeFile('./procedure-call.ejs', { ...it, route }) %>
|
||||
|
||||
<% }) %>
|
||||
}
|
||||
<% }) %>
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
<%
|
||||
const { data, utils } = it;
|
||||
const { formatDescription, require, _ } = utils;
|
||||
|
||||
const stringify = (value) => _.isObject(value) ? JSON.stringify(value) : _.isString(value) ? `"${value}"` : value
|
||||
|
||||
const jsDocLines = _.compact([
|
||||
data.title,
|
||||
data.description && formatDescription(data.description),
|
||||
!_.isUndefined(data.deprecated) && data.deprecated && '@deprecated',
|
||||
!_.isUndefined(data.format) && `@format ${data.format}`,
|
||||
!_.isUndefined(data.minimum) && `@min ${data.minimum}`,
|
||||
!_.isUndefined(data.multipleOf) && `@multipleOf ${data.multipleOf}`,
|
||||
!_.isUndefined(data.exclusiveMinimum) && `@exclusiveMin ${data.exclusiveMinimum}`,
|
||||
!_.isUndefined(data.maximum) && `@max ${data.maximum}`,
|
||||
!_.isUndefined(data.minLength) && `@minLength ${data.minLength}`,
|
||||
!_.isUndefined(data.maxLength) && `@maxLength ${data.maxLength}`,
|
||||
!_.isUndefined(data.exclusiveMaximum) && `@exclusiveMax ${data.exclusiveMaximum}`,
|
||||
!_.isUndefined(data.maxItems) && `@maxItems ${data.maxItems}`,
|
||||
!_.isUndefined(data.minItems) && `@minItems ${data.minItems}`,
|
||||
!_.isUndefined(data.uniqueItems) && `@uniqueItems ${data.uniqueItems}`,
|
||||
!_.isUndefined(data.default) && `@default ${stringify(data.default)}`,
|
||||
!_.isUndefined(data.pattern) && `@pattern ${data.pattern}`,
|
||||
!_.isUndefined(data.example) && `@example ${stringify(data.example)}`
|
||||
]).join('\n').split('\n');
|
||||
%>
|
||||
<% if (jsDocLines.every(_.isEmpty)) { %>
|
||||
<% } else if (jsDocLines.length === 1) { %>
|
||||
/** <%~ jsDocLines[0] %> */
|
||||
<% } else if (jsDocLines.length) { %>
|
||||
/**
|
||||
<% for (jsDocLine of jsDocLines) { %>
|
||||
* <%~ jsDocLine %>
|
||||
|
||||
<% } %>
|
||||
*/
|
||||
<% } %>
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
<%
|
||||
const { modelTypes, utils, config } = it;
|
||||
const { formatDescription, require, _, Ts } = utils;
|
||||
|
||||
|
||||
const dataContractTemplates = {
|
||||
enum: (contract) => {
|
||||
return `enum ${contract.name} {\r\n${contract.content} \r\n }`;
|
||||
},
|
||||
interface: (contract) => {
|
||||
return `interface ${contract.name} {\r\n${contract.content}}`;
|
||||
},
|
||||
type: (contract) => {
|
||||
return `type ${contract.name} = ${contract.content}`;
|
||||
},
|
||||
}
|
||||
%>
|
||||
|
||||
<% if (config.internalTemplateOptions.addUtilRequiredKeysType) { %>
|
||||
type <%~ config.Ts.CodeGenKeyword.UtilRequiredKeys %><T, K extends keyof T> = Omit<T, K> & Required<Pick<T, K>>
|
||||
<% } %>
|
||||
|
||||
<% modelTypes.forEach((contract) => { %>
|
||||
<%~ includeFile('@base/data-contract-jsdoc.ejs', { ...it, data: { ...contract, ...contract.typeData } }) %>
|
||||
export <%~ (dataContractTemplates[contract.typeIdentifier] || dataContractTemplates.type)(contract) %>
|
||||
|
||||
|
||||
<% }) %>
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
<%
|
||||
const { contract, utils, config } = it;
|
||||
const { formatDescription, require, _ } = utils;
|
||||
const { name, $content } = contract;
|
||||
%>
|
||||
<% if (config.generateUnionEnums) { %>
|
||||
export type <%~ name %> = <%~ _.map($content, ({ value }) => value).join(" | ") %>
|
||||
<% } else { %>
|
||||
export enum <%~ name %> {
|
||||
<%~ _.map($content, ({ key, value }) => `${key} = ${value}`).join(",\n") %>
|
||||
}
|
||||
<% } %>
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
<% const { config } = it; %>
|
||||
<% /* https://github.com/acacode/swagger-typescript-api/tree/next/templates/base/http-clients/ */ %>
|
||||
<%~ includeFile(`./http-clients/${config.httpClientType}-http-client`, it) %>
|
||||
|
|
@ -0,0 +1,138 @@
|
|||
<%
|
||||
const { apiConfig, generateResponses, config } = it;
|
||||
%>
|
||||
|
||||
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse, ResponseType, HeadersDefaults } from "axios";
|
||||
|
||||
export type QueryParamsType = Record<string | number, any>;
|
||||
|
||||
export interface FullRequestParams extends Omit<AxiosRequestConfig, "data" | "params" | "url" | "responseType"> {
|
||||
/** set11 parameter to `true` for call `securityWorker` for this request */
|
||||
secure?: boolean;
|
||||
/** request path */
|
||||
path: string;
|
||||
/** content type of request body */
|
||||
type?: ContentType;
|
||||
/** query params */
|
||||
query?: QueryParamsType;
|
||||
/** format of response (i.e. response.json() -> format: "json") */
|
||||
format?: ResponseType;
|
||||
/** request body */
|
||||
body?: unknown;
|
||||
}
|
||||
|
||||
export type RequestParams = Omit<FullRequestParams, "body" | "method" | "query" | "path">;
|
||||
|
||||
export interface ApiConfig<SecurityDataType = unknown> extends Omit<AxiosRequestConfig, "data" | "cancelToken"> {
|
||||
securityWorker?: (securityData: SecurityDataType | null) => Promise<AxiosRequestConfig | void> | AxiosRequestConfig | void;
|
||||
secure?: boolean;
|
||||
format?: ResponseType;
|
||||
}
|
||||
|
||||
export enum ContentType {
|
||||
Json = "application/json",
|
||||
FormData = "multipart/form-data",
|
||||
UrlEncoded = "application/x-www-form-urlencoded",
|
||||
Text = "text/plain",
|
||||
}
|
||||
|
||||
export class HttpClient<SecurityDataType = unknown> {
|
||||
public instance: AxiosInstance;
|
||||
private securityData: SecurityDataType | null = null;
|
||||
private securityWorker?: ApiConfig<SecurityDataType>["securityWorker"];
|
||||
private secure?: boolean;
|
||||
private format?: ResponseType;
|
||||
|
||||
constructor({ securityWorker, secure, format, ...axiosConfig }: ApiConfig<SecurityDataType> = {}) {
|
||||
this.instance = axios.create({ ...axiosConfig, baseURL: axiosConfig.baseURL || "<%~ apiConfig.baseUrl %>" })
|
||||
this.secure = secure;
|
||||
this.format = format;
|
||||
this.securityWorker = securityWorker;
|
||||
}
|
||||
|
||||
public setSecurityData = (data: SecurityDataType | null) => {
|
||||
this.securityData = data
|
||||
}
|
||||
|
||||
protected mergeRequestParams(params1: AxiosRequestConfig, params2?: AxiosRequestConfig): AxiosRequestConfig {
|
||||
const method = params1.method || (params2 && params2.method)
|
||||
|
||||
return {
|
||||
...this.instance.defaults,
|
||||
...params1,
|
||||
...(params2 || {}),
|
||||
headers: {
|
||||
...((method && this.instance.defaults.headers[method.toLowerCase() as keyof HeadersDefaults]) || {}),
|
||||
...(params1.headers || {}),
|
||||
...((params2 && params2.headers) || {}),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
protected stringifyFormItem(formItem: unknown) {
|
||||
if (typeof formItem === "object" && formItem !== null) {
|
||||
return JSON.stringify(formItem);
|
||||
} else {
|
||||
return `${formItem}`;
|
||||
}
|
||||
}
|
||||
|
||||
protected createFormData(input: Record<string, unknown>): FormData {
|
||||
return Object.keys(input || {}).reduce((formData, key) => {
|
||||
const property = input[key];
|
||||
const propertyContent: any[] = (property instanceof Array) ? property : [property]
|
||||
|
||||
for (const formItem of propertyContent) {
|
||||
const isFileType = formItem instanceof Blob || formItem instanceof File;
|
||||
formData.append(
|
||||
key,
|
||||
isFileType ? formItem : this.stringifyFormItem(formItem)
|
||||
);
|
||||
}
|
||||
|
||||
return formData;
|
||||
}, new FormData());
|
||||
}
|
||||
|
||||
public request = async <T = any, _E = any>({
|
||||
secure,
|
||||
path,
|
||||
type,
|
||||
query,
|
||||
format,
|
||||
body,
|
||||
...params
|
||||
<% if (config.unwrapResponseData) { %>
|
||||
}: FullRequestParams): Promise<T> => {
|
||||
<% } else { %>
|
||||
}: FullRequestParams): Promise<AxiosResponse<T>> => {
|
||||
<% } %>
|
||||
const secureParams = ((typeof secure === 'boolean' ? secure : this.secure) && this.securityWorker && (await this.securityWorker(this.securityData))) || {};
|
||||
const requestParams = this.mergeRequestParams(params, secureParams);
|
||||
const responseFormat = (format || this.format) || undefined;
|
||||
|
||||
if (type === ContentType.FormData && body && body !== null && typeof body === "object") {
|
||||
body = this.createFormData(body as Record<string, unknown>);
|
||||
}
|
||||
|
||||
if (type === ContentType.Text && body && body !== null && typeof body !== "string") {
|
||||
body = JSON.stringify(body);
|
||||
}
|
||||
|
||||
return this.instance.request({
|
||||
...requestParams,
|
||||
headers: {
|
||||
...(requestParams.headers || {}),
|
||||
...(type && type !== ContentType.FormData ? { "Content-Type": type } : {}),
|
||||
},
|
||||
params: query,
|
||||
responseType: responseFormat,
|
||||
data: body,
|
||||
url: path,
|
||||
<% if (config.unwrapResponseData) { %>
|
||||
}).then(response => response.data);
|
||||
<% } else { %>
|
||||
});
|
||||
<% } %>
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,224 @@
|
|||
<%
|
||||
const { apiConfig, generateResponses, config } = it;
|
||||
%>
|
||||
|
||||
export type QueryParamsType = Record<string | number, any>;
|
||||
export type ResponseFormat = keyof Omit<Body, "body" | "bodyUsed">;
|
||||
|
||||
export interface FullRequestParams extends Omit<RequestInit, "body"> {
|
||||
/** set parameter to `true` for call `securityWorker` for this request */
|
||||
secure?: boolean;
|
||||
/** request path */
|
||||
path: string;
|
||||
/** content type of request body */
|
||||
type?: ContentType;
|
||||
/** query params */
|
||||
query?: QueryParamsType;
|
||||
/** format of response (i.e. response.json() -> format: "json") */
|
||||
format?: ResponseFormat;
|
||||
/** request body */
|
||||
body?: unknown;
|
||||
/** base url */
|
||||
baseUrl?: string;
|
||||
/** request cancellation token */
|
||||
cancelToken?: CancelToken;
|
||||
}
|
||||
|
||||
export type RequestParams = Omit<FullRequestParams, "body" | "method" | "query" | "path">
|
||||
|
||||
|
||||
export interface ApiConfig<SecurityDataType = unknown> {
|
||||
baseUrl?: string;
|
||||
baseApiParams?: Omit<RequestParams, "baseUrl" | "cancelToken" | "signal">;
|
||||
securityWorker?: (securityData: SecurityDataType | null) => Promise<RequestParams | void> | RequestParams | void;
|
||||
customFetch?: typeof fetch;
|
||||
}
|
||||
|
||||
export interface HttpResponse<D extends unknown, E extends unknown = unknown> extends Response {
|
||||
data: D;
|
||||
error: E;
|
||||
}
|
||||
|
||||
type CancelToken = Symbol | string | number;
|
||||
|
||||
export enum ContentType {
|
||||
Json = "application/json",
|
||||
FormData = "multipart/form-data",
|
||||
UrlEncoded = "application/x-www-form-urlencoded",
|
||||
Text = "text/plain",
|
||||
}
|
||||
|
||||
export class HttpClient<SecurityDataType = unknown> {
|
||||
public baseUrl: string = "<%~ apiConfig.baseUrl %>";
|
||||
private securityData: SecurityDataType | null = null;
|
||||
private securityWorker?: ApiConfig<SecurityDataType>["securityWorker"];
|
||||
private abortControllers = new Map<CancelToken, AbortController>();
|
||||
private customFetch = (...fetchParams: Parameters<typeof fetch>) => fetch(...fetchParams);
|
||||
|
||||
private baseApiParams: RequestParams = {
|
||||
credentials: 'same-origin',
|
||||
headers: {},
|
||||
redirect: 'follow',
|
||||
referrerPolicy: 'no-referrer',
|
||||
}
|
||||
|
||||
constructor(apiConfig: ApiConfig<SecurityDataType> = {}) {
|
||||
Object.assign(this, apiConfig);
|
||||
}
|
||||
|
||||
public setSecurityData = (data: SecurityDataType | null) => {
|
||||
this.securityData = data;
|
||||
}
|
||||
|
||||
protected encodeQueryParam(key: string, value: any) {
|
||||
const encodedKey = encodeURIComponent(key);
|
||||
return `${encodedKey}=${encodeURIComponent(typeof value === "number" ? value : `${value}`)}`;
|
||||
}
|
||||
|
||||
protected addQueryParam(query: QueryParamsType, key: string) {
|
||||
return this.encodeQueryParam(key, query[key]);
|
||||
}
|
||||
|
||||
protected addArrayQueryParam(query: QueryParamsType, key: string) {
|
||||
const value = query[key];
|
||||
return value.map((v: any) => this.encodeQueryParam(key, v)).join("&");
|
||||
}
|
||||
|
||||
protected toQueryString(rawQuery?: QueryParamsType): string {
|
||||
const query = rawQuery || {};
|
||||
const keys = Object.keys(query).filter((key) => "undefined" !== typeof query[key]);
|
||||
return keys
|
||||
.map((key) =>
|
||||
Array.isArray(query[key])
|
||||
? this.addArrayQueryParam(query, key)
|
||||
: this.addQueryParam(query, key),
|
||||
)
|
||||
.join("&");
|
||||
}
|
||||
|
||||
protected addQueryParams(rawQuery?: QueryParamsType): string {
|
||||
const queryString = this.toQueryString(rawQuery);
|
||||
return queryString ? `?${queryString}` : "";
|
||||
}
|
||||
|
||||
private contentFormatters: Record<ContentType, (input: any) => any> = {
|
||||
[ContentType.Json]: (input:any) => input !== null && (typeof input === "object" || typeof input === "string") ? JSON.stringify(input) : input,
|
||||
[ContentType.Text]: (input:any) => input !== null && typeof input !== "string" ? JSON.stringify(input) : input,
|
||||
[ContentType.FormData]: (input: any) =>
|
||||
Object.keys(input || {}).reduce((formData, key) => {
|
||||
const property = input[key];
|
||||
formData.append(
|
||||
key,
|
||||
property instanceof Blob ?
|
||||
property :
|
||||
typeof property === "object" && property !== null ?
|
||||
JSON.stringify(property) :
|
||||
`${property}`
|
||||
);
|
||||
return formData;
|
||||
}, new FormData()),
|
||||
[ContentType.UrlEncoded]: (input: any) => this.toQueryString(input),
|
||||
}
|
||||
|
||||
protected mergeRequestParams(params1: RequestParams, params2?: RequestParams): RequestParams {
|
||||
return {
|
||||
...this.baseApiParams,
|
||||
...params1,
|
||||
...(params2 || {}),
|
||||
headers: {
|
||||
...(this.baseApiParams.headers || {}),
|
||||
...(params1.headers || {}),
|
||||
...((params2 && params2.headers) || {}),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
protected createAbortSignal = (cancelToken: CancelToken): AbortSignal | undefined => {
|
||||
if (this.abortControllers.has(cancelToken)) {
|
||||
const abortController = this.abortControllers.get(cancelToken);
|
||||
if (abortController) {
|
||||
return abortController.signal;
|
||||
}
|
||||
return void 0;
|
||||
}
|
||||
|
||||
const abortController = new AbortController();
|
||||
this.abortControllers.set(cancelToken, abortController);
|
||||
return abortController.signal;
|
||||
}
|
||||
|
||||
public abortRequest = (cancelToken: CancelToken) => {
|
||||
const abortController = this.abortControllers.get(cancelToken)
|
||||
|
||||
if (abortController) {
|
||||
abortController.abort();
|
||||
this.abortControllers.delete(cancelToken);
|
||||
}
|
||||
}
|
||||
|
||||
public request = async <T = any, E = any>({
|
||||
body,
|
||||
secure,
|
||||
path,
|
||||
type,
|
||||
query,
|
||||
format,
|
||||
baseUrl,
|
||||
cancelToken,
|
||||
...params
|
||||
<% if (config.unwrapResponseData) { %>
|
||||
}: FullRequestParams): Promise<T> => {
|
||||
<% } else { %>
|
||||
}: FullRequestParams): Promise<HttpResponse<T, E>> => {
|
||||
<% } %>
|
||||
const secureParams = ((typeof secure === 'boolean' ? secure : this.baseApiParams.secure) && this.securityWorker && await this.securityWorker(this.securityData)) || {};
|
||||
const requestParams = this.mergeRequestParams(params, secureParams);
|
||||
const queryString = query && this.toQueryString(query);
|
||||
const payloadFormatter = this.contentFormatters[type || ContentType.Json];
|
||||
const responseFormat = format || requestParams.format;
|
||||
|
||||
return this.customFetch(
|
||||
`${baseUrl || this.baseUrl || ""}${path}${queryString ? `?${queryString}` : ""}`,
|
||||
{
|
||||
...requestParams,
|
||||
headers: {
|
||||
...(requestParams.headers || {}),
|
||||
...(type && type !== ContentType.FormData ? { "Content-Type": type } : {}),
|
||||
},
|
||||
signal: cancelToken ? this.createAbortSignal(cancelToken) : requestParams.signal,
|
||||
body: typeof body === "undefined" || body === null ? null : payloadFormatter(body),
|
||||
}
|
||||
).then(async (response) => {
|
||||
const r = response as HttpResponse<T, E>;
|
||||
r.data = (null as unknown) as T;
|
||||
r.error = (null as unknown) as E;
|
||||
|
||||
const data = !responseFormat ? r : await response[responseFormat]()
|
||||
.then((data) => {
|
||||
if (r.ok) {
|
||||
r.data = data;
|
||||
} else {
|
||||
r.error = data;
|
||||
}
|
||||
return r;
|
||||
})
|
||||
.catch((e) => {
|
||||
r.error = e;
|
||||
return r;
|
||||
});
|
||||
|
||||
if (cancelToken) {
|
||||
this.abortControllers.delete(cancelToken);
|
||||
}
|
||||
|
||||
<% if (!config.disableThrowOnError) { %>
|
||||
if (!response.ok) throw data;
|
||||
<% } %>
|
||||
<% if (config.unwrapResponseData) { %>
|
||||
return data.data;
|
||||
<% } else { %>
|
||||
return data;
|
||||
<% } %>
|
||||
});
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
<%
|
||||
const { contract, utils } = it;
|
||||
const { formatDescription, require, _ } = utils;
|
||||
%>
|
||||
export interface <%~ contract.name %> {
|
||||
<% _.forEach(contract.$content, (field) => { %>
|
||||
<%~ includeFile('@base/object-field-jsdoc.ejs', { ...it, field }) %>
|
||||
<%~ field.name %><%~ field.isRequired ? '' : '?' %>: <%~ field.value %><%~ field.isNullable ? ' | null' : ''%>;
|
||||
<% }) %>
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
<%
|
||||
const { field, utils } = it;
|
||||
const { formatDescription, require, _ } = utils;
|
||||
|
||||
const comments = _.uniq(
|
||||
_.compact([
|
||||
field.title,
|
||||
field.description,
|
||||
field.deprecated && ` * @deprecated`,
|
||||
!_.isUndefined(field.format) && `@format ${field.format}`,
|
||||
!_.isUndefined(field.minimum) && `@min ${field.minimum}`,
|
||||
!_.isUndefined(field.maximum) && `@max ${field.maximum}`,
|
||||
!_.isUndefined(field.pattern) && `@pattern ${field.pattern}`,
|
||||
!_.isUndefined(field.example) &&
|
||||
`@example ${_.isObject(field.example) ? JSON.stringify(field.example) : field.example}`,
|
||||
]).reduce((acc, comment) => [...acc, ...comment.split(/\n/g)], []),
|
||||
);
|
||||
%>
|
||||
<% if (comments.length === 1) { %>
|
||||
/** <%~ comments[0] %> */
|
||||
<% } else if (comments.length) { %>
|
||||
/**
|
||||
<% comments.forEach(comment => { %>
|
||||
* <%~ comment %>
|
||||
|
||||
<% }) %>
|
||||
*/
|
||||
<% } %>
|
||||
|
|
@ -0,0 +1,101 @@
|
|||
<%
|
||||
const { utils, route, config } = it;
|
||||
const { requestBodyInfo, responseBodyInfo, specificArgNameResolver } = route;
|
||||
const { _, getInlineParseContent, getParseContent, parseSchema, getComponentByRef, require } = utils;
|
||||
const { parameters, path, method, payload, query, formData, security, requestParams } = route.request;
|
||||
const { type, errorType, contentTypes } = route.response;
|
||||
const { HTTP_CLIENT, RESERVED_REQ_PARAMS_ARG_NAMES } = config.constants;
|
||||
const routeDocs = includeFile("./route-docs", { config, route, utils });
|
||||
const queryName = (query && query.name) || "query";
|
||||
const pathParams = _.values(parameters);
|
||||
const pathParamsNames = _.map(pathParams, "name");
|
||||
|
||||
const isFetchTemplate = config.httpClientType === HTTP_CLIENT.FETCH;
|
||||
|
||||
const requestConfigParam = {
|
||||
name: specificArgNameResolver.resolve(RESERVED_REQ_PARAMS_ARG_NAMES),
|
||||
optional: true,
|
||||
type: "RequestParams",
|
||||
defaultValue: "{}",
|
||||
}
|
||||
|
||||
const argToTmpl = ({ name, optional, type, defaultValue }) => `${name}${!defaultValue && optional ? '?' : ''}: ${type}${defaultValue ? ` = ${defaultValue}` : ''}`;
|
||||
|
||||
const rawWrapperArgs = config.extractRequestParams ?
|
||||
_.compact([
|
||||
requestParams && {
|
||||
name: pathParams.length ? `{ ${_.join(pathParamsNames, ", ")}, ...${queryName} }` : queryName,
|
||||
optional: false,
|
||||
type: getInlineParseContent(requestParams),
|
||||
},
|
||||
...(!requestParams ? pathParams : []),
|
||||
payload,
|
||||
requestConfigParam,
|
||||
]) :
|
||||
_.compact([
|
||||
...pathParams,
|
||||
query,
|
||||
payload,
|
||||
requestConfigParam,
|
||||
])
|
||||
|
||||
const wrapperArgs = _
|
||||
// Sort by optionality
|
||||
.sortBy(rawWrapperArgs, [o => o.optional])
|
||||
.map(argToTmpl)
|
||||
.join(', ')
|
||||
|
||||
// RequestParams["type"]
|
||||
const requestContentKind = {
|
||||
"JSON": "ContentType.Json",
|
||||
"URL_ENCODED": "ContentType.UrlEncoded",
|
||||
"FORM_DATA": "ContentType.FormData",
|
||||
"TEXT": "ContentType.Text",
|
||||
}
|
||||
// RequestParams["format"]
|
||||
const responseContentKind = {
|
||||
"JSON": '"json"',
|
||||
"IMAGE": '"blob"',
|
||||
"FORM_DATA": isFetchTemplate ? '"formData"' : '"document"'
|
||||
}
|
||||
|
||||
const bodyTmpl = _.get(payload, "name") || null;
|
||||
const queryTmpl = (query != null && queryName) || null;
|
||||
const bodyContentKindTmpl = requestContentKind[requestBodyInfo.contentKind] || null;
|
||||
const responseFormatTmpl = responseContentKind[responseBodyInfo.success && responseBodyInfo.success.schema && responseBodyInfo.success.schema.contentKind] || null;
|
||||
const securityTmpl = security ? 'true' : null;
|
||||
|
||||
const describeReturnType = () => {
|
||||
if (!config.toJS) return "";
|
||||
|
||||
switch(config.httpClientType) {
|
||||
case HTTP_CLIENT.AXIOS: {
|
||||
return `Promise<AxiosResponse<${type}>>`
|
||||
}
|
||||
default: {
|
||||
return `Promise<HttpResponse<${type}, ${errorType}>`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
%>
|
||||
/**
|
||||
<%~ routeDocs.description %>
|
||||
|
||||
*<% /* Here you can add some other JSDoc tags */ %>
|
||||
|
||||
<%~ routeDocs.lines %>
|
||||
|
||||
*/
|
||||
<%~ route.routeName.usage %><%~ route.namespace ? ': ' : ' = ' %>(<%~ wrapperArgs %>)<%~ config.toJS ? `: ${describeReturnType()}` : "" %> => {
|
||||
return <%~ config.singleHttpClient ? 'this.http.request' : 'this.request' %><<%~ type %>, <%~ errorType %>>({
|
||||
path: `<%~ path %>`,
|
||||
method: '<%~ _.upperCase(method) %>',
|
||||
<%~ queryTmpl ? `query: ${queryTmpl},` : '' %>
|
||||
<%~ bodyTmpl ? `body: ${bodyTmpl},` : '' %>
|
||||
<%~ securityTmpl ? `secure: ${securityTmpl},` : '' %>
|
||||
<%~ bodyContentKindTmpl ? `type: ${bodyContentKindTmpl},` : '' %>
|
||||
<%~ responseFormatTmpl ? `format: ${responseFormatTmpl},` : '' %>
|
||||
...<%~ _.get(requestConfigParam, "name") %>,
|
||||
})
|
||||
}<%~ route.namespace ? ',' : '' %>
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
<%
|
||||
const { config, route, utils } = it;
|
||||
const { _, formatDescription, fmtToJSDocLine, pascalCase, require } = utils;
|
||||
const { raw, request, routeName } = route;
|
||||
|
||||
const jsDocDescription = raw.description ?
|
||||
` * @description ${formatDescription(raw.description, true)}` :
|
||||
fmtToJSDocLine('No description', { eol: false });
|
||||
const jsDocLines = _.compact([
|
||||
_.size(raw.tags) && ` * @tags ${raw.tags.join(", ")}`,
|
||||
` * @name ${pascalCase(routeName.usage)}`,
|
||||
raw.summary && ` * @summary ${raw.summary}`,
|
||||
` * @request ${_.upperCase(request.method)}:${raw.route}`,
|
||||
raw.deprecated && ` * @deprecated`,
|
||||
routeName.duplicate && ` * @originalName ${routeName.original}`,
|
||||
routeName.duplicate && ` * @duplicate`,
|
||||
request.security && ` * @secure`,
|
||||
...(config.generateResponses && raw.responsesTypes.length
|
||||
? raw.responsesTypes.map(
|
||||
({ type, status, description, isSuccess }) =>
|
||||
` * @response \`${status}\` \`${_.replace(_.replace(type, /\/\*/g, "\\*"), /\*\//g, "*\\")}\` ${description}`,
|
||||
)
|
||||
: []),
|
||||
]).map(str => str.trimEnd()).join("\n");
|
||||
|
||||
return {
|
||||
description: jsDocDescription,
|
||||
lines: jsDocLines,
|
||||
}
|
||||
%>
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
<%
|
||||
const { routeInfo, utils } = it;
|
||||
const {
|
||||
operationId,
|
||||
method,
|
||||
route,
|
||||
moduleName,
|
||||
responsesTypes,
|
||||
description,
|
||||
tags,
|
||||
summary,
|
||||
pathArgs,
|
||||
} = routeInfo;
|
||||
const { _, fmtToJSDocLine, require } = utils;
|
||||
|
||||
const methodAliases = {
|
||||
get: (pathName, hasPathInserts) =>
|
||||
_.camelCase(`${pathName}_${hasPathInserts ? "detail" : "list"}`),
|
||||
post: (pathName, hasPathInserts) => _.camelCase(`${pathName}_create`),
|
||||
put: (pathName, hasPathInserts) => _.camelCase(`${pathName}_update`),
|
||||
patch: (pathName, hasPathInserts) => _.camelCase(`${pathName}_partial_update`),
|
||||
delete: (pathName, hasPathInserts) => _.camelCase(`${pathName}_delete`),
|
||||
};
|
||||
|
||||
const createCustomOperationId = (method, route, moduleName) => {
|
||||
const hasPathInserts = /\{(\w){1,}\}/g.test(route);
|
||||
const splitedRouteBySlash = _.compact(_.replace(route, /\{(\w){1,}\}/g, "").split("/"));
|
||||
const routeParts = (splitedRouteBySlash.length > 1
|
||||
? splitedRouteBySlash.splice(1)
|
||||
: splitedRouteBySlash
|
||||
).join("_");
|
||||
return routeParts.length > 3 && methodAliases[method]
|
||||
? methodAliases[method](routeParts, hasPathInserts)
|
||||
: _.camelCase(_.lowerCase(method) + "_" + [moduleName].join("_")) || "index";
|
||||
};
|
||||
|
||||
if (operationId)
|
||||
return _.camelCase(operationId);
|
||||
if (route === "/")
|
||||
return _.camelCase(`${_.lowerCase(method)}Root`);
|
||||
|
||||
return createCustomOperationId(method, route, moduleName);
|
||||
%>
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
<%
|
||||
const { route, utils, config } = it;
|
||||
const { _, pascalCase, require } = utils;
|
||||
const { query, payload, pathParams, headers } = route.request;
|
||||
|
||||
const routeDocs = includeFile("@base/route-docs", { config, route, utils });
|
||||
const routeNamespace = pascalCase(route.routeName.usage);
|
||||
|
||||
%>
|
||||
/**
|
||||
<%~ routeDocs.description %>
|
||||
|
||||
<%~ routeDocs.lines %>
|
||||
|
||||
*/
|
||||
export namespace <%~ routeNamespace %> {
|
||||
export type RequestParams = <%~ (pathParams && pathParams.type) || '{}' %>;
|
||||
export type RequestQuery = <%~ (query && query.type) || '{}' %>;
|
||||
export type RequestBody = <%~ (payload && payload.type) || 'never' %>;
|
||||
export type RequestHeaders = <%~ (headers && headers.type) || '{}' %>;
|
||||
export type ResponseBody = <%~ route.response.type %>;
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
<%
|
||||
const { utils, config, routes, modelTypes } = it;
|
||||
const { _, pascalCase } = utils;
|
||||
const dataContracts = config.modular ? _.map(modelTypes, "name") : [];
|
||||
%>
|
||||
|
||||
<% if (dataContracts.length) { %>
|
||||
import { <%~ dataContracts.join(", ") %> } from "./<%~ config.fileNames.dataContracts %>"
|
||||
<% } %>
|
||||
|
||||
<%
|
||||
/* TODO: outOfModule, combined should be attributes of route, which will allow to avoid duplication of code */
|
||||
%>
|
||||
|
||||
<% routes.outOfModule && routes.outOfModule.forEach(({ routes = [] }) => { %>
|
||||
<% routes.forEach((route) => { %>
|
||||
<%~ includeFile('@base/route-type.ejs', { ...it, route }) %>
|
||||
<% }) %>
|
||||
<% }) %>
|
||||
|
||||
<% routes.combined && routes.combined.forEach(({ routes = [], moduleName }) => { %>
|
||||
export namespace <%~ pascalCase(moduleName) %> {
|
||||
<% routes.forEach((route) => { %>
|
||||
<%~ includeFile('@base/route-type.ejs', { ...it, route }) %>
|
||||
<% }) %>
|
||||
}
|
||||
|
||||
<% }) %>
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
<%
|
||||
const { contract, utils } = it;
|
||||
const { formatDescription, require, _ } = utils;
|
||||
|
||||
%>
|
||||
<% if (contract.$content.length) { %>
|
||||
export type <%~ contract.name %> = {
|
||||
<% _.forEach(contract.$content, (field) => { %>
|
||||
<%~ includeFile('@base/object-field-jsdoc.ejs', { ...it, field }) %>
|
||||
<%~ field.field %>;
|
||||
<% }) %>
|
||||
}<%~ utils.isNeedToAddNull(contract) ? ' | null' : ''%>
|
||||
<% } else { %>
|
||||
export type <%~ contract.name %> = Record<string, any>;
|
||||
<% } %>
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
/**
|
||||
* 模板生成器
|
||||
* @param {import('plop').NodePlopAPI} plop
|
||||
*/
|
||||
export default function (plop) {
|
||||
plop.setGenerator('route', {
|
||||
description: '创建一个路由',
|
||||
prompts: [
|
||||
{
|
||||
type: 'input',
|
||||
name: 'name',
|
||||
message: '请输入路由名称',
|
||||
validate: (value) => {
|
||||
if (!value) {
|
||||
return '请输入路由名称';
|
||||
}
|
||||
return true;
|
||||
},
|
||||
},
|
||||
],
|
||||
actions: [
|
||||
{
|
||||
type: 'add',
|
||||
path: '../../src/pages/{{name}}.vue',
|
||||
templateFile: 'template-page.hbs',
|
||||
},
|
||||
{
|
||||
type: 'add',
|
||||
path: '../../src/pages/{{name}}/index.vue',
|
||||
templateFile: 'template-page.hbs',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
plop.setGenerator('page', {
|
||||
description: '创建一个页面',
|
||||
prompts: [
|
||||
{
|
||||
type: 'input',
|
||||
name: 'name',
|
||||
message: '请输入页面名称',
|
||||
validate: (value) => {
|
||||
if (!value) {
|
||||
return '请输入页面名称';
|
||||
}
|
||||
return true;
|
||||
},
|
||||
},
|
||||
],
|
||||
actions: [
|
||||
{
|
||||
type: 'add',
|
||||
path: '../../src/pages/{{name}}.vue',
|
||||
templateFile: 'template-page.hbs',
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
<template>
|
||||
<div class="p-4">
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<style scoped></style>
|
||||
|
||||
<route lang="json">
|
||||
{
|
||||
"meta": {
|
||||
"sort": 101,
|
||||
"title": "{{name}}",
|
||||
"icon": "icon-park-outline-home"
|
||||
}
|
||||
}
|
||||
</route>
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
const loadTemplate = (name) => {
|
||||
const filePath = path.join(__dirname, `template-${name}.hbs`);
|
||||
return fs.readFileSync(filePath, "utf8");
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
git: {
|
||||
commitMessage: "chore(release): v${version}",
|
||||
},
|
||||
npm: {
|
||||
publish: false,
|
||||
},
|
||||
github: {
|
||||
release: false,
|
||||
},
|
||||
gitlab: {
|
||||
release: false,
|
||||
},
|
||||
plugins: {
|
||||
"@release-it/conventional-changelog": {
|
||||
ignoreRecommendedBump: true,
|
||||
infile: "CHANGELOG.md",
|
||||
header: "# 版本记录",
|
||||
preset: {
|
||||
name: "conventionalcommits",
|
||||
types: [
|
||||
{
|
||||
type: "feat",
|
||||
section: "✨功能新增",
|
||||
},
|
||||
{
|
||||
type: "impr",
|
||||
section: "⚡️改进优化",
|
||||
},
|
||||
{
|
||||
type: "fix",
|
||||
section: "🐛问题修复",
|
||||
},
|
||||
],
|
||||
},
|
||||
context: {
|
||||
host: "https://github.com",
|
||||
owner: "juetan",
|
||||
repository: "template-vue",
|
||||
},
|
||||
gitRawCommitsOpts: {
|
||||
format:
|
||||
"%B%n-hash-%n%H%n-shortHash-%n%h%n-gitTags-%n%d%n-committerDate-%n%ci%n-author-%n%an%n-email-%n%ae%n-date-%n%ci",
|
||||
},
|
||||
writerOpts: {
|
||||
commitsSort: false,
|
||||
commitGroupsSort: (a, b) => {
|
||||
const order = ["✨功能新增", "⚡️改进优化", "🐛问题修复"];
|
||||
return order.indexOf(a.title) - order.indexOf(b.title);
|
||||
},
|
||||
commitPartial: loadTemplate("commit"),
|
||||
// headerPartial: loadTemplate('header'),
|
||||
mainTemplate: loadTemplate("main"),
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
* [{{type}}] {{subject}}
|
||||
|
||||
{{~!-- commit link --}}
|
||||
{{~#if @root.linkReferences}} ([{{shortHash}}](
|
||||
{{~#if @root.repository}}
|
||||
{{~#if @root.host}}
|
||||
{{~@root.host}}/
|
||||
{{~/if}}
|
||||
{{~#if @root.owner}}
|
||||
{{~@root.owner}}/
|
||||
{{~/if}}
|
||||
{{~@root.repository}}
|
||||
{{~else}}
|
||||
{{~@root.repoUrl}}
|
||||
{{~/if}}/
|
||||
{{~@root.commit}}/{{hash}}))
|
||||
{{~else if hash}} {{hash}}{{~/if}}
|
||||
|
||||
{{~!-- commit references --}}
|
||||
{{~#if references~}}
|
||||
, closes
|
||||
{{~#each references}} {{#if @root.linkReferences~}}
|
||||
[
|
||||
{{~#if this.owner}}
|
||||
{{~this.owner}}/
|
||||
{{~/if}}
|
||||
{{~this.repository}}#{{this.issue}}](
|
||||
{{~#if @root.repository}}
|
||||
{{~#if @root.host}}
|
||||
{{~@root.host}}/
|
||||
{{~/if}}
|
||||
{{~#if this.repository}}
|
||||
{{~#if this.owner}}
|
||||
{{~this.owner}}/
|
||||
{{~/if}}
|
||||
{{~this.repository}}
|
||||
{{~else}}
|
||||
{{~#if @root.owner}}
|
||||
{{~@root.owner}}/
|
||||
{{~/if}}
|
||||
{{~@root.repository}}
|
||||
{{~/if}}
|
||||
{{~else}}
|
||||
{{~@root.repoUrl}}
|
||||
{{~/if}}/
|
||||
{{~@root.issue}}/{{this.issue}})
|
||||
{{~else}}
|
||||
{{~#if this.owner}}
|
||||
{{~this.owner}}/
|
||||
{{~/if}}
|
||||
{{~this.repository}}#{{this.issue}}
|
||||
{{~/if}}{{/each}}
|
||||
{{~/if}}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
{{> header}}
|
||||
|
||||
{{#each commitGroups}}
|
||||
{{#each commits}}
|
||||
{{> commit root=@root}}
|
||||
{{/each}}
|
||||
{{/each}}
|
||||
|
||||
{{> footer}}
|
||||
|
|
@ -0,0 +1,115 @@
|
|||
import { spawn } from "child_process";
|
||||
import fs from "fs";
|
||||
import { Plugin, ResolvedConfig } from "vite";
|
||||
import pkg from "../../package.json";
|
||||
|
||||
/**
|
||||
* 项目 logo
|
||||
* @description 内容:APPTIFY
|
||||
*/
|
||||
const LOGO = `
|
||||
________ ______ ______ _________ ________ ______ __ __
|
||||
/_______/\\\\ /_____/\\\\ /_____/\\\\ /________/\\\\/_______/\\\\/_____/\\\\ /_/\\\\/_/\\\\
|
||||
\\\\::: _ \\\\ \\\\\\\\:::_ \\\\ \\\\\\\\:::_ \\\\ \\\\\\\\__.::.__\\\\/\\\\__.::._\\\\/\\\\::::_\\\\/_\\\\ \\\\ \\\\ \\\\ \\\\
|
||||
\\\\::(_) \\\\ \\\\\\\\:(_) \\\\ \\\\\\\\:(_) \\\\ \\\\ \\\\::\\\\ \\\\ \\\\::\\\\ \\\\ \\\\:\\\\/___/\\\\\\\\:\\\\_\\\\ \\\\ \\\\
|
||||
\\\\:: __ \\\\ \\\\\\\\: ___\\\\/ \\\\: ___\\\\/ \\\\::\\\\ \\\\ _\\\\::\\\\ \\\\__\\\\:::._\\\\/ \\\\::::_\\\\/
|
||||
\\\\:.\\\\ \\\\ \\\\ \\\\\\\\ \\\\ \\\\ \\\\ \\\\ \\\\ \\\\::\\\\ \\\\ /__\\\\::\\\\__/\\\\\\\\:\\\\ \\\\ \\\\::\\\\ \\\\
|
||||
\\\\__\\\\/\\\\__\\\\/ \\\\_\\\\/ \\\\_\\\\/ \\\\__\\\\/ \\\\________\\\\/ \\\\_\\\\/ \\\\__\\\\/
|
||||
`;
|
||||
|
||||
/**
|
||||
* 以 shell 形式执行命令,成功返回输出的字符串
|
||||
* @param cmd 命令
|
||||
* @returns Promise<string | void>
|
||||
*/
|
||||
const exec = (cmd: string) => {
|
||||
return new Promise<string | void>((resolve) => {
|
||||
if (!cmd) {
|
||||
return resolve();
|
||||
}
|
||||
const child = spawn(cmd, [], { shell: true });
|
||||
child.stdout.once("data", (data) => {
|
||||
resolve(data.toString().replace(/"|\n/g, ""));
|
||||
});
|
||||
child.stderr.once("data", () => {
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取构建信息
|
||||
* @returns Promise<string>
|
||||
*/
|
||||
const getBuildInfo = async () => {
|
||||
const hash = await exec("git log --format=%h -n 1");
|
||||
const time = new Date().toLocaleString("zh-Hans-CN");
|
||||
const latestTag = await exec("git describe --tags --abbrev=0");
|
||||
const commits = await exec(`git rev-list --count ${latestTag}..HEAD`);
|
||||
const version = commits ? `${latestTag}.${commits}` : `v${pkg.version}`;
|
||||
const content = ` 欢迎访问!版本: ${version} 标识: ${hash} 构建时间: ${time}`;
|
||||
const style = `"color: #09f; font-weight: 900;", "font-size: 12px; color: #09f; font-family: ''"`;
|
||||
const script = `console.log(\`%c${LOGO} \n%c${content}\n\`, ${style});\n`;
|
||||
return script;
|
||||
};
|
||||
|
||||
/**
|
||||
* 项目构建插件
|
||||
* @returns Plugin
|
||||
*/
|
||||
export default function plugin(): Plugin {
|
||||
let config: ResolvedConfig;
|
||||
let extension: string;
|
||||
|
||||
return {
|
||||
name: "vite:customizer",
|
||||
enforce: "pre",
|
||||
|
||||
configResolved(resolvedConfig) {
|
||||
config = resolvedConfig;
|
||||
const defaultExt = config.mode === "development" ? "dev" : "prod";
|
||||
extension = config.env.VITE_BUILD_EXTENTION || defaultExt;
|
||||
},
|
||||
|
||||
async transformIndexHtml(html) {
|
||||
const script = await getBuildInfo();
|
||||
const replacedHtml = html.replace(/__((\w|_|-)+)__/g, (match, p1) => {
|
||||
return config.env[`VITE_${p1}`] || "";
|
||||
});
|
||||
return {
|
||||
html: replacedHtml,
|
||||
tags: [
|
||||
{
|
||||
tag: "script",
|
||||
injectTo: "body",
|
||||
children: script,
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
|
||||
async resolveId(id, importer, options) {
|
||||
if (!extension || !id.startsWith("/src")) {
|
||||
return;
|
||||
}
|
||||
const resolution = await this.resolve(id, importer, { skipSelf: true, ...options });
|
||||
const targetPath = resolution?.id.replace(/\.([^.]*?)$/, `.${extension}.$1`);
|
||||
if (targetPath && fs.existsSync(targetPath)) {
|
||||
return targetPath;
|
||||
}
|
||||
},
|
||||
|
||||
load(id) {
|
||||
if (!extension || !id.includes("src")) {
|
||||
return;
|
||||
}
|
||||
if (id.includes("?")) {
|
||||
return;
|
||||
}
|
||||
const targetPath = id.replace(/\.([^.]*?)$/, `.${extension}.$1`);
|
||||
if (targetPath && fs.existsSync(targetPath)) {
|
||||
return fs.readFileSync(targetPath, "utf-8");
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
<template>
|
||||
<a-config-provider>
|
||||
<router-view v-slot="{ Component }">
|
||||
<page-403 v-if="Math.random() > 0.99"></page-403>
|
||||
<component v-else :is="Component"></component>
|
||||
</router-view>
|
||||
</a-config-provider>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
export * from "./instance";
|
||||
export * from "./service";
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
import "axios";
|
||||
import { IToastOptions } from "@/components";
|
||||
|
||||
declare module "axios" {
|
||||
interface AxiosRequestConfig {
|
||||
/**
|
||||
* toast config
|
||||
* @default false
|
||||
*/
|
||||
toast?: boolean | string | IToastOptions;
|
||||
/**
|
||||
* close toast(internal)
|
||||
* @private
|
||||
*/
|
||||
_closeToast?: () => void;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
export * from "./instance";
|
||||
export * from "./useRequest";
|
||||
|
||||
|
|
@ -0,0 +1,85 @@
|
|||
import { Api } from "../service";
|
||||
import { toast, IToastOptions } from "@/components";
|
||||
import { useUserStore, store } from "@/store";
|
||||
|
||||
const userStore = useUserStore(store);
|
||||
|
||||
/**
|
||||
* 自定义扩展, 例如添加额外的请求函数
|
||||
*/
|
||||
class Service extends Api<unknown> {
|
||||
github = {
|
||||
/**
|
||||
* 获取当前仓库信息
|
||||
*/
|
||||
getRepoInfo: async () => {
|
||||
const info: Record<string, any> = await this.request({
|
||||
baseURL: "https://api.github.com",
|
||||
path: "/repos/juetan/apptify-admin",
|
||||
method: "GET",
|
||||
});
|
||||
return info;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* api实例
|
||||
* @see src/api/instance/instance.ts
|
||||
*/
|
||||
const api = new Service({
|
||||
baseURL: import.meta.env.VITE_API_BASE_URL,
|
||||
});
|
||||
|
||||
/**
|
||||
* 请求拦截器
|
||||
*/
|
||||
api.instance.interceptors.request.use(
|
||||
(config) => {
|
||||
if (userStore.accessToken) {
|
||||
config.headers.Authorization = `Bearer ${userStore.accessToken}`;
|
||||
}
|
||||
if (config.toast) {
|
||||
let options: IToastOptions = {};
|
||||
if (typeof config.toast === "string") {
|
||||
options = {
|
||||
message: config.toast,
|
||||
};
|
||||
}
|
||||
if (typeof config.toast === "object") {
|
||||
options = config.toast;
|
||||
}
|
||||
config._closeToast = toast(options);
|
||||
}
|
||||
return config;
|
||||
},
|
||||
(error) => {
|
||||
error.config?._closeToast?.();
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* 响应拦截器
|
||||
*/
|
||||
api.instance.interceptors.response.use(
|
||||
(res) => {
|
||||
res.config?._closeToast?.();
|
||||
if (res.data?.code && res.data.code !== 2000) {
|
||||
return Promise.reject(res);
|
||||
}
|
||||
return res;
|
||||
},
|
||||
(error) => {
|
||||
error.config?._closeToast?.();
|
||||
if (error.request) {
|
||||
console.log("request error", error.request);
|
||||
}
|
||||
if (error.response) {
|
||||
console.log("response error", error.response);
|
||||
}
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
export { api };
|
||||
|
|
@ -0,0 +1,179 @@
|
|||
type PromiseFn = (...args: any[]) => Promise<any>;
|
||||
|
||||
type Options<T extends PromiseFn = PromiseFn> = {
|
||||
/**
|
||||
* 是否显示全局的 loading
|
||||
*/
|
||||
toast?: boolean | string;
|
||||
/**
|
||||
* 是否立即执行
|
||||
*/
|
||||
initialParams?: boolean | Parameters<T>;
|
||||
/**
|
||||
* 默认值
|
||||
*/
|
||||
initialData?: Partial<Awaited<ReturnType<T>>>;
|
||||
/**
|
||||
* 请求失败后重试的次数
|
||||
*/
|
||||
retry?: number;
|
||||
/**
|
||||
* 请求失败后重试的间隔(ms)
|
||||
*/
|
||||
retryDelay?: number;
|
||||
/**
|
||||
* 轮询间隔(ms)
|
||||
*/
|
||||
interval?: number;
|
||||
/**
|
||||
* 请求前回调
|
||||
*/
|
||||
onBefore?: (args: Parameters<T>) => void;
|
||||
/**
|
||||
* 请求成功回调
|
||||
*/
|
||||
onSuccess?: (data: Awaited<ReturnType<T>>) => void;
|
||||
/**
|
||||
* 请求失败回调
|
||||
*/
|
||||
onError?: (error: unknown) => void;
|
||||
/**
|
||||
* 请求结束回调
|
||||
*/
|
||||
onFinally?: () => void;
|
||||
};
|
||||
|
||||
type State<T extends PromiseFn = PromiseFn, D = Awaited<ReturnType<T>>> = {
|
||||
/**
|
||||
* 请求返回的数据
|
||||
*/
|
||||
data: D | undefined;
|
||||
/**
|
||||
* 请求返回的错误
|
||||
*/
|
||||
error: unknown;
|
||||
/**
|
||||
* 请求是否中
|
||||
*/
|
||||
loading: boolean;
|
||||
/**
|
||||
* 发送请求
|
||||
*/
|
||||
send: (...args: Parameters<T>) => Promise<[unknown, undefined] | [undefined, D]>;
|
||||
/**
|
||||
* 取消请求
|
||||
*/
|
||||
cancel: () => void;
|
||||
};
|
||||
|
||||
const log = (...args: any[]) => {
|
||||
if (process.env.NODE_ENV === "development") {
|
||||
console.log(...args);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 包装请求函数,返回响应式状态和请求方法
|
||||
* @see src/api/instance/useRequest.ts
|
||||
*/
|
||||
export function useRequest<T extends PromiseFn>(fn: T, options: Options<T> = {}) {
|
||||
const {
|
||||
initialParams,
|
||||
retry,
|
||||
retryDelay = 0,
|
||||
interval,
|
||||
initialData,
|
||||
onBefore,
|
||||
onSuccess,
|
||||
onError,
|
||||
onFinally,
|
||||
} = options;
|
||||
|
||||
const state = reactive<State<T>>({
|
||||
data: initialData,
|
||||
error: null,
|
||||
loading: false,
|
||||
send: null,
|
||||
cancel: null,
|
||||
} as any);
|
||||
|
||||
const inner = {
|
||||
canceled: false,
|
||||
retryCount: 0,
|
||||
retryTimer: 0 as any,
|
||||
intervalTimer: 0 as any,
|
||||
latestParams: (initialParams || []) as any,
|
||||
clearAllTimer: () => {
|
||||
inner.retryTimer && clearTimeout(inner.retryTimer);
|
||||
inner.intervalTimer && clearTimeout(inner.intervalTimer);
|
||||
},
|
||||
};
|
||||
|
||||
const _send: any = async (...args: Parameters<T>) => {
|
||||
let data;
|
||||
let error;
|
||||
inner.retryCount && log(`retry: ${inner.retryCount}`);
|
||||
try {
|
||||
state.loading = true;
|
||||
onBefore?.(args);
|
||||
const res = await fn(...args);
|
||||
inner.retryCount = 0;
|
||||
if (!inner.canceled) {
|
||||
onSuccess?.(res.data);
|
||||
data = res.data;
|
||||
}
|
||||
} catch (err) {
|
||||
if (!inner.canceled) {
|
||||
error = err;
|
||||
onError?.(err);
|
||||
if (retry && retry > 0 && inner.retryCount < retry) {
|
||||
inner.retryCount++;
|
||||
inner.retryTimer = setTimeout(() => {
|
||||
_send(...args);
|
||||
}, retryDelay);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
log("finally");
|
||||
state.loading = false;
|
||||
state.error = error;
|
||||
if (!error) {
|
||||
state.data = data;
|
||||
}
|
||||
if (!inner.canceled) {
|
||||
onFinally?.();
|
||||
if (!inner.retryCount && interval && interval > 0) {
|
||||
inner.intervalTimer = setTimeout(() => {
|
||||
_send(...args);
|
||||
}, interval);
|
||||
}
|
||||
}
|
||||
}
|
||||
return [error, data];
|
||||
};
|
||||
|
||||
state.cancel = () => {
|
||||
inner.canceled = true;
|
||||
inner.clearAllTimer();
|
||||
};
|
||||
|
||||
state.send = (...args: Parameters<T>) => {
|
||||
inner.canceled = false;
|
||||
inner.retryCount = 0;
|
||||
inner.latestParams = args;
|
||||
inner.clearAllTimer();
|
||||
return _send(...args);
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
if (initialParams) {
|
||||
state.send(...(Array.isArray(initialParams) ? initialParams : ([] as any)));
|
||||
}
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
state.cancel();
|
||||
});
|
||||
|
||||
return state;
|
||||
}
|
||||
|
|
@ -0,0 +1,622 @@
|
|||
/* eslint-disable */
|
||||
/* tslint:disable */
|
||||
/*
|
||||
* ---------------------------------------------------------------
|
||||
* ## THIS FILE WAS GENERATED VIA SWAGGER-TYPESCRIPT-API ##
|
||||
* ## ##
|
||||
* ## AUTHOR: acacode ##
|
||||
* ## SOURCE: https://github.com/acacode/swagger-typescript-api ##
|
||||
* ---------------------------------------------------------------
|
||||
*/
|
||||
|
||||
export interface ApiResponse {
|
||||
/** @format int32 */
|
||||
code?: number;
|
||||
type?: string;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
export interface Category {
|
||||
/** @format int64 */
|
||||
id?: number;
|
||||
name?: string;
|
||||
}
|
||||
|
||||
export interface Pet {
|
||||
/** @format int64 */
|
||||
id?: number;
|
||||
category?: Category;
|
||||
/** @example "doggie" */
|
||||
name: string;
|
||||
photoUrls: string[];
|
||||
tags?: Tag[];
|
||||
/** pet status in the store */
|
||||
status?: "available" | "pending" | "sold";
|
||||
}
|
||||
|
||||
export interface Tag {
|
||||
/** @format int64 */
|
||||
id?: number;
|
||||
name?: string;
|
||||
}
|
||||
|
||||
export interface Order {
|
||||
/** @format int64 */
|
||||
id?: number;
|
||||
/** @format int64 */
|
||||
petId?: number;
|
||||
/** @format int32 */
|
||||
quantity?: number;
|
||||
/** @format date-time */
|
||||
shipDate?: string;
|
||||
/** Order Status */
|
||||
status?: "placed" | "approved" | "delivered";
|
||||
complete?: boolean;
|
||||
}
|
||||
|
||||
export interface User {
|
||||
/** @format int64 */
|
||||
id?: number;
|
||||
username?: string;
|
||||
firstName?: string;
|
||||
lastName?: string;
|
||||
email?: string;
|
||||
password?: string;
|
||||
phone?: string;
|
||||
/**
|
||||
* User Status
|
||||
* @format int32
|
||||
*/
|
||||
userStatus?: number;
|
||||
}
|
||||
|
||||
export interface FindPetsByStatusParams {
|
||||
/** Status values that need to be considered for filter */
|
||||
status: ("available" | "pending" | "sold")[];
|
||||
}
|
||||
|
||||
export interface FindPetsByTagsParams {
|
||||
/** Tags to filter by */
|
||||
tags: string[];
|
||||
}
|
||||
|
||||
export interface LoginUserParams {
|
||||
/** The user name for login */
|
||||
username: string;
|
||||
/** The password for login in clear text */
|
||||
password: string;
|
||||
}
|
||||
|
||||
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse, HeadersDefaults, ResponseType } from "axios";
|
||||
|
||||
export type QueryParamsType = Record<string | number, any>;
|
||||
|
||||
export interface FullRequestParams extends Omit<AxiosRequestConfig, "data" | "params" | "url" | "responseType"> {
|
||||
/** set11 parameter to `true` for call `securityWorker` for this request */
|
||||
secure?: boolean;
|
||||
/** request path */
|
||||
path: string;
|
||||
/** content type of request body */
|
||||
type?: ContentType;
|
||||
/** query params */
|
||||
query?: QueryParamsType;
|
||||
/** format of response (i.e. response.json() -> format: "json") */
|
||||
format?: ResponseType;
|
||||
/** request body */
|
||||
body?: unknown;
|
||||
}
|
||||
|
||||
export type RequestParams = Omit<FullRequestParams, "body" | "method" | "query" | "path">;
|
||||
|
||||
export interface ApiConfig<SecurityDataType = unknown> extends Omit<AxiosRequestConfig, "data" | "cancelToken"> {
|
||||
securityWorker?: (
|
||||
securityData: SecurityDataType | null,
|
||||
) => Promise<AxiosRequestConfig | void> | AxiosRequestConfig | void;
|
||||
secure?: boolean;
|
||||
format?: ResponseType;
|
||||
}
|
||||
|
||||
export enum ContentType {
|
||||
Json = "application/json",
|
||||
FormData = "multipart/form-data",
|
||||
UrlEncoded = "application/x-www-form-urlencoded",
|
||||
Text = "text/plain",
|
||||
}
|
||||
|
||||
export class HttpClient<SecurityDataType = unknown> {
|
||||
public instance: AxiosInstance;
|
||||
private securityData: SecurityDataType | null = null;
|
||||
private securityWorker?: ApiConfig<SecurityDataType>["securityWorker"];
|
||||
private secure?: boolean;
|
||||
private format?: ResponseType;
|
||||
|
||||
constructor({ securityWorker, secure, format, ...axiosConfig }: ApiConfig<SecurityDataType> = {}) {
|
||||
this.instance = axios.create({ ...axiosConfig, baseURL: axiosConfig.baseURL || "https://petstore.swagger.io/v2" });
|
||||
this.secure = secure;
|
||||
this.format = format;
|
||||
this.securityWorker = securityWorker;
|
||||
}
|
||||
|
||||
public setSecurityData = (data: SecurityDataType | null) => {
|
||||
this.securityData = data;
|
||||
};
|
||||
|
||||
protected mergeRequestParams(params1: AxiosRequestConfig, params2?: AxiosRequestConfig): AxiosRequestConfig {
|
||||
const method = params1.method || (params2 && params2.method);
|
||||
|
||||
return {
|
||||
...this.instance.defaults,
|
||||
...params1,
|
||||
...(params2 || {}),
|
||||
headers: {
|
||||
...((method && this.instance.defaults.headers[method.toLowerCase() as keyof HeadersDefaults]) || {}),
|
||||
...(params1.headers || {}),
|
||||
...((params2 && params2.headers) || {}),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
protected stringifyFormItem(formItem: unknown) {
|
||||
if (typeof formItem === "object" && formItem !== null) {
|
||||
return JSON.stringify(formItem);
|
||||
} else {
|
||||
return `${formItem}`;
|
||||
}
|
||||
}
|
||||
|
||||
protected createFormData(input: Record<string, unknown>): FormData {
|
||||
return Object.keys(input || {}).reduce((formData, key) => {
|
||||
const property = input[key];
|
||||
const propertyContent: any[] = property instanceof Array ? property : [property];
|
||||
|
||||
for (const formItem of propertyContent) {
|
||||
const isFileType = formItem instanceof Blob || formItem instanceof File;
|
||||
formData.append(key, isFileType ? formItem : this.stringifyFormItem(formItem));
|
||||
}
|
||||
|
||||
return formData;
|
||||
}, new FormData());
|
||||
}
|
||||
|
||||
public request = async <T = any, _E = any>({
|
||||
secure,
|
||||
path,
|
||||
type,
|
||||
query,
|
||||
format,
|
||||
body,
|
||||
...params
|
||||
}: FullRequestParams): Promise<AxiosResponse<T>> => {
|
||||
const secureParams =
|
||||
((typeof secure === "boolean" ? secure : this.secure) &&
|
||||
this.securityWorker &&
|
||||
(await this.securityWorker(this.securityData))) ||
|
||||
{};
|
||||
const requestParams = this.mergeRequestParams(params, secureParams);
|
||||
const responseFormat = format || this.format || undefined;
|
||||
|
||||
if (type === ContentType.FormData && body && body !== null && typeof body === "object") {
|
||||
body = this.createFormData(body as Record<string, unknown>);
|
||||
}
|
||||
|
||||
if (type === ContentType.Text && body && body !== null && typeof body !== "string") {
|
||||
body = JSON.stringify(body);
|
||||
}
|
||||
|
||||
return this.instance.request({
|
||||
...requestParams,
|
||||
headers: {
|
||||
...(requestParams.headers || {}),
|
||||
...(type && type !== ContentType.FormData ? { "Content-Type": type } : {}),
|
||||
},
|
||||
params: query,
|
||||
responseType: responseFormat,
|
||||
data: body,
|
||||
url: path,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @title Swagger Petstore
|
||||
* @version 1.0.6
|
||||
* @license Apache 2.0 (http://www.apache.org/licenses/LICENSE-2.0.html)
|
||||
* @termsOfService http://swagger.io/terms/
|
||||
* @baseUrl https://petstore.swagger.io/v2
|
||||
* @externalDocs http://swagger.io
|
||||
* @contact <apiteam@swagger.io>
|
||||
*
|
||||
* This is a sample server Petstore server. You can find out more about Swagger at [http://swagger.io](http://swagger.io) or on [irc.freenode.net, #swagger](http://swagger.io/irc/). For this sample, you can use the api key `special-key` to test the authorization filters.
|
||||
*/
|
||||
export class Api<SecurityDataType extends unknown> extends HttpClient<SecurityDataType> {
|
||||
pet = {
|
||||
/**
|
||||
* No description
|
||||
*
|
||||
* @tags pet
|
||||
* @name UploadFile
|
||||
* @summary uploads an image
|
||||
* @request POST:/pet/{petId}/uploadImage
|
||||
* @secure
|
||||
*/
|
||||
uploadFile: (
|
||||
petId: number,
|
||||
data: {
|
||||
/** Additional data to pass to server */
|
||||
additionalMetadata?: string;
|
||||
/** file to upload */
|
||||
file?: File;
|
||||
},
|
||||
params: RequestParams = {},
|
||||
) => {
|
||||
return this.request<ApiResponse, any>({
|
||||
path: `/pet/${petId}/uploadImage`,
|
||||
method: "POST",
|
||||
body: data,
|
||||
secure: true,
|
||||
type: ContentType.FormData,
|
||||
format: "json",
|
||||
...params,
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* No description
|
||||
*
|
||||
* @tags pet
|
||||
* @name AddPet
|
||||
* @summary Add a new pet to the store
|
||||
* @request POST:/pet
|
||||
* @secure
|
||||
*/
|
||||
addPet: (body: Pet, params: RequestParams = {}) => {
|
||||
return this.request<any, void>({
|
||||
path: `/pet`,
|
||||
method: "POST",
|
||||
body: body,
|
||||
secure: true,
|
||||
type: ContentType.Json,
|
||||
...params,
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* No description
|
||||
*
|
||||
* @tags pet
|
||||
* @name UpdatePet
|
||||
* @summary Update an existing pet
|
||||
* @request PUT:/pet
|
||||
* @secure
|
||||
*/
|
||||
updatePet: (body: Pet, params: RequestParams = {}) => {
|
||||
return this.request<any, void>({
|
||||
path: `/pet`,
|
||||
method: "PUT",
|
||||
body: body,
|
||||
secure: true,
|
||||
type: ContentType.Json,
|
||||
...params,
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* @description Multiple status values can be provided with comma separated strings
|
||||
*
|
||||
* @tags pet
|
||||
* @name FindPetsByStatus
|
||||
* @summary Finds Pets by status
|
||||
* @request GET:/pet/findByStatus
|
||||
* @secure
|
||||
*/
|
||||
findPetsByStatus: (query: FindPetsByStatusParams, params: RequestParams = {}) => {
|
||||
return this.request<Pet[], void>({
|
||||
path: `/pet/findByStatus`,
|
||||
method: "GET",
|
||||
query: query,
|
||||
secure: true,
|
||||
format: "json",
|
||||
...params,
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* @description Multiple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing.
|
||||
*
|
||||
* @tags pet
|
||||
* @name FindPetsByTags
|
||||
* @summary Finds Pets by tags
|
||||
* @request GET:/pet/findByTags
|
||||
* @deprecated
|
||||
* @secure
|
||||
*/
|
||||
findPetsByTags: (query: FindPetsByTagsParams, params: RequestParams = {}) => {
|
||||
return this.request<Pet[], void>({
|
||||
path: `/pet/findByTags`,
|
||||
method: "GET",
|
||||
query: query,
|
||||
secure: true,
|
||||
format: "json",
|
||||
...params,
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* @description Returns a single pet
|
||||
*
|
||||
* @tags pet
|
||||
* @name GetPetById
|
||||
* @summary Find pet by ID
|
||||
* @request GET:/pet/{petId}
|
||||
* @secure
|
||||
*/
|
||||
getPetById: (petId: number, params: RequestParams = {}) => {
|
||||
return this.request<Pet, void>({
|
||||
path: `/pet/${petId}`,
|
||||
method: "GET",
|
||||
secure: true,
|
||||
format: "json",
|
||||
...params,
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* No description
|
||||
*
|
||||
* @tags pet
|
||||
* @name UpdatePetWithForm
|
||||
* @summary Updates a pet in the store with form data
|
||||
* @request POST:/pet/{petId}
|
||||
* @secure
|
||||
*/
|
||||
updatePetWithForm: (
|
||||
petId: number,
|
||||
data: {
|
||||
/** Updated name of the pet */
|
||||
name?: string;
|
||||
/** Updated status of the pet */
|
||||
status?: string;
|
||||
},
|
||||
params: RequestParams = {},
|
||||
) => {
|
||||
return this.request<any, void>({
|
||||
path: `/pet/${petId}`,
|
||||
method: "POST",
|
||||
body: data,
|
||||
secure: true,
|
||||
type: ContentType.FormData,
|
||||
...params,
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* No description
|
||||
*
|
||||
* @tags pet
|
||||
* @name DeletePet
|
||||
* @summary Deletes a pet
|
||||
* @request DELETE:/pet/{petId}
|
||||
* @secure
|
||||
*/
|
||||
deletePet: (petId: number, params: RequestParams = {}) => {
|
||||
return this.request<any, void>({
|
||||
path: `/pet/${petId}`,
|
||||
method: "DELETE",
|
||||
secure: true,
|
||||
...params,
|
||||
});
|
||||
},
|
||||
};
|
||||
store = {
|
||||
/**
|
||||
* No description
|
||||
*
|
||||
* @tags store
|
||||
* @name PlaceOrder
|
||||
* @summary Place an order for a pet
|
||||
* @request POST:/store/order
|
||||
*/
|
||||
placeOrder: (body: Order, params: RequestParams = {}) => {
|
||||
return this.request<Order, void>({
|
||||
path: `/store/order`,
|
||||
method: "POST",
|
||||
body: body,
|
||||
type: ContentType.Json,
|
||||
format: "json",
|
||||
...params,
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* @description For valid response try integer IDs with value >= 1 and <= 10. Other values will generated exceptions
|
||||
*
|
||||
* @tags store
|
||||
* @name GetOrderById
|
||||
* @summary Find purchase order by ID
|
||||
* @request GET:/store/order/{orderId}
|
||||
*/
|
||||
getOrderById: (orderId: number, params: RequestParams = {}) => {
|
||||
return this.request<Order, void>({
|
||||
path: `/store/order/${orderId}`,
|
||||
method: "GET",
|
||||
format: "json",
|
||||
...params,
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* @description For valid response try integer IDs with positive integer value. Negative or non-integer values will generate API errors
|
||||
*
|
||||
* @tags store
|
||||
* @name DeleteOrder
|
||||
* @summary Delete purchase order by ID
|
||||
* @request DELETE:/store/order/{orderId}
|
||||
*/
|
||||
deleteOrder: (orderId: number, params: RequestParams = {}) => {
|
||||
return this.request<any, void>({
|
||||
path: `/store/order/${orderId}`,
|
||||
method: "DELETE",
|
||||
...params,
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* @description Returns a map of status codes to quantities
|
||||
*
|
||||
* @tags store
|
||||
* @name GetInventory
|
||||
* @summary Returns pet inventories by status
|
||||
* @request GET:/store/inventory
|
||||
* @secure
|
||||
*/
|
||||
getInventory: (params: RequestParams = {}) => {
|
||||
return this.request<Record<string, number>, any>({
|
||||
path: `/store/inventory`,
|
||||
method: "GET",
|
||||
secure: true,
|
||||
format: "json",
|
||||
...params,
|
||||
});
|
||||
},
|
||||
};
|
||||
user = {
|
||||
/**
|
||||
* No description
|
||||
*
|
||||
* @tags user
|
||||
* @name CreateUsersWithArrayInput
|
||||
* @summary Creates list of users with given input array
|
||||
* @request POST:/user/createWithArray
|
||||
*/
|
||||
createUsersWithArrayInput: (body: User[], params: RequestParams = {}) => {
|
||||
return this.request<any, void>({
|
||||
path: `/user/createWithArray`,
|
||||
method: "POST",
|
||||
body: body,
|
||||
type: ContentType.Json,
|
||||
...params,
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* No description
|
||||
*
|
||||
* @tags user
|
||||
* @name CreateUsersWithListInput
|
||||
* @summary Creates list of users with given input array
|
||||
* @request POST:/user/createWithList
|
||||
*/
|
||||
createUsersWithListInput: (body: User[], params: RequestParams = {}) => {
|
||||
return this.request<any, void>({
|
||||
path: `/user/createWithList`,
|
||||
method: "POST",
|
||||
body: body,
|
||||
type: ContentType.Json,
|
||||
...params,
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* No description
|
||||
*
|
||||
* @tags user
|
||||
* @name GetUserByName
|
||||
* @summary Get user by user name
|
||||
* @request GET:/user/{username}
|
||||
*/
|
||||
getUserByName: (username: string, params: RequestParams = {}) => {
|
||||
return this.request<User, void>({
|
||||
path: `/user/${username}`,
|
||||
method: "GET",
|
||||
format: "json",
|
||||
...params,
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* @description This can only be done by the logged in user.
|
||||
*
|
||||
* @tags user
|
||||
* @name UpdateUser
|
||||
* @summary Updated user
|
||||
* @request PUT:/user/{username}
|
||||
*/
|
||||
updateUser: (username: string, body: User, params: RequestParams = {}) => {
|
||||
return this.request<any, void>({
|
||||
path: `/user/${username}`,
|
||||
method: "PUT",
|
||||
body: body,
|
||||
type: ContentType.Json,
|
||||
...params,
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* @description This can only be done by the logged in user.
|
||||
*
|
||||
* @tags user
|
||||
* @name DeleteUser
|
||||
* @summary Delete user
|
||||
* @request DELETE:/user/{username}
|
||||
*/
|
||||
deleteUser: (username: string, params: RequestParams = {}) => {
|
||||
return this.request<any, void>({
|
||||
path: `/user/${username}`,
|
||||
method: "DELETE",
|
||||
...params,
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* No description
|
||||
*
|
||||
* @tags user
|
||||
* @name LoginUser
|
||||
* @summary Logs user into the system
|
||||
* @request GET:/user/login
|
||||
*/
|
||||
loginUser: (query: LoginUserParams, params: RequestParams = {}) => {
|
||||
return this.request<string, void>({
|
||||
path: `/user/login`,
|
||||
method: "GET",
|
||||
query: query,
|
||||
format: "json",
|
||||
...params,
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* No description
|
||||
*
|
||||
* @tags user
|
||||
* @name LogoutUser
|
||||
* @summary Logs out current logged in user session
|
||||
* @request GET:/user/logout
|
||||
*/
|
||||
logoutUser: (params: RequestParams = {}) => {
|
||||
return this.request<any, void>({
|
||||
path: `/user/logout`,
|
||||
method: "GET",
|
||||
...params,
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* @description This can only be done by the logged in user.
|
||||
*
|
||||
* @tags user
|
||||
* @name CreateUser
|
||||
* @summary Create user
|
||||
* @request POST:/user
|
||||
*/
|
||||
createUser: (body: User, params: RequestParams = {}) => {
|
||||
return this.request<any, void>({
|
||||
path: `/user`,
|
||||
method: "POST",
|
||||
body: body,
|
||||
type: ContentType.Json,
|
||||
...params,
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,245 @@
|
|||
<svg viewBox="0 0 400 300" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="m70.66 196.612 3.04 13.52-13.23-4.13 10.19-9.39Z"
|
||||
fill="#5578EC"
|
||||
/>
|
||||
<path
|
||||
d="m73.7 210.132-3.04-13.52-10.19 9.39 13.23 4.13Z"
|
||||
stroke="#3351E5"
|
||||
stroke-width="2.126"
|
||||
/>
|
||||
<path
|
||||
d="M65.84 203.277c-11.39-10.1-17.84-21.64-17.84-33.89 0-39.79 68.07-72.04 152.03-72.04 83.97 0 152.03 32.25 152.03 72.04 0 12.82-7.07 24.87-19.47 35.3"
|
||||
stroke="#31333A"
|
||||
stroke-width="2.126"
|
||||
/>
|
||||
<path
|
||||
d="M125.452 252.76c-6.94-1.86-13.54-3.96-19.74-6.29-15.97-5.99-29.33-13.47-39.16-21.99-11.83-10.25-18.55-22.01-18.55-34.51 0-39.79 68.07-72.04 152.03-72.04 83.97 0 152.03 32.25 152.03 72.04 0 39.79-68.06 72.04-152.03 72.04-16.94 0-33.23-1.31-48.45-3.74"
|
||||
stroke="#31333A"
|
||||
stroke-width="2.126"
|
||||
/>
|
||||
<path
|
||||
d="M238.644 239.084c-12.33 1.53-25.27 2.34-38.61 2.34-50.25 0-94.8-11.55-122.48-29.35"
|
||||
stroke="#31333A"
|
||||
stroke-width="2.126"
|
||||
/>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M249.193 232.08c-2.76 0-5 2.24-5 5s2.24 5 5 5 5-2.24 5-5-2.24-5-5-5Z"
|
||||
fill="#31333A"
|
||||
/>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M102.154 197.846v-82.26c0-4.11 6.14-7.44 13.72-7.44h148.67c7.58 0 13.73 3.33 13.73 7.44v82.26c0 4.11-6.15 7.44-13.73 7.44h-148.67c-7.58 0-13.72-3.33-13.72-7.44Z"
|
||||
fill="#fff"
|
||||
/>
|
||||
<path
|
||||
d="M102.154 115.586v82.26c0 4.11 6.14 7.44 13.72 7.44h148.67c7.58 0 13.73-3.33 13.73-7.44v-82.26c0-4.11-6.15-7.44-13.73-7.44h-148.67c-7.58 0-13.72 3.33-13.72 7.44Z"
|
||||
stroke="#31333A"
|
||||
stroke-width="2.126"
|
||||
/>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M112.314 97.49h155.81c5.6 0 10.15 4.55 10.15 10.16v14.24h-176.12v-14.24c0-5.61 4.55-10.16 10.16-10.16Z"
|
||||
fill="#5578EC"
|
||||
/>
|
||||
<path
|
||||
d="M268.124 97.49h-155.81c-5.61 0-10.16 4.55-10.16 10.16v14.24h176.12v-14.24c0-5.61-4.55-10.16-10.15-10.16Z"
|
||||
stroke="#3351E5"
|
||||
stroke-width="2.126"
|
||||
/>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M265.712 195.453c1.24 5.51 6.72 8.98 12.23 7.75l16.36-3.68c5.51-1.23 8.98-6.72 7.74-12.22l-2.79-12.47c-2.19-9.76-11.92-15.92-21.69-13.73l-.92.21c-9.76 2.19-15.92 11.92-13.73 21.68l2.8 12.46Zm28.167-6.323-2.8-12.46c-1.18-5.25-6.42-8.57-11.68-7.39l-.92.2c-5.25 1.18-8.57 6.42-7.39 11.68l2.8 12.46c.22.98 1.23 1.63 2.22 1.41l16.36-3.68c.98-.22 1.63-1.23 1.41-2.22Z"
|
||||
fill="#686A6D"
|
||||
/>
|
||||
<path
|
||||
d="M265.712 195.453c1.24 5.51 6.72 8.98 12.23 7.75l16.36-3.68c5.51-1.23 8.98-6.72 7.74-12.22l-2.79-12.47c-2.19-9.76-11.92-15.92-21.69-13.73l-.92.21c-9.76 2.19-15.92 11.92-13.73 21.68l2.8 12.46Zm28.167-6.323-2.8-12.46c-1.18-5.25-6.42-8.57-11.68-7.39l-.92.2c-5.25 1.18-8.57 6.42-7.39 11.68l2.8 12.46c.22.98 1.23 1.63 2.22 1.41l16.36-3.68c.98-.22 1.63-1.23 1.41-2.22Z"
|
||||
stroke="#31333A"
|
||||
stroke-width="2.126"
|
||||
/>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="m285.415 228.924 13.124-2.947c10.888-2.444 17.729-13.245 15.284-24.134l-3.446-15.348a4.468 4.468 0 0 0-5.34-3.382l-43.82 9.838a4.468 4.468 0 0 0-3.382 5.34l3.446 15.348c2.445 10.889 13.245 17.729 24.134 15.285Z"
|
||||
fill="#5578EC"
|
||||
/>
|
||||
<path
|
||||
d="m310.672 182.937-52.546 11.797 8.385 37.348 52.546-11.797-8.385-37.348Z"
|
||||
stroke="#3351E5"
|
||||
stroke-width="2.126"
|
||||
/>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M286.765 194.724c-3.82 0-6.92 3.1-6.92 6.92s3.1 6.92 6.92 6.92 6.92-3.1 6.92-6.92-3.1-6.92-6.92-6.92Z"
|
||||
fill="#31333A"
|
||||
/>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="m288.891 203.001 3.21 10.45c.32 1.41-.57 2.82-1.98 3.13-1.42.32-2.82-.57-3.14-1.98l-1.56-10.82 3.47-.78Z"
|
||||
fill="#31333A"
|
||||
/>
|
||||
<path
|
||||
d="m144.481 167.653-1.15 2.01c-.34.58-.68 1.2-.64 1.87.05.67.67 1.34 1.32 1.16"
|
||||
stroke="#31333A"
|
||||
stroke-width=".425"
|
||||
/>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M186.839 142.49h67.25v-9.57h-67.25v9.57Z"
|
||||
fill="#AFAEB4"
|
||||
/>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M186.839 155.631h67.25v-6.3h-67.25v6.3ZM186.839 168.529h67.25v-6.3h-67.25v6.3ZM186.839 181.426h47.55v-6.3h-47.55v6.3Z"
|
||||
fill="#D7D9DD"
|
||||
/>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M201.174 116.591h-21.91c-2.79 0-5.05-2.26-5.05-5.05v-15.43h32v15.43c0 2.79-2.26 5.05-5.04 5.05Z"
|
||||
fill="#3351E5"
|
||||
/>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M184.268 75.95h11.75c.76 0 1.5-.3 2.04-.84s.84-1.28.84-2.04v-6.19c0-.76-.3-1.5-.84-2.04s-1.28-.84-2.04-.84h-11.75c-.76 0-1.5.3-2.04.84s-.84 1.28-.84 2.04v6.19c0 .76.3 1.5.84 2.04s1.28.84 2.04.84Z"
|
||||
fill="#686A6D"
|
||||
/>
|
||||
<path
|
||||
d="M199.958 65.064h-17.507v9.822h17.507v-9.822Z"
|
||||
stroke="#31333A"
|
||||
stroke-width="2.126"
|
||||
/>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M181.337 111.919h17.62a2.6 2.6 0 0 0 2.59-2.6v-33.31a2.6 2.6 0 0 0-2.59-2.6h-17.62a2.595 2.595 0 0 0-2.6 2.6v33.31a2.595 2.595 0 0 0 2.6 2.6Z"
|
||||
fill="#DCDDDD"
|
||||
/>
|
||||
<path
|
||||
d="M202.613 74.473H179.8v36.381h22.813V74.473Z"
|
||||
stroke="#31333A"
|
||||
stroke-width="2.126"
|
||||
/>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M143.589 126.485c-17.43 0-31.56 14.13-31.56 31.56 0 17.42 14.13 31.55 31.56 31.55 17.42 0 31.55-14.13 31.55-31.55 0-17.43-14.13-31.56-31.55-31.56Z"
|
||||
fill="#D7D9DD"
|
||||
/>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M183.536 100.461a6.61 6.61 0 0 1 6.61-6.61v13.22a6.61 6.61 0 0 1-6.61-6.61Z"
|
||||
fill="#5578EC"
|
||||
/>
|
||||
<path
|
||||
d="M190.146 93.85a6.61 6.61 0 0 0 0 13.221v-13.22Z"
|
||||
stroke="#3351E5"
|
||||
stroke-width="2.126"
|
||||
/>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M190.144 93.85a6.61 6.61 0 0 1 0 13.221v-13.22Z"
|
||||
fill="#5578EC"
|
||||
/>
|
||||
<path
|
||||
d="M196.754 100.461a6.61 6.61 0 0 0-6.61-6.61v13.22a6.61 6.61 0 0 0 6.61-6.61Z"
|
||||
stroke="#3351E5"
|
||||
stroke-width="2.126"
|
||||
/>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M160.252 171.477c-3.64 4.61-9.57 7.62-16.26 7.62-1.78 0-3.51-.22-5.16-.62-.26-.06-.5-.13-.75-.2-.02 0-.04-.01-.06-.01a21.012 21.012 0 0 1-11.09-7.89c-.04-.05-.07-.1-.11-.16-1.84-2.78-2.9-6.06-2.9-9.55 0-1.21.13-2.39.37-3.54 2.45.71 5 1.07 7.56 1.06 5.54 0 10.57-1.62 14.3-4.25 1.92-1.32 3.54-3.03 4.76-5.02.01-.01.01-.02.02-.04.21-.31.63-.38.94-.17 7.38 5.02 10.73 14.17 8.38 22.77Z"
|
||||
fill="#fff"
|
||||
/>
|
||||
<path
|
||||
d="M143.992 179.097c6.69 0 12.62-3.01 16.26-7.62 2.35-8.6-1-17.75-8.38-22.77a.674.674 0 0 0-.94.17c-.01.02-.01.03-.02.04-1.22 1.99-2.84 3.7-4.76 5.02-3.73 2.63-8.76 4.25-14.3 4.25-2.56.01-5.11-.35-7.56-1.06-.24 1.15-.37 2.33-.37 3.54 0 3.49 1.06 6.77 2.9 9.55.04.06.07.11.11.16 2.68 3.73 6.56 6.55 11.09 7.89.02 0 .04.01.06.01.25.07.49.14.75.2 1.65.4 3.38.62 5.16.62Z"
|
||||
stroke="#31333A"
|
||||
stroke-width="1.417"
|
||||
/>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M125.008 149.562c4.87-10.06 16.71-14.61 27.06-10.41 7.9 3.2 13.08 10.87 13.1 19.4 0 4.95-1.76 9.74-4.98 13.51 2.42-8.46-.72-17.51-7.85-22.66-.02-.02-.04-.03-.06-.04-.55-.36-1.28-.2-1.64.36-1.19 1.8-2.71 3.36-4.49 4.58-3.73 2.63-8.76 4.25-14.3 4.25-1.03 0-2.07-.06-3.1-.18-.95 2.63-1.44 5.4-1.43 8.2-.01 1.77.19 3.53.57 5.25a20.777 20.777 0 0 1-5.13-14.84c-.49-.17-.98-.36-1.45-.57a25.9 25.9 0 0 1-1.67-.82c-.47-.26-.84-.67-1.05-1.16-.55-1.24.02-2.69 1.26-3.23 1.66-.74 3.39-1.29 5.16-1.64Z"
|
||||
fill="#31333A"
|
||||
/>
|
||||
<path
|
||||
d="M152.068 139.152c-10.35-4.2-22.19.35-27.06 10.41-1.77.35-3.5.9-5.16 1.64a2.445 2.445 0 0 0-1.26 3.23c.21.49.58.9 1.05 1.16.54.29 1.09.56 1.67.82.47.21.96.4 1.45.57a20.777 20.777 0 0 0 5.13 14.84 23.72 23.72 0 0 1-.57-5.25c-.01-2.8.48-5.57 1.43-8.2 1.03.12 2.07.18 3.1.18 5.54 0 10.57-1.62 14.3-4.25 1.78-1.22 3.3-2.78 4.49-4.58.36-.56 1.09-.72 1.64-.36.02.01.04.02.06.04a20.861 20.861 0 0 1 7.85 22.66c3.22-3.77 4.98-8.56 4.98-13.51-.02-8.53-5.2-16.2-13.1-19.4Z"
|
||||
stroke="#31333A"
|
||||
stroke-width=".709"
|
||||
/>
|
||||
<path
|
||||
d="M138.049 166.896c0-1-.8-1.8-1.79-1.8-1 0-1.8.8-1.8 1.8M153.378 166.896c0-1-.8-1.8-1.79-1.8-1 0-1.8.8-1.8 1.8"
|
||||
stroke="#31333A"
|
||||
stroke-width="1.417"
|
||||
/>
|
||||
<path
|
||||
d="M143.992 142.231c-11.08 0-20.07 8.25-20.07 18.43s8.99 18.43 20.07 18.43c11.09 0 20.08-8.25 20.08-18.43s-8.99-18.43-20.08-18.43Z"
|
||||
stroke="#31333A"
|
||||
stroke-width="1.417"
|
||||
/>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M123.049 154.826a3.726 3.726 0 0 0 0 7.45 3.73 3.73 0 0 0 3.73-3.73c0-2.05-1.67-3.72-3.73-3.72Z"
|
||||
fill="#31333A"
|
||||
/>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M143.995 175.298c1.56 0 2.83-1.1 2.83-2.47h-5.66c0 1.37 1.27 2.47 2.83 2.47Z"
|
||||
fill="#fff"
|
||||
/>
|
||||
<path
|
||||
d="M143.995 175.298c1.56 0 2.83-1.1 2.83-2.47h-5.66c0 1.37 1.27 2.47 2.83 2.47Z"
|
||||
stroke="#31333A"
|
||||
stroke-width="1.5"
|
||||
/>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="m316.12 134.275-7.16 1.19c-1.12.19-1.97-1-1.44-2l3.42-6.41c.21-.4.21-.87 0-1.28l-3.34-6.44c-.52-1.01.34-2.18 1.46-1.99l7.15 1.27c.44.08.9-.07 1.22-.39l5.09-5.17c.8-.81 2.18-.35 2.34.78l1.01 7.18c.06.45.34.84.74 1.04l6.49 3.25c1.02.51 1.01 1.97-.01 2.47l-6.52 3.17c-.41.2-.7.59-.76 1.03l-1.09 7.18c-.17 1.13-1.55 1.57-2.35.75l-5.03-5.22c-.32-.33-.77-.48-1.22-.41Z"
|
||||
fill="#fff"
|
||||
/>
|
||||
<path
|
||||
d="m308.96 135.465 7.16-1.19c.45-.07.9.08 1.22.41l5.03 5.22c.8.82 2.18.38 2.35-.75l1.09-7.18c.06-.44.35-.83.76-1.03l6.52-3.17c1.02-.5 1.03-1.96.01-2.47l-6.49-3.25c-.4-.2-.68-.59-.74-1.04l-1.01-7.18c-.16-1.13-1.54-1.59-2.34-.78l-5.09 5.17c-.32.32-.78.47-1.22.39l-7.15-1.27c-1.12-.19-1.98.98-1.46 1.99l3.34 6.44c.21.41.21.88 0 1.28l-3.42 6.41c-.53 1 .32 2.19 1.44 2Z"
|
||||
stroke="#31333A"
|
||||
stroke-width="2.126"
|
||||
/>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M138.052 250.496c-2.76 0-5 2.24-5 5s2.24 5 5 5 5-2.24 5-5-2.24-5-5-5Z"
|
||||
fill="#fff"
|
||||
/>
|
||||
<path
|
||||
d="M138.052 250.496c-2.76 0-5 2.24-5 5s2.24 5 5 5 5-2.24 5-5-2.24-5-5-5Z"
|
||||
stroke="#31333A"
|
||||
stroke-width="2.126"
|
||||
/>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M153.384 131.631c-3.95 0-7.15 3.2-7.15 7.15 0 3.94 3.2 7.14 7.15 7.14 3.94 0 7.14-3.2 7.14-7.14 0-3.95-3.2-7.15-7.14-7.15Z"
|
||||
fill="#31333A"
|
||||
/>
|
||||
<path
|
||||
d="m81.614 106.969 8.38 5.73M92.944 89.777l6.25 8M112.25 79.528l.51 10.14"
|
||||
stroke="#31333A"
|
||||
stroke-width="2"
|
||||
/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 10 KiB |
|
|
@ -0,0 +1,239 @@
|
|||
<svg
|
||||
viewBox="0 0 400 300"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="w-[280px]"
|
||||
>
|
||||
<path
|
||||
d="M55.92 169.384c-1.7-2.38-3.14-4.82-4.3-7.31-2.37-5.06-3.62-10.31-3.62-15.7 0-36.68 57.86-66.96 132.69-71.46M297.606 91.128c33.29 13.22 54.46 33.06 54.46 55.25 0 10.3-4.57 20.1-12.79 28.97M196.192 74.356c1.28-.01 2.56-.02 3.84-.02 24.71 0 48.05 2.79 68.67 7.75"
|
||||
stroke="#31333A"
|
||||
stroke-width="2.126"
|
||||
/>
|
||||
<path
|
||||
d="M315.532 118.444c12.36 7.6 21.59 16.4 26.77 25.97M201.24 110.279c83.97 0 152.03 32.25 152.03 72.04 0 39.79-68.06 72.04-152.03 72.04-35.63 0-68.41-5.81-94.32-15.54-15.97-5.99-29.33-13.47-39.16-21.99-11.83-10.25-18.55-22.01-18.55-34.51 0-22.37 21.52-42.36 55.28-55.57 2.1-.83 4.25-1.62 6.44-2.39"
|
||||
stroke="#31333A"
|
||||
stroke-width="2.126"
|
||||
/>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M126.986 201.18h128.76c6.56 0 11.88-5.33 11.88-11.89V57.88c0-6.56-5.32-11.88-11.88-11.88h-128.76c-3.15 0-6.18 1.25-8.41 3.48a11.899 11.899 0 0 0-3.48 8.41v131.4c0 3.15 1.25 6.18 3.48 8.41 2.23 2.23 5.26 3.48 8.41 3.48Z"
|
||||
fill="#DCDDDD"
|
||||
/>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M126.975 46h128.77c6.56 0 11.89 5.32 11.89 11.89v9.24h-152.54v-9.24c0-6.57 5.32-11.89 11.88-11.89Z"
|
||||
fill="#999"
|
||||
/>
|
||||
<path
|
||||
d="M136.064 99.007c12.54-2.76 26.01-4.74 40.13-5.79M64.414 131.78c9.47-8.83 22.67-16.61 38.62-22.88"
|
||||
stroke="#31333A"
|
||||
stroke-width="2.126"
|
||||
/>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M62.193 127.069c-2.76 0-5 2.24-5 5s2.24 5 5 5 5-2.24 5-5-2.24-5-5-5Z"
|
||||
fill="#31333A"
|
||||
/>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M147.693 73.57c-2.76 0-5 2.24-5 5s2.24 5 5 5 5-2.24 5-5-2.24-5-5-5Z"
|
||||
fill="#fff"
|
||||
/>
|
||||
<path
|
||||
d="M147.693 73.57c-2.76 0-5 2.24-5 5s2.24 5 5 5 5-2.24 5-5-2.24-5-5-5Z"
|
||||
stroke="#31333A"
|
||||
stroke-width="2.126"
|
||||
/>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="m89.926 158.793 6.93 12h-13.86l6.93-12Z"
|
||||
fill="#5578EC"
|
||||
/>
|
||||
<path
|
||||
d="m96.856 170.793-6.93-12-6.93 12h13.86Z"
|
||||
stroke="#3351E5"
|
||||
stroke-width="2.126"
|
||||
/>
|
||||
<path
|
||||
d="M259.143 237.095c-15.48 2.52-41.5 4.94-58.78 4.94-18.54 0-39.27-2.06-55.7-4.94M112.663 228.745c-2.15-.73-4.25-1.48-6.32-2.25-.42-.16-.84-.32-1.25-.48-15.42-5.9-28.34-13.21-37.91-21.51M332.263 202.087c-8.42 7.24-19.07 13.93-31.48 19.79-1.75.83-3.54 1.64-5.36 2.43"
|
||||
stroke="#31333A"
|
||||
stroke-width="2.126"
|
||||
/>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="m137.509 224.27-2.07 4.53c-.13.28-.11.61.05.88l2.56 4.28c.4.67-.14 1.51-.92 1.42l-4.95-.57a.94.94 0 0 0-.82.32l-3.28 3.76c-.51.59-1.48.33-1.63-.44l-.99-4.88a.945.945 0 0 0-.56-.68l-4.59-1.96a.944.944 0 0 1-.09-1.69l4.34-2.45a.97.97 0 0 0 .48-.74l.44-4.97c.07-.78 1-1.14 1.58-.61l3.67 3.37c.23.21.55.3.85.23l4.86-1.12c.77-.17 1.4.6 1.07 1.32Z"
|
||||
fill="#fff"
|
||||
/>
|
||||
<path
|
||||
d="m135.439 228.8 2.07-4.53a.945.945 0 0 0-1.07-1.32l-4.86 1.12a.95.95 0 0 1-.85-.23l-3.67-3.37a.944.944 0 0 0-1.58.61l-.44 4.97a.97.97 0 0 1-.48.74l-4.34 2.45a.944.944 0 0 0 .09 1.69l4.59 1.96c.29.12.49.37.56.68l.99 4.88c.15.77 1.12 1.03 1.63.44l3.28-3.76c.2-.24.51-.35.82-.32l4.95.57c.78.09 1.32-.75.92-1.42l-2.56-4.28a.947.947 0 0 1-.05-.88Z"
|
||||
stroke="#31333A"
|
||||
stroke-width="2.126"
|
||||
/>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="m335.832 196.284 8.478-1.554-1.554-8.479-8.479 1.555 1.555 8.478Z"
|
||||
fill="#5578EC"
|
||||
/>
|
||||
<path
|
||||
d="m343.99 187.106-8.475 1.553 1.17 6.384 8.475-1.554-1.17-6.383Z"
|
||||
stroke="#3351E5"
|
||||
stroke-width="2.126"
|
||||
/>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M136.863 209.542h128.76c6.56 0 11.88-5.33 11.88-11.89V66.242c0-6.56-5.32-11.88-11.88-11.88h-128.76c-3.15 0-6.18 1.25-8.41 3.48a11.899 11.899 0 0 0-3.48 8.41v131.4c0 3.15 1.25 6.18 3.48 8.41 2.23 2.23 5.26 3.48 8.41 3.48Z"
|
||||
fill="#fff"
|
||||
/>
|
||||
<path
|
||||
d="M278.57 55.425H126.036v153.05H278.57V55.425Z"
|
||||
stroke="#31333A"
|
||||
stroke-width="2.126"
|
||||
/>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M136.853 54.362h128.77c6.56 0 11.88 5.32 11.88 11.89v9.24h-152.53v-9.24c0-6.57 5.32-11.89 11.88-11.89Z"
|
||||
fill="#5578EC"
|
||||
/>
|
||||
<path
|
||||
d="M265.623 54.362h-128.77c-6.56 0-11.88 5.32-11.88 11.89v9.24h152.53v-9.24c0-6.57-5.32-11.89-11.88-11.89Z"
|
||||
stroke="#3351E5"
|
||||
stroke-width="2.126"
|
||||
/>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M132.492 64.922a5.695 5.695 0 1 1 11.39.01 5.695 5.695 0 0 1-11.39-.01ZM148.833 64.922a5.695 5.695 0 1 1 11.39.01 5.695 5.695 0 0 1-11.39-.01ZM165.176 64.922a5.695 5.695 0 1 1 11.39.011 5.695 5.695 0 0 1-11.39-.01Z"
|
||||
fill="#fff"
|
||||
/>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M145.772 162.308h110.93v-6.85h-110.93v6.85ZM145.772 173.204h110.93v-6.85h-110.93v6.85ZM145.772 184.1h62.4v-6.85h-62.4v6.85Z"
|
||||
fill="#C6C6C5"
|
||||
/>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M212.883 107.575c.27-.3.74-.32 1.05-.05a23.046 23.046 0 0 1 5.85 26.13c-4.66 4.52-11.57 6.95-18.89 5.98-1.96-.26-3.82-.75-5.57-1.43-.26-.11-.53-.21-.79-.33-.02 0-.04-.01-.06-.02a23.175 23.175 0 0 1-10.98-10.25c-.04-.06-.06-.12-.1-.18-1.61-3.32-2.29-7.05-1.79-10.88.18-1.32.49-2.6.93-3.81 2.58 1.13 5.31 1.89 8.11 2.25 6.06.81 11.8-.23 16.26-2.56a18.35 18.35 0 0 0 5.94-4.81l.04-.04Z"
|
||||
fill="#fff"
|
||||
/>
|
||||
<path
|
||||
d="M213.933 107.525a.752.752 0 0 0-1.05.05l-.04.04c-1.62 2-3.64 3.64-5.94 4.81-4.46 2.33-10.2 3.37-16.26 2.56-2.8-.36-5.53-1.12-8.11-2.25a18.45 18.45 0 0 0-.93 3.81c-.5 3.83.18 7.56 1.79 10.88.04.06.06.12.1.18 2.39 4.48 6.22 8.12 10.98 10.25.02.01.04.02.06.02.26.12.53.22.79.33 1.75.68 3.61 1.17 5.57 1.43 7.32.97 14.23-1.46 18.89-5.98a23.046 23.046 0 0 0-5.85-26.13Z"
|
||||
stroke="#31333A"
|
||||
stroke-width=".992"
|
||||
/>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M214.277 108.303c-.55-.47-1.38-.4-1.84.15-1.57 1.8-3.46 3.28-5.58 4.37-4.47 2.34-10.21 3.37-16.27 2.56-1.13-.15-2.25-.36-3.36-.64a26.29 26.29 0 0 0-2.77 8.76c-.26 1.93-.3 3.89-.13 5.83a22.908 22.908 0 0 1-3.65-15.77c.05-.41.12-.81.19-1.21-.51-.27-1.02-.55-1.5-.84-.59-.36-1.16-.74-1.71-1.14-.48-.35-.82-.85-.98-1.42a2.71 2.71 0 0 1 1.85-3.36c1.92-.56 3.89-.91 5.89-1.04 6.78-10.3 20.4-13.55 31.11-7.45a23.125 23.125 0 0 1 11.51 23.13c-.71 5.42-3.34 10.4-7.4 14.06 3.86-8.9 1.75-19.27-5.3-25.94a.265.265 0 0 0-.06-.05Z"
|
||||
fill="#31333A"
|
||||
/>
|
||||
<path
|
||||
d="M212.437 108.453c.46-.55 1.29-.62 1.84-.15.02.01.04.03.06.05 7.05 6.67 9.16 17.04 5.3 25.94 4.06-3.66 6.69-8.64 7.4-14.06a23.125 23.125 0 0 0-11.51-23.13c-10.71-6.1-24.33-2.85-31.11 7.45-2 .13-3.97.48-5.89 1.04a2.71 2.71 0 0 0-1.85 3.36c.16.57.5 1.07.98 1.42.55.4 1.12.78 1.71 1.14.48.29.99.57 1.5.84-.07.4-.14.8-.19 1.21-.74 5.52.55 11.13 3.65 15.77-.17-1.94-.13-3.9.13-5.83.4-3.06 1.34-6.02 2.77-8.76 1.11.28 2.23.49 3.36.64 6.06.81 11.8-.22 16.27-2.56 2.12-1.09 4.01-2.57 5.58-4.37Z"
|
||||
stroke="#31333A"
|
||||
/>
|
||||
<path
|
||||
d="m194.061 126.161 3.34-3.6M193.931 122.69l3.6 3.34M208.133 126.305l3.34-3.6M208.002 122.835l3.6 3.34M181.607 116.548c-1.479 11.132 7.154 21.47 19.288 23.082 12.123 1.611 23.157-6.112 24.636-17.245 1.481-11.142-7.153-21.479-19.276-23.09-12.133-1.613-23.167 6.111-24.648 17.253Z"
|
||||
stroke="#31333A"
|
||||
stroke-width="2.126"
|
||||
/>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="m169.931 103.186-9.09-.09v.07l-16.32 21.59.49 4.83 15.56.16-.07 7.12 9.08.09.07-7.12 3.96.04.06-6.07-3.95-.04.21-20.58Zm-9.192 9.971-.16-.02-1.01 1.73-7.24 8.72 8.31.08.1-10.51ZM247.096 136.867l9.09.09.07-7.12 3.95.04.07-6.07-3.95-.04.21-20.58-9.09-.09v.07l-16.32 21.59.49 4.83 15.56.16-.08 7.12Zm.248-23.7-.16-.03-1.01 1.73-7.24 8.72 8.3.08.11-10.5Z"
|
||||
fill="#31333A"
|
||||
/>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M264.771 220.051c-5.87 3.29-13 4.45-20.09 2.69-14.71-3.64-23.72-18.37-20.12-32.9 3.6-14.54 18.43-23.37 33.14-19.73 14.71 3.64 23.71 18.37 20.12 32.9-1.03 4.18-3 7.88-5.61 10.95l16.29 18.45.81-3.28 2.35 1.24-1.08 4.39-7.35 5.5-18.46-20.21Z"
|
||||
fill="#3351E5"
|
||||
/>
|
||||
<path
|
||||
d="M244.681 222.741c7.09 1.76 14.22.6 20.09-2.69l18.46 20.21 7.35-5.5 1.08-4.39-2.35-1.24-.81 3.28-16.29-18.45c2.61-3.07 4.58-6.77 5.61-10.95 3.59-14.53-5.41-29.26-20.12-32.9-14.71-3.64-29.54 5.19-33.14 19.73-3.6 14.53 5.41 29.26 20.12 32.9Z"
|
||||
stroke="#3351E5"
|
||||
stroke-width=".992"
|
||||
/>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M277.111 200.513c-.8 3.23-2.25 6.15-4.17 8.66l18.72 21.2-7.34 5.5-18.61-20.37c-5.79 3.37-13.02 4.57-20.26 2.78-14.19-3.51-23.02-17.16-19.72-30.48 3.29-13.33 17.47-21.28 31.66-17.77 14.18 3.51 23.01 17.16 19.72 30.48Z"
|
||||
fill="#5578EC"
|
||||
/>
|
||||
<path
|
||||
d="M272.941 209.173c1.92-2.51 3.37-5.43 4.17-8.66 3.29-13.32-5.54-26.97-19.72-30.48-14.19-3.51-28.37 4.44-31.66 17.77-3.3 13.32 5.53 26.97 19.72 30.48 7.24 1.79 14.47.59 20.26-2.78l18.61 20.37 7.34-5.5-18.72-21.2Z"
|
||||
stroke="#3351E5"
|
||||
stroke-width=".992"
|
||||
/>
|
||||
<path
|
||||
d="M258.139 169.99c-.06-.01-.11-.03-.16-.04-11.16-2.76-22.31 1.57-28.17 10.03a26.8 26.8 0 0 0-4.66 9.77c-3.6 14.54 5.41 29.27 20.12 32.91 7.09 1.75 14.22.6 20.09-2.69l18.46 20.21 7.35-5.5 1.08-4.39-17.21-19.48c1.5-2.37 2.66-5.01 3.37-7.88 3.6-14.53-5.41-29.27-20.12-32.9a.833.833 0 0 0-.15-.04Z"
|
||||
stroke="#31333A"
|
||||
stroke-width="2"
|
||||
/>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M232.624 189.502c-2.44 9.862 3.998 19.943 14.375 22.51 10.387 2.57 20.782-3.347 23.222-13.21 2.44-9.862-4-19.934-14.387-22.503-10.377-2.567-20.77 3.34-23.21 13.203Z"
|
||||
fill="#fff"
|
||||
/>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M235.672 205.485c-3.227-4.276-4.582-9.782-3.237-15.222 2.44-9.863 12.833-15.77 23.21-13.203 6.397 1.582 11.297 6.01 13.547 11.482-2.625-3.478-6.491-6.141-11.152-7.294-10.378-2.567-20.77 3.34-23.21 13.203-.938 3.792-.564 7.617.842 11.034Z"
|
||||
fill="#E7E7E7"
|
||||
/>
|
||||
<path
|
||||
d="M232.624 189.502c-2.44 9.862 3.998 19.943 14.375 22.51 10.387 2.57 20.782-3.347 23.222-13.21 2.44-9.862-4-19.934-14.387-22.503-10.377-2.567-20.77 3.34-23.21 13.203Z"
|
||||
stroke="#31333A"
|
||||
stroke-width="2"
|
||||
/>
|
||||
<path
|
||||
d="M250.303 204.983s-4.05.08-6.64-1.6c-2.59-1.67-2.95-3.98-2.95-3.98"
|
||||
stroke="#31333A"
|
||||
stroke-width="2.126"
|
||||
/>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="m287.587 249.187-16.93-18.84c-1.36-1.52-.21-4.8 2.59-7.31l3.38-3.03c2.79-2.52 6.17-3.32 7.54-1.8l16.93 18.84c1.37 1.53.21 4.8-2.59 7.31l-3.38 3.04c-2.79 2.51-6.17 3.31-7.54 1.79Z"
|
||||
fill="#DCDDDD"
|
||||
/>
|
||||
<path
|
||||
d="m270.657 230.347 16.93 18.84c1.37 1.52 4.75.72 7.54-1.79l3.38-3.04c2.8-2.51 3.96-5.78 2.59-7.31l-16.93-18.84c-1.37-1.52-4.75-.72-7.54 1.8l-3.38 3.03c-2.8 2.51-3.95 5.79-2.59 7.31Z"
|
||||
stroke="#31333A"
|
||||
stroke-width="2.126"
|
||||
/>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="m298.505 244.358-3.37 3.03c-2.8 2.52-6.18 3.32-7.54 1.8l-1.38-1.54 13.51-12.13 1.37 1.53c1.37 1.52.21 4.8-2.59 7.31Z"
|
||||
fill="#0D1D38"
|
||||
/>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="m287.587 249.186-16.93-18.84c-1.36-1.52-.21-4.8 2.59-7.31l.85-.76 21.88 24.36-.85.76c-2.79 2.51-6.17 3.31-7.54 1.79Z"
|
||||
fill="#DCDDDD"
|
||||
/>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M281.843 220.296c.82-.74 1.81-.97 2.21-.53l15.48 17.24c.4.44.06 1.4-.76 2.13-.81.73-1.8.97-2.2.52l-15.48-17.23c-.4-.44-.06-1.4.75-2.13Z"
|
||||
fill="#fff"
|
||||
/>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M180.431 108.9a3.55 3.55 0 1 0 .002 7.102 3.55 3.55 0 0 0-.002-7.102Z"
|
||||
fill="#31333A"
|
||||
/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 79 KiB |
|
|
@ -0,0 +1,167 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:svgjs="http://svgjs.com/svgjs" width="1920" height="960" preserveAspectRatio="none" viewBox="0 0 1920 960">
|
||||
<g mask="url("#SvgjsMask1032")" fill="none">
|
||||
<use xlink:href="#SvgjsSymbol1039" x="0" y="0"></use>
|
||||
<use xlink:href="#SvgjsSymbol1039" x="0" y="720"></use>
|
||||
<use xlink:href="#SvgjsSymbol1039" x="720" y="0"></use>
|
||||
<use xlink:href="#SvgjsSymbol1039" x="720" y="720"></use>
|
||||
<use xlink:href="#SvgjsSymbol1039" x="1440" y="0"></use>
|
||||
<use xlink:href="#SvgjsSymbol1039" x="1440" y="720"></use>
|
||||
</g>
|
||||
<defs>
|
||||
<mask id="SvgjsMask1032">
|
||||
<rect width="1920" height="960" fill="#ffffff"></rect>
|
||||
</mask>
|
||||
<path d="M-1 0 a1 1 0 1 0 2 0 a1 1 0 1 0 -2 0z" id="SvgjsPath1037"></path>
|
||||
<path d="M-3 0 a3 3 0 1 0 6 0 a3 3 0 1 0 -6 0z" id="SvgjsPath1036"></path>
|
||||
<path d="M-5 0 a5 5 0 1 0 10 0 a5 5 0 1 0 -10 0z" id="SvgjsPath1033"></path>
|
||||
<path d="M2 -2 L-2 2z" id="SvgjsPath1034"></path>
|
||||
<path d="M6 -6 L-6 6z" id="SvgjsPath1035"></path>
|
||||
<path d="M30 -30 L-30 30z" id="SvgjsPath1038"></path>
|
||||
</defs>
|
||||
<symbol id="SvgjsSymbol1039">
|
||||
<use xlink:href="#SvgjsPath1033" x="30" y="30" stroke="#1c538e"></use>
|
||||
<use xlink:href="#SvgjsPath1033" x="30" y="90" stroke="#1c538e"></use>
|
||||
<use xlink:href="#SvgjsPath1034" x="30" y="150" stroke="#1c538e"></use>
|
||||
<use xlink:href="#SvgjsPath1035" x="30" y="210" stroke="#1c538e"></use>
|
||||
<use xlink:href="#SvgjsPath1036" x="30" y="270" stroke="#1c538e"></use>
|
||||
<use xlink:href="#SvgjsPath1033" x="30" y="330" stroke="#1c538e"></use>
|
||||
<use xlink:href="#SvgjsPath1035" x="30" y="390" stroke="#1c538e"></use>
|
||||
<use xlink:href="#SvgjsPath1037" x="30" y="450" stroke="#1c538e"></use>
|
||||
<use xlink:href="#SvgjsPath1036" x="30" y="510" stroke="#1c538e"></use>
|
||||
<use xlink:href="#SvgjsPath1033" x="30" y="570" stroke="#1c538e"></use>
|
||||
<use xlink:href="#SvgjsPath1035" x="30" y="630" stroke="#1c538e"></use>
|
||||
<use xlink:href="#SvgjsPath1036" x="30" y="690" stroke="#1c538e"></use>
|
||||
<use xlink:href="#SvgjsPath1036" x="90" y="30" stroke="#1c538e"></use>
|
||||
<use xlink:href="#SvgjsPath1034" x="90" y="90" stroke="#1c538e"></use>
|
||||
<use xlink:href="#SvgjsPath1034" x="90" y="150" stroke="#1c538e"></use>
|
||||
<use xlink:href="#SvgjsPath1035" x="90" y="210" stroke="#1c538e"></use>
|
||||
<use xlink:href="#SvgjsPath1033" x="90" y="270" stroke="#1c538e"></use>
|
||||
<use xlink:href="#SvgjsPath1033" x="90" y="330" stroke="#1c538e"></use>
|
||||
<use xlink:href="#SvgjsPath1034" x="90" y="390" stroke="#1c538e"></use>
|
||||
<use xlink:href="#SvgjsPath1033" x="90" y="450" stroke="#1c538e"></use>
|
||||
<use xlink:href="#SvgjsPath1035" x="90" y="510" stroke="#1c538e"></use>
|
||||
<use xlink:href="#SvgjsPath1033" x="90" y="570" stroke="#1c538e"></use>
|
||||
<use xlink:href="#SvgjsPath1035" x="90" y="630" stroke="#1c538e"></use>
|
||||
<use xlink:href="#SvgjsPath1036" x="90" y="690" stroke="#1c538e"></use>
|
||||
<use xlink:href="#SvgjsPath1035" x="150" y="30" stroke="#1c538e"></use>
|
||||
<use xlink:href="#SvgjsPath1033" x="150" y="90" stroke="#1c538e"></use>
|
||||
<use xlink:href="#SvgjsPath1034" x="150" y="150" stroke="#1c538e"></use>
|
||||
<use xlink:href="#SvgjsPath1035" x="150" y="210" stroke="#1c538e"></use>
|
||||
<use xlink:href="#SvgjsPath1034" x="150" y="270" stroke="#1c538e"></use>
|
||||
<use xlink:href="#SvgjsPath1035" x="150" y="330" stroke="#1c538e"></use>
|
||||
<use xlink:href="#SvgjsPath1033" x="150" y="390" stroke="#1c538e"></use>
|
||||
<use xlink:href="#SvgjsPath1035" x="150" y="450" stroke="#1c538e"></use>
|
||||
<use xlink:href="#SvgjsPath1033" x="150" y="510" stroke="#1c538e"></use>
|
||||
<use xlink:href="#SvgjsPath1037" x="150" y="570" stroke="#1c538e"></use>
|
||||
<use xlink:href="#SvgjsPath1035" x="150" y="630" stroke="#1c538e"></use>
|
||||
<use xlink:href="#SvgjsPath1037" x="150" y="690" stroke="#1c538e"></use>
|
||||
<use xlink:href="#SvgjsPath1034" x="210" y="30" stroke="#1c538e"></use>
|
||||
<use xlink:href="#SvgjsPath1033" x="210" y="90" stroke="#1c538e"></use>
|
||||
<use xlink:href="#SvgjsPath1036" x="210" y="150" stroke="#1c538e"></use>
|
||||
<use xlink:href="#SvgjsPath1037" x="210" y="210" stroke="#1c538e"></use>
|
||||
<use xlink:href="#SvgjsPath1035" x="210" y="270" stroke="#1c538e"></use>
|
||||
<use xlink:href="#SvgjsPath1033" x="210" y="330" stroke="#1c538e"></use>
|
||||
<use xlink:href="#SvgjsPath1033" x="210" y="390" stroke="#1c538e"></use>
|
||||
<use xlink:href="#SvgjsPath1035" x="210" y="450" stroke="#1c538e"></use>
|
||||
<use xlink:href="#SvgjsPath1037" x="210" y="510" stroke="#1c538e"></use>
|
||||
<use xlink:href="#SvgjsPath1035" x="210" y="570" stroke="#1c538e"></use>
|
||||
<use xlink:href="#SvgjsPath1033" x="210" y="630" stroke="#1c538e"></use>
|
||||
<use xlink:href="#SvgjsPath1037" x="210" y="690" stroke="#1c538e"></use>
|
||||
<!-- <use xlink:href="#SvgjsPath1038" x="270" y="30" stroke="#1c538e" ></use> -->
|
||||
<use xlink:href="#SvgjsPath1035" x="270" y="90" stroke="#1c538e"></use>
|
||||
<use xlink:href="#SvgjsPath1034" x="270" y="150" stroke="#1c538e"></use>
|
||||
<use xlink:href="#SvgjsPath1037" x="270" y="210" stroke="#1c538e"></use>
|
||||
<use xlink:href="#SvgjsPath1037" x="270" y="270" stroke="#1c538e"></use>
|
||||
<use xlink:href="#SvgjsPath1035" x="270" y="330" stroke="#1c538e"></use>
|
||||
<use xlink:href="#SvgjsPath1035" x="270" y="390" stroke="#1c538e"></use>
|
||||
<use xlink:href="#SvgjsPath1033" x="270" y="450" stroke="#1c538e"></use>
|
||||
<use xlink:href="#SvgjsPath1035" x="270" y="510" stroke="#1c538e"></use>
|
||||
<use xlink:href="#SvgjsPath1035" x="270" y="570" stroke="#1c538e"></use>
|
||||
<use xlink:href="#SvgjsPath1033" x="270" y="630" stroke="#1c538e"></use>
|
||||
<use xlink:href="#SvgjsPath1035" x="270" y="690" stroke="#1c538e"></use>
|
||||
<use xlink:href="#SvgjsPath1037" x="330" y="30" stroke="#1c538e"></use>
|
||||
<use xlink:href="#SvgjsPath1035" x="330" y="90" stroke="#1c538e"></use>
|
||||
<use xlink:href="#SvgjsPath1033" x="330" y="150" stroke="#1c538e"></use>
|
||||
<use xlink:href="#SvgjsPath1037" x="330" y="210" stroke="#1c538e"></use>
|
||||
<use xlink:href="#SvgjsPath1037" x="330" y="270" stroke="#1c538e"></use>
|
||||
<use xlink:href="#SvgjsPath1036" x="330" y="330" stroke="#1c538e"></use>
|
||||
<use xlink:href="#SvgjsPath1035" x="330" y="390" stroke="#1c538e"></use>
|
||||
<use xlink:href="#SvgjsPath1036" x="330" y="450" stroke="#1c538e"></use>
|
||||
<use xlink:href="#SvgjsPath1035" x="330" y="510" stroke="#1c538e"></use>
|
||||
<use xlink:href="#SvgjsPath1034" x="330" y="570" stroke="#1c538e"></use>
|
||||
<use xlink:href="#SvgjsPath1034" x="330" y="630" stroke="#1c538e"></use>
|
||||
<use xlink:href="#SvgjsPath1033" x="330" y="690" stroke="#1c538e"></use>
|
||||
<use xlink:href="#SvgjsPath1035" x="390" y="30" stroke="#1c538e"></use>
|
||||
<use xlink:href="#SvgjsPath1035" x="390" y="90" stroke="#1c538e"></use>
|
||||
<use xlink:href="#SvgjsPath1033" x="390" y="150" stroke="#1c538e"></use>
|
||||
<use xlink:href="#SvgjsPath1035" x="390" y="210" stroke="#1c538e"></use>
|
||||
<use xlink:href="#SvgjsPath1033" x="390" y="270" stroke="#1c538e"></use>
|
||||
<use xlink:href="#SvgjsPath1033" x="390" y="330" stroke="#1c538e"></use>
|
||||
<use xlink:href="#SvgjsPath1035" x="390" y="390" stroke="#1c538e"></use>
|
||||
<use xlink:href="#SvgjsPath1034" x="390" y="450" stroke="#1c538e"></use>
|
||||
<use xlink:href="#SvgjsPath1033" x="390" y="510" stroke="#1c538e"></use>
|
||||
<use xlink:href="#SvgjsPath1034" x="390" y="570" stroke="#1c538e"></use>
|
||||
<use xlink:href="#SvgjsPath1035" x="390" y="630" stroke="#1c538e"></use>
|
||||
<use xlink:href="#SvgjsPath1035" x="390" y="690" stroke="#1c538e"></use>
|
||||
<use xlink:href="#SvgjsPath1033" x="450" y="30" stroke="#1c538e"></use>
|
||||
<use xlink:href="#SvgjsPath1033" x="450" y="90" stroke="#1c538e"></use>
|
||||
<use xlink:href="#SvgjsPath1033" x="450" y="150" stroke="#1c538e"></use>
|
||||
<use xlink:href="#SvgjsPath1033" x="450" y="210" stroke="#1c538e"></use>
|
||||
<use xlink:href="#SvgjsPath1037" x="450" y="270" stroke="#1c538e"></use>
|
||||
<use xlink:href="#SvgjsPath1034" x="450" y="330" stroke="#1c538e"></use>
|
||||
<use xlink:href="#SvgjsPath1035" x="450" y="390" stroke="#1c538e"></use>
|
||||
<use xlink:href="#SvgjsPath1033" x="450" y="450" stroke="#1c538e"></use>
|
||||
<use xlink:href="#SvgjsPath1033" x="450" y="510" stroke="#1c538e"></use>
|
||||
<use xlink:href="#SvgjsPath1033" x="450" y="570" stroke="#1c538e"></use>
|
||||
<use xlink:href="#SvgjsPath1033" x="450" y="630" stroke="#1c538e"></use>
|
||||
<use xlink:href="#SvgjsPath1033" x="450" y="690" stroke="#1c538e"></use>
|
||||
<use xlink:href="#SvgjsPath1033" x="510" y="30" stroke="#1c538e"></use>
|
||||
<use xlink:href="#SvgjsPath1033" x="510" y="90" stroke="#1c538e"></use>
|
||||
<use xlink:href="#SvgjsPath1034" x="510" y="150" stroke="#1c538e"></use>
|
||||
<use xlink:href="#SvgjsPath1033" x="510" y="210" stroke="#1c538e"></use>
|
||||
<use xlink:href="#SvgjsPath1034" x="510" y="270" stroke="#1c538e"></use>
|
||||
<use xlink:href="#SvgjsPath1036" x="510" y="330" stroke="#1c538e"></use>
|
||||
<use xlink:href="#SvgjsPath1035" x="510" y="390" stroke="#1c538e"></use>
|
||||
<use xlink:href="#SvgjsPath1035" x="510" y="450" stroke="#1c538e"></use>
|
||||
<use xlink:href="#SvgjsPath1034" x="510" y="510" stroke="#1c538e"></use>
|
||||
<use xlink:href="#SvgjsPath1034" x="510" y="570" stroke="#1c538e"></use>
|
||||
<use xlink:href="#SvgjsPath1035" x="510" y="630" stroke="#1c538e"></use>
|
||||
<use xlink:href="#SvgjsPath1035" x="510" y="690" stroke="#1c538e"></use>
|
||||
<use xlink:href="#SvgjsPath1033" x="570" y="30" stroke="#1c538e"></use>
|
||||
<use xlink:href="#SvgjsPath1035" x="570" y="90" stroke="#1c538e"></use>
|
||||
<use xlink:href="#SvgjsPath1033" x="570" y="150" stroke="#1c538e"></use>
|
||||
<use xlink:href="#SvgjsPath1035" x="570" y="210" stroke="#1c538e"></use>
|
||||
<use xlink:href="#SvgjsPath1035" x="570" y="270" stroke="#1c538e"></use>
|
||||
<use xlink:href="#SvgjsPath1036" x="570" y="330" stroke="#1c538e"></use>
|
||||
<use xlink:href="#SvgjsPath1035" x="570" y="390" stroke="#1c538e"></use>
|
||||
<use xlink:href="#SvgjsPath1033" x="570" y="450" stroke="#1c538e"></use>
|
||||
<use xlink:href="#SvgjsPath1033" x="570" y="510" stroke="#1c538e"></use>
|
||||
<use xlink:href="#SvgjsPath1038" x="570" y="570" stroke="#1c538e" stroke-width="3"></use>
|
||||
<use xlink:href="#SvgjsPath1037" x="570" y="630" stroke="#1c538e"></use>
|
||||
<use xlink:href="#SvgjsPath1033" x="570" y="690" stroke="#1c538e"></use>
|
||||
<use xlink:href="#SvgjsPath1033" x="630" y="30" stroke="#1c538e"></use>
|
||||
<use xlink:href="#SvgjsPath1035" x="630" y="90" stroke="#1c538e"></use>
|
||||
<use xlink:href="#SvgjsPath1033" x="630" y="150" stroke="#1c538e"></use>
|
||||
<use xlink:href="#SvgjsPath1035" x="630" y="210" stroke="#1c538e"></use>
|
||||
<use xlink:href="#SvgjsPath1033" x="630" y="270" stroke="#1c538e"></use>
|
||||
<use xlink:href="#SvgjsPath1037" x="630" y="330" stroke="#1c538e"></use>
|
||||
<use xlink:href="#SvgjsPath1037" x="630" y="390" stroke="#1c538e"></use>
|
||||
<use xlink:href="#SvgjsPath1036" x="630" y="450" stroke="#1c538e"></use>
|
||||
<use xlink:href="#SvgjsPath1033" x="630" y="510" stroke="#1c538e"></use>
|
||||
<use xlink:href="#SvgjsPath1035" x="630" y="570" stroke="#1c538e"></use>
|
||||
<use xlink:href="#SvgjsPath1038" x="630" y="630" stroke="#1c538e" stroke-width="3"></use>
|
||||
<use xlink:href="#SvgjsPath1035" x="630" y="690" stroke="#1c538e"></use>
|
||||
<use xlink:href="#SvgjsPath1033" x="690" y="30" stroke="#1c538e"></use>
|
||||
<use xlink:href="#SvgjsPath1033" x="690" y="90" stroke="#1c538e"></use>
|
||||
<use xlink:href="#SvgjsPath1035" x="690" y="150" stroke="#1c538e"></use>
|
||||
<use xlink:href="#SvgjsPath1034" x="690" y="210" stroke="#1c538e"></use>
|
||||
<use xlink:href="#SvgjsPath1038" x="690" y="270" stroke="#1c538e" stroke-width="3"></use>
|
||||
<use xlink:href="#SvgjsPath1035" x="690" y="330" stroke="#1c538e"></use>
|
||||
<use xlink:href="#SvgjsPath1033" x="690" y="390" stroke="#1c538e"></use>
|
||||
<use xlink:href="#SvgjsPath1033" x="690" y="450" stroke="#1c538e"></use>
|
||||
<use xlink:href="#SvgjsPath1036" x="690" y="510" stroke="#1c538e"></use>
|
||||
<use xlink:href="#SvgjsPath1034" x="690" y="570" stroke="#1c538e"></use>
|
||||
<use xlink:href="#SvgjsPath1036" x="690" y="630" stroke="#1c538e"></use>
|
||||
<use xlink:href="#SvgjsPath1035" x="690" y="690" stroke="#1c538e"></use>
|
||||
</symbol>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 12 KiB |
|
|
@ -0,0 +1,43 @@
|
|||
<template>
|
||||
<div class="mt-4 mx-5">
|
||||
<a-breadcrumb>
|
||||
<a-breadcrumb-item>
|
||||
<i class="icon-park-outline-all-application text-gray-400"></i>
|
||||
</a-breadcrumb-item>
|
||||
<a-breadcrumb-item v-for="(item, index) in items" :key="item">
|
||||
<span
|
||||
:class="
|
||||
index !== items.length - 1 ? 'text-gray-400' : 'text-gray-500'
|
||||
"
|
||||
>
|
||||
{{ item }}
|
||||
</span>
|
||||
</a-breadcrumb-item>
|
||||
</a-breadcrumb>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { MenuItem, menus } from "@/router";
|
||||
|
||||
const route = useRoute();
|
||||
const getPaths = (items: MenuItem[], path: string, paths: string[] = []) => {
|
||||
const item = items.find((i) => {
|
||||
if (i.id.endsWith("index")) {
|
||||
return i.id.includes(path);
|
||||
}
|
||||
return path.includes(i.id) && path.includes(i.path);
|
||||
});
|
||||
if (item) {
|
||||
paths.push(item.title as string);
|
||||
if (item.children?.length) {
|
||||
getPaths(item.children, path, paths);
|
||||
}
|
||||
}
|
||||
return paths;
|
||||
};
|
||||
|
||||
const items = computed(() => getPaths(menus, route.path));
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
<template>
|
||||
<div>
|
||||
<BreadCrumb></BreadCrumb>
|
||||
<div class="mx-4 mt-4 p-4 bg-white">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import BreadCrumb from "./bread-crumb.vue";
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
<template>
|
||||
<div class="w-full h-full flex justify-center items-center p-4">
|
||||
<div class="flex gap-2 items-center">
|
||||
<div>
|
||||
<img src="@/assets/403.svg" alt="forbiden" class="w-[320px]" />
|
||||
</div>
|
||||
<div>
|
||||
<h2 class="text-3xl m-0 font-bold">403</h2>
|
||||
<p class="mt-2">您的权限不足,如需访问请联系管理员分配权限!</p>
|
||||
<div class="space-x-3 mt-6">
|
||||
<a-button type="primary" @click="router.back()">
|
||||
<template #icon>
|
||||
<i class="icon-park-outline-back"></i>
|
||||
</template>
|
||||
返回
|
||||
</a-button>
|
||||
<a-button type="outline" @click="router.push('/')">
|
||||
<template #icon>
|
||||
<i class="icon-park-outline-home"></i>
|
||||
</template>
|
||||
首页
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const router = useRouter();
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
|
||||
<route lang="json">
|
||||
{
|
||||
"meta": {
|
||||
"title": "404",
|
||||
"icon": "icon-park-outline-home"
|
||||
}
|
||||
}
|
||||
</route>
|
||||
|
|
@ -0,0 +1,237 @@
|
|||
### 介绍
|
||||
基于`Arco-Design`组件封装的表单,旨在通过较少的配置提升开发效率,将一些通过的状态管理内置,使得开发者只需关注核心内容即可快速开发通用型表单。
|
||||
|
||||
本表单适用于通用型表单,对于自定义要求较高的需求,可能不太适合。
|
||||
|
||||
### 功能
|
||||
- 配置化编写代码,保证UI一致性,提供开发效率。
|
||||
- 提供typesciprt类型提示
|
||||
- 表单项和校验规则之间可联动、可动态显示/隐藏
|
||||
- 内置常用校验规则,开箱即用
|
||||
- 支持组件参数透传,让每个组件都能自定义。
|
||||
|
||||
### 基本功能
|
||||
基本用法:
|
||||
```tsx
|
||||
<template>
|
||||
<Form v-bind="form" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Form, useForm } from '@/components'
|
||||
|
||||
const form = useForm({
|
||||
model: {
|
||||
id: undefined
|
||||
},
|
||||
items: [
|
||||
{
|
||||
field: 'username',
|
||||
label: '用户名称',
|
||||
type: 'input'
|
||||
}
|
||||
],
|
||||
submit: async ({ model, items }) => {
|
||||
await new Promise(res => setTimeout(res, 2000));
|
||||
return { message: '操作成功!' }
|
||||
},
|
||||
formProps: {},
|
||||
})
|
||||
</script>
|
||||
```
|
||||
以上, 只有四个参数,只需定义关注的内容,剩下的内容如内部状态等, 由表单管理。
|
||||
| 参数 | 说明 | 类型 |
|
||||
| :--- | :--- | :--- |
|
||||
| model | 表单数据(可选),默认从`items`每一项的`field`和`initialValue`生成,如果存在同名属性,将与其合并。 | `Record<string, any>` |
|
||||
| items | 表单项,具体用法看下文。| `FormItem[]` |
|
||||
| submit | 提交表单的函数,可为同步/异步函数。当有返回值且返回值为包含`message`的对象时,将弹出成功提示。| `({ model, items }) => Promise<any>` |
|
||||
| formProps | 传递给`AForm`组件的参数(可选),具体可参考`Arco-Design`的`Form`组件,部分参数不可用,如`model`等。 | `FormInstance['$props']` |
|
||||
|
||||
### 表单数据
|
||||
`model`表示当前表单的数据,当使用`useForm`时,将从`items`中每一项的`field`和`initialValue`生成。如果`model`中的属性与`field`值同名,且`initialValue`值不为空,则原`model`中的同名属性值将被覆盖。
|
||||
|
||||
对于日期范围框、级联选择器等值为数组的组件,提供有一份便捷的语法,请看如下示例:
|
||||
```typescript
|
||||
const form = useForm({
|
||||
items: [
|
||||
{
|
||||
field: `startDate:endDate`,
|
||||
label: '日期范围',
|
||||
type: 'dateRange',
|
||||
},
|
||||
{
|
||||
field: 'provice:city:town',
|
||||
label: '省市区',
|
||||
type: 'cascader',
|
||||
options: []
|
||||
}
|
||||
]
|
||||
})
|
||||
```
|
||||
以上,`field`可通过`:`分隔的语法,指定提交表单时,将数组值划分到指定的属性上,最终提交的数据如下:
|
||||
```typescript
|
||||
{
|
||||
startDate: '',
|
||||
endDate: '',
|
||||
province: '',
|
||||
city: '',
|
||||
town: ''
|
||||
}
|
||||
```
|
||||
|
||||
### 表单项
|
||||
用法示例:
|
||||
```typescript
|
||||
const form = useForm({
|
||||
items: [
|
||||
{
|
||||
field: 'username',
|
||||
initialValue: 'apptify',
|
||||
|
||||
label: '用户名称',
|
||||
type: 'input',
|
||||
|
||||
itemProps: {},
|
||||
nodeProps: {},
|
||||
|
||||
visible: ({ model, item, items }) => true,
|
||||
disable: ({ model, item, items }) => true,
|
||||
|
||||
required: true,
|
||||
rules: ['email'],
|
||||
|
||||
options: ({ model }) => api.xx(model.id)
|
||||
|
||||
component: ({ model, item, items }) => <div> </div>,
|
||||
help: string |
|
||||
}
|
||||
]
|
||||
})
|
||||
```
|
||||
用法说明:
|
||||
| 参数 | 说明 | 类型 | 默认值 |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| field | 字段名,将合并合并到`model`中,默认值为`undefined`,可通过`initalValue`指定初始值 | string | - |
|
||||
| initialValue | 初始值, 作为默认初始值以及通过`formRef.reset`重置表单数据时的值 | any | undefined |
|
||||
| label | 标签名,可为字符串或函数, 作用同`AFormItem`的`label`参数 | string \| ({ model,item }) => JSX.Element | - |
|
||||
| type | 输入控件的类型,具体可参考下文 | NodeType | 'input' |
|
||||
| visible | 动态控制该表单项是否显示 | boolean \| ({ model,item }) => boolean | - |
|
||||
| disable | 动态控制该表单项是否禁止,作用同`FormItem`的`disabled`属性 | boolean \| ({ model, item }) => boolean | - |
|
||||
| required | 是否必填,作用同`AFormItem`的`required`属性 | boolean | - |
|
||||
| rules | 校验规则,内置常用规则,并支持动态生效,详见下文 | RuleType[] | - |
|
||||
| options | 作用域`select`等多选项组件,支持动态获取 | (Option[]) \| ({ model, item }) => Option[] | - |
|
||||
| itemProps | 传递给`AFormItem`组件的参数,部分参数不可用,如上面的`field`等参数 | FormItemInstance['$props'] | - |
|
||||
| nodeProps | 传递给`type`属性对应组件的参数,如当`type`为`input`时, `nodeProps`类型为`Input`组件的props。 | NodeProps | - |
|
||||
|
||||
### 控件类型
|
||||
表单项的`type`指定表单控件的类型,当输入具体的值时,`nodeProps`会提供对应的参数类型提示。内置有常见的组件,且带有默认的参数,具体默认参数可在`src/components/form/form-node.tsx`中查看:
|
||||
|
||||
| 类型 | 说明 |
|
||||
| :--- | :--- |
|
||||
| input | 同 [Input](https://arco.design/vue/component/input) 组件
|
||||
| number | 同 [InputNumber](https://arco.design/vue/component/input-number) 组件
|
||||
| password | 同 [InputPassword](https://arco.design/vue/component/input#password) 组件
|
||||
| select | 同 [Select](https://arco.design/vue/component/select) 组件
|
||||
| time | 同 [TimePicker](https://arco.design/vue/component/time-picker) 组件
|
||||
| date | 同 [DatePicker](https://arco.design/vue/component/date-picker) 组件
|
||||
| dateRange | 同 [RangePicker](https://arco.design/vue/component/date-picker#range) 组件
|
||||
| textarea | 同 [Textarea](https://arco.design/vue/component/textarea) 组件
|
||||
| cascader | 同 [Cascader](https://arco.design/vue/component/cascader) 组件
|
||||
| checkbox | 同 [Checkbox](https://arco.design/vue/component/checkbox) 组件
|
||||
| radio | 同 [Radio](https://arco.design/vue/component/radio) 组件
|
||||
| slider | 同 [Slider](https://arco.design/vue/component/slider) 组件
|
||||
| submit | 提交表单按钮,应只有一个。
|
||||
| custom | 自定义组件,通过表单项的`component`属性定义,需返回一个JSX元素。
|
||||
|
||||
对于`select`、`checkbox`、`radio`和`cascader`类型,其`options`参数不通过`nodeProps`传递,而是写在表单项的`options`属性。该属性支持数组和函数类型,当为数组类型时将直接传递给控件,当为函数时可动态请求,返回值需为数组类型。
|
||||
|
||||
以上描述,示例如下:
|
||||
```typescript
|
||||
const form = useForm({
|
||||
items: [
|
||||
{
|
||||
field: 'gender',
|
||||
label: '性别',
|
||||
type: 'select',
|
||||
options: [
|
||||
{
|
||||
label: '男',
|
||||
value: 1,
|
||||
},
|
||||
{
|
||||
label: '女',
|
||||
value: 2,
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
field: 'departmentId',
|
||||
label: '部门',
|
||||
type: 'cascader',
|
||||
options: async ({ model, item }) => {
|
||||
const res = await api.getDepartments(model.xx);
|
||||
return res.data;
|
||||
}
|
||||
}
|
||||
]
|
||||
})
|
||||
```
|
||||
|
||||
### 表单校验
|
||||
|
||||
跟表单校验相关的属性有2个,`required`(必填)和`rules`属性,其中`rules`内置常见的校验规则,参考如下:
|
||||
| 校验规则 | 说明 |
|
||||
| :--- | :--- |
|
||||
| string | 格式为字符串 |
|
||||
| number | 格式为数字 |
|
||||
| passwod | 格式为密码类型,即至少包含大写字母、小写字母、数字和特殊字符。|
|
||||
| required | 该项必填 |
|
||||
| email | 格式为邮箱类型,例如: xx@abc.com |
|
||||
| url | 格式为URL类型, 例如: https://abc.com |
|
||||
| ip | 格式为IP类型, 例如: 101.10.10.302 |
|
||||
| phone | 格式为11位手机号,例如: 15912345678 |
|
||||
| idcard | 格式为18位身份证号,例如: 12345619991205131x |
|
||||
| alphabet | 格式为26字母,例如:apptify |
|
||||
|
||||
当以上规则不满足需求时,可通过对象自定义校验规则,具体语法可参考`AFormItem`的 [FieldRule](https://arco.design/vue/component/form#FieldRule) 文档。在其基础上,可添加一个`disable`函数,用于动态禁止/允许当前校验规则。
|
||||
|
||||
用法示例:
|
||||
```typescript
|
||||
const form = useForm({
|
||||
items: [
|
||||
{
|
||||
required: true,
|
||||
rules: [
|
||||
'email',
|
||||
{
|
||||
match: /\d{2,3}/,
|
||||
message: '请输入2~3位数字',
|
||||
disable: ({ model, item, items }) => !model.username
|
||||
}
|
||||
],
|
||||
}
|
||||
]
|
||||
})
|
||||
```
|
||||
|
||||
### 提交表单
|
||||
`submit`为提交表单的函数,通常返回一个`promise`,当该函数抛出异常,则默认为提交失败。该函数有一个可选的返回值,如果返回值为包含`message`的对象时,将弹出一个包含`message`值的成功提示。
|
||||
|
||||
示例如下:
|
||||
```typescript
|
||||
const form = useForm({
|
||||
submit: async ({ model, items }) => {
|
||||
const res = await api.xx(model);
|
||||
return { message: res.msg }
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
### 常见问题
|
||||
- Q:为什么不是模板形式?
|
||||
- A:配置式更易于描述逻辑,模板介入和引入的组件比较多,且对于做typescript类型提示不是很方便。
|
||||
- Q:为什么不是JSON形式?
|
||||
- A:对于自定义组件支持、联动等不是非常友好,尽管可以通过解析字符串执行等方式实现,对typescript提示也不是很友好。
|
||||
|
||||
### 最后
|
||||
尽管看起来是低代码,但其实我更倾向于是业务组件。
|
||||
|
|
@ -0,0 +1,225 @@
|
|||
import { FormItem as BaseFormItem, FieldRule, FormItemInstance, SelectOptionData } from "@arco-design/web-vue";
|
||||
import { NodeType, NodeUnion, nodeMap } from "./form-node";
|
||||
|
||||
const defineRuleMap = <T extends Record<string, FieldRule>>(ruleMap: T) => ruleMap;
|
||||
|
||||
const ruleMap = defineRuleMap({
|
||||
required: {
|
||||
required: true,
|
||||
message: "该项不能为空",
|
||||
},
|
||||
string: {
|
||||
type: "string",
|
||||
message: "请输入字符串",
|
||||
},
|
||||
number: {
|
||||
type: "number",
|
||||
message: "请输入数字",
|
||||
},
|
||||
email: {
|
||||
type: "email",
|
||||
message: "邮箱格式错误,示例: xx@abc.com",
|
||||
},
|
||||
url: {
|
||||
type: "url",
|
||||
message: "URL格式错误, 示例: www.abc.com",
|
||||
},
|
||||
ip: {
|
||||
type: "ip",
|
||||
message: "IP格式错误, 示例: 101.10.10.30",
|
||||
},
|
||||
phone: {
|
||||
match: /^(?:(?:\+|00)86)?1\d{10}$/,
|
||||
message: "手机格式错误, 示例(11位): 15912345678",
|
||||
},
|
||||
idcard: {
|
||||
match: /^[1-9]\d{5}(?:18|19|20)\d{2}(?:0[1-9]|10|11|12)(?:0[1-9]|[1-2]\d|30|31)\d{3}[\dXx]$/,
|
||||
message: "身份证格式错误, 长度为15或18位",
|
||||
},
|
||||
alphabet: {
|
||||
match: /^[a-zA-Z]\w{4,15}$/,
|
||||
message: "请输入英文字母, 长度为4~15位",
|
||||
},
|
||||
password: {
|
||||
match: /^\S*(?=\S{6,})(?=\S*\d)(?=\S*[A-Z])(?=\S*[a-z])(?=\S*[!@#$%^&*? ])\S*$/,
|
||||
message: "至少包含大写字母、小写字母、数字和特殊字符",
|
||||
},
|
||||
});
|
||||
|
||||
export type FieldStringRule = keyof typeof ruleMap;
|
||||
|
||||
export type FieldObjectRule = FieldRule & {
|
||||
disable?: (arg: { item: IFormItem; model: Record<string, any> }) => boolean;
|
||||
};
|
||||
|
||||
export type FieldRuleType = FieldStringRule | FieldObjectRule;
|
||||
|
||||
/**
|
||||
* 表单项
|
||||
*/
|
||||
export const FormItem = (props: any, { emit }: any) => {
|
||||
const { item } = props;
|
||||
const args = {
|
||||
...props,
|
||||
field: item.field,
|
||||
};
|
||||
|
||||
const rules = computed(() => {
|
||||
const result = [];
|
||||
if (item.required) {
|
||||
result.push(ruleMap.required);
|
||||
}
|
||||
item.rules?.forEach((rule: any) => {
|
||||
if (typeof rule === "string") {
|
||||
result.push(ruleMap[rule as FieldStringRule]);
|
||||
return;
|
||||
}
|
||||
if (!rule.disable) {
|
||||
result.push(rule);
|
||||
return;
|
||||
}
|
||||
if (!rule.disable({ model: props.model, item, items: props.items })) {
|
||||
result.push(rule);
|
||||
}
|
||||
});
|
||||
return result;
|
||||
});
|
||||
|
||||
const disabled = computed(() => {
|
||||
if (item.disable === undefined) {
|
||||
return false;
|
||||
}
|
||||
if (typeof item.disable === "function") {
|
||||
return item.disable(args);
|
||||
}
|
||||
return item.disable;
|
||||
});
|
||||
|
||||
if (item.visible && !item.visible(args)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<BaseFormItem rules={rules.value} disabled={disabled.value} field={item.field} {...item.itemProps}>
|
||||
{{
|
||||
default: () => {
|
||||
if (item.component) {
|
||||
return <item.component {...item.nodeProps} />;
|
||||
}
|
||||
const comp = nodeMap[item.type as NodeType]?.component;
|
||||
if (!comp) {
|
||||
return null;
|
||||
}
|
||||
if (item.type === "submit") {
|
||||
return <comp loading={props.loading} onSubmit={() => emit("submit")} onCancel={emit("cancel")} />;
|
||||
}
|
||||
return <comp v-model={props.model[item.field]} {...item.nodeProps} />;
|
||||
},
|
||||
label: item.label && (() => (typeof item.label === "string" ? item.label : item.label?.(args))),
|
||||
help: item.help && (() => (typeof item.help === "string" ? item.help : item.help?.(args))),
|
||||
extra: item.extra && (() => (typeof item.extra === "string" ? item.extra : item.extra?.(args))),
|
||||
}}
|
||||
</BaseFormItem>
|
||||
);
|
||||
};
|
||||
|
||||
type FormItemBase = {
|
||||
/**
|
||||
* 字段名,用于表单数据、表单校验和输入框值绑定,支持特殊语法。
|
||||
* @example
|
||||
* ```typescript
|
||||
* // 1. 以:分隔的字段名,将用作数组值解构。例如:
|
||||
* {
|
||||
* field: 'v1:v2',
|
||||
* type: 'dateRange',
|
||||
* }
|
||||
* // 将得到
|
||||
* {
|
||||
* v1: '2021-01-01',
|
||||
* v2: '2021-01-02',
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
field: string;
|
||||
|
||||
/**
|
||||
* 初始值
|
||||
* @description 默认值为undefined,优先级比model中的同名属性高。
|
||||
*/
|
||||
initialValue?: any;
|
||||
|
||||
/**
|
||||
* 标签名
|
||||
* @description 同FormItem组件的label属性
|
||||
*/
|
||||
label?: string | ((item: IFormItem, model: Record<string, any>) => any);
|
||||
|
||||
/**
|
||||
* 传递给`FormItem`组件的参数
|
||||
* @description 部分属性会不可用,如field、label、required、rules、disabled等
|
||||
*/
|
||||
itemProps?: Partial<Omit<FormItemInstance["$props"], "field" | "label" | "required" | "rules" | "disabled">>;
|
||||
|
||||
/**
|
||||
* 是否必填
|
||||
* @description 默认值为false
|
||||
*/
|
||||
required?: boolean;
|
||||
|
||||
/**
|
||||
* 校验规则数组
|
||||
* @description 支持字符串(内置)、对象形式
|
||||
* @example
|
||||
* ```typescript
|
||||
* rules: [
|
||||
* 'idcard', // 内置的身份证号校验规则
|
||||
* {
|
||||
* match: /\d+/,
|
||||
* message: '请输入数字',
|
||||
* },
|
||||
* ]
|
||||
*```
|
||||
* @see https://arco.design/vue/component/form#FieldRule
|
||||
*/
|
||||
rules?: FieldRuleType[];
|
||||
|
||||
/**
|
||||
* 是否可见
|
||||
* @description 动态控制表单项是否可见
|
||||
*/
|
||||
visible?: (arg: { item: IFormItem; model: Record<string, any> }) => boolean;
|
||||
|
||||
/**
|
||||
* 是否禁用
|
||||
* @description 动态控制表单项是否禁用
|
||||
*/
|
||||
disable?: (arg: { item: IFormItem; model: Record<string, any> }) => boolean;
|
||||
|
||||
/**
|
||||
* 选项,数组或者函数
|
||||
* @description 用于下拉框、单选框、多选框等组件, 支持动态加载
|
||||
*/
|
||||
options?: SelectOptionData[] | ((arg: { item: IFormItem; model: Record<string, any> }) => Promise<any>);
|
||||
|
||||
/**
|
||||
* 表单项内容的渲染函数
|
||||
* @description 用于自定义表单项内容
|
||||
*/
|
||||
component?: (args: { item: IFormItem; model: Record<string, any>; field: string }) => any;
|
||||
|
||||
/**
|
||||
* 帮助提示
|
||||
* @description 同FormItem组件的help插槽
|
||||
* @see https://arco.design/vue/component/form#form-item%20Slots
|
||||
*/
|
||||
help?: string | ((args: { item: IFormItem; model: Record<string, any> }) => any);
|
||||
|
||||
/**
|
||||
* 额外内容
|
||||
* @description 同FormItem组件的extra插槽
|
||||
* @see https://arco.design/vue/component/form#form-item%20Slots
|
||||
*/
|
||||
extra?: string | ((args: { item: IFormItem; model: Record<string, any> }) => any);
|
||||
};
|
||||
|
||||
export type IFormItem = FormItemBase & NodeUnion;
|
||||
|
|
@ -0,0 +1,183 @@
|
|||
import { Button, ButtonInstance, FormInstance, Message, Modal } from "@arco-design/web-vue";
|
||||
import { assign, cloneDeep, omit } from "lodash-es";
|
||||
import { PropType, defineComponent } from "vue";
|
||||
import { Form } from "./form";
|
||||
import { IFormItem } from "./form-item";
|
||||
|
||||
/**
|
||||
* 表单弹窗组件
|
||||
*/
|
||||
export const FormModal = defineComponent({
|
||||
name: "FormModal",
|
||||
inheritAttrs: false,
|
||||
props: {
|
||||
/**
|
||||
* 弹窗标题
|
||||
* @default '新建'
|
||||
*/
|
||||
title: {
|
||||
type: [String, Function] as PropType<
|
||||
string | ((args: { model: Record<string, any>; items: IFormItem[] }) => string)
|
||||
>,
|
||||
default: "新建",
|
||||
},
|
||||
/**
|
||||
* 触发元素
|
||||
*/
|
||||
trigger: {
|
||||
type: [Boolean, Object] as PropType<boolean | (ButtonInstance["$props"] & { text: string })>,
|
||||
default: true,
|
||||
},
|
||||
/**
|
||||
* 传递给Modal组件的props
|
||||
*/
|
||||
modalProps: {
|
||||
type: Object as PropType<Omit<InstanceType<typeof Modal>["$props"], "visible" | "title" | "onBeforeOk">>,
|
||||
},
|
||||
/**
|
||||
* 表单数据
|
||||
*/
|
||||
model: {
|
||||
type: Object as PropType<Record<string, any>>,
|
||||
required: true,
|
||||
},
|
||||
/**
|
||||
* 表单各项
|
||||
*/
|
||||
items: {
|
||||
type: Array as PropType<IFormItem[]>,
|
||||
required: true,
|
||||
},
|
||||
/**
|
||||
* 提交表单的函数
|
||||
* @description 可返回`{ message }`类型,用于显示提示信息
|
||||
*/
|
||||
submit: {
|
||||
type: Function as PropType<(arg: { model: Record<string, any>; items: IFormItem[] }) => any | Promise<any>>,
|
||||
default: () => true,
|
||||
},
|
||||
/**
|
||||
* 传递给Form组件的props
|
||||
*/
|
||||
formProps: {
|
||||
type: Object as PropType<Omit<FormInstance["$props"], "model">>,
|
||||
},
|
||||
},
|
||||
emits: ["close", "submited"],
|
||||
setup(props, { slots, emit, attrs }) {
|
||||
const origin = cloneDeep(props.model);
|
||||
const formRef = ref<InstanceType<typeof Form>>();
|
||||
const loading = ref(false);
|
||||
const visible = ref(false);
|
||||
|
||||
const open = async (data: Record<string, any> = {}) => {
|
||||
visible.value = true;
|
||||
await nextTick();
|
||||
for (const key in data) {
|
||||
props.model[key] = data[key];
|
||||
}
|
||||
};
|
||||
|
||||
const onBeforeOk = async () => {
|
||||
if (typeof attrs.onBeforeOk === "function") {
|
||||
const isOk = await attrs.onBeforeOk();
|
||||
if (!isOk) return false;
|
||||
}
|
||||
const errors = await formRef.value?.formRef?.validate();
|
||||
if (errors) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
const model = formRef.value?.getModel() || {};
|
||||
const res = await props.submit?.({ items: props.items, model });
|
||||
res?.message && Message.success(`提示: ${res.message}`);
|
||||
emit("submited", res);
|
||||
} catch (error: any) {
|
||||
error.message && Message.error(`提示: ${error.message}`);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const onClose = () => {
|
||||
visible.value = false;
|
||||
assign(props.model, origin);
|
||||
emit("close");
|
||||
};
|
||||
|
||||
const modalTitle = computed(() => {
|
||||
if (typeof props.title === "string") {
|
||||
return props.title;
|
||||
}
|
||||
if (typeof props.title === "function") {
|
||||
return props.title({ model: props.model, items: props.items });
|
||||
}
|
||||
});
|
||||
|
||||
const modalTrigger = computed(() => {
|
||||
if (!props.trigger) {
|
||||
return null;
|
||||
}
|
||||
let content;
|
||||
if (typeof props.trigger === "boolean") {
|
||||
content = (
|
||||
<Button type="primary">
|
||||
{{
|
||||
default: () => "新建",
|
||||
icon: () => <i class="icon-park-outline-plus" />,
|
||||
}}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
if (typeof props.trigger === "object") {
|
||||
content = (
|
||||
<Button type="primary" {...omit(props.trigger, "text")}>
|
||||
{props.trigger?.text || "新建"}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
if (slots.trigger) {
|
||||
content = slots.trigger({ model: props.model, items: props.items });
|
||||
}
|
||||
return <span onClick={() => open()}>{content}</span>;
|
||||
});
|
||||
|
||||
return {
|
||||
origin,
|
||||
formRef,
|
||||
loading,
|
||||
visible,
|
||||
modalTitle,
|
||||
modalTrigger,
|
||||
open,
|
||||
onClose,
|
||||
onBeforeOk,
|
||||
};
|
||||
},
|
||||
render() {
|
||||
return (
|
||||
<>
|
||||
{this.modalTrigger}
|
||||
<Modal
|
||||
{...this.modalProps}
|
||||
v-model:visible={this.visible}
|
||||
onBeforeOk={this.onBeforeOk}
|
||||
onClose={this.onClose}
|
||||
title={this.modalTitle}
|
||||
>
|
||||
{this.visible && (
|
||||
<Form ref={(el: any) => (this.formRef = el)} {...this.formProps} model={this.model} items={this.items}>
|
||||
{{ ...this.$slots }}
|
||||
</Form>
|
||||
)}
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
export type FormModalInstance = InstanceType<typeof FormModal>;
|
||||
|
||||
export type FormModalProps = FormModalInstance["$props"];
|
||||
|
||||
export default FormModal;
|
||||
|
|
@ -0,0 +1,223 @@
|
|||
import {
|
||||
Button,
|
||||
Cascader,
|
||||
CheckboxGroup,
|
||||
DatePicker,
|
||||
Input,
|
||||
InputNumber,
|
||||
InputPassword,
|
||||
RadioGroup,
|
||||
RangePicker,
|
||||
Select,
|
||||
Slider,
|
||||
Textarea,
|
||||
TimePicker,
|
||||
} from "@arco-design/web-vue";
|
||||
|
||||
const initOptions = ({ item, model }: any) => {
|
||||
if (Array.isArray(item.options)) {
|
||||
item.nodeProps.options = item.options;
|
||||
return;
|
||||
}
|
||||
if (typeof item.options !== "function") {
|
||||
return;
|
||||
}
|
||||
item.nodeProps.options = reactive([]);
|
||||
const fetchData = item.options;
|
||||
item._updateOptions = async () => {
|
||||
let data = await fetchData({ item, model });
|
||||
if (Array.isArray(data?.data)) {
|
||||
data = data.data.map((i: any) => ({ label: i.name, value: i.id }));
|
||||
}
|
||||
if (Array.isArray(data)) {
|
||||
item.nodeProps.options.splice(0);
|
||||
item.nodeProps.options.push(...data);
|
||||
}
|
||||
};
|
||||
item._updateOptions();
|
||||
};
|
||||
|
||||
const defineNodeMap = <T extends { [key: string]: { component: any, nodeProps: any } }>(map: T) => {
|
||||
return map
|
||||
}
|
||||
|
||||
/**
|
||||
* 表单项组件映射
|
||||
*/
|
||||
export const nodeMap = {
|
||||
/**
|
||||
* 输入框
|
||||
*/
|
||||
input: {
|
||||
component: Input,
|
||||
nodeProps: {
|
||||
placeholder: "请输入",
|
||||
allowClear: true,
|
||||
} as InstanceType<typeof Input>["$props"],
|
||||
},
|
||||
/**
|
||||
* 文本域
|
||||
*/
|
||||
textarea: {
|
||||
component: Textarea,
|
||||
nodeProps: {
|
||||
placeholder: "请输入",
|
||||
allowClear: true,
|
||||
} as InstanceType<typeof Textarea>["$props"],
|
||||
},
|
||||
/**
|
||||
* 数值输入框
|
||||
*/
|
||||
number: {
|
||||
component: InputNumber,
|
||||
nodeProps: {
|
||||
placeholder: "请输入",
|
||||
defaultValue: 0,
|
||||
allowClear: true,
|
||||
} as InstanceType<typeof InputNumber>["$props"],
|
||||
},
|
||||
/**
|
||||
* 密码输入框
|
||||
*/
|
||||
password: {
|
||||
component: InputPassword,
|
||||
nodeProps: {
|
||||
placeholder: "请输入",
|
||||
} as InstanceType<typeof InputPassword>["$props"],
|
||||
},
|
||||
/**
|
||||
* 选择框
|
||||
*/
|
||||
select: {
|
||||
component: Select,
|
||||
nodeProps: {
|
||||
placeholder: "请选择",
|
||||
allowClear: true,
|
||||
allowSearch: true,
|
||||
options: [{}],
|
||||
} as InstanceType<typeof Select>["$props"],
|
||||
init: initOptions,
|
||||
},
|
||||
/**
|
||||
* 级联选择框
|
||||
*/
|
||||
cascader: {
|
||||
component: Cascader,
|
||||
nodeProps: {
|
||||
placeholder: "请选择",
|
||||
allowClear: true,
|
||||
expandTrigger: "hover",
|
||||
} as InstanceType<typeof Cascader>["$props"],
|
||||
init: initOptions,
|
||||
},
|
||||
/**
|
||||
* 时间选择框
|
||||
*/
|
||||
time: {
|
||||
component: TimePicker,
|
||||
nodeProps: {
|
||||
allowClear: true,
|
||||
} as InstanceType<typeof TimePicker>["$props"],
|
||||
},
|
||||
/**
|
||||
* 日期选择框
|
||||
*/
|
||||
date: {
|
||||
component: DatePicker,
|
||||
nodeProps: {
|
||||
allowClear: true,
|
||||
} as InstanceType<typeof DatePicker>["$props"],
|
||||
},
|
||||
/**
|
||||
* 日期范围选择框
|
||||
*/
|
||||
dateRange: {
|
||||
component: RangePicker,
|
||||
nodeProps: {
|
||||
allowClear: true,
|
||||
} as InstanceType<typeof RangePicker>["$props"],
|
||||
},
|
||||
/**
|
||||
* 复选框
|
||||
*/
|
||||
checkbox: {
|
||||
component: CheckboxGroup,
|
||||
nodeProps: {
|
||||
allowClear: true,
|
||||
} as InstanceType<typeof CheckboxGroup>["$props"],
|
||||
init: initOptions,
|
||||
},
|
||||
/**
|
||||
* 复选框
|
||||
*/
|
||||
radio: {
|
||||
component: RadioGroup,
|
||||
nodeProps: {
|
||||
allowClear: true,
|
||||
} as InstanceType<typeof RadioGroup>["$props"],
|
||||
init: initOptions,
|
||||
},
|
||||
/**
|
||||
* 滑动输入条
|
||||
*/
|
||||
slider: {
|
||||
component: Slider,
|
||||
nodeProps: {
|
||||
allowClear: true,
|
||||
} as InstanceType<typeof Slider>["$props"],
|
||||
},
|
||||
/**
|
||||
* 底部
|
||||
*/
|
||||
submit: {
|
||||
component: (props: any, { emit }: any) => {
|
||||
const state = inject("tableInstance");
|
||||
console.log("st", state);
|
||||
return (
|
||||
<>
|
||||
<Button type="primary" loading={props.loading} onClick={() => emit("submit")} class="mr-3">
|
||||
立即提交
|
||||
</Button>
|
||||
{/* <Button loading={props.loading} onClick={() => emit("cancel")}>
|
||||
重置
|
||||
</Button> */}
|
||||
</>
|
||||
);
|
||||
},
|
||||
nodeProps: {},
|
||||
},
|
||||
/**
|
||||
* 自定义组件
|
||||
*/
|
||||
custom: {
|
||||
nodeProps: {},
|
||||
component: () => null,
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* 所有组件类型
|
||||
*/
|
||||
export type NodeMap = typeof nodeMap;
|
||||
|
||||
/**
|
||||
* 组件类型
|
||||
*/
|
||||
export type NodeType = keyof NodeMap;
|
||||
|
||||
/**
|
||||
* 提供给`FormItem`的联合类型
|
||||
* @description 当输入type,nodeProps会提供对应类型提示
|
||||
*/
|
||||
export type NodeUnion = {
|
||||
[key in NodeType]: {
|
||||
/**
|
||||
* 输入框类型,默认为`input`
|
||||
*/
|
||||
type: key;
|
||||
/**
|
||||
* 传递给`type`属性对应组件的参数
|
||||
*/
|
||||
nodeProps?: NodeMap[key]["nodeProps"];
|
||||
};
|
||||
}[NodeType];
|
||||
|
|
@ -0,0 +1,133 @@
|
|||
import { Form as BaseForm, FormInstance as BaseFormInstance, Message } from "@arco-design/web-vue";
|
||||
import { assign, cloneDeep, defaultsDeep } from "lodash-es";
|
||||
import { PropType } from "vue";
|
||||
import { FormItem, IFormItem } from "./form-item";
|
||||
import { NodeType, nodeMap } from "./form-node";
|
||||
|
||||
/**
|
||||
* 表单组件
|
||||
*/
|
||||
export const Form = defineComponent({
|
||||
name: "Form",
|
||||
props: {
|
||||
/**
|
||||
* 表单数据
|
||||
*/
|
||||
model: {
|
||||
type: Object as PropType<Record<string, any>>,
|
||||
default: () => reactive({}),
|
||||
},
|
||||
/**
|
||||
* 表单项
|
||||
*/
|
||||
items: {
|
||||
type: Array as PropType<IFormItem[]>,
|
||||
default: () => [],
|
||||
},
|
||||
/**
|
||||
* 提交表单
|
||||
*/
|
||||
submit: {
|
||||
type: Function as PropType<(arg: { model: Record<string, any>; items: IFormItem[] }) => Promise<any>>,
|
||||
},
|
||||
/**
|
||||
* 传给Form组件的参数
|
||||
*/
|
||||
formProps: {
|
||||
type: Object as PropType<Omit<BaseFormInstance["$props"], "model">>,
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const model = cloneDeep(props.model);
|
||||
const formRef = ref<InstanceType<typeof BaseForm>>();
|
||||
const loading = ref(false);
|
||||
|
||||
props.items.forEach((item: any) => {
|
||||
const node = nodeMap[item.type as NodeType];
|
||||
defaultsDeep(item, { nodeProps: node?.nodeProps ?? {} });
|
||||
(node as any).init?.({ item, model: props.model });
|
||||
});
|
||||
|
||||
const getItem = (field: string) => {
|
||||
return props.items.find((item) => item.field === field);
|
||||
};
|
||||
|
||||
const getModel = () => {
|
||||
const model: Record<string, any> = {};
|
||||
for (const key of Object.keys(props.model)) {
|
||||
if (/[^:]+:[^:]+/.test(key)) {
|
||||
const keys = key.split(":");
|
||||
const vals = cloneDeep(props.model[key] || []);
|
||||
for (const k of keys) {
|
||||
model[k] = vals.shift();
|
||||
}
|
||||
} else {
|
||||
model[key] = cloneDeep(props.model[key]);
|
||||
}
|
||||
}
|
||||
return model;
|
||||
};
|
||||
|
||||
const submitForm = async () => {
|
||||
if (await formRef.value?.validate()) {
|
||||
return;
|
||||
}
|
||||
const model: Record<string, any> = getModel();
|
||||
try {
|
||||
loading.value = true;
|
||||
const res = await props.submit?.({ model, items: props.items });
|
||||
res.message && Message.success(`提示: ${res.message}`);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const resetModel = () => {
|
||||
assign(props.model, model);
|
||||
};
|
||||
|
||||
const setModel = (model: Record<string, any>) => {
|
||||
for (const key of Object.keys(props.model)) {
|
||||
if (/.+:.+/.test(key)) {
|
||||
const [key1, key2] = key.split(":");
|
||||
props.model[key] = [model[key1], model[key2]];
|
||||
} else {
|
||||
props.model[key] = model[key];
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
formRef,
|
||||
loading,
|
||||
getItem,
|
||||
submitForm,
|
||||
resetModel,
|
||||
setModel,
|
||||
getModel,
|
||||
};
|
||||
},
|
||||
render() {
|
||||
(this.items as any).instance = this;
|
||||
|
||||
const props = {
|
||||
items: this.items,
|
||||
model: this.model,
|
||||
slots: this.$slots,
|
||||
};
|
||||
|
||||
return (
|
||||
<BaseForm ref="formRef" layout="vertical" model={this.model} {...this.$attrs} {...this.formProps}>
|
||||
{this.items.map((item) => (
|
||||
<FormItem loading={this.loading} onSubmit={this.submitForm} item={item} {...props}></FormItem>
|
||||
))}
|
||||
</BaseForm>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
export type FormInstance = InstanceType<typeof Form>;
|
||||
|
||||
export type FormProps = FormInstance["$props"];
|
||||
|
||||
export type FormDefinedProps = Pick<FormProps, "model" | "items" | "submit" | "formProps">;
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
export * from "./form";
|
||||
export * from "./use-form";
|
||||
export * from "./form-modal";
|
||||
export * from "./use-form-modal";
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
import { Modal } from "@arco-design/web-vue";
|
||||
import { assign } from "lodash-es";
|
||||
import { reactive } from "vue";
|
||||
import { useForm } from "./use-form";
|
||||
import { FormModalProps } from "./form-modal";
|
||||
|
||||
const defaults: Partial<InstanceType<typeof Modal>> = {
|
||||
width: 1080,
|
||||
titleAlign: "start",
|
||||
};
|
||||
|
||||
/**
|
||||
* 构建传给FormModal组件的参数
|
||||
* @see src/components/form/use-form-modal.tsx
|
||||
*/
|
||||
export const useFormModal = (options: FormModalProps): FormModalProps & { model: Record<string, any> } => {
|
||||
const { model, items } = options || {};
|
||||
|
||||
const form = useForm({ model, items });
|
||||
|
||||
return reactive(assign({ modalProps: { ...defaults } }, { ...options, ...form }));
|
||||
};
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
import { FormInstance } from "@arco-design/web-vue";
|
||||
import { IFormItem } from "./form-item";
|
||||
|
||||
export type Options = {
|
||||
/**
|
||||
* 表单数据模型
|
||||
*/
|
||||
model?: Record<string, any>;
|
||||
/**
|
||||
* 表单项数组
|
||||
*/
|
||||
items: IFormItem[];
|
||||
/**
|
||||
* 提交表单
|
||||
*/
|
||||
submit?: (arg: { model: Record<string, any>; items: IFormItem[] }) => Promise<any>;
|
||||
/**
|
||||
* 表单实例属性
|
||||
*/
|
||||
formProps?: Partial<FormInstance["$props"]>;
|
||||
};
|
||||
|
||||
/**
|
||||
* 构建表单组件的参数
|
||||
* @see src/components/form/use-form.tsx
|
||||
*/
|
||||
export const useForm = (options: Options) => {
|
||||
const { model = { id: undefined } } = options;
|
||||
const items: IFormItem[] = [];
|
||||
|
||||
options.items.forEach((item) => {
|
||||
if (!item.nodeProps) {
|
||||
item.nodeProps = {} as any;
|
||||
}
|
||||
if (/(.+)\?(.+)/.test(item.field)) {
|
||||
const [field, condition] = item.field.split("?");
|
||||
model[field] = item.initialValue ?? model[item.field];
|
||||
const params = new URLSearchParams(condition);
|
||||
for (const [key, value] of params.entries()) {
|
||||
model[key] = value;
|
||||
}
|
||||
}
|
||||
model[item.field] = model[item.field] ?? item.initialValue;
|
||||
const _item = { ...item };
|
||||
items.push(_item);
|
||||
});
|
||||
|
||||
if (options.submit) {
|
||||
const submit = items.find((item) => item.type === "submit");
|
||||
if (!submit) {
|
||||
items.push({
|
||||
field: "id",
|
||||
type: "submit",
|
||||
itemProps: {
|
||||
hideLabel: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return reactive({ ...options, model, items }) as any;
|
||||
};
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
export * from "./form";
|
||||
export * from "./table";
|
||||
export * from "./toast";
|
||||
|
|
@ -0,0 +1,156 @@
|
|||
### 基本用法
|
||||
|
||||
```typescript
|
||||
<template>
|
||||
<Table v-bind="table" />
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { Table, useTable } from '@/components'
|
||||
const table = useTable({
|
||||
data: (search, paging) => {
|
||||
return {
|
||||
data: [
|
||||
{
|
||||
username: '用户A'
|
||||
}
|
||||
],
|
||||
meta: {
|
||||
total: 30
|
||||
}
|
||||
};
|
||||
},
|
||||
columns: [
|
||||
{
|
||||
title: "用户名称",
|
||||
dataIndex: "username",
|
||||
},
|
||||
],
|
||||
pagination: {
|
||||
pageSize: 10,
|
||||
showTotal: true
|
||||
},
|
||||
search: {
|
||||
items: [
|
||||
{
|
||||
field: "username",
|
||||
label: "用户名称",
|
||||
type: "input",
|
||||
},
|
||||
],
|
||||
},
|
||||
common: {
|
||||
items: [
|
||||
{
|
||||
field: "username",
|
||||
label: "用户名称",
|
||||
type: "input",
|
||||
},
|
||||
],
|
||||
},
|
||||
create: {
|
||||
title: "新建用户",
|
||||
submit: async ({ model }) => {
|
||||
return api.xx(model);
|
||||
},
|
||||
},
|
||||
modify: {
|
||||
title: "修改用户",
|
||||
submit: async ({ model }) => {
|
||||
return api.xx(model);
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
```
|
||||
以上,就是一个CRUD表格的简单用法。参数描述:
|
||||
| 参数 | 说明 | 类型 |
|
||||
| :--- | :--- | :--- |
|
||||
| data | 表格数据,可为数组或函数(发起HTTP请求) | BaseData[] | ((search, paging) => Promise<any>) |
|
||||
| columns | 表格列,参见 [TableColumnData](https://arco.design/vue/component/table#TableColumnData) 文档,增加和扩展部分属性,详见下文。 | TableColumnData[] |
|
||||
| pagination | 分页参数,参见 [Pagination](https://arco.design/vue/component/pagination) 文档,默认 15/每页。| Pagination |
|
||||
| search | 搜索表单的配置,参见 [Form]() 说明,其中 `submit` 参数不可用 | FormProps |
|
||||
| common | 新增和修改表单弹窗的公用参数,参见 [FormModal]() 说明。 | FormModalProps |
|
||||
| create | 新增表单弹窗的参数,参见 [FormModal]() 说明, 将与`common`参数合并。 | FormModalProps |
|
||||
| modify | 修改表单弹窗的参数,参见 [FormModal]() 说明, 将与`common`参数合并。 | FormModalProps |
|
||||
| tableProps | 传递给`Table`组件的参数,参见 [Table](https://arco.design/vue/component/table) 文档,其中`columns`参数不可用。| TableProps |
|
||||
|
||||
### 表格数据
|
||||
`data`定义表格数据,可以是数组或函数。
|
||||
- 当是数组时,直接用作数据源。
|
||||
- 当是函数时,传入查询参数和分页参数,可返回数组或对象,返回数组作用同上,返回对象时需遵循`{ data: [], meta: { total: number } }`格式,用于分页处理。
|
||||
|
||||
用法示例:
|
||||
```typescript
|
||||
const table = useTable({
|
||||
data: async (search, paging) {
|
||||
const { page, size: pageSize } = paging
|
||||
const res = await api.xx({ ...search, page, pageSize });
|
||||
return {
|
||||
data: res.data,
|
||||
meta: {
|
||||
total: res.total
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
### 表格列
|
||||
`columns`定义表格列,并在原本基础上增加默认值并扩展部分属性。增加和扩展的属性如下:
|
||||
|
||||
| 参数 | 说明 | 类型 |
|
||||
| :--- | :--- | :--- |
|
||||
| type | 特殊类型, 目前支持`index`(表示行数)、`button`(行操作按钮) | 'index' | 'button' |
|
||||
| buttons | 当`type`为`button`时的按钮数组,如果子项是对象则为`Button`组件的参数,如果为函数则为自定义渲染函数。 | Button[]
|
||||
|
||||
### 表格分页
|
||||
`pagination`定义分页行为,具体参数可参考 [Pagination](https://arco.design/vue/component/pagination) 文档。当`data`为数组时,将作为数据源进行分页;当`data`为函数且返回值为对象时,则根据`total`值进行分页。
|
||||
|
||||
### 搜索表单
|
||||
参阅
|
||||
|
||||
### 公共参数
|
||||
参数为`FormModal`的参数,主要作为新增和修改的公共参数。在大多数情况,新增和修改的配置大多是相似的,没必要写两份,把相同的参数写在这里即可,不同的参数在`create`和`modify`中单独配置。
|
||||
|
||||
注意,这里的`items`也可以被搜索表单复用,搜索表单可通过`extends: <field>`继承`common.items`中对应的字段配置。使用示例如下:
|
||||
```typescript
|
||||
const table = useTable({
|
||||
common: {
|
||||
items: [
|
||||
{
|
||||
field: 'username',
|
||||
label: '用户名称',
|
||||
type: 'input',
|
||||
required: true,
|
||||
}
|
||||
]
|
||||
},
|
||||
search: {
|
||||
items: [
|
||||
{
|
||||
extend: 'usernam',
|
||||
required: false,
|
||||
}
|
||||
]
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
### 新增弹窗
|
||||
`create`为新增表单弹窗的参数,即`useFormModal`对应的参数。参阅。当指定该参数时,会在表格左上添加新建按钮,如需自定义按钮样式或自定义渲染,可通过`create.trigger`参数配置。
|
||||
|
||||
### 修改弹窗
|
||||
`modify`为新增表单弹窗的参数,即`useFormModal`对应的参数。参阅。当指定该参数时,会在表格行添加修改按钮。
|
||||
|
||||
### 表格参数
|
||||
`tableProps`为传递给`Table`组件的额外参数,其中部分参数不可用,如`data`和`columns`等。此外,部分参数有默认值,具体参数可查看`src/components/table/table.config.ts`文件。
|
||||
|
||||
### 插槽
|
||||
- `Table`组件的插槽可正常使用
|
||||
- `action`插槽用作表格左上方的操作区。
|
||||
|
||||
## 问题
|
||||
- 问题:日期范围框值为数组,处理不方便
|
||||
- 解决:字段名使用`v1:v2`格式,提交时会生成`{ v1: '00:00:01', v2: '00:00:02' }`数据
|
||||
- 问题:搜索表单、新增表单和修改表单通常用到同一表单项,如何避免重复定义
|
||||
- 解决:表单项使用`{ extends: <field-name> }`会在`common.items`中寻找相同的项,并合并值。
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
export * from "./table";
|
||||
export * from "./use-table";
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
import { Button } from "@arco-design/web-vue";
|
||||
import { IconRefresh, IconSearch } from "@arco-design/web-vue/es/icon";
|
||||
|
||||
/**
|
||||
* 搜索表单默认参数
|
||||
*/
|
||||
export const TABLE_SEARCH_DEFAULTS = {
|
||||
labelAlign: "left",
|
||||
autoLabelWidth: true,
|
||||
model: {},
|
||||
};
|
||||
|
||||
/**
|
||||
* 表格列默认参数
|
||||
*/
|
||||
export const TABLE_COLUMN_DEFAULTS = {
|
||||
ellipsis: true,
|
||||
tooltip: true,
|
||||
render: ({ record, column }: any) => record[column.dataIndex] || "-",
|
||||
};
|
||||
|
||||
/**
|
||||
* 行操作按钮默认参数
|
||||
*/
|
||||
export const TABLE_ACTION_DEFAULTS = {
|
||||
buttonProps: {
|
||||
type: "primary",
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* 删除弹窗默认参数
|
||||
*/
|
||||
export const TABLE_DELTE_DEFAULTS = {
|
||||
title: "删除确认",
|
||||
content: "确认删除当前数据吗?",
|
||||
modalClass: "text-center",
|
||||
hideCancel: false,
|
||||
maskClosable: false,
|
||||
};
|
||||
|
||||
export const TALBE_INDEX_DEFAULTS = {
|
||||
title: "#",
|
||||
width: 60,
|
||||
align: "center",
|
||||
render: ({ rowIndex }: any) => rowIndex + 1,
|
||||
};
|
||||
|
||||
export const searchItem = {
|
||||
field: "id",
|
||||
type: "custom",
|
||||
itemProps: {
|
||||
class: "table-search-item col-start-4 !mr-0 grid grid-cols-[0_1fr]",
|
||||
hideLabel: true,
|
||||
},
|
||||
component: () => {
|
||||
const tableRef = inject<any>("ref:table");
|
||||
return (
|
||||
<div class="w-full flex gap-x-2 justify-end">
|
||||
{(tableRef.search?.items?.length || 0) > 3 && (
|
||||
<Button disabled={tableRef?.loading.value} onClick={() => tableRef?.reloadData()}>
|
||||
{{ icon: () => <IconRefresh></IconRefresh>, default: () => "重置" }}
|
||||
</Button>
|
||||
)}
|
||||
<Button type="primary" loading={tableRef?.loading.value} onClick={() => tableRef?.loadData()}>
|
||||
{{ icon: () => <IconSearch></IconSearch>, default: () => "查询" }}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
|
@ -0,0 +1,241 @@
|
|||
import {
|
||||
TableColumnData as BaseColumn,
|
||||
TableData as BaseData,
|
||||
Table as BaseTable,
|
||||
Divider,
|
||||
} from "@arco-design/web-vue";
|
||||
import { PropType, computed, defineComponent, reactive, ref, watch } from "vue";
|
||||
import { Form, FormInstance, FormModal, FormModalInstance, FormModalProps, FormProps } from "../form";
|
||||
|
||||
/**
|
||||
* 表格组件
|
||||
* @see src/components/table/table.tsx
|
||||
*/
|
||||
export const Table = defineComponent({
|
||||
name: "Table",
|
||||
props: {
|
||||
/**
|
||||
* 表格数据
|
||||
*/
|
||||
data: {
|
||||
type: [Array, Function] as PropType<
|
||||
BaseData[] | ((search: Record<string, any>, paging: { page: number; size: number }) => Promise<any>)
|
||||
>,
|
||||
},
|
||||
/**
|
||||
* 表格列设置
|
||||
*/
|
||||
columns: {
|
||||
type: Array as PropType<BaseColumn[]>,
|
||||
default: () => [],
|
||||
},
|
||||
/**
|
||||
* 分页参数配置
|
||||
*/
|
||||
pagination: {
|
||||
type: Object as PropType<any>,
|
||||
default: () => reactive({ current: 1, pageSize: 10, total: 300, showTotal: true }),
|
||||
},
|
||||
/**
|
||||
* 搜索表单配置
|
||||
*/
|
||||
search: {
|
||||
type: Object as PropType<FormProps>,
|
||||
},
|
||||
/**
|
||||
* 新建弹窗配置
|
||||
*/
|
||||
create: {
|
||||
type: Object as PropType<FormModalProps>,
|
||||
},
|
||||
/**
|
||||
* 修改弹窗配置
|
||||
*/
|
||||
modify: {
|
||||
type: Object as PropType<FormModalProps>,
|
||||
},
|
||||
/**
|
||||
* 详情弹窗配置
|
||||
*/
|
||||
detail: {
|
||||
type: Object as PropType<any>,
|
||||
},
|
||||
/**
|
||||
* 传递给 Table 组件的属性
|
||||
*/
|
||||
tableProps: {
|
||||
type: Object as PropType<InstanceType<typeof BaseTable>["$props"]>,
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const loading = ref(false);
|
||||
const searchRef = ref<FormInstance>();
|
||||
const createRef = ref<FormModalInstance>();
|
||||
const modifyRef = ref<FormModalInstance>();
|
||||
const renderData = ref<BaseData[]>([]);
|
||||
const inlineSearch = computed(() => (props.search?.items?.length || 0) < 4);
|
||||
|
||||
Object.assign(props.columns, { getInstance: () => getCurrentInstance() });
|
||||
|
||||
const getPaging = (pagination: Partial<any>) => {
|
||||
const { current: page, pageSize: size } = { ...props.pagination, ...pagination } as any;
|
||||
return { page, size };
|
||||
};
|
||||
|
||||
const loadData = async (pagination: Partial<any> = {}) => {
|
||||
if (!props.data) {
|
||||
return;
|
||||
}
|
||||
if (Array.isArray(props.data)) {
|
||||
if (!props.search?.model) {
|
||||
return;
|
||||
}
|
||||
const filters = Object.entries(props.search?.model || {});
|
||||
const data = props.data?.filter((item) => {
|
||||
return filters.every(([key, value]) => {
|
||||
if (typeof value === "string") {
|
||||
return item[key].includes(value);
|
||||
}
|
||||
return item[key] === value;
|
||||
});
|
||||
});
|
||||
renderData.value = data || [];
|
||||
props.pagination.total = renderData.value.length;
|
||||
props.pagination.current = 1;
|
||||
return;
|
||||
}
|
||||
if (typeof props.data !== "function") {
|
||||
return;
|
||||
}
|
||||
const model = searchRef.value?.getModel() || {};
|
||||
const paging = getPaging(pagination);
|
||||
try {
|
||||
loading.value = true;
|
||||
const resData = await props.data(model, paging);
|
||||
const { data = [], meta = {} } = resData || {};
|
||||
const { page: pageNum, total } = meta;
|
||||
renderData.value = data;
|
||||
Object.assign(props.pagination, { current: pageNum, total });
|
||||
} catch (error) {
|
||||
console.log("table error", error);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const reloadData = () => {
|
||||
loadData({ current: 1, pageSize: 10 });
|
||||
};
|
||||
|
||||
const openModifyModal = (data: any) => {
|
||||
modifyRef.value?.open(data.record);
|
||||
};
|
||||
|
||||
const onPageChange = (current: number) => {
|
||||
loadData({ current });
|
||||
};
|
||||
|
||||
const onCreateOk = () => {
|
||||
reloadData();
|
||||
};
|
||||
|
||||
const onModifyOk = () => {
|
||||
reloadData();
|
||||
};
|
||||
|
||||
watch(
|
||||
() => props.data,
|
||||
(data) => {
|
||||
if (!Array.isArray(data)) {
|
||||
return;
|
||||
}
|
||||
renderData.value = data;
|
||||
props.pagination.total = data.length;
|
||||
props.pagination.current = 1;
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
}
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
loadData();
|
||||
});
|
||||
|
||||
const state = {
|
||||
loading,
|
||||
searchRef,
|
||||
createRef,
|
||||
modifyRef,
|
||||
renderData,
|
||||
inlineSearch,
|
||||
loadData,
|
||||
reloadData,
|
||||
openModifyModal,
|
||||
onPageChange,
|
||||
onCreateOk,
|
||||
onModifyOk,
|
||||
};
|
||||
|
||||
provide("ref:table", { ...state, ...props });
|
||||
|
||||
return state;
|
||||
},
|
||||
render() {
|
||||
(this.columns as any).instance = this;
|
||||
return (
|
||||
<div class="bh-table w-full">
|
||||
{!this.inlineSearch && (
|
||||
<div class="">
|
||||
<Form ref={(el: any) => (this.searchRef = el)} class="grid grid-cols-4 gap-x-4" {...this.search}></Form>
|
||||
</div>
|
||||
)}
|
||||
{!this.inlineSearch && <Divider class="mt-0 border-gray-200" />}
|
||||
<div class={`mb-2 flex justify-between ${!this.inlineSearch && "mt-2"}`}>
|
||||
<div class="flex-1 flex gap-2">
|
||||
{this.create && (
|
||||
<FormModal
|
||||
ref={(el: any) => (this.createRef = el)}
|
||||
onOk={this.onCreateOk}
|
||||
{...(this.create as any)}
|
||||
></FormModal>
|
||||
)}
|
||||
{this.modify && (
|
||||
<FormModal
|
||||
ref={(el: any) => (this.modifyRef = el)}
|
||||
onOk={this.onModifyOk}
|
||||
trigger={false}
|
||||
{...(this.modify as any)}
|
||||
></FormModal>
|
||||
)}
|
||||
{this.$slots.action?.()}
|
||||
</div>
|
||||
<div>
|
||||
{this.inlineSearch && (
|
||||
<Form
|
||||
ref={(el: any) => (this.searchRef = el)}
|
||||
{...{ ...this.search, formProps: { layout: "inline" } }}
|
||||
></Form>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<BaseTable
|
||||
row-key="id"
|
||||
bordered={false}
|
||||
{...this.tableProps}
|
||||
loading={this.loading}
|
||||
pagination={this.pagination}
|
||||
data={this.renderData}
|
||||
columns={this.columns}
|
||||
onPageChange={this.onPageChange}
|
||||
></BaseTable>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
export type TableInstance = InstanceType<typeof Table>;
|
||||
|
||||
export type TableProps = TableInstance["$props"];
|
||||
|
|
@ -0,0 +1,116 @@
|
|||
import { Link, TableColumnData, TableData } from "@arco-design/web-vue";
|
||||
import { FormModalProps, FormProps } from "../form";
|
||||
import { IFormItem } from "../form/form-item";
|
||||
import { TableProps } from "./table";
|
||||
|
||||
interface UseColumnRenderOptions {
|
||||
/**
|
||||
* 当前行数据
|
||||
*/
|
||||
record: TableData;
|
||||
/**
|
||||
* 当前列配置
|
||||
*/
|
||||
column: TableColumnData;
|
||||
/**
|
||||
* 当前行索引
|
||||
*/
|
||||
rowIndex: number;
|
||||
}
|
||||
|
||||
export interface TableColumnButton {
|
||||
/**
|
||||
* button text
|
||||
*/
|
||||
text?: string;
|
||||
|
||||
/**
|
||||
* button type
|
||||
*/
|
||||
type?: "delete" | "modify";
|
||||
|
||||
/**
|
||||
* onClick callback
|
||||
*/
|
||||
onClick?: (data: UseColumnRenderOptions, openModify?: (model: Record<string, any>) => void) => void;
|
||||
|
||||
/**
|
||||
* disable button dynamicly
|
||||
*/
|
||||
disabled?: (data: UseColumnRenderOptions) => boolean;
|
||||
|
||||
/**
|
||||
* show or hide button dynamicly
|
||||
*/
|
||||
visible?: (data: UseColumnRenderOptions) => boolean;
|
||||
|
||||
/**
|
||||
* props for `Button`
|
||||
*/
|
||||
buttonProps?: Partial<Omit<InstanceType<typeof Link>["$props"], "onClick" | "disabled">>;
|
||||
}
|
||||
|
||||
export interface UseTableColumn extends TableColumnData {
|
||||
/**
|
||||
* column type
|
||||
*/
|
||||
type?: "index" | "button";
|
||||
|
||||
/**
|
||||
* only for `type: "button"`
|
||||
*/
|
||||
buttons?: TableColumnButton[];
|
||||
}
|
||||
|
||||
type ExtendableFormItem = (
|
||||
| string
|
||||
| ({
|
||||
/**
|
||||
* 继承common.items中指定field值的项
|
||||
*/
|
||||
extend: string;
|
||||
} & Partial<IFormItem>)
|
||||
| IFormItem
|
||||
)[];
|
||||
|
||||
export interface UseTableOptions extends Omit<TableProps, "search" | "create" | "modify" | "columns"> {
|
||||
/**
|
||||
* columns config, extends from `TableColumnData`
|
||||
* @see https://arco.design/web-vue/components/table/#tablecolumn
|
||||
*/
|
||||
columns: UseTableColumn[];
|
||||
/**
|
||||
* search form config
|
||||
* @see FormProps
|
||||
*/
|
||||
search?: Partial<{
|
||||
[k in keyof FormProps]: k extends "items" ? ExtendableFormItem : FormProps[k];
|
||||
}>;
|
||||
/**
|
||||
* common props for `create` and `modify` modal
|
||||
* @see FormModalProps
|
||||
*/
|
||||
common?: Partial<FormModalProps>;
|
||||
/**
|
||||
* 新建弹窗配置
|
||||
*/
|
||||
create?: Partial<
|
||||
{
|
||||
[k in keyof FormModalProps]: k extends "items"
|
||||
? (string | (IFormItem & { extend: string }))[]
|
||||
: FormModalProps[k];
|
||||
} & { extend: boolean }
|
||||
>;
|
||||
/**
|
||||
* 新建弹窗配置
|
||||
*/
|
||||
modify?: Partial<
|
||||
{ [k in keyof FormModalProps]: k extends "items" ? (string | IFormItem)[] : FormModalProps[k] } & {
|
||||
extend: boolean;
|
||||
}
|
||||
>;
|
||||
/**
|
||||
* 详情弹窗配置
|
||||
*/
|
||||
detail?: any;
|
||||
}
|
||||
|
|
@ -0,0 +1,144 @@
|
|||
import { Link, Message, Modal, TableColumnData } from "@arco-design/web-vue";
|
||||
import { defaultsDeep, isArray, isFunction, mergeWith, omit } from "lodash-es";
|
||||
import { reactive } from "vue";
|
||||
import { TableInstance } from "./table";
|
||||
import {
|
||||
TABLE_ACTION_DEFAULTS,
|
||||
TABLE_COLUMN_DEFAULTS,
|
||||
TABLE_DELTE_DEFAULTS,
|
||||
TALBE_INDEX_DEFAULTS,
|
||||
searchItem,
|
||||
} from "./table.config";
|
||||
import { UseTableOptions } from "./use-interface";
|
||||
|
||||
const merge = (...args: any[]) => {
|
||||
return mergeWith({}, ...args, (obj: any, src: any) => {
|
||||
if (Array.isArray(obj) && Array.isArray(src)) {
|
||||
return obj.concat(src);
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
};
|
||||
|
||||
const has = (obj: any, key: string) => Object.prototype.hasOwnProperty.call(obj, key);
|
||||
|
||||
const propTruly = (obj: any, key: string) => !has(obj, key) || !!obj[key];
|
||||
|
||||
/**
|
||||
* 提供便捷语法,构建传给Table组件的参数
|
||||
* @see src/components/table/use-table.tsx
|
||||
*/
|
||||
export const useTable = (optionsOrFn: UseTableOptions | (() => UseTableOptions)): any => {
|
||||
const options: UseTableOptions = typeof optionsOrFn === "function" ? optionsOrFn() : optionsOrFn;
|
||||
const columns: TableColumnData[] = [];
|
||||
|
||||
const getTable = (): TableInstance => (columns as any).instance;
|
||||
|
||||
options.columns.forEach((column) => {
|
||||
// 序号
|
||||
if (column.type === "index") {
|
||||
defaultsDeep(column, TALBE_INDEX_DEFAULTS);
|
||||
}
|
||||
|
||||
// 操作
|
||||
if (column.type === "button" && isArray(column.buttons)) {
|
||||
if (options.detail) {
|
||||
column.buttons.unshift({ text: "详情", onClick: (data) => {} });
|
||||
}
|
||||
|
||||
if (options.modify) {
|
||||
const modifyAction = column.buttons.find((i) => i.type === "modify");
|
||||
if (modifyAction) {
|
||||
const { onClick } = modifyAction;
|
||||
modifyAction.onClick = (columnData) => {
|
||||
const fn = (data: Record<string, any>) => getTable()?.openModifyModal(data);
|
||||
if (isFunction(onClick)) {
|
||||
onClick(columnData, fn);
|
||||
} else {
|
||||
fn(columnData);
|
||||
}
|
||||
};
|
||||
} else {
|
||||
column.buttons.unshift({
|
||||
text: "修改",
|
||||
onClick: (data) => getTable()?.openModifyModal(data),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
column.buttons = column.buttons?.map((action) => {
|
||||
let onClick = action?.onClick;
|
||||
|
||||
if (action.type === "delete") {
|
||||
onClick = (data) => {
|
||||
Modal.warning({
|
||||
...TABLE_DELTE_DEFAULTS,
|
||||
onOk: async () => {
|
||||
const resData: any = await action?.onClick?.(data);
|
||||
resData.msg && Message.success(resData?.msg || "");
|
||||
getTable()?.loadData();
|
||||
},
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
return { ...TABLE_ACTION_DEFAULTS, ...action, onClick } as any;
|
||||
});
|
||||
|
||||
column.render = (columnData) =>
|
||||
column.buttons?.map((action) => {
|
||||
const onClick = () => action.onClick?.(columnData);
|
||||
const omitKeys = ["text", "render", "api", "action", "onClick", "disabled"];
|
||||
const disabled = () => action.disabled?.(columnData);
|
||||
if (action.visible && !action.visible(columnData)) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<Link onClick={onClick} disabled={disabled()} {...omit(action as any, omitKeys)}>
|
||||
{action.text}
|
||||
</Link>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
columns.push({ ...TABLE_COLUMN_DEFAULTS, ...column });
|
||||
});
|
||||
|
||||
const itemsMap = options.common?.items?.reduce((map, item) => {
|
||||
map[item.field] = item;
|
||||
return map;
|
||||
}, {} as any);
|
||||
|
||||
/**
|
||||
* 搜索表单的处理
|
||||
*/
|
||||
if (options.search && options.search.items) {
|
||||
const searchItems: any[] = [];
|
||||
options.search.items.forEach((item) => {
|
||||
if (typeof item === "string") {
|
||||
if (!itemsMap[item]) {
|
||||
throw new Error(`search item ${item} not found in common items`);
|
||||
}
|
||||
searchItems.push(itemsMap[item]);
|
||||
return;
|
||||
}
|
||||
if ("extend" in item && item.extend && itemsMap[item.extend]) {
|
||||
searchItems.push(merge({}, itemsMap[item.extend], item));
|
||||
return;
|
||||
}
|
||||
searchItems.push(item);
|
||||
});
|
||||
searchItems.push(searchItem);
|
||||
options.search.items = searchItems;
|
||||
}
|
||||
|
||||
if (options.create && propTruly(options.create, "extend")) {
|
||||
options.create = merge(options.common, options.create);
|
||||
}
|
||||
|
||||
if (options.modify && propTruly(options.modify, "extend")) {
|
||||
options.modify = merge(options.common, options.modify);
|
||||
}
|
||||
|
||||
return reactive({ ...options, columns });
|
||||
};
|
||||
|
|
@ -0,0 +1 @@
|
|||
export * from "./toast";
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
import { createVNode, render } from "vue";
|
||||
import Toast from "./toast.vue";
|
||||
|
||||
export interface IToastOptions {
|
||||
/**
|
||||
* 展示内容
|
||||
* @default '正在操作中,请稍等...'
|
||||
*/
|
||||
message?: string;
|
||||
/**
|
||||
* 图标
|
||||
* @default 'icon-park-outline-loading-one'
|
||||
*/
|
||||
icon?: string;
|
||||
/**
|
||||
* 是否显示遮罩层
|
||||
* @default true
|
||||
*/
|
||||
mask?: boolean;
|
||||
/**
|
||||
* 是否覆盖窗口(即不允许其他操作)
|
||||
* @default false
|
||||
*/
|
||||
cover?: boolean;
|
||||
}
|
||||
|
||||
export const toast = (messageOrOptions?: string | IToastOptions) => {
|
||||
if (typeof messageOrOptions === "string") {
|
||||
messageOrOptions = {
|
||||
message: messageOrOptions,
|
||||
};
|
||||
}
|
||||
const container = document.createElement("div");
|
||||
const vnode = createVNode(Toast, messageOrOptions as any);
|
||||
render(vnode, container);
|
||||
document.body.appendChild(container);
|
||||
const close = () => {
|
||||
render(null, container);
|
||||
document.body.removeChild(container);
|
||||
};
|
||||
return close;
|
||||
};
|
||||
|
|
@ -0,0 +1,80 @@
|
|||
<template>
|
||||
<div class="toast">
|
||||
<div class="toast-content">
|
||||
<i :class="[icon, iconRotate && 'rotate']"></i>
|
||||
<span>{{ message }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
defineOptions({
|
||||
name: "toast",
|
||||
});
|
||||
|
||||
const props = defineProps({
|
||||
message: {
|
||||
type: String,
|
||||
default: "正在操作中,请稍等...",
|
||||
},
|
||||
icon: {
|
||||
type: String,
|
||||
default: "icon-park-outline-loading-one",
|
||||
},
|
||||
iconRotate: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
mask: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
cover: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
});
|
||||
|
||||
const style = computed(() => {
|
||||
return {
|
||||
pointerEvents: props.cover ? "initial" : "none",
|
||||
backgroundColor: props.mask ? "rgba(0, 0, 0, 0.2)" : "transparent",
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.toast {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
z-index: 9999;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
pointer-events: v-bind("style.pointerEvents");
|
||||
background-color: v-bind("style.backgroundColor");
|
||||
}
|
||||
.toast-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 10px 20px;
|
||||
border-radius: 3px;
|
||||
background-color: rgba(0, 0, 0, 0.8);
|
||||
color: #fff;
|
||||
}
|
||||
.rotate {
|
||||
animation: rotate 2s linear infinite;
|
||||
}
|
||||
@keyframes rotate {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,127 @@
|
|||
type ArrayMap<
|
||||
A extends readonly Item[],
|
||||
K extends keyof A[number],
|
||||
B extends string[] = []
|
||||
> = A["length"] extends B["length"] ? B : ArrayMap<A, K, [...B, A[B["length"]][K]]>;
|
||||
|
||||
type ArrayFind<A extends readonly Item[], V extends A[number]["value"]> = A extends readonly [
|
||||
infer I extends Item,
|
||||
...infer R extends Item[]
|
||||
]
|
||||
? I["value"] extends V
|
||||
? I["label"]
|
||||
: ArrayFind<R, V>
|
||||
: "nev1er";
|
||||
|
||||
type MergeIntersection<A> = A extends infer T ? { [Key in keyof T]: T[Key] } : never;
|
||||
|
||||
interface Item {
|
||||
label: string;
|
||||
value: any;
|
||||
enumKey: string;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
type ConstantType<T extends readonly Item[]> = MergeIntersection<
|
||||
{
|
||||
/**
|
||||
* 枚举值
|
||||
*/
|
||||
[K in T[number] as K["enumKey"]]: K["value"];
|
||||
} & {
|
||||
/**
|
||||
* 原始字典项
|
||||
*/
|
||||
items: T;
|
||||
/**
|
||||
* 字典项映射
|
||||
*/
|
||||
map: {
|
||||
[k in T[number] as k["value"]]: k;
|
||||
};
|
||||
/**
|
||||
* 所有值组成的数组
|
||||
*/
|
||||
values: ArrayMap<T, "value">;
|
||||
/**
|
||||
* 获取指定值的项
|
||||
*/
|
||||
pick(...values: T[number]["value"][]): Item[];
|
||||
/**
|
||||
* 排除指定值的项
|
||||
*/
|
||||
omit(...values: T[number]["value"][]): Item[];
|
||||
/**
|
||||
* 返回数组,由指定属性的值组成
|
||||
*/
|
||||
each<K extends keyof T[number]>(key: K): T[number][K][];
|
||||
/**
|
||||
* 格式化
|
||||
* @param value value值
|
||||
* @param key 指定返回的属性,默认为label
|
||||
*/
|
||||
format<K extends T[number]["value"]>(value: K, key?: keyof T[number]): ArrayFind<T, K>;
|
||||
}
|
||||
>;
|
||||
|
||||
/**
|
||||
* 提供公共方法
|
||||
*/
|
||||
class Constanter {
|
||||
// @ts-ignore
|
||||
items: Item[];
|
||||
pick(...values: any[]) {
|
||||
return this.items.filter((item) => values.includes(item.value));
|
||||
}
|
||||
omit(...values: any[]) {
|
||||
return this.items.filter((item) => !values.includes(item.value));
|
||||
}
|
||||
each(key: string) {
|
||||
return this.items.map((item) => item[key]);
|
||||
}
|
||||
format(value: any, key: string = "label") {
|
||||
return this.items.find((item) => item.value === value)?.[key];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 定义字典常量
|
||||
*/
|
||||
export function defineConstants<T extends readonly Item[]>(items: T): ConstantType<T> {
|
||||
const constants: any = {
|
||||
items,
|
||||
map: {},
|
||||
values: [],
|
||||
};
|
||||
for (const item of items) {
|
||||
constants[item.enumKey] = item.value;
|
||||
constants.map[item.value] = item;
|
||||
constants.values.push(item.value);
|
||||
}
|
||||
return Object.setPrototypeOf(constants, Constanter.prototype);
|
||||
}
|
||||
|
||||
// const media = defineConstants([
|
||||
// {
|
||||
// label: "视频",
|
||||
// value: 1,
|
||||
// enumKey: "VIDEO",
|
||||
// },
|
||||
// {
|
||||
// label: "图片",
|
||||
// value: 2,
|
||||
// enumKey: "IMAGE",
|
||||
// },
|
||||
// {
|
||||
// label: "文本",
|
||||
// value: 3,
|
||||
// enumKey: "TEXT",
|
||||
// },
|
||||
// ] as const);
|
||||
|
||||
// console.log("media", media, media.VIDEO, media.IMAGE, media.TEXT);
|
||||
// console.log("media pick", media.pick(media.VIDEO));
|
||||
// console.log("media omit", media.omit(media.TEXT));
|
||||
// console.log("media each", media.each("label"));
|
||||
// console.log("media format", media.format(2));
|
||||
// console.log("media maps", media.map);
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
import { createApp } from "vue";
|
||||
import App from "./App.vue";
|
||||
import { router } from "./router";
|
||||
import { store } from "./store";
|
||||
import { style } from "./style";
|
||||
|
||||
const run = async () => {
|
||||
const app = createApp(App);
|
||||
app.use(store);
|
||||
app.use(style);
|
||||
app.use(router);
|
||||
await router.isReady();
|
||||
app.mount("#app");
|
||||
};
|
||||
|
||||
run();
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
<template>
|
||||
<div class="w-full h-full flex justify-center items-center p-4">
|
||||
<div class="flex flex-col items-center">
|
||||
<div>
|
||||
<img src="@/assets/404.svg" alt="404" class="w-[320px]" />
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<!-- <h2 class="text-3xl m-0 font-bold">页面不存在</h2> -->
|
||||
<p class="mt-0">抱歉,页面未找到,你访问的地址不存在!</p>
|
||||
<div class="space-x-3 mt-5">
|
||||
<a-button type="primary" @click="router.back()">
|
||||
<template #icon>
|
||||
<i class="icon-park-outline-back"></i>
|
||||
</template>
|
||||
返回上页
|
||||
</a-button>
|
||||
<a-button type="outline" @click="router.push('/')">
|
||||
<template #icon>
|
||||
<i class="icon-park-outline-home"></i>
|
||||
</template>
|
||||
返回首页
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useRouter } from "vue-router";
|
||||
const router = useRouter();
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
|
||||
<route lang="json">
|
||||
{
|
||||
"meta": {
|
||||
"title": "404",
|
||||
"icon": "icon-park-outline-home"
|
||||
}
|
||||
}
|
||||
</route>
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
<script lang="tsx">
|
||||
import { MenuItem, menus } from "@/router";
|
||||
|
||||
export default defineComponent({
|
||||
name: "LayoutMenu",
|
||||
setup() {
|
||||
const selectedKeys = ref<string[]>([]);
|
||||
const route = useRoute();
|
||||
|
||||
watch(
|
||||
() => route.path,
|
||||
() => {
|
||||
selectedKeys.value = route.matched.map((i) => i.path);
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
return { selectedKeys };
|
||||
},
|
||||
methods: {
|
||||
goto(route: MenuItem) {
|
||||
if (route.external) {
|
||||
window.open(route.path, "_blank");
|
||||
return;
|
||||
}
|
||||
this.$router.push(route);
|
||||
},
|
||||
|
||||
renderItem(routes: MenuItem[], isTop = false) {
|
||||
return routes.map((route) => {
|
||||
const icon = route.icon && isTop ? () => <i class={route.icon} /> : null;
|
||||
const node = route.children?.length ? (
|
||||
<a-sub-menu key={route.path} v-slots={{ icon, title: () => route.title }}>
|
||||
{this.renderItem(route?.children)}
|
||||
</a-sub-menu>
|
||||
) : (
|
||||
<a-menu-item key={route.path} v-slots={{ icon }} onClick={() => this.goto(route)}>
|
||||
{route.title}
|
||||
</a-menu-item>
|
||||
);
|
||||
|
||||
return node;
|
||||
});
|
||||
},
|
||||
},
|
||||
|
||||
render() {
|
||||
return (
|
||||
<a-menu
|
||||
style={{ width: "100%", height: "100%" }}
|
||||
breakpoint="xl"
|
||||
selectedKeys={this.selectedKeys}
|
||||
autoOpenSelected={true}
|
||||
>
|
||||
{this.renderItem(menus, true)}
|
||||
</a-menu>
|
||||
);
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
|
@ -0,0 +1,202 @@
|
|||
<template>
|
||||
<a-layout class="layout">
|
||||
<a-layout-header
|
||||
class="h-13 overflow-hidden flex justify-between items-center gap-4 px-4 border-b border-slate-200 bg-white dark:bg-slate-800 dark:border-slate-700"
|
||||
>
|
||||
<div class="h-13 flex items-center border-b border-slate-200 dark:border-slate-800">
|
||||
<router-link to="/" class="ml-1 flex items-center gap-2 text-slate-700">
|
||||
<img src="/favicon.ico" alt="" width="20" height="20" />
|
||||
<h1 class="text-lg leading-[19px] dark:text-white m-0 p-0">
|
||||
{{ appStore.title }}
|
||||
</h1>
|
||||
</router-link>
|
||||
</div>
|
||||
<div class="flex items-center gap-4">
|
||||
<a-tooltip v-for="btn in buttons" :key="btn.icon" :content="btn.tooltip">
|
||||
<a-button shape="round" @click="btn.onClick">
|
||||
<template #icon>
|
||||
<i :class="btn.icon"></i>
|
||||
</template>
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
<a-dropdown>
|
||||
<span class="cursor-pointer">
|
||||
<a-avatar :size="28">
|
||||
<img :src="userStore.avatar" :alt="userStore.name">
|
||||
</a-avatar>
|
||||
<span class="mx-2">
|
||||
{{ userStore.name }}
|
||||
</span>
|
||||
<i class="icon-park-outline-down"></i>
|
||||
</span>
|
||||
<template #content>
|
||||
<a-doption v-for="item in userButtons" :key="item.text" @click="item.onClick">
|
||||
<template #icon>
|
||||
<i :class="item.icon"></i>
|
||||
</template>
|
||||
{{ item.text }}
|
||||
</a-doption>
|
||||
</template>
|
||||
</a-dropdown>
|
||||
<a-drawer v-model:visible="themeConfig.visible" title="主题设置" :width="280"></a-drawer>
|
||||
</div>
|
||||
</a-layout-header>
|
||||
|
||||
<a-layout class="flex flex-1 overflow-hidden">
|
||||
<a-layout-sider
|
||||
class="h-full overflow-hidden dark:bg-slate-800 border-r border-slate-200 dark:border-slate-700"
|
||||
:width="208"
|
||||
:collapsed-width="52"
|
||||
:collapsible="true"
|
||||
:collapsed="isCollapsed"
|
||||
:hide-trigger="false"
|
||||
@collapse="onCollapse"
|
||||
>
|
||||
<div class="">
|
||||
<Menu />
|
||||
</div>
|
||||
</a-layout-sider>
|
||||
<a-layout class="layout-content flex-1">
|
||||
<a-layout-header class="h-8 bg-white border-b border-slate-200 dark:bg-slate-800 dark:border-slate-700">
|
||||
<div class="h-full flex items-center gap-2 px-4">
|
||||
<a-tag class="cursor-pointer">首页</a-tag>
|
||||
</div>
|
||||
</a-layout-header>
|
||||
<a-layout-content class="overflow-x-auto">
|
||||
<router-view v-slot="{ Component }">
|
||||
<component :is="Component"></component>
|
||||
</router-view>
|
||||
</a-layout-content>
|
||||
</a-layout>
|
||||
</a-layout>
|
||||
</a-layout>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useAppStore, useUserStore } from "@/store";
|
||||
import { Message } from "@arco-design/web-vue";
|
||||
import Menu from "./components/menu.vue";
|
||||
|
||||
const appStore = useAppStore();
|
||||
const userStore = useUserStore();
|
||||
const isCollapsed = ref(false);
|
||||
const router = useRouter();
|
||||
const themeConfig = ref({ visible: false });
|
||||
const onCollapse = (val: boolean) => {
|
||||
isCollapsed.value = val;
|
||||
};
|
||||
|
||||
const buttons = [
|
||||
{
|
||||
icon: "icon-park-outline-moon",
|
||||
tooltip: "点击切换主题色",
|
||||
onClick: () => {
|
||||
appStore.toggleDark();
|
||||
},
|
||||
},
|
||||
{
|
||||
icon: "icon-park-outline-config",
|
||||
tooltip: "点击打开设置",
|
||||
onClick: () => {
|
||||
themeConfig.value.visible = true;
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const userButtons = [
|
||||
{
|
||||
icon: "icon-park-outline-config",
|
||||
text: "个人设置",
|
||||
onClick: () => {
|
||||
console.log("个人设置");
|
||||
},
|
||||
},
|
||||
{
|
||||
icon: "icon-park-outline-logout",
|
||||
text: "退出登录",
|
||||
onClick: async () => {
|
||||
Message.loading({
|
||||
content: '提示: 正在退出,请稍后...',
|
||||
duration: 2000,
|
||||
onClose: () => {
|
||||
Message.success(`提示: 已成功退出登录!`)
|
||||
router.push({ name: "_login" });
|
||||
}
|
||||
})
|
||||
},
|
||||
},
|
||||
];
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
@nav-size-height: 60px;
|
||||
@layout-max-width: 1100px;
|
||||
|
||||
.layout {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.layout-navbar {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 100;
|
||||
width: 100%;
|
||||
height: @nav-size-height;
|
||||
}
|
||||
|
||||
.layout-sider {
|
||||
z-index: 99;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
transition: all 0.2s cubic-bezier(0.34, 0.69, 0.1, 1);
|
||||
|
||||
> :deep(.arco-layout-sider-children) {
|
||||
overflow-y: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.menu-wrapper {
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
overflow-x: hidden;
|
||||
|
||||
:deep(.arco-menu) {
|
||||
::-webkit-scrollbar {
|
||||
width: 12px;
|
||||
height: 4px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
border: 4px solid transparent;
|
||||
background-clip: padding-box;
|
||||
border-radius: 7px;
|
||||
background-color: var(--color-text-4);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background-color: var(--color-text-3);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.layout-content {
|
||||
min-height: 100vh;
|
||||
overflow-y: hidden;
|
||||
background-color: var(--color-fill-2);
|
||||
transition: padding 0.2s cubic-bezier(0.34, 0.69, 0.1, 1);
|
||||
}
|
||||
</style>
|
||||
|
||||
<route lang="json">
|
||||
{
|
||||
"meta": {
|
||||
"sort": 101,
|
||||
"title": "首页",
|
||||
"icon": "icon-park-outline-home"
|
||||
}
|
||||
}
|
||||
</route>
|
||||
|
|
@ -0,0 +1,102 @@
|
|||
<template>
|
||||
<div class="page-login w-full h-full flex items-center justify-center bg-white">
|
||||
<div class="fixed flex items-center justify-between top-0 m-0 h-13 w-full px-10 z-10">
|
||||
<!-- <div class="flex items-center">
|
||||
<img src="/favicon.ico" alt="" width="20" height="20" class="mr-1" />
|
||||
<h1 class="text-lg m-0">
|
||||
{{ appStore.title }}
|
||||
<span class="mx-1 text-slate-500">|</span>
|
||||
<span class="text-slate-500 font-normal text-sm">{{ appStore.subtitle }}</span>
|
||||
</h1>
|
||||
</div>
|
||||
<div>敬请期待</div> -->
|
||||
</div>
|
||||
<div
|
||||
class="login-box relative mx-6 grid md:grid-cols-[1fr_500px] rounded overflow-hidden w-[1020px] h-[600px] border border-blue-100"
|
||||
>
|
||||
<div class="relative hidden md:block w-full h-full overflow-hidden bg-[#09f] px-4">
|
||||
<img src="@/assets/td.svg" :alt="appStore.title" class="w-full h-full select-none" />
|
||||
</div>
|
||||
<div class="relative p-20 px-14 bg-white shadow-sm">
|
||||
<div class="text-2xl">欢迎登陆</div>
|
||||
<div class="text-base text-gray-500 mt-3">{{ meridiem }}好,欢迎登陆{{ appStore.title }}!</div>
|
||||
<a-form ref="loginForm" :model="model" layout="vertical" class="mt-8">
|
||||
<a-form-item field="username" label="账号" hide-asterisk>
|
||||
<a-input v-model="model.username" placeholder="请输入账号/手机号/邮箱" allow-clear>
|
||||
<template #prefix>
|
||||
<i class="icon-park-outline-user" />
|
||||
</template>
|
||||
</a-input>
|
||||
</a-form-item>
|
||||
<a-form-item field="password" label="密码" hide-asterisk>
|
||||
<a-input-password v-model="model.password" placeholder="请输入密码" allow-clear>
|
||||
<template #prefix>
|
||||
<i class="icon-park-outline-lock" />
|
||||
</template>
|
||||
</a-input-password>
|
||||
</a-form-item>
|
||||
<a-space :size="16" direction="vertical">
|
||||
<div class="flex items-center justify-between">
|
||||
<a-checkbox checked="rememberPassword">记住我</a-checkbox>
|
||||
<a-link @click="onForgetPasswordClick">忘记密码?</a-link>
|
||||
</div>
|
||||
<a-button type="primary" html-type="submit" long class="mt-2" :loading="loading" @click="onSubmitClick">
|
||||
立即登录
|
||||
</a-button>
|
||||
<p type="text" long class="text-gray-400 text-center m-0">暂不支持其他方式登录</p>
|
||||
</a-space>
|
||||
</a-form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { dayjs } from "@/plugins";
|
||||
import { useAppStore } from "@/store";
|
||||
import { Modal } from "@arco-design/web-vue";
|
||||
import { reactive } from "vue";
|
||||
|
||||
const meridiem = dayjs.localeData().meridiem(dayjs().hour(), dayjs().minute());
|
||||
const appStore = useAppStore();
|
||||
const model = reactive({ username: "", password: "" });
|
||||
const router = useRouter();
|
||||
const loading = ref(false);
|
||||
|
||||
const onForgetPasswordClick = () => {
|
||||
Modal.info({
|
||||
title: "忘记密码?",
|
||||
content: "如已忘记密码,请联系管理员进行密码重置!",
|
||||
modalClass: "text-center",
|
||||
maskClosable: false,
|
||||
});
|
||||
};
|
||||
|
||||
const onSubmitClick = async () => {
|
||||
loading.value = true;
|
||||
await new Promise((resolve) => setTimeout(resolve, 2000));
|
||||
loading.value = false;
|
||||
router.push({ path: "/" });
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.page-login .bg {
|
||||
background-image: url(@/assets/wave.svg);
|
||||
filter: opacity(0.2);
|
||||
background-color: #c9e5fc;
|
||||
}
|
||||
.login-box {
|
||||
box-shadow: 0 0 8px 0 rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
</style>
|
||||
|
||||
<route lang="json">
|
||||
{
|
||||
"meta": {
|
||||
"sort": 101,
|
||||
"title": "登录",
|
||||
"icon": "icon-park-outline-home"
|
||||
}
|
||||
}
|
||||
</route>
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
<template>
|
||||
<bread-page class="">Demo Page</bread-page>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<style scoped></style>
|
||||
|
||||
<route lang="json">
|
||||
{
|
||||
"meta": {
|
||||
"sort": 10201,
|
||||
"title": "测试页面1",
|
||||
"icon": "icon-park-outline-add-subtract"
|
||||
},
|
||||
"parentMeta": {
|
||||
"sort": 10201,
|
||||
"title": "测试分类",
|
||||
"icon": "icon-park-outline-add-subtract"
|
||||
}
|
||||
}
|
||||
</route>
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
<template>
|
||||
<bread-page class="">Demo/test Page</bread-page>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<style scoped></style>
|
||||
|
||||
<route lang="json">
|
||||
{
|
||||
"meta": {
|
||||
"sort": 10202,
|
||||
"title": "测试页面2",
|
||||
"icon": "icon-park-outline-aperture-priority"
|
||||
}
|
||||
}
|
||||
</route>
|
||||
|
|
@ -0,0 +1,164 @@
|
|||
<template>
|
||||
<div class="m-4 p-4 bg-white">
|
||||
<Form v-bind="form"></Form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="tsx">
|
||||
import { Form, useForm } from "@/components";
|
||||
|
||||
const sleep = (wait: number) => new Promise((res) => setTimeout(res, wait));
|
||||
|
||||
const form = useForm({
|
||||
items: [
|
||||
{
|
||||
field: "username",
|
||||
label: "姓名",
|
||||
type: "input",
|
||||
required: true,
|
||||
itemProps: {
|
||||
hideLabel: false,
|
||||
},
|
||||
rules: ["password"],
|
||||
},
|
||||
{
|
||||
field: "nickname",
|
||||
label: "昵称",
|
||||
type: "input",
|
||||
disable: ({ model }) => !model.username,
|
||||
rules: [
|
||||
{
|
||||
message: "昵称不能超过 10 个字符",
|
||||
required: true,
|
||||
disable: ({ model }) => !model.username,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
field: "password",
|
||||
label: "密码",
|
||||
type: "password",
|
||||
visible: ({ model }) => model.username,
|
||||
nodeProps: {
|
||||
class: "w-full",
|
||||
},
|
||||
},
|
||||
{
|
||||
field: "gender",
|
||||
label: "性别",
|
||||
type: "select",
|
||||
options: [
|
||||
{
|
||||
label: "男",
|
||||
value: 1,
|
||||
},
|
||||
{
|
||||
label: "女",
|
||||
value: 2,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
field: "startTime:endTime",
|
||||
label: "时间",
|
||||
type: "time",
|
||||
nodeProps: {
|
||||
type: "time-range",
|
||||
},
|
||||
help: "时间段",
|
||||
},
|
||||
{
|
||||
field: "startDate:endDate",
|
||||
label: "日期",
|
||||
type: "dateRange",
|
||||
},
|
||||
{
|
||||
field: "checkbox",
|
||||
label: "多选",
|
||||
type: "checkbox",
|
||||
options: [
|
||||
{
|
||||
label: "选项1",
|
||||
value: 1,
|
||||
},
|
||||
{
|
||||
label: "选项2",
|
||||
value: 2,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
field: "radio",
|
||||
label: "单选",
|
||||
type: "radio",
|
||||
options: [
|
||||
{
|
||||
label: "选项1",
|
||||
value: 1,
|
||||
},
|
||||
{
|
||||
label: "选项2",
|
||||
value: 2,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
field: "slider",
|
||||
label: "音量",
|
||||
type: "slider",
|
||||
},
|
||||
{
|
||||
field: "provice:city:town",
|
||||
label: "城市",
|
||||
type: "cascader",
|
||||
nodeProps: {
|
||||
checkStrictly: true,
|
||||
pathMode: true,
|
||||
},
|
||||
options: [
|
||||
{
|
||||
label: "广西",
|
||||
value: "gx",
|
||||
children: [
|
||||
{
|
||||
label: "南宁",
|
||||
value: "nn",
|
||||
},
|
||||
{
|
||||
label: "桂林",
|
||||
value: "gl",
|
||||
children: [
|
||||
{
|
||||
label: "阳朔",
|
||||
value: "ys",
|
||||
},
|
||||
{
|
||||
label: "临桂",
|
||||
value: "lg",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
submit: async ({ model }) => {
|
||||
await sleep(3000);
|
||||
console.log("submit", model);
|
||||
return { message: "操作成功" };
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
|
||||
<route lang="json">
|
||||
{
|
||||
"meta": {
|
||||
"sort": 10101,
|
||||
"title": "首页111",
|
||||
"icon": "icon-park-outline-home"
|
||||
}
|
||||
}
|
||||
</route>
|
||||
|
|
@ -0,0 +1,161 @@
|
|||
<template>
|
||||
<div class="m-4 p-4 bg-white">
|
||||
<Table v-bind="table"></Table>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="tsx">
|
||||
import { ContentType, api } from "@/api";
|
||||
import { Table, useTable } from "@/components";
|
||||
import { dayjs } from "@/plugins";
|
||||
import { Avatar } from "@arco-design/web-vue";
|
||||
|
||||
const url = ref<any>(null);
|
||||
|
||||
const table = useTable({
|
||||
data: async () => {
|
||||
return [];
|
||||
},
|
||||
columns: [
|
||||
// {
|
||||
// type: 'index'
|
||||
// },
|
||||
{
|
||||
title: "姓名",
|
||||
dataIndex: "username",
|
||||
width: 200,
|
||||
render: ({ record }) => {
|
||||
return (
|
||||
<div class="flex items-center gap-2 w-full">
|
||||
<div>
|
||||
<Avatar size={32}>
|
||||
<img src={record.avatar} width={32} height={32} />
|
||||
</Avatar>
|
||||
</div>
|
||||
<div class="flex-1 overflow-hidden">
|
||||
<span class="ml-0">{record.nickname}</span>
|
||||
<div class="text-xs text-gray-400 mt-1 truncate">{record.description}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "昵称",
|
||||
dataIndex: "username",
|
||||
},
|
||||
{
|
||||
title: "昵称",
|
||||
dataIndex: "username",
|
||||
width: 200,
|
||||
render: ({ record }) => {
|
||||
return (
|
||||
<div class="">
|
||||
<span class="ml-0">{record.username}</span>
|
||||
<div class="text-xs text-gray-400 mt-1 truncate">创建于 {dayjs(record.createAt).format()}</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "操作",
|
||||
type: "button",
|
||||
width: 70,
|
||||
buttons: [],
|
||||
},
|
||||
],
|
||||
common: {
|
||||
modalProps: {
|
||||
width: 432,
|
||||
maskClosable: false,
|
||||
},
|
||||
formProps: {
|
||||
layout: "vertical",
|
||||
},
|
||||
model: {
|
||||
avatar: "11",
|
||||
},
|
||||
items: [
|
||||
{
|
||||
field: "username",
|
||||
label: "姓名",
|
||||
type: "input",
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
field: "nickname",
|
||||
label: "昵称",
|
||||
type: "input",
|
||||
},
|
||||
{
|
||||
field: "password",
|
||||
label: "密码",
|
||||
type: "password",
|
||||
},
|
||||
{
|
||||
label: "头像",
|
||||
field: "avatar",
|
||||
type: "input",
|
||||
component: ({ model, field }) => {
|
||||
const onInputChange = (e: Event) => {
|
||||
const target = e.target as HTMLInputElement;
|
||||
const file = target.files?.[0];
|
||||
if (!file) {
|
||||
return;
|
||||
}
|
||||
model[field] = file;
|
||||
console.log(file);
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
url.value = e.target?.result;
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
};
|
||||
return (
|
||||
<div class="w-full h-12 flex gap-4 items-center justify-between">
|
||||
<input type="file" onChange={onInputChange} class="flex-1" />
|
||||
{url.value && (
|
||||
<a-avatar size={40}>
|
||||
<img src={url.value} />
|
||||
</a-avatar>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
search: {
|
||||
items: [
|
||||
{
|
||||
field: "username",
|
||||
label: "姓名",
|
||||
type: "input",
|
||||
},
|
||||
],
|
||||
},
|
||||
create: {
|
||||
title: "新建用户",
|
||||
submit: ({ model }) => {
|
||||
return api.user.createUser(model, {
|
||||
type: ContentType.FormData,
|
||||
});
|
||||
},
|
||||
},
|
||||
modify: {
|
||||
title: "修改用户",
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
|
||||
<route lang="json">
|
||||
{
|
||||
"meta": {
|
||||
"sort": 10101,
|
||||
"title": "首页",
|
||||
"icon": "icon-park-outline-home"
|
||||
}
|
||||
}
|
||||
</route>
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
<template>
|
||||
<div class="m-4 p-4 bg-white">
|
||||
Post Page
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="tsx" name="PostPage">
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
|
||||
<route lang="json">
|
||||
{
|
||||
"meta": {
|
||||
"sort": 10401,
|
||||
"title": "文章管理",
|
||||
"icon": "icon-park-outline-document-folder"
|
||||
}
|
||||
}
|
||||
</route>
|
||||
|
|
@ -0,0 +1,117 @@
|
|||
<template>
|
||||
<BreadPage>
|
||||
<Table v-bind="table"></Table>
|
||||
</BreadPage>
|
||||
</template>
|
||||
|
||||
<script setup lang="tsx">
|
||||
import { ContentType, api } from "@/api";
|
||||
import { useTable } from "@/components";
|
||||
|
||||
const table = useTable({
|
||||
data: async (model, paging) => [],
|
||||
columns: [
|
||||
{
|
||||
title: "姓名",
|
||||
dataIndex: "username",
|
||||
width: 200,
|
||||
},
|
||||
{
|
||||
title: "昵称",
|
||||
dataIndex: "name",
|
||||
},
|
||||
{
|
||||
title: "创建时间",
|
||||
dataIndex: "createdAt",
|
||||
width: 200,
|
||||
},
|
||||
{
|
||||
title: "操作",
|
||||
type: "button",
|
||||
width: 70,
|
||||
buttons: [
|
||||
{
|
||||
type: "modify",
|
||||
text: "修改",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
common: {
|
||||
model: {
|
||||
avatarUrl: "",
|
||||
},
|
||||
items: [
|
||||
{
|
||||
field: "username",
|
||||
label: "姓名1",
|
||||
type: "input",
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
field: "nickname",
|
||||
label: "昵称",
|
||||
type: "input",
|
||||
},
|
||||
{
|
||||
field: "description",
|
||||
label: "个人描述",
|
||||
type: "input",
|
||||
},
|
||||
{
|
||||
field: "password",
|
||||
label: "密码",
|
||||
type: "password",
|
||||
},
|
||||
{
|
||||
label: "头像",
|
||||
field: "avatar?avatarUrl",
|
||||
type: "input",
|
||||
},
|
||||
],
|
||||
modalProps: {
|
||||
width: 772,
|
||||
maskClosable: false,
|
||||
},
|
||||
formProps: {
|
||||
layout: "vertical",
|
||||
class: "!grid grid-cols-2 gap-x-3",
|
||||
},
|
||||
},
|
||||
search: {
|
||||
items: [
|
||||
{
|
||||
extend: "username",
|
||||
required: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
create: {
|
||||
title: "新建用户",
|
||||
submit: ({ model }) => {
|
||||
return api.user.createUser(model as any, {
|
||||
type: ContentType.FormData,
|
||||
});
|
||||
},
|
||||
},
|
||||
modify: {
|
||||
extend: true,
|
||||
title: "修改用户",
|
||||
submit: ({ model }) => {
|
||||
return api.user.updateUser(model.id, model);
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
|
||||
<route lang="json">
|
||||
{
|
||||
"meta": {
|
||||
"sort": 10301,
|
||||
"title": "用户管理",
|
||||
"icon": "icon-park-outline-user"
|
||||
}
|
||||
}
|
||||
</route>
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
import dayjs from "dayjs";
|
||||
import "dayjs/locale/zh-cn";
|
||||
import localData from "dayjs/plugin/localeData";
|
||||
import relativeTime from "dayjs/plugin/relativeTime";
|
||||
|
||||
/**
|
||||
*
|
||||
* 默认日期时间格式
|
||||
*/
|
||||
const DATETIME = "YYYY-MM-DD HH:mm:ss";
|
||||
|
||||
/**
|
||||
* 默认日期格式
|
||||
*/
|
||||
const DATE = "YYYY-MM-DD";
|
||||
|
||||
/**
|
||||
* 默认时间格式
|
||||
*/
|
||||
const TIME = "HH:mm:ss";
|
||||
|
||||
/**
|
||||
* 中文语言包
|
||||
*/
|
||||
dayjs.locale("zh-cn");
|
||||
|
||||
/**
|
||||
* 相对时间插件
|
||||
* @see https://dayjs.gitee.io/docs/zh-CN/plugin/relative-time
|
||||
*/
|
||||
dayjs.extend(relativeTime);
|
||||
|
||||
/**
|
||||
* 本地化插件
|
||||
* @see https://dayjs.gitee.io/docs/zh-CN/plugin/locale-data
|
||||
*/
|
||||
dayjs.extend(localData);
|
||||
|
||||
/**
|
||||
*
|
||||
* 默认时间格式
|
||||
*/
|
||||
dayjs.DATETIME = DATETIME;
|
||||
|
||||
/**
|
||||
* 默认日期格式
|
||||
*/
|
||||
dayjs.DATE = DATE;
|
||||
|
||||
/**
|
||||
* 默认时间格式
|
||||
*/
|
||||
dayjs.TIME = TIME;
|
||||
|
||||
/**
|
||||
* 重写format方法,如果没有传入format参数,则使用默认的时间格式
|
||||
*/
|
||||
dayjs.prototype._format = dayjs.prototype.format;
|
||||
dayjs.prototype.format = function (format?: string) {
|
||||
if (format) {
|
||||
return this._format(format);
|
||||
}
|
||||
return this._format(dayjs.DATETIME);
|
||||
};
|
||||
|
||||
export { dayjs, DATETIME, DATE, TIME };
|
||||
|
||||