PageSourceSearch

https://ttfdg.cn/assets/js/condition-builder.js

js ttfdg.cn collected 2026-10-02 01:20:46 UTC 23,112 bytes, 545 lines download raw bytes

1/**
2 * 条件构建器模块
3 * 功能:提供可视化条件编辑界面,支持手动输入和构建器两种模式
4 * 核心方法:builderRender() - 渲染构建器界面, parseManualCondition() - 解析手动条件
5 * 依赖:ui-utils.js 提供的卡名获取功能
6 */
7// 条件构建器模块
8(function (global) {
9    // ====== 数据结构与工具 ======
10    // 构建器的根条件对象
11    let builderRootCondition = null;
12    // 构建器生成的条件表达式文本
13    let builderConditionText = '';
14    // 当前输入模式(manual/builder)
15    let currentConditionInputMode = 'manual';
16
17    // 运算符映射表
18    const builderOperators = {
19        gt: '>',
20        eq: '==',
21        lt: '<',
22        neq: '!=',
23        gte: '>=',
24        lte: '<='
25    };
26
27    // 获取所有卡名(变量名和自定义名)
28    function getAllCardNames() {
29        const varNames = [];
30        for (let i = 0; i < 52; i++) {
31            const count = document.getElementById(`card${i}`)?.value;
32            const name = document.getElementById(`cardName${i}`)?.value.trim();
33            if ((count && parseInt(count) > 0) || (name && name !== '')) {
34                if (i < 26) {
35                    varNames.push(String.fromCharCode(97 + i));
36                } else {
37                    varNames.push('a' + String.fromCharCode(97 + i - 26));
38                }
39            }
40        }
41        const customNames = [];
42        for (let i = 0; i < 52; i++) {
43            const name = document.getElementById(`cardName${i}`)?.value.trim();
44            if (name && !varNames.includes(name) && !customNames.includes(name)) customNames.push(name);
45        }
46        const allNames = [...customNames, ...varNames];
47        return allNames.length > 0 ? allNames : ['请先输入数量或卡名'];
48    }
49
50    // 创建条件节点对象(单条件或条件组)
51    function builderCreateCondition(type, children) {
52        return type === 'single' ? {
53            type: 'single',
54            cards: [{ name: getAllCardNames()[0] }],
55            symbol: 'gt',
56            num: '0'
57        } : { type, children: children || [] };
58    }
59
60    // 创建概率函数节点
61    function builderCreateProbCondition(value = "50") {
62        return {
63            type: 'prob',
64            value: String(value) // 保持原始字符串值
65        };
66    }
67
68    // ====== 渲染逻辑 ======
69    // 渲染整个条件构建器界面
70    function builderRender() {
71        const builder = document.getElementById('conditionBuilder');
72        if (!builder) return;
73        builder.innerHTML = '';
74        builderRootCondition && builder.appendChild(builderRenderCondition(builderRootCondition, true));
75        builderUpdateOutput();
76    }
77    // 渲染单个条件节点(递归)
78    function builderRenderCondition(condition, isRoot = false) {
79        const container = document.createElement('div');
80        // 概率函数也统一用condition-single样式
81        if (condition.type === 'prob') {
82            container.className = 'condition-builder condition-single';
83            builderRenderProbCondition(condition, container, isRoot);
84        } else {
85            container.className = `condition-builder condition-${condition.type}`;
86            if (condition.type === 'single') {
87                builderRenderSingleCondition(condition, container, isRoot);
88            } else {
89                builderRenderGroupCondition(condition, container, isRoot);
90            }
91        }
92        return container;
93    }
94    // 渲染单条件节点(如“抽到X的数量 > N”)
95    function builderRenderSingleCondition(condition, container, isRoot) {
96        container.appendChild(document.createTextNode('抽到'));
97        const cardsWrapper = document.createElement('div');
98        cardsWrapper.className = 'builder-cards-wrapper';
99        condition.cards.forEach((card, index) => {
100            const cardRow = document.createElement('div');
101            cardRow.className = 'builder-card-row';
102            if (index > 0) {
103                const opSelect = builderCreateSelect(['+', '-', '*', '/'], card.operator || '+',
104                    e => { card.operator = e.target.value; builderUpdateOutput(); });
105                cardRow.appendChild(opSelect);
106            }
107            const cardNameContainer = document.createElement('span');
108            let cardNameControl = builderCreateSelect(
109                getAllCardNames().map(name => ({ display: name, value: name })),
110                card.name,
111                e => { card.name = e.target.value; builderUpdateOutput(); }
112            );
113            cardNameContainer.appendChild(cardNameControl);
114            const toggleButton = builderCreateButton('✏️', () => {
115                if (cardNameControl.tagName.toLowerCase() === 'select') {
116                    const customInput = builderCreateInput(card.name, e => { card.name = e.target.value; builderUpdateOutput(); });
117                    customInput.classList.add('builder-card-input');
118                    cardNameContainer.replaceChild(customInput, cardNameControl);
119                    cardNameControl = customInput;
120                    toggleButton.textContent = '📑';
121                } else {
122                    const newSelect = builderCreateSelect(
123                        getAllCardNames().map(name => ({ display: name, value: name })),
124                        card.name,
125                        e => { card.name = e.target.value; builderUpdateOutput(); }
126                    );
127                    cardNameContainer.replaceChild(newSelect, cardNameControl);
128                    cardNameControl = newSelect;
129                    toggleButton.textContent = '✏️';
130                }
131            });
132            cardRow.appendChild(cardNameContainer);
133            cardRow.appendChild(toggleButton);
134            if (condition.cards.length > 1) {
135                cardRow.appendChild(builderCreateButton('×', () => {
136                    condition.cards.splice(index, 1);
137                    builderRender();
138                }));
139            }
140            cardsWrapper.appendChild(cardRow);
141        });
142        container.appendChild(cardsWrapper);
143        container.appendChild(builderCreateButton('+', () => {
144            condition.cards.push({ name: getAllCardNames()[0], operator: '+' });
145            builderRender();
146        }));
147        container.appendChild(document.createTextNode('的数量'));
148        const symbolSelect = builderCreateSelect(
149            Object.entries(builderOperators).map(([value, display]) => ({ display, value })),
150            condition.symbol,
151            e => { condition.symbol = e.target.value; builderUpdateOutput(); }
152        );
153        container.appendChild(symbolSelect);
154        container.appendChild(builderCreateInput(condition.num, e => { condition.num = e.target.value; builderUpdateOutput(); }, '40px'));
155        !isRoot && container.appendChild(builderCreateButton('删除', () => {
156            container.dispatchEvent(new CustomEvent('delete', { bubbles: true }));
157        }));
158    }
159    // 渲染概率函数节点
160    function builderRenderProbCondition(condition, container, isRoot) {
161        container.appendChild(document.createTextNode('有 '));
162        const valueInput = document.createElement('input');
163        valueInput.type = 'number';
164        valueInput.value = condition.value;
165        valueInput.min = 0;
166        valueInput.max = 100;
167        valueInput.step = 0.01;
168        
169        // 只在失去焦点时进行完整验证
170        valueInput.addEventListener('blur', e => {
171            let value = parseFloat(e.target.value);
172            if (value > 100) value = 100;
173            condition.value = value;
174            e.target.value = value;
175            builderUpdateOutput();
176        });
177
178        // 输入时只做基本验证,不更新condition值
179        valueInput.addEventListener('input', e => {
180            if (e.target.value === '' || e.target.value === '.') return;
181            let value = parseFloat(e.target.value);
182            if (isNaN(value)) {
183                e.target.value = condition.value;
184            }
185        });
186        container.appendChild(valueInput);
187        container.appendChild(document.createTextNode('% 的概率满足此条件'));
188        if (!isRoot) {
189            container.appendChild(builderCreateButton('删除', () => {
190                container.dispatchEvent(new CustomEvent('delete', { bubbles: true }));
191            }));
192        }
193    }
194    // 渲染条件组节点(如“全部满足/任一满足”)
195    function builderRenderGroupCondition(condition, container, isRoot) {
196        const header = document.createElement('div');
197        header.className = 'builder-group-header';
198        header.appendChild(document.createTextNode('满足以下'));
199        header.appendChild(builderCreateSelect(
200            ['全部', '任一'].map((text, i) => ({ display: text, value: i === 0 ? 'and' : 'or' })),
201            condition.type,
202            e => { condition.type = e.target.value; builderUpdateOutput(); }
203        ));
204        header.appendChild(document.createTextNode('条件'));
205        if (!isRoot) header.appendChild(builderCreateButton('删除', () => {
206            container.dispatchEvent(new CustomEvent('delete', { bubbles: true }));
207        }));
208        container.appendChild(header);
209        const childrenContainer = document.createElement('div');
210        childrenContainer.className = 'builder-group-children';
211        condition.children.forEach(child => {
212            const childElement = builderRenderCondition(child);
213            childElement.addEventListener('delete', e => {
214                e.stopPropagation();
215                const index = condition.children.indexOf(child);
216                index !== -1 && condition.children.splice(index, 1) && builderRender();
217            });
218            childrenContainer.appendChild(childElement);
219        });
220        const buttons = document.createElement('div');
221        buttons.className = 'builder-buttons';
222        buttons.appendChild(builderCreateButton('添加条件组', () => {
223            condition.children.push(builderCreateCondition('and', []));
224            builderRender();
225        }));
226        buttons.appendChild(builderCreateButton('添加条件', 
226() => {
227            condition.children.push(builderCreateCondition('single'));
228            builderRender();
229        }));
230        const probBtn = builderCreateButton('添加概率函数', () => {
231            condition.children.push(builderCreateProbCondition());
232            builderRender();
233        });
234        probBtn.className += ' btn--auto-width';
235        buttons.appendChild(probBtn);
236        childrenContainer.appendChild(buttons);
237        container.appendChild(childrenContainer);
238    }
239    // 更新条件表达式文本框和预览
240    function builderUpdateOutput() {
241        builderConditionText = builderRootCondition ? builderGenerateConditionText(builderRootCondition) : '';
242        if (document.querySelector('input[name="conditionInputMode"]:checked')?.value === 'builder') {
243            document.getElementById('condition').value = builderConditionText;
244            const preview = document.getElementById('builderConditionPreview');
245            if (preview) preview.value = builderConditionText;
246        }
247    }
248    // 生成条件表达式字符串,支持prob类型
249    function builderGenerateConditionText(condition) {
250        if (condition.type === 'single') {
251            const cardsText = condition.cards.map((c, i) =>
252                i === 0 ? c.name : `${c.operator || '+'} ${c.name}`).join(' ');
253            const operator = builderOperators[condition.symbol] || condition.symbol || '';
254            return `(${cardsText}) ${operator} ${condition.num}`;
255        } else if (condition.type === 'prob') {
256            return `PROB(${condition.value})`; // 直接输出原始值
257        }
258        const childrenText = condition.children.map(builderGenerateConditionText).filter(Boolean);
259        return childrenText.length > 1
260            ? `(${childrenText.join(condition.type === 'and' ? ' && ' : ' || ')})`
261            : childrenText[0] || '';
262    }
263    // 创建下拉框控件
264    function builderCreateSelect(options, value, onChange) {
265        const select = document.createElement('select');
266        options.forEach(opt => {
267            const option = document.createElement('option');
268            option.value = opt.value || opt;
269            option.textContent = opt.display || opt;
270            select.appendChild(option);
271        });
272        select.value = value;
273        select.addEventListener('change', onChange);
274        return select;
275    }
276    // 创建输入框控件
277    function builderCreateInput(value, onChange, width) {
278        const input = document.createElement('input');
279        input.type = 'text';
280        input.value = value;
281        input.setAttribute('list', 'cardNamesDatalist');
282        if (width) input.style.width = width;
283        input.addEventListener('input', onChange);
284        return input;
285    }
286    // 创建按钮控件
287    function builderCreateButton(text, onClick) {
288        const button = document.createElement('button');
289        button.type = 'button';
290        button.textContent = text;
291        let variant = 'btn--outline';
292        if (text.trim() === '×' || text.trim() === '删除') {
293            variant = 'btn--danger';
294        }
295        button.className = `btn ${variant}`;
296        if (text.trim() === '添加条件组' || text.trim() === '添加条件') {
297            button.className += ' btn--auto-width';
298        }
299        button.addEventListener('click', onClick);
300        return button;
301    }
302
303    // ====== 手动条件解析器 ======
304    // 将表达式字符串分割为词法单元
305    function tokenize(expr) {
306        const regex = /\s*([A-Za-z0-9\u4e00-\u9fa5]+|>=|<=|==|!=|&&|\|\||[-+*/()<>])\s*/g;
307        let tokens = [];
308        let m;
309        while ((m = regex.exec(expr)) !== null) {
310            tokens.push(m[1]);
311        }
312        return tokens;
313    }
314    // 简单表达式解析器构造函数
315    function Parser(tokens) {
316        this.tokens = tokens;
317        this.pos = 0;
318    }
319    // 查看下一个token
320    Parser.prototype.peek = function () { return this.tokens[this.pos]; };
321    // 消耗一个token
322    Parser.prototype.consume = function (token) {
323        if (token && this.tokens[this.pos] !== token) {
324            throw new Error("预期 " + token + ",但得到 " + this.tokens[this.pos]);
325        }
326        return this.tokens[this.pos++];
327    };
328    // 判断是否到达末尾
329    Parser.prototype.eof = function () { return this.pos >= this.tokens.length; };
330    // 解析表达式入口
331    function parseExpression(parser) {
332        return parseLogicalOr(parser);
333    }
334    // 解析或表达式
335    function parseLogicalOr(parser) {
336        let node = parseLogicalAnd(parser);
337        while (!parser.eof() && parser.peek() === '||') {
338            parser.consume('||');
339            const right = parseLogicalAnd(parser);
340            node = { type: "or", children: [node, right] };
341        }
342        return node;
343    }
344    // 解析与表达式
345    function parseLogicalAnd(parser) {
346        let node = parseRelational(parser);
347        while (!parser.eof() && parser.peek() === '&&') {
348            parser.consume('&&');
349            const right = parseRelational(parser);
350            node = { type: "and", children: [node, right] };
351        }
352        return node;
353    }
354    // 解析关系表达式
355    function parseRelational(parser) {
356        let left = parseSum(parser);
357        if (!parser.eof() && /^(>
357=|<=|==|!=|>|<)$/.test(parser.peek())) {
358            const op = parser.consume();
359            const num = parser.consume();
360            if (!/^\d+$/.test(num)) {
361                throw new Error("预期数字,但得到 " + num);
362            }
363            let cards = [];
364            if (Array.isArray(left)) {
365                cards.push({ name: left[0] });
366                for (let i = 1; i < left.length; i += 2) {
367                    let operator = left[i];
368                    let operand = left[i + 1];
369                    cards.push({ operator, name: operand });
370                }
371            } else {
372                cards.push({ name: left });
373            }
374            return { type: "single", cards: cards, symbol: mapOperator(op), num: num };
375        }
376        return left;
377    }
378    // 解析加减表达式
379    function parseSum(parser) {
380        if (parser.peek() === '(') {
381            parser.consume('(');
382            const node = parseExpression(parser);
383            parser.consume(')');
384            return node;
385        }
386        // 支持PROB函数型表达式递归解析
387        if (parser.peek() === 'PROB') {
388            parser.consume('PROB');
389            parser.consume('(');
390            const value = parser.consume();
391            if (!/^\d+(\.\d+)?$/.test(value)) {
392                throw new Error("PROB函数参数必须是数字");
393            }
394            parser.consume(')');
395            return { type: "prob", value: value }; // 保持原始字符串值
396        }
397        let items = [];
398        items.push(parser.consume());
399        while (!parser.eof() && (parser.peek() === '+' || parser.peek() === '-')) {
400            let operator = parser.consume();
401            items.push(operator);
402            items.push(parser.consume());
403        }
404        return items.length === 1 ? items[0] : items;
405    }
406    // 运算符映射(字符串转内部标识)
407    function mapOperator(op) {
408        const opMap = {
409            ">": "gt",
410            "<": "lt",
411            "==": "eq",
412            "!=": "neq",
413            ">=": "gte",
414            "<=": "lte",
415            "大于等于": "gte",
416            "小于等于": "lte",
417            "大于": "gt",
418            "小于": "lt"
419        };
420        if (!opMap[op]) throw new Error("不支持的运算符:" + op);
421        return opMap[op];
422    }
423    // 解析手动输入条件为构建器结构
424    function parseManualCondition(manualStr) {
425        manualStr = manualStr.trim();
426        if (!manualStr) throw new Error("空的条件");
427        const tokens = tokenize(manualStr);
428        const parser = new Parser(tokens);
429        const tree = parseExpression(parser);
430        if (!parser.eof()) {
431            throw new Error("无法解析条件:" + manualStr);
432        }
433        // 修改:无论何种类型都包裹在and条件组下,保证有母节点
434        if (tree && tree.type === 'and') {
435            return tree;
436        } else {
437            return { type: 'and', children: [tree] };
438        }
439    }
440
441    // ====== 输入模式切换 ======
442    // 切换条件输入模式(手动/构建器)
443    function switchConditionInputMode(mode, skipConfirm = false) {
444        const currentCondition = document.getElementById('condition').value.trim();
445        if (!currentCondition) { skipConfirm = true; }
446        if (!skipConfirm) {
447            let msg = "";
448            if (mode === 'builder') {
449                msg = "将当前逻辑判断条件转化至条件构建器时优先级嵌套可能会混乱,不支持转化字符 */%,是否继续切换到条件构建器输入?";
450            } else if (mode === 'manual') {
451                msg = "是否转化至手动输入?";
452            }
453            if (!confirm(msg)) return false;
454        }
455        const manualDiv = document.getElementById('manualConditionInput');
456        const builderDiv = document.getElementById('builderConditionInput');
457        if (mode === 'manual') {
458            if (builderRootCondition) {
459                const raw = builderGenerateConditionText(builderRootCondition);
460                document.getElementById('condition').value = raw;
461            }
462            builderDiv.classList.add('hidden');
463            manualDiv.classList.remove('hidden');
464        } else {
465            let manualStr = document.getElementById('condition').value.trim();
466            if (manualStr) {
467                try {
468                    builderRootCondition = parseManualCondition(manualStr);
469                    builderRender();
470                } catch (err) {
471                    alert("手动条件转换失败:" + err.message);
472                    document.querySelector('input[name="conditionInputMode"][value="manual"]').checked = true;
473                    return false;
474                }
475            } else {
476                builderRootCondition = builderCreateCondition('and', []);
477                builderRender();
478            }
479            manualDiv.classList.add('hidden');
480            builderDiv.classList.remove('hidden');
481        }
482        currentConditionInputMode = mode;
483        return true;
484    }
485
486    // ====== 事件绑定与初始化 ======
487    // 绑定输入模式切换事件
488    function bindModeSwitch() {
489        document.querySelectorAll('input[name="conditionInputMode"]').forEach(radio => {
490            radio.addEventListener('change', function (e) {
491                const newMode = e.target.value;
492                if (!switchConditionInputMode(newMode)) {
493                    document.querySelector(`input[name="conditionInputMode"][value="${currentConditionInputMode}"]`).checked = true;
494                }
495            });
496        });
497    }
498    // 绑定卡名输入监听,卡名变化时刷新下拉
499    function bindCardNameListener() {
500        if (window.UIUtils && window.UIUtils.setupCardNameInputListener) {
501            window.UIUtils.setupCardNameInputListener();
502        }
503        // 监听卡名和数量输入变化,实时刷新条件构建器下拉
504        const cardInputs = document.getElementById('cardInputs');
505        if (cardInputs) {
506            cardInputs.addEventListener('input', function (e) {
507                if (e.target && e.target.id &&
508                    (e.target.id.startsWith('cardName') || e.target.id.startsWith('card'))) {
509                    if (document.querySelector('input[name="conditionInputMode"]:checked')?.value === 'builder') {
510                        builderRender();
511                    }
512                }
513            });
514        }
515    }
516    // 初始化条件构建器
517    function init() {
518        builderRootCondition = builderCreateCondition('and', []);
519        builderRender();
520        switchConditionInputMode('manual', true);
521        bindModeSwitch();
522        bindCardNameListener();
523    }
524
525    // ====== 对外接口 ======
526    // 提供初始化、获取/设置条件等接口
527    global.ConditionBuilder = {
528        init,
529        getConditionInputMode: function () {
530            return document.querySelector('input[name="conditionInputMode"]:checked')?.value || 'manual';
531        },
532        getBuilderConditionData: function () {
533            return builderRootCondition ? JSON.stringify(builderRootCondition) : '';
534        },
535        setBuilderConditionData: function (json) {
536            try {
537                builderRootCondition = JSON.parse(json);
538                builderRender();
539            } catch (e) {
540                builderRootCondition = builderCreateCondition('and', []);
541                builderRender();
542            }
543        }
544    };
545})(window);

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.