chore(deps): 前端构建与测试工具链升级到 Vite 8 / Vitest 5 / TS 6 / Storybook 10

- vite 8、@vitejs/plugin-vue 6、vitest 5、happy-dom 21、@vue/test-utils 2.5、
  typescript ~6.0.3、vue-tsc 3.3、unocss 66.11、unplugin-auto-import 21、
  unplugin-vue-components 32、sass-embedded 1.105、playwright 1.64.1、storybook 10.6
- vite.config:去掉 defineModel 开关;Sass 改用 @use;esbuild.drop 换成
  rolldown 的 dropConsole;__dirname 换成 import.meta.dirname;两处 alias
  改为 resolve.tsconfigPaths
- 删除 plugin-vue-jsx:三个 SFC 的 JSX 改写为 h()
- 删除 preset-attributify:模板里的属性式工具类全部改回 class
  (含 space-y-*、text-*、line、i-carbon-* 等,不止 5 处 flex="~")
- 删除 vite-plugin-pwa:改为静态 manifest.webmanifest + 自毁 sw.js(4.1 删除)
- tsconfig:moduleResolution Bundler、去掉 baseUrl 与 JSX 配置,
  app 的 types 只留 vitest/globals
- Storybook 10:addon-links + addon-docs,stories 改从 @storybook/vue3-vite 导入类型
- @intlify/unplugin-vue-i18n 0.11 -> 11.2.7(Vite 8 下旧版产物被 vite-json 再次解析而失败);
  新版只产出 AST 文案,故打开 __INTLIFY_JIT_COMPILATION__,hasKey 识别 AST 叶子
This commit is contained in:
Estrella Pan
2026-10-20 11:00:31 +02:00
parent 02e0c3ea63
commit 2f1fa317d1
65 changed files with 3811 additions and 8357 deletions

1
.gitignore vendored
View File

