diff --git a/.github/workflows/end-to-end-tests.yml b/.github/workflows/end-to-end-tests.yml index 7e1928ca6..965d682a5 100644 --- a/.github/workflows/end-to-end-tests.yml +++ b/.github/workflows/end-to-end-tests.yml @@ -12,7 +12,7 @@ jobs: - name: Install dependencies run: npm install - name: Install http-server - run: npm install -g http-server + run: npm install -g http-server pm2 - name: Build e2e outputs run: npm run dev:e2e - name: Build production outputs @@ -20,8 +20,8 @@ jobs: - name: Start up mock server run: | - nohup http-server ./test-e2e/example -p 12345 -s > test.log 2>&1 & - nohup http-server ./test-e2e/example -p 12346 -s > test.log 2>&1 & + pm2 start 'http-server ./test-e2e/example -p 12345' + pm2 start 'http-server ./test-e2e/example -p 12346' - name: Run tests env: diff --git a/.vscode/settings.json b/.vscode/settings.json index 9ab58c46d..7e4ac6633 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -40,6 +40,7 @@ "rtlcss", "selectchanged", "sheepzh", + "subpages", "Treemap", "Vnode", "vueuse", diff --git a/package.json b/package.json index 6a907d97b..11d8d03cb 100644 --- a/package.json +++ b/package.json @@ -31,8 +31,8 @@ "@babel/plugin-transform-modules-commonjs": "^7.26.3", "@babel/preset-env": "^7.26.9", "@crowdin/crowdin-api-client": "^1.42.0", - "@rsdoctor/webpack-plugin": "^1.0.1", - "@swc/core": "^1.11.20", + "@rsdoctor/webpack-plugin": "^1.0.2", + "@swc/core": "^1.11.21", "@swc/jest": "^0.2.37", "@types/chrome": "0.0.315", "@types/decompress": "^4.2.7", @@ -47,7 +47,7 @@ "copy-webpack-plugin": "^13.0.0", "css-loader": "^7.1.2", "decompress": "^4.2.1", - "eslint": "^9.24.0", + "eslint": "^9.25.0", "filemanager-webpack-plugin": "^8.0.0", "generate-json-webpack-plugin": "^2.0.0", "html-webpack-plugin": "^5.6.3", @@ -69,7 +69,7 @@ "typescript": "5.8.3", "url-loader": "^4.1.1", "web-ext": "^8.5.0", - "webpack": "^5.99.5", + "webpack": "^5.99.6", "webpack-cli": "^6.0.1" }, "dependencies": { @@ -77,7 +77,7 @@ "@vueuse/core": "^13.1.0", "countup.js": "^2.8.0", "echarts": "^5.6.0", - "element-plus": "2.9.7", + "element-plus": "2.9.8", "js-base64": "^3.7.7", "punycode": "^2.3.1", "stream-browserify": "^3.0.0", diff --git a/src/background/migrator/index.ts b/src/background/migrator/index.ts index c21d0bbbe..a4932a982 100644 --- a/src/background/migrator/index.ts +++ b/src/background/migrator/index.ts @@ -11,6 +11,7 @@ import { type Migrator } from "./common" import HostMergeInitializer from "./host-merge-initializer" import LocalFileInitializer from "./local-file-initializer" import WhitelistInitializer from "./whitelist-initializer" +import LimitRuleMigrator from "./limit-rule-migrator" /** * Version manager @@ -26,6 +27,7 @@ class VersionManager { new LocalFileInitializer(), new WhitelistInitializer(), new CateInitializer(), + new LimitRuleMigrator(), ) } diff --git a/src/background/migrator/limit-rule-migrator.ts b/src/background/migrator/limit-rule-migrator.ts new file mode 100644 index 000000000..2dac2a865 --- /dev/null +++ b/src/background/migrator/limit-rule-migrator.ts @@ -0,0 +1,35 @@ +import limitService from "@service/limit-service" +import { cleanCond } from "@util/limit" +import type { Migrator } from "./common" + +export default class LimitRuleMigrator implements Migrator { + onInstall(): void { + } + + async onUpdate(_version: string): Promise { + const rules = await limitService.select() + if (!rules?.length) return + const needUpdate: timer.limit.Rule[] = [] + const needRemoved: timer.limit.Rule[] = [] + rules.forEach(async rule => { + const { cond } = rule + let changed = false + const newCond: string[] = [] + cond?.forEach(url => { + const clean = cleanCond(url) + changed = changed || clean !== url + clean && newCond.push(clean) + }) + if (!changed) return + if (rule.cond.length) { + rule.cond = newCond + needUpdate.push(rule) + } else { + needRemoved.push(rule) + } + + }) + needRemoved.length && await limitService.remove(...needRemoved) + needUpdate.length && await limitService.update(...needUpdate) + } +} \ No newline at end of file diff --git a/src/database/site-database.ts b/src/database/site-database.ts index 85482fe93..c868dbddb 100644 --- a/src/database/site-database.ts +++ b/src/database/site-database.ts @@ -81,8 +81,8 @@ function cvt2Entry({ alias, iconUrl, cate, run }: timer.site.SiteInfo): _Entry { return entry } -function cvt2SiteInfo(key: timer.site.SiteKey, entry: _Entry): timer.site.SiteInfo { - const { a, i, c, r } = entry +function cvt2SiteInfo(key: timer.site.SiteKey, entry: _Entry | undefined): timer.site.SiteInfo { + const { a, i, c, r } = entry || {} const siteInfo: timer.site.SiteInfo = { ...key } siteInfo.alias = a siteInfo.cate = c diff --git a/src/i18n/message/app/limit-resource.json b/src/i18n/message/app/limit-resource.json index 2851a93a1..b30da3022 100644 --- a/src/i18n/message/app/limit-resource.json +++ b/src/i18n/message/app/limit-resource.json @@ -1,6 +1,7 @@ { "zh_CN": { "filterDisabled": "过滤无效规则", + "wildcardTip": "您可以使用通配符来匹配子域名或子页面!", "item": { "name": "规则名称", "condition": "限制网址", @@ -27,10 +28,8 @@ }, "button": { "test": "网址测试", - "option": "全局设置", - "parseUrl": "解析 URL" + "option": "全局设置" }, - "useWildcard": "是否使用通配符", "message": { "noUrl": "未配置限制网址", "noRule": "未填写任何规则", @@ -43,7 +42,6 @@ "rulesMatched": "该网址命中以下规则:", "timeout": "倒计时已结束 XD" }, - "urlPlaceholder": "请输入你想要限制的网址,然后点击解析按钮", "verification": { "inputTip": "规则已触发或者被锁定,如要继续操作,请在 {second} 秒内输入以下问题的答案:{prompt}", "inputTip2": "时限规则已触发或者被锁定,如要继续操作,请于 {second} 秒内在下列输入框中原样输入:{answer}", @@ -82,10 +80,8 @@ "rule": "規則設定" }, "button": { - "test": "測試網址", - "parseUrl": "解析網址" + "test": "測試網址" }, - "useWildcard": "是否使用萬用字元", "message": { "noUrl": "未填寫限制網址", "noRule": "未設定任何規則", @@ -95,7 +91,6 @@ "noRuleMatched": "此網址未符合任何規則", "rulesMatched": "此網址符合以下規則:" }, - "urlPlaceholder": "請輸入要限制的網址,然後點擊解析按鈕", "verification": { "inputTip": "規則已觸發或鎖定,請於 {second} 秒內回答:{prompt}", "inputTip2": "規則已觸發或鎖定。請在 {second} 秒內輸入:{answer}", @@ -110,6 +105,7 @@ }, "en": { "filterDisabled": "Only enabled", + "wildcardTip": "You can use wildcards to match subdomains or subpages!", "item": { "name": "Rule name", "condition": "Restricted URL", @@ -135,10 +131,8 @@ "rule": "Config rule" }, "button": { - "test": "Test URL", - "parseUrl": "Parse URL" + "test": "Test URL" }, - "useWildcard": "Whether to use wildcard", "message": { "noUrl": "No restriction URLs configured", "noRule": "No rules filled in", @@ -150,7 +144,6 @@ "rulesMatched": "The URL hits the following rules:", "timeout": "Time is up! XD" }, - "urlPlaceholder": "Please enter the URL you want to restrict and click the Parse button", "verification": { "inputTip": "The rule has been triggered or locked. To continue, please enter the answer to the following question within {second} seconds: {prompt}", "inputTip2": "The rule has been triggered or locked. To continue, please enter it as it is within {second} seconds: {answer}", @@ -189,10 +182,8 @@ "rule": "設定ルール" }, "button": { - "test": "テストURL", - "parseUrl": "解析 URL" + "test": "テストURL" }, - "useWildcard": "ワイルドカードを使用するかどうか", "message": { "noUrl": "埋められていない制限URL", "noRule": "ルールが記入されていません", @@ -202,7 +193,6 @@ "noRuleMatched": "URL がどのルールとも一致しません", "rulesMatched": "URL は次のルールに一致します。" }, - "urlPlaceholder": "制限したいURLを入力して解析ボタンをクリックしてください", "verification": { "inputTip": "ルールがトリガーされたかロックされました。続行するには、次の質問に対する回答を {second} 秒以内に入力してください: {prompt}", "inputTip2": "ルールがトリガーされたかロックされました。続行するには、{second} 秒以内にそのまま入力してください: {answer}", @@ -241,10 +231,8 @@ "rule": "Configurar regra" }, "button": { - "test": "Testar URL", - "parseUrl": "Analisar" + "test": "Testar URL" }, - "useWildcard": "Se deseja usar caractere curinga", "message": { "noUrl": "URL limitada não preenchida", "noRule": "Nenhuma regra preenchida", @@ -254,7 +242,6 @@ "noRuleMatched": "O URL não atinge nenhuma regra", "rulesMatched": "A URL atinge as seguintes regras:" }, - "urlPlaceholder": "Insira o URL que deseja restringir e clique no botão Analisar", "verification": { "inputTip": "A regra foi acionada ou bloqueada. Para continuar, introduza a resposta à seguinte questão em {second} segundos: {prompt}", "inputTip2": "A regra foi acionada ou bloqueada. Para continuar, digite-o tal como está em {segundos} segundos: {resposta}", @@ -293,10 +280,8 @@ "rule": "Правило конфігурації" }, "button": { - "test": "Тестова URL-адреса", - "parseUrl": "Обробити URL" + "test": "Тестова URL-адреса" }, - "useWildcard": "Використовувати символ підставлення", "message": { "noUrl": "Не заповнена обмежена URL-адреса", "noRule": "Не заповнено жодного правила", @@ -306,7 +291,6 @@ "noRuleMatched": "URL не відповідає жодному правилу", "rulesMatched": "URL-адреса отримує такі правила:" }, - "urlPlaceholder": "Введіть URL-адресу, яку ви хочете обмежити, а потім натисніть кнопку Обробити", "verification": { "inputTip": "Правило було запущено або заблоковано. Щоб продовжити, введіть відповідь на таке запитання протягом {second} секунд: {prompt}", "inputTip2": "Правило було запущено або заблоковано. Щоб продовжити, введіть його таким, яким він є, протягом {second} секунд: {answer}", @@ -341,10 +325,8 @@ "rule": "Configurar regla" }, "button": { - "test": "Probar URL", - "parseUrl": "Analizar URL" + "test": "Probar URL" }, - "useWildcard": "Cuando usar un comodín", "message": { "noUrl": "URL restringida sin completar", "noRule": "No hay reglas llenadas", @@ -354,7 +336,6 @@ "noRuleMatched": "La URL no sigue ninguna regla", "rulesMatched": "La URL sigue las siguientes reglas:" }, - "urlPlaceholder": "Ingrese la URL que desea restringir y haga clic en el botón Analizar", "verification": { "inputTip": "La regla se ha activado o bloqueado. Para continuar, responda la siguiente pregunta en menos de {second} segundos: {prompt}", "inputTip2": "La regla se ha activado o bloqueado. Para continuar, introdúzcala en {second} segundos: {answer}", @@ -388,10 +369,8 @@ "rule": "Konfiguration Regel" }, "button": { - "test": "Test-URL", - "parseUrl": "URL analysieren" + "test": "Test-URL" }, - "useWildcard": "Ob Platzhalter verwendet wird", "message": { "noUrl": "Nicht ausgefüllte eingeschränkte URL", "noRule": "Keine Regeln ausgefüllt", @@ -401,7 +380,6 @@ "noRuleMatched": "Die URL entspricht keinen Regeln", "rulesMatched": "Die URL erfüllt die folgenden Regeln:" }, - "urlPlaceholder": "Geben Sie bitte die URL ein, die Sie einschränken möchten, und klicken Sie auf die Schaltfläche „URL analysieren“", "verification": { "inputTip": "Die Regel wurde ausgelöst oder gesperrt. Um fortzufahren, geben Sie bitte innerhalb von {second} Sekunden die Antwort auf die folgende Frage ein: {prompt}", "inputTip2": "Die Regel wurde ausgelöst oder gesperrt. Um fortzufahren, geben Sie sie bitte innerhalb von {second} Sekunden unverändert ein: {answer}", @@ -434,10 +412,8 @@ "rule": "Règle de configuration" }, "button": { - "test": "Test URL", - "parseUrl": "Parse URL" + "test": "Test URL" }, - "useWildcard": "Utiliser des caractères génériques", "message": { "noUrl": "Aucune URL de restriction configurée", "noRule": "Aucune règle remplie", @@ -447,7 +423,6 @@ "noRuleMatched": "L'URL ne correspond à aucune règle", "rulesMatched": "L'URL atteint les règles suivantes :" }, - "urlPlaceholder": "Veuillez entrer l'URL que vous voulez restreindre et cliquez sur le bouton Analyser", "verification": { "inputTip": "La règle a été déclenchée ou verrouillée. Pour continuer, veuillez répondre à la question suivante dans un délai de {second} secondes : {prompt}", "inputTip2": "La règle a été déclenchée ou verrouillée. Pour continuer, veuillez la saisir telle quelle dans un délai de {second} secondes : {answer}", @@ -481,10 +456,8 @@ "rule": "Установить правило" }, "button": { - "test": "Тестовый URL", - "parseUrl": "Анализ URL" + "test": "Тестовый URL" }, - "useWildcard": "Использовать ли подстановку", "message": { "noUrl": "Ограничение URL-адресов не настроено", "noRule": "Нет заполненных правил", @@ -494,7 +467,6 @@ "noRuleMatched": "URL не содержит правил", "rulesMatched": "URL попадает в следующие правила:" }, - "urlPlaceholder": "Пожалуйста, введите URL, который вы хотите ограничить и нажмите на кнопку \"Анализ\"", "verification": { "inputTip": "Правило было активировано или заблокировано. Чтобы продолжить, введите ответ на следующий вопрос в течение {second} секунд: {prompt}", "inputTip2": "Правило было запущено или заблокировано. Чтобы продолжить, введите его как есть в течение {second} секунд: {answer}", @@ -527,10 +499,8 @@ "rule": "قاعدة التكوين" }, "button": { - "test": "اختبار عنوان URL", - "parseUrl": "تحليل عنوان URL" + "test": "اختبار عنوان URL" }, - "useWildcard": "هل يجب استخدام الأحرف البدل", "message": { "noUrl": "لم يتم تكوين عناوين URL الخاصة بالقيود", "noRule": "لم يتم ملء أي قواعد", @@ -540,7 +510,6 @@ "noRuleMatched": "عنوان URL لا يصطدم بأي قواعد", "rulesMatched": "يتوافق عنوان URL مع القواعد التالية:" }, - "urlPlaceholder": "الرجاء إدخال عنوان URL الذي تريد تقييده ثم انقر فوق الزر \"تحليل\"", "verification": { "inputTip": "تم تفعيل القاعدة أو قفلها. للمتابعة، يُرجى إدخال إجابة السؤال التالي خلال {second} ثانية: {prompt}", "inputTip2": "تم تفعيل القاعدة أو قفلها. للمتابعة، يُرجى إدخالها كما هي خلال {second} ثانية: {answer}", diff --git a/src/i18n/message/app/limit.ts b/src/i18n/message/app/limit.ts index bca56532d..e01ff833c 100644 --- a/src/i18n/message/app/limit.ts +++ b/src/i18n/message/app/limit.ts @@ -9,8 +9,7 @@ import resource from './limit-resource.json' export type LimitMessage = { filterDisabled: string - useWildcard: string - urlPlaceholder: string + wildcardTip: string step: { base: string url: string @@ -37,7 +36,6 @@ export type LimitMessage = { } button: { test: string - parseUrl: string } message: { noUrl: string diff --git a/src/i18n/message/common/button-resource.json b/src/i18n/message/common/button-resource.json index b99a854a1..d80daa374 100644 --- a/src/i18n/message/common/button-resource.json +++ b/src/i18n/message/common/button-resource.json @@ -1,6 +1,7 @@ { "en": { "create": "New", + "add": "Add", "delete": "Delete", "batchDelete": "Batch Delete", "modify": "Edit", @@ -22,6 +23,7 @@ }, "zh_CN": { "create": "新建", + "add": "添加", "delete": "删除", "batchDelete": "批量删除", "modify": "编辑", diff --git a/src/i18n/message/common/button.ts b/src/i18n/message/common/button.ts index 571e77577..eb5f4fe12 100644 --- a/src/i18n/message/common/button.ts +++ b/src/i18n/message/common/button.ts @@ -9,6 +9,7 @@ import resource from './button-resource.json' export type ButtonMessage = { create: string + add: string delete: string batchDelete: string modify: string diff --git a/src/pages/app/components/Limit/LimitFilter.tsx b/src/pages/app/components/Limit/LimitFilter.tsx index bf4ab4f2e..56d8bae43 100644 --- a/src/pages/app/components/Limit/LimitFilter.tsx +++ b/src/pages/app/components/Limit/LimitFilter.tsx @@ -16,19 +16,14 @@ import Flex from "@pages/components/Flex" import { getAppPageUrl } from "@util/constant/url" import { defineComponent } from "vue" import DropdownButton, { type DropdownButtonItem } from "../common/DropdownButton" -import { useLimitBatch, useLimitFilter } from "./context" +import { useLimitAction, useLimitBatch, useLimitFilter } from "./context" const optionPageUrl = getAppPageUrl(OPTION_ROUTE, { i: 'dailyLimit' }) type BatchOpt = 'delete' | 'enable' | 'disable' -type Props = { - onCreate?: NoArgCallback - onTest?: NoArgCallback -} - -const _default = defineComponent((props: Props) => { - const { onTest, onCreate } = props +const _default = defineComponent(() => { + const { create, test } = useLimitAction() const filter = useLimitFilter() const { batchDelete, batchDisable, batchEnable } = useLimitBatch() @@ -79,7 +74,7 @@ const _default = defineComponent((props: Props) => { text={t(msg => msg.limit.button.test)} type="primary" icon={} - onClick={onTest} + onClick={test} /> msg.base.option)} @@ -91,11 +86,11 @@ const _default = defineComponent((props: Props) => { text={t(msg => msg.button.create)} type="success" icon={} - onClick={onCreate} + onClick={create} /> ) -}, { props: ['onCreate', 'onTest'] }) +}) export default _default \ No newline at end of file diff --git a/src/pages/app/components/Limit/LimitModify/Sop/Step1.tsx b/src/pages/app/components/Limit/LimitModify/Sop/Step1.tsx index 9765e5e70..d4dd76e86 100644 --- a/src/pages/app/components/Limit/LimitModify/Sop/Step1.tsx +++ b/src/pages/app/components/Limit/LimitModify/Sop/Step1.tsx @@ -1,72 +1,44 @@ import { t } from "@app/locale" -import { useShadow } from "@hooks" -import { ElCol, ElForm, ElFormItem, ElInput, ElMessage, ElOption, ElRow, ElSelect, ElSwitch } from "element-plus" -import { defineComponent, type PropType, watch } from "vue" -import { type StepFromInstance } from "./common" +import { ElCol, ElForm, ElFormItem, ElInput, ElOption, ElRow, ElSelect, ElSwitch } from "element-plus" +import { defineComponent } from "vue" +import { useSopData } from "./context" -const _default = defineComponent({ - props: { - defaultName: String, - defaultEnabled: Boolean, - defaultWeekdays: Array as PropType, - }, - emits: { - change: (_name: string, _enabled: boolean, _weekdays: number[]) => true, - }, - setup(props, ctx) { - const [name, setName] = useShadow(() => props.defaultName) - const [enabled, setEnabled] = useShadow(() => props.defaultEnabled) - const [weekdays, setWeekdays] = useShadow(() => props.defaultWeekdays) - watch([enabled, name, weekdays], () => ctx.emit("change", name.value ?? '', !!enabled.value, weekdays.value ?? [])) +const _default = defineComponent(() => { + const data = useSopData() - const validate = () => { - const nameVal = name.value?.trim?.() - const weekdaysVal = weekdays.value - if (!nameVal) { - ElMessage.error("Name is empty") - return false - } if (!weekdaysVal?.length) { - ElMessage.error("Effective days are empty") - return false - } - return true - } - - ctx.expose({ validate } satisfies StepFromInstance) - return () => ( - - - - msg.limit.item.name)} required> - setName(undefined)} - /> - - - - msg.limit.item.enabled)} required> - setEnabled(v as boolean)} /> - - - - - - msg.limit.item.effectiveDay)} required> - - {t(msg => msg.calendar.weekDays).split('|').map((weekDay, idx) => )} - - - - - - ) - } + return () => ( + + + + msg.limit.item.name)} required> + data.name = val} + clearable onClear={() => data.name = ''} + /> + + + + msg.limit.item.enabled)} required> + data.enabled = !!v} /> + + + + + + msg.limit.item.effectiveDay)} required> + data.weekdays = v} + placeholder="" + > + {t(msg => msg.calendar.weekDays).split('|').map((weekDay, idx) => )} + + + + + + ) }) export default _default \ No newline at end of file diff --git a/src/pages/app/components/Limit/LimitModify/Sop/Step2.tsx b/src/pages/app/components/Limit/LimitModify/Sop/Step2.tsx new file mode 100644 index 000000000..4c71d52cb --- /dev/null +++ b/src/pages/app/components/Limit/LimitModify/Sop/Step2.tsx @@ -0,0 +1,97 @@ +/** + * 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, WarnTriangleFilled } from "@element-plus/icons-vue" +import { useState } from "@hooks/index" +import Flex from "@pages/components/Flex" +import { cleanCond } from "@util/limit" +import { ElButton, ElDivider, ElIcon, ElInput, ElLink, ElMessage, ElScrollbar, ElText, type ScrollbarInstance } from "element-plus" +import { type StyleValue, defineComponent, ref } from "vue" +import { useSopData } from "./context" + +const _default = defineComponent(() => { + const data = useSopData() + const scrollbar = ref() + + const handleAdd = () => { + let url: string | undefined = inputting.value?.trim?.()?.toLowerCase?.() + if (!url) return ElMessage.warning('URL is blank') + url = cleanCond(url) + if (!url) return ElMessage.warning('URL is invalid') + const urls = data.cond + if (urls.includes(url)) return ElMessage.warning('Duplicated URL') + urls.unshift(url) + scrollbar.value?.scrollTo(0) + + clearInputting() + } + + const handleRemove = (idx: number) => data.cond.splice(idx, 1) + + const [inputting, setInputting, clearInputting] = useState('') + + return () => ( + + + (ev as KeyboardEvent)?.code === "Enter" && handleAdd()} + v-slots={{ + append: () => ( + + {t(msg => msg.button.add)} + + ) + }} + placeholder="www.demo.com, *.demo.com, demo.com/blog/*, demo.com/**" + /> + + {t(msg => msg.limit.wildcardTip)} + + + + + + {data.cond?.map((url, idx) => ( + + {url} + } + type="danger" + onClick={() => handleRemove(idx)} + /> + + ))} + + + + + + + {t(msg => msg.limit.message.noUrl)} + + + + + ) +}) + +export default _default diff --git a/src/pages/app/components/Limit/LimitModify/Sop/Step2/UrlInput.tsx b/src/pages/app/components/Limit/LimitModify/Sop/Step2/UrlInput.tsx deleted file mode 100644 index 2aa7f9018..000000000 --- a/src/pages/app/components/Limit/LimitModify/Sop/Step2/UrlInput.tsx +++ /dev/null @@ -1,139 +0,0 @@ -import { t } from "@app/locale" -import { Check, Close, Cpu } from "@element-plus/icons-vue" -import { useState, useSwitch } from "@hooks" -import Flex from "@pages/components/Flex" -import { ElButton, ElInput, ElLink, ElMessage, ElOption, ElSelect, ElSwitch, ElTag, ElTooltip } from "element-plus" -import { defineComponent, reactive, type StyleValue, toRaw, toRefs, type VNode } from "vue" -import { parseUrl, Protocol, type UrlPart } from "../common" - -const ALL_PROTOCOLS: Protocol[] = ['http://', 'https://', '*://'] - -const combineTags = (arr: VNode[], current: VNode) => { - arr.length && arr.push(/) - arr.push(current) - return arr -} - -const Part = defineComponent({ - props: { - first: Boolean, - origin: String, - ignored: Boolean, - }, - emits: { - ignoredChange: (_val: boolean) => true, - delete: () => true, - }, - setup(props, ctx) { - const tagStyle: StyleValue = { height: '32px' } - const { origin, first, ignored } = toRefs(props) - return () => first.value - ? ( - - {origin.value} - - ) : ( - ctx.emit('delete')} - style={tagStyle} - > - - msg.limit.useWildcard)}> - ctx.emit('ignoredChange', val as boolean)} - /> - - {ignored.value ? '*' : origin.value} - - - ) - } -}) - -const _default = defineComponent({ - emits: { - save: (_url: string) => true - }, - setup(_, ctx) { - const [editing, openEditing, closeEditing] = useSwitch() - const [inputVal, setInputVal, resetInputVal] = useState() - const parts: UrlPart[] = reactive([]) - const [protocol, setProtocol] = useState("*://") - - const handleParse = () => { - const originUrl = inputVal.value?.trim?.() - if (!originUrl) return ElMessage.warning("URL is blank") - const { protocol, parts: newParts } = parseUrl(originUrl) - - setProtocol(protocol) - parts.splice(0, parts.length) - parts.push(...newParts || []) - openEditing() - } - - const handleSave = () => { - const url = protocol.value + parts?.map(part => { - const { origin, ignored } = toRaw(part) - return ignored ? '*' : origin - })?.join('/') || '-' - ctx.emit("save", url) - reset() - } - - const reset = () => { - closeEditing() - resetInputVal() - } - - return () => ( - - (ev as KeyboardEvent)?.code === "Enter" && handleParse()} - v-slots={{ - append: () => ( - } onClick={handleParse}> - {t(msg => msg.limit.button.parseUrl)} - - ) - }} - placeholder={t(msg => msg.limit.urlPlaceholder)} - /> - - - - {ALL_PROTOCOLS.map(p => )} - - {parts?.map((item, idx) => ( - parts.splice(idx, parts.length - idx)} - onIgnoredChange={newVal => item.ignored = newVal} - /> - )).reduce(combineTags, [])} - - - } onClick={reset}> - {t(msg => msg.button.cancel)} - - } onClick={handleSave} type="primary"> - {t(msg => msg.button.save)} - - - - - ) - } -}) - -export default _default diff --git a/src/pages/app/components/Limit/LimitModify/Sop/Step2/index.tsx b/src/pages/app/components/Limit/LimitModify/Sop/Step2/index.tsx deleted file mode 100644 index 630c34540..000000000 --- a/src/pages/app/components/Limit/LimitModify/Sop/Step2/index.tsx +++ /dev/null @@ -1,92 +0,0 @@ -/** - * 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 } from "@element-plus/icons-vue" -import Flex from "@pages/components/Flex" -import { ElDivider, ElLink, ElMessage, ElScrollbar, ElText, type ScrollbarInstance } from "element-plus" -import { type PropType, defineComponent, reactive, ref, toRaw, watch } from "vue" -import { type StepFromInstance } from "../common" -import UrlInput from "./UrlInput" - -const _default = defineComponent({ - props: { - modelValue: { - type: Object as PropType, - required: true, - }, - }, - emits: { - change: (_urls: string[]) => true, - }, - setup(props, ctx) { - const urls = reactive(props.modelValue) - - watch(() => props.modelValue, () => { - urls.splice(0, urls.length) - props.modelValue?.forEach(v => urls.push(v)) - }) - - const emitChange = () => ctx.emit('change', toRaw(urls)) - const scrollbar = ref() - - const validate = () => { - if (!urls?.length) { - ElMessage.error(t(msg => msg.limit.message.noUrl)) - return false - } - return true - } - ctx.expose({ validate } satisfies StepFromInstance) - - const handleSave = (url: string) => { - urls.unshift(url) - scrollbar.value?.scrollTo(0) - emitChange() - } - - const handleRemove = (idx: number) => { - urls.splice(idx, 1) - emitChange() - } - - return () => ( - - - - - - {urls?.map((url, idx) => ( - - {url} - } - type="danger" - onClick={() => handleRemove(idx)} - /> - - ))} - -
- - {t(msg => msg.limit.message.noUrl)} - -
-
-
- ) - } -}) - -export default _default diff --git a/src/pages/app/components/Limit/LimitModify/Sop/Step3/index.tsx b/src/pages/app/components/Limit/LimitModify/Sop/Step3/index.tsx index c5d47c51d..25acb2b8f 100644 --- a/src/pages/app/components/Limit/LimitModify/Sop/Step3/index.tsx +++ b/src/pages/app/components/Limit/LimitModify/Sop/Step3/index.tsx @@ -6,105 +6,56 @@ */ import { t } from "@app/locale" -import { useShadow } from "@hooks" import Flex from "@pages/components/Flex" -import { ElForm, ElFormItem, ElInputNumber, ElMessage } from "element-plus" -import { type PropType, defineComponent, watch } from "vue" -import { type StepFromInstance } from "../common" +import { ElForm, ElFormItem, ElInputNumber } from "element-plus" +import { defineComponent } from "vue" +import { useSopData } from "../context" import PeriodInput from "./PeriodInput" import TimeInput from "./TimeInput" -type Value = Pick - const MAX_HOUR_WEEKLY = 7 * 24 -const _default = defineComponent({ - props: { - time: Number, - visitTime: Number, - weekly: Number, - count: Number, - weeklyCount: Number, - periods: Array as PropType, - }, - emits: { - change: (_val: Value) => true, - }, - setup(props, ctx) { - // Time - const [time, setTime] = useShadow(() => props.time) - const [weekly, setWeekly] = useShadow(() => props.weekly) - const [visitTime, setVisitTime] = useShadow(() => props.visitTime) - // Visit count - const [count, setCount] = useShadow(() => props.count) - const [weeklyCount, setWeeklyCount] = useShadow(() => props.weeklyCount) - // Periods - const [periods, setPeriods] = useShadow(() => props.periods, []) - - watch([time, visitTime, periods, weekly, count, weeklyCount], () => { - const val: Value = { - time: time.value, - visitTime: visitTime.value, - weekly: weekly.value, - count: count.value, - weeklyCount: weeklyCount.value, - periods: periods.value, - } - ctx.emit("change", val) - }) - - const validate = () => { - if (true - && !time.value && !count.value - && !weekly.value && !weeklyCount.value - && !visitTime.value && !periods.value?.length - ) { - ElMessage.error(t(msg => msg.limit.message.noRule)) - return false - } - return true - } - ctx.expose({ validate } satisfies StepFromInstance) - - return () => ( - - - msg.limit.item.daily)}> - - - {t(msg => msg.limit.item.or)} - t(msg => msg.limit.item.visits) }} - /> - - - msg.limit.item.weekly)}> - - - {t(msg => msg.limit.item.or)} - t(msg => msg.limit.item.visits) }} - /> - - - msg.limit.item.visitTime)}> - - - msg.limit.item.period)}> - - - - - ) - } +const _default = defineComponent(() => { + const data = useSopData() + + return () => ( + + + msg.limit.item.daily)}> + + data.time = v} /> + {t(msg => msg.limit.item.or)} + data.count = v ?? 0} + 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)} + data.weeklyCount = v ?? 0} + v-slots={{ suffix: () => t(msg => msg.limit.item.visits) }} + /> + + + msg.limit.item.visitTime)}> + data.visitTime = v} /> + + msg.limit.item.period)}> + data.periods = v} /> + + + + ) }) export default _default \ No newline at end of file diff --git a/src/pages/app/components/Limit/LimitModify/Sop/common.ts b/src/pages/app/components/Limit/LimitModify/Sop/common.ts deleted file mode 100644 index d25518aba..000000000 --- a/src/pages/app/components/Limit/LimitModify/Sop/common.ts +++ /dev/null @@ -1,70 +0,0 @@ -/** - * Copyright (c) 2021 Hengyang Zhang - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -/** - * The protocol of rule host - */ -export type Protocol = - | 'http://' - | 'https://' - | '*://' - -export type UrlInfo = { - protocol: Protocol - parts: UrlPart[] -} - -export type UrlPart = { - /** - * The origin part text - */ - origin: string - /** - * Whether to replace with wildcard - */ - ignored: boolean -} - - -function cleanUrl(url: string): string { - if (!url) return url - - const querySign = url.indexOf('?') - querySign > -1 && (url = url.substring(0, querySign)) - const hashSign = url.indexOf('#') - hashSign > -1 && (url = url.substring(0, hashSign)) - return url -} - -export function parseUrl(url: string): UrlInfo { - let protocol: Protocol = '*://' - - url = decodeURI(url)?.trim() - if (url.startsWith('http://')) { - protocol = 'http://' - url = url.substring(protocol.length) - } else if (url.startsWith('https://')) { - protocol = 'https://' - url = url.substring(protocol.length) - } else if (url.startsWith('*://')) { - protocol = '*://' - url = url.substring(protocol.length) - } - url = cleanUrl(url) - return { protocol, parts: url2Parts(url) } -} - -const url2Parts = (url: string): UrlPart[] => { - if (!url) return [] - return url.split('/') - .filter(path => path) - .map(path => ({ origin: path, ignored: path === '*' })) -} - -export type StepFromInstance = { - validate: () => boolean -} \ No newline at end of file diff --git a/src/pages/app/components/Limit/LimitModify/Sop/context.ts b/src/pages/app/components/Limit/LimitModify/Sop/context.ts new file mode 100644 index 000000000..5391caf09 --- /dev/null +++ b/src/pages/app/components/Limit/LimitModify/Sop/context.ts @@ -0,0 +1,95 @@ +import { t } from "@app/locale" +import { useProvide, useProvider } from "@hooks/useProvider" +import { range } from "@util/array" +import { ElMessage } from "element-plus" +import { type Reactive, reactive, ref, toRaw } from "vue" + +type Step = 0 | 1 | 2 + +type SopData = Required> + +type Context = { + data: Reactive +} + +const createInitial = (): SopData => ({ + name: '', + time: 3600, + weekly: 0, + cond: [], + visitTime: 0, + periods: [], + enabled: true, + weekdays: range(7), + count: 0, + weeklyCount: 0, + allowDelay: false, + locked: false, +}) + +type Options = { + onSave?: (data: SopData) => void +} + +const NAMESPACE = 'limit_sop_model' + +export const initSop = ({ onSave }: Options) => { + const step = ref(0) + const data = reactive(createInitial()) + + const validator: Record Promise> = { + 0: async () => { + const nameVal = data.name?.trim?.() + const weekdaysVal = data.weekdays + if (!nameVal) { + ElMessage.error("Name is empty") + return false + } if (!weekdaysVal?.length) { + ElMessage.error("Effective days are empty") + return false + } + return true + }, + 1: async () => { + if (!data.cond?.length) { + ElMessage.error(t(msg => msg.limit.message.noUrl)) + return false + } + return true + }, + 2: async () => { + const { time, count, weekly, weeklyCount, visitTime, periods } = data + if (true + && !time && !count + && !weekly && !weeklyCount + && !visitTime && !periods?.length + ) { + ElMessage.error(t(msg => msg.limit.message.noRule)) + return false + } + return true + }, + } + + const reset = (rule?: timer.limit.Rule) => { + const rawRule = rule ? toRaw(rule) : createInitial() + Object.entries(rawRule).forEach(([k, v]) => (data as any)[k] = v) + step.value = 0 + } + + const handleNext = async () => { + const stepVal = step.value + const isValid = await validator[stepVal]?.() + if (isValid) { + stepVal === 2 ? onSave?.(toRaw(data)) : step.value++ + } + } + + useProvide(NAMESPACE, { data }) + + return { + step, reset, handleNext + } +} + +export const useSopData = () => useProvider(NAMESPACE, 'data').data \ No newline at end of file diff --git a/src/pages/app/components/Limit/LimitModify/Sop/index.tsx b/src/pages/app/components/Limit/LimitModify/Sop/index.tsx index 0f25a575c..46333b734 100644 --- a/src/pages/app/components/Limit/LimitModify/Sop/index.tsx +++ b/src/pages/app/components/Limit/LimitModify/Sop/index.tsx @@ -7,17 +7,13 @@ import DialogSop from "@app/components/common/DialogSop" import { t } from "@app/locale" -import { useState } from "@hooks" -import { range } from "@util/array" import { ElStep, ElSteps } from "element-plus" -import { type Ref, computed, defineComponent, reactive, ref, toRaw } from "vue" -import { StepFromInstance } from "./common" +import { computed, defineComponent } from "vue" +import { initSop } from "./context" import Step1 from "./Step1" import Step2 from "./Step2" import Step3 from "./Step3" -type Step = 0 | 1 | 2 - export type SopInstance = { /** * Reset with rule or initial value @@ -25,21 +21,6 @@ export type SopInstance = { reset: (rule?: timer.limit.Rule) => void } -const createInitial = (): Required> => ({ - name: '', - time: 3600, - weekly: 0, - cond: [], - visitTime: 0, - periods: [], - enabled: true, - weekdays: range(7), - count: 0, - weeklyCount: 0, - allowDelay: false, - locked: false, -}) - const _default = defineComponent({ props: { condDisabled: Boolean, @@ -49,41 +30,11 @@ const _default = defineComponent({ save: (_rule: MakeOptional) => true, }, setup(_, ctx) { - const [step, setStep] = useState(0) + const { reset, step, handleNext } = initSop({ onSave: data => ctx.emit('save', data) }) const last = computed(() => step.value === 2) const first = computed(() => step.value === 0) - const data = reactive(createInitial()) - const stepInstances: { [step in Step]: Ref } = { - 0: ref(), - 1: ref(), - 2: ref(), - } - - const reset = (rule?: timer.limit.Rule) => { - Object.entries(rule || createInitial()).forEach(([k, v]) => (data as any)[k] = v) - // Compatible with old items - if (!data.weekdays?.length) data.weekdays = range(7) - setStep(0) - } - ctx.expose({ reset } satisfies SopInstance) - const handleNext = () => { - const stepInst = stepInstances[step.value]?.value - if (!stepInst?.validate?.()) return - last.value ? ctx.emit("save", toRaw(data)) : step.value++ - } - - const handleUrlsChange = (urls: string[]) => { - let cond = data.cond - if (cond) { - cond.splice(0, data.cond?.length) - } else { - cond = data.cond = [] - } - urls.forEach(v => data.cond.push(v)) - } - return () => ( ), content: () => <> - { - data.name = name - data.enabled = enabled - data.weekdays = weekdays - }} - /> - - { - data.time = time ?? 0 - data.visitTime = visitTime ?? 0 - data.periods = periods ?? [] - data.weekly = weekly ?? 0 - data.count = count ?? 0 - data.weeklyCount = weeklyCount ?? 0 - }} - /> + + + }} /> diff --git a/src/pages/app/components/Limit/LimitModify/index.tsx b/src/pages/app/components/Limit/LimitModify/index.tsx index dff1ad1f0..78d91963d 100644 --- a/src/pages/app/components/Limit/LimitModify/index.tsx +++ b/src/pages/app/components/Limit/LimitModify/index.tsx @@ -10,84 +10,77 @@ import { useSwitch } from "@hooks" import limitService from "@service/limit-service" import { ElDialog, ElMessage } from "element-plus" import { computed, defineComponent, nextTick, ref, toRaw } from "vue" -import { useLimitTable } from "../context" +import { type ModifyInstance, useLimitTable } from "../context" import Sop, { type SopInstance } from "./Sop" -export type ModifyInstance = { - create(): void - modify(row: timer.limit.Item): void -} - type Mode = "create" | "modify" -const _default = defineComponent({ - setup: (_, ctx) => { - const { refresh } = useLimitTable() - const [visible, open, close] = useSwitch() - const sop = ref() - const mode = ref() - const title = computed(() => mode.value === "create" ? t(msg => msg.button.create) : t(msg => msg.button.modify)) - // Cache - let modifyingItem: timer.limit.Rule | undefined = undefined +const _default = defineComponent((_, ctx) => { + const { refresh } = useLimitTable() + const [visible, open, close] = useSwitch() + const sop = ref() + const mode = ref() + const title = computed(() => mode.value === "create" ? t(msg => msg.button.create) : t(msg => msg.button.modify)) + // Cache + let modifyingItem: timer.limit.Rule | undefined = undefined - const handleSave = async (rule: MakeOptional) => { - if (!rule) return - const { cond, enabled, name, time, weekly, visitTime, periods, weekdays, count, weeklyCount } = rule - let saved: timer.limit.Rule - if (mode.value === 'modify') { - if (!modifyingItem) return - saved = { - ...modifyingItem, - cond, enabled, name, time, weekly, visitTime, weekdays, count, weeklyCount, - // Object to array - periods: periods?.map(i => ([i?.[0], i?.[1]] satisfies Vector)), - } satisfies timer.limit.Rule - await limitService.update(saved) - } else { - const toCreate = { - cond, enabled, name, time, weekly, visitTime, weekdays, count, weeklyCount, - // Object to array - periods: periods?.map(i => ([i?.[0], i?.[1]] satisfies Vector)), - allowDelay: false, locked: false, - } satisfies MakeOptional - const id = await limitService.create(toCreate) - saved = { ...toCreate, id } - } - close() - ElMessage.success(t(msg => msg.operation.successMsg)) - sop.value?.reset?.() - refresh?.() + const handleSave = async (rule: MakeOptional) => { + if (!rule) return + const { cond, enabled, name, time, weekly, visitTime, periods, weekdays, count, weeklyCount } = rule + let saved: timer.limit.Rule + if (mode.value === 'modify') { + if (!modifyingItem) return + saved = { + ...modifyingItem, + cond, enabled, name, time, weekly, visitTime, weekdays, count, weeklyCount, + // Object to array + periods: periods?.map(i => ([i?.[0], i?.[1]] satisfies Vector)), + } satisfies timer.limit.Rule + await limitService.update(saved) + } else { + const toCreate = { + cond, enabled, name, time, weekly, visitTime, weekdays, count, weeklyCount, + // Object to array + periods: periods?.map(i => ([i?.[0], i?.[1]] satisfies Vector)), + allowDelay: false, locked: false, + } satisfies MakeOptional + const id = await limitService.create(toCreate) + saved = { ...toCreate, id } } + close() + ElMessage.success(t(msg => msg.operation.successMsg)) + sop.value?.reset?.() + refresh?.() + } - const instance: ModifyInstance = { - create() { - open() - mode.value = 'create' - modifyingItem = undefined - nextTick(() => sop.value?.reset()) - }, - modify(row: timer.limit.Item) { - open() - mode.value = 'modify' - modifyingItem = { ...row } - nextTick(() => sop.value?.reset?.(toRaw(row))) - }, - } + const instance: ModifyInstance = { + create() { + open() + mode.value = 'create' + modifyingItem = undefined + nextTick(() => sop.value?.reset()) + }, + modify(row: timer.limit.Item) { + open() + mode.value = 'modify' + modifyingItem = { ...row } + nextTick(() => sop.value?.reset?.(toRaw(row))) + }, + } - ctx.expose(instance) + ctx.expose(instance) - return () => ( - - - - ) - } + return () => ( + + + + ) }) export default _default \ No newline at end of file diff --git a/src/pages/app/components/Limit/LimitTable/column/LimitOperationColumn.tsx b/src/pages/app/components/Limit/LimitTable/column/LimitOperationColumn.tsx index 1ee3cc2a6..f0fd0e3a7 100644 --- a/src/pages/app/components/Limit/LimitTable/column/LimitOperationColumn.tsx +++ b/src/pages/app/components/Limit/LimitTable/column/LimitOperationColumn.tsx @@ -11,7 +11,7 @@ import { type ElTableRowScope } from "@pages/element-ui/table" import { ElButton, ElTableColumn } from "element-plus" import { defineComponent } from "vue" import { verifyCanModify } from "../../common" -import { useLimitTable } from "../../context" +import { useLimitAction, useLimitTable } from "../../context" const LOCALE_WIDTH: { [locale in timer.Locale]: number } = { en: 220, @@ -27,39 +27,35 @@ const LOCALE_WIDTH: { [locale in timer.Locale]: number } = { ar: 220, } -const _default = defineComponent({ - emits: { - rowModify: (_row: timer.limit.Item) => true, - }, - setup(_props, ctx) { - const { deleteRow } = useLimitTable() +const _default = defineComponent(() => { + const { deleteRow } = useLimitTable() + const { modify } = useLimitAction() - const handleModify = (row: timer.limit.Item) => verifyCanModify(row) - .then(() => ctx.emit("rowModify", row)) - .catch(() => {/** Do nothing */ }) + const handleModify = (row: timer.limit.Item) => verifyCanModify(row) + .then(() => modify(row)) + .catch(() => {/** Do nothing */ }) - return () => msg.button.operation)} - width={LOCALE_WIDTH[locale]} - align="center" - fixed="right" - v-slots={({ row }: ElTableRowScope) => <> - } onClick={() => deleteRow(row)}> - {t(msg => msg.button.delete)} - - } - onClick={() => handleModify(row)} - > - {t(msg => msg.button.modify)} - - - } - /> - }, + return () => msg.button.operation)} + width={LOCALE_WIDTH[locale]} + align="center" + fixed="right" + v-slots={({ row }: ElTableRowScope) => <> + } onClick={() => deleteRow(row)}> + {t(msg => msg.button.delete)} + + } + onClick={() => handleModify(row)} + > + {t(msg => msg.button.modify)} + + + } + /> }) export default _default diff --git a/src/pages/app/components/Limit/LimitTable/index.tsx b/src/pages/app/components/Limit/LimitTable/index.tsx index 8e180eabe..17a4874c6 100644 --- a/src/pages/app/components/Limit/LimitTable/index.tsx +++ b/src/pages/app/components/Limit/LimitTable/index.tsx @@ -29,155 +29,149 @@ 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({ - emits: { - modify: (_row: timer.limit.Item) => true, - }, - setup(_, ctx) { - const { data: weekStartName } = useRequest(async () => { - const offset = await weekHelper.getRealWeekStart() - const name = t(msg => msg.calendar.weekDays)?.split('|')?.[offset] - return name || 'NaN' - }) +const _default = defineComponent(() => { + 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 + } = useLimitTable() - const historySort = useLocalStorage('__limit_sort_default__', { prop: DEFAULT_SORT_COL, order: 'descending' }) + const historySort = useLocalStorage('__limit_sort_default__', { prop: DEFAULT_SORT_COL, order: 'descending' }) - return () => ( - historySort.value = { prop: val?.prop, order: val?.order }} + return () => ( + historySort.value = { prop: val?.prop, order: val?.order }} + > + + msg.limit.item.name)} + minWidth={120} + align="center" + formatter={({ name }: timer.limit.Item) => name || '-'} + fixed + sortable + sortBy={(row: timer.limit.Item) => row.name} + /> + msg.limit.item.condition)} + minWidth={180} + align="center" + formatter={({ cond }: timer.limit.Item) => <>{cond?.map?.(c => {c}) || ''}} + /> + msg.limit.item.detail)} + minWidth={200} + align="center" > - - msg.limit.item.name)} - minWidth={120} - align="center" - formatter={({ name }: timer.limit.Item) => name || '-'} - fixed - sortable - sortBy={(row: timer.limit.Item) => row.name} - /> - msg.limit.item.condition)} - minWidth={180} - align="center" - formatter={({ cond }: timer.limit.Item) => <>{cond?.map?.(c => {c}) || ''}} - /> + {({ row }: ElTableRowScope) => } + + msg.limit.item.effectiveDay)} + minWidth={170} + align="center" + sortable + sortMethod={sortByEffectiveDays} + > + {({ row: { weekdays } }: ElTableRowScope) => } + + msg.calendar.range.today)} + minWidth={90} + align="center" + > + {({ row }: ElTableRowScope) => isEffective(row.weekdays) ? ( + + ) : ( + + {t(msg => msg.limit.item.notEffective)} + + )} + + ( + msg.calendar.range.thisWeek)} + tooltipContent={t(msg => msg.limit.item.weekStartInfo, { weekStart: weekStartName.value })} + /> + ), + default: ({ row: { + weeklyWaste, weekly, + weeklyVisit, weeklyCount, + weeklyDelayCount, allowDelay, + } }: ElTableRowScope) => ( + + ), + }} + /> + msg.button.configuration)}> msg.limit.item.detail)} - minWidth={200} + label={t(msg => msg.limit.item.enabled)} + minWidth={80} align="center" + fixed="right" > - {({ row }: ElTableRowScope) => } + {({ row }: ElTableRowScope) => ( + changeEnabled(row, !!v)} /> + )} msg.limit.item.effectiveDay)} - minWidth={170} + label={t(msg => msg.limit.item.delayAllowed)} + minWidth={80} align="center" - sortable - sortMethod={sortByEffectiveDays} + fixed="right" > - {({ row: { weekdays } }: ElTableRowScope) => } + {({ row }: ElTableRowScope) => ( + changeDelay(row, !!v)} /> + )} msg.calendar.range.today)} - minWidth={90} + label={t(msg => msg.limit.item.locked)} + minWidth={80} align="center" + fixed="right" > - {({ row }: ElTableRowScope) => isEffective(row.weekdays) ? ( - - ) : ( - - {t(msg => msg.limit.item.notEffective)} - + {({ row }: ElTableRowScope) => ( + changeLocked(row, !!v)} /> )} - ( - msg.calendar.range.thisWeek)} - tooltipContent={t(msg => msg.limit.item.weekStartInfo, { weekStart: weekStartName.value })} - /> - ), - default: ({ row: { - weeklyWaste, weekly, - weeklyVisit, weeklyCount, - weeklyDelayCount, allowDelay, - } }: ElTableRowScope) => ( - - ), - }} - /> - msg.button.configuration)}> - msg.limit.item.enabled)} - minWidth={80} - align="center" - fixed="right" - > - {({ row }: ElTableRowScope) => ( - changeEnabled(row, !!v)} /> - )} - - msg.limit.item.delayAllowed)} - minWidth={80} - align="center" - fixed="right" - > - {({ row }: ElTableRowScope) => ( - changeDelay(row, !!v)} /> - )} - - msg.limit.item.locked)} - minWidth={80} - align="center" - fixed="right" - > - {({ row }: ElTableRowScope) => ( - changeLocked(row, !!v)} /> - )} - - - ctx.emit("modify", row)} /> - - ) - } + + + + ) }) export default _default \ No newline at end of file diff --git a/src/pages/app/components/Limit/LimitTest.tsx b/src/pages/app/components/Limit/LimitTest.tsx index 0e310b0a5..0f78ab604 100644 --- a/src/pages/app/components/Limit/LimitTest.tsx +++ b/src/pages/app/components/Limit/LimitTest.tsx @@ -10,10 +10,7 @@ import { useState, useSwitch } from "@hooks" import limitService from "@service/limit-service" import { AlertProps, ElAlert, ElButton, ElDialog, ElFormItem, ElInput } from "element-plus" import { computed, defineComponent } from "vue" - -export type TestInstance = { - show(): void -} +import { type TestInstance } from "./context" function computeResultTitle(url: string | undefined, inputting: boolean, matched: timer.limit.Rule[]): string { if (!url) { diff --git a/src/pages/app/components/Limit/context.ts b/src/pages/app/components/Limit/context.ts index a9cd4da3d..02f2bcf04 100644 --- a/src/pages/app/components/Limit/context.ts +++ b/src/pages/app/components/Limit/context.ts @@ -8,17 +8,28 @@ import { useRoute, useRouter } from "vue-router" import { verifyCanModify } from "./common" import type { LimitFilterOption } from "./types" +export type ModifyInstance = { + create(): void + modify(row: timer.limit.Item): void +} + +export type TestInstance = { + show(): void +} + type Context = { filter: Reactive list: Ref, refresh: NoArgCallback, deleteRow: ArgCallback - table: Ref batchDelete: NoArgCallback batchEnable: NoArgCallback batchDisable: NoArgCallback changeEnabled: (item: timer.limit.Item, val: boolean) => Promise changeDelay: (item: timer.limit.Item, val: boolean) => Promise changeLocked: (item: timer.limit.Item, val: boolean) => Promise + modify: (item: timer.limit.Item) => void + create: () => void + test: () => void } const NAMESPACE = 'limit' @@ -49,7 +60,7 @@ export const useLimitProvider = () => { await verifyCanModify(row) const message = t(msg => msg.limit.message.deleteConfirm, { name: row.name }) await ElMessageBox.confirm(message, { type: "warning" }) - limitService.remove(row) + await limitService.remove(row) }, { onSuccess() { ElMessage.success(t(msg => msg.operation.successMsg)) @@ -129,23 +140,35 @@ export const useLimitProvider = () => { } } + + const modifyInst = ref() + const testInst = ref() + const modify = (row: timer.limit.Item) => modifyInst.value?.modify?.(toRaw(row)) + const create = () => modifyInst.value?.create?.() + const test = () => testInst.value?.show?.() + useProvide(NAMESPACE, { filter, - list, refresh, table, + list, refresh, deleteRow, batchDelete: () => selectedAndThen(handleBatchDelete), batchEnable: () => selectedAndThen(handleBatchEnable), batchDisable: () => selectedAndThen(handleBatchDisable), changeEnabled, changeDelay, changeLocked, + modify, create, test, }) + + return { modifyInst, testInst } } export const useLimitFilter = (): Reactive => useProvider(NAMESPACE, "filter").filter -export const useLimitTable = () => useProvider( - NAMESPACE, 'list', 'refresh', 'deleteRow', 'table', 'changeEnabled', 'changeDelay', 'changeLocked' +export const useLimitTable = () => useProvider( + NAMESPACE, 'list', 'refresh', 'deleteRow', 'changeEnabled', 'changeDelay', 'changeLocked' ) export const useLimitBatch = () => useProvider( NAMESPACE, 'batchDelete', 'batchDisable', 'batchEnable' -) \ No newline at end of file +) + +export const useLimitAction = () => useProvider(NAMESPACE, 'modify', 'test', 'create') \ No newline at end of file diff --git a/src/pages/app/components/Limit/index.tsx b/src/pages/app/components/Limit/index.tsx index 4cc54e74d..77877f850 100644 --- a/src/pages/app/components/Limit/index.tsx +++ b/src/pages/app/components/Limit/index.tsx @@ -5,30 +5,24 @@ * https://opensource.org/licenses/MIT */ -import { defineComponent, ref, toRaw } from "vue" +import { defineComponent } from "vue" import ContentContainer from "../common/ContentContainer" import { useLimitProvider } from "./context" import LimitFilter from "./LimitFilter" -import LimitModify, { type ModifyInstance } from "./LimitModify" +import LimitModify from "./LimitModify" import LimitTable from "./LimitTable" -import LimitTest, { type TestInstance } from "./LimitTest" +import LimitTest from "./LimitTest" const _default = defineComponent(() => { - useLimitProvider() - - const modify = ref() - const test = ref() - const showModify = (row: timer.limit.Item) => modify.value?.modify?.(toRaw(row)) - const showCreate = () => modify.value?.create?.() - const showTest = () => test.value?.show?.() + const { modifyInst, testInst } = useLimitProvider() return () => ( , + filter: () => , content: () => <> - - - + + + }} /> ) diff --git a/src/service/limit-service/index.ts b/src/service/limit-service/index.ts index 80751b83f..c041d4a57 100644 --- a/src/service/limit-service/index.ts +++ b/src/service/limit-service/index.ts @@ -74,7 +74,7 @@ async function noticeLimitChanged() { }) } -async function updateEnabled(...items: timer.limit.Item[]): Promise { +async function updateEnabled(...items: timer.limit.Rule[]): Promise { if (!items?.length) return for (const item of items) { await db.updateEnabled(item.id, !!item.enabled) @@ -82,14 +82,14 @@ async function updateEnabled(...items: timer.limit.Item[]): Promise { await noticeLimitChanged() } -async function updateLocked(...items: timer.limit.Item[]): Promise { +async function updateLocked(...items: timer.limit.Rule[]): Promise { if (!items?.length) return for (const item of items) { await db.updateLocked(item.id, item.locked) } } -async function updateDelay(...items: timer.limit.Item[]) { +async function updateDelay(...items: timer.limit.Rule[]) { if (!items?.length) return for (const item of items) { await db.updateDelay(item.id, !!item.allowDelay) @@ -97,7 +97,7 @@ async function updateDelay(...items: timer.limit.Item[]) { await noticeLimitChanged() } -async function remove(...items: timer.limit.Item[]): Promise { +async function remove(...items: timer.limit.Rule[]): Promise { if (!items?.length) return for (const item of items) { await db.remove(item.id) @@ -203,8 +203,11 @@ async function moreMinutes(url: string): Promise { return rules.filter(r => !hasLimited(r)) } -async function update(rule: timer.limit.Rule) { - await db.save(rule, true) +async function update(...rules: timer.limit.Rule[]) { + if (!rules?.length) return + for (const rule of rules) { + await db.save(rule, true) + } await noticeLimitChanged() } diff --git a/src/util/limit.ts b/src/util/limit.ts index 09ba1b34a..546cb53dc 100644 --- a/src/util/limit.ts +++ b/src/util/limit.ts @@ -2,9 +2,21 @@ import { getWeekDay, MILL_PER_MINUTE, MILL_PER_SECOND } from "./time" export const DELAY_MILL = 5 * MILL_PER_MINUTE +export const cleanCond = (origin: string | undefined): string | undefined => { + if (!origin) return undefined + + const startIdx = origin?.indexOf('//') + const endIdx = origin?.indexOf('?') + let res = origin.substring(startIdx === -1 ? 0 : startIdx + 2, endIdx === -1 ? undefined : endIdx) + while (res.endsWith('/')) { + res = res.substring(0, res.length - 1) + } + return res || undefined +} + export function matches(cond: timer.limit.Item['cond'], url: string): boolean { return cond?.some?.( - c => new RegExp(`^${(c || '').split('*').join('.*')}`).test(url) + c => new RegExp(`^.*//${(c || '').split('*').join('.*')}`).test(url) ) } diff --git a/test-e2e/common/base.ts b/test-e2e/common/base.ts index 236f7eff7..4f8921a65 100644 --- a/test-e2e/common/base.ts +++ b/test-e2e/common/base.ts @@ -78,10 +78,10 @@ export function sleep(seconds: number): Promise { return new Promise(resolve => setTimeout(resolve, seconds * 1000)) } -export const MOCK_HOST = "localhost:12345" +export const MOCK_HOST = "127.0.0.1:12345" export const MOCK_URL = "http://" + MOCK_HOST -export const MOCK_HOST_2 = "localhost:12346" +export const MOCK_HOST_2 = "127.0.0.1:12346" export const MOCK_URL_2 = "http://" + MOCK_HOST_2 diff --git a/test-e2e/limit/common.ts b/test-e2e/limit/common.ts index 70763b15f..5e29d8463 100644 --- a/test-e2e/limit/common.ts +++ b/test-e2e/limit/common.ts @@ -16,10 +16,8 @@ export async function createLimitRule(rule: timer.limit.Rule, page: Page) { for (const url of rule.cond || []) { await configInput!.focus() await page.keyboard.type(url) - await new Promise(resolve => setTimeout(resolve, 100)) + await sleep(.1) await page.keyboard.press('Enter') - const saveBtn = await page.$('.el-dialog .el-link.el-link--primary') - await saveBtn!.click() } await sleep(.1) await page.click('.el-dialog .el-button.el-button--primary') @@ -35,6 +33,7 @@ export async function createLimitRule(rule: timer.limit.Rule, page: Page) { await fillVisitLimit(weeklyCount!, visitInputs[1], page) // 4. Save + await sleep(.3) await page.click('.el-dialog .el-button.el-button--success') } diff --git a/test/util/limit.test.ts b/test/util/limit.test.ts index bd3f4febb..c960b7248 100644 --- a/test/util/limit.test.ts +++ b/test/util/limit.test.ts @@ -1,8 +1,16 @@ -import { calcTimeState, dateMinute2Idx, hasLimited, hasWeeklyLimited, isEffective, isEnabledAndEffective, matches, meetLimit, meetTimeLimit, period2Str } from "@util/limit" +import { calcTimeState, cleanCond, dateMinute2Idx, hasLimited, hasWeeklyLimited, isEffective, isEnabledAndEffective, matches, meetLimit, meetTimeLimit, period2Str } from "@util/limit" describe('util/limit', () => { + test('cleanCond', () => { + expect(cleanCond('https://github.com?a=2')).toEqual('github.com') + expect(cleanCond('https://github.com/?a=2')).toEqual('github.com') + expect(cleanCond('*://github.com/?a=2')).toEqual('github.com') + expect(cleanCond('www.github.com/sheepzh/?a=2')).toEqual('www.github.com/sheepzh') + expect(cleanCond('https://')).toBeUndefined() + }) + test('matches', () => { - const cond = ['https://www.baidu.com', '*://*.google.com', '*://github.com/sheepzh'] + const cond = ['www.baidu.com', '*.google.com', 'github.com/sheepzh'] expect(matches(cond, 'https://www.baidu.com')).toBe(true) expect(matches(cond, 'http://hk.google.com')).toBe(true)