diff --git a/.commitlintrc.ts b/.commitlintrc.ts new file mode 100644 index 000000000..eaf00cf7f --- /dev/null +++ b/.commitlintrc.ts @@ -0,0 +1,47 @@ +import { + RuleConfigCondition, + RuleConfigSeverity, + TargetCaseType +} from "@commitlint/types" + +export default { + rules: { + "body-leading-blank": [RuleConfigSeverity.Error, "always"] as const, + "body-max-line-length": [RuleConfigSeverity.Error, "always", 100] as const, + "footer-leading-blank": [RuleConfigSeverity.Warning, "never"] as const, + "footer-max-line-length": [ + RuleConfigSeverity.Error, + "always", + 100, + ] as const, + "header-max-length": [RuleConfigSeverity.Error, "always", 100] as const, + "header-trim": [RuleConfigSeverity.Error, "always"] as const, + "subject-case": [ + RuleConfigSeverity.Error, + "never", + ["sentence-case", "start-case", "pascal-case", "upper-case"], + ] as [RuleConfigSeverity, RuleConfigCondition, TargetCaseType[]], + "subject-empty": [RuleConfigSeverity.Error, "never"] as const, + "subject-full-stop": [RuleConfigSeverity.Error, "never", "."] as const, + "type-case": [RuleConfigSeverity.Error, "always", "lower-case"] as const, + "type-empty": [RuleConfigSeverity.Error, "never"] as const, + "type-enum": [ + RuleConfigSeverity.Error, + "always", + [ + "build", + "chore", + "ci", + "docs", + "feat", + "fix", + "perf", + "refactor", + "revert", + "style", + "test", + ], + ] satisfies [RuleConfigSeverity, RuleConfigCondition, string[]], + }, + prompt: {}, +} \ No newline at end of file diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 000000000..0a7cfecfc --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,43 @@ +name: Publish to extension/addon store +on: + push: + branches: + - "release" +jobs: + publish: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Install dependencies + uses: actions/setup-node@v1 + with: + node-version: "v20.11.0" + - run: npm install + - name: Build for MV3 + run: npm run build + - name: Build for Firefox + run: npm run build:firefox + - name: Upload to chrome webstore + uses: mnao305/chrome-extension-upload@v5.0.0 + with: + file-path: market_packages/target.zip + extension-id: dkdhhcbjijekmneelocdllcldcpmekmm + client-id: ${{ secrets.CHROME_CLIENT_ID }} + client-secret: ${{ secrets.CHROME_CLIENT_SECRET }} + refresh-token: ${{ secrets.CHROME_REFRESH_TOKEN }} + # publish: false # Not to publish + - name: Upload to Edge addon store + uses: wdzeng/edge-addon@v2 + with: + zip-path: market_packages/target.zip + product-id: 2a99ae83-5ec8-4ad2-aa63-9a276fc708ce + client-id: ${{ secrets.EDGE_CLIENT_ID }} + api-key: ${{ secrets.EDGE_API_KEY}} + upload-only: true # Not to publish + - name: Upload to Firefox addon store + uses: cardinalby/webext-buildtools-firefox-addons-action@v1 + with: + extensionId: '{7b312f5e-9680-436b-acc1-9b09f60e8aaa}' + zipFilePath: market_packages/target.firefox.zip + jwtIssuer: ${{ secrets.FIREFOX_API_KEY }} + jwtSecret: ${{ secrets.FIREFOX_API_SECRET }} \ No newline at end of file diff --git a/.husky/commit-msg b/.husky/commit-msg new file mode 100644 index 000000000..0a4b97de5 --- /dev/null +++ b/.husky/commit-msg @@ -0,0 +1 @@ +npx --no -- commitlint --edit $1 diff --git a/.vscode/settings.json b/.vscode/settings.json index 39007a312..1a19a48e8 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -46,6 +46,7 @@ "webtime", "wfhg", "zcvf", + "Zhang", "zrender" ], "cSpell.ignorePaths": [ diff --git a/CHANGELOG.md b/CHANGELOG.md index 9422cc5b5..e0f7023ec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,11 +4,18 @@ All notable changes to Time Tracker will be documented in this file. It is worth mentioning that the release time of each change refers to the time when the installation package is submitted to the webstore. It is about one week for Edge to moderate packages, while only 1-2 days for Chrome and Firefox. +## [3.3.4] - 2025-03-26 + +- Fixed some bugs + +## [3.3.3] - 2025-03-22 + +- Fixed can't modify the name of site + ## [3.3.2] - 2025-03-20 - Fixed Reddit not blocked correctly - ## [3.3.1] - 2025-03-16 - Modified the English name of the extension diff --git a/jest.config.ts b/jest.config.ts index e649c6b6c..f3083f20b 100644 --- a/jest.config.ts +++ b/jest.config.ts @@ -9,15 +9,20 @@ const sourcePattern = /^(.*)\/\*$/ const moduleNameMapper: { [key: string]: string } = {} Object.entries(paths).forEach(([alias, sourceArr]) => { - if (!aliasPattern.test(alias)) { + const aliasMatch = alias.match(aliasPattern) + if (!aliasMatch) { return } - if (sourceArr.length !== 1 || !sourcePattern.test(sourceArr[0])) { + if (sourceArr.length !== 1) { return } - const prefix = alias.match(aliasPattern)[1] + const sourceMath = sourceArr[0]?.match(sourcePattern) + if (!sourceMath) { + return + } + const prefix = aliasMatch[1] const pattern = `^${prefix}/(.*)$` - const source = sourceArr[0].match(sourcePattern)[1] + const source = sourceMath[1] const sourcePath = `/${source}/$1` moduleNameMapper[pattern] = sourcePath }) diff --git a/package.json b/package.json index 976829457..c4f15078e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "timer", - "version": "3.3.2", + "version": "3.3.4", "description": "Time tracker", "homepage": "https://www.wfhg.cc", "scripts": { @@ -15,7 +15,8 @@ "analyze": "webpack --config=webpack/webpack.analyze.ts", "test": "jest --env=jsdom test/", "test-c": "jest --coverage --reporters=jest-junit --env=jsdom test/", - "test-e2e": "jest test-e2e/ --runInBand" + "test-e2e": "jest test-e2e/ --runInBand", + "prepare": "husky" }, "author": { "name": "zhy", @@ -33,7 +34,7 @@ "@rsdoctor/webpack-plugin": "^1.0.0", "@swc/core": "^1.11.11", "@swc/jest": "^0.2.37", - "@types/chrome": "0.0.310", + "@types/chrome": "0.0.312", "@types/decompress": "^4.2.7", "@types/echarts": "^5.0.0", "@types/generate-json-webpack-plugin": "^0.3.7", @@ -43,6 +44,7 @@ "@types/webpack": "^5.28.5", "@vue/babel-plugin-jsx": "^1.4.0", "babel-loader": "^10.0.0", + "commitlint": "^19.8.0", "copy-webpack-plugin": "^13.0.0", "css-loader": "^7.1.2", "decompress": "^4.2.1", @@ -50,6 +52,7 @@ "filemanager-webpack-plugin": "^8.0.0", "generate-json-webpack-plugin": "^2.0.0", "html-webpack-plugin": "^5.6.3", + "husky": "^9.1.7", "jest": "^29.7.0", "jest-environment-jsdom": "^29.7.0", "jest-junit": "^16.0.0", @@ -75,7 +78,7 @@ "@vueuse/core": "^13.0.0", "countup.js": "^2.8.0", "echarts": "^5.6.0", - "element-plus": "2.9.6", + "element-plus": "2.9.7", "js-base64": "^3.7.7", "punycode": "^2.3.1", "stream-browserify": "^3.0.0", diff --git a/script/crowdin/client.ts b/script/crowdin/client.ts index 9e351bf97..1cbc7bf2a 100644 --- a/script/crowdin/client.ts +++ b/script/crowdin/client.ts @@ -36,7 +36,7 @@ class PaginationIterator { this.cursor = 0 } - async findFirst(predicate: (ele: T) => boolean): Promise { + async findFirst(predicate: (ele: T) => boolean): Promise { while (true) { const data = await this.next() if (!data) { @@ -50,7 +50,7 @@ class PaginationIterator { } async findAll(predicate?: ((ele: T) => boolean)): Promise { - const result = [] + const result: T[] = [] while (true) { const data = await this.next() if (!data) { @@ -63,7 +63,7 @@ class PaginationIterator { return result } - async next(): Promise { + async next(): Promise { if (this.isEnd) { return undefined } @@ -90,54 +90,6 @@ class PaginationIterator { } } -async function createStorage(fileName: string, content: any): Promise { - const response = await this.crowdin.uploadStorageApi.addStorage(fileName, content) - return response.data -} - -async function createFile(this: CrowdinClient, - directoryId: number, - storage: UploadStorageModel.Storage, - fileName: string -): Promise { - const request: SourceFilesModel.CreateFileRequest = { - name: fileName, - storageId: storage.id, - directoryId, - type: 'json', - } - const response = await this.crowdin.sourceFilesApi.createFile(PROJECT_ID, request) - return response.data -} - -async function restoreFile(this: CrowdinClient, storage: UploadStorageModel.Storage, existFile: SourceFilesModel.File): Promise { - const response = await this.crowdin.sourceFilesApi.updateOrRestoreFile(PROJECT_ID, existFile.id, { storageId: storage.id }) - return response.data -} - -async function getMainBranch(this: CrowdinClient): Promise { - return new PaginationIterator( - pagination => this.crowdin.sourceFilesApi.listProjectBranches(PROJECT_ID, { ...pagination }) - ).findFirst(e => e.name === MAIN_BRANCH_NAME) -} - -async function createMainBranch(): Promise { - const request: SourceFilesModel.CreateBranchRequest = { - name: MAIN_BRANCH_NAME - } - const res = await this.crowdin.sourceFilesApi.createBranch(PROJECT_ID, request) - return res.data -} - -async function getOrCreateMainBranch(this: CrowdinClient): Promise { - let branch = await this.getMainBranch() - if (!branch) { - branch = await this.createMainBranch() - } - console.info("getOrCreateMainBranch: " + JSON.stringify(branch)) - return branch -} - /** * Key of crowdin file/directory */ @@ -146,124 +98,11 @@ export type NameKey = { branchId: number } -function getFileByName(this: CrowdinClient, param: NameKey): Promise { - return new PaginationIterator( - p => this.crowdin.sourceFilesApi.listProjectFiles(PROJECT_ID, { ...p, branchId: param.branchId }) - ).findFirst(t => t.name === param.name) -} - -function getDirByName(this: CrowdinClient, param: NameKey): Promise { - return new PaginationIterator( - p => this.crowdin.sourceFilesApi.listProjectDirectories(PROJECT_ID, { ...p, branchId: param.branchId }) - ).findFirst(d => d.name === param.name) -} - -async function createDirectory(this: CrowdinClient, param: NameKey): Promise { - const res = await this.crowdin.sourceFilesApi.createDirectory(PROJECT_ID, { - name: param.name, - branchId: param.branchId, - }) - return res.data -} - -function listFilesByDirectory(this: CrowdinClient, directoryId: number) { - return new PaginationIterator( - p => this.crowdin.sourceFilesApi.listProjectFiles(PROJECT_ID, { ...p, directoryId: directoryId }) - ).findAll() -} - -function listStringsByFile(this: CrowdinClient, fileId: number): Promise { - return new PaginationIterator( - p => this.crowdin.sourceStringsApi.listProjectStrings(PROJECT_ID, { ...p, fileId: fileId }) - ).findAll() -} - -async function batchCreateString( - this: CrowdinClient, - fileId: number, - content: ItemSet, -): Promise { - for (const [path, value] of Object.entries(content)) { - const request: SourceStringsModel.CreateStringRequest = { - fileId, - text: value, - identifier: path, - } - console.log(`Try to create new string: ${JSON.stringify(request)}`) - await this.crowdin.sourceStringsApi.addString(PROJECT_ID, request) - } -} - -async function batchUpdateIfNecessary(this: CrowdinClient, - content: ItemSet, - existStringsKeyMap: { [path: string]: SourceStringsModel.String } -): Promise { - console.log("=========start to update strings========") - console.log("Content length: " + Object.keys(content).length) - for (const [path, value] of Object.entries(content)) { - const string = existStringsKeyMap[path] - const patch: PatchRequest[] = [] - string?.text !== value && patch.push({ - op: 'replace', - path: '/text', - value: value - }) - if (!patch.length) { - continue - } - console.log('Try to edit string: ' + string.identifier) - await this.crowdin.sourceStringsApi.editString(PROJECT_ID, string.id, patch) - } - console.log("=========end to update strings========") -} - -async function batchDeleteString(this: CrowdinClient, stringIds: number[]): Promise { - console.log("=========start to delete strings========") - for (const stringId of stringIds) { - await this.crowdin.sourceStringsApi.deleteString(PROJECT_ID, stringId) - console.log("Delete string: id=" + stringId) - } - console.log("=========end to delete strings========") -} - export type TranslationKey = { stringId: number lang: CrowdinLanguage } -async function existTranslationByStringAndLang(this: CrowdinClient, transKey: TranslationKey): Promise { - const { stringId, lang } = transKey - const trans = await new PaginationIterator( - p => this.crowdin.stringTranslationsApi.listStringTranslations(PROJECT_ID, stringId, lang, { ...p }) - ).findFirst(_ => true) - return !!trans -} - -async function createTranslation(this: CrowdinClient, transKey: TranslationKey, text: string) { - const { stringId, lang } = transKey - const request: StringTranslationsModel.AddStringTranslationRequest = { - stringId, - languageId: lang, - text - } - await this.crowdin.stringTranslationsApi.addTranslation(PROJECT_ID, request) -} - -async function buildProjectTranslation(this: CrowdinClient, branchId: number) { - const buildRes = await this.crowdin.translationsApi.buildProject(PROJECT_ID, { - branchId, - targetLanguageIds: [...ALL_CROWDIN_LANGUAGES], - skipUntranslatedStrings: true, - }) - const buildId = buildRes?.data?.id - while (true) { - // Wait finished - const res = await this.crowdin.translationsApi.downloadTranslations(PROJECT_ID, buildId) - const url = res?.data?.url - if (url) return url - } -} - /** * The wrapper of client with auth */ @@ -277,46 +116,175 @@ export class CrowdinClient { this.crowdin = new Crowdin(credentials) console.info("Initialized client successfully") } - createStorage = createStorage + + async createStorage(fileName: string, content: any): Promise { + const response = await this.crowdin.uploadStorageApi.addStorage(fileName, content) + return response.data + } + /** * Get the main branch * * @returns main branch or undefined */ - getMainBranch = getMainBranch + + async getMainBranch(): Promise { + return new PaginationIterator( + pagination => this.crowdin.sourceFilesApi.listProjectBranches(PROJECT_ID, { ...pagination }) + ).findFirst(e => e.name === MAIN_BRANCH_NAME) + } /** * Create the main branch */ - createMainBranch = createMainBranch + async createMainBranch(): Promise { + const request: SourceFilesModel.CreateBranchRequest = { + name: MAIN_BRANCH_NAME + } + const res = await this.crowdin.sourceFilesApi.createBranch(PROJECT_ID, request) + return res.data + } - getOrCreateMainBranch = getOrCreateMainBranch + async getOrCreateMainBranch(): Promise { + let branch = await this.getMainBranch() + if (!branch) { + branch = await this.createMainBranch() + } + console.info("getOrCreateMainBranch: " + JSON.stringify(branch)) + return branch + } - createFile = createFile + async createFile( + directoryId: number, + storage: UploadStorageModel.Storage, + fileName: string + ): Promise { + const request: SourceFilesModel.CreateFileRequest = { + name: fileName, + storageId: storage.id, + directoryId, + type: 'json', + } + const response = await this.crowdin.sourceFilesApi.createFile(PROJECT_ID, request) + return response.data + } - restoreFile = restoreFile + async restoreFile(storage: UploadStorageModel.Storage, existFile: SourceFilesModel.File): Promise { + const response = await this.crowdin.sourceFilesApi.updateOrRestoreFile(PROJECT_ID, existFile.id, { storageId: storage.id }) + return response.data + } - getFileByName = getFileByName + getFileByName(param: NameKey): Promise { + return new PaginationIterator( + p => this.crowdin.sourceFilesApi.listProjectFiles(PROJECT_ID, { ...p, branchId: param.branchId }) + ).findFirst(t => t.name === param.name) + } - getDirByName = getDirByName + getDirByName(param: NameKey): Promise { + return new PaginationIterator( + p => this.crowdin.sourceFilesApi.listProjectDirectories(PROJECT_ID, { ...p, branchId: param.branchId }) + ).findFirst(d => d.name === param.name) + } - createDirectory = createDirectory + async createDirectory(param: NameKey): Promise { + const res = await this.crowdin.sourceFilesApi.createDirectory(PROJECT_ID, { + name: param.name, + branchId: param.branchId, + }) + return res.data + } - listFilesByDirectory = listFilesByDirectory + listFilesByDirectory(directoryId: number) { + return new PaginationIterator( + p => this.crowdin.sourceFilesApi.listProjectFiles(PROJECT_ID, { ...p, directoryId: directoryId }) + ).findAll() + } - listStringsByFile = listStringsByFile + listStringsByFile(fileId: number): Promise { + return new PaginationIterator( + p => this.crowdin.sourceStringsApi.listProjectStrings(PROJECT_ID, { ...p, fileId: fileId }) + ).findAll() + } - batchCreateString = batchCreateString + async batchCreateString( + fileId: number, + content: ItemSet, + ): Promise { + for (const [path, value] of Object.entries(content)) { + const request: SourceStringsModel.CreateStringRequest = { + fileId, + text: value, + identifier: path, + } + console.log(`Try to create new string: ${JSON.stringify(request)}`) + await this.crowdin.sourceStringsApi.addString(PROJECT_ID, request) + } + } - batchUpdateIfNecessary = batchUpdateIfNecessary + async batchUpdateIfNecessary( + content: ItemSet, + existStringsKeyMap: { [path: string]: SourceStringsModel.String } + ): Promise { + console.log("=========start to update strings========") + console.log("Content length: " + Object.keys(content).length) + for (const [path, value] of Object.entries(content)) { + const string = existStringsKeyMap[path] + const patch: PatchRequest[] = [] + string?.text !== value && patch.push({ + op: 'replace', + path: '/text', + value: value + }) + if (!patch.length) { + continue + } + console.log('Try to edit string: ' + string.identifier) + await this.crowdin.sourceStringsApi.editString(PROJECT_ID, string.id, patch) + } + console.log("=========end to update strings========") + } - batchDeleteString = batchDeleteString + async batchDeleteString(stringIds: number[]): Promise { + console.log("=========start to delete strings========") + for (const stringId of stringIds) { + await this.crowdin.sourceStringsApi.deleteString(PROJECT_ID, stringId) + console.log("Delete string: id=" + stringId) + } + console.log("=========end to delete strings========") + } - existTranslationByStringAndLang = existTranslationByStringAndLang + async existTranslationByStringAndLang(transKey: TranslationKey): Promise { + const { stringId, lang } = transKey + const trans = await new PaginationIterator( + p => this.crowdin.stringTranslationsApi.listStringTranslations(PROJECT_ID, stringId, lang, { ...p }) + ).findFirst(_ => true) + return !!trans + } - createTranslation = createTranslation + async createTranslation(transKey: TranslationKey, text: string) { + const { stringId, lang } = transKey + const request: StringTranslationsModel.AddStringTranslationRequest = { + stringId, + languageId: lang, + text + } + await this.crowdin.stringTranslationsApi.addTranslation(PROJECT_ID, request) + } - buildProjectTranslation = buildProjectTranslation + async buildProjectTranslation(branchId: number) { + const buildRes = await this.crowdin.translationsApi.buildProject(PROJECT_ID, { + branchId, + targetLanguageIds: [...ALL_CROWDIN_LANGUAGES], + skipUntranslatedStrings: true, + }) + const buildId = buildRes?.data?.id + while (true) { + // Wait finished + const res = await this.crowdin.translationsApi.downloadTranslations(PROJECT_ID, buildId) + const url = res?.data?.url + if (url) return url + } + } } /** diff --git a/script/crowdin/common.ts b/script/crowdin/common.ts index e60b1128b..d02a4399a 100644 --- a/script/crowdin/common.ts +++ b/script/crowdin/common.ts @@ -1,3 +1,4 @@ +import { type SourceFilesModel } from '@crowdin/crowdin-api-client' import fs from 'fs' import path from 'path' import { exitWith } from '../util/process' @@ -29,7 +30,7 @@ export type CrowdinLanguage = typeof ALL_CROWDIN_LANGUAGES[number] export const SOURCE_LOCALE: timer.SourceLocale = 'en' // Not include en and zh_CN -export const ALL_TRANS_LOCALES: timer.Locale[] = [ +export const ALL_TRANS_LOCALES: timer.OptionalLocale[] = [ 'ja', 'zh_TW', 'pt_PT', @@ -41,7 +42,7 @@ export const ALL_TRANS_LOCALES: timer.Locale[] = [ 'ar', ] -const CROWDIN_I18N_MAP: Record = { +const CROWDIN_I18N_MAP: Record = { ja: 'ja', 'zh-TW': 'zh_TW', 'pt-PT': 'pt_PT', @@ -65,7 +66,7 @@ const I18N_CROWDIN_MAP: Record = { ar: 'ar', } -export const crowdinLangOf = (locale: timer.Locale) => I18N_CROWDIN_MAP[locale] +export const crowdinLangOf = (locale: timer.OptionalLocale): CrowdinLanguage => I18N_CROWDIN_MAP[locale] export const localeOf = (crowdinLang: CrowdinLanguage) => CROWDIN_I18N_MAP[crowdinLang] @@ -95,7 +96,7 @@ export async function readAllMessages(dir: Dir): Promise = {} await Promise.all(files.map(async file => { if (!file.endsWith(RSC_FILE_SUFFIX)) { return @@ -146,7 +147,7 @@ export async function mergeMessage( const pathSeg = path.split('.') fillItem(pathSeg, 0, newMessage, text) }) - Object.entries(newMessage).length && (existMessages[locale] = newMessage) + Object.entries(newMessage).length && (existMessages[locale as timer.Locale] = newMessage) }) const newFileContent = JSON.stringify(existMessages, null, 4) @@ -173,7 +174,7 @@ function checkPlaceholder(translated: string, source: string) { return true } -function fillItem(fields: string[], index: number, obj: Object, text: string) { +function fillItem(fields: string[], index: number, obj: Record, text: string) { const field = fields[index] if (index === fields.length - 1) { obj[field] = text @@ -192,7 +193,7 @@ function fillItem(fields: string[], index: number, obj: Object, text: string) { * Trans msg object to k-v map */ export function transMsg(message: any, prefix?: string): ItemSet { - const result = {} + const result: Record = {} const pathPrefix = prefix ? prefix + '.' : '' Object.entries(message).forEach(([key, value]) => { const path = pathPrefix + key @@ -210,10 +211,10 @@ export function transMsg(message: any, prefix?: string): ItemSet { return result } -export async function checkMainBranch(client: CrowdinClient) { +export async function checkMainBranch(client: CrowdinClient): Promise { const branch = await client.getMainBranch() if (!branch) { exitWith("Main branch is null") } - return branch + return branch! } diff --git a/script/crowdin/sync-translation.ts b/script/crowdin/sync-translation.ts index 7b62300af..fd0c6af41 100644 --- a/script/crowdin/sync-translation.ts +++ b/script/crowdin/sync-translation.ts @@ -43,7 +43,7 @@ async function processDir(client: CrowdinClient, dir: Dir, branch: SourceFilesMo if (!directory) { exitWith("Directory not found: " + dir) } - const files = await client.listFilesByDirectory(directory.id) + const files = await client.listFilesByDirectory(directory!.id) console.log(`find ${files.length} files of ${dir}`) const fileMap = groupBy(files, f => f.name, l => l[0]) for (const [fileName, message] of Object.entries(messages)) { diff --git a/script/user-chart/add.ts b/script/user-chart/add.ts index 7f1d4b1eb..5021576f5 100644 --- a/script/user-chart/add.ts +++ b/script/user-chart/add.ts @@ -1,13 +1,14 @@ import { createGist as createGistApi, + type FileForm, getJsonFileContent, type Gist, type GistForm, updateGist as updateGistApi } from "@src/api/gist" import fs from "fs" -import { descriptionOf, filenameOf, getExistGist, validateTokenFromEnv } from "./common" import { exitWith } from "../util/process" +import { descriptionOf, filenameOf, getExistGist, validateTokenFromEnv } from "./common" type AddArgv = { browser: Browser @@ -44,7 +45,7 @@ async function createGist(token: string, browser: Browser, data: UserCount) { const sorted: UserCount = {} Object.keys(data).sort().forEach(key => sorted[key] = data[key]) // 2. create - const files = {} + const files: Record = {} files[filename] = { filename: filename, content: JSON.stringify(sorted, null, 2) } const gistForm: GistForm = { public: true, @@ -59,12 +60,12 @@ async function updateGist(token: string, browser: Browser, data: UserCount, gist const filename = filenameOf(browser) // 1. merge const file = gist.files[filename] - const existData = (await getJsonFileContent(file)) || {} + const existData = (await getJsonFileContent(file!)) || {} Object.entries(data).forEach(([key, val]) => existData[key] = val) // 2. sort by key const sorted: UserCount = {} Object.keys(existData).sort().forEach(key => sorted[key] = existData[key]) - const files = {} + const files: Record = {} files[filename] = { filename: filename, content: JSON.stringify(sorted, null, 2) } const gistForm: GistForm = { public: true, @@ -76,7 +77,7 @@ async function updateGist(token: string, browser: Browser, data: UserCount, gist function parseChrome(content: string): UserCount { const lines = content.split('\n') - const result = {} + const result: Record = {} if (!(lines?.length > 2)) { return result } @@ -95,7 +96,7 @@ function parseChrome(content: string): UserCount { function parseEdge(content: string): UserCount { const lines = content.split('\n') - const result = {} + const result: Record = {} if (!(lines?.length > 1)) { return result } @@ -116,7 +117,7 @@ function parseEdge(content: string): UserCount { function parseFirefox(content: string): UserCount { const lines = content.split('\n') - const result = {} + const result: Record = {} if (!(lines?.length > 4)) { return result } diff --git a/script/user-chart/common.ts b/script/user-chart/common.ts index 6a7755839..6b69633d4 100644 --- a/script/user-chart/common.ts +++ b/script/user-chart/common.ts @@ -9,7 +9,7 @@ export function validateTokenFromEnv(): string { if (!token) { exitWith("Can't find token from env variable [TIMER_USER_COUNT_GIST_TOKEN]") } - return token + return token! } /** @@ -26,6 +26,6 @@ export function filenameOf(browser: Browser): string { return descriptionOf(browser) + '.json' } -export async function getExistGist(token: string, browser: Browser): Promise { +export async function getExistGist(token: string, browser: Browser): Promise { return await findTarget(token, gist => gist.description === descriptionOf(browser)) } \ No newline at end of file diff --git a/script/user-chart/render.ts b/script/user-chart/render.ts index 0e42515d5..f12a09516 100644 --- a/script/user-chart/render.ts +++ b/script/user-chart/render.ts @@ -3,7 +3,6 @@ import { type FileForm, findTarget, getJsonFileContent, - type Gist, type GistForm, updateGist } from "@api/gist" @@ -109,7 +108,7 @@ class SmoothContext { function zoom(data: T[], reduction: number): T[] { let i = 0 - const newData = [] + const newData: T[] = [] while (i < data.length) { newData.push(data[i]) i += reduction @@ -174,9 +173,9 @@ async function getOriginData(token: string): Promise { * Get the data from gist */ async function getDataFromGist(token: string, browser: Browser): Promise { - const gist: Gist = await getExistGist(token, browser) + const gist = await getExistGist(token, browser) const file = gist?.files[filenameOf(browser)] - return file ? getJsonFileContent(file) : {} + return (file && await getJsonFileContent(file)) ?? {} } /** diff --git a/script/util/process.ts b/script/util/process.ts index d941ad86f..ce80dc4ce 100644 --- a/script/util/process.ts +++ b/script/util/process.ts @@ -1,4 +1,7 @@ +/** + * @throws Will invoke ```process.exit()``` + */ export function exitWith(msg: string) { console.error(msg) process.exit() diff --git a/src/api/chrome/action.ts b/src/api/chrome/action.ts index 846a737e7..8ae11e46d 100644 --- a/src/api/chrome/action.ts +++ b/src/api/chrome/action.ts @@ -3,23 +3,19 @@ import { handleError } from "./common" const action = IS_MV3 ? chrome.action : chrome.browserAction -export function setBadgeText(text: string, tabId: number): Promise { +export function setBadgeText(text: string, tabId: number | undefined): Promise { return new Promise(resolve => action?.setBadgeText({ text, tabId }, () => { handleError('setBadgeText') resolve() })) } -export function setBadgeBgColor(color: string | chrome.action.ColorArray): Promise { - if (!color) { - if (IS_FIREFOX) { - // Use null to clear bg color for Firefox - color = null - } else { - color = [0, 0, 0, 0] - } - } - return new Promise(resolve => action?.setBadgeBackgroundColor({ color }, () => { +export function setBadgeBgColor(color: string | chrome.action.ColorArray | undefined): Promise { + let realColor: string | chrome.action.ColorArray = color ?? ( + // Use null to clear bg color for Firefox + IS_FIREFOX ? null as unknown as string : [0, 0, 0, 0] + ) + return new Promise(resolve => action?.setBadgeBackgroundColor({ color: realColor }, () => { handleError('setBadgeColor') resolve() })) diff --git a/src/api/chrome/common.ts b/src/api/chrome/common.ts index 9ecee98d6..f5d59988b 100644 --- a/src/api/chrome/common.ts +++ b/src/api/chrome/common.ts @@ -1,10 +1,10 @@ -export function handleError(scene: string, e?: any): string { +export function handleError(scene: string, e?: any): string | undefined { try { - const lastError = chrome.runtime.lastError ?? e + const lastError = chrome.runtime.lastError ?? (e as Error) lastError && console.log(`Errored when ${scene}: ${lastError?.message}`) return lastError?.message } catch (e) { console.info("Can't execute here") } - return null + return undefined } \ No newline at end of file diff --git a/src/api/chrome/context-menu.ts b/src/api/chrome/context-menu.ts index c2a2406ea..448573615 100644 --- a/src/api/chrome/context-menu.ts +++ b/src/api/chrome/context-menu.ts @@ -8,23 +8,22 @@ function onClick(id: string, handler: Function) { chrome.contextMenus?.onClicked?.addListener(({ menuItemId }) => menuItemId === id && handler?.()) } -export function createContextMenu(props: ChromeContextMenuCreateProps): Promise { - if (IS_ANDROID) { +export async function createContextMenu(props: ChromeContextMenuCreateProps): Promise { + const { id, onclick: clickHandler } = props + if (IS_ANDROID || !id) { return } // Add listener by param - let clickHandler: Function = undefined - clickHandler = props.onclick delete props.onclick return new Promise(resolve => chrome.contextMenus?.create?.(props, () => { handleError('createContextMenu') - clickHandler && onClick(props.id, clickHandler) + clickHandler && onClick(id, clickHandler) resolve() })) } -export function updateContextMenu(menuId: string, props: ChromeContextMenuUpdateProps): Promise { +export async function updateContextMenu(menuId: string, props: ChromeContextMenuUpdateProps): Promise { if (IS_ANDROID) { return } diff --git a/src/api/chrome/runtime.ts b/src/api/chrome/runtime.ts index 90b591d7f..f4d8c66e8 100644 --- a/src/api/chrome/runtime.ts +++ b/src/api/chrome/runtime.ts @@ -8,7 +8,7 @@ export function getRuntimeName(): string { return chrome.runtime.getManifest().name } -export function sendMsg2Runtime(code: timer.mq.ReqCode, data?: T): Promise { +export function sendMsg2Runtime(code: timer.mq.ReqCode, data?: T): Promise { // Fix proxy data failed to serialized in Firefox if (data !== undefined) { data = JSON.parse(JSON.stringify(data)) diff --git a/src/api/chrome/tab.ts b/src/api/chrome/tab.ts index 4cafeac16..83cb02cdd 100644 --- a/src/api/chrome/tab.ts +++ b/src/api/chrome/tab.ts @@ -21,8 +21,8 @@ export function resetTabUrl(tabId: number, url: string): Promise { }, () => resolve())) } -export async function getRightOf(target: ChromeTab): Promise { - if (!target) return null +export async function getRightOf(target: ChromeTab): Promise { + if (!target) return const { windowId, index } = target return new Promise(resolve => chrome.tabs.query({ windowId }, tabs => { const rightTab = tabs @@ -33,7 +33,7 @@ export async function getRightOf(target: ChromeTab): Promise { })) } -export function getCurrentTab(): Promise { +export function getCurrentTab(): Promise { return new Promise(resolve => chrome.tabs.getCurrent(tab => { handleError("getCurrentTab") resolve(tab) diff --git a/src/api/chrome/window.ts b/src/api/chrome/window.ts index 05e938253..94b9f5f1c 100644 --- a/src/api/chrome/window.ts +++ b/src/api/chrome/window.ts @@ -19,16 +19,17 @@ export function isNoneWindowId(windowId: number) { return !windowId || windowId === chrome.windows.WINDOW_ID_NONE } -export function getFocusedNormalWindow(): Promise { +export async function getFocusedNormalWindow(): Promise { if (IS_ANDROID) { - return Promise.resolve(null) + return } return new Promise(resolve => chrome.windows.getLastFocused( // Only find normal window { windowTypes: ['normal'] }, window => { + const { focused, id } = window handleError('getFocusedNormalWindow') - if (!window?.focused || isNoneWindowId(window?.id)) { + if (!focused || !id || isNoneWindowId(id)) { resolve(undefined) } else { resolve(window) @@ -37,9 +38,9 @@ export function getFocusedNormalWindow(): Promise { )) } -export function getWindow(id: number): Promise { +export async function getWindow(id: number): Promise { if (IS_ANDROID) { - return Promise.resolve(null) + return } return new Promise(resolve => chrome.windows.get(id, win => resolve(win))) } diff --git a/src/api/gist.ts b/src/api/gist.ts index 6c6e1920d..53ee29746 100644 --- a/src/api/gist.ts +++ b/src/api/gist.ts @@ -26,7 +26,7 @@ export type File = BaseFile & { type BaseGist = { public: boolean description: string - files: { [filename: string]: FileInfo } + files: { [filename: string]: FileInfo | null } } export type Gist = BaseGist & { @@ -88,7 +88,7 @@ export function getGist(token: string, id: string): Promise { * @param predicate predicate * @returns */ -export async function findTarget(token: string, predicate: (gist: Gist) => boolean): Promise { +export async function findTarget(token: string, predicate: (gist: Gist) => boolean): Promise { let pageNum = 1 while (true) { const uri = `?per_page=100&page=${pageNum}` @@ -102,7 +102,7 @@ export async function findTarget(token: string, predicate: (gist: Gist) => boole } pageNum += 1 } - return undefined + return null } /** @@ -130,7 +130,7 @@ export async function updateGist(token: string, id: string, gist: GistForm): Pro /** * Get content of file */ -export async function getJsonFileContent(file: File): Promise { +export async function getJsonFileContent(file: File): Promise { const content = file.content if (content) { try { @@ -142,7 +142,7 @@ export async function getJsonFileContent(file: File): Promise { } const rawUrl = file.raw_url if (!rawUrl) { - return undefined + return null } const response = await fetchGet(rawUrl) return await response.json() @@ -153,7 +153,7 @@ export async function getJsonFileContent(file: File): Promise { * * @returns errorMsg or null/undefined */ -export async function testToken(token: string): Promise { +export async function testToken(token: string): Promise { const response = await fetchGet(BASE_URL + '?per_page=1&page=1', { headers: { "Accept": "application/vnd.github+json", @@ -161,5 +161,5 @@ export async function testToken(token: string): Promise { } }) const { status, statusText } = response || {} - return status === 200 ? null : statusText || ("ERROR " + status) + return status === 200 ? undefined : statusText || ("ERROR " + status) } diff --git a/src/api/http.ts b/src/api/http.ts index 89a9822ca..a675bbc8a 100644 --- a/src/api/http.ts +++ b/src/api/http.ts @@ -14,7 +14,7 @@ export async function fetchGet(url: string, option?: Option): Promise return response } catch (e) { console.error("Failed to fetch get", e) - throw Error(e) + throw Error(e?.toString?.() ?? 'Unknown error') } } @@ -28,7 +28,7 @@ export async function fetchPost(url: string, body?: T, option?: Option): Prom return response } catch (e) { console.error("Failed to fetch post", e) - throw Error(e) + throw Error(e?.toString?.() ?? 'Unknown error') } } @@ -42,7 +42,7 @@ export async function fetchPutText(url: string, bodyText?: string, option?: Opti return response } catch (e) { console.error("Failed to fetch putText", e) - throw Error(e) + throw Error(e?.toString?.() ?? 'Unknown error') } } @@ -55,6 +55,6 @@ export async function fetchDelete(url: string, option?: Option): Promise ({ +const authHeaders = (auth: string): Record => ({ "Authorization": `Bearer ${auth}` }) @@ -42,7 +42,7 @@ export async function updateFile(context: ObsidianRequestContext, filePath: stri await fetchPutText(url, content, { headers }) } -export async function getFileContent(context: ObsidianRequestContext, filePath: string): Promise { +export async function getFileContent(context: ObsidianRequestContext, filePath: string): Promise { const { endpoint, auth, vault } = context || {} const url = `${endpoint || DEFAULT_ENDPOINT}/${vault || DEFAULT_VAULT}/${filePath}` const headers = authHeaders(auth) diff --git a/src/api/version.ts b/src/api/version.ts index 9995d5857..29c2b127d 100644 --- a/src/api/version.ts +++ b/src/api/version.ts @@ -30,28 +30,28 @@ type EdgeDetail = { lastUpdateDate: string } -async function getFirefoxVersion(): Promise { +async function getFirefoxVersion(): Promise { const response = await fetchGet('https://addons.mozilla.org/api/v3/addons/addon/2690100') const detail: FirefoxDetail = await response.json() return detail?.current_version?.version } -async function getEdgeVersion(): Promise { +async function getEdgeVersion(): Promise { const response = await fetchGet('https://microsoftedge.microsoft.com/addons/getproductdetailsbycrxid/fepjgblalcnepokjblgbgmapmlkgfahc') const detail: EdgeDetail = await response.json() return detail?.version } -async function getChromeVersion(): Promise { +async function getChromeVersion(): Promise { // Get info from shields.io const response = await fetchGet('https://img.shields.io/chrome-web-store/v/dkdhhcbjijekmneelocdllcldcpmekmm?label=Google%20Chrome') const data = await response.text() const pattern = /:\sv(\d+\.\d+\.\d+)/ const matchResult = pattern.exec(data) - return matchResult.length === 2 ? matchResult[1] : null + return matchResult?.length === 2 ? matchResult?.[1] : undefined } -export function getLatestVersion(): Promise { +export function getLatestVersion(): Promise { if (IS_FIREFOX) { return getFirefoxVersion() } else if (IS_CHROME) { @@ -59,5 +59,5 @@ export function getLatestVersion(): Promise { } else if (IS_EDGE) { return getEdgeVersion() } - return Promise.resolve(null) + return Promise.resolve(undefined) } diff --git a/src/api/web-dav.ts b/src/api/web-dav.ts index 0062d6036..39a4eb240 100644 --- a/src/api/web-dav.ts +++ b/src/api/web-dav.ts @@ -20,12 +20,10 @@ export type WebDAVContext = { const authHeaders = (auth: WebDAVAuth): Headers => { const type = auth?.type - let headerVal = null + const headers = new Headers() if (type === 'password') { - headerVal = `Basic ${encode(`${auth?.username}:${auth?.password}`)}` + headers.set('Authorization', `Basic ${encode(`${auth?.username}:${auth?.password}`)}`) } - const headers = new Headers() - headers.set('Authorization', headerVal) return headers } @@ -92,7 +90,7 @@ function handleWriteResponse(response: Response) { } } -export async function readFile(context: WebDAVContext, filePath: string): Promise { +export async function readFile(context: WebDAVContext, filePath: string): Promise { const { auth, endpoint } = context || {} const headers = authHeaders(auth) const url = `${endpoint}/${filePath}` diff --git a/src/background/active-tab-listener.ts b/src/background/active-tab-listener.ts index 95bbb7cbc..6d5fd3ce0 100644 --- a/src/background/active-tab-listener.ts +++ b/src/background/active-tab-listener.ts @@ -20,6 +20,7 @@ export default class ActiveTabListener { listener: _Handler[] = [] private async processWithTabInfo({ url, id }: ChromeTab) { + if (!url || !id) return const hostInfo: HostInfo = extractHostname(url) const host: string = hostInfo.host const param: _Param = { url, tabId: id, host } diff --git a/src/background/badge-manager.ts b/src/background/badge-manager.ts index ebaa9a6b7..c623b63d1 100644 --- a/src/background/badge-manager.ts +++ b/src/background/badge-manager.ts @@ -47,7 +47,7 @@ function setBadgeTextOfMills(milliseconds: number | undefined, tabId: number | u setBadgeText(text, tabId) } -async function findActiveTab(): Promise { +async function findActiveTab(): Promise { const window = await getFocusedNormalWindow() if (!window) { return undefined @@ -55,11 +55,11 @@ async function findActiveTab(): Promise { const tabs = await listTabs({ active: true, windowId: window.id }) // Fix #131 // Edge will return two active tabs, including the new tab with url 'edge://newtab/', GG - const tab = tabs.filter(tab => !isBrowserUrl(tab?.url))[0] - if (!tab) { - return undefined + for (const { id: tabId, url } of tabs) { + if (!tabId || !url || isBrowserUrl(url)) continue + return { tabId, url } } - return { tabId: tab.id, url: tab.url } + return undefined } async function clearAllBadge(): Promise { @@ -78,10 +78,10 @@ interface BadgeManager { } class DefaultBadgeManager { - pausedTabId: number - current: BadgeLocation - visible: boolean - state: BadgeState + pausedTabId: number | undefined + current: BadgeLocation | undefined + visible: boolean | undefined + state: BadgeState | undefined async init(messageDispatcher: MessageDispatcher) { const option = await optionHolder.get() @@ -107,7 +107,7 @@ class DefaultBadgeManager { /** * Show the badge text */ - private resume(tabId: number) { + private resume(tabId?: number) { if (!this.pausedTabId || this.pausedTabId !== tabId) return this.pausedTabId = undefined this.render() diff --git a/src/background/content-script-handler.ts b/src/background/content-script-handler.ts index 4598da9e0..e0f4098e4 100644 --- a/src/background/content-script-handler.ts +++ b/src/background/content-script-handler.ts @@ -25,7 +25,7 @@ const handleOpenAnalysisPage = (sender: ChromeMessageSender) => { const newTabUrl = getAppPageUrl(ANALYSIS_ROUTE, { host }) const tabIndex = tab?.index - const newTabIndex = tabIndex ? tabIndex + 1 : null + const newTabIndex = tabIndex ? tabIndex + 1 : undefined createTab({ url: newTabUrl, index: newTabIndex }) } @@ -34,7 +34,7 @@ const handleOpenLimitPage = (sender: ChromeMessageSender) => { if (!url) return const newTabUrl = getAppPageUrl(LIMIT_ROUTE, { url }) const tabIndex = tab?.index - const newTabIndex = tabIndex ? tabIndex + 1 : null + const newTabIndex = tabIndex ? tabIndex + 1 : undefined createTab({ url: newTabUrl, index: newTabIndex }) } @@ -54,7 +54,7 @@ const handleInjected = async (sender: ChromeMessageSender) => { export default function init(dispatcher: MessageDispatcher) { dispatcher // Judge is in whitelist - .register<{ host?: string, url?: string }, boolean>('cs.isInWhitelist', ({ host, url } = {}) => whitelistHolder.contains(host, url)) + .register<{ host?: string, url?: string }, boolean>('cs.isInWhitelist', ({ host, url } = {}) => !!host && !!url && whitelistHolder.contains(host, url)) // Need to print the information of today .register('cs.printTodayInfo', async () => { const option = await optionHolder.get() @@ -66,7 +66,7 @@ export default function init(dispatcher: MessageDispatcher) { .register('cs.openLimit', (_, sender) => handleOpenLimitPage(sender)) .register('cs.onInjected', async (_, sender) => handleInjected(sender)) // Get sites which need to count run time - .register('cs.getRunSites', async url => { + .register('cs.getRunSites', async url => { const { host } = extractHostname(url) || {} if (!host) return null const site: timer.site.SiteKey = { host, type: 'normal' } diff --git a/src/background/icon-and-alias-collector.ts b/src/background/icon-and-alias-collector.ts index 113a0a3d3..8c7a71084 100644 --- a/src/background/icon-and-alias-collector.ts +++ b/src/background/icon-and-alias-collector.ts @@ -29,21 +29,19 @@ async function collectAlias(key: timer.site.SiteKey, tabTitle: string) { if (!tabTitle) return if (isUrl(tabTitle)) return const siteName = extractSiteName(tabTitle, key.host) - siteName && await siteService.saveAlias(key, siteName) + siteName && await siteService.saveAlias(key, siteName, true) } /** * Process the tab */ async function processTabInfo(tab: ChromeTab): Promise { - if (!tab) return - const url = tab.url - if (!url) return + let { favIconUrl, url, title } = tab + if (!url || !title) return if (isBrowserUrl(url)) return const hostInfo = extractHostname(url) const host = hostInfo.host if (!host) return - let favIconUrl = tab.favIconUrl // localhost hosts with Chrome use cache, so keep the favIcon url undefined IS_CHROME && /^localhost(:.+)?/.test(host) && (favIconUrl = undefined) const siteKey: timer.site.SiteKey = { host, type: 'normal' } @@ -51,7 +49,7 @@ async function processTabInfo(tab: ChromeTab): Promise { collectAliasEnabled && !isBrowserUrl(url) && isHomepage(url) - && await collectAlias(siteKey, tab.title) + && await collectAlias(siteKey, title) } /** diff --git a/src/background/index.ts b/src/background/index.ts index 48420fcea..8d706e381 100644 --- a/src/background/index.ts +++ b/src/background/index.ts @@ -68,6 +68,9 @@ messageDispatcher.start() onNormalWindowFocusChanged(async windowId => { if (isNoneWindowId(windowId)) return const tabs = await listTabs({ windowId, active: true }) - tabs.filter(tab => !isBrowserUrl(tab?.url)) - .forEach(({ url, id }) => badgeTextManager.updateFocus({ url, tabId: id })) + tabs.forEach(tab => { + const { url, id: tabId } = tab + if (!url || isBrowserUrl(url) || !tabId) return + badgeTextManager.updateFocus({ url, tabId }) + }) }) diff --git a/src/background/install-handler/index.ts b/src/background/install-handler/index.ts index 2101d0d6a..de3c44b55 100644 --- a/src/background/install-handler/index.ts +++ b/src/background/install-handler/index.ts @@ -19,7 +19,7 @@ async function reloadContentScript() { } const tabs = await listTabs() tabs.filter(({ url }) => url && !isBrowserUrl(url)) - .forEach(tab => executeScript(tab.id, files)) + .forEach(({ id: tabId }) => tabId && executeScript(tabId, files)) } export default function handleInstall() { diff --git a/src/background/limit-processor.ts b/src/background/limit-processor.ts index 929d2ab1e..3723b35a8 100644 --- a/src/background/limit-processor.ts +++ b/src/background/limit-processor.ts @@ -15,13 +15,14 @@ import { getStartOfDay, MILL_PER_DAY, MILL_PER_SECOND } from "@util/time" import alarmManager from "./alarm-manager" import MessageDispatcher from "./message-dispatcher" -function processLimitWaking(rules: timer.limit.Item[], tab: ChromeTab) { - const { url } = tab +function processLimitWaking(rules: timer.limit.Item[], tab: ChromeTab): void { + const { url, id: tabId } = tab + if (!url || !tabId) return const anyMatch = rules.map(rule => matches(rule?.cond, url)).reduce((a, b) => a || b, false) if (!anyMatch) { return } - sendMsg2Tab(tab.id, 'limitWaking', rules) + sendMsg2Tab(tabId, 'limitWaking', rules) .then(() => console.log(`Waked tab[id=${tab.id}]`)) .catch(err => console.error(`Failed to wake with limit rule: rules=${JSON.stringify(rules)}, msg=${err.msg}`)) } @@ -32,10 +33,10 @@ async function processOpenPage(limitedUrl: string, sender: ChromeMessageSender) const realUrl = getAppPageUrl(LIMIT_ROUTE, { url: encodeURI(limitedUrl) }) const baseUrl = getAppPageUrl(LIMIT_ROUTE) const rightTab = await getRightOf(originTab) - const rightUrl = rightTab?.url - if (rightUrl && isBrowserUrl(rightUrl) && rightUrl.includes(baseUrl)) { + const { id: rightId, url: rightUrl } = rightTab || {} + if (rightId && rightUrl && isBrowserUrl(rightUrl) && rightUrl.includes(baseUrl)) { // Reset url - await resetTabUrl(rightTab.id, realUrl) + await resetTabUrl(rightId, realUrl) } else { await createTabAfterCurrent(realUrl, sender?.tab) } @@ -62,10 +63,11 @@ const processMoreMinutes = async (url: string) => { const processAskHitVisit = async (item: timer.limit.Item) => { let tabs = await listTabs() - tabs = tabs?.filter(({ url }) => matches(item?.cond, url)) - const { visitTime = 0 } = item || {} - for (const { id } of tabs) { + const { visitTime = 0, cond } = item || {} + for (const { id, url } of tabs) { try { + if (!url || !matches(cond, url) || !id) continue + const tabFocus = await sendMsg2Tab(id, "askVisitTime") if (tabFocus && tabFocus > visitTime * MILL_PER_SECOND) return true } catch { diff --git a/src/background/message-dispatcher.ts b/src/background/message-dispatcher.ts index 508a2bac8..7ee147690 100644 --- a/src/background/message-dispatcher.ts +++ b/src/background/message-dispatcher.ts @@ -33,7 +33,8 @@ class MessageDispatcher { const result = await handler(message.data, sender) return { code: 'success', data: result } } catch (error) { - return { code: 'fail', msg: error } + const msg = (error as Error)?.message ?? error?.toString?.() + return { code: 'fail', msg } } } diff --git a/src/background/migrator/cate-initializer.ts b/src/background/migrator/cate-initializer.ts index ecc4d34ba..d42cb61f4 100644 --- a/src/background/migrator/cate-initializer.ts +++ b/src/background/migrator/cate-initializer.ts @@ -32,7 +32,7 @@ const DEMO_ITEMS: InitialCate[] = [ async function initItem(item: InitialCate) { const { name, hosts } = item const cate = await cateService.add(name) - const cateId = cate?.id + const cateId = cate.id const siteKeys = hosts.map(host => ({ host, type: 'normal' } satisfies timer.site.SiteKey)) await siteService.batchSaveCate(cateId, siteKeys) } diff --git a/src/background/track-server.ts b/src/background/track-server.ts index 00574fc35..a4d6af1ce 100644 --- a/src/background/track-server.ts +++ b/src/background/track-server.ts @@ -10,7 +10,7 @@ import { formatTimeYMD, getStartOfDay, MILL_PER_DAY } from "@util/time" import badgeManager from "./badge-manager" import MessageDispatcher from "./message-dispatcher" -async function handleTime(host: string, url: string, dateRange: [number, number], tabId: number): Promise { +async function handleTime(host: string, url: string, dateRange: [number, number], tabId: number | undefined): Promise { const [start, end] = dateRange const focusTime = end - start // 1. Save async @@ -20,7 +20,7 @@ async function handleTime(host: string, url: string, dateRange: [number, number] // If time limited after this operation, send messages limited?.length && sendLimitedMessage(limited) // If need to reminder, send messages - reminder?.items?.length && sendMsg2Tab(tabId, 'limitReminder', reminder) + reminder?.items?.length && tabId && sendMsg2Tab(tabId, 'limitReminder', reminder) // 3. Add period time await periodService.add(start, focusTime) return focusTime @@ -47,14 +47,14 @@ async function handleTrackTimeEvent(event: timer.core.Event, sender: ChromeMessa } } -async function windowNotFocused(winId: number): Promise { +async function windowNotFocused(winId: number | undefined): Promise { if (IS_ANDROID) return false if (!winId) return true const window = await getWindow(winId) return !window?.focused } -async function tabNotActive(tabId: number): Promise { +async function tabNotActive(tabId: number | undefined): Promise { if (!tabId) return true const tab = await getTab(tabId) return !tab?.active @@ -65,7 +65,8 @@ async function sendLimitedMessage(items: timer.limit.Item[]) { if (!tabs?.length) return for (const tab of tabs) { try { - await sendMsg2Tab(tab.id, 'limitTimeMeet', items) + const { id } = tab + id && await sendMsg2Tab(id, 'limitTimeMeet', items) } catch { /* Ignored */ } diff --git a/src/background/whitelist-menu-manager.ts b/src/background/whitelist-menu-manager.ts index 68caadcea..166841f39 100644 --- a/src/background/whitelist-menu-manager.ts +++ b/src/background/whitelist-menu-manager.ts @@ -32,11 +32,11 @@ const menuInitialOptions: ChromeContextMenuCreateProps = { visible: false } -async function updateContextMenuInner(param: ChromeTab | number) { +async function updateContextMenuInner(param: ChromeTab | number | undefined): Promise { if (typeof param === 'number') { // If number, get the tabInfo first const tab: ChromeTab = await getTab(currentActiveId) - tab && updateContextMenuInner(tab) + tab && await updateContextMenuInner(tab) } else { const { url } = param || {} const targetHost = url && !isBrowserUrl(url) ? extractHostname(url).host : '' @@ -53,7 +53,7 @@ async function updateContextMenuInner(param: ChromeTab | number) { changeProp.title = t2Chrome(root => root.contextMenus[titleMsgField]).replace('{host}', targetHost) changeProp.onclick = () => removeOrAdd(existsInWhitelist, targetHost) } - updateContextMenu(menuId, changeProp) + await updateContextMenu(menuId, changeProp) } } @@ -62,7 +62,7 @@ const handleListChange = (newWhitelist: string[]) => { updateContextMenuInner(currentActiveId) } -const handleTabUpdated = (tabId: number, changeInfo: ChromeTabChangeInfo, tab: number | ChromeTab) => { +const handleTabUpdated = (tabId: number, changeInfo: ChromeTabChangeInfo, tab?: ChromeTab) => { // Current active tab updated tabId === currentActiveId && changeInfo.status === 'loading' diff --git a/src/common/logger.ts b/src/common/logger.ts index 40a7f3cbe..7f4189b14 100644 --- a/src/common/logger.ts +++ b/src/common/logger.ts @@ -1,6 +1,6 @@ /** * Copyright (c) 2021 Hengyang Zhang - * + * * This software is released under the MIT License. * https://opensource.org/licenses/MIT */ diff --git a/src/content-script/index.ts b/src/content-script/index.ts index 282cde385..c90690f02 100644 --- a/src/content-script/index.ts +++ b/src/content-script/index.ts @@ -41,7 +41,7 @@ function getOrSetFlag(): boolean { * Wrap for hooks, after the extension reloaded or upgraded, the context of current content script will be invalid * And sending messages to the runtime will be failed */ -async function trySendMsg2Runtime(code: timer.mq.ReqCode, data?: Req): Promise { +async function trySendMsg2Runtime(code: timer.mq.ReqCode, data?: Req): Promise { try { return await sendMsg2Runtime(code, data) } catch { diff --git a/src/content-script/limit/modal/components/Footer.tsx b/src/content-script/limit/modal/components/Footer.tsx index 6f8918d89..b10649c6c 100644 --- a/src/content-script/limit/modal/components/Footer.tsx +++ b/src/content-script/limit/modal/components/Footer.tsx @@ -11,11 +11,11 @@ import { computed, defineComponent } from "vue" import { useDelayHandler, useReason, useRule } from "../context" async function handleMore5Minutes(rule: timer.limit.Item, callback: () => void) { - let promise: Promise = undefined - const ele = document.querySelector(TAG_NAME).shadowRoot.querySelector('body') + let promise: Promise | undefined = undefined + const ele = document.querySelector(TAG_NAME)?.shadowRoot?.querySelector('body') if (await judgeVerificationRequired(rule)) { const option = await optionHolder.get() - promise = processVerification(option, { appendTo: ele }) + promise = processVerification(option, { appendTo: ele ?? undefined }) promise ? promise.then(callback).catch(() => { }) : callback() } else { callback() @@ -26,20 +26,20 @@ const _default = defineComponent(() => { const reason = useReason() const rule = useRule() const showDelay = computed(() => { - const { type, allowDelay, delayCount } = reason.value || {} + const { type, allowDelay, delayCount = 0 } = reason.value || {} if (!allowDelay) return false const { time, weekly, visit, waste, weeklyWaste } = rule.value || {} let realLimit = 0, realWaste = 0 if (type === 'DAILY') { - realLimit = time + realLimit = time ?? 0 realWaste = waste } else if (type === 'WEEKLY') { - realLimit = weekly + realLimit = weekly ?? 0 realWaste = weeklyWaste } else if (type === 'VISIT') { realLimit = visit - realWaste = reason.value?.getVisitTime?.() + realWaste = reason.value?.getVisitTime?.() ?? 0 } else { return false } diff --git a/src/content-script/limit/modal/components/Reason.tsx b/src/content-script/limit/modal/components/Reason.tsx index bf6295686..391fa8c67 100644 --- a/src/content-script/limit/modal/components/Reason.tsx +++ b/src/content-script/limit/modal/components/Reason.tsx @@ -22,6 +22,9 @@ const TimeDescriptions = defineComponent({ const rule = useRule() const reason = useReason() + 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 () => ( msg.limit.item.name)} labelAlign="right"> @@ -37,13 +40,13 @@ const TimeDescriptions = defineComponent({ {formatPeriodCommon(props.waste ?? 0)} {`${props.visit ?? 0} ${t(msg => msg.limit.item.visits)}`} @@ -99,7 +102,7 @@ const _default = defineComponent(() => { {rule.value?.name || '-'} msg.limit.item.visitTime)} labelAlign="right"> - {formatPeriodCommon(rule.value?.visitTime * MILL_PER_SECOND) || '-'} + {formatPeriodCommon((rule.value?.visitTime ?? 0) * MILL_PER_SECOND) || '-'} msg.modal.browsingTime)} labelAlign="right"> {browsingTime.value ? formatPeriodCommon(browsingTime.value) : '-'} diff --git a/src/content-script/limit/modal/context.ts b/src/content-script/limit/modal/context.ts index ffc985e9d..3165ec73e 100644 --- a/src/content-script/limit/modal/context.ts +++ b/src/content-script/limit/modal/context.ts @@ -8,13 +8,13 @@ const REASON_KEY = "display_reason" const RULE_KEY = "display_rule" const DELAY_HANDLER_KEY = 'delay_handler' -export const provideReason = (app: App) => { +export const provideReason = (app: App): Ref => { const reason = ref() app?.provide(REASON_KEY, reason) return reason } -export const useReason = (): Ref => inject(REASON_KEY) +export const useReason = () => inject(REASON_KEY) as Ref export const provideRule = () => { const reason = useReason() @@ -33,10 +33,10 @@ export const provideRule = () => { provide(RULE_KEY, rule) } -export const useRule = (): Ref => inject(RULE_KEY) +export const useRule = () => inject(RULE_KEY) as Ref export const provideDelayHandler = (app: App, handlers: () => void) => { app?.provide(DELAY_HANDLER_KEY, handlers) } -export const useDelayHandler = (): () => void => inject(DELAY_HANDLER_KEY) \ No newline at end of file +export const useDelayHandler = () => inject(DELAY_HANDLER_KEY) as () => void \ No newline at end of file diff --git a/src/content-script/limit/modal/index.ts b/src/content-script/limit/modal/index.ts index 81106584d..c49db0008 100644 --- a/src/content-script/limit/modal/index.ts +++ b/src/content-script/limit/modal/index.ts @@ -67,14 +67,14 @@ class ScreenLocker { class ModalInstance implements MaskModal { url: string - rootElement: RootElement - body: HTMLBodyElement + rootElement: RootElement | undefined + body: HTMLBodyElement | undefined delayHandlers: (() => void)[] = [ () => sendMsg2Runtime('cs.moreMinutes', this.url), ] reasons: LimitReason[] = [] - reason: Ref - app: App + reason: Ref | undefined + app: App | undefined screenLocker = new ScreenLocker() constructor(url: string) { @@ -123,7 +123,7 @@ class ModalInstance implements MaskModal { // 1. Create mask element const root = await this.prepareRoot() const html = document.createElement('html') - root.append(html) + root?.append(html) // header const header = createHeader() @@ -145,11 +145,11 @@ class ModalInstance implements MaskModal { this.app = createApp(Main) this.reason = provideReason(this.app) provideDelayHandler(this.app, () => this.delayHandlers?.forEach(h => h?.())) - this.app.mount(this.body) + this.body && this.app.mount(this.body) } - private async prepareRoot(): Promise { - const inner = () => { + private async prepareRoot(): Promise { + const inner = (): ShadowRoot | null => { const exist = this.rootElement || document.querySelector(TAG_NAME) as RootElement if (exist) { this.rootElement = exist @@ -179,16 +179,16 @@ class ModalInstance implements MaskModal { pauseAllAudio() this.rootElement && (this.rootElement.style.visibility = 'visible') - this.reason.value = reason + this.reason && (this.reason.value = reason) this.screenLocker.lock() - this.body.style.display = 'block' + this.body && (this.body.style.display = 'block') } private hide() { this.rootElement && (this.rootElement.style.visibility = 'hidden') this.screenLocker.unlock() this.body && (this.body.style.display = 'none') - this.reason && (this.reason.value = null) + this.reason && (this.reason.value = undefined) } } diff --git a/src/content-script/limit/processor/message-adaptor.ts b/src/content-script/limit/processor/message-adaptor.ts index 34b067321..0cea7bf7c 100644 --- a/src/content-script/limit/processor/message-adaptor.ts +++ b/src/content-script/limit/processor/message-adaptor.ts @@ -55,7 +55,7 @@ class MessageAdaptor implements Processor { async initRules(): Promise { this.context.modal?.removeReasonsByType?.('DAILY', 'WEEKLY') - const limitedRules: timer.limit.Item[] = await sendMsg2Runtime('cs.getLimitedRules', this.context.url) + const limitedRules = await sendMsg2Runtime('cs.getLimitedRules', this.context.url) limitedRules ?.flatMap?.(cvtItem2AddReason) diff --git a/src/content-script/limit/processor/period-processor.ts b/src/content-script/limit/processor/period-processor.ts index 946a180c8..e22c7f29c 100644 --- a/src/content-script/limit/processor/period-processor.ts +++ b/src/content-script/limit/processor/period-processor.ts @@ -19,7 +19,7 @@ function processRule(rule: timer.limit.Rule, nowSeconds: number, context: ModalC timers.push(setTimeout(() => context.modal.removeReason(reason), (endSeconds - nowSeconds) * MILL_PER_SECOND)) } return timers - }) + }) ?? [] } class PeriodProcessor implements Processor { diff --git a/src/content-script/limit/processor/visit-processor.ts b/src/content-script/limit/processor/visit-processor.ts index 2f17b7d76..9514eb442 100644 --- a/src/content-script/limit/processor/visit-processor.ts +++ b/src/content-script/limit/processor/visit-processor.ts @@ -8,8 +8,8 @@ class VisitProcessor implements Processor { private context: ModalContext private focusTime: number = 0 - private rules: timer.limit.Rule[] - private tracker: NormalTracker + private rules: timer.limit.Rule[] = [] + private tracker: NormalTracker | undefined private delayCount: number = 0 constructor(context: ModalContext) { @@ -50,7 +50,7 @@ class VisitProcessor implements Processor { } async initRules() { - this.rules = await sendMsg2Runtime("cs.getRelatedRules", this.context.url) + this.rules = await sendMsg2Runtime("cs.getRelatedRules", this.context.url) ?? [] this.context.modal.removeReasonsByType("VISIT") } diff --git a/src/content-script/limit/reminder/component.ts b/src/content-script/limit/reminder/component.ts index df7480e63..06378f176 100644 --- a/src/content-script/limit/reminder/component.ts +++ b/src/content-script/limit/reminder/component.ts @@ -56,7 +56,7 @@ const closeBtnStyle = (dark: boolean): Partial => ({ function mountStyle(el: HTMLElement, style: Partial) { if (!el || !style) return - Object.entries(style).forEach(([key, val]) => el.style[key] = val) + Object.entries(style).forEach(([key, val]) => typeof val === 'string' && el.style.setProperty(key, val)) } function createIcon(): HTMLImageElement { diff --git a/src/content-script/limit/reminder/index.ts b/src/content-script/limit/reminder/index.ts index 10a139b91..44f1663ca 100644 --- a/src/content-script/limit/reminder/index.ts +++ b/src/content-script/limit/reminder/index.ts @@ -5,8 +5,8 @@ import { createComponent } from "./component" class Reminder implements Processor { private id = 0 - private el: HTMLElement - private darkMode: boolean + private el: HTMLElement | undefined + private darkMode: boolean = false handleMsg(code: timer.mq.ReqCode, data: unknown): timer.mq.Response | Promise { if (code !== 'limitReminder') { @@ -34,7 +34,7 @@ class Reminder implements Processor { private close() { if (!this.el) return this.el.remove() - this.el = null + this.el = undefined } async init(): Promise { diff --git a/src/content-script/printer.ts b/src/content-script/printer.ts index 2f06b2dcd..11f619d9c 100644 --- a/src/content-script/printer.ts +++ b/src/content-script/printer.ts @@ -13,11 +13,12 @@ import { t } from "./locale" * Print info of today */ export default async function printInfo(host: string) { - const waste: timer.core.Result = await sendMsg2Runtime('cs.getTodayInfo', host) + const waste = await sendMsg2Runtime('cs.getTodayInfo', host) + const { time, focus } = waste || {} const param = { - time: waste.time ? '' + waste.time : '-', - focus: formatPeriodCommon(waste.focus), + time: `${time ?? '-'}`, + focus: formatPeriodCommon(focus ?? 0), host, } const info0 = t(msg => msg.console.consoleLog, param) diff --git a/src/content-script/tracker/normal/idle-detector.ts b/src/content-script/tracker/normal/idle-detector.ts index 933880b83..26dec2210 100644 --- a/src/content-script/tracker/normal/idle-detector.ts +++ b/src/content-script/tracker/normal/idle-detector.ts @@ -9,7 +9,7 @@ export default class IdleDetector { lastActiveTime: number = Date.now() userActive: boolean = true - pauseTimeout: NodeJS.Timeout + pauseTimeout: NodeJS.Timeout | undefined onIdle: () => void onActive: () => void diff --git a/src/content-script/tracker/normal/index.ts b/src/content-script/tracker/normal/index.ts index 9e6ba52eb..9adb167ab 100644 --- a/src/content-script/tracker/normal/index.ts +++ b/src/content-script/tracker/normal/index.ts @@ -14,10 +14,6 @@ class TrackContext { this.onPause = onPause this.onResume = onResume - this.init() - } - - private init() { this.detectDocVisible() document?.addEventListener('visibilitychange', () => this.detectDocVisible()) @@ -52,7 +48,7 @@ export type NormalTrackerOption = { * Normal tracker */ export default class NormalTracker { - context: TrackContext + context: TrackContext | undefined start: number = Date.now() option: NormalTrackerOption @@ -89,7 +85,7 @@ export default class NormalTracker { start: lastTime, end: now, url: location?.href, - ignoreTabCheck + ignoreTabCheck: !!ignoreTabCheck } try { await this.option?.onReport?.(data) diff --git a/src/content-script/tracker/run-time.ts b/src/content-script/tracker/run-time.ts index 042b427f5..9b1a0a487 100644 --- a/src/content-script/tracker/run-time.ts +++ b/src/content-script/tracker/run-time.ts @@ -1,17 +1,17 @@ import { onRuntimeMessage, sendMsg2Runtime } from "@api/chrome/runtime" class RunTimeTracker { - private start: number + private start: number = Date.now() private url: string // Real host, including builtin hosts - private host: string + private host: string | undefined constructor(url: string) { this.url = url + this.start = Date.now() } init(): void { - this.start = Date.now() this.fetchSite() onRuntimeMessage(async req => { @@ -29,7 +29,7 @@ class RunTimeTracker { sendMsg2Runtime('cs.getRunSites', this.url) .then((site: timer.site.SiteKey) => this.host = site?.host) // Extension reloaded, so terminate - .catch(() => this.host = null) + .catch(() => this.host = undefined) } private collect() { diff --git a/src/database/backup-database.ts b/src/database/backup-database.ts index 93284dbad..9426d0ba3 100644 --- a/src/database/backup-database.ts +++ b/src/database/backup-database.ts @@ -38,7 +38,7 @@ class BackupDatabase extends BaseDatabase { } async updateCache(type: timer.backup.Type, newVal: unknown): Promise { - return this.storage.put(cacheKeyOf(type), newVal) + return this.storage.put(cacheKeyOf(type), newVal as Object) } async importData(_data: any): Promise { diff --git a/src/database/common/storage-promise.ts b/src/database/common/storage-promise.ts index e9f1aad91..0750bacdd 100644 --- a/src/database/common/storage-promise.ts +++ b/src/database/common/storage-promise.ts @@ -22,14 +22,14 @@ export default class StoragePromise { get( keys?: NoInferX | Array> | Partial> | null, - ): Promise<{ [key: string]: any }> { - return new Promise(resolve => this.storage.get(keys, resolve)) + ): Promise { + return new Promise(resolve => this.storage.get(keys ?? null, resolve)) } /** * @since 0.5.0 */ - async getOne(key: string): Promise { + async getOne(key: string): Promise { return (await this.get(key))[key] as T } diff --git a/src/database/limit-database.ts b/src/database/limit-database.ts index 1d0fa5c5d..4bdfbae59 100644 --- a/src/database/limit-database.ts +++ b/src/database/limit-database.ts @@ -39,7 +39,7 @@ type ItemValue = { /** * Limited time, second */ - t: number + t?: number /** * Limited count */ @@ -158,7 +158,7 @@ class LimitDatabase extends BaseDatabase { return Object.values(items).map(cvtItem2Rec) } - async save(data: timer.limit.Rule, rewrite?: boolean): Promise { + async save(data: MakeOptional, rewrite?: boolean): Promise { const items = await this.getItems() let { id, name, weekdays, @@ -181,7 +181,7 @@ class LimitDatabase extends BaseDatabase { // Can be overridden by existing ...(existItem || {}), i: id, n: name, c: cond, wd: weekdays, - e: enabled, ad: allowDelay, + e: !!enabled, ad: !!allowDelay, t: time, ct: count, wt: weekly, wct: weeklyCount, v: visitTime, p: periods, diff --git a/src/database/meta-database.ts b/src/database/meta-database.ts index e9fe1913c..b15606c04 100644 --- a/src/database/meta-database.ts +++ b/src/database/meta-database.ts @@ -13,7 +13,7 @@ import { META_KEY } from "./common/constant" */ class MetaDatabase extends BaseDatabase { async getMeta(): Promise { - const meta: timer.ExtensionMeta = await this.storage.getOne(META_KEY) + const meta = await this.storage.getOne(META_KEY) return meta || {} } @@ -22,21 +22,14 @@ class MetaDatabase extends BaseDatabase { if (!meta) return const existMeta = await this.getMeta() - if (!existMeta.popupCounter) { - existMeta.popupCounter = {} - } - existMeta.popupCounter._total = (existMeta.popupCounter._total || 0) + (meta.popupCounter._total || 0) - if (!existMeta.appCounter) { - existMeta.appCounter = {} - } - const existAppCounter = existMeta.appCounter + const { popupCounter = {}, appCounter = {} } = existMeta + popupCounter._total = (popupCounter._total ?? 0) + (popupCounter._total ?? 0) if (meta.appCounter) { Object.entries(meta.appCounter).forEach(([routePath, count]) => { - existAppCounter[routePath] = (existAppCounter[routePath] || 0) + count + appCounter[routePath] = (appCounter[routePath] ?? 0) + count }) } - existMeta.appCounter = existAppCounter - await this.update(existMeta) + await this.update({ ...existMeta, popupCounter, appCounter }) } async update(existMeta: timer.ExtensionMeta): Promise { diff --git a/src/database/option-database.ts b/src/database/option-database.ts index 790473e52..4995f3973 100644 --- a/src/database/option-database.ts +++ b/src/database/option-database.ts @@ -21,12 +21,12 @@ class OptionDatabase extends BaseDatabase { const newVal = data[DB_KEY] const exist = await this.getOption() if (exist) { - Object.entries(exist).forEach(([key, value]) => exist[key] = value) + Object.entries(exist).forEach(([key, value]) => (exist as any)[key] = value) } await this.setOption(newVal) } - async getOption(): Promise> { + async getOption(): Promise { const option = await this.storage.getOne(DB_KEY) return option || defaultOption() } @@ -41,7 +41,7 @@ class OptionDatabase extends BaseDatabase { addOptionChangeListener(listener: (newVal: timer.option.AllOption) => void) { const storageListener = ( changes: { [key: string]: chrome.storage.StorageChange }, - _areaName: "sync" | "local" | "managed" + _areaName: chrome.storage.AreaName, ) => { const optionInfo = changes[DB_KEY] optionInfo && listener(optionInfo.newValue || {} as timer.option.AllOption) diff --git a/src/database/period-database.ts b/src/database/period-database.ts index 3ae3d836f..aea5e3e21 100644 --- a/src/database/period-database.ts +++ b/src/database/period-database.ts @@ -102,7 +102,7 @@ class PeriodDatabase extends BaseDatabase { if (typeof data !== "object") return const items = await this.storage.get() const keyReg = new RegExp(`^${KEY_PREFIX}20\\d{2}[01]\\d[0-3]\\d$`) - const toSave = {} + const toSave: Record = {} Object.entries(data) .filter(([key]) => keyReg.test(key)) .forEach(([key, value]) => toSave[key] = migrate(items[key], value as _Value)) diff --git a/src/database/site-cate-database.ts b/src/database/site-cate-database.ts index 52401c8b9..a17ecb2ee 100644 --- a/src/database/site-cate-database.ts +++ b/src/database/site-cate-database.ts @@ -49,7 +49,7 @@ class SiteCateDatabase extends BaseDatabase { async listAll(): Promise { const items = await this.getItems() - return Object.entries(items).map(([id, { n } = {}]) => { + return Object.entries(items).map(([id, { n = '' } = {}]) => { return { id: parseInt(id), name: n, @@ -58,8 +58,6 @@ class SiteCateDatabase extends BaseDatabase { } async add(name: string): Promise { - if (!name) return - const items = await this.getItems() const existId = Object.entries(items).find(([_, v]) => v.n === name)?.[0] if (existId) { @@ -68,7 +66,7 @@ class SiteCateDatabase extends BaseDatabase { } const id = (Object.keys(items || {}).map(k => parseInt(k)).sort().reverse()?.[0] ?? 0) + 1 - items[id] = { n: name } + items[id] = { n: name || items[id]?.n } await this.saveItems(items) return { name, id } diff --git a/src/database/site-database.ts b/src/database/site-database.ts index 0f5609100..85482fe93 100644 --- a/src/database/site-database.ts +++ b/src/database/site-database.ts @@ -26,10 +26,6 @@ type _Entry = { * Alias */ a?: string - /** - * Auto-detected - */ - d?: boolean /** * Icon url */ @@ -60,16 +56,19 @@ function cvt2Key({ host, type }: timer.site.SiteKey): string { } function cvt2SiteKey(key: string): timer.site.SiteKey { - if (key?.startsWith(VIRTUAL_KEY_PREFIX)) { + if (key.startsWith(VIRTUAL_KEY_PREFIX)) { return { host: key.substring(VIRTUAL_KEY_PREFIX.length), type: 'virtual', } - } else if (key?.startsWith(HOST_KEY_PREFIX)) { + } else if (key.startsWith(HOST_KEY_PREFIX)) { return { host: key.substring(HOST_KEY_PREFIX.length + 1), type: key.charAt(HOST_KEY_PREFIX.length) === MERGED_FLAG ? 'merged' : 'normal', } + } else { + // Can't go there + return { host: key, type: 'normal' } } } @@ -83,8 +82,7 @@ function cvt2Entry({ alias, iconUrl, cate, run }: timer.site.SiteInfo): _Entry { } function cvt2SiteInfo(key: timer.site.SiteKey, entry: _Entry): timer.site.SiteInfo { - if (!entry) return undefined - const { a, d, i, c, r } = entry + const { a, i, c, r } = entry const siteInfo: timer.site.SiteInfo = { ...key } siteInfo.alias = a siteInfo.cate = c @@ -113,7 +111,7 @@ async function select(this: SiteDatabase, condition?: SiteCondition): Promise boolean { +function buildFilter(condition?: SiteCondition): (site: timer.site.SiteInfo) => boolean { const { fuzzyQuery, cateIds, types } = condition || {} let cateFilter = typeof cateIds === 'number' ? [cateIds] : (cateIds?.length ? cateIds : undefined) let typeFilter = typeof types === 'string' ? [types] : (types?.length ? types : undefined) @@ -142,12 +140,9 @@ function matchType(types: timer.site.Type[], site: timer.site.SiteInfo): boolean * * @returns site info, or undefined */ -async function get(this: SiteDatabase, key: timer.site.SiteKey): Promise { - const entry: _Entry = await this.storage.getOne(cvt2Key(key)) - if (!entry) { - return undefined - } - return cvt2SiteInfo(key, entry) +async function get(this: SiteDatabase, key: timer.site.SiteKey): Promise { + const entry = await this.storage.getOne<_Entry>(cvt2Key(key)) + return entry ? cvt2SiteInfo(key, entry) : null } async function getBatch(this: SiteDatabase, keys: timer.site.SiteKey[]): Promise { @@ -161,7 +156,7 @@ async function getBatch(this: SiteDatabase, keys: timer.site.SiteKey[]): Promise */ async function save(this: SiteDatabase, ...sites: timer.site.SiteInfo[]): Promise { if (!sites?.length) return - const toSet = {} + const toSet: Record = {} sites?.forEach(s => toSet[cvt2Key(s)] = cvt2Entry(s)) await this.storage.set(toSet) } @@ -174,7 +169,7 @@ async function remove(this: SiteDatabase, ...siteKeys: timer.site.SiteKey[]): Pr async function exist(this: SiteDatabase, siteKey: timer.site.SiteKey): Promise { const key = cvt2Key(siteKey) - const entry: _Entry = await this.storage.getOne(key) + const entry = await this.storage.getOne<_Entry>(key) return !!entry } @@ -212,7 +207,7 @@ class SiteDatabase extends BaseDatabase { addChangeListener(listener: (oldAndNew: [timer.site.SiteInfo, timer.site.SiteInfo][]) => void) { const storageListener = ( changes: { [key: string]: chrome.storage.StorageChange }, - _areaName: "sync" | "local" | "managed" + _areaName: chrome.storage.AreaName, ) => { const changedSites: [timer.site.SiteInfo, timer.site.SiteInfo][] = Object.entries(changes) .filter(([k]) => k.startsWith(DB_KEY_PREFIX)) diff --git a/src/database/stat-database/filter.ts b/src/database/stat-database/filter.ts index 93f6d0e78..8d46f13d3 100644 --- a/src/database/stat-database/filter.ts +++ b/src/database/stat-database/filter.ts @@ -49,7 +49,7 @@ function filterDate( return true } -function filterNumberRange(val: number, [start, end]: [start: number, end: number]): boolean { +function filterNumberRange(val: number, [start, end]: [start?: number, end?: number]): boolean { if (start !== null && start !== undefined && start > val) return false if (end !== null && end !== undefined && end < val) return false return true @@ -76,15 +76,15 @@ function filterByCond(result: _FilterResult, condition: _StatCondition): boolean } -function processDateCondition(cond: _StatCondition, paramDate: Date | [Date, Date?]) { +function processDateCondition(cond: _StatCondition, paramDate?: Date | [Date, Date?]) { if (!paramDate) return if (paramDate instanceof Date) { cond.useExactDate = true cond.exactDateStr = formatTimeYMD(paramDate as Date) } else { - let startDate: Date = undefined - let endDate: Date = undefined + let startDate: Date | undefined = undefined + let endDate: Date | undefined = undefined const dateArr = paramDate as [Date, Date] dateArr && dateArr.length >= 2 && (endDate = dateArr[1]) dateArr && dateArr.length >= 1 && (startDate = dateArr[0]) @@ -94,13 +94,13 @@ function processDateCondition(cond: _StatCondition, paramDate: Date | [Date, Dat } } -function processParamTimeCondition(cond: _StatCondition, paramTime: Vector<2>) { +function processParamTimeCondition(cond: _StatCondition, paramTime?: [number, number?]) { if (!paramTime) return paramTime.length >= 2 && (cond.timeEnd = paramTime[1]) paramTime.length >= 1 && (cond.timeStart = paramTime[0]) } -function processParamFocusCondition(cond: _StatCondition, paramFocus: Vector<2>) { +function processParamFocusCondition(cond: _StatCondition, paramFocus?: Vector<2>) { if (!paramFocus) return paramFocus.length >= 2 && (cond.focusEnd = paramFocus[1]) paramFocus.length >= 1 && (cond.focusStart = paramFocus[0]) diff --git a/src/database/stat-database/index.ts b/src/database/stat-database/index.ts index 3097d39ab..ec4cc0cbe 100644 --- a/src/database/stat-database/index.ts +++ b/src/database/stat-database/index.ts @@ -34,7 +34,7 @@ export type StatCondition = { * * @since 0.0.9 */ - timeRange?: Vector<2> + timeRange?: [number, number?] /** * Whether to enable full host search, default is false * @@ -79,8 +79,8 @@ function generateKey(host: string, date: Date | string) { return str + host } -function migrate(exists: { [key: string]: timer.core.Result }, data: any): { [key: string]: timer.core.Result } { - const result = {} +function migrate(exists: { [key: string]: timer.core.Result }, data: any): timer.stat.ResultSet { + const result: timer.stat.ResultSet = {} Object.entries(data) .filter(([key]) => /^20\d{2}[01]\d[0-3]\d.*/.test(key)) .forEach(([key, value]) => { @@ -96,7 +96,7 @@ class StatDatabase extends BaseDatabase { async refresh(): Promise<{ [key: string]: unknown }> { const result = await this.storage.get() - const items = {} + const items: timer.stat.ResultSet = {} Object.entries(result) .filter(([key]) => !key.startsWith(REMAIN_WORD_PREFIX)) .forEach(([key, value]) => items[key] = value) @@ -124,14 +124,14 @@ class StatDatabase extends BaseDatabase { */ async accumulateBatch(data: timer.stat.ResultSet, date: Date | string): Promise { const hosts = Object.keys(data) - if (!hosts.length) return + if (!hosts.length) return {} const dateStr = typeof date === 'string' ? date : formatTimeYMD(date) const keys: { [host: string]: string } = {} hosts.forEach(host => keys[host] = generateKey(host, dateStr)) const items = await this.storage.get(Object.values(keys)) - const toUpdate = {} + const toUpdate: timer.stat.ResultSet = {} const afterUpdated: timer.stat.ResultSet = {} Object.entries(keys).forEach(([host, key]) => { const item = data[host] diff --git a/src/database/whitelist-database.ts b/src/database/whitelist-database.ts index a9bb580fa..7eb1f0c3d 100644 --- a/src/database/whitelist-database.ts +++ b/src/database/whitelist-database.ts @@ -44,7 +44,7 @@ class WhitelistDatabase extends BaseDatabase { addChangeListener(listener: (whitelist: string[]) => void) { const storageListener = ( changes: { [key: string]: chrome.storage.StorageChange }, - _areaName: "sync" | "local" | "managed" + _areaName: chrome.storage.AreaName, ) => { const changeInfo = changes[WHITELIST_KEY] changeInfo && listener(changeInfo.newValue || []) diff --git a/src/i18n/chrome/compile.ts b/src/i18n/chrome/compile.ts index 0ad28b8a8..927d329e0 100644 --- a/src/i18n/chrome/compile.ts +++ b/src/i18n/chrome/compile.ts @@ -1,13 +1,13 @@ /** * Copyright (c) 2021 Hengyang Zhang - * + * * This software is released under the MIT License. * https://opensource.org/licenses/MIT */ // Generate the messages used by Chrome function compile(obj: any, parentKey = ''): any { - const result = {} + const result: any = {} if (typeof obj === 'object') { for (const key in obj) { const val = obj[key] diff --git a/src/i18n/element.ts b/src/i18n/element.ts index 3312248e8..d5628e4d5 100644 --- a/src/i18n/element.ts +++ b/src/i18n/element.ts @@ -18,11 +18,9 @@ const LOCALES: { [locale in timer.Locale]: () => Promise<{ default: Language }> ar: () => import('element-plus/lib/locale/lang/ar'), } -let EL_LOCALE: Language = null - export const initElementLocale = async (app: App) => { const module = await LOCALES[locale]?.() - EL_LOCALE = module?.default + const EL_LOCALE = module?.default app.use(ElementPlus, { locale: EL_LOCALE }) } diff --git a/src/i18n/index.ts b/src/i18n/index.ts index df2e44a3c..6790e8a97 100644 --- a/src/i18n/index.ts +++ b/src/i18n/index.ts @@ -123,7 +123,7 @@ function tryGetOriginalI18nVal( specLocale?: timer.Locale ) { try { - return keyPath(messages[specLocale || locale]) + return keyPath(messages[specLocale || locale] as MessageType) } catch (ignore) { return undefined } @@ -135,7 +135,7 @@ export function getI18nVal( specLocale?: timer.Locale ): string { const result = tryGetOriginalI18nVal(messages, keyPath, specLocale) - || keyPath(messages[FEEDBACK_LOCALE]) + || keyPath(messages[FEEDBACK_LOCALE] as MessageType) || '' return typeof result === 'string' ? result : JSON.stringify(result) } @@ -161,18 +161,18 @@ export function t(messages: Messages, props: Translate return param ? fillWithParam(result, param) : result } -export type I18nKey = (messages: MessageType | EmbeddedPartial) => any +export type I18nKey = (messages: MessageType) => any export type I18nResultItem = Node | string const findParamAndReplace = (resultArr: I18nResultItem[], [key, value]: any) => { const paramPlacement = `{${key}}` - const temp = [] + const temp: I18nResultItem[] = [] resultArr.forEach((item) => { if (typeof item === 'string' && item.includes(paramPlacement)) { // 将 string 替换成具体的 VNode let splits: I18nResultItem[] = (item as string).split(paramPlacement) - splits = splits.reduce((left, right) => left.length ? left.concat(value, right) : left.concat(right), []) + splits = splits.reduce[]>((left, right) => left.length ? left.concat(value, right) : left.concat(right), []) temp.push(...splits) } else { temp.push(item) diff --git a/src/i18n/message/merge.ts b/src/i18n/message/merge.ts index 900301897..06acdd627 100644 --- a/src/i18n/message/merge.ts +++ b/src/i18n/message/merge.ts @@ -17,7 +17,7 @@ export const ALL_LOCALES: timer.Locale[] = Object.keys(ALL_LOCALE_VALIDATOR) as export type MessageRoot = { [key in keyof T]: Messages } export function merge(messageRoot: MessageRoot): Required> { - const result = {} + const result: Partial>> = {} ALL_LOCALES.forEach(locale => { const message = messageOfRoot(locale, messageRoot) result[locale] = message as T & EmbeddedPartial @@ -26,7 +26,7 @@ export function merge(messageRoot: MessageRoot): Required> { } function messageOfRoot(locale: timer.Locale, messageRoot: MessageRoot): T { - const entries: [string, any][] = Object.entries(messageRoot).map(([key, val]) => ([key, val[locale]])) + const entries: [string, any][] = Object.entries(messageRoot).map(([key, val]) => ([key, (val as Record>)[locale]])) const result = Object.fromEntries(entries) as T return result } \ No newline at end of file diff --git a/src/pages/app/Layout/menu/Side.tsx b/src/pages/app/Layout/menu/Side.tsx index c4878b2c2..57955db83 100644 --- a/src/pages/app/Layout/menu/Side.tsx +++ b/src/pages/app/Layout/menu/Side.tsx @@ -19,7 +19,7 @@ const iconStyle: StyleValue = { lineHeight: '0.83em' } -function renderMenuLeaf(menu: MenuItem, router: Router, currentActive: Ref) { +function renderMenuLeaf(menu: MenuItem, router: Router, currentActive: Ref) { const { route, title, icon, index } = menu return ( { const router = useRouter() - const currentActive: Ref = ref() + const currentActive = ref() const syncRouter = () => { const route = router.currentRoute.value route && (currentActive.value = route.path) diff --git a/src/pages/app/Layout/menu/route.ts b/src/pages/app/Layout/menu/route.ts index f1f3b7eb9..ccfee5fe8 100644 --- a/src/pages/app/Layout/menu/route.ts +++ b/src/pages/app/Layout/menu/route.ts @@ -14,11 +14,11 @@ function openMenu(route: string, title: I18nKey, router: Router) { const openHref = (href: string) => createTabAfterCurrent(href) -export function handleClick(menuItem: MenuItem, router: Router, currentActive?: Ref) { +export function handleClick(menuItem: MenuItem, router: Router, currentActive?: Ref) { const { route, title, href } = menuItem if (route) { openMenu(route, title, router) - } else { + } else if (href) { openHref(href) currentActive && (currentActive.value = router.currentRoute?.value?.path) } diff --git a/src/pages/app/components/About/DescLink.tsx b/src/pages/app/components/About/DescLink.tsx index cc5d2e006..6651884d6 100644 --- a/src/pages/app/components/About/DescLink.tsx +++ b/src/pages/app/components/About/DescLink.tsx @@ -1,5 +1,5 @@ import { ElLink } from "element-plus" -import { defineComponent, h, type PropType } from "vue" +import { defineComponent, h, useSlots, type PropType } from "vue" import "./desc-link.sass" export type Icon = "github" | "element-plus" | "echarts" | "vue" @@ -9,12 +9,13 @@ const _default = defineComponent({ href: String, icon: String as PropType }, - setup(props, ctx) { + setup(props) { const { icon, href } = props + const { default: default_ } = useSlots() return () => ( {icon ?
: null} - {h(ctx.slots.default)} + {!!default_ && h(default_)} ) } diff --git a/src/pages/app/components/Analysis/components/AnalysisFilter/TargetSelect.tsx b/src/pages/app/components/Analysis/components/AnalysisFilter/TargetSelect.tsx index f9c172dd1..228935b2d 100644 --- a/src/pages/app/components/Analysis/components/AnalysisFilter/TargetSelect.tsx +++ b/src/pages/app/components/Analysis/components/AnalysisFilter/TargetSelect.tsx @@ -7,6 +7,7 @@ import statService from "@service/stat-service" import { identifySiteKey, parseSiteKeyFromIdentity, SiteMap } from "@util/site" import { useDebounce } from "@vueuse/core" import { ElSelectV2, ElTag } from "element-plus" +import type { OptionType } from "element-plus/es/components/select-v2/src/select.types" import { computed, defineComponent, type PropType, type StyleValue } from "vue" import type { AnalysisTarget } from "../../types" import { labelOfHostInfo } from "../../util" @@ -14,7 +15,7 @@ import { labelOfHostInfo } from "../../util" const SITE_PREFIX = 'S' const CATE_PREFIX = 'C' -const cvtTarget2Key = (target: AnalysisTarget): string => { +const cvtTarget2Key = (target: AnalysisTarget | undefined): string => { if (target?.type === 'site') { return `${SITE_PREFIX}${identifySiteKey(target.key)}` } else if (target?.type === 'cate') { @@ -23,18 +24,19 @@ const cvtTarget2Key = (target: AnalysisTarget): string => { return '' } -const cvtKey2Target = (key: string): AnalysisTarget => { +const cvtKey2Target = (key: string | undefined): AnalysisTarget | undefined => { if (!key) return undefined const prefix = key?.charAt?.(0) const content = key?.substring(1) if (prefix === SITE_PREFIX) { - return { type: 'site', key: parseSiteKeyFromIdentity(content) } + const key = parseSiteKeyFromIdentity(content) + if (key) return { type: 'site', key } } else if (prefix === CATE_PREFIX) { - let cateId = undefined + let cateId: number | undefined try { cateId = parseInt(content) } catch { } - return { type: 'cate', key: cateId } + if (cateId) return { type: 'cate', key: cateId } } return undefined } @@ -68,7 +70,9 @@ const fetchItems = async (categories: timer.site.Cate[]): Promise<[siteItems: Ta const sites = await siteService.selectAll() sites?.forEach(site => siteSet.put(site, site)) - const siteItems = siteSet?.map((_, site) => ({ type: 'site', key: site, label: labelOfHostInfo(site) }) satisfies TargetItem) + const siteItems = siteSet?.map((_, site) => site) + .filter(site => !!site) + .map(site => ({ type: 'site', key: site, label: labelOfHostInfo(site) }) satisfies TargetItem) return [cateItems, siteItems] } @@ -130,7 +134,7 @@ const TargetSelect = defineComponent({ { deps: categories }, ) - const [query, setQuery] = useState() + const [query, setQuery] = useState('') const debouncedQuery = useDebounce(query, 50) const options = computed(() => { @@ -144,7 +148,7 @@ const TargetSelect = defineComponent({ cateItems = cateItems?.filter(item => item.label?.includes?.(q)) } - let res = [] + let res: OptionType[] = [] cateItems?.length && res.push({ value: 'cate', label: t(msg => msg.analysis.target.cate), @@ -173,8 +177,8 @@ const TargetSelect = defineComponent({ defaultFirstOption options={options.value ?? []} fitInputWidth={false} - v-slots={({ item }) => { - const target = item.data as TargetItem + v-slots={({ item }: any) => { + const target = (item as any).data as TargetItem return target?.type === 'site' ? : target?.label }} /> diff --git a/src/pages/app/components/Analysis/components/AnalysisFilter/index.tsx b/src/pages/app/components/Analysis/components/AnalysisFilter/index.tsx index 7726ba732..fd6edaa1c 100644 --- a/src/pages/app/components/Analysis/components/AnalysisFilter/index.tsx +++ b/src/pages/app/components/Analysis/components/AnalysisFilter/index.tsx @@ -20,10 +20,13 @@ export type AnalysisFilterInstance = { const AnalysisFilter = defineComponent({ props: { target: Object as PropType, - timeFormat: String as PropType + timeFormat: { + type: String as PropType, + required: true, + } }, emits: { - targetChange: (_target: AnalysisTarget) => true, + targetChange: (_target: AnalysisTarget | undefined) => true, timeFormatChange: (_format: timer.app.TimeFormat) => true, }, setup(props, ctx) { diff --git a/src/pages/app/components/Analysis/components/Summary/Calendar/Wrapper.ts b/src/pages/app/components/Analysis/components/Summary/Calendar/Wrapper.ts index dba42d478..0a98fe927 100644 --- a/src/pages/app/components/Analysis/components/Summary/Calendar/Wrapper.ts +++ b/src/pages/app/components/Analysis/components/Summary/Calendar/Wrapper.ts @@ -19,6 +19,7 @@ import { type TooltipComponentOption, type VisualMapComponentOption } from "echarts" +import { TopLevelFormatterParams } from "echarts/types/dist/shared" type EcOption = ComposeOption< | EffectScatterSeriesOption @@ -47,7 +48,7 @@ const getWeekNum = (domWidth: number): number => { return Math.floor(Math.min(weekNum, MAX_WEEK_NUM)) } -type EffectScatterItem = EffectScatterSeriesOption["data"][number] +type EffectScatterItem = MakeRequired["data"][number] const cvtHeatmapItem = (d: _Value): EffectScatterItem => { let item: EffectScatterItem = { value: d, itemStyle: undefined, label: undefined, emphasis: undefined } const minutes = d[2] @@ -60,10 +61,10 @@ const cvtHeatmapItem = (d: _Value): EffectScatterItem => { function getXAxisLabelMap(data: _Value[]): { [x: string]: string } { const allMonthLabel = t(msg => msg.calendar.months).split('|') - const result = {} + const result: Record = {} // {[ x:string ]: Set } const xAndMonthMap = groupBy(data, e => e[0], grouped => new Set(grouped.map(a => a[3].substring(4, 6)))) - let lastMonth = undefined + let lastMonth: string | undefined = undefined Object.entries(xAndMonthMap).forEach(([x, monthSet]) => { if (monthSet.size != 1) { return @@ -97,13 +98,14 @@ function optionOf(data: _Value[], weekDays: string[], format: timer.app.TimeForm }, tooltip: { borderWidth: 0, - formatter: (params: any) => { - const { data } = params - const { value } = data + formatter: (params: TopLevelFormatterParams) => { + const parma = Array.isArray(params) ? params[0] : params + const { data } = parma + const { value } = data as any const [_1, _2, mills, date] = value as _Value - if (!mills) return undefined + if (!mills) return '' const time = parseTime(date) - return `${formatTime(time, t(msg => msg.calendar.dateFormat))}
${periodFormatter(mills, { format })}` + return time ? `${formatTime(time, t(msg => msg.calendar.dateFormat))}
${periodFormatter(mills, { format })}` : '' }, }, grid: { height: '68%', left: gridLeft, right: 0, top: '18%' }, diff --git a/src/pages/app/components/Analysis/components/Summary/index.tsx b/src/pages/app/components/Analysis/components/Summary/index.tsx index 03bb918fe..5a9ae0a7a 100644 --- a/src/pages/app/components/Analysis/components/Summary/index.tsx +++ b/src/pages/app/components/Analysis/components/Summary/index.tsx @@ -21,7 +21,7 @@ type Summary = { firstDay?: string } -function computeSummary(target: AnalysisTarget, rows: timer.stat.Row[]): Summary { +function computeSummary(target: AnalysisTarget | undefined, rows: timer.stat.Row[]): Summary | undefined { if (!target) return undefined const summary: Summary = { focus: 0, visit: 0, day: 0 } diff --git a/src/pages/app/components/Analysis/components/Trend/Dimension/Wrapper.ts b/src/pages/app/components/Analysis/components/Trend/Dimension/Wrapper.ts index 37a3ead1a..fbf4efddc 100644 --- a/src/pages/app/components/Analysis/components/Trend/Dimension/Wrapper.ts +++ b/src/pages/app/components/Analysis/components/Trend/Dimension/Wrapper.ts @@ -33,7 +33,7 @@ type BizOption = { valueFormatter: ValueFormatter } -type ValueItem = LineSeriesOption["data"][0] & { +type ValueItem = Exclude[0] & { _data: DimensionEntry } diff --git a/src/pages/app/components/Analysis/components/Trend/Filter.tsx b/src/pages/app/components/Analysis/components/Trend/Filter.tsx index ba9266bd1..156b72838 100644 --- a/src/pages/app/components/Analysis/components/Trend/Filter.tsx +++ b/src/pages/app/components/Analysis/components/Trend/Filter.tsx @@ -16,7 +16,7 @@ import { defineComponent, ref, type PropType } from "vue" function datePickerShortcut(agoOfStart?: number, agoOfEnd?: number): ElementDatePickerShortcut { return { text: t(msg => msg.calendar.range.lastDays, { n: agoOfStart }), - value: daysAgo(agoOfStart - 1 || 0, agoOfEnd || 0), + value: daysAgo((agoOfStart ?? 0) - 1 || 0, agoOfEnd || 0), } } @@ -35,7 +35,7 @@ const _default = defineComponent({ dateRangeChange: (_val: [Date, Date]) => true }, setup(props, ctx) { - const dateRange = ref<[Date, Date]>(props.dateRange) + const dateRange = ref<[Date, Date] | undefined>(props.dateRange) return () => (
msg.analysis.trend.activeDay)}/${t(msg => msg.anal const VISIT_LABEL = t(msg => msg.analysis.common.visitTotal) const FOCUS_LABEL = t(msg => msg.analysis.common.focusTotal) -const computeDayValue = (activeDay: RingValue, rangeLength: number) => { +const computeDayValue = (activeDay: RingValue | undefined, rangeLength: number) => { const thisActiveDay = activeDay?.[0] return `${thisActiveDay?.toString() || '-'}/${rangeLength?.toString() || '-'}` } diff --git a/src/pages/app/components/Analysis/components/Trend/context.ts b/src/pages/app/components/Analysis/components/Trend/context.ts index 1df66dc6c..2ba9bd1f7 100644 --- a/src/pages/app/components/Analysis/components/Trend/context.ts +++ b/src/pages/app/components/Analysis/components/Trend/context.ts @@ -22,6 +22,6 @@ export const initProvider = ( dateRange, rangeLength }) -export const useAnalysisTrendDateRange = (): Ref<[Date, Date]> => useProvider(NAMESPACE, "dateRange").dateRange +export const useAnalysisTrendDateRange = () => useProvider(NAMESPACE, "dateRange").dateRange -export const useAnalysisTrendRangeLength = (): Ref => useProvider(NAMESPACE, "rangeLength").rangeLength \ No newline at end of file +export const useAnalysisTrendRangeLength = () => useProvider(NAMESPACE, "rangeLength").rangeLength \ No newline at end of file diff --git a/src/pages/app/components/Analysis/components/Trend/index.tsx b/src/pages/app/components/Analysis/components/Trend/index.tsx index 4265cf4f1..2f049912f 100644 --- a/src/pages/app/components/Analysis/components/Trend/index.tsx +++ b/src/pages/app/components/Analysis/components/Trend/index.tsx @@ -20,8 +20,8 @@ import './style.sass' import Total from "./Total" type DailyIndicator = { - value: number - date: string + value: number | undefined + date: string | undefined } type GlobalIndicator = number @@ -31,21 +31,21 @@ type DimensionType = 'focus' | 'visit' type IndicatorSet = Record & { activeDay: number } type SourceParam = { dateRange: [Date, Date] - rows?: timer.stat.Row[] + rows: timer.stat.Row[] } type EffectParam = { - indicators: Ref - previousIndicators: Ref - focusData: Ref - visitData: Ref + indicators: Ref + previousIndicators: Ref + focusData: Ref + visitData: Ref } const VISIT_MAX = t(msg => msg.analysis.trend.maxVisit) @@ -55,7 +55,10 @@ const FOCUS_MAX = t(msg => msg.analysis.trend.maxFocus) const FOCUS_AVE = t(msg => msg.analysis.trend.averageFocus) const FOCUS_CHART_TITLE = t(msg => msg.analysis.trend.focusTitle) -function computeIndicatorSet(rows: timer.stat.Row[], dateRange: [Date, Date]): [IndicatorSet, Record] { +function computeIndicatorSet( + rows: timer.stat.Row[], + dateRange: [Date, Date] | undefined, +): [IndicatorSet | undefined, Record] { const [start, end] = dateRange || [] const allDates = start && end ? getAllDatesBetween(start, end) : [] if (!rows) { @@ -64,7 +67,7 @@ function computeIndicatorSet(rows: timer.stat.Row[], dateRange: [Date, Date]): [ } const days = allDates.length - const periodRows = rows.filter(({ date }) => allDates.includes(date)) + const periodRows = rows.filter(({ date }) => allDates.includes(date ?? '')) const periodRowMap = groupBy(periodRows, r => r.date, a => a[0]) let focusMax: DailyIndicator let visitMax: DailyIndicator @@ -92,7 +95,7 @@ function computeIndicatorSet(rows: timer.stat.Row[], dateRange: [Date, Date]): [ return [indicators, fullPeriodRow] } -function lastRange(dateRange: [Date, Date]): [Date, Date] { +function lastRange(dateRange: [Date, Date]): [Date, Date] | undefined { const [start, end] = dateRange || [] if (!start || !end) return undefined const dayLength = getDayLength(start, end) @@ -101,7 +104,7 @@ function lastRange(dateRange: [Date, Date]): [Date, Date] { return [newStart, newEnd] } -const visitFormatter: ValueFormatter = (val: number) => Number.isInteger(val) ? val.toString() : val?.toFixed(1) || '-' +const visitFormatter: ValueFormatter = (val: number | undefined) => (Number.isInteger(val) ? val?.toString() : val?.toFixed(1)) ?? '-' function handleDataChange(source: SourceParam, effect: EffectParam) { const { dateRange, rows } = source diff --git a/src/pages/app/components/Analysis/context.ts b/src/pages/app/components/Analysis/context.ts index 231662fc6..95422bb56 100644 --- a/src/pages/app/components/Analysis/context.ts +++ b/src/pages/app/components/Analysis/context.ts @@ -6,11 +6,11 @@ */ import { useProvide, useProvider } from "@hooks" -import { watch, type Ref } from "vue" +import { type Ref } from "vue" import { AnalysisTarget } from "./types" type Context = { - target: Ref + target: Ref timeFormat: Ref rows: Ref } @@ -18,15 +18,15 @@ type Context = { const NAMESPACE = 'siteAnalysis' export const initProvider = ( - target: Ref, + target: Ref, timeFormat: Ref, rows: Ref, ) => useProvide(NAMESPACE, { target, timeFormat, rows }) -export const useAnalysisTarget = (): Ref => useProvider(NAMESPACE, "target").target +export const useAnalysisTarget = () => useProvider(NAMESPACE, "target").target -export const useAnalysisTimeFormat = (): Ref => useProvider(NAMESPACE, "timeFormat").timeFormat +export const useAnalysisTimeFormat = () => useProvider(NAMESPACE, "timeFormat").timeFormat -export const useAnalysisRows = (): Ref => useProvider(NAMESPACE, "rows").rows \ No newline at end of file +export const useAnalysisRows = () => useProvider(NAMESPACE, "rows").rows \ No newline at end of file diff --git a/src/pages/app/components/Analysis/index.tsx b/src/pages/app/components/Analysis/index.tsx index 4eec68897..9af411b15 100644 --- a/src/pages/app/components/Analysis/index.tsx +++ b/src/pages/app/components/Analysis/index.tsx @@ -17,7 +17,7 @@ import Trend from "./components/Trend" import { initProvider } from "./context" import type { AnalysisTarget } from "./types" -function getTargetFromQuery(): AnalysisTarget { +function getTargetFromQuery(): AnalysisTarget | undefined { // Process the query param const query: AnalysisQuery = useRoute().query as unknown as AnalysisQuery useRouter().replace({ query: {} }) @@ -27,7 +27,7 @@ function getTargetFromQuery(): AnalysisTarget { return undefined } -async function query(target: AnalysisTarget): Promise { +async function query(target: AnalysisTarget | undefined): Promise { if (!target?.key) return [] let param: StatQueryParam = { @@ -53,7 +53,7 @@ const _default = defineComponent(() => { const [target, setTarget] = useState(getTargetFromQuery()) const [timeFormat, setTimeFormat] = useState('default') - const { data: rows, loading } = useRequest(() => query(target.value), { deps: target }) + const { data: rows, loading } = useRequest(() => query(target.value), { deps: target, defaultValue: [] }) initProvider(target, timeFormat, rows) diff --git a/src/pages/app/components/Analysis/util.ts b/src/pages/app/components/Analysis/util.ts index 5fd3cd085..2d5e0a6d9 100644 --- a/src/pages/app/components/Analysis/util.ts +++ b/src/pages/app/components/Analysis/util.ts @@ -10,7 +10,7 @@ import { t } from "@app/locale" /** * Transfer host info to label */ -export function labelOfHostInfo(site: timer.site.SiteKey): string { +export function labelOfHostInfo(site: timer.site.SiteKey | undefined): string { if (!site) return '' const { host, type } = site if (!host) return '' @@ -21,8 +21,8 @@ export function labelOfHostInfo(site: timer.site.SiteKey): string { } export type RingValue = [ - current: number, - last: number, + current?: number, + last?: number, ] /** @@ -47,9 +47,9 @@ export function computeRingText(ring: RingValue, formatter?: ValueFormatter): st return result } -export type ValueFormatter = (val: number) => string +export type ValueFormatter = (val: number | undefined) => string -export const formatValue = (val: number, formatter?: ValueFormatter) => formatter ? formatter(val) : val?.toString() || '-' +export const formatValue = (val: number | undefined, formatter?: ValueFormatter) => formatter ? formatter(val) : val?.toString() || '-' export type DimensionEntry = { date: string diff --git a/src/pages/app/components/Dashboard/components/Calendar/Wrapper.ts b/src/pages/app/components/Dashboard/components/Calendar/Wrapper.ts index 35ae710a8..8441ab945 100644 --- a/src/pages/app/components/Dashboard/components/Calendar/Wrapper.ts +++ b/src/pages/app/components/Dashboard/components/Calendar/Wrapper.ts @@ -20,6 +20,7 @@ import { type TooltipComponentOption, type VisualMapComponentOption, } from "echarts" +import { TopLevelFormatterParams } from "echarts/types/dist/shared" export type ChartValue = [ x: number, @@ -49,10 +50,10 @@ function formatTooltip(mills: number, date: string): string { function getXAxisLabelMap(data: ChartValue[]): { [x: string]: string } { const allMonthLabel = t(msg => msg.calendar.months).split('|') - const result = {} + const result: Record = {} // {[ x:string ]: Set } const xAndMonthMap = groupBy(data, e => e[0], grouped => new Set(grouped.map(a => a[3].substring(4, 6)))) - let lastMonth = undefined + let lastMonth: string Object.entries(xAndMonthMap).forEach(([x, monthSet]) => { if (monthSet.size != 1) { return @@ -69,7 +70,7 @@ function getXAxisLabelMap(data: ChartValue[]): { [x: string]: string } { return result } -type HeatmapItem = HeatmapSeriesOption["data"][number] +type HeatmapItem = Exclude[number] const cvtHeatmapItem = (d: ChartValue): HeatmapItem => { let item: HeatmapItem = { value: d, itemStyle: undefined, label: undefined, emphasis: undefined } @@ -124,11 +125,12 @@ function optionOf(data: ChartValue[], weekDays: string[], dom: HTMLElement): EcO return { tooltip: { borderWidth: 0, - formatter: (params: any) => { - const { data } = params - const { value } = data + formatter: (params: TopLevelFormatterParams) => { + const param = Array.isArray(params) ? params[0] : params + const { data } = param + const { value } = data as any const [_1, _2, mills, date] = value - return mills ? formatTooltip(mills as number, date) : undefined + return mills ? formatTooltip(mills as number, date) : '' }, }, grid: { height: '70%', left: '7%', width: `${gridWidth * 100}%`, top: '18%', }, diff --git a/src/pages/app/components/Dashboard/components/Calendar/index.tsx b/src/pages/app/components/Dashboard/components/Calendar/index.tsx index 104093cf6..88ce15f9c 100644 --- a/src/pages/app/components/Dashboard/components/Calendar/index.tsx +++ b/src/pages/app/components/Dashboard/components/Calendar/index.tsx @@ -20,9 +20,9 @@ import { computed, defineComponent } from "vue" import ChartTitle from "../../ChartTitle" import Wrapper, { type BizOption, type ChartValue } from "./Wrapper" -const titleText = (option: Result) => { +const titleText = (option: Result | undefined) => { const { value, yearAgo } = option || {} - const start = formatTimeYMD(yearAgo) + const start = yearAgo ? formatTimeYMD(yearAgo) : '-' const statValues = Object.entries(value || {}).filter(([date]) => date.localeCompare(start) >= 0).map(([, v]) => v) const totalMills = sum(statValues) const totalHours = Math.floor(totalMills / MILL_PER_HOUR) @@ -75,7 +75,7 @@ const _default = defineComponent(() => { const { elRef } = useEcharts(Wrapper, biz, { afterInit(ew) { const supportClick = !window.matchMedia("(any-pointer:coarse)").matches - supportClick && ew.instance.on("click", (params: any) => handleClick(params.value as ChartValue)) + supportClick && ew.instance?.on("click", (params: any) => handleClick(params.value as ChartValue)) } }) diff --git a/src/pages/app/components/Dashboard/components/Indicator/index.tsx b/src/pages/app/components/Dashboard/components/Indicator/index.tsx index 1696a81fb..fd3bb3efe 100644 --- a/src/pages/app/components/Dashboard/components/Indicator/index.tsx +++ b/src/pages/app/components/Dashboard/components/Indicator/index.tsx @@ -42,7 +42,8 @@ async function query(): Promise<_Value> { let visits = 0 let browsingTime = 0 allData.forEach(({ siteKey, focus, time }) => { - hostSet.add(siteKey?.host) + const { host } = siteKey || {} + host && hostSet.add(host) visits += time browsingTime += focus }) @@ -55,21 +56,20 @@ async function query(): Promise<_Value> { browsingTime, most2Hour } - let installTime = undefined + // 2. if not exist, calculate from all data items const firstDate = allData.map(a => a.date).filter(d => d?.length === 8).sort()[0] if (firstDate) { const year = parseInt(firstDate.substring(0, 4)) const month = parseInt(firstDate.substring(4, 6)) - 1 const date = parseInt(firstDate.substring(6, 8)) - installTime = new Date(year, month, date) + const installTime = new Date(year, month, date) + result.installedDays = calculateInstallDays(installTime, new Date()) } - - installTime && (result.installedDays = calculateInstallDays(installTime, new Date())) return result } -const computeI18nParam = (valueParam: Record, duration: number): Record => { +const computeI18nParam = (valueParam: Record, duration?: number): Record => { return Object.fromEntries( Object.entries(valueParam || {}).map(([key, val]) => [key, ]) ) @@ -100,7 +100,7 @@ const IndicatorLabel = defineComponent({ } }) -const computeMost2HourParam = (value: _Value): { start: number, end: number } => { +const computeMost2HourParam = (value: _Value | undefined): { start: number, end: number } => { const most2HourIndex = value?.most2Hour const [start, end] = most2HourIndex === undefined || isNaN(most2HourIndex) ? [0, 0] diff --git a/src/pages/app/components/Dashboard/components/MonthOnMonth/Wrapper.ts b/src/pages/app/components/Dashboard/components/MonthOnMonth/Wrapper.ts index 11ecb26c6..cbafdd4ab 100644 --- a/src/pages/app/components/Dashboard/components/MonthOnMonth/Wrapper.ts +++ b/src/pages/app/components/Dashboard/components/MonthOnMonth/Wrapper.ts @@ -96,7 +96,7 @@ type Row = { class Wrapper extends EchartsWrapper<[Row[], Row[]], EcOption> { protected isSizeSensitize: boolean = true - generateOption = ([lastPeriodItems = [], thisPeriodItems = []] = []) => { + generateOption([lastPeriodItems, thisPeriodItems]: [Row[], Row[]]) { const domWidth = this.getDomWidth() if (!domWidth) return {} return optionOf(lastPeriodItems, thisPeriodItems, domWidth) diff --git a/src/pages/app/components/Dashboard/components/MonthOnMonth/index.tsx b/src/pages/app/components/Dashboard/components/MonthOnMonth/index.tsx index 87d91f01c..47e8a32b8 100644 --- a/src/pages/app/components/Dashboard/components/MonthOnMonth/index.tsx +++ b/src/pages/app/components/Dashboard/components/MonthOnMonth/index.tsx @@ -1,12 +1,12 @@ -import { defineComponent } from "vue" -import ChartTitle from "../../ChartTitle" +import { t } from "@app/locale" import { useEcharts } from "@hooks/useEcharts" -import Wrapper from "./Wrapper" +import statService from "@service/stat-service" import { groupBy, sum } from "@util/array" import DateIterator from "@util/date-iterator" import { MILL_PER_DAY } from "@util/time" -import statService from "@service/stat-service" -import { t } from "@app/locale" +import { defineComponent } from "vue" +import ChartTitle from "../../ChartTitle" +import Wrapper from "./Wrapper" const PERIOD_WIDTH = 30 const TOP_NUM = 15 @@ -36,9 +36,9 @@ const fetchData = async (): Promise<[thisMonth: Row[], lastMonth: Row[]]> => { // Query with alias // @since 1.1.8 - const lastPeriodItems: timer.stat.Row[] = await statService.select({ date: [lastPeriodStart, lastPeriodEnd] }) + const lastPeriodItems = await statService.select({ date: [lastPeriodStart, lastPeriodEnd] }) const lastRows = cvtRow(lastPeriodItems, lastPeriodStart, lastPeriodEnd) - const thisPeriodItems: timer.stat.Row[] = await statService.select({ date: [thisPeriodStart, thisPeriodEnd] }) + const thisPeriodItems = await statService.select({ date: [thisPeriodStart, thisPeriodEnd] }) const thisRows = cvtRow(thisPeriodItems, thisPeriodStart, thisPeriodEnd) return [lastRows, thisRows] } diff --git a/src/pages/app/components/Dashboard/components/TopKVisit/index.tsx b/src/pages/app/components/Dashboard/components/TopKVisit/index.tsx index 57cf59a07..5e3bfa7da 100644 --- a/src/pages/app/components/Dashboard/components/TopKVisit/index.tsx +++ b/src/pages/app/components/Dashboard/components/TopKVisit/index.tsx @@ -23,8 +23,8 @@ const fetchData = async () => { } const top = (await statService.selectByPage(query, { num: 1, size: TOP_NUM })).list const data: BizOption[] = top.map(({ time, siteKey, alias }) => ({ - name: alias || siteKey?.host, - host: siteKey?.host, + name: alias ?? siteKey?.host ?? '', + host: siteKey?.host ?? '', alias, value: time, })) diff --git a/src/pages/app/components/DataManage/ClearPanel/ClearFilter/DateFilter.tsx b/src/pages/app/components/DataManage/ClearPanel/ClearFilter/DateFilter.tsx index 145bb2540..e642a987e 100644 --- a/src/pages/app/components/DataManage/ClearPanel/ClearFilter/DateFilter.tsx +++ b/src/pages/app/components/DataManage/ClearPanel/ClearFilter/DateFilter.tsx @@ -10,38 +10,37 @@ import { EL_DATE_FORMAT } from "@i18n/element" import { type ElementDatePickerShortcut } from "@pages/element-ui/date" import { getDatePickerIconSlots } from "@pages/element-ui/rtl" import { formatTime, getBirthday, MILL_PER_DAY } from "@util/time" -import { type DateModelType, ElDatePicker } from "element-plus" -import { defineComponent, type PropType } from "vue" - -const yesterday = new Date().getTime() - MILL_PER_DAY -const daysBefore = (days: number) => new Date(new Date().getTime() - days * MILL_PER_DAY) - -const birthday = getBirthday() -const pickerShortcuts: ElementDatePickerShortcut[] = [ - { - text: t(msg => msg.calendar.range.tillYesterday), - value: [birthday, daysBefore(1)], - }, { - text: t(msg => msg.calendar.range.tillDaysAgo, { n: 7 }), - value: [birthday, daysBefore(7)], - }, { - text: t(msg => msg.calendar.range.tillDaysAgo, { n: 30 }), - value: [birthday, daysBefore(30)], - } -] - -// The birthday of browser -const startPlaceholder = t(msg => msg.calendar.dateFormat, { y: '1994', m: '12', d: '15' }) -const endPlaceholder = formatTime(yesterday, t(msg => msg.calendar.dateFormat)) +import { ElDatePicker } from "element-plus" +import { defineComponent, type StyleValue, type PropType } from "vue" const _default = defineComponent({ emits: { - change: (_date: [DateModelType, DateModelType]) => true + change: (_date: [Date, Date]) => true }, props: { - dateRange: Object as PropType<[DateModelType, DateModelType]> + dateRange: Object as PropType<[Date, Date]> }, setup(props, ctx) { + const yesterday = new Date().getTime() - MILL_PER_DAY + const daysBefore = (days: number) => new Date(new Date().getTime() - days * MILL_PER_DAY) + + const birthday = getBirthday() + const pickerShortcuts: ElementDatePickerShortcut[] = [ + { + text: t(msg => msg.calendar.range.tillYesterday), + value: [birthday, daysBefore(1)], + }, { + text: t(msg => msg.calendar.range.tillDaysAgo, { n: 7 }), + value: [birthday, daysBefore(7)], + }, { + text: t(msg => msg.calendar.range.tillDaysAgo, { n: 30 }), + value: [birthday, daysBefore(30)], + } + ] + const dateFormat = '{y}-{m}-{d}' + const startPlaceholder = formatTime(birthday, dateFormat) + const endPlaceholder = formatTime(yesterday, dateFormat) + return () => (

1. @@ -52,7 +51,7 @@ const _default = defineComponent({ modelValue={props.dateRange} onUpdate:modelValue={date => ctx.emit("change", date)} size="small" - style={{ width: "250px" }} + style={{ width: "250px" } satisfies StyleValue} startPlaceholder={startPlaceholder} endPlaceholder={endPlaceholder} dateFormat={EL_DATE_FORMAT} diff --git a/src/pages/app/components/DataManage/ClearPanel/ClearFilter/NumberFilter.tsx b/src/pages/app/components/DataManage/ClearPanel/ClearFilter/NumberFilter.tsx index 962339d61..25ccf2799 100644 --- a/src/pages/app/components/DataManage/ClearPanel/ClearFilter/NumberFilter.tsx +++ b/src/pages/app/components/DataManage/ClearPanel/ClearFilter/NumberFilter.tsx @@ -10,7 +10,7 @@ import { type DataManageMessage } from "@i18n/message/app/data-manage" import { ElInput } from "element-plus" import { defineComponent, type PropType, type Ref, ref, type StyleValue, watch } from "vue" -const elInput = (ref: Ref, placeholder: string) => ( +const elInput = (ref: Ref, placeholder: string) => ( , placeholder: string) => ( const _default = defineComponent({ props: { - translateKey: String as PropType, - defaultValue: Object as PropType<[string, string]>, + translateKey: { + type: String as PropType, + required: true, + }, + defaultValue: { + type: Object as PropType<[string?, string?]>, + required: true, + }, lineNo: Number, }, emits: { - change: (_val: [string, string]) => true, + change: (_val: [string?, string?]) => true, }, setup(props, ctx) { - const start = ref(props.defaultValue?.[0]) - const end = ref(props.defaultValue?.[1]) + const start = ref(props.defaultValue[0]) + const end = ref(props.defaultValue[1]) watch([start, end], () => ctx.emit("change", [start.value, end.value])) return () => ( diff --git a/src/pages/app/components/DataManage/ClearPanel/ClearFilter/index.tsx b/src/pages/app/components/DataManage/ClearPanel/ClearFilter/index.tsx index ead62e68b..bc9ea7b7c 100644 --- a/src/pages/app/components/DataManage/ClearPanel/ClearFilter/index.tsx +++ b/src/pages/app/components/DataManage/ClearPanel/ClearFilter/index.tsx @@ -14,12 +14,12 @@ import NumberFilter from "./NumberFilter" const _default = defineComponent({ emits: { - delete: (_date: [Date, Date], _focus: [string, string], _time: [string, string]) => true + delete: (_date: [Date, Date] | undefined, _focus: [string?, string?], _time: [string?, string?]) => true }, setup(_, ctx) { - const [date, setDate] = useState<[Date, Date]>([null, null]) - const [focus, setFocus] = useState<[string, string]>(['0', '2']) - const [time, setTime] = useState<[string, string]>(['0', null]) + const [date, setDate] = useState<[Date, Date]>() + const [focus, setFocus] = useState<[string?, string?]>(['0', '2']) + const [time, setTime] = useState<[string?, string?]>(['0',]) return () => (

diff --git a/src/pages/app/components/DataManage/ClearPanel/index.tsx b/src/pages/app/components/DataManage/ClearPanel/index.tsx index 27b20442d..f015c3900 100644 --- a/src/pages/app/components/DataManage/ClearPanel/index.tsx +++ b/src/pages/app/components/DataManage/ClearPanel/index.tsx @@ -15,16 +15,16 @@ import { alertProps } from "../common" import ClearFilter from "./ClearFilter" type FilterOption = { - date: [Date, Date] - focus: [string, string] - time: [string, string] + date: [Date, Date] | undefined + focus: [string?, string?] + time: [string?, string?] } -function generateParamAndSelect(option: FilterOption): Promise | undefined { +async function generateParamAndSelect(option: FilterOption): Promise { const param = checkParam(option) if (!param) { ElMessage.warning(t(msg => msg.dataManage.paramError)) - return + return [] } const { date } = option @@ -33,8 +33,8 @@ function generateParamAndSelect(option: FilterOption): Promise // default end time is the yesterday dateEnd = new Date(new Date().getTime() - MILL_PER_DAY) } - param.date = [dateStart, dateEnd] - return statService.selectBase(param) + param.date = dateStart ? [dateStart, dateEnd] : undefined + return await statService.selectBase(param) } /** @@ -44,7 +44,7 @@ function generateParamAndSelect(option: FilterOption): Promise * @param mustInteger must be integer? * @returns true when has error, or false */ -function assertQueryParam(range: Vector<2>, mustInteger?: boolean): boolean { +function assertQueryParam(range: [number, number?], mustInteger?: boolean): boolean { const reg = mustInteger ? /^[0-9]+$/ : /^[0-9]+.?[0-9]*$/ const [start, end] = range || [] const noStart = start !== undefined && start !== null @@ -54,13 +54,13 @@ function assertQueryParam(range: Vector<2>, mustInteger?: boolean): boolean { || (noStart && noEnd && start > end) } -const str2Num = (str: string, defaultVal?: number) => (str && str !== '') ? parseInt(str) : defaultVal +const str2Num = (str: string | undefined) => str ? parseInt(str) : undefined const seconds2Milliseconds = (a: number) => a * MILL_PER_SECOND function checkParam(option: FilterOption): StatCondition | undefined { - const { focus = [null, null], time = [null, null] } = option || {} + const { focus, time } = option let hasError = false - const focusRange = str2Range(focus, seconds2Milliseconds) + const focusRange = str2Range(focus, seconds2Milliseconds) as [number, number] hasError = hasError || assertQueryParam(focusRange) const timeRange = str2Range(time) hasError = hasError || assertQueryParam(timeRange, true) @@ -73,10 +73,10 @@ function checkParam(option: FilterOption): StatCondition | undefined { return condition } -function str2Range(startAndEnd: [string, string], numAmplifier?: (origin: number) => number): Vector<2> { +function str2Range(startAndEnd: [string?, string?], numAmplifier?: (origin: number) => number): [number, number | undefined] { const startStr = startAndEnd[0] const endStr = startAndEnd[1] - let start = str2Num(startStr, 0) + let start = str2Num(startStr) ?? 0 numAmplifier && (start = numAmplifier(start)) let end = str2Num(endStr) end && numAmplifier && (end = numAmplifier(end)) @@ -89,7 +89,7 @@ const _default = defineComponent({ }, setup(_, ctx) { async function handleClick(option: FilterOption) { - const result: timer.core.Row[] = await generateParamAndSelect(option) + const result = await generateParamAndSelect(option) const count = result.length const confirmMsg = t(msg => msg.dataManage.deleteConfirm, { count }) diff --git a/src/pages/app/components/DataManage/Migration/ImportButton.tsx b/src/pages/app/components/DataManage/Migration/ImportButton.tsx index 2a682ab54..f697eafad 100644 --- a/src/pages/app/components/DataManage/Migration/ImportButton.tsx +++ b/src/pages/app/components/DataManage/Migration/ImportButton.tsx @@ -14,9 +14,9 @@ import { defineComponent, ref } from "vue" const immigration: Immigration = new Immigration() -async function handleFileSelected(fileInput: HTMLInputElement, callback: () => void) { - const files: FileList | null = fileInput?.files - if (!files || !files.length) { +async function handleFileSelected(fileInput: HTMLInputElement | undefined, callback: () => void) { + const files = fileInput?.files + if (!files?.length) { return } const loading = ElLoading.service({ fullscreen: true }) diff --git a/src/pages/app/components/DataManage/Migration/ImportOtherButton/Sop.tsx b/src/pages/app/components/DataManage/Migration/ImportOtherButton/Sop.tsx index ed74ba0b1..baebb13bc 100644 --- a/src/pages/app/components/DataManage/Migration/ImportOtherButton/Sop.tsx +++ b/src/pages/app/components/DataManage/Migration/ImportOtherButton/Sop.tsx @@ -24,9 +24,10 @@ const _default = defineComponent({ const step1 = ref>() const step2 = ref>() - const { data, refresh: handleNext, loading: parsing } = useManualRequest(() => step1.value?.parseData?.(), { + const { data, refresh: handleNext, loading: parsing } = useManualRequest(() => step1.value!.parseData(), { + defaultValue: { rows: [] }, onSuccess: () => step.value = 1, - onError: (e: Error) => ElMessage.error(e?.message ?? 'Unknown Error') + onError: e => ElMessage.error((e as Error)?.message ?? 'Unknown Error') }) const { loading: importing, refresh: doImport } = useManualRequest( @@ -40,7 +41,7 @@ const _default = defineComponent({ ElMessage.success(t(msg => msg.operation.successMsg)) ctx.emit('import') }, - onError: (e) => ElMessage.warning(e), + onError: e => ElMessage.warning((e as Error)?.message ?? 'Unknown error'), } ) diff --git a/src/pages/app/components/DataManage/Migration/ImportOtherButton/Step2.tsx b/src/pages/app/components/DataManage/Migration/ImportOtherButton/Step2.tsx index a48c4c5cf..72fd2052f 100644 --- a/src/pages/app/components/DataManage/Migration/ImportOtherButton/Step2.tsx +++ b/src/pages/app/components/DataManage/Migration/ImportOtherButton/Step2.tsx @@ -15,14 +15,17 @@ import { defineComponent, type PropType } from "vue" const _default = defineComponent({ props: { - data: Object as PropType, + data: { + type: Object as PropType, + required: true, + }, }, setup(props, ctx) { const [resolution, setResolution] = useState() ctx.expose({ parseData: () => resolution.value - } satisfies SopStepInstance) + } satisfies SopStepInstance) return () => ( diff --git a/src/pages/app/components/DataManage/Migration/ImportOtherButton/processor.ts b/src/pages/app/components/DataManage/Migration/ImportOtherButton/processor.ts index 2b9066fc6..20999e772 100644 --- a/src/pages/app/components/DataManage/Migration/ImportOtherButton/processor.ts +++ b/src/pages/app/components/DataManage/Migration/ImportOtherButton/processor.ts @@ -47,13 +47,13 @@ export async function parseFile(ext: OtherExtension, file: File): Promise { const text = await file.text() const lines = text.split('\n').map(line => line.trim()).filter(line => !!line).splice(1) - const rows: timer.imported.Row[] = lines.map(line => { + const rows = lines.map(line => { const [host, date, seconds] = line.split(',').map(cell => cell.trim()) !host || !date || (!seconds && seconds !== '0') && throwError() const [year, month, day] = date.split('/') !year || !month || !day && throwError() const realDate = `${year}${month.length == 2 ? month : '0' + month}${day.length == 2 ? day : '0' + day}` - return { host, date: realDate, focus: parseInt(seconds) * MILL_PER_SECOND } + return { host, date: realDate, focus: parseInt(seconds) * MILL_PER_SECOND, time: 0 } satisfies timer.imported.Row }) return rows } @@ -73,7 +73,7 @@ type WebtimeTrackerBackup = { } const WEBTIME_TRACKER_DATE_REG = /(\d{2})-(\d{2})-\d{2}/ -const cvtWebtimeTrackerDate = (date: string): string => WEBTIME_TRACKER_DATE_REG.test(date) ? date.split('-').join('') : undefined +const cvtWebtimeTrackerDate = (date: string): string | undefined => WEBTIME_TRACKER_DATE_REG.test(date) ? date.split('-').join('') : undefined async function parseWebtimeTracker(file: File): Promise { const text = await file.text() @@ -104,7 +104,7 @@ async function parseWebtimeTracker(file: File): Promise { for (let i = 1; i < colHeaders?.length; i++) { const seconds = Number.parseInt(cells[i]) const date = cvtWebtimeTrackerDate(colHeaders[i]) - seconds && date && rows.push({ host, date, focus: seconds * MILL_PER_SECOND }) + seconds && date && rows.push({ host, date, focus: seconds * MILL_PER_SECOND, time: 0 }) } }) return rows @@ -150,7 +150,7 @@ async function parseHistoryTrendsUnlimited(file: File): Promise { const date = dateAndHost.substring(0, 8) const host = dateAndHost.substring(8) - return { date, host, time } + return { date, host, time, focus: 0 } satisfies timer.imported.Row }) } throw new Error("Invalid file format") diff --git a/src/pages/app/components/Habit/components/HabitFilter.tsx b/src/pages/app/components/Habit/components/HabitFilter.tsx index b7d3415d9..225be1547 100644 --- a/src/pages/app/components/Habit/components/HabitFilter.tsx +++ b/src/pages/app/components/Habit/components/HabitFilter.tsx @@ -34,14 +34,17 @@ const SHORTCUTS: ElementDatePickerShortcut[] = shortcutProps.map(([text, agoOfSt const _default = defineComponent({ props: { - defaultValue: Object as PropType, + defaultValue: { + type: Object as PropType, + required: true, + }, }, emits: { change: (_option: FilterOption) => true }, setup(props, ctx) { - const [dateRange, setDateRange] = useState<[Date, Date]>(props.defaultValue?.dateRange || [null, null]) - const [timeFormat, setTimeFormat] = useState(props.defaultValue?.timeFormat) + const [dateRange, setDateRange] = useState(props.defaultValue.dateRange) + const [timeFormat, setTimeFormat] = useState(props.defaultValue.timeFormat) watch([dateRange, timeFormat], () => ctx.emit("change", { dateRange: dateRange.value, @@ -54,7 +57,7 @@ const _default = defineComponent({ clearable={false} defaultRange={dateRange.value} shortcuts={SHORTCUTS} - onChange={setDateRange} + onChange={val => val && setDateRange(val)} /> { const milliseconds = row.milliseconds diff --git a/src/pages/app/components/Habit/components/Period/Filter.tsx b/src/pages/app/components/Habit/components/Period/Filter.tsx index 56f2a0a99..14b30ebe0 100644 --- a/src/pages/app/components/Habit/components/Period/Filter.tsx +++ b/src/pages/app/components/Habit/components/Period/Filter.tsx @@ -17,7 +17,7 @@ import { type ChartType, type FilterOption } from './common' type _SizeOption = [number, keyof HabitMessage['period']['sizes']] function allOptions(): Record { - const allOptions = {} + const allOptions: Record = {} const allSizes: _SizeOption[] = [ [1, 'fifteen'], [2, 'halfHour'], @@ -38,14 +38,17 @@ const CHART_CONFIG: { [type in ChartType]: string } = { const _default = defineComponent({ props: { - defaultValue: Object as PropType, + defaultValue: { + type: Object as PropType, + required: true, + }, }, emits: { change: (_newVal: FilterOption) => true, }, setup(prop, ctx) { - const periodSize = ref(prop.defaultValue?.periodSize || 1) - const { data: chartType, setter: setChartType } = useCached('habit-period-chart-type', prop.defaultValue?.chartType) + const periodSize = ref(prop.defaultValue.periodSize) + const { data: chartType, setter: setChartType } = useCached('habit-period-chart-type', prop.defaultValue.chartType) watch([periodSize, chartType], () => ctx.emit('change', { periodSize: periodSize.value, @@ -58,7 +61,8 @@ const _default = defineComponent({ historyName='periodSize' defaultValue={periodSize.value?.toString?.()} options={allOptions()} - onSelect={(val: string) => { + onSelect={val => { + if (!val) return const newPeriodSize = parseInt(val) if (isNaN(newPeriodSize)) return periodSize.value = newPeriodSize diff --git a/src/pages/app/components/Habit/components/Period/Summary.tsx b/src/pages/app/components/Habit/components/Period/Summary.tsx index bfa306660..bff5b4258 100644 --- a/src/pages/app/components/Habit/components/Period/Summary.tsx +++ b/src/pages/app/components/Habit/components/Period/Summary.tsx @@ -28,17 +28,18 @@ const computeSummary = (rows: timer.period.Row[], periodSize: number): Result => favoritePeriod = `${formatTime(start, "{h}:{i}")}-${formatTime(end, "{h}:{i}")}` } - let maxIdle: [timer.period.Row, timer.period.Row, number] = [, , 0] + let maxIdle: [timer.period.Row | undefined, timer.period.Row | undefined, number] = [, , 0] - let idleStart: timer.period.Row = null, idleEnd: timer.period.Row = null + let idleStart: timer.period.Row | undefined + let idleEnd: timer.period.Row | undefined rows.forEach(r => { if (r.milliseconds) { - if (!idleStart) return + if (!idleStart || !idleEnd) return const newEmptyTs = idleEnd.endTime.getTime() - idleStart.endTime.getTime() if (newEmptyTs > maxIdle[2]) { maxIdle = [idleStart, idleEnd, newEmptyTs] } - idleStart = idleEnd = null + idleStart = idleEnd = undefined } else { idleEnd = r !idleStart && (idleStart = idleEnd) @@ -48,7 +49,7 @@ const computeSummary = (rows: timer.period.Row[], periodSize: number): Result => const [start, end] = maxIdle let idleLength = '-' - let idlePeriod = null + let idlePeriod = '' if (start && end) { idleLength = periodFormatter(end.endTime.getTime() - start.startTime.getTime(), { format: 'hour' }) const format = t(msg => msg.calendar.simpleTimeFormat) diff --git a/src/pages/app/components/Habit/components/Period/Trend/Wrapper.ts b/src/pages/app/components/Habit/components/Period/Trend/Wrapper.ts index e4bf5409d..6746a279b 100644 --- a/src/pages/app/components/Habit/components/Period/Trend/Wrapper.ts +++ b/src/pages/app/components/Habit/components/Period/Trend/Wrapper.ts @@ -15,6 +15,7 @@ import { type GridComponentOption, type TooltipComponentOption, } from "echarts" +import type { TopLevelFormatterParams } from "echarts/types/dist/shared" import { formatXAxisTime, generateGridOption } from "../common" type EcOption = ComposeOption< @@ -29,12 +30,12 @@ export type BizOption = { } -function formatTimeOfEcharts(params: any, timeFormat: timer.app.TimeFormat): string { +function formatTimeOfEcharts(params: TopLevelFormatterParams, timeFormat: timer.app.TimeFormat): string { const format = Array.isArray(params) ? params[0] : params - const { value } = format - const milliseconds = value[1] ?? 0 - const start = formatTime(value[2], '{m}-{d} {h}:{i}') - const end = formatTime(value[3], '{h}:{i}') + const value = format.value as number[] + const milliseconds = value?.[1] ?? 0 + const start = formatTime(value?.[2], '{m}-{d} {h}:{i}') + const end = formatTime(value?.[3], '{h}:{i}') return `
${start}-${end}
@@ -45,7 +46,7 @@ function formatTimeOfEcharts(params: any, timeFormat: timer.app.TimeFormat): str ` } -type BarItem = BarSeriesOption["data"][number] +type BarItem = Exclude[number] const cvt2Item = (row: timer.period.Row): BarItem => { const startTime = row.startTime.getTime() @@ -63,8 +64,8 @@ function generateOption({ data, timeFormat }: BizOption): EcOption { return { tooltip: { - formatter: (params: any) => formatTimeOfEcharts(params, timeFormat), - borderColor: null, + formatter: (params: TopLevelFormatterParams) => formatTimeOfEcharts(params, timeFormat), + borderColor: undefined, }, grid: generateGridOption(), xAxis: { @@ -72,8 +73,8 @@ function generateOption({ data, timeFormat }: BizOption): EcOption { axisLabel: { formatter: formatXAxisTime, color: textColor }, axisLine: { show: false }, axisTick: { show: false }, - min: seriesData[0]?.[0], - max: seriesData[seriesData.length - 1]?.[0], + min: (seriesData[0] as Exclude)?.[0], + max: (seriesData[seriesData.length - 1] as Exclude)?.[0], }, yAxis: { type: 'value', diff --git a/src/pages/app/components/Habit/components/Period/context.ts b/src/pages/app/components/Habit/components/Period/context.ts index cc93e0f0d..0af0d725f 100644 --- a/src/pages/app/components/Habit/components/Period/context.ts +++ b/src/pages/app/components/Habit/components/Period/context.ts @@ -33,8 +33,8 @@ export const initProvider = ( periodRange: Ref, ) => useProvide(NAMESPACE, { value, filter, periodRange }) -export const usePeriodValue = (): Ref => useProvider(NAMESPACE, "value").value +export const usePeriodValue = () => useProvider(NAMESPACE, "value").value -export const usePeriodFilter = (): Ref => useProvider(NAMESPACE, "filter").filter +export const usePeriodFilter = () => useProvider(NAMESPACE, "filter").filter -export const usePeriodRange = (): Ref => useProvider(NAMESPACE, "periodRange").periodRange \ No newline at end of file +export const usePeriodRange = () => useProvider(NAMESPACE, "periodRange").periodRange \ No newline at end of file diff --git a/src/pages/app/components/Habit/components/Site/DailyTrend/Wrapper.ts b/src/pages/app/components/Habit/components/Site/DailyTrend/Wrapper.ts index 988cf2205..0e5c6dc04 100644 --- a/src/pages/app/components/Habit/components/Site/DailyTrend/Wrapper.ts +++ b/src/pages/app/components/Habit/components/Site/DailyTrend/Wrapper.ts @@ -39,8 +39,8 @@ const TITLE = t(msg => msg.habit.site.trend.title) export type BizOption = { rows: timer.stat.Row[] - dateRange?: [Date, Date] - timeFormat?: timer.app.TimeFormat + dateRange: [Date, Date] + timeFormat: timer.app.TimeFormat } const valueYAxis = (): YAXisOption => ({ @@ -60,7 +60,7 @@ const formatTimeTooltip = (params: TopLevelFormatterParams, format: timer.app.Ti TITLE, ) const valueLines = params?.reverse?.()?.map(param => { - const { value, seriesName } = param + const { value, seriesName = '' } = param const color = LEGEND_COLOR_MAP[seriesName] if (!color) return '' let valueStr: string | number = seriesName === FOCUS_LEGEND @@ -89,14 +89,14 @@ const lineOptionOf = ( } } -const legendOptionOf = (color: LinearGradientObject, name: string): LegendComponentOption['data'][0] => { +const legendOptionOf = (color: LinearGradientObject, name: string): Exclude[number] => { return { name, itemStyle: { color } } } function generateOption(bizOption: BizOption): EcOption { - const { dateRange, rows, timeFormat } = bizOption || {} + const { dateRange, rows, timeFormat } = bizOption - const [start, end] = dateRange || [] + const [start, end] = dateRange const allDates = getAllDatesBetween(start, end) const focusMap = groupBy(rows, r => r.date, l => sum(l.map(e => e.focus))) const visitMap = groupBy(rows, r => r.date, l => sum(l.map(e => e.time))) diff --git a/src/pages/app/components/Habit/components/Site/Distribution/Wrapper.ts b/src/pages/app/components/Habit/components/Site/Distribution/Wrapper.ts index 316ef6513..38fd3482e 100644 --- a/src/pages/app/components/Habit/components/Site/Distribution/Wrapper.ts +++ b/src/pages/app/components/Habit/components/Site/Distribution/Wrapper.ts @@ -12,7 +12,8 @@ import { } from "echarts" import { type GridOption, - type TooltipOption + type TooltipOption, + type TopLevelFormatterParams } from "echarts/types/dist/shared" import { computeAverageLen, generateTitleOption } from "../common" @@ -38,7 +39,7 @@ type FocusBound = [val: number, unit: FocusUnit] const focusBoundMill = ([val, unit]: FocusBound) => (val ?? 0) * (UNIT_CHANGE[unit] ?? 0) -const FOCUS_COUNT_CATEGORIES: Tuple[] = [ +const FOCUS_COUNT_CATEGORIES: Tuple[] = [ [, [5, 's']], [[5, 's'], [20, 's']], [[20, 's'], [60, 's']], @@ -46,10 +47,10 @@ const FOCUS_COUNT_CATEGORIES: Tuple[] = [ [[10, 'm'], [30, 'm']], [[30, 'm'], [60, 'm']], [[1, 'h'], [2, 'h']], - [[2, 'h'], null] + [[2, 'h'], undefined], ] -const VISIT_COUNT_CATEGORIES: Vector<2>[] = [ +const VISIT_COUNT_CATEGORIES: Tuple[] = [ [, 1], [1, 3], [3, 10], @@ -57,27 +58,27 @@ const VISIT_COUNT_CATEGORIES: Vector<2>[] = [ [20, 50], [50, 100], [100, 200], - [200, null] + [200, undefined] ] const PALETTE_COLOR = getSeriesPalette() -const formatFocusLegend = (range: Tuple) => { +const formatFocusLegend = (range: Tuple): string => { const [start, end] = range || [] - if (!start && !end) { - return 'NaN' - } else if (start && !end) { + if (start && !end) { return `>=${start[0]}${start[1]}` } else if (!start && end) { return `<${end[0]}${end[1]}` - } else { + } else if (start && end) { return start[1] === end[1] ? `${start[0]}-${end[0]}${start[1]}` : `${start[0]}${start[1]}-${end[0]}${end[1]}` + } else { + return 'NaN' } } -const formatVisitLegend = (range: Vector<2>) => { +const formatVisitLegend = (range: Tuple) => { const [start, end] = range || [] if (!start && !end) { return 'NaN' @@ -113,8 +114,9 @@ const pieOptionOf = (centerX: string, data: PieSeriesOption['data']): PieSeriesO }, }, tooltip: { - formatter(params: any): string { - const data: PieSeriesOption['data'][number] = params?.data || {} + formatter: (params: TopLevelFormatterParams) => { + const param = Array.isArray(params) ? params[0] : params + const data = param?.data || {} satisfies Exclude[number] const { value } = data as { value: number } return `${t(msg => msg.habit.site.distribution.tooltip, { value })}` } @@ -140,8 +142,8 @@ function generateOption(bizOption: BizOption): EcOption { rows = rows.filter(r => r.date !== exclusiveDate) } - const focusAve = groupBy(rows, r => r.siteKey.host, l => sum(l.map(e => e.focus ?? 0)) / averageLen) - const visitAve = groupBy(rows, r => r.siteKey.host, l => sum(l.map(e => e.time ?? 0)) / averageLen) + const focusAve = groupBy(rows, r => r.siteKey?.host, l => sum(l.map(e => e.focus ?? 0)) / averageLen) + const visitAve = groupBy(rows, r => r.siteKey?.host, l => sum(l.map(e => e.time ?? 0)) / averageLen) const focusGroup = groupBy( Object.entries(focusAve), diff --git a/src/pages/app/components/Habit/components/Site/Summary.tsx b/src/pages/app/components/Habit/components/Site/Summary.tsx index 66aeb255f..88a6f7113 100644 --- a/src/pages/app/components/Habit/components/Site/Summary.tsx +++ b/src/pages/app/components/Habit/components/Site/Summary.tsx @@ -60,14 +60,14 @@ const _default = defineComponent(() => { mainValue={periodFormatter(summary.value?.focus?.total, { format: filter.value?.timeFormat })} subTips={msg => msg.habit.common.focusAverage} subValue={periodFormatter(summary.value?.focus?.average, { format: filter.value?.timeFormat })} - subInfo={summary.value?.exclusiveToday4Average ? t(msg => msg.habit.site.exclusiveToday) : null} + subInfo={summary.value?.exclusiveToday4Average ? t(msg => msg.habit.site.exclusiveToday) : undefined} /> msg.habit.site.countTotal)} mainValue={computeCountText(summary.value?.count)} subTips={msg => msg.habit.site.siteAverage} subValue={summary.value?.count?.siteAverage?.toFixed(0) || '-'} - subInfo={summary.value?.exclusiveToday4Average ? t(msg => msg.habit.site.exclusiveToday) : null} + subInfo={summary.value?.exclusiveToday4Average ? t(msg => msg.habit.site.exclusiveToday) : undefined} />
) diff --git a/src/pages/app/components/Habit/components/Site/TopK/Wrapper.ts b/src/pages/app/components/Habit/components/Site/TopK/Wrapper.ts index e3f370ca7..52fce33cd 100644 --- a/src/pages/app/components/Habit/components/Site/TopK/Wrapper.ts +++ b/src/pages/app/components/Habit/components/Site/TopK/Wrapper.ts @@ -53,10 +53,10 @@ const formatFocusTooltip = (params: TopLevelFormatterParams, format: timer.app.T } function mergeDate(origin: timer.stat.Row[]): timer.stat.Row[] { - const map: Record = {} + const map: Record> = {} origin.forEach(ele => { - const { date, siteKey, cateKey, focus, time } = ele || {} - const key = [date ?? '', identifySiteKey(siteKey), cateKey?.toString?.() ?? ''].join('_') + const { date = '', siteKey, cateKey, focus, time } = ele || {} + const key = [date, identifySiteKey(siteKey), cateKey?.toString?.() ?? ''].join('_') let exist = map[key] if (!exist) { exist = map[key] = { @@ -80,7 +80,7 @@ async function generateOption(rows: timer.stat.Row[] = [], timeFormat: timer.app const merged = mergeDate(rows) const topList = merged.sort((a, b) => b.focus - a.focus).splice(0, TOP_NUM).reverse() const max = topList[topList.length - 1]?.focus ?? 0 - const hosts = topList.map(r => r?.alias || r.siteKey?.host) + const hosts = topList.map(r => r?.alias ?? r.siteKey?.host ?? '').filter(s => !!s) const domW = dom.getBoundingClientRect().width const chartW = domW * (100 - MARGIN_LEFT_P - MARGIN_RIGHT_P) / 100 @@ -146,10 +146,11 @@ async function generateOption(rows: timer.stat.Row[] = [], timeFormat: timer.app ellipsis: '...', minMargin: 5, padding: [0, 4, 0, 0], - formatter: (param: any) => { - const { row } = (param?.data || {}) as SeriesDataItem + formatter: (params: TopLevelFormatterParams) => { + const param = Array.isArray(params) ? params[0] : params + const { row } = (param.data || {}) as SeriesDataItem const { siteKey, alias } = row - return alias || siteKey?.host + return alias ?? siteKey?.host ?? '' }, }, colorBy: 'data', diff --git a/src/pages/app/components/Habit/components/Site/common.ts b/src/pages/app/components/Habit/components/Site/common.ts index 3eeeddc3f..26529f06b 100644 --- a/src/pages/app/components/Habit/components/Site/common.ts +++ b/src/pages/app/components/Habit/components/Site/common.ts @@ -32,9 +32,9 @@ export type SeriesDataItem = { * @param dateRange date range of filter * @returns [averageLen, exclusiveToday4Average, exclusiveDate] */ -export const computeAverageLen = (dateRange: [Date, Date] = [null, null]): [number, boolean, string] => { +export const computeAverageLen = (dateRange: [Date, Date] | undefined): [number, boolean, string | null] => { + if (!dateRange) return [0, false, null] const [start, end] = dateRange - if (!end) return [0, false, null] if (isSameDay(start, end)) return [1, false, null] const dateDiff = getDayLength(start, end) const endIsTody = isSameDay(end, new Date()) diff --git a/src/pages/app/components/Habit/components/Site/context.ts b/src/pages/app/components/Habit/components/Site/context.ts index ea136fcf5..834ff17de 100644 --- a/src/pages/app/components/Habit/components/Site/context.ts +++ b/src/pages/app/components/Habit/components/Site/context.ts @@ -21,6 +21,6 @@ export const initProvider = (rows: Ref) => { useProvide(NAMESPACE, { rows, dateMergedRows }) } -export const useRows = (): Ref => useProvider(NAMESPACE, "rows").rows +export const useRows = (): Ref => useProvider(NAMESPACE, "rows").rows -export const useDateMergedRows = (): Ref => useProvider(NAMESPACE, 'dateMergedRows').dateMergedRows \ No newline at end of file +export const useDateMergedRows = (): Ref => useProvider(NAMESPACE, 'dateMergedRows').dateMergedRows \ No newline at end of file diff --git a/src/pages/app/components/Habit/components/Site/index.tsx b/src/pages/app/components/Habit/components/Site/index.tsx index 7b919884c..7f3f656c7 100644 --- a/src/pages/app/components/Habit/components/Site/index.tsx +++ b/src/pages/app/components/Habit/components/Site/index.tsx @@ -29,7 +29,7 @@ const _default = defineComponent(() => { date: filter.value?.dateRange, } return statService.select(param, true) - }) + }, []) initProvider(rows) const dateRangeLength = computed(() => getDayLength(filter.value?.dateRange?.[0], filter?.value?.dateRange?.[1])) diff --git a/src/pages/app/components/Habit/components/context.ts b/src/pages/app/components/Habit/components/context.ts index c561cd796..79be573ac 100644 --- a/src/pages/app/components/Habit/components/context.ts +++ b/src/pages/app/components/Habit/components/context.ts @@ -17,4 +17,4 @@ const NAMESPACE = 'habit' export const initProvider = (filter: Ref) => useProvide(NAMESPACE, { filter }) -export const useHabitFilter = (): Ref => useProvider(NAMESPACE, "filter").filter \ No newline at end of file +export const useHabitFilter = () => useProvider(NAMESPACE, "filter").filter \ No newline at end of file diff --git a/src/pages/app/components/HelpUs/ProgressList.tsx b/src/pages/app/components/HelpUs/ProgressList.tsx index 5dd15a30a..a35c3823c 100644 --- a/src/pages/app/components/HelpUs/ProgressList.tsx +++ b/src/pages/app/components/HelpUs/ProgressList.tsx @@ -90,7 +90,7 @@ const _default = defineComponent(() => { {list.value?.map?.(({ locale, progress }) => ( {`${progress}%`} - {localeMessages[locale]?.name || locale} + {localeMessages[locale as timer.Locale]?.name || locale} ))}
diff --git a/src/pages/app/components/Limit/LimitFilter.tsx b/src/pages/app/components/Limit/LimitFilter.tsx index d3498a9d4..2db145f0d 100644 --- a/src/pages/app/components/Limit/LimitFilter.tsx +++ b/src/pages/app/components/Limit/LimitFilter.tsx @@ -25,7 +25,10 @@ type BatchOpt = 'delete' | 'enable' | 'disable' const _default = defineComponent({ props: { - defaultValue: Object as PropType + defaultValue: { + type: Object as PropType, + required: true, + } }, emits: { batchDelete: () => true, @@ -36,7 +39,7 @@ const _default = defineComponent({ test: () => true, }, setup(props, ctx) { - const [url, setUrl] = useState(props.defaultValue?.url) + const [url, setUrl] = useState(props.defaultValue.url) const [onlyEnabled, setOnlyEnabled] = useState(props.defaultValue?.onlyEnabled) watch([url, onlyEnabled], () => ctx.emit("change", { url: url.value, onlyEnabled: onlyEnabled.value })) @@ -82,7 +85,7 @@ const _default = defineComponent({ /> - + handleBatchClick(key as BatchOpt)} /> msg.limit.button.test)} type="primary" diff --git a/src/pages/app/components/Limit/LimitModify/Sop/Step1.tsx b/src/pages/app/components/Limit/LimitModify/Sop/Step1.tsx index 1204431a2..9765e5e70 100644 --- a/src/pages/app/components/Limit/LimitModify/Sop/Step1.tsx +++ b/src/pages/app/components/Limit/LimitModify/Sop/Step1.tsx @@ -17,7 +17,7 @@ const _default = defineComponent({ 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)) + watch([enabled, name, weekdays], () => ctx.emit("change", name.value ?? '', !!enabled.value, weekdays.value ?? [])) const validate = () => { const nameVal = name.value?.trim?.() @@ -38,12 +38,15 @@ const _default = defineComponent({ msg.limit.item.name)} required> - setName()} /> + setName(undefined)} + /> msg.limit.item.enabled)} required> - + setEnabled(v as boolean)} /> diff --git a/src/pages/app/components/Limit/LimitModify/Sop/Step2/UrlInput.tsx b/src/pages/app/components/Limit/LimitModify/Sop/Step2/UrlInput.tsx index 9b3a85753..2aa7f9018 100644 --- a/src/pages/app/components/Limit/LimitModify/Sop/Step2/UrlInput.tsx +++ b/src/pages/app/components/Limit/LimitModify/Sop/Step2/UrlInput.tsx @@ -44,7 +44,7 @@ const Part = defineComponent({ ctx.emit('ignoredChange', val)} + onChange={val => ctx.emit('ignoredChange', val as boolean)} /> {ignored.value ? '*' : origin.value} @@ -97,7 +97,7 @@ const _default = defineComponent({ onInput={setInputVal} clearable onClear={resetInputVal} - onKeydown={(e: KeyboardEvent) => e.code === "Enter" && handleParse()} + onKeydown={ev => (ev as KeyboardEvent)?.code === "Enter" && handleParse()} v-slots={{ append: () => ( } onClick={handleParse}> diff --git a/src/pages/app/components/Limit/LimitModify/Sop/Step3/PeriodInput.tsx b/src/pages/app/components/Limit/LimitModify/Sop/Step3/PeriodInput.tsx index 367a26661..35959361a 100644 --- a/src/pages/app/components/Limit/LimitModify/Sop/Step3/PeriodInput.tsx +++ b/src/pages/app/components/Limit/LimitModify/Sop/Step3/PeriodInput.tsx @@ -22,8 +22,7 @@ const BUTTON_STYLE: StyleValue = { } const range2Period = (range: [Date, Date]): [number, number] => { - const [start, end] = range || [] - if (start === undefined || end === undefined) return undefined + const [start, end] = range const startIdx = dateMinute2Idx(start) const endIdx = dateMinute2Idx(end) return [Math.min(startIdx, endIdx), Math.max(startIdx, endIdx)] @@ -70,6 +69,11 @@ const checkImpact = (p1: timer.limit.Period, p2: timer.limit.Period): boolean => return (s1 >= s2 && s1 <= e2) || (s2 >= s1 && s2 <= e1) } +const rangeInitial = (): [Date, Date] => { + const now = new Date() + return [now, new Date(now.getTime() + MILL_PER_HOUR)] +} + const _default = defineComponent({ props: { modelValue: Array as PropType @@ -79,12 +83,11 @@ const _default = defineComponent({ }, setup(props, ctx) { const [editing, openEditing, closeEditing] = useSwitch(false) - const [editingRange, setEditingRange] = useState<[Date, Date]>() + const [editingRange, setEditingRange] = useState(rangeInitial()) const handleEdit = () => { openEditing() - const now = new Date() - setEditingRange([now, new Date(now.getTime() + MILL_PER_HOUR)]) + setEditingRange(rangeInitial()) } const handleSave = () => { diff --git a/src/pages/app/components/Limit/LimitModify/Sop/Step3/TimeInput.tsx b/src/pages/app/components/Limit/LimitModify/Sop/Step3/TimeInput.tsx index 0ba9be74d..70658c93d 100644 --- a/src/pages/app/components/Limit/LimitModify/Sop/Step3/TimeInput.tsx +++ b/src/pages/app/components/Limit/LimitModify/Sop/Step3/TimeInput.tsx @@ -21,9 +21,15 @@ const formatTimeVal = (val: number): string => { const TimeSpinner = defineComponent({ props: { - max: Number, + max: { + type: Number, + required: true, + }, visible: Boolean, - modelValue: Number, + modelValue: { + type: Number, + required: true, + }, }, emits: { change: (_val: number) => true, @@ -68,7 +74,7 @@ const TimeSpinner = defineComponent({ scrollbarEl.addEventListener('scroll', () => { scrolling.value = true const scrollTop = getScrollbarElement()?.scrollTop ?? 0 - const scrollbarH = (scrollbar.value.$el as HTMLUListElement)!.offsetHeight ?? 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) @@ -143,7 +149,10 @@ const useTimeInput = (source: () => number) => { */ const TimeInput = defineComponent({ props: { - modelValue: Number, + modelValue: { + type: Number, + required: true, + }, hourMax: Number, }, emits: { 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 53150e54d..c5d47c51d 100644 --- a/src/pages/app/components/Limit/LimitModify/Sop/Step3/index.tsx +++ b/src/pages/app/components/Limit/LimitModify/Sop/Step3/index.tsx @@ -36,8 +36,8 @@ const _default = defineComponent({ const [weekly, setWeekly] = useShadow(() => props.weekly) const [visitTime, setVisitTime] = useShadow(() => props.visitTime) // Visit count - const [count, setCount] = useShadow(() => props.count ?? 0) - const [weeklyCount, setWeeklyCount] = useShadow(() => props.weeklyCount ?? 0) + const [count, setCount] = useShadow(() => props.count) + const [weeklyCount, setWeeklyCount] = useShadow(() => props.weeklyCount) // Periods const [periods, setPeriods] = useShadow(() => props.periods, []) @@ -71,7 +71,7 @@ const _default = defineComponent({ msg.limit.item.daily)}> - + {t(msg => msg.limit.item.or)} msg.limit.item.weekly)}> - + {t(msg => msg.limit.item.or)} msg.limit.item.visitTime)}> - + msg.limit.item.period)}> diff --git a/src/pages/app/components/Limit/LimitModify/Sop/common.ts b/src/pages/app/components/Limit/LimitModify/Sop/common.ts index b0d2cf60e..d25518aba 100644 --- a/src/pages/app/components/Limit/LimitModify/Sop/common.ts +++ b/src/pages/app/components/Limit/LimitModify/Sop/common.ts @@ -41,9 +41,6 @@ function cleanUrl(url: string): string { } export function parseUrl(url: string): UrlInfo { - if (!url) { - return { protocol: null, parts: null } - } let protocol: Protocol = '*://' url = decodeURI(url)?.trim() diff --git a/src/pages/app/components/Limit/LimitModify/Sop/index.tsx b/src/pages/app/components/Limit/LimitModify/Sop/index.tsx index 796b23688..5793395e3 100644 --- a/src/pages/app/components/Limit/LimitModify/Sop/index.tsx +++ b/src/pages/app/components/Limit/LimitModify/Sop/index.tsx @@ -25,17 +25,18 @@ export type SopInstance = { reset: (rule?: timer.limit.Rule) => void } -const createInitial = (): Required> => ({ - name: null, +const createInitial = (): Required> => ({ + name: '', time: 3600, - weekly: null, + weekly: 0, cond: [], - visitTime: null, - periods: null, + visitTime: 0, + periods: [], enabled: true, weekdays: range(7), - count: null, - weeklyCount: null, + count: 0, + weeklyCount: 0, + allowDelay: false, }) const _default = defineComponent({ @@ -44,21 +45,21 @@ const _default = defineComponent({ }, emits: { cancel: () => true, - save: (_rule: timer.limit.Rule) => true, + save: (_rule: MakeOptional) => true, }, setup(_, ctx) { const [step, setStep] = useState(0) const last = computed(() => step.value === 2) const first = computed(() => step.value === 0) const data = reactive(createInitial()) - const stepInstances: { [step in Step]: Ref } = { + 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[k] = v) + 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) @@ -127,12 +128,12 @@ const _default = defineComponent({ weeklyCount={data.weeklyCount} periods={data.periods} onChange={({ time, visitTime, periods, weekly, count, weeklyCount }) => { - data.time = time - data.visitTime = visitTime - data.periods = periods - data.weekly = weekly - data.count = count - data.weeklyCount = weeklyCount + 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 4f7969f4c..e5a39746f 100644 --- a/src/pages/app/components/Limit/LimitModify/index.tsx +++ b/src/pages/app/components/Limit/LimitModify/index.tsx @@ -29,25 +29,35 @@ const _default = defineComponent({ 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 + let modifyingItem: timer.limit.Rule | undefined = undefined - const handleSave = async (rule: timer.limit.Rule) => { + const handleSave = async (rule: MakeOptional) => { + if (!rule) return const { cond, enabled, name, time, weekly, visitTime, periods, weekdays, count, weeklyCount } = rule - const toSave: timer.limit.Rule = { - ...modifyingItem || {}, - cond, enabled, name, time, weekly, visitTime, weekdays, count, weeklyCount, - // Object to array - periods: periods?.map(i => ([i?.[0], i?.[1]] satisfies Vector)), - } + let saved: timer.limit.Rule if (mode.value === 'modify') { - await limitService.update(toSave) + 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 { - await limitService.create(toSave) + const toCreate = { + ...modifyingItem || {}, + cond, enabled, name, time, weekly, visitTime, weekdays, count, weeklyCount, + // Object to array + periods: periods?.map(i => ([i?.[0], i?.[1]] satisfies Vector)), + } + const id = await limitService.create(toCreate) + saved = { ...toCreate, id } } close() ElMessage.success(t(msg => msg.operation.successMsg)) sop.value?.reset?.() - ctx.emit("save", toSave) + ctx.emit("save", saved) } const instance: ModifyInstance = { diff --git a/src/pages/app/components/Limit/LimitTable/Waste.tsx b/src/pages/app/components/Limit/LimitTable/Waste.tsx index 0213656c8..e4eb81bae 100644 --- a/src/pages/app/components/Limit/LimitTable/Waste.tsx +++ b/src/pages/app/components/Limit/LimitTable/Waste.tsx @@ -9,7 +9,10 @@ import { computed, defineComponent } from "vue" const Waste = defineComponent({ props: { time: Number, - waste: Number, + waste: { + type: Number, + required: true, + }, count: Number, visit: Number, delayCount: Number, diff --git a/src/pages/app/components/Limit/LimitTable/column/LimitDelayColumn.tsx b/src/pages/app/components/Limit/LimitTable/column/LimitDelayColumn.tsx index fb2ddbd86..7b69ed0ff 100644 --- a/src/pages/app/components/Limit/LimitTable/column/LimitDelayColumn.tsx +++ b/src/pages/app/components/Limit/LimitTable/column/LimitDelayColumn.tsx @@ -40,13 +40,7 @@ const _default = defineComponent({ handleChange(row, val) - .then(() => { - row.allowDelay = val - ctx.emit("rowChange", toRaw(row), val) - }) - .catch(console.log) - } + onChange={val => handleChange(row, val as boolean)} /> ), }} diff --git a/src/pages/app/components/Limit/LimitTest.tsx b/src/pages/app/components/Limit/LimitTest.tsx index 15e3d4b58..0e310b0a5 100644 --- a/src/pages/app/components/Limit/LimitTest.tsx +++ b/src/pages/app/components/Limit/LimitTest.tsx @@ -15,7 +15,7 @@ export type TestInstance = { show(): void } -function computeResultTitle(url: string, inputting: boolean, matched: timer.limit.Rule[]): string { +function computeResultTitle(url: string | undefined, inputting: boolean, matched: timer.limit.Rule[]): string { if (!url) { return t(msg => msg.limit.message.inputTestUrl) } @@ -29,7 +29,7 @@ function computeResultTitle(url: string, inputting: boolean, matched: timer.limi } } -function computeResultDesc(url: string, inputting: boolean, matched: timer.limit.Rule[]): string[] { +function computeResultDesc(url: string | undefined, inputting: boolean, matched: timer.limit.Rule[]): string[] { if (!url || inputting || !matched?.length) { return [] } @@ -38,7 +38,7 @@ function computeResultDesc(url: string, inputting: boolean, matched: timer.limit type _ResultType = AlertProps['type'] -function computeResultType(url: string, inputting: boolean, matched: timer.limit.Rule[]): _ResultType { +function computeResultType(url: string | undefined, inputting: boolean, matched: timer.limit.Rule[]): _ResultType { if (!url || inputting) { return 'info' } diff --git a/src/pages/app/components/Limit/context.ts b/src/pages/app/components/Limit/context.ts index e8cb41622..5f74b2b1b 100644 --- a/src/pages/app/components/Limit/context.ts +++ b/src/pages/app/components/Limit/context.ts @@ -15,4 +15,4 @@ export const initProvider = (defaultFilter: LimitFilterOption) => { return { filter, setFilter } } -export const useLimitFilter = (): Ref => useProvider(NAMESPACE, "filter").filter +export const useLimitFilter = (): Ref => useProvider(NAMESPACE, "filter").filter diff --git a/src/pages/app/components/Limit/index.tsx b/src/pages/app/components/Limit/index.tsx index 3cd40c508..40e6eb471 100644 --- a/src/pages/app/components/Limit/index.tsx +++ b/src/pages/app/components/Limit/index.tsx @@ -35,7 +35,7 @@ const _default = defineComponent(() => { const selectedAndThen = (then: (list: timer.limit.Item[]) => void): void => { const list = table.value?.getSelected?.() - if (!list.length) { + if (!list?.length) { ElMessage.info('No limit rule selected') return } diff --git a/src/pages/app/components/Limit/types.d.ts b/src/pages/app/components/Limit/types.d.ts index bc021b30f..9e00296ce 100644 --- a/src/pages/app/components/Limit/types.d.ts +++ b/src/pages/app/components/Limit/types.d.ts @@ -1,4 +1,4 @@ export type LimitFilterOption = { - url: string + url: string | undefined onlyEnabled: boolean } diff --git a/src/pages/app/components/Option/Select.tsx b/src/pages/app/components/Option/Select.tsx index 951d2b68a..b39d68c05 100644 --- a/src/pages/app/components/Option/Select.tsx +++ b/src/pages/app/components/Option/Select.tsx @@ -15,31 +15,25 @@ const _default = defineComponent(() => { const slots = useSlots() return () => ( - ( - tab.value = val} - > - {Object.keys(slots) - .filter(key => !IGNORED_CATE.includes(key as OptionCategory) && key !== 'default') - .map((cate: OptionCategory) => ( - - )) - } - - ), - default: () => ( - - {h(slots[tab.value])} - - ) - }} - /> + ( + tab.value = val} + > + {Object.keys(slots) + .filter(key => !IGNORED_CATE.includes(key as OptionCategory) && key !== 'default') + .map(cate => ( + + )) + } + + ), + default: () => { + const slot = slots[tab.value] + return !!slot && {h(slot)} + } + }} /> ) }) diff --git a/src/pages/app/components/Option/Tabs.tsx b/src/pages/app/components/Option/Tabs.tsx index 92673c92f..1cd8e23d7 100644 --- a/src/pages/app/components/Option/Tabs.tsx +++ b/src/pages/app/components/Option/Tabs.tsx @@ -1,6 +1,6 @@ import { t } from "@app/locale" import { Refresh } from "@element-plus/icons-vue" -import { ElIcon, ElMessage, ElTabPane, ElTabs } from "element-plus" +import { ElIcon, ElMessage, ElTabPane, ElTabs, TabPaneName } from "element-plus" import { defineComponent, h, ref, useSlots } from "vue" import { useRouter } from "vue-router" import ContentContainer from "../common/ContentContainer" @@ -16,7 +16,7 @@ const _default = defineComponent({ const tab = ref(parseQuery() || 'appearance') const router = useRouter() - const handleBeforeLeave = async (activeName: string, oldActiveName: string): Promise => { + const handleBeforeLeave = async (activeName: TabPaneName, oldActiveName: TabPaneName): Promise => { if (activeName === resetButtonName) { const cate: OptionCategory = oldActiveName as OptionCategory await new Promise(res => ctx.emit('reset', cate, res)) @@ -36,8 +36,8 @@ const _default = defineComponent({ class="option-tab" > {Object.entries(useSlots()).filter(([key]) => key !== 'default').map(([key, slot]) => ( - - {h(slot)} + + {!!slot && h(slot)} ))} = {} query[PARAM] = cate router.replace({ query }) } diff --git a/src/pages/app/components/Option/components/AccessibilityOption.tsx b/src/pages/app/components/Option/components/AccessibilityOption.tsx index b2503493a..fc834e01b 100644 --- a/src/pages/app/components/Option/components/AccessibilityOption.tsx +++ b/src/pages/app/components/Option/components/AccessibilityOption.tsx @@ -23,7 +23,7 @@ const _default = defineComponent((_, ctx) => { v-slots={{ default: () => option.chartDecal = val} + onChange={val => option.chartDecal = val as boolean} /> }} /> diff --git a/src/pages/app/components/Option/components/AppearanceOption/DarkModeInput.tsx b/src/pages/app/components/Option/components/AppearanceOption/DarkModeInput.tsx index d643ef967..db085ebb1 100644 --- a/src/pages/app/components/Option/components/AppearanceOption/DarkModeInput.tsx +++ b/src/pages/app/components/Option/components/AppearanceOption/DarkModeInput.tsx @@ -5,9 +5,8 @@ * https://opensource.org/licenses/MIT */ import { t } from "@app/locale" -import { useShadow } from "@hooks" import { ElOption, ElSelect, ElTimePicker } from "element-plus" -import { computed, defineComponent, watch, type PropType } from "vue" +import { computed, defineComponent, toRef, watch, type PropType } from "vue" const ALL_MODES: timer.option.DarkMode[] = ["default", "on", "off", "timed"] @@ -32,24 +31,27 @@ function computeDateToSecond(date: Date) { const _default = defineComponent({ props: { - modelValue: String as PropType, + modelValue: { + type: String as PropType, + required: true, + }, startSecond: Number, endSecond: Number }, emits: { - change: (_darkMode: timer.option.DarkMode, [_startSecond, _endSecond]: [number, number]) => true + change: (_darkMode: timer.option.DarkMode, [_startSecond, _endSecond]: [number?, number?]) => true }, setup(props, ctx) { - const [darkMode, setDarkMode] = useShadow(() => props.modelValue) - const [startSecond, setStartSecond] = useShadow(() => props.startSecond) - const [endSecond, setEndSecond] = useShadow(() => props.endSecond) + const darkMode = toRef(props, 'modelValue') + const startSecond = toRef(props, 'startSecond') + const endSecond = toRef(props, 'endSecond') const start = computed({ - get: () => computeSecondToDate(startSecond.value), - set: val => setStartSecond(computeDateToSecond(val)), + get: () => startSecond.value && computeSecondToDate(startSecond.value), + set: val => startSecond.value = val && computeDateToSecond(val), }) const end = computed({ - get: () => computeSecondToDate(endSecond.value), - set: val => setEndSecond(computeDateToSecond(val)), + get: () => endSecond.value && computeSecondToDate(endSecond.value), + set: val => endSecond.value = val && computeDateToSecond(val), }) watch( @@ -62,7 +64,7 @@ const _default = defineComponent({ modelValue={darkMode.value} size="small" style={{ width: "120px" }} - onChange={setDarkMode} + onChange={val => darkMode.value = val as timer.option.DarkMode} > { ALL_MODES.map(value => msg.option.appearance.darkMode.options[value])} />) diff --git a/src/pages/app/components/Option/components/AppearanceOption/index.tsx b/src/pages/app/components/Option/components/AppearanceOption/index.tsx index b213e76fa..2b8ab1198 100644 --- a/src/pages/app/components/Option/components/AppearanceOption/index.tsx +++ b/src/pages/app/components/Option/components/AppearanceOption/index.tsx @@ -40,7 +40,7 @@ function copy(target: timer.option.AppearanceOption, source: timer.option.Appear const DEFAULT_ANIMA_DURATION = defaultAppearance().chartAnimationDuration const _default = defineComponent((_props, ctx) => { - const { option } = useOption({ + const { option } = useOption({ defaultValue: defaultAppearance, copy, onChange: async val => optionService.isDarkMode(val).then(toggle) }) @@ -117,7 +117,7 @@ const _default = defineComponent((_props, ctx) => { > option.displayWhitelistMenu = val} + onChange={val => option.displayWhitelistMenu = val as boolean} /> { > option.displayBadgeText = val} + onChange={val => option.displayBadgeText = val as boolean} /> { option.badgeBgColor = val} + onChange={val => option.badgeBgColor = val ?? undefined} /> { > option.printInConsole = val} + onChange={val => option.printInConsole = val as boolean} /> } diff --git a/src/pages/app/components/Option/components/BackupOption/AutoInput.tsx b/src/pages/app/components/Option/components/BackupOption/AutoInput.tsx index ed94f632a..2fa3c511b 100644 --- a/src/pages/app/components/Option/components/BackupOption/AutoInput.tsx +++ b/src/pages/app/components/Option/components/BackupOption/AutoInput.tsx @@ -19,14 +19,14 @@ const _default = defineComponent({ interval: Number }, emits: { - change: (_autoBackUp: boolean, _interval: number) => true + change: (_autoBackUp: boolean, _interval: number | undefined) => true }, setup(props, ctx) { const [autoBackUp, setAutoBackUp] = useShadow(() => props.autoBackup) const [interval, setInterval] = useShadow(() => props.interval) - watch([autoBackUp, interval], () => ctx.emit('change', autoBackUp.value, interval.value)) + watch([autoBackUp, interval], () => ctx.emit('change', !!autoBackUp.value, interval.value)) return () => <> - + setAutoBackUp(!!val)} /> {' ' + t(msg => msg.option.backup.auto.label)} {!!autoBackUp.value && <> {localeMessages[locale].comma || ' '} diff --git a/src/pages/app/components/Option/components/BackupOption/Clear/Sop.tsx b/src/pages/app/components/Option/components/BackupOption/Clear/Sop.tsx index 72d404c80..e509e4f3a 100644 --- a/src/pages/app/components/Option/components/BackupOption/Clear/Sop.tsx +++ b/src/pages/app/components/Option/components/BackupOption/Clear/Sop.tsx @@ -18,18 +18,20 @@ const _default = defineComponent({ const { data, refresh: handleNext, loading: readingClient } = useManualRequest(() => step1.value?.parseData?.(), { onSuccess: () => setStep(1), - onError: (e: Error) => ElMessage.warning(e.message || 'Unknown error'), + onError: e => ElMessage.warning((e as Error)?.message || 'Unknown error'), }) const { refresh: handleClear, loading: deleting } = useManualRequest(async () => { - const result = await processor.clear(data.value?.client?.id) + const cid = data.value?.client?.id + if (!cid) throw new Error('Client not selected') + const result = await processor.clear(cid) if (!result.success) throw new Error(result.errorMsg) }, { onSuccess: () => { ElMessage.success(t(msg => msg.operation.successMsg)) ctx.emit('clear') }, - onError: (e: Error) => ElMessage.warning(e.message || 'Unknown error'), + onError: e => ElMessage.warning((e as Error)?.message || 'Unknown error'), }) ctx.expose({ init: () => setStep(0) } satisfies SopInstance) diff --git a/src/pages/app/components/Option/components/BackupOption/Clear/Step1.tsx b/src/pages/app/components/Option/components/BackupOption/Clear/Step1.tsx index a6ce894ae..1961c7d1d 100644 --- a/src/pages/app/components/Option/components/BackupOption/Clear/Step1.tsx +++ b/src/pages/app/components/Option/components/BackupOption/Clear/Step1.tsx @@ -9,7 +9,7 @@ import { type SopStepInstance } from "@app/components/common/DialogSop" import { t } from "@app/locale" import { useState } from "@hooks" import processor from "@service/backup/processor" -import { BIRTHDAY, parseTime } from "@util/time" +import { BIRTHDAY, getBirthday, parseTime } from "@util/time" import { defineComponent } from "vue" import ClientTable from "../ClientTable" @@ -21,8 +21,8 @@ export type StatResult = { async function fetchStatResult(client: timer.backup.Client): Promise { const { id: specCid, maxDate, minDate = BIRTHDAY } = client - const start = parseTime(minDate) - const end = maxDate ? parseTime(maxDate) : new Date() + const start = parseTime(minDate) ?? getBirthday() + const end = parseTime(maxDate) ?? new Date() const remoteRows: timer.core.Row[] = await processor.query({ specCid, start, end }) const siteSet: Set = new Set() remoteRows?.forEach(row => { diff --git a/src/pages/app/components/Option/components/BackupOption/Download/Sop.tsx b/src/pages/app/components/Option/components/BackupOption/Download/Sop.tsx index 1c307449e..353d06c5e 100644 --- a/src/pages/app/components/Option/components/BackupOption/Download/Sop.tsx +++ b/src/pages/app/components/Option/components/BackupOption/Download/Sop.tsx @@ -7,20 +7,19 @@ import DialogSop, { type SopInstance, type SopStepInstance } from "@app/components/common/DialogSop" import { t } from "@app/locale" -import { useManualRequest } from "@hooks/index" -import { useState } from "@hooks/useState" +import { useManualRequest, useState } from "@hooks" import processor from "@service/backup/processor" import { fillExist, processImportedData } from "@service/components/import-processor" -import { BIRTHDAY, parseTime } from "@util/time" +import { getBirthday, parseTime } from "@util/time" import { ElMessage, ElStep, ElSteps } from "element-plus" import { defineComponent, ref } from "vue" import ClientTable from "../ClientTable" import Step2 from "./Step2" async function fetchData(client: timer.backup.Client): Promise { - const { id: specCid, maxDate, minDate = BIRTHDAY } = client - const start = parseTime(minDate) - const end = maxDate ? parseTime(maxDate) : new Date() + const { id: specCid, maxDate, minDate } = client + const start = parseTime(minDate) ?? getBirthday() + const end = parseTime(maxDate) ?? new Date() const remoteRows = await processor.query({ specCid, start, end }) const rows: timer.imported.Row[] = (remoteRows || []).map(rr => ({ date: rr.date, @@ -50,14 +49,15 @@ const _default = defineComponent({ ctx.expose({ init } satisfies SopInstance) const { data, refresh: handleNext, loading: dataFetching } = useManualRequest(() => { - if (step.value !== 0) return + if (step.value !== 0) throw new Error("Data already loaded") const clientVal = client.value if (!clientVal) throw new Error(t(msg => msg.option.backup.clientTable.notSelected)) return fetchData(clientVal) }, { + defaultValue: { rows: [] }, onSuccess: () => step.value = 1, - onError: (e: Error) => ElMessage.error(e.message || 'Unknown error...'), + onError: e => ElMessage.error((e as Error)?.message || 'Unknown error...'), }) const { refresh: handleDownload, loading: downloading } = useManualRequest(async () => { @@ -70,7 +70,7 @@ const _default = defineComponent({ ElMessage.success(t(msg => msg.operation.successMsg)) ctx.emit('download') }, - onError: (e: Error) => ElMessage.error(e.message || 'Unknown error...'), + onError: e => ElMessage.error((e as Error)?.message || 'Unknown error...'), }) return () => ( @@ -96,7 +96,7 @@ const _default = defineComponent({ : }} /> diff --git a/src/pages/app/components/Option/components/BackupOption/Download/Step2.tsx b/src/pages/app/components/Option/components/BackupOption/Download/Step2.tsx index e953524b4..4df667d75 100644 --- a/src/pages/app/components/Option/components/BackupOption/Download/Step2.tsx +++ b/src/pages/app/components/Option/components/BackupOption/Download/Step2.tsx @@ -16,7 +16,10 @@ import { type PropType, defineComponent } from "vue" const _default = defineComponent({ props: { - data: Object as PropType, + data: { + type: Object as PropType, + required: true, + }, clientName: { type: String, required: true, @@ -25,7 +28,7 @@ const _default = defineComponent({ setup(props, ctx) { const [resolution, setResolution] = useState() - ctx.expose({ parseData: () => resolution.value } satisfies SopStepInstance) + ctx.expose({ parseData: () => resolution.value } satisfies SopStepInstance) return () => ( diff --git a/src/pages/app/components/Option/components/BackupOption/Footer.tsx b/src/pages/app/components/Option/components/BackupOption/Footer.tsx index 5d0aaea28..9792f80bd 100644 --- a/src/pages/app/components/Option/components/BackupOption/Footer.tsx +++ b/src/pages/app/components/Option/components/BackupOption/Footer.tsx @@ -7,28 +7,15 @@ import { t } from "@app/locale" import { Operation, UploadFilled } from "@element-plus/icons-vue" -import metaService from "@service/meta-service" +import { useRequest, useState } from "@hooks" import processor from "@service/backup/processor" +import metaService from "@service/meta-service" import { formatTime } from "@util/time" import { ElButton, ElDivider, ElLoading, ElMessage, ElText } from "element-plus" -import { defineComponent, type PropType, type Ref, ref, watch } from "vue" +import { defineComponent, type PropType, toRef } from "vue" import Clear from "./Clear" import Download from "./Download" -async function handleBackup(lastTime: Ref) { - const loading = ElLoading.service({ - text: "Doing backup...." - }) - const result = await processor.syncData() - loading.close() - if (result.success) { - ElMessage.success('Successfully!') - lastTime.value = result.data || Date.now() - } else { - ElMessage.error(result.errorMsg || 'Unknown error') - } -} - async function handleTest() { const loading = ElLoading.service({ text: "Please wait...." }) try { @@ -53,15 +40,28 @@ const _default = defineComponent({ } }, setup(props) { - const lastTime: Ref = ref(undefined) + const type = toRef(props, 'type') - const queryLastTime = async () => { - const backInfo = await metaService.getLastBackUp(props.type) - lastTime.value = backInfo?.ts - } + const [lastTime, setLastTime] = useState() - queryLastTime() - watch(() => props.type, queryLastTime) + useRequest(async () => { + const typeVal = type.value + return typeVal && (await metaService.getLastBackUp(typeVal))?.ts + }, { deps: type, onSuccess: setLastTime }) + + async function handleBackup() { + const loading = ElLoading.service({ + text: "Doing backup...." + }) + const result = await processor.syncData() + loading.close() + if (result.success) { + ElMessage.success('Successfully!') + setLastTime(result.data ?? Date.now()) + } else { + ElMessage.error(result.errorMsg || 'Unknown error') + } + } return () =>
@@ -71,15 +71,14 @@ const _default = defineComponent({ - } - onClick={() => handleBackup(lastTime)} - > + } onClick={handleBackup}> {t(msg => msg.option.backup.operation)} - {t(msg => msg.option.backup.lastTimeTip, { lastTime: formatTime(lastTime.value, TIME_FORMAT) })} + {t( + msg => msg.option.backup.lastTimeTip, + { lastTime: (lastTime.value && formatTime(lastTime.value, TIME_FORMAT)) ?? '' } + )}
diff --git a/src/pages/app/components/Option/components/BackupOption/state.ts b/src/pages/app/components/Option/components/BackupOption/state.ts index e315aea57..b77e492c2 100644 --- a/src/pages/app/components/Option/components/BackupOption/state.ts +++ b/src/pages/app/components/Option/components/BackupOption/state.ts @@ -8,11 +8,11 @@ type Result = { backupType: Ref clientName: Ref autoBackUp: Ref - autoBackUpInterval: Ref - auth: Ref - account: Ref - password: Ref - ext: Ref + autoBackUpInterval: Ref + auth: Ref + account: Ref + password: Ref + ext: Ref setExtField: (field: keyof timer.backup.TypeExt, val: string) => void } @@ -71,7 +71,7 @@ export const useOptionState = (): Result => { } }) - const ext = computed({ + const ext = computed({ get: () => backupExts.value?.[backupType.value], set: val => { const typeVal = backupType.value @@ -99,14 +99,14 @@ export const useOptionState = (): Result => { login.value = newLogin } - const account = computed({ + const account = computed({ get: () => login.value?.[backupType?.value]?.acc, - set: (val: string) => setLoginField('acc', val) + set: (val: string | undefined) => setLoginField('acc', val ?? '') }) - const password = computed({ + const password = computed({ get: () => login.value?.[backupType?.value]?.psw, - set: (val: string) => setLoginField('psw', val) + set: (val: string | undefined) => setLoginField('psw', val ?? '') }) return { diff --git a/src/pages/app/components/Option/components/LimitOption/index.tsx b/src/pages/app/components/Option/components/LimitOption/index.tsx index 85b581b33..2348258b9 100644 --- a/src/pages/app/components/Option/components/LimitOption/index.tsx +++ b/src/pages/app/components/Option/components/LimitOption/index.tsx @@ -40,14 +40,14 @@ function copy(target: timer.option.LimitOption, source: timer.option.LimitOption } function reset(target: timer.option.LimitOption) { - const defaultValue = defaultDailyLimit() + const defaultValue: MakeOptional = defaultDailyLimit() // Not to reset limitPassword delete defaultValue.limitPassword // Not to reset difficulty delete defaultValue.limitVerifyDifficulty // Not to reset notification duration delete defaultValue.limitReminderDuration - Object.entries(defaultValue).forEach(([key, val]) => target[key] = val) + Object.entries(defaultValue).forEach(([key, val]) => (target as any)[key] = val) } const confirm4Strict = async (): Promise => { @@ -61,7 +61,7 @@ const confirm4Strict = async (): Promise => { } const _default = defineComponent((_, ctx) => { - const { option } = useOption({ defaultValue: defaultDailyLimit, copy }) + const { option } = useOption({ defaultValue: defaultDailyLimit, copy }) const { verified, verify } = useVerify(option) const { modifyPsw } = usePswEdit({ reset: () => option.limitPassword }) @@ -102,14 +102,14 @@ const _default = defineComponent((_, ctx) => { default: () => ( option.limitReminder = val} + onChange={val => option.limitReminder = val as boolean} /> ), minInput: () => ( option.limitReminderDuration = val} + onChange={val => val && (option.limitReminderDuration = val)} min={1} max={20} size="small" /> @@ -147,7 +147,7 @@ const _default = defineComponent((_, ctx) => { msg.option.dailyLimit.level.verificationLabel} - defaultValue={t(msg => msg.option.dailyLimit.level[defaultDailyLimit().limitVerifyDifficulty])} + defaultValue={t(msg => (msg.option.dailyLimit.level as any)[defaultDailyLimit().limitVerifyDifficulty])} > string + reset: () => string | undefined } export const usePswEdit = (options: Options) => { @@ -12,7 +12,7 @@ export const usePswEdit = (options: Options) => { const [psw, setPsw] = useState(reset?.()) const [confirmPsw, setConfirmPsw, resetConfirmPsw] = useState('') - const modifyPsw = async (): Promise => { + const modifyPsw = async (): Promise => { setPsw(reset?.()) resetConfirmPsw() diff --git a/src/pages/app/components/Option/components/OptionItem.tsx b/src/pages/app/components/Option/components/OptionItem.tsx index 36ec02976..3c58f7ccf 100644 --- a/src/pages/app/components/Option/components/OptionItem.tsx +++ b/src/pages/app/components/Option/components/OptionItem.tsx @@ -1,6 +1,5 @@ import I18nNode from "@app/components/common/I18nNode" import { type I18nKey } from "@app/locale" -import Flex from "@pages/components/Flex" import { ElDivider, ElTag } from "element-plus" import { defineComponent, h, type PropType, type VNode } from "vue" @@ -20,7 +19,7 @@ const _default = defineComponent({ setup: (props, ctx) => { return () => { const param: Record = {} - Object.entries(ctx.slots).forEach(([k, slot]) => param[k === "default" ? "input" : k] = h(slot)) + Object.entries(ctx.slots).forEach(([k, slot]) => slot && (param[k === "default" ? "input" : k] = h(slot))) return (
diff --git a/src/pages/app/components/Option/components/OptionTag.tsx b/src/pages/app/components/Option/components/OptionTag.tsx index b3a5c3a12..0372af89b 100644 --- a/src/pages/app/components/Option/components/OptionTag.tsx +++ b/src/pages/app/components/Option/components/OptionTag.tsx @@ -1,7 +1,12 @@ -import { defineComponent, h } from "vue" +import { defineComponent, h, type StyleValue, useSlots } from "vue" -const _default = defineComponent((_, ctx) => { - return () => {h(ctx.slots.default)} +const _default = defineComponent(() => { + const { default: default_ } = useSlots() + return () => ( + + {!!default_ && h(default_)} + + ) }) export default _default \ No newline at end of file diff --git a/src/pages/app/components/Option/components/PopupOption.tsx b/src/pages/app/components/Option/components/PopupOption.tsx index 4b6b9abe5..74cb496a3 100644 --- a/src/pages/app/components/Option/components/PopupOption.tsx +++ b/src/pages/app/components/Option/components/PopupOption.tsx @@ -58,7 +58,7 @@ function copy(target: timer.option.PopupOption, source: timer.option.PopupOption } const _default = defineComponent((_props, ctx) => { - const { option } = useOption({ defaultValue: defaultPopup, copy }) + const { option } = useOption({ defaultValue: defaultPopup, copy }) ctx.expose({ reset: () => copy(option, defaultPopup()) @@ -119,7 +119,7 @@ const _default = defineComponent((_props, ctx) => { size="small" min={5} max={100} - onChange={val => option.popupMax = val} + onChange={val => option.popupMax = val!} /> {!IS_ANDROID && ( @@ -132,7 +132,7 @@ const _default = defineComponent((_props, ctx) => { > option.displaySiteName = val} + onChange={val => option.displaySiteName = val as boolean} /> )} diff --git a/src/pages/app/components/Option/components/StatisticsOption.tsx b/src/pages/app/components/Option/components/StatisticsOption.tsx index 5862ada73..2c15dd2fb 100644 --- a/src/pages/app/components/Option/components/StatisticsOption.tsx +++ b/src/pages/app/components/Option/components/StatisticsOption.tsx @@ -90,7 +90,7 @@ const _default = defineComponent((_props, ctx) => { />, default: () => option.autoPauseTracking = val} + onChange={val => option.autoPauseTracking = val as boolean} /> }} /> @@ -102,18 +102,18 @@ const _default = defineComponent((_props, ctx) => { siteNameUsage: () => {t(msg => msg.option.statistics.siteNameUsage)}, default: () => option.collectSiteName = val} + onChange={val => option.collectSiteName = val as boolean} /> }} /> msg.option.statistics.countLocalFiles} - defaultValue={fileAccess.value ? t(msg => msg.option.yes) : null} + defaultValue={fileAccess.value ? t(msg => msg.option.yes) : undefined} v-slots={{ info: () => {t(msg => msg.option.statistics.localFilesInfo)}, localFileTime: () => {t(msg => msg.option.statistics.localFileTime)}, default: () => fileAccess.value - ? option.countLocalFiles = val} /> + ? option.countLocalFiles = val as boolean} /> : { - const paneRefMap: Record> = { + const paneRefMap: Record> = { appearance: ref(), statistics: ref(), popup: ref(), diff --git a/src/pages/app/components/Option/style.sass b/src/pages/app/components/Option/style.sass index 9833496e9..b06c0ffb0 100644 --- a/src/pages/app/components/Option/style.sass +++ b/src/pages/app/components/Option/style.sass @@ -62,8 +62,6 @@ margin-right: 6px .el-color-picker margin-left: 6px - .option-tag - color: #F56C6C .option-default display: flex color: var(--el-text-color-primary) diff --git a/src/pages/app/components/Report/CompositionTable.tsx b/src/pages/app/components/Report/CompositionTable.tsx index 919a26c13..0ff130be3 100644 --- a/src/pages/app/components/Report/CompositionTable.tsx +++ b/src/pages/app/components/Report/CompositionTable.tsx @@ -43,7 +43,10 @@ const formatValue = (value: number, valueFormatter?: ValueFormatter) => { const _default = defineComponent({ props: { - data: Array as PropType, + data: { + type: Array as PropType, + required: true, + }, valueFormatter: Function as PropType, }, setup(props) { @@ -66,7 +69,7 @@ const _default = defineComponent({ r.percent} + formatter={(r: Row) => r.percent ?? ''} width={100} /> diff --git a/src/pages/app/components/Report/ReportFilter/MergeFilterItem.tsx b/src/pages/app/components/Report/ReportFilter/MergeFilterItem.tsx index b6edcc3bf..c44d693b1 100644 --- a/src/pages/app/components/Report/ReportFilter/MergeFilterItem.tsx +++ b/src/pages/app/components/Report/ReportFilter/MergeFilterItem.tsx @@ -2,7 +2,7 @@ import { t } from "@app/locale" import { useCached } from "@hooks" import Flex from "@pages/components/Flex" import { ALL_MERGE_METHODS, processNewMethod } from "@util/merge" -import { ElCheckboxButton, ElCheckboxGroup, ElText } from "element-plus" +import { type CheckboxValueType, ElCheckboxButton, ElCheckboxGroup, ElText } from "element-plus" import { defineComponent, type PropType, watch } from "vue" import "./merge-filter-item.sass" @@ -18,9 +18,9 @@ const MergeFilterItem = defineComponent({ const { data, setter } = useCached('__filter_item_report_merge_method', props.defaultValue, true) watch(data, () => ctx.emit('change', data.value || [])) - const handleChange = (newVal: timer.stat.MergeMethod[]) => { - newVal = processNewMethod(data.value, newVal) - setter?.(newVal) + const handleChange = (val: CheckboxValueType[]) => { + const methods = processNewMethod(data.value, val as timer.stat.MergeMethod[]) + setter?.(methods) } return () => ( diff --git a/src/pages/app/components/Report/ReportFilter/RemoteClient.tsx b/src/pages/app/components/Report/ReportFilter/RemoteClient.tsx index 4056315a9..7a61053ab 100644 --- a/src/pages/app/components/Report/ReportFilter/RemoteClient.tsx +++ b/src/pages/app/components/Report/ReportFilter/RemoteClient.tsx @@ -29,7 +29,7 @@ const _default = defineComponent({ v-show={visible.value} size="small" style={ICON_BTN_STYLE} - type={readRemote.value ? 'primary' : null} + type={readRemote.value ? 'primary' : undefined} onClick={() => readRemote.value = !readRemote.value} > diff --git a/src/pages/app/components/Report/ReportFilter/index.tsx b/src/pages/app/components/Report/ReportFilter/index.tsx index 65ae8db89..90140de6c 100644 --- a/src/pages/app/components/Report/ReportFilter/index.tsx +++ b/src/pages/app/components/Report/ReportFilter/index.tsx @@ -45,12 +45,15 @@ const initMergeMethod = (filter: ReportFilterOption): timer.stat.MergeMethod[] = const res: timer.stat.MergeMethod[] = [] mergeDate && (res.push('date')) siteMerge && (res.push(siteMerge)) - return res.length ? res : undefined + return res } const _default = defineComponent({ props: { - initial: Object as PropType, + initial: { + type: Object as PropType, + required: true, + }, hideCateFilter: Boolean, }, emits: { @@ -59,20 +62,20 @@ const _default = defineComponent({ }, setup(props, ctx) { const { categories } = useCategories() + const { initial } = props - const initial: ReportFilterOption = props.initial - const [host, setHost] = useState(initial?.host) - const [dateRange, setDateRange] = useState(initial?.dateRange) - const [mergeMethod, setMergeMethod] = useState(initMergeMethod(props.initial)) + const [host, setHost] = useState(initial.host) + const [dateRange, setDateRange] = useState(initial.dateRange) + const [mergeMethod, setMergeMethod] = useState(initMergeMethod(initial)) const [cateIds, setCateIds] = useState(initial.cateIds) - const [timeFormat, setTimeFormat] = useState(initial?.timeFormat) + const [timeFormat, setTimeFormat] = useState(initial.timeFormat) // Whether to read remote backup data - const [readRemote, setReadRemote] = useState(initial?.readRemote) + const [readRemote, setReadRemote] = useState(initial.readRemote) const option = computed(() => ({ host: host.value, dateRange: dateRange.value, - mergeDate: mergeMethod.value?.includes?.('date'), + mergeDate: mergeMethod.value.includes('date'), siteMerge: (['domain', 'cate'] satisfies ReportFilterOption['siteMerge'][]) .filter(t => mergeMethod.value?.includes?.(t)) ?.[0], diff --git a/src/pages/app/components/Report/ReportList/Item.tsx b/src/pages/app/components/Report/ReportList/Item.tsx index ba93390dc..2474f0760 100644 --- a/src/pages/app/components/Report/ReportList/Item.tsx +++ b/src/pages/app/components/Report/ReportList/Item.tsx @@ -12,7 +12,10 @@ import TooltipSiteList from "../ReportTable/columns/TooltipSiteList" const _default = defineComponent({ props: { - value: Object as PropType, + value: { + type: Object as PropType, + required: true, + }, }, emits: { selectedChange: (_val: boolean) => true, @@ -22,7 +25,7 @@ const _default = defineComponent({ const filter = useReportFilter() const mergeHost = computed(() => filter.value?.siteMerge === 'domain') const formatter = (focus: number): string => periodFormatter(focus, { format: filter.value?.timeFormat }) - const { siteKey, iconUrl, mergedRows, date, focus, composition, time } = props.value || {} + const { siteKey, iconUrl, mergedRows, date, focus, composition, time } = props.value const selected = ref(false) watch(selected, val => ctx.emit('selectedChange', val)) @@ -41,22 +44,24 @@ const _default = defineComponent({ value={selected.value} onChange={val => selected.value = !!val} /> - , - }} - > - - + {!!siteKey && ( + , + }} + > + + + )}
} diff --git a/src/pages/app/components/Report/ReportTable/columns/CateColumn.tsx b/src/pages/app/components/Report/ReportTable/columns/CateColumn.tsx index 80b6e75ee..4fb7468a9 100644 --- a/src/pages/app/components/Report/ReportTable/columns/CateColumn.tsx +++ b/src/pages/app/components/Report/ReportTable/columns/CateColumn.tsx @@ -10,10 +10,10 @@ import { defineComponent } from "vue" import TooltipSiteList from "./TooltipSiteList" const renderMerged = (cateId: number, categories: timer.site.Cate[], merged: timer.stat.Row[]) => { - let cateName = undefined + let cateName: string let isNotSet = false const siteMap = new SiteMap() - merged?.forEach(({ siteKey, iconUrl }) => siteMap.put(siteKey, iconUrl)) + merged?.forEach(({ siteKey, iconUrl }) => siteKey && siteMap.put(siteKey, iconUrl)) if (cateId === CATE_NOT_SET_ID) { cateName = t(msg => msg.shared.cate.notSet) @@ -43,7 +43,7 @@ const renderMerged = (cateId: number, categories: timer.site.Cate[], merged: tim const CateColumn = defineComponent({ emits: { - change: (_siteKey: timer.site.SiteKey, _newCate: number) => true, + change: (_siteKey: timer.site.SiteKey, _newCate: number | undefined) => true, }, setup(_, ctx) { const { categories } = useCategories() @@ -55,11 +55,11 @@ const CateColumn = defineComponent({ return ( {cateKey - ? renderMerged(cateKey, categories.value, mergedRows) + ? renderMerged(cateKey, categories.value, mergedRows ?? []) : ctx.emit('change', siteKey, newCateId)} + onChange={newCateId => ctx.emit('change', siteKey!, newCateId)} /> } diff --git a/src/pages/app/components/Report/ReportTable/columns/OperationColumn.tsx b/src/pages/app/components/Report/ReportTable/columns/OperationColumn.tsx index 0b6e8a456..ece34dd45 100644 --- a/src/pages/app/components/Report/ReportTable/columns/OperationColumn.tsx +++ b/src/pages/app/components/Report/ReportTable/columns/OperationColumn.tsx @@ -95,9 +95,10 @@ const _default = defineComponent({ buttonType="warning" buttonText={t(msg => msg.item.operation.add2Whitelist)} confirmText={t(msg => msg.whitelist.addConfirmMsg, { url: row.siteKey?.host })} - visible={canOperate.value && !whitelist.value?.includes(row.siteKey?.host)} + visible={canOperate.value && !!row.siteKey?.host && !whitelist.value?.includes(row.siteKey?.host)} onConfirm={async () => { - await whitelistService.add(row.siteKey?.host) + const host = row.siteKey?.host + host && await whitelistService.add(host) refreshWhitelist() ElMessage.success(t(msg => msg.operation.successMsg)) }} @@ -108,9 +109,10 @@ const _default = defineComponent({ buttonType="primary" buttonText={t(msg => msg.item.operation.removeFromWhitelist)} confirmText={t(msg => msg.whitelist.removeConfirmMsg, { url: row.siteKey?.host })} - visible={canOperate.value && whitelist.value?.includes(row.siteKey?.host)} + visible={canOperate.value && !!row.siteKey?.host && whitelist.value?.includes(row.siteKey?.host)} onConfirm={async () => { - await whitelistService.remove(row.siteKey?.host) + const host = row.siteKey?.host + host && await whitelistService.remove(host) refreshWhitelist() ElMessage.success(t(msg => msg.operation.successMsg)) }} diff --git a/src/pages/app/components/Report/ReportTable/columns/TimeColumn.tsx b/src/pages/app/components/Report/ReportTable/columns/TimeColumn.tsx index 30b28893d..dd5b3e2b7 100644 --- a/src/pages/app/components/Report/ReportTable/columns/TimeColumn.tsx +++ b/src/pages/app/components/Report/ReportTable/columns/TimeColumn.tsx @@ -24,7 +24,7 @@ const TimeColumn = defineComponent({ }, setup(props) { const filter = useReportFilter() - const formatter = (focus: number): string => periodFormatter(focus, { format: filter.value?.timeFormat }) + const formatter = (focus: number | undefined): string => periodFormatter(focus, { format: filter.value?.timeFormat }) return () => ( { const siteMap = new SiteMap() - props.modelValue?.forEach(({ siteKey, iconUrl }) => siteMap.put(siteKey, iconUrl)) + props.modelValue?.forEach(({ siteKey, iconUrl }) => siteKey && siteMap.put(siteKey, iconUrl)) return ( diff --git a/src/pages/app/components/Report/ReportTable/index.tsx b/src/pages/app/components/Report/ReportTable/index.tsx index 5cf77cd72..519feac93 100644 --- a/src/pages/app/components/Report/ReportTable/index.tsx +++ b/src/pages/app/components/Report/ReportTable/index.tsx @@ -32,7 +32,7 @@ function computeTimerQueryParam(filterOption: ReportFilterOption, sort: ReportSo return param } -async function handleAliasChange(key: timer.site.SiteKey, newAlias: string, data: timer.stat.Row[]) { +async function handleAliasChange(key: timer.site.SiteKey, newAlias: string | undefined, data: timer.stat.Row[]) { newAlias = newAlias?.trim?.() if (!newAlias) { await siteService.removeAlias(key) @@ -45,7 +45,10 @@ async function handleAliasChange(key: timer.site.SiteKey, newAlias: string, data const _default = defineComponent({ props: { - defaultSort: Object as PropType, + defaultSort: { + type: Object as PropType, + required: true, + }, }, setup(props, ctx) { const [page, setPage] = useState({ size: 10, num: 1 }) @@ -74,7 +77,7 @@ const _default = defineComponent({ () => filterOption.value?.siteMerge, ], () => tableRef.value?.doLayout?.()) - const handleCateChange = (key: timer.site.SiteKey, newCateId: number) => { + const handleCateChange = (key: timer.site.SiteKey, newCateId: number | undefined) => { data.value?.list ?.filter(({ siteKey }) => siteEqual(siteKey, key)) ?.forEach(i => i.cateId = newCateId) @@ -104,7 +107,7 @@ const _default = defineComponent({ v-slots={({ row }: { row: timer.stat.Row }) => ( handleAliasChange(row.siteKey, newAlias, data.value?.list)} + onChange={newAlias => row.siteKey && handleAliasChange(row.siteKey, newAlias, data.value?.list ?? [])} /> )} /> diff --git a/src/pages/app/components/Report/common.ts b/src/pages/app/components/Report/common.ts index bee2a329d..ef2d57ea3 100644 --- a/src/pages/app/components/Report/common.ts +++ b/src/pages/app/components/Report/common.ts @@ -8,7 +8,7 @@ const statDatabase = new StatDatabase(chrome.storage.local) export const cvtOption2Param = ({ host, dateRange, mergeDate, siteMerge, cateIds, readRemote }: ReportFilterOption): StatQueryParam => ({ host: host, - date: [dateRange?.[0], dateRange?.[1]], + date: dateRange, mergeDate, mergeHost: siteMerge === 'domain', mergeCate: siteMerge === 'cate', @@ -26,7 +26,7 @@ function computeSingleConfirmText(url: string, date: string): string { return t(msg => msg.item.operation.deleteConfirmMsg, { url, date }) } -function computeRangeConfirmText(url: string, dateRange: [Date, Date]): string { +function computeRangeConfirmText(url: string, dateRange: [Date, Date] | undefined): string { const hasDateRange = dateRange?.length === 2 && (dateRange[0] || dateRange[1]) if (!hasDateRange) { // Delete all @@ -45,11 +45,11 @@ function computeRangeConfirmText(url: string, dateRange: [Date, Date]): string { export function computeDeleteConfirmMsg(row: timer.stat.Row, filterOption: ReportFilterOption): string { const { siteKey, date } = row || {} - const { host } = siteKey || {} + const host = siteKey?.host ?? 'Unknown Host' const { mergeDate, dateRange } = filterOption || {} return mergeDate ? computeRangeConfirmText(host, dateRange) - : computeSingleConfirmText(host, date) + : computeSingleConfirmText(host, date ?? '') } export async function handleDelete(row: timer.stat.Row, filterOption: ReportFilterOption) { @@ -58,17 +58,17 @@ export async function handleDelete(row: timer.stat.Row, filterOption: ReportFilt const { mergeDate, dateRange } = filterOption || {} if (!mergeDate) { // Delete one day - await statDatabase.deleteByUrlAndDate(host, date) + host && date && await statDatabase.deleteByUrlAndDate(host, date) return } const start = dateRange?.[0] const end = dateRange?.[1] if (!start && !end) { // Delete all - await statDatabase.deleteByUrl(host) + host && await statDatabase.deleteByUrl(host) return } // Delete by range - await statDatabase.deleteByUrlBetween(host, start, end) + host && await statDatabase.deleteByUrlBetween(host, start, end) } \ No newline at end of file diff --git a/src/pages/app/components/Report/context.ts b/src/pages/app/components/Report/context.ts index 1c186cc15..905365b3a 100644 --- a/src/pages/app/components/Report/context.ts +++ b/src/pages/app/components/Report/context.ts @@ -14,4 +14,4 @@ export const initProvider = ( filter }) -export const useReportFilter = (): Ref => useProvider(NAMESPACE, "filter").filter \ No newline at end of file +export const useReportFilter = (): Ref => useProvider(NAMESPACE, "filter").filter \ No newline at end of file diff --git a/src/pages/app/components/Report/file-export.ts b/src/pages/app/components/Report/file-export.ts index 535b7d436..bf5374efa 100644 --- a/src/pages/app/components/Report/file-export.ts +++ b/src/pages/app/components/Report/file-export.ts @@ -43,7 +43,7 @@ function computeFileName(filterParam: ReportFilterOption): string { } const generateJsonData = (rows: timer.stat.Row[], categories: timer.site.Cate[]) => rows.map(row => ({ - host: row.siteKey?.host, + host: row.siteKey?.host ?? '', date: row.date, alias: row.alias, cate: getCateName(row, categories), @@ -53,14 +53,14 @@ const generateJsonData = (rows: timer.stat.Row[], categories: timer.site.Cate[]) const getCateName = (row: timer.stat.Row, categories: timer.site.Cate[]): string => { const cateId = row?.cateId || row?.cateKey - let cate: string + let cate: string = '' if (cateId === CATE_NOT_SET_ID) { cate = t(msg => msg.shared.cate.notSet) } else if (cateId) { const current = categories?.find(c => c.id === cateId) - cate = current?.name + cate = current?.name ?? '' } - return cate ?? '' + return cate } /** @@ -87,7 +87,7 @@ const CSV_COLUMN_CONFIGS: Record = { date: { visible: mergeDate => !mergeDate, i18n: msg => msg.item.date, - formatter: row => row.date, + formatter: row => row.date ?? '', }, host: { visible: (_, siteMerge) => siteMerge !== 'cate', diff --git a/src/pages/app/components/Report/index.tsx b/src/pages/app/components/Report/index.tsx index ff2e7b454..7b2399f28 100644 --- a/src/pages/app/components/Report/index.tsx +++ b/src/pages/app/components/Report/index.tsx @@ -24,7 +24,7 @@ import type { DisplayComponent, ReportFilterOption, ReportQueryParam, ReportSort const statDatabase = new StatDatabase(chrome.storage.local) -async function computeBatchDeleteMsg(selected: timer.stat.Row[], mergeDate: boolean, dateRange: [Date, Date]): Promise { +async function computeBatchDeleteMsg(selected: timer.stat.Row[], mergeDate: boolean, dateRange: [Date, Date] | undefined): Promise { // host => total focus const hostFocus: { [host: string]: number } = groupBy(selected, a => a.siteKey?.host, @@ -40,7 +40,7 @@ async function computeBatchDeleteMsg(selected: timer.stat.Row[], mergeDate: bool ? sum(await Promise.all(Array.from(hosts).map(host => statService.count({ host, fullHost: true, date: dateRange })))) // The count of row : selected?.length || 0 - const i18nParam = { + const i18nParam: Record = { // count count: count2Delete, // example for hosts @@ -53,7 +53,7 @@ async function computeBatchDeleteMsg(selected: timer.stat.Row[], mergeDate: bool date: undefined, } - let key: I18nKey = undefined + let key: I18nKey | undefined = undefined const hasDateRange = dateRange?.length === 2 && (dateRange[0] || dateRange[1]) if (!hasDateRange) { // Delete all @@ -84,7 +84,7 @@ async function handleBatchDelete(displayComp: DisplayComponent, filterOption: Re ElMessage.info(t(msg => msg.report.batchDelete.noSelectedMsg)) return } - const { dateRange, mergeDate } = filterOption || {} + const { dateRange, mergeDate } = filterOption ElMessageBox({ message: await computeBatchDeleteMsg(selected, mergeDate, dateRange), type: "warning", @@ -105,12 +105,13 @@ async function handleBatchDelete(displayComp: DisplayComponent, filterOption: Re }) } -async function deleteBatch(selected: timer.stat.Row[], mergeDate: boolean, dateRange: [Date, Date]) { +async function deleteBatch(selected: timer.stat.Row[], mergeDate: boolean, dateRange: [Date, Date] | undefined) { if (mergeDate) { // Delete according to the date range const start = dateRange?.[0] const end = dateRange?.[1] - await Promise.all(selected.map(d => statDatabase.deleteByUrlBetween(d.siteKey.host, start, end))) + const hosts = selected.map(d => d.siteKey?.host) + await Promise.all(hosts.map(async h => h && await statDatabase.deleteByUrlBetween(h, start, end))) } else { // If not merge date, batch delete await statService.batchDelete(selected) @@ -124,7 +125,7 @@ function initQueryParam(route: RouteLocation, router: Router): [ReportFilterOpti const routeQuery: ReportQueryParam = route.query as unknown as ReportQueryParam const { mh, md, ds, de, sc } = routeQuery const dateStart = ds ? new Date(Number.parseInt(ds)) : undefined - const dateEnd = ds ? new Date(Number.parseInt(de)) : undefined + const dateEnd = de ? new Date(Number.parseInt(de)) : undefined // Remove queries router.replace({ query: {} }) @@ -163,7 +164,7 @@ const _default = defineComponent(() => { handleBatchDelete(displayComp.value, filterOption)} + onBatchDelete={filterOption => displayComp.value && handleBatchDelete(displayComp.value, filterOption)} hideCateFilter={isXs.value} /> ), diff --git a/src/pages/app/components/Report/types.d.ts b/src/pages/app/components/Report/types.d.ts index d94ce0d93..09e6c92ad 100644 --- a/src/pages/app/components/Report/types.d.ts +++ b/src/pages/app/components/Report/types.d.ts @@ -33,8 +33,8 @@ export type ReportQueryParam = { } export type ReportFilterOption = { - host: string - dateRange: [Date, Date] + host: string | undefined + dateRange: [Date, Date] | undefined mergeDate: boolean siteMerge?: timer.stat.MergeMethod & ('cate' | 'domain') cateIds?: number[] diff --git a/src/pages/app/components/RuleMerge/ItemList.tsx b/src/pages/app/components/RuleMerge/ItemList.tsx index 6530bc664..df086e677 100644 --- a/src/pages/app/components/RuleMerge/ItemList.tsx +++ b/src/pages/app/components/RuleMerge/ItemList.tsx @@ -9,7 +9,7 @@ import { t } from "@app/locale" import MergeRuleDatabase from "@db/merge-rule-database" import { useManualRequest, useRequest } from "@hooks" import { ElMessage, ElMessageBox } from "element-plus" -import { defineComponent, type Ref, ref } from "vue" +import { defineComponent, ref } from "vue" import AddButton, { type AddButtonInstance } from './components/AddButton' import Item, { type ItemInstance } from './components/Item' @@ -48,7 +48,7 @@ const _default = defineComponent(() => { update(origin, merged) } - const addButton: Ref = ref() + const addButton = ref() const { refresh: add } = useManualRequest( (rule: timer.merge.Rule) => mergeRuleDatabase.add(rule), @@ -61,7 +61,7 @@ const _default = defineComponent(() => { ) const handleAdd = (origin: string, merged: string | number) => { - const alreadyExist = items.value?.filter(item => item.origin === origin).length > 0 + const alreadyExist = !!items.value?.find(item => item.origin === origin) if (alreadyExist) { ElMessage.warning(t(msg => msg.mergeRule.duplicateMsg, { origin })) return @@ -69,8 +69,7 @@ const _default = defineComponent(() => { const title = t(msg => msg.operation.confirmTitle) const content = t(msg => msg.mergeRule.addConfirmMsg, { origin }) - ElMessageBox.confirm(content, title, { dangerouslyUseHTMLString: true }) - .then(() => add({ origin, merged })) + ElMessageBox.confirm(content, title, { dangerouslyUseHTMLString: true }).then(() => add({ origin, merged })) } return () => ( diff --git a/src/pages/app/components/RuleMerge/components/Item.tsx b/src/pages/app/components/RuleMerge/components/Item.tsx index 5811f5335..3d4d37352 100644 --- a/src/pages/app/components/RuleMerge/components/Item.tsx +++ b/src/pages/app/components/RuleMerge/components/Item.tsx @@ -24,16 +24,22 @@ function computeMergeTxt(mergedVal: number | string,): string { return mergedVal } -function computeMergeType(mergedVal: number | string): TagProps["type"] { +function computeMergeType(mergedVal: number | string): TagProps["type"] | undefined { if (typeof mergedVal === 'number') return 'success' if (!mergedVal) return 'info' - return null + return undefined } const _default = defineComponent({ props: { - origin: String, - merged: [String, Number], + origin: { + type: String, + required: true, + }, + merged: { + type: [String, Number], + required: true, + }, }, emits: { change: (_origin: string, _merged: string | number) => true, diff --git a/src/pages/app/components/RuleMerge/components/ItemInput.tsx b/src/pages/app/components/RuleMerge/components/ItemInput.tsx index 30a65e764..791921c6e 100644 --- a/src/pages/app/components/RuleMerge/components/ItemInput.tsx +++ b/src/pages/app/components/RuleMerge/components/ItemInput.tsx @@ -43,7 +43,7 @@ const _default = defineComponent({ const handleSave = () => { const originVal = origin.value const mergedVal = merged.value - if (isValidHost(originVal)) { + if (originVal && mergedVal && isValidHost(originVal)) { ctx.emit("save", originVal, mergedVal) } else { ElMessage.warning(t(msg => msg.mergeRule.errorOrigin)) @@ -63,7 +63,7 @@ const _default = defineComponent({ modelValue={origin.value} placeholder={t(msg => msg.mergeRule.originPlaceholder)} clearable - onClear={() => setOrigin()} + onClear={() => setOrigin(undefined)} onInput={setOrigin} disabled={origin.value === LOCAL_HOST_PATTERN} /> diff --git a/src/pages/app/components/SiteManage/SiteManageFilter.tsx b/src/pages/app/components/SiteManage/SiteManageFilter.tsx index 8002da50a..c2193f5c6 100644 --- a/src/pages/app/components/SiteManage/SiteManageFilter.tsx +++ b/src/pages/app/components/SiteManage/SiteManageFilter.tsx @@ -98,7 +98,7 @@ const _default = defineComponent({ placeholder={t(msg => msg.siteManage.column.type)} options={ALL_TYPES.map(type => ({ value: type, label: t(msg => msg.siteManage.type[type].name) }))} defaultValue={types.value} - onChange={setTypes} + onChange={val => setTypes(val as timer.site.Type[])} /> - + handleBatchClick(key as BatchOpt)} /> msg.button.create)} icon={} diff --git a/src/pages/app/components/SiteManage/SiteManageModify/HostSelect.tsx b/src/pages/app/components/SiteManage/SiteManageModify/HostSelect.tsx index 6e2bc4ded..50f941b76 100644 --- a/src/pages/app/components/SiteManage/SiteManageModify/HostSelect.tsx +++ b/src/pages/app/components/SiteManage/SiteManageModify/HostSelect.tsx @@ -88,7 +88,7 @@ const _default = defineComponent({ modelValue: Object as PropType }, emits: { - change: (_siteKey: timer.site.SiteKey) => true + change: (_siteKey: timer.site.SiteKey | undefined) => true }, setup(props, ctx) { const { data: options, loading: searching, refresh: searchOption } = useManualRequest(handleRemoteSearch) diff --git a/src/pages/app/components/SiteManage/SiteManageModify/index.tsx b/src/pages/app/components/SiteManage/SiteManageModify/index.tsx index e625ac168..94c6b89e4 100644 --- a/src/pages/app/components/SiteManage/SiteManageModify/index.tsx +++ b/src/pages/app/components/SiteManage/SiteManageModify/index.tsx @@ -22,9 +22,9 @@ type _FormData = { /** * Value of alias key */ - key: timer.site.SiteKey - alias: string - category: number + key: timer.site.SiteKey | undefined + alias: string | undefined + category: number | undefined } const formRule = { @@ -44,7 +44,7 @@ const formRule = { ] } -function validateForm(form: FormInstance): Promise { +function validateForm(form: FormInstance | undefined): Promise { return new Promise((resolve, reject) => { const validate = form?.validate validate @@ -78,7 +78,7 @@ function initData(): _FormData { const _default = defineComponent({ emits: { - save: (_siteKey: timer.site.SiteKey, _name: string) => true + save: (_siteKey: timer.site.SiteKey, _name: string | undefined) => true }, setup: (_, ctx) => { const [visible, open, close] = useSwitch() @@ -100,8 +100,10 @@ const _default = defineComponent({ const valid: boolean = await validateForm(form.value) if (!valid) return false - const siteKey = formData.key - const alias = formData.alias?.trim() + let { key: siteKey, alias } = formData + if (!siteKey) return false + + alias = alias?.trim() const siteInfo: timer.site.SiteInfo = { ...siteKey, alias, cate: formData.category } const saved = await handleAdd(siteInfo) if (saved) { @@ -136,7 +138,7 @@ const _default = defineComponent({ formData.alias = val} - onKeydown={(ev: KeyboardEvent) => ev.key === "Enter" && handleSave()} + onKeydown={ev => (ev as KeyboardEvent).key === "Enter" && handleSave()} /> {showCate.value && ( diff --git a/src/pages/app/components/SiteManage/SiteManageTable/column/AliasColumn.tsx b/src/pages/app/components/SiteManage/SiteManageTable/column/AliasColumn.tsx index 0856135a9..87e319a8a 100644 --- a/src/pages/app/components/SiteManage/SiteManageTable/column/AliasColumn.tsx +++ b/src/pages/app/components/SiteManage/SiteManageTable/column/AliasColumn.tsx @@ -25,7 +25,7 @@ function cvt2Alias(part: string): string { return decoded.charAt(0).toUpperCase() + decoded.slice(1) } -export function genInitialAlias(site: timer.site.SiteInfo): string { +export function genInitialAlias(site: timer.site.SiteInfo): string | undefined { const { host, alias, type } = site || {} if (alias) return if (type !== 'normal') return @@ -44,7 +44,7 @@ const _default = defineComponent({ batchGenerate: () => true, }, setup: (_, ctx) => { - const handleChange = async (newAlias: string, row: timer.site.SiteInfo) => { + const handleChange = async (newAlias: string | undefined, row: timer.site.SiteInfo) => { newAlias = newAlias?.trim?.() row.alias = newAlias if (newAlias) { diff --git a/src/pages/app/components/SiteManage/SiteManageTable/column/TypeColumn.tsx b/src/pages/app/components/SiteManage/SiteManageTable/column/TypeColumn.tsx index 184e601c8..88df419d4 100644 --- a/src/pages/app/components/SiteManage/SiteManageTable/column/TypeColumn.tsx +++ b/src/pages/app/components/SiteManage/SiteManageTable/column/TypeColumn.tsx @@ -3,17 +3,18 @@ import { t } from "@app/locale" import { type ElTableRowScope } from "@pages/element-ui/table" import { ElTableColumn, ElTag, type TagProps } from "element-plus" import { defineComponent } from "vue" +import type { JSX } from "vue/jsx-runtime" import { ALL_TYPES } from "../../common" function computeText({ type }: timer.site.SiteInfo): string { return t(msg => msg.siteManage.type[type].name) } -function computeType({ type }: timer.site.SiteInfo): TagProps["type"] { +function computeType({ type }: timer.site.SiteInfo): TagProps["type"] | undefined { switch (type) { case 'merged': return 'info' case 'virtual': return 'success' - default: return null + default: return undefined } } @@ -29,7 +30,7 @@ const _default = defineComponent(() => { v-slots={{ tooltipContent: () => ALL_TYPES .map(type => `${t(msg => msg.siteManage.type[type].name)} - ${t(msg => msg.siteManage.type[type].info)}`) - .reduce((a, b) => { + .reduce<(string | JSX.Element)[]>((a, b) => { a.length && a.push(
) a.push(b) return a diff --git a/src/pages/app/components/SiteManage/SiteManageTable/index.tsx b/src/pages/app/components/SiteManage/SiteManageTable/index.tsx index ee0d748f0..77ecf6cea 100644 --- a/src/pages/app/components/SiteManage/SiteManageTable/index.tsx +++ b/src/pages/app/components/SiteManage/SiteManageTable/index.tsx @@ -30,7 +30,7 @@ const _default = defineComponent({ setup(props, ctx) { const handleIconError = async (row: timer.site.SiteInfo) => { await siteService.removeIconUrl(row) - row.iconUrl = null + row.iconUrl = undefined } const handleRunChange = async (val: boolean, row: timer.site.SiteInfo) => { @@ -46,11 +46,12 @@ const _default = defineComponent({ return ElMessage.info("No data") } const toSave = new SiteMap() - data.forEach(site => { + const items = await siteService.batchSelect(data) + items.filter(i => !i.alias).forEach(site => { const newAlias = genInitialAlias(site) newAlias && toSave.put(site, newAlias) }) - await siteService.batchSaveAlias(toSave) + await siteService.batchSaveAliasNoRewrite(toSave) ctx.emit('aliasGenerated') ElMessage.success(t(msg => msg.operation.successMsg)) } @@ -115,7 +116,7 @@ const _default = defineComponent({ handleRunChange(val, row)} + onChange={val => handleRunChange(val as boolean, row)} /> )}
diff --git a/src/pages/app/components/SiteManage/common.ts b/src/pages/app/components/SiteManage/common.ts index 2e83506ce..a3cfed2ca 100644 --- a/src/pages/app/components/SiteManage/common.ts +++ b/src/pages/app/components/SiteManage/common.ts @@ -14,7 +14,7 @@ const NONE_FLAG = '_' /** * site key => option value */ -export function cvt2OptionValue(siteKey: timer.site.SiteKey): string { +export function cvt2OptionValue(siteKey: timer.site.SiteKey | undefined): string { if (!siteKey) return '' const { type } = siteKey let flag = NONE_FLAG @@ -55,7 +55,7 @@ export const VIRTUAL_MSG = t(msg => msg.siteManage.type.virtual?.name)?.toLocale */ export function labelOf(siteKey: timer.site.SiteKey, exists?: boolean): string { let { host: label, type } = siteKey || {} - const suffix = [] + const suffix: string[] = [] type === 'merged' && suffix.push(MERGED_MSG) type === 'virtual' && suffix.push(VIRTUAL_MSG) exists && suffix.push(EXIST_MSG) diff --git a/src/pages/app/components/Whitelist/WhitePanel/WhiteInput.tsx b/src/pages/app/components/Whitelist/WhitePanel/WhiteInput.tsx index 03fde73bd..2102566b7 100644 --- a/src/pages/app/components/Whitelist/WhitePanel/WhiteInput.tsx +++ b/src/pages/app/components/Whitelist/WhitePanel/WhiteInput.tsx @@ -42,6 +42,7 @@ const _default = defineComponent({ const handleSubmit = () => { const val = white.value + if (!val) return if (isRemainHost(val) || isValidHost(val) || judgeVirtualFast(val)) { ctx.emit("save", val) } else { @@ -56,7 +57,7 @@ const _default = defineComponent({ onChange={setWhite} placeholder={t(msg => msg.item.host)} clearable - onClear={() => setWhite()} + onClear={() => setWhite(undefined)} filterable remote loading={searching.value} diff --git a/src/pages/app/components/Whitelist/WhitePanel/WhiteItem.tsx b/src/pages/app/components/Whitelist/WhitePanel/WhiteItem.tsx index 3e643f4a0..c3f27fb96 100644 --- a/src/pages/app/components/Whitelist/WhitePanel/WhiteItem.tsx +++ b/src/pages/app/components/Whitelist/WhitePanel/WhiteItem.tsx @@ -22,7 +22,7 @@ const _default = defineComponent({ }, setup(props, ctx) { const [white, , resetWhite] = useShadow(() => props.white) - const isVirtual = computed(() => judgeVirtualFast(white.value)) + const isVirtual = computed(() => !!white.value && judgeVirtualFast(white.value)) const [editing, openEditing, closeEditing] = useSwitch() return () => <> @@ -42,7 +42,7 @@ const _default = defineComponent({ v-show={!editing.value} class="editable-item" closable - onClose={() => ctx.emit("delete", white.value)} + onClose={() => white.value && ctx.emit("delete", white.value)} type={isVirtual.value ? 'warning' : 'primary'} > {white.value} diff --git a/src/pages/app/components/common/ContentContainer.tsx b/src/pages/app/components/common/ContentContainer.tsx index 655776971..7024064c9 100644 --- a/src/pages/app/components/common/ContentContainer.tsx +++ b/src/pages/app/components/common/ContentContainer.tsx @@ -46,7 +46,7 @@ const _default = defineComponent(() => { v-slots={filter} /> )} - {default_ && h(default_)} + {!!default_ && h(default_)} {!default_ && content && }
diff --git a/src/pages/app/components/common/DialogSop.tsx b/src/pages/app/components/common/DialogSop.tsx index ca53bf949..aff040730 100644 --- a/src/pages/app/components/common/DialogSop.tsx +++ b/src/pages/app/components/common/DialogSop.tsx @@ -27,6 +27,8 @@ const DialogSop = defineComponent({ finish: () => true, }, setup(props, ctx) { + const { steps, content } = ctx.slots + return () => (
- {h(ctx.slots.steps)} + {!!steps && h(steps)}
- {h(ctx.slots.content)} + {!!content && h(content)}
diff --git a/src/pages/app/components/common/DropdownButton.tsx b/src/pages/app/components/common/DropdownButton.tsx index 25bd44eda..3d81ee5a0 100644 --- a/src/pages/app/components/common/DropdownButton.tsx +++ b/src/pages/app/components/common/DropdownButton.tsx @@ -1,24 +1,27 @@ import { ArrowDown } from "@element-plus/icons-vue" import Flex from "@pages/components/Flex" import { ElButton, ElDropdown, ElDropdownItem, ElDropdownMenu, ElLink } from "element-plus" -import { computed, defineComponent, type PropType } from "vue" +import { type Component, computed, defineComponent, type PropType, VNode } from "vue" export type DropdownButtonItem = { key: T - icon: unknown + icon: Component | VNode label: string } const DropdownButton = defineComponent({ props: { - items: Array as PropType[]>, + items: { + type: Array as PropType[]>, + required: true, + }, }, emits: { click: (_key: unknown) => true, }, setup(props, ctx) { const trigger = computed(() => props.items?.[0]) - const list = computed(() => props.items?.slice(1)) + const list = computed(() => props.items.slice(1)) return () => !!trigger.value && ( diff --git a/src/pages/app/components/common/Editable.tsx b/src/pages/app/components/common/Editable.tsx index 4793dd4d1..b03aaf196 100644 --- a/src/pages/app/components/common/Editable.tsx +++ b/src/pages/app/components/common/Editable.tsx @@ -20,15 +20,15 @@ const _default = defineComponent({ initialValue: String, }, emits: { - change: (_newVal: string) => true + change: (_newVal: string | undefined) => true }, setup(props, ctx) { const [editing, openEditing, closeEditing] = useSwitch(false) const originVal = toRef(props, 'modelValue') const [inputVal, setInputVal, refreshInputVal] = useShadow(originVal) const input = ref() - const handleEnter = (ev: KeyboardEvent) => { - if (ev.key !== 'Enter') return + const handleEnter = (ev: KeyboardEvent | Event) => { + if ((ev as KeyboardEvent).key !== 'Enter') return closeEditing() ctx.emit("change", inputVal.value) } diff --git a/src/pages/app/components/common/NumberGrow.tsx b/src/pages/app/components/common/NumberGrow.tsx index 87301fd36..4d91d7b36 100644 --- a/src/pages/app/components/common/NumberGrow.tsx +++ b/src/pages/app/components/common/NumberGrow.tsx @@ -11,20 +11,25 @@ import { defineComponent, onMounted, ref, watch, type Ref, type StyleValue } fro const _default = defineComponent({ props: { - value: Number, + value: { + type: Number, + required: true, + }, duration: Number, fontSize: Number, }, setup(props) { - const el: Ref = ref() - const countUp: Ref = ref() + const el: Ref = ref() + const countUp: Ref = ref() const style: StyleValue = { textDecoration: 'underline' } props.fontSize && (style.fontSize = `${props.fontSize}px`) onMounted(() => { - countUp.value = new CountUp(el.value, props.value, { + const countEl = el.value + if (!countEl) return + countUp.value = new CountUp(countEl, props.value, { startVal: 0, duration: props.duration || 1.5, separator: getNumberSeparator(), diff --git a/src/pages/app/components/common/category/CategoryEditable.tsx b/src/pages/app/components/common/category/CategoryEditable.tsx index 90443f0ed..d5a19f69f 100644 --- a/src/pages/app/components/common/category/CategoryEditable.tsx +++ b/src/pages/app/components/common/category/CategoryEditable.tsx @@ -17,7 +17,7 @@ const CategoryEditable = defineComponent({ cateId: Number, }, emits: { - change: (_val: number) => true, + change: (_val: number | undefined) => true, }, setup(props, ctx) { const { categories } = useCategories() @@ -29,7 +29,7 @@ const CategoryEditable = defineComponent({ return categories.value?.find(c => c.id == id) }) - const { refresh: saveSiteCate } = useManualRequest(async (cateId: number | string) => { + const { refresh: saveSiteCate } = useManualRequest(async (cateId: number | string | undefined) => { const realCateId = typeof cateId === 'string' ? parseInt(cateId) : cateId await siteService.saveCate(props.siteKey, realCateId) return realCateId diff --git a/src/pages/app/components/common/category/CategorySelect/OptionItem.tsx b/src/pages/app/components/common/category/CategorySelect/OptionItem.tsx index b10f9abc6..5936262e4 100644 --- a/src/pages/app/components/common/category/CategorySelect/OptionItem.tsx +++ b/src/pages/app/components/common/category/CategorySelect/OptionItem.tsx @@ -11,7 +11,10 @@ import { defineComponent, nextTick, type PropType, ref } from "vue" const OptionItem = defineComponent({ props: { - value: Object as PropType, + value: { + type: Object as PropType, + required: true, + }, }, setup(props) { const { refreshCategories } = useCategories() @@ -86,8 +89,8 @@ const OptionItem = defineComponent({ modelValue={editingName.value} onInput={setEditingName} style={{ maxWidth: '120px' }} - onKeydown={(ev: KeyboardEvent) => { - const { key } = ev + onKeydown={ev => { + const { key } = ev as KeyboardEvent if (key === 'Enter') { onSaveClick() } else if (key === 'Escape') { diff --git a/src/pages/app/components/common/category/CategorySelect/SelectFooter.tsx b/src/pages/app/components/common/category/CategorySelect/SelectFooter.tsx index d795d3a00..843df9b50 100644 --- a/src/pages/app/components/common/category/CategorySelect/SelectFooter.tsx +++ b/src/pages/app/components/common/category/CategorySelect/SelectFooter.tsx @@ -56,8 +56,8 @@ const SelectFooter = defineComponent(() => { size="small" modelValue={name.value} onInput={setName} - onKeydown={(ev: KeyboardEvent) => { - const { key } = ev + onKeydown={ev => { + const { key } = ev as KeyboardEvent if (key === 'Escape') { stopPropagationAfter(ev, closeEditing) } else if (key === 'Enter') { diff --git a/src/pages/app/components/common/category/CategorySelect/index.tsx b/src/pages/app/components/common/category/CategorySelect/index.tsx index 6944ee533..b07241884 100644 --- a/src/pages/app/components/common/category/CategorySelect/index.tsx +++ b/src/pages/app/components/common/category/CategorySelect/index.tsx @@ -17,7 +17,7 @@ const CategorySelect = defineComponent({ }, emits: { visibleChange: (_visible: boolean) => true, - change: (_newVal: number) => true, + change: (_newVal: number | undefined) => true, }, setup(props, ctx) { const { categories } = useCategories() diff --git a/src/pages/app/components/common/filter/DateRangeFilterItem.tsx b/src/pages/app/components/common/filter/DateRangeFilterItem.tsx index 2fdfb805b..6e2172ffe 100644 --- a/src/pages/app/components/common/filter/DateRangeFilterItem.tsx +++ b/src/pages/app/components/common/filter/DateRangeFilterItem.tsx @@ -19,7 +19,10 @@ const clearShortcut = (): ElementDatePickerShortcut => ({ const _default = defineComponent({ props: { - defaultRange: Object as PropType<[Date, Date]>, + defaultRange: { + type: Object as PropType<[Date, Date] | undefined>, + required: true, + }, disabledDate: Function, startPlaceholder: String, endPlaceholder: String, @@ -30,13 +33,13 @@ const _default = defineComponent({ } }, emits: { - change: (_value: [Date, Date]) => true + change: (_value: [Date, Date] | undefined) => true }, setup(props, ctx) { - const handleChange = (newVal: [Date, Date]) => { + const handleChange = (newVal: [Date, Date] | undefined) => { const [start, end] = newVal || [] const isClearChosen = start?.getTime?.() === 0 && end?.getTime?.() === 0 - if (isClearChosen) newVal = null + if (isClearChosen) newVal = undefined ctx.emit("change", dateRange.value = newVal) } const shortcuts = () => { @@ -45,7 +48,8 @@ const _default = defineComponent({ return [...value, clearShortcut()] } - const dateRange: Ref<[Date, Date]> = ref(props.defaultRange || [undefined, undefined]) + const dateRange = ref(props.defaultRange) + return () => ( - + @@ -35,7 +35,7 @@ const InputFilterItem = defineComponent({ width: [Number, String], }, emits: { - search: (_text: string) => true + search: (_text: string | undefined) => true }, setup(props, ctx) { const modelValue = ref(props.defaultValue) @@ -59,7 +59,7 @@ const InputFilterItem = defineComponent({ clearable={!props.enter} onClear={() => ctx.emit('search', modelValue.value = '')} onInput={val => modelValue.value = val.trim()} - onKeydown={(ev: KeyboardEvent) => ev.key === 'Enter' && props.enter && ctx.emit("search", modelValue.value)} + onKeydown={ev => (ev as KeyboardEvent).key === 'Enter' && props.enter && ctx.emit("search", modelValue.value)} onBlur={() => handleBlur()} onFocus={() => setFocused(true)} style={{ width: width.value } satisfies StyleValue} diff --git a/src/pages/app/components/common/filter/MultiSelectFilterItem.tsx b/src/pages/app/components/common/filter/MultiSelectFilterItem.tsx index 372ce785d..138404aea 100644 --- a/src/pages/app/components/common/filter/MultiSelectFilterItem.tsx +++ b/src/pages/app/components/common/filter/MultiSelectFilterItem.tsx @@ -19,9 +19,9 @@ const MultiSelectFilterItem = defineComponent({ change: (_val: (string | number)[]) => true, }, setup(props, ctx) { - const cacheKey = props.historyName ? `__filter_item_multi_select_${useRoute().path}_${props.historyName}` : null + const cacheKey = props.historyName && `__filter_item_multi_select_${useRoute().path}_${props.historyName}` const { data, setter } = useCached(cacheKey, props.defaultValue) - watch(data, () => ctx.emit('change', data.value)) + watch(data, () => ctx.emit('change', data.value ?? [])) return () => ( > }, emits: { - select: (_val: string) => true + select: (_val: string | undefined) => true }, setup(props, ctx) { - const cacheKey = props.historyName ? `__filter_item_select_${useRoute().path}_${props.historyName}` : null + const cacheKey = props.historyName && `__filter_item_select_${useRoute().path}_${props.historyName}` const { data, setter } = useCached(cacheKey, props.defaultValue) watch(data, () => ctx.emit('select', data.value)) return () => ( diff --git a/src/pages/app/components/common/filter/SwitchFilterItem.tsx b/src/pages/app/components/common/filter/SwitchFilterItem.tsx index 88429cbb4..b59a7a9c6 100644 --- a/src/pages/app/components/common/filter/SwitchFilterItem.tsx +++ b/src/pages/app/components/common/filter/SwitchFilterItem.tsx @@ -21,14 +21,14 @@ const _default = defineComponent({ historyName: String, }, setup(props, ctx) { - const cacheKey = props.historyName ? `__filter_item_switch_${useRoute().path}_${props.historyName}` : null + const cacheKey = props.historyName ? `__filter_item_switch_${useRoute().path}_${props.historyName}` : undefined const { data, setter } = useCached(cacheKey, props.defaultValue) - watch(data, () => ctx.emit("change", data.value)) + watch(data, () => ctx.emit("change", !!data.value)) return () => ( {props.label} - + setter(val as boolean)} /> ) } diff --git a/src/pages/app/components/common/filter/TimeFormatFilterItem.tsx b/src/pages/app/components/common/filter/TimeFormatFilterItem.tsx index 20ff06f83..fd02fb487 100644 --- a/src/pages/app/components/common/filter/TimeFormatFilterItem.tsx +++ b/src/pages/app/components/common/filter/TimeFormatFilterItem.tsx @@ -30,7 +30,7 @@ const _default = defineComponent({ historyName="timeFormat" defaultValue={data.value} options={TIME_FORMAT_LABELS} - onSelect={(val: timer.app.TimeFormat) => ctx.emit("change", data.value = val)} + onSelect={val => ctx.emit("change", data.value = val as timer.app.TimeFormat)} /> } }) diff --git a/src/pages/app/components/common/imported/CompareTable.tsx b/src/pages/app/components/common/imported/CompareTable.tsx index b5f65b11d..04ebf5cc3 100644 --- a/src/pages/app/components/common/imported/CompareTable.tsx +++ b/src/pages/app/components/common/imported/CompareTable.tsx @@ -23,7 +23,7 @@ function computeList(sort: SortInfo, originRows: timer.imported.Row[]): timer.im return originRows } const comparator = (a: timer.imported.Row, b: timer.imported.Row): number => { - const av = a[prop], bv = b[prop] + const av = a[prop] ?? 0, bv = b[prop] ?? 0 if (av == bv) return 0 if (order === 'descending') { return av > bv ? -1 : 1 @@ -82,7 +82,10 @@ const renderTime = (data: timer.imported.Data, comparedColName: string): VNode | const _default = defineComponent({ props: { - data: Object as PropType, + data: { + type: Object as PropType, + required: true, + }, comparedColName: { type: String, required: true, diff --git a/src/pages/app/components/common/imported/conflict/index.tsx b/src/pages/app/components/common/imported/conflict/index.tsx index 06b34d406..950d2c0fe 100644 --- a/src/pages/app/components/common/imported/conflict/index.tsx +++ b/src/pages/app/components/common/imported/conflict/index.tsx @@ -18,7 +18,7 @@ const ResolutionRadio = defineComponent({ modelValue: String as PropType, }, emits: { - change: (_val: timer.imported.ConflictResolution) => true, + change: (_val: timer.imported.ConflictResolution | undefined) => true, }, setup(props, ctx) { return () => ( @@ -41,7 +41,7 @@ const ResolutionRadio = defineComponent({ > ctx.emit('change', val)} + onChange={val => ctx.emit('change', val as timer.imported.ConflictResolution | undefined)} > {ALL_RESOLUTIONS.map((resolution: timer.imported.ConflictResolution) => ( diff --git a/src/pages/app/components/common/kanban/Card.tsx b/src/pages/app/components/common/kanban/Card.tsx index 7d4644565..8c4e928ea 100644 --- a/src/pages/app/components/common/kanban/Card.tsx +++ b/src/pages/app/components/common/kanban/Card.tsx @@ -27,7 +27,7 @@ const _default = defineComponent({ {h(filter)} )} - {h(default_, { class: 'kanban-card-body' })} + {!!default_ && h(default_, { class: 'kanban-card-body' })} ) }, diff --git a/src/pages/app/components/common/kanban/IndicatorCell.tsx b/src/pages/app/components/common/kanban/IndicatorCell.tsx index 4ad8ecf4c..b64cf0469 100644 --- a/src/pages/app/components/common/kanban/IndicatorCell.tsx +++ b/src/pages/app/components/common/kanban/IndicatorCell.tsx @@ -12,6 +12,7 @@ import { classNames } from "@pages/util/style" import { range } from "@util/array" import { ElIcon, ElTooltip } from "element-plus" import { defineComponent, type PropType, type VNode } from "vue" +import type { JSX } from "vue/jsx-runtime" import "./indicator-cell.sass" export type SubProps = { @@ -26,7 +27,7 @@ function renderSubVal(valText: string) { return {valText} } -function renderComparisonIcons(ring: RingValue): VNode { +function renderComparisonIcons(ring: RingValue): VNode | null { const [current = 0, last = 0] = ring if (current === last) return null const clz = classNames( @@ -49,13 +50,13 @@ function renderComparisonIcons(ring: RingValue): VNode { return
{icons}
} -function renderSub(props: SubProps): VNode { +function renderSub(props: SubProps): VNode | null { const { subTips, subValue, subInfo, subRing, valueFormatter } = props if (!subTips && !subValue && !subRing) { return null } - const subTipsLine = [] + const subTipsLine: (JSX.Element | string)[] = [] if (subRing) { const ringText = computeRingText(subRing, valueFormatter) if (ringText) { diff --git a/src/pages/app/context.ts b/src/pages/app/context.ts index ee4d951d9..84336ad17 100644 --- a/src/pages/app/context.ts +++ b/src/pages/app/context.ts @@ -10,9 +10,8 @@ type AppContextValue = { const NAMESPACE = '_' export const initAppContext = () => { - const { data: categories, refresh: refreshCategories } = useRequest(() => cateService.listAll()) + const { data: categories, refresh: refreshCategories } = useRequest(() => cateService.listAll(), { defaultValue: [] }) useProvide(NAMESPACE, { categories, refreshCategories }) } -export const useCategories = (): Pick => - useProvider(NAMESPACE, "categories", "refreshCategories") as Pick +export const useCategories = () => useProvider(NAMESPACE, "categories", "refreshCategories") diff --git a/src/pages/app/util/echarts.ts b/src/pages/app/util/echarts.ts index 42838daa4..090c823d4 100644 --- a/src/pages/app/util/echarts.ts +++ b/src/pages/app/util/echarts.ts @@ -17,8 +17,10 @@ const splitVectors = (vectorRange: Tuple, 2>, count: number, grad } export const getStepColors = (count: number, gradientFactor?: number): string[] => { - const p1 = getCssVariable('--echarts-step-color-1') - const p2 = getCssVariable('--echarts-step-color-2') + const p1 = getCssVariable('--echarts-step-color-1') ?? '' + const p2 = getCssVariable('--echarts-step-color-2') ?? '' + if (!p1 || !p2) return [p1, p2].filter(s => !!s) + if (count <= 0) return [] if (count === 1) return [p1] if (count === 2) return [p1, p2] @@ -41,6 +43,7 @@ export const getSeriesPalette = (): string[] => { return range(4) .map(idx => `--echarts-series-color-${idx + 1}`) .map(val => getCssVariable(val)) + .filter(s => !!s) as string[] } const linearGradientColor = (color1: string, color2: string): LinearGradientObject => ({ @@ -61,14 +64,14 @@ export const getLineSeriesPalette = (): Tuple => { ] } -export const getCompareColor = (): [string, string] => { +export const getCompareColor = (): [string?, string?] => { return [ getCssVariable('--echarts-compare-color-1'), getCssVariable('--echarts-compare-color-2'), ] } -export const getDiffColor = (): [incColor: string, decColor: string] => { +export const getDiffColor = (): [incColor?: string, decColor?: string] => { return [ getCssVariable('--echarts-increase-color'), getCssVariable('--echarts-decrease-color'), @@ -98,6 +101,6 @@ export const tooltipSpaceLine = (height?: number): string => { return `
` } -export const getPieBorderColor = (): string => { +export const getPieBorderColor = (): string | undefined => { return getCssVariable('--echarts-pie-border-color') } \ No newline at end of file diff --git a/src/pages/app/util/limit.tsx b/src/pages/app/util/limit.tsx index 3aecf132e..bc8597670 100644 --- a/src/pages/app/util/limit.tsx +++ b/src/pages/app/util/limit.tsx @@ -3,7 +3,6 @@ import I18nNode from "@app/components/common/I18nNode" import { t } from "@app/locale" import { locale } from "@i18n" import { getCssVariable } from "@pages/util/style" -import { type VerificationPair } from "@service/limit-service/verification/common" import verificationProcessor from "@service/limit-service/verification/processor" import { dateMinute2Idx, hasLimited, isEnabledAndEffective } from "@util/limit" import { ElMessage, ElMessageBox, type ElMessageBoxOptions } from "element-plus" @@ -30,7 +29,7 @@ export async function judgeVerificationRequired(item: timer.limit.Item): Promise if (visitTime) { let hitVisit = false try { - hitVisit = await sendMsg2Runtime("askHitVisit", item) + hitVisit = !!await sendMsg2Runtime("askHitVisit", item) } catch (e) { // If error occurs, regarded as not hitting // ignored @@ -45,7 +44,10 @@ const ANSWER_CANVAS_FONT_SIZE = 24 const AnswerCanvas = defineComponent({ props: { - text: String + text: { + type: String, + required: true, + } }, setup: (props => { const dom = ref() @@ -54,10 +56,13 @@ const AnswerCanvas = defineComponent({ onMounted(() => { const ele = dom.value + if (!ele) return const ctx = ele.getContext("2d") const height = Math.floor(ANSWER_CANVAS_FONT_SIZE * 1.3) ele.height = height - const font = getComputedStyle(wrapper.value).font + const wrapperEl = wrapper.value + if (!wrapperEl || !ctx) return + const font = getComputedStyle(wrapperEl).font // Set font to measure width ctx.font = font const { width } = ctx.measureText(text) @@ -65,7 +70,7 @@ const AnswerCanvas = defineComponent({ // Need set font again after width changed ctx.font = font const color = getCssVariable("--el-text-color-primary") - ctx.fillStyle = color + color && (ctx.fillStyle = color) ctx.fillText(text, 0, ANSWER_CANVAS_FONT_SIZE) }) @@ -103,15 +108,15 @@ export function processVerification(option: timer.option.LimitOption, context?: message:
{t(msg => msg.limit.verification.strictTip)}
, }).catch(() => { })) } - let answerValue: string - let messageNode: VNode | string | Element + let answerValue: string | undefined + let messageNode: VNode | string | Element | undefined let incorrectMessage: string if (limitLevel === 'password' && limitPassword) { answerValue = limitPassword messageNode = t(msg => msg.limit.verification.pswInputTip) incorrectMessage = t(msg => msg.limit.verification.incorrectPsw) } else if (limitLevel === 'verification') { - const pair: VerificationPair = verificationProcessor.generate(limitVerifyDifficulty, locale) + const pair = verificationProcessor.generate(limitVerifyDifficulty ?? 'easy', locale) const { prompt, promptParam, answer } = pair || {} answerValue = typeof answer === 'function' ? t(msg => answer(msg.limit.verification)) : answer incorrectMessage = t(msg => msg.limit.verification.incorrectAnswer) @@ -125,7 +130,7 @@ export function processVerification(option: timer.option.LimitOption, context?: param={{ prompt: {promptTxt} }} /> ) - } else { + } else if (answerValue) { messageNode = ( msg.limit.verification.inputTip2} @@ -134,7 +139,7 @@ export function processVerification(option: timer.option.LimitOption, context?: ) } } - if (!messageNode || !answerValue) return null + if (!messageNode || !answerValue) return Promise.resolve() return new Promise(resolve => { ElMessageBox({ diff --git a/src/pages/app/util/time.ts b/src/pages/app/util/time.ts index b8839f2ec..68b47191a 100644 --- a/src/pages/app/util/time.ts +++ b/src/pages/app/util/time.ts @@ -13,7 +13,7 @@ import { formatPeriodCommon, MILL_PER_MINUTE, MILL_PER_SECOND } from "@util/time * * @param date {yyyy}{mm}{dd} */ -export function cvt2LocaleTime(date: string) { +export function cvt2LocaleTime(date: string | undefined): string { if (!date) return '-' const y = date.substring(0, 4) const m = date.substring(4, 6) @@ -40,13 +40,13 @@ const UNIT_MAP: { [unit in Exclude]: string } = * @param timeFormat * @param hideUnit */ -export function periodFormatter(milliseconds: number, option?: PeriodFormatOption): string { +export function periodFormatter(milliseconds: number | undefined | null, option?: PeriodFormatOption): string { let { format = "default", hideUnit } = option || {} if (milliseconds === undefined || Number.isNaN(milliseconds) || milliseconds === null) { return "-" } if (format === "default") return formatPeriodCommon(milliseconds) - let val: string = null + let val: string if (format === "second") { val = Math.floor(milliseconds / MILL_PER_SECOND).toFixed(0) } else if (format === "minute") { diff --git a/src/pages/components/common.ts b/src/pages/components/common.ts index f21fed0a1..3956cfd3e 100644 --- a/src/pages/components/common.ts +++ b/src/pages/components/common.ts @@ -1,2 +1,2 @@ -export const cvtPxScale = (val: number | string): string => typeof val === 'number' ? `${val}px` : val \ No newline at end of file +export const cvtPxScale = (val: number | string | undefined): string | undefined => typeof val === 'number' ? `${val}px` : val \ No newline at end of file diff --git a/src/pages/element-ui/rtl.tsx b/src/pages/element-ui/rtl.tsx index 9446d8d9b..74974e259 100644 --- a/src/pages/element-ui/rtl.tsx +++ b/src/pages/element-ui/rtl.tsx @@ -1,6 +1,7 @@ import { ArrowLeft, ArrowRight, DArrowLeft, DArrowRight } from "@element-plus/icons-vue" import { isRtl } from "@util/document" import { ElIcon } from "element-plus" +import { VNode } from "vue" export const getDatePickerIconSlots = () => { const rtl = isRtl() @@ -14,8 +15,8 @@ export const getDatePickerIconSlots = () => { } export const getPaginationIconProps = (): { - prevIcon?: unknown - nextIcon?: unknown + prevIcon?: VNode + nextIcon?: VNode } => { if (!isRtl()) return {} return { diff --git a/src/pages/hooks/index.ts b/src/pages/hooks/index.ts index edf13a90f..b3b8e7fb8 100644 --- a/src/pages/hooks/index.ts +++ b/src/pages/hooks/index.ts @@ -1,16 +1,43 @@ -import { useCached as useCached0 } from "./useCached" -import { useMediaSize as useMediaSize0 } from "./useMediaSize" -import { useProvide as useProvide0, useProvider as useProvider0 } from "./useProvider" -import { useManualRequest as useManualRequest0, useRequest as useRequest0 } from "./useRequest" -import { useShadow as useShadow0 } from "./useShadow" -import { useState as useState0 } from "./useState" -import { useSwitch as useSwitch0 } from "./useSwitch" +import type { Ref, WatchSource } from "vue" +import type { RequestOption, RequestResult } from "./useRequest" -export const useRequest = useRequest0 -export const useManualRequest = useManualRequest0 -export const useShadow = useShadow0 -export const useProvide = useProvide0, useProvider = useProvider0 -export const useCached = useCached0 -export const useState = useState0 -export const useSwitch = useSwitch0 -export const useMediaSize = useMediaSize0 \ No newline at end of file +export declare function useState(defaultValue: T): [state: Ref, setter: (val: T) => void, reset: () => void] +export declare function useState(defaultValue?: T): [state: Ref, setter: (val?: T) => void, reset: () => void] + +export declare function useShadow(source: WatchSource): [Ref, setter: (val: T) => void, refresh: () => void] +export declare function useShadow(source: WatchSource, defaultValue: T): [Ref, setter: (val: T) => void, refresh: () => void] +export declare function useShadow(source: WatchSource, defaultValue?: T): [Ref, setter: (val?: T) => void, refresh: () => void] + +export declare function useManualRequest

( + getter: (...p: P) => Awaitable, + option: MakeRequired, 'manual'>, 'defaultValue'>, +): RequestResult +export declare function useManualRequest

( + getter: (...p: P) => Awaitable, + option?: Omit, 'manual'>, +): RequestResult + +export declare function useRequest

( + getter: (...p: P) => Awaitable, + option: MakeRequired, 'defaultValue'>, +): RequestResult +export declare function useRequest

( + getter: (...p: P) => Awaitable, + option?: RequestOption, +): RequestResult + + +export declare function useCached(key: string, defaultValue: T, defaultFirst?: boolean): { data: Ref, setter: (val: T) => void } +export declare function useCached( + key: string | undefined, + defaultValue?: T, + defaultFirst?: boolean, +): { data: Ref, setter: (val: T | undefined) => void } + +export * from "./useCached" +export * from "./useMediaSize" +export * from "./useProvider" +export * from "./useRequest" +export * from "./useShadow" +export * from "./useState" +export * from "./useSwitch" diff --git a/src/pages/hooks/useCached.ts b/src/pages/hooks/useCached.ts index ede26195a..e4fd56c0b 100644 --- a/src/pages/hooks/useCached.ts +++ b/src/pages/hooks/useCached.ts @@ -9,18 +9,18 @@ import { onBeforeMount, ref, type Ref, watch } from "vue" import { useState } from "." type Result = { - data: Ref + data: Ref setter: (val: T) => void } -const getInitialValue = (key: string, defaultValue?: T): T => { +const getInitialValue = (key: string, defaultValue?: T): T | undefined => { if (!key) return defaultValue const exist = localStorage.getItem(key) if (!exist) return defaultValue try { return JSON.parse(exist) as T } catch (e) { - return null + return undefined } } @@ -33,13 +33,13 @@ const saveCache = (key: string, val: T) => { } } -export const useCached = (key: string, defaultValue?: T, defaultFirst?: boolean): Result => { +export const useCached = (key: string | undefined, defaultValue?: T, defaultFirst?: boolean): Result => { if (!key) { const [data, setter] = useState(defaultValue) return { data, setter } } - const data: Ref = ref() - const setter = (val: T) => data.value = val + const data: Ref = ref() + const setter = (val: T | undefined) => data.value = val onBeforeMount(() => { let cachedValue = getInitialValue(key, defaultValue) let initial = defaultFirst ? defaultValue || cachedValue : cachedValue diff --git a/src/pages/hooks/useEcharts.ts b/src/pages/hooks/useEcharts.ts index 1c718f572..75bf0b23c 100644 --- a/src/pages/hooks/useEcharts.ts +++ b/src/pages/hooks/useEcharts.ts @@ -16,12 +16,12 @@ import { type Ref, isRef, onMounted, ref, watch } from "vue" type BaseEchartsOption = ComposeOption export abstract class EchartsWrapper { - public instance: ECharts + public instance: ECharts | undefined /** * true if need to re-generate option while size changing, or false */ protected isSizeSensitize: boolean = false - private lastBizOption: BizOption + private lastBizOption: BizOption | undefined init(container: HTMLDivElement) { this.instance = init(container) @@ -35,12 +35,12 @@ export abstract class EchartsWrapper { private async innerRender() { const biz = this.lastBizOption - const option = await this.generateOption(biz) as (EchartsOption & BaseEchartsOption) + const option = biz && await this.generateOption(biz) as (EchartsOption & BaseEchartsOption) if (!option) return await this.postChartOption(option) - this.instance.setOption(option, { notMerge: false }) + this.instance?.setOption(option, { notMerge: false }) } protected async postChartOption(option: EchartsOption & BaseEchartsOption) { @@ -57,7 +57,7 @@ export abstract class EchartsWrapper { } protected getDom(): HTMLElement { - return this.instance?.getDom?.() + return this.instance!.getDom() } protected abstract generateOption(biz: BizOption): Promise | EchartsOption @@ -70,7 +70,7 @@ export abstract class EchartsWrapper { type WrapperResult> = { refresh: () => Promise - elRef: Ref + elRef: Ref wrapper: EW } @@ -83,7 +83,7 @@ export const useEcharts = void }): WrapperResult => { - const elRef: Ref = ref() + const elRef = ref() const wrapperInstance = new Wrapper() const { hideLoading = false, @@ -103,7 +103,7 @@ export const useEcharts = { const target = elRef.value - wrapperInstance.init(target) + target && wrapperInstance.init(target) afterInit?.(wrapperInstance) !manual && refresh() watchRef && isRef(fetch) && watch(fetch, refresh) diff --git a/src/pages/hooks/useProvider.ts b/src/pages/hooks/useProvider.ts index 487d44776..19eaa709e 100644 --- a/src/pages/hooks/useProvider.ts +++ b/src/pages/hooks/useProvider.ts @@ -1,11 +1,11 @@ import { inject, provide } from "vue" export const useProvide = >(namespace: string, ctx: T) => { - Object.entries(ctx).forEach(([key, val]) => provide(`${namespace}_${key}`, val)) + Object.entries(ctx || {}).forEach(([key, val]) => provide(`${namespace}_${key}`, val)) } -export const useProvider = >(namespace: string, ...keys: (keyof T)[]): Partial => { - const result: Partial = {} +export const useProvider = , P extends keyof T>(namespace: string, ...keys: P[]): Pick => { + const result: any = {} Array.from(new Set(keys || [])).forEach(key => result[key] = inject(`${namespace}_${key as string}`)) - return result + return result as Pick } diff --git a/src/pages/hooks/useRequest.ts b/src/pages/hooks/useRequest.ts index a5bde7e20..bbe397ccb 100644 --- a/src/pages/hooks/useRequest.ts +++ b/src/pages/hooks/useRequest.ts @@ -1,7 +1,7 @@ import { ElLoadingService } from "element-plus" import { onBeforeMount, onMounted, ref, watch, type Ref, type WatchSource } from "vue" -type Option = { +export type RequestOption = { manual?: boolean defaultValue?: T loadingTarget?: string | Ref @@ -12,19 +12,19 @@ type Option = { onError?: (e: unknown) => void } -type Result = { +export type RequestResult = { data: Ref refresh: (...p: P) => void refreshAsync: (...p: P) => Promise refreshAgain: () => void loading: Ref - param: Ref

+ param: Ref

} -export const useRequest =

(getter: (...p: P) => Promise | T, option?: Option): Result => { +export const useRequest =

(getter: (...p: P) => Promise | T, option?: RequestOption): RequestResult => { const { manual = false, - defaultValue, defaultParam = ([] as P), + defaultValue, defaultParam = ([] as any[] as P), loadingTarget, loadingText, deps, onSuccess, onError, @@ -62,6 +62,6 @@ export const useRequest =

(getter: (...p: P) => Promise | return { data, refresh, refreshAsync, refreshAgain, loading, param } } -export const useManualRequest =

(getter: (...p: P) => Promise | T, option?: Omit, 'manual'>): Result => { +export const useManualRequest =

(getter: (...p: P) => Promise | T, option?: Omit, 'manual'>): RequestResult => { return useRequest(getter, { ...option || {}, manual: true }) } \ No newline at end of file diff --git a/src/pages/hooks/useShadow.ts b/src/pages/hooks/useShadow.ts index b70d54c66..c5467ae5f 100644 --- a/src/pages/hooks/useShadow.ts +++ b/src/pages/hooks/useShadow.ts @@ -1,8 +1,9 @@ import { type Ref, type WatchSource, ref, watch } from "vue" -export const useShadow = (source: WatchSource, defaultValue?: T): [Ref, setter: (val?: T) => void, refresh: () => void] => { +export const useShadow = (source: WatchSource, defaultValue?: T): [Ref, setter: (val?: T) => void, refresh: () => void] => { const getVal = () => typeof source === "function" ? source() : source?.value - const shadow: Ref = ref(getVal() ?? defaultValue) as Ref + const initial = getVal() ?? defaultValue + const shadow = initial ? ref(initial) : ref() watch(source, () => shadow.value = getVal()) - return [shadow, (val?: T) => shadow.value = val, () => shadow.value = getVal()] + return [shadow as Ref, (val?: T) => shadow.value = val, () => shadow.value = getVal()] } \ No newline at end of file diff --git a/src/pages/hooks/useState.ts b/src/pages/hooks/useState.ts index ddee8669b..ece80575a 100644 --- a/src/pages/hooks/useState.ts +++ b/src/pages/hooks/useState.ts @@ -1,7 +1,21 @@ -import { type Ref, shallowRef } from "vue" +import { ref, type Ref, shallowRef } from "vue" -export const useState = (defaultValue?: T): [state: Ref, setter: (val?: T) => void, reset: () => void] => { - const result = shallowRef(defaultValue) - const setter = (val?: T) => result.value = val - return [result, setter, () => setter(defaultValue)] +export const useState = (defaultValue?: T): + | [state: Ref, setter: (val: T) => void, reset: () => void] + | [state: Ref, setter: (val?: T) => void, reset: () => void] => { + if (defaultValue === undefined || defaultValue === null) { + const result = ref() + return [ + result, + (val?: T) => result.value = val, + () => result.value = undefined + ] + } else { + const result = shallowRef(defaultValue) + return [ + result, + (val: T) => result.value = val, + () => result.value = defaultValue + ] + } } \ No newline at end of file diff --git a/src/pages/popup/Main.tsx b/src/pages/popup/Main.tsx index 9bce115d5..17ac97007 100644 --- a/src/pages/popup/Main.tsx +++ b/src/pages/popup/Main.tsx @@ -1,22 +1,26 @@ import Flex from "@pages/components/Flex" -import { defineComponent } from "vue" +import { DefaultOption } from "@util/constant/option" +import { defineComponent, inject } from "vue" import { RouterView } from "vue-router" import Footer from "./components/Footer" import Header from "./components/Header" import { initPopupContext } from "./context" +export const PROVIDE_KEY = 'POPUP_OPTION' + const Main = defineComponent(() => { + const option = inject(PROVIDE_KEY) as DefaultOption + const { defaultDuration, defaultType, defaultDurationNum, defaultMergeMethod } = option - const appKey = initPopupContext() + const appKey = initPopupContext({ + mergeMethod: defaultMergeMethod, + duration: defaultDuration, + type: defaultType, + durationNum: defaultDurationNum, + }) return () => ( - +

diff --git a/src/pages/popup/common.tsx b/src/pages/popup/common.tsx index 2e8def97c..3ba7dfcfe 100644 --- a/src/pages/popup/common.tsx +++ b/src/pages/popup/common.tsx @@ -7,13 +7,13 @@ import { getAppPageUrl } from "@util/constant/url" import { getMonthTime, MILL_PER_DAY } from "@util/time" export type PopupQuery = { - mergeMethod: timer.stat.MergeMethod + mergeMethod: timer.stat.MergeMethod | undefined duration: timer.option.PopupDuration durationNum?: number type: timer.core.Dimension } -type DateRangeCalculator = (now: Date, num?: number) => Promise | Date | [Date, Date] +type DateRangeCalculator = (now: Date, num?: number) => Awaitable const DATE_RANGE_CALCULATORS: { [duration in timer.option.PopupDuration]: DateRangeCalculator } = { today: now => now, @@ -23,8 +23,8 @@ const DATE_RANGE_CALCULATORS: { [duration in timer.option.PopupDuration]: DateRa return [start, now] }, thisMonth: now => [getMonthTime(now)[0], now], - lastDays: (now, num) => [new Date(now.getTime() - MILL_PER_DAY * (num - 1)), now], - allTime: () => null, + lastDays: (now, num) => [new Date(now.getTime() - MILL_PER_DAY * (num ?? 1 - 1)), now], + allTime: () => undefined, } export const cvt2StatQuery = async (param: PopupQuery): Promise => { @@ -41,7 +41,7 @@ export const cvt2StatQuery = async (param: PopupQuery): Promise return stat } -function buildReportQuery(siteType: timer.site.Type, date: Date | [Date, Date?], type: timer.core.Dimension): ReportQueryParam { +function buildReportQuery(siteType: timer.site.Type, date: Date | [Date, Date?] | undefined, type: timer.core.Dimension): ReportQueryParam { const query: ReportQueryParam = {} // Merge host siteType === 'merged' && (query.mh = "1") @@ -64,10 +64,9 @@ function buildReportQuery(siteType: timer.site.Type, date: Date | [Date, Date?], return query } -export function calJumpUrl(siteKey: timer.site.SiteKey, date: Date | [Date, Date?], type: timer.core.Dimension): string { - const { host, type: siteType } = siteKey || {} - - if (!host) return +export function calJumpUrl(siteKey: timer.site.SiteKey | undefined, date: Date | [Date, Date?] | undefined, type: timer.core.Dimension): string | undefined { + if (!siteKey) return + const { host, type: siteType } = siteKey if (siteType === 'normal' && !isRemainHost(host)) { return `http://${host}` diff --git a/src/pages/popup/components/Footer/DurationSelect/index.tsx b/src/pages/popup/components/Footer/DurationSelect/index.tsx index 0f069e1c2..2e01cf47f 100644 --- a/src/pages/popup/components/Footer/DurationSelect/index.tsx +++ b/src/pages/popup/components/Footer/DurationSelect/index.tsx @@ -56,7 +56,7 @@ const DurationSelect = defineComponent({ return () => ( ctx.emit('change', val)} + onChange={val => ctx.emit('change', val as [timer.option.PopupDuration, number?])} options={options(props.reverse)} props={{ expandTrigger: props.expandTrigger }} show-all-levels={false} diff --git a/src/pages/popup/components/Footer/index.tsx b/src/pages/popup/components/Footer/index.tsx index 94bb9493e..a3322e08b 100644 --- a/src/pages/popup/components/Footer/index.tsx +++ b/src/pages/popup/components/Footer/index.tsx @@ -5,30 +5,18 @@ import { usePopupContext } from "@popup/context" import { t } from "@popup/locale" import { ALL_DIMENSIONS } from "@util/stat" import { ElOption, ElSelect, ElText } from "element-plus" -import { defineComponent, watch } from "vue" +import { defineComponent } from "vue" import Menu from "./Menu" const Footer = defineComponent({ props: { total: String, }, - emits: { - queryChange: (_query: PopupQuery) => true, - }, - setup(_, ctx) { + setup() { const { query, setQuery } = usePopupContext() - watch(query, () => ctx.emit('queryChange', query.value)) const updateQuery = (k: K, v: PopupQuery[K]) => { - const newQuery = { - ...query.value || ({ - mergeMethod: undefined, - duration: undefined, - type: undefined, - } satisfies PopupQuery), - [k]: v, - } satisfies PopupQuery - setQuery(newQuery) + setQuery({ ...query.value, [k]: v } satisfies PopupQuery) } return () => ( @@ -58,14 +46,7 @@ const Footer = defineComponent({ setQuery({ - ...query.value || ({ - mergeMethod: undefined, - duration: undefined, - type: undefined, - } satisfies PopupQuery), - duration, durationNum, - })} + onChange={([duration, durationNum]) => setQuery({ ...query.value, duration, durationNum })} /> @@ -27,7 +27,6 @@ const computeUpdateText = (version: string) => { const Extra = defineComponent(() => { const { data: latestVersion } = useRequest(getLatestVersion) - const upgradeVisible = computed(() => latestVersion.value && packageInfo.version !== latestVersion.value && IS_FROM_STORE) const { data: rateVisible } = useRequest(() => metaService.recommendRate()) const handleRateClick = async () => { @@ -41,7 +40,7 @@ const Extra = defineComponent(() => { } return () => { - if (upgradeVisible.value) { + if (latestVersion.value && packageInfo.version !== latestVersion.value && IS_FROM_STORE) { return ( { - private resultCache: PercentageResult - private selectedCache: number + private resultCache: PercentageResult | undefined + private selectedCache: number | undefined init(container: HTMLDivElement): void { super.init(container) - this.instance.on('selectchanged', (ev: SelectChangeEvent) => { - const { type, fromAction, fromActionPayload } = ev || {} + this.instance?.on('selectchanged', ev => { + const { type, fromAction, fromActionPayload } = (ev as SelectChangeEvent) || {} const { seriesIndex, dataIndexInside } = fromActionPayload || {} if (type !== 'selectchanged' || seriesIndex !== 0) return @@ -52,26 +52,24 @@ export default class SiteWrapper extends EchartsWrapper { + this.instance?.on('click', (ev: ECElementEvent) => { const { type: evType, componentType, seriesIndex, data } = ev || {} if (evType !== 'click' || componentType !== 'series' || seriesIndex !== 1) { return } const { query: { type } = {}, date } = this.resultCache || {} - handleClick(data as PieSeriesItemOption, date, type) + type && handleClick(data as PieSeriesItemOption, date, type) }) } protected async generateOption(result: PercentageResult): Promise { - if (!result) return - // Let not set to the end const rows = result.rows?.sort((_, a) => a.cateKey === CATE_NOT_SET_ID ? -1 : 0) this.resultCache = { ...result, rows } @@ -86,7 +84,7 @@ export default class SiteWrapper extends EchartsWrapper r?.cateKey === this.selectedCache)?.[0] + const selected: timer.stat.Row | undefined = this.selectedCache ? rows?.filter(r => r?.cateKey === this.selectedCache)?.[0] : undefined const textColor = getPrimaryTextColor() const inactiveColor = getInfoColor() @@ -104,7 +102,7 @@ export default class SiteWrapper extends EchartsWrapper ({ name: cateNameMap[cateKey] ?? `${cateKey}`, cateKey })), + data: rows.map(({ cateKey }) => ({ name: cateNameMap[cateKey ?? ''] ?? `${cateKey}`, cateKey })), } const series: PieSeriesOption[] = [{ @@ -117,7 +115,7 @@ export default class SiteWrapper extends EchartsWrapper ({ name })), + data: siteSeries.data?.map(val => ({ name: (val as any).name })), } series.push(siteSeries) } - const titleSuffix = this.selectedCache !== CATE_NOT_SET_ID ? cateNameMap[this.selectedCache] : undefined + const titleSuffix = this.selectedCache && this.selectedCache !== CATE_NOT_SET_ID ? cateNameMap[this.selectedCache] : undefined const option: EcOption = { title: generateTitleOption(result, titleSuffix), legend, @@ -172,10 +170,10 @@ export default class SiteWrapper extends EchartsWrapper { - private resultCache: PercentageResult + private resultCache: PercentageResult | undefined init(container: HTMLDivElement): void { super.init(container) - this.instance.on('click', (params: ECElementEvent) => { + this.instance?.on('click', (params: ECElementEvent) => { const { type: evType, componentType, data } = params || {} if (evType !== 'click' || componentType !== 'series') return const { query: { type } = {}, date } = this.resultCache || {} - handleClick(data as PieSeriesItemOption, date, type) + type && handleClick(data as PieSeriesItemOption, date, type) }) } diff --git a/src/pages/popup/components/Percentage/chart.ts b/src/pages/popup/components/Percentage/chart.ts index 7b123a3fa..c17a42a83 100644 --- a/src/pages/popup/components/Percentage/chart.ts +++ b/src/pages/popup/components/Percentage/chart.ts @@ -1,5 +1,5 @@ import { createTab } from "@api/chrome/tab" -import { getPrimaryTextColor, getSecondaryTextColor } from "@pages/util/style" +import { getCssVariable, getPrimaryTextColor, getSecondaryTextColor } from "@pages/util/style" import { calJumpUrl } from "@popup/common" import { t } from "@popup/locale" import { sum } from "@util/array" @@ -42,25 +42,23 @@ function combineDate(start: Date, end: Date, format: string): string { .replace('{ed}', ed.toString().padStart(2, '0')) } -function formatDateStr(date: Date | [Date, Date?], dataDate: [string, string]): string { +function formatDateStr(date: Date | [Date, Date?] | undefined, dataDate: [string, string]): string { const format = t(msg => msg.calendar.dateFormat) if (!date) { date = dataDate?.map(parseTime) as [Date, Date] - } else if (!(date instanceof Array)) { + } + if (!date) return '' + if (!(date instanceof Array)) { // Single day return formatTime(date, format) } const [start, end] = date - if (!start && !end) return '' - if (!start) return formatTime(end, format) - if (!end) return formatTime(start, format) - - return combineDate(start, end, format) + return end ? combineDate(start, end, format) : formatTime(start, format) } -function formatTotalStr(rows: timer.stat.Row[], type: timer.core.Dimension): string { +function formatTotalStr(rows: timer.stat.Row[], type: timer.core.Dimension | undefined): string { if (type === 'focus') { const total = sum(rows.map(r => r?.focus ?? 0)) const totalTime = formatPeriodCommon(total) @@ -75,7 +73,7 @@ function formatTotalStr(rows: timer.stat.Row[], type: timer.core.Dimension): str function calculateSubTitleText(result: PercentageResult): string { let { date, dataDate, rows, query: { type } = {} } = result - const dateStr = formatDateStr(date, dataDate) + const dateStr = dataDate ? formatDateStr(date, dataDate) : '' const totalStr = formatTotalStr(rows, type) let parts = [totalStr, dateStr].filter(str => !!str) isRtl() && (parts = parts.reverse()) @@ -105,7 +103,8 @@ export function generateToolboxOption(): ToolboxComponentOption { // file name name: 'Time_Tracker_Percentage', excludeComponents: ['toolbox'], - pixelRatio: 1 + pixelRatio: 7, + backgroundColor: getCssVariable('--el-card-bg-color', '.el-card'), }, } } @@ -139,19 +138,20 @@ function cvt2ChartRows(rows: timer.stat.Row[], type: timer.core.Dimension, itemC otherCount++ } } - other.siteKey.host = t(msg => msg.content.percentage.otherLabel, { count: otherCount }) + const { siteKey } = other + siteKey && (siteKey.host = t(msg => msg.content.percentage.otherLabel, { count: otherCount })) popupRows.push(other) const data = popupRows.filter(item => !!item[type]) return data } // The declaration of data item -export type PieSeriesItemOption = PieSeriesOption['data'][0] +export type PieSeriesItemOption = Exclude[0] & Pick // The declarations of labels -type PieLabelRichOption = PieSeriesOption['label']['rich'] -type PieLabelRichValueOption = PieLabelRichOption[string] +type PieLabelRichOption = Exclude['rich'] +type PieLabelRichValueOption = Exclude[string] const LABEL_FONT_SIZE = 13 const LABEL_ICON_SIZE = 13 @@ -161,9 +161,9 @@ const BASE_LABEL_RICH_VALUE: PieLabelRichValueOption = { fontSize: LABEL_FONT_SIZE, } -const legend2LabelStyle = (legend: string) => { +const legend2LabelStyle = (legend: string): string => { if (!legend) return '' - const code = [] + const code: string[] = [] for (let i = 0; i < legend.length; i++) { code.push(legend.charCodeAt(i).toString(36).padStart(3, '0')) } @@ -195,14 +195,14 @@ type CustomOption = Pick< > export function generateSiteSeriesOption(rows: timer.stat.Row[], result: PercentageResult, customOption: CustomOption): PieSeriesOption { - const { displaySiteName, query: { type } = {}, itemCount } = result || {} + const { displaySiteName, query: { type }, itemCount } = result || {} const chartRows = cvt2ChartRows(rows, type, itemCount) const iconRich: PieLabelRichOption = {} const data = chartRows.map(d => { const { siteKey, cateKey, alias, isOther, iconUrl } = d const host = siteKey?.host - const legend = displaySiteName ? (alias || host) : host + const legend = (displaySiteName ? (alias ?? host) : host) ?? '' const richValue: PieLabelRichValueOption = { ...BASE_LABEL_RICH_VALUE } iconUrl && (richValue.backgroundColor = { image: iconUrl }) iconRich[legend2LabelStyle(legend)] = richValue diff --git a/src/pages/popup/components/Percentage/query.ts b/src/pages/popup/components/Percentage/query.ts index 154a07b33..63a62179e 100644 --- a/src/pages/popup/components/Percentage/query.ts +++ b/src/pages/popup/components/Percentage/query.ts @@ -8,9 +8,9 @@ export type PercentageResult = { query: PopupQuery rows: timer.stat.Row[] // Actually date range according to duration - date: Date | [Date, Date?] + date: Date | [Date, Date?] | undefined displaySiteName: boolean - dataDate: [string, string] + dataDate: [string, string] | undefined chartTitle: string itemCount: number dateLength: number @@ -28,18 +28,19 @@ const findAllDates = (row: timer.stat.Row): Set => { return set } -const findDateRange = (rows: timer.stat.Row[]): [string, string] => { +const findDateRange = (rows: timer.stat.Row[]): [string, string] | undefined => { const set = new Set() rows?.forEach(row => { const dates = findAllDates(row) dates.forEach(d => set.add(d)) }) - let minDate: string, maxDate: string + let minDate: string | undefined = undefined + let maxDate: string | undefined = undefined set.forEach(d => { if (!minDate || d < minDate) minDate = d if (!maxDate || d > maxDate) maxDate = d }) - return [minDate, maxDate] + return minDate && maxDate ? [minDate, maxDate] : undefined } export const doQuery = async (query: PopupQuery): Promise => { @@ -52,7 +53,7 @@ export const doQuery = async (query: PopupQuery): Promise => { return { query, rows, date, dataDate: findDateRange(rows), - dateLength: date instanceof Array ? getDayLength(date[0], date[1]) : 1, + dateLength: date instanceof Array ? getDayLength(date[0], date[1] ?? new Date()) : 1, displaySiteName: option.displaySiteName, chartTitle: t(msg => msg.content.percentage.title[query?.duration], { n: query?.durationNum }), itemCount, diff --git a/src/pages/popup/components/Ranking/Item.tsx b/src/pages/popup/components/Ranking/Item.tsx index 1183f1bab..87e5e11b8 100644 --- a/src/pages/popup/components/Ranking/Item.tsx +++ b/src/pages/popup/components/Ranking/Item.tsx @@ -3,7 +3,7 @@ import TooltipWrapper from "@app/components/common/TooltipWrapper" import { Mouse, Timer } from "@element-plus/icons-vue" import Flex from "@pages/components/Flex" import { calJumpUrl } from "@popup/common" -import { useCateNameMap } from "@popup/context" +import { useCateNameMap, useDimension } from "@popup/context" import { t } from "@popup/locale" import { isRemainHost } from "@util/constant/remain-host" import { formatPeriodCommon } from "@util/time" @@ -21,12 +21,15 @@ const TITLE_STYLE: StyleValue = { const Title = defineComponent({ props: { - value: Object as PropType, - type: String as PropType, + value: { + type: Object as PropType, + required: true, + }, date: [Date, Array] as PropType, displaySiteName: Boolean, }, setup(props) { + const type = useDimension() const cateNameMap = useCateNameMap() const name = computed(() => { const { alias, siteKey: { host } = {}, cateKey } = props.value || {} @@ -41,7 +44,7 @@ const Title = defineComponent({ cateKey, } = props.value || {} if (!!cateKey || siteType === 'merged') { - return t(msg => msg.content.ranking.includingCount, { siteCount: mergedRows.length ?? 0 }) + return t(msg => msg.content.ranking.includingCount, { siteCount: mergedRows?.length ?? 0 }) } if (!props.displaySiteName) { return '' @@ -49,7 +52,7 @@ const Title = defineComponent({ return alias ? host : '' }) - const url = computed(() => calJumpUrl(props.value?.siteKey, props.date, props.type)) + const url = computed(() => props.value.siteKey && calJumpUrl(props.value.siteKey, props.date, type.value)) return () => ( ) => { const { siteKey, alias, cateKey } = row || {} - const cateName = cateNameMap?.[cateKey] + const cateName = cateKey !== undefined ? cateNameMap?.[cateKey] : undefined return [cateName, alias, siteKey?.host] .find(a => !!a) ?.substring?.(0, 1) @@ -80,16 +83,19 @@ const renderAvatarText = (row: timer.stat.Row, cateNameMap: Record, + value: { + type: Object as PropType, + required: true, + }, max: Number, total: Number, - type: String as PropType, date: [Date, Array] as PropType, displaySiteName: Boolean, }, setup(props, ctx) { - const rate = computed(() => props.max ? (props.value?.[props.type] ?? 0) / props.max * 100 : 0) - const percentage = computed(() => props.total ? (props.value?.[props.type] ?? 0) / props.total * 100 : 0) + const type = useDimension() + const rate = computed(() => props.max ? (props.value?.[type.value] ?? 0) / props.max * 100 : 0) + const percentage = computed(() => props.total ? (props.value?.[type.value] ?? 0) / props.total * 100 : 0) const cateNameMap = useCateNameMap() @@ -135,7 +141,6 @@ const Item = defineComponent({ @@ -149,13 +154,13 @@ const Item = defineComponent({ </Flex> <Flex column justify="space-around" flex={1}> <Flex justify="space-between" width="100%" cursor="unset"> - <ElText type={props.type === 'time' ? 'primary' : 'info'} size="small"> + <ElText type={type.value === 'time' ? 'primary' : 'info'} size="small"> <ElIcon> <Mouse /> </ElIcon> {props.value?.time ?? 0} </ElText> - <ElText type={props.type === 'focus' ? 'primary' : 'info'} size="small"> + <ElText type={type.value === 'focus' ? 'primary' : 'info'} size="small"> <ElIcon> <Timer /> </ElIcon> diff --git a/src/pages/popup/components/Ranking/index.tsx b/src/pages/popup/components/Ranking/index.tsx index eea90efc8..bb8f1d9a5 100644 --- a/src/pages/popup/components/Ranking/index.tsx +++ b/src/pages/popup/components/Ranking/index.tsx @@ -16,7 +16,6 @@ const Ranking = defineComponent(() => { <ElCol span={24 / 3}> <Item value={row} - type={result.value?.query?.type} max={result.value?.max} total={result.value?.total} date={result.value?.date} diff --git a/src/pages/popup/components/Ranking/query.tsx b/src/pages/popup/components/Ranking/query.tsx index 064408317..e0b900c01 100644 --- a/src/pages/popup/components/Ranking/query.tsx +++ b/src/pages/popup/components/Ranking/query.tsx @@ -7,9 +7,8 @@ export type RankingResult = { rows: timer.stat.Row[] max: number total: number - query: PopupQuery displaySiteName: boolean - date: Date | [Date, Date?] + date: Date | [Date, Date?] | undefined } export const doQuery = async (query: PopupQuery): Promise<RankingResult> => { @@ -21,5 +20,5 @@ export const doQuery = async (query: PopupQuery): Promise<RankingResult> => { const total = sum(values) const date = statQuery.date const { displaySiteName } = await optionHolder.get() || {} - return { max, total, rows, query, date, displaySiteName } + return { max, total, rows, date, displaySiteName } } \ No newline at end of file diff --git a/src/pages/popup/context.ts b/src/pages/popup/context.ts index df5de5965..be5ca8e06 100644 --- a/src/pages/popup/context.ts +++ b/src/pages/popup/context.ts @@ -1,13 +1,11 @@ +import { useRequest, useState } from "@hooks" import { useProvide, useProvider } from "@hooks/useProvider" -import { useRequest } from "@hooks/useRequest" -import { useState } from "@hooks/useState" import cateService from "@service/cate-service" -import optionHolder from "@service/components/option-holder" import optionService from "@service/option-service" import { groupBy } from "@util/array" import { isDarkMode, toggle } from "@util/dark-mode" import { CATE_NOT_SET_ID } from "@util/site" -import { onBeforeMount, ref, type Ref } from "vue" +import { computed, ComputedRef, ref, type Ref } from "vue" import { type PopupQuery } from "./common" import { t } from "./locale" @@ -17,29 +15,19 @@ type PopupContextValue = { setDarkMode: (val: boolean) => void query: Ref<PopupQuery> setQuery: (val: PopupQuery) => void + dimension: ComputedRef<timer.core.Dimension> cateNameMap: Ref<Record<number, string>> } const NAMESPACE = '_' -export const initPopupContext = (): Ref<number> => { +export const initPopupContext = (defaultQuery: PopupQuery): Ref<number> => { const appKey = ref(Date.now()) const reload = () => appKey.value = Date.now() const { data: darkMode, refresh: refreshDarkMode } = useRequest(() => optionService.isDarkMode(), { defaultValue: isDarkMode() }) - const [query, setQuery] = useState<PopupQuery>() - - onBeforeMount(async () => { - const option = await optionHolder.get() - const { defaultDuration, defaultType, defaultDurationNum, defaultMergeMethod } = option || {} - setQuery({ - mergeMethod: defaultMergeMethod, - type: defaultType, - duration: defaultDuration, - durationNum: defaultDurationNum, - }) - }) + const [query, setQuery] = useState(defaultQuery) const setDarkMode = async (val: boolean) => { const option: timer.option.DarkMode = val ? 'on' : 'off' @@ -53,14 +41,18 @@ export const initPopupContext = (): Ref<number> => { const result = groupBy(categories || [], c => c?.id, l => l?.[0]?.name) result[CATE_NOT_SET_ID] = t(msg => msg.shared.cate.notSet) return result - }) - useProvide<PopupContextValue>(NAMESPACE, { reload, darkMode, setDarkMode, query, setQuery, cateNameMap }) + }, { defaultValue: {} }) + + const dimension = computed(() => query.value.type) + useProvide<PopupContextValue>(NAMESPACE, { reload, darkMode, setDarkMode, query, setQuery, cateNameMap, dimension }) return appKey } -export const usePopupContext = () => useProvider<PopupContextValue>( +export const usePopupContext = () => useProvider<PopupContextValue, 'reload' | 'darkMode' | 'setDarkMode' | 'query' | 'setQuery' | 'cateNameMap'>( NAMESPACE, 'reload', 'darkMode', 'setDarkMode', 'query', 'setQuery', 'cateNameMap' ) -export const useCateNameMap = () => useProvider<PopupContextValue>(NAMESPACE, 'cateNameMap')?.cateNameMap \ No newline at end of file +export const useDimension = () => useProvider<PopupContextValue, 'dimension'>(NAMESPACE, 'dimension').dimension + +export const useCateNameMap = () => useProvider<PopupContextValue, 'cateNameMap'>(NAMESPACE, 'cateNameMap')?.cateNameMap \ No newline at end of file diff --git a/src/pages/popup/index.ts b/src/pages/popup/index.ts index a532b796e..4dd98a348 100644 --- a/src/pages/popup/index.ts +++ b/src/pages/popup/index.ts @@ -11,10 +11,11 @@ import optionService from "@service/option-service" import { toggle } from "@util/dark-mode" import "element-plus/theme-chalk/index.css" import { createApp } from "vue" -import Main from "./Main" +import Main, { PROVIDE_KEY } from "./Main" import { type FrameRequest, type FrameResponse } from "./message" import initRouter from "./router" import "./style" +import optionHolder from "@service/components/option-holder" function send2ParentWindow(data: any): Promise<void> { return new Promise(resolve => { @@ -46,7 +47,9 @@ async function main() { el.id = 'app' document.body.append(el) + const option = await optionHolder.get() const app = createApp(Main) + app.provide(PROVIDE_KEY, option) initRouter(app) app.mount(el) diff --git a/src/pages/side/Layout.tsx b/src/pages/side/Layout.tsx index 6cd872d2d..93eaf2f1b 100644 --- a/src/pages/side/Layout.tsx +++ b/src/pages/side/Layout.tsx @@ -2,14 +2,14 @@ import { useRequest } from "@hooks" import statService, { type StatQueryParam } from "@service/stat-service" import { formatTime } from "@util/time" import { ElText } from "element-plus" -import { defineComponent, ref } from "vue" +import { defineComponent, ref, type StyleValue } from "vue" import RowList from "./components/RowList" import Search from "./components/Search" import { t } from "./locale" const _default = defineComponent(() => { const date = ref(new Date()) - const query = ref<string>() + const query = ref('') const { data, refresh, loading } = useRequest(() => { const statParam: StatQueryParam = { @@ -24,6 +24,7 @@ const _default = defineComponent(() => { return () => <div class="main"> <Search + defaultQuery={query.value} defaultDate={date.value} onSearch={(newQuery, newDate) => { query.value = newQuery @@ -41,7 +42,11 @@ const _default = defineComponent(() => { @{formatTime(date.value, t(msg => msg.calendar.dateFormat))} </ElText> </div> - <RowList loading={loading.value} data={data.value} style={{ flex: 1, overflow: "auto" }} /> + <RowList + loading={loading.value} + data={data.value ?? []} + style={{ flex: 1, overflow: "auto" } satisfies StyleValue} + /> </div> }) diff --git a/src/pages/side/components/RowList/Item.tsx b/src/pages/side/components/RowList/Item.tsx index 6dc72a1cc..a2dcd7849 100644 --- a/src/pages/side/components/RowList/Item.tsx +++ b/src/pages/side/components/RowList/Item.tsx @@ -6,8 +6,8 @@ import { formatPeriodCommon } from "@util/time" import { ElAvatar, ElCard, ElLink, ElProgress, ElTag, ElText, ElTooltip } from "element-plus" import { computed, defineComponent, type PropType } from "vue" -const renderTitle = (siteName: string, host: string, handleJump: () => void) => { - const text = siteName || host +const renderTitle = (siteName: string | undefined, host: string | undefined, handleJump: () => void) => { + const text = siteName ?? host ?? '' const tooltip = siteName ? host : null const textNode = <ElLink onClick={handleJump}>{text}</ElLink> if (!tooltip) return textNode @@ -26,7 +26,10 @@ const renderAvatarText = (row: timer.stat.Row) => { const _default = defineComponent({ props: { - value: Object as PropType<timer.stat.Row>, + value: { + type: Object as PropType<timer.stat.Row>, + required: true, + }, max: Number, total: Number, }, @@ -34,7 +37,7 @@ const _default = defineComponent({ const [iconUrl] = useShadow(() => props.value?.iconUrl) const [host] = useShadow(() => props.value?.siteKey?.host) const [siteName] = useShadow(() => props.value?.alias) - const clickable = computed(() => !isRemainHost(host.value)) + const clickable = computed(() => host?.value && !isRemainHost(host.value)) const [rate] = useShadow(() => { if (!props.max) return 0 return (props.value?.focus ?? 0) / props.max * 100 diff --git a/src/pages/side/components/RowList/index.tsx b/src/pages/side/components/RowList/index.tsx index d418e82dd..99f84bf14 100644 --- a/src/pages/side/components/RowList/index.tsx +++ b/src/pages/side/components/RowList/index.tsx @@ -1,19 +1,21 @@ -import { useShadow } from "@hooks" import Flex from "@pages/components/Flex" import { sum } from "@util/array" import { ElEmpty, ElScrollbar, type ScrollbarInstance } from "element-plus" -import { computed, type CSSProperties, defineComponent, type PropType, ref, watch } from "vue" +import { computed, type CSSProperties, defineComponent, type PropType, ref, toRef, watch } from "vue" import Item from "./Item" import "./row-list.sass" const _default = defineComponent({ props: { loading: Boolean, - data: Array as PropType<timer.stat.Row[]>, + data: { + type: Array as PropType<timer.stat.Row[]>, + required: true, + }, style: Object as PropType<CSSProperties>, }, setup(props) { - const [data] = useShadow(() => (props.data || []).filter(i => i.focus), []) + const data = toRef(props, 'data') const maxFocus = computed(() => data.value.map(r => r.focus).reduce((a, b) => a > b ? a : b, 0) ?? 0) const totalFocus = computed(() => sum(data.value.map(i => i?.focus ?? 0))) const scrollbar = ref<ScrollbarInstance>() @@ -23,13 +25,7 @@ const _default = defineComponent({ <ElScrollbar v-loading={props.loading} height="100%" ref={scrollbar} style={{ width: '100%' }}> <Flex column gap={8}> {!data.value?.length && !props.loading && <ElEmpty class="row-list-empty" />} - {data.value?.map(item => - <Item - value={item} - max={maxFocus.value} - total={totalFocus.value} - /> - )} + {data.value?.map(item => <Item value={item} max={maxFocus.value} total={totalFocus.value} />)} </Flex> </ElScrollbar> </Flex> diff --git a/src/pages/side/components/Search/index.tsx b/src/pages/side/components/Search/index.tsx index 5233c6acf..c2d9fe048 100644 --- a/src/pages/side/components/Search/index.tsx +++ b/src/pages/side/components/Search/index.tsx @@ -9,8 +9,14 @@ import "./search.sass" const _default = defineComponent({ props: { - defaultDate: Object as PropType<Date>, - defaultQuery: String, + defaultDate: { + type: Object as PropType<Date>, + required: true, + }, + defaultQuery: { + type: String, + required: true, + }, }, emits: { search: (_query: string, _date: Date) => true @@ -19,8 +25,8 @@ const _default = defineComponent({ const now = Date.now() const [query, setQuery] = useState(props.defaultQuery) - const [date, setDate] = useState<Date>(props.defaultDate) - const handleSearch = () => ctx.emit("search", query.value?.trim?.(), date.value) + const [date, setDate] = useState(props.defaultDate) + const handleSearch = () => ctx.emit("search", query.value.trim(), date.value) watch(date, handleSearch) @@ -37,7 +43,7 @@ const _default = defineComponent({ setQuery('') handleSearch() }} - onKeydown={(kv: KeyboardEvent) => kv.code === 'Enter' && handleSearch()} + onKeydown={kv => (kv as KeyboardEvent).code === 'Enter' && handleSearch()} /> <ElDatePicker clearable={false} diff --git a/src/pages/util/style.ts b/src/pages/util/style.ts index 06863f187..a533c8a74 100644 --- a/src/pages/util/style.ts +++ b/src/pages/util/style.ts @@ -24,27 +24,28 @@ export const getStyle = ( } } -export function getCssVariable(varName: string, ele?: HTMLElement): string { - const realEle = ele || document.documentElement +export function getCssVariable(varName: string, eleOrSelector?: HTMLElement | string): string | undefined { + const ele = typeof eleOrSelector === 'string' ? document.querySelector(eleOrSelector) : eleOrSelector + const realEle = ele ?? document.documentElement if (!realEle) { return undefined } - return getComputedStyle(ele || document.documentElement).getPropertyValue(varName) + return getComputedStyle(realEle).getPropertyValue(varName) } -export function getPrimaryTextColor(): string { +export function getPrimaryTextColor(): string | undefined { return getCssVariable("--el-text-color-primary") } -export function getRegularTextColor(): string { +export function getRegularTextColor(): string | undefined { return getCssVariable("--el-text-color-regular") } -export function getSecondaryTextColor(): string { +export function getSecondaryTextColor(): string | undefined { return getCssVariable("--el-text-color-secondary") } -export function getInfoColor(): string { +export function getInfoColor(): string | undefined { return getCssVariable("--el-color-info") } diff --git a/src/service/backup/common.ts b/src/service/backup/common.ts index f54b7dbce..343df572c 100644 --- a/src/service/backup/common.ts +++ b/src/service/backup/common.ts @@ -1,7 +1,7 @@ -export function processDir(dirPath: string) { +export function processDir(dirPath: string | undefined): string { dirPath = dirPath?.trim?.() if (!dirPath) { - return null + return '' } while (dirPath.startsWith("/")) { dirPath = dirPath.substring(1) diff --git a/src/service/backup/gist/compressor.ts b/src/service/backup/gist/compressor.ts index 08024211b..f9e6a045d 100644 --- a/src/service/backup/gist/compressor.ts +++ b/src/service/backup/gist/compressor.ts @@ -28,7 +28,7 @@ export type GistRow = { ] } -function calcGroupKey(row: timer.core.Row): string { +function calcGroupKey(row: timer.core.Row): string | undefined { const date = row.date if (!date) { return undefined @@ -67,11 +67,11 @@ export function divide2Buckets(rows: timer.core.Row[]): [string, GistData][] { /** * Calculate all the buckets between {@param startDate} and {@param endDate} */ -export function calcAllBuckets(startDate: string, endDate: string) { +export function calcAllBuckets(startDate: string | undefined, endDate: string | undefined) { endDate = endDate || formatTimeYMD(new Date()) - const result = [] - const start = startDate ? parseTime(startDate) : getBirthday() - const end = parseTime(endDate) + const result: string[] = [] + const start = parseTime(startDate) ?? getBirthday() + const end = parseTime(endDate) ?? new Date() while (start < end) { result.push(formatTimeYMD(start)) start.setMonth(start.getMonth() + 1) @@ -89,7 +89,7 @@ export function calcAllBuckets(startDate: string, endDate: string) { * @returns rows */ export function gistData2Rows(yearMonth: string, gistData: GistData): timer.core.Row[] { - const result = [] + const result: timer.core.Row[] = [] Object.entries(gistData).forEach(([dateOfMonth, gistRow]) => { const date = yearMonth + dateOfMonth Object.entries(gistRow).forEach(([host, val]) => { diff --git a/src/service/backup/gist/coordinator.ts b/src/service/backup/gist/coordinator.ts index a68026a3c..80226a3a9 100644 --- a/src/service/backup/gist/coordinator.ts +++ b/src/service/backup/gist/coordinator.ts @@ -7,7 +7,7 @@ import { createGist, findTarget, getGist, getJsonFileContent, testToken, updateGist, - type File, type FileForm, type Gist, type GistForm + type FileForm, type Gist, type GistForm } from "@api/gist" import { SOURCE_CODE_PAGE } from "@util/constant/url" import MonthIterator from "@util/month-iterator" @@ -56,6 +56,14 @@ function filterDate(row: timer.core.Row, start: string, end: string) { return true } +function checkTokenExist(context: timer.backup.CoordinatorContext<Cache>): string { + const token = context.auth?.token + if (!token) { + throw new Error("Token must not be empty. This can't happen, please contact to the developer") + } + return token +} + export default class GistCoordinator implements timer.backup.Coordinator<Cache> { async updateClients( context: timer.backup.CoordinatorContext<Cache>, @@ -71,7 +79,7 @@ export default class GistCoordinator implements timer.backup.Coordinator<Cache> filename: CLIENT_FILE_NAME, content: JSON.stringify(clients) } - await updateGist(context.auth?.token, gist.id, { description: gist.description, public: false, files }) + await updateGist(checkTokenExist(context), gist.id, { description: gist.description, public: false, files }) } async listAllClients(context: timer.backup.CoordinatorContext<Cache>): Promise<timer.backup.Client[]> { @@ -80,7 +88,7 @@ export default class GistCoordinator implements timer.backup.Coordinator<Cache> return [] } const file = gist?.files[CLIENT_FILE_NAME] - return file ? getJsonFileContent(file) || [] : [] + return file ? await getJsonFileContent(file) ?? [] : [] } async download(context: timer.backup.CoordinatorContext<Cache>, startTime: Date, endTime: Date, targetCid?: string): Promise<timer.core.Row[]> { @@ -91,12 +99,12 @@ export default class GistCoordinator implements timer.backup.Coordinator<Cache> await Promise.all(allYearMonth.map(async yearMonth => { const filename = bucket2filename(yearMonth, targetCid || context.cid) const gist: Gist = await this.getStatGist(context) - const file: File = gist.files[filename] + const file = gist.files[filename] if (file) { - const gistData: GistData = await getJsonFileContent(file) - const rows = gistData2Rows(yearMonth, gistData) - rows.filter(row => filterDate(row, start, end)) - .forEach(row => result.push(row)) + const gistData = await getJsonFileContent<GistData>(file) + const rows = gistData && gistData2Rows(yearMonth, gistData) + rows?.filter(row => filterDate(row, start, end)) + ?.forEach(row => result.push(row)) } })) return result @@ -118,7 +126,7 @@ export default class GistCoordinator implements timer.backup.Coordinator<Cache> files: files2Update, description: TIMER_DATA_GIST_DESC } - updateGist(context.auth?.token, gist.id, gist2update) + updateGist(checkTokenExist(context), gist.id, gist2update) } private isTargetMetaGist(gist: Gist): boolean { @@ -131,7 +139,7 @@ export default class GistCoordinator implements timer.backup.Coordinator<Cache> private async getMetaGist(context: timer.backup.CoordinatorContext<Cache>): Promise<Gist> { const gistId = context.cache.metaGistId - const token = context.auth?.token + const token = checkTokenExist(context) // 1. Find by id if (gistId) { const gist = await getGist(token, gistId) @@ -147,7 +155,7 @@ export default class GistCoordinator implements timer.backup.Coordinator<Cache> return anotherGist } // 3. Create new one - const files = {} + const files: Record<string, FileForm> = {} files[INIT_README_MD.filename] = INIT_README_MD files[INIT_CLIENT_JSON.filename] = INIT_CLIENT_JSON const gist2Create: GistForm = { description: TIMER_META_GIST_DESC, files, public: false } @@ -159,7 +167,7 @@ export default class GistCoordinator implements timer.backup.Coordinator<Cache> private async getStatGist(context: timer.backup.CoordinatorContext<Cache>): Promise<Gist> { const gistId = context.cache.statGistId - const token = context.auth?.token + const token = checkTokenExist(context) // 1. Find by id if (gistId) { const gist = await getGist(token, gistId) @@ -175,7 +183,7 @@ export default class GistCoordinator implements timer.backup.Coordinator<Cache> return anotherGist } // 3. Create new one - const files = {} + const files: Record<string, FileForm> = {} files[README_FILE_NAME] = INIT_README_MD const gist2Create: GistForm = { description: TIMER_DATA_GIST_DESC, files, public: false } const created = await createGist(token, gist2Create) @@ -184,8 +192,10 @@ export default class GistCoordinator implements timer.backup.Coordinator<Cache> return created } - testAuth(auth: timer.backup.Auth): Promise<string> { - return testToken(auth?.token) + async testAuth(auth: timer.backup.Auth): Promise<string | undefined> { + const { token } = auth + if (!token) return 'Token is empty' + return testToken(token) } async clear(context: timer.backup.CoordinatorContext<Cache>, client: timer.backup.Client): Promise<void> { @@ -196,13 +206,13 @@ export default class GistCoordinator implements timer.backup.Coordinator<Cache> const gist = await this.getStatGist(context) const deletingFileNames = Object.keys(gist?.files || {}).filter(fileName => allFileNames.includes(fileName)) // 2. delete - const files2Delete: { [filename: string]: FileForm } = {} + const files2Delete: { [filename: string]: FileForm | null } = {} deletingFileNames.forEach(fileName => files2Delete[fileName] = null) const gist2update: GistForm = { public: false, files: files2Delete, description: TIMER_DATA_GIST_DESC } - await updateGist(context.auth?.token, gist.id, gist2update) + await updateGist(checkTokenExist(context), gist.id, gist2update) } } diff --git a/src/service/backup/markdown.ts b/src/service/backup/markdown.ts index 771011a3e..2f3c9a961 100644 --- a/src/service/backup/markdown.ts +++ b/src/service/backup/markdown.ts @@ -19,10 +19,10 @@ const CLIENT_FIELDS: MarkdownTableField<timer.backup.Client>[] = [ formatter: r => r.name, }, { name: "Earliest Date", - formatter: r => r.minDate, + formatter: r => r.minDate ?? '', }, { name: "Latest Date", - formatter: r => r.maxDate, + formatter: r => r.maxDate ?? '', } ] @@ -102,12 +102,13 @@ export function divideByDate(rows: timer.core.Row[]): { [date: string]: string } return groupBy(rows, row => row.date, list => genMarkdownTable(list, ROW_FIELDS)) } -export function parseData<T>(markdown: string): T { +export function parseData<T>(markdown: string | undefined | null): T | undefined { + if (!markdown) return undefined let line2 = markdown?.split('\n')?.[1] if (!line2) { - return null + return } line2 = line2?.replace("<!-- ", "").replace("-->", "").trim() - if (!line2) return null + if (!line2) return return JSON.parse(line2) } \ No newline at end of file diff --git a/src/service/backup/obsidian/coordinator.ts b/src/service/backup/obsidian/coordinator.ts index 077055fe3..3a96a8eeb 100644 --- a/src/service/backup/obsidian/coordinator.ts +++ b/src/service/backup/obsidian/coordinator.ts @@ -14,9 +14,13 @@ import { CLIENT_FILE_NAME, convertClients2Markdown, divideByDate, parseData } fr function prepareContext(context: timer.backup.CoordinatorContext<never>) { const { auth, ext, cid } = context + const { token } = auth || {} + if (!token) { + throw new Error("Token must not be empty. This can't happen, please contact the developer") + } let { endpoint, dirPath, bucket } = ext || {} dirPath = processDir(dirPath) - const ctx: ObsidianRequestContext = { auth: auth?.token, endpoint, vault: bucket } + const ctx: ObsidianRequestContext = { auth: token, endpoint, vault: bucket } return { ctx, dirPath, cid } } @@ -49,7 +53,7 @@ export default class ObsidianCoordinator implements timer.backup.Coordinator<nev await Promise.all(dateIterator.toArray().map(async date => { const filePath = `${dirPath}${targetCid || cid}/${date}.md` const fileContent = await getFileContent(ctx, filePath) - const rows: timer.core.Row[] = parseData(fileContent) + const rows = parseData<timer.core.Row[]>(fileContent) rows?.forEach?.(row => result.push(row)) })) return result @@ -67,7 +71,7 @@ export default class ObsidianCoordinator implements timer.backup.Coordinator<nev ) } - async testAuth(authInfo: timer.backup.Auth, ext: timer.backup.TypeExt): Promise<string> { + async testAuth(authInfo: timer.backup.Auth, ext: timer.backup.TypeExt): Promise<string | undefined> { let { endpoint, dirPath, bucket } = ext || {} let { token: auth } = authInfo || {} dirPath = processDir(dirPath) @@ -87,13 +91,14 @@ export default class ObsidianCoordinator implements timer.backup.Coordinator<nev } return message } catch (e) { - const message = e.message?.toLowerCase?.() - if (message?.includes("failed to fetch")) { + const { message: errMsg } = e as Error + const lowerErrMsg = errMsg?.toLocaleLowerCase?.() + if (lowerErrMsg?.includes("failed to fetch")) { return "Unable to fetch this endpoint, please make sure it is accessible" - } else if (message?.includes("failed to parse url from")) { + } else if (lowerErrMsg?.includes("failed to parse url from")) { return "The endpoint is invalid, please check it" } - return e.message + return errMsg ?? e?.toString?.() ?? 'Unknown error' } } @@ -101,7 +106,7 @@ export default class ObsidianCoordinator implements timer.backup.Coordinator<nev const cid = client.id const { ctx, dirPath } = prepareContext(context) const clientDirPath = `${dirPath}${cid}/` - let files = [] + let files: string[] = [] try { const result = await listAllFiles(ctx, clientDirPath) files = result.files || [] diff --git a/src/service/backup/processor.ts b/src/service/backup/processor.ts index ab4c522da..a2cd4b8a2 100644 --- a/src/service/backup/processor.ts +++ b/src/service/backup/processor.ts @@ -29,7 +29,7 @@ export type AuthCheckResult = { class CoordinatorContextWrapper<Cache> implements timer.backup.CoordinatorContext<Cache> { auth: timer.backup.Auth ext?: timer.backup.TypeExt - cache: Cache + cache: Cache = {} as unknown as Cache type: timer.backup.Type cid: string @@ -79,13 +79,12 @@ function generateCid() { const uaData = (navigator as any)?.userAgentData as NavigatorUAData let prefix = 'unknown' if (uaData) { - const brand: string = uaData.brands + const brand = uaData.brands ?.map(e => e.brand) - ?.filter(brand => brand !== "Chromium" && !brand.includes("Not")) + ?.filter(brand => brand && brand !== "Chromium" && !brand.includes("Not")) ?.[0] ?.replace(' ', '-') - || undefined - const platform: string = uaData.platform + const platform = uaData.platform brand && platform && (prefix = `${platform.toLowerCase()}-${brand.toLowerCase()}`) } return prefix + '-' + new Date().getTime() @@ -123,7 +122,7 @@ async function syncFull( } } -function filterClient(c: timer.backup.Client, excludeLocal: boolean, localClientId: string, start: string, end: string) { +function filterClient(c: timer.backup.Client, excludeLocal: boolean, localClientId: string, start?: string, end?: string) { // Exclude local client if (excludeLocal && c.id === localClientId) return false // Judge range @@ -153,7 +152,7 @@ class Processor { constructor() { this.coordinators = { - none: undefined, + none: null as unknown as timer.backup.Coordinator<never>, gist: new GistCoordinator(), obsidian_local_rest_api: new ObsidianCoordinator(), web_dav: new WebDAVCoordinator(), @@ -184,7 +183,7 @@ class Processor { return success(now) } catch (e) { console.error("Error to sync data", e) - const msg = (e as Error)?.message || e + const msg = (e as Error)?.message ?? e?.toString?.() return error(msg) } } @@ -200,8 +199,8 @@ class Processor { async checkAuth(): Promise<AuthCheckResult> { const option = await optionHolder.get() - const type = option?.backupType || 'none' - const ext = option?.backupExts?.[type] + const { backupType: type, backupExts } = option + const ext = backupExts?.[type] ?? {} const auth = prepareAuth(option) const coordinator: timer.backup.Coordinator<unknown> = type && this.coordinators[type] @@ -209,7 +208,7 @@ class Processor { // no coordinator, do nothing return { option, auth, ext, type, coordinator, errorMsg: "Invalid type" } } - let errorMsg: string + let errorMsg try { errorMsg = await coordinator.testAuth(auth, ext) } catch (e) { @@ -225,14 +224,14 @@ class Processor { } const { start = getBirthday(), end, specCid, excludeLocal } = param - let localCid = await metaService.getCid() + let localCid = await lazyGetCid() // 1. init context const context: timer.backup.CoordinatorContext<unknown> = await new CoordinatorContextWrapper<unknown>(localCid, auth, ext, type).init() // 2. query all clients, and filter them let startStr = start ? formatTimeYMD(start) : undefined let endStr = end ? formatTimeYMD(end) : undefined const allClients = (await coordinator.listAllClients(context)) - .filter(c => filterClient(c, excludeLocal, localCid, startStr, endStr)) + .filter(c => filterClient(c, !!excludeLocal, localCid, startStr, endStr)) .filter(c => !specCid || c.id === specCid) // 3. iterate clients const result: timer.backup.Row[] = [] @@ -254,13 +253,13 @@ class Processor { async clear(cid: string): Promise<Result<void>> { const { auth, ext, type, coordinator, errorMsg } = await this.checkAuth() if (errorMsg) return error(errorMsg) - let localCid = await metaService.getCid() + let localCid = await lazyGetCid() const context: timer.backup.CoordinatorContext<unknown> = await new CoordinatorContextWrapper<unknown>(localCid, auth, ext, type).init() // 1. Find the client const allClients = await coordinator.listAllClients(context) const client = allClients?.filter(c => c?.id === cid)?.[0] if (!client) { - return + return success() } // 2. clear await coordinator.clear(context, client) diff --git a/src/service/backup/web-dav/coordinator.ts b/src/service/backup/web-dav/coordinator.ts index 535852a66..a8a040aa6 100644 --- a/src/service/backup/web-dav/coordinator.ts +++ b/src/service/backup/web-dav/coordinator.ts @@ -6,7 +6,7 @@ import DateIterator from "@util/date-iterator" import { processDir } from "../common" import { CLIENT_FILE_NAME, convertClients2Markdown, divideByDate, parseData } from "../markdown" -function getEndpoint(ext: timer.backup.TypeExt): string { +function getEndpoint(ext: timer.backup.TypeExt | undefined): string | undefined { let { endpoint } = ext || {} if (endpoint?.endsWith('/')) { endpoint = endpoint.substring(0, endpoint.length - 1) @@ -17,11 +17,14 @@ function getEndpoint(ext: timer.backup.TypeExt): string { function prepareContext(context: timer.backup.CoordinatorContext<never>): WebDAVContext { const { auth, ext } = context const endpoint = getEndpoint(ext) - const webDavAuth: WebDAVAuth = { - type: "password", - username: auth?.login?.acc, - password: auth?.login?.psw, + if (!endpoint) { + throw new Error('Endpoint must be empty. This can\'t happen, please contact the developer') } + const { acc: username, psw: password } = auth?.login || {} + if (!username || !password) { + throw new Error('Neither username nor password must be empty. This can\'t happen, please contact the developer') + } + const webDavAuth: WebDAVAuth = { type: "password", username, password } return { auth: webDavAuth, endpoint } } @@ -40,7 +43,7 @@ export default class WebDAVCoordinator implements timer.backup.Coordinator<never const davContext = prepareContext(context) try { const content = await readFile(davContext, clientFilePath) - return parseData(content) || [] + return parseData(content) ?? [] } catch (e) { console.warn("Failed to read WebDav file content", e) return [] @@ -57,7 +60,7 @@ export default class WebDAVCoordinator implements timer.backup.Coordinator<never await Promise.all(dateIterator.toArray().map(async date => { const filePath = `${dirPath}${targetCid}/${date}.md` const fileContent = await readFile(davContext, filePath) - const rows: timer.core.Row[] = parseData(fileContent) + const rows = parseData<timer.core.Row[]>(fileContent) rows?.forEach?.(row => result.push(row)) })) return result @@ -88,7 +91,7 @@ export default class WebDAVCoordinator implements timer.backup.Coordinator<never return clientDirPath } - async testAuth(auth: timer.backup.Auth, ext: timer.backup.TypeExt): Promise<string> { + async testAuth(auth: timer.backup.Auth, ext: timer.backup.TypeExt): Promise<string | undefined> { const endpoint = getEndpoint(ext) if (!endpoint) { return "The endpoint is blank" @@ -113,7 +116,7 @@ export default class WebDAVCoordinator implements timer.backup.Coordinator<never return "Directory not found" } } catch (e) { - return (e as Error)?.message || e + return (e as Error)?.message ?? e?.toString?.() ?? 'Unknown error' } } diff --git a/src/service/components/host-merge-ruler.ts b/src/service/components/host-merge-ruler.ts index e76472542..b8afce9a2 100644 --- a/src/service/components/host-merge-ruler.ts +++ b/src/service/components/host-merge-ruler.ts @@ -15,7 +15,7 @@ import { get } from "@util/psl" const getTheSuffix = (origin: string, dotCount: number) => { if (isIpAndPort(origin)) return origin - let result = [] + let result: string[] = [] while (true) { if (dotCount-- < 0) { break diff --git a/src/service/components/option-holder.ts b/src/service/components/option-holder.ts index b8529c181..9181c370d 100644 --- a/src/service/components/option-holder.ts +++ b/src/service/components/option-holder.ts @@ -1,10 +1,9 @@ import OptionDatabase from "@db/option-database" -import { defaultOption } from "@util/constant/option" +import { type DefaultOption, defaultOption } from "@util/constant/option" const db = new OptionDatabase(chrome.storage.local) -function migrateOld(result: timer.option.AllOption): timer.option.AllOption { - if (!result) return result +function migrateOld<T extends timer.option.AllOption>(result: T): T { const newRes = { ...result } const duration = newRes['defaultDuration'] if (duration as string === 'last30Days') { @@ -18,36 +17,34 @@ function migrateOld(result: timer.option.AllOption): timer.option.AllOption { type ChangeListener = (option: timer.option.AllOption) => void class OptionHolder { - private option: timer.option.AllOption + private option: DefaultOption | undefined private listeners: ChangeListener[] = [] constructor() { db.addOptionChangeListener(async () => { - await this.reset() - this.listeners.forEach(listener => listener?.(this.option)) + const option = await this.reset() + this.listeners.forEach(listener => listener?.(option)) }) } - private async reset(): Promise<void> { + private async reset(): Promise<DefaultOption> { const exist: Partial<timer.option.AllOption> = await db.getOption() - const result: timer.option.AllOption = defaultOption() - Object.entries(exist).forEach(([key, val]) => result[key] = val) + const result = defaultOption() + Object.entries(exist).forEach(([key, val]) => (result as any)[key] = val) const newVal = migrateOld(result) this.option = newVal + return newVal } - async get(): Promise<timer.option.AllOption> { - if (!this.option) { - await this.reset() - } - return this.option + async get(): Promise<DefaultOption> { + return this.option ?? await this.reset() } async set(option: Partial<timer.option.AllOption>): Promise<void> { const exist: Partial<timer.option.AllOption> = await db.getOption() const toSet = defaultOption() - Object.entries(exist).forEach(([key, val]) => toSet[key] = val) - Object.entries(option).forEach(([key, val]) => toSet[key] = val) + Object.entries(exist).forEach(([key, val]) => (toSet as any)[key] = val) + Object.entries(option).forEach(([key, val]) => (toSet as any)[key] = val) await db.setOption(toSet) } diff --git a/src/service/components/page-info.ts b/src/service/components/page-info.ts index 14675c693..a36fb7621 100644 --- a/src/service/components/page-info.ts +++ b/src/service/components/page-info.ts @@ -11,11 +11,10 @@ const DEFAULT_PAGE_SIZE = 10 /** * Slice the origin list to page */ -export function slicePageResult<T>(originList: T[], pageQuery: timer.common.PageQuery): timer.common.PageResult<T> { - let pageNum = pageQuery.num - let pageSize = pageQuery.size - pageNum === undefined || pageNum < 1 && (pageNum = DEFAULT_PAGE_NUM) - pageSize === undefined || pageSize < 1 && (pageSize = DEFAULT_PAGE_SIZE) +export function slicePageResult<T>(originList: T[], pageQuery?: timer.common.PageQuery): timer.common.PageResult<T> { + let { num: pageNum = DEFAULT_PAGE_NUM, size: pageSize = DEFAULT_PAGE_SIZE } = pageQuery || {} + pageNum < 1 && (pageNum = DEFAULT_PAGE_NUM) + pageSize < 1 && (pageSize = DEFAULT_PAGE_SIZE) const startIndex = (pageNum - 1) * pageSize const endIndex = (pageNum) * pageSize const total = originList.length diff --git a/src/service/components/period-calculator.ts b/src/service/components/period-calculator.ts index 564fd4139..29883c414 100644 --- a/src/service/components/period-calculator.ts +++ b/src/service/components/period-calculator.ts @@ -71,7 +71,7 @@ export function merge(periods: timer.period.Result[], config: MergeConfig): time let { start, end, periodSize } = config const map: Map<number, number> = new Map() periods.forEach(p => map.set(indexOf(p), p.milliseconds)) - let mills = [] + let mills: number[] = [] for (; compare(start, end) <= 0; start = after(start, 1)) { mills.push(map.get(indexOf(start)) ?? 0) const isEndOfWindow = (start.order % periodSize) === periodSize - 1 diff --git a/src/service/components/week-helper.ts b/src/service/components/week-helper.ts index 18b65b120..2f01f5fe1 100644 --- a/src/service/components/week-helper.ts +++ b/src/service/components/week-helper.ts @@ -2,7 +2,7 @@ import OptionDatabase from "@db/option-database" import { locale } from "@i18n" import { formatTimeYMD, getWeekDay, MILL_PER_DAY } from "@util/time" -function getRealWeekStart(weekStart: timer.option.WeekStartOption, locale: timer.Locale): number { +function getRealWeekStart(weekStart: timer.option.WeekStartOption | undefined, locale: timer.Locale): number { weekStart = weekStart ?? 'default' if (weekStart === 'default') { return locale === 'zh_CN' ? 0 : 6 @@ -22,7 +22,7 @@ function getRealWeekStart(weekStart: timer.option.WeekStartOption, locale: timer function getWeekTime(now: Date, weekStart: number): [Date, Date] { // Returns 0 - 6 means Monday to Sunday const weekDayNow = getWeekDay(now) - let start: Date = undefined + let start: Date | undefined = undefined if (weekDayNow === weekStart) { start = now } else if (weekDayNow < weekStart) { @@ -37,7 +37,7 @@ function getWeekTime(now: Date, weekStart: number): [Date, Date] { class WeekHelper { private optionDb = new OptionDatabase(chrome.storage.local) - private weekStart: timer.option.WeekStartOption + private weekStart: timer.option.WeekStartOption | undefined private initialized: boolean = false private async init(): Promise<void> { @@ -57,7 +57,7 @@ class WeekHelper { return getWeekTime(typeof now === 'number' ? new Date(now) : now, weekStart) } - private async getWeekStartOpt(): Promise<timer.option.WeekStartOption> { + private async getWeekStartOpt(): Promise<timer.option.WeekStartOption | undefined> { if (!this.initialized) { await this.init() } diff --git a/src/service/components/whitelist-holder.ts b/src/service/components/whitelist-holder.ts index b1adf5e8b..2a96b2523 100644 --- a/src/service/components/whitelist-holder.ts +++ b/src/service/components/whitelist-holder.ts @@ -15,8 +15,8 @@ const whitelistDatabase = new WhitelistDatabase(chrome.storage.local) * The singleton implementation of whitelist holder */ class WhitelistHolder { - private host: string[] - private virtual: RegExp[] + private host: string[] = [] + private virtual: RegExp[] = [] private postHandlers: (() => void)[] constructor() { diff --git a/src/service/limit-service/index.ts b/src/service/limit-service/index.ts index c5bf5c9e8..3512aeafa 100644 --- a/src/service/limit-service/index.ts +++ b/src/service/limit-service/index.ts @@ -67,17 +67,17 @@ async function selectEffective(url?: string) { async function noticeLimitChanged() { const effectiveItems = await selectEffective() const tabs = await listTabs() - tabs.forEach(tab => { - const limitedItems = effectiveItems.filter(item => matches(item?.cond, tab.url)) - sendMsg2Tab(tab?.id, 'limitChanged', limitedItems) - .catch(err => console.log(err.message)) + tabs.forEach(({ id, url }) => { + if (!id || !url) return + const limitedItems = effectiveItems.filter(item => matches(item?.cond, url)) + sendMsg2Tab(id, 'limitChanged', limitedItems).catch(err => console.log(err.message)) }) } async function updateEnabled(...items: timer.limit.Item[]): Promise<void> { if (!items?.length) return for (const item of items) { - await db.updateEnabled(item.id, item.enabled) + await db.updateEnabled(item.id, !!item.enabled) } await noticeLimitChanged() } @@ -85,7 +85,7 @@ async function updateEnabled(...items: timer.limit.Item[]): Promise<void> { async function updateDelay(...items: timer.limit.Item[]) { if (!items?.length) return for (const item of items) { - await db.updateDelay(item.id, item.allowDelay) + await db.updateDelay(item.id, !!item.allowDelay) } await noticeLimitChanged() } @@ -129,8 +129,8 @@ async function addFocusTime(host: string, url: string, focusTime: number): Promi const limited: timer.limit.Item[] = [] const needReminder: timer.limit.Item[] = [] - const { limitReminder, limitReminderDuration } = await optionHolder.get() - const durationMill = limitReminder ? (limitReminderDuration ?? 0) * MILL_PER_MINUTE : 0 + const { limitReminder, limitReminderDuration = 0 } = await optionHolder.get() + const durationMill = limitReminder ? limitReminderDuration * MILL_PER_MINUTE : 0 allEffective.forEach(item => { const [met, reminder] = addFocusForEach(item, focusTime, durationMill) met && limited.push(item) @@ -201,9 +201,10 @@ async function update(rule: timer.limit.Rule) { await noticeLimitChanged() } -async function create(rule: timer.limit.Rule) { - await db.save(rule, false) +async function create(rule: MakeOptional<timer.limit.Rule, 'id'>): Promise<number> { + const id = await db.save(rule, false) await noticeLimitChanged() + return id } class LimitService { diff --git a/src/service/limit-service/verification/processor.ts b/src/service/limit-service/verification/processor.ts index aae85aa2e..d2561e726 100644 --- a/src/service/limit-service/verification/processor.ts +++ b/src/service/limit-service/verification/processor.ts @@ -15,7 +15,7 @@ class VerificationProcessor { this.generators = ALL_GENERATORS } - generate(difficulty: timer.limit.VerificationDifficulty, locale: timer.Locale): VerificationPair { + generate(difficulty: timer.limit.VerificationDifficulty, locale: timer.Locale): VerificationPair | null { const context: VerificationContext = { difficulty, locale } const supported = this.generators.filter(g => g.supports(context)) const len = supported?.length diff --git a/src/service/meta-service.ts b/src/service/meta-service.ts index 449cdc970..3f2a05206 100644 --- a/src/service/meta-service.ts +++ b/src/service/meta-service.ts @@ -45,7 +45,7 @@ function increasePopup(): void { }) } -async function getCid(): Promise<string> { +async function getCid(): Promise<string | undefined> { const meta: timer.ExtensionMeta = await db.getMeta() return meta?.cid } @@ -68,7 +68,7 @@ async function updateBackUpTime(type: timer.backup.Type, time: number) { await db.update(meta) } -async function getLastBackUp(type: timer.backup.Type): Promise<{ ts: number, msg?: string }> { +async function getLastBackUp(type: timer.backup.Type): Promise<{ ts: number, msg?: string } | undefined> { const meta = await db.getMeta() return meta?.backup?.[type] } diff --git a/src/service/option-service.ts b/src/service/option-service.ts index cf351c15f..87595220d 100644 --- a/src/service/option-service.ts +++ b/src/service/option-service.ts @@ -18,11 +18,12 @@ async function setBackupOption(option: Partial<timer.option.BackupOption>): Prom const existOption = await optionHolder.get() const existAuths = existOption.backupAuths || {} const existExts = existOption.backupExts || {} - Object.entries(option.backupAuths || {}).forEach(([type, auth]) => existAuths[type] = auth) - Object.entries(option.backupExts || {}).forEach(([type, ext]) => { + Object.entries(option.backupAuths || {}).forEach(([key, auth]) => existAuths[key as timer.backup.Type] = auth) + Object.entries(option.backupExts || {}).forEach(([key, ext]) => { if (!ext) return + const type = key as timer.backup.Type const existExt = existExts[type] || {} - Object.entries(ext).forEach(([key, val]) => existExt[key] = val) + Object.entries(ext).forEach(([extKey, val]) => existExt[extKey as keyof timer.backup.TypeExt] = val) existExts[type] = existExt }) option.backupAuths = existAuths @@ -61,7 +62,7 @@ async function isDarkMode(targetVal?: timer.option.AppearanceOption): Promise<bo } async function setDarkMode(mode: timer.option.DarkMode, period?: [number, number]): Promise<void> { - const exist = await optionHolder.get() + const exist: timer.option.AllOption = await optionHolder.get() exist.darkMode = mode if (mode === 'timed') { const [start, end] = period || [] diff --git a/src/service/period-service.ts b/src/service/period-service.ts index 4a6d26b61..74c13e42d 100644 --- a/src/service/period-service.ts +++ b/src/service/period-service.ts @@ -1,6 +1,6 @@ /** * Copyright (c) 2021 Hengyang Zhang - * + * * This software is released under the MIT License. * https://opensource.org/licenses/MIT */ @@ -24,7 +24,7 @@ function add(timestamp: number, milliseconds: number): Promise<void> { } function dateStrBetween(startDate: timer.period.Key, endDate: timer.period.Key): string[] { - const result = [] + const result: string[] = [] while (compare(startDate, endDate) <= 0) { result.push(getDateString(startDate)) startDate = after(startDate, 1) @@ -33,7 +33,7 @@ function dateStrBetween(startDate: timer.period.Key, endDate: timer.period.Key): } -async function listBetween(param?: PeriodQueryParam): Promise<timer.period.Result[]> { +async function listBetween(param: PeriodQueryParam): Promise<timer.period.Result[]> { const [start, end] = param?.periodRange || [] const allDates = dateStrBetween(start, end) return periodDatabase.getBatch(allDates) diff --git a/src/service/site-service.ts b/src/service/site-service.ts index c82f8c04d..fe8b3e27a 100644 --- a/src/service/site-service.ts +++ b/src/service/site-service.ts @@ -23,12 +23,12 @@ async function removeAlias(key: timer.site.SiteKey) { await siteDatabase.save(exist) } -async function saveAlias(key: timer.site.SiteKey, alias: string) { +async function saveAlias(key: timer.site.SiteKey, alias: string, noRewrite?: boolean) { const exist = await siteDatabase.get(key) let toUpdate: timer.site.SiteInfo if (exist) { // Can't overwrite if alias is already existed - if (exist.alias) return + if (exist.alias && noRewrite) return toUpdate = exist toUpdate.alias = alias } else { @@ -37,7 +37,7 @@ async function saveAlias(key: timer.site.SiteKey, alias: string) { await siteDatabase.save(toUpdate) } -async function batchSaveAlias(siteMap: SiteMap<string>): Promise<void> { +async function batchSaveAliasNoRewrite(siteMap: SiteMap<string>): Promise<void> { if (!siteMap?.count?.()) return const allSites = await siteDatabase.getBatch(siteMap.keys()) const existMap = new SiteMap<timer.site.SiteInfo>() @@ -46,7 +46,7 @@ async function batchSaveAlias(siteMap: SiteMap<string>): Promise<void> { const toSave: timer.site.SiteInfo[] = [] siteMap.forEach((k, alias) => { const exist = existMap.get(k) - if (exist.alias || !alias) return + if (exist?.alias || !alias) return toSave.push({ ...exist || k, alias }) }) await siteDatabase.save(...toSave) @@ -78,9 +78,9 @@ async function saveRun(key: timer.site.SiteKey, run: boolean) { await siteDatabase.save(exist) // send msg to tabs const tabs = await listTabs() - for (const tab of tabs) { + for (const { id } of tabs) { try { - await sendMsg2Tab(tab.id, 'siteRunChange') + id && await sendMsg2Tab(id, 'siteRunChange') } catch { } } } @@ -113,20 +113,20 @@ class SiteService { } saveAlias = saveAlias - batchSaveAlias = batchSaveAlias + batchSaveAliasNoRewrite = batchSaveAliasNoRewrite removeAlias = removeAlias saveIconUrl = saveIconUrl removeIconUrl = removeIconUrl saveRun = saveRun - async saveCate(key: timer.site.SiteKey, cateId: number): Promise<void> { + async saveCate(key: timer.site.SiteKey, cateId: number | undefined): Promise<void> { if (!supportCategory(key)) return const exist = await siteDatabase.get(key) await siteDatabase.save({ ...exist || key, cate: cateId }) } - async batchSaveCate(cateId: number, keys: timer.site.SiteKey[]): Promise<void> { + async batchSaveCate(cateId: number | undefined, keys: timer.site.SiteKey[]): Promise<void> { keys = keys?.filter(supportCategory) if (!keys?.length) return diff --git a/src/service/stat-service/common.ts b/src/service/stat-service/common.ts index f0d1fc948..6f583d918 100644 --- a/src/service/stat-service/common.ts +++ b/src/service/stat-service/common.ts @@ -1,7 +1,6 @@ import { judgeVirtualFast } from "@util/pattern" export function cvt2StatRow(rowBase: timer.core.Row): timer.stat.Row { - if (!rowBase) return undefined const { host, ...otherFields } = rowBase return { siteKey: { host, type: judgeVirtualFast(host) ? 'virtual' : 'normal' }, diff --git a/src/service/stat-service/index.ts b/src/service/stat-service/index.ts index 7fa30b130..5c89265a9 100644 --- a/src/service/stat-service/index.ts +++ b/src/service/stat-service/index.ts @@ -64,7 +64,7 @@ export type StatQueryParam = StatCondition & { sortOrder?: SortDirect } -function cvtStatRow2BaseKey(statRow: timer.stat.Row): timer.core.RowKey { +function cvtStatRow2BaseKey(statRow: timer.stat.Row): timer.core.RowKey | undefined { const { siteKey, date } = statRow || {} if (!date) return undefined const { type, host } = siteKey || {} @@ -81,8 +81,9 @@ function extractAllSiteKeys(rows: timer.stat.Row[], container: timer.site.SiteKe } function fillRowWithSiteInfo(row: timer.stat.Row, siteMap: SiteMap<timer.site.SiteInfo>): void { - if (!row) return - const { siteKey, mergedRows } = row + const { siteKey, mergedRows } = row || {} + if (!siteKey) return + mergedRows?.map(m => fillRowWithSiteInfo(m, siteMap)) const siteInfo = siteMap.get(siteKey) if (siteInfo) { @@ -160,15 +161,20 @@ class StatService { return count } - private processSort(origin: timer.stat.Row[], param: StatQueryParam) { - const { sort, sortOrder } = param + private processSort(origin: timer.stat.Row[], param?: StatQueryParam) { + const { sort, sortOrder } = param || {} if (!sort) return const order = sortOrder || 'ASC' + const sortValue: (row: timer.stat.Row) => string | number | undefined = sort === 'host' + ? r => r.siteKey?.host ?? '' + : r => r[sort] ?? 0 origin.sort((a, b) => { - const aa = a[sort] - const bb = b[sort] + const aa = sortValue(a) + const bb = sortValue(b) if (aa === bb) return 0 + if (aa === undefined) return -1 + if (bb === undefined) return 1 return (order === 'ASC' ? 1 : -1) * (aa > bb ? 1 : -1) }) } @@ -193,7 +199,7 @@ class StatService { private async filterRows(param?: StatQueryParam): Promise<timer.stat.Row[]> { // Need match full host after merged - let fullHost = undefined + let fullHost: string | undefined = undefined // If merged and full host // Then set the host blank // And filter them after merge @@ -281,7 +287,7 @@ class StatService { async batchDelete(rows: timer.stat.Row[]) { if (!rows?.length) return - const baseKeys = rows.map(cvtStatRow2BaseKey) + const baseKeys: timer.core.RowKey[] = rows.map(cvtStatRow2BaseKey).filter(k => !!k) await this.batchDeleteBase(baseKeys) } diff --git a/src/service/stat-service/merge/cate.ts b/src/service/stat-service/merge/cate.ts index b4b18d23e..cfc03e017 100644 --- a/src/service/stat-service/merge/cate.ts +++ b/src/service/stat-service/merge/cate.ts @@ -2,7 +2,7 @@ import { CATE_NOT_SET_ID } from "@util/site" import { mergeResult } from "./common" export async function mergeCate(origin: timer.stat.Row[]): Promise<timer.stat.Row[]> { - const rowMap: Record<string, timer.stat.Row> = {} + const rowMap: Record<string, MakeRequired<timer.stat.Row, 'mergedRows'>> = {} origin?.forEach(ele => { let { siteKey, date, cateId } = ele || {} if (siteKey?.type !== 'normal') return diff --git a/src/service/stat-service/merge/common.ts b/src/service/stat-service/merge/common.ts index b2e3743cc..9b6eb5aca 100644 --- a/src/service/stat-service/merge/common.ts +++ b/src/service/stat-service/merge/common.ts @@ -7,7 +7,7 @@ type _RemoteCompositionMap = Record<'_' | string, timer.stat.RemoteCompositionVal> -function mergeComposition(c1: timer.stat.RemoteComposition, c2: timer.stat.RemoteComposition): timer.stat.RemoteComposition { +function mergeComposition(c1: timer.stat.RemoteComposition | undefined, c2: timer.stat.RemoteComposition | undefined): timer.stat.RemoteComposition { const focusMap: _RemoteCompositionMap = {} const timeMap: _RemoteCompositionMap = {} const runMap: _RemoteCompositionMap = {} diff --git a/src/service/stat-service/merge/date.ts b/src/service/stat-service/merge/date.ts index cb0752ede..291e686cc 100644 --- a/src/service/stat-service/merge/date.ts +++ b/src/service/stat-service/merge/date.ts @@ -2,7 +2,7 @@ import { identifySiteKey } from "@util/site" import { mergeResult } from "./common" export function mergeDate(origin: timer.stat.Row[]): timer.stat.Row[] { - const map: Record<string, timer.stat.Row> = {} + const map: Record<string, MakeRequired<timer.stat.Row, 'mergedDates' | 'mergedRows'>> = {} origin.forEach(ele => { const { date, siteKey, cateKey, iconUrl, alias, cateId } = ele || {} const key = [identifySiteKey(siteKey), cateKey?.toString?.() ?? ''].join('_') @@ -22,7 +22,7 @@ export function mergeDate(origin: timer.stat.Row[]): timer.stat.Row[] { if (ele.siteKey?.type === 'merged') { exist.mergedRows.push(...ele.mergedRows ?? []) } - exist.mergedDates.push(date) + date && exist.mergedDates.push(date) }) const newRows = Object.values(map) return newRows diff --git a/src/service/stat-service/merge/host.ts b/src/service/stat-service/merge/host.ts index f4084a3c1..dd82975ec 100644 --- a/src/service/stat-service/merge/host.ts +++ b/src/service/stat-service/merge/host.ts @@ -5,7 +5,7 @@ import { mergeResult } from "./common" const mergeRuleDatabase = new MergeRuleDatabase(chrome.storage.local) export async function mergeHost(origin: timer.stat.Row[]): Promise<timer.stat.Row[]> { - const map: Record<string, timer.stat.Row> = {} + const map: Record<string, MakeRequired<timer.stat.Row, 'mergedRows'>> = {} // Generate ruler const mergeRuleItems: timer.merge.Rule[] = await mergeRuleDatabase.selectAll() @@ -14,7 +14,7 @@ export async function mergeHost(origin: timer.stat.Row[]): Promise<timer.stat.Ro origin.forEach(ele => { const { siteKey, date } = ele || {} const { host, type } = siteKey || {} - if (type !== 'normal') return + if (type !== 'normal' || !host) return let mergedHost = mergeRuler.merge(host) const key = (date ?? '') + mergedHost let exist = map[key] diff --git a/src/service/stat-service/remote.ts b/src/service/stat-service/remote.ts index b2ff9fdfb..4ae979110 100644 --- a/src/service/stat-service/remote.ts +++ b/src/service/stat-service/remote.ts @@ -16,13 +16,13 @@ export async function processRemote(param: StatCondition, origin: timer.stat.Row return origin } // Map to merge - const originMap: Record<string, timer.stat.Row> = {} + const originMap: Record<string, MakeRequired<timer.stat.Row, 'composition'>> = {} origin.forEach(row => originMap[identifyStatKey(row)] = { ...row, composition: { focus: [row.focus], time: [row.time], - run: [row.run].filter(v => !!v), + run: row.run ? [row.run] : [], } }) // Predicate with host @@ -33,11 +33,11 @@ export async function processRemote(param: StatCondition, origin: timer.stat.Row // Full match ? r => r.host === host // Fuzzy match - : r => r.host && r.host.includes(host) + : r => !!r.host && r.host.includes(host) // Without host condition : _r => true // 1. query remote - let start: Date = undefined, end: Date = undefined + let start: Date | undefined = undefined, end: Date | undefined = undefined if (param.date instanceof Array) { start = param.date?.[0] end = param.date?.[1] @@ -62,7 +62,7 @@ export async function canReadRemote(): Promise<boolean> { return !errorMsg } -function processRemoteRow(rowMap: Record<string, timer.stat.Row>, remoteBase: timer.core.Row) { +function processRemoteRow(rowMap: Record<string, MakeRequired<timer.stat.Row, 'composition'>>, remoteBase: timer.core.Row) { const row = cvt2StatRow(remoteBase) const key = identifyStatKey(row) let exist = rowMap[key] @@ -76,14 +76,14 @@ function processRemoteRow(rowMap: Record<string, timer.stat.Row>, remoteBase: ti time: [], run: [], }, - } satisfies timer.stat.Row) + } satisfies MakeRequired<timer.stat.Row, 'composition'>) - const { focus = 0, time = 0, run = 0 } = row + const { focus = 0, time = 0, run = 0, cid = '', cname } = row exist.focus += focus exist.time += time run && (exist.run = run) - focus && exist.composition.focus.push({ cid: row.cid, cname: row.cname, value: focus }) - time && exist.composition.time.push({ cid: row.cid, cname: row.cname, value: time }) - run && exist.composition.run.push({ cid: row.cid, cname: row.cname, value: run }) + focus && exist.composition.focus.push({ cid, cname, value: focus }) + time && exist.composition.time.push({ cid, cname, value: time }) + run && exist.composition.run.push({ cid, cname, value: run }) } \ No newline at end of file diff --git a/src/util/array.ts b/src/util/array.ts index 92a23ba49..adce194ae 100644 --- a/src/util/array.ts +++ b/src/util/array.ts @@ -15,9 +15,9 @@ */ export function groupBy<T, R>( arr: T[], - keyFunc: (e: T, idx: number) => string | number, + keyFunc: (e: T, idx: number) => string | number | undefined | null, downstream: (grouped: T[], key: string) => R -): { [key: string]: R } { +): Record<string, R> { const groupedMap: { [key: string]: T[] } = {} arr.forEach((e, i) => { const key = keyFunc(e, i) @@ -28,7 +28,7 @@ export function groupBy<T, R>( existArr.push(e) groupedMap[key] = existArr }) - const result = {} + const result: Record<string, R> = {} Object.entries(groupedMap) .forEach(([key, grouped]) => result[key] = downstream(grouped, key)) return result @@ -48,9 +48,15 @@ export function rotate<T>(arr: T[], count?: number, rightOrLeft?: boolean): void } const operation = !!rightOrLeft // Right - ? (a: T[]) => a.unshift(a.pop()) + ? (a: T[]) => { + const first = a.pop() + first && a.unshift(first) + } // Left - : (a: T[]) => a.push(a.shift()) + : (a: T[]) => { + const last = a.shift() + last && a.push(last) + } for (; realTime > 0; realTime--) { operation(arr) } @@ -62,14 +68,14 @@ export function rotate<T>(arr: T[], count?: number, rightOrLeft?: boolean): void * @param arr target arr */ export function sum(arr: number[]): number { - return arr?.reduce?.((a, b) => (a || 0) + (b || 0), 0) ?? 0 + return arr?.reduce?.((a, b) => (a ?? 0) + (b ?? 0), 0) ?? 0 } /** * @since 2.1.0 * @returns null if arr is empty or null */ -export function average(arr: number[]): number { +export function average(arr: number[]): number | null { if (!arr?.length) return null return sum(arr) / arr.length } @@ -83,7 +89,7 @@ export function anyMatch<T>(arr: T[], predicate: (t: T) => boolean): boolean { } export function range(len: number): number[] { - const arr = [] + const arr: number[] = [] for (let i = 0; i < len; i++) { arr.push(i) } diff --git a/src/util/constant/environment.ts b/src/util/constant/environment.ts index e58402f24..ebf171824 100644 --- a/src/util/constant/environment.ts +++ b/src/util/constant/environment.ts @@ -14,32 +14,28 @@ let isChrome = false let isEdge = false let isOpera = false let isSafari = false -let browserMajorVersion = undefined +let browserMajorVersionStr: string | undefined if (/Firefox[\/\s](\d+\.\d+)/.test(userAgent)) { isFirefox = true - browserMajorVersion = /Firefox\/([0-9]+)/.exec(userAgent)?.[1] + browserMajorVersionStr = /Firefox\/([0-9]+)/.exec(userAgent)?.[1] } else if (userAgent.includes('Edg')) { // The Edge implements the chrome isEdge = true - browserMajorVersion = /Edg\/([0-9]+)/.exec(userAgent)?.[1] + browserMajorVersionStr = /Edg\/([0-9]+)/.exec(userAgent)?.[1] } else if (userAgent.includes("Opera") || userAgent.includes("OPR")) { // The Opera implements the chrome isOpera = true - browserMajorVersion = /OPR\/([0-9]+)/.exec(userAgent)?.[1] || /Opera\/([0-9]+)/.exec(userAgent)?.[1] + browserMajorVersionStr = /OPR\/([0-9]+)/.exec(userAgent)?.[1] || /Opera\/([0-9]+)/.exec(userAgent)?.[1] } else if (userAgent.includes('Safari') && !userAgent.includes('Chrome')) { // Chrome on macOs includes 'Safari' isSafari = true - browserMajorVersion = /Safari\/([0-9]+)/.exec(userAgent)?.[1] + browserMajorVersionStr = /Safari\/([0-9]+)/.exec(userAgent)?.[1] } else if (userAgent.includes('Chrome')) { isChrome = true - browserMajorVersion = /Chrome\/([0-9]+)/.exec(userAgent)?.[1] + browserMajorVersionStr = /Chrome\/([0-9]+)/.exec(userAgent)?.[1] } -try { - browserMajorVersion && (browserMajorVersion = Number.parseInt(browserMajorVersion)) -} catch (ignored) { } - export const IS_FIREFOX: boolean = isFirefox export const IS_EDGE: boolean = isEdge @@ -61,6 +57,11 @@ export const IS_OPERA: boolean = isOpera */ export const IS_SAFARI: boolean = isSafari +let browserMajorVersion: number | undefined = undefined +try { + browserMajorVersion = browserMajorVersionStr ? Number.parseInt(browserMajorVersionStr) : undefined +} catch (ignored) { } + /** * @since 1.3.2 */ diff --git a/src/util/constant/option.ts b/src/util/constant/option.ts index ced41650a..bf6e9dd69 100644 --- a/src/util/constant/option.ts +++ b/src/util/constant/option.ts @@ -5,7 +5,9 @@ * https://opensource.org/licenses/MIT */ -export function defaultPopup(): timer.option.PopupOption { +type PopupRequired = MakeRequired<timer.option.PopupOption, 'defaultDurationNum'> + +export function defaultPopup(): PopupRequired { // Use template return { popupMax: 10, @@ -20,7 +22,9 @@ export function defaultPopup(): timer.option.PopupOption { } } -export function defaultAppearance(): timer.option.AppearanceOption { +type AppearanceRequired = MakeRequired<timer.option.AppearanceOption, 'darkModeTimeStart' | 'darkModeTimeEnd'> + +export function defaultAppearance(): AppearanceRequired { return { displayWhitelistMenu: true, // Change false to true @since 0.8.4 @@ -38,7 +42,9 @@ export function defaultAppearance(): timer.option.AppearanceOption { } } -export function defaultStatistics(): timer.option.StatisticsOption { +type StatisticsRequired = MakeRequired<timer.option.StatisticsOption, 'weekStart'> + +export function defaultStatistics(): StatisticsRequired { return { autoPauseTracking: false, // 10 minutes @@ -49,7 +55,9 @@ export function defaultStatistics(): timer.option.StatisticsOption { } } -export function defaultDailyLimit(): timer.option.LimitOption { +type DailyLimitRequired = MakeRequired<timer.option.LimitOption, 'limitPassword' | 'limitVerifyDifficulty' | 'limitReminderDuration'> + +export function defaultDailyLimit(): DailyLimitRequired { return { limitLevel: 'nothing', limitPassword: '', @@ -77,7 +85,11 @@ export function defaultAccessibility(): timer.option.AccessibilityOption { } } -export function defaultOption(): timer.option.AllOption { +export type DefaultOption = + & PopupRequired & AppearanceRequired & StatisticsRequired & DailyLimitRequired + & timer.option.BackupOption & timer.option.AccessibilityOption + +export function defaultOption(): DefaultOption { return { ...defaultPopup(), ...defaultAppearance(), diff --git a/src/util/constant/remain-host.ts b/src/util/constant/remain-host.ts index 75bb75693..99242add3 100644 --- a/src/util/constant/remain-host.ts +++ b/src/util/constant/remain-host.ts @@ -17,7 +17,7 @@ export const LOCAL_HOST_PATTERN = "__local_*__" export const ALL_HOSTS = [PDF_HOST, JSON_HOST, TXT_HOST, PIC_HOST] export const MERGED_HOST = "__local_files__" -export const SUFFIX_HOST_MAP = { +export const SUFFIX_HOST_MAP: Record<string, string> = { txt: TXT_HOST, pdf: PDF_HOST, json: JSON_HOST, diff --git a/src/util/constant/url.ts b/src/util/constant/url.ts index f74d5a7fc..14562d192 100644 --- a/src/util/constant/url.ts +++ b/src/util/constant/url.ts @@ -13,28 +13,6 @@ export const FIREFOX_HOMEPAGE = 'https://addons.mozilla.org/firefox/addon/bestti export const CHROME_HOMEPAGE = 'https://chromewebstore.google.com/detail/time-tracker/dkdhhcbjijekmneelocdllcldcpmekmm' export const EDGE_HOMEPAGE = 'https://microsoftedge.microsoft.com/addons/detail/timer-the-web-time-is-e/fepjgblalcnepokjblgbgmapmlkgfahc' -let webstorePage = undefined -let reviewPage = undefined -if (IS_FIREFOX) { - webstorePage = FIREFOX_HOMEPAGE - reviewPage = FIREFOX_HOMEPAGE + "/reviews" -} else if (IS_CHROME) { - webstorePage = CHROME_HOMEPAGE - reviewPage = CHROME_HOMEPAGE + "/reviews" -} else if (IS_EDGE) { - webstorePage = reviewPage = EDGE_HOMEPAGE -} - -/** - * @since 0.0.5 - */ -export const WEBSTORE_PAGE = webstorePage - -/** - * @since 2.2.4 - */ -export const REVIEW_PAGE = reviewPage - /** * @since 0.4.0 */ @@ -147,3 +125,28 @@ export const CROWDIN_PROJECT_ID = 516822 * @since 1.4.0 */ export const CROWDIN_HOMEPAGE = 'https://crowdin.com/project/timer-chrome-edge-firefox' + +let webstorePage: string +let reviewPage: string +if (IS_FIREFOX) { + webstorePage = FIREFOX_HOMEPAGE + reviewPage = FIREFOX_HOMEPAGE + "/reviews" +} else if (IS_CHROME) { + webstorePage = CHROME_HOMEPAGE + reviewPage = CHROME_HOMEPAGE + "/reviews" +} else if (IS_EDGE) { + webstorePage = reviewPage = EDGE_HOMEPAGE +} else { + webstorePage = HOMEPAGE + reviewPage = CHROME_HOMEPAGE + "/reviews" +} + +/** + * @since 0.0.5 + */ +export const WEBSTORE_PAGE = webstorePage + +/** + * @since 2.2.4 + */ +export const REVIEW_PAGE = reviewPage diff --git a/src/util/dark-mode.ts b/src/util/dark-mode.ts index 9ab3f27af..3f694f446 100644 --- a/src/util/dark-mode.ts +++ b/src/util/dark-mode.ts @@ -32,7 +32,7 @@ export function init(el?: Element) { export function toggle(isDarkMode: boolean, el?: Element) { toggle0(isDarkMode, el) - localStorage.setItem(STORAGE_KEY, isDarkMode ? STORAGE_FLAG : undefined) + localStorage.setItem(STORAGE_KEY, isDarkMode ? STORAGE_FLAG : '') } export function isDarkMode() { diff --git a/src/util/date-iterator.ts b/src/util/date-iterator.ts index bd54932d7..2f0b97d2c 100644 --- a/src/util/date-iterator.ts +++ b/src/util/date-iterator.ts @@ -52,7 +52,7 @@ export default class DateIterator { } toArray(): string[] { - const result = [] + const result: string[] = [] this.forEach(yearMonth => result.push(yearMonth)) return result } diff --git a/src/util/echarts.ts b/src/util/echarts.ts index a389fd5b4..6a95230d7 100644 --- a/src/util/echarts.ts +++ b/src/util/echarts.ts @@ -95,7 +95,7 @@ export const processAnimation = (toProcess: unknown, duration: number) => { }) } -const processArrayLike = <T,>(arr: T | T[], processor: (t: T) => void) => { +const processArrayLike = <T,>(arr: T | T[] | undefined, processor: (t: T) => void) => { if (!arr) return if (Array.isArray(arr)) { arr.filter(e => !!e).forEach(processor) @@ -166,7 +166,7 @@ const processLineSeriesOption = (global: GlobalEcOption) => { } const processBarSeriesOption = (option: BarSeriesOption, global: GlobalEcOption) => { - const isHorizontal = someArrayLike(global.xAxis, x => x.type === 'value') + const isHorizontal = someArrayLike(global.xAxis, x => x?.type === 'value') if (isHorizontal) { // Let yAxis stay right processArrayLike(global.yAxis, yOpt => swapAlign(yOpt, 'position', 'left')) diff --git a/src/util/lang.ts b/src/util/lang.ts index b7f49933c..06de864ea 100644 --- a/src/util/lang.ts +++ b/src/util/lang.ts @@ -1,11 +1,11 @@ /** * @since 2.1.7 */ -export const deepCopy = <T = any>(obj: T): T => { - if (obj === null) return null +export const deepCopy = <T = any | null | undefined>(obj: T): T => { + if (!obj) return obj if (typeof obj !== 'object') return obj - let deep = {} + let deep: Record<string, any> = {} Object.entries(obj).forEach(([k, v]) => { if (typeof v !== "object" || v === null) { deep[k] = v diff --git a/src/util/limit.ts b/src/util/limit.ts index 6deafb580..62465c680 100644 --- a/src/util/limit.ts +++ b/src/util/limit.ts @@ -8,9 +8,14 @@ export function matches(cond: timer.limit.Item['cond'], url: string): boolean { ) } -export const meetLimit = (limit: number, value: number) => !!limit && !!value && value > limit +export const meetLimit = (limit: number | undefined, value: number | undefined): boolean => { + return !!limit && !!value && value > limit +} -export const meetTimeLimit = (limitSec: number, wastedMill: number, allowDelay: boolean, delayCount: number) => { +export const meetTimeLimit = ( + limitSec: number | undefined, wastedMill: number | undefined, + allowDelay: boolean | undefined, delayCount: number | undefined +): boolean => { let realLimit = (limitSec ?? 0) * MILL_PER_SECOND allowDelay && realLimit && (realLimit += DELAY_MILL * (delayCount ?? 0)) return meetLimit(realLimit, wastedMill) @@ -29,8 +34,8 @@ export function calcTimeState(item: timer.limit.Item, reminderMills: number): Li weekly: "NORMAL", } const { - time, waste = 0, delayCount = 0, - weekly, weeklyWaste = 0, weeklyDelayCount = 0, + time, waste, delayCount, + weekly, weeklyWaste, weeklyDelayCount, allowDelay, } = item || {} // 1. daily states @@ -55,24 +60,24 @@ export function hasLimited(item: timer.limit.Item): boolean { } export function hasDailyLimited(item: timer.limit.Item): boolean { - const { time, count, waste = 0, visit = 0, delayCount = 0, allowDelay } = item || {} + const { time, count = 0, waste = 0, visit = 0, delayCount = 0, allowDelay = false } = item || {} const timeMeet = meetTimeLimit(time, waste, allowDelay, delayCount) const countMeet = meetLimit(count, visit) return timeMeet || countMeet } export function hasWeeklyLimited(item: timer.limit.Item): boolean { - const { weekly, weeklyCount, weeklyWaste = 0, weeklyVisit = 0, weeklyDelayCount = 0, allowDelay } = item || {} + const { weekly = 0, weeklyCount = 0, weeklyWaste = 0, weeklyVisit = 0, weeklyDelayCount = 0, allowDelay = false } = item || {} const timeMeet = meetTimeLimit(weekly, weeklyWaste, allowDelay, weeklyDelayCount) const countMeet = meetLimit(weeklyCount, weeklyVisit) return timeMeet || countMeet } export function isEnabledAndEffective(rule: timer.limit.Rule): boolean { - return rule?.enabled && isEffective(rule) + return !!rule?.enabled && isEffective(rule) } -export function isEffective(rule: timer.limit.Rule): boolean { +export function isEffective(rule: timer.limit.Rule | undefined): boolean { if (!rule) return false const { weekdays } = rule @@ -84,7 +89,7 @@ export function isEffective(rule: timer.limit.Rule): boolean { return weekdays.includes(weekday) } -const idx2Str = (time: number): string => { +const idx2Str = (time: number | undefined): string => { time = time ?? 0 const hour = Math.floor(time / 60) const min = time - hour * 60 @@ -101,8 +106,7 @@ export const dateMinute2Idx = (date: Date): number => { return hour * 60 + min } -export const period2Str = (p: timer.limit.Period): string => { - const start = p?.[0] - const end = p?.[1] +export const period2Str = (p: timer.limit.Period | undefined): string => { + const [start, end] = p || [] return `${idx2Str(start)}-${idx2Str(end)}` } diff --git a/src/util/merge.ts b/src/util/merge.ts index f02a0ff56..4b8294cb1 100644 --- a/src/util/merge.ts +++ b/src/util/merge.ts @@ -6,7 +6,7 @@ function judgeAdded(target: Method, newVal: Method[], oldVal: Method[]): boolean return newVal?.includes?.(target) && !oldVal?.includes?.(target) } -export function processNewMethod(oldVal: Method[], newVal: Method[]): Method[] { +export function processNewMethod(oldVal: Method[] | undefined, newVal: Method[]): Method[] { oldVal = oldVal || [] if (judgeAdded('cate', newVal, oldVal)) { // Add cate, so remove domain diff --git a/src/util/month-iterator.ts b/src/util/month-iterator.ts index de729c4e4..a4a62e534 100644 --- a/src/util/month-iterator.ts +++ b/src/util/month-iterator.ts @@ -28,7 +28,7 @@ export default class MonthIterator { } } - next(): string { + next(): string | undefined { if (this.hasNext()) { const [year, month] = this.cursor const result = year.toString().padStart(4, '0') + (month + 1).toString().padStart(2, '0') @@ -42,13 +42,14 @@ export default class MonthIterator { } forEach(callback: (yearMonth: string) => void) { - while (this.hasNext()) { - callback(this.next()) + let next: string | undefined = undefined + while (next = this.next()) { + callback(next) } } toArray(): string[] { - const result = [] + const result: string[] = [] this.forEach(yearMonth => result.push(yearMonth)) return result } diff --git a/src/util/pattern.ts b/src/util/pattern.ts index fcc68f8ea..f74f3c86c 100644 --- a/src/util/pattern.ts +++ b/src/util/pattern.ts @@ -143,17 +143,17 @@ export function extractHostname(url: string): HostInfo { /** * @since 0.7.0 */ -export function extractFileHost(url: string): string { +export function extractFileHost(url: string): string | undefined { url = url?.trim?.() if (!url) { - return undefined + return } if (!url.startsWith("file://")) { - return undefined + return } const dotIdx = url.lastIndexOf(".") if (dotIdx < 0) { - return undefined + return } const suffix = url.substring(dotIdx + 1).toLowerCase() return suffix ? SUFFIX_HOST_MAP[suffix] : undefined diff --git a/src/util/period.ts b/src/util/period.ts index 02322fbe4..920000c44 100644 --- a/src/util/period.ts +++ b/src/util/period.ts @@ -132,7 +132,7 @@ function generateOrderMap(data: timer.period.Row[], periodSize: number): Map<num } function cvt2AverageResult(map: Map<number, number>, periodSize: number, dateNum: number): timer.period.Row[] { - const result = [] + const result: timer.period.Row[] = [] let period = keyOf(new Date(), 0) for (let i = 0; i < PERIOD_PER_DATE / periodSize; i++) { const key = period.order / periodSize diff --git a/src/util/site.ts b/src/util/site.ts index 14127049d..a188d5aa4 100644 --- a/src/util/site.ts +++ b/src/util/site.ts @@ -9,7 +9,7 @@ const SEPARATORS = /[-\|–_::,]/ const INVALID_SITE_NAME = /(登录)|(我的)|(个人)|(主页)|(首页)|(Welcome)/ -const SPECIAL_MAP = { +const SPECIAL_MAP: Record<string, string> = { // 哔哩哔哩 (゜-゜)つロ 干杯~-bilibili 'www.bilibili.com': 'bilibili' } @@ -21,8 +21,8 @@ const SPECIAL_MAP = { * @returns siteName, undefined if disable to detect * @since 0.5.1 */ -export function extractSiteName(title: string, host?: string) { - title = title?.trim?.() +export function extractSiteName(title: string, host?: string): string | undefined { + title = title.trim() if (!title) { return undefined } @@ -54,12 +54,12 @@ export function generateSiteLabel(host: string, name?: string): string { * * @since 3.0.0 */ -export function supportCategory(siteKey: timer.site.SiteKey): boolean { +export function supportCategory(siteKey: timer.site.SiteKey | undefined): boolean { const { type } = siteKey || {} return type === 'normal' } -export function siteEqual(a: timer.site.SiteKey, b: timer.site.SiteKey) { +export function siteEqual(a: timer.site.SiteKey | undefined, b: timer.site.SiteKey | undefined) { if (!a && !b) return true if (a === b) return true return a?.host === b?.host && a?.type === b?.type @@ -84,22 +84,22 @@ const PREFIX_TYPE_MAP: { [prefix in SiteIdentityPrefix]: timer.site.Type } = { v: 'virtual', } -export function identifySiteKey(site: timer.site.SiteKey): string { +export function identifySiteKey(site: timer.site.SiteKey | undefined): string { if (!site) return '' const { host, type } = site || {} return (TYPE_PREFIX_MAP[type] ?? ' ') + (host || '') } -export function parseSiteKeyFromIdentity(keyIdentity: string): timer.site.SiteKey { - const type = PREFIX_TYPE_MAP[keyIdentity?.charAt?.(0)] - if (!type) return null +export function parseSiteKeyFromIdentity(keyIdentity: string): timer.site.SiteKey | undefined { + const type = PREFIX_TYPE_MAP[keyIdentity?.charAt?.(0) as SiteIdentityPrefix] + if (!type) return const host = keyIdentity?.substring(1)?.trim?.() - if (!host) return null + if (!host) return return { type, host } } -function cloneSiteKey(origin: timer.site.SiteKey): timer.site.SiteKey { - if (!origin) return null +function cloneSiteKey(origin: timer.site.SiteKey | undefined): timer.site.SiteKey | undefined { + if (!origin) return return { host: origin.host, type: origin.type } } @@ -108,29 +108,30 @@ export function distinctSites(list: timer.site.SiteKey[]): timer.site.SiteKey[] list?.forEach(ele => { const key = identifySiteKey(ele) if (map[key]) return - map[key] = cloneSiteKey(ele) + const cloned = cloneSiteKey(ele) + cloned && (map[key] = cloned) }) return Object.values(map) } export class SiteMap<T> { - private innerMap: Record<string, [timer.site.SiteKey, T]> + private innerMap: Record<string, [timer.site.SiteKey, T | undefined]> constructor() { this.innerMap = {} } - public put(site: timer.site.SiteKey, t: T): void { + public put(site: timer.site.SiteKey, t: T | undefined): void { const key = identifySiteKey(site) this.innerMap[key] = [site, t] } - public get(site: timer.site.SiteKey): T { + public get(site: timer.site.SiteKey): T | undefined { const key = identifySiteKey(site) return this.innerMap[key]?.[1] } - public map<R>(mapper: (key: timer.site.SiteKey, value: T) => R): R[] { + public map<R>(mapper: (key: timer.site.SiteKey, value: T | undefined) => R): R[] { return Object.values(this.innerMap).map(([site, val]) => mapper?.(site, val)) } @@ -142,7 +143,7 @@ export class SiteMap<T> { return Object.values(this.innerMap).map(v => v[0]) } - public forEach(func: (k: timer.site.SiteKey, v: T, idx: number) => void) { + public forEach(func: (k: timer.site.SiteKey, v: T | undefined, idx: number) => void) { if (!func) return Object.values(this.innerMap).forEach(([k, v], idx) => func(k, v, idx)) } diff --git a/src/util/time.ts b/src/util/time.ts index 6115953a4..2a84c933a 100644 --- a/src/util/time.ts +++ b/src/util/time.ts @@ -39,7 +39,7 @@ export function formatTime(time: Date | string | number, cFormat?: string) { } date = new Date(time) } - const formatObj = { + const formatObj: Record<string, number> = { y: date.getFullYear(), m: date.getMonth() + 1, d: date.getDate(), @@ -229,7 +229,7 @@ export function getDayLength(dateStart: Date, dateEnd: Date): number { */ export function getAllDatesBetween(dateStart: Date, dateEnd: Date): string[] { let cursor = new Date(dateStart) - let dates = [] + let dates: string[] = [] do { dates.push(formatTimeYMD(cursor)) cursor = new Date(cursor.getTime() + MILL_PER_DAY) @@ -241,7 +241,7 @@ export function getAllDatesBetween(dateStart: Date, dateEnd: Date): string[] { /** * yyyyMMdd => Date */ -export function parseTime(dateStr: string): Date { +export function parseTime(dateStr: string | undefined): Date | undefined { if (!dateStr) return undefined const year = parseInt(dateStr.substring(0, 4)) const month = parseInt(dateStr.substring(4, 6)) diff --git a/src/util/tuple.ts b/src/util/tuple.ts index 6de107310..7d114d701 100644 --- a/src/util/tuple.ts +++ b/src/util/tuple.ts @@ -2,7 +2,7 @@ import { range } from "./array" const isTuple = (arg: unknown): arg is Tuple<any, never> => { if (Array.isArray(arg)) return true - if (!arg.hasOwnProperty?.("get")) return false + if (!(arg as Object).hasOwnProperty?.("get")) return false const predicate = arg as Tuple<any, never> const len = predicate?.length return typeof len === 'number' && !isNaN(len) && isFinite(len) && len >= 0 && Number.isInteger(len) diff --git a/test-e2e/common/base.ts b/test-e2e/common/base.ts index 3589b704e..7c594f011 100644 --- a/test-e2e/common/base.ts +++ b/test-e2e/common/base.ts @@ -3,12 +3,55 @@ import { E2E_OUTPUT_PATH } from "../../webpack/constant" const USE_HEADLESS_PUPPETEER = !!process.env['USE_HEADLESS_PUPPETEER'] -export type LaunchResult = { +export interface LaunchContext { browser: Browser extensionId: string + + close(): Promise<void> + + openAppPage(route: string): Promise<Page> + + newPage(url?: string): Promise<Page> + + newPageAndWaitCsInjected(url: string): Promise<Page> +} + +class LaunchContextWrapper implements LaunchContext { + browser: Browser + extensionId: string + + constructor(browser: Browser, extensionId: string) { + this.browser = browser + this.extensionId = extensionId + } + + close(): Promise<void> { + return this.browser.close() + } + + async openAppPage(route: string): Promise<Page> { + const page = await this.browser.newPage() + await page.goto(`chrome-extension://${this.extensionId}/static/app.html#${route}`) + return page + } + + async newPage(url?: string): Promise<Page> { + const page = await this.browser.newPage() + if (url) { + await page.goto(url, { waitUntil: 'load' }) + } + return page + } + + async newPageAndWaitCsInjected(url: string): Promise<Page> { + const page = await this.browser.newPage() + await page.goto(url) + await page.waitForSelector(`#__TIMER_INJECTION_FLAG__${this.extensionId}`) + return page + } } -export async function launchBrowser(dirPath?: string): Promise<LaunchResult> { +export async function launchBrowser(dirPath?: string): Promise<LaunchContext> { dirPath = dirPath ?? E2E_OUTPUT_PATH const browser = await launch({ @@ -23,33 +66,14 @@ export async function launchBrowser(dirPath?: string): Promise<LaunchResult> { }) const serviceWorker = await browser.waitForTarget(target => target.type() === 'service_worker') const url = serviceWorker.url() - let extensionId = url?.split?.('/')?.[2] + let extensionId: string | undefined = url.split('/')[2] if (!extensionId) { throw new Error('Failed to detect extension id') } - return { browser, extensionId } + return new LaunchContextWrapper(browser, extensionId) } export function sleep(seconds: number): Promise<void> { return new Promise(resolve => setTimeout(resolve, seconds * 1000)) -} - -export async function openAppPage(browser: Browser, extId: string, route: string): Promise<Page> { - const page = await browser.newPage() - await page.goto(`chrome-extension://${extId}/static/app.html#${route}`) - return page -} - -export async function newPage(browser: Browser, url: string): Promise<Page> { - const page = await browser.newPage() - await page.goto(url, { waitUntil: 'load' }) - return page -} - -export async function newPageAndWaitCsInjected(browser: Browser, extensionId: string, url: string) { - const page = await browser.newPage() - await page.goto(url) - await page.waitForSelector(`#__TIMER_INJECTION_FLAG__${extensionId}`) - return page } \ No newline at end of file diff --git a/test-e2e/common/record.ts b/test-e2e/common/record.ts index 2da677652..27eb18306 100644 --- a/test-e2e/common/record.ts +++ b/test-e2e/common/record.ts @@ -1,4 +1,5 @@ import type { Browser } from "puppeteer" +import { LaunchContext } from "./base" type RecordRow = { date: string @@ -14,24 +15,24 @@ function readRecords(): RecordRow[] { const rows = document.querySelectorAll('.el-table .el-table__body-wrapper table tbody tr') return Array.from(rows).map(row => { const cells = row.querySelectorAll('td') - const date = cells[1].textContent - const url = cells[2].textContent - const name = cells[3].textContent - const category = cells[4].textContent - const time = cells[5].textContent - let runTime = undefined, visit = undefined + const date = cells[1].textContent ?? '' + const url = cells[2].textContent ?? '' + const name = cells[3].textContent ?? '' + const category = cells[4].textContent ?? '' + const time = cells[5].textContent ?? '' + let runTime: string | undefined = undefined, visit = '' if (cells?.length === 9) { // Including run time - runTime = cells[6].textContent - visit = cells[7].textContent + runTime = cells[6].textContent ?? undefined + visit = cells[7].textContent ?? '' } else { - visit = cells[6].textContent + visit = cells[6].textContent ?? '' } return { date, url, name, category, time, runTime, visit } }) } -export function parseTime2Sec(timeStr: string): number { +export function parseTime2Sec(timeStr: string | undefined): number | undefined { if (!timeStr || timeStr === '-') return undefined const regRes = /^(\s*(?<hour>\d+)\s*h)?(\s*(?<min>\d+)\s*m)?(\s*(?<sec>\d+)\s*s)$/.exec(timeStr) if (!regRes) return NaN @@ -42,9 +43,8 @@ export function parseTime2Sec(timeStr: string): number { return res } -export async function readRecordsOfFirstPage(browser: Browser, extensionId: string) { - const recordPage = await browser.newPage() - await recordPage.goto(`chrome-extension://${extensionId}/static/app.html#/data/report`) +export async function readRecordsOfFirstPage(context: LaunchContext) { + const recordPage = await context.openAppPage('/data/report') // At least one record await recordPage.waitForSelector('.el-table .el-table__body-wrapper table tbody tr td') let records = await recordPage.evaluate(readRecords) diff --git a/test-e2e/common/whitelist.ts b/test-e2e/common/whitelist.ts index a8480dd6f..4d9f6bae6 100644 --- a/test-e2e/common/whitelist.ts +++ b/test-e2e/common/whitelist.ts @@ -1,23 +1,22 @@ import { type Browser } from "puppeteer" -import { sleep } from "./base" +import { LaunchContext, sleep } from "./base" -export async function createWhitelist(browser: Browser, extensionId: string, white: string) { - const whitePage = await browser.newPage() - await whitePage.goto(`chrome-extension://${extensionId}/static/app.html#/additional/whitelist`) +export async function createWhitelist(context: LaunchContext, white: string) { + const whitePage = await context.openAppPage('/additional/whitelist') const btn = await whitePage.waitForSelector('.item-add-button') - await btn.click() + await btn?.click() await sleep(.2) const input = await whitePage.$('.item-input-container:not([style*="display: none"]) input') - await input.focus() + await input?.focus() await whitePage.keyboard.type(white) await sleep(.4) const selectItem = await whitePage.waitForSelector('.el-popper .el-select-dropdown li:nth-child(2)') - await selectItem.click() + await selectItem?.click() await whitePage.click('.item-input-container:not([style*="display: none"]) .item-check-button.editable-item') const checkBtn = await whitePage.waitForSelector('.el-overlay.is-message-box .el-button.el-button--primary') - await checkBtn.click() + await checkBtn?.click() await sleep(.2) await whitePage.close() diff --git a/test-e2e/install.test.ts b/test-e2e/install.test.ts index 22201f431..bb774d355 100644 --- a/test-e2e/install.test.ts +++ b/test-e2e/install.test.ts @@ -1,18 +1,19 @@ import { join } from "path" -import { launchBrowser, type LaunchResult } from "./common/base" +import { launchBrowser, type LaunchContext } from "./common/base" -let launchRes: LaunchResult +let context: LaunchContext describe('After installed', () => { beforeEach(async () => { const path = join(__dirname, '..', 'dist_prod') - launchRes = await launchBrowser(path) + context = await launchBrowser(path) }) + afterEach(async () => context.close()) + test('Open the official page', async () => { - const { browser } = launchRes + const { browser } = context await browser.waitForTarget(target => target.url().includes('wfhg.cc')) - await launchRes?.browser.close() }, 5000) }) diff --git a/test-e2e/limit/common.ts b/test-e2e/limit/common.ts index 797c5d744..70763b15f 100644 --- a/test-e2e/limit/common.ts +++ b/test-e2e/limit/common.ts @@ -3,23 +3,23 @@ import { sleep } from "../common/base" export async function createLimitRule(rule: timer.limit.Rule, page: Page) { const createButton = await page.$('.el-card:first-child .el-button:last-child') - await createButton.click() + await createButton!.click() // 1 Fill the name await page.waitForSelector('.el-dialog .el-input input') const nameInput = await page.$('.el-dialog .el-input input') - await nameInput.focus() + await nameInput!.focus() page.keyboard.type(rule.name) await new Promise(resolve => setTimeout(resolve, 400)) await page.click('.el-dialog .el-button.el-button--primary') // 2. Fill the condition const configInput = await page.$('.el-dialog .el-input.el-input-group input') for (const url of rule.cond || []) { - await configInput.focus() + await configInput!.focus() await page.keyboard.type(url) await new Promise(resolve => setTimeout(resolve, 100)) await page.keyboard.press('Enter') const saveBtn = await page.$('.el-dialog .el-link.el-link--primary') - await saveBtn.click() + await saveBtn!.click() } await sleep(.1) await page.click('.el-dialog .el-button.el-button--primary') @@ -27,12 +27,12 @@ export async function createLimitRule(rule: timer.limit.Rule, page: Page) { await sleep(.1) const { time, weekly, visitTime, count, weeklyCount } = rule || {} const timeInputs = await page.$$('.el-dialog .el-date-editor input') - await fillTimeLimit(time, timeInputs[0], page) - await fillTimeLimit(weekly, timeInputs[1], page) - await fillTimeLimit(visitTime, timeInputs[2], page) + await fillTimeLimit(time!, timeInputs[0], page) + await fillTimeLimit(weekly!, timeInputs[1], page) + await fillTimeLimit(visitTime!, timeInputs[2], page) const visitInputs = await page.$$('.el-dialog .el-input-number input') - await fillVisitLimit(count, visitInputs[0], page) - await fillVisitLimit(weeklyCount, visitInputs[1], page) + await fillVisitLimit(count!, visitInputs[0], page) + await fillVisitLimit(weeklyCount!, visitInputs[1], page) // 4. Save await page.click('.el-dialog .el-button.el-button--success') @@ -47,13 +47,13 @@ export async function fillTimeLimit(value: number, input: ElementHandle<HTMLInpu await input.click() await sleep(.5) const panel = await page.$('.el-popper div.el-time-panel') - await panel.evaluate(async (el, hour, minute, second) => { + await panel!.evaluate(async (el, hour, minute, second) => { const hourSpinner = el.querySelector('.el-scrollbar:first-child .el-scrollbar__wrap') - hourSpinner.scrollTo(0, hour * 32) + hourSpinner!.scrollTo(0, hour * 32) const minuteSpinner = el.querySelector('.el-scrollbar:nth-child(2) .el-scrollbar__wrap') - minuteSpinner.scrollTo(0, minute * 32) + minuteSpinner!.scrollTo(0, minute * 32) const secondSpinner = el.querySelector('.el-scrollbar:nth-child(3) .el-scrollbar__wrap') - secondSpinner.scrollTo(0, second * 32) + secondSpinner!.scrollTo(0, second * 32) // Wait scroll handler finished await new Promise(resolve => setTimeout(resolve, 250)) const confirmBtn = el.querySelector('.el-time-panel__footer .el-time-panel__btn.confirm') as HTMLButtonElement diff --git a/test-e2e/limit/daily-time.test.ts b/test-e2e/limit/daily-time.test.ts index dd00792b9..124bbc88d 100644 --- a/test-e2e/limit/daily-time.test.ts +++ b/test-e2e/limit/daily-time.test.ts @@ -1,31 +1,22 @@ -import { type Browser } from "puppeteer" -import { launchBrowser, newPageAndWaitCsInjected, openAppPage, sleep } from "../common/base" +import { launchBrowser, type LaunchContext, sleep } from "../common/base" import { createLimitRule, fillTimeLimit } from "./common" -let browser: Browser, extensionId: string +let context: LaunchContext describe('Daily time limit', () => { - beforeEach(async () => { - const launchRes = await launchBrowser() - browser = launchRes.browser - extensionId = launchRes.extensionId - }) + beforeEach(async () => context = await launchBrowser()) - afterEach(async () => { - await browser.close() - browser = undefined - extensionId = undefined - }) + afterEach(() => context.close()) test('basic', async () => { - const limitPage = await openAppPage(browser, extensionId, '/behavior/limit') - const demoRule: timer.limit.Rule = { name: 'TEST DAILY LIMIT', cond: ['https://www.baidu.com'], time: 5 } + const limitPage = await context.openAppPage('/behavior/limit') + const demoRule: timer.limit.Rule = { id: 1, name: 'TEST DAILY LIMIT', cond: ['https://www.baidu.com'], time: 5 } // 1. Insert limit rule await createLimitRule(demoRule, limitPage) // 2. Open test page - const testPage = await newPageAndWaitCsInjected(browser, extensionId, 'https://www.baidu.com') + const testPage = await context.newPageAndWaitCsInjected('https://www.baidu.com') await sleep(4) // Assert not limited @@ -34,8 +25,8 @@ describe('Daily time limit', () => { await sleep(.1) let wastedTime = await limitPage.evaluate(() => { const timeTag = document.querySelector('.el-table .el-table__body-wrapper table tbody tr td:nth-child(6) .el-tag:first-child') - const timeStr = timeTag.textContent - return parseInt(timeStr.replace('s', '').trim()) + const timeStr = timeTag?.textContent + return parseInt(timeStr?.replace('s', '')?.trim() ?? '0') }) expect(wastedTime >= 4).toBe(true) @@ -46,11 +37,11 @@ describe('Daily time limit', () => { // 4. Limited const { name, time } = await testPage.evaluate(async () => { const shadow = document.querySelector('extension-time-tracker-overlay') - const descEl = shadow.shadowRoot.querySelector('#app .el-descriptions:not([style*="display: none"])') - const trs = descEl.querySelectorAll('tr') - const name = trs[0].querySelector('td:nth-child(2)').textContent - const timeStr = trs[2].querySelector('td:nth-child(2) .el-tag--danger').textContent - return { name, time: parseInt(timeStr.replace('s', '').trim()) } + const descEl = shadow?.shadowRoot?.querySelector('#app .el-descriptions:not([style*="display: none"])') + const trs = descEl?.querySelectorAll('tr') + const name = trs?.[0]?.querySelector('td:nth-child(2)')?.textContent + const timeStr = trs?.[2]?.querySelector('td:nth-child(2) .el-tag--danger')?.textContent + return { name, time: parseInt(timeStr?.replace('s', '').trim() ?? '0') } }) expect(name).toEqual(demoRule.name) expect(time >= 5).toBeTruthy() @@ -60,8 +51,8 @@ describe('Daily time limit', () => { await sleep(.1) wastedTime = await limitPage.evaluate(() => { const timeTag = document.querySelector('.el-table .el-table__body-wrapper table tbody tr td:nth-child(6) .el-tag--danger') - const timeStr = timeTag.textContent - return parseInt(timeStr.replace('s', '').trim()) + const timeStr = timeTag?.textContent + return parseInt(timeStr?.replace('s', '').trim() ?? '') }) expect(wastedTime).toEqual(5) @@ -76,7 +67,7 @@ describe('Daily time limit', () => { await sleep(.1) const timeInput = await limitPage.$('.el-dialog .el-date-editor:first-child input') - await fillTimeLimit(10, timeInput, limitPage) + await fillTimeLimit(10, timeInput!, limitPage) await limitPage.click('.el-dialog .el-button.el-button--success') // 7. Modal disappear @@ -85,7 +76,7 @@ describe('Daily time limit', () => { const modalExist = await testPage.evaluate(() => { const shadow = document.querySelector('extension-time-tracker-overlay') if (!shadow) return false - return !!shadow.shadowRoot.querySelector('body:not([style*="display: none"])') + return !!shadow.shadowRoot!.querySelector('body:not([style*="display: none"])') }) expect(modalExist).toBeFalsy() }, 60000) diff --git a/test-e2e/limit/daily-visit.test.ts b/test-e2e/limit/daily-visit.test.ts index 6f5eba84d..d110d5c3f 100644 --- a/test-e2e/limit/daily-visit.test.ts +++ b/test-e2e/limit/daily-visit.test.ts @@ -1,31 +1,22 @@ -import { type Browser } from "puppeteer" -import { launchBrowser, newPageAndWaitCsInjected, openAppPage, sleep } from "../common/base" +import { launchBrowser, type LaunchContext, sleep } from "../common/base" import { createLimitRule } from "./common" -let browser: Browser, extensionId: string +let context: LaunchContext describe('Daily time limit', () => { - beforeEach(async () => { - const launchRes = await launchBrowser() - browser = launchRes.browser - extensionId = launchRes.extensionId - }) + beforeEach(async () => context = await launchBrowser()) - afterEach(async () => { - await browser.close() - browser = undefined - extensionId = undefined - }) + afterEach(() => context.close()) test("Daily visit limit", async () => { - const limitPage = await openAppPage(browser, extensionId, '/behavior/limit') - const demoRule: timer.limit.Rule = { name: 'TEST DAILY LIMIT', cond: ['https://www.baidu.com'], time: 0, count: 1 } + const limitPage = await context.openAppPage('/behavior/limit') + const demoRule: timer.limit.Rule = { id: 1, name: 'TEST DAILY LIMIT', cond: ['https://www.baidu.com'], time: 0, count: 1 } // 1. Insert limit rule await createLimitRule(demoRule, limitPage) // 2. Open test page - const testPage = await newPageAndWaitCsInjected(browser, extensionId, 'https://www.baidu.com') + const testPage = await context.newPageAndWaitCsInjected('https://www.baidu.com') // Assert not limited await limitPage.bringToFront() @@ -43,15 +34,15 @@ describe('Daily time limit', () => { const { name, count } = await testPage.evaluate(async () => { const shadow = document.querySelector('extension-time-tracker-overlay') if (!shadow) return {} - const descEl = shadow.shadowRoot.querySelector('#app .el-descriptions:not([style*="display: none"])') - const trs = descEl.querySelectorAll('tr') - const name = trs[0].querySelector('td:nth-child(2)').textContent - const count = trs[2].querySelector('td:nth-child(2) .el-tag--danger').textContent + const descEl = shadow!.shadowRoot!.querySelector('#app .el-descriptions:not([style*="display: none"])') + const trs = descEl!.querySelectorAll('tr') + const name = trs[0].querySelector('td:nth-child(2)')!.textContent + const count = trs[2].querySelector('td:nth-child(2) .el-tag--danger')!.textContent return { name, count } }) expect(name).toBe(demoRule.name) - expect(count.split?.(' ')[0]).toBe('2') + expect(count!.split?.(' ')[0]).toBe('2') // 4. Change visit limit await limitPage.bringToFront() @@ -65,7 +56,7 @@ describe('Daily time limit', () => { await sleep(.1) const visitInput = await limitPage.$('.el-dialog .el-input-number input') - await visitInput.focus() + await visitInput!.focus() await limitPage.keyboard.type('2') await limitPage.click('.el-dialog .el-button.el-button--success') @@ -75,7 +66,7 @@ describe('Daily time limit', () => { const modalExist = await testPage.evaluate(() => { const shadow = document.querySelector('extension-time-tracker-overlay') if (!shadow) return false - return !!shadow.shadowRoot.querySelector('body:not([style*="display: none"])') + return !!shadow!.shadowRoot!.querySelector('body:not([style*="display: none"])') }) expect(modalExist).toBeFalsy() }, 60000) diff --git a/test-e2e/tracker/base.test.ts b/test-e2e/tracker/base.test.ts index 37854312d..65716dd01 100644 --- a/test-e2e/tracker/base.test.ts +++ b/test-e2e/tracker/base.test.ts @@ -1,27 +1,18 @@ -import { type Browser } from "puppeteer" -import { launchBrowser, newPageAndWaitCsInjected, sleep } from "../common/base" +import { launchBrowser, type LaunchContext, sleep } from "../common/base" import { readRecordsOfFirstPage } from "../common/record" import { createWhitelist } from "../common/whitelist" -let browser: Browser, extensionId: string +let context: LaunchContext describe('Tracking', () => { - beforeEach(async () => { - const launchRes = await launchBrowser() - browser = launchRes.browser - extensionId = launchRes.extensionId - }) + beforeEach(async () => context = await launchBrowser()) - afterEach(async () => { - await browser.close() - browser = undefined - extensionId = undefined - }) + afterEach(() => context.close()) test('basic tracking', async () => { - const page = await newPageAndWaitCsInjected(browser, extensionId, 'https://www.google.com') + const page = await context.newPageAndWaitCsInjected('https://www.google.com') await sleep(2) - let records = await readRecordsOfFirstPage(browser, extensionId) + let records = await readRecordsOfFirstPage(context) expect(records.length).toEqual(1) const { visit: visitStr, time: timeStr } = records[0] @@ -35,16 +26,16 @@ describe('Tracking', () => { await page.bringToFront() await page.goto('https://www.baidu.com') - records = await readRecordsOfFirstPage(browser, extensionId) + records = await readRecordsOfFirstPage(context) expect(records.length).toEqual(2) const urls = records.map(r => r.url) expect(urls.includes('baidu.com') || urls.includes('www.baidu.com')) }, 60000) test('white list', async () => { - const page = await newPageAndWaitCsInjected(browser, extensionId, 'https://www.google.com') + const page = await context.newPageAndWaitCsInjected('https://www.google.com') await sleep(2) - let records = await readRecordsOfFirstPage(browser, extensionId) + let records = await readRecordsOfFirstPage(context) expect(records.length).toEqual(1) const { visit: visitStr, time: timeStr } = records[0] @@ -54,11 +45,11 @@ describe('Tracking', () => { const time = parseInt(timeStr.replace('s', '').trim()) expect(time >= 2) - await createWhitelist(browser, extensionId, 'www.google.com') + await createWhitelist(context, 'www.google.com') await page.bringToFront() await page.reload() await sleep(2) - records = await readRecordsOfFirstPage(browser, extensionId) + records = await readRecordsOfFirstPage(context) expect(records.length).toEqual(1) expect(records[0].time).toEqual(timeStr) expect(records[0].visit).toEqual("1") diff --git a/test-e2e/tracker/common.ts b/test-e2e/tracker/common.ts deleted file mode 100644 index e69de29bb..000000000 diff --git a/test-e2e/tracker/run-time.test.ts b/test-e2e/tracker/run-time.test.ts index 846bd61df..6bdee856e 100644 --- a/test-e2e/tracker/run-time.test.ts +++ b/test-e2e/tracker/run-time.test.ts @@ -1,43 +1,33 @@ -import { type Browser } from "puppeteer" -import { launchBrowser, newPage, newPageAndWaitCsInjected, sleep } from "../common/base" +import { launchBrowser, sleep, type LaunchContext } from "../common/base" import { parseTime2Sec, readRecordsOfFirstPage } from "../common/record" import { createWhitelist } from "../common/whitelist" -let browser: Browser, extensionId: string +let context: LaunchContext async function clickRunTimeChange(siteHost: string): Promise<void> { - const sitePage = await browser.newPage() - await sitePage.goto(`chrome-extension://${extensionId}/static/app.html#/additional/site-manage`, { waitUntil: 'domcontentloaded' }) + const sitePage = await context.openAppPage("/additional/site-manage") await sitePage.focus('.filter-container input') await sitePage.keyboard.type(siteHost) await sitePage.keyboard.press('Enter') await sleep(.1) await sitePage.evaluate(() => { - const runTimeSwitch: HTMLDivElement = document.querySelector('#site-manage-table-wrapper table > tbody > tr > td.el-table_1_column_7 .el-switch') - runTimeSwitch.click() + const runTimeSwitch = document.querySelector<HTMLDivElement>('#site-manage-table-wrapper table > tbody > tr > td.el-table_1_column_7 .el-switch') + runTimeSwitch?.click() }) await sleep(.2) await sitePage.close() } describe('Run time tracking', () => { - beforeEach(async () => { - const launchRes = await launchBrowser() - browser = launchRes.browser - extensionId = launchRes.extensionId - }) + beforeEach(async () => context = await launchBrowser()) - afterEach(async () => { - await browser.close() - browser = undefined - extensionId = undefined - }) + afterEach(() => context.close()) test('Basically track', async () => { - const page = await browser.newPage() + const page = await context.newPage() await page.goto('https://www.baidu.com', { waitUntil: 'load' }) await sleep(1) - let records = await readRecordsOfFirstPage(browser, extensionId) + let records = await readRecordsOfFirstPage(context) let record = records[0] expect(parseTime2Sec(record.time)).toBeGreaterThanOrEqual(1) expect(record.runTime).toBeFalsy() @@ -47,20 +37,20 @@ describe('Run time tracking', () => { await clickRunTimeChange('www.baidu.com') // 2. Sleep - const emptyPage = await browser.newPage() + const emptyPage = await context.newPage() await sleep(2) - records = await readRecordsOfFirstPage(browser, extensionId) - const runTime1 = parseTime2Sec(records[0].runTime) + records = await readRecordsOfFirstPage(context) + const runTime1 = parseTime2Sec(records[0]?.runTime) ?? 0 expect(runTime1).toBeGreaterThanOrEqual(2) // 3. Add another page sharing the same run time with old page - await newPageAndWaitCsInjected(browser, extensionId, 'https://www.baidu.com') + await context.newPageAndWaitCsInjected('https://www.baidu.com') // jump to new page await emptyPage.bringToFront() await sleep(2) - records = await readRecordsOfFirstPage(browser, extensionId) + records = await readRecordsOfFirstPage(context) const runTime2 = parseTime2Sec(records[0].runTime) expect(runTime2).toBeGreaterThanOrEqual(runTime1 + 2) expect(runTime2).toBeLessThan((Date.now() - enableTs) / 1000) @@ -70,31 +60,31 @@ describe('Run time tracking', () => { const disableTs = Date.now() await emptyPage.bringToFront() await sleep(4) - records = await readRecordsOfFirstPage(browser, extensionId) + records = await readRecordsOfFirstPage(context) const runTime3 = parseTime2Sec(records[0].runTime) expect(runTime3).toBeLessThanOrEqual((disableTs - enableTs) / 1000) }, 60000) - test.only('white list', async () => { - await newPage(browser, 'https://www.baidu.com') + test('white list', async () => { + await context.newPage('https://www.baidu.com') // 1. Enable await clickRunTimeChange('www.baidu.com') const enableTs = Date.now() await sleep(4) - let records = await readRecordsOfFirstPage(browser, extensionId) + let records = await readRecordsOfFirstPage(context) const runTime = parseTime2Sec(records[0].runTime) expect(runTime).toBeTruthy() expect(runTime).toBeLessThanOrEqual((Date.now() - enableTs + 500) / 1000) // 2. Add whitelist - await createWhitelist(browser, extensionId, 'www.baidu.com') + await createWhitelist(context, 'www.baidu.com') const disableTs = Date.now() await sleep(2) - records = await readRecordsOfFirstPage(browser, extensionId) + records = await readRecordsOfFirstPage(context) const runTime1 = parseTime2Sec(records[0].runTime) expect(runTime1).toBeLessThan((disableTs - enableTs) / 1000) }, 60000) diff --git a/test/__mock__/storage.ts b/test/__mock__/storage.ts index f6e102d64..931dc67cd 100644 --- a/test/__mock__/storage.ts +++ b/test/__mock__/storage.ts @@ -1,8 +1,8 @@ import StoragePromise from "@db/common/storage-promise" -let store = {} +let store: Record<string, any> = {} -function resolveOneKey(key: string, result: {}) { +function resolveOneKey(key: string, result: Record<string, any>) { const val = store[key] val !== undefined && (result[key] = val) } @@ -19,8 +19,8 @@ function resolveKey(key: string | Object | string[] | null) { key.forEach(curr => resolveOneKey(curr, result)) return result } else if (typeof key === 'object') { - return Object.keys(key).reduce((acc, curr) => { - acc[curr] = store[curr] || key[curr] + return Object.keys(key).reduce<Record<string, any>>((acc, curr) => { + acc[curr] = store[curr] ?? (key as any)[curr] return acc }, {}) } diff --git a/test/database/limit-database.test.ts b/test/database/limit-database.test.ts index be105e309..c0674eae5 100644 --- a/test/database/limit-database.test.ts +++ b/test/database/limit-database.test.ts @@ -8,6 +8,8 @@ describe('limit-database', () => { beforeEach(async () => storage.local.clear()) test('test1', async () => { const toAdd: timer.limit.Rule = { + id: 1, + name: "foobar", cond: ['123'], time: 20, enabled: true, @@ -48,12 +50,14 @@ describe('limit-database', () => { test("update waste", async () => { const date = formatTimeYMD(new Date()) const id1 = await db.save({ + name: "foobar", cond: ["a.*.com"], time: 21, enabled: true, allowDelay: false, }) await db.save({ + name: "foobar", cond: ["*.b.com"], time: 20, enabled: true, @@ -71,13 +75,15 @@ describe('limit-database', () => { }) test("import data", async () => { - const cond1: timer.limit.Rule = { + const cond1: MakeOptional<timer.limit.Rule, 'id'> = { + name: 'foobar1', cond: ["cond1"], time: 20, allowDelay: false, enabled: true } - const cond2: timer.limit.Rule = { + const cond2: MakeOptional<timer.limit.Rule, 'id'> = { + name: 'foobar2', cond: ["cond2"], time: 20, allowDelay: false, @@ -95,13 +101,13 @@ describe('limit-database', () => { const imported = await db.all() const cond2After = imported.find(a => a.cond?.includes("cond2")) - expect(Object.values(cond2After?.records)).toBeTruthy() + expect(Object.values(cond2After?.records || {})).toBeTruthy() expect(cond2After?.allowDelay).toEqual(cond2.allowDelay) expect(cond2After?.enabled).toEqual(cond2.enabled) }) test("import data2", async () => { - const importData = {} + const importData: Record<string, any> = {} // Invalid data, no error throws await db.importData(importData) // Valid data @@ -111,7 +117,8 @@ describe('limit-database', () => { }) test("update delay", async () => { - const data: timer.limit.Rule = { + const data: MakeOptional<timer.limit.Rule, 'id'> = { + name: 'foobar', cond: ["cond1"], time: 20, allowDelay: false, diff --git a/test/database/period-database.test.ts b/test/database/period-database.test.ts index 398ee89a9..094fe5840 100644 --- a/test/database/period-database.test.ts +++ b/test/database/period-database.test.ts @@ -104,7 +104,7 @@ describe('period-database', () => { }) const imported: timer.period.Result[] = await db.getAll() expect(imported.length).toEqual(3) - const orderMillMap = {} + const orderMillMap: Record<string, number> = {} imported.forEach(({ milliseconds, order }) => orderMillMap[order] = milliseconds) expect(orderMillMap).toEqual({ 0: MILL_PER_PERIOD, 1: 100, 2: 100 }) }) diff --git a/test/database/stat-database.test.ts b/test/database/stat-database.test.ts index 0022ee5c8..bbdf94884 100644 --- a/test/database/stat-database.test.ts +++ b/test/database/stat-database.test.ts @@ -1,4 +1,4 @@ -import StatDatabase, { StatCondition } from "@db/stat-database" +import StatDatabase, { type StatCondition } from "@db/stat-database" import { resultOf } from "@util/stat" import { formatTimeYMD, MILL_PER_DAY } from "@util/time" import storage from "../__mock__/storage" @@ -96,7 +96,7 @@ describe('stat-database', () => { // time [2, 3] cond.timeRange = [2, 3] - cond.focusRange = [, null] + cond.focusRange = undefined expect((await db.select(cond)).length).toEqual(2) }) @@ -209,8 +209,6 @@ describe('stat-database', () => { host: baidu, fullHost: true })).toEqual(2) - // Count all - expect(await db.count(undefined)).toEqual(4) // Count by fuzzy expect(await db.count({ host: "www", fullHost: false })).toEqual(4) // Count by date diff --git a/test/util/array.test.ts b/test/util/array.test.ts index f11f4e700..52e76bcb2 100644 --- a/test/util/array.test.ts +++ b/test/util/array.test.ts @@ -42,18 +42,17 @@ describe("util/array", () => { }) test("sum", () => { - let arr: number[] = [1, undefined, 2, 3, 4, null, NaN] + let arr: number[] = [1, 2, 3, 4] expect(10).toEqual(sum(arr)) - arr = undefined + arr = [] expect(0).toEqual(sum(arr)) }) test("average", () => { expect(average([10, 1])).toEqual(11 / 2) - expect(average(null)).toBeNull() expect(average([])).toBeNull() - expect(average([null])).toEqual(0) + expect(average([0])).toEqual(0) }) test("allMatch", () => { diff --git a/test/util/limit.test.ts b/test/util/limit.test.ts index c95f87c40..561815511 100644 --- a/test/util/limit.test.ts +++ b/test/util/limit.test.ts @@ -32,7 +32,7 @@ describe('util/limit', () => { test('period2Str', () => { expect(period2Str(undefined)).toBe('00:00-00:00') - expect(period2Str([, 100])).toBe('00:00-01:40') + expect(period2Str([0, 100])).toBe('00:00-01:40') expect(period2Str([100, 900])).toBe('01:40-15:00') }) @@ -44,7 +44,7 @@ describe('util/limit', () => { }) test('isEffective', () => { - const rule = (weekdays?: number[]): timer.limit.Rule => ({ cond: [], time: 0, weekdays }) + const rule = (weekdays?: number[]): timer.limit.Rule => ({ id: 1, name: 'foobar', cond: [], time: 0, weekdays }) expect(isEffective(undefined)).toBe(false) expect(isEffective(rule())).toBe(true) @@ -69,7 +69,7 @@ describe('util/limit', () => { monday.setDate(20) jest.setSystemTime(monday) - const rule = (weekdays: number[], enabled: boolean): timer.limit.Rule => ({ cond: [], time: 0, weekdays, enabled }) + const rule = (weekdays: number[], enabled: boolean): timer.limit.Rule => ({ id: 1, name: 'foobar', cond: [], time: 0, weekdays, enabled }) expect(isEnabledAndEffective(rule([0, 1, 2], true))).toBe(true) expect(isEnabledAndEffective(rule([0, 1, 2], false))).toBe(false) @@ -78,6 +78,8 @@ describe('util/limit', () => { test('hasWeeklyLimited', () => { const item: timer.limit.Item = { + id: 1, + name: 'foobar', cond: [], time: 0, waste: 0, @@ -105,6 +107,8 @@ describe('util/limit', () => { test('calcTimeState', () => { const item: timer.limit.Item = { + id: 1, + name: 'foobar', cond: [], time: 10, weekly: 10, @@ -148,6 +152,8 @@ describe('util/limit', () => { test('hasLimit', () => { const assert = (setup: (item: timer.limit.Item) => void, limited: boolean) => { const item: timer.limit.Item = { + id: 1, + name: 'foobar', cond: [], time: 1, weekly: 1, diff --git a/test/util/pattern.test.ts b/test/util/pattern.test.ts index 8da7a849f..607f4df41 100644 --- a/test/util/pattern.test.ts +++ b/test/util/pattern.test.ts @@ -29,7 +29,6 @@ test('ip and port', () => { test('merge host origin', () => { expect(isValidHost('')).toBeFalsy() - expect(isValidHost(undefined)).toBeFalsy() expect(isValidHost('wwdad.basd.com.111:12345')).toBeTruthy() expect(isValidHost('wwdad.basd.com.a111a:12345')).toBeTruthy() @@ -75,10 +74,8 @@ test("extractFileHost", () => { expect(extractFileHost(" file://123.jpeg ")).toEqual(PIC_HOST) expect(extractFileHost("file://123.pdf")).toEqual(PDF_HOST) - expect(extractFileHost(undefined)).toEqual(undefined) expect(extractFileHost("")).toEqual(undefined) expect(extractFileHost(" ")).toEqual(undefined) - expect(extractFileHost(null)).toEqual(undefined) expect(extractFileHost("file:/123.json")).toEqual(undefined) expect(extractFileHost("file://123. jpeg")).toEqual(undefined) expect(extractFileHost("file://123json")).toEqual(undefined) @@ -87,7 +84,6 @@ test("extractFileHost", () => { }) test("valid virtual host", () => { - expect(isValidVirtualHost(undefined)).toBeFalsy() expect(isValidVirtualHost("github.com")).toBeFalsy() expect(isValidVirtualHost("http://github.com")).toBeFalsy() expect(isValidVirtualHost("github.com/")).toBeFalsy() diff --git a/test/util/site.test.ts b/test/util/site.test.ts index 8662811ad..a39cea4aa 100644 --- a/test/util/site.test.ts +++ b/test/util/site.test.ts @@ -9,8 +9,6 @@ import { extractSiteName, generateSiteLabel } from "@util/site" test('extract site name', () => { expect(extractSiteName("")).toEqual(undefined) - expect(extractSiteName(undefined)).toEqual(undefined) - expect(extractSiteName(null)).toEqual(undefined) expect(extractSiteName(" ")).toEqual(undefined) expect(extractSiteName('Product Hunt – The best new products in tech.')).toEqual('Product Hunt') expect(extractSiteName('Product Hunt – The - best new products in tech.')).toEqual('The') diff --git a/tsconfig.json b/tsconfig.json index 18d0ae687..09e901544 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -12,6 +12,7 @@ "jsxFragmentFactory": "Fragment", "esModuleInterop": true, "sourceMap": true, + "strict": true, "resolveJsonModule": true, "importHelpers": true, "moduleResolution": "node", @@ -70,11 +71,10 @@ "node_modules", "dist" ], - "strict": true, "ts-node": { "files": true, "require": [ "tsconfig-paths/register" ] } -} \ No newline at end of file +} diff --git a/types/chrome.d.ts b/types/chrome.d.ts index ff2e1a972..d830d4652 100644 --- a/types/chrome.d.ts +++ b/types/chrome.d.ts @@ -33,7 +33,7 @@ declare namespace chrome { } } // see https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/sidebar_action - sidebar_action?: Pick<ManifestV2['page_action'], 'default_icon' | 'default_title'> & { + sidebar_action?: Pick<ManifestAction, 'default_icon' | 'default_title'> & { default_panel?: string open_at_install?: boolean } diff --git a/types/common.d.ts b/types/common.d.ts index d823f5062..312206be3 100644 --- a/types/common.d.ts +++ b/types/common.d.ts @@ -12,6 +12,8 @@ declare type EmbeddedPartial<T> = { */ type MakeOptional<T, K extends keyof T> = Omit<T, K> & { [P in K]?: T[P] } +type MakeRequired<T, K extends keyof T> = Omit<T, K> & Required<{ [P in K]: T[P] }> + /** * Tuple with length * @@ -32,4 +34,6 @@ declare type Tuple<E, L extends number, Arr = [E, ...Array<E>]> = */ declare type Vector<D extends number> = Tuple<number, D> -declare type CompareFn<T> = (a: T, b: T) => number \ No newline at end of file +declare type CompareFn<T> = (a: T, b: T) => number + +declare type Awaitable<T> = T | Promise<T> \ No newline at end of file diff --git a/types/timer/backup.d.ts b/types/timer/backup.d.ts index 7f397e5c0..28ea41ce7 100644 --- a/types/timer/backup.d.ts +++ b/types/timer/backup.d.ts @@ -57,7 +57,7 @@ declare namespace timer.backup { * * @returns errorMsg or null/undefined */ - testAuth(auth: Auth, ext: timer.backup.TypeExt): Promise<string> + testAuth(auth: Auth, ext: timer.backup.TypeExt): Promise<string | undefined> /** * Clear data */ diff --git a/types/timer/imported.d.ts b/types/timer/imported.d.ts index 4b0e3c449..bdeb81bf8 100644 --- a/types/timer/imported.d.ts +++ b/types/timer/imported.d.ts @@ -4,7 +4,7 @@ declare namespace timer.imported { type ConflictResolution = 'overwrite' | 'accumulate' - type Row = Required<timer.core.RowKey> & Partial<timer.core.Result> & { + type Row = Required<timer.core.RowKey> & timer.core.Result & { exist?: timer.core.Result } diff --git a/types/timer/limit.d.ts b/types/timer/limit.d.ts index 33586512b..51fed4d1c 100644 --- a/types/timer/limit.d.ts +++ b/types/timer/limit.d.ts @@ -45,11 +45,11 @@ declare namespace timer.limit { /** * Id */ - id?: number + id: number /** * Name */ - name?: string + name: string /** * Condition, can be regular expression with star signs */ @@ -57,7 +57,7 @@ declare namespace timer.limit { /** * Time limit per day, seconds */ - time: number + time?: number /** * Visit count per day * diff --git a/types/timer/mq.d.ts b/types/timer/mq.d.ts index 63c8a6531..cacd7168a 100644 --- a/types/timer/mq.d.ts +++ b/types/timer/mq.d.ts @@ -42,7 +42,7 @@ declare namespace timer.mq { */ type Request<T = any> = { code: ReqCode - data: T + data?: T } /** * @since 0.8.4 @@ -55,7 +55,7 @@ declare namespace timer.mq { /** * @since 1.3.0 */ - type Handler<Req, Res> = (data: Req, sender?: chrome.runtime.MessageSender) => Promise<Res> | Res + type Handler<Req, Res> = (data: Req, sender: chrome.runtime.MessageSender) => Promise<Res> | Res /** * @since 0.8.4 */ diff --git a/types/timer/option.d.ts b/types/timer/option.d.ts index df3d17fdb..991a734ab 100644 --- a/types/timer/option.d.ts +++ b/types/timer/option.d.ts @@ -112,7 +112,7 @@ declare namespace timer.option { * * @since 3.2.2 */ - chartAnimationDuration?: number + chartAnimationDuration: number } type StatisticsOption = { @@ -158,11 +158,11 @@ declare namespace timer.option { /** * The password to unlock */ - limitPassword: string + limitPassword?: string /** * The difficulty of verification */ - limitVerifyDifficulty: limit.VerificationDifficulty + limitVerifyDifficulty?: limit.VerificationDifficulty /** * Whether to reminder before time will meet * @@ -174,7 +174,7 @@ declare namespace timer.option { * * @since 3.1.0 */ - limitReminderDuration: number + limitReminderDuration?: number } /** diff --git a/webpack/webpack.analyze.ts b/webpack/webpack.analyze.ts index be9311ad0..59cc396be 100644 --- a/webpack/webpack.analyze.ts +++ b/webpack/webpack.analyze.ts @@ -3,10 +3,11 @@ // This software is released under the MIT License. // https://opensource.org/licenses/MIT -import path from 'path' -import generateOption from "./webpack.common" import { RsdoctorWebpackPlugin } from '@rsdoctor/webpack-plugin' +import path from 'path' +import { type Configuration } from 'webpack' import manifest from '../src/manifest' +import generateOption from "./webpack.common" const option = generateOption({ outputPath: path.join(__dirname, '..', 'dist_analyze'), @@ -14,8 +15,9 @@ const option = generateOption({ mode: "production", }) -option.optimization.minimize = true -option.optimization.usedExports = true -option.plugins.push(new RsdoctorWebpackPlugin()) +const { optimization = {}, plugins = [] } = option +optimization.minimize = true +optimization.usedExports = true +plugins.push(new RsdoctorWebpackPlugin()) -export default option \ No newline at end of file +export default { ...option, optimization, plugins } satisfies Configuration \ No newline at end of file diff --git a/webpack/webpack.common.ts b/webpack/webpack.common.ts index bfcb22a5e..ed2fa6b7c 100644 --- a/webpack/webpack.common.ts +++ b/webpack/webpack.common.ts @@ -4,7 +4,7 @@ import HtmlWebpackPlugin from "html-webpack-plugin" import MiniCssExtractPlugin from "mini-css-extract-plugin" import path from "path" import postcssRTLCSS from 'postcss-rtlcss' -import webpack, { type Chunk, DefinePlugin, type RuleSetRule } from "webpack" +import { type Chunk, type Configuration, DefinePlugin, type RuleSetRule, type WebpackPluginInstance } from "webpack" import i18nChrome from "../src/i18n/chrome" import tsConfig from '../tsconfig.json' @@ -12,11 +12,11 @@ export const MANIFEST_JSON_NAME = "manifest.json" const tsPathAlias = tsConfig.compilerOptions.paths -const generateJsonPlugins = [] +const generateJsonPlugins: WebpackPluginInstance[] = [] const localeJsonFiles = Object.entries(i18nChrome) .map(([locale, message]) => new GenerateJsonPlugin(`_locales/${locale}/messages.json`, message)) - .map(plugin => plugin as unknown as webpack.WebpackPluginInstance) + .map(plugin => plugin as unknown as WebpackPluginInstance) generateJsonPlugins.push(...localeJsonFiles) // Process the alias of typescript modules @@ -24,19 +24,25 @@ const resolveAlias: { [index: string]: string | false | string[] } = {} const aliasPattern = /^(@.*)\/\*$/ const sourcePattern = /^(src(\/.*)?)\/\*$/ Object.entries(tsPathAlias).forEach(([alias, sourceArr]) => { - // Only process the alias starts with '@' - if (!aliasPattern.test(alias)) { + if (!sourceArr.length) { return } - if (!sourceArr.length) { + const aliasMatchRes = alias.match(aliasPattern) + if (!aliasMatchRes) { + // Only process the alias starts with '@' return } - const index = alias.match(aliasPattern)[1] - const webpackSourceArr = sourceArr - .filter(source => sourcePattern.test(source)) - // Only set alias which is in /src folder - .map(source => source.match(sourcePattern)[1]) - .map(folder => path.resolve(__dirname, '..', folder)) + const [, index] = aliasMatchRes + const webpackSourceArr: string[] = [] + sourceArr.forEach(source => { + const matchRes = source.match(sourcePattern) + if (!matchRes) { + // Only set alias which is in /src folder + return + } + const [, folder] = matchRes + webpackSourceArr.push(path.resolve(__dirname, '..', folder)) + }) resolveAlias[index] = webpackSourceArr }) console.log("Alias of typescript: ") @@ -85,12 +91,12 @@ const POSTCSS_LOADER_CONF: RuleSetRule['use'] = { } const chunkFilter = ({ name }: Chunk) => { - return ![BACKGROUND, CONTENT_SCRIPT, CONTENT_SCRIPT_SKELETON].includes(name) + return !name || ![BACKGROUND, CONTENT_SCRIPT, CONTENT_SCRIPT_SKELETON].includes(name) } -const staticOptions: webpack.Configuration = { +const staticOptions: Configuration = { entry() { - const entry = {} + const entry: Record<string, string> = {} entryConfigs.forEach(({ name, path }) => entry[name] = path) return entry }, @@ -159,13 +165,13 @@ const staticOptions: webpack.Configuration = { type Option = { outputPath: string manifest: chrome.runtime.ManifestV3 | chrome.runtime.ManifestFirefox - mode: webpack.Configuration["mode"] + mode: Configuration["mode"] } const generateOption = ({ outputPath, manifest, mode }: Option) => { const plugins = [ ...generateJsonPlugins, - new GenerateJsonPlugin(MANIFEST_JSON_NAME, manifest) as unknown as webpack.WebpackPluginInstance, + new GenerateJsonPlugin(MANIFEST_JSON_NAME, manifest) as unknown as WebpackPluginInstance, // copy static resources new CopyWebpackPlugin({ patterns: [ @@ -207,7 +213,7 @@ const generateOption = ({ outputPath, manifest, mode }: Option) => { __VUE_PROD_DEVTOOLS__: false, }), ] - const config: webpack.Configuration = { + const config: Configuration = { ...staticOptions, output: { path: outputPath, diff --git a/webpack/webpack.prod.firefox.ts b/webpack/webpack.prod.firefox.ts index 401e31b0f..ed1dccf46 100644 --- a/webpack/webpack.prod.firefox.ts +++ b/webpack/webpack.prod.firefox.ts @@ -1,8 +1,8 @@ -import optionGenerator from "./webpack.common" -import path from "path" import FileManagerWebpackPlugin from "filemanager-webpack-plugin" -import webpack from "webpack" +import path from "path" +import type { WebpackPluginInstance } from "webpack" import manifestFirefox from "../src/manifest-firefox" +import optionGenerator from "./webpack.common" const { name, version } = require(path.join(__dirname, '..', 'package.json')) @@ -10,6 +10,7 @@ const outputPath = path.resolve(__dirname, '..', 'dist_prod_firefox') const marketPkgPath = path.resolve(__dirname, '..', 'market_packages') const normalZipFilePath = path.resolve(marketPkgPath, `${name}-${version}.firefox.zip`) +const targetZipFilePath = path.resolve(marketPkgPath, 'target.firefox.zip') const sourceCodePath = path.resolve(__dirname, '..', 'market_packages', `${name}-${version}-src.zip`) const readmeForFirefox = path.join(__dirname, '..', 'doc', 'for-fire-fox.md') // Temporary directory for source code to archive on Firefox @@ -28,6 +29,9 @@ const filemanagerWebpackPlugin = new FileManagerWebpackPlugin({ archive: [{ source: outputPath, destination: normalZipFilePath, + }, { + source: outputPath, + destination: targetZipFilePath, }] }, // Archive source code for FireFox @@ -47,6 +51,8 @@ const filemanagerWebpackPlugin = new FileManagerWebpackPlugin({ }) const option = optionGenerator({ outputPath, manifest: manifestFirefox, mode: "production" }) -option.plugins.push(filemanagerWebpackPlugin as webpack.WebpackPluginInstance) +const { plugins = [] } = option +plugins.push(filemanagerWebpackPlugin as WebpackPluginInstance) +option.plugins = plugins export default option \ No newline at end of file diff --git a/webpack/webpack.prod.safari.ts b/webpack/webpack.prod.safari.ts index 8068662ae..ee830602d 100644 --- a/webpack/webpack.prod.safari.ts +++ b/webpack/webpack.prod.safari.ts @@ -25,6 +25,8 @@ const filemanagerWebpackPlugin = new FileManagerWebpackPlugin({ } }) -options.plugins.push(filemanagerWebpackPlugin as webpack.WebpackPluginInstance) +const { plugins = [] } = options +plugins.push(filemanagerWebpackPlugin as webpack.WebpackPluginInstance) +options.plugins = plugins export default options \ No newline at end of file diff --git a/webpack/webpack.prod.ts b/webpack/webpack.prod.ts index 87dd1b345..880753f59 100644 --- a/webpack/webpack.prod.ts +++ b/webpack/webpack.prod.ts @@ -1,8 +1,8 @@ -import optionGenerator from "./webpack.common" -import path from "path" import FileManagerWebpackPlugin from "filemanager-webpack-plugin" -import webpack from "webpack" +import path from "path" +import type { WebpackPluginInstance } from "webpack" import manifest from "../src/manifest" +import optionGenerator from "./webpack.common" const { name, version } = require(path.join(__dirname, '..', 'package.json')) @@ -10,6 +10,7 @@ const outputPath = path.resolve(__dirname, '..', 'dist_prod') const marketPkgPath = path.resolve(__dirname, '..', 'market_packages') const normalZipFilePath = path.resolve(marketPkgPath, `${name}-${version}.mv3.zip`) +const targetZipFilePath = path.resolve(marketPkgPath, `target.zip`) const filemanagerWebpackPlugin = new FileManagerWebpackPlugin({ events: { @@ -22,6 +23,9 @@ const filemanagerWebpackPlugin = new FileManagerWebpackPlugin({ archive: [{ source: outputPath, destination: normalZipFilePath, + }, { + source: outputPath, + destination: targetZipFilePath, }] }, ] @@ -30,6 +34,8 @@ const filemanagerWebpackPlugin = new FileManagerWebpackPlugin({ const option = optionGenerator({ outputPath, manifest, mode: "production" }) -option.plugins.push(filemanagerWebpackPlugin as webpack.WebpackPluginInstance) +const { plugins = [] } = option +plugins.push(filemanagerWebpackPlugin as WebpackPluginInstance) +option.plugins = plugins export default option \ No newline at end of file