提交 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>
<script setup lang="ts">
import { Search, RefreshLeft } from '@element-plus/icons-vue'
import { Search, RefreshLeft, Filter } from '@element-plus/icons-vue'
import { useMediaQuery } from '@vueuse/core'
import { cloneDeep, get } from 'lodash-es'
const isMobile = useMediaQuery('(max-width: 767px)')
const attrs = useAttrs()
// 移动端卡片列(过滤序号、操作等 slot 列留给卡片底部渲染)
const cardColumns = computed(() => props.columns.filter((c) => c.type !== 'index' && c.slots !== 'table-x'))
// 筛选、表格和移动端卡片通过动态名称传递不同的插槽参数。
defineSlots<{
[name: string]: (props: any) => any
}>()
const cardColumns = computed(() => props.columns.filter((c) => c.type !== 'index' && c.type !== 'selection' && c.slots !== 'table-x'))
const hasSelectionColumn = computed(() => props.columns.some((c) => c.type === 'selection'))
const hasActionsColumn = computed(() => props.columns.some((c) => c.slots === 'table-x'))
interface IRemoteProps {
......@@ -40,153 +47,169 @@ const filterFormRef = ref()
const loading = ref(false)
const tableRef = ref()
const dataList = ref<any[]>([])
// 树形和展开表格保留原有结构,避免卡片模式漏掉子级及详情。
const useMobileCards = computed(() => isMobile.value && props.mobileCards
&& !props.columns.some(column => column.type === 'expand')
&& !dataList.value.some(row => row.children?.length))
const page = reactive({ total: 0, size: props.limit, currentPage: 1 })
const initialFilterParams = cloneDeep(props.remote?.params || {})
const params = reactive({ ...props.remote?.params })
watchEffect(() => {
Object.assign(params, props.remote?.params)
})
watchEffect(() => {
dataList.value = props.data || []
})
const mobileFilterVisible = ref(false)
const mobileFilterDraft = reactive<Record<string, any>>({})
watch(isMobile, () => { mobileFilterVisible.value = false })
const activeFilterCount = computed(() => props.filters.filter(item => {
const value = params[item.prop]
return value !== undefined && value !== null && value !== '' && (!Array.isArray(value) || value.length > 0)
}).length)
function openMobileFilters() {
Object.keys(mobileFilterDraft).forEach(key => delete mobileFilterDraft[key])
Object.assign(mobileFilterDraft, cloneDeep(params))
mobileFilterVisible.value = true
}
function resetMobileFilters() {
props.filters.forEach(item => {
mobileFilterDraft[item.prop] = cloneDeep(initialFilterParams[item.prop] ?? '')
})
}
function applyMobileFilters() {
Object.assign(params, cloneDeep(mobileFilterDraft))
mobileFilterVisible.value = false
search()
}
watchEffect(() => { Object.assign(params, props.remote?.params) })
watchEffect(() => { dataList.value = props.data || [] })
// 获取数据
const fetchList = (isReset = false) => {
/**
* @param function httpRequest api接口
* @param function beforeRequest 接口请求之前
* @param function callback 接口请求成功回调
*/
const { httpRequest, beforeRequest, callback } = props.remote || {}
if (!httpRequest) {
return
}
// 参数设置
if (!httpRequest) return
let requestParams = { ...params }
// 翻页参数设置
if (props.hasPagination) {
requestParams.page = page.currentPage
requestParams['per-page'] = page.size
}
// 接口请求之前
if (beforeRequest) {
requestParams = beforeRequest(requestParams, isReset)
}
if (beforeRequest) requestParams = beforeRequest(requestParams, isReset)
for (const key in requestParams) {
if (requestParams[key] === '' || requestParams[key] === undefined || requestParams[key] === null) {
delete requestParams[key]
}
if (requestParams[key] === '' || requestParams[key] === undefined || requestParams[key] === null) delete requestParams[key]
}
loading.value = true
return (
httpRequest(requestParams)
.then((res: any) => {
const { list = [], total = 0 } = callback ? callback(res.data, requestParams) : res.data || {}
page.total = parseInt(total)
dataList.value = list
})
// .catch(() => {
// page.total = 0
// dataList.value = []
// })
.finally(() => {
loading.value = false
})
)
return httpRequest(requestParams)
.then((res: any) => {
const { list = [], total = 0 } = callback ? callback(res.data, requestParams) : res.data || {}
page.total = parseInt(total)
dataList.value = list
})
.finally(() => { loading.value = false })
}
// 搜索
const search = () => {
page.currentPage = 1
return fetchList()
}
// 重置
const search = () => { page.currentPage = 1; return fetchList() }
const reset = () => {
// 清空筛选条件
filterFormRef.value?.resetFields()
// 初始化页码
props.filters.forEach(item => { params[item.prop] = cloneDeep(initialFilterParams[item.prop] ?? '') })
page.currentPage = 1
// 刷新列表
return fetchList(true)
}
const refetch = (isForce = false) => isForce ? reset() : fetchList()
const pageSizeChange = (value: number) => { page.currentPage = 1; page.size = value; fetchList() }
// 刷新
const refetch = (isForce = false) => {
return isForce ? reset() : fetchList()
// 移动端多选
const mobileSelection = ref<any[]>([])
const mobileIsAllSelected = computed(() => dataList.value.length > 0 && mobileSelection.value.length === dataList.value.length)
function toggleMobileSelectAll() {
mobileSelection.value = mobileIsAllSelected.value ? [] : [...dataList.value]
emitSelectionChange()
}
// 页数改变
const pageSizeChange = (value: number) => {
page.currentPage = 1
page.size = value
fetchList()
function toggleMobileSelectRow(row: any) {
const idx = mobileSelection.value.findIndex((r) => r === row)
if (idx > -1) mobileSelection.value.splice(idx, 1)
else mobileSelection.value.push(row)
emitSelectionChange()
}
function isMobileRowSelected(row: any) { return mobileSelection.value.some((r) => r === row) }
function emitSelectionChange() {
const handler = (attrs as Record<string, unknown>).onSelectionChange as ((val: any[]) => void) | undefined
if (handler) handler(mobileSelection.value)
}
onMounted(() => {
fetchList()
watch([dataList, isMobile], () => {
if (mobileSelection.value.length) {
mobileSelection.value = []
emitSelectionChange()
}
})
onMounted(() => { fetchList() })
defineExpose({ refetch, tableRef })
</script>
<template>
<div class="table-list">
<div class="table-list-hd">
<!-- 筛选 -->
<div class="table-list-filter" v-if="filters && filters.length">
<div class="table-list-hd" v-if="!isMobile && filters.length">
<div class="table-list-filter" v-if="!isMobile && filters.length">
<el-form :inline="true" :model="params" v-bind="filterForm" ref="filterFormRef" @submit.prevent>
<template v-for="item in filters" :key="item.prop">
<el-form-item :label="item.label" :prop="item.prop">
<template v-if="item.slots">
<slot :name="item.slots" v-bind="{ params }"></slot>
<slot :name="item.slots" v-bind="{ params, onChange: search }"></slot>
</template>
<template v-else>
<!-- input -->
<el-input
v-model="params[item.prop]"
v-bind="item"
clearable
@change="search"
v-if="item.type === 'input'" />
<!-- select -->
<el-select
v-model="params[item.prop]"
v-bind="item"
filterable
clearable
@change="search"
v-if="item.type === 'select'">
<el-option
v-for="(option, index) in item.options"
:label="option[item.labelKey] || option.label || option"
:value="option[item.valueKey] || option.value || option"
:key="index" />
<el-input v-model="params[item.prop]" v-bind="item" clearable @change="search" style="width: 200px" v-if="item.type === 'input'" />
<el-select v-model="params[item.prop]" v-bind="item" filterable clearable @change="search" v-if="item.type === 'select'" value-on-clear="">
<el-option :label="option[item.labelKey] || option.label" :value="option[item.valueKey] || option.value"
v-for="(option, index) in item.options" :key="index" />
</el-select>
</template>
</el-form-item>
</template>
<el-form-item class="filter-buttons" v-if="!isMobile">
<el-form-item class="filter-buttons">
<el-button type="primary" :icon="Search" @click="search">搜索</el-button>
<el-button :icon="RefreshLeft" @click="reset">重置</el-button>
</el-form-item>
</el-form>
</div>
<!-- <div class="table-list-hd-aside"><slot name="header-aside" /></div> -->
</div>
<div class="table-list-buttons"><slot name="header-buttons"></slot></div>
<el-drawer v-model="mobileFilterVisible" title="筛选" direction="btt" size="75%" append-to-body class="mobile-filter-drawer">
<div class="table-list-filter">
<el-form :model="mobileFilterDraft" v-bind="filterForm" :inline="false" label-position="top" @submit.prevent="applyMobileFilters">
<template v-for="item in filters" :key="item.prop">
<el-form-item :label="item.label" :prop="item.prop">
<template v-if="item.slots">
<slot :name="item.slots" v-bind="{ params: mobileFilterDraft, onChange: () => {} }"></slot>
</template>
<template v-else>
<el-input v-model="mobileFilterDraft[item.prop]" v-bind="item" clearable style="width: 200px" v-if="item.type === 'input'" />
<el-select v-model="mobileFilterDraft[item.prop]" v-bind="item" filterable clearable v-if="item.type === 'select'" value-on-clear="">
<el-option :label="option[item.labelKey] || option.label" :value="option[item.valueKey] || option.value"
v-for="(option, index) in item.options" :key="index" />
</el-select>
</template>
</el-form-item>
</template>
</el-form>
</div>
<template #footer>
<div class="mobile-filter-footer">
<el-button :icon="RefreshLeft" @click="resetMobileFilters">重置</el-button>
<el-button type="primary" @click="applyMobileFilters">确定</el-button>
</div>
</template>
</el-drawer>
<div class="table-list-buttons">
<slot name="header-buttons"></slot>
<el-button v-if="isMobile && filters.length" :icon="Filter" class="mobile-filter-trigger"
:aria-label="activeFilterCount ? `筛选,已选 ${activeFilterCount} 项` : '筛选'"
:class="{ 'is-active': activeFilterCount > 0 }" @click="openMobileFilters">
<span v-if="activeFilterCount" class="mobile-filter-count">{{ activeFilterCount }}</span>
</el-button>
</div>
<slot></slot>
<!-- 主体 -->
<div class="table-list-bd">
<slot name="body" v-bind="{ data: dataList, loading }">
<!-- 桌面端 / 禁用移动端卡片时:表格 -->
<el-table
v-if="!isMobile || !mobileCards"
:header-cell-style="{ background: '#ededed' }"
:data="dataList"
v-loading="loading && hasLoading"
v-bind="$attrs"
ref="tableRef">
<!-- 桌面端表格 -->
<el-table v-if="!useMobileCards" stripe :header-cell-style="{ background: '#ededed' }"
:data="dataList" v-loading="loading && hasLoading" v-bind="$attrs" ref="tableRef">
<el-table-column align="center" v-bind="item || {}" v-for="item in columns" :key="item.prop">
<template #default="scope" v-if="item.slots || item.computed">
<slot :name="item.slots" v-bind="scope" v-if="item.slots"></slot>
......@@ -194,24 +217,31 @@ defineExpose({ refetch, tableRef })
</template>
</el-table-column>
</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 v-if="useMobileCards" class="mobile-card-list" v-loading="loading && hasLoading">
<div class="mobile-card-select-all" v-if="hasSelectionColumn && dataList.length">
<el-checkbox :model-value="mobileIsAllSelected" @change="toggleMobileSelectAll">
全选({{ mobileSelection.length }}/{{ dataList.length }})
</el-checkbox>
</div>
<div class="mobile-card" v-for="(row, rowIndex) in dataList" :key="rowIndex"
:class="{ 'is-selected': isMobileRowSelected(row) }">
<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 class="mobile-card-checkbox" v-if="hasSelectionColumn" @click.stop>
<el-checkbox :model-value="isMobileRowSelected(row)" @change="toggleMobileSelectRow(row)" />
</div>
<div class="mobile-card-rows">
<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>{{ get(row, col.prop, '-') ?? '-' }}</template>
</span>
</div>
</div>
</div>
<!-- 操作栏:如果有 slot 列,提取渲染 -->
<div class="mobile-card-ft" v-if="hasActionsColumn">
<slot name="table-x" v-bind="{ row, $index: rowIndex }"></slot>
</div>
......@@ -222,204 +252,116 @@ defineExpose({ refetch, tableRef })
</div>
<!-- 底部 -->
<div class="table-list-ft">
<div>
<slot name="footer"></slot>
</div>
<el-pagination
class="table-list-pagination"
background
layout="total, sizes, prev, pager, next, jumper"
:page-sizes="[10, 20, 30, 50]"
:page-size="page.size"
:total="page.total"
v-model:currentPage="page.currentPage"
@size-change="pageSizeChange"
@current-change="fetchList()"
:hide-on-single-page="true"
v-if="hasPagination">
<div><slot name="footer"></slot></div>
<el-pagination class="table-list-pagination" background
:layout="isMobile ? 'prev, pager, next' : 'total, sizes, prev, pager, next, jumper'"
:page-sizes="[10, 20, 30, 50]" :page-size="page.size" :total="page.total"
:pager-count="isMobile ? 5 : 7"
v-model:currentPage="page.currentPage" @size-change="pageSizeChange" @current-change="fetchList()"
:hide-on-single-page="true" v-if="hasPagination">
</el-pagination>
</div>
</div>
</template>
<style lang="scss">
.table-list-hd {
display: flex;
margin-bottom: 20px;
&:empty {
display: none;
}
}
.table-list-filter {
flex: 1;
}
.table-list-buttons {
margin-bottom: 20px;
&:empty {
display: none;
}
.table-list-hd { display: flex; margin-bottom: 20px; &:empty { display: none; } }
.table-list { min-width: 0; }
.table-list-filter { flex: 1; min-width: 0; }
.table-list-bd { min-width: 0; max-width: 100%; overflow-x: auto; }
.table-list-buttons { margin-bottom: 20px; display: flex; flex-wrap: wrap; gap: 8px; &:empty { display: none; } }
.table-list-ft { padding: 10px 0; display: flex; align-items: center; justify-content: space-between; }
.table-list-pagination { text-align: right; }
.el-table-column--selection .cell { padding: 0 14px !important; }
.table-list-bd .el-button {
a { margin: -8px -15px; padding: 8px 15px; }
&.is-text { padding: 8px; }
&.is-text + .is-text { margin-left: 0; }
&.is-plain { --el-button-border-color: var(--el-button-bg-color); }
}
.table-list-ft {
padding: 10px 0;
display: flex;
align-items: center;
justify-content: space-between;
// 移动端卡片
.mobile-card-list { min-height: 120px; }
.mobile-card-select-all { padding: 8px 0; .el-checkbox { font-size: 13px; color: #666; } }
.mobile-card { background: #fff; border-radius: 8px; border: 1px solid #eee; margin-bottom: 12px; overflow: hidden; display: flex; flex-direction: column;
&.is-selected { border-color: var(--main-color); background: #fef5f7; }
}
.table-list-pagination {
text-align: right;
.mobile-card-body { flex: 1; padding: 14px 16px; display: flex; align-items: flex-start; gap: 10px; }
.mobile-card-checkbox { flex-shrink: 0; padding-top: 2px; }
.mobile-card-rows { flex: 1; min-width: 0; }
.mobile-card-row { display: flex; padding: 5px 0; border-bottom: 1px solid #f5f5f5; line-height: 1.6;
&:last-child { border-bottom: none; }
}
.el-table-column--selection .cell {
padding: 0 14px !important;
}
.table-list-bd {
.el-button {
a {
margin: -8px -15px;
padding: 8px 15px;
}
&.is-text {
padding: 8px;
}
&.is-text + .is-text {
margin-left: 0;
}
&.is-plain {
--el-button-border-color: var(--el-button-bg-color);
}
}
.mobile-card-label { flex-shrink: 0; max-width: 35%; overflow-wrap: anywhere; 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: 8px 16px; display: flex; gap: 2px; flex-wrap: wrap; background: #fafafa; border-top: 1px solid #f0f0f0;
.el-button.is-text { padding: 4px 6px; font-size: 12px; }
}
// 移动端卡片列表
.mobile-card-list {
min-height: 120px;
}
.mobile-card {
background: #fff;
border-radius: 12px;
border: 1px solid #e5e5e5;
margin-bottom: 12px;
overflow: hidden;
display: flex;
flex-direction: column;
}
.mobile-card-body {
flex: 1;
padding: 14px;
}
.mobile-card-row {
display: flex;
align-items: flex-start;
padding: 8px 0;
border-bottom: 1px solid #f2f2f2;
line-height: 1.6;
&:last-child {
border-bottom: none;
}
}
.mobile-card-label {
flex-shrink: 0;
color: #999;
font-size: 13px;
}
.mobile-card-colon {
color: #999;
font-size: 13px;
margin-right: 6px;
}
.mobile-card-value {
flex: 1;
font-size: 14px;
color: #333;
min-width: 0;
word-break: break-all;
}
.mobile-card-row:first-child {
.mobile-card-value {
color: var(--main-color);
font-size: 16px;
font-weight: 600;
line-height: 1.5;
}
}
.mobile-card-ft {
padding: 10px 12px;
display: flex;
flex-wrap: wrap;
gap: 4px 8px;
background: #fafafa;
border-top: 1px solid #f0f0f0;
.el-button {
padding: 5px 8px;
font-size: 12px;
min-width: 44px;
text-align: center;
+ .el-button {
margin-left: 0;
}
// 移动端
@media (max-width: 767px) {
.table-list-buttons .el-button.mobile-filter-trigger {
position: relative;
margin-left: auto;
display: inline-flex;
align-items: center;
justify-content: center;
flex: 0 0 40px;
width: 40px;
height: 40px;
padding: 0;
border-color: #e6e8eb;
border-radius: 10px;
> .el-icon, > span { margin: 0; }
&.is-active { color: var(--main-color); border-color: var(--main-color); }
}
}
.table-list-buttons:has(.view-switch) .mobile-filter-trigger { margin-left: 0; }
.mobile-filter-count { position: absolute; top: -5px; right: -5px; min-width: 16px; height: 16px; padding: 0 3px; border-radius: 8px; background: var(--main-color); color: #fff; font-size: 10px; line-height: 16px; }
// 移动端筛选表单
@media (max-width: 767px) {
.table-list-hd .el-form-item__label {
display: none;
.mobile-filter-drawer {
border-radius: 16px 16px 0 0;
.el-drawer__header { margin-bottom: 0; padding: 16px; }
.el-drawer__body { padding: 16px; overflow-y: auto; }
.el-drawer__footer { padding: 12px 16px max(12px, env(safe-area-inset-bottom)); border-top: 1px solid #eee; }
.table-list-filter { min-width: 0; padding: 0; }
}
.mobile-filter-footer { display: flex; gap: 12px; .el-button { flex: 1; height: 40px; margin-left: 0; } }
.table-list-hd .el-form-item__label { justify-content: flex-start; height: auto; line-height: 24px; padding-bottom: 4px; }
.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 {
display: none;
}
.el-form, .el-form--inline { width: 100%; }
.el-form-item { display: flex; flex-direction: column; align-items: stretch; width: 100%; margin-right: 0; margin-bottom: 12px; }
.el-form-item__content { width: 100%; }
.el-input, .el-select, .el-cascader { width: 100% !important; }
.filter-buttons { width: 100%; .el-form-item__content { display: flex; gap: 8px; } .el-button { flex: 1; min-height: 40px; margin-left: 0; } }
}
// 移动端分页
.table-list-ft {
flex-direction: column;
.table-list-buttons { align-items: center; gap: 8px; .el-button { min-height: 40px; margin-left: 0; } }
.mobile-card-ft {
justify-content: flex-end;
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;
padding: 4px 12px;
.el-button, .el-button.is-text {
min-height: 40px;
margin-left: 0;
padding: 8px 10px;
border: 0;
border-radius: 4px;
background: transparent;
color: var(--el-text-color-regular);
font-size: 14px;
a { margin: 0; padding: 0; }
&:hover { background: var(--el-fill-color-light); }
&.el-button--primary { color: var(--main-color); }
&.el-button--danger { color: var(--el-color-danger); }
&.is-disabled { color: var(--el-disabled-text-color); background: transparent; }
}
}
.mobile-card-checkbox { min-height: 44px; display: flex; align-items: center; }
.table-list-ft { flex-direction: column; gap: 8px; padding: 8px 0; }
.table-list-pagination .el-pagination {
justify-content: center;
}
.el-pager .number { min-width: 28px; height: 28px; line-height: 28px; font-size: 13px; }
.btn-prev, .btn-next { height: 28px; min-width: 28px; }
}
</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 人 到此讨论。请谨慎行事。
请先完成此评论的编辑!
请 注册 或者 后发表评论