Merge branch 'dev' of https://gitee.com/wangbosheng/yudao-ui-admin-vue3 into dev
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<Error type="403" @error-click="push('/')" />
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
<script lang="ts" name="Error403" setup>
|
||||
const { push } = useRouter()
|
||||
</script>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<Error @error-click="push('/')" />
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
<script lang="ts" name="Error404" setup>
|
||||
const { push } = useRouter()
|
||||
</script>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<Error type="500" @error-click="push('/')" />
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
<script lang="ts" name="Error500" setup>
|
||||
const { push } = useRouter()
|
||||
</script>
|
||||
|
||||
@@ -1,26 +1,26 @@
|
||||
<template>
|
||||
<el-row :gutter="20" justify="space-between" :class="prefixCls">
|
||||
<el-col :xl="6" :lg="6" :md="12" :sm="12" :xs="24">
|
||||
<el-card shadow="hover" class="mb-20px">
|
||||
<el-skeleton :loading="loading" animated :rows="2">
|
||||
<el-row :class="prefixCls" :gutter="20" justify="space-between">
|
||||
<el-col :lg="6" :md="12" :sm="12" :xl="6" :xs="24">
|
||||
<el-card class="mb-20px" shadow="hover">
|
||||
<el-skeleton :loading="loading" :rows="2" animated>
|
||||
<template #default>
|
||||
<div :class="`${prefixCls}__item flex justify-between`">
|
||||
<div>
|
||||
<div
|
||||
:class="`${prefixCls}__item--icon ${prefixCls}__item--peoples p-16px inline-block rounded-6px`"
|
||||
>
|
||||
<Icon icon="svg-icon:peoples" :size="40" />
|
||||
<Icon :size="40" icon="svg-icon:peoples" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col justify-between">
|
||||
<div :class="`${prefixCls}__item--text text-16px text-gray-500 text-right`">{{
|
||||
t('analysis.newUser')
|
||||
}}</div>
|
||||
<div :class="`${prefixCls}__item--text text-16px text-gray-500 text-right`"
|
||||
>{{ t('analysis.newUser') }}
|
||||
</div>
|
||||
<CountTo
|
||||
class="text-20px font-700 text-right"
|
||||
:start-val="0"
|
||||
:end-val="102400"
|
||||
:duration="2600"
|
||||
:end-val="102400"
|
||||
:start-val="0"
|
||||
class="text-20px font-700 text-right"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -29,27 +29,27 @@
|
||||
</el-card>
|
||||
</el-col>
|
||||
|
||||
<el-col :xl="6" :lg="6" :md="12" :sm="12" :xs="24">
|
||||
<el-card shadow="hover" class="mb-20px">
|
||||
<el-skeleton :loading="loading" animated :rows="2">
|
||||
<el-col :lg="6" :md="12" :sm="12" :xl="6" :xs="24">
|
||||
<el-card class="mb-20px" shadow="hover">
|
||||
<el-skeleton :loading="loading" :rows="2" animated>
|
||||
<template #default>
|
||||
<div :class="`${prefixCls}__item flex justify-between`">
|
||||
<div>
|
||||
<div
|
||||
:class="`${prefixCls}__item--icon ${prefixCls}__item--message p-16px inline-block rounded-6px`"
|
||||
>
|
||||
<Icon icon="svg-icon:message" :size="40" />
|
||||
<Icon :size="40" icon="svg-icon:message" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col justify-between">
|
||||
<div :class="`${prefixCls}__item--text text-16px text-gray-500 text-right`">{{
|
||||
t('analysis.unreadInformation')
|
||||
}}</div>
|
||||
<div :class="`${prefixCls}__item--text text-16px text-gray-500 text-right`"
|
||||
>{{ t('analysis.unreadInformation') }}
|
||||
</div>
|
||||
<CountTo
|
||||
class="text-20px font-700 text-right"
|
||||
:start-val="0"
|
||||
:end-val="81212"
|
||||
:duration="2600"
|
||||
:end-val="81212"
|
||||
:start-val="0"
|
||||
class="text-20px font-700 text-right"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -58,27 +58,27 @@
|
||||
</el-card>
|
||||
</el-col>
|
||||
|
||||
<el-col :xl="6" :lg="6" :md="12" :sm="12" :xs="24">
|
||||
<el-card shadow="hover" class="mb-20px">
|
||||
<el-skeleton :loading="loading" animated :rows="2">
|
||||
<el-col :lg="6" :md="12" :sm="12" :xl="6" :xs="24">
|
||||
<el-card class="mb-20px" shadow="hover">
|
||||
<el-skeleton :loading="loading" :rows="2" animated>
|
||||
<template #default>
|
||||
<div :class="`${prefixCls}__item flex justify-between`">
|
||||
<div>
|
||||
<div
|
||||
:class="`${prefixCls}__item--icon ${prefixCls}__item--money p-16px inline-block rounded-6px`"
|
||||
>
|
||||
<Icon icon="svg-icon:money" :size="40" />
|
||||
<Icon :size="40" icon="svg-icon:money" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col justify-between">
|
||||
<div :class="`${prefixCls}__item--text text-16px text-gray-500 text-right`">{{
|
||||
t('analysis.transactionAmount')
|
||||
}}</div>
|
||||
<div :class="`${prefixCls}__item--text text-16px text-gray-500 text-right`"
|
||||
>{{ t('analysis.transactionAmount') }}
|
||||
</div>
|
||||
<CountTo
|
||||
class="text-20px font-700 text-right"
|
||||
:start-val="0"
|
||||
:end-val="9280"
|
||||
:duration="2600"
|
||||
:end-val="9280"
|
||||
:start-val="0"
|
||||
class="text-20px font-700 text-right"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -87,27 +87,27 @@
|
||||
</el-card>
|
||||
</el-col>
|
||||
|
||||
<el-col :xl="6" :lg="6" :md="12" :sm="12" :xs="24">
|
||||
<el-card shadow="hover" class="mb-20px">
|
||||
<el-skeleton :loading="loading" animated :rows="2">
|
||||
<el-col :lg="6" :md="12" :sm="12" :xl="6" :xs="24">
|
||||
<el-card class="mb-20px" shadow="hover">
|
||||
<el-skeleton :loading="loading" :rows="2" animated>
|
||||
<template #default>
|
||||
<div :class="`${prefixCls}__item flex justify-between`">
|
||||
<div>
|
||||
<div
|
||||
:class="`${prefixCls}__item--icon ${prefixCls}__item--shopping p-16px inline-block rounded-6px`"
|
||||
>
|
||||
<Icon icon="svg-icon:shopping" :size="40" />
|
||||
<Icon :size="40" icon="svg-icon:shopping" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col justify-between">
|
||||
<div :class="`${prefixCls}__item--text text-16px text-gray-500 text-right`">{{
|
||||
t('analysis.totalShopping')
|
||||
}}</div>
|
||||
<div :class="`${prefixCls}__item--text text-16px text-gray-500 text-right`"
|
||||
>{{ t('analysis.totalShopping') }}
|
||||
</div>
|
||||
<CountTo
|
||||
class="text-20px font-700 text-right"
|
||||
:start-val="0"
|
||||
:end-val="13600"
|
||||
:duration="2600"
|
||||
:end-val="13600"
|
||||
:start-val="0"
|
||||
class="text-20px font-700 text-right"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -117,36 +117,36 @@
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="20" justify="space-between">
|
||||
<el-col :xl="10" :lg="10" :md="24" :sm="24" :xs="24">
|
||||
<el-card shadow="hover" class="mb-20px">
|
||||
<el-col :lg="10" :md="24" :sm="24" :xl="10" :xs="24">
|
||||
<el-card class="mb-20px" shadow="hover">
|
||||
<el-skeleton :loading="loading" animated>
|
||||
<Echart :options="pieOptionsData" :height="300" />
|
||||
<Echart :height="300" :options="pieOptionsData" />
|
||||
</el-skeleton>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :xl="14" :lg="14" :md="24" :sm="24" :xs="24">
|
||||
<el-card shadow="hover" class="mb-20px">
|
||||
<el-col :lg="14" :md="24" :sm="24" :xl="14" :xs="24">
|
||||
<el-card class="mb-20px" shadow="hover">
|
||||
<el-skeleton :loading="loading" animated>
|
||||
<Echart :options="barOptionsData" :height="300" />
|
||||
<Echart :height="300" :options="barOptionsData" />
|
||||
</el-skeleton>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="24">
|
||||
<el-card shadow="hover" class="mb-20px">
|
||||
<el-skeleton :loading="loading" animated :rows="4">
|
||||
<Echart :options="lineOptionsData" :height="350" />
|
||||
<el-card class="mb-20px" shadow="hover">
|
||||
<el-skeleton :loading="loading" :rows="4" animated>
|
||||
<Echart :height="350" :options="lineOptionsData" />
|
||||
</el-skeleton>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
<script lang="ts" name="Home2" setup>
|
||||
import { set } from 'lodash-es'
|
||||
import { EChartsOption } from 'echarts'
|
||||
|
||||
import { useDesign } from '@/hooks/web/useDesign'
|
||||
import type { AnalysisTotalTypes } from './types'
|
||||
import { pieOptions, barOptions, lineOptions } from './echarts-data'
|
||||
import { barOptions, lineOptions, pieOptions } from './echarts-data'
|
||||
|
||||
const { t } = useI18n()
|
||||
const loading = ref(true)
|
||||
@@ -291,18 +291,23 @@ $prefix-cls: #{$namespace}-panel;
|
||||
:deep(.#{$namespace}-icon) {
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
.#{$prefix-cls}__item--icon {
|
||||
transition: all 0.38s ease-out;
|
||||
}
|
||||
|
||||
.#{$prefix-cls}__item--peoples {
|
||||
background: #40c9c6;
|
||||
}
|
||||
|
||||
.#{$prefix-cls}__item--message {
|
||||
background: #36a3f7;
|
||||
}
|
||||
|
||||
.#{$prefix-cls}__item--money {
|
||||
background: #f4516c;
|
||||
}
|
||||
|
||||
.#{$prefix-cls}__item--shopping {
|
||||
background: #34bfa3;
|
||||
}
|
||||
|
||||
@@ -60,7 +60,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
<script lang="ts" name="Login" setup>
|
||||
import { underlineToHump } from '@/utils'
|
||||
|
||||
import { useDesign } from '@/hooks/web/useDesign'
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
<template>
|
||||
<el-form
|
||||
:model="loginData.loginForm"
|
||||
:rules="LoginRules"
|
||||
label-position="top"
|
||||
class="login-form"
|
||||
label-width="120px"
|
||||
size="large"
|
||||
v-show="getShow"
|
||||
ref="formLogin"
|
||||
:model="loginData.loginForm"
|
||||
:rules="LoginRules"
|
||||
class="login-form"
|
||||
label-position="top"
|
||||
label-width="120px"
|
||||
size="large"
|
||||
>
|
||||
<el-row style="maring-left: -10px; maring-right: -10px">
|
||||
<el-col :span="24" style="padding-left: 10px; padding-right: 10px">
|
||||
@@ -16,12 +16,13 @@
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="24" style="padding-left: 10px; padding-right: 10px">
|
||||
<el-form-item prop="tenantName" v-if="loginData.tenantEnable === 'true'">
|
||||
<el-form-item v-if="loginData.tenantEnable === 'true'" prop="tenantName">
|
||||
<el-input
|
||||
type="text"
|
||||
v-model="loginData.loginForm.tenantName"
|
||||
:placeholder="t('login.tenantNamePlaceholder')"
|
||||
:prefix-icon="iconHouse"
|
||||
type="primary"
|
||||
link
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
@@ -38,11 +39,11 @@
|
||||
<el-form-item prop="password">
|
||||
<el-input
|
||||
v-model="loginData.loginForm.password"
|
||||
type="password"
|
||||
:placeholder="t('login.passwordPlaceholder')"
|
||||
show-password
|
||||
@keyup.enter="getCode()"
|
||||
:prefix-icon="iconLock"
|
||||
show-password
|
||||
type="password"
|
||||
@keyup.enter="getCode()"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
@@ -57,8 +58,8 @@
|
||||
{{ t('login.remember') }}
|
||||
</el-checkbox>
|
||||
</el-col>
|
||||
<el-col :span="12" :offset="6">
|
||||
<el-link type="primary" style="float: right">{{ t('login.forgetPassword') }}</el-link>
|
||||
<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>
|
||||
@@ -67,41 +68,41 @@
|
||||
<el-form-item>
|
||||
<XButton
|
||||
:loading="loginLoading"
|
||||
type="primary"
|
||||
class="w-[100%]"
|
||||
:title="t('login.login')"
|
||||
class="w-[100%]"
|
||||
type="primary"
|
||||
@click="getCode()"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<Verify
|
||||
ref="verify"
|
||||
mode="pop"
|
||||
:captchaType="captchaType"
|
||||
:imgSize="{ width: '400px', height: '200px' }"
|
||||
mode="pop"
|
||||
@success="handleLogin"
|
||||
/>
|
||||
<el-col :span="24" style="padding-left: 10px; padding-right: 10px">
|
||||
<el-form-item>
|
||||
<el-row justify="space-between" style="width: 100%" :gutter="5">
|
||||
<el-row :gutter="5" justify="space-between" style="width: 100%">
|
||||
<el-col :span="8">
|
||||
<XButton
|
||||
class="w-[100%]"
|
||||
:title="t('login.btnMobile')"
|
||||
class="w-[100%]"
|
||||
@click="setLoginState(LoginStateEnum.MOBILE)"
|
||||
/>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<XButton
|
||||
class="w-[100%]"
|
||||
:title="t('login.btnQRCode')"
|
||||
class="w-[100%]"
|
||||
@click="setLoginState(LoginStateEnum.QR_CODE)"
|
||||
/>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<XButton
|
||||
class="w-[100%]"
|
||||
:title="t('login.btnRegister')"
|
||||
class="w-[100%]"
|
||||
@click="setLoginState(LoginStateEnum.REGISTER)"
|
||||
/>
|
||||
</el-col>
|
||||
@@ -124,10 +125,25 @@
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-divider content-position="center">萌新必读</el-divider>
|
||||
<el-col :span="24" style="padding-left: 10px; padding-right: 10px">
|
||||
<el-form-item>
|
||||
<div class="flex justify-between w-[100%]">
|
||||
<el-link href="https://doc.iocoder.cn/" target="_blank">📚开发指南</el-link>
|
||||
<el-link href="https://doc.iocoder.cn/video/" target="_blank">🔥视频教程</el-link>
|
||||
<el-link href="https://www.iocoder.cn/Interview/good-collection/" target="_blank">
|
||||
⚡面试手册
|
||||
</el-link>
|
||||
<el-link href="http://static.yudao.iocoder.cn/mp/Aix9975.jpeg" target="_blank">
|
||||
🤝外包咨询
|
||||
</el-link>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-form>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
<script lang="ts" name="LoginForm" setup>
|
||||
import { ElLoading } from 'element-plus'
|
||||
import LoginFormTitle from './LoginFormTitle.vue'
|
||||
import type { RouteLocationNormalizedLoaded } from 'vue-router'
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
{{ getFormTitle }}
|
||||
</h2>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
<script lang="ts" name="LoginFormTitle" setup>
|
||||
import { LoginStateEnum, useLoginState } from './useLogin'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
<template>
|
||||
<el-form
|
||||
:model="loginData.loginForm"
|
||||
:rules="rules"
|
||||
label-position="top"
|
||||
class="login-form"
|
||||
label-width="120px"
|
||||
size="large"
|
||||
v-show="getShow"
|
||||
ref="formSmsLogin"
|
||||
:model="loginData.loginForm"
|
||||
:rules="rules"
|
||||
class="login-form"
|
||||
label-position="top"
|
||||
label-width="120px"
|
||||
size="large"
|
||||
>
|
||||
<el-row style="margin-left: -10px; margin-right: -10px">
|
||||
<!-- 租户名 -->
|
||||
@@ -17,12 +17,13 @@
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="24" style="padding-left: 10px; padding-right: 10px">
|
||||
<el-form-item prop="tenantName" v-if="loginData.tenantEnable === 'true'">
|
||||
<el-form-item v-if="loginData.tenantEnable === 'true'" prop="tenantName">
|
||||
<el-input
|
||||
type="text"
|
||||
v-model="loginData.loginForm.tenantName"
|
||||
:placeholder="t('login.tenantNamePlaceholder')"
|
||||
:prefix-icon="iconHouse"
|
||||
type="primary"
|
||||
link
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
@@ -39,7 +40,7 @@
|
||||
<!-- 验证码 -->
|
||||
<el-col :span="24" style="padding-left: 10px; padding-right: 10px">
|
||||
<el-form-item prop="code">
|
||||
<el-row justify="space-between" style="width: 100%" :gutter="5">
|
||||
<el-row :gutter="5" justify="space-between" style="width: 100%">
|
||||
<el-col :span="24">
|
||||
<el-input
|
||||
v-model="loginData.loginForm.code"
|
||||
@@ -50,9 +51,9 @@
|
||||
<template #append>
|
||||
<span
|
||||
v-if="mobileCodeTimer <= 0"
|
||||
@click="getSmsCode"
|
||||
class="getMobileCode"
|
||||
style="cursor: pointer"
|
||||
@click="getSmsCode"
|
||||
>
|
||||
{{ t('login.getSmsCode') }}
|
||||
</span>
|
||||
@@ -71,9 +72,9 @@
|
||||
<el-form-item>
|
||||
<XButton
|
||||
:loading="loginLoading"
|
||||
type="primary"
|
||||
class="w-[100%]"
|
||||
:title="t('login.login')"
|
||||
class="w-[100%]"
|
||||
type="primary"
|
||||
@click="signIn()"
|
||||
/>
|
||||
</el-form-item>
|
||||
@@ -82,8 +83,8 @@
|
||||
<el-form-item>
|
||||
<XButton
|
||||
:loading="loginLoading"
|
||||
class="w-[100%]"
|
||||
:title="t('login.backLogin')"
|
||||
class="w-[100%]"
|
||||
@click="handleBackLogin()"
|
||||
/>
|
||||
</el-form-item>
|
||||
@@ -91,7 +92,7 @@
|
||||
</el-row>
|
||||
</el-form>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
<script lang="ts" name="MobileForm" setup>
|
||||
import type { RouteLocationNormalizedLoaded } from 'vue-router'
|
||||
|
||||
import { useIcon } from '@/hooks/web/useIcon'
|
||||
@@ -100,7 +101,7 @@ import { setTenantId, setToken } from '@/utils/auth'
|
||||
import { usePermissionStore } from '@/store/modules/permission'
|
||||
import { getTenantIdByName, sendSmsCode, smsLogin } from '@/api/login'
|
||||
import LoginFormTitle from './LoginFormTitle.vue'
|
||||
import { useLoginState, LoginStateEnum, useFormValid } from './useLogin'
|
||||
import { LoginStateEnum, useFormValid, useLoginState } from './useLogin'
|
||||
|
||||
const { t } = useI18n()
|
||||
const message = useMessage()
|
||||
|
||||
@@ -4,23 +4,23 @@
|
||||
<LoginFormTitle style="width: 100%" />
|
||||
</el-col>
|
||||
<el-col :span="24" style="padding-left: 10px; padding-right: 10px">
|
||||
<el-card shadow="hover" class="mb-10px text-center">
|
||||
<el-card class="mb-10px text-center" shadow="hover">
|
||||
<Qrcode :logo="logoImg" />
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-divider class="enter-x">{{ t('login.qrcode') }}</el-divider>
|
||||
<el-col :span="24" style="padding-left: 10px; padding-right: 10px">
|
||||
<div class="w-[100%] mt-15px">
|
||||
<XButton class="w-[100%]" :title="t('login.backLogin')" @click="handleBackLogin()" />
|
||||
<XButton :title="t('login.backLogin')" class="w-[100%]" @click="handleBackLogin()" />
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
<script lang="ts" name="QrCodeForm" setup>
|
||||
import logoImg from '@/assets/imgs/logo.png'
|
||||
|
||||
import LoginFormTitle from './LoginFormTitle.vue'
|
||||
import { useLoginState, LoginStateEnum } from './useLogin'
|
||||
import { LoginStateEnum, useLoginState } from './useLogin'
|
||||
|
||||
const { t } = useI18n()
|
||||
const { handleBackLogin, getLoginState } = useLoginState()
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
<template>
|
||||
<Form
|
||||
:schema="schema"
|
||||
:rules="rules"
|
||||
label-position="top"
|
||||
hide-required-asterisk
|
||||
size="large"
|
||||
v-show="getShow"
|
||||
:rules="rules"
|
||||
:schema="schema"
|
||||
class="dark:(border-1 border-[var(--el-border-color)] border-solid)"
|
||||
hide-required-asterisk
|
||||
label-position="top"
|
||||
size="large"
|
||||
@register="register"
|
||||
>
|
||||
<template #title>
|
||||
@@ -23,25 +23,25 @@
|
||||
<div class="w-[100%]">
|
||||
<XButton
|
||||
:loading="loading"
|
||||
type="primary"
|
||||
class="w-[100%]"
|
||||
:title="t('login.register')"
|
||||
class="w-[100%]"
|
||||
type="primary"
|
||||
@click="loginRegister()"
|
||||
/>
|
||||
</div>
|
||||
<div class="w-[100%] mt-15px">
|
||||
<XButton class="w-[100%]" :title="t('login.hasUser')" @click="handleBackLogin()" />
|
||||
<XButton :title="t('login.hasUser')" class="w-[100%]" @click="handleBackLogin()" />
|
||||
</div>
|
||||
</template>
|
||||
</Form>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
<script lang="ts" name="RegisterForm" setup>
|
||||
import type { FormRules } from 'element-plus'
|
||||
|
||||
import { useForm } from '@/hooks/web/useForm'
|
||||
import { useValidator } from '@/hooks/web/useValidator'
|
||||
import LoginFormTitle from './LoginFormTitle.vue'
|
||||
import { useLoginState, LoginStateEnum } from './useLogin'
|
||||
import { LoginStateEnum, useLoginState } from './useLogin'
|
||||
import { FormSchema } from '@/types/form'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<Form ref="formRef" :rules="rules" :schema="schema" :labelWidth="80">
|
||||
<Form ref="formRef" :labelWidth="80" :rules="rules" :schema="schema">
|
||||
<template #sex="form">
|
||||
<el-radio-group v-model="form['sex']">
|
||||
<el-radio :label="1">{{ t('profile.user.man') }}</el-radio>
|
||||
@@ -8,9 +8,9 @@
|
||||
</template>
|
||||
</Form>
|
||||
<XButton :title="t('common.save')" @click="submit()" />
|
||||
<XButton type="danger" :title="t('common.reset')" @click="init()" />
|
||||
<XButton :title="t('common.reset')" type="danger" @click="init()" />
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
<script lang="ts" name="BasicInfo" setup>
|
||||
import type { FormRules } from 'element-plus'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
|
||||
@@ -5,41 +5,48 @@
|
||||
</div>
|
||||
<ul class="list-group list-group-striped">
|
||||
<li class="list-group-item">
|
||||
<Icon icon="ep:user" class="mr-5px" />{{ t('profile.user.username') }}
|
||||
<Icon class="mr-5px" icon="ep:user" />
|
||||
{{ t('profile.user.username') }}
|
||||
<div class="pull-right">{{ userInfo?.username }}</div>
|
||||
</li>
|
||||
<li class="list-group-item">
|
||||
<Icon icon="ep:phone" class="mr-5px" />{{ t('profile.user.mobile') }}
|
||||
<Icon class="mr-5px" icon="ep:phone" />
|
||||
{{ t('profile.user.mobile') }}
|
||||
<div class="pull-right">{{ userInfo?.mobile }}</div>
|
||||
</li>
|
||||
<li class="list-group-item">
|
||||
<Icon icon="fontisto:email" class="mr-5px" />{{ t('profile.user.email') }}
|
||||
<Icon class="mr-5px" icon="fontisto:email" />
|
||||
{{ t('profile.user.email') }}
|
||||
<div class="pull-right">{{ userInfo?.email }}</div>
|
||||
</li>
|
||||
<li class="list-group-item">
|
||||
<Icon icon="carbon:tree-view-alt" class="mr-5px" />{{ t('profile.user.dept') }}
|
||||
<div class="pull-right" v-if="userInfo?.dept">{{ userInfo?.dept.name }}</div>
|
||||
<Icon class="mr-5px" icon="carbon:tree-view-alt" />
|
||||
{{ t('profile.user.dept') }}
|
||||
<div v-if="userInfo?.dept" class="pull-right">{{ userInfo?.dept.name }}</div>
|
||||
</li>
|
||||
<li class="list-group-item">
|
||||
<Icon icon="ep:suitcase" class="mr-5px" />{{ t('profile.user.posts') }}
|
||||
<div class="pull-right" v-if="userInfo?.posts">
|
||||
<Icon class="mr-5px" icon="ep:suitcase" />
|
||||
{{ t('profile.user.posts') }}
|
||||
<div v-if="userInfo?.posts" class="pull-right">
|
||||
{{ userInfo?.posts.map((post) => post.name).join(',') }}
|
||||
</div>
|
||||
</li>
|
||||
<li class="list-group-item">
|
||||
<Icon icon="icon-park-outline:peoples" class="mr-5px" />{{ t('profile.user.roles') }}
|
||||
<div class="pull-right" v-if="userInfo?.roles">
|
||||
<Icon class="mr-5px" icon="icon-park-outline:peoples" />
|
||||
{{ t('profile.user.roles') }}
|
||||
<div v-if="userInfo?.roles" class="pull-right">
|
||||
{{ userInfo?.roles.map((role) => role.name).join(',') }}
|
||||
</div>
|
||||
</li>
|
||||
<li class="list-group-item">
|
||||
<Icon icon="ep:calendar" class="mr-5px" />{{ t('profile.user.createTime') }}
|
||||
<Icon class="mr-5px" icon="ep:calendar" />
|
||||
{{ t('profile.user.createTime') }}
|
||||
<div class="pull-right">{{ formatDate(userInfo?.createTime) }}</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
<script lang="ts" name="ProfileUser" setup>
|
||||
import { formatDate } from '@/utils/formatTime'
|
||||
import UserAvatar from './UserAvatar.vue'
|
||||
|
||||
@@ -62,6 +69,7 @@ onMounted(async () => {
|
||||
position: relative;
|
||||
height: 120px;
|
||||
}
|
||||
|
||||
.list-group-striped > .list-group-item {
|
||||
border-left: 0;
|
||||
border-right: 0;
|
||||
@@ -82,6 +90,7 @@ onMounted(async () => {
|
||||
padding: 11px 0px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.pull-right {
|
||||
float: right !important;
|
||||
}
|
||||
|
||||
@@ -10,13 +10,13 @@
|
||||
<InputPassword v-model="password.confirmPassword" strength />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<XButton type="primary" @click="submit(formRef)" :title="t('common.save')" />
|
||||
<XButton type="danger" :title="t('common.reset')" @click="reset(formRef)" />
|
||||
<XButton :title="t('common.save')" type="primary" @click="submit(formRef)" />
|
||||
<XButton :title="t('common.reset')" type="danger" @click="reset(formRef)" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import type { FormRules, FormInstance } from 'element-plus'
|
||||
<script lang="ts" name="ResetPwd" setup>
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
|
||||
import { InputPassword } from '@/components/InputPassword'
|
||||
import { updateUserPassword } from '@/api/system/user/profile'
|
||||
|
||||
@@ -2,17 +2,18 @@
|
||||
<div class="change-avatar">
|
||||
<CropperAvatar
|
||||
ref="cropperRef"
|
||||
:value="avatar"
|
||||
:showBtn="false"
|
||||
@change="handelUpload"
|
||||
:btnProps="{ preIcon: 'ant-design:cloud-upload-outlined' }"
|
||||
:showBtn="false"
|
||||
:value="avatar"
|
||||
width="120px"
|
||||
@change="handelUpload"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
<script lang="ts" name="UserAvatar" setup>
|
||||
import { propTypes } from '@/utils/propTypes'
|
||||
import { uploadAvatar } from '@/api/system/user/profile'
|
||||
|
||||
const props = defineProps({
|
||||
img: propTypes.string.def('')
|
||||
})
|
||||
@@ -27,7 +28,7 @@ const handelUpload = async ({ data }) => {
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
<style lang="scss" scoped>
|
||||
.change-avatar {
|
||||
img {
|
||||
display: block;
|
||||
|
||||
@@ -1,30 +1,30 @@
|
||||
<template>
|
||||
<el-table :data="socialUsers" :show-header="false">
|
||||
<el-table-column type="seq" title="序号" width="60" fixed="left" />
|
||||
<el-table-column label="社交平台" align="left" width="120">
|
||||
<el-table-column fixed="left" title="序号" type="seq" width="60" />
|
||||
<el-table-column align="left" label="社交平台" width="120">
|
||||
<template #default="{ row }">
|
||||
<img class="h-5 align-middle" :src="row.img" alt="" />
|
||||
<img :src="row.img" alt="" class="h-5 align-middle" />
|
||||
<p class="mr-5">{{ row.title }}</p>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" align="center">
|
||||
<el-table-column align="center" label="操作">
|
||||
<template #default="{ row }">
|
||||
<template v-if="row.openid">
|
||||
已绑定
|
||||
<XTextButton type="primary" class="mr-5" @click="unbind(row)" title="(解绑)" />
|
||||
<XTextButton class="mr-5" title="(解绑)" type="primary" @click="unbind(row)" />
|
||||
</template>
|
||||
<template v-else>
|
||||
未绑定
|
||||
<XTextButton type="primary" class="mr-5" @click="bind(row)" title="(绑定)" />
|
||||
<XTextButton class="mr-5" title="(绑定)" type="primary" @click="bind(row)" />
|
||||
</template>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
<script lang="ts" name="UserSocial" setup>
|
||||
import { SystemUserSocialTypeEnum } from '@/utils/constants'
|
||||
import { getUserProfile, ProfileVO } from '@/api/system/user/profile'
|
||||
import { socialAuthRedirect, socialUnbind } from '@/api/system/user/socialUser'
|
||||
import { socialAuthRedirect, socialBind, socialUnbind } from '@/api/system/user/socialUser'
|
||||
|
||||
const message = useMessage()
|
||||
const socialUsers = ref<any[]>([])
|
||||
@@ -46,11 +46,25 @@ const initSocial = async () => {
|
||||
}
|
||||
}
|
||||
}
|
||||
const route = useRoute()
|
||||
const bindSocial = () => {
|
||||
// 社交绑定
|
||||
const type = route.query.type
|
||||
const code = route.query.code
|
||||
const state = route.query.state
|
||||
if (!code) {
|
||||
return
|
||||
}
|
||||
socialBind(type, code, state).then(() => {
|
||||
message.success('绑定成功')
|
||||
initSocial()
|
||||
})
|
||||
}
|
||||
const bind = (row) => {
|
||||
const redirectUri = location.origin + '/user/profile?type=' + row.type
|
||||
// 进行跳转
|
||||
socialAuthRedirect(row.type, encodeURIComponent(redirectUri)).then((res) => {
|
||||
window.location.href = res.data
|
||||
window.location.href = res
|
||||
})
|
||||
}
|
||||
const unbind = async (row) => {
|
||||
@@ -64,4 +78,15 @@ const unbind = async (row) => {
|
||||
onMounted(async () => {
|
||||
await initSocial()
|
||||
})
|
||||
|
||||
watch(
|
||||
() => route,
|
||||
(newRoute) => {
|
||||
bindSocial()
|
||||
console.log(newRoute)
|
||||
},
|
||||
{
|
||||
immediate: true
|
||||
}
|
||||
)
|
||||
</script>
|
||||
|
||||
@@ -18,7 +18,8 @@
|
||||
<template #default="scope">
|
||||
<el-button
|
||||
v-if="scope.row.formType === 10"
|
||||
type="text"
|
||||
type="primary"
|
||||
link
|
||||
@click="handleFormDetail(scope.row)"
|
||||
>
|
||||
<span>{{ scope.row.formName }}</span>
|
||||
@@ -83,7 +84,7 @@
|
||||
|
||||
<!-- 弹窗:流程模型图的预览 -->
|
||||
<Dialog title="流程图" v-model="bpmnDetailVisible" width="800">
|
||||
<my-process-viewer
|
||||
<MyProcessViewer
|
||||
key="designer"
|
||||
v-model="bpmnXML"
|
||||
:value="bpmnXML"
|
||||
@@ -96,6 +97,7 @@
|
||||
<script setup lang="ts" name="BpmProcessDefinition">
|
||||
import { DICT_TYPE } from '@/utils/dict'
|
||||
import { dateFormatter } from '@/utils/formatTime'
|
||||
import { MyProcessViewer } from '@/components/bpmnProcessDesigner/package'
|
||||
import * as DefinitionApi from '@/api/bpm/definition'
|
||||
import { setConfAndFields2 } from '@/utils/formCreate'
|
||||
const { push } = useRouter() // 路由
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
<template>
|
||||
<ContentWrap>
|
||||
<!-- 表单设计器 -->
|
||||
<fc-designer ref="designer" height="780px">
|
||||
<FcDesigner ref="designer" height="780px">
|
||||
<template #handle>
|
||||
<el-button round size="small" type="primary" @click="handleSave">
|
||||
<Icon icon="ep:plus" class="mr-5px" /> 保存
|
||||
<Icon class="mr-5px" icon="ep:plus" />
|
||||
保存
|
||||
</el-button>
|
||||
</template>
|
||||
</fc-designer>
|
||||
</FcDesigner>
|
||||
</ContentWrap>
|
||||
|
||||
<!-- 表单保存的弹窗 -->
|
||||
<Dialog title="保存表单" v-model="dialogVisible" width="600">
|
||||
<Dialog v-model="dialogVisible" title="保存表单" width="600">
|
||||
<el-form ref="formRef" :model="formData" :rules="formRules" label-width="80px">
|
||||
<el-form-item label="表单名" prop="name">
|
||||
<el-input v-model="formData.name" placeholder="请输入表单名" />
|
||||
@@ -28,24 +29,28 @@
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="备注" prop="remark">
|
||||
<el-input v-model="formData.remark" type="textarea" placeholder="请输入备注" />
|
||||
<el-input v-model="formData.remark" placeholder="请输入备注" type="textarea" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button>
|
||||
<el-button :disabled="formLoading" type="primary" @click="submitForm">确 定</el-button>
|
||||
<el-button @click="dialogVisible = false">取 消</el-button>
|
||||
</template>
|
||||
</Dialog>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
<script lang="ts" name="BpmFormEditor" setup>
|
||||
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
|
||||
import { CommonStatusEnum } from '@/utils/constants'
|
||||
import * as FormApi from '@/api/bpm/form'
|
||||
import FcDesigner from '@form-create/designer'
|
||||
import { encodeConf, encodeFields, setConfAndFields } from '@/utils/formCreate'
|
||||
import { useTagsViewStore } from '@/store/modules/tagsView'
|
||||
|
||||
const { t } = useI18n() // 国际化
|
||||
const message = useMessage() // 消息
|
||||
const { query } = useRoute() // 路由
|
||||
const { push, currentRoute } = useRouter() // 路由
|
||||
const { query } = useRoute() // 路由信息
|
||||
const { delView } = useTagsViewStore() // 视图操作
|
||||
|
||||
const designer = ref() // 表单设计器
|
||||
const dialogVisible = ref(false) // 弹窗是否展示
|
||||
@@ -86,10 +91,16 @@ const submitForm = async () => {
|
||||
message.success(t('common.updateSuccess'))
|
||||
}
|
||||
dialogVisible.value = false
|
||||
close()
|
||||
} finally {
|
||||
formLoading.value = false
|
||||
}
|
||||
}
|
||||
/** 关闭按钮 */
|
||||
const close = () => {
|
||||
delView(unref(currentRoute))
|
||||
push('/bpm/manager/form')
|
||||
}
|
||||
|
||||
/** 初始化 **/
|
||||
onMounted(async () => {
|
||||
|
||||
@@ -2,26 +2,33 @@
|
||||
<ContentWrap>
|
||||
<!-- 搜索工作栏 -->
|
||||
<el-form
|
||||
class="-mb-15px"
|
||||
:model="queryParams"
|
||||
ref="queryFormRef"
|
||||
:inline="true"
|
||||
:model="queryParams"
|
||||
class="-mb-15px"
|
||||
label-width="68px"
|
||||
>
|
||||
<el-form-item label="表单名" prop="name">
|
||||
<el-input
|
||||
v-model="queryParams.name"
|
||||
placeholder="请输入表单名"
|
||||
clearable
|
||||
@keyup.enter="handleQuery"
|
||||
class="!w-240px"
|
||||
clearable
|
||||
placeholder="请输入表单名"
|
||||
@keyup.enter="handleQuery"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
|
||||
<el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
|
||||
<el-button type="primary" plain @click="openForm" v-hasPermi="['bpm:form:create']">
|
||||
<Icon icon="ep:plus" class="mr-5px" /> 新增
|
||||
<el-button @click="handleQuery">
|
||||
<Icon class="mr-5px" icon="ep:search" />
|
||||
搜索
|
||||
</el-button>
|
||||
<el-button @click="resetQuery">
|
||||
<Icon class="mr-5px" icon="ep:refresh" />
|
||||
重置
|
||||
</el-button>
|
||||
<el-button v-hasPermi="['bpm:form:create']" plain type="primary" @click="openForm">
|
||||
<Icon class="mr-5px" icon="ep:plus" />
|
||||
新增
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
@@ -30,38 +37,38 @@
|
||||
<!-- 列表 -->
|
||||
<ContentWrap>
|
||||
<el-table v-loading="loading" :data="list">
|
||||
<el-table-column label="编号" align="center" prop="id" />
|
||||
<el-table-column label="表单名" align="center" prop="name" />
|
||||
<el-table-column label="状态" align="center" prop="status">
|
||||
<el-table-column align="center" label="编号" prop="id" />
|
||||
<el-table-column align="center" label="表单名" prop="name" />
|
||||
<el-table-column align="center" label="状态" prop="status">
|
||||
<template #default="scope">
|
||||
<dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="备注" align="center" prop="remark" />
|
||||
<el-table-column align="center" label="备注" prop="remark" />
|
||||
<el-table-column
|
||||
label="创建时间"
|
||||
align="center"
|
||||
prop="createTime"
|
||||
:formatter="dateFormatter"
|
||||
align="center"
|
||||
label="创建时间"
|
||||
prop="createTime"
|
||||
/>
|
||||
<el-table-column label="操作" align="center">
|
||||
<el-table-column align="center" label="操作">
|
||||
<template #default="scope">
|
||||
<el-button
|
||||
v-hasPermi="['bpm:form:update']"
|
||||
link
|
||||
type="primary"
|
||||
@click="openForm(scope.row.id)"
|
||||
v-hasPermi="['bpm:form:update']"
|
||||
>
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button link @click="openDetail(scope.row.id)" v-hasPermi="['bpm:form:query']">
|
||||
<el-button v-hasPermi="['bpm:form:query']" link @click="openDetail(scope.row.id)">
|
||||
详情
|
||||
</el-button>
|
||||
<el-button
|
||||
v-hasPermi="['bpm:form:delete']"
|
||||
link
|
||||
type="danger"
|
||||
@click="handleDelete(scope.row.id)"
|
||||
v-hasPermi="['bpm:form:delete']"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
@@ -70,27 +77,28 @@
|
||||
</el-table>
|
||||
<!-- 分页 -->
|
||||
<Pagination
|
||||
:total="total"
|
||||
v-model:page="queryParams.pageNo"
|
||||
v-model:limit="queryParams.pageSize"
|
||||
v-model:page="queryParams.pageNo"
|
||||
:total="total"
|
||||
@pagination="getList"
|
||||
/>
|
||||
</ContentWrap>
|
||||
|
||||
<!-- 表单详情的弹窗 -->
|
||||
<Dialog title="表单详情" v-model="detailVisible" width="800">
|
||||
<form-create :rule="detailData.rule" :option="detailData.option" />
|
||||
<Dialog v-model="detailVisible" title="表单详情" width="800">
|
||||
<form-create :option="detailData.option" :rule="detailData.rule" />
|
||||
</Dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" name="BpmForm">
|
||||
<script lang="ts" name="BpmForm" setup>
|
||||
import { DICT_TYPE } from '@/utils/dict'
|
||||
import { dateFormatter } from '@/utils/formatTime'
|
||||
import * as FormApi from '@/api/bpm/form'
|
||||
import { setConfAndFields2 } from '@/utils/formCreate'
|
||||
|
||||
const message = useMessage() // 消息弹窗
|
||||
const { t } = useI18n() // 国际化
|
||||
const { push } = useRouter() // 路由
|
||||
const { currentRoute, push } = useRouter() // 路由
|
||||
|
||||
const loading = ref(true) // 列表的加载中
|
||||
const total = ref(0) // 列表的总页数
|
||||
@@ -128,12 +136,16 @@ const resetQuery = () => {
|
||||
|
||||
/** 添加/修改操作 */
|
||||
const openForm = (id?: number) => {
|
||||
push({
|
||||
name: 'BpmFormEditor',
|
||||
query: {
|
||||
const toRouter: { name: string; query?: { id: number } } = {
|
||||
name: 'BpmFormEditor'
|
||||
}
|
||||
// 表单新建的时候id传的是event需要排除
|
||||
if (typeof id === 'number') {
|
||||
toRouter.query = {
|
||||
id
|
||||
}
|
||||
})
|
||||
}
|
||||
push(toRouter)
|
||||
}
|
||||
|
||||
/** 删除按钮操作 */
|
||||
@@ -162,7 +174,16 @@ const openDetail = async (rowId: number) => {
|
||||
// 弹窗打开
|
||||
detailVisible.value = true
|
||||
}
|
||||
|
||||
/**表单保存返回后重新加载列表 */
|
||||
watch(
|
||||
() => currentRoute.value,
|
||||
() => {
|
||||
getList()
|
||||
},
|
||||
{
|
||||
immediate: true
|
||||
}
|
||||
)
|
||||
/** 初始化 **/
|
||||
onMounted(() => {
|
||||
getList()
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
<template>
|
||||
<Dialog :title="dialogTitle" v-model="dialogVisible">
|
||||
<Dialog v-model="dialogVisible" :title="dialogTitle">
|
||||
<el-form
|
||||
ref="formRef"
|
||||
v-loading="formLoading"
|
||||
:model="formData"
|
||||
:rules="formRules"
|
||||
label-width="100px"
|
||||
v-loading="formLoading"
|
||||
>
|
||||
<el-form-item label="组名" prop="name">
|
||||
<el-input v-model="formData.name" placeholder="请输入组名" />
|
||||
</el-form-item>
|
||||
<el-form-item label="描述">
|
||||
<el-input type="textarea" v-model="formData.name" placeholder="请输入描述" />
|
||||
<el-input v-model="formData.description" placeholder="请输入描述" type="textarea" />
|
||||
</el-form-item>
|
||||
<el-form-item label="成员" prop="memberUserIds">
|
||||
<el-select v-model="formData.memberUserIds" multiple placeholder="请选择成员">
|
||||
@@ -36,12 +36,12 @@
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button>
|
||||
<el-button :disabled="formLoading" type="primary" @click="submitForm">确 定</el-button>
|
||||
<el-button @click="dialogVisible = false">取 消</el-button>
|
||||
</template>
|
||||
</Dialog>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
<script lang="ts" name="UserGroupForm" setup>
|
||||
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
|
||||
import { CommonStatusEnum } from '@/utils/constants'
|
||||
import * as UserGroupApi from '@/api/bpm/userGroup'
|
||||
|
||||
@@ -1,45 +1,45 @@
|
||||
<template>
|
||||
<Dialog :title="dialogTitle" v-model="dialogVisible" width="600">
|
||||
<Dialog v-model="dialogVisible" :title="dialogTitle" width="600">
|
||||
<el-form
|
||||
ref="formRef"
|
||||
v-loading="formLoading"
|
||||
:model="formData"
|
||||
:rules="formRules"
|
||||
label-width="110px"
|
||||
v-loading="formLoading"
|
||||
>
|
||||
<el-form-item label="流程标识" prop="key">
|
||||
<el-input
|
||||
v-model="formData.key"
|
||||
:disabled="!!formData.id"
|
||||
placeholder="请输入流标标识"
|
||||
style="width: 330px"
|
||||
:disabled="!!formData.id"
|
||||
/>
|
||||
<el-tooltip
|
||||
v-if="!formData.id"
|
||||
class="item"
|
||||
effect="light"
|
||||
content="新建后,流程标识不可修改!"
|
||||
effect="light"
|
||||
placement="top"
|
||||
>
|
||||
<i style="padding-left: 5px" class="el-icon-question"></i>
|
||||
<i class="el-icon-question" style="padding-left: 5px"></i>
|
||||
</el-tooltip>
|
||||
<el-tooltip v-else class="item" effect="light" content="流程标识不可修改!" placement="top">
|
||||
<i style="padding-left: 5px" class="el-icon-question"></i>
|
||||
<el-tooltip v-else class="item" content="流程标识不可修改!" effect="light" placement="top">
|
||||
<i class="el-icon-question" style="padding-left: 5px"></i>
|
||||
</el-tooltip>
|
||||
</el-form-item>
|
||||
<el-form-item label="流程名称" prop="name">
|
||||
<el-input
|
||||
v-model="formData.name"
|
||||
placeholder="请输入流程名称"
|
||||
:disabled="!!formData.id"
|
||||
clearable
|
||||
placeholder="请输入流程名称"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="formData.id" label="流程分类" prop="category">
|
||||
<el-select
|
||||
v-model="formData.category"
|
||||
placeholder="请选择流程分类"
|
||||
clearable
|
||||
placeholder="请选择流程分类"
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-option
|
||||
@@ -51,7 +51,7 @@
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="流程描述" prop="description">
|
||||
<el-input type="textarea" v-model="formData.description" clearable />
|
||||
<el-input v-model="formData.description" clearable type="textarea" />
|
||||
</el-form-item>
|
||||
<div v-if="formData.id">
|
||||
<el-form-item label="表单类型" prop="formType">
|
||||
@@ -87,11 +87,11 @@
|
||||
/>
|
||||
<el-tooltip
|
||||
class="item"
|
||||
effect="light"
|
||||
content="自定义表单的提交路径,使用 Vue 的路由地址,例如说:bpm/oa/leave/create"
|
||||
effect="light"
|
||||
placement="top"
|
||||
>
|
||||
<i style="padding-left: 5px" class="el-icon-question"></i>
|
||||
<i class="el-icon-question" style="padding-left: 5px"></i>
|
||||
</el-tooltip>
|
||||
</el-form-item>
|
||||
<el-form-item
|
||||
@@ -106,26 +106,27 @@
|
||||
/>
|
||||
<el-tooltip
|
||||
class="item"
|
||||
effect="light"
|
||||
content="自定义表单的查看路径,使用 Vue 的路由地址,例如说:bpm/oa/leave/view"
|
||||
effect="light"
|
||||
placement="top"
|
||||
>
|
||||
<i style="padding-left: 5px" class="el-icon-question"></i>
|
||||
<i class="el-icon-question" style="padding-left: 5px"></i>
|
||||
</el-tooltip>
|
||||
</el-form-item>
|
||||
</div>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button>
|
||||
<el-button :disabled="formLoading" type="primary" @click="submitForm">确 定</el-button>
|
||||
<el-button @click="dialogVisible = false">取 消</el-button>
|
||||
</template>
|
||||
</Dialog>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
<script lang="ts" name="ModelForm" setup>
|
||||
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
|
||||
import { ElMessageBox } from 'element-plus'
|
||||
import * as ModelApi from '@/api/bpm/model'
|
||||
import * as FormApi from '@/api/bpm/form'
|
||||
|
||||
const { t } = useI18n() // 国际化
|
||||
const message = useMessage() // 消息弹窗
|
||||
|
||||
|
||||
@@ -1,24 +1,24 @@
|
||||
<template>
|
||||
<Dialog title="导入流程" v-model="dialogVisible" width="400">
|
||||
<Dialog v-model="dialogVisible" title="导入流程" width="400">
|
||||
<div>
|
||||
<el-upload
|
||||
ref="uploadRef"
|
||||
:action="importUrl"
|
||||
:headers="uploadHeaders"
|
||||
:data="formData"
|
||||
name="bpmnFile"
|
||||
v-model:file-list="fileList"
|
||||
drag
|
||||
:action="importUrl"
|
||||
:auto-upload="false"
|
||||
accept=".bpmn, .xml"
|
||||
:data="formData"
|
||||
:disabled="formLoading"
|
||||
:headers="uploadHeaders"
|
||||
:limit="1"
|
||||
:on-error="submitFormError"
|
||||
:on-exceed="handleExceed"
|
||||
:on-success="submitFormSuccess"
|
||||
:on-error="submitFormError"
|
||||
:disabled="formLoading"
|
||||
accept=".bpmn, .xml"
|
||||
drag
|
||||
name="bpmnFile"
|
||||
>
|
||||
<Icon class="el-icon--upload" icon="ep:upload-filled" />
|
||||
<div class="el-upload__text"> 将文件拖到此处,或 <em>点击上传</em> </div>
|
||||
<div class="el-upload__text"> 将文件拖到此处,或 <em>点击上传</em></div>
|
||||
<template #tip>
|
||||
<div class="el-upload__tip" style="color: red">
|
||||
提示:仅允许导入“bpm”或“xml”格式文件!
|
||||
@@ -33,10 +33,10 @@
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="流程名称" prop="name">
|
||||
<el-input v-model="formData.name" placeholder="请输入流程名称" clearable />
|
||||
<el-input v-model="formData.name" clearable placeholder="请输入流程名称" />
|
||||
</el-form-item>
|
||||
<el-form-item label="流程描述" prop="description">
|
||||
<el-input type="textarea" v-model="formData.description" clearable />
|
||||
<el-input v-model="formData.description" clearable type="textarea" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
@@ -44,13 +44,14 @@
|
||||
</el-upload>
|
||||
</div>
|
||||
<template #footer>
|
||||
<el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button>
|
||||
<el-button :disabled="formLoading" type="primary" @click="submitForm">确 定</el-button>
|
||||
<el-button @click="dialogVisible = false">取 消</el-button>
|
||||
</template>
|
||||
</Dialog>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
<script lang="ts" name="ModelImportForm" setup>
|
||||
import { getAccessToken, getTenantId } from '@/utils/auth'
|
||||
|
||||
const message = useMessage() // 消息弹窗
|
||||
|
||||
const dialogVisible = ref(false) // 弹窗的是否展示
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<ContentWrap>
|
||||
<!-- 流程设计器,负责绘制流程等 -->
|
||||
<my-process-designer
|
||||
<MyProcessDesigner
|
||||
key="designer"
|
||||
v-if="xmlString !== undefined"
|
||||
v-model="xmlString"
|
||||
@@ -14,7 +14,7 @@
|
||||
@save="save"
|
||||
/>
|
||||
<!-- 流程属性器,负责编辑每个流程节点的属性 -->
|
||||
<my-properties-panel
|
||||
<MyProcessPenal
|
||||
key="penal"
|
||||
:bpmnModeler="modeler"
|
||||
:prefix="controlForm.prefix"
|
||||
@@ -25,6 +25,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" name="BpmModelEditor">
|
||||
import { MyProcessDesigner, MyProcessPenal } from '@/components/bpmnProcessDesigner/package'
|
||||
// 自定义元素选中时的弹出菜单(修改 默认任务 为 用户任务)
|
||||
import CustomContentPadProvider from '@/components/bpmnProcessDesigner/package/designer/plugins/content-pad'
|
||||
// 自定义左侧菜单(修改 默认任务 为 用户任务)
|
||||
|
||||
@@ -79,14 +79,16 @@
|
||||
<template #default="scope">
|
||||
<el-button
|
||||
v-if="scope.row.formType === 10"
|
||||
type="text"
|
||||
type="primary"
|
||||
link
|
||||
@click="handleFormDetail(scope.row)"
|
||||
>
|
||||
<span>{{ scope.row.formName }}</span>
|
||||
</el-button>
|
||||
<el-button
|
||||
v-else-if="scope.row.formType === 20"
|
||||
type="text"
|
||||
type="primary"
|
||||
link
|
||||
@click="handleFormDetail(scope.row)"
|
||||
>
|
||||
<span>{{ scope.row.formCustomCreatePath }}</span>
|
||||
@@ -214,7 +216,7 @@
|
||||
|
||||
<!-- 弹窗:流程模型图的预览 -->
|
||||
<Dialog title="流程图" v-model="bpmnDetailVisible" width="800">
|
||||
<my-process-viewer
|
||||
<MyProcessViewer
|
||||
key="designer"
|
||||
v-model="bpmnXML"
|
||||
:value="bpmnXML"
|
||||
@@ -227,6 +229,7 @@
|
||||
<script setup lang="ts" name="BpmModel">
|
||||
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
|
||||
import { dateFormatter, formatDate } from '@/utils/formatTime'
|
||||
import { MyProcessViewer } from '@/components/bpmnProcessDesigner/package'
|
||||
import * as ModelApi from '@/api/bpm/model'
|
||||
import * as FormApi from '@/api/bpm/form'
|
||||
import ModelForm from './ModelForm.vue'
|
||||
|
||||
@@ -1,56 +1,55 @@
|
||||
<template>
|
||||
<Dialog title="发起 OA 请假流程" v-model="dialogVisible">
|
||||
<el-form
|
||||
ref="formRef"
|
||||
:model="formData"
|
||||
:rules="formRules"
|
||||
label-width="80px"
|
||||
v-loading="formLoading"
|
||||
>
|
||||
<el-form-item label="请假类型" prop="type">
|
||||
<el-select v-model="formData.type" placeholder="请选择请假类型" clearable>
|
||||
<el-option
|
||||
v-for="dict in getIntDictOptions(DICT_TYPE.BPM_OA_LEAVE_TYPE)"
|
||||
:key="dict.value"
|
||||
:label="dict.label"
|
||||
:value="dict.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="开始时间" prop="startTime">
|
||||
<el-date-picker
|
||||
clearable
|
||||
v-model="formData.startTime"
|
||||
type="datetime"
|
||||
value-format="x"
|
||||
placeholder="请选择开始时间"
|
||||
<el-form
|
||||
ref="formRef"
|
||||
:model="formData"
|
||||
:rules="formRules"
|
||||
label-width="80px"
|
||||
v-loading="formLoading"
|
||||
>
|
||||
<el-form-item label="请假类型" prop="type">
|
||||
<el-select v-model="formData.type" placeholder="请选择请假类型" clearable>
|
||||
<el-option
|
||||
v-for="dict in getIntDictOptions(DICT_TYPE.BPM_OA_LEAVE_TYPE)"
|
||||
:key="dict.value"
|
||||
:label="dict.label"
|
||||
:value="dict.value"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="结束时间" prop="endTime">
|
||||
<el-date-picker
|
||||
clearable
|
||||
v-model="formData.endTime"
|
||||
type="datetime"
|
||||
value-format="x"
|
||||
placeholder="请选择结束时间"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="原因" prop="reason">
|
||||
<el-input v-model="formData.reason" type="textarea" placeholder="请输请假原因" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="开始时间" prop="startTime">
|
||||
<el-date-picker
|
||||
clearable
|
||||
v-model="formData.startTime"
|
||||
type="datetime"
|
||||
value-format="x"
|
||||
placeholder="请选择开始时间"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="结束时间" prop="endTime">
|
||||
<el-date-picker
|
||||
clearable
|
||||
v-model="formData.endTime"
|
||||
type="datetime"
|
||||
value-format="x"
|
||||
placeholder="请选择结束时间"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="原因" prop="reason">
|
||||
<el-input v-model="formData.reason" type="textarea" placeholder="请输请假原因" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button>
|
||||
<el-button @click="dialogVisible = false">取 消</el-button>
|
||||
</template>
|
||||
</Dialog>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
<script setup name="BpmOALeaveCreate" lang="ts">
|
||||
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
|
||||
import * as LeaveApi from '@/api/bpm/leave'
|
||||
import { useTagsViewStore } from '@/store/modules/tagsView'
|
||||
const message = useMessage() // 消息弹窗
|
||||
const { delView } = useTagsViewStore() // 视图操作
|
||||
const { currentRoute } = useRouter() // 路由
|
||||
|
||||
const dialogVisible = ref(false) // 弹窗的是否展示
|
||||
const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
|
||||
const formData = ref({
|
||||
type: undefined,
|
||||
@@ -66,15 +65,7 @@ const formRules = reactive({
|
||||
})
|
||||
const formRef = ref() // 表单 Ref
|
||||
|
||||
/** 打开弹窗 */
|
||||
const open = async () => {
|
||||
dialogVisible.value = true
|
||||
resetForm()
|
||||
}
|
||||
defineExpose({ open }) // 提供 open 方法,用于打开弹窗
|
||||
|
||||
/** 提交表单 */
|
||||
const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
|
||||
const submitForm = async () => {
|
||||
// 校验表单
|
||||
if (!formRef) return
|
||||
@@ -85,23 +76,11 @@ const submitForm = async () => {
|
||||
try {
|
||||
const data = formData.value as unknown as LeaveApi.LeaveVO
|
||||
await LeaveApi.createLeave(data)
|
||||
message.success('新增成功')
|
||||
dialogVisible.value = false
|
||||
// 发送操作成功的事件
|
||||
emit('success')
|
||||
message.success('发起成功')
|
||||
// 关闭当前 Tab
|
||||
delView(unref(currentRoute))
|
||||
} finally {
|
||||
formLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 重置表单 */
|
||||
const resetForm = () => {
|
||||
formData.value = {
|
||||
type: undefined,
|
||||
reason: undefined,
|
||||
startTime: undefined,
|
||||
endTime: undefined
|
||||
}
|
||||
formRef.value?.resetFields()
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,40 +1,49 @@
|
||||
<template>
|
||||
<Dialog title="详情" v-model="modelVisible" :scroll="true" :max-height="200">
|
||||
<el-descriptions border :column="1">
|
||||
<ContentWrap>
|
||||
<el-descriptions :column="1" border>
|
||||
<el-descriptions-item label="请假类型">
|
||||
<dict-tag :type="DICT_TYPE.BPM_OA_LEAVE_TYPE" :value="detailData.type" />
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="开始时间">
|
||||
{{ formatDate(detailData.startTime) }}
|
||||
{{ formatDate(detailData.startTime, 'YYYY-MM-DD') }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="结束时间">
|
||||
{{ formatDate(detailData.endTime) }}
|
||||
{{ formatDate(detailData.endTime, 'YYYY-MM-DD') }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="原因">
|
||||
{{ detailData.reason }}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</Dialog>
|
||||
</ContentWrap>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
<script lang="ts" name="BpmOALeaveDetail" setup>
|
||||
import { DICT_TYPE } from '@/utils/dict'
|
||||
import { formatDate } from '@/utils/formatTime'
|
||||
import { propTypes } from '@/utils/propTypes'
|
||||
import * as LeaveApi from '@/api/bpm/leave'
|
||||
|
||||
const modelVisible = ref(false) // 弹窗的是否展示
|
||||
const detailLoading = ref(false) // 表单的加载中
|
||||
const detailData = ref() // 详情数据
|
||||
const { query } = useRoute() // 查询参数
|
||||
|
||||
/** 打开弹窗 */
|
||||
const open = async (data: LeaveApi.LeaveVO) => {
|
||||
modelVisible.value = true
|
||||
// 设置数据
|
||||
const props = defineProps({
|
||||
id: propTypes.number.def(undefined)
|
||||
})
|
||||
const detailLoading = ref(false) // 表单的加载中
|
||||
const detailData = ref({}) // 详情数据
|
||||
const queryId = query.id as unknown as number // 从 URL 传递过来的 id 编号
|
||||
|
||||
/** 获得数据 */
|
||||
const getInfo = async () => {
|
||||
detailLoading.value = true
|
||||
try {
|
||||
detailData.value = data
|
||||
detailData.value = await LeaveApi.getLeave(props.id || queryId)
|
||||
} finally {
|
||||
detailLoading.value = false
|
||||
}
|
||||
}
|
||||
defineExpose({ open }) // 提供 open 方法,用于打开弹窗
|
||||
defineExpose({ open: getInfo }) // 提供 open 方法,用于打开弹窗
|
||||
|
||||
/** 初始化 **/
|
||||
onMounted(() => {
|
||||
getInfo()
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -137,19 +137,12 @@
|
||||
@pagination="getList"
|
||||
/>
|
||||
</ContentWrap>
|
||||
|
||||
<!-- 表单弹窗:详情 -->
|
||||
<LeaveDetail ref="detailRef" />
|
||||
<!-- 表单弹窗:添加 -->
|
||||
<LeaveForm ref="formRef" @success="getList" />
|
||||
</template>
|
||||
<script setup lang="ts" name="BpmOALeave">
|
||||
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
|
||||
import { dateFormatter } from '@/utils/formatTime'
|
||||
import * as LeaveApi from '@/api/bpm/leave'
|
||||
import * as ProcessInstanceApi from '@/api/bpm/processInstance'
|
||||
import LeaveDetail from './detail.vue'
|
||||
import LeaveForm from './create.vue'
|
||||
const message = useMessage() // 消息弹窗
|
||||
const router = useRouter() // 路由
|
||||
const { t } = useI18n() // 国际化
|
||||
@@ -192,15 +185,18 @@ const resetQuery = () => {
|
||||
}
|
||||
|
||||
/** 添加操作 */
|
||||
const formRef = ref()
|
||||
const handleCreate = () => {
|
||||
formRef.value.open()
|
||||
router.push({ name: 'OALeaveCreate' })
|
||||
}
|
||||
|
||||
/** 详情操作 */
|
||||
const detailRef = ref()
|
||||
const handleDetail = (data: LeaveApi.LeaveVO) => {
|
||||
detailRef.value.open(data)
|
||||
const handleDetail = (row: LeaveApi.LeaveVO) => {
|
||||
router.push({
|
||||
name: 'OALeaveDetail',
|
||||
query: {
|
||||
id: row.id
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/** 取消请假操作 */
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
<template>
|
||||
<el-card class="box-card" v-loading="loading">
|
||||
<el-card v-loading="loading" class="box-card">
|
||||
<template #header>
|
||||
<span class="el-icon-picture-outline">流程图</span>
|
||||
</template>
|
||||
<my-process-viewer
|
||||
<MyProcessViewer
|
||||
key="designer"
|
||||
:value="bpmnXml"
|
||||
v-bind="bpmnControlForm"
|
||||
:prefix="bpmnControlForm.prefix"
|
||||
:activityData="activityList"
|
||||
:prefix="bpmnControlForm.prefix"
|
||||
:processInstanceData="processInstance"
|
||||
:taskData="tasks"
|
||||
:value="bpmnXml"
|
||||
v-bind="bpmnControlForm"
|
||||
/>
|
||||
</el-card>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
<script lang="ts" name="BpmProcessInstanceBpmnViewer" setup>
|
||||
import { propTypes } from '@/utils/propTypes'
|
||||
import { MyProcessViewer } from '@/components/bpmnProcessDesigner/package'
|
||||
import * as ActivityApi from '@/api/bpm/activity'
|
||||
// import * as DefinitionApi from '@/api/bpm/definition'
|
||||
|
||||
const props = defineProps({
|
||||
loading: propTypes.bool, // 是否加载中
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<template>
|
||||
<el-card class="box-card" v-loading="loading">
|
||||
<el-card v-loading="loading" class="box-card">
|
||||
<template #header>
|
||||
<span class="el-icon-picture-outline">审批记录</span>
|
||||
</template>
|
||||
<el-col :span="16" :offset="4">
|
||||
<el-col :offset="4" :span="16">
|
||||
<div class="block">
|
||||
<el-timeline>
|
||||
<el-timeline-item
|
||||
@@ -16,9 +16,9 @@
|
||||
<el-card :body-style="{ padding: '10px' }">
|
||||
<label v-if="item.assigneeUser" style="font-weight: normal; margin-right: 30px">
|
||||
审批人:{{ item.assigneeUser.nickname }}
|
||||
<el-tag type="info" size="small">{{ item.assigneeUser.deptName }}</el-tag>
|
||||
<el-tag size="small" type="info">{{ item.assigneeUser.deptName }}</el-tag>
|
||||
</label>
|
||||
<label style="font-weight: normal" v-if="item.createTime">创建时间:</label>
|
||||
<label v-if="item.createTime" style="font-weight: normal">创建时间:</label>
|
||||
<label style="color: #8a909c; font-weight: normal">
|
||||
{{ formatDate(item?.createTime) }}
|
||||
</label>
|
||||
@@ -44,7 +44,7 @@
|
||||
</el-col>
|
||||
</el-card>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
<script lang="ts" name="BpmProcessInstanceTaskList" setup>
|
||||
import { formatDate, formatPast2 } from '@/utils/formatTime'
|
||||
import { propTypes } from '@/utils/propTypes'
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<template>
|
||||
<Dialog title="转派审批人" v-model="dialogVisible" width="500">
|
||||
<Dialog v-model="dialogVisible" title="转派审批人" width="500">
|
||||
<el-form
|
||||
ref="formRef"
|
||||
v-loading="formLoading"
|
||||
:model="formData"
|
||||
:rules="formRules"
|
||||
label-width="110px"
|
||||
v-loading="formLoading"
|
||||
>
|
||||
<el-form-item label="新审批人" prop="assigneeUserId">
|
||||
<el-select v-model="formData.assigneeUserId" clearable style="width: 100%">
|
||||
@@ -19,12 +19,12 @@
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button>
|
||||
<el-button :disabled="formLoading" type="primary" @click="submitForm">确 定</el-button>
|
||||
<el-button @click="dialogVisible = false">取 消</el-button>
|
||||
</template>
|
||||
</Dialog>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
<script lang="ts" name="BpmTaskUpdateAssigneeForm" setup>
|
||||
import * as TaskApi from '@/api/bpm/task'
|
||||
import * as UserApi from '@/api/system/user'
|
||||
|
||||
|
||||
@@ -2,81 +2,78 @@
|
||||
<ContentWrap>
|
||||
<!-- 审批信息 -->
|
||||
<el-card
|
||||
class="box-card"
|
||||
v-loading="processInstanceLoading"
|
||||
v-for="(item, index) in runningTasks"
|
||||
:key="index"
|
||||
v-loading="processInstanceLoading"
|
||||
class="box-card"
|
||||
>
|
||||
<template #header>
|
||||
<span class="el-icon-picture-outline">审批任务【{{ item.name }}】</span>
|
||||
</template>
|
||||
<el-col :span="16" :offset="6">
|
||||
<el-col :offset="6" :span="16">
|
||||
<el-form
|
||||
:ref="'form' + index"
|
||||
:model="auditForms[index]"
|
||||
:rules="auditRule"
|
||||
label-width="100px"
|
||||
>
|
||||
<el-form-item label="流程名" v-if="processInstance && processInstance.name">
|
||||
<el-form-item v-if="processInstance && processInstance.name" label="流程名">
|
||||
{{ processInstance.name }}
|
||||
</el-form-item>
|
||||
<el-form-item label="流程发起人" v-if="processInstance && processInstance.startUser">
|
||||
<el-form-item v-if="processInstance && processInstance.startUser" label="流程发起人">
|
||||
{{ processInstance.startUser.nickname }}
|
||||
<el-tag type="info" size="small">{{ processInstance.startUser.deptName }}</el-tag>
|
||||
<el-tag size="small" type="info">{{ processInstance.startUser.deptName }}</el-tag>
|
||||
</el-form-item>
|
||||
<el-form-item label="审批建议" prop="reason">
|
||||
<el-input
|
||||
type="textarea"
|
||||
v-model="auditForms[index].reason"
|
||||
placeholder="请输入审批建议"
|
||||
type="textarea"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<div style="margin-left: 10%; margin-bottom: 20px; font-size: 14px">
|
||||
<el-button type="success" @click="handleAudit(item, true)">
|
||||
<Icon icon="ep:select" /> 通过
|
||||
<Icon icon="ep:select" />
|
||||
通过
|
||||
</el-button>
|
||||
<el-button type="danger" @click="handleAudit(item, false)">
|
||||
<Icon icon="ep:close" /> 不通过
|
||||
<Icon icon="ep:close" />
|
||||
不通过
|
||||
</el-button>
|
||||
<el-button type="primary" @click="openTaskUpdateAssigneeForm(item.id)">
|
||||
<Icon icon="ep:edit" /> 转办
|
||||
<Icon icon="ep:edit" />
|
||||
转办
|
||||
</el-button>
|
||||
<el-button type="primary" @click="handleDelegate(item)">
|
||||
<Icon icon="ep:position" /> 委派
|
||||
<Icon icon="ep:position" />
|
||||
委派
|
||||
</el-button>
|
||||
<el-button type="warning" @click="handleBack(item)">
|
||||
<Icon icon="ep:back" /> 回退
|
||||
<Icon icon="ep:back" />
|
||||
回退
|
||||
</el-button>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-card>
|
||||
|
||||
<!-- 申请信息 -->
|
||||
<el-card class="box-card" v-loading="processInstanceLoading">
|
||||
<el-card v-loading="processInstanceLoading" class="box-card">
|
||||
<template #header>
|
||||
<span class="el-icon-document">申请信息【{{ processInstance.name }}】</span>
|
||||
</template>
|
||||
<!-- 情况一:流程表单 -->
|
||||
<el-col v-if="processInstance?.processDefinition?.formType === 10" :span="16" :offset="6">
|
||||
<el-col v-if="processInstance?.processDefinition?.formType === 10" :offset="6" :span="16">
|
||||
<form-create
|
||||
ref="fApi"
|
||||
:rule="detailForm.rule"
|
||||
:option="detailForm.option"
|
||||
v-model="detailForm.value"
|
||||
:option="detailForm.option"
|
||||
:rule="detailForm.rule"
|
||||
/>
|
||||
</el-col>
|
||||
<!-- 情况二:流程表单 -->
|
||||
<!-- 情况二:业务表单 -->
|
||||
<div v-if="processInstance?.processDefinition?.formType === 20">
|
||||
<router-link
|
||||
:to="
|
||||
processInstance.processDefinition.formCustomViewPath +
|
||||
'?id=' +
|
||||
processInstance.businessKey
|
||||
"
|
||||
>
|
||||
<el-button type="primary"><Icon icon="ep:view" /> 点击查看</el-button>
|
||||
</router-link>
|
||||
<BusinessFormComponent :id="processInstance.businessKey" />
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
@@ -85,18 +82,18 @@
|
||||
|
||||
<!-- 高亮流程图 -->
|
||||
<ProcessInstanceBpmnViewer
|
||||
:id="id"
|
||||
:process-instance="processInstance"
|
||||
:loading="processInstanceLoading"
|
||||
:tasks="tasks"
|
||||
:id="`${id}`"
|
||||
:bpmn-xml="bpmnXML"
|
||||
:loading="processInstanceLoading"
|
||||
:process-instance="processInstance"
|
||||
:tasks="tasks"
|
||||
/>
|
||||
|
||||
<!-- 弹窗:转派审批人 -->
|
||||
<TaskUpdateAssigneeForm ref="taskUpdateAssigneeFormRef" @success="getDetail" />
|
||||
</ContentWrap>
|
||||
</template>
|
||||
<script setup lang="ts" name="BpmProcessInstanceDetail">
|
||||
<script lang="ts" name="BpmProcessInstanceDetail" setup>
|
||||
import { useUserStore } from '@/store/modules/user'
|
||||
import { setConfAndFields2 } from '@/utils/formCreate'
|
||||
import type { ApiAttrs } from '@form-create/element-ui/types/config'
|
||||
@@ -106,6 +103,8 @@ import * as TaskApi from '@/api/bpm/task'
|
||||
import TaskUpdateAssigneeForm from './TaskUpdateAssigneeForm.vue'
|
||||
import ProcessInstanceBpmnViewer from './ProcessInstanceBpmnViewer.vue'
|
||||
import ProcessInstanceTaskList from './ProcessInstanceTaskList.vue'
|
||||
import { registerComponent } from '@/utils/routerHelper'
|
||||
|
||||
const { query } = useRoute() // 查询参数
|
||||
const message = useMessage() // 消息弹窗
|
||||
const { proxy } = getCurrentInstance() as any
|
||||
@@ -186,6 +185,7 @@ const getDetail = () => {
|
||||
}
|
||||
|
||||
/** 加载流程实例 */
|
||||
const BusinessFormComponent = ref(null) // 异步组件
|
||||
const getProcessInstance = async () => {
|
||||
try {
|
||||
processInstanceLoading.value = true
|
||||
@@ -210,10 +210,12 @@ const getProcessInstance = async () => {
|
||||
fApi.value?.fapi?.resetBtn.show(false)
|
||||
fApi.value?.fapi?.disabled(true)
|
||||
})
|
||||
} else {
|
||||
BusinessFormComponent.value = registerComponent(data.processDefinition.formCustomViewPath)
|
||||
}
|
||||
|
||||
// 加载流程图
|
||||
bpmnXML.value = await DefinitionApi.getProcessDefinitionBpmnXML(processDefinition.id)
|
||||
bpmnXML.value = await DefinitionApi.getProcessDefinitionBpmnXML(processDefinition.id as number)
|
||||
} finally {
|
||||
processInstanceLoading.value = false
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<Dialog title="详情" v-model="dialogVisible" :scroll="true" :max-height="500">
|
||||
<el-descriptions border :column="1">
|
||||
<Dialog v-model="dialogVisible" :max-height="500" :scroll="true" title="详情">
|
||||
<el-descriptions :column="1" border>
|
||||
<el-descriptions-item label="任务编号" min-width="120">
|
||||
{{ detailData.id }}
|
||||
</el-descriptions-item>
|
||||
@@ -25,7 +25,7 @@
|
||||
</el-descriptions>
|
||||
</Dialog>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
<script lang="ts" name="BpmTaskDetail" setup>
|
||||
import { DICT_TYPE } from '@/utils/dict'
|
||||
import { formatDate } from '@/utils/formatTime'
|
||||
import * as TaskApi from '@/api/bpm/task'
|
||||
|
||||
@@ -2,35 +2,41 @@
|
||||
<ContentWrap>
|
||||
<!-- 搜索工作栏 -->
|
||||
<el-form
|
||||
class="-mb-15px"
|
||||
:model="queryParams"
|
||||
ref="queryFormRef"
|
||||
:inline="true"
|
||||
:model="queryParams"
|
||||
class="-mb-15px"
|
||||
label-width="68px"
|
||||
>
|
||||
<el-form-item label="任务名称" prop="name">
|
||||
<el-input
|
||||
v-model="queryParams.name"
|
||||
placeholder="请输入任务名称"
|
||||
clearable
|
||||
@keyup.enter="handleQuery"
|
||||
class="!w-240px"
|
||||
clearable
|
||||
placeholder="请输入任务名称"
|
||||
@keyup.enter="handleQuery"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="创建时间" prop="createTime">
|
||||
<el-date-picker
|
||||
v-model="queryParams.createTime"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
type="daterange"
|
||||
start-placeholder="开始日期"
|
||||
end-placeholder="结束日期"
|
||||
:default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
|
||||
class="!w-240px"
|
||||
end-placeholder="结束日期"
|
||||
start-placeholder="开始日期"
|
||||
type="daterange"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
|
||||
<el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
|
||||
<el-button @click="handleQuery">
|
||||
<Icon class="mr-5px" icon="ep:search" />
|
||||
搜索
|
||||
</el-button>
|
||||
<el-button @click="resetQuery">
|
||||
<Icon class="mr-5px" icon="ep:refresh" />
|
||||
重置
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</ContentWrap>
|
||||
@@ -38,24 +44,24 @@
|
||||
<!-- 列表 -->
|
||||
<ContentWrap>
|
||||
<el-table v-loading="loading" :data="list">
|
||||
<el-table-column label="任务编号" align="center" prop="id" width="300px" />
|
||||
<el-table-column label="任务名称" align="center" prop="name" />
|
||||
<el-table-column label="所属流程" align="center" prop="processInstance.name" />
|
||||
<el-table-column label="流程发起人" align="center" prop="processInstance.startUserNickname" />
|
||||
<el-table-column label="状态" align="center" prop="result">
|
||||
<el-table-column align="center" label="任务编号" prop="id" width="300px" />
|
||||
<el-table-column align="center" label="任务名称" prop="name" />
|
||||
<el-table-column align="center" label="所属流程" prop="processInstance.name" />
|
||||
<el-table-column align="center" label="流程发起人" prop="processInstance.startUserNickname" />
|
||||
<el-table-column align="center" 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="reason" />
|
||||
<el-table-column align="center" label="原因" prop="reason" />
|
||||
<el-table-column
|
||||
label="创建时间"
|
||||
:formatter="dateFormatter"
|
||||
align="center"
|
||||
label="创建时间"
|
||||
prop="createTime"
|
||||
width="180"
|
||||
:formatter="dateFormatter"
|
||||
/>
|
||||
<el-table-column label="操作" align="center">
|
||||
<el-table-column align="center" label="操作">
|
||||
<template #default="scope">
|
||||
<el-button link type="primary" @click="openDetail(scope.row)">详情</el-button>
|
||||
<el-button link type="primary" @click="handleAudit(scope.row)">流程</el-button>
|
||||
@@ -64,9 +70,9 @@
|
||||
</el-table>
|
||||
<!-- 分页 -->
|
||||
<Pagination
|
||||
:total="total"
|
||||
v-model:page="queryParams.pageNo"
|
||||
v-model:limit="queryParams.pageSize"
|
||||
v-model:page="queryParams.pageNo"
|
||||
:total="total"
|
||||
@pagination="getList"
|
||||
/>
|
||||
</ContentWrap>
|
||||
@@ -74,11 +80,12 @@
|
||||
<!-- 表单弹窗:详情 -->
|
||||
<TaskDetail ref="detailRef" @success="getList" />
|
||||
</template>
|
||||
<script setup lang="tsx" name="BpmTodoTask">
|
||||
<script lang="ts" name="BpmTodoTask" setup>
|
||||
import { DICT_TYPE } from '@/utils/dict'
|
||||
import { dateFormatter } from '@/utils/formatTime'
|
||||
import * as TaskApi from '@/api/bpm/task'
|
||||
import TaskDetail from './TaskDetail.vue'
|
||||
|
||||
const { push } = useRouter() // 路由
|
||||
|
||||
const loading = ref(true) // 列表的加载中
|
||||
|
||||
@@ -2,35 +2,41 @@
|
||||
<ContentWrap>
|
||||
<!-- 搜索工作栏 -->
|
||||
<el-form
|
||||
class="-mb-15px"
|
||||
:model="queryParams"
|
||||
ref="queryFormRef"
|
||||
:inline="true"
|
||||
:model="queryParams"
|
||||
class="-mb-15px"
|
||||
label-width="68px"
|
||||
>
|
||||
<el-form-item label="任务名称" prop="name">
|
||||
<el-input
|
||||
v-model="queryParams.name"
|
||||
placeholder="请输入任务名称"
|
||||
clearable
|
||||
@keyup.enter="handleQuery"
|
||||
class="!w-240px"
|
||||
clearable
|
||||
placeholder="请输入任务名称"
|
||||
@keyup.enter="handleQuery"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="创建时间" prop="createTime">
|
||||
<el-date-picker
|
||||
v-model="queryParams.createTime"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
type="daterange"
|
||||
start-placeholder="开始日期"
|
||||
end-placeholder="结束日期"
|
||||
:default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
|
||||
class="!w-240px"
|
||||
end-placeholder="结束日期"
|
||||
start-placeholder="开始日期"
|
||||
type="daterange"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
|
||||
<el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
|
||||
<el-button @click="handleQuery">
|
||||
<Icon class="mr-5px" icon="ep:search" />
|
||||
搜索
|
||||
</el-button>
|
||||
<el-button @click="resetQuery">
|
||||
<Icon class="mr-5px" icon="ep:refresh" />
|
||||
重置
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</ContentWrap>
|
||||
@@ -38,24 +44,24 @@
|
||||
<!-- 列表 -->
|
||||
<ContentWrap>
|
||||
<el-table v-loading="loading" :data="list">
|
||||
<el-table-column label="任务编号" align="center" prop="id" width="300px" />
|
||||
<el-table-column label="任务名称" align="center" prop="name" />
|
||||
<el-table-column label="所属流程" align="center" prop="processInstance.name" />
|
||||
<el-table-column label="流程发起人" align="center" prop="processInstance.startUserNickname" />
|
||||
<el-table-column align="center" label="任务编号" prop="id" width="300px" />
|
||||
<el-table-column align="center" label="任务名称" prop="name" />
|
||||
<el-table-column align="center" label="所属流程" prop="processInstance.name" />
|
||||
<el-table-column align="center" label="流程发起人" prop="processInstance.startUserNickname" />
|
||||
<el-table-column
|
||||
label="创建时间"
|
||||
:formatter="dateFormatter"
|
||||
align="center"
|
||||
label="创建时间"
|
||||
prop="createTime"
|
||||
width="180"
|
||||
:formatter="dateFormatter"
|
||||
/>
|
||||
<el-table-column label="任务状态" prop="suspensionState">
|
||||
<template #default="scope">
|
||||
<el-tag type="success" v-if="scope.row.suspensionState === 1">激活</el-tag>
|
||||
<el-tag type="warning" v-if="scope.row.suspensionState === 2">挂起</el-tag>
|
||||
<el-tag v-if="scope.row.suspensionState === 1" type="success">激活</el-tag>
|
||||
<el-tag v-if="scope.row.suspensionState === 2" type="warning">挂起</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" align="center">
|
||||
<el-table-column align="center" label="操作">
|
||||
<template #default="scope">
|
||||
<el-button link type="primary" @click="handleAudit(scope.row)">审批进度</el-button>
|
||||
</template>
|
||||
@@ -63,19 +69,20 @@
|
||||
</el-table>
|
||||
<!-- 分页 -->
|
||||
<Pagination
|
||||
:total="total"
|
||||
v-model:page="queryParams.pageNo"
|
||||
v-model:limit="queryParams.pageSize"
|
||||
v-model:page="queryParams.pageNo"
|
||||
:total="total"
|
||||
@pagination="getList"
|
||||
/>
|
||||
</ContentWrap>
|
||||
</template>
|
||||
|
||||
<script setup lang="tsx" name="BpmDoneTask">
|
||||
<script lang="ts" name="BpmDoneTask" setup>
|
||||
import { dateFormatter } from '@/utils/formatTime'
|
||||
const { push } = useRouter() // 路由
|
||||
import * as TaskApi from '@/api/bpm/task'
|
||||
|
||||
const { push } = useRouter() // 路由
|
||||
|
||||
const loading = ref(true) // 列表的加载中
|
||||
const total = ref(0) // 列表的总页数
|
||||
const list = ref([]) // 列表的数据
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<template>
|
||||
<Dialog title="修改任务规则" v-model="dialogVisible" width="600">
|
||||
<Dialog v-model="dialogVisible" title="修改任务规则" width="600">
|
||||
<el-form ref="formRef" :model="formData" :rules="formRules" label-width="80px">
|
||||
<el-form-item label="任务名称" prop="taskDefinitionName">
|
||||
<el-input v-model="formData.taskDefinitionName" placeholder="请输入流标标识" disabled />
|
||||
<el-input v-model="formData.taskDefinitionName" disabled placeholder="请输入流标标识" />
|
||||
</el-form-item>
|
||||
<el-form-item label="任务标识" prop="taskDefinitionKey">
|
||||
<el-input v-model="formData.taskDefinitionKey" placeholder="请输入任务标识" disabled />
|
||||
<el-input v-model="formData.taskDefinitionKey" disabled placeholder="请输入任务标识" />
|
||||
</el-form-item>
|
||||
<el-form-item label="规则类型" prop="type">
|
||||
<el-select v-model="formData.type" clearable style="width: 100%">
|
||||
@@ -18,7 +18,7 @@
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="formData.type === 10" label="指定角色" prop="roleIds">
|
||||
<el-select v-model="formData.roleIds" multiple clearable style="width: 100%">
|
||||
<el-select v-model="formData.roleIds" clearable multiple style="width: 100%">
|
||||
<el-option
|
||||
v-for="item in roleOptions"
|
||||
:key="item.id"
|
||||
@@ -28,24 +28,24 @@
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item
|
||||
v-if="formData.type === 20 || formData.type === 21"
|
||||
label="指定部门"
|
||||
prop="deptIds"
|
||||
span="24"
|
||||
v-if="formData.type === 20 || formData.type === 21"
|
||||
>
|
||||
<el-tree-select
|
||||
ref="treeRef"
|
||||
v-model="formData.deptIds"
|
||||
node-key="id"
|
||||
show-checkbox
|
||||
:props="defaultProps"
|
||||
:data="deptTreeOptions"
|
||||
:props="defaultProps"
|
||||
empty-text="加载中,请稍后"
|
||||
multiple
|
||||
node-key="id"
|
||||
show-checkbox
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="指定岗位" prop="postIds" span="24" v-if="formData.type === 22">
|
||||
<el-select v-model="formData.postIds" multiple clearable style="width: 100%">
|
||||
<el-form-item v-if="formData.type === 22" label="指定岗位" prop="postIds" span="24">
|
||||
<el-select v-model="formData.postIds" clearable multiple style="width: 100%">
|
||||
<el-option
|
||||
v-for="item in postOptions"
|
||||
:key="parseInt(item.id)"
|
||||
@@ -55,12 +55,12 @@
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item
|
||||
v-if="formData.type === 30 || formData.type === 31 || formData.type === 32"
|
||||
label="指定用户"
|
||||
prop="userIds"
|
||||
span="24"
|
||||
v-if="formData.type === 30 || formData.type === 31 || formData.type === 32"
|
||||
>
|
||||
<el-select v-model="formData.userIds" multiple clearable style="width: 100%">
|
||||
<el-select v-model="formData.userIds" clearable multiple style="width: 100%">
|
||||
<el-option
|
||||
v-for="item in userOptions"
|
||||
:key="parseInt(item.id)"
|
||||
@@ -69,8 +69,8 @@
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="指定用户组" prop="userGroupIds" v-if="formData.type === 40">
|
||||
<el-select v-model="formData.userGroupIds" multiple clearable style="width: 100%">
|
||||
<el-form-item v-if="formData.type === 40" label="指定用户组" prop="userGroupIds">
|
||||
<el-select v-model="formData.userGroupIds" clearable multiple style="width: 100%">
|
||||
<el-option
|
||||
v-for="item in userGroupOptions"
|
||||
:key="parseInt(item.id)"
|
||||
@@ -79,8 +79,8 @@
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="指定脚本" prop="scripts" v-if="formData.type === 50">
|
||||
<el-select v-model="formData.scripts" multiple clearable style="width: 100%">
|
||||
<el-form-item v-if="formData.type === 50" label="指定脚本" prop="scripts">
|
||||
<el-select v-model="formData.scripts" clearable multiple style="width: 100%">
|
||||
<el-option
|
||||
v-for="dict in taskAssignScriptDictDatas"
|
||||
:key="dict.value"
|
||||
@@ -92,20 +92,21 @@
|
||||
</el-form>
|
||||
<!-- 操作按钮 -->
|
||||
<template #footer>
|
||||
<el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button>
|
||||
<el-button :disabled="formLoading" type="primary" @click="submitForm">确 定</el-button>
|
||||
<el-button @click="dialogVisible = false">取 消</el-button>
|
||||
</template>
|
||||
</Dialog>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
<script lang="ts" name="BpmTaskAssignRuleForm" setup>
|
||||
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
|
||||
import { handleTree, defaultProps } from '@/utils/tree'
|
||||
import { defaultProps, handleTree } from '@/utils/tree'
|
||||
import * as TaskAssignRuleApi from '@/api/bpm/taskAssignRule'
|
||||
import * as RoleApi from '@/api/system/role'
|
||||
import * as DeptApi from '@/api/system/dept'
|
||||
import * as PostApi from '@/api/system/post'
|
||||
import * as UserApi from '@/api/system/user'
|
||||
import * as UserGroupApi from '@/api/bpm/userGroup'
|
||||
|
||||
const { t } = useI18n() // 国际化
|
||||
const message = useMessage() // 消息弹窗
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<Dialog title="详情" v-model="dialogVisible" :scroll="true" :max-height="500" width="800">
|
||||
<el-descriptions border :column="1">
|
||||
<Dialog v-model="dialogVisible" :max-height="500" :scroll="true" title="详情" width="800">
|
||||
<el-descriptions :column="1" border>
|
||||
<el-descriptions-item label="日志主键" min-width="120">
|
||||
{{ detailData.id }}
|
||||
</el-descriptions-item>
|
||||
@@ -33,14 +33,14 @@
|
||||
<el-descriptions-item label="操作结果">
|
||||
<div v-if="detailData.resultCode === 0">正常</div>
|
||||
<div v-else-if="detailData.resultCode > 0"
|
||||
>失败 | {{ detailData.resultCode }} | {{ detailData.resultMsg }}</div
|
||||
>
|
||||
>失败 | {{ detailData.resultCode }} | {{ detailData.resultMsg }}
|
||||
</div>
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</Dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
<script lang="ts" name="ApiAccessLogDetail" setup>
|
||||
import { DICT_TYPE } from '@/utils/dict'
|
||||
import { formatDate } from '@/utils/formatTime'
|
||||
import * as ApiAccessLog from '@/api/infra/apiAccessLog'
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<Dialog title="详情" v-model="dialogVisible" :scroll="true" :max-height="500" width="800">
|
||||
<el-descriptions border :column="1">
|
||||
<Dialog v-model="dialogVisible" :max-height="500" :scroll="true" title="详情" width="800">
|
||||
<el-descriptions :column="1" border>
|
||||
<el-descriptions-item label="日志主键" min-width="120">
|
||||
{{ detailData.id }}
|
||||
</el-descriptions-item>
|
||||
@@ -32,12 +32,12 @@
|
||||
<el-descriptions-item label="异常名">
|
||||
{{ detailData.exceptionName }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="异常堆栈" v-if="detailData.exceptionStackTrace">
|
||||
<el-descriptions-item v-if="detailData.exceptionStackTrace" label="异常堆栈">
|
||||
<el-input
|
||||
type="textarea"
|
||||
:readonly="true"
|
||||
:autosize="{ maxRows: 20 }"
|
||||
v-model="detailData.exceptionStackTrace"
|
||||
:autosize="{ maxRows: 20 }"
|
||||
:readonly="true"
|
||||
type="textarea"
|
||||
/>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="处理状态">
|
||||
@@ -46,16 +46,16 @@
|
||||
:value="detailData.processStatus"
|
||||
/>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="处理人" v-if="detailData.processUserId">
|
||||
<el-descriptions-item v-if="detailData.processUserId" label="处理人">
|
||||
{{ detailData.processUserId }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="处理时间" v-if="detailData.processTime">
|
||||
<el-descriptions-item v-if="detailData.processTime" label="处理时间">
|
||||
{{ formatDate(detailData.processTime) }}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</Dialog>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
<script lang="ts" name="ApiErrorLogDetail" setup>
|
||||
import { DICT_TYPE } from '@/utils/dict'
|
||||
import { formatDate } from '@/utils/formatTime'
|
||||
import * as ApiErrorLog from '@/api/infra/apiErrorLog'
|
||||
|
||||
@@ -4,13 +4,13 @@
|
||||
<el-col>
|
||||
<div class="mb-2 float-right">
|
||||
<el-button size="small" type="primary" @click="showJson">生成 JSON</el-button>
|
||||
<el-button size="small" type="success" @click="showOption">生成O ptions</el-button>
|
||||
<el-button size="small" type="success" @click="showOption">生成 Options</el-button>
|
||||
<el-button size="small" type="danger" @click="showTemplate">生成组件</el-button>
|
||||
</div>
|
||||
</el-col>
|
||||
<!-- 表单设计器 -->
|
||||
<el-col>
|
||||
<fc-designer ref="designer" height="780px" />
|
||||
<FcDesigner ref="designer" height="780px" />
|
||||
</el-col>
|
||||
</el-row>
|
||||
</ContentWrap>
|
||||
@@ -22,18 +22,17 @@
|
||||
{{ t('common.copy') }}
|
||||
</el-button>
|
||||
<el-scrollbar height="580">
|
||||
<div v-highlight>
|
||||
<code class="hljs">
|
||||
{{ formData }}
|
||||
</code>
|
||||
<div>
|
||||
<pre><code class="hljs" v-html="highlightedCode(formData)"></code></pre>
|
||||
</div>
|
||||
</el-scrollbar>
|
||||
</div>
|
||||
</Dialog>
|
||||
</template>
|
||||
<script setup lang="ts" name="InfraBuild">
|
||||
import formCreate from '@form-create/element-ui'
|
||||
import FcDesigner from '@form-create/designer'
|
||||
import { useClipboard } from '@vueuse/core'
|
||||
import { isString } from '@/utils/is'
|
||||
const { t } = useI18n() // 国际化
|
||||
const message = useMessage() // 消息
|
||||
|
||||
@@ -109,4 +108,32 @@ const copy = async (text: string) => {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 代码高亮
|
||||
*/
|
||||
import hljs from 'highlight.js' // 导入代码高亮文件
|
||||
import 'highlight.js/styles/github.css' // 导入代码高亮样式
|
||||
import xml from 'highlight.js/lib/languages/java'
|
||||
import json from 'highlight.js/lib/languages/json'
|
||||
const highlightedCode = (code) => {
|
||||
// 处理语言和代码
|
||||
let language = 'json'
|
||||
if (formType.value === 2) {
|
||||
language = 'xml'
|
||||
}
|
||||
if (!isString(code)) {
|
||||
code = JSON.stringify(code)
|
||||
}
|
||||
// 高亮
|
||||
const result = hljs.highlight(language, code, true)
|
||||
return result.value || ' '
|
||||
}
|
||||
|
||||
/** 初始化 **/
|
||||
onMounted(async () => {
|
||||
// 注册代码高亮的各种语言
|
||||
hljs.registerLanguage('xml', xml)
|
||||
hljs.registerLanguage('json', json)
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -13,16 +13,17 @@
|
||||
</el-tabs>
|
||||
<el-form>
|
||||
<el-form-item style="float: right">
|
||||
<el-button type="primary" @click="submitForm" :loading="formLoading">保存</el-button>
|
||||
<el-button :loading="formLoading" type="primary" @click="submitForm">保存</el-button>
|
||||
<el-button @click="close">返回</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</ContentWrap>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
<script lang="ts" name="InfraCodegenEditTable" setup>
|
||||
import { useTagsViewStore } from '@/store/modules/tagsView'
|
||||
import { BasicInfoForm, ColumInfoForm, GenerateInfoForm } from './components'
|
||||
import * as CodegenApi from '@/api/infra/codegen'
|
||||
|
||||
const { t } = useI18n() // 国际化
|
||||
const message = useMessage() // 消息弹窗
|
||||
const { push, currentRoute } = useRouter() // 路由
|
||||
@@ -30,7 +31,7 @@ const { query } = useRoute() // 查询参数
|
||||
const { delView } = useTagsViewStore() // 视图操作
|
||||
|
||||
const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
|
||||
const activeName = ref('basicInfo') // Tag 激活的窗口
|
||||
const activeName = ref('colum') // Tag 激活的窗口
|
||||
const basicInfoRef = ref<ComponentRef<typeof BasicInfoForm>>()
|
||||
const columInfoRef = ref<ComponentRef<typeof ColumInfoForm>>()
|
||||
const generateInfoRef = ref<ComponentRef<typeof GenerateInfoForm>>()
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
<template>
|
||||
<Dialog title="导入表" v-model="dialogVisible" width="800px">
|
||||
<Dialog v-model="dialogVisible" title="导入表" width="800px">
|
||||
<!-- 搜索栏 -->
|
||||
<el-form :model="queryParams" ref="queryFormRef" :inline="true" label-width="68px">
|
||||
<el-form ref="queryFormRef" :inline="true" :model="queryParams" label-width="68px">
|
||||
<el-form-item label="数据源" prop="dataSourceConfigId">
|
||||
<el-select
|
||||
v-model="queryParams.dataSourceConfigId"
|
||||
placeholder="请选择数据源"
|
||||
class="!w-240px"
|
||||
placeholder="请选择数据源"
|
||||
>
|
||||
<el-option
|
||||
v-for="config in dataSourceConfigList"
|
||||
@@ -19,54 +19,61 @@
|
||||
<el-form-item label="表名称" prop="name">
|
||||
<el-input
|
||||
v-model="queryParams.name"
|
||||
placeholder="请输入表名称"
|
||||
clearable
|
||||
@keyup.enter="getList"
|
||||
class="!w-240px"
|
||||
clearable
|
||||
placeholder="请输入表名称"
|
||||
@keyup.enter="getList"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="表描述" prop="comment">
|
||||
<el-input
|
||||
v-model="queryParams.comment"
|
||||
placeholder="请输入表描述"
|
||||
clearable
|
||||
@keyup.enter="getList"
|
||||
class="!w-240px"
|
||||
clearable
|
||||
placeholder="请输入表描述"
|
||||
@keyup.enter="getList"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button @click="getList"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
|
||||
<el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
|
||||
<el-button @click="getList">
|
||||
<Icon class="mr-5px" icon="ep:search" />
|
||||
搜索
|
||||
</el-button>
|
||||
<el-button @click="resetQuery">
|
||||
<Icon class="mr-5px" icon="ep:refresh" />
|
||||
重置
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<!-- 列表 -->
|
||||
<el-row>
|
||||
<el-table
|
||||
v-loading="dbTableLoading"
|
||||
@row-click="handleRowClick"
|
||||
ref="tableRef"
|
||||
v-loading="dbTableLoading"
|
||||
:data="dbTableList"
|
||||
@selection-change="handleSelectionChange"
|
||||
height="260px"
|
||||
@row-click="handleRowClick"
|
||||
@selection-change="handleSelectionChange"
|
||||
>
|
||||
<el-table-column type="selection" width="55" />
|
||||
<el-table-column prop="name" label="表名称" :show-overflow-tooltip="true" />
|
||||
<el-table-column prop="comment" label="表描述" :show-overflow-tooltip="true" />
|
||||
<el-table-column :show-overflow-tooltip="true" label="表名称" prop="name" />
|
||||
<el-table-column :show-overflow-tooltip="true" label="表描述" prop="comment" />
|
||||
</el-table>
|
||||
</el-row>
|
||||
<!-- 操作 -->
|
||||
<template #footer>
|
||||
<el-button @click="handleImportTable" type="primary" :disabled="tableList.length === 0">
|
||||
<el-button :disabled="tableList.length === 0" type="primary" @click="handleImportTable">
|
||||
导入
|
||||
</el-button>
|
||||
<el-button @click="close">关闭</el-button>
|
||||
</template>
|
||||
</Dialog>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
<script lang="ts" name="InfraCodegenImportTable" setup>
|
||||
import * as CodegenApi from '@/api/infra/codegen'
|
||||
import * as DataSourceConfigApi from '@/api/infra/dataSourceConfig'
|
||||
import { ElTable } from 'element-plus'
|
||||
|
||||
const message = useMessage() // 消息弹窗
|
||||
|
||||
const dialogVisible = ref(false) // 弹窗的是否展示
|
||||
|
||||
@@ -1,52 +1,52 @@
|
||||
<template>
|
||||
<Dialog
|
||||
title="代码预览"
|
||||
v-model="dialogVisible"
|
||||
align-center
|
||||
width="80%"
|
||||
class="app-infra-codegen-preview-container"
|
||||
title="代码预览"
|
||||
width="80%"
|
||||
>
|
||||
<div class="flex">
|
||||
<!-- 代码目录树 -->
|
||||
<el-card
|
||||
class="w-1/3"
|
||||
:gutter="12"
|
||||
shadow="hover"
|
||||
v-loading="loading"
|
||||
:gutter="12"
|
||||
class="w-1/3"
|
||||
element-loading-text="生成文件目录中..."
|
||||
shadow="hover"
|
||||
>
|
||||
<el-scrollbar height="calc(100vh - 88px - 40px - 50px)">
|
||||
<el-scrollbar height="calc(100vh - 88px - 40px)">
|
||||
<el-tree
|
||||
ref="treeRef"
|
||||
node-key="id"
|
||||
:data="preview.fileTree"
|
||||
:expand-on-click-node="false"
|
||||
highlight-current
|
||||
@node-click="handleNodeClick"
|
||||
default-expand-all
|
||||
highlight-current
|
||||
node-key="id"
|
||||
@node-click="handleNodeClick"
|
||||
/>
|
||||
</el-scrollbar>
|
||||
</el-card>
|
||||
<!-- 代码 -->
|
||||
<el-card
|
||||
class="w-2/3 ml-3"
|
||||
:gutter="12"
|
||||
shadow="hover"
|
||||
v-loading="loading"
|
||||
:gutter="12"
|
||||
class="w-2/3 ml-3"
|
||||
element-loading-text="加载代码中..."
|
||||
shadow="hover"
|
||||
>
|
||||
<el-tabs v-model="preview.activeName">
|
||||
<el-tab-pane
|
||||
v-for="item in previewCodegen"
|
||||
:key="item.filePath"
|
||||
:label="item.filePath.substring(item.filePath.lastIndexOf('/') + 1)"
|
||||
:name="item.filePath"
|
||||
:key="item.filePath"
|
||||
>
|
||||
<el-button text type="primary" class="float-right" @click="copy(item.code)">
|
||||
<el-button class="float-right" text type="primary" @click="copy(item.code)">
|
||||
{{ t('common.copy') }}
|
||||
</el-button>
|
||||
<div v-highlight>
|
||||
<code>{{ item.code }}</code>
|
||||
<div>
|
||||
<pre><code class="hljs" v-html="highlightedCode(item)"></code></pre>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
@@ -54,10 +54,11 @@
|
||||
</div>
|
||||
</Dialog>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
<script lang="ts" name="InfraCodegenPreviewCode" setup>
|
||||
import { useClipboard } from '@vueuse/core'
|
||||
import { handleTree2 } from '@/utils/tree'
|
||||
import * as CodegenApi from '@/api/infra/codegen'
|
||||
|
||||
const { t } = useI18n() // 国际化
|
||||
const message = useMessage() // 消息弹窗
|
||||
|
||||
@@ -179,6 +180,34 @@ const copy = async (text: string) => {
|
||||
message.success(t('common.copySuccess'))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 代码高亮
|
||||
*/
|
||||
import hljs from 'highlight.js' // 导入代码高亮文件
|
||||
import 'highlight.js/styles/github.css' // 导入代码高亮样式
|
||||
import java from 'highlight.js/lib/languages/java'
|
||||
import xml from 'highlight.js/lib/languages/java'
|
||||
import javascript from 'highlight.js/lib/languages/javascript'
|
||||
import sql from 'highlight.js/lib/languages/sql'
|
||||
import typescript from 'highlight.js/lib/languages/typescript'
|
||||
const highlightedCode = (item) => {
|
||||
const language = item.filePath.substring(item.filePath.lastIndexOf('.') + 1)
|
||||
const result = hljs.highlight(language, item.code || '', true)
|
||||
return result.value || ' '
|
||||
}
|
||||
|
||||
/** 初始化 **/
|
||||
onMounted(async () => {
|
||||
// 注册代码高亮的各种语言
|
||||
hljs.registerLanguage('java', java)
|
||||
hljs.registerLanguage('xml', xml)
|
||||
hljs.registerLanguage('html', xml)
|
||||
hljs.registerLanguage('vue', xml)
|
||||
hljs.registerLanguage('javascript', javascript)
|
||||
hljs.registerLanguage('sql', sql)
|
||||
hljs.registerLanguage('typescript', typescript)
|
||||
})
|
||||
</script>
|
||||
<style lang="scss">
|
||||
.app-infra-codegen-preview-container {
|
||||
|
||||
@@ -3,12 +3,12 @@
|
||||
<el-row>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="表名称" prop="tableName">
|
||||
<el-input placeholder="请输入仓库名称" v-model="formData.tableName" />
|
||||
<el-input v-model="formData.tableName" placeholder="请输入仓库名称" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="表描述" prop="tableComment">
|
||||
<el-input placeholder="请输入" v-model="formData.tableComment" />
|
||||
<el-input v-model="formData.tableComment" placeholder="请输入" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
@@ -20,27 +20,27 @@
|
||||
content="默认去除表名的前缀。如果存在重复,则需要手动添加前缀,避免 MyBatis 报 Alias 重复的问题。"
|
||||
placement="top"
|
||||
>
|
||||
<Icon icon="ep:question-filled" class="" />
|
||||
<Icon class="" icon="ep:question-filled" />
|
||||
</el-tooltip>
|
||||
</span>
|
||||
</template>
|
||||
<el-input placeholder="请输入" v-model="formData.className" />
|
||||
<el-input v-model="formData.className" placeholder="请输入" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="作者" prop="author">
|
||||
<el-input placeholder="请输入" v-model="formData.author" />
|
||||
<el-input v-model="formData.author" placeholder="请输入" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="24">
|
||||
<el-form-item label="备注" prop="remark">
|
||||
<el-input type="textarea" :rows="3" v-model="formData.remark" />
|
||||
<el-input v-model="formData.remark" :rows="3" type="textarea" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-form>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
<script lang="ts" name="InfraCodegenBasicInfoForm" setup>
|
||||
import * as CodegenApi from '@/api/infra/codegen'
|
||||
import { PropType } from 'vue'
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<template>
|
||||
<el-table ref="dragTable" :data="formData" row-key="columnId" :max-height="tableHeight">
|
||||
<el-table ref="dragTable" :data="formData" :max-height="tableHeight" row-key="columnId">
|
||||
<el-table-column
|
||||
label="字段列名"
|
||||
prop="columnName"
|
||||
min-width="10%"
|
||||
:show-overflow-tooltip="true"
|
||||
label="字段列名"
|
||||
min-width="10%"
|
||||
prop="columnName"
|
||||
/>
|
||||
<el-table-column label="字段描述" min-width="10%">
|
||||
<template #default="scope">
|
||||
@@ -12,10 +12,10 @@
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
label="物理类型"
|
||||
prop="dataType"
|
||||
min-width="10%"
|
||||
:show-overflow-tooltip="true"
|
||||
label="物理类型"
|
||||
min-width="10%"
|
||||
prop="dataType"
|
||||
/>
|
||||
<el-table-column label="Java类型" min-width="11%">
|
||||
<template #default="scope">
|
||||
@@ -37,26 +37,26 @@
|
||||
</el-table-column>
|
||||
<el-table-column label="插入" min-width="4%">
|
||||
<template #default="scope">
|
||||
<el-checkbox true-label="true" false-label="false" v-model="scope.row.createOperation" />
|
||||
<el-checkbox v-model="scope.row.createOperation" false-label="false" true-label="true" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="编辑" min-width="4%">
|
||||
<template #default="scope">
|
||||
<el-checkbox true-label="true" false-label="false" v-model="scope.row.updateOperation" />
|
||||
<el-checkbox v-model="scope.row.updateOperation" false-label="false" true-label="true" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="列表" min-width="4%">
|
||||
<template #default="scope">
|
||||
<el-checkbox
|
||||
true-label="true"
|
||||
false-label="false"
|
||||
v-model="scope.row.listOperationResult"
|
||||
false-label="false"
|
||||
true-label="true"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="查询" min-width="4%">
|
||||
<template #default="scope">
|
||||
<el-checkbox true-label="true" false-label="false" v-model="scope.row.listOperation" />
|
||||
<el-checkbox v-model="scope.row.listOperation" false-label="false" true-label="true" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="查询方式" min-width="10%">
|
||||
@@ -75,7 +75,7 @@
|
||||
</el-table-column>
|
||||
<el-table-column label="允许空" min-width="5%">
|
||||
<template #default="scope">
|
||||
<el-checkbox true-label="true" false-label="false" v-model="scope.row.nullable" />
|
||||
<el-checkbox v-model="scope.row.nullable" false-label="false" true-label="true" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="显示类型" min-width="12%">
|
||||
@@ -112,7 +112,7 @@
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
<script lang="ts" name="InfraCodegenColumInfoForm" setup>
|
||||
import { PropType } from 'vue'
|
||||
import * as CodegenApi from '@/api/infra/codegen'
|
||||
import * as DictDataApi from '@/api/system/dict/dict.type'
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<el-form ref="formRef" :model="formData" :rules="rules" label-width="150px">
|
||||
<el-row>
|
||||
<el-col :span="12">
|
||||
<el-form-item prop="templateType" label="生成模板">
|
||||
<el-form-item label="生成模板" prop="templateType">
|
||||
<el-select v-model="formData.templateType" @change="tplSelectChange">
|
||||
<el-option
|
||||
v-for="dict in getIntDictOptions(DICT_TYPE.INFRA_CODEGEN_TEMPLATE_TYPE)"
|
||||
@@ -14,7 +14,20 @@
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item prop="scene" label="生成场景">
|
||||
<el-form-item label="前端类型" prop="frontType">
|
||||
<el-select v-model="formData.frontType">
|
||||
<el-option
|
||||
v-for="dict in getIntDictOptions(DICT_TYPE.INFRA_CODEGEN_FRONT_TYPE)"
|
||||
:key="dict.value"
|
||||
:label="dict.label"
|
||||
:value="dict.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="12">
|
||||
<el-form-item label="生成场景" prop="scene">
|
||||
<el-select v-model="formData.scene">
|
||||
<el-option
|
||||
v-for="dict in getIntDictOptions(DICT_TYPE.INFRA_CODEGEN_SCENE)"
|
||||
@@ -25,6 +38,26 @@
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item>
|
||||
<template #label>
|
||||
<span>
|
||||
上级菜单
|
||||
<el-tooltip content="分配到指定菜单下,例如 系统管理" placement="top">
|
||||
<Icon icon="ep:question-filled" />
|
||||
</el-tooltip>
|
||||
</span>
|
||||
</template>
|
||||
<el-tree-select
|
||||
v-model="formData.parentMenuId"
|
||||
:data="menus"
|
||||
:props="menuTreeProps"
|
||||
check-strictly
|
||||
node-key="id"
|
||||
placeholder="请选择系统菜单"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
|
||||
<!-- <el-col :span="12">-->
|
||||
<!-- <el-form-item prop="packageName">-->
|
||||
@@ -115,28 +148,7 @@
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="12">
|
||||
<el-form-item>
|
||||
<template #label>
|
||||
<span>
|
||||
上级菜单
|
||||
<el-tooltip content="分配到指定菜单下,例如 系统管理" placement="top">
|
||||
<Icon icon="ep:question-filled" />
|
||||
</el-tooltip>
|
||||
</span>
|
||||
</template>
|
||||
<el-tree-select
|
||||
v-model="formData.parentMenuId"
|
||||
placeholder="请选择系统菜单"
|
||||
node-key="id"
|
||||
check-strictly
|
||||
:data="menus"
|
||||
:props="menuTreeProps"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="24" v-if="formData.genType === '1'">
|
||||
<el-col v-if="formData.genType === '1'" :span="24">
|
||||
<el-form-item prop="genPath">
|
||||
<template #label>
|
||||
<span>
|
||||
@@ -279,8 +291,8 @@
|
||||
</el-row>
|
||||
</el-form>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { getIntDictOptions, DICT_TYPE } from '@/utils/dict'
|
||||
<script lang="ts" name="InfraCodegenGenerateInfoForm" setup>
|
||||
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
|
||||
import { handleTree } from '@/utils/tree'
|
||||
import * as CodegenApi from '@/api/infra/codegen'
|
||||
import * as MenuApi from '@/api/system/menu'
|
||||
@@ -297,6 +309,7 @@ const props = defineProps({
|
||||
const formRef = ref()
|
||||
const formData = ref({
|
||||
templateType: null,
|
||||
frontType: null,
|
||||
scene: null,
|
||||
moduleName: '',
|
||||
businessName: '',
|
||||
@@ -315,6 +328,7 @@ const formData = ref({
|
||||
|
||||
const rules = reactive({
|
||||
templateType: [required],
|
||||
frontType: [required],
|
||||
scene: [required],
|
||||
moduleName: [required],
|
||||
businessName: [required],
|
||||
|
||||
@@ -5,46 +5,53 @@
|
||||
<!-- 搜索 -->
|
||||
<ContentWrap>
|
||||
<el-form
|
||||
class="-mb-15px"
|
||||
:model="queryParams"
|
||||
ref="queryFormRef"
|
||||
:inline="true"
|
||||
:model="queryParams"
|
||||
class="-mb-15px"
|
||||
label-width="68px"
|
||||
>
|
||||
<el-form-item label="表名称" prop="tableName">
|
||||
<el-input
|
||||
v-model="queryParams.tableName"
|
||||
placeholder="请输入表名称"
|
||||
clearable
|
||||
@keyup.enter="handleQuery"
|
||||
class="!w-240px"
|
||||
clearable
|
||||
placeholder="请输入表名称"
|
||||
@keyup.enter="handleQuery"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="表描述" prop="tableComment">
|
||||
<el-input
|
||||
v-model="queryParams.tableComment"
|
||||
placeholder="请输入表描述"
|
||||
clearable
|
||||
@keyup.enter="handleQuery"
|
||||
class="!w-240px"
|
||||
clearable
|
||||
placeholder="请输入表描述"
|
||||
@keyup.enter="handleQuery"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="创建时间" prop="createTime">
|
||||
<el-date-picker
|
||||
v-model="queryParams.createTime"
|
||||
value-format="YYYY-MM-dd HH:mm:ss"
|
||||
type="daterange"
|
||||
start-placeholder="开始日期"
|
||||
end-placeholder="结束日期"
|
||||
:default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
|
||||
class="!w-240px"
|
||||
end-placeholder="结束日期"
|
||||
start-placeholder="开始日期"
|
||||
type="daterange"
|
||||
value-format="YYYY-MM-dd HH:mm:ss"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" />搜索</el-button>
|
||||
<el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" />重置</el-button>
|
||||
<el-button type="primary" v-hasPermi="['infra:codegen:create']" @click="openImportTable()">
|
||||
<Icon icon="ep:zoom-in" class="mr-5px" /> 导入
|
||||
<el-button @click="handleQuery">
|
||||
<Icon class="mr-5px" icon="ep:search" />
|
||||
搜索
|
||||
</el-button>
|
||||
<el-button @click="resetQuery">
|
||||
<Icon class="mr-5px" icon="ep:refresh" />
|
||||
重置
|
||||
</el-button>
|
||||
<el-button v-hasPermi="['infra:codegen:create']" type="primary" @click="openImportTable()">
|
||||
<Icon class="mr-5px" icon="ep:zoom-in" />
|
||||
导入
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
@@ -53,75 +60,75 @@
|
||||
<!-- 列表 -->
|
||||
<ContentWrap>
|
||||
<el-table v-loading="loading" :data="list">
|
||||
<el-table-column label="数据源" align="center">
|
||||
<el-table-column align="center" label="数据源">
|
||||
<template #default="scope">
|
||||
{{
|
||||
dataSourceConfigList.find((config) => config.id === scope.row.dataSourceConfigId)?.name
|
||||
}}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="表名称" align="center" prop="tableName" width="200" />
|
||||
<el-table-column align="center" label="表名称" prop="tableName" width="200" />
|
||||
<el-table-column
|
||||
label="表描述"
|
||||
align="center"
|
||||
prop="tableComment"
|
||||
:show-overflow-tooltip="true"
|
||||
align="center"
|
||||
label="表描述"
|
||||
prop="tableComment"
|
||||
width="200"
|
||||
/>
|
||||
<el-table-column label="实体" align="center" prop="className" width="200" />
|
||||
<el-table-column align="center" label="实体" prop="className" width="200" />
|
||||
<el-table-column
|
||||
:formatter="dateFormatter"
|
||||
align="center"
|
||||
label="创建时间"
|
||||
align="center"
|
||||
prop="createTime"
|
||||
width="180"
|
||||
:formatter="dateFormatter"
|
||||
/>
|
||||
<el-table-column
|
||||
label="更新时间"
|
||||
:formatter="dateFormatter"
|
||||
align="center"
|
||||
label="更新时间"
|
||||
prop="createTime"
|
||||
width="180"
|
||||
:formatter="dateFormatter"
|
||||
/>
|
||||
<el-table-column label="操作" align="center" width="300px" fixed="right">
|
||||
<el-table-column align="center" fixed="right" label="操作" width="300px">
|
||||
<template #default="scope">
|
||||
<el-button
|
||||
v-hasPermi="['infra:codegen:preview']"
|
||||
link
|
||||
type="primary"
|
||||
@click="handlePreview(scope.row)"
|
||||
v-hasPermi="['infra:codegen:preview']"
|
||||
>
|
||||
预览
|
||||
</el-button>
|
||||
<el-button
|
||||
v-hasPermi="['infra:codegen:update']"
|
||||
link
|
||||
type="primary"
|
||||
@click="handleUpdate(scope.row.id)"
|
||||
v-hasPermi="['infra:codegen:update']"
|
||||
>
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button
|
||||
v-hasPermi="['infra:codegen:delete']"
|
||||
link
|
||||
type="danger"
|
||||
@click="handleDelete(scope.row.id)"
|
||||
v-hasPermi="['infra:codegen:delete']"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
<el-button
|
||||
v-hasPermi="['infra:codegen:update']"
|
||||
link
|
||||
type="primary"
|
||||
@click="handleSyncDB(scope.row)"
|
||||
v-hasPermi="['infra:codegen:update']"
|
||||
>
|
||||
同步
|
||||
</el-button>
|
||||
<el-button
|
||||
v-hasPermi="['infra:codegen:download']"
|
||||
link
|
||||
type="primary"
|
||||
@click="handleGenTable(scope.row)"
|
||||
v-hasPermi="['infra:codegen:download']"
|
||||
>
|
||||
生成代码
|
||||
</el-button>
|
||||
@@ -130,25 +137,26 @@
|
||||
</el-table>
|
||||
<!-- 分页 -->
|
||||
<Pagination
|
||||
:total="total"
|
||||
v-model:page="queryParams.pageNo"
|
||||
v-model:limit="queryParams.pageSize"
|
||||
v-model:page="queryParams.pageNo"
|
||||
:total="total"
|
||||
@pagination="getList"
|
||||
/>
|
||||
</ContentWrap>
|
||||
|
||||
<!-- 弹窗:导入表 -->
|
||||
<ImportTable ref="importRef" success="getList" />
|
||||
<ImportTable ref="importRef" @success="getList" />
|
||||
<!-- 弹窗:预览代码 -->
|
||||
<PreviewCode ref="previewRef" />
|
||||
</template>
|
||||
<script setup lang="ts" name="InfraCodegen">
|
||||
<script lang="ts" name="InfraCodegen" setup>
|
||||
import { dateFormatter } from '@/utils/formatTime'
|
||||
import download from '@/utils/download'
|
||||
import * as CodegenApi from '@/api/infra/codegen'
|
||||
import * as DataSourceConfigApi from '@/api/infra/dataSourceConfig'
|
||||
import ImportTable from './ImportTable.vue'
|
||||
import PreviewCode from './PreviewCode.vue'
|
||||
|
||||
const message = useMessage() // 消息弹窗
|
||||
const { t } = useI18n() // 国际化
|
||||
const { push } = useRouter() // 路由跳转
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<template>
|
||||
<Dialog :title="dialogTitle" v-model="dialogVisible">
|
||||
<Dialog v-model="dialogVisible" :title="dialogTitle">
|
||||
<el-form
|
||||
ref="formRef"
|
||||
v-loading="formLoading"
|
||||
:model="formData"
|
||||
:rules="formRules"
|
||||
label-width="80px"
|
||||
v-loading="formLoading"
|
||||
>
|
||||
<el-form-item label="参数分类" prop="category">
|
||||
<el-input v-model="formData.category" placeholder="请输入参数分类" />
|
||||
@@ -31,16 +31,16 @@
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="备注" prop="remark">
|
||||
<el-input v-model="formData.remark" type="textarea" placeholder="请输入内容" />
|
||||
<el-input v-model="formData.remark" placeholder="请输入内容" type="textarea" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button>
|
||||
<el-button :disabled="formLoading" type="primary" @click="submitForm">确 定</el-button>
|
||||
<el-button @click="dialogVisible = false">取 消</el-button>
|
||||
</template>
|
||||
</Dialog>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
<script lang="ts" name="InfraConfigForm" setup>
|
||||
import { DICT_TYPE, getBoolDictOptions } from '@/utils/dict'
|
||||
import * as ConfigApi from '@/api/infra/config'
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<template>
|
||||
<Dialog :title="dialogTitle" v-model="dialogVisible">
|
||||
<Dialog v-model="dialogVisible" :title="dialogTitle">
|
||||
<el-form
|
||||
ref="formRef"
|
||||
v-loading="formLoading"
|
||||
:model="formData"
|
||||
:rules="formRules"
|
||||
label-width="100px"
|
||||
v-loading="formLoading"
|
||||
>
|
||||
<el-form-item label="数据源名称" prop="name">
|
||||
<el-input v-model="formData.name" placeholder="请输入参数名称" />
|
||||
@@ -21,12 +21,12 @@
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button>
|
||||
<el-button :disabled="formLoading" type="primary" @click="submitForm">确 定</el-button>
|
||||
<el-button @click="dialogVisible = false">取 消</el-button>
|
||||
</template>
|
||||
</Dialog>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
<script lang="ts" name="InfraDataSourceConfigForm" setup>
|
||||
import * as DataSourceConfigApi from '@/api/infra/dataSourceConfig'
|
||||
|
||||
const { t } = useI18n() // 国际化
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
<template>
|
||||
<Dialog title="上传文件" v-model="dialogVisible">
|
||||
<Dialog v-model="dialogVisible" title="上传文件">
|
||||
<el-upload
|
||||
ref="uploadRef"
|
||||
:action="url"
|
||||
:data="data"
|
||||
:headers="uploadHeaders"
|
||||
v-model:file-list="fileList"
|
||||
drag
|
||||
accept=".jpg, .png, .gif"
|
||||
:limit="1"
|
||||
:on-success="submitFormSuccess"
|
||||
:on-exceed="handleExceed"
|
||||
:on-error="submitFormError"
|
||||
:on-change="handleFileChange"
|
||||
:action="url"
|
||||
:auto-upload="false"
|
||||
:data="data"
|
||||
:disabled="formLoading"
|
||||
:headers="uploadHeaders"
|
||||
:limit="1"
|
||||
:on-change="handleFileChange"
|
||||
:on-error="submitFormError"
|
||||
:on-exceed="handleExceed"
|
||||
:on-success="submitFormSuccess"
|
||||
accept=".jpg, .png, .gif"
|
||||
drag
|
||||
>
|
||||
<i class="el-icon-upload"></i>
|
||||
<div class="el-upload__text"> 将文件拖到此处,或 <em>点击上传</em> </div>
|
||||
<div class="el-upload__text"> 将文件拖到此处,或 <em>点击上传</em></div>
|
||||
<template #tip>
|
||||
<div class="el-upload__tip" style="color: red">
|
||||
提示:仅允许导入 jpg、png、gif 格式文件!
|
||||
@@ -25,13 +25,14 @@
|
||||
</template>
|
||||
</el-upload>
|
||||
<template #footer>
|
||||
<el-button @click="submitFileForm" type="primary" :disabled="formLoading">确 定</el-button>
|
||||
<el-button :disabled="formLoading" type="primary" @click="submitFileForm">确 定</el-button>
|
||||
<el-button @click="dialogVisible = false">取 消</el-button>
|
||||
</template>
|
||||
</Dialog>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
<script lang="ts" name="InfraFileForm" setup>
|
||||
import { getAccessToken, getTenantId } from '@/utils/auth'
|
||||
|
||||
const { t } = useI18n() // 国际化
|
||||
const message = useMessage() // 消息弹窗
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<template>
|
||||
<Dialog :title="dialogTitle" v-model="dialogVisible">
|
||||
<Dialog v-model="dialogVisible" :title="dialogTitle">
|
||||
<el-form
|
||||
ref="formRef"
|
||||
v-loading="formLoading"
|
||||
:model="formData"
|
||||
:rules="formRules"
|
||||
label-width="120px"
|
||||
v-loading="formLoading"
|
||||
>
|
||||
<el-form-item label="配置名" prop="name">
|
||||
<el-input v-model="formData.name" placeholder="请输入配置名" />
|
||||
@@ -16,8 +16,8 @@
|
||||
<el-form-item label="存储器" prop="storage">
|
||||
<el-select
|
||||
v-model="formData.storage"
|
||||
placeholder="请选择存储器"
|
||||
:disabled="formData.id !== undefined"
|
||||
placeholder="请选择存储器"
|
||||
>
|
||||
<el-option
|
||||
v-for="dict in getDictOptions(DICT_TYPE.INFRA_FILE_STORAGE)"
|
||||
@@ -48,7 +48,7 @@
|
||||
label="主机端口"
|
||||
prop="config.port"
|
||||
>
|
||||
<el-input-number :min="0" v-model="formData.config.port" placeholder="请输入主机端口" />
|
||||
<el-input-number v-model="formData.config.port" :min="0" placeholder="请输入主机端口" />
|
||||
</el-form-item>
|
||||
<el-form-item
|
||||
v-if="formData.storage >= 11 && formData.storage <= 12"
|
||||
@@ -93,14 +93,15 @@
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button>
|
||||
<el-button :disabled="formLoading" type="primary" @click="submitForm">确 定</el-button>
|
||||
<el-button @click="dialogVisible = false">取 消</el-button>
|
||||
</template>
|
||||
</Dialog>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
<script lang="ts" name="InfraFileConfigForm" setup>
|
||||
import { DICT_TYPE, getDictOptions } from '@/utils/dict'
|
||||
import * as FileConfigApi from '@/api/infra/fileConfig'
|
||||
|
||||
const { t } = useI18n() // 国际化
|
||||
const message = useMessage() // 消息弹窗
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<Dialog title="任务详细" v-model="dialogVisible" width="700px">
|
||||
<el-descriptions border :column="1">
|
||||
<Dialog v-model="dialogVisible" title="任务详细" width="700px">
|
||||
<el-descriptions :column="1" border>
|
||||
<el-descriptions-item label="任务编号" min-width="60">
|
||||
{{ detailData.id }}
|
||||
</el-descriptions-item>
|
||||
@@ -42,7 +42,7 @@
|
||||
</el-descriptions>
|
||||
</Dialog>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
<script lang="ts" name="InfraJobDetail" setup>
|
||||
import { DICT_TYPE } from '@/utils/dict'
|
||||
import { formatDate } from '@/utils/formatTime'
|
||||
import * as JobApi from '@/api/infra/job'
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<Dialog title="任务详细" v-model="dialogVisible" width="700px">
|
||||
<el-descriptions border :column="1">
|
||||
<Dialog v-model="dialogVisible" title="任务详细" width="700px">
|
||||
<el-descriptions :column="1" border>
|
||||
<el-descriptions-item label="日志编号" min-width="60">
|
||||
{{ detailData.id }}
|
||||
</el-descriptions-item>
|
||||
@@ -31,7 +31,7 @@
|
||||
</el-descriptions>
|
||||
</Dialog>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
<script lang="ts" name="JobLogDetail" setup>
|
||||
import { DICT_TYPE } from '@/utils/dict'
|
||||
import { formatDate } from '@/utils/formatTime'
|
||||
import * as JobLogApi from '@/api/infra/jobLog'
|
||||
|
||||
@@ -218,6 +218,7 @@ const getSummary = () => {
|
||||
usedMemoryInstance()
|
||||
}
|
||||
|
||||
<<<<<<< HEAD
|
||||
/** 命令使用情况 */
|
||||
const initcommandStatsChart = async () => {
|
||||
usedmemoryEchartChika.series[0].data = []
|
||||
@@ -264,6 +265,10 @@ const usedMemoryInstance = async () => {
|
||||
|
||||
/** 初始化 **/
|
||||
onMounted(() => {
|
||||
=======
|
||||
onBeforeMount(() => {
|
||||
// TODO @hiiwbs 微信,优化使用 Echart 组件
|
||||
>>>>>>> e92361ed401771998c6abd6d9ba53801a6e0cfa6
|
||||
readRedisInfo()
|
||||
// 加载数据
|
||||
getSummary()
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="flex">
|
||||
<el-card class="w-1/2" :gutter="12" shadow="always">
|
||||
<el-card :gutter="12" class="w-1/2" shadow="always">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>连接</span>
|
||||
@@ -14,7 +14,7 @@
|
||||
|
||||
<div class="flex">
|
||||
<el-input v-model="server" disabled>
|
||||
<template #prepend> 服务地址 </template>
|
||||
<template #prepend> 服务地址</template>
|
||||
</el-input>
|
||||
<el-button :type="getIsOpen ? 'danger' : 'primary'" @click="toggle">
|
||||
{{ getIsOpen ? '关闭连接' : '开启连接' }}
|
||||
@@ -25,15 +25,15 @@
|
||||
<el-input
|
||||
v-model="sendValue"
|
||||
:autosize="{ minRows: 2, maxRows: 4 }"
|
||||
type="textarea"
|
||||
:disabled="!getIsOpen"
|
||||
clearable
|
||||
type="textarea"
|
||||
/>
|
||||
<el-button type="primary" block class="mt-4" :disabled="!getIsOpen" @click="handlerSend">
|
||||
<el-button :disabled="!getIsOpen" block class="mt-4" type="primary" @click="handlerSend">
|
||||
发送
|
||||
</el-button>
|
||||
</el-card>
|
||||
<el-card class="w-1/2" :gutter="12" shadow="always">
|
||||
<el-card :gutter="12" class="w-1/2" shadow="always">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>消息记录</span>
|
||||
@@ -41,7 +41,7 @@
|
||||
</template>
|
||||
<div class="max-h-80 overflow-auto">
|
||||
<ul>
|
||||
<li v-for="item in getList" class="mt-2" :key="item.time">
|
||||
<li v-for="item in getList" :key="item.time" class="mt-2">
|
||||
<div class="flex items-center">
|
||||
<span class="mr-2 text-primary font-medium">收到消息:</span>
|
||||
<span>{{ formatDate(item.time) }}</span>
|
||||
@@ -55,7 +55,7 @@
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
<script lang="ts" name="InfraWebSocket" setup>
|
||||
import { formatDate } from '@/utils/formatTime'
|
||||
import { useUserStore } from '@/store/modules/user'
|
||||
import { useWebSocket } from '@vueuse/core'
|
||||
|
||||
@@ -4,27 +4,30 @@
|
||||
ref="formRef"
|
||||
:model="formData"
|
||||
:rules="formRules"
|
||||
label-width="80px"
|
||||
label-width="120px"
|
||||
v-loading="formLoading"
|
||||
>
|
||||
<el-form-item label="上级分类" prop="parentId">
|
||||
<el-tree-select
|
||||
v-model="formData.parentId"
|
||||
:data="categoryTree"
|
||||
:props="{ label: 'name', value: 'id' }"
|
||||
:render-after-expand="false"
|
||||
placeholder="请选择上级分类"
|
||||
check-strictly
|
||||
default-expand-all
|
||||
/>
|
||||
<el-select v-model="formData.parentId" placeholder="请选择上级分类">
|
||||
<el-option :key="0" label="顶级分类" :value="0" />
|
||||
<el-option
|
||||
v-for="item in categoryList"
|
||||
:key="item.id"
|
||||
:label="item.name"
|
||||
:value="item.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="分类名称" prop="name">
|
||||
<el-input v-model="formData.name" placeholder="请输入分类名称" />
|
||||
</el-form-item>
|
||||
<el-form-item label="分类图片" prop="picUrl">
|
||||
<el-form-item label="移动端分类图" prop="picUrl">
|
||||
<UploadImg v-model="formData.picUrl" :limit="1" :is-show-tip="false" />
|
||||
<div v-if="formData.parentId === 0" style="font-size: 10px">推荐 200x100 图片分辨率</div>
|
||||
<div v-else style="font-size: 10px">推荐 100x100 图片分辨率</div>
|
||||
<div style="font-size: 10px" class="pl-10px">推荐 180x180 图片分辨率</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="PC 端分类图" prop="bigPicUrl">
|
||||
<UploadImg v-model="formData.bigPicUrl" :limit="1" :is-show-tip="false" />
|
||||
<div style="font-size: 10px" class="pl-10px">推荐 468x340 图片分辨率</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="分类排序" prop="sort">
|
||||
<el-input-number v-model="formData.sort" controls-position="right" :min="0" />
|
||||
@@ -40,9 +43,6 @@
|
||||
</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="分类描述">
|
||||
<el-input v-model="formData.description" type="textarea" placeholder="请输入分类描述" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button>
|
||||
@@ -53,7 +53,6 @@
|
||||
<script setup lang="ts" name="ProductCategory">
|
||||
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
|
||||
import { CommonStatusEnum } from '@/utils/constants'
|
||||
import { handleTree } from '@/utils/tree'
|
||||
import * as ProductCategoryApi from '@/api/mall/product/category'
|
||||
const { t } = useI18n() // 国际化
|
||||
const message = useMessage() // 消息弹窗
|
||||
@@ -66,8 +65,8 @@ const formData = ref({
|
||||
id: undefined,
|
||||
name: '',
|
||||
picUrl: '',
|
||||
status: CommonStatusEnum.ENABLE,
|
||||
description: ''
|
||||
bigPicUrl: '',
|
||||
status: CommonStatusEnum.ENABLE
|
||||
})
|
||||
const formRules = reactive({
|
||||
parentId: [{ required: true, message: '请选择上级分类', trigger: 'blur' }],
|
||||
@@ -77,7 +76,7 @@ const formRules = reactive({
|
||||
status: [{ required: true, message: '开启状态不能为空', trigger: 'blur' }]
|
||||
})
|
||||
const formRef = ref() // 表单 Ref
|
||||
const categoryTree = ref<any[]>([]) // 分类树
|
||||
const categoryList = ref<any[]>([]) // 分类树
|
||||
|
||||
/** 打开弹窗 */
|
||||
const open = async (type: string, id?: number) => {
|
||||
@@ -95,7 +94,7 @@ const open = async (type: string, id?: number) => {
|
||||
}
|
||||
}
|
||||
// 获得分类树
|
||||
await getTree()
|
||||
categoryList.value = await ProductCategoryApi.getCategoryList({ parentId: 0 })
|
||||
}
|
||||
defineExpose({ open }) // 提供 open 方法,用于打开弹窗
|
||||
|
||||
@@ -131,17 +130,9 @@ const resetForm = () => {
|
||||
id: undefined,
|
||||
name: '',
|
||||
picUrl: '',
|
||||
status: CommonStatusEnum.ENABLE,
|
||||
description: ''
|
||||
bigPicUrl: '',
|
||||
status: CommonStatusEnum.ENABLE
|
||||
}
|
||||
formRef.value?.resetFields()
|
||||
}
|
||||
|
||||
/** 获得分类树 */
|
||||
const getTree = async () => {
|
||||
const data = await ProductCategoryApi.getCategoryList({})
|
||||
const tree = handleTree(data, 'id', 'parentId')
|
||||
const menu = { id: 0, name: '顶级分类', children: tree }
|
||||
categoryTree.value = [menu]
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -36,9 +36,9 @@
|
||||
<ContentWrap>
|
||||
<el-table v-loading="loading" :data="list" row-key="id" default-expand-all>
|
||||
<el-table-column label="分类名称" prop="name" sortable />
|
||||
<el-table-column label="分类图片" align="center" prop="picUrl">
|
||||
<el-table-column label="移动端分类图" align="center" prop="picUrl">
|
||||
<template #default="scope">
|
||||
<img v-if="scope.row.picUrl" :src="scope.row.picUrl" alt="分类图片" class="h-100px" />
|
||||
<img v-if="scope.row.picUrl" :src="scope.row.picUrl" alt="移动端分类图" class="h-100px" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="分类排序" align="center" prop="sort" />
|
||||
|
||||
@@ -1,27 +1,28 @@
|
||||
<template>
|
||||
<Dialog :title="dialogTitle" v-model="dialogVisible">
|
||||
<Dialog v-model="dialogVisible" :title="dialogTitle">
|
||||
<el-form
|
||||
ref="formRef"
|
||||
v-loading="formLoading"
|
||||
:model="formData"
|
||||
:rules="formRules"
|
||||
label-width="80px"
|
||||
v-loading="formLoading"
|
||||
>
|
||||
<el-form-item label="名称" prop="name">
|
||||
<el-input v-model="formData.name" placeholder="请输入名称" />
|
||||
</el-form-item>
|
||||
<el-form-item label="备注" prop="remark">
|
||||
<el-input v-model="formData.remark" type="textarea" placeholder="请输入内容" />
|
||||
<el-input v-model="formData.remark" placeholder="请输入内容" type="textarea" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button>
|
||||
<el-button :disabled="formLoading" type="primary" @click="submitForm">确 定</el-button>
|
||||
<el-button @click="dialogVisible = false">取 消</el-button>
|
||||
</template>
|
||||
</Dialog>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
<script lang="ts" name="ProductPropertyForm" setup>
|
||||
import * as PropertyApi from '@/api/mall/product/property'
|
||||
|
||||
const { t } = useI18n() // 国际化
|
||||
const message = useMessage() // 消息弹窗
|
||||
|
||||
|
||||
@@ -2,42 +2,49 @@
|
||||
<!-- 搜索工作栏 -->
|
||||
<ContentWrap>
|
||||
<el-form
|
||||
class="-mb-15px"
|
||||
:model="queryParams"
|
||||
ref="queryFormRef"
|
||||
:inline="true"
|
||||
:model="queryParams"
|
||||
class="-mb-15px"
|
||||
label-width="68px"
|
||||
>
|
||||
<el-form-item label="名称" prop="name">
|
||||
<el-input
|
||||
v-model="queryParams.name"
|
||||
placeholder="请输入名称"
|
||||
clearable
|
||||
@keyup.enter="handleQuery"
|
||||
class="!w-240px"
|
||||
clearable
|
||||
placeholder="请输入名称"
|
||||
@keyup.enter="handleQuery"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="创建时间" prop="createTime">
|
||||
<el-date-picker
|
||||
v-model="queryParams.createTime"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
type="daterange"
|
||||
start-placeholder="开始日期"
|
||||
end-placeholder="结束日期"
|
||||
:default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
|
||||
class="!w-240px"
|
||||
end-placeholder="结束日期"
|
||||
start-placeholder="开始日期"
|
||||
type="daterange"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
|
||||
<el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
|
||||
<el-button @click="handleQuery">
|
||||
<Icon class="mr-5px" icon="ep:search" />
|
||||
搜索
|
||||
</el-button>
|
||||
<el-button @click="resetQuery">
|
||||
<Icon class="mr-5px" icon="ep:refresh" />
|
||||
重置
|
||||
</el-button>
|
||||
<el-button
|
||||
v-hasPermi="['product:property:create']"
|
||||
plain
|
||||
type="primary"
|
||||
@click="openForm('create')"
|
||||
v-hasPermi="['product:property:create']"
|
||||
>
|
||||
<Icon icon="ep:plus" class="mr-5px" /> 新增
|
||||
<Icon class="mr-5px" icon="ep:plus" />
|
||||
新增
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
@@ -46,23 +53,23 @@
|
||||
<!-- 列表 -->
|
||||
<ContentWrap>
|
||||
<el-table v-loading="loading" :data="list">
|
||||
<el-table-column label="编号" align="center" prop="id" />
|
||||
<el-table-column label="名称" align="center" />
|
||||
<el-table-column label="备注" align="center" prop="remark" :show-overflow-tooltip="true" />
|
||||
<el-table-column align="center" label="编号" prop="id" />
|
||||
<el-table-column align="center" label="名称" prop="name" />
|
||||
<el-table-column :show-overflow-tooltip="true" align="center" label="备注" prop="remark" />
|
||||
<el-table-column
|
||||
label="创建时间"
|
||||
:formatter="dateFormatter"
|
||||
align="center"
|
||||
label="创建时间"
|
||||
prop="createTime"
|
||||
width="180"
|
||||
:formatter="dateFormatter"
|
||||
/>
|
||||
<el-table-column label="操作" align="center">
|
||||
<el-table-column align="center" label="操作">
|
||||
<template #default="scope">
|
||||
<el-button
|
||||
v-hasPermi="['product:property:update']"
|
||||
link
|
||||
type="primary"
|
||||
@click="openForm('update', scope.row.id)"
|
||||
v-hasPermi="['product:property:update']"
|
||||
>
|
||||
编辑
|
||||
</el-button>
|
||||
@@ -70,10 +77,10 @@
|
||||
<router-link :to="'/property/value/' + scope.row.id">属性值</router-link>
|
||||
</el-button>
|
||||
<el-button
|
||||
v-hasPermi="['product:property:delete']"
|
||||
link
|
||||
type="danger"
|
||||
@click="handleDelete(scope.row.id)"
|
||||
v-hasPermi="['product:property:delete']"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
@@ -82,9 +89,9 @@
|
||||
</el-table>
|
||||
<!-- 分页 -->
|
||||
<Pagination
|
||||
:total="total"
|
||||
v-model:page="queryParams.pageNo"
|
||||
v-model:limit="queryParams.pageSize"
|
||||
v-model:page="queryParams.pageNo"
|
||||
:total="total"
|
||||
@pagination="getList"
|
||||
/>
|
||||
</ContentWrap>
|
||||
@@ -92,10 +99,11 @@
|
||||
<!-- 表单弹窗:添加/修改 -->
|
||||
<PropertyForm ref="formRef" @success="getList" />
|
||||
</template>
|
||||
<script setup lang="ts" name="ProductProperty">
|
||||
<script lang="ts" name="ProductProperty" setup>
|
||||
import { dateFormatter } from '@/utils/formatTime'
|
||||
import * as PropertyApi from '@/api/mall/product/property'
|
||||
import PropertyForm from './PropertyForm.vue'
|
||||
|
||||
const message = useMessage() // 消息弹窗
|
||||
const { t } = useI18n() // 国际化
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<template>
|
||||
<Dialog :title="dialogTitle" v-model="dialogVisible">
|
||||
<Dialog v-model="dialogVisible" :title="dialogTitle">
|
||||
<el-form
|
||||
ref="formRef"
|
||||
v-loading="formLoading"
|
||||
:model="formData"
|
||||
:rules="formRules"
|
||||
label-width="80px"
|
||||
v-loading="formLoading"
|
||||
>
|
||||
<el-form-item label="属性编号" prop="category">
|
||||
<el-input v-model="formData.propertyId" disabled="" />
|
||||
@@ -14,17 +14,18 @@
|
||||
<el-input v-model="formData.name" placeholder="请输入名称" />
|
||||
</el-form-item>
|
||||
<el-form-item label="备注" prop="remark">
|
||||
<el-input v-model="formData.remark" type="textarea" placeholder="请输入内容" />
|
||||
<el-input v-model="formData.remark" placeholder="请输入内容" type="textarea" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button>
|
||||
<el-button :disabled="formLoading" type="primary" @click="submitForm">确 定</el-button>
|
||||
<el-button @click="dialogVisible = false">取 消</el-button>
|
||||
</template>
|
||||
</Dialog>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
<script lang="ts" name="ProductPropertyValueForm" setup>
|
||||
import * as PropertyApi from '@/api/mall/product/property'
|
||||
|
||||
const { t } = useI18n() // 国际化
|
||||
const message = useMessage() // 消息弹窗
|
||||
|
||||
|
||||
240
src/views/mall/product/spu/addForm.vue
Normal file
240
src/views/mall/product/spu/addForm.vue
Normal file
@@ -0,0 +1,240 @@
|
||||
<template>
|
||||
<ContentWrap v-loading="formLoading">
|
||||
<el-tabs v-model="activeName">
|
||||
<el-tab-pane label="商品信息" name="basicInfo">
|
||||
<BasicInfoForm
|
||||
ref="BasicInfoRef"
|
||||
v-model:activeName="activeName"
|
||||
:propFormData="formData"
|
||||
/>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="商品详情" name="description">
|
||||
<DescriptionForm
|
||||
ref="DescriptionRef"
|
||||
v-model:activeName="activeName"
|
||||
:propFormData="formData"
|
||||
/>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="其他设置" name="otherSettings">
|
||||
<OtherSettingsForm
|
||||
ref="OtherSettingsRef"
|
||||
v-model:activeName="activeName"
|
||||
:propFormData="formData"
|
||||
/>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
<el-form>
|
||||
<el-form-item style="float: right">
|
||||
<el-button :loading="formLoading" type="primary" @click="submitForm">保存</el-button>
|
||||
<el-button @click="close">返回</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</ContentWrap>
|
||||
</template>
|
||||
<script lang="ts" name="ProductManagementForm" setup>
|
||||
import { useTagsViewStore } from '@/store/modules/tagsView'
|
||||
import { BasicInfoForm, DescriptionForm, OtherSettingsForm } from './components'
|
||||
import type { SpuType } from '@/api/mall/product/management/type/spuType' // 业务api
|
||||
import * as managementApi from '@/api/mall/product/management/spu'
|
||||
import * as PropertyApi from '@/api/mall/product/property'
|
||||
const { t } = useI18n() // 国际化
|
||||
const message = useMessage() // 消息弹窗
|
||||
const { push, currentRoute } = useRouter() // 路由
|
||||
const { query } = useRoute() // 查询参数
|
||||
const { delView } = useTagsViewStore() // 视图操作
|
||||
|
||||
const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
|
||||
const activeName = ref('basicInfo') // Tag 激活的窗口
|
||||
const BasicInfoRef = ref<ComponentRef<typeof BasicInfoForm>>() // 商品信息Ref
|
||||
const DescriptionRef = ref<ComponentRef<typeof DescriptionForm>>() // 商品详情Ref
|
||||
const OtherSettingsRef = ref<ComponentRef<typeof OtherSettingsForm>>() // 其他设置Ref
|
||||
const formData = ref<SpuType>({
|
||||
name: '213', // 商品名称
|
||||
categoryId: null, // 商品分类
|
||||
keyword: '213', // 关键字
|
||||
unit: null, // 单位
|
||||
picUrl:
|
||||
'http://127.0.0.1:48080/admin-api/infra/file/4/get/6ffdf8f5dfc03f7ceec1ff1ebc138adb8b721a057d505ccfb0e61a8783af1371.png', // 商品封面图
|
||||
sliderPicUrls: [
|
||||
{
|
||||
name: 'http://127.0.0.1:48080/admin-api/infra/file/4/get/6ffdf8f5dfc03f7ceec1ff1ebc138adb8b721a057d505ccfb0e61a8783af1371.png',
|
||||
url: 'http://127.0.0.1:48080/admin-api/infra/file/4/get/6ffdf8f5dfc03f7ceec1ff1ebc138adb8b721a057d505ccfb0e61a8783af1371.png'
|
||||
}
|
||||
], // 商品轮播图
|
||||
introduction: '213', // 商品简介
|
||||
deliveryTemplateId: 0, // 运费模版
|
||||
specType: false, // 商品规格
|
||||
subCommissionType: false, // 分销类型
|
||||
skus: [
|
||||
{
|
||||
/**
|
||||
* 商品价格,单位:分 TODO @puhui999:注释放在尾巴哈,简洁一点~
|
||||
*/
|
||||
price: 0,
|
||||
/**
|
||||
* 市场价,单位:分
|
||||
*/
|
||||
marketPrice: 0,
|
||||
/**
|
||||
* 成本价,单位:分
|
||||
*/
|
||||
costPrice: 0,
|
||||
/**
|
||||
* 商品条码
|
||||
*/
|
||||
barCode: '',
|
||||
/**
|
||||
* 图片地址
|
||||
*/
|
||||
picUrl: '',
|
||||
/**
|
||||
* 库存
|
||||
*/
|
||||
stock: 0,
|
||||
/**
|
||||
* 商品重量,单位:kg 千克
|
||||
*/
|
||||
weight: 0,
|
||||
/**
|
||||
* 商品体积,单位:m^3 平米
|
||||
*/
|
||||
volume: 0,
|
||||
/**
|
||||
* 一级分销的佣金,单位:分
|
||||
*/
|
||||
subCommissionFirstPrice: 0,
|
||||
/**
|
||||
* 二级分销的佣金,单位:分
|
||||
*/
|
||||
subCommissionSecondPrice: 0
|
||||
}
|
||||
],
|
||||
description: '5425', // 商品详情
|
||||
sort: 1, // 商品排序
|
||||
giveIntegral: 1, // 赠送积分
|
||||
virtualSalesCount: 1, // 虚拟销量
|
||||
recommendHot: false, // 是否热卖
|
||||
recommendBenefit: false, // 是否优惠
|
||||
recommendBest: false, // 是否精品
|
||||
recommendNew: false, // 是否新品
|
||||
recommendGood: false // 是否优品
|
||||
})
|
||||
|
||||
/** 获得详情 */
|
||||
const getDetail = async () => {
|
||||
const id = query.id as unknown as number
|
||||
if (id) {
|
||||
formLoading.value = true
|
||||
try {
|
||||
const res = (await managementApi.getSpu(id)) as SpuType
|
||||
formData.value = res
|
||||
// 直接取第一个值就能得到所有属性的id
|
||||
// TODO @puhui999:可以直接拿 propertyName 拼接处规格 id + 属性,可以看下商品 uniapp 详情的做法
|
||||
const propertyIds = res.skus[0]?.properties.map((item) => item.propertyId)
|
||||
const PropertyS = await PropertyApi.getPropertyListAndValue({ propertyIds })
|
||||
await nextTick()
|
||||
// 回显商品属性
|
||||
BasicInfoRef.value.addAttribute(PropertyS)
|
||||
} finally {
|
||||
formLoading.value = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 提交按钮 */
|
||||
const submitForm = async () => {
|
||||
// 提交请求
|
||||
formLoading.value = true
|
||||
const newSkus = JSON.parse(JSON.stringify(formData.value.skus)) //深拷贝一份skus保存失败时使用
|
||||
// TODO 三个表单逐一校验,如果有一个表单校验不通过则切换到对应表单,如果有两个及以上的情况则切换到最前面的一个并弹出提示消息
|
||||
// 校验各表单
|
||||
try {
|
||||
await unref(BasicInfoRef)?.validate()
|
||||
await unref(DescriptionRef)?.validate()
|
||||
await unref(OtherSettingsRef)?.validate()
|
||||
// TODO @puhui:直接做深拷贝?这样最终 server 端不满足,不需要恢复
|
||||
// 处理掉一些无关数据
|
||||
formData.value.skus.forEach((item) => {
|
||||
// 给sku name赋值
|
||||
item.name = formData.value.name
|
||||
// 多规格情况移除skus相关属性值value
|
||||
if (formData.value.specType) {
|
||||
item.properties.forEach((item2) => {
|
||||
delete item2.valueName
|
||||
})
|
||||
}
|
||||
})
|
||||
// 处理轮播图列表
|
||||
const newSliderPicUrls = []
|
||||
formData.value.sliderPicUrls.forEach((item) => {
|
||||
// 如果是前端选的图
|
||||
// TODO @puhui999:疑问哈,为啥会是 object 呀?
|
||||
if (typeof item === 'object') {
|
||||
newSliderPicUrls.push(item.url)
|
||||
} else {
|
||||
newSliderPicUrls.push(item)
|
||||
}
|
||||
})
|
||||
formData.value.sliderPicUrls = newSliderPicUrls
|
||||
// 校验都通过后提交表单
|
||||
const data = formData.value as SpuType
|
||||
// 移除skus.
|
||||
const id = query.id as unknown as number
|
||||
if (!id) {
|
||||
await managementApi.createSpu(data)
|
||||
message.success(t('common.createSuccess'))
|
||||
} else {
|
||||
await managementApi.updateSpu(data)
|
||||
message.success(t('common.updateSuccess'))
|
||||
}
|
||||
close()
|
||||
} catch (e) {
|
||||
// 如果是后端校验失败,恢复skus数据
|
||||
if (typeof e === 'string') {
|
||||
formData.value.skus = newSkus
|
||||
}
|
||||
} finally {
|
||||
formLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置表单
|
||||
*/
|
||||
const resetForm = async () => {
|
||||
formData.value = {
|
||||
name: '', // 商品名称
|
||||
categoryId: 0, // 商品分类
|
||||
keyword: '', // 关键字
|
||||
unit: '', // 单位
|
||||
picUrl: '', // 商品封面图
|
||||
sliderPicUrls: [], // 商品轮播图
|
||||
introduction: '', // 商品简介
|
||||
deliveryTemplateId: 0, // 运费模版
|
||||
selectRule: '',
|
||||
specType: false, // 商品规格
|
||||
subCommissionType: false, // 分销类型
|
||||
description: '', // 商品详情
|
||||
sort: 1, // 商品排序
|
||||
giveIntegral: 1, // 赠送积分
|
||||
virtualSalesCount: 1, // 虚拟销量
|
||||
recommendHot: false, // 是否热卖
|
||||
recommendBenefit: false, // 是否优惠
|
||||
recommendBest: false, // 是否精品
|
||||
recommendNew: false, // 是否新品
|
||||
recommendGood: false // 是否优品
|
||||
}
|
||||
}
|
||||
/** 关闭按钮 */
|
||||
const close = () => {
|
||||
// TODO @puhui999:是不是不用 reset 呀?close 默认销毁
|
||||
resetForm()
|
||||
delView(unref(currentRoute))
|
||||
push('/product/product-management')
|
||||
}
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(() => {
|
||||
getDetail()
|
||||
})
|
||||
</script>
|
||||
238
src/views/mall/product/spu/components/BasicInfoForm.vue
Normal file
238
src/views/mall/product/spu/components/BasicInfoForm.vue
Normal file
@@ -0,0 +1,238 @@
|
||||
<template>
|
||||
<el-form ref="ProductManagementBasicInfoRef" :model="formData" :rules="rules" label-width="120px">
|
||||
<el-row>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="商品名称" prop="name">
|
||||
<el-input v-model="formData.name" placeholder="请输入商品名称" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<!-- TODO @puhui999:只能选根节点 -->
|
||||
<el-form-item label="商品分类" prop="categoryId">
|
||||
<el-tree-select
|
||||
v-model="formData.categoryId"
|
||||
:data="categoryList"
|
||||
:props="defaultProps"
|
||||
check-strictly
|
||||
node-key="id"
|
||||
placeholder="请选择商品分类"
|
||||
class="w-1/1"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="商品关键字" prop="keyword">
|
||||
<el-input v-model="formData.keyword" placeholder="请输入商品关键字" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="单位" prop="unit">
|
||||
<el-select v-model="formData.unit" placeholder="请选择单位" class="w-1/1">
|
||||
<el-option
|
||||
v-for="dict in getIntDictOptions(DICT_TYPE.PRODUCT_UNIT)"
|
||||
:key="dict.value"
|
||||
:label="dict.label"
|
||||
:value="dict.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="商品简介" prop="introduction">
|
||||
<el-input
|
||||
v-model="formData.introduction"
|
||||
:rows="3"
|
||||
placeholder="请输入商品简介"
|
||||
type="textarea"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="商品封面图" prop="picUrl">
|
||||
<UploadImg v-model="formData.picUrl" height="80px" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="24">
|
||||
<el-form-item label="商品轮播图" prop="sliderPicUrls">
|
||||
<UploadImgs v-model="formData.sliderPicUrls" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="运费模板" prop="deliveryTemplateId">
|
||||
<el-select v-model="formData.deliveryTemplateId" placeholder="请选择" class="w-1/1">
|
||||
<el-option v-for="item in []" :key="item.id" :label="item.name" :value="item.id" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-button class="ml-20px">运费模板</el-button>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="商品规格" props="specType">
|
||||
<el-radio-group v-model="formData.specType" @change="onChangeSpec">
|
||||
<el-radio :label="false" class="radio">单规格</el-radio>
|
||||
<el-radio :label="true">多规格</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="分销类型" props="subCommissionType">
|
||||
<el-radio-group v-model="formData.subCommissionType" @change="changeSubCommissionType">
|
||||
<el-radio :label="false">默认设置</el-radio>
|
||||
<el-radio :label="true" class="radio">自行设置</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<!-- 多规格添加-->
|
||||
<el-col :span="24">
|
||||
<el-form-item v-if="formData.specType" label="商品属性">
|
||||
<!-- TODO @puhui999:参考 https://admin.java.crmeb.net/store/list/creatProduct 添加规格好做么?添加的时候,不用输入备注哈 -->
|
||||
<el-button class="mr-15px mb-10px" @click="AttributesAddFormRef.open">添加规格</el-button>
|
||||
<ProductAttributes :attribute-data="attributeList" />
|
||||
</el-form-item>
|
||||
<template v-if="formData.specType && attributeList.length > 0">
|
||||
<el-form-item label="批量设置">
|
||||
<SkuList :attributeList="attributeList" :is-batch="true" :prop-form-data="formData" />
|
||||
</el-form-item>
|
||||
<el-form-item label="属性列表">
|
||||
<SkuList :attributeList="attributeList" :prop-form-data="formData" />
|
||||
</el-form-item>
|
||||
</template>
|
||||
<el-form-item v-if="!formData.specType">
|
||||
<SkuList :attributeList="attributeList" :prop-form-data="formData" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-form>
|
||||
<ProductAttributesAddForm ref="AttributesAddFormRef" @success="addAttribute" />
|
||||
</template>
|
||||
<script lang="ts" name="ProductManagementBasicInfoForm" setup>
|
||||
import { PropType } from 'vue'
|
||||
import { defaultProps, handleTree } from '@/utils/tree'
|
||||
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
|
||||
import type { SpuType } from '@/api/mall/product/management/type/spuType'
|
||||
import { UploadImg, UploadImgs } from '@/components/UploadFile'
|
||||
import { copyValueToTarget } from '@/utils/object'
|
||||
import { ProductAttributes, ProductAttributesAddForm, SkuList } from './index'
|
||||
import * as ProductCategoryApi from '@/api/mall/product/category'
|
||||
import { propTypes } from '@/utils/propTypes'
|
||||
const message = useMessage() // 消息弹窗
|
||||
|
||||
const props = defineProps({
|
||||
propFormData: {
|
||||
type: Object as PropType<SpuType>,
|
||||
default: () => {}
|
||||
},
|
||||
activeName: propTypes.string.def('')
|
||||
})
|
||||
const AttributesAddFormRef = ref() // 添加商品属性表单 TODO @puhui999:小写开头哈
|
||||
const ProductManagementBasicInfoRef = ref() // 表单Ref TODO @puhui999:小写开头哈
|
||||
// TODO @puhui999:attributeList 改成 propertyList,会更统一一点
|
||||
const attributeList = ref([]) // 商品属性列表
|
||||
/** 添加商品属性 */ // TODO @puhui999:propFormData 算出来
|
||||
const addAttribute = (property: any) => {
|
||||
if (Array.isArray(property)) {
|
||||
attributeList.value = property
|
||||
return
|
||||
}
|
||||
attributeList.value.push(property)
|
||||
}
|
||||
const formData = reactive<SpuType>({
|
||||
name: '', // 商品名称
|
||||
categoryId: undefined, // 商品分类
|
||||
keyword: '', // 关键字
|
||||
unit: '', // 单位
|
||||
picUrl: '', // 商品封面图
|
||||
sliderPicUrls: [], // 商品轮播图
|
||||
introduction: '', // 商品简介
|
||||
deliveryTemplateId: 1, // 运费模版
|
||||
specType: false, // 商品规格
|
||||
subCommissionType: false, // 分销类型
|
||||
skus: []
|
||||
})
|
||||
const rules = reactive({
|
||||
name: [required],
|
||||
categoryId: [required],
|
||||
keyword: [required],
|
||||
unit: [required],
|
||||
introduction: [required],
|
||||
picUrl: [required],
|
||||
sliderPicUrls: [required],
|
||||
// deliveryTemplateId: [required],
|
||||
specType: [required],
|
||||
subCommissionType: [required]
|
||||
})
|
||||
|
||||
/**
|
||||
* 将传进来的值赋值给 formData
|
||||
*/
|
||||
watch(
|
||||
() => props.propFormData,
|
||||
(data) => {
|
||||
if (!data) return
|
||||
copyValueToTarget(formData, data)
|
||||
},
|
||||
{
|
||||
deep: true,
|
||||
immediate: true
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* 表单校验
|
||||
*/
|
||||
const emit = defineEmits(['update:activeName'])
|
||||
const validate = async () => {
|
||||
// 校验表单
|
||||
if (!ProductManagementBasicInfoRef) return
|
||||
return await unref(ProductManagementBasicInfoRef).validate((valid) => {
|
||||
if (!valid) {
|
||||
message.warning('商品信息未完善!!')
|
||||
emit('update:activeName', 'basicInfo')
|
||||
// 目的截断之后的校验
|
||||
throw new Error('商品信息未完善!!')
|
||||
} else {
|
||||
// 校验通过更新数据
|
||||
Object.assign(props.propFormData, formData)
|
||||
}
|
||||
})
|
||||
}
|
||||
defineExpose({ validate, addAttribute })
|
||||
|
||||
/** 分销类型 */
|
||||
const changeSubCommissionType = () => {
|
||||
// 默认为零,类型切换后也要重置为零
|
||||
for (const item of formData.skus) {
|
||||
item.subCommissionFirstPrice = 0
|
||||
item.subCommissionSecondPrice = 0
|
||||
}
|
||||
}
|
||||
|
||||
/** 选择规格 */
|
||||
const onChangeSpec = () => {
|
||||
// 重置商品属性列表
|
||||
attributeList.value = []
|
||||
// 重置sku列表
|
||||
formData.skus = [
|
||||
{
|
||||
price: 0,
|
||||
marketPrice: 0,
|
||||
costPrice: 0,
|
||||
barCode: '',
|
||||
picUrl: '',
|
||||
stock: 0,
|
||||
weight: 0,
|
||||
volume: 0,
|
||||
subCommissionFirstPrice: 0,
|
||||
subCommissionSecondPrice: 0
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
const categoryList = ref() // 分类树
|
||||
onMounted(async () => {
|
||||
// 获得分类树
|
||||
const data = await ProductCategoryApi.getCategoryList({})
|
||||
categoryList.value = handleTree(data, 'id', 'parentId')
|
||||
})
|
||||
</script>
|
||||
84
src/views/mall/product/spu/components/DescriptionForm.vue
Normal file
84
src/views/mall/product/spu/components/DescriptionForm.vue
Normal file
@@ -0,0 +1,84 @@
|
||||
<template>
|
||||
<el-form ref="DescriptionFormRef" :model="formData" :rules="rules" label-width="120px">
|
||||
<!--富文本编辑器组件-->
|
||||
<el-form-item label="商品详情" prop="description">
|
||||
<Editor v-model:modelValue="formData.description" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</template>
|
||||
<script lang="ts" name="DescriptionForm" setup>
|
||||
import type { SpuType } from '@/api/mall/product/management/type/spuType'
|
||||
import { Editor } from '@/components/Editor'
|
||||
import { PropType } from 'vue'
|
||||
import { copyValueToTarget } from '@/utils/object'
|
||||
import { propTypes } from '@/utils/propTypes'
|
||||
|
||||
const message = useMessage() // 消息弹窗
|
||||
const props = defineProps({
|
||||
propFormData: {
|
||||
type: Object as PropType<SpuType>,
|
||||
default: () => {}
|
||||
},
|
||||
activeName: propTypes.string.def('')
|
||||
})
|
||||
const DescriptionFormRef = ref() // 表单Ref
|
||||
const formData = ref<SpuType>({
|
||||
description: '' // 商品详情
|
||||
})
|
||||
// 表单规则
|
||||
const rules = reactive({
|
||||
description: [required]
|
||||
})
|
||||
|
||||
/**
|
||||
* 富文本编辑器如果输入过再清空会有残留,需再重置一次
|
||||
*/
|
||||
watch(
|
||||
() => formData.value.description,
|
||||
(newValue) => {
|
||||
if ('<p><br></p>' === newValue) {
|
||||
formData.value.description = ''
|
||||
}
|
||||
},
|
||||
{
|
||||
deep: true,
|
||||
immediate: true
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* 将传进来的值赋值给formData
|
||||
*/
|
||||
watch(
|
||||
() => props.propFormData,
|
||||
(data) => {
|
||||
if (!data) return
|
||||
copyValueToTarget(formData.value, data)
|
||||
},
|
||||
{
|
||||
deep: true,
|
||||
immediate: true
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* 表单校验
|
||||
*/
|
||||
const emit = defineEmits(['update:activeName'])
|
||||
const validate = async () => {
|
||||
// 校验表单
|
||||
if (!DescriptionFormRef) return
|
||||
return unref(DescriptionFormRef).validate((valid) => {
|
||||
if (!valid) {
|
||||
message.warning('商品详情为完善!!')
|
||||
emit('update:activeName', 'description')
|
||||
// 目的截断之后的校验
|
||||
throw new Error('商品详情为完善!!')
|
||||
} else {
|
||||
// 校验通过更新数据
|
||||
Object.assign(props.propFormData, formData.value)
|
||||
}
|
||||
})
|
||||
}
|
||||
defineExpose({ validate })
|
||||
</script>
|
||||
156
src/views/mall/product/spu/components/OtherSettingsForm.vue
Normal file
156
src/views/mall/product/spu/components/OtherSettingsForm.vue
Normal file
@@ -0,0 +1,156 @@
|
||||
<template>
|
||||
<el-form ref="OtherSettingsFormRef" :model="formData" :rules="rules" label-width="120px">
|
||||
<el-row>
|
||||
<!-- TODO @puhui999:横着三个哈 -->
|
||||
<el-col :span="24">
|
||||
<el-col :span="8">
|
||||
<el-form-item label="商品排序" prop="sort">
|
||||
<el-input-number v-model="formData.sort" :min="0" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="赠送积分" prop="giveIntegral">
|
||||
<el-input-number v-model="formData.giveIntegral" :min="0" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="虚拟销量" prop="virtualSalesCount">
|
||||
<el-input-number
|
||||
v-model="formData.virtualSalesCount"
|
||||
:min="0"
|
||||
placeholder="请输入虚拟销量"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-col>
|
||||
<el-col :span="24">
|
||||
<el-form-item label="商品推荐">
|
||||
<el-checkbox-group v-model="checkboxGroup" @change="onChangeGroup">
|
||||
<el-checkbox v-for="(item, index) in recommend" :key="index" :label="item.value">
|
||||
{{ item.name }}
|
||||
</el-checkbox>
|
||||
</el-checkbox-group>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="24">
|
||||
<!-- TODO tag展示暂时不考虑排序 -->
|
||||
<el-form-item label="活动优先级">
|
||||
<el-tag>默认</el-tag>
|
||||
<el-tag class="ml-2" type="success">秒杀</el-tag>
|
||||
<el-tag class="ml-2" type="info">砍价</el-tag>
|
||||
<el-tag class="ml-2" type="warning">拼团</el-tag>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<!-- TODO @puhui999:等优惠劵 ok 在搞 -->
|
||||
<el-col :span="24">
|
||||
<el-form-item label="赠送优惠劵">
|
||||
<el-button>选择优惠券</el-button>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-form>
|
||||
</template>
|
||||
<script lang="ts" name="OtherSettingsForm" setup>
|
||||
import type { SpuType } from '@/api/mall/product/management/type/spuType'
|
||||
import { PropType } from 'vue'
|
||||
import { copyValueToTarget } from '@/utils/object'
|
||||
import { propTypes } from '@/utils/propTypes'
|
||||
const message = useMessage() // 消息弹窗
|
||||
|
||||
const props = defineProps({
|
||||
propFormData: {
|
||||
type: Object as PropType<SpuType>,
|
||||
default: () => {}
|
||||
},
|
||||
activeName: propTypes.string.def('')
|
||||
})
|
||||
// 商品推荐选项 TODO @puhui999:这种叫 recommendOptions 会更合适哈
|
||||
const recommend = [
|
||||
{ name: '是否热卖', value: 'recommendHot' },
|
||||
{ name: '是否优惠', value: 'recommendBenefit' },
|
||||
{ name: '是否精品', value: 'recommendBest' },
|
||||
{ name: '是否新品', value: 'recommendNew' },
|
||||
{ name: '是否优品', value: 'recommendGood' }
|
||||
]
|
||||
const checkboxGroup = ref<string[]>(['recommendHot']) // 选中推荐选项
|
||||
/** 选择商品后赋值 */
|
||||
const onChangeGroup = () => {
|
||||
// TODO @puhui999:是不是可以遍历 recommend,然后进行是否选中;
|
||||
checkboxGroup.value.includes('recommendHot')
|
||||
? (formData.value.recommendHot = true)
|
||||
: (formData.value.recommendHot = false)
|
||||
checkboxGroup.value.includes('recommendBenefit')
|
||||
? (formData.value.recommendBenefit = true)
|
||||
: (formData.value.recommendBenefit = false)
|
||||
checkboxGroup.value.includes('recommendBest')
|
||||
? (formData.value.recommendBest = true)
|
||||
: (formData.value.recommendBest = false)
|
||||
checkboxGroup.value.includes('recommendNew')
|
||||
? (formData.value.recommendNew = true)
|
||||
: (formData.value.recommendNew = false)
|
||||
checkboxGroup.value.includes('recommendGood')
|
||||
? (formData.value.recommendGood = true)
|
||||
: (formData.value.recommendGood = false)
|
||||
}
|
||||
const OtherSettingsFormRef = ref() // 表单Ref
|
||||
// 表单数据
|
||||
const formData = ref<SpuType>({
|
||||
sort: 1, // 商品排序
|
||||
giveIntegral: 1, // 赠送积分
|
||||
virtualSalesCount: 1, // 虚拟销量
|
||||
recommendHot: false, // 是否热卖
|
||||
recommendBenefit: false, // 是否优惠
|
||||
recommendBest: false, // 是否精品
|
||||
recommendNew: false, // 是否新品
|
||||
recommendGood: false // 是否优品
|
||||
})
|
||||
// 表单规则
|
||||
const rules = reactive({
|
||||
sort: [required],
|
||||
giveIntegral: [required],
|
||||
virtualSalesCount: [required]
|
||||
})
|
||||
|
||||
/**
|
||||
* 将传进来的值赋值给formData
|
||||
*/
|
||||
watch(
|
||||
() => props.propFormData,
|
||||
(data) => {
|
||||
if (!data) return
|
||||
copyValueToTarget(formData.value, data)
|
||||
// TODO 如果先修改其他设置的值,再改变商品详情或是商品信息会重置其他设置页面中的相关值 下一个版本修复
|
||||
checkboxGroup.value = []
|
||||
formData.value.recommendHot ? checkboxGroup.value.push('recommendHot') : ''
|
||||
formData.value.recommendBenefit ? checkboxGroup.value.push('recommendBenefit') : ''
|
||||
formData.value.recommendBest ? checkboxGroup.value.push('recommendBest') : ''
|
||||
formData.value.recommendNew ? checkboxGroup.value.push('recommendNew') : ''
|
||||
formData.value.recommendGood ? checkboxGroup.value.push('recommendGood') : ''
|
||||
},
|
||||
{
|
||||
deep: true,
|
||||
immediate: true
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* 表单校验
|
||||
*/
|
||||
const emit = defineEmits(['update:activeName'])
|
||||
const validate = async () => {
|
||||
// 校验表单
|
||||
if (!OtherSettingsFormRef) return
|
||||
return await unref(OtherSettingsFormRef).validate((valid) => {
|
||||
if (!valid) {
|
||||
message.warning('商品其他设置未完善!!')
|
||||
emit('update:activeName', 'otherSettings')
|
||||
// 目的截断之后的校验
|
||||
throw new Error('商品其他设置未完善!!')
|
||||
} else {
|
||||
// 校验通过更新数据
|
||||
Object.assign(props.propFormData, formData.value)
|
||||
}
|
||||
})
|
||||
}
|
||||
defineExpose({ validate })
|
||||
</script>
|
||||
102
src/views/mall/product/spu/components/ProductAttributes.vue
Normal file
102
src/views/mall/product/spu/components/ProductAttributes.vue
Normal file
@@ -0,0 +1,102 @@
|
||||
<template>
|
||||
<el-col v-for="(item, index) in attributeList" :key="index">
|
||||
<div>
|
||||
<el-text class="mx-1">属性名:</el-text>
|
||||
<el-text class="mx-1">{{ item.name }}</el-text>
|
||||
</div>
|
||||
<div>
|
||||
<el-text class="mx-1">属性值:</el-text>
|
||||
<el-tag
|
||||
v-for="(value, valueIndex) in item.values"
|
||||
:key="value.id"
|
||||
:disable-transitions="false"
|
||||
class="mx-1"
|
||||
closable
|
||||
@close="handleClose(index, valueIndex)"
|
||||
>
|
||||
{{ value.name }}
|
||||
</el-tag>
|
||||
<el-input
|
||||
v-show="inputVisible(index)"
|
||||
ref="InputRef"
|
||||
v-model="inputValue"
|
||||
class="!w-20"
|
||||
size="small"
|
||||
@blur="handleInputConfirm(index, item.id)"
|
||||
@keyup.enter="handleInputConfirm(index, item.id)"
|
||||
/>
|
||||
<el-button
|
||||
v-show="!inputVisible(index)"
|
||||
class="button-new-tag ml-1"
|
||||
size="small"
|
||||
@click="showInput(index)"
|
||||
>
|
||||
+ 添加
|
||||
</el-button>
|
||||
</div>
|
||||
<el-divider class="my-10px" />
|
||||
</el-col>
|
||||
</template>
|
||||
|
||||
<script lang="ts" name="ProductAttributes" setup>
|
||||
import { ElInput } from 'element-plus'
|
||||
import * as PropertyApi from '@/api/mall/product/property'
|
||||
|
||||
const { t } = useI18n() // 国际化
|
||||
const message = useMessage() // 消息弹窗
|
||||
const inputValue = ref('') // 输入框值
|
||||
const attributeIndex = ref<number | null>(null) // 获取焦点时记录当前属性项的index
|
||||
// 输入框显隐控制
|
||||
const inputVisible = computed(() => (index) => {
|
||||
if (attributeIndex.value === null) return false
|
||||
if (attributeIndex.value === index) return true
|
||||
})
|
||||
const InputRef = ref() //标签输入框Ref
|
||||
const attributeList = ref([]) // 商品属性列表
|
||||
const props = defineProps({
|
||||
attributeData: {
|
||||
type: Array,
|
||||
default: () => {}
|
||||
}
|
||||
})
|
||||
|
||||
watch(
|
||||
() => props.attributeData,
|
||||
(data) => {
|
||||
if (!data) return
|
||||
attributeList.value = data
|
||||
},
|
||||
{
|
||||
deep: true,
|
||||
immediate: true
|
||||
}
|
||||
)
|
||||
|
||||
/** 删除标签 tagValue 标签值*/
|
||||
const handleClose = (index, valueIndex) => {
|
||||
attributeList.value[index].values?.splice(valueIndex, 1)
|
||||
}
|
||||
|
||||
/** 显示输入框并获取焦点 */
|
||||
const showInput = async (index) => {
|
||||
attributeIndex.value = index
|
||||
// 因为组件在ref中所以需要用索引获取对应的Ref
|
||||
InputRef.value[index]!.input!.focus()
|
||||
}
|
||||
|
||||
/** 输入框失去焦点或点击回车时触发 */
|
||||
const handleInputConfirm = async (index, propertyId) => {
|
||||
if (inputValue.value) {
|
||||
// 保存属性值
|
||||
try {
|
||||
const id = await PropertyApi.createPropertyValue({ propertyId, name: inputValue.value })
|
||||
attributeList.value[index].values.push({ id, name: inputValue.value })
|
||||
message.success(t('common.createSuccess'))
|
||||
} catch {
|
||||
message.error('添加失败,请重试') // TODO 缺少国际化
|
||||
}
|
||||
}
|
||||
attributeIndex.value = null
|
||||
inputValue.value = ''
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,85 @@
|
||||
<template>
|
||||
<Dialog v-model="dialogVisible" :title="dialogTitle">
|
||||
<el-form
|
||||
ref="formRef"
|
||||
v-loading="formLoading"
|
||||
:model="formData"
|
||||
:rules="formRules"
|
||||
label-width="80px"
|
||||
>
|
||||
<el-form-item label="名称" prop="name">
|
||||
<el-input v-model="formData.name" placeholder="请输入名称" />
|
||||
</el-form-item>
|
||||
<el-form-item label="备注" prop="remark">
|
||||
<el-input v-model="formData.remark" placeholder="请输入内容" type="textarea" />
|
||||
</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="ProductPropertyForm" setup>
|
||||
import * as PropertyApi from '@/api/mall/product/property'
|
||||
|
||||
const { t } = useI18n() // 国际化
|
||||
const message = useMessage() // 消息弹窗
|
||||
|
||||
const dialogVisible = ref(false) // 弹窗的是否展示
|
||||
const dialogTitle = ref('添加商品属性') // 弹窗的标题
|
||||
const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
|
||||
const formData = ref({
|
||||
name: '',
|
||||
remark: ''
|
||||
})
|
||||
const formRules = reactive({
|
||||
name: [{ required: true, message: '名称不能为空', trigger: 'blur' }]
|
||||
})
|
||||
const formRef = ref() // 表单 Ref
|
||||
|
||||
/** 打开弹窗 */
|
||||
const open = async () => {
|
||||
dialogVisible.value = true
|
||||
resetForm()
|
||||
}
|
||||
defineExpose({ open }) // 提供 open 方法,用于打开弹窗
|
||||
|
||||
/** 提交表单 */
|
||||
const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
|
||||
const submitForm = async () => {
|
||||
// 校验表单
|
||||
if (!formRef) return
|
||||
const valid = await formRef.value.validate()
|
||||
if (!valid) return
|
||||
// 提交请求
|
||||
formLoading.value = true
|
||||
try {
|
||||
const data = formData.value as PropertyApi.PropertyVO
|
||||
// 检查属性是否已存在,如果有则返回属性和其下属性值
|
||||
const res = await PropertyApi.getPropertyListAndValue({ name: data.name })
|
||||
if (res.length === 0) {
|
||||
const propertyId = await PropertyApi.createProperty(data)
|
||||
emit('success', { id: propertyId, ...formData.value, values: [] })
|
||||
} else {
|
||||
if (res[0].values === null) {
|
||||
res[0].values = []
|
||||
}
|
||||
emit('success', res[0]) // 因为只用一个
|
||||
}
|
||||
message.success(t('common.createSuccess'))
|
||||
dialogVisible.value = false
|
||||
} finally {
|
||||
formLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 重置表单 */
|
||||
const resetForm = () => {
|
||||
formData.value = {
|
||||
name: '',
|
||||
remark: ''
|
||||
}
|
||||
formRef.value?.resetFields()
|
||||
}
|
||||
</script>
|
||||
309
src/views/mall/product/spu/components/SkuList.vue
Normal file
309
src/views/mall/product/spu/components/SkuList.vue
Normal file
@@ -0,0 +1,309 @@
|
||||
<template>
|
||||
<el-table
|
||||
:data="isBatch ? SkuData : formData.skus"
|
||||
border
|
||||
class="tabNumWidth"
|
||||
max-height="500"
|
||||
size="small"
|
||||
>
|
||||
<el-table-column align="center" fixed="left" label="图片" min-width="100">
|
||||
<template #default="{ row }">
|
||||
<UploadImg v-model="row.picUrl" height="80px" width="100%" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<template v-if="formData.specType && !isBatch">
|
||||
<!-- 根据商品属性动态添加 -->
|
||||
<el-table-column
|
||||
v-for="(item, index) in tableHeaderList"
|
||||
:key="index"
|
||||
:label="item.label"
|
||||
align="center"
|
||||
min-width="120"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
{{ row.properties[index]?.valueName }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
</template>
|
||||
<!-- TODO @puhui999: controls-position="right" 可以去掉哈,不然太长了,手动输入更方便 -->
|
||||
<el-table-column align="center" label="商品条码" min-width="168">
|
||||
<template #default="{ row }">
|
||||
<el-input v-model="row.barCode" class="w-100%" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<!-- TODO @puhui999:用户输入的时候,是按照元;分主要是我们自己用; -->
|
||||
<el-table-column align="center" label="销售价(分)" min-width="168">
|
||||
<template #default="{ row }">
|
||||
<el-input-number v-model="row.price" :min="0" class="w-100%" controls-position="right" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="center" label="市场价(分)" min-width="168">
|
||||
<template #default="{ row }">
|
||||
<el-input-number
|
||||
v-model="row.marketPrice"
|
||||
:min="0"
|
||||
class="w-100%"
|
||||
controls-position="right"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="center" label="成本价(分)" min-width="168">
|
||||
<template #default="{ row }">
|
||||
<el-input-number
|
||||
v-model="row.costPrice"
|
||||
:min="0"
|
||||
class="w-100%"
|
||||
controls-position="right"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="center" label="库存" min-width="168">
|
||||
<template #default="{ row }">
|
||||
<el-input-number v-model="row.stock" :min="0" class="w-100%" controls-position="right" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="center" label="重量(kg)" min-width="168">
|
||||
<template #default="{ row }">
|
||||
<el-input-number v-model="row.weight" :min="0" class="w-100%" controls-position="right" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="center" label="体积(m^3)" min-width="168">
|
||||
<template #default="{ row }">
|
||||
<el-input-number v-model="row.volume" :min="0" class="w-100%" controls-position="right" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<template v-if="formData.subCommissionType">
|
||||
<el-table-column align="center" label="一级返佣(分)" min-width="168">
|
||||
<template #default="{ row }">
|
||||
<el-input-number
|
||||
v-model="row.subCommissionFirstPrice"
|
||||
:min="0"
|
||||
class="w-100%"
|
||||
controls-position="right"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="center" label="二级返佣(分)" min-width="168">
|
||||
<template #default="{ row }">
|
||||
<el-input-number
|
||||
v-model="row.subCommissionSecondPrice"
|
||||
:min="0"
|
||||
class="w-100%"
|
||||
controls-position="right"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</template>
|
||||
<el-table-column v-if="formData.specType" align="center" fixed="right" label="操作" width="80">
|
||||
<template #default>
|
||||
<el-button v-if="isBatch" link size="small" type="primary" @click="batchAdd">
|
||||
批量添加
|
||||
</el-button>
|
||||
<el-button v-else link size="small" type="primary">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</template>
|
||||
<script lang="ts" name="SkuList" setup>
|
||||
import { UploadImg } from '@/components/UploadFile'
|
||||
import { PropType } from 'vue'
|
||||
import { SpuType } from '@/api/mall/product/management/type/spuType'
|
||||
import { propTypes } from '@/utils/propTypes'
|
||||
import { SkuType } from '@/api/mall/product/management/type/skuType'
|
||||
import { copyValueToTarget } from '@/utils/object'
|
||||
|
||||
const props = defineProps({
|
||||
propFormData: {
|
||||
type: Object as PropType<SpuType>,
|
||||
default: () => {}
|
||||
},
|
||||
attributeList: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
isBatch: propTypes.bool.def(false) // 是否批量操作
|
||||
})
|
||||
const formData = ref<SpuType>() // 表单数据
|
||||
// 批量添加时的零时数据 TODO @puhui999:小写开头哈;然后变量都尾注释
|
||||
const SkuData = ref<SkuType[]>([
|
||||
{
|
||||
/**
|
||||
* 商品价格,单位:分
|
||||
*/
|
||||
price: 0,
|
||||
/**
|
||||
* 市场价,单位:分
|
||||
*/
|
||||
marketPrice: 0,
|
||||
/**
|
||||
* 成本价,单位:分
|
||||
*/
|
||||
costPrice: 0,
|
||||
/**
|
||||
* 商品条码
|
||||
*/
|
||||
barCode: '',
|
||||
/**
|
||||
* 图片地址
|
||||
*/
|
||||
picUrl: '',
|
||||
/**
|
||||
* 库存
|
||||
*/
|
||||
stock: 0,
|
||||
/**
|
||||
* 商品重量,单位:kg 千克
|
||||
*/
|
||||
weight: 0,
|
||||
/**
|
||||
* 商品体积,单位:m^3 平米
|
||||
*/
|
||||
volume: 0,
|
||||
/**
|
||||
* 一级分销的佣金,单位:分
|
||||
*/
|
||||
subCommissionFirstPrice: 0,
|
||||
/**
|
||||
* 二级分销的佣金,单位:分
|
||||
*/
|
||||
subCommissionSecondPrice: 0
|
||||
}
|
||||
])
|
||||
|
||||
/** 批量添加 */
|
||||
const batchAdd = () => {
|
||||
formData.value.skus.forEach((item) => {
|
||||
copyValueToTarget(item, SkuData.value[0])
|
||||
})
|
||||
}
|
||||
|
||||
const tableHeaderList = ref<{ prop: string; label: string }[]>([])
|
||||
|
||||
/**
|
||||
* 将传进来的值赋值给SkuData
|
||||
*/
|
||||
watch(
|
||||
() => props.propFormData,
|
||||
(data) => {
|
||||
if (!data) return
|
||||
formData.value = data
|
||||
},
|
||||
{
|
||||
deep: true,
|
||||
immediate: true
|
||||
}
|
||||
)
|
||||
|
||||
// TODO @芋艿:看看 chatgpt 可以进一步下面几个方法的实现不
|
||||
/** 生成表数据 */
|
||||
const generateTableData = (data: any[]) => {
|
||||
// 构建数据结构
|
||||
const propertiesItemList = []
|
||||
for (const item of data) {
|
||||
const objList = []
|
||||
for (const v of item.values) {
|
||||
const obj = { propertyId: 0, valueId: 0, valueName: '' }
|
||||
obj.propertyId = item.id
|
||||
obj.valueId = v.id
|
||||
obj.valueName = v.name
|
||||
objList.push(obj)
|
||||
}
|
||||
propertiesItemList.push(objList)
|
||||
}
|
||||
const buildList = build(propertiesItemList)
|
||||
// 如果构建后的组合数跟sku数量一样的话则不用处理,添加新属性没有属性值也不做处理 (解决编辑表单时或查看详情时数据回显问题)
|
||||
if (
|
||||
buildList.length === formData.value.skus.length ||
|
||||
data.some((item) => item.values.length === 0)
|
||||
) {
|
||||
return
|
||||
}
|
||||
// 重置表数据
|
||||
formData.value!.skus = []
|
||||
buildList.forEach((item) => {
|
||||
const row = {
|
||||
properties: [],
|
||||
price: 0,
|
||||
marketPrice: 0,
|
||||
costPrice: 0,
|
||||
barCode: '',
|
||||
picUrl: '',
|
||||
stock: 0,
|
||||
weight: 0,
|
||||
volume: 0,
|
||||
subCommissionFirstPrice: 0,
|
||||
subCommissionSecondPrice: 0
|
||||
}
|
||||
// 判断是否是单一属性的情况
|
||||
if (Array.isArray(item)) {
|
||||
row.properties = item
|
||||
} else {
|
||||
row.properties.push(item)
|
||||
}
|
||||
formData.value.skus.push(row)
|
||||
})
|
||||
}
|
||||
|
||||
/** 构建所有排列组合 */
|
||||
const build = (list: any[]) => {
|
||||
if (list.length === 0) {
|
||||
return []
|
||||
} else if (list.length === 1) {
|
||||
return list[0]
|
||||
} else {
|
||||
const result = []
|
||||
const rest = build(list.slice(1))
|
||||
for (let i = 0; i < list[0].length; i++) {
|
||||
for (let j = 0; j < rest.length; j++) {
|
||||
// 第一次不是数组结构,后面的都是数组结构
|
||||
if (Array.isArray(rest[j])) {
|
||||
result.push([list[0][i], ...rest[j]])
|
||||
} else {
|
||||
result.push([list[0][i], rest[j]])
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
/** 监听属性列表生成相关参数和表头 */
|
||||
watch(
|
||||
() => props.attributeList,
|
||||
(data) => {
|
||||
// 如果不是多规格则结束
|
||||
if (!formData.value.specType) return
|
||||
// 如果当前组件作为批量添加数据使用则重置表数据
|
||||
if (props.isBatch) {
|
||||
SkuData.value = [
|
||||
{
|
||||
price: 0,
|
||||
marketPrice: 0,
|
||||
costPrice: 0,
|
||||
barCode: '',
|
||||
picUrl: '',
|
||||
stock: 0,
|
||||
weight: 0,
|
||||
volume: 0,
|
||||
subCommissionFirstPrice: 0,
|
||||
subCommissionSecondPrice: 0
|
||||
}
|
||||
]
|
||||
}
|
||||
// 判断代理对象是否为空
|
||||
if (JSON.stringify(data) === '[]') return
|
||||
// 重置表头
|
||||
tableHeaderList.value = []
|
||||
// 生成表头
|
||||
data.forEach((item, index) => {
|
||||
// name加属性项index区分属性值
|
||||
tableHeaderList.value.push({ prop: `name${index}`, label: item.name })
|
||||
})
|
||||
generateTableData(data)
|
||||
},
|
||||
{
|
||||
deep: true,
|
||||
immediate: true
|
||||
}
|
||||
)
|
||||
</script>
|
||||
15
src/views/mall/product/spu/components/index.ts
Normal file
15
src/views/mall/product/spu/components/index.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import BasicInfoForm from './BasicInfoForm.vue'
|
||||
import DescriptionForm from './DescriptionForm.vue'
|
||||
import OtherSettingsForm from './OtherSettingsForm.vue'
|
||||
import ProductAttributes from './ProductAttributes.vue'
|
||||
import ProductAttributesAddForm from './ProductAttributesAddForm.vue'
|
||||
import SkuList from './SkuList.vue'
|
||||
|
||||
export {
|
||||
BasicInfoForm,
|
||||
DescriptionForm,
|
||||
OtherSettingsForm,
|
||||
ProductAttributes,
|
||||
ProductAttributesAddForm,
|
||||
SkuList
|
||||
}
|
||||
388
src/views/mall/product/spu/index.vue
Normal file
388
src/views/mall/product/spu/index.vue
Normal file
@@ -0,0 +1,388 @@
|
||||
<template>
|
||||
<!-- 搜索工作栏 -->
|
||||
<ContentWrap>
|
||||
<el-form
|
||||
ref="queryFormRef"
|
||||
:inline="true"
|
||||
:model="queryParams"
|
||||
class="-mb-15px"
|
||||
label-width="68px"
|
||||
>
|
||||
<!-- TODO @puhui999:https://admin.java.crmeb.net/store/index,参考,使用分类 + 标题搜索 -->
|
||||
<el-form-item label="品牌名称" prop="name">
|
||||
<el-input
|
||||
v-model="queryParams.name"
|
||||
class="!w-240px"
|
||||
clearable
|
||||
placeholder="请输入品牌名称"
|
||||
@keyup.enter="handleQuery"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="状态" prop="status">
|
||||
<el-select v-model="queryParams.status" class="!w-240px" clearable placeholder="请选择状态">
|
||||
<el-option
|
||||
v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
|
||||
:key="dict.value"
|
||||
:label="dict.label"
|
||||
:value="dict.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="创建时间" prop="createTime">
|
||||
<el-date-picker
|
||||
v-model="queryParams.createTime"
|
||||
:default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
|
||||
class="!w-240px"
|
||||
end-placeholder="结束日期"
|
||||
start-placeholder="开始日期"
|
||||
type="daterange"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button @click="handleQuery">
|
||||
<Icon class="mr-5px" icon="ep:search" />
|
||||
搜索
|
||||
</el-button>
|
||||
<el-button @click="resetQuery">
|
||||
<Icon class="mr-5px" icon="ep:refresh" />
|
||||
重置
|
||||
</el-button>
|
||||
<el-button v-hasPermi="['product:brand:create']" plain type="primary" @click="openForm">
|
||||
<Icon class="mr-5px" icon="ep:plus" />
|
||||
新增
|
||||
</el-button>
|
||||
<!-- TODO @puhui999:增加一个【导出】操作 -->
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</ContentWrap>
|
||||
|
||||
<!-- 列表 -->
|
||||
<ContentWrap>
|
||||
<el-tabs v-model="queryParams.tabType" @tab-click="handleClick">
|
||||
<el-tab-pane
|
||||
v-for="item in tabsData"
|
||||
:key="item.type"
|
||||
:label="item.name + '(' + item.count + ')'"
|
||||
:name="item.type"
|
||||
/>
|
||||
</el-tabs>
|
||||
<el-table v-loading="loading" :data="list">
|
||||
<!-- TODO puhui999: ID 编号的展示 -->
|
||||
<!-- TODO 暂时不做折叠数据 -->
|
||||
<!-- <el-table-column type="expand">-->
|
||||
<!-- <template #default="{ row }">-->
|
||||
<!-- <el-form inline label-position="left">-->
|
||||
<!-- <el-form-item label="市场价:">-->
|
||||
<!-- <span>{{ row.marketPrice }}</span>-->
|
||||
<!-- </el-form-item>-->
|
||||
<!-- <el-form-item label="成本价:">-->
|
||||
<!-- <span>{{ row.costPrice }}</span>-->
|
||||
<!-- </el-form-item>-->
|
||||
<!-- <el-form-item label="虚拟销量:">-->
|
||||
<!-- <span>{{ row.virtualSalesCount }}</span>-->
|
||||
<!-- </el-form-item>-->
|
||||
<!-- </el-form>-->
|
||||
<!-- </template>-->
|
||||
<!-- </el-table-column>-->
|
||||
<el-table-column label="商品图" min-width="80">
|
||||
<template #default="{ row }">
|
||||
<el-image
|
||||
:src="row.picUrl"
|
||||
style="width: 36px; height: 36px"
|
||||
@click="imagePreview(row.picUrl)"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :show-overflow-tooltip="true" label="商品名称" min-width="300" prop="name" />
|
||||
<!-- TODO 价格 / 100.0 -->
|
||||
<el-table-column align="center" label="商品售价" min-width="90" prop="price" />
|
||||
<el-table-column align="center" label="销量" min-width="90" prop="salesCount" />
|
||||
<el-table-column align="center" label="库存" min-width="90" prop="stock" />
|
||||
<el-table-column align="center" label="排序" min-width="70" prop="sort" />
|
||||
<el-table-column
|
||||
:formatter="dateFormatter"
|
||||
align="center"
|
||||
label="创建时间"
|
||||
prop="createTime"
|
||||
width="180"
|
||||
/>
|
||||
<el-table-column fixed="right" label="状态" min-width="80">
|
||||
<template #default="{ row }">
|
||||
<!-- TODO @puhui:是不是不用 Number(row.status) 去比较哈,直接 row.status < 0 -->
|
||||
<el-switch
|
||||
v-model="row.status"
|
||||
:active-value="1"
|
||||
:disabled="Number(row.status) < 0"
|
||||
:inactive-value="0"
|
||||
active-text="上架"
|
||||
inactive-text="下架"
|
||||
inline-prompt
|
||||
@change="changeStatus(row)"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="center" fixed="right" label="操作" min-width="150">
|
||||
<template #default="{ row }">
|
||||
<!-- TODO @puhui999:【详情】,可以后面点做哈 -->
|
||||
<template v-if="queryParams.tabType === 4">
|
||||
<el-button
|
||||
v-hasPermi="['product:spu:delete']"
|
||||
link
|
||||
type="danger"
|
||||
@click="handleDelete(row.id)"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
<el-button
|
||||
v-hasPermi="['product:spu:update']"
|
||||
link
|
||||
type="primary"
|
||||
@click="addToTrash(row, ProductSpuStatusEnum.DISABLE.status)"
|
||||
>
|
||||
恢复到仓库
|
||||
</el-button>
|
||||
</template>
|
||||
<template v-else>
|
||||
<el-button
|
||||
v-hasPermi="['product:spu:update']"
|
||||
link
|
||||
type="primary"
|
||||
@click="openForm(row.id)"
|
||||
>
|
||||
修改
|
||||
</el-button>
|
||||
<el-button
|
||||
v-hasPermi="['product:spu:update']"
|
||||
link
|
||||
type="primary"
|
||||
@click="addToTrash(row, ProductSpuStatusEnum.RECYCLE.status)"
|
||||
>
|
||||
加入回收站
|
||||
</el-button>
|
||||
</template>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<!-- 分页 -->
|
||||
<Pagination
|
||||
v-model:limit="queryParams.pageSize"
|
||||
v-model:page="queryParams.pageNo"
|
||||
:total="total"
|
||||
@pagination="getList"
|
||||
/>
|
||||
</ContentWrap>
|
||||
<!-- https://kailong110120130.gitee.io/vue-element-plus-admin-doc/components/image-viewer.html,可以用这个么? -->
|
||||
<!-- 必须在表格外面展示。不然单元格会遮挡图层 -->
|
||||
<el-image-viewer
|
||||
v-if="imgViewVisible"
|
||||
:url-list="imageViewerList"
|
||||
@close="imgViewVisible = false"
|
||||
/>
|
||||
</template>
|
||||
<script lang="ts" name="ProductList" setup>
|
||||
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
|
||||
import { dateFormatter } from '@/utils/formatTime'
|
||||
// TODO @puhui999:managementApi=》ProductSpuApi
|
||||
import * as managementApi from '@/api/mall/product/management/spu'
|
||||
import { ProductSpuStatusEnum } from '@/utils/constants'
|
||||
import { TabsPaneContext } from 'element-plus'
|
||||
const message = useMessage() // 消息弹窗
|
||||
const { t } = useI18n() // 国际化
|
||||
const { currentRoute, push } = useRouter() // 路由跳转
|
||||
|
||||
const loading = ref(false) // 列表的加载中
|
||||
const total = ref(0) // 列表的总页数
|
||||
const list = ref<any[]>([]) // 列表的数据
|
||||
// tabs 数据
|
||||
const tabsData = ref([
|
||||
{
|
||||
count: 0,
|
||||
name: '出售中商品',
|
||||
type: 0
|
||||
},
|
||||
{
|
||||
count: 0,
|
||||
name: '仓库中商品',
|
||||
type: 1
|
||||
},
|
||||
{
|
||||
count: 0,
|
||||
name: '已经售空商品',
|
||||
type: 2
|
||||
},
|
||||
{
|
||||
count: 0,
|
||||
name: '警戒库存',
|
||||
type: 3
|
||||
},
|
||||
{
|
||||
count: 0,
|
||||
name: '商品回收站',
|
||||
type: 4
|
||||
}
|
||||
])
|
||||
|
||||
/** 获得每个 Tab 的数量 */
|
||||
const getTabsCount = async () => {
|
||||
// TODO @puhui999:这里是不是可以不要 try catch 哈
|
||||
try {
|
||||
const res = await managementApi.getTabsCount()
|
||||
for (let objName in res) {
|
||||
tabsData.value[Number(objName)].count = res[objName]
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
const imgViewVisible = ref(false) // 商品图预览
|
||||
const imageViewerList = ref<string[]>([]) // 商品图预览列表
|
||||
const queryParams = ref({
|
||||
pageNo: 1,
|
||||
pageSize: 10,
|
||||
tabType: 0
|
||||
})
|
||||
const queryFormRef = ref() // 搜索的表单
|
||||
|
||||
// TODO @puhui999:可以改成 handleTabClick:更准确一点;
|
||||
const handleClick = (tab: TabsPaneContext) => {
|
||||
queryParams.value.tabType = tab.paneName
|
||||
getList()
|
||||
}
|
||||
|
||||
/** 查询列表 */
|
||||
const getList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const data = await managementApi.getSpuList(queryParams.value)
|
||||
list.value = data.list
|
||||
total.value = data.total
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// TODO @puhui999:是不是 changeStatus 和 addToTrash 调用一个统一的方法,去更新状态。这样逻辑会更干净一些。
|
||||
/**
|
||||
* 更改 SPU 状态
|
||||
*
|
||||
* @param row
|
||||
* @param status 更改前的值
|
||||
*/
|
||||
const changeStatus = async (row, status?: number) => {
|
||||
// TODO 测试过程中似乎有点问题,下一版修复
|
||||
try {
|
||||
let text = ''
|
||||
switch (row.status) {
|
||||
case ProductSpuStatusEnum.DISABLE.status:
|
||||
text = ProductSpuStatusEnum.DISABLE.name
|
||||
break
|
||||
case ProductSpuStatusEnum.ENABLE.status:
|
||||
text = ProductSpuStatusEnum.ENABLE.name
|
||||
break
|
||||
case ProductSpuStatusEnum.RECYCLE.status:
|
||||
text = `加入${ProductSpuStatusEnum.RECYCLE.name}`
|
||||
break
|
||||
}
|
||||
await message.confirm(
|
||||
row.status === -1 ? `确认要将[${row.name}]${text}吗?` : `确认要${text}[${row.name}]吗?`
|
||||
)
|
||||
await managementApi.updateStatus({ id: row.id, status: row.status })
|
||||
message.success('更新状态成功')
|
||||
// 刷新 tabs 数据
|
||||
await getTabsCount()
|
||||
// 刷新列表
|
||||
await getList()
|
||||
} catch {
|
||||
// 取消加入回收站时回显数据
|
||||
if (typeof status !== 'undefined') {
|
||||
row.status = status
|
||||
return
|
||||
}
|
||||
// 取消更改状态时回显数据
|
||||
row.status =
|
||||
row.status === ProductSpuStatusEnum.DISABLE.status
|
||||
? ProductSpuStatusEnum.ENABLE.status
|
||||
: ProductSpuStatusEnum.DISABLE.status
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 加入回收站
|
||||
*
|
||||
* @param row
|
||||
* @param status
|
||||
*/
|
||||
const addToTrash = (row, status) => {
|
||||
// 复制一份原值
|
||||
const num = Number(`${row.status}`)
|
||||
row.status = status
|
||||
changeStatus(row, num)
|
||||
}
|
||||
|
||||
/** 删除按钮操作 */
|
||||
const handleDelete = async (id: number) => {
|
||||
try {
|
||||
// 删除的二次确认
|
||||
await message.delConfirm()
|
||||
// 发起删除
|
||||
await managementApi.deleteSpu(id)
|
||||
message.success(t('common.delSuccess'))
|
||||
// 刷新tabs数据
|
||||
await getTabsCount()
|
||||
// 刷新列表
|
||||
await getList()
|
||||
} catch {}
|
||||
}
|
||||
|
||||
/**
|
||||
* 商品图预览
|
||||
* @param imgUrl
|
||||
*/
|
||||
const imagePreview = (imgUrl: string) => {
|
||||
imageViewerList.value = [imgUrl]
|
||||
imgViewVisible.value = true
|
||||
}
|
||||
|
||||
/** 搜索按钮操作 */
|
||||
const handleQuery = () => {
|
||||
getList()
|
||||
}
|
||||
|
||||
/** 重置按钮操作 */
|
||||
const resetQuery = () => {
|
||||
queryFormRef.value.resetFields()
|
||||
handleQuery()
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增或修改
|
||||
*
|
||||
* @param id 商品 SPU 编号
|
||||
*/
|
||||
const openForm = (id?: number) => {
|
||||
// 修改
|
||||
if (typeof id === 'number') {
|
||||
push('/product/productManagementAdd?id=' + id)
|
||||
return
|
||||
}
|
||||
// 新增
|
||||
push('/product/productManagementAdd')
|
||||
}
|
||||
|
||||
// 监听路由变化更新列表 TODO @puhui999:这个是必须加的么?
|
||||
watch(
|
||||
() => currentRoute.value,
|
||||
() => {
|
||||
getList()
|
||||
},
|
||||
{
|
||||
immediate: true
|
||||
}
|
||||
)
|
||||
|
||||
/** 初始化 **/
|
||||
onMounted(() => {
|
||||
getTabsCount()
|
||||
getList()
|
||||
})
|
||||
</script>
|
||||
@@ -1,11 +1,11 @@
|
||||
<template>
|
||||
<Dialog :title="dialogTitle" v-model="dialogVisible">
|
||||
<Dialog v-model="dialogVisible" :title="dialogTitle">
|
||||
<el-form
|
||||
ref="formRef"
|
||||
v-loading="formLoading"
|
||||
:model="formData"
|
||||
:rules="rules"
|
||||
label-width="120px"
|
||||
v-loading="formLoading"
|
||||
>
|
||||
<el-form-item label="名称" prop="name">
|
||||
<el-input v-model="formData.name" placeholder="请输入名称" />
|
||||
@@ -59,17 +59,18 @@
|
||||
<el-input v-model="formData.aesKey" placeholder="请输入消息加解密密钥" />
|
||||
</el-form-item>
|
||||
<el-form-item label="备注" prop="remark">
|
||||
<el-input type="textarea" v-model="formData.remark" placeholder="请输入备注" />
|
||||
<el-input v-model="formData.remark" placeholder="请输入备注" type="textarea" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button>
|
||||
<el-button :disabled="formLoading" type="primary" @click="submitForm">确 定</el-button>
|
||||
<el-button @click="dialogVisible = false">取 消</el-button>
|
||||
</template>
|
||||
</Dialog>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
<script lang="ts" name="MpAccountForm" setup>
|
||||
import * as AccountApi from '@/api/mp/account'
|
||||
|
||||
const { t } = useI18n() // 国际化
|
||||
const message = useMessage() // 消息弹窗
|
||||
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
v-if="scope.row.qrCodeUrl"
|
||||
:src="scope.row.qrCodeUrl"
|
||||
alt="二维码"
|
||||
style="height: 100px; display: inline-block"
|
||||
style="display: inline-block; height: 100px"
|
||||
/>
|
||||
<el-button
|
||||
link
|
||||
|
||||
115
src/views/mp/autoReply/components/ReplyTable.vue
Normal file
115
src/views/mp/autoReply/components/ReplyTable.vue
Normal file
@@ -0,0 +1,115 @@
|
||||
<template>
|
||||
<el-table v-loading="props.loading" :data="props.list">
|
||||
<el-table-column
|
||||
label="请求消息类型"
|
||||
align="center"
|
||||
prop="requestMessageType"
|
||||
v-if="msgType === MsgType.Message"
|
||||
/>
|
||||
<el-table-column
|
||||
label="关键词"
|
||||
align="center"
|
||||
prop="requestKeyword"
|
||||
v-if="msgType === MsgType.Keyword"
|
||||
/>
|
||||
<el-table-column
|
||||
label="匹配类型"
|
||||
align="center"
|
||||
prop="requestMatch"
|
||||
v-if="msgType === MsgType.Keyword"
|
||||
>
|
||||
<template #default="scope">
|
||||
<dict-tag :type="DICT_TYPE.MP_AUTO_REPLY_REQUEST_MATCH" :value="scope.row.requestMatch" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="回复消息类型" align="center">
|
||||
<template #default="scope">
|
||||
<dict-tag :type="DICT_TYPE.MP_MESSAGE_TYPE" :value="scope.row.responseMessageType" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="回复内容" align="center">
|
||||
<template #default="scope">
|
||||
<div v-if="scope.row.responseMessageType === 'text'">{{ scope.row.responseContent }}</div>
|
||||
<div v-else-if="scope.row.responseMessageType === 'voice'">
|
||||
<WxVoicePlayer v-if="scope.row.responseMediaUrl" :url="scope.row.responseMediaUrl" />
|
||||
</div>
|
||||
<div v-else-if="scope.row.responseMessageType === 'image'">
|
||||
<a target="_blank" :href="scope.row.responseMediaUrl">
|
||||
<img :src="scope.row.responseMediaUrl" style="width: 100px" />
|
||||
</a>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="
|
||||
scope.row.responseMessageType === 'video' ||
|
||||
scope.row.responseMessageType === 'shortvideo'
|
||||
"
|
||||
>
|
||||
<WxVideoPlayer
|
||||
v-if="scope.row.responseMediaUrl"
|
||||
:url="scope.row.responseMediaUrl"
|
||||
style="margin-top: 10px"
|
||||
/>
|
||||
</div>
|
||||
<div v-else-if="scope.row.responseMessageType === 'news'">
|
||||
<WxNews :articles="scope.row.responseArticles" />
|
||||
</div>
|
||||
<div v-else-if="scope.row.responseMessageType === 'music'">
|
||||
<WxMusic
|
||||
:title="scope.row.responseTitle"
|
||||
:description="scope.row.responseDescription"
|
||||
:thumb-media-url="scope.row.responseThumbMediaUrl"
|
||||
:music-url="scope.row.responseMusicUrl"
|
||||
:hq-music-url="scope.row.responseHqMusicUrl"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
label="创建时间"
|
||||
align="center"
|
||||
prop="createTime"
|
||||
:formatter="dateFormatter"
|
||||
width="180"
|
||||
/>
|
||||
<el-table-column label="操作" align="center">
|
||||
<template #default="scope">
|
||||
<el-button
|
||||
type="primary"
|
||||
link
|
||||
@click="emit('on-update', scope.row.id)"
|
||||
v-hasPermi="['mp:auto-reply:update']"
|
||||
>
|
||||
修改
|
||||
</el-button>
|
||||
<el-button
|
||||
type="danger"
|
||||
link
|
||||
@click="emit('on-delete', scope.row.id)"
|
||||
v-hasPermi="['mp:auto-reply:delete']"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import WxVideoPlayer from '@/views/mp/components/wx-video-play'
|
||||
import WxVoicePlayer from '@/views/mp/components/wx-voice-play'
|
||||
import WxMusic from '@/views/mp/components/wx-music'
|
||||
import WxNews from '@/views/mp/components/wx-news'
|
||||
import { dateFormatter } from '@/utils/formatTime'
|
||||
import { DICT_TYPE } from '@/utils/dict'
|
||||
import { MsgType } from './types'
|
||||
|
||||
const props = defineProps<{
|
||||
loading: boolean
|
||||
list: any[]
|
||||
msgType: MsgType
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'on-update', v: number)
|
||||
(e: 'on-delete', v: number)
|
||||
}>()
|
||||
</script>
|
||||
7
src/views/mp/autoReply/components/types.ts
Normal file
7
src/views/mp/autoReply/components/types.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
// 消息类型(Follow: 关注时回复;Message: 消息回复;Keyword: 关键词回复)
|
||||
// 作为 tab.name,enum 的数字不能随意修改,与 api 参数相关
|
||||
export enum MsgType {
|
||||
Follow = 1,
|
||||
Message = 2,
|
||||
Keyword = 3
|
||||
}
|
||||
@@ -3,236 +3,127 @@
|
||||
|
||||
<!-- 搜索工作栏 -->
|
||||
<ContentWrap>
|
||||
<el-form
|
||||
class="-mb-15px"
|
||||
:model="queryParams"
|
||||
ref="queryFormRef"
|
||||
:inline="true"
|
||||
label-width="68px"
|
||||
>
|
||||
<el-form class="-mb-15px" :model="queryParams" :inline="true" label-width="68px">
|
||||
<el-form-item label="公众号" prop="accountId">
|
||||
<el-select v-model="queryParams.accountId" placeholder="请选择公众号" class="!w-240px">
|
||||
<el-option
|
||||
v-for="item in accountList"
|
||||
:key="item.id"
|
||||
:label="item.name"
|
||||
:value="item.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button @click="handleQuery"><Icon icon="ep:search" />搜索</el-button>
|
||||
<el-button @click="resetQuery"><Icon icon="ep:refresh" />重置</el-button>
|
||||
<WxAccountSelect @change="onAccountChanged" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</ContentWrap>
|
||||
|
||||
<!-- tab 切换 -->
|
||||
<ContentWrap>
|
||||
<el-tabs v-model="type" @tab-change="handleTabChange">
|
||||
<el-tabs v-model="msgType" @tab-change="onTabChange">
|
||||
<!-- 操作工具栏 -->
|
||||
<el-row :gutter="10" class="mb8">
|
||||
<el-col :span="1.5">
|
||||
<el-button
|
||||
type="primary"
|
||||
plain
|
||||
@click="handleAdd"
|
||||
@click="onCreate"
|
||||
v-hasPermi="['mp:auto-reply:create']"
|
||||
v-if="type !== '1' || list.length <= 0"
|
||||
v-if="msgType !== MsgType.Follow || list.length <= 0"
|
||||
>
|
||||
<Icon icon="ep:plus" />新增
|
||||
</el-button>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<!-- tab 项 -->
|
||||
<el-tab-pane name="1">
|
||||
<el-tab-pane :name="MsgType.Follow">
|
||||
<template #label>
|
||||
<span><Icon icon="ep:star-off" /> 关注时回复</span>
|
||||
<el-row align="middle"><Icon icon="ep:star" class="mr-2px" /> 关注时回复</el-row>
|
||||
</template>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane name="2">
|
||||
<el-tab-pane :name="MsgType.Message">
|
||||
<template #label>
|
||||
<span><Icon icon="ep:chat-line-round" /> 消息回复</span>
|
||||
<el-row align="middle"><Icon icon="ep:chat-line-round" class="mr-2px" /> 消息回复</el-row>
|
||||
</template>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane name="3">
|
||||
<el-tab-pane :name="MsgType.Keyword">
|
||||
<template #label>
|
||||
<span><Icon icon="ep:news" /> 关键词回复</span>
|
||||
<el-row align="middle"><Icon icon="fa:newspaper-o" class="mr-2px" /> 关键词回复</el-row>
|
||||
</template>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
<!-- 列表 -->
|
||||
<el-table v-loading="loading" :data="list">
|
||||
<el-table-column
|
||||
label="请求消息类型"
|
||||
align="center"
|
||||
prop="requestMessageType"
|
||||
v-if="type === '2'"
|
||||
/>
|
||||
<el-table-column label="关键词" align="center" prop="requestKeyword" v-if="type === '3'" />
|
||||
<el-table-column label="匹配类型" align="center" prop="requestMatch" v-if="type === '3'">
|
||||
<template #default="scope">
|
||||
<dict-tag :type="DICT_TYPE.MP_AUTO_REPLY_REQUEST_MATCH" :value="scope.row.requestMatch" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="回复消息类型" align="center">
|
||||
<template #default="scope">
|
||||
<dict-tag :type="DICT_TYPE.MP_MESSAGE_TYPE" :value="scope.row.responseMessageType" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="回复内容" align="center">
|
||||
<template #default="scope">
|
||||
<div v-if="scope.row.responseMessageType === 'text'">{{ scope.row.responseContent }}</div>
|
||||
<div v-else-if="scope.row.responseMessageType === 'voice'">
|
||||
<WxVoicePlayer :url="scope.row.responseMediaUrl" />
|
||||
</div>
|
||||
<div v-else-if="scope.row.responseMessageType === 'image'">
|
||||
<a target="_blank" :href="scope.row.responseMediaUrl">
|
||||
<img :src="scope.row.responseMediaUrl" style="width: 100px" />
|
||||
</a>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="
|
||||
scope.row.responseMessageType === 'video' ||
|
||||
scope.row.responseMessageType === 'shortvideo'
|
||||
"
|
||||
>
|
||||
<WxVideoPlayer :url="scope.row.responseMediaUrl" style="margin-top: 10px" />
|
||||
</div>
|
||||
<div v-else-if="scope.row.responseMessageType === 'news'">
|
||||
<WxNews :articles="scope.row.responseArticles" />
|
||||
</div>
|
||||
<div v-else-if="scope.row.responseMessageType === 'music'">
|
||||
<WxMusic
|
||||
:title="scope.row.responseTitle"
|
||||
:description="scope.row.responseDescription"
|
||||
:thumb-media-url="scope.row.responseThumbMediaUrl"
|
||||
:music-url="scope.row.responseMusicUrl"
|
||||
:hq-music-url="scope.row.responseHqMusicUrl"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
label="创建时间"
|
||||
align="center"
|
||||
prop="createTime"
|
||||
:formatter="dateFormatter"
|
||||
width="180"
|
||||
/>
|
||||
<el-table-column label="操作" align="center">
|
||||
<template #default="scope">
|
||||
<el-button
|
||||
type="primary"
|
||||
link
|
||||
@click="handleUpdate(scope.row)"
|
||||
v-hasPermi="['mp:auto-reply:update']"
|
||||
>
|
||||
修改
|
||||
</el-button>
|
||||
<el-button
|
||||
type="danger"
|
||||
link
|
||||
@click="handleDelete(scope.row)"
|
||||
v-hasPermi="['mp:auto-reply:delete']"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<ReplyTable
|
||||
:loading="loading"
|
||||
:list="list"
|
||||
:msg-type="msgType"
|
||||
@on-update="onUpdate"
|
||||
@on-delete="onDelete"
|
||||
/>
|
||||
|
||||
<!-- 添加或修改自动回复的对话框 -->
|
||||
<el-dialog :title="title" v-model="open" width="800px" append-to-body>
|
||||
<el-form ref="formRef" :model="form" :rules="rules" label-width="80px">
|
||||
<el-form-item label="消息类型" prop="requestMessageType" v-if="type === '2'">
|
||||
<el-select v-model="form.requestMessageType" placeholder="请选择">
|
||||
<!-- TODO @Dhb52 -->
|
||||
<el-dialog :title="dialogTitle" v-model="showFormDialog" width="800px" destroy-on-close>
|
||||
<el-form ref="formRef" :model="replyForm" :rules="rules" label-width="80px">
|
||||
<el-form-item label="消息类型" prop="requestMessageType" v-if="msgType === MsgType.Message">
|
||||
<el-select v-model="replyForm.requestMessageType" placeholder="请选择">
|
||||
<template v-for="dict in getDictOptions(DICT_TYPE.MP_MESSAGE_TYPE)" :key="dict.value">
|
||||
<el-option
|
||||
v-if="requestMessageTypes.includes(dict.value)"
|
||||
v-if="RequestMessageTypes.includes(dict.value)"
|
||||
:label="dict.label"
|
||||
:value="dict.value"
|
||||
/>
|
||||
</template>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="匹配类型" prop="requestMatch" v-if="type === '3'">
|
||||
<el-select v-model="form.requestMatch" placeholder="请选择匹配类型" clearable>
|
||||
<el-form-item label="匹配类型" prop="requestMatch" v-if="msgType === MsgType.Keyword">
|
||||
<el-select v-model="replyForm.requestMatch" placeholder="请选择匹配类型" clearable>
|
||||
<el-option
|
||||
v-for="dict in getDictOptions(DICT_TYPE.MP_AUTO_REPLY_REQUEST_MATCH)"
|
||||
v-for="dict in getIntDictOptions(DICT_TYPE.MP_AUTO_REPLY_REQUEST_MATCH)"
|
||||
:key="dict.value"
|
||||
:label="dict.label"
|
||||
:value="dict.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="关键词" prop="requestKeyword" v-if="type === '3'">
|
||||
<el-input v-model="form.requestKeyword" placeholder="请输入内容" clearable />
|
||||
<el-form-item label="关键词" prop="requestKeyword" v-if="msgType === MsgType.Keyword">
|
||||
<el-input v-model="replyForm.requestKeyword" placeholder="请输入内容" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item label="回复消息">
|
||||
<WxReplySelect :objData="objData" v-if="hackResetWxReplySelect" />
|
||||
<WxReplySelect v-model="reply" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="cancel">取 消</el-button>
|
||||
<el-button type="primary" @click="handleSubmit">确 定</el-button>
|
||||
<el-button type="primary" @click="onSubmit">确 定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</ContentWrap>
|
||||
</template>
|
||||
<script setup name="MpAutoReply">
|
||||
import { ref, reactive, onMounted, nextTick } from 'vue'
|
||||
import WxVideoPlayer from '@/views/mp/components/wx-video-play/main.vue'
|
||||
import WxVoicePlayer from '@/views/mp/components/wx-voice-play/main.vue'
|
||||
import WxMusic from '@/views/mp/components/wx-music/main.vue'
|
||||
import WxNews from '@/views/mp/components/wx-news/main.vue'
|
||||
import WxReplySelect from '@/views/mp/components/wx-reply/main.vue'
|
||||
import { getSimpleAccountList } from '@/api/mp/account'
|
||||
import {
|
||||
createAutoReply,
|
||||
deleteAutoReply,
|
||||
getAutoReply,
|
||||
getAutoReplyPage,
|
||||
updateAutoReply
|
||||
} from '@/api/mp/autoReply'
|
||||
|
||||
import { DICT_TYPE, getDictOptions } from '@/utils/dict'
|
||||
import { dateFormatter } from '@/utils/formatTime'
|
||||
<script setup lang="ts" name="MpAutoReply">
|
||||
import WxReplySelect, { type Reply, ReplyType } from '@/views/mp/components/wx-reply'
|
||||
import WxAccountSelect from '@/views/mp/components/wx-account-select'
|
||||
import * as MpAutoReplyApi from '@/api/mp/autoReply'
|
||||
import { DICT_TYPE, getDictOptions, getIntDictOptions } from '@/utils/dict'
|
||||
import { ContentWrap } from '@/components/ContentWrap'
|
||||
import type { FormInstance, TabPaneName } from 'element-plus'
|
||||
import ReplyTable from './components/ReplyTable.vue'
|
||||
import { MsgType } from './components/types'
|
||||
const message = useMessage() // 消息
|
||||
|
||||
const message = useMessage()
|
||||
|
||||
const queryFormRef = ref()
|
||||
const formRef = ref()
|
||||
|
||||
// tab 类型(1、关注时回复;2、消息回复;3、关键词回复)
|
||||
const type = ref('3')
|
||||
// 允许选择的请求消息类型
|
||||
const requestMessageTypes = ['text', 'image', 'voice', 'video', 'shortvideo', 'location', 'link']
|
||||
// 遮罩层
|
||||
const loading = ref(true)
|
||||
// 显示搜索条件
|
||||
// const showSearch = ref(true)
|
||||
// 总条数
|
||||
const total = ref(0)
|
||||
// 自动回复列表
|
||||
const list = ref([])
|
||||
const accountId = ref(-1) // 公众号ID
|
||||
const msgType = ref<MsgType>(MsgType.Keyword) // 消息类型
|
||||
const RequestMessageTypes = ['text', 'image', 'voice', 'video', 'shortvideo', 'location', 'link'] // 允许选择的请求消息类型
|
||||
const loading = ref(true) // 遮罩层
|
||||
const total = ref(0) // 总条数
|
||||
const list = ref<any[]>([]) // 自动回复列表
|
||||
const formRef = ref<FormInstance | null>(null) // 表单 ref
|
||||
// 查询参数
|
||||
const queryParams = reactive({
|
||||
pageNo: 1,
|
||||
pageSize: 10,
|
||||
accountId: undefined
|
||||
accountId: accountId
|
||||
})
|
||||
|
||||
// 弹出层标题
|
||||
const title = ref('')
|
||||
// 是否显示弹出层
|
||||
const open = ref(false)
|
||||
// 表单参数
|
||||
const form = ref({})
|
||||
const dialogTitle = ref('') // 弹出层标题
|
||||
const showFormDialog = ref(false) // 是否显示弹出层
|
||||
const replyForm = ref<any>({}) // 表单参数
|
||||
// 回复消息
|
||||
const objData = ref({
|
||||
type: 'text'
|
||||
const reply = ref<Reply>({
|
||||
type: ReplyType.Text,
|
||||
accountId: -1
|
||||
})
|
||||
// 表单校验
|
||||
const rules = {
|
||||
@@ -240,43 +131,27 @@ const rules = {
|
||||
requestMatch: [{ required: true, message: '请求的关键字的匹配不能为空', trigger: 'blur' }]
|
||||
}
|
||||
|
||||
const hackResetWxReplySelect = ref(false) // 重置 WxReplySelect 组件,解决无法清除的问题
|
||||
|
||||
// 公众号账号列表
|
||||
const accountList = ref([])
|
||||
|
||||
onMounted(() => {
|
||||
getSimpleAccountList().then((data) => {
|
||||
accountList.value = data
|
||||
// 默认选中第一个
|
||||
if (accountList.value.length > 0) {
|
||||
queryParams.accountId = accountList.value[0].id
|
||||
}
|
||||
// 加载数据
|
||||
getList()
|
||||
})
|
||||
})
|
||||
/** 侦听账号变化 */
|
||||
const onAccountChanged = (id: number) => {
|
||||
accountId.value = id
|
||||
reply.value.accountId = id
|
||||
queryParams.pageNo = 1
|
||||
getList()
|
||||
}
|
||||
|
||||
/** 查询列表 */
|
||||
const getList = async () => {
|
||||
// 如果没有选中公众号账号,则进行提示。
|
||||
if (!queryParams.accountId) {
|
||||
message.error('未选中公众号,无法查询自动回复')
|
||||
return false
|
||||
}
|
||||
|
||||
loading.value = false
|
||||
// 处理查询参数
|
||||
let params = {
|
||||
...queryParams,
|
||||
type: type.value
|
||||
}
|
||||
// 执行查询
|
||||
getAutoReplyPage(params).then((data) => {
|
||||
loading.value = true
|
||||
try {
|
||||
const data = await MpAutoReplyApi.getAutoReplyPage({
|
||||
...queryParams,
|
||||
type: msgType.value
|
||||
})
|
||||
list.value = data.list
|
||||
total.value = data.total
|
||||
} finally {
|
||||
loading.value = false
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/** 搜索按钮操作 */
|
||||
@@ -285,114 +160,103 @@ const handleQuery = () => {
|
||||
getList()
|
||||
}
|
||||
|
||||
/** 重置按钮操作 */
|
||||
const resetQuery = () => {
|
||||
queryFormRef.value?.resetFields()
|
||||
// 默认选中第一个
|
||||
if (accountList.value.length > 0) {
|
||||
queryParams.accountId = accountList.value[0].id
|
||||
}
|
||||
handleQuery()
|
||||
}
|
||||
|
||||
const handleTabChange = (tabName) => {
|
||||
type.value = tabName
|
||||
const onTabChange = (tabName: TabPaneName) => {
|
||||
msgType.value = tabName as MsgType
|
||||
handleQuery()
|
||||
}
|
||||
|
||||
/** 新增按钮操作 */
|
||||
const handleAdd = () => {
|
||||
const onCreate = () => {
|
||||
reset()
|
||||
resetEditor()
|
||||
// 打开表单,并设置初始化
|
||||
open.value = true
|
||||
title.value = '新增自动回复'
|
||||
objData.value = {
|
||||
type: 'text',
|
||||
reply.value = {
|
||||
type: ReplyType.Text,
|
||||
accountId: queryParams.accountId
|
||||
}
|
||||
|
||||
dialogTitle.value = '新增自动回复'
|
||||
showFormDialog.value = true
|
||||
}
|
||||
|
||||
/** 修改按钮操作 */
|
||||
const handleUpdate = (row) => {
|
||||
const onUpdate = async (id: number) => {
|
||||
reset()
|
||||
resetEditor()
|
||||
console.log(row)
|
||||
|
||||
getAutoReply(row.id).then((data) => {
|
||||
// 设置属性
|
||||
form.value = { ...data }
|
||||
delete form.value['responseMessageType']
|
||||
delete form.value['responseContent']
|
||||
delete form.value['responseMediaId']
|
||||
delete form.value['responseMediaUrl']
|
||||
delete form.value['responseDescription']
|
||||
delete form.value['responseArticles']
|
||||
objData.value = {
|
||||
type: data.responseMessageType,
|
||||
accountId: queryParams.accountId,
|
||||
content: data.responseContent,
|
||||
mediaId: data.responseMediaId,
|
||||
url: data.responseMediaUrl,
|
||||
title: data.responseTitle,
|
||||
description: data.responseDescription,
|
||||
thumbMediaId: data.responseThumbMediaId,
|
||||
thumbMediaUrl: data.responseThumbMediaUrl,
|
||||
articles: data.responseArticles,
|
||||
musicUrl: data.responseMusicUrl,
|
||||
hqMusicUrl: data.responseHqMusicUrl
|
||||
}
|
||||
const data = await MpAutoReplyApi.getAutoReply(id)
|
||||
// 设置属性
|
||||
replyForm.value = { ...data }
|
||||
delete replyForm.value['responseMessageType']
|
||||
delete replyForm.value['responseContent']
|
||||
delete replyForm.value['responseMediaId']
|
||||
delete replyForm.value['responseMediaUrl']
|
||||
delete replyForm.value['responseDescription']
|
||||
delete replyForm.value['responseArticles']
|
||||
reply.value = {
|
||||
type: data.responseMessageType,
|
||||
accountId: queryParams.accountId,
|
||||
content: data.responseContent,
|
||||
mediaId: data.responseMediaId,
|
||||
url: data.responseMediaUrl,
|
||||
title: data.responseTitle,
|
||||
description: data.responseDescription,
|
||||
thumbMediaId: data.responseThumbMediaId,
|
||||
thumbMediaUrl: data.responseThumbMediaUrl,
|
||||
articles: data.responseArticles,
|
||||
musicUrl: data.responseMusicUrl,
|
||||
hqMusicUrl: data.responseHqMusicUrl
|
||||
}
|
||||
|
||||
// 打开表单
|
||||
open.value = true
|
||||
title.value = '修改自动回复'
|
||||
})
|
||||
// 打开表单
|
||||
dialogTitle.value = '修改自动回复'
|
||||
showFormDialog.value = true
|
||||
}
|
||||
|
||||
const handleSubmit = () => {
|
||||
formRef.value?.validate((valid) => {
|
||||
if (!valid) {
|
||||
return
|
||||
}
|
||||
/** 删除按钮操作 */
|
||||
const onDelete = async (id: number) => {
|
||||
await message.confirm('是否确认删除此数据?')
|
||||
await MpAutoReplyApi.deleteAutoReply(id)
|
||||
await getList()
|
||||
message.success('删除成功')
|
||||
}
|
||||
|
||||
// 处理回复消息
|
||||
const form = { ...form.value }
|
||||
form.responseMessageType = objData.value.type
|
||||
form.responseContent = objData.value.content
|
||||
form.responseMediaId = objData.value.mediaId
|
||||
form.responseMediaUrl = objData.value.url
|
||||
form.responseTitle = objData.value.title
|
||||
form.responseDescription = objData.value.description
|
||||
form.responseThumbMediaId = objData.value.thumbMediaId
|
||||
form.responseThumbMediaUrl = objData.value.thumbMediaUrl
|
||||
form.responseArticles = objData.value.articles
|
||||
form.responseMusicUrl = objData.value.musicUrl
|
||||
form.responseHqMusicUrl = objData.value.hqMusicUrl
|
||||
const onSubmit = async () => {
|
||||
const valid = await formRef.value?.validate()
|
||||
if (!valid) return
|
||||
|
||||
if (form.value.id !== undefined) {
|
||||
updateAutoReply(form).then(() => {
|
||||
message.success('修改成功')
|
||||
open.value = false
|
||||
getList()
|
||||
})
|
||||
} else {
|
||||
createAutoReply(form).then(() => {
|
||||
message.success('新增成功')
|
||||
open.value = false
|
||||
getList()
|
||||
})
|
||||
}
|
||||
})
|
||||
// 处理回复消息
|
||||
const submitForm: any = { ...replyForm.value }
|
||||
submitForm.responseMessageType = reply.value.type
|
||||
submitForm.responseContent = reply.value.content
|
||||
submitForm.responseMediaId = reply.value.mediaId
|
||||
submitForm.responseMediaUrl = reply.value.url
|
||||
submitForm.responseTitle = reply.value.title
|
||||
submitForm.responseDescription = reply.value.description
|
||||
submitForm.responseThumbMediaId = reply.value.thumbMediaId
|
||||
submitForm.responseThumbMediaUrl = reply.value.thumbMediaUrl
|
||||
submitForm.responseArticles = reply.value.articles
|
||||
submitForm.responseMusicUrl = reply.value.musicUrl
|
||||
submitForm.responseHqMusicUrl = reply.value.hqMusicUrl
|
||||
|
||||
if (replyForm.value.id !== undefined) {
|
||||
await MpAutoReplyApi.updateAutoReply(submitForm)
|
||||
message.success('修改成功')
|
||||
} else {
|
||||
await MpAutoReplyApi.createAutoReply(submitForm)
|
||||
message.success('新增成功')
|
||||
}
|
||||
|
||||
showFormDialog.value = false
|
||||
await getList()
|
||||
}
|
||||
|
||||
// 表单重置
|
||||
const reset = () => {
|
||||
form.value = {
|
||||
replyForm.value = {
|
||||
id: undefined,
|
||||
accountId: queryParams.accountId,
|
||||
type: type.value,
|
||||
type: msgType.value,
|
||||
requestKeyword: undefined,
|
||||
requestMatch: type.value === '3' ? 1 : undefined,
|
||||
requestMatch: msgType.value === MsgType.Keyword ? 1 : undefined,
|
||||
requestMessageType: undefined
|
||||
}
|
||||
formRef.value?.resetFields()
|
||||
@@ -400,22 +264,7 @@ const reset = () => {
|
||||
|
||||
// 取消按钮
|
||||
const cancel = () => {
|
||||
open.value = false
|
||||
showFormDialog.value = false
|
||||
reset()
|
||||
}
|
||||
|
||||
// 表单 Editor 重置
|
||||
const resetEditor = () => {
|
||||
hackResetWxReplySelect.value = false // 销毁组件
|
||||
nextTick(() => {
|
||||
hackResetWxReplySelect.value = true // 重建组件
|
||||
})
|
||||
}
|
||||
|
||||
const handleDelete = async (row) => {
|
||||
await message.confirm('是否确认删除此数据?')
|
||||
await deleteAutoReply(row.id)
|
||||
await getList()
|
||||
message.success('删除成功')
|
||||
}
|
||||
</script>
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 15 KiB |
3
src/views/mp/components/wx-account-select/index.ts
Normal file
3
src/views/mp/components/wx-account-select/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import WxAccountSelect from './main.vue'
|
||||
|
||||
export default WxAccountSelect
|
||||
45
src/views/mp/components/wx-account-select/main.vue
Normal file
45
src/views/mp/components/wx-account-select/main.vue
Normal file
@@ -0,0 +1,45 @@
|
||||
<template>
|
||||
<el-select v-model="account.id" placeholder="请选择公众号" class="!w-240px" @change="onChanged">
|
||||
<el-option v-for="item in accountList" :key="item.id" :label="item.name" :value="item.id" />
|
||||
</el-select>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup name="WxAccountSelect">
|
||||
import * as MpAccountApi from '@/api/mp/account'
|
||||
|
||||
const account: MpAccountApi.AccountVO = reactive({
|
||||
id: -1,
|
||||
name: ''
|
||||
})
|
||||
|
||||
const accountList = ref<MpAccountApi.AccountVO[]>([])
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'change', id: number, name: string)
|
||||
}>()
|
||||
|
||||
const handleQuery = async () => {
|
||||
accountList.value = await MpAccountApi.getSimpleAccountList()
|
||||
// 默认选中第一个
|
||||
if (accountList.value.length > 0) {
|
||||
account.id = accountList.value[0].id
|
||||
if (account.id) {
|
||||
account.name = accountList.value[0].name
|
||||
emit('change', account.id, account.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const onChanged = (id?: number) => {
|
||||
const found = accountList.value.find((v) => v.id === id)
|
||||
if (account.id) {
|
||||
account.name = found ? found.name : ''
|
||||
emit('change', account.id, account.name)
|
||||
}
|
||||
}
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(() => {
|
||||
handleQuery()
|
||||
})
|
||||
</script>
|
||||
@@ -1,201 +0,0 @@
|
||||
<script setup>
|
||||
import { ref, reactive } from 'vue'
|
||||
import { QuillEditor } from '@vueup/vue-quill'
|
||||
import '@vueup/vue-quill/dist/vue-quill.snow.css'
|
||||
import { getAccessToken } from '@/utils/auth'
|
||||
import editorOptions from './quill-options'
|
||||
|
||||
const BASE_URL = import.meta.env.VITE_BASE_URL
|
||||
|
||||
const message = useMessage()
|
||||
|
||||
const props = defineProps({
|
||||
/* 公众号账号编号 */
|
||||
accountId: {
|
||||
type: Number,
|
||||
required: true
|
||||
},
|
||||
/* 编辑器的内容 */
|
||||
value: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
/* 图片大小 */
|
||||
maxSize: {
|
||||
type: Number,
|
||||
default: 4000 // kb
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['input'])
|
||||
|
||||
const myQuillEditorRef = ref()
|
||||
|
||||
const content = ref(props.value.replace(/data-src/g, 'src'))
|
||||
|
||||
const loading = ref(false) // 根据图片上传状态来确定是否显示loading动画,刚开始是false,不显示
|
||||
|
||||
const actionUrl = ref(BASE_URL + '/admin-api/mp/material/upload-news-image') // 这里写你要上传的图片服务器地址
|
||||
const headers = ref({ Authorization: 'Bearer ' + getAccessToken() }) // 设置上传的请求头部
|
||||
const uploadData = reactive({
|
||||
type: 'image', // TODO 芋艿:试试要不要换成 thumb
|
||||
accountId: props.accountId
|
||||
})
|
||||
|
||||
const onEditorChange = () => {
|
||||
//内容改变事件
|
||||
emit('input', content.value)
|
||||
}
|
||||
|
||||
// 富文本图片上传前
|
||||
const beforeUpload = () => {
|
||||
// 显示 loading 动画
|
||||
loading.value = true
|
||||
}
|
||||
|
||||
// 图片上传成功
|
||||
// 注意!由于微信公众号的图片有访问限制,所以会显示“此图片来自微信公众号,未经允许不可引用”
|
||||
const uploadSuccess = (res) => {
|
||||
// res为图片服务器返回的数据
|
||||
// 获取富文本组件实例
|
||||
const quill = myQuillEditorRef.value.quill
|
||||
// 如果上传成功
|
||||
const link = res.data
|
||||
if (link) {
|
||||
// 获取光标所在位置
|
||||
let length = quill.getSelection().index
|
||||
// 插入图片 res.info为服务器返回的图片地址
|
||||
quill.insertEmbed(length, 'image', link)
|
||||
// 调整光标到最后
|
||||
quill.setSelection(length + 1)
|
||||
} else {
|
||||
message.error('图片插入失败')
|
||||
}
|
||||
// loading 动画消失
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
// 富文本图片上传失败
|
||||
const uploadError = () => {
|
||||
// loading 动画消失
|
||||
loading.value = false
|
||||
message.error('图片插入失败')
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div id="wxEditor">
|
||||
<div v-loading="loading" element-loading-text="请稍等,图片上传中">
|
||||
<!-- 图片上传组件辅助-->
|
||||
<el-upload
|
||||
class="avatar-uploader"
|
||||
name="file"
|
||||
:action="actionUrl"
|
||||
:headers="headers"
|
||||
:show-file-list="false"
|
||||
:data="uploadData"
|
||||
:on-success="uploadSuccess"
|
||||
:on-error="uploadError"
|
||||
:before-upload="beforeUpload"
|
||||
/>
|
||||
<QuillEditor
|
||||
class="editor"
|
||||
v-model="content"
|
||||
ref="quillEditorRef"
|
||||
:options="editorOptions"
|
||||
@change="onEditorChange($event)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.editor {
|
||||
line-height: normal !important;
|
||||
height: 500px;
|
||||
}
|
||||
|
||||
.ql-snow .ql-tooltip[data-mode='link']::before {
|
||||
content: '请输入链接地址:';
|
||||
}
|
||||
|
||||
.ql-snow .ql-tooltip.ql-editing a.ql-action::after {
|
||||
border-right: 0;
|
||||
content: '保存';
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
.ql-snow .ql-tooltip[data-mode='video']::before {
|
||||
content: '请输入视频地址:';
|
||||
}
|
||||
|
||||
.ql-snow .ql-picker.ql-size .ql-picker-label::before,
|
||||
.ql-snow .ql-picker.ql-size .ql-picker-item::before {
|
||||
content: '14px';
|
||||
}
|
||||
|
||||
.ql-snow .ql-picker.ql-size .ql-picker-label[data-value='small']::before,
|
||||
.ql-snow .ql-picker.ql-size .ql-picker-item[data-value='small']::before {
|
||||
content: '10px';
|
||||
}
|
||||
|
||||
.ql-snow .ql-picker.ql-size .ql-picker-label[data-value='large']::before,
|
||||
.ql-snow .ql-picker.ql-size .ql-picker-item[data-value='large']::before {
|
||||
content: '18px';
|
||||
}
|
||||
|
||||
.ql-snow .ql-picker.ql-size .ql-picker-label[data-value='huge']::before,
|
||||
.ql-snow .ql-picker.ql-size .ql-picker-item[data-value='huge']::before {
|
||||
content: '32px';
|
||||
}
|
||||
|
||||
.ql-snow .ql-picker.ql-header .ql-picker-label::before,
|
||||
.ql-snow .ql-picker.ql-header .ql-picker-item::before {
|
||||
content: '文本';
|
||||
}
|
||||
|
||||
.ql-snow .ql-picker.ql-header .ql-picker-label[data-value='1']::before,
|
||||
.ql-snow .ql-picker.ql-header .ql-picker-item[data-value='1']::before {
|
||||
content: '标题1';
|
||||
}
|
||||
|
||||
.ql-snow .ql-picker.ql-header .ql-picker-label[data-value='2']::before,
|
||||
.ql-snow .ql-picker.ql-header .ql-picker-item[data-value='2']::before {
|
||||
content: '标题2';
|
||||
}
|
||||
|
||||
.ql-snow .ql-picker.ql-header .ql-picker-label[data-value='3']::before,
|
||||
.ql-snow .ql-picker.ql-header .ql-picker-item[data-value='3']::before {
|
||||
content: '标题3';
|
||||
}
|
||||
|
||||
.ql-snow .ql-picker.ql-header .ql-picker-label[data-value='4']::before,
|
||||
.ql-snow .ql-picker.ql-header .ql-picker-item[data-value='4']::before {
|
||||
content: '标题4';
|
||||
}
|
||||
|
||||
.ql-snow .ql-picker.ql-header .ql-picker-label[data-value='5']::before,
|
||||
.ql-snow .ql-picker.ql-header .ql-picker-item[data-value='5']::before {
|
||||
content: '标题5';
|
||||
}
|
||||
|
||||
.ql-snow .ql-picker.ql-header .ql-picker-label[data-value='6']::before,
|
||||
.ql-snow .ql-picker.ql-header .ql-picker-item[data-value='6']::before {
|
||||
content: '标题6';
|
||||
}
|
||||
|
||||
.ql-snow .ql-picker.ql-font .ql-picker-label::before,
|
||||
.ql-snow .ql-picker.ql-font .ql-picker-item::before {
|
||||
content: '标准字体';
|
||||
}
|
||||
|
||||
.ql-snow .ql-picker.ql-font .ql-picker-label[data-value='serif']::before,
|
||||
.ql-snow .ql-picker.ql-font .ql-picker-item[data-value='serif']::before {
|
||||
content: '衬线字体';
|
||||
}
|
||||
|
||||
.ql-snow .ql-picker.ql-font .ql-picker-label[data-value='monospace']::before,
|
||||
.ql-snow .ql-picker.ql-font .ql-picker-item[data-value='monospace']::before {
|
||||
content: '等宽字体';
|
||||
}
|
||||
</style>
|
||||
@@ -1,45 +0,0 @@
|
||||
const toolbarOptions = [
|
||||
['bold', 'italic', 'underline', 'strike'], // 加粗 斜体 下划线 删除线
|
||||
['blockquote', 'code-block'], // 引用 代码块
|
||||
[{ header: 1 }, { header: 2 }], // 1、2 级标题
|
||||
[{ list: 'ordered' }, { list: 'bullet' }], // 有序、无序列表
|
||||
[{ script: 'sub' }, { script: 'super' }], // 上标/下标
|
||||
[{ indent: '-1' }, { indent: '+1' }], // 缩进
|
||||
// [{'direction': 'rtl'}], // 文本方向
|
||||
[{ size: ['small', false, 'large', 'huge'] }], // 字体大小
|
||||
[{ header: [1, 2, 3, 4, 5, 6, false] }], // 标题
|
||||
[{ color: [] }, { background: [] }], // 字体颜色、字体背景颜色
|
||||
[{ font: [] }], // 字体种类
|
||||
[{ align: [] }], // 对齐方式
|
||||
['clean'], // 清除文本格式
|
||||
['link', 'image', 'video'] // 链接、图片、视频
|
||||
]
|
||||
|
||||
export default {
|
||||
theme: 'snow',
|
||||
placeholder: '请输入文章内容',
|
||||
modules: {
|
||||
toolbar: {
|
||||
container: toolbarOptions,
|
||||
// container: "#toolbar",
|
||||
handlers: {
|
||||
image: function (value) {
|
||||
if (value) {
|
||||
// 触发input框选择图片文件
|
||||
document.querySelector('.avatar-uploader input').click()
|
||||
} else {
|
||||
this.quill.format('image', false)
|
||||
}
|
||||
},
|
||||
link: function (value) {
|
||||
if (value) {
|
||||
const href = prompt('注意!只支持公众号图文链接')
|
||||
this.quill.format('link', href)
|
||||
} else {
|
||||
this.quill.format('link', false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
3
src/views/mp/components/wx-location/index.ts
Normal file
3
src/views/mp/components/wx-location/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import WxLocation from './main.vue'
|
||||
|
||||
export default WxLocation
|
||||
@@ -1,5 +1,5 @@
|
||||
<!--
|
||||
【微信消息 - 定位】
|
||||
【微信消息 - 定位】TODO @Dhb52 目前未启用
|
||||
-->
|
||||
<template>
|
||||
<div>
|
||||
@@ -31,7 +31,6 @@
|
||||
/>
|
||||
</el-row>
|
||||
<el-row>
|
||||
<el-icon><Location /></el-icon>
|
||||
<Icon icon="ep:location" />
|
||||
{{ label }}
|
||||
</el-row>
|
||||
@@ -39,6 +38,7 @@
|
||||
</el-link>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" name="WxLocation">
|
||||
const props = defineProps({
|
||||
locationX: {
|
||||
|
||||
6
src/views/mp/components/wx-material-select/index.ts
Normal file
6
src/views/mp/components/wx-material-select/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import WxMaterialSelect from './main.vue'
|
||||
import { NewsType, MaterialType } from './types'
|
||||
|
||||
export { NewsType, MaterialType }
|
||||
|
||||
export default WxMaterialSelect
|
||||
@@ -7,14 +7,15 @@
|
||||
<template>
|
||||
<div class="pb-30px">
|
||||
<!-- 类型:image -->
|
||||
<div v-if="objData.type === 'image'">
|
||||
<div v-if="props.type === 'image'">
|
||||
<div class="waterfall" v-loading="loading">
|
||||
<div class="waterfall-item" v-for="item in list" :key="item.mediaId">
|
||||
<img class="material-img" :src="item.url" />
|
||||
<p class="item-name">{{ item.name }}</p>
|
||||
<el-row class="ope-row">
|
||||
<el-button type="success" @click="selectMaterialFun(item)">
|
||||
选择 <Icon icon="ep:circle-check" />
|
||||
选择
|
||||
<Icon icon="ep:circle-check" />
|
||||
</el-button>
|
||||
</el-row>
|
||||
</div>
|
||||
@@ -28,7 +29,7 @@
|
||||
/>
|
||||
</div>
|
||||
<!-- 类型:voice -->
|
||||
<div v-else-if="objData.type === 'voice'">
|
||||
<div v-else-if="props.type === 'voice'">
|
||||
<!-- 列表 -->
|
||||
<el-table v-loading="loading" :data="list">
|
||||
<el-table-column label="编号" align="center" prop="mediaId" />
|
||||
@@ -48,7 +49,8 @@
|
||||
<el-table-column label="操作" align="center" fixed="right">
|
||||
<template #default="scope">
|
||||
<el-button type="primary" link @click="selectMaterialFun(scope.row)"
|
||||
>选择<Icon icon="ep:plus" />
|
||||
>选择
|
||||
<Icon icon="ep:plus" />
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
@@ -62,7 +64,7 @@
|
||||
/>
|
||||
</div>
|
||||
<!-- 类型:video -->
|
||||
<div v-else-if="objData.type === 'video'">
|
||||
<div v-else-if="props.type === 'video'">
|
||||
<!-- 列表 -->
|
||||
<el-table v-loading="loading" :data="list">
|
||||
<el-table-column label="编号" align="center" prop="mediaId" />
|
||||
@@ -89,7 +91,8 @@
|
||||
>
|
||||
<template #default="scope">
|
||||
<el-button type="primary" link @click="selectMaterialFun(scope.row)"
|
||||
>选择<Icon icon="akar-icons:circle-plus" />
|
||||
>选择
|
||||
<Icon icon="akar-icons:circle-plus" />
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
@@ -103,14 +106,15 @@
|
||||
/>
|
||||
</div>
|
||||
<!-- 类型:news -->
|
||||
<div v-else-if="objData.type === 'news'">
|
||||
<div v-else-if="props.type === 'news'">
|
||||
<div class="waterfall" v-loading="loading">
|
||||
<div class="waterfall-item" v-for="item in list" :key="item.mediaId">
|
||||
<div v-if="item.content && item.content.newsItem">
|
||||
<WxNews :articles="item.content.newsItem" />
|
||||
<el-row class="ope-row">
|
||||
<el-button type="success" @click="selectMaterialFun(item)">
|
||||
选择<Icon icon="ep:circle-check" />
|
||||
选择
|
||||
<Icon icon="ep:circle-check" />
|
||||
</el-button>
|
||||
</el-row>
|
||||
</div>
|
||||
@@ -127,129 +131,128 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" name="WxMaterialSelect">
|
||||
import WxNews from '@/views/mp/components/wx-news/main.vue'
|
||||
import WxVoicePlayer from '@/views/mp/components/wx-voice-play/main.vue'
|
||||
import WxVideoPlayer from '@/views/mp/components/wx-video-play/main.vue'
|
||||
import { getMaterialPage } from '@/api/mp/material'
|
||||
import { getFreePublishPage } from '@/api/mp/freePublish'
|
||||
import { getDraftPage } from '@/api/mp/draft'
|
||||
<script lang="ts" setup name="WxMaterialSelect">
|
||||
import WxNews from '@/views/mp/components/wx-news'
|
||||
import WxVoicePlayer from '@/views/mp/components/wx-voice-play'
|
||||
import WxVideoPlayer from '@/views/mp/components/wx-video-play'
|
||||
import { NewsType } from './types'
|
||||
import * as MpMaterialApi from '@/api/mp/material'
|
||||
import * as MpFreePublishApi from '@/api/mp/freePublish'
|
||||
import * as MpDraftApi from '@/api/mp/draft'
|
||||
import { dateFormatter } from '@/utils/formatTime'
|
||||
import { defineComponent, PropType } from 'vue'
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
WxNews,
|
||||
WxVoicePlayer,
|
||||
WxVideoPlayer
|
||||
},
|
||||
props: {
|
||||
objData: {
|
||||
type: Object, // type - 类型;accountId - 公众号账号编号
|
||||
required: true
|
||||
},
|
||||
newsType: {
|
||||
// 图文类型:1、已发布图文;2、草稿箱图文
|
||||
type: String as PropType<string>,
|
||||
default: '1'
|
||||
}
|
||||
},
|
||||
setup(props, ctx) {
|
||||
// 遮罩层
|
||||
const loading = ref(false)
|
||||
// 总条数
|
||||
const total = ref(0)
|
||||
// 数据列表
|
||||
const list = ref([])
|
||||
// 查询参数
|
||||
const queryParams = reactive({
|
||||
pageNo: 1,
|
||||
pageSize: 10,
|
||||
accountId: props.objData.accountId
|
||||
})
|
||||
const objDataRef = reactive(props.objData)
|
||||
const newsTypeRef = ref(props.newsType)
|
||||
|
||||
const selectMaterialFun = (item) => {
|
||||
ctx.emit('select-material', item)
|
||||
}
|
||||
/** 搜索按钮操作 */
|
||||
const handleQuery = () => {
|
||||
queryParams.pageNo = 1
|
||||
getPage()
|
||||
}
|
||||
const getPage = () => {
|
||||
loading.value = true
|
||||
if (objDataRef.type === 'news' && newsTypeRef.value === '1') {
|
||||
// 【图文】+ 【已发布】
|
||||
getFreePublishPageFun()
|
||||
} else if (objDataRef.type === 'news' && newsTypeRef.value === '2') {
|
||||
// 【图文】+ 【草稿】
|
||||
getDraftPageFun()
|
||||
} else {
|
||||
// 【素材】
|
||||
getMaterialPageFun()
|
||||
}
|
||||
}
|
||||
|
||||
const getMaterialPageFun = async () => {
|
||||
let data = await getMaterialPage({
|
||||
...queryParams,
|
||||
type: objDataRef.type
|
||||
})
|
||||
list.value = data.list
|
||||
total.value = data.total
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
const getFreePublishPageFun = async () => {
|
||||
let data = await getFreePublishPage(queryParams)
|
||||
data.list.forEach((item) => {
|
||||
const newsItem = item.content.newsItem
|
||||
newsItem.forEach((article) => {
|
||||
article.picUrl = article.thumbUrl
|
||||
})
|
||||
})
|
||||
list.value = data.list
|
||||
total.value = data.total
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
const getDraftPageFun = async () => {
|
||||
let data = await getDraftPage(queryParams)
|
||||
data.list.forEach((item) => {
|
||||
const newsItem = item.content.newsItem
|
||||
newsItem.forEach((article) => {
|
||||
article.picUrl = article.thumbUrl
|
||||
})
|
||||
})
|
||||
list.value = data.list
|
||||
total.value = data.total
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
getPage()
|
||||
})
|
||||
|
||||
return {
|
||||
handleQuery,
|
||||
dateFormatter,
|
||||
selectMaterialFun,
|
||||
getMaterialPageFun,
|
||||
getPage,
|
||||
formatDate,
|
||||
queryParams,
|
||||
objDataRef,
|
||||
list,
|
||||
total,
|
||||
loading
|
||||
}
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
type: string
|
||||
accountId: number
|
||||
newsType?: NewsType
|
||||
}>(),
|
||||
{
|
||||
newsType: NewsType.Published
|
||||
}
|
||||
)
|
||||
|
||||
const emit = defineEmits(['select-material'])
|
||||
|
||||
// 遮罩层
|
||||
const loading = ref(false)
|
||||
// 总条数
|
||||
const total = ref(0)
|
||||
// 数据列表
|
||||
const list = ref<any[]>([])
|
||||
// 查询参数
|
||||
const queryParams = reactive({
|
||||
pageNo: 1,
|
||||
pageSize: 10,
|
||||
accountId: props.accountId
|
||||
})
|
||||
|
||||
const selectMaterialFun = (item) => {
|
||||
emit('select-material', item)
|
||||
}
|
||||
|
||||
const getPage = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
if (props.type === 'news' && props.newsType === NewsType.Published) {
|
||||
// 【图文】+ 【已发布】
|
||||
await getFreePublishPageFun()
|
||||
} else if (props.type === 'news' && props.newsType === NewsType.Draft) {
|
||||
// 【图文】+ 【草稿】
|
||||
await getDraftPageFun()
|
||||
} else {
|
||||
// 【素材】
|
||||
await getMaterialPageFun()
|
||||
}
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const getMaterialPageFun = async () => {
|
||||
const data = await MpMaterialApi.getMaterialPage({
|
||||
...queryParams,
|
||||
type: props.type
|
||||
})
|
||||
list.value = data.list
|
||||
total.value = data.total
|
||||
}
|
||||
|
||||
const getFreePublishPageFun = async () => {
|
||||
const data = await MpFreePublishApi.getFreePublishPage(queryParams)
|
||||
data.list.forEach((item: any) => {
|
||||
const articles = item.content.newsItem
|
||||
articles.forEach((article: any) => {
|
||||
article.picUrl = article.thumbUrl
|
||||
})
|
||||
})
|
||||
list.value = data.list
|
||||
total.value = data.total
|
||||
}
|
||||
|
||||
const getDraftPageFun = async () => {
|
||||
const data = await MpDraftApi.getDraftPage(queryParams)
|
||||
data.list.forEach((draft: any) => {
|
||||
const articles = draft.content.newsItem
|
||||
articles.forEach((article: any) => {
|
||||
article.picUrl = article.thumbUrl
|
||||
})
|
||||
})
|
||||
list.value = data.list
|
||||
total.value = data.total
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
getPage()
|
||||
})
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
/*瀑布流样式*/
|
||||
@media (min-width: 992px) and (max-width: 1300px) {
|
||||
.waterfall {
|
||||
column-count: 3;
|
||||
}
|
||||
|
||||
p {
|
||||
color: red;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 768px) and (max-width: 991px) {
|
||||
.waterfall {
|
||||
column-count: 2;
|
||||
}
|
||||
|
||||
p {
|
||||
color: orange;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
.waterfall {
|
||||
column-count: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.waterfall {
|
||||
width: 100%;
|
||||
column-gap: 10px;
|
||||
@@ -271,30 +274,4 @@ export default defineComponent({
|
||||
p {
|
||||
line-height: 30px;
|
||||
}
|
||||
|
||||
@media (min-width: 992px) and (max-width: 1300px) {
|
||||
.waterfall {
|
||||
column-count: 3;
|
||||
}
|
||||
p {
|
||||
color: red;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 768px) and (max-width: 991px) {
|
||||
.waterfall {
|
||||
column-count: 2;
|
||||
}
|
||||
p {
|
||||
color: orange;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
.waterfall {
|
||||
column-count: 1;
|
||||
}
|
||||
}
|
||||
|
||||
/*瀑布流样式*/
|
||||
</style>
|
||||
|
||||
11
src/views/mp/components/wx-material-select/types.ts
Normal file
11
src/views/mp/components/wx-material-select/types.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
export enum NewsType {
|
||||
Draft = '2',
|
||||
Published = '1'
|
||||
}
|
||||
|
||||
export enum MaterialType {
|
||||
Image = 'image',
|
||||
Voice = 'voice',
|
||||
Video = 'video',
|
||||
News = 'news'
|
||||
}
|
||||
@@ -1,25 +1,27 @@
|
||||
.avue-card{
|
||||
&__item{
|
||||
.avue-card {
|
||||
&__item {
|
||||
margin-bottom: 16px;
|
||||
border: 1px solid #e8e8e8;
|
||||
background-color: #fff;
|
||||
box-sizing: border-box;
|
||||
color: rgba(0,0,0,.65);
|
||||
color: rgba(0, 0, 0, 0.65);
|
||||
font-size: 14px;
|
||||
font-variant: tabular-nums;
|
||||
line-height: 1.5;
|
||||
list-style: none;
|
||||
font-feature-settings: "tnum";
|
||||
font-feature-settings: 'tnum';
|
||||
cursor: pointer;
|
||||
height:200px;
|
||||
&:hover{
|
||||
border-color: rgba(0,0,0,.09);
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,.09);
|
||||
height: 200px;
|
||||
|
||||
&:hover {
|
||||
border-color: rgba(0, 0, 0, 0.09);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.09);
|
||||
}
|
||||
&--add{
|
||||
border:1px dashed #000;
|
||||
|
||||
&--add {
|
||||
border: 1px dashed #000;
|
||||
width: 100%;
|
||||
color: rgba(0,0,0,.45);
|
||||
color: rgba(0, 0, 0, 0.45);
|
||||
background-color: #fff;
|
||||
border-color: #d9d9d9;
|
||||
border-radius: 2px;
|
||||
@@ -27,74 +29,87 @@
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 16px;
|
||||
i{
|
||||
|
||||
i {
|
||||
margin-right: 10px;
|
||||
}
|
||||
&:hover{
|
||||
|
||||
&:hover {
|
||||
color: #40a9ff;
|
||||
background-color: #fff;
|
||||
border-color: #40a9ff;
|
||||
}
|
||||
}
|
||||
}
|
||||
&__body{
|
||||
|
||||
&__body {
|
||||
display: flex;
|
||||
padding: 24px;
|
||||
}
|
||||
&__detail{
|
||||
flex:1
|
||||
|
||||
&__detail {
|
||||
flex: 1;
|
||||
}
|
||||
&__avatar{
|
||||
|
||||
&__avatar {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 48px;
|
||||
overflow: hidden;
|
||||
margin-right: 12px;
|
||||
img{
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
&__title{
|
||||
color: rgba(0,0,0,.85);
|
||||
|
||||
&__title {
|
||||
color: rgba(0, 0, 0, 0.85);
|
||||
margin-bottom: 12px;
|
||||
font-size: 16px;
|
||||
&:hover{
|
||||
color:#1890ff;
|
||||
|
||||
&:hover {
|
||||
color: #1890ff;
|
||||
}
|
||||
}
|
||||
&__info{
|
||||
color: rgba(0,0,0,.45);
|
||||
|
||||
&__info {
|
||||
color: rgba(0, 0, 0, 0.45);
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 3;
|
||||
overflow: hidden;
|
||||
height: 64px;
|
||||
}
|
||||
&__menu{
|
||||
|
||||
&__menu {
|
||||
display: flex;
|
||||
justify-content:space-around;
|
||||
justify-content: space-around;
|
||||
height: 50px;
|
||||
background: #f7f9fa;
|
||||
color: rgba(0,0,0,.45);
|
||||
color: rgba(0, 0, 0, 0.45);
|
||||
text-align: center;
|
||||
line-height: 50px;
|
||||
&:hover{
|
||||
color:#1890ff;
|
||||
|
||||
&:hover {
|
||||
color: #1890ff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** joolun 额外加的 */
|
||||
.avue-comment__main {
|
||||
flex: unset!important;
|
||||
border-radius: 5px!important;
|
||||
margin: 0 8px!important;
|
||||
flex: unset !important;
|
||||
border-radius: 5px !important;
|
||||
margin: 0 8px !important;
|
||||
}
|
||||
|
||||
.avue-comment__header {
|
||||
border-top-left-radius: 5px;
|
||||
border-top-right-radius: 5px;
|
||||
}
|
||||
|
||||
.avue-comment__body {
|
||||
border-bottom-right-radius: 5px;
|
||||
border-bottom-left-radius: 5px;
|
||||
|
||||
@@ -1,27 +1,33 @@
|
||||
/* 来自 https://github.com/nmxiaowei/avue/blob/master/styles/src/element-ui/comment.scss */
|
||||
.avue-comment{
|
||||
.avue-comment {
|
||||
margin-bottom: 30px;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
&--reverse{
|
||||
flex-direction:row-reverse;
|
||||
.avue-comment__main{
|
||||
&:before,&:after{
|
||||
|
||||
&--reverse {
|
||||
flex-direction: row-reverse;
|
||||
|
||||
.avue-comment__main {
|
||||
&:before,
|
||||
&:after {
|
||||
left: auto;
|
||||
right: -8px;
|
||||
border-width: 8px 0 8px 8px;
|
||||
}
|
||||
&:before{
|
||||
|
||||
&:before {
|
||||
border-left-color: #dedede;
|
||||
}
|
||||
&:after{
|
||||
|
||||
&:after {
|
||||
border-left-color: #f8f8f8;
|
||||
margin-right: 1px;
|
||||
margin-left: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
&__avatar{
|
||||
|
||||
&__avatar {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 50%;
|
||||
@@ -29,7 +35,8 @@
|
||||
box-sizing: border-box;
|
||||
vertical-align: middle;
|
||||
}
|
||||
&__header{
|
||||
|
||||
&__header {
|
||||
padding: 5px 15px;
|
||||
background: #f8f8f8;
|
||||
border-bottom: 1px solid #eee;
|
||||
@@ -37,18 +44,22 @@
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
&__author{
|
||||
|
||||
&__author {
|
||||
font-weight: 700;
|
||||
font-size: 14px;
|
||||
color: #999;
|
||||
}
|
||||
&__main{
|
||||
flex:1;
|
||||
|
||||
&__main {
|
||||
flex: 1;
|
||||
margin: 0 20px;
|
||||
position: relative;
|
||||
border: 1px solid #dedede;
|
||||
border-radius: 2px;
|
||||
&:before,&:after{
|
||||
|
||||
&:before,
|
||||
&:after {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
left: -8px;
|
||||
@@ -56,32 +67,39 @@
|
||||
width: 0;
|
||||
height: 0;
|
||||
display: block;
|
||||
content: " ";
|
||||
content: ' ';
|
||||
border-color: transparent;
|
||||
border-style: solid solid outset;
|
||||
border-width: 8px 8px 8px 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
&:before {
|
||||
border-right-color: #dedede;
|
||||
z-index: 1;
|
||||
}
|
||||
&:after{
|
||||
|
||||
&:after {
|
||||
border-right-color: #f8f8f8;
|
||||
margin-left: 1px;
|
||||
z-index: 2;
|
||||
}
|
||||
}
|
||||
&__body{
|
||||
|
||||
&__body {
|
||||
padding: 15px;
|
||||
overflow: hidden;
|
||||
background: #fff;
|
||||
font-family: Segoe UI,Lucida Grande,Helvetica,Arial,Microsoft YaHei,FreeSans,Arimo,Droid Sans,wenquanyi micro hei,Hiragino Sans GB,Hiragino Sans GB W3,FontAwesome,sans-serif;color: #333;
|
||||
font-family: Segoe UI, Lucida Grande, Helvetica, Arial, Microsoft YaHei, FreeSans, Arimo,
|
||||
Droid Sans, wenquanyi micro hei, Hiragino Sans GB, Hiragino Sans GB W3, FontAwesome,
|
||||
sans-serif;
|
||||
color: #333;
|
||||
font-size: 14px;
|
||||
}
|
||||
blockquote{
|
||||
margin:0;
|
||||
font-family: Georgia,Times New Roman,Times,Kai,Kaiti SC,KaiTi,BiauKai,FontAwesome,serif;
|
||||
|
||||
blockquote {
|
||||
margin: 0;
|
||||
font-family: Georgia, Times New Roman, Times, Kai, Kaiti SC, KaiTi, BiauKai, FontAwesome, serif;
|
||||
padding: 1px 0 1px 15px;
|
||||
border-left: 4px solid #ddd;
|
||||
}
|
||||
|
||||
67
src/views/mp/components/wx-msg/components/Msg.vue
Normal file
67
src/views/mp/components/wx-msg/components/Msg.vue
Normal file
@@ -0,0 +1,67 @@
|
||||
<template>
|
||||
<div>
|
||||
<MsgEvent v-if="item.type === MsgType.Event" :item="item" />
|
||||
|
||||
<div v-else-if="item.type === MsgType.Text">{{ item.content }}</div>
|
||||
|
||||
<div v-else-if="item.type === MsgType.Voice">
|
||||
<WxVoicePlayer :url="item.mediaUrl" :content="item.recognition" />
|
||||
</div>
|
||||
|
||||
<div v-else-if="item.type === MsgType.Image">
|
||||
<a target="_blank" :href="item.mediaUrl">
|
||||
<img :src="item.mediaUrl" style="width: 100px" />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-else-if="item.type === MsgType.Video || item.type === 'shortvideo'"
|
||||
style="text-align: center"
|
||||
>
|
||||
<WxVideoPlayer :url="item.mediaUrl" />
|
||||
</div>
|
||||
|
||||
<div v-else-if="item.type === MsgType.Link" class="avue-card__detail">
|
||||
<el-link type="success" :underline="false" target="_blank" :href="item.url">
|
||||
<div class="avue-card__title"><i class="el-icon-link"></i>{{ item.title }}</div>
|
||||
</el-link>
|
||||
<div class="avue-card__info" style="height: unset">{{ item.description }}</div>
|
||||
</div>
|
||||
|
||||
<div v-else-if="item.type === MsgType.Location">
|
||||
<WxLocation :label="item.label" :location-y="item.locationY" :location-x="item.locationX" />
|
||||
</div>
|
||||
|
||||
<div v-else-if="item.type === MsgType.News" style="width: 300px">
|
||||
<WxNews :articles="item.articles" />
|
||||
</div>
|
||||
|
||||
<div v-else-if="item.type === MsgType.Music">
|
||||
<WxMusic
|
||||
:title="item.title"
|
||||
:description="item.description"
|
||||
:thumb-media-url="item.thumbMediaUrl"
|
||||
:music-url="item.musicUrl"
|
||||
:hq-music-url="item.hqMusicUrl"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" name="Msg">
|
||||
import WxVideoPlayer from '@/views/mp/components/wx-video-play'
|
||||
import WxVoicePlayer from '@/views/mp/components/wx-voice-play'
|
||||
import WxNews from '@/views/mp/components/wx-news'
|
||||
import WxLocation from '@/views/mp/components/wx-location'
|
||||
import WxMusic from '@/views/mp/components/wx-music'
|
||||
import MsgEvent from './MsgEvent.vue'
|
||||
import { MsgType } from '../types'
|
||||
|
||||
const props = defineProps<{
|
||||
item: any
|
||||
}>()
|
||||
|
||||
const item = ref<any>(props.item)
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
49
src/views/mp/components/wx-msg/components/MsgEvent.vue
Normal file
49
src/views/mp/components/wx-msg/components/MsgEvent.vue
Normal file
@@ -0,0 +1,49 @@
|
||||
<template>
|
||||
<div>
|
||||
<div v-if="item.event === 'subscribe'">
|
||||
<el-tag type="success">关注</el-tag>
|
||||
</div>
|
||||
<div v-else-if="item.event === 'unsubscribe'">
|
||||
<el-tag type="danger">取消关注</el-tag>
|
||||
</div>
|
||||
<div v-else-if="item.event === 'CLICK'">
|
||||
<el-tag>点击菜单</el-tag>
|
||||
【{{ item.eventKey }}】
|
||||
</div>
|
||||
<div v-else-if="item.event === 'VIEW'">
|
||||
<el-tag>点击菜单链接</el-tag>
|
||||
【{{ item.eventKey }}】
|
||||
</div>
|
||||
<div v-else-if="item.event === 'scancode_waitmsg'">
|
||||
<el-tag>扫码结果</el-tag>
|
||||
【{{ item.eventKey }}】
|
||||
</div>
|
||||
<div v-else-if="item.event === 'scancode_push'">
|
||||
<el-tag>扫码结果</el-tag>
|
||||
【{{ item.eventKey }}】
|
||||
</div>
|
||||
<div v-else-if="item.event === 'pic_sysphoto'">
|
||||
<el-tag>系统拍照发图</el-tag>
|
||||
</div>
|
||||
<div v-else-if="item.event === 'pic_photo_or_album'">
|
||||
<el-tag>拍照或者相册</el-tag>
|
||||
</div>
|
||||
<div v-else-if="item.event === 'pic_weixin'">
|
||||
<el-tag>微信相册</el-tag>
|
||||
</div>
|
||||
<div v-else-if="item.event === 'location_select'">
|
||||
<el-tag>选择地理位置</el-tag>
|
||||
</div>
|
||||
<div v-else>
|
||||
<el-tag type="danger">未知事件类型</el-tag>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const props = defineProps<{
|
||||
item: any
|
||||
}>()
|
||||
|
||||
const item = ref(props.item)
|
||||
</script>
|
||||
60
src/views/mp/components/wx-msg/components/MsgList.vue
Normal file
60
src/views/mp/components/wx-msg/components/MsgList.vue
Normal file
@@ -0,0 +1,60 @@
|
||||
<template>
|
||||
<div class="execution" v-for="item in props.list" :key="item.id">
|
||||
<div
|
||||
class="avue-comment"
|
||||
:class="{ 'avue-comment--reverse': item.sendFrom === SendFrom.MpBot }"
|
||||
>
|
||||
<div class="avatar-div">
|
||||
<img :src="getAvatar(item.sendFrom)" class="avue-comment__avatar" />
|
||||
<div class="avue-comment__author">
|
||||
{{ getNickname(item.sendFrom) }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="avue-comment__main">
|
||||
<div class="avue-comment__header">
|
||||
<div class="avue-comment__create_time">{{ formatDate(item.createTime) }}</div>
|
||||
</div>
|
||||
<div
|
||||
class="avue-comment__body"
|
||||
:style="item.sendFrom === SendFrom.MpBot ? 'background: #6BED72;' : ''"
|
||||
>
|
||||
<Msg :item="item" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts" name="MsgList">
|
||||
import Msg from './Msg.vue'
|
||||
import { formatDate } from '@/utils/formatTime'
|
||||
import { User } from '../types'
|
||||
import avatarWechat from '@/assets/imgs/wechat.png'
|
||||
|
||||
const props = defineProps<{
|
||||
list: any[]
|
||||
accountId: number
|
||||
user: User
|
||||
}>()
|
||||
|
||||
enum SendFrom {
|
||||
User = 1,
|
||||
MpBot = 2
|
||||
}
|
||||
|
||||
const getAvatar = (sendFrom: SendFrom) =>
|
||||
sendFrom === SendFrom.User ? props.user.avatar : avatarWechat
|
||||
|
||||
const getNickname = (sendFrom: SendFrom) =>
|
||||
sendFrom === SendFrom.User ? props.user.nickname : '公众号'
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
/* 因为 joolun 实现依赖 avue 组件,该页面使用了 comment.scss、card.scc */
|
||||
@import '../comment.scss';
|
||||
@import '../card.scss';
|
||||
|
||||
.avatar-div {
|
||||
text-align: center;
|
||||
width: 80px;
|
||||
}
|
||||
</style>
|
||||
6
src/views/mp/components/wx-msg/index.ts
Normal file
6
src/views/mp/components/wx-msg/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import WxMsg from './main.vue'
|
||||
import { MsgType } from './types'
|
||||
|
||||
export { MsgType }
|
||||
|
||||
export default WxMsg
|
||||
@@ -7,310 +7,169 @@
|
||||
-->
|
||||
<template>
|
||||
<ContentWrap>
|
||||
<div class="msg-div" :id="'msg-div' + nowStr">
|
||||
<div class="msg-div" ref="msgDivRef">
|
||||
<!-- 加载更多 -->
|
||||
<div v-loading="loading"></div>
|
||||
<div v-if="!loading">
|
||||
<div class="el-table__empty-block" v-if="loadMore" @click="loadingMore"
|
||||
<div class="el-table__empty-block" v-if="hasMore" @click="loadMore"
|
||||
><span class="el-table__empty-text">点击加载更多</span></div
|
||||
>
|
||||
<div class="el-table__empty-block" v-if="!loadMore"
|
||||
<div class="el-table__empty-block" v-if="!hasMore"
|
||||
><span class="el-table__empty-text">没有更多了</span></div
|
||||
>
|
||||
</div>
|
||||
|
||||
<!-- 消息列表 -->
|
||||
<div class="execution" v-for="item in list" :key="item.id">
|
||||
<div class="avue-comment" :class="item.sendFrom === 2 ? 'avue-comment--reverse' : ''">
|
||||
<div class="avatar-div">
|
||||
<img
|
||||
:src="item.sendFrom === 1 ? user.avatar : mp.avatar"
|
||||
class="avue-comment__avatar"
|
||||
/>
|
||||
<div class="avue-comment__author"
|
||||
>{{ item.sendFrom === 1 ? user.nickname : mp.nickname }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="avue-comment__main">
|
||||
<div class="avue-comment__header">
|
||||
<div class="avue-comment__create_time">{{ formatDate(item.createTime) }}</div>
|
||||
</div>
|
||||
<div
|
||||
class="avue-comment__body"
|
||||
:style="item.sendFrom === 2 ? 'background: #6BED72;' : ''"
|
||||
>
|
||||
<!-- 【事件】区域 -->
|
||||
<div v-if="item.type === 'event' && item.event === 'subscribe'">
|
||||
<el-tag type="success">关注</el-tag>
|
||||
</div>
|
||||
<div v-else-if="item.type === 'event' && item.event === 'unsubscribe'">
|
||||
<el-tag type="danger">取消关注</el-tag>
|
||||
</div>
|
||||
<div v-else-if="item.type === 'event' && item.event === 'CLICK'">
|
||||
<el-tag>点击菜单</el-tag>
|
||||
【{{ item.eventKey }}】
|
||||
</div>
|
||||
<div v-else-if="item.type === 'event' && item.event === 'VIEW'">
|
||||
<el-tag>点击菜单链接</el-tag>
|
||||
【{{ item.eventKey }}】
|
||||
</div>
|
||||
<div v-else-if="item.type === 'event' && item.event === 'scancode_waitmsg'">
|
||||
<el-tag>扫码结果</el-tag>
|
||||
【{{ item.eventKey }}】
|
||||
</div>
|
||||
<div v-else-if="item.type === 'event' && item.event === 'scancode_push'">
|
||||
<el-tag>扫码结果</el-tag>
|
||||
【{{ item.eventKey }}】
|
||||
</div>
|
||||
<div v-else-if="item.type === 'event' && item.event === 'pic_sysphoto'">
|
||||
<el-tag>系统拍照发图</el-tag>
|
||||
</div>
|
||||
<div v-else-if="item.type === 'event' && item.event === 'pic_photo_or_album'">
|
||||
<el-tag>拍照或者相册</el-tag>
|
||||
</div>
|
||||
<div v-else-if="item.type === 'event' && item.event === 'pic_weixin'">
|
||||
<el-tag>微信相册</el-tag>
|
||||
</div>
|
||||
<div v-else-if="item.type === 'event' && item.event === 'location_select'">
|
||||
<el-tag>选择地理位置</el-tag>
|
||||
</div>
|
||||
<div v-else-if="item.type === 'event'">
|
||||
<el-tag type="danger">未知事件类型</el-tag>
|
||||
</div>
|
||||
<!-- 【消息】区域 -->
|
||||
<div v-else-if="item.type === 'text'">{{ item.content }}</div>
|
||||
<div v-else-if="item.type === 'voice'">
|
||||
<wx-voice-player :url="item.mediaUrl" :content="item.recognition" />
|
||||
</div>
|
||||
<div v-else-if="item.type === 'image'">
|
||||
<a target="_blank" :href="item.mediaUrl">
|
||||
<img :src="item.mediaUrl" style="width: 100px" />
|
||||
</a>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="item.type === 'video' || item.type === 'shortvideo'"
|
||||
style="text-align: center"
|
||||
>
|
||||
<wx-video-player :url="item.mediaUrl" />
|
||||
</div>
|
||||
<div v-else-if="item.type === 'link'" class="avue-card__detail">
|
||||
<el-link type="success" :underline="false" target="_blank" :href="item.url">
|
||||
<div class="avue-card__title"><i class="el-icon-link"></i>{{ item.title }}</div>
|
||||
</el-link>
|
||||
<div class="avue-card__info" style="height: unset">{{ item.description }}</div>
|
||||
</div>
|
||||
<!-- TODO 芋艿:待完善 -->
|
||||
<div v-else-if="item.type === 'location'">
|
||||
<wx-location
|
||||
:label="item.label"
|
||||
:location-y="item.locationY"
|
||||
:location-x="item.locationX"
|
||||
/>
|
||||
</div>
|
||||
<div v-else-if="item.type === 'news'" style="width: 300px">
|
||||
<!-- TODO 芋艿:待测试;详情页也存在类似的情况 -->
|
||||
<wx-news :articles="item.articles" />
|
||||
</div>
|
||||
<div v-else-if="item.type === 'music'">
|
||||
<wx-music
|
||||
:title="item.title"
|
||||
:description="item.description"
|
||||
:thumb-media-url="item.thumbMediaUrl"
|
||||
:music-url="item.musicUrl"
|
||||
:hq-music-url="item.hqMusicUrl"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<MsgList :list="list" :account-id="accountId" :user="user" />
|
||||
</div>
|
||||
|
||||
<div class="msg-send" v-loading="sendLoading">
|
||||
<wx-reply-select ref="replySelect" :objData="objData" />
|
||||
<el-button type="success" size="small" class="send-but" @click="sendMsg">发送(S)</el-button>
|
||||
<WxReplySelect ref="replySelectRef" v-model="reply" />
|
||||
<el-button type="success" class="send-but" @click="sendMsg">发送(S)</el-button>
|
||||
</div>
|
||||
</ContentWrap>
|
||||
</template>
|
||||
|
||||
<script lang="ts" name="WxMsg">
|
||||
<script setup lang="ts" name="WxMsg">
|
||||
import WxReplySelect, { Reply, ReplyType } from '@/views/mp/components/wx-reply'
|
||||
import MsgList from './components/MsgList.vue'
|
||||
import { getMessagePage, sendMessage } from '@/api/mp/message'
|
||||
import WxReplySelect from '@/views/mp/components/wx-reply/main.vue'
|
||||
import WxVideoPlayer from '@/views/mp/components/wx-video-play/main.vue'
|
||||
import WxVoicePlayer from '@/views/mp/components/wx-voice-play/main.vue'
|
||||
import WxNews from '@/views/mp/components/wx-news/main.vue'
|
||||
import WxLocation from '@/views/mp/components/wx-location/main.vue'
|
||||
import WxMusic from '@/views/mp/components/wx-music/main.vue'
|
||||
import { getUser } from '@/api/mp/user'
|
||||
import { defineComponent } from 'vue'
|
||||
import profile from '@/assets/imgs/profile.jpg'
|
||||
import { User } from './types'
|
||||
|
||||
const message = useMessage() // 消息弹窗
|
||||
import profile from '@/assets/imgs/profile.jpg'
|
||||
import wechat from '@/assets/imgs/wechat.png'
|
||||
import { formatDate } from '@/utils/formatTime'
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
WxReplySelect,
|
||||
WxVideoPlayer,
|
||||
WxVoicePlayer,
|
||||
WxNews,
|
||||
WxLocation,
|
||||
WxMusic
|
||||
},
|
||||
props: {
|
||||
userId: {
|
||||
type: Number,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
setup(props) {
|
||||
const nowStr = ref(new Date().getTime()) // 当前的时间戳,用于每次消息加载后,回到原位置;具体见 :id="'msg-div' + nowStr" 处
|
||||
const loading = ref(false) // 消息列表是否正在加载中
|
||||
const loadMore = ref(true) // 是否可以加载更多
|
||||
const list = ref<any[]>([]) // 消息列表
|
||||
const queryParams = reactive({
|
||||
pageNo: 1, // 当前页数
|
||||
pageSize: 14, // 每页显示多少条
|
||||
accountId: undefined
|
||||
})
|
||||
const user = reactive({
|
||||
// 由于微信不再提供昵称,直接使用“用户”展示
|
||||
nickname: '用户',
|
||||
avatar: profile,
|
||||
accountId: 0 // 公众号账号编号
|
||||
})
|
||||
const mp = reactive({
|
||||
nickname: '公众号',
|
||||
avatar: wechat
|
||||
})
|
||||
|
||||
// ========= 消息发送 =========
|
||||
const sendLoading = ref(false) // 发送消息是否加载中
|
||||
const objData = reactive({
|
||||
// 微信发送消息
|
||||
type: 'text',
|
||||
accountId: null,
|
||||
articles: []
|
||||
})
|
||||
|
||||
const replySelect = ref(null)
|
||||
// 执行发送
|
||||
const sendMsg = async () => {
|
||||
if (!objData) {
|
||||
return
|
||||
}
|
||||
// // 公众号限制:客服消息,公众号只允许发送一条
|
||||
if (objData.type === 'news' && objData.articles.length > 1) {
|
||||
objData.articles = [objData.articles[0]]
|
||||
message.success('图文消息条数限制在 1 条以内,已默认发送第一条')
|
||||
}
|
||||
let data = await sendMessage(Object.assign({ userId: props.userId }, { ...objData }))
|
||||
sendLoading.value = false
|
||||
list.value = [...list.value, ...[data]]
|
||||
scrollToBottom()
|
||||
//ts檢查的時候會判斷這個組件可能是空的,所以需要進行斷言。
|
||||
//避免 tab 的数据未清理
|
||||
const deleteObj = (replySelect.value as any).deleteObj
|
||||
if (deleteObj) {
|
||||
deleteObj()
|
||||
}
|
||||
}
|
||||
const loadingMore = () => {
|
||||
queryParams.pageNo++
|
||||
getPage(queryParams, null)
|
||||
}
|
||||
const getPage = async (page, params) => {
|
||||
loading.value = true
|
||||
let dataTemp = await getMessagePage(
|
||||
Object.assign(
|
||||
{
|
||||
pageNo: page.pageNo,
|
||||
pageSize: page.pageSize,
|
||||
userId: props.userId,
|
||||
accountId: page.accountId
|
||||
},
|
||||
params
|
||||
)
|
||||
)
|
||||
const msgDiv = document.getElementById('msg-div' + nowStr.value)
|
||||
let scrollHeight = 0
|
||||
if (msgDiv) {
|
||||
scrollHeight = msgDiv.scrollHeight
|
||||
}
|
||||
// 处理数据
|
||||
let data = dataTemp.list.reverse()
|
||||
list.value = [...data, ...list.value]
|
||||
loading.value = false
|
||||
if (data.length < queryParams.pageSize || data.length === 0) {
|
||||
loadMore.value = false
|
||||
}
|
||||
queryParams.pageNo = page.pageNo
|
||||
queryParams.pageSize = page.pageSize
|
||||
// 滚动到原来的位置
|
||||
if (queryParams.pageNo === 1) {
|
||||
// 定位到消息底部
|
||||
scrollToBottom()
|
||||
} else if (data.length !== 0) {
|
||||
// 定位滚动条
|
||||
await nextTick(() => {
|
||||
if (scrollHeight !== 0) {
|
||||
let div = document.getElementById('msg-div' + nowStr.value)
|
||||
if (div && msgDiv) {
|
||||
msgDiv.scrollTop = div.scrollHeight - scrollHeight - 100
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
const refreshChange = () => {
|
||||
getPage(queryParams, null)
|
||||
}
|
||||
/** 定位到消息底部 */
|
||||
const scrollToBottom = () => {
|
||||
nextTick(() => {
|
||||
let div = document.getElementById('msg-div' + nowStr.value)
|
||||
if (div) {
|
||||
div.scrollTop = div.scrollHeight
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
let data = await getUser(props.userId)
|
||||
user.nickname = data.nickname && data.nickname.length > 0 ? data.nickname : user.nickname
|
||||
user.avatar = data.avatar && user.avatar.length > 0 ? data.avatar : user.avatar
|
||||
user.accountId = data.accountId
|
||||
queryParams.accountId = data.accountId
|
||||
objData.accountId = data.accountId
|
||||
refreshChange()
|
||||
})
|
||||
return {
|
||||
sendMsg,
|
||||
loadingMore,
|
||||
formatDate,
|
||||
scrollToBottom,
|
||||
objData,
|
||||
mp,
|
||||
user,
|
||||
queryParams,
|
||||
list,
|
||||
loadMore,
|
||||
loading,
|
||||
nowStr,
|
||||
sendLoading
|
||||
}
|
||||
const props = defineProps({
|
||||
userId: {
|
||||
type: Number,
|
||||
required: true
|
||||
}
|
||||
})
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
/* 因为 joolun 实现依赖 avue 组件,该页面使用了 comment.scss、card.scc */
|
||||
@import './comment.scss';
|
||||
@import './card.scss';
|
||||
|
||||
.msg-main {
|
||||
margin-top: -30px;
|
||||
padding: 10px;
|
||||
const accountId = ref(-1) // 公众号ID,需要通过userId初始化
|
||||
const loading = ref(false) // 消息列表是否正在加载中
|
||||
const hasMore = ref(true) // 是否可以加载更多
|
||||
const list = ref<any[]>([]) // 消息列表
|
||||
const queryParams = reactive({
|
||||
pageNo: 1, // 当前页数
|
||||
pageSize: 14, // 每页显示多少条
|
||||
accountId: accountId
|
||||
})
|
||||
|
||||
// 由于微信不再提供昵称,直接使用“用户”展示
|
||||
const user: User = reactive({
|
||||
nickname: '用户',
|
||||
avatar: profile,
|
||||
accountId: accountId // 公众号账号编号
|
||||
})
|
||||
|
||||
// ========= 消息发送 =========
|
||||
const sendLoading = ref(false) // 发送消息是否加载中
|
||||
// 微信发送消息
|
||||
const reply = ref<Reply>({
|
||||
type: ReplyType.Text,
|
||||
accountId: -1,
|
||||
articles: []
|
||||
})
|
||||
|
||||
const replySelectRef = ref<InstanceType<typeof WxReplySelect> | null>(null) // WxReplySelect组件ref,用于消息发送成功后清除内容
|
||||
const msgDivRef = ref<HTMLDivElement | null>(null) // 消息显示窗口ref,用于滚动到底部
|
||||
|
||||
/** 完成加载 */
|
||||
onMounted(async () => {
|
||||
const data = await getUser(props.userId)
|
||||
user.nickname = data.nickname?.length > 0 ? data.nickname : user.nickname
|
||||
user.avatar = user.avatar?.length > 0 ? data.avatar : user.avatar
|
||||
accountId.value = data.accountId
|
||||
reply.value.accountId = data.accountId
|
||||
|
||||
refreshChange()
|
||||
})
|
||||
|
||||
// 执行发送
|
||||
const sendMsg = async () => {
|
||||
if (!unref(reply)) {
|
||||
return
|
||||
}
|
||||
// 公众号限制:客服消息,公众号只允许发送一条
|
||||
if (
|
||||
reply.value.type === ReplyType.News &&
|
||||
reply.value.articles &&
|
||||
reply.value.articles.length > 1
|
||||
) {
|
||||
reply.value.articles = [reply.value.articles[0]]
|
||||
message.success('图文消息条数限制在 1 条以内,已默认发送第一条')
|
||||
}
|
||||
|
||||
const data = await sendMessage({ userId: props.userId, ...reply.value })
|
||||
sendLoading.value = false
|
||||
|
||||
list.value = [...list.value, ...[data]]
|
||||
await scrollToBottom()
|
||||
|
||||
// 发送后清空数据
|
||||
replySelectRef.value?.clear()
|
||||
}
|
||||
|
||||
const loadMore = () => {
|
||||
queryParams.pageNo++
|
||||
getPage(queryParams, null)
|
||||
}
|
||||
|
||||
const getPage = async (page: any, params: any = null) => {
|
||||
loading.value = true
|
||||
let dataTemp = await getMessagePage(
|
||||
Object.assign(
|
||||
{
|
||||
pageNo: page.pageNo,
|
||||
pageSize: page.pageSize,
|
||||
userId: props.userId,
|
||||
accountId: page.accountId
|
||||
},
|
||||
params
|
||||
)
|
||||
)
|
||||
|
||||
const scrollHeight = msgDivRef.value?.scrollHeight ?? 0
|
||||
// 处理数据
|
||||
const data = dataTemp.list.reverse()
|
||||
list.value = [...data, ...list.value]
|
||||
loading.value = false
|
||||
if (data.length < queryParams.pageSize || data.length === 0) {
|
||||
hasMore.value = false
|
||||
}
|
||||
queryParams.pageNo = page.pageNo
|
||||
queryParams.pageSize = page.pageSize
|
||||
// 滚动到原来的位置
|
||||
if (queryParams.pageNo === 1) {
|
||||
// 定位到消息底部
|
||||
await scrollToBottom()
|
||||
} else if (data.length !== 0) {
|
||||
// 定位滚动条
|
||||
await nextTick()
|
||||
if (scrollHeight !== 0) {
|
||||
if (msgDivRef.value) {
|
||||
msgDivRef.value.scrollTop = msgDivRef.value.scrollHeight - scrollHeight - 100
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const refreshChange = () => {
|
||||
getPage(queryParams)
|
||||
}
|
||||
|
||||
/** 定位到消息底部 */
|
||||
const scrollToBottom = async () => {
|
||||
await nextTick()
|
||||
if (msgDivRef.value) {
|
||||
msgDivRef.value.scrollTop = msgDivRef.value.scrollHeight
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.msg-div {
|
||||
height: 50vh;
|
||||
overflow: auto;
|
||||
@@ -323,13 +182,9 @@ export default defineComponent({
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.avatar-div {
|
||||
text-align: center;
|
||||
width: 80px;
|
||||
}
|
||||
|
||||
.send-but {
|
||||
float: right;
|
||||
margin-top: 8px !important;
|
||||
margin-top: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
</style>
|
||||
|
||||
17
src/views/mp/components/wx-msg/types.ts
Normal file
17
src/views/mp/components/wx-msg/types.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
export enum MsgType {
|
||||
Event = 'event',
|
||||
Text = 'text',
|
||||
Voice = 'voice',
|
||||
Image = 'image',
|
||||
Video = 'video',
|
||||
Link = 'link',
|
||||
Location = 'location',
|
||||
Music = 'music',
|
||||
News = 'news'
|
||||
}
|
||||
|
||||
export interface User {
|
||||
nickname: string
|
||||
avatar: string
|
||||
accountId: number
|
||||
}
|
||||
3
src/views/mp/components/wx-music/index.ts
Normal file
3
src/views/mp/components/wx-music/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import WxMusic from './main.vue'
|
||||
|
||||
export default WxMusic
|
||||
@@ -55,6 +55,6 @@ defineExpose({
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
/* 因为 joolun 实现依赖 avue 组件,该页面使用了 card.scc */
|
||||
/* 因为 joolun 实现依赖 avue 组件,该页面使用了 card.scss */
|
||||
@import '../wx-msg/card.scss';
|
||||
</style>
|
||||
|
||||
3
src/views/mp/components/wx-news/index.ts
Normal file
3
src/views/mp/components/wx-news/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import WxNews from './main.vue'
|
||||
|
||||
export default WxNews
|
||||
@@ -9,13 +9,13 @@
|
||||
<div class="news-home">
|
||||
<div v-for="(article, index) in articles" :key="index" class="news-div">
|
||||
<!-- 头条 -->
|
||||
<a target="_blank" :href="article.url" v-if="index === 0">
|
||||
<a v-if="index === 0" :href="article.url" target="_blank">
|
||||
<div class="news-main">
|
||||
<div class="news-content">
|
||||
<el-image
|
||||
:src="article.picUrl"
|
||||
class="material-img"
|
||||
style="width: 100%; height: 120px"
|
||||
:src="article.picUrl"
|
||||
/>
|
||||
<div class="news-content-title">
|
||||
<span>{{ article.title }}</span>
|
||||
@@ -24,12 +24,12 @@
|
||||
</div>
|
||||
</a>
|
||||
<!-- 二条/三条等等 -->
|
||||
<a target="_blank" :href="article.url" v-else>
|
||||
<a v-else :href="article.url" target="_blank">
|
||||
<div class="news-main-item">
|
||||
<div class="news-content-item">
|
||||
<div class="news-content-item-title">{{ article.title }}</div>
|
||||
<div class="news-content-item-img">
|
||||
<img class="material-img" :src="article.picUrl" height="100%" />
|
||||
<img :src="article.picUrl" class="material-img" height="100%" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -38,13 +38,15 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const props = defineProps({
|
||||
articles: {
|
||||
type: Array,
|
||||
default: () => null
|
||||
<script lang="ts" name="WxNews" setup>
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
articles: any[] | null
|
||||
}>(),
|
||||
{
|
||||
articles: null
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
defineExpose({
|
||||
articles: props.articles
|
||||
@@ -53,54 +55,62 @@ defineExpose({
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.news-home {
|
||||
background-color: #ffffff;
|
||||
width: 100%;
|
||||
margin: auto;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.news-main {
|
||||
width: 100%;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.news-content {
|
||||
background-color: #acadae;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
background-color: #acadae;
|
||||
}
|
||||
|
||||
.news-content-title {
|
||||
display: inline-block;
|
||||
font-size: 12px;
|
||||
color: #ffffff;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
background-color: black;
|
||||
left: 0;
|
||||
display: inline-block;
|
||||
width: 98%;
|
||||
padding: 1%;
|
||||
opacity: 0.65;
|
||||
font-size: 12px;
|
||||
color: #fff;
|
||||
white-space: normal;
|
||||
background-color: black;
|
||||
opacity: 0.65;
|
||||
box-sizing: unset !important;
|
||||
}
|
||||
|
||||
.news-main-item {
|
||||
background-color: #ffffff;
|
||||
padding: 5px 0;
|
||||
background-color: #fff;
|
||||
border-top: 1px solid #eaeaea;
|
||||
}
|
||||
|
||||
.news-content-item {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.news-content-item-title {
|
||||
display: inline-block;
|
||||
font-size: 10px;
|
||||
width: 70%;
|
||||
margin-left: 1%;
|
||||
font-size: 10px;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.news-content-item-img {
|
||||
display: inline-block;
|
||||
width: 25%;
|
||||
background-color: #acadae;
|
||||
margin-right: 1%;
|
||||
background-color: #acadae;
|
||||
}
|
||||
|
||||
.material-img {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
171
src/views/mp/components/wx-reply/components/TabImage.vue
Normal file
171
src/views/mp/components/wx-reply/components/TabImage.vue
Normal file
@@ -0,0 +1,171 @@
|
||||
<template>
|
||||
<div>
|
||||
<!-- 情况一:已经选择好素材、或者上传好图片 -->
|
||||
<div class="select-item" v-if="reply.url">
|
||||
<img class="material-img" :src="reply.url" />
|
||||
<p class="item-name" v-if="reply.name">{{ reply.name }}</p>
|
||||
<el-row class="ope-row" justify="center">
|
||||
<el-button type="danger" circle @click="onDelete">
|
||||
<Icon icon="ep:delete" />
|
||||
</el-button>
|
||||
</el-row>
|
||||
</div>
|
||||
<!-- 情况二:未做完上述操作 -->
|
||||
<el-row v-else style="text-align: center" align="middle">
|
||||
<!-- 选择素材 -->
|
||||
<el-col :span="12" class="col-select">
|
||||
<el-button type="success" @click="showDialog = true">
|
||||
素材库选择 <Icon icon="ep:circle-check" />
|
||||
</el-button>
|
||||
<el-dialog
|
||||
title="选择图片"
|
||||
v-model="showDialog"
|
||||
width="90%"
|
||||
append-to-body
|
||||
destroy-on-close
|
||||
>
|
||||
<WxMaterialSelect
|
||||
type="image"
|
||||
:account-id="reply.accountId"
|
||||
@select-material="selectMaterial"
|
||||
/>
|
||||
</el-dialog>
|
||||
</el-col>
|
||||
<!-- 文件上传 -->
|
||||
<el-col :span="12" class="col-add">
|
||||
<el-upload
|
||||
:action="UPLOAD_URL"
|
||||
:headers="HEADERS"
|
||||
multiple
|
||||
:limit="1"
|
||||
:file-list="fileList"
|
||||
:data="uploadData"
|
||||
:before-upload="beforeImageUpload"
|
||||
:on-success="onUploadSuccess"
|
||||
>
|
||||
<el-button type="primary">上传图片</el-button>
|
||||
<template #tip>
|
||||
<span>
|
||||
<div class="el-upload__tip">支持 bmp/png/jpeg/jpg/gif 格式,大小不超过 2M</div>
|
||||
</span>
|
||||
</template>
|
||||
</el-upload>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import WxMaterialSelect from '@/views/mp/components/wx-material-select'
|
||||
import { UploadType, useBeforeUpload } from '@/views/mp/hooks/useUpload'
|
||||
import type { UploadRawFile } from 'element-plus'
|
||||
import { getAccessToken } from '@/utils/auth'
|
||||
import { Reply } from './types'
|
||||
const message = useMessage()
|
||||
|
||||
const UPLOAD_URL = import.meta.env.VITE_API_BASEPATH + '/admin-api/mp/material/upload-temporary'
|
||||
const HEADERS = { Authorization: 'Bearer ' + getAccessToken() } // 设置上传的请求头部
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: Reply
|
||||
}>()
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modelValue', v: Reply)
|
||||
}>()
|
||||
const reply = computed<Reply>({
|
||||
get: () => props.modelValue,
|
||||
set: (val) => emit('update:modelValue', val)
|
||||
})
|
||||
|
||||
const showDialog = ref(false)
|
||||
const fileList = ref([])
|
||||
const uploadData = reactive({
|
||||
accountId: reply.value.accountId,
|
||||
type: 'image',
|
||||
title: '',
|
||||
introduction: ''
|
||||
})
|
||||
|
||||
const beforeImageUpload = (rawFile: UploadRawFile) => useBeforeUpload(UploadType.Image, 2)(rawFile)
|
||||
|
||||
const onUploadSuccess = (res: any) => {
|
||||
if (res.code !== 0) {
|
||||
message.error('上传出错:' + res.msg)
|
||||
return false
|
||||
}
|
||||
|
||||
// 清空上传时的各种数据
|
||||
fileList.value = []
|
||||
uploadData.title = ''
|
||||
uploadData.introduction = ''
|
||||
|
||||
// 上传好的文件,本质是个素材,所以可以进行选中
|
||||
selectMaterial(res.data)
|
||||
}
|
||||
|
||||
const onDelete = () => {
|
||||
reply.value.mediaId = null
|
||||
reply.value.url = null
|
||||
reply.value.name = null
|
||||
}
|
||||
|
||||
const selectMaterial = (item) => {
|
||||
showDialog.value = false
|
||||
|
||||
// reply.value.type = 'image'
|
||||
reply.value.mediaId = item.mediaId
|
||||
reply.value.url = item.url
|
||||
reply.value.name = item.name
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.select-item {
|
||||
width: 280px;
|
||||
padding: 10px;
|
||||
margin: 0 auto 10px;
|
||||
border: 1px solid #eaeaea;
|
||||
|
||||
.material-img {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.item-name {
|
||||
overflow: hidden;
|
||||
font-size: 12px;
|
||||
text-align: center;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
|
||||
.item-infos {
|
||||
width: 30%;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.ope-row {
|
||||
padding-top: 10px;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
.col-select {
|
||||
width: 49.5%;
|
||||
height: 160px;
|
||||
padding: 50px 0;
|
||||
border: 1px solid rgb(234 234 234);
|
||||
}
|
||||
|
||||
.col-add {
|
||||
float: right;
|
||||
width: 49.5%;
|
||||
height: 160px;
|
||||
padding: 50px 0;
|
||||
border: 1px solid rgb(234 234 234);
|
||||
|
||||
.el-upload__tip {
|
||||
line-height: 18px;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
116
src/views/mp/components/wx-reply/components/TabMusic.vue
Normal file
116
src/views/mp/components/wx-reply/components/TabMusic.vue
Normal file
@@ -0,0 +1,116 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-row align="middle" justify="center">
|
||||
<el-col :span="6">
|
||||
<el-row align="middle" justify="center" class="thumb-div">
|
||||
<el-col :span="24">
|
||||
<el-row align="middle" justify="center">
|
||||
<img style="width: 100px" v-if="reply.thumbMediaUrl" :src="reply.thumbMediaUrl" />
|
||||
<icon v-else icon="ep:plus" />
|
||||
</el-row>
|
||||
<el-row align="middle" justify="center" style="margin-top: 2%">
|
||||
<div class="thumb-but">
|
||||
<el-upload
|
||||
:action="UPLOAD_URL"
|
||||
:headers="HEADERS"
|
||||
multiple
|
||||
:limit="1"
|
||||
:file-list="fileList"
|
||||
:data="uploadData"
|
||||
:before-upload="beforeImageUpload"
|
||||
:on-success="onUploadSuccess"
|
||||
>
|
||||
<template #trigger>
|
||||
<el-button type="primary" link>本地上传</el-button>
|
||||
</template>
|
||||
<el-button type="primary" link @click="showDialog = true" style="margin-left: 5px"
|
||||
>素材库选择
|
||||
</el-button>
|
||||
</el-upload>
|
||||
</div>
|
||||
</el-row>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-dialog
|
||||
title="选择图片"
|
||||
v-model="showDialog"
|
||||
width="80%"
|
||||
append-to-body
|
||||
destroy-on-close
|
||||
>
|
||||
<WxMaterialSelect
|
||||
type="image"
|
||||
:account-id="reply.accountId"
|
||||
@select-material="selectMaterial"
|
||||
/>
|
||||
</el-dialog>
|
||||
</el-col>
|
||||
<el-col :span="18">
|
||||
<el-input v-model="reply.title" placeholder="请输入标题" />
|
||||
<div style="margin: 20px 0"></div>
|
||||
<el-input v-model="reply.description" placeholder="请输入描述" />
|
||||
</el-col>
|
||||
</el-row>
|
||||
<div style="margin: 20px 0"></div>
|
||||
<el-input v-model="reply.musicUrl" placeholder="请输入音乐链接" />
|
||||
<div style="margin: 20px 0"></div>
|
||||
<el-input v-model="reply.hqMusicUrl" placeholder="请输入高质量音乐链接" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import WxMaterialSelect from '@/views/mp/components/wx-material-select'
|
||||
import type { UploadRawFile } from 'element-plus'
|
||||
import { UploadType, useBeforeUpload } from '@/views/mp/hooks/useUpload'
|
||||
import { getAccessToken } from '@/utils/auth'
|
||||
import { Reply } from './types'
|
||||
|
||||
const message = useMessage()
|
||||
|
||||
const UPLOAD_URL = import.meta.env.VITE_API_BASEPATH + '/admin-api/mp/material/upload-temporary'
|
||||
const HEADERS = { Authorization: 'Bearer ' + getAccessToken() } // 设置上传的请求头部
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: Reply
|
||||
}>()
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modelValue', v: Reply)
|
||||
}>()
|
||||
const reply = computed<Reply>({
|
||||
get: () => props.modelValue,
|
||||
set: (val) => emit('update:modelValue', val)
|
||||
})
|
||||
|
||||
const showDialog = ref(false)
|
||||
const fileList = ref([])
|
||||
const uploadData = reactive({
|
||||
accountId: reply.value.accountId,
|
||||
type: 'thumb', // 音乐类型为thumb
|
||||
title: '',
|
||||
introduction: ''
|
||||
})
|
||||
|
||||
const beforeImageUpload = (rawFile: UploadRawFile) => useBeforeUpload(UploadType.Image, 2)(rawFile)
|
||||
|
||||
const onUploadSuccess = (res: any) => {
|
||||
if (res.code !== 0) {
|
||||
message.error('上传出错:' + res.msg)
|
||||
return false
|
||||
}
|
||||
|
||||
// 清空上传时的各种数据
|
||||
fileList.value = []
|
||||
uploadData.title = ''
|
||||
uploadData.introduction = ''
|
||||
|
||||
// 上传好的文件,本质是个素材,所以可以进行选中
|
||||
selectMaterial(res.data)
|
||||
}
|
||||
|
||||
const selectMaterial = (item: any) => {
|
||||
showDialog.value = false
|
||||
|
||||
reply.value.thumbMediaId = item.mediaId
|
||||
reply.value.thumbMediaUrl = item.url
|
||||
}
|
||||
</script>
|
||||
76
src/views/mp/components/wx-reply/components/TabNews.vue
Normal file
76
src/views/mp/components/wx-reply/components/TabNews.vue
Normal file
@@ -0,0 +1,76 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-row>
|
||||
<div class="select-item" v-if="reply.articles && reply.articles.length > 0">
|
||||
<WxNews :articles="reply.articles" />
|
||||
<el-col class="ope-row">
|
||||
<el-button type="danger" circle @click="onDelete">
|
||||
<Icon icon="ep:delete" />
|
||||
</el-button>
|
||||
</el-col>
|
||||
</div>
|
||||
<!-- 选择素材 -->
|
||||
<el-col :span="24" v-if="!reply.content">
|
||||
<el-row style="text-align: center" align="middle">
|
||||
<el-col :span="24">
|
||||
<el-button type="success" @click="showDialog = true">
|
||||
{{ newsType === NewsType.Published ? '选择已发布图文' : '选择草稿箱图文' }}
|
||||
<Icon icon="ep:circle-check" />
|
||||
</el-button>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-col>
|
||||
<el-dialog title="选择图文" v-model="showDialog" width="90%" append-to-body destroy-on-close>
|
||||
<WxMaterialSelect
|
||||
type="news"
|
||||
:account-id="reply.accountId"
|
||||
:newsType="newsType"
|
||||
@select-material="selectMaterial"
|
||||
/>
|
||||
</el-dialog>
|
||||
</el-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import WxNews from '@/views/mp/components/wx-news'
|
||||
import WxMaterialSelect from '@/views/mp/components/wx-material-select'
|
||||
import { Reply, NewsType } from './types'
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: Reply
|
||||
newsType: NewsType
|
||||
}>()
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modelValue', v: Reply)
|
||||
}>()
|
||||
const reply = computed<Reply>({
|
||||
get: () => props.modelValue,
|
||||
set: (val) => emit('update:modelValue', val)
|
||||
})
|
||||
|
||||
const showDialog = ref(false)
|
||||
|
||||
const selectMaterial = (item: any) => {
|
||||
showDialog.value = false
|
||||
reply.value.articles = item.content.newsItem
|
||||
}
|
||||
|
||||
const onDelete = () => {
|
||||
reply.value.articles = []
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.select-item {
|
||||
width: 280px;
|
||||
padding: 10px;
|
||||
margin: 0 auto 10px;
|
||||
border: 1px solid #eaeaea;
|
||||
|
||||
.ope-row {
|
||||
padding-top: 10px;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
22
src/views/mp/components/wx-reply/components/TabText.vue
Normal file
22
src/views/mp/components/wx-reply/components/TabText.vue
Normal file
@@ -0,0 +1,22 @@
|
||||
<template>
|
||||
<el-input type="textarea" :rows="5" placeholder="请输入内容" v-model="content" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const props = defineProps<{
|
||||
modelValue?: string | null
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modelValue', v: string | null)
|
||||
(e: 'input', v: string | null)
|
||||
}>()
|
||||
|
||||
const content = computed<string | null | undefined>({
|
||||
get: () => props.modelValue,
|
||||
set: (val: string | null) => {
|
||||
emit('update:modelValue', val)
|
||||
emit('input', val)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
128
src/views/mp/components/wx-reply/components/TabVideo.vue
Normal file
128
src/views/mp/components/wx-reply/components/TabVideo.vue
Normal file
@@ -0,0 +1,128 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-row>
|
||||
<el-input v-model="reply.title" class="input-margin-bottom" placeholder="请输入标题" />
|
||||
<el-input class="input-margin-bottom" v-model="reply.description" placeholder="请输入描述" />
|
||||
<el-row class="ope-row" justify="center">
|
||||
<WxVideoPlayer v-if="reply.url" :url="reply.url" />
|
||||
</el-row>
|
||||
<el-col>
|
||||
<el-row style="text-align: center" align="middle">
|
||||
<!-- 选择素材 -->
|
||||
<el-col :span="12">
|
||||
<el-button type="success" @click="showDialog = true">
|
||||
素材库选择 <Icon icon="ep:circle-check" />
|
||||
</el-button>
|
||||
<el-dialog
|
||||
title="选择视频"
|
||||
v-model="showDialog"
|
||||
width="90%"
|
||||
append-to-body
|
||||
destroy-on-close
|
||||
>
|
||||
<WxMaterialSelect
|
||||
type="video"
|
||||
:account-id="reply.accountId"
|
||||
@select-material="selectMaterial"
|
||||
/>
|
||||
</el-dialog>
|
||||
</el-col>
|
||||
<!-- 文件上传 -->
|
||||
<el-col :span="12">
|
||||
<el-upload
|
||||
:action="UPLOAD_URL"
|
||||
:headers="HEADERS"
|
||||
multiple
|
||||
:limit="1"
|
||||
:file-list="fileList"
|
||||
:data="uploadData"
|
||||
:before-upload="beforeVideoUpload"
|
||||
:on-success="onUploadSuccess"
|
||||
>
|
||||
<el-button type="primary">新建视频 <Icon icon="ep:upload" /></el-button>
|
||||
</el-upload>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import WxVideoPlayer from '@/views/mp/components/wx-video-play'
|
||||
import WxMaterialSelect from '@/views/mp/components/wx-material-select'
|
||||
import type { UploadRawFile } from 'element-plus'
|
||||
import { UploadType, useBeforeUpload } from '@/views/mp/hooks/useUpload'
|
||||
import { getAccessToken } from '@/utils/auth'
|
||||
import { Reply } from './types'
|
||||
|
||||
const message = useMessage()
|
||||
|
||||
const UPLOAD_URL = import.meta.env.VITE_API_BASEPATH + '/admin-api/mp/material/upload-temporary'
|
||||
const HEADERS = { Authorization: 'Bearer ' + getAccessToken() }
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: Reply
|
||||
}>()
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modelValue', v: Reply)
|
||||
}>()
|
||||
const reply = computed<Reply>({
|
||||
get: () => props.modelValue,
|
||||
set: (val) => emit('update:modelValue', val)
|
||||
})
|
||||
|
||||
const showDialog = ref(false)
|
||||
const fileList = ref([])
|
||||
const uploadData = reactive({
|
||||
accountId: reply.value.accountId,
|
||||
type: 'video',
|
||||
title: '',
|
||||
introduction: ''
|
||||
})
|
||||
|
||||
const beforeVideoUpload = (rawFile: UploadRawFile) => useBeforeUpload(UploadType.Video, 10)(rawFile)
|
||||
|
||||
const onUploadSuccess = (res: any) => {
|
||||
if (res.code !== 0) {
|
||||
message.error('上传出错:' + res.msg)
|
||||
return false
|
||||
}
|
||||
|
||||
// 清空上传时的各种数据
|
||||
fileList.value = []
|
||||
uploadData.title = ''
|
||||
uploadData.introduction = ''
|
||||
|
||||
selectMaterial(res.data)
|
||||
}
|
||||
|
||||
/** 选择素材后设置 */
|
||||
const selectMaterial = (item: any) => {
|
||||
showDialog.value = false
|
||||
|
||||
reply.value.mediaId = item.mediaId
|
||||
reply.value.url = item.url
|
||||
reply.value.name = item.name
|
||||
|
||||
// title、introduction:从 item 到 tempObjItem,因为素材里有 title、introduction
|
||||
if (item.title) {
|
||||
reply.value.title = item.title || ''
|
||||
}
|
||||
if (item.introduction) {
|
||||
reply.value.description = item.introduction || ''
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.input-margin-bottom {
|
||||
margin-bottom: 2%;
|
||||
}
|
||||
|
||||
.ope-row {
|
||||
width: 100%;
|
||||
padding-top: 10px;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
160
src/views/mp/components/wx-reply/components/TabVoice.vue
Normal file
160
src/views/mp/components/wx-reply/components/TabVoice.vue
Normal file
@@ -0,0 +1,160 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="select-item2" v-if="reply.url">
|
||||
<p class="item-name">{{ reply.name }}</p>
|
||||
<el-row class="ope-row" justify="center">
|
||||
<WxVoicePlayer :url="reply.url" />
|
||||
</el-row>
|
||||
<el-row class="ope-row" justify="center">
|
||||
<el-button type="danger" circle @click="onDelete"><Icon icon="ep:delete" /></el-button>
|
||||
</el-row>
|
||||
</div>
|
||||
<el-row v-else style="text-align: center">
|
||||
<!-- 选择素材 -->
|
||||
<el-col :span="12" class="col-select">
|
||||
<el-button type="success" @click="showDialog = true">
|
||||
素材库选择<Icon icon="ep:circle-check" />
|
||||
</el-button>
|
||||
<el-dialog
|
||||
title="选择语音"
|
||||
v-model="showDialog"
|
||||
width="90%"
|
||||
append-to-body
|
||||
destroy-on-close
|
||||
>
|
||||
<WxMaterialSelect
|
||||
type="voice"
|
||||
:account-id="reply.accountId"
|
||||
@select-material="selectMaterial"
|
||||
/>
|
||||
</el-dialog>
|
||||
</el-col>
|
||||
<!-- 文件上传 -->
|
||||
<el-col :span="12" class="col-add">
|
||||
<el-upload
|
||||
:action="UPLOAD_URL"
|
||||
:headers="HEADERS"
|
||||
multiple
|
||||
:limit="1"
|
||||
:file-list="fileList"
|
||||
:data="uploadData"
|
||||
:before-upload="beforeVoiceUpload"
|
||||
:on-success="onUploadSuccess"
|
||||
>
|
||||
<el-button type="primary">点击上传</el-button>
|
||||
<template #tip>
|
||||
<div class="el-upload__tip">
|
||||
格式支持 mp3/wma/wav/amr,文件大小不超过 2M,播放长度不超过 60s
|
||||
</div>
|
||||
</template>
|
||||
</el-upload>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import WxMaterialSelect from '@/views/mp/components/wx-material-select'
|
||||
import WxVoicePlayer from '@/views/mp/components/wx-voice-play'
|
||||
import { UploadType, useBeforeUpload } from '@/views/mp/hooks/useUpload'
|
||||
import type { UploadRawFile } from 'element-plus'
|
||||
import { getAccessToken } from '@/utils/auth'
|
||||
import { Reply } from './types'
|
||||
const message = useMessage()
|
||||
|
||||
const UPLOAD_URL = import.meta.env.VITE_API_BASEPATH + '/admin-api/mp/material/upload-temporary'
|
||||
const HEADERS = { Authorization: 'Bearer ' + getAccessToken() } // 设置上传的请求头部
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: Reply
|
||||
}>()
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modelValue', v: Reply)
|
||||
}>()
|
||||
const reply = computed<Reply>({
|
||||
get: () => props.modelValue,
|
||||
set: (val) => emit('update:modelValue', val)
|
||||
})
|
||||
|
||||
const showDialog = ref(false)
|
||||
const fileList = ref([])
|
||||
const uploadData = reactive({
|
||||
accountId: reply.value.accountId,
|
||||
type: 'voice',
|
||||
title: '',
|
||||
introduction: ''
|
||||
})
|
||||
|
||||
const beforeVoiceUpload = (rawFile: UploadRawFile) => useBeforeUpload(UploadType.Voice, 10)(rawFile)
|
||||
|
||||
const onUploadSuccess = (res: any) => {
|
||||
if (res.code !== 0) {
|
||||
message.error('上传出错:' + res.msg)
|
||||
return false
|
||||
}
|
||||
|
||||
// 清空上传时的各种数据
|
||||
fileList.value = []
|
||||
uploadData.title = ''
|
||||
uploadData.introduction = ''
|
||||
|
||||
// 上传好的文件,本质是个素材,所以可以进行选中
|
||||
selectMaterial(res.data)
|
||||
}
|
||||
|
||||
const onDelete = () => {
|
||||
reply.value.mediaId = null
|
||||
reply.value.url = null
|
||||
reply.value.name = null
|
||||
}
|
||||
|
||||
const selectMaterial = (item: Reply) => {
|
||||
showDialog.value = false
|
||||
|
||||
// reply.value.type = ReplyType.Voice
|
||||
reply.value.mediaId = item.mediaId
|
||||
reply.value.url = item.url
|
||||
reply.value.name = item.name
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.select-item2 {
|
||||
padding: 10px;
|
||||
margin: 0 auto 10px;
|
||||
border: 1px solid #eaeaea;
|
||||
|
||||
.item-name {
|
||||
overflow: hidden;
|
||||
font-size: 12px;
|
||||
text-align: center;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
|
||||
.ope-row {
|
||||
width: 100%;
|
||||
padding-top: 10px;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
.col-select {
|
||||
width: 49.5%;
|
||||
height: 160px;
|
||||
padding: 50px 0;
|
||||
border: 1px solid rgb(234 234 234);
|
||||
}
|
||||
|
||||
.col-add {
|
||||
float: right;
|
||||
width: 49.5%;
|
||||
height: 160px;
|
||||
padding: 50px 0;
|
||||
border: 1px solid rgb(234 234 234);
|
||||
|
||||
.el-upload__tip {
|
||||
line-height: 18px;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
54
src/views/mp/components/wx-reply/components/types.ts
Normal file
54
src/views/mp/components/wx-reply/components/types.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
enum ReplyType {
|
||||
News = 'news',
|
||||
Image = 'image',
|
||||
Voice = 'voice',
|
||||
Video = 'video',
|
||||
Music = 'music',
|
||||
Text = 'text'
|
||||
}
|
||||
|
||||
interface _Reply {
|
||||
accountId: number
|
||||
type: ReplyType
|
||||
name?: string | null
|
||||
content?: string | null
|
||||
mediaId?: string | null
|
||||
url?: string | null
|
||||
title?: string | null
|
||||
description?: string | null
|
||||
thumbMediaId?: string | null
|
||||
thumbMediaUrl?: string | null
|
||||
musicUrl?: string | null
|
||||
hqMusicUrl?: string | null
|
||||
introduction?: string | null
|
||||
articles?: any[]
|
||||
}
|
||||
|
||||
type Reply = _Reply //Partial<_Reply>
|
||||
|
||||
enum NewsType {
|
||||
Published = '1',
|
||||
Draft = '2'
|
||||
}
|
||||
|
||||
/** 利用旧的reply[accountId, type]初始化新的Reply */
|
||||
const createEmptyReply = (old: Reply | Ref<Reply>): Reply => {
|
||||
return {
|
||||
accountId: unref(old).accountId,
|
||||
type: unref(old).type,
|
||||
name: null,
|
||||
content: null,
|
||||
mediaId: null,
|
||||
url: null,
|
||||
title: null,
|
||||
description: null,
|
||||
thumbMediaId: null,
|
||||
thumbMediaUrl: null,
|
||||
musicUrl: null,
|
||||
hqMusicUrl: null,
|
||||
introduction: null,
|
||||
articles: []
|
||||
}
|
||||
}
|
||||
|
||||
export { Reply, NewsType, ReplyType, createEmptyReply }
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user