Merge pull request #846 from Rewrite0/webui

Update WebUI
This commit is contained in:
Rewrite0
2024-09-23 16:45:41 +08:00
committed by GitHub
29 changed files with 5311 additions and 6402 deletions

View File

@@ -1,7 +1,7 @@
{
"i18n-ally.localesPaths": ["src/i18n"],
"commentTranslate.targetLanguage": "zh-CN",
"i18n-ally.sourceLanguage": "en",
"i18n-ally.sourceLanguage": "zh-CN",
"typescript.tsdk": "node_modules/typescript/lib",
"i18n-ally.keystyle": "nested"
}

View File

@@ -3,6 +3,7 @@
"type": "module",
"version": "0.0.0",
"private": true,
"packageManager": "pnpm@9.11.0+sha512.0a203ffaed5a3f63242cd064c8fb5892366c103e328079318f78062f24ea8c9d50bc6a47aa3567cabefd824d170e78fa2745ed1f16b132e16436146b7688f19b",
"scripts": {
"prepare": "cd .. && husky install ./webui/.husky",
"test:build": "vue-tsc --noEmit",
@@ -19,53 +20,51 @@
"generate-pwa-assets": "pwa-assets-generator --preset minimal public/images/logo.svg"
},
"dependencies": {
"@headlessui/vue": "^1.7.13",
"@vueuse/components": "^10.4.1",
"@vueuse/core": "^8.9.4",
"@headlessui/vue": "^1.7.23",
"@vueuse/components": "^10.11.1",
"@vueuse/core": "^10.11.1",
"axios": "^0.27.2",
"naive-ui": "^2.34.4",
"pinia": "^2.1.3",
"rxjs": "^7.8.1",
"vue": "^3.3.4",
"vue-i18n": "^9.2.2",
"vue-inline-svg": "^3.1.2",
"vue-router": "^4.2.1"
"naive-ui": "^2.39.0",
"pinia": "^2.2.2",
"vue": "^3.5.8",
"vue-i18n": "^9.14.0",
"vue-inline-svg": "^3.1.4",
"vue-router": "^4.4.5"
},
"devDependencies": {
"@antfu/eslint-config": "^0.38.6",
"@icon-park/vue-next": "^1.4.2",
"@intlify/unplugin-vue-i18n": "^0.11.0",
"@storybook/addon-essentials": "^7.0.12",
"@storybook/addon-interactions": "^7.0.12",
"@storybook/addon-links": "^7.0.12",
"@storybook/blocks": "^7.0.12",
"@storybook/addon-essentials": "^7.6.20",
"@storybook/addon-interactions": "^7.6.20",
"@storybook/addon-links": "^7.6.20",
"@storybook/blocks": "^7.6.20",
"@storybook/testing-library": "0.0.14-next.2",
"@storybook/vue3": "^7.0.12",
"@storybook/vue3-vite": "^7.0.12",
"@types/node": "^18.16.14",
"@unocss/preset-attributify": "^0.55.3",
"@storybook/vue3": "^7.6.20",
"@storybook/vue3-vite": "^7.6.20",
"@types/node": "^18.19.50",
"@unocss/preset-attributify": "^0.55.7",
"@unocss/preset-rem-to-px": "^0.51.13",
"@unocss/reset": "^0.51.13",
"@vitejs/plugin-vue": "^4.2.3",
"@vitejs/plugin-vue": "^4.6.2",
"@vitejs/plugin-vue-jsx": "^3.1.0",
"@vue/runtime-dom": "^3.3.4",
"eslint": "^8.41.0",
"eslint-config-prettier": "^8.8.0",
"eslint-plugin-storybook": "^0.6.12",
"@vue/runtime-dom": "^3.5.8",
"eslint": "^8.57.1",
"eslint-config-prettier": "^8.10.0",
"eslint-plugin-storybook": "^0.6.15",
"husky": "^8.0.3",
"prettier": "^2.8.8",
"radash": "^12.1.0",
"sass": "^1.62.1",
"storybook": "^7.0.12",
"sass-embedded": "^1.79.3",
"storybook": "^7.6.20",
"typescript": "^4.9.5",
"unocss": "^0.51.13",
"unplugin-auto-import": "^0.10.3",
"unplugin-vue-components": "^0.24.1",
"unplugin-vue-router": "^0.6.4",
"vite": "^4.3.5",
"vite-plugin-pwa": "^0.16.4",
"vite": "^4.5.5",
"vite-plugin-pwa": "^0.16.7",
"vitest": "^0.30.1",
"vue-tsc": "^1.6.4"
},
"packageManager": "pnpm@9.1.4+sha512.9df9cf27c91715646c7d675d1c9c8e41f6fce88246f1318c1aa6a1ed1aeb3c4f032fcdf4ba63cc69c4fe6d634279176b5358727d8f2cc1e65b65f43ce2f8bfb0"
"vue-tsc": "^2.1.6"
}
}

