绿满眶商城微信小程序-uniapp
zxl
2025-07-11 01439f14432ee731be561d193c88e3ea31399345
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
/**
 * 富文本解析工具
 * @author sonve
 * @version 1.0.0
 * @date 2024-12-04
 */
 
import config from './config.js'
 
/**
 * 将含有封面占位图形式的视频富文本转换成正常视频的富文本
 * @param {String} richText 要进行处理的富文本字符串
 * @returns {String} 返回处理结果
 */
export function parseHtmlWithVideo(richText) {
    // 正则表达式匹配<img>标签及其属性
    const imgRegex = /<img\s+([^>]+)>/gi;
    // 正则表达式匹配data-custom属性中的url值
    const customUrlRegex = /\bdata-custom="[^"]*url=([^&"]+)/i;
 
    return richText.replace(imgRegex, (match, attrs) => {
        // 查找data-custom属性中的url值
        const urlMatch = attrs.match(customUrlRegex);
        if (urlMatch) {
            // 获取data-custom中的url
            const videoUrl = urlMatch[1];
 
            // 解析出所有属性
            const attrArray = attrs.split(/\s+/).filter(attr => attr.trim() !== '');
 
            // 过滤掉src属性和data-custom属性
            const newAttrs = attrArray.filter(attr => !attr.startsWith('src=') && !attr.startsWith('data-custom='))
                .join(' ');
 
            // 构建新的video标签,保留原有的其他属性,但去除src和data-custom
            return `<video controls ${newAttrs}><source src="${videoUrl}" /></video>`;
        }
        // 如果没有匹配到data-custom中的url,则保持原样
        return match;
    });
}
 
/**
 * 带有视频的富文本逆向转换
 * @description 可自定义处理封面
 * @param {Promise} richText 要转换的富文本
 * @param {Function<Promise>} customCallback 自定义处理封面回调,需要return封面图片资源,自带参数为视频地址
 * @returns {Promise} 转换后的富文本 注意异步处理
 */
export async function replaceVideoWithImageRender(richText, customCallback) {
 
    // 正则表达式用于匹配 <video> 标签以及其内部的 <source> 标签
    const videoRegex = /<video\s+([^>]+)>(.*?)<\/video>/gi;
 
    // 找到所有的 <video> 标签
    const matches = [];
    let match;
    while ((match = videoRegex.exec(richText)) !== null) {
        matches.push(match);
    }
 
    // 并行处理每个 <video> 标签,生成对应的缩略图
    const replacements = await Promise.all(
        matches.map(async (match) => {
            const [fullMatch, attributes, content] = match;
 
            // 匹配 <source> 标签中的 src 属性
            const sourceRegex = /<source\s+[^>]*src="([^">]+)"/i;
            const matchSource = content.match(sourceRegex);
 
            let videoUrl = '';
            if (matchSource && matchSource.length > 1) {
                videoUrl = matchSource[1];
            }
 
            // 生成视频封面图
            let thumbnailRes
            if (customCallback) thumbnailRes = await customCallback(videoUrl) // 自定义封面处理
            if (!thumbnailRes) thumbnailRes = config.video_thumbnail // 无效值则默认封面处理
 
            // 过滤掉不需要的属性,例如 controls
            const filteredAttributes = attributes
                .split(/\s+/)
                .filter(attr => !attr.startsWith('controls'))
                .join(' ');
 
            // 构建新的 img 标签,继承 video 的属性(除了 controls)并添加 data-custom 属性
            const imgTag = `<img ${filteredAttributes} src="${thumbnailRes}" data-custom="url=${videoUrl}" />`;
 
            return { fullMatch, imgTag };
        }));
 
    // 使用 replacements 替换原始的 <video> 标签
    let result = richText;
    for (const { fullMatch, imgTag } of replacements) {
        result = result.replace(fullMatch, imgTag);
    }
 
    return result;
}
 
/**
 * 解析出富文本中的图片和视频
 * @param {String} richText 要解析的富文本
 * @returns {Array} 图片和视频数组
 */
export function parseImagesAndVideos(richText) {
    // 创建一个空数组用于存储图片和视频信息
    const result = [];
 
    // 正则表达式匹配 <img> 标签及其属性
    const imgRegex = /<img\s+[^>]*>/gi;
    // 匹配属性名和值的正则表达式,改进后的版本可以处理属性名中包含连字符的情况
    const attrRegex = /(\w+(-\w+)*)\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'<>]+))/gi;
 
    // 找到所有的 <img> 标签
    const matches = richText.match(imgRegex);
    // 如果没有找到任何 <img> 标签,返回空数组
    if (!matches) return [];
 
    // 遍历所有的 <img> 标签
    matches.forEach(match => {
        // 创建一个对象用于存储单个图片或视频的信息
        const ivInfo = {};
        // 使用正则表达式匹配每个 <img> 标签的属性
        let attrsMatch;
        while ((attrsMatch = attrRegex.exec(match)) !== null) {
            // 属性名
            const name = attrsMatch[1].toLowerCase();
            // 属性值可能存在于第三、第四或第五个捕获组中
            let value = attrsMatch[3] || attrsMatch[4] || attrsMatch[5] || '';
 
            // 去除属性值两端可能存在的引号
            if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
                value = value.substring(1, value.length - 1);
            }
 
            // 将属性名和值添加到 ivInfo 对象中
            ivInfo[name] = value;
        }
        // 将单个图片或视频信息添加到数组中
        result.push(ivInfo);
    });
 
    // 返回包含所有图片和视频信息的数组
    return result;
}
 
/**
 * 解析出富文本中的图片
 * @param {String} richText 要解析的富文本
 * @returns {Array} 图片数组
 */
export function parseImages(richText) {
    let result = []
    const ivList = parseImagesAndVideos(richText)
    ivList.forEach(item => {
        if (!item['data-custom'] || !item['data-custom'].startsWith('url')) {
            result.push(item)
        }
    })
    return result
}
 
/**
 * 解析出富文本中的视频
 * @param {String} richText 要解析的富文本
 * @returns {Array} 视频数组
 */
export function parseVideos(richText) {
    let result = []
    const ivList = parseImagesAndVideos(richText)
    ivList.forEach(item => {
        if (item['data-custom'] && item['data-custom'].startsWith('url')) {
            result.push(item)
        }
    })
    return result
}