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> |
<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' }); |
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> |
</script> |
||||
|
|
||||
<template> |
<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> |
</template> |
||||
|
|||||
@ -1,15 +1,98 @@ |
|||||
<script lang="ts" setup> |
<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({ |
defineOptions({ |
||||
name: 'Vben5AccountMySettings', |
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> |
</script> |
||||
|
|
||||
<template> |
<template> |
||||
<Page> |
<Page> |
||||
<MySetting /> |
<MySetting :bind-items="externalLogins" @on-bind-init="onInit" /> |
||||
|
<WechatWorkUserBindModal @on-login="onBindWorkWeixin" /> |
||||
</Page> |
</Page> |
||||
</template> |
</template> |
||||
|
|||||
@ -1,266 +1,32 @@ |
|||||
<script lang="ts" setup> |
<script setup lang="ts"> |
||||
import type { |
import type { FavoriteMenu } from '@abp/platform'; |
||||
WorkbenchProjectItem, |
|
||||
WorkbenchQuickNavItem, |
|
||||
WorkbenchTodoItem, |
|
||||
WorkbenchTrendItem, |
|
||||
} from '@vben/common-ui'; |
|
||||
|
|
||||
import { ref } from 'vue'; |
|
||||
import { useRouter } from 'vue-router'; |
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 { openWindow } from '@vben/utils'; |
||||
|
|
||||
import AnalyticsVisitsSource from '../analytics/analytics-visits-source.vue'; |
import { Workbench } from '@abp/platform'; |
||||
|
|
||||
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', |
|
||||
}, |
|
||||
]; |
|
||||
|
|
||||
const router = useRouter(); |
const router = useRouter(); |
||||
|
function navTo(menu: FavoriteMenu) { |
||||
// 这是一个示例方法,实际项目中需要根据实际情况进行调整 |
if (menu.path?.startsWith('http')) { |
||||
// This is a sample method, adjust according to the actual project requirements |
openWindow(menu.path); |
||||
function navTo(nav: WorkbenchProjectItem | WorkbenchQuickNavItem) { |
|
||||
if (nav.url?.startsWith('http')) { |
|
||||
openWindow(nav.url); |
|
||||
return; |
return; |
||||
} |
} |
||||
if (nav.url?.startsWith('/')) { |
if (menu.path?.startsWith('/')) { |
||||
router.push(nav.url).catch((error) => { |
router.push(menu.path).catch((error) => { |
||||
console.error('Navigation failed:', error); |
console.error('Navigation failed:', error); |
||||
}); |
}); |
||||
} else { |
} else { |
||||
console.warn(`Unknown URL for navigation item: ${nav.title} -> ${nav.url}`); |
console.warn( |
||||
|
`Unknown URL for navigation item: ${menu.displayName} -> ${menu.path}`, |
||||
|
); |
||||
} |
} |
||||
} |
} |
||||
</script> |
</script> |
||||
|
|
||||
<template> |
<template> |
||||
<div class="p-5"> |
<Workbench @nav-to="navTo" /> |
||||
<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> |
|
||||
</template> |
</template> |
||||
|
|
||||
|
<style scoped></style> |
||||
|
|||||
@ -1,7 +1,5 @@ |
|||||
export { useAccountApi } from './useAccountApi'; |
export { useAccountApi } from './useAccountApi'; |
||||
|
export { useExternalLoginsApi } from './useExternalLoginsApi'; |
||||
export { useMySessionApi } from './useMySessionApi'; |
export { useMySessionApi } from './useMySessionApi'; |
||||
export { usePhoneLoginApi } from './usePhoneLoginApi'; |
|
||||
export { useProfileApi } from './useProfileApi'; |
export { useProfileApi } from './useProfileApi'; |
||||
export { useQrCodeLoginApi } from './useQrCodeLoginApi'; |
export { useScanQrCodeApi } from './useScanQrCodeApi'; |
||||
export { useTokenApi } from './useTokenApi'; |
|
||||
export { useUserInfoApi } from './useUserInfoApi'; |
|
||||
|
|||||
@ -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 './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 './account'; |
||||
|
export * from './bind'; |
||||
|
export * from './external-logins'; |
||||
export * from './profile'; |
export * from './profile'; |
||||
export * from './token'; |
export * from './token'; |
||||
export * from './user'; |
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 './array'; |
||||
export * from './date'; |
export * from './date'; |
||||
|
export * from './file'; |
||||
export * from './is'; |
export * from './is'; |
||||
export * from './mitt'; |
export * from './mitt'; |
||||
export * from './regex'; |
export * from './regex'; |
||||
export * from './string'; |
export * from './string'; |
||||
|
export * from './table'; |
||||
export * from './tree'; |
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