提交 0611fee6 authored 作者: 王鹏飞's avatar 王鹏飞

feat: 页面响应式设计

上级 130196c4
差异被折叠。
...@@ -85,6 +85,13 @@ textarea:focus { ...@@ -85,6 +85,13 @@ textarea:focus {
margin: 0 -16px 30px; margin: 0 -16px 30px;
padding: 0 16px; padding: 0 16px;
} }
/* 移动端弹框宽度 90% */
@media (max-width: 767px) {
.el-overlay-dialog .el-dialog {
width: 90% !important;
}
}
.info .el-form-item { .info .el-form-item {
margin-bottom: 0; margin-bottom: 0;
} }
...@@ -119,3 +126,100 @@ textarea:focus { ...@@ -119,3 +126,100 @@ textarea:focus {
padding: 8px 16px; padding: 8px 16px;
border: 1px solid #e6e6e6; border: 1px solid #e6e6e6;
} }
/* ============ 系统级响应式 ============ */
/* 移动端弹框宽度 90% */
@media (max-width: 767px) {
.el-overlay-dialog .el-dialog {
width: 90% !important;
}
}
@media (max-width: 767px) {
/* AppCard 移动端内边距缩减 */
.app-card {
padding: 12px !important;
}
.app-card + .app-card {
margin-top: 12px !important;
}
.app-card-hd__title {
font-size: 16px;
margin-bottom: 16px;
}
/* 全局 el-table 横向滚动 */
.el-table {
width: 100%;
}
.el-table__body-wrapper {
overflow-x: auto;
-webkit-overflow-scrolling: touch;
}
/* el-pagination 移动端简化 */
.el-pagination {
flex-wrap: wrap;
justify-content: center;
gap: 4px;
font-size: 12px;
}
.el-pagination__total,
.el-pagination__sizes,
.el-pagination__jump {
display: none !important;
}
.el-pager .number {
min-width: 28px;
height: 28px;
line-height: 28px;
}
.btn-prev,
.btn-next {
height: 28px;
min-width: 28px;
}
/* el-steps 移动端垂直 */
.el-steps--horizontal {
flex-direction: column;
}
/* 弹框底部按钮组换行 */
.el-dialog__footer {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 8px;
.el-button {
margin-left: 0 !important;
}
}
/* 弹框按钮选中不独占一行,保持自然间距 */
.el-dialog__footer .el-button + .el-button {
margin-left: 0;
}
/* 固定宽度容器防溢出 */
[style*="width: 200px"],
[style*="width: 220px"],
[style*="width: 300px"],
[style*="width: 340px"],
[style*="width: 400px"],
[style*="width: 150px"] {
max-width: 100% !important;
}
/* hover 显示的元素在触摸设备上常显 */
.connect-item__remove,
.connect-item__edit,
.hover-show {
display: block !important;
}
[class*="remove"]:hover,
[class*="delete"]:hover {
opacity: 1;
}
}
...@@ -107,4 +107,17 @@ const color = ['#af1c40', '#c17933', '#8f0034', '#d45548', '#ab3259', '#dec34c', ...@@ -107,4 +107,17 @@ const color = ['#af1c40', '#c17933', '#8f0034', '#d45548', '#ab3259', '#dec34c',
height: 290px; height: 290px;
} }
} }
@media (max-width: 767px) {
.chart-hd {
padding: 8px 12px;
height: auto;
flex-wrap: wrap;
gap: 4px;
h3 { font-size: 14px; }
}
.chart-bd {
margin: 0 12px 12px;
.chart { height: 220px; }
}
}
</style> </style>
<script setup lang="ts"> <script setup lang="ts">
import { getCategoryList } from '@/api/base' import { getCategoryList } from '@/api/base'
withDefaults(defineProps<{ placeholder?: string }>(), { placeholder: '请选择' })
const options = ref([]) const options = ref([])
async function fetchList() { async function fetchList() {
const res = await getCategoryList() const res = await getCategoryList()
...@@ -14,7 +16,7 @@ watchEffect(() => { ...@@ -14,7 +16,7 @@ watchEffect(() => {
<template> <template>
<el-cascader <el-cascader
clearable clearable
placeholder="请选择" :placeholder="placeholder"
style="width: 100%" style="width: 100%"
:options="options" :options="options"
:props="{ :props="{
......
<script setup lang="ts"> <script setup lang="ts">
import { Search, RefreshLeft } from '@element-plus/icons-vue' import { Search, RefreshLeft } from '@element-plus/icons-vue'
import { useMediaQuery } from '@vueuse/core'
const isMobile = useMediaQuery('(max-width: 767px)')
// 移动端卡片列(过滤序号、操作等 slot 列留给卡片底部渲染)
const cardColumns = computed(() => props.columns.filter((c) => c.type !== 'index' && c.slots !== 'table-x'))
const hasActionsColumn = computed(() => props.columns.some((c) => c.slots === 'table-x'))
interface IRemoteProps { interface IRemoteProps {
params?: any params?: any
...@@ -17,6 +24,7 @@ interface Props { ...@@ -17,6 +24,7 @@ interface Props {
hasPagination?: boolean hasPagination?: boolean
limit?: number limit?: number
hasLoading?: boolean hasLoading?: boolean
mobileCards?: boolean
} }
const props = withDefaults(defineProps<Props>(), { const props = withDefaults(defineProps<Props>(), {
hasPagination: true, hasPagination: true,
...@@ -25,6 +33,7 @@ const props = withDefaults(defineProps<Props>(), { ...@@ -25,6 +33,7 @@ const props = withDefaults(defineProps<Props>(), {
filters: () => [], filters: () => [],
columns: () => [], columns: () => [],
data: () => [], data: () => [],
mobileCards: true,
}) })
const filterFormRef = ref() const filterFormRef = ref()
...@@ -170,7 +179,9 @@ defineExpose({ refetch, tableRef }) ...@@ -170,7 +179,9 @@ defineExpose({ refetch, tableRef })
<!-- 主体 --> <!-- 主体 -->
<div class="table-list-bd"> <div class="table-list-bd">
<slot name="body" v-bind="{ data: dataList }"> <slot name="body" v-bind="{ data: dataList }">
<!-- 桌面端 / 禁用移动端卡片时:表格 -->
<el-table <el-table
v-if="!isMobile || !mobileCards"
:header-cell-style="{ background: '#ededed' }" :header-cell-style="{ background: '#ededed' }"
:data="dataList" :data="dataList"
v-loading="loading && hasLoading" v-loading="loading && hasLoading"
...@@ -183,6 +194,27 @@ defineExpose({ refetch, tableRef }) ...@@ -183,6 +194,27 @@ defineExpose({ refetch, tableRef })
</template> </template>
</el-table-column> </el-table-column>
</el-table> </el-table>
<!-- 移动端:卡片列表 -->
<div v-if="isMobile && mobileCards" class="mobile-card-list" v-loading="loading && hasLoading">
<div class="mobile-card" v-for="(row, rowIndex) in dataList" :key="rowIndex">
<div class="mobile-card-body">
<div class="mobile-card-row" v-for="col in cardColumns" :key="col.prop || col.label">
<span class="mobile-card-label">{{ col.label }}</span>
<span class="mobile-card-colon">:</span>
<span class="mobile-card-value">
<slot v-if="col.slots && $slots[col.slots]" :name="col.slots" v-bind="{ row, $index: rowIndex }"></slot>
<div v-html="col.computed({ row, $index: rowIndex })" v-else-if="col.computed"></div>
<template v-else>{{ row[col.prop] ?? '-' }}</template>
</span>
</div>
</div>
<!-- 操作栏:如果有 slot 列,提取渲染 -->
<div class="mobile-card-ft" v-if="hasActionsColumn">
<slot name="table-x" v-bind="{ row, $index: rowIndex }"></slot>
</div>
</div>
<el-empty v-if="!dataList.length && !loading" description="暂无数据" />
</div>
</slot> </slot>
</div> </div>
<!-- 底部 --> <!-- 底部 -->
...@@ -254,4 +286,96 @@ defineExpose({ refetch, tableRef }) ...@@ -254,4 +286,96 @@ defineExpose({ refetch, tableRef })
} }
} }
} }
// 移动端卡片列表
.mobile-card-list {
min-height: 120px;
}
.mobile-card {
background: #fff; border-radius: 8px; border: 1px solid #eee;
margin-bottom: 12px; overflow: hidden; display: flex; flex-direction: column;
}
.mobile-card-body { flex: 1; padding: 14px 16px; }
.mobile-card-row {
display: flex; padding: 5px 0; border-bottom: 1px solid #f5f5f5; line-height: 1.6;
&:last-child { border-bottom: none; }
}
.mobile-card-label { flex-shrink: 0; color: #999; font-size: 14px; }
.mobile-card-colon { color: #999; font-size: 14px; margin-right: 6px; }
.mobile-card-value { flex: 1; font-size: 14px; color: #333; min-width: 0; word-break: break-all; }
.mobile-card-ft {
padding: 6px 12px; display: flex; flex-wrap: wrap;
gap: 6px; row-gap: 4px;
background: #fafafa; border-top: 1px solid #f0f0f0;
.el-button {
padding: 5px 10px;
font-size: 12px;
min-width: 44px;
text-align: center;
+ .el-button { margin-left: 0; }
}
}
// 移动端筛选表单
@media (max-width: 767px) {
.table-list-hd .el-form-item__label {
display: none;
}
.table-list-filter {
width: 100%;
.el-form,
.el-form--inline {
width: 100%;
}
.el-form-item {
width: 100%;
margin-right: 0;
margin-bottom: 8px;
}
.el-form-item__content {
width: 100%;
}
.el-input,
.el-select,
.el-cascader {
width: 100% !important;
}
.filter-buttons {
width: auto;
}
}
// 移动端分页
.table-list-ft {
flex-direction: column;
gap: 8px;
padding: 8px 0;
}
.table-list-pagination {
.el-pagination {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 2px;
}
.el-pagination__total,
.el-pagination__sizes,
.el-pagination__jump {
display: none;
}
.el-pager {
.number {
min-width: 28px;
height: 28px;
line-height: 28px;
font-size: 12px;
}
}
.btn-prev,
.btn-next {
height: 28px;
min-width: 28px;
}
}
}
</style> </style>
...@@ -5,12 +5,16 @@ export default { name: 'AppAside' } ...@@ -5,12 +5,16 @@ export default { name: 'AppAside' }
<script setup lang="ts"> <script setup lang="ts">
import type { IMenuItem } from '@/types' import type { IMenuItem } from '@/types'
import { useMenuStore } from '@/stores/menu' import { useMenuStore } from '@/stores/menu'
import { ArrowLeft, ArrowRight } from '@element-plus/icons-vue'
const menus = useMenuStore().menus const menus = useMenuStore().menus
const route = useRoute() const route = useRoute()
const collapsed = ref(false)
const currentMenu = computed(() => { const currentMenu = computed(() => {
return findMenu(route.path, menus) return findMenu(route.fullPath, menus)
}) })
const currentSubmenu = computed(() => { const currentSubmenu = computed(() => {
...@@ -21,21 +25,43 @@ function findMenu(path: string, menus: IMenuItem[]) { ...@@ -21,21 +25,43 @@ function findMenu(path: string, menus: IMenuItem[]) {
return menus.find((item) => { return menus.find((item) => {
if ( if (
item.children && item.children &&
item.children.find((item) => { item.children.find((child) => {
const regExp = new RegExp(`^${item.path.replaceAll('?', '\\?')}`) // 匹配 path + query,不要求 query 参数顺序(如 /material?type=2 应匹配 /material?experiment_id=xxx&type=2)
return regExp.test(path) const [p, qs] = child.path.split('?')
if (!path.startsWith(p)) return false
if (!qs) return true
return qs.split('&').every((param) => path.includes(param))
}) })
) { ) {
return item return item
} }
const regExp = new RegExp(`^${item.path.replaceAll('?', '\\?')}`) const [p, qs] = item.path.split('?')
return regExp.test(path) if (!path.startsWith(p)) return false
if (!qs) return true
return qs.split('&').every((param) => path.includes(param))
}) })
} }
function toggleCollapse() {
collapsed.value = !collapsed.value
}
// 处理菜单路径中的 query 参数(如 /material?type=1),与 experiment_id 合并
function menuLinkTo(item: { path: string }) {
const baseQuery: Record<string, any> = { experiment_id: route.query.experiment_id }
const idx = item.path.indexOf('?')
if (idx === -1) return { path: item.path, query: baseQuery }
const path = item.path.slice(0, idx)
const params = new URLSearchParams(item.path.slice(idx + 1))
for (const [key, value] of params) {
baseQuery[key] = value
}
return { path, query: baseQuery }
}
</script> </script>
<template> <template>
<aside class="app-aside"> <aside class="app-aside" :class="{ 'is-collapsed': collapsed }">
<nav class="menu"> <nav class="menu">
<ul> <ul>
<li <li
...@@ -45,12 +71,12 @@ function findMenu(path: string, menus: IMenuItem[]) { ...@@ -45,12 +71,12 @@ function findMenu(path: string, menus: IMenuItem[]) {
v-permission="item.tag"> v-permission="item.tag">
<div class="menu-item"> <div class="menu-item">
<template v-if="item.children"> <template v-if="item.children">
<RouterLink :to="{ path: item.path, query: { experiment_id: $route.query.experiment_id } }"> <RouterLink :to="menuLinkTo(item)">
<component :is="item.icon" class="menu-icon"></component> <component :is="item.icon" class="menu-icon"></component>
</RouterLink> </RouterLink>
</template> </template>
<el-tooltip :content="item.name" placement="right" v-else> <el-tooltip :content="item.name" placement="right" v-else>
<RouterLink :to="{ path: item.path, query: { experiment_id: $route.query.experiment_id } }"> <RouterLink :to="menuLinkTo(item)">
<component :is="item.icon" class="menu-icon"></component> <component :is="item.icon" class="menu-icon"></component>
</RouterLink> </RouterLink>
</el-tooltip> </el-tooltip>
...@@ -63,7 +89,7 @@ function findMenu(path: string, menus: IMenuItem[]) { ...@@ -63,7 +89,7 @@ function findMenu(path: string, menus: IMenuItem[]) {
:key="submenu.path" :key="submenu.path"
:class="{ 'is-active': submenu.path === currentSubmenu?.path }" :class="{ 'is-active': submenu.path === currentSubmenu?.path }"
v-permission="submenu.tag"> v-permission="submenu.tag">
<RouterLink :to="{ path: submenu.path, query: { experiment_id: $route.query.experiment_id } }"> <RouterLink :to="menuLinkTo(submenu)">
<component :is="submenu.icon" class="submenu-icon" v-if="submenu.icon"></component> <component :is="submenu.icon" class="submenu-icon" v-if="submenu.icon"></component>
{{ submenu.name }} {{ submenu.name }}
</RouterLink> </RouterLink>
...@@ -72,7 +98,15 @@ function findMenu(path: string, menus: IMenuItem[]) { ...@@ -72,7 +98,15 @@ function findMenu(path: string, menus: IMenuItem[]) {
</nav> </nav>
</li> </li>
</ul> </ul>
<!-- 展开时的收起按钮 -->
<div class="menu-toggle" @click="toggleCollapse" v-if="!collapsed">
<el-icon><ArrowLeft /></el-icon>
</div>
</nav> </nav>
<!-- 收起后的展开胶囊按钮 -->
<div class="aside-float-btn" @click="toggleCollapse" v-if="collapsed">
<el-icon><ArrowRight /></el-icon>
</div>
</aside> </aside>
</template> </template>
...@@ -82,15 +116,19 @@ function findMenu(path: string, menus: IMenuItem[]) { ...@@ -82,15 +116,19 @@ function findMenu(path: string, menus: IMenuItem[]) {
top: 0; top: 0;
display: flex; display: flex;
height: 100vh; height: 100vh;
// overflow-x: hidden;
// overflow-y: auto;
box-sizing: content-box; box-sizing: content-box;
z-index: 2001; z-index: 2001;
transition: width 0.25s ease;
.menu { .menu {
width: 60px; width: 60px;
flex-shrink: 0;
height: 100%; height: 100%;
background: #fff; background: #fff;
box-sizing: border-box; box-sizing: border-box;
position: relative;
z-index: 1;
.menu-item { .menu-item {
height: 44px; height: 44px;
text-align: center; text-align: center;
...@@ -106,19 +144,18 @@ function findMenu(path: string, menus: IMenuItem[]) { ...@@ -106,19 +144,18 @@ function findMenu(path: string, menus: IMenuItem[]) {
} }
} }
} }
> ul > li { > ul {
padding-bottom: 80px;
> li {
padding: 8px; padding: 8px;
&.is-active { &.is-active {
.menu-item { .menu-item {
background-color: var(--main-color); background-color: var(--main-color);
svg { svg { fill: #fff; }
fill: #fff;
}
} }
} }
&:hover { &:hover {
.submenu { .submenu { display: block; }
display: block;
} }
} }
} }
...@@ -165,5 +202,67 @@ function findMenu(path: string, menus: IMenuItem[]) { ...@@ -165,5 +202,67 @@ function findMenu(path: string, menus: IMenuItem[]) {
line-height: 1; line-height: 1;
color: var(--main-color); color: var(--main-color);
} }
// 底部收起按钮
.menu-toggle {
position: absolute;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
width: 28px;
height: 56px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
color: #999;
font-size: 14px;
background: #fff;
border: 1px solid #e0e0e0;
border-radius: 28px;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.08);
z-index: 10;
&:hover {
background: var(--main-color);
color: #fff;
border-color: var(--main-color);
}
}
// 收起状态
&.is-collapsed {
width: 0;
overflow: hidden;
.menu {
transform: translateX(-60px);
}
}
}
// 悬浮胶囊按钮
.aside-float-btn {
position: fixed;
left: 0;
bottom: 20px;
width: 28px;
height: 56px;
background: #fff;
border: 1px solid #e0e0e0;
border-left: none;
border-radius: 0 28px 28px 0;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
z-index: 2000;
box-shadow: 2px 0 6px rgba(0, 0, 0, 0.08);
color: #666;
font-size: 14px;
transition: all 0.2s;
&:hover {
background: var(--main-color);
color: #fff;
border-color: var(--main-color);
}
} }
</style> </style>
...@@ -5,7 +5,7 @@ export default { name: 'AppFooter' } ...@@ -5,7 +5,7 @@ export default { name: 'AppFooter' }
<template> <template>
<footer class="app-footer"> <footer class="app-footer">
<p>实验室版本:V2.1.0</p> <p>实验室版本:V2.1.0</p>
<p>Copyright © 2017 Zijing Education. All rights reserved. 清控紫荆(北京)教育科技股份有限公司</p> <p>Copyright © 2026 Zijing Education. All rights reserved. 清控紫荆(北京)教育科技股份有限公司</p>
</footer> </footer>
</template> </template>
......
...@@ -5,16 +5,13 @@ export default { name: 'AppLayout' } ...@@ -5,16 +5,13 @@ export default { name: 'AppLayout' }
<script setup lang="ts"> <script setup lang="ts">
import AppAside from './Aside.vue' import AppAside from './Aside.vue'
import AppMain from './Main.vue' import AppMain from './Main.vue'
import AppFooter from './Footer.vue'
interface Props { interface Props {
sidebar?: boolean sidebar?: boolean
footer?: boolean
} }
withDefaults(defineProps<Props>(), { withDefaults(defineProps<Props>(), {
sidebar: true, sidebar: true,
footer: true
}) })
</script> </script>
...@@ -23,7 +20,6 @@ withDefaults(defineProps<Props>(), { ...@@ -23,7 +20,6 @@ withDefaults(defineProps<Props>(), {
<AppAside v-if="sidebar"></AppAside> <AppAside v-if="sidebar"></AppAside>
<div class="app-layout-container"> <div class="app-layout-container">
<AppMain></AppMain> <AppMain></AppMain>
<AppFooter v-if="footer"></AppFooter>
</div> </div>
</div> </div>
</template> </template>
......
...@@ -4,7 +4,9 @@ const isIframe = computed(() => { ...@@ -4,7 +4,9 @@ const isIframe = computed(() => {
}) })
const padding = computed(() => { const padding = computed(() => {
return isIframe.value ? '0 10px' : '10px' if (isIframe.value) return '0 10px'
if (typeof window !== 'undefined' && window.innerWidth < 768) return '6px'
return '10px'
}) })
</script> </script>
......
...@@ -242,4 +242,9 @@ const eventTimeOption = computed(() => { ...@@ -242,4 +242,9 @@ const eventTimeOption = computed(() => {
margin-top: 20px; margin-top: 20px;
} }
} }
@media (max-width: 767px) {
.row {
flex-direction: column;
}
}
</style> </style>
...@@ -315,4 +315,9 @@ const groupMemberTopOption = computed(() => { ...@@ -315,4 +315,9 @@ const groupMemberTopOption = computed(() => {
margin-top: 20px; margin-top: 20px;
} }
} }
@media (max-width: 767px) {
.row {
flex-direction: column;
}
}
</style> </style>
...@@ -39,7 +39,12 @@ async function fetchEventAction() { ...@@ -39,7 +39,12 @@ async function fetchEventAction() {
if (!userValue.value || !eventValue.value || !startDate || !endDate) return if (!userValue.value || !eventValue.value || !startDate || !endDate) return
loading1.value = true loading1.value = true
try { try {
const res = await api.getEventAction({ sso_id: userValue.value, start_date: startDate, end_date: endDate, event_id: eventValue.value }) const res = await api.getEventAction({
sso_id: userValue.value,
start_date: startDate,
end_date: endDate,
event_id: eventValue.value,
})
eventAction.value = res.data eventAction.value = res.data
} finally { } finally {
loading1.value = false loading1.value = false
...@@ -52,14 +57,14 @@ const eventActionOption = computed(() => { ...@@ -52,14 +57,14 @@ const eventActionOption = computed(() => {
tooltip: { trigger: 'axis' }, tooltip: { trigger: 'axis' },
legend: { legend: {
bottom: '10', bottom: '10',
data: ['行为数量', '用户数量'] data: ['行为数量', '用户数量'],
}, },
xAxis: { xAxis: {
type: 'category', type: 'category',
boundaryGap: ['20%', '20%'], boundaryGap: ['20%', '20%'],
data: eventAction.value.event_action_items.map(item => { data: eventAction.value.event_action_items.map((item) => {
return item.group_name return item.group_name
}) }),
}, },
yAxis: { type: 'value' }, yAxis: { type: 'value' },
series: [ series: [
...@@ -68,16 +73,16 @@ const eventActionOption = computed(() => { ...@@ -68,16 +73,16 @@ const eventActionOption = computed(() => {
type: 'line', type: 'line',
smooth: true, smooth: true,
label: { show: true, position: 'top' }, label: { show: true, position: 'top' },
data: eventAction.value.event_action_items.map(item => item.total) data: eventAction.value.event_action_items.map((item) => item.total),
}, },
{ {
name: '用户数量', name: '用户数量',
type: 'line', type: 'line',
smooth: true, smooth: true,
label: { show: true, position: 'top' }, label: { show: true, position: 'top' },
data: eventAction.value.event_member_items.map(item => item.total) data: eventAction.value.event_member_items.map((item) => item.total),
} },
] ],
} }
}) })
...@@ -123,7 +128,11 @@ async function fetchUser() { ...@@ -123,7 +128,11 @@ async function fetchUser() {
if (!userValue.value || !userStartDate.value || !userEndDate.value) return if (!userValue.value || !userStartDate.value || !userEndDate.value) return
loading3.value = true loading3.value = true
try { try {
const res = await api.getMemberList({ sso_id: userValue.value, start_date: userStartDate.value, end_date: userEndDate.value }) const res = await api.getMemberList({
sso_id: userValue.value,
start_date: userStartDate.value,
end_date: userEndDate.value,
})
user.value = res.data.items user.value = res.data.items
} finally { } finally {
loading3.value = false loading3.value = false
...@@ -149,7 +158,7 @@ function format(date) { ...@@ -149,7 +158,7 @@ function format(date) {
</el-form-item> </el-form-item>
</el-form> </el-form>
<el-divider /> <el-divider />
<el-row :gutter="20"> <el-row :gutter="20" class="main-row">
<el-col :span="16"> <el-col :span="16">
<ChartCard title="事件行为分析" :options="eventActionOption" :loading="loading1"> <ChartCard title="事件行为分析" :options="eventActionOption" :loading="loading1">
<template #tools> <template #tools>
...@@ -326,3 +335,28 @@ function format(date) { ...@@ -326,3 +335,28 @@ function format(date) {
color: var(--main-color); color: var(--main-color);
} }
</style> </style>
<style lang="scss">
@media (max-width: 767px) {
.main-row .el-col {
max-width: 100% !important;
width: 100% !important;
flex: 0 0 100% !important;
}
.el-form--inline .el-form-item {
width: 100%;
margin-right: 0;
}
.chart-card .tools {
flex-wrap: wrap;
gap: 6px;
}
.chart-card .el-date-editor,
.chart-card .el-select {
width: 100% !important;
}
.el-table {
min-width: 600px;
}
}
</style>
...@@ -236,4 +236,9 @@ const statusOption = computed(() => { ...@@ -236,4 +236,9 @@ const statusOption = computed(() => {
margin-top: 20px; margin-top: 20px;
} }
} }
@media (max-width: 767px) {
.row {
flex-direction: column;
}
}
</style> </style>
...@@ -63,4 +63,10 @@ function handleChange(item: PlatformItem) { ...@@ -63,4 +63,10 @@ function handleChange(item: PlatformItem) {
box-shadow: 2px 2px 10px 0px rgba(0, 0, 0, 0.2); box-shadow: 2px 2px 10px 0px rgba(0, 0, 0, 0.2);
} }
} }
@media (max-width: 767px) {
.connect-from_icon__box {
grid-template-columns: repeat(2, 1fr);
row-gap: 12px;
}
}
</style> </style>
...@@ -302,4 +302,9 @@ const sendChat = function () { ...@@ -302,4 +302,9 @@ const sendChat = function () {
} }
} }
} }
@media (max-width: 767px) {
.step1-btn {
padding: 20px 0 !important;
}
}
</style> </style>
...@@ -170,9 +170,11 @@ const handleStudentFollow = function (experimentId: string, id: string, type: st ...@@ -170,9 +170,11 @@ const handleStudentFollow = function (experimentId: string, id: string, type: st
grid-template-columns: repeat(5, 1fr); grid-template-columns: repeat(5, 1fr);
gap: 20px; gap: 20px;
padding: 20px; padding: 20px;
// background-color: #efefef;
border-radius: 5px; border-radius: 5px;
} }
@media (max-width: 1200px) { .connect-list { grid-template-columns: repeat(4, 1fr); } }
@media (max-width: 992px) { .connect-list { grid-template-columns: repeat(3, 1fr); } }
@media (max-width: 640px) { .connect-list { grid-template-columns: repeat(2, 1fr); gap: 12px; padding: 12px; } }
.connect-add-button { .connect-add-button {
display: flex; display: flex;
align-items: center; align-items: center;
......
...@@ -679,4 +679,20 @@ const surveyKingDialogVisible = ref<boolean>(false) ...@@ -679,4 +679,20 @@ const surveyKingDialogVisible = ref<boolean>(false)
.tabs-box { .tabs-box {
margin-bottom: 20px; margin-bottom: 20px;
} }
@media (max-width: 767px) {
.view-info {
flex-direction: column;
align-items: center;
text-align: center;
.view-info_content {
margin-left: 0;
margin-top: 12px;
flex-direction: column;
p { margin-right: 0; margin-bottom: 6px; }
}
}
.tag, .tag-null {
width: 100%;
}
}
</style> </style>
...@@ -364,6 +364,7 @@ onMounted(() => { ...@@ -364,6 +364,7 @@ onMounted(() => {
flex-direction: column; flex-direction: column;
.row { .row {
flex: 1; flex: 1;
gap: 10px;
} }
} }
.card { .card {
...@@ -381,6 +382,28 @@ onMounted(() => { ...@@ -381,6 +382,28 @@ onMounted(() => {
::v-deep(.chart-card) { ::v-deep(.chart-card) {
background-color: #fff; background-color: #fff;
} }
@media (max-width: 767px) {
> .row {
flex-direction: column;
}
.row {
flex-wrap: wrap;
.col {
width: 100%;
.row {
flex-wrap: wrap;
.card {
flex: 0 0 calc(50% - 5px);
p {
line-height: 60px;
font-size: 20px;
}
}
}
}
}
}
} }
.content-user { .content-user {
background: #efefef; background: #efefef;
......
...@@ -112,4 +112,9 @@ function handleUpdate() { ...@@ -112,4 +112,9 @@ function handleUpdate() {
box-shadow: rgb(0 0 0 / 40%) 0px 2px 6px 0px; box-shadow: rgb(0 0 0 / 40%) 0px 2px 6px 0px;
} }
} }
@media (max-width: 767px) {
.label-type-icon-list {
grid-template-columns: repeat(4, 50px);
}
}
</style> </style>
...@@ -237,4 +237,21 @@ const handleRemoves = async function () { ...@@ -237,4 +237,21 @@ const handleRemoves = async function () {
overflow-x: hidden; overflow-x: hidden;
overflow-y: auto; overflow-y: auto;
} }
@media (max-width: 767px) {
.label-wrap {
flex-direction: column;
height: auto;
min-height: calc(100vh - 130px);
}
.label-left {
flex: none;
width: 100%;
padding-right: 0;
margin-right: 0;
border-right: none;
border-bottom: 1px solid rgb(187, 187, 187);
padding-bottom: 12px;
margin-bottom: 12px;
}
}
</style> </style>
...@@ -23,7 +23,7 @@ const listOptions = computed(() => { ...@@ -23,7 +23,7 @@ const listOptions = computed(() => {
}, },
filters: [ filters: [
{ label: '直播主题品类', prop: 'live_commodity_type_id', slots: 'filter-category' }, { label: '直播主题品类', prop: 'live_commodity_type_id', slots: 'filter-category' },
{ label: '直播主题名称', prop: 'live_commodity_title', type: 'input' }, { label: '直播主题名称', prop: 'live_commodity_title', type: 'input', placeholder: '请输入直播主题名称' },
], ],
columns: [ columns: [
{ label: '序号', type: 'index', width: 60 }, { label: '序号', type: 'index', width: 60 },
...@@ -35,18 +35,17 @@ const listOptions = computed(() => { ...@@ -35,18 +35,17 @@ const listOptions = computed(() => {
try { try {
const info = JSON.parse(row.commodity_info) const info = JSON.parse(row.commodity_info)
const [picture] = JSON.parse(info.picture_addreses) || [] const [picture] = JSON.parse(info.picture_addreses) || []
return `<div style="display: flex; align-items: center; gap: 10px;"><img src="${picture.url}" width="100" />${info.title}</div>` return `<div style="display:flex;align-items:center;gap:10px"><img src="${picture.url}" width="80" style="border-radius:4px"/>${info.title}</div>`
} catch (e) { } catch {
console.log(e)
}
return '--' return '--'
}
}, },
}, },
{ {
label: '单价/数量', label: '单价/数量',
prop: 'num', prop: 'num',
computed({ row }) { computed({ row }) {
return `¥${row.price}<br/>x${row.num}` return `¥${row.price} × ${row.num}`
}, },
}, },
{ label: '订单编号', prop: 'id' }, { label: '订单编号', prop: 'id' },
...@@ -67,7 +66,10 @@ const listOptions = computed(() => { ...@@ -67,7 +66,10 @@ const listOptions = computed(() => {
<AppCard title="订单管理"> <AppCard title="订单管理">
<AppList v-bind="listOptions" ref="appList"> <AppList v-bind="listOptions" ref="appList">
<template #filter-category> <template #filter-category>
<LiveProductCategory v-model="listParams.live_commodity_type_id" @change="handleRefresh"></LiveProductCategory> <LiveProductCategory
v-model="listParams.live_commodity_type_id"
@change="handleRefresh"
placeholder="请选择直播主题品类"></LiveProductCategory>
</template> </template>
</AppList> </AppList>
</AppCard> </AppCard>
......
...@@ -37,26 +37,14 @@ const listOptions = computed(() => { ...@@ -37,26 +37,14 @@ const listOptions = computed(() => {
{ label: '序号', type: 'index', width: 60 }, { label: '序号', type: 'index', width: 60 },
{ label: '商品属性名称', prop: 'name' }, { label: '商品属性名称', prop: 'name' },
{ label: '所属商品品类', prop: 'live_commodity_type_full_name' }, { label: '所属商品品类', prop: 'live_commodity_type_full_name' },
{ { label: '重要性', prop: 'is_importance', computed: ({ row }) => getNameByValue(row.is_importance, importType) },
label: '重要性', { label: '必要性', prop: 'is_essential', computed: ({ row }) => getNameByValue(row.is_essential, requiredType) },
prop: 'is_importance',
computed({ row }) {
return getNameByValue(row.is_importance, importType)
},
},
{
label: '必要性',
prop: 'is_essential',
computed({ row }) {
return getNameByValue(row.is_essential, requiredType)
},
},
{ {
label: '状态', label: '状态',
prop: 'status', prop: 'status',
computed({ row }) { computed: ({ row }) => {
const color = row.status === '1' ? 'var(--main-success-color)' : 'var(--main-color)' const color = row.status === '1' ? 'var(--main-success-color)' : 'var(--main-color)'
return `<span style="color: ${color}">${getNameByValue(row.status, statusList)}</span>` return `<span style="color:${color}">${getNameByValue(row.status, statusList)}</span>`
}, },
}, },
{ label: '创建人', prop: 'created_operator', computed: ({ row }) => getOperatorName(row.created_operator) }, { label: '创建人', prop: 'created_operator', computed: ({ row }) => getOperatorName(row.created_operator) },
...@@ -65,6 +53,7 @@ const listOptions = computed(() => { ...@@ -65,6 +53,7 @@ const listOptions = computed(() => {
], ],
} }
}) })
const action = ref(null) const action = ref(null)
const formVisible = ref(false) const formVisible = ref(false)
const currentRow = ref(null) const currentRow = ref(null)
...@@ -99,11 +88,12 @@ const handleRemove = async (row) => { ...@@ -99,11 +88,12 @@ const handleRemove = async (row) => {
<template> <template>
<AppCard title="商品属性管理"> <AppCard title="商品属性管理">
<AppList v-bind="listOptions" ref="appList"> <AppList v-bind="listOptions" ref="appList">
<template #header-buttons> <template #header-buttons><el-button type="primary" @click="handleAdd">新增商品属性</el-button></template>
<el-button type="primary" @click="handleAdd">新增商品属性</el-button>
</template>
<template #filter-category> <template #filter-category>
<LiveProductCategory v-model="listParams.live_commodity_type_id" @change="handleRefresh"></LiveProductCategory> <LiveProductCategory
v-model="listParams.live_commodity_type_id"
@change="handleRefresh"
placeholder="请选择商品品类"></LiveProductCategory>
</template> </template>
<template #table-x="{ row }"> <template #table-x="{ row }">
<el-button text type="primary" @click="handleView(row)">查看</el-button> <el-button text type="primary" @click="handleView(row)">查看</el-button>
...@@ -112,6 +102,5 @@ const handleRemove = async (row) => { ...@@ -112,6 +102,5 @@ const handleRemove = async (row) => {
</template> </template>
</AppList> </AppList>
</AppCard> </AppCard>
<!-- 新建/修改 -->
<FormDialog v-model="formVisible" :action="action" :data="currentRow" @update="handleRefresh" v-if="formVisible" /> <FormDialog v-model="formVisible" :action="action" :data="currentRow" @update="handleRefresh" v-if="formVisible" />
</template> </template>
...@@ -67,7 +67,7 @@ const handleRemove = async (row) => { ...@@ -67,7 +67,7 @@ const handleRemove = async (row) => {
<template> <template>
<AppCard title="商品品类管理"> <AppCard title="商品品类管理">
<AppList v-bind="listOptions" row-key="id" ref="appList"> <AppList v-bind="listOptions" row-key="id" ref="appList" :mobile-cards="false">
<template #header-buttons> <template #header-buttons>
<el-button type="primary" @click="handleAdd()">新增类别</el-button> <el-button type="primary" @click="handleAdd()">新增类别</el-button>
</template> </template>
......
...@@ -105,7 +105,7 @@ const unimportanceTotal = computed(() => { ...@@ -105,7 +105,7 @@ const unimportanceTotal = computed(() => {
<div> <div>
<el-form-item label="商品标题" prop="title" :rules="[{ required: true, message: '请输入', trigger: 'blur' }]"> <el-form-item label="商品标题" prop="title" :rules="[{ required: true, message: '请输入', trigger: 'blur' }]">
<div class="form-tips">标题不规范会有可能引起商品下架,影响您的正常销售,请点击学习商品发布规范认真填写</div> <div class="form-tips">标题不规范会有可能引起商品下架,影响您的正常销售,请点击学习商品发布规范认真填写</div>
<div style="display: flex; width: 100%; align-items: center; gap: 20px"> <div class="form-inline-row">
<el-input <el-input
placeholder="至少输入8个字(16个字符)以上,30个字(60个字符)以下" placeholder="至少输入8个字(16个字符)以上,30个字(60个字符)以下"
size="large" size="large"
...@@ -115,12 +115,11 @@ const unimportanceTotal = computed(() => { ...@@ -115,12 +115,11 @@ const unimportanceTotal = computed(() => {
</el-form-item> </el-form-item>
<el-form-item label="导购短标题" prop="shopping_guide_short_title"> <el-form-item label="导购短标题" prop="shopping_guide_short_title">
<div class="form-tips">短标题可用于商品搜索、首页推荐、物流单等场景,请提炼商品关键信息,客观准确填写</div> <div class="form-tips">短标题可用于商品搜索、首页推荐、物流单等场景,请提炼商品关键信息,客观准确填写</div>
<div style="display: flex; width: 100%; align-items: center; gap: 20px"> <div class="form-inline-row">
<el-input <el-input
placeholder="建议填写简明准确的标题内容,避免重复表达" placeholder="建议填写简明准确的标题内容,避免重复表达"
size="large" size="large"
v-model="form.shopping_guide_short_title" v-model="form.shopping_guide_short_title" />
style="flex: 1" />
<el-button type="primary" plain @click="handleAIGenerate" :loading="descLoading">一键智能推荐</el-button> <el-button type="primary" plain @click="handleAIGenerate" :loading="descLoading">一键智能推荐</el-button>
</div> </div>
</el-form-item> </el-form-item>
...@@ -129,14 +128,11 @@ const unimportanceTotal = computed(() => { ...@@ -129,14 +128,11 @@ const unimportanceTotal = computed(() => {
prop="live_commodity_attrs.importance_attrs"> prop="live_commodity_attrs.importance_attrs">
<div class="form-tips">错误填写属性,会引起商品下架,请认真准确填写。</div> <div class="form-tips">错误填写属性,会引起商品下架,请认真准确填写。</div>
<div style="display: flex; flex-direction: column; gap: 16px"> <div style="display: flex; flex-direction: column; gap: 16px">
<div <div v-for="(item, index) in form.live_commodity_attrs.importance_attrs" :key="index" class="attr-row">
v-for="(item, index) in form.live_commodity_attrs.importance_attrs"
:key="index"
style="display: flex; align-items: flex-start; gap: 12px">
<el-select <el-select
v-model="item.id" v-model="item.id"
placeholder="请选择属性" placeholder="请选择属性"
style="width: 200px" class="attr-select"
@change="(value) => handleAttrSelect(value, item, 'importance_attrs')"> @change="(value) => handleAttrSelect(value, item, 'importance_attrs')">
<el-option <el-option
v-for="attr in getAvailableAttrs('importance_attrs', item.id)" v-for="attr in getAvailableAttrs('importance_attrs', item.id)"
...@@ -145,7 +141,7 @@ const unimportanceTotal = computed(() => { ...@@ -145,7 +141,7 @@ const unimportanceTotal = computed(() => {
:value="attr.id"> :value="attr.id">
</el-option> </el-option>
</el-select> </el-select>
<el-input v-model="item.value" placeholder="请输入属性值" style="flex: 1; max-width: 300px"> </el-input> <el-input v-model="item.value" placeholder="请输入属性值" class="attr-value"></el-input>
<el-button type="danger" plain :icon="Delete" circle @click="removeAttrItem('importance_attrs', index)"> <el-button type="danger" plain :icon="Delete" circle @click="removeAttrItem('importance_attrs', index)">
</el-button> </el-button>
</div> </div>
...@@ -156,14 +152,11 @@ const unimportanceTotal = computed(() => { ...@@ -156,14 +152,11 @@ const unimportanceTotal = computed(() => {
:label="`非重要属性 ${unimportanceTotal}/${attrs.unimportance_attrs.total}`" :label="`非重要属性 ${unimportanceTotal}/${attrs.unimportance_attrs.total}`"
prop="live_commodity_attrs.unimportance_attrs"> prop="live_commodity_attrs.unimportance_attrs">
<div style="display: flex; flex-direction: column; gap: 16px"> <div style="display: flex; flex-direction: column; gap: 16px">
<div <div v-for="(item, index) in form.live_commodity_attrs.unimportance_attrs" :key="index" class="attr-row">
v-for="(item, index) in form.live_commodity_attrs.unimportance_attrs"
:key="index"
style="display: flex; align-items: flex-start; gap: 12px">
<el-select <el-select
v-model="item.id" v-model="item.id"
placeholder="请选择属性" placeholder="请选择属性"
style="width: 200px" class="attr-select"
@change="(value) => handleAttrSelect(value, item, 'unimportance_attrs')"> @change="(value) => handleAttrSelect(value, item, 'unimportance_attrs')">
<el-option <el-option
v-for="attr in getAvailableAttrs('unimportance_attrs', item.id)" v-for="attr in getAvailableAttrs('unimportance_attrs', item.id)"
...@@ -172,7 +165,7 @@ const unimportanceTotal = computed(() => { ...@@ -172,7 +165,7 @@ const unimportanceTotal = computed(() => {
:value="attr.id"> :value="attr.id">
</el-option> </el-option>
</el-select> </el-select>
<el-input v-model="item.value" placeholder="请输入属性值" style="flex: 1; max-width: 300px"> </el-input> <el-input v-model="item.value" placeholder="请输入属性值" class="attr-value"></el-input>
<el-button type="danger" plain :icon="Delete" circle @click="removeAttrItem('unimportance_attrs', index)"> <el-button type="danger" plain :icon="Delete" circle @click="removeAttrItem('unimportance_attrs', index)">
</el-button> </el-button>
</div> </div>
...@@ -181,3 +174,36 @@ const unimportanceTotal = computed(() => { ...@@ -181,3 +174,36 @@ const unimportanceTotal = computed(() => {
</el-form-item> </el-form-item>
</div> </div>
</template> </template>
<style scoped>
.form-inline-row {
display: flex;
width: 100%;
align-items: center;
gap: 12px;
.el-input { flex: 1; }
}
.attr-row {
display: flex;
align-items: flex-start;
gap: 12px;
}
.attr-select {
width: 200px;
flex-shrink: 0;
}
.attr-value { flex: 1; }
@media (max-width: 767px) {
.form-inline-row {
flex-direction: column;
align-items: stretch;
.el-button { width: 100%; }
}
.attr-row {
flex-direction: column;
align-items: stretch;
}
.attr-select { width: 100%; }
.attr-value { max-width: none; }
}
</style>
...@@ -57,6 +57,7 @@ const handleSelect = (item) => { ...@@ -57,6 +57,7 @@ const handleSelect = (item) => {
<span @click="form.live_commodity_type_id = item.id">{{ item.name }}</span> <span @click="form.live_commodity_type_id = item.id">{{ item.name }}</span>
</template> </template>
</p> </p>
<div class="cascader-wrap">
<el-cascader-panel <el-cascader-panel
v-model="form.live_commodity_type_id" v-model="form.live_commodity_type_id"
filterable filterable
...@@ -68,12 +69,12 @@ const handleSelect = (item) => { ...@@ -68,12 +69,12 @@ const handleSelect = (item) => {
label: 'name', label: 'name',
value: 'id', value: 'id',
emitPath: false, emitPath: false,
expandTrigger: 'hover', expandTrigger: 'click',
checkStrictly: true, checkStrictly: true,
checkOnClickNode: true, checkOnClickNode: true,
showPrefix: false, showPrefix: false,
}" }" />
style="width: 100%" /> </div>
</el-form-item> </el-form-item>
</div> </div>
</template> </template>
...@@ -90,4 +91,12 @@ const handleSelect = (item) => { ...@@ -90,4 +91,12 @@ const handleSelect = (item) => {
} }
} }
} }
.cascader-wrap {
width: 100%;
overflow-x: auto;
-webkit-overflow-scrolling: touch;
.el-cascader-panel {
display: inline-flex;
}
}
</style> </style>
...@@ -28,7 +28,7 @@ const handleCopy = () => { ...@@ -28,7 +28,7 @@ const handleCopy = () => {
<el-form-item label=""> <el-form-item label="">
<template #label> <template #label>
<span>主图3:4</span> <span>主图3:4</span>
<el-button type="primary" plain size="small" style="margin-left: 430px" @click="handleCopy" <el-button type="primary" plain size="small" class="copy-btn" @click="handleCopy"
>从主图一键填入</el-button >从主图一键填入</el-button
> >
</template> </template>
...@@ -68,4 +68,14 @@ const handleCopy = () => { ...@@ -68,4 +68,14 @@ const handleCopy = () => {
color: #999; color: #999;
line-height: 24px; line-height: 24px;
} }
.copy-btn {
margin-left: 430px;
}
@media (max-width: 767px) {
.copy-btn {
margin-left: 0;
margin-top: 8px;
width: 100%;
}
}
</style> </style>
...@@ -313,6 +313,7 @@ function batchSetReferencePrice() { ...@@ -313,6 +313,7 @@ function batchSetReferencePrice() {
</el-button> </el-button>
</div> </div>
</div> </div>
<div class="sku-table">
<el-table :data="form.info.sku" border :header-cell-style="{ backgroundColor: '#f5f7fa' }" style="width: 100%" :row-key="Date.now() + Math.random()"> <el-table :data="form.info.sku" border :header-cell-style="{ backgroundColor: '#f5f7fa' }" style="width: 100%" :row-key="Date.now() + Math.random()">
<el-table-column v-if="hasSpecs" label="规格组合" align="center" width="200" column-key="specs"> <el-table-column v-if="hasSpecs" label="规格组合" align="center" width="200" column-key="specs">
<template #default="{ row }"> <template #default="{ row }">
...@@ -341,6 +342,7 @@ function batchSetReferencePrice() { ...@@ -341,6 +342,7 @@ function batchSetReferencePrice() {
</template> </template>
</el-table-column> </el-table-column>
</el-table> </el-table>
</div>
</el-form-item> </el-form-item>
<el-form-item label="参考价" prop="info.reference_price" v-if="false"> <el-form-item label="参考价" prop="info.reference_price" v-if="false">
<template #label> <template #label>
...@@ -362,7 +364,7 @@ function batchSetReferencePrice() { ...@@ -362,7 +364,7 @@ function batchSetReferencePrice() {
</div> </div>
</template> </template>
<style scoped> <style lang="scss" scoped>
.spec-list { .spec-list {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
...@@ -433,5 +435,20 @@ function batchSetReferencePrice() { ...@@ -433,5 +435,20 @@ function batchSetReferencePrice() {
.batch-set-item .el-input { .batch-set-item .el-input {
flex: 1; flex: 1;
} }
// SKU 表格横向滚动
.sku-table {
overflow-x: auto;
-webkit-overflow-scrolling: touch;
.el-table {
min-width: 600px;
}
}
// 参考价区域
.reference-price-row {
flex-direction: column;
align-items: stretch !important;
}
} }
</style> </style>
...@@ -6,12 +6,12 @@ const form = inject('form') ...@@ -6,12 +6,12 @@ const form = inject('form')
<template> <template>
<div> <div>
<el-form-item label="运费模板" prop="info.shipping_template" :rules="[{ required: true, message: '请选择' }]"> <el-form-item label="运费模板" prop="info.shipping_template" :rules="[{ required: true, message: '请选择' }]">
<el-select v-model="form.info.shipping_template" style="width: 300px"> <el-select v-model="form.info.shipping_template" style="width: 300px; max-width: 100%">
<el-option v-for="item in shippingTemplate" :key="item.value" v-bind="item"></el-option> <el-option v-for="item in shippingTemplate" :key="item.value" v-bind="item"></el-option>
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item label="售后政策" prop="info.after_sales_policy" :rules="[{ required: true, message: '请选择' }]"> <el-form-item label="售后政策" prop="info.after_sales_policy" :rules="[{ required: true, message: '请选择' }]">
<el-select v-model="form.info.after_sales_policy" style="width: 300px" multiple> <el-select v-model="form.info.after_sales_policy" style="width: 300px; max-width: 100%" multiple>
<el-option v-for="item in afterSalesPolicy" :key="item.value" v-bind="item"></el-option> <el-option v-for="item in afterSalesPolicy" :key="item.value" v-bind="item"></el-option>
</el-select> </el-select>
</el-form-item> </el-form-item>
......
...@@ -2,6 +2,9 @@ ...@@ -2,6 +2,9 @@
import { getChuanKitDesignList } from '../api' import { getChuanKitDesignList } from '../api'
import { useUserStore } from '@/stores/user' import { useUserStore } from '@/stores/user'
import { uploadFileByUrl } from '@/utils/upload' import { uploadFileByUrl } from '@/utils/upload'
import { useMediaQuery } from '@vueuse/core'
const isMobile = useMediaQuery('(max-width: 767px)')
const ChuangKitDesign = defineAsyncComponent(() => import('@/components/ChuangKitDesign.vue')) const ChuangKitDesign = defineAsyncComponent(() => import('@/components/ChuangKitDesign.vue'))
...@@ -71,7 +74,7 @@ async function handleSubmit() { ...@@ -71,7 +74,7 @@ async function handleSubmit() {
</script> </script>
<template> <template>
<el-dialog title="图库选择" :close-on-click-modal="false" @closed="$emit('update:modelValue', false)" width="860px"> <el-dialog title="图库选择" :close-on-click-modal="false" @closed="$emit('update:modelValue', false)" :width="isMobile ? '95%' : '860px'" destroy-on-close>
<div class="image-design"> <div class="image-design">
<el-button type="primary" @click="designVisible = true" v-if="false">打开编辑器</el-button> <el-button type="primary" @click="designVisible = true" v-if="false">打开编辑器</el-button>
<ul> <ul>
...@@ -97,10 +100,10 @@ async function handleSubmit() { ...@@ -97,10 +100,10 @@ async function handleSubmit() {
<style lang="scss"> <style lang="scss">
.image-design { .image-design {
ul { ul {
margin: 30px auto; margin: 20px auto;
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
gap: 30px; gap: 16px;
} }
li { li {
width: 140px; width: 140px;
...@@ -117,5 +120,17 @@ async function handleSubmit() { ...@@ -117,5 +120,17 @@ async function handleSubmit() {
height: 100%; height: 100%;
object-fit: cover; object-fit: cover;
} }
@media (max-width: 767px) {
ul {
justify-content: space-around;
gap: 12px;
}
li {
width: calc(33.33% - 12px);
min-width: 80px;
border-radius: 12px;
}
}
} }
</style> </style>
...@@ -92,9 +92,9 @@ const handleRemove = (i) => { ...@@ -92,9 +92,9 @@ const handleRemove = (i) => {
<div class="upload-wrapper"> <div class="upload-wrapper">
<template v-if="Array.isArray(props.modelValue)"> <template v-if="Array.isArray(props.modelValue)">
<div v-for="(item, i) in props.limit" :key="i"> <div v-for="(item, i) in props.limit" :key="i">
<el-popover placement="top" width="220px" trigger="hover"> <el-popover placement="top" width="220px" trigger="click">
<ul class="upload-popover"> <ul class="upload-popover">
<li @click="handleOpen(i)" v-if="false"> <li @click="handleOpen(i)">
<i class="el-icon"><UploadFilled /></i>本地上传 <i class="el-icon"><UploadFilled /></i>本地上传
</li> </li>
<li @click="handleOpenDesign(i)" v-if="!isVideo"> <li @click="handleOpenDesign(i)" v-if="!isVideo">
...@@ -113,7 +113,7 @@ const handleRemove = (i) => { ...@@ -113,7 +113,7 @@ const handleRemove = (i) => {
<i class="el-icon delete-icon" @click.stop="handleRemove(i)"><CircleClose /></i> <i class="el-icon delete-icon" @click.stop="handleRemove(i)"><CircleClose /></i>
</template> </template>
<template v-else> <template v-else>
<div class="upload-item-uploader" @click="handleOpen(i)"> <div class="upload-item-uploader">
<div class="upload-item__title">{{ getTitle(i) }}</div> <div class="upload-item__title">{{ getTitle(i) }}</div>
<i class="el-icon"><Plus /></i> <i class="el-icon"><Plus /></i>
<div class="upload-item__tips">{{ getTips(i) }}</div> <div class="upload-item__tips">{{ getTips(i) }}</div>
...@@ -126,9 +126,9 @@ const handleRemove = (i) => { ...@@ -126,9 +126,9 @@ const handleRemove = (i) => {
</template> </template>
<template v-else> <template v-else>
<el-popover placement="top" width="160px" trigger="hover"> <el-popover placement="top" width="160px" trigger="click">
<ul class="upload-popover"> <ul class="upload-popover">
<li @click="handleOpen" v-if="false"> <li @click="handleOpen">
<i class="el-icon"><UploadFilled /></i>本地上传 <i class="el-icon"><UploadFilled /></i>本地上传
</li> </li>
<li @click="handleOpenDesign" v-if="!isVideo"> <li @click="handleOpenDesign" v-if="!isVideo">
...@@ -148,7 +148,7 @@ const handleRemove = (i) => { ...@@ -148,7 +148,7 @@ const handleRemove = (i) => {
<i class="el-icon delete-icon" @click.stop="handleRemove()"><CircleClose /></i> <i class="el-icon delete-icon" @click.stop="handleRemove()"><CircleClose /></i>
</template> </template>
<template v-else> <template v-else>
<div class="upload-item-uploader" @click="handleOpen"> <div class="upload-item-uploader">
<div class="upload-item__title">{{ props.firstTitle || props.title }}</div> <div class="upload-item__title">{{ props.firstTitle || props.title }}</div>
<i class="el-icon"><Plus /></i> <i class="el-icon"><Plus /></i>
<div class="upload-item__tips">{{ props.firstTips || props.tips }}</div> <div class="upload-item__tips">{{ props.firstTips || props.tips }}</div>
...@@ -168,6 +168,10 @@ const handleRemove = (i) => { ...@@ -168,6 +168,10 @@ const handleRemove = (i) => {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
gap: 20px; gap: 20px;
@media (max-width: 767px) {
gap: 12px;
justify-content: center;
}
} }
.upload-popover { .upload-popover {
display: flex; display: flex;
......
...@@ -35,7 +35,7 @@ const listOptions = computed(() => { ...@@ -35,7 +35,7 @@ const listOptions = computed(() => {
}, },
filters: [ filters: [
{ label: '商品品类', prop: 'live_commodity_type_id', slots: 'filter-category' }, { label: '商品品类', prop: 'live_commodity_type_id', slots: 'filter-category' },
{ label: '商品名称', prop: 'title', type: 'input' }, { label: '商品名称', prop: 'title', type: 'input', placeholder: '请输入商品名称' },
], ],
columns: [ columns: [
{ label: '序号', type: 'index', width: 60 }, { label: '序号', type: 'index', width: 60 },
...@@ -49,6 +49,7 @@ const listOptions = computed(() => { ...@@ -49,6 +49,7 @@ const listOptions = computed(() => {
} }
}) })
// 删除
const handleRemove = async (row) => { const handleRemove = async (row) => {
await ElMessageBox.confirm('此操作将永久删除该数据, 是否继续?', '提示') await ElMessageBox.confirm('此操作将永久删除该数据, 是否继续?', '提示')
await deleteProduct({ id: row.id }) await deleteProduct({ id: row.id })
...@@ -61,30 +62,22 @@ const handleRemove = async (row) => { ...@@ -61,30 +62,22 @@ const handleRemove = async (row) => {
<AppCard title="商品管理"> <AppCard title="商品管理">
<AppList v-bind="listOptions" ref="appList"> <AppList v-bind="listOptions" ref="appList">
<template #header-buttons> <template #header-buttons>
<el-button type="primary"> <el-button type="primary"><router-link to="management/add">添加商品</router-link></el-button>
<router-link to="management/add">添加商品</router-link>
</el-button>
</template> </template>
<template #filter-category> <template #filter-category>
<LiveProductCategory v-model="listParams.live_commodity_type_id" @change="handleRefresh"></LiveProductCategory> <LiveProductCategory v-model="listParams.live_commodity_type_id" @change="handleRefresh" placeholder="请选择商品品类"></LiveProductCategory>
</template> </template>
<template #table-picture="{ row }"> <template #table-picture="{ row }">
<el-image <el-image fit="cover" preview-teleported :src="row.picture_url" :previewSrcList="row.picture_url_list" style="width:100px;height:100px"></el-image>
fit="cover"
preview-teleported
:src="row.picture_url"
:previewSrcList="row.picture_url_list"
style="width: 100px; height: 100px"></el-image>
</template> </template>
<template #table-x="{ row }"> <template #table-x="{ row }">
<el-button text type="primary"> <el-button text type="primary"><router-link :to="{ path: 'management/view', query: { id: row.id } }">查看</router-link></el-button>
<router-link :to="{ path: 'management/view', query: { id: row.id } }">查看</router-link> <el-button text type="primary"><router-link :to="{ path: 'management/update', query: { id: row.id } }">编辑</router-link></el-button>
</el-button>
<el-button text type="primary">
<router-link :to="{ path: 'management/update', query: { id: row.id } }">编辑</router-link>
</el-button>
<el-button text type="primary" @click="handleRemove(row)">删除</el-button> <el-button text type="primary" @click="handleRemove(row)">删除</el-button>
</template> </template>
</AppList> </AppList>
</AppCard> </AppCard>
</template> </template>
<style scoped>
</style>
...@@ -149,6 +149,26 @@ async function handleUpdate(params) { ...@@ -149,6 +149,26 @@ async function handleUpdate(params) {
.el-tabs__nav-wrap::after { .el-tabs__nav-wrap::after {
display: none; display: none;
} }
// 移动端 tab 横向滚动
@media (max-width: 767px) {
.el-tabs__header {
width: 100%;
}
.el-tabs__nav-wrap,
.el-tabs__nav-scroll {
overflow-x: auto;
overflow-y: hidden;
}
.el-tabs__nav {
display: inline-flex;
flex-wrap: nowrap;
}
.el-tabs__item {
flex-shrink: 0;
font-size: 13px;
padding: 0 8px;
}
}
} }
.product { .product {
.el-card { .el-card {
...@@ -163,5 +183,24 @@ async function handleUpdate(params) { ...@@ -163,5 +183,24 @@ async function handleUpdate(params) {
line-height: 22px; line-height: 22px;
color: #92939a; color: #92939a;
} }
@media (max-width: 767px) {
.form-tips {
position: static;
margin-bottom: 8px;
}
.el-row[style*="margin: 100px"] {
margin: 24px 0 16px !important;
flex-direction: column;
gap: 8px;
.el-button {
width: 100%;
margin-left: 0 !important;
}
}
.el-row[style*="min-height: 400px"] {
min-height: auto !important;
}
}
} }
</style> </style>
...@@ -2,6 +2,9 @@ ...@@ -2,6 +2,9 @@
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
import { createReport, getReport, getTestList } from '../api' import { createReport, getReport, getTestList } from '../api'
import AppUpload from '@/components/base/AppUpload.vue' import AppUpload from '@/components/base/AppUpload.vue'
import { useMediaQuery } from '@vueuse/core'
const isMobile = useMediaQuery('(max-width: 767px)')
const props = defineProps(['data', 'action']) const props = defineProps(['data', 'action'])
...@@ -73,13 +76,18 @@ function handleSuccess(file) { ...@@ -73,13 +76,18 @@ function handleSuccess(file) {
</script> </script>
<template> <template>
<el-dialog :title="title" :close-on-click-modal="false" @closed="$emit('update:modelValue', false)" width="500px"> <el-dialog
:title="title"
:close-on-click-modal="false"
@closed="$emit('update:modelValue', false)"
:width="isMobile ? '95%' : '500px'"
destroy-on-close>
<el-form <el-form
ref="formRef" ref="formRef"
:model="form" :model="form"
:rules="rules" :rules="rules"
label-position="right" :label-position="isMobile ? 'top' : 'right'"
label-width="110px" :label-width="isMobile ? undefined : '110px'"
:disabled="action === 'view'"> :disabled="action === 'view'">
<el-form-item label="关联直播" prop="live_practice_id"> <el-form-item label="关联直播" prop="live_practice_id">
<el-select v-model="form.live_practice_id" style="width: 100%" placeholder="请选择" filterable> <el-select v-model="form.live_practice_id" style="width: 100%" placeholder="请选择" filterable>
......
...@@ -26,7 +26,7 @@ const listOptions = computed(() => { ...@@ -26,7 +26,7 @@ const listOptions = computed(() => {
}, },
filters: [ filters: [
{ label: '直播主题品类', prop: 'live_commodity_type_id', slots: 'filter-category' }, { label: '直播主题品类', prop: 'live_commodity_type_id', slots: 'filter-category' },
{ label: '直播主题名称', prop: 'live_commodity_title', type: 'input' }, { label: '直播主题名称', prop: 'live_commodity_title', type: 'input', placeholder: '请输入直播主题名称' },
], ],
columns: [ columns: [
{ label: '序号', type: 'index', width: 60 }, { label: '序号', type: 'index', width: 60 },
...@@ -58,6 +58,7 @@ const handleView = (row) => { ...@@ -58,6 +58,7 @@ const handleView = (row) => {
currentRow.value = row currentRow.value = row
formVisible.value = true formVisible.value = true
} }
// 删除 // 删除
const handleRemove = async (row) => { const handleRemove = async (row) => {
await ElMessageBox.confirm('此操作将永久删除该数据, 是否继续?', '提示') await ElMessageBox.confirm('此操作将永久删除该数据, 是否继续?', '提示')
...@@ -74,7 +75,7 @@ const handleRemove = async (row) => { ...@@ -74,7 +75,7 @@ const handleRemove = async (row) => {
<el-button type="primary" @click="handleAdd">上传总结报告</el-button> <el-button type="primary" @click="handleAdd">上传总结报告</el-button>
</template> </template>
<template #filter-category> <template #filter-category>
<LiveProductCategory v-model="listParams.live_commodity_type_id" @change="handleRefresh"></LiveProductCategory> <LiveProductCategory v-model="listParams.live_commodity_type_id" @change="handleRefresh" placeholder="请选择直播主题品类"></LiveProductCategory>
</template> </template>
<template #table-x="{ row }"> <template #table-x="{ row }">
<el-button text type="primary" @click="handleView(row)">查看</el-button> <el-button text type="primary" @click="handleView(row)">查看</el-button>
...@@ -82,6 +83,8 @@ const handleRemove = async (row) => { ...@@ -82,6 +83,8 @@ const handleRemove = async (row) => {
</template> </template>
</AppList> </AppList>
</AppCard> </AppCard>
<!-- 新建/修改 -->
<FormDialog v-model="formVisible" :action="action" :data="currentRow" @update="handleRefresh" v-if="formVisible" /> <FormDialog v-model="formVisible" :action="action" :data="currentRow" @update="handleRefresh" v-if="formVisible" />
</template> </template>
<style scoped>
</style>
...@@ -5,6 +5,10 @@ import { createTalk, updateTalk, getTalk, getProduct } from '../api' ...@@ -5,6 +5,10 @@ import { createTalk, updateTalk, getTalk, getProduct } from '../api'
import { useChat } from '@/composables/useChat' import { useChat } from '@/composables/useChat'
import Editor from '@tinymce/tinymce-vue' import Editor from '@tinymce/tinymce-vue'
import { useMapStore } from '@/stores/map' import { useMapStore } from '@/stores/map'
import { useMediaQuery } from '@vueuse/core'
const isMobile = useMediaQuery('(max-width: 767px)')
const editorHeight = computed(() => (isMobile.value ? 250 : 420))
const props = defineProps(['data', 'action']) const props = defineProps(['data', 'action'])
...@@ -200,8 +204,13 @@ function handleAIGenerate(index) { ...@@ -200,8 +204,13 @@ function handleAIGenerate(index) {
</script> </script>
<template> <template>
<el-dialog :title="title" :close-on-click-modal="false" @closed="$emit('update:modelValue', false)" width="1000px"> <el-dialog
<el-row justify="center" :gutter="40"> :title="title"
:close-on-click-modal="false"
@closed="$emit('update:modelValue', false)"
:width="isMobile ? '95%' : '1000px'"
destroy-on-close>
<el-row justify="center" :gutter="40" class="talk-dialog-row">
<el-col :sm="24" :md="12"> <el-col :sm="24" :md="12">
<el-form ref="formRef" :model="form" :rules="rules" label-position="top" :disabled="action === 'view'"> <el-form ref="formRef" :model="form" :rules="rules" label-position="top" :disabled="action === 'view'">
<el-form-item label="直播话术名称" prop="name"> <el-form-item label="直播话术名称" prop="name">
...@@ -257,7 +266,7 @@ function handleAIGenerate(index) { ...@@ -257,7 +266,7 @@ function handleAIGenerate(index) {
</el-form-item> </el-form-item>
</el-form> </el-form>
</el-col> </el-col>
<el-col :sm="24" :md="12" style="border-left: 1px solid #dcdfe6"> <el-col :sm="24" :md="12" class="talk-dialog-right">
<div style="text-align: center; margin-bottom: 20px"> <div style="text-align: center; margin-bottom: 20px">
<h2 style="margin-bottom: 20px">直播话术</h2> <h2 style="margin-bottom: 20px">直播话术</h2>
<el-button <el-button
...@@ -276,7 +285,7 @@ function handleAIGenerate(index) { ...@@ -276,7 +285,7 @@ function handleAIGenerate(index) {
quickbars_insert_toolbar: false, quickbars_insert_toolbar: false,
menubar: false, menubar: false,
statusbar: false, statusbar: false,
height: 420, height: editorHeight,
content_style: 'body { font-family:Helvetica,Arial,sans-serif; font-size:13px }', content_style: 'body { font-family:Helvetica,Arial,sans-serif; font-size:13px }',
}" /> }" />
</el-col> </el-col>
...@@ -307,4 +316,19 @@ function handleAIGenerate(index) { ...@@ -307,4 +316,19 @@ function handleAIGenerate(index) {
line-height: 22px; line-height: 22px;
color: #92939a; color: #92939a;
} }
@media (max-width: 767px) {
.talk-dialog-row {
flex-direction: column;
}
.talk-dialog-right {
border-left: none !important;
border-bottom: 1px solid #dcdfe6;
padding-bottom: 16px;
margin-bottom: 16px;
}
.form-tips {
position: static;
margin-bottom: 8px;
}
}
</style> </style>
...@@ -32,8 +32,13 @@ const listOptions = computed(() => { ...@@ -32,8 +32,13 @@ const listOptions = computed(() => {
}, },
filters: [ filters: [
{ label: '直播主题品类', prop: 'live_commodity_type_id', slots: 'filter-category' }, { label: '直播主题品类', prop: 'live_commodity_type_id', slots: 'filter-category' },
{ label: '直播主题名称', prop: 'live_commodity_title', type: 'input' }, {
{ label: '话术名称', prop: 'name', type: 'input' }, label: '直播主题名称',
prop: 'live_commodity_title',
type: 'input',
placeholder: '请输入直播主题名称',
},
{ label: '话术名称', prop: 'name', type: 'input', placeholder: '请输入话术名称' },
], ],
columns: [ columns: [
{ label: '序号', type: 'index', width: 60 }, { label: '序号', type: 'index', width: 60 },
...@@ -82,10 +87,13 @@ const handleRemove = async (row) => { ...@@ -82,10 +87,13 @@ const handleRemove = async (row) => {
<AppCard title="直播话术管理"> <AppCard title="直播话术管理">
<AppList v-bind="listOptions" ref="appList"> <AppList v-bind="listOptions" ref="appList">
<template #header-buttons> <template #header-buttons>
<el-button type="primary" @click="handleAdd" v-if="!listLength">添加话术</el-button> <el-button type="primary" @click="handleAdd">添加话术</el-button>
</template> </template>
<template #filter-category> <template #filter-category>
<LiveProductCategory v-model="listParams.live_commodity_type_id" @change="handleRefresh"></LiveProductCategory> <LiveProductCategory
v-model="listParams.live_commodity_type_id"
@change="handleRefresh"
placeholder="请选择直播主题品类"></LiveProductCategory>
</template> </template>
<template #table-x="{ row }"> <template #table-x="{ row }">
<el-button text type="primary" @click="handleView(row)">查看</el-button> <el-button text type="primary" @click="handleView(row)">查看</el-button>
...@@ -97,3 +105,5 @@ const handleRemove = async (row) => { ...@@ -97,3 +105,5 @@ const handleRemove = async (row) => {
<!-- 新建/修改 --> <!-- 新建/修改 -->
<FormDialog v-model="formVisible" :action="action" :data="currentRow" @update="handleRefresh" v-if="formVisible" /> <FormDialog v-model="formVisible" :action="action" :data="currentRow" @update="handleRefresh" v-if="formVisible" />
</template> </template>
<style scoped></style>
...@@ -263,4 +263,34 @@ const onStatsChange = (stats) => { ...@@ -263,4 +263,34 @@ const onStatsChange = (stats) => {
overflow-y: auto; overflow-y: auto;
line-height: 24px; line-height: 24px;
} }
@media (max-width: 767px) {
.live-row {
flex-direction: column;
gap: 12px;
height: auto;
}
.live-col {
width: 100% !important;
padding: 12px;
border-radius: 0;
border-left: none;
border-right: none;
}
.live-time {
height: auto;
font-size: 42px;
margin: 8px 0;
}
.live-data2 dd {
font-size: 32px;
}
.live-tag {
margin: 12px 0;
padding: 12px;
}
.live-talk-content {
max-height: none;
}
}
</style> </style>
...@@ -3,6 +3,9 @@ import { ElMessage } from 'element-plus' ...@@ -3,6 +3,9 @@ import { ElMessage } from 'element-plus'
import LiveProductSelect from '@/components/LiveProductSelect.vue' import LiveProductSelect from '@/components/LiveProductSelect.vue'
import { createTest, updateTest, getTalkList } from '../api' import { createTest, updateTest, getTalkList } from '../api'
import { useMapStore } from '@/stores/map' import { useMapStore } from '@/stores/map'
import { useMediaQuery } from '@vueuse/core'
const isMobile = useMediaQuery('(max-width: 767px)')
const props = defineProps(['data']) const props = defineProps(['data'])
...@@ -62,7 +65,7 @@ async function handleUpdate() { ...@@ -62,7 +65,7 @@ async function handleUpdate() {
</script> </script>
<template> <template>
<el-dialog :title="title" :close-on-click-modal="false" @closed="$emit('update:modelValue', false)"> <el-dialog :title="title" :close-on-click-modal="false" @closed="$emit('update:modelValue', false)" :width="isMobile ? '90%' : undefined">
<el-row justify="center"> <el-row justify="center">
<el-col :sm="24" :md="16"> <el-col :sm="24" :md="16">
<el-form ref="formRef" :model="form" :rules="rules" label-position="top"> <el-form ref="formRef" :model="form" :rules="rules" label-position="top">
......
...@@ -80,6 +80,8 @@ const { ...@@ -80,6 +80,8 @@ const {
endTime, endTime,
duration, duration,
currentTime: currentLiveTime, currentTime: currentLiveTime,
switchCamera,
isMobile,
} = useLive({ } = useLive({
onStart: () => { onStart: () => {
console.log('开始直播') console.log('开始直播')
...@@ -215,15 +217,28 @@ defineExpose({ enabled, start, stop }) ...@@ -215,15 +217,28 @@ defineExpose({ enabled, start, stop })
<img :src="enabled ? '/live/live2.png' : '/live/live1.png'" alt="直播状态" class="live-icon" /> <img :src="enabled ? '/live/live2.png' : '/live/live1.png'" alt="直播状态" class="live-icon" />
</div> </div>
<div class="live-bd"> <div class="live-bd">
<video muted autoplay ref="video"></video> <video muted autoplay ref="video" playsinline></video>
<LiveCover :messages="messages" :stats="stats" :viewers="viewers" v-if="enabled" /> <LiveCover :messages="messages" :stats="stats" :viewers="viewers" v-if="enabled" />
<!-- 移动端摄像头切换按钮 -->
<el-button
class="camera-switch-btn"
circle
size="small"
@click="switchCamera"
v-if="isMobile && !enabled && !recordId">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"/>
<circle cx="12" cy="13" r="4"/>
</svg>
</el-button>
</div> </div>
<div class="live-ft"> <div class="live-ft">
<div class="live-ft-row">
<el-button <el-button
type="primary" :type="enabled ? 'danger' : 'primary'"
size="large" size="large"
round round
style="width: 80%" class="live-ft-btn"
@click="enabled ? stop() : start()" @click="enabled ? stop() : start()"
v-if="!recordId"> v-if="!recordId">
{{ enabled ? '结束直播' : '开始直播' }} {{ enabled ? '结束直播' : '开始直播' }}
...@@ -232,7 +247,7 @@ defineExpose({ enabled, start, stop }) ...@@ -232,7 +247,7 @@ defineExpose({ enabled, start, stop })
type="primary" type="primary"
size="large" size="large"
round round
style="width: 80%; margin: 10px 0 0 0" class="live-ft-btn"
:loading="uploading" :loading="uploading"
@click="handleUpload" @click="handleUpload"
v-if="isLocalUpload && recordId" v-if="isLocalUpload && recordId"
...@@ -240,6 +255,7 @@ defineExpose({ enabled, start, stop }) ...@@ -240,6 +255,7 @@ defineExpose({ enabled, start, stop })
> >
</div> </div>
</div> </div>
</div>
<el-dialog title="提示" :model-value="dialogVisible" :showClose="false" width="500px"> <el-dialog title="提示" :model-value="dialogVisible" :showClose="false" width="500px">
<div class="modal-content"> <div class="modal-content">
<template v-if="isPolling"> <template v-if="isPolling">
...@@ -271,6 +287,7 @@ defineExpose({ enabled, start, stop }) ...@@ -271,6 +287,7 @@ defineExpose({ enabled, start, stop })
<style lang="scss"> <style lang="scss">
.live { .live {
width: 375px; width: 375px;
max-width: 100%;
&-hd { &-hd {
display: flex; display: flex;
...@@ -285,6 +302,7 @@ defineExpose({ enabled, start, stop }) ...@@ -285,6 +302,7 @@ defineExpose({ enabled, start, stop })
&-bd { &-bd {
position: relative; position: relative;
height: 667px; height: 667px;
max-height: 70vh;
border-radius: 8px; border-radius: 8px;
overflow: hidden; overflow: hidden;
background-color: #000; background-color: #000;
...@@ -298,6 +316,60 @@ defineExpose({ enabled, start, stop }) ...@@ -298,6 +316,60 @@ defineExpose({ enabled, start, stop })
&-ft { &-ft {
margin-top: 10px; margin-top: 10px;
text-align: center; text-align: center;
&-row {
display: flex;
justify-content: center;
gap: 10px;
}
&-btn {
min-width: 160px;
}
}
// 摄像头切换按钮
.camera-switch-btn {
position: absolute;
top: 10px;
right: 10px;
z-index: 130;
background: rgba(0, 0, 0, 0.4);
color: #fff;
border: 1px solid rgba(255, 255, 255, 0.3);
&:hover {
background: rgba(0, 0, 0, 0.6);
}
}
// 移动端适配
@media (max-width: 768px) {
width: 100%;
&-bd {
height: 50vh;
max-height: 50vh;
border-radius: 0;
}
&-hd {
padding: 0 4px;
font-size: 14px;
}
&-ft {
&-btn {
width: 80%;
min-width: auto;
}
}
}
@media (max-width: 480px) {
&-bd {
height: 40vh;
max-height: 40vh;
}
} }
} }
.modal-content { .modal-content {
......
...@@ -110,7 +110,7 @@ const handleInput = (value) => { ...@@ -110,7 +110,7 @@ const handleInput = (value) => {
</defs> </defs>
</svg> </svg>
</div> </div>
<div class="live-slider" v-show="hover"> <div class="live-slider" :class="{ 'is-visible': hover }">
<el-slider <el-slider
v-model="currentTime" v-model="currentTime"
:step="0.1" :step="0.1"
...@@ -127,6 +127,7 @@ const handleInput = (value) => { ...@@ -127,6 +127,7 @@ const handleInput = (value) => {
<style lang="scss"> <style lang="scss">
.live { .live {
width: 375px; width: 375px;
max-width: 100%;
&-hd { &-hd {
display: flex; display: flex;
...@@ -141,6 +142,7 @@ const handleInput = (value) => { ...@@ -141,6 +142,7 @@ const handleInput = (value) => {
&-bd { &-bd {
position: relative; position: relative;
height: 667px; height: 667px;
max-height: 70vh;
border-radius: 8px; border-radius: 8px;
overflow: hidden; overflow: hidden;
background-color: #000; background-color: #000;
...@@ -155,6 +157,38 @@ const handleInput = (value) => { ...@@ -155,6 +157,38 @@ const handleInput = (value) => {
margin-top: 10px; margin-top: 10px;
text-align: center; text-align: center;
} }
@media (max-width: 768px) {
width: 100%;
&-bd {
height: 50vh;
max-height: 50vh;
border-radius: 0;
}
&-hd {
padding: 0 4px;
font-size: 14px;
}
.live-play-button {
display: flex;
}
.live-slider {
display: block;
}
.live-slider.is-visible {
display: block;
}
}
@media (max-width: 480px) {
&-bd {
height: 40vh;
max-height: 40vh;
}
}
} }
.live-play-button { .live-play-button {
position: absolute; position: absolute;
......
...@@ -4,6 +4,7 @@ import Demo from '../components/Demo.vue' ...@@ -4,6 +4,7 @@ import Demo from '../components/Demo.vue'
import ImprovementPlan from '../components/ImprovementPlan.vue' import ImprovementPlan from '../components/ImprovementPlan.vue'
import RecordViewText from './RecordViewText.vue' import RecordViewText from './RecordViewText.vue'
import { useLiveRecord } from '../composables/useLiveRecord' import { useLiveRecord } from '../composables/useLiveRecord'
import { useMediaQuery } from '@vueuse/core'
const props = defineProps({ const props = defineProps({
isView: { type: Boolean, default: true }, isView: { type: Boolean, default: true },
...@@ -12,6 +13,8 @@ const props = defineProps({ ...@@ -12,6 +13,8 @@ const props = defineProps({
const { liveRecord: detail, handleAIScore, isLoading } = useLiveRecord({ id: props.recordId, immediate: true }) const { liveRecord: detail, handleAIScore, isLoading } = useLiveRecord({ id: props.recordId, immediate: true })
const isMobile = useMediaQuery('(max-width: 767px)')
const timelines = computed(() => { const timelines = computed(() => {
return detail.value ? JSON.parse(detail.value.subtitle)?.Result?.Sentences || [] : [] return detail.value ? JSON.parse(detail.value.subtitle)?.Result?.Sentences || [] : []
}) })
...@@ -225,8 +228,8 @@ function formatDuration(seconds) { ...@@ -225,8 +228,8 @@ function formatDuration(seconds) {
</el-step> </el-step>
</el-steps> </el-steps>
</AppCard> </AppCard>
<el-dialog v-model="dialogVisible" title="直播回放" width="1200px"> <el-dialog v-model="dialogVisible" title="直播回放" :width="isMobile ? '95%' : '1200px'" destroy-on-close>
<div style="height: 740px"> <div :style="{ height: isMobile ? 'auto' : '740px', maxHeight: isMobile ? '80vh' : 'none', overflowY: isMobile ? 'auto' : 'hidden' }">
<Demo :id="id" :recordId="recordId" :isView="true" /> <Demo :id="id" :recordId="recordId" :isView="true" />
</div> </div>
</el-dialog> </el-dialog>
...@@ -272,6 +275,38 @@ function formatDuration(seconds) { ...@@ -272,6 +275,38 @@ function formatDuration(seconds) {
width: 200px; width: 200px;
} }
} }
@media (max-width: 767px) {
flex-direction: column;
.live-info-header {
border-right: none;
border-bottom: 1px solid #e5e5e5;
padding-right: 0;
padding-bottom: 12px;
h2 {
font-size: 20px;
}
h6 {
font-size: 16px;
}
}
.live-info-item {
div {
width: calc(50% - 12px);
min-width: 120px;
}
}
}
@media (max-width: 480px) {
.live-info-item {
div {
width: 100%;
}
}
}
} }
.live-title { .live-title {
font-size: 14px; font-size: 14px;
......
...@@ -36,10 +36,25 @@ export function useLive({ enabledUserMedia = true, onStart, onRecord, onStop }: ...@@ -36,10 +36,25 @@ export function useLive({ enabledUserMedia = true, onStart, onRecord, onStop }:
const currentCamera = computed(() => cameras.value[0]?.deviceId) const currentCamera = computed(() => cameras.value[0]?.deviceId)
const currentMicrophone = computed(() => microphones.value[0]?.deviceId) const currentMicrophone = computed(() => microphones.value[0]?.deviceId)
// 移动端摄像头切换
const facingMode = ref<'user' | 'environment'>('user')
const isFrontCamera = computed(() => facingMode.value === 'user')
// 检测移动端
const isMobile = /Android|iPhone|iPad|iPod/i.test(navigator.userAgent) || window.innerWidth < 768
// 响应式媒体约束:移动端使用 facingMode,桌面端使用 deviceId
const constraints = computed(() => {
if (isMobile) {
return { video: { facingMode: { ideal: facingMode.value } }, audio: true }
}
return { video: { deviceId: currentCamera.value as any }, audio: { deviceId: currentMicrophone.value as any } }
})
// 创建媒体流 // 创建媒体流
const { stream } = useUserMedia({ const { stream } = useUserMedia({
enabled: enabledUserMedia, enabled: enabledUserMedia,
constraints: { video: { deviceId: currentCamera as any }, audio: { deviceId: currentMicrophone as any } }, constraints: constraints as any,
}) })
// 录像设置 // 录像设置
...@@ -102,5 +117,13 @@ export function useLive({ enabledUserMedia = true, onStart, onRecord, onStop }: ...@@ -102,5 +117,13 @@ export function useLive({ enabledUserMedia = true, onStart, onRecord, onStop }:
if (mediaRecorder) mediaRecorder.stop() if (mediaRecorder) mediaRecorder.stop()
} }
return { stream, start, stop, startTime, endTime, duration, currentTime } // 切换摄像头(移动端前置/后置)
const switchCamera = () => {
if (mediaRecorder?.state === 'recording') return
facingMode.value = facingMode.value === 'user' ? 'environment' : 'user'
// 流自动更新后重新初始化 MediaRecorder
nextTick(() => initializeMediaRecorder())
}
return { stream, start, stop, startTime, endTime, duration, currentTime, switchCamera, facingMode, isFrontCamera, isMobile }
} }
...@@ -34,7 +34,7 @@ const listOptions = computed(() => { ...@@ -34,7 +34,7 @@ const listOptions = computed(() => {
}, },
filters: [ filters: [
{ label: '直播主题品类', prop: 'live_commodity_type_id', slots: 'filter-category' }, { label: '直播主题品类', prop: 'live_commodity_type_id', slots: 'filter-category' },
{ label: '直播主题标题', prop: 'live_commodity_title', type: 'input' }, { label: '直播主题标题', prop: 'live_commodity_title', type: 'input', placeholder: '请输入直播主题标题' },
], ],
columns: [ columns: [
{ label: '序号', type: 'index', width: 60 }, { label: '序号', type: 'index', width: 60 },
...@@ -42,13 +42,7 @@ const listOptions = computed(() => { ...@@ -42,13 +42,7 @@ const listOptions = computed(() => {
{ label: '直播主题标题', prop: 'live_commodity_title' }, { label: '直播主题标题', prop: 'live_commodity_title' },
{ label: '直播话术', prop: 'live_commodity_speeches_name' }, { label: '直播话术', prop: 'live_commodity_speeches_name' },
{ label: '所属直播主题品类', prop: 'live_commodity_type_full_name' }, { label: '所属直播主题品类', prop: 'live_commodity_type_full_name' },
{ { label: '上传方式', prop: 'upload_way', computed: ({ row }) => getNameByValue(row.upload_way, liveUploadWay) },
label: '上传方式',
prop: 'upload_way',
computed({ row }) {
return getNameByValue(row.upload_way, liveUploadWay)
},
},
{ label: '创建人', prop: 'created_operator', computed: ({ row }) => getOperatorName(row.created_operator) }, { label: '创建人', prop: 'created_operator', computed: ({ row }) => getOperatorName(row.created_operator) },
{ label: '更新时间', prop: 'updated_time' }, { label: '更新时间', prop: 'updated_time' },
{ label: '操作', slots: 'table-x', width: 200 }, { label: '操作', slots: 'table-x', width: 200 },
...@@ -60,6 +54,7 @@ const formVisible = ref(false) ...@@ -60,6 +54,7 @@ const formVisible = ref(false)
const recordVisible = ref(false) const recordVisible = ref(false)
const currentRow = ref(null) const currentRow = ref(null)
// 查看
const handelView = (row) => { const handelView = (row) => {
recordVisible.value = true recordVisible.value = true
currentRow.value = row currentRow.value = row
...@@ -88,7 +83,10 @@ const handleStart = async (row) => { ...@@ -88,7 +83,10 @@ const handleStart = async (row) => {
<el-button type="primary" @click="formVisible = true">添加直播</el-button> <el-button type="primary" @click="formVisible = true">添加直播</el-button>
</template> </template>
<template #filter-category> <template #filter-category>
<LiveProductCategory v-model="listParams.live_commodity_type_id" @change="handleRefresh"></LiveProductCategory> <LiveProductCategory
v-model="listParams.live_commodity_type_id"
@change="handleRefresh"
placeholder="请选择直播主题品类"></LiveProductCategory>
</template> </template>
<template #table-x="{ row }"> <template #table-x="{ row }">
<el-button text type="primary" @click="handleStart(row)" v-if="row.practice_count == 0">开始直播</el-button> <el-button text type="primary" @click="handleStart(row)" v-if="row.practice_count == 0">开始直播</el-button>
...@@ -97,8 +95,6 @@ const handleStart = async (row) => { ...@@ -97,8 +95,6 @@ const handleStart = async (row) => {
</template> </template>
</AppList> </AppList>
</AppCard> </AppCard>
<!-- 新建/修改 -->
<FormDialog v-model="formVisible" @update="handleRefresh" v-if="formVisible" /> <FormDialog v-model="formVisible" @update="handleRefresh" v-if="formVisible" />
<!-- 查看 -->
<RecordDialog v-model="recordVisible" :data="currentRow" v-if="recordVisible"></RecordDialog> <RecordDialog v-model="recordVisible" :data="currentRow" v-if="recordVisible"></RecordDialog>
</template> </template>
...@@ -135,4 +135,9 @@ async function handleNext() { ...@@ -135,4 +135,9 @@ async function handleNext() {
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
} }
@media (max-width: 767px) {
.remove {
display: block !important;
}
}
</style> </style>
...@@ -77,4 +77,5 @@ const handleView = row => { ...@@ -77,4 +77,5 @@ const handleView = row => {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
} }
@media (max-width: 767px) { .info { flex-direction: column; gap: 8px; } }
</style> </style>
...@@ -114,4 +114,10 @@ async function handleNext(data, isCheck = false) { ...@@ -114,4 +114,10 @@ async function handleNext(data, isCheck = false) {
margin: 0; margin: 0;
} }
} }
@media (max-width: 767px) {
.el-row[style*="padding: 60px"] {
padding: 24px 0 !important;
gap: 20px !important;
}
}
</style> </style>
...@@ -139,4 +139,7 @@ function isComplete(is: boolean) { ...@@ -139,4 +139,7 @@ function isComplete(is: boolean) {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
} }
@media (max-width: 767px) {
.info { flex-direction: column; gap: 8px; }
}
</style> </style>
...@@ -112,4 +112,5 @@ const handleSubmit = function () { ...@@ -112,4 +112,5 @@ const handleSubmit = function () {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
} }
@media (max-width: 767px) { .info { flex-direction: column; gap: 8px; } }
</style> </style>
...@@ -50,4 +50,5 @@ const data = $computed(() => { ...@@ -50,4 +50,5 @@ const data = $computed(() => {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
} }
@media (max-width: 767px) { .info { flex-direction: column; gap: 8px; } }
</style> </style>
...@@ -171,7 +171,7 @@ const popoverText = function (row: any) { ...@@ -171,7 +171,7 @@ const popoverText = function (row: any) {
</el-table-column> </el-table-column>
<el-table-column width="30"> <el-table-column width="30">
<template #default="{ row }"> <template #default="{ row }">
<el-popover placement="top-start" :width="300" trigger="hover" :title="row.type_name"> <el-popover placement="top-start" :width="300" trigger="click" :title="row.type_name">
<template #reference> <template #reference>
<div style="display: flex; justify-content: center; cursor: pointer"> <div style="display: flex; justify-content: center; cursor: pointer">
<el-icon size="20"><QuestionFilled /></el-icon> <el-icon size="20"><QuestionFilled /></el-icon>
......
...@@ -169,7 +169,7 @@ const popoverText = function (type: any) { ...@@ -169,7 +169,7 @@ const popoverText = function (type: any) {
v-else v-else
v-model="form.format" v-model="form.format"
:placeholder="formatPlaceholder" /> :placeholder="formatPlaceholder" />
<el-popover placement="top-start" :width="300" trigger="hover" :title="form.type_name"> <el-popover placement="top-start" :width="300" trigger="click" :title="form.type_name">
<template #reference> <template #reference>
<div style="display: flex; justify-content: center; cursor: pointer"> <div style="display: flex; justify-content: center; cursor: pointer">
<el-icon size="20"><QuestionFilled /></el-icon> <el-icon size="20"><QuestionFilled /></el-icon>
......
...@@ -161,4 +161,46 @@ const handleDelete = (url) => { ...@@ -161,4 +161,46 @@ const handleDelete = (url) => {
color: #000; color: #000;
line-height: 30px; line-height: 30px;
} }
@media (max-width: 767px) {
li .el-icon {
display: block !important;
}
.operations-tabs :deep(.el-tabs__header) {
width: 100%;
overflow-x: auto;
}
.operations-tabs :deep(.el-tabs__nav-wrap),
.operations-tabs :deep(.el-tabs__nav-scroll) {
overflow-x: auto;
overflow-y: hidden;
}
.operations-tabs :deep(.el-tabs__nav) {
display: inline-flex;
flex-wrap: nowrap;
}
.operations-tabs :deep(.el-tabs__item) {
flex-shrink: 0;
font-size: 13px;
padding: 0 8px;
}
:deep(.el-card__body) > div[style*="padding: 40px"] {
padding: 16px !important;
min-height: auto !important;
}
.image-list {
gap: 12px;
li {
width: calc(50% - 6px);
height: 160px;
}
}
.el-row {
flex-wrap: wrap;
justify-content: center;
gap: 8px;
.el-button {
margin-left: 0 !important;
}
}
}
</style> </style>
...@@ -244,7 +244,7 @@ const handleNext = () => { ...@@ -244,7 +244,7 @@ const handleNext = () => {
<div class="table-expand-box"> <div class="table-expand-box">
<el-form label-position="top"> <el-form label-position="top">
<el-row> <el-row>
<el-col :span="12"> <el-col :xs="24" :sm="12">
<el-form-item label="主图"> <el-form-item label="主图">
<el-image <el-image
:src="item.url" :src="item.url"
...@@ -266,7 +266,7 @@ const handleNext = () => { ...@@ -266,7 +266,7 @@ const handleNext = () => {
style="width: 100px; height: 134px; margin-right: 10px"></el-image> style="width: 100px; height: 134px; margin-right: 10px"></el-image>
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-col :span="12"> <el-col :xs="24" :sm="12">
<el-form-item label="主图视频"> <el-form-item label="主图视频">
<video <video
:src="row.video_url" :src="row.video_url"
...@@ -385,6 +385,25 @@ const handleNext = () => { ...@@ -385,6 +385,25 @@ const handleNext = () => {
:deep(.el-tabs__nav-wrap::after) { :deep(.el-tabs__nav-wrap::after) {
display: none; display: none;
} }
@media (max-width: 767px) {
:deep(.el-tabs__header) {
width: 100%;
}
:deep(.el-tabs__nav-wrap),
:deep(.el-tabs__nav-scroll) {
overflow-x: auto;
overflow-y: hidden;
}
:deep(.el-tabs__nav) {
display: inline-flex;
flex-wrap: nowrap;
}
:deep(.el-tabs__item) {
flex-shrink: 0;
font-size: 13px;
padding: 0 8px;
}
}
} }
.score-header { .score-header {
display: flex; display: flex;
......
...@@ -279,7 +279,7 @@ const disableTabPanel = (type) => { ...@@ -279,7 +279,7 @@ const disableTabPanel = (type) => {
<div class="table-expand-box"> <div class="table-expand-box">
<el-form label-position="top"> <el-form label-position="top">
<el-row> <el-row>
<el-col :span="12"> <el-col :xs="24" :sm="12">
<el-form-item label="主图"> <el-form-item label="主图">
<el-image <el-image
:src="item.url" :src="item.url"
...@@ -301,7 +301,7 @@ const disableTabPanel = (type) => { ...@@ -301,7 +301,7 @@ const disableTabPanel = (type) => {
style="width: 100px; height: 134px; margin-right: 10px"></el-image> style="width: 100px; height: 134px; margin-right: 10px"></el-image>
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-col :span="12"> <el-col :xs="24" :sm="12">
<el-form-item label="主图视频"> <el-form-item label="主图视频">
<video <video
:src="row.video_url" :src="row.video_url"
...@@ -397,6 +397,14 @@ const disableTabPanel = (type) => { ...@@ -397,6 +397,14 @@ const disableTabPanel = (type) => {
:deep(.el-tabs__nav-wrap::after) { :deep(.el-tabs__nav-wrap::after) {
display: none; display: none;
} }
@media (max-width: 767px) {
:deep(.el-tabs__header) { width: 100%; }
:deep(.el-tabs__nav-wrap),
:deep(.el-tabs__nav-scroll) { overflow-x: auto; overflow-y: hidden; }
:deep(.el-tabs__nav) { display: inline-flex; flex-wrap: nowrap; }
:deep(.el-tabs__item) { flex-shrink: 0; font-size: 13px; padding: 0 8px; }
}
} }
.score-header { .score-header {
......
...@@ -245,7 +245,7 @@ const handleNext = () => { ...@@ -245,7 +245,7 @@ const handleNext = () => {
<div class="table-expand-box"> <div class="table-expand-box">
<el-form label-position="top"> <el-form label-position="top">
<el-row> <el-row>
<el-col :span="12"> <el-col :xs="24" :sm="12">
<el-form-item label="主图"> <el-form-item label="主图">
<el-image <el-image
:src="item.url" :src="item.url"
...@@ -267,7 +267,7 @@ const handleNext = () => { ...@@ -267,7 +267,7 @@ const handleNext = () => {
style="width: 100px; height: 134px; margin-right: 10px"></el-image> style="width: 100px; height: 134px; margin-right: 10px"></el-image>
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-col :span="12"> <el-col :xs="24" :sm="12">
<el-form-item label="主图视频"> <el-form-item label="主图视频">
<video <video
:src="row.video_url" :src="row.video_url"
...@@ -386,6 +386,13 @@ const handleNext = () => { ...@@ -386,6 +386,13 @@ const handleNext = () => {
:deep(.el-tabs__nav-wrap::after) { :deep(.el-tabs__nav-wrap::after) {
display: none; display: none;
} }
@media (max-width: 767px) {
:deep(.el-tabs__header) { width: 100%; }
:deep(.el-tabs__nav-wrap),
:deep(.el-tabs__nav-scroll) { overflow-x: auto; overflow-y: hidden; }
:deep(.el-tabs__nav) { display: inline-flex; flex-wrap: nowrap; }
:deep(.el-tabs__item) { flex-shrink: 0; font-size: 13px; padding: 0 8px; }
}
} }
.score-header { .score-header {
display: flex; display: flex;
......
...@@ -204,6 +204,15 @@ const handleNext = () => { ...@@ -204,6 +204,15 @@ const handleNext = () => {
} }
} }
} }
@media (max-width: 767px) {
.image-list {
gap: 12px;
li {
width: calc(50% - 6px);
height: 160px;
}
}
}
.video-player { .video-player {
max-height: 400px; max-height: 400px;
} }
......
...@@ -24,11 +24,12 @@ const listOptions = computed(() => { ...@@ -24,11 +24,12 @@ const listOptions = computed(() => {
return { return {
remote: { httpRequest: getScoreList, params: listParams }, remote: { httpRequest: getScoreList, params: listParams },
filters: [ filters: [
{ label: '姓名', prop: 'name', type: 'input' }, { label: '姓名', prop: 'name', type: 'input', placeholder: '请输入姓名' },
{ {
label: '状态', label: '状态',
prop: 'check_status', prop: 'check_status',
type: 'select', type: 'select',
placeholder: '请选择状态',
options: [ options: [
{ label: '暂未评分', value: '0' }, { label: '暂未评分', value: '0' },
{ label: '评分中', value: '1' }, { label: '评分中', value: '1' },
...@@ -101,8 +102,12 @@ useIntervalFn(() => { ...@@ -101,8 +102,12 @@ useIntervalFn(() => {
</template> </template>
<template #filter-category> <template #filter-category>
<LiveProductCategory v-model="listParams.live_commodity_type_id" @change="handleRefresh"></LiveProductCategory> <LiveProductCategory
v-model="listParams.live_commodity_type_id"
@change="handleRefresh"
placeholder="请选择直播主题品类"></LiveProductCategory>
</template> </template>
<template #table-x="{ row }"> <template #table-x="{ row }">
<el-button text type="primary" v-if="row.check_status != '1'"> <el-button text type="primary" v-if="row.check_status != '1'">
<router-link :to="{ path: 'score/view', query: { ...$route.query, id: row.id } }">评分</router-link> <router-link :to="{ path: 'score/view', query: { ...$route.query, id: row.id } }">评分</router-link>
......
...@@ -197,6 +197,13 @@ useEventListener(window, 'pagehide', () => { ...@@ -197,6 +197,13 @@ useEventListener(window, 'pagehide', () => {
:deep(.el-tabs__nav-wrap::after) { :deep(.el-tabs__nav-wrap::after) {
display: none; display: none;
} }
@media (max-width: 767px) {
:deep(.el-tabs__header) { width: 100%; }
:deep(.el-tabs__nav-wrap),
:deep(.el-tabs__nav-scroll) { overflow-x: auto; overflow-y: hidden; }
:deep(.el-tabs__nav) { display: inline-flex; flex-wrap: nowrap; }
:deep(.el-tabs__item) { flex-shrink: 0; font-size: 13px; padding: 0 8px; }
}
} }
.score-header { .score-header {
display: flex; display: flex;
......
...@@ -311,6 +311,42 @@ watch(currentConnection, () => { ...@@ -311,6 +311,42 @@ watch(currentConnection, () => {
} }
} }
} }
@media (max-width: 767px) {
.info-box {
flex-direction: column;
align-items: stretch;
.info-name {
flex-direction: column;
min-width: auto !important;
margin-bottom: 12px;
}
h1 {
font-size: 22px;
margin: 10px 0 0 0;
}
.line {
display: none;
}
.info-box > div {
flex-direction: column;
gap: 8px;
}
}
.card-box {
flex-direction: column;
height: auto;
.card {
height: auto;
min-height: 300px;
margin-bottom: 10px;
.demo-tabs {
height: auto;
min-height: 200px;
}
}
}
}
.event-box { .event-box {
border-bottom: 1px solid #ccc; border-bottom: 1px solid #ccc;
padding: 20px 0 5px; padding: 20px 0 5px;
......
...@@ -328,3 +328,16 @@ const handleClear = async () => { ...@@ -328,3 +328,16 @@ const handleClear = async () => {
<!-- 用户事件数据 --> <!-- 用户事件数据 -->
<ViewGenerateEventData v-model="generateEventVisible" v-if="generateEventVisible"></ViewGenerateEventData> <ViewGenerateEventData v-model="generateEventVisible" v-if="generateEventVisible"></ViewGenerateEventData>
</template> </template>
<style scoped>
@media (max-width: 767px) {
.el-row[style*="space-between"] {
flex-direction: column;
gap: 8px;
}
.el-space {
flex-wrap: wrap;
gap: 6px;
}
}
</style>
Markdown 格式
0% 或
您添加了 0 人 到此讨论。请谨慎行事。
请先完成此评论的编辑!
请 注册 或者 后发表评论