10597
webui/pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,57 +1,60 @@
import { Observable } from 'rxjs';
import type { Ref } from 'vue';
import type { BangumiRule } from '#/bangumi';
import type { BangumiAPI, BangumiRule } from '#/bangumi';
type EventSourceStatus = 'OPEN' | 'CONNECTING' | 'CLOSED';
export const apiSearch = {
/**
* 番剧搜索接口是 Server Send 流式数据,每条是一个 Bangumi JSON 字符串,
* 使用接口方式是监听连接消息后,转为 Observable 配合外层调用时 switchMap 订阅使用
*/
get(keyword: string, site = 'mikan'): Observable<BangumiRule> {
const bangumiInfo$ = new Observable<BangumiRule>((observer) => {
const eventSource = new EventSource(
`api/v1/search/bangumi?site=${site}&keywords=${encodeURIComponent(
keyword
)}`,
{ withCredentials: true }
);
get() {
const eventSource = ref(null) as Ref<EventSource | null>;
const status = ref<EventSourceStatus>('CLOSED');
const data = ref<BangumiRule[]>([]);
eventSource.onmessage = (ev) => {
try {
const apiData: BangumiAPI = JSON.parse(ev.data);
const data: BangumiRule = {
...apiData,
filter: apiData.filter.split(','),
rss_link: apiData.rss_link.split(','),
};
observer.next(data);
} catch (error) {
console.error(
'[/search/bangumi] Parse Error |',
{ keyword },
'response:',
ev.data
);
}
const keyword = ref('');
const provider = ref('');
const close = () => {
if (eventSource.value) {
eventSource.value.close();
eventSource.value = null;
status.value = 'CLOSED';
}
};
const _init = () => {
status.value = 'CONNECTING';
const url = `api/v1/search/bangumi?site=${
provider.value
}&keywords=${encodeURIComponent(keyword.value)}`;
const es = new EventSource(url, { withCredentials: true });
eventSource.value = es;
es.onopen = () => {
status.value = 'OPEN';
};
eventSource.onerror = (ev) => {
console.error(
'[/search/bangumi] Server Error |',
{ keyword },
'error:',
ev
);
// 目前后端搜索完成关闭连接时会触发 error 事件,前端手动调用 close 不再自动重连
eventSource.close();
es.onmessage = (e) => {
const newData = JSON.parse(e.data) as BangumiRule;
data.value = [...data.value, newData];
};
return () => {
eventSource.close();
es.onerror = (err) => {
console.error('EventSource error:', err);
close();
};
});
};
return bangumiInfo$;
const open = () => {
data.value = [];
_init();
};
return {
keyword,
provider,
status,
data,
open,
close,
};
},
async getProvider() {

View File

@@ -1,10 +1,10 @@
<script lang="ts" setup>
import { ErrorPicture, Write } from '@icon-park/vue-next';
import { Write } from '@icon-park/vue-next';
import type { BangumiRule } from '#/bangumi';
withDefaults(
defineProps<{
type?: 'primary' | 'search' | 'mobile';
type?: 'primary' | 'search';
bangumi: BangumiRule;
}>(),
{
@@ -16,89 +16,77 @@ defineEmits(['click']);
</script>
<template>
<div v-if="type === 'primary'" w-150 is-btn @click="() => $emit('click')">
<div rounded-4 overflow-hidden poster-shandow rel>
<div w-full h-210>
<template v-if="bangumi.poster_link">
<img :src="bangumi.poster_link" alt="poster" wh-full />
</template>
<template v-if="type === 'primary'">
<div w="full pc:150" is-btn @click="() => $emit('click')">
<div rounded-4 overflow-hidden poster-shandow rel>
<ab-image
:src="bangumi.poster_link"
:aspect-ratio="1 / 1.5"
w-full
></ab-image>
<template v-else>
<div wh-full f-cer border="1 white">
<ErrorPicture theme="outline" size="24" fill="#333" />
</div>
</template>
</div>
<div
abs
f-cer
z-1
inset-0
opacity-0
transition="all duration-300"
hover="backdrop-blur-2 bg-white bg-opacity-30 opacity-100"
active="duration-0 bg-opacity-60"
class="group"
>
<div
text-white
rounded="1/2"
wh-44
abs
f-cer
bg-theme-row
group-active="poster-pen-active"
z-1
inset-0
opacity-0
transition="all duration-300"
hover="backdrop-blur-2 bg-white bg-opacity-30 opacity-100"
active="duration-0 bg-opacity-60"
class="group"
>
<Write size="20" />
<div
text-white
rounded="1/2"
wh-44
f-cer
bg-theme-row
group-active="poster-pen-active"
>
<Write size="20" />
</div>
</div>
</div>
</div>
<div py-4>
<div text-h3 truncate>{{ bangumi.official_title }}</div>
<div py-4>
<div text-h3 truncate>{{ bangumi.official_title }}</div>
<div space-x-5>
<ab-tag :title="`Season ${bangumi.season}`" type="primary" />
<ab-tag
v-if="bangumi.group_name"
:title="bangumi.group_name"
type="primary"
/>
</div>
</div>
</div>
<div
v-else-if="type === 'search'"
w-480
rounded-12
p-4
shadow
bg="#eee5f4"
transition="opacity ease-in-out duration-300"
>
<div bg-white rounded-8 p-12 fx-cer justify-between gap-x-16>
<div w-400 gap-x-16 fx-cer>
<div h-44 w-72 rounded-6 overflow-hidden>
<template v-if="bangumi.poster_link">
<img
:src="bangumi.poster_link"
alt="poster"
w-full
translate-y="-25%"
<div flex="~ wrap col" pc:flex-row gap-5>
<template v-for="i in ['season', 'group_name']" :key="i">
<ab-tag
v-if="bangumi[i]"
:title="i === 'season' ? `Season ${bangumi[i]}` : bangumi[i]"
type="primary"
pc:max-w="1/2"
/>
</template>
<template v-else>
<div wh-full f-cer border="1 white">
<ErrorPicture theme="outline" size="24" fill="#333" />
</div>
</template>
</div>
<div flex="~ col gap-y-4">
<div w-300 text="h3 primary" truncate>
</div>
</div>
</template>
<template v-else-if="type === 'search'">
<div
w-480
max-w-90vw
rounded-12
p-4
shadow
bg="#eee5f4"
transition="opacity ease-in-out duration-300"
>
<div w-full bg-white rounded-8 p-12 flex gap-x-14>
<div w-72 rounded-6 overflow-hidden>
<ab-image :src="bangumi.poster_link" w-full></ab-image>
</div>
<div flex="~ col 1 gap-y-4 justify-between">
<div text="h3 primary">
{{ bangumi.official_title }}
</div>
<div flex="~ gap-x-8">
<div flex="~ wrap gap-8">
<template
v-for="i in ['season', 'group_name', 'subtitle']"
:key="i"
@@ -111,8 +99,14 @@ defineEmits(['click']);
</template>
</div>
</div>
<ab-add
my-auto
:round="true"
type="medium"
@click="() => $emit('click')"
/>
</div>
<ab-add :round="true" type="medium" @click="() => $emit('click')" />
</div>
</div>
</template>
</template>

View File

@@ -15,7 +15,7 @@ withDefaults(
bg-theme-row
w-full
text-white
px-20
px="10 pc:20"
h="38 pc:45"
fx-cer
justify-between

View File

@@ -20,7 +20,7 @@ withDefaults(
w-full
text-white
fx-cer
px-20
px="10 pc:20"
h="38 pc:45"
justify-between
>

View File

@@ -0,0 +1,46 @@
<script lang="ts" setup>
import { ErrorPicture } from '@icon-park/vue-next';
withDefaults(
defineProps<{
src?: string | null;
aspectRatio?: number;
objectFit?: 'fill' | 'contain' | 'cover' | 'none' | 'scale-down';
}>(),
{
objectFit: 'cover',
}
);
</script>
<template>
<div rel>
<template v-if="aspectRatio">
<div
w-full
:style="{ paddingBottom: `calc(${1 / aspectRatio} * 100%)` }"
></div>
<img
v-if="src"
:src="src"
alt="poster"
abs
top-0
left-0
:style="{ objectFit }"
wh-full
/>
</template>
<template v-else>
<img v-if="src" :src="src" alt="poster" :style="{ objectFit }" wh-full />
<div v-else wh-full f-cer border="1 white">
<ErrorPicture theme="outline" size="24" fill="#333" />
</div>
</template>
</div>
</template>
<style lang="scss" scope></style>

View File

@@ -1,73 +1,102 @@
<script lang="ts" setup>
import {
Popover,
PopoverButton,
PopoverOverlay,
PopoverPanel,
} from '@headlessui/vue';
import { vOnClickOutside } from '@vueuse/components';
import { Search } from '@icon-park/vue-next';
import type { BangumiRule } from '#/bangumi';
defineEmits<{
(e: 'add-bangumi', bangumiRule: BangumiRule): void;
}>();
const showProvider = ref(false);
const { providers, provider, loading, inputValue, bangumiList } = storeToRefs(
const { providers, provider, loading, keyword, searchData } = storeToRefs(
useSearchStore()
);
const { getProviders, onSearch, clearSearch } = useSearchStore();
const { getProviders, clearSearch, openSearch } = useSearchStore();
onMounted(() => {
getProviders();
});
function onSelect(site: string) {
provider.value = site;
showProvider.value = false;
}
</script>
<template>
<ab-search
v-model:inputValue="inputValue"
:provider="provider"
:loading="loading"
@search="onSearch"
@select="() => (showProvider = !showProvider)"
/>
<Popover v-bind="$attrs">
<transition name="fade">
<PopoverOverlay
class="fixed top-0 left-0 w-full h-full bg-black bg-opacity-50 z-5"
/>
</transition>
<div
v-show="showProvider"
v-on-click-outside="() => (showProvider = false)"
abs
top-84
left-540
w-100
rounded-12
shadow
bg-white
z-99
overflow-hidden
>
<div
v-for="site in providers"
:key="site"
hover:bg-theme-row
is-btn
@click="() => onSelect(site)"
<PopoverButton bg-transparent text="pc:24 20" is-btn btn-click>
<Search size="1em" fill="#fff" />
</PopoverButton>
<transition
enter-active-class="transition duration-200 ease-out"
enter-from-class="translate-y--20 opacity-0"
enter-to-class="translate-y-0 opacity-100"
leave-active-class="transition duration-150 ease-in"
leave-from-class="translate-y-0 opacity-100"
leave-to-class="translate-y--20 opacity-0"
>
<div text="h3 primary" hover="text-white" p-12 truncate>
{{ site }}
</div>
</div>
</div>
<div v-on-click-outside="clearSearch" abs top-84 left-192 z-8>
<transition-group name="fade-list" tag="ul" space-y-12>
<li v-for="bangumi in bangumiList" :key="bangumi.order">
<ab-bangumi-card
:bangumi="bangumi.value"
type="search"
@click="() => $emit('add-bangumi', bangumi.value)"
<PopoverPanel
v-on-click-outside="clearSearch"
class="search-panel"
fixed
left-0
right-0
m-auto
w-max
z-5
>
<ab-search
v-model:input-value="keyword"
v-model:provider="provider"
:providers="providers"
:loading="loading"
@search="openSearch"
/>
</li>
</transition-group>
</div>
<div class="search-list" space-y-10 overflow-auto>
<transition-group name="fade-list">
<template v-for="i in searchData" :key="i.rss_link">
<ab-bangumi-card
:bangumi="i"
type="search"
@click="() => $emit('add-bangumi', i)"
/>
</template>
</transition-group>
</div>
</PopoverPanel>
</transition>
</Popover>
</template>
<style lang="scss" scoped></style>
<style lang="scss" scoped>
.search-panel {
--_offset-top: 80px;
--_offset-bottom: 40px;
--_search-input-height: 36px;
--_search-list-offset: 20px;
@include forMobile {
--_offset-top: 65px;
--_search-list-offset: 10px;
}
top: var(--_offset-top);
.search-list {
margin-top: var(--_search-list-offset);
max-height: calc(
100vh - var(--_offset-top) - var(--_offset-bottom) -
var(--_search-input-height) - var(--_search-list-offset)
);
}
}
</style>

View File

@@ -1,26 +1,28 @@
<script lang="ts" setup>
import { Down, Search } from '@icon-park/vue-next';
import { NSpin } from 'naive-ui';
import { watch } from 'vue';
withDefaults(
defineProps<{
provider: string;
providers: string[];
loading: boolean;
}>(),
{
provider: '',
loading: false,
}
);
defineEmits(['select', 'search']);
defineEmits(['search']);
const provider = defineModel<string>('provider');
const inputValue = defineModel<string>('inputValue');
watch(inputValue, (val) => {
console.log(val);
});
const showProvider = ref(false);
function onSelect(site: string) {
provider.value = site;
showProvider.value = false;
}
</script>
<template>
@@ -33,7 +35,7 @@ watch(inputValue, (val) => {
pl-12
gap-x-12
w-400
overflow-hidden
max-w-90vw
shadow-inner
>
<Search
@@ -54,22 +56,45 @@ watch(inputValue, (val) => {
input-reset
@keyup.enter="$emit('search')"
/>
<div
h-full
f-cer
justify-between
px-12
w-100
class="provider"
is-btn
@click="$emit('select')"
>
<div text-h3 truncate>
{{ provider }}
</div>
<div class="provider">
<div rel w-100 h-full px-12 rounded-inherit class="provider" is-btn>
<div
fx-cer
wh-full
justify-between
@click="() => (showProvider = !showProvider)"
>
<div text-h3 truncate>
{{ provider }}
</div>
<Down />
</div>
<div
v-show="showProvider"
abs
top="100%"
left-0
w-100
rounded-12
shadow
bg-white
z-1
overflow-hidden
>
<div
v-for="site in providers"
:key="site"
hover:bg-theme-row
is-btn
@click="() => onSelect(site)"
>
<div text="h3 primary" hover="text-white" p-12 truncate>
{{ site }}
</div>
</div>
</div>
</div>
</div>
</template>

View File

@@ -16,8 +16,8 @@ const InnerStyle = computed(() => {
</script>
<template>
<div p-1 rounded-16 inline-flex :class="type">
<div bg-white rounded-12 px-8 text-10 truncate max-w-72 :class="InnerStyle">
<div p-1 rounded-16 inline-flex w-max :class="type">
<div w-full bg-white rounded-12 px-8 text-10 truncate :class="InnerStyle">
{{ title }}
</div>
</div>

View File

@@ -114,13 +114,11 @@ onUnmounted(() => {
pc:top-2
/>
</div>
<div hidden pc:block>
<ab-search-bar @add-bangumi="addSearchResult" />
</div>
</div>
<div ml-auto>
<div ml-auto fx-cer>
<ab-search-bar mr="pc:16 10" fx-cer @add-bangumi="addSearchResult" />
<ab-status-bar
:items="items"
:running="running"
@@ -128,10 +126,8 @@ onUnmounted(() => {
@change-lang="changeLocale"
/>
</div>
<ab-change-account v-model:show="showAccount"></ab-change-account>
<ab-add-rss
v-model:show="showAddRSS"
v-model:rule="searchRule"
></ab-add-rss>
</div>
<ab-change-account v-model:show="showAccount"></ab-change-account>
<ab-add-rss v-model:show="showAddRSS" v-model:rule="searchRule"></ab-add-rss>
</template>

View File

@@ -7,7 +7,6 @@ const { getSettingGroup } = useConfigStore();
const parser = getSettingGroup('rss_parser');
/** @ts-expect-error Incorrect order */
const langs: RssParserLang = ['zh', 'en', 'jp'];
const items: SettingItem<RssParser>[] = [

View File

@@ -1,3 +1,5 @@
import { createSharedComposable, useIntervalFn } from '@vueuse/core';
export const useAppInfo = createSharedComposable(() => {
const { isLoggedIn } = useAuth();
const running = ref<boolean>(false);

View File

@@ -1,3 +1,4 @@
import { createSharedComposable, useLocalStorage } from '@vueuse/core';
import type { User } from '#/auth';
import type { ApiError } from '#/api';
import { router } from '@/router';

View File

@@ -1,4 +1,5 @@
import { createDiscreteApi } from 'naive-ui';
import { createSharedComposable } from '@vueuse/core';
export const useMessage = createSharedComposable(() => {
const { message } = createDiscreteApi(['message']);

View File

@@ -1,4 +1,5 @@
import { createI18n } from 'vue-i18n';
import { createSharedComposable, useLocalStorage } from '@vueuse/core';
import enUS from '@/i18n/en.json';
import zhCN from '@/i18n/zh-CN.json';
import type { ApiSuccess } from '#/api';
@@ -24,7 +25,7 @@ export const useMyI18n = createSharedComposable(() => {
});
watch(lang, (val) => {
i18n.global.locale.value = val;
i18n.global.locale.value = val as unknown as Languages;
});
function changeLocale() {

View File

@@ -3,6 +3,8 @@ definePage({
name: 'Index',
redirect: '/bangumi',
});
const title = computed(() => useRoute().name);
</script>
<template>
@@ -13,7 +15,7 @@ definePage({
<ab-sidebar />
<div class="layout-content">
<ab-page-title :title="$route.name"></ab-page-title>
<ab-page-title :title="title"></ab-page-title>
<RouterView v-slot="{ Component }">
<KeepAlive>
@@ -27,6 +29,10 @@ definePage({
<style lang="scss" scoped>
.layout-container {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
@@ -45,7 +51,6 @@ definePage({
@include forMobile {
overflow: hidden;
height: 100vh;
}
}

View File

@@ -15,14 +15,17 @@ onActivated(() => {
</script>
<template>
<div overflow-auto mt-12 flex-grow>
<div overflow-auto pr-10 mt-12 flex-grow>
<div>
<transition-group
name="bangumi"
tag="div"
flex="~ wrap"
gap="20"
:class="{ 'justify-center': isMobile }"
gap="10"
pc:gap="20"
:class="[
{ 'justify-center': isMobile },
isMobile ? 'grid grid-cols-3' : 'flex flex-wrap',
]"
>
<ab-bangumi-card
v-for="i in bangumi"

View File

@@ -1,4 +1,6 @@
<script lang="ts" setup>
import { watchOnce } from '@vueuse/core';
definePage({
name: 'Log',
});
@@ -52,11 +54,14 @@ onActivated(() => {
if (log.value) {
backToBottom();
} else {
watchOnce(log, () => {
nextTick(() => {
backToBottom();
});
});
watchOnce(
() => log.value,
() => {
nextTick(() => {
backToBottom();
});
}
);
}
});

View File

@@ -8,7 +8,7 @@ const { url } = storeToRefs(usePlayerStore());
<template>
<div overflow-auto mt-12 flex-grow>
<template v-if="url === ''">
<template v-if="url.length === 0">
<div wh-full f-cer text-h1 text-primary>
<RouterLink to="/config" hover:underline>{{
$t('player.hit')

View File

@@ -1,3 +1,5 @@
import { useClipboard, useIntervalFn } from '@vueuse/core';
export const useLogStore = defineStore('log', () => {
const message = useMessage();
const { isLoggedIn } = useAuth();
@@ -26,8 +28,12 @@ export const useLogStore = defineStore('log', () => {
});
function copy() {
const { copy: copyLog, isSupported } = useClipboard({ source: log });
if (isSupported) {
const { copy: copyLog, isSupported } = useClipboard({
source: log.value,
legacy: true,
});
if (isSupported.value) {
copyLog();
message.success(t('notify.copy_success'));
} else {

View File

@@ -1,9 +1,11 @@
import { useLocalStorage } from '@vueuse/core';
type MediaPlayerType = 'jump' | 'iframe';
export const usePlayerStore = defineStore('player', () => {
const types = ref<MediaPlayerType[]>(['jump', 'iframe']);
const type = useLocalStorage<MediaPlayerType>('media-player-type', 'jump');
const url = useLocalStorage<string>('media-player-url', '');
const url = useLocalStorage('media-player-url', '');
return {
types,

View File

@@ -1,74 +1,38 @@
import { ref } from 'vue';
import { EMPTY, Subject, debounceTime, switchMap, tap } from 'rxjs';
import type { BangumiRule, SearchResult } from '#/bangumi';
export const useSearchStore = defineStore('search', () => {
const bangumiList = ref<SearchResult[]>([]);
const inputValue = ref<string>('');
const providers = ref<string[]>(['mikan', 'dmhy', 'nyaa']);
const provider = ref<string>(providers.value[0]);
const loading = ref<boolean>(false);
const {
keyword,
provider,
open: openSearch,
close: closeSearch,
data: searchData,
status,
} = apiSearch.get();
const input$ = new Subject<string>();
provider.value = providers.value[0];
watch(inputValue, (input) => {
input$.next(input);
loading.value = !!input;
});
const loading = computed(() => status.value !== 'CLOSED');
function getProviders() {
apiSearch.getProvider().then((res) => {
providers.value = res;
});
}
/**
* - 输入中 debounce 600ms 后触发搜索
* - 按回车或点击搜索 icon 按钮后触发搜索
* - 切换 provider 源站时触发搜索
*/
const bangumiInfo$ = input$
.pipe(
debounceTime(600),
// switchMap 把输入 keyword 查询为 bangumiInfo$ 流,多次输入自动取消并停止前一次查询
switchMap((input: string) => {
// 有输入更新后清理之前的搜索结果
bangumiList.value = [];
return input ? apiSearch.get(input, provider.value) : EMPTY;
}),
tap((bangumi: BangumiRule) => {
const result: SearchResult = {
order: bangumiList.value.length + 1,
value: bangumi,
};
bangumiList.value.push(result);
})
)
.subscribe();
function onSearch() {
input$.next(inputValue.value);
async function getProviders() {
providers.value = await apiSearch.getProvider();
provider.value = providers.value[0];
}
function clearSearch() {
inputValue.value = '';
bangumiList.value = [];
keyword.value = '';
}
return {
input$,
bangumiInfo$,
inputValue,
keyword,
loading,
provider,
providers,
bangumiList,
searchData,
onSearch,
clearSearch,
getProviders,
openSearch,
closeSearch,
};
});

View File

@@ -1,62 +1,91 @@
import type { UnionToTuple } from '#/utils';
import type { TupleToUnion } from './utils';
/** 下载方式 */
export type DownloaderType = ['qbittorrent'];
/** rss parser 源 */
export type RssParserType = ['mikan'];
/** rss parser 方法 */
export type RssParserMethodType = ['tmdb', 'mikan', 'parser'];
/** rss parser 语言 */
export type RssParserLang = ['zh', 'en', 'jp'];
/** 重命名方式 */
export type RenameMethod = ['normal', 'pn', 'advance', 'none'];
/** 代理类型 */
export type ProxyType = ['http', 'https', 'socks5'];
/** 通知类型 */
export type NotificationType = ['telegram', 'server-chan', 'bark', 'wecom'];
/** OpenAI Model List */
export type OpenAIModel = ['gpt-3.5-turbo'];
/** OpenAI API Type */
export type OpenAIType = ['openai', 'azure'];
export interface Program {
rss_time: number;
rename_time: number;
webui_port: number;
}
export interface Downloader {
type: TupleToUnion<DownloaderType>;
host: string;
username: string;
password: string;
path: string;
ssl: boolean;
}
export interface RssParser {
enable: boolean;
type: TupleToUnion<RssParserType>;
token: string;
custom_url: string;
filter: Array<string>;
language: TupleToUnion<RssParserLang>;
parser_type: TupleToUnion<RssParserMethodType>;
}
export interface BangumiManage {
enable: boolean;
eps_complete: boolean;
rename_method: TupleToUnion<RenameMethod>;
group_tag: boolean;
remove_bad_torrent: boolean;
}
export interface Log {
debug_enable: boolean;
}
export interface Proxy {
enable: boolean;
type: TupleToUnion<ProxyType>;
host: string;
port: number;
username: string;
password: string;
}
export interface Notification {
enable: boolean;
type: 'telegram' | 'server-chan' | 'bark' | 'wecom';
token: string;
chat_id: string;
}
export interface ExperimentalOpenAI {
enable: boolean;
api_key: string;
api_base: string;
model: TupleToUnion<OpenAIModel>;
// azure
api_type: TupleToUnion<OpenAIType>;
api_version?: string;
deployment_id?: string;
}
export interface Config {
program: {
rss_time: number;
rename_time: number;
webui_port: number;
};
downloader: {
type: 'qbittorrent';
host: string;
username: string;
password: string;
path: string;
ssl: boolean;
};
rss_parser: {
enable: boolean;
type: 'mikan';
token: string;
custom_url: string;
filter: Array<string>;
language: 'zh' | 'en' | 'jp';
parser_type: 'tmdb' | 'mikan' | 'parser';
};
bangumi_manage: {
enable: boolean;
eps_complete: boolean;
rename_method: 'normal' | 'pn' | 'advance' | 'none';
group_tag: boolean;
remove_bad_torrent: boolean;
};
log: {
debug_enable: boolean;
};
proxy: {
enable: boolean;
type: 'http' | 'https' | 'socks5';
host: string;
port: number;
username: string;
password: string;
};
notification: {
enable: boolean;
type: 'telegram' | 'server-chan' | 'bark' | 'wecom';
token: string;
chat_id: string;
};
experimental_openai: {
enable: boolean;
api_key: string;
api_base: string;
model: 'gpt-3.5-turbo';
// azure
api_type: 'openai' | 'azure';
api_version?: string;
deployment_id?: string;
};
program: Program;
downloader: Downloader;
rss_parser: RssParser;
bangumi_manage: BangumiManage;
log: Log;
proxy: Proxy;
notification: Notification;
experimental_openai: ExperimentalOpenAI;
}
export const initConfig: Config = {
@@ -117,33 +146,3 @@ export const initConfig: Config = {
deployment_id: '',
},
};
type getItem<T extends keyof Config> = Pick<Config, T>[T];
export type Program = getItem<'program'>;
export type Downloader = getItem<'downloader'>;
export type RssParser = getItem<'rss_parser'>;
export type BangumiManage = getItem<'bangumi_manage'>;
export type Log = getItem<'log'>;
export type Proxy = getItem<'proxy'>;
export type Notification = getItem<'notification'>;
export type ExperimentalOpenAI = getItem<'experimental_openai'>;
/** 下载方式 */
export type DownloaderType = UnionToTuple<Downloader['type']>;
/** rss parser 源 */
export type RssParserType = UnionToTuple<RssParser['type']>;
/** rss parser 方法 */
export type RssParserMethodType = UnionToTuple<RssParser['parser_type']>;
/** rss parser 语言 */
export type RssParserLang = UnionToTuple<RssParser['language']>;
/** 重命名方式 */
export type RenameMethod = UnionToTuple<BangumiManage['rename_method']>;
/** 代理类型 */
export type ProxyType = UnionToTuple<Proxy['type']>;
/** 通知类型 */
export type NotificationType = UnionToTuple<Notification['type']>;
/** OpenAI Model List */
export type OpenAIModel = UnionToTuple<ExperimentalOpenAI['model']>;
/** OpenAI API Type */
export type OpenAIType = UnionToTuple<ExperimentalOpenAI['api_type']>;

View File

@@ -15,56 +15,32 @@ declare global {
const apiRSS: typeof import('../../src/api/rss')['apiRSS']
const apiSearch: typeof import('../../src/api/search')['apiSearch']
const assert: typeof import('vitest')['assert']
const asyncComputed: typeof import('@vueuse/core')['asyncComputed']
const autoResetRef: typeof import('@vueuse/core')['autoResetRef']
const axios: typeof import('../../src/utils/axios')['axios']
const beforeAll: typeof import('vitest')['beforeAll']
const beforeEach: typeof import('vitest')['beforeEach']
const chai: typeof import('vitest')['chai']
const computed: typeof import('vue')['computed']
const computedAsync: typeof import('@vueuse/core')['computedAsync']
const computedEager: typeof import('@vueuse/core')['computedEager']
const computedInject: typeof import('@vueuse/core')['computedInject']
const computedWithControl: typeof import('@vueuse/core')['computedWithControl']
const controlledComputed: typeof import('@vueuse/core')['controlledComputed']
const controlledRef: typeof import('@vueuse/core')['controlledRef']
const createApp: typeof import('vue')['createApp']
const createEventHook: typeof import('@vueuse/core')['createEventHook']
const createGlobalState: typeof import('@vueuse/core')['createGlobalState']
const createInjectionState: typeof import('@vueuse/core')['createInjectionState']
const createPinia: typeof import('pinia')['createPinia']
const createReactiveFn: typeof import('@vueuse/core')['createReactiveFn']
const createSharedComposable: typeof import('@vueuse/core')['createSharedComposable']
const createUnrefFn: typeof import('@vueuse/core')['createUnrefFn']
const customRef: typeof import('vue')['customRef']
const debouncedRef: typeof import('@vueuse/core')['debouncedRef']
const debouncedWatch: typeof import('@vueuse/core')['debouncedWatch']
const defineAsyncComponent: typeof import('vue')['defineAsyncComponent']
const defineComponent: typeof import('vue')['defineComponent']
const defineLoader: typeof import('vue-router/auto')['defineLoader']
const definePage: typeof import('unplugin-vue-router/runtime')['_definePage']
const defineStore: typeof import('pinia')['defineStore']
const describe: typeof import('vitest')['describe']
const eagerComputed: typeof import('@vueuse/core')['eagerComputed']
const effectScope: typeof import('vue')['effectScope']
const expect: typeof import('vitest')['expect']
const extendRef: typeof import('@vueuse/core')['extendRef']
const getActivePinia: typeof import('pinia')['getActivePinia']
const getCurrentInstance: typeof import('vue')['getCurrentInstance']
const getCurrentScope: typeof import('vue')['getCurrentScope']
const h: typeof import('vue')['h']
const ignorableWatch: typeof import('@vueuse/core')['ignorableWatch']
const inject: typeof import('vue')['inject']
const isDefined: typeof import('@vueuse/core')['isDefined']
const isProxy: typeof import('vue')['isProxy']
const isReactive: typeof import('vue')['isReactive']
const isReadonly: typeof import('vue')['isReadonly']
const isRef: typeof import('vue')['isRef']
const it: typeof import('vitest')['it']
const logicAnd: typeof import('@vueuse/core')['logicAnd']
const logicNot: typeof import('@vueuse/core')['logicNot']
const logicOr: typeof import('@vueuse/core')['logicOr']
const makeDestructurable: typeof import('@vueuse/core')['makeDestructurable']
const mapActions: typeof import('pinia')['mapActions']
const mapGetters: typeof import('pinia')['mapGetters']
const mapState: typeof import('pinia')['mapState']
@@ -78,37 +54,20 @@ declare global {
const onBeforeRouteUpdate: typeof import('vue-router/auto')['onBeforeRouteUpdate']
const onBeforeUnmount: typeof import('vue')['onBeforeUnmount']
const onBeforeUpdate: typeof import('vue')['onBeforeUpdate']
const onClickOutside: typeof import('@vueuse/core')['onClickOutside']
const onDeactivated: typeof import('vue')['onDeactivated']
const onErrorCaptured: typeof import('vue')['onErrorCaptured']
const onKeyStroke: typeof import('@vueuse/core')['onKeyStroke']
const onLongPress: typeof import('@vueuse/core')['onLongPress']
const onMounted: typeof import('vue')['onMounted']
const onRenderTracked: typeof import('vue')['onRenderTracked']
const onRenderTriggered: typeof import('vue')['onRenderTriggered']
const onScopeDispose: typeof import('vue')['onScopeDispose']
const onServerPrefetch: typeof import('vue')['onServerPrefetch']
const onStartTyping: typeof import('@vueuse/core')['onStartTyping']
const onUnmounted: typeof import('vue')['onUnmounted']
const onUpdated: typeof import('vue')['onUpdated']
const pausableWatch: typeof import('@vueuse/core')['pausableWatch']
const provide: typeof import('vue')['provide']
const reactify: typeof import('@vueuse/core')['reactify']
const reactifyObject: typeof import('@vueuse/core')['reactifyObject']
const reactive: typeof import('vue')['reactive']
const reactiveComputed: typeof import('@vueuse/core')['reactiveComputed']
const reactiveOmit: typeof import('@vueuse/core')['reactiveOmit']
const reactivePick: typeof import('@vueuse/core')['reactivePick']
const readonly: typeof import('vue')['readonly']
const ref: typeof import('vue')['ref']
const refAutoReset: typeof import('@vueuse/core')['refAutoReset']
const refDebounced: typeof import('@vueuse/core')['refDebounced']
const refDefault: typeof import('@vueuse/core')['refDefault']
const refThrottled: typeof import('@vueuse/core')['refThrottled']
const refWithControl: typeof import('@vueuse/core')['refWithControl']
const resolveComponent: typeof import('vue')['resolveComponent']
const resolveRef: typeof import('@vueuse/core')['resolveRef']
const resolveUnref: typeof import('@vueuse/core')['resolveUnref']
const setActivePinia: typeof import('pinia')['setActivePinia']
const setMapStoreSuffix: typeof import('pinia')['setMapStoreSuffix']
const shallowReactive: typeof import('vue')['shallowReactive']
@@ -116,188 +75,36 @@ declare global {
const shallowRef: typeof import('vue')['shallowRef']
const storeToRefs: typeof import('pinia')['storeToRefs']
const suite: typeof import('vitest')['suite']
const syncRef: typeof import('@vueuse/core')['syncRef']
const syncRefs: typeof import('@vueuse/core')['syncRefs']
const templateRef: typeof import('@vueuse/core')['templateRef']
const test: typeof import('vitest')['test']
const throttledRef: typeof import('@vueuse/core')['throttledRef']
const throttledWatch: typeof import('@vueuse/core')['throttledWatch']
const toRaw: typeof import('vue')['toRaw']
const toReactive: typeof import('@vueuse/core')['toReactive']
const toRef: typeof import('vue')['toRef']
const toRefs: typeof import('vue')['toRefs']
const triggerRef: typeof import('vue')['triggerRef']
const tryOnBeforeMount: typeof import('@vueuse/core')['tryOnBeforeMount']
const tryOnBeforeUnmount: typeof import('@vueuse/core')['tryOnBeforeUnmount']
const tryOnMounted: typeof import('@vueuse/core')['tryOnMounted']
const tryOnScopeDispose: typeof import('@vueuse/core')['tryOnScopeDispose']
const tryOnUnmounted: typeof import('@vueuse/core')['tryOnUnmounted']
const unref: typeof import('vue')['unref']
const unrefElement: typeof import('@vueuse/core')['unrefElement']
const until: typeof import('@vueuse/core')['until']
const useActiveElement: typeof import('@vueuse/core')['useActiveElement']
const useApi: typeof import('../../src/hooks/useApi')['useApi']
const useAppInfo: typeof import('../../src/hooks/useAppInfo')['useAppInfo']
const useAsyncQueue: typeof import('@vueuse/core')['useAsyncQueue']
const useAsyncState: typeof import('@vueuse/core')['useAsyncState']
const useAttrs: typeof import('vue')['useAttrs']
const useAuth: typeof import('../../src/hooks/useAuth')['useAuth']
const useBangumiStore: typeof import('../../src/store/bangumi')['useBangumiStore']
const useBase64: typeof import('@vueuse/core')['useBase64']
const useBattery: typeof import('@vueuse/core')['useBattery']
const useBluetooth: typeof import('@vueuse/core')['useBluetooth']
const useBreakpointQuery: typeof import('../../src/hooks/useBreakpointQuery')['useBreakpointQuery']
const useBreakpoints: typeof import('@vueuse/core')['useBreakpoints']
const useBroadcastChannel: typeof import('@vueuse/core')['useBroadcastChannel']
const useBrowserLocation: typeof import('@vueuse/core')['useBrowserLocation']
const useCached: typeof import('@vueuse/core')['useCached']
const useClamp: typeof import('@vueuse/core')['useClamp']
const useClipboard: typeof import('@vueuse/core')['useClipboard']
const useColorMode: typeof import('@vueuse/core')['useColorMode']
const useConfigStore: typeof import('../../src/store/config')['useConfigStore']
const useConfirmDialog: typeof import('@vueuse/core')['useConfirmDialog']
const useCounter: typeof import('@vueuse/core')['useCounter']
const useCssModule: typeof import('vue')['useCssModule']
const useCssVar: typeof import('@vueuse/core')['useCssVar']
const useCssVars: typeof import('vue')['useCssVars']
const useCurrentElement: typeof import('@vueuse/core')['useCurrentElement']
const useCycleList: typeof import('@vueuse/core')['useCycleList']
const useDark: typeof import('@vueuse/core')['useDark']
const useDateFormat: typeof import('@vueuse/core')['useDateFormat']
const useDebounce: typeof import('@vueuse/core')['useDebounce']
const useDebounceFn: typeof import('@vueuse/core')['useDebounceFn']
const useDebouncedRefHistory: typeof import('@vueuse/core')['useDebouncedRefHistory']
const useDeviceMotion: typeof import('@vueuse/core')['useDeviceMotion']
const useDeviceOrientation: typeof import('@vueuse/core')['useDeviceOrientation']
const useDevicePixelRatio: typeof import('@vueuse/core')['useDevicePixelRatio']
const useDevicesList: typeof import('@vueuse/core')['useDevicesList']
const useDisplayMedia: typeof import('@vueuse/core')['useDisplayMedia']
const useDocumentVisibility: typeof import('@vueuse/core')['useDocumentVisibility']
const useDraggable: typeof import('@vueuse/core')['useDraggable']
const useDropZone: typeof import('@vueuse/core')['useDropZone']
const useElementBounding: typeof import('@vueuse/core')['useElementBounding']
const useElementByPoint: typeof import('@vueuse/core')['useElementByPoint']
const useElementHover: typeof import('@vueuse/core')['useElementHover']
const useElementSize: typeof import('@vueuse/core')['useElementSize']
const useElementVisibility: typeof import('@vueuse/core')['useElementVisibility']
const useEventBus: typeof import('@vueuse/core')['useEventBus']
const useEventListener: typeof import('@vueuse/core')['useEventListener']
const useEventSource: typeof import('@vueuse/core')['useEventSource']
const useEyeDropper: typeof import('@vueuse/core')['useEyeDropper']
const useFavicon: typeof import('@vueuse/core')['useFavicon']
const useFetch: typeof import('@vueuse/core')['useFetch']
const useFileDialog: typeof import('@vueuse/core')['useFileDialog']
const useFileSystemAccess: typeof import('@vueuse/core')['useFileSystemAccess']
const useFocus: typeof import('@vueuse/core')['useFocus']
const useFocusWithin: typeof import('@vueuse/core')['useFocusWithin']
const useFps: typeof import('@vueuse/core')['useFps']
const useFullscreen: typeof import('@vueuse/core')['useFullscreen']
const useGamepad: typeof import('@vueuse/core')['useGamepad']
const useGeolocation: typeof import('@vueuse/core')['useGeolocation']
const useI18n: typeof import('vue-i18n')['useI18n']
const useIdle: typeof import('@vueuse/core')['useIdle']
const useImage: typeof import('@vueuse/core')['useImage']
const useInfiniteScroll: typeof import('@vueuse/core')['useInfiniteScroll']
const useIntersectionObserver: typeof import('@vueuse/core')['useIntersectionObserver']
const useInterval: typeof import('@vueuse/core')['useInterval']
const useIntervalFn: typeof import('@vueuse/core')['useIntervalFn']
const useKeyModifier: typeof import('@vueuse/core')['useKeyModifier']
const useLastChanged: typeof import('@vueuse/core')['useLastChanged']
const useLocalStorage: typeof import('@vueuse/core')['useLocalStorage']
const useLogStore: typeof import('../../src/store/log')['useLogStore']
const useMagicKeys: typeof import('@vueuse/core')['useMagicKeys']
const useManualRefHistory: typeof import('@vueuse/core')['useManualRefHistory']
const useMediaControls: typeof import('@vueuse/core')['useMediaControls']
const useMediaQuery: typeof import('@vueuse/core')['useMediaQuery']
const useMemoize: typeof import('@vueuse/core')['useMemoize']
const useMemory: typeof import('@vueuse/core')['useMemory']
const useMessage: typeof import('../../src/hooks/useMessage')['useMessage']
const useMounted: typeof import('@vueuse/core')['useMounted']
const useMouse: typeof import('@vueuse/core')['useMouse']
const useMouseInElement: typeof import('@vueuse/core')['useMouseInElement']
const useMousePressed: typeof import('@vueuse/core')['useMousePressed']
const useMutationObserver: typeof import('@vueuse/core')['useMutationObserver']
const useMyI18n: typeof import('../../src/hooks/useMyI18n')['useMyI18n']
const useNavigatorLanguage: typeof import('@vueuse/core')['useNavigatorLanguage']
const useNetwork: typeof import('@vueuse/core')['useNetwork']
const useNow: typeof import('@vueuse/core')['useNow']
const useObjectUrl: typeof import('@vueuse/core')['useObjectUrl']
const useOffsetPagination: typeof import('@vueuse/core')['useOffsetPagination']
const useOnline: typeof import('@vueuse/core')['useOnline']
const usePageLeave: typeof import('@vueuse/core')['usePageLeave']
const useParallax: typeof import('@vueuse/core')['useParallax']
const usePermission: typeof import('@vueuse/core')['usePermission']
const usePlayerStore: typeof import('../../src/store/player')['usePlayerStore']
const usePointer: typeof import('@vueuse/core')['usePointer']
const usePointerSwipe: typeof import('@vueuse/core')['usePointerSwipe']
const usePreferredColorScheme: typeof import('@vueuse/core')['usePreferredColorScheme']
const usePreferredDark: typeof import('@vueuse/core')['usePreferredDark']
const usePreferredLanguages: typeof import('@vueuse/core')['usePreferredLanguages']
const useProgramStore: typeof import('../../src/store/program')['useProgramStore']
const useRSSStore: typeof import('../../src/store/rss')['useRSSStore']
const useRafFn: typeof import('@vueuse/core')['useRafFn']
const useRefHistory: typeof import('@vueuse/core')['useRefHistory']
const useResizeObserver: typeof import('@vueuse/core')['useResizeObserver']
const useRoute: typeof import('vue-router/auto')['useRoute']
const useRouter: typeof import('vue-router/auto')['useRouter']
const useScreenOrientation: typeof import('@vueuse/core')['useScreenOrientation']
const useScreenSafeArea: typeof import('@vueuse/core')['useScreenSafeArea']
const useScriptTag: typeof import('@vueuse/core')['useScriptTag']
const useScroll: typeof import('@vueuse/core')['useScroll']
const useScrollLock: typeof import('@vueuse/core')['useScrollLock']
const useSearchStore: typeof import('../../src/store/search')['useSearchStore']
const useSessionStorage: typeof import('@vueuse/core')['useSessionStorage']
const useShare: typeof import('@vueuse/core')['useShare']
const useSlots: typeof import('vue')['useSlots']
const useSpeechRecognition: typeof import('@vueuse/core')['useSpeechRecognition']
const useSpeechSynthesis: typeof import('@vueuse/core')['useSpeechSynthesis']
const useStepper: typeof import('@vueuse/core')['useStepper']
const useStorage: typeof import('@vueuse/core')['useStorage']
const useStorageAsync: typeof import('@vueuse/core')['useStorageAsync']
const useStyleTag: typeof import('@vueuse/core')['useStyleTag']
const useSwipe: typeof import('@vueuse/core')['useSwipe']
const useTemplateRefsList: typeof import('@vueuse/core')['useTemplateRefsList']
const useTextSelection: typeof import('@vueuse/core')['useTextSelection']
const useTextareaAutosize: typeof import('@vueuse/core')['useTextareaAutosize']
const useThrottle: typeof import('@vueuse/core')['useThrottle']
const useThrottleFn: typeof import('@vueuse/core')['useThrottleFn']
const useThrottledRefHistory: typeof import('@vueuse/core')['useThrottledRefHistory']
const useTimeAgo: typeof import('@vueuse/core')['useTimeAgo']
const useTimeout: typeof import('@vueuse/core')['useTimeout']
const useTimeoutFn: typeof import('@vueuse/core')['useTimeoutFn']
const useTimeoutPoll: typeof import('@vueuse/core')['useTimeoutPoll']
const useTimestamp: typeof import('@vueuse/core')['useTimestamp']
const useTitle: typeof import('@vueuse/core')['useTitle']
const useToggle: typeof import('@vueuse/core')['useToggle']
const useTransition: typeof import('@vueuse/core')['useTransition']
const useUrlSearchParams: typeof import('@vueuse/core')['useUrlSearchParams']
const useUserMedia: typeof import('@vueuse/core')['useUserMedia']
const useVModel: typeof import('@vueuse/core')['useVModel']
const useVModels: typeof import('@vueuse/core')['useVModels']
const useVibrate: typeof import('@vueuse/core')['useVibrate']
const useVirtualList: typeof import('@vueuse/core')['useVirtualList']
const useWakeLock: typeof import('@vueuse/core')['useWakeLock']
const useWebNotification: typeof import('@vueuse/core')['useWebNotification']
const useWebSocket: typeof import('@vueuse/core')['useWebSocket']
const useWebWorker: typeof import('@vueuse/core')['useWebWorker']
const useWebWorkerFn: typeof import('@vueuse/core')['useWebWorkerFn']
const useWindowFocus: typeof import('@vueuse/core')['useWindowFocus']
const useWindowScroll: typeof import('@vueuse/core')['useWindowScroll']
const useWindowSize: typeof import('@vueuse/core')['useWindowSize']
const vi: typeof import('vitest')['vi']
const vitest: typeof import('vitest')['vitest']
const watch: typeof import('vue')['watch']
const watchArray: typeof import('@vueuse/core')['watchArray']
const watchAtMost: typeof import('@vueuse/core')['watchAtMost']
const watchDebounced: typeof import('@vueuse/core')['watchDebounced']
const watchEffect: typeof import('vue')['watchEffect']
const watchIgnorable: typeof import('@vueuse/core')['watchIgnorable']
const watchOnce: typeof import('@vueuse/core')['watchOnce']
const watchPausable: typeof import('@vueuse/core')['watchPausable']
const watchPostEffect: typeof import('vue')['watchPostEffect']
const watchSyncEffect: typeof import('vue')['watchSyncEffect']
const watchThrottled: typeof import('@vueuse/core')['watchThrottled']
const watchTriggerable: typeof import('@vueuse/core')['watchTriggerable']
const watchWithFilter: typeof import('@vueuse/core')['watchWithFilter']
const whenever: typeof import('@vueuse/core')['whenever']
}

View File

@@ -19,6 +19,7 @@ declare module '@vue/runtime-core' {
AbContainer: typeof import('./../../src/components/ab-container.vue')['default']
AbEditRule: typeof import('./../../src/components/ab-edit-rule.vue')['default']
AbFoldPanel: typeof import('./../../src/components/ab-fold-panel.vue')['default']
AbImage: typeof import('./../../src/components/ab-image.vue')['default']
AbLabel: typeof import('./../../src/components/ab-label.vue')['default']
AbPageTitle: typeof import('./../../src/components/basic/ab-page-title.vue')['default']
AbPopup: typeof import('./../../src/components/ab-popup.vue')['default']

View File

@@ -1,39 +1 @@
/**
* 将联合类型转为对应的交叉函数类型
* @template U 联合类型
*/
export type UnionToInterFunction<U> = (
U extends any ? (k: () => U) => void : never
) extends (k: infer I) => void
? I
: never;
/**
* 获取联合类型中的最后一个类型
* @template U 联合类型
*/
export type GetUnionLast<U> = UnionToInterFunction<U> extends { (): infer A }
? A
: never;
/**
* 在元组类型中前置插入一个新的类型(元素);
* @template Tuple 元组类型
* @template E 新的类型
*/
export type Prepend<Tuple extends any[], E> = [E, ...Tuple];
/**
* 联合类型转元组类型;
* @template Union 联合类型
* @template T 初始元组类型
* @template Last 传入联合类型中的最后一个类型(元素),自动生成,内部使用
*/
export type UnionToTuple<
Union,
T extends any[] = [],
Last = GetUnionLast<Union>
> = {
0: T;
1: UnionToTuple<Exclude<Union, Last>, Prepend<T, Last>>;
}[[Union] extends [never] ? 0 : 1];
export type TupleToUnion<T extends any[]> = T[number];

View File

@@ -25,14 +25,7 @@ export default defineConfig(({ mode }) => ({
}),
UnoCSS(),
AutoImport({
imports: [
'vue',
'vitest',
'pinia',
'@vueuse/core',
VueRouterAutoImports,
'vue-i18n',
],
imports: ['vue', 'vitest', 'pinia', VueRouterAutoImports, 'vue-i18n'],
dts: 'types/dts/auto-imports.d.ts',
dirs: ['src/api', 'src/store', 'src/hooks', 'src/utils'],
}),
@@ -88,6 +81,7 @@ export default defineConfig(({ mode }) => ({
css: {
preprocessorOptions: {
scss: {
api: 'modern-compiler',
additionalData: '@import "./src/style/mixin.scss";',
},
},