diff --git a/src/pages/events/banner/components/BannerFormModal.tsx b/src/pages/events/banner/components/BannerFormModal.tsx index e662968..cd6fadc 100644 --- a/src/pages/events/banner/components/BannerFormModal.tsx +++ b/src/pages/events/banner/components/BannerFormModal.tsx @@ -50,7 +50,7 @@ const getDefaultForm = () => ({ type: '1' as '1' | '2' | '3' | '0', linkUrl: '', relatedEventId: undefined as string | undefined, - area: '全国', + area: '', displayTimeRange: null as [string, string] | null, sort: 1, }); @@ -84,18 +84,49 @@ function base64ToFile(dataUrl: string, fileName: string): File { // 工具函数 // ============================================================ -/** 从 Cascader 路径中提取城市名(最后一级),用于区域筛选 */ -function extractCityName(path: string[]): string { - if (!path || path.length === 0 || path[0] === '全国') return ''; - return path[path.length - 1]; +/** 在 Cascader options 树中按路径查找末端节点 */ +function findNodeByPath(options: any[], path: string[]): any | null { + let level = options; + let node: any = null; + for (const seg of path) { + node = level.find((o) => o.value === seg); + if (!node) return null; + level = node.children || []; + } + return node; } -/** 从 Cascader 路径中提取存储用的区域字符串 */ -function extractAreaValue(path: string[]): string { - if (!path || path.length === 0) return '全国'; - if (path[0] === '全国') return '全国'; - // 直辖市路径如 ["北京市"],普通城市如 ["广东省", "深圳市"] - return path[path.length - 1]; +/** 递归收集节点下的所有叶子节点 value */ +function collectLeafValues(node: any, acc: string[]) { + if (node.children && node.children.length > 0) { + node.children.forEach((child: any) => collectLeafValues(child, acc)); + } else { + acc.push(String(node.value)); + } +} + +/** + * 将 Cascader 多选选中的路径数组转为叶子节点 value 数组(去重) + * 多选时可能选中父节点(省),这里递归展开为最下级的叶子节点(市) + * 如 [["广东省"], ["北京市"]] -> ["广州市", "深圳市", ..., "北京市"] + */ +function extractLeafValues(options: any[], paths: string[][]): string[] { + const result: string[] = []; + const seen = new Set(); + for (const path of paths) { + if (!path || path.length === 0) continue; + const node = findNodeByPath(options, path); + if (!node) continue; + const leaves: string[] = []; + collectLeafValues(node, leaves); + leaves.forEach((v) => { + if (!seen.has(v)) { + seen.add(v); + result.push(v); + } + }); + } + return result; } /** 根据跳转类型计算 parms 字段的值 */ @@ -128,8 +159,8 @@ export default defineComponent({ const fileInputRef = ref(null); const formData = reactive(getDefaultForm()); - /** Cascader 区域的路径数组:如 ["全国"]、["广东省", "深圳市"] */ - const areaPath = ref(['全国']); + /** Cascader 区域的路径数组(多选):如 [["全国"]]、[["广东省", "深圳市"], ["北京市"]] */ + const areaPath = ref([]); /** 是否由用户主动切换了跳转类型 */ const typeChanged = ref(false); @@ -152,22 +183,22 @@ export default defineComponent({ return [{ value: '', label: '不关联' }, ...eventOptions.value]; }); - // ---- 赛事搜索 API 调用 ---- + // ---- 赛事列表获取(keyword 可为空:选择跳转类型/所属区域后自动获取) ---- const doSearch = async (keyword: string) => { - if (!keyword.trim()) { - setEventOptions([]); - return; - } setEventSearchLoading(true); try { - const cityName = extractCityName(areaPath.value); + // 当前选中的叶子区域名(多选去重后) + const leafNames = extractLeafValues(areaCascaderOptions, areaPath.value); const params: Record = { page: '1', limit: '999', - name: keyword.trim(), }; - if (cityName) { - params.areaName = cityName; + if (keyword.trim()) { + params.name = keyword.trim(); + } + // 区域筛选:接口 areaName 为单值,仅在恰好选中一个区域时传递 + if (leafNames.length === 1 && leafNames[0] !== '全国') { + params.areaName = leafNames[0]; } const res = await getEventList(params as any); if (res.code == 200) { @@ -182,7 +213,7 @@ export default defineComponent({ setEventOptions([]); } } catch (e) { - console.error('[Banner] 搜索赛事失败:', e); + console.error('[Banner] 获取赛事列表失败:', e); setEventOptions([]); } finally { setEventSearchLoading(false); @@ -202,26 +233,24 @@ export default defineComponent({ }, 350); }; - /** 所属区域 Cascader 变化 */ + /** 所属区域 Cascader 变化(多选) */ const onAreaChange = (val: any) => { - const path: string[] = val || []; - const newArea = extractAreaValue(path); - areaPath.value = path; - formData.area = newArea; + const paths: string[][] = Array.isArray(val) ? val : []; + areaPath.value = paths; + + // 多选后只保留最下级的叶子节点,并用逗号分隔成字符串 + const leafNames = extractLeafValues(areaCascaderOptions, paths); + formData.area = leafNames.join(','); // 切换到非"全国"区域时清空已关联赛事 - if (newArea !== '全国') { + if (formData.area !== '全国') { formData.relatedEventId = undefined; formRef.value?.clearValidate('relatedEventId'); } - // 区域变化时重新搜索(如果当前有关键词) - if (eventKeyword.value.trim()) { - if (searchTimer) clearTimeout(searchTimer); - searchTimer = setTimeout(() => { - doSearch(eventKeyword.value); - }, 0); - } + // 区域变化时自动重新获取赛事列表 + if (searchTimer) clearTimeout(searchTimer); + doSearch(eventKeyword.value); }; // ---- 根据 record 初始化表单 ---- @@ -229,21 +258,30 @@ export default defineComponent({ const fresh = getDefaultForm(); if (!record) { Object.assign(formData, fresh); - areaPath.value = ['全国']; + areaPath.value = []; eventKeyword.value = ''; setEventOptions([]); + // 新增时默认跳转类型为"赛事详情页",自动加载赛事下拉 + if (formData.type === '1') { + doSearch(''); + } return; } - // 区域:兼容旧格式 "广东省-深圳市"、新格式 "深圳市"、"全国" + // 区域:兼容旧格式 "广东省-深圳市"、新格式多选 "深圳市,北京市"、"全国" const rawArea = record.area || record.region || ''; - let cityName: string; - if (rawArea.includes('-')) { - cityName = rawArea.split('-').pop() || '全国'; - } else { - cityName = rawArea || '全国'; - } - areaPath.value = cityNameToCascaderPath(cityName); + const areaNames = rawArea + .split(',') + .map((s: string) => s.trim()) + .filter(Boolean); + const leafNames = areaNames.map((name: string) => + name.includes('-') ? name.split('-').pop()! : name, + ); + const paths: string[][] = leafNames.length + ? leafNames.map((name: string) => cityNameToCascaderPath(name)) + : [['全国']]; + areaPath.value = paths; + const normalizedArea = leafNames.length ? leafNames.join(',') : '全国'; // 跳转类型 const type = String(record.type || record.jumpType || '1'); @@ -285,7 +323,7 @@ export default defineComponent({ type, linkUrl: record.params || record.linkUrl || record.jumpUrl || '', relatedEventId, - area: cityName, + area: normalizedArea, displayTimeRange: (record.showTimeBegin || record.displayStartTime) && (record.showTimeEnd || record.displayEndTime) @@ -341,6 +379,12 @@ export default defineComponent({ if (activeNames.length) { await formRef.value?.validateFields(activeNames).catch(() => {}); } + + // 切换到"赛事详情页"时自动加载赛事下拉 + if (formData.type === '1') { + if (searchTimer) clearTimeout(searchTimer); + doSearch(eventKeyword.value); + } }; syncTypeFields(); @@ -614,8 +658,9 @@ export default defineComponent({ diff --git a/src/pages/events/banner/index.module.less b/src/pages/events/banner/index.module.less new file mode 100644 index 0000000..158a6ae --- /dev/null +++ b/src/pages/events/banner/index.module.less @@ -0,0 +1,33 @@ +.cell { + display: flex; + align-items: center; + gap: 6px; + width: 100%; + cursor: default; +} + +.text { + flex: 1; + min-width: 0; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; + text-overflow: ellipsis; + line-height: 1.5; + word-break: break-all; + color: rgba(0, 0, 0, 0.88); +} + +.view { + flex-shrink: 0; + color: #1677ff; + cursor: pointer; + user-select: none; + line-height: 1.5; + white-space: nowrap; + + &:hover { + opacity: 0.85; + } +} diff --git a/src/pages/events/banner/index.tsx b/src/pages/events/banner/index.tsx index 1e7f042..de557f8 100644 --- a/src/pages/events/banner/index.tsx +++ b/src/pages/events/banner/index.tsx @@ -1,4 +1,4 @@ -import { defineComponent } from 'vue'; +import { defineComponent, ref, nextTick } from 'vue'; import { Button, Input, @@ -12,10 +12,70 @@ import { Image, } from 'ant-design-vue'; import { useBannerModel, BANNER_STATUS_OPTIONS } from './model/useBannerModel'; -import { useContainerSize } from '@/hooks'; +import { useContainerSize, useState, useEffect } from '@/hooks'; import { usePermissionStore } from '@/stores/permissionStore'; import BannerFormModal from './components/BannerFormModal'; import pageStyles from '@/assets/styles/pageLayout.module.less'; +import cellStyles from './index.module.less'; + +/** + * 所属区域单元格:最多 2 行截断,溢出时显示"查看"按钮 + */ +const AreaCell = defineComponent({ + name: 'AreaCell', + props: { text: { type: String, required: true } }, + setup(props) { + const textRef = ref(null); + const [overflow, setOverflow] = useState(false); + const [modalOpen, setModalOpen] = useState(false); + + const checkOverflow = () => { + const el = textRef.value; + if (el) setOverflow(el.scrollHeight > el.clientHeight); + }; + + let observer: ResizeObserver | null = null; + useEffect(() => { + if (!textRef.value) return; + nextTick(checkOverflow); + observer = new ResizeObserver(checkOverflow); + observer.observe(textRef.value); + return () => { + observer?.disconnect(); + observer = null; + }; + }, [() => textRef.value]); + + return () => { + const raw = props.text; + return ( +
+
+ {raw} +
+ {overflow.value ? ( + setModalOpen(true)}> + 查看 + + ) : null} + setModalOpen(false)} + > +
+ {raw.replace(/,/g, ' ')} +
+
+
+ ); + }; + }, +}); /** * bodyCell 渲染函数 @@ -72,6 +132,12 @@ function renderBodyCell({ ); } + // 所属区域列 + if (column.dataIndex === 'area') { + const raw = text || ''; + return raw ? : --; + } + // 跳转类型列 if (column.dataIndex === 'type') { return renderJumpType(text); diff --git a/src/pages/events/banner/model/useBannerModel.ts b/src/pages/events/banner/model/useBannerModel.ts index 7adaafc..a12d4ee 100644 --- a/src/pages/events/banner/model/useBannerModel.ts +++ b/src/pages/events/banner/model/useBannerModel.ts @@ -116,7 +116,7 @@ export function useBannerModel() { key: 'type', width: 120, }, - { title: '所属区域', dataIndex: 'area', key: 'area', width: 140 }, + { title: '所属区域', dataIndex: 'area', key: 'area', width: 300 }, { title: '展示时间', key: 'displayTime', width: 260 }, { title: '创建时间', dataIndex: 'createDate', key: 'createDate', width: 160 }, { title: '状态', dataIndex: 'status', key: 'status', width: 100, align: 'center' as const },