This commit is contained in:
2025-12-09 16:42:38 +08:00
parent 89a1391b40
commit 4f8d37483e
9 changed files with 403 additions and 4 deletions

View File

@@ -302,6 +302,7 @@
"useMediaQuery": true,
"useMemoize": true,
"useMemory": true,
"useMobileTable": true,
"useModel": true,
"useMounted": true,
"useMouse": true,

2
auto-imports.d.ts vendored
View File

@@ -336,6 +336,7 @@ declare global {
const useMediaQuery: typeof import('@vueuse/core')['useMediaQuery']
const useMemoize: typeof import('@vueuse/core')['useMemoize']
const useMemory: typeof import('@vueuse/core')['useMemory']
const useMobileTable: typeof import('./src/composables/useMobileTable.js')['useMobileTable']
const useModel: typeof import('vue')['useModel']
const useMounted: typeof import('@vueuse/core')['useMounted']
const useMouse: typeof import('@vueuse/core')['useMouse']
@@ -747,6 +748,7 @@ declare module 'vue' {
readonly useMediaQuery: UnwrapRef<typeof import('@vueuse/core')['useMediaQuery']>
readonly useMemoize: UnwrapRef<typeof import('@vueuse/core')['useMemoize']>
readonly useMemory: UnwrapRef<typeof import('@vueuse/core')['useMemory']>
readonly useMobileTable: UnwrapRef<typeof import('./src/composables/useMobileTable.js')['useMobileTable']>
readonly useModel: UnwrapRef<typeof import('vue')['useModel']>
readonly useMounted: UnwrapRef<typeof import('@vueuse/core')['useMounted']>
readonly useMouse: UnwrapRef<typeof import('@vueuse/core')['useMouse']>

1
components.d.ts vendored
View File

@@ -80,6 +80,7 @@ declare module 'vue' {
ProductCard: typeof import('./src/components/product/ProductCard.vue')['default']
ProductDocumentationDialog: typeof import('./src/components/admin/ProductDocumentationDialog.vue')['default']
ProductFormDialog: typeof import('./src/components/admin/ProductFormDialog.vue')['default']
ResponsiveActionColumn: typeof import('./src/components/common/ResponsiveActionColumn.vue')['default']
RichTextEditor: typeof import('./src/components/common/RichTextEditor.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']

View File

@@ -396,6 +396,138 @@
.filter-buttons {
justify-content: center;
}
/* ===== 移动端表格优化 ===== */
/* 表格容器允许横向滚动 */
.table-wrapper {
overflow-x: auto;
-webkit-overflow-scrolling: touch;
/* 隐藏滚动条但保持滚动功能 */
scrollbar-width: thin;
}
/* 移除固定列效果 - 通过覆盖 Element Plus 的固定列样式 */
.list-page-container .el-table .el-table__fixed,
.list-page-container .el-table .el-table__fixed-right {
position: static !important;
box-shadow: none !important;
background-color: transparent !important;
}
/* 固定列的表头和表体都改为静态定位 */
.list-page-container .el-table .el-table__fixed-header-wrapper,
.list-page-container .el-table .el-table__fixed-body-wrapper,
.list-page-container .el-table .el-table__fixed-footer-wrapper {
position: static !important;
}
/* 表格单元格在移动端优化 */
.list-page-container .el-table th,
.list-page-container .el-table td {
padding: 12px 8px !important;
font-size: 13px;
}
/* 操作按钮组在移动端改为紧凑布局 */
.list-page-container .el-table .el-table__cell .flex.gap-2,
.list-page-container .el-table .el-table__cell .flex.items-center,
.list-page-container .el-table .el-table__cell .flex.space-x-2 {
flex-wrap: wrap;
gap: 6px !important;
}
/* 操作按钮在移动端缩小 */
.list-page-container .el-table .el-button--small {
padding: 6px 10px;
font-size: 12px;
min-width: auto;
}
/* 表格列宽度优化 - 允许更灵活的宽度 */
.list-page-container .el-table .el-table__cell {
min-width: 80px;
}
/* 操作列宽度自适应,不设置最小宽度 */
.list-page-container .el-table .el-table__cell[data-label="操作"],
.list-page-container .el-table th:last-child,
.list-page-container .el-table td:last-child {
min-width: auto !important;
width: auto !important;
}
/* 隐藏部分次要列在移动端 - 通过类名控制 */
.list-page-container .el-table .el-table__cell.hidden-mobile {
display: none;
}
/* 操作按钮在移动端自动换行,避免溢出 */
.list-page-container .el-table .el-table__cell .el-button + .el-button {
margin-left: 0;
}
/* 表格在移动端允许横向滚动 */
.list-page-container .el-table {
min-width: 600px;
}
/* 操作列在移动端不设置最小宽度,允许换行 */
.list-page-container .el-table .el-table__cell[data-label="操作"] {
white-space: normal;
word-break: break-word;
}
/* 操作按钮组在移动端更紧凑 */
.list-page-container .el-table .el-table__cell .flex {
justify-content: flex-start;
}
/* 下拉菜单按钮在移动端优化 */
.list-page-container .el-table .el-dropdown .el-button {
padding: 6px 10px;
}
}
/* 超小屏幕进一步优化 */
@media (max-width: 480px) {
.list-page-container {
padding: 12px;
}
.list-page-header {
padding: 16px 12px 12px;
}
.list-page-title {
font-size: 20px;
}
.list-page-filters {
padding: 16px 12px;
}
.list-page-table {
padding: 0 12px 12px;
}
/* 表格单元格进一步缩小 */
.list-page-container .el-table th,
.list-page-container .el-table td {
padding: 10px 6px !important;
font-size: 12px;
}
/* 操作按钮更紧凑 */
.list-page-container .el-table .el-button--small {
padding: 4px 8px;
font-size: 11px;
}
/* 操作按钮组更紧凑 */
.list-page-container .el-table .el-table__cell .flex.gap-2,
.list-page-container .el-table .el-table__cell .flex.items-center {
gap: 4px !important;
}
}
/* 动画效果 */

View File

@@ -316,7 +316,6 @@ import RichTextEditor from '@/components/common/RichTextEditor.vue'
import { Rank, Search } from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus'
import draggable from 'vuedraggable'
const props = defineProps({
modelValue: {

View File

@@ -47,9 +47,9 @@ import { onMounted, onUnmounted, ref } from 'vue'
})
// 固定配置
const FETCH_PAGE_SIZE = 5// 每次获取的记录数
const BASE_EMIT_INTERVAL = 4700// 基础5秒避免弹幕重叠
const RANDOM_EMIT_RANGE = 1000 // 0-1秒随机范围
const FETCH_PAGE_SIZE = 8// 每次获取的记录数
const BASE_EMIT_INTERVAL = 5500// 基础5秒避免弹幕重叠
const RANDOM_EMIT_RANGE = 2000 // 0-1秒随机范围
const enabled = ref(true)
const danmakuWrapper = ref(null)

View File

@@ -38,6 +38,9 @@
</template>
<script setup>
import { useMobileTable } from '@/composables/useMobileTable'
import { watch, nextTick, onMounted } from 'vue'
defineProps({
title: {
type: String,
@@ -48,6 +51,23 @@ defineProps({
default: ''
}
})
// 移动端表格优化
const { isMobile, removeFixedColumns } = useMobileTable()
// 监听表格内容变化,重新应用优化
watch(() => isMobile.value, () => {
nextTick(() => {
removeFixedColumns()
})
})
// 在组件挂载后应用优化
onMounted(() => {
nextTick(() => {
removeFixedColumns()
})
})
</script>
<style scoped>

View File

@@ -0,0 +1,143 @@
<template>
<div class="responsive-action-column">
<!-- 桌面端显示所有按钮 -->
<div v-if="!isMobile" class="action-buttons-desktop">
<slot />
</div>
<!-- 移动端主要操作按钮 + 更多操作下拉菜单 -->
<div v-else class="action-buttons-mobile">
<!-- 主要操作按钮最多显示2个 -->
<template v-for="(action, index) in primaryActions" :key="index">
<el-button
:type="action.type || 'default'"
:size="action.size || 'small'"
@click="action.handler"
>
{{ action.label }}
</el-button>
</template>
<!-- 更多操作下拉菜单 -->
<el-dropdown v-if="moreActions.length > 0" @command="handleCommand" trigger="click">
<el-button type="info" size="small">
更多
<el-icon class="el-icon--right">
<ArrowDown />
</el-icon>
</el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item
v-for="(action, index) in moreActions"
:key="index"
:command="action"
>
<el-icon v-if="action.icon" class="dropdown-item-icon">
<component :is="action.icon" />
</el-icon>
{{ action.label }}
</el-dropdown-item>
</template>
</el-dropdown>
</el-dropdown>
</div>
</div>
</template>
<script setup>
import { computed, useSlots } from 'vue'
import { useMobileTable } from '@/composables/useMobileTable'
import { ArrowDown } from '@element-plus/icons-vue'
const props = defineProps({
// 主要操作按钮数量(移动端显示)
primaryCount: {
type: Number,
default: 2
},
// 操作按钮配置(如果使用配置方式)
actions: {
type: Array,
default: () => []
}
})
const { isMobile } = useMobileTable()
const slots = useSlots()
// 从插槽中提取按钮信息(如果使用插槽方式)
const extractActionsFromSlots = () => {
if (!slots.default) return []
const actions = []
// 这里需要从插槽中解析按钮,但 Vue 3 的插槽是渲染函数,比较难解析
// 所以建议使用 actions prop 方式
return actions
}
// 计算主要操作和更多操作
const primaryActions = computed(() => {
if (props.actions.length > 0) {
return props.actions.slice(0, props.primaryCount)
}
return []
})
const moreActions = computed(() => {
if (props.actions.length > 0) {
return props.actions.slice(props.primaryCount)
}
return []
})
// 处理下拉菜单命令
const handleCommand = (action) => {
if (action && action.handler) {
action.handler()
}
}
</script>
<style scoped>
.responsive-action-column {
display: flex;
align-items: center;
gap: 8px;
}
.action-buttons-desktop {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
}
.action-buttons-mobile {
display: flex;
align-items: center;
gap: 6px;
flex-wrap: wrap;
}
.dropdown-item-icon {
margin-right: 6px;
font-size: 14px;
}
/* 移动端按钮更紧凑 */
@media (max-width: 768px) {
.action-buttons-mobile .el-button {
padding: 6px 10px;
font-size: 12px;
}
}
@media (max-width: 480px) {
.action-buttons-mobile .el-button {
padding: 4px 8px;
font-size: 11px;
}
}
</style>

View File

@@ -0,0 +1,101 @@
import { ref, onMounted, onUnmounted, nextTick } from 'vue'
/**
* 移动端表格优化 composable
* 用于在移动端移除表格固定列,优化显示效果
*/
export function useMobileTable() {
const isMobile = ref(false)
const isTablet = ref(false)
// 检测屏幕尺寸
const checkScreenSize = () => {
if (typeof window === 'undefined') return
const width = window.innerWidth
isMobile.value = width < 768
isTablet.value = width >= 768 && width < 1024
}
// 移除表格固定列
const removeFixedColumns = () => {
if (typeof window === 'undefined') return
// 只在移动端执行
if (!isMobile.value) {
// 桌面端恢复固定列样式
const tables = document.querySelectorAll('.list-page-container .el-table')
tables.forEach((table) => {
const fixedElements = table.querySelectorAll('.el-table__fixed, .el-table__fixed-right')
fixedElements.forEach((el) => {
el.style.position = ''
el.style.boxShadow = ''
el.style.backgroundColor = ''
})
})
return
}
// 使用 nextTick 确保 DOM 已更新
nextTick(() => {
setTimeout(() => {
const tables = document.querySelectorAll('.list-page-container .el-table')
tables.forEach((table) => {
// 移除固定列元素
const fixedElements = table.querySelectorAll('.el-table__fixed, .el-table__fixed-right')
fixedElements.forEach((el) => {
el.style.position = 'static'
el.style.boxShadow = 'none'
el.style.backgroundColor = 'transparent'
el.style.zIndex = 'auto'
})
// 移除固定列的表头、表体、表尾包装器
const fixedWrappers = table.querySelectorAll(
'.el-table__fixed-header-wrapper, .el-table__fixed-body-wrapper, .el-table__fixed-footer-wrapper'
)
fixedWrappers.forEach((el) => {
el.style.position = 'static'
})
// 移除固定列的遮罩层
const fixedPatch = table.querySelectorAll('.el-table__fixed-right-patch, .el-table__fixed-patch')
fixedPatch.forEach((el) => {
el.style.display = 'none'
})
})
}, 150)
})
}
// 监听窗口大小变化
const handleResize = () => {
const wasMobile = isMobile.value
checkScreenSize()
// 如果移动状态发生变化,重新应用优化
if (wasMobile !== isMobile.value) {
removeFixedColumns()
}
}
onMounted(() => {
checkScreenSize()
if (typeof window !== 'undefined') {
window.addEventListener('resize', handleResize)
// 初始移除固定列
removeFixedColumns()
}
})
onUnmounted(() => {
if (typeof window !== 'undefined') {
window.removeEventListener('resize', handleResize)
}
})
return {
isMobile,
isTablet,
removeFixedColumns
}
}