From 17c03b4311b9a8d03c637c50348f099e2f6ebecb Mon Sep 17 00:00:00 2001 From: Chuian Date: Fri, 10 May 2024 11:21:41 +0800 Subject: [PATCH] add warning --- components/calculator/index.vue | 6 +- components/calculator/lodash.js | 296 +++++++++++++++++++++++++++ components/calculator/retirement.vue | 25 +-- 3 files changed, 310 insertions(+), 17 deletions(-) create mode 100644 components/calculator/lodash.js diff --git a/components/calculator/index.vue b/components/calculator/index.vue index 60cc49a..ddee28a 100644 --- a/components/calculator/index.vue +++ b/components/calculator/index.vue @@ -216,6 +216,9 @@ function setUserAndInitialize(form, { showMessage = false }) { Object.assign(userForm.asset, form.investment) } } + if (showMessage) { + ElMessage.success('載入成功') + } nextTick(async () => { await ProfileRef.value.calculateProfile({ propagate: true, @@ -238,9 +241,6 @@ function setUserAndInitialize(form, { showMessage = false }) { await MortgageRef.value.calculateMortgage({ propagate: true, }) - if (showMessage) { - ElMessage.success('載入成功') - } window.scrollTo(0, 0) }) } diff --git a/components/calculator/lodash.js b/components/calculator/lodash.js new file mode 100644 index 0000000..7880172 --- /dev/null +++ b/components/calculator/lodash.js @@ -0,0 +1,296 @@ +function isObject(value) { + const type = typeof value; + return value != null && (type === 'object' || type === 'function'); +} + +/** Detect free variable `global` from Node.js. */ +const freeGlobal = typeof global === 'object' && global !== null && global.Object === Object && global + +/** Detect free variable `globalThis` */ +const freeGlobalThis = typeof globalThis === 'object' && globalThis !== null && globalThis.Object === Object && globalThis + +/** Detect free variable `self`. */ +const freeSelf = typeof self === 'object' && self !== null && self.Object === Object && self + +/** Used as a reference to the global object. */ +const root = freeGlobalThis || freeGlobal || freeSelf || Function('return this')() + +/** + * Creates a debounced function that delays invoking `func` until after `wait` + * milliseconds have elapsed since the last time the debounced function was + * invoked, or until the next browser frame is drawn. The debounced function + * comes with a `cancel` method to cancel delayed `func` invocations and a + * `flush` method to immediately invoke them. Provide `options` to indicate + * whether `func` should be invoked on the leading and/or trailing edge of the + * `wait` timeout. The `func` is invoked with the last arguments provided to the + * debounced function. Subsequent calls to the debounced function return the + * result of the last `func` invocation. + * + * **Note:** If `leading` and `trailing` options are `true`, `func` is + * invoked on the trailing edge of the timeout only if the debounced function + * is invoked more than once during the `wait` timeout. + * + * If `wait` is `0` and `leading` is `false`, `func` invocation is deferred + * until the next tick, similar to `setTimeout` with a timeout of `0`. + * + * If `wait` is omitted in an environment with `requestAnimationFrame`, `func` + * invocation will be deferred until the next frame is drawn (typically about + * 16ms). + * + * See [David Corbacho's article](https://css-tricks.com/debouncing-throttling-explained-examples/) + * for details over the differences between `debounce` and `throttle`. + * + * @since 0.1.0 + * @category Function + * @param {Function} func The function to debounce. + * @param {number} [wait=0] + * The number of milliseconds to delay; if omitted, `requestAnimationFrame` is + * used (if available). + * @param {Object} [options={}] The options object. + * @param {boolean} [options.leading=false] + * Specify invoking on the leading edge of the timeout. + * @param {number} [options.maxWait] + * The maximum time `func` is allowed to be delayed before it's invoked. + * @param {boolean} [options.trailing=true] + * Specify invoking on the trailing edge of the timeout. + * @returns {Function} Returns the new debounced function. + * @example + * + * // Avoid costly calculations while the window size is in flux. + * jQuery(window).on('resize', debounce(calculateLayout, 150)) + * + * // Invoke `sendMail` when clicked, debouncing subsequent calls. + * jQuery(element).on('click', debounce(sendMail, 300, { + * 'leading': true, + * 'trailing': false + * })) + * + * // Ensure `batchLog` is invoked once after 1 second of debounced calls. + * const debounced = debounce(batchLog, 250, { 'maxWait': 1000 }) + * const source = new EventSource('/stream') + * jQuery(source).on('message', debounced) + * + * // Cancel the trailing debounced invocation. + * jQuery(window).on('popstate', debounced.cancel) + * + * // Check for pending invocations. + * const status = debounced.pending() ? "Pending..." : "Ready" + */ +export function debounce(func, wait, options) { + console.log(func, wait) + let lastArgs; + let lastThis; + let maxWait; + let result; + let timerId; + let lastCallTime; + let lastInvokeTime = 0; + let leading = false; + let maxing = false; + let trailing = true; + + // Bypass `requestAnimationFrame` by explicitly setting `wait=0`. + const useRAF = !wait && wait !== 0 && typeof root.requestAnimationFrame === 'function'; + + if (typeof func !== 'function') { + throw new TypeError('Expected a function'); + } + wait = +wait || 0; + if (isObject(options)) { + leading = !!options.leading; + maxing = 'maxWait' in options; + maxWait = maxing ? Math.max(+options.maxWait || 0, wait) : maxWait; + trailing = 'trailing' in options ? !!options.trailing : trailing; + } + + function invokeFunc(time) { + const args = lastArgs; + const thisArg = lastThis; + + lastArgs = lastThis = undefined; + lastInvokeTime = time; + result = func.apply(thisArg, args); + return result; + } + + function startTimer(pendingFunc, milliseconds) { + if (useRAF) { + root.cancelAnimationFrame(timerId); + return root.requestAnimationFrame(pendingFunc); + } + // eslint-disable-next-line @typescript-eslint/no-implied-eval + return setTimeout(pendingFunc, milliseconds); + } + + function cancelTimer(id) { + if (useRAF) { + root.cancelAnimationFrame(id); + return; + } + clearTimeout(id); + } + + function leadingEdge(time) { + // Reset any `maxWait` timer. + lastInvokeTime = time; + // Start the timer for the trailing edge. + timerId = startTimer(timerExpired, wait); + // Invoke the leading edge. + return leading ? invokeFunc(time) : result; + } + + function remainingWait(time) { + const timeSinceLastCall = time - lastCallTime; + const timeSinceLastInvoke = time - lastInvokeTime; + const timeWaiting = wait - timeSinceLastCall; + + return maxing ? Math.min(timeWaiting, maxWait - timeSinceLastInvoke) : timeWaiting; + } + + function shouldInvoke(time) { + const timeSinceLastCall = time - lastCallTime; + const timeSinceLastInvoke = time - lastInvokeTime; + + // Either this is the first call, activity has stopped and we're at the + // trailing edge, the system time has gone backwards and we're treating + // it as the trailing edge, or we've hit the `maxWait` limit. + return ( + lastCallTime === undefined || + timeSinceLastCall >= wait || + timeSinceLastCall < 0 || + (maxing && timeSinceLastInvoke >= maxWait) + ); + } + + function timerExpired() { + const time = Date.now(); + if (shouldInvoke(time)) { + return trailingEdge(time); + } + // Restart the timer. + timerId = startTimer(timerExpired, remainingWait(time)); + return undefined; + } + + function trailingEdge(time) { + timerId = undefined; + + // Only invoke if we have `lastArgs` which means `func` has been + // debounced at least once. + if (trailing && lastArgs) { + return invokeFunc(time); + } + lastArgs = lastThis = undefined; + return result; + } + + function cancel() { + if (timerId !== undefined) { + cancelTimer(timerId); + } + lastInvokeTime = 0; + lastArgs = lastCallTime = lastThis = timerId = undefined; + } + + function flush() { + return timerId === undefined ? result : trailingEdge(Date.now()); + } + + function pending() { + return timerId !== undefined; + } + + function debounced(...args) { + const time = Date.now(); + const isInvoking = shouldInvoke(time); + + lastArgs = args; + lastThis = this; + lastCallTime = time; + + if (isInvoking) { + if (timerId === undefined) { + return leadingEdge(lastCallTime); + } + if (maxing) { + // Handle invocations in a tight loop. + timerId = startTimer(timerExpired, wait); + return invokeFunc(lastCallTime); + } + } + if (timerId === undefined) { + timerId = startTimer(timerExpired, wait); + } + return result; + } + debounced.cancel = cancel; + debounced.flush = flush; + debounced.pending = pending; + return debounced; +} + +/** + * Creates a throttled function that only invokes `func` at most once per + * every `wait` milliseconds (or once per browser frame). The throttled function + * comes with a `cancel` method to cancel delayed `func` invocations and a + * `flush` method to immediately invoke them. Provide `options` to indicate + * whether `func` should be invoked on the leading and/or trailing edge of the + * `wait` timeout. The `func` is invoked with the last arguments provided to the + * throttled function. Subsequent calls to the throttled function return the + * result of the last `func` invocation. + * + * **Note:** If `leading` and `trailing` options are `true`, `func` is + * invoked on the trailing edge of the timeout only if the throttled function + * is invoked more than once during the `wait` timeout. + * + * If `wait` is `0` and `leading` is `false`, `func` invocation is deferred + * until the next tick, similar to `setTimeout` with a timeout of `0`. + * + * If `wait` is omitted in an environment with `requestAnimationFrame`, `func` + * invocation will be deferred until the next frame is drawn (typically about + * 16ms). + * + * See [David Corbacho's article](https://css-tricks.com/debouncing-throttling-explained-examples/) + * for details over the differences between `throttle` and `debounce`. + * + * @since 0.1.0 + * @category Function + * @param {Function} func The function to throttle. + * @param {number} [wait=0] + * The number of milliseconds to throttle invocations to; if omitted, + * `requestAnimationFrame` is used (if available). + * @param {Object} [options={}] The options object. + * @param {boolean} [options.leading=true] + * Specify invoking on the leading edge of the timeout. + * @param {boolean} [options.trailing=true] + * Specify invoking on the trailing edge of the timeout. + * @returns {Function} Returns the new throttled function. + * @example + * + * // Avoid excessively updating the position while scrolling. + * jQuery(window).on('scroll', throttle(updatePosition, 100)) + * + * // Invoke `renewToken` when the click event is fired, but not more than once every 5 minutes. + * const throttled = throttle(renewToken, 300000, { 'trailing': false }) + * jQuery(element).on('click', throttled) + * + * // Cancel the trailing throttled invocation. + * jQuery(window).on('popstate', throttled.cancel) + */ +export function throttle(func, wait, options) { + let leading = true; + let trailing = true; + + if (typeof func !== 'function') { + throw new TypeError('Expected a function'); + } + if (isObject(options)) { + leading = 'leading' in options ? !!options.leading : leading; + trailing = 'trailing' in options ? !!options.trailing : trailing; + } + return debounce(func, wait, { + leading, + trailing, + maxWait: wait, + }); +} \ No newline at end of file diff --git a/components/calculator/retirement.vue b/components/calculator/retirement.vue index 13e1c87..a84d190 100644 --- a/components/calculator/retirement.vue +++ b/components/calculator/retirement.vue @@ -228,6 +228,7 @@ import { ref, computed, shallowRef, reactive } from 'vue' import Chart from 'chart.js/auto'; import econSelect from '../econSelect.vue' import { ElMessage, } from 'element-plus' +import { throttle, debounce } from './lodash.js' interface IOptionItem { label: string, value: string | number | boolean, @@ -342,7 +343,7 @@ function calculateRetirement(options: any = { propagate: true }) { calculateRetirementExpense() const { propagate = true } = options - debounce(() => { + customDebounce(() => { drawRetirementAssetChart(propagate) })(propagate) } @@ -562,11 +563,6 @@ async function drawRetirementAssetChart(propagate = false) { // 未還完的房貸支出 const simYear = currentYear + yearToRetirement + i const annualRepay = monthlyRepay * 12 - console.log({ - downpayYear, - loanTerm, - simYear - }) if (loanEndYear >= simYear) { pmt -= annualRepay mortgageData.push([0, -annualRepay]) @@ -581,9 +577,11 @@ async function drawRetirementAssetChart(propagate = false) { labels.push(calculatedYear) pv = fv } - // if (fv <= 0) { - // ElMessage.error('退休,晚節不保!') - // } + if (fv <= 0) { + if (!errorMssage.pending()) { + errorMssage() + } + } // 繪圖 const tension = 0.5 const datasets = [ @@ -607,10 +605,6 @@ async function drawRetirementAssetChart(propagate = false) { } ] const hasMortgage = mortgageData.some(data => data[1]) - console.log({ - mortgageData, - hasMortgage - }) if (hasMortgage) { datasets.push({ label: '房貸剩餘利息', @@ -650,6 +644,9 @@ async function drawRetirementAssetChart(propagate = false) { }) pensionChartInstance = shallowRef(chartInstance) } +const errorMssage = throttle(() => { + ElMessage.warning('退休,晚節不保!') +}, 4000) function calculateLaborPensionLumpSum(fv = 0) { retirement.value.pension.lumpSum = Number(fv) const { futureSeniority } = retirement.value.insurance @@ -664,7 +661,7 @@ function calculateLaborPensionLumpSum(fv = 0) { } const debounceId = ref() -function debounce(func, delay = 100) { +function customDebounce(func, delay = 100) { return (immediate) => { clearTimeout(debounceId.value) if (immediate) {