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

feat: 优化移动端布局与学生操作入口

- 优化导航、筛选抽屉、列表卡片及弹窗表单 - 修复嵌套字段展示和树形表格数据遗漏 - 隐藏学生无权限的操作按钮 - 修复首次访问的实验参数与菜单初始化 - 清理无用菜单配置和重复筛选逻辑
上级 0441efbf
......@@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
<title>数字营销实验室DML</title>
</head>
<body>
......
......@@ -95,12 +95,6 @@ textarea:focus {
padding: 0 16px;
}
/* 移动端弹框宽度 90% */
@media (max-width: 767px) {
.el-overlay-dialog .el-dialog {
width: 90% !important;
}
}
.info .el-form-item {
margin-bottom: 0;
}
......@@ -136,98 +130,167 @@ textarea:focus {
border: 1px solid #e6e6e6;
}
/* ============ 系统级响应式 ============ */
/* ========== 全局响应式 ========== */
/* 移动端弹框宽度 90% */
@media (max-width: 767px) {
.el-overlay-dialog .el-dialog {
width: 90% !important;
}
img,
video {
max-width: 100%;
height: auto;
}
@media (max-width: 767px) {
/* AppCard 移动端内边距缩减 */
.app-card {
padding: 12px !important;
.el-overlay-dialog {
padding: max(12px, env(safe-area-inset-top)) 12px max(12px, env(safe-area-inset-bottom));
box-sizing: border-box;
}
.app-card + .app-card {
margin-top: 12px !important;
.el-dialog {
width: 100% !important;
max-width: 500px;
margin: 0 auto !important;
max-height: calc(100dvh - max(12px, env(safe-area-inset-top)) - max(12px, env(safe-area-inset-bottom)));
display: flex;
flex-direction: column;
}
.app-card-hd__title {
font-size: 16px;
margin-bottom: 16px;
.el-dialog__header,
.el-dialog__footer {
flex-shrink: 0;
}
/* 全局 el-table 横向滚动 */
.el-table {
width: 100%;
.el-dialog__body {
min-height: 0;
overflow-y: auto;
overscroll-behavior: contain;
}
.el-table__body-wrapper {
overflow-x: auto;
-webkit-overflow-scrolling: touch;
.el-dialog .el-form:not(.el-form--inline) .el-form-item {
display: block;
}
/* el-pagination 移动端简化 */
.el-pagination {
.el-dialog .el-form:not(.el-form--inline) .el-form-item__label {
display: block;
width: auto !important;
height: auto;
float: none;
line-height: 24px;
padding-bottom: 4px;
white-space: normal;
text-align: left;
}
.el-dialog .el-form:not(.el-form--inline) .el-form-item__content {
margin-left: 0 !important;
min-width: 0;
}
.el-dialog__footer {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 4px;
font-size: 12px;
justify-content: flex-end;
gap: 8px;
}
.el-pagination__total,
.el-pagination__sizes,
.el-pagination__jump {
display: none !important;
.el-dialog__footer .el-button {
min-height: 40px;
margin-left: 0;
}
.el-pager .number {
min-width: 28px;
height: 28px;
line-height: 28px;
.el-message-box {
width: 90% !important;
max-width: 420px;
}
.el-drawer.ltr,
.el-drawer.rtl {
width: 90% !important;
max-width: 420px;
}
.btn-prev,
.btn-next {
height: 28px;
min-width: 28px;
.el-drawer.mobile-menu-drawer {
width: min(76vw, 280px) !important;
}
.el-input__inner,
.el-textarea__inner {
font-size: 16px;
}
.el-form--inline .el-form-item {
margin-right: 0;
}
.el-table {
font-size: 13px;
}
}
/* el-steps 移动端垂直 */
.el-steps--horizontal {
flex-direction: column;
@media (min-width: 768px) {
.hide-on-desktop {
display: none !important;
}
}
@media (max-width: 767px) {
.el-button.el-button {
--el-button-size: 40px;
min-height: 40px;
font-size: 14px;
box-sizing: border-box;
}
.el-button.el-button.is-circle {
width: 40px;
min-width: 40px;
padding: 8px;
}
.hide-on-mobile {
display: none !important;
}
}
/* 弹框底部按钮组换行 */
.el-dialog__footer {
@media (max-width: 767px) {
.app-main .el-form:not(.el-form--inline) > .el-form-item {
display: block;
}
.app-main .el-form:not(.el-form--inline) > .el-form-item > .el-form-item__label {
width: auto !important;
height: auto;
float: none;
justify-content: flex-start;
line-height: 24px;
padding-bottom: 4px;
white-space: normal;
}
.app-main .el-form:not(.el-form--inline) > .el-form-item > .el-form-item__content {
margin-left: 0 !important;
min-width: 0;
}
.el-form-item__content > .el-input,
.el-form-item__content > .el-select,
.el-form-item__content > .el-select-v2,
.el-form-item__content > .el-cascader,
.el-form-item__content > .el-date-editor,
.el-form-item__content > .el-autocomplete {
max-width: 100%;
}
.el-radio-group, .el-checkbox-group {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 8px;
.el-button {
margin-left: 0 !important;
}
gap: 4px 12px;
max-width: 100%;
}
/* 弹框按钮选中不独占一行,保持自然间距 */
.el-dialog__footer .el-button + .el-button {
margin-left: 0;
.el-radio, .el-checkbox {
min-height: 40px;
height: auto;
margin-right: 0;
max-width: 100%;
}
/* 固定宽度容器防溢出 */
[style*='width: 200px'],
[style*='width: 220px'],
[style*='width: 300px'],
[style*='width: 340px'],
[style*='width: 400px'],
[style*='width: 150px'] {
max-width: 100% !important;
.el-radio__label, .el-checkbox__label {
white-space: normal;
overflow-wrap: anywhere;
line-height: 1.5;
}
/* hover 显示的元素在触摸设备上常显 */
.connect-item__remove,
.connect-item__edit,
.hover-show {
display: block !important;
.el-dialog__footer > .el-row {
flex-wrap: wrap;
gap: 8px;
}
[class*='remove']:hover,
[class*='delete']:hover {
opacity: 1;
.markdown-body {
overflow-wrap: anywhere;
}
.markdown-body pre, .markdown-body table {
display: block;
max-width: 100%;
overflow-x: auto;
}
}
@media (max-width: 767px) {
.el-dialog .el-form { max-width: 100%; }
.el-popover.el-popper { max-width: calc(100vw - 24px); }
.rfm-popover { overflow-x: auto; }
}
......@@ -80,3 +80,25 @@
.rule-input {
width: 100px;
}
@media (max-width: 767px) {
.rule { min-width: 0; }
.rule-list { min-width: 0; }
.rule-operator { min-width: 32px; margin-right: 8px; }
.rule-operator span { width: 32px; height: 40px; line-height: 38px; border-radius: 16px; }
.rule-item { min-width: 0; gap: 8px; }
.rule-item > div:not(.el-form-item), .rule-item > .el-row {
min-width: 0;
max-width: 100%;
flex-wrap: wrap;
gap: 8px;
}
.rule .el-form-item { max-width: 100%; margin-right: 0; margin-bottom: 8px; }
.rule .el-form-item__content { min-width: 0; max-width: 100%; gap: 8px; }
.rule .el-input, .rule .el-select, .rule .el-select-v2,
.rule .el-date-editor, .rule .el-autocomplete, .rule .el-input-number {
max-width: 100%;
}
.rule-tips { align-items: flex-start; line-height: 1.6; overflow-wrap: anywhere; }
.rule-tips .el-icon { flex-shrink: 0; margin-top: 4px; }
}
......@@ -66,6 +66,7 @@ const color = ['#af1c40', '#c17933', '#8f0034', '#d45548', '#ab3259', '#dec34c',
display: flex;
flex-direction: column;
flex: 1;
min-width: 0;
background-color: rgba(234, 234, 234, 0.6);
border-radius: 6px;
overflow: hidden;
......@@ -99,6 +100,7 @@ const color = ['#af1c40', '#c17933', '#8f0034', '#d45548', '#ab3259', '#dec34c',
}
.chart-bd {
flex: 1;
min-width: 0;
background-color: #fff;
margin: 0 20px 20px;
border-radius: 4px;
......
......@@ -466,4 +466,10 @@ const handleSend = () => {
.prompt-tag:active {
transform: translateY(0);
}
@media (max-width: 767px) {
.chat-container { height: min(600px, 72dvh); min-height: 0; }
.chat-messages { min-height: 0; padding: 12px; }
.message-content { max-width: 100%; overflow-wrap: anywhere; }
.chat-input-container { flex-wrap: wrap; gap: 8px; }
}
</style>
......@@ -26,6 +26,7 @@ const styleHeight = computed(() => {
display: flex;
flex-direction: column;
min-height: v-bind(styleHeight);
min-width: 0;
background: #fff;
box-shadow: 0 1px 6px 0 rgb(228 232 235 / 20%);
border-radius: 6px;
......@@ -52,6 +53,7 @@ const styleHeight = computed(() => {
}
.app-card-bd {
flex: 1;
min-width: 0;
overflow-x: hidden;
overflow-y: auto;
}
......@@ -67,4 +69,12 @@ const styleHeight = computed(() => {
align-items: center;
justify-content: space-between;
}
@media (max-width: 767px) {
.app-card { padding: 12px; min-height: auto; }
.app-card + .app-card { margin-top: 12px; }
.app-card-hd { flex-wrap: wrap; gap: 8px; }
.app-card-hd__title { font-size: 16px; line-height: 1.4; margin-bottom: 16px; }
.app-card-hd-aside { max-width: 100%; }
.h2-title { font-size: 16px; line-height: 1.4; flex-wrap: wrap; gap: 8px; }
}
</style>
......@@ -211,4 +211,12 @@ const onAddText = (event: MouseEvent) => {
}
}
}
@media (max-width: 767px) {
.flow { flex-direction: column; height: auto; gap: 12px; }
.flow-main { min-width: 0; height: 70dvh; min-height: 400px; flex: none; }
.flow-main .el-card__body { padding: 12px; }
.flow-main .vue-flow { min-height: 0; }
.flow-main .vue-flow__controls { flex-wrap: wrap; max-width: calc(100% - 20px); margin: 8px; }
}
</style>
......@@ -419,4 +419,7 @@ const onDragStart = (event: DragEvent, data: any) => {
}
}
}
@media (max-width: 767px) {
.flow-sidebar { width: 100%; max-height: 240px; }
}
</style>
<template>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="24" height="24">
<path d="M256 32 16 224l40 50 40-32v206c0 18 14 32 32 32h96V320h64v160h96c18 0 32-14 32-32V242l40 32 40-50L256 32z" />
</svg>
</template>
......@@ -5,9 +5,11 @@ export default { name: 'AppAside' }
<script setup lang="ts">
import type { IMenuItem } from '@/types'
import { useMenuStore } from '@/stores/menu'
import { ArrowLeft, ArrowRight, Menu as MenuIcon } from '@element-plus/icons-vue'
import { ArrowLeft, ArrowRight, Close, Menu as MenuIcon } from '@element-plus/icons-vue'
import IconHome from '@/components/icon/IconHome.vue'
const menus = useMenuStore().menus
const menuStore = useMenuStore()
const menus = computed(() => menuStore.menus)
const route = useRoute()
const isMobile = useMediaQuery('(max-width: 767px)')
......@@ -17,17 +19,21 @@ const mobileMenuVisible = ref(false)
const mobileActiveGroup = ref('')
const currentMenu = computed(() => {
return findMenu(route.fullPath, menus)
return findMenu(route.fullPath, menus.value)
})
const currentSubmenu = computed(() => {
return currentMenu.value?.children ? findMenu(route.fullPath, currentMenu.value.children) : null
})
const mobilePageTitle = computed(() => currentSubmenu.value?.name || currentMenu.value?.name || (route.path === '/' ? '主页' : ''))
watchEffect(() => {
mobileActiveGroup.value = currentMenu.value?.children ? currentMenu.value.path : ''
})
watch(isMobile, () => { mobileMenuVisible.value = false })
watch(
() => route.fullPath,
() => {
......@@ -61,6 +67,9 @@ function toggleCollapse() {
}
function toggleMobileMenu() {
if (!mobileMenuVisible.value) {
mobileActiveGroup.value = currentMenu.value?.children ? currentMenu.value.path : ''
}
mobileMenuVisible.value = !mobileMenuVisible.value
}
......@@ -80,17 +89,35 @@ function menuLinkTo(item: { path: string }) {
<template>
<template v-if="isMobile">
<header class="mobile-nav">
<button class="mobile-nav__trigger" type="button" aria-label="打开导航菜单" :aria-expanded="mobileMenuVisible" @click="toggleMobileMenu">
<el-icon><MenuIcon /></el-icon>
</button>
<h1 v-if="mobilePageTitle" class="mobile-nav__title">{{ mobilePageTitle }}</h1>
</header>
<el-drawer
v-model="mobileMenuVisible"
direction="ltr"
size="84%"
size="85%"
title="导航菜单"
append-to-body
:with-header="false"
class="app-aside-drawer">
<div class="mobile-menu">
<div class="mobile-menu__header">
<el-button circle plain :icon="MenuIcon" @click="toggleMobileMenu" />
<button class="mobile-nav__trigger" type="button" aria-label="关闭导航菜单" @click="mobileMenuVisible = false">
<el-icon><Close /></el-icon>
</button>
</div>
<div class="mobile-menu__body">
<RouterLink
:to="menuLinkTo({ path: '/' })"
class="mobile-menu__link"
:class="{ 'is-active': route.path === '/' }"
@click="mobileMenuVisible = false">
<IconHome class="mobile-menu__link-icon home-menu-icon" />
<span>主页</span>
</RouterLink>
<template v-for="item in menus" :key="item.path">
<RouterLink
v-if="!item.children"
......@@ -98,7 +125,7 @@ function menuLinkTo(item: { path: string }) {
class="mobile-menu__link"
:class="{ 'is-active': item.path === currentMenu?.path }"
v-permission="item.tag"
@click="toggleMobileMenu">
@click="mobileMenuVisible = false">
<component :is="item.icon" class="mobile-menu__link-icon" v-if="item.icon" />
<span>{{ item.name }}</span>
</RouterLink>
......@@ -110,7 +137,6 @@ function menuLinkTo(item: { path: string }) {
<component :is="item.icon" class="mobile-menu__link-icon" v-if="item.icon" />
<span>{{ item.name }}</span>
</div>
<span class="mobile-menu__group-count">{{ item.children.length }} 项</span>
</div>
</template>
<div class="mobile-menu__children">
......@@ -121,7 +147,7 @@ function menuLinkTo(item: { path: string }) {
class="mobile-menu__sublink"
:class="{ 'is-active': submenu.path === currentSubmenu?.path }"
v-permission="submenu.tag"
@click="toggleMobileMenu">
@click="mobileMenuVisible = false">
<component :is="submenu.icon" class="mobile-menu__sublink-icon" v-if="submenu.icon"></component>
<span>{{ submenu.name }}</span>
</RouterLink>
......@@ -132,15 +158,20 @@ function menuLinkTo(item: { path: string }) {
</div>
</div>
</el-drawer>
<button class="mobile-menu-float-btn" type="button" @click="toggleMobileMenu">
<el-icon><ArrowRight /></el-icon>
</button>
</template>
<aside v-else class="app-aside" :class="{ 'is-collapsed': collapsed }">
<nav class="menu">
<ul>
<li :class="{ 'is-active': route.path === '/' }">
<div class="menu-item">
<el-tooltip content="主页" placement="right">
<RouterLink :to="menuLinkTo({ path: '/' })" aria-label="主页">
<IconHome class="menu-icon home-menu-icon" />
</RouterLink>
</el-tooltip>
</div>
</li>
<li
v-for="item in menus"
:key="item.path"
......@@ -344,9 +375,7 @@ function menuLinkTo(item: { path: string }) {
}
.app-aside-drawer {
.el-drawer {
background: #ecf2f7;
}
background: #ecf2f7;
.el-drawer__body {
padding: 0;
}
......@@ -362,14 +391,18 @@ function menuLinkTo(item: { path: string }) {
display: flex;
align-items: center;
justify-content: flex-end;
padding: 14px 18px 10px;
gap: 8px;
padding: 10px 16px;
flex-shrink: 0;
background: #fff;
border-bottom: 1px solid #e5e5e5;
}
.mobile-menu__body {
flex: 1;
min-height: 0;
overflow-y: auto;
overscroll-behavior: contain;
padding: 14px;
}
......@@ -388,8 +421,9 @@ function menuLinkTo(item: { path: string }) {
color: #333;
border: 1px solid #e5e5e5;
&.is-active {
background: var(--main-color);
color: #fff;
background: rgba(186, 20, 62, 0.06);
color: var(--main-color);
font-weight: 600;
border-color: var(--main-color);
.mobile-menu__link-icon {
fill: currentColor;
......@@ -398,9 +432,21 @@ function menuLinkTo(item: { path: string }) {
}
}
.home-menu-icon {
fill: currentColor;
}
.app-aside .home-menu-icon {
color: var(--main-color);
}
.app-aside .menu > ul > li.is-active .home-menu-icon {
color: #fff;
}
.mobile-menu__link-icon,
.mobile-menu__sublink-icon {
width: 18px;
height: 18px;
fill: currentColor;
flex-shrink: 0;
}
......@@ -444,10 +490,7 @@ function menuLinkTo(item: { path: string }) {
font-weight: 600;
}
.mobile-menu__group-count {
font-size: 12px;
color: #999;
}
.mobile-menu__children {
display: flex;
......@@ -473,21 +516,48 @@ function menuLinkTo(item: { path: string }) {
}
}
.mobile-menu-float-btn {
position: fixed;
left: 0;
bottom: 24px;
width: 32px;
height: 52px;
.mobile-nav {
position: sticky;
top: 0;
z-index: 100;
display: flex;
align-items: center;
gap: 8px;
min-height: 52px;
flex-shrink: 0;
padding: env(safe-area-inset-top) 12px 0;
background: #fff;
border-bottom: 1px solid #e6e6e6;
}
.mobile-nav__trigger {
flex-shrink: 0;
width: 44px;
height: 44px;
display: inline-flex;
align-items: center;
justify-content: center;
border: 1px solid #e0e0e0;
border-left: none;
border-radius: 0 18px 18px 0;
background: #fff;
border-radius: 8px;
background: transparent;
color: var(--main-color);
box-shadow: 2px 0 10px rgba(0, 0, 0, 0.08);
z-index: 2000;
font-size: 22px;
cursor: pointer;
&:hover { background: var(--el-fill-color-light); }
&:focus-visible { outline: 2px solid var(--main-color); outline-offset: 2px; }
}
.mobile-nav__title {
min-width: 0;
font-size: 16px;
font-weight: 600;
line-height: 24px;
overflow-wrap: anywhere;
padding: 10px 0;
}
@media (max-width: 767px) {
.app-aside-drawer.el-drawer { width: min(85vw, 340px) !important; }
.mobile-menu__header { padding-top: max(14px, env(safe-area-inset-top)); }
.mobile-menu__body { padding-bottom: max(14px, env(safe-area-inset-bottom)); }
.mobile-menu__link, .mobile-menu__sublink { min-height: 44px; overflow-wrap: anywhere; }
.mobile-menu__group-main { min-width: 0; }
}
</style>
......@@ -22,4 +22,7 @@ export default { name: 'AppFooter' }
margin: 10px;
border-radius: 6px;
}
@media (max-width: 767px) {
.app-footer { height: auto; min-height: 50px; padding: 12px; flex-direction: column; gap: 8px; text-align: center; line-height: 1.6; overflow-wrap: anywhere; }
}
</style>
......@@ -32,8 +32,12 @@ withDefaults(defineProps<Props>(), {
}
.app-layout-container {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
overflow: hidden;
}
@media (max-width: 767px) {
.app-layout { flex-direction: column; min-height: 100dvh; }
}
</style>
......@@ -20,6 +20,7 @@ const padding = computed(() => {
<style>
.app-main {
flex: 1;
min-width: 0;
padding: v-bind(padding);
overflow: hidden;
box-sizing: border-box;
......
......@@ -13,6 +13,7 @@ const { buttonText = '添加条件' } = defineProps<{
const { tagList } = useTag()
const { rfmResList } = useRfmRes()
const isMobile = useMediaQuery('(max-width: 767px)')
// 获取逻辑运算符名称
function getLogicalName(value: 'and' | 'or') {
......@@ -102,7 +103,7 @@ function handleRfmChange(rfmKey: string, item: any) {
</div>
</el-option>
</el-select>
<el-popover popper-class="rfm-popover" placement="top" :width="800" trigger="hover">
<el-popover popper-class="rfm-popover" placement="top" :width="800" :trigger="isMobile ? 'click' : 'hover'">
<el-table :data="rfmResList" border stripe>
<el-table-column prop="frm_extend_info.group" label="组合" width="70" />
<el-table-column prop="frm_extend_info.r" label="R值" width="52" />
......
......@@ -330,4 +330,10 @@ const defaultOptions = Array.from({ length: 5 }).map((_, index) => ({ value: ind
display: flex;
align-items: center;
}
@media (max-width: 767px) {
.rfm-top, .rfm-header, .rfm-header-extra { flex-wrap: wrap; gap: 8px; min-width: 0; }
.rfm-body { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px; }
.rfm-box { min-width: 0; }
.rfm-header-extra > .el-input { max-width: 100%; }
}
</style>
......@@ -107,4 +107,10 @@ watch(
}
}
}
@media (max-width: 767px) {
.user-label { height: auto; min-height: 160px; margin: 16px 0; background: none; }
.user-label ul { width: 100%; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; }
.user-label li { scale: 1 !important; text-align: center; min-width: 0; }
.user-label .user-label__inner { border-radius: 8px; max-width: 100%; overflow-wrap: anywhere; }
}
</style>
......@@ -115,15 +115,15 @@ const handleStudentFollow = function () {
<span>自动生成用户数据</span>
</li>
<li @click.stop="generateEventData">
<el-icon size="16" color="#000" @click.stop="edit"><Avatar /></el-icon>
<el-icon size="16" color="#000"><Avatar /></el-icon>
<span>自动生成事件数据</span>
</li>
<li @click.stop="viewDataProgress">
<el-icon size="16" color="#000" @click.stop="edit"><PieChart /></el-icon>
<el-icon size="16" color="#000"><PieChart /></el-icon>
<span>数据生成进度</span>
</li>
<li @click.stop="handleRemove" v-if="userStore.role?.id !== 1">
<el-icon size="16" color="#000" @click.stop="edit"><Delete /></el-icon>
<el-icon size="16" color="#000"><Delete /></el-icon>
<span>删除</span>
</li>
</ul>
......
......@@ -26,7 +26,7 @@ const listOptions = computed(() => {
params: listParams,
beforeRequest(params: any, isReset: boolean) {
if (isReset) listParams.updated_operator = ''
params.updated_operator = listParams.updated_operator
return params
}
},
......@@ -145,8 +145,8 @@ function handleView(row: Group) {
>
</el-space>
</template>
<template #filter-user>
<SelectUser v-model="listParams.updated_operator" placeholder="更新人" @change="handleRefresh"></SelectUser>
<template #filter-user="{ params, onChange }">
<SelectUser v-model="params.updated_operator" placeholder="更新人" @change="onChange"></SelectUser>
</template>
<template #table-x="{ row }">
......
......@@ -288,7 +288,7 @@ onMounted(() => {
</AppCard>
</div>
</div>
<div style="width: 340px">
<div class="home-user-tracking">
<AppCard class="card" title="最近活跃用户跟踪">
<div class="content-user">
<template v-for="(item, index) in userList" :key="item.id">
......@@ -359,6 +359,7 @@ onMounted(() => {
margin-top: 10px;
}
.col {
min-width: 0;
flex: 1;
display: flex;
flex-direction: column;
......@@ -383,7 +384,11 @@ onMounted(() => {
background-color: #fff;
}
.home-user-tracking { width: 340px; flex-shrink: 0; }
@media (max-width: 767px) {
.home-user-tracking { width: 100%; }
:deep(.chart-card) { min-width: 0; width: 100%; }
> .row {
flex-direction: column;
}
......@@ -394,6 +399,7 @@ onMounted(() => {
.row {
flex-wrap: wrap;
.card {
min-width: 0;
flex: 0 0 calc(50% - 5px);
p {
line-height: 60px;
......@@ -412,7 +418,8 @@ onMounted(() => {
display: flex;
flex-wrap: wrap;
.content-user_item {
width: 50px;
flex: 0 0 60px;
width: 60px;
padding: 10px;
border-radius: 10px;
cursor: pointer;
......@@ -423,6 +430,7 @@ onMounted(() => {
img {
width: 40px;
height: 40px;
object-fit: contain;
margin: 0 auto;
display: block;
}
......
......@@ -34,8 +34,6 @@ const listOptions = computed(() => {
if (isReset) {
listParams.updated_operator = ''
listParams.type_id = ''
} else {
params.updated_operator = listParams.updated_operator
}
return params
},
......@@ -171,7 +169,7 @@ const handleRemoves = async function () {
<div class="label-left"><LabelType :active-id="listParams.type_id" @select="handleSelect"></LabelType></div>
<AppList v-bind="listOptions" ref="appList" class="label-right" @selection-change="handleSelectionChange">
<template #header-buttons>
<div style="display: flex; justify-content: space-between">
<div class="label-toolbar">
<div>
<el-button type="primary" :icon="Plus" @click="handleAdd" v-if="!userStore.status.tag_status"
>新建</el-button
......@@ -189,8 +187,8 @@ const handleRemoves = async function () {
<el-button type="primary" @click="aiDialogVisible = true">AI建议</el-button>
</div>
</template>
<template #filter-user>
<SelectUser v-model="listParams.updated_operator" placeholder="更新人" @change="handleRefresh"></SelectUser>
<template #filter-user="{ params, onChange }">
<SelectUser v-model="params.updated_operator" placeholder="更新人" @change="onChange"></SelectUser>
</template>
<template #table-x="{ row }">
......@@ -224,6 +222,13 @@ const handleRemoves = async function () {
height: calc(100vh - 130px);
display: flex;
}
.label-toolbar {
flex: 1;
min-width: 0;
display: flex;
justify-content: space-between;
gap: 8px;
}
.label-left {
padding-right: 20px;
margin-right: 20px;
......
......@@ -3,10 +3,6 @@ import LiveProductCategory from '@/components/LiveProductCategory.vue'
import { getOrderList } from '../api'
const appList = ref(null)
// 刷新
const handleRefresh = () => {
appList.value?.refetch()
}
const listParams = reactive({ name: '', live_commodity_type_id: '', live_commodity_title: '' })
// 列表配置
......@@ -17,7 +13,7 @@ const listOptions = computed(() => {
params: listParams,
beforeRequest(params, isReset) {
if (isReset) listParams.live_commodity_type_id = ''
params.live_commodity_type_id = listParams.live_commodity_type_id
return params
},
},
......@@ -65,10 +61,10 @@ const listOptions = computed(() => {
<template>
<AppCard title="订单管理">
<AppList v-bind="listOptions" ref="appList">
<template #filter-category>
<template #filter-category="{ params, onChange }">
<LiveProductCategory
v-model="listParams.live_commodity_type_id"
@change="handleRefresh"
v-model="params.live_commodity_type_id"
@change="onChange"
placeholder="请选择直播主题品类"></LiveProductCategory>
</template>
</AppList>
......
......@@ -25,7 +25,7 @@ const listOptions = computed(() => {
params: listParams,
beforeRequest(params, isReset) {
if (isReset) listParams.live_commodity_type_id = ''
params.live_commodity_type_id = listParams.live_commodity_type_id
return params
},
},
......@@ -89,10 +89,10 @@ const handleRemove = async (row) => {
<AppCard title="商品属性管理">
<AppList v-bind="listOptions" ref="appList">
<template #header-buttons><el-button type="primary" @click="handleAdd">新增商品属性</el-button></template>
<template #filter-category>
<template #filter-category="{ params, onChange }">
<LiveProductCategory
v-model="listParams.live_commodity_type_id"
@change="handleRefresh"
v-model="params.live_commodity_type_id"
@change="onChange"
placeholder="请选择商品品类"></LiveProductCategory>
</template>
<template #table-x="{ row }">
......
......@@ -326,21 +326,21 @@ function batchSetReferencePrice() {
<span>{{ row.specs ? row.specs.join(' / ') : '-' }}</span>
</template>
</el-table-column>
<el-table-column prop="price" label="价格" align="center" column-key="price">
<el-table-column prop="price" label="价格" min-width="150" align="center" column-key="price">
<template #default="{ row }">
<el-input v-model="row.price" placeholder="请输入价格" type="number">
<template #suffix>¥</template>
</el-input>
</template>
</el-table-column>
<el-table-column prop="reference_price" label="参考价" align="center" column-key="reference_price">
<el-table-column prop="reference_price" label="参考价" min-width="150" align="center" column-key="reference_price">
<template #default="{ row }">
<el-input v-model="row.reference_price" placeholder="请输入参考价" type="number">
<template #suffix>¥</template>
</el-input>
</template>
</el-table-column>
<el-table-column prop="stock" label="库存" align="center" column-key="stock">
<el-table-column prop="stock" label="库存" min-width="150" align="center" column-key="stock">
<template #default="{ row }">
<el-input v-model.number="row.stock" placeholder="请输入库存" type="number">
<template #suffix>件</template>
......@@ -421,15 +421,19 @@ function batchSetReferencePrice() {
}
.sku-table {
width: 100%;
min-width: 0;
}
@media (max-width: 768px) {
@media (max-width: 767px) {
.spec-list {
width: 100%;
flex-direction: column;
}
.spec-item {
min-width: 100%;
min-width: 0;
width: 100%;
padding: 12px;
}
.batch-set-container {
......
......@@ -5,10 +5,6 @@ import { getProductList, deleteProduct } from '../api'
import { getOperatorName } from '@/utils/utils'
const appList = ref(null)
// 刷新
const handleRefresh = () => {
appList.value?.refetch()
}
const listParams = reactive({ name: '', live_commodity_type_id: '' })
......@@ -20,7 +16,7 @@ const listOptions = computed(() => {
params: listParams,
beforeRequest(params, isReset) {
if (isReset) listParams.live_commodity_type_id = ''
params.live_commodity_type_id = listParams.live_commodity_type_id
return params
},
callback(data) {
......@@ -64,8 +60,8 @@ const handleRemove = async (row) => {
<template #header-buttons>
<el-button type="primary"><router-link to="management/add">添加商品</router-link></el-button>
</template>
<template #filter-category>
<LiveProductCategory v-model="listParams.live_commodity_type_id" @change="handleRefresh" placeholder="请选择商品品类"></LiveProductCategory>
<template #filter-category="{ params, onChange }">
<LiveProductCategory v-model="params.live_commodity_type_id" @change="onChange" placeholder="请选择商品品类"></LiveProductCategory>
</template>
<template #table-picture="{ row }">
<el-image fit="cover" preview-teleported :src="row.picture_url" :previewSrcList="row.picture_url_list" style="width:100px;height:100px"></el-image>
......
......@@ -20,7 +20,7 @@ const listOptions = computed(() => {
params: listParams,
beforeRequest(params, isReset) {
if (isReset) listParams.live_commodity_type_id = ''
params.live_commodity_type_id = listParams.live_commodity_type_id
return params
},
},
......@@ -74,8 +74,8 @@ const handleRemove = async (row) => {
<template #header-buttons>
<el-button type="primary" @click="handleAdd">上传总结报告</el-button>
</template>
<template #filter-category>
<LiveProductCategory v-model="listParams.live_commodity_type_id" @change="handleRefresh" placeholder="请选择直播主题品类"></LiveProductCategory>
<template #filter-category="{ params, onChange }">
<LiveProductCategory v-model="params.live_commodity_type_id" @change="onChange" placeholder="请选择直播主题品类"></LiveProductCategory>
</template>
<template #table-x="{ row }">
<el-button text type="primary" @click="handleView(row)">查看</el-button>
......
......@@ -22,7 +22,7 @@ const listOptions = computed(() => {
params: listParams,
beforeRequest(params, isReset) {
if (isReset) listParams.live_commodity_type_id = ''
params.live_commodity_type_id = listParams.live_commodity_type_id
return params
},
callback(data) {
......@@ -89,10 +89,10 @@ const handleRemove = async (row) => {
<template #header-buttons>
<el-button type="primary" @click="handleAdd">添加话术</el-button>
</template>
<template #filter-category>
<template #filter-category="{ params, onChange }">
<LiveProductCategory
v-model="listParams.live_commodity_type_id"
@change="handleRefresh"
v-model="params.live_commodity_type_id"
@change="onChange"
placeholder="请选择直播主题品类"></LiveProductCategory>
</template>
<template #table-x="{ row }">
......
......@@ -28,7 +28,7 @@ const listOptions = computed(() => {
params: listParams,
beforeRequest(params, isReset) {
if (isReset) listParams.live_commodity_type_id = ''
params.live_commodity_type_id = listParams.live_commodity_type_id
return params
},
},
......@@ -82,10 +82,10 @@ const handleStart = async (row) => {
<template #header-buttons>
<el-button type="primary" @click="formVisible = true">添加直播</el-button>
</template>
<template #filter-category>
<template #filter-category="{ params, onChange }">
<LiveProductCategory
v-model="listParams.live_commodity_type_id"
@change="handleRefresh"
v-model="params.live_commodity_type_id"
@change="onChange"
placeholder="请选择直播主题品类"></LiveProductCategory>
</template>
<template #table-x="{ row }">
......
......@@ -378,4 +378,12 @@ defineExpose({ generateImage, generatePdf })
}
}
}
@media (max-width: 767px) {
.market-report { padding: 0 12px; overflow-wrap: anywhere; }
.market-report .market-report-header { padding: 20px 0; margin-bottom: 20px; }
.market-report .market-report-header ul { margin-top: 16px; flex-wrap: wrap; gap: 8px 16px; }
.market-report h2 { font-size: 18px; }
.market-report p { margin-left: 0; }
.market-report .market-report-step { display: none; }
}
</style>
......@@ -345,4 +345,12 @@ defineExpose({ generateImage, generatePdf })
}
}
}
@media (max-width: 767px) {
.market-report { padding: 0 12px; overflow-wrap: anywhere; }
.market-report .market-report-header { padding: 20px 0; margin-bottom: 20px; }
.market-report .market-report-header ul { margin-top: 16px; flex-wrap: wrap; gap: 8px 16px; }
.market-report h2 { font-size: 18px; }
.market-report p { margin-left: 0; }
.market-report .market-report-step { display: none; }
}
</style>
......@@ -365,4 +365,11 @@ async function handleSave(message) {
font-weight: bold;
color: var(--main-color);
}
@media (max-width: 767px) {
.chat { height: auto; max-height: 55dvh; min-height: 180px; }
.chat-footer { margin-top: 16px; }
.chat-message { margin: 16px 0; gap: 8px; }
.chat-message-main { min-width: 0; padding: 12px; }
.chat-message-avatar { flex-basis: 32px; height: 32px; }
}
</style>
......@@ -79,4 +79,8 @@ function onClose() {
width: 100%;
}
}
@media (max-width: 767px) {
.image-design ul { margin: 16px 0; gap: 12px; }
.image-design li { width: calc(50% - 6px); min-width: 0; border-radius: 12px; }
}
</style>
......@@ -167,4 +167,8 @@ async function handleNext() {
margin-bottom: 0;
}
}
@media (max-width: 767px) {
.info-form { flex-direction: column; gap: 12px; }
.info-form .el-form-item { width: 100%; }
}
</style>
......@@ -127,4 +127,10 @@ async function handleChatSave(url: string) {
background-color: rgba(242, 242, 242, 0.49);
}
}
@media (max-width: 767px) {
.material-tabs .el-tabs__header { width: 100%; max-width: 300px; }
.material .el-card__body { padding: 12px; }
.material .el-row { gap: 8px; }
.material .el-row > .el-button { margin-left: 0; }
}
</style>
......@@ -5,9 +5,12 @@ import { ElMessageBox, ElMessage } from 'element-plus'
import AppList from '@/components/base/AppList.vue'
import { getMemberMeta, deleteMemberMeta } from '../api'
import { useMapStore } from '@/stores/map'
import { useUserStore } from '@/stores/user'
import { getNameByValue } from '@/utils/dictionary'
const store = useMapStore()
const userStore = useUserStore()
const canManage = computed(() => userStore.role?.id !== 1)
const FormDialog = defineAsyncComponent(() => import('../components/FormDialog.vue'))
const ViewDialog = defineAsyncComponent(() => import('../components/ViewDialog.vue'))
......@@ -110,14 +113,14 @@ function handleRemove(row: UserProp) {
<AppCard>
<AppList v-bind="listOptions" ref="appList">
<template #header-buttons>
<el-space>
<el-space v-if="canManage">
<el-button type="primary" :icon="Plus" @click="handleAdd">新建</el-button>
</el-space>
</template>
<template #table-x="{ row }">
<el-button type="primary" plain @click="handleView(row)">查看</el-button>
<el-button type="primary" plain @click="handleUpdate(row)" v-if="row.id !== '10000'">编辑</el-button>
<el-button type="primary" plain @click="handleRemove(row)" v-if="row.id !== '10000'">删除</el-button>
<el-button type="primary" plain @click="handleUpdate(row)" v-if="canManage && row.id !== '10000'">编辑</el-button>
<el-button type="primary" plain @click="handleRemove(row)" v-if="canManage && row.id !== '10000'">删除</el-button>
</template>
</AppList>
</AppCard>
......
......@@ -99,10 +99,10 @@ useIntervalFn(() => {
</el-row>
</template>
<template #filter-category>
<template #filter-category="{ params, onChange }">
<LiveProductCategory
v-model="listParams.live_commodity_type_id"
@change="handleRefresh"
v-model="params.live_commodity_type_id"
@change="onChange"
placeholder="请选择直播主题品类"></LiveProductCategory>
</template>
......
......@@ -30,8 +30,6 @@ const listOptions = computed(() => {
if (isReset) {
listParams.updated_operator = ''
listParams.type_id = ''
} else {
params.updated_operator = listParams.updated_operator
}
return params
},
......@@ -169,8 +167,8 @@ const handleRemoves = async function () {
</div>
</div>
</template>
<template #filter-user>
<SelectUser v-model="listParams.updated_operator" placeholder="更新人" @change="handleRefresh"></SelectUser>
<template #filter-user="{ params, onChange }">
<SelectUser v-model="params.updated_operator" placeholder="更新人" @change="onChange"></SelectUser>
</template>
<template #table-x="{ row }">
......
......@@ -3,6 +3,8 @@ import type { Trip } from '../types'
import TripFlow from '@/components/flow/Index.vue'
import { getStudentTrip } from '../api'
const isMobile = useMediaQuery('(max-width: 767px)')
const detail = ref<Trip>()
const elements = ref([])
......@@ -24,7 +26,7 @@ const commentVisible = ref(false)
<template>
<AppCard>
<el-descriptions :column="4" v-if="detail">
<el-descriptions :column="isMobile ? 1 : 4" v-if="detail">
<el-descriptions-item label="课程名称:">{{ detail.course_name }}</el-descriptions-item>
<el-descriptions-item label="实验名称:">{{ detail.experiment_name }}</el-descriptions-item>
<el-descriptions-item label="用户旅程名称:">{{ detail.experiment_itinerary_name }}</el-descriptions-item>
......
......@@ -5,6 +5,8 @@ import TripFlow from '@/components/flow/Index.vue'
import { getStudentTripRecord, submitStudentTripRecordComment, checkStudentTripRecord } from '../api'
import { getNameByValue, tripTemplateTypeList } from '@/utils/dictionary'
const isMobile = useMediaQuery('(max-width: 767px)')
const detail = ref()
const elements = ref([])
......@@ -58,7 +60,7 @@ function handleSubmit() {
</script>
<template>
<AppCard v-if="detail">
<el-descriptions :column="4">
<el-descriptions :column="isMobile ? 1 : 4">
<el-descriptions-item label="课程名称:">{{ detail.course.name }}</el-descriptions-item>
<el-descriptions-item label="实验名称:">{{ detail.experiment.name }}</el-descriptions-item>
<el-descriptions-item label="用户旅程名称:">{{ detail.itinerary.name }}</el-descriptions-item>
......
......@@ -108,7 +108,7 @@ const submitForm = (formEl: FormInstance | undefined) => {
<el-form
:disabled="props.data?.isView"
ref="ruleFormRef"
style="width: 400px; margin: 0 auto; padding: 0 30px 30px 0"
class="user-update-form"
:model="form"
:rules="rules"
label-suffix=":"
......@@ -172,3 +172,10 @@ const submitForm = (formEl: FormInstance | undefined) => {
margin-bottom: 20px;
}
</style>
<style scoped>
.user-update-form { width: 400px; max-width: 100%; margin: 0 auto; padding: 0 30px 30px 0; }
@media (max-width: 767px) {
.user-update-form { width: 100%; padding: 0; }
}
</style>
......@@ -65,7 +65,7 @@ const refetch = function () {
@closed="$emit('update:modelValue', false)">
<AppList v-bind="listOptions" ref="appList">
<template #table-x>
<el-button type="primary" plain @click="refetch" v-permission="'v1-experiment-member-delete'">刷新</el-button>
<el-button type="primary" plain @click="refetch">刷新</el-button>
</template>
</AppList>
</el-dialog>
......
......@@ -5,6 +5,9 @@ import AppList from '@/components/base/AppList.vue'
import UpdateEventsDialog from '../components/UpdateEventsDialog.vue'
import { getMemberEventList, deleteEvent } from '../api'
import type { MemberProp, AttributesProp } from '../types'
import { useUserStore } from '@/stores/user'
const userStore = useUserStore()
const route = useRoute()
......@@ -85,12 +88,12 @@ const handleView = function (row: AttributesProp) {
<AppCard>
<AppList v-bind="listOptions" ref="appList">
<template #header-buttons>
<el-button type="primary" :icon="Plus" @click="handleAdd">新建用户事件</el-button>
<el-button v-if="!userStore.status.status" type="primary" :icon="Plus" @click="handleAdd">新建用户事件</el-button>
</template>
<template #table-x="{ row }">
<el-button type="primary" plain @click="handleView(row)">查看</el-button>
<el-button type="primary" plain @click="handleEdit(row)">编辑</el-button>
<el-button type="primary" plain @click="handleRemove(row)">删除</el-button>
<el-button v-if="!userStore.status.status" type="primary" plain @click="handleEdit(row)">编辑</el-button>
<el-button v-if="!userStore.status.status" type="primary" plain @click="handleRemove(row)">删除</el-button>
</template>
</AppList>
</AppCard>
......
......@@ -239,17 +239,16 @@ const handleClear = async () => {
</script>
<template>
<AppCard>
<AppCard class="user-list-page">
<AppList v-bind="listOptions" ref="appList" @selection-change="handleSelectionChange">
<template #header-buttons>
<el-row justify="space-between">
<el-space>
<div class="user-toolbar">
<div class="user-toolbar-group">
<el-button
v-if="!userStore.status.status"
type="primary"
:icon="Plus"
@click="handleAdd"
v-permission="'v1-experiment-member-create'"
>新建</el-button
>
<el-button v-if="userStore.status.status" type="primary" @click="handleSync">同步用户和事件数据</el-button>
......@@ -265,7 +264,6 @@ const handleClear = async () => {
</el-dropdown-menu>
</template>
</el-dropdown>
<!-- v-permission="['v1-experiment-member-member-upload', 'v1-experiment-member-event-upload']" -->
<el-dropdown v-if="!userStore.status.status">
<el-button type="primary" :icon="Upload">导入</el-button>
<template #dropdown>
......@@ -284,7 +282,7 @@ const handleClear = async () => {
>
<!-- <el-button type="danger" plain :icon="Delete" :disabled="!multipleSelection.length" @click="handleRemoves()" v-permission="'v1-experiment-member-delete'">删除</el-button> -->
<!-- v-permission="'v1-experiment-member-delete'" -->
<el-dropdown>
<el-dropdown v-if="!userStore.status.status">
<el-button type="danger" :icon="Delete">删除</el-button>
<template #dropdown>
<el-dropdown-menu>
......@@ -296,21 +294,21 @@ const handleClear = async () => {
</template>
</el-dropdown>
<el-button type="danger" @click="handleClear" v-if="userStore.role?.id == 1">清空数据</el-button>
</el-space>
<el-space>
</div>
<div class="user-toolbar-group">
<router-link to="/analyze/user"><el-button type="primary">用户分析</el-button></router-link>
<el-button type="primary" @click="generateEventVisible = true">用户事件数据</el-button>
<ListColumnOptions :fields="defaultFields" />
</el-space>
</el-row>
<el-button v-if="!userStore.status.status" type="primary" @click="generateEventVisible = true">用户事件数据</el-button>
<div class="user-column-options"><ListColumnOptions :fields="defaultFields" /></div>
</div>
</div>
</template>
<template #table-x="{ row }">
<el-button type="primary" plain @click="handleImage(row)">画像</el-button>
<el-button type="primary" plain @click="handleView(row)">查看</el-button>
<el-button type="primary" plain @click="handleEdit(row)" v-permission="'v1-experiment-member-update'"
<el-button v-if="!userStore.status.status" type="primary" plain @click="handleEdit(row)" v-permission="'v1-experiment-member-update'"
>编辑</el-button
>
<el-button type="primary" plain @click="handleRemove(row)" v-permission="'v1-experiment-member-delete'"
<el-button v-if="!userStore.status.status" type="primary" plain @click="handleRemove(row)" v-permission="'v1-experiment-member-delete'"
>删除</el-button
>
<el-button type="primary" plain @click="goPage(row)">事件</el-button>
......@@ -330,14 +328,64 @@ const handleClear = async () => {
</template>
<style scoped>
.user-toolbar {
width: 100%;
display: flex;
justify-content: space-between;
flex-wrap: wrap;
gap: 12px;
}
.user-toolbar-group {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 8px;
}
.user-toolbar-group > .el-button {
margin-left: 0;
}
@media (max-width: 767px) {
.el-row[style*="space-between"] {
.user-toolbar {
flex-direction: column;
gap: 12px;
}
.user-toolbar-group {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 8px;
width: 100%;
}
.user-toolbar-group > * {
min-width: 0;
}
.user-toolbar-group :deep(.el-button) {
width: 100%;
min-height: 40px;
height: auto;
margin-left: 0;
padding: 10px 8px;
line-height: 20px;
white-space: normal;
}
.user-toolbar-group :deep(.el-button > span) {
min-width: 0;
white-space: normal;
}
.user-toolbar-group > .el-dropdown {
width: 100%;
}
.user-column-options {
grid-column: 1 / -1;
justify-self: end;
}
.user-list-page :deep(.mobile-card-ft) {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 4px;
}
.el-space {
flex-wrap: wrap;
gap: 6px;
.user-list-page :deep(.mobile-card-ft .el-button) {
width: 100%;
margin-left: 0;
}
}
</style>
import { createRouter, createWebHistory } from 'vue-router'
import { useUserStore } from '@/stores/user'
import { setRequestContext } from '@/utils/requestContext'
const router = createRouter({
history: createWebHistory(),
......@@ -7,6 +8,21 @@ const router = createRouter({
})
router.beforeEach(async (to, from, next) => {
if (!to.query.experiment_id) {
next({
path: to.path,
hash: to.hash,
replace: true,
query: {
...to.query,
experiment_id: from.query.experiment_id || '7028276368903241728',
student_id: to.query.student_id ?? from.query.student_id,
force_tgc: to.query.force_tgc ?? from.query.force_tgc,
},
})
return
}
setRequestContext(to.fullPath)
const whiteList = ['/401', '/one/live/test/view']
const user = useUserStore()
if (!user.isLogin && !whiteList.includes(to.path)) {
......@@ -16,23 +32,11 @@ router.beforeEach(async (to, from, next) => {
console.error(e)
}
if (!user.isLogin) {
next('/401')
next({ path: '/401', query: to.query, replace: true })
return
}
}
if (!to.query.experiment_id) {
next({
path: to.path,
query: {
...to.query,
experiment_id: from.query.experiment_id || '7028276368903241728',
student_id: from.query.student_id,
force_tgc: from.query.force_tgc,
},
})
} else {
next()
}
next()
})
export default router
......@@ -43,166 +43,10 @@ import {
} from '@remixicon/vue'
interface State {
studentMenus: IMenuItem[]
adminMenus: IMenuItem[]
baseMenus: IMenuItem[]
}
// 学生菜单
const studentMenus: IMenuItem[] = [
{
id: 1,
name: '基础配置',
path: '/connect',
icon: markRaw(IconMetadata),
children: [
{
id: 2,
name: '连接管理',
path: '/connect',
icon: markRaw(IconConnect),
},
],
},
{
id: 5,
name: '营销策划',
path: '/market/my',
icon: markRaw(IconMarket),
},
{
id: 6,
name: '用户画像',
path: '/user',
icon: markRaw(IconUser),
},
{
id: 7,
name: '用户识别',
path: '/label',
icon: markRaw(IconGroup2),
children: [
{
id: 8,
name: '标签管理',
path: '/label',
icon: markRaw(IconLabel),
},
{
id: 9,
name: '群组管理',
path: '/group',
icon: markRaw(IconGroup),
},
],
},
{
id: 10,
name: '营销内容设计',
path: '/material',
icon: markRaw(IconMaterial),
children: [
{
id: 11,
name: '文本资料管理',
path: '/material?type=1',
icon: markRaw(IconText),
// tag: 'v1-experiment-marketing-material-list'
tag: '',
},
{
id: 12,
name: '图片资料管理',
path: '/material?type=2',
icon: markRaw(IconImage),
// tag: 'v1-experiment-marketing-material-list'
tag: '',
},
{
id: 13,
name: '卡券资料管理',
path: '/material?type=8',
icon: markRaw(IconCard),
// tag: 'v1-experiment-marketing-material-list'
tag: '',
},
{
id: 17,
name: '语音资料管理',
path: '/material?type=3',
icon: markRaw(IconAudio),
// tag: 'v1-experiment-marketing-material-list'
tag: '',
},
{
id: 14,
name: '视频资料管理',
path: '/material?type=4',
icon: markRaw(IconVideo),
// tag: 'v1-experiment-marketing-material-list'
tag: '',
},
{
id: 15,
name: 'H5资料管理',
path: '/material?type=5',
icon: markRaw(IconH5),
// tag: 'v1-experiment-marketing-material-list',
},
{
id: 16,
name: '二维码资料管理',
path: '/material?type=6',
icon: markRaw(IconQrcode),
// tag: 'v1-experiment-marketing-material-list'
tag: '',
},
{
id: 18,
name: '小程序资料管理',
path: '/material?type=7',
icon: markRaw(IconMiniProgram),
// tag: 'v1-experiment-marketing-material-list'
tag: '',
},
],
},
{
id: 19,
name: '自动化营销',
path: '/trip/my',
icon: markRaw(IconTrip),
},
{
id: 20,
name: '直播',
path: '/live',
icon: markRaw(RiLiveLine),
children: [
{
id: 23,
name: '商品管理',
path: '/live/product/management',
icon: markRaw(RiShoppingBagLine),
},
{ id: 24, name: '直播话术管理', path: '/live/talk', icon: markRaw(RiMessage3Line) },
{ id: 25, name: '直播', path: '/live/test', icon: markRaw(RiVideoLine) },
],
},
{
id: 26,
name: '数据分析',
path: '/analyze',
icon: markRaw(IconAnalyze),
children: [
{ id: 27, name: '用户分析', path: '/analyze/user', icon: markRaw(RiUserLine) },
{ id: 28, name: '标签群组分析', path: '/analyze/label', icon: markRaw(RiGroupLine) },
{ id: 29, name: '事件分析', path: '/analyze/event', icon: markRaw(RiCalendarEventLine) },
{ id: 30, name: '营销分析', path: '/analyze/marketing', icon: markRaw(RiBarChartLine) },
],
},
]
const adminMenus: IMenuItem[] = [
const baseMenus: IMenuItem[] = [
{
id: 1,
name: '基础配置',
......@@ -228,6 +72,7 @@ const adminMenus: IMenuItem[] = [
name: '事件属性管理',
path: '/metadata/event',
icon: markRaw(IconEvent),
role: [5, 6],
// tag: 'v1-experiment-meta-event',
},
],
......@@ -426,8 +271,7 @@ const adminMenus: IMenuItem[] = [
export const useMenuStore = defineStore({
id: 'menu',
state: (): State => ({
studentMenus,
adminMenus,
baseMenus,
}),
getters: {
menus: (state) => {
......@@ -452,12 +296,11 @@ export const useMenuStore = defineStore({
}
return filteredMenu
})
.filter((menu) => !menu.children || menu.children.length > 0)
}
// 根据用户角色选择基础菜单
// const baseMenus = userRole === 1 ? state.studentMenus : state.adminMenus
const filteredMenus = filterMenus(state.adminMenus)
return filteredMenus.length === 1 ? filteredMenus[0]?.children || [] : filteredMenus
const filteredMenus = filterMenus(state.baseMenus)
return filteredMenus.length === 1 && filteredMenus[0].children ? filteredMenus[0].children : filteredMenus
},
},
})
......@@ -46,7 +46,6 @@ export const useUserStore = defineStore({
const res = await getUser()
const { info, role } = res.data
const { organization, project, roles = [], permissions = [] } = res.data.permissions || {}
this.user = info
this.role = role
this.organization = organization
this.project = project
......@@ -59,10 +58,14 @@ export const useUserStore = defineStore({
await getAuth().then((res) => {
this.menus = res.data.items || []
})
this.user = info
},
async logout() {
await logout()
this.user = null
this.status = {}
this.menus = []
this.permissions = []
},
},
})
import axios from 'axios'
import { ElMessage } from 'element-plus'
import { getLoginUrl } from '@/utils/env'
import { getRequestContext } from '@/utils/requestContext'
// import router from '@/router'
const httpRequest = axios.create({
......@@ -14,7 +15,7 @@ const httpRequest = axios.create({
// 请求拦截
httpRequest.interceptors.request.use(
function (config) {
const params = new URLSearchParams(window.location.search)
const params = getRequestContext()
if (config.method === 'get')
config.params = Object.assign({}, config.params, {
......
let routeParams: URLSearchParams | undefined
export function setRequestContext(fullPath: string) {
routeParams = new URL(fullPath, window.location.origin).searchParams
}
export function getRequestContext() {
return routeParams || new URLSearchParams(window.location.search)
}
Markdown 格式
0% 或
您添加了 0 人 到此讨论。请谨慎行事。
请先完成此评论的编辑!
请 注册 或者 后发表评论