@@ -197,7 +197,6 @@ webui/test-results/
dist.zip
dist-ssr
*.local
dev-dist
# Editor directories and files
.vscode/*

View File

@@ -54,7 +54,7 @@ Semantic scale only: `--z-dropdown` 10 · `--z-sticky` 20 · `--z-fixed` 30 · `
- **Universal `ab-*` layer** (`src/components/basic/`): pages and composites use only `ab-*` components — never raw `NButton`/`NInput`/bare styled `<button>`. Where naive-ui fits, the `ab-*` component wraps it (`ab-select`, `ab-switch`, `ab-tooltip`); where it doesn't, the component is custom on tokens (`ab-button`, `ab-tag`, `ab-modal`).
- **Composites**: app-specific `ab-*` components (`ab-add-rss`, `ab-edit-rule`, calendar pieces, layout shells) compose the universal layer + tokens.
- **Utility CSS**: UnoCSS with attributify preset; prefer token-backed values over arbitrary literals.
- **Utility CSS**: UnoCSS (`presetWind3`), utilities go in `class` (no attributify); prefer token-backed values over arbitrary literals.
- Every interactive component ships all states: default, hover, focus-visible, active, disabled, loading, error — and ≥44px touch targets on mobile (`--touch-target`).
- Soft Ink component grammar: filled controls on `--color-surface-2` (border only on focus/error); tags are hairline chips with a square semantic mark and ink text; status is a square mark + ink label; tabs underline (no thumb); alerts are hairline outlines led by a bold colored word (no icons, no tinted fills); toasts are inverted ink chips; empty states are left-aligned prose with one action; loading is skeletons, not spinners-in-content.
- Every universal component ships a Storybook story and (where it has behavior) a vitest test in `__tests__/`.

View File

@@ -1,6 +1,5 @@
/build
/dist
/dev-dist
/e2e/.auth
/playwright-report
/test-results

View File

@@ -1,6 +1,5 @@
/build
/dist
/dev-dist
/e2e/.auth
/playwright-report
/test-results

View File

@@ -3,18 +3,11 @@ import Unocss from 'unocss/vite';
const config: StorybookConfig = {
stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|ts|tsx)'],
addons: [
'@storybook/addon-links',
'@storybook/addon-essentials',
'@storybook/addon-interactions',
],
addons: ['@storybook/addon-links', '@storybook/addon-docs'],
framework: {
name: '@storybook/vue3-vite',
options: {},
},
docs: {
autodocs: 'tag',
},
viteFinal(config) {
config.plugins?.push(Unocss());
// Add other configuration here depending on your use case

View File

@@ -1,6 +1,6 @@
import type { Preview } from '@storybook/vue3';
import type { Preview } from '@storybook/vue3-vite';
import '@unocss/reset/tailwind-compat.css';
import 'uno.css';
import 'virtual:uno.css';
const preview: Preview = {
parameters: {

View File

@@ -10,6 +10,7 @@
<meta name="theme-color" content="#FAFAFA" />
<link rel="icon" href="/images/logo.svg" />
<link rel="apple-touch-icon" href="/images/apple-touch-icon.png" />
<link rel="manifest" href="/manifest.webmanifest" />
<meta name="description" content="Automated Bangumi Download Tool" />
<!-- Self-hosted Inter (variable, 300–700): no render-blocking Google
Fonts round-trip, and it works where fonts.googleapis.com is slow

View File

@@ -39,39 +39,31 @@
"@autobangumi/plugin-ui": "workspace:*",
"@icon-park/vue-next": "^1.4.2",
"@iconify-json/simple-icons": "1.2.89",
"@intlify/unplugin-vue-i18n": "^0.11.0",
"@playwright/test": "1.61.1",
"@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.6.20",
"@storybook/vue3-vite": "^7.6.20",
"@intlify/unplugin-vue-i18n": "^11.2.7",
"@playwright/test": "1.64.1",
"@storybook/addon-docs": "^10.6.2",
"@storybook/addon-links": "^10.6.2",
"@storybook/vue3-vite": "^10.6.2",
"@types/node": "^24",
"@unocss/preset-attributify": "^0.55.7",
"@unocss/preset-rem-to-px": "^0.51.13",
"@unocss/reset": "^0.51.13",
"@vitejs/plugin-vue": "^4.6.2",
"@vitejs/plugin-vue-jsx": "^3.1.0",
"@vue/runtime-dom": "^3.5.8",
"@vue/test-utils": "^2.4.6",
"@unocss/preset-rem-to-px": "^66.11.0",
"@unocss/reset": "^66.11.0",
"@vitejs/plugin-vue": "^6.0.10",
"@vue/test-utils": "^2.5.1",
"eslint": "^8.57.1",
"eslint-config-prettier": "^8.10.0",
"eslint-plugin-storybook": "^0.6.15",
"happy-dom": "^12.10.3",
"eslint-plugin-storybook": "^10.6.2",
"happy-dom": "^21.0.3",
"husky": "^8.0.3",
"prettier": "^2.8.8",
"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",
"sass-embedded": "^1.105.2",
"storybook": "^10.6.2",
"typescript": "~6.0.3",
"unocss": "^66.11.0",
"unplugin-auto-import": "^21.1.0",
"unplugin-vue-components": "^32.1.0",
"unplugin-vue-router": "^0.6.4",
"vite": "^4.5.5",
"vite-plugin-pwa": "^0.16.7",
"vitest": "^0.30.1",
"vue-tsc": "^1.8.27"
"vite": "^8.3.4",
"vitest": "^5.0.5",
"vue-tsc": "^3.3.13"
}
}

View File

@@ -2,7 +2,7 @@
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "Node",
"moduleResolution": "Bundler",
"strict": true,
"isolatedModules": true,
"lib": ["ESNext", "DOM"],

11207
webui/pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,9 +1,10 @@
packages:
- packages/*
publicHoistPattern:
- '@vue/runtime-core'
- '*eslint*'
- '*prettier*'
allowBuilds:
esbuild: true
vue-demi: true
# sass 的可选依赖,平台预编译包已覆盖,无需跑源码构建
'@parcel/watcher': false

View File

@@ -0,0 +1,30 @@
{
"name": "AutoBangumi",
"short_name": "AutoBangumi",
"start_url": "./",
"display": "standalone",
"background_color": "#ffffff",
"lang": "en",
"scope": "./",
"description": "Automated Bangumi Download Tool",
"theme_color": "#ffffff",
"icons": [
{
"src": "/images/logo.svg",
"sizes": "any",
"type": "image/svg+xml",
"purpose": "any"
},
{
"src": "/images/pwa-192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/images/pwa-512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "any"
}
]
}

16
webui/public/sw.js Normal file
View File

@@ -0,0 +1,16 @@
// ponytail: 自毁 SW,清理 2023-09 前注册的旧 SW;4.1 删除本文件
// 旧版 vite-plugin-pwa 注册的 SW 仍会按 /sw.js 检查更新,取到本文件后:
// 清空它留下的缓存、注销自己,再让它控制的页面重新加载,回到无 SW 的普通请求
self.addEventListener('install', () => self.skipWaiting());
self.addEventListener('activate', (event) => {
event.waitUntil(
(async () => {
const keys = await caches.keys();
await Promise.all(keys.map((key) => caches.delete(key)));
await self.registration.unregister();
const clients = await self.clients.matchAll({ type: 'window' });
clients.forEach((client) => client.navigate(client.url));
})()
);
});

View File

@@ -155,7 +155,7 @@ const naiveTheme = computed(() => (isDark.value ? darkTheme : null));
</template>
<style lang="scss">
@import './style/var';
@import './style/transition';
@import './style/global';
@use './style/var';
@use './style/transition';
@use './style/global';
</style>

View File

@@ -12,7 +12,7 @@ const { user, update } = useAuth();
size="sm"
:title="$t('topbar.profile.pop_title')"
>
<div space-y-16>
<div class="space-y-16">
<ab-field :label="$t('topbar.profile.username')">
<ab-input
v-model="user.username"

View File

@@ -7,7 +7,7 @@ describe('ab-input', () => {
const wrapper = mount(AbInput, {
props: {
modelValue: 'abc',
'onUpdate:modelValue': (v: string) =>
'onUpdate:modelValue': (v: string | number) =>
wrapper.setProps({ modelValue: v }),
},
});
@@ -27,7 +27,7 @@ describe('ab-input', () => {
props: {
modelValue: 'abc',
clearable: true,
'onUpdate:modelValue': (v: string) =>
'onUpdate:modelValue': (v: string | number) =>
wrapper.setProps({ modelValue: v }),
},
});

View File

@@ -1,4 +1,4 @@
import type { Meta, StoryObj } from '@storybook/vue3';
import type { Meta, StoryObj } from '@storybook/vue3-vite';
import AbAlert from './ab-alert.vue';

View File

@@ -1,4 +1,4 @@
import type { Meta, StoryObj } from '@storybook/vue3';
import type { Meta, StoryObj } from '@storybook/vue3-vite';
import AbBadge from './ab-badge.vue';

View File

@@ -1,4 +1,4 @@
import type { Meta, StoryObj } from '@storybook/vue3';
import type { Meta, StoryObj } from '@storybook/vue3-vite';
import AbButton from './ab-button.vue';

View File

@@ -1,4 +1,4 @@
import type { Meta, StoryObj } from '@storybook/vue3';
import type { Meta, StoryObj } from '@storybook/vue3-vite';
import AbEmpty from './ab-empty.vue';

View File

@@ -1,4 +1,4 @@
import type { Meta, StoryObj } from '@storybook/vue3';
import type { Meta, StoryObj } from '@storybook/vue3-vite';
import AbField from './ab-field.vue';
import AbInput from './ab-input.vue';

View File

@@ -1,4 +1,4 @@
import type { Meta, StoryObj } from '@storybook/vue3';
import type { Meta, StoryObj } from '@storybook/vue3-vite';
import { AddOne, Delete, Edit } from '@icon-park/vue-next';
import AbIconButton from './ab-icon-button.vue';

View File

@@ -1,4 +1,4 @@
import type { Meta, StoryObj } from '@storybook/vue3';
import type { Meta, StoryObj } from '@storybook/vue3-vite';
import { ref } from 'vue';
import AbInput from './ab-input.vue';

View File

@@ -1,4 +1,4 @@
import type { Meta, StoryObj } from '@storybook/vue3';
import type { Meta, StoryObj } from '@storybook/vue3-vite';
import AbList from './ab-list.vue';

View File

@@ -1,4 +1,4 @@
import type { Meta, StoryObj } from '@storybook/vue3';
import type { Meta, StoryObj } from '@storybook/vue3-vite';
import AbButton from './ab-button.vue';
import AbMenu from './ab-menu.vue';

View File

@@ -1,4 +1,4 @@
import type { Meta, StoryObj } from '@storybook/vue3';
import type { Meta, StoryObj } from '@storybook/vue3-vite';
import { ref } from 'vue';
import AbButton from './ab-button.vue';

View File

@@ -1,4 +1,4 @@
import type { Meta, StoryObj } from '@storybook/vue3';
import type { Meta, StoryObj } from '@storybook/vue3-vite';
import AbPageTitle from './ab-page-title.vue';

View File

@@ -1,4 +1,4 @@
import type { Meta, StoryObj } from '@storybook/vue3';
import type { Meta, StoryObj } from '@storybook/vue3-vite';
import AbProgress from './ab-progress.vue';

View File

@@ -1,4 +1,4 @@
import type { Meta, StoryObj } from '@storybook/vue3';
import type { Meta, StoryObj } from '@storybook/vue3-vite';
import AbSearch from './ab-search.vue';

View File

@@ -1,4 +1,4 @@
import type { Meta, StoryObj } from '@storybook/vue3';
import type { Meta, StoryObj } from '@storybook/vue3-vite';
import { ref } from 'vue';
import AbSegmented from './ab-segmented.vue';

View File

@@ -1,4 +1,4 @@
import type { Meta, StoryObj } from '@storybook/vue3';
import type { Meta, StoryObj } from '@storybook/vue3-vite';
import { ref } from 'vue';
import AbSelect from './ab-select.vue';

View File

@@ -1,4 +1,4 @@
import type { Meta, StoryObj } from '@storybook/vue3';
import type { Meta, StoryObj } from '@storybook/vue3-vite';
import AbSkeleton from './ab-skeleton.vue';

View File

@@ -1,4 +1,4 @@
import type { Meta, StoryObj } from '@storybook/vue3';
import type { Meta, StoryObj } from '@storybook/vue3-vite';
import { ref } from 'vue';
import AbSplitButton from './ab-split-button.vue';

View File

@@ -1,4 +1,4 @@
import type { Meta, StoryObj } from '@storybook/vue3';
import type { Meta, StoryObj } from '@storybook/vue3-vite';
import AbStatus from './ab-status.vue';

View File

@@ -1,4 +1,4 @@
import type { Meta, StoryObj } from '@storybook/vue3';
import type { Meta, StoryObj } from '@storybook/vue3-vite';
import { ref } from 'vue';
import AbSwitch from './ab-switch.vue';

View File

@@ -1,4 +1,4 @@
import type { Meta, StoryObj } from '@storybook/vue3';
import type { Meta, StoryObj } from '@storybook/vue3-vite';
import AbTag from './ab-tag.vue';

View File

@@ -1,4 +1,4 @@
import type { Meta, StoryObj } from '@storybook/vue3';
import type { Meta, StoryObj } from '@storybook/vue3-vite';
import AbToolbar from './ab-toolbar.vue';

View File

@@ -1,4 +1,4 @@
import type { Meta, StoryObj } from '@storybook/vue3';
import type { Meta, StoryObj } from '@storybook/vue3-vite';
import AbButton from './ab-button.vue';
import AbTooltip from './ab-tooltip.vue';

View File

@@ -1,4 +1,4 @@
<script lang="tsx" setup>
<script lang="ts" setup>
import {
AppStore,
Calendar,
@@ -119,21 +119,23 @@ const navItems = computed(() => {
});
function Exit() {
return (
<button
type="button"
aria-label={t('sidebar.logout')}
class={[
return h(
'button',
{
type: 'button',
'aria-label': t('sidebar.logout'),
class: [
'sidebar-item sidebar-item--action',
isMobileOrTablet.value ? 'h-40' : '',
]}
onClick={logout}
>
<Logout size={20} />
{!isMobileOrTablet.value && show.value && (
<div class="sidebar-item-label">{t('sidebar.logout')}</div>
)}
</button>
],
onClick: logout,
},
[
h(Logout, { size: 20 }),
!isMobileOrTablet.value &&
show.value &&
h('div', { class: 'sidebar-item-label' }, t('sidebar.logout')),
]
);
}
</script>

View File

@@ -22,7 +22,7 @@ let generation = 0;
let teardown: (() => void) | null = null;
function fail(error: unknown) {
// 不用 `[plugin:id]` 式模板串:UnoCSS 的 attributify 会把它当成样式规则
// 不用 `[plugin:id]` 式模板串:UnoCSS 会把它当成任意属性规则
console.error('plugin component failed:', props.ui.plugin_id, error);
failed.value = true;
loading.value = false;

View File

@@ -218,7 +218,7 @@ const items = computed(() =>
</div>
</div>
<div v-if="downloader" space-y-8>
<div v-if="downloader" class="space-y-8">
<ab-alert v-if="downloader.provider === 'aria2'">
{{ $t('config.downloader_set.aria2_hint') }}
</ab-alert>

View File

@@ -91,7 +91,7 @@ const items = computed<SettingItem<BangumiManage>[]>(() => [
<template>
<ab-fold-panel :title="$t('config.manage_set.title')">
<div space-y-8>
<div class="space-y-8">
<ab-setting v-bind="enableItem" v-model:data="manage.enable"></ab-setting>
<ab-setting
v-for="i in slotItems"

View File

@@ -40,7 +40,7 @@ const items: SettingItem<Network>[] = [
<template>
<ab-fold-panel :title="$t('config.network_set.title')">
<div space-y-8>
<div class="space-y-8">
<ab-setting
v-for="i in items"
:key="i.configKey"

View File

@@ -51,7 +51,7 @@ const logItems: SettingItem<Log> = {
<template>
<ab-fold-panel :title="$t('config.normal_set.title')">
<div space-y-8>
<div class="space-y-8">
<ab-setting
v-for="i in programItems"
:key="i.configKey"

View File

@@ -271,7 +271,7 @@ function getFieldsForType(type: string) {
<template>
<ab-fold-panel :title="$t('config.notification_set.title')">
<div space-y-8>
<div class="space-y-8">
<!-- Global enable switch -->
<ab-setting
v-model:data="notificationEnabled"
@@ -282,7 +282,7 @@ function getFieldsForType(type: string) {
/>
<!-- Provider list -->
<div v-if="notificationEnabled" space-y-8>
<div v-if="notificationEnabled" class="space-y-8">
<div
v-for="(provider, index) in providers"
:key="index"
@@ -307,17 +307,16 @@ function getFieldsForType(type: string) {
>
<div
v-if="testingIndex === index"
i-carbon-circle-dash
animate-spin
class="i-carbon-circle-dash animate-spin"
/>
<div v-else i-carbon-play />
<div v-else class="i-carbon-play" />
</ab-icon-button>
<ab-icon-button
size="sm"
:label="$t('config.notification_set.edit')"
@click="openEditDialog(index)"
>
<div i-carbon-edit />
<div class="i-carbon-edit" />
</ab-icon-button>
<ab-icon-button
size="sm"
@@ -340,7 +339,7 @@ function getFieldsForType(type: string) {
:label="$t('config.notification_set.remove')"
@click="onRemoveProvider(index)"
>
<div i-carbon-trash-can />
<div class="i-carbon-trash-can" />
</ab-icon-button>
</div>
</div>
@@ -354,12 +353,12 @@ function getFieldsForType(type: string) {
{{ listResult.message }}
</div>
<div line></div>
<div class="line"></div>
<!-- Add provider button -->
<div flex="~ justify-end">
<div class="flex justify-end">
<ab-button size="sm" variant="primary" @click="openAddDialog">
<div i-carbon-add />
<div class="i-carbon-add" />
{{ $t('config.notification_set.add_provider') }}
</ab-button>
</div>
@@ -372,7 +371,7 @@ function getFieldsForType(type: string) {
size="sm"
:title="$t('config.notification_set.add_provider')"
>
<div space-y-16>
<div class="space-y-16">
<ab-field :label="$t('config.notification_set.type')">
<NSelect
v-model:value="newProvider.type"
@@ -421,8 +420,11 @@ function getFieldsForType(type: string) {
:disabled="testingIndex === -999"
@click="testNewProvider"
>
<div v-if="testingIndex === -999" i-carbon-circle-dash animate-spin />
<div v-else i-carbon-play />
<div
v-if="testingIndex === -999"
class="i-carbon-circle-dash animate-spin"
/>
<div v-else class="i-carbon-play" />
{{ $t('config.notification_set.test') }}
</ab-button>
<ab-button size="sm" @click="showAddDialog = false">
@@ -440,7 +442,7 @@ function getFieldsForType(type: string) {
size="sm"
:title="$t('config.notification_set.edit_provider')"
>
<div space-y-16>
<div class="space-y-16">
<ab-field :label="$t('config.notification_set.type')">
<NSelect
v-model:value="newProvider.type"
@@ -490,8 +492,11 @@ function getFieldsForType(type: string) {
:disabled="testingIndex === -999"
@click="testNewProvider"
>
<div v-if="testingIndex === -999" i-carbon-circle-dash animate-spin />
<div v-else i-carbon-play />
<div
v-if="testingIndex === -999"
class="i-carbon-circle-dash animate-spin"
/>
<div v-else class="i-carbon-play" />
{{ $t('config.notification_set.test') }}
</ab-button>
<ab-button size="sm" @click="showEditDialog = false">

View File

@@ -40,7 +40,7 @@ const items: SettingItem<RssParser>[] = [
<template>
<ab-fold-panel :title="$t('config.parser_set.title')">
<div space-y-8>
<div class="space-y-8">
<ab-setting
v-model:data="parser.enable"
:label="() => t('config.parser_set.enable')"

View File

@@ -81,43 +81,40 @@ function formatDate(dateString: string | null): string {
<template>
<ab-fold-panel :title="$t('passkey.title')">
<div space-y-8>
<div class="space-y-8">
<!-- 不支持提示 -->
<div v-if="!isSupported" text-orange-500 text-14>
<div v-if="!isSupported" class="text-orange-500 text-14">
{{ $t('passkey.not_supported') }}
</div>
<!-- 加载中 -->
<div v-else-if="loading" text-gray-500 text-14>
<div v-else-if="loading" class="text-gray-500 text-14">
{{ $t('passkey.loading') }}
</div>
<!-- 无 Passkey -->
<div v-else-if="passkeys.length === 0" text-gray-500 text-14>
<div v-else-if="passkeys.length === 0" class="text-gray-500 text-14">
{{ $t('passkey.no_passkeys') }}
</div>
<!-- Passkey 列表 -->
<div v-else space-y-8>
<div v-else class="space-y-8">
<div
v-for="passkey in passkeys"
:key="passkey.id"
flex="~ justify-between items-center"
p-12
bg-gray-50
rounded-8
class="flex justify-between items-center p-12 bg-gray-50 rounded-8"
>
<div>
<div font-medium>{{ passkey.name }}</div>
<div text-12 text-gray-500>
<div class="font-medium">{{ passkey.name }}</div>
<div class="text-12 text-gray-500">
{{ $t('passkey.created_at') }}:
{{ formatDate(passkey.created_at) }}
</div>
<div v-if="passkey.last_used_at" text-12 text-gray-500>
<div v-if="passkey.last_used_at" class="text-12 text-gray-500">
{{ $t('passkey.last_used') }}:
{{ formatDate(passkey.last_used_at) }}
</div>
<div v-if="passkey.backup_eligible" text-12 text-green-600>
<div v-if="passkey.backup_eligible" class="text-12 text-green-600">
{{ $t('passkey.synced') }}
</div>
</div>
@@ -132,10 +129,10 @@ function formatDate(dateString: string | null): string {
</div>
</div>
<div line></div>
<div class="line"></div>
<!-- 添加按钮 -->
<div flex="~ justify-end">
<div class="flex justify-end">
<ab-button
v-if="isSupported"
size="sm"
@@ -153,7 +150,7 @@ function formatDate(dateString: string | null): string {
size="sm"
:title="$t('passkey.register_title')"
>
<div space-y-16>
<div class="space-y-16">
<ab-field :label="$t('passkey.device_name')">
<ab-input
v-model="deviceName"
@@ -163,7 +160,7 @@ function formatDate(dateString: string | null): string {
/>
</ab-field>
<div text-14 text-gray-500>
<div class="text-14 text-gray-500">
{{ $t('passkey.register_hint') }}
</div>
</div>

View File

@@ -4,7 +4,7 @@ const { types, type, rawUrl } = storeToRefs(usePlayerStore());
<template>
<ab-fold-panel :title="$t('config.media_player_set.title')">
<div space-y-8>
<div class="space-y-8">
<ab-setting
v-model:data="type"
type="select"

View File

@@ -64,7 +64,7 @@ const items: SettingItem<Proxy>[] = [
<template>
<ab-fold-panel :title="$t('config.proxy_set.title')">
<div space-y-8>
<div class="space-y-8">
<ab-setting
v-for="i in items"
:key="i.configKey"

View File

@@ -208,19 +208,19 @@ function validateUrl(url: string): boolean {
<template>
<ab-fold-panel :title="$t('config.search_provider_set.title')">
<div space-y-8>
<div class="space-y-8">
<!-- Loading state -->
<div v-if="loading" text-gray-500 text-14>
<div v-if="loading" class="text-gray-500 text-14">
{{ $t('passkey.loading') }}
</div>
<!-- Empty state -->
<div v-else-if="providers.length === 0" text-gray-500 text-14>
<div v-else-if="providers.length === 0" class="text-gray-500 text-14">
{{ $t('config.search_provider_set.no_providers') }}
</div>
<!-- Provider list -->
<div v-else space-y-8>
<div v-else class="space-y-8">
<div
v-for="(provider, index) in providers"
:key="provider.name"
@@ -261,7 +261,7 @@ function validateUrl(url: string): boolean {
</div>
</div>
<div line></div>
<div class="line"></div>
<!-- Hint text -->
<div class="hint-text">
@@ -269,7 +269,7 @@ function validateUrl(url: string): boolean {
</div>
<!-- Add button -->
<div flex="~ justify-end">
<div class="flex justify-end">
<ab-button size="sm" variant="primary" @click="openAddDialog">
<Plus size="16" />
{{ $t('config.search_provider_set.add_new') }}
@@ -283,7 +283,7 @@ function validateUrl(url: string): boolean {
size="sm"
:title="$t('config.search_provider_set.add_title')"
>
<div space-y-16>
<div class="space-y-16">
<ab-segmented
v-model:value="formMode"
size="sm"
@@ -391,7 +391,7 @@ function validateUrl(url: string): boolean {
size="sm"
:title="$t('config.search_provider_set.edit_title')"
>
<div space-y-16>
<div class="space-y-16">
<ab-field :label="$t('config.search_provider_set.name')">
<ab-input
v-model="formName"

View File

@@ -32,7 +32,7 @@ const items: SettingItem<Security>[] = [
<template>
<ab-fold-panel :title="$t('config.security_set.title')">
<p class="hint-text">{{ $t('config.security_set.hint') }}</p>
<div space-y-8>
<div class="space-y-8">
<ab-setting
v-for="i in items"
:key="i.configKey"

View File

@@ -8,13 +8,17 @@ definePage({
const { t } = useI18n();
const title = computed(() => t('homepage.others.title'));
// 闭包放在脚本里:vue-tsc 3.3 把模板闭包里的导入对象误当 ref,补出 .value
const loadFn = () => apiBangumi.getOrphanTorrents();
const deleteOne = (id: number) => apiBangumi.deleteOrphanTorrent(id);
const deleteAll = () => apiBangumi.deleteOrphanTorrents();
</script>
<template>
<AbTorrentListPage
:title="title"
:load-fn="() => apiBangumi.getOrphanTorrents()"
:delete-one="(id: number) => apiBangumi.deleteOrphanTorrent(id)"
:delete-all="() => apiBangumi.deleteOrphanTorrents()"
:load-fn="loadFn"
:delete-one="deleteOne"
:delete-all="deleteAll"
/>
</template>

View File

@@ -1,6 +1,7 @@
<script lang="tsx" setup>
<script lang="ts" setup>
import { type DataTableColumns, NDataTable } from 'naive-ui';
import AbProgress from '@/components/basic/ab-progress.vue';
import AbTag from '@/components/basic/ab-tag.vue';
import { useConfirm } from '@/hooks/useConfirm';
import type { QbTorrentInfo, TorrentGroup } from '#/downloader';
import { useDownloaderInstances } from '@/hooks/useDownloaderInstances';
@@ -110,7 +111,7 @@ function stateLabel(state: string): string {
return map[state] || state;
}
function stateType(state: string): string {
function stateType(state: string): 'neutral' | 'success' | 'info' | 'danger' {
if (state.includes('paused') || state.includes('stopped')) return 'neutral';
if (state === 'downloading' || state === 'forcedDL') return 'success';
if (state.includes('UP') || state === 'uploading') return 'info';
@@ -142,17 +143,14 @@ const tableColumnsValue = computed<DataTableColumns<QbTorrentInfo>>(() => [
key: 'progress',
width: 160,
render(row: QbTorrentInfo) {
return (
<AbProgress
value={row.progress * 100}
label={`${Math.round(row.progress * 100)}%`}
state={
row.state === 'error' || row.state === 'missingFiles'
? 'error'
: 'active'
}
/>
);
return h(AbProgress, {
value: row.progress * 100,
label: `${Math.round(row.progress * 100)}%`,
state:
row.state === 'error' || row.state === 'missingFiles'
? 'error'
: 'active',
});
},
},
{
@@ -160,9 +158,10 @@ const tableColumnsValue = computed<DataTableColumns<QbTorrentInfo>>(() => [
key: 'state',
width: 100,
render(row: QbTorrentInfo) {
return (
<ab-tag type={stateType(row.state)} title={stateLabel(row.state)} />
);
return h(AbTag, {
type: stateType(row.state),
title: stateLabel(row.state),
});
},
},
{

View File

@@ -1,6 +1,7 @@
<script lang="tsx" setup>
<script lang="ts" setup>
import { type DataTableColumns, NDataTable, NTooltip } from 'naive-ui';
import AbButton from '@/components/basic/ab-button.vue';
import AbTag from '@/components/basic/ab-tag.vue';
import { useConfirm } from '@/hooks/useConfirm';
import type { RSS } from '#/rss';
@@ -63,15 +64,11 @@ const rssColumns = computed<DataTableColumns<RSS>>(() => [
key: 'name',
className: 'text-h3',
render(rss: RSS) {
return (
<div class="rss-name-cell">
<span class="rss-name" title={rss.name}>
{rss.name}
</span>
{rss.parser && <ab-tag type="info" title={rss.parser} />}
{rss.aggregate && <ab-tag type="info" title={t('rss.aggregate')} />}
</div>
);
return h('div', { class: 'rss-name-cell' }, [
h('span', { class: 'rss-name', title: rss.name }, rss.name),
rss.parser && h(AbTag, { type: 'info', title: rss.parser }),
rss.aggregate && h(AbTag, { type: 'info', title: t('rss.aggregate') }),
]);
},
},
{
@@ -90,26 +87,18 @@ const rssColumns = computed<DataTableColumns<RSS>>(() => [
align: 'right',
minWidth: 140,
render(rss: RSS) {
return (
<div flex="~ justify-end gap-x-8">
{rss.connection_status === 'healthy' && (
<ab-tag type="success" title={t('rss.connected')} />
)}
{rss.connection_status === 'error' && (
<NTooltip>
{{
trigger: () => <ab-tag type="danger" title={t('rss.error')} />,
default: () => rss.last_error || 'Unknown error',
}}
</NTooltip>
)}
{rss.enabled ? (
<ab-tag type="success" title={t('rss.active')} />
) : (
<ab-tag type="neutral" title={t('rss.inactive')} />
)}
</div>
);
return h('div', { class: 'flex justify-end gap-x-8' }, [
rss.connection_status === 'healthy' &&
h(AbTag, { type: 'success', title: t('rss.connected') }),
rss.connection_status === 'error' &&
h(NTooltip, null, {
trigger: () => h(AbTag, { type: 'danger', title: t('rss.error') }),
default: () => rss.last_error || 'Unknown error',
}),
rss.enabled
? h(AbTag, { type: 'success', title: t('rss.active') })
: h(AbTag, { type: 'neutral', title: t('rss.inactive') }),
]);
},
},
{
@@ -120,20 +109,21 @@ const rssColumns = computed<DataTableColumns<RSS>>(() => [
render(rss: RSS) {
const isRowRefreshing = refreshingId.value === rss.id;
const isRowFailed = refreshFailedId.value === rss.id;
return (
<AbButton
size="sm"
variant={isRowFailed ? 'danger' : 'secondary'}
{...{ title: t('rss.refresh') }}
disabled={isRowRefreshing}
onClick={() => onRefreshOne(rss.id)}
>
<div
class={
isRowRefreshing ? 'i-carbon-renew animate-spin' : 'i-carbon-renew'
}
/>
</AbButton>
return h(
AbButton,
{
size: 'sm',
variant: isRowFailed ? 'danger' : 'secondary',
title: t('rss.refresh'),
disabled: isRowRefreshing,
onClick: () => onRefreshOne(rss.id),
},
() =>
h('div', {
class: isRowRefreshing
? 'i-carbon-renew animate-spin'
: 'i-carbon-renew',
})
);
},
},
@@ -184,19 +174,19 @@ function onSelectedRSSUpdate(keys: unknown[]) {
<div class="rss-card-name">{{ item.name }}</div>
<div class="rss-card-url">{{ item.url }}</div>
<div class="rss-card-tags">
<ab-tag v-if="item.parser" type="info" :title="item.parser" />
<ab-tag v-if="item.aggregate" type="info" title="aggregate" />
<ab-tag
<AbTag v-if="item.parser" type="info" :title="item.parser" />
<AbTag v-if="item.aggregate" type="info" title="aggregate" />
<AbTag
v-if="item.connection_status === 'healthy'"
type="success"
:title="$t('rss.connected')"
/>
<ab-tag
<AbTag
v-if="item.connection_status === 'error'"
type="danger"
:title="$t('rss.error')"
/>
<ab-tag
<AbTag
:type="item.enabled ? 'success' : 'neutral'"
:title="
item.enabled

View File

@@ -1,6 +1,8 @@
// Design System Variables
// Light theme (default) + Dark theme (.dark class on html)
@use 'mixin' as *;
:root {
// --- Colors ---
--color-primary: #6C4AB6;

View File

@@ -5,12 +5,12 @@ import zhCN from '@/i18n/zh-CN.json';
const t = (key: string) => `t:${key}`;
// 应用里 @intlify/unplugin-vue-i18n 把 i18n/*.json 的文案预编译成消息函数,
// 这里同样把叶子换成函数,按生产环境的形态测试
// 应用里 @intlify/unplugin-vue-i18n 把 i18n/*.json 的文案预编译成 AST,
// 这里同样把叶子换成生产构建的 AST,按生产环境的形态测试
vi.mock('@/i18n/en.json', async (importOriginal) => {
const compile = (node: unknown): unknown =>
typeof node === 'string'
? () => node
? { t: 0, b: { t: 2, i: [{ t: 3 }], s: node } }
: Object.fromEntries(
Object.entries(node as object).map(([k, v]) => [k, compile(v)])
);

View File

@@ -22,6 +22,9 @@ export function hasKey(key: string): boolean {
(node, part) => (node as Record<string, unknown> | undefined)?.[part],
en
);
// 构建时 i18n 插件把文案预编译成消息函数,测试里则是原始字符串
return typeof found === 'string' || typeof found === 'function';
// 构建时 i18n 插件把文案预编译成 AST(根节点 t 为 0,开发态键名不压缩为 type),
// 测试里则是原始字符串
if (typeof found === 'string') return true;
const node = found as { t?: unknown; type?: unknown } | undefined;
return node?.t === 0 || node?.type === 0;
}

View File

@@ -3,29 +3,24 @@
"target": "ESNext",
"useDefineForClassFields": true,
"module": "ESNext",
"moduleResolution": "Node",
"moduleResolution": "Bundler",
"strict": true,
"jsx": "preserve",
"jsxImportSource": "vue",
"sourceMap": true,
"resolveJsonModule": true,
"isolatedModules": true,
"esModuleInterop": true,
"lib": ["ESNext", "DOM"],
"skipLibCheck": true,
"noImplicitAny": true,
"baseUrl": "./",
"types": ["vite-plugin-pwa/client"],
"types": ["vitest/globals"],
"paths": {
"~/*": ["./*"],
"@/*": ["src/*"],
"#/*": ["types/*"]
"@/*": ["./src/*"],
"#/*": ["./types/*"]
}
},
"include": [
"src/**/*.ts",
"src/**/*.d.ts",
"src/**/*.tsx",
"src/**/*.vue",
"types/*.ts",
"types/dts/*.d.ts"

