diff --git a/README.md b/README.md index 56683973..aa498ce2 100644 --- a/README.md +++ b/README.md @@ -39,14 +39,14 @@ | 框架 | 说明 | 版本 | |----------------------------------------------------------------------|------------------|--------| | [Vue](https://staging-cn.vuejs.org/) | Vue 框架 | 3.3.4 | -| [Vite](https://cn.vitejs.dev//) | 开发与构建工具 | 4.4.9 | -| [Element Plus](https://element-plus.org/zh-CN/) | Element Plus | 2.3.14 | +| [Vite](https://cn.vitejs.dev//) | 开发与构建工具 | 4.4.11 | +| [Element Plus](https://element-plus.org/zh-CN/) | Element Plus | 2.4.0 | | [TypeScript](https://www.typescriptlang.org/docs/) | JavaScript 的超集 | 5.2.2 | -| [pinia](https://pinia.vuejs.org/) | Vue 存储库 替代 vuex5 | 2.1.6 | -| [vueuse](https://vueuse.org/) | 常用工具集 | 10.4.1 | -| [vue-i18n](https://kazupon.github.io/vue-i18n/zh/introduction.html/) | 国际化 | 9.4.1 | +| [pinia](https://pinia.vuejs.org/) | Vue 存储库 替代 vuex5 | 2.1.7 | +| [vueuse](https://vueuse.org/) | 常用工具集 | 10.5.0 | +| [vue-i18n](https://kazupon.github.io/vue-i18n/zh/introduction.html/) | 国际化 | 9.5.0 | | [vue-router](https://router.vuejs.org/) | Vue 路由 | 4.2.5 | -| [unocss](https://uno.antfu.me/) | 原子 css | 0.56.1 | +| [unocss](https://uno.antfu.me/) | 原子 css | 0.56.5 | | [iconify](https://icon-sets.iconify.design/) | 在线图标库 | 3.1.1 | | [wangeditor](https://www.wangeditor.com/) | 富文本编辑器 | 5.1.23 | diff --git a/package.json b/package.json index 22334907..1fb585f3 100644 --- a/package.json +++ b/package.json @@ -31,31 +31,31 @@ "@form-create/element-ui": "^3.1.24", "@iconify/iconify": "^3.1.1", "@videojs-player/vue": "^1.0.0", - "@vueuse/core": "^10.4.1", + "@vueuse/core": "^10.5.0", "@wangeditor/editor": "^5.1.23", "@wangeditor/editor-for-vue": "^5.1.10", "@zxcvbn-ts/core": "^3.0.4", "animate.css": "^4.1.1", - "axios": "^1.5.0", + "axios": "^1.5.1", "benz-amr-recorder": "^1.1.5", "bpmn-js-token-simulation": "^0.10.0", "camunda-bpmn-moddle": "^7.0.1", "cropperjs": "^1.6.1", "crypto-js": "^4.1.1", "dayjs": "^1.11.10", - "diagram-js": "^12.3.0", + "diagram-js": "^12.4.0", "echarts": "^5.4.3", "echarts-wordcloud": "^2.1.0", - "element-plus": "2.3.14", - "fast-xml-parser": "^4.3.0", - "highlight.js": "^11.8.0", + "element-plus": "2.4.0", + "fast-xml-parser": "^4.3.2", + "highlight.js": "^11.9.0", "intro.js": "^7.2.0", "jsencrypt": "^3.3.2", "lodash-es": "^4.17.21", "min-dash": "^4.1.1", "mitt": "^3.0.1", "nprogress": "^0.2.0", - "pinia": "^2.1.6", + "pinia": "^2.1.7", "qrcode": "^1.5.3", "qs": "^6.11.2", "steady-xml": "^0.1.0", @@ -63,7 +63,7 @@ "video.js": "^7.21.5", "vue": "^3.3.4", "vue-dompurify-html": "^4.1.4", - "vue-i18n": "^9.4.1", + "vue-i18n": "^9.5.0", "vue-router": "^4.2.5", "vue-types": "^5.1.1", "vuedraggable": "^4.1.0", @@ -71,54 +71,54 @@ "xml-js": "^1.6.11" }, "devDependencies": { - "@commitlint/cli": "^17.7.1", - "@commitlint/config-conventional": "^17.7.0", - "@iconify/json": "^2.2.119", - "@intlify/unplugin-vue-i18n": "^1.2.0", + "@commitlint/cli": "^17.8.0", + "@commitlint/config-conventional": "^17.8.0", + "@iconify/json": "^2.2.129", + "@intlify/unplugin-vue-i18n": "^1.4.0", "@purge-icons/generated": "^0.9.0", - "@types/intro.js": "^5.1.1", + "@types/intro.js": "^5.1.2", "@types/lodash-es": "^4.17.9", - "@types/node": "^20.6.0", - "@types/nprogress": "^0.2.0", + "@types/node": "^20.8.6", + "@types/nprogress": "^0.2.1", "@types/qrcode": "^1.5.2", "@types/qs": "^6.9.8", - "@typescript-eslint/eslint-plugin": "^6.7.2", - "@typescript-eslint/parser": "^6.7.2", - "@unocss/transformer-variant-group": "^0.56.1", - "@unocss/eslint-config": "^0.56.1", + "@typescript-eslint/eslint-plugin": "^6.7.5", + "@typescript-eslint/parser": "^6.7.5", + "@unocss/transformer-variant-group": "^0.56.5", + "@unocss/eslint-config": "^0.56.5", "@vitejs/plugin-legacy": "^4.1.1", - "@vitejs/plugin-vue": "^4.3.4", + "@vitejs/plugin-vue": "^4.4.0", "@vitejs/plugin-vue-jsx": "^3.0.2", - "@vue-macros/volar": "^0.14.3", + "@vue-macros/volar": "^0.17.0", "autoprefixer": "^10.4.16", "bpmn-js": "8.9.0", "bpmn-js-properties-panel": "0.46.0", "consola": "^3.2.3", - "eslint": "^8.49.0", + "eslint": "^8.51.0", "eslint-config-prettier": "^9.0.0", - "eslint-define-config": "^1.23.0", - "eslint-plugin-prettier": "^5.0.0", + "eslint-define-config": "^1.24.1", + "eslint-plugin-prettier": "^5.0.1", "eslint-plugin-vue": "^9.17.0", - "lint-staged": "^14.0.1", - "postcss": "^8.4.30", + "lint-staged": "^15.0.1", + "postcss": "^8.4.31", "postcss-html": "^1.5.0", - "postcss-scss": "^4.0.8", + "postcss-scss": "^4.0.9", "prettier": "^3.0.3", - "rimraf": "^5.0.1", - "rollup": "^3.29.2", - "sass": "^1.68.0", + "rimraf": "^5.0.5", + "rollup": "^4.1.4", + "sass": "^1.69.3", "stylelint": "^15.10.3", "stylelint-config-html": "^1.1.0", "stylelint-config-recommended": "^13.0.0", "stylelint-config-standard": "^34.0.0", "stylelint-order": "^6.0.3", - "terser": "^5.20.0", + "terser": "^5.21.0", "typescript": "5.2.2", - "unocss": "^0.56.1", + "unocss": "^0.56.5", "unplugin-auto-import": "^0.16.6", "unplugin-element-plus": "^0.8.0", "unplugin-vue-components": "^0.25.2", - "vite": "4.4.9", + "vite": "4.4.11", "vite-plugin-compression": "^0.5.1", "vite-plugin-ejs": "^1.6.4", "vite-plugin-eslint": "^1.8.1", @@ -126,8 +126,8 @@ "vite-plugin-purge-icons": "^0.9.2", "vite-plugin-svg-icons": "^2.0.1", "vite-plugin-top-level-await": "^1.3.1", - "vue-eslint-parser": "^9.3.1", - "vue-tsc": "^1.8.13" + "vue-eslint-parser": "^9.3.2", + "vue-tsc": "^1.8.19" }, "license": "MIT", "repository": { diff --git a/src/api/bpm/task/index.ts b/src/api/bpm/task/index.ts index ccd5c4ee..f1359194 100644 --- a/src/api/bpm/task/index.ts +++ b/src/api/bpm/task/index.ts @@ -58,3 +58,24 @@ export const returnTask = async (data) => { export const delegateTask = async (data) => { return await request.put({ url: '/bpm/task/delegate', data }) } + +/** + * 加签 + */ +export const taskAddSign = async (data) => { + return await request.put({ url: '/bpm/task/add-sign', data }) +} + +/** + * 获取减签任务列表 + */ +export const getChildrenTaskList = async (id: string) => { + return await request.get({ url: '/bpm/task/get-children-task-list?taskId=' + id }) +} + +/** + * 减签 + */ +export const taskSubSign = async (data) => { + return await request.put({ url: '/bpm/task/sub-sign', data }) +} diff --git a/src/api/login/index.ts b/src/api/login/index.ts index b65a90cf..1ffb38d6 100644 --- a/src/api/login/index.ts +++ b/src/api/login/index.ts @@ -47,6 +47,18 @@ export const smsLogin = (data: SmsLoginVO) => { return request.post({ url: '/system/auth/sms-login', data }) } +// 社交快捷登录,使用 code 授权码 +export function socialLogin(type: string, code: string, state: string) { + return request.post({ + url: '/system/auth/social-login', + data: { + type, + code, + state + } + }) +} + // 社交授权的跳转 export const socialAuthRedirect = (type: number, redirectUri: string) => { return request.get({ diff --git a/src/api/login/types.ts b/src/api/login/types.ts index b2173f72..fff81225 100644 --- a/src/api/login/types.ts +++ b/src/api/login/types.ts @@ -2,6 +2,9 @@ export type UserLoginVO = { username: string password: string captchaVerification: string + socialType?: string + socialCode?: string + socialState?: string } export type TokenType = { diff --git a/src/components/bpmnProcessDesigner/package/designer/ProcessViewer.vue b/src/components/bpmnProcessDesigner/package/designer/ProcessViewer.vue index e2cd4679..a7958adb 100644 --- a/src/components/bpmnProcessDesigner/package/designer/ProcessViewer.vue +++ b/src/components/bpmnProcessDesigner/package/designer/ProcessViewer.vue @@ -250,6 +250,12 @@ const getResultCss = (result) => { } else if (result === 5) { // 退回 return 'highlight-return' + } else if (result === 6) { + // 委派 + return 'highlight-return' + } else if (result === 7 || result === 8 || result === 9) { + // 待后加签任务完成/待前加签任务完成/待前置任务完成 + return 'highlight-return' } return '' } @@ -362,7 +368,7 @@ const elementHover = (element) => { } } console.log(html, 'html111111111111111') - elementOverlayIds.value[element.value.id] = toRaw(overlays.value).add(element.value, { + elementOverlayIds.value[element.value.id] = toRaw(overlays.value)?.add(element.value, { position: { left: 0, bottom: 0 }, html: `<div class="element-overlays">${html}</div>` }) @@ -591,14 +597,17 @@ watch( stroke: #e6a23c !important; fill-opacity: 0.2 !important; } + .highlight-return.djs-shape .djs-visual > :nth-child(2) { fill: #e6a23c !important; } + .highlight-return.djs-shape .djs-visual > path { fill: #e6a23c !important; fill-opacity: 0.2 !important; stroke: #e6a23c !important; } + .highlight-return.djs-connection > .djs-visual > path { stroke: #e6a23c !important; } @@ -612,14 +621,17 @@ watch( stroke: #e6a23c !important; fill-opacity: 0.2 !important; } + :deep(.highlight-return.djs-shape .djs-visual > :nth-child(2)) { fill: #e6a23c !important; } + :deep(.highlight-return.djs-shape .djs-visual > path) { fill: #e6a23c !important; fill-opacity: 0.2 !important; stroke: #e6a23c !important; } + :deep(.highlight-return.djs-connection > .djs-visual > path) { stroke: #e6a23c !important; } diff --git a/src/layout/components/Breadcrumb/src/Breadcrumb.vue b/src/layout/components/Breadcrumb/src/Breadcrumb.vue index 1852a59b..4079a066 100644 --- a/src/layout/components/Breadcrumb/src/Breadcrumb.vue +++ b/src/layout/components/Breadcrumb/src/Breadcrumb.vue @@ -114,6 +114,7 @@ $prefix-cls: #{$elNamespace}-breadcrumb; } } } + :deep(&__item):last-child { .#{$prefix-cls}__inner { display: flex; diff --git a/src/locales/zh-CN.ts b/src/locales/zh-CN.ts index cc4bb47e..4f95852f 100644 --- a/src/locales/zh-CN.ts +++ b/src/locales/zh-CN.ts @@ -141,6 +141,7 @@ export default { }, router: { login: '登录', + socialLogin: '社交登录', home: '首页', analysis: '分析页', workplace: '工作台' diff --git a/src/router/modules/remaining.ts b/src/router/modules/remaining.ts index d8172d27..aa260cf1 100644 --- a/src/router/modules/remaining.ts +++ b/src/router/modules/remaining.ts @@ -195,6 +195,16 @@ const remainingRouter: AppRouteRecordRaw[] = [ noTagsView: true } }, + { + path: '/social-login', + component: () => import('@/views/Login/SocialLogin.vue'), + name: 'SocialLogin', + meta: { + hidden: true, + title: t('router.socialLogin'), + noTagsView: true + } + }, { path: '/403', component: () => import('@/views/Error/403.vue'), @@ -333,6 +343,7 @@ const remainingRouter: AppRouteRecordRaw[] = [ { path: '/mall/product', // 商品中心 component: Layout, + name: 'ProductCenter', meta: { hidden: true }, @@ -394,6 +405,7 @@ const remainingRouter: AppRouteRecordRaw[] = [ { path: '/mall/trade', // 交易中心 component: Layout, + name: 'TradeCenter', meta: { hidden: true }, @@ -415,7 +427,7 @@ const remainingRouter: AppRouteRecordRaw[] = [ { path: '/member', component: Layout, - name: 'member', + name: 'MemberCenter', meta: { hidden: true }, children: [ { diff --git a/src/utils/index.ts b/src/utils/index.ts index e6b3173c..d5301ddb 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -205,6 +205,9 @@ export const floatToFixed2 = (num: number | string | undefined): string => { case 1: str = f.toString() + '0' break + case 2: + str = f.toString() + break } return str } diff --git a/src/utils/is.ts b/src/utils/is.ts index 37529859..eec86a93 100644 --- a/src/utils/is.ts +++ b/src/utils/is.ts @@ -19,6 +19,9 @@ export const isObject = (val: any): val is Record<any, any> => { } export const isEmpty = <T = unknown>(val: T): val is T => { + if (val === null) { + return true + } if (isArray(val) || isString(val)) { return val.length === 0 } @@ -103,3 +106,12 @@ export const isUrl = (path: string): boolean => { export const isDark = (): boolean => { return window.matchMedia('(prefers-color-scheme: dark)').matches } + +// 是否是图片链接 +export const isImgPath = (path: string): boolean => { + return /(https?:\/\/|data:image\/).*?\.(png|jpg|jpeg|gif|svg|webp|ico)/gi.test(path) +} + +export const isEmptyVal = (val: any): boolean => { + return val === '' || val === null || val === undefined +} diff --git a/src/utils/propTypes.ts b/src/utils/propTypes.ts index fb8f84e7..863f55cc 100644 --- a/src/utils/propTypes.ts +++ b/src/utils/propTypes.ts @@ -1,12 +1,10 @@ -import { createTypes, VueTypesInterface, VueTypeValidableDef } from 'vue-types' +import { VueTypeValidableDef, VueTypesInterface, createTypes, toValidableType } from 'vue-types' import { CSSProperties } from 'vue' -// 自定义扩展vue-types type PropTypes = VueTypesInterface & { readonly style: VueTypeValidableDef<CSSProperties> } - -const propTypes = createTypes({ +const newPropTypes = createTypes({ func: undefined, bool: undefined, string: undefined, @@ -15,14 +13,12 @@ const propTypes = createTypes({ integer: undefined }) as PropTypes -// 需要自定义扩展的类型 -// see: https://dwightjack.github.io/vue-types/advanced/extending-vue-types.html#the-extend-method -// propTypes.extend([ -// { -// name: 'style', -// getter: true, -// type: [String, Object], -// default: undefined -// } -// ]) +class propTypes extends newPropTypes { + static get style() { + return toValidableType('style', { + type: [String, Object] + }) + } +} + export { propTypes } diff --git a/src/utils/routerHelper.ts b/src/utils/routerHelper.ts index a6825653..d9fe42aa 100644 --- a/src/utils/routerHelper.ts +++ b/src/utils/routerHelper.ts @@ -93,7 +93,10 @@ export const generateRoute = (routes: AppCustomRouteRecordRaw[]): AppRouteRecord meta.alwaysShow = true const childrenData: AppRouteRecordRaw = { path: '', - name: toCamelCase(route.path, true), + name: + route.componentName && route.componentName.length > 0 + ? route.componentName + : toCamelCase(route.path, true), redirect: route.redirect, meta: meta } diff --git a/src/views/Login/SocialLogin.vue b/src/views/Login/SocialLogin.vue new file mode 100644 index 00000000..6bbfc1df --- /dev/null +++ b/src/views/Login/SocialLogin.vue @@ -0,0 +1,343 @@ +<template> + <div + :class="prefixCls" + class="relative h-[100%] lt-xl:bg-[var(--login-bg-color)] lt-md:px-10px lt-sm:px-10px lt-xl:px-10px" + > + <div class="relative mx-auto h-full flex"> + <div + :class="`${prefixCls}__left flex-1 bg-gray-500 bg-opacity-20 relative p-30px lt-xl:hidden`" + > + <!-- 左上角的 logo + 系统标题 --> + <div class="relative flex items-center text-white"> + <img alt="" class="mr-10px h-48px w-48px" src="@/assets/imgs/logo.png" /> + <span class="text-20px font-bold">{{ underlineToHump(appStore.getTitle) }}</span> + </div> + <!-- 左边的背景图 + 欢迎语 --> + <div class="h-[calc(100%-60px)] flex items-center justify-center"> + <TransitionGroup + appear + enter-active-class="animate__animated animate__bounceInLeft" + tag="div" + > + <img key="1" alt="" class="w-350px" src="@/assets/svgs/login-box-bg.svg" /> + <div key="2" class="text-3xl text-white">{{ t('login.welcome') }}</div> + <div key="3" class="mt-5 text-14px font-normal text-white"> + {{ t('login.message') }} + </div> + </TransitionGroup> + </div> + </div> + <div class="relative flex-1 p-30px dark:bg-[var(--login-bg-color)] lt-sm:p-10px"> + <!-- 右上角的主题、语言选择 --> + <div + class="flex items-center justify-between text-white at-2xl:justify-end at-xl:justify-end" + > + <div class="flex items-center at-2xl:hidden at-xl:hidden"> + <img alt="" class="mr-10px h-48px w-48px" src="@/assets/imgs/logo.png" /> + <span class="text-20px font-bold">{{ underlineToHump(appStore.getTitle) }}</span> + </div> + <div class="flex items-center justify-end space-x-10px"> + <ThemeSwitch /> + <LocaleDropdown class="dark:text-white lt-xl:text-white" /> + </div> + </div> + <!-- 右边的登录界面 --> + <Transition appear enter-active-class="animate__animated animate__bounceInRight"> + <div + class="m-auto h-full w-[100%] flex items-center at-2xl:max-w-500px at-lg:max-w-500px at-md:max-w-500px at-xl:max-w-500px" + > + <!-- 账号登录 --> + <el-form + v-show="getShow" + ref="formLogin" + :model="loginData.loginForm" + :rules="LoginRules" + class="login-form" + label-position="top" + label-width="120px" + size="large" + > + <el-row style="margin-right: -10px; margin-left: -10px"> + <el-col :span="24" style="padding-right: 10px; padding-left: 10px"> + <el-form-item> + <LoginFormTitle style="width: 100%" /> + </el-form-item> + </el-col> + <el-col :span="24" style="padding-right: 10px; padding-left: 10px"> + <el-form-item v-if="loginData.tenantEnable === 'true'" prop="tenantName"> + <el-input + v-model="loginData.loginForm.tenantName" + :placeholder="t('login.tenantNamePlaceholder')" + :prefix-icon="iconHouse" + link + type="primary" + /> + </el-form-item> + </el-col> + <el-col :span="24" style="padding-right: 10px; padding-left: 10px"> + <el-form-item prop="username"> + <el-input + v-model="loginData.loginForm.username" + :placeholder="t('login.usernamePlaceholder')" + :prefix-icon="iconAvatar" + /> + </el-form-item> + </el-col> + <el-col :span="24" style="padding-right: 10px; padding-left: 10px"> + <el-form-item prop="password"> + <el-input + v-model="loginData.loginForm.password" + :placeholder="t('login.passwordPlaceholder')" + :prefix-icon="iconLock" + show-password + type="password" + @keyup.enter="getCode()" + /> + </el-form-item> + </el-col> + <el-col + :span="24" + style=" + padding-right: 10px; + padding-left: 10px; + margin-top: -20px; + margin-bottom: -20px; + " + > + <el-form-item> + <el-row justify="space-between" style="width: 100%"> + <el-col :span="6"> + <el-checkbox v-model="loginData.loginForm.rememberMe"> + {{ t('login.remember') }} + </el-checkbox> + </el-col> + <el-col :offset="6" :span="12"> + <el-link style="float: right" type="primary">{{ + t('login.forgetPassword') + }}</el-link> + </el-col> + </el-row> + </el-form-item> + </el-col> + <el-col :span="24" style="padding-right: 10px; padding-left: 10px"> + <el-form-item> + <XButton + :loading="loginLoading" + :title="t('login.login')" + class="w-[100%]" + type="primary" + @click="getCode()" + /> + </el-form-item> + </el-col> + <Verify + ref="verify" + :captchaType="captchaType" + :imgSize="{ width: '400px', height: '200px' }" + mode="pop" + @success="handleLogin" + /> + </el-row> + </el-form> + </div> + </Transition> + </div> + </div> + </div> +</template> + +<script lang="ts" setup> +import { underlineToHump } from '@/utils' + +import { ElLoading } from 'element-plus' + +import { useDesign } from '@/hooks/web/useDesign' +import { useAppStore } from '@/store/modules/app' +import { useIcon } from '@/hooks/web/useIcon' +import { usePermissionStore } from '@/store/modules/permission' + +import * as LoginApi from '@/api/login' +import * as authUtil from '@/utils/auth' +import { ThemeSwitch } from '@/layout/components/ThemeSwitch' +import { LocaleDropdown } from '@/layout/components/LocaleDropdown' +import { LoginStateEnum, useFormValid, useLoginState } from './components/useLogin' +import LoginFormTitle from './components/LoginFormTitle.vue' +import router from '@/router' + +defineOptions({ name: 'SocialLogin' }) + +const { t } = useI18n() +const route = useRoute() + +const appStore = useAppStore() +const { getPrefixCls } = useDesign() +const prefixCls = getPrefixCls('login') +const iconHouse = useIcon({ icon: 'ep:house' }) +const iconAvatar = useIcon({ icon: 'ep:avatar' }) +const iconLock = useIcon({ icon: 'ep:lock' }) +const formLogin = ref<any>() +const { validForm } = useFormValid(formLogin) +const { getLoginState } = useLoginState() +const { push } = useRouter() +const permissionStore = usePermissionStore() +const loginLoading = ref(false) +const verify = ref() +const captchaType = ref('blockPuzzle') // blockPuzzle 滑块 clickWord 点击文字 + +const getShow = computed(() => unref(getLoginState) === LoginStateEnum.LOGIN) + +const LoginRules = { + tenantName: [required], + username: [required], + password: [required] +} +const loginData = reactive({ + isShowPassword: false, + captchaEnable: import.meta.env.VITE_APP_CAPTCHA_ENABLE, + tenantEnable: import.meta.env.VITE_APP_TENANT_ENABLE, + loginForm: { + tenantName: '芋道源码', + username: 'admin', + password: 'admin123', + captchaVerification: '', + rememberMe: false + } +}) + +// 获取验证码 +const getCode = async () => { + // 情况一,未开启:则直接登录 + if (loginData.captchaEnable === 'false') { + await handleLogin({}) + } else { + // 情况二,已开启:则展示验证码;只有完成验证码的情况,才进行登录 + // 弹出验证码 + verify.value.show() + } +} +//获取租户ID +const getTenantId = async () => { + if (loginData.tenantEnable === 'true') { + const res = await LoginApi.getTenantIdByName(loginData.loginForm.tenantName) + authUtil.setTenantId(res) + } +} +// 记住我 +const getCookie = () => { + const loginForm = authUtil.getLoginForm() + if (loginForm) { + loginData.loginForm = { + ...loginData.loginForm, + username: loginForm.username ? loginForm.username : loginData.loginForm.username, + password: loginForm.password ? loginForm.password : loginData.loginForm.password, + rememberMe: loginForm.rememberMe ? true : false, + tenantName: loginForm.tenantName ? loginForm.tenantName : loginData.loginForm.tenantName + } + } +} +const loading = ref() // ElLoading.service 返回的实例 + +// tricky: 配合LoginForm.vue中redirectUri需要对参数进行encode,需要在回调后进行decode +function getUrlValue(key: string): string { + const url = new URL(decodeURIComponent(location.href)) + return url.searchParams.get(key) ?? '' +} + +// 尝试登录: 当账号已经绑定,socialLogin会直接获得token +const tryLogin = async () => { + try { + const type = getUrlValue('type') + const redirect = getUrlValue('redirect') + const code = route?.query?.code as string + const state = route?.query?.state as string + + const res = await LoginApi.socialLogin(type, code, state) + authUtil.setToken(res) + + router.push({ path: redirect || '/' }) + } catch (err) {} +} + +// 登录 +const handleLogin = async (params) => { + loginLoading.value = true + try { + await getTenantId() + const data = await validForm() + if (!data) { + return + } + + let redirect = getUrlValue('redirect') + + const type = getUrlValue('type') + const code = route?.query?.code as string + const state = route?.query?.state as string + + const res = await LoginApi.login({ + // 账号密码登录 + username: loginData.loginForm.username, + password: loginData.loginForm.password, + captchaVerification: params.captchaVerification, + // 社交登录 + socialCode: code, + socialState: state, + socialType: type + }) + if (!res) { + return + } + loading.value = ElLoading.service({ + lock: true, + text: '正在加载系统中...', + background: 'rgba(0, 0, 0, 0.7)' + }) + if (loginData.loginForm.rememberMe) { + authUtil.setLoginForm(loginData.loginForm) + } else { + authUtil.removeLoginForm() + } + authUtil.setToken(res) + if (!redirect) { + redirect = '/' + } + // 判断是否为SSO登录 + if (redirect.indexOf('sso') !== -1) { + window.location.href = window.location.href.replace('/login?redirect=', '') + } else { + push({ path: redirect || permissionStore.addRouters[0].path }) + } + } finally { + loginLoading.value = false + loading.value.close() + } +} + +onMounted(() => { + getCookie() + tryLogin() +}) +</script> + +<style lang="scss" scoped> +$prefix-cls: #{$namespace}-login; + +.#{$prefix-cls} { + overflow: auto; + + &__left { + &::before { + position: absolute; + top: 0; + left: 0; + z-index: -1; + width: 100%; + height: 100%; + background-image: url('@/assets/svgs/login-bg.svg'); + background-position: center; + background-repeat: no-repeat; + content: ''; + } + } +} +</style> diff --git a/src/views/Login/components/LoginForm.vue b/src/views/Login/components/LoginForm.vue index a4eb0b92..9bee2523 100644 --- a/src/views/Login/components/LoginForm.vue +++ b/src/views/Login/components/LoginForm.vue @@ -284,8 +284,13 @@ const doSocialLogin = async (type: number) => { }) } // 计算 redirectUri + // tricky: type、redirect需要先encode一次,否则钉钉回调会丢失。 + // 配合 Login/SocialLogin.vue#getUrlValue() 使用 const redirectUri = - location.origin + '/social-login?type=' + type + '&redirect=' + (redirect.value || '/') + location.origin + + '/social-login?' + + encodeURIComponent(`type=${type}&redirect=${redirect.value || '/'}`) + // 进行跳转 const res = await LoginApi.socialAuthRedirect(type, encodeURIComponent(redirectUri)) window.location.href = res diff --git a/src/views/bpm/processInstance/detail/ProcessInstanceChildrenTaskList.vue b/src/views/bpm/processInstance/detail/ProcessInstanceChildrenTaskList.vue new file mode 100644 index 00000000..f162d1fb --- /dev/null +++ b/src/views/bpm/processInstance/detail/ProcessInstanceChildrenTaskList.vue @@ -0,0 +1,99 @@ +<template> + <el-drawer v-model="drawerVisible" title="子任务" size="70%"> + <template #header> + <h4>【{{ baseTask.name }} 】审批人:{{ baseTask.assigneeUser?.nickname }}</h4> + <el-button style="margin-left: 5px" v-if="showSubSignButton(baseTask)" type="danger" plain @click="handleSubSign(baseTask)"> + <Icon icon="ep:remove" /> + 减签 + </el-button> + </template> + <el-table :data="tableData" style="width: 100%" row-key="id" border> + <el-table-column prop="assigneeUser.nickname" label="审批人" /> + <el-table-column prop="assigneeUser.deptName" label="所在部门" /> + <el-table-column label="审批状态" prop="result"> + <template #default="scope"> + <dict-tag :type="DICT_TYPE.BPM_PROCESS_INSTANCE_RESULT" :value="scope.row.result" /> + </template> + </el-table-column> + <el-table-column + label="提交时间" + align="center" + prop="createTime" + width="180" + :formatter="dateFormatter" + /> + <el-table-column + label="结束时间" + align="center" + prop="endTime" + width="180" + :formatter="dateFormatter" + /> + <el-table-column label="操作" prop="operation"> + <template #default="scope"> + <el-button + v-if="showSubSignButton(scope.row)" + type="danger" + plain + @click="handleSubSign(scope.row)" + > + <Icon icon="ep:remove" /> + 减签 + </el-button> + </template> + </el-table-column> + </el-table> + <!-- 减签 --> + <TaskSubSignDialogForm ref="taskSubSignDialogForm" /> + </el-drawer> +</template> +<script lang="ts" setup> +import { isEmpty } from '@/utils/is' +import { DICT_TYPE } from '@/utils/dict' +import { dateFormatter } from '@/utils/formatTime' +import TaskSubSignDialogForm from './TaskSubSignDialogForm.vue' + +const message = useMessage() // 消息弹窗 +defineOptions({ name: 'ProcessInstancechildrenList' }) + +const drawerVisible = ref(false) // 抽屉的是否展示 + +const tableData = ref<any[]>([]) //表格数据 +const baseTask = ref<object>({}) +/** 打开弹窗 */ +const open = async (task: any) => { + if (isEmpty(task.children)) { + message.warning('该任务没有子任务') + return + } + baseTask.value = task + //设置表格数据 + tableData.value = task.children + //展开抽屉 + drawerVisible.value = true +} +defineExpose({ open }) // 提供 openModal 方法,用于打开弹窗 + +const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调 + +/** + * 减签 + */ +const taskSubSignDialogForm = ref() +const handleSubSign = (item) => { + taskSubSignDialogForm.value.open(item.id) +} + +/** + * 显示减签按钮 + * @param task + */ +const showSubSignButton = (task:any) => { + if(!isEmpty(task.children)){ + //有子任务,且子任务有任意一个是 待处理 和 待前置任务完成 则显示减签按钮 + const subTask = task.children.find((item) => item.result === 1 || item.result === 9) + return !isEmpty(subTask) + } + return false +} +</script> diff --git a/src/views/bpm/processInstance/detail/ProcessInstanceTaskList.vue b/src/views/bpm/processInstance/detail/ProcessInstanceTaskList.vue index 6f4557ae..97287e99 100644 --- a/src/views/bpm/processInstance/detail/ProcessInstanceTaskList.vue +++ b/src/views/bpm/processInstance/detail/ProcessInstanceTaskList.vue @@ -12,7 +12,18 @@ :icon="getTimelineItemIcon(item)" :type="getTimelineItemType(item)" > - <p style="font-weight: 700">任务:{{ item.name }}</p> + <p style="font-weight: 700"> + 任务:{{ item.name }} + <dict-tag :type="DICT_TYPE.BPM_PROCESS_INSTANCE_RESULT" :value="item.result" /> + <el-button + style="margin-left: 5px" + v-if="!isEmpty(item.children)" + @click="openChildrenTask(item)" + > + <Icon icon="ep:memo" /> + 子任务 + </el-button> + </p> <el-card :body-style="{ padding: '10px' }"> <label v-if="item.assigneeUser" style="margin-right: 30px; font-weight: normal"> 审批人:{{ item.assigneeUser.nickname }} @@ -42,11 +53,16 @@ </el-timeline> </div> </el-col> + <!-- 子任务 --> + <ProcessInstanceChildrenTaskList ref="processInstanceChildrenTaskList" /> </el-card> </template> <script lang="ts" setup> import { formatDate, formatPast2 } from '@/utils/formatTime' import { propTypes } from '@/utils/propTypes' +import { DICT_TYPE } from '@/utils/dict' +import { isEmpty } from '@/utils/is' +import ProcessInstanceChildrenTaskList from './ProcessInstanceChildrenTaskList.vue' defineOptions({ name: 'BpmProcessInstanceTaskList' }) @@ -95,6 +111,18 @@ const getTimelineItemType = (item) => { if (item.result === 6) { return 'default' } + if (item.result === 7 || item.result === 8) { + return 'warning' + } return '' } + +/** + * 子任务 + */ +const processInstanceChildrenTaskList = ref() + +const openChildrenTask = (item) => { + processInstanceChildrenTaskList.value.open(item) +} </script> diff --git a/src/views/bpm/processInstance/detail/TaskAddSignDialogForm.vue b/src/views/bpm/processInstance/detail/TaskAddSignDialogForm.vue new file mode 100644 index 00000000..4b91c9b9 --- /dev/null +++ b/src/views/bpm/processInstance/detail/TaskAddSignDialogForm.vue @@ -0,0 +1,97 @@ +<template> + <Dialog v-model="dialogVisible" title="加签" width="500"> + <el-form + ref="formRef" + v-loading="formLoading" + :model="formData" + :rules="formRules" + label-width="110px" + > + <el-form-item label="加签处理人" prop="userIdList"> + <el-select v-model="formData.userIdList" multiple clearable style="width: 100%"> + <el-option + v-for="item in userList" + :key="item.id" + :label="item.nickname" + :value="item.id" + /> + </el-select> + </el-form-item> + <el-form-item label="加签理由" prop="reason"> + <el-input v-model="formData.reason" clearable placeholder="请输入加签理由" /> + </el-form-item> + </el-form> + <template #footer> + <el-button :disabled="formLoading" type="primary" @click="submitForm('before')" + >向前加签</el-button + > + <el-button :disabled="formLoading" type="primary" @click="submitForm('after')" + >向后加签</el-button + > + <el-button @click="dialogVisible = false">取 消</el-button> + </template> + </Dialog> +</template> +<script lang="ts" setup> +import * as TaskApi from '@/api/bpm/task' +import * as UserApi from '@/api/system/user' + +const message = useMessage() // 消息弹窗 +defineOptions({ name: 'BpmTaskUpdateAssigneeForm' }) + +const dialogVisible = ref(false) // 弹窗的是否展示 +const formLoading = ref(false) // 表单的加载中 +const formData = ref({ + id: '', + userIdList: [], + type: '' +}) +const formRules = ref({ + userIdList: [{ required: true, message: '加签处理人不能为空', trigger: 'change' }], + reason: [{ required: true, message: '加签理由不能为空', trigger: 'change' }] +}) + +const formRef = ref() // 表单 Ref +const userList = ref<any[]>([]) // 用户列表 + +/** 打开弹窗 */ +const open = async (id: string) => { + dialogVisible.value = true + resetForm() + formData.value.id = id + // 获得用户列表 + userList.value = await UserApi.getSimpleUserList() +} +defineExpose({ open }) // 提供 openModal 方法,用于打开弹窗 + +/** 提交表单 */ +const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调 +const submitForm = async (type: string) => { + // 校验表单 + if (!formRef) return + const valid = await formRef.value.validate() + if (!valid) return + // 提交请求 + formLoading.value = true + formData.value.type = type + try { + await TaskApi.taskAddSign(formData.value) + message.success('加签成功') + dialogVisible.value = false + // 发送操作成功的事件 + emit('success') + } finally { + formLoading.value = false + } +} + +/** 重置表单 */ +const resetForm = () => { + formData.value = { + id: '', + userIdList: [], + type: '' + } + formRef.value?.resetFields() +} +</script> diff --git a/src/views/bpm/processInstance/detail/TaskSubSignDialogForm.vue b/src/views/bpm/processInstance/detail/TaskSubSignDialogForm.vue new file mode 100644 index 00000000..61f7d68c --- /dev/null +++ b/src/views/bpm/processInstance/detail/TaskSubSignDialogForm.vue @@ -0,0 +1,85 @@ +<template> + <Dialog v-model="dialogVisible" title="减签" width="500"> + <el-form + ref="formRef" + v-loading="formLoading" + :model="formData" + :rules="formRules" + label-width="110px" + > + <el-form-item label="减签任务" prop="id"> + <el-radio-group v-model="formData.id"> + <el-radio-button v-for="item in subTaskList" :key="item.id" :label="item.id"> + {{ item.name }}({{ item.assigneeUser.deptName }}{{ item.assigneeUser.nickname }}--审批) + </el-radio-button> + </el-radio-group> + </el-form-item> + <el-form-item label="减签理由" prop="reason"> + <el-input v-model="formData.reason" clearable placeholder="请输入减签理由" /> + </el-form-item> + </el-form> + <template #footer> + <el-button :disabled="formLoading" type="primary" @click="submitForm">确 定</el-button> + <el-button @click="dialogVisible = false">取 消</el-button> + </template> + </Dialog> +</template> +<script lang="ts" name="TaskRollbackDialogForm" setup> +import * as TaskApi from '@/api/bpm/task' +import { isEmpty } from '@/utils/is' + +const message = useMessage() // 消息弹窗 +const dialogVisible = ref(false) // 弹窗的是否展示 +const formLoading = ref(false) // 表单的加载中 +const formData = ref({ + id: '', + reason: '' +}) +const formRules = ref({ + id: [{ required: true, message: '必须选择减签任务', trigger: 'change' }], + reason: [{ required: true, message: '减签理由不能为空', trigger: 'blur' }] +}) + +const formRef = ref() // 表单 Ref +const subTaskList = ref([]) +/** 打开弹窗 */ +const open = async (id: string) => { + subTaskList.value = await TaskApi.getChildrenTaskList(id) + if (isEmpty(subTaskList.value)) { + message.warning('当前没有可减签的任务') + return false + } + dialogVisible.value = true + resetForm() +} +defineExpose({ open }) // 提供 openModal 方法,用于打开弹窗 + +/** 提交表单 */ +const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调 +const submitForm = async () => { + // 校验表单 + if (!formRef) return + const valid = await formRef.value.validate() + if (!valid) return + // 提交请求 + formLoading.value = true + try { + await TaskApi.taskSubSign(formData.value) + message.success('减签成功') + dialogVisible.value = false + // 发送操作成功的事件 + emit('success') + } finally { + formLoading.value = false + } +} + +/** 重置表单 */ +const resetForm = () => { + formData.value = { + id: '', + reason: '' + } + formRef.value?.resetFields() +} +</script> diff --git a/src/views/bpm/processInstance/detail/index.vue b/src/views/bpm/processInstance/detail/index.vue index 585c60db..f9c5452b 100644 --- a/src/views/bpm/processInstance/detail/index.vue +++ b/src/views/bpm/processInstance/detail/index.vue @@ -49,6 +49,10 @@ <Icon icon="ep:position" /> 委派 </el-button> + <el-button type="primary" @click="handleSign(item)"> + <Icon icon="ep:plus" /> + 加签 + </el-button> <el-button type="warning" @click="handleBack(item)"> <Icon icon="ep:back" /> 回退 @@ -95,6 +99,8 @@ <TaskReturnDialog ref="taskReturnDialogRef" @success="getDetail" /> <!-- 委派,将任务委派给别人处理,处理完成后,会重新回到原审批人手中--> <TaskDelegateForm ref="taskDelegateForm" @success="getDetail" /> + <!-- 加签,当前任务审批人为A,向前加签选了一个C,则需要C先审批,然后再是A审批,向后加签B,A审批完,需要B再审批完,才算完成这个任务节点 --> + <TaskAddSignDialogForm ref="taskAddSignDialogForm" @success="getDetail" /> </ContentWrap> </template> <script lang="ts" setup> @@ -109,7 +115,9 @@ import ProcessInstanceBpmnViewer from './ProcessInstanceBpmnViewer.vue' import ProcessInstanceTaskList from './ProcessInstanceTaskList.vue' import TaskReturnDialog from './TaskReturnDialogForm.vue' import TaskDelegateForm from './taskDelegateForm.vue' +import TaskAddSignDialogForm from './TaskAddSignDialogForm.vue' import { registerComponent } from '@/utils/routerHelper' +import { isEmpty } from '@/utils/is' defineOptions({ name: 'BpmProcessInstanceDetail' }) @@ -185,6 +193,12 @@ const handleBack = async (task) => { taskReturnDialogRef.value.open(task.id) } +const taskAddSignDialogForm = ref() +/** 处理审批加签的操作 */ +const handleSign = async (task) => { + taskAddSignDialogForm.value.open(task.id) +} + /** 获得详情 */ const getDetail = () => { // 1. 获得流程实例相关 @@ -261,26 +275,36 @@ const getTaskList = async () => { // 获得需要自己审批的任务 runningTasks.value = [] auditForms.value = [] - tasks.value.forEach((task) => { - // 2.1 只有待处理才需要 - if (task.result !== 1 && task.result !== 6) { - return - } - // 2.2 自己不是处理人 - if (!task.assigneeUser || task.assigneeUser.id !== userId) { - return - } - // 2.3 添加到处理任务 - runningTasks.value.push({ ...task }) - auditForms.value.push({ - reason: '' - }) - }) + loadRunningTask(tasks.value) } finally { tasksLoad.value = false } } +/** + * 设置 runningTasks 中的任务 + */ +const loadRunningTask = (tasks) => { + tasks.forEach((task) => { + if (!isEmpty(task.children)) { + loadRunningTask(task.children) + } + // 2.1 只有待处理才需要 + if (task.result !== 1 && task.result !== 6) { + return + } + // 2.2 自己不是处理人 + if (!task.assigneeUser || task.assigneeUser.id !== userId) { + return + } + // 2.3 添加到处理任务 + runningTasks.value.push({ ...task }) + auditForms.value.push({ + reason: '' + }) + }) +} + /** 初始化 */ onMounted(() => { getDetail() diff --git a/src/views/mall/promotion/combination/record/index.vue b/src/views/mall/promotion/combination/record/index.vue index cc7a3b26..ec06a0b7 100644 --- a/src/views/mall/promotion/combination/record/index.vue +++ b/src/views/mall/promotion/combination/record/index.vue @@ -6,7 +6,7 @@ <div class="flex items-center"> <div class="h-[50px] w-[50px] flex items-center justify-center" - style="color: rgb(24, 144, 255); background-color: rgba(24, 144, 255, 0.1)" + style="color: rgb(24 144 255); background-color: rgb(24 144 255 / 10%)" > <Icon :size="23" icon="fa:user-times" /> </div> @@ -27,7 +27,7 @@ <div class="flex items-center"> <div class="h-[50px] w-[50px] flex items-center justify-center" - style="color: rgb(162, 119, 255); background-color: rgba(162, 119, 255, 0.1)" + style="color: rgb(162 119 255); background-color: rgb(162 119 255 / 10%)" > <Icon :size="23" icon="fa:user-plus" /> </div> @@ -48,7 +48,7 @@ <div class="flex items-center"> <div class="h-[50px] w-[50px] flex items-center justify-center" - style="color: rgb(162, 119, 255); background-color: rgba(162, 119, 255, 0.1)" + style="color: rgb(162 119 255); background-color: rgb(162 119 255 / 10%)" > <Icon :size="23" icon="fa:user-plus" /> </div> diff --git a/src/views/mall/trade/afterSale/detail/index.vue b/src/views/mall/trade/afterSale/detail/index.vue index 50c759d5..26df0d34 100644 --- a/src/views/mall/trade/afterSale/detail/index.vue +++ b/src/views/mall/trade/afterSale/detail/index.vue @@ -307,7 +307,7 @@ onMounted(async () => { // 时间线样式调整 :deep(.el-timeline) { - margin: 10px 0px 0px 160px; + margin: 10px 0 0 160px; .el-timeline-item__wrapper { position: relative; @@ -328,27 +328,27 @@ onMounted(async () => { background-color: #f7f8fa; &::before { - content: ''; position: absolute; top: 10px; left: 13px; - border-width: 8px; /* 调整尖角大小 */ - border-style: solid; border-color: transparent #f7f8fa transparent transparent; /* 尖角颜色,左侧朝向 */ + border-style: solid; + border-width: 8px; /* 调整尖角大小 */ + content: ''; } } .dot-node-style { - width: 20px; - height: 20px; position: absolute; left: -5px; display: flex; + width: 20px; + height: 20px; + font-size: 10px; + color: #fff; + border-radius: 50%; justify-content: center; align-items: center; - border-radius: 50%; - color: #fff; - font-size: 10px; } } </style> diff --git a/src/views/mall/trade/brokerage/withdraw/index.vue b/src/views/mall/trade/brokerage/withdraw/index.vue index ce4f8768..b14a7767 100644 --- a/src/views/mall/trade/brokerage/withdraw/index.vue +++ b/src/views/mall/trade/brokerage/withdraw/index.vue @@ -122,7 +122,7 @@ <el-image v-if="scope.row.accountQrCodeUrl" :src="scope.row.accountQrCodeUrl" - class="w-40px h-40px" + class="h-40px w-40px" :preview-src-list="[scope.row.accountQrCodeUrl]" preview-teleported /> diff --git a/src/views/mall/trade/config/index.vue b/src/views/mall/trade/config/index.vue index 7905871b..48d04e08 100644 --- a/src/views/mall/trade/config/index.vue +++ b/src/views/mall/trade/config/index.vue @@ -16,9 +16,9 @@ <el-form-item label="退款理由" prop="afterSaleRefundReasons"> <el-select v-model="formData.afterSaleRefundReasons" - allow-create filterable multiple + allow-create placeholder="请直接输入退款理由" > <el-option diff --git a/src/views/mall/trade/order/detail/index.vue b/src/views/mall/trade/order/detail/index.vue index a68eecbc..38b9342e 100644 --- a/src/views/mall/trade/order/detail/index.vue +++ b/src/views/mall/trade/order/detail/index.vue @@ -400,27 +400,27 @@ onMounted(async () => { background-color: #f7f8fa; &::before { - content: ''; /* 必须设置 content 属性 */ position: absolute; top: 10px; left: 13px; /* 将伪元素水平居中 */ - border-width: 8px; /* 调整尖角大小 */ - border-style: solid; border-color: transparent #f7f8fa transparent transparent; /* 尖角颜色,左侧朝向 */ + border-style: solid; + border-width: 8px; /* 调整尖角大小 */ + content: ''; /* 必须设置 content 属性 */ } } .dot-node-style { - width: 20px; - height: 20px; position: absolute; left: -5px; display: flex; + width: 20px; + height: 20px; + font-size: 10px; + color: #fff; + border-radius: 50%; justify-content: center; align-items: center; - border-radius: 50%; - color: #fff; - font-size: 10px; } } </style> diff --git a/src/views/member/user/detail/UserOrderList.vue b/src/views/member/user/detail/UserOrderList.vue index 86c121bc..bf220696 100644 --- a/src/views/member/user/detail/UserOrderList.vue +++ b/src/views/member/user/detail/UserOrderList.vue @@ -192,7 +192,7 @@ <div class="flex items-center"> <el-image :src="row.picUrl" - class="w-30px h-30px mr-10px" + class="mr-10px h-30px w-30px" @click="imagePreview(row.picUrl)" /> <span class="mr-10px">{{ row.spuName }}</span> diff --git a/src/views/system/menu/index.vue b/src/views/system/menu/index.vue index c75c091d..2f564dd9 100644 --- a/src/views/system/menu/index.vue +++ b/src/views/system/menu/index.vue @@ -188,6 +188,7 @@ const refreshMenu = async () => { try { await message.confirm('即将更新缓存刷新浏览器!', '刷新菜单缓存') // 清空,从而触发刷新 + wsCache.delete(CACHE_KEY.USER) wsCache.delete(CACHE_KEY.ROLE_ROUTERS) // 刷新浏览器 location.reload()