Compare commits
8 Commits
a581f64ccb
..
master
| Author | SHA1 | Date | |
|---|---|---|---|
| ae960df897 | |||
| 955914995c | |||
| 1e9b013065 | |||
| e21c1458ae | |||
| 5f6325689f | |||
| 48ab5d013b | |||
| ce1a5e3f1f | |||
| 8c023e63ec |
+1
-1
@@ -9,7 +9,7 @@
|
|||||||
"serve:prod": "vite --mode production",
|
"serve:prod": "vite --mode production",
|
||||||
"build:dev": "vue-tsc --noEmit && vite build --mode development",
|
"build:dev": "vue-tsc --noEmit && vite build --mode development",
|
||||||
"build:test": "vue-tsc --noEmit && vite build --mode test",
|
"build:test": "vue-tsc --noEmit && vite build --mode test",
|
||||||
"build:prod": "vite build --mode production",
|
"build:prod": "node --max-old-space-size=4096 ./node_modules/vite/bin/vite.js build --mode production",
|
||||||
"test": "vitest",
|
"test": "vitest",
|
||||||
"test:run": "vitest run",
|
"test:run": "vitest run",
|
||||||
"test:coverage": "vitest run --coverage",
|
"test:coverage": "vitest run --coverage",
|
||||||
|
|||||||
@@ -76,6 +76,10 @@ export interface EventListQueryParams {
|
|||||||
idCardImg?: string;
|
idCardImg?: string;
|
||||||
/** 是否要求提供证件号码;0=无需,1=需要 */
|
/** 是否要求提供证件号码;0=无需,1=需要 */
|
||||||
idCardNo?: string;
|
idCardNo?: string;
|
||||||
|
/** 报名人数起始(含) */
|
||||||
|
scBegin?: string;
|
||||||
|
/** 报名人数结束(含) */
|
||||||
|
scEnd?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 分页数据 */
|
/** 分页数据 */
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ const getDefaultForm = () => ({
|
|||||||
type: '1' as '1' | '2' | '3' | '0',
|
type: '1' as '1' | '2' | '3' | '0',
|
||||||
linkUrl: '',
|
linkUrl: '',
|
||||||
relatedEventId: undefined as string | undefined,
|
relatedEventId: undefined as string | undefined,
|
||||||
area: '全国',
|
area: '',
|
||||||
displayTimeRange: null as [string, string] | null,
|
displayTimeRange: null as [string, string] | null,
|
||||||
sort: 1,
|
sort: 1,
|
||||||
});
|
});
|
||||||
@@ -84,18 +84,49 @@ function base64ToFile(dataUrl: string, fileName: string): File {
|
|||||||
// 工具函数
|
// 工具函数
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
/** 从 Cascader 路径中提取城市名(最后一级),用于区域筛选 */
|
/** 在 Cascader options 树中按路径查找末端节点 */
|
||||||
function extractCityName(path: string[]): string {
|
function findNodeByPath(options: any[], path: string[]): any | null {
|
||||||
if (!path || path.length === 0 || path[0] === '全国') return '';
|
let level = options;
|
||||||
return path[path.length - 1];
|
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 路径中提取存储用的区域字符串 */
|
/** 递归收集节点下的所有叶子节点 value */
|
||||||
function extractAreaValue(path: string[]): string {
|
function collectLeafValues(node: any, acc: string[]) {
|
||||||
if (!path || path.length === 0) return '全国';
|
if (node.children && node.children.length > 0) {
|
||||||
if (path[0] === '全国') return '全国';
|
node.children.forEach((child: any) => collectLeafValues(child, acc));
|
||||||
// 直辖市路径如 ["北京市"],普通城市如 ["广东省", "深圳市"]
|
} else {
|
||||||
return path[path.length - 1];
|
acc.push(String(node.value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将 Cascader 多选选中的路径数组转为叶子节点 value 数组(去重)
|
||||||
|
* 多选时可能选中父节点(省),这里递归展开为最下级的叶子节点(市)
|
||||||
|
* 如 [["广东省"], ["北京市"]] -> ["广州市", "深圳市", ..., "北京市"]
|
||||||
|
*/
|
||||||
|
function extractLeafValues(options: any[], paths: string[][]): string[] {
|
||||||
|
const result: string[] = [];
|
||||||
|
const seen = new Set<string>();
|
||||||
|
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 字段的值 */
|
/** 根据跳转类型计算 parms 字段的值 */
|
||||||
@@ -128,8 +159,8 @@ export default defineComponent({
|
|||||||
const fileInputRef = ref<HTMLInputElement | null>(null);
|
const fileInputRef = ref<HTMLInputElement | null>(null);
|
||||||
const formData = reactive(getDefaultForm());
|
const formData = reactive(getDefaultForm());
|
||||||
|
|
||||||
/** Cascader 区域的路径数组:如 ["全国"]、["广东省", "深圳市"] */
|
/** Cascader 区域的路径数组(多选):如 [["全国"]]、[["广东省", "深圳市"], ["北京市"]] */
|
||||||
const areaPath = ref<string[]>(['全国']);
|
const areaPath = ref<string[][]>([]);
|
||||||
|
|
||||||
/** 是否由用户主动切换了跳转类型 */
|
/** 是否由用户主动切换了跳转类型 */
|
||||||
const typeChanged = ref(false);
|
const typeChanged = ref(false);
|
||||||
@@ -152,22 +183,22 @@ export default defineComponent({
|
|||||||
return [{ value: '', label: '不关联' }, ...eventOptions.value];
|
return [{ value: '', label: '不关联' }, ...eventOptions.value];
|
||||||
});
|
});
|
||||||
|
|
||||||
// ---- 赛事搜索 API 调用 ----
|
// ---- 赛事列表获取(keyword 可为空:选择跳转类型/所属区域后自动获取) ----
|
||||||
const doSearch = async (keyword: string) => {
|
const doSearch = async (keyword: string) => {
|
||||||
if (!keyword.trim()) {
|
|
||||||
setEventOptions([]);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setEventSearchLoading(true);
|
setEventSearchLoading(true);
|
||||||
try {
|
try {
|
||||||
const cityName = extractCityName(areaPath.value);
|
// 当前选中的叶子区域名(多选去重后)
|
||||||
|
const leafNames = extractLeafValues(areaCascaderOptions, areaPath.value);
|
||||||
const params: Record<string, string> = {
|
const params: Record<string, string> = {
|
||||||
page: '1',
|
page: '1',
|
||||||
limit: '999',
|
limit: '999',
|
||||||
name: keyword.trim(),
|
|
||||||
};
|
};
|
||||||
if (cityName) {
|
if (keyword.trim()) {
|
||||||
params.areaName = cityName;
|
params.name = keyword.trim();
|
||||||
|
}
|
||||||
|
// 区域筛选:接口 areaName 为单值,仅在恰好选中一个区域时传递
|
||||||
|
if (leafNames.length === 1 && leafNames[0] !== '全国') {
|
||||||
|
params.areaName = leafNames[0];
|
||||||
}
|
}
|
||||||
const res = await getEventList(params as any);
|
const res = await getEventList(params as any);
|
||||||
if (res.code == 200) {
|
if (res.code == 200) {
|
||||||
@@ -182,7 +213,7 @@ export default defineComponent({
|
|||||||
setEventOptions([]);
|
setEventOptions([]);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('[Banner] 搜索赛事失败:', e);
|
console.error('[Banner] 获取赛事列表失败:', e);
|
||||||
setEventOptions([]);
|
setEventOptions([]);
|
||||||
} finally {
|
} finally {
|
||||||
setEventSearchLoading(false);
|
setEventSearchLoading(false);
|
||||||
@@ -202,26 +233,24 @@ export default defineComponent({
|
|||||||
}, 350);
|
}, 350);
|
||||||
};
|
};
|
||||||
|
|
||||||
/** 所属区域 Cascader 变化 */
|
/** 所属区域 Cascader 变化(多选) */
|
||||||
const onAreaChange = (val: any) => {
|
const onAreaChange = (val: any) => {
|
||||||
const path: string[] = val || [];
|
const paths: string[][] = Array.isArray(val) ? val : [];
|
||||||
const newArea = extractAreaValue(path);
|
areaPath.value = paths;
|
||||||
areaPath.value = path;
|
|
||||||
formData.area = newArea;
|
// 多选后只保留最下级的叶子节点,并用逗号分隔成字符串
|
||||||
|
const leafNames = extractLeafValues(areaCascaderOptions, paths);
|
||||||
|
formData.area = leafNames.join(',');
|
||||||
|
|
||||||
// 切换到非"全国"区域时清空已关联赛事
|
// 切换到非"全国"区域时清空已关联赛事
|
||||||
if (newArea !== '全国') {
|
if (formData.area !== '全国') {
|
||||||
formData.relatedEventId = undefined;
|
formData.relatedEventId = undefined;
|
||||||
formRef.value?.clearValidate('relatedEventId');
|
formRef.value?.clearValidate('relatedEventId');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 区域变化时重新搜索(如果当前有关键词)
|
// 区域变化时自动重新获取赛事列表
|
||||||
if (eventKeyword.value.trim()) {
|
if (searchTimer) clearTimeout(searchTimer);
|
||||||
if (searchTimer) clearTimeout(searchTimer);
|
doSearch(eventKeyword.value);
|
||||||
searchTimer = setTimeout(() => {
|
|
||||||
doSearch(eventKeyword.value);
|
|
||||||
}, 0);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// ---- 根据 record 初始化表单 ----
|
// ---- 根据 record 初始化表单 ----
|
||||||
@@ -229,21 +258,30 @@ export default defineComponent({
|
|||||||
const fresh = getDefaultForm();
|
const fresh = getDefaultForm();
|
||||||
if (!record) {
|
if (!record) {
|
||||||
Object.assign(formData, fresh);
|
Object.assign(formData, fresh);
|
||||||
areaPath.value = ['全国'];
|
areaPath.value = [];
|
||||||
eventKeyword.value = '';
|
eventKeyword.value = '';
|
||||||
setEventOptions([]);
|
setEventOptions([]);
|
||||||
|
// 新增时默认跳转类型为"赛事详情页",自动加载赛事下拉
|
||||||
|
if (formData.type === '1') {
|
||||||
|
doSearch('');
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 区域:兼容旧格式 "广东省-深圳市"、新格式 "深圳市"、"全国"
|
// 区域:兼容旧格式 "广东省-深圳市"、新格式多选 "深圳市,北京市"、"全国"
|
||||||
const rawArea = record.area || record.region || '';
|
const rawArea = record.area || record.region || '';
|
||||||
let cityName: string;
|
const areaNames = rawArea
|
||||||
if (rawArea.includes('-')) {
|
.split(',')
|
||||||
cityName = rawArea.split('-').pop() || '全国';
|
.map((s: string) => s.trim())
|
||||||
} else {
|
.filter(Boolean);
|
||||||
cityName = rawArea || '全国';
|
const leafNames = areaNames.map((name: string) =>
|
||||||
}
|
name.includes('-') ? name.split('-').pop()! : name,
|
||||||
areaPath.value = cityNameToCascaderPath(cityName);
|
);
|
||||||
|
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');
|
const type = String(record.type || record.jumpType || '1');
|
||||||
@@ -285,7 +323,7 @@ export default defineComponent({
|
|||||||
type,
|
type,
|
||||||
linkUrl: record.params || record.linkUrl || record.jumpUrl || '',
|
linkUrl: record.params || record.linkUrl || record.jumpUrl || '',
|
||||||
relatedEventId,
|
relatedEventId,
|
||||||
area: cityName,
|
area: normalizedArea,
|
||||||
displayTimeRange:
|
displayTimeRange:
|
||||||
(record.showTimeBegin || record.displayStartTime) &&
|
(record.showTimeBegin || record.displayStartTime) &&
|
||||||
(record.showTimeEnd || record.displayEndTime)
|
(record.showTimeEnd || record.displayEndTime)
|
||||||
@@ -341,6 +379,12 @@ export default defineComponent({
|
|||||||
if (activeNames.length) {
|
if (activeNames.length) {
|
||||||
await formRef.value?.validateFields(activeNames).catch(() => {});
|
await formRef.value?.validateFields(activeNames).catch(() => {});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 切换到"赛事详情页"时自动加载赛事下拉
|
||||||
|
if (formData.type === '1') {
|
||||||
|
if (searchTimer) clearTimeout(searchTimer);
|
||||||
|
doSearch(eventKeyword.value);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
syncTypeFields();
|
syncTypeFields();
|
||||||
@@ -614,8 +658,9 @@ export default defineComponent({
|
|||||||
<Cascader
|
<Cascader
|
||||||
v-model:value={areaPath.value}
|
v-model:value={areaPath.value}
|
||||||
options={areaCascaderOptions}
|
options={areaCascaderOptions}
|
||||||
|
multiple
|
||||||
placeholder="请选择所属区域"
|
placeholder="请选择所属区域"
|
||||||
allowClear={false}
|
allowClear
|
||||||
onChange={onAreaChange}
|
onChange={onAreaChange}
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { defineComponent } from 'vue';
|
import { defineComponent, ref, nextTick } from 'vue';
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Input,
|
Input,
|
||||||
@@ -12,10 +12,70 @@ import {
|
|||||||
Image,
|
Image,
|
||||||
} from 'ant-design-vue';
|
} from 'ant-design-vue';
|
||||||
import { useBannerModel, BANNER_STATUS_OPTIONS } from './model/useBannerModel';
|
import { useBannerModel, BANNER_STATUS_OPTIONS } from './model/useBannerModel';
|
||||||
import { useContainerSize } from '@/hooks';
|
import { useContainerSize, useState, useEffect } from '@/hooks';
|
||||||
import { usePermissionStore } from '@/stores/permissionStore';
|
import { usePermissionStore } from '@/stores/permissionStore';
|
||||||
import BannerFormModal from './components/BannerFormModal';
|
import BannerFormModal from './components/BannerFormModal';
|
||||||
import pageStyles from '@/assets/styles/pageLayout.module.less';
|
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<HTMLElement | null>(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 (
|
||||||
|
<div class={cellStyles.cell}>
|
||||||
|
<div ref={textRef} class={cellStyles.text}>
|
||||||
|
{raw}
|
||||||
|
</div>
|
||||||
|
{overflow.value ? (
|
||||||
|
<span class={cellStyles.view} onClick={() => setModalOpen(true)}>
|
||||||
|
查看
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
|
<Modal
|
||||||
|
visible={modalOpen.value}
|
||||||
|
title="所属区域"
|
||||||
|
footer={null}
|
||||||
|
width={600}
|
||||||
|
centered
|
||||||
|
destroyOnClose
|
||||||
|
onCancel={() => setModalOpen(false)}
|
||||||
|
>
|
||||||
|
<div style={{ whiteSpace: 'pre-wrap', wordBreak: 'break-all', lineHeight: 1.8 }}>
|
||||||
|
{raw.replace(/,/g, ' ')}
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* bodyCell 渲染函数
|
* bodyCell 渲染函数
|
||||||
@@ -72,6 +132,12 @@ function renderBodyCell({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 所属区域列
|
||||||
|
if (column.dataIndex === 'area') {
|
||||||
|
const raw = text || '';
|
||||||
|
return raw ? <AreaCell text={raw} /> : <span>--</span>;
|
||||||
|
}
|
||||||
|
|
||||||
// 跳转类型列
|
// 跳转类型列
|
||||||
if (column.dataIndex === 'type') {
|
if (column.dataIndex === 'type') {
|
||||||
return renderJumpType(text);
|
return renderJumpType(text);
|
||||||
|
|||||||
@@ -116,7 +116,7 @@ export function useBannerModel() {
|
|||||||
key: 'type',
|
key: 'type',
|
||||||
width: 120,
|
width: 120,
|
||||||
},
|
},
|
||||||
{ title: '所属区域', dataIndex: 'area', key: 'area', width: 140 },
|
{ title: '所属区域', dataIndex: 'area', key: 'area', width: 300 },
|
||||||
{ title: '展示时间', key: 'displayTime', width: 260 },
|
{ title: '展示时间', key: 'displayTime', width: 260 },
|
||||||
{ title: '创建时间', dataIndex: 'createDate', key: 'createDate', width: 160 },
|
{ title: '创建时间', dataIndex: 'createDate', key: 'createDate', width: 160 },
|
||||||
{ title: '状态', dataIndex: 'status', key: 'status', width: 100, align: 'center' as const },
|
{ title: '状态', dataIndex: 'status', key: 'status', width: 100, align: 'center' as const },
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { defineComponent } from 'vue';
|
|||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Input,
|
Input,
|
||||||
|
InputNumber,
|
||||||
Table,
|
Table,
|
||||||
Form,
|
Form,
|
||||||
Space,
|
Space,
|
||||||
@@ -266,6 +267,45 @@ export default defineComponent({
|
|||||||
onUpdate:value={(val: any) => (filterForm.region = val || [])}
|
onUpdate:value={(val: any) => (filterForm.region = val || [])}
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
<Form.Item label="报名人数">
|
||||||
|
<Space.Compact>
|
||||||
|
<InputNumber
|
||||||
|
placeholder="请输入"
|
||||||
|
style={{ width: '140px' }}
|
||||||
|
value={filterForm.scBegin || undefined}
|
||||||
|
precision={0}
|
||||||
|
step={0}
|
||||||
|
min={0}
|
||||||
|
controls={false}
|
||||||
|
onUpdate:value={(val: any) =>
|
||||||
|
(filterForm.scBegin = val != null ? String(val) : '')
|
||||||
|
}
|
||||||
|
onPressEnter={handleSearch}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
style={{
|
||||||
|
width: '40px',
|
||||||
|
textAlign: 'center',
|
||||||
|
backgroundColor: '#fafafa',
|
||||||
|
pointerEvents: 'none',
|
||||||
|
color: 'rgba(0,0,0,0.45)',
|
||||||
|
}}
|
||||||
|
value="至"
|
||||||
|
disabled
|
||||||
|
/>
|
||||||
|
<InputNumber
|
||||||
|
placeholder="请输入"
|
||||||
|
style={{ width: '140px' }}
|
||||||
|
value={filterForm.scEnd || undefined}
|
||||||
|
precision={0}
|
||||||
|
step={0}
|
||||||
|
min={0}
|
||||||
|
controls={false}
|
||||||
|
onUpdate:value={(val: any) => (filterForm.scEnd = val != null ? String(val) : '')}
|
||||||
|
onPressEnter={handleSearch}
|
||||||
|
/>
|
||||||
|
</Space.Compact>
|
||||||
|
</Form.Item>
|
||||||
<Form.Item>
|
<Form.Item>
|
||||||
<Space>
|
<Space>
|
||||||
<Button type="primary" onClick={handleSearch} loading={loading.value}>
|
<Button type="primary" onClick={handleSearch} loading={loading.value}>
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ export const FILTER_DEFAULTS = {
|
|||||||
isPublic: '',
|
isPublic: '',
|
||||||
idCardNo: '',
|
idCardNo: '',
|
||||||
idCardImg: '',
|
idCardImg: '',
|
||||||
|
scBegin: '',
|
||||||
|
scEnd: '',
|
||||||
};
|
};
|
||||||
|
|
||||||
export type FieldMapping = [
|
export type FieldMapping = [
|
||||||
@@ -35,6 +37,8 @@ export const FIELD_MAPPINGS: FieldMapping[] = [
|
|||||||
['createDateRange', 'createDateEnd', (v) => v?.[1]],
|
['createDateRange', 'createDateEnd', (v) => v?.[1]],
|
||||||
['startDateRange', 'startDate', (v) => v?.[0]],
|
['startDateRange', 'startDate', (v) => v?.[0]],
|
||||||
['startDateRange', 'startDateEnd', (v) => v?.[1]],
|
['startDateRange', 'startDateEnd', (v) => v?.[1]],
|
||||||
|
['scBegin', 'scBegin'],
|
||||||
|
['scEnd', 'scEnd'],
|
||||||
];
|
];
|
||||||
|
|
||||||
/** 赛事状态选项(value 对应 API status) */
|
/** 赛事状态选项(value 对应 API status) */
|
||||||
|
|||||||
@@ -92,11 +92,24 @@ export function useEventListModel() {
|
|||||||
f.region.length > 0 ||
|
f.region.length > 0 ||
|
||||||
!!f.isPublic ||
|
!!f.isPublic ||
|
||||||
!!f.idCardNo ||
|
!!f.idCardNo ||
|
||||||
!!f.idCardImg
|
!!f.idCardImg ||
|
||||||
|
!!f.scBegin?.trim() ||
|
||||||
|
!!f.scEnd?.trim()
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/** 报名人数区间:起始大于结束时对调 */
|
||||||
|
const normalizeSignupCountRange = () => {
|
||||||
|
const begin = filterForm.scBegin.trim();
|
||||||
|
const end = filterForm.scEnd.trim();
|
||||||
|
if (begin && end && Number(begin) > Number(end)) {
|
||||||
|
filterForm.scBegin = end;
|
||||||
|
filterForm.scEnd = begin;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleSearch = () => {
|
const handleSearch = () => {
|
||||||
|
normalizeSignupCountRange();
|
||||||
pagination.reset();
|
pagination.reset();
|
||||||
fetchList();
|
fetchList();
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,10 +1,22 @@
|
|||||||
import { defineComponent, ref, nextTick } from 'vue';
|
import { defineComponent, ref, nextTick } from 'vue';
|
||||||
import { DatePicker, Select, Table } from 'ant-design-vue';
|
import { DatePicker, Select, Table } from 'ant-design-vue';
|
||||||
import * as echarts from 'echarts';
|
import * as echarts from 'echarts/core';
|
||||||
|
import { BarChart, LineChart } from 'echarts/charts';
|
||||||
|
import { GridComponent, TooltipComponent, LegendComponent } from 'echarts/components';
|
||||||
|
import { CanvasRenderer } from 'echarts/renderers';
|
||||||
import { useReportsModel } from './model/useReportsModel';
|
import { useReportsModel } from './model/useReportsModel';
|
||||||
import { useEffect, useContainerSize } from '@/hooks';
|
import { useEffect, useContainerSize } from '@/hooks';
|
||||||
import styles from './index.module.less';
|
import styles from './index.module.less';
|
||||||
|
|
||||||
|
echarts.use([
|
||||||
|
BarChart,
|
||||||
|
LineChart,
|
||||||
|
GridComponent,
|
||||||
|
TooltipComponent,
|
||||||
|
LegendComponent,
|
||||||
|
CanvasRenderer,
|
||||||
|
]);
|
||||||
|
|
||||||
const { RangePicker } = DatePicker;
|
const { RangePicker } = DatePicker;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ export const ACTION_TYPE_OPTIONS = [
|
|||||||
{ value: '3', label: '禁用用户' },
|
{ value: '3', label: '禁用用户' },
|
||||||
{ value: '4', label: '启用用户' },
|
{ value: '4', label: '启用用户' },
|
||||||
{ value: '7', label: '系统参数' },
|
{ value: '7', label: '系统参数' },
|
||||||
|
{ value: '8', label: '用户登录' },
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
/** 操作类型文案映射 */
|
/** 操作类型文案映射 */
|
||||||
@@ -33,6 +34,7 @@ export const ACTION_TYPE_MAP: Record<number, string> = {
|
|||||||
3: '禁用用户',
|
3: '禁用用户',
|
||||||
4: '启用用户',
|
4: '启用用户',
|
||||||
7: '系统参数',
|
7: '系统参数',
|
||||||
|
8: '用户登录',
|
||||||
};
|
};
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|||||||
+3
-3
@@ -34,8 +34,8 @@ export const REAL_NAME_REGEX = /^[\u4e00-\u9fa5A-Za-z0-9·]{1,8}$/;
|
|||||||
export const IMAGE_TYPE_REGEX = /^image\//;
|
export const IMAGE_TYPE_REGEX = /^image\//;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 纯数字(整数)
|
* 非数字字符
|
||||||
* - 用于过滤非数字字符
|
* - 用于过滤 0-9 以外的字符(含小数点、字母、符号等)
|
||||||
*/
|
*/
|
||||||
export const INTEGER_ONLY_REGEX = /\D/g;
|
export const INTEGER_ONLY_REGEX = /\D/g;
|
||||||
|
|
||||||
@@ -63,7 +63,7 @@ export function isImageFile(file: { type: string }): boolean {
|
|||||||
return IMAGE_TYPE_REGEX.test(file.type);
|
return IMAGE_TYPE_REGEX.test(file.type);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 过滤非数字字符,只保留整数 */
|
/** 只保留数字字符 */
|
||||||
export function toIntegerOnly(value: unknown): string {
|
export function toIntegerOnly(value: unknown): string {
|
||||||
return String(value ?? '').replace(INTEGER_ONLY_REGEX, '');
|
return String(value ?? '').replace(INTEGER_ONLY_REGEX, '');
|
||||||
}
|
}
|
||||||
|
|||||||
+15
-3
@@ -7,6 +7,12 @@ import { resolve } from 'path';
|
|||||||
|
|
||||||
// https://vitejs.dev/config/
|
// https://vitejs.dev/config/
|
||||||
export default defineConfig(({ mode }) => {
|
export default defineConfig(({ mode }) => {
|
||||||
|
// 增大 Node 内存限制,防止构建时 OOM
|
||||||
|
const v8Flags = '--max-old-space-size=4096';
|
||||||
|
if (!process.env.NODE_OPTIONS?.includes('max-old-space-size')) {
|
||||||
|
process.env.NODE_OPTIONS = [process.env.NODE_OPTIONS, v8Flags].filter(Boolean).join(' ');
|
||||||
|
}
|
||||||
|
|
||||||
const env = loadEnv(mode, process.cwd(), '');
|
const env = loadEnv(mode, process.cwd(), '');
|
||||||
const isProd = mode === 'production';
|
const isProd = mode === 'production';
|
||||||
|
|
||||||
@@ -54,12 +60,10 @@ export default defineConfig(({ mode }) => {
|
|||||||
build: {
|
build: {
|
||||||
outDir: 'dist',
|
outDir: 'dist',
|
||||||
sourcemap: mode !== 'production',
|
sourcemap: mode !== 'production',
|
||||||
// 生产构建移除 console 和注释(esbuild minify 阶段)
|
|
||||||
minify: isProd ? 'esbuild' : false,
|
minify: isProd ? 'esbuild' : false,
|
||||||
chunkSizeWarningLimit: 1500,
|
chunkSizeWarningLimit: 1500,
|
||||||
rollupOptions: {
|
rollupOptions: {
|
||||||
output: {
|
output: {
|
||||||
// 更细粒度的分包,确保按需加载
|
|
||||||
manualChunks(id) {
|
manualChunks(id) {
|
||||||
if (id.includes('node_modules')) {
|
if (id.includes('node_modules')) {
|
||||||
if (id.includes('ant-design-vue') || id.includes('@ant-design')) {
|
if (id.includes('ant-design-vue') || id.includes('@ant-design')) {
|
||||||
@@ -68,7 +72,15 @@ export default defineConfig(({ mode }) => {
|
|||||||
if (id.includes('vue') || id.includes('@vue') || id.includes('vue-router')) {
|
if (id.includes('vue') || id.includes('@vue') || id.includes('vue-router')) {
|
||||||
return 'vue-vendor';
|
return 'vue-vendor';
|
||||||
}
|
}
|
||||||
// 其他第三方依赖单独打包,未使用的不会进入
|
if (id.includes('echarts')) {
|
||||||
|
return 'echarts';
|
||||||
|
}
|
||||||
|
if (id.includes('china-area-data')) {
|
||||||
|
return 'area-data';
|
||||||
|
}
|
||||||
|
if (id.includes('big.js')) {
|
||||||
|
return 'bigjs';
|
||||||
|
}
|
||||||
return 'vendor';
|
return 'vendor';
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user