# Conflicts:
#	src/views/mall/statistics/member/index.vue
This commit is contained in:
YunaiV 2023-10-17 19:02:37 +08:00
commit aa99c53729
27 changed files with 856 additions and 98 deletions

View File

@ -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 |

View File

@ -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": {

View File

@ -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 })
}

View File

@ -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({

View File

@ -2,6 +2,9 @@ export type UserLoginVO = {
username: string
password: string
captchaVerification: string
socialType?: string
socialCode?: string
socialState?: string
}
export type TokenType = {

View File

@ -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;
}

View File

@ -114,6 +114,7 @@ $prefix-cls: #{$elNamespace}-breadcrumb;
}
}
}
:deep(&__item):last-child {
.#{$prefix-cls}__inner {
display: flex;

View File

@ -141,6 +141,7 @@ export default {
},
router: {
login: '登录',
socialLogin: '社交登录',
home: '首页',
analysis: '分析页',
workplace: '工作台'

View File

@ -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: [
{

View File

@ -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
}

View File

@ -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
}

View File

@ -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 }

View File

@ -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
}

View File

@ -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.vueredirectUriencodedecode
function getUrlValue(key: string): string {
const url = new URL(decodeURIComponent(location.href))
return url.searchParams.get(key) ?? ''
}
// : socialLogintoken
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>

View File

@ -284,8 +284,13 @@ const doSocialLogin = async (type: number) => {
})
}
// redirectUri
// tricky: typeredirectencode
// 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

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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审批向后加签BA审批完需要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()

View File

@ -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>

View File

@ -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>

View File

@ -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
/>

View File

@ -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

View File

@ -395,27 +395,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>

View File

@ -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>

View File

@ -188,6 +188,7 @@ const refreshMenu = async () => {
try {
await message.confirm('即将更新缓存刷新浏览器!', '刷新菜单缓存')
//
wsCache.delete(CACHE_KEY.USER)
wsCache.delete(CACHE_KEY.ROLE_ROUTERS)
//
location.reload()