Explorar el Código

update:从admin更新一个版本

xuruhua hace 1 año
padre
commit
e8503b651f
Se han modificado 58 ficheros con 6735 adiciones y 6071 borrados
  1. 1 1
      .env
  2. 3 3
      .env.dev
  3. 2 2
      .env.prod
  4. 2 2
      .env.test
  5. 1 1
      src/App.vue
  6. BIN
      src/assets/imgs/zxlogo.png
  7. 1 0
      src/assets/svgs/folderShare.svg
  8. 1 0
      src/assets/svgs/olderOpen.svg
  9. 13 0
      src/assets/svgs/wenjianjia_shouqi.svg
  10. 17 0
      src/assets/svgs/wenjianjia_zhankai.svg
  11. 1 1
      src/components/DiyEditor/components/mobile/UserCard/index.vue
  12. 6 5
      src/components/UploadFile/index.ts
  13. 268 0
      src/components/UploadFile/src/SPuUploadImg.vue
  14. 1 1
      src/components/bpmnProcessDesigner/package/designer/ProcessDesigner.vue
  15. 71 72
      src/layout/components/AppView.vue
  16. 22 24
      src/layout/components/Footer/src/Footer.vue
  17. 29 32
      src/layout/components/Screenfull/src/Screenfull.vue
  18. 287 299
      src/layout/components/Setting/src/Setting.vue
  19. 224 224
      src/layout/components/Setting/src/components/InterfaceDisplay.vue
  20. 2 2
      src/layout/components/UserInfo/src/UserInfo.vue
  21. 442 442
      src/locales/zh-CN.ts
  22. 13 13
      src/router/modules/remaining.ts
  23. 277 276
      src/store/modules/app.ts
  24. 66 66
      src/styles/var.css
  25. 1 1
      src/utils/constants.ts
  26. 363 384
      src/views/Home/Index.vue
  27. 224 0
      src/views/Home/components/ToBeDone.vue
  28. 347 308
      src/views/Home/echarts-data.ts
  29. 92 104
      src/views/Login/Login.vue
  30. 2 2
      src/views/Login/SocialLogin.vue
  31. 306 348
      src/views/Login/components/LoginForm.vue
  32. 1 1
      src/views/Login/components/MobileForm.vue
  33. 167 196
      src/views/infra/fileConfig/FileConfigForm.vue
  34. 76 82
      src/views/mall/home/components/ShortcutCard.vue
  35. 116 146
      src/views/mall/product/spu/form/InfoForm.vue
  36. 460 204
      src/views/mall/product/spu/form/index.vue
  37. 535 449
      src/views/mall/product/spu/index.vue
  38. 101 99
      src/views/mall/trade/order/form/OrderDeliveryForm.vue
  39. 260 354
      src/views/mall/trade/order/index.vue
  40. 123 132
      src/views/member/signin/config/SignInConfigForm.vue
  41. 1 1
      src/views/mp/components/wx-material-select/main.vue
  42. 1 1
      src/views/mp/components/wx-msg/main.vue
  43. 1 1
      src/views/mp/components/wx-news/main.vue
  44. 1 1
      src/views/mp/components/wx-reply/main.vue
  45. 1 1
      src/views/mp/components/wx-video-play/main.vue
  46. 1 1
      src/views/mp/components/wx-voice-play/main.vue
  47. 1 1
      src/views/mp/material/components/ImageTable.vue
  48. 186 174
      src/views/system/dept/DeptForm.vue
  49. 118 124
      src/views/system/dict/DictTypeForm.vue
  50. 197 183
      src/views/system/dict/data/DictDataForm.vue
  51. 244 256
      src/views/system/menu/MenuForm.vue
  52. 138 125
      src/views/system/post/PostForm.vue
  53. 150 160
      src/views/system/role/RoleAssignMenuForm.vue
  54. 147 167
      src/views/system/role/RoleDataPermissionForm.vue
  55. 139 126
      src/views/system/role/RoleForm.vue
  56. 198 183
      src/views/system/tenant/TenantForm.vue
  57. 178 194
      src/views/system/tenantPackage/TenantPackageForm.vue
  58. 109 96
      src/views/system/user/UserAssignRoleForm.vue

+ 1 - 1
.env

@@ -2,7 +2,7 @@
 VITE_APP_TITLE=中星
 
 # 项目本地运行端口号
-VITE_PORT=80
+VITE_PORT=9001
 
 # open 运行 npm run dev 时自动打开浏览器
 VITE_OPEN=true

+ 3 - 3
.env.dev

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

+ 2 - 2
.env.prod

@@ -4,10 +4,10 @@ NODE_ENV=production
 VITE_DEV=false
 
 # 请求路径
-VITE_BASE_URL='http://10.0.5.228:8001'
+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=

+ 2 - 2
.env.test

@@ -4,10 +4,10 @@ NODE_ENV=production
 VITE_DEV=false
 
 # 请求路径
-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'
 
 # 接口前缀
 VITE_API_BASEPATH=

+ 1 - 1
src/App.vue

@@ -22,7 +22,7 @@ const setDefaultTheme = () => {
   }
   appStore.setIsDark(isDarkTheme)
 }
-setDefaultTheme()
+// setDefaultTheme()
 </script>
 <template>
   <ConfigGlobal :size="currentSize">

BIN
src/assets/imgs/zxlogo.png


+ 1 - 0
src/assets/svgs/folderShare.svg

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

+ 1 - 0
src/assets/svgs/olderOpen.svg

@@ -0,0 +1 @@
+<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>

+ 13 - 0
src/assets/svgs/wenjianjia_shouqi.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>

+ 17 - 0
src/assets/svgs/wenjianjia_zhankai.svg

@@ -0,0 +1,17 @@
+<?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 852.8 698.6" style="enable-background:new 0 0 852.8 698.6;" xml:space="preserve">
+<style type="text/css">
+	.st0{fill:#555C6C;}
+</style>
+<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"/>
+</svg>

+ 1 - 1
src/components/DiyEditor/components/mobile/UserCard/index.vue

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

+ 6 - 5
src/components/UploadFile/index.ts

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

+ 268 - 0
src/components/UploadFile/src/SPuUploadImg.vue

@@ -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>
+					<div v-if="showDelete && !disabled" class="handle-icon" @click="deleteImg">
+						<Icon icon="ep:delete" />
+						<span v-if="showBtnText">{{ t('action.del') }}</span>
+					</div>
+				</div>
+			</template>
+			<template v-else>
+				<div class="upload-empty">
+					<slot name="empty">
+						<Icon icon="ep:plus" />
+						<!-- <span>请上传图片</span> -->
+					</slot>
+				</div>
+			</template>
+		</el-upload>
+		<div class="el-upload__tip">
+			<slot name="tip"></slot>
+		</div>
+	</div>
+</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;
+
+			&:hover {
+				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);
+
+				&:hover {
+					border-color: var(--el-color-primary);
+
+					.upload-handle {
+						opacity: 1;
+					}
+				}
+
+				.el-upload-dragger {
+					display: flex;
+					align-items: center;
+					justify-content: center;
+					width: 100%;
+					height: 100%;
+					padding: 0;
+					overflow: hidden;
+					background-color: transparent;
+					border: none;
+					border-radius: 0;
+
+					&:hover {
+						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 {
+					width: 100%;
+					height: 100%;
+					object-fit: contain;
+				}
+
+				.upload-empty {
+					position: relative;
+					display: flex;
+					flex-direction: column;
+					align-items: center;
+					justify-content: center;
+					font-size: 12px;
+					line-height: 30px;
+					color: var(--el-color-info);
+
+					.el-icon {
+						font-size: 28px;
+						color: var(--el-text-color-secondary);
+					}
+				}
+
+				.upload-handle {
+					position: absolute;
+					top: 0;
+					right: 0;
+					display: flex;
+					width: 100%;
+					height: 100%;
+					cursor: pointer;
+					background: rgb(0 0 0 / 60%);
+					opacity: 0;
+					box-sizing: border-box;
+					transition: var(--el-transition-duration-fast);
+					align-items: center;
+					justify-content: center;
+
+					.handle-icon {
+						display: flex;
+						flex-direction: column;
+						align-items: center;
+						justify-content: center;
+						padding: 0 6%;
+						color: aliceblue;
+
+						.el-icon {
+							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;
+		}
+	}
+</style>

+ 1 - 1
src/components/bpmnProcessDesigner/package/designer/ProcessDesigner.vue

@@ -664,7 +664,7 @@ const previewProcessJson = () => {
     previewModelVisible.value = true
   })
 }
-/* ------------------------------------------------ 芋道源码 methods ------------------------------------------------------ */
+/* ------------------------------------------------ 非繁源码 methods ------------------------------------------------------ */
 const processSave = async () => {
   console.log(bpmnModeler, 'bpmnModelerbpmnModelerbpmnModelerbpmnModeler')
   const { err, xml } = await bpmnModeler.saveXML()

+ 71 - 72
src/layout/components/AppView.vue

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

+ 22 - 24
src/layout/components/Footer/src/Footer.vue

@@ -1,24 +1,22 @@
-<script lang="ts" setup>
-import { useAppStore } from '@/store/modules/app'
-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 appStore = useAppStore()
-
-const title = computed(() => appStore.getTitle)
-</script>
-
-<template>
-  <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>
-</template>
+<script lang="ts" setup>
+	import { useAppStore } from '@/store/modules/app'
+	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 appStore = useAppStore()
+
+	const title = computed(() => appStore.getTitle)
+</script>
+
+<template>
+	<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>
+	</div>
+</template>

+ 29 - 32
src/layout/components/Screenfull/src/Screenfull.vue

@@ -1,32 +1,29 @@
-<script lang="ts" setup>
-import { Icon } from '@/components/Icon'
-import { useFullscreen } from '@vueuse/core'
-import { propTypes } from '@/utils/propTypes'
-import { useDesign } from '@/hooks/web/useDesign'
-
-defineOptions({ name: 'ScreenFull' })
-
-const { getPrefixCls } = useDesign()
-
-const prefixCls = getPrefixCls('screenfull')
-
-defineProps({
-  color: propTypes.string.def('')
-})
-
-const { toggle, isFullscreen } = useFullscreen()
-
-const toggleFullscreen = () => {
-  toggle()
-}
-</script>
-
-<template>
-  <div :class="prefixCls" @click="toggleFullscreen">
-    <Icon
-      :color="color"
-      :icon="isFullscreen ? 'zmdi:fullscreen-exit' : 'zmdi:fullscreen'"
-      :size="18"
-    />
-  </div>
-</template>
+<script lang="ts" setup>
+	import { Icon } from '@/components/Icon'
+	import { useFullscreen } from '@vueuse/core'
+	import { propTypes } from '@/utils/propTypes'
+	import { useDesign } from '@/hooks/web/useDesign'
+
+	defineOptions({ name: 'ScreenFull' })
+
+	const { getPrefixCls } = useDesign()
+
+	const prefixCls = getPrefixCls('screenfull')
+
+	defineProps({
+		color: propTypes.string.def('')
+	})
+
+	const { toggle, isFullscreen } = useFullscreen()
+
+	const toggleFullscreen = () => {
+		toggle()
+	}
+</script>
+
+<template>
+	<div :class="prefixCls" @click="toggleFullscreen">
+
+		<Icon :color="color" :icon="isFullscreen ? 'zmdi:fullscreen-exit' : 'zmdi:fullscreen'" :size="18" />
+	</div>
+</template>

+ 287 - 299
src/layout/components/Setting/src/Setting.vue

@@ -1,299 +1,287 @@
-<script lang="ts" setup>
-import { ElMessage } from 'element-plus'
-import { useClipboard, useCssVar } from '@vueuse/core'
-
-import { CACHE_KEY, useCache } from '@/hooks/web/useCache'
-import { useDesign } from '@/hooks/web/useDesign'
-
-import { setCssVar, trim } from '@/utils'
-import { colorIsDark, hexToRGB, lighten } from '@/utils/color'
-import { useAppStore } from '@/store/modules/app'
-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 appStore = useAppStore()
-
-const { getPrefixCls } = useDesign()
-const prefixCls = getPrefixCls('setting')
-const layout = computed(() => appStore.getLayout)
-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 isDarkColor = colorIsDark(color)
-  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
-      ? 'var(--el-color-primary)'
-      : hexToRGB(unref(primaryColor), 0.1),
-    // 左侧菜单字体颜色
-    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) {
-      headerTheme.value = '#fff'
-      setHeaderTheme('#fff')
-    } 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}',
-        // logo字体颜色
-        logoTitleTextColor: '${appStore.getTheme.logoTitleTextColor}',
-        // logo边框颜色
-        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'))
-  } else {
-    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()
-}
-</script>
-
-<template>
-  <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>
-    </template>
-
-    <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',
-          '#009688',
-          '#383f45'
-        ]"
-        @change="setHeaderTheme"
-      />
-
-      <!-- 菜单主题 -->
-      <template v-if="layout !== 'top'">
-        <ElDivider>{{ t('setting.menuTheme') }}</ElDivider>
-        <ColorRadioPicker
-          v-model="menuTheme"
-          :schema="[
-            '#fff',
-            '#001529',
-            '#212121',
-            '#273352',
-            '#191b24',
-            '#383f45',
-            '#001628',
-            '#344058'
-          ]"
-          @change="setMenuTheme"
-        />
-      </template>
-    </div>
-
-    <!-- 界面显示 -->
-    <ElDivider>{{ t('setting.interfaceDisplay') }}</ElDivider>
-    <InterfaceDisplay />
-
-    <ElDivider />
-    <div>
-      <ElButton class="w-full" type="primary" @click="copyConfig">{{ t('setting.copy') }}</ElButton>
-    </div>
-    <div class="mt-5px">
-      <ElButton class="w-full" type="danger" @click="clear">
-        {{ t('setting.clearAndReset') }}
-      </ElButton>
-    </div>
-  </ElDrawer>
-</template>
-
-<style lang="scss" scoped>
-$prefix-cls: #{$namespace}-setting;
-
-.#{$prefix-cls} {
-  border-radius: 6px 0 0 6px;
-}
-</style>
+<script lang="ts" setup>
+	import { ElMessage } from 'element-plus'
+	import { useClipboard, useCssVar } from '@vueuse/core'
+
+	import { CACHE_KEY, useCache } from '@/hooks/web/useCache'
+	import { useDesign } from '@/hooks/web/useDesign'
+
+	import { setCssVar, trim } from '@/utils'
+	import { colorIsDark, hexToRGB, lighten } from '@/utils/color'
+	import { useAppStore } from '@/store/modules/app'
+	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 appStore = useAppStore()
+
+	const { getPrefixCls } = useDesign()
+	const prefixCls = getPrefixCls('setting')
+	const layout = computed(() => appStore.getLayout)
+	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 isDarkColor = colorIsDark(color)
+		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
+				? 'var(--el-color-primary)'
+				: hexToRGB(unref(primaryColor), 0.1),
+			// 左侧菜单字体颜色
+			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) {
+				headerTheme.value = '#fff'
+				setHeaderTheme('#fff')
+			} 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}',
+        // logo字体颜色
+        logoTitleTextColor: '${appStore.getTheme.logoTitleTextColor}',
+        // logo边框颜色
+        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'))
+		} else {
+			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()
+	}
+</script>
+
+<template>
+	<!-- <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>
+		</template>
+
+		<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',
+          '#009688',
+          '#383f45'
+        ]" @change="setHeaderTheme" />
+
+			<!-- 菜单主题 -->
+			<template v-if="layout !== 'top'">
+				<ElDivider>{{ t('setting.menuTheme') }}</ElDivider>
+				<ColorRadioPicker v-model="menuTheme" :schema="[
+            '#fff',
+            '#001529',
+            '#212121',
+            '#273352',
+            '#191b24',
+            '#383f45',
+            '#001628',
+            '#344058'
+          ]" @change="setMenuTheme" />
+			</template>
+		</div>
+
+		<!-- 界面显示 -->
+		<ElDivider>{{ t('setting.interfaceDisplay') }}</ElDivider>
+		<InterfaceDisplay />
+
+		<ElDivider />
+		<div>
+			<ElButton class="w-full" type="primary" @click="copyConfig">{{ t('setting.copy') }}</ElButton>
+		</div>
+		<div class="mt-5px">
+			<ElButton class="w-full" type="danger" @click="clear">
+				{{ t('setting.clearAndReset') }}
+			</ElButton>
+		</div>
+	</ElDrawer>
+</template>
+
+<style lang="scss" scoped>
+	$prefix-cls: #{$namespace}-setting;
+
+	.#{$prefix-cls} {
+		border-radius: 6px 0 0 6px;
+	}
+</style>

+ 224 - 224
src/layout/components/Setting/src/components/InterfaceDisplay.vue

@@ -1,224 +1,224 @@
-<script lang="ts" setup>
-import { setCssVar } from '@/utils'
-
-import { useDesign } from '@/hooks/web/useDesign'
-import { useWatermark } from '@/hooks/web/useWatermark'
-import { useAppStore } from '@/store/modules/app'
-
-defineOptions({ name: 'InterfaceDisplay' })
-
-const { t } = useI18n()
-const { getPrefixCls } = useDesign()
-const { setWatermark } = useWatermark()
-const prefixCls = getPrefixCls('interface-display')
-const appStore = useAppStore()
-
-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)
-}
-
-const layout = computed(() => appStore.getLayout)
-
-watch(
-  () => layout.value,
-  (n) => {
-    if (n === 'top') {
-      appStore.setCollapse(false)
-    }
-  }
-)
-</script>
-
-<template>
-  <div :class="prefixCls">
-    <div class="flex items-center justify-between">
-      <span class="text-14px">{{ t('setting.breadcrumb') }}</span>
-      <ElSwitch v-model="breadcrumb" @change="breadcrumbChange" />
-    </div>
-
-    <div class="flex items-center justify-between">
-      <span class="text-14px">{{ t('setting.breadcrumbIcon') }}</span>
-      <ElSwitch v-model="breadcrumbIcon" @change="breadcrumbIconChange" />
-    </div>
-
-    <div class="flex items-center justify-between">
-      <span class="text-14px">{{ t('setting.hamburgerIcon') }}</span>
-      <ElSwitch v-model="hamburger" @change="hamburgerChange" />
-    </div>
-
-    <div class="flex items-center justify-between">
-      <span class="text-14px">{{ t('setting.screenfullIcon') }}</span>
-      <ElSwitch v-model="screenfull" @change="screenfullChange" />
-    </div>
-
-    <div class="flex items-center justify-between">
-      <span class="text-14px">{{ t('setting.sizeIcon') }}</span>
-      <ElSwitch v-model="size" @change="sizeChange" />
-    </div>
-
-    <div class="flex items-center justify-between">
-      <span class="text-14px">{{ t('setting.localeIcon') }}</span>
-      <ElSwitch v-model="locale" @change="localeChange" />
-    </div>
-
-    <div class="flex items-center justify-between">
-      <span class="text-14px">{{ t('setting.messageIcon') }}</span>
-      <ElSwitch v-model="message" @change="messageChange" />
-    </div>
-
-    <div class="flex items-center justify-between">
-      <span class="text-14px">{{ t('setting.tagsView') }}</span>
-      <ElSwitch v-model="tagsView" @change="tagsViewChange" />
-    </div>
-
-    <div class="flex items-center justify-between">
-      <span class="text-14px">{{ t('setting.tagsViewIcon') }}</span>
-      <ElSwitch v-model="tagsViewIcon" @change="tagsViewIconChange" />
-    </div>
-
-    <div class="flex items-center justify-between">
-      <span class="text-14px">{{ t('setting.logo') }}</span>
-      <ElSwitch v-model="logo" @change="logoChange" />
-    </div>
-
-    <div class="flex items-center justify-between">
-      <span class="text-14px">{{ t('setting.uniqueOpened') }}</span>
-      <ElSwitch v-model="uniqueOpened" @change="uniqueOpenedChange" />
-    </div>
-
-    <div class="flex items-center justify-between">
-      <span class="text-14px">{{ t('setting.fixedHeader') }}</span>
-      <ElSwitch v-model="fixedHeader" @change="fixedHeaderChange" />
-    </div>
-
-    <div class="flex items-center justify-between">
-      <span class="text-14px">{{ t('setting.footer') }}</span>
-      <ElSwitch v-model="footer" @change="footerChange" />
-    </div>
-
-    <div class="flex items-center justify-between">
-      <span class="text-14px">{{ t('setting.greyMode') }}</span>
-      <ElSwitch v-model="greyMode" @change="greyModeChange" />
-    </div>
-
-    <div class="flex items-center justify-between">
-      <span class="text-14px">{{ t('setting.fixedMenu') }}</span>
-      <ElSwitch v-model="fixedMenu" @change="fixedMenuChange" />
-    </div>
-
-    <div class="flex items-center justify-between">
-      <span class="text-14px">{{ t('watermark.watermark') }}</span>
-      <ElInput v-model="water" class="right-1 w-20" @change="setWater()" />
-    </div>
-  </div>
-</template>
+<script lang="ts" setup>
+	import { setCssVar } from '@/utils'
+
+	import { useDesign } from '@/hooks/web/useDesign'
+	import { useWatermark } from '@/hooks/web/useWatermark'
+	import { useAppStore } from '@/store/modules/app'
+
+	defineOptions({ name: 'InterfaceDisplay' })
+
+	const { t } = useI18n()
+	const { getPrefixCls } = useDesign()
+	const { setWatermark } = useWatermark()
+	const prefixCls = getPrefixCls('interface-display')
+	const appStore = useAppStore()
+
+	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)
+	}
+
+	const layout = computed(() => appStore.getLayout)
+
+	watch(
+		() => layout.value,
+		(n) => {
+			if (n === 'top') {
+				appStore.setCollapse(false)
+			}
+		}
+	)
+</script>
+
+<template>
+	<div :class="prefixCls">
+		<div class="flex items-center justify-between">
+			<span class="text-14px">{{ t('setting.breadcrumb') }}</span>
+			<ElSwitch v-model="breadcrumb" @change="breadcrumbChange" />
+		</div>
+
+		<div class="flex items-center justify-between">
+			<span class="text-14px">{{ t('setting.breadcrumbIcon') }}</span>
+			<ElSwitch v-model="breadcrumbIcon" @change="breadcrumbIconChange" />
+		</div>
+
+		<div class="flex items-center justify-between">
+			<span class="text-14px">{{ t('setting.hamburgerIcon') }}</span>
+			<ElSwitch v-model="hamburger" @change="hamburgerChange" />
+		</div>
+
+		<div class="flex items-center justify-between">
+			<span class="text-14px">{{ t('setting.screenfullIcon') }}</span>
+			<ElSwitch v-model="screenfull" @change="screenfullChange" />
+		</div>
+
+		<div class="flex items-center justify-between">
+			<span class="text-14px">{{ t('setting.sizeIcon') }}</span>
+			<ElSwitch v-model="size" @change="sizeChange" />
+		</div>
+
+		<div class="flex items-center justify-between">
+			<span class="text-14px">{{ t('setting.localeIcon') }}</span>
+			<ElSwitch v-model="locale" @change="localeChange" />
+		</div>
+
+		<div class="flex items-center justify-between">
+			<span class="text-14px">{{ t('setting.messageIcon') }}</span>
+			<ElSwitch v-model="message" @change="messageChange" />
+		</div>
+
+		<div class="flex items-center justify-between">
+			<span class="text-14px">{{ t('setting.tagsView') }}</span>
+			<ElSwitch v-model="tagsView" @change="tagsViewChange" />
+		</div>
+
+		<div class="flex items-center justify-between">
+			<span class="text-14px">{{ t('setting.tagsViewIcon') }}</span>
+			<ElSwitch v-model="tagsViewIcon" @change="tagsViewIconChange" />
+		</div>
+
+		<div class="flex items-center justify-between">
+			<span class="text-14px">{{ t('setting.logo') }}</span>
+			<ElSwitch v-model="logo" @change="logoChange" />
+		</div>
+
+		<div class="flex items-center justify-between">
+			<span class="text-14px">{{ t('setting.uniqueOpened') }}</span>
+			<ElSwitch v-model="uniqueOpened" @change="uniqueOpenedChange" />
+		</div>
+
+		<div class="flex items-center justify-between">
+			<span class="text-14px">{{ t('setting.fixedHeader') }}</span>
+			<ElSwitch v-model="fixedHeader" @change="fixedHeaderChange" />
+		</div>
+
+		<div class="flex items-center justify-between">
+			<span class="text-14px">{{ t('setting.footer') }}</span>
+			<ElSwitch v-model="footer" @change="footerChange" />
+		</div>
+
+		<div class="flex items-center justify-between">
+			<span class="text-14px">{{ t('setting.greyMode') }}</span>
+			<ElSwitch v-model="greyMode" @change="greyModeChange" />
+		</div>
+
+		<div class="flex items-center justify-between">
+			<span class="text-14px">{{ t('setting.fixedMenu') }}</span>
+			<ElSwitch v-model="fixedMenu" @change="fixedMenuChange" />
+		</div>
+
+		<div class="flex items-center justify-between">
+			<span class="text-14px">{{ t('watermark.watermark') }}</span>
+			<ElInput v-model="water" class="right-1 w-20" @change="setWater()" />
+		</div>
+	</div>
+</template>

+ 2 - 2
src/layout/components/UserInfo/src/UserInfo.vue

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

+ 442 - 442
src/locales/zh-CN.ts

@@ -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: '消息图标',
-    tagsView: '标签页',
-    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: '验证码',
-    login: '登录',
-    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: {
-    login: '登录',
-    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: '数字输入框',
-    default: '默认',
-    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: '表格',
-    index: '序号',
-    title: '标题',
-    author: '作者',
-    createTime: '创建时间',
-    action: '操作',
-    pagination: '分页',
-    reserveIndex: '叠加序号',
-    restoreIndex: '还原序号',
-    showSelections: '显示多选',
-    hiddenSelections: '隐藏多选',
-    showExpandedRows: '显示展开行',
-    hiddenExpandedRows: '隐藏展开行',
-    header: '头部'
-  },
-  action: {
-    create: '新增',
-    add: '新增',
-    del: '删除',
-    delete: '删除',
-    edit: '编辑',
-    update: '编辑',
-    preview: '预览',
-    more: '更多',
-    sync: '同步',
-    save: '保存',
-    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: '请输入锁屏密码或者用户密码'
-    },
-    login: {
-      backSignIn: '返回',
-      signInFormTitle: '登录',
-      ssoFormTitle: '三方授权',
-      mobileSignInFormTitle: '手机登录',
-      qrSignInFormTitle: '二维码登录',
-      signUpFormTitle: '注册',
-      forgetFormTitle: '重置密码',
-      signInTitle: '开箱即用的中后台管理系统',
-      signInDesc: '输入您的个人详细信息开始使用!',
-      policy: '我同意xxx隐私政策',
-      scanSign: `扫码后点击"确认",即可完成登录`,
-      loginButton: '登录',
-      registerButton: '注册',
-      rememberMe: '记住我',
-      forgetPassword: '忘记密码?',
-      otherSignIn: '其他登录方式',
-      // notify
-      loginSuccessTitle: '登录成功',
-      loginSuccessDesc: '欢迎回来',
-      // placeholder
-      accountPlaceholder: '请输入账号',
-      passwordPlaceholder: '请输入密码',
-      smsPlaceholder: '请输入验证码',
-      mobilePlaceholder: '请输入手机号码',
-      policyPlaceholder: '勾选后才能注册',
-      diffPwd: '两次输入密码不一致',
-      userName: '账号',
-      password: '密码',
-      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: '新密码',
-      confirmPassword: '确认密码',
-      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: '消息图标',
+		tagsView: '标签页',
+		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: '验证码',
+		login: '登录',
+		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: {
+		login: '登录',
+		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: '数字输入框',
+		default: '默认',
+		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: '表格',
+		index: '序号',
+		title: '标题',
+		author: '作者',
+		createTime: '创建时间',
+		action: '操作',
+		pagination: '分页',
+		reserveIndex: '叠加序号',
+		restoreIndex: '还原序号',
+		showSelections: '显示多选',
+		hiddenSelections: '隐藏多选',
+		showExpandedRows: '显示展开行',
+		hiddenExpandedRows: '隐藏展开行',
+		header: '头部'
+	},
+	action: {
+		create: '新增',
+		add: '新增',
+		del: '删除',
+		delete: '删除',
+		edit: '编辑',
+		update: '编辑',
+		preview: '预览',
+		more: '更多',
+		sync: '同步',
+		save: '保存',
+		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: '请输入锁屏密码或者用户密码'
+		},
+		login: {
+			backSignIn: '返回',
+			signInFormTitle: '登录',
+			ssoFormTitle: '三方授权',
+			mobileSignInFormTitle: '手机登录',
+			qrSignInFormTitle: '二维码登录',
+			signUpFormTitle: '注册',
+			forgetFormTitle: '重置密码',
+			signInTitle: '开箱即用的中后台管理系统',
+			signInDesc: '输入您的个人详细信息开始使用!',
+			policy: '我同意xxx隐私政策',
+			scanSign: `扫码后点击"确认",即可完成登录`,
+			loginButton: '登录',
+			registerButton: '注册',
+			rememberMe: '记住我',
+			forgetPassword: '忘记密码?',
+			otherSignIn: '其他登录方式',
+			// notify
+			loginSuccessTitle: '登录成功',
+			loginSuccessDesc: '欢迎回来',
+			// placeholder
+			accountPlaceholder: '请输入账号',
+			passwordPlaceholder: '请输入密码',
+			smsPlaceholder: '请输入验证码',
+			mobilePlaceholder: '请输入手机号码',
+			policyPlaceholder: '勾选后才能注册',
+			diffPwd: '两次输入密码不一致',
+			userName: '账号',
+			password: '密码',
+			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: '新密码',
+			confirmPassword: '确认密码',
+			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 报错
+}

+ 13 - 13
src/router/modules/remaining.ts

@@ -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'),

+ 277 - 276
src/store/modules/app.ts

@@ -1,276 +1,277 @@
-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: 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',
-        // logo字体颜色
-        logoTitleTextColor: '#fff',
-        // logo边框颜色
-        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')
-      } else {
-        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',
+				// logo字体颜色
+				logoTitleTextColor: '#fff',
+				// logo边框颜色
+				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')
+			} else {
+				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)
+}

