+
+
+ {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