Forráskód Böngészése

update:1.商品列表修改;2.隐藏自定义设置按钮,关闭标签栏

xuruhua 1 éve
szülő
commit
73a46cb597

+ 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: 106px;
+				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>

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

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

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

@@ -1,276 +1,276 @@
-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: true, // 搜索图标
+			size: true, // 尺寸图标
+			locale: true, // 多语言图标
+			message: true, // 消息图标
+			tagsView: false, // 标签页
+			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)
+}

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

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

@@ -1,146 +1,118 @@
-<!-- 商品发布 - 基础设置 -->
-<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: '', // 关键字
+		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>

+ 121 - 12
src/views/mall/product/spu/form/index.vue

@@ -4,8 +4,8 @@
 		<ContentWrap v-loading="formLoading">
 
 			<div class="left">
-				<img :src="picUrl" @error="handleError" />
-				<el-tabs v-model="activeName" tab-position="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" />
@@ -34,6 +34,25 @@
 			<el-form style="clear: both;">
 				<el-form-item style="float: right">
 					<el-button @click="dialogVisible = false">返回</el-button>
+					<!-- 不在回收站的才可以停用 -->
+					<el-button v-if="!isDetail && parentTabType!=4" :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" :loading="formLoading" type="primary" @click="handleStatusChange">
+						{{parentRow.status == 0 ?"上架":"下架"}}
+					</el-button>
+
 					<el-button v-if="!isDetail" :loading="formLoading" type="primary" @click="submitForm">
 						保存
 					</el-button>
@@ -56,6 +75,7 @@
 	import DeliveryForm from './DeliveryForm.vue'
 	import { convertToInteger, floatToFixed2, formatToFraction } from '@/utils'
 
+	import { ProductSpuStatusEnum } from '@/utils/constants'
 	defineOptions({ name: 'ProductSpuForm' })
 
 	const { t } = useI18n() // 国际化
@@ -137,18 +157,26 @@
 	})
 	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 handleError = (e) => {
 		e.target.src = "https://static.iocoder.cn/mall/a79f5d2ea6bf0c3c11b2127332dfe2df.jpg"
 	}
 	/** 打开弹窗 */
-	const open = async (id : number, type : string, imageUrl : string) => {
+	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 = id
-		picUrl.value = imageUrl
-		console.log(id, type, imageUrl)
+		productId.value = row.id
+		picUrl.value = row.picUrl
+
 		await getDetail()
 		activeName.value = 'info'
 		openType.value = type
@@ -168,7 +196,7 @@
 	defineExpose({ open }) // 提供 open 方法,用于打开弹窗
 	/** 获得详情 */
 	const getDetail = async () => {
-
+		console.log(productId.value)
 		const id = productId.value as any as number
 		if (id) {
 			formLoading.value = true
@@ -197,6 +225,51 @@
 		}
 	}
 	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 () => {
 		// 提交请求
@@ -253,20 +326,56 @@
 	.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;
+	}
 </style>
 <style lang="scss" scoped>
-	.left {
-		width: 96px;
+	::v-deep .left {
+		width: 106px;
 		float: left;
 
 		img {
-			width: 96%;
-			border: 2px solid #e4e7ee;
-			margin-bottom: -5px;
+			// 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__item {
+		width: 106px;
+		justify-content: center;
+	}
+
+	::v-deep .child-tabs .el-tabs__item {}
+
 	.right {
+		padding: 10px 0;
 		border-left: 2px solid #e4e7ee;
 		margin-left: -2px;
 		float: left;

+ 52 - 61
src/views/mall/product/spu/index.vue

@@ -1,55 +1,11 @@
 <!-- 商品中心 - 商品列表  -->
 <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 style="position: relative;min-height:400px">
+	<ContentWrap style="" class="spu-div">
 		<div style="position: relative;">
 			<div style="text-align: right;background: #fafbfc;padding: 10px;">
-				<el-button v-hasPermi="['product:spu:create']" plain type="primary" @click="openForm(0,'create','')">
-					<Icon class="mr-5px" icon="ep:plus" />
-					新增
-				</el-button>
-				&nbsp;
+
+
 				<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;">
@@ -61,6 +17,11 @@
 					</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">
@@ -96,27 +57,30 @@
 				</el-form>
 			</div>
 		</div>
-		<el-tabs v-model="queryParams.tabType" @tab-click="handleTabClick">
+		<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(o.id,'view',o.picUrl)">
+				<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;"
+						<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">
+								<Setting />
+							</el-icon>
+						</div>
 
 
-						<el-button :icon="Setting" plain type="primary" circle
-							@click.stop="openForm(o.id,'update',o.picUrl)" v-show="o.showSetting" class="setting" />
-
 					</div>
 					<div style="display:flex;align-items: center;">
 						<img :src="o.picUrl"
-							style="width: 100px;height: 100px;margin-right:10px;border:1px solid #303133;border-radius: 5px;"
+							style="width: 100px;height: 100px;margin-right:10px;border:1px solid rgb(220 223 231);border-radius: 5px;"
 							@error="handleError" />
 						<div style='line-height: 25px;'>
 							<p>¥{{o.price}}</p>
@@ -335,9 +299,12 @@
 			list.value = data.list
 			// console.log(list.value)
 			total.value = data.total
+
 		} finally {
 			loading.value = false
 			searchMoreShow.value = false
+			getTabsCount()
+
 		}
 	}
 
@@ -422,15 +389,19 @@
 
 	/** 重置按钮操作 */
 	const resetQuery = () => {
+		queryParams.value.categoryId = undefined
+		queryParams.value.createTime = undefined
 		queryParams.value.name = ""
-		queryFormRef.value.resetFields()
+		// queryFormRef.value.resetFields()
+		// console.log(queryParams.value)
+		// console.log(queryFormRef.value)
 		handleQuery()
 	}
 
 	const formRef = ref()
 	/** 新增或修改 */
-	const openForm = (id : number, type : string, imageUrl : string) => {
-		formRef.value.open(id, type, imageUrl)
+	const openForm = (type : string, row : any, newStatus : number, tabType : number) => {
+		formRef.value.open(type, row, newStatus, tabType)
 		// 修改
 		// if (typeof id === 'number') {
 		// 	push({ name: 'ProductSpuEdit', params: { id } })
@@ -441,8 +412,8 @@
 	}
 
 	/** 查看商品详情 */
-	const openDetail = (id : number, type : string, imageUrl : string) => {
-		formRef.value.open(id, type, imageUrl)
+	const openDetail = (type : string, row : any, newStatus : number) => {
+		formRef.value.open(type, row, newStatus)
 		// push({ name: 'ProductSpuDetail', params: { id } })
 	}
 
@@ -482,6 +453,22 @@
 	})
 </script>
 <style lang="scss" scoped>
+	.spu-div {
+		position: relative;
+		min-height: 400px;
+	}
+
+
+	::v-deep .parent-tabs .el-tabs__nav-wrap::after {
+		position: static !important;
+		/* background-color: #fff; */
+	}
+
+	::v-deep .el-tabs__active-bar {
+		// background-color: #30fdff;
+		background-color: unset;
+	}
+
 	.searchMore {
 		text-align: right;
 		background: #f3f5f8;
@@ -518,9 +505,13 @@
 		}
 
 		.setting {
-			top: 8px;
-			right: 8px;
+			width: 30px;
+			top: 5px;
+			right: 0px;
 			position: absolute;
+			height: 30px;
+			line-height: 30px;
+			text-align: center;
 		}
 
 		p,