+ 66 - 66
src/styles/var.css

@@ -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;
-  /* header start */
-
-  /* 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;
+	/* header start */
+
+	/* 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 - 1
src/utils/constants.ts

@@ -1,5 +1,5 @@
 /**
- * Created by 芋道源码
+ * Created by 非繁源码
  *
  * 枚举类
  */

+ 363 - 384
src/views/Home/Index.vue

@@ -1,384 +1,363 @@
-<template>
-  <div>
-    <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>
-                <div class="text-20px">
-                  {{ t('workplace.welcome') }} {{ username }} {{ t('workplace.happyDay') }}
-                </div>
-                <div class="mt-10px text-14px text-gray-500">
-                  {{ t('workplace.toady') }},20℃ - 32℃!
-                </div>
-              </div>
-            </div>
-          </el-col>
-          <el-col :xl="12" :lg="12" :md="12" :sm="24" :xs="24">
-            <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"
-                />
-              </div>
-              <el-divider direction="vertical" />
-              <div class="px-8px text-right">
-                <div class="mb-16px text-14px text-gray-400">{{ t('workplace.toDo') }}</div>
-                <CountTo
-                  class="text-20px"
-                  :start-val="0"
-                  :end-val="totalSate.todo"
-                  :duration="2600"
-                />
-              </div>
-              <el-divider direction="vertical" border-style="dashed" />
-              <div class="px-8px text-right">
-                <div class="mb-16px text-14px text-gray-400">{{ t('workplace.access') }}</div>
-                <CountTo
-                  class="text-20px"
-                  :start-val="0"
-                  :end-val="totalSate.access"
-                  :duration="2600"
-                />
-              </div>
-            </div>
-          </el-col>
-        </el-row>
-      </el-skeleton>
-    </el-card>
-  </div>
-
-  <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">
-      <el-card shadow="never">
-        <template #header>
-          <div class="h-3 flex justify-between">
-            <span>{{ t('workplace.project') }}</span>
-            <el-link type="primary" :underline="false">{{ t('action.more') }}</el-link>
-          </div>
-        </template>
-        <el-skeleton :loading="loading" animated>
-          <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">
-                <div class="flex items-center">
-                  <Icon :icon="item.icon" :size="25" class="mr-8px" />
-                  <span class="text-16px">{{ item.name }}</span>
-                </div>
-                <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>
-                </div>
-              </el-card>
-            </el-col>
-          </el-row>
-        </el-skeleton>
-      </el-card>
-
-      <el-card shadow="never" class="mt-8px">
-        <el-skeleton :loading="loading" animated>
-          <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">
-                <el-skeleton :loading="loading" animated>
-                  <Echart :options="pieOptionsData" :height="280" />
-                </el-skeleton>
-              </el-card>
-            </el-col>
-            <el-col :xl="14" :lg="14" :md="24" :sm="24" :xs="24">
-              <el-card shadow="hover" class="mb-8px">
-                <el-skeleton :loading="loading" animated>
-                  <Echart :options="barOptionsData" :height="280" />
-                </el-skeleton>
-              </el-card>
-            </el-col>
-          </el-row>
-        </el-skeleton>
-      </el-card>
-    </el-col>
-    <el-col :xl="8" :lg="8" :md="24" :sm="24" :xs="24" class="mb-8px">
-      <el-card shadow="never">
-        <template #header>
-          <div class="h-3 flex justify-between">
-            <span>{{ t('workplace.shortcutOperation') }}</span>
-          </div>
-        </template>
-        <el-skeleton :loading="loading" animated>
-          <el-row>
-            <el-col v-for="item in shortcut" :key="`team-${item.name}`" :span="8" class="mb-8px">
-              <div class="flex items-center">
-                <Icon :icon="item.icon" class="mr-8px" />
-                <el-link type="default" :underline="false" @click="setWatermark(item.name)">
-                  {{ item.name }}
-                </el-link>
-              </div>
-            </el-col>
-          </el-row>
-        </el-skeleton>
-      </el-card>
-      <el-card shadow="never" class="mt-8px">
-        <template #header>
-          <div class="h-3 flex justify-between">
-            <span>{{ t('workplace.notice') }}</span>
-            <el-link type="primary" :underline="false">{{ t('action.more') }}</el-link>
-          </div>
-        </template>
-        <el-skeleton :loading="loading" animated>
-          <div v-for="(item, index) in notice" :key="`dynamics-${index}`">
-            <div class="flex items-center">
-              <el-avatar :src="avatar" :size="35" class="mr-16px">
-                <img src="@/assets/imgs/avatar.gif" alt="" />
-              </el-avatar>
-              <div>
-                <div class="text-14px">
-                  <Highlight :keys="item.keys.map((v) => t(v))">
-                    {{ item.type }} : {{ item.title }}
-                  </Highlight>
-                </div>
-                <div class="mt-16px text-12px text-gray-400">
-                  {{ formatTime(item.date, 'yyyy-MM-dd') }}
-                </div>
-              </div>
-            </div>
-            <el-divider />
-          </div>
-        </el-skeleton>
-      </el-card>
-    </el-col>
-  </el-row>
-</template>
-<script lang="ts" setup>
-import { set } from 'lodash-es'
-import { EChartsOption } from 'echarts'
-import { formatTime } from '@/utils'
-
-import { useUserStore } from '@/store/modules/user'
-import { useWatermark } from '@/hooks/web/useWatermark'
-import type { WorkplaceTotal, Project, Notice, Shortcut } from './types'
-import { pieOptions, barOptions } from './echarts-data'
-
-defineOptions({ name: 'Home' })
-
-const { t } = useI18n()
-const userStore = useUserStore()
-const { setWatermark } = useWatermark()
-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',
-      message: 'workplace.introduction',
-      personal: 'Archer',
-      time: new Date()
-    },
-    {
-      name: 'Angular',
-      icon: 'logos:angular-icon',
-      message: 'workplace.introduction',
-      personal: 'Archer',
-      time: new Date()
-    },
-    {
-      name: 'React',
-      icon: 'logos:react',
-      message: 'workplace.introduction',
-      personal: 'Archer',
-      time: new Date()
-    },
-    {
-      name: 'Webpack',
-      icon: 'logos:webpack',
-      message: 'workplace.introduction',
-      personal: 'Archer',
-      time: new Date()
-    },
-    {
-      name: 'Vite',
-      icon: 'vscode-icons:file-type-vite',
-      message: 'workplace.introduction',
-      personal: 'Archer',
-      time: new Date()
-    }
-  ]
-  projects = Object.assign(projects, data)
-}
-
-// 获取通知公告
-let notice = reactive<Notice[]>([])
-const getNotice = async () => {
-  const data = [
-    {
-      title: '系统升级版本',
-      type: '通知',
-      keys: ['通知', '升级'],
-      date: new Date()
-    },
-    {
-      title: '系统凌晨维护',
-      type: '公告',
-      keys: ['公告', '维护'],
-      date: new Date()
-    },
-    {
-      title: '系统升级版本',
-      type: '通知',
-      keys: ['通知', '升级'],
-      date: new Date()
-    },
-    {
-      title: '系统凌晨维护',
-      type: '公告',
-      keys: ['公告', '维护'],
-      date: new Date()
-    }
-  ]
-  notice = Object.assign(notice, data)
-}
-
-// 获取快捷入口
-let shortcut = reactive<Shortcut[]>([])
-
-const getShortcut = async () => {
-  const data = [
-    {
-      name: 'Github',
-      icon: 'akar-icons:github-fill',
-      url: 'github.io'
-    },
-    {
-      name: 'Vue',
-      icon: 'logos:vue',
-      url: 'vuejs.org'
-    },
-    {
-      name: 'Vite',
-      icon: 'vscode-icons:file-type-vite',
-      url: 'https://vitejs.dev/'
-    },
-    {
-      name: 'Angular',
-      icon: 'logos:angular-icon',
-      url: 'github.io'
-    },
-    {
-      name: 'React',
-      icon: 'logos:react',
-      url: 'github.io'
-    },
-    {
-      name: 'Webpack',
-      icon: 'logos:webpack',
-      url: 'github.io'
-    }
-  ]
-  shortcut = Object.assign(shortcut, data)
-}
-
-// 用户来源
-const getUserAccessSource = async () => {
-  const data = [
-    { 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) => {
-    return {
-      name: t(v.name),
-      value: v.value
-    }
-  })
-}
-const barOptionsData = reactive<EChartsOption>(barOptions) as EChartsOption
-
-// 周活跃量
-const getWeeklyUserActivity = async () => {
-  const data = [
-    { 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' }
-  ]
-  set(
-    barOptionsData,
-    'xAxis.data',
-    data.map((v) => t(v.name))
-  )
-  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()
-</script>
+<template>
+	<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">
+				<template #header>
+					<div class="h-3 flex justify-between">
+						<span>待办</span>
+						<!-- <el-link type="primary" :underline="false">{{ t('action.more') }}</el-link> -->
+					</div>
+				</template>
+				<el-skeleton :loading="loading" animated>
+					<ToBeDone />
+				</el-skeleton>
+			</el-card>
+		</el-col>
+
+		<el-col :xl="12" :lg="12" :md="24" :sm="24" :xs="24" class="mb-8px">
+			<el-card shadow="never">
+				<template #header>
+					<div class="h-3 flex justify-between">
+						<span>今日订单【统计】</span>
+					</div>
+				</template>
+				<el-skeleton :loading="loading" animated>
+					<el-row>
+						<el-col>
+							<el-card shadow="hover" class="mb-8px">
+								<el-skeleton :loading="loading" animated>
+									<Echart :options="todayOrderOptionsData" :height="210" />
+								</el-skeleton>
+							</el-card>
+						</el-col>
+					</el-row>
+				</el-skeleton>
+			</el-card>
+		</el-col>
+	</el-row>
+	<el-row :gutter="8" justify="space-between">
+		<el-col :xl="8" :lg="8" :md="24" :sm="24" :xs="24" class="mb-8px">
+			<el-card shadow="never">
+				<template #header>
+					<div class="h-3 flex justify-between">
+						<span>快捷方式</span>
+					</div>
+				</template>
+				<el-skeleton :loading="loading" animated>
+					<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!" />
+							</div>
+							<span>新增商品</span>
+						</div>
+					</div>
+
+
+				</el-skeleton>
+			</el-card>
+		</el-col>
+		<el-col :xl="8" :lg="8" :md="24" :sm="24" :xs="24" class="mb-8px">
+			<el-card shadow="never">
+				<template #header>
+					<div class="h-3 flex justify-between">
+						<span>用户访问来源</span>
+					</div>
+				</template>
+				<el-skeleton :loading="loading" animated>
+					<el-row>
+						<el-col>
+							<el-card shadow="hover" class="mb-8px">
+								<el-skeleton :loading="loading" animated>
+									<Echart :options="pieOptionsData" :height="280" />
+								</el-skeleton>
+							</el-card>
+						</el-col>
+					</el-row>
+				</el-skeleton>
+			</el-card>
+		</el-col>
+		<el-col :xl="8" :lg="8" :md="24" :sm="24" :xs="24" class="mb-8px">
+			<el-card shadow="never">
+				<template #header>
+					<div class="h-3 flex justify-between">
+						<span>每周用户活跃量</span>
+					</div>
+				</template>
+				<el-skeleton :loading="loading" animated>
+					<el-row>
+						<el-col>
+							<el-card shadow="hover" class="mb-8px">
+								<el-skeleton :loading="loading" animated>
+									<Echart :options="barOptionsData" :height="280" />
+								</el-skeleton>
+							</el-card>
+						</el-col>
+					</el-row>
+				</el-skeleton>
+			</el-card>
+		</el-col>
+	</el-row>
+	<!-- 商品列表表单弹窗:添加/修改 -->
+	<SpuIndex ref="spuFormRef" @success="getList" />
+</template>
+<script lang="ts" setup>
+	import { set } from 'lodash-es'
+	import { EChartsOption } from 'echarts'
+	import { formatTime } from '@/utils'
+
+	import { useUserStore } from '@/store/modules/user'
+	import { useWatermark } from '@/hooks/web/useWatermark'
+	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 { t } = useI18n()
+	const userStore = useUserStore()
+	const { setWatermark } = useWatermark()
+	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',
+				message: 'workplace.introduction',
+				personal: 'Archer',
+				time: new Date()
+			},
+			{
+				name: 'Angular',
+				icon: 'logos:angular-icon',
+				message: 'workplace.introduction',
+				personal: 'Archer',
+				time: new Date()
+			},
+			{
+				name: 'React',
+				icon: 'logos:react',
+				message: 'workplace.introduction',
+				personal: 'Archer',
+				time: new Date()
+			},
+			{
+				name: 'Webpack',
+				icon: 'logos:webpack',
+				message: 'workplace.introduction',
+				personal: 'Archer',
+				time: new Date()
+			},
+			{
+				name: 'Vite',
+				icon: 'vscode-icons:file-type-vite',
+				message: 'workplace.introduction',
+				personal: 'Archer',
+				time: new Date()
+			}
+		]
+		projects = Object.assign(projects, data)
+	}
+
+	// 获取通知公告
+	let notice = reactive<Notice[]>([])
+	const getNotice = async () => {
+		const data = [
+			{
+				title: '系统升级版本',
+				type: '通知',
+				keys: ['通知', '升级'],
+				date: new Date()
+			},
+			{
+				title: '系统凌晨维护',
+				type: '公告',
+				keys: ['公告', '维护'],
+				date: new Date()
+			},
+			{
+				title: '系统升级版本',
+				type: '通知',
+				keys: ['通知', '升级'],
+				date: new Date()
+			},
+			{
+				title: '系统凌晨维护',
+				type: '公告',
+				keys: ['公告', '维护'],
+				date: new Date()
+			}
+		]
+		notice = Object.assign(notice, data)
+	}
+
+	// 获取快捷入口
+	let shortcut = reactive<Shortcut[]>([])
+	const getShortcut = async () => {
+		const data = [
+			{
+				name: 'Github',
+				icon: 'akar-icons:github-fill',
+				url: 'github.io'
+			},
+			{
+				name: 'Vue',
+				icon: 'logos:vue',
+				url: 'vuejs.org'
+			},
+			{
+				name: 'Vite',
+				icon: 'vscode-icons:file-type-vite',
+				url: 'https://vitejs.dev/'
+			},
+			{
+				name: 'Angular',
+				icon: 'logos:angular-icon',
+				url: 'github.io'
+			},
+			{
+				name: 'React',
+				icon: 'logos:react',
+				url: 'github.io'
+			},
+			{
+				name: 'Webpack',
+				icon: 'logos:webpack',
+				url: 'github.io'
+			}
+		]
+		shortcut = Object.assign(shortcut, data)
+	}
+
+	// 用户来源
+	const getUserAccessSource = async () => {
+		const data = [
+			{ 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) => {
+			return {
+				name: t(v.name),
+				value: v.value
+			}
+		})
+	}
+	const barOptionsData = reactive<EChartsOption>(barOptions) as EChartsOption
+	// 周活跃量
+	const getWeeklyUserActivity = async () => {
+		const data = [
+			{ 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' }
+		]
+		set(
+			barOptionsData,
+			'xAxis.data',
+			data.map((v) => t(v.name))
+		)
+		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 () => {
+		const data = [
+			{ value: 335, name: '待支付' },
+			{ value: 310, name: '已下单' },
+			{ value: 234, name: '取消支付' },
+
+		]
+		set(
+			todayOrderOptionsData,
+			'legend.data',
+			data.map((v) => t(v.name))
+		)
+		todayOrderOptionsData!.series![0].data = data.map((v) => {
+			return {
+				name: t(v.name),
+				value: v.value
+			}
+		})
+	}
+
+	const getAllApi = async () => {
+		await Promise.all([
+			getCount(),
+			getProject(),
+			getNotice(),
+			getShortcut(),
+			getUserAccessSource(),
+			getWeeklyUserActivity(),
+			getTodayOrder()
+		])
+		loading.value = false
+	}
+
+	getAllApi()
+</script>
+<style scoped lang='scss'>
+	.kjfs {
+		cursor: pointer;
+		width: 65px;
+		height: 65px;
+		display: flex;
+		flex-direction: column;
+		justify-content: center;
+		align-items: center;
+		border-radius: 4px;
+		border: 1px solid #e4e7ed;
+		padding: 10px;
+		font-size: 14px;
+	}
+</style>

+ 224 - 0
src/views/Home/components/ToBeDone.vue

@@ -0,0 +1,224 @@
+<template>
+	<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>
+						{{ node.label }}
+					</span>
+					<span>{{data.time}}</span>
+				</p>
+
+			</div>
+		</template>
+
+	</el-tree-v2>
+
+	<OrderDeliveryForm ref="deliveryFormRef" @success="getList" />
+</template>
+<script lang="ts" setup>
+	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',
+						time: "2024-2-24 20:40"
+					},
+				],
+			},
+			{
+				id: 2,
+				name: '待发货',
+				isOpen: false,
+				children: [
+					{
+						type: "2",
+						orderId: 10,
+						name: '待发货1111',
+						time: "2024-2-24 20:40"
+					},
+					{
+						type: "2",
+						orderId: 10,
+						name: '待发货1111',
+						time: "2024-2-24 20:40"
+					},
+					{
+						type: "2",
+						orderId: 10,
+						name: '待发货1111',
+						time: "2024-2-24 20:40"
+					},
+					{
+						type: "2",
+						orderId: 10,
+						name: '待发货1111',
+						time: "2024-2-24 20:40"
+					},
+					{
+						type: "2",
+						orderId: 10,
+						name: '待发货1111',
+						time: "2024-2-24 20:40"
+					},
+					{
+						type: "2",
+						orderId: 10,
+						name: '待发货1111',
+						time: "2024-2-24 20:40"
+					},
+				],
+			},
+			{
+				id: 3,
+				name: '待售后',
+				isOpen: false,
+				children: [
+					{
+						name: '待售后1111',
+						time: "2024-2-24 20:40"
+					},
+				],
+			},
+			{
+				id: 4,
+				name: '待开票',
+				isOpen: false,
+				children: [
+					{
+						name: '待开票1111',
+						time: "2024-2-24 20:40"
+					},
+				],
+			},
+		]
+	)
+	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)
+		}
+	}
+</script>
+<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>
+<style lang='scss' scoped>
+	.file {
+		width: 100%;
+		display: flex;
+		align-items: center;
+		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 {
+			display: flex;
+			align-items: center;
+			position: relative;
+			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;
+		text-align: center;
+		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;
+			color: white;
+		}
+	}
+</style>

+ 347 - 308
src/views/Home/echarts-data.ts

