committed by
GitHub
330 changed files with 13377 additions and 4984 deletions
@ -1,6 +0,0 @@ |
|||
echo Start running commit-msg hook... |
|||
|
|||
# Check whether the git commit information is standardized |
|||
pnpm exec commitlint --edit "$1" |
|||
|
|||
echo Run commit-msg hook done. |
|||
@ -1,3 +0,0 @@ |
|||
# 每次 git pull 之后, 安装依赖 |
|||
|
|||
pnpm install |
|||
@ -1,7 +0,0 @@ |
|||
# update `.vscode/vben-admin.code-workspace` file |
|||
pnpm vsh code-workspace --auto-commit |
|||
|
|||
# Format and submit code according to lintstagedrc.js configuration |
|||
pnpm exec lint-staged |
|||
|
|||
echo Run pre-commit hook done. |
|||
@ -1,20 +0,0 @@ |
|||
export default { |
|||
'*.md': ['prettier --cache --ignore-unknown --write'], |
|||
'*.vue': [ |
|||
'prettier --write', |
|||
'eslint --cache --fix', |
|||
'stylelint --fix --allow-empty-input', |
|||
], |
|||
'*.{js,jsx,ts,tsx}': [ |
|||
'prettier --cache --ignore-unknown --write', |
|||
'eslint --cache --fix', |
|||
], |
|||
'*.{scss,less,styl,html,vue,css}': [ |
|||
'prettier --cache --ignore-unknown --write', |
|||
'stylelint --fix --allow-empty-input', |
|||
], |
|||
'package.json': ['prettier --cache --write'], |
|||
'{!(package)*.json,*.code-snippets,.!(browserslist)*rc}': [ |
|||
'prettier --cache --write--parser json', |
|||
], |
|||
}; |
|||
@ -0,0 +1,30 @@ |
|||
{ |
|||
"recommendations": [ |
|||
// Vue 3 的语言支持 |
|||
"Vue.volar", |
|||
// 将 ESLint JavaScript 集成到 VS Code 中。 |
|||
"dbaeumer.vscode-eslint", |
|||
// Visual Studio Code 的官方 Stylelint 扩展 |
|||
"stylelint.vscode-stylelint", |
|||
// 使用 Prettier 的代码格式化程序 |
|||
"esbenp.prettier-vscode", |
|||
// 支持 dotenv 文件语法 |
|||
"mikestead.dotenv", |
|||
// 源代码的拼写检查器 |
|||
"streetsidesoftware.code-spell-checker", |
|||
// Tailwind CSS 的官方 VS Code 插件 |
|||
"bradlc.vscode-tailwindcss", |
|||
// iconify 图标插件 |
|||
"antfu.iconify", |
|||
// i18n 插件 |
|||
"Lokalise.i18n-ally", |
|||
// CSS 变量提示 |
|||
"vunguyentuan.vscode-css-variables", |
|||
// 在 package.json 中显示 PNPM catalog 的版本 |
|||
"antfu.pnpm-catalog-lens" |
|||
], |
|||
"unwantedRecommendations": [ |
|||
// 和 volar 冲突 |
|||
"octref.vetur" |
|||
] |
|||
} |
|||
@ -0,0 +1,37 @@ |
|||
{ |
|||
"import": { |
|||
"scope": "javascript,typescript", |
|||
"prefix": "im", |
|||
"body": ["import { $2 } from '$1';"], |
|||
"description": "Import a module", |
|||
}, |
|||
"export-all": { |
|||
"scope": "javascript,typescript", |
|||
"prefix": "ex", |
|||
"body": ["export * from '$1';"], |
|||
"description": "Export a module", |
|||
}, |
|||
"vue-script-setup": { |
|||
"scope": "vue", |
|||
"prefix": "<sc", |
|||
"body": [ |
|||
"<script setup lang=\"ts\">", |
|||
"const props = defineProps<{", |
|||
" modelValue?: boolean,", |
|||
"}>()", |
|||
"$1", |
|||
"</script>", |
|||
"", |
|||
"<template>", |
|||
" <div>", |
|||
" <slot/>", |
|||
" </div>", |
|||
"</template>", |
|||
], |
|||
}, |
|||
"vue-computed": { |
|||
"scope": "javascript,typescript,vue", |
|||
"prefix": "com", |
|||
"body": ["computed(() => { $1 })"], |
|||
}, |
|||
} |
|||
@ -0,0 +1,51 @@ |
|||
{ |
|||
"$schema": "https://json.schemastore.org/launchsettings.json", |
|||
"version": "0.2.0", |
|||
"configurations": [ |
|||
{ |
|||
"type": "chrome", |
|||
"name": "vben admin playground dev", |
|||
"request": "launch", |
|||
"url": "http://localhost:5555", |
|||
"env": { "NODE_ENV": "development" }, |
|||
"sourceMaps": true, |
|||
"webRoot": "${workspaceFolder}/playground" |
|||
}, |
|||
{ |
|||
"type": "chrome", |
|||
"name": "vben abp antd dev", |
|||
"request": "launch", |
|||
"url": "http://localhost:5666", |
|||
"env": { "NODE_ENV": "development" }, |
|||
"sourceMaps": true, |
|||
"webRoot": "${workspaceFolder}/apps/app-antd" |
|||
}, |
|||
{ |
|||
"type": "chrome", |
|||
"name": "vben admin antd dev", |
|||
"request": "launch", |
|||
"url": "http://localhost:5666", |
|||
"env": { "NODE_ENV": "development" }, |
|||
"sourceMaps": true, |
|||
"webRoot": "${workspaceFolder}/apps/web-antd" |
|||
}, |
|||
{ |
|||
"type": "chrome", |
|||
"name": "vben admin ele dev", |
|||
"request": "launch", |
|||
"url": "http://localhost:5777", |
|||
"env": { "NODE_ENV": "development" }, |
|||
"sourceMaps": true, |
|||
"webRoot": "${workspaceFolder}/apps/web-ele" |
|||
}, |
|||
{ |
|||
"type": "chrome", |
|||
"name": "vben admin naive dev", |
|||
"request": "launch", |
|||
"url": "http://localhost:5888", |
|||
"env": { "NODE_ENV": "development" }, |
|||
"sourceMaps": true, |
|||
"webRoot": "${workspaceFolder}/apps/web-naive" |
|||
} |
|||
] |
|||
} |
|||
@ -0,0 +1,241 @@ |
|||
{ |
|||
"tailwindCSS.experimental.configFile": "internal/tailwind-config/src/index.ts", |
|||
// workbench |
|||
"workbench.list.smoothScrolling": true, |
|||
"workbench.startupEditor": "newUntitledFile", |
|||
"workbench.tree.indent": 10, |
|||
"workbench.editor.highlightModifiedTabs": true, |
|||
"workbench.editor.closeOnFileDelete": true, |
|||
"workbench.editor.limit.enabled": true, |
|||
"workbench.editor.limit.perEditorGroup": true, |
|||
"workbench.editor.limit.value": 5, |
|||
|
|||
// editor |
|||
"editor.tabSize": 2, |
|||
"editor.detectIndentation": false, |
|||
"editor.cursorBlinking": "expand", |
|||
"editor.largeFileOptimizations": true, |
|||
"editor.accessibilitySupport": "off", |
|||
"editor.cursorSmoothCaretAnimation": "on", |
|||
"editor.guides.bracketPairs": "active", |
|||
"editor.inlineSuggest.enabled": true, |
|||
"editor.suggestSelection": "recentlyUsedByPrefix", |
|||
"editor.acceptSuggestionOnEnter": "smart", |
|||
"editor.suggest.snippetsPreventQuickSuggestions": false, |
|||
"editor.stickyScroll.enabled": true, |
|||
"editor.hover.sticky": true, |
|||
"editor.suggest.insertMode": "replace", |
|||
"editor.bracketPairColorization.enabled": true, |
|||
"editor.autoClosingBrackets": "beforeWhitespace", |
|||
"editor.autoClosingDelete": "always", |
|||
"editor.autoClosingOvertype": "always", |
|||
"editor.autoClosingQuotes": "beforeWhitespace", |
|||
"editor.wordSeparators": "`~!@#%^&*()=+[{]}\\|;:'\",.<>/?", |
|||
"editor.codeActionsOnSave": { |
|||
"source.fixAll.eslint": "explicit", |
|||
"source.fixAll.stylelint": "explicit", |
|||
"source.organizeImports": "never" |
|||
}, |
|||
"editor.defaultFormatter": "esbenp.prettier-vscode", |
|||
"[html]": { |
|||
"editor.defaultFormatter": "esbenp.prettier-vscode" |
|||
}, |
|||
"[css]": { |
|||
"editor.defaultFormatter": "esbenp.prettier-vscode" |
|||
}, |
|||
"[scss]": { |
|||
"editor.defaultFormatter": "esbenp.prettier-vscode" |
|||
}, |
|||
"[javascript]": { |
|||
"editor.defaultFormatter": "esbenp.prettier-vscode" |
|||
}, |
|||
"[typescript]": { |
|||
"editor.defaultFormatter": "esbenp.prettier-vscode" |
|||
}, |
|||
"[json]": { |
|||
"editor.defaultFormatter": "esbenp.prettier-vscode" |
|||
}, |
|||
"[markdown]": { |
|||
"editor.defaultFormatter": "esbenp.prettier-vscode" |
|||
}, |
|||
"[jsonc]": { |
|||
"editor.defaultFormatter": "esbenp.prettier-vscode" |
|||
}, |
|||
"[vue]": { |
|||
"editor.defaultFormatter": "esbenp.prettier-vscode" |
|||
}, |
|||
// extensions |
|||
"extensions.ignoreRecommendations": true, |
|||
|
|||
// terminal |
|||
"terminal.integrated.cursorBlinking": true, |
|||
"terminal.integrated.persistentSessionReviveProcess": "never", |
|||
"terminal.integrated.tabs.enabled": true, |
|||
"terminal.integrated.scrollback": 10000, |
|||
"terminal.integrated.stickyScroll.enabled": true, |
|||
|
|||
// files |
|||
"files.eol": "\n", |
|||
"files.insertFinalNewline": true, |
|||
"files.simpleDialog.enable": true, |
|||
"files.associations": { |
|||
"*.ejs": "html", |
|||
"*.art": "html", |
|||
"**/tsconfig.json": "jsonc", |
|||
"*.json": "jsonc", |
|||
"package.json": "json" |
|||
}, |
|||
|
|||
"files.exclude": { |
|||
"**/.eslintcache": true, |
|||
"**/bower_components": true, |
|||
"**/.turbo": true, |
|||
"**/.idea": true, |
|||
"**/.vitepress": true, |
|||
"**/tmp": true, |
|||
"**/.git": true, |
|||
"**/.svn": true, |
|||
"**/.hg": true, |
|||
"**/CVS": true, |
|||
"**/.stylelintcache": true, |
|||
"**/.DS_Store": true, |
|||
"**/vite.config.mts.*": true, |
|||
"**/tea.yaml": true |
|||
}, |
|||
"files.watcherExclude": { |
|||
"**/.git/objects/**": true, |
|||
"**/.git/subtree-cache/**": true, |
|||
"**/.vscode/**": true, |
|||
"**/node_modules/**": true, |
|||
"**/tmp/**": true, |
|||
"**/bower_components/**": true, |
|||
"**/dist/**": true, |
|||
"**/yarn.lock": true |
|||
}, |
|||
|
|||
"typescript.tsserver.exclude": ["**/node_modules", "**/dist", "**/.turbo"], |
|||
|
|||
// search |
|||
"search.searchEditor.singleClickBehaviour": "peekDefinition", |
|||
"search.followSymlinks": false, |
|||
// 在使用搜索功能时,将这些文件夹/文件排除在外 |
|||
"search.exclude": { |
|||
"**/node_modules": true, |
|||
"**/*.log": true, |
|||
"**/*.log*": true, |
|||
"**/bower_components": true, |
|||
"**/dist": true, |
|||
"**/elehukouben": true, |
|||
"**/.git": true, |
|||
"**/.github": true, |
|||
"**/.gitignore": true, |
|||
"**/.svn": true, |
|||
"**/.DS_Store": true, |
|||
"**/.vitepress/cache": true, |
|||
"**/.idea": true, |
|||
"**/.vscode": false, |
|||
"**/.yarn": true, |
|||
"**/tmp": true, |
|||
"*.xml": true, |
|||
"out": true, |
|||
"dist": true, |
|||
"node_modules": true, |
|||
"CHANGELOG.md": true, |
|||
"**/pnpm-lock.yaml": true, |
|||
"**/yarn.lock": true |
|||
}, |
|||
|
|||
"debug.onTaskErrors": "debugAnyway", |
|||
"diffEditor.ignoreTrimWhitespace": false, |
|||
"npm.packageManager": "pnpm", |
|||
|
|||
"css.validate": false, |
|||
"less.validate": false, |
|||
"scss.validate": false, |
|||
|
|||
// extension |
|||
"emmet.showSuggestionsAsSnippets": true, |
|||
"emmet.triggerExpansionOnTab": false, |
|||
|
|||
"errorLens.enabledDiagnosticLevels": ["warning", "error"], |
|||
"errorLens.excludeBySource": ["cSpell", "Grammarly", "eslint"], |
|||
|
|||
"stylelint.enable": true, |
|||
"stylelint.packageManager": "pnpm", |
|||
"stylelint.validate": ["css", "less", "postcss", "scss", "vue"], |
|||
"stylelint.customSyntax": "postcss-html", |
|||
"stylelint.snippet": ["css", "less", "postcss", "scss", "vue"], |
|||
|
|||
"typescript.inlayHints.enumMemberValues.enabled": true, |
|||
"typescript.preferences.preferTypeOnlyAutoImports": true, |
|||
"typescript.preferences.includePackageJsonAutoImports": "on", |
|||
|
|||
"eslint.validate": [ |
|||
"javascript", |
|||
"typescript", |
|||
"javascriptreact", |
|||
"typescriptreact", |
|||
"vue", |
|||
"html", |
|||
"markdown", |
|||
"json", |
|||
"jsonc", |
|||
"json5" |
|||
], |
|||
|
|||
"tailwindCSS.experimental.classRegex": [ |
|||
["cva\\(([^)]*)\\)", "[\"'`]([^\"'`]*).*?[\"'`]"] |
|||
], |
|||
|
|||
"github.copilot.enable": { |
|||
"*": true, |
|||
"markdown": true, |
|||
"plaintext": false, |
|||
"yaml": false |
|||
}, |
|||
|
|||
"cssVariables.lookupFiles": ["packages/core/base/design/src/**/*.css"], |
|||
|
|||
"i18n-ally.localesPaths": [ |
|||
"packages/locales/src/langs", |
|||
"playground/src/locales/langs", |
|||
"apps/*/src/locales/langs" |
|||
], |
|||
"i18n-ally.pathMatcher": "{locale}/{namespace}.{ext}", |
|||
"i18n-ally.enabledParsers": ["json"], |
|||
"i18n-ally.sourceLanguage": "en", |
|||
"i18n-ally.displayLanguage": "zh-CN", |
|||
"i18n-ally.enabledFrameworks": ["vue", "react"], |
|||
"i18n-ally.keystyle": "nested", |
|||
"i18n-ally.sortKeys": true, |
|||
"i18n-ally.namespace": true, |
|||
|
|||
// 控制相关文件嵌套展示 |
|||
"explorer.fileNesting.enabled": true, |
|||
"explorer.fileNesting.expand": false, |
|||
"explorer.fileNesting.patterns": { |
|||
"*.ts": "$(capture).test.ts, $(capture).test.tsx, $(capture).spec.ts, $(capture).spec.tsx, $(capture).d.ts", |
|||
"*.tsx": "$(capture).test.ts, $(capture).test.tsx, $(capture).spec.ts, $(capture).spec.tsx,$(capture).d.ts", |
|||
"*.env": "$(capture).env.*", |
|||
"README.md": "README*,CHANGELOG*,LICENSE,CNAME", |
|||
"package.json": "pnpm-lock.yaml,pnpm-workspace.yaml,.gitattributes,.gitignore,.gitpod.yml,.npmrc,.browserslistrc,.node-version,.git*,.tazerc.json", |
|||
"eslint.config.mjs": ".eslintignore,.prettierignore,.stylelintignore,.commitlintrc.*,.prettierrc.*,stylelint.config.*,.lintstagedrc.mjs,cspell.json,lefthook.yml", |
|||
"tailwind.config.mjs": "postcss.*" |
|||
}, |
|||
"commentTranslate.hover.enabled": false, |
|||
"commentTranslate.multiLineMerge": true, |
|||
"vue.server.hybridMode": true, |
|||
"typescript.tsdk": "node_modules/typescript/lib", |
|||
"oxc.enable": false, |
|||
"cSpell.words": [ |
|||
"archiver", |
|||
"axios", |
|||
"dotenv", |
|||
"isequal", |
|||
"jspm", |
|||
"napi", |
|||
"nolebase", |
|||
"rollup", |
|||
"vitest" |
|||
] |
|||
} |
|||
@ -1,9 +1,51 @@ |
|||
<script lang="ts" setup> |
|||
import { Fallback } from '@vben/common-ui'; |
|||
import { useRouter } from 'vue-router'; |
|||
|
|||
import { Fallback, VbenButton } from '@vben/common-ui'; |
|||
import { ArrowLeft, LogOut } from '@vben/icons'; |
|||
import { $t } from '@vben/locales'; |
|||
import { preferences } from '@vben/preferences'; |
|||
|
|||
import { Modal } from 'ant-design-vue'; |
|||
|
|||
import { useAuthStore } from '#/store'; |
|||
|
|||
defineOptions({ name: 'Fallback404Demo' }); |
|||
|
|||
const authStore = useAuthStore(); |
|||
const { push } = useRouter(); |
|||
|
|||
// 返回首页 |
|||
function back() { |
|||
push(preferences.app.defaultHomePath); |
|||
} |
|||
|
|||
// 退出登录 |
|||
function logout() { |
|||
Modal.confirm({ |
|||
centered: true, |
|||
title: $t('common.logout'), |
|||
content: $t('ui.widgets.logoutTip'), |
|||
async onOk() { |
|||
await authStore.logout(); |
|||
}, |
|||
}); |
|||
} |
|||
</script> |
|||
|
|||
<template> |
|||
<Fallback status="404" /> |
|||
<Fallback status="404"> |
|||
<template #action> |
|||
<div class="flex gap-2"> |
|||
<VbenButton size="lg" @click="back"> |
|||
<ArrowLeft class="mr-2 size-4" /> |
|||
{{ $t('common.backToHome') }} |
|||
</VbenButton> |
|||
<VbenButton size="lg" variant="destructive" @click="logout"> |
|||
<LogOut class="mr-2 size-4" /> |
|||
{{ $t('common.logout') }} |
|||
</VbenButton> |
|||
</div> |
|||
</template> |
|||
</Fallback> |
|||
</template> |
|||
|
|||
@ -1,15 +1,98 @@ |
|||
<script lang="ts" setup> |
|||
import { Page } from '@vben/common-ui'; |
|||
import type { BindItem } from '@abp/account'; |
|||
|
|||
import { MySetting } from '@abp/account'; |
|||
import { defineAsyncComponent, ref } from 'vue'; |
|||
|
|||
import { Page, useVbenModal } from '@vben/common-ui'; |
|||
import { useRefresh } from '@vben/hooks'; |
|||
import { $t } from '@vben/locales'; |
|||
|
|||
import { MySetting, useExternalLoginsApi } from '@abp/account'; |
|||
import { message, Modal } from 'ant-design-vue'; |
|||
|
|||
defineOptions({ |
|||
name: 'Vben5AccountMySettings', |
|||
}); |
|||
|
|||
const { bindWorkWeixinApi, getExternalLoginsApi, removeExternalLoginApi } = |
|||
useExternalLoginsApi(); |
|||
const { refresh } = useRefresh(); |
|||
|
|||
const [WechatWorkUserBindModal, weComBindModal] = useVbenModal({ |
|||
connectedComponent: defineAsyncComponent(async () => { |
|||
const component = await import('@abp/wechat'); |
|||
return component.WechatWorkUserBinder; |
|||
}), |
|||
}); |
|||
const externalLogins = ref<BindItem[]>([]); |
|||
|
|||
async function onBindWorkWeixin(code: string) { |
|||
weComBindModal.setState({ submitting: true }); |
|||
try { |
|||
await bindWorkWeixinApi({ code }); |
|||
weComBindModal.close(); |
|||
message.success($t('AbpAccount.BindSuccessfully')); |
|||
refresh(); |
|||
} finally { |
|||
weComBindModal.setState({ submitting: false }); |
|||
} |
|||
} |
|||
|
|||
async function onRemoveBind(provider: string, key: string) { |
|||
Modal.confirm({ |
|||
title: $t('AbpUi.AreYouSure'), |
|||
centered: true, |
|||
content: $t('AbpAccount.CancelBindWarningMessage'), |
|||
async onOk() { |
|||
await removeExternalLoginApi({ |
|||
loginProvider: provider, |
|||
providerKey: key, |
|||
}); |
|||
message.success($t('AbpAccount.CancelBindSuccessfully')); |
|||
}, |
|||
}); |
|||
} |
|||
|
|||
function onBind(provider: string) { |
|||
switch (provider.toLocaleLowerCase()) { |
|||
case 'workweixin': { |
|||
weComBindModal.open(); |
|||
} |
|||
} |
|||
} |
|||
|
|||
async function onClick(provider: string, key?: string) { |
|||
if (key) { |
|||
await onRemoveBind(provider, key); |
|||
return; |
|||
} |
|||
onBind(provider); |
|||
} |
|||
|
|||
async function onInit() { |
|||
const res = await getExternalLoginsApi(); |
|||
externalLogins.value = res.externalLogins.map((item) => { |
|||
const userLogin = res.userLogins.find((x) => x.loginProvider === item.name); |
|||
return { |
|||
title: item.displayName, |
|||
description: userLogin?.providerKey ?? $t('AbpAccount.UnBind'), |
|||
buttons: [ |
|||
{ |
|||
title: userLogin?.providerKey |
|||
? $t('AbpAccount.CancelBind') |
|||
: $t('AbpAccount.Bind'), |
|||
type: 'link', |
|||
click: () => onClick(item.name, userLogin?.providerKey), |
|||
}, |
|||
], |
|||
}; |
|||
}); |
|||
} |
|||
</script> |
|||
|
|||
<template> |
|||
<Page> |
|||
<MySetting /> |
|||
<MySetting :bind-items="externalLogins" @on-bind-init="onInit" /> |
|||
<WechatWorkUserBindModal @on-login="onBindWorkWeixin" /> |
|||
</Page> |
|||
</template> |
|||
|
|||
@ -1,266 +1,32 @@ |
|||
<script lang="ts" setup> |
|||
import type { |
|||
WorkbenchProjectItem, |
|||
WorkbenchQuickNavItem, |
|||
WorkbenchTodoItem, |
|||
WorkbenchTrendItem, |
|||
} from '@vben/common-ui'; |
|||
<script setup lang="ts"> |
|||
import type { FavoriteMenu } from '@abp/platform'; |
|||
|
|||
import { ref } from 'vue'; |
|||
import { useRouter } from 'vue-router'; |
|||
|
|||
import { |
|||
AnalysisChartCard, |
|||
WorkbenchHeader, |
|||
WorkbenchProject, |
|||
WorkbenchQuickNav, |
|||
WorkbenchTodo, |
|||
WorkbenchTrends, |
|||
} from '@vben/common-ui'; |
|||
import { preferences } from '@vben/preferences'; |
|||
import { useUserStore } from '@vben/stores'; |
|||
import { openWindow } from '@vben/utils'; |
|||
|
|||
import AnalyticsVisitsSource from '../analytics/analytics-visits-source.vue'; |
|||
|
|||
const userStore = useUserStore(); |
|||
|
|||
// 这是一个示例数据,实际项目中需要根据实际情况进行调整 |
|||
// url 也可以是内部路由,在 navTo 方法中识别处理,进行内部跳转 |
|||
// 例如:url: /dashboard/workspace |
|||
const projectItems: WorkbenchProjectItem[] = [ |
|||
{ |
|||
color: '', |
|||
content: '不要等待机会,而要创造机会。', |
|||
date: '2021-04-01', |
|||
group: '开源组', |
|||
icon: 'carbon:logo-github', |
|||
title: 'Github', |
|||
url: 'https://github.com', |
|||
}, |
|||
{ |
|||
color: '#3fb27f', |
|||
content: '现在的你决定将来的你。', |
|||
date: '2021-04-01', |
|||
group: '算法组', |
|||
icon: 'ion:logo-vue', |
|||
title: 'Vue', |
|||
url: 'https://vuejs.org', |
|||
}, |
|||
{ |
|||
color: '#e18525', |
|||
content: '没有什么才能比努力更重要。', |
|||
date: '2021-04-01', |
|||
group: '上班摸鱼', |
|||
icon: 'ion:logo-html5', |
|||
title: 'Html5', |
|||
url: 'https://developer.mozilla.org/zh-CN/docs/Web/HTML', |
|||
}, |
|||
{ |
|||
color: '#bf0c2c', |
|||
content: '热情和欲望可以突破一切难关。', |
|||
date: '2021-04-01', |
|||
group: 'UI', |
|||
icon: 'ion:logo-angular', |
|||
title: 'Angular', |
|||
url: 'https://angular.io', |
|||
}, |
|||
{ |
|||
color: '#00d8ff', |
|||
content: '健康的身体是实现目标的基石。', |
|||
date: '2021-04-01', |
|||
group: '技术牛', |
|||
icon: 'bx:bxl-react', |
|||
title: 'React', |
|||
url: 'https://reactjs.org', |
|||
}, |
|||
{ |
|||
color: '#EBD94E', |
|||
content: '路是走出来的,而不是空想出来的。', |
|||
date: '2021-04-01', |
|||
group: '架构组', |
|||
icon: 'ion:logo-javascript', |
|||
title: 'Js', |
|||
url: 'https://developer.mozilla.org/zh-CN/docs/Web/JavaScript', |
|||
}, |
|||
]; |
|||
|
|||
// 同样,这里的 url 也可以使用以 http 开头的外部链接 |
|||
const quickNavItems: WorkbenchQuickNavItem[] = [ |
|||
{ |
|||
color: '#1fdaca', |
|||
icon: 'ion:home-outline', |
|||
title: '首页', |
|||
url: '/', |
|||
}, |
|||
{ |
|||
color: '#bf0c2c', |
|||
icon: 'ion:grid-outline', |
|||
title: '仪表盘', |
|||
url: '/dashboard', |
|||
}, |
|||
{ |
|||
color: '#e18525', |
|||
icon: 'ion:layers-outline', |
|||
title: '组件', |
|||
url: '/demos/features/icons', |
|||
}, |
|||
{ |
|||
color: '#3fb27f', |
|||
icon: 'ion:settings-outline', |
|||
title: '系统管理', |
|||
url: '/demos/features/login-expired', // 这里的 URL 是示例,实际项目中需要根据实际情况进行调整 |
|||
}, |
|||
{ |
|||
color: '#4daf1bc9', |
|||
icon: 'ion:key-outline', |
|||
title: '权限管理', |
|||
url: '/demos/access/page-control', |
|||
}, |
|||
{ |
|||
color: '#00d8ff', |
|||
icon: 'ion:bar-chart-outline', |
|||
title: '图表', |
|||
url: '/analytics', |
|||
}, |
|||
]; |
|||
|
|||
const todoItems = ref<WorkbenchTodoItem[]>([ |
|||
{ |
|||
completed: false, |
|||
content: `审查最近提交到Git仓库的前端代码,确保代码质量和规范。`, |
|||
date: '2024-07-30 11:00:00', |
|||
title: '审查前端代码提交', |
|||
}, |
|||
{ |
|||
completed: true, |
|||
content: `检查并优化系统性能,降低CPU使用率。`, |
|||
date: '2024-07-30 11:00:00', |
|||
title: '系统性能优化', |
|||
}, |
|||
{ |
|||
completed: false, |
|||
content: `进行系统安全检查,确保没有安全漏洞或未授权的访问。 `, |
|||
date: '2024-07-30 11:00:00', |
|||
title: '安全检查', |
|||
}, |
|||
{ |
|||
completed: false, |
|||
content: `更新项目中的所有npm依赖包,确保使用最新版本。`, |
|||
date: '2024-07-30 11:00:00', |
|||
title: '更新项目依赖', |
|||
}, |
|||
{ |
|||
completed: false, |
|||
content: `修复用户报告的页面UI显示问题,确保在不同浏览器中显示一致。 `, |
|||
date: '2024-07-30 11:00:00', |
|||
title: '修复UI显示问题', |
|||
}, |
|||
]); |
|||
const trendItems: WorkbenchTrendItem[] = [ |
|||
{ |
|||
avatar: 'svg:avatar-1', |
|||
content: `在 <a>开源组</a> 创建了项目 <a>Vue</a>`, |
|||
date: '刚刚', |
|||
title: '威廉', |
|||
}, |
|||
{ |
|||
avatar: 'svg:avatar-2', |
|||
content: `关注了 <a>威廉</a> `, |
|||
date: '1个小时前', |
|||
title: '艾文', |
|||
}, |
|||
{ |
|||
avatar: 'svg:avatar-3', |
|||
content: `发布了 <a>个人动态</a> `, |
|||
date: '1天前', |
|||
title: '克里斯', |
|||
}, |
|||
{ |
|||
avatar: 'svg:avatar-4', |
|||
content: `发表文章 <a>如何编写一个Vite插件</a> `, |
|||
date: '2天前', |
|||
title: 'Vben', |
|||
}, |
|||
{ |
|||
avatar: 'svg:avatar-1', |
|||
content: `回复了 <a>杰克</a> 的问题 <a>如何进行项目优化?</a>`, |
|||
date: '3天前', |
|||
title: '皮特', |
|||
}, |
|||
{ |
|||
avatar: 'svg:avatar-2', |
|||
content: `关闭了问题 <a>如何运行项目</a> `, |
|||
date: '1周前', |
|||
title: '杰克', |
|||
}, |
|||
{ |
|||
avatar: 'svg:avatar-3', |
|||
content: `发布了 <a>个人动态</a> `, |
|||
date: '1周前', |
|||
title: '威廉', |
|||
}, |
|||
{ |
|||
avatar: 'svg:avatar-4', |
|||
content: `推送了代码到 <a>Github</a>`, |
|||
date: '2021-04-01 20:00', |
|||
title: '威廉', |
|||
}, |
|||
{ |
|||
avatar: 'svg:avatar-4', |
|||
content: `发表文章 <a>如何编写使用 Admin Vben</a> `, |
|||
date: '2021-03-01 20:00', |
|||
title: 'Vben', |
|||
}, |
|||
]; |
|||
import { Workbench } from '@abp/platform'; |
|||
|
|||
const router = useRouter(); |
|||
|
|||
// 这是一个示例方法,实际项目中需要根据实际情况进行调整 |
|||
// This is a sample method, adjust according to the actual project requirements |
|||
function navTo(nav: WorkbenchProjectItem | WorkbenchQuickNavItem) { |
|||
if (nav.url?.startsWith('http')) { |
|||
openWindow(nav.url); |
|||
function navTo(menu: FavoriteMenu) { |
|||
if (menu.path?.startsWith('http')) { |
|||
openWindow(menu.path); |
|||
return; |
|||
} |
|||
if (nav.url?.startsWith('/')) { |
|||
router.push(nav.url).catch((error) => { |
|||
if (menu.path?.startsWith('/')) { |
|||
router.push(menu.path).catch((error) => { |
|||
console.error('Navigation failed:', error); |
|||
}); |
|||
} else { |
|||
console.warn(`Unknown URL for navigation item: ${nav.title} -> ${nav.url}`); |
|||
console.warn( |
|||
`Unknown URL for navigation item: ${menu.displayName} -> ${menu.path}`, |
|||
); |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<template> |
|||
<div class="p-5"> |
|||
<WorkbenchHeader |
|||
:avatar="userStore.userInfo?.avatar || preferences.app.defaultAvatar" |
|||
> |
|||
<template #title> |
|||
早安, {{ userStore.userInfo?.realName }}, 开始您一天的工作吧! |
|||
</template> |
|||
<template #description> 今日晴,20℃ - 32℃! </template> |
|||
</WorkbenchHeader> |
|||
|
|||
<div class="mt-5 flex flex-col lg:flex-row"> |
|||
<div class="mr-4 w-full lg:w-3/5"> |
|||
<WorkbenchProject :items="projectItems" title="项目" @click="navTo" /> |
|||
<WorkbenchTrends :items="trendItems" class="mt-5" title="最新动态" /> |
|||
</div> |
|||
<div class="w-full lg:w-2/5"> |
|||
<WorkbenchQuickNav |
|||
:items="quickNavItems" |
|||
class="mt-5 lg:mt-0" |
|||
title="快捷导航" |
|||
@click="navTo" |
|||
/> |
|||
<WorkbenchTodo :items="todoItems" class="mt-5" title="待办事项" /> |
|||
<AnalysisChartCard class="mt-5" title="访问来源"> |
|||
<AnalyticsVisitsSource /> |
|||
</AnalysisChartCard> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
<Workbench @nav-to="navTo" /> |
|||
</template> |
|||
|
|||
<style scoped></style> |
|||
|
|||
@ -1,7 +1,5 @@ |
|||
export { useAccountApi } from './useAccountApi'; |
|||
export { useExternalLoginsApi } from './useExternalLoginsApi'; |
|||
export { useMySessionApi } from './useMySessionApi'; |
|||
export { usePhoneLoginApi } from './usePhoneLoginApi'; |
|||
export { useProfileApi } from './useProfileApi'; |
|||
export { useQrCodeLoginApi } from './useQrCodeLoginApi'; |
|||
export { useTokenApi } from './useTokenApi'; |
|||
export { useUserInfoApi } from './useUserInfoApi'; |
|||
export { useScanQrCodeApi } from './useScanQrCodeApi'; |
|||
|
|||
@ -0,0 +1,58 @@ |
|||
import type { |
|||
ExternalLoginResultDto, |
|||
RemoveExternalLoginInput, |
|||
WorkWeixinLoginBindInput, |
|||
} from '../types/external-logins'; |
|||
|
|||
import { useRequest } from '@abp/request'; |
|||
|
|||
export function useExternalLoginsApi() { |
|||
const { cancel, request } = useRequest(); |
|||
|
|||
/** |
|||
* 绑定企业微信 |
|||
* @param input 绑定参数 |
|||
* @returns { Promise<void> } |
|||
*/ |
|||
async function bindWorkWeixinApi( |
|||
input: WorkWeixinLoginBindInput, |
|||
): Promise<void> { |
|||
return await request(`/api/account/oauth/work-weixin/bind`, { |
|||
method: 'POST', |
|||
data: input, |
|||
}); |
|||
} |
|||
|
|||
/** |
|||
* 获取外部登录提供者列表 |
|||
* @returns 外部登录提供者列表 |
|||
*/ |
|||
async function getExternalLoginsApi(): Promise<ExternalLoginResultDto> { |
|||
return await request<ExternalLoginResultDto>( |
|||
`/api/account/external-logins`, |
|||
{ |
|||
method: 'GET', |
|||
}, |
|||
); |
|||
} |
|||
|
|||
/** |
|||
* 移除外部登录提供者 |
|||
* @returns { Promise<void> } |
|||
*/ |
|||
async function removeExternalLoginApi( |
|||
input: RemoveExternalLoginInput, |
|||
): Promise<void> { |
|||
return await request(`/api/account/external-logins/remove`, { |
|||
method: 'DELETE', |
|||
params: input, |
|||
}); |
|||
} |
|||
|
|||
return { |
|||
cancel, |
|||
bindWorkWeixinApi, |
|||
getExternalLoginsApi, |
|||
removeExternalLoginApi, |
|||
}; |
|||
} |
|||
@ -1,46 +0,0 @@ |
|||
import type { OAuthTokenResult, PhoneNumberTokenRequest } from '../types/token'; |
|||
|
|||
import { useAppConfig } from '@vben/hooks'; |
|||
|
|||
import { useRequest } from '@abp/request'; |
|||
|
|||
export function usePhoneLoginApi() { |
|||
const { cancel, request } = useRequest(); |
|||
|
|||
/** |
|||
* 手机验证码登录 |
|||
* @param input 登录参数 |
|||
* @returns 用户token |
|||
*/ |
|||
async function loginApi(input: PhoneNumberTokenRequest) { |
|||
const { audience, clientId, clientSecret } = useAppConfig( |
|||
import.meta.env, |
|||
import.meta.env.PROD, |
|||
); |
|||
const result = await request<OAuthTokenResult>('/connect/token', { |
|||
data: { |
|||
client_id: clientId, |
|||
client_secret: clientSecret, |
|||
grant_type: 'phone_verify', |
|||
phone_number: input.phoneNumber, |
|||
phone_verify_code: input.code, |
|||
scope: audience, |
|||
}, |
|||
headers: { |
|||
'Content-Type': 'application/x-www-form-urlencoded', |
|||
}, |
|||
method: 'POST', |
|||
}); |
|||
return { |
|||
accessToken: result.access_token, |
|||
expiresIn: result.expires_in, |
|||
refreshToken: result.refresh_token, |
|||
tokenType: result.token_type, |
|||
}; |
|||
} |
|||
|
|||
return { |
|||
cancel, |
|||
loginApi, |
|||
}; |
|||
} |
|||
@ -1,73 +0,0 @@ |
|||
import type { |
|||
GenerateQrCodeResult, |
|||
QrCodeUserInfoResult, |
|||
} from '../types/qrcode'; |
|||
import type { OAuthTokenResult, QrCodeTokenRequest } from '../types/token'; |
|||
|
|||
import { useAppConfig } from '@vben/hooks'; |
|||
|
|||
import { useRequest } from '@abp/request'; |
|||
|
|||
export function useQrCodeLoginApi() { |
|||
const { cancel, request } = useRequest(); |
|||
|
|||
/** |
|||
* 生成登录二维码 |
|||
* @returns 二维码信息 |
|||
*/ |
|||
function generateApi(): Promise<GenerateQrCodeResult> { |
|||
return request<GenerateQrCodeResult>('/api/account/qrcode/generate', { |
|||
method: 'POST', |
|||
}); |
|||
} |
|||
|
|||
/** |
|||
* 检查二维码状态 |
|||
* @param key 二维码Key |
|||
* @returns 二维码信息 |
|||
*/ |
|||
function checkCodeApi(key: string): Promise<QrCodeUserInfoResult> { |
|||
return request<QrCodeUserInfoResult>(`/api/account/qrcode/${key}/check`, { |
|||
method: 'GET', |
|||
}); |
|||
} |
|||
|
|||
/** |
|||
* 二维码登录 |
|||
* @param input 登录参数 |
|||
* @returns 用户token |
|||
*/ |
|||
async function loginApi(input: QrCodeTokenRequest) { |
|||
const { audience, clientId, clientSecret } = useAppConfig( |
|||
import.meta.env, |
|||
import.meta.env.PROD, |
|||
); |
|||
const result = await request<OAuthTokenResult>('/connect/token', { |
|||
data: { |
|||
client_id: clientId, |
|||
client_secret: clientSecret, |
|||
grant_type: 'qr_code', |
|||
qrcode_key: input.key, |
|||
scope: audience, |
|||
tenant_id: input.tenantId, |
|||
}, |
|||
headers: { |
|||
'Content-Type': 'application/x-www-form-urlencoded', |
|||
}, |
|||
method: 'POST', |
|||
}); |
|||
return { |
|||
accessToken: result.access_token, |
|||
expiresIn: result.expires_in, |
|||
refreshToken: result.refresh_token, |
|||
tokenType: result.token_type, |
|||
}; |
|||
} |
|||
|
|||
return { |
|||
cancel, |
|||
checkCodeApi, |
|||
generateApi, |
|||
loginApi, |
|||
}; |
|||
} |
|||
@ -0,0 +1,37 @@ |
|||
import type { |
|||
GenerateQrCodeResult, |
|||
QrCodeUserInfoResult, |
|||
} from '../types/qrcode'; |
|||
|
|||
import { useRequest } from '@abp/request'; |
|||
|
|||
export function useScanQrCodeApi() { |
|||
const { cancel, request } = useRequest(); |
|||
|
|||
/** |
|||
* 生成登录二维码 |
|||
* @returns 二维码信息 |
|||
*/ |
|||
function generateApi(): Promise<GenerateQrCodeResult> { |
|||
return request<GenerateQrCodeResult>('/api/account/qrcode/generate', { |
|||
method: 'POST', |
|||
}); |
|||
} |
|||
|
|||
/** |
|||
* 检查二维码状态 |
|||
* @param key 二维码Key |
|||
* @returns 二维码信息 |
|||
*/ |
|||
function checkCodeApi(key: string): Promise<QrCodeUserInfoResult> { |
|||
return request<QrCodeUserInfoResult>(`/api/account/qrcode/${key}/check`, { |
|||
method: 'GET', |
|||
}); |
|||
} |
|||
|
|||
return { |
|||
cancel, |
|||
checkCodeApi, |
|||
generateApi, |
|||
}; |
|||
} |
|||
@ -1,83 +0,0 @@ |
|||
import type { |
|||
OAuthTokenRefreshModel, |
|||
OAuthTokenResult, |
|||
PasswordTokenRequestModel, |
|||
TokenResult, |
|||
} from '../types'; |
|||
|
|||
import { useAppConfig } from '@vben/hooks'; |
|||
|
|||
import { useRequest } from '@abp/request'; |
|||
|
|||
export function useTokenApi() { |
|||
const { cancel, request } = useRequest(); |
|||
/** |
|||
* 用户登录 |
|||
* @param input 参数 |
|||
* @returns 用户token |
|||
*/ |
|||
async function loginApi( |
|||
input: PasswordTokenRequestModel, |
|||
): Promise<TokenResult> { |
|||
const { audience, clientId, clientSecret } = useAppConfig( |
|||
import.meta.env, |
|||
import.meta.env.PROD, |
|||
); |
|||
const result = await request<OAuthTokenResult>('/connect/token', { |
|||
data: { |
|||
client_id: clientId, |
|||
client_secret: clientSecret, |
|||
grant_type: 'password', |
|||
scope: audience, |
|||
...input, |
|||
}, |
|||
headers: { |
|||
'Content-Type': 'application/x-www-form-urlencoded', |
|||
}, |
|||
method: 'POST', |
|||
}); |
|||
return { |
|||
accessToken: result.access_token, |
|||
expiresIn: result.expires_in, |
|||
refreshToken: result.refresh_token, |
|||
tokenType: result.token_type, |
|||
}; |
|||
} |
|||
|
|||
/** |
|||
* 刷新令牌 |
|||
* @param input 参数 |
|||
* @returns 用户token |
|||
*/ |
|||
async function refreshTokenApi(input: OAuthTokenRefreshModel) { |
|||
const { audience, clientId, clientSecret } = useAppConfig( |
|||
import.meta.env, |
|||
import.meta.env.PROD, |
|||
); |
|||
const result = await request<OAuthTokenResult>('/connect/token', { |
|||
data: { |
|||
client_id: clientId, |
|||
client_secret: clientSecret, |
|||
grant_type: 'refresh_token', |
|||
refresh_token: input.refreshToken, |
|||
scope: audience, |
|||
}, |
|||
headers: { |
|||
'Content-Type': 'application/x-www-form-urlencoded', |
|||
}, |
|||
method: 'POST', |
|||
}); |
|||
return { |
|||
accessToken: result.access_token, |
|||
expiresIn: result.expires_in, |
|||
refreshToken: result.refresh_token, |
|||
tokenType: result.token_type, |
|||
}; |
|||
} |
|||
|
|||
return { |
|||
cancel, |
|||
loginApi, |
|||
refreshTokenApi, |
|||
}; |
|||
} |
|||
@ -1,29 +0,0 @@ |
|||
import type { OAuthUserInfo, UserInfo } from '../types/user'; |
|||
|
|||
import { useRequest } from '@abp/request'; |
|||
|
|||
export function useUserInfoApi() { |
|||
const { cancel, request } = useRequest(); |
|||
|
|||
/** |
|||
* 获取用户信息 |
|||
*/ |
|||
async function getUserInfoApi(): Promise<UserInfo> { |
|||
const result = await request<OAuthUserInfo>('/connect/userinfo', { |
|||
method: 'GET', |
|||
}); |
|||
return { |
|||
...result, |
|||
emailVerified: result.email_verified, |
|||
givenName: result.given_name, |
|||
phoneNumberVerified: result.phone_number_verified, |
|||
preferredUsername: result.preferred_username, |
|||
uniqueName: result.unique_name, |
|||
}; |
|||
} |
|||
|
|||
return { |
|||
cancel, |
|||
getUserInfoApi, |
|||
}; |
|||
} |
|||
@ -1,2 +1,2 @@ |
|||
export * from './useOAuthError'; |
|||
export * from './useOidcClient'; |
|||
export * from './useOAuthService'; |
|||
|
|||
@ -0,0 +1,17 @@ |
|||
import type { ButtonType } from 'ant-design-vue/lib/button'; |
|||
|
|||
interface BindButton { |
|||
click: () => Promise<void> | void; |
|||
title: string; |
|||
type?: ButtonType; |
|||
} |
|||
|
|||
interface BindItem { |
|||
buttons?: BindButton[]; |
|||
description?: string; |
|||
enable?: boolean; |
|||
slot?: string; |
|||
title: string; |
|||
} |
|||
|
|||
export type { BindItem }; |
|||
@ -0,0 +1,32 @@ |
|||
interface UserLoginInfoDto { |
|||
loginProvider: string; |
|||
providerDisplayName: string; |
|||
providerKey: string; |
|||
} |
|||
|
|||
interface ExternalLoginInfoDto { |
|||
displayName: string; |
|||
name: string; |
|||
} |
|||
|
|||
interface WorkWeixinLoginBindInput { |
|||
code: string; |
|||
} |
|||
|
|||
interface ExternalLoginResultDto { |
|||
externalLogins: ExternalLoginInfoDto[]; |
|||
userLogins: UserLoginInfoDto[]; |
|||
} |
|||
|
|||
interface RemoveExternalLoginInput { |
|||
loginProvider: string; |
|||
providerKey: string; |
|||
} |
|||
|
|||
export type { |
|||
ExternalLoginInfoDto, |
|||
ExternalLoginResultDto, |
|||
RemoveExternalLoginInput, |
|||
UserLoginInfoDto, |
|||
WorkWeixinLoginBindInput, |
|||
}; |
|||
@ -1,4 +1,6 @@ |
|||
export * from './account'; |
|||
export * from './bind'; |
|||
export * from './external-logins'; |
|||
export * from './profile'; |
|||
export * from './token'; |
|||
export * from './user'; |
|||
|
|||
@ -0,0 +1,200 @@ |
|||
<script lang="ts" setup> |
|||
import type { CSSProperties, PropType } from 'vue'; |
|||
|
|||
import type { Nullable } from '@vben/types'; |
|||
|
|||
import { |
|||
computed, |
|||
onMounted, |
|||
onUnmounted, |
|||
ref, |
|||
unref, |
|||
useAttrs, |
|||
useTemplateRef, |
|||
} from 'vue'; |
|||
|
|||
import { useNamespace } from '@vben/hooks'; |
|||
|
|||
import { useDebounceFn } from '@vueuse/core'; |
|||
import Cropper from 'cropperjs'; |
|||
|
|||
import 'cropperjs/dist/cropper.css'; |
|||
|
|||
type Options = Cropper.Options; |
|||
type ElRef<T extends HTMLElement = HTMLDivElement> = Nullable<T>; |
|||
|
|||
const props = defineProps({ |
|||
src: { type: String, required: true }, |
|||
alt: { type: String, default: '' }, |
|||
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: () => ({}) }, |
|||
}); |
|||
const emits = defineEmits(['cropend', 'ready', 'cropendError']); |
|||
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 attrs = useAttrs(); |
|||
|
|||
const imgElRef = useTemplateRef<ElRef<HTMLImageElement>>('imgElRef'); |
|||
const cropper = ref<Nullable<Cropper>>(); |
|||
const isReady = ref(false); |
|||
|
|||
const { b, is } = useNamespace('cropper-image'); |
|||
const debounceRealTimeCroppered = useDebounceFn(realTimeCroppered, 80); |
|||
|
|||
const getImageStyle = computed((): CSSProperties => { |
|||
return { |
|||
height: props.height, |
|||
maxWidth: '100%', |
|||
...props.imageStyle, |
|||
}; |
|||
}); |
|||
|
|||
const getClass = computed(() => { |
|||
return [b(), attrs.class, is('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(); |
|||
emits('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; |
|||
} |
|||
const imgInfo = cropper.value.getData(); |
|||
const canvas = props.circled |
|||
? getRoundedCanvas() |
|||
: cropper.value.getCroppedCanvas(); |
|||
canvas.toBlob((blob) => { |
|||
if (!blob) { |
|||
return; |
|||
} |
|||
const fileReader: FileReader = new FileReader(); |
|||
fileReader.readAsDataURL(blob); |
|||
fileReader.onloadend = (e) => { |
|||
emits('cropend', { |
|||
imgBase64: e.target?.result ?? '', |
|||
imgInfo, |
|||
}); |
|||
}; |
|||
// eslint-disable-next-line unicorn/prefer-add-event-listener |
|||
fileReader.onerror = () => { |
|||
emits('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; |
|||
} |
|||
</script> |
|||
<template> |
|||
<div :class="getClass" :style="getWrapperStyle"> |
|||
<img |
|||
v-show="isReady" |
|||
ref="imgElRef" |
|||
:src="src" |
|||
:alt="alt" |
|||
:crossorigin="crossorigin" |
|||
:style="getImageStyle" |
|||
/> |
|||
</div> |
|||
</template> |
|||
<style scoped lang="scss"> |
|||
$namespace: vben; |
|||
|
|||
.#{$namespace}-cropper-image { |
|||
&.is-circled { |
|||
.cropper-view-box, |
|||
.cropper-face { |
|||
border-radius: 50%; |
|||
} |
|||
} |
|||
} |
|||
</style> |
|||
@ -0,0 +1,159 @@ |
|||
<script setup lang="ts"> |
|||
import type { ButtonProps } from 'ant-design-vue/es/button'; |
|||
|
|||
import type { CSSProperties, PropType } from 'vue'; |
|||
|
|||
import { computed, ref, unref, watch, watchEffect } from 'vue'; |
|||
|
|||
import { useVbenModal } from '@vben/common-ui'; |
|||
import { useNamespace } from '@vben/hooks'; |
|||
import { createIconifyIcon } from '@vben/icons'; |
|||
import { useI18n } from '@vben/locales'; |
|||
|
|||
import { Button } from 'ant-design-vue'; |
|||
|
|||
import CropperModal from './CropperModal.vue'; |
|||
|
|||
interface File { |
|||
file: Blob; |
|||
fileName?: string; |
|||
name: string; |
|||
} |
|||
|
|||
const props = defineProps({ |
|||
width: { type: [String, Number], default: '200px' }, |
|||
value: { type: String, default: '' }, |
|||
showBtn: { type: Boolean, default: true }, |
|||
btnProps: { type: Object as PropType<ButtonProps>, default: undefined }, |
|||
btnText: { type: String, default: '' }, |
|||
uploadApi: { |
|||
type: Function as PropType<(file: File) => Promise<void>>, |
|||
default: undefined, |
|||
}, |
|||
}); |
|||
|
|||
const emits = defineEmits(['update:value', 'change']); |
|||
|
|||
const UploadIcon = createIconifyIcon('ant-design:cloud-upload-outlined'); |
|||
|
|||
const sourceValue = ref(props.value || ''); |
|||
const { b, e } = useNamespace('cropper-avatar'); |
|||
const [Modal, modalApi] = useVbenModal({ |
|||
connectedComponent: CropperModal, |
|||
}); |
|||
const { t } = useI18n(); |
|||
|
|||
const getWidth = computed(() => `${`${props.width}`.replace(/px/, '')}px`); |
|||
|
|||
const getIconWidth = computed( |
|||
() => `${Number.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) => { |
|||
emits('update:value', v); |
|||
}, |
|||
); |
|||
|
|||
function handleUploadSuccess(url: string) { |
|||
sourceValue.value = url; |
|||
emits('change', url); |
|||
} |
|||
function openModal() { |
|||
modalApi.open(); |
|||
} |
|||
function closeModal() { |
|||
modalApi.close(); |
|||
} |
|||
|
|||
defineExpose({ openModal, closeModal }); |
|||
</script> |
|||
|
|||
<template> |
|||
<div :class="b()" :style="getStyle"> |
|||
<div |
|||
:class="e(`image-wrapper`)" |
|||
:style="getImageWrapperStyle" |
|||
@click="openModal" |
|||
> |
|||
<div :class="e(`image-mask`)" :style="getImageWrapperStyle"> |
|||
<UploadIcon |
|||
:width="getIconWidth" |
|||
:style="getImageWrapperStyle" |
|||
color="#d6d6d6" |
|||
/> |
|||
</div> |
|||
<img :src="sourceValue" v-if="sourceValue" alt="avatar" /> |
|||
</div> |
|||
<Button |
|||
:class="e(`upload-btn`)" |
|||
@click="openModal" |
|||
v-if="showBtn" |
|||
v-bind="btnProps" |
|||
> |
|||
{{ btnText ? btnText : t('cropper.selectImage') }} |
|||
</Button> |
|||
|
|||
<Modal |
|||
@upload-success="handleUploadSuccess" |
|||
:upload-api="uploadApi" |
|||
:src="sourceValue" |
|||
/> |
|||
</div> |
|||
</template> |
|||
|
|||
<style scoped lang="scss"> |
|||
$namespace: vben; |
|||
|
|||
.#{$namespace}-cropper-avatar { |
|||
display: inline-block; |
|||
text-align: center; |
|||
|
|||
&__image-wrapper { |
|||
overflow: hidden; |
|||
cursor: pointer; |
|||
border: var(--border); |
|||
border-radius: 50%; |
|||
|
|||
img { |
|||
width: 100%; |
|||
// height: 100%; |
|||
} |
|||
} |
|||
|
|||
&__image-mask { |
|||
position: absolute; |
|||
width: inherit; |
|||
height: inherit; |
|||
cursor: pointer; |
|||
background: rgb(0 0 0 / 40%); |
|||
border: inherit; |
|||
border-radius: inherit; |
|||
opacity: 0; |
|||
transition: opacity 0.4s; |
|||
|
|||
::v-deep(svg) { |
|||
margin: auto; |
|||
} |
|||
} |
|||
|
|||
&__image-mask:hover { |
|||
opacity: 40; |
|||
} |
|||
|
|||
&__upload-btn { |
|||
margin: 10px auto; |
|||
} |
|||
} |
|||
</style> |
|||
@ -0,0 +1,309 @@ |
|||
<script setup lang="ts"> |
|||
import type { UploadProps } from 'ant-design-vue'; |
|||
|
|||
import type { PropType } from 'vue'; |
|||
|
|||
import type { CropendResult, Cropper } from './types'; |
|||
|
|||
import { h, ref } from 'vue'; |
|||
|
|||
import { useVbenModal } from '@vben/common-ui'; |
|||
import { useNamespace } from '@vben/hooks'; |
|||
import { createIconifyIcon } from '@vben/icons'; |
|||
import { $t } from '@vben/locales'; |
|||
import { isFunction } from '@vben/utils'; |
|||
|
|||
import { dataURLtoBlob } from '@abp/core'; |
|||
import { Avatar, Button, Space, Tooltip, Upload } from 'ant-design-vue'; |
|||
|
|||
import CropperImage from './Cropper.vue'; |
|||
|
|||
type ApiFunParams = { file: Blob; fileName: string; name: string }; |
|||
|
|||
const props = defineProps({ |
|||
circled: { type: Boolean, default: true }, |
|||
uploadApi: { |
|||
type: Function as PropType<(params: ApiFunParams) => Promise<any>>, |
|||
default: undefined, |
|||
}, |
|||
src: { type: String, default: '' }, |
|||
}); |
|||
|
|||
const emits = defineEmits<{ |
|||
(event: 'uploadSuccess', url: string): void; |
|||
}>(); |
|||
|
|||
const UploadIcon = createIconifyIcon('ant-design:upload-outlined'); |
|||
const ResetIcon = createIconifyIcon('ant-design:reload-outlined'); |
|||
const RotateLeftIcon = createIconifyIcon('ant-design:rotate-left-outlined'); |
|||
const RotateRightIcon = createIconifyIcon('ant-design:rotate-right-outlined'); |
|||
const ScaleXIcon = createIconifyIcon('vaadin:arrows-long-h'); |
|||
const ScaleYIcon = createIconifyIcon('vaadin:arrows-long-v'); |
|||
const ZoomInIcon = createIconifyIcon('ant-design:zoom-in-outlined'); |
|||
const ZoomOutIcon = createIconifyIcon('ant-design:zoom-out-outlined'); |
|||
|
|||
let fileName = ''; |
|||
const src = ref(props.src || ''); |
|||
const previewSource = ref(''); |
|||
const fileList = ref<UploadProps['fileList']>([]); |
|||
const cropper = ref<Cropper>(); |
|||
let scaleX = 1; |
|||
let scaleY = 1; |
|||
|
|||
const { b, e } = useNamespace('cropper-am'); |
|||
const [Modal, modalApi] = useVbenModal({ |
|||
class: 'w-[800px]', |
|||
fullscreen: false, |
|||
fullscreenButton: false, |
|||
confirmText: $t('cropper.confirmText'), |
|||
onConfirm: handleOk, |
|||
title: $t('cropper.title'), |
|||
}); |
|||
function handleBeforeUpload(file: File) { |
|||
const reader = new FileReader(); |
|||
reader.readAsDataURL(file); |
|||
src.value = ''; |
|||
previewSource.value = ''; |
|||
reader.addEventListener('load', (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 (!cropper.value) { |
|||
return; |
|||
} |
|||
if (event === 'scaleX') { |
|||
scaleX = arg = scaleX === -1 ? 1 : -1; |
|||
} |
|||
if (event === 'scaleY') { |
|||
scaleY = arg = scaleY === -1 ? 1 : -1; |
|||
} |
|||
switch (event) { |
|||
case 'reset': { |
|||
return cropper.value.reset(); |
|||
} |
|||
case 'rotate': { |
|||
return cropper.value.rotate(arg!); |
|||
} |
|||
case 'scaleX': { |
|||
return cropper.value.scaleX(scaleX); |
|||
} |
|||
case 'scaleY': { |
|||
return cropper.value.scaleY(scaleY); |
|||
} |
|||
case 'zoom': { |
|||
return cropper.value.zoom(arg!); |
|||
} |
|||
} |
|||
} |
|||
async function handleOk() { |
|||
const uploadApi = props.uploadApi; |
|||
if (uploadApi && isFunction(uploadApi)) { |
|||
const blob = dataURLtoBlob(previewSource.value); |
|||
try { |
|||
modalApi.setState({ submitting: true }); |
|||
await uploadApi({ name: 'file', file: blob, fileName }); |
|||
emits('uploadSuccess', previewSource.value); |
|||
modalApi.close(); |
|||
} finally { |
|||
modalApi.setState({ submitting: false }); |
|||
} |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<template> |
|||
<Modal> |
|||
<div :class="b()"> |
|||
<div :class="e('left')"> |
|||
<div :class="e('cropper')"> |
|||
<CropperImage |
|||
v-if="src" |
|||
:src="src" |
|||
height="300px" |
|||
:circled="circled" |
|||
@cropend="handleCropend" |
|||
@ready="handleReady" |
|||
/> |
|||
</div> |
|||
<div :class="e('toolbar')"> |
|||
<Upload |
|||
:file-list="fileList" |
|||
accept="image/*" |
|||
:before-upload="handleBeforeUpload" |
|||
> |
|||
<Tooltip :title="$t('cropper.selectImage')" placement="bottom"> |
|||
<Button size="small" :icon="h(UploadIcon)" type="primary" /> |
|||
</Tooltip> |
|||
</Upload> |
|||
<Space> |
|||
<Tooltip :title="$t('cropper.btn_reset')" placement="bottom"> |
|||
<Button |
|||
type="primary" |
|||
:icon="h(ResetIcon)" |
|||
size="small" |
|||
:disabled="!src" |
|||
@click="handlerToolbar('reset')" |
|||
/> |
|||
</Tooltip> |
|||
<Tooltip :title="$t('cropper.btn_rotate_left')" placement="bottom"> |
|||
<Button |
|||
type="primary" |
|||
:icon="h(RotateLeftIcon)" |
|||
size="small" |
|||
:disabled="!src" |
|||
@click="handlerToolbar('rotate', -45)" |
|||
/> |
|||
</Tooltip> |
|||
<Tooltip :title="$t('cropper.btn_rotate_right')" placement="bottom"> |
|||
<Button |
|||
type="primary" |
|||
:icon="h(RotateRightIcon)" |
|||
size="small" |
|||
:disabled="!src" |
|||
@click="handlerToolbar('rotate', 45)" |
|||
/> |
|||
</Tooltip> |
|||
<Tooltip :title="$t('cropper.btn_scale_x')" placement="bottom"> |
|||
<Button |
|||
type="primary" |
|||
:icon="h(ScaleXIcon)" |
|||
size="small" |
|||
:disabled="!src" |
|||
@click="handlerToolbar('scaleX')" |
|||
/> |
|||
</Tooltip> |
|||
<Tooltip :title="$t('cropper.btn_scale_y')" placement="bottom"> |
|||
<Button |
|||
type="primary" |
|||
:icon="h(ScaleYIcon)" |
|||
size="small" |
|||
:disabled="!src" |
|||
@click="handlerToolbar('scaleY')" |
|||
/> |
|||
</Tooltip> |
|||
<Tooltip :title="$t('cropper.btn_zoom_in')" placement="bottom"> |
|||
<Button |
|||
type="primary" |
|||
:icon="h(ZoomInIcon)" |
|||
size="small" |
|||
:disabled="!src" |
|||
@click="handlerToolbar('zoom', 0.1)" |
|||
/> |
|||
</Tooltip> |
|||
<Tooltip :title="$t('cropper.btn_zoom_out')" placement="bottom"> |
|||
<Button |
|||
type="primary" |
|||
:icon="h(ZoomOutIcon)" |
|||
size="small" |
|||
:disabled="!src" |
|||
@click="handlerToolbar('zoom', -0.1)" |
|||
/> |
|||
</Tooltip> |
|||
</Space> |
|||
</div> |
|||
</div> |
|||
<div :class="e('right')"> |
|||
<div :class="e(`preview`)"> |
|||
<img |
|||
:src="previewSource" |
|||
v-if="previewSource" |
|||
:alt="$t('cropper.preview')" |
|||
/> |
|||
</div> |
|||
<template v-if="previewSource"> |
|||
<div :class="e(`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> |
|||
</Modal> |
|||
</template> |
|||
|
|||
<style scoped lang="scss"> |
|||
$namespace: vben; |
|||
|
|||
.#{$namespace}-cropper-am { |
|||
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; |
|||
align-items: center; |
|||
justify-content: space-between; |
|||
margin-top: 10px; |
|||
} |
|||
|
|||
&__preview { |
|||
width: 220px; |
|||
height: 220px; |
|||
margin: 0 auto; |
|||
overflow: hidden; |
|||
border: var(--border); |
|||
border-radius: 50%; |
|||
|
|||
img { |
|||
width: 100%; |
|||
height: 100%; |
|||
} |
|||
} |
|||
|
|||
&__group { |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: space-around; |
|||
padding-top: 8px; |
|||
margin-top: 8px; |
|||
border-top: var(--border); |
|||
} |
|||
} |
|||
</style> |
|||
@ -0,0 +1,2 @@ |
|||
export { default as CropperAvatar } from './CropperAvatar.vue'; |
|||
export { default as CropperModal } from './CropperModal.vue'; |
|||
@ -0,0 +1,8 @@ |
|||
import type Cropper from 'cropperjs'; |
|||
|
|||
export interface CropendResult { |
|||
imgBase64: string; |
|||
imgInfo: Cropper.Data; |
|||
} |
|||
|
|||
export type { Cropper }; |
|||
@ -0,0 +1,20 @@ |
|||
import type { SupportedLanguagesType } from '@vben/locales'; |
|||
|
|||
import { loadLocalesMapFromDir } from '@vben/locales'; |
|||
|
|||
const modules = import.meta.glob('./langs/**/*.json'); |
|||
|
|||
const localesMap = loadLocalesMapFromDir( |
|||
/\.\/langs\/([^/]+)\/(.*)\.json$/, |
|||
modules, |
|||
); |
|||
|
|||
/** |
|||
* 加载自定义组件本地化资源 |
|||
* @param lang 当前语言 |
|||
* @returns 资源集合 |
|||
*/ |
|||
export async function loadComponentMessages(lang: SupportedLanguagesType) { |
|||
const locales = localesMap[lang]?.(); |
|||
return locales; |
|||
} |
|||
@ -0,0 +1,14 @@ |
|||
{ |
|||
"confirmText": "Confirm and upload", |
|||
"title": "Avatar upload", |
|||
"selectImage": "Select Image", |
|||
"btn_rotate_left": "Counterclockwise rotation", |
|||
"btn_rotate_right": "Clockwise rotation", |
|||
"btn_scale_x": "Flip horizontal", |
|||
"btn_scale_y": "Flip vertical", |
|||
"btn_zoom_in": "Zoom in", |
|||
"btn_zoom_out": "Zoom out", |
|||
"btn_reset": "Reset", |
|||
"preview": "Preivew", |
|||
"uploadSuccess": "Uploaded success!" |
|||
} |
|||
@ -0,0 +1,14 @@ |
|||
{ |
|||
"confirmText": "确认并上传", |
|||
"title": "头像上传", |
|||
"selectImage": "选择图片", |
|||
"btn_rotate_left": "逆时针旋转", |
|||
"btn_rotate_right": "顺时针旋转", |
|||
"btn_scale_x": "水平翻转", |
|||
"btn_scale_y": "垂直翻转", |
|||
"btn_zoom_in": "放大", |
|||
"btn_zoom_out": "缩小", |
|||
"btn_reset": "重置", |
|||
"preview": "预览", |
|||
"uploadSuccess": "上传成功!" |
|||
} |
|||
@ -0,0 +1,42 @@ |
|||
/* eslint-disable @typescript-eslint/no-non-null-assertion */ |
|||
/** |
|||
* @description: base64 to blob |
|||
*/ |
|||
export function dataURLtoBlob(base64Buf: string): Blob { |
|||
const arr = base64Buf.split(','); |
|||
const typeItem = arr[0]; |
|||
const mime = typeItem?.match(/:(.*?);/)?.[1]; |
|||
const bstr = window.atob(arr[1]!); |
|||
let n = bstr.length; |
|||
const u8arr = new Uint8Array(n); |
|||
while (n--) { |
|||
u8arr[n] = bstr.codePointAt(n)!; |
|||
} |
|||
return new Blob([u8arr], { type: mime }); |
|||
} |
|||
|
|||
/** |
|||
* img url to base64 |
|||
* @param url |
|||
*/ |
|||
export function urlToBase64(url: string, mineType?: string): Promise<string> { |
|||
return new Promise((resolve, reject) => { |
|||
let canvas = document.createElement('CANVAS') as HTMLCanvasElement | null; |
|||
const ctx = canvas!.getContext('2d'); |
|||
|
|||
const img = new Image(); |
|||
img.crossOrigin = ''; |
|||
img.addEventListener('load', () => { |
|||
if (!canvas || !ctx) { |
|||
return reject(new Error('canvas or ctx is null!')); |
|||
} |
|||
canvas.height = img.height; |
|||
canvas.width = img.width; |
|||
ctx.drawImage(img, 0, 0); |
|||
const dataURL = canvas.toDataURL(mineType || 'image/png'); |
|||
canvas = null; |
|||
resolve(dataURL); |
|||
}); |
|||
img.src = url; |
|||
}); |
|||
} |
|||
@ -1,7 +1,10 @@ |
|||
export * from './array'; |
|||
export * from './date'; |
|||
export * from './file'; |
|||
export * from './is'; |
|||
export * from './mitt'; |
|||
export * from './regex'; |
|||
export * from './string'; |
|||
export * from './table'; |
|||
export * from './tree'; |
|||
export * from './uuid'; |
|||
|
|||
@ -0,0 +1,31 @@ |
|||
import { isDate, isNumber } from './is'; |
|||
|
|||
export function sorter( |
|||
a: Record<string, any>, |
|||
b: Record<string, any>, |
|||
field: string, |
|||
): number { |
|||
if (!a[field] && !b[field]) { |
|||
return 0; |
|||
} |
|||
if (a[field] && !b[field]) { |
|||
return 1; |
|||
} |
|||
if (b[field] && !a[field]) { |
|||
return -1; |
|||
} |
|||
const va = a[field]; |
|||
const vb = b[field]; |
|||
if (isDate(va) && isDate(vb)) { |
|||
return va.getTime() - vb.getTime(); |
|||
} |
|||
if (isNumber(va) && isNumber(vb)) { |
|||
return va - vb; |
|||
} |
|||
if (Array.isArray(va) && Array.isArray(vb)) { |
|||
return va.length - vb.length; |
|||
} |
|||
return String(va).localeCompare(String(vb)); |
|||
} |
|||
|
|||
export { default as sortby } from 'lodash.sortby'; |
|||
@ -0,0 +1,42 @@ |
|||
const hexList: string[] = []; |
|||
for (let i = 0; i <= 15; i++) { |
|||
hexList[i] = i.toString(16); |
|||
} |
|||
|
|||
export function buildUUID(): string { |
|||
let uuid = ''; |
|||
for (let i = 1; i <= 36; i++) { |
|||
switch (i) { |
|||
case 9: |
|||
case 14: |
|||
case 19: |
|||
case 24: { |
|||
uuid += '-'; |
|||
|
|||
break; |
|||
} |
|||
case 15: { |
|||
uuid += 4; |
|||
|
|||
break; |
|||
} |
|||
case 20: { |
|||
uuid += hexList[(Math.random() * 4) | 8]; |
|||
|
|||
break; |
|||
} |
|||
default: { |
|||
uuid += hexList[Math.trunc(Math.random() * 16)]; |
|||
} |
|||
} |
|||
} |
|||
return uuid.replaceAll('-', ''); |
|||
} |
|||
|
|||
let unique = 0; |
|||
export function buildShortUUID(prefix = ''): string { |
|||
const time = Date.now(); |
|||
const random = Math.floor(Math.random() * 1_000_000_000); |
|||
unique++; |
|||
return `${prefix}_${random}${unique}${String(time)}`; |
|||
} |
|||
@ -0,0 +1,62 @@ |
|||
import type { ListResultDto } from '@abp/core'; |
|||
|
|||
import type { |
|||
UserFavoriteMenuCreateDto, |
|||
UserFavoriteMenuDto, |
|||
} from '../types/favorites'; |
|||
|
|||
import { useRequest } from '@abp/request'; |
|||
|
|||
export function useMyFavoriteMenusApi() { |
|||
const { cancel, request } = useRequest(); |
|||
|
|||
/** |
|||
* 新增常用菜单 |
|||
* @param input 参数 |
|||
* @returns 常用菜单 |
|||
*/ |
|||
function createApi( |
|||
input: UserFavoriteMenuCreateDto, |
|||
): Promise<UserFavoriteMenuDto> { |
|||
return request<UserFavoriteMenuDto>( |
|||
`/api/platform/menus/favorites/my-favorite-menus`, |
|||
{ |
|||
data: input, |
|||
method: 'POST', |
|||
}, |
|||
); |
|||
} |
|||
|
|||
/** |
|||
* 删除常用菜单 |
|||
* @param id 菜单Id |
|||
*/ |
|||
function deleteApi(id: string): Promise<void> { |
|||
return request(`/api/platform/menus/favorites/my-favorite-menus/${id}`, { |
|||
method: 'DELETE', |
|||
}); |
|||
} |
|||
|
|||
/** |
|||
* 获取常用菜单列表 |
|||
* @param framework ui框架 |
|||
* @returns 菜单列表 |
|||
*/ |
|||
function getListApi( |
|||
framework?: string, |
|||
): Promise<ListResultDto<UserFavoriteMenuDto>> { |
|||
return request<ListResultDto<UserFavoriteMenuDto>>( |
|||
`/api/platform/menus/favorites/my-favorite-menus?framework=${framework}`, |
|||
{ |
|||
method: 'GET', |
|||
}, |
|||
); |
|||
} |
|||
|
|||
return { |
|||
cancel, |
|||
createApi, |
|||
deleteApi, |
|||
getListApi, |
|||
}; |
|||
} |
|||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue