搜索结果列表页面开发

This commit is contained in:
付民康
2025-03-12 18:41:20 +08:00
commit 7cebe8fc00
739 changed files with 88149 additions and 0 deletions

View File

@@ -0,0 +1,45 @@
import type { Directive } from 'vue';
const updateTitle = (el:Element) => {
const containerHeight = el.clientHeight;
const containerWidth = el.clientWidth;
// 创建一个临时元素
const tempElement = document.createElement('span');
tempElement.style.visibility = 'hidden';
tempElement.style.position = 'absolute';
tempElement.style.whiteSpace = 'wrap';
tempElement.style.wordBreak = 'break-all';
const style = window.getComputedStyle(el);
tempElement.style.width = containerWidth + 'px';
tempElement.style.fontSize = style.fontSize;
tempElement.style.fontFamily = style.fontFamily;
tempElement.style.lineHeight = style.lineHeight;
tempElement.style.letterSpacing = style.letterSpacing;
tempElement.textContent = el.textContent;
// 将临时元素添加到文档中
document.body.appendChild(tempElement);
const textHeight = tempElement.clientHeight;
// 移除临时元素
document.body.removeChild(tempElement);
if (textHeight > containerHeight) {
el.setAttribute('title', el.textContent + '');
} else {
el.removeAttribute('title');
}
};
export default {
mounted(el) {
el.addEventListener('mouseenter', () => {
updateTitle(el);
});
},
updated(el) {
setTimeout(() => updateTitle(el), 500);
}
} as Directive;

View File

@@ -0,0 +1,38 @@
import type { Directive, VNode, DirectiveBinding } from 'vue';
import { hasClassInParent } from '@/utils';
interface Binding extends DirectiveBinding {
value: {
// 类型
type: string;
// 参数
params?: {
[name: string]: any;
};
// 触发上报函数
trigger: Function;
// 过滤的点击元素
filterClickElement: string[];
};
}
export default {
mounted(el, binding: Binding, vnode: VNode) {
const observer = new IntersectionObserver(function(entries, observer) {
entries.forEach(function(entry) {
if (entry.isIntersecting) { // 当元素进入视口时
binding.value?.trigger('expo');
observer.unobserve(entry.target); // 注销监听
}
});
}, { threshold: 0 });
observer.observe(el);
el.addEventListener('click', (e) => {
if (!hasClassInParent(e.target, binding.value.filterClickElement || [])) { // 剔除预设排除元素的冒泡点击
binding.value?.trigger('click');
}
});
},
beforeUnmount() {
}
} as Directive;

25
src/directives/index.ts Normal file
View File

@@ -0,0 +1,25 @@
import type { App } from 'vue';
import addTitleAtEllipsis from './add-title-at-ellipsis';
import elementExposure from './element-exposure';
import safeHtml from './safe-html';
import xssProtect from './xss-protect';
import { LoadingDirective } from 'vue-devui/loading';
import { fileDropDirective } from 'vue-devui/upload';
// 注册到全局的指令
const GLOBAL_DIRECTIVES = {
'add-title-at-ellipsis': addTitleAtEllipsis,
'element-exposure': elementExposure,
'safe-html': safeHtml,
'xss-protect': xssProtect,
'loading': LoadingDirective,
'file-drop': fileDropDirective
} as { [name: string]: object; };
export default {
install(app: App) {
Object.keys(GLOBAL_DIRECTIVES).forEach((name: string) => {
app.directive(name, GLOBAL_DIRECTIVES[name]);
});
}
};

View File

@@ -0,0 +1,8 @@
import type { Directive, DirectiveBinding, VNode } from 'vue';
import DOMPurify from 'dompurify';
export default {
created(el, binding: DirectiveBinding, vnode: VNode) {
const clean = DOMPurify.sanitize(binding.value || '');
el.innerHTML = clean;
}
} as Directive;

View File

@@ -0,0 +1,67 @@
import type { Directive } from 'vue';
import { watch } from 'vue';
const xssTokenReplace = { // xss repalce rules
'<': '&lt;',
'>': '&gt;'
// '"': '&quot;',
// "'": '&#x27;'
};
function generateReg(replaceRules:Record<string, string>) { // 替换字符查找
const temArr = [];
temArr.push('(');
for (const key in replaceRules) {
temArr.push(key);
temArr.push('|');
}
temArr.length > 1 && temArr.pop();
temArr.push(')');
return new RegExp(temArr.join(''), 'g');
}
const replaceReg = generateReg(xssTokenReplace);
function replaceAll(str:string, replaceRules:Record<string, string>) {
return str.replace(replaceReg, function(match, position, original) { return replaceRules[match]; });
}
const repairMaxLength = function(str:string, maxLength:number) { // 设置最大长度不生效的问题
const len = str.length;
return len > maxLength ? str.slice(0, maxLength) : str;
};
const handlePaste = function(vnode:any, event:any) { // 复制粘贴过滤
const value = event.target.value;
const pos = event.target.selectionStart;
const pasetText = (event.clipboardData || (window as any).clipboardData).getData('text');
const insertStr = replaceAll(pasetText, xssTokenReplace);
let composeStr = value.slice(0, pos) + insertStr + value.slice(pos);
if (vnode.ctx.attrs?.maxlength) composeStr = repairMaxLength(composeStr, vnode.ctx.attrs?.maxlength);
event.target.value = composeStr;
event.target.setSelectionRange(pos + insertStr.length, pos + insertStr.length);// 计算鼠标位置
vnode.ctx.emit('update:modelValue', event.target.value);// 双向绑定更新
event.preventDefault();
};
const handleInput = function(vnode:any, event:any) { // 输入替换
if (replaceReg.test(event.key)) {
const pos = event.target.selectionStart;
const value = event.target.value;
const insertStr = replaceAll(event.key, xssTokenReplace);
let composeStr = value.slice(0, pos) + insertStr + value.slice(pos);
if (vnode.ctx.attrs?.maxlength) composeStr = repairMaxLength(composeStr, vnode.ctx.attrs?.maxlength);
event.target.value = composeStr;
event.target.setSelectionRange(pos + insertStr.length, pos + insertStr.length);// 计算鼠标位置
vnode.ctx.emit('update:modelValue', event.target.value);// 双向绑定更新
event.preventDefault();
}
};
export const xssEscape = (str:string) => replaceAll(str, xssTokenReplace);// 替换渲染中的非法字符如果需要保留v-html的元素渲染请勿使用
let handleMoveInput:any = null;
let handleMovePaste:any = null;
export default {
mounted(el, binding, vnode) {
el.addEventListener('paste', handleMovePaste = handlePaste.bind(null, vnode));
el.addEventListener('keypress', handleMoveInput = handleInput.bind(null, vnode));
},
beforeUnmount(el) {
el.removeEventListener('paste', handleMovePaste);
el.removeEventListener('keypress', handleMoveInput);
}
} as Directive;