@@ -1,308 +1,347 @@
-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: {
-    axisTick: {
-      show: false
-    }
-  },
-  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'),
-      smooth: true,
-      type: 'line',
-      itemStyle: {},
-      data: [120, 82, 91, 154, 162, 140, 145, 250, 134, 56, 99, 123],
-      animationDuration: 2800,
-      animationEasing: 'quadraticOut'
-    }
-  ]
-}
-
-export const pieOptions: EChartsOption = {
-  title: {
-    text: t('analysis.userAccessSource'),
-    left: 'center'
-  },
-  tooltip: {
-    trigger: 'item',
-    formatter: '{a} <br/>{b} : {c} ({d}%)'
-  },
-  legend: {
-    orient: 'vertical',
-    left: 'left',
-    data: [
-      t('analysis.directAccess'),
-      t('analysis.mailMarketing'),
-      t('analysis.allianceAdvertising'),
-      t('analysis.videoAdvertising'),
-      t('analysis.searchEngines')
-    ]
-  },
-  series: [
-    {
-      name: t('analysis.userAccessSource'),
-      type: 'pie',
-      radius: '55%',
-      center: ['50%', '60%'],
-      data: [
-        { 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 = {
-  title: {
-    text: t('analysis.weeklyUserActivity'),
-    left: 'center'
-  },
-  tooltip: {
-    trigger: 'axis',
-    axisPointer: {
-      type: 'shadow'
-    }
-  },
-  grid: {
-    left: 50,
-    right: 20,
-    bottom: 20
-  },
-  xAxis: {
-    type: 'category',
-    data: [
-      t('analysis.monday'),
-      t('analysis.tuesday'),
-      t('analysis.wednesday'),
-      t('analysis.thursday'),
-      t('analysis.friday'),
-      t('analysis.saturday'),
-      t('analysis.sunday')
-    ],
-    axisTick: {
-      alignWithLabel: true
-    }
-  },
-  yAxis: {
-    type: 'value'
-  },
-  series: [
-    {
-      name: t('analysis.activeQuantity'),
-      data: [13253, 34235, 26321, 12340, 24643, 1322, 1324],
-      type: 'bar'
-    }
-  ]
-}
-
-export const radarOption: EChartsOption = {
-  legend: {
-    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 }
-    ]
-  },
-  series: [
-    {
-      name: `xxx${t('workplace.index')}`,
-      type: 'radar',
-      data: [
-        {
-          value: [42, 30, 20, 35, 80],
-          name: t('workplace.personal')
-        },
-        {
-          value: [50, 140, 290, 100, 90],
-          name: t('workplace.team')
-        }
-      ]
-    }
-  ]
-}
-
-export const wordOptions = {
-  series: [
-    {
-      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),
-              Math.round(Math.random() * 160)
-            ].join(',') +
-            ')'
-          )
-        }
-      },
-      emphasis: {
-        textStyle: {
-          shadowBlur: 10,
-          shadowColor: '#333'
-        }
-      },
-      data: [
-        {
-          name: 'Sam S Club',
-          value: 10000,
-          textStyle: {
-            color: 'black'
-          },
-          emphasis: {
-            textStyle: {
-              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: {
+		axisTick: {
+			show: false
+		}
+	},
+	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'),
+			smooth: true,
+			type: 'line',
+			itemStyle: {},
+			data: [120, 82, 91, 154, 162, 140, 145, 250, 134, 56, 99, 123],
+			animationDuration: 2800,
+			animationEasing: 'quadraticOut'
+		}
+	]
+}
+
+export const pieOptions : EChartsOption = {
+	title: {
+		// text: t('analysis.userAccessSource'),
+		left: 'center'
+	},
+	tooltip: {
+		trigger: 'item',
+		formatter: '{a} <br/>{b} : {c} ({d}%)'
+	},
+	legend: {
+		orient: 'vertical',
+		left: 'left',
+		data: [
+			t('analysis.directAccess'),
+			t('analysis.mailMarketing'),
+			t('analysis.allianceAdvertising'),
+			t('analysis.videoAdvertising'),
+			t('analysis.searchEngines')
+		]
+	},
+	series: [
+		{
+			name: t('analysis.userAccessSource'),
+			type: 'pie',
+			radius: '55%',
+			center: ['50%', '60%'],
+			data: [
+				{ 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 = {
+	title: {
+		// text: '今日订单',
+		left: 'center'
+	},
+	tooltip: {
+		trigger: 'item',
+		formatter: '{a} <br/>{b} : {c} ({d}%)'
+	},
+	legend: {
+		orient: 'vertical',
+		left: 'left',
+		data: [
+			t('analysis.directAccess'),
+			t('analysis.mailMarketing'),
+			t('analysis.allianceAdvertising'),
+			t('analysis.videoAdvertising'),
+			t('analysis.searchEngines')
+		]
+	},
+	series: [
+		{
+			name: t('analysis.userAccessSource'),
+			type: 'pie',
+			radius: '55%',
+			center: ['50%', '60%'],
+			data: [
+				{ 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 = {
+	title: {
+		// text: t('analysis.weeklyUserActivity'),
+		left: 'center'
+	},
+	tooltip: {
+		trigger: 'axis',
+		axisPointer: {
+			type: 'shadow'
+		}
+	},
+	grid: {
+		left: 50,
+		right: 20,
+		bottom: 20
+	},
+	xAxis: {
+		type: 'category',
+		data: [
+			t('analysis.monday'),
+			t('analysis.tuesday'),
+			t('analysis.wednesday'),
+			t('analysis.thursday'),
+			t('analysis.friday'),
+			t('analysis.saturday'),
+			t('analysis.sunday')
+		],
+		axisTick: {
+			alignWithLabel: true
+		}
+	},
+	yAxis: {
+		type: 'value'
+	},
+	series: [
+		{
+			name: t('analysis.activeQuantity'),
+			data: [13253, 34235, 26321, 12340, 24643, 1322, 1324],
+			type: 'bar'
+		}
+	]
+}
+
+
+
+export const radarOption : EChartsOption = {
+	legend: {
+		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 }
+		]
+	},
+	series: [
+		{
+			name: `xxx${t('workplace.index')}`,
+			type: 'radar',
+			data: [
+				{
+					value: [42, 30, 20, 35, 80],
+					name: t('workplace.personal')
+				},
+				{
+					value: [50, 140, 290, 100, 90],
+					name: t('workplace.team')
+				}
+			]
+		}
+	]
+}
+
+export const wordOptions = {
+	series: [
+		{
+			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),
+							Math.round(Math.random() * 160)
+						].join(',') +
+						')'
+					)
+				}
+			},
+			emphasis: {
+				textStyle: {
+					shadowBlur: 10,
+					shadowColor: '#333'
+				}
+			},
+			data: [
+				{
+					name: 'Sam S Club',
+					value: 10000,
+					textStyle: {
+						color: 'black'
+					},
+					emphasis: {
+						textStyle: {
+							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
+				}
+			]
+		}
+	]
+}

+ 92 - 104
src/views/Login/Login.vue

@@ -1,104 +1,92 @@
-<template>
-  <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-gray-500 bg-opacity-20 relative p-30px lt-xl:hidden`"
-      >
-        <!-- 左上角的 logo + 系统标题 -->
-        <div class="relative flex items-center text-white">
-          <img alt="" class="mr-10px h-48px w-48px" src="@/assets/imgs/logo.png" />
-          <span class="text-20px font-bold">{{ underlineToHump(appStore.getTitle) }}</span>
-        </div>
-        <!-- 左边的背景图 + 欢迎语 -->
-        <div class="h-[calc(100%-60px)] flex items-center justify-center">
-          <TransitionGroup
-            appear
-            enter-active-class="animate__animated animate__bounceInLeft"
-            tag="div"
-          >
-            <img key="1" alt="" class="w-350px" src="@/assets/svgs/login-box-bg.svg" />
-            <div key="2" class="text-3xl text-white">{{ t('login.welcome') }}</div>
-            <div key="3" class="mt-5 text-14px font-normal text-white">
-              {{ t('login.message') }}
-            </div>
-          </TransitionGroup>
-        </div>
-      </div>
-      <div class="relative flex-1 p-30px dark:bg-[var(--login-bg-color)] lt-sm:p-10px">
-        <!-- 右上角的主题、语言选择 -->
-        <div
-          class="flex items-center justify-between text-white at-2xl:justify-end at-xl:justify-end"
-        >
-          <div class="flex items-center at-2xl:hidden at-xl:hidden">
-            <img alt="" class="mr-10px h-48px w-48px" src="@/assets/imgs/logo.png" />
-            <span class="text-20px font-bold">{{ underlineToHump(appStore.getTitle) }}</span>
-          </div>
-          <div class="flex items-center justify-end space-x-10px">
-            <ThemeSwitch />
-            <LocaleDropdown class="dark:text-white lt-xl:text-white" />
-          </div>
-        </div>
-        <!-- 右边的登录界面 -->
-        <Transition appear enter-active-class="animate__animated animate__bounceInRight">
-          <div
-            class="m-auto h-full w-[100%] flex items-center at-2xl:max-w-500px at-lg:max-w-500px at-md:max-w-500px at-xl:max-w-500px"
-          >
-            <!-- 账号登录 -->
-            <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)" />
-          </div>
-        </Transition>
-      </div>
-    </div>
-  </div>
-</template>
-<script lang="ts" setup>
-import { underlineToHump } from '@/utils'
-
-import { useDesign } from '@/hooks/web/useDesign'
-import { useAppStore } from '@/store/modules/app'
-import { ThemeSwitch } from '@/layout/components/ThemeSwitch'
-import { LocaleDropdown } from '@/layout/components/LocaleDropdown'
-
-import { LoginForm, MobileForm, QrCodeForm, RegisterForm, SSOLoginVue } from './components'
-
-defineOptions({ name: 'Login' })
-
-const { t } = useI18n()
-const appStore = useAppStore()
-const { getPrefixCls } = useDesign()
-const prefixCls = getPrefixCls('login')
-</script>
-
-<style lang="scss" scoped>
-$prefix-cls: #{$namespace}-login;
-
-.#{$prefix-cls} {
-  overflow: auto;
-
-  &__left {
-    &::before {
-      position: absolute;
-      top: 0;
-      left: 0;
-      z-index: -1;
-      width: 100%;
-      height: 100%;
-      background-image: url('@/assets/svgs/login-bg.svg');
-      background-position: center;
-      background-repeat: no-repeat;
-      content: '';
-    }
-  }
-}
-</style>
+<template>
+	<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> -->
+				<!-- 左边的背景图 + 欢迎语 -->
+				<div class="h-[calc(100%-60px)] flex items-center justify-center">
+					<TransitionGroup appear enter-active-class="animate__animated animate__bounceInLeft" tag="div">
+						<!-- <img key="1" alt="" class="w-350px" src="@/assets/svgs/login-box-bg.svg" /> -->
+						<!-- <div key="2" class="text-3xl text-white">{{ t('login.welcome') }}</div> -->
+						<!-- <div key="3" class="mt-5 text-14px font-normal text-white">
+              {{ t('login.message') }}
+            </div> -->
+					</TransitionGroup>
+				</div>
+			</div>
+			<div class="relative flex-1 p-30px dark:bg-[var(--login-bg-color)] lt-sm:p-10px">
+				<!-- 右上角的主题、语言选择 -->
+				<!-- <div class="flex items-center justify-between text-white at-2xl:justify-end at-xl:justify-end">
+					<div class="flex items-center at-2xl:hidden at-xl:hidden">
+						<img alt="" class="mr-10px h-48px w-48px" src="@/assets/imgs/logo.png" />
+						<span class="text-20px font-bold">{{ underlineToHump(appStore.getTitle) }}</span>
+					</div>
+					<div class="flex items-center justify-end space-x-10px">
+						<ThemeSwitch />
+						<LocaleDropdown class="dark:text-white lt-xl:text-white" />
+					</div>
+				</div> -->
+				<!-- 右边的登录界面 -->
+				<Transition appear enter-active-class="animate__animated animate__bounceInRight">
+					<div
+						class="m-auto h-full w-[100%] flex items-center at-2xl:max-w-500px at-lg:max-w-500px at-md:max-w-500px at-xl:max-w-500px">
+						<!-- 账号登录 -->
+						<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)" />
+					</div>
+				</Transition>
+			</div>
+		</div>
+	</div>
+</template>
+<script lang="ts" setup>
+	import { underlineToHump } from '@/utils'
+
+	import { useDesign } from '@/hooks/web/useDesign'
+	import { useAppStore } from '@/store/modules/app'
+	import { ThemeSwitch } from '@/layout/components/ThemeSwitch'
+	import { LocaleDropdown } from '@/layout/components/LocaleDropdown'
+
+	import { LoginForm, MobileForm, QrCodeForm, RegisterForm, SSOLoginVue } from './components'
+
+	defineOptions({ name: 'Login' })
+
+	const { t } = useI18n()
+	const appStore = useAppStore()
+	const { getPrefixCls } = useDesign()
+	const prefixCls = getPrefixCls('login')
+</script>
+
+<style lang="scss" scoped>
+	$prefix-cls: #{$namespace}-login;
+
+	.#{$prefix-cls} {
+		overflow: auto;
+
+		&__left {
+			&::before {
+				position: absolute;
+				top: 0;
+				left: 0;
+				z-index: -1;
+				width: 100%;
+				height: 100%;
+				background-image: url('@/assets/svgs/login-bg.svg');
+				background-position: center;
+				background-repeat: no-repeat;
+				content: '';
+			}
+		}
+	}
+</style>

+ 2 - 2
src/views/Login/SocialLogin.vue

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

+ 306 - 348
src/views/Login/components/LoginForm.vue

@@ -1,348 +1,306 @@
-<template>
-  <el-form
-    v-show="getShow"
-    ref="formLogin"
-    :model="loginData.loginForm"
-    :rules="LoginRules"
-    class="login-form"
-    label-position="top"
-    label-width="120px"
-    size="large"
-  >
-    <el-row style="margin-right: -10px; margin-left: -10px">
-      <el-col :span="24" style="padding-right: 10px; padding-left: 10px">
-        <el-form-item>
-          <LoginFormTitle style="width: 100%" />
-        </el-form-item>
-      </el-col>
-      <el-col :span="24" style="padding-right: 10px; padding-left: 10px">
-        <el-form-item v-if="loginData.tenantEnable === 'true'" prop="tenantName">
-          <el-input
-            v-model="loginData.loginForm.tenantName"
-            :placeholder="t('login.tenantNamePlaceholder')"
-            :prefix-icon="iconHouse"
-            link
-            type="primary"
-          />
-        </el-form-item>
-      </el-col>
-      <el-col :span="24" style="padding-right: 10px; padding-left: 10px">
-        <el-form-item prop="username">
-          <el-input
-            v-model="loginData.loginForm.username"
-            :placeholder="t('login.usernamePlaceholder')"
-            :prefix-icon="iconAvatar"
-          />
-        </el-form-item>
-      </el-col>
-      <el-col :span="24" style="padding-right: 10px; padding-left: 10px">
-        <el-form-item prop="password">
-          <el-input
-            v-model="loginData.loginForm.password"
-            :placeholder="t('login.passwordPlaceholder')"
-            :prefix-icon="iconLock"
-            show-password
-            type="password"
-            @keyup.enter="getCode()"
-          />
-        </el-form-item>
-      </el-col>
-      <el-col
-        :span="24"
-        style="padding-right: 10px; padding-left: 10px; margin-top: -20px; margin-bottom: -20px"
-      >
-        <el-form-item>
-          <el-row justify="space-between" style="width: 100%">
-            <el-col :span="6">
-              <el-checkbox v-model="loginData.loginForm.rememberMe">
-                {{ t('login.remember') }}
-              </el-checkbox>
-            </el-col>
-            <el-col :offset="6" :span="12">
-              <el-link style="float: right" type="primary">{{ t('login.forgetPassword') }}</el-link>
-            </el-col>
-          </el-row>
-        </el-form-item>
-      </el-col>
-      <el-col :span="24" style="padding-right: 10px; padding-left: 10px">
-        <el-form-item>
-          <XButton
-            :loading="loginLoading"
-            :title="t('login.login')"
-            class="w-[100%]"
-            type="primary"
-            @click="getCode()"
-          />
-        </el-form-item>
-      </el-col>
-      <Verify
-        ref="verify"
-        :captchaType="captchaType"
-        :imgSize="{ width: '400px', height: '200px' }"
-        mode="pop"
-        @success="handleLogin"
-      />
-      <el-col :span="24" style="padding-right: 10px; padding-left: 10px">
-        <el-form-item>
-          <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)"
-              />
-            </el-col>
-            <el-col :span="8">
-              <XButton
-                :title="t('login.btnQRCode')"
-                class="w-[100%]"
-                @click="setLoginState(LoginStateEnum.QR_CODE)"
-              />
-            </el-col>
-            <el-col :span="8">
-              <XButton
-                :title="t('login.btnRegister')"
-                class="w-[100%]"
-                @click="setLoginState(LoginStateEnum.REGISTER)"
-              />
-            </el-col>
-          </el-row>
-        </el-form-item>
-      </el-col>
-      <el-divider content-position="center">{{ t('login.otherLogin') }}</el-divider>
-      <el-col :span="24" style="padding-right: 10px; padding-left: 10px">
-        <el-form-item>
-          <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)"
-            />
-          </div>
-        </el-form-item>
-      </el-col>
-      <el-divider content-position="center">萌新必读</el-divider>
-      <el-col :span="24" style="padding-right: 10px; padding-left: 10px">
-        <el-form-item>
-          <div class="w-[100%] flex justify-between">
-            <el-link href="https://doc.iocoder.cn/" target="_blank">📚开发指南</el-link>
-            <el-link href="https://doc.iocoder.cn/video/" target="_blank">🔥视频教程</el-link>
-            <el-link href="https://www.iocoder.cn/Interview/good-collection/" target="_blank">
-              ⚡面试手册
-            </el-link>
-            <el-link href="http://static.yudao.iocoder.cn/mp/Aix9975.jpeg" target="_blank">
-              🤝外包咨询
-            </el-link>
-          </div>
-        </el-form-item>
-      </el-col>
-    </el-row>
-  </el-form>
-</template>
-<script lang="ts" setup>
-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 { t } = useI18n()
-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: {
-    tenantName: '芋道源码',
-    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({})
-  } else {
-    // 情况二,已开启:则展示验证码;只有完成验证码的情况,才进行登录
-    // 弹出验证码
-    verify.value.show()
-  }
-}
-// 获取租户 ID
-const getTenantId = async () => {
-  if (loginData.tenantEnable === 'true') {
-    const res = await LoginApi.getTenantIdByName(loginData.loginForm.tenantName)
-    authUtil.setTenantId(res)
-  }
-}
-// 记住我
-const getCookie = () => {
-  const loginForm = authUtil.getLoginForm()
-  if (loginForm) {
-    loginData.loginForm = {
-      ...loginData.loginForm,
-      username: loginForm.username ? loginForm.username : loginData.loginForm.username,
-      password: loginForm.password ? loginForm.password : loginData.loginForm.password,
-      rememberMe: loginForm.rememberMe ? true : false,
-      tenantName: loginForm.tenantName ? loginForm.tenantName : loginData.loginForm.tenantName
-    }
-  }
-}
-// 根据域名,获得租户信息
-const 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) {
-      return
-    }
-    loginData.loginForm.captchaVerification = params.captchaVerification
-    const res = await LoginApi.login(loginData.loginForm)
-    if (!res) {
-      return
-    }
-    loading.value = ElLoading.service({
-      lock: true,
-      text: '正在加载系统中...',
-      background: 'rgba(0, 0, 0, 0.7)'
-    })
-    if (loginData.loginForm.rememberMe) {
-      authUtil.setLoginForm(loginData.loginForm)
-    } else {
-      authUtil.removeLoginForm()
-    }
-    authUtil.setToken(res)
-    if (!redirect.value) {
-      redirect.value = '/'
-    }
-    // 判断是否为SSO登录
-    if (redirect.value.indexOf('sso') !== -1) {
-      window.location.href = window.location.href.replace('/login?redirect=', '')
-    } else {
-      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('此方式未配置')
-  } else {
-    loginLoading.value = true
-    if (loginData.tenantEnable === 'true') {
-      // 尝试先通过 tenantName 获取租户
-      await getTenantId()
-      // 如果获取不到,则需要弹出提示,进行处理
-      if (!authUtil.getTenantId()) {
-        await message.prompt('请输入租户名称', t('common.reminder')).then(async ({ value }) => {
-          const res = await LoginApi.getTenantIdByName(value)
-          authUtil.setTenantId(res)
-        })
-      }
-    }
-    // 计算 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
-  }
-}
-watch(
-  () => currentRoute.value,
-  (route: RouteLocationNormalizedLoaded) => {
-    redirect.value = route?.query?.redirect as string
-  },
-  {
-    immediate: true
-  }
-)
-onMounted(() => {
-  getCookie()
-  getTenantByWebsite()
-})
-</script>
-
-<style lang="scss" scoped>
-:deep(.anticon) {
-  &:hover {
-    color: var(--el-color-primary) !important;
-  }
-}
-
-.login-code {
-  float: right;
-  width: 100%;
-  height: 38px;
-
-  img {
-    width: 100%;
-    height: auto;
-    max-width: 100px;
-    vertical-align: middle;
-    cursor: pointer;
-  }
-}
-</style>
+<template>
+	<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-col>
+			<el-col :span="24" style="padding-right: 10px; padding-left: 10px">
+				<el-form-item v-if="loginData.tenantEnable === 'true'" prop="tenantName">
+					<el-input v-model="loginData.loginForm.tenantName" :placeholder="t('login.tenantNamePlaceholder')"
+						:prefix-icon="iconHouse" link type="primary" />
+				</el-form-item>
+			</el-col>
+			<el-col :span="24" style="padding-right: 10px; padding-left: 10px">
+				<el-form-item prop="username">
+					<el-input v-model="loginData.loginForm.username" :placeholder="t('login.usernamePlaceholder')"
+						:prefix-icon="iconAvatar" />
+				</el-form-item>
+			</el-col>
+			<el-col :span="24" style="padding-right: 10px; padding-left: 10px">
+				<el-form-item prop="password">
+					<el-input v-model="loginData.loginForm.password" :placeholder="t('login.passwordPlaceholder')"
+						:prefix-icon="iconLock" show-password type="password" @keyup.enter="getCode()" />
+				</el-form-item>
+			</el-col>
+			<el-col :span="24" style="padding-right: 10px; padding-left: 10px; margin-top: -20px; margin-bottom: -20px">
+				<el-form-item>
+					<el-row justify="space-between" style="width: 100%">
+						<el-col :span="6">
+							<el-checkbox v-model="loginData.loginForm.rememberMe">
+								{{ t('login.remember') }}
+							</el-checkbox>
+						</el-col>
+						<el-col :offset="6" :span="12">
+							<el-link style="float: right" type="primary">{{ t('login.forgetPassword') }}</el-link>
+						</el-col>
+					</el-row>
+				</el-form-item>
+			</el-col>
+			<el-col :span="24" style="padding-right: 10px; padding-left: 10px">
+				<el-form-item>
+					<XButton :loading="loginLoading" :title="t('login.login')" class="w-[100%]" type="primary"
+						@click="getCode()" />
+				</el-form-item>
+			</el-col>
+			<Verify ref="verify" :captchaType="captchaType" :imgSize="{ width: '400px', height: '200px' }" mode="pop"
+				@success="handleLogin" />
+			<el-col :span="24" style="padding-right: 10px; padding-left: 10px">
+				<el-form-item>
+					<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)" />
+						</el-col>
+						<el-col :span="8">
+							<XButton :title="t('login.btnQRCode')" class="w-[100%]"
+								@click="setLoginState(LoginStateEnum.QR_CODE)" />
+						</el-col>
+						<el-col :span="8">
+							<XButton :title="t('login.btnRegister')" class="w-[100%]"
+								@click="setLoginState(LoginStateEnum.REGISTER)" />
+						</el-col>
+					</el-row>
+				</el-form-item>
+			</el-col>
+			<!-- <el-divider content-position="center">{{ t('login.otherLogin') }}</el-divider> -->
+			<!-- <el-col :span="24" style="padding-right: 10px; padding-left: 10px">
+				<el-form-item>
+					<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)" />
+					</div>
+				</el-form-item>
+			</el-col> -->
+			<!-- <el-divider content-position="center">萌新必读</el-divider>
+      <el-col :span="24" style="padding-right: 10px; padding-left: 10px">
+        <el-form-item>
+          <div class="w-[100%] flex justify-between">
+            <el-link href="https://doc.iocoder.cn/" target="_blank">📚开发指南</el-link>
+            <el-link href="https://doc.iocoder.cn/video/" target="_blank">🔥视频教程</el-link>
+            <el-link href="https://www.iocoder.cn/Interview/good-collection/" target="_blank">
+              ⚡面试手册
+            </el-link>
+            <el-link href="http://static.yudao.iocoder.cn/mp/Aix9975.jpeg" target="_blank">
+              🤝外包咨询
+            </el-link>
+          </div>
+        </el-form-item>
+      </el-col> -->
+		</el-row>
+	</el-form>
+</template>
+<script lang="ts" setup>
+	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 { t } = useI18n()
+	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: {
+			tenantName: '非繁科技',
+			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({})
+		} else {
+			// 情况二,已开启:则展示验证码;只有完成验证码的情况,才进行登录
+			// 弹出验证码
+			verify.value.show()
+		}
+	}
+	// 获取租户 ID
+	const getTenantId = async () => {
+		if (loginData.tenantEnable === 'true') {
+			const res = await LoginApi.getTenantIdByName(loginData.loginForm.tenantName)
+			authUtil.setTenantId(res)
+		}
+	}
+	// 记住我
+	const getCookie = () => {
+		const loginForm = authUtil.getLoginForm()
+		if (loginForm) {
+			loginData.loginForm = {
+				...loginData.loginForm,
+				username: loginForm.username ? loginForm.username : loginData.loginForm.username,
+				password: loginForm.password ? loginForm.password : loginData.loginForm.password,
+				rememberMe: loginForm.rememberMe ? true : false,
+				tenantName: loginForm.tenantName ? loginForm.tenantName : loginData.loginForm.tenantName
+			}
+		}
+	}
+	// 根据域名,获得租户信息
+	const 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) {
+				return
+			}
+			loginData.loginForm.captchaVerification = params.captchaVerification
+			console.log(loginData.loginForm)
+			const res = await LoginApi.login(loginData.loginForm)
+			if (!res) {
+				return
+			}
+			loading.value = ElLoading.service({
+				lock: true,
+				text: '正在加载系统中...',
+				background: 'rgba(0, 0, 0, 0.7)'
+			})
+			if (loginData.loginForm.rememberMe) {
+				authUtil.setLoginForm(loginData.loginForm)
+			} else {
+				authUtil.removeLoginForm()
+			}
+			authUtil.setToken(res)
+			if (!redirect.value) {
+				redirect.value = '/'
+			}
+			// 判断是否为SSO登录
+			if (redirect.value.indexOf('sso') !== -1) {
+				window.location.href = window.location.href.replace('/login?redirect=', '')
+			} else {
+				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('此方式未配置')
+		} else {
+			loginLoading.value = true
+			if (loginData.tenantEnable === 'true') {
+				// 尝试先通过 tenantName 获取租户
+				await getTenantId()
+				// 如果获取不到,则需要弹出提示,进行处理
+				if (!authUtil.getTenantId()) {
+					await message.prompt('请输入租户名称', t('common.reminder')).then(async ({ value }) => {
+						const res = await LoginApi.getTenantIdByName(value)
+						authUtil.setTenantId(res)
+					})
+				}
+			}
+			// 计算 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
+		}
+	}
+	watch(
+		() => currentRoute.value,
+		(route : RouteLocationNormalizedLoaded) => {
+			redirect.value = route?.query?.redirect as string
+		},
+		{
+			immediate: true
+		}
+	)
+	onMounted(() => {
+		getCookie()
+		getTenantByWebsite()
+	})
+</script>
+
+<style lang="scss" scoped>
+	:deep(.anticon) {
+		&:hover {
+			color: var(--el-color-primary) !important;
+		}
+	}
+
+	.login-code {
+		float: right;
+		width: 100%;
+		height: 38px;
+
+		img {
+			width: 100%;
+			height: auto;
+			max-width: 100px;
+			vertical-align: middle;
+			cursor: pointer;
+		}
+	}
+
+	.login-form {
+		background: rgba(255, 255, 255, 0.95);
+	}
+</style>

+ 1 - 1
src/views/Login/components/MobileForm.vue

@@ -133,7 +133,7 @@ const loginData = reactive({
   },
   loginForm: {
     uuid: '',
-    tenantName: '芋道源码',
+    tenantName: '非繁源码',
     mobileNumber: '',
     code: ''
   }

+ 167 - 196
src/views/infra/fileConfig/FileConfigForm.vue

@@ -1,196 +1,167 @@
-<template>
-  <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>
-      <el-form-item label="备注" prop="remark">
-        <el-input v-model="formData.remark" placeholder="请输入备注" />
-      </el-form-item>
-      <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>
-      </el-form-item>
-      <!-- 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>
-      <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>
-      <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>
-      <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>
-      <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>
-      <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>
-      </el-form-item>
-      <!-- S3 -->
-      <el-form-item v-if="formData.storage === 20" label="节点地址" prop="config.endpoint">
-        <el-input v-model="formData.config.endpoint" placeholder="请输入节点地址" />
-      </el-form-item>
-      <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>
-      <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>
-      <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>
-      <!-- 通用 -->
-      <el-form-item v-if="formData.storage === 20" label="自定义域名">
-        <!-- 无需参数校验,所以去掉 prop -->
-        <el-input v-model="formData.config.domain" placeholder="请输入自定义域名" />
-      </el-form-item>
-      <el-form-item v-else-if="formData.storage" label="自定义域名" prop="config.domain">
-        <el-input v-model="formData.config.domain" placeholder="请输入自定义域名" />
-      </el-form-item>
-    </el-form>
-    <template #footer>
-      <el-button :disabled="formLoading" type="primary" @click="submitForm">确 定</el-button>
-      <el-button @click="dialogVisible = false">取 消</el-button>
-    </template>
-  </Dialog>
-</template>
-<script lang="ts" setup>
-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
-    try {
-      formData.value = await FileConfigApi.getFileConfig(id)
-    } finally {
-      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
-  // 提交请求
-  formLoading.value = true
-  try {
-    const data = formData.value as unknown as FileConfigApi.FileConfigVO
-    if (formType.value === 'create') {
-      await FileConfigApi.createFileConfig(data)
-      message.success(t('common.createSuccess'))
-    } else {
-      await FileConfigApi.updateFileConfig(data)
-      message.success(t('common.updateSuccess'))
-    }
-    dialogVisible.value = false
-    // 发送操作成功的事件
-    emit('success')
-  } finally {
-    formLoading.value = false
-  }
-}
-
-/** 重置表单 */
-const resetForm = () => {
-  formData.value = {
-    id: undefined,
-    name: '',
-    storage: undefined!,
-    remark: '',
-    config: {} as FileConfigApi.FileClientConfig
-  }
-  formRef.value?.resetFields()
-}
-</script>
+<template>
+	<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>
+			<el-form-item label="备注" prop="remark">
+				<el-input v-model="formData.remark" placeholder="请输入备注" />
+			</el-form-item>
+			<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>
+			</el-form-item>
+			<!-- 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>
+			<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>
+			<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>
+			<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>
+			<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>
+			<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>
+			</el-form-item>
+			<!-- S3 -->
+			<el-form-item v-if="formData.storage === 20" label="节点地址" prop="config.endpoint">
+				<el-input v-model="formData.config.endpoint" placeholder="请输入节点地址" />
+			</el-form-item>
+			<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>
+			<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>
+			<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>
+			<!-- 通用 -->
+			<el-form-item v-if="formData.storage === 20" label="自定义域名">
+				<!-- 无需参数校验,所以去掉 prop -->
+				<el-input v-model="formData.config.domain" placeholder="请输入自定义域名" />
+			</el-form-item>
+			<el-form-item v-else-if="formData.storage" label="自定义域名" prop="config.domain">
+				<el-input v-model="formData.config.domain" placeholder="请输入自定义域名" />
+			</el-form-item>
+		</el-form>
+		<template #footer>
+			<el-button :disabled="formLoading" type="primary" @click="submitForm">确 定</el-button>
+			<el-button @click="dialogVisible = false">取 消</el-button>
+		</template>
+	</Dialog>
+</template>
+<script lang="ts" setup>
+	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
+			try {
+				formData.value = await FileConfigApi.getFileConfig(id)
+			} finally {
+				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
+		// 提交请求
+		formLoading.value = true
+		try {
+			const data = formData.value as unknown as FileConfigApi.FileConfigVO
+			if (formType.value === 'create') {
+				await FileConfigApi.createFileConfig(data)
+				message.success(t('common.createSuccess'))
+			} else {
+				await FileConfigApi.updateFileConfig(data)
+				message.success(t('common.updateSuccess'))
+			}
+			dialogVisible.value = false
+			// 发送操作成功的事件
+			emit('success')
+		} finally {
+			formLoading.value = false
+		}
+	}
+
+	/** 重置表单 */
+	const resetForm = () => {
+		formData.value = {
+			id: undefined,
+			name: '',
+			storage: undefined!,
+			remark: '',
+			config: {} as FileConfigApi.FileClientConfig
+		}
+		formRef.value?.resetFields()
+	}
+</script>
+<style lang='scss' scoped>
+	::v-deep .el-input__suffix-inner>:first-child {
+		display: none
+	}
+</style>

+ 76 - 82
src/views/mall/home/components/ShortcutCard.vue

@@ -1,82 +1,76 @@
-<template>
-  <el-card shadow="never">
-    <template #header>
-      <CardTitle title="快捷入口" />
-    </template>
-    <div class="flex flex-row flex-wrap gap-8 p-4">
-      <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!" />
-        </div>
-        <span>{{ menu.name }}</span>
-      </div>
-    </div>
-  </el-card>
-</template>
-<script lang="ts" setup>
-/** 快捷入口卡片 */
-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 })
-}
-</script>
+<template>
+	<el-card shadow="never">
+		<template #header>
+			<CardTitle title="快捷入口" />
+		</template>
+		<div class="flex flex-row flex-wrap gap-8 p-4">
+			<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!" />
+				</div>
+				<span>{{ menu.name }}</span>
+			</div>
+		</div>
+	</el-card>
+</template>
+<script lang="ts" setup>
+	/** 快捷入口卡片 */
+	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 })
+	}
+</script>

+ 116 - 146
src/views/mall/product/spu/form/InfoForm.vue

@@ -1,146 +1,116 @@
-<!-- 商品发布 - 基础设置 -->
-<template>
-  <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>
-    <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>
-    <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-select>
-    </el-form-item>
-    <el-form-item label="商品关键字" prop="keyword">
-      <el-input v-model="formData.keyword" placeholder="请输入商品关键字" class="w-80!" />
-    </el-form-item>
-    <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"
-        class="w-80!"
-      />
-    </el-form-item>
-    <el-form-item label="商品封面图" prop="picUrl">
-      <UploadImg v-model="formData.picUrl" height="80px" :disabled="isDetail" />
-    </el-form-item>
-    <el-form-item label="商品轮播图" prop="sliderPicUrls">
-      <UploadImgs v-model:modelValue="formData.sliderPicUrls" :disabled="isDetail" />
-    </el-form-item>
-  </el-form>
-</template>
-<script lang="ts" setup>
-import { PropType } from 'vue'
-import { copyValueToTarget } from '@/utils'
-import { propTypes } from '@/utils/propTypes'
-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 message = useMessage() // 消息弹窗
-
-const formRef = ref() // 表单 Ref
-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 */
-watch(
-  () => props.propFormData,
-  (data) => {
-    if (!data) {
-      return
-    }
-    copyValueToTarget(formData, data)
-    // TODO @puhui999:优化多文件上传,看看有没可能搞成返回 v-model 图片列表这种
-    formData.sliderPicUrls = data['sliderPicUrls']?.map((item) => ({
-      url: item
-    }))
-  },
-  {
-    immediate: true
-  }
-)
-
-/** 表单校验 */
-const emit = defineEmits(['update:activeName'])
-const validate = async () => {
-  if (!formRef) return
-  try {
-    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()
-})
-</script>
+<!-- 商品发布 - 基础设置 -->
+<template>
+	<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>
+		<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>
+		<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-select>
+		</el-form-item>
+		<el-form-item label="商品关键字" prop="keyword">
+			<el-input v-model="formData.keyword" placeholder="请输入商品关键字" class="w-80!" />
+		</el-form-item>
+		<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"
+				class="w-80!" />
+		</el-form-item>
+
+		<el-form-item label="商品轮播图" prop="sliderPicUrls">
+			<UploadImgs v-model:modelValue="formData.sliderPicUrls" :disabled="isDetail" />
+		</el-form-item>
+	</el-form>
+</template>
+<script lang="ts" setup>
+	import { PropType } from 'vue'
+	import { copyValueToTarget } from '@/utils'
+	import { propTypes } from '@/utils/propTypes'
+	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 message = useMessage() // 消息弹窗
+
+	const formRef = ref() // 表单 Ref
+	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 */
+	watch(
+		() => props.propFormData,
+		(data) => {
+			if (!data) {
+				return
+			}
+			copyValueToTarget(formData, data)
+			// TODO @puhui999:优化多文件上传,看看有没可能搞成返回 v-model 图片列表这种
+			formData.sliderPicUrls = data['sliderPicUrls']?.map((item) => ({
+				url: item
+			}))
+		},
+		{
+			immediate: true
+		}
+	)
+
+	/** 表单校验 */
+	const emit = defineEmits(['update:activeName'])
+	const validate = async () => {
+		if (!formRef) return
+		try {
+			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()
+	})
+</script>

+ 460 - 204
src/views/mall/product/spu/form/index.vue

@@ -1,204 +1,460 @@
-<template>
-  <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"
-          v-model:activeName="activeName"
-          :is-detail="isDetail"
-          :propFormData="formData"
-        />
-      </el-tab-pane>
-      <el-tab-pane label="物流设置" name="delivery">
-        <DeliveryForm
-          ref="deliveryRef"
-          v-model:activeName="activeName"
-          :is-detail="isDetail"
-          :propFormData="formData"
-        />
-      </el-tab-pane>
-      <el-tab-pane label="商品详情" name="description">
-        <DescriptionForm
-          ref="descriptionRef"
-          v-model:activeName="activeName"
-          :is-detail="isDetail"
-          :propFormData="formData"
-        />
-      </el-tab-pane>
-      <el-tab-pane label="其它设置" name="other">
-        <OtherForm
-          ref="otherRef"
-          v-model:activeName="activeName"
-          :is-detail="isDetail"
-          :propFormData="formData"
-        />
-      </el-tab-pane>
-    </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>
-      </el-form-item>
-    </el-form>
-  </ContentWrap>
-</template>
-<script lang="ts" setup>
-import { cloneDeep } from 'lodash-es'
-import { useTagsViewStore } from '@/store/modules/tagsView'
-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 { t } = useI18n() // 国际化
-const message = useMessage() // 消息弹窗
-const { push, currentRoute } = useRouter() // 路由
-const { params, name } = useRoute() // 查询参数
-const { delView } = useTagsViewStore() // 视图操作
-
-const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
-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>({
-  name: '', // 商品名称
-  categoryId: undefined, // 商品分类
-  keyword: '', // 关键字
-  picUrl: '', // 商品封面图
-  sliderPicUrls: [], // 商品轮播图
-  introduction: '', // 商品简介
-  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
-  if (id) {
-    formLoading.value = true
-    try {
-      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)
-        } else {
-          // 回显价格分转元
-          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
-    } finally {
-      formLoading.value = false
-    }
-  }
-}
-
-/** 提交按钮 */
-const submitForm = async () => {
-  // 提交请求
-  formLoading.value = true
-  try {
-    // 校验各表单
-    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
-    const id = params.id as unknown as number
-    if (!id) {
-      await ProductSpuApi.createSpu(data)
-      message.success(t('common.createSuccess'))
-    } else {
-      await ProductSpuApi.updateSpu(data)
-      message.success(t('common.updateSuccess'))
-    }
-    close()
-  } finally {
-    formLoading.value = false
-  }
-}
-
-/** 关闭按钮 */
-const close = () => {
-  delView(unref(currentRoute))
-  push({ name: 'ProductSpu' })
-}
-
-/** 初始化 */
-onMounted(async () => {
-  await getDetail()
-})
-</script>
+<template>
+
+	<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>
+					<span @click="close">
+						<el-icon>
+							<Close />
+						</el-icon>
+					</span>
+				</div>
+			</div>
+		</template>
+		<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>
+			<div class="right">
+				<div v-show="activeName == 'info'">
+					<InfoForm ref="infoRef" :is-detail="isDetail" :propFormData="formData" />
+				</div>
+				<div v-show="activeName == 'sku'">
+					<SkuForm ref="skuRef" :is-detail="isDetail" :propFormData="formData" />
+				</div>
+				<div v-show="activeName == 'delivery'">
+					<DeliveryForm ref="deliveryRef" :is-detail="isDetail" :propFormData="formData" />
+				</div>
+				<div v-show="activeName == 'description'">
+					<DescriptionForm ref="descriptionRef" :is-detail="isDetail" :propFormData="formData" />
+				</div>
+				<div v-show="activeName == 'other'">
+					<OtherForm ref="otherRef" :is-detail="isDetail" :propFormData="formData" />
+				</div>
+			</div>
+
+			<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)">
+						删除
+					</el-button>
+					<el-button v-if="!isDetail && parentTabType==4" :loading="formLoading"
+						v-hasPermi="['product:spu:update']" type="primary"
+						@click="handleStatus02Change(ProductSpuStatusEnum.DISABLE.status)">
+						恢复
+					</el-button>
+					<el-button v-if="!isDetail && openType != 'create'" :loading="formLoading" type="primary"
+						@click="handleStatusChange">
+						{{parentRow.status == 0 ?"上架":"下架"}}
+					</el-button>
+
+					<el-button v-if="!isDetail" :loading="formLoading" type="primary" @click="submitForm">
+						保存
+					</el-button>
+
+				</el-form-item>
+			</el-form>
+		</ContentWrap>
+	</el-dialog>
+	<!-- </div> -->
+
+</template>
+<script lang="ts" setup>
+	import { cloneDeep } from 'lodash-es'
+	import { useTagsViewStore } from '@/store/modules/tagsView'
+	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 { Icon } from '@/components/Icon'
+	import { ProductSpuStatusEnum } from '@/utils/constants'
+	defineOptions({ name: 'ProductSpuForm' })
+
+	const { t } = useI18n() // 国际化
+	const message = useMessage() // 消息弹窗
+	const { push, currentRoute } = useRouter() // 路由
+	const { params, name } = useRoute() // 查询参数
+	const { delView } = useTagsViewStore() // 视图操作
+
+	const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
+	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>({
+		name: '', // 商品名称
+		categoryId: undefined, // 商品分类
+		keyword: '', // 关键字
+		picUrl: '', // 商品封面图
+		sliderPicUrls: [], // 商品轮播图
+		introduction: '', // 商品简介
+		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>({
+		name: '', // 商品名称
+		categoryId: undefined, // 商品分类
+		keyword: '', // 关键字
+		picUrl: '', // 商品封面图
+		sliderPicUrls: [], // 商品轮播图
+		introduction: '', // 商品简介
+		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 dialogVisible = ref(false) // 弹窗的是否展示
+	const dialogTitle = ref('') // 弹窗的标题
+
+	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
+		dialogVisible.value = true
+		productId.value = row.id
+		picUrl.value = row.picUrl
+		isFullScreen.value = false
+		await getDetail()
+		activeName.value = 'info'
+		openType.value = type
+		dialogTitle.value = t('action.' + 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
+		} else {
+			isDetail.value = false
+		}
+	}
+	defineExpose({ open }) // 提供 open 方法,用于打开弹窗
+
+	//全屏/取消全屏弹窗
+	const fullScreen = () => {
+		isFullScreen.value = !isFullScreen.value
+	}
+	/** 获得详情 */
+	const getDetail = async () => {
+		console.log(productId.value)
+		const id = productId.value as any as number
+		if (id) {
+			formLoading.value = true
+			try {
+				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)
+					} else {
+						// 回显价格分转元
+						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
+			} finally {
+				formLoading.value = false
+			}
+		}
+	}
+	const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
+	/** 添加到仓库 / 回收站的状态 */
+	const handleStatus02Change = async (newStatus : number) => {
+		try {
+			// 二次确认
+			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()
+			emit('success')
+		} catch { }
+	}
+	/** 删除按钮操作 */
+	const handleDelete = async (id : number) => {
+		try {
+			// 删除的二次确认
+			await message.delConfirm()
+			// 发起删除
+			await ProductSpuApi.deleteSpu(id)
+			message.success(t('common.delSuccess'))
+			close()
+			emit('success')
+		} catch { }
+	}
+	/** 更新上架/下架状态 */
+	const handleStatusChange = async () => {
+		console.log(parentRow.value.status)
+		try {
+			// 二次确认
+			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 })
+			message.success(text + '成功')
+			close()
+			emit('success')
+		} catch {
+
+		}
+	}
+	/** 提交按钮 */
+	const submitForm = async () => {
+		// 提交请求
+		formLoading.value = true
+		try {
+			// 校验各表单
+			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
+			const id = productId.value as any as number
+			if (!id) {
+				await ProductSpuApi.createSpu(data)
+				message.success(t('common.createSuccess'))
+			} else {
+				await ProductSpuApi.updateSpu(data)
+				message.success(t('common.updateSuccess'))
+			}
+			close()
+			emit('success')
+		} finally {
+			formLoading.value = false
+		}
+	}
+
+	/** 关闭按钮 */
+	const close = () => {
+		dialogVisible.value = false
+	}
+</script>
+<style type="text/css">
+	.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 {
+		padding: 0;
+		margin: 0
+	}
+</style>
+<style lang="scss" scoped>
+	::v-deep .left {
+		width: 106px;
+		float: left;
+
+		img {
+			// 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 {
+		width: 106px;
+		justify-content: center;
+	}
+
+	::v-deep .child-tabs .el-tabs__item {}
+
+	.right {
+		padding: 10px 0 60px;
+		border-left: 2px solid #e4e7ee;
+		margin-left: -2px;
+		float: left;
+		width: calc(100% - 120px);
+	}
+
+
+
+	.my-header {
+		display: flex;
+		justify-content: space-between;
+		align-items: center;
+
+		&-left {
+			font-weight: bold;
+			font-size: 18px;
+			padding: 20px;
+			padding-bottom: 10px
+		}
+
+		&-right {
+			span {
+				width: 55px;
+				height: 55px;
+				display: inline-block;
+				line-height: 55px;
+				text-align: center;
+				cursor: pointer;
+			}
+
+			span:first-child:hover {
+				background-color: #f6f6f6;
+				// color: white
+			}
+
+			span:last-child:hover {
+				background-color: #ef6b6b;
+				color: white
+			}
+		}
+	}
+</style>

+ 535 - 449
src/views/mall/product/spu/index.vue

@@ -1,449 +1,535 @@
-<!-- 商品中心 - 商品列表  -->
-<template>
-  <!-- 搜索工作栏 -->
-  <ContentWrap>
-    <el-form
-      ref="queryFormRef"
-      :inline="true"
-      :model="queryParams"
-      class="-mb-15px"
-      label-width="68px"
-    >
-      <el-form-item label="商品名称" prop="name">
-        <el-input
-          v-model="queryParams.name"
-          class="!w-240px"
-          clearable
-          placeholder="请输入商品名称"
-          @keyup.enter="handleQuery"
-        />
-      </el-form-item>
-      <el-form-item label="商品分类" prop="categoryId">
-        <el-cascader
-          v-model="queryParams.categoryId"
-          :options="categoryList"
-          :props="defaultProps"
-          class="w-1/1"
-          clearable
-          filterable
-          placeholder="请选择商品分类"
-        />
-      </el-form-item>
-      <el-form-item label="创建时间" prop="createTime">
-        <el-date-picker
-          v-model="queryParams.createTime"
-          :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
-          class="!w-240px"
-          end-placeholder="结束日期"
-          start-placeholder="开始日期"
-          type="daterange"
-          value-format="YYYY-MM-DD HH:mm:ss"
-        />
-      </el-form-item>
-      <el-form-item>
-        <el-button @click="handleQuery">
-          <Icon class="mr-5px" icon="ep:search" />
-          搜索
-        </el-button>
-        <el-button @click="resetQuery">
-          <Icon class="mr-5px" icon="ep:refresh" />
-          重置
-        </el-button>
-        <el-button
-          v-hasPermi="['product:spu:create']"
-          plain
-          type="primary"
-          @click="openForm(undefined)"
-        >
-          <Icon class="mr-5px" icon="ep:plus" />
-          新增
-        </el-button>
-        <el-button
-          v-hasPermi="['product:spu:export']"
-          :loading="exportLoading"
-          plain
-          type="success"
-          @click="handleExport"
-        >
-          <Icon class="mr-5px" icon="ep:download" />
-          导出
-        </el-button>
-      </el-form-item>
-    </el-form>
-  </ContentWrap>
-
-  <!-- 列表 -->
-  <ContentWrap>
-    <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-tabs>
-    <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-row>
-              <el-col :span="24">
-                <el-row>
-                  <el-col :span="8">
-                    <el-form-item label="商品分类:">
-                      <span>{{ formatCategoryName(row.categoryId) }}</span>
-                    </el-form-item>
-                  </el-col>
-                  <el-col :span="8">
-                    <el-form-item label="市场价:">
-                      <span>{{ fenToYuan(row.marketPrice) }}</span>
-                    </el-form-item>
-                  </el-col>
-                  <el-col :span="8">
-                    <el-form-item label="成本价:">
-                      <span>{{ fenToYuan(row.costPrice) }}</span>
-                    </el-form-item>
-                  </el-col>
-                </el-row>
-              </el-col>
-            </el-row>
-            <el-row>
-              <el-col :span="24">
-                <el-row>
-                  <el-col :span="8">
-                    <el-form-item label="浏览量:">
-                      <span>{{ row.browseCount }}</span>
-                    </el-form-item>
-                  </el-col>
-                  <el-col :span="8">
-                    <el-form-item label="虚拟销量:">
-                      <span>{{ row.virtualSalesCount }}</span>
-                    </el-form-item>
-                  </el-col>
-                </el-row>
-              </el-col>
-            </el-row>
-          </el-form>
-        </template>
-      </el-table-column>
-      <el-table-column label="商品编号" min-width="140" prop="id" />
-      <el-table-column label="商品信息" min-width="300">
-        <template #default="{ row }">
-          <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">
-                <div>
-                  {{ row.name }}
-                </div>
-              </el-tooltip>
-            </div>
-          </div>
-        </template>
-      </el-table-column>
-      <el-table-column align="center" label="价格" min-width="160" prop="price">
-        <template #default="{ row }"> ¥ {{ fenToYuan(row.price) }}</template>
-      </el-table-column>
-      <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 #default="{ row }">
-          <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>
-          <template v-else>
-            <el-tag type="info">回收站</el-tag>
-          </template>
-        </template>
-      </el-table-column>
-      <el-table-column
-        :formatter="dateFormatter"
-        align="center"
-        label="创建时间"
-        prop="createTime"
-        width="180"
-      />
-      <el-table-column align="center" fixed="right" label="操作" min-width="200">
-        <template #default="{ row }">
-          <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)"
-          >
-            修改
-          </el-button>
-          <template v-if="queryParams.tabType === 4">
-            <el-button
-              v-hasPermi="['product:spu:delete']"
-              link
-              type="danger"
-              @click="handleDelete(row.id)"
-            >
-              删除
-            </el-button>
-            <el-button
-              v-hasPermi="['product:spu:update']"
-              link
-              type="primary"
-              @click="handleStatus02Change(row, ProductSpuStatusEnum.DISABLE.status)"
-            >
-              恢复
-            </el-button>
-          </template>
-          <template v-else>
-            <el-button
-              v-hasPermi="['product:spu:update']"
-              link
-              type="danger"
-              @click="handleStatus02Change(row, ProductSpuStatusEnum.RECYCLE.status)"
-            >
-              回收
-            </el-button>
-          </template>
-        </template>
-      </el-table-column>
-    </el-table>
-    <!-- 分页 -->
-    <Pagination
-      v-model:limit="queryParams.pageSize"
-      v-model:page="queryParams.pageNo"
-      :total="total"
-      @pagination="getList"
-    />
-  </ContentWrap>
-</template>
-<script lang="ts" setup>
-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'
-import * as ProductSpuApi from '@/api/mall/product/spu'
-import * as ProductCategoryApi from '@/api/mall/product/category'
-
-defineOptions({ name: 'ProductSpu' })
-
-const message = useMessage() // 消息弹窗
-const { t } = useI18n() // 国际化
-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,
-    count: 0
-  },
-  {
-    name: '已售罄',
-    type: 2,
-    count: 0
-  },
-  {
-    name: '警戒库存',
-    type: 3,
-    count: 0
-  },
-  {
-    name: '回收站',
-    type: 4,
-    count: 0
-  }
-])
-
-const queryParams = ref({
-  pageNo: 1,
-  pageSize: 10,
-  tabType: 0,
-  name: '',
-  categoryId: undefined,
-  createTime: undefined
-}) // 查询参数
-const queryFormRef = ref() // 搜索的表单Ref
-
-/** 查询列表 */
-const getList = async () => {
-  loading.value = true
-  try {
-    const data = await ProductSpuApi.getSpuPage(queryParams.value)
-    list.value = data.list
-    total.value = data.total
-  } finally {
-    loading.value = false
-  }
-}
-
-/** 切换 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) => {
-  try {
-    // 二次确认
-    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) => {
-  try {
-    // 二次确认
-    const text = row.status ? '上架' : '下架'
-    await message.confirm(`确认要${text}"${row.name}"吗?`)
-    // 发起修改
-    await ProductSpuApi.updateStatus({ id: row.id, status: row.status })
-    message.success(text + '成功')
-    // 刷新 tabs 数据
-    await getTabsCount()
-    // 刷新列表
-    await getList()
-  } catch {
-    // 异常时,需要重置回之前的值
-    row.status =
-      row.status === ProductSpuStatusEnum.DISABLE.status
-        ? ProductSpuStatusEnum.ENABLE.status
-        : ProductSpuStatusEnum.DISABLE.status
-  }
-}
-
-/** 删除按钮操作 */
-const handleDelete = async (id: number) => {
-  try {
-    // 删除的二次确认
-    await message.delConfirm()
-    // 发起删除
-    await ProductSpuApi.deleteSpu(id)
-    message.success(t('common.delSuccess'))
-    // 刷新tabs数据
-    await getTabsCount()
-    // 刷新列表
-    await getList()
-  } catch {}
-}
-
-/** 商品图预览 */
-const imagePreview = (imgUrl: string) => {
-  createImageViewer({
-    urlList: [imgUrl]
-  })
-}
-
-/** 搜索按钮操作 */
-const handleQuery = () => {
-  getList()
-}
-
-/** 重置按钮操作 */
-const resetQuery = () => {
-  queryFormRef.value.resetFields()
-  handleQuery()
-}
-
-/** 新增或修改 */
-const openForm = (id?: number) => {
-  // 修改
-  if (typeof id === 'number') {
-    push({ name: 'ProductSpuEdit', params: { id } })
-    return
-  }
-  // 新增
-  push({ name: 'ProductSpuAdd' })
-}
-
-/** 查看商品详情 */
-const openDetail = (id: number) => {
-  push({ name: 'ProductSpuDetail', params: { id } })
-}
-
-/** 导出按钮操作 */
-const handleExport = async () => {
-  try {
-    // 导出的二次确认
-    await message.exportConfirm()
-    // 发起导出
-    exportLoading.value = true
-    const data = await ProductSpuApi.exportSpu(queryParams)
-    download.excel(data, '商品列表.xls')
-  } catch {
-  } finally {
-    exportLoading.value = false
-  }
-}
-
-/** 获取分类的节点的完整结构 */
-const categoryList = ref() // 分类树
-const formatCategoryName = (categoryId: number) => {
-  return treeToString(categoryList.value, categoryId)
-}
-
-/** 激活时 */
-onActivated(() => {
-  getList()
-})
-
-/** 初始化 **/
-onMounted(async () => {
-  await getTabsCount()
-  await getList()
-  // 获得分类树
-  const data = await ProductCategoryApi.getCategoryList({})
-  categoryList.value = handleTree(data, 'id', 'parentId')
-})
-</script>
-<style lang="scss" scoped>
-.spu-table-expand {
-  padding-left: 42px;
-
-  :deep(.el-form-item__label) {
-    width: 82px;
-    font-weight: bold;
-    color: #99a9bf;
-  }
-}
-</style>
+<!-- 商品中心 - 商品列表  -->
+<template>
+	<!-- 列表 -->
+	<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 />
+						</el-icon>
+					</template>
+					<template #append>
+						<el-button :icon="Operation" @click="showSearchMore" />
+					</template>
+
+				</el-input>
+				&nbsp;
+				<el-button v-hasPermi="['product:spu:create']" @click="openForm('create','',0)">
+					<Icon class="mr-5px" icon="ep:plus" />
+					新增
+				</el-button>
+			</div>
+			<div class="searchMore" v-if="searchMoreShow">
+				<el-form ref="queryFormRef" :inline="true" :model="queryParams" class="-mb-15px" label-width="68px">
+					<div>
+						<el-form-item label="商品分类" prop="categoryId">
+							<el-cascader v-model="queryParams.categoryId" :options="categoryList" :props="defaultProps"
+								class="w-1/1" clearable filterable placeholder="请选择商品分类" />
+						</el-form-item>
+						<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-form-item>
+					</div>
+					<div>
+						<el-button @click="hideSearchMore">
+							收起
+						</el-button>
+						<el-button @click="resetQuery">
+							重置
+						</el-button>
+						<el-button @click="handleQuery" plain type="primary">
+							<Icon class="mr-5px" icon="ep:search" />
+							搜索
+						</el-button>
+						<!-- <el-button v-hasPermi="['product:spu:export']" :loading="exportLoading" plain type="success"
+								@click="handleExport">
+								<Icon class="mr-5px" icon="ep:download" />
+								导出
+							</el-button> -->
+					</div>
+				</el-form>
+			</div>
+		</div>
+		<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-tabs>
+		<el-row>
+			<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}}
+						</p>
+						<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 />
+							</el-icon>
+						</div>
+
+
+					</div>
+					<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>
+
+						<div style='line-height: 25px;'>
+							<p>¥{{ fenToYuan(o.price) }}</p>
+							<p>销量:{{o.salesCount}}</p>
+							<p>库存:{{o.stock}}</p>
+							<p>状态:{{o.status == 1?"上架":"下架"}}</p>
+						</div>
+					</div>
+				</el-card>
+			</el-col>
+		</el-row>
+		<div v-if="list.length == 0">
+			<p style="text-align: center;margin-top: 100px;">暂无商品</p>
+		</div>
+		<!--<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-row>
+							<el-col :span="24">
+								<el-row>
+									<el-col :span="8">
+										<el-form-item label="商品分类:">
+											<span>{{ formatCategoryName(row.categoryId) }}</span>
+										</el-form-item>
+									</el-col>
+									<el-col :span="8">
+										<el-form-item label="市场价:">
+											<span>{{ fenToYuan(row.marketPrice) }}</span>
+										</el-form-item>
+									</el-col>
+									<el-col :span="8">
+										<el-form-item label="成本价:">
+											<span>{{ fenToYuan(row.costPrice) }}</span>
+										</el-form-item>
+									</el-col>
+								</el-row>
+							</el-col>
+						</el-row>
+						<el-row>
+							<el-col :span="24">
+								<el-row>
+									<el-col :span="8">
+										<el-form-item label="浏览量:">
+											<span>{{ row.browseCount }}</span>
+										</el-form-item>
+									</el-col>
+									<el-col :span="8">
+										<el-form-item label="虚拟销量:">
+											<span>{{ row.virtualSalesCount }}</span>
+										</el-form-item>
+									</el-col>
+								</el-row>
+							</el-col>
+						</el-row>
+					</el-form>
+				</template>
+			</el-table-column>
+			<el-table-column label="商品编号" min-width="140" prop="id" />
+			<el-table-column label="商品信息" min-width="300">
+				<template #default="{ row }">
+					<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">
+								<div>
+									{{ row.name }}
+								</div>
+							</el-tooltip>
+						</div>
+					</div>
+				</template>
+			</el-table-column>
+			<el-table-column align="center" label="价格" min-width="160" prop="price">
+				<template #default="{ row }"> ¥ {{ fenToYuan(row.price) }}</template>
+			</el-table-column>
+			<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 #default="{ row }">
+					<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>
+					<template v-else>
+						<el-tag type="info">回收站</el-tag>
+					</template>
+				</template>
+			</el-table-column>
+			<el-table-column :formatter="dateFormatter" align="center" label="创建时间" prop="createTime" width="180" />
+			<el-table-column align="center" fixed="right" label="操作" min-width="200">
+				<template #default="{ row }">
+					<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)">
+						修改
+					</el-button>
+					<template v-if="queryParams.tabType === 4">
+						<el-button v-hasPermi="['product:spu:delete']" link type="danger" @click="handleDelete(row.id)">
+							删除
+						</el-button>
+						<el-button v-hasPermi="['product:spu:update']" link type="primary"
+							@click="handleStatus02Change(row, ProductSpuStatusEnum.DISABLE.status)">
+							恢复
+						</el-button>
+					</template>
+					<template v-else>
+						<el-button v-hasPermi="['product:spu:update']" link type="danger"
+							@click="handleStatus02Change(row, ProductSpuStatusEnum.RECYCLE.status)">
+							回收
+						</el-button>
+					</template>
+				</template>
+			</el-table-column>
+		</el-table>-->
+
+		<!-- 分页 -->
+		<Pagination v-model:limit="queryParams.pageSize" v-model:page="queryParams.pageNo" :total="total"
+			@pagination="getList" />
+		<!-- 表单弹窗:添加/修改 -->
+		<SpuIndex ref="formRef" @success="getList" />
+	</ContentWrap>
+</template>
+<script lang="ts" setup>
+	import { TabsPaneContext } from 'element-plus'
+	import { Setting, Search, Operation } from '@element-plus/icons-vue'
+	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'
+	import * as ProductSpuApi from '@/api/mall/product/spu'
+	import * as ProductCategoryApi from '@/api/mall/product/category'
+	import SpuIndex from "./form/index.vue"
+
+	defineOptions({ name: 'ProductSpu' })
+
+	const message = useMessage() // 消息弹窗
+	const { t } = useI18n() // 国际化
+	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,
+			count: 0
+		},
+		{
+			name: '已售罄',
+			type: 2,
+			count: 0
+		},
+		{
+			name: '警戒库存',
+			type: 3,
+			count: 0
+		},
+		{
+			name: '回收站',
+			type: 4,
+			count: 0
+		}
+	])
+
+	const queryParams = ref({
+		pageNo: 1,
+		pageSize: 10,
+		tabType: 0,
+		name: '',
+		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 handleError = (e) => {
+		e.target.src = "https://static.iocoder.cn/mall/a79f5d2ea6bf0c3c11b2127332dfe2df.jpg"
+	}
+	// 打开更多搜索条件
+	const showSearchMore = () => {
+		searchMoreShow.value = !searchMoreShow.value
+	}
+	const hideSearchMore = () => {
+		searchMoreShow.value = false
+	}
+
+	/** 查询列表 */
+	const getList = async () => {
+		loading.value = true
+		try {
+			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
+
+		} finally {
+			loading.value = false
+			searchMoreShow.value = false
+			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) => {
+		try {
+			// 二次确认
+			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) => {
+		try {
+			// 二次确认
+			const text = row.status ? '上架' : '下架'
+			await message.confirm(`确认要${text}"${row.name}"吗?`)
+			// 发起修改
+			await ProductSpuApi.updateStatus({ id: row.id, status: row.status })
+			message.success(text + '成功')
+			// 刷新 tabs 数据
+			await getTabsCount()
+			// 刷新列表
+			await getList()
+		} catch {
+			// 异常时,需要重置回之前的值
+			row.status =
+				row.status === ProductSpuStatusEnum.DISABLE.status
+					? ProductSpuStatusEnum.ENABLE.status
+					: ProductSpuStatusEnum.DISABLE.status
+		}
+	}
+
+	/** 删除按钮操作 */
+	const handleDelete = async (id : number) => {
+		try {
+			// 删除的二次确认
+			await message.delConfirm()
+			// 发起删除
+			await ProductSpuApi.deleteSpu(id)
+			message.success(t('common.delSuccess'))
+			// 刷新tabs数据
+			await getTabsCount()
+			// 刷新列表
+			await getList()
+		} catch { }
+	}
+
+	/** 商品图预览 */
+	const imagePreview = (imgUrl : string) => {
+		createImageViewer({
+			urlList: [imgUrl]
+		})
+	}
+
+	/** 搜索按钮操作 */
+	const handleQuery = () => {
+		getList()
+	}
+
+	/** 重置按钮操作 */
+	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 () => {
+		try {
+			// 导出的二次确认
+			await message.exportConfirm()
+			// 发起导出
+			exportLoading.value = true
+			const data = await ProductSpuApi.exportSpu(queryParams)
+			download.excel(data, '商品列表.xls')
+		} catch {
+		} finally {
+			exportLoading.value = false
+		}
+	}
+
+	/** 获取分类的节点的完整结构 */
+	const categoryList = ref() // 分类树
+	const formatCategoryName = (categoryId : number) => {
+		return treeToString(categoryList.value, categoryId)
+	}
+
+	/** 激活时 */
+	onActivated(() => {
+		getList()
+	})
+
+	/** 初始化 **/
+	onMounted(async () => {
+		await getTabsCount()
+		await getList()
+		// 获得分类树
+		const data = await ProductCategoryApi.getCategoryList({})
+		categoryList.value = handleTree(data, 'id', 'parentId')
+	})
+</script>
+<style lang="scss" scoped>
+	.spu-div {
+		position: relative;
+		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;
+		padding: 10px;
+		position: absolute;
+		height: 100px;
+		width: calc(100% - 20px);
+		z-index: 111;
+		box-shadow: 0px 9px 6px rgba(0, 0, 0, 0.2);
+
+
+	}
+
+	.el-tabs {
+		position: absolute;
+		z-index: 111;
+		top: 25px;
+		left: 40px;
+	}
+
+	.product-card {
+
+		font-size: 14px;
+		margin: 10px;
+
+		.el-card {
+			padding: 15px;
+			cursor: pointer;
+			position: relative;
+
+			:deep(.el-card__body) {
+				padding: unset;
+			}
+		}
+
+		.setting {
+			width: 30px;
+			top: 0px;
+			right: 0px;
+			position: absolute;
+			height: 30px;
+			line-height: 30px;
+			text-align: center;
+			display: flex;
+			align-items: center;
+			justify-content: center;
+		}
+
+		.setting:hover {
+			background: #666
+		}
+
+		p,
+		div {
+			margin: unset;
+			padding: unset;
+		}
+	}
+
+	.spu-table-expand {
+		padding-left: 42px;
+
+		:deep(.el-form-item__label) {
+			width: 82px;
+			font-weight: bold;
+			color: #99a9bf;
+		}
+	}
+</style>

+ 101 - 99
src/views/mall/trade/order/form/OrderDeliveryForm.vue

@@ -1,99 +1,101 @@
-<template>
-  <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>
-        </el-radio-group>
-      </el-form-item>
-      <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-select>
-        </el-form-item>
-        <el-form-item label="物流单号">
-          <el-input v-model="formData.logisticsNo" />
-        </el-form-item>
-      </template>
-    </el-form>
-    <template #footer>
-      <el-button :disabled="formLoading" type="primary" @click="submitForm">确 定</el-button>
-      <el-button @click="dialogVisible = false">取 消</el-button>
-    </template>
-  </Dialog>
-</template>
-<script lang="ts" setup>
-import * as DeliveryExpressApi from '@/api/mall/trade/delivery/express'
-import * as TradeOrderApi from '@/api/mall/trade/order'
-import { copyValueToTarget } from '@/utils'
-
-defineOptions({ name: 'OrderDeliveryForm' })
-
-const { t } = useI18n() // 国际化
-const message = useMessage() // 消息弹窗
-
-const dialogVisible = ref(false) // 弹窗的是否展示
-const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
-const expressType = ref('express') // 如果值是 express,则是快递;none 则是无;未来做同城配送;
-const formData = ref<TradeOrderApi.DeliveryVO>({
-  id: 0, // 订单编号
-  logisticsId: null, // 物流公司编号
-  logisticsNo: '' // 物流编号
-})
-const formRef = ref() // 表单 Ref
-
-/** 打开弹窗 */
-const open = async (row: TradeOrderApi.OrderVO) => {
-  resetForm()
-  // 设置数据
-  copyValueToTarget(formData.value, row)
-  if (row.logisticsId === 0) {
-    expressType.value = 'none'
-  }
-  dialogVisible.value = true
-}
-defineExpose({ open }) // 提供 open 方法,用于打开弹窗
-
-/** 提交表单 */
-const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
-const submitForm = async () => {
-  // 提交请求
-  formLoading.value = true
-  try {
-    const data = unref(formData)
-    if (expressType.value === 'none') {
-      // 无需发货的情况
-      data.logisticsId = 0
-      data.logisticsNo = ''
-    }
-    await TradeOrderApi.deliveryOrder(data)
-    message.success(t('common.updateSuccess'))
-    dialogVisible.value = false
-    // 发送操作成功的事件
-    emit('success', true)
-  } finally {
-    formLoading.value = false
-  }
-}
-
-/** 重置表单 */
-const resetForm = () => {
-  formData.value = {
-    id: 0, // 订单编号
-    logisticsId: null, // 物流公司编号
-    logisticsNo: '' // 物流编号
-  }
-  formRef.value?.resetFields()
-}
-const deliveryExpressList = ref([])
-onMounted(async () => {
-  deliveryExpressList.value = await DeliveryExpressApi.getSimpleDeliveryExpressList()
-})
-</script>
+<template>
+	<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>
+				</el-radio-group>
+			</el-form-item>
+			<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-select>
+				</el-form-item>
+				<el-form-item label="物流单号">
+					<el-input v-model="formData.logisticsNo" />
+				</el-form-item>
+			</template>
+		</el-form>
+		<template #footer>
+			<el-button :disabled="formLoading" type="primary" @click="submitForm">确 定</el-button>
+			<el-button @click="dialogVisible = false">取 消</el-button>
+		</template>
+	</Dialog>
+</template>
+<script lang="ts" setup>
+	import * as DeliveryExpressApi from '@/api/mall/trade/delivery/express'
+	import * as TradeOrderApi from '@/api/mall/trade/order'
+	import { copyValueToTarget } from '@/utils'
+
+	defineOptions({ name: 'OrderDeliveryForm' })
+
+	const { t } = useI18n() // 国际化
+	const message = useMessage() // 消息弹窗
+
+	const dialogVisible = ref(false) // 弹窗的是否展示
+	const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
+	const expressType = ref('express') // 如果值是 express,则是快递;none 则是无;未来做同城配送;
+	const formData = ref<TradeOrderApi.DeliveryVO>({
+		id: 0, // 订单编号
+		logisticsId: null, // 物流公司编号
+		logisticsNo: '' // 物流编号
+	})
+	const formRef = ref() // 表单 Ref
+
+	/** 打开弹窗 */
+	const open = async (row : TradeOrderApi.OrderVO, openType : any, orderId : any) => {
+
+		resetForm()
+		// 设置数据
+		copyValueToTarget(formData.value, row)
+		//如果是首页待办点进来的,即点进来将id设为从待办传来的id
+		if (openType == "待办") {
+			formData.value.id = orderId
+		}
+
+		if (row.logisticsId === 0) {
+			expressType.value = 'none'
+		}
+		dialogVisible.value = true
+	}
+	defineExpose({ open }) // 提供 open 方法,用于打开弹窗
+
+	/** 提交表单 */
+	const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
+	const submitForm = async () => {
+		// 提交请求
+		formLoading.value = true
+		try {
+			const data = unref(formData)
+			if (expressType.value === 'none') {
+				// 无需发货的情况
+				data.logisticsId = 0
+				data.logisticsNo = ''
+			}
+			await TradeOrderApi.deliveryOrder(data)
+			message.success(t('common.updateSuccess'))
+			dialogVisible.value = false
+			// 发送操作成功的事件
+			emit('success', true)
+		} finally {
+			formLoading.value = false
+		}
+	}
+
+	/** 重置表单 */
+	const resetForm = () => {
+		formData.value = {
+			id: 0, // 订单编号
+			logisticsId: null, // 物流公司编号
+			logisticsNo: '' // 物流编号
+		}
+		formRef.value?.resetFields()
+	}
+	const deliveryExpressList = ref([])
+	onMounted(async () => {
+		deliveryExpressList.value = await DeliveryExpressApi.getSimpleDeliveryExpressList()
+	})
+</script>

+ 260 - 354
src/views/mall/trade/order/index.vue

@@ -1,354 +1,260 @@
-<template>
-  <!-- 搜索 -->
-  <ContentWrap>
-    <el-form
-      ref="queryFormRef"
-      :inline="true"
-      :model="queryParams"
-      class="-mb-15px"
-      label-width="68px"
-    >
-      <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-select>
-      </el-form-item>
-      <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"
-            :label="dict.label"
-            :value="dict.value"
-          />
-        </el-select>
-      </el-form-item>
-      <el-form-item label="创建时间" prop="createTime">
-        <el-date-picker
-          v-model="queryParams.createTime"
-          :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
-          class="!w-280px"
-          end-placeholder="自定义时间"
-          start-placeholder="自定义时间"
-          type="daterange"
-          value-format="YYYY-MM-DD HH:mm:ss"
-        />
-      </el-form-item>
-      <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"
-            :label="dict.label"
-            :value="dict.value"
-          />
-        </el-select>
-      </el-form-item>
-      <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"
-            :label="dict.label"
-            :value="dict.value"
-          />
-        </el-select>
-      </el-form-item>
-      <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"
-            :label="dict.label"
-            :value="dict.value"
-          />
-        </el-select>
-      </el-form-item>
-      <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-select>
-      </el-form-item>
-      <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-select>
-      </el-form-item>
-      <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>
-      <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-select>
-          </template>
-        </el-input>
-      </el-form-item>
-      <el-form-item>
-        <el-button @click="handleQuery">
-          <Icon class="mr-5px" icon="ep:search" />
-          搜索
-        </el-button>
-        <el-button @click="resetQuery">
-          <Icon class="mr-5px" icon="ep:refresh" />
-          重置
-        </el-button>
-      </el-form-item>
-    </el-form>
-  </ContentWrap>
-
-  <!-- 列表 -->
-  <ContentWrap>
-    <!-- 添加 row-key="id" 解决列数据中的 table#header 数据不刷新的问题  -->
-    <el-table v-loading="loading" :data="list" row-key="id">
-      <OrderTableColumn :list="list" :pick-up-store-list="pickUpStoreList">
-        <template #default="{ row }">
-          <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-button>
-            <el-dropdown
-              v-hasPermi="['trade:order:update']"
-              @command="(command) => handleCommand(command, row)"
-            >
-              <el-button link type="primary">
-                <Icon icon="ep:d-arrow-right" />
-                更多
-              </el-button>
-              <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-item>
-                </el-dropdown-menu>
-              </template>
-            </el-dropdown>
-          </div>
-        </template>
-      </OrderTableColumn>
-    </el-table>
-    <!-- 分页 -->
-    <Pagination
-      v-model:limit="queryParams.pageSize"
-      v-model:page="queryParams.pageNo"
-      :total="total"
-      @pagination="getList"
-    />
-  </ContentWrap>
-
-  <!-- 各种操作的弹窗 -->
-  <OrderDeliveryForm ref="deliveryFormRef" @success="getList" />
-  <OrderUpdateRemarkForm ref="updateRemarkForm" @success="getList" />
-</template>
-
-<script lang="ts" setup>
-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 TradeOrderApi from '@/api/mall/trade/order'
-import * as PickUpStoreApi from '@/api/mall/trade/delivery/pickUpStore'
-import { DICT_TYPE, getIntDictOptions, getStrDictOptions } from '@/utils/dict'
-import * as DeliveryExpressApi from '@/api/mall/trade/delivery/express'
-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>() // 搜索的表单
-// 表单搜索
-const queryParams = ref({
-  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 getList = async () => {
-  loading.value = true
-  try {
-    const data = await TradeOrderApi.getOrderPage(unref(queryParams))
-    list.value = data.list
-    total.value = data.total
-  } finally {
-    loading.value = false
-  }
-}
-
-/** 搜索按钮操作 */
-const handleQuery = async () => {
-  queryParams.value.pageNo = 1
-  await getList()
-}
-
-/** 重置按钮操作 */
-const resetQuery = () => {
-  queryFormRef.value?.resetFields()
-  queryParams.value = {
-    pageNo: 1, // 页数
-    pageSize: 10, // 每页显示数量
-    status: undefined, // 订单状态
-    payChannelCode: undefined, // 支付方式
-    createTime: undefined, // 创建时间
-    terminal: undefined, // 订单来源
-    type: undefined, // 订单类型
-    deliveryType: undefined, // 配送方式
-    logisticsId: undefined, // 快递公司
-    pickUpStoreId: undefined, // 自提门店
-    pickUpVerifyCode: undefined // 自提核销码
-  }
-  handleQuery()
-}
-
-/** 查看订单详情 */
-const openDetail = (id: number) => {
-  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)
-      break
-  }
-}
-
-// 监听路由变化更新列表,解决订单保存/更新后,列表不刷新的问题。
-watch(
-  () => currentRoute.value,
-  () => {
-    getList()
-  }
-)
-
-const pickUpStoreList = ref<PickUpStoreApi.DeliveryPickUpStoreVO[]>([]) // 自提门店精简列表
-const deliveryExpressList = ref<DeliveryExpressApi.DeliveryExpressVO[]>([]) // 物流公司
-/** 初始化 **/
-onMounted(async () => {
-  await getList()
-  pickUpStoreList.value = await PickUpStoreApi.getListAllSimple()
-  deliveryExpressList.value = await DeliveryExpressApi.getSimpleDeliveryExpressList()
-})
-</script>
+<template>
+	<!-- 搜索 -->
+	<ContentWrap>
+		<el-form ref="queryFormRef" :inline="true" :model="queryParams" class="-mb-15px" label-width="68px">
+			<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-select>
+			</el-form-item>
+			<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"
+						:label="dict.label" :value="dict.value" />
+				</el-select>
+			</el-form-item>
+			<el-form-item label="创建时间" prop="createTime">
+				<el-date-picker v-model="queryParams.createTime"
+					:default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]" class="!w-280px"
+					end-placeholder="自定义时间" start-placeholder="自定义时间" type="daterange"
+					value-format="YYYY-MM-DD HH:mm:ss" />
+			</el-form-item>
+			<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"
+						:label="dict.label" :value="dict.value" />
+				</el-select>
+			</el-form-item>
+			<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"
+						:label="dict.label" :value="dict.value" />
+				</el-select>
+			</el-form-item>
+			<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"
+						:label="dict.label" :value="dict.value" />
+				</el-select>
+			</el-form-item>
+			<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-select>
+			</el-form-item>
+			<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-select>
+			</el-form-item>
+			<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>
+			<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-select>
+					</template>
+				</el-input>
+			</el-form-item>
+			<el-form-item>
+				<el-button @click="handleQuery">
+					<Icon class="mr-5px" icon="ep:search" />
+					搜索
+				</el-button>
+				<el-button @click="resetQuery">
+					<Icon class="mr-5px" icon="ep:refresh" />
+					重置
+				</el-button>
+			</el-form-item>
+		</el-form>
+	</ContentWrap>
+
+	<!-- 列表 -->
+	<ContentWrap>
+		<!-- 添加 row-key="id" 解决列数据中的 table#header 数据不刷新的问题  -->
+		<el-table v-loading="loading" :data="list" row-key="id">
+			<OrderTableColumn :list="list" :pick-up-store-list="pickUpStoreList">
+				<template #default="{ row }">
+					<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-button>
+						<el-dropdown v-hasPermi="['trade:order:update']"
+							@command="(command) => handleCommand(command, row)">
+							<el-button link type="primary">
+								<Icon icon="ep:d-arrow-right" />
+								更多
+							</el-button>
+							<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-item>
+								</el-dropdown-menu>
+							</template>
+						</el-dropdown>
+					</div>
+				</template>
+			</OrderTableColumn>
+		</el-table>
+		<!-- 分页 -->
+		<Pagination v-model:limit="queryParams.pageSize" v-model:page="queryParams.pageNo" :total="total"
+			@pagination="getList" />
+	</ContentWrap>
+
+	<!-- 各种操作的弹窗 -->
+	<OrderDeliveryForm ref="deliveryFormRef" @success="getList" />
+	<OrderUpdateRemarkForm ref="updateRemarkForm" @success="getList" />
+</template>
+
+<script lang="ts" setup>
+	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 TradeOrderApi from '@/api/mall/trade/order'
+	import * as PickUpStoreApi from '@/api/mall/trade/delivery/pickUpStore'
+	import { DICT_TYPE, getIntDictOptions, getStrDictOptions } from '@/utils/dict'
+	import * as DeliveryExpressApi from '@/api/mall/trade/delivery/express'
+	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>() // 搜索的表单
+	// 表单搜索
+	const queryParams = ref({
+		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 getList = async () => {
+		loading.value = true
+		try {
+			const data = await TradeOrderApi.getOrderPage(unref(queryParams))
+			list.value = data.list
+			total.value = data.total
+		} finally {
+			loading.value = false
+		}
+	}
+
+	/** 搜索按钮操作 */
+	const handleQuery = async () => {
+		queryParams.value.pageNo = 1
+		await getList()
+	}
+
+	/** 重置按钮操作 */
+	const resetQuery = () => {
+		queryFormRef.value?.resetFields()
+		queryParams.value = {
+			pageNo: 1, // 页数
+			pageSize: 10, // 每页显示数量
+			status: undefined, // 订单状态
+			payChannelCode: undefined, // 支付方式
+			createTime: undefined, // 创建时间
+			terminal: undefined, // 订单来源
+			type: undefined, // 订单类型
+			deliveryType: undefined, // 配送方式
+			logisticsId: undefined, // 快递公司
+			pickUpStoreId: undefined, // 自提门店
+			pickUpVerifyCode: undefined // 自提核销码
+		}
+		handleQuery()
+	}
+
+	/** 查看订单详情 */
+	const openDetail = (id : number) => {
+		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)
+				break
+		}
+	}
+
+	// 监听路由变化更新列表,解决订单保存/更新后,列表不刷新的问题。
+	watch(
+		() => currentRoute.value,
+		() => {
+			getList()
+		}
+	)
+
+	const pickUpStoreList = ref<PickUpStoreApi.DeliveryPickUpStoreVO[]>([]) // 自提门店精简列表
+	const deliveryExpressList = ref<DeliveryExpressApi.DeliveryExpressVO[]>([]) // 物流公司
+	/** 初始化 **/
+	onMounted(async () => {
+		await getList()
+		pickUpStoreList.value = await PickUpStoreApi.getListAllSimple()
+		deliveryExpressList.value = await DeliveryExpressApi.getSimpleDeliveryExpressList()
+	})
+</script>

+ 123 - 132
src/views/member/signin/config/SignInConfigForm.vue

@@ -1,132 +1,123 @@
-<template>
-  <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="7" :precision="0" />
-        <el-text class="mx-1" style="margin-left: 10px" type="danger">
-          只允许设置 1-7,默认签到 7 天为一个周期
-        </el-text>
-      </el-form-item>
-      <el-form-item label="奖励积分" prop="point">
-        <el-input-number v-model="formData.point" :min="0" :precision="0" />
-      </el-form-item>
-      <el-form-item label="奖励经验" prop="experience">
-        <el-input-number v-model="formData.experience" :min="0" :precision="0" />
-      </el-form-item>
-      <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-radio-group>
-      </el-form-item>
-    </el-form>
-    <template #footer>
-      <el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button>
-      <el-button @click="dialogVisible = false">取 消</el-button>
-    </template>
-  </Dialog>
-</template>
-<script lang="ts" setup>
-import * as SignInConfigApi from '@/api/member/signin/config'
-import { CommonStatusEnum } from '@/utils/constants'
-import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
-
-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<SignInConfigApi.SignInConfigVO>({} as SignInConfigApi.SignInConfigVO)
-// 奖励校验规则
-const awardValidator = (rule: any, _value: any, callback: any) => {
-  if (!formData.value.point && !formData.value.experience) {
-    callback(new Error('奖励积分与奖励经验至少配置一个'))
-    return
-  }
-
-  // 清除另一个字段的错误提示
-  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' },
-    { validator: awardValidator, trigger: 'blur' }
-  ]
-})
-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
-    try {
-      formData.value = await SignInConfigApi.getSignInConfig(id)
-    } finally {
-      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
-  // 提交请求
-  formLoading.value = true
-  try {
-    if (formType.value === 'create') {
-      await SignInConfigApi.createSignInConfig(formData.value)
-      message.success(t('common.createSuccess'))
-    } else {
-      await SignInConfigApi.updateSignInConfig(formData.value)
-      message.success(t('common.updateSuccess'))
-    }
-    dialogVisible.value = false
-    // 发送操作成功的事件
-    emit('success')
-  } finally {
-    formLoading.value = false
-  }
-}
-
-/** 重置表单 */
-const resetForm = () => {
-  formData.value = {
-    id: undefined,
-    day: undefined,
-    point: 0,
-    experience: 0,
-    status: CommonStatusEnum.ENABLE
-  }
-  formRef.value?.resetFields()
-}
-</script>
+<template>
+	<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>
+			<el-form-item label="奖励积分" prop="point">
+				<el-input-number v-model="formData.point" :min="0" :precision="0" />
+			</el-form-item>
+			<el-form-item label="奖励经验" prop="experience">
+				<el-input-number v-model="formData.experience" :min="0" :precision="0" />
+			</el-form-item>
+			<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-radio-group>
+			</el-form-item>
+		</el-form>
+		<template #footer>
+			<el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button>
+			<el-button @click="dialogVisible = false">取 消</el-button>
+		</template>
+	</Dialog>
+</template>
+<script lang="ts" setup>
+	import * as SignInConfigApi from '@/api/member/signin/config'
+	import { CommonStatusEnum } from '@/utils/constants'
+	import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+
+	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<SignInConfigApi.SignInConfigVO>({} as SignInConfigApi.SignInConfigVO)
+	// 奖励校验规则
+	const awardValidator = (rule : any, _value : any, callback : any) => {
+		if (!formData.value.point && !formData.value.experience) {
+			callback(new Error('奖励积分与奖励经验至少配置一个'))
+			return
+		}
+
+		// 清除另一个字段的错误提示
+		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' },
+			{ validator: awardValidator, trigger: 'blur' }
+		]
+	})
+	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
+			try {
+				formData.value = await SignInConfigApi.getSignInConfig(id)
+			} finally {
+				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
+		// 提交请求
+		formLoading.value = true
+		try {
+			if (formType.value === 'create') {
+				await SignInConfigApi.createSignInConfig(formData.value)
+				message.success(t('common.createSuccess'))
+			} else {
+				await SignInConfigApi.updateSignInConfig(formData.value)
+				message.success(t('common.updateSuccess'))
+			}
+			dialogVisible.value = false
+			// 发送操作成功的事件
+			emit('success')
+		} finally {
+			formLoading.value = false
+		}
+	}
+
+	/** 重置表单 */
+	const resetForm = () => {
+		formData.value = {
+			id: undefined,
+			day: undefined,
+			point: 0,
+			experience: 0,
+			status: CommonStatusEnum.ENABLE
+		}
+		formRef.value?.resetFields()
+	}
+</script>

+ 1 - 1
src/views/mp/components/wx-material-select/main.vue

@@ -1,7 +1,7 @@
 <!--
   - Copyright (C) 2018-2019
   - All rights reserved, Designed By www.joolun.com
-  芋道源码:
+  非繁源码:
   ① 移除 avue 组件,使用 ElementUI 原生组件
 -->
 <template>

+ 1 - 1
src/views/mp/components/wx-msg/main.vue

@@ -1,7 +1,7 @@
 <!--
   - Copyright (C) 2018-2019
   - All rights reserved, Designed By www.joolun.com
-  芋道源码:
+  非繁源码:
   ① 移除暂时用不到的 websocket
   ② 代码优化,补充注释,提升阅读性
 -->

+ 1 - 1
src/views/mp/components/wx-news/main.vue

@@ -2,7 +2,7 @@
   - Copyright (C) 2018-2019
   - All rights reserved, Designed By www.joolun.com
   【微信消息 - 图文】
-  芋道源码:
+  非繁源码:
   ① 代码优化,补充注释,提升阅读性
 -->
 <template>

+ 1 - 1
src/views/mp/components/wx-reply/main.vue

@@ -1,7 +1,7 @@
 <!--
   - Copyright (C) 2018-2019
   - All rights reserved, Designed By www.joolun.com
-  芋道源码:
+  非繁源码:
   ① 移除多余的 rep 为前缀的变量,让 message 消息更简单
   ② 代码优化,补充注释,提升阅读性
   ③ 优化消息的临时缓存策略,发送消息时,只清理被发送消息的 tab,不会强制切回到 text 输入

+ 1 - 1
src/views/mp/components/wx-video-play/main.vue

@@ -2,7 +2,7 @@
   - Copyright (C) 2018-2019
   - All rights reserved, Designed By www.joolun.com
   【微信消息 - 视频】
-  芋道源码:
+  非繁源码:
   ① bug 修复:
     1)joolun 的做法:使用 mediaId 从微信公众号,下载对应的 mp4 素材,从而播放内容;
       存在的问题:mediaId 有效期是 3 天,超过时间后无法播放

+ 1 - 1
src/views/mp/components/wx-voice-play/main.vue

@@ -2,7 +2,7 @@
   - Copyright (C) 2018-2019
   - All rights reserved, Designed By www.joolun.com
   【微信消息 - 语音】
-   芋道源码:
+   非繁源码:
   ① bug 修复:
     1)joolun 的做法:使用 mediaId 从微信公众号,下载对应的 mp4 素材,从而播放内容;
       存在的问题:mediaId 有效期是 3 天,超过时间后无法播放

+ 1 - 1
src/views/mp/material/components/ImageTable.vue

@@ -63,7 +63,7 @@ const emit = defineEmits<{
   column-count: 5;
   margin-top: 10px;
 
-  /* 芋道源码:增加 10px,避免顶着上面 */
+  /* 非繁源码:增加 10px,避免顶着上面 */
 }
 
 .waterfall-item {

+ 186 - 174
src/views/system/dept/DeptForm.vue

@@ -1,174 +1,186 @@
-<template>
-  <Dialog v-model="dialogVisible" :title="dialogTitle">
-    <el-form
-      ref="formRef"
-      v-loading="formLoading"
-      :model="formData"
-      :rules="formRules"
-      label-width="80px"
-    >
-      <el-form-item label="上级部门" prop="parentId">
-        <el-tree-select
-          v-model="formData.parentId"
-          :data="deptTree"
-          :props="defaultProps"
-          check-strictly
-          default-expand-all
-          placeholder="请选择上级部门"
-          value-key="deptId"
-        />
-      </el-form-item>
-      <el-form-item label="部门名称" prop="name">
-        <el-input v-model="formData.name" placeholder="请输入部门名称" />
-      </el-form-item>
-      <el-form-item label="显示排序" prop="sort">
-        <el-input-number v-model="formData.sort" :min="0" controls-position="right" />
-      </el-form-item>
-      <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"
-            :value="item.id"
-          />
-        </el-select>
-      </el-form-item>
-      <el-form-item label="联系电话" prop="phone">
-        <el-input v-model="formData.phone" maxlength="11" placeholder="请输入联系电话" />
-      </el-form-item>
-      <el-form-item label="邮箱" prop="email">
-        <el-input v-model="formData.email" maxlength="50" placeholder="请输入邮箱" />
-      </el-form-item>
-      <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"
-            :label="dict.label"
-            :value="dict.value"
-          />
-        </el-select>
-      </el-form-item>
-    </el-form>
-    <template #footer>
-      <el-button type="primary" @click="submitForm">确 定</el-button>
-      <el-button @click="dialogVisible = false">取 消</el-button>
-    </template>
-  </Dialog>
-</template>
-<script lang="ts" setup>
-import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
-import { defaultProps, handleTree } from '@/utils/tree'
-import * as DeptApi from '@/api/system/dept'
-import * as UserApi from '@/api/system/user'
-import { CommonStatusEnum } from '@/utils/constants'
-import { FormRules } from 'element-plus'
-
-defineOptions({ name: 'SystemDeptForm' })
-
-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,
-  title: '',
-  parentId: undefined,
-  name: undefined,
-  sort: undefined,
-  leaderUserId: undefined,
-  phone: undefined,
-  email: undefined,
-  status: CommonStatusEnum.ENABLE
-})
-const formRules = reactive<FormRules>({
-  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 formRef = ref() // 表单 Ref
-const deptTree = ref() // 树形结构
-const userList = ref<UserApi.UserVO[]>([]) // 用户列表
-
-/** 打开弹窗 */
-const open = async (type: string, id?: number) => {
-  dialogVisible.value = true
-  dialogTitle.value = t('action.' + type)
-  formType.value = type
-  resetForm()
-  // 修改时,设置数据
-  if (id) {
-    formLoading.value = true
-    try {
-      formData.value = await DeptApi.getDept(id)
-    } finally {
-      formLoading.value = false
-    }
-  }
-  // 获得用户列表
-  userList.value = await UserApi.getSimpleUserList()
-  // 获得部门树
-  await getTree()
-}
-defineExpose({ open }) // 提供 open 方法,用于打开弹窗
-
-/** 提交表单 */
-const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
-const submitForm = async () => {
-  // 校验表单
-  if (!formRef) return
-  const valid = await formRef.value.validate()
-  if (!valid) return
-  // 提交请求
-  formLoading.value = true
-  try {
-    const data = formData.value as unknown as DeptApi.DeptVO
-    if (formType.value === 'create') {
-      await DeptApi.createDept(data)
-      message.success(t('common.createSuccess'))
-    } else {
-      await DeptApi.updateDept(data)
-      message.success(t('common.updateSuccess'))
-    }
-    dialogVisible.value = false
-    // 发送操作成功的事件
-    emit('success')
-  } finally {
-    formLoading.value = false
-  }
-}
-
-/** 重置表单 */
-const resetForm = () => {
-  formData.value = {
-    id: undefined,
-    title: '',
-    parentId: undefined,
-    name: undefined,
-    sort: undefined,
-    leaderUserId: undefined,
-    phone: undefined,
-    email: undefined,
-    status: CommonStatusEnum.ENABLE
-  }
-  formRef.value?.resetFields()
-}
-
-/** 获得部门树 */
-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)
-}
-</script>
+<template>
+	<Dialog v-model="dialogVisible" :title="dialogTitle">
+		<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>
+				</el-col>
+				<el-col span="12">
+					<el-form-item label="部门名称" prop="name">
+						<el-input v-model="formData.name" placeholder="请输入部门名称" />
+					</el-form-item>
+				</el-col>
+			</el-row>
+			<el-row :gutter="30">
+				<el-col span="12">
+					<el-form-item label="显示排序" prop="sort">
+						<el-input-number v-model="formData.sort" :min="0" controls-position="right" />
+					</el-form-item>
+				</el-col>
+				<el-col span="12">
+					<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"
+								:value="item.id" />
+						</el-select>
+					</el-form-item>
+				</el-col>
+			</el-row>
+			<el-row :gutter="30">
+				<el-col span="12">
+					<el-form-item label="联系电话" prop="phone">
+						<el-input v-model="formData.phone" maxlength="11" placeholder="请输入联系电话" />
+					</el-form-item>
+				</el-col>
+				<el-col span="12">
+					<el-form-item label="邮箱" prop="email">
+						<el-input v-model="formData.email" maxlength="50" placeholder="请输入邮箱" />
+					</el-form-item>
+				</el-col>
+			</el-row>
+			<el-row :gutter="30">
+				<el-col span="12">
+					<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"
+								:label="dict.label" :value="dict.value" />
+						</el-select>
+					</el-form-item>
+				</el-col>
+			</el-row>
+		</el-form>
+		<template #footer>
+			<el-button type="primary" @click="submitForm">确 定</el-button>
+			<el-button @click="dialogVisible = false">取 消</el-button>
+		</template>
+	</Dialog>
+</template>
+<script lang="ts" setup>
+	import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+	import { defaultProps, handleTree } from '@/utils/tree'
+	import * as DeptApi from '@/api/system/dept'
+	import * as UserApi from '@/api/system/user'
+	import { CommonStatusEnum } from '@/utils/constants'
+	import { FormRules } from 'element-plus'
+
+	defineOptions({ name: 'SystemDeptForm' })
+
+	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,
+		title: '',
+		parentId: undefined,
+		name: undefined,
+		sort: undefined,
+		leaderUserId: undefined,
+		phone: undefined,
+		email: undefined,
+		status: CommonStatusEnum.ENABLE
+	})
+	const formRules = reactive<FormRules>({
+		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 formRef = ref() // 表单 Ref
+	const deptTree = ref() // 树形结构
+	const userList = ref<UserApi.UserVO[]>([]) // 用户列表
+
+	/** 打开弹窗 */
+	const open = async (type : string, id ?: number) => {
+		dialogVisible.value = true
+		dialogTitle.value = t('action.' + type)
+		formType.value = type
+		resetForm()
+		// 修改时,设置数据
+		if (id) {
+			formLoading.value = true
+			try {
+				formData.value = await DeptApi.getDept(id)
+			} finally {
+				formLoading.value = false
+			}
+		}
+		// 获得用户列表
+		userList.value = await UserApi.getSimpleUserList()
+		// 获得部门树
+		await getTree()
+	}
+	defineExpose({ open }) // 提供 open 方法,用于打开弹窗
+
+	/** 提交表单 */
+	const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
+	const submitForm = async () => {
+		// 校验表单
+		if (!formRef) return
+		const valid = await formRef.value.validate()
+		if (!valid) return
+		// 提交请求
+		formLoading.value = true
+		try {
+			const data = formData.value as unknown as DeptApi.DeptVO
+			if (formType.value === 'create') {
+				await DeptApi.createDept(data)
+				message.success(t('common.createSuccess'))
+			} else {
+				await DeptApi.updateDept(data)
+				message.success(t('common.updateSuccess'))
+			}
+			dialogVisible.value = false
+			// 发送操作成功的事件
+			emit('success')
+		} finally {
+			formLoading.value = false
+		}
+	}
+
+	/** 重置表单 */
+	const resetForm = () => {
+		formData.value = {
+			id: undefined,
+			title: '',
+			parentId: undefined,
+			name: undefined,
+			sort: undefined,
+			leaderUserId: undefined,
+			phone: undefined,
+			email: undefined,
+			status: CommonStatusEnum.ENABLE
+		}
+		formRef.value?.resetFields()
+	}
+
+	/** 获得部门树 */
+	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)
+	}
+</script>
+<style lang="scss" scoped>
+	.el-row {
+
+		.el-input,
+		.el-select,
+		.el-input-number,
+		.el-date-editor {
+			width: 170px;
+		}
+	}
+</style>

+ 118 - 124
src/views/system/dict/DictTypeForm.vue

@@ -1,124 +1,118 @@
-<template>
-  <Dialog v-model="dialogVisible" :title="dialogTitle">
-    <el-form
-      ref="formRef"
-      v-loading="formLoading"
-      :model="formData"
-      :rules="formRules"
-      label-width="80px"
-    >
-      <el-form-item label="字典名称" prop="name">
-        <el-input v-model="formData.name" placeholder="请输入字典名称" />
-      </el-form-item>
-      <el-form-item label="字典类型" prop="type">
-        <el-input
-          v-model="formData.type"
-          :disabled="typeof formData.id !== 'undefined'"
-          placeholder="请输入参数名称"
-        />
-      </el-form-item>
-      <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-radio-group>
-      </el-form-item>
-      <el-form-item label="备注" prop="remark">
-        <el-input v-model="formData.remark" placeholder="请输入内容" type="textarea" />
-      </el-form-item>
-    </el-form>
-    <template #footer>
-      <el-button :disabled="formLoading" type="primary" @click="submitForm">确 定</el-button>
-      <el-button @click="dialogVisible = false">取 消</el-button>
-    </template>
-  </Dialog>
-</template>
-<script lang="ts" setup>
-import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
-import * as DictTypeApi from '@/api/system/dict/dict.type'
-import { CommonStatusEnum } from '@/utils/constants'
-
-defineOptions({ name: 'SystemDictTypeForm' })
-
-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: '',
-  type: '',
-  status: CommonStatusEnum.ENABLE,
-  remark: ''
-})
-const formRules = reactive({
-  name: [{ required: true, message: '字典名称不能为空', trigger: 'blur' }],
-  type: [{ required: true, message: '字典类型不能为空', trigger: 'blur' }],
-  status: [{ required: true, message: '状态不能为空', trigger: 'change' }]
-})
-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
-    try {
-      formData.value = await DictTypeApi.getDictType(id)
-    } finally {
-      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
-  // 提交请求
-  formLoading.value = true
-  try {
-    const data = formData.value as DictTypeApi.DictTypeVO
-    if (formType.value === 'create') {
-      await DictTypeApi.createDictType(data)
-      message.success(t('common.createSuccess'))
-    } else {
-      await DictTypeApi.updateDictType(data)
-      message.success(t('common.updateSuccess'))
-    }
-    dialogVisible.value = false
-    // 发送操作成功的事件
-    emit('success')
-  } finally {
-    formLoading.value = false
-  }
-}
-
-/** 重置表单 */
-const resetForm = () => {
-  formData.value = {
-    id: undefined,
-    type: '',
-    name: '',
-    status: CommonStatusEnum.ENABLE,
-    remark: ''
-  }
-  formRef.value?.resetFields()
-}
-</script>
+<template>
+	<Dialog v-model="dialogVisible" :title="dialogTitle">
+		<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="name">
+						<el-input v-model="formData.name" placeholder="请输入字典名称" />
+					</el-form-item>
+				</el-col>
+				<el-col span="12">
+					<el-form-item label="字典类型" prop="type">
+						<el-input v-model="formData.type" :disabled="typeof formData.id !== 'undefined'"
+							placeholder="请输入参数名称" />
+					</el-form-item>
+				</el-col>
+			</el-row>
+			<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-radio-group>
+			</el-form-item>
+			<el-form-item label="备注" prop="remark">
+				<el-input v-model="formData.remark" placeholder="请输入内容" type="textarea" />
+			</el-form-item>
+		</el-form>
+		<template #footer>
+			<el-button :disabled="formLoading" type="primary" @click="submitForm">确 定</el-button>
+			<el-button @click="dialogVisible = false">取 消</el-button>
+		</template>
+	</Dialog>
+</template>
+<script lang="ts" setup>
+	import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+	import * as DictTypeApi from '@/api/system/dict/dict.type'
+	import { CommonStatusEnum } from '@/utils/constants'
+
+	defineOptions({ name: 'SystemDictTypeForm' })
+
+	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: '',
+		type: '',
+		status: CommonStatusEnum.ENABLE,
+		remark: ''
+	})
+	const formRules = reactive({
+		name: [{ required: true, message: '字典名称不能为空', trigger: 'blur' }],
+		type: [{ required: true, message: '字典类型不能为空', trigger: 'blur' }],
+		status: [{ required: true, message: '状态不能为空', trigger: 'change' }]
+	})
+	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
+			try {
+				formData.value = await DictTypeApi.getDictType(id)
+			} finally {
+				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
+		// 提交请求
+		formLoading.value = true
+		try {
+			const data = formData.value as DictTypeApi.DictTypeVO
+			if (formType.value === 'create') {
+				await DictTypeApi.createDictType(data)
+				message.success(t('common.createSuccess'))
+			} else {
+				await DictTypeApi.updateDictType(data)
+				message.success(t('common.updateSuccess'))
+			}
+			dialogVisible.value = false
+			// 发送操作成功的事件
+			emit('success')
+		} finally {
+			formLoading.value = false
+		}
+	}
+
+	/** 重置表单 */
+	const resetForm = () => {
+		formData.value = {
+			id: undefined,
+			type: '',
+			name: '',
+			status: CommonStatusEnum.ENABLE,
+			remark: ''
+		}
+		formRef.value?.resetFields()
+	}
+</script>

+ 197 - 183
src/views/system/dict/data/DictDataForm.vue

@@ -1,183 +1,197 @@
-<template>
-  <Dialog v-model="dialogVisible" :title="dialogTitle">
-    <el-form
-      ref="formRef"
-      v-loading="formLoading"
-      :model="formData"
-      :rules="formRules"
-      label-width="80px"
-    >
-      <el-form-item label="字典类型" prop="type">
-        <el-input
-          v-model="formData.dictType"
-          :disabled="typeof formData.id !== 'undefined'"
-          placeholder="请输入参数名称"
-        />
-      </el-form-item>
-      <el-form-item label="数据标签" prop="label">
-        <el-input v-model="formData.label" placeholder="请输入数据标签" />
-      </el-form-item>
-      <el-form-item label="数据键值" prop="value">
-        <el-input v-model="formData.value" placeholder="请输入数据键值" />
-      </el-form-item>
-      <el-form-item label="显示排序" prop="sort">
-        <el-input-number v-model="formData.sort" :min="0" controls-position="right" />
-      </el-form-item>
-      <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-radio-group>
-      </el-form-item>
-      <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-select>
-      </el-form-item>
-      <el-form-item label="CSS Class" prop="cssClass">
-        <el-input v-model="formData.cssClass" placeholder="请输入 CSS Class" />
-      </el-form-item>
-      <el-form-item label="备注" prop="remark">
-        <el-input v-model="formData.remark" placeholder="请输入内容" type="textarea" />
-      </el-form-item>
-    </el-form>
-    <template #footer>
-      <el-button :disabled="formLoading" type="primary" @click="submitForm">确 定</el-button>
-      <el-button @click="dialogVisible = false">取 消</el-button>
-    </template>
-  </Dialog>
-</template>
-<script lang="ts" setup>
-import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
-import * as DictDataApi from '@/api/system/dict/dict.data'
-import { CommonStatusEnum } from '@/utils/constants'
-
-defineOptions({ name: 'SystemDictDataForm' })
-
-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,
-  sort: undefined,
-  label: '',
-  value: '',
-  dictType: '',
-  status: CommonStatusEnum.ENABLE,
-  colorType: '',
-  cssClass: '',
-  remark: ''
-})
-const formRules = reactive({
-  label: [{ required: true, message: '数据标签不能为空', trigger: 'blur' }],
-  value: [{ required: true, message: '数据键值不能为空', trigger: 'blur' }],
-  sort: [{ required: true, message: '数据顺序不能为空', trigger: 'blur' }],
-  status: [{ required: true, message: '状态不能为空', trigger: 'change' }]
-})
-const formRef = ref() // 表单 Ref
-
-// 数据标签回显样式
-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) => {
-  dialogVisible.value = true
-  dialogTitle.value = t('action.' + type)
-  formType.value = type
-  resetForm()
-  if (dictType) {
-    formData.value.dictType = dictType
-  }
-  // 修改时,设置数据
-  if (id) {
-    formLoading.value = true
-    try {
-      formData.value = await DictDataApi.getDictData(id)
-    } finally {
-      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
-  // 提交请求
-  formLoading.value = true
-  try {
-    const data = formData.value as DictDataApi.DictDataVO
-    if (formType.value === 'create') {
-      await DictDataApi.createDictData(data)
-      message.success(t('common.createSuccess'))
-    } else {
-      await DictDataApi.updateDictData(data)
-      message.success(t('common.updateSuccess'))
-    }
-    dialogVisible.value = false
-    // 发送操作成功的事件
-    emit('success')
-  } finally {
-    formLoading.value = false
-  }
-}
-
-/** 重置表单 */
-const resetForm = () => {
-  formData.value = {
-    id: undefined,
-    sort: undefined,
-    label: '',
-    value: '',
-    dictType: '',
-    status: CommonStatusEnum.ENABLE,
-    colorType: '',
-    cssClass: '',
-    remark: ''
-  }
-  formRef.value?.resetFields()
-}
-</script>
+<template>
+	<Dialog v-model="dialogVisible" :title="dialogTitle">
+		<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="type">
+						<el-input v-model="formData.dictType" :disabled="typeof formData.id !== 'undefined'"
+							placeholder="请输入参数名称" />
+					</el-form-item>
+				</el-col>
+				<el-col span="12">
+					<el-form-item label="数据标签" prop="label">
+						<el-input v-model="formData.label" placeholder="请输入数据标签" />
+					</el-form-item>
+				</el-col>
+			</el-row>
+			<el-row :gutter="30">
+				<el-col span="12">
+					<el-form-item label="数据键值" prop="value">
+						<el-input v-model="formData.value" placeholder="请输入数据键值" />
+					</el-form-item>
+				</el-col>
+				<el-col span="12">
+					<el-form-item label="显示排序" prop="sort">
+						<el-input-number v-model="formData.sort" :min="0" controls-position="right" />
+					</el-form-item>
+				</el-col>
+			</el-row>
+			<el-row :gutter="30">
+
+				<el-col span="12">
+					<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-select>
+					</el-form-item>
+				</el-col>
+				<el-col span="12">
+					<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-radio-group>
+					</el-form-item>
+				</el-col>
+			</el-row>
+			<el-form-item label="CSS Class" prop="cssClass">
+				<el-input v-model="formData.cssClass" placeholder="请输入 CSS Class" />
+			</el-form-item>
+			<el-form-item label="备注" prop="remark">
+				<el-input v-model="formData.remark" placeholder="请输入内容" type="textarea" />
+			</el-form-item>
+		</el-form>
+		<template #footer>
+			<el-button :disabled="formLoading" type="primary" @click="submitForm">确 定</el-button>
+			<el-button @click="dialogVisible = false">取 消</el-button>
+		</template>
+	</Dialog>
+</template>
+<script lang="ts" setup>
+	import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+	import * as DictDataApi from '@/api/system/dict/dict.data'
+	import { CommonStatusEnum } from '@/utils/constants'
+
+	defineOptions({ name: 'SystemDictDataForm' })
+
+	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,
+		sort: undefined,
+		label: '',
+		value: '',
+		dictType: '',
+		status: CommonStatusEnum.ENABLE,
+		colorType: '',
+		cssClass: '',
+		remark: ''
+	})
+	const formRules = reactive({
+		label: [{ required: true, message: '数据标签不能为空', trigger: 'blur' }],
+		value: [{ required: true, message: '数据键值不能为空', trigger: 'blur' }],
+		sort: [{ required: true, message: '数据顺序不能为空', trigger: 'blur' }],
+		status: [{ required: true, message: '状态不能为空', trigger: 'change' }]
+	})
+	const formRef = ref() // 表单 Ref
+
+	// 数据标签回显样式
+	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) => {
+		dialogVisible.value = true
+		dialogTitle.value = t('action.' + type)
+		formType.value = type
+		resetForm()
+		if (dictType) {
+			formData.value.dictType = dictType
+		}
+		// 修改时,设置数据
+		if (id) {
+			formLoading.value = true
+			try {
+				formData.value = await DictDataApi.getDictData(id)
+			} finally {
+				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
+		// 提交请求
+		formLoading.value = true
+		try {
+			const data = formData.value as DictDataApi.DictDataVO
+			if (formType.value === 'create') {
+				await DictDataApi.createDictData(data)
+				message.success(t('common.createSuccess'))
+			} else {
+				await DictDataApi.updateDictData(data)
+				message.success(t('common.updateSuccess'))
+			}
+			dialogVisible.value = false
+			// 发送操作成功的事件
+			emit('success')
+		} finally {
+			formLoading.value = false
+		}
+	}
+
+	/** 重置表单 */
+	const resetForm = () => {
+		formData.value = {
+			id: undefined,
+			sort: undefined,
+			label: '',
+			value: '',
+			dictType: '',
+			status: CommonStatusEnum.ENABLE,
+			colorType: '',
+			cssClass: '',
+			remark: ''
+		}
+		formRef.value?.resetFields()
+	}
+</script>
+<style lang="scss" scoped>
+	.el-row {
+
+		.el-input,
+		.el-select,
+		.el-input-number,
+		.el-date-editor {
+			width: 170px;
+		}
+	}
+</style>

+ 244 - 256
src/views/system/menu/MenuForm.vue

@@ -1,256 +1,244 @@
-<template>
-  <Dialog v-model="dialogVisible" :title="dialogTitle">
-    <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>
-      <el-form-item label="菜单名称" prop="name">
-        <el-input v-model="formData.name" clearable placeholder="请输入菜单名称" />
-      </el-form-item>
-      <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"
-            :label="dict.value"
-          >
-            {{ dict.label }}
-          </el-radio-button>
-        </el-radio-group>
-      </el-form-item>
-      <el-form-item v-if="formData.type !== 3" label="菜单图标">
-        <IconSelect v-model="formData.icon" clearable />
-      </el-form-item>
-      <el-form-item v-if="formData.type !== 3" label="路由地址" prop="path">
-        <template #label>
-          <Tooltip
-            message="访问的路由地址,如:`user`。如需外网地址时,则以 `http(s)://` 开头"
-            title="路由地址"
-          />
-        </template>
-        <el-input v-model="formData.path" clearable placeholder="请输入路由地址" />
-      </el-form-item>
-      <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>
-      <el-form-item v-if="formData.type === 2" label="组件名字" prop="componentName">
-        <el-input v-model="formData.componentName" clearable placeholder="例如说:SystemUser" />
-      </el-form-item>
-      <el-form-item v-if="formData.type !== 1" label="权限标识" prop="permission">
-        <template #label>
-          <Tooltip
-            message="Controller 方法上的权限字符,如:@PreAuthorize(`@ss.hasPermission('system:user:list')`)"
-            title="权限标识"
-          />
-        </template>
-        <el-input v-model="formData.permission" clearable placeholder="请输入权限标识" />
-      </el-form-item>
-      <el-form-item label="显示排序" prop="sort">
-        <el-input-number v-model="formData.sort" :min="0" clearable controls-position="right" />
-      </el-form-item>
-      <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.label"
-            :label="dict.value"
-          >
-            {{ dict.label }}
-          </el-radio>
-        </el-radio-group>
-      </el-form-item>
-      <el-form-item v-if="formData.type !== 3" label="显示状态" prop="visible">
-        <template #label>
-          <Tooltip message="选择隐藏时,路由将不会出现在侧边栏,但仍然可以访问" title="显示状态" />
-        </template>
-        <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-radio-group>
-      </el-form-item>
-      <el-form-item v-if="formData.type !== 3" label="总是显示" prop="alwaysShow">
-        <template #label>
-          <Tooltip
-            message="选择不是时,当该菜单只有一个子菜单时,不展示自己,直接展示子菜单"
-            title="总是显示"
-          />
-        </template>
-        <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-radio-group>
-      </el-form-item>
-      <el-form-item v-if="formData.type === 2" label="缓存状态" prop="keepAlive">
-        <template #label>
-          <Tooltip
-            message="选择缓存时,则会被 `keep-alive` 缓存,必须填写「组件名称」字段"
-            title="缓存状态"
-          />
-        </template>
-        <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>
-        </el-radio-group>
-      </el-form-item>
-    </el-form>
-    <template #footer>
-      <el-button :disabled="formLoading" type="primary" @click="submitForm">确 定</el-button>
-      <el-button @click="dialogVisible = false">取 消</el-button>
-    </template>
-  </Dialog>
-</template>
-<script lang="ts" setup>
-import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
-import * as MenuApi from '@/api/system/menu'
-import { CACHE_KEY, useCache } from '@/hooks/web/useCache'
-import { CommonStatusEnum, SystemMenuTypeEnum } from '@/utils/constants'
-import { defaultProps, handleTree } from '@/utils/tree'
-
-defineOptions({ name: 'SystemMenuForm' })
-
-const { wsCache } = useCache()
-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: 0,
-  name: '',
-  permission: '',
-  type: SystemMenuTypeEnum.DIR,
-  sort: Number(undefined),
-  parentId: 0,
-  path: '',
-  icon: '',
-  component: '',
-  componentName: '',
-  status: CommonStatusEnum.ENABLE,
-  visible: true,
-  keepAlive: true,
-  alwaysShow: true
-})
-const formRules = reactive({
-  name: [{ required: true, message: '菜单名称不能为空', trigger: 'blur' }],
-  sort: [{ required: true, message: '菜单顺序不能为空', trigger: 'blur' }],
-  path: [{ required: true, message: '路由地址不能为空', trigger: 'blur' }],
-  status: [{ required: true, message: '状态不能为空', trigger: 'blur' }]
-})
-const formRef = ref() // 表单 Ref
-
-/** 打开弹窗 */
-const open = async (type: string, id?: number, parentId?: number) => {
-  dialogVisible.value = true
-  dialogTitle.value = t('action.' + type)
-  formType.value = type
-  resetForm()
-  if (parentId) {
-    formData.value.parentId = parentId
-  }
-  // 修改时,设置数据
-  if (id) {
-    formLoading.value = true
-    try {
-      formData.value = await MenuApi.getMenu(id)
-    } finally {
-      formLoading.value = false
-    }
-  }
-  // 获得菜单列表
-  await getTree()
-}
-defineExpose({ open }) // 提供 open 方法,用于打开弹窗
-
-/** 提交表单 */
-const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
-const submitForm = async () => {
-  // 校验表单
-  if (!formRef) return
-  const valid = await formRef.value.validate()
-  if (!valid) return
-  // 提交请求
-  formLoading.value = true
-  try {
-    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('路径必须以 / 开头')
-          return
-        } else if (formData.value.parentId !== 0 && formData.value.path.charAt(0) === '/') {
-          message.error('路径不能以 / 开头')
-          return
-        }
-      }
-    }
-    const data = formData.value as unknown as MenuApi.MenuVO
-    if (formType.value === 'create') {
-      await MenuApi.createMenu(data)
-      message.success(t('common.createSuccess'))
-    } else {
-      await MenuApi.updateMenu(data)
-      message.success(t('common.updateSuccess'))
-    }
-    dialogVisible.value = false
-    // 发送操作成功的事件
-    emit('success')
-  } finally {
-    formLoading.value = false
-    // 清空,从而触发刷新
-    wsCache.delete(CACHE_KEY.ROLE_ROUTERS)
-  }
-}
-
-/** 获取下拉框[上级菜单]的数据  */
-const menuTree = ref<Tree[]>([]) // 树形结构
-const getTree = async () => {
-  menuTree.value = []
-  const res = await MenuApi.getSimpleMenusList()
-  let menu: Tree = { id: 0, name: '主类目', children: [] }
-  menu.children = handleTree(res)
-  menuTree.value.push(menu)
-}
-
-/** 重置表单 */
-const resetForm = () => {
-  formData.value = {
-    id: 0,
-    name: '',
-    permission: '',
-    type: SystemMenuTypeEnum.DIR,
-    sort: Number(undefined),
-    parentId: 0,
-    path: '',
-    icon: '',
-    component: '',
-    componentName: '',
-    status: CommonStatusEnum.ENABLE,
-    visible: true,
-    keepAlive: true,
-    alwaysShow: true
-  }
-  formRef.value?.resetFields()
-}
-
-/** 判断 path 是不是外部的 HTTP 等链接 */
-const isExternal = (path: string) => {
-  return /^(https?:|mailto:|tel:)/.test(path)
-}
-</script>
+<template>
+	<Dialog v-model="dialogVisible" :title="dialogTitle">
+		<el-form ref="formRef" v-loading="formLoading" :model="formData" :rules="formRules" label-width="100px">
+			<el-row :gutter="30">
+				<el-col span="12">
+					<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>
+				</el-col>
+				<el-col span="12">
+					<el-form-item label="菜单名称" prop="name">
+						<el-input v-model="formData.name" clearable placeholder="请输入菜单名称" />
+					</el-form-item>
+				</el-col>
+			</el-row>
+			<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"
+						:label="dict.value">
+						{{ dict.label }}
+					</el-radio-button>
+				</el-radio-group>
+			</el-form-item>
+			<el-form-item v-if="formData.type !== 3" label="菜单图标">
+				<IconSelect v-model="formData.icon" clearable />
+			</el-form-item>
+			<el-form-item v-if="formData.type !== 3" label="路由地址" prop="path">
+				<template #label>
+					<Tooltip message="访问的路由地址,如:`user`。如需外网地址时,则以 `http(s)://` 开头" title="路由地址" />
+				</template>
+				<el-input v-model="formData.path" clearable placeholder="请输入路由地址" />
+			</el-form-item>
+			<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>
+			<el-form-item v-if="formData.type === 2" label="组件名字" prop="componentName">
+				<el-input v-model="formData.componentName" clearable placeholder="例如说:SystemUser" />
+			</el-form-item>
+			<el-form-item v-if="formData.type !== 1" label="权限标识" prop="permission">
+				<template #label>
+					<Tooltip message="Controller 方法上的权限字符,如:@PreAuthorize(`@ss.hasPermission('system:user:list')`)"
+						title="权限标识" />
+				</template>
+				<el-input v-model="formData.permission" clearable placeholder="请输入权限标识" />
+			</el-form-item>
+			<el-form-item label="显示排序" prop="sort">
+				<el-input-number v-model="formData.sort" :min="0" clearable controls-position="right" />
+			</el-form-item>
+			<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.label"
+						:label="dict.value">
+						{{ dict.label }}
+					</el-radio>
+				</el-radio-group>
+			</el-form-item>
+			<el-form-item v-if="formData.type !== 3" label="显示状态" prop="visible">
+				<template #label>
+					<Tooltip message="选择隐藏时,路由将不会出现在侧边栏,但仍然可以访问" title="显示状态" />
+				</template>
+				<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-radio-group>
+			</el-form-item>
+			<el-form-item v-if="formData.type !== 3" label="总是显示" prop="alwaysShow">
+				<template #label>
+					<Tooltip message="选择不是时,当该菜单只有一个子菜单时,不展示自己,直接展示子菜单" title="总是显示" />
+				</template>
+				<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-radio-group>
+			</el-form-item>
+			<el-form-item v-if="formData.type === 2" label="缓存状态" prop="keepAlive">
+				<template #label>
+					<Tooltip message="选择缓存时,则会被 `keep-alive` 缓存,必须填写「组件名称」字段" title="缓存状态" />
+				</template>
+				<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>
+				</el-radio-group>
+			</el-form-item>
+		</el-form>
+		<template #footer>
+			<el-button :disabled="formLoading" type="primary" @click="submitForm">确 定</el-button>
+			<el-button @click="dialogVisible = false">取 消</el-button>
+		</template>
+	</Dialog>
+</template>
+<script lang="ts" setup>
+	import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+	import * as MenuApi from '@/api/system/menu'
+	import { CACHE_KEY, useCache } from '@/hooks/web/useCache'
+	import { CommonStatusEnum, SystemMenuTypeEnum } from '@/utils/constants'
+	import { defaultProps, handleTree } from '@/utils/tree'
+
+	defineOptions({ name: 'SystemMenuForm' })
+
+	const { wsCache } = useCache()
+	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: 0,
+		name: '',
+		permission: '',
+		type: SystemMenuTypeEnum.DIR,
+		sort: Number(undefined),
+		parentId: 0,
+		path: '',
+		icon: '',
+		component: '',
+		componentName: '',
+		status: CommonStatusEnum.ENABLE,
+		visible: true,
+		keepAlive: true,
+		alwaysShow: true
+	})
+	const formRules = reactive({
+		name: [{ required: true, message: '菜单名称不能为空', trigger: 'blur' }],
+		sort: [{ required: true, message: '菜单顺序不能为空', trigger: 'blur' }],
+		path: [{ required: true, message: '路由地址不能为空', trigger: 'blur' }],
+		status: [{ required: true, message: '状态不能为空', trigger: 'blur' }]
+	})
+	const formRef = ref() // 表单 Ref
+
+	/** 打开弹窗 */
+	const open = async (type : string, id ?: number, parentId ?: number) => {
+		dialogVisible.value = true
+		dialogTitle.value = t('action.' + type)
+		formType.value = type
+		resetForm()
+		if (parentId) {
+			formData.value.parentId = parentId
+		}
+		// 修改时,设置数据
+		if (id) {
+			formLoading.value = true
+			try {
+				formData.value = await MenuApi.getMenu(id)
+			} finally {
+				formLoading.value = false
+			}
+		}
+		// 获得菜单列表
+		await getTree()
+	}
+	defineExpose({ open }) // 提供 open 方法,用于打开弹窗
+
+	/** 提交表单 */
+	const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
+	const submitForm = async () => {
+		// 校验表单
+		if (!formRef) return
+		const valid = await formRef.value.validate()
+		if (!valid) return
+		// 提交请求
+		formLoading.value = true
+		try {
+			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('路径必须以 / 开头')
+						return
+					} else if (formData.value.parentId !== 0 && formData.value.path.charAt(0) === '/') {
+						message.error('路径不能以 / 开头')
+						return
+					}
+				}
+			}
+			const data = formData.value as unknown as MenuApi.MenuVO
+			if (formType.value === 'create') {
+				await MenuApi.createMenu(data)
+				message.success(t('common.createSuccess'))
+			} else {
+				await MenuApi.updateMenu(data)
+				message.success(t('common.updateSuccess'))
+			}
+			dialogVisible.value = false
+			// 发送操作成功的事件
+			emit('success')
+		} finally {
+			formLoading.value = false
+			// 清空,从而触发刷新
+			wsCache.delete(CACHE_KEY.ROLE_ROUTERS)
+		}
+	}
+
+	/** 获取下拉框[上级菜单]的数据  */
+	const menuTree = ref<Tree[]>([]) // 树形结构
+	const getTree = async () => {
+		menuTree.value = []
+		const res = await MenuApi.getSimpleMenusList()
+		let menu : Tree = { id: 0, name: '主类目', children: [] }
+		menu.children = handleTree(res)
+		menuTree.value.push(menu)
+	}
+
+	/** 重置表单 */
+	const resetForm = () => {
+		formData.value = {
+			id: 0,
+			name: '',
+			permission: '',
+			type: SystemMenuTypeEnum.DIR,
+			sort: Number(undefined),
+			parentId: 0,
+			path: '',
+			icon: '',
+			component: '',
+			componentName: '',
+			status: CommonStatusEnum.ENABLE,
+			visible: true,
+			keepAlive: true,
+			alwaysShow: true
+		}
+		formRef.value?.resetFields()
+	}
+
+	/** 判断 path 是不是外部的 HTTP 等链接 */
+	const isExternal = (path : string) => {
+		return /^(https?:|mailto:|tel:)/.test(path)
+	}
+</script>
+<style lang="scss" scoped>
+	.el-row {
+
+		.el-input,
+		.el-select,
+		.el-input-number,
+		.el-date-editor {
+			width: 150px;
+		}
+	}
+</style>

+ 138 - 125
src/views/system/post/PostForm.vue

@@ -1,125 +1,138 @@
-<template>
-  <Dialog v-model="dialogVisible" :title="dialogTitle" width="800">
-    <el-form
-      ref="formRef"
-      v-loading="formLoading"
-      :model="formData"
-      :rules="formRules"
-      label-width="80px"
-    >
-      <el-form-item label="岗位标题" prop="name">
-        <el-input v-model="formData.name" placeholder="请输入岗位标题" />
-      </el-form-item>
-      <el-form-item label="岗位编码" prop="code">
-        <el-input v-model="formData.code" placeholder="请输入岗位编码" />
-      </el-form-item>
-      <el-form-item label="岗位顺序" prop="sort">
-        <el-input v-model="formData.sort" placeholder="请输入岗位顺序" />
-      </el-form-item>
-      <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"
-            :label="dict.label"
-            :value="dict.value"
-          />
-        </el-select>
-      </el-form-item>
-      <el-form-item label="备注" prop="remark">
-        <el-input v-model="formData.remark" placeholder="请输备注" type="textarea" />
-      </el-form-item>
-    </el-form>
-    <template #footer>
-      <el-button :disabled="formLoading" type="primary" @click="submitForm">确 定</el-button>
-      <el-button @click="dialogVisible = false">取 消</el-button>
-    </template>
-  </Dialog>
-</template>
-<script lang="ts" setup>
-import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
-import { CommonStatusEnum } from '@/utils/constants'
-import * as PostApi from '@/api/system/post'
-
-defineOptions({ name: 'SystemPostForm' })
-
-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: '',
-  code: '',
-  sort: 0,
-  status: CommonStatusEnum.ENABLE,
-  remark: ''
-})
-const formRules = reactive({
-  name: [{ required: true, message: '岗位标题不能为空', trigger: 'blur' }],
-  code: [{ required: true, message: '岗位编码不能为空', trigger: 'change' }],
-  status: [{ required: true, message: '岗位状态不能为空', trigger: 'change' }],
-  remark: [{ required: false, message: '岗位内容不能为空', trigger: 'blur' }]
-})
-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
-    try {
-      formData.value = await PostApi.getPost(id)
-    } finally {
-      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
-  // 提交请求
-  formLoading.value = true
-  try {
-    const data = formData.value as unknown as PostApi.PostVO
-    if (formType.value === 'create') {
-      await PostApi.createPost(data)
-      message.success(t('common.createSuccess'))
-    } else {
-      await PostApi.updatePost(data)
-      message.success(t('common.updateSuccess'))
-    }
-    dialogVisible.value = false
-    // 发送操作成功的事件
-    emit('success')
-  } finally {
-    formLoading.value = false
-  }
-}
-
-/** 重置表单 */
-const resetForm = () => {
-  formData.value = {
-    id: undefined,
-    name: '',
-    code: '',
-    sort: undefined,
-    status: CommonStatusEnum.ENABLE,
-    remark: ''
-  } as any
-  formRef.value?.resetFields()
-}
-</script>
+<template>
+	<Dialog v-model="dialogVisible" :title="dialogTitle" width="700">
+		<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="name">
+						<el-input v-model="formData.name" placeholder="请输入岗位标题" />
+					</el-form-item>
+				</el-col>
+				<el-col span="12">
+					<el-form-item label="岗位编码" prop="code">
+						<el-input v-model="formData.code" placeholder="请输入岗位编码" />
+					</el-form-item>
+				</el-col>
+			</el-row>
+			<el-row :gutter="30">
+				<el-col span="12">
+					<el-form-item label="岗位顺序" prop="sort">
+						<el-input v-model="formData.sort" placeholder="请输入岗位顺序" />
+					</el-form-item>
+				</el-col>
+				<el-col span="12">
+					<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"
+								:label="dict.label" :value="dict.value" />
+						</el-select>
+					</el-form-item>
+				</el-col>
+			</el-row>
+			<el-form-item label="备注" prop="remark">
+				<el-input style="width:580px" v-model="formData.remark" placeholder="请输备注" type="textarea" />
+			</el-form-item>
+		</el-form>
+		<template #footer>
+			<el-button :disabled="formLoading" type="primary" @click="submitForm">确 定</el-button>
+			<el-button @click="dialogVisible = false">取 消</el-button>
+		</template>
+	</Dialog>
+</template>
+<script lang="ts" setup>
+	import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+	import { CommonStatusEnum } from '@/utils/constants'
+	import * as PostApi from '@/api/system/post'
+
+	defineOptions({ name: 'SystemPostForm' })
+
+	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: '',
+		code: '',
+		sort: 0,
+		status: CommonStatusEnum.ENABLE,
+		remark: ''
+	})
+	const formRules = reactive({
+		name: [{ required: true, message: '岗位标题不能为空', trigger: 'blur' }],
+		code: [{ required: true, message: '岗位编码不能为空', trigger: 'change' }],
+		status: [{ required: true, message: '岗位状态不能为空', trigger: 'change' }],
+		remark: [{ required: false, message: '岗位内容不能为空', trigger: 'blur' }]
+	})
+	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
+			try {
+				formData.value = await PostApi.getPost(id)
+			} finally {
+				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
+		// 提交请求
+		formLoading.value = true
+		try {
+			const data = formData.value as unknown as PostApi.PostVO
+			if (formType.value === 'create') {
+				await PostApi.createPost(data)
+				message.success(t('common.createSuccess'))
+			} else {
+				await PostApi.updatePost(data)
+				message.success(t('common.updateSuccess'))
+			}
+			dialogVisible.value = false
+			// 发送操作成功的事件
+			emit('success')
+		} finally {
+			formLoading.value = false
+		}
+	}
+
+	/** 重置表单 */
+	const resetForm = () => {
+		formData.value = {
+			id: undefined,
+			name: '',
+			code: '',
+			sort: undefined,
+			status: CommonStatusEnum.ENABLE,
+			remark: ''
+		} as any
+		formRef.value?.resetFields()
+	}
+</script>
+<style lang="scss" scoped>
+	.el-row {
+
+		.el-input,
+		.el-select,
+		.el-input-number,
+		.el-date-editor {
+			width: 235px;
+		}
+	}
+</style>

+ 150 - 160
src/views/system/role/RoleAssignMenuForm.vue

@@ -1,160 +1,150 @@
-<template>
-  <Dialog v-model="dialogVisible" title="菜单权限">
-    <el-form ref="formRef" v-loading="formLoading" :model="formData" label-width="80px">
-      <el-form-item label="角色名称">
-        <el-tag>{{ formData.name }}</el-tag>
-      </el-form-item>
-      <el-form-item label="角色标识">
-        <el-tag>{{ formData.code }}</el-tag>
-      </el-form-item>
-      <el-form-item label="菜单权限">
-        <el-card class="cardHeight">
-          <template #header>
-            全选/全不选:
-            <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"
-            />
-          </template>
-          <el-tree
-            ref="treeRef"
-            :data="menuOptions"
-            :props="defaultProps"
-            empty-text="加载中,请稍候"
-            node-key="id"
-            show-checkbox
-          />
-        </el-card>
-      </el-form-item>
-    </el-form>
-    <template #footer>
-      <el-button :disabled="formLoading" type="primary" @click="submitForm">确 定</el-button>
-      <el-button @click="dialogVisible = false">取 消</el-button>
-    </template>
-  </Dialog>
-</template>
-<script lang="ts" setup>
-import { defaultProps, handleTree } from '@/utils/tree'
-import * as RoleApi from '@/api/system/role'
-import * as MenuApi from '@/api/system/menu'
-import * as PermissionApi from '@/api/system/permission'
-
-defineOptions({ name: 'SystemRoleAssignMenuForm' })
-
-const { t } = useI18n() // 国际化
-const message = useMessage() // 消息弹窗
-
-const dialogVisible = ref(false) // 弹窗的是否展示
-const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
-const formData = reactive({
-  id: 0,
-  name: '',
-  code: '',
-  menuIds: []
-})
-const formRef = ref() // 表单 Ref
-const menuOptions = ref<any[]>([]) // 菜单树形结构
-const menuExpand = ref(false) // 展开/折叠
-const treeRef = ref() // 菜单树组件 Ref
-const treeNodeAll = ref(false) // 全选/全不选
-
-/** 打开弹窗 */
-const open = async (row: RoleApi.RoleVO) => {
-  dialogVisible.value = true
-  resetForm()
-  // 加载 Menu 列表。注意,必须放在前面,不然下面 setChecked 没数据节点
-  menuOptions.value = handleTree(await MenuApi.getSimpleMenusList())
-  // 设置数据
-  formData.id = row.id
-  formData.name = row.name
-  formData.code = row.code
-  formLoading.value = true
-  try {
-    formData.value.menuIds = await PermissionApi.getRoleMenuList(row.id)
-    // 设置选中
-    formData.value.menuIds.forEach((menuId: number) => {
-      treeRef.value.setChecked(menuId, true, false)
-    })
-  } finally {
-    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
-  // 提交请求
-  formLoading.value = true
-  try {
-    const data = {
-      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)
-    message.success(t('common.updateSuccess'))
-    dialogVisible.value = false
-    // 发送操作成功的事件
-    emit('success')
-  } finally {
-    formLoading.value = false
-  }
-}
-
-/** 重置表单 */
-const resetForm = () => {
-  // 重置选项
-  treeNodeAll.value = false
-  menuExpand.value = false
-  // 重置表单
-  formData.value = {
-    id: 0,
-    name: '',
-    code: '',
-    menuIds: []
-  }
-  treeRef.value?.setCheckedNodes([])
-  formRef.value?.resetFields()
-}
-
-/** 全选/全不选 */
-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
-  }
-}
-</script>
-<style lang="scss" scoped>
-.cardHeight {
-  width: 100%;
-  max-height: 400px;
-  overflow-y: scroll;
-}
-</style>
+<template>
+	<Dialog v-model="dialogVisible" title="菜单权限">
+		<el-form ref="formRef" v-loading="formLoading" :model="formData" label-width="80px">
+			<el-row :gutter="30">
+				<el-col span="12">
+					<el-form-item label="角色名称">
+						<el-tag>{{ formData.name }}</el-tag>
+					</el-form-item>
+				</el-col>
+				<el-col span="12">
+					<el-form-item label="角色标识">
+						<el-tag>{{ formData.code }}</el-tag>
+					</el-form-item>
+				</el-col>
+			</el-row>
+			<el-form-item label="菜单权限">
+				<el-card class="cardHeight">
+					<template #header>
+						全选/全不选:
+						<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" />
+					</template>
+					<el-tree ref="treeRef" :data="menuOptions" :props="defaultProps" empty-text="加载中,请稍候" node-key="id"
+						show-checkbox />
+				</el-card>
+			</el-form-item>
+		</el-form>
+		<template #footer>
+			<el-button :disabled="formLoading" type="primary" @click="submitForm">确 定</el-button>
+			<el-button @click="dialogVisible = false">取 消</el-button>
+		</template>
+	</Dialog>
+</template>
+<script lang="ts" setup>
+	import { defaultProps, handleTree } from '@/utils/tree'
+	import * as RoleApi from '@/api/system/role'
+	import * as MenuApi from '@/api/system/menu'
+	import * as PermissionApi from '@/api/system/permission'
+
+	defineOptions({ name: 'SystemRoleAssignMenuForm' })
+
+	const { t } = useI18n() // 国际化
+	const message = useMessage() // 消息弹窗
+
+	const dialogVisible = ref(false) // 弹窗的是否展示
+	const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
+	const formData = reactive({
+		id: 0,
+		name: '',
+		code: '',
+		menuIds: []
+	})
+	const formRef = ref() // 表单 Ref
+	const menuOptions = ref<any[]>([]) // 菜单树形结构
+	const menuExpand = ref(false) // 展开/折叠
+	const treeRef = ref() // 菜单树组件 Ref
+	const treeNodeAll = ref(false) // 全选/全不选
+
+	/** 打开弹窗 */
+	const open = async (row : RoleApi.RoleVO) => {
+		dialogVisible.value = true
+		resetForm()
+		// 加载 Menu 列表。注意,必须放在前面,不然下面 setChecked 没数据节点
+		menuOptions.value = handleTree(await MenuApi.getSimpleMenusList())
+		// 设置数据
+		formData.id = row.id
+		formData.name = row.name
+		formData.code = row.code
+		formLoading.value = true
+		try {
+			formData.value.menuIds = await PermissionApi.getRoleMenuList(row.id)
+			// 设置选中
+			formData.value.menuIds.forEach((menuId : number) => {
+				treeRef.value.setChecked(menuId, true, false)
+			})
+		} finally {
+			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
+		// 提交请求
+		formLoading.value = true
+		try {
+			const data = {
+				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)
+			message.success(t('common.updateSuccess'))
+			dialogVisible.value = false
+			// 发送操作成功的事件
+			emit('success')
+		} finally {
+			formLoading.value = false
+		}
+	}
+
+	/** 重置表单 */
+	const resetForm = () => {
+		// 重置选项
+		treeNodeAll.value = false
+		menuExpand.value = false
+		// 重置表单
+		formData.value = {
+			id: 0,
+			name: '',
+			code: '',
+			menuIds: []
+		}
+		treeRef.value?.setCheckedNodes([])
+		formRef.value?.resetFields()
+	}
+
+	/** 全选/全不选 */
+	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
+		}
+	}
+</script>
+<style lang="scss" scoped>
+	.cardHeight {
+		width: 100%;
+		max-height: 400px;
+		overflow-y: scroll;
+	}
+</style>

