@@ -2,7 +2,7 @@
VITE_APP_TITLE=中星
# 项目本地运行端口号
-VITE_PORT=80
+VITE_PORT=9001
# open 运行 npm run dev 时自动打开浏览器
VITE_OPEN=true
@@ -3,11 +3,11 @@ NODE_ENV=development
VITE_DEV=true
-# 请求路径
-VITE_BASE_URL='http://10.0.5.228:8001'
+# 请求路径o
+VITE_BASE_URL='https://zxsh.newfeifan.cn'
# 上传路径
-VITE_UPLOAD_URL='http://10.0.5.228:8001/admin-api/infra/file/upload'
+VITE_UPLOAD_URL='https://zxsh.newfeifan.cn/admin-api/infra/file/upload'
# 接口前缀
VITE_API_BASEPATH=/dev-api
@@ -4,10 +4,10 @@ NODE_ENV=production
VITE_DEV=false
# 请求路径
VITE_API_BASEPATH=
-VITE_BASE_URL='http://10.0.5.228:48080'
+VITE_BASE_URL='https://zxpt.newfeifan.cn'
-VITE_UPLOAD_URL='http://10.0.5.228:48080/admin-api/infra/file/upload'
+VITE_UPLOAD_URL='https://zxpt.newfeifan.cn/admin-api/infra/file/upload'
@@ -22,7 +22,7 @@ const setDefaultTheme = () => {
}
appStore.setIsDark(isDarkTheme)
-setDefaultTheme()
+// setDefaultTheme()
</script>
<template>
<ConfigGlobal :size="currentSize">
@@ -0,0 +1 @@
+<svg id="图层_1" data-name="图层 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 811.6 648.6"><defs><style>.cls-1{fill:#b4b5b5;}</style></defs><path class="cls-1" d="M921.8,1045a48,48,0,0,1-48-48V439.1a42.74,42.74,0,0,1,42.7-42.7h202.7a42.77,42.77,0,0,1,30.2,12.5l83.3,83.3c5.7,5.5,8.8,6.5,16.2,6.6h393.8c34.5.5,42.6,22.3,42.7,45.9V999.2a45.75,45.75,0,0,1-45.7,45.7l-717.9.1Z" transform="translate(-873.8 -396.4)"/></svg>
+<svg id="图层_1" data-name="图层 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 811.52 648.7"><defs><style>.cls-1{fill:#b4b5b5;}</style></defs><path class="cls-1" d="M873.9,933.9V439.1a42.74,42.74,0,0,1,42.7-42.7h202.7a42.77,42.77,0,0,1,30.2,12.5l83.3,83.3c5.7,5.5,8.8,6.5,16.2,6.6h299c23.6,0,42.6-.9,42.7,22.7v16.7H1009.8c-26.6,0-56.1,40.8-57.2,44.9Zm768.6-371.5H1012.9a42.79,42.79,0,0,0-41.2,31.5L883.3,991.2a42.75,42.75,0,0,0,41.2,53.9h629.6a42.79,42.79,0,0,0,41.2-31.5l88.4-397.3c2.7-9.8,3.1-25-5.4-36.6C1668.5,566.4,1651.7,562.3,1642.5,562.4Z" transform="translate(-873.9 -396.4)"/></svg>
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 27.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<svg version="1.1" id="图层_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 861.6 698.6" style="enable-background:new 0 0 861.6 698.6;" xml:space="preserve">
+<style type="text/css">
+ .st0{fill:#555C6C;}
+</style>
+<path class="st0" d="M73,698.6L73,698.6c-40.3,0-73-32.7-73-73V67.7C0,30.4,30.4,0,67.7,0h202.7c18.1,0,35.1,7,47.9,19.8l82.6,82.6
+ l393.4,0c29.8,0.4,45.8,13.4,53.9,24.3c9,12,13.4,27.2,13.5,46.5v454.6c0,39-31.7,70.7-70.7,70.7L73,698.6z M67.7,50
+ C58,50,50,58,50,67.7v557.9c0,12.7,10.3,23,23,23l717.9-0.1c11.4,0,20.7-9.3,20.7-20.7V173.4c-0.1-18.7-5.2-20.8-17.9-21l-394,0
+ c-14.1-0.2-23.1-3.9-33.2-13.6l-0.3-0.3l-83.3-83.3c-3.3-3.3-7.8-5.2-12.5-5.2H67.7z M402.4,103.9
+ C402.4,103.9,402.4,103.9,402.4,103.9S402.4,103.9,402.4,103.9z"/>
+</svg>
@@ -0,0 +1,17 @@
+ viewBox="0 0 852.8 698.6" style="enable-background:new 0 0 852.8 698.6;" xml:space="preserve">
+<path class="st0" d="M127.9,698.6c-37.4-0.1-67.7-30.5-67.7-67.9c0-5.7,0.8-11.5,2.2-17l79.8-348.4l0.1-0.5
+ c8-29.3,34.9-49.8,65.3-49.9H785c16.5,0,41.1,7.2,55.9,27.6c16.6,22.7,11.7,48.9,9.5,57.4l-79.8,348.4l-0.1,0.5
+ c-8,29.3-34.9,49.8-65.3,49.9H127.9z M190.7,277.6L111,625.8l-0.1,0.5c-0.4,1.5-0.6,3-0.6,4.6c0,9.8,7.9,17.8,17.7,17.8h577.2
+ c7.8,0,14.7-5.2,17-12.7l79.8-348.2l0.1-0.5c1.3-4.8,1.2-11.5-1.5-15.2c-3.9-5.3-12.3-7.1-15.5-7.1H207.7
+ C199.9,264.9,193,270.1,190.7,277.6z M25.2,585.4c-3.2,0-6.5-0.6-9.6-1.9l-0.2-0.1C6.1,579.4,0,570.3,0,560.1V67.7
+ C0,30.4,30.4,0,67.7,0h202.7c17.7,0,37.7,2.2,69.7,27.9l0.1,0.1l60.3,49.1l0.3,0.3l300.7,0c13.4,0,26,0,37.6,4.5
+ c10.3,4,27.5,15,27.6,43.1v41.8H160.9c-13.3,0-25.9,18.6-32.3,47.5L49.4,567.8l-0.8,2C44.6,579.5,35.1,585.4,25.2,585.4z M67.7,50
+ C58,50,50,58,50,67.7v268.8l29.8-133c11.9-54.2,42.3-86.6,81.1-86.6h209c-0.8-0.7-1.6-1.4-2.5-2.2l-58.7-47.8
+ C287.8,50,278.9,50,270.4,50H67.7z M402.4,78.9L402.4,78.9L402.4,78.9z"/>
@@ -5,7 +5,7 @@
<el-avatar :size="60">
<Icon icon="ep:avatar" :size="60" />
</el-avatar>
- <span class="text-18px font-bold">芋道源码</span>
+ <span class="text-18px font-bold">非繁源码</span>
</div>
<Icon icon="tdesign:qrcode" :size="20" />
@@ -1,5 +1,6 @@
-import UploadImg from './src/UploadImg.vue'
-import UploadImgs from './src/UploadImgs.vue'
-import UploadFile from './src/UploadFile.vue'
-
-export { UploadImg, UploadImgs, UploadFile }
+import SPuUploadImg from './src/SPuUploadImg.vue'
+import UploadImg from './src/UploadImg.vue'
+import UploadImgs from './src/UploadImgs.vue'
+import UploadFile from './src/UploadFile.vue'
+
+export { UploadImg, SPuUploadImg, UploadImgs, UploadFile }
@@ -0,0 +1,268 @@
+<template>
+ <div class="upload-box">
+ <el-upload :id="uuid" :accept="fileType.join(',')" :action="updateUrl" :before-upload="beforeUpload"
+ :class="['upload', drag ? 'no-border' : '']" :drag="drag" :headers="uploadHeaders" :multiple="false"
+ :on-error="uploadError" :on-success="uploadSuccess" :show-file-list="false">
+ <template v-if="modelValue">
+ <img :src="modelValue" class="upload-image" />
+ <div class="upload-handle" @click.stop>
+ <div class="handle-icon" @click="editImg" v-if="!disabled">
+ <Icon icon="ep:edit" />
+ <span v-if="showBtnText">{{ t('action.edit') }}</span>
+ </div>
+ <div class="handle-icon" @click="imagePreview(modelValue)">
+ <Icon icon="ep:zoom-in" />
+ <span v-if="showBtnText">{{ t('action.detail') }}</span>
+ <div v-if="showDelete && !disabled" class="handle-icon" @click="deleteImg">
+ <Icon icon="ep:delete" />
+ <span v-if="showBtnText">{{ t('action.del') }}</span>
+ </template>
+ <template v-else>
+ <div class="upload-empty">
+ <slot name="empty">
+ <Icon icon="ep:plus" />
+ <!-- <span>请上传图片</span> -->
+ </slot>
+ </el-upload>
+ <div class="el-upload__tip">
+ <slot name="tip"></slot>
+</template>
+<script lang="ts" setup>
+ import type { UploadProps } from 'element-plus'
+ import { generateUUID } from '@/utils'
+ import { propTypes } from '@/utils/propTypes'
+ import { getAccessToken, getTenantId } from '@/utils/auth'
+ import { createImageViewer } from '@/components/ImageViewer'
+ defineOptions({ name: 'UploadImg' })
+ type FileTypes =
+ | 'image/apng'
+ | 'image/bmp'
+ | 'image/gif'
+ | 'image/jpeg'
+ | 'image/pjpeg'
+ | 'image/png'
+ | 'image/svg+xml'
+ | 'image/tiff'
+ | 'image/webp'
+ | 'image/x-icon'
+ // 接受父组件参数
+ const props = defineProps({
+ modelValue: propTypes.string.def(''),
+ updateUrl: propTypes.string.def(import.meta.env.VITE_UPLOAD_URL),
+ drag: propTypes.bool.def(true), // 是否支持拖拽上传 ==> 非必传(默认为 true)
+ disabled: propTypes.bool.def(false), // 是否禁用上传组件 ==> 非必传(默认为 false)
+ fileSize: propTypes.number.def(5), // 图片大小限制 ==> 非必传(默认为 5M)
+ fileType: propTypes.array.def(['image/jpeg', 'image/png', 'image/gif']), // 图片类型限制 ==> 非必传(默认为 ["image/jpeg", "image/png", "image/gif"])
+ height: propTypes.string.def('150px'), // 组件高度 ==> 非必传(默认为 150px)
+ width: propTypes.string.def('150px'), // 组件宽度 ==> 非必传(默认为 150px)
+ borderradius: propTypes.string.def('8px'), // 组件边框圆角 ==> 非必传(默认为 8px)
+ // 是否显示删除按钮
+ showDelete: propTypes.bool.def(true),
+ // 是否显示按钮文字
+ showBtnText: propTypes.bool.def(true)
+ })
+ const { t } = useI18n() // 国际化
+ const message = useMessage() // 消息弹窗
+ // 生成组件唯一id
+ const uuid = ref('id-' + generateUUID())
+ // 查看图片
+ const imagePreview = (imgUrl : string) => {
+ createImageViewer({
+ zIndex: 9999999,
+ urlList: [imgUrl]
+ }
+ const emit = defineEmits(['update:modelValue'])
+ const deleteImg = () => {
+ emit('update:modelValue', '')
+ const uploadHeaders = ref({
+ Authorization: 'Bearer ' + getAccessToken(),
+ 'tenant-id': getTenantId()
+ const editImg = () => {
+ const dom = document.querySelector(`#${uuid.value} .el-upload__input`)
+ dom && dom.dispatchEvent(new MouseEvent('click'))
+ const beforeUpload : UploadProps['beforeUpload'] = (rawFile) => {
+ const imgSize = rawFile.size / 1024 / 1024 < props.fileSize
+ const imgType = props.fileType
+ if (!imgType.includes(rawFile.type as FileTypes))
+ message.notifyWarning('上传图片不符合所需的格式!')
+ if (!imgSize) message.notifyWarning(`上传图片大小不能超过 ${props.fileSize}M!`)
+ return imgType.includes(rawFile.type as FileTypes) && imgSize
+ // 图片上传成功提示
+ const uploadSuccess : UploadProps['onSuccess'] = (res : any) : void => {
+ message.success('上传成功')
+ emit('update:modelValue', res.data)
+ // 图片上传错误提示
+ const uploadError = () => {
+ message.notifyError('图片上传失败,请您重新上传!')
+</script>
+<style lang="scss" scoped>
+ .is-error {
+ .upload {
+ :deep(.el-upload),
+ :deep(.el-upload-dragger) {
+ border: 1px dashed var(--el-color-danger) !important;
+ &:hover {
+ border-color: var(--el-color-primary) !important;
+ :deep(.disabled) {
+ .el-upload,
+ .el-upload-dragger {
+ cursor: not-allowed !important;
+ background: var(--el-disabled-bg-color);
+ border: 1px dashed var(--el-border-color-darker) !important;
+ .upload-box {
+ .no-border {
+ :deep(.el-upload) {
+ border: none !important;
+ :deep(.upload) {
+ .el-upload {
+ position: relative;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ // width: v-bind(width);
+ height: 60px;
+ overflow: hidden;
+ border: 1px dashed var(--el-border-color-darker);
+ // border-radius: v-bind(borderradius);
+ transition: var(--el-transition-duration-fast);
+ border-color: var(--el-color-primary);
+ .upload-handle {
+ opacity: 1;
+ width: 100%;
+ height: 100%;
+ padding: 0;
+ background-color: transparent;
+ border: none;
+ border-radius: 0;
+ border: 1px dashed var(--el-color-primary);
+ .el-upload-dragger.is-dragover {
+ background-color: var(--el-color-primary-light-9);
+ border: 2px dashed var(--el-color-primary) !important;
+ .upload-image {
+ object-fit: contain;
+ .upload-empty {
+ flex-direction: column;
+ font-size: 12px;
+ line-height: 30px;
+ color: var(--el-color-info);
+ .el-icon {
+ font-size: 28px;
+ color: var(--el-text-color-secondary);
+ position: absolute;
+ top: 0;
+ right: 0;
+ cursor: pointer;
+ background: rgb(0 0 0 / 60%);
+ opacity: 0;
+ box-sizing: border-box;
+ .handle-icon {
+ padding: 0 6%;
+ color: aliceblue;
+ margin-bottom: 40%;
+ font-size: 130%;
+ line-height: 130%;
+ span {
+ font-size: 85%;
+ line-height: 85%;
+ .el-upload__tip {
+ line-height: 18px;
+ text-align: center;
@@ -664,7 +664,7 @@ const previewProcessJson = () => {
previewModelVisible.value = true
})
-/* ------------------------------------------------ 芋道源码 methods ------------------------------------------------------ */
+/* ------------------------------------------------ 非繁源码 methods ------------------------------------------------------ */
const processSave = async () => {
console.log(bpmnModeler, 'bpmnModelerbpmnModelerbpmnModelerbpmnModeler')
const { err, xml } = await bpmnModeler.saveXML()
@@ -1,72 +1,71 @@
-<script lang="ts" setup>
-import { useTagsViewStore } from '@/store/modules/tagsView'
-import { useAppStore } from '@/store/modules/app'
-import { Footer } from '@/layout/components/Footer'
-defineOptions({ name: 'AppView' })
-const appStore = useAppStore()
-const layout = computed(() => appStore.getLayout)
-const fixedHeader = computed(() => appStore.getFixedHeader)
-const footer = computed(() => appStore.getFooter)
-const tagsViewStore = useTagsViewStore()
-const getCaches = computed((): string[] => {
- return tagsViewStore.getCachedViews
-})
-const tagsView = computed(() => appStore.getTagsView)
-//region 无感刷新
-const routerAlive = ref(true)
-// 无感刷新,防止出现页面闪烁白屏
-const reload = () => {
- routerAlive.value = false
- nextTick(() => (routerAlive.value = true))
-}
-// 为组件后代提供刷新方法
-provide('reload', reload)
-//endregion
-</script>
-<template>
- <section
- :class="[
- 'p-[var(--app-content-padding)] w-[calc(100%-var(--app-content-padding)-var(--app-content-padding))] bg-[var(--app-content-bg-color)] dark:bg-[var(--el-bg-color)]',
- {
- '!min-h-[calc(100%-var(--app-content-padding)-var(--app-content-padding)-var(--app-footer-height))]':
- (fixedHeader &&
- (layout === 'classic' || layout === 'topLeft' || layout === 'top') &&
- footer) ||
- (!tagsView && layout === 'top' && footer),
- '!min-h-[calc(100%-var(--app-content-padding)-var(--app-content-padding)-var(--app-footer-height)-var(--tags-view-height))]':
- tagsView && layout === 'top' && footer,
- '!min-h-[calc(100%-var(--tags-view-height)-var(--app-content-padding)-var(--app-content-padding)-var(--top-tool-height)-var(--app-footer-height))]':
- !fixedHeader && layout === 'classic' && footer,
- '!min-h-[calc(100%-var(--tags-view-height)-var(--app-content-padding)-var(--app-content-padding)-var(--app-footer-height))]':
- !fixedHeader && layout === 'topLeft' && footer,
- '!min-h-[calc(100%-var(--top-tool-height)-var(--app-content-padding)-var(--app-content-padding))]':
- fixedHeader && layout === 'cutMenu' && footer,
- '!min-h-[calc(100%-var(--top-tool-height)-var(--app-content-padding)-var(--app-content-padding)-var(--tags-view-height))]':
- !fixedHeader && layout === 'cutMenu' && footer
- }
- ]"
- >
- <router-view v-if="routerAlive">
- <template #default="{ Component, route }">
- <keep-alive :include="getCaches">
- <component :is="Component" :key="route.fullPath" />
- </keep-alive>
- </template>
- </router-view>
- </section>
- <Footer v-if="footer" />
-</template>
+ import { useTagsViewStore } from '@/store/modules/tagsView'
+ import { useAppStore } from '@/store/modules/app'
+ import { Footer } from '@/layout/components/Footer'
+ defineOptions({ name: 'AppView' })
+ const appStore = useAppStore()
+ const layout = computed(() => appStore.getLayout)
+ const fixedHeader = computed(() => appStore.getFixedHeader)
+ const footer = computed(() => appStore.getFooter)
+ const tagsViewStore = useTagsViewStore()
+ const getCaches = computed(() : string[] => {
+ return tagsViewStore.getCachedViews
+ const tagsView = computed(() => appStore.getTagsView)
+ //region 无感刷新
+ const routerAlive = ref(true)
+ // 无感刷新,防止出现页面闪烁白屏
+ const reload = () => {
+ routerAlive.value = false
+ nextTick(() => (routerAlive.value = true))
+ // 为组件后代提供刷新方法
+ provide('reload', reload)
+//endregion
+ <section :class="[
+ 'p-[var(--app-content-padding)] w-[calc(100%-var(--app-content-padding)-var(--app-content-padding))] bg-[var(--app-content-bg-color)] dark:bg-[var(--el-bg-color)]',
+ {
+ '!min-h-[calc(100%-var(--app-content-padding)-var(--app-content-padding)-var(--app-footer-height))]':
+ (fixedHeader &&
+ (layout === 'classic' || layout === 'topLeft' || layout === 'top') &&
+ footer) ||
+ (!tagsView && layout === 'top' && footer),
+ '!min-h-[calc(100%-var(--app-content-padding)-var(--app-content-padding)-var(--app-footer-height)-var(--tags-view-height))]':
+ tagsView && layout === 'top' && footer,
+ '!min-h-[calc(100%-var(--tags-view-height)-var(--app-content-padding)-var(--app-content-padding)-var(--top-tool-height)-var(--app-footer-height))]':
+ !fixedHeader && layout === 'classic' && footer,
+ '!min-h-[calc(100%-var(--tags-view-height)-var(--app-content-padding)-var(--app-content-padding)-var(--app-footer-height))]':
+ !fixedHeader && layout === 'topLeft' && footer,
+ '!min-h-[calc(100%-var(--top-tool-height)-var(--app-content-padding)-var(--app-content-padding))]':
+ fixedHeader && layout === 'cutMenu' && footer,
+ '!min-h-[calc(100%-var(--top-tool-height)-var(--app-content-padding)-var(--app-content-padding)-var(--tags-view-height))]':
+ !fixedHeader && layout === 'cutMenu' && footer
+ ]">
+ <router-view v-if="routerAlive">
+ <template #default="{ Component, route }">
+ <keep-alive :include="getCaches">
+ <component :is="Component" :key="route.fullPath" />
+ </keep-alive>
+ </router-view>
+ </section>
+ <Footer v-if="footer" />
@@ -1,24 +1,22 @@
-import { useDesign } from '@/hooks/web/useDesign'
-// eslint-disable-next-line vue/no-reserved-component-names
-defineOptions({ name: 'Footer' })
-const { getPrefixCls } = useDesign()
-const prefixCls = getPrefixCls('footer')
-const title = computed(() => appStore.getTitle)
- <div
- :class="prefixCls"
- class="h-[var(--app-footer-height)] bg-[var(--app-content-bg-color)] text-center leading-[var(--app-footer-height)] text-[var(--el-text-color-placeholder)] dark:bg-[var(--el-bg-color)]"
- <span class="text-14px">Copyright ©2022-{{ title }}</span>
- </div>
+ import { useDesign } from '@/hooks/web/useDesign'
+ // eslint-disable-next-line vue/no-reserved-component-names
+ defineOptions({ name: 'Footer' })
+ const { getPrefixCls } = useDesign()
+ const prefixCls = getPrefixCls('footer')
+ const title = computed(() => appStore.getTitle)
+ <div :class="prefixCls"
+ class="h-[var(--app-footer-height)] bg-[var(--app-content-bg-color)] text-center leading-[var(--app-footer-height)] text-[var(--el-text-color-placeholder)] dark:bg-[var(--el-bg-color)]">
+ <span class="text-14px">Copyright ©2024-{{ title }}</span>
@@ -1,32 +1,29 @@
-import { Icon } from '@/components/Icon'
-import { useFullscreen } from '@vueuse/core'
-import { propTypes } from '@/utils/propTypes'
-defineOptions({ name: 'ScreenFull' })
-const prefixCls = getPrefixCls('screenfull')
-defineProps({
- color: propTypes.string.def('')
-const { toggle, isFullscreen } = useFullscreen()
-const toggleFullscreen = () => {
- toggle()
- <div :class="prefixCls" @click="toggleFullscreen">
- <Icon
- :color="color"
- :icon="isFullscreen ? 'zmdi:fullscreen-exit' : 'zmdi:fullscreen'"
- :size="18"
- />
+ import { Icon } from '@/components/Icon'
+ import { useFullscreen } from '@vueuse/core'
+ defineOptions({ name: 'ScreenFull' })
+ const prefixCls = getPrefixCls('screenfull')
+ defineProps({
+ color: propTypes.string.def('')
+ const { toggle, isFullscreen } = useFullscreen()
+ const toggleFullscreen = () => {
+ toggle()
+ <div :class="prefixCls" @click="toggleFullscreen">
+ <Icon :color="color" :icon="isFullscreen ? 'zmdi:fullscreen-exit' : 'zmdi:fullscreen'" :size="18" />
@@ -1,299 +1,287 @@
-import { ElMessage } from 'element-plus'
-import { useClipboard, useCssVar } from '@vueuse/core'
-import { CACHE_KEY, useCache } from '@/hooks/web/useCache'
-import { setCssVar, trim } from '@/utils'
-import { colorIsDark, hexToRGB, lighten } from '@/utils/color'
-import { ThemeSwitch } from '@/layout/components/ThemeSwitch'
-import ColorRadioPicker from './components/ColorRadioPicker.vue'
-import InterfaceDisplay from './components/InterfaceDisplay.vue'
-import LayoutRadioPicker from './components/LayoutRadioPicker.vue'
-defineOptions({ name: 'Setting' })
-const { t } = useI18n()
-const prefixCls = getPrefixCls('setting')
-const drawer = ref(false)
-// 主题色相关
-const systemTheme = ref(appStore.getTheme.elColorPrimary)
-const setSystemTheme = (color: string) => {
- setCssVar('--el-color-primary', color)
- appStore.setTheme({ elColorPrimary: color })
- const leftMenuBgColor = useCssVar('--left-menu-bg-color', document.documentElement)
- setMenuTheme(trim(unref(leftMenuBgColor)))
-// 头部主题相关
-const headerTheme = ref(appStore.getTheme.topHeaderBgColor || '')
-const setHeaderTheme = (color: string) => {
- const isDarkColor = colorIsDark(color)
- const textColor = isDarkColor ? '#fff' : 'inherit'
- const textHoverColor = isDarkColor ? lighten(color!, 6) : '#f6f6f6'
- const topToolBorderColor = isDarkColor ? color : '#eee'
- setCssVar('--top-header-bg-color', color)
- setCssVar('--top-header-text-color', textColor)
- setCssVar('--top-header-hover-color', textHoverColor)
- appStore.setTheme({
- topHeaderBgColor: color,
- topHeaderTextColor: textColor,
- topHeaderHoverColor: textHoverColor,
- topToolBorderColor
- })
- if (unref(layout) === 'top') {
- setMenuTheme(color)
-// 菜单主题相关
-const menuTheme = ref(appStore.getTheme.leftMenuBgColor || '')
-const setMenuTheme = (color: string) => {
- const primaryColor = useCssVar('--el-color-primary', document.documentElement)
- const theme: Recordable = {
- // 左侧菜单边框颜色
- leftMenuBorderColor: isDarkColor ? 'inherit' : '#eee',
- // 左侧菜单背景颜色
- leftMenuBgColor: color,
- // 左侧菜单浅色背景颜色
- leftMenuBgLightColor: isDarkColor ? lighten(color!, 6) : color,
- // 左侧菜单选中背景颜色
- leftMenuBgActiveColor: isDarkColor
- ? 'var(--el-color-primary)'
- : hexToRGB(unref(primaryColor), 0.1),
- // 左侧菜单收起选中背景颜色
- leftMenuCollapseBgActiveColor: isDarkColor
- // 左侧菜单字体颜色
- leftMenuTextColor: isDarkColor ? '#bfcbd9' : '#333',
- // 左侧菜单选中字体颜色
- leftMenuTextActiveColor: isDarkColor ? '#fff' : 'var(--el-color-primary)',
- // logo字体颜色
- logoTitleTextColor: isDarkColor ? '#fff' : 'inherit',
- // logo边框颜色
- logoBorderColor: isDarkColor ? color : '#eee'
- appStore.setTheme(theme)
- appStore.setCssVarTheme()
-if (layout.value === 'top' && !appStore.getIsDark) {
- headerTheme.value = '#fff'
- setHeaderTheme('#fff')
-// 监听layout变化,重置一些主题色
-watch(
- () => layout.value,
- (n) => {
- if (n === 'top' && !appStore.getIsDark) {
- } else {
- setMenuTheme(unref(menuTheme))
-)
-// 拷贝
-const copyConfig = async () => {
- const { copy, copied, isSupported } = useClipboard({
- source: `
- // 面包屑
- breadcrumb: ${appStore.getBreadcrumb},
- // 面包屑图标
- breadcrumbIcon: ${appStore.getBreadcrumbIcon},
- // 折叠图标
- hamburger: ${appStore.getHamburger},
- // 全屏图标
- screenfull: ${appStore.getScreenfull},
- // 尺寸图标
- size: ${appStore.getSize},
- // 多语言图标
- locale: ${appStore.getLocale},
- // 消息图标
- message: ${appStore.getMessage},
- // 标签页
- tagsView: ${appStore.getTagsView},
- // 标签页图标
- getTagsViewIcon: ${appStore.getTagsViewIcon},
- // logo
- logo: ${appStore.getLogo},
- // 菜单手风琴
- uniqueOpened: ${appStore.getUniqueOpened},
- // 固定header
- fixedHeader: ${appStore.getFixedHeader},
- // 页脚
- footer: ${appStore.getFooter},
- // 灰色模式
- greyMode: ${appStore.getGreyMode},
- // layout布局
- layout: '${appStore.getLayout}',
- // 暗黑模式
- isDark: ${appStore.getIsDark},
- // 组件尺寸
- currentSize: '${appStore.getCurrentSize}',
- // 主题相关
- theme: {
- // 主题色
- elColorPrimary: '${appStore.getTheme.elColorPrimary}',
- leftMenuBorderColor: '${appStore.getTheme.leftMenuBorderColor}',
- leftMenuBgColor: '${appStore.getTheme.leftMenuBgColor}',
- leftMenuBgLightColor: '${appStore.getTheme.leftMenuBgLightColor}',
- leftMenuBgActiveColor: '${appStore.getTheme.leftMenuBgActiveColor}',
- leftMenuCollapseBgActiveColor: '${appStore.getTheme.leftMenuCollapseBgActiveColor}',
- leftMenuTextColor: '${appStore.getTheme.leftMenuTextColor}',
- leftMenuTextActiveColor: '${appStore.getTheme.leftMenuTextActiveColor}',
- logoTitleTextColor: '${appStore.getTheme.logoTitleTextColor}',
- logoBorderColor: '${appStore.getTheme.logoBorderColor}',
- // 头部背景颜色
- topHeaderBgColor: '${appStore.getTheme.topHeaderBgColor}',
- // 头部字体颜色
- topHeaderTextColor: '${appStore.getTheme.topHeaderTextColor}',
- // 头部悬停颜色
- topHeaderHoverColor: '${appStore.getTheme.topHeaderHoverColor}',
- // 头部边框颜色
- topToolBorderColor: '${appStore.getTheme.topToolBorderColor}'
- `
- if (!isSupported) {
- ElMessage.error(t('setting.copyFailed'))
- await copy()
- if (unref(copied)) {
- ElMessage.success(t('setting.copySuccess'))
-// 清空缓存
-const clear = () => {
- const { wsCache } = useCache()
- wsCache.delete(CACHE_KEY.LAYOUT)
- wsCache.delete(CACHE_KEY.THEME)
- wsCache.delete(CACHE_KEY.IS_DARK)
- window.location.reload()
- class="fixed right-0 top-[45%] h-40px w-40px cursor-pointer bg-[var(--el-color-primary)] text-center leading-40px"
- @click="drawer = true"
- <Icon color="#fff" icon="ep:setting" />
- <ElDrawer v-model="drawer" :z-index="4000" direction="rtl" size="350px">
- <template #header>
- <span class="text-16px font-700">{{ t('setting.projectSetting') }}</span>
- <div class="text-center">
- <!-- 主题 -->
- <ElDivider>{{ t('setting.theme') }}</ElDivider>
- <ThemeSwitch />
- <!-- 布局 -->
- <ElDivider>{{ t('setting.layout') }}</ElDivider>
- <LayoutRadioPicker />
- <!-- 系统主题 -->
- <ElDivider>{{ t('setting.systemTheme') }}</ElDivider>
- <ColorRadioPicker
- v-model="systemTheme"
- :schema="[
- '#409eff',
- '#009688',
- '#536dfe',
- '#ff5c93',
- '#ee4f12',
- '#0096c7',
- '#9c27b0',
- '#ff9800'
- @change="setSystemTheme"
- <!-- 头部主题 -->
- <ElDivider>{{ t('setting.headerTheme') }}</ElDivider>
- v-model="headerTheme"
- '#fff',
- '#151515',
- '#5172dc',
- '#e74c3c',
- '#24292e',
- '#394664',
- '#383f45'
- @change="setHeaderTheme"
- <!-- 菜单主题 -->
- <template v-if="layout !== 'top'">
- <ElDivider>{{ t('setting.menuTheme') }}</ElDivider>
- v-model="menuTheme"
- '#001529',
- '#212121',
- '#273352',
- '#191b24',
- '#383f45',
- '#001628',
- '#344058'
- @change="setMenuTheme"
- <!-- 界面显示 -->
- <ElDivider>{{ t('setting.interfaceDisplay') }}</ElDivider>
- <InterfaceDisplay />
- <ElDivider />
- <div>
- <ElButton class="w-full" type="primary" @click="copyConfig">{{ t('setting.copy') }}</ElButton>
- <div class="mt-5px">
- <ElButton class="w-full" type="danger" @click="clear">
- {{ t('setting.clearAndReset') }}
- </ElButton>
- </ElDrawer>
-<style lang="scss" scoped>
-$prefix-cls: #{$namespace}-setting;
-.#{$prefix-cls} {
- border-radius: 6px 0 0 6px;
-</style>
+ import { ElMessage } from 'element-plus'
+ import { useClipboard, useCssVar } from '@vueuse/core'
+ import { CACHE_KEY, useCache } from '@/hooks/web/useCache'
+ import { setCssVar, trim } from '@/utils'
+ import { colorIsDark, hexToRGB, lighten } from '@/utils/color'
+ import { ThemeSwitch } from '@/layout/components/ThemeSwitch'
+ import ColorRadioPicker from './components/ColorRadioPicker.vue'
+ import InterfaceDisplay from './components/InterfaceDisplay.vue'
+ import LayoutRadioPicker from './components/LayoutRadioPicker.vue'
+ defineOptions({ name: 'Setting' })
+ const { t } = useI18n()
+ const prefixCls = getPrefixCls('setting')
+ const drawer = ref(false)
+ // 主题色相关
+ const systemTheme = ref(appStore.getTheme.elColorPrimary)
+ const setSystemTheme = (color : string) => {
+ setCssVar('--el-color-primary', color)
+ appStore.setTheme({ elColorPrimary: color })
+ const leftMenuBgColor = useCssVar('--left-menu-bg-color', document.documentElement)
+ setMenuTheme(trim(unref(leftMenuBgColor)))
+ // 头部主题相关
+ const headerTheme = ref(appStore.getTheme.topHeaderBgColor || '')
+ const setHeaderTheme = (color : string) => {
+ const isDarkColor = colorIsDark(color)
+ const textColor = isDarkColor ? '#fff' : 'inherit'
+ const textHoverColor = isDarkColor ? lighten(color!, 6) : '#f6f6f6'
+ const topToolBorderColor = isDarkColor ? color : '#eee'
+ setCssVar('--top-header-bg-color', color)
+ setCssVar('--top-header-text-color', textColor)
+ setCssVar('--top-header-hover-color', textHoverColor)
+ appStore.setTheme({
+ topHeaderBgColor: color,
+ topHeaderTextColor: textColor,
+ topHeaderHoverColor: textHoverColor,
+ topToolBorderColor
+ if (unref(layout) === 'top') {
+ setMenuTheme(color)
+ // 菜单主题相关
+ const menuTheme = ref(appStore.getTheme.leftMenuBgColor || '')
+ const setMenuTheme = (color : string) => {
+ const primaryColor = useCssVar('--el-color-primary', document.documentElement)
+ const theme : Recordable = {
+ // 左侧菜单边框颜色
+ leftMenuBorderColor: isDarkColor ? 'inherit' : '#eee',
+ // 左侧菜单背景颜色
+ leftMenuBgColor: color,
+ // 左侧菜单浅色背景颜色
+ leftMenuBgLightColor: isDarkColor ? lighten(color!, 6) : color,
+ // 左侧菜单选中背景颜色
+ leftMenuBgActiveColor: isDarkColor
+ ? 'var(--el-color-primary)'
+ : hexToRGB(unref(primaryColor), 0.1),
+ // 左侧菜单收起选中背景颜色
+ leftMenuCollapseBgActiveColor: isDarkColor
+ // 左侧菜单字体颜色
+ leftMenuTextColor: isDarkColor ? '#bfcbd9' : '#333',
+ // 左侧菜单选中字体颜色
+ leftMenuTextActiveColor: isDarkColor ? '#fff' : 'var(--el-color-primary)',
+ // logo字体颜色
+ logoTitleTextColor: isDarkColor ? '#fff' : 'inherit',
+ // logo边框颜色
+ logoBorderColor: isDarkColor ? color : '#eee'
+ appStore.setTheme(theme)
+ appStore.setCssVarTheme()
+ if (layout.value === 'top' && !appStore.getIsDark) {
+ headerTheme.value = '#fff'
+ setHeaderTheme('#fff')
+ // 监听layout变化,重置一些主题色
+ watch(
+ () => layout.value,
+ (n) => {
+ if (n === 'top' && !appStore.getIsDark) {
+ } else {
+ setMenuTheme(unref(menuTheme))
+ )
+ // 拷贝
+ const copyConfig = async () => {
+ const { copy, copied, isSupported } = useClipboard({
+ source: `
+ // 面包屑
+ breadcrumb: ${appStore.getBreadcrumb},
+ // 面包屑图标
+ breadcrumbIcon: ${appStore.getBreadcrumbIcon},
+ // 折叠图标
+ hamburger: ${appStore.getHamburger},
+ // 全屏图标
+ screenfull: ${appStore.getScreenfull},
+ // 尺寸图标
+ size: ${appStore.getSize},
+ // 多语言图标
+ locale: ${appStore.getLocale},
+ // 消息图标
+ message: ${appStore.getMessage},
+ // 标签页
+ tagsView: ${appStore.getTagsView},
+ // 标签页图标
+ getTagsViewIcon: ${appStore.getTagsViewIcon},
+ // logo
+ logo: ${appStore.getLogo},
+ // 菜单手风琴
+ uniqueOpened: ${appStore.getUniqueOpened},
+ // 固定header
+ fixedHeader: ${appStore.getFixedHeader},
+ // 页脚
+ footer: ${appStore.getFooter},
+ // 灰色模式
+ greyMode: ${appStore.getGreyMode},
+ // layout布局
+ layout: '${appStore.getLayout}',
+ // 暗黑模式
+ isDark: ${appStore.getIsDark},
+ // 组件尺寸
+ currentSize: '${appStore.getCurrentSize}',
+ // 主题相关
+ theme: {
+ // 主题色
+ elColorPrimary: '${appStore.getTheme.elColorPrimary}',
+ leftMenuBorderColor: '${appStore.getTheme.leftMenuBorderColor}',
+ leftMenuBgColor: '${appStore.getTheme.leftMenuBgColor}',
+ leftMenuBgLightColor: '${appStore.getTheme.leftMenuBgLightColor}',
+ leftMenuBgActiveColor: '${appStore.getTheme.leftMenuBgActiveColor}',
+ leftMenuCollapseBgActiveColor: '${appStore.getTheme.leftMenuCollapseBgActiveColor}',
+ leftMenuTextColor: '${appStore.getTheme.leftMenuTextColor}',
+ leftMenuTextActiveColor: '${appStore.getTheme.leftMenuTextActiveColor}',
+ logoTitleTextColor: '${appStore.getTheme.logoTitleTextColor}',
+ logoBorderColor: '${appStore.getTheme.logoBorderColor}',
+ // 头部背景颜色
+ topHeaderBgColor: '${appStore.getTheme.topHeaderBgColor}',
+ // 头部字体颜色
+ topHeaderTextColor: '${appStore.getTheme.topHeaderTextColor}',
+ // 头部悬停颜色
+ topHeaderHoverColor: '${appStore.getTheme.topHeaderHoverColor}',
+ // 头部边框颜色
+ topToolBorderColor: '${appStore.getTheme.topToolBorderColor}'
+ `
+ if (!isSupported) {
+ ElMessage.error(t('setting.copyFailed'))
+ await copy()
+ if (unref(copied)) {
+ ElMessage.success(t('setting.copySuccess'))
+ // 清空缓存
+ const clear = () => {
+ const { wsCache } = useCache()
+ wsCache.delete(CACHE_KEY.LAYOUT)
+ wsCache.delete(CACHE_KEY.THEME)
+ wsCache.delete(CACHE_KEY.IS_DARK)
+ window.location.reload()
+ <!-- <div
+ :class="prefixCls"
+ class="fixed right-0 top-[45%] h-40px w-40px cursor-pointer bg-[var(--el-color-primary)] text-center leading-40px"
+ @click="drawer = true"
+ >
+ <Icon color="#fff" icon="ep:setting" />
+ </div> -->
+ <ElDrawer v-model="drawer" :z-index="4000" direction="rtl" size="350px">
+ <template #header>
+ <span class="text-16px font-700">{{ t('setting.projectSetting') }}</span>
+ <div class="text-center">
+ <!-- 主题 -->
+ <ElDivider>{{ t('setting.theme') }}</ElDivider>
+ <ThemeSwitch />
+ <!-- 布局 -->
+ <ElDivider>{{ t('setting.layout') }}</ElDivider>
+ <LayoutRadioPicker />
+ <!-- 系统主题 -->
+ <ElDivider>{{ t('setting.systemTheme') }}</ElDivider>
+ <ColorRadioPicker v-model="systemTheme" :schema="[
+ '#409eff',
+ '#009688',
+ '#536dfe',
+ '#ff5c93',
+ '#ee4f12',
+ '#0096c7',
+ '#9c27b0',
+ '#ff9800'
+ ]" @change="setSystemTheme" />
+ <!-- 头部主题 -->
+ <ElDivider>{{ t('setting.headerTheme') }}</ElDivider>
+ <ColorRadioPicker v-model="headerTheme" :schema="[
+ '#fff',
+ '#151515',
+ '#5172dc',
+ '#e74c3c',
+ '#24292e',
+ '#394664',
+ '#383f45'
+ ]" @change="setHeaderTheme" />
+ <!-- 菜单主题 -->
+ <template v-if="layout !== 'top'">
+ <ElDivider>{{ t('setting.menuTheme') }}</ElDivider>
+ <ColorRadioPicker v-model="menuTheme" :schema="[
+ '#001529',
+ '#212121',
+ '#273352',
+ '#191b24',
+ '#383f45',
+ '#001628',
+ '#344058'
+ ]" @change="setMenuTheme" />
+ <!-- 界面显示 -->
+ <ElDivider>{{ t('setting.interfaceDisplay') }}</ElDivider>
+ <InterfaceDisplay />
+ <ElDivider />
+ <div>
+ <ElButton class="w-full" type="primary" @click="copyConfig">{{ t('setting.copy') }}</ElButton>
+ <div class="mt-5px">
+ <ElButton class="w-full" type="danger" @click="clear">
+ {{ t('setting.clearAndReset') }}
+ </ElButton>
+ </ElDrawer>
+ $prefix-cls: #{$namespace}-setting;
+ .#{$prefix-cls} {
+ border-radius: 6px 0 0 6px;
@@ -1,224 +1,224 @@
-import { setCssVar } from '@/utils'
-import { useWatermark } from '@/hooks/web/useWatermark'
-defineOptions({ name: 'InterfaceDisplay' })
-const { setWatermark } = useWatermark()
-const prefixCls = getPrefixCls('interface-display')
-const water = ref()
-// 面包屑
-const breadcrumb = ref(appStore.getBreadcrumb)
-const breadcrumbChange = (show: boolean) => {
- appStore.setBreadcrumb(show)
-// 面包屑图标
-const breadcrumbIcon = ref(appStore.getBreadcrumbIcon)
-const breadcrumbIconChange = (show: boolean) => {
- appStore.setBreadcrumbIcon(show)
-// 折叠图标
-const hamburger = ref(appStore.getHamburger)
-const hamburgerChange = (show: boolean) => {
- appStore.setHamburger(show)
-// 全屏图标
-const screenfull = ref(appStore.getScreenfull)
-const screenfullChange = (show: boolean) => {
- appStore.setScreenfull(show)
-// 尺寸图标
-const size = ref(appStore.getSize)
-const sizeChange = (show: boolean) => {
- appStore.setSize(show)
-// 多语言图标
-const locale = ref(appStore.getLocale)
-const localeChange = (show: boolean) => {
- appStore.setLocale(show)
-// 消息图标
-const message = ref(appStore.getMessage)
-const messageChange = (show: boolean) => {
- appStore.setMessage(show)
-// 标签页
-const tagsView = ref(appStore.getTagsView)
-const tagsViewChange = (show: boolean) => {
- // 切换标签栏显示时,同步切换标签栏的高度
- setCssVar('--tags-view-height', show ? '35px' : '0px')
- appStore.setTagsView(show)
-// 标签页图标
-const tagsViewIcon = ref(appStore.getTagsViewIcon)
-const tagsViewIconChange = (show: boolean) => {
- appStore.setTagsViewIcon(show)
-// logo
-const logo = ref(appStore.getLogo)
-const logoChange = (show: boolean) => {
- appStore.setLogo(show)
-// 菜单手风琴
-const uniqueOpened = ref(appStore.getUniqueOpened)
-const uniqueOpenedChange = (uniqueOpened: boolean) => {
- appStore.setUniqueOpened(uniqueOpened)
-// 固定头部
-const fixedHeader = ref(appStore.getFixedHeader)
-const fixedHeaderChange = (show: boolean) => {
- appStore.setFixedHeader(show)
-// 页脚
-const footer = ref(appStore.getFooter)
-const footerChange = (show: boolean) => {
- appStore.setFooter(show)
-// 灰色模式
-const greyMode = ref(appStore.getGreyMode)
-const greyModeChange = (show: boolean) => {
- appStore.setGreyMode(show)
-// 固定菜单
-const fixedMenu = ref(appStore.getFixedMenu)
-const fixedMenuChange = (show: boolean) => {
- appStore.setFixedMenu(show)
-// 设置水印
-const setWater = () => {
- setWatermark(water.value)
- if (n === 'top') {
- appStore.setCollapse(false)
- <div :class="prefixCls">
- <div class="flex items-center justify-between">
- <span class="text-14px">{{ t('setting.breadcrumb') }}</span>
- <ElSwitch v-model="breadcrumb" @change="breadcrumbChange" />
- <span class="text-14px">{{ t('setting.breadcrumbIcon') }}</span>
- <ElSwitch v-model="breadcrumbIcon" @change="breadcrumbIconChange" />
- <span class="text-14px">{{ t('setting.hamburgerIcon') }}</span>
- <ElSwitch v-model="hamburger" @change="hamburgerChange" />
- <span class="text-14px">{{ t('setting.screenfullIcon') }}</span>
- <ElSwitch v-model="screenfull" @change="screenfullChange" />
- <span class="text-14px">{{ t('setting.sizeIcon') }}</span>
- <ElSwitch v-model="size" @change="sizeChange" />
- <span class="text-14px">{{ t('setting.localeIcon') }}</span>
- <ElSwitch v-model="locale" @change="localeChange" />
- <span class="text-14px">{{ t('setting.messageIcon') }}</span>
- <ElSwitch v-model="message" @change="messageChange" />
- <span class="text-14px">{{ t('setting.tagsView') }}</span>
- <ElSwitch v-model="tagsView" @change="tagsViewChange" />
- <span class="text-14px">{{ t('setting.tagsViewIcon') }}</span>
- <ElSwitch v-model="tagsViewIcon" @change="tagsViewIconChange" />
- <span class="text-14px">{{ t('setting.logo') }}</span>
- <ElSwitch v-model="logo" @change="logoChange" />
- <span class="text-14px">{{ t('setting.uniqueOpened') }}</span>
- <ElSwitch v-model="uniqueOpened" @change="uniqueOpenedChange" />
- <span class="text-14px">{{ t('setting.fixedHeader') }}</span>
- <ElSwitch v-model="fixedHeader" @change="fixedHeaderChange" />
- <span class="text-14px">{{ t('setting.footer') }}</span>
- <ElSwitch v-model="footer" @change="footerChange" />
- <span class="text-14px">{{ t('setting.greyMode') }}</span>
- <ElSwitch v-model="greyMode" @change="greyModeChange" />
- <span class="text-14px">{{ t('setting.fixedMenu') }}</span>
- <ElSwitch v-model="fixedMenu" @change="fixedMenuChange" />
- <span class="text-14px">{{ t('watermark.watermark') }}</span>
- <ElInput v-model="water" class="right-1 w-20" @change="setWater()" />
+ import { setCssVar } from '@/utils'
+ import { useWatermark } from '@/hooks/web/useWatermark'
+ defineOptions({ name: 'InterfaceDisplay' })
+ const { setWatermark } = useWatermark()
+ const prefixCls = getPrefixCls('interface-display')
+ const water = ref()
+ const breadcrumb = ref(appStore.getBreadcrumb)
+ const breadcrumbChange = (show : boolean) => {
+ appStore.setBreadcrumb(show)
+ const breadcrumbIcon = ref(appStore.getBreadcrumbIcon)
+ const breadcrumbIconChange = (show : boolean) => {
+ appStore.setBreadcrumbIcon(show)
+ const hamburger = ref(appStore.getHamburger)
+ const hamburgerChange = (show : boolean) => {
+ appStore.setHamburger(show)
+ const screenfull = ref(appStore.getScreenfull)
+ const screenfullChange = (show : boolean) => {
+ appStore.setScreenfull(show)
+ const size = ref(appStore.getSize)
+ const sizeChange = (show : boolean) => {
+ appStore.setSize(show)
+ const locale = ref(appStore.getLocale)
+ const localeChange = (show : boolean) => {
+ appStore.setLocale(show)
+ const message = ref(appStore.getMessage)
+ const messageChange = (show : boolean) => {
+ appStore.setMessage(show)
+ const tagsView = ref(appStore.getTagsView)
+ const tagsViewChange = (show : boolean) => {
+ // 切换标签栏显示时,同步切换标签栏的高度
+ setCssVar('--tags-view-height', show ? '35px' : '0px')
+ appStore.setTagsView(show)
+ const tagsViewIcon = ref(appStore.getTagsViewIcon)
+ const tagsViewIconChange = (show : boolean) => {
+ appStore.setTagsViewIcon(show)
+ const logo = ref(appStore.getLogo)
+ const logoChange = (show : boolean) => {
+ appStore.setLogo(show)
+ const uniqueOpened = ref(appStore.getUniqueOpened)
+ const uniqueOpenedChange = (uniqueOpened : boolean) => {
+ appStore.setUniqueOpened(uniqueOpened)
+ // 固定头部
+ const fixedHeader = ref(appStore.getFixedHeader)
+ const fixedHeaderChange = (show : boolean) => {
+ appStore.setFixedHeader(show)
+ const footer = ref(appStore.getFooter)
+ const footerChange = (show : boolean) => {
+ appStore.setFooter(show)
+ const greyMode = ref(appStore.getGreyMode)
+ const greyModeChange = (show : boolean) => {
+ appStore.setGreyMode(show)
+ // 固定菜单
+ const fixedMenu = ref(appStore.getFixedMenu)
+ const fixedMenuChange = (show : boolean) => {
+ appStore.setFixedMenu(show)
+ // 设置水印
+ const setWater = () => {
+ // setWatermark(water.value)
+ if (n === 'top') {
+ appStore.setCollapse(false)
+ <div :class="prefixCls">
+ <div class="flex items-center justify-between">
+ <span class="text-14px">{{ t('setting.breadcrumb') }}</span>
+ <ElSwitch v-model="breadcrumb" @change="breadcrumbChange" />
+ <span class="text-14px">{{ t('setting.breadcrumbIcon') }}</span>
+ <ElSwitch v-model="breadcrumbIcon" @change="breadcrumbIconChange" />
+ <span class="text-14px">{{ t('setting.hamburgerIcon') }}</span>
+ <ElSwitch v-model="hamburger" @change="hamburgerChange" />
+ <span class="text-14px">{{ t('setting.screenfullIcon') }}</span>
+ <ElSwitch v-model="screenfull" @change="screenfullChange" />
+ <span class="text-14px">{{ t('setting.sizeIcon') }}</span>
+ <ElSwitch v-model="size" @change="sizeChange" />
+ <span class="text-14px">{{ t('setting.localeIcon') }}</span>
+ <ElSwitch v-model="locale" @change="localeChange" />
+ <span class="text-14px">{{ t('setting.messageIcon') }}</span>
+ <ElSwitch v-model="message" @change="messageChange" />
+ <span class="text-14px">{{ t('setting.tagsView') }}</span>
+ <ElSwitch v-model="tagsView" @change="tagsViewChange" />
+ <span class="text-14px">{{ t('setting.tagsViewIcon') }}</span>
+ <ElSwitch v-model="tagsViewIcon" @change="tagsViewIconChange" />
+ <span class="text-14px">{{ t('setting.logo') }}</span>
+ <ElSwitch v-model="logo" @change="logoChange" />
+ <span class="text-14px">{{ t('setting.uniqueOpened') }}</span>
+ <ElSwitch v-model="uniqueOpened" @change="uniqueOpenedChange" />
+ <span class="text-14px">{{ t('setting.fixedHeader') }}</span>
+ <ElSwitch v-model="fixedHeader" @change="fixedHeaderChange" />
+ <span class="text-14px">{{ t('setting.footer') }}</span>
+ <ElSwitch v-model="footer" @change="footerChange" />
+ <span class="text-14px">{{ t('setting.greyMode') }}</span>
+ <ElSwitch v-model="greyMode" @change="greyModeChange" />
+ <span class="text-14px">{{ t('setting.fixedMenu') }}</span>
+ <ElSwitch v-model="fixedMenu" @change="fixedMenuChange" />
+ <span class="text-14px">{{ t('watermark.watermark') }}</span>
+ <ElInput v-model="water" class="right-1 w-20" @change="setWater()" />
@@ -64,10 +64,10 @@ const toDocument = () => {
<Icon icon="ep:tools" />
<div @click="toProfile">{{ t('common.profile') }}</div>
</ElDropdownItem>
- <ElDropdownItem>
+ <!-- <ElDropdownItem>
<Icon icon="ep:menu" />
<div @click="toDocument">{{ t('common.document') }}</div>
- </ElDropdownItem>
+ </ElDropdownItem> -->
<ElDropdownItem divided @click="loginOut">
<Icon icon="ep:switch-button" />
<div>{{ t('common.loginOut') }}</div>
@@ -1,442 +1,442 @@
-export default {
- common: {
- inputText: '请输入',
- selectText: '请选择',
- startTimeText: '开始时间',
- endTimeText: '结束时间',
- login: '登录',
- required: '该项为必填项',
- loginOut: '退出系统',
- document: '项目文档',
- profile: '个人中心',
- reminder: '温馨提示',
- loginOutMessage: '是否退出本系统?',
- back: '返回',
- ok: '确定',
- save: '保存',
- cancel: '取消',
- close: '关闭',
- reload: '重新加载',
- success: '成功',
- closeTab: '关闭标签页',
- closeTheLeftTab: '关闭左侧标签页',
- closeTheRightTab: '关闭右侧标签页',
- closeOther: '关闭其他标签页',
- closeAll: '关闭全部标签页',
- prevLabel: '上一步',
- nextLabel: '下一步',
- skipLabel: '跳过',
- doneLabel: '结束',
- menu: '菜单',
- menuDes: '以路由的结构渲染的菜单栏',
- collapse: '展开缩收',
- collapseDes: '展开和缩放菜单栏',
- tagsView: '标签页',
- tagsViewDes: '用于记录路由历史记录',
- tool: '工具',
- toolDes: '用于设置定制系统',
- query: '查询',
- reset: '重置',
- shrink: '收起',
- expand: '展开',
- confirmTitle: '系统提示',
- exportMessage: '是否确认导出数据项?',
- importMessage: '是否确认导入数据项?',
- createSuccess: '新增成功',
- updateSuccess: '修改成功',
- delMessage: '是否删除所选中数据?',
- delDataMessage: '是否删除数据?',
- delNoData: '请选择需要删除的数据',
- delSuccess: '删除成功',
- index: '序号',
- status: '状态',
- createTime: '创建时间',
- updateTime: '更新时间',
- copy: '复制',
- copySuccess: '复制成功',
- copyError: '复制失败'
- },
- error: {
- noPermission: `抱歉,您无权访问此页面。`,
- pageError: '抱歉,您访问的页面不存在。',
- networkError: '抱歉,服务器报告错误。',
- returnToHome: '返回首页'
- permission: {
- hasPermission: `请设置操作权限标签值`,
- hasRole: `请设置角色权限标签值`
- setting: {
- projectSetting: '项目配置',
- theme: '主题',
- layout: '布局',
- systemTheme: '系统主题',
- menuTheme: '菜单主题',
- interfaceDisplay: '界面显示',
- breadcrumb: '面包屑',
- breadcrumbIcon: '面包屑图标',
- collapseMenu: '折叠菜单',
- hamburgerIcon: '折叠图标',
- screenfullIcon: '全屏图标',
- sizeIcon: '尺寸图标',
- localeIcon: '多语言图标',
- messageIcon: '消息图标',
- logo: '标志',
- greyMode: '灰色模式',
- fixedHeader: '固定头部',
- headerTheme: '头部主题',
- cutMenu: '切割菜单',
- copy: '拷贝',
- clearAndReset: '清除缓存并且重置',
- copySuccess: '拷贝成功',
- copyFailed: '拷贝失败',
- footer: '页脚',
- uniqueOpened: '菜单手风琴',
- tagsViewIcon: '标签页图标',
- reExperienced: '请重新退出登录体验',
- fixedMenu: '固定菜单'
- size: {
- default: '默认',
- large: '大',
- small: '小'
- login: {
- welcome: '欢迎使用本系统',
- message: '开箱即用的中后台管理系统',
- tenantname: '租户名称',
- username: '用户名',
- password: '密码',
- code: '验证码',
- relogin: '重新登录',
- otherLogin: '其他登录方式',
- register: '注册',
- checkPassword: '确认密码',
- remember: '记住我',
- hasUser: '已有账号?去登录',
- forgetPassword: '忘记密码?',
- tenantNamePlaceholder: '请输入租户名称',
- usernamePlaceholder: '请输入用户名',
- passwordPlaceholder: '请输入密码',
- codePlaceholder: '请输入验证码',
- mobileTitle: '手机登录',
- mobileNumber: '手机号码',
- mobileNumberPlaceholder: '请输入手机号码',
- backLogin: '返回',
- getSmsCode: '获取验证码',
- btnMobile: '手机登录',
- btnQRCode: '二维码登录',
- qrcode: '扫描二维码登录',
- btnRegister: '注册',
- SmsSendMsg: '验证码已发送'
- captcha: {
- verification: '请完成安全验证',
- slide: '向右滑动完成验证',
- point: '请依次点击',
- success: '验证成功',
- fail: '验证失败'
- router: {
- socialLogin: '社交登录',
- home: '首页',
- analysis: '分析页',
- workplace: '工作台'
- analysis: {
- newUser: '新增用户',
- unreadInformation: '未读消息',
- transactionAmount: '成交金额',
- totalShopping: '购物总量',
- monthlySales: '每月销售额',
- userAccessSource: '用户访问来源',
- january: '一月',
- february: '二月',
- march: '三月',
- april: '四月',
- may: '五月',
- june: '六月',
- july: '七月',
- august: '八月',
- september: '九月',
- october: '十月',
- november: '十一月',
- december: '十二月',
- estimate: '预计',
- actual: '实际',
- directAccess: '直接访问',
- mailMarketing: '邮件营销',
- allianceAdvertising: '联盟广告',
- videoAdvertising: '视频广告',
- searchEngines: '搜索引擎',
- weeklyUserActivity: '每周用户活跃量',
- activeQuantity: '活跃量',
- monday: '周一',
- tuesday: '周二',
- wednesday: '周三',
- thursday: '周四',
- friday: '周五',
- saturday: '周六',
- sunday: '周日'
- workplace: {
- welcome: '你好',
- happyDay: '祝你开心每一天!',
- toady: '今日晴',
- notice: '通知公告',
- project: '项目数',
- access: '项目访问',
- toDo: '待办',
- introduction: '一个正经的简介',
- shortcutOperation: '快捷入口',
- operation: '操作',
- index: '指数',
- personal: '个人',
- team: '团队',
- quote: '引用',
- contribution: '贡献',
- hot: '热度',
- yield: '产量',
- dynamic: '动态',
- push: '推送',
- follow: '关注'
- form: {
- input: '输入框',
- inputNumber: '数字输入框',
- icon: '图标',
- mixed: '复合型',
- textarea: '多行文本',
- slot: '插槽',
- position: '位置',
- autocomplete: '自动补全',
- select: '选择器',
- selectGroup: '选项分组',
- selectV2: '虚拟列表选择器',
- cascader: '级联选择器',
- switch: '开关',
- rate: '评分',
- colorPicker: '颜色选择器',
- transfer: '穿梭框',
- render: '渲染器',
- radio: '单选框',
- button: '按钮',
- checkbox: '多选框',
- slider: '滑块',
- datePicker: '日期选择器',
- shortcuts: '快捷选项',
- today: '今天',
- yesterday: '昨天',
- aWeekAgo: '一周前',
- week: '周',
- year: '年',
- month: '月',
- dates: '日期',
- daterange: '日期范围',
- monthrange: '月份范围',
- dateTimePicker: '日期时间选择器',
- dateTimerange: '日期时间范围',
- timePicker: '时间选择器',
- timeSelect: '时间选择',
- inputPassword: '密码输入框',
- passwordStrength: '密码强度',
- operate: '操作',
- change: '更改',
- restore: '还原',
- disabled: '禁用',
- disablement: '解除禁用',
- delete: '删除',
- add: '添加',
- setValue: '设置值',
- resetValue: '重置值',
- set: '设置',
- subitem: '子项',
- formValidation: '表单验证',
- verifyReset: '验证重置',
- remark: '备注'
- watermark: {
- watermark: '水印'
- table: {
- table: '表格',
- title: '标题',
- author: '作者',
- action: '操作',
- pagination: '分页',
- reserveIndex: '叠加序号',
- restoreIndex: '还原序号',
- showSelections: '显示多选',
- hiddenSelections: '隐藏多选',
- showExpandedRows: '显示展开行',
- hiddenExpandedRows: '隐藏展开行',
- header: '头部'
- action: {
- create: '新增',
- add: '新增',
- del: '删除',
- edit: '编辑',
- update: '编辑',
- preview: '预览',
- more: '更多',
- sync: '同步',
- detail: '详情',
- export: '导出',
- import: '导入',
- generate: '生成',
- logout: '强制退出',
- test: '测试',
- typeCreate: '字典类型新增',
- typeUpdate: '字典类型编辑',
- dataCreate: '字典数据新增',
- dataUpdate: '字典数据编辑'
- dialog: {
- dialog: '弹窗',
- open: '打开',
- close: '关闭'
- sys: {
- api: {
- operationFailed: '操作失败',
- errorTip: '错误提示',
- errorMessage: '操作失败,系统异常!',
- timeoutMessage: '登录超时,请重新登录!',
- apiTimeoutMessage: '接口请求超时,请刷新页面重试!',
- apiRequestFailed: '请求出错,请稍候重试',
- networkException: '网络异常',
- networkExceptionMsg: '网络异常,请检查您的网络连接是否正常!',
- errMsg401: '用户没有权限(令牌、用户名、密码错误)!',
- errMsg403: '用户得到授权,但是访问是被禁止的。!',
- errMsg404: '网络请求错误,未找到该资源!',
- errMsg405: '网络请求错误,请求方法未允许!',
- errMsg408: '网络请求超时!',
- errMsg500: '服务器错误,请联系管理员!',
- errMsg501: '网络未实现!',
- errMsg502: '网络错误!',
- errMsg503: '服务不可用,服务器暂时过载或维护!',
- errMsg504: '网络超时!',
- errMsg505: 'http版本不支持该请求!',
- errMsg901: '演示模式,无法进行写操作!'
- app: {
- logoutTip: '温馨提醒',
- logoutMessage: '是否确认退出系统?',
- menuLoading: '菜单加载中...'
- exception: {
- backLogin: '返回登录',
- backHome: '返回首页',
- subTitle403: '抱歉,您无权访问此页面。',
- subTitle404: '抱歉,您访问的页面不存在。',
- subTitle500: '抱歉,服务器报告错误。',
- noDataTitle: '当前页无数据',
- networkErrorTitle: '网络错误',
- networkErrorSubTitle: '抱歉,您的网络连接已断开,请检查您的网络!'
- lock: {
- unlock: '点击解锁',
- alert: '锁屏密码错误',
- backToLogin: '返回登录',
- entry: '进入系统',
- placeholder: '请输入锁屏密码或者用户密码'
- backSignIn: '返回',
- signInFormTitle: '登录',
- ssoFormTitle: '三方授权',
- mobileSignInFormTitle: '手机登录',
- qrSignInFormTitle: '二维码登录',
- signUpFormTitle: '注册',
- forgetFormTitle: '重置密码',
- signInTitle: '开箱即用的中后台管理系统',
- signInDesc: '输入您的个人详细信息开始使用!',
- policy: '我同意xxx隐私政策',
- scanSign: `扫码后点击"确认",即可完成登录`,
- loginButton: '登录',
- registerButton: '注册',
- rememberMe: '记住我',
- otherSignIn: '其他登录方式',
- // notify
- loginSuccessTitle: '登录成功',
- loginSuccessDesc: '欢迎回来',
- // placeholder
- accountPlaceholder: '请输入账号',
- smsPlaceholder: '请输入验证码',
- mobilePlaceholder: '请输入手机号码',
- policyPlaceholder: '勾选后才能注册',
- diffPwd: '两次输入密码不一致',
- userName: '账号',
- confirmPassword: '确认密码',
- email: '邮箱',
- smsCode: '短信验证码',
- mobile: '手机号码'
- profile: {
- user: {
- title: '个人信息',
- username: '用户名称',
- nickname: '用户昵称',
- mobile: '手机号码',
- email: '用户邮箱',
- dept: '所属部门',
- posts: '所属岗位',
- roles: '所属角色',
- sex: '性别',
- man: '男',
- woman: '女',
- createTime: '创建日期'
- info: {
- title: '基本信息',
- basicInfo: '基本资料',
- resetPwd: '修改密码',
- userSocial: '社交信息'
- rules: {
- nickname: '请输入用户昵称',
- mail: '请输入邮箱地址',
- truemail: '请输入正确的邮箱地址',
- phone: '请输入正确的手机号码',
- truephone: '请输入正确的手机号码'
- password: {
- oldPassword: '旧密码',
- newPassword: '新密码',
- oldPwdMsg: '请输入旧密码',
- newPwdMsg: '请输入新密码',
- cfPwdMsg: '请输入确认密码',
- pwdRules: '长度在 6 到 20 个字符',
- diffPwd: '两次输入密码不一致'
- cropper: {
- selectImage: '选择图片',
- uploadSuccess: '上传成功',
- modalTitle: '头像上传',
- okText: '确认并上传',
- btn_reset: '重置',
- btn_rotate_left: '逆时针旋转',
- btn_rotate_right: '顺时针旋转',
- btn_scale_x: '水平翻转',
- btn_scale_y: '垂直翻转',
- btn_zoom_in: '放大',
- btn_zoom_out: '缩小',
- preview: '预览'
- 'OAuth 2.0': 'OAuth 2.0' // 避免菜单名是 OAuth 2.0 时,一直 warn 报错
+export default {
+ common: {
+ inputText: '请输入',
+ selectText: '请选择',
+ startTimeText: '开始时间',
+ endTimeText: '结束时间',
+ login: '登录',
+ required: '该项为必填项',
+ loginOut: '退出系统',
+ document: '项目文档',
+ profile: '个人中心',
+ reminder: '温馨提示',
+ loginOutMessage: '是否退出本系统?',
+ back: '返回',
+ ok: '确定',
+ save: '保存',
+ cancel: '取消',
+ close: '关闭',
+ reload: '重新加载',
+ success: '成功',
+ closeTab: '关闭标签页',
+ closeTheLeftTab: '关闭左侧标签页',
+ closeTheRightTab: '关闭右侧标签页',
+ closeOther: '关闭其他标签页',
+ closeAll: '关闭全部标签页',
+ prevLabel: '上一步',
+ nextLabel: '下一步',
+ skipLabel: '跳过',
+ doneLabel: '结束',
+ menu: '菜单',
+ menuDes: '以路由的结构渲染的菜单栏',
+ collapse: '展开缩收',
+ collapseDes: '展开和缩放菜单栏',
+ tagsView: '标签页',
+ tagsViewDes: '用于记录路由历史记录',
+ tool: '工具',
+ toolDes: '用于设置定制系统',
+ query: '查询',
+ reset: '重置',
+ shrink: '收起',
+ expand: '展开',
+ confirmTitle: '系统提示',
+ exportMessage: '是否确认导出数据项?',
+ importMessage: '是否确认导入数据项?',
+ createSuccess: '新增成功',
+ updateSuccess: '修改成功',
+ delMessage: '是否删除所选中数据?',
+ delDataMessage: '是否删除数据?',
+ delNoData: '请选择需要删除的数据',
+ delSuccess: '删除成功',
+ index: '序号',
+ status: '状态',
+ createTime: '创建时间',
+ updateTime: '更新时间',
+ copy: '复制',
+ copySuccess: '复制成功',
+ copyError: '复制失败'
+ },
+ error: {
+ noPermission: `抱歉,您无权访问此页面。`,
+ pageError: '抱歉,您访问的页面不存在。',
+ networkError: '抱歉,服务器报告错误。',
+ returnToHome: '返回首页'
+ permission: {
+ hasPermission: `请设置操作权限标签值`,
+ hasRole: `请设置角色权限标签值`
+ setting: {
+ projectSetting: '项目配置',
+ theme: '主题',
+ layout: '布局',
+ systemTheme: '系统主题',
+ menuTheme: '菜单主题',
+ interfaceDisplay: '界面显示',
+ breadcrumb: '面包屑',
+ breadcrumbIcon: '面包屑图标',
+ collapseMenu: '折叠菜单',
+ hamburgerIcon: '折叠图标',
+ screenfullIcon: '全屏图标',
+ sizeIcon: '尺寸图标',
+ localeIcon: '多语言图标',
+ messageIcon: '消息图标',
+ logo: '标志',
+ greyMode: '灰色模式',
+ fixedHeader: '固定头部',
+ headerTheme: '头部主题',
+ cutMenu: '切割菜单',
+ copy: '拷贝',
+ clearAndReset: '清除缓存并且重置',
+ copySuccess: '拷贝成功',
+ copyFailed: '拷贝失败',
+ footer: '页脚',
+ uniqueOpened: '菜单手风琴',
+ tagsViewIcon: '标签页图标',
+ reExperienced: '请重新退出登录体验',
+ fixedMenu: '固定菜单'
+ size: {
+ default: '默认',
+ large: '大',
+ small: '小'
+ login: {
+ welcome: '欢迎使用本系统',
+ message: '开箱即用的中后台管理系统',
+ tenantname: '租户名称',
+ username: '用户名',
+ password: '密码',
+ code: '验证码',
+ relogin: '重新登录',
+ otherLogin: '其他登录方式',
+ register: '注册',
+ checkPassword: '确认密码',
+ remember: '记住我',
+ hasUser: '已有账号?去登录',
+ forgetPassword: '忘记密码?',
+ tenantNamePlaceholder: '请输入租户名称',
+ usernamePlaceholder: '请输入用户名',
+ passwordPlaceholder: '请输入密码',
+ codePlaceholder: '请输入验证码',
+ mobileTitle: '手机登录',
+ mobileNumber: '手机号码',
+ mobileNumberPlaceholder: '请输入手机号码',
+ backLogin: '返回',
+ getSmsCode: '获取验证码',
+ btnMobile: '手机登录',
+ btnQRCode: '微信扫码登录',
+ qrcode: '扫描二维码登录',
+ btnRegister: '注册',
+ SmsSendMsg: '验证码已发送'
+ captcha: {
+ verification: '请完成安全验证',
+ slide: '向右滑动完成验证',
+ point: '请依次点击',
+ success: '验证成功',
+ fail: '验证失败'
+ router: {
+ socialLogin: '社交登录',
+ home: '首页',
+ analysis: '分析页',
+ workplace: '工作台'
+ analysis: {
+ newUser: '新增用户',
+ unreadInformation: '未读消息',
+ transactionAmount: '成交金额',
+ totalShopping: '购物总量',
+ monthlySales: '每月销售额',
+ userAccessSource: '用户访问来源',
+ january: '一月',
+ february: '二月',
+ march: '三月',
+ april: '四月',
+ may: '五月',
+ june: '六月',
+ july: '七月',
+ august: '八月',
+ september: '九月',
+ october: '十月',
+ november: '十一月',
+ december: '十二月',
+ estimate: '预计',
+ actual: '实际',
+ directAccess: '直接访问',
+ mailMarketing: '邮件营销',
+ allianceAdvertising: '联盟广告',
+ videoAdvertising: '视频广告',
+ searchEngines: '搜索引擎',
+ weeklyUserActivity: '每周用户活跃量',
+ activeQuantity: '活跃量',
+ monday: '周一',
+ tuesday: '周二',
+ wednesday: '周三',
+ thursday: '周四',
+ friday: '周五',
+ saturday: '周六',
+ sunday: '周日'
+ workplace: {
+ welcome: '你好',
+ happyDay: '祝你开心每一天!',
+ toady: '今日晴',
+ notice: '通知公告',
+ project: '项目数',
+ access: '项目访问',
+ toDo: '待办',
+ introduction: '一个正经的简介',
+ shortcutOperation: '快捷入口',
+ operation: '操作',
+ index: '指数',
+ personal: '个人',
+ team: '团队',
+ quote: '引用',
+ contribution: '贡献',
+ hot: '热度',
+ yield: '产量',
+ dynamic: '动态',
+ push: '推送',
+ follow: '关注'
+ form: {
+ input: '输入框',
+ inputNumber: '数字输入框',
+ icon: '图标',
+ mixed: '复合型',
+ textarea: '多行文本',
+ slot: '插槽',
+ position: '位置',
+ autocomplete: '自动补全',
+ select: '选择器',
+ selectGroup: '选项分组',
+ selectV2: '虚拟列表选择器',
+ cascader: '级联选择器',
+ switch: '开关',
+ rate: '评分',
+ colorPicker: '颜色选择器',
+ transfer: '穿梭框',
+ render: '渲染器',
+ radio: '单选框',
+ button: '按钮',
+ checkbox: '多选框',
+ slider: '滑块',
+ datePicker: '日期选择器',
+ shortcuts: '快捷选项',
+ today: '今天',
+ yesterday: '昨天',
+ aWeekAgo: '一周前',
+ week: '周',
+ year: '年',
+ month: '月',
+ dates: '日期',
+ daterange: '日期范围',
+ monthrange: '月份范围',
+ dateTimePicker: '日期时间选择器',
+ dateTimerange: '日期时间范围',
+ timePicker: '时间选择器',
+ timeSelect: '时间选择',
+ inputPassword: '密码输入框',
+ passwordStrength: '密码强度',
+ operate: '操作',
+ change: '更改',
+ restore: '还原',
+ disabled: '禁用',
+ disablement: '解除禁用',
+ delete: '删除',
+ add: '添加',
+ setValue: '设置值',
+ resetValue: '重置值',
+ set: '设置',
+ subitem: '子项',
+ formValidation: '表单验证',
+ verifyReset: '验证重置',
+ remark: '备注'
+ watermark: {
+ watermark: '水印'
+ table: {
+ table: '表格',
+ title: '标题',
+ author: '作者',
+ action: '操作',
+ pagination: '分页',
+ reserveIndex: '叠加序号',
+ restoreIndex: '还原序号',
+ showSelections: '显示多选',
+ hiddenSelections: '隐藏多选',
+ showExpandedRows: '显示展开行',
+ hiddenExpandedRows: '隐藏展开行',
+ header: '头部'
+ action: {
+ create: '新增',
+ add: '新增',
+ del: '删除',
+ edit: '编辑',
+ update: '编辑',
+ preview: '预览',
+ more: '更多',
+ sync: '同步',
+ detail: '详情',
+ export: '导出',
+ import: '导入',
+ generate: '生成',
+ logout: '强制退出',
+ test: '测试',
+ typeCreate: '字典类型新增',
+ typeUpdate: '字典类型编辑',
+ dataCreate: '字典数据新增',
+ dataUpdate: '字典数据编辑'
+ dialog: {
+ dialog: '弹窗',
+ open: '打开',
+ close: '关闭'
+ sys: {
+ api: {
+ operationFailed: '操作失败',
+ errorTip: '错误提示',
+ errorMessage: '操作失败,系统异常!',
+ timeoutMessage: '登录超时,请重新登录!',
+ apiTimeoutMessage: '接口请求超时,请刷新页面重试!',
+ apiRequestFailed: '请求出错,请稍候重试',
+ networkException: '网络异常',
+ networkExceptionMsg: '网络异常,请检查您的网络连接是否正常!',
+ errMsg401: '用户没有权限(令牌、用户名、密码错误)!',
+ errMsg403: '用户得到授权,但是访问是被禁止的。!',
+ errMsg404: '网络请求错误,未找到该资源!',
+ errMsg405: '网络请求错误,请求方法未允许!',
+ errMsg408: '网络请求超时!',
+ errMsg500: '服务器错误,请联系管理员!',
+ errMsg501: '网络未实现!',
+ errMsg502: '网络错误!',
+ errMsg503: '服务不可用,服务器暂时过载或维护!',
+ errMsg504: '网络超时!',
+ errMsg505: 'http版本不支持该请求!',
+ errMsg901: '演示模式,无法进行写操作!'
+ app: {
+ logoutTip: '温馨提醒',
+ logoutMessage: '是否确认退出系统?',
+ menuLoading: '菜单加载中...'
+ exception: {
+ backLogin: '返回登录',
+ backHome: '返回首页',
+ subTitle403: '抱歉,您无权访问此页面。',
+ subTitle404: '抱歉,您访问的页面不存在。',
+ subTitle500: '抱歉,服务器报告错误。',
+ noDataTitle: '当前页无数据',
+ networkErrorTitle: '网络错误',
+ networkErrorSubTitle: '抱歉,您的网络连接已断开,请检查您的网络!'
+ lock: {
+ unlock: '点击解锁',
+ alert: '锁屏密码错误',
+ backToLogin: '返回登录',
+ entry: '进入系统',
+ placeholder: '请输入锁屏密码或者用户密码'
+ backSignIn: '返回',
+ signInFormTitle: '登录',
+ ssoFormTitle: '三方授权',
+ mobileSignInFormTitle: '手机登录',
+ qrSignInFormTitle: '二维码登录',
+ signUpFormTitle: '注册',
+ forgetFormTitle: '重置密码',
+ signInTitle: '开箱即用的中后台管理系统',
+ signInDesc: '输入您的个人详细信息开始使用!',
+ policy: '我同意xxx隐私政策',
+ scanSign: `扫码后点击"确认",即可完成登录`,
+ loginButton: '登录',
+ registerButton: '注册',
+ rememberMe: '记住我',
+ otherSignIn: '其他登录方式',
+ // notify
+ loginSuccessTitle: '登录成功',
+ loginSuccessDesc: '欢迎回来',
+ // placeholder
+ accountPlaceholder: '请输入账号',
+ smsPlaceholder: '请输入验证码',
+ mobilePlaceholder: '请输入手机号码',
+ policyPlaceholder: '勾选后才能注册',
+ diffPwd: '两次输入密码不一致',
+ userName: '账号',
+ confirmPassword: '确认密码',
+ email: '邮箱',
+ smsCode: '短信验证码',
+ mobile: '手机号码'
+ profile: {
+ user: {
+ title: '个人信息',
+ username: '用户名称',
+ nickname: '用户昵称',
+ mobile: '手机号码',
+ email: '用户邮箱',
+ dept: '所属部门',
+ posts: '所属岗位',
+ roles: '所属角色',
+ sex: '性别',
+ man: '男',
+ woman: '女',
+ createTime: '创建日期'
+ info: {
+ title: '基本信息',
+ basicInfo: '基本资料',
+ resetPwd: '修改密码',
+ userSocial: '社交信息'
+ rules: {
+ nickname: '请输入用户昵称',
+ mail: '请输入邮箱地址',
+ truemail: '请输入正确的邮箱地址',
+ phone: '请输入正确的手机号码',
+ truephone: '请输入正确的手机号码'
+ password: {
+ oldPassword: '旧密码',
+ newPassword: '新密码',
+ oldPwdMsg: '请输入旧密码',
+ newPwdMsg: '请输入新密码',
+ cfPwdMsg: '请输入确认密码',
+ pwdRules: '长度在 6 到 20 个字符',
+ diffPwd: '两次输入密码不一致'
+ cropper: {
+ selectImage: '选择图片',
+ uploadSuccess: '上传成功',
+ modalTitle: '头像上传',
+ okText: '确认并上传',
+ btn_reset: '重置',
+ btn_rotate_left: '逆时针旋转',
+ btn_rotate_right: '顺时针旋转',
+ btn_scale_x: '水平翻转',
+ btn_scale_y: '垂直翻转',
+ btn_zoom_in: '放大',
+ btn_zoom_out: '缩小',
+ preview: '预览'
+ 'OAuth 2.0': 'OAuth 2.0' // 避免菜单名是 OAuth 2.0 时,一直 warn 报错
+}
@@ -361,19 +361,19 @@ const remainingRouter: AppRouteRecordRaw[] = [
activeMenu: '/mall/product/spu'
},
- path: 'spu/edit/:id(\\d+)',
- component: () => import('@/views/mall/product/spu/form/index.vue'),
- name: 'ProductSpuEdit',
- meta: {
- noCache: true,
- hidden: true,
- canTo: true,
- icon: 'ep:edit',
- title: '商品编辑',
- activeMenu: '/mall/product/spu'
+ path: 'spu/edit/:id(\\d+)',
+ component: () => import('@/views/mall/product/spu/form/index.vue'),
+ name: 'ProductSpuEdit',
+ meta: {
+ noCache: true,
+ hidden: true,
+ canTo: true,
+ icon: 'ep:edit',
+ title: '商品编辑',
+ activeMenu: '/mall/product/spu'
{
path: 'spu/detail/:id(\\d+)',
component: () => import('@/views/mall/product/spu/form/index.vue'),
@@ -1,276 +1,277 @@
-import { defineStore } from 'pinia'
-import { store } from '../index'
-import { setCssVar, humpToUnderline } from '@/utils'
-import { ElementPlusSize } from '@/types/elementPlus'
-import { LayoutType } from '@/types/layout'
-import { ThemeTypes } from '@/types/theme'
-const { wsCache } = useCache()
-interface AppState {
- breadcrumb: boolean
- breadcrumbIcon: boolean
- collapse: boolean
- uniqueOpened: boolean
- hamburger: boolean
- screenfull: boolean
- search: boolean
- size: boolean
- locale: boolean
- message: boolean
- tagsView: boolean
- tagsViewIcon: boolean
- logo: boolean
- fixedHeader: boolean
- greyMode: boolean
- pageLoading: boolean
- layout: LayoutType
- title: string
- userInfo: string
- isDark: boolean
- currentSize: ElementPlusSize
- sizeMap: ElementPlusSize[]
- mobile: boolean
- footer: boolean
- theme: ThemeTypes
- fixedMenu: boolean
-export const useAppStore = defineStore('app', {
- state: (): AppState => {
- return {
- userInfo: 'userInfo', // 登录信息存储字段-建议每个项目换一个字段,避免与其他项目冲突
- sizeMap: ['default', 'large', 'small'],
- mobile: false, // 是否是移动端
- title: import.meta.env.VITE_APP_TITLE, // 标题
- pageLoading: false, // 路由跳转loading
- breadcrumb: true, // 面包屑
- breadcrumbIcon: true, // 面包屑图标
- collapse: false, // 折叠菜单
- uniqueOpened: true, // 是否只保持一个子菜单的展开
- hamburger: true, // 折叠图标
- screenfull: true, // 全屏图标
- search: true, // 搜索图标
- size: true, // 尺寸图标
- locale: true, // 多语言图标
- message: true, // 消息图标
- tagsView: true, // 标签页
- tagsViewIcon: true, // 是否显示标签图标
- logo: true, // logo
- fixedHeader: true, // 固定toolheader
- footer: true, // 显示页脚
- greyMode: false, // 是否开始灰色模式,用于特殊悼念日
- fixedMenu: wsCache.get('fixedMenu') || false, // 是否固定菜单
- layout: wsCache.get(CACHE_KEY.LAYOUT) || 'classic', // layout布局
- isDark: wsCache.get(CACHE_KEY.IS_DARK) || false, // 是否是暗黑模式
- currentSize: wsCache.get('default') || 'default', // 组件尺寸
- theme: wsCache.get(CACHE_KEY.THEME) || {
- elColorPrimary: '#409eff',
- leftMenuBorderColor: 'inherit',
- leftMenuBgColor: '#001529',
- leftMenuBgLightColor: '#0f2438',
- leftMenuBgActiveColor: 'var(--el-color-primary)',
- leftMenuCollapseBgActiveColor: 'var(--el-color-primary)',
- leftMenuTextColor: '#bfcbd9',
- leftMenuTextActiveColor: '#fff',
- logoTitleTextColor: '#fff',
- logoBorderColor: 'inherit',
- topHeaderBgColor: '#fff',
- topHeaderTextColor: 'inherit',
- topHeaderHoverColor: '#f6f6f6',
- topToolBorderColor: '#eee'
- getters: {
- getBreadcrumb(): boolean {
- return this.breadcrumb
- getBreadcrumbIcon(): boolean {
- return this.breadcrumbIcon
- getCollapse(): boolean {
- return this.collapse
- getUniqueOpened(): boolean {
- return this.uniqueOpened
- getHamburger(): boolean {
- return this.hamburger
- getScreenfull(): boolean {
- return this.screenfull
- getSize(): boolean {
- return this.size
- getLocale(): boolean {
- return this.locale
- getMessage(): boolean {
- return this.message
- getTagsView(): boolean {
- return this.tagsView
- getTagsViewIcon(): boolean {
- return this.tagsViewIcon
- getLogo(): boolean {
- return this.logo
- getFixedHeader(): boolean {
- return this.fixedHeader
- getGreyMode(): boolean {
- return this.greyMode
- getFixedMenu(): boolean {
- return this.fixedMenu
- getPageLoading(): boolean {
- return this.pageLoading
- getLayout(): LayoutType {
- return this.layout
- getTitle(): string {
- return this.title
- getUserInfo(): string {
- return this.userInfo
- getIsDark(): boolean {
- return this.isDark
- getCurrentSize(): ElementPlusSize {
- return this.currentSize
- getSizeMap(): ElementPlusSize[] {
- return this.sizeMap
- getMobile(): boolean {
- return this.mobile
- getTheme(): ThemeTypes {
- return this.theme
- getFooter(): boolean {
- return this.footer
- actions: {
- setBreadcrumb(breadcrumb: boolean) {
- this.breadcrumb = breadcrumb
- setBreadcrumbIcon(breadcrumbIcon: boolean) {
- this.breadcrumbIcon = breadcrumbIcon
- setCollapse(collapse: boolean) {
- this.collapse = collapse
- setUniqueOpened(uniqueOpened: boolean) {
- this.uniqueOpened = uniqueOpened
- setHamburger(hamburger: boolean) {
- this.hamburger = hamburger
- setScreenfull(screenfull: boolean) {
- this.screenfull = screenfull
- setSize(size: boolean) {
- this.size = size
- setLocale(locale: boolean) {
- this.locale = locale
- setMessage(message: boolean) {
- this.message = message
- setTagsView(tagsView: boolean) {
- this.tagsView = tagsView
- setTagsViewIcon(tagsViewIcon: boolean) {
- this.tagsViewIcon = tagsViewIcon
- setLogo(logo: boolean) {
- this.logo = logo
- setFixedHeader(fixedHeader: boolean) {
- this.fixedHeader = fixedHeader
- setGreyMode(greyMode: boolean) {
- this.greyMode = greyMode
- setFixedMenu(fixedMenu: boolean) {
- wsCache.set('fixedMenu', fixedMenu)
- this.fixedMenu = fixedMenu
- setPageLoading(pageLoading: boolean) {
- this.pageLoading = pageLoading
- setLayout(layout: LayoutType) {
- if (this.mobile && layout !== 'classic') {
- ElMessage.warning('移动端模式下不支持切换其他布局')
- return
- this.layout = layout
- wsCache.set(CACHE_KEY.LAYOUT, this.layout)
- setTitle(title: string) {
- this.title = title
- setIsDark(isDark: boolean) {
- this.isDark = isDark
- if (this.isDark) {
- document.documentElement.classList.add('dark')
- document.documentElement.classList.remove('light')
- document.documentElement.classList.add('light')
- document.documentElement.classList.remove('dark')
- wsCache.set(CACHE_KEY.IS_DARK, this.isDark)
- setCurrentSize(currentSize: ElementPlusSize) {
- this.currentSize = currentSize
- wsCache.set('currentSize', this.currentSize)
- setMobile(mobile: boolean) {
- this.mobile = mobile
- setTheme(theme: ThemeTypes) {
- this.theme = Object.assign(this.theme, theme)
- wsCache.set(CACHE_KEY.THEME, this.theme)
- setCssVarTheme() {
- for (const key in this.theme) {
- setCssVar(`--${humpToUnderline(key)}`, this.theme[key])
- setFooter(footer: boolean) {
- this.footer = footer
-export const useAppStoreWithOut = () => {
- return useAppStore(store)
+import { defineStore } from 'pinia'
+import { store } from '../index'
+import { setCssVar, humpToUnderline } from '@/utils'
+import { ElMessage } from 'element-plus'
+import { CACHE_KEY, useCache } from '@/hooks/web/useCache'
+import { ElementPlusSize } from '@/types/elementPlus'
+import { LayoutType } from '@/types/layout'
+import { ThemeTypes } from '@/types/theme'
+const { wsCache } = useCache()
+interface AppState {
+ breadcrumb : boolean
+ breadcrumbIcon : boolean
+ collapse : boolean
+ uniqueOpened : boolean
+ hamburger : boolean
+ screenfull : boolean
+ search : boolean
+ size : boolean
+ locale : boolean
+ message : boolean
+ tagsView : boolean
+ tagsViewIcon : boolean
+ logo : boolean
+ fixedHeader : boolean
+ greyMode : boolean
+ pageLoading : boolean
+ layout : LayoutType
+ title : string
+ userInfo : string
+ isDark : boolean
+ currentSize : ElementPlusSize
+ sizeMap : ElementPlusSize[]
+ mobile : boolean
+ footer : boolean
+ theme : ThemeTypes
+ fixedMenu : boolean
+export const useAppStore = defineStore('app', {
+ state: () : AppState => {
+ return {
+ userInfo: 'userInfo', // 登录信息存储字段-建议每个项目换一个字段,避免与其他项目冲突
+ sizeMap: ['default', 'large', 'small'],
+ mobile: false, // 是否是移动端
+ title: import.meta.env.VITE_APP_TITLE, // 标题
+ pageLoading: false, // 路由跳转loading
+ breadcrumb: true, // 面包屑
+ breadcrumbIcon: true, // 面包屑图标
+ collapse: false, // 折叠菜单
+ uniqueOpened: true, // 是否只保持一个子菜单的展开
+ hamburger: true, // 折叠图标
+ screenfull: true, // 全屏图标
+ search: false, // 搜索图标
+ size: false, // 尺寸图标
+ locale: false, // 多语言图标
+ message: true, // 消息图标
+ tagsView: false, // 标签页 zx:默认不要标签页
+ tagsViewIcon: true, // 是否显示标签图标
+ logo: true, // logo
+ fixedHeader: true, // 固定toolheader
+ footer: true, // 显示页脚
+ greyMode: false, // 是否开始灰色模式,用于特殊悼念日
+ fixedMenu: wsCache.get('fixedMenu') || false, // 是否固定菜单
+ layout: wsCache.get(CACHE_KEY.LAYOUT) || 'classic', // layout布局
+ // isDark: wsCache.get(CACHE_KEY.IS_DARK) || false, // 是否是暗黑模式
+ isDark: false, // zx:默认没有暗黑模式
+ currentSize: wsCache.get('default') || 'default', // 组件尺寸
+ theme: wsCache.get(CACHE_KEY.THEME) || {
+ elColorPrimary: '#409eff',
+ leftMenuBorderColor: 'inherit',
+ leftMenuBgColor: '#001529',
+ leftMenuBgLightColor: '#0f2438',
+ leftMenuBgActiveColor: 'var(--el-color-primary)',
+ leftMenuCollapseBgActiveColor: 'var(--el-color-primary)',
+ leftMenuTextColor: '#bfcbd9',
+ leftMenuTextActiveColor: '#fff',
+ logoTitleTextColor: '#fff',
+ logoBorderColor: 'inherit',
+ topHeaderBgColor: '#fff',
+ topHeaderTextColor: 'inherit',
+ topHeaderHoverColor: '#f6f6f6',
+ topToolBorderColor: '#eee'
+ getters: {
+ getBreadcrumb() : boolean {
+ return this.breadcrumb
+ getBreadcrumbIcon() : boolean {
+ return this.breadcrumbIcon
+ getCollapse() : boolean {
+ return this.collapse
+ getUniqueOpened() : boolean {
+ return this.uniqueOpened
+ getHamburger() : boolean {
+ return this.hamburger
+ getScreenfull() : boolean {
+ return this.screenfull
+ getSize() : boolean {
+ return this.size
+ getLocale() : boolean {
+ return this.locale
+ getMessage() : boolean {
+ return this.message
+ getTagsView() : boolean {
+ return this.tagsView
+ getTagsViewIcon() : boolean {
+ return this.tagsViewIcon
+ getLogo() : boolean {
+ return this.logo
+ getFixedHeader() : boolean {
+ return this.fixedHeader
+ getGreyMode() : boolean {
+ return this.greyMode
+ getFixedMenu() : boolean {
+ return this.fixedMenu
+ getPageLoading() : boolean {
+ return this.pageLoading
+ getLayout() : LayoutType {
+ return this.layout
+ getTitle() : string {
+ return this.title
+ getUserInfo() : string {
+ return this.userInfo
+ getIsDark() : boolean {
+ return this.isDark
+ getCurrentSize() : ElementPlusSize {
+ return this.currentSize
+ getSizeMap() : ElementPlusSize[] {
+ return this.sizeMap
+ getMobile() : boolean {
+ return this.mobile
+ getTheme() : ThemeTypes {
+ return this.theme
+ getFooter() : boolean {
+ return this.footer
+ actions: {
+ setBreadcrumb(breadcrumb : boolean) {
+ this.breadcrumb = breadcrumb
+ setBreadcrumbIcon(breadcrumbIcon : boolean) {
+ this.breadcrumbIcon = breadcrumbIcon
+ setCollapse(collapse : boolean) {
+ this.collapse = collapse
+ setUniqueOpened(uniqueOpened : boolean) {
+ this.uniqueOpened = uniqueOpened
+ setHamburger(hamburger : boolean) {
+ this.hamburger = hamburger
+ setScreenfull(screenfull : boolean) {
+ this.screenfull = screenfull
+ setSize(size : boolean) {
+ this.size = size
+ setLocale(locale : boolean) {
+ this.locale = locale
+ setMessage(message : boolean) {
+ this.message = message
+ setTagsView(tagsView : boolean) {
+ this.tagsView = tagsView
+ setTagsViewIcon(tagsViewIcon : boolean) {
+ this.tagsViewIcon = tagsViewIcon
+ setLogo(logo : boolean) {
+ this.logo = logo
+ setFixedHeader(fixedHeader : boolean) {
+ this.fixedHeader = fixedHeader
+ setGreyMode(greyMode : boolean) {
+ this.greyMode = greyMode
+ setFixedMenu(fixedMenu : boolean) {
+ wsCache.set('fixedMenu', fixedMenu)
+ this.fixedMenu = fixedMenu
+ setPageLoading(pageLoading : boolean) {
+ this.pageLoading = pageLoading
+ setLayout(layout : LayoutType) {
+ if (this.mobile && layout !== 'classic') {
+ ElMessage.warning('移动端模式下不支持切换其他布局')
+ return
+ this.layout = layout
+ wsCache.set(CACHE_KEY.LAYOUT, this.layout)
+ setTitle(title : string) {
+ this.title = title
+ setIsDark(isDark : boolean) {
+ this.isDark = isDark
+ if (this.isDark) {
+ document.documentElement.classList.add('dark')
+ document.documentElement.classList.remove('light')
+ document.documentElement.classList.add('light')
+ document.documentElement.classList.remove('dark')
+ wsCache.set(CACHE_KEY.IS_DARK, this.isDark)
+ setCurrentSize(currentSize : ElementPlusSize) {
+ this.currentSize = currentSize
+ wsCache.set('currentSize', this.currentSize)
+ setMobile(mobile : boolean) {
+ this.mobile = mobile
+ setTheme(theme : ThemeTypes) {
+ this.theme = Object.assign(this.theme, theme)
+ wsCache.set(CACHE_KEY.THEME, this.theme)
+ setCssVarTheme() {
+ for (const key in this.theme) {
+ setCssVar(`--${humpToUnderline(key)}`, this.theme[key])
+ setFooter(footer : boolean) {
+ this.footer = footer
+})
+export const useAppStoreWithOut = () => {
+ return useAppStore(store)
@@ -1,66 +1,66 @@
-:root {
- --login-bg-color: #293146;
- --left-menu-max-width: 200px;
- --left-menu-min-width: 64px;
- --left-menu-bg-color: #001529;
- --left-menu-bg-light-color: #0f2438;
- --left-menu-bg-active-color: var(--el-color-primary);
- --left-menu-text-color: #bfcbd9;
- --left-menu-text-active-color: #fff;
- --left-menu-collapse-bg-active-color: var(--el-color-primary);
- /* left menu end */
- /* logo start */
- --logo-height: 50px;
- --logo-title-text-color: #fff;
- /* logo end */
- /* header start */
- --top-header-bg-color: '#fff';
- --top-header-text-color: 'inherit';
- --top-header-hover-color: #f6f6f6;
- --top-tool-height: var(--logo-height);
- --top-tool-p-x: 0;
- --tags-view-height: 35px;
- /* tab menu start */
- --tab-menu-max-width: 80px;
- --tab-menu-min-width: 30px;
- --tab-menu-collapse-height: 36px;
- /* tab menu end */
- --app-content-padding: 20px;
- --app-content-bg-color: #f5f7f9;
- --app-footer-height: 50px;
- --transition-time-02: 0.2s;
-.dark {
- --app-content-bg-color: var(--el-bg-color);
-html,
-body {
- -webkit-font-smoothing: antialiased;
- -moz-osx-font-smoothing: grayscale;
+:root {
+ --login-bg-color: #293146;
+ --left-menu-max-width: 200px;
+ --left-menu-min-width: 64px;
+ --left-menu-bg-color: #001529;
+ --left-menu-bg-light-color: #0f2438;
+ --left-menu-bg-active-color: var(--el-color-primary);
+ --left-menu-text-color: #bfcbd9;
+ --left-menu-text-active-color: #fff;
+ --left-menu-collapse-bg-active-color: var(--el-color-primary);
+ /* left menu end */
+ /* logo start */
+ --logo-height: 50px;
+ --logo-title-text-color: #fff;
+ /* logo end */
+ /* header start */
+ --top-header-bg-color: '#fff';
+ --top-header-text-color: 'inherit';
+ --top-header-hover-color: #f6f6f6;
+ --top-tool-height: var(--logo-height);
+ --top-tool-p-x: 0;
+ --tags-view-height: 0px;
+ /* tab menu start */
+ --tab-menu-max-width: 80px;
+ --tab-menu-min-width: 30px;
+ --tab-menu-collapse-height: 36px;
+ /* tab menu end */
+ --app-content-padding: 20px;
+ --app-content-bg-color: #f5f7f9;
+ --app-footer-height: 50px;
+ --transition-time-02: 0.2s;
+.dark {
+ --app-content-bg-color: var(--el-bg-color);
+html,
+body {
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
@@ -1,5 +1,5 @@
/**
- * Created by 芋道源码
+ * Created by 非繁源码
*
* 枚举类
*/
@@ -1,384 +1,363 @@
- <el-card shadow="never">
- <el-skeleton :loading="loading" animated>
- <el-row :gutter="16" justify="space-between">
- <el-col :xl="12" :lg="12" :md="12" :sm="24" :xs="24">
- <div class="flex items-center">
- <el-avatar :src="avatar" :size="70" class="mr-16px">
- <img src="@/assets/imgs/avatar.gif" alt="" />
- </el-avatar>
- <div class="text-20px">
- {{ t('workplace.welcome') }} {{ username }} {{ t('workplace.happyDay') }}
- <div class="mt-10px text-14px text-gray-500">
- {{ t('workplace.toady') }},20℃ - 32℃!
- </el-col>
- <div class="h-70px flex items-center justify-end lt-sm:mt-10px">
- <div class="px-8px text-right">
- <div class="mb-16px text-14px text-gray-400">{{ t('workplace.project') }}</div>
- <CountTo
- class="text-20px"
- :start-val="0"
- :end-val="totalSate.project"
- :duration="2600"
- <el-divider direction="vertical" />
- <div class="mb-16px text-14px text-gray-400">{{ t('workplace.toDo') }}</div>
- :end-val="totalSate.todo"
- <el-divider direction="vertical" border-style="dashed" />
- <div class="mb-16px text-14px text-gray-400">{{ t('workplace.access') }}</div>
- :end-val="totalSate.access"
- </el-row>
- </el-skeleton>
- </el-card>
- <el-row class="mt-8px" :gutter="8" justify="space-between">
- <el-col :xl="16" :lg="16" :md="24" :sm="24" :xs="24" class="mb-8px">
- <div class="h-3 flex justify-between">
- <span>{{ t('workplace.project') }}</span>
- <el-link type="primary" :underline="false">{{ t('action.more') }}</el-link>
- <el-row>
- <el-col
- v-for="(item, index) in projects"
- :key="`card-${index}`"
- :xl="8"
- :lg="8"
- :md="8"
- :sm="24"
- :xs="24"
- <el-card shadow="hover">
- <Icon :icon="item.icon" :size="25" class="mr-8px" />
- <span class="text-16px">{{ item.name }}</span>
- <div class="mt-16px text-14px text-gray-400">{{ t(item.message) }}</div>
- <div class="mt-16px flex justify-between text-12px text-gray-400">
- <span>{{ item.personal }}</span>
- <span>{{ formatTime(item.time, 'yyyy-MM-dd') }}</span>
- <el-card shadow="never" class="mt-8px">
- <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-8px">
- <Echart :options="pieOptionsData" :height="280" />
- <el-col :xl="14" :lg="14" :md="24" :sm="24" :xs="24">
- <Echart :options="barOptionsData" :height="280" />
- <el-col :xl="8" :lg="8" :md="24" :sm="24" :xs="24" class="mb-8px">
- <span>{{ t('workplace.shortcutOperation') }}</span>
- <el-col v-for="item in shortcut" :key="`team-${item.name}`" :span="8" class="mb-8px">
- <Icon :icon="item.icon" class="mr-8px" />
- <el-link type="default" :underline="false" @click="setWatermark(item.name)">
- {{ item.name }}
- </el-link>
- <span>{{ t('workplace.notice') }}</span>
- <div v-for="(item, index) in notice" :key="`dynamics-${index}`">
- <el-avatar :src="avatar" :size="35" class="mr-16px">
- <div class="text-14px">
- <Highlight :keys="item.keys.map((v) => t(v))">
- {{ item.type }} : {{ item.title }}
- </Highlight>
- <div class="mt-16px text-12px text-gray-400">
- {{ formatTime(item.date, 'yyyy-MM-dd') }}
- <el-divider />
-import { set } from 'lodash-es'
-import { EChartsOption } from 'echarts'
-import { formatTime } from '@/utils'
-import { useUserStore } from '@/store/modules/user'
-import type { WorkplaceTotal, Project, Notice, Shortcut } from './types'
-import { pieOptions, barOptions } from './echarts-data'
-defineOptions({ name: 'Home' })
-const userStore = useUserStore()
-const loading = ref(true)
-const avatar = userStore.getUser.avatar
-const username = userStore.getUser.nickname
-const pieOptionsData = reactive<EChartsOption>(pieOptions) as EChartsOption
-// 获取统计数
-let totalSate = reactive<WorkplaceTotal>({
- project: 0,
- access: 0,
- todo: 0
-const getCount = async () => {
- const data = {
- project: 40,
- access: 2340,
- todo: 10
- totalSate = Object.assign(totalSate, data)
-// 获取项目数
-let projects = reactive<Project[]>([])
-const getProject = async () => {
- const data = [
- name: 'Github',
- icon: 'akar-icons:github-fill',
- message: 'workplace.introduction',
- personal: 'Archer',
- time: new Date()
- name: 'Vue',
- icon: 'logos:vue',
- name: 'Angular',
- icon: 'logos:angular-icon',
- name: 'React',
- icon: 'logos:react',
- name: 'Webpack',
- icon: 'logos:webpack',
- name: 'Vite',
- icon: 'vscode-icons:file-type-vite',
- ]
- projects = Object.assign(projects, data)
-// 获取通知公告
-let notice = reactive<Notice[]>([])
-const getNotice = async () => {
- title: '系统升级版本',
- type: '通知',
- keys: ['通知', '升级'],
- date: new Date()
- title: '系统凌晨维护',
- type: '公告',
- keys: ['公告', '维护'],
- notice = Object.assign(notice, data)
-// 获取快捷入口
-let shortcut = reactive<Shortcut[]>([])
-const getShortcut = async () => {
- url: 'github.io'
- url: 'vuejs.org'
- url: 'https://vitejs.dev/'
- shortcut = Object.assign(shortcut, data)
-// 用户来源
-const getUserAccessSource = async () => {
- { value: 335, name: 'analysis.directAccess' },
- { value: 310, name: 'analysis.mailMarketing' },
- { value: 234, name: 'analysis.allianceAdvertising' },
- { value: 135, name: 'analysis.videoAdvertising' },
- { value: 1548, name: 'analysis.searchEngines' }
- set(
- pieOptionsData,
- 'legend.data',
- data.map((v) => t(v.name))
- )
- pieOptionsData!.series![0].data = data.map((v) => {
- name: t(v.name),
- value: v.value
-const barOptionsData = reactive<EChartsOption>(barOptions) as EChartsOption
-// 周活跃量
-const getWeeklyUserActivity = async () => {
- { value: 13253, name: 'analysis.monday' },
- { value: 34235, name: 'analysis.tuesday' },
- { value: 26321, name: 'analysis.wednesday' },
- { value: 12340, name: 'analysis.thursday' },
- { value: 24643, name: 'analysis.friday' },
- { value: 1322, name: 'analysis.saturday' },
- { value: 1324, name: 'analysis.sunday' }
- barOptionsData,
- 'xAxis.data',
- set(barOptionsData, 'series', [
- name: t('analysis.activeQuantity'),
- data: data.map((v) => v.value),
- type: 'bar'
- ])
-const getAllApi = async () => {
- await Promise.all([
- getCount(),
- getProject(),
- getNotice(),
- getShortcut(),
- getUserAccessSource(),
- getWeeklyUserActivity()
- loading.value = false
-getAllApi()
+ <el-row class="mt-8px" :gutter="8" justify="space-between">
+ <el-col :xl="12" :lg="12" :md="24" :sm="24" :xs="24" class="mb-8px">
+ <el-card shadow="never">
+ <div class="h-3 flex justify-between">
+ <span>待办</span>
+ <!-- <el-link type="primary" :underline="false">{{ t('action.more') }}</el-link> -->
+ <el-skeleton :loading="loading" animated>
+ <ToBeDone />
+ </el-skeleton>
+ </el-card>
+ </el-col>
+ <span>今日订单【统计】</span>
+ <el-row>
+ <el-col>
+ <el-card shadow="hover" class="mb-8px">
+ <Echart :options="todayOrderOptionsData" :height="210" />
+ </el-row>
+ <el-row :gutter="8" justify="space-between">
+ <el-col :xl="8" :lg="8" :md="24" :sm="24" :xs="24" class="mb-8px">
+ <span>快捷方式</span>
+ <div class="flex flex-row flex-wrap gap-8 p-4">
+ <div class="h-20 w-20% flex flex-col cursor-pointer items-center justify-center gap-2"
+ @click="openSpuForm('create','',0)">
+ <div
+ class="bg-orange-400 h-48px w-48px flex items-center justify-center rounded text-white">
+ <Icon icon="fluent-mdl2:product" class="text-7.5!" />
+ <span>新增商品</span>
+ <span>用户访问来源</span>
+ <Echart :options="pieOptionsData" :height="280" />
+ <span>每周用户活跃量</span>
+ <Echart :options="barOptionsData" :height="280" />
+ <!-- 商品列表表单弹窗:添加/修改 -->
+ <SpuIndex ref="spuFormRef" @success="getList" />
+ import { set } from 'lodash-es'
+ import { EChartsOption } from 'echarts'
+ import { formatTime } from '@/utils'
+ import { useUserStore } from '@/store/modules/user'
+ import type { WorkplaceTotal, Project, Notice, Shortcut } from './types'
+ import { pieOptions, barOptions, pieOptions2 } from './echarts-data'
+ import ToBeDone from './components/ToBeDone.vue'
+ import SpuIndex from "../mall/product/spu/form/index.vue"
+ defineOptions({ name: 'Home' })
+ const spuFormRef = ref()
+ const openSpuForm = (type : string, row : any, newStatus : number, tabType : number) => {
+ spuFormRef.value.open(type, row, newStatus, tabType)
+ const userStore = useUserStore()
+ const loading = ref(true)
+ const avatar = userStore.getUser.avatar
+ const username = userStore.getUser.nickname
+ const pieOptionsData = reactive<EChartsOption>(pieOptions) as EChartsOption
+ // 获取统计数
+ let totalSate = reactive<WorkplaceTotal>({
+ project: 0,
+ access: 0,
+ todo: 0
+ const getCount = async () => {
+ const data = {
+ project: 40,
+ access: 2340,
+ todo: 10
+ totalSate = Object.assign(totalSate, data)
+ // 获取项目数
+ let projects = reactive<Project[]>([])
+ const getProject = async () => {
+ const data = [
+ name: 'Github',
+ icon: 'akar-icons:github-fill',
+ message: 'workplace.introduction',
+ personal: 'Archer',
+ time: new Date()
+ name: 'Vue',
+ icon: 'logos:vue',
+ name: 'Angular',
+ icon: 'logos:angular-icon',
+ name: 'React',
+ icon: 'logos:react',
+ name: 'Webpack',
+ icon: 'logos:webpack',
+ name: 'Vite',
+ icon: 'vscode-icons:file-type-vite',
+ ]
+ projects = Object.assign(projects, data)
+ // 获取通知公告
+ let notice = reactive<Notice[]>([])
+ const getNotice = async () => {
+ title: '系统升级版本',
+ type: '通知',
+ keys: ['通知', '升级'],
+ date: new Date()
+ title: '系统凌晨维护',
+ type: '公告',
+ keys: ['公告', '维护'],
+ notice = Object.assign(notice, data)
+ // 获取快捷入口
+ let shortcut = reactive<Shortcut[]>([])
+ const getShortcut = async () => {
+ url: 'github.io'
+ url: 'vuejs.org'
+ url: 'https://vitejs.dev/'
+ shortcut = Object.assign(shortcut, data)
+ // 用户来源
+ const getUserAccessSource = async () => {
+ { value: 335, name: 'analysis.directAccess' },
+ { value: 310, name: 'analysis.mailMarketing' },
+ { value: 234, name: 'analysis.allianceAdvertising' },
+ { value: 135, name: 'analysis.videoAdvertising' },
+ { value: 1548, name: 'analysis.searchEngines' }
+ set(
+ pieOptionsData,
+ 'legend.data',
+ data.map((v) => t(v.name))
+ pieOptionsData!.series![0].data = data.map((v) => {
+ name: t(v.name),
+ value: v.value
+ const barOptionsData = reactive<EChartsOption>(barOptions) as EChartsOption
+ // 周活跃量
+ const getWeeklyUserActivity = async () => {
+ { value: 13253, name: 'analysis.monday' },
+ { value: 34235, name: 'analysis.tuesday' },
+ { value: 26321, name: 'analysis.wednesday' },
+ { value: 12340, name: 'analysis.thursday' },
+ { value: 24643, name: 'analysis.friday' },
+ { value: 1322, name: 'analysis.saturday' },
+ { value: 1324, name: 'analysis.sunday' }
+ barOptionsData,
+ 'xAxis.data',
+ set(barOptionsData, 'series', [
+ name: t('analysis.activeQuantity'),
+ data: data.map((v) => v.value),
+ type: 'bar'
+ ])
+ const todayOrderOptionsData = reactive<EChartsOption>(pieOptions2) as EChartsOption
+ // 今日订单
+ const getTodayOrder = async () => {
+ { value: 335, name: '待支付' },
+ { value: 310, name: '已下单' },
+ { value: 234, name: '取消支付' },
+ todayOrderOptionsData,
+ todayOrderOptionsData!.series![0].data = data.map((v) => {
+ const getAllApi = async () => {
+ await Promise.all([
+ getCount(),
+ getProject(),
+ getNotice(),
+ getShortcut(),
+ getUserAccessSource(),
+ getWeeklyUserActivity(),
+ getTodayOrder()
+ loading.value = false
+ getAllApi()
+<style scoped lang='scss'>
+ .kjfs {
+ width: 65px;
+ height: 65px;
+ border-radius: 4px;
+ border: 1px solid #e4e7ed;
+ padding: 10px;
+ font-size: 14px;
@@ -0,0 +1,224 @@
+ <el-tree-v2 :data="treeData" :props="defaultProps" ref="treeRef" :height="250" @node-expand="nodeExpand"
+ @node-collapse="nodeCollapse">
+ <template #default="{ node, data }">
+ <!-- 目录并且子项目大于1 -->
+ <p v-if="data.children != null" :title=" node.label"
+ style="display:flex;align-items: center;position: relative;">
+ <span v-if="!data.isOpen" class="mr-10px folder folder-close">{{node.children.length}}</span>
+ <span v-else class="mr-10px folder folder-open">{{node.children.length}}</span>
+ <!-- <span style="position: absolute;left: 8px;top:5px"></span> -->
+ {{ node.label }}
+ </p>
+ <!-- <i style="display: inline-flex; align-items: center;">
+ <svg style="margin: 5px 5px 2px 3px" viewBox="0 0 20 20" width="25" height="25">
+ <path
+ d="M13,6 L9,6 L9,5 L9,2 L3,2 L3,14 L13,14 L13,6 Z M12.5857864,5 L10,2.41421356 L10,5 L12.5857864,5 Z M2,1 L10,1 L14,5 L14,15 L2,15 L2,1 Z"
+ stroke-width="1" fill="#8a8e99" />
+ </svg>
+ </i> -->
+ <!-- 文档 -->
+ <div v-else :title="node.label" class="file" @click="toBeDoneClick(data.type,data.orderId)">
+ <span></span>
+ <p>
+ <span>
+ </span>
+ <span>{{data.time}}</span>
+ </el-tree-v2>
+ <OrderDeliveryForm ref="deliveryFormRef" @success="getList" />
+ import OrderDeliveryForm from '@/views/mall/trade/order/form/OrderDeliveryForm.vue'
+ const deliveryFormRef = ref()
+ const defaultProps = {
+ children: 'children',
+ label: 'name',
+ nums: 'childrenNums'
+ const treeRef = ref(null)
+ const treeData = ref(
+ [
+ id: 1,
+ name: '草稿',
+ isOpen: false,
+ children: [
+ name: '草稿1111',
+ time: "2024-2-24 20:40"
+ name: '草稿2222',
+ ],
+ id: 2,
+ name: '待发货',
+ type: "2",
+ orderId: 10,
+ name: '待发货1111',
+ id: 3,
+ name: '待售后',
+ name: '待售后1111',
+ id: 4,
+ name: '待开票',
+ name: '待开票1111',
+ const nodeExpand = (data) => {
+ data.isOpen = true
+ const nodeCollapse = (data) => {
+ data.isOpen = false
+ const toBeDoneClick = (type, id) => {
+ console.log(type, id)
+ // 如果是类型是2,代表是待发货,调起待发货弹窗
+ if (type == 2) {
+ deliveryFormRef.value?.open({}, "待办", id)
+<style>
+ .el-tree-node__expand-icon {
+ display: none
+ .el-tree-node__content:hover,
+ .el-tree-node:focus>.el-tree-node__content {
+ background: unset;
+ .el-tree-node {
+ /* height: 40px !important; */
+ /* line-height: 50px */
+<style lang='scss' scoped>
+ .file {
+ padding-left: 10px;
+ height: 26px;
+ /* margin-top: 10px; */
+ &>span {
+ width: 5px;
+ height: 5px;
+ background-color: #575d6d;
+ border-radius: 50%;
+ margin-right: 5px;
+ /* margin-bottom: 10px; */
+ p {
+ width: calc(100% - 25px);
+ justify-content: space-between;
+ padding-bottom: 2px;
+ border-bottom: 1px solid #eeeeee;
+ margin: 0;
+ color: #575d6d
+ .folder {
+ width: 23px;
+ height: 23px;
+ font-size: 10px;
+ line-height: 28px;
+ /* background: red; */
+ &-close {
+ background: url("@/assets/svgs/wenjianjia_shouqi.svg") no-repeat center center;
+ &-open {
+ background: url('@/assets/svgs/wenjianjia_zhankai.svg') no-repeat center center;
+ &-close:hover {
+ background: url("/src/assets/svgs/folderShare.svg") no-repeat center center;
+ color: white;
+ &-open:hover {
+ background: url('@/assets/svgs/olderOpen.svg') no-repeat center center;
@@ -1,308 +1,347 @@
-export const lineOptions: EChartsOption = {
- title: {
- text: t('analysis.monthlySales'),
- left: 'center'
- xAxis: {
- data: [
- t('analysis.january'),
- t('analysis.february'),
- t('analysis.march'),
- t('analysis.april'),
- t('analysis.may'),
- t('analysis.june'),
- t('analysis.july'),
- t('analysis.august'),
- t('analysis.september'),
- t('analysis.october'),
- t('analysis.november'),
- t('analysis.december')
- ],
- boundaryGap: false,
- axisTick: {
- show: false
- grid: {
- left: 20,
- right: 20,
- bottom: 20,
- top: 80,
- containLabel: true
- tooltip: {
- trigger: 'axis',
- axisPointer: {
- type: 'cross'
- padding: [5, 10]
- yAxis: {
- legend: {
- data: [t('analysis.estimate'), t('analysis.actual')],
- top: 50
- series: [
- name: t('analysis.estimate'),
- smooth: true,
- type: 'line',
- data: [100, 120, 161, 134, 105, 160, 165, 114, 163, 185, 118, 123],
- animationDuration: 2800,
- animationEasing: 'cubicInOut'
- name: t('analysis.actual'),
- itemStyle: {},
- data: [120, 82, 91, 154, 162, 140, 145, 250, 134, 56, 99, 123],
- animationEasing: 'quadraticOut'
-export const pieOptions: EChartsOption = {
- text: t('analysis.userAccessSource'),
- trigger: 'item',
- formatter: '{a} <br/>{b} : {c} ({d}%)'
- orient: 'vertical',
- left: 'left',
- t('analysis.directAccess'),
- t('analysis.mailMarketing'),
- t('analysis.allianceAdvertising'),
- t('analysis.videoAdvertising'),
- t('analysis.searchEngines')
- name: t('analysis.userAccessSource'),
- type: 'pie',
- radius: '55%',
- center: ['50%', '60%'],
- { value: 335, name: t('analysis.directAccess') },
- { value: 310, name: t('analysis.mailMarketing') },
- { value: 234, name: t('analysis.allianceAdvertising') },
- { value: 135, name: t('analysis.videoAdvertising') },
- { value: 1548, name: t('analysis.searchEngines') }
-export const barOptions: EChartsOption = {
- text: t('analysis.weeklyUserActivity'),
- type: 'shadow'
- left: 50,
- bottom: 20
- type: 'category',
- t('analysis.monday'),
- t('analysis.tuesday'),
- t('analysis.wednesday'),
- t('analysis.thursday'),
- t('analysis.friday'),
- t('analysis.saturday'),
- t('analysis.sunday')
- alignWithLabel: true
- type: 'value'
- data: [13253, 34235, 26321, 12340, 24643, 1322, 1324],
-export const radarOption: EChartsOption = {
- data: [t('workplace.personal'), t('workplace.team')]
- radar: {
- // shape: 'circle',
- indicator: [
- { name: t('workplace.quote'), max: 65 },
- { name: t('workplace.contribution'), max: 160 },
- { name: t('workplace.hot'), max: 300 },
- { name: t('workplace.yield'), max: 130 },
- { name: t('workplace.follow'), max: 100 }
- name: `xxx${t('workplace.index')}`,
- type: 'radar',
- value: [42, 30, 20, 35, 80],
- name: t('workplace.personal')
- value: [50, 140, 290, 100, 90],
- name: t('workplace.team')
-export const wordOptions = {
- type: 'wordCloud',
- gridSize: 2,
- sizeRange: [12, 50],
- rotationRange: [-90, 90],
- shape: 'pentagon',
- width: 600,
- height: 400,
- drawOutOfBound: true,
- textStyle: {
- color: function () {
- return (
- 'rgb(' +
- [
- Math.round(Math.random() * 160),
- Math.round(Math.random() * 160)
- ].join(',') +
- ')'
- emphasis: {
- shadowBlur: 10,
- shadowColor: '#333'
- name: 'Sam S Club',
- value: 10000,
- color: 'black'
- color: 'red'
- name: 'Macys',
- value: 6181
- name: 'Amy Schumer',
- value: 4386
- name: 'Jurassic World',
- value: 4055
- name: 'Charter Communications',
- value: 2467
- name: 'Chick Fil A',
- value: 2244
- name: 'Planet Fitness',
- value: 1898
- name: 'Pitch Perfect',
- value: 1484
- name: 'Express',
- value: 1112
- name: 'Home',
- value: 965
- name: 'Johnny Depp',
- value: 847
- name: 'Lena Dunham',
- value: 582
- name: 'Lewis Hamilton',
- value: 555
- name: 'KXAN',
- value: 550
- name: 'Mary Ellen Mark',
- value: 462
- name: 'Farrah Abraham',
- value: 366
- name: 'Rita Ora',
- value: 360
- name: 'Serena Williams',
- value: 282
- name: 'NCAA baseball tournament',
- value: 273
- name: 'Point Break',
- value: 265
+import { EChartsOption } from 'echarts'
+const { t } = useI18n()
+export const lineOptions : EChartsOption = {
+ title: {
+ text: t('analysis.monthlySales'),
+ left: 'center'
+ xAxis: {
+ data: [
+ t('analysis.january'),
+ t('analysis.february'),
+ t('analysis.march'),
+ t('analysis.april'),
+ t('analysis.may'),
+ t('analysis.june'),
+ t('analysis.july'),
+ t('analysis.august'),
+ t('analysis.september'),
+ t('analysis.october'),
+ t('analysis.november'),
+ t('analysis.december')
+ boundaryGap: false,
+ axisTick: {
+ show: false
+ grid: {
+ left: 20,
+ right: 20,
+ bottom: 20,
+ top: 80,
+ containLabel: true
+ tooltip: {
+ trigger: 'axis',
+ axisPointer: {
+ type: 'cross'
+ padding: [5, 10]
+ yAxis: {
+ legend: {
+ data: [t('analysis.estimate'), t('analysis.actual')],
+ top: 50
+ series: [
+ name: t('analysis.estimate'),
+ smooth: true,
+ type: 'line',
+ data: [100, 120, 161, 134, 105, 160, 165, 114, 163, 185, 118, 123],
+ animationDuration: 2800,
+ animationEasing: 'cubicInOut'
+ name: t('analysis.actual'),
+ itemStyle: {},
+ data: [120, 82, 91, 154, 162, 140, 145, 250, 134, 56, 99, 123],
+ animationEasing: 'quadraticOut'
+export const pieOptions : EChartsOption = {
+ // text: t('analysis.userAccessSource'),
+ trigger: 'item',
+ formatter: '{a} <br/>{b} : {c} ({d}%)'
+ orient: 'vertical',
+ left: 'left',
+ t('analysis.directAccess'),
+ t('analysis.mailMarketing'),
+ t('analysis.allianceAdvertising'),
+ t('analysis.videoAdvertising'),
+ t('analysis.searchEngines')
+ name: t('analysis.userAccessSource'),
+ type: 'pie',
+ radius: '55%',
+ center: ['50%', '60%'],
+ { value: 335, name: t('analysis.directAccess') },
+ { value: 310, name: t('analysis.mailMarketing') },
+ { value: 234, name: t('analysis.allianceAdvertising') },
+ { value: 135, name: t('analysis.videoAdvertising') },
+ { value: 1548, name: t('analysis.searchEngines') }
+export const pieOptions2 : EChartsOption = {
+ // text: '今日订单',
+export const barOptions : EChartsOption = {
+ // text: t('analysis.weeklyUserActivity'),
+ type: 'shadow'
+ left: 50,
+ bottom: 20
+ type: 'category',
+ t('analysis.monday'),
+ t('analysis.tuesday'),
+ t('analysis.wednesday'),
+ t('analysis.thursday'),
+ t('analysis.friday'),
+ t('analysis.saturday'),
+ t('analysis.sunday')
+ alignWithLabel: true
+ type: 'value'
+ data: [13253, 34235, 26321, 12340, 24643, 1322, 1324],
+export const radarOption : EChartsOption = {
+ data: [t('workplace.personal'), t('workplace.team')]
+ radar: {
+ // shape: 'circle',
+ indicator: [
+ { name: t('workplace.quote'), max: 65 },
+ { name: t('workplace.contribution'), max: 160 },
+ { name: t('workplace.hot'), max: 300 },
+ { name: t('workplace.yield'), max: 130 },
+ { name: t('workplace.follow'), max: 100 }
+ name: `xxx${t('workplace.index')}`,
+ type: 'radar',
+ value: [42, 30, 20, 35, 80],
+ name: t('workplace.personal')
+ value: [50, 140, 290, 100, 90],
+ name: t('workplace.team')
+export const wordOptions = {
+ type: 'wordCloud',
+ gridSize: 2,
+ sizeRange: [12, 50],
+ rotationRange: [-90, 90],
+ shape: 'pentagon',
+ width: 600,
+ height: 400,
+ drawOutOfBound: true,
+ textStyle: {
+ color: function () {
+ return (
+ 'rgb(' +
+ Math.round(Math.random() * 160),
+ Math.round(Math.random() * 160)
+ ].join(',') +
+ ')'
+ emphasis: {
+ shadowBlur: 10,
+ shadowColor: '#333'
+ name: 'Sam S Club',
+ value: 10000,
+ color: 'black'
+ color: 'red'
+ name: 'Macys',
+ value: 6181
+ name: 'Amy Schumer',
+ value: 4386
+ name: 'Jurassic World',
+ value: 4055
+ name: 'Charter Communications',
+ value: 2467
+ name: 'Chick Fil A',
+ value: 2244
+ name: 'Planet Fitness',
+ value: 1898
+ name: 'Pitch Perfect',
+ value: 1484
+ name: 'Express',
+ value: 1112
+ name: 'Home',
+ value: 965
+ name: 'Johnny Depp',
+ value: 847
+ name: 'Lena Dunham',
+ value: 582
+ name: 'Lewis Hamilton',
+ value: 555
+ name: 'KXAN',
+ value: 550
+ name: 'Mary Ellen Mark',
+ value: 462
+ name: 'Farrah Abraham',
+ value: 366
+ name: 'Rita Ora',
+ value: 360
+ name: 'Serena Williams',
+ value: 282
+ name: 'NCAA baseball tournament',
+ value: 273
+ name: 'Point Break',
+ value: 265
@@ -1,104 +1,92 @@
- class="relative h-[100%] lt-xl:px-10px lt-md:px-10px lt-sm:px-10px lt-xl:px-10px"
- <div class="relative mx-auto h-full flex">
- :class="`${prefixCls}__left flex-1 bg-gray-500 bg-opacity-20 relative p-30px lt-xl:hidden`"
- <!-- 左上角的 logo + 系统标题 -->
- <div class="relative flex items-center text-white">
- <img alt="" class="mr-10px h-48px w-48px" src="@/assets/imgs/logo.png" />
- <span class="text-20px font-bold">{{ underlineToHump(appStore.getTitle) }}</span>
- <!-- 左边的背景图 + 欢迎语 -->
- <div class="h-[calc(100%-60px)] flex items-center justify-center">
- <TransitionGroup
- appear
- enter-active-class="animate__animated animate__bounceInLeft"
- tag="div"
- <img key="1" alt="" class="w-350px" src="@/assets/svgs/login-box-bg.svg" />
- <div key="2" class="text-3xl text-white">{{ t('login.welcome') }}</div>
- <div key="3" class="mt-5 text-14px font-normal text-white">
- {{ t('login.message') }}
- </TransitionGroup>
- <div class="relative flex-1 p-30px dark:bg-[var(--login-bg-color)] lt-sm:p-10px">
- <!-- 右上角的主题、语言选择 -->
- class="flex items-center justify-between text-white at-2xl:justify-end at-xl:justify-end"
- <div class="flex items-center at-2xl:hidden at-xl:hidden">
- <div class="flex items-center justify-end space-x-10px">
- <LocaleDropdown class="dark:text-white lt-xl:text-white" />
- <!-- 右边的登录界面 -->
- <Transition appear enter-active-class="animate__animated animate__bounceInRight">
- class="m-auto h-full w-[100%] flex items-center at-2xl:max-w-500px at-lg:max-w-500px at-md:max-w-500px at-xl:max-w-500px"
- <!-- 账号登录 -->
- <LoginForm class="m-auto h-auto p-20px lt-xl:(rounded-3xl light:bg-white)" />
- <!-- 手机登录 -->
- <MobileForm class="m-auto h-auto p-20px lt-xl:(rounded-3xl light:bg-white)" />
- <!-- 二维码登录 -->
- <QrCodeForm class="m-auto h-auto p-20px lt-xl:(rounded-3xl light:bg-white)" />
- <!-- 注册 -->
- <RegisterForm class="m-auto h-auto p-20px lt-xl:(rounded-3xl light:bg-white)" />
- <!-- 三方登录 -->
- <SSOLoginVue class="m-auto h-auto p-20px lt-xl:(rounded-3xl light:bg-white)" />
- </Transition>
-import { underlineToHump } from '@/utils'
-import { LocaleDropdown } from '@/layout/components/LocaleDropdown'
-import { LoginForm, MobileForm, QrCodeForm, RegisterForm, SSOLoginVue } from './components'
-defineOptions({ name: 'Login' })
-const prefixCls = getPrefixCls('login')
-$prefix-cls: #{$namespace}-login;
- overflow: auto;
- &__left {
- &::before {
- position: absolute;
- top: 0;
- left: 0;
- z-index: -1;
- width: 100%;
- height: 100%;
- background-image: url('@/assets/svgs/login-bg.svg');
- background-position: center;
- background-repeat: no-repeat;
- content: '';
+ <div :class="prefixCls" class="relative h-[100%] lt-xl:px-10px lt-md:px-10px lt-sm:px-10px lt-xl:px-10px">
+ <div class="relative mx-auto h-full flex">
+ <div :class="`${prefixCls}__left flex-1 bg-opacity-20 relative p-30px lt-xl:hidden`">
+ <!-- 左上角的 logo + 系统标题 -->
+ <!-- <div class="relative flex items-center text-white">
+ <img alt="" class="mr-10px h-48px w-48px" src="@/assets/imgs/logo.png" />
+ <span class="text-20px font-bold">{{ underlineToHump(appStore.getTitle) }}</span>
+ <!-- 左边的背景图 + 欢迎语 -->
+ <div class="h-[calc(100%-60px)] flex items-center justify-center">
+ <TransitionGroup appear enter-active-class="animate__animated animate__bounceInLeft" tag="div">
+ <!-- <img key="1" alt="" class="w-350px" src="@/assets/svgs/login-box-bg.svg" /> -->
+ <!-- <div key="2" class="text-3xl text-white">{{ t('login.welcome') }}</div> -->
+ <!-- <div key="3" class="mt-5 text-14px font-normal text-white">
+ {{ t('login.message') }}
+ </TransitionGroup>
+ <div class="relative flex-1 p-30px dark:bg-[var(--login-bg-color)] lt-sm:p-10px">
+ <!-- 右上角的主题、语言选择 -->
+ <!-- <div class="flex items-center justify-between text-white at-2xl:justify-end at-xl:justify-end">
+ <div class="flex items-center at-2xl:hidden at-xl:hidden">
+ <div class="flex items-center justify-end space-x-10px">
+ <LocaleDropdown class="dark:text-white lt-xl:text-white" />
+ <!-- 右边的登录界面 -->
+ <Transition appear enter-active-class="animate__animated animate__bounceInRight">
+ class="m-auto h-full w-[100%] flex items-center at-2xl:max-w-500px at-lg:max-w-500px at-md:max-w-500px at-xl:max-w-500px">
+ <!-- 账号登录 -->
+ <LoginForm class="m-auto h-auto p-20px lt-xl:(rounded-3xl light:bg-white)" />
+ <!-- 手机登录 -->
+ <MobileForm class="m-auto h-auto p-20px lt-xl:(rounded-3xl light:bg-white)" />
+ <!-- 二维码登录 -->
+ <QrCodeForm class="m-auto h-auto p-20px lt-xl:(rounded-3xl light:bg-white)" />
+ <!-- 注册 -->
+ <RegisterForm class="m-auto h-auto p-20px lt-xl:(rounded-3xl light:bg-white)" />
+ <!-- 三方登录 -->
+ <SSOLoginVue class="m-auto h-auto p-20px lt-xl:(rounded-3xl light:bg-white)" />
+ </Transition>
+ import { underlineToHump } from '@/utils'
+ import { LocaleDropdown } from '@/layout/components/LocaleDropdown'
+ import { LoginForm, MobileForm, QrCodeForm, RegisterForm, SSOLoginVue } from './components'
+ defineOptions({ name: 'Login' })
+ const prefixCls = getPrefixCls('login')
+ $prefix-cls: #{$namespace}-login;
+ overflow: auto;
+ &__left {
+ &::before {
+ left: 0;
+ z-index: -1;
+ background-image: url('@/assets/svgs/login-bg.svg');
+ background-position: center;
+ background-repeat: no-repeat;
+ content: '';
@@ -196,9 +196,9 @@ const loginData = reactive({
captchaEnable: import.meta.env.VITE_APP_CAPTCHA_ENABLE,
tenantEnable: import.meta.env.VITE_APP_TENANT_ENABLE,
loginForm: {
- tenantName: '芋道源码',
+ tenantName: '非繁科技',
username: 'admin',
- password: 'admin123',
+ password: 'zx123',
captchaVerification: '',
rememberMe: false
@@ -1,348 +1,306 @@
- <el-form
- v-show="getShow"
- ref="formLogin"
- :model="loginData.loginForm"
- :rules="LoginRules"
- class="login-form"
- label-position="top"
- label-width="120px"
- size="large"
- <el-row style="margin-right: -10px; margin-left: -10px">
- <el-col :span="24" style="padding-right: 10px; padding-left: 10px">
- <el-form-item>
- <LoginFormTitle style="width: 100%" />
- </el-form-item>
- <el-form-item v-if="loginData.tenantEnable === 'true'" prop="tenantName">
- <el-input
- v-model="loginData.loginForm.tenantName"
- :placeholder="t('login.tenantNamePlaceholder')"
- :prefix-icon="iconHouse"
- link
- type="primary"
- <el-form-item prop="username">
- v-model="loginData.loginForm.username"
- :placeholder="t('login.usernamePlaceholder')"
- :prefix-icon="iconAvatar"
- <el-form-item prop="password">
- v-model="loginData.loginForm.password"
- :placeholder="t('login.passwordPlaceholder')"
- :prefix-icon="iconLock"
- show-password
- type="password"
- @keyup.enter="getCode()"
- :span="24"
- style="padding-right: 10px; padding-left: 10px; margin-top: -20px; margin-bottom: -20px"
- <el-row justify="space-between" style="width: 100%">
- <el-col :span="6">
- <el-checkbox v-model="loginData.loginForm.rememberMe">
- {{ t('login.remember') }}
- </el-checkbox>
- <el-col :offset="6" :span="12">
- <el-link style="float: right" type="primary">{{ t('login.forgetPassword') }}</el-link>
- <XButton
- :loading="loginLoading"
- :title="t('login.login')"
- class="w-[100%]"
- @click="getCode()"
- <Verify
- ref="verify"
- :captchaType="captchaType"
- :imgSize="{ width: '400px', height: '200px' }"
- mode="pop"
- @success="handleLogin"
- <el-row :gutter="5" justify="space-between" style="width: 100%">
- <el-col :span="8">
- :title="t('login.btnMobile')"
- @click="setLoginState(LoginStateEnum.MOBILE)"
- :title="t('login.btnQRCode')"
- @click="setLoginState(LoginStateEnum.QR_CODE)"
- :title="t('login.btnRegister')"
- @click="setLoginState(LoginStateEnum.REGISTER)"
- <el-divider content-position="center">{{ t('login.otherLogin') }}</el-divider>
- <div class="w-[100%] flex justify-between">
- v-for="(item, key) in socialList"
- :key="key"
- :icon="item.icon"
- :size="30"
- class="anticon cursor-pointer"
- color="#999"
- @click="doSocialLogin(item.type)"
- <el-divider content-position="center">萌新必读</el-divider>
- <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 href="http://static.yudao.iocoder.cn/mp/Aix9975.jpeg" target="_blank">
- 🤝外包咨询
- </el-form>
-import { ElLoading } from 'element-plus'
-import LoginFormTitle from './LoginFormTitle.vue'
-import type { RouteLocationNormalizedLoaded } from 'vue-router'
-import { useIcon } from '@/hooks/web/useIcon'
-import * as authUtil from '@/utils/auth'
-import { usePermissionStore } from '@/store/modules/permission'
-import * as LoginApi from '@/api/login'
-import { LoginStateEnum, useFormValid, useLoginState } from './useLogin'
-defineOptions({ name: 'LoginForm' })
-const message = useMessage()
-const iconHouse = useIcon({ icon: 'ep:house' })
-const iconAvatar = useIcon({ icon: 'ep:avatar' })
-const iconLock = useIcon({ icon: 'ep:lock' })
-const formLogin = ref()
-const { validForm } = useFormValid(formLogin)
-const { setLoginState, getLoginState } = useLoginState()
-const { currentRoute, push } = useRouter()
-const permissionStore = usePermissionStore()
-const redirect = ref<string>('')
-const loginLoading = ref(false)
-const verify = ref()
-const captchaType = ref('blockPuzzle') // blockPuzzle 滑块 clickWord 点击文字
-const getShow = computed(() => unref(getLoginState) === LoginStateEnum.LOGIN)
-const LoginRules = {
- tenantName: [required],
- username: [required],
- password: [required]
-const loginData = reactive({
- isShowPassword: false,
- captchaEnable: import.meta.env.VITE_APP_CAPTCHA_ENABLE,
- tenantEnable: import.meta.env.VITE_APP_TENANT_ENABLE,
- loginForm: {
- username: 'admin',
- password: 'zx123',
- captchaVerification: '',
- rememberMe: false
-const socialList = [
- { icon: 'ant-design:wechat-filled', type: 30 },
- { icon: 'ant-design:dingtalk-circle-filled', type: 20 },
- { icon: 'ant-design:github-filled', type: 0 },
- { icon: 'ant-design:alipay-circle-filled', type: 0 }
-]
-// 获取验证码
-const getCode = async () => {
- // 情况一,未开启:则直接登录
- if (loginData.captchaEnable === 'false') {
- await handleLogin({})
- // 情况二,已开启:则展示验证码;只有完成验证码的情况,才进行登录
- // 弹出验证码
- verify.value.show()
-// 获取租户 ID
-const getTenantId = async () => {
- if (loginData.tenantEnable === 'true') {
- const res = await LoginApi.getTenantIdByName(loginData.loginForm.tenantName)
- authUtil.setTenantId(res)
-// 记住我
-const getCookie = () => {
- const loginForm = authUtil.getLoginForm()
- if (loginForm) {
- loginData.loginForm = {
- ...loginData.loginForm,
- username: loginForm.username ? loginForm.username : loginData.loginForm.username,
- password: loginForm.password ? loginForm.password : loginData.loginForm.password,
- rememberMe: loginForm.rememberMe ? true : false,
- tenantName: loginForm.tenantName ? loginForm.tenantName : loginData.loginForm.tenantName
-// 根据域名,获得租户信息
-const getTenantByWebsite = async () => {
- const website = location.host
- const res = await LoginApi.getTenantByWebsite(website)
- if (res) {
- loginData.loginForm.tenantName = res.name
- authUtil.setTenantId(res.id)
-const loading = ref() // ElLoading.service 返回的实例
-// 登录
-const handleLogin = async (params) => {
- loginLoading.value = true
- try {
- await getTenantId()
- const data = await validForm()
- if (!data) {
- loginData.loginForm.captchaVerification = params.captchaVerification
- const res = await LoginApi.login(loginData.loginForm)
- if (!res) {
- loading.value = ElLoading.service({
- lock: true,
- text: '正在加载系统中...',
- background: 'rgba(0, 0, 0, 0.7)'
- if (loginData.loginForm.rememberMe) {
- authUtil.setLoginForm(loginData.loginForm)
- authUtil.removeLoginForm()
- authUtil.setToken(res)
- if (!redirect.value) {
- redirect.value = '/'
- // 判断是否为SSO登录
- if (redirect.value.indexOf('sso') !== -1) {
- window.location.href = window.location.href.replace('/login?redirect=', '')
- push({ path: redirect.value || permissionStore.addRouters[0].path })
- } finally {
- loginLoading.value = false
- loading.value.close()
-// 社交登录
-const doSocialLogin = async (type: number) => {
- if (type === 0) {
- message.error('此方式未配置')
- // 尝试先通过 tenantName 获取租户
- // 如果获取不到,则需要弹出提示,进行处理
- if (!authUtil.getTenantId()) {
- await message.prompt('请输入租户名称', t('common.reminder')).then(async ({ value }) => {
- const res = await LoginApi.getTenantIdByName(value)
- // 计算 redirectUri
- // tricky: type、redirect需要先encode一次,否则钉钉回调会丢失。
- // 配合 Login/SocialLogin.vue#getUrlValue() 使用
- const redirectUri =
- location.origin +
- '/social-login?' +
- encodeURIComponent(`type=${type}&redirect=${redirect.value || '/'}`)
- // 进行跳转
- const res = await LoginApi.socialAuthRedirect(type, encodeURIComponent(redirectUri))
- window.location.href = res
- () => currentRoute.value,
- (route: RouteLocationNormalizedLoaded) => {
- redirect.value = route?.query?.redirect as string
- immediate: true
-onMounted(() => {
- getCookie()
- getTenantByWebsite()
-:deep(.anticon) {
- &:hover {
- color: var(--el-color-primary) !important;
-.login-code {
- float: right;
- height: 38px;
- img {
- height: auto;
- max-width: 100px;
- vertical-align: middle;
- cursor: pointer;
+ <el-form v-show="getShow" ref="formLogin" :model="loginData.loginForm" :rules="LoginRules" class="login-form"
+ label-position="top" label-width="120px" size="large">
+ <el-row style="margin-right: -10px; margin-left: -10px">
+ <el-col :span="24" style="padding-right: 10px; padding-left: 10px">
+ <el-form-item class="justify-center flex">
+ <!-- <LoginFormTitle style="width: 100%" /> -->
+ <img alt="" class="h-59px w-143px" style="margin: 0 auto;" src="@/assets/imgs/zxlogo.png" />
+ </el-form-item>
+ <el-form-item v-if="loginData.tenantEnable === 'true'" prop="tenantName">
+ <el-input v-model="loginData.loginForm.tenantName" :placeholder="t('login.tenantNamePlaceholder')"
+ :prefix-icon="iconHouse" link type="primary" />
+ <el-form-item prop="username">
+ <el-input v-model="loginData.loginForm.username" :placeholder="t('login.usernamePlaceholder')"
+ :prefix-icon="iconAvatar" />
+ <el-form-item prop="password">
+ <el-input v-model="loginData.loginForm.password" :placeholder="t('login.passwordPlaceholder')"
+ :prefix-icon="iconLock" show-password type="password" @keyup.enter="getCode()" />
+ <el-col :span="24" style="padding-right: 10px; padding-left: 10px; margin-top: -20px; margin-bottom: -20px">
+ <el-form-item>
+ <el-row justify="space-between" style="width: 100%">
+ <el-col :span="6">
+ <el-checkbox v-model="loginData.loginForm.rememberMe">
+ {{ t('login.remember') }}
+ </el-checkbox>
+ <el-col :offset="6" :span="12">
+ <el-link style="float: right" type="primary">{{ t('login.forgetPassword') }}</el-link>
+ <XButton :loading="loginLoading" :title="t('login.login')" class="w-[100%]" type="primary"
+ @click="getCode()" />
+ <Verify ref="verify" :captchaType="captchaType" :imgSize="{ width: '400px', height: '200px' }" mode="pop"
+ @success="handleLogin" />
+ <el-row :gutter="5" justify="space-between" style="width: 100%">
+ <el-col :span="8">
+ <XButton :title="t('login.btnMobile')" class="w-[100%]"
+ @click="setLoginState(LoginStateEnum.MOBILE)" />
+ <XButton :title="t('login.btnQRCode')" class="w-[100%]"
+ @click="setLoginState(LoginStateEnum.QR_CODE)" />
+ <XButton :title="t('login.btnRegister')" class="w-[100%]"
+ @click="setLoginState(LoginStateEnum.REGISTER)" />
+ <!-- <el-divider content-position="center">{{ t('login.otherLogin') }}</el-divider> -->
+ <!-- <el-col :span="24" style="padding-right: 10px; padding-left: 10px">
+ <div class="w-[100%] flex justify-between">
+ <Icon v-for="(item, key) in socialList" :key="key" :icon="item.icon" :size="30"
+ class="anticon cursor-pointer" color="#999" @click="doSocialLogin(item.type)" />
+ </el-col> -->
+ <!-- <el-divider content-position="center">萌新必读</el-divider>
+ <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-form>
+ import { ElLoading } from 'element-plus'
+ import LoginFormTitle from './LoginFormTitle.vue'
+ import type { RouteLocationNormalizedLoaded } from 'vue-router'
+ import { useIcon } from '@/hooks/web/useIcon'
+ import * as authUtil from '@/utils/auth'
+ import { usePermissionStore } from '@/store/modules/permission'
+ import * as LoginApi from '@/api/login'
+ import { LoginStateEnum, useFormValid, useLoginState } from './useLogin'
+ defineOptions({ name: 'LoginForm' })
+ const message = useMessage()
+ const iconHouse = useIcon({ icon: 'ep:house' })
+ const iconAvatar = useIcon({ icon: 'ep:avatar' })
+ const iconLock = useIcon({ icon: 'ep:lock' })
+ const formLogin = ref()
+ const { validForm } = useFormValid(formLogin)
+ const { setLoginState, getLoginState } = useLoginState()
+ const { currentRoute, push } = useRouter()
+ const permissionStore = usePermissionStore()
+ const redirect = ref<string>('')
+ const loginLoading = ref(false)
+ const verify = ref()
+ const captchaType = ref('blockPuzzle') // blockPuzzle 滑块 clickWord 点击文字
+ const getShow = computed(() => unref(getLoginState) === LoginStateEnum.LOGIN)
+ const LoginRules = {
+ tenantName: [required],
+ username: [required],
+ password: [required]
+ const loginData = reactive({
+ isShowPassword: false,
+ captchaEnable: import.meta.env.VITE_APP_CAPTCHA_ENABLE,
+ tenantEnable: import.meta.env.VITE_APP_TENANT_ENABLE,
+ loginForm: {
+ username: '',
+ password: '',
+ captchaVerification: '',
+ rememberMe: false
+ const socialList = [
+ { icon: 'ant-design:wechat-filled', type: 30 },
+ { icon: 'ant-design:dingtalk-circle-filled', type: 20 },
+ { icon: 'ant-design:github-filled', type: 0 },
+ { icon: 'ant-design:alipay-circle-filled', type: 0 }
+ // 获取验证码
+ const getCode = async () => {
+ // 情况一,未开启:则直接登录
+ if (loginData.captchaEnable === 'false') {
+ await handleLogin({})
+ // 情况二,已开启:则展示验证码;只有完成验证码的情况,才进行登录
+ // 弹出验证码
+ verify.value.show()
+ // 获取租户 ID
+ const getTenantId = async () => {
+ if (loginData.tenantEnable === 'true') {
+ const res = await LoginApi.getTenantIdByName(loginData.loginForm.tenantName)
+ authUtil.setTenantId(res)
+ // 记住我
+ const getCookie = () => {
+ const loginForm = authUtil.getLoginForm()
+ if (loginForm) {
+ loginData.loginForm = {
+ ...loginData.loginForm,
+ username: loginForm.username ? loginForm.username : loginData.loginForm.username,
+ password: loginForm.password ? loginForm.password : loginData.loginForm.password,
+ rememberMe: loginForm.rememberMe ? true : false,
+ tenantName: loginForm.tenantName ? loginForm.tenantName : loginData.loginForm.tenantName
+ // 根据域名,获得租户信息
+ const getTenantByWebsite = async () => {
+ const website = location.host
+ const res = await LoginApi.getTenantByWebsite(website)
+ if (res) {
+ loginData.loginForm.tenantName = res.name
+ authUtil.setTenantId(res.id)
+ const loading = ref() // ElLoading.service 返回的实例
+ // 登录
+ const handleLogin = async (params) => {
+ loginLoading.value = true
+ try {
+ await getTenantId()
+ const data = await validForm()
+ if (!data) {
+ loginData.loginForm.captchaVerification = params.captchaVerification
+ console.log(loginData.loginForm)
+ const res = await LoginApi.login(loginData.loginForm)
+ if (!res) {
+ loading.value = ElLoading.service({
+ lock: true,
+ text: '正在加载系统中...',
+ background: 'rgba(0, 0, 0, 0.7)'
+ if (loginData.loginForm.rememberMe) {
+ authUtil.setLoginForm(loginData.loginForm)
+ authUtil.removeLoginForm()
+ authUtil.setToken(res)
+ if (!redirect.value) {
+ redirect.value = '/'
+ // 判断是否为SSO登录
+ if (redirect.value.indexOf('sso') !== -1) {
+ window.location.href = window.location.href.replace('/login?redirect=', '')
+ push({ path: redirect.value || permissionStore.addRouters[0].path })
+ } finally {
+ loginLoading.value = false
+ loading.value.close()
+ // 社交登录
+ const doSocialLogin = async (type : number) => {
+ if (type === 0) {
+ message.error('此方式未配置')
+ // 尝试先通过 tenantName 获取租户
+ // 如果获取不到,则需要弹出提示,进行处理
+ if (!authUtil.getTenantId()) {
+ await message.prompt('请输入租户名称', t('common.reminder')).then(async ({ value }) => {
+ const res = await LoginApi.getTenantIdByName(value)
+ // 计算 redirectUri
+ // tricky: type、redirect需要先encode一次,否则钉钉回调会丢失。
+ // 配合 Login/SocialLogin.vue#getUrlValue() 使用
+ const redirectUri =
+ location.origin +
+ '/social-login?' +
+ encodeURIComponent(`type=${type}&redirect=${redirect.value || '/'}`)
+ // 进行跳转
+ const res = await LoginApi.socialAuthRedirect(type, encodeURIComponent(redirectUri))
+ window.location.href = res
+ () => currentRoute.value,
+ (route : RouteLocationNormalizedLoaded) => {
+ redirect.value = route?.query?.redirect as string
+ immediate: true
+ onMounted(() => {
+ getCookie()
+ getTenantByWebsite()
+ :deep(.anticon) {
+ color: var(--el-color-primary) !important;
+ .login-code {
+ float: right;
+ height: 38px;
+ img {
+ height: auto;
+ max-width: 100px;
+ vertical-align: middle;
+ .login-form {
+ background: rgba(255, 255, 255, 0.95);
@@ -133,7 +133,7 @@ const loginData = reactive({
uuid: '',
+ tenantName: '非繁源码',
mobileNumber: '',
code: ''
@@ -1,196 +1,167 @@
- <Dialog v-model="dialogVisible" :title="dialogTitle">
- ref="formRef"
- v-loading="formLoading"
- :model="formData"
- :rules="formRules"
- <el-form-item label="配置名" prop="name">
- <el-input v-model="formData.name" placeholder="请输入配置名" />
- <el-form-item label="备注" prop="remark">
- <el-input v-model="formData.remark" placeholder="请输入备注" />
- <el-form-item label="存储器" prop="storage">
- <el-select
- v-model="formData.storage"
- :disabled="formData.id !== undefined"
- placeholder="请选择存储器"
- <el-option
- v-for="dict in getDictOptions(DICT_TYPE.INFRA_FILE_STORAGE)"
- :key="dict.value"
- :label="dict.label"
- :value="parseInt(dict.value)"
- </el-select>
- <!-- DB -->
- <!-- Local / FTP / SFTP -->
- <el-form-item
- v-if="formData.storage >= 10 && formData.storage <= 12"
- label="基础路径"
- prop="config.basePath"
- <el-input v-model="formData.config.basePath" placeholder="请输入基础路径" />
- v-if="formData.storage >= 11 && formData.storage <= 12"
- label="主机地址"
- prop="config.host"
- <el-input v-model="formData.config.host" placeholder="请输入主机地址" />
- label="主机端口"
- prop="config.port"
- <el-input-number v-model="formData.config.port" :min="0" placeholder="请输入主机端口" />
- label="用户名"
- prop="config.username"
- <el-input v-model="formData.config.username" placeholder="请输入密码" />
- label="密码"
- prop="config.password"
- <el-input v-model="formData.config.password" placeholder="请输入密码" />
- <el-form-item v-if="formData.storage === 11" label="连接模式" prop="config.mode">
- <el-radio-group v-model="formData.config.mode">
- <el-radio key="Active" label="Active">主动模式</el-radio>
- <el-radio key="Passive" label="Passive">被动模式</el-radio>
- </el-radio-group>
- <!-- S3 -->
- <el-form-item v-if="formData.storage === 20" label="节点地址" prop="config.endpoint">
- <el-input v-model="formData.config.endpoint" placeholder="请输入节点地址" />
- <el-form-item v-if="formData.storage === 20" label="存储 bucket" prop="config.bucket">
- <el-input v-model="formData.config.bucket" placeholder="请输入 bucket" />
- <el-form-item v-if="formData.storage === 20" label="accessKey" prop="config.accessKey">
- <el-input v-model="formData.config.accessKey" placeholder="请输入 accessKey" />
- <el-form-item v-if="formData.storage === 20" label="accessSecret" prop="config.accessSecret">
- <el-input v-model="formData.config.accessSecret" placeholder="请输入 accessSecret" />
- <!-- 通用 -->
- <el-form-item v-if="formData.storage === 20" label="自定义域名">
- <!-- 无需参数校验,所以去掉 prop -->
- <el-input v-model="formData.config.domain" placeholder="请输入自定义域名" />
- <el-form-item v-else-if="formData.storage" label="自定义域名" prop="config.domain">
- <template #footer>
- <el-button :disabled="formLoading" type="primary" @click="submitForm">确 定</el-button>
- <el-button @click="dialogVisible = false">取 消</el-button>
- </Dialog>
-import { DICT_TYPE, getDictOptions } from '@/utils/dict'
-import * as FileConfigApi from '@/api/infra/fileConfig'
-import { FormRules } from 'element-plus'
-defineOptions({ name: 'InfraFileConfigForm' })
-const { t } = useI18n() // 国际化
-const message = useMessage() // 消息弹窗
-const dialogVisible = ref(false) // 弹窗的是否展示
-const dialogTitle = ref('') // 弹窗的标题
-const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
-const formType = ref('') // 表单的类型:create - 新增;update - 修改
-const formData = ref({
- id: undefined,
- name: '',
- storage: 0,
- remark: '',
- config: {} as FileConfigApi.FileClientConfig
-const formRules = reactive<FormRules>({
- name: [{ required: true, message: '配置名不能为空', trigger: 'blur' }],
- storage: [{ required: true, message: '存储器不能为空', trigger: 'change' }],
- config: {
- basePath: [{ required: true, message: '基础路径不能为空', trigger: 'blur' }],
- host: [{ required: true, message: '主机地址不能为空', trigger: 'blur' }],
- port: [{ required: true, message: '主机端口不能为空', trigger: 'blur' }],
- username: [{ required: true, message: '用户名不能为空', trigger: 'blur' }],
- password: [{ required: true, message: '密码不能为空', trigger: 'blur' }],
- mode: [{ required: true, message: '连接模式不能为空', trigger: 'change' }],
- endpoint: [{ required: true, message: '节点地址不能为空', trigger: 'blur' }],
- bucket: [{ required: true, message: '存储 bucket 不能为空', trigger: 'blur' }],
- accessKey: [{ required: true, message: 'accessKey 不能为空', trigger: 'blur' }],
- accessSecret: [{ required: true, message: 'accessSecret 不能为空', trigger: 'blur' }],
- domain: [{ required: true, message: '自定义域名不能为空', trigger: 'blur' }]
- } as FormRules
-const formRef = ref() // 表单 Ref
-/** 打开弹窗 */
-const open = async (type: string, id?: number) => {
- dialogVisible.value = true
- dialogTitle.value = t('action.' + type)
- formType.value = type
- resetForm()
- // 修改时,设置数据
- if (id) {
- formLoading.value = true
- formData.value = await FileConfigApi.getFileConfig(id)
- formLoading.value = false
-defineExpose({ open }) // 提供 open 方法,用于打开弹窗
-/** 提交表单 */
-const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
-const submitForm = async () => {
- // 校验表单
- if (!formRef) return
- const valid = await formRef.value.validate()
- if (!valid) return
- // 提交请求
- const data = formData.value as unknown as FileConfigApi.FileConfigVO
- if (formType.value === 'create') {
- await FileConfigApi.createFileConfig(data)
- message.success(t('common.createSuccess'))
- await FileConfigApi.updateFileConfig(data)
- message.success(t('common.updateSuccess'))
- dialogVisible.value = false
- // 发送操作成功的事件
- emit('success')
-/** 重置表单 */
-const resetForm = () => {
- formData.value = {
- storage: undefined!,
- formRef.value?.resetFields()
+ <Dialog v-model="dialogVisible" :title="dialogTitle">
+ <el-form ref="formRef" v-loading="formLoading" :model="formData" :rules="formRules" label-width="120px">
+ <el-form-item label="配置名" prop="name">
+ <el-input v-model="formData.name" placeholder="请输入配置名" />
+ <el-form-item label="备注" prop="remark">
+ <el-input v-model="formData.remark" placeholder="请输入备注" />
+ <el-form-item label="存储器" prop="storage">
+ <el-select v-model="formData.storage" :disabled="formData.id !== undefined" placeholder="请选择存储器">
+ <el-option v-for="dict in getDictOptions(DICT_TYPE.INFRA_FILE_STORAGE)" :key="dict.value"
+ :label="dict.label" :value="parseInt(dict.value)" />
+ </el-select>
+ <!-- DB -->
+ <!-- Local / FTP / SFTP -->
+ <el-form-item v-if="formData.storage >= 10 && formData.storage <= 12" label="基础路径" prop="config.basePath">
+ <el-input v-model="formData.config.basePath" placeholder="请输入基础路径" />
+ <el-form-item v-if="formData.storage >= 11 && formData.storage <= 12" label="主机地址" prop="config.host">
+ <el-input v-model="formData.config.host" placeholder="请输入主机地址" />
+ <el-form-item v-if="formData.storage >= 11 && formData.storage <= 12" label="主机端口" prop="config.port">
+ <el-input-number v-model="formData.config.port" :min="0" placeholder="请输入主机端口" />
+ <el-form-item v-if="formData.storage >= 11 && formData.storage <= 12" label="用户名" prop="config.username">
+ <el-input v-model="formData.config.username" placeholder="请输入密码" />
+ <el-form-item v-if="formData.storage >= 11 && formData.storage <= 12" label="密码" prop="config.password">
+ <el-input v-model="formData.config.password" placeholder="请输入密码" />
+ <el-form-item v-if="formData.storage === 11" label="连接模式" prop="config.mode">
+ <el-radio-group v-model="formData.config.mode">
+ <el-radio key="Active" label="Active">主动模式</el-radio>
+ <el-radio key="Passive" label="Passive">被动模式</el-radio>
+ </el-radio-group>
+ <!-- S3 -->
+ <el-form-item v-if="formData.storage === 20" label="节点地址" prop="config.endpoint">
+ <el-input v-model="formData.config.endpoint" placeholder="请输入节点地址" />
+ <el-form-item v-if="formData.storage === 20" label="存储 bucket" prop="config.bucket">
+ <el-input v-model="formData.config.bucket" placeholder="请输入 bucket" />
+ <el-form-item v-if="formData.storage === 20" label="accessKey" prop="config.accessKey">
+ <el-input v-model="formData.config.accessKey" show-password placeholder="请输入 accessKey" />
+ <el-form-item v-if="formData.storage === 20" label="accessSecret" prop="config.accessSecret">
+ <el-input v-model="formData.config.accessSecret" show-password placeholder="请输入 accessSecret" />
+ <!-- 通用 -->
+ <el-form-item v-if="formData.storage === 20" label="自定义域名">
+ <!-- 无需参数校验,所以去掉 prop -->
+ <el-input v-model="formData.config.domain" placeholder="请输入自定义域名" />
+ <el-form-item v-else-if="formData.storage" label="自定义域名" prop="config.domain">
+ <template #footer>
+ <el-button :disabled="formLoading" type="primary" @click="submitForm">确 定</el-button>
+ <el-button @click="dialogVisible = false">取 消</el-button>
+ </Dialog>
+ import { DICT_TYPE, getDictOptions } from '@/utils/dict'
+ import * as FileConfigApi from '@/api/infra/fileConfig'
+ import { FormRules } from 'element-plus'
+ defineOptions({ name: 'InfraFileConfigForm' })
+ const dialogVisible = ref(false) // 弹窗的是否展示
+ const dialogTitle = ref('') // 弹窗的标题
+ const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
+ const formType = ref('') // 表单的类型:create - 新增;update - 修改
+ const formData = ref({
+ id: undefined,
+ name: '',
+ storage: 0,
+ remark: '',
+ config: {} as FileConfigApi.FileClientConfig
+ const formRules = reactive<FormRules>({
+ name: [{ required: true, message: '配置名不能为空', trigger: 'blur' }],
+ storage: [{ required: true, message: '存储器不能为空', trigger: 'change' }],
+ config: {
+ basePath: [{ required: true, message: '基础路径不能为空', trigger: 'blur' }],
+ host: [{ required: true, message: '主机地址不能为空', trigger: 'blur' }],
+ port: [{ required: true, message: '主机端口不能为空', trigger: 'blur' }],
+ username: [{ required: true, message: '用户名不能为空', trigger: 'blur' }],
+ password: [{ required: true, message: '密码不能为空', trigger: 'blur' }],
+ mode: [{ required: true, message: '连接模式不能为空', trigger: 'change' }],
+ endpoint: [{ required: true, message: '节点地址不能为空', trigger: 'blur' }],
+ bucket: [{ required: true, message: '存储 bucket 不能为空', trigger: 'blur' }],
+ accessKey: [{ required: true, message: 'accessKey 不能为空', trigger: 'blur' }],
+ accessSecret: [{ required: true, message: 'accessSecret 不能为空', trigger: 'blur' }],
+ domain: [{ required: true, message: '自定义域名不能为空', trigger: 'blur' }]
+ } as FormRules
+ const formRef = ref() // 表单 Ref
+ /** 打开弹窗 */
+ const open = async (type : string, id ?: number) => {
+ dialogVisible.value = true
+ dialogTitle.value = t('action.' + type)
+ formType.value = type
+ resetForm()
+ // 修改时,设置数据
+ if (id) {
+ formLoading.value = true
+ formData.value = await FileConfigApi.getFileConfig(id)
+ formLoading.value = false
+ defineExpose({ open }) // 提供 open 方法,用于打开弹窗
+ /** 提交表单 */
+ const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
+ const submitForm = async () => {
+ // 校验表单
+ if (!formRef) return
+ const valid = await formRef.value.validate()
+ if (!valid) return
+ // 提交请求
+ const data = formData.value as unknown as FileConfigApi.FileConfigVO
+ if (formType.value === 'create') {
+ await FileConfigApi.createFileConfig(data)
+ message.success(t('common.createSuccess'))
+ await FileConfigApi.updateFileConfig(data)
+ message.success(t('common.updateSuccess'))
+ dialogVisible.value = false
+ // 发送操作成功的事件
+ emit('success')
+ /** 重置表单 */
+ const resetForm = () => {
+ formData.value = {
+ storage: undefined!,
+ formRef.value?.resetFields()
+ ::v-deep .el-input__suffix-inner>:first-child {
@@ -1,82 +1,76 @@
- <CardTitle title="快捷入口" />
- <div class="flex flex-row flex-wrap gap-8 p-4">
- v-for="menu in menuList"
- :key="menu.name"
- class="h-20 w-20% flex flex-col cursor-pointer items-center justify-center gap-2"
- @click="handleMenuClick(menu.routerName)"
- :class="menu.bgColor"
- class="h-48px w-48px flex items-center justify-center rounded text-white"
- <Icon :icon="menu.icon" class="text-7.5!" />
- <span>{{ menu.name }}</span>
-/** 快捷入口卡片 */
-import { CardTitle } from '@/components/Card'
-defineOptions({ name: 'ShortcutCard' })
-const router = useRouter() // 路由
-/** 菜单列表 */
-const menuList = [
- { name: '用户管理', icon: 'ep:user-filled', bgColor: 'bg-red-400', routerName: 'MemberUser' },
- name: '商品管理',
- icon: 'fluent-mdl2:product',
- bgColor: 'bg-orange-400',
- routerName: 'ProductSpu'
- { name: '订单管理', icon: 'ep:list', bgColor: 'bg-yellow-500', routerName: 'TradeOrder' },
- name: '售后管理',
- icon: 'ri:refund-2-line',
- bgColor: 'bg-green-600',
- routerName: 'TradeAfterSale'
- name: '分销管理',
- icon: 'fa-solid:project-diagram',
- bgColor: 'bg-cyan-500',
- routerName: 'TradeBrokerageUser'
- name: '优惠券',
- icon: 'ep:ticket',
- bgColor: 'bg-blue-500',
- routerName: 'PromotionCoupon'
- name: '拼团活动',
- icon: 'fa:group',
- bgColor: 'bg-purple-500',
- routerName: 'PromotionBargainActivity'
- name: '佣金提现',
- icon: 'vaadin:money-withdraw',
- bgColor: 'bg-rose-500',
- routerName: 'TradeBrokerageWithdraw'
-/**
- * 跳转到菜单对应页面
- *
- * @param routerName 路由页面组件的名称
- */
-const handleMenuClick = (routerName: string) => {
- router.push({ name: routerName })
+ <CardTitle title="快捷入口" />
+ <div v-for="menu in menuList" :key="menu.name"
+ class="h-20 w-20% flex flex-col cursor-pointer items-center justify-center gap-2"
+ @click="handleMenuClick(menu.routerName)">
+ <div :class="menu.bgColor" class="h-48px w-48px flex items-center justify-center rounded text-white">
+ <Icon :icon="menu.icon" class="text-7.5!" />
+ <span>{{ menu.name }}</span>
+ /** 快捷入口卡片 */
+ import { CardTitle } from '@/components/Card'
+ defineOptions({ name: 'ShortcutCard' })
+ const router = useRouter() // 路由
+ /** 菜单列表 */
+ const menuList = [
+ { name: '用户管理', icon: 'ep:user-filled', bgColor: 'bg-red-400', routerName: 'MemberUser' },
+ name: '商品管理',
+ icon: 'fluent-mdl2:product',
+ bgColor: 'bg-orange-400',
+ routerName: 'ProductSpu'
+ { name: '订单管理', icon: 'ep:list', bgColor: 'bg-yellow-500', routerName: 'TradeOrder' },
+ name: '售后管理',
+ icon: 'ri:refund-2-line',
+ bgColor: 'bg-green-600',
+ routerName: 'TradeAfterSale'
+ name: '分销管理',
+ icon: 'fa-solid:project-diagram',
+ bgColor: 'bg-cyan-500',
+ routerName: 'TradeBrokerageUser'
+ name: '优惠券',
+ icon: 'ep:ticket',
+ bgColor: 'bg-blue-500',
+ routerName: 'PromotionCoupon'
+ name: '拼团活动',
+ icon: 'fa:group',
+ bgColor: 'bg-purple-500',
+ routerName: 'PromotionBargainActivity'
+ name: '佣金提现',
+ icon: 'vaadin:money-withdraw',
+ bgColor: 'bg-rose-500',
+ routerName: 'TradeBrokerageWithdraw'
+ /**
+ * 跳转到菜单对应页面
+ *
+ * @param routerName 路由页面组件的名称
+ */
+ const handleMenuClick = (routerName : string) => {
+ router.push({ name: routerName })
@@ -1,146 +1,116 @@
-<!-- 商品发布 - 基础设置 -->
- <el-form ref="formRef" :model="formData" :rules="rules" label-width="120px" :disabled="isDetail">
- <el-form-item label="商品名称" prop="name">
- v-model="formData.name"
- placeholder="请输入商品名称"
- type="textarea"
- :autosize="{ minRows: 2, maxRows: 2 }"
- maxlength="64"
- :show-word-limit="true"
- :clearable="true"
- class="w-80!"
- <el-form-item label="商品分类" prop="categoryId">
- <el-cascader
- v-model="formData.categoryId"
- :options="categoryList"
- :props="defaultProps"
- class="w-80"
- clearable
- placeholder="请选择商品分类"
- filterable
- <el-form-item label="商品品牌" prop="brandId">
- <el-select v-model="formData.brandId" placeholder="请选择商品品牌" class="w-80">
- v-for="item in brandList"
- :key="item.id"
- :label="item.name"
- :value="item.id as number"
- <el-form-item label="商品关键字" prop="keyword">
- <el-input v-model="formData.keyword" placeholder="请输入商品关键字" class="w-80!" />
- <el-form-item label="商品简介" prop="introduction">
- v-model="formData.introduction"
- maxlength="128"
- <el-form-item label="商品封面图" prop="picUrl">
- <UploadImg v-model="formData.picUrl" height="80px" :disabled="isDetail" />
- <el-form-item label="商品轮播图" prop="sliderPicUrls">
- <UploadImgs v-model:modelValue="formData.sliderPicUrls" :disabled="isDetail" />
-import { PropType } from 'vue'
-import { copyValueToTarget } from '@/utils'
-import { defaultProps, handleTree } from '@/utils/tree'
-import type { Spu } from '@/api/mall/product/spu'
-import * as ProductCategoryApi from '@/api/mall/product/category'
-import * as ProductBrandApi from '@/api/mall/product/brand'
-import { BrandVO } from '@/api/mall/product/brand'
-import { CategoryVO } from '@/api/mall/product/category'
-defineOptions({ name: 'ProductSpuInfoForm' })
-const props = defineProps({
- propFormData: {
- type: Object as PropType<Spu>,
- default: () => {}
- isDetail: propTypes.bool.def(false) // 是否作为详情组件
-const formData = reactive<Spu>({
- name: '', // 商品名称
- categoryId: undefined, // 商品分类
- keyword: '', // 关键字
- picUrl: '', // 商品封面图
- sliderPicUrls: [], // 商品轮播图
- introduction: '', // 商品简介
- brandId: undefined // 商品品牌
-const rules = reactive({
- name: [required],
- categoryId: [required],
- keyword: [required],
- introduction: [required],
- picUrl: [required],
- sliderPicUrls: [required],
- brandId: [required]
-/** 将传进来的值赋值给 formData */
- () => props.propFormData,
- (data) => {
- copyValueToTarget(formData, data)
- // TODO @puhui999:优化多文件上传,看看有没可能搞成返回 v-model 图片列表这种
- formData.sliderPicUrls = data['sliderPicUrls']?.map((item) => ({
- url: item
- }))
-/** 表单校验 */
-const emit = defineEmits(['update:activeName'])
-const validate = async () => {
- await unref(formRef)?.validate()
- // 校验通过更新数据
- Object.assign(props.propFormData, formData)
- } catch (e) {
- message.error('【基础设置】不完善,请填写相关信息')
- emit('update:activeName', 'info')
- throw e // 目的截断之后的校验
-defineExpose({ validate })
-/** 初始化 */
-const brandList = ref<BrandVO[]>([]) // 商品品牌列表
-const categoryList = ref<CategoryVO[]>([]) // 商品分类树
-onMounted(async () => {
- // 获得分类树
- const data = await ProductCategoryApi.getCategoryList({})
- categoryList.value = handleTree(data, 'id')
- // 获取商品品牌列表
- brandList.value = await ProductBrandApi.getSimpleBrandList()
+<!-- 商品发布 - 基础设置 -->
+ <el-form ref="formRef" :model="formData" :rules="rules" label-width="120px" :disabled="isDetail">
+ <el-form-item label="商品名称" prop="name">
+ <el-input v-model="formData.name" placeholder="请输入商品名称" type="textarea"
+ :autosize="{ minRows: 2, maxRows: 2 }" maxlength="64" :show-word-limit="true" :clearable="true"
+ class="w-80!" />
+ <el-form-item label="商品分类" prop="categoryId">
+ <el-cascader v-model="formData.categoryId" :options="categoryList" :props="defaultProps" class="w-80"
+ clearable placeholder="请选择商品分类" filterable />
+ <el-form-item label="商品品牌" prop="brandId">
+ <el-select v-model="formData.brandId" placeholder="请选择商品品牌" class="w-80">
+ <el-option v-for="item in brandList" :key="item.id" :label="item.name" :value="item.id as number" />
+ <el-form-item label="商品关键字" prop="keyword">
+ <el-input v-model="formData.keyword" placeholder="请输入商品关键字" class="w-80!" />
+ <el-form-item label="商品简介" prop="introduction">
+ <el-input v-model="formData.introduction" placeholder="请输入商品名称" type="textarea"
+ :autosize="{ minRows: 2, maxRows: 2 }" maxlength="128" :show-word-limit="true" :clearable="true"
+ <el-form-item label="商品轮播图" prop="sliderPicUrls">
+ <UploadImgs v-model:modelValue="formData.sliderPicUrls" :disabled="isDetail" />
+ import { PropType } from 'vue'
+ import { copyValueToTarget } from '@/utils'
+ import { defaultProps, handleTree } from '@/utils/tree'
+ import type { Spu } from '@/api/mall/product/spu'
+ import * as ProductCategoryApi from '@/api/mall/product/category'
+ import * as ProductBrandApi from '@/api/mall/product/brand'
+ import { BrandVO } from '@/api/mall/product/brand'
+ import { CategoryVO } from '@/api/mall/product/category'
+ defineOptions({ name: 'ProductSpuInfoForm' })
+ propFormData: {
+ type: Object as PropType<Spu>,
+ default: () => { }
+ isDetail: propTypes.bool.def(false) // 是否作为详情组件
+ const formData = reactive<Spu>({
+ name: '', // 商品名称
+ categoryId: undefined, // 商品分类
+ keyword: '', // 关键字
+ sliderPicUrls: [], // 商品轮播图
+ introduction: '', // 商品简介
+ brandId: undefined // 商品品牌
+ const rules = reactive({
+ name: [required],
+ categoryId: [required],
+ keyword: [required],
+ introduction: [required],
+ sliderPicUrls: [required],
+ brandId: [required]
+ /** 将传进来的值赋值给 formData */
+ () => props.propFormData,
+ (data) => {
+ copyValueToTarget(formData, data)
+ // TODO @puhui999:优化多文件上传,看看有没可能搞成返回 v-model 图片列表这种
+ formData.sliderPicUrls = data['sliderPicUrls']?.map((item) => ({
+ url: item
+ }))
+ /** 表单校验 */
+ const emit = defineEmits(['update:activeName'])
+ const validate = async () => {
+ await unref(formRef)?.validate()
+ // 校验通过更新数据
+ Object.assign(props.propFormData, formData)
+ } catch (e) {
+ message.error('【基础设置】不完善,请填写相关信息')
+ emit('update:activeName', 'info')
+ throw e // 目的截断之后的校验
+ defineExpose({ validate })
+ /** 初始化 */
+ const brandList = ref<BrandVO[]>([]) // 商品品牌列表
+ const categoryList = ref<CategoryVO[]>([]) // 商品分类树
+ onMounted(async () => {
+ // 获得分类树
+ const data = await ProductCategoryApi.getCategoryList({})
+ categoryList.value = handleTree(data, 'id')
+ // 获取商品品牌列表
+ brandList.value = await ProductBrandApi.getSimpleBrandList()
@@ -1,204 +1,460 @@
- <ContentWrap v-loading="formLoading">
- <el-tabs v-model="activeName">
- <el-tab-pane label="基础设置" name="info">
- <InfoForm
- ref="infoRef"
- v-model:activeName="activeName"
- :is-detail="isDetail"
- :propFormData="formData"
- </el-tab-pane>
- <el-tab-pane label="价格库存" name="sku">
- <SkuForm
- ref="skuRef"
- <el-tab-pane label="物流设置" name="delivery">
- <DeliveryForm
- ref="deliveryRef"
- <el-tab-pane label="商品详情" name="description">
- <DescriptionForm
- ref="descriptionRef"
- <el-tab-pane label="其它设置" name="other">
- <OtherForm
- ref="otherRef"
- </el-tabs>
- <el-form>
- <el-form-item style="float: right">
- <el-button v-if="!isDetail" :loading="formLoading" type="primary" @click="submitForm">
- 保存
- </el-button>
- <el-button @click="close">返回</el-button>
- </ContentWrap>
-import { cloneDeep } from 'lodash-es'
-import * as ProductSpuApi from '@/api/mall/product/spu'
-import InfoForm from './InfoForm.vue'
-import DescriptionForm from './DescriptionForm.vue'
-import OtherForm from './OtherForm.vue'
-import SkuForm from './SkuForm.vue'
-import DeliveryForm from './DeliveryForm.vue'
-import { convertToInteger, floatToFixed2, formatToFraction } from '@/utils'
-defineOptions({ name: 'ProductSpuForm' })
-const { push, currentRoute } = useRouter() // 路由
-const { params, name } = useRoute() // 查询参数
-const { delView } = useTagsViewStore() // 视图操作
-const activeName = ref('info') // Tag 激活的窗口
-const isDetail = ref(false) // 是否查看详情
-const infoRef = ref() // 商品信息 Ref
-const skuRef = ref() // 商品规格 Ref
-const deliveryRef = ref() // 物流设置 Ref
-const descriptionRef = ref() // 商品详情 Ref
-const otherRef = ref() // 其他设置 Ref
-// SPU 表单数据
-const formData = ref<ProductSpuApi.Spu>({
- deliveryTypes: [], // 配送方式数组
- deliveryTemplateId: undefined, // 运费模版
- brandId: undefined, // 商品品牌
- specType: false, // 商品规格
- subCommissionType: false, // 分销类型
- skus: [
- price: 0, // 商品价格
- marketPrice: 0, // 市场价
- costPrice: 0, // 成本价
- barCode: '', // 商品条码
- picUrl: '', // 图片地址
- stock: 0, // 库存
- weight: 0, // 商品重量
- volume: 0, // 商品体积
- firstBrokeragePrice: 0, // 一级分销的佣金
- secondBrokeragePrice: 0 // 二级分销的佣金
- description: '', // 商品详情
- sort: 0, // 商品排序
- giveIntegral: 0, // 赠送积分
- virtualSalesCount: 0 // 虚拟销量
-/** 获得详情 */
-const getDetail = async () => {
- if ('ProductSpuDetail' === name) {
- isDetail.value = true
- const id = params.id as unknown as number
- const res = (await ProductSpuApi.getSpu(id)) as ProductSpuApi.Spu
- res.skus?.forEach((item) => {
- if (isDetail.value) {
- item.price = floatToFixed2(item.price)
- item.marketPrice = floatToFixed2(item.marketPrice)
- item.costPrice = floatToFixed2(item.costPrice)
- item.firstBrokeragePrice = floatToFixed2(item.firstBrokeragePrice)
- item.secondBrokeragePrice = floatToFixed2(item.secondBrokeragePrice)
- // 回显价格分转元
- item.price = formatToFraction(item.price)
- item.marketPrice = formatToFraction(item.marketPrice)
- item.costPrice = formatToFraction(item.costPrice)
- item.firstBrokeragePrice = formatToFraction(item.firstBrokeragePrice)
- item.secondBrokeragePrice = formatToFraction(item.secondBrokeragePrice)
- formData.value = res
-/** 提交按钮 */
- // 校验各表单
- await unref(infoRef)?.validate()
- await unref(skuRef)?.validate()
- await unref(deliveryRef)?.validate()
- await unref(descriptionRef)?.validate()
- await unref(otherRef)?.validate()
- // 深拷贝一份, 这样最终 server 端不满足,不需要影响原始数据
- const deepCopyFormData = cloneDeep(unref(formData.value)) as ProductSpuApi.Spu
- deepCopyFormData.skus!.forEach((item) => {
- // 给sku name赋值
- item.name = deepCopyFormData.name
- // sku相关价格元转分
- item.price = convertToInteger(item.price)
- item.marketPrice = convertToInteger(item.marketPrice)
- item.costPrice = convertToInteger(item.costPrice)
- item.firstBrokeragePrice = convertToInteger(item.firstBrokeragePrice)
- item.secondBrokeragePrice = convertToInteger(item.secondBrokeragePrice)
- // 处理轮播图列表
- const newSliderPicUrls: any[] = []
- deepCopyFormData.sliderPicUrls!.forEach((item: any) => {
- // 如果是前端选的图
- typeof item === 'object' ? newSliderPicUrls.push(item.url) : newSliderPicUrls.push(item)
- deepCopyFormData.sliderPicUrls = newSliderPicUrls
- // 校验都通过后提交表单
- const data = deepCopyFormData as ProductSpuApi.Spu
- if (!id) {
- await ProductSpuApi.createSpu(data)
- await ProductSpuApi.updateSpu(data)
- close()
-/** 关闭按钮 */
-const close = () => {
- delView(unref(currentRoute))
- push({ name: 'ProductSpu' })
- await getDetail()
+ <el-dialog v-model="dialogVisible" :title="dialogTitle" :fullscreen="isFullScreen" :show-close="false" width="70%"
+ draggable class="dialog">
+ <template #header="{ close }">
+ <div class="my-header">
+ <div class="my-header-left">{{dialogTitle}}</div>
+ <div class="my-header-right">
+ <span @click="fullScreen">
+ <Icon :icon="isFullScreen ? 'zmdi:fullscreen-exit' : 'zmdi:fullscreen'" />
+ <span @click="close">
+ <el-icon>
+ <Close />
+ </el-icon>
+ <ContentWrap v-loading="formLoading">
+ <div class="left">
+ <SPuUploadImg v-model="formData.picUrl" :disabled="isDetail" />
+ <el-tabs v-model="activeName" tab-position="left" class="child-tabs">
+ <el-tab-pane label="基础设置" name="info" />
+ <el-tab-pane label="价格库存" name="sku" />
+ <el-tab-pane label="物流设置" name="delivery" />
+ <el-tab-pane label="商品详情" name="description" />
+ <el-tab-pane label="其它设置" name="other" />
+ </el-tabs>
+ <div class="right">
+ <div v-show="activeName == 'info'">
+ <InfoForm ref="infoRef" :is-detail="isDetail" :propFormData="formData" />
+ <div v-show="activeName == 'sku'">
+ <SkuForm ref="skuRef" :is-detail="isDetail" :propFormData="formData" />
+ <div v-show="activeName == 'delivery'">
+ <DeliveryForm ref="deliveryRef" :is-detail="isDetail" :propFormData="formData" />
+ <div v-show="activeName == 'description'">
+ <DescriptionForm ref="descriptionRef" :is-detail="isDetail" :propFormData="formData" />
+ <div v-show="activeName == 'other'">
+ <OtherForm ref="otherRef" :is-detail="isDetail" :propFormData="formData" />
+ <el-form style="clear: both;">
+ <el-form-item style="float: right">
+ <!-- 不在回收站的才可以停用 -->
+ <el-button v-if="!isDetail && parentTabType!=4 && openType != 'create'" :loading="formLoading"
+ type="danger" plain @click="handleStatus02Change(ProductSpuStatusEnum.RECYCLE.status)">
+ 停用
+ </el-button>
+ <!-- 在回收站的可以选择恢复或者删除 -->
+ <el-button v-if="!isDetail && parentTabType==4" :loading="formLoading"
+ v-hasPermi="['product:spu:delete']" type="danger" plain @click="handleDelete(productId)">
+ 删除
+ v-hasPermi="['product:spu:update']" type="primary"
+ @click="handleStatus02Change(ProductSpuStatusEnum.DISABLE.status)">
+ 恢复
+ <el-button v-if="!isDetail && openType != 'create'" :loading="formLoading" type="primary"
+ @click="handleStatusChange">
+ {{parentRow.status == 0 ?"上架":"下架"}}
+ <el-button v-if="!isDetail" :loading="formLoading" type="primary" @click="submitForm">
+ 保存
+ </ContentWrap>
+ </el-dialog>
+ <!-- </div> -->
+ import { cloneDeep } from 'lodash-es'
+ import * as ProductSpuApi from '@/api/mall/product/spu'
+ import InfoForm from './InfoForm.vue'
+ import DescriptionForm from './DescriptionForm.vue'
+ import OtherForm from './OtherForm.vue'
+ import SkuForm from './SkuForm.vue'
+ import DeliveryForm from './DeliveryForm.vue'
+ import { convertToInteger, floatToFixed2, formatToFraction } from '@/utils'
+ import {
+ Close,
+ FullScreen
+ } from '@element-plus/icons-vue'
+ import { ProductSpuStatusEnum } from '@/utils/constants'
+ defineOptions({ name: 'ProductSpuForm' })
+ const { push, currentRoute } = useRouter() // 路由
+ const { params, name } = useRoute() // 查询参数
+ const { delView } = useTagsViewStore() // 视图操作
+ const activeName = ref('info') // Tag 激活的窗口
+ const isDetail = ref(false) // 是否查看详情
+ const infoRef = ref() // 商品信息 Ref
+ const skuRef = ref() // 商品规格 Ref
+ const deliveryRef = ref() // 物流设置 Ref
+ const descriptionRef = ref() // 商品详情 Ref
+ const otherRef = ref() // 其他设置 Ref
+ // SPU 表单数据
+ const formData = ref<ProductSpuApi.Spu>({
+ picUrl: '', // 商品封面图
+ deliveryTypes: [], // 配送方式数组
+ deliveryTemplateId: undefined, // 运费模版
+ brandId: undefined, // 商品品牌
+ specType: false, // 商品规格
+ subCommissionType: false, // 分销类型
+ skus: [
+ price: 0, // 商品价格
+ marketPrice: 0, // 市场价
+ costPrice: 0, // 成本价
+ barCode: '', // 商品条码
+ picUrl: '', // 图片地址
+ stock: 0, // 库存
+ weight: 0, // 商品重量
+ volume: 0, // 商品体积
+ firstBrokeragePrice: 0, // 一级分销的佣金
+ secondBrokeragePrice: 0 // 二级分销的佣金
+ description: '', // 商品详情
+ sort: 0, // 商品排序
+ giveIntegral: 0, // 赠送积分
+ virtualSalesCount: 0 // 虚拟销量
+ const addFormData = ref<ProductSpuApi.Spu>({
+ const parentTabType = ref(0)
+ const productId = ref()
+ const picUrl = ref("")
+ const parentRow = ref()
+ const openType = ref("")
+ const parentNewStatus = ref(0)
+ const isFullScreen = ref(false)
+ const handleError = (e) => {
+ e.target.src = "https://static.iocoder.cn/mall/a79f5d2ea6bf0c3c11b2127332dfe2df.jpg"
+ const open = async (type : string, row : any, newStatus : number, tabType : number) => {
+ parentTabType.value = tabType
+ parentRow.value = row
+ parentNewStatus.value = newStatus
+ productId.value = row.id
+ picUrl.value = row.picUrl
+ isFullScreen.value = false
+ await getDetail()
+ activeName.value = 'info'
+ openType.value = type
+ if (type == "view") {
+ dialogTitle.value = "查看"
+ } else if (type == "create") {
+ formData.value = cloneDeep(addFormData.value)
+ // console.log(formData.value)
+ // console.log(addFormData.value)
+ // 判断打开状态,如果是view那就只能查看 否则可以编辑
+ if ('view' == openType.value) {
+ isDetail.value = true
+ isDetail.value = false
+ //全屏/取消全屏弹窗
+ const fullScreen = () => {
+ isFullScreen.value = !isFullScreen.value
+ /** 获得详情 */
+ const getDetail = async () => {
+ console.log(productId.value)
+ const id = productId.value as any as number
+ const res = (await ProductSpuApi.getSpu(id)) as ProductSpuApi.Spu
+ res.skus?.forEach((item) => {
+ if (isDetail.value) {
+ item.price = floatToFixed2(item.price)
+ item.marketPrice = floatToFixed2(item.marketPrice)
+ item.costPrice = floatToFixed2(item.costPrice)
+ item.firstBrokeragePrice = floatToFixed2(item.firstBrokeragePrice)
+ item.secondBrokeragePrice = floatToFixed2(item.secondBrokeragePrice)
+ // 回显价格分转元
+ item.price = formatToFraction(item.price)
+ item.marketPrice = formatToFraction(item.marketPrice)
+ item.costPrice = formatToFraction(item.costPrice)
+ item.firstBrokeragePrice = formatToFraction(item.firstBrokeragePrice)
+ item.secondBrokeragePrice = formatToFraction(item.secondBrokeragePrice)
+ formData.value = res
+ /** 添加到仓库 / 回收站的状态 */
+ const handleStatus02Change = async (newStatus : number) => {
+ // 二次确认
+ const text = newStatus === ProductSpuStatusEnum.RECYCLE.status ? '加入到回收站' : '恢复到仓库'
+ await message.confirm(`确认要"${parentRow.value.name}"${text}吗?`)
+ // 发起修改
+ await ProductSpuApi.updateStatus({ id: parentRow.value.id, status: newStatus })
+ message.success(text + '成功')
+ // 刷新 tabs 数据
+ // await getTabsCount()
+ // 刷新列表
+ close()
+ } catch { }
+ /** 删除按钮操作 */
+ const handleDelete = async (id : number) => {
+ // 删除的二次确认
+ await message.delConfirm()
+ // 发起删除
+ await ProductSpuApi.deleteSpu(id)
+ message.success(t('common.delSuccess'))
+ /** 更新上架/下架状态 */
+ const handleStatusChange = async () => {
+ console.log(parentRow.value.status)
+ const text = !parentRow.value.status ? '上架' : '下架'
+ const updateStatus = !parentRow.value.status ? 1 : 0
+ await message.confirm(`确认要${text}"${parentRow.value.name}"吗?`)
+ await ProductSpuApi.updateStatus({ id: parentRow.value.id, status: updateStatus })
+ } catch {
+ /** 提交按钮 */
+ // 校验各表单
+ await unref(infoRef)?.validate()
+ await unref(skuRef)?.validate()
+ await unref(deliveryRef)?.validate()
+ await unref(descriptionRef)?.validate()
+ await unref(otherRef)?.validate()
+ // 深拷贝一份, 这样最终 server 端不满足,不需要影响原始数据
+ const deepCopyFormData = cloneDeep(unref(formData.value)) as ProductSpuApi.Spu
+ deepCopyFormData.skus!.forEach((item) => {
+ // 给sku name赋值
+ item.name = deepCopyFormData.name
+ // sku相关价格元转分
+ item.price = convertToInteger(item.price)
+ item.marketPrice = convertToInteger(item.marketPrice)
+ item.costPrice = convertToInteger(item.costPrice)
+ item.firstBrokeragePrice = convertToInteger(item.firstBrokeragePrice)
+ item.secondBrokeragePrice = convertToInteger(item.secondBrokeragePrice)
+ // 处理轮播图列表
+ const newSliderPicUrls : any[] = []
+ deepCopyFormData.sliderPicUrls!.forEach((item : any) => {
+ // 如果是前端选的图
+ typeof item === 'object' ? newSliderPicUrls.push(item.url) : newSliderPicUrls.push(item)
+ deepCopyFormData.sliderPicUrls = newSliderPicUrls
+ // 校验都通过后提交表单
+ const data = deepCopyFormData as ProductSpuApi.Spu
+ if (!id) {
+ await ProductSpuApi.createSpu(data)
+ await ProductSpuApi.updateSpu(data)
+ /** 关闭按钮 */
+ const close = () => {
+ .el-dialog__body {
+ padding: unset;
+ .dialog .el-card__body {
+ padding: 0 20px 20px 0;
+ .dialog .el-dialog__headerbtn {
+ top: 0px;
+ line-height: 60px;
+ .dialog .el-dialog__headerbtn:hover {
+ background-color: #ef6b6b;
+ .dialog .el-dialog__headerbtn:hover .el-dialog__close {
+ color: #fff;
+ .dialog .el-dialog__header {
+ margin: 0
+ ::v-deep .left {
+ width: 106px;
+ float: left;
+ // width: 98%;
+ // border-bottom: 2px solid #e4e7ee;
+ // border-right: 2px solid #e4e7ee;
+ // margin-bottom: -5px;
+ .child-tabs {
+ border-top: 2px solid #e4e7ef;
+ margin-top: -7px;
+ ::v-deep .child-tabs .is-active {
+ // border-left: 2px solid;
+ ::v-deep .child-tabs .el-tabs__active-bar {
+ // background-color: #30fdff;
+ // background-color: unset;
+ ::v-deep .child-tabs .el-tabs__item {
+ ::v-deep .child-tabs .el-tabs__item {}
+ .right {
+ padding: 10px 0 60px;
+ border-left: 2px solid #e4e7ee;
+ margin-left: -2px;
+ width: calc(100% - 120px);
+ .my-header {
+ &-left {
+ font-weight: bold;
+ font-size: 18px;
+ padding: 20px;
+ padding-bottom: 10px
+ &-right {
+ width: 55px;
+ height: 55px;
+ display: inline-block;
+ line-height: 55px;
+ span:first-child:hover {
+ background-color: #f6f6f6;
+ // color: white
+ span:last-child:hover {
+ color: white
@@ -1,449 +1,535 @@
-<!-- 商品中心 - 商品列表 -->
- <!-- 搜索工作栏 -->
- <ContentWrap>
- ref="queryFormRef"
- :inline="true"
- :model="queryParams"
- class="-mb-15px"
- label-width="68px"
- v-model="queryParams.name"
- class="!w-240px"
- @keyup.enter="handleQuery"
- v-model="queryParams.categoryId"
- class="w-1/1"
- <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')]"
- end-placeholder="结束日期"
- start-placeholder="开始日期"
- type="daterange"
- value-format="YYYY-MM-DD HH:mm:ss"
- <el-button @click="handleQuery">
- <Icon class="mr-5px" icon="ep:search" />
- 搜索
- <el-button @click="resetQuery">
- <Icon class="mr-5px" icon="ep:refresh" />
- 重置
- <el-button
- v-hasPermi="['product:spu:create']"
- plain
- @click="openForm(undefined)"
- <Icon class="mr-5px" icon="ep:plus" />
- 新增
- v-hasPermi="['product:spu:export']"
- :loading="exportLoading"
- type="success"
- @click="handleExport"
- <Icon class="mr-5px" icon="ep:download" />
- 导出
- <!-- 列表 -->
- <el-tabs v-model="queryParams.tabType" @tab-click="handleTabClick">
- <el-tab-pane
- v-for="item in tabsData"
- :key="item.type"
- :label="item.name + '(' + item.count + ')'"
- :name="item.type"
- <el-table v-loading="loading" :data="list">
- <el-table-column type="expand">
- <template #default="{ row }">
- <el-form class="spu-table-expand" label-position="left">
- <el-col :span="24">
- <el-form-item label="商品分类:">
- <span>{{ formatCategoryName(row.categoryId) }}</span>
- <el-form-item label="市场价:">
- <span>{{ fenToYuan(row.marketPrice) }}</span>
- <el-form-item label="成本价:">
- <span>{{ fenToYuan(row.costPrice) }}</span>
- <el-form-item label="浏览量:">
- <span>{{ row.browseCount }}</span>
- <el-form-item label="虚拟销量:">
- <span>{{ row.virtualSalesCount }}</span>
- </el-table-column>
- <el-table-column label="商品编号" min-width="140" prop="id" />
- <el-table-column label="商品信息" min-width="300">
- <div class="flex">
- <el-image
- fit="cover"
- :src="row.picUrl"
- class="flex-none w-50px h-50px"
- @click="imagePreview(row.picUrl)"
- <div class="ml-4 overflow-hidden">
- <el-tooltip effect="dark" :content="row.name" placement="top">
- {{ row.name }}
- </el-tooltip>
- <el-table-column align="center" label="价格" min-width="160" prop="price">
- <template #default="{ row }"> ¥ {{ fenToYuan(row.price) }}</template>
- <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 align="center" label="销售状态" min-width="80">
- <template v-if="row.status >= 0">
- <el-switch
- v-model="row.status"
- :active-value="1"
- :inactive-value="0"
- active-text="上架"
- inactive-text="下架"
- inline-prompt
- @change="handleStatusChange(row)"
- <template v-else>
- <el-tag type="info">回收站</el-tag>
- <el-table-column
- :formatter="dateFormatter"
- align="center"
- label="创建时间"
- prop="createTime"
- width="180"
- <el-table-column align="center" fixed="right" label="操作" min-width="200">
- <el-button link type="primary" @click="openDetail(row.id)"> 详情 </el-button>
- v-hasPermi="['product:spu:update']"
- @click="openForm(row.id)"
- 修改
- <template v-if="queryParams.tabType === 4">
- v-hasPermi="['product:spu:delete']"
- type="danger"
- @click="handleDelete(row.id)"
- 删除
- @click="handleStatus02Change(row, ProductSpuStatusEnum.DISABLE.status)"
- 恢复
- @click="handleStatus02Change(row, ProductSpuStatusEnum.RECYCLE.status)"
- 回收
- </el-table>
- <!-- 分页 -->
- <Pagination
- v-model:limit="queryParams.pageSize"
- v-model:page="queryParams.pageNo"
- :total="total"
- @pagination="getList"
-import { TabsPaneContext } from 'element-plus'
-import { createImageViewer } from '@/components/ImageViewer'
-import { dateFormatter } from '@/utils/formatTime'
-import { defaultProps, handleTree, treeToString } from '@/utils/tree'
-import { ProductSpuStatusEnum } from '@/utils/constants'
-import { fenToYuan } from '@/utils'
-import download from '@/utils/download'
-defineOptions({ name: 'ProductSpu' })
-const { push } = useRouter() // 路由跳转
-const loading = ref(false) // 列表的加载中
-const exportLoading = ref(false) // 导出的加载中
-const total = ref(0) // 列表的总页数
-const list = ref<ProductSpuApi.Spu[]>([]) // 列表的数据
-// tabs 数据
-const tabsData = ref([
- name: '出售中',
- type: 0,
- count: 0
- name: '仓库中',
- type: 1,
- name: '已售罄',
- type: 2,
- name: '警戒库存',
- type: 3,
- name: '回收站',
- type: 4,
-])
-const queryParams = ref({
- pageNo: 1,
- pageSize: 10,
- tabType: 0,
- categoryId: undefined,
- createTime: undefined
-}) // 查询参数
-const queryFormRef = ref() // 搜索的表单Ref
-/** 查询列表 */
-const getList = async () => {
- loading.value = true
- const data = await ProductSpuApi.getSpuPage(queryParams.value)
- list.value = data.list
- total.value = data.total
-/** 切换 Tab */
-const handleTabClick = (tab: TabsPaneContext) => {
- queryParams.value.tabType = tab.paneName as number
- getList()
-/** 获得每个 Tab 的数量 */
-const getTabsCount = async () => {
- const res = await ProductSpuApi.getTabsCount()
- for (let objName in res) {
- tabsData.value[Number(objName)].count = res[objName]
-/** 添加到仓库 / 回收站的状态 */
-const handleStatus02Change = async (row: any, newStatus: number) => {
- // 二次确认
- const text = newStatus === ProductSpuStatusEnum.RECYCLE.status ? '加入到回收站' : '恢复到仓库'
- await message.confirm(`确认要"${row.name}"${text}吗?`)
- // 发起修改
- await ProductSpuApi.updateStatus({ id: row.id, status: newStatus })
- message.success(text + '成功')
- // 刷新 tabs 数据
- await getTabsCount()
- // 刷新列表
- await getList()
- } catch {}
-/** 更新上架/下架状态 */
-const handleStatusChange = async (row: any) => {
- const text = row.status ? '上架' : '下架'
- await message.confirm(`确认要${text}"${row.name}"吗?`)
- await ProductSpuApi.updateStatus({ id: row.id, status: row.status })
- } catch {
- // 异常时,需要重置回之前的值
- row.status =
- row.status === ProductSpuStatusEnum.DISABLE.status
- ? ProductSpuStatusEnum.ENABLE.status
- : ProductSpuStatusEnum.DISABLE.status
-/** 删除按钮操作 */
-const handleDelete = async (id: number) => {
- // 删除的二次确认
- await message.delConfirm()
- // 发起删除
- await ProductSpuApi.deleteSpu(id)
- message.success(t('common.delSuccess'))
- // 刷新tabs数据
-/** 商品图预览 */
-const imagePreview = (imgUrl: string) => {
- createImageViewer({
- urlList: [imgUrl]
-/** 搜索按钮操作 */
-const handleQuery = () => {
-/** 重置按钮操作 */
-const resetQuery = () => {
- queryFormRef.value.resetFields()
- handleQuery()
-/** 新增或修改 */
-const openForm = (id?: number) => {
- // 修改
- if (typeof id === 'number') {
- push({ name: 'ProductSpuEdit', params: { id } })
- // 新增
- push({ name: 'ProductSpuAdd' })
-/** 查看商品详情 */
-const openDetail = (id: number) => {
- push({ name: 'ProductSpuDetail', params: { id } })
-/** 导出按钮操作 */
-const handleExport = async () => {
- // 导出的二次确认
- await message.exportConfirm()
- // 发起导出
- exportLoading.value = true
- const data = await ProductSpuApi.exportSpu(queryParams)
- download.excel(data, '商品列表.xls')
- exportLoading.value = false
-/** 获取分类的节点的完整结构 */
-const categoryList = ref() // 分类树
-const formatCategoryName = (categoryId: number) => {
- return treeToString(categoryList.value, categoryId)
-/** 激活时 */
-onActivated(() => {
-/** 初始化 **/
- categoryList.value = handleTree(data, 'id', 'parentId')
-.spu-table-expand {
- padding-left: 42px;
- :deep(.el-form-item__label) {
- width: 82px;
- font-weight: bold;
- color: #99a9bf;
+<!-- 商品中心 - 商品列表 -->
+ <!-- 列表 -->
+ <ContentWrap style="" class="spu-div">
+ <div style="position: relative;">
+ <div style="text-align: right;background: #fafbfc;padding: 10px;">
+ <el-input v-model="queryParams.name" class="!w-240px" placeholder="搜索商品名称" @keyup.enter="handleQuery">
+ <template #suffix>
+ <el-icon class="el-input__icon" @click="handleQuery" style="cursor: pointer;">
+ <Search />
+ <template #append>
+ <el-button :icon="Operation" @click="showSearchMore" />
+ </el-input>
+
+ <el-button v-hasPermi="['product:spu:create']" @click="openForm('create','',0)">
+ <Icon class="mr-5px" icon="ep:plus" />
+ 新增
+ <div class="searchMore" v-if="searchMoreShow">
+ <el-form ref="queryFormRef" :inline="true" :model="queryParams" class="-mb-15px" label-width="68px">
+ <el-cascader v-model="queryParams.categoryId" :options="categoryList" :props="defaultProps"
+ class="w-1/1" clearable filterable placeholder="请选择商品分类" />
+ <el-form-item label="创建时间" prop="createTime">
+ <el-date-picker style="margin-right: unset;" 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-button @click="hideSearchMore">
+ 收起
+ <el-button @click="resetQuery">
+ 重置
+ <el-button @click="handleQuery" plain type="primary">
+ <Icon class="mr-5px" icon="ep:search" />
+ 搜索
+ <!-- <el-button v-hasPermi="['product:spu:export']" :loading="exportLoading" plain type="success"
+ @click="handleExport">
+ <Icon class="mr-5px" icon="ep:download" />
+ 导出
+ </el-button> -->
+ <el-tabs v-model="queryParams.tabType" @tab-click="handleTabClick" class="parent-tabs">
+ <el-tab-pane v-for="item in tabsData" :key="item.type" :label="item.name + '(' + item.count + ')'"
+ :name="item.type" />
+ <el-col v-for="(o, index) in list" class="product-card" :key="index" :span="7"
+ @mouseover="handleMouseOver(index)" @mouseout="handleMouseOut(index)">
+ <el-card @click="openDetail('view',o,ProductSpuStatusEnum.RECYCLE.status,queryParams.tabType)">
+ <div style="display: flex;justify-content: space-between;align-items: center;margin-bottom: 5px;">
+ <p style="width: 100%;white-space: nowrap;text-overflow: ellipsis;overflow: hidden;font-size: 16px;color:#000"
+ :title="o.name">{{o.name}}
+ <div @click.stop="openForm('update',o,ProductSpuStatusEnum.RECYCLE.status,queryParams.tabType)"
+ v-show="o.showSetting" class="setting">
+ <el-icon size="20" color="rgb(220 223 231)">
+ <Setting />
+ <div style="display:flex;align-items: center;">
+ <div style="width: 178px;height: 100px;margin-right:10px;border:1px solid rgb(220 223 231);border-radius: 5px;display: flex;align-items: center;justify-content: center;"
+ @error="handleError">
+ <img :src="o.picUrl" style="width: auto;height: auto;max-width: 100%;max-height: 100%;" />
+ <div style='line-height: 25px;'>
+ <p>¥{{ fenToYuan(o.price) }}</p>
+ <p>销量:{{o.salesCount}}</p>
+ <p>库存:{{o.stock}}</p>
+ <p>状态:{{o.status == 1?"上架":"下架"}}</p>
+ <div v-if="list.length == 0">
+ <p style="text-align: center;margin-top: 100px;">暂无商品</p>
+ <!--<el-table v-loading="loading" :data="list">
+ <el-table-column type="expand">
+ <template #default="{ row }">
+ <el-form class="spu-table-expand" label-position="left">
+ <el-col :span="24">
+ <el-form-item label="商品分类:">
+ <span>{{ formatCategoryName(row.categoryId) }}</span>
+ <el-form-item label="市场价:">
+ <span>{{ fenToYuan(row.marketPrice) }}</span>
+ <el-form-item label="成本价:">
+ <span>{{ fenToYuan(row.costPrice) }}</span>
+ <el-form-item label="浏览量:">
+ <span>{{ row.browseCount }}</span>
+ <el-form-item label="虚拟销量:">
+ <span>{{ row.virtualSalesCount }}</span>
+ </el-table-column>
+ <el-table-column label="商品编号" min-width="140" prop="id" />
+ <el-table-column label="商品信息" min-width="300">
+ <div class="flex">
+ <el-image fit="cover" :src="row.picUrl" class="flex-none w-50px h-50px"
+ @click="imagePreview(row.picUrl)" />
+ <div class="ml-4 overflow-hidden">
+ <el-tooltip effect="dark" :content="row.name" placement="top">
+ {{ row.name }}
+ </el-tooltip>
+ <el-table-column align="center" label="价格" min-width="160" prop="price">
+ <template #default="{ row }"> ¥ {{ fenToYuan(row.price) }}</template>
+ <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 align="center" label="销售状态" min-width="80">
+ <template v-if="row.status >= 0">
+ <el-switch v-model="row.status" :active-value="1" :inactive-value="0" active-text="上架"
+ inactive-text="下架" inline-prompt @change="handleStatusChange(row)" />
+ <el-tag type="info">回收站</el-tag>
+ <el-table-column :formatter="dateFormatter" align="center" label="创建时间" prop="createTime" width="180" />
+ <el-table-column align="center" fixed="right" label="操作" min-width="200">
+ <el-button link type="primary" @click="openDetail(row.id)"> 详情 </el-button>
+ <el-button v-hasPermi="['product:spu:update']" link type="primary" @click="openForm(row.id)">
+ 修改
+ <template v-if="queryParams.tabType === 4">
+ <el-button v-hasPermi="['product:spu:delete']" link type="danger" @click="handleDelete(row.id)">
+ <el-button v-hasPermi="['product:spu:update']" link type="primary"
+ @click="handleStatus02Change(row, ProductSpuStatusEnum.DISABLE.status)">
+ <el-button v-hasPermi="['product:spu:update']" link type="danger"
+ @click="handleStatus02Change(row, ProductSpuStatusEnum.RECYCLE.status)">
+ 回收
+ </el-table>-->
+ <!-- 分页 -->
+ <Pagination v-model:limit="queryParams.pageSize" v-model:page="queryParams.pageNo" :total="total"
+ @pagination="getList" />
+ <!-- 表单弹窗:添加/修改 -->
+ <SpuIndex ref="formRef" @success="getList" />
+ import { TabsPaneContext } from 'element-plus'
+ import { Setting, Search, Operation } from '@element-plus/icons-vue'
+ import { dateFormatter } from '@/utils/formatTime'
+ import { defaultProps, handleTree, treeToString } from '@/utils/tree'
+ import { fenToYuan } from '@/utils'
+ import download from '@/utils/download'
+ import SpuIndex from "./form/index.vue"
+ defineOptions({ name: 'ProductSpu' })
+ const { push } = useRouter() // 路由跳转
+ const loading = ref(false) // 列表的加载中
+ const exportLoading = ref(false) // 导出的加载中
+ const total = ref(0) // 列表的总页数
+ const list = ref<ProductSpuApi.Spu[]>([]) // 列表的数据
+ // tabs 数据
+ const tabsData = ref([
+ name: '出售中',
+ type: 0,
+ count: 0
+ name: '仓库中',
+ type: 1,
+ name: '已售罄',
+ type: 2,
+ name: '警戒库存',
+ type: 3,
+ name: '回收站',
+ type: 4,
+ const queryParams = ref({
+ pageNo: 1,
+ pageSize: 10,
+ tabType: 0,
+ categoryId: undefined,
+ createTime: undefined
+ }) // 查询参数
+ const queryFormRef = ref() // 搜索的表单Ref
+ const searchMoreShow = ref(false)
+ // 鼠标移入显示变动图标
+ function handleMouseOver(index) {
+ list.value[index].showSetting = true;
+ // 鼠标移出不显示图标
+ function handleMouseOut(index) {
+ list.value[index].showSetting = false;
+ // 裂图替换
+ // 打开更多搜索条件
+ const showSearchMore = () => {
+ searchMoreShow.value = !searchMoreShow.value
+ const hideSearchMore = () => {
+ searchMoreShow.value = false
+ /** 查询列表 */
+ const getList = async () => {
+ loading.value = true
+ const data = await ProductSpuApi.getSpuPage(queryParams.value)
+ data.list.forEach(obj => {
+ obj.showSetting = false;
+ });
+ list.value = data.list
+ // console.log(list.value)
+ total.value = data.total
+ getTabsCount()
+ /** 切换 Tab */
+ const handleTabClick = (tab : TabsPaneContext) => {
+ queryParams.value.tabType = tab.paneName as number
+ getList()
+ /** 获得每个 Tab 的数量 */
+ const getTabsCount = async () => {
+ const res = await ProductSpuApi.getTabsCount()
+ for (let objName in res) {
+ tabsData.value[Number(objName)].count = res[objName]
+ const handleStatus02Change = async (row : any, newStatus : number) => {
+ await message.confirm(`确认要"${row.name}"${text}吗?`)
+ await ProductSpuApi.updateStatus({ id: row.id, status: newStatus })
+ await getTabsCount()
+ await getList()
+ const handleStatusChange = async (row : any) => {
+ const text = row.status ? '上架' : '下架'
+ await message.confirm(`确认要${text}"${row.name}"吗?`)
+ await ProductSpuApi.updateStatus({ id: row.id, status: row.status })
+ // 异常时,需要重置回之前的值
+ row.status =
+ row.status === ProductSpuStatusEnum.DISABLE.status
+ ? ProductSpuStatusEnum.ENABLE.status
+ : ProductSpuStatusEnum.DISABLE.status
+ // 刷新tabs数据
+ /** 商品图预览 */
+ /** 搜索按钮操作 */
+ const handleQuery = () => {
+ /** 重置按钮操作 */
+ const resetQuery = () => {
+ queryParams.value.categoryId = undefined
+ queryParams.value.createTime = undefined
+ queryParams.value.name = ""
+ // queryFormRef.value.resetFields()
+ // console.log(queryParams.value)
+ // console.log(queryFormRef.value)
+ handleQuery()
+ const formRef = ref()
+ /** 新增或修改 */
+ const openForm = (type : string, row : any, newStatus : number, tabType : number) => {
+ formRef.value.open(type, row, newStatus, tabType)
+ /** 查看商品详情 */
+ const openDetail = (type : string, row : any, newStatus : number) => {
+ formRef.value.open(type, row, newStatus)
+ // push({ name: 'ProductSpuDetail', params: { id } })
+ /** 导出按钮操作 */
+ const handleExport = async () => {
+ // 导出的二次确认
+ await message.exportConfirm()
+ // 发起导出
+ exportLoading.value = true
+ const data = await ProductSpuApi.exportSpu(queryParams)
+ download.excel(data, '商品列表.xls')
+ exportLoading.value = false
+ /** 获取分类的节点的完整结构 */
+ const categoryList = ref() // 分类树
+ const formatCategoryName = (categoryId : number) => {
+ return treeToString(categoryList.value, categoryId)
+ /** 激活时 */
+ onActivated(() => {
+ /** 初始化 **/
+ categoryList.value = handleTree(data, 'id', 'parentId')
+ .spu-div {
+ min-height: 400px;
+ ::v-deep .parent-tabs .el-tabs__nav-wrap::after {
+ position: static !important;
+ ::v-deep .parent-tabs .el-tabs__active-bar {
+ background-color: unset;
+ .searchMore {
+ text-align: right;
+ background: #f3f5f8;
+ height: 100px;
+ width: calc(100% - 20px);
+ z-index: 111;
+ box-shadow: 0px 9px 6px rgba(0, 0, 0, 0.2);
+ .el-tabs {
+ top: 25px;
+ left: 40px;
+ .product-card {
+ margin: 10px;
+ .el-card {
+ padding: 15px;
+ :deep(.el-card__body) {
+ .setting {
+ width: 30px;
+ right: 0px;
+ height: 30px;
+ .setting:hover {
+ background: #666
+ p,
+ div {
+ margin: unset;
+ .spu-table-expand {
+ padding-left: 42px;
+ :deep(.el-form-item__label) {
+ width: 82px;
+ color: #99a9bf;
@@ -1,99 +1,101 @@
- <Dialog v-model="dialogVisible" title="订单发货" width="25%">
- <el-form ref="formRef" v-loading="formLoading" :model="formData" label-width="80px">
- <el-form-item label="发货方式">
- <el-radio-group v-model="expressType">
- <el-radio border label="express">快递物流</el-radio>
- <el-radio border label="none">无需发货</el-radio>
- <template v-if="expressType === 'express'">
- <el-form-item label="物流公司">
- <el-select v-model="formData.logisticsId" placeholder="请选择" style="width: 100%">
- v-for="item in deliveryExpressList"
- :value="item.id"
- <el-form-item label="物流单号">
- <el-input v-model="formData.logisticsNo" />
-import * as DeliveryExpressApi from '@/api/mall/trade/delivery/express'
-import * as TradeOrderApi from '@/api/mall/trade/order'
-defineOptions({ name: 'OrderDeliveryForm' })
-const expressType = ref('express') // 如果值是 express,则是快递;none 则是无;未来做同城配送;
-const formData = ref<TradeOrderApi.DeliveryVO>({
- id: 0, // 订单编号
- logisticsId: null, // 物流公司编号
- logisticsNo: '' // 物流编号
-const open = async (row: TradeOrderApi.OrderVO) => {
- // 设置数据
- copyValueToTarget(formData.value, row)
- if (row.logisticsId === 0) {
- expressType.value = 'none'
- const data = unref(formData)
- if (expressType.value === 'none') {
- // 无需发货的情况
- data.logisticsId = 0
- data.logisticsNo = ''
- await TradeOrderApi.deliveryOrder(data)
- emit('success', true)
-const deliveryExpressList = ref([])
- deliveryExpressList.value = await DeliveryExpressApi.getSimpleDeliveryExpressList()
+ <Dialog v-model="dialogVisible" title="订单发货" width="25%">
+ <el-form ref="formRef" v-loading="formLoading" :model="formData" label-width="80px">
+ <el-form-item label="发货方式">
+ <el-radio-group v-model="expressType">
+ <el-radio border label="express">快递物流</el-radio>
+ <el-radio border label="none">无需发货</el-radio>
+ <template v-if="expressType === 'express'">
+ <el-form-item label="物流公司">
+ <el-select v-model="formData.logisticsId" placeholder="请选择" style="width: 100%">
+ <el-option v-for="item in deliveryExpressList" :key="item.id" :label="item.name"
+ :value="item.id" />
+ <el-form-item label="物流单号">
+ <el-input v-model="formData.logisticsNo" />
+ import * as DeliveryExpressApi from '@/api/mall/trade/delivery/express'
+ import * as TradeOrderApi from '@/api/mall/trade/order'
+ defineOptions({ name: 'OrderDeliveryForm' })
+ const expressType = ref('express') // 如果值是 express,则是快递;none 则是无;未来做同城配送;
+ const formData = ref<TradeOrderApi.DeliveryVO>({
+ id: 0, // 订单编号
+ logisticsId: null, // 物流公司编号
+ logisticsNo: '' // 物流编号
+ const open = async (row : TradeOrderApi.OrderVO, openType : any, orderId : any) => {
+ // 设置数据
+ copyValueToTarget(formData.value, row)
+ //如果是首页待办点进来的,即点进来将id设为从待办传来的id
+ if (openType == "待办") {
+ formData.value.id = orderId
+ if (row.logisticsId === 0) {
+ expressType.value = 'none'
+ const data = unref(formData)
+ if (expressType.value === 'none') {
+ // 无需发货的情况
+ data.logisticsId = 0
+ data.logisticsNo = ''
+ await TradeOrderApi.deliveryOrder(data)
+ emit('success', true)
+ const deliveryExpressList = ref([])
+ deliveryExpressList.value = await DeliveryExpressApi.getSimpleDeliveryExpressList()
@@ -1,354 +1,260 @@
- <!-- 搜索 -->
- <el-form-item label="订单状态" prop="status">
- <el-select v-model="queryParams.status" class="!w-280px" clearable placeholder="全部">
- v-for="dict in getIntDictOptions(DICT_TYPE.TRADE_ORDER_STATUS)"
- :value="dict.value"
- <el-form-item label="支付方式" prop="payChannelCode">
- v-model="queryParams.payChannelCode"
- class="!w-280px"
- placeholder="全部"
- v-for="dict in getStrDictOptions(DICT_TYPE.PAY_CHANNEL_CODE)"
- end-placeholder="自定义时间"
- start-placeholder="自定义时间"
- <el-form-item label="订单来源" prop="terminal">
- <el-select v-model="queryParams.terminal" class="!w-280px" clearable placeholder="全部">
- v-for="dict in getIntDictOptions(DICT_TYPE.TERMINAL)"
- <el-form-item label="订单类型" prop="type">
- <el-select v-model="queryParams.type" class="!w-280px" clearable placeholder="全部">
- v-for="dict in getIntDictOptions(DICT_TYPE.TRADE_ORDER_TYPE)"
- <el-form-item label="配送方式" prop="deliveryType">
- <el-select v-model="queryParams.deliveryType" class="!w-280px" clearable placeholder="全部">
- v-for="dict in getIntDictOptions(DICT_TYPE.TRADE_DELIVERY_TYPE)"
- v-if="queryParams.deliveryType === DeliveryTypeEnum.EXPRESS.type"
- label="快递公司"
- prop="logisticsId"
- <el-select v-model="queryParams.logisticsId" class="!w-280px" clearable placeholder="全部">
- v-if="queryParams.deliveryType === DeliveryTypeEnum.PICK_UP.type"
- label="自提门店"
- prop="pickUpStoreId"
- v-model="queryParams.pickUpStoreId"
- multiple
- v-for="item in pickUpStoreList"
- label="核销码"
- prop="pickUpVerifyCode"
- v-model="queryParams.pickUpVerifyCode"
- placeholder="请输入自提核销码"
- <el-form-item label="聚合搜索">
- v-show="true"
- v-model="queryParams[queryType.queryParam]"
- :type="queryType.queryParam === 'userId' ? 'number' : 'text'"
- placeholder="请输入"
- <template #prepend>
- v-model="queryType.queryParam"
- class="!w-110px"
- @change="inputChangeSelect"
- v-for="dict in dynamicSearchList"
- </el-input>
- <!-- 添加 row-key="id" 解决列数据中的 table#header 数据不刷新的问题 -->
- <el-table v-loading="loading" :data="list" row-key="id">
- <OrderTableColumn :list="list" :pick-up-store-list="pickUpStoreList">
- <div class="flex items-center justify-center">
- v-hasPermi="['trade:order:query']"
- @click="openDetail(row.id)"
- <Icon icon="ep:notification" />
- 详情
- <el-dropdown
- v-hasPermi="['trade:order:update']"
- @command="(command) => handleCommand(command, row)"
- <el-button link type="primary">
- <Icon icon="ep:d-arrow-right" />
- 更多
- <template #dropdown>
- <el-dropdown-menu>
- <!-- 如果是【快递】,并且【未发货】,则展示【发货】按钮 -->
- <el-dropdown-item
- v-if="
- row.deliveryType === DeliveryTypeEnum.EXPRESS.type &&
- row.status === TradeOrderStatusEnum.UNDELIVERED.status
- "
- command="delivery"
- <Icon icon="ep:takeaway-box" />
- 发货
- </el-dropdown-item>
- <el-dropdown-item command="remark">
- <Icon icon="ep:chat-line-square" />
- 备注
- </el-dropdown-menu>
- </el-dropdown>
- </OrderTableColumn>
- <!-- 各种操作的弹窗 -->
- <OrderDeliveryForm ref="deliveryFormRef" @success="getList" />
- <OrderUpdateRemarkForm ref="updateRemarkForm" @success="getList" />
-import type { FormInstance } from 'element-plus'
-import OrderDeliveryForm from '@/views/mall/trade/order/form/OrderDeliveryForm.vue'
-import OrderUpdateRemarkForm from '@/views/mall/trade/order/form/OrderUpdateRemarkForm.vue'
-import * as PickUpStoreApi from '@/api/mall/trade/delivery/pickUpStore'
-import { DICT_TYPE, getIntDictOptions, getStrDictOptions } from '@/utils/dict'
-import { DeliveryTypeEnum, TradeOrderStatusEnum } from '@/utils/constants'
-import { OrderTableColumn } from './components'
-defineOptions({ name: 'TradeOrder' })
-const { currentRoute, push } = useRouter() // 路由跳转
-const loading = ref(true) // 列表的加载中
-const total = ref(2) // 列表的总页数
-const list = ref<TradeOrderApi.OrderVO[]>([]) // 列表的数据
-const queryFormRef = ref<FormInstance>() // 搜索的表单
-// 表单搜索
- pageNo: 1, // 页数
- pageSize: 10, // 每页显示数量
- status: undefined, // 订单状态
- payChannelCode: undefined, // 支付方式
- createTime: undefined, // 创建时间
- terminal: undefined, // 订单来源
- type: undefined, // 订单类型
- deliveryType: undefined, // 配送方式
- logisticsId: undefined, // 快递公司
- pickUpStoreId: undefined, // 自提门店
- pickUpVerifyCode: undefined // 自提核销码
-const queryType = reactive({ queryParam: '' }) // 订单搜索类型 queryParam
-// 订单聚合搜索 select 类型配置(动态搜索)
-const dynamicSearchList = ref([
- { value: 'no', label: '订单号' },
- { value: 'userId', label: '用户UID' },
- { value: 'userNickname', label: '用户昵称' },
- { value: 'userMobile', label: '用户电话' }
- * 聚合搜索切换查询对象时触发
- * @param val
-const inputChangeSelect = (val: string) => {
- dynamicSearchList.value
- .filter((item) => item.value !== val)
- ?.forEach((item1) => {
- // 清除集合搜索无用属性
- if (queryParams.value.hasOwnProperty(item1.value)) {
- delete queryParams.value[item1.value]
- const data = await TradeOrderApi.getOrderPage(unref(queryParams))
-const handleQuery = async () => {
- queryParams.value.pageNo = 1
- queryFormRef.value?.resetFields()
- queryParams.value = {
-/** 查看订单详情 */
- push({ name: 'TradeOrderDetail', params: { id } })
-/** 操作分发 */
-const deliveryFormRef = ref()
-const updateRemarkForm = ref()
-const handleCommand = (command: string, row: TradeOrderApi.OrderVO) => {
- switch (command) {
- case 'remark':
- updateRemarkForm.value?.open(row)
- break
- case 'delivery':
- deliveryFormRef.value?.open(row)
-// 监听路由变化更新列表,解决订单保存/更新后,列表不刷新的问题。
- () => {
-const pickUpStoreList = ref<PickUpStoreApi.DeliveryPickUpStoreVO[]>([]) // 自提门店精简列表
-const deliveryExpressList = ref<DeliveryExpressApi.DeliveryExpressVO[]>([]) // 物流公司
- pickUpStoreList.value = await PickUpStoreApi.getListAllSimple()
+ <!-- 搜索 -->
+ <ContentWrap>
+ <el-form-item label="订单状态" prop="status">
+ <el-select v-model="queryParams.status" class="!w-280px" clearable placeholder="全部">
+ <el-option v-for="dict in getIntDictOptions(DICT_TYPE.TRADE_ORDER_STATUS)" :key="dict.value"
+ :label="dict.label" :value="dict.value" />
+ <el-form-item label="支付方式" prop="payChannelCode">
+ <el-select v-model="queryParams.payChannelCode" class="!w-280px" clearable placeholder="全部">
+ <el-option v-for="dict in getStrDictOptions(DICT_TYPE.PAY_CHANNEL_CODE)" :key="dict.value"
+ <el-date-picker v-model="queryParams.createTime"
+ :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]" class="!w-280px"
+ end-placeholder="自定义时间" start-placeholder="自定义时间" type="daterange"
+ <el-form-item label="订单来源" prop="terminal">
+ <el-select v-model="queryParams.terminal" class="!w-280px" clearable placeholder="全部">
+ <el-option v-for="dict in getIntDictOptions(DICT_TYPE.TERMINAL)" :key="dict.value"
+ <el-form-item label="订单类型" prop="type">
+ <el-select v-model="queryParams.type" class="!w-280px" clearable placeholder="全部">
+ <el-option v-for="dict in getIntDictOptions(DICT_TYPE.TRADE_ORDER_TYPE)" :key="dict.value"
+ <el-form-item label="配送方式" prop="deliveryType">
+ <el-select v-model="queryParams.deliveryType" class="!w-280px" clearable placeholder="全部">
+ <el-option v-for="dict in getIntDictOptions(DICT_TYPE.TRADE_DELIVERY_TYPE)" :key="dict.value"
+ <el-form-item v-if="queryParams.deliveryType === DeliveryTypeEnum.EXPRESS.type" label="快递公司"
+ prop="logisticsId">
+ <el-select v-model="queryParams.logisticsId" class="!w-280px" clearable placeholder="全部">
+ <el-option v-for="item in deliveryExpressList" :key="item.id" :label="item.name" :value="item.id" />
+ <el-form-item v-if="queryParams.deliveryType === DeliveryTypeEnum.PICK_UP.type" label="自提门店"
+ prop="pickUpStoreId">
+ <el-select v-model="queryParams.pickUpStoreId" class="!w-280px" clearable multiple placeholder="全部">
+ <el-option v-for="item in pickUpStoreList" :key="item.id" :label="item.name" :value="item.id" />
+ <el-form-item v-if="queryParams.deliveryType === DeliveryTypeEnum.PICK_UP.type" label="核销码"
+ prop="pickUpVerifyCode">
+ <el-input v-model="queryParams.pickUpVerifyCode" class="!w-280px" clearable placeholder="请输入自提核销码"
+ @keyup.enter="handleQuery" />
+ <el-form-item label="聚合搜索">
+ <el-input v-show="true" v-model="queryParams[queryType.queryParam]"
+ :type="queryType.queryParam === 'userId' ? 'number' : 'text'" class="!w-280px" clearable
+ placeholder="请输入">
+ <template #prepend>
+ <el-select v-model="queryType.queryParam" class="!w-110px" clearable placeholder="全部"
+ @change="inputChangeSelect">
+ <el-option v-for="dict in dynamicSearchList" :key="dict.value" :label="dict.label"
+ :value="dict.value" />
+ <el-button @click="handleQuery">
+ <Icon class="mr-5px" icon="ep:refresh" />
+ <!-- 添加 row-key="id" 解决列数据中的 table#header 数据不刷新的问题 -->
+ <el-table v-loading="loading" :data="list" row-key="id">
+ <OrderTableColumn :list="list" :pick-up-store-list="pickUpStoreList">
+ <div class="flex items-center justify-center">
+ <el-button v-hasPermi="['trade:order:query']" link type="primary" @click="openDetail(row.id)">
+ <Icon icon="ep:notification" />
+ 详情
+ <el-dropdown v-hasPermi="['trade:order:update']"
+ @command="(command) => handleCommand(command, row)">
+ <el-button link type="primary">
+ <Icon icon="ep:d-arrow-right" />
+ 更多
+ <template #dropdown>
+ <el-dropdown-menu>
+ <!-- 如果是【快递】,并且【未发货】,则展示【发货】按钮 -->
+ <el-dropdown-item v-if="
+ row.deliveryType === DeliveryTypeEnum.EXPRESS.type &&
+ row.status === TradeOrderStatusEnum.UNDELIVERED.status
+ " command="delivery">
+ <Icon icon="ep:takeaway-box" />
+ 发货
+ </el-dropdown-item>
+ <el-dropdown-item command="remark">
+ <Icon icon="ep:chat-line-square" />
+ 备注
+ </el-dropdown-menu>
+ </el-dropdown>
+ </OrderTableColumn>
+ </el-table>
+ <!-- 各种操作的弹窗 -->
+ <OrderUpdateRemarkForm ref="updateRemarkForm" @success="getList" />
+ import type { FormInstance } from 'element-plus'
+ import OrderUpdateRemarkForm from '@/views/mall/trade/order/form/OrderUpdateRemarkForm.vue'
+ import * as PickUpStoreApi from '@/api/mall/trade/delivery/pickUpStore'
+ import { DICT_TYPE, getIntDictOptions, getStrDictOptions } from '@/utils/dict'
+ import { DeliveryTypeEnum, TradeOrderStatusEnum } from '@/utils/constants'
+ import { OrderTableColumn } from './components'
+ defineOptions({ name: 'TradeOrder' })
+ const { currentRoute, push } = useRouter() // 路由跳转
+ const loading = ref(true) // 列表的加载中
+ const total = ref(2) // 列表的总页数
+ const list = ref<TradeOrderApi.OrderVO[]>([]) // 列表的数据
+ const queryFormRef = ref<FormInstance>() // 搜索的表单
+ // 表单搜索
+ pageNo: 1, // 页数
+ pageSize: 10, // 每页显示数量
+ status: undefined, // 订单状态
+ payChannelCode: undefined, // 支付方式
+ createTime: undefined, // 创建时间
+ terminal: undefined, // 订单来源
+ type: undefined, // 订单类型
+ deliveryType: undefined, // 配送方式
+ logisticsId: undefined, // 快递公司
+ pickUpStoreId: undefined, // 自提门店
+ pickUpVerifyCode: undefined // 自提核销码
+ const queryType = reactive({ queryParam: '' }) // 订单搜索类型 queryParam
+ // 订单聚合搜索 select 类型配置(动态搜索)
+ const dynamicSearchList = ref([
+ { value: 'no', label: '订单号' },
+ { value: 'userId', label: '用户UID' },
+ { value: 'userNickname', label: '用户昵称' },
+ { value: 'userMobile', label: '用户电话' }
+ * 聚合搜索切换查询对象时触发
+ * @param val
+ const inputChangeSelect = (val : string) => {
+ dynamicSearchList.value
+ .filter((item) => item.value !== val)
+ ?.forEach((item1) => {
+ // 清除集合搜索无用属性
+ if (queryParams.value.hasOwnProperty(item1.value)) {
+ delete queryParams.value[item1.value]
+ const data = await TradeOrderApi.getOrderPage(unref(queryParams))
+ const handleQuery = async () => {
+ queryParams.value.pageNo = 1
+ queryFormRef.value?.resetFields()
+ queryParams.value = {
+ /** 查看订单详情 */
+ const openDetail = (id : number) => {
+ push({ name: 'TradeOrderDetail', params: { id } })
+ /** 操作分发 */
+ const updateRemarkForm = ref()
+ const handleCommand = (command : string, row : TradeOrderApi.OrderVO) => {
+ switch (command) {
+ case 'remark':
+ updateRemarkForm.value?.open(row)
+ break
+ case 'delivery':
+ deliveryFormRef.value?.open(row)
+ // 监听路由变化更新列表,解决订单保存/更新后,列表不刷新的问题。
+ () => {
+ const pickUpStoreList = ref<PickUpStoreApi.DeliveryPickUpStoreVO[]>([]) // 自提门店精简列表
+ const deliveryExpressList = ref<DeliveryExpressApi.DeliveryExpressVO[]>([]) // 物流公司
+ pickUpStoreList.value = await PickUpStoreApi.getListAllSimple()
@@ -1,132 +1,123 @@
- <Dialog :title="dialogTitle" v-model="dialogVisible">
- label-width="100px"
- <el-form-item label="签到天数" prop="day">
- <el-input-number v-model="formData.day" :min="1" :max="7" :precision="0" />
- <el-text class="mx-1" style="margin-left: 10px" type="danger">
- 只允许设置 1-7,默认签到 7 天为一个周期
- </el-text>
- <el-form-item label="奖励积分" prop="point">
- <el-input-number v-model="formData.point" :min="0" :precision="0" />
- <el-form-item label="奖励经验" prop="experience">
- <el-input-number v-model="formData.experience" :min="0" :precision="0" />
- <el-form-item label="开启状态" prop="status">
- <el-radio-group v-model="formData.status">
- <el-radio
- v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
- :label="dict.value"
- {{ dict.label }}
- </el-radio>
- <el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button>
-import * as SignInConfigApi from '@/api/member/signin/config'
-import { CommonStatusEnum } from '@/utils/constants'
-import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
-const formData = ref<SignInConfigApi.SignInConfigVO>({} as SignInConfigApi.SignInConfigVO)
-// 奖励校验规则
-const awardValidator = (rule: any, _value: any, callback: any) => {
- if (!formData.value.point && !formData.value.experience) {
- callback(new Error('奖励积分与奖励经验至少配置一个'))
- // 清除另一个字段的错误提示
- const otherAwardField = rule?.field === 'point' ? 'experience' : 'point'
- formRef.value.validateField(otherAwardField, () => null)
- callback()
-const formRules = reactive({
- day: [{ required: true, message: '签到天数不能空', trigger: 'blur' }],
- point: [
- { required: true, message: '奖励积分不能空', trigger: 'blur' },
- { validator: awardValidator, trigger: 'blur' }
- experience: [
- { required: true, message: '奖励经验不能空', trigger: 'blur' },
- formData.value = await SignInConfigApi.getSignInConfig(id)
- await SignInConfigApi.createSignInConfig(formData.value)
- await SignInConfigApi.updateSignInConfig(formData.value)
- day: undefined,
- point: 0,
- experience: 0,
- status: CommonStatusEnum.ENABLE
+ <Dialog :title="dialogTitle" v-model="dialogVisible">
+ <el-form ref="formRef" :model="formData" :rules="formRules" label-width="100px" v-loading="formLoading">
+ <el-form-item label="签到天数" prop="day">
+ <el-input-number v-model="formData.day" :min="1" :max="31" :precision="0" />
+ <el-text class="mx-1" style="margin-left: 10px" type="danger">
+ <!-- 只允许设置 1-7,默认签到 7 天为一个周期 -->
+ </el-text>
+ <el-form-item label="奖励积分" prop="point">
+ <el-input-number v-model="formData.point" :min="0" :precision="0" />
+ <el-form-item label="奖励经验" prop="experience">
+ <el-input-number v-model="formData.experience" :min="0" :precision="0" />
+ <el-form-item label="开启状态" prop="status">
+ <el-radio-group v-model="formData.status">
+ <el-radio v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)" :key="dict.value"
+ :label="dict.value">
+ {{ dict.label }}
+ </el-radio>
+ <el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button>
+ import * as SignInConfigApi from '@/api/member/signin/config'
+ import { CommonStatusEnum } from '@/utils/constants'
+ import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+ const formData = ref<SignInConfigApi.SignInConfigVO>({} as SignInConfigApi.SignInConfigVO)
+ // 奖励校验规则
+ const awardValidator = (rule : any, _value : any, callback : any) => {
+ if (!formData.value.point && !formData.value.experience) {
+ callback(new Error('奖励积分与奖励经验至少配置一个'))
+ // 清除另一个字段的错误提示
+ const otherAwardField = rule?.field === 'point' ? 'experience' : 'point'
+ formRef.value.validateField(otherAwardField, () => null)
+ callback()
+ const formRules = reactive({
+ day: [{ required: true, message: '签到天数不能空', trigger: 'blur' }],
+ point: [
+ { required: true, message: '奖励积分不能空', trigger: 'blur' },
+ { validator: awardValidator, trigger: 'blur' }
+ experience: [
+ { required: true, message: '奖励经验不能空', trigger: 'blur' },
+ formData.value = await SignInConfigApi.getSignInConfig(id)
+ await SignInConfigApi.createSignInConfig(formData.value)
+ await SignInConfigApi.updateSignInConfig(formData.value)
+ day: undefined,
+ point: 0,
+ experience: 0,
+ status: CommonStatusEnum.ENABLE
@@ -1,7 +1,7 @@
<!--
- Copyright (C) 2018-2019
- All rights reserved, Designed By www.joolun.com
- 芋道源码:
+ 非繁源码:
① 移除 avue 组件,使用 ElementUI 原生组件
-->
① 移除暂时用不到的 websocket
② 代码优化,补充注释,提升阅读性
【微信消息 - 图文】
① 代码优化,补充注释,提升阅读性
① 移除多余的 rep 为前缀的变量,让 message 消息更简单
③ 优化消息的临时缓存策略,发送消息时,只清理被发送消息的 tab,不会强制切回到 text 输入
【微信消息 - 视频】
① bug 修复:
1)joolun 的做法:使用 mediaId 从微信公众号,下载对应的 mp4 素材,从而播放内容;
存在的问题:mediaId 有效期是 3 天,超过时间后无法播放
【微信消息 - 语音】
@@ -63,7 +63,7 @@ const emit = defineEmits<{
column-count: 5;
margin-top: 10px;
- /* 芋道源码:增加 10px,避免顶着上面 */
+ /* 非繁源码:增加 10px,避免顶着上面 */
.waterfall-item {
@@ -1,174 +1,186 @@
- label-width="80px"
- <el-form-item label="上级部门" prop="parentId">
- <el-tree-select
- v-model="formData.parentId"
- :data="deptTree"
- check-strictly
- default-expand-all
- placeholder="请选择上级部门"
- value-key="deptId"
- <el-form-item label="部门名称" prop="name">
- <el-input v-model="formData.name" placeholder="请输入部门名称" />
- <el-form-item label="显示排序" prop="sort">
- <el-input-number v-model="formData.sort" :min="0" controls-position="right" />
- <el-form-item label="负责人" prop="leaderUserId">
- <el-select v-model="formData.leaderUserId" clearable placeholder="请输入负责人">
- v-for="item in userList"
- :label="item.nickname"
- <el-form-item label="联系电话" prop="phone">
- <el-input v-model="formData.phone" maxlength="11" placeholder="请输入联系电话" />
- <el-form-item label="邮箱" prop="email">
- <el-input v-model="formData.email" maxlength="50" placeholder="请输入邮箱" />
- <el-form-item label="状态" prop="status">
- <el-select v-model="formData.status" clearable placeholder="请选择状态">
- <el-button type="primary" @click="submitForm">确 定</el-button>
-import * as DeptApi from '@/api/system/dept'
-import * as UserApi from '@/api/system/user'
-defineOptions({ name: 'SystemDeptForm' })
- title: '',
- parentId: undefined,
- name: undefined,
- sort: undefined,
- leaderUserId: undefined,
- phone: undefined,
- email: undefined,
- parentId: [{ required: true, message: '上级部门不能为空', trigger: 'blur' }],
- name: [{ required: true, message: '部门名称不能为空', trigger: 'blur' }],
- sort: [{ required: true, message: '显示排序不能为空', trigger: 'blur' }],
- email: [{ type: 'email', message: '请输入正确的邮箱地址', trigger: ['blur', 'change'] }],
- phone: [
- { pattern: /^1[3|4|5|6|7|8|9][0-9]\d{8}$/, message: '请输入正确的手机号码', trigger: 'blur' }
- status: [{ required: true, message: '状态不能为空', trigger: 'blur' }]
-const deptTree = ref() // 树形结构
-const userList = ref<UserApi.UserVO[]>([]) // 用户列表
- formData.value = await DeptApi.getDept(id)
- // 获得用户列表
- userList.value = await UserApi.getSimpleUserList()
- // 获得部门树
- await getTree()
- const data = formData.value as unknown as DeptApi.DeptVO
- await DeptApi.createDept(data)
- await DeptApi.updateDept(data)
-/** 获得部门树 */
-const getTree = async () => {
- deptTree.value = []
- const data = await DeptApi.getSimpleDeptList()
- let dept: Tree = { id: 0, name: '顶级部门', children: [] }
- dept.children = handleTree(data)
- deptTree.value.push(dept)
+ <el-form ref="formRef" v-loading="formLoading" :model="formData" :rules="formRules" label-width="80px">
+ <el-row :gutter="30">
+ <el-col span="12">
+ <el-form-item label="上级部门" prop="parentId">
+ <el-tree-select v-model="formData.parentId" :data="deptTree" :props="defaultProps"
+ check-strictly default-expand-all placeholder="请选择上级部门" value-key="deptId" />
+ <el-form-item label="部门名称" prop="name">
+ <el-input v-model="formData.name" placeholder="请输入部门名称" />
+ <el-form-item label="显示排序" prop="sort">
+ <el-input-number v-model="formData.sort" :min="0" controls-position="right" />
+ <el-form-item label="负责人" prop="leaderUserId">
+ <el-select v-model="formData.leaderUserId" clearable placeholder="请输入负责人">
+ <el-option v-for="item in userList" :key="item.id" :label="item.nickname"
+ <el-form-item label="联系电话" prop="phone">
+ <el-input v-model="formData.phone" maxlength="11" placeholder="请输入联系电话" />
+ <el-form-item label="邮箱" prop="email">
+ <el-input v-model="formData.email" maxlength="50" placeholder="请输入邮箱" />
+ <el-form-item label="状态" prop="status">
+ <el-select v-model="formData.status" clearable placeholder="请选择状态">
+ <el-option v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)" :key="dict.value"
+ <el-button type="primary" @click="submitForm">确 定</el-button>
+ import * as DeptApi from '@/api/system/dept'
+ import * as UserApi from '@/api/system/user'
+ defineOptions({ name: 'SystemDeptForm' })
+ title: '',
+ parentId: undefined,
+ name: undefined,
+ sort: undefined,
+ leaderUserId: undefined,
+ phone: undefined,
+ email: undefined,
+ parentId: [{ required: true, message: '上级部门不能为空', trigger: 'blur' }],
+ name: [{ required: true, message: '部门名称不能为空', trigger: 'blur' }],
+ sort: [{ required: true, message: '显示排序不能为空', trigger: 'blur' }],
+ email: [{ type: 'email', message: '请输入正确的邮箱地址', trigger: ['blur', 'change'] }],
+ phone: [
+ { pattern: /^1[3|4|5|6|7|8|9][0-9]\d{8}$/, message: '请输入正确的手机号码', trigger: 'blur' }
+ status: [{ required: true, message: '状态不能为空', trigger: 'blur' }]
+ const deptTree = ref() // 树形结构
+ const userList = ref<UserApi.UserVO[]>([]) // 用户列表
+ formData.value = await DeptApi.getDept(id)
+ // 获得用户列表
+ userList.value = await UserApi.getSimpleUserList()
+ // 获得部门树
+ await getTree()
+ const data = formData.value as unknown as DeptApi.DeptVO
+ await DeptApi.createDept(data)
+ await DeptApi.updateDept(data)
+ /** 获得部门树 */
+ const getTree = async () => {
+ deptTree.value = []
+ const data = await DeptApi.getSimpleDeptList()
+ let dept : Tree = { id: 0, name: '顶级部门', children: [] }
+ dept.children = handleTree(data)
+ deptTree.value.push(dept)
+ .el-row {
+ .el-input,
+ .el-select,
+ .el-input-number,
+ .el-date-editor {
+ width: 170px;
@@ -1,124 +1,118 @@
- <el-form-item label="字典名称" prop="name">
- <el-input v-model="formData.name" placeholder="请输入字典名称" />
- <el-form-item label="字典类型" prop="type">
- v-model="formData.type"
- :disabled="typeof formData.id !== 'undefined'"
- placeholder="请输入参数名称"
- <el-input v-model="formData.remark" placeholder="请输入内容" type="textarea" />
-import * as DictTypeApi from '@/api/system/dict/dict.type'
-defineOptions({ name: 'SystemDictTypeForm' })
- type: '',
- status: CommonStatusEnum.ENABLE,
- remark: ''
- name: [{ required: true, message: '字典名称不能为空', trigger: 'blur' }],
- type: [{ required: true, message: '字典类型不能为空', trigger: 'blur' }],
- status: [{ required: true, message: '状态不能为空', trigger: 'change' }]
- formData.value = await DictTypeApi.getDictType(id)
- const data = formData.value as DictTypeApi.DictTypeVO
- await DictTypeApi.createDictType(data)
- await DictTypeApi.updateDictType(data)
+ <el-form-item label="字典名称" prop="name">
+ <el-input v-model="formData.name" placeholder="请输入字典名称" />
+ <el-form-item label="字典类型" prop="type">
+ <el-input v-model="formData.type" :disabled="typeof formData.id !== 'undefined'"
+ placeholder="请输入参数名称" />
+ <el-input v-model="formData.remark" placeholder="请输入内容" type="textarea" />
+ import * as DictTypeApi from '@/api/system/dict/dict.type'
+ defineOptions({ name: 'SystemDictTypeForm' })
+ type: '',
+ status: CommonStatusEnum.ENABLE,
+ remark: ''
+ name: [{ required: true, message: '字典名称不能为空', trigger: 'blur' }],
+ type: [{ required: true, message: '字典类型不能为空', trigger: 'blur' }],
+ status: [{ required: true, message: '状态不能为空', trigger: 'change' }]
+ formData.value = await DictTypeApi.getDictType(id)
+ const data = formData.value as DictTypeApi.DictTypeVO
+ await DictTypeApi.createDictType(data)
+ await DictTypeApi.updateDictType(data)
@@ -1,183 +1,197 @@
- v-model="formData.dictType"
- <el-form-item label="数据标签" prop="label">
- <el-input v-model="formData.label" placeholder="请输入数据标签" />
- <el-form-item label="数据键值" prop="value">
- <el-input v-model="formData.value" placeholder="请输入数据键值" />
- <el-form-item label="颜色类型" prop="colorType">
- <el-select v-model="formData.colorType">
- v-for="item in colorTypeOptions"
- :key="item.value"
- :label="item.label + '(' + item.value + ')'"
- :value="item.value"
- <el-form-item label="CSS Class" prop="cssClass">
- <el-input v-model="formData.cssClass" placeholder="请输入 CSS Class" />
-import * as DictDataApi from '@/api/system/dict/dict.data'
-defineOptions({ name: 'SystemDictDataForm' })
- label: '',
- value: '',
- dictType: '',
- colorType: '',
- cssClass: '',
- label: [{ required: true, message: '数据标签不能为空', trigger: 'blur' }],
- value: [{ required: true, message: '数据键值不能为空', trigger: 'blur' }],
- sort: [{ required: true, message: '数据顺序不能为空', trigger: 'blur' }],
-// 数据标签回显样式
-const colorTypeOptions = readonly([
- value: 'default',
- label: '默认'
- value: 'primary',
- label: '主要'
- value: 'success',
- label: '成功'
- value: 'info',
- label: '信息'
- value: 'warning',
- label: '警告'
- value: 'danger',
- label: '危险'
-const open = async (type: string, id?: number, dictType?: string) => {
- if (dictType) {
- formData.value.dictType = dictType
- formData.value = await DictDataApi.getDictData(id)
- const data = formData.value as DictDataApi.DictDataVO
- await DictDataApi.createDictData(data)
- await DictDataApi.updateDictData(data)
+ <el-input v-model="formData.dictType" :disabled="typeof formData.id !== 'undefined'"
+ <el-form-item label="数据标签" prop="label">
+ <el-input v-model="formData.label" placeholder="请输入数据标签" />
+ <el-form-item label="数据键值" prop="value">
+ <el-input v-model="formData.value" placeholder="请输入数据键值" />
+ <el-form-item label="颜色类型" prop="colorType">
+ <el-select v-model="formData.colorType">
+ <el-option v-for="item in colorTypeOptions" :key="item.value"
+ :label="item.label + '(' + item.value + ')'" :value="item.value" />
+ <el-form-item label="CSS Class" prop="cssClass">
+ <el-input v-model="formData.cssClass" placeholder="请输入 CSS Class" />
+ import * as DictDataApi from '@/api/system/dict/dict.data'
+ defineOptions({ name: 'SystemDictDataForm' })
+ label: '',
+ value: '',
+ dictType: '',
+ colorType: '',
+ cssClass: '',
+ label: [{ required: true, message: '数据标签不能为空', trigger: 'blur' }],
+ value: [{ required: true, message: '数据键值不能为空', trigger: 'blur' }],
+ sort: [{ required: true, message: '数据顺序不能为空', trigger: 'blur' }],
+ // 数据标签回显样式
+ const colorTypeOptions = readonly([
+ value: 'default',
+ label: '默认'
+ value: 'primary',
+ label: '主要'
+ value: 'success',
+ label: '成功'
+ value: 'info',
+ label: '信息'
+ value: 'warning',
+ label: '警告'
+ value: 'danger',
+ label: '危险'
+ const open = async (type : string, id ?: number, dictType ?: string) => {
+ if (dictType) {
+ formData.value.dictType = dictType
+ formData.value = await DictDataApi.getDictData(id)
+ const data = formData.value as DictDataApi.DictDataVO
+ await DictDataApi.createDictData(data)
+ await DictDataApi.updateDictData(data)
@@ -1,256 +1,244 @@
- <el-form-item label="上级菜单">
- :data="menuTree"
- :default-expanded-keys="[0]"
- node-key="id"
- <el-form-item label="菜单名称" prop="name">
- <el-input v-model="formData.name" clearable placeholder="请输入菜单名称" />
- <el-form-item label="菜单类型" prop="type">
- <el-radio-group v-model="formData.type">
- <el-radio-button
- v-for="dict in getIntDictOptions(DICT_TYPE.SYSTEM_MENU_TYPE)"
- :key="dict.label"
- </el-radio-button>
- <el-form-item v-if="formData.type !== 3" label="菜单图标">
- <IconSelect v-model="formData.icon" clearable />
- <el-form-item v-if="formData.type !== 3" label="路由地址" prop="path">
- <template #label>
- <Tooltip
- message="访问的路由地址,如:`user`。如需外网地址时,则以 `http(s)://` 开头"
- title="路由地址"
- <el-input v-model="formData.path" clearable placeholder="请输入路由地址" />
- <el-form-item v-if="formData.type === 2" label="组件地址" prop="component">
- <el-input v-model="formData.component" clearable placeholder="例如说:system/user/index" />
- <el-form-item v-if="formData.type === 2" label="组件名字" prop="componentName">
- <el-input v-model="formData.componentName" clearable placeholder="例如说:SystemUser" />
- <el-form-item v-if="formData.type !== 1" label="权限标识" prop="permission">
- message="Controller 方法上的权限字符,如:@PreAuthorize(`@ss.hasPermission('system:user:list')`)"
- title="权限标识"
- <el-input v-model="formData.permission" clearable placeholder="请输入权限标识" />
- <el-input-number v-model="formData.sort" :min="0" clearable controls-position="right" />
- <el-form-item label="菜单状态" prop="status">
- <el-form-item v-if="formData.type !== 3" label="显示状态" prop="visible">
- <Tooltip message="选择隐藏时,路由将不会出现在侧边栏,但仍然可以访问" title="显示状态" />
- <el-radio-group v-model="formData.visible">
- <el-radio key="true" :label="true" border>显示</el-radio>
- <el-radio key="false" :label="false" border>隐藏</el-radio>
- <el-form-item v-if="formData.type !== 3" label="总是显示" prop="alwaysShow">
- message="选择不是时,当该菜单只有一个子菜单时,不展示自己,直接展示子菜单"
- title="总是显示"
- <el-radio-group v-model="formData.alwaysShow">
- <el-radio key="true" :label="true" border>总是</el-radio>
- <el-radio key="false" :label="false" border>不是</el-radio>
- <el-form-item v-if="formData.type === 2" label="缓存状态" prop="keepAlive">
- message="选择缓存时,则会被 `keep-alive` 缓存,必须填写「组件名称」字段"
- title="缓存状态"
- <el-radio-group v-model="formData.keepAlive">
- <el-radio key="true" :label="true" border>缓存</el-radio>
- <el-radio key="false" :label="false" border>不缓存</el-radio>
-import * as MenuApi from '@/api/system/menu'
-import { CommonStatusEnum, SystemMenuTypeEnum } from '@/utils/constants'
-defineOptions({ name: 'SystemMenuForm' })
- id: 0,
- permission: '',
- type: SystemMenuTypeEnum.DIR,
- sort: Number(undefined),
- parentId: 0,
- path: '',
- icon: '',
- component: '',
- componentName: '',
- visible: true,
- keepAlive: true,
- alwaysShow: true
- name: [{ required: true, message: '菜单名称不能为空', trigger: 'blur' }],
- sort: [{ required: true, message: '菜单顺序不能为空', trigger: 'blur' }],
- path: [{ required: true, message: '路由地址不能为空', trigger: 'blur' }],
-const open = async (type: string, id?: number, parentId?: number) => {
- if (parentId) {
- formData.value.parentId = parentId
- formData.value = await MenuApi.getMenu(id)
- // 获得菜单列表
- if (
- formData.value.type === SystemMenuTypeEnum.DIR ||
- formData.value.type === SystemMenuTypeEnum.MENU
- ) {
- if (!isExternal(formData.value.path)) {
- if (formData.value.parentId === 0 && formData.value.path.charAt(0) !== '/') {
- message.error('路径必须以 / 开头')
- } else if (formData.value.parentId !== 0 && formData.value.path.charAt(0) === '/') {
- message.error('路径不能以 / 开头')
- const data = formData.value as unknown as MenuApi.MenuVO
- await MenuApi.createMenu(data)
- await MenuApi.updateMenu(data)
- // 清空,从而触发刷新
- wsCache.delete(CACHE_KEY.ROLE_ROUTERS)
-/** 获取下拉框[上级菜单]的数据 */
-const menuTree = ref<Tree[]>([]) // 树形结构
- menuTree.value = []
- const res = await MenuApi.getSimpleMenusList()
- let menu: Tree = { id: 0, name: '主类目', children: [] }
- menu.children = handleTree(res)
- menuTree.value.push(menu)
-/** 判断 path 是不是外部的 HTTP 等链接 */
-const isExternal = (path: string) => {
- return /^(https?:|mailto:|tel:)/.test(path)
+ <el-form ref="formRef" v-loading="formLoading" :model="formData" :rules="formRules" label-width="100px">
+ <el-form-item label="上级菜单">
+ <el-tree-select v-model="formData.parentId" :data="menuTree" :default-expanded-keys="[0]"
+ :props="defaultProps" check-strictly node-key="id" />
+ <el-form-item label="菜单名称" prop="name">
+ <el-input v-model="formData.name" clearable placeholder="请输入菜单名称" />
+ <el-form-item label="菜单类型" prop="type">
+ <el-radio-group v-model="formData.type">
+ <el-radio-button v-for="dict in getIntDictOptions(DICT_TYPE.SYSTEM_MENU_TYPE)" :key="dict.label"
+ </el-radio-button>
+ <el-form-item v-if="formData.type !== 3" label="菜单图标">
+ <IconSelect v-model="formData.icon" clearable />
+ <el-form-item v-if="formData.type !== 3" label="路由地址" prop="path">
+ <template #label>
+ <Tooltip message="访问的路由地址,如:`user`。如需外网地址时,则以 `http(s)://` 开头" title="路由地址" />
+ <el-input v-model="formData.path" clearable placeholder="请输入路由地址" />
+ <el-form-item v-if="formData.type === 2" label="组件地址" prop="component">
+ <el-input v-model="formData.component" clearable placeholder="例如说:system/user/index" />
+ <el-form-item v-if="formData.type === 2" label="组件名字" prop="componentName">
+ <el-input v-model="formData.componentName" clearable placeholder="例如说:SystemUser" />
+ <el-form-item v-if="formData.type !== 1" label="权限标识" prop="permission">
+ <Tooltip message="Controller 方法上的权限字符,如:@PreAuthorize(`@ss.hasPermission('system:user:list')`)"
+ title="权限标识" />
+ <el-input v-model="formData.permission" clearable placeholder="请输入权限标识" />
+ <el-input-number v-model="formData.sort" :min="0" clearable controls-position="right" />
+ <el-form-item label="菜单状态" prop="status">
+ <el-radio v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)" :key="dict.label"
+ <el-form-item v-if="formData.type !== 3" label="显示状态" prop="visible">
+ <Tooltip message="选择隐藏时,路由将不会出现在侧边栏,但仍然可以访问" title="显示状态" />
+ <el-radio-group v-model="formData.visible">
+ <el-radio key="true" :label="true" border>显示</el-radio>
+ <el-radio key="false" :label="false" border>隐藏</el-radio>
+ <el-form-item v-if="formData.type !== 3" label="总是显示" prop="alwaysShow">
+ <Tooltip message="选择不是时,当该菜单只有一个子菜单时,不展示自己,直接展示子菜单" title="总是显示" />
+ <el-radio-group v-model="formData.alwaysShow">
+ <el-radio key="true" :label="true" border>总是</el-radio>
+ <el-radio key="false" :label="false" border>不是</el-radio>
+ <el-form-item v-if="formData.type === 2" label="缓存状态" prop="keepAlive">
+ <Tooltip message="选择缓存时,则会被 `keep-alive` 缓存,必须填写「组件名称」字段" title="缓存状态" />
+ <el-radio-group v-model="formData.keepAlive">
+ <el-radio key="true" :label="true" border>缓存</el-radio>
+ <el-radio key="false" :label="false" border>不缓存</el-radio>
+ import * as MenuApi from '@/api/system/menu'
+ import { CommonStatusEnum, SystemMenuTypeEnum } from '@/utils/constants'
+ defineOptions({ name: 'SystemMenuForm' })
+ id: 0,
+ permission: '',
+ type: SystemMenuTypeEnum.DIR,
+ sort: Number(undefined),
+ parentId: 0,
+ path: '',
+ icon: '',
+ component: '',
+ componentName: '',
+ visible: true,
+ keepAlive: true,
+ alwaysShow: true
+ name: [{ required: true, message: '菜单名称不能为空', trigger: 'blur' }],
+ sort: [{ required: true, message: '菜单顺序不能为空', trigger: 'blur' }],
+ path: [{ required: true, message: '路由地址不能为空', trigger: 'blur' }],
+ const open = async (type : string, id ?: number, parentId ?: number) => {
+ if (parentId) {
+ formData.value.parentId = parentId
+ formData.value = await MenuApi.getMenu(id)
+ // 获得菜单列表
+ if (
+ formData.value.type === SystemMenuTypeEnum.DIR ||
+ formData.value.type === SystemMenuTypeEnum.MENU
+ ) {
+ if (!isExternal(formData.value.path)) {
+ if (formData.value.parentId === 0 && formData.value.path.charAt(0) !== '/') {
+ message.error('路径必须以 / 开头')
+ } else if (formData.value.parentId !== 0 && formData.value.path.charAt(0) === '/') {
+ message.error('路径不能以 / 开头')
+ const data = formData.value as unknown as MenuApi.MenuVO
+ await MenuApi.createMenu(data)
+ await MenuApi.updateMenu(data)
+ // 清空,从而触发刷新
+ wsCache.delete(CACHE_KEY.ROLE_ROUTERS)
+ /** 获取下拉框[上级菜单]的数据 */
+ const menuTree = ref<Tree[]>([]) // 树形结构
+ menuTree.value = []
+ const res = await MenuApi.getSimpleMenusList()
+ let menu : Tree = { id: 0, name: '主类目', children: [] }
+ menu.children = handleTree(res)
+ menuTree.value.push(menu)
+ /** 判断 path 是不是外部的 HTTP 等链接 */
+ const isExternal = (path : string) => {
+ return /^(https?:|mailto:|tel:)/.test(path)
+ width: 150px;
@@ -1,125 +1,138 @@
- <Dialog v-model="dialogVisible" :title="dialogTitle" width="800">
- <el-form-item label="岗位标题" prop="name">
- <el-input v-model="formData.name" placeholder="请输入岗位标题" />
- <el-form-item label="岗位编码" prop="code">
- <el-input v-model="formData.code" placeholder="请输入岗位编码" />
- <el-form-item label="岗位顺序" prop="sort">
- <el-input v-model="formData.sort" placeholder="请输入岗位顺序" />
- <el-input v-model="formData.remark" placeholder="请输备注" type="textarea" />
-import * as PostApi from '@/api/system/post'
-defineOptions({ name: 'SystemPostForm' })
- code: '',
- sort: 0,
- name: [{ required: true, message: '岗位标题不能为空', trigger: 'blur' }],
- code: [{ required: true, message: '岗位编码不能为空', trigger: 'change' }],
- status: [{ required: true, message: '岗位状态不能为空', trigger: 'change' }],
- remark: [{ required: false, message: '岗位内容不能为空', trigger: 'blur' }]
- formData.value = await PostApi.getPost(id)
- const data = formData.value as unknown as PostApi.PostVO
- await PostApi.createPost(data)
- await PostApi.updatePost(data)
- } as any
+ <Dialog v-model="dialogVisible" :title="dialogTitle" width="700">
+ <el-form-item label="岗位标题" prop="name">
+ <el-input v-model="formData.name" placeholder="请输入岗位标题" />
+ <el-form-item label="岗位编码" prop="code">
+ <el-input v-model="formData.code" placeholder="请输入岗位编码" />
+ <el-form-item label="岗位顺序" prop="sort">
+ <el-input v-model="formData.sort" placeholder="请输入岗位顺序" />
+ <el-input style="width:580px" v-model="formData.remark" placeholder="请输备注" type="textarea" />
+ import * as PostApi from '@/api/system/post'
+ defineOptions({ name: 'SystemPostForm' })
+ code: '',
+ sort: 0,
+ name: [{ required: true, message: '岗位标题不能为空', trigger: 'blur' }],
+ code: [{ required: true, message: '岗位编码不能为空', trigger: 'change' }],
+ status: [{ required: true, message: '岗位状态不能为空', trigger: 'change' }],
+ remark: [{ required: false, message: '岗位内容不能为空', trigger: 'blur' }]
+ formData.value = await PostApi.getPost(id)
+ const data = formData.value as unknown as PostApi.PostVO
+ await PostApi.createPost(data)
+ await PostApi.updatePost(data)
+ } as any
+ width: 235px;
@@ -1,160 +1,150 @@
- <Dialog v-model="dialogVisible" title="菜单权限">
- <el-form-item label="角色名称">
- <el-tag>{{ formData.name }}</el-tag>
- <el-form-item label="角色标识">
- <el-tag>{{ formData.code }}</el-tag>
- <el-form-item label="菜单权限">
- <el-card class="cardHeight">
- 全选/全不选:
- v-model="treeNodeAll"
- active-text="是"
- inactive-text="否"
- @change="handleCheckedTreeNodeAll"
- 全部展开/折叠:
- v-model="menuExpand"
- active-text="展开"
- inactive-text="折叠"
- @change="handleCheckedTreeExpand"
- <el-tree
- ref="treeRef"
- :data="menuOptions"
- empty-text="加载中,请稍候"
- show-checkbox
-import * as RoleApi from '@/api/system/role'
-import * as PermissionApi from '@/api/system/permission'
-defineOptions({ name: 'SystemRoleAssignMenuForm' })
-const formData = reactive({
- menuIds: []
-const menuOptions = ref<any[]>([]) // 菜单树形结构
-const menuExpand = ref(false) // 展开/折叠
-const treeRef = ref() // 菜单树组件 Ref
-const treeNodeAll = ref(false) // 全选/全不选
-const open = async (row: RoleApi.RoleVO) => {
- // 加载 Menu 列表。注意,必须放在前面,不然下面 setChecked 没数据节点
- menuOptions.value = handleTree(await MenuApi.getSimpleMenusList())
- formData.id = row.id
- formData.name = row.name
- formData.code = row.code
- formData.value.menuIds = await PermissionApi.getRoleMenuList(row.id)
- // 设置选中
- formData.value.menuIds.forEach((menuId: number) => {
- treeRef.value.setChecked(menuId, true, false)
- roleId: formData.id,
- menuIds: [
- ...(treeRef.value.getCheckedKeys(false) as unknown as Array<number>), // 获得当前选中节点
- ...(treeRef.value.getHalfCheckedKeys() as unknown as Array<number>) // 获得半选中的父节点
- await PermissionApi.assignRoleMenu(data)
- // 重置选项
- treeNodeAll.value = false
- menuExpand.value = false
- // 重置表单
- treeRef.value?.setCheckedNodes([])
-/** 全选/全不选 */
-const handleCheckedTreeNodeAll = () => {
- treeRef.value.setCheckedNodes(treeNodeAll.value ? menuOptions.value : [])
-/** 展开/折叠全部 */
-const handleCheckedTreeExpand = () => {
- const nodes = treeRef.value?.store.nodesMap
- for (let node in nodes) {
- if (nodes[node].expanded === menuExpand.value) {
- continue
- nodes[node].expanded = menuExpand.value
-.cardHeight {
- max-height: 400px;
- overflow-y: scroll;
+ <Dialog v-model="dialogVisible" title="菜单权限">
+ <el-form-item label="角色名称">
+ <el-tag>{{ formData.name }}</el-tag>
+ <el-form-item label="角色标识">
+ <el-tag>{{ formData.code }}</el-tag>
+ <el-form-item label="菜单权限">
+ <el-card class="cardHeight">
+ 全选/全不选:
+ <el-switch v-model="treeNodeAll" active-text="是" inactive-text="否" inline-prompt
+ @change="handleCheckedTreeNodeAll" />
+ 全部展开/折叠:
+ <el-switch v-model="menuExpand" active-text="展开" inactive-text="折叠" inline-prompt
+ @change="handleCheckedTreeExpand" />
+ <el-tree ref="treeRef" :data="menuOptions" :props="defaultProps" empty-text="加载中,请稍候" node-key="id"
+ show-checkbox />
+ import * as RoleApi from '@/api/system/role'
+ import * as PermissionApi from '@/api/system/permission'
+ defineOptions({ name: 'SystemRoleAssignMenuForm' })
+ const formData = reactive({
+ menuIds: []
+ const menuOptions = ref<any[]>([]) // 菜单树形结构
+ const menuExpand = ref(false) // 展开/折叠
+ const treeRef = ref() // 菜单树组件 Ref
+ const treeNodeAll = ref(false) // 全选/全不选
+ const open = async (row : RoleApi.RoleVO) => {
+ // 加载 Menu 列表。注意,必须放在前面,不然下面 setChecked 没数据节点
+ menuOptions.value = handleTree(await MenuApi.getSimpleMenusList())
+ formData.id = row.id
+ formData.name = row.name
+ formData.code = row.code
+ formData.value.menuIds = await PermissionApi.getRoleMenuList(row.id)
+ // 设置选中
+ formData.value.menuIds.forEach((menuId : number) => {
+ treeRef.value.setChecked(menuId, true, false)
+ roleId: formData.id,
+ menuIds: [
+ ...(treeRef.value.getCheckedKeys(false) as unknown as Array<number>), // 获得当前选中节点
+ ...(treeRef.value.getHalfCheckedKeys() as unknown as Array<number>) // 获得半选中的父节点
+ await PermissionApi.assignRoleMenu(data)
+ // 重置选项
+ treeNodeAll.value = false
+ menuExpand.value = false
+ // 重置表单
+ treeRef.value?.setCheckedNodes([])
+ /** 全选/全不选 */
+ const handleCheckedTreeNodeAll = () => {
+ treeRef.value.setCheckedNodes(treeNodeAll.value ? menuOptions.value : [])
+ /** 展开/折叠全部 */
+ const handleCheckedTreeExpand = () => {
+ const nodes = treeRef.value?.store.nodesMap
+ for (let node in nodes) {
+ if (nodes[node].expanded === menuExpand.value) {
+ continue
+ nodes[node].expanded = menuExpand.value
+ .cardHeight {
+ max-height: 400px;
+ overflow-y: scroll;
@@ -1,167 +1,147 @@
- <Dialog v-model="dialogVisible" title="菜单权限" width="800">
- <el-form-item label="权限范围">
- <el-select v-model="formData.dataScope">
- v-for="item in getIntDictOptions(DICT_TYPE.SYSTEM_DATA_SCOPE)"
- :label="item.label"
- v-if="formData.dataScope === SystemDataScopeEnum.DEPT_CUSTOM"
- label="权限范围"
- style="display: flex"
- <el-card class="card" shadow="never">
- @change="handleCheckedTreeNodeAll()"
- v-model="deptExpand"
- 父子联动(选中父节点,自动选择子节点):
- <el-switch v-model="checkStrictly" active-text="是" inactive-text="否" inline-prompt />
- :check-strictly="!checkStrictly"
- :data="deptOptions"
- empty-text="加载中,请稍后"
-import { SystemDataScopeEnum } from '@/utils/constants'
-defineOptions({ name: 'SystemRoleDataPermissionForm' })
- dataScope: undefined,
- dataScopeDeptIds: []
-const deptOptions = ref<any[]>([]) // 部门树形结构
-const deptExpand = ref(false) // 展开/折叠
-const checkStrictly = ref(true) // 是否严格模式,即父子不关联
- // 加载 Dept 列表。注意,必须放在前面,不然下面 setChecked 没数据节点
- deptOptions.value = handleTree(await DeptApi.getSimpleDeptList())
- formData.dataScope = row.dataScope
- row.dataScopeDeptIds?.forEach((deptId: number) => {
- treeRef.value.setChecked(deptId, true, false)
- dataScope: formData.dataScope,
- dataScopeDeptIds:
- formData.dataScope !== SystemDataScopeEnum.DEPT_CUSTOM
- ? []
- : treeRef.value.getCheckedKeys(false)
- await PermissionApi.assignRoleDataScope(data)
- deptExpand.value = false
- checkStrictly.value = true
- treeRef.value.setCheckedNodes(treeNodeAll.value ? deptOptions.value : [])
- if (nodes[node].expanded === deptExpand.value) {
- nodes[node].expanded = deptExpand.value
+ <Dialog v-model="dialogVisible" title="菜单权限" width="800">
+ <el-form-item label="权限范围">
+ <el-select v-model="formData.dataScope">
+ <el-option v-for="item in getIntDictOptions(DICT_TYPE.SYSTEM_DATA_SCOPE)" :key="item.value"
+ :label="item.label" :value="item.value" />
+ <el-form-item v-if="formData.dataScope === SystemDataScopeEnum.DEPT_CUSTOM" label="权限范围" style="display: flex">
+ <el-card class="card" shadow="never">
+ @change="handleCheckedTreeNodeAll()" />
+ <el-switch v-model="deptExpand" active-text="展开" inactive-text="折叠" inline-prompt
+ 父子联动(选中父节点,自动选择子节点):
+ <el-switch v-model="checkStrictly" active-text="是" inactive-text="否" inline-prompt />
+ <el-tree ref="treeRef" :check-strictly="!checkStrictly" :data="deptOptions" :props="defaultProps"
+ default-expand-all empty-text="加载中,请稍后" node-key="id" show-checkbox />
+ import { SystemDataScopeEnum } from '@/utils/constants'
+ defineOptions({ name: 'SystemRoleDataPermissionForm' })
+ dataScope: undefined,
+ dataScopeDeptIds: []
+ const deptOptions = ref<any[]>([]) // 部门树形结构
+ const deptExpand = ref(false) // 展开/折叠
+ const checkStrictly = ref(true) // 是否严格模式,即父子不关联
+ // 加载 Dept 列表。注意,必须放在前面,不然下面 setChecked 没数据节点
+ deptOptions.value = handleTree(await DeptApi.getSimpleDeptList())
+ formData.dataScope = row.dataScope
+ row.dataScopeDeptIds?.forEach((deptId : number) => {
+ treeRef.value.setChecked(deptId, true, false)
+ dataScope: formData.dataScope,
+ dataScopeDeptIds:
+ formData.dataScope !== SystemDataScopeEnum.DEPT_CUSTOM
+ ? []
+ : treeRef.value.getCheckedKeys(false)
+ await PermissionApi.assignRoleDataScope(data)
+ deptExpand.value = false
+ checkStrictly.value = true
+ treeRef.value.setCheckedNodes(treeNodeAll.value ? deptOptions.value : [])
+ if (nodes[node].expanded === deptExpand.value) {
+ nodes[node].expanded = deptExpand.value
@@ -1,126 +1,139 @@
- <el-form-item label="角色名称" prop="name">
- <el-input v-model="formData.name" placeholder="请输入角色名称" />
- <el-form-item label="角色标识" prop="code">
- <el-input v-model="formData.code" placeholder="请输入角色标识" />
- <el-form-item label="显示顺序" prop="sort">
- <el-input v-model="formData.sort" placeholder="请输入显示顺序" />
-defineOptions({ name: 'SystemRoleForm' })
- sort: [{ required: true, message: '岗位顺序不能为空', trigger: 'change' }],
- formData.value = await RoleApi.getRole(id)
- const data = formData.value as unknown as RoleApi.RoleVO
- await RoleApi.createRole(data)
- await RoleApi.updateRole(data)
+ <el-form-item label="角色名称" prop="name">
+ <el-input v-model="formData.name" placeholder="请输入角色名称" />
+ <el-form-item label="角色标识" prop="code">
+ <el-input v-model="formData.code" placeholder="请输入角色标识" />
+ <el-form-item label="显示顺序" prop="sort">
+ <el-input v-model="formData.sort" placeholder="请输入显示顺序" />
+ <el-input style="width:430px" v-model="formData.remark" placeholder="请输备注" type="textarea" />
+ defineOptions({ name: 'SystemRoleForm' })
+ sort: [{ required: true, message: '岗位顺序不能为空', trigger: 'change' }],
+ formData.value = await RoleApi.getRole(id)
+ const data = formData.value as unknown as RoleApi.RoleVO
+ await RoleApi.createRole(data)
+ await RoleApi.updateRole(data)
+ width: 160px;
@@ -1,183 +1,198 @@
- <Dialog v-model="dialogVisible" :title="dialogTitle" width="50%">
- <el-form-item label="租户名" prop="name">
- <el-input v-model="formData.name" placeholder="请输入租户名" />
- <el-form-item label="租户套餐" prop="packageId">
- <el-select v-model="formData.packageId" clearable placeholder="请选择租户套餐">
- v-for="item in packageList"
- <el-form-item label="联系人" prop="contactName">
- <el-input v-model="formData.contactName" placeholder="请输入联系人" />
- <el-form-item label="联系手机" prop="contactMobile">
- <el-input v-model="formData.contactMobile" placeholder="请输入联系手机" />
- <el-form-item v-if="formData.id === undefined" label="用户名称" prop="username">
- <el-input v-model="formData.username" placeholder="请输入用户名称" />
- <el-form-item v-if="formData.id === undefined" label="用户密码" prop="password">
- v-model="formData.password"
- placeholder="请输入用户密码"
- <el-form-item label="账号额度" prop="accountCount">
- <el-input-number
- v-model="formData.accountCount"
- :min="0"
- controls-position="right"
- placeholder="请输入账号额度"
- <el-form-item label="过期时间" prop="expireTime">
- v-model="formData.expireTime"
- placeholder="请选择过期时间"
- type="date"
- value-format="x"
- <el-form-item label="绑定域名" prop="website">
- <el-input v-model="formData.website" placeholder="请输入绑定域名" />
- <el-form-item label="租户状态" prop="status">
-import * as TenantApi from '@/api/system/tenant'
-import * as TenantPackageApi from '@/api/system/tenantPackage'
-defineOptions({ name: 'SystemTenantForm' })
- packageId: undefined,
- contactName: undefined,
- contactMobile: undefined,
- accountCount: undefined,
- expireTime: undefined,
- website: undefined,
- // 新增专属
- username: undefined,
- password: undefined
- name: [{ required: true, message: '租户名不能为空', trigger: 'blur' }],
- packageId: [{ required: true, message: '租户套餐不能为空', trigger: 'blur' }],
- contactName: [{ required: true, message: '联系人不能为空', trigger: 'blur' }],
- status: [{ required: true, message: '租户状态不能为空', trigger: 'blur' }],
- accountCount: [{ required: true, message: '账号额度不能为空', trigger: 'blur' }],
- expireTime: [{ required: true, message: '过期时间不能为空', trigger: 'blur' }],
- website: [{ required: true, message: '绑定域名不能为空', trigger: 'blur' }],
- username: [{ required: true, message: '用户名称不能为空', trigger: 'blur' }],
- password: [{ required: true, message: '用户密码不能为空', trigger: 'blur' }]
-const packageList = ref([] as TenantPackageApi.TenantPackageVO[]) // 租户套餐
- formData.value = await TenantApi.getTenant(id)
- // 加载套餐列表
- packageList.value = await TenantPackageApi.getTenantPackageList()
- const data = formData.value as unknown as TenantApi.TenantVO
- await TenantApi.createTenant(data)
- await TenantApi.updateTenant(data)
+ <Dialog v-model="dialogVisible" :title="dialogTitle" width="50%">
+ <el-form-item label="品牌方" :label-width="labelWidth" prop="name">
+ <el-input v-model="formData.name" placeholder="请输入品牌方" />
+ <el-form-item label="品牌方套餐" :label-width="labelWidth" prop="packageId">
+ <el-select v-model="formData.packageId" clearable placeholder="请选择品牌方套餐">
+ <el-option v-for="item in packageList" :key="item.id" :label="item.name" :value="item.id" />
+ <el-form-item label="联系人" :label-width="labelWidth" prop="contactName">
+ <el-input v-model="formData.contactName" placeholder="请输入联系人" />
+ <el-form-item label="联系手机" :label-width="labelWidth" prop="contactMobile">
+ <el-input v-model="formData.contactMobile" placeholder="请输入联系手机" />
+ <el-form-item v-if="formData.id === undefined" label="用户名称" :label-width="labelWidth"
+ prop="username">
+ <el-input v-model="formData.username" placeholder="请输入用户名称" />
+ <el-form-item v-if="formData.id === undefined" label="用户密码" :label-width="labelWidth"
+ prop="password">
+ <el-input v-model="formData.password" placeholder="请输入用户密码" show-password type="password" />
+ <el-form-item label="账号额度" :label-width="labelWidth" prop="accountCount">
+ <el-input-number v-model="formData.accountCount" :min="0" controls-position="right"
+ placeholder="请输入账号额度" />
+ <el-form-item label="过期时间" :label-width="labelWidth" prop="expireTime">
+ <el-date-picker style="width:200px" v-model="formData.expireTime" clearable
+ placeholder="请选择过期时间" type="date" value-format="x" />
+ <el-form-item label="绑定域名" :label-width="labelWidth" prop="website">
+ <el-input v-model="formData.website" placeholder="请输入绑定域名" />
+ <el-form-item label="品牌方状态" :label-width="labelWidth" class="item" prop="status">
+ import * as TenantApi from '@/api/system/tenant'
+ import * as TenantPackageApi from '@/api/system/tenantPackage'
+ defineOptions({ name: 'SystemTenantForm' })
+ const labelWidth = 100
+ packageId: undefined,
+ contactName: undefined,
+ contactMobile: undefined,
+ accountCount: undefined,
+ expireTime: undefined,
+ website: undefined,
+ // 新增专属
+ username: undefined,
+ password: undefined
+ name: [{ required: true, message: '租户名不能为空', trigger: 'blur' }],
+ packageId: [{ required: true, message: '租户套餐不能为空', trigger: 'blur' }],
+ contactName: [{ required: true, message: '联系人不能为空', trigger: 'blur' }],
+ status: [{ required: true, message: '租户状态不能为空', trigger: 'blur' }],
+ accountCount: [{ required: true, message: '账号额度不能为空', trigger: 'blur' }],
+ expireTime: [{ required: true, message: '过期时间不能为空', trigger: 'blur' }],
+ website: [{ required: true, message: '绑定域名不能为空', trigger: 'blur' }],
+ username: [{ required: true, message: '用户名称不能为空', trigger: 'blur' }],
+ password: [{ required: true, message: '用户密码不能为空', trigger: 'blur' }]
+ const packageList = ref([] as TenantPackageApi.TenantPackageVO[]) // 租户套餐
+ formData.value = await TenantApi.getTenant(id)
+ // 加载套餐列表
+ packageList.value = await TenantPackageApi.getTenantPackageList()
+ const data = formData.value as unknown as TenantApi.TenantVO
+ await TenantApi.createTenant(data)
+ await TenantApi.updateTenant(data)
+ width: 200px;
@@ -1,194 +1,178 @@
- <el-form-item label="套餐名" prop="name">
- <el-input v-model="formData.name" placeholder="请输入套餐名" />
-import { ElTree } from 'element-plus'
-defineOptions({ name: 'SystemTenantPackageForm' })
- id: null,
- name: null,
- remark: null,
- menuIds: [],
- name: [{ required: true, message: '套餐名不能为空', trigger: 'blur' }],
- status: [{ required: true, message: '状态不能为空', trigger: 'blur' }],
- menuIds: [{ required: true, message: '关联的菜单编号不能为空', trigger: 'blur' }]
-const menuOptions = ref<any[]>([]) // 树形结构数据
-const treeRef = ref<InstanceType<typeof ElTree>>() // 树组件 Ref
- const res = await TenantPackageApi.getTenantPackage(id)
- res.menuIds.forEach((menuId: number) => {
- treeRef.value!.setChecked(menuId, true, false)
- const data = formData.value as unknown as TenantPackageApi.TenantPackageVO
- data.menuIds = [
- ...(treeRef.value!.getCheckedKeys(false) as unknown as Array<number>), // 获得当前选中节点
- ...(treeRef.value!.getHalfCheckedKeys() as unknown as Array<number>) // 获得半选中的父节点
- await TenantPackageApi.createTenantPackage(data)
- await TenantPackageApi.updateTenantPackage(data)
- treeRef.value!.setCheckedNodes(treeNodeAll.value ? menuOptions.value : [])
+ <el-form-item label="套餐名" prop="name">
+ <el-input v-model="formData.name" placeholder="请输入套餐名" />
+ import { ElTree } from 'element-plus'
+ defineOptions({ name: 'SystemTenantPackageForm' })
+ id: null,
+ name: null,
+ remark: null,
+ menuIds: [],
+ name: [{ required: true, message: '套餐名不能为空', trigger: 'blur' }],
+ status: [{ required: true, message: '状态不能为空', trigger: 'blur' }],
+ menuIds: [{ required: true, message: '关联的菜单编号不能为空', trigger: 'blur' }]
+ const menuOptions = ref<any[]>([]) // 树形结构数据
+ const treeRef = ref<InstanceType<typeof ElTree>>() // 树组件 Ref
+ const res = await TenantPackageApi.getTenantPackage(id)
+ res.menuIds.forEach((menuId : number) => {
+ treeRef.value!.setChecked(menuId, true, false)
+ const data = formData.value as unknown as TenantPackageApi.TenantPackageVO
+ data.menuIds = [
+ ...(treeRef.value!.getCheckedKeys(false) as unknown as Array<number>), // 获得当前选中节点
+ ...(treeRef.value!.getHalfCheckedKeys() as unknown as Array<number>) // 获得半选中的父节点
+ await TenantPackageApi.createTenantPackage(data)
+ await TenantPackageApi.updateTenantPackage(data)
+ treeRef.value!.setCheckedNodes(treeNodeAll.value ? menuOptions.value : [])
@@ -1,96 +1,109 @@
- <Dialog v-model="dialogVisible" title="分配角色">
- <el-form-item label="用户名称">
- <el-input v-model="formData.username" :disabled="true" />
- <el-form-item label="用户昵称">
- <el-input v-model="formData.nickname" :disabled="true" />
- <el-form-item label="角色">
- <el-select v-model="formData.roleIds" multiple placeholder="请选择角色">
- <el-option v-for="item in roleList" :key="item.id" :label="item.name" :value="item.id" />
-defineOptions({ name: 'SystemUserAssignRoleForm' })
- id: -1,
- nickname: '',
- username: '',
- roleIds: []
-const roleList = ref([] as RoleApi.RoleVO[]) // 角色的列表
-const open = async (row: UserApi.UserVO) => {
- formData.value.id = row.id
- formData.value.username = row.username
- formData.value.nickname = row.nickname
- // 获得角色拥有的菜单集合
- formData.value.roleIds = await PermissionApi.getUserRoleList(row.id)
- // 获得角色列表
- roleList.value = await RoleApi.getSimpleRoleList()
- await PermissionApi.assignUserRole({
- userId: formData.value.id,
- roleIds: formData.value.roleIds
+ <Dialog v-model="dialogVisible" title="分配角色">
+ <el-form-item label="用户名称">
+ <el-input v-model="formData.username" :disabled="true" />
+ <el-form-item label="用户昵称">
+ <el-input v-model="formData.nickname" :disabled="true" />
+ <el-form-item label="角色">
+ <el-select style="width:150px" v-model="formData.roleIds" multiple placeholder="请选择角色">
+ <el-option v-for="item in roleList" :key="item.id" :label="item.name" :value="item.id" />
+ .el-input {
+ width: 150px
+ defineOptions({ name: 'SystemUserAssignRoleForm' })
+ id: -1,
+ nickname: '',
+ roleIds: []
+ const roleList = ref([] as RoleApi.RoleVO[]) // 角色的列表
+ const open = async (row : UserApi.UserVO) => {
+ formData.value.id = row.id
+ formData.value.username = row.username
+ formData.value.nickname = row.nickname
+ // 获得角色拥有的菜单集合
+ formData.value.roleIds = await PermissionApi.getUserRoleList(row.id)
+ // 获得角色列表
+ roleList.value = await RoleApi.getSimpleRoleList()
+ await PermissionApi.assignUserRole({
+ userId: formData.value.id,
+ roleIds: formData.value.roleIds