View File

@@ -2,8 +2,8 @@
"compilerOptions": {
"composite": true,
"module": "ESNext",
"moduleResolution": "Node",
"allowSyntheticDefaultImports": true
"moduleResolution": "Bundler",
"types": ["node"]
},
"include": ["vite.config.ts"]
}

View File

@@ -1,168 +1,224 @@
// Generated by 'unplugin-auto-import'
/* eslint-disable */
/* prettier-ignore */
/* oxlint-disable */
/* oxfmt-ignore */
// @ts-nocheck
// noinspection JSUnusedGlobalSymbols
// Generated by unplugin-auto-import
// biome-ignore lint: disable
export {}
declare global {
const EffectScope: typeof import('vue')['EffectScope']
const MARKET_CATEGORIES: typeof import('../../src/utils/plugin-market')['MARKET_CATEGORIES']
const MARKET_STATES: typeof import('../../src/utils/plugin-market')['MARKET_STATES']
const acceptHMRUpdate: typeof import('pinia')['acceptHMRUpdate']
const apiAuth: typeof import('../../src/api/auth')['apiAuth']
const apiBangumi: typeof import('../../src/api/bangumi')['apiBangumi']
const apiCheck: typeof import('../../src/api/check')['apiCheck']
const apiConfig: typeof import('../../src/api/config')['apiConfig']
const apiDownload: typeof import('../../src/api/download')['apiDownload']
const apiDownloader: typeof import('../../src/api/downloader')['apiDownloader']
const apiLLM: typeof import('../../src/api/llm')['apiLLM']
const apiLog: typeof import('../../src/api/log')['apiLog']
const apiNotification: typeof import('../../src/api/notification')['apiNotification']
const apiPasskey: typeof import('../../src/api/passkey')['apiPasskey']
const apiPlugins: typeof import('../../src/api/plugins')['apiPlugins']
const apiProgram: typeof import('../../src/api/program')['apiProgram']
const apiRSS: typeof import('../../src/api/rss')['apiRSS']
const apiSearch: typeof import('../../src/api/search')['apiSearch']
const apiSetup: typeof import('../../src/api/setup')['apiSetup']
const apiTokens: typeof import('../../src/api/access')['apiTokens']
const apiUpdate: typeof import('../../src/api/update')['apiUpdate']
const apiUsers: typeof import('../../src/api/access')['apiUsers']
const axios: typeof import('../../src/utils/axios')['axios']
const buildNexusPhpSearchUrl: typeof import('../../src/utils/nexusphp')['buildNexusPhpSearchUrl']
const categoriesOf: typeof import('../../src/utils/plugin-market')['categoriesOf']
const categoryCounts: typeof import('../../src/utils/plugin-market')['categoryCounts']
const computed: typeof import('vue')['computed']
const configSectionMatches: typeof import('../../src/utils/config-search')['configSectionMatches']
const countLogLevels: typeof import('../../src/utils/log-parse')['countLogLevels']
const createApp: typeof import('vue')['createApp']
const createPinia: typeof import('pinia')['createPinia']
const createSharedComposable: typeof import('@vueuse/core')['createSharedComposable']
const customRef: typeof import('vue')['customRef']
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 dirtyConfigGroups: typeof import('../../src/utils/config-diff')['dirtyConfigGroups']
const effectScope: typeof import('vue')['effectScope']
const entryState: typeof import('../../src/utils/plugin-market')['entryState']
const fillSchemaDefaults: typeof import('../../src/utils/plugin-schema')['fillSchemaDefaults']
const filterEntries: typeof import('../../src/utils/plugin-market')['filterEntries']
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 hasConfiguredDownloader: typeof import('../../src/store/downloader')['hasConfiguredDownloader']
const hasKey: typeof import('../../src/utils/id-label')['hasKey']
const i18n: typeof import('../../src/hooks/useMyI18n')['i18n']
const idLabel: typeof import('../../src/utils/id-label')['idLabel']
const inject: typeof import('vue')['inject']
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 isValidNexusPhpCategoryIds: typeof import('../../src/utils/nexusphp')['isValidNexusPhpCategoryIds']
const localizeFields: typeof import('../../src/utils/plugin-text')['localizeFields']
const mapActions: typeof import('pinia')['mapActions']
const mapGetters: typeof import('pinia')['mapGetters']
const mapState: typeof import('pinia')['mapState']
const mapStores: typeof import('pinia')['mapStores']
const mapWritableState: typeof import('pinia')['mapWritableState']
const markRaw: typeof import('vue')['markRaw']
const nextTick: typeof import('vue')['nextTick']
const onActivated: typeof import('vue')['onActivated']
const onBeforeMount: typeof import('vue')['onBeforeMount']
const onBeforeRouteLeave: typeof import('vue-router/auto')['onBeforeRouteLeave']
const onBeforeRouteUpdate: typeof import('vue-router/auto')['onBeforeRouteUpdate']
const onBeforeUnmount: typeof import('vue')['onBeforeUnmount']
const onBeforeUpdate: typeof import('vue')['onBeforeUpdate']
const onDeactivated: typeof import('vue')['onDeactivated']
const onErrorCaptured: typeof import('vue')['onErrorCaptured']
const onMounted: typeof import('vue')['onMounted']
const onRenderTracked: typeof import('vue')['onRenderTracked']
const onRenderTriggered: typeof import('vue')['onRenderTriggered']
const onResponseError: typeof import('../../src/utils/axios')['onResponseError']
const onScopeDispose: typeof import('vue')['onScopeDispose']
const onServerPrefetch: typeof import('vue')['onServerPrefetch']
const onUnmounted: typeof import('vue')['onUnmounted']
const onUpdated: typeof import('vue')['onUpdated']
const parseLogLines: typeof import('../../src/utils/log-parse')['parseLogLines']
const permissionDiff: typeof import('../../src/utils/plugin-market')['permissionDiff']
const pluginDescription: typeof import('../../src/utils/plugin-text')['pluginDescription']
const pluginName: typeof import('../../src/utils/plugin-text')['pluginName']
const pluginReason: typeof import('../../src/utils/plugin-text')['pluginReason']
const pluginSetVersion: typeof import('../../src/hooks/usePluginRefresh')['pluginSetVersion']
const provide: typeof import('vue')['provide']
const reactive: typeof import('vue')['reactive']
const readCachedCatalog: typeof import('../../src/utils/plugin-market')['readCachedCatalog']
const readonly: typeof import('vue')['readonly']
const ref: typeof import('vue')['ref']
const refreshPluginProviders: typeof import('../../src/hooks/usePluginProviders')['refreshPluginProviders']
const refreshPluginState: typeof import('../../src/hooks/usePluginRefresh')['refreshPluginState']
const refreshPluginUi: typeof import('../../src/hooks/usePluginUi')['refreshPluginUi']
const relativeTime: typeof import('../../src/utils/relative-time')['relativeTime']
const renderMarkdown: typeof import('../../src/utils/markdown')['renderMarkdown']
const resolveComponent: typeof import('vue')['resolveComponent']
const resolvePosterUrl: typeof import('../../src/utils/poster')['resolvePosterUrl']
const schemaFields: typeof import('../../src/utils/plugin-schema')['schemaFields']
const setActivePinia: typeof import('pinia')['setActivePinia']
const setMapStoreSuffix: typeof import('pinia')['setMapStoreSuffix']
const shallowReactive: typeof import('vue')['shallowReactive']
const shallowReadonly: typeof import('vue')['shallowReadonly']
const shallowRef: typeof import('vue')['shallowRef']
const slotTitle: typeof import('../../src/hooks/usePluginUi')['slotTitle']
const sourceUrl: typeof import('../../src/utils/plugin-market')['sourceUrl']
const storeToRefs: typeof import('pinia')['storeToRefs']
const toRaw: typeof import('vue')['toRaw']
const toRef: typeof import('vue')['toRef']
const toRefs: typeof import('vue')['toRefs']
const tokensCss: typeof import('../../src/hooks/usePluginHostDeps')['tokensCss']
const torrentKey: typeof import('../../src/store/downloader')['torrentKey']
const triggerRef: typeof import('vue')['triggerRef']
const unref: typeof import('vue')['unref']
const useAccessStore: typeof import('../../src/store/access')['useAccessStore']
const useAddRss: typeof import('../../src/hooks/useAddRss')['useAddRss']
const useApi: typeof import('../../src/hooks/useApi')['useApi']
const useAppInfo: typeof import('../../src/hooks/useAppInfo')['useAppInfo']
const useAttrs: typeof import('vue')['useAttrs']
const useAuth: typeof import('../../src/hooks/useAuth')['useAuth']
const useBangumiRuleForm: typeof import('../../src/hooks/useBangumiRuleForm')['useBangumiRuleForm']
const useBangumiStore: typeof import('../../src/store/bangumi')['useBangumiStore']
const useBreakpointQuery: typeof import('../../src/hooks/useBreakpointQuery')['useBreakpointQuery']
const useBreakpoints: typeof import('@vueuse/core')['useBreakpoints']
const useClipboard: typeof import('@vueuse/core')['useClipboard']
const useConfigStore: typeof import('../../src/store/config')['useConfigStore']
const useConfirm: typeof import('../../src/hooks/useConfirm')['useConfirm']
const useConfirmState: typeof import('../../src/hooks/useConfirm')['useConfirmState']
const useCssModule: typeof import('vue')['useCssModule']
const useCssVars: typeof import('vue')['useCssVars']
const useDarkMode: typeof import('../../src/hooks/useDarkMode')['useDarkMode']
const useDownloaderInstances: typeof import('../../src/hooks/useDownloaderInstances')['useDownloaderInstances']
const useDownloaderStore: typeof import('../../src/store/downloader')['useDownloaderStore']
const useEventStream: typeof import('../../src/hooks/useEventStream')['useEventStream']
const useI18n: typeof import('vue-i18n')['useI18n']
const useIntervalFn: typeof import('@vueuse/core')['useIntervalFn']
const useLocalStorage: typeof import('@vueuse/core')['useLocalStorage']
const useLogStore: typeof import('../../src/store/log')['useLogStore']
const useMessage: typeof import('../../src/hooks/useMessage')['useMessage']
const useMyI18n: typeof import('../../src/hooks/useMyI18n')['useMyI18n']
const useNotificationStore: typeof import('../../src/store/notification')['useNotificationStore']
const usePasskey: typeof import('../../src/hooks/usePasskey')['usePasskey']
const usePlayerStore: typeof import('../../src/store/player')['usePlayerStore']
const usePluginHostDeps: typeof import('../../src/hooks/usePluginHostDeps')['usePluginHostDeps']
const usePluginPages: typeof import('../../src/hooks/usePluginUi')['usePluginPages']
const usePluginProviders: typeof import('../../src/hooks/usePluginProviders')['usePluginProviders']
const usePluginUi: typeof import('../../src/hooks/usePluginUi')['usePluginUi']
const usePreferredDark: typeof import('@vueuse/core')['usePreferredDark']
const useProgramStore: typeof import('../../src/store/program')['useProgramStore']
const useRSSStore: typeof import('../../src/store/rss')['useRSSStore']
const useRoute: typeof import('vue-router/auto')['useRoute']
const useRouter: typeof import('vue-router/auto')['useRouter']
const useSafeArea: typeof import('../../src/hooks/useSafeArea')['useSafeArea']
const useSearchStore: typeof import('../../src/store/search')['useSearchStore']
const useSetupStore: typeof import('../../src/store/setup')['useSetupStore']
const useSlots: typeof import('vue')['useSlots']
const useTorrentList: typeof import('../../src/hooks/useTorrentList')['useTorrentList']
const useUiSlots: typeof import('../../src/hooks/usePluginUi')['useUiSlots']
const watch: typeof import('vue')['watch']
const watchEffect: typeof import('vue')['watchEffect']
const watchPostEffect: typeof import('vue')['watchPostEffect']
const watchSyncEffect: typeof import('vue')['watchSyncEffect']
const writeCachedCatalog: typeof import('../../src/utils/plugin-market')['writeCachedCatalog']
const EffectScope: typeof import('vue').EffectScope
const MARKET_CATEGORIES: typeof import('../../src/utils/plugin-market').MARKET_CATEGORIES
const MARKET_STATES: typeof import('../../src/utils/plugin-market').MARKET_STATES
const acceptHMRUpdate: typeof import('pinia').acceptHMRUpdate
const apiAuth: typeof import('../../src/api/auth').apiAuth
const apiBangumi: typeof import('../../src/api/bangumi').apiBangumi
const apiCheck: typeof import('../../src/api/check').apiCheck
const apiConfig: typeof import('../../src/api/config').apiConfig
const apiDownload: typeof import('../../src/api/download').apiDownload
const apiDownloader: typeof import('../../src/api/downloader').apiDownloader
const apiLLM: typeof import('../../src/api/llm').apiLLM
const apiLog: typeof import('../../src/api/log').apiLog
const apiNotification: typeof import('../../src/api/notification').apiNotification
const apiPasskey: typeof import('../../src/api/passkey').apiPasskey
const apiPlugins: typeof import('../../src/api/plugins').apiPlugins
const apiProgram: typeof import('../../src/api/program').apiProgram
const apiRSS: typeof import('../../src/api/rss').apiRSS
const apiSearch: typeof import('../../src/api/search').apiSearch
const apiSetup: typeof import('../../src/api/setup').apiSetup
const apiTokens: typeof import('../../src/api/access').apiTokens
const apiUpdate: typeof import('../../src/api/update').apiUpdate
const apiUsers: typeof import('../../src/api/access').apiUsers
const axios: typeof import('../../src/utils/axios').axios
const buildNexusPhpSearchUrl: typeof import('../../src/utils/nexusphp').buildNexusPhpSearchUrl
const categoriesOf: typeof import('../../src/utils/plugin-market').categoriesOf
const categoryCounts: typeof import('../../src/utils/plugin-market').categoryCounts
const computed: typeof import('vue').computed
const configSectionMatches: typeof import('../../src/utils/config-search').configSectionMatches
const countLogLevels: typeof import('../../src/utils/log-parse').countLogLevels
const createApp: typeof import('vue').createApp
const createPinia: typeof import('pinia').createPinia
const createSharedComposable: typeof import('@vueuse/core').createSharedComposable
const customRef: typeof import('vue').customRef
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 dirtyConfigGroups: typeof import('../../src/utils/config-diff').dirtyConfigGroups
const effectScope: typeof import('vue').effectScope
const entryState: typeof import('../../src/utils/plugin-market').entryState
const fillSchemaDefaults: typeof import('../../src/utils/plugin-schema').fillSchemaDefaults
const filterEntries: typeof import('../../src/utils/plugin-market').filterEntries
const getActivePinia: typeof import('pinia').getActivePinia
const getCurrentInstance: typeof import('vue').getCurrentInstance
const getCurrentScope: typeof import('vue').getCurrentScope
const getCurrentWatcher: typeof import('vue').getCurrentWatcher
const h: typeof import('vue').h
const hasConfiguredDownloader: typeof import('../../src/store/downloader').hasConfiguredDownloader
const hasKey: typeof import('../../src/utils/id-label').hasKey
const i18n: typeof import('../../src/hooks/useMyI18n').i18n
const idLabel: typeof import('../../src/utils/id-label').idLabel
const inject: typeof import('vue').inject
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 isShallow: typeof import('vue').isShallow
const isValidNexusPhpCategoryIds: typeof import('../../src/utils/nexusphp').isValidNexusPhpCategoryIds
const localizeFields: typeof import('../../src/utils/plugin-text').localizeFields
const mapActions: typeof import('pinia').mapActions
const mapGetters: typeof import('pinia').mapGetters
const mapState: typeof import('pinia').mapState
const mapStores: typeof import('pinia').mapStores
const mapWritableState: typeof import('pinia').mapWritableState
const markRaw: typeof import('vue').markRaw
const nextTick: typeof import('vue').nextTick
const onActivated: typeof import('vue').onActivated
const onBeforeMount: typeof import('vue').onBeforeMount
const onBeforeRouteLeave: typeof import('vue-router/auto').onBeforeRouteLeave
const onBeforeRouteUpdate: typeof import('vue-router/auto').onBeforeRouteUpdate
const onBeforeUnmount: typeof import('vue').onBeforeUnmount
const onBeforeUpdate: typeof import('vue').onBeforeUpdate
const onDeactivated: typeof import('vue').onDeactivated
const onErrorCaptured: typeof import('vue').onErrorCaptured
const onMounted: typeof import('vue').onMounted
const onRenderTracked: typeof import('vue').onRenderTracked
const onRenderTriggered: typeof import('vue').onRenderTriggered
const onResponseError: typeof import('../../src/utils/axios').onResponseError
const onScopeDispose: typeof import('vue').onScopeDispose
const onServerPrefetch: typeof import('vue').onServerPrefetch
const onUnmounted: typeof import('vue').onUnmounted
const onUpdated: typeof import('vue').onUpdated
const onWatcherCleanup: typeof import('vue').onWatcherCleanup
const parseLogLines: typeof import('../../src/utils/log-parse').parseLogLines
const permissionDiff: typeof import('../../src/utils/plugin-market').permissionDiff
const pluginDescription: typeof import('../../src/utils/plugin-text').pluginDescription
const pluginName: typeof import('../../src/utils/plugin-text').pluginName
const pluginReason: typeof import('../../src/utils/plugin-text').pluginReason
const pluginSetVersion: typeof import('../../src/hooks/usePluginRefresh').pluginSetVersion
const provide: typeof import('vue').provide
const reactive: typeof import('vue').reactive
const readCachedCatalog: typeof import('../../src/utils/plugin-market').readCachedCatalog
const readonly: typeof import('vue').readonly
const ref: typeof import('vue').ref
const refreshPluginProviders: typeof import('../../src/hooks/usePluginProviders').refreshPluginProviders
const refreshPluginState: typeof import('../../src/hooks/usePluginRefresh').refreshPluginState
const refreshPluginUi: typeof import('../../src/hooks/usePluginUi').refreshPluginUi
const relativeTime: typeof import('../../src/utils/relative-time').relativeTime
const renderMarkdown: typeof import('../../src/utils/markdown').renderMarkdown
const resolveComponent: typeof import('vue').resolveComponent
const resolvePosterUrl: typeof import('../../src/utils/poster').resolvePosterUrl
const schemaFields: typeof import('../../src/utils/plugin-schema').schemaFields
const setActivePinia: typeof import('pinia').setActivePinia
const setMapStoreSuffix: typeof import('pinia').setMapStoreSuffix
const shallowReactive: typeof import('vue').shallowReactive
const shallowReadonly: typeof import('vue').shallowReadonly
const shallowRef: typeof import('vue').shallowRef
const slotTitle: typeof import('../../src/hooks/usePluginUi').slotTitle
const sourceUrl: typeof import('../../src/utils/plugin-market').sourceUrl
const storeToRefs: typeof import('pinia').storeToRefs
const toRaw: typeof import('vue').toRaw
const toRef: typeof import('vue').toRef
const toRefs: typeof import('vue').toRefs
const toValue: typeof import('vue').toValue
const tokensCss: typeof import('../../src/hooks/usePluginHostDeps').tokensCss
const torrentKey: typeof import('../../src/store/downloader').torrentKey
const triggerRef: typeof import('vue').triggerRef
const unref: typeof import('vue').unref
const useAccessStore: typeof import('../../src/store/access').useAccessStore
const useAddRss: typeof import('../../src/hooks/useAddRss').useAddRss
const useApi: typeof import('../../src/hooks/useApi').useApi
const useAppInfo: typeof import('../../src/hooks/useAppInfo').useAppInfo
const useAttrs: typeof import('vue').useAttrs
const useAuth: typeof import('../../src/hooks/useAuth').useAuth
const useBangumiRuleForm: typeof import('../../src/hooks/useBangumiRuleForm').useBangumiRuleForm
const useBangumiStore: typeof import('../../src/store/bangumi').useBangumiStore
const useBreakpointQuery: typeof import('../../src/hooks/useBreakpointQuery').useBreakpointQuery
const useBreakpoints: typeof import('@vueuse/core').useBreakpoints
const useClipboard: typeof import('@vueuse/core').useClipboard
const useConfigStore: typeof import('../../src/store/config').useConfigStore
const useConfirm: typeof import('../../src/hooks/useConfirm').useConfirm
const useConfirmState: typeof import('../../src/hooks/useConfirm').useConfirmState
const useCssModule: typeof import('vue').useCssModule
const useCssVars: typeof import('vue').useCssVars
const useDarkMode: typeof import('../../src/hooks/useDarkMode').useDarkMode
const useDownloaderInstances: typeof import('../../src/hooks/useDownloaderInstances').useDownloaderInstances
const useDownloaderStore: typeof import('../../src/store/downloader').useDownloaderStore
const useEventStream: typeof import('../../src/hooks/useEventStream').useEventStream
const useI18n: typeof import('vue-i18n').useI18n
const useId: typeof import('vue').useId
const useIntervalFn: typeof import('@vueuse/core').useIntervalFn
const useLocalStorage: typeof import('@vueuse/core').useLocalStorage
const useLogStore: typeof import('../../src/store/log').useLogStore
const useMessage: typeof import('../../src/hooks/useMessage').useMessage
const useModel: typeof import('vue').useModel
const useMyI18n: typeof import('../../src/hooks/useMyI18n').useMyI18n
const useNotificationStore: typeof import('../../src/store/notification').useNotificationStore
const usePasskey: typeof import('../../src/hooks/usePasskey').usePasskey
const usePlayerStore: typeof import('../../src/store/player').usePlayerStore
const usePluginHostDeps: typeof import('../../src/hooks/usePluginHostDeps').usePluginHostDeps
const usePluginPages: typeof import('../../src/hooks/usePluginUi').usePluginPages
const usePluginProviders: typeof import('../../src/hooks/usePluginProviders').usePluginProviders
const usePluginUi: typeof import('../../src/hooks/usePluginUi').usePluginUi
const usePreferredDark: typeof import('@vueuse/core').usePreferredDark
const useProgramStore: typeof import('../../src/store/program').useProgramStore
const useRSSStore: typeof import('../../src/store/rss').useRSSStore
const useRoute: typeof import('vue-router/auto').useRoute
const useRouter: typeof import('vue-router/auto').useRouter
const useSafeArea: typeof import('../../src/hooks/useSafeArea').useSafeArea
const useSearchStore: typeof import('../../src/store/search').useSearchStore
const useSetupStore: typeof import('../../src/store/setup').useSetupStore
const useSlots: typeof import('vue').useSlots
const useTemplateRef: typeof import('vue').useTemplateRef
const useTorrentList: typeof import('../../src/hooks/useTorrentList').useTorrentList
const useUiSlots: typeof import('../../src/hooks/usePluginUi').useUiSlots
const watch: typeof import('vue').watch
const watchEffect: typeof import('vue').watchEffect
const watchPostEffect: typeof import('vue').watchPostEffect
const watchSyncEffect: typeof import('vue').watchSyncEffect
const writeCachedCatalog: typeof import('../../src/utils/plugin-market').writeCachedCatalog
}
// for type re-export
declare global {
// @ts-ignore
export type { Component, Slot, Slots, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, ShallowRef, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue'
import('vue')
// @ts-ignore
export type { LLMAuthKind, LLMProviderView, LLMAuthChallenge, LLMAuthStatus } from '../../src/api/llm'
import('../../src/api/llm')
// @ts-ignore
export type { TestProviderRequest, TestProviderConfigRequest, TestResponse, InboxSeverity, InboxMessage, InboxListResponse, InboxListParams } from '../../src/api/notification'
import('../../src/api/notification')
// @ts-ignore
export type { SearchFilters, FilterOptions, GroupedBangumi } from '../../src/store/search'
import('../../src/store/search')
// @ts-ignore
export type { ApiExecuteResult } from '../../src/hooks/useApi'
import('../../src/hooks/useApi')
// @ts-ignore
export type { InfoTag } from '../../src/hooks/useBangumiRuleForm'
import('../../src/hooks/useBangumiRuleForm')
// @ts-ignore
export type { ConfirmOptions } from '../../src/hooks/useConfirm'
import('../../src/hooks/useConfirm')
// @ts-ignore
export type { StatusPayload, NotificationStreamPayload, BusEvent } from '../../src/hooks/useEventStream'
import('../../src/hooks/useEventStream')
// @ts-ignore
export type { AxiosRequestConfig } from '../../src/utils/axios'
import('../../src/utils/axios')
// @ts-ignore
export type { LogLevel, LogEntry, LogLevelCounts } from '../../src/utils/log-parse'
import('../../src/utils/log-parse')
// @ts-ignore
export type { NexusPhpProviderOptions } from '../../src/utils/nexusphp'
import('../../src/utils/nexusphp')
// @ts-ignore
export type { MarketCategory, MarketState, MarketFilter, CachedCatalog } from '../../src/utils/plugin-market'
import('../../src/utils/plugin-market')
// @ts-ignore
export type { SchemaFieldKind, SchemaField } from '../../src/utils/plugin-schema'
import('../../src/utils/plugin-schema')
}

View File

@@ -1,13 +1,15 @@
/* eslint-disable */
/* prettier-ignore */
// @ts-nocheck
// biome-ignore lint: disable
// oxlint-disable
// ------
// Generated by unplugin-vue-components
// Read more: https://github.com/vuejs/core/pull/3399
import '@vue/runtime-core'
export {}
declare module '@vue/runtime-core' {
/* prettier-ignore */
declare module 'vue' {
export interface GlobalComponents {
AbAddRss: typeof import('./../../src/components/ab-add-rss.vue')['default']
AbAlert: typeof import('./../../src/components/basic/ab-alert.vue')['default']

View File

@@ -1,9 +0,0 @@
/**
* https://unocss.dev/presets/attributify#vue-3
*/
import type { AttributifyAttributes } from '@unocss/preset-attributify';
declare module '@vue/runtime-dom' {
interface HTMLAttributes extends AttributifyAttributes {}
}

View File

@@ -1,20 +1,12 @@
import {
defineConfig,
presetAttributify,
presetIcons,
presetUno,
} from 'unocss';
import { defineConfig, presetIcons, presetWind3 } from 'unocss';
import presetRemToPx from '@unocss/preset-rem-to-px';
export default defineConfig({
presets: [
presetUno({
dark: 'class',
}),
presetWind3(),
presetRemToPx({
baseFontSize: 4,
}),
presetAttributify(),
// Icon collections are installed locally so production builds remain
// reproducible when the E2E runtime has no public network access.
presetIcons(),

View File

@@ -7,26 +7,19 @@ import Components from 'unplugin-vue-components/vite';
import VueRouter from 'unplugin-vue-router/vite';
import { VueRouterAutoImports } from 'unplugin-vue-router';
import VueI18nPlugin from '@intlify/unplugin-vue-i18n/vite';
import { VitePWA } from 'vite-plugin-pwa';
import VueJsx from '@vitejs/plugin-vue-jsx';
// 与后端给 SPA 文档加的响应头一致(backend/src/main.py)。只加在 preview:
// dev 服务器与 vite-plugin-pwa 的开发态会注入内联脚本,会被这个策略拦下
// dev 服务器会注入内联脚本,会被这个策略拦下
const headers = { 'Content-Security-Policy': "script-src 'self'" };
// https://vitejs.dev/config/
export default defineConfig(({ mode }) => ({
export default defineConfig({
base: './',
plugins: [
VueJsx(),
VueRouter({
dts: 'types/dts/router-type.d.ts',
}),
vue({
script: {
defineModel: true,
},
}),
vue(),
UnoCSS(),
AutoImport({
imports: [
@@ -52,65 +45,26 @@ export default defineConfig(({ mode }) => ({
dts: 'types/dts/components.d.ts',
}),
VueI18nPlugin({
include: resolve(__dirname, './src/i18n/**'),
}),
VitePWA({
injectRegister: false,
registerType: 'autoUpdate',
devOptions: {
enabled: true,
},
workbox: {
globPatterns: ['**/*.{js,css,html,ico,png,svg}'],
},
manifest: {
name: 'AutoBangumi',
display: 'standalone',
short_name: 'AutoBangumi',
description: 'Automated Bangumi Download Tool',
theme_color: '#ffffff',
icons: [
{
src: '/images/logo.svg',
sizes: 'any',
type: 'image/svg+xml',
purpose: 'any',
},
{
src: '/images/pwa-192.png',
sizes: '192x192',
type: 'image/png',
},
{
src: '/images/pwa-512.png',
sizes: '512x512',
type: 'image/png',
purpose: 'any',
},
],
},
include: resolve(import.meta.dirname, './src/i18n/**'),
}),
],
// unplugin-vue-i18n 11 只产出 AST 形式的文案,vue-i18n 9 要打开 JIT 才认 AST;
// vue-i18n 升到 10+ 后默认开启,届时删掉这一项
define: { __INTLIFY_JIT_COMPILATION__: true },
css: {
preprocessorOptions: {
scss: {
additionalData: '@import "./src/style/mixin.scss";',
additionalData: '@use "@/style/mixin.scss" as *;',
},
},
},
esbuild: {
drop: mode === 'production' ? ['console', 'debugger'] : [],
},
build: {
cssCodeSplit: false,
},
resolve: {
alias: {
'~': resolve(__dirname, './'),
'@': resolve(__dirname, 'src'),
'#': resolve(__dirname, 'types'),
rolldownOptions: {
output: { minify: { compress: { dropConsole: true } } },
},
},
resolve: { tsconfigPaths: true },
preview: { headers },
server: {
proxy: {
@@ -121,4 +75,4 @@ export default defineConfig(({ mode }) => ({
'^/posters/.*': 'http://localhost:7892',
},
},
}));
});

View File

@@ -1,4 +1,3 @@
import { resolve } from 'node:path';
import { defineConfig } from 'vitest/config';
import vue from '@vitejs/plugin-vue';
import AutoImport from 'unplugin-auto-import/vite';
@@ -18,11 +17,5 @@ export default defineConfig({
setupFiles: ['./src/test/setup.ts'],
include: ['src/**/*.{test,spec}.{js,ts}'],
},
resolve: {
alias: {
'~': resolve(__dirname, './'),
'@': resolve(__dirname, 'src'),
'#': resolve(__dirname, 'types'),
},
},
resolve: { tsconfigPaths: true },
});