+ 147 - 167
src/views/system/role/RoleDataPermissionForm.vue

@@ -1,167 +1,147 @@
-<template>
-  <Dialog v-model="dialogVisible" title="菜单权限" width="800">
-    <el-form ref="formRef" v-loading="formLoading" :model="formData" label-width="80px">
-      <el-form-item label="角色名称">
-        <el-tag>{{ formData.name }}</el-tag>
-      </el-form-item>
-      <el-form-item label="角色标识">
-        <el-tag>{{ formData.code }}</el-tag>
-      </el-form-item>
-      <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-select>
-      </el-form-item>
-    </el-form>
-    <el-form-item
-      v-if="formData.dataScope === SystemDataScopeEnum.DEPT_CUSTOM"
-      label="权限范围"
-      style="display: flex"
-    >
-      <el-card class="card" shadow="never">
-        <template #header>
-          全选/全不选:
-          <el-switch
-            v-model="treeNodeAll"
-            active-text="是"
-            inactive-text="否"
-            inline-prompt
-            @change="handleCheckedTreeNodeAll()"
-          />
-          全部展开/折叠:
-          <el-switch
-            v-model="deptExpand"
-            active-text="展开"
-            inactive-text="折叠"
-            inline-prompt
-            @change="handleCheckedTreeExpand"
-          />
-          父子联动(选中父节点,自动选择子节点):
-          <el-switch v-model="checkStrictly" active-text="是" inactive-text="否" inline-prompt />
-        </template>
-        <el-tree
-          ref="treeRef"
-          :check-strictly="!checkStrictly"
-          :data="deptOptions"
-          :props="defaultProps"
-          default-expand-all
-          empty-text="加载中,请稍后"
-          node-key="id"
-          show-checkbox
-        />
-      </el-card>
-    </el-form-item>
-    <template #footer>
-      <el-button :disabled="formLoading" type="primary" @click="submitForm">确 定</el-button>
-      <el-button @click="dialogVisible = false">取 消</el-button>
-    </template>
-  </Dialog>
-</template>
-<script lang="ts" setup>
-import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
-import { defaultProps, handleTree } from '@/utils/tree'
-import { SystemDataScopeEnum } from '@/utils/constants'
-import * as RoleApi from '@/api/system/role'
-import * as DeptApi from '@/api/system/dept'
-import * as PermissionApi from '@/api/system/permission'
-
-defineOptions({ name: 'SystemRoleDataPermissionForm' })
-
-const { t } = useI18n() // 国际化
-const message = useMessage() // 消息弹窗
-
-const dialogVisible = ref(false) // 弹窗的是否展示
-const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
-const formData = reactive({
-  id: 0,
-  name: '',
-  code: '',
-  dataScope: undefined,
-  dataScopeDeptIds: []
-})
-const formRef = ref() // 表单 Ref
-const deptOptions = ref<any[]>([]) // 部门树形结构
-const deptExpand = ref(false) // 展开/折叠
-const treeRef = ref() // 菜单树组件 Ref
-const treeNodeAll = ref(false) // 全选/全不选
-const checkStrictly = ref(true) // 是否严格模式,即父子不关联
-
-/** 打开弹窗 */
-const open = async (row: RoleApi.RoleVO) => {
-  dialogVisible.value = true
-  resetForm()
-  // 加载 Dept 列表。注意,必须放在前面,不然下面 setChecked 没数据节点
-  deptOptions.value = handleTree(await DeptApi.getSimpleDeptList())
-  // 设置数据
-  formData.id = row.id
-  formData.name = row.name
-  formData.code = row.code
-  formData.dataScope = row.dataScope
-  row.dataScopeDeptIds?.forEach((deptId: number) => {
-    treeRef.value.setChecked(deptId, true, false)
-  })
-}
-defineExpose({ open }) // 提供 open 方法,用于打开弹窗
-
-/** 提交表单 */
-const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
-const submitForm = async () => {
-  formLoading.value = true
-  try {
-    const data = {
-      roleId: formData.id,
-      dataScope: formData.dataScope,
-      dataScopeDeptIds:
-        formData.dataScope !== SystemDataScopeEnum.DEPT_CUSTOM
-          ? []
-          : treeRef.value.getCheckedKeys(false)
-    }
-    await PermissionApi.assignRoleDataScope(data)
-    message.success(t('common.updateSuccess'))
-    dialogVisible.value = false
-    // 发送操作成功的事件
-    emit('success')
-  } finally {
-    formLoading.value = false
-  }
-}
-
-/** 重置表单 */
-const resetForm = () => {
-  // 重置选项
-  treeNodeAll.value = false
-  deptExpand.value = false
-  checkStrictly.value = true
-  // 重置表单
-  formData.value = {
-    id: 0,
-    name: '',
-    code: '',
-    dataScope: undefined,
-    dataScopeDeptIds: []
-  }
-  treeRef.value?.setCheckedNodes([])
-  formRef.value?.resetFields()
-}
-
-/** 全选/全不选 */
-const handleCheckedTreeNodeAll = () => {
-  treeRef.value.setCheckedNodes(treeNodeAll.value ? deptOptions.value : [])
-}
-
-/** 展开/折叠全部 */
-const handleCheckedTreeExpand = () => {
-  const nodes = treeRef.value?.store.nodesMap
-  for (let node in nodes) {
-    if (nodes[node].expanded === deptExpand.value) {
-      continue
-    }
-    nodes[node].expanded = deptExpand.value
-  }
-}
-</script>
+<template>
+	<Dialog v-model="dialogVisible" title="菜单权限" width="800">
+		<el-form ref="formRef" v-loading="formLoading" :model="formData" label-width="80px">
+			<el-row :gutter="30">
+				<el-col span="12">
+					<el-form-item label="角色名称">
+						<el-tag>{{ formData.name }}</el-tag>
+					</el-form-item>
+				</el-col>
+				<el-col span="12">
+					<el-form-item label="角色标识">
+						<el-tag>{{ formData.code }}</el-tag>
+					</el-form-item>
+				</el-col>
+			</el-row>
+			<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-select>
+			</el-form-item>
+		</el-form>
+		<el-form-item v-if="formData.dataScope === SystemDataScopeEnum.DEPT_CUSTOM" label="权限范围" style="display: flex">
+			<el-card class="card" shadow="never">
+				<template #header>
+					全选/全不选:
+					<el-switch v-model="treeNodeAll" active-text="是" inactive-text="否" inline-prompt
+						@change="handleCheckedTreeNodeAll()" />
+					全部展开/折叠:
+					<el-switch v-model="deptExpand" active-text="展开" inactive-text="折叠" inline-prompt
+						@change="handleCheckedTreeExpand" />
+					父子联动(选中父节点,自动选择子节点):
+					<el-switch v-model="checkStrictly" active-text="是" inactive-text="否" inline-prompt />
+				</template>
+				<el-tree ref="treeRef" :check-strictly="!checkStrictly" :data="deptOptions" :props="defaultProps"
+					default-expand-all empty-text="加载中,请稍后" node-key="id" show-checkbox />
+			</el-card>
+		</el-form-item>
+		<template #footer>
+			<el-button :disabled="formLoading" type="primary" @click="submitForm">确 定</el-button>
+			<el-button @click="dialogVisible = false">取 消</el-button>
+		</template>
+	</Dialog>
+</template>
+<script lang="ts" setup>
+	import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+	import { defaultProps, handleTree } from '@/utils/tree'
+	import { SystemDataScopeEnum } from '@/utils/constants'
+	import * as RoleApi from '@/api/system/role'
+	import * as DeptApi from '@/api/system/dept'
+	import * as PermissionApi from '@/api/system/permission'
+
+	defineOptions({ name: 'SystemRoleDataPermissionForm' })
+
+	const { t } = useI18n() // 国际化
+	const message = useMessage() // 消息弹窗
+
+	const dialogVisible = ref(false) // 弹窗的是否展示
+	const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
+	const formData = reactive({
+		id: 0,
+		name: '',
+		code: '',
+		dataScope: undefined,
+		dataScopeDeptIds: []
+	})
+	const formRef = ref() // 表单 Ref
+	const deptOptions = ref<any[]>([]) // 部门树形结构
+	const deptExpand = ref(false) // 展开/折叠
+	const treeRef = ref() // 菜单树组件 Ref
+	const treeNodeAll = ref(false) // 全选/全不选
+	const checkStrictly = ref(true) // 是否严格模式,即父子不关联
+
+	/** 打开弹窗 */
+	const open = async (row : RoleApi.RoleVO) => {
+		dialogVisible.value = true
+		resetForm()
+		// 加载 Dept 列表。注意,必须放在前面,不然下面 setChecked 没数据节点
+		deptOptions.value = handleTree(await DeptApi.getSimpleDeptList())
+		// 设置数据
+		formData.id = row.id
+		formData.name = row.name
+		formData.code = row.code
+		formData.dataScope = row.dataScope
+		row.dataScopeDeptIds?.forEach((deptId : number) => {
+			treeRef.value.setChecked(deptId, true, false)
+		})
+	}
+	defineExpose({ open }) // 提供 open 方法,用于打开弹窗
+
+	/** 提交表单 */
+	const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
+	const submitForm = async () => {
+		formLoading.value = true
+		try {
+			const data = {
+				roleId: formData.id,
+				dataScope: formData.dataScope,
+				dataScopeDeptIds:
+					formData.dataScope !== SystemDataScopeEnum.DEPT_CUSTOM
+						? []
+						: treeRef.value.getCheckedKeys(false)
+			}
+			await PermissionApi.assignRoleDataScope(data)
+			message.success(t('common.updateSuccess'))
+			dialogVisible.value = false
+			// 发送操作成功的事件
+			emit('success')
+		} finally {
+			formLoading.value = false
+		}
+	}
+
+	/** 重置表单 */
+	const resetForm = () => {
+		// 重置选项
+		treeNodeAll.value = false
+		deptExpand.value = false
+		checkStrictly.value = true
+		// 重置表单
+		formData.value = {
+			id: 0,
+			name: '',
+			code: '',
+			dataScope: undefined,
+			dataScopeDeptIds: []
+		}
+		treeRef.value?.setCheckedNodes([])
+		formRef.value?.resetFields()
+	}
+
+	/** 全选/全不选 */
+	const handleCheckedTreeNodeAll = () => {
+		treeRef.value.setCheckedNodes(treeNodeAll.value ? deptOptions.value : [])
+	}
+
+	/** 展开/折叠全部 */
+	const handleCheckedTreeExpand = () => {
+		const nodes = treeRef.value?.store.nodesMap
+		for (let node in nodes) {
+			if (nodes[node].expanded === deptExpand.value) {
+				continue
+			}
+			nodes[node].expanded = deptExpand.value
+		}
+	}
+</script>

+ 139 - 126
src/views/system/role/RoleForm.vue

@@ -1,126 +1,139 @@
-<template>
-  <Dialog v-model="dialogVisible" :title="dialogTitle">
-    <el-form
-      ref="formRef"
-      v-loading="formLoading"
-      :model="formData"
-      :rules="formRules"
-      label-width="80px"
-    >
-      <el-form-item label="角色名称" prop="name">
-        <el-input v-model="formData.name" placeholder="请输入角色名称" />
-      </el-form-item>
-      <el-form-item label="角色标识" prop="code">
-        <el-input v-model="formData.code" placeholder="请输入角色标识" />
-      </el-form-item>
-      <el-form-item label="显示顺序" prop="sort">
-        <el-input v-model="formData.sort" placeholder="请输入显示顺序" />
-      </el-form-item>
-      <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"
-            :label="dict.label"
-            :value="dict.value"
-          />
-        </el-select>
-      </el-form-item>
-      <el-form-item label="备注" prop="remark">
-        <el-input v-model="formData.remark" placeholder="请输备注" type="textarea" />
-      </el-form-item>
-    </el-form>
-    <template #footer>
-      <el-button :disabled="formLoading" type="primary" @click="submitForm">确 定</el-button>
-      <el-button @click="dialogVisible = false">取 消</el-button>
-    </template>
-  </Dialog>
-</template>
-<script lang="ts" setup>
-import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
-import { CommonStatusEnum } from '@/utils/constants'
-import * as RoleApi from '@/api/system/role'
-
-defineOptions({ name: 'SystemRoleForm' })
-
-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: '',
-  code: '',
-  sort: undefined,
-  status: CommonStatusEnum.ENABLE,
-  remark: ''
-})
-const formRules = reactive({
-  name: [{ required: true, message: '岗位标题不能为空', trigger: 'blur' }],
-  code: [{ required: true, message: '岗位编码不能为空', trigger: 'change' }],
-  sort: [{ required: true, message: '岗位顺序不能为空', trigger: 'change' }],
-  status: [{ required: true, message: '岗位状态不能为空', trigger: 'change' }],
-  remark: [{ required: false, message: '岗位内容不能为空', trigger: 'blur' }]
-})
-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
-    try {
-      formData.value = await RoleApi.getRole(id)
-    } finally {
-      formLoading.value = false
-    }
-  }
-}
-
-/** 重置表单 */
-const resetForm = () => {
-  formData.value = {
-    id: undefined,
-    name: '',
-    code: '',
-    sort: undefined,
-    status: CommonStatusEnum.ENABLE,
-    remark: ''
-  }
-  formRef.value?.resetFields()
-}
-defineExpose({ open }) // 提供 open 方法,用于打开弹窗
-
-/** 提交表单 */
-const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
-const submitForm = async () => {
-  // 校验表单
-  if (!formRef) return
-  const valid = await formRef.value.validate()
-  if (!valid) return
-  // 提交请求
-  formLoading.value = true
-  try {
-    const data = formData.value as unknown as RoleApi.RoleVO
-    if (formType.value === 'create') {
-      await RoleApi.createRole(data)
-      message.success(t('common.createSuccess'))
-    } else {
-      await RoleApi.updateRole(data)
-      message.success(t('common.updateSuccess'))
-    }
-    dialogVisible.value = false
-    // 发送操作成功的事件
-    emit('success')
-  } finally {
-    formLoading.value = false
-  }
-}
-</script>
+<template>
+	<Dialog v-model="dialogVisible" :title="dialogTitle">
+		<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="name">
+						<el-input v-model="formData.name" placeholder="请输入角色名称" />
+					</el-form-item>
+				</el-col>
+				<el-col span="12">
+					<el-form-item label="角色标识" prop="code">
+						<el-input v-model="formData.code" placeholder="请输入角色标识" />
+					</el-form-item>
+				</el-col>
+			</el-row>
+			<el-row :gutter="30">
+				<el-col span="12">
+					<el-form-item label="显示顺序" prop="sort">
+						<el-input v-model="formData.sort" placeholder="请输入显示顺序" />
+					</el-form-item>
+				</el-col>
+				<el-col span="12">
+					<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"
+								:label="dict.label" :value="dict.value" />
+						</el-select>
+					</el-form-item>
+				</el-col>
+			</el-row>
+			<el-form-item label="备注" prop="remark">
+				<el-input style="width:430px" v-model="formData.remark" placeholder="请输备注" type="textarea" />
+			</el-form-item>
+		</el-form>
+		<template #footer>
+			<el-button :disabled="formLoading" type="primary" @click="submitForm">确 定</el-button>
+			<el-button @click="dialogVisible = false">取 消</el-button>
+		</template>
+	</Dialog>
+</template>
+<script lang="ts" setup>
+	import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+	import { CommonStatusEnum } from '@/utils/constants'
+	import * as RoleApi from '@/api/system/role'
+
+	defineOptions({ name: 'SystemRoleForm' })
+
+	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: '',
+		code: '',
+		sort: undefined,
+		status: CommonStatusEnum.ENABLE,
+		remark: ''
+	})
+	const formRules = reactive({
+		name: [{ required: true, message: '岗位标题不能为空', trigger: 'blur' }],
+		code: [{ required: true, message: '岗位编码不能为空', trigger: 'change' }],
+		sort: [{ required: true, message: '岗位顺序不能为空', trigger: 'change' }],
+		status: [{ required: true, message: '岗位状态不能为空', trigger: 'change' }],
+		remark: [{ required: false, message: '岗位内容不能为空', trigger: 'blur' }]
+	})
+	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
+			try {
+				formData.value = await RoleApi.getRole(id)
+			} finally {
+				formLoading.value = false
+			}
+		}
+	}
+
+	/** 重置表单 */
+	const resetForm = () => {
+		formData.value = {
+			id: undefined,
+			name: '',
+			code: '',
+			sort: undefined,
+			status: CommonStatusEnum.ENABLE,
+			remark: ''
+		}
+		formRef.value?.resetFields()
+	}
+	defineExpose({ open }) // 提供 open 方法,用于打开弹窗
+
+	/** 提交表单 */
+	const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
+	const submitForm = async () => {
+		// 校验表单
+		if (!formRef) return
+		const valid = await formRef.value.validate()
+		if (!valid) return
+		// 提交请求
+		formLoading.value = true
+		try {
+			const data = formData.value as unknown as RoleApi.RoleVO
+			if (formType.value === 'create') {
+				await RoleApi.createRole(data)
+				message.success(t('common.createSuccess'))
+			} else {
+				await RoleApi.updateRole(data)
+				message.success(t('common.updateSuccess'))
+			}
+			dialogVisible.value = false
+			// 发送操作成功的事件
+			emit('success')
+		} finally {
+			formLoading.value = false
+		}
+	}
+</script>
+<style lang="scss" scoped>
+	.el-row {
+
+		.el-input,
+		.el-select,
+		.el-input-number,
+		.el-date-editor {
+			width: 160px;
+		}
+	}
+</style>

+ 198 - 183
src/views/system/tenant/TenantForm.vue

@@ -1,183 +1,198 @@
-<template>
-  <Dialog v-model="dialogVisible" :title="dialogTitle" width="50%">
-    <el-form
-      ref="formRef"
-      v-loading="formLoading"
-      :model="formData"
-      :rules="formRules"
-      label-width="80px"
-    >
-      <el-form-item label="租户名" prop="name">
-        <el-input v-model="formData.name" placeholder="请输入租户名" />
-      </el-form-item>
-      <el-form-item label="租户套餐" prop="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-select>
-      </el-form-item>
-      <el-form-item label="联系人" prop="contactName">
-        <el-input v-model="formData.contactName" placeholder="请输入联系人" />
-      </el-form-item>
-      <el-form-item label="联系手机" prop="contactMobile">
-        <el-input v-model="formData.contactMobile" placeholder="请输入联系手机" />
-      </el-form-item>
-      <el-form-item v-if="formData.id === undefined" label="用户名称" prop="username">
-        <el-input v-model="formData.username" placeholder="请输入用户名称" />
-      </el-form-item>
-      <el-form-item v-if="formData.id === undefined" label="用户密码" prop="password">
-        <el-input
-          v-model="formData.password"
-          placeholder="请输入用户密码"
-          show-password
-          type="password"
-        />
-      </el-form-item>
-      <el-form-item label="账号额度" prop="accountCount">
-        <el-input-number
-          v-model="formData.accountCount"
-          :min="0"
-          controls-position="right"
-          placeholder="请输入账号额度"
-        />
-      </el-form-item>
-      <el-form-item label="过期时间" prop="expireTime">
-        <el-date-picker
-          v-model="formData.expireTime"
-          clearable
-          placeholder="请选择过期时间"
-          type="date"
-          value-format="x"
-        />
-      </el-form-item>
-      <el-form-item label="绑定域名" prop="website">
-        <el-input v-model="formData.website" placeholder="请输入绑定域名" />
-      </el-form-item>
-      <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-radio-group>
-      </el-form-item>
-    </el-form>
-    <template #footer>
-      <el-button :disabled="formLoading" type="primary" @click="submitForm">确 定</el-button>
-      <el-button @click="dialogVisible = false">取 消</el-button>
-    </template>
-  </Dialog>
-</template>
-<script lang="ts" setup>
-import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
-import * as TenantApi from '@/api/system/tenant'
-import { CommonStatusEnum } from '@/utils/constants'
-import * as TenantPackageApi from '@/api/system/tenantPackage'
-
-defineOptions({ name: 'SystemTenantForm' })
-
-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: undefined,
-  packageId: undefined,
-  contactName: undefined,
-  contactMobile: undefined,
-  accountCount: undefined,
-  expireTime: undefined,
-  website: undefined,
-  status: CommonStatusEnum.ENABLE,
-  // 新增专属
-  username: undefined,
-  password: undefined
-})
-const formRules = reactive({
-  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 formRef = ref() // 表单 Ref
-const packageList = ref([] as TenantPackageApi.TenantPackageVO[]) // 租户套餐
-
-/** 打开弹窗 */
-const open = async (type: string, id?: number) => {
-  dialogVisible.value = true
-  dialogTitle.value = t('action.' + type)
-  formType.value = type
-  resetForm()
-  // 修改时,设置数据
-  if (id) {
-    formLoading.value = true
-    try {
-      formData.value = await TenantApi.getTenant(id)
-    } finally {
-      formLoading.value = false
-    }
-  }
-  // 加载套餐列表
-  packageList.value = await TenantPackageApi.getTenantPackageList()
-}
-defineExpose({ open }) // 提供 open 方法,用于打开弹窗
-
-/** 提交表单 */
-const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
-const submitForm = async () => {
-  // 校验表单
-  if (!formRef) return
-  const valid = await formRef.value.validate()
-  if (!valid) return
-  // 提交请求
-  formLoading.value = true
-  try {
-    const data = formData.value as unknown as TenantApi.TenantVO
-    if (formType.value === 'create') {
-      await TenantApi.createTenant(data)
-      message.success(t('common.createSuccess'))
-    } else {
-      await TenantApi.updateTenant(data)
-      message.success(t('common.updateSuccess'))
-    }
-    dialogVisible.value = false
-    // 发送操作成功的事件
-    emit('success')
-  } finally {
-    formLoading.value = false
-  }
-}
-
-/** 重置表单 */
-const resetForm = () => {
-  formData.value = {
-    id: undefined,
-    name: undefined,
-    packageId: undefined,
-    contactName: undefined,
-    contactMobile: undefined,
-    accountCount: undefined,
-    expireTime: undefined,
-    website: undefined,
-    status: CommonStatusEnum.ENABLE,
-    username: undefined,
-    password: undefined
-  }
-  formRef.value?.resetFields()
-}
-</script>
+<template>
+	<Dialog v-model="dialogVisible" :title="dialogTitle" width="50%">
+		<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="品牌方" :label-width="labelWidth" prop="name">
+						<el-input v-model="formData.name" placeholder="请输入品牌方" />
+					</el-form-item>
+				</el-col>
+				<el-col span="12">
+					<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-select>
+					</el-form-item>
+				</el-col>
+			</el-row>
+			<el-row :gutter="30">
+				<el-col span="12">
+					<el-form-item label="联系人" :label-width="labelWidth" prop="contactName">
+						<el-input v-model="formData.contactName" placeholder="请输入联系人" />
+					</el-form-item>
+				</el-col>
+				<el-col span="12">
+					<el-form-item label="联系手机" :label-width="labelWidth" prop="contactMobile">
+						<el-input v-model="formData.contactMobile" placeholder="请输入联系手机" />
+					</el-form-item>
+				</el-col>
+			</el-row>
+			<el-row :gutter="30">
+				<el-col span="12">
+					<el-form-item v-if="formData.id === undefined" label="用户名称" :label-width="labelWidth"
+						prop="username">
+						<el-input v-model="formData.username" placeholder="请输入用户名称" />
+					</el-form-item>
+				</el-col>
+				<el-col span="12">
+					<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>
+				</el-col>
+			</el-row>
+			<el-row :gutter="30">
+				<el-col span="12">
+					<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>
+				</el-col>
+				<el-col span="12">
+					<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>
+				</el-col>
+			</el-row>
+			<el-row :gutter="30">
+				<el-col span="12">
+					<el-form-item label="绑定域名" :label-width="labelWidth" prop="website">
+						<el-input v-model="formData.website" placeholder="请输入绑定域名" />
+					</el-form-item>
+				</el-col>
+				<el-col span="12">
+					<el-form-item label="品牌方状态" :label-width="labelWidth" class="item" 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-radio-group>
+					</el-form-item>
+				</el-col>
+			</el-row>
+		</el-form>
+		<template #footer>
+			<el-button :disabled="formLoading" type="primary" @click="submitForm">确 定</el-button>
+			<el-button @click="dialogVisible = false">取 消</el-button>
+		</template>
+	</Dialog>
+</template>
+<script lang="ts" setup>
+	import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+	import * as TenantApi from '@/api/system/tenant'
+	import { CommonStatusEnum } from '@/utils/constants'
+	import * as TenantPackageApi from '@/api/system/tenantPackage'
+
+	defineOptions({ name: 'SystemTenantForm' })
+	const labelWidth = 100
+	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: undefined,
+		packageId: undefined,
+		contactName: undefined,
+		contactMobile: undefined,
+		accountCount: undefined,
+		expireTime: undefined,
+		website: undefined,
+		status: CommonStatusEnum.ENABLE,
+		// 新增专属
+		username: undefined,
+		password: undefined
+	})
+	const formRules = reactive({
+		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 formRef = ref() // 表单 Ref
+	const packageList = ref([] as TenantPackageApi.TenantPackageVO[]) // 租户套餐
+
+	/** 打开弹窗 */
+	const open = async (type : string, id ?: number) => {
+		dialogVisible.value = true
+		dialogTitle.value = t('action.' + type)
+		formType.value = type
+		resetForm()
+		// 修改时,设置数据
+		if (id) {
+			formLoading.value = true
+			try {
+				formData.value = await TenantApi.getTenant(id)
+			} finally {
+				formLoading.value = false
+			}
+		}
+		// 加载套餐列表
+		packageList.value = await TenantPackageApi.getTenantPackageList()
+	}
+	defineExpose({ open }) // 提供 open 方法,用于打开弹窗
+
+	/** 提交表单 */
+	const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
+	const submitForm = async () => {
+		// 校验表单
+		if (!formRef) return
+		const valid = await formRef.value.validate()
+		if (!valid) return
+		// 提交请求
+		formLoading.value = true
+		try {
+			const data = formData.value as unknown as TenantApi.TenantVO
+			if (formType.value === 'create') {
+				await TenantApi.createTenant(data)
+				message.success(t('common.createSuccess'))
+			} else {
+				await TenantApi.updateTenant(data)
+				message.success(t('common.updateSuccess'))
+			}
+			dialogVisible.value = false
+			// 发送操作成功的事件
+			emit('success')
+		} finally {
+			formLoading.value = false
+		}
+	}
+
+	/** 重置表单 */
+	const resetForm = () => {
+		formData.value = {
+			id: undefined,
+			name: undefined,
+			packageId: undefined,
+			contactName: undefined,
+			contactMobile: undefined,
+			accountCount: undefined,
+			expireTime: undefined,
+			website: undefined,
+			status: CommonStatusEnum.ENABLE,
+			username: undefined,
+			password: undefined
+		}
+		formRef.value?.resetFields()
+	}
+</script>
+<style lang="scss" scoped>
+	.el-row {
+
+		.el-input,
+		.el-select,
+		.el-input-number,
+		.el-date-editor {
+			width: 200px;
+		}
+	}
+</style>

+ 178 - 194
src/views/system/tenantPackage/TenantPackageForm.vue

@@ -1,194 +1,178 @@
-<template>
-  <Dialog v-model="dialogVisible" :title="dialogTitle">
-    <el-form
-      ref="formRef"
-      v-loading="formLoading"
-      :model="formData"
-      :rules="formRules"
-      label-width="80px"
-    >
-      <el-form-item label="套餐名" prop="name">
-        <el-input v-model="formData.name" placeholder="请输入套餐名" />
-      </el-form-item>
-      <el-form-item label="菜单权限">
-        <el-card class="cardHeight">
-          <template #header>
-            全选/全不选:
-            <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"
-            />
-          </template>
-          <el-tree
-            ref="treeRef"
-            :data="menuOptions"
-            :props="defaultProps"
-            empty-text="加载中,请稍候"
-            node-key="id"
-            show-checkbox
-          />
-        </el-card>
-      </el-form-item>
-      <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-radio-group>
-      </el-form-item>
-      <el-form-item label="备注" prop="remark">
-        <el-input v-model="formData.remark" placeholder="请输入备注" />
-      </el-form-item>
-    </el-form>
-    <template #footer>
-      <el-button :disabled="formLoading" type="primary" @click="submitForm">确 定</el-button>
-      <el-button @click="dialogVisible = false">取 消</el-button>
-    </template>
-  </Dialog>
-</template>
-<script lang="ts" setup>
-import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
-import { CommonStatusEnum } from '@/utils/constants'
-import { defaultProps, handleTree } from '@/utils/tree'
-import * as TenantPackageApi from '@/api/system/tenantPackage'
-import * as MenuApi from '@/api/system/menu'
-import { ElTree } from 'element-plus'
-
-defineOptions({ name: 'SystemTenantPackageForm' })
-
-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: null,
-  name: null,
-  remark: null,
-  menuIds: [],
-  status: CommonStatusEnum.ENABLE
-})
-const formRules = reactive({
-  name: [{ required: true, message: '套餐名不能为空', trigger: 'blur' }],
-  status: [{ required: true, message: '状态不能为空', trigger: 'blur' }],
-  menuIds: [{ required: true, message: '关联的菜单编号不能为空', trigger: 'blur' }]
-})
-const formRef = ref() // 表单 Ref
-const menuOptions = ref<any[]>([]) // 树形结构数据
-const menuExpand = ref(false) // 展开/折叠
-const treeRef = ref<InstanceType<typeof ElTree>>() // 树组件 Ref
-const treeNodeAll = ref(false) // 全选/全不选
-
-/** 打开弹窗 */
-const open = async (type: string, id?: number) => {
-  dialogVisible.value = true
-  dialogTitle.value = t('action.' + type)
-  formType.value = type
-  resetForm()
-  // 加载 Menu 列表。注意,必须放在前面,不然下面 setChecked 没数据节点
-  menuOptions.value = handleTree(await MenuApi.getSimpleMenusList())
-  // 修改时,设置数据
-  if (id) {
-    formLoading.value = true
-    try {
-      const res = await TenantPackageApi.getTenantPackage(id)
-      // 设置选中
-      formData.value = res
-      // 设置选中
-      res.menuIds.forEach((menuId: number) => {
-        treeRef.value!.setChecked(menuId, true, false)
-      })
-    } finally {
-      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
-  // 提交请求
-  formLoading.value = true
-  try {
-    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>) // 获得半选中的父节点
-    ]
-    if (formType.value === 'create') {
-      await TenantPackageApi.createTenantPackage(data)
-      message.success(t('common.createSuccess'))
-    } else {
-      await TenantPackageApi.updateTenantPackage(data)
-      message.success(t('common.updateSuccess'))
-    }
-    dialogVisible.value = false
-    // 发送操作成功的事件
-    emit('success')
-  } finally {
-    formLoading.value = false
-  }
-}
-
-/** 重置表单 */
-const resetForm = () => {
-  // 重置选项
-  treeNodeAll.value = false
-  menuExpand.value = false
-  // 重置表单
-  formData.value = {
-    id: null,
-    name: null,
-    remark: null,
-    menuIds: [],
-    status: CommonStatusEnum.ENABLE
-  }
-  treeRef.value?.setCheckedNodes([])
-  formRef.value?.resetFields()
-}
-
-/** 全选/全不选 */
-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
-  }
-}
-</script>
-<style lang="scss" scoped>
-.cardHeight {
-  width: 100%;
-  max-height: 400px;
-  overflow-y: scroll;
-}
-</style>
+<template>
+	<Dialog v-model="dialogVisible" :title="dialogTitle">
+		<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="name">
+						<el-input v-model="formData.name" placeholder="请输入套餐名" />
+					</el-form-item>
+				</el-col>
+				<el-col span="12">
+					<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-radio-group>
+					</el-form-item>
+				</el-col>
+			</el-row>
+
+			<el-form-item label="菜单权限">
+				<el-card class="cardHeight">
+					<template #header>
+						全选/全不选:
+						<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" />
+					</template>
+					<el-tree ref="treeRef" :data="menuOptions" :props="defaultProps" empty-text="加载中,请稍候" node-key="id"
+						show-checkbox />
+				</el-card>
+			</el-form-item>
+
+			<el-form-item label="备注" prop="remark">
+				<el-input v-model="formData.remark" placeholder="请输入备注" />
+			</el-form-item>
+		</el-form>
+		<template #footer>
+			<el-button :disabled="formLoading" type="primary" @click="submitForm">确 定</el-button>
+			<el-button @click="dialogVisible = false">取 消</el-button>
+		</template>
+	</Dialog>
+</template>
+<script lang="ts" setup>
+	import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+	import { CommonStatusEnum } from '@/utils/constants'
+	import { defaultProps, handleTree } from '@/utils/tree'
+	import * as TenantPackageApi from '@/api/system/tenantPackage'
+	import * as MenuApi from '@/api/system/menu'
+	import { ElTree } from 'element-plus'
+
+	defineOptions({ name: 'SystemTenantPackageForm' })
+
+	const labelWidth = 100
+	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: null,
+		name: null,
+		remark: null,
+		menuIds: [],
+		status: CommonStatusEnum.ENABLE
+	})
+	const formRules = reactive({
+		name: [{ required: true, message: '套餐名不能为空', trigger: 'blur' }],
+		status: [{ required: true, message: '状态不能为空', trigger: 'blur' }],
+		menuIds: [{ required: true, message: '关联的菜单编号不能为空', trigger: 'blur' }]
+	})
+	const formRef = ref() // 表单 Ref
+	const menuOptions = ref<any[]>([]) // 树形结构数据
+	const menuExpand = ref(false) // 展开/折叠
+	const treeRef = ref<InstanceType<typeof ElTree>>() // 树组件 Ref
+	const treeNodeAll = ref(false) // 全选/全不选
+
+	/** 打开弹窗 */
+	const open = async (type : string, id ?: number) => {
+		dialogVisible.value = true
+		dialogTitle.value = t('action.' + type)
+		formType.value = type
+		resetForm()
+		// 加载 Menu 列表。注意,必须放在前面,不然下面 setChecked 没数据节点
+		menuOptions.value = handleTree(await MenuApi.getSimpleMenusList())
+		// 修改时,设置数据
+		if (id) {
+			formLoading.value = true
+			try {
+				const res = await TenantPackageApi.getTenantPackage(id)
+				// 设置选中
+				formData.value = res
+				// 设置选中
+				res.menuIds.forEach((menuId : number) => {
+					treeRef.value!.setChecked(menuId, true, false)
+				})
+			} finally {
+				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
+		// 提交请求
+		formLoading.value = true
+		try {
+			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>) // 获得半选中的父节点
+			]
+			if (formType.value === 'create') {
+				await TenantPackageApi.createTenantPackage(data)
+				message.success(t('common.createSuccess'))
+			} else {
+				await TenantPackageApi.updateTenantPackage(data)
+				message.success(t('common.updateSuccess'))
+			}
+			dialogVisible.value = false
+			// 发送操作成功的事件
+			emit('success')
+		} finally {
+			formLoading.value = false
+		}
+	}
+
+	/** 重置表单 */
+	const resetForm = () => {
+		// 重置选项
+		treeNodeAll.value = false
+		menuExpand.value = false
+		// 重置表单
+		formData.value = {
+			id: null,
+			name: null,
+			remark: null,
+			menuIds: [],
+			status: CommonStatusEnum.ENABLE
+		}
+		treeRef.value?.setCheckedNodes([])
+		formRef.value?.resetFields()
+	}
+
+	/** 全选/全不选 */
+	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
+		}
+	}
+</script>
+<style lang="scss" scoped>
+	.cardHeight {
+		width: 100%;
+		max-height: 400px;
+		overflow-y: scroll;
+	}
+</style>

+ 109 - 96
src/views/system/user/UserAssignRoleForm.vue

@@ -1,96 +1,109 @@
-<template>
-  <Dialog v-model="dialogVisible" title="分配角色">
-    <el-form ref="formRef" v-loading="formLoading" :model="formData" label-width="80px">
-      <el-form-item label="用户名称">
-        <el-input v-model="formData.username" :disabled="true" />
-      </el-form-item>
-      <el-form-item label="用户昵称">
-        <el-input v-model="formData.nickname" :disabled="true" />
-      </el-form-item>
-      <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" />
-        </el-select>
-      </el-form-item>
-    </el-form>
-    <template #footer>
-      <el-button :disabled="formLoading" type="primary" @click="submitForm">确 定</el-button>
-      <el-button @click="dialogVisible = false">取 消</el-button>
-    </template>
-  </Dialog>
-</template>
-<script lang="ts" setup>
-import * as PermissionApi from '@/api/system/permission'
-import * as UserApi from '@/api/system/user'
-import * as RoleApi from '@/api/system/role'
-
-defineOptions({ name: 'SystemUserAssignRoleForm' })
-
-const { t } = useI18n() // 国际化
-const message = useMessage() // 消息弹窗
-
-const dialogVisible = ref(false) // 弹窗的是否展示
-const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
-const formData = ref({
-  id: -1,
-  nickname: '',
-  username: '',
-  roleIds: []
-})
-const formRef = ref() // 表单 Ref
-const roleList = ref([] as RoleApi.RoleVO[]) // 角色的列表
-
-/** 打开弹窗 */
-const open = async (row: UserApi.UserVO) => {
-  dialogVisible.value = true
-  resetForm()
-  // 设置数据
-  formData.value.id = row.id
-  formData.value.username = row.username
-  formData.value.nickname = row.nickname
-  // 获得角色拥有的菜单集合
-  formLoading.value = true
-  try {
-    formData.value.roleIds = await PermissionApi.getUserRoleList(row.id)
-  } finally {
-    formLoading.value = false
-  }
-  // 获得角色列表
-  roleList.value = await RoleApi.getSimpleRoleList()
-}
-defineExpose({ open }) // 提供 open 方法,用于打开弹窗
-
-/** 提交表单 */
-const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
-const submitForm = async () => {
-  // 校验表单
-  if (!formRef) return
-  const valid = await formRef.value.validate()
-  if (!valid) return
-  // 提交请求
-  formLoading.value = true
-  try {
-    await PermissionApi.assignUserRole({
-      userId: formData.value.id,
-      roleIds: formData.value.roleIds
-    })
-    message.success(t('common.updateSuccess'))
-    dialogVisible.value = false
-    // 发送操作成功的事件
-    emit('success', true)
-  } finally {
-    formLoading.value = false
-  }
-}
-
-/** 重置表单 */
-const resetForm = () => {
-  formData.value = {
-    id: -1,
-    nickname: '',
-    username: '',
-    roleIds: []
-  }
-  formRef.value?.resetFields()
-}
-</script>
+<template>
+	<Dialog v-model="dialogVisible" title="分配角色">
+		<el-form ref="formRef" v-loading="formLoading" :model="formData" label-width="80px">
+			<el-row :gutter="30">
+				<el-col span="12">
+					<el-form-item label="用户名称">
+						<el-input v-model="formData.username" :disabled="true" />
+					</el-form-item>
+				</el-col>
+				<el-col span="12">
+					<el-form-item label="用户昵称">
+						<el-input v-model="formData.nickname" :disabled="true" />
+					</el-form-item>
+				</el-col>
+			</el-row>
+			<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-select>
+			</el-form-item>
+		</el-form>
+		<template #footer>
+			<el-button :disabled="formLoading" type="primary" @click="submitForm">确 定</el-button>
+			<el-button @click="dialogVisible = false">取 消</el-button>
+		</template>
+	</Dialog>
+</template>
+<style lang="scss" scoped>
+	.el-row {
+		.el-input {
+			width: 150px
+		}
+	}
+</style>
+<script lang="ts" setup>
+	import * as PermissionApi from '@/api/system/permission'
+	import * as UserApi from '@/api/system/user'
+	import * as RoleApi from '@/api/system/role'
+
+	defineOptions({ name: 'SystemUserAssignRoleForm' })
+
+	const { t } = useI18n() // 国际化
+	const message = useMessage() // 消息弹窗
+
+	const dialogVisible = ref(false) // 弹窗的是否展示
+	const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
+	const formData = ref({
+		id: -1,
+		nickname: '',
+		username: '',
+		roleIds: []
+	})
+	const formRef = ref() // 表单 Ref
+	const roleList = ref([] as RoleApi.RoleVO[]) // 角色的列表
+
+	/** 打开弹窗 */
+	const open = async (row : UserApi.UserVO) => {
+		dialogVisible.value = true
+		resetForm()
+		// 设置数据
+		formData.value.id = row.id
+		formData.value.username = row.username
+		formData.value.nickname = row.nickname
+		// 获得角色拥有的菜单集合
+		formLoading.value = true
+		try {
+			formData.value.roleIds = await PermissionApi.getUserRoleList(row.id)
+		} finally {
+			formLoading.value = false
+		}
+		// 获得角色列表
+		roleList.value = await RoleApi.getSimpleRoleList()
+	}
+	defineExpose({ open }) // 提供 open 方法,用于打开弹窗
+
+	/** 提交表单 */
+	const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
+	const submitForm = async () => {
+		// 校验表单
+		if (!formRef) return
+		const valid = await formRef.value.validate()
+		if (!valid) return
+		// 提交请求
+		formLoading.value = true
+		try {
+			await PermissionApi.assignUserRole({
+				userId: formData.value.id,
+				roleIds: formData.value.roleIds
+			})
+			message.success(t('common.updateSuccess'))
+			dialogVisible.value = false
+			// 发送操作成功的事件
+			emit('success', true)
+		} finally {
+			formLoading.value = false
+		}
+	}
+
+	/** 重置表单 */
+	const resetForm = () => {
+		formData.value = {
+			id: -1,
+			nickname: '',
+			username: '',
+			roleIds: []
+		}
+		formRef.value?.resetFields()
+	}
+</script>