diff --git a/package.json b/package.json index cd87a5fae..4c7f19ce9 100644 --- a/package.json +++ b/package.json @@ -34,12 +34,12 @@ "@rsdoctor/rspack-plugin": "^1.5.2", "@rspack/cli": "^1.7.6", "@rspack/core": "^1.7.6", - "@swc/core": "^1.15.13", + "@swc/core": "^1.15.17", "@swc/jest": "^0.2.39", "@types/chrome": "0.1.37", "@types/decompress": "^4.2.7", "@types/jest": "^30.0.0", - "@types/node": "^25.3.1", + "@types/node": "^25.3.2", "@types/punycode": "^2.1.4", "@vue/babel-plugin-jsx": "^2.0.1", "babel-loader": "^10.0.0", @@ -64,7 +64,7 @@ "dependencies": { "@element-plus/icons-vue": "^2.3.2", "echarts": "^6.0.0", - "element-plus": "2.13.2", + "element-plus": "2.13.3", "punycode": "^2.3.1", "typescript-guard": "^0.2.1", "vue": "^3.5.29", diff --git a/src/content-script/limit/modal/components/Alert.tsx b/src/content-script/limit/modal/components/Alert.tsx index b8d96977e..af0ce41d2 100644 --- a/src/content-script/limit/modal/components/Alert.tsx +++ b/src/content-script/limit/modal/components/Alert.tsx @@ -1,5 +1,6 @@ import { getUrl } from "@api/chrome/runtime" import { t } from "@cs/locale" +import { useXsState } from '@hooks/useMediaSize' import { useRequest } from "@hooks/useRequest" import Box from '@pages/components/Box' import Flex from '@pages/components/Flex' @@ -21,13 +22,19 @@ const _default = defineComponent(() => { return option?.limitPrompt || defaultPrompt }, { defaultValue: defaultPrompt }) + const isXs = useXsState() + return () => ( {t(msg => msg.meta.name)?.toUpperCase()} - + {prompt.value} diff --git a/src/content-script/limit/modal/components/Reason.tsx b/src/content-script/limit/modal/components/Reason.tsx index 78bbe160a..1dc2204ec 100644 --- a/src/content-script/limit/modal/components/Reason.tsx +++ b/src/content-script/limit/modal/components/Reason.tsx @@ -1,4 +1,5 @@ import { t } from "@cs/locale" +import { useXsState } from '@hooks/index' import { useRequest } from "@hooks/useRequest" import Flex from "@pages/components/Flex" import { matchCond, meetLimit, meetTimeLimit, period2Str } from "@util/limit" @@ -7,8 +8,13 @@ import { ElDescriptions, ElDescriptionsItem, ElTag } from 'element-plus' import { computed, defineComponent, type StyleValue } from "vue" import { useGlobalParam, useReason, useRule } from "../context" -const DESCRIPTIONS_STYLE: StyleValue = { - width: '400px', +const useDescriptions = () => { + const isXs = useXsState() + const style = computed(() => ({ + width: isXs.value ? '90vw' : '400px', + } satisfies StyleValue)) + const size = computed(() => isXs.value ? 'small' : undefined) + return { style, size } } const renderBaseItems = (rule: timer.limit.Rule | null, url: string) => <> @@ -35,12 +41,13 @@ const TimeDescriptions = defineComponent({ const rule = useRule() const reason = useReason() const { url } = useGlobalParam() + const { style, size } = useDescriptions() const timeLimited = computed(() => meetTimeLimit(props.time ?? 0, props.waste ?? 0, !!reason.value?.allowDelay, reason.value?.delayCount ?? 0)) const visitLimited = computed(() => meetLimit(props.count ?? 0, props.visit ?? 0)) return () => ( - + {renderBaseItems(rule.value, url)} @@ -90,6 +97,8 @@ const _default = defineComponent(() => { setInterval(refreshBrowsingTime, 1000) + const { style, size } = useDescriptions() + return () => ( { ruleLabel={t(msg => msg.limit.item.weekly)} dataLabel={t(msg => msg.calendar.range.thisWeek)} /> - + {renderBaseItems(rule.value, url)} msg.limit.item.visitTime)} labelAlign="right"> {formatPeriodCommon((rule.value?.visitTime ?? 0) * MILL_PER_SECOND) || '-'} @@ -124,7 +133,7 @@ const _default = defineComponent(() => { {reason.value?.delayCount ?? 0} - + {renderBaseItems(rule.value, url)} msg.limit.item.period)} labelAlign="right"> {rule.value?.periods?.length diff --git a/src/pages/app/Layout/menu/item.ts b/src/pages/app/Layout/menu/item.ts index c8f1c4b27..90c9f206f 100644 --- a/src/pages/app/Layout/menu/item.ts +++ b/src/pages/app/Layout/menu/item.ts @@ -76,7 +76,8 @@ export const menuGroups = (): MenuGroup[] => [{ }, { title: msg => msg.menu.limit, route: '/behavior/limit', - icon: Timer + icon: Timer, + mobile: true, }] }, { title: msg => msg.menu.additional, diff --git a/src/pages/app/components/Dashboard/index.tsx b/src/pages/app/components/Dashboard/index.tsx index 55673ab9f..c9d9d62b5 100644 --- a/src/pages/app/components/Dashboard/index.tsx +++ b/src/pages/app/components/Dashboard/index.tsx @@ -12,7 +12,7 @@ import Flex from "@pages/components/Flex" import { recommendRate, saveFlag } from "@service/meta-service" import { REVIEW_PAGE } from "@util/constant/url" import { ElRow, ElScrollbar } from "element-plus" -import { computed, defineComponent, FunctionalComponent } from "vue" +import { computed, defineComponent, type FunctionalComponent } from "vue" import { useRouter } from "vue-router" import ContentContainer from "../common/ContentContainer" import Calendar from "./components/Calendar" diff --git a/src/pages/app/components/Limit/LimitModify/Sop/Step3/TimeInput.tsx b/src/pages/app/components/Limit/LimitModify/Sop/Step3/TimeInput.tsx deleted file mode 100644 index fde155be6..000000000 --- a/src/pages/app/components/Limit/LimitModify/Sop/Step3/TimeInput.tsx +++ /dev/null @@ -1,258 +0,0 @@ -import { CircleClose, Clock } from "@element-plus/icons-vue" -import { useDebounceFn, useState } from "@hooks" -import { getStyle } from "@pages/util/style" -import { range } from "@util/array" -import { Effect, ElIcon, ElInput, ElPopover, ElScrollbar, ScrollbarInstance, useLocale, useNamespace } from "element-plus" -import { computed, defineComponent, nextTick, onMounted, ref, Transition, watch } from "vue" - -function computeSecond2LimitInfo(time: number): [number, number, number] { - time = time || 0 - const second = time % 60 - const totalMinutes = (time - second) / 60 - const minute = totalMinutes % 60 - const hour = (totalMinutes - minute) / 60 - return [hour, minute, second] -} - -const formatTimeVal = (val: number): string => { - return val?.toString?.()?.padStart?.(2, '0') ?? 'NaN' -} - -const TimeSpinner = defineComponent({ - props: { - max: { - type: Number, - required: true, - }, - visible: Boolean, - modelValue: { - type: Number, - required: true, - }, - }, - emits: { - change: (_val: number) => true, - }, - setup(props, ctx) { - const ns = useNamespace('time') - const scrollbar = ref() - const scrolling = ref(false) - - const debounceChangeValue = useDebounceFn((val: number) => { - scrolling.value = false - ctx.emit('change', val) - }, 200) - - const getScrollbarElement = () => { - const el = scrollbar.value?.$el - return el?.querySelector(`.${ns.namespace.value}-scrollbar__wrap`) as HTMLElement - } - - const adjustSpinner = (value: number) => { - let scrollbarEl = getScrollbarElement() - if (!scrollbarEl) return - - scrollbarEl.scrollTop = Math.max(0, value * typeItemHeight()) - } - - watch(() => props.modelValue, () => adjustSpinner(props.modelValue)) - watch(() => props.visible, () => props.visible && nextTick(() => adjustSpinner(props.modelValue))) - - const typeItemHeight = (): number => { - const listItem = scrollbar.value?.$el.querySelector('li') as HTMLLinkElement - if (listItem) { - return Number.parseFloat(getStyle(listItem, 'height')) || 0 - } - return 0 - } - - const bindScroll = () => { - let scrollbarEl = getScrollbarElement() - if (!scrollbarEl) return - - scrollbarEl.addEventListener('scroll', () => { - scrolling.value = true - const scrollTop = getScrollbarElement()?.scrollTop ?? 0 - const scrollbarH = (scrollbar.value?.$el as HTMLUListElement)!.offsetHeight ?? 0 - const itemH = typeItemHeight() - const estimatedIdx = Math.round((scrollTop - (scrollbarH * 0.5 - 10) / itemH + 3) / itemH) - const value = Math.min(estimatedIdx, props.max - 1) - debounceChangeValue(value) - }, { passive: true }) - } - - onMounted(() => { - bindScroll() - adjustSpinner(props.modelValue) - }) - - return () => ( - - {range(props.max).map(idx => ( -
  • ctx.emit('change', idx)} - class={[ - ns.be('spinner', 'item'), - ns.is('active', idx === props.modelValue), - ]} - > - {idx.toString().padStart(2, '0')} -
  • - ))} -
    - ) - }, -}) - -const useTimeInput = (source: () => number) => { - const [initialHour, initialMin, initialSec] = computeSecond2LimitInfo(source?.() ?? 0) - const [hour, setHour] = useState(initialHour) - const [minute, setMinute] = useState(initialMin) - const [second, setSecond] = useState(initialSec) - - const reset = () => { - const [hour, min, sec] = computeSecond2LimitInfo(source?.() ?? 0) - setHour(hour) - setMinute(min) - setSecond(sec) - } - - watch(source, reset) - - const getTotalSecond = () => { - let time = 0 - time += (hour.value ?? 0) * 3600 - time += (minute.value ?? 0) * 60 - time += (second.value ?? 0) - return time - } - - return { - hour, minute, second, - setHour, setMinute, setSecond, - reset, getTotalSecond, - } -} - -/** - * Rewrite - * - * https://github.com/element-plus/element-plus/blob/dev/packages/components/time-picker/src/time-picker-com/panel-time-pick.vue - */ -const TimeInput = defineComponent({ - props: { - modelValue: { - type: Number, - required: true, - }, - hourMax: Number, - }, - emits: { - change: (_val: number) => true, - }, - setup(props, ctx) { - const [popoverVisible, setPopoverVisible] = useState(false) - const { - hour, minute, second, - setHour, setMinute, setSecond, - reset, getTotalSecond, - } = useTimeInput(() => props.modelValue) - - const inputText = computed(() => `${formatTimeVal(hour.value)} h ${formatTimeVal(minute.value)} m ${formatTimeVal(second.value)} s`) - - const ns = useNamespace('time') - const nsDate = useNamespace('date') - const nsInput = useNamespace('input') - - const { t: tEle } = useLocale() - - const transitionName = computed(() => popoverVisible.value ? '' : `${ns.namespace.value}-zoom-in-top`) - - const handleCancel = () => { - reset() - setPopoverVisible(false) - } - - const handleConfirm = () => { - ctx.emit('change', getTotalSecond()) - setPopoverVisible(false) - } - - const handleVisibleChange = (newVal: boolean) => { - setPopoverVisible(newVal) - !newVal && handleCancel() - } - - const handleClear = (ev: MouseEvent) => { - ctx.emit('change', 0) - ev.stopPropagation() - } - - return () => ( - ( - !!props.modelValue && ( -
    - - - -
    - ) - }} - /> - ) - }}> - -
    -
    -
    - - - -
    -
    -
    - - -
    -
    -
    -
    - ) - }, -}) - -export default TimeInput \ No newline at end of file diff --git a/src/pages/app/components/Limit/LimitTable/RuleContent.tsx b/src/pages/app/components/Limit/LimitTable/RuleContent.tsx deleted file mode 100644 index 0a969f2c2..000000000 --- a/src/pages/app/components/Limit/LimitTable/RuleContent.tsx +++ /dev/null @@ -1,78 +0,0 @@ -import { t } from "@app/locale" -import Flex from "@pages/components/Flex" -import { period2Str } from "@util/limit" -import { formatPeriod, MILL_PER_SECOND } from "@util/time" -import { ElTag } from "element-plus" -import { computed, defineComponent, toRef, type PropType } from "vue" - -const TIME_FORMAT = { - dayMsg: '{day}d{hour}h{minute}m{second}s', - hourMsg: '{hour}h{minute}m{second}s', - minuteMsg: '{minute}m{second}s', - secondMsg: '{second}s', -} - -const TimeCountTag = defineComponent({ - props: { - time: Number, - count: Number, - label: String, - }, - setup(props) { - const visible = computed(() => !!props.time || !!props.count) - const content = computed(() => { - const timeContent = props.time ? formatPeriod(props.time * MILL_PER_SECOND, TIME_FORMAT) : '' - const countContent = props.count ? `${props.count} ${t(msg => msg.limit.item.visits)}` : '' - return [timeContent, countContent].filter(str => !!str).join(` ${t(msg => msg.limit.item.or)} `) - }) - - return () => ( -
    - - {props.label}: {content.value} - -
    - ) - }, -}) - -const RuleContent = defineComponent({ - props: { - value: Object as PropType - }, - setup(props) { - const row = toRef(props, 'value') - - return () => ( - - msg.limit.item.daily)} - /> - msg.limit.item.weekly)} - /> - {!!row.value?.visitTime && ( -
    - - {t(msg => msg.limit.item.visitTime)}: {formatPeriod(row.value?.visitTime * MILL_PER_SECOND, TIME_FORMAT)} - -
    - )} - {!!row.value?.periods?.length && <> -
    - {t(msg => msg.limit.item.period)} -
    - - {row.value?.periods?.map(p => {period2Str(p)})} - - } -
    - ) - }, -}) - -export default RuleContent \ No newline at end of file diff --git a/src/pages/app/components/Limit/LimitTable/Waste.tsx b/src/pages/app/components/Limit/LimitTable/Waste.tsx deleted file mode 100644 index 0b1baa8f4..000000000 --- a/src/pages/app/components/Limit/LimitTable/Waste.tsx +++ /dev/null @@ -1,52 +0,0 @@ -import TooltipWrapper from "@app/components/common/TooltipWrapper" -import { t } from "@app/locale" -import Flex from "@pages/components/Flex" -import { meetLimit, meetTimeLimit } from "@util/limit" -import { formatPeriodCommon } from "@util/time" -import { ElTag } from "element-plus" -import { computed, defineComponent } from "vue" - -const Waste = defineComponent({ - props: { - time: Number, - waste: { - type: Number, - required: true, - }, - count: Number, - visit: Number, - delayCount: Number, - allowDelay: Boolean, - }, - setup(props) { - const timeType = computed(() => meetTimeLimit(props.time, props.waste, props.allowDelay, props.delayCount) ? 'danger' : 'info') - const visitType = computed(() => meetLimit(props.count, props.visit) ? 'danger' : 'info') - - return () => ( - -
    - `${t(msg => msg.limit.item.delayCount)}: ${props.delayCount ?? 0}`, - default: () => ( - - {formatPeriodCommon(props.waste)} - - ), - }} - /> -
    -
    - - {props.visit ?? 0} {t(msg => msg.limit.item.visits)} - -
    -
    - ) - }, -}) - -export default Waste \ No newline at end of file diff --git a/src/pages/app/components/Limit/LimitFilter.tsx b/src/pages/app/components/Limit/components/Filter.tsx similarity index 85% rename from src/pages/app/components/Limit/LimitFilter.tsx rename to src/pages/app/components/Limit/components/Filter.tsx index f70896544..0c2302094 100644 --- a/src/pages/app/components/Limit/LimitFilter.tsx +++ b/src/pages/app/components/Limit/components/Filter.tsx @@ -12,18 +12,19 @@ import SwitchFilterItem from "@app/components/common/filter/SwitchFilterItem" import { t } from "@app/locale" import { OPTION_ROUTE } from "@app/router/constants" import { Delete, Open, Operation, Plus, SetUp, TurnOff, WarningFilled } from "@element-plus/icons-vue" +import { useXsState } from '@hooks/useMediaSize' import Flex from "@pages/components/Flex" import { getAppPageUrl } from "@util/constant/url" import { ElIcon, ElText, ElTooltip } from 'element-plus' -import { defineComponent, ref, Ref, watch } from "vue" -import DropdownButton, { type DropdownButtonItem } from "../common/DropdownButton" -import { useLimitAction, useLimitBatch, useLimitFilter } from "./context" +import { computed, defineComponent, ref, Ref, watch } from "vue" +import DropdownButton, { type DropdownButtonItem } from "../../common/DropdownButton" +import { useLimitAction, useLimitBatch, useLimitFilter } from "../context" const optionPageUrl = getAppPageUrl(OPTION_ROUTE, { i: 'limit' }) type BatchOpt = 'delete' | 'enable' | 'disable' -const useCreateTip = (empty: Ref) => { +const useCreateTip = (empty: Ref, isXs: Ref) => { const tipVisible = ref(false) let initialized = false watch(empty, emptyVal => { @@ -33,12 +34,14 @@ const useCreateTip = (empty: Ref) => { setTimeout(closeTip, 10000) }) const closeTip = () => tipVisible.value = false - return { tipVisible, closeTip } + const finalVisible = computed(() => !isXs.value && tipVisible.value) + return { tipVisible: finalVisible, closeTip } } const _default = defineComponent(() => { const { create, test, empty } = useLimitAction() - const { tipVisible, closeTip } = useCreateTip(empty) + const isXs = useXsState() + const { tipVisible, closeTip } = useCreateTip(empty, isXs) const filter = useLimitFilter() const { batchDelete, batchDisable, batchEnable } = useLimitBatch() @@ -75,20 +78,22 @@ const _default = defineComponent(() => { onSearch={val => filter.url = val} /> msg.limit.filterDisabled)} defaultValue={filter.onlyEnabled} onChange={val => filter.onlyEnabled = val} />
    - - + + msg.limit.button.test} icon={Operation} onClick={test} /> msg.base.option} icon={SetUp} onClick={() => createTabAfterCurrent(optionPageUrl)} diff --git a/src/pages/app/components/Limit/components/List/Card.tsx b/src/pages/app/components/Limit/components/List/Card.tsx new file mode 100644 index 000000000..d09ec6e29 --- /dev/null +++ b/src/pages/app/components/Limit/components/List/Card.tsx @@ -0,0 +1,111 @@ +/** + * Copyright (c) 2021 Hengyang Zhang + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ +import { t } from "@app/locale" +import { Delete, EditPen } from "@element-plus/icons-vue" +import { css } from '@emotion/css' +import Flex from "@pages/components/Flex" +import { ElButton, ElCard, ElDivider, ElMessageBox, ElTag, type TagProps, useNamespace } from "element-plus" +import { defineComponent, type FunctionalComponent, type StyleValue } from "vue" +import { verifyCanModify } from "../../common" +import { useLimitAction, useLimitData } from "../../context" +import Rule from "./Rule" + +type Props = { + value: timer.limit.Item +} + +const CARD_PADDING = 10 + +const useStyle = () => { + const cardNs = useNamespace('card') + const cardCls = css` + .${cardNs.e('body')} { + padding: ${CARD_PADDING}px; + } + ` + return cardCls +} + +const ALL_WEEKDAYS = t(msg => msg.calendar.weekDays).split('|') + +const Divider: FunctionalComponent<{}> = () => { + const marginInline = `${-CARD_PADDING}px` + const width = `calc(100% + ${CARD_PADDING * 2}px)` + return +} + +const EffectiveDays: FunctionalComponent<{ weekdays?: number[] }> = ({ weekdays = [] }) => { + const weekdayNum = weekdays?.length + let text: string = '' + let type: TagProps['type'] | undefined = undefined + if (!weekdayNum || weekdayNum === 7) { + text = t(msg => msg.calendar.range.everyday) + type = 'success' + } else if (weekdayNum === 1) { + text = ALL_WEEKDAYS[weekdays[0]] + } else { + const firstDay = ALL_WEEKDAYS[weekdays[0]] + text = `${firstDay}...${weekdayNum}` + } + + return {text} +} + +const _default = defineComponent(props => { + const { deleteRow } = useLimitData() + const { modify } = useLimitAction() + + const handleModify = () => verifyCanModify(props.value) + .then(() => modify(props.value)) + .catch(() => {/** Do nothing */ }) + + const handleDelete = () => verifyCanModify(props.value) + .then(() => ElMessageBox.confirm(t(msg => msg.limit.message.deleteConfirm, { name: props.value.name }))) + .then(() => deleteRow(props.value)) + .catch(() => {/** Do nothing */ }) + + const clz = useStyle() + + return () => ( + + + + + + {props.value.name ?? 'Unnamed'} + + + + + + + {/* Sites */} + + {props.value.cond.map((c, idx) => {c})} + + + {/** Content */} + + + {/* Footer Button */} + + + {t(msg => msg.button.modify)} + + + + + ) +}, { props: ['value'] }) + +export default _default diff --git a/src/pages/app/components/Limit/components/List/Rule.tsx b/src/pages/app/components/Limit/components/List/Rule.tsx new file mode 100644 index 000000000..b53664d77 --- /dev/null +++ b/src/pages/app/components/Limit/components/List/Rule.tsx @@ -0,0 +1,115 @@ +import { t } from '@app/locale' +import Flex from '@pages/components/Flex' +import { isEffective, meetLimit, meetTimeLimit, period2Str } from '@util/limit' +import { formatPeriodCommon, MILL_PER_SECOND } from '@util/time' +import { ElDescriptions, ElDescriptionsItem, ElTag } from 'element-plus' +import { computed, defineComponent, type FunctionalComponent, toRefs } from 'vue' +import { DAILY_WEEKLY_TAG_TYPE, PERIOD_TAG_TYPE, VISIT_TAG_TYPE } from '../style' + +type Props = { + value: timer.limit.Item +} + +const TimeCountPair: FunctionalComponent<{ time?: number, count?: number }> = ({ time, count }) => { + if (!time && !count) return null + return ( + + {!!time && ( + {formatPeriodCommon(time * MILL_PER_SECOND, true)} + )} + {!!count && ( + {`${count} ${t(msg => msg.limit.item.visits)}`} + )} + + ) +} + +type WastePairProps = { + time?: number + waste: number + count?: number + visit?: number + delayCount?: number + allowDelay?: boolean +} + +const WastePair: FunctionalComponent = props => { + const timeType = computed(() => meetTimeLimit(props.time, props.waste, props.allowDelay, props.delayCount) ? 'danger' : 'info') + const visitType = computed(() => meetLimit(props.count, props.visit) ? 'danger' : 'info') + + return ( + + + {formatPeriodCommon(props.waste)} + + + {props.visit ?? 0} {t(msg => msg.limit.item.visits)} + + + ) +} + +const Rule = defineComponent(({ value }) => { + const { + time, count, waste, visit, + weekly, weeklyCount, weeklyWaste, weeklyVisit, + visitTime, periods, + weekdays, + allowDelay, delayCount, weeklyDelayCount, + } = toRefs(value) + + return () => <> + + msg.limit.item.daily)} v-show={time?.value || count?.value}> + + + msg.limit.item.weekly)} v-show={weekly?.value || weeklyCount?.value}> + + + {!!visitTime?.value && ( + msg.limit.item.visitTime)}> + {formatPeriodCommon(visitTime.value * MILL_PER_SECOND, true)} + + )} + {!!periods?.value?.length && ( + msg.limit.item.period)}> + + {periods.value.map((p, idx) => ( + {period2Str(p)} + ))} + + + )} + + + msg.calendar.range.today)}> + {isEffective(weekdays?.value) ? ( + + ) : ( + + {t(msg => msg.limit.item.notEffective)} + + )} + + msg.calendar.range.thisWeek)}> + + + + +}, { props: ['value'] }) + +export default Rule \ No newline at end of file diff --git a/src/pages/app/components/Limit/components/List/index.tsx b/src/pages/app/components/Limit/components/List/index.tsx new file mode 100644 index 000000000..5dd519205 --- /dev/null +++ b/src/pages/app/components/Limit/components/List/index.tsx @@ -0,0 +1,28 @@ +/** + * Copyright (c) 2021 Hengyang Zhang + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ +import Flex from '@pages/components/Flex' +import { ElScrollbar } from 'element-plus' +import { defineComponent } from "vue" +import { useLimitData } from "../../context" +import Card from "./Card" + +const _default = defineComponent(() => { + const { list } = useLimitData() + + return () => ( + + + {list.value.map(row => )} + + + ) +}) + +export default _default diff --git a/src/pages/app/components/Limit/LimitModify/Sop/Step1.tsx b/src/pages/app/components/Limit/components/Modify/Sop/Step1.tsx similarity index 90% rename from src/pages/app/components/Limit/LimitModify/Sop/Step1.tsx rename to src/pages/app/components/Limit/components/Modify/Sop/Step1.tsx index c27a9713e..e9f4d1de2 100644 --- a/src/pages/app/components/Limit/LimitModify/Sop/Step1.tsx +++ b/src/pages/app/components/Limit/components/Modify/Sop/Step1.tsx @@ -1,15 +1,17 @@ import { t } from "@app/locale" +import { useXsState } from '@hooks/useMediaSize' import { ElCol, ElForm, ElFormItem, ElInput, ElRow, ElSelect, ElSwitch } from "element-plus" import { defineComponent } from "vue" import { useSopData } from "./context" const _default = defineComponent(() => { const data = useSopData() + const isXs = useXsState() return () => ( - + msg.limit.item.name)} required> data.name = val} @@ -17,7 +19,7 @@ const _default = defineComponent(() => { /> - + msg.limit.item.enabled)} required> data.enabled = !!v} /> diff --git a/src/pages/app/components/Limit/LimitModify/Sop/Step2/SiteInput.tsx b/src/pages/app/components/Limit/components/Modify/Sop/Step2/SiteInput.tsx similarity index 100% rename from src/pages/app/components/Limit/LimitModify/Sop/Step2/SiteInput.tsx rename to src/pages/app/components/Limit/components/Modify/Sop/Step2/SiteInput.tsx diff --git a/src/pages/app/components/Limit/LimitModify/Sop/Step2/index.tsx b/src/pages/app/components/Limit/components/Modify/Sop/Step2/index.tsx similarity index 100% rename from src/pages/app/components/Limit/LimitModify/Sop/Step2/index.tsx rename to src/pages/app/components/Limit/components/Modify/Sop/Step2/index.tsx diff --git a/src/pages/app/components/Limit/LimitModify/Sop/Step3/PeriodInput.tsx b/src/pages/app/components/Limit/components/Modify/Sop/Step3/PeriodInput.tsx similarity index 82% rename from src/pages/app/components/Limit/LimitModify/Sop/Step3/PeriodInput.tsx rename to src/pages/app/components/Limit/components/Modify/Sop/Step3/PeriodInput.tsx index 6c90e74df..59f1fcb3c 100644 --- a/src/pages/app/components/Limit/LimitModify/Sop/Step3/PeriodInput.tsx +++ b/src/pages/app/components/Limit/components/Modify/Sop/Step3/PeriodInput.tsx @@ -8,7 +8,7 @@ import { t } from "@app/locale" import { Check, Close, Plus } from "@element-plus/icons-vue" import { css } from '@emotion/css' -import { useState, useSwitch } from "@hooks" +import { useState, useSwitch, useXsState } from "@hooks" import Flex from "@pages/components/Flex" import { dateMinute2Idx, period2Str } from "@util/limit" import { MILL_PER_HOUR } from "@util/time" @@ -95,6 +95,7 @@ const usePickerStyle = () => { const PeriodInput = defineComponent>(props => { const [editing, openEditing, closeEditing] = useSwitch(false) const [editingRange, setEditingRange] = useState(rangeInitial()) + const isXs = useXsState() const handleEdit = () => { openEditing() @@ -118,17 +119,19 @@ const PeriodInput = defineComponent>(props => { const pickerCls = usePickerStyle() return () => ( - - {props.modelValue?.map((p, idx) => - handleDelete(idx)} - > - {period2Str(p)} - - )} -
    + + + {props.modelValue?.map((p, idx) => + handleDelete(idx)} + > + {period2Str(p)} + + )} + + >(props => { onClick={handleSave} style={{ ...BUTTON_STYLE, marginInlineStart: 0 } satisfies StyleValue} /> + +
    + + {t(msg => msg.button.create)} +
    - - {t(msg => msg.button.create)} -
    ) }, { props: ['modelValue', 'onChange'] }) diff --git a/src/pages/app/components/Limit/components/Modify/Sop/Step3/TimeInput.tsx b/src/pages/app/components/Limit/components/Modify/Sop/Step3/TimeInput.tsx new file mode 100644 index 000000000..a04b4dc92 --- /dev/null +++ b/src/pages/app/components/Limit/components/Modify/Sop/Step3/TimeInput.tsx @@ -0,0 +1,251 @@ +import { CircleClose, Clock } from "@element-plus/icons-vue" +import { useDebounceFn, useState, useXsState } from "@hooks" +import { getStyle } from "@pages/util/style" +import { range } from "@util/array" +import { + Effect, ElIcon, ElInput, ElPopover, ElScrollbar, + type ScrollbarInstance, + useLocale, useNamespace +} from "element-plus" +import { computed, defineComponent, nextTick, onMounted, ref, Transition, watch } from "vue" + +function computeSecond2LimitInfo(time: number): [number, number, number] { + time = time || 0 + const second = time % 60 + const totalMinutes = (time - second) / 60 + const minute = totalMinutes % 60 + const hour = (totalMinutes - minute) / 60 + return [hour, minute, second] +} + +const formatTimeVal = (val: number): string => { + return val?.toString?.()?.padStart?.(2, '0') ?? 'NaN' +} + +type TimeSpinnerProps = { + max: number + modelValue: number + visible: boolean + onChange?: (val: number) => void +} + +const TimeSpinner = defineComponent(props => { + const ns = useNamespace('time') + const scrollbar = ref() + const scrolling = ref(false) + + const debounceChangeValue = useDebounceFn((val: number) => { + scrolling.value = false + props.onChange?.(val) + }, 200) + + const getScrollbarElement = () => { + const el = scrollbar.value?.$el + return el?.querySelector(`.${ns.namespace.value}-scrollbar__wrap`) as HTMLElement + } + + const adjustSpinner = (value: number) => { + let scrollbarEl = getScrollbarElement() + if (!scrollbarEl) return + + scrollbarEl.scrollTop = Math.max(0, value * typeItemHeight()) + } + + watch(() => props.modelValue, () => adjustSpinner(props.modelValue)) + watch(() => props.visible, () => props.visible && nextTick(() => adjustSpinner(props.modelValue))) + + const typeItemHeight = (): number => { + const listItem = scrollbar.value?.$el.querySelector('li') as HTMLLinkElement + if (listItem) { + return Number.parseFloat(getStyle(listItem, 'height')) || 0 + } + return 0 + } + + const bindScroll = () => { + let scrollbarEl = getScrollbarElement() + if (!scrollbarEl) return + + scrollbarEl.addEventListener('scroll', () => { + scrolling.value = true + const scrollTop = getScrollbarElement()?.scrollTop ?? 0 + const scrollbarH = (scrollbar.value?.$el as HTMLUListElement)!.offsetHeight ?? 0 + const itemH = typeItemHeight() + const estimatedIdx = Math.round((scrollTop - (scrollbarH * 0.5 - 10) / itemH + 3) / itemH) + const value = Math.min(estimatedIdx, props.max - 1) + debounceChangeValue(value) + }, { passive: true }) + } + + onMounted(() => { + bindScroll() + adjustSpinner(props.modelValue) + }) + + return () => ( + + {range(props.max).map(idx => ( +
  • props.onChange?.(idx)} + class={[ + ns.be('spinner', 'item'), + ns.is('active', idx === props.modelValue), + ]} + > + {idx.toString().padStart(2, '0')} +
  • + ))} +
    + ) +}, { props: ['max', 'modelValue', 'visible', 'onChange'] }) + +const useTimeInput = (source: () => number) => { + const [initialHour, initialMin, initialSec] = computeSecond2LimitInfo(source?.() ?? 0) + const [hour, setHour] = useState(initialHour) + const [minute, setMinute] = useState(initialMin) + const [second, setSecond] = useState(initialSec) + + const reset = () => { + const [hour, min, sec] = computeSecond2LimitInfo(source?.() ?? 0) + setHour(hour) + setMinute(min) + setSecond(sec) + } + + watch(source, reset) + + const getTotalSecond = () => { + let time = 0 + time += (hour.value ?? 0) * 3600 + time += (minute.value ?? 0) * 60 + time += (second.value ?? 0) + return time + } + + return { + hour, minute, second, + setHour, setMinute, setSecond, + reset, getTotalSecond, + } +} + +type TimeInputProps = { + modelValue: number + hourMax?: number + onChange?: (val: number) => void +} + +/** + * Rewrite + * + * https://github.com/element-plus/element-plus/blob/dev/packages/components/time-picker/src/time-picker-com/panel-time-pick.vue + */ +const TimeInput = defineComponent(props => { + const [popoverVisible, setPopoverVisible] = useState(false) + const { + hour, minute, second, + setHour, setMinute, setSecond, + reset, getTotalSecond, + } = useTimeInput(() => props.modelValue) + + const inputText = computed(() => `${formatTimeVal(hour.value)} h ${formatTimeVal(minute.value)} m ${formatTimeVal(second.value)} s`) + + const ns = useNamespace('time') + const nsDate = useNamespace('date') + const nsInput = useNamespace('input') + + const { t: tEle } = useLocale() + + const transitionName = computed(() => popoverVisible.value ? '' : `${ns.namespace.value}-zoom-in-top`) + + const handleCancel = () => { + reset() + setPopoverVisible(false) + } + + const handleConfirm = () => { + props.onChange?.(getTotalSecond()) + setPopoverVisible(false) + } + + const handleVisibleChange = (newVal: boolean) => { + setPopoverVisible(newVal) + !newVal && handleCancel() + } + + const handleClear = (ev: MouseEvent) => { + props.onChange?.(0) + ev.stopPropagation() + } + + const isXs = useXsState() + + return () => ( + ( + !!props.modelValue && ( +
    + + + +
    + ) + }} + /> + ) + }}> + +
    +
    +
    + + + +
    +
    +
    + + +
    +
    +
    +
    + ) +}, { props: ['hourMax', 'modelValue', 'onChange'] }) + +export default TimeInput \ No newline at end of file diff --git a/src/pages/app/components/Limit/LimitModify/Sop/Step3/index.tsx b/src/pages/app/components/Limit/components/Modify/Sop/Step3/index.tsx similarity index 80% rename from src/pages/app/components/Limit/LimitModify/Sop/Step3/index.tsx rename to src/pages/app/components/Limit/components/Modify/Sop/Step3/index.tsx index 25acb2b8f..6c815967c 100644 --- a/src/pages/app/components/Limit/LimitModify/Sop/Step3/index.tsx +++ b/src/pages/app/components/Limit/components/Modify/Sop/Step3/index.tsx @@ -6,6 +6,7 @@ */ import { t } from "@app/locale" +import { useXsState } from '@hooks/useMediaSize' import Flex from "@pages/components/Flex" import { ElForm, ElFormItem, ElInputNumber } from "element-plus" import { defineComponent } from "vue" @@ -17,32 +18,35 @@ const MAX_HOUR_WEEKLY = 7 * 24 const _default = defineComponent(() => { const data = useSopData() + const isXs = useXsState() return () => ( - + msg.limit.item.daily)}> - + data.time = v} /> - {t(msg => msg.limit.item.or)} + {!isXs.value && t(msg => msg.limit.item.or)} data.count = v ?? 0} + size={isXs.value ? 'small' : undefined} v-slots={{ suffix: () => t(msg => msg.limit.item.visits) }} /> msg.limit.item.weekly)}> - + data.weekly = v} hourMax={MAX_HOUR_WEEKLY} /> - {t(msg => msg.limit.item.or)} + {!isXs.value && t(msg => msg.limit.item.or)} data.weeklyCount = v ?? 0} + size={isXs.value ? 'small' : undefined} v-slots={{ suffix: () => t(msg => msg.limit.item.visits) }} /> diff --git a/src/pages/app/components/Limit/LimitModify/Sop/context.ts b/src/pages/app/components/Limit/components/Modify/Sop/context.ts similarity index 98% rename from src/pages/app/components/Limit/LimitModify/Sop/context.ts rename to src/pages/app/components/Limit/components/Modify/Sop/context.ts index 51eb12b71..d2ea4273c 100644 --- a/src/pages/app/components/Limit/LimitModify/Sop/context.ts +++ b/src/pages/app/components/Limit/components/Modify/Sop/context.ts @@ -4,7 +4,7 @@ import { range } from "@util/array" import { ElMessage } from "element-plus" import { type Reactive, reactive, type Ref, ref, toRaw } from "vue" -type Step = 0 | 1 | 2 +export type Step = 0 | 1 | 2 type SopData = Required> diff --git a/src/pages/app/components/Limit/LimitModify/Sop/index.tsx b/src/pages/app/components/Limit/components/Modify/Sop/index.tsx similarity index 54% rename from src/pages/app/components/Limit/LimitModify/Sop/index.tsx rename to src/pages/app/components/Limit/components/Modify/Sop/index.tsx index 6dbd7b5a9..a3d5a11f3 100644 --- a/src/pages/app/components/Limit/LimitModify/Sop/index.tsx +++ b/src/pages/app/components/Limit/components/Modify/Sop/index.tsx @@ -7,9 +7,11 @@ import DialogSop from "@app/components/common/DialogSop" import { t } from "@app/locale" -import { ElStep, ElSteps } from "element-plus" -import { computed, defineComponent } from "vue" -import { initSop } from "./context" +import { useXsState } from '@hooks' +import Flex from '@pages/components/Flex' +import { ElDivider, ElStep, ElSteps, ElText } from "element-plus" +import { computed, defineComponent, StyleValue } from "vue" +import { initSop, type Step } from "./context" import Step1 from "./Step1" import Step2 from "./Step2" import Step3 from "./Step3" @@ -26,12 +28,20 @@ type Props = { onSave?: ArgCallback> } +const STEP_TITLE: Record = { + 0: t(msg => msg.limit.step.base), + 1: t(msg => msg.limit.step.url), + 2: t(msg => msg.limit.step.rule), +} + const _default = defineComponent(({ onSave, onCancel }, ctx) => { const { reset, step, handleNext } = initSop({ onSave }) const last = computed(() => step.value === 2) const first = computed(() => step.value === 0) ctx.expose({ reset } satisfies SopInstance) + const isXs = useXsState() + return () => ( (({ onSave, onCancel }, ctx) => { onNext={handleNext} onFinish={handleNext} v-slots={{ - steps: () => ( - - msg.limit.step.base)} /> - msg.limit.step.url)} /> - msg.limit.step.rule)} /> - - ), + steps: () => isXs.value + ? ( + + {STEP_TITLE[step.value]} + + + ) + : ( + + + + + + ), content: () => <> diff --git a/src/pages/app/components/Limit/LimitModify/index.tsx b/src/pages/app/components/Limit/components/Modify/index.tsx similarity index 91% rename from src/pages/app/components/Limit/LimitModify/index.tsx rename to src/pages/app/components/Limit/components/Modify/index.tsx index 2abe7dbe6..1a01ad798 100644 --- a/src/pages/app/components/Limit/LimitModify/index.tsx +++ b/src/pages/app/components/Limit/components/Modify/index.tsx @@ -6,17 +6,17 @@ */ import { t } from "@app/locale" -import { useSwitch } from "@hooks" +import { useSwitch, useXsState } from "@hooks" import limitService from "@service/limit-service" import { ElDialog, ElMessage } from "element-plus" import { computed, defineComponent, nextTick, ref, toRaw } from "vue" -import { type ModifyInstance, useLimitTable } from "../context" +import { type ModifyInstance, useLimitData } from "../../context" import Sop, { type SopInstance } from "./Sop" type Mode = "create" | "modify" const _default = defineComponent((_, ctx) => { - const { refresh } = useLimitTable() + const { refresh } = useLimitData() const [visible, open, close] = useSwitch() const sop = ref() const mode = ref() @@ -68,12 +68,15 @@ const _default = defineComponent((_, ctx) => { }, } satisfies ModifyInstance) + const isXs = useXsState() + return () => ( diff --git a/src/pages/app/components/Limit/LimitTable/column/LimitOperationColumn.tsx b/src/pages/app/components/Limit/components/Table/OperationColumn.tsx similarity index 93% rename from src/pages/app/components/Limit/LimitTable/column/LimitOperationColumn.tsx rename to src/pages/app/components/Limit/components/Table/OperationColumn.tsx index 3d6e7f08f..9d531f128 100644 --- a/src/pages/app/components/Limit/LimitTable/column/LimitOperationColumn.tsx +++ b/src/pages/app/components/Limit/components/Table/OperationColumn.tsx @@ -10,7 +10,7 @@ import { locale } from "@i18n" import { ElButton, ElTableColumn, type RenderRowData } from "element-plus" import { defineComponent } from "vue" import { verifyCanModify } from "../../common" -import { useLimitAction, useLimitTable } from "../../context" +import { useLimitAction, useLimitData } from "../../context" const LOCALE_WIDTH: { [locale in timer.Locale]: number } = { en: 220, @@ -29,7 +29,7 @@ const LOCALE_WIDTH: { [locale in timer.Locale]: number } = { } const _default = defineComponent<{}>(() => { - const { deleteRow } = useLimitTable() + const { deleteRow } = useLimitData() const { modify } = useLimitAction() const handleModify = (row: timer.limit.Item) => verifyCanModify(row) diff --git a/src/pages/app/components/Limit/components/Table/Rule.tsx b/src/pages/app/components/Limit/components/Table/Rule.tsx new file mode 100644 index 000000000..6ed4ce8ee --- /dev/null +++ b/src/pages/app/components/Limit/components/Table/Rule.tsx @@ -0,0 +1,68 @@ +import { t } from "@app/locale" +import Flex from "@pages/components/Flex" +import { period2Str } from '@util/limit' +import { formatPeriodCommon, MILL_PER_SECOND } from '@util/time' +import { ElTag, TagProps } from 'element-plus' +import { defineComponent, type FunctionalComponent, toRef } from "vue" +import { DAILY_WEEKLY_TAG_TYPE, VISIT_TAG_TYPE } from '../style' + +type TimeCountPairProps = { + time?: number + count?: number + label: string + type?: TagProps['type'] +} + +const TimeCountPair: FunctionalComponent = ({ time, count, label, type = DAILY_WEEKLY_TAG_TYPE }) => { + if (!time && !count) return null + + const timeContent = time ? formatPeriodCommon(time * MILL_PER_SECOND, true) : null + const countContent = count ? `${count} ${t(msg => msg.limit.item.visits)}` : null + const content = [timeContent, countContent].filter(str => !!str).join(` ${t(msg => msg.limit.item.or)} `) + + return ( +
    + {label}: {content} +
    + ) +} + +const PeriodTag: FunctionalComponent<{ periods?: timer.limit.Period[], }> = ({ periods }) => { + if (!periods?.length) return null + + return <> +
    + {t(msg => msg.limit.item.period)} +
    + + {periods.map((p, idx) => {period2Str(p)})} + + +} + +const Rule = defineComponent<{ value: timer.limit.Item }>(props => { + const row = toRef(props, 'value') + + return () => ( + + msg.limit.item.daily)} + /> + msg.limit.item.weekly)} + /> + msg.limit.item.visitTime)} + type={VISIT_TAG_TYPE} + /> + + + ) +}, { props: ['value'] }) + +export default Rule \ No newline at end of file diff --git a/src/pages/app/components/Limit/components/Table/Waste.tsx b/src/pages/app/components/Limit/components/Table/Waste.tsx new file mode 100644 index 000000000..226a05ac2 --- /dev/null +++ b/src/pages/app/components/Limit/components/Table/Waste.tsx @@ -0,0 +1,48 @@ +import TooltipWrapper from "@app/components/common/TooltipWrapper" +import { t } from "@app/locale" +import Flex from "@pages/components/Flex" +import { meetLimit, meetTimeLimit } from "@util/limit" +import { formatPeriodCommon } from "@util/time" +import { ElTag } from "element-plus" +import { computed, defineComponent } from "vue" + +type Props = { + time?: number + waste: number + count?: number + visit?: number + delayCount?: number + allowDelay?: boolean +} + +const Waste = defineComponent(props => { + const timeType = computed(() => meetTimeLimit(props.time, props.waste, props.allowDelay, props.delayCount) ? 'danger' : 'info') + const visitType = computed(() => meetLimit(props.count, props.visit) ? 'danger' : 'info') + + return () => ( + +
    + `${t(msg => msg.limit.item.delayCount)}: ${props.delayCount ?? 0}`, + default: () => ( + + {formatPeriodCommon(props.waste)} + + ), + }} + /> +
    +
    + + {props.visit ?? 0} {t(msg => msg.limit.item.visits)} + +
    +
    + ) +}, { props: ['time', 'waste', 'count', 'visit', 'allowDelay', 'delayCount'] }) + +export default Waste \ No newline at end of file diff --git a/src/pages/app/components/Limit/LimitTable/Weekday.tsx b/src/pages/app/components/Limit/components/Table/Weekday.tsx similarity index 100% rename from src/pages/app/components/Limit/LimitTable/Weekday.tsx rename to src/pages/app/components/Limit/components/Table/Weekday.tsx diff --git a/src/pages/app/components/Limit/LimitTable/index.tsx b/src/pages/app/components/Limit/components/Table/index.tsx similarity index 92% rename from src/pages/app/components/Limit/LimitTable/index.tsx rename to src/pages/app/components/Limit/components/Table/index.tsx index ea7c6401a..18f806431 100644 --- a/src/pages/app/components/Limit/LimitTable/index.tsx +++ b/src/pages/app/components/Limit/components/Table/index.tsx @@ -9,11 +9,11 @@ import { t } from "@app/locale" import { useLocalStorage, useRequest, useState } from "@hooks" import weekHelper from "@service/components/week-helper" import { isEffective } from "@util/limit" -import { ElSwitch, ElTable, ElTableColumn, ElTag, type RenderRowData, type Sort } from "element-plus" -import { defineComponent, watch } from "vue" -import { useLimitTable } from "../context" -import LimitOperationColumn from "./column/LimitOperationColumn" -import RuleContent from "./RuleContent" +import { ElSwitch, ElTable, ElTableColumn, ElTag, type RenderRowData, type Sort, type TableInstance } from "element-plus" +import { defineComponent, ref, watch } from "vue" +import { useLimitData, type LimitInstance } from "../../context" +import LimitOperationColumn from "./OperationColumn" +import Rule from "./Rule" import Waste from "./Waste" import Weekday from "./Weekday" @@ -27,17 +27,14 @@ const sortMethodByNumVal = (key: keyof timer.limit.Item & 'waste' | 'weeklyWaste const sortByEffectiveDays = ({ weekdays: a }: timer.limit.Item, { weekdays: b }: timer.limit.Item) => (a?.length ?? 0) - (b?.length ?? 0) -const _default = defineComponent(() => { +const _default = defineComponent((_, ctx) => { const { data: weekStartName } = useRequest(async () => { const offset = await weekHelper.getRealWeekStart() const name = t(msg => msg.calendar.weekDays)?.split('|')?.[offset] return name || 'NaN' }) - const { - list, table, - changeEnabled, changeDelay, changeLocked - } = useLimitTable() + const { list, changeEnabled, changeDelay, changeLocked } = useLimitData() const [cachedSort, setCachedSort] = useLocalStorage( '__limit_sort_default__', { prop: DEFAULT_SORT_COL, order: 'descending' } @@ -46,6 +43,12 @@ const _default = defineComponent(() => { const [sort, setSort] = useState(cachedSort) watch(sort, () => setCachedSort(sort.value)) + const table = ref() + + ctx.expose({ + getSelected: () => table.value?.getSelectionRows?.() ?? [] + } satisfies LimitInstance) + return () => ( { minWidth={200} align="center" > - {({ row }: RenderRowData) => } + {({ row }: RenderRowData) => } { if (!url) { @@ -35,6 +35,7 @@ const _default = defineComponent((_props, ctx) => { const debouncedUrl = useDebounce(url) const [visible, open, close] = useSwitch() const { data: result } = useRequest(() => fetchResult(debouncedUrl.value), { deps: debouncedUrl }) + const isXs = useXsState() ctx.expose({ show: () => { @@ -49,6 +50,7 @@ const _default = defineComponent((_props, ctx) => { modelValue={visible.value} closeOnClickModal={false} onClose={close} + fullscreen={isXs.value} > filter: Reactive - list: Ref, refresh: NoArgCallback, + list: Ref + refresh: NoArgCallback deleteRow: ArgCallback batchDelete: NoArgCallback batchEnable: NoArgCallback @@ -69,10 +73,10 @@ export const useLimitProvider = () => { } }) - const table = ref() + const inst = ref() const selectedAndThen = (then: (list: timer.limit.Item[]) => void): void => { - const list = table.value?.getSelectionRows?.() + const list = inst.value?.getSelected?.() if (!list?.length) { ElMessage.info('No limit rule selected') return @@ -85,10 +89,14 @@ export const useLimitProvider = () => { refresh() } - const handleBatchDelete = (list: timer.limit.Item[]) => verifyCanModify(...list) - .then(() => limitService.remove(...list)) - .then(onBatchSuccess) - .catch(() => { }) + const handleBatchDelete = (list: timer.limit.Item[]) => { + const names = list.map(item => item.name ?? item.id).join(', ') + verifyCanModify(...list) + .then(() => ElMessageBox.confirm(t(msg => msg.limit.message.deleteConfirm, { name: names }), { type: "warning" })) + .then(() => limitService.remove(...list)) + .then(onBatchSuccess) + .catch(() => { }) + } const handleBatchEnable = (list: timer.limit.Item[]) => { list.forEach(item => item.enabled = true) @@ -149,7 +157,6 @@ export const useLimitProvider = () => { const empty = computed(() => !loading.value && !list.value.length) useProvide(NAMESPACE, { - table, filter, list, empty, refresh, deleteRow, @@ -160,13 +167,13 @@ export const useLimitProvider = () => { modify, create, test, }) - return { modifyInst, testInst } + return { modifyInst, testInst, inst } } export const useLimitFilter = (): Reactive => useProvider(NAMESPACE, "filter").filter -export const useLimitTable = () => useProvider( - NAMESPACE, 'list', 'table', 'refresh', 'deleteRow', 'changeEnabled', 'changeDelay', 'changeLocked' +export const useLimitData = () => useProvider( + NAMESPACE, 'list', 'refresh', 'deleteRow', 'changeEnabled', 'changeDelay', 'changeLocked' ) export const useLimitBatch = () => useProvider( diff --git a/src/pages/app/components/Limit/index.tsx b/src/pages/app/components/Limit/index.tsx index 77877f850..611ca303d 100644 --- a/src/pages/app/components/Limit/index.tsx +++ b/src/pages/app/components/Limit/index.tsx @@ -5,24 +5,24 @@ * https://opensource.org/licenses/MIT */ +import { useXsState } from '@hooks' import { defineComponent } from "vue" +import ContentCard from '../common/ContentCard' import ContentContainer from "../common/ContentContainer" +import { Filter, List, Modify, Table, Test } from "./components" import { useLimitProvider } from "./context" -import LimitFilter from "./LimitFilter" -import LimitModify from "./LimitModify" -import LimitTable from "./LimitTable" -import LimitTest from "./LimitTest" const _default = defineComponent(() => { - const { modifyInst, testInst } = useLimitProvider() + const { modifyInst, testInst, inst } = useLimitProvider() + const isXs = useXsState() return () => ( , - content: () => <> - - - + filter: () => , + default: () => <> + {isXs.value ? : } + + }} /> ) diff --git a/src/pages/app/components/Option/Select.tsx b/src/pages/app/components/Option/Select.tsx index 77f880af5..d1c178712 100644 --- a/src/pages/app/components/Option/Select.tsx +++ b/src/pages/app/components/Option/Select.tsx @@ -5,8 +5,6 @@ import { useRouter } from "vue-router" import ContentContainer from "../common/ContentContainer" import { CATE_LABELS, changeQuery, type OptionCategory, parseQuery } from "./common" -const IGNORED_CATE: OptionCategory[] = ['limit'] - const _default = defineComponent(() => { const tab = ref(parseQuery() || 'appearance') const router = useRouter() @@ -21,12 +19,9 @@ const _default = defineComponent(() => { modelValue={tab.value} onChange={val => tab.value = val} > - {Object.keys(slots) - .filter(key => !IGNORED_CATE.includes(key as OptionCategory) && key !== 'default') - .map(cate => ( - - )) - } + {Object.keys(slots).map(cate => ( + + ))} ), default: () => { diff --git a/src/pages/app/components/Option/components/LimitOption/index.tsx b/src/pages/app/components/Option/components/LimitOption/index.tsx index b414d8d7d..aebf7517f 100644 --- a/src/pages/app/components/Option/components/LimitOption/index.tsx +++ b/src/pages/app/components/Option/components/LimitOption/index.tsx @@ -126,7 +126,7 @@ const _default = defineComponent((_, ctx) => { default: () => ( option.limitReminder = val as boolean} + onChange={val => option.limitReminder = !!val} /> ), minInput: () => ( diff --git a/src/pages/app/components/common/TooltipWrapper.tsx b/src/pages/app/components/common/TooltipWrapper.tsx index 3d502c141..e4f15b184 100644 --- a/src/pages/app/components/common/TooltipWrapper.tsx +++ b/src/pages/app/components/common/TooltipWrapper.tsx @@ -1,7 +1,7 @@ -import { ElTooltip, ElTooltipProps } from "element-plus" +import { ElTooltip, type UseTooltipProps } from "element-plus" import { defineComponent, ref, useSlots } from "vue" -type Props = PartialPick & { +type Props = PartialPick & { usePopover?: boolean } diff --git a/src/pages/app/components/common/filter/ButtonFilterItem.tsx b/src/pages/app/components/common/filter/ButtonFilterItem.tsx index 4e173b2e8..9eda1817b 100644 --- a/src/pages/app/components/common/filter/ButtonFilterItem.tsx +++ b/src/pages/app/components/common/filter/ButtonFilterItem.tsx @@ -5,6 +5,7 @@ * https://opensource.org/licenses/MIT */ import { type I18nKey, t } from '@app/locale' +import { useXsState } from '@hooks/useMediaSize' import { type ButtonProps, ElButton } from "element-plus" import { defineComponent } from "vue" @@ -16,11 +17,21 @@ type Props = { } const ButtonFilterItem = defineComponent(props => { - return () => ( - - {t(props.text)} - - ) + const isXs = useXsState() + return () => isXs.value + ? ( + + ) : ( + + {t(props.text)} + + ) }, { props: ['icon', 'onClick', 'text', 'type'] }) export default ButtonFilterItem \ No newline at end of file diff --git a/src/pages/components/Flex.tsx b/src/pages/components/Flex.tsx index f611489a4..711c256fe 100644 --- a/src/pages/components/Flex.tsx +++ b/src/pages/components/Flex.tsx @@ -23,32 +23,34 @@ type Props = { } & BaseProps const Flex = defineComponent(props => { - const { default: defaultSlots } = useSlots() const Comp = props.as ?? 'div' - return () => ( - - {defaultSlots && h(defaultSlots)} - - ) + return () => { + const { default: defaultSlots } = useSlots() + return ( + + {defaultSlots && h(defaultSlots)} + + ) + } }, { props: [...ALL_BASE_PROPS, 'direction', 'column', 'flex', 'align', 'justify', 'gap', 'columnGap', 'rowGap', 'wrap', 'as'] }) export default Flex \ No newline at end of file diff --git a/src/pages/popup/components/Header/index.tsx b/src/pages/popup/components/Header/index.tsx index 41aa376b9..caaf3366a 100644 --- a/src/pages/popup/components/Header/index.tsx +++ b/src/pages/popup/components/Header/index.tsx @@ -6,7 +6,7 @@ import { t } from "@popup/locale" import { IS_ANDROID } from "@util/constant/environment" import { getAppPageUrl } from "@util/constant/url" import { ElLink } from "element-plus" -import { FunctionalComponent } from "vue" +import type { FunctionalComponent } from "vue" import DarkSwitch from "./DarkSwitch" import Logo from "./Logo" import MoreInfo from './MoreInfo' @@ -29,7 +29,7 @@ const openAppPage = async () => { await createTab(appPageUrl) } -const Header: FunctionalComponent = () => ( +const Header: FunctionalComponent<{}> = () => ( diff --git a/src/util/time.ts b/src/util/time.ts index 8cb3e195b..9d5dd1372 100644 --- a/src/util/time.ts +++ b/src/util/time.ts @@ -51,10 +51,12 @@ export function formatTimeYMD(time: Date | number) { return formatTime(time, '{y}{m}{d}') } +type PeriodMsgFormat = Record<'dayMsg' | 'hourMsg' | 'minuteMsg' | 'secondMsg', string> + /** * Format milliseconds for display */ -export function formatPeriod(milliseconds: number, message: Record<'dayMsg' | 'hourMsg' | 'minuteMsg' | 'secondMsg', string>): string { +export function formatPeriod(milliseconds: number, message: PeriodMsgFormat): string { const prefix = milliseconds < 0 ? '-' : '' milliseconds = Math.abs(milliseconds) const { dayMsg, hourMsg, minuteMsg, secondMsg } = message @@ -82,29 +84,45 @@ export function formatPeriod(milliseconds: number, message: Record<'dayMsg' | 'h return prefix + result } +const PERIOD_RTL: PeriodMsgFormat = { + dayMsg: 's{second} m{minute} h{hour} d{day}', + hourMsg: 's{second} m{minute} h{hour}', + minuteMsg: 's{second} m{minute}', + secondMsg: 's{second}', +} +const PERIOD_RTL_SIMPLIFIED: PeriodMsgFormat = { + dayMsg: 's{second}m{minute}h{hour}d{day}', + hourMsg: 's{second}m{minute}h{hour}', + minuteMsg: 's{second}m{minute}', + secondMsg: 's{second}', +} +const PERIOD_LTR: PeriodMsgFormat = { + dayMsg: '{day}d {hour}h {minute}m {second}s', + hourMsg: '{hour}h {minute}m {second}s', + minuteMsg: '{minute}m {second}s', + secondMsg: '{second}s', +} +const PERIOD_LTR_SIMPLIFIED: PeriodMsgFormat = { + dayMsg: '{day}d{hour}h{minute}m{second}s', + hourMsg: '{hour}h{minute}m{second}s', + minuteMsg: '{minute}m{second}s', + secondMsg: '{second}s', +} + /** * e.g. * - * 100h0m0s - * 20h10m59s - * 20h0m1s - * 10m20s + * 2d 10h 0m 0s + * 20h 0m 1s + * 10m 20s * 30s * * @return (xx+h)(xx+m)xx+s */ -export function formatPeriodCommon(milliseconds: number): string { - const defaultMessage = isRtl() ? { - dayMsg: 's{second} m{minute} h{hour} d{day}', - hourMsg: 's{second} m{minute} h{hour}', - minuteMsg: 's{second} m{minute}', - secondMsg: 's{second}', - } : { - dayMsg: '{day}d {hour}h {minute}m {second}s', - hourMsg: '{hour}h {minute}m {second}s', - minuteMsg: '{minute}m {second}s', - secondMsg: '{second}s', - } +export function formatPeriodCommon(milliseconds: number, simplified?: boolean): string { + const defaultMessage = isRtl() + ? (simplified ? PERIOD_RTL_SIMPLIFIED : PERIOD_RTL) + : (simplified ? PERIOD_LTR_SIMPLIFIED : PERIOD_LTR) return formatPeriod(milliseconds, defaultMessage) } diff --git a/test-e2e/common/base.ts b/test-e2e/common/base.ts index eb5e75e3d..1ce962712 100644 --- a/test-e2e/common/base.ts +++ b/test-e2e/common/base.ts @@ -1,6 +1,6 @@ import { type Browser, launch, type Page } from "puppeteer" import { E2E_OUTPUT_PATH } from "../../rspack/constant" -import { removeAllWhitelist } from './whitelist' +import { removeAllWhitelist } from './whitelist.test' const USE_HEADLESS_PUPPETEER = !!process.env['USE_HEADLESS_PUPPETEER'] diff --git a/test-e2e/common/whitelist.ts b/test-e2e/common/whitelist.test.ts similarity index 70% rename from test-e2e/common/whitelist.ts rename to test-e2e/common/whitelist.test.ts index 357e7afc5..c15ed8336 100644 --- a/test-e2e/common/whitelist.ts +++ b/test-e2e/common/whitelist.test.ts @@ -1,4 +1,4 @@ -import { type LaunchContext, sleep } from "./base" +import { launchBrowser, type LaunchContext, sleep } from "./base" export async function createWhitelist(context: LaunchContext, white: string) { const whitePage = await context.openAppPage('/additional/whitelist') @@ -11,8 +11,10 @@ export async function createWhitelist(context: LaunchContext, white: string) { await input?.focus() await whitePage.keyboard.type(white) await sleep(.4) - const selectItem = await whitePage.waitForSelector('.el-popper .el-select-dropdown li:nth-child(1)') - await selectItem?.click() + await whitePage.keyboard.press('ArrowDown') + await sleep(.2) + await whitePage.keyboard.press('Enter') + await whitePage.click('.el-button:nth-child(3)') const checkBtn = await whitePage.waitForSelector('.el-overlay.is-message-box .el-button.el-button--primary') await checkBtn?.click() @@ -25,4 +27,10 @@ export async function removeAllWhitelist(context: LaunchContext) { await chrome.storage.local.remove('__timer__WHITELIST') }) await whitePage.close() -} \ No newline at end of file +} + +// Run to test the function, but skip it in normal test runs +test.skip('create whitelist', async () => { + const context = await launchBrowser() + await createWhitelist(context, 'example.com') +}) \ No newline at end of file diff --git a/test-e2e/tracker/base.test.ts b/test-e2e/tracker/base.test.ts index aeb4fccf8..3857b3942 100644 --- a/test-e2e/tracker/base.test.ts +++ b/test-e2e/tracker/base.test.ts @@ -1,6 +1,6 @@ import { launchBrowser, type LaunchContext, MOCK_HOST, MOCK_URL, MOCK_URL_2, sleep } from "../common/base" import { readRecordsOfFirstPage } from "../common/record" -import { createWhitelist } from "../common/whitelist" +import { createWhitelist } from "../common/whitelist.test" let context: LaunchContext diff --git a/test-e2e/tracker/run-time.test.ts b/test-e2e/tracker/run-time.test.ts index 238791bf9..ce35a29f6 100644 --- a/test-e2e/tracker/run-time.test.ts +++ b/test-e2e/tracker/run-time.test.ts @@ -1,6 +1,6 @@ import { launchBrowser, MOCK_HOST, MOCK_URL, sleep, type LaunchContext } from "../common/base" import { parseTime2Sec, readRecordsOfFirstPage } from "../common/record" -import { createWhitelist } from "../common/whitelist" +import { createWhitelist } from "../common/whitelist.test" let context: LaunchContext