Browse Source
* init * init * fix: 修改外联路由打包bug * fix: sime * wip(lock): remove * fix: LOCK * fix: lock * init * feat: remove lock * chore: remove semi * chore: chore * chore: chore * chore: chore * init * init * init * init * init * init * init * init * init * init * init * init * init * init * init * init * init * init * init * init * init * initpull/2464/head
committed by
GitHub
716 changed files with 7342 additions and 48287 deletions
File diff suppressed because it is too large
File diff suppressed because it is too large
@ -1,175 +0,0 @@ |
|||
<div align="center"> <a href="https://github.com/anncwb/vue-vben-admin"> <img alt="VbenAdmin Logo" width="200" height="200" src="https://anncwb.github.io/anncwb/images/logo.png"> </a> <br> <br> |
|||
|
|||
[](LICENSE) |
|||
|
|||
<h1>Vue vben admin</h1> |
|||
</div> |
|||
|
|||
**中文** | [English](./README.md) |
|||
|
|||
## 简介 |
|||
|
|||
Vue Vben Admin 是一个免费开源的中后台模版。使用了最新的`vue3`,`vite2`,`TypeScript`等主流技术开发,开箱即用的中后台前端解决方案,也可用于学习参考。 |
|||
|
|||
## 特性 |
|||
|
|||
- **最新技术栈**:使用 Vue3/vite2 等前端前沿技术开发 |
|||
- **TypeScript**: 应用程序级 JavaScript 的语言 |
|||
- **主题**:可配置的主题 |
|||
- **国际化**:内置完善的国际化方案 |
|||
- **Mock 数据** 内置 Mock 数据方案 |
|||
- **权限** 内置完善的动态路由权限生成方案 |
|||
- **组件** 二次封装了多个常用的组件 |
|||
|
|||
## 预览 |
|||
|
|||
- [vue-vben-admin](https://vvbin.cn/next/) - 完整版中文站点 |
|||
- [vue-vben-admin-gh-pages](https://anncwb.github.io/vue-vben-admin/) - 完整版 github 站点 |
|||
- [vben-admin-thin-next](https://vvbin.cn/thin/next/) - 简化版中文站点 |
|||
- [vben-admin-thin-gh-pages](https://anncwb.github.io/vben-admin-thin-next/) - 简化版 github 站点 |
|||
|
|||
测试账号: vben/123456 |
|||
|
|||
<p align="center"> |
|||
<img alt="VbenAdmin Logo" width="100%" src="https://anncwb.github.io/anncwb/images/preview1.png"> |
|||
<img alt="VbenAdmin Logo" width="100%" src="https://anncwb.github.io/anncwb/images/preview2.png"> |
|||
<img alt="VbenAdmin Logo" width="100%" src="https://anncwb.github.io/anncwb/images/preview3.png"> |
|||
</p> |
|||
|
|||
### 使用 Gitpod |
|||
|
|||
在 Gitpod(适用于 GitHub 的免费在线开发环境)中打开项目,并立即开始编码. |
|||
|
|||
[](https://gitpod.io/#https://github.com/anncwb/vue-vben-admin) |
|||
|
|||
## 文档 |
|||
|
|||
[文档地址](https://vvbin.cn/doc-next/) |
|||
|
|||
## 准备 |
|||
|
|||
- [node](http://nodejs.org/) 和 [git](https://git-scm.com/) -项目开发环境 |
|||
- [Vite](https://vitejs.dev/) - 熟悉 vite 特性 |
|||
- [Vue3](https://v3.vuejs.org/) - 熟悉 Vue 基础语法 |
|||
- [TypeScript](https://www.typescriptlang.org/) - 熟悉`TypeScript`基本语法 |
|||
- [Es6+](http://es6.ruanyifeng.com/) - 熟悉 es6 基本语法 |
|||
- [Vue-Router-Next](https://next.router.vuejs.org/) - 熟悉 vue-router 基本使用 |
|||
- [Ant-Design-Vue](https://2x.antdv.com/docs/vue/introduce-cn/) - ui 基本使用 |
|||
- [Mock.js](https://github.com/nuysoft/Mock) - mockjs 基本语法 |
|||
|
|||
## 安装使用 |
|||
|
|||
- 获取项目代码 |
|||
|
|||
```bash |
|||
git clone https://github.com/anncwb/vue-vben-admin.git |
|||
``` |
|||
|
|||
- 安装依赖 |
|||
|
|||
```bash |
|||
cd vue-vben-admin |
|||
|
|||
pnpm install |
|||
|
|||
``` |
|||
|
|||
- 运行 |
|||
|
|||
```bash |
|||
pnpm serve |
|||
``` |
|||
|
|||
- 打包 |
|||
|
|||
```bash |
|||
pnpm build |
|||
``` |
|||
|
|||
## 更新日志 |
|||
|
|||
[CHANGELOG](./CHANGELOG.zh_CN.md) |
|||
|
|||
## 项目地址 |
|||
|
|||
- [vue-vben-admin](https://github.com/anncwb/vue-vben-admin) - 完整版 |
|||
- [vue-vben-admin-thin-next](https://github.com/anncwb/vben-admin-thin-next) - 简化版 |
|||
|
|||
## 如何贡献 |
|||
|
|||
非常欢迎你的加入 或者提交一个 Pull Request。 |
|||
|
|||
**Pull Request:** |
|||
|
|||
1. Fork 代码! |
|||
2. 创建自己的分支: `git checkout -b feat/xxxx` |
|||
3. 提交你的修改: `git commit -am 'feat(function): add xxxxx'` |
|||
4. 推送您的分支: `git push origin feat/xxxx` |
|||
5. 提交`pull request` |
|||
|
|||
## Git 贡献提交规范 |
|||
|
|||
- 参考 [vue](https://github.com/vuejs/vue/blob/dev/.github/COMMIT_CONVENTION.md) 规范 ([Angular](https://github.com/conventional-changelog/conventional-changelog/tree/master/packages/conventional-changelog-angular)) |
|||
|
|||
- `feat` 增加新功能 |
|||
- `fix` 修复问题/BUG |
|||
- `style` 代码风格相关无影响运行结果的 |
|||
- `perf` 优化/性能提升 |
|||
- `refactor` 重构 |
|||
- `revert` 撤销修改 |
|||
- `test` 测试相关 |
|||
- `docs` 文档/注释 |
|||
- `chore` 依赖更新/脚手架配置修改等 |
|||
- `workflow` 工作流改进 |
|||
- `ci` 持续集成 |
|||
- `types` 类型定义文件更改 |
|||
- `wip` 开发中 |
|||
|
|||
## 浏览器支持 |
|||
|
|||
本地开发推荐使用`Chrome 80+` 浏览器 |
|||
|
|||
支持现代浏览器, 不支持 IE |
|||
|
|||
| [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/edge/edge_48x48.png" alt=" Edge" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>IE | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/edge/edge_48x48.png" alt=" Edge" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Edge | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/firefox/firefox_48x48.png" alt="Firefox" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Firefox | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/chrome/chrome_48x48.png" alt="Chrome" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Chrome | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/safari/safari_48x48.png" alt="Safari" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Safari | |
|||
| :-: | :-: | :-: | :-: | :-: | |
|||
| not support | last 2 versions | last 2 versions | last 2 versions | last 2 versions | |
|||
|
|||
## 相关仓库 |
|||
|
|||
如果这些插件对你有帮助,可以给一个 star 支持下 |
|||
|
|||
- [vite-plugin-mock](https://github.com/anncwb/vite-plugin-mock) - 用于本地及开发环境数据 mock |
|||
- [vite-plugin-html](https://github.com/anncwb/vite-plugin-html) - 用于 html 模版转换及压缩 |
|||
- [vite-plugin-style-import](https://github.com/anncwb/vite-plugin-style-import) - 用于组件库样式按需引入 |
|||
- [vite-plugin-theme](https://github.com/anncwb/vite-plugin-theme) - 用于在线切换主题色等颜色相关配置 |
|||
- [vite-plugin-imagemin](https://github.com/anncwb/vite-plugin-imagemin) - 用于打包压缩图片资源 |
|||
- [vite-plugin-compression](https://github.com/anncwb/vite-plugin-compression) - 用于打包输出.gz|.brotil 文件 |
|||
- [vite-plugin-svg-icons](https://github.com/anncwb/vite-plugin-svg-icons) - 用于快速生成 svg 雪碧图 |
|||
|
|||
## 后台整合示例 |
|||
|
|||
- [lamp-cloud](https://github.com/zuihou/lamp-cloud) - 基于 SpringCloud Alibaba 的微服务中后台快速开发平台 |
|||
- [matecloud](https://github.com/matevip/matecloud) - MateCloud 微服务脚手架,基于 Spring Cloud 2020.0.3、SpringBoot 2.5.3 的全开源平台 |
|||
|
|||
## 维护者 |
|||
|
|||
[@Vben](https://github.com/anncwb) |
|||
|
|||
## 捐赠 |
|||
|
|||
如果你觉得这个项目对你有帮助,你可以帮作者买一杯咖啡表示支持! |
|||
|
|||
 |
|||
|
|||
<a style="display: block;width: 100px;height: 50px;line-height: 50px; color: #fff;text-align: center; background: #408aed;border-radius: 4px;" href="https://www.paypal.com/paypalme/cvvben">Paypal Me</a> |
|||
|
|||
## 交流 |
|||
|
|||
`Vue-vben-Admin` 是完全开源免费的项目,在帮助开发者更方便地进行中大型管理系统开发,同时也提供 QQ 交流群使用问题欢迎在群内提问。 |
|||
|
|||
- QQ 群 `569291866` |
|||
|
|||
## License |
|||
|
|||
[MIT © Vben-2020](./LICENSE) |
|||
File diff suppressed because it is too large
@ -1,10 +1,10 @@ |
|||
module.exports = { |
|||
printWidth: 100, |
|||
semi: true, |
|||
semi: false, |
|||
vueIndentScriptAndStyle: true, |
|||
singleQuote: true, |
|||
trailingComma: 'all', |
|||
proseWrap: 'never', |
|||
htmlWhitespaceSensitivity: 'strict', |
|||
endOfLine: 'auto', |
|||
}; |
|||
} |
|||
|
|||
@ -1,9 +1,9 @@ |
|||
import { defHttp } from '/@/utils/http/axios'; |
|||
import { AreaModel, AreaParams } from '/@/api/demo/model/areaModel'; |
|||
import { defHttp } from '/@/utils/http/axios' |
|||
import { AreaModel, AreaParams } from '/@/api/demo/model/areaModel' |
|||
|
|||
enum Api { |
|||
AREA_RECORD = '/cascader/getAreaRecord', |
|||
} |
|||
|
|||
export const areaRecord = (data: AreaParams) => |
|||
defHttp.post<AreaModel>({ url: Api.AREA_RECORD, data }); |
|||
defHttp.post<AreaModel>({ url: Api.AREA_RECORD, data }) |
|||
|
|||
@ -1,7 +1,7 @@ |
|||
export interface GetAccountInfoModel { |
|||
email: string; |
|||
name: string; |
|||
introduction: string; |
|||
phone: string; |
|||
address: string; |
|||
email: string |
|||
name: string |
|||
introduction: string |
|||
phone: string |
|||
address: string |
|||
} |
|||
|
|||
@ -1,12 +1,12 @@ |
|||
export interface AreaModel { |
|||
id: string; |
|||
code: string; |
|||
parentCode: string; |
|||
name: string; |
|||
levelType: number; |
|||
[key: string]: string | number; |
|||
id: string |
|||
code: string |
|||
parentCode: string |
|||
name: string |
|||
levelType: number |
|||
[key: string]: string | number |
|||
} |
|||
|
|||
export interface AreaParams { |
|||
parentCode: string; |
|||
parentCode: string |
|||
} |
|||
|
|||
@ -1,15 +1,15 @@ |
|||
import { BasicFetchResult } from '/@/api/model/baseModel'; |
|||
import { BasicFetchResult } from '/@/api/model/baseModel' |
|||
|
|||
export interface DemoOptionsItem { |
|||
label: string; |
|||
value: string; |
|||
label: string |
|||
value: string |
|||
} |
|||
|
|||
export interface selectParams { |
|||
id: number | string; |
|||
id: number | string |
|||
} |
|||
|
|||
/** |
|||
* @description: Request list return value |
|||
*/ |
|||
export type DemoOptionsGetResultModel = BasicFetchResult<DemoOptionsItem>; |
|||
export type DemoOptionsGetResultModel = BasicFetchResult<DemoOptionsItem> |
|||
|
|||
@ -1,74 +1,74 @@ |
|||
import { BasicPageParams, BasicFetchResult } from '/@/api/model/baseModel'; |
|||
import { BasicPageParams, BasicFetchResult } from '/@/api/model/baseModel' |
|||
|
|||
export type AccountParams = BasicPageParams & { |
|||
account?: string; |
|||
nickname?: string; |
|||
}; |
|||
account?: string |
|||
nickname?: string |
|||
} |
|||
|
|||
export type RoleParams = { |
|||
roleName?: string; |
|||
status?: string; |
|||
}; |
|||
roleName?: string |
|||
status?: string |
|||
} |
|||
|
|||
export type RolePageParams = BasicPageParams & RoleParams; |
|||
export type RolePageParams = BasicPageParams & RoleParams |
|||
|
|||
export type DeptParams = { |
|||
deptName?: string; |
|||
status?: string; |
|||
}; |
|||
deptName?: string |
|||
status?: string |
|||
} |
|||
|
|||
export type MenuParams = { |
|||
menuName?: string; |
|||
status?: string; |
|||
}; |
|||
menuName?: string |
|||
status?: string |
|||
} |
|||
|
|||
export interface AccountListItem { |
|||
id: string; |
|||
account: string; |
|||
email: string; |
|||
nickname: string; |
|||
role: number; |
|||
createTime: string; |
|||
remark: string; |
|||
status: number; |
|||
id: string |
|||
account: string |
|||
email: string |
|||
nickname: string |
|||
role: number |
|||
createTime: string |
|||
remark: string |
|||
status: number |
|||
} |
|||
|
|||
export interface DeptListItem { |
|||
id: string; |
|||
orderNo: string; |
|||
createTime: string; |
|||
remark: string; |
|||
status: number; |
|||
id: string |
|||
orderNo: string |
|||
createTime: string |
|||
remark: string |
|||
status: number |
|||
} |
|||
|
|||
export interface MenuListItem { |
|||
id: string; |
|||
orderNo: string; |
|||
createTime: string; |
|||
status: number; |
|||
icon: string; |
|||
component: string; |
|||
permission: string; |
|||
id: string |
|||
orderNo: string |
|||
createTime: string |
|||
status: number |
|||
icon: string |
|||
component: string |
|||
permission: string |
|||
} |
|||
|
|||
export interface RoleListItem { |
|||
id: string; |
|||
roleName: string; |
|||
roleValue: string; |
|||
status: number; |
|||
orderNo: string; |
|||
createTime: string; |
|||
id: string |
|||
roleName: string |
|||
roleValue: string |
|||
status: number |
|||
orderNo: string |
|||
createTime: string |
|||
} |
|||
|
|||
/** |
|||
* @description: Request list return value |
|||
*/ |
|||
export type AccountListGetResultModel = BasicFetchResult<AccountListItem>; |
|||
export type AccountListGetResultModel = BasicFetchResult<AccountListItem> |
|||
|
|||
export type DeptListGetResultModel = BasicFetchResult<DeptListItem>; |
|||
export type DeptListGetResultModel = BasicFetchResult<DeptListItem> |
|||
|
|||
export type MenuListGetResultModel = BasicFetchResult<MenuListItem>; |
|||
export type MenuListGetResultModel = BasicFetchResult<MenuListItem> |
|||
|
|||
export type RolePageListGetResultModel = BasicFetchResult<RoleListItem>; |
|||
export type RolePageListGetResultModel = BasicFetchResult<RoleListItem> |
|||
|
|||
export type RoleListGetResultModel = RoleListItem[]; |
|||
export type RoleListGetResultModel = RoleListItem[] |
|||
|
|||
@ -1,20 +1,20 @@ |
|||
import { BasicPageParams, BasicFetchResult } from '/@/api/model/baseModel'; |
|||
import { BasicPageParams, BasicFetchResult } from '/@/api/model/baseModel' |
|||
/** |
|||
* @description: Request list interface parameters |
|||
*/ |
|||
export type DemoParams = BasicPageParams; |
|||
export type DemoParams = BasicPageParams |
|||
|
|||
export interface DemoListItem { |
|||
id: string; |
|||
beginTime: string; |
|||
endTime: string; |
|||
address: string; |
|||
name: string; |
|||
no: number; |
|||
status: number; |
|||
id: string |
|||
beginTime: string |
|||
endTime: string |
|||
address: string |
|||
name: string |
|||
no: number |
|||
status: number |
|||
} |
|||
|
|||
/** |
|||
* @description: Request list return value |
|||
*/ |
|||
export type DemoListGetResultModel = BasicFetchResult<DemoListItem>; |
|||
export type DemoListGetResultModel = BasicFetchResult<DemoListItem> |
|||
|
|||
@ -1,9 +1,9 @@ |
|||
export interface BasicPageParams { |
|||
page: number; |
|||
pageSize: number; |
|||
page: number |
|||
pageSize: number |
|||
} |
|||
|
|||
export interface BasicFetchResult<T> { |
|||
items: T[]; |
|||
total: number; |
|||
items: T[] |
|||
total: number |
|||
} |
|||
|
|||
@ -1,16 +1,16 @@ |
|||
import type { RouteMeta } from 'vue-router'; |
|||
import type { RouteMeta } from 'vue-router' |
|||
export interface RouteItem { |
|||
path: string; |
|||
component: any; |
|||
meta: RouteMeta; |
|||
name?: string; |
|||
alias?: string | string[]; |
|||
redirect?: string; |
|||
caseSensitive?: boolean; |
|||
children?: RouteItem[]; |
|||
path: string |
|||
component: any |
|||
meta: RouteMeta |
|||
name?: string |
|||
alias?: string | string[] |
|||
redirect?: string |
|||
caseSensitive?: boolean |
|||
children?: RouteItem[] |
|||
} |
|||
|
|||
/** |
|||
* @description: Get menu return value |
|||
*/ |
|||
export type getMenuListResultModel = RouteItem[]; |
|||
export type getMenuListResultModel = RouteItem[] |
|||
|
|||
@ -1,5 +1,5 @@ |
|||
export interface UploadApiResult { |
|||
message: string; |
|||
code: number; |
|||
url: string; |
|||
message: string |
|||
code: number |
|||
url: string |
|||
} |
|||
|
|||
@ -1,22 +0,0 @@ |
|||
import { UploadApiResult } from './model/uploadModel'; |
|||
import { defHttp } from '/@/utils/http/axios'; |
|||
import { UploadFileParams } from '/#/axios'; |
|||
import { useGlobSetting } from '/@/hooks/setting'; |
|||
|
|||
const { uploadUrl = '' } = useGlobSetting(); |
|||
|
|||
/** |
|||
* @description: Upload interface |
|||
*/ |
|||
export function uploadApi( |
|||
params: UploadFileParams, |
|||
onUploadProgress: (progressEvent: ProgressEvent) => void, |
|||
) { |
|||
return defHttp.uploadFile<UploadApiResult>( |
|||
{ |
|||
url: uploadUrl, |
|||
onUploadProgress, |
|||
}, |
|||
params, |
|||
); |
|||
} |
|||
@ -1,15 +1,15 @@ |
|||
import { withInstall } from '/@/utils'; |
|||
import { withInstall } from '/@/utils' |
|||
|
|||
import appLogo from './src/AppLogo.vue'; |
|||
import appProvider from './src/AppProvider.vue'; |
|||
import appSearch from './src/search/AppSearch.vue'; |
|||
import appLocalePicker from './src/AppLocalePicker.vue'; |
|||
import appDarkModeToggle from './src/AppDarkModeToggle.vue'; |
|||
import appLogo from './src/AppLogo.vue' |
|||
import appProvider from './src/AppProvider.vue' |
|||
import appSearch from './src/search/AppSearch.vue' |
|||
import appLocalePicker from './src/AppLocalePicker.vue' |
|||
import appDarkModeToggle from './src/AppDarkModeToggle.vue' |
|||
|
|||
export { useAppProviderContext } from './src/useAppContext'; |
|||
export { useAppProviderContext } from './src/useAppContext' |
|||
|
|||
export const AppLogo = withInstall(appLogo); |
|||
export const AppProvider = withInstall(appProvider); |
|||
export const AppSearch = withInstall(appSearch); |
|||
export const AppLocalePicker = withInstall(appLocalePicker); |
|||
export const AppDarkModeToggle = withInstall(appDarkModeToggle); |
|||
export const AppLogo = withInstall(appLogo) |
|||
export const AppProvider = withInstall(appProvider) |
|||
export const AppSearch = withInstall(appSearch) |
|||
export const AppLocalePicker = withInstall(appLocalePicker) |
|||
export const AppDarkModeToggle = withInstall(appDarkModeToggle) |
|||
|
|||
@ -1,166 +1,166 @@ |
|||
import type { Menu } from '/@/router/types'; |
|||
import { ref, onBeforeMount, unref, Ref, nextTick } from 'vue'; |
|||
import { getMenus } from '/@/router/menus'; |
|||
import { cloneDeep } from 'lodash-es'; |
|||
import { filter, forEach } from '/@/utils/helper/treeHelper'; |
|||
import { useGo } from '/@/hooks/web/usePage'; |
|||
import { useScrollTo } from '/@/hooks/event/useScrollTo'; |
|||
import { onKeyStroke, useDebounceFn } from '@vueuse/core'; |
|||
import { useI18n } from '/@/hooks/web/useI18n'; |
|||
import type { Menu } from '/@/router/types' |
|||
import { ref, onBeforeMount, unref, Ref, nextTick } from 'vue' |
|||
import { getMenus } from '/@/router/menus' |
|||
import { cloneDeep } from 'lodash-es' |
|||
import { filter, forEach } from '/@/utils/helper/treeHelper' |
|||
import { useGo } from '/@/hooks/web/usePage' |
|||
import { useScrollTo } from '/@/hooks/event/useScrollTo' |
|||
import { onKeyStroke, useDebounceFn } from '@vueuse/core' |
|||
import { useI18n } from '/@/hooks/web/useI18n' |
|||
|
|||
export interface SearchResult { |
|||
name: string; |
|||
path: string; |
|||
icon?: string; |
|||
name: string |
|||
path: string |
|||
icon?: string |
|||
} |
|||
|
|||
// Translate special characters
|
|||
function transform(c: string) { |
|||
const code: string[] = ['$', '(', ')', '*', '+', '.', '[', ']', '?', '\\', '^', '{', '}', '|']; |
|||
return code.includes(c) ? `\\${c}` : c; |
|||
const code: string[] = ['$', '(', ')', '*', '+', '.', '[', ']', '?', '\\', '^', '{', '}', '|'] |
|||
return code.includes(c) ? `\\${c}` : c |
|||
} |
|||
|
|||
function createSearchReg(key: string) { |
|||
const keys = [...key].map((item) => transform(item)); |
|||
const str = ['', ...keys, ''].join('.*'); |
|||
return new RegExp(str); |
|||
const keys = [...key].map((item) => transform(item)) |
|||
const str = ['', ...keys, ''].join('.*') |
|||
return new RegExp(str) |
|||
} |
|||
|
|||
export function useMenuSearch(refs: Ref<HTMLElement[]>, scrollWrap: Ref<ElRef>, emit: EmitType) { |
|||
const searchResult = ref<SearchResult[]>([]); |
|||
const keyword = ref(''); |
|||
const activeIndex = ref(-1); |
|||
const searchResult = ref<SearchResult[]>([]) |
|||
const keyword = ref('') |
|||
const activeIndex = ref(-1) |
|||
|
|||
let menuList: Menu[] = []; |
|||
let menuList: Menu[] = [] |
|||
|
|||
const { t } = useI18n(); |
|||
const go = useGo(); |
|||
const handleSearch = useDebounceFn(search, 200); |
|||
const { t } = useI18n() |
|||
const go = useGo() |
|||
const handleSearch = useDebounceFn(search, 200) |
|||
|
|||
onBeforeMount(async () => { |
|||
const list = await getMenus(); |
|||
menuList = cloneDeep(list); |
|||
const list = await getMenus() |
|||
menuList = cloneDeep(list) |
|||
forEach(menuList, (item) => { |
|||
item.name = t(item.name); |
|||
}); |
|||
}); |
|||
item.name = t(item.name) |
|||
}) |
|||
}) |
|||
|
|||
function search(e: ChangeEvent) { |
|||
e?.stopPropagation(); |
|||
const key = e.target.value; |
|||
keyword.value = key.trim(); |
|||
e?.stopPropagation() |
|||
const key = e.target.value |
|||
keyword.value = key.trim() |
|||
if (!key) { |
|||
searchResult.value = []; |
|||
return; |
|||
searchResult.value = [] |
|||
return |
|||
} |
|||
const reg = createSearchReg(unref(keyword)); |
|||
const reg = createSearchReg(unref(keyword)) |
|||
const filterMenu = filter(menuList, (item) => { |
|||
return reg.test(item.name) && !item.hideMenu; |
|||
}); |
|||
searchResult.value = handlerSearchResult(filterMenu, reg); |
|||
activeIndex.value = 0; |
|||
return reg.test(item.name) && !item.hideMenu |
|||
}) |
|||
searchResult.value = handlerSearchResult(filterMenu, reg) |
|||
activeIndex.value = 0 |
|||
} |
|||
|
|||
function handlerSearchResult(filterMenu: Menu[], reg: RegExp, parent?: Menu) { |
|||
const ret: SearchResult[] = []; |
|||
const ret: SearchResult[] = [] |
|||
filterMenu.forEach((item) => { |
|||
const { name, path, icon, children, hideMenu, meta } = item; |
|||
const { name, path, icon, children, hideMenu, meta } = item |
|||
if (!hideMenu && reg.test(name) && (!children?.length || meta?.hideChildrenInMenu)) { |
|||
ret.push({ |
|||
name: parent?.name ? `${parent.name} > ${name}` : name, |
|||
path, |
|||
icon, |
|||
}); |
|||
}) |
|||
} |
|||
if (!meta?.hideChildrenInMenu && Array.isArray(children) && children.length) { |
|||
ret.push(...handlerSearchResult(children, reg, item)); |
|||
ret.push(...handlerSearchResult(children, reg, item)) |
|||
} |
|||
}); |
|||
return ret; |
|||
}) |
|||
return ret |
|||
} |
|||
|
|||
// Activate when the mouse moves to a certain line
|
|||
function handleMouseenter(e: any) { |
|||
const index = e.target.dataset.index; |
|||
activeIndex.value = Number(index); |
|||
const index = e.target.dataset.index |
|||
activeIndex.value = Number(index) |
|||
} |
|||
|
|||
// Arrow key up
|
|||
function handleUp() { |
|||
if (!searchResult.value.length) return; |
|||
activeIndex.value--; |
|||
if (!searchResult.value.length) return |
|||
activeIndex.value-- |
|||
if (activeIndex.value < 0) { |
|||
activeIndex.value = searchResult.value.length - 1; |
|||
activeIndex.value = searchResult.value.length - 1 |
|||
} |
|||
handleScroll(); |
|||
handleScroll() |
|||
} |
|||
|
|||
// Arrow key down
|
|||
function handleDown() { |
|||
if (!searchResult.value.length) return; |
|||
activeIndex.value++; |
|||
if (!searchResult.value.length) return |
|||
activeIndex.value++ |
|||
if (activeIndex.value > searchResult.value.length - 1) { |
|||
activeIndex.value = 0; |
|||
activeIndex.value = 0 |
|||
} |
|||
handleScroll(); |
|||
handleScroll() |
|||
} |
|||
|
|||
// When the keyboard up and down keys move to an invisible place
|
|||
// the scroll bar needs to scroll automatically
|
|||
function handleScroll() { |
|||
const refList = unref(refs); |
|||
const refList = unref(refs) |
|||
if (!refList || !Array.isArray(refList) || refList.length === 0 || !unref(scrollWrap)) { |
|||
return; |
|||
return |
|||
} |
|||
|
|||
const index = unref(activeIndex); |
|||
const currentRef = refList[index]; |
|||
const index = unref(activeIndex) |
|||
const currentRef = refList[index] |
|||
if (!currentRef) { |
|||
return; |
|||
return |
|||
} |
|||
const wrapEl = unref(scrollWrap); |
|||
const wrapEl = unref(scrollWrap) |
|||
if (!wrapEl) { |
|||
return; |
|||
return |
|||
} |
|||
const scrollHeight = currentRef.offsetTop + currentRef.offsetHeight; |
|||
const wrapHeight = wrapEl.offsetHeight; |
|||
const scrollHeight = currentRef.offsetTop + currentRef.offsetHeight |
|||
const wrapHeight = wrapEl.offsetHeight |
|||
const { start } = useScrollTo({ |
|||
el: wrapEl, |
|||
duration: 100, |
|||
to: scrollHeight - wrapHeight, |
|||
}); |
|||
start(); |
|||
}) |
|||
start() |
|||
} |
|||
|
|||
// enter keyboard event
|
|||
async function handleEnter() { |
|||
if (!searchResult.value.length) { |
|||
return; |
|||
return |
|||
} |
|||
const result = unref(searchResult); |
|||
const index = unref(activeIndex); |
|||
const result = unref(searchResult) |
|||
const index = unref(activeIndex) |
|||
if (result.length === 0 || index < 0) { |
|||
return; |
|||
return |
|||
} |
|||
const to = result[index]; |
|||
handleClose(); |
|||
await nextTick(); |
|||
go(to.path); |
|||
const to = result[index] |
|||
handleClose() |
|||
await nextTick() |
|||
go(to.path) |
|||
} |
|||
|
|||
// close search modal
|
|||
function handleClose() { |
|||
searchResult.value = []; |
|||
emit('close'); |
|||
searchResult.value = [] |
|||
emit('close') |
|||
} |
|||
|
|||
// enter search
|
|||
onKeyStroke('Enter', handleEnter); |
|||
onKeyStroke('Enter', handleEnter) |
|||
// Monitor keyboard arrow keys
|
|||
onKeyStroke('ArrowUp', handleUp); |
|||
onKeyStroke('ArrowDown', handleDown); |
|||
onKeyStroke('ArrowUp', handleUp) |
|||
onKeyStroke('ArrowDown', handleDown) |
|||
// esc close
|
|||
onKeyStroke('Escape', handleClose); |
|||
onKeyStroke('Escape', handleClose) |
|||
|
|||
return { handleSearch, searchResult, keyword, activeIndex, handleMouseenter, handleEnter }; |
|||
return { handleSearch, searchResult, keyword, activeIndex, handleMouseenter, handleEnter } |
|||
} |
|||
|
|||
@ -1,17 +1,17 @@ |
|||
import { InjectionKey, Ref } from 'vue'; |
|||
import { createContext, useContext } from '/@/hooks/core/useContext'; |
|||
import { InjectionKey, Ref } from 'vue' |
|||
import { createContext, useContext } from '/@/hooks/core/useContext' |
|||
|
|||
export interface AppProviderContextProps { |
|||
prefixCls: Ref<string>; |
|||
isMobile: Ref<boolean>; |
|||
prefixCls: Ref<string> |
|||
isMobile: Ref<boolean> |
|||
} |
|||
|
|||
const key: InjectionKey<AppProviderContextProps> = Symbol(); |
|||
const key: InjectionKey<AppProviderContextProps> = Symbol() |
|||
|
|||
export function createAppProviderContext(context: AppProviderContextProps) { |
|||
return createContext<AppProviderContextProps>(context, key); |
|||
return createContext<AppProviderContextProps>(context, key) |
|||
} |
|||
|
|||
export function useAppProviderContext() { |
|||
return useContext<AppProviderContextProps>(key); |
|||
return useContext<AppProviderContextProps>(key) |
|||
} |
|||
|
|||
@ -1,4 +0,0 @@ |
|||
import { withInstall } from '/@/utils'; |
|||
import authority from './src/Authority.vue'; |
|||
|
|||
export const Authority = withInstall(authority); |
|||
@ -1,45 +0,0 @@ |
|||
<!-- |
|||
Access control component for fine-grained access control. |
|||
--> |
|||
<script lang="ts"> |
|||
import type { PropType } from 'vue'; |
|||
import { defineComponent } from 'vue'; |
|||
import { RoleEnum } from '/@/enums/roleEnum'; |
|||
import { usePermission } from '/@/hooks/web/usePermission'; |
|||
import { getSlot } from '/@/utils/helper/tsxHelper'; |
|||
|
|||
export default defineComponent({ |
|||
name: 'Authority', |
|||
props: { |
|||
/** |
|||
* Specified role is visible |
|||
* When the permission mode is the role mode, the value value can pass the role value. |
|||
* When the permission mode is background, the value value can pass the code permission value |
|||
* @default '' |
|||
*/ |
|||
value: { |
|||
type: [Number, Array, String] as PropType<RoleEnum | RoleEnum[] | string | string[]>, |
|||
default: '', |
|||
}, |
|||
}, |
|||
setup(props, { slots }) { |
|||
const { hasPermission } = usePermission(); |
|||
|
|||
/** |
|||
* Render role button |
|||
*/ |
|||
function renderAuth() { |
|||
const { value } = props; |
|||
if (!value) { |
|||
return getSlot(slots); |
|||
} |
|||
return hasPermission(value) ? getSlot(slots) : null; |
|||
} |
|||
|
|||
return () => { |
|||
// Role-based value control |
|||
return renderAuth(); |
|||
}; |
|||
}, |
|||
}); |
|||
</script> |
|||
@ -1,8 +1,8 @@ |
|||
import { withInstall } from '/@/utils'; |
|||
import basicArrow from './src/BasicArrow.vue'; |
|||
import basicTitle from './src/BasicTitle.vue'; |
|||
import basicHelp from './src/BasicHelp.vue'; |
|||
import { withInstall } from '/@/utils' |
|||
import basicArrow from './src/BasicArrow.vue' |
|||
import basicTitle from './src/BasicTitle.vue' |
|||
import basicHelp from './src/BasicHelp.vue' |
|||
|
|||
export const BasicArrow = withInstall(basicArrow); |
|||
export const BasicTitle = withInstall(basicTitle); |
|||
export const BasicHelp = withInstall(basicHelp); |
|||
export const BasicArrow = withInstall(basicArrow) |
|||
export const BasicTitle = withInstall(basicTitle) |
|||
export const BasicHelp = withInstall(basicHelp) |
|||
|
|||
@ -1,9 +1,9 @@ |
|||
import { withInstall } from '/@/utils'; |
|||
import type { ExtractPropTypes } from 'vue'; |
|||
import button from './src/BasicButton.vue'; |
|||
import popConfirmButton from './src/PopConfirmButton.vue'; |
|||
import { buttonProps } from './src/props'; |
|||
import { withInstall } from '/@/utils' |
|||
import type { ExtractPropTypes } from 'vue' |
|||
import button from './src/BasicButton.vue' |
|||
import popConfirmButton from './src/PopConfirmButton.vue' |
|||
import { buttonProps } from './src/props' |
|||
|
|||
export const Button = withInstall(button); |
|||
export const PopConfirmButton = withInstall(popConfirmButton); |
|||
export declare type ButtonProps = Partial<ExtractPropTypes<typeof buttonProps>>; |
|||
export const Button = withInstall(button) |
|||
export const PopConfirmButton = withInstall(popConfirmButton) |
|||
export declare type ButtonProps = Partial<ExtractPropTypes<typeof buttonProps>> |
|||
|
|||
@ -1,4 +0,0 @@ |
|||
import { withInstall } from '/@/utils'; |
|||
import cardList from './src/CardList.vue'; |
|||
|
|||
export const CardList = withInstall(cardList); |
|||
@ -1,177 +0,0 @@ |
|||
<template> |
|||
<div class="p-2"> |
|||
<div class="p-4 mb-2 bg-white"> |
|||
<BasicForm @register="registerForm" /> |
|||
</div> |
|||
<div class="p-2 bg-white"> |
|||
<List |
|||
:grid="{ gutter: 5, xs: 1, sm: 2, md: 4, lg: 4, xl: 6, xxl: grid }" |
|||
:data-source="data" |
|||
:pagination="paginationProp" |
|||
> |
|||
<template #header> |
|||
<div class="flex justify-end space-x-2" |
|||
><slot name="header"></slot> |
|||
<Tooltip> |
|||
<template #title> |
|||
<div class="w-50">每行显示数量</div |
|||
><Slider |
|||
id="slider" |
|||
v-bind="sliderProp" |
|||
v-model:value="grid" |
|||
@change="sliderChange" |
|||
/></template> |
|||
<Button><TableOutlined /></Button> |
|||
</Tooltip> |
|||
<Tooltip @click="fetch"> |
|||
<template #title>刷新</template> |
|||
<Button><RedoOutlined /></Button> |
|||
</Tooltip> |
|||
</div> |
|||
</template> |
|||
<template #renderItem="{ item }"> |
|||
<ListItem> |
|||
<Card> |
|||
<template #title></template> |
|||
<template #cover> |
|||
<div :class="height"> |
|||
<Image :src="item.imgs[0]" /> |
|||
</div> |
|||
</template> |
|||
<template #actions> |
|||
<!-- <SettingOutlined key="setting" />--> |
|||
<EditOutlined key="edit" /> |
|||
<Dropdown |
|||
:trigger="['hover']" |
|||
:dropMenuList="[ |
|||
{ |
|||
text: '删除', |
|||
event: '1', |
|||
popConfirm: { |
|||
title: '是否确认删除', |
|||
confirm: handleDelete.bind(null, item.id), |
|||
}, |
|||
}, |
|||
]" |
|||
popconfirm |
|||
> |
|||
<EllipsisOutlined key="ellipsis" /> |
|||
</Dropdown> |
|||
</template> |
|||
|
|||
<CardMeta> |
|||
<template #title> |
|||
<TypographyText :content="item.name" :ellipsis="{ tooltip: item.address }" /> |
|||
</template> |
|||
<template #avatar> |
|||
<Avatar :src="item.avatar" /> |
|||
</template> |
|||
<template #description>{{ item.time }}</template> |
|||
</CardMeta> |
|||
</Card> |
|||
</ListItem> |
|||
</template> |
|||
</List> |
|||
</div> |
|||
</div> |
|||
</template> |
|||
<script lang="ts" setup> |
|||
import { computed, onMounted, ref } from 'vue'; |
|||
import { |
|||
EditOutlined, |
|||
EllipsisOutlined, |
|||
RedoOutlined, |
|||
TableOutlined, |
|||
} from '@ant-design/icons-vue'; |
|||
import { List, Card, Image, Typography, Tooltip, Slider, Avatar } from 'ant-design-vue'; |
|||
import { Dropdown } from '/@/components/Dropdown'; |
|||
import { BasicForm, useForm } from '/@/components/Form'; |
|||
import { propTypes } from '/@/utils/propTypes'; |
|||
import { Button } from '/@/components/Button'; |
|||
import { isFunction } from '/@/utils/is'; |
|||
import { useSlider, grid } from './data'; |
|||
const ListItem = List.Item; |
|||
const CardMeta = Card.Meta; |
|||
const TypographyText = Typography.Text; |
|||
// 获取slider属性 |
|||
const sliderProp = computed(() => useSlider(4)); |
|||
// 组件接收参数 |
|||
const props = defineProps({ |
|||
// 请求API的参数 |
|||
params: propTypes.object.def({}), |
|||
//api |
|||
api: propTypes.func, |
|||
}); |
|||
//暴露内部方法 |
|||
const emit = defineEmits(['getMethod', 'delete']); |
|||
//数据 |
|||
const data = ref([]); |
|||
// 切换每行个数 |
|||
// cover图片自适应高度 |
|||
//修改pageSize并重新请求数据 |
|||
|
|||
const height = computed(() => { |
|||
return `h-${120 - grid.value * 6}`; |
|||
}); |
|||
//表单 |
|||
const [registerForm, { validate }] = useForm({ |
|||
schemas: [{ field: 'type', component: 'Input', label: '类型' }], |
|||
labelWidth: 80, |
|||
baseColProps: { span: 6 }, |
|||
actionColOptions: { span: 24 }, |
|||
autoSubmitOnEnter: true, |
|||
submitFunc: handleSubmit, |
|||
}); |
|||
//表单提交 |
|||
async function handleSubmit() { |
|||
const data = await validate(); |
|||
await fetch(data); |
|||
} |
|||
function sliderChange(n) { |
|||
pageSize.value = n * 4; |
|||
fetch(); |
|||
} |
|||
|
|||
// 自动请求并暴露内部方法 |
|||
onMounted(() => { |
|||
fetch(); |
|||
emit('getMethod', fetch); |
|||
}); |
|||
|
|||
async function fetch(p = {}) { |
|||
const { api, params } = props; |
|||
if (api && isFunction(api)) { |
|||
const res = await api({ ...params, page: page.value, pageSize: pageSize.value, ...p }); |
|||
data.value = res.items; |
|||
total.value = res.total; |
|||
} |
|||
} |
|||
//分页相关 |
|||
const page = ref(1); |
|||
const pageSize = ref(36); |
|||
const total = ref(0); |
|||
const paginationProp = ref({ |
|||
showSizeChanger: false, |
|||
showQuickJumper: true, |
|||
pageSize, |
|||
current: page, |
|||
total, |
|||
showTotal: (total) => `总 ${total} 条`, |
|||
onChange: pageChange, |
|||
onShowSizeChange: pageSizeChange, |
|||
}); |
|||
|
|||
function pageChange(p, pz) { |
|||
page.value = p; |
|||
pageSize.value = pz; |
|||
fetch(); |
|||
} |
|||
function pageSizeChange(_current, size) { |
|||
pageSize.value = size; |
|||
fetch(); |
|||
} |
|||
|
|||
async function handleDelete(id) { |
|||
emit('delete', id); |
|||
} |
|||
</script> |
|||
@ -1,25 +0,0 @@ |
|||
import { ref } from 'vue'; |
|||
// 每行个数
|
|||
export const grid = ref(12); |
|||
// slider属性
|
|||
export const useSlider = (min = 6, max = 12) => { |
|||
// 每行显示个数滑动条
|
|||
const getMarks = () => { |
|||
const l = {}; |
|||
for (let i = min; i < max + 1; i++) { |
|||
l[i] = { |
|||
style: { |
|||
color: '#fff', |
|||
}, |
|||
label: i, |
|||
}; |
|||
} |
|||
return l; |
|||
}; |
|||
return { |
|||
min, |
|||
max, |
|||
marks: getMarks(), |
|||
step: 1, |
|||
}; |
|||
}; |
|||
@ -1,4 +0,0 @@ |
|||
import { withInstall } from '/@/utils'; |
|||
import clickOutSide from './src/ClickOutSide.vue'; |
|||
|
|||
export const ClickOutSide = withInstall(clickOutSide); |
|||
@ -1,19 +0,0 @@ |
|||
<template> |
|||
<div ref="wrap"> |
|||
<slot></slot> |
|||
</div> |
|||
</template> |
|||
<script lang="ts" setup> |
|||
import { ref, onMounted } from 'vue'; |
|||
import { onClickOutside } from '@vueuse/core'; |
|||
const emit = defineEmits(['mounted', 'clickOutside']); |
|||
const wrap = ref<ElRef>(null); |
|||
|
|||
onClickOutside(wrap, () => { |
|||
emit('clickOutside'); |
|||
}); |
|||
|
|||
onMounted(() => { |
|||
emit('mounted'); |
|||
}); |
|||
</script> |
|||
@ -1,8 +0,0 @@ |
|||
import { withInstall } from '/@/utils'; |
|||
import codeEditor from './src/CodeEditor.vue'; |
|||
import jsonPreview from './src/json-preview/JsonPreview.vue'; |
|||
|
|||
export const CodeEditor = withInstall(codeEditor); |
|||
export const JsonPreview = withInstall(jsonPreview); |
|||
|
|||
export * from './src/typing'; |
|||
@ -1,54 +0,0 @@ |
|||
<template> |
|||
<div class="h-full"> |
|||
<CodeMirrorEditor |
|||
:value="getValue" |
|||
@change="handleValueChange" |
|||
:mode="mode" |
|||
:readonly="readonly" |
|||
/> |
|||
</div> |
|||
</template> |
|||
<script lang="ts" setup> |
|||
import { computed } from 'vue'; |
|||
import CodeMirrorEditor from './codemirror/CodeMirror.vue'; |
|||
import { isString } from '/@/utils/is'; |
|||
import { MODE } from './typing'; |
|||
|
|||
const props = defineProps({ |
|||
value: { type: [Object, String] as PropType<Record<string, any> | string> }, |
|||
mode: { |
|||
type: String as PropType<MODE>, |
|||
default: MODE.JSON, |
|||
validator(value: any) { |
|||
// 这个值必须匹配下列字符串中的一个 |
|||
return Object.values(MODE).includes(value); |
|||
}, |
|||
}, |
|||
readonly: { type: Boolean }, |
|||
autoFormat: { type: Boolean, default: true }, |
|||
}); |
|||
|
|||
const emit = defineEmits(['change', 'update:value', 'format-error']); |
|||
|
|||
const getValue = computed(() => { |
|||
const { value, mode, autoFormat } = props; |
|||
if (!autoFormat || mode !== MODE.JSON) { |
|||
return value as string; |
|||
} |
|||
let result = value; |
|||
if (isString(value)) { |
|||
try { |
|||
result = JSON.parse(value); |
|||
} catch (e) { |
|||
emit('format-error', value); |
|||
return value as string; |
|||
} |
|||
} |
|||
return JSON.stringify(result, null, 2); |
|||
}); |
|||
|
|||
function handleValueChange(v) { |
|||
emit('update:value', v); |
|||
emit('change', v); |
|||
} |
|||
</script> |
|||
@ -1,113 +0,0 @@ |
|||
<template> |
|||
<div class="relative !h-full w-full overflow-hidden" ref="el"></div> |
|||
</template> |
|||
|
|||
<script lang="ts" setup> |
|||
import { ref, onMounted, onUnmounted, watchEffect, watch, unref, nextTick } from 'vue'; |
|||
import { useDebounceFn } from '@vueuse/core'; |
|||
import { useAppStore } from '/@/store/modules/app'; |
|||
import { useWindowSizeFn } from '/@/hooks/event/useWindowSizeFn'; |
|||
import CodeMirror from 'codemirror'; |
|||
import { MODE } from './../typing'; |
|||
// css |
|||
import './codemirror.css'; |
|||
import 'codemirror/theme/idea.css'; |
|||
import 'codemirror/theme/material-palenight.css'; |
|||
// modes |
|||
import 'codemirror/mode/javascript/javascript'; |
|||
import 'codemirror/mode/css/css'; |
|||
import 'codemirror/mode/htmlmixed/htmlmixed'; |
|||
|
|||
const props = defineProps({ |
|||
mode: { |
|||
type: String as PropType<MODE>, |
|||
default: MODE.JSON, |
|||
validator(value: any) { |
|||
// 这个值必须匹配下列字符串中的一个 |
|||
return Object.values(MODE).includes(value); |
|||
}, |
|||
}, |
|||
value: { type: String, default: '' }, |
|||
readonly: { type: Boolean, default: false }, |
|||
}); |
|||
|
|||
const emit = defineEmits(['change']); |
|||
|
|||
const el = ref(); |
|||
let editor: Nullable<CodeMirror.Editor>; |
|||
|
|||
const debounceRefresh = useDebounceFn(refresh, 100); |
|||
const appStore = useAppStore(); |
|||
|
|||
watch( |
|||
() => props.value, |
|||
async (value) => { |
|||
await nextTick(); |
|||
const oldValue = editor?.getValue(); |
|||
if (value !== oldValue) { |
|||
editor?.setValue(value ? value : ''); |
|||
} |
|||
}, |
|||
{ flush: 'post' }, |
|||
); |
|||
|
|||
watchEffect(() => { |
|||
editor?.setOption('mode', props.mode); |
|||
}); |
|||
|
|||
watch( |
|||
() => appStore.getDarkMode, |
|||
async () => { |
|||
setTheme(); |
|||
}, |
|||
{ |
|||
immediate: true, |
|||
}, |
|||
); |
|||
|
|||
function setTheme() { |
|||
unref(editor)?.setOption( |
|||
'theme', |
|||
appStore.getDarkMode === 'light' ? 'idea' : 'material-palenight', |
|||
); |
|||
} |
|||
|
|||
function refresh() { |
|||
editor?.refresh(); |
|||
} |
|||
|
|||
async function init() { |
|||
const addonOptions = { |
|||
autoCloseBrackets: true, |
|||
autoCloseTags: true, |
|||
foldGutter: true, |
|||
gutters: ['CodeMirror-linenumbers'], |
|||
}; |
|||
|
|||
editor = CodeMirror(el.value!, { |
|||
value: '', |
|||
mode: props.mode, |
|||
readOnly: props.readonly, |
|||
tabSize: 2, |
|||
theme: 'material-palenight', |
|||
lineWrapping: true, |
|||
lineNumbers: true, |
|||
...addonOptions, |
|||
}); |
|||
editor?.setValue(props.value); |
|||
setTheme(); |
|||
editor?.on('change', () => { |
|||
emit('change', editor?.getValue()); |
|||
}); |
|||
} |
|||
|
|||
onMounted(async () => { |
|||
await nextTick(); |
|||
init(); |
|||
useWindowSizeFn(debounceRefresh); |
|||
}); |
|||
|
|||
onUnmounted(() => { |
|||
editor = null; |
|||
}); |
|||
</script> |
|||
@ -1,21 +0,0 @@ |
|||
import CodeMirror from 'codemirror'; |
|||
import './codemirror.css'; |
|||
import 'codemirror/theme/idea.css'; |
|||
import 'codemirror/theme/material-palenight.css'; |
|||
// import 'codemirror/addon/lint/lint.css';
|
|||
|
|||
// modes
|
|||
import 'codemirror/mode/javascript/javascript'; |
|||
import 'codemirror/mode/css/css'; |
|||
import 'codemirror/mode/htmlmixed/htmlmixed'; |
|||
// addons
|
|||
// import 'codemirror/addon/edit/closebrackets';
|
|||
// import 'codemirror/addon/edit/closetag';
|
|||
// import 'codemirror/addon/comment/comment';
|
|||
// import 'codemirror/addon/fold/foldcode';
|
|||
// import 'codemirror/addon/fold/foldgutter';
|
|||
// import 'codemirror/addon/fold/brace-fold';
|
|||
// import 'codemirror/addon/fold/indent-fold';
|
|||
// import 'codemirror/addon/lint/json-lint';
|
|||
// import 'codemirror/addon/fold/comment-fold';
|
|||
export { CodeMirror }; |
|||
@ -1,525 +0,0 @@ |
|||
/* BASICS */ |
|||
|
|||
.CodeMirror { |
|||
--base: #545281; |
|||
--comment: hsl(210deg 25% 60%); |
|||
--keyword: #af4ab1; |
|||
--variable: #0055d1; |
|||
--function: #c25205; |
|||
--string: #2ba46d; |
|||
--number: #c25205; |
|||
--tags: #d00; |
|||
--qualifier: #ff6032; |
|||
--important: var(--string); |
|||
|
|||
position: relative; |
|||
height: auto; |
|||
height: 100%; |
|||
overflow: hidden; |
|||
font-family: var(--font-code); |
|||
background: white; |
|||
direction: ltr; |
|||
} |
|||
|
|||
/* PADDING */ |
|||
|
|||
.CodeMirror-lines { |
|||
min-height: 1px; /* prevents collapsing before first draw */ |
|||
padding: 4px 0; /* Vertical padding around content */ |
|||
cursor: text; |
|||
} |
|||
|
|||
.CodeMirror-scrollbar-filler, |
|||
.CodeMirror-gutter-filler { |
|||
background-color: white; /* The little square between H and V scrollbars */ |
|||
} |
|||
|
|||
/* GUTTER */ |
|||
|
|||
.CodeMirror-gutters { |
|||
position: absolute; |
|||
top: 0; |
|||
left: 0; |
|||
z-index: 3; |
|||
min-height: 100%; |
|||
white-space: nowrap; |
|||
background-color: transparent; |
|||
border-right: 1px solid #ddd; |
|||
} |
|||
|
|||
.CodeMirror-linenumber { |
|||
min-width: 20px; |
|||
padding: 0 3px 0 5px; |
|||
color: var(--comment); |
|||
text-align: right; |
|||
white-space: nowrap; |
|||
opacity: 0.6; |
|||
} |
|||
|
|||
.CodeMirror-guttermarker { |
|||
color: black; |
|||
} |
|||
|
|||
.CodeMirror-guttermarker-subtle { |
|||
color: #999; |
|||
} |
|||
|
|||
/* FOLD GUTTER */ |
|||
|
|||
.CodeMirror-foldmarker { |
|||
font-family: arial; |
|||
line-height: 0.3; |
|||
color: #414141; |
|||
text-shadow: #f96 1px 1px 2px, #f96 -1px -1px 2px, #f96 1px -1px 2px, #f96 -1px 1px 2px; |
|||
cursor: pointer; |
|||
} |
|||
|
|||
.CodeMirror-foldgutter { |
|||
width: 0.7em; |
|||
} |
|||
|
|||
.CodeMirror-foldgutter-open, |
|||
.CodeMirror-foldgutter-folded { |
|||
cursor: pointer; |
|||
} |
|||
|
|||
.CodeMirror-foldgutter-open::after, |
|||
.CodeMirror-foldgutter-folded::after { |
|||
position: relative; |
|||
top: -0.1em; |
|||
display: inline-block; |
|||
font-size: 0.8em; |
|||
content: '>'; |
|||
opacity: 0.8; |
|||
transform: rotate(90deg); |
|||
transition: transform 0.2s; |
|||
} |
|||
|
|||
.CodeMirror-foldgutter-folded::after { |
|||
transform: none; |
|||
} |
|||
|
|||
/* CURSOR */ |
|||
|
|||
.CodeMirror-cursor { |
|||
position: absolute; |
|||
width: 0; |
|||
pointer-events: none; |
|||
border-right: none; |
|||
border-left: 1px solid black; |
|||
} |
|||
|
|||
/* Shown when moving in bi-directional text */ |
|||
.CodeMirror div.CodeMirror-secondarycursor { |
|||
border-left: 1px solid silver; |
|||
} |
|||
|
|||
.cm-fat-cursor .CodeMirror-cursor { |
|||
width: auto; |
|||
background: #7e7; |
|||
border: 0 !important; |
|||
} |
|||
|
|||
.cm-fat-cursor div.CodeMirror-cursors { |
|||
z-index: 1; |
|||
} |
|||
|
|||
.cm-fat-cursor-mark { |
|||
background-color: rgb(20 255 20 / 50%); |
|||
animation: blink 1.06s steps(1) infinite; |
|||
} |
|||
|
|||
.cm-animate-fat-cursor { |
|||
width: auto; |
|||
background-color: #7e7; |
|||
border: 0; |
|||
animation: blink 1.06s steps(1) infinite; |
|||
} |
|||
@keyframes blink { |
|||
50% { |
|||
background-color: transparent; |
|||
} |
|||
} |
|||
@keyframes blink { |
|||
50% { |
|||
background-color: transparent; |
|||
} |
|||
} |
|||
@keyframes blink { |
|||
50% { |
|||
background-color: transparent; |
|||
} |
|||
} |
|||
|
|||
.cm-tab { |
|||
display: inline-block; |
|||
text-decoration: inherit; |
|||
} |
|||
|
|||
.CodeMirror-rulers { |
|||
position: absolute; |
|||
top: -50px; |
|||
right: 0; |
|||
bottom: -20px; |
|||
left: 0; |
|||
overflow: hidden; |
|||
} |
|||
|
|||
.CodeMirror-ruler { |
|||
position: absolute; |
|||
top: 0; |
|||
bottom: 0; |
|||
border-left: 1px solid #ccc; |
|||
} |
|||
|
|||
/* DEFAULT THEME */ |
|||
.cm-s-default.CodeMirror { |
|||
background-color: transparent; |
|||
} |
|||
|
|||
.cm-s-default .cm-header { |
|||
color: blue; |
|||
} |
|||
|
|||
.cm-s-default .cm-quote { |
|||
color: #090; |
|||
} |
|||
|
|||
.cm-negative { |
|||
color: #d44; |
|||
} |
|||
|
|||
.cm-positive { |
|||
color: #292; |
|||
} |
|||
|
|||
.cm-header, |
|||
.cm-strong { |
|||
font-weight: bold; |
|||
} |
|||
|
|||
.cm-em { |
|||
font-style: italic; |
|||
} |
|||
|
|||
.cm-link { |
|||
text-decoration: underline; |
|||
} |
|||
|
|||
.cm-strikethrough { |
|||
text-decoration: line-through; |
|||
} |
|||
|
|||
.cm-s-default .cm-atom, |
|||
.cm-s-default .cm-def, |
|||
.cm-s-default .cm-property, |
|||
.cm-s-default .cm-variable-2, |
|||
.cm-s-default .cm-variable-3, |
|||
.cm-s-default .cm-punctuation { |
|||
color: var(--base); |
|||
} |
|||
|
|||
.cm-s-default .cm-hr, |
|||
.cm-s-default .cm-comment { |
|||
color: var(--comment); |
|||
} |
|||
|
|||
.cm-s-default .cm-attribute, |
|||
.cm-s-default .cm-keyword { |
|||
color: var(--keyword); |
|||
} |
|||
|
|||
.cm-s-default .cm-variable { |
|||
color: var(--variable); |
|||
} |
|||
|
|||
.cm-s-default .cm-bracket, |
|||
.cm-s-default .cm-tag { |
|||
color: var(--tags); |
|||
} |
|||
|
|||
.cm-s-default .cm-number { |
|||
color: var(--number); |
|||
} |
|||
|
|||
.cm-s-default .cm-string, |
|||
.cm-s-default .cm-string-2 { |
|||
color: var(--string); |
|||
} |
|||
|
|||
.cm-s-default .cm-type { |
|||
color: #085; |
|||
} |
|||
|
|||
.cm-s-default .cm-meta { |
|||
color: #555; |
|||
} |
|||
|
|||
.cm-s-default .cm-qualifier { |
|||
color: var(--qualifier); |
|||
} |
|||
|
|||
.cm-s-default .cm-builtin { |
|||
color: #7539ff; |
|||
} |
|||
|
|||
.cm-s-default .cm-link { |
|||
color: var(--flash); |
|||
} |
|||
|
|||
.cm-s-default .cm-error { |
|||
color: #ff008c; |
|||
} |
|||
|
|||
.cm-invalidchar { |
|||
color: #ff008c; |
|||
} |
|||
|
|||
.CodeMirror-composing { |
|||
border-bottom: 2px solid; |
|||
} |
|||
|
|||
/* Default styles for common addons */ |
|||
|
|||
div.CodeMirror span.CodeMirror-matchingbracket { |
|||
color: #0b0; |
|||
} |
|||
|
|||
div.CodeMirror span.CodeMirror-nonmatchingbracket { |
|||
color: #a22; |
|||
} |
|||
|
|||
.CodeMirror-matchingtag { |
|||
background: rgb(255 150 0 / 30%); |
|||
} |
|||
|
|||
.CodeMirror-activeline-background { |
|||
background: #e8f2ff; |
|||
} |
|||
|
|||
/* STOP */ |
|||
|
|||
/* The rest of this file contains styles related to the mechanics of |
|||
the editor. You probably shouldn't touch them. */ |
|||
|
|||
.CodeMirror-scroll { |
|||
position: relative; |
|||
height: 100%; |
|||
padding-bottom: 30px; |
|||
margin-right: -30px; |
|||
|
|||
/* 30px is the magic margin used to hide the element's real scrollbars */ |
|||
|
|||
/* See overflow: hidden in .CodeMirror */ |
|||
margin-bottom: -30px; |
|||
overflow: scroll !important; /* Things will break if this is overridden */ |
|||
outline: none; /* Prevent dragging from highlighting the element */ |
|||
} |
|||
|
|||
.CodeMirror-sizer { |
|||
position: relative; |
|||
margin-bottom: 20px !important; |
|||
border-right: 30px solid transparent; |
|||
} |
|||
|
|||
/* The fake, visible scrollbars. Used to force redraw during scrolling |
|||
before actual scrolling happens, thus preventing shaking and |
|||
flickering artifacts. */ |
|||
.CodeMirror-vscrollbar, |
|||
.CodeMirror-hscrollbar, |
|||
.CodeMirror-scrollbar-filler, |
|||
.CodeMirror-gutter-filler { |
|||
position: absolute; |
|||
z-index: 6; |
|||
display: none; |
|||
} |
|||
|
|||
.CodeMirror-vscrollbar { |
|||
top: 0; |
|||
right: 0; |
|||
overflow-x: hidden; |
|||
overflow-y: scroll; |
|||
} |
|||
|
|||
.CodeMirror-hscrollbar { |
|||
bottom: 0; |
|||
left: 0; |
|||
overflow-x: scroll; |
|||
overflow-y: hidden; |
|||
} |
|||
|
|||
.CodeMirror-scrollbar-filler { |
|||
right: 0; |
|||
bottom: 0; |
|||
} |
|||
|
|||
.CodeMirror-gutter-filler { |
|||
bottom: 0; |
|||
left: 0; |
|||
} |
|||
|
|||
.CodeMirror-gutter { |
|||
display: inline-block; |
|||
height: 100%; |
|||
margin-bottom: -30px; |
|||
white-space: normal; |
|||
vertical-align: top; |
|||
} |
|||
|
|||
.CodeMirror-gutter-wrapper { |
|||
position: absolute; |
|||
z-index: 4; |
|||
background: none !important; |
|||
border: none !important; |
|||
} |
|||
|
|||
.CodeMirror-gutter-background { |
|||
position: absolute; |
|||
top: 0; |
|||
bottom: 0; |
|||
z-index: 4; |
|||
} |
|||
|
|||
.CodeMirror-gutter-elt { |
|||
position: absolute; |
|||
z-index: 4; |
|||
cursor: default; |
|||
} |
|||
|
|||
.CodeMirror-gutter-wrapper ::selection { |
|||
background-color: transparent; |
|||
} |
|||
|
|||
.CodeMirrorwrapper ::selection { |
|||
background-color: transparent; |
|||
} |
|||
|
|||
.CodeMirror pre { |
|||
position: relative; |
|||
z-index: 2; |
|||
padding: 0 4px; /* Horizontal padding of content */ |
|||
margin: 0; |
|||
overflow: visible; |
|||
font-family: inherit; |
|||
font-size: inherit; |
|||
line-height: inherit; |
|||
color: inherit; |
|||
word-wrap: normal; |
|||
white-space: pre; |
|||
background: transparent; |
|||
border-width: 0; |
|||
|
|||
/* Reset some styles that the rest of the page might have set */ |
|||
border-radius: 0; |
|||
-webkit-tap-highlight-color: transparent; |
|||
font-variant-ligatures: contextual; |
|||
} |
|||
|
|||
.CodeMirror-wrap pre { |
|||
word-break: normal; |
|||
word-wrap: break-word; |
|||
white-space: pre-wrap; |
|||
} |
|||
|
|||
.CodeMirror-linebackground { |
|||
position: absolute; |
|||
top: 0; |
|||
right: 0; |
|||
bottom: 0; |
|||
left: 0; |
|||
z-index: 0; |
|||
} |
|||
|
|||
.CodeMirror-linewidget { |
|||
position: relative; |
|||
z-index: 2; |
|||
padding: 0.1px; /* Force widget margins to stay inside of the container */ |
|||
} |
|||
|
|||
.CodeMirror-rtl pre { |
|||
direction: rtl; |
|||
} |
|||
|
|||
.CodeMirror-code { |
|||
outline: none; |
|||
} |
|||
|
|||
/* Force content-box sizing for the elements where we expect it */ |
|||
.CodeMirror-scroll, |
|||
.CodeMirror-sizer, |
|||
.CodeMirror-gutter, |
|||
.CodeMirror-gutters, |
|||
.CodeMirror-linenumber { |
|||
box-sizing: content-box; |
|||
} |
|||
|
|||
.CodeMirror-measure { |
|||
position: absolute; |
|||
width: 100%; |
|||
height: 0; |
|||
overflow: hidden; |
|||
visibility: hidden; |
|||
} |
|||
|
|||
.CodeMirror-measure pre { |
|||
position: static; |
|||
} |
|||
|
|||
div.CodeMirror-cursors { |
|||
position: relative; |
|||
z-index: 3; |
|||
visibility: hidden; |
|||
} |
|||
|
|||
div.CodeMirror-dragcursors { |
|||
visibility: visible; |
|||
} |
|||
|
|||
.CodeMirror-focused div.CodeMirror-cursors { |
|||
visibility: visible; |
|||
} |
|||
|
|||
.CodeMirror-selected { |
|||
background: #d9d9d9; |
|||
} |
|||
|
|||
.CodeMirror-focused .CodeMirror-selected { |
|||
background: #d7d4f0; |
|||
} |
|||
|
|||
.CodeMirror-crosshair { |
|||
cursor: crosshair; |
|||
} |
|||
|
|||
.CodeMirror-line::selection, |
|||
.CodeMirror-line > span::selection, |
|||
.CodeMirror-line > span > span::selection { |
|||
background: #d7d4f0; |
|||
} |
|||
|
|||
.cm-searching { |
|||
background-color: #ffa; |
|||
background-color: rgb(255 255 0 / 40%); |
|||
} |
|||
|
|||
/* Used to force a border model for a node */ |
|||
.cm-force-border { |
|||
padding-right: 0.1px; |
|||
} |
|||
|
|||
@media print { |
|||
/* Hide the cursor when printing */ |
|||
.CodeMirror div.CodeMirror-cursors { |
|||
visibility: hidden; |
|||
} |
|||
} |
|||
|
|||
/* See issue #2901 */ |
|||
.cm-tab-wrap-hack::after { |
|||
content: ''; |
|||
} |
|||
|
|||
/* Help users use markselection to safely style text background */ |
|||
span.CodeMirror-selectedtext { |
|||
background: none; |
|||
} |
|||
@ -1,12 +0,0 @@ |
|||
<template> |
|||
<vue-json-pretty :path="'res'" :deep="3" :showLength="true" :data="data" /> |
|||
</template> |
|||
|
|||
<script lang="ts" setup> |
|||
import VueJsonPretty from 'vue-json-pretty'; |
|||
import 'vue-json-pretty/lib/styles.css'; |
|||
|
|||
defineProps({ |
|||
data: Object, |
|||
}); |
|||
</script> |
|||
@ -1,5 +0,0 @@ |
|||
export enum MODE { |
|||
JSON = 'application/json', |
|||
HTML = 'htmlmixed', |
|||
JS = 'javascript', |
|||
} |
|||
@ -1,10 +1,10 @@ |
|||
import { withInstall } from '/@/utils'; |
|||
import collapseContainer from './src/collapse/CollapseContainer.vue'; |
|||
import scrollContainer from './src/ScrollContainer.vue'; |
|||
import lazyContainer from './src/LazyContainer.vue'; |
|||
import { withInstall } from '/@/utils' |
|||
import collapseContainer from './src/collapse/CollapseContainer.vue' |
|||
import scrollContainer from './src/ScrollContainer.vue' |
|||
import lazyContainer from './src/LazyContainer.vue' |
|||
|
|||
export const CollapseContainer = withInstall(collapseContainer); |
|||
export const ScrollContainer = withInstall(scrollContainer); |
|||
export const LazyContainer = withInstall(lazyContainer); |
|||
export const CollapseContainer = withInstall(collapseContainer) |
|||
export const ScrollContainer = withInstall(scrollContainer) |
|||
export const LazyContainer = withInstall(lazyContainer) |
|||
|
|||
export * from './src/typing'; |
|||
export * from './src/typing' |
|||
|
|||
@ -1,17 +1,17 @@ |
|||
export type ScrollType = 'default' | 'main'; |
|||
export type ScrollType = 'default' | 'main' |
|||
|
|||
export interface CollapseContainerOptions { |
|||
canExpand?: boolean; |
|||
title?: string; |
|||
helpMessage?: Array<any> | string; |
|||
canExpand?: boolean |
|||
title?: string |
|||
helpMessage?: Array<any> | string |
|||
} |
|||
export interface ScrollContainerOptions { |
|||
enableScroll?: boolean; |
|||
type?: ScrollType; |
|||
enableScroll?: boolean |
|||
type?: ScrollType |
|||
} |
|||
|
|||
export type ScrollActionType = RefType<{ |
|||
scrollBottom: () => void; |
|||
getScrollWrap: () => Nullable<HTMLElement>; |
|||
scrollTo: (top: number) => void; |
|||
}>; |
|||
scrollBottom: () => void |
|||
getScrollWrap: () => Nullable<HTMLElement> |
|||
scrollTo: (top: number) => void |
|||
}> |
|||
|
|||
@ -1,3 +0,0 @@ |
|||
export { createContextMenu, destroyContextMenu } from './src/createContextMenu'; |
|||
|
|||
export * from './src/typing'; |
|||
@ -1,209 +0,0 @@ |
|||
<script lang="tsx"> |
|||
import type { ContextMenuItem, ItemContentProps, Axis } from './typing'; |
|||
import type { FunctionalComponent, CSSProperties, PropType } from 'vue'; |
|||
import { defineComponent, nextTick, onMounted, computed, ref, unref, onUnmounted } from 'vue'; |
|||
import Icon from '/@/components/Icon'; |
|||
import { Menu, Divider } from 'ant-design-vue'; |
|||
|
|||
const prefixCls = 'context-menu'; |
|||
|
|||
const props = { |
|||
width: { type: Number, default: 156 }, |
|||
customEvent: { type: Object as PropType<Event>, default: null }, |
|||
styles: { type: Object as PropType<CSSProperties> }, |
|||
showIcon: { type: Boolean, default: true }, |
|||
axis: { |
|||
// The position of the right mouse button click |
|||
type: Object as PropType<Axis>, |
|||
default() { |
|||
return { x: 0, y: 0 }; |
|||
}, |
|||
}, |
|||
items: { |
|||
// The most important list, if not, will not be displayed |
|||
type: Array as PropType<ContextMenuItem[]>, |
|||
default() { |
|||
return []; |
|||
}, |
|||
}, |
|||
}; |
|||
|
|||
const ItemContent: FunctionalComponent<ItemContentProps> = (props) => { |
|||
const { item } = props; |
|||
return ( |
|||
<span |
|||
style="display: inline-block; width: 100%; " |
|||
class="px-4" |
|||
onClick={props.handler.bind(null, item)} |
|||
> |
|||
{props.showIcon && item.icon && <Icon class="mr-2" icon={item.icon} />} |
|||
<span>{item.label}</span> |
|||
</span> |
|||
); |
|||
}; |
|||
|
|||
export default defineComponent({ |
|||
name: 'ContextMenu', |
|||
props, |
|||
setup(props) { |
|||
const wrapRef = ref(null); |
|||
const showRef = ref(false); |
|||
|
|||
const getStyle = computed((): CSSProperties => { |
|||
const { axis, items, styles, width } = props; |
|||
const { x, y } = axis || { x: 0, y: 0 }; |
|||
const menuHeight = (items || []).length * 40; |
|||
const menuWidth = width; |
|||
const body = document.body; |
|||
|
|||
const left = body.clientWidth < x + menuWidth ? x - menuWidth : x; |
|||
const top = body.clientHeight < y + menuHeight ? y - menuHeight : y; |
|||
return { |
|||
...styles, |
|||
position: 'absolute', |
|||
width: `${width}px`, |
|||
left: `${left + 1}px`, |
|||
top: `${top + 1}px`, |
|||
zIndex: 9999, |
|||
}; |
|||
}); |
|||
|
|||
onMounted(() => { |
|||
nextTick(() => (showRef.value = true)); |
|||
}); |
|||
|
|||
onUnmounted(() => { |
|||
const el = unref(wrapRef); |
|||
el && document.body.removeChild(el); |
|||
}); |
|||
|
|||
function handleAction(item: ContextMenuItem, e: MouseEvent) { |
|||
const { handler, disabled } = item; |
|||
if (disabled) { |
|||
return; |
|||
} |
|||
showRef.value = false; |
|||
e?.stopPropagation(); |
|||
e?.preventDefault(); |
|||
handler?.(); |
|||
} |
|||
|
|||
function renderMenuItem(items: ContextMenuItem[]) { |
|||
const visibleItems = items.filter((item) => !item.hidden); |
|||
return visibleItems.map((item) => { |
|||
const { disabled, label, children, divider = false } = item; |
|||
|
|||
const contentProps = { |
|||
item, |
|||
handler: handleAction, |
|||
showIcon: props.showIcon, |
|||
}; |
|||
|
|||
if (!children || children.length === 0) { |
|||
return ( |
|||
<> |
|||
<Menu.Item disabled={disabled} class={`${prefixCls}__item`} key={label}> |
|||
<ItemContent {...contentProps} /> |
|||
</Menu.Item> |
|||
{divider ? <Divider key={`d-${label}`} /> : null} |
|||
</> |
|||
); |
|||
} |
|||
if (!unref(showRef)) return null; |
|||
|
|||
return ( |
|||
<Menu.SubMenu key={label} disabled={disabled} popupClassName={`${prefixCls}__popup`}> |
|||
{{ |
|||
title: () => <ItemContent {...contentProps} />, |
|||
default: () => renderMenuItem(children), |
|||
}} |
|||
</Menu.SubMenu> |
|||
); |
|||
}); |
|||
} |
|||
return () => { |
|||
if (!unref(showRef)) { |
|||
return null; |
|||
} |
|||
const { items } = props; |
|||
return ( |
|||
<div class={prefixCls}> |
|||
<Menu inlineIndent={12} mode="vertical" ref={wrapRef} style={unref(getStyle)}> |
|||
{renderMenuItem(items)} |
|||
</Menu> |
|||
</div> |
|||
); |
|||
}; |
|||
}, |
|||
}); |
|||
</script> |
|||
<style lang="less"> |
|||
@default-height: 42px !important; |
|||
|
|||
@small-height: 36px !important; |
|||
|
|||
@large-height: 36px !important; |
|||
|
|||
.item-style() { |
|||
li { |
|||
display: inline-block; |
|||
width: 100%; |
|||
height: @default-height; |
|||
margin: 0 !important; |
|||
line-height: @default-height; |
|||
|
|||
span { |
|||
line-height: @default-height; |
|||
} |
|||
|
|||
> div { |
|||
margin: 0 !important; |
|||
} |
|||
|
|||
&:not(.ant-menu-item-disabled):hover { |
|||
color: @text-color-base; |
|||
background-color: @item-hover-bg; |
|||
} |
|||
} |
|||
} |
|||
|
|||
.context-menu { |
|||
position: fixed; |
|||
top: 0; |
|||
left: 0; |
|||
z-index: 200; |
|||
display: block; |
|||
width: 156px; |
|||
margin: 0; |
|||
list-style: none; |
|||
background-color: @component-background; |
|||
border: 1px solid rgb(0 0 0 / 8%); |
|||
border-radius: 0.25rem; |
|||
box-shadow: 0 2px 2px 0 rgb(0 0 0 / 14%), 0 3px 1px -2px rgb(0 0 0 / 10%), |
|||
0 1px 5px 0 rgb(0 0 0 / 6%); |
|||
background-clip: padding-box; |
|||
user-select: none; |
|||
|
|||
&__item { |
|||
margin: 0 !important; |
|||
} |
|||
.item-style(); |
|||
|
|||
.ant-divider { |
|||
margin: 0; |
|||
} |
|||
|
|||
&__popup { |
|||
.ant-divider { |
|||
margin: 0; |
|||
} |
|||
|
|||
.item-style(); |
|||
} |
|||
|
|||
.ant-menu-submenu-title, |
|||
.ant-menu-item { |
|||
padding: 0 !important; |
|||
} |
|||
} |
|||
</style> |
|||
@ -1,75 +0,0 @@ |
|||
import contextMenuVue from './ContextMenu.vue'; |
|||
import { isClient } from '/@/utils/is'; |
|||
import { CreateContextOptions, ContextMenuProps } from './typing'; |
|||
import { createVNode, render } from 'vue'; |
|||
|
|||
const menuManager: { |
|||
domList: Element[]; |
|||
resolve: Fn; |
|||
} = { |
|||
domList: [], |
|||
resolve: () => {}, |
|||
}; |
|||
|
|||
export const createContextMenu = function (options: CreateContextOptions) { |
|||
const { event } = options || {}; |
|||
|
|||
event && event?.preventDefault(); |
|||
|
|||
if (!isClient) { |
|||
return; |
|||
} |
|||
return new Promise((resolve) => { |
|||
const body = document.body; |
|||
|
|||
const container = document.createElement('div'); |
|||
const propsData: Partial<ContextMenuProps> = {}; |
|||
if (options.styles) { |
|||
propsData.styles = options.styles; |
|||
} |
|||
|
|||
if (options.items) { |
|||
propsData.items = options.items; |
|||
} |
|||
|
|||
if (options.event) { |
|||
propsData.customEvent = event; |
|||
propsData.axis = { x: event.clientX, y: event.clientY }; |
|||
} |
|||
|
|||
const vm = createVNode(contextMenuVue, propsData); |
|||
render(vm, container); |
|||
|
|||
const handleClick = function () { |
|||
menuManager.resolve(''); |
|||
}; |
|||
|
|||
menuManager.domList.push(container); |
|||
|
|||
const remove = function () { |
|||
menuManager.domList.forEach((dom: Element) => { |
|||
try { |
|||
dom && body.removeChild(dom); |
|||
} catch (error) {} |
|||
}); |
|||
body.removeEventListener('click', handleClick); |
|||
body.removeEventListener('scroll', handleClick); |
|||
}; |
|||
|
|||
menuManager.resolve = function (arg) { |
|||
remove(); |
|||
resolve(arg); |
|||
}; |
|||
remove(); |
|||
body.appendChild(container); |
|||
body.addEventListener('click', handleClick); |
|||
body.addEventListener('scroll', handleClick); |
|||
}); |
|||
}; |
|||
|
|||
export const destroyContextMenu = function () { |
|||
if (menuManager) { |
|||
menuManager.resolve(''); |
|||
menuManager.domList = []; |
|||
} |
|||
}; |
|||
@ -1,36 +0,0 @@ |
|||
export interface Axis { |
|||
x: number; |
|||
y: number; |
|||
} |
|||
|
|||
export interface ContextMenuItem { |
|||
label: string; |
|||
icon?: string; |
|||
hidden?: boolean; |
|||
disabled?: boolean; |
|||
handler?: Fn; |
|||
divider?: boolean; |
|||
children?: ContextMenuItem[]; |
|||
} |
|||
export interface CreateContextOptions { |
|||
event: MouseEvent; |
|||
icon?: string; |
|||
styles?: any; |
|||
items?: ContextMenuItem[]; |
|||
} |
|||
|
|||
export interface ContextMenuProps { |
|||
event?: MouseEvent; |
|||
styles?: any; |
|||
items: ContextMenuItem[]; |
|||
customEvent?: MouseEvent; |
|||
axis?: Axis; |
|||
width?: number; |
|||
showIcon?: boolean; |
|||
} |
|||
|
|||
export interface ItemContentProps { |
|||
showIcon: boolean | undefined; |
|||
item: ContextMenuItem; |
|||
handler: Fn; |
|||
} |
|||
@ -1,6 +1,6 @@ |
|||
import { withInstall } from '/@/utils'; |
|||
import countButton from './src/CountButton.vue'; |
|||
import countdownInput from './src/CountdownInput.vue'; |
|||
import { withInstall } from '/@/utils' |
|||
import countButton from './src/CountButton.vue' |
|||
import countdownInput from './src/CountdownInput.vue' |
|||
|
|||
export const CountdownInput = withInstall(countdownInput); |
|||
export const CountButton = withInstall(countButton); |
|||
export const CountdownInput = withInstall(countdownInput) |
|||
export const CountButton = withInstall(countButton) |
|||
|
|||
@ -1,51 +1,51 @@ |
|||
import { ref, unref } from 'vue'; |
|||
import { tryOnUnmounted } from '@vueuse/core'; |
|||
import { ref, unref } from 'vue' |
|||
import { tryOnUnmounted } from '@vueuse/core' |
|||
|
|||
export function useCountdown(count: number) { |
|||
const currentCount = ref(count); |
|||
const currentCount = ref(count) |
|||
|
|||
const isStart = ref(false); |
|||
const isStart = ref(false) |
|||
|
|||
let timerId: ReturnType<typeof setInterval> | null; |
|||
let timerId: ReturnType<typeof setInterval> | null |
|||
|
|||
function clear() { |
|||
timerId && window.clearInterval(timerId); |
|||
timerId && window.clearInterval(timerId) |
|||
} |
|||
|
|||
function stop() { |
|||
isStart.value = false; |
|||
clear(); |
|||
timerId = null; |
|||
isStart.value = false |
|||
clear() |
|||
timerId = null |
|||
} |
|||
|
|||
function start() { |
|||
if (unref(isStart) || !!timerId) { |
|||
return; |
|||
return |
|||
} |
|||
isStart.value = true; |
|||
isStart.value = true |
|||
timerId = setInterval(() => { |
|||
if (unref(currentCount) === 1) { |
|||
stop(); |
|||
currentCount.value = count; |
|||
stop() |
|||
currentCount.value = count |
|||
} else { |
|||
currentCount.value -= 1; |
|||
currentCount.value -= 1 |
|||
} |
|||
}, 1000); |
|||
}, 1000) |
|||
} |
|||
|
|||
function reset() { |
|||
currentCount.value = count; |
|||
stop(); |
|||
currentCount.value = count |
|||
stop() |
|||
} |
|||
|
|||
function restart() { |
|||
reset(); |
|||
start(); |
|||
reset() |
|||
start() |
|||
} |
|||
|
|||
tryOnUnmounted(() => { |
|||
reset(); |
|||
}); |
|||
reset() |
|||
}) |
|||
|
|||
return { start, reset, restart, clear, stop, currentCount, isStart }; |
|||
return { start, reset, restart, clear, stop, currentCount, isStart } |
|||
} |
|||
|
|||
@ -1,4 +0,0 @@ |
|||
import { withInstall } from '/@/utils'; |
|||
import countTo from './src/CountTo.vue'; |
|||
|
|||
export const CountTo = withInstall(countTo); |
|||
@ -1,110 +0,0 @@ |
|||
<template> |
|||
<span :style="{ color }"> |
|||
{{ value }} |
|||
</span> |
|||
</template> |
|||
<script lang="ts"> |
|||
import { defineComponent, ref, computed, watchEffect, unref, onMounted, watch } from 'vue'; |
|||
import { useTransition, TransitionPresets } from '@vueuse/core'; |
|||
import { isNumber } from '/@/utils/is'; |
|||
|
|||
const props = { |
|||
startVal: { type: Number, default: 0 }, |
|||
endVal: { type: Number, default: 2021 }, |
|||
duration: { type: Number, default: 1500 }, |
|||
autoplay: { type: Boolean, default: true }, |
|||
decimals: { |
|||
type: Number, |
|||
default: 0, |
|||
validator(value: number) { |
|||
return value >= 0; |
|||
}, |
|||
}, |
|||
prefix: { type: String, default: '' }, |
|||
suffix: { type: String, default: '' }, |
|||
separator: { type: String, default: ',' }, |
|||
decimal: { type: String, default: '.' }, |
|||
/** |
|||
* font color |
|||
*/ |
|||
color: { type: String }, |
|||
/** |
|||
* Turn on digital animation |
|||
*/ |
|||
useEasing: { type: Boolean, default: true }, |
|||
/** |
|||
* Digital animation |
|||
*/ |
|||
transition: { type: String, default: 'linear' }, |
|||
}; |
|||
|
|||
export default defineComponent({ |
|||
name: 'CountTo', |
|||
props, |
|||
emits: ['onStarted', 'onFinished'], |
|||
setup(props, { emit }) { |
|||
const source = ref(props.startVal); |
|||
const disabled = ref(false); |
|||
let outputValue = useTransition(source); |
|||
|
|||
const value = computed(() => formatNumber(unref(outputValue))); |
|||
|
|||
watchEffect(() => { |
|||
source.value = props.startVal; |
|||
}); |
|||
|
|||
watch([() => props.startVal, () => props.endVal], () => { |
|||
if (props.autoplay) { |
|||
start(); |
|||
} |
|||
}); |
|||
|
|||
onMounted(() => { |
|||
props.autoplay && start(); |
|||
}); |
|||
|
|||
function start() { |
|||
run(); |
|||
source.value = props.endVal; |
|||
} |
|||
|
|||
function reset() { |
|||
source.value = props.startVal; |
|||
run(); |
|||
} |
|||
|
|||
function run() { |
|||
outputValue = useTransition(source, { |
|||
disabled, |
|||
duration: props.duration, |
|||
onFinished: () => emit('onFinished'), |
|||
onStarted: () => emit('onStarted'), |
|||
...(props.useEasing ? { transition: TransitionPresets[props.transition] } : {}), |
|||
}); |
|||
} |
|||
|
|||
function formatNumber(num: number | string) { |
|||
if (!num && num !== 0) { |
|||
return ''; |
|||
} |
|||
const { decimals, decimal, separator, suffix, prefix } = props; |
|||
num = Number(num).toFixed(decimals); |
|||
num += ''; |
|||
|
|||
const x = num.split('.'); |
|||
let x1 = x[0]; |
|||
const x2 = x.length > 1 ? decimal + x[1] : ''; |
|||
|
|||
const rgx = /(\d+)(\d{3})/; |
|||
if (separator && !isNumber(separator)) { |
|||
while (rgx.test(x1)) { |
|||
x1 = x1.replace(rgx, '$1' + separator + '$2'); |
|||
} |
|||
} |
|||
return prefix + x1 + x2 + suffix; |
|||
} |
|||
|
|||
return { value, start, reset }; |
|||
}, |
|||
}); |
|||
</script> |
|||
@ -1,7 +0,0 @@ |
|||
import { withInstall } from '/@/utils'; |
|||
import cropperImage from './src/Cropper.vue'; |
|||
import avatarCropper from './src/CropperAvatar.vue'; |
|||
|
|||
export * from './src/typing'; |
|||
export const CropperImage = withInstall(cropperImage); |
|||
export const CropperAvatar = withInstall(avatarCropper); |
|||
@ -1,283 +0,0 @@ |
|||
<template> |
|||
<BasicModal |
|||
v-bind="$attrs" |
|||
@register="register" |
|||
:title="t('component.cropper.modalTitle')" |
|||
width="800px" |
|||
:canFullscreen="false" |
|||
@ok="handleOk" |
|||
:okText="t('component.cropper.okText')" |
|||
> |
|||
<div :class="prefixCls"> |
|||
<div :class="`${prefixCls}-left`"> |
|||
<div :class="`${prefixCls}-cropper`"> |
|||
<CropperImage |
|||
v-if="src" |
|||
:src="src" |
|||
height="300px" |
|||
:circled="circled" |
|||
@cropend="handleCropend" |
|||
@ready="handleReady" |
|||
/> |
|||
</div> |
|||
|
|||
<div :class="`${prefixCls}-toolbar`"> |
|||
<Upload :fileList="[]" accept="image/*" :beforeUpload="handleBeforeUpload"> |
|||
<Tooltip :title="t('component.cropper.selectImage')" placement="bottom"> |
|||
<a-button size="small" preIcon="ant-design:upload-outlined" type="primary" /> |
|||
</Tooltip> |
|||
</Upload> |
|||
<Space> |
|||
<Tooltip :title="t('component.cropper.btn_reset')" placement="bottom"> |
|||
<a-button |
|||
type="primary" |
|||
preIcon="ant-design:reload-outlined" |
|||
size="small" |
|||
:disabled="!src" |
|||
@click="handlerToolbar('reset')" |
|||
/> |
|||
</Tooltip> |
|||
<Tooltip :title="t('component.cropper.btn_rotate_left')" placement="bottom"> |
|||
<a-button |
|||
type="primary" |
|||
preIcon="ant-design:rotate-left-outlined" |
|||
size="small" |
|||
:disabled="!src" |
|||
@click="handlerToolbar('rotate', -45)" |
|||
/> |
|||
</Tooltip> |
|||
<Tooltip :title="t('component.cropper.btn_rotate_right')" placement="bottom"> |
|||
<a-button |
|||
type="primary" |
|||
preIcon="ant-design:rotate-right-outlined" |
|||
size="small" |
|||
:disabled="!src" |
|||
@click="handlerToolbar('rotate', 45)" |
|||
/> |
|||
</Tooltip> |
|||
<Tooltip :title="t('component.cropper.btn_scale_x')" placement="bottom"> |
|||
<a-button |
|||
type="primary" |
|||
preIcon="vaadin:arrows-long-h" |
|||
size="small" |
|||
:disabled="!src" |
|||
@click="handlerToolbar('scaleX')" |
|||
/> |
|||
</Tooltip> |
|||
<Tooltip :title="t('component.cropper.btn_scale_y')" placement="bottom"> |
|||
<a-button |
|||
type="primary" |
|||
preIcon="vaadin:arrows-long-v" |
|||
size="small" |
|||
:disabled="!src" |
|||
@click="handlerToolbar('scaleY')" |
|||
/> |
|||
</Tooltip> |
|||
<Tooltip :title="t('component.cropper.btn_zoom_in')" placement="bottom"> |
|||
<a-button |
|||
type="primary" |
|||
preIcon="ant-design:zoom-in-outlined" |
|||
size="small" |
|||
:disabled="!src" |
|||
@click="handlerToolbar('zoom', 0.1)" |
|||
/> |
|||
</Tooltip> |
|||
<Tooltip :title="t('component.cropper.btn_zoom_out')" placement="bottom"> |
|||
<a-button |
|||
type="primary" |
|||
preIcon="ant-design:zoom-out-outlined" |
|||
size="small" |
|||
:disabled="!src" |
|||
@click="handlerToolbar('zoom', -0.1)" |
|||
/> |
|||
</Tooltip> |
|||
</Space> |
|||
</div> |
|||
</div> |
|||
<div :class="`${prefixCls}-right`"> |
|||
<div :class="`${prefixCls}-preview`"> |
|||
<img :src="previewSource" v-if="previewSource" :alt="t('component.cropper.preview')" /> |
|||
</div> |
|||
<template v-if="previewSource"> |
|||
<div :class="`${prefixCls}-group`"> |
|||
<Avatar :src="previewSource" size="large" /> |
|||
<Avatar :src="previewSource" :size="48" /> |
|||
<Avatar :src="previewSource" :size="64" /> |
|||
<Avatar :src="previewSource" :size="80" /> |
|||
</div> |
|||
</template> |
|||
</div> |
|||
</div> |
|||
</BasicModal> |
|||
</template> |
|||
<script lang="ts"> |
|||
import type { CropendResult, Cropper } from './typing'; |
|||
|
|||
import { defineComponent, ref } from 'vue'; |
|||
import CropperImage from './Cropper.vue'; |
|||
import { Space, Upload, Avatar, Tooltip } from 'ant-design-vue'; |
|||
import { useDesign } from '/@/hooks/web/useDesign'; |
|||
import { BasicModal, useModalInner } from '/@/components/Modal'; |
|||
import { dataURLtoBlob } from '/@/utils/file/base64Conver'; |
|||
import { isFunction } from '/@/utils/is'; |
|||
import { useI18n } from '/@/hooks/web/useI18n'; |
|||
|
|||
type apiFunParams = { file: Blob; name: string; filename: string }; |
|||
|
|||
const props = { |
|||
circled: { type: Boolean, default: true }, |
|||
uploadApi: { |
|||
type: Function as PropType<(params: apiFunParams) => Promise<any>>, |
|||
}, |
|||
}; |
|||
|
|||
export default defineComponent({ |
|||
name: 'CropperModal', |
|||
components: { BasicModal, Space, CropperImage, Upload, Avatar, Tooltip }, |
|||
props, |
|||
emits: ['uploadSuccess', 'register'], |
|||
setup(props, { emit }) { |
|||
let filename = ''; |
|||
const src = ref(''); |
|||
const previewSource = ref(''); |
|||
const cropper = ref<Cropper>(); |
|||
let scaleX = 1; |
|||
let scaleY = 1; |
|||
|
|||
const { prefixCls } = useDesign('cropper-am'); |
|||
const [register, { closeModal, setModalProps }] = useModalInner(); |
|||
const { t } = useI18n(); |
|||
|
|||
// Block upload |
|||
function handleBeforeUpload(file: File) { |
|||
const reader = new FileReader(); |
|||
reader.readAsDataURL(file); |
|||
src.value = ''; |
|||
previewSource.value = ''; |
|||
reader.onload = function (e) { |
|||
src.value = (e.target?.result as string) ?? ''; |
|||
filename = file.name; |
|||
}; |
|||
return false; |
|||
} |
|||
|
|||
function handleCropend({ imgBase64 }: CropendResult) { |
|||
previewSource.value = imgBase64; |
|||
} |
|||
|
|||
function handleReady(cropperInstance: Cropper) { |
|||
cropper.value = cropperInstance; |
|||
} |
|||
|
|||
function handlerToolbar(event: string, arg?: number) { |
|||
if (event === 'scaleX') { |
|||
scaleX = arg = scaleX === -1 ? 1 : -1; |
|||
} |
|||
if (event === 'scaleY') { |
|||
scaleY = arg = scaleY === -1 ? 1 : -1; |
|||
} |
|||
cropper?.value?.[event]?.(arg); |
|||
} |
|||
|
|||
async function handleOk() { |
|||
const uploadApi = props.uploadApi; |
|||
if (uploadApi && isFunction(uploadApi)) { |
|||
const blob = dataURLtoBlob(previewSource.value); |
|||
try { |
|||
setModalProps({ confirmLoading: true }); |
|||
const result = await uploadApi({ name: 'file', file: blob, filename }); |
|||
emit('uploadSuccess', { source: previewSource.value, data: result.data }); |
|||
closeModal(); |
|||
} finally { |
|||
setModalProps({ confirmLoading: false }); |
|||
} |
|||
} |
|||
} |
|||
|
|||
return { |
|||
t, |
|||
prefixCls, |
|||
src, |
|||
register, |
|||
previewSource, |
|||
handleBeforeUpload, |
|||
handleCropend, |
|||
handleReady, |
|||
handlerToolbar, |
|||
handleOk, |
|||
}; |
|||
}, |
|||
}); |
|||
</script> |
|||
|
|||
<style lang="less"> |
|||
@prefix-cls: ~'@{namespace}-cropper-am'; |
|||
|
|||
.@{prefix-cls} { |
|||
display: flex; |
|||
|
|||
&-left, |
|||
&-right { |
|||
height: 340px; |
|||
} |
|||
|
|||
&-left { |
|||
width: 55%; |
|||
} |
|||
|
|||
&-right { |
|||
width: 45%; |
|||
} |
|||
|
|||
&-cropper { |
|||
height: 300px; |
|||
background: #eee; |
|||
background-image: linear-gradient( |
|||
45deg, |
|||
rgb(0 0 0 / 25%) 25%, |
|||
transparent 0, |
|||
transparent 75%, |
|||
rgb(0 0 0 / 25%) 0 |
|||
), |
|||
linear-gradient( |
|||
45deg, |
|||
rgb(0 0 0 / 25%) 25%, |
|||
transparent 0, |
|||
transparent 75%, |
|||
rgb(0 0 0 / 25%) 0 |
|||
); |
|||
background-position: 0 0, 12px 12px; |
|||
background-size: 24px 24px; |
|||
} |
|||
|
|||
&-toolbar { |
|||
display: flex; |
|||
justify-content: space-between; |
|||
align-items: center; |
|||
margin-top: 10px; |
|||
} |
|||
|
|||
&-preview { |
|||
width: 220px; |
|||
height: 220px; |
|||
margin: 0 auto; |
|||
overflow: hidden; |
|||
border: 1px solid @border-color-base; |
|||
border-radius: 50%; |
|||
|
|||
img { |
|||
width: 100%; |
|||
height: 100%; |
|||
} |
|||
} |
|||
|
|||
&-group { |
|||
display: flex; |
|||
padding-top: 8px; |
|||
margin-top: 8px; |
|||
border-top: 1px solid @border-color-base; |
|||
justify-content: space-around; |
|||
align-items: center; |
|||
} |
|||
} |
|||
</style> |
|||
@ -1,188 +0,0 @@ |
|||
<template> |
|||
<div :class="getClass" :style="getWrapperStyle"> |
|||
<img |
|||
v-show="isReady" |
|||
ref="imgElRef" |
|||
:src="src" |
|||
:alt="alt" |
|||
:crossorigin="crossorigin" |
|||
:style="getImageStyle" |
|||
/> |
|||
</div> |
|||
</template> |
|||
<script lang="ts"> |
|||
import type { CSSProperties } from 'vue'; |
|||
import { defineComponent, onMounted, ref, unref, computed, onUnmounted } from 'vue'; |
|||
import Cropper from 'cropperjs'; |
|||
import 'cropperjs/dist/cropper.css'; |
|||
import { useDesign } from '/@/hooks/web/useDesign'; |
|||
import { useDebounceFn } from '@vueuse/shared'; |
|||
|
|||
type Options = Cropper.Options; |
|||
|
|||
const defaultOptions: Options = { |
|||
aspectRatio: 1, |
|||
zoomable: true, |
|||
zoomOnTouch: true, |
|||
zoomOnWheel: true, |
|||
cropBoxMovable: true, |
|||
cropBoxResizable: true, |
|||
toggleDragModeOnDblclick: true, |
|||
autoCrop: true, |
|||
background: true, |
|||
highlight: true, |
|||
center: true, |
|||
responsive: true, |
|||
restore: true, |
|||
checkCrossOrigin: true, |
|||
checkOrientation: true, |
|||
scalable: true, |
|||
modal: true, |
|||
guides: true, |
|||
movable: true, |
|||
rotatable: true, |
|||
}; |
|||
|
|||
const props = { |
|||
src: { type: String, required: true }, |
|||
alt: { type: String }, |
|||
circled: { type: Boolean, default: false }, |
|||
realTimePreview: { type: Boolean, default: true }, |
|||
height: { type: [String, Number], default: '360px' }, |
|||
crossorigin: { |
|||
type: String as PropType<'' | 'anonymous' | 'use-credentials' | undefined>, |
|||
default: undefined, |
|||
}, |
|||
imageStyle: { type: Object as PropType<CSSProperties>, default: () => ({}) }, |
|||
options: { type: Object as PropType<Options>, default: () => ({}) }, |
|||
}; |
|||
|
|||
export default defineComponent({ |
|||
name: 'CropperImage', |
|||
props, |
|||
emits: ['cropend', 'ready', 'cropendError'], |
|||
setup(props, { attrs, emit }) { |
|||
const imgElRef = ref<ElRef<HTMLImageElement>>(); |
|||
const cropper = ref<Nullable<Cropper>>(); |
|||
const isReady = ref(false); |
|||
|
|||
const { prefixCls } = useDesign('cropper-image'); |
|||
const debounceRealTimeCroppered = useDebounceFn(realTimeCroppered, 80); |
|||
|
|||
const getImageStyle = computed((): CSSProperties => { |
|||
return { |
|||
height: props.height, |
|||
maxWidth: '100%', |
|||
...props.imageStyle, |
|||
}; |
|||
}); |
|||
|
|||
const getClass = computed(() => { |
|||
return [ |
|||
prefixCls, |
|||
attrs.class, |
|||
{ |
|||
[`${prefixCls}--circled`]: props.circled, |
|||
}, |
|||
]; |
|||
}); |
|||
|
|||
const getWrapperStyle = computed((): CSSProperties => { |
|||
return { height: `${props.height}`.replace(/px/, '') + 'px' }; |
|||
}); |
|||
|
|||
onMounted(init); |
|||
|
|||
onUnmounted(() => { |
|||
cropper.value?.destroy(); |
|||
}); |
|||
|
|||
async function init() { |
|||
const imgEl = unref(imgElRef); |
|||
if (!imgEl) { |
|||
return; |
|||
} |
|||
cropper.value = new Cropper(imgEl, { |
|||
...defaultOptions, |
|||
ready: () => { |
|||
isReady.value = true; |
|||
realTimeCroppered(); |
|||
emit('ready', cropper.value); |
|||
}, |
|||
crop() { |
|||
debounceRealTimeCroppered(); |
|||
}, |
|||
zoom() { |
|||
debounceRealTimeCroppered(); |
|||
}, |
|||
cropmove() { |
|||
debounceRealTimeCroppered(); |
|||
}, |
|||
...props.options, |
|||
}); |
|||
} |
|||
|
|||
// Real-time display preview |
|||
function realTimeCroppered() { |
|||
props.realTimePreview && croppered(); |
|||
} |
|||
|
|||
// event: return base64 and width and height information after cropping |
|||
function croppered() { |
|||
if (!cropper.value) { |
|||
return; |
|||
} |
|||
let imgInfo = cropper.value.getData(); |
|||
const canvas = props.circled ? getRoundedCanvas() : cropper.value.getCroppedCanvas(); |
|||
canvas.toBlob((blob) => { |
|||
if (!blob) { |
|||
return; |
|||
} |
|||
let fileReader: FileReader = new FileReader(); |
|||
fileReader.readAsDataURL(blob); |
|||
fileReader.onloadend = (e) => { |
|||
emit('cropend', { |
|||
imgBase64: e.target?.result ?? '', |
|||
imgInfo, |
|||
}); |
|||
}; |
|||
fileReader.onerror = () => { |
|||
emit('cropendError'); |
|||
}; |
|||
}, 'image/png'); |
|||
} |
|||
|
|||
// Get a circular picture canvas |
|||
function getRoundedCanvas() { |
|||
const sourceCanvas = cropper.value!.getCroppedCanvas(); |
|||
const canvas = document.createElement('canvas'); |
|||
const context = canvas.getContext('2d')!; |
|||
const width = sourceCanvas.width; |
|||
const height = sourceCanvas.height; |
|||
canvas.width = width; |
|||
canvas.height = height; |
|||
context.imageSmoothingEnabled = true; |
|||
context.drawImage(sourceCanvas, 0, 0, width, height); |
|||
context.globalCompositeOperation = 'destination-in'; |
|||
context.beginPath(); |
|||
context.arc(width / 2, height / 2, Math.min(width, height) / 2, 0, 2 * Math.PI, true); |
|||
context.fill(); |
|||
return canvas; |
|||
} |
|||
|
|||
return { getClass, imgElRef, getWrapperStyle, getImageStyle, isReady, croppered }; |
|||
}, |
|||
}); |
|||
</script> |
|||
<style lang="less"> |
|||
@prefix-cls: ~'@{namespace}-cropper-image'; |
|||
|
|||
.@{prefix-cls} { |
|||
&--circled { |
|||
.cropper-view-box, |
|||
.cropper-face { |
|||
border-radius: 50%; |
|||
} |
|||
} |
|||
} |
|||
</style> |
|||
@ -1,161 +0,0 @@ |
|||
<template> |
|||
<div :class="getClass" :style="getStyle"> |
|||
<div :class="`${prefixCls}-image-wrapper`" :style="getImageWrapperStyle" @click="openModal"> |
|||
<div :class="`${prefixCls}-image-mask`" :style="getImageWrapperStyle"> |
|||
<Icon |
|||
icon="ant-design:cloud-upload-outlined" |
|||
:size="getIconWidth" |
|||
:style="getImageWrapperStyle" |
|||
color="#d6d6d6" |
|||
/> |
|||
</div> |
|||
<img :src="sourceValue" v-if="sourceValue" alt="avatar" /> |
|||
</div> |
|||
<a-button |
|||
:class="`${prefixCls}-upload-btn`" |
|||
@click="openModal" |
|||
v-if="showBtn" |
|||
v-bind="btnProps" |
|||
> |
|||
{{ btnText ? btnText : t('component.cropper.selectImage') }} |
|||
</a-button> |
|||
|
|||
<CopperModal |
|||
@register="register" |
|||
@upload-success="handleUploadSuccess" |
|||
:uploadApi="uploadApi" |
|||
:src="sourceValue" |
|||
/> |
|||
</div> |
|||
</template> |
|||
<script lang="ts"> |
|||
import { |
|||
defineComponent, |
|||
computed, |
|||
CSSProperties, |
|||
unref, |
|||
ref, |
|||
watchEffect, |
|||
watch, |
|||
PropType, |
|||
} from 'vue'; |
|||
import CopperModal from './CopperModal.vue'; |
|||
import { useDesign } from '/@/hooks/web/useDesign'; |
|||
import { useModal } from '/@/components/Modal'; |
|||
import { useMessage } from '/@/hooks/web/useMessage'; |
|||
import { useI18n } from '/@/hooks/web/useI18n'; |
|||
import type { ButtonProps } from '/@/components/Button'; |
|||
import Icon from '/@/components/Icon'; |
|||
|
|||
const props = { |
|||
width: { type: [String, Number], default: '200px' }, |
|||
value: { type: String }, |
|||
showBtn: { type: Boolean, default: true }, |
|||
btnProps: { type: Object as PropType<ButtonProps> }, |
|||
btnText: { type: String, default: '' }, |
|||
uploadApi: { type: Function as PropType<({ file: Blob, name: string }) => Promise<void>> }, |
|||
}; |
|||
|
|||
export default defineComponent({ |
|||
name: 'CropperAvatar', |
|||
components: { CopperModal, Icon }, |
|||
props, |
|||
emits: ['update:value', 'change'], |
|||
setup(props, { emit, expose }) { |
|||
const sourceValue = ref(props.value || ''); |
|||
const { prefixCls } = useDesign('cropper-avatar'); |
|||
const [register, { openModal, closeModal }] = useModal(); |
|||
const { createMessage } = useMessage(); |
|||
const { t } = useI18n(); |
|||
|
|||
const getClass = computed(() => [prefixCls]); |
|||
|
|||
const getWidth = computed(() => `${props.width}`.replace(/px/, '') + 'px'); |
|||
|
|||
const getIconWidth = computed(() => parseInt(`${props.width}`.replace(/px/, '')) / 2 + 'px'); |
|||
|
|||
const getStyle = computed((): CSSProperties => ({ width: unref(getWidth) })); |
|||
|
|||
const getImageWrapperStyle = computed( |
|||
(): CSSProperties => ({ width: unref(getWidth), height: unref(getWidth) }), |
|||
); |
|||
|
|||
watchEffect(() => { |
|||
sourceValue.value = props.value || ''; |
|||
}); |
|||
|
|||
watch( |
|||
() => sourceValue.value, |
|||
(v: string) => { |
|||
emit('update:value', v); |
|||
}, |
|||
); |
|||
|
|||
function handleUploadSuccess({ source, data }) { |
|||
sourceValue.value = source; |
|||
emit('change', { source, data }); |
|||
createMessage.success(t('component.cropper.uploadSuccess')); |
|||
} |
|||
|
|||
expose({ openModal: openModal.bind(null, true), closeModal }); |
|||
|
|||
return { |
|||
t, |
|||
prefixCls, |
|||
register, |
|||
openModal: openModal as any, |
|||
getIconWidth, |
|||
sourceValue, |
|||
getClass, |
|||
getImageWrapperStyle, |
|||
getStyle, |
|||
handleUploadSuccess, |
|||
}; |
|||
}, |
|||
}); |
|||
</script> |
|||
|
|||
<style lang="less" scoped> |
|||
@prefix-cls: ~'@{namespace}-cropper-avatar'; |
|||
|
|||
.@{prefix-cls} { |
|||
display: inline-block; |
|||
text-align: center; |
|||
|
|||
&-image-wrapper { |
|||
overflow: hidden; |
|||
cursor: pointer; |
|||
background: @component-background; |
|||
border: 1px solid @border-color-base; |
|||
border-radius: 50%; |
|||
|
|||
img { |
|||
width: 100%; |
|||
} |
|||
} |
|||
|
|||
&-image-mask { |
|||
opacity: 0%; |
|||
position: absolute; |
|||
width: inherit; |
|||
height: inherit; |
|||
border-radius: inherit; |
|||
border: inherit; |
|||
background: rgb(0 0 0 / 40%); |
|||
cursor: pointer; |
|||
transition: opacity 0.4s; |
|||
|
|||
::v-deep(svg) { |
|||
margin: auto; |
|||
} |
|||
} |
|||
|
|||
&-image-mask:hover { |
|||
opacity: 4000%; |
|||
} |
|||
|
|||
&-upload-btn { |
|||
margin: 10px auto; |
|||
} |
|||
} |
|||
</style> |
|||
@ -1,8 +0,0 @@ |
|||
import type Cropper from 'cropperjs'; |
|||
|
|||
export interface CropendResult { |
|||
imgBase64: string; |
|||
imgInfo: Cropper.Data; |
|||
} |
|||
|
|||
export type { Cropper }; |
|||
@ -1,6 +1,6 @@ |
|||
import { withInstall } from '/@/utils'; |
|||
import description from './src/Description.vue'; |
|||
import { withInstall } from '/@/utils' |
|||
import description from './src/Description.vue' |
|||
|
|||
export * from './src/typing'; |
|||
export { useDescription } from './src/useDescription'; |
|||
export const Description = withInstall(description); |
|||
export * from './src/typing' |
|||
export { useDescription } from './src/useDescription' |
|||
export const Description = withInstall(description) |
|||
|
|||
@ -1,50 +1,50 @@ |
|||
import type { VNode, CSSProperties } from 'vue'; |
|||
import type { CollapseContainerOptions } from '/@/components/Container/index'; |
|||
import type { DescriptionsProps } from 'ant-design-vue/es/descriptions/index'; |
|||
import type { VNode, CSSProperties } from 'vue' |
|||
import type { CollapseContainerOptions } from '/@/components/Container/index' |
|||
import type { DescriptionsProps } from 'ant-design-vue/es/descriptions/index' |
|||
|
|||
export interface DescItem { |
|||
labelMinWidth?: number; |
|||
contentMinWidth?: number; |
|||
labelStyle?: CSSProperties; |
|||
field: string; |
|||
label: string | VNode | JSX.Element; |
|||
labelMinWidth?: number |
|||
contentMinWidth?: number |
|||
labelStyle?: CSSProperties |
|||
field: string |
|||
label: string | VNode | JSX.Element |
|||
// Merge column
|
|||
span?: number; |
|||
show?: (...arg: any) => boolean; |
|||
span?: number |
|||
show?: (...arg: any) => boolean |
|||
// render
|
|||
render?: ( |
|||
val: any, |
|||
data: Recordable, |
|||
) => VNode | undefined | JSX.Element | Element | string | number; |
|||
) => VNode | undefined | JSX.Element | Element | string | number |
|||
} |
|||
|
|||
export interface DescriptionProps extends DescriptionsProps { |
|||
// Whether to include the collapse component
|
|||
useCollapse?: boolean; |
|||
useCollapse?: boolean |
|||
/** |
|||
* item configuration |
|||
* @type DescItem |
|||
*/ |
|||
schema: DescItem[]; |
|||
schema: DescItem[] |
|||
/** |
|||
* 数据 |
|||
* @type object |
|||
*/ |
|||
data: Recordable; |
|||
data: Recordable |
|||
/** |
|||
* Built-in CollapseContainer component configuration |
|||
* @type CollapseContainerOptions |
|||
*/ |
|||
collapseOptions?: CollapseContainerOptions; |
|||
collapseOptions?: CollapseContainerOptions |
|||
} |
|||
|
|||
export interface DescInstance { |
|||
setDescProps(descProps: Partial<DescriptionProps>): void; |
|||
setDescProps(descProps: Partial<DescriptionProps>): void |
|||
} |
|||
|
|||
export type Register = (descInstance: DescInstance) => void; |
|||
export type Register = (descInstance: DescInstance) => void |
|||
|
|||
/** |
|||
* @description: |
|||
*/ |
|||
export type UseDescReturnType = [Register, DescInstance]; |
|||
export type UseDescReturnType = [Register, DescInstance] |
|||
|
|||
@ -1,28 +1,28 @@ |
|||
import type { DescriptionProps, DescInstance, UseDescReturnType } from './typing'; |
|||
import { ref, getCurrentInstance, unref } from 'vue'; |
|||
import { isProdMode } from '/@/utils/env'; |
|||
import type { DescriptionProps, DescInstance, UseDescReturnType } from './typing' |
|||
import { ref, getCurrentInstance, unref } from 'vue' |
|||
import { isProdMode } from '/@/utils/env' |
|||
|
|||
export function useDescription(props?: Partial<DescriptionProps>): UseDescReturnType { |
|||
if (!getCurrentInstance()) { |
|||
throw new Error('useDescription() can only be used inside setup() or functional components!'); |
|||
throw new Error('useDescription() can only be used inside setup() or functional components!') |
|||
} |
|||
const desc = ref<Nullable<DescInstance>>(null); |
|||
const loaded = ref(false); |
|||
const desc = ref<Nullable<DescInstance>>(null) |
|||
const loaded = ref(false) |
|||
|
|||
function register(instance: DescInstance) { |
|||
if (unref(loaded) && isProdMode()) { |
|||
return; |
|||
return |
|||
} |
|||
desc.value = instance; |
|||
props && instance.setDescProps(props); |
|||
loaded.value = true; |
|||
desc.value = instance |
|||
props && instance.setDescProps(props) |
|||
loaded.value = true |
|||
} |
|||
|
|||
const methods: DescInstance = { |
|||
setDescProps: (descProps: Partial<DescriptionProps>): void => { |
|||
unref(desc)?.setDescProps(descProps); |
|||
unref(desc)?.setDescProps(descProps) |
|||
}, |
|||
}; |
|||
} |
|||
|
|||
return [register, methods]; |
|||
return [register, methods] |
|||
} |
|||
|
|||
@ -1,6 +1,6 @@ |
|||
import { withInstall } from '/@/utils'; |
|||
import basicDrawer from './src/BasicDrawer.vue'; |
|||
import { withInstall } from '/@/utils' |
|||
import basicDrawer from './src/BasicDrawer.vue' |
|||
|
|||
export const BasicDrawer = withInstall(basicDrawer); |
|||
export * from './src/typing'; |
|||
export { useDrawer, useDrawerInner } from './src/useDrawer'; |
|||
export const BasicDrawer = withInstall(basicDrawer) |
|||
export * from './src/typing' |
|||
export { useDrawer, useDrawerInner } from './src/useDrawer' |
|||
|
|||
@ -1,193 +1,193 @@ |
|||
import type { ButtonProps } from 'ant-design-vue/lib/button/buttonTypes'; |
|||
import type { CSSProperties, VNodeChild, ComputedRef } from 'vue'; |
|||
import type { ScrollContainerOptions } from '/@/components/Container/index'; |
|||
import type { ButtonProps } from 'ant-design-vue/lib/button/buttonTypes' |
|||
import type { CSSProperties, VNodeChild, ComputedRef } from 'vue' |
|||
import type { ScrollContainerOptions } from '/@/components/Container/index' |
|||
|
|||
export interface DrawerInstance { |
|||
setDrawerProps: (props: Partial<DrawerProps> | boolean) => void; |
|||
emitVisible?: (visible: boolean, uid: number) => void; |
|||
setDrawerProps: (props: Partial<DrawerProps> | boolean) => void |
|||
emitVisible?: (visible: boolean, uid: number) => void |
|||
} |
|||
|
|||
export interface ReturnMethods extends DrawerInstance { |
|||
openDrawer: <T = any>(visible?: boolean, data?: T, openOnSet?: boolean) => void; |
|||
closeDrawer: () => void; |
|||
getVisible?: ComputedRef<boolean>; |
|||
openDrawer: <T = any>(visible?: boolean, data?: T, openOnSet?: boolean) => void |
|||
closeDrawer: () => void |
|||
getVisible?: ComputedRef<boolean> |
|||
} |
|||
|
|||
export type RegisterFn = (drawerInstance: DrawerInstance, uuid?: string) => void; |
|||
export type RegisterFn = (drawerInstance: DrawerInstance, uuid?: string) => void |
|||
|
|||
export interface ReturnInnerMethods extends DrawerInstance { |
|||
closeDrawer: () => void; |
|||
changeLoading: (loading: boolean) => void; |
|||
changeOkLoading: (loading: boolean) => void; |
|||
getVisible?: ComputedRef<boolean>; |
|||
closeDrawer: () => void |
|||
changeLoading: (loading: boolean) => void |
|||
changeOkLoading: (loading: boolean) => void |
|||
getVisible?: ComputedRef<boolean> |
|||
} |
|||
|
|||
export type UseDrawerReturnType = [RegisterFn, ReturnMethods]; |
|||
export type UseDrawerReturnType = [RegisterFn, ReturnMethods] |
|||
|
|||
export type UseDrawerInnerReturnType = [RegisterFn, ReturnInnerMethods]; |
|||
export type UseDrawerInnerReturnType = [RegisterFn, ReturnInnerMethods] |
|||
|
|||
export interface DrawerFooterProps { |
|||
showOkBtn: boolean; |
|||
showCancelBtn: boolean; |
|||
showOkBtn: boolean |
|||
showCancelBtn: boolean |
|||
/** |
|||
* Text of the Cancel button |
|||
* @default 'cancel' |
|||
* @type string |
|||
*/ |
|||
cancelText: string; |
|||
cancelText: string |
|||
/** |
|||
* Text of the OK button |
|||
* @default 'OK' |
|||
* @type string |
|||
*/ |
|||
okText: string; |
|||
okText: string |
|||
|
|||
/** |
|||
* Button type of the OK button |
|||
* @default 'primary' |
|||
* @type string |
|||
*/ |
|||
okType: 'primary' | 'danger' | 'dashed' | 'ghost' | 'default'; |
|||
okType: 'primary' | 'danger' | 'dashed' | 'ghost' | 'default' |
|||
/** |
|||
* The ok button props, follow jsx rules |
|||
* @type object |
|||
*/ |
|||
okButtonProps: { props: ButtonProps; on: {} }; |
|||
okButtonProps: { props: ButtonProps; on: {} } |
|||
|
|||
/** |
|||
* The cancel button props, follow jsx rules |
|||
* @type object |
|||
*/ |
|||
cancelButtonProps: { props: ButtonProps; on: {} }; |
|||
cancelButtonProps: { props: ButtonProps; on: {} } |
|||
/** |
|||
* Whether to apply loading visual effect for OK button or not |
|||
* @default false |
|||
* @type boolean |
|||
*/ |
|||
confirmLoading: boolean; |
|||
confirmLoading: boolean |
|||
|
|||
showFooter: boolean; |
|||
footerHeight: string | number; |
|||
showFooter: boolean |
|||
footerHeight: string | number |
|||
} |
|||
export interface DrawerProps extends DrawerFooterProps { |
|||
isDetail?: boolean; |
|||
loading?: boolean; |
|||
showDetailBack?: boolean; |
|||
visible?: boolean; |
|||
isDetail?: boolean |
|||
loading?: boolean |
|||
showDetailBack?: boolean |
|||
visible?: boolean |
|||
/** |
|||
* Built-in ScrollContainer component configuration |
|||
* @type ScrollContainerOptions |
|||
*/ |
|||
scrollOptions?: ScrollContainerOptions; |
|||
closeFunc?: () => Promise<any>; |
|||
triggerWindowResize?: boolean; |
|||
scrollOptions?: ScrollContainerOptions |
|||
closeFunc?: () => Promise<any> |
|||
triggerWindowResize?: boolean |
|||
/** |
|||
* Whether a close (x) button is visible on top right of the Drawer dialog or not. |
|||
* @default true |
|||
* @type boolean |
|||
*/ |
|||
closable?: boolean; |
|||
closable?: boolean |
|||
|
|||
/** |
|||
* Whether to unmount child components on closing drawer or not. |
|||
* @default false |
|||
* @type boolean |
|||
*/ |
|||
destroyOnClose?: boolean; |
|||
destroyOnClose?: boolean |
|||
|
|||
/** |
|||
* Return the mounted node for Drawer. |
|||
* @default 'body' |
|||
* @type any ( HTMLElement| () => HTMLElement | string) |
|||
*/ |
|||
getContainer?: () => HTMLElement | string; |
|||
getContainer?: () => HTMLElement | string |
|||
|
|||
/** |
|||
* Whether to show mask or not. |
|||
* @default true |
|||
* @type boolean |
|||
*/ |
|||
mask?: boolean; |
|||
mask?: boolean |
|||
|
|||
/** |
|||
* Clicking on the mask (area outside the Drawer) to close the Drawer or not. |
|||
* @default true |
|||
* @type boolean |
|||
*/ |
|||
maskClosable?: boolean; |
|||
maskClosable?: boolean |
|||
|
|||
/** |
|||
* Style for Drawer's mask element. |
|||
* @default {} |
|||
* @type object |
|||
*/ |
|||
maskStyle?: CSSProperties; |
|||
maskStyle?: CSSProperties |
|||
|
|||
/** |
|||
* The title for Drawer. |
|||
* @type any (string | slot) |
|||
*/ |
|||
title?: VNodeChild | JSX.Element; |
|||
title?: VNodeChild | JSX.Element |
|||
/** |
|||
* The class name of the container of the Drawer dialog. |
|||
* @type string |
|||
*/ |
|||
wrapClassName?: string; |
|||
class?: string; |
|||
wrapClassName?: string |
|||
class?: string |
|||
/** |
|||
* Style of wrapper element which **contains mask** compare to `drawerStyle` |
|||
* @type object |
|||
*/ |
|||
wrapStyle?: CSSProperties; |
|||
wrapStyle?: CSSProperties |
|||
|
|||
/** |
|||
* Style of the popup layer element |
|||
* @type object |
|||
*/ |
|||
drawerStyle?: CSSProperties; |
|||
drawerStyle?: CSSProperties |
|||
|
|||
/** |
|||
* Style of floating layer, typically used for adjusting its position. |
|||
* @type object |
|||
*/ |
|||
bodyStyle?: CSSProperties; |
|||
headerStyle?: CSSProperties; |
|||
bodyStyle?: CSSProperties |
|||
headerStyle?: CSSProperties |
|||
|
|||
/** |
|||
* Width of the Drawer dialog. |
|||
* @default 256 |
|||
* @type string | number |
|||
*/ |
|||
width?: string | number; |
|||
width?: string | number |
|||
|
|||
/** |
|||
* placement is top or bottom, height of the Drawer dialog. |
|||
* @type string | number |
|||
*/ |
|||
height?: string | number; |
|||
height?: string | number |
|||
|
|||
/** |
|||
* The z-index of the Drawer. |
|||
* @default 1000 |
|||
* @type number |
|||
*/ |
|||
zIndex?: number; |
|||
zIndex?: number |
|||
|
|||
/** |
|||
* The placement of the Drawer. |
|||
* @default 'right' |
|||
* @type string |
|||
*/ |
|||
placement?: 'top' | 'right' | 'bottom' | 'left'; |
|||
afterVisibleChange?: (visible?: boolean) => void; |
|||
keyboard?: boolean; |
|||
placement?: 'top' | 'right' | 'bottom' | 'left' |
|||
afterVisibleChange?: (visible?: boolean) => void |
|||
keyboard?: boolean |
|||
/** |
|||
* Specify a callback that will be called when a user clicks mask, close button or Cancel button. |
|||
*/ |
|||
onClose?: (e?: Event) => void; |
|||
onClose?: (e?: Event) => void |
|||
} |
|||
export interface DrawerActionType { |
|||
scrollBottom: () => void; |
|||
scrollTo: (to: number) => void; |
|||
getScrollWrap: () => Element | null; |
|||
scrollBottom: () => void |
|||
scrollTo: (to: number) => void |
|||
getScrollWrap: () => Element | null |
|||
} |
|||
|
|||
@ -1,5 +1,5 @@ |
|||
import { withInstall } from '/@/utils'; |
|||
import dropdown from './src/Dropdown.vue'; |
|||
import { withInstall } from '/@/utils' |
|||
import dropdown from './src/Dropdown.vue' |
|||
|
|||
export * from './src/typing'; |
|||
export const Dropdown = withInstall(dropdown); |
|||
export * from './src/typing' |
|||
export const Dropdown = withInstall(dropdown) |
|||
|
|||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue