refactor(developer-config): 移除开发者配置页面相关代码和文档

- 删除应用配置页面及相关组件,重构路由为 /developer/config/[id].vue
- 移除开发者文档页面及其导航与样式实现
- 清理开发者侧功能完善工作日志文件
- 删除全局.gitignore配置文件,清理无用忽略规则
- 优化应用配置页面的参数读取和路由结构,解决刷新404问题
- 解决数据库配置唯一键冲突,调整保存逻辑避免重复插入
- 移除对后端配置加密字段的 secret 标记,修正加密异常问题
This commit is contained in:
2026-04-09 07:35:34 +08:00
parent 3209d92cc5
commit f9e1286ab1
130 changed files with 18656 additions and 22143 deletions

Binary file not shown.

26
.gitignore vendored
View File

@@ -1,26 +0,0 @@
# Nuxt dev/build outputs
.output
.data
.nuxt
.nitro
.cache
dist
# Node dependencies
node_modules
# Logs
logs
*.log
.pm2
# Misc
.DS_Store
.fleet
.idea
# Local env files
.env
.env.*
!.env.example
/.npm-cache/

21
.nuxt/app.config.mjs Normal file
View File

@@ -0,0 +1,21 @@
import { defuFn } from 'defu'
const inlineConfig = {
"nuxt": {}
}
/** client **/
import { _replaceAppConfig } from '#app/config'
// Vite - webpack is handled directly in #app/config
if (import.meta.dev && !import.meta.nitro && import.meta.hot) {
import.meta.hot.accept((newModule) => {
_replaceAppConfig(newModule.default)
})
}
/** client-end **/
export default /*@__PURE__*/ defuFn(inlineConfig)

162
.nuxt/components.d.ts vendored Normal file
View File

@@ -0,0 +1,162 @@
import type { DefineComponent, SlotsType } from 'vue'
type IslandComponent<T> = DefineComponent<{}, {refresh: () => Promise<void>}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, SlotsType<{ fallback: { error: unknown } }>> & T
type HydrationStrategies = {
hydrateOnVisible?: IntersectionObserverInit | true
hydrateOnIdle?: number | true
hydrateOnInteraction?: keyof HTMLElementEventMap | Array<keyof HTMLElementEventMap> | true
hydrateOnMediaQuery?: string
hydrateAfter?: number
hydrateWhen?: boolean
hydrateNever?: true
}
type LazyComponent<T> = DefineComponent<HydrationStrategies, {}, {}, {}, {}, {}, {}, { hydrated: () => void }> & T
export const LangSwitch: typeof import("../app/components/LangSwitch.vue").default
export const NotificationBell: typeof import("../app/components/NotificationBell.vue").default
export const QrCodeModal: typeof import("../app/components/QrCodeModal.vue").default
export const QrLogin: typeof import("../app/components/QrLogin.vue").default
export const SiteFooter: typeof import("../app/components/SiteFooter.vue").default
export const SiteHeader: typeof import("../app/components/SiteHeader.vue").default
export const AdminMarkdownEditor: typeof import("../app/components/admin/MarkdownEditor.vue").default
export const AdminMarkdownRenderer: typeof import("../app/components/admin/MarkdownRenderer.vue").default
export const ConsoleAppsCenter: typeof import("../app/components/console/AppsCenter.vue").default
export const ConsoleHeader: typeof import("../app/components/console/ConsoleHeader.vue").default
export const DeveloperAppDetail: typeof import("../app/components/developer/AppDetail.vue").default
export const DeveloperAppsCenter: typeof import("../app/components/developer/AppsCenter.vue").default
export const DeveloperPermissionGuard: typeof import("../app/components/developer/PermissionGuard.vue").default
export const DeveloperRoleTag: typeof import("../app/components/developer/RoleTag.vue").default
export const InviteBell: typeof import("../app/components/invite/InviteBell.vue").default
export const InviteNotification: typeof import("../app/components/invite/InviteNotification.vue").default
export const PaymentModal: typeof import("../app/components/payment/PaymentModal.vue").default
export const ProseA: typeof import("../node_modules/.pnpm/@nuxtjs+mdc@0.20.2_magicast@0.5.1/node_modules/@nuxtjs/mdc/dist/runtime/components/prose/ProseA.vue").default
export const ProseBlockquote: typeof import("../node_modules/.pnpm/@nuxtjs+mdc@0.20.2_magicast@0.5.1/node_modules/@nuxtjs/mdc/dist/runtime/components/prose/ProseBlockquote.vue").default
export const ProseCode: typeof import("../node_modules/.pnpm/@nuxtjs+mdc@0.20.2_magicast@0.5.1/node_modules/@nuxtjs/mdc/dist/runtime/components/prose/ProseCode.vue").default
export const ProseEm: typeof import("../node_modules/.pnpm/@nuxtjs+mdc@0.20.2_magicast@0.5.1/node_modules/@nuxtjs/mdc/dist/runtime/components/prose/ProseEm.vue").default
export const ProseH1: typeof import("../node_modules/.pnpm/@nuxtjs+mdc@0.20.2_magicast@0.5.1/node_modules/@nuxtjs/mdc/dist/runtime/components/prose/ProseH1.vue").default
export const ProseH2: typeof import("../node_modules/.pnpm/@nuxtjs+mdc@0.20.2_magicast@0.5.1/node_modules/@nuxtjs/mdc/dist/runtime/components/prose/ProseH2.vue").default
export const ProseH3: typeof import("../node_modules/.pnpm/@nuxtjs+mdc@0.20.2_magicast@0.5.1/node_modules/@nuxtjs/mdc/dist/runtime/components/prose/ProseH3.vue").default
export const ProseH4: typeof import("../node_modules/.pnpm/@nuxtjs+mdc@0.20.2_magicast@0.5.1/node_modules/@nuxtjs/mdc/dist/runtime/components/prose/ProseH4.vue").default
export const ProseH5: typeof import("../node_modules/.pnpm/@nuxtjs+mdc@0.20.2_magicast@0.5.1/node_modules/@nuxtjs/mdc/dist/runtime/components/prose/ProseH5.vue").default
export const ProseH6: typeof import("../node_modules/.pnpm/@nuxtjs+mdc@0.20.2_magicast@0.5.1/node_modules/@nuxtjs/mdc/dist/runtime/components/prose/ProseH6.vue").default
export const ProseHr: typeof import("../node_modules/.pnpm/@nuxtjs+mdc@0.20.2_magicast@0.5.1/node_modules/@nuxtjs/mdc/dist/runtime/components/prose/ProseHr.vue").default
export const ProseImg: typeof import("../node_modules/.pnpm/@nuxtjs+mdc@0.20.2_magicast@0.5.1/node_modules/@nuxtjs/mdc/dist/runtime/components/prose/ProseImg.vue").default
export const ProseLi: typeof import("../node_modules/.pnpm/@nuxtjs+mdc@0.20.2_magicast@0.5.1/node_modules/@nuxtjs/mdc/dist/runtime/components/prose/ProseLi.vue").default
export const ProseOl: typeof import("../node_modules/.pnpm/@nuxtjs+mdc@0.20.2_magicast@0.5.1/node_modules/@nuxtjs/mdc/dist/runtime/components/prose/ProseOl.vue").default
export const ProseP: typeof import("../node_modules/.pnpm/@nuxtjs+mdc@0.20.2_magicast@0.5.1/node_modules/@nuxtjs/mdc/dist/runtime/components/prose/ProseP.vue").default
export const ProsePre: typeof import("../node_modules/.pnpm/@nuxtjs+mdc@0.20.2_magicast@0.5.1/node_modules/@nuxtjs/mdc/dist/runtime/components/prose/ProsePre.vue").default
export const ProseScript: typeof import("../node_modules/.pnpm/@nuxtjs+mdc@0.20.2_magicast@0.5.1/node_modules/@nuxtjs/mdc/dist/runtime/components/prose/ProseScript.vue").default
export const ProseStrong: typeof import("../node_modules/.pnpm/@nuxtjs+mdc@0.20.2_magicast@0.5.1/node_modules/@nuxtjs/mdc/dist/runtime/components/prose/ProseStrong.vue").default
export const ProseTable: typeof import("../node_modules/.pnpm/@nuxtjs+mdc@0.20.2_magicast@0.5.1/node_modules/@nuxtjs/mdc/dist/runtime/components/prose/ProseTable.vue").default
export const ProseTbody: typeof import("../node_modules/.pnpm/@nuxtjs+mdc@0.20.2_magicast@0.5.1/node_modules/@nuxtjs/mdc/dist/runtime/components/prose/ProseTbody.vue").default
export const ProseTd: typeof import("../node_modules/.pnpm/@nuxtjs+mdc@0.20.2_magicast@0.5.1/node_modules/@nuxtjs/mdc/dist/runtime/components/prose/ProseTd.vue").default
export const ProseTh: typeof import("../node_modules/.pnpm/@nuxtjs+mdc@0.20.2_magicast@0.5.1/node_modules/@nuxtjs/mdc/dist/runtime/components/prose/ProseTh.vue").default
export const ProseThead: typeof import("../node_modules/.pnpm/@nuxtjs+mdc@0.20.2_magicast@0.5.1/node_modules/@nuxtjs/mdc/dist/runtime/components/prose/ProseThead.vue").default
export const ProseTr: typeof import("../node_modules/.pnpm/@nuxtjs+mdc@0.20.2_magicast@0.5.1/node_modules/@nuxtjs/mdc/dist/runtime/components/prose/ProseTr.vue").default
export const ProseUl: typeof import("../node_modules/.pnpm/@nuxtjs+mdc@0.20.2_magicast@0.5.1/node_modules/@nuxtjs/mdc/dist/runtime/components/prose/ProseUl.vue").default
export const NuxtWelcome: typeof import("../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/components/welcome.vue").default
export const NuxtLayout: typeof import("../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/components/nuxt-layout").default
export const NuxtErrorBoundary: typeof import("../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/components/nuxt-error-boundary.vue").default
export const ClientOnly: typeof import("../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/components/client-only").default
export const DevOnly: typeof import("../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/components/dev-only").default
export const ServerPlaceholder: typeof import("../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/components/server-placeholder").default
export const NuxtLink: typeof import("../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/components/nuxt-link").default
export const NuxtLoadingIndicator: typeof import("../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/components/nuxt-loading-indicator").default
export const NuxtTime: typeof import("../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/components/nuxt-time.vue").default
export const NuxtRouteAnnouncer: typeof import("../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/components/nuxt-route-announcer").default
export const NuxtImg: typeof import("../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/components/nuxt-stubs").NuxtImg
export const NuxtPicture: typeof import("../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/components/nuxt-stubs").NuxtPicture
export const ContentRenderer: typeof import("../node_modules/.pnpm/@nuxt+content@3.12.0_better-sqlite3@12.8.0_magicast@0.5.1/node_modules/@nuxt/content/dist/runtime/components/ContentRenderer.vue").default
export const NuxtLinkLocale: typeof import("../node_modules/.pnpm/@nuxtjs+i18n@10.2.4_@emnapi+core@1.7.1_@emnapi+runtime@1.7.1_@vue+compiler-dom@3.5.26_d_a5323b549840adcc2cb57cfe4f807173/node_modules/@nuxtjs/i18n/dist/runtime/components/NuxtLinkLocale").default
export const SwitchLocalePathLink: typeof import("../node_modules/.pnpm/@nuxtjs+i18n@10.2.4_@emnapi+core@1.7.1_@emnapi+runtime@1.7.1_@vue+compiler-dom@3.5.26_d_a5323b549840adcc2cb57cfe4f807173/node_modules/@nuxtjs/i18n/dist/runtime/components/SwitchLocalePathLink").default
export const NuxtPage: typeof import("../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/pages/runtime/page").default
export const NoScript: typeof import("../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/head/runtime/components").NoScript
export const Link: typeof import("../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/head/runtime/components").Link
export const Base: typeof import("../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/head/runtime/components").Base
export const Title: typeof import("../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/head/runtime/components").Title
export const Meta: typeof import("../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/head/runtime/components").Meta
export const Style: typeof import("../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/head/runtime/components").Style
export const Head: typeof import("../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/head/runtime/components").Head
export const Html: typeof import("../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/head/runtime/components").Html
export const Body: typeof import("../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/head/runtime/components").Body
export const MDC: typeof import("../node_modules/.pnpm/@nuxtjs+mdc@0.20.2_magicast@0.5.1/node_modules/@nuxtjs/mdc/dist/runtime/components/MDC.vue").default
export const MDCCached: typeof import("../node_modules/.pnpm/@nuxtjs+mdc@0.20.2_magicast@0.5.1/node_modules/@nuxtjs/mdc/dist/runtime/components/MDCCached.vue").default
export const MDCRenderer: typeof import("../node_modules/.pnpm/@nuxtjs+mdc@0.20.2_magicast@0.5.1/node_modules/@nuxtjs/mdc/dist/runtime/components/MDCRenderer.vue").default
export const MDCSlot: typeof import("../node_modules/.pnpm/@nuxtjs+mdc@0.20.2_magicast@0.5.1/node_modules/@nuxtjs/mdc/dist/runtime/components/MDCSlot.vue").default
export const NuxtIsland: typeof import("../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/components/nuxt-island").default
export const LazyLangSwitch: LazyComponent<typeof import("../app/components/LangSwitch.vue").default>
export const LazyNotificationBell: LazyComponent<typeof import("../app/components/NotificationBell.vue").default>
export const LazyQrCodeModal: LazyComponent<typeof import("../app/components/QrCodeModal.vue").default>
export const LazyQrLogin: LazyComponent<typeof import("../app/components/QrLogin.vue").default>
export const LazySiteFooter: LazyComponent<typeof import("../app/components/SiteFooter.vue").default>
export const LazySiteHeader: LazyComponent<typeof import("../app/components/SiteHeader.vue").default>
export const LazyAdminMarkdownEditor: LazyComponent<typeof import("../app/components/admin/MarkdownEditor.vue").default>
export const LazyAdminMarkdownRenderer: LazyComponent<typeof import("../app/components/admin/MarkdownRenderer.vue").default>
export const LazyConsoleAppsCenter: LazyComponent<typeof import("../app/components/console/AppsCenter.vue").default>
export const LazyConsoleHeader: LazyComponent<typeof import("../app/components/console/ConsoleHeader.vue").default>
export const LazyDeveloperAppDetail: LazyComponent<typeof import("../app/components/developer/AppDetail.vue").default>
export const LazyDeveloperAppsCenter: LazyComponent<typeof import("../app/components/developer/AppsCenter.vue").default>
export const LazyDeveloperPermissionGuard: LazyComponent<typeof import("../app/components/developer/PermissionGuard.vue").default>
export const LazyDeveloperRoleTag: LazyComponent<typeof import("../app/components/developer/RoleTag.vue").default>
export const LazyInviteBell: LazyComponent<typeof import("../app/components/invite/InviteBell.vue").default>
export const LazyInviteNotification: LazyComponent<typeof import("../app/components/invite/InviteNotification.vue").default>
export const LazyPaymentModal: LazyComponent<typeof import("../app/components/payment/PaymentModal.vue").default>
export const LazyProseA: LazyComponent<typeof import("../node_modules/.pnpm/@nuxtjs+mdc@0.20.2_magicast@0.5.1/node_modules/@nuxtjs/mdc/dist/runtime/components/prose/ProseA.vue").default>
export const LazyProseBlockquote: LazyComponent<typeof import("../node_modules/.pnpm/@nuxtjs+mdc@0.20.2_magicast@0.5.1/node_modules/@nuxtjs/mdc/dist/runtime/components/prose/ProseBlockquote.vue").default>
export const LazyProseCode: LazyComponent<typeof import("../node_modules/.pnpm/@nuxtjs+mdc@0.20.2_magicast@0.5.1/node_modules/@nuxtjs/mdc/dist/runtime/components/prose/ProseCode.vue").default>
export const LazyProseEm: LazyComponent<typeof import("../node_modules/.pnpm/@nuxtjs+mdc@0.20.2_magicast@0.5.1/node_modules/@nuxtjs/mdc/dist/runtime/components/prose/ProseEm.vue").default>
export const LazyProseH1: LazyComponent<typeof import("../node_modules/.pnpm/@nuxtjs+mdc@0.20.2_magicast@0.5.1/node_modules/@nuxtjs/mdc/dist/runtime/components/prose/ProseH1.vue").default>
export const LazyProseH2: LazyComponent<typeof import("../node_modules/.pnpm/@nuxtjs+mdc@0.20.2_magicast@0.5.1/node_modules/@nuxtjs/mdc/dist/runtime/components/prose/ProseH2.vue").default>
export const LazyProseH3: LazyComponent<typeof import("../node_modules/.pnpm/@nuxtjs+mdc@0.20.2_magicast@0.5.1/node_modules/@nuxtjs/mdc/dist/runtime/components/prose/ProseH3.vue").default>
export const LazyProseH4: LazyComponent<typeof import("../node_modules/.pnpm/@nuxtjs+mdc@0.20.2_magicast@0.5.1/node_modules/@nuxtjs/mdc/dist/runtime/components/prose/ProseH4.vue").default>
export const LazyProseH5: LazyComponent<typeof import("../node_modules/.pnpm/@nuxtjs+mdc@0.20.2_magicast@0.5.1/node_modules/@nuxtjs/mdc/dist/runtime/components/prose/ProseH5.vue").default>
export const LazyProseH6: LazyComponent<typeof import("../node_modules/.pnpm/@nuxtjs+mdc@0.20.2_magicast@0.5.1/node_modules/@nuxtjs/mdc/dist/runtime/components/prose/ProseH6.vue").default>
export const LazyProseHr: LazyComponent<typeof import("../node_modules/.pnpm/@nuxtjs+mdc@0.20.2_magicast@0.5.1/node_modules/@nuxtjs/mdc/dist/runtime/components/prose/ProseHr.vue").default>
export const LazyProseImg: LazyComponent<typeof import("../node_modules/.pnpm/@nuxtjs+mdc@0.20.2_magicast@0.5.1/node_modules/@nuxtjs/mdc/dist/runtime/components/prose/ProseImg.vue").default>
export const LazyProseLi: LazyComponent<typeof import("../node_modules/.pnpm/@nuxtjs+mdc@0.20.2_magicast@0.5.1/node_modules/@nuxtjs/mdc/dist/runtime/components/prose/ProseLi.vue").default>
export const LazyProseOl: LazyComponent<typeof import("../node_modules/.pnpm/@nuxtjs+mdc@0.20.2_magicast@0.5.1/node_modules/@nuxtjs/mdc/dist/runtime/components/prose/ProseOl.vue").default>
export const LazyProseP: LazyComponent<typeof import("../node_modules/.pnpm/@nuxtjs+mdc@0.20.2_magicast@0.5.1/node_modules/@nuxtjs/mdc/dist/runtime/components/prose/ProseP.vue").default>
export const LazyProsePre: LazyComponent<typeof import("../node_modules/.pnpm/@nuxtjs+mdc@0.20.2_magicast@0.5.1/node_modules/@nuxtjs/mdc/dist/runtime/components/prose/ProsePre.vue").default>
export const LazyProseScript: LazyComponent<typeof import("../node_modules/.pnpm/@nuxtjs+mdc@0.20.2_magicast@0.5.1/node_modules/@nuxtjs/mdc/dist/runtime/components/prose/ProseScript.vue").default>
export const LazyProseStrong: LazyComponent<typeof import("../node_modules/.pnpm/@nuxtjs+mdc@0.20.2_magicast@0.5.1/node_modules/@nuxtjs/mdc/dist/runtime/components/prose/ProseStrong.vue").default>
export const LazyProseTable: LazyComponent<typeof import("../node_modules/.pnpm/@nuxtjs+mdc@0.20.2_magicast@0.5.1/node_modules/@nuxtjs/mdc/dist/runtime/components/prose/ProseTable.vue").default>
export const LazyProseTbody: LazyComponent<typeof import("../node_modules/.pnpm/@nuxtjs+mdc@0.20.2_magicast@0.5.1/node_modules/@nuxtjs/mdc/dist/runtime/components/prose/ProseTbody.vue").default>
export const LazyProseTd: LazyComponent<typeof import("../node_modules/.pnpm/@nuxtjs+mdc@0.20.2_magicast@0.5.1/node_modules/@nuxtjs/mdc/dist/runtime/components/prose/ProseTd.vue").default>
export const LazyProseTh: LazyComponent<typeof import("../node_modules/.pnpm/@nuxtjs+mdc@0.20.2_magicast@0.5.1/node_modules/@nuxtjs/mdc/dist/runtime/components/prose/ProseTh.vue").default>
export const LazyProseThead: LazyComponent<typeof import("../node_modules/.pnpm/@nuxtjs+mdc@0.20.2_magicast@0.5.1/node_modules/@nuxtjs/mdc/dist/runtime/components/prose/ProseThead.vue").default>
export const LazyProseTr: LazyComponent<typeof import("../node_modules/.pnpm/@nuxtjs+mdc@0.20.2_magicast@0.5.1/node_modules/@nuxtjs/mdc/dist/runtime/components/prose/ProseTr.vue").default>
export const LazyProseUl: LazyComponent<typeof import("../node_modules/.pnpm/@nuxtjs+mdc@0.20.2_magicast@0.5.1/node_modules/@nuxtjs/mdc/dist/runtime/components/prose/ProseUl.vue").default>
export const LazyNuxtWelcome: LazyComponent<typeof import("../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/components/welcome.vue").default>
export const LazyNuxtLayout: LazyComponent<typeof import("../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/components/nuxt-layout").default>
export const LazyNuxtErrorBoundary: LazyComponent<typeof import("../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/components/nuxt-error-boundary.vue").default>
export const LazyClientOnly: LazyComponent<typeof import("../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/components/client-only").default>
export const LazyDevOnly: LazyComponent<typeof import("../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/components/dev-only").default>
export const LazyServerPlaceholder: LazyComponent<typeof import("../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/components/server-placeholder").default>
export const LazyNuxtLink: LazyComponent<typeof import("../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/components/nuxt-link").default>
export const LazyNuxtLoadingIndicator: LazyComponent<typeof import("../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/components/nuxt-loading-indicator").default>
export const LazyNuxtTime: LazyComponent<typeof import("../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/components/nuxt-time.vue").default>
export const LazyNuxtRouteAnnouncer: LazyComponent<typeof import("../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/components/nuxt-route-announcer").default>
export const LazyNuxtImg: LazyComponent<typeof import("../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/components/nuxt-stubs").NuxtImg>
export const LazyNuxtPicture: LazyComponent<typeof import("../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/components/nuxt-stubs").NuxtPicture>
export const LazyContentRenderer: LazyComponent<typeof import("../node_modules/.pnpm/@nuxt+content@3.12.0_better-sqlite3@12.8.0_magicast@0.5.1/node_modules/@nuxt/content/dist/runtime/components/ContentRenderer.vue").default>
export const LazyNuxtLinkLocale: LazyComponent<typeof import("../node_modules/.pnpm/@nuxtjs+i18n@10.2.4_@emnapi+core@1.7.1_@emnapi+runtime@1.7.1_@vue+compiler-dom@3.5.26_d_a5323b549840adcc2cb57cfe4f807173/node_modules/@nuxtjs/i18n/dist/runtime/components/NuxtLinkLocale").default>
export const LazySwitchLocalePathLink: LazyComponent<typeof import("../node_modules/.pnpm/@nuxtjs+i18n@10.2.4_@emnapi+core@1.7.1_@emnapi+runtime@1.7.1_@vue+compiler-dom@3.5.26_d_a5323b549840adcc2cb57cfe4f807173/node_modules/@nuxtjs/i18n/dist/runtime/components/SwitchLocalePathLink").default>
export const LazyNuxtPage: LazyComponent<typeof import("../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/pages/runtime/page").default>
export const LazyNoScript: LazyComponent<typeof import("../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/head/runtime/components").NoScript>
export const LazyLink: LazyComponent<typeof import("../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/head/runtime/components").Link>
export const LazyBase: LazyComponent<typeof import("../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/head/runtime/components").Base>
export const LazyTitle: LazyComponent<typeof import("../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/head/runtime/components").Title>
export const LazyMeta: LazyComponent<typeof import("../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/head/runtime/components").Meta>
export const LazyStyle: LazyComponent<typeof import("../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/head/runtime/components").Style>
export const LazyHead: LazyComponent<typeof import("../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/head/runtime/components").Head>
export const LazyHtml: LazyComponent<typeof import("../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/head/runtime/components").Html>
export const LazyBody: LazyComponent<typeof import("../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/head/runtime/components").Body>
export const LazyMDC: LazyComponent<typeof import("../node_modules/.pnpm/@nuxtjs+mdc@0.20.2_magicast@0.5.1/node_modules/@nuxtjs/mdc/dist/runtime/components/MDC.vue").default>
export const LazyMDCCached: LazyComponent<typeof import("../node_modules/.pnpm/@nuxtjs+mdc@0.20.2_magicast@0.5.1/node_modules/@nuxtjs/mdc/dist/runtime/components/MDCCached.vue").default>
export const LazyMDCRenderer: LazyComponent<typeof import("../node_modules/.pnpm/@nuxtjs+mdc@0.20.2_magicast@0.5.1/node_modules/@nuxtjs/mdc/dist/runtime/components/MDCRenderer.vue").default>
export const LazyMDCSlot: LazyComponent<typeof import("../node_modules/.pnpm/@nuxtjs+mdc@0.20.2_magicast@0.5.1/node_modules/@nuxtjs/mdc/dist/runtime/components/MDCSlot.vue").default>
export const LazyNuxtIsland: LazyComponent<typeof import("../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/components/nuxt-island").default>
export const componentNames: string[]

View File

@@ -0,0 +1,58 @@
const pickExport = (mod, exportName, componentName, path) => {
const resolved = exportName === 'default' ? mod?.default : mod?.[exportName]
if (!resolved) {
throw new Error(`[nuxt-content] Missing export "${exportName}" for component "${componentName}" in "${path}".`)
}
return resolved
}
export const localComponentLoaders = {
LangSwitch: () => import("./../../app/components/LangSwitch.vue").then(m => pickExport(m, "default", "LangSwitch", "./../../app/components/LangSwitch.vue")),
NotificationBell: () => import("./../../app/components/NotificationBell.vue").then(m => pickExport(m, "default", "NotificationBell", "./../../app/components/NotificationBell.vue")),
QrCodeModal: () => import("./../../app/components/QrCodeModal.vue").then(m => pickExport(m, "default", "QrCodeModal", "./../../app/components/QrCodeModal.vue")),
QrLogin: () => import("./../../app/components/QrLogin.vue").then(m => pickExport(m, "default", "QrLogin", "./../../app/components/QrLogin.vue")),
SiteFooter: () => import("./../../app/components/SiteFooter.vue").then(m => pickExport(m, "default", "SiteFooter", "./../../app/components/SiteFooter.vue")),
SiteHeader: () => import("./../../app/components/SiteHeader.vue").then(m => pickExport(m, "default", "SiteHeader", "./../../app/components/SiteHeader.vue")),
AdminMarkdownEditor: () => import("./../../app/components/admin/MarkdownEditor.vue").then(m => pickExport(m, "default", "AdminMarkdownEditor", "./../../app/components/admin/MarkdownEditor.vue")),
AdminMarkdownRenderer: () => import("./../../app/components/admin/MarkdownRenderer.vue").then(m => pickExport(m, "default", "AdminMarkdownRenderer", "./../../app/components/admin/MarkdownRenderer.vue")),
ConsoleAppsCenter: () => import("./../../app/components/console/AppsCenter.vue").then(m => pickExport(m, "default", "ConsoleAppsCenter", "./../../app/components/console/AppsCenter.vue")),
ConsoleHeader: () => import("./../../app/components/console/ConsoleHeader.vue").then(m => pickExport(m, "default", "ConsoleHeader", "./../../app/components/console/ConsoleHeader.vue")),
DeveloperAppDetail: () => import("./../../app/components/developer/AppDetail.vue").then(m => pickExport(m, "default", "DeveloperAppDetail", "./../../app/components/developer/AppDetail.vue")),
DeveloperAppsCenter: () => import("./../../app/components/developer/AppsCenter.vue").then(m => pickExport(m, "default", "DeveloperAppsCenter", "./../../app/components/developer/AppsCenter.vue")),
DeveloperPermissionGuard: () => import("./../../app/components/developer/PermissionGuard.vue").then(m => pickExport(m, "default", "DeveloperPermissionGuard", "./../../app/components/developer/PermissionGuard.vue")),
DeveloperRoleTag: () => import("./../../app/components/developer/RoleTag.vue").then(m => pickExport(m, "default", "DeveloperRoleTag", "./../../app/components/developer/RoleTag.vue")),
InviteBell: () => import("./../../app/components/invite/InviteBell.vue").then(m => pickExport(m, "default", "InviteBell", "./../../app/components/invite/InviteBell.vue")),
InviteNotification: () => import("./../../app/components/invite/InviteNotification.vue").then(m => pickExport(m, "default", "InviteNotification", "./../../app/components/invite/InviteNotification.vue")),
PaymentModal: () => import("./../../app/components/payment/PaymentModal.vue").then(m => pickExport(m, "default", "PaymentModal", "./../../app/components/payment/PaymentModal.vue")),
NuxtWelcome: () => import("./../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/components/welcome.vue").then(m => pickExport(m, "default", "NuxtWelcome", "./../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/components/welcome.vue")),
NuxtLayout: () => import("./../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/components/nuxt-layout").then(m => pickExport(m, "default", "NuxtLayout", "./../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/components/nuxt-layout")),
NuxtErrorBoundary: () => import("./../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/components/nuxt-error-boundary.vue").then(m => pickExport(m, "default", "NuxtErrorBoundary", "./../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/components/nuxt-error-boundary.vue")),
ClientOnly: () => import("./../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/components/client-only").then(m => pickExport(m, "default", "ClientOnly", "./../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/components/client-only")),
DevOnly: () => import("./../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/components/dev-only").then(m => pickExport(m, "default", "DevOnly", "./../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/components/dev-only")),
ServerPlaceholder: () => import("./../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/components/server-placeholder").then(m => pickExport(m, "default", "ServerPlaceholder", "./../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/components/server-placeholder")),
NuxtLink: () => import("./../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/components/nuxt-link").then(m => pickExport(m, "default", "NuxtLink", "./../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/components/nuxt-link")),
NuxtLoadingIndicator: () => import("./../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/components/nuxt-loading-indicator").then(m => pickExport(m, "default", "NuxtLoadingIndicator", "./../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/components/nuxt-loading-indicator")),
NuxtTime: () => import("./../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/components/nuxt-time.vue").then(m => pickExport(m, "default", "NuxtTime", "./../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/components/nuxt-time.vue")),
NuxtRouteAnnouncer: () => import("./../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/components/nuxt-route-announcer").then(m => pickExport(m, "default", "NuxtRouteAnnouncer", "./../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/components/nuxt-route-announcer")),
NuxtImg: () => import("./../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/components/nuxt-stubs").then(m => pickExport(m, "NuxtImg", "NuxtImg", "./../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/components/nuxt-stubs")),
NuxtPicture: () => import("./../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/components/nuxt-stubs").then(m => pickExport(m, "NuxtPicture", "NuxtPicture", "./../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/components/nuxt-stubs")),
ContentRenderer: () => import("./../../node_modules/.pnpm/@nuxt+content@3.12.0_better-sqlite3@12.8.0_magicast@0.5.1/node_modules/@nuxt/content/dist/runtime/components/ContentRenderer.vue").then(m => pickExport(m, "default", "ContentRenderer", "./../../node_modules/.pnpm/@nuxt+content@3.12.0_better-sqlite3@12.8.0_magicast@0.5.1/node_modules/@nuxt/content/dist/runtime/components/ContentRenderer.vue")),
NuxtLinkLocale: () => import("./../../node_modules/.pnpm/@nuxtjs+i18n@10.2.4_@emnapi+core@1.7.1_@emnapi+runtime@1.7.1_@vue+compiler-dom@3.5.26_d_a5323b549840adcc2cb57cfe4f807173/node_modules/@nuxtjs/i18n/dist/runtime/components/NuxtLinkLocale").then(m => pickExport(m, "default", "NuxtLinkLocale", "./../../node_modules/.pnpm/@nuxtjs+i18n@10.2.4_@emnapi+core@1.7.1_@emnapi+runtime@1.7.1_@vue+compiler-dom@3.5.26_d_a5323b549840adcc2cb57cfe4f807173/node_modules/@nuxtjs/i18n/dist/runtime/components/NuxtLinkLocale")),
SwitchLocalePathLink: () => import("./../../node_modules/.pnpm/@nuxtjs+i18n@10.2.4_@emnapi+core@1.7.1_@emnapi+runtime@1.7.1_@vue+compiler-dom@3.5.26_d_a5323b549840adcc2cb57cfe4f807173/node_modules/@nuxtjs/i18n/dist/runtime/components/SwitchLocalePathLink").then(m => pickExport(m, "default", "SwitchLocalePathLink", "./../../node_modules/.pnpm/@nuxtjs+i18n@10.2.4_@emnapi+core@1.7.1_@emnapi+runtime@1.7.1_@vue+compiler-dom@3.5.26_d_a5323b549840adcc2cb57cfe4f807173/node_modules/@nuxtjs/i18n/dist/runtime/components/SwitchLocalePathLink")),
NuxtPage: () => import("./../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/pages/runtime/page").then(m => pickExport(m, "default", "NuxtPage", "./../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/pages/runtime/page")),
NoScript: () => import("./../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/head/runtime/components").then(m => pickExport(m, "NoScript", "NoScript", "./../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/head/runtime/components")),
Link: () => import("./../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/head/runtime/components").then(m => pickExport(m, "Link", "Link", "./../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/head/runtime/components")),
Base: () => import("./../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/head/runtime/components").then(m => pickExport(m, "Base", "Base", "./../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/head/runtime/components")),
Title: () => import("./../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/head/runtime/components").then(m => pickExport(m, "Title", "Title", "./../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/head/runtime/components")),
Meta: () => import("./../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/head/runtime/components").then(m => pickExport(m, "Meta", "Meta", "./../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/head/runtime/components")),
Style: () => import("./../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/head/runtime/components").then(m => pickExport(m, "Style", "Style", "./../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/head/runtime/components")),
Head: () => import("./../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/head/runtime/components").then(m => pickExport(m, "Head", "Head", "./../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/head/runtime/components")),
Html: () => import("./../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/head/runtime/components").then(m => pickExport(m, "Html", "Html", "./../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/head/runtime/components")),
Body: () => import("./../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/head/runtime/components").then(m => pickExport(m, "Body", "Body", "./../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/head/runtime/components")),
MDC: () => import("./../../node_modules/.pnpm/@nuxtjs+mdc@0.20.2_magicast@0.5.1/node_modules/@nuxtjs/mdc/dist/runtime/components/MDC.vue").then(m => pickExport(m, "default", "MDC", "./../../node_modules/.pnpm/@nuxtjs+mdc@0.20.2_magicast@0.5.1/node_modules/@nuxtjs/mdc/dist/runtime/components/MDC.vue")),
MDCCached: () => import("./../../node_modules/.pnpm/@nuxtjs+mdc@0.20.2_magicast@0.5.1/node_modules/@nuxtjs/mdc/dist/runtime/components/MDCCached.vue").then(m => pickExport(m, "default", "MDCCached", "./../../node_modules/.pnpm/@nuxtjs+mdc@0.20.2_magicast@0.5.1/node_modules/@nuxtjs/mdc/dist/runtime/components/MDCCached.vue")),
MDCRenderer: () => import("./../../node_modules/.pnpm/@nuxtjs+mdc@0.20.2_magicast@0.5.1/node_modules/@nuxtjs/mdc/dist/runtime/components/MDCRenderer.vue").then(m => pickExport(m, "default", "MDCRenderer", "./../../node_modules/.pnpm/@nuxtjs+mdc@0.20.2_magicast@0.5.1/node_modules/@nuxtjs/mdc/dist/runtime/components/MDCRenderer.vue")),
MDCSlot: () => import("./../../node_modules/.pnpm/@nuxtjs+mdc@0.20.2_magicast@0.5.1/node_modules/@nuxtjs/mdc/dist/runtime/components/MDCSlot.vue").then(m => pickExport(m, "default", "MDCSlot", "./../../node_modules/.pnpm/@nuxtjs+mdc@0.20.2_magicast@0.5.1/node_modules/@nuxtjs/mdc/dist/runtime/components/MDCSlot.vue")),
NuxtIsland: () => import("./../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/components/nuxt-island").then(m => pickExport(m, "default", "NuxtIsland", "./../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/components/nuxt-island")),
}
export const globalComponents: string[] = ["ProseA","ProseBlockquote","ProseCode","ProseEm","ProseH1","ProseH2","ProseH3","ProseH4","ProseH5","ProseH6","ProseHr","ProseImg","ProseLi","ProseOl","ProseP","ProsePre","ProseScript","ProseStrong","ProseTable","ProseTbody","ProseTd","ProseTh","ProseThead","ProseTr","ProseUl"]
export const localComponents: string[] = ["LangSwitch","NotificationBell","QrCodeModal","QrLogin","SiteFooter","SiteHeader","AdminMarkdownEditor","AdminMarkdownRenderer","ConsoleAppsCenter","ConsoleHeader","DeveloperAppDetail","DeveloperAppsCenter","DeveloperPermissionGuard","DeveloperRoleTag","InviteBell","InviteNotification","PaymentModal","NuxtWelcome","NuxtLayout","NuxtErrorBoundary","ClientOnly","DevOnly","ServerPlaceholder","NuxtLink","NuxtLoadingIndicator","NuxtTime","NuxtRouteAnnouncer","NuxtImg","NuxtPicture","ContentRenderer","NuxtLinkLocale","SwitchLocalePathLink","NuxtPage","NoScript","Link","Base","Title","Meta","Style","Head","Html","Body","MDC","MDCCached","MDCRenderer","MDCSlot","NuxtIsland"]

File diff suppressed because one or more lines are too long

33
.nuxt/content/manifest.ts Normal file
View File

@@ -0,0 +1,33 @@
export const checksums = {
"docs": "v3.5.0--jyttfyMCdlkkD2Cy6uAigSOupLz7w9JK12BEQDx-Jb8"
}
export const checksumsStructure = {
"docs": "quFkNIUZZFAwcn0ok74-KsIERem9u0p5DW-cqEgxrPA"
}
export const tables = {
"docs": "_content_docs",
"info": "_content_info"
}
export default {
"docs": {
"type": "page",
"fields": {
"id": "string",
"title": "string",
"body": "json",
"description": "string",
"extension": "string",
"meta": "json",
"navigation": "json",
"path": "string",
"seo": "json",
"stem": "string"
}
},
"info": {
"type": "data",
"fields": {}
}
}

File diff suppressed because one or more lines are too long

21
.nuxt/content/types.d.ts vendored Normal file
View File

@@ -0,0 +1,21 @@
import type { PageCollectionItemBase, DataCollectionItemBase } from '@nuxt/content'
declare module '@nuxt/content' {
/* eslint-disable */
/**
* This file was automatically generated by json-schema-to-typescript.
* DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file,
* and run json-schema-to-typescript to regenerate this file.
*/
interface DocsCollectionItem extends PageCollectionItemBase {}
interface PageCollections {
docs: DocsCollectionItem
}
interface Collections {
docs: DocsCollectionItem
}
}

4271
.nuxt/dev/index.mjs Normal file

File diff suppressed because it is too large Load Diff

1
.nuxt/dev/index.mjs.map Normal file

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,3 @@
// Generated by @nuxtjs/i18n
export const pathToI18nConfig = {};
export const i18nPathToPath = {};

49
.nuxt/imports.d.ts vendored Normal file
View File

@@ -0,0 +1,49 @@
export { useScriptTriggerConsent, useScriptEventPage, useScriptTriggerElement, useScript, useScriptGoogleAnalytics, useScriptPlausibleAnalytics, useScriptCrisp, useScriptClarity, useScriptCloudflareWebAnalytics, useScriptFathomAnalytics, useScriptMatomoAnalytics, useScriptGoogleTagManager, useScriptGoogleAdsense, useScriptSegment, useScriptMetaPixel, useScriptXPixel, useScriptIntercom, useScriptHotjar, useScriptStripe, useScriptLemonSqueezy, useScriptVimeoPlayer, useScriptYouTubePlayer, useScriptGoogleMaps, useScriptNpm, useScriptUmamiAnalytics, useScriptSnapchatPixel, useScriptRybbitAnalytics, useScriptDatabuddyAnalytics, useScriptRedditPixel, useScriptPayPal } from '#app/composables/script-stubs';
export { isVue2, isVue3 } from 'vue-demi';
export { defineNuxtLink } from '#app/components/nuxt-link';
export { useNuxtApp, tryUseNuxtApp, defineNuxtPlugin, definePayloadPlugin, useRuntimeConfig, defineAppConfig } from '#app/nuxt';
export { useAppConfig, updateAppConfig } from '#app/config';
export { defineNuxtComponent } from '#app/composables/component';
export { useAsyncData, useLazyAsyncData, useNuxtData, refreshNuxtData, clearNuxtData } from '#app/composables/asyncData';
export { useHydration } from '#app/composables/hydrate';
export { callOnce } from '#app/composables/once';
export { useState, clearNuxtState } from '#app/composables/state';
export { clearError, createError, isNuxtError, showError, useError } from '#app/composables/error';
export { useFetch, useLazyFetch } from '#app/composables/fetch';
export { useCookie, refreshCookie } from '#app/composables/cookie';
export { onPrehydrate, prerenderRoutes, useRequestHeader, useRequestHeaders, useResponseHeader, useRequestEvent, useRequestFetch, setResponseStatus } from '#app/composables/ssr';
export { onNuxtReady } from '#app/composables/ready';
export { preloadComponents, prefetchComponents, preloadRouteComponents } from '#app/composables/preload';
export { abortNavigation, addRouteMiddleware, defineNuxtRouteMiddleware, setPageLayout, navigateTo, useRoute, useRouter } from '#app/composables/router';
export { isPrerendered, loadPayload, preloadPayload, definePayloadReducer, definePayloadReviver } from '#app/composables/payload';
export { useLoadingIndicator } from '#app/composables/loading-indicator';
export { getAppManifest, getRouteRules } from '#app/composables/manifest';
export { reloadNuxtApp } from '#app/composables/chunk';
export { useRequestURL } from '#app/composables/url';
export { usePreviewMode } from '#app/composables/preview';
export { useRouteAnnouncer } from '#app/composables/route-announcer';
export { useRuntimeHook } from '#app/composables/runtime-hook';
export { useHead, useHeadSafe, useServerHeadSafe, useServerHead, useSeoMeta, useServerSeoMeta, injectHead } from '#app/composables/head';
export { onBeforeRouteLeave, onBeforeRouteUpdate, useLink } from 'vue-router';
export { withCtx, withDirectives, withKeys, withMemo, withModifiers, withScopeId, onActivated, onBeforeMount, onBeforeUnmount, onBeforeUpdate, onDeactivated, onErrorCaptured, onMounted, onRenderTracked, onRenderTriggered, onServerPrefetch, onUnmounted, onUpdated, computed, customRef, isProxy, isReactive, isReadonly, isRef, markRaw, proxyRefs, reactive, readonly, ref, shallowReactive, shallowReadonly, shallowRef, toRaw, toRef, toRefs, triggerRef, unref, watch, watchEffect, watchPostEffect, watchSyncEffect, onWatcherCleanup, isShallow, effect, effectScope, getCurrentScope, onScopeDispose, defineComponent, defineAsyncComponent, resolveComponent, getCurrentInstance, h, inject, hasInjectionContext, nextTick, provide, toValue, useModel, useAttrs, useCssModule, useCssVars, useSlots, useTransitionState, useId, useTemplateRef, useShadowRoot, Component, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue';
export { requestIdleCallback, cancelIdleCallback } from '#app/compat/idle-callback';
export { setInterval } from '#app/compat/interval';
export { useRouteBaseName, useLocalePath, useLocaleRoute, useSwitchLocalePath, useLocaleHead, useBrowserLocale, useCookieLocale, useSetI18nParams, useI18nPreloadKeys, defineI18nRoute, defineI18nLocale, defineI18nConfig } from '../node_modules/.pnpm/@nuxtjs+i18n@10.2.4_@emnapi+core@1.7.1_@emnapi+runtime@1.7.1_@vue+compiler-dom@3.5.26_d_a5323b549840adcc2cb57cfe4f807173/node_modules/@nuxtjs/i18n/dist/runtime/composables/index';
export { definePageMeta } from '../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/pages/runtime/composables';
export { defineLazyHydrationComponent } from '#app/composables/lazy-hydration';
export { ROLE_HIERARCHY, ROLE_LABEL, ROLE_COLOR, useAppPermission, AppPermission } from '../app/composables/useAppPermission';
export { notificationTypeMap, useNotificationCenter } from '../app/composables/useNotificationCenter';
export { usePageSeo } from '../app/composables/usePageSeo';
export { generateQrCodeDataUrl, generateQrCodeCanvas } from '../app/composables/useQRCode';
export { isResourceOwner, getResourceAccessLevel, canViewBasic, canViewConnection, canViewSensitive, canManageResource, isMaskedValue, useResourceAccess, enrichResourcesWithPermission, ResourceAccessLevel } from '../app/composables/useResourceAccess';
export { resolveWebHomeUrl, getAppEntries, getPrimaryEntry, getScanTip, executeEntry, EntryType, AppEntry } from '../app/utils/appEntry';
export { getTenantId } from '../app/utils/domain';
export { default as permission, setAuthz, clearAuthz, setAuthzFromUser, hasRole, hasAnyRole, hasPermission, hasAnyPermission } from '../app/utils/permission';
export { default as request } from '../app/utils/request';
export { setToken, getToken, removeToken } from '../app/utils/token-util';
export { queryCollection, queryCollectionSearchSections, queryCollectionNavigation, queryCollectionItemSurroundings } from '../node_modules/.pnpm/@nuxt+content@3.12.0_better-sqlite3@12.8.0_magicast@0.5.1/node_modules/@nuxt/content/dist/runtime/client';
export { useI18n } from 'vue-i18n';
export { useNuxtDevTools } from '../node_modules/.pnpm/@nuxt+devtools@3.1.1_vite@7.3.0_@types+node@25.0.3_jiti@2.6.1_terser@5.44.1_yaml@2.8.2__vue@3.5.26_typescript@5.9.3_/node_modules/@nuxt/devtools/dist/runtime/use-nuxt-devtools';
export { flatUnwrap as unwrapSlot } from '../node_modules/.pnpm/@nuxtjs+mdc@0.20.2_magicast@0.5.1/node_modules/@nuxtjs/mdc/dist/runtime/utils/node';
export { parseMarkdown } from '../node_modules/.pnpm/@nuxtjs+mdc@0.20.2_magicast@0.5.1/node_modules/@nuxtjs/mdc/dist/runtime/parser';
export { stringifyMarkdown } from '../node_modules/.pnpm/@nuxtjs+mdc@0.20.2_magicast@0.5.1/node_modules/@nuxtjs/mdc/dist/runtime/stringify';

8
.nuxt/mdc-configs.mjs Normal file
View File

@@ -0,0 +1,8 @@
let configs
export function getMdcConfigs () {
if (!configs) {
configs = Promise.all([
])
}
return configs
}

217
.nuxt/mdc-highlighter.mjs Normal file
View File

@@ -0,0 +1,217 @@
import { getMdcConfigs } from '#mdc-configs'
import { createOnigurumaEngine } from '@shikijs/engine-oniguruma'
import { createJavaScriptRegexEngine } from "@shikijs/engine-javascript";
export function createShikiHighlighter({
langs = [],
themes = [],
bundledLangs = {},
bundledThemes = {},
getMdcConfigs,
options: shikiOptions,
engine
} = {}) {
let shiki;
let configs;
async function _getShiki() {
const { createHighlighterCore, addClassToHast, isSpecialLang, isSpecialTheme } = await import("@shikijs/core");
const { transformerNotationDiff, transformerNotationErrorLevel, transformerNotationFocus, transformerNotationHighlight } = await import("@shikijs/transformers");
const shiki2 = await createHighlighterCore({
langs,
themes,
engine: engine || createJavaScriptRegexEngine()
});
for await (const config of await getConfigs()) {
await config.shiki?.setup?.(shiki2);
}
return {
shiki: shiki2,
addClassToHast,
isSpecialLang,
isSpecialTheme,
transformers: [
transformerNotationDiff(),
transformerNotationErrorLevel(),
transformerNotationFocus(),
transformerNotationHighlight()
]
};
}
async function getShiki() {
if (!shiki) {
shiki = _getShiki();
}
return shiki;
}
async function getConfigs() {
if (!configs) {
configs = Promise.resolve(getMdcConfigs?.() || []);
}
return configs;
}
const highlighter = async (code, lang, theme, options = {}) => {
const {
shiki: shiki2,
addClassToHast,
isSpecialLang,
isSpecialTheme,
transformers: baseTransformers
} = await getShiki();
const codeToHastOptions = {
defaultColor: false,
meta: {
__raw: options.meta
}
};
if (lang === "ts-type" || lang === "typescript-type") {
lang = "typescript";
codeToHastOptions.grammarContextCode = "let a:";
} else if (lang === "vue-html" || lang === "vue-template") {
lang = "vue";
codeToHastOptions.grammarContextCode = "<template>";
}
const themesObject = { ...typeof theme === "string" ? { default: theme } : theme || {} };
const loadedThemes = shiki2.getLoadedThemes();
const loadedLanguages = shiki2.getLoadedLanguages();
if (typeof lang === "string" && !loadedLanguages.includes(lang) && !isSpecialLang(lang)) {
if (bundledLangs[lang]) {
await shiki2.loadLanguage(bundledLangs[lang]);
} else {
if (process.dev) {
console.warn(`[@nuxtjs/mdc] Language "${lang}" is not loaded to the Shiki highlighter, fallback to plain text. Add the language to "mdc.highlight.langs" to fix this.`);
}
lang = "text";
}
}
for (const [color, theme2] of Object.entries(themesObject)) {
if (typeof theme2 === "string" && !loadedThemes.includes(theme2) && !isSpecialTheme(theme2)) {
if (bundledThemes[theme2]) {
await shiki2.loadTheme(bundledThemes[theme2]);
} else {
if (process.dev) {
console.warn(`[@nuxtjs/mdc] Theme "${theme2}" is not loaded to the Shiki highlighter. Add the theme to "mdc.highlight.themes" to fix this.`);
}
themesObject[color] = "none";
}
}
}
const transformersMap = /* @__PURE__ */ new Map();
for (const transformer of baseTransformers) {
transformersMap.set(transformer.name || `transformer:${Math.random()}-${transformer.constructor.name}`, transformer);
}
for (const config of await getConfigs()) {
const newTransformers = typeof config.shiki?.transformers === "function" ? await config.shiki?.transformers(code, lang, theme, options) : config.shiki?.transformers || [];
for (const transformer of newTransformers) {
transformersMap.set(transformer.name || `transformer:${Math.random()}-${transformer.constructor.name}`, transformer);
}
}
const root = shiki2.codeToHast(code.trimEnd(), {
lang,
...codeToHastOptions,
themes: themesObject,
transformers: [
...transformersMap.values(),
{
name: "mdc:highlight",
line(node, line) {
if (options.highlights?.includes(line))
addClassToHast(node, "highlight");
node.properties.line = line;
}
},
{
name: "mdc:newline",
line(node) {
if (code?.includes("\n")) {
if (node.children.length === 0 || node.children.length === 1 && node.children[0].type === "element" && node.children[0].children.length === 1 && node.children[0].children[0].type === "text" && node.children[0].children[0].value === "") {
node.children = [{
type: "element",
tagName: "span",
properties: {
emptyLinePlaceholder: true
},
children: [{ type: "text", value: "\n" }]
}];
return;
}
const last = node.children.at(-1);
if (last?.type === "element" && last.tagName === "span") {
const text = last.children.at(-1);
if (text?.type === "text")
text.value += "\n";
}
}
}
}
]
});
const preEl = root.children[0];
const codeEl = preEl.children[0];
const wrapperStyle = shikiOptions?.wrapperStyle;
preEl.properties.style = wrapperStyle ? typeof wrapperStyle === "string" ? wrapperStyle : preEl.properties.style : "";
const styles = [];
Object.keys(themesObject).forEach((color) => {
const colorScheme = color !== "default" ? `.${color}` : "";
styles.push(
wrapperStyle ? `${colorScheme} .shiki,` : "",
`html .${color} .shiki span {`,
`color: var(--shiki-${color});`,
`background: var(--shiki-${color}-bg);`,
`font-style: var(--shiki-${color}-font-style);`,
`font-weight: var(--shiki-${color}-font-weight);`,
`text-decoration: var(--shiki-${color}-text-decoration);`,
"}"
);
styles.push(
`html${colorScheme} .shiki span {`,
`color: var(--shiki-${color});`,
`background: var(--shiki-${color}-bg);`,
`font-style: var(--shiki-${color}-font-style);`,
`font-weight: var(--shiki-${color}-font-weight);`,
`text-decoration: var(--shiki-${color}-text-decoration);`,
"}"
);
});
return {
tree: codeEl.children,
className: Array.isArray(preEl.properties.class) ? preEl.properties.class.join(" ") : preEl.properties.class,
inlineStyle: preEl.properties.style,
style: styles.join("")
};
};
return highlighter;
}
const bundledLangs = {
"javascript": () => import('@shikijs/langs/javascript').then(r => r.default || r),
"js": () => import('@shikijs/langs/javascript').then(r => r.default || r),
"cjs": () => import('@shikijs/langs/javascript').then(r => r.default || r),
"mjs": () => import('@shikijs/langs/javascript').then(r => r.default || r),
"jsx": () => import('@shikijs/langs/jsx').then(r => r.default || r),
"json": () => import('@shikijs/langs/json').then(r => r.default || r),
"typescript": () => import('@shikijs/langs/typescript').then(r => r.default || r),
"ts": () => import('@shikijs/langs/typescript').then(r => r.default || r),
"cts": () => import('@shikijs/langs/typescript').then(r => r.default || r),
"mts": () => import('@shikijs/langs/typescript').then(r => r.default || r),
"tsx": () => import('@shikijs/langs/tsx').then(r => r.default || r),
"vue": () => import('@shikijs/langs/vue').then(r => r.default || r),
"css": () => import('@shikijs/langs/css').then(r => r.default || r),
"html": () => import('@shikijs/langs/html').then(r => r.default || r),
"shellscript": () => import('@shikijs/langs/shellscript').then(r => r.default || r),
"bash": () => import('@shikijs/langs/shellscript').then(r => r.default || r),
"sh": () => import('@shikijs/langs/shellscript').then(r => r.default || r),
"shell": () => import('@shikijs/langs/shellscript').then(r => r.default || r),
"zsh": () => import('@shikijs/langs/shellscript').then(r => r.default || r),
"markdown": () => import('@shikijs/langs/markdown').then(r => r.default || r),
"md": () => import('@shikijs/langs/markdown').then(r => r.default || r),
"mdc": () => import('@shikijs/langs/mdc').then(r => r.default || r),
"yaml": () => import('@shikijs/langs/yaml').then(r => r.default || r),
"yml": () => import('@shikijs/langs/yaml').then(r => r.default || r),
}
const bundledThemes = {
"github-light": () => import('@shikijs/themes/github-light').then(r => r.default || r),
"github-dark": () => import('@shikijs/themes/github-dark').then(r => r.default || r),
}
const options = {"theme":{"default":"github-light","dark":"github-dark"}}
const engine = createOnigurumaEngine(() => import('shiki/wasm'))
const highlighter = createShikiHighlighter({ bundledLangs, bundledThemes, options, getMdcConfigs, engine })
export default highlighter

View File

@@ -0,0 +1 @@
export default "img"

12
.nuxt/mdc-imports.mjs Normal file
View File

@@ -0,0 +1,12 @@
import _RemarkEmoji from 'remark-emoji'
import _Highlight from '/Users/gxwebsoft/VUE/tiantian-system/node_modules/.pnpm/@nuxtjs+mdc@0.20.2_magicast@0.5.1/node_modules/@nuxtjs/mdc/dist/runtime/highlighter/rehype-nuxt.js'
export const remarkPlugins = {
'remark-emoji': { instance: _RemarkEmoji },
}
export const rehypePlugins = {
'highlight': { instance: _Highlight, options: {} },
}
export const highlight = {"theme":{"default":"github-light","dark":"github-dark"}}

17
.nuxt/nitro.json Normal file
View File

@@ -0,0 +1,17 @@
{
"date": "2026-04-08T23:33:08.624Z",
"preset": "nitro-dev",
"framework": {
"name": "nuxt",
"version": "4.2.2"
},
"versions": {
"nitro": "2.12.8"
},
"dev": {
"pid": 94352,
"workerAddress": {
"socketPath": "/var/folders/qz/k8gbknb502j_f8wbdcc8bs6h0000gn/T/nitro-worker-94352-1-1-8709.sock"
}
}
}

26
.nuxt/nuxt.d.ts vendored Normal file
View File

@@ -0,0 +1,26 @@
/// <reference types="./modules/fix-tailwind-postcss" />
/// <reference types="@nuxtjs/tailwindcss" />
/// <reference types="@nuxt/content" />
/// <reference types="@nuxtjs/i18n" />
/// <reference types="@nuxt/devtools" />
/// <reference types="@nuxt/telemetry" />
/// <reference path="types/builder-env.d.ts" />
/// <reference path="types/plugins.d.ts" />
/// <reference path="types/build.d.ts" />
/// <reference path="types/app.config.d.ts" />
/// <reference path="types/runtime-config.d.ts" />
/// <reference types="nuxt/app" />
/// <reference types="/Users/gxwebsoft/VUE/tiantian-system/node_modules/.pnpm/@nuxt+nitro-server@4.2.2_better-sqlite3@12.8.0_db0@0.3.4_better-sqlite3@12.8.0__ioredis_a0a2be7525d559e696e64db570f075d2/node_modules/@nuxt/nitro-server/dist/index.mjs" />
/// <reference path="content/types.d.ts" />
/// <reference path="types/i18n-plugin.d.ts" />
/// <reference types="vue-router" />
/// <reference path="types/middleware.d.ts" />
/// <reference path="types/nitro-middleware.d.ts" />
/// <reference path="types/layouts.d.ts" />
/// <reference path="types/components.d.ts" />
/// <reference path="imports.d.ts" />
/// <reference path="types/imports.d.ts" />
/// <reference path="schema/nuxt.schema.d.ts" />
/// <reference path="types/nitro.d.ts" />
export {}

9
.nuxt/nuxt.json Normal file
View File

@@ -0,0 +1,9 @@
{
"_hash": "ZY4lm8jhCM6pZliWKYkdQZl-nhCk0lcFDozHfi-GXz4",
"project": {
"rootDir": "/Users/gxwebsoft/VUE/tiantian-system"
},
"versions": {
"nuxt": "4.2.2"
}
}

16
.nuxt/nuxt.node.d.ts vendored Normal file
View File

@@ -0,0 +1,16 @@
/// <reference types="./modules/fix-tailwind-postcss" />
/// <reference types="@nuxtjs/tailwindcss" />
/// <reference types="@nuxt/content" />
/// <reference types="@nuxtjs/i18n" />
/// <reference types="@nuxt/devtools" />
/// <reference types="@nuxt/telemetry" />
/// <reference path="types/modules.d.ts" />
/// <reference path="types/runtime-config.d.ts" />
/// <reference path="types/app.config.d.ts" />
/// <reference types="nuxt" />
/// <reference types="../node_modules/.pnpm/@nuxt+vite-builder@4.2.2_@types+node@25.0.3_eslint@9.39.2_jiti@2.6.1__magicast@0.5.1_nu_d02b07d05348134360f42d727d38b333/node_modules/@nuxt/vite-builder/dist/index.mjs" />
/// <reference types="/Users/gxwebsoft/VUE/tiantian-system/node_modules/.pnpm/@nuxt+nitro-server@4.2.2_better-sqlite3@12.8.0_db0@0.3.4_better-sqlite3@12.8.0__ioredis_a0a2be7525d559e696e64db570f075d2/node_modules/@nuxt/nitro-server/dist/index.mjs" />
/// <reference path="types/nitro-middleware.d.ts" />
/// <reference path="schema/nuxt.schema.d.ts" />
export {}

5
.nuxt/nuxt.shared.d.ts vendored Normal file
View File

@@ -0,0 +1,5 @@
/// <reference path="types/runtime-config.d.ts" />
/// <reference path="types/app.config.d.ts" />
/// <reference path="schema/nuxt.schema.d.ts" />
export {}

17
.nuxt/schema/nuxt.schema.d.ts vendored Normal file
View File

@@ -0,0 +1,17 @@
export interface NuxtCustomSchema {
}
export type CustomAppConfig = Exclude<NuxtCustomSchema['appConfig'], undefined>
type _CustomAppConfig = CustomAppConfig
declare module '@nuxt/schema' {
interface NuxtConfig extends Omit<NuxtCustomSchema, 'appConfig'> {}
interface NuxtOptions extends Omit<NuxtCustomSchema, 'appConfig'> {}
interface CustomAppConfig extends _CustomAppConfig {}
}
declare module 'nuxt/schema' {
interface NuxtConfig extends Omit<NuxtCustomSchema, 'appConfig'> {}
interface NuxtOptions extends Omit<NuxtCustomSchema, 'appConfig'> {}
interface CustomAppConfig extends _CustomAppConfig {}
}

View File

@@ -0,0 +1,3 @@
{
"id": "#"
}

View File

@@ -0,0 +1,16 @@
// generated by the @nuxtjs/tailwindcss <https://github.com/nuxt-modules/tailwindcss> module at 4/9/2026, 7:35:30 AM
import "@nuxtjs/tailwindcss/config-ctx"
import configMerger from "@nuxtjs/tailwindcss/merger";
import cfg2 from "./../../tailwind.config.cjs";
import cfg3 from "./../../tailwind.config.cjs";
const config = [
{"content":{"files":["/Users/gxwebsoft/VUE/tiantian-system/app/components/**/*.{vue,js,jsx,mjs,ts,tsx}","/Users/gxwebsoft/VUE/tiantian-system/app/components/global/**/*.{vue,js,jsx,mjs,ts,tsx}","/Users/gxwebsoft/VUE/tiantian-system/app/components/**/*.{vue,js,jsx,mjs,ts,tsx}","/Users/gxwebsoft/VUE/tiantian-system/app/layouts/**/*.{vue,js,jsx,mjs,ts,tsx}","/Users/gxwebsoft/VUE/tiantian-system/app/plugins/**/*.{js,ts,mjs}","/Users/gxwebsoft/VUE/tiantian-system/app/composables/**/*.{js,ts,mjs}","/Users/gxwebsoft/VUE/tiantian-system/app/utils/**/*.{js,ts,mjs}","/Users/gxwebsoft/VUE/tiantian-system/app/pages/**/*.{vue,js,jsx,mjs,ts,tsx}","/Users/gxwebsoft/VUE/tiantian-system/app/{A,a}pp.{vue,js,jsx,mjs,ts,tsx}","/Users/gxwebsoft/VUE/tiantian-system/app/{E,e}rror.{vue,js,jsx,mjs,ts,tsx}","/Users/gxwebsoft/VUE/tiantian-system/app/app.config.{js,ts,mjs}"]}},
{},
cfg2,
cfg3
].reduce((acc, curr) => configMerger(acc, curr), {});
const resolvedConfig = config;
export default resolvedConfig;

263
.nuxt/tsconfig.app.json Normal file
View File

@@ -0,0 +1,263 @@
{
"compilerOptions": {
"paths": {
"nitropack/types": [
"../node_modules/.pnpm/nitropack@2.12.9_better-sqlite3@12.8.0/node_modules/nitropack/types"
],
"nitropack/runtime": [
"../node_modules/.pnpm/nitropack@2.12.9_better-sqlite3@12.8.0/node_modules/nitropack/runtime"
],
"nitropack": [
"../node_modules/.pnpm/nitropack@2.12.9_better-sqlite3@12.8.0/node_modules/nitropack"
],
"defu": [
"../node_modules/.pnpm/defu@6.1.4/node_modules/defu"
],
"h3": [
"../node_modules/.pnpm/h3@1.15.4/node_modules/h3"
],
"consola": [
"../node_modules/.pnpm/consola@3.4.2/node_modules/consola"
],
"ofetch": [
"../node_modules/.pnpm/ofetch@1.5.1/node_modules/ofetch"
],
"@unhead/vue": [
"../node_modules/.pnpm/@unhead+vue@2.1.1_vue@3.5.26_typescript@5.9.3_/node_modules/@unhead/vue"
],
"@nuxt/devtools": [
"../node_modules/.pnpm/@nuxt+devtools@3.1.1_vite@7.3.0_@types+node@25.0.3_jiti@2.6.1_terser@5.44.1_yaml@2.8.2__vue@3.5.26_typescript@5.9.3_/node_modules/@nuxt/devtools"
],
"@vue/runtime-core": [
"../node_modules/.pnpm/@vue+runtime-core@3.5.26/node_modules/@vue/runtime-core"
],
"@vue/compiler-sfc": [
"../node_modules/.pnpm/@vue+compiler-sfc@3.5.26/node_modules/@vue/compiler-sfc"
],
"unplugin-vue-router/client": [
"../node_modules/.pnpm/unplugin-vue-router@0.19.1_@vue+compiler-sfc@3.5.26_vue-router@4.6.4_vue@3.5.26_typescr_afeae63e4951cf0abe0304554948e4ca/node_modules/unplugin-vue-router/client"
],
"@nuxt/schema": [
"../node_modules/.pnpm/@nuxt+schema@4.2.2/node_modules/@nuxt/schema"
],
"nuxt": [
"../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt"
],
"vite/client": [
"../node_modules/.pnpm/vite@7.3.0_@types+node@25.0.3_jiti@2.6.1_terser@5.44.1_yaml@2.8.2/node_modules/vite/client"
],
"@nuxtjs/mdc": [
"../node_modules/.pnpm/@nuxtjs+mdc@0.20.2_magicast@0.5.1/node_modules/@nuxtjs/mdc"
],
"vue-i18n": [
"../node_modules/.pnpm/vue-i18n@11.3.0_vue@3.5.26_typescript@5.9.3_/node_modules/vue-i18n/dist/vue-i18n",
"../node_modules/.pnpm/vue-i18n@11.3.0_vue@3.5.26_typescript@5.9.3_/node_modules/vue-i18n"
],
"@intlify/shared": [
"../node_modules/.pnpm/@intlify+shared@11.3.0/node_modules/@intlify/shared/dist/shared",
"../node_modules/.pnpm/@intlify+shared@11.3.0/node_modules/@intlify/shared"
],
"@intlify/core": [
"../node_modules/.pnpm/@intlify+core@11.3.0/node_modules/@intlify/core/dist/core.node",
"../node_modules/.pnpm/@intlify+core@11.3.0/node_modules/@intlify/core"
],
"@intlify/core-base": [
"../node_modules/.pnpm/@intlify+core-base@11.3.0/node_modules/@intlify/core-base/dist/core-base",
"../node_modules/.pnpm/@intlify+core-base@11.3.0/node_modules/@intlify/core-base"
],
"@intlify/utils": [
"../node_modules/.pnpm/@intlify+utils@0.14.1/node_modules/@intlify/utils/dist/index",
"../node_modules/.pnpm/@intlify+utils@0.14.1/node_modules/@intlify/utils"
],
"@intlify/utils/h3": [
"../node_modules/.pnpm/@intlify+utils@0.14.1/node_modules/@intlify/utils/dist/h3",
"../node_modules/.pnpm/@intlify+utils@0.14.1/node_modules/@intlify/utils"
],
"@intlify/message-compiler": [
"../node_modules/.pnpm/@intlify+message-compiler@11.3.0/node_modules/@intlify/message-compiler/dist/message-compiler",
"../node_modules/.pnpm/@intlify+message-compiler@11.3.0/node_modules/@intlify/message-compiler"
],
"#content/components": [
"./content/components"
],
"#content/manifest": [
"./content/manifest"
],
"~": [
"../app"
],
"~/*": [
"../app/*"
],
"@": [
"../app"
],
"@/*": [
"../app/*"
],
"~~": [
".."
],
"~~/*": [
"../*"
],
"@@": [
".."
],
"@@/*": [
"../*"
],
"#shared": [
"../shared"
],
"#shared/*": [
"../shared/*"
],
"#app": [
"../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app"
],
"#app/*": [
"../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/*"
],
"vue-demi": [
"../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/compat/vue-demi"
],
"#i18n": [
"../node_modules/.pnpm/@nuxtjs+i18n@10.2.4_@emnapi+core@1.7.1_@emnapi+runtime@1.7.1_@vue+compiler-dom@3.5.26_d_a5323b549840adcc2cb57cfe4f807173/node_modules/@nuxtjs/i18n/dist/runtime/composables/index"
],
"#i18n-kit": [
"../node_modules/.pnpm/@nuxtjs+i18n@10.2.4_@emnapi+core@1.7.1_@emnapi+runtime@1.7.1_@vue+compiler-dom@3.5.26_d_a5323b549840adcc2cb57cfe4f807173/node_modules/@nuxtjs/i18n/dist/runtime/kit"
],
"#i18n-kit/*": [
"../node_modules/.pnpm/@nuxtjs+i18n@10.2.4_@emnapi+core@1.7.1_@emnapi+runtime@1.7.1_@vue+compiler-dom@3.5.26_d_a5323b549840adcc2cb57cfe4f807173/node_modules/@nuxtjs/i18n/dist/runtime/kit/*"
],
"#internal-i18n-types": [
"../node_modules/.pnpm/@nuxtjs+i18n@10.2.4_@emnapi+core@1.7.1_@emnapi+runtime@1.7.1_@vue+compiler-dom@3.5.26_d_a5323b549840adcc2cb57cfe4f807173/node_modules/@nuxtjs/i18n/dist/types"
],
"#vue-router": [
"../node_modules/.pnpm/vue-router@4.6.4_vue@3.5.26_typescript@5.9.3_/node_modules/vue-router"
],
"#unhead/composables": [
"../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/head/runtime/composables"
],
"#imports": [
"./imports"
],
"#mdc-configs": [
"./mdc-configs"
],
"#mdc-highlighter": [
"./mdc-highlighter"
],
"#mdc-imports": [
"./mdc-imports"
],
"#app-manifest": [
"../node_modules/.pnpm/mocked-exports@0.1.1/node_modules/mocked-exports/lib/proxy"
],
"#components": [
"./components"
],
"#build": [
"."
],
"#build/*": [
"./*"
]
},
"esModuleInterop": true,
"skipLibCheck": true,
"target": "ESNext",
"allowJs": true,
"resolveJsonModule": true,
"moduleDetection": "force",
"isolatedModules": true,
"verbatimModuleSyntax": true,
"strict": true,
"noUncheckedIndexedAccess": true,
"forceConsistentCasingInFileNames": true,
"noImplicitOverride": true,
"module": "preserve",
"noEmit": true,
"lib": [
"ESNext",
"dom",
"dom.iterable",
"webworker"
],
"jsx": "preserve",
"jsxImportSource": "vue",
"types": [],
"moduleResolution": "Bundler",
"useDefineForClassFields": true,
"noImplicitThis": true,
"allowSyntheticDefaultImports": true
},
"include": [
"../i18n/**/*",
"./nuxt.d.ts",
"../app/**/*",
"../modules/*/runtime/**/*",
"../test/nuxt/**/*",
"../tests/nuxt/**/*",
"../layers/*/app/**/*",
"../layers/*/modules/*/runtime/**/*",
"../shared/**/*.d.ts",
"../modules/*/shared/**/*.d.ts",
"../layers/*/shared/**/*.d.ts",
"../*.d.ts",
"../layers/*/*.d.ts",
"../modules/runtime",
"../modules/dist/runtime"
],
"exclude": [
"../node_modules",
"../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/node_modules",
"../node_modules/.pnpm/@nuxt+content@3.12.0_better-sqlite3@12.8.0_magicast@0.5.1/node_modules/@nuxt/content/node_modules",
"../node_modules/.pnpm/@nuxtjs+tailwindcss@6.14.0_magicast@0.5.1_yaml@2.8.2/node_modules/@nuxtjs/tailwindcss/node_modules",
"../node_modules/.pnpm/@nuxtjs+i18n@10.2.4_@emnapi+core@1.7.1_@emnapi+runtime@1.7.1_@vue+compiler-dom@3.5.26_d_a5323b549840adcc2cb57cfe4f807173/node_modules/@nuxtjs/i18n/node_modules",
"../node_modules/.pnpm/@nuxt+devtools@3.1.1_vite@7.3.0_@types+node@25.0.3_jiti@2.6.1_terser@5.44.1_yaml@2.8.2__vue@3.5.26_typescript@5.9.3_/node_modules/@nuxt/devtools/node_modules",
"../node_modules/.pnpm/@nuxt+telemetry@2.6.6_magicast@0.5.1/node_modules/@nuxt/telemetry/node_modules",
"../node_modules/.pnpm/@nuxtjs+mdc@0.20.2_magicast@0.5.1/node_modules/@nuxtjs/mdc/node_modules",
"../dist",
"../.data",
"../modules/*/runtime/server/**/*",
"../layers/*/server/**/*",
"../layers/*/modules/*/runtime/server/**/*",
"../modules/*.*",
"../nuxt.config.*",
"../.config/nuxt.*",
"../layers/*/nuxt.config.*",
"../layers/*/.config/nuxt.*",
"../layers/*/modules/**/*",
"../node_modules/.pnpm/@nuxt+content@3.12.0_better-sqlite3@12.8.0_magicast@0.5.1/node_modules/runtime/server",
"../node_modules/.pnpm/@nuxt+content@3.12.0_better-sqlite3@12.8.0_magicast@0.5.1/node_modules/dist/runtime/server",
"../node_modules/.pnpm/@nuxt+content@3.12.0_better-sqlite3@12.8.0_magicast@0.5.1/node_modules/*.*",
"../node_modules/.pnpm/@nuxt+content@3.12.0_better-sqlite3@12.8.0_magicast@0.5.1/node_modules/dist/*.*",
"../node_modules/.pnpm/@nuxtjs+tailwindcss@6.14.0_magicast@0.5.1_yaml@2.8.2/node_modules/runtime/server",
"../node_modules/.pnpm/@nuxtjs+tailwindcss@6.14.0_magicast@0.5.1_yaml@2.8.2/node_modules/dist/runtime/server",
"../node_modules/.pnpm/@nuxtjs+tailwindcss@6.14.0_magicast@0.5.1_yaml@2.8.2/node_modules/*.*",
"../node_modules/.pnpm/@nuxtjs+tailwindcss@6.14.0_magicast@0.5.1_yaml@2.8.2/node_modules/dist/*.*",
"../node_modules/.pnpm/@nuxtjs+i18n@10.2.4_@emnapi+core@1.7.1_@emnapi+runtime@1.7.1_@vue+compiler-dom@3.5.26_d_a5323b549840adcc2cb57cfe4f807173/node_modules/runtime/server",
"../node_modules/.pnpm/@nuxtjs+i18n@10.2.4_@emnapi+core@1.7.1_@emnapi+runtime@1.7.1_@vue+compiler-dom@3.5.26_d_a5323b549840adcc2cb57cfe4f807173/node_modules/dist/runtime/server",
"../node_modules/.pnpm/@nuxtjs+i18n@10.2.4_@emnapi+core@1.7.1_@emnapi+runtime@1.7.1_@vue+compiler-dom@3.5.26_d_a5323b549840adcc2cb57cfe4f807173/node_modules/*.*",
"../node_modules/.pnpm/@nuxtjs+i18n@10.2.4_@emnapi+core@1.7.1_@emnapi+runtime@1.7.1_@vue+compiler-dom@3.5.26_d_a5323b549840adcc2cb57cfe4f807173/node_modules/dist/*.*",
"../modules/runtime/server",
"../modules/dist/runtime/server",
"../modules/dist/*.*",
"../node_modules/.pnpm/@nuxt+devtools@3.1.1_vite@7.3.0_@types+node@25.0.3_jiti@2.6.1_terser@5.44.1_yaml@2.8.2__vue@3.5.26_typescript@5.9.3_/node_modules/runtime/server",
"../node_modules/.pnpm/@nuxt+devtools@3.1.1_vite@7.3.0_@types+node@25.0.3_jiti@2.6.1_terser@5.44.1_yaml@2.8.2__vue@3.5.26_typescript@5.9.3_/node_modules/dist/runtime/server",
"../node_modules/.pnpm/@nuxt+devtools@3.1.1_vite@7.3.0_@types+node@25.0.3_jiti@2.6.1_terser@5.44.1_yaml@2.8.2__vue@3.5.26_typescript@5.9.3_/node_modules/*.*",
"../node_modules/.pnpm/@nuxt+devtools@3.1.1_vite@7.3.0_@types+node@25.0.3_jiti@2.6.1_terser@5.44.1_yaml@2.8.2__vue@3.5.26_typescript@5.9.3_/node_modules/dist/*.*",
"../node_modules/.pnpm/@nuxt+telemetry@2.6.6_magicast@0.5.1/node_modules/runtime/server",
"../node_modules/.pnpm/@nuxt+telemetry@2.6.6_magicast@0.5.1/node_modules/dist/runtime/server",
"../node_modules/.pnpm/@nuxt+telemetry@2.6.6_magicast@0.5.1/node_modules/*.*",
"../node_modules/.pnpm/@nuxt+telemetry@2.6.6_magicast@0.5.1/node_modules/dist/*.*",
"../node_modules/.pnpm/@nuxtjs+mdc@0.20.2_magicast@0.5.1/node_modules/runtime/server",
"../node_modules/.pnpm/@nuxtjs+mdc@0.20.2_magicast@0.5.1/node_modules/dist/runtime/server",
"../node_modules/.pnpm/@nuxtjs+mdc@0.20.2_magicast@0.5.1/node_modules/*.*",
"../node_modules/.pnpm/@nuxtjs+mdc@0.20.2_magicast@0.5.1/node_modules/dist/*.*",
"dev",
"../server"
]
}

264
.nuxt/tsconfig.json Normal file
View File

@@ -0,0 +1,264 @@
{
"compilerOptions": {
"paths": {
"nitropack/types": [
"../node_modules/.pnpm/nitropack@2.12.9_better-sqlite3@12.8.0/node_modules/nitropack/types"
],
"nitropack/runtime": [
"../node_modules/.pnpm/nitropack@2.12.9_better-sqlite3@12.8.0/node_modules/nitropack/runtime"
],
"nitropack": [
"../node_modules/.pnpm/nitropack@2.12.9_better-sqlite3@12.8.0/node_modules/nitropack"
],
"defu": [
"../node_modules/.pnpm/defu@6.1.4/node_modules/defu"
],
"h3": [
"../node_modules/.pnpm/h3@1.15.4/node_modules/h3"
],
"consola": [
"../node_modules/.pnpm/consola@3.4.2/node_modules/consola"
],
"ofetch": [
"../node_modules/.pnpm/ofetch@1.5.1/node_modules/ofetch"
],
"@unhead/vue": [
"../node_modules/.pnpm/@unhead+vue@2.1.1_vue@3.5.26_typescript@5.9.3_/node_modules/@unhead/vue"
],
"@nuxt/devtools": [
"../node_modules/.pnpm/@nuxt+devtools@3.1.1_vite@7.3.0_@types+node@25.0.3_jiti@2.6.1_terser@5.44.1_yaml@2.8.2__vue@3.5.26_typescript@5.9.3_/node_modules/@nuxt/devtools"
],
"@vue/runtime-core": [
"../node_modules/.pnpm/@vue+runtime-core@3.5.26/node_modules/@vue/runtime-core"
],
"@vue/compiler-sfc": [
"../node_modules/.pnpm/@vue+compiler-sfc@3.5.26/node_modules/@vue/compiler-sfc"
],
"unplugin-vue-router/client": [
"../node_modules/.pnpm/unplugin-vue-router@0.19.1_@vue+compiler-sfc@3.5.26_vue-router@4.6.4_vue@3.5.26_typescr_afeae63e4951cf0abe0304554948e4ca/node_modules/unplugin-vue-router/client"
],
"@nuxt/schema": [
"../node_modules/.pnpm/@nuxt+schema@4.2.2/node_modules/@nuxt/schema"
],
"nuxt": [
"../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt"
],
"vite/client": [
"../node_modules/.pnpm/vite@7.3.0_@types+node@25.0.3_jiti@2.6.1_terser@5.44.1_yaml@2.8.2/node_modules/vite/client"
],
"@nuxtjs/mdc": [
"../node_modules/.pnpm/@nuxtjs+mdc@0.20.2_magicast@0.5.1/node_modules/@nuxtjs/mdc"
],
"vue-i18n": [
"../node_modules/.pnpm/vue-i18n@11.3.0_vue@3.5.26_typescript@5.9.3_/node_modules/vue-i18n/dist/vue-i18n",
"../node_modules/.pnpm/vue-i18n@11.3.0_vue@3.5.26_typescript@5.9.3_/node_modules/vue-i18n"
],
"@intlify/shared": [
"../node_modules/.pnpm/@intlify+shared@11.3.0/node_modules/@intlify/shared/dist/shared",
"../node_modules/.pnpm/@intlify+shared@11.3.0/node_modules/@intlify/shared"
],
"@intlify/core": [
"../node_modules/.pnpm/@intlify+core@11.3.0/node_modules/@intlify/core/dist/core.node",
"../node_modules/.pnpm/@intlify+core@11.3.0/node_modules/@intlify/core"
],
"@intlify/core-base": [
"../node_modules/.pnpm/@intlify+core-base@11.3.0/node_modules/@intlify/core-base/dist/core-base",
"../node_modules/.pnpm/@intlify+core-base@11.3.0/node_modules/@intlify/core-base"
],
"@intlify/utils": [
"../node_modules/.pnpm/@intlify+utils@0.14.1/node_modules/@intlify/utils/dist/index",
"../node_modules/.pnpm/@intlify+utils@0.14.1/node_modules/@intlify/utils"
],
"@intlify/utils/h3": [
"../node_modules/.pnpm/@intlify+utils@0.14.1/node_modules/@intlify/utils/dist/h3",
"../node_modules/.pnpm/@intlify+utils@0.14.1/node_modules/@intlify/utils"
],
"@intlify/message-compiler": [
"../node_modules/.pnpm/@intlify+message-compiler@11.3.0/node_modules/@intlify/message-compiler/dist/message-compiler",
"../node_modules/.pnpm/@intlify+message-compiler@11.3.0/node_modules/@intlify/message-compiler"
],
"#content/components": [
"./content/components"
],
"#content/manifest": [
"./content/manifest"
],
"~": [
"../app"
],
"~/*": [
"../app/*"
],
"@": [
"../app"
],
"@/*": [
"../app/*"
],
"~~": [
".."
],
"~~/*": [
"../*"
],
"@@": [
".."
],
"@@/*": [
"../*"
],
"#shared": [
"../shared"
],
"#shared/*": [
"../shared/*"
],
"#app": [
"../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app"
],
"#app/*": [
"../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/*"
],
"vue-demi": [
"../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/compat/vue-demi"
],
"#i18n": [
"../node_modules/.pnpm/@nuxtjs+i18n@10.2.4_@emnapi+core@1.7.1_@emnapi+runtime@1.7.1_@vue+compiler-dom@3.5.26_d_a5323b549840adcc2cb57cfe4f807173/node_modules/@nuxtjs/i18n/dist/runtime/composables/index"
],
"#i18n-kit": [
"../node_modules/.pnpm/@nuxtjs+i18n@10.2.4_@emnapi+core@1.7.1_@emnapi+runtime@1.7.1_@vue+compiler-dom@3.5.26_d_a5323b549840adcc2cb57cfe4f807173/node_modules/@nuxtjs/i18n/dist/runtime/kit"
],
"#i18n-kit/*": [
"../node_modules/.pnpm/@nuxtjs+i18n@10.2.4_@emnapi+core@1.7.1_@emnapi+runtime@1.7.1_@vue+compiler-dom@3.5.26_d_a5323b549840adcc2cb57cfe4f807173/node_modules/@nuxtjs/i18n/dist/runtime/kit/*"
],
"#internal-i18n-types": [
"../node_modules/.pnpm/@nuxtjs+i18n@10.2.4_@emnapi+core@1.7.1_@emnapi+runtime@1.7.1_@vue+compiler-dom@3.5.26_d_a5323b549840adcc2cb57cfe4f807173/node_modules/@nuxtjs/i18n/dist/types"
],
"#vue-router": [
"../node_modules/.pnpm/vue-router@4.6.4_vue@3.5.26_typescript@5.9.3_/node_modules/vue-router"
],
"#unhead/composables": [
"../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/head/runtime/composables"
],
"#imports": [
"./imports"
],
"#mdc-configs": [
"./mdc-configs"
],
"#mdc-highlighter": [
"./mdc-highlighter"
],
"#mdc-imports": [
"./mdc-imports"
],
"#app-manifest": [
"../node_modules/.pnpm/mocked-exports@0.1.1/node_modules/mocked-exports/lib/proxy"
],
"#components": [
"./components"
],
"#build": [
"."
],
"#build/*": [
"./*"
]
},
"esModuleInterop": true,
"skipLibCheck": true,
"target": "ESNext",
"allowJs": true,
"resolveJsonModule": true,
"moduleDetection": "force",
"isolatedModules": true,
"verbatimModuleSyntax": true,
"strict": true,
"noUncheckedIndexedAccess": true,
"forceConsistentCasingInFileNames": true,
"noImplicitOverride": true,
"module": "preserve",
"noEmit": true,
"lib": [
"ESNext",
"dom",
"dom.iterable",
"webworker"
],
"jsx": "preserve",
"jsxImportSource": "vue",
"types": [],
"moduleResolution": "Bundler",
"useDefineForClassFields": true,
"noImplicitThis": true,
"allowSyntheticDefaultImports": true
},
"include": [
"../i18n/**/*",
"./nuxt.d.ts",
"../app/**/*",
"../modules/*/runtime/**/*",
"../test/nuxt/**/*",
"../tests/nuxt/**/*",
"../layers/*/app/**/*",
"../layers/*/modules/*/runtime/**/*",
"../shared/**/*.d.ts",
"../modules/*/shared/**/*.d.ts",
"../layers/*/shared/**/*.d.ts",
"../*.d.ts",
"../layers/*/*.d.ts",
"../modules/runtime",
"../modules/dist/runtime",
"./nuxt.node.d.ts",
"../modules/*.*",
"../nuxt.config.*",
"../.config/nuxt.*",
"../layers/*/nuxt.config.*",
"../layers/*/.config/nuxt.*",
"../layers/*/modules/**/*",
"../shared/**/*",
"../modules/*/shared/**/*",
"../layers/*/shared/**/*",
"../node_modules/.pnpm/@nuxt+content@3.12.0_better-sqlite3@12.8.0_magicast@0.5.1/node_modules/runtime",
"../node_modules/.pnpm/@nuxt+content@3.12.0_better-sqlite3@12.8.0_magicast@0.5.1/node_modules/dist/runtime",
"../node_modules/.pnpm/@nuxtjs+tailwindcss@6.14.0_magicast@0.5.1_yaml@2.8.2/node_modules/runtime",
"../node_modules/.pnpm/@nuxtjs+tailwindcss@6.14.0_magicast@0.5.1_yaml@2.8.2/node_modules/dist/runtime",
"../node_modules/.pnpm/@nuxtjs+i18n@10.2.4_@emnapi+core@1.7.1_@emnapi+runtime@1.7.1_@vue+compiler-dom@3.5.26_d_a5323b549840adcc2cb57cfe4f807173/node_modules/runtime",
"../node_modules/.pnpm/@nuxtjs+i18n@10.2.4_@emnapi+core@1.7.1_@emnapi+runtime@1.7.1_@vue+compiler-dom@3.5.26_d_a5323b549840adcc2cb57cfe4f807173/node_modules/dist/runtime",
"../node_modules/.pnpm/@nuxt+devtools@3.1.1_vite@7.3.0_@types+node@25.0.3_jiti@2.6.1_terser@5.44.1_yaml@2.8.2__vue@3.5.26_typescript@5.9.3_/node_modules/runtime",
"../node_modules/.pnpm/@nuxt+devtools@3.1.1_vite@7.3.0_@types+node@25.0.3_jiti@2.6.1_terser@5.44.1_yaml@2.8.2__vue@3.5.26_typescript@5.9.3_/node_modules/dist/runtime",
"../node_modules/.pnpm/@nuxt+telemetry@2.6.6_magicast@0.5.1/node_modules/runtime",
"../node_modules/.pnpm/@nuxt+telemetry@2.6.6_magicast@0.5.1/node_modules/dist/runtime",
"../node_modules/.pnpm/@nuxtjs+mdc@0.20.2_magicast@0.5.1/node_modules/runtime",
"../node_modules/.pnpm/@nuxtjs+mdc@0.20.2_magicast@0.5.1/node_modules/dist/runtime"
],
"exclude": [
"../node_modules",
"../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/node_modules",
"../node_modules/.pnpm/@nuxt+content@3.12.0_better-sqlite3@12.8.0_magicast@0.5.1/node_modules/@nuxt/content/node_modules",
"../node_modules/.pnpm/@nuxtjs+tailwindcss@6.14.0_magicast@0.5.1_yaml@2.8.2/node_modules/@nuxtjs/tailwindcss/node_modules",
"../node_modules/.pnpm/@nuxtjs+i18n@10.2.4_@emnapi+core@1.7.1_@emnapi+runtime@1.7.1_@vue+compiler-dom@3.5.26_d_a5323b549840adcc2cb57cfe4f807173/node_modules/@nuxtjs/i18n/node_modules",
"../node_modules/.pnpm/@nuxt+devtools@3.1.1_vite@7.3.0_@types+node@25.0.3_jiti@2.6.1_terser@5.44.1_yaml@2.8.2__vue@3.5.26_typescript@5.9.3_/node_modules/@nuxt/devtools/node_modules",
"../node_modules/.pnpm/@nuxt+telemetry@2.6.6_magicast@0.5.1/node_modules/@nuxt/telemetry/node_modules",
"../node_modules/.pnpm/@nuxtjs+mdc@0.20.2_magicast@0.5.1/node_modules/@nuxtjs/mdc/node_modules",
"../dist",
"../.data",
"../modules/*/runtime/server/**/*",
"../layers/*/server/**/*",
"../layers/*/modules/*/runtime/server/**/*",
"../node_modules/.pnpm/@nuxt+content@3.12.0_better-sqlite3@12.8.0_magicast@0.5.1/node_modules/runtime/server",
"../node_modules/.pnpm/@nuxt+content@3.12.0_better-sqlite3@12.8.0_magicast@0.5.1/node_modules/dist/runtime/server",
"../node_modules/.pnpm/@nuxtjs+tailwindcss@6.14.0_magicast@0.5.1_yaml@2.8.2/node_modules/runtime/server",
"../node_modules/.pnpm/@nuxtjs+tailwindcss@6.14.0_magicast@0.5.1_yaml@2.8.2/node_modules/dist/runtime/server",
"../node_modules/.pnpm/@nuxtjs+i18n@10.2.4_@emnapi+core@1.7.1_@emnapi+runtime@1.7.1_@vue+compiler-dom@3.5.26_d_a5323b549840adcc2cb57cfe4f807173/node_modules/runtime/server",
"../node_modules/.pnpm/@nuxtjs+i18n@10.2.4_@emnapi+core@1.7.1_@emnapi+runtime@1.7.1_@vue+compiler-dom@3.5.26_d_a5323b549840adcc2cb57cfe4f807173/node_modules/dist/runtime/server",
"../modules/runtime/server",
"../modules/dist/runtime/server",
"../node_modules/.pnpm/@nuxt+devtools@3.1.1_vite@7.3.0_@types+node@25.0.3_jiti@2.6.1_terser@5.44.1_yaml@2.8.2__vue@3.5.26_typescript@5.9.3_/node_modules/runtime/server",
"../node_modules/.pnpm/@nuxt+devtools@3.1.1_vite@7.3.0_@types+node@25.0.3_jiti@2.6.1_terser@5.44.1_yaml@2.8.2__vue@3.5.26_typescript@5.9.3_/node_modules/dist/runtime/server",
"../node_modules/.pnpm/@nuxt+telemetry@2.6.6_magicast@0.5.1/node_modules/runtime/server",
"../node_modules/.pnpm/@nuxt+telemetry@2.6.6_magicast@0.5.1/node_modules/dist/runtime/server",
"../node_modules/.pnpm/@nuxtjs+mdc@0.20.2_magicast@0.5.1/node_modules/runtime/server",
"../node_modules/.pnpm/@nuxtjs+mdc@0.20.2_magicast@0.5.1/node_modules/dist/runtime/server"
]
}

140
.nuxt/tsconfig.node.json Normal file
View File

@@ -0,0 +1,140 @@
{
"compilerOptions": {
"paths": {
"nitropack/types": [
"../node_modules/.pnpm/nitropack@2.12.9_better-sqlite3@12.8.0/node_modules/nitropack/types"
],
"nitropack/runtime": [
"../node_modules/.pnpm/nitropack@2.12.9_better-sqlite3@12.8.0/node_modules/nitropack/runtime"
],
"nitropack": [
"../node_modules/.pnpm/nitropack@2.12.9_better-sqlite3@12.8.0/node_modules/nitropack"
],
"defu": [
"../node_modules/.pnpm/defu@6.1.4/node_modules/defu"
],
"h3": [
"../node_modules/.pnpm/h3@1.15.4/node_modules/h3"
],
"consola": [
"../node_modules/.pnpm/consola@3.4.2/node_modules/consola"
],
"ofetch": [
"../node_modules/.pnpm/ofetch@1.5.1/node_modules/ofetch"
],
"@unhead/vue": [
"../node_modules/.pnpm/@unhead+vue@2.1.1_vue@3.5.26_typescript@5.9.3_/node_modules/@unhead/vue"
],
"@nuxt/devtools": [
"../node_modules/.pnpm/@nuxt+devtools@3.1.1_vite@7.3.0_@types+node@25.0.3_jiti@2.6.1_terser@5.44.1_yaml@2.8.2__vue@3.5.26_typescript@5.9.3_/node_modules/@nuxt/devtools"
],
"@vue/runtime-core": [
"../node_modules/.pnpm/@vue+runtime-core@3.5.26/node_modules/@vue/runtime-core"
],
"@vue/compiler-sfc": [
"../node_modules/.pnpm/@vue+compiler-sfc@3.5.26/node_modules/@vue/compiler-sfc"
],
"unplugin-vue-router/client": [
"../node_modules/.pnpm/unplugin-vue-router@0.19.1_@vue+compiler-sfc@3.5.26_vue-router@4.6.4_vue@3.5.26_typescr_afeae63e4951cf0abe0304554948e4ca/node_modules/unplugin-vue-router/client"
],
"@nuxt/schema": [
"../node_modules/.pnpm/@nuxt+schema@4.2.2/node_modules/@nuxt/schema"
],
"nuxt": [
"../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt"
],
"vite/client": [
"../node_modules/.pnpm/vite@7.3.0_@types+node@25.0.3_jiti@2.6.1_terser@5.44.1_yaml@2.8.2/node_modules/vite/client"
],
"@nuxtjs/mdc": [
"../node_modules/.pnpm/@nuxtjs+mdc@0.20.2_magicast@0.5.1/node_modules/@nuxtjs/mdc"
],
"vue-i18n": [
"../node_modules/.pnpm/vue-i18n@11.3.0_vue@3.5.26_typescript@5.9.3_/node_modules/vue-i18n"
],
"@intlify/shared": [
"../node_modules/.pnpm/@intlify+shared@11.3.0/node_modules/@intlify/shared"
],
"@intlify/core": [
"../node_modules/.pnpm/@intlify+core@11.3.0/node_modules/@intlify/core"
],
"@intlify/core-base": [
"../node_modules/.pnpm/@intlify+core-base@11.3.0/node_modules/@intlify/core-base"
],
"@intlify/utils": [
"../node_modules/.pnpm/@intlify+utils@0.14.1/node_modules/@intlify/utils"
],
"@intlify/utils/h3": [
"../node_modules/.pnpm/@intlify+utils@0.14.1/node_modules/@intlify/utils"
],
"@intlify/message-compiler": [
"../node_modules/.pnpm/@intlify+message-compiler@11.3.0/node_modules/@intlify/message-compiler"
]
},
"esModuleInterop": true,
"skipLibCheck": true,
"target": "ESNext",
"allowJs": true,
"resolveJsonModule": true,
"moduleDetection": "force",
"isolatedModules": true,
"verbatimModuleSyntax": true,
"strict": true,
"noUncheckedIndexedAccess": true,
"forceConsistentCasingInFileNames": true,
"noImplicitOverride": true,
"module": "preserve",
"noEmit": true,
"types": [],
"moduleResolution": "Bundler",
"useDefineForClassFields": true,
"noImplicitThis": true,
"allowSyntheticDefaultImports": true
},
"include": [
"./nuxt.node.d.ts",
"../modules/*.*",
"../nuxt.config.*",
"../.config/nuxt.*",
"../layers/*/nuxt.config.*",
"../layers/*/.config/nuxt.*",
"../layers/*/modules/**/*",
"../nuxt.schema.*",
"../layers/*/nuxt.schema.*"
],
"exclude": [
"../node_modules",
"../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/node_modules",
"../node_modules/.pnpm/@nuxt+content@3.12.0_better-sqlite3@12.8.0_magicast@0.5.1/node_modules/@nuxt/content/node_modules",
"../node_modules/.pnpm/@nuxtjs+tailwindcss@6.14.0_magicast@0.5.1_yaml@2.8.2/node_modules/@nuxtjs/tailwindcss/node_modules",
"../node_modules/.pnpm/@nuxtjs+i18n@10.2.4_@emnapi+core@1.7.1_@emnapi+runtime@1.7.1_@vue+compiler-dom@3.5.26_d_a5323b549840adcc2cb57cfe4f807173/node_modules/@nuxtjs/i18n/node_modules",
"../node_modules/.pnpm/@nuxt+devtools@3.1.1_vite@7.3.0_@types+node@25.0.3_jiti@2.6.1_terser@5.44.1_yaml@2.8.2__vue@3.5.26_typescript@5.9.3_/node_modules/@nuxt/devtools/node_modules",
"../node_modules/.pnpm/@nuxt+telemetry@2.6.6_magicast@0.5.1/node_modules/@nuxt/telemetry/node_modules",
"../node_modules/.pnpm/@nuxtjs+mdc@0.20.2_magicast@0.5.1/node_modules/@nuxtjs/mdc/node_modules",
"../dist",
"../.data",
"../app/**/*",
"../modules/*/runtime/**/*",
"../test/nuxt/**/*",
"../tests/nuxt/**/*",
"../layers/*/app/**/*",
"../layers/*/modules/*/runtime/**/*",
"../modules/*/runtime/server/**/*",
"../layers/*/server/**/*",
"../layers/*/modules/*/runtime/server/**/*",
"../node_modules/.pnpm/@nuxt+content@3.12.0_better-sqlite3@12.8.0_magicast@0.5.1/node_modules/runtime",
"../node_modules/.pnpm/@nuxt+content@3.12.0_better-sqlite3@12.8.0_magicast@0.5.1/node_modules/dist/runtime",
"../node_modules/.pnpm/@nuxtjs+tailwindcss@6.14.0_magicast@0.5.1_yaml@2.8.2/node_modules/runtime",
"../node_modules/.pnpm/@nuxtjs+tailwindcss@6.14.0_magicast@0.5.1_yaml@2.8.2/node_modules/dist/runtime",
"../node_modules/.pnpm/@nuxtjs+i18n@10.2.4_@emnapi+core@1.7.1_@emnapi+runtime@1.7.1_@vue+compiler-dom@3.5.26_d_a5323b549840adcc2cb57cfe4f807173/node_modules/runtime",
"../node_modules/.pnpm/@nuxtjs+i18n@10.2.4_@emnapi+core@1.7.1_@emnapi+runtime@1.7.1_@vue+compiler-dom@3.5.26_d_a5323b549840adcc2cb57cfe4f807173/node_modules/dist/runtime",
"../modules/runtime",
"../modules/dist/runtime",
"../node_modules/.pnpm/@nuxt+devtools@3.1.1_vite@7.3.0_@types+node@25.0.3_jiti@2.6.1_terser@5.44.1_yaml@2.8.2__vue@3.5.26_typescript@5.9.3_/node_modules/runtime",
"../node_modules/.pnpm/@nuxt+devtools@3.1.1_vite@7.3.0_@types+node@25.0.3_jiti@2.6.1_terser@5.44.1_yaml@2.8.2__vue@3.5.26_typescript@5.9.3_/node_modules/dist/runtime",
"../node_modules/.pnpm/@nuxt+telemetry@2.6.6_magicast@0.5.1/node_modules/runtime",
"../node_modules/.pnpm/@nuxt+telemetry@2.6.6_magicast@0.5.1/node_modules/dist/runtime",
"../node_modules/.pnpm/@nuxtjs+mdc@0.20.2_magicast@0.5.1/node_modules/runtime",
"../node_modules/.pnpm/@nuxtjs+mdc@0.20.2_magicast@0.5.1/node_modules/dist/runtime"
]
}

194
.nuxt/tsconfig.server.json Normal file
View File

@@ -0,0 +1,194 @@
{
"compilerOptions": {
"forceConsistentCasingInFileNames": true,
"strict": true,
"noEmit": true,
"skipLibCheck": true,
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "Bundler",
"allowJs": true,
"resolveJsonModule": true,
"jsx": "preserve",
"allowSyntheticDefaultImports": true,
"jsxFactory": "h",
"jsxFragmentFactory": "Fragment",
"paths": {
"#imports": [
"./types/nitro-imports"
],
"~/*": [
"../app/*"
],
"@/*": [
"../app/*"
],
"~~/*": [
"../*"
],
"@@/*": [
"../*"
],
"nitropack/types": [
"../node_modules/.pnpm/nitropack@2.12.9_better-sqlite3@12.8.0/node_modules/nitropack/types"
],
"nitropack/runtime": [
"../node_modules/.pnpm/nitropack@2.12.9_better-sqlite3@12.8.0/node_modules/nitropack/runtime"
],
"nitropack": [
"../node_modules/.pnpm/nitropack@2.12.9_better-sqlite3@12.8.0/node_modules/nitropack"
],
"defu": [
"../node_modules/.pnpm/defu@6.1.4/node_modules/defu"
],
"h3": [
"../node_modules/.pnpm/h3@1.15.4/node_modules/h3"
],
"consola": [
"../node_modules/.pnpm/consola@3.4.2/node_modules/consola"
],
"ofetch": [
"../node_modules/.pnpm/ofetch@1.5.1/node_modules/ofetch"
],
"@unhead/vue": [
"../node_modules/.pnpm/@unhead+vue@2.1.1_vue@3.5.26_typescript@5.9.3_/node_modules/@unhead/vue"
],
"@nuxt/devtools": [
"../node_modules/.pnpm/@nuxt+devtools@3.1.1_vite@7.3.0_@types+node@25.0.3_jiti@2.6.1_terser@5.44.1_yaml@2.8.2__vue@3.5.26_typescript@5.9.3_/node_modules/@nuxt/devtools"
],
"@vue/runtime-core": [
"../node_modules/.pnpm/@vue+runtime-core@3.5.26/node_modules/@vue/runtime-core"
],
"@vue/compiler-sfc": [
"../node_modules/.pnpm/@vue+compiler-sfc@3.5.26/node_modules/@vue/compiler-sfc"
],
"unplugin-vue-router/client": [
"../node_modules/.pnpm/unplugin-vue-router@0.19.1_@vue+compiler-sfc@3.5.26_vue-router@4.6.4_vue@3.5.26_typescr_afeae63e4951cf0abe0304554948e4ca/node_modules/unplugin-vue-router/client"
],
"@nuxt/schema": [
"../node_modules/.pnpm/@nuxt+schema@4.2.2/node_modules/@nuxt/schema"
],
"nuxt": [
"../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt"
],
"vite/client": [
"../node_modules/.pnpm/vite@7.3.0_@types+node@25.0.3_jiti@2.6.1_terser@5.44.1_yaml@2.8.2/node_modules/vite/client"
],
"@nuxtjs/mdc": [
"../node_modules/.pnpm/@nuxtjs+mdc@0.20.2_magicast@0.5.1/node_modules/@nuxtjs/mdc"
],
"vue-i18n": [
"../node_modules/.pnpm/vue-i18n@11.3.0_vue@3.5.26_typescript@5.9.3_/node_modules/vue-i18n"
],
"@intlify/shared": [
"../node_modules/.pnpm/@intlify+shared@11.3.0/node_modules/@intlify/shared"
],
"@intlify/core": [
"../node_modules/.pnpm/@intlify+core@11.3.0/node_modules/@intlify/core"
],
"@intlify/core-base": [
"../node_modules/.pnpm/@intlify+core-base@11.3.0/node_modules/@intlify/core-base"
],
"@intlify/utils": [
"../node_modules/.pnpm/@intlify+utils@0.14.1/node_modules/@intlify/utils"
],
"@intlify/utils/h3": [
"../node_modules/.pnpm/@intlify+utils@0.14.1/node_modules/@intlify/utils"
],
"@intlify/message-compiler": [
"../node_modules/.pnpm/@intlify+message-compiler@11.3.0/node_modules/@intlify/message-compiler"
],
"#content/components": [
"./content/components"
],
"#content/manifest": [
"./content/manifest"
],
"#shared": [
"../shared"
],
"#shared/*": [
"../shared/*"
],
"#build": [
"./"
],
"#build/*": [
"./*"
],
"#internal/nuxt/paths": [
"../node_modules/.pnpm/@nuxt+nitro-server@4.2.2_better-sqlite3@12.8.0_db0@0.3.4_better-sqlite3@12.8.0__ioredis_a0a2be7525d559e696e64db570f075d2/node_modules/@nuxt/nitro-server/dist/runtime/utils/paths"
],
"#i18n": [
"../node_modules/.pnpm/@nuxtjs+i18n@10.2.4_@emnapi+core@1.7.1_@emnapi+runtime@1.7.1_@vue+compiler-dom@3.5.26_d_a5323b549840adcc2cb57cfe4f807173/node_modules/@nuxtjs/i18n/dist/runtime/composables/index-server"
],
"#i18n-kit": [
"../node_modules/.pnpm/@nuxtjs+i18n@10.2.4_@emnapi+core@1.7.1_@emnapi+runtime@1.7.1_@vue+compiler-dom@3.5.26_d_a5323b549840adcc2cb57cfe4f807173/node_modules/@nuxtjs/i18n/dist/runtime/kit"
],
"#i18n-kit/*": [
"../node_modules/.pnpm/@nuxtjs+i18n@10.2.4_@emnapi+core@1.7.1_@emnapi+runtime@1.7.1_@vue+compiler-dom@3.5.26_d_a5323b549840adcc2cb57cfe4f807173/node_modules/@nuxtjs/i18n/dist/runtime/kit/*"
],
"#internal-i18n-types": [
"../node_modules/.pnpm/@nuxtjs+i18n@10.2.4_@emnapi+core@1.7.1_@emnapi+runtime@1.7.1_@vue+compiler-dom@3.5.26_d_a5323b549840adcc2cb57cfe4f807173/node_modules/@nuxtjs/i18n/dist/types"
],
"#unhead/composables": [
"../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/head/runtime/composables"
],
"#mdc-configs": [
"./mdc-configs"
],
"#mdc-highlighter": [
"./mdc-highlighter"
],
"#mdc-imports": [
"./mdc-imports"
],
"#content/dump": [
"./content/database.compressed"
],
"#content/adapter": [
"./db0/connectors/better-sqlite3"
],
"#content/local-adapter": [
"./db0/connectors/better-sqlite3"
]
},
"lib": [
"esnext",
"webworker",
"dom.iterable"
]
},
"include": [
"./content/types.d.ts",
"./types/nitro-nuxt.d.ts",
"../node_modules/.pnpm/@nuxt+content@3.12.0_better-sqlite3@12.8.0_magicast@0.5.1/node_modules/runtime/server",
"../node_modules/.pnpm/@nuxt+content@3.12.0_better-sqlite3@12.8.0_magicast@0.5.1/node_modules/dist/runtime/server",
"../node_modules/.pnpm/@nuxtjs+tailwindcss@6.14.0_magicast@0.5.1_yaml@2.8.2/node_modules/runtime/server",
"../node_modules/.pnpm/@nuxtjs+tailwindcss@6.14.0_magicast@0.5.1_yaml@2.8.2/node_modules/dist/runtime/server",
"../node_modules/.pnpm/@nuxtjs+i18n@10.2.4_@emnapi+core@1.7.1_@emnapi+runtime@1.7.1_@vue+compiler-dom@3.5.26_d_a5323b549840adcc2cb57cfe4f807173/node_modules/runtime/server",
"../node_modules/.pnpm/@nuxtjs+i18n@10.2.4_@emnapi+core@1.7.1_@emnapi+runtime@1.7.1_@vue+compiler-dom@3.5.26_d_a5323b549840adcc2cb57cfe4f807173/node_modules/dist/runtime/server",
"../modules/runtime/server",
"../modules/dist/runtime/server",
"../node_modules/.pnpm/@nuxt+devtools@3.1.1_vite@7.3.0_@types+node@25.0.3_jiti@2.6.1_terser@5.44.1_yaml@2.8.2__vue@3.5.26_typescript@5.9.3_/node_modules/runtime/server",
"../node_modules/.pnpm/@nuxt+devtools@3.1.1_vite@7.3.0_@types+node@25.0.3_jiti@2.6.1_terser@5.44.1_yaml@2.8.2__vue@3.5.26_typescript@5.9.3_/node_modules/dist/runtime/server",
"../node_modules/.pnpm/@nuxt+telemetry@2.6.6_magicast@0.5.1/node_modules/runtime/server",
"../node_modules/.pnpm/@nuxt+telemetry@2.6.6_magicast@0.5.1/node_modules/dist/runtime/server",
"../node_modules/.pnpm/@nuxtjs+mdc@0.20.2_magicast@0.5.1/node_modules/runtime/server",
"../node_modules/.pnpm/@nuxtjs+mdc@0.20.2_magicast@0.5.1/node_modules/dist/runtime/server",
"../server/**/*",
"../shared/**/*.d.ts",
"./types/nitro.d.ts"
],
"exclude": [
"../node_modules",
"../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/node_modules",
"../node_modules/.pnpm/@nuxt+content@3.12.0_better-sqlite3@12.8.0_magicast@0.5.1/node_modules/@nuxt/content/node_modules",
"../node_modules/.pnpm/@nuxtjs+tailwindcss@6.14.0_magicast@0.5.1_yaml@2.8.2/node_modules/@nuxtjs/tailwindcss/node_modules",
"../node_modules/.pnpm/@nuxtjs+i18n@10.2.4_@emnapi+core@1.7.1_@emnapi+runtime@1.7.1_@vue+compiler-dom@3.5.26_d_a5323b549840adcc2cb57cfe4f807173/node_modules/@nuxtjs/i18n/node_modules",
"../node_modules/.pnpm/@nuxt+devtools@3.1.1_vite@7.3.0_@types+node@25.0.3_jiti@2.6.1_terser@5.44.1_yaml@2.8.2__vue@3.5.26_typescript@5.9.3_/node_modules/@nuxt/devtools/node_modules",
"../node_modules/.pnpm/@nuxt+telemetry@2.6.6_magicast@0.5.1/node_modules/@nuxt/telemetry/node_modules",
"../node_modules/.pnpm/@nuxtjs+mdc@0.20.2_magicast@0.5.1/node_modules/@nuxtjs/mdc/node_modules",
"../dist"
]
}

188
.nuxt/tsconfig.shared.json Normal file
View File

@@ -0,0 +1,188 @@
{
"compilerOptions": {
"paths": {
"nitropack/types": [
"../node_modules/.pnpm/nitropack@2.12.9_better-sqlite3@12.8.0/node_modules/nitropack/types"
],
"nitropack/runtime": [
"../node_modules/.pnpm/nitropack@2.12.9_better-sqlite3@12.8.0/node_modules/nitropack/runtime"
],
"nitropack": [
"../node_modules/.pnpm/nitropack@2.12.9_better-sqlite3@12.8.0/node_modules/nitropack"
],
"defu": [
"../node_modules/.pnpm/defu@6.1.4/node_modules/defu"
],
"h3": [
"../node_modules/.pnpm/h3@1.15.4/node_modules/h3"
],
"consola": [
"../node_modules/.pnpm/consola@3.4.2/node_modules/consola"
],
"ofetch": [
"../node_modules/.pnpm/ofetch@1.5.1/node_modules/ofetch"
],
"@unhead/vue": [
"../node_modules/.pnpm/@unhead+vue@2.1.1_vue@3.5.26_typescript@5.9.3_/node_modules/@unhead/vue"
],
"@nuxt/devtools": [
"../node_modules/.pnpm/@nuxt+devtools@3.1.1_vite@7.3.0_@types+node@25.0.3_jiti@2.6.1_terser@5.44.1_yaml@2.8.2__vue@3.5.26_typescript@5.9.3_/node_modules/@nuxt/devtools"
],
"@vue/runtime-core": [
"../node_modules/.pnpm/@vue+runtime-core@3.5.26/node_modules/@vue/runtime-core"
],
"@vue/compiler-sfc": [
"../node_modules/.pnpm/@vue+compiler-sfc@3.5.26/node_modules/@vue/compiler-sfc"
],
"unplugin-vue-router/client": [
"../node_modules/.pnpm/unplugin-vue-router@0.19.1_@vue+compiler-sfc@3.5.26_vue-router@4.6.4_vue@3.5.26_typescr_afeae63e4951cf0abe0304554948e4ca/node_modules/unplugin-vue-router/client"
],
"@nuxt/schema": [
"../node_modules/.pnpm/@nuxt+schema@4.2.2/node_modules/@nuxt/schema"
],
"nuxt": [
"../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt"
],
"vite/client": [
"../node_modules/.pnpm/vite@7.3.0_@types+node@25.0.3_jiti@2.6.1_terser@5.44.1_yaml@2.8.2/node_modules/vite/client"
],
"@nuxtjs/mdc": [
"../node_modules/.pnpm/@nuxtjs+mdc@0.20.2_magicast@0.5.1/node_modules/@nuxtjs/mdc"
],
"vue-i18n": [
"../node_modules/.pnpm/vue-i18n@11.3.0_vue@3.5.26_typescript@5.9.3_/node_modules/vue-i18n/dist/vue-i18n",
"../node_modules/.pnpm/vue-i18n@11.3.0_vue@3.5.26_typescript@5.9.3_/node_modules/vue-i18n"
],
"@intlify/shared": [
"../node_modules/.pnpm/@intlify+shared@11.3.0/node_modules/@intlify/shared/dist/shared",
"../node_modules/.pnpm/@intlify+shared@11.3.0/node_modules/@intlify/shared"
],
"@intlify/core": [
"../node_modules/.pnpm/@intlify+core@11.3.0/node_modules/@intlify/core/dist/core.node",
"../node_modules/.pnpm/@intlify+core@11.3.0/node_modules/@intlify/core"
],
"@intlify/core-base": [
"../node_modules/.pnpm/@intlify+core-base@11.3.0/node_modules/@intlify/core-base/dist/core-base",
"../node_modules/.pnpm/@intlify+core-base@11.3.0/node_modules/@intlify/core-base"
],
"@intlify/utils": [
"../node_modules/.pnpm/@intlify+utils@0.14.1/node_modules/@intlify/utils/dist/index",
"../node_modules/.pnpm/@intlify+utils@0.14.1/node_modules/@intlify/utils"
],
"@intlify/utils/h3": [
"../node_modules/.pnpm/@intlify+utils@0.14.1/node_modules/@intlify/utils/dist/h3",
"../node_modules/.pnpm/@intlify+utils@0.14.1/node_modules/@intlify/utils"
],
"@intlify/message-compiler": [
"../node_modules/.pnpm/@intlify+message-compiler@11.3.0/node_modules/@intlify/message-compiler/dist/message-compiler",
"../node_modules/.pnpm/@intlify+message-compiler@11.3.0/node_modules/@intlify/message-compiler"
],
"#content/components": [
"./content/components"
],
"#content/manifest": [
"./content/manifest"
],
"~": [
"../app"
],
"~/*": [
"../app/*"
],
"@": [
"../app"
],
"@/*": [
"../app/*"
],
"~~": [
".."
],
"~~/*": [
"../*"
],
"@@": [
".."
],
"@@/*": [
"../*"
],
"#shared": [
"../shared"
],
"#shared/*": [
"../shared/*"
],
"#app": [
"../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app"
],
"#app/*": [
"../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/*"
],
"vue-demi": [
"../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/compat/vue-demi"
],
"#i18n-kit": [
"../node_modules/.pnpm/@nuxtjs+i18n@10.2.4_@emnapi+core@1.7.1_@emnapi+runtime@1.7.1_@vue+compiler-dom@3.5.26_d_a5323b549840adcc2cb57cfe4f807173/node_modules/@nuxtjs/i18n/dist/runtime/kit"
],
"#i18n-kit/*": [
"../node_modules/.pnpm/@nuxtjs+i18n@10.2.4_@emnapi+core@1.7.1_@emnapi+runtime@1.7.1_@vue+compiler-dom@3.5.26_d_a5323b549840adcc2cb57cfe4f807173/node_modules/@nuxtjs/i18n/dist/runtime/kit/*"
],
"#internal-i18n-types": [
"../node_modules/.pnpm/@nuxtjs+i18n@10.2.4_@emnapi+core@1.7.1_@emnapi+runtime@1.7.1_@vue+compiler-dom@3.5.26_d_a5323b549840adcc2cb57cfe4f807173/node_modules/@nuxtjs/i18n/dist/types"
],
"#vue-router": [
"../node_modules/.pnpm/vue-router@4.6.4_vue@3.5.26_typescript@5.9.3_/node_modules/vue-router"
],
"#unhead/composables": [
"../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/head/runtime/composables"
],
"#imports": [
"./imports"
],
"#mdc-configs": [
"./mdc-configs"
],
"#mdc-highlighter": [
"./mdc-highlighter"
],
"#mdc-imports": [
"./mdc-imports"
],
"#build": [
"."
],
"#build/*": [
"./*"
]
},
"esModuleInterop": true,
"skipLibCheck": true,
"target": "ESNext",
"allowJs": true,
"resolveJsonModule": true,
"moduleDetection": "force",
"isolatedModules": true,
"verbatimModuleSyntax": true,
"strict": true,
"noUncheckedIndexedAccess": true,
"forceConsistentCasingInFileNames": true,
"noImplicitOverride": true,
"module": "preserve",
"noEmit": true,
"types": [],
"moduleResolution": "Bundler",
"useDefineForClassFields": true,
"noImplicitThis": true,
"allowSyntheticDefaultImports": true
},
"include": [
"./nuxt.shared.d.ts",
"../shared/**/*",
"../modules/*/shared/**/*",
"../layers/*/shared/**/*",
"../*.d.ts",
"../layers/*/*.d.ts"
],
"exclude": []
}

35
.nuxt/types/app.config.d.ts vendored Normal file
View File

@@ -0,0 +1,35 @@
import type { AppConfigInput, CustomAppConfig } from 'nuxt/schema'
import type { Defu } from 'defu'
declare global {
const defineAppConfig: <C extends AppConfigInput> (config: C) => C
}
declare const inlineConfig = {
"nuxt": {}
}
type ResolvedAppConfig = Defu<typeof inlineConfig, []>
type IsAny<T> = 0 extends 1 & T ? true : false
type MergedAppConfig<Resolved extends Record<string, unknown>, Custom extends Record<string, unknown>> = {
[K in keyof (Resolved & Custom)]: K extends keyof Custom
? unknown extends Custom[K]
? Resolved[K]
: IsAny<Custom[K]> extends true
? Resolved[K]
: Custom[K] extends Record<string, any>
? Resolved[K] extends Record<string, any>
? MergedAppConfig<Resolved[K], Custom[K]>
: Exclude<Custom[K], undefined>
: Exclude<Custom[K], undefined>
: Resolved[K]
}
declare module 'nuxt/schema' {
interface AppConfig extends MergedAppConfig<ResolvedAppConfig, CustomAppConfig> { }
}
declare module '@nuxt/schema' {
interface AppConfig extends MergedAppConfig<ResolvedAppConfig, CustomAppConfig> { }
}

26
.nuxt/types/build.d.ts vendored Normal file
View File

@@ -0,0 +1,26 @@
declare module "#build/app-component.mjs";
declare module "#build/nitro.client.mjs";
declare module "#build/plugins.client.mjs";
declare module "#build/css.mjs";
declare module "#build/fetch.mjs";
declare module "#build/error-component.mjs";
declare module "#build/global-polyfills.mjs";
declare module "#build/layouts.mjs";
declare module "#build/middleware.mjs";
declare module "#build/nuxt.config.mjs";
declare module "#build/paths.mjs";
declare module "#build/root-component.mjs";
declare module "#build/plugins.server.mjs";
declare module "#build/test-component-wrapper.mjs";
declare module "#build/i18n-options.mjs";
declare module "#build/devtools/settings.mjs";
declare module "#build/runtime.vue-devtools-client.4ZzBjUfIM0gmkcrAc1rO47_8oFLoIV_ocm_Yu0quyK4.js";
declare module "#build/routes.mjs";
declare module "#build/pages.mjs";
declare module "#build/router.options.mjs";
declare module "#build/unhead-options.mjs";
declare module "#build/unhead.config.mjs";
declare module "#build/components.plugin.mjs";
declare module "#build/component-names.mjs";
declare module "#build/components.islands.mjs";
declare module "#build/component-chunk.mjs";

1
.nuxt/types/builder-env.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
import "vite/client";

167
.nuxt/types/components.d.ts vendored Normal file
View File

@@ -0,0 +1,167 @@
import type { DefineComponent, SlotsType } from 'vue'
type IslandComponent<T> = DefineComponent<{}, {refresh: () => Promise<void>}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, SlotsType<{ fallback: { error: unknown } }>> & T
type HydrationStrategies = {
hydrateOnVisible?: IntersectionObserverInit | true
hydrateOnIdle?: number | true
hydrateOnInteraction?: keyof HTMLElementEventMap | Array<keyof HTMLElementEventMap> | true
hydrateOnMediaQuery?: string
hydrateAfter?: number
hydrateWhen?: boolean
hydrateNever?: true
}
type LazyComponent<T> = DefineComponent<HydrationStrategies, {}, {}, {}, {}, {}, {}, { hydrated: () => void }> & T
interface _GlobalComponents {
'LangSwitch': typeof import("../../app/components/LangSwitch.vue").default
'NotificationBell': typeof import("../../app/components/NotificationBell.vue").default
'QrCodeModal': typeof import("../../app/components/QrCodeModal.vue").default
'QrLogin': typeof import("../../app/components/QrLogin.vue").default
'SiteFooter': typeof import("../../app/components/SiteFooter.vue").default
'SiteHeader': typeof import("../../app/components/SiteHeader.vue").default
'AdminMarkdownEditor': typeof import("../../app/components/admin/MarkdownEditor.vue").default
'AdminMarkdownRenderer': typeof import("../../app/components/admin/MarkdownRenderer.vue").default
'ConsoleAppsCenter': typeof import("../../app/components/console/AppsCenter.vue").default
'ConsoleHeader': typeof import("../../app/components/console/ConsoleHeader.vue").default
'DeveloperAppDetail': typeof import("../../app/components/developer/AppDetail.vue").default
'DeveloperAppsCenter': typeof import("../../app/components/developer/AppsCenter.vue").default
'DeveloperPermissionGuard': typeof import("../../app/components/developer/PermissionGuard.vue").default
'DeveloperRoleTag': typeof import("../../app/components/developer/RoleTag.vue").default
'InviteBell': typeof import("../../app/components/invite/InviteBell.vue").default
'InviteNotification': typeof import("../../app/components/invite/InviteNotification.vue").default
'PaymentModal': typeof import("../../app/components/payment/PaymentModal.vue").default
'ProseA': typeof import("../../node_modules/.pnpm/@nuxtjs+mdc@0.20.2_magicast@0.5.1/node_modules/@nuxtjs/mdc/dist/runtime/components/prose/ProseA.vue").default
'ProseBlockquote': typeof import("../../node_modules/.pnpm/@nuxtjs+mdc@0.20.2_magicast@0.5.1/node_modules/@nuxtjs/mdc/dist/runtime/components/prose/ProseBlockquote.vue").default
'ProseCode': typeof import("../../node_modules/.pnpm/@nuxtjs+mdc@0.20.2_magicast@0.5.1/node_modules/@nuxtjs/mdc/dist/runtime/components/prose/ProseCode.vue").default
'ProseEm': typeof import("../../node_modules/.pnpm/@nuxtjs+mdc@0.20.2_magicast@0.5.1/node_modules/@nuxtjs/mdc/dist/runtime/components/prose/ProseEm.vue").default
'ProseH1': typeof import("../../node_modules/.pnpm/@nuxtjs+mdc@0.20.2_magicast@0.5.1/node_modules/@nuxtjs/mdc/dist/runtime/components/prose/ProseH1.vue").default
'ProseH2': typeof import("../../node_modules/.pnpm/@nuxtjs+mdc@0.20.2_magicast@0.5.1/node_modules/@nuxtjs/mdc/dist/runtime/components/prose/ProseH2.vue").default
'ProseH3': typeof import("../../node_modules/.pnpm/@nuxtjs+mdc@0.20.2_magicast@0.5.1/node_modules/@nuxtjs/mdc/dist/runtime/components/prose/ProseH3.vue").default
'ProseH4': typeof import("../../node_modules/.pnpm/@nuxtjs+mdc@0.20.2_magicast@0.5.1/node_modules/@nuxtjs/mdc/dist/runtime/components/prose/ProseH4.vue").default
'ProseH5': typeof import("../../node_modules/.pnpm/@nuxtjs+mdc@0.20.2_magicast@0.5.1/node_modules/@nuxtjs/mdc/dist/runtime/components/prose/ProseH5.vue").default
'ProseH6': typeof import("../../node_modules/.pnpm/@nuxtjs+mdc@0.20.2_magicast@0.5.1/node_modules/@nuxtjs/mdc/dist/runtime/components/prose/ProseH6.vue").default
'ProseHr': typeof import("../../node_modules/.pnpm/@nuxtjs+mdc@0.20.2_magicast@0.5.1/node_modules/@nuxtjs/mdc/dist/runtime/components/prose/ProseHr.vue").default
'ProseImg': typeof import("../../node_modules/.pnpm/@nuxtjs+mdc@0.20.2_magicast@0.5.1/node_modules/@nuxtjs/mdc/dist/runtime/components/prose/ProseImg.vue").default
'ProseLi': typeof import("../../node_modules/.pnpm/@nuxtjs+mdc@0.20.2_magicast@0.5.1/node_modules/@nuxtjs/mdc/dist/runtime/components/prose/ProseLi.vue").default
'ProseOl': typeof import("../../node_modules/.pnpm/@nuxtjs+mdc@0.20.2_magicast@0.5.1/node_modules/@nuxtjs/mdc/dist/runtime/components/prose/ProseOl.vue").default
'ProseP': typeof import("../../node_modules/.pnpm/@nuxtjs+mdc@0.20.2_magicast@0.5.1/node_modules/@nuxtjs/mdc/dist/runtime/components/prose/ProseP.vue").default
'ProsePre': typeof import("../../node_modules/.pnpm/@nuxtjs+mdc@0.20.2_magicast@0.5.1/node_modules/@nuxtjs/mdc/dist/runtime/components/prose/ProsePre.vue").default
'ProseScript': typeof import("../../node_modules/.pnpm/@nuxtjs+mdc@0.20.2_magicast@0.5.1/node_modules/@nuxtjs/mdc/dist/runtime/components/prose/ProseScript.vue").default
'ProseStrong': typeof import("../../node_modules/.pnpm/@nuxtjs+mdc@0.20.2_magicast@0.5.1/node_modules/@nuxtjs/mdc/dist/runtime/components/prose/ProseStrong.vue").default
'ProseTable': typeof import("../../node_modules/.pnpm/@nuxtjs+mdc@0.20.2_magicast@0.5.1/node_modules/@nuxtjs/mdc/dist/runtime/components/prose/ProseTable.vue").default
'ProseTbody': typeof import("../../node_modules/.pnpm/@nuxtjs+mdc@0.20.2_magicast@0.5.1/node_modules/@nuxtjs/mdc/dist/runtime/components/prose/ProseTbody.vue").default
'ProseTd': typeof import("../../node_modules/.pnpm/@nuxtjs+mdc@0.20.2_magicast@0.5.1/node_modules/@nuxtjs/mdc/dist/runtime/components/prose/ProseTd.vue").default
'ProseTh': typeof import("../../node_modules/.pnpm/@nuxtjs+mdc@0.20.2_magicast@0.5.1/node_modules/@nuxtjs/mdc/dist/runtime/components/prose/ProseTh.vue").default
'ProseThead': typeof import("../../node_modules/.pnpm/@nuxtjs+mdc@0.20.2_magicast@0.5.1/node_modules/@nuxtjs/mdc/dist/runtime/components/prose/ProseThead.vue").default
'ProseTr': typeof import("../../node_modules/.pnpm/@nuxtjs+mdc@0.20.2_magicast@0.5.1/node_modules/@nuxtjs/mdc/dist/runtime/components/prose/ProseTr.vue").default
'ProseUl': typeof import("../../node_modules/.pnpm/@nuxtjs+mdc@0.20.2_magicast@0.5.1/node_modules/@nuxtjs/mdc/dist/runtime/components/prose/ProseUl.vue").default
'NuxtWelcome': typeof import("../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/components/welcome.vue").default
'NuxtLayout': typeof import("../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/components/nuxt-layout").default
'NuxtErrorBoundary': typeof import("../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/components/nuxt-error-boundary.vue").default
'ClientOnly': typeof import("../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/components/client-only").default
'DevOnly': typeof import("../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/components/dev-only").default
'ServerPlaceholder': typeof import("../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/components/server-placeholder").default
'NuxtLink': typeof import("../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/components/nuxt-link").default
'NuxtLoadingIndicator': typeof import("../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/components/nuxt-loading-indicator").default
'NuxtTime': typeof import("../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/components/nuxt-time.vue").default
'NuxtRouteAnnouncer': typeof import("../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/components/nuxt-route-announcer").default
'NuxtImg': typeof import("../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/components/nuxt-stubs").NuxtImg
'NuxtPicture': typeof import("../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/components/nuxt-stubs").NuxtPicture
'ContentRenderer': typeof import("../../node_modules/.pnpm/@nuxt+content@3.12.0_better-sqlite3@12.8.0_magicast@0.5.1/node_modules/@nuxt/content/dist/runtime/components/ContentRenderer.vue").default
'NuxtLinkLocale': typeof import("../../node_modules/.pnpm/@nuxtjs+i18n@10.2.4_@emnapi+core@1.7.1_@emnapi+runtime@1.7.1_@vue+compiler-dom@3.5.26_d_a5323b549840adcc2cb57cfe4f807173/node_modules/@nuxtjs/i18n/dist/runtime/components/NuxtLinkLocale").default
'SwitchLocalePathLink': typeof import("../../node_modules/.pnpm/@nuxtjs+i18n@10.2.4_@emnapi+core@1.7.1_@emnapi+runtime@1.7.1_@vue+compiler-dom@3.5.26_d_a5323b549840adcc2cb57cfe4f807173/node_modules/@nuxtjs/i18n/dist/runtime/components/SwitchLocalePathLink").default
'NuxtPage': typeof import("../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/pages/runtime/page").default
'NoScript': typeof import("../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/head/runtime/components").NoScript
'Link': typeof import("../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/head/runtime/components").Link
'Base': typeof import("../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/head/runtime/components").Base
'Title': typeof import("../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/head/runtime/components").Title
'Meta': typeof import("../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/head/runtime/components").Meta
'Style': typeof import("../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/head/runtime/components").Style
'Head': typeof import("../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/head/runtime/components").Head
'Html': typeof import("../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/head/runtime/components").Html
'Body': typeof import("../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/head/runtime/components").Body
'MDC': typeof import("../../node_modules/.pnpm/@nuxtjs+mdc@0.20.2_magicast@0.5.1/node_modules/@nuxtjs/mdc/dist/runtime/components/MDC.vue").default
'MDCCached': typeof import("../../node_modules/.pnpm/@nuxtjs+mdc@0.20.2_magicast@0.5.1/node_modules/@nuxtjs/mdc/dist/runtime/components/MDCCached.vue").default
'MDCRenderer': typeof import("../../node_modules/.pnpm/@nuxtjs+mdc@0.20.2_magicast@0.5.1/node_modules/@nuxtjs/mdc/dist/runtime/components/MDCRenderer.vue").default
'MDCSlot': typeof import("../../node_modules/.pnpm/@nuxtjs+mdc@0.20.2_magicast@0.5.1/node_modules/@nuxtjs/mdc/dist/runtime/components/MDCSlot.vue").default
'NuxtIsland': typeof import("../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/components/nuxt-island").default
'LazyLangSwitch': LazyComponent<typeof import("../../app/components/LangSwitch.vue").default>
'LazyNotificationBell': LazyComponent<typeof import("../../app/components/NotificationBell.vue").default>
'LazyQrCodeModal': LazyComponent<typeof import("../../app/components/QrCodeModal.vue").default>
'LazyQrLogin': LazyComponent<typeof import("../../app/components/QrLogin.vue").default>
'LazySiteFooter': LazyComponent<typeof import("../../app/components/SiteFooter.vue").default>
'LazySiteHeader': LazyComponent<typeof import("../../app/components/SiteHeader.vue").default>
'LazyAdminMarkdownEditor': LazyComponent<typeof import("../../app/components/admin/MarkdownEditor.vue").default>
'LazyAdminMarkdownRenderer': LazyComponent<typeof import("../../app/components/admin/MarkdownRenderer.vue").default>
'LazyConsoleAppsCenter': LazyComponent<typeof import("../../app/components/console/AppsCenter.vue").default>
'LazyConsoleHeader': LazyComponent<typeof import("../../app/components/console/ConsoleHeader.vue").default>
'LazyDeveloperAppDetail': LazyComponent<typeof import("../../app/components/developer/AppDetail.vue").default>
'LazyDeveloperAppsCenter': LazyComponent<typeof import("../../app/components/developer/AppsCenter.vue").default>
'LazyDeveloperPermissionGuard': LazyComponent<typeof import("../../app/components/developer/PermissionGuard.vue").default>
'LazyDeveloperRoleTag': LazyComponent<typeof import("../../app/components/developer/RoleTag.vue").default>
'LazyInviteBell': LazyComponent<typeof import("../../app/components/invite/InviteBell.vue").default>
'LazyInviteNotification': LazyComponent<typeof import("../../app/components/invite/InviteNotification.vue").default>
'LazyPaymentModal': LazyComponent<typeof import("../../app/components/payment/PaymentModal.vue").default>
'LazyProseA': LazyComponent<typeof import("../../node_modules/.pnpm/@nuxtjs+mdc@0.20.2_magicast@0.5.1/node_modules/@nuxtjs/mdc/dist/runtime/components/prose/ProseA.vue").default>
'LazyProseBlockquote': LazyComponent<typeof import("../../node_modules/.pnpm/@nuxtjs+mdc@0.20.2_magicast@0.5.1/node_modules/@nuxtjs/mdc/dist/runtime/components/prose/ProseBlockquote.vue").default>
'LazyProseCode': LazyComponent<typeof import("../../node_modules/.pnpm/@nuxtjs+mdc@0.20.2_magicast@0.5.1/node_modules/@nuxtjs/mdc/dist/runtime/components/prose/ProseCode.vue").default>
'LazyProseEm': LazyComponent<typeof import("../../node_modules/.pnpm/@nuxtjs+mdc@0.20.2_magicast@0.5.1/node_modules/@nuxtjs/mdc/dist/runtime/components/prose/ProseEm.vue").default>
'LazyProseH1': LazyComponent<typeof import("../../node_modules/.pnpm/@nuxtjs+mdc@0.20.2_magicast@0.5.1/node_modules/@nuxtjs/mdc/dist/runtime/components/prose/ProseH1.vue").default>
'LazyProseH2': LazyComponent<typeof import("../../node_modules/.pnpm/@nuxtjs+mdc@0.20.2_magicast@0.5.1/node_modules/@nuxtjs/mdc/dist/runtime/components/prose/ProseH2.vue").default>
'LazyProseH3': LazyComponent<typeof import("../../node_modules/.pnpm/@nuxtjs+mdc@0.20.2_magicast@0.5.1/node_modules/@nuxtjs/mdc/dist/runtime/components/prose/ProseH3.vue").default>
'LazyProseH4': LazyComponent<typeof import("../../node_modules/.pnpm/@nuxtjs+mdc@0.20.2_magicast@0.5.1/node_modules/@nuxtjs/mdc/dist/runtime/components/prose/ProseH4.vue").default>
'LazyProseH5': LazyComponent<typeof import("../../node_modules/.pnpm/@nuxtjs+mdc@0.20.2_magicast@0.5.1/node_modules/@nuxtjs/mdc/dist/runtime/components/prose/ProseH5.vue").default>
'LazyProseH6': LazyComponent<typeof import("../../node_modules/.pnpm/@nuxtjs+mdc@0.20.2_magicast@0.5.1/node_modules/@nuxtjs/mdc/dist/runtime/components/prose/ProseH6.vue").default>
'LazyProseHr': LazyComponent<typeof import("../../node_modules/.pnpm/@nuxtjs+mdc@0.20.2_magicast@0.5.1/node_modules/@nuxtjs/mdc/dist/runtime/components/prose/ProseHr.vue").default>
'LazyProseImg': LazyComponent<typeof import("../../node_modules/.pnpm/@nuxtjs+mdc@0.20.2_magicast@0.5.1/node_modules/@nuxtjs/mdc/dist/runtime/components/prose/ProseImg.vue").default>
'LazyProseLi': LazyComponent<typeof import("../../node_modules/.pnpm/@nuxtjs+mdc@0.20.2_magicast@0.5.1/node_modules/@nuxtjs/mdc/dist/runtime/components/prose/ProseLi.vue").default>
'LazyProseOl': LazyComponent<typeof import("../../node_modules/.pnpm/@nuxtjs+mdc@0.20.2_magicast@0.5.1/node_modules/@nuxtjs/mdc/dist/runtime/components/prose/ProseOl.vue").default>
'LazyProseP': LazyComponent<typeof import("../../node_modules/.pnpm/@nuxtjs+mdc@0.20.2_magicast@0.5.1/node_modules/@nuxtjs/mdc/dist/runtime/components/prose/ProseP.vue").default>
'LazyProsePre': LazyComponent<typeof import("../../node_modules/.pnpm/@nuxtjs+mdc@0.20.2_magicast@0.5.1/node_modules/@nuxtjs/mdc/dist/runtime/components/prose/ProsePre.vue").default>
'LazyProseScript': LazyComponent<typeof import("../../node_modules/.pnpm/@nuxtjs+mdc@0.20.2_magicast@0.5.1/node_modules/@nuxtjs/mdc/dist/runtime/components/prose/ProseScript.vue").default>
'LazyProseStrong': LazyComponent<typeof import("../../node_modules/.pnpm/@nuxtjs+mdc@0.20.2_magicast@0.5.1/node_modules/@nuxtjs/mdc/dist/runtime/components/prose/ProseStrong.vue").default>
'LazyProseTable': LazyComponent<typeof import("../../node_modules/.pnpm/@nuxtjs+mdc@0.20.2_magicast@0.5.1/node_modules/@nuxtjs/mdc/dist/runtime/components/prose/ProseTable.vue").default>
'LazyProseTbody': LazyComponent<typeof import("../../node_modules/.pnpm/@nuxtjs+mdc@0.20.2_magicast@0.5.1/node_modules/@nuxtjs/mdc/dist/runtime/components/prose/ProseTbody.vue").default>
'LazyProseTd': LazyComponent<typeof import("../../node_modules/.pnpm/@nuxtjs+mdc@0.20.2_magicast@0.5.1/node_modules/@nuxtjs/mdc/dist/runtime/components/prose/ProseTd.vue").default>
'LazyProseTh': LazyComponent<typeof import("../../node_modules/.pnpm/@nuxtjs+mdc@0.20.2_magicast@0.5.1/node_modules/@nuxtjs/mdc/dist/runtime/components/prose/ProseTh.vue").default>
'LazyProseThead': LazyComponent<typeof import("../../node_modules/.pnpm/@nuxtjs+mdc@0.20.2_magicast@0.5.1/node_modules/@nuxtjs/mdc/dist/runtime/components/prose/ProseThead.vue").default>
'LazyProseTr': LazyComponent<typeof import("../../node_modules/.pnpm/@nuxtjs+mdc@0.20.2_magicast@0.5.1/node_modules/@nuxtjs/mdc/dist/runtime/components/prose/ProseTr.vue").default>
'LazyProseUl': LazyComponent<typeof import("../../node_modules/.pnpm/@nuxtjs+mdc@0.20.2_magicast@0.5.1/node_modules/@nuxtjs/mdc/dist/runtime/components/prose/ProseUl.vue").default>
'LazyNuxtWelcome': LazyComponent<typeof import("../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/components/welcome.vue").default>
'LazyNuxtLayout': LazyComponent<typeof import("../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/components/nuxt-layout").default>
'LazyNuxtErrorBoundary': LazyComponent<typeof import("../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/components/nuxt-error-boundary.vue").default>
'LazyClientOnly': LazyComponent<typeof import("../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/components/client-only").default>
'LazyDevOnly': LazyComponent<typeof import("../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/components/dev-only").default>
'LazyServerPlaceholder': LazyComponent<typeof import("../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/components/server-placeholder").default>
'LazyNuxtLink': LazyComponent<typeof import("../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/components/nuxt-link").default>
'LazyNuxtLoadingIndicator': LazyComponent<typeof import("../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/components/nuxt-loading-indicator").default>
'LazyNuxtTime': LazyComponent<typeof import("../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/components/nuxt-time.vue").default>
'LazyNuxtRouteAnnouncer': LazyComponent<typeof import("../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/components/nuxt-route-announcer").default>
'LazyNuxtImg': LazyComponent<typeof import("../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/components/nuxt-stubs").NuxtImg>
'LazyNuxtPicture': LazyComponent<typeof import("../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/components/nuxt-stubs").NuxtPicture>
'LazyContentRenderer': LazyComponent<typeof import("../../node_modules/.pnpm/@nuxt+content@3.12.0_better-sqlite3@12.8.0_magicast@0.5.1/node_modules/@nuxt/content/dist/runtime/components/ContentRenderer.vue").default>
'LazyNuxtLinkLocale': LazyComponent<typeof import("../../node_modules/.pnpm/@nuxtjs+i18n@10.2.4_@emnapi+core@1.7.1_@emnapi+runtime@1.7.1_@vue+compiler-dom@3.5.26_d_a5323b549840adcc2cb57cfe4f807173/node_modules/@nuxtjs/i18n/dist/runtime/components/NuxtLinkLocale").default>
'LazySwitchLocalePathLink': LazyComponent<typeof import("../../node_modules/.pnpm/@nuxtjs+i18n@10.2.4_@emnapi+core@1.7.1_@emnapi+runtime@1.7.1_@vue+compiler-dom@3.5.26_d_a5323b549840adcc2cb57cfe4f807173/node_modules/@nuxtjs/i18n/dist/runtime/components/SwitchLocalePathLink").default>
'LazyNuxtPage': LazyComponent<typeof import("../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/pages/runtime/page").default>
'LazyNoScript': LazyComponent<typeof import("../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/head/runtime/components").NoScript>
'LazyLink': LazyComponent<typeof import("../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/head/runtime/components").Link>
'LazyBase': LazyComponent<typeof import("../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/head/runtime/components").Base>
'LazyTitle': LazyComponent<typeof import("../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/head/runtime/components").Title>
'LazyMeta': LazyComponent<typeof import("../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/head/runtime/components").Meta>
'LazyStyle': LazyComponent<typeof import("../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/head/runtime/components").Style>
'LazyHead': LazyComponent<typeof import("../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/head/runtime/components").Head>
'LazyHtml': LazyComponent<typeof import("../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/head/runtime/components").Html>
'LazyBody': LazyComponent<typeof import("../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/head/runtime/components").Body>
'LazyMDC': LazyComponent<typeof import("../../node_modules/.pnpm/@nuxtjs+mdc@0.20.2_magicast@0.5.1/node_modules/@nuxtjs/mdc/dist/runtime/components/MDC.vue").default>
'LazyMDCCached': LazyComponent<typeof import("../../node_modules/.pnpm/@nuxtjs+mdc@0.20.2_magicast@0.5.1/node_modules/@nuxtjs/mdc/dist/runtime/components/MDCCached.vue").default>
'LazyMDCRenderer': LazyComponent<typeof import("../../node_modules/.pnpm/@nuxtjs+mdc@0.20.2_magicast@0.5.1/node_modules/@nuxtjs/mdc/dist/runtime/components/MDCRenderer.vue").default>
'LazyMDCSlot': LazyComponent<typeof import("../../node_modules/.pnpm/@nuxtjs+mdc@0.20.2_magicast@0.5.1/node_modules/@nuxtjs/mdc/dist/runtime/components/MDCSlot.vue").default>
'LazyNuxtIsland': LazyComponent<typeof import("../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/components/nuxt-island").default>
}
declare module 'vue' {
export interface GlobalComponents extends _GlobalComponents { }
}
export {}

115
.nuxt/types/i18n-plugin.d.ts vendored Normal file
View File

@@ -0,0 +1,115 @@
// Generated by @nuxtjs/i18n
import type { Composer } from 'vue-i18n'
import type { ComposerCustomProperties } from '../../node_modules/.pnpm/@nuxtjs+i18n@10.2.4_@emnapi+core@1.7.1_@emnapi+runtime@1.7.1_@vue+compiler-dom@3.5.26_d_a5323b549840adcc2cb57cfe4f807173/node_modules/@nuxtjs/i18n/dist/runtime/types.ts'
import type { Strategies, Directions, LocaleObject } from '../../node_modules/.pnpm/@nuxtjs+i18n@10.2.4_@emnapi+core@1.7.1_@emnapi+runtime@1.7.1_@vue+compiler-dom@3.5.26_d_a5323b549840adcc2cb57cfe4f807173/node_modules/@nuxtjs/i18n/dist/types.d.mts'
import type { I18nRoute } from '#i18n'
declare module 'vue-i18n' {
interface ComposerCustom extends ComposerCustomProperties<LocaleObject[]> {}
interface ExportedGlobalComposer extends ComposerCustomProperties<LocaleObject[]> {}
interface VueI18n extends ComposerCustomProperties<LocaleObject[]> {}
}
declare module '@intlify/core-base' {
// generated based on configured locales
interface GeneratedTypeConfig {
locale: "zh-CN" | "en"
legacy: false
}
}
interface I18nMeta {
i18n?: I18nRoute | false
}
declare module '#app' {
interface NuxtApp {
$i18n: Composer
}
interface PageMeta extends I18nMeta {}
}
declare module 'vue-router' {
interface RouteMeta extends I18nMeta {}
}
declare module 'vue-router' {
export type RouteMapI18n =
TypesConfig extends Record<'RouteNamedMapI18n', infer RouteNamedMap> ? RouteNamedMap : RouteMapGeneric
// Prefer named resolution for i18n
export type RouteLocationNamedI18n<Name extends keyof RouteMapI18n = keyof RouteMapI18n> =
| Name
| Omit<RouteLocationAsRelativeI18n, 'path'> & { path?: string }
/**
* Note: disabled route path string autocompletion, this can break depending on `strategy`
* this can be enabled again after route resolve has been improved.
*/
// | RouteLocationAsStringI18n
// | RouteLocationAsPathI18n
export type RouteLocationRawI18n<Name extends keyof RouteMapI18n = keyof RouteMapI18n> =
RouteMapGeneric extends RouteMapI18n
? RouteLocationAsStringI18n | RouteLocationAsRelativeGeneric | RouteLocationAsPathGeneric
:
| _LiteralUnion<RouteLocationAsStringTypedList<RouteMapI18n>[Name], string>
| RouteLocationAsRelativeTypedList<RouteMapI18n>[Name]
export type RouteLocationResolvedI18n<Name extends keyof RouteMapI18n = keyof RouteMapI18n> =
RouteMapGeneric extends RouteMapI18n
? RouteLocationResolvedGeneric
: RouteLocationResolvedTypedList<RouteMapI18n>[Name]
export interface RouteLocationNormalizedLoadedTypedI18n<
RouteMapI18n extends RouteMapGeneric = RouteMapGeneric,
Name extends keyof RouteMapI18n = keyof RouteMapI18n
> extends RouteLocationNormalizedLoadedGeneric {
name: Extract<Name, string | symbol>
params: RouteMapI18n[Name]['params']
}
export type RouteLocationNormalizedLoadedTypedListI18n<RouteMapOriginal extends RouteMapGeneric = RouteMapGeneric> = {
[N in keyof RouteMapOriginal]: RouteLocationNormalizedLoadedTypedI18n<RouteMapOriginal, N>
}
export type RouteLocationNormalizedLoadedI18n<Name extends keyof RouteMapI18n = keyof RouteMapI18n> =
RouteMapGeneric extends RouteMapI18n
? RouteLocationNormalizedLoadedGeneric
: RouteLocationNormalizedLoadedTypedListI18n<RouteMapI18n>[Name]
export type RouteLocationAsStringI18n<Name extends keyof RouteMapI18n = keyof RouteMapI18n> =
RouteMapGeneric extends RouteMapI18n
? string
: _LiteralUnion<RouteLocationAsStringTypedList<RouteMapI18n>[Name], string>
export type RouteLocationAsRelativeI18n<Name extends keyof RouteMapI18n = keyof RouteMapI18n> =
RouteMapGeneric extends RouteMapI18n
? RouteLocationAsRelativeGeneric
: RouteLocationAsRelativeTypedList<RouteMapI18n>[Name]
export type RouteLocationAsPathI18n<Name extends keyof RouteMapI18n = keyof RouteMapI18n> =
RouteMapGeneric extends RouteMapI18n ? RouteLocationAsPathGeneric : RouteLocationAsPathTypedList<RouteMapI18n>[Name]
/**
* Helper to generate a type safe version of the {@link RouteLocationAsRelative} type.
*/
export interface RouteLocationAsRelativeTypedI18n<
RouteMapI18n extends RouteMapGeneric = RouteMapGeneric,
Name extends keyof RouteMapI18n = keyof RouteMapI18n
> extends RouteLocationAsRelativeGeneric {
name?: Extract<Name, string | symbol>
params?: RouteMapI18n[Name]['paramsRaw']
}
}
declare global {
var $t: (Composer)['t']
var $rt: (Composer)['rt']
var $n: (Composer)['n']
var $d: (Composer)['d']
var $tm: (Composer)['tm']
var $te: (Composer)['te']
}
export {}

485
.nuxt/types/imports.d.ts vendored Normal file
View File

@@ -0,0 +1,485 @@
// Generated by auto imports
export {}
declare global {
const ROLE_COLOR: typeof import('../../app/composables/useAppPermission').ROLE_COLOR
const ROLE_HIERARCHY: typeof import('../../app/composables/useAppPermission').ROLE_HIERARCHY
const ROLE_LABEL: typeof import('../../app/composables/useAppPermission').ROLE_LABEL
const abortNavigation: typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/composables/router').abortNavigation
const addRouteMiddleware: typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/composables/router').addRouteMiddleware
const callOnce: typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/composables/once').callOnce
const canManageResource: typeof import('../../app/composables/useResourceAccess').canManageResource
const canViewBasic: typeof import('../../app/composables/useResourceAccess').canViewBasic
const canViewConnection: typeof import('../../app/composables/useResourceAccess').canViewConnection
const canViewSensitive: typeof import('../../app/composables/useResourceAccess').canViewSensitive
const cancelIdleCallback: typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/compat/idle-callback').cancelIdleCallback
const clearAuthz: typeof import('../../app/utils/permission').clearAuthz
const clearError: typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/composables/error').clearError
const clearNuxtData: typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/composables/asyncData').clearNuxtData
const clearNuxtState: typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/composables/state').clearNuxtState
const computed: typeof import('vue').computed
const createError: typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/composables/error').createError
const customRef: typeof import('vue').customRef
const defineAppConfig: typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/nuxt').defineAppConfig
const defineAsyncComponent: typeof import('vue').defineAsyncComponent
const defineComponent: typeof import('vue').defineComponent
const defineI18nConfig: typeof import('../../node_modules/.pnpm/@nuxtjs+i18n@10.2.4_@emnapi+core@1.7.1_@emnapi+runtime@1.7.1_@vue+compiler-dom@3.5.26_d_a5323b549840adcc2cb57cfe4f807173/node_modules/@nuxtjs/i18n/dist/runtime/composables/index').defineI18nConfig
const defineI18nLocale: typeof import('../../node_modules/.pnpm/@nuxtjs+i18n@10.2.4_@emnapi+core@1.7.1_@emnapi+runtime@1.7.1_@vue+compiler-dom@3.5.26_d_a5323b549840adcc2cb57cfe4f807173/node_modules/@nuxtjs/i18n/dist/runtime/composables/index').defineI18nLocale
const defineI18nRoute: typeof import('../../node_modules/.pnpm/@nuxtjs+i18n@10.2.4_@emnapi+core@1.7.1_@emnapi+runtime@1.7.1_@vue+compiler-dom@3.5.26_d_a5323b549840adcc2cb57cfe4f807173/node_modules/@nuxtjs/i18n/dist/runtime/composables/index').defineI18nRoute
const defineLazyHydrationComponent: typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/composables/lazy-hydration').defineLazyHydrationComponent
const defineNuxtComponent: typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/composables/component').defineNuxtComponent
const defineNuxtLink: typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/components/nuxt-link').defineNuxtLink
const defineNuxtPlugin: typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/nuxt').defineNuxtPlugin
const defineNuxtRouteMiddleware: typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/composables/router').defineNuxtRouteMiddleware
const definePageMeta: typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/pages/runtime/composables').definePageMeta
const definePayloadPlugin: typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/nuxt').definePayloadPlugin
const definePayloadReducer: typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/composables/payload').definePayloadReducer
const definePayloadReviver: typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/composables/payload').definePayloadReviver
const effect: typeof import('vue').effect
const effectScope: typeof import('vue').effectScope
const enrichResourcesWithPermission: typeof import('../../app/composables/useResourceAccess').enrichResourcesWithPermission
const executeEntry: typeof import('../../app/utils/appEntry').executeEntry
const generateQrCodeCanvas: typeof import('../../app/composables/useQRCode').generateQrCodeCanvas
const generateQrCodeDataUrl: typeof import('../../app/composables/useQRCode').generateQrCodeDataUrl
const getAppEntries: typeof import('../../app/utils/appEntry').getAppEntries
const getAppManifest: typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/composables/manifest').getAppManifest
const getCurrentInstance: typeof import('vue').getCurrentInstance
const getCurrentScope: typeof import('vue').getCurrentScope
const getPrimaryEntry: typeof import('../../app/utils/appEntry').getPrimaryEntry
const getResourceAccessLevel: typeof import('../../app/composables/useResourceAccess').getResourceAccessLevel
const getRouteRules: typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/composables/manifest').getRouteRules
const getScanTip: typeof import('../../app/utils/appEntry').getScanTip
const getTenantId: typeof import('../../app/utils/domain').getTenantId
const getToken: typeof import('../../app/utils/token-util').getToken
const h: typeof import('vue').h
const hasAnyPermission: typeof import('../../app/utils/permission').hasAnyPermission
const hasAnyRole: typeof import('../../app/utils/permission').hasAnyRole
const hasInjectionContext: typeof import('vue').hasInjectionContext
const hasPermission: typeof import('../../app/utils/permission').hasPermission
const hasRole: typeof import('../../app/utils/permission').hasRole
const inject: typeof import('vue').inject
const injectHead: typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/composables/head').injectHead
const isMaskedValue: typeof import('../../app/composables/useResourceAccess').isMaskedValue
const isNuxtError: typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/composables/error').isNuxtError
const isPrerendered: typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/composables/payload').isPrerendered
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 isResourceOwner: typeof import('../../app/composables/useResourceAccess').isResourceOwner
const isShallow: typeof import('vue').isShallow
const isVue2: typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/compat/vue-demi').isVue2
const isVue3: typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/compat/vue-demi').isVue3
const loadPayload: typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/composables/payload').loadPayload
const markRaw: typeof import('vue').markRaw
const navigateTo: typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/composables/router').navigateTo
const nextTick: typeof import('vue').nextTick
const notificationTypeMap: typeof import('../../app/composables/useNotificationCenter').notificationTypeMap
const onActivated: typeof import('vue').onActivated
const onBeforeMount: typeof import('vue').onBeforeMount
const onBeforeRouteLeave: typeof import('vue-router').onBeforeRouteLeave
const onBeforeRouteUpdate: typeof import('vue-router').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 onNuxtReady: typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/composables/ready').onNuxtReady
const onPrehydrate: typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/composables/ssr').onPrehydrate
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 onUnmounted: typeof import('vue').onUnmounted
const onUpdated: typeof import('vue').onUpdated
const onWatcherCleanup: typeof import('vue').onWatcherCleanup
const parseMarkdown: typeof import('../../node_modules/.pnpm/@nuxtjs+mdc@0.20.2_magicast@0.5.1/node_modules/@nuxtjs/mdc/dist/runtime/parser').parseMarkdown
const permission: typeof import('../../app/utils/permission').default
const prefetchComponents: typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/composables/preload').prefetchComponents
const preloadComponents: typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/composables/preload').preloadComponents
const preloadPayload: typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/composables/payload').preloadPayload
const preloadRouteComponents: typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/composables/preload').preloadRouteComponents
const prerenderRoutes: typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/composables/ssr').prerenderRoutes
const provide: typeof import('vue').provide
const proxyRefs: typeof import('vue').proxyRefs
const queryCollection: typeof import('../../node_modules/.pnpm/@nuxt+content@3.12.0_better-sqlite3@12.8.0_magicast@0.5.1/node_modules/@nuxt/content/dist/runtime/client').queryCollection
const queryCollectionItemSurroundings: typeof import('../../node_modules/.pnpm/@nuxt+content@3.12.0_better-sqlite3@12.8.0_magicast@0.5.1/node_modules/@nuxt/content/dist/runtime/client').queryCollectionItemSurroundings
const queryCollectionNavigation: typeof import('../../node_modules/.pnpm/@nuxt+content@3.12.0_better-sqlite3@12.8.0_magicast@0.5.1/node_modules/@nuxt/content/dist/runtime/client').queryCollectionNavigation
const queryCollectionSearchSections: typeof import('../../node_modules/.pnpm/@nuxt+content@3.12.0_better-sqlite3@12.8.0_magicast@0.5.1/node_modules/@nuxt/content/dist/runtime/client').queryCollectionSearchSections
const reactive: typeof import('vue').reactive
const readonly: typeof import('vue').readonly
const ref: typeof import('vue').ref
const refreshCookie: typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/composables/cookie').refreshCookie
const refreshNuxtData: typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/composables/asyncData').refreshNuxtData
const reloadNuxtApp: typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/composables/chunk').reloadNuxtApp
const removeToken: typeof import('../../app/utils/token-util').removeToken
const request: typeof import('../../app/utils/request').default
const requestIdleCallback: typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/compat/idle-callback').requestIdleCallback
const resolveComponent: typeof import('vue').resolveComponent
const resolveWebHomeUrl: typeof import('../../app/utils/appEntry').resolveWebHomeUrl
const setAuthz: typeof import('../../app/utils/permission').setAuthz
const setAuthzFromUser: typeof import('../../app/utils/permission').setAuthzFromUser
const setInterval: typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/compat/interval').setInterval
const setPageLayout: typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/composables/router').setPageLayout
const setResponseStatus: typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/composables/ssr').setResponseStatus
const setToken: typeof import('../../app/utils/token-util').setToken
const shallowReactive: typeof import('vue').shallowReactive
const shallowReadonly: typeof import('vue').shallowReadonly
const shallowRef: typeof import('vue').shallowRef
const showError: typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/composables/error').showError
const stringifyMarkdown: typeof import('../../node_modules/.pnpm/@nuxtjs+mdc@0.20.2_magicast@0.5.1/node_modules/@nuxtjs/mdc/dist/runtime/stringify').stringifyMarkdown
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 triggerRef: typeof import('vue').triggerRef
const tryUseNuxtApp: typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/nuxt').tryUseNuxtApp
const unref: typeof import('vue').unref
const unwrapSlot: typeof import('../../node_modules/.pnpm/@nuxtjs+mdc@0.20.2_magicast@0.5.1/node_modules/@nuxtjs/mdc/dist/runtime/utils/node').flatUnwrap
const updateAppConfig: typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/config').updateAppConfig
const useAppConfig: typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/config').useAppConfig
const useAppPermission: typeof import('../../app/composables/useAppPermission').useAppPermission
const useAsyncData: typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/composables/asyncData').useAsyncData
const useAttrs: typeof import('vue').useAttrs
const useBrowserLocale: typeof import('../../node_modules/.pnpm/@nuxtjs+i18n@10.2.4_@emnapi+core@1.7.1_@emnapi+runtime@1.7.1_@vue+compiler-dom@3.5.26_d_a5323b549840adcc2cb57cfe4f807173/node_modules/@nuxtjs/i18n/dist/runtime/composables/index').useBrowserLocale
const useCookie: typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/composables/cookie').useCookie
const useCookieLocale: typeof import('../../node_modules/.pnpm/@nuxtjs+i18n@10.2.4_@emnapi+core@1.7.1_@emnapi+runtime@1.7.1_@vue+compiler-dom@3.5.26_d_a5323b549840adcc2cb57cfe4f807173/node_modules/@nuxtjs/i18n/dist/runtime/composables/index').useCookieLocale
const useCssModule: typeof import('vue').useCssModule
const useCssVars: typeof import('vue').useCssVars
const useError: typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/composables/error').useError
const useFetch: typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/composables/fetch').useFetch
const useHead: typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/composables/head').useHead
const useHeadSafe: typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/composables/head').useHeadSafe
const useHydration: typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/composables/hydrate').useHydration
const useI18n: typeof import('../../node_modules/.pnpm/vue-i18n@11.3.0_vue@3.5.26_typescript@5.9.3_/node_modules/vue-i18n/dist/vue-i18n').useI18n
const useI18nPreloadKeys: typeof import('../../node_modules/.pnpm/@nuxtjs+i18n@10.2.4_@emnapi+core@1.7.1_@emnapi+runtime@1.7.1_@vue+compiler-dom@3.5.26_d_a5323b549840adcc2cb57cfe4f807173/node_modules/@nuxtjs/i18n/dist/runtime/composables/index').useI18nPreloadKeys
const useId: typeof import('vue').useId
const useLazyAsyncData: typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/composables/asyncData').useLazyAsyncData
const useLazyFetch: typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/composables/fetch').useLazyFetch
const useLink: typeof import('vue-router').useLink
const useLoadingIndicator: typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/composables/loading-indicator').useLoadingIndicator
const useLocaleHead: typeof import('../../node_modules/.pnpm/@nuxtjs+i18n@10.2.4_@emnapi+core@1.7.1_@emnapi+runtime@1.7.1_@vue+compiler-dom@3.5.26_d_a5323b549840adcc2cb57cfe4f807173/node_modules/@nuxtjs/i18n/dist/runtime/composables/index').useLocaleHead
const useLocalePath: typeof import('../../node_modules/.pnpm/@nuxtjs+i18n@10.2.4_@emnapi+core@1.7.1_@emnapi+runtime@1.7.1_@vue+compiler-dom@3.5.26_d_a5323b549840adcc2cb57cfe4f807173/node_modules/@nuxtjs/i18n/dist/runtime/composables/index').useLocalePath
const useLocaleRoute: typeof import('../../node_modules/.pnpm/@nuxtjs+i18n@10.2.4_@emnapi+core@1.7.1_@emnapi+runtime@1.7.1_@vue+compiler-dom@3.5.26_d_a5323b549840adcc2cb57cfe4f807173/node_modules/@nuxtjs/i18n/dist/runtime/composables/index').useLocaleRoute
const useModel: typeof import('vue').useModel
const useNotificationCenter: typeof import('../../app/composables/useNotificationCenter').useNotificationCenter
const useNuxtApp: typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/nuxt').useNuxtApp
const useNuxtData: typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/composables/asyncData').useNuxtData
const useNuxtDevTools: typeof import('../../node_modules/.pnpm/@nuxt+devtools@3.1.1_vite@7.3.0_@types+node@25.0.3_jiti@2.6.1_terser@5.44.1_yaml@2.8.2__vue@3.5.26_typescript@5.9.3_/node_modules/@nuxt/devtools/dist/runtime/use-nuxt-devtools').useNuxtDevTools
const usePageSeo: typeof import('../../app/composables/usePageSeo').usePageSeo
const usePreviewMode: typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/composables/preview').usePreviewMode
const useRequestEvent: typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/composables/ssr').useRequestEvent
const useRequestFetch: typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/composables/ssr').useRequestFetch
const useRequestHeader: typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/composables/ssr').useRequestHeader
const useRequestHeaders: typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/composables/ssr').useRequestHeaders
const useRequestURL: typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/composables/url').useRequestURL
const useResourceAccess: typeof import('../../app/composables/useResourceAccess').useResourceAccess
const useResponseHeader: typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/composables/ssr').useResponseHeader
const useRoute: typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/composables/router').useRoute
const useRouteAnnouncer: typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/composables/route-announcer').useRouteAnnouncer
const useRouteBaseName: typeof import('../../node_modules/.pnpm/@nuxtjs+i18n@10.2.4_@emnapi+core@1.7.1_@emnapi+runtime@1.7.1_@vue+compiler-dom@3.5.26_d_a5323b549840adcc2cb57cfe4f807173/node_modules/@nuxtjs/i18n/dist/runtime/composables/index').useRouteBaseName
const useRouter: typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/composables/router').useRouter
const useRuntimeConfig: typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/nuxt').useRuntimeConfig
const useRuntimeHook: typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/composables/runtime-hook').useRuntimeHook
const useScript: typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/composables/script-stubs').useScript
const useScriptClarity: typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/composables/script-stubs').useScriptClarity
const useScriptCloudflareWebAnalytics: typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/composables/script-stubs').useScriptCloudflareWebAnalytics
const useScriptCrisp: typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/composables/script-stubs').useScriptCrisp
const useScriptDatabuddyAnalytics: typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/composables/script-stubs').useScriptDatabuddyAnalytics
const useScriptEventPage: typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/composables/script-stubs').useScriptEventPage
const useScriptFathomAnalytics: typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/composables/script-stubs').useScriptFathomAnalytics
const useScriptGoogleAdsense: typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/composables/script-stubs').useScriptGoogleAdsense
const useScriptGoogleAnalytics: typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/composables/script-stubs').useScriptGoogleAnalytics
const useScriptGoogleMaps: typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/composables/script-stubs').useScriptGoogleMaps
const useScriptGoogleTagManager: typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/composables/script-stubs').useScriptGoogleTagManager
const useScriptHotjar: typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/composables/script-stubs').useScriptHotjar
const useScriptIntercom: typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/composables/script-stubs').useScriptIntercom
const useScriptLemonSqueezy: typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/composables/script-stubs').useScriptLemonSqueezy
const useScriptMatomoAnalytics: typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/composables/script-stubs').useScriptMatomoAnalytics
const useScriptMetaPixel: typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/composables/script-stubs').useScriptMetaPixel
const useScriptNpm: typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/composables/script-stubs').useScriptNpm
const useScriptPayPal: typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/composables/script-stubs').useScriptPayPal
const useScriptPlausibleAnalytics: typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/composables/script-stubs').useScriptPlausibleAnalytics
const useScriptRedditPixel: typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/composables/script-stubs').useScriptRedditPixel
const useScriptRybbitAnalytics: typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/composables/script-stubs').useScriptRybbitAnalytics
const useScriptSegment: typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/composables/script-stubs').useScriptSegment
const useScriptSnapchatPixel: typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/composables/script-stubs').useScriptSnapchatPixel
const useScriptStripe: typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/composables/script-stubs').useScriptStripe
const useScriptTriggerConsent: typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/composables/script-stubs').useScriptTriggerConsent
const useScriptTriggerElement: typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/composables/script-stubs').useScriptTriggerElement
const useScriptUmamiAnalytics: typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/composables/script-stubs').useScriptUmamiAnalytics
const useScriptVimeoPlayer: typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/composables/script-stubs').useScriptVimeoPlayer
const useScriptXPixel: typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/composables/script-stubs').useScriptXPixel
const useScriptYouTubePlayer: typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/composables/script-stubs').useScriptYouTubePlayer
const useSeoMeta: typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/composables/head').useSeoMeta
const useServerHead: typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/composables/head').useServerHead
const useServerHeadSafe: typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/composables/head').useServerHeadSafe
const useServerSeoMeta: typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/composables/head').useServerSeoMeta
const useSetI18nParams: typeof import('../../node_modules/.pnpm/@nuxtjs+i18n@10.2.4_@emnapi+core@1.7.1_@emnapi+runtime@1.7.1_@vue+compiler-dom@3.5.26_d_a5323b549840adcc2cb57cfe4f807173/node_modules/@nuxtjs/i18n/dist/runtime/composables/index').useSetI18nParams
const useShadowRoot: typeof import('vue').useShadowRoot
const useSlots: typeof import('vue').useSlots
const useState: typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/composables/state').useState
const useSwitchLocalePath: typeof import('../../node_modules/.pnpm/@nuxtjs+i18n@10.2.4_@emnapi+core@1.7.1_@emnapi+runtime@1.7.1_@vue+compiler-dom@3.5.26_d_a5323b549840adcc2cb57cfe4f807173/node_modules/@nuxtjs/i18n/dist/runtime/composables/index').useSwitchLocalePath
const useTemplateRef: typeof import('vue').useTemplateRef
const useTransitionState: typeof import('vue').useTransitionState
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 withCtx: typeof import('vue').withCtx
const withDirectives: typeof import('vue').withDirectives
const withKeys: typeof import('vue').withKeys
const withMemo: typeof import('vue').withMemo
const withModifiers: typeof import('vue').withModifiers
const withScopeId: typeof import('vue').withScopeId
}
// for type re-export
declare global {
// @ts-ignore
export type { Component, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue'
import('vue')
// @ts-ignore
export type { AppPermission } from '../../app/composables/useAppPermission'
import('../../app/composables/useAppPermission')
// @ts-ignore
export type { ResourceAccessLevel } from '../../app/composables/useResourceAccess'
import('../../app/composables/useResourceAccess')
// @ts-ignore
export type { EntryType, AppEntry } from '../../app/utils/appEntry'
import('../../app/utils/appEntry')
}
// for vue template auto import
import { UnwrapRef } from 'vue'
declare module 'vue' {
interface ComponentCustomProperties {
readonly ROLE_COLOR: UnwrapRef<typeof import('../../app/composables/useAppPermission')['ROLE_COLOR']>
readonly ROLE_HIERARCHY: UnwrapRef<typeof import('../../app/composables/useAppPermission')['ROLE_HIERARCHY']>
readonly ROLE_LABEL: UnwrapRef<typeof import('../../app/composables/useAppPermission')['ROLE_LABEL']>
readonly abortNavigation: UnwrapRef<typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/composables/router')['abortNavigation']>
readonly addRouteMiddleware: UnwrapRef<typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/composables/router')['addRouteMiddleware']>
readonly callOnce: UnwrapRef<typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/composables/once')['callOnce']>
readonly canManageResource: UnwrapRef<typeof import('../../app/composables/useResourceAccess')['canManageResource']>
readonly canViewBasic: UnwrapRef<typeof import('../../app/composables/useResourceAccess')['canViewBasic']>
readonly canViewConnection: UnwrapRef<typeof import('../../app/composables/useResourceAccess')['canViewConnection']>
readonly canViewSensitive: UnwrapRef<typeof import('../../app/composables/useResourceAccess')['canViewSensitive']>
readonly cancelIdleCallback: UnwrapRef<typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/compat/idle-callback')['cancelIdleCallback']>
readonly clearAuthz: UnwrapRef<typeof import('../../app/utils/permission')['clearAuthz']>
readonly clearError: UnwrapRef<typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/composables/error')['clearError']>
readonly clearNuxtData: UnwrapRef<typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/composables/asyncData')['clearNuxtData']>
readonly clearNuxtState: UnwrapRef<typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/composables/state')['clearNuxtState']>
readonly computed: UnwrapRef<typeof import('vue')['computed']>
readonly createError: UnwrapRef<typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/composables/error')['createError']>
readonly customRef: UnwrapRef<typeof import('vue')['customRef']>
readonly defineAppConfig: UnwrapRef<typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/nuxt')['defineAppConfig']>
readonly defineAsyncComponent: UnwrapRef<typeof import('vue')['defineAsyncComponent']>
readonly defineComponent: UnwrapRef<typeof import('vue')['defineComponent']>
readonly defineI18nConfig: UnwrapRef<typeof import('../../node_modules/.pnpm/@nuxtjs+i18n@10.2.4_@emnapi+core@1.7.1_@emnapi+runtime@1.7.1_@vue+compiler-dom@3.5.26_d_a5323b549840adcc2cb57cfe4f807173/node_modules/@nuxtjs/i18n/dist/runtime/composables/index')['defineI18nConfig']>
readonly defineI18nLocale: UnwrapRef<typeof import('../../node_modules/.pnpm/@nuxtjs+i18n@10.2.4_@emnapi+core@1.7.1_@emnapi+runtime@1.7.1_@vue+compiler-dom@3.5.26_d_a5323b549840adcc2cb57cfe4f807173/node_modules/@nuxtjs/i18n/dist/runtime/composables/index')['defineI18nLocale']>
readonly defineI18nRoute: UnwrapRef<typeof import('../../node_modules/.pnpm/@nuxtjs+i18n@10.2.4_@emnapi+core@1.7.1_@emnapi+runtime@1.7.1_@vue+compiler-dom@3.5.26_d_a5323b549840adcc2cb57cfe4f807173/node_modules/@nuxtjs/i18n/dist/runtime/composables/index')['defineI18nRoute']>
readonly defineLazyHydrationComponent: UnwrapRef<typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/composables/lazy-hydration')['defineLazyHydrationComponent']>
readonly defineNuxtComponent: UnwrapRef<typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/composables/component')['defineNuxtComponent']>
readonly defineNuxtLink: UnwrapRef<typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/components/nuxt-link')['defineNuxtLink']>
readonly defineNuxtPlugin: UnwrapRef<typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/nuxt')['defineNuxtPlugin']>
readonly defineNuxtRouteMiddleware: UnwrapRef<typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/composables/router')['defineNuxtRouteMiddleware']>
readonly definePageMeta: UnwrapRef<typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/pages/runtime/composables')['definePageMeta']>
readonly definePayloadPlugin: UnwrapRef<typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/nuxt')['definePayloadPlugin']>
readonly definePayloadReducer: UnwrapRef<typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/composables/payload')['definePayloadReducer']>
readonly definePayloadReviver: UnwrapRef<typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/composables/payload')['definePayloadReviver']>
readonly effect: UnwrapRef<typeof import('vue')['effect']>
readonly effectScope: UnwrapRef<typeof import('vue')['effectScope']>
readonly enrichResourcesWithPermission: UnwrapRef<typeof import('../../app/composables/useResourceAccess')['enrichResourcesWithPermission']>
readonly executeEntry: UnwrapRef<typeof import('../../app/utils/appEntry')['executeEntry']>
readonly generateQrCodeCanvas: UnwrapRef<typeof import('../../app/composables/useQRCode')['generateQrCodeCanvas']>
readonly generateQrCodeDataUrl: UnwrapRef<typeof import('../../app/composables/useQRCode')['generateQrCodeDataUrl']>
readonly getAppEntries: UnwrapRef<typeof import('../../app/utils/appEntry')['getAppEntries']>
readonly getAppManifest: UnwrapRef<typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/composables/manifest')['getAppManifest']>
readonly getCurrentInstance: UnwrapRef<typeof import('vue')['getCurrentInstance']>
readonly getCurrentScope: UnwrapRef<typeof import('vue')['getCurrentScope']>
readonly getPrimaryEntry: UnwrapRef<typeof import('../../app/utils/appEntry')['getPrimaryEntry']>
readonly getResourceAccessLevel: UnwrapRef<typeof import('../../app/composables/useResourceAccess')['getResourceAccessLevel']>
readonly getRouteRules: UnwrapRef<typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/composables/manifest')['getRouteRules']>
readonly getScanTip: UnwrapRef<typeof import('../../app/utils/appEntry')['getScanTip']>
readonly getTenantId: UnwrapRef<typeof import('../../app/utils/domain')['getTenantId']>
readonly getToken: UnwrapRef<typeof import('../../app/utils/token-util')['getToken']>
readonly h: UnwrapRef<typeof import('vue')['h']>
readonly hasAnyPermission: UnwrapRef<typeof import('../../app/utils/permission')['hasAnyPermission']>
readonly hasAnyRole: UnwrapRef<typeof import('../../app/utils/permission')['hasAnyRole']>
readonly hasInjectionContext: UnwrapRef<typeof import('vue')['hasInjectionContext']>
readonly hasPermission: UnwrapRef<typeof import('../../app/utils/permission')['hasPermission']>
readonly hasRole: UnwrapRef<typeof import('../../app/utils/permission')['hasRole']>
readonly inject: UnwrapRef<typeof import('vue')['inject']>
readonly injectHead: UnwrapRef<typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/composables/head')['injectHead']>
readonly isMaskedValue: UnwrapRef<typeof import('../../app/composables/useResourceAccess')['isMaskedValue']>
readonly isNuxtError: UnwrapRef<typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/composables/error')['isNuxtError']>
readonly isPrerendered: UnwrapRef<typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/composables/payload')['isPrerendered']>
readonly isProxy: UnwrapRef<typeof import('vue')['isProxy']>
readonly isReactive: UnwrapRef<typeof import('vue')['isReactive']>
readonly isReadonly: UnwrapRef<typeof import('vue')['isReadonly']>
readonly isRef: UnwrapRef<typeof import('vue')['isRef']>
readonly isResourceOwner: UnwrapRef<typeof import('../../app/composables/useResourceAccess')['isResourceOwner']>
readonly isShallow: UnwrapRef<typeof import('vue')['isShallow']>
readonly isVue2: UnwrapRef<typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/compat/vue-demi')['isVue2']>
readonly isVue3: UnwrapRef<typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/compat/vue-demi')['isVue3']>
readonly loadPayload: UnwrapRef<typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/composables/payload')['loadPayload']>
readonly markRaw: UnwrapRef<typeof import('vue')['markRaw']>
readonly navigateTo: UnwrapRef<typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/composables/router')['navigateTo']>
readonly nextTick: UnwrapRef<typeof import('vue')['nextTick']>
readonly notificationTypeMap: UnwrapRef<typeof import('../../app/composables/useNotificationCenter')['notificationTypeMap']>
readonly onActivated: UnwrapRef<typeof import('vue')['onActivated']>
readonly onBeforeMount: UnwrapRef<typeof import('vue')['onBeforeMount']>
readonly onBeforeRouteLeave: UnwrapRef<typeof import('vue-router')['onBeforeRouteLeave']>
readonly onBeforeRouteUpdate: UnwrapRef<typeof import('vue-router')['onBeforeRouteUpdate']>
readonly onBeforeUnmount: UnwrapRef<typeof import('vue')['onBeforeUnmount']>
readonly onBeforeUpdate: UnwrapRef<typeof import('vue')['onBeforeUpdate']>
readonly onDeactivated: UnwrapRef<typeof import('vue')['onDeactivated']>
readonly onErrorCaptured: UnwrapRef<typeof import('vue')['onErrorCaptured']>
readonly onMounted: UnwrapRef<typeof import('vue')['onMounted']>
readonly onNuxtReady: UnwrapRef<typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/composables/ready')['onNuxtReady']>
readonly onPrehydrate: UnwrapRef<typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/composables/ssr')['onPrehydrate']>
readonly onRenderTracked: UnwrapRef<typeof import('vue')['onRenderTracked']>
readonly onRenderTriggered: UnwrapRef<typeof import('vue')['onRenderTriggered']>
readonly onScopeDispose: UnwrapRef<typeof import('vue')['onScopeDispose']>
readonly onServerPrefetch: UnwrapRef<typeof import('vue')['onServerPrefetch']>
readonly onUnmounted: UnwrapRef<typeof import('vue')['onUnmounted']>
readonly onUpdated: UnwrapRef<typeof import('vue')['onUpdated']>
readonly onWatcherCleanup: UnwrapRef<typeof import('vue')['onWatcherCleanup']>
readonly parseMarkdown: UnwrapRef<typeof import('../../node_modules/.pnpm/@nuxtjs+mdc@0.20.2_magicast@0.5.1/node_modules/@nuxtjs/mdc/dist/runtime/parser')['parseMarkdown']>
readonly permission: UnwrapRef<typeof import('../../app/utils/permission')['default']>
readonly prefetchComponents: UnwrapRef<typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/composables/preload')['prefetchComponents']>
readonly preloadComponents: UnwrapRef<typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/composables/preload')['preloadComponents']>
readonly preloadPayload: UnwrapRef<typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/composables/payload')['preloadPayload']>
readonly preloadRouteComponents: UnwrapRef<typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/composables/preload')['preloadRouteComponents']>
readonly prerenderRoutes: UnwrapRef<typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/composables/ssr')['prerenderRoutes']>
readonly provide: UnwrapRef<typeof import('vue')['provide']>
readonly proxyRefs: UnwrapRef<typeof import('vue')['proxyRefs']>
readonly queryCollection: UnwrapRef<typeof import('../../node_modules/.pnpm/@nuxt+content@3.12.0_better-sqlite3@12.8.0_magicast@0.5.1/node_modules/@nuxt/content/dist/runtime/client')['queryCollection']>
readonly queryCollectionItemSurroundings: UnwrapRef<typeof import('../../node_modules/.pnpm/@nuxt+content@3.12.0_better-sqlite3@12.8.0_magicast@0.5.1/node_modules/@nuxt/content/dist/runtime/client')['queryCollectionItemSurroundings']>
readonly queryCollectionNavigation: UnwrapRef<typeof import('../../node_modules/.pnpm/@nuxt+content@3.12.0_better-sqlite3@12.8.0_magicast@0.5.1/node_modules/@nuxt/content/dist/runtime/client')['queryCollectionNavigation']>
readonly queryCollectionSearchSections: UnwrapRef<typeof import('../../node_modules/.pnpm/@nuxt+content@3.12.0_better-sqlite3@12.8.0_magicast@0.5.1/node_modules/@nuxt/content/dist/runtime/client')['queryCollectionSearchSections']>
readonly reactive: UnwrapRef<typeof import('vue')['reactive']>
readonly readonly: UnwrapRef<typeof import('vue')['readonly']>
readonly ref: UnwrapRef<typeof import('vue')['ref']>
readonly refreshCookie: UnwrapRef<typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/composables/cookie')['refreshCookie']>
readonly refreshNuxtData: UnwrapRef<typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/composables/asyncData')['refreshNuxtData']>
readonly reloadNuxtApp: UnwrapRef<typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/composables/chunk')['reloadNuxtApp']>
readonly removeToken: UnwrapRef<typeof import('../../app/utils/token-util')['removeToken']>
readonly request: UnwrapRef<typeof import('../../app/utils/request')['default']>
readonly requestIdleCallback: UnwrapRef<typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/compat/idle-callback')['requestIdleCallback']>
readonly resolveComponent: UnwrapRef<typeof import('vue')['resolveComponent']>
readonly resolveWebHomeUrl: UnwrapRef<typeof import('../../app/utils/appEntry')['resolveWebHomeUrl']>
readonly setAuthz: UnwrapRef<typeof import('../../app/utils/permission')['setAuthz']>
readonly setAuthzFromUser: UnwrapRef<typeof import('../../app/utils/permission')['setAuthzFromUser']>
readonly setInterval: UnwrapRef<typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/compat/interval')['setInterval']>
readonly setPageLayout: UnwrapRef<typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/composables/router')['setPageLayout']>
readonly setResponseStatus: UnwrapRef<typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/composables/ssr')['setResponseStatus']>
readonly setToken: UnwrapRef<typeof import('../../app/utils/token-util')['setToken']>
readonly shallowReactive: UnwrapRef<typeof import('vue')['shallowReactive']>
readonly shallowReadonly: UnwrapRef<typeof import('vue')['shallowReadonly']>
readonly shallowRef: UnwrapRef<typeof import('vue')['shallowRef']>
readonly showError: UnwrapRef<typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/composables/error')['showError']>
readonly stringifyMarkdown: UnwrapRef<typeof import('../../node_modules/.pnpm/@nuxtjs+mdc@0.20.2_magicast@0.5.1/node_modules/@nuxtjs/mdc/dist/runtime/stringify')['stringifyMarkdown']>
readonly toRaw: UnwrapRef<typeof import('vue')['toRaw']>
readonly toRef: UnwrapRef<typeof import('vue')['toRef']>
readonly toRefs: UnwrapRef<typeof import('vue')['toRefs']>
readonly toValue: UnwrapRef<typeof import('vue')['toValue']>
readonly triggerRef: UnwrapRef<typeof import('vue')['triggerRef']>
readonly tryUseNuxtApp: UnwrapRef<typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/nuxt')['tryUseNuxtApp']>
readonly unref: UnwrapRef<typeof import('vue')['unref']>
readonly unwrapSlot: UnwrapRef<typeof import('../../node_modules/.pnpm/@nuxtjs+mdc@0.20.2_magicast@0.5.1/node_modules/@nuxtjs/mdc/dist/runtime/utils/node')['flatUnwrap']>
readonly updateAppConfig: UnwrapRef<typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/config')['updateAppConfig']>
readonly useAppConfig: UnwrapRef<typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/config')['useAppConfig']>
readonly useAppPermission: UnwrapRef<typeof import('../../app/composables/useAppPermission')['useAppPermission']>
readonly useAsyncData: UnwrapRef<typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/composables/asyncData')['useAsyncData']>
readonly useAttrs: UnwrapRef<typeof import('vue')['useAttrs']>
readonly useBrowserLocale: UnwrapRef<typeof import('../../node_modules/.pnpm/@nuxtjs+i18n@10.2.4_@emnapi+core@1.7.1_@emnapi+runtime@1.7.1_@vue+compiler-dom@3.5.26_d_a5323b549840adcc2cb57cfe4f807173/node_modules/@nuxtjs/i18n/dist/runtime/composables/index')['useBrowserLocale']>
readonly useCookie: UnwrapRef<typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/composables/cookie')['useCookie']>
readonly useCookieLocale: UnwrapRef<typeof import('../../node_modules/.pnpm/@nuxtjs+i18n@10.2.4_@emnapi+core@1.7.1_@emnapi+runtime@1.7.1_@vue+compiler-dom@3.5.26_d_a5323b549840adcc2cb57cfe4f807173/node_modules/@nuxtjs/i18n/dist/runtime/composables/index')['useCookieLocale']>
readonly useCssModule: UnwrapRef<typeof import('vue')['useCssModule']>
readonly useCssVars: UnwrapRef<typeof import('vue')['useCssVars']>
readonly useError: UnwrapRef<typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/composables/error')['useError']>
readonly useFetch: UnwrapRef<typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/composables/fetch')['useFetch']>
readonly useHead: UnwrapRef<typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/composables/head')['useHead']>
readonly useHeadSafe: UnwrapRef<typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/composables/head')['useHeadSafe']>
readonly useHydration: UnwrapRef<typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/composables/hydrate')['useHydration']>
readonly useI18n: UnwrapRef<typeof import('../../node_modules/.pnpm/vue-i18n@11.3.0_vue@3.5.26_typescript@5.9.3_/node_modules/vue-i18n/dist/vue-i18n')['useI18n']>
readonly useI18nPreloadKeys: UnwrapRef<typeof import('../../node_modules/.pnpm/@nuxtjs+i18n@10.2.4_@emnapi+core@1.7.1_@emnapi+runtime@1.7.1_@vue+compiler-dom@3.5.26_d_a5323b549840adcc2cb57cfe4f807173/node_modules/@nuxtjs/i18n/dist/runtime/composables/index')['useI18nPreloadKeys']>
readonly useId: UnwrapRef<typeof import('vue')['useId']>
readonly useLazyAsyncData: UnwrapRef<typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/composables/asyncData')['useLazyAsyncData']>
readonly useLazyFetch: UnwrapRef<typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/composables/fetch')['useLazyFetch']>
readonly useLink: UnwrapRef<typeof import('vue-router')['useLink']>
readonly useLoadingIndicator: UnwrapRef<typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/composables/loading-indicator')['useLoadingIndicator']>
readonly useLocaleHead: UnwrapRef<typeof import('../../node_modules/.pnpm/@nuxtjs+i18n@10.2.4_@emnapi+core@1.7.1_@emnapi+runtime@1.7.1_@vue+compiler-dom@3.5.26_d_a5323b549840adcc2cb57cfe4f807173/node_modules/@nuxtjs/i18n/dist/runtime/composables/index')['useLocaleHead']>
readonly useLocalePath: UnwrapRef<typeof import('../../node_modules/.pnpm/@nuxtjs+i18n@10.2.4_@emnapi+core@1.7.1_@emnapi+runtime@1.7.1_@vue+compiler-dom@3.5.26_d_a5323b549840adcc2cb57cfe4f807173/node_modules/@nuxtjs/i18n/dist/runtime/composables/index')['useLocalePath']>
readonly useLocaleRoute: UnwrapRef<typeof import('../../node_modules/.pnpm/@nuxtjs+i18n@10.2.4_@emnapi+core@1.7.1_@emnapi+runtime@1.7.1_@vue+compiler-dom@3.5.26_d_a5323b549840adcc2cb57cfe4f807173/node_modules/@nuxtjs/i18n/dist/runtime/composables/index')['useLocaleRoute']>
readonly useModel: UnwrapRef<typeof import('vue')['useModel']>
readonly useNotificationCenter: UnwrapRef<typeof import('../../app/composables/useNotificationCenter')['useNotificationCenter']>
readonly useNuxtApp: UnwrapRef<typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/nuxt')['useNuxtApp']>
readonly useNuxtData: UnwrapRef<typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/composables/asyncData')['useNuxtData']>
readonly useNuxtDevTools: UnwrapRef<typeof import('../../node_modules/.pnpm/@nuxt+devtools@3.1.1_vite@7.3.0_@types+node@25.0.3_jiti@2.6.1_terser@5.44.1_yaml@2.8.2__vue@3.5.26_typescript@5.9.3_/node_modules/@nuxt/devtools/dist/runtime/use-nuxt-devtools')['useNuxtDevTools']>
readonly usePageSeo: UnwrapRef<typeof import('../../app/composables/usePageSeo')['usePageSeo']>
readonly usePreviewMode: UnwrapRef<typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/composables/preview')['usePreviewMode']>
readonly useRequestEvent: UnwrapRef<typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/composables/ssr')['useRequestEvent']>
readonly useRequestFetch: UnwrapRef<typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/composables/ssr')['useRequestFetch']>
readonly useRequestHeader: UnwrapRef<typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/composables/ssr')['useRequestHeader']>
readonly useRequestHeaders: UnwrapRef<typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/composables/ssr')['useRequestHeaders']>
readonly useRequestURL: UnwrapRef<typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/composables/url')['useRequestURL']>
readonly useResourceAccess: UnwrapRef<typeof import('../../app/composables/useResourceAccess')['useResourceAccess']>
readonly useResponseHeader: UnwrapRef<typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/composables/ssr')['useResponseHeader']>
readonly useRoute: UnwrapRef<typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/composables/router')['useRoute']>
readonly useRouteAnnouncer: UnwrapRef<typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/composables/route-announcer')['useRouteAnnouncer']>
readonly useRouteBaseName: UnwrapRef<typeof import('../../node_modules/.pnpm/@nuxtjs+i18n@10.2.4_@emnapi+core@1.7.1_@emnapi+runtime@1.7.1_@vue+compiler-dom@3.5.26_d_a5323b549840adcc2cb57cfe4f807173/node_modules/@nuxtjs/i18n/dist/runtime/composables/index')['useRouteBaseName']>
readonly useRouter: UnwrapRef<typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/composables/router')['useRouter']>
readonly useRuntimeConfig: UnwrapRef<typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/nuxt')['useRuntimeConfig']>
readonly useRuntimeHook: UnwrapRef<typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/composables/runtime-hook')['useRuntimeHook']>
readonly useScript: UnwrapRef<typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/composables/script-stubs')['useScript']>
readonly useScriptClarity: UnwrapRef<typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/composables/script-stubs')['useScriptClarity']>
readonly useScriptCloudflareWebAnalytics: UnwrapRef<typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/composables/script-stubs')['useScriptCloudflareWebAnalytics']>
readonly useScriptCrisp: UnwrapRef<typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/composables/script-stubs')['useScriptCrisp']>
readonly useScriptDatabuddyAnalytics: UnwrapRef<typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/composables/script-stubs')['useScriptDatabuddyAnalytics']>
readonly useScriptEventPage: UnwrapRef<typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/composables/script-stubs')['useScriptEventPage']>
readonly useScriptFathomAnalytics: UnwrapRef<typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/composables/script-stubs')['useScriptFathomAnalytics']>
readonly useScriptGoogleAdsense: UnwrapRef<typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/composables/script-stubs')['useScriptGoogleAdsense']>
readonly useScriptGoogleAnalytics: UnwrapRef<typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/composables/script-stubs')['useScriptGoogleAnalytics']>
readonly useScriptGoogleMaps: UnwrapRef<typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/composables/script-stubs')['useScriptGoogleMaps']>
readonly useScriptGoogleTagManager: UnwrapRef<typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/composables/script-stubs')['useScriptGoogleTagManager']>
readonly useScriptHotjar: UnwrapRef<typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/composables/script-stubs')['useScriptHotjar']>
readonly useScriptIntercom: UnwrapRef<typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/composables/script-stubs')['useScriptIntercom']>
readonly useScriptLemonSqueezy: UnwrapRef<typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/composables/script-stubs')['useScriptLemonSqueezy']>
readonly useScriptMatomoAnalytics: UnwrapRef<typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/composables/script-stubs')['useScriptMatomoAnalytics']>
readonly useScriptMetaPixel: UnwrapRef<typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/composables/script-stubs')['useScriptMetaPixel']>
readonly useScriptNpm: UnwrapRef<typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/composables/script-stubs')['useScriptNpm']>
readonly useScriptPayPal: UnwrapRef<typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/composables/script-stubs')['useScriptPayPal']>
readonly useScriptPlausibleAnalytics: UnwrapRef<typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/composables/script-stubs')['useScriptPlausibleAnalytics']>
readonly useScriptRedditPixel: UnwrapRef<typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/composables/script-stubs')['useScriptRedditPixel']>
readonly useScriptRybbitAnalytics: UnwrapRef<typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/composables/script-stubs')['useScriptRybbitAnalytics']>
readonly useScriptSegment: UnwrapRef<typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/composables/script-stubs')['useScriptSegment']>
readonly useScriptSnapchatPixel: UnwrapRef<typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/composables/script-stubs')['useScriptSnapchatPixel']>
readonly useScriptStripe: UnwrapRef<typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/composables/script-stubs')['useScriptStripe']>
readonly useScriptTriggerConsent: UnwrapRef<typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/composables/script-stubs')['useScriptTriggerConsent']>
readonly useScriptTriggerElement: UnwrapRef<typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/composables/script-stubs')['useScriptTriggerElement']>
readonly useScriptUmamiAnalytics: UnwrapRef<typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/composables/script-stubs')['useScriptUmamiAnalytics']>
readonly useScriptVimeoPlayer: UnwrapRef<typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/composables/script-stubs')['useScriptVimeoPlayer']>
readonly useScriptXPixel: UnwrapRef<typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/composables/script-stubs')['useScriptXPixel']>
readonly useScriptYouTubePlayer: UnwrapRef<typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/composables/script-stubs')['useScriptYouTubePlayer']>
readonly useSeoMeta: UnwrapRef<typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/composables/head')['useSeoMeta']>
readonly useServerHead: UnwrapRef<typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/composables/head')['useServerHead']>
readonly useServerHeadSafe: UnwrapRef<typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/composables/head')['useServerHeadSafe']>
readonly useServerSeoMeta: UnwrapRef<typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/composables/head')['useServerSeoMeta']>
readonly useSetI18nParams: UnwrapRef<typeof import('../../node_modules/.pnpm/@nuxtjs+i18n@10.2.4_@emnapi+core@1.7.1_@emnapi+runtime@1.7.1_@vue+compiler-dom@3.5.26_d_a5323b549840adcc2cb57cfe4f807173/node_modules/@nuxtjs/i18n/dist/runtime/composables/index')['useSetI18nParams']>
readonly useShadowRoot: UnwrapRef<typeof import('vue')['useShadowRoot']>
readonly useSlots: UnwrapRef<typeof import('vue')['useSlots']>
readonly useState: UnwrapRef<typeof import('../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/composables/state')['useState']>
readonly useSwitchLocalePath: UnwrapRef<typeof import('../../node_modules/.pnpm/@nuxtjs+i18n@10.2.4_@emnapi+core@1.7.1_@emnapi+runtime@1.7.1_@vue+compiler-dom@3.5.26_d_a5323b549840adcc2cb57cfe4f807173/node_modules/@nuxtjs/i18n/dist/runtime/composables/index')['useSwitchLocalePath']>
readonly useTemplateRef: UnwrapRef<typeof import('vue')['useTemplateRef']>
readonly useTransitionState: UnwrapRef<typeof import('vue')['useTransitionState']>
readonly watch: UnwrapRef<typeof import('vue')['watch']>
readonly watchEffect: UnwrapRef<typeof import('vue')['watchEffect']>
readonly watchPostEffect: UnwrapRef<typeof import('vue')['watchPostEffect']>
readonly watchSyncEffect: UnwrapRef<typeof import('vue')['watchSyncEffect']>
readonly withCtx: UnwrapRef<typeof import('vue')['withCtx']>
readonly withDirectives: UnwrapRef<typeof import('vue')['withDirectives']>
readonly withKeys: UnwrapRef<typeof import('vue')['withKeys']>
readonly withMemo: UnwrapRef<typeof import('vue')['withMemo']>
readonly withModifiers: UnwrapRef<typeof import('vue')['withModifiers']>
readonly withScopeId: UnwrapRef<typeof import('vue')['withScopeId']>
}
}

7
.nuxt/types/layouts.d.ts vendored Normal file
View File

@@ -0,0 +1,7 @@
import type { ComputedRef, MaybeRef } from 'vue'
export type LayoutKey = "admin" | "blank" | "console" | "default" | "oa"
declare module 'nuxt/app' {
interface PageMeta {
layout?: MaybeRef<LayoutKey | false> | ComputedRef<LayoutKey | false>
}
}

7
.nuxt/types/middleware.d.ts vendored Normal file
View File

@@ -0,0 +1,7 @@
import type { NavigationGuard } from 'vue-router'
export type MiddlewareKey = never
declare module 'nuxt/app' {
interface PageMeta {
middleware?: MiddlewareKey | NavigationGuard | Array<MiddlewareKey | NavigationGuard>
}
}

139
.nuxt/types/modules.d.ts vendored Normal file
View File

@@ -0,0 +1,139 @@
import { NuxtModule, ModuleDependencyMeta } from '@nuxt/schema'
declare module '@nuxt/schema' {
interface ModuleDependencies {
["@nuxt/content"]?: ModuleDependencyMeta<typeof import("@nuxt/content").default extends NuxtModule<infer O> ? O : Record<string, unknown>>
["@nuxtjs/tailwindcss"]?: ModuleDependencyMeta<typeof import("@nuxtjs/tailwindcss").default extends NuxtModule<infer O> ? O : Record<string, unknown>>
["@nuxtjs/i18n"]?: ModuleDependencyMeta<typeof import("@nuxtjs/i18n").default extends NuxtModule<infer O> ? O : Record<string, unknown>>
["@nuxt/devtools"]?: ModuleDependencyMeta<typeof import("@nuxt/devtools").default extends NuxtModule<infer O> ? O : Record<string, unknown>>
["@nuxt/telemetry"]?: ModuleDependencyMeta<typeof import("@nuxt/telemetry").default extends NuxtModule<infer O> ? O : Record<string, unknown>>
["@nuxtjs/mdc"]?: ModuleDependencyMeta<typeof import("@nuxtjs/mdc").default extends NuxtModule<infer O> ? O : Record<string, unknown>>
}
interface NuxtOptions {
/**
* Configuration for `@nuxt/content`
*/
["content"]: typeof import("@nuxt/content").default extends NuxtModule<infer O, unknown, boolean> ? O : Record<string, any>
/**
* Configuration for `@nuxtjs/tailwindcss`
*/
["tailwindcss"]: typeof import("@nuxtjs/tailwindcss").default extends NuxtModule<infer O, unknown, boolean> ? O : Record<string, any>
/**
* Configuration for `@nuxtjs/i18n`
*/
["i18n"]: typeof import("@nuxtjs/i18n").default extends NuxtModule<infer O, unknown, boolean> ? O : Record<string, any>
/**
* Configuration for `@nuxt/devtools`
*/
["devtools"]: typeof import("@nuxt/devtools").default extends NuxtModule<infer O, unknown, boolean> ? O : Record<string, any>
/**
* Configuration for `@nuxt/telemetry`
*/
["telemetry"]: typeof import("@nuxt/telemetry").default extends NuxtModule<infer O, unknown, boolean> ? O : Record<string, any>
/**
* Configuration for `@nuxtjs/mdc`
*/
["mdc"]: typeof import("@nuxtjs/mdc").default extends NuxtModule<infer O, unknown, boolean> ? O : Record<string, any>
}
interface NuxtConfig {
/**
* Configuration for `@nuxt/content`
*/
["content"]?: typeof import("@nuxt/content").default extends NuxtModule<infer O, unknown, boolean> ? Partial<O> : Record<string, any>
/**
* Configuration for `@nuxtjs/tailwindcss`
*/
["tailwindcss"]?: typeof import("@nuxtjs/tailwindcss").default extends NuxtModule<infer O, unknown, boolean> ? Partial<O> : Record<string, any>
/**
* Configuration for `@nuxtjs/i18n`
*/
["i18n"]?: typeof import("@nuxtjs/i18n").default extends NuxtModule<infer O, unknown, boolean> ? Partial<O> : Record<string, any>
/**
* Configuration for `@nuxt/devtools`
*/
["devtools"]?: typeof import("@nuxt/devtools").default extends NuxtModule<infer O, unknown, boolean> ? Partial<O> : Record<string, any>
/**
* Configuration for `@nuxt/telemetry`
*/
["telemetry"]?: typeof import("@nuxt/telemetry").default extends NuxtModule<infer O, unknown, boolean> ? Partial<O> : Record<string, any>
/**
* Configuration for `@nuxtjs/mdc`
*/
["mdc"]?: typeof import("@nuxtjs/mdc").default extends NuxtModule<infer O, unknown, boolean> ? Partial<O> : Record<string, any>
modules?: (undefined | null | false | NuxtModule<any> | string | [NuxtModule | string, Record<string, any>] | ["@nuxt/content", Exclude<NuxtConfig["content"], boolean>] | ["@nuxtjs/tailwindcss", Exclude<NuxtConfig["tailwindcss"], boolean>] | ["@nuxtjs/i18n", Exclude<NuxtConfig["i18n"], boolean>] | ["@nuxt/devtools", Exclude<NuxtConfig["devtools"], boolean>] | ["@nuxt/telemetry", Exclude<NuxtConfig["telemetry"], boolean>] | ["@nuxtjs/mdc", Exclude<NuxtConfig["mdc"], boolean>])[],
}
}
declare module 'nuxt/schema' {
interface ModuleDependencies {
["@nuxt/content"]?: ModuleDependencyMeta<typeof import("@nuxt/content").default extends NuxtModule<infer O> ? O : Record<string, unknown>>
["@nuxtjs/tailwindcss"]?: ModuleDependencyMeta<typeof import("@nuxtjs/tailwindcss").default extends NuxtModule<infer O> ? O : Record<string, unknown>>
["@nuxtjs/i18n"]?: ModuleDependencyMeta<typeof import("@nuxtjs/i18n").default extends NuxtModule<infer O> ? O : Record<string, unknown>>
["@nuxt/devtools"]?: ModuleDependencyMeta<typeof import("@nuxt/devtools").default extends NuxtModule<infer O> ? O : Record<string, unknown>>
["@nuxt/telemetry"]?: ModuleDependencyMeta<typeof import("@nuxt/telemetry").default extends NuxtModule<infer O> ? O : Record<string, unknown>>
["@nuxtjs/mdc"]?: ModuleDependencyMeta<typeof import("@nuxtjs/mdc").default extends NuxtModule<infer O> ? O : Record<string, unknown>>
}
interface NuxtOptions {
/**
* Configuration for `@nuxt/content`
* @see https://content.nuxt.com
*/
["content"]: typeof import("@nuxt/content").default extends NuxtModule<infer O, unknown, boolean> ? O : Record<string, any>
/**
* Configuration for `@nuxtjs/tailwindcss`
* @see https://www.npmjs.com/package/@nuxtjs/tailwindcss
*/
["tailwindcss"]: typeof import("@nuxtjs/tailwindcss").default extends NuxtModule<infer O, unknown, boolean> ? O : Record<string, any>
/**
* Configuration for `@nuxtjs/i18n`
* @see https://www.npmjs.com/package/@nuxtjs/i18n
*/
["i18n"]: typeof import("@nuxtjs/i18n").default extends NuxtModule<infer O, unknown, boolean> ? O : Record<string, any>
/**
* Configuration for `@nuxt/devtools`
* @see https://www.npmjs.com/package/@nuxt/devtools
*/
["devtools"]: typeof import("@nuxt/devtools").default extends NuxtModule<infer O, unknown, boolean> ? O : Record<string, any>
/**
* Configuration for `@nuxt/telemetry`
* @see https://www.npmjs.com/package/@nuxt/telemetry
*/
["telemetry"]: typeof import("@nuxt/telemetry").default extends NuxtModule<infer O, unknown, boolean> ? O : Record<string, any>
/**
* Configuration for `@nuxtjs/mdc`
* @see https://www.npmjs.com/package/@nuxtjs/mdc
*/
["mdc"]: typeof import("@nuxtjs/mdc").default extends NuxtModule<infer O, unknown, boolean> ? O : Record<string, any>
}
interface NuxtConfig {
/**
* Configuration for `@nuxt/content`
* @see https://content.nuxt.com
*/
["content"]?: typeof import("@nuxt/content").default extends NuxtModule<infer O, unknown, boolean> ? Partial<O> : Record<string, any>
/**
* Configuration for `@nuxtjs/tailwindcss`
* @see https://www.npmjs.com/package/@nuxtjs/tailwindcss
*/
["tailwindcss"]?: typeof import("@nuxtjs/tailwindcss").default extends NuxtModule<infer O, unknown, boolean> ? Partial<O> : Record<string, any>
/**
* Configuration for `@nuxtjs/i18n`
* @see https://www.npmjs.com/package/@nuxtjs/i18n
*/
["i18n"]?: typeof import("@nuxtjs/i18n").default extends NuxtModule<infer O, unknown, boolean> ? Partial<O> : Record<string, any>
/**
* Configuration for `@nuxt/devtools`
* @see https://www.npmjs.com/package/@nuxt/devtools
*/
["devtools"]?: typeof import("@nuxt/devtools").default extends NuxtModule<infer O, unknown, boolean> ? Partial<O> : Record<string, any>
/**
* Configuration for `@nuxt/telemetry`
* @see https://www.npmjs.com/package/@nuxt/telemetry
*/
["telemetry"]?: typeof import("@nuxt/telemetry").default extends NuxtModule<infer O, unknown, boolean> ? Partial<O> : Record<string, any>
/**
* Configuration for `@nuxtjs/mdc`
* @see https://www.npmjs.com/package/@nuxtjs/mdc
*/
["mdc"]?: typeof import("@nuxtjs/mdc").default extends NuxtModule<infer O, unknown, boolean> ? Partial<O> : Record<string, any>
modules?: (undefined | null | false | NuxtModule<any> | string | [NuxtModule | string, Record<string, any>] | ["@nuxt/content", Exclude<NuxtConfig["content"], boolean>] | ["@nuxtjs/tailwindcss", Exclude<NuxtConfig["tailwindcss"], boolean>] | ["@nuxtjs/i18n", Exclude<NuxtConfig["i18n"], boolean>] | ["@nuxt/devtools", Exclude<NuxtConfig["devtools"], boolean>] | ["@nuxt/telemetry", Exclude<NuxtConfig["telemetry"], boolean>] | ["@nuxtjs/mdc", Exclude<NuxtConfig["mdc"], boolean>])[],
}
}

14
.nuxt/types/nitro-config.d.ts vendored Normal file
View File

@@ -0,0 +1,14 @@
// Generated by nitro
// App Config
import type { Defu } from 'defu'
type UserAppConfig = Defu<{}, []>
declare module "nitropack/types" {
interface AppConfig extends UserAppConfig {}
}
export {}

171
.nuxt/types/nitro-imports.d.ts vendored Normal file
View File

@@ -0,0 +1,171 @@
declare global {
const __buildAssetsURL: typeof import('../../node_modules/.pnpm/@nuxt+nitro-server@4.2.2_better-sqlite3@12.8.0_db0@0.3.4_better-sqlite3@12.8.0__ioredis_a0a2be7525d559e696e64db570f075d2/node_modules/@nuxt/nitro-server/dist/runtime/utils/paths').buildAssetsURL
const __publicAssetsURL: typeof import('../../node_modules/.pnpm/@nuxt+nitro-server@4.2.2_better-sqlite3@12.8.0_db0@0.3.4_better-sqlite3@12.8.0__ioredis_a0a2be7525d559e696e64db570f075d2/node_modules/@nuxt/nitro-server/dist/runtime/utils/paths').publicAssetsURL
const appendCorsHeaders: typeof import('../../node_modules/.pnpm/h3@1.15.4/node_modules/h3').appendCorsHeaders
const appendCorsPreflightHeaders: typeof import('../../node_modules/.pnpm/h3@1.15.4/node_modules/h3').appendCorsPreflightHeaders
const appendHeader: typeof import('../../node_modules/.pnpm/h3@1.15.4/node_modules/h3').appendHeader
const appendHeaders: typeof import('../../node_modules/.pnpm/h3@1.15.4/node_modules/h3').appendHeaders
const appendResponseHeader: typeof import('../../node_modules/.pnpm/h3@1.15.4/node_modules/h3').appendResponseHeader
const appendResponseHeaders: typeof import('../../node_modules/.pnpm/h3@1.15.4/node_modules/h3').appendResponseHeaders
const assertMethod: typeof import('../../node_modules/.pnpm/h3@1.15.4/node_modules/h3').assertMethod
const cachedEventHandler: typeof import('../../node_modules/.pnpm/nitropack@2.12.9_better-sqlite3@12.8.0/node_modules/nitropack/dist/runtime/internal/cache').cachedEventHandler
const cachedFunction: typeof import('../../node_modules/.pnpm/nitropack@2.12.9_better-sqlite3@12.8.0/node_modules/nitropack/dist/runtime/internal/cache').cachedFunction
const callNodeListener: typeof import('../../node_modules/.pnpm/h3@1.15.4/node_modules/h3').callNodeListener
const clearResponseHeaders: typeof import('../../node_modules/.pnpm/h3@1.15.4/node_modules/h3').clearResponseHeaders
const clearSession: typeof import('../../node_modules/.pnpm/h3@1.15.4/node_modules/h3').clearSession
const createApp: typeof import('../../node_modules/.pnpm/h3@1.15.4/node_modules/h3').createApp
const createAppEventHandler: typeof import('../../node_modules/.pnpm/h3@1.15.4/node_modules/h3').createAppEventHandler
const createError: typeof import('../../node_modules/.pnpm/h3@1.15.4/node_modules/h3').createError
const createEvent: typeof import('../../node_modules/.pnpm/h3@1.15.4/node_modules/h3').createEvent
const createEventStream: typeof import('../../node_modules/.pnpm/h3@1.15.4/node_modules/h3').createEventStream
const createRouter: typeof import('../../node_modules/.pnpm/h3@1.15.4/node_modules/h3').createRouter
const defaultContentType: typeof import('../../node_modules/.pnpm/h3@1.15.4/node_modules/h3').defaultContentType
const defineAppConfig: typeof import('../../node_modules/.pnpm/@nuxt+nitro-server@4.2.2_better-sqlite3@12.8.0_db0@0.3.4_better-sqlite3@12.8.0__ioredis_a0a2be7525d559e696e64db570f075d2/node_modules/@nuxt/nitro-server/dist/runtime/utils/config').defineAppConfig
const defineCachedEventHandler: typeof import('../../node_modules/.pnpm/nitropack@2.12.9_better-sqlite3@12.8.0/node_modules/nitropack/dist/runtime/internal/cache').defineCachedEventHandler
const defineCachedFunction: typeof import('../../node_modules/.pnpm/nitropack@2.12.9_better-sqlite3@12.8.0/node_modules/nitropack/dist/runtime/internal/cache').defineCachedFunction
const defineEventHandler: typeof import('../../node_modules/.pnpm/h3@1.15.4/node_modules/h3').defineEventHandler
const defineI18nConfig: typeof import('../../node_modules/.pnpm/@nuxtjs+i18n@10.2.4_@emnapi+core@1.7.1_@emnapi+runtime@1.7.1_@vue+compiler-dom@3.5.26_d_a5323b549840adcc2cb57cfe4f807173/node_modules/@nuxtjs/i18n/dist/runtime/composables/shared').defineI18nConfig
const defineI18nLocale: typeof import('../../node_modules/.pnpm/@nuxtjs+i18n@10.2.4_@emnapi+core@1.7.1_@emnapi+runtime@1.7.1_@vue+compiler-dom@3.5.26_d_a5323b549840adcc2cb57cfe4f807173/node_modules/@nuxtjs/i18n/dist/runtime/composables/shared').defineI18nLocale
const defineI18nLocaleDetector: typeof import('../../node_modules/.pnpm/@nuxtjs+i18n@10.2.4_@emnapi+core@1.7.1_@emnapi+runtime@1.7.1_@vue+compiler-dom@3.5.26_d_a5323b549840adcc2cb57cfe4f807173/node_modules/@nuxtjs/i18n/dist/runtime/composables/server').defineI18nLocaleDetector
const defineLazyEventHandler: typeof import('../../node_modules/.pnpm/h3@1.15.4/node_modules/h3').defineLazyEventHandler
const defineNitroErrorHandler: typeof import('../../node_modules/.pnpm/nitropack@2.12.9_better-sqlite3@12.8.0/node_modules/nitropack/dist/runtime/internal/error/utils').defineNitroErrorHandler
const defineNitroPlugin: typeof import('../../node_modules/.pnpm/nitropack@2.12.9_better-sqlite3@12.8.0/node_modules/nitropack/dist/runtime/internal/plugin').defineNitroPlugin
const defineNodeListener: typeof import('../../node_modules/.pnpm/h3@1.15.4/node_modules/h3').defineNodeListener
const defineNodeMiddleware: typeof import('../../node_modules/.pnpm/h3@1.15.4/node_modules/h3').defineNodeMiddleware
const defineRenderHandler: typeof import('../../node_modules/.pnpm/nitropack@2.12.9_better-sqlite3@12.8.0/node_modules/nitropack/dist/runtime/internal/renderer').defineRenderHandler
const defineRequestMiddleware: typeof import('../../node_modules/.pnpm/h3@1.15.4/node_modules/h3').defineRequestMiddleware
const defineResponseMiddleware: typeof import('../../node_modules/.pnpm/h3@1.15.4/node_modules/h3').defineResponseMiddleware
const defineRouteMeta: typeof import('../../node_modules/.pnpm/nitropack@2.12.9_better-sqlite3@12.8.0/node_modules/nitropack/dist/runtime/internal/meta').defineRouteMeta
const defineTask: typeof import('../../node_modules/.pnpm/nitropack@2.12.9_better-sqlite3@12.8.0/node_modules/nitropack/dist/runtime/internal/task').defineTask
const defineWebSocket: typeof import('../../node_modules/.pnpm/h3@1.15.4/node_modules/h3').defineWebSocket
const defineWebSocketHandler: typeof import('../../node_modules/.pnpm/h3@1.15.4/node_modules/h3').defineWebSocketHandler
const deleteCookie: typeof import('../../node_modules/.pnpm/h3@1.15.4/node_modules/h3').deleteCookie
const dynamicEventHandler: typeof import('../../node_modules/.pnpm/h3@1.15.4/node_modules/h3').dynamicEventHandler
const eventHandler: typeof import('../../node_modules/.pnpm/h3@1.15.4/node_modules/h3').eventHandler
const fetchWithEvent: typeof import('../../node_modules/.pnpm/h3@1.15.4/node_modules/h3').fetchWithEvent
const fromNodeMiddleware: typeof import('../../node_modules/.pnpm/h3@1.15.4/node_modules/h3').fromNodeMiddleware
const fromPlainHandler: typeof import('../../node_modules/.pnpm/h3@1.15.4/node_modules/h3').fromPlainHandler
const fromWebHandler: typeof import('../../node_modules/.pnpm/h3@1.15.4/node_modules/h3').fromWebHandler
const getCookie: typeof import('../../node_modules/.pnpm/h3@1.15.4/node_modules/h3').getCookie
const getCookieLocale: typeof import('../../node_modules/.pnpm/@intlify+utils@0.14.1/node_modules/@intlify/utils/dist/h3').getCookieLocale
const getHeader: typeof import('../../node_modules/.pnpm/h3@1.15.4/node_modules/h3').getHeader
const getHeaderLanguage: typeof import('../../node_modules/.pnpm/@intlify+utils@0.14.1/node_modules/@intlify/utils/dist/h3').getHeaderLanguage
const getHeaderLanguages: typeof import('../../node_modules/.pnpm/@intlify+utils@0.14.1/node_modules/@intlify/utils/dist/h3').getHeaderLanguages
const getHeaderLocale: typeof import('../../node_modules/.pnpm/@intlify+utils@0.14.1/node_modules/@intlify/utils/dist/h3').getHeaderLocale
const getHeaderLocales: typeof import('../../node_modules/.pnpm/@intlify+utils@0.14.1/node_modules/@intlify/utils/dist/h3').getHeaderLocales
const getHeaders: typeof import('../../node_modules/.pnpm/h3@1.15.4/node_modules/h3').getHeaders
const getMethod: typeof import('../../node_modules/.pnpm/h3@1.15.4/node_modules/h3').getMethod
const getPathLocale: typeof import('../../node_modules/.pnpm/@intlify+utils@0.14.1/node_modules/@intlify/utils/dist/h3').getPathLocale
const getProxyRequestHeaders: typeof import('../../node_modules/.pnpm/h3@1.15.4/node_modules/h3').getProxyRequestHeaders
const getQuery: typeof import('../../node_modules/.pnpm/h3@1.15.4/node_modules/h3').getQuery
const getQueryLocale: typeof import('../../node_modules/.pnpm/@intlify+utils@0.14.1/node_modules/@intlify/utils/dist/h3').getQueryLocale
const getRequestFingerprint: typeof import('../../node_modules/.pnpm/h3@1.15.4/node_modules/h3').getRequestFingerprint
const getRequestHeader: typeof import('../../node_modules/.pnpm/h3@1.15.4/node_modules/h3').getRequestHeader
const getRequestHeaders: typeof import('../../node_modules/.pnpm/h3@1.15.4/node_modules/h3').getRequestHeaders
const getRequestHost: typeof import('../../node_modules/.pnpm/h3@1.15.4/node_modules/h3').getRequestHost
const getRequestIP: typeof import('../../node_modules/.pnpm/h3@1.15.4/node_modules/h3').getRequestIP
const getRequestPath: typeof import('../../node_modules/.pnpm/h3@1.15.4/node_modules/h3').getRequestPath
const getRequestProtocol: typeof import('../../node_modules/.pnpm/h3@1.15.4/node_modules/h3').getRequestProtocol
const getRequestURL: typeof import('../../node_modules/.pnpm/h3@1.15.4/node_modules/h3').getRequestURL
const getRequestWebStream: typeof import('../../node_modules/.pnpm/h3@1.15.4/node_modules/h3').getRequestWebStream
const getResponseHeader: typeof import('../../node_modules/.pnpm/h3@1.15.4/node_modules/h3').getResponseHeader
const getResponseHeaders: typeof import('../../node_modules/.pnpm/h3@1.15.4/node_modules/h3').getResponseHeaders
const getResponseStatus: typeof import('../../node_modules/.pnpm/h3@1.15.4/node_modules/h3').getResponseStatus
const getResponseStatusText: typeof import('../../node_modules/.pnpm/h3@1.15.4/node_modules/h3').getResponseStatusText
const getRouteRules: typeof import('../../node_modules/.pnpm/nitropack@2.12.9_better-sqlite3@12.8.0/node_modules/nitropack/dist/runtime/internal/route-rules').getRouteRules
const getRouterParam: typeof import('../../node_modules/.pnpm/h3@1.15.4/node_modules/h3').getRouterParam
const getRouterParams: typeof import('../../node_modules/.pnpm/h3@1.15.4/node_modules/h3').getRouterParams
const getSession: typeof import('../../node_modules/.pnpm/h3@1.15.4/node_modules/h3').getSession
const getValidatedQuery: typeof import('../../node_modules/.pnpm/h3@1.15.4/node_modules/h3').getValidatedQuery
const getValidatedRouterParams: typeof import('../../node_modules/.pnpm/h3@1.15.4/node_modules/h3').getValidatedRouterParams
const handleCacheHeaders: typeof import('../../node_modules/.pnpm/h3@1.15.4/node_modules/h3').handleCacheHeaders
const handleCors: typeof import('../../node_modules/.pnpm/h3@1.15.4/node_modules/h3').handleCors
const isCorsOriginAllowed: typeof import('../../node_modules/.pnpm/h3@1.15.4/node_modules/h3').isCorsOriginAllowed
const isError: typeof import('../../node_modules/.pnpm/h3@1.15.4/node_modules/h3').isError
const isEvent: typeof import('../../node_modules/.pnpm/h3@1.15.4/node_modules/h3').isEvent
const isEventHandler: typeof import('../../node_modules/.pnpm/h3@1.15.4/node_modules/h3').isEventHandler
const isMethod: typeof import('../../node_modules/.pnpm/h3@1.15.4/node_modules/h3').isMethod
const isPreflightRequest: typeof import('../../node_modules/.pnpm/h3@1.15.4/node_modules/h3').isPreflightRequest
const isStream: typeof import('../../node_modules/.pnpm/h3@1.15.4/node_modules/h3').isStream
const isWebResponse: typeof import('../../node_modules/.pnpm/h3@1.15.4/node_modules/h3').isWebResponse
const lazyEventHandler: typeof import('../../node_modules/.pnpm/h3@1.15.4/node_modules/h3').lazyEventHandler
const nitroPlugin: typeof import('../../node_modules/.pnpm/nitropack@2.12.9_better-sqlite3@12.8.0/node_modules/nitropack/dist/runtime/internal/plugin').nitroPlugin
const parseCookies: typeof import('../../node_modules/.pnpm/h3@1.15.4/node_modules/h3').parseCookies
const parseMarkdown: typeof import('../../node_modules/.pnpm/@nuxtjs+mdc@0.20.2_magicast@0.5.1/node_modules/@nuxtjs/mdc/dist/runtime/parser').parseMarkdown
const promisifyNodeListener: typeof import('../../node_modules/.pnpm/h3@1.15.4/node_modules/h3').promisifyNodeListener
const proxyRequest: typeof import('../../node_modules/.pnpm/h3@1.15.4/node_modules/h3').proxyRequest
const queryCollection: typeof import('../../node_modules/.pnpm/@nuxt+content@3.12.0_better-sqlite3@12.8.0_magicast@0.5.1/node_modules/@nuxt/content/dist/runtime/nitro').queryCollection
const queryCollectionItemSurroundings: typeof import('../../node_modules/.pnpm/@nuxt+content@3.12.0_better-sqlite3@12.8.0_magicast@0.5.1/node_modules/@nuxt/content/dist/runtime/nitro').queryCollectionItemSurroundings
const queryCollectionNavigation: typeof import('../../node_modules/.pnpm/@nuxt+content@3.12.0_better-sqlite3@12.8.0_magicast@0.5.1/node_modules/@nuxt/content/dist/runtime/nitro').queryCollectionNavigation
const queryCollectionSearchSections: typeof import('../../node_modules/.pnpm/@nuxt+content@3.12.0_better-sqlite3@12.8.0_magicast@0.5.1/node_modules/@nuxt/content/dist/runtime/nitro').queryCollectionSearchSections
const readBody: typeof import('../../node_modules/.pnpm/h3@1.15.4/node_modules/h3').readBody
const readFormData: typeof import('../../node_modules/.pnpm/h3@1.15.4/node_modules/h3').readFormData
const readMultipartFormData: typeof import('../../node_modules/.pnpm/h3@1.15.4/node_modules/h3').readMultipartFormData
const readRawBody: typeof import('../../node_modules/.pnpm/h3@1.15.4/node_modules/h3').readRawBody
const readValidatedBody: typeof import('../../node_modules/.pnpm/h3@1.15.4/node_modules/h3').readValidatedBody
const removeResponseHeader: typeof import('../../node_modules/.pnpm/h3@1.15.4/node_modules/h3').removeResponseHeader
const runTask: typeof import('../../node_modules/.pnpm/nitropack@2.12.9_better-sqlite3@12.8.0/node_modules/nitropack/dist/runtime/internal/task').runTask
const sanitizeStatusCode: typeof import('../../node_modules/.pnpm/h3@1.15.4/node_modules/h3').sanitizeStatusCode
const sanitizeStatusMessage: typeof import('../../node_modules/.pnpm/h3@1.15.4/node_modules/h3').sanitizeStatusMessage
const sealSession: typeof import('../../node_modules/.pnpm/h3@1.15.4/node_modules/h3').sealSession
const send: typeof import('../../node_modules/.pnpm/h3@1.15.4/node_modules/h3').send
const sendError: typeof import('../../node_modules/.pnpm/h3@1.15.4/node_modules/h3').sendError
const sendIterable: typeof import('../../node_modules/.pnpm/h3@1.15.4/node_modules/h3').sendIterable
const sendNoContent: typeof import('../../node_modules/.pnpm/h3@1.15.4/node_modules/h3').sendNoContent
const sendProxy: typeof import('../../node_modules/.pnpm/h3@1.15.4/node_modules/h3').sendProxy
const sendRedirect: typeof import('../../node_modules/.pnpm/h3@1.15.4/node_modules/h3').sendRedirect
const sendStream: typeof import('../../node_modules/.pnpm/h3@1.15.4/node_modules/h3').sendStream
const sendWebResponse: typeof import('../../node_modules/.pnpm/h3@1.15.4/node_modules/h3').sendWebResponse
const serveStatic: typeof import('../../node_modules/.pnpm/h3@1.15.4/node_modules/h3').serveStatic
const setCookie: typeof import('../../node_modules/.pnpm/h3@1.15.4/node_modules/h3').setCookie
const setCookieLocale: typeof import('../../node_modules/.pnpm/@intlify+utils@0.14.1/node_modules/@intlify/utils/dist/h3').setCookieLocale
const setHeader: typeof import('../../node_modules/.pnpm/h3@1.15.4/node_modules/h3').setHeader
const setHeaders: typeof import('../../node_modules/.pnpm/h3@1.15.4/node_modules/h3').setHeaders
const setResponseHeader: typeof import('../../node_modules/.pnpm/h3@1.15.4/node_modules/h3').setResponseHeader
const setResponseHeaders: typeof import('../../node_modules/.pnpm/h3@1.15.4/node_modules/h3').setResponseHeaders
const setResponseStatus: typeof import('../../node_modules/.pnpm/h3@1.15.4/node_modules/h3').setResponseStatus
const splitCookiesString: typeof import('../../node_modules/.pnpm/h3@1.15.4/node_modules/h3').splitCookiesString
const stringifyMarkdown: typeof import('../../node_modules/.pnpm/@nuxtjs+mdc@0.20.2_magicast@0.5.1/node_modules/@nuxtjs/mdc/dist/runtime/stringify').stringifyMarkdown
const toEventHandler: typeof import('../../node_modules/.pnpm/h3@1.15.4/node_modules/h3').toEventHandler
const toNodeListener: typeof import('../../node_modules/.pnpm/h3@1.15.4/node_modules/h3').toNodeListener
const toPlainHandler: typeof import('../../node_modules/.pnpm/h3@1.15.4/node_modules/h3').toPlainHandler
const toWebHandler: typeof import('../../node_modules/.pnpm/h3@1.15.4/node_modules/h3').toWebHandler
const toWebRequest: typeof import('../../node_modules/.pnpm/h3@1.15.4/node_modules/h3').toWebRequest
const tryCookieLocale: typeof import('../../node_modules/.pnpm/@intlify+utils@0.14.1/node_modules/@intlify/utils/dist/h3').tryCookieLocale
const tryHeaderLocale: typeof import('../../node_modules/.pnpm/@intlify+utils@0.14.1/node_modules/@intlify/utils/dist/h3').tryHeaderLocale
const tryHeaderLocales: typeof import('../../node_modules/.pnpm/@intlify+utils@0.14.1/node_modules/@intlify/utils/dist/h3').tryHeaderLocales
const tryPathLocale: typeof import('../../node_modules/.pnpm/@intlify+utils@0.14.1/node_modules/@intlify/utils/dist/h3').tryPathLocale
const tryQueryLocale: typeof import('../../node_modules/.pnpm/@intlify+utils@0.14.1/node_modules/@intlify/utils/dist/h3').tryQueryLocale
const unsealSession: typeof import('../../node_modules/.pnpm/h3@1.15.4/node_modules/h3').unsealSession
const updateSession: typeof import('../../node_modules/.pnpm/h3@1.15.4/node_modules/h3').updateSession
const useAppConfig: typeof import('../../node_modules/.pnpm/nitropack@2.12.9_better-sqlite3@12.8.0/node_modules/nitropack/dist/runtime/internal/config').useAppConfig
const useBase: typeof import('../../node_modules/.pnpm/h3@1.15.4/node_modules/h3').useBase
const useEvent: typeof import('../../node_modules/.pnpm/nitropack@2.12.9_better-sqlite3@12.8.0/node_modules/nitropack/dist/runtime/internal/context').useEvent
const useNitroApp: typeof import('../../node_modules/.pnpm/nitropack@2.12.9_better-sqlite3@12.8.0/node_modules/nitropack/dist/runtime/internal/app').useNitroApp
const useRuntimeConfig: typeof import('../../node_modules/.pnpm/nitropack@2.12.9_better-sqlite3@12.8.0/node_modules/nitropack/dist/runtime/internal/config').useRuntimeConfig
const useSession: typeof import('../../node_modules/.pnpm/h3@1.15.4/node_modules/h3').useSession
const useStorage: typeof import('../../node_modules/.pnpm/nitropack@2.12.9_better-sqlite3@12.8.0/node_modules/nitropack/dist/runtime/internal/storage').useStorage
const useTranslation: typeof import('../../node_modules/.pnpm/@intlify+h3@0.7.4/node_modules/@intlify/h3').useTranslation
const writeEarlyHints: typeof import('../../node_modules/.pnpm/h3@1.15.4/node_modules/h3').writeEarlyHints
}
export { useNitroApp } from 'nitropack/runtime/internal/app';
export { useRuntimeConfig, useAppConfig } from 'nitropack/runtime/internal/config';
export { defineNitroPlugin, nitroPlugin } from 'nitropack/runtime/internal/plugin';
export { defineCachedFunction, defineCachedEventHandler, cachedFunction, cachedEventHandler } from 'nitropack/runtime/internal/cache';
export { useStorage } from 'nitropack/runtime/internal/storage';
export { defineRenderHandler } from 'nitropack/runtime/internal/renderer';
export { defineRouteMeta } from 'nitropack/runtime/internal/meta';
export { getRouteRules } from 'nitropack/runtime/internal/route-rules';
export { useEvent } from 'nitropack/runtime/internal/context';
export { defineTask, runTask } from 'nitropack/runtime/internal/task';
export { defineNitroErrorHandler } from 'nitropack/runtime/internal/error/utils';
export { appendCorsHeaders, appendCorsPreflightHeaders, appendHeader, appendHeaders, appendResponseHeader, appendResponseHeaders, assertMethod, callNodeListener, clearResponseHeaders, clearSession, createApp, createAppEventHandler, createError, createEvent, createEventStream, createRouter, defaultContentType, defineEventHandler, defineLazyEventHandler, defineNodeListener, defineNodeMiddleware, defineRequestMiddleware, defineResponseMiddleware, defineWebSocket, defineWebSocketHandler, deleteCookie, dynamicEventHandler, eventHandler, fetchWithEvent, fromNodeMiddleware, fromPlainHandler, fromWebHandler, getCookie, getHeader, getHeaders, getMethod, getProxyRequestHeaders, getQuery, getRequestFingerprint, getRequestHeader, getRequestHeaders, getRequestHost, getRequestIP, getRequestPath, getRequestProtocol, getRequestURL, getRequestWebStream, getResponseHeader, getResponseHeaders, getResponseStatus, getResponseStatusText, getRouterParam, getRouterParams, getSession, getValidatedQuery, getValidatedRouterParams, handleCacheHeaders, handleCors, isCorsOriginAllowed, isError, isEvent, isEventHandler, isMethod, isPreflightRequest, isStream, isWebResponse, lazyEventHandler, parseCookies, promisifyNodeListener, proxyRequest, readBody, readFormData, readMultipartFormData, readRawBody, readValidatedBody, removeResponseHeader, sanitizeStatusCode, sanitizeStatusMessage, sealSession, send, sendError, sendIterable, sendNoContent, sendProxy, sendRedirect, sendStream, sendWebResponse, serveStatic, setCookie, setHeader, setHeaders, setResponseHeader, setResponseHeaders, setResponseStatus, splitCookiesString, toEventHandler, toNodeListener, toPlainHandler, toWebHandler, toWebRequest, unsealSession, updateSession, useBase, useSession, writeEarlyHints } from 'h3';
export { buildAssetsURL as __buildAssetsURL, publicAssetsURL as __publicAssetsURL } from '/Users/gxwebsoft/VUE/tiantian-system/node_modules/.pnpm/@nuxt+nitro-server@4.2.2_better-sqlite3@12.8.0_db0@0.3.4_better-sqlite3@12.8.0__ioredis_a0a2be7525d559e696e64db570f075d2/node_modules/@nuxt/nitro-server/dist/runtime/utils/paths';
export { defineAppConfig } from '/Users/gxwebsoft/VUE/tiantian-system/node_modules/.pnpm/@nuxt+nitro-server@4.2.2_better-sqlite3@12.8.0_db0@0.3.4_better-sqlite3@12.8.0__ioredis_a0a2be7525d559e696e64db570f075d2/node_modules/@nuxt/nitro-server/dist/runtime/utils/config';
export { queryCollection, queryCollectionSearchSections, queryCollectionNavigation, queryCollectionItemSurroundings } from '/Users/gxwebsoft/VUE/tiantian-system/node_modules/.pnpm/@nuxt+content@3.12.0_better-sqlite3@12.8.0_magicast@0.5.1/node_modules/@nuxt/content/dist/runtime/nitro';
export { parseMarkdown } from '/Users/gxwebsoft/VUE/tiantian-system/node_modules/.pnpm/@nuxtjs+mdc@0.20.2_magicast@0.5.1/node_modules/@nuxtjs/mdc/dist/runtime/parser';
export { stringifyMarkdown } from '/Users/gxwebsoft/VUE/tiantian-system/node_modules/.pnpm/@nuxtjs+mdc@0.20.2_magicast@0.5.1/node_modules/@nuxtjs/mdc/dist/runtime/stringify';
export { defineI18nLocale, defineI18nConfig } from '/Users/gxwebsoft/VUE/tiantian-system/node_modules/.pnpm/@nuxtjs+i18n@10.2.4_@emnapi+core@1.7.1_@emnapi+runtime@1.7.1_@vue+compiler-dom@3.5.26_d_a5323b549840adcc2cb57cfe4f807173/node_modules/@nuxtjs/i18n/dist/runtime/composables/shared';
export { defineI18nLocaleDetector } from '/Users/gxwebsoft/VUE/tiantian-system/node_modules/.pnpm/@nuxtjs+i18n@10.2.4_@emnapi+core@1.7.1_@emnapi+runtime@1.7.1_@vue+compiler-dom@3.5.26_d_a5323b549840adcc2cb57cfe4f807173/node_modules/@nuxtjs/i18n/dist/runtime/composables/server';
export { useTranslation } from '@intlify/h3';
export { getCookieLocale, getHeaderLanguage, getHeaderLanguages, getHeaderLocale, getHeaderLocales, getPathLocale, getQueryLocale, setCookieLocale, tryCookieLocale, tryHeaderLocale, tryHeaderLocales, tryPathLocale, tryQueryLocale } from '@intlify/utils/h3';

11
.nuxt/types/nitro-middleware.d.ts vendored Normal file
View File

@@ -0,0 +1,11 @@
export type MiddlewareKey = never
declare module 'nitropack/types' {
interface NitroRouteConfig {
appMiddleware?: MiddlewareKey | MiddlewareKey[] | Record<MiddlewareKey, boolean>
}
}
declare module 'nitropack' {
interface NitroRouteConfig {
appMiddleware?: MiddlewareKey | MiddlewareKey[] | Record<MiddlewareKey, boolean>
}
}

61
.nuxt/types/nitro-nuxt.d.ts vendored Normal file
View File

@@ -0,0 +1,61 @@
/// <reference path="app.config.d.ts" />
/// <reference path="runtime-config.d.ts" />
/// <reference types="/Users/gxwebsoft/VUE/tiantian-system/node_modules/.pnpm/@nuxt+nitro-server@4.2.2_better-sqlite3@12.8.0_db0@0.3.4_better-sqlite3@12.8.0__ioredis_a0a2be7525d559e696e64db570f075d2/node_modules/@nuxt/nitro-server/dist/index.mjs" />
/// <reference path="nitro-middleware.d.ts" />
import type { RuntimeConfig } from 'nuxt/schema'
import type { H3Event } from 'h3'
import type { LogObject } from 'consola'
import type { NuxtIslandContext, NuxtIslandResponse, NuxtRenderHTMLContext } from 'nuxt/app'
declare module 'nitropack' {
interface NitroRuntimeConfigApp {
buildAssetsDir: string
cdnURL: string
}
interface NitroRuntimeConfig extends RuntimeConfig {}
interface NitroRouteConfig {
ssr?: boolean
noScripts?: boolean
/** @deprecated Use `noScripts` instead */
experimentalNoScripts?: boolean
}
interface NitroRouteRules {
ssr?: boolean
noScripts?: boolean
/** @deprecated Use `noScripts` instead */
experimentalNoScripts?: boolean
appMiddleware?: Record<string, boolean>
}
interface NitroRuntimeHooks {
'dev:ssr-logs': (ctx: { logs: LogObject[], path: string }) => void | Promise<void>
'render:html': (htmlContext: NuxtRenderHTMLContext, context: { event: H3Event }) => void | Promise<void>
'render:island': (islandResponse: NuxtIslandResponse, context: { event: H3Event, islandContext: NuxtIslandContext }) => void | Promise<void>
}
}
declare module 'nitropack/types' {
interface NitroRuntimeConfigApp {
buildAssetsDir: string
cdnURL: string
}
interface NitroRuntimeConfig extends RuntimeConfig {}
interface NitroRouteConfig {
ssr?: boolean
noScripts?: boolean
/** @deprecated Use `noScripts` instead */
experimentalNoScripts?: boolean
}
interface NitroRouteRules {
ssr?: boolean
noScripts?: boolean
/** @deprecated Use `noScripts` instead */
experimentalNoScripts?: boolean
appMiddleware?: Record<string, boolean>
}
interface NitroRuntimeHooks {
'dev:ssr-logs': (ctx: { logs: LogObject[], path: string }) => void | Promise<void>
'render:html': (htmlContext: NuxtRenderHTMLContext, context: { event: H3Event }) => void | Promise<void>
'render:island': (islandResponse: NuxtIslandResponse, context: { event: H3Event, islandContext: NuxtIslandContext }) => void | Promise<void>
}
}

50
.nuxt/types/nitro-routes.d.ts vendored Normal file
View File

@@ -0,0 +1,50 @@
// Generated by nitro
import type { Serialize, Simplify } from "nitropack/types";
declare module "nitropack/types" {
type Awaited<T> = T extends PromiseLike<infer U> ? Awaited<U> : T
interface InternalApi {
'/api/_app/**:path': {
'default': Simplify<Serialize<Awaited<ReturnType<typeof import('../../server/api/_app/[...path]').default>>>>
}
'/api/_file/**:path': {
'default': Simplify<Serialize<Awaited<ReturnType<typeof import('../../server/api/_file/[...path]').default>>>>
}
'/api/_modules/**:path': {
'default': Simplify<Serialize<Awaited<ReturnType<typeof import('../../server/api/_modules/[...path]').default>>>>
}
'/api/_server/**:path': {
'default': Simplify<Serialize<Awaited<ReturnType<typeof import('../../server/api/_server/[...path]').default>>>>
}
'/api/cms/**:path': {
'default': Simplify<Serialize<Awaited<ReturnType<typeof import('../../server/api/cms/[...path]').default>>>>
}
'/api/cms/cms-website/getSiteInfo': {
'get': Simplify<Serialize<Awaited<ReturnType<typeof import('../../server/api/cms/cms-website/getSiteInfo.get').default>>>>
}
'/api/cms/cms-website/pageAll': {
'get': Simplify<Serialize<Awaited<ReturnType<typeof import('../../server/api/cms/cms-website/pageAll.get').default>>>>
}
'/__nuxt_error': {
'default': Simplify<Serialize<Awaited<ReturnType<typeof import('../../node_modules/.pnpm/@nuxt+nitro-server@4.2.2_better-sqlite3@12.8.0_db0@0.3.4_better-sqlite3@12.8.0__ioredis_a0a2be7525d559e696e64db570f075d2/node_modules/@nuxt/nitro-server/dist/runtime/handlers/renderer').default>>>>
}
'/__nuxt_island/**': {
'default': Simplify<Serialize<Awaited<ReturnType<typeof import('../../server/#internal/nuxt/island-renderer').default>>>>
}
'/__nuxt_content/docs/sql_dump.txt': {
'default': Simplify<Serialize<Awaited<ReturnType<typeof import('../../node_modules/.pnpm/@nuxt+content@3.12.0_better-sqlite3@12.8.0_magicast@0.5.1/node_modules/@nuxt/content/dist/runtime/presets/node/database-handler').default>>>>
}
'/__nuxt_content/info/sql_dump.txt': {
'default': Simplify<Serialize<Awaited<ReturnType<typeof import('../../node_modules/.pnpm/@nuxt+content@3.12.0_better-sqlite3@12.8.0_magicast@0.5.1/node_modules/@nuxt/content/dist/runtime/presets/node/database-handler').default>>>>
}
'/_i18n/:hash/:locale/messages.json': {
'default': Simplify<Serialize<Awaited<ReturnType<typeof import('../../node_modules/.pnpm/@nuxtjs+i18n@10.2.4_@emnapi+core@1.7.1_@emnapi+runtime@1.7.1_@vue+compiler-dom@3.5.26_d_a5323b549840adcc2cb57cfe4f807173/node_modules/@nuxtjs/i18n/dist/runtime/server/routes/messages').default>>>>
}
'/__nuxt_content/docs/query': {
'default': Simplify<Serialize<Awaited<ReturnType<typeof import('../../node_modules/.pnpm/@nuxt+content@3.12.0_better-sqlite3@12.8.0_magicast@0.5.1/node_modules/@nuxt/content/dist/runtime/api/query.post').default>>>>
}
'/__nuxt_content/info/query': {
'default': Simplify<Serialize<Awaited<ReturnType<typeof import('../../node_modules/.pnpm/@nuxt+content@3.12.0_better-sqlite3@12.8.0_magicast@0.5.1/node_modules/@nuxt/content/dist/runtime/api/query.post').default>>>>
}
}
}
export {}

3
.nuxt/types/nitro.d.ts vendored Normal file
View File

@@ -0,0 +1,3 @@
/// <reference path="./nitro-routes.d.ts" />
/// <reference path="./nitro-config.d.ts" />
/// <reference path="./nitro-imports.d.ts" />

45
.nuxt/types/plugins.d.ts vendored Normal file
View File

@@ -0,0 +1,45 @@
// Generated by Nuxt'
import type { Plugin } from '#app'
type Decorate<T extends Record<string, any>> = { [K in keyof T as K extends string ? `$${K}` : never]: T[K] }
type InjectionType<A extends Plugin> = A extends {default: Plugin<infer T>} ? Decorate<T> : unknown
type NuxtAppInjections =
InjectionType<typeof import("../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/plugins/revive-payload.client.js")> &
InjectionType<typeof import("../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/head/runtime/plugins/unhead.js")> &
InjectionType<typeof import("../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/pages/runtime/plugins/router.js")> &
InjectionType<typeof import("../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/plugins/browser-devtools-timing.client.js")> &
InjectionType<typeof import("../../node_modules/.pnpm/@nuxt+content@3.12.0_better-sqlite3@12.8.0_magicast@0.5.1/node_modules/@nuxt/content/dist/runtime/plugins/websocket.dev.js")> &
InjectionType<typeof import("../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/plugins/dev-server-logs.js")> &
InjectionType<typeof import("../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/plugins/navigation-repaint.client.js")> &
InjectionType<typeof import("../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/plugins/revive-payload.server.js")> &
InjectionType<typeof import("../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/plugins/chunk-reload.client.js")> &
InjectionType<typeof import("../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/pages/runtime/plugins/prefetch.client.js")> &
InjectionType<typeof import("../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/pages/runtime/plugins/check-if-page-unused.js")> &
InjectionType<typeof import("../../node_modules/.pnpm/@nuxt+devtools@3.1.1_vite@7.3.0_@types+node@25.0.3_jiti@2.6.1_terser@5.44.1_yaml@2.8.2__vue@3.5.26_typescript@5.9.3_/node_modules/@nuxt/devtools/dist/runtime/plugins/devtools.server.js")> &
InjectionType<typeof import("../../node_modules/.pnpm/@nuxt+devtools@3.1.1_vite@7.3.0_@types+node@25.0.3_jiti@2.6.1_terser@5.44.1_yaml@2.8.2__vue@3.5.26_typescript@5.9.3_/node_modules/@nuxt/devtools/dist/runtime/plugins/devtools.client.js")> &
InjectionType<typeof import("../../node_modules/.pnpm/@nuxtjs+i18n@10.2.4_@emnapi+core@1.7.1_@emnapi+runtime@1.7.1_@vue+compiler-dom@3.5.26_d_a5323b549840adcc2cb57cfe4f807173/node_modules/@nuxtjs/i18n/dist/runtime/plugins/switch-locale-path-ssr.js")> &
InjectionType<typeof import("../../node_modules/.pnpm/@nuxtjs+i18n@10.2.4_@emnapi+core@1.7.1_@emnapi+runtime@1.7.1_@vue+compiler-dom@3.5.26_d_a5323b549840adcc2cb57cfe4f807173/node_modules/@nuxtjs/i18n/dist/runtime/plugins/route-locale-detect.js")> &
InjectionType<typeof import("../../node_modules/.pnpm/@nuxtjs+i18n@10.2.4_@emnapi+core@1.7.1_@emnapi+runtime@1.7.1_@vue+compiler-dom@3.5.26_d_a5323b549840adcc2cb57cfe4f807173/node_modules/@nuxtjs/i18n/dist/runtime/plugins/preload.js")> &
InjectionType<typeof import("../../node_modules/.pnpm/@nuxtjs+i18n@10.2.4_@emnapi+core@1.7.1_@emnapi+runtime@1.7.1_@vue+compiler-dom@3.5.26_d_a5323b549840adcc2cb57cfe4f807173/node_modules/@nuxtjs/i18n/dist/runtime/plugins/dev.js")> &
InjectionType<typeof import("../../node_modules/.pnpm/@nuxtjs+i18n@10.2.4_@emnapi+core@1.7.1_@emnapi+runtime@1.7.1_@vue+compiler-dom@3.5.26_d_a5323b549840adcc2cb57cfe4f807173/node_modules/@nuxtjs/i18n/dist/runtime/plugins/i18n.js")> &
InjectionType<typeof import("../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/plugins/warn.dev.server.js")> &
InjectionType<typeof import("../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/app/plugins/check-if-layout-used.js")> &
InjectionType<typeof import("../../app/plugins/antd")> &
InjectionType<typeof import("../../node_modules/.pnpm/nuxt@4.2.2_@parcel+watcher@2.5.1_@types+node@25.0.3_@vue+compiler-sfc@3.5.26_better-sql_ac6a4d57fe59bc83c2ad1951937f70f6/node_modules/nuxt/dist/pages/runtime/plugins/prerender.server.js")> &
InjectionType<typeof import("../../node_modules/.pnpm/@nuxtjs+i18n@10.2.4_@emnapi+core@1.7.1_@emnapi+runtime@1.7.1_@vue+compiler-dom@3.5.26_d_a5323b549840adcc2cb57cfe4f807173/node_modules/@nuxtjs/i18n/dist/runtime/plugins/ssg-detect.js")>
declare module '#app' {
interface NuxtApp extends NuxtAppInjections { }
interface NuxtAppLiterals {
pluginName: 'vue-devtools-client' | 'nuxt:revive-payload:client' | 'nuxt:head' | 'nuxt:router' | 'nuxt:browser-devtools-timing' | 'nuxt:revive-payload:server' | 'nuxt:chunk-reload' | 'nuxt:global-components' | 'nuxt:prefetch' | 'nuxt:checkIfPageUnused' | 'i18n:plugin:switch-locale-path-ssr' | 'i18n:plugin:route-locale-detect' | 'i18n:plugin:preload' | 'i18n:dev' | 'i18n:plugin' | 'nuxt:checkIfLayoutUsed' | 'i18n:plugin:ssg-detect'
}
}
declare module 'vue' {
interface ComponentCustomProperties extends NuxtAppInjections { }
}
export { }

184
.nuxt/types/runtime-config.d.ts vendored Normal file
View File

@@ -0,0 +1,184 @@
import { RuntimeConfig as UserRuntimeConfig, PublicRuntimeConfig as UserPublicRuntimeConfig } from 'nuxt/schema'
interface SharedRuntimeConfig {
app: {
buildId: string,
baseURL: string,
buildAssetsDir: string,
cdnURL: string,
},
nitro: {
envPrefix: string,
},
content: {
databaseVersion: string,
version: string,
database: {
type: string,
filename: string,
},
localDatabase: {
type: string,
filename: string,
},
integrityCheck: boolean,
},
}
interface SharedPublicRuntimeConfig {
tenantId: string,
serverApiBase: string,
modulesApiBase: string,
appApiBase: string,
mpApiBase: string,
fileServerBase: string,
templateId: string,
ServerApi: string,
ApiBase: string,
TenantId: string,
mdc: {
components: {
prose: boolean,
map: any,
customElements: Array<any>,
},
headings: {
anchorLinks: {
h1: boolean,
h2: boolean,
h3: boolean,
h4: boolean,
h5: boolean,
h6: boolean,
},
},
highlight: {
noApiRoute: boolean,
highlighter: string,
theme: {
default: string,
dark: string,
},
shikiEngine: string,
langs: Array<string>,
},
},
content: {
wsUrl: string,
},
i18n: {
baseUrl: string,
defaultLocale: string,
rootRedirect: any,
redirectStatusCode: number,
skipSettingLocaleOnNavigate: boolean,
locales: Array<{
}>,
detectBrowserLanguage: {
alwaysRedirect: boolean,
cookieCrossOrigin: boolean,
cookieDomain: any,
cookieKey: string,
cookieSecure: boolean,
fallbackLocale: string,
redirectOn: string,
useCookie: boolean,
},
experimental: {
localeDetector: string,
typedPages: boolean,
typedOptionsAndMessages: boolean,
alternateLinkCanonicalQueries: boolean,
devCache: boolean,
cacheLifetime: any,
stripMessagesPayload: boolean,
preload: boolean,
strictSeo: boolean,
nitroContextDetection: boolean,
httpCacheDuration: number,
},
domainLocales: {
"zh-CN": {
domain: string,
},
en: {
domain: string,
},
},
},
}
declare module '@nuxt/schema' {
interface RuntimeConfig extends UserRuntimeConfig {}
interface PublicRuntimeConfig extends UserPublicRuntimeConfig {}
}
declare module 'nuxt/schema' {
interface RuntimeConfig extends SharedRuntimeConfig {}
interface PublicRuntimeConfig extends SharedPublicRuntimeConfig {}
}
declare module 'vue' {
interface ComponentCustomProperties {
$config: UserRuntimeConfig
}
}

0
.nuxt/types/vue-shim.d.ts vendored Normal file
View File

View File

@@ -8,10 +8,54 @@
"profession": "高级开发工程师", "profession": "高级开发工程师",
"avatarUrl": "https://acc-1258344699.cos.accelerate.myqcloud.com/workbuddy/experts/avatars/02-Engineering/SeniorDeveloper/SeniorDeveloper.png", "avatarUrl": "https://acc-1258344699.cos.accelerate.myqcloud.com/workbuddy/experts/avatars/02-Engineering/SeniorDeveloper/SeniorDeveloper.png",
"promptUrl": "https://acc-1258344699.cos.accelerate.myqcloud.com/workbuddy/experts/experts/02-Engineering/SeniorDeveloper/SeniorDeveloper_zh.md", "promptUrl": "https://acc-1258344699.cos.accelerate.myqcloud.com/workbuddy/experts/experts/02-Engineering/SeniorDeveloper/SeniorDeveloper_zh.md",
"usedAt": 1775638741301, "usedAt": 1775689373969,
"industryId": "all"
}
],
"5b0e0ab2a79f433a81feeffe9cfdae81": [
{
"expertId": "SeniorDeveloper",
"name": "Will",
"profession": "高级开发工程师",
"avatarUrl": "https://acc-1258344699.cos.accelerate.myqcloud.com/workbuddy/experts/avatars/02-Engineering/SeniorDeveloper/SeniorDeveloper.png",
"promptUrl": "https://acc-1258344699.cos.accelerate.myqcloud.com/workbuddy/experts/experts/02-Engineering/SeniorDeveloper/SeniorDeveloper_zh.md",
"usedAt": 1775672993038,
"industryId": "all"
}
],
"3a5f73545e884d2eb0bb971193e859a6": [
{
"expertId": "SeniorDeveloper",
"name": "Will",
"profession": "高级开发工程师",
"avatarUrl": "https://acc-1258344699.cos.accelerate.myqcloud.com/workbuddy/experts/avatars/02-Engineering/SeniorDeveloper/SeniorDeveloper.png",
"promptUrl": "https://acc-1258344699.cos.accelerate.myqcloud.com/workbuddy/experts/experts/02-Engineering/SeniorDeveloper/SeniorDeveloper_zh.md",
"usedAt": 1775690111439,
"industryId": "all"
}
],
"495fc8add86e46feb8a39ab77587a1bb": [
{
"expertId": "SeniorDeveloper",
"name": "Will",
"profession": "高级开发工程师",
"avatarUrl": "https://acc-1258344699.cos.accelerate.myqcloud.com/workbuddy/experts/avatars/02-Engineering/SeniorDeveloper/SeniorDeveloper.png",
"promptUrl": "https://acc-1258344699.cos.accelerate.myqcloud.com/workbuddy/experts/experts/02-Engineering/SeniorDeveloper/SeniorDeveloper_zh.md",
"usedAt": 1775690194537,
"industryId": "all"
}
],
"5dd0d6b21e754bb581db828aee297bd7": [
{
"expertId": "SeniorDeveloper",
"name": "Will",
"profession": "高级开发工程师",
"avatarUrl": "https://acc-1258344699.cos.accelerate.myqcloud.com/workbuddy/experts/avatars/02-Engineering/SeniorDeveloper/SeniorDeveloper.png",
"promptUrl": "https://acc-1258344699.cos.accelerate.myqcloud.com/workbuddy/experts/experts/02-Engineering/SeniorDeveloper/SeniorDeveloper_zh.md",
"usedAt": 1775690686771,
"industryId": "all" "industryId": "all"
} }
] ]
}, },
"lastUpdated": 1775670138823 "lastUpdated": 1775691132330
} }

View File

@@ -0,0 +1,35 @@
# 2026-04-09 工作日志
## 制造业数字化管理后台 /admin 重构
将 /admin 从之前的通用SaaS后端改造为制造业数字化标准体系包含4大模块
### 菜单结构 (admin-nav.ts)
- 首页总览 `/admin`
- 产品生命周期数字化:产品设计 `/admin/product/design`、营销管理 `/admin/product/marketing`、售后服务 `/admin/product/service`
- 生产执行数字化:计划排程 `/admin/production/schedule`、生产管控 `/admin/production/control`、质量管理 `/admin/production/quality`、设备管理 `/admin/production/equipment`、安全生产 `/admin/production/safety`、能耗管理 `/admin/production/energy`
- 供应链数字化:采购管理 `/admin/supply/purchase`、仓储物流 `/admin/supply/warehouse`
- 管理决策数字化:财务管理 `/admin/management/finance`、人力资源 `/admin/management/hr`、协同办公 `/admin/management/office`、决策支持 `/admin/management/decision`
- 系统设置 `/admin/settings`(已有,保持不变)
### 技术说明
- 所有页面使用 Ant Design Vue
- 布局复用 admin.vue左侧固定菜单栏支持折叠/展开)
- 每个页面通过 useNav().activeTab 设置当前菜单高亮
- 图标全部使用 @ant-design/icons-vue 有效图标
## 页面风格升级 - 玻璃态+渐变
所有制造业管理后台页面已升级为现代风格:
- 玻璃态效果 (glass): rgba背景 + backdrop-filter模糊
- 渐变色图标背景
- 悬停卡片动画 (card-hover)
- 紫蓝橙绿现代配色
涉及页面:
- /admin/production/equipment.vue - 设备管理
- /admin/supply/purchase.vue - 采购管理
- /admin/supply/warehouse.vue - 仓储物流
- /admin/management/finance.vue - 财务管理
- /admin/management/hr.vue - 人力资源
- /admin/management/office.vue - 协同办公

View File

@@ -1,100 +1,116 @@
import type { Component } from 'vue'
import { import {
AppstoreOutlined, AppstoreOutlined,
AuditOutlined, DesktopOutlined,
DashboardOutlined,
TeamOutlined,
SettingOutlined,
FileTextOutlined, FileTextOutlined,
ShoppingOutlined,
ShoppingCartOutlined,
TrophyOutlined,
DollarOutlined,
TeamOutlined,
NotificationOutlined,
FundOutlined,
SettingOutlined,
ApartmentOutlined,
AuditOutlined,
CarOutlined,
ContainerOutlined,
CustomerServiceOutlined, CustomerServiceOutlined,
DashboardOutlined,
FireOutlined,
GoldOutlined,
LineChartOutlined,
MedicineBoxOutlined,
PercentageOutlined,
SafetyOutlined,
ScanOutlined,
ScheduleOutlined,
SecurityScanOutlined,
ShopOutlined, ShopOutlined,
GitlabOutlined, KeyOutlined,
ThunderboltOutlined,
ToolOutlined,
UnorderedListOutlined,
UserAddOutlined,
UserOutlined,
UserSwitchOutlined,
} from '@ant-design/icons-vue' } from '@ant-design/icons-vue'
export type AdminNavItem = { // AdminNavLink: 单条菜单项
key: string export interface AdminNavLink {
label: string
icon?: Component
to: string to: string
}
export type AdminNavGroup = {
key: string
label: string label: string
icon?: Component icon?: any
iconColor?: string
badge?: string badge?: string
disabled?: boolean
children: AdminNavItem[]
} }
export type AdminNavLink = Omit<AdminNavGroup, 'children'> & { // AdminNavGroup: 含有子菜单的分组
to: string export interface AdminNavGroup {
key: string
label: string
icon?: any
children: AdminNavLink[]
} }
export type AdminNavEntry = AdminNavGroup | AdminNavLink // 联合类型
export type AdminNavEntry = AdminNavLink | AdminNavGroup
export const adminNav: AdminNavEntry[] = [ export const adminNav: AdminNavEntry[] = [
{ {
key: 'admin-home',
label: '管理首页',
icon: DashboardOutlined,
to: '/admin', to: '/admin',
label: '首页总览',
icon: DashboardOutlined,
}, },
{ {
key: 'admin-app-review', key: 'product',
label: '应用审核', label: '产品生命周期数字化',
icon: AuditOutlined,
badge: 'NEW',
to: '/admin/app-review',
},
{
key: 'admin-git-review',
label: 'Git 审核',
icon: GitlabOutlined,
to: '/admin/git-review',
},
{
key: 'admin-apps',
label: '应用管理',
icon: AppstoreOutlined,
to: '/admin/apps',
},
{
key: 'admin-market',
label: '应用市场',
icon: ShopOutlined,
to: '/admin/market',
},
{
key: 'admin-users',
label: '用户管理',
icon: TeamOutlined,
children: [
{ key: 'admin-users-list', label: '所有用户', icon: TeamOutlined, to: '/admin/users' },
{ key: 'admin-developers', label: '开发者', icon: TeamOutlined, to: '/admin/developers' },
],
},
{
key: 'admin-tickets',
label: '工单处理',
icon: CustomerServiceOutlined, icon: CustomerServiceOutlined,
to: '/admin/tickets',
},
{
key: 'admin-content',
label: '内容管理',
icon: FileTextOutlined,
children: [ children: [
{ key: 'admin-articles', label: '文章管理', icon: FileTextOutlined, to: '/admin/articles' }, { to: '/admin/product/design', label: '产品设计', icon: FileTextOutlined },
{ key: 'admin-article-categories', label: '文章分类', icon: FileTextOutlined, to: '/admin/article-categories' }, { to: '/admin/product/marketing', label: '营销管理', icon: ShopOutlined },
{ key: 'admin-announcements', label: '公告管理', icon: FileTextOutlined, to: '/admin/announcements' }, { to: '/admin/product/service', label: '售后服务', icon: KeyOutlined },
], ],
}, },
{ {
key: 'admin-settings', key: 'production',
label: '平台设置', label: '生产执行数字化',
icon: SettingOutlined, icon: ThunderboltOutlined,
children: [
{ to: '/admin/production/schedule', label: '计划排程', icon: ScheduleOutlined },
{ to: '/admin/production/control', label: '生产管控', icon: DesktopOutlined },
{ to: '/admin/production/quality', label: '质量管理', icon: SafetyOutlined },
{ to: '/admin/production/equipment', label: '设备管理', icon: ToolOutlined },
{ to: '/admin/production/safety', label: '安全生产', icon: SecurityScanOutlined },
{ to: '/admin/production/energy', label: '能耗管理', icon: FireOutlined },
],
},
{
key: 'supply',
label: '供应链数字化',
icon: ApartmentOutlined,
children: [
{ to: '/admin/supply/purchase', label: '采购管理', icon: ShoppingOutlined },
{ to: '/admin/supply/warehouse', label: '仓储物流', icon: ContainerOutlined },
],
},
{
key: 'management',
label: '管理决策数字化',
icon: FundOutlined,
children: [
{ to: '/admin/management/finance', label: '财务管理', icon: DollarOutlined },
{ to: '/admin/management/hr', label: '人力资源', icon: TeamOutlined },
{ to: '/admin/management/office', label: '协同办公', icon: NotificationOutlined },
{ to: '/admin/management/decision', label: '决策支持', icon: LineChartOutlined },
],
},
{
to: '/admin/account',
label: '个人信息',
icon: UserOutlined,
},
{
to: '/admin/settings', to: '/admin/settings',
label: '系统设置',
icon: SettingOutlined,
}, },
] ]

View File

@@ -5,6 +5,14 @@ export type OaNavItem = {
} }
export const oaNav: OaNavItem[] = [ export const oaNav: OaNavItem[] = [
{ key: 'oa-home', label: '概览', to: '/oa' } { key: 'oa-home', label: '概览', to: '/oa' },
{ key: 'oa-projects', label: '项目管理', to: '/oa/projects' },
{ key: 'oa-tasks', label: '任务看板', to: '/oa/tasks' },
{ key: 'oa-documents', label: '文档协同', to: '/oa/documents' },
{ key: 'oa-meetings', label: '会议管理', to: '/oa/meetings' },
{ key: 'oa-approvals', label: '审批流程', to: '/oa/approvals' },
{ key: 'oa-teams', label: '团队协作', to: '/oa/teams' },
{ key: 'oa-calendar', label: '日程日历', to: '/oa/calendar' },
{ key: 'oa-chat', label: '即时通讯', to: '/oa/chat' }
] ]

View File

@@ -1,664 +0,0 @@
<template>
<a-layout class="min-h-screen layout-shell">
<a-layout class="w-full">
<SiteHeader />
<a-layout class="body w-full mx-auto max-w-screen-xl">
<!-- 未授权提示页面 -->
<div v-if="!accessible && ready" class="w-full">
<!-- 场景 1已登录但既不是平台开发者也没有被邀请到任何应用 -->
<div v-if="accessChecked && !hasJoinedApps" class="apply-developer-page">
<div class="apply-card">
<div class="icon-wrapper">
<span class="icon">🛠</span>
</div>
<h1 class="title">加入开发者中心</h1>
<p class="subtitle">
你还没有加入任何应用请联系应用管理员发送邀请链接或申请平台开发者资质以创建自己的应用
</p>
<div class="features">
<div class="feature-item">
<span class="feature-icon">🔑</span>
<span>获取 API Key调用 200+ REST 接口</span>
</div>
<div class="feature-item">
<span class="feature-icon">📦</span>
<span>创建和管理您的应用</span>
</div>
<div class="feature-item">
<span class="feature-icon">💻</span>
<span>申请源码访问权限</span>
</div>
<div class="feature-item">
<span class="feature-icon">🤖</span>
<span>接入 AI 智能体 API</span>
</div>
</div>
<div class="actions">
<a-button
type="primary"
size="large"
class="apply-btn"
:loading="applying"
@click="handleApply"
>
申请平台开发者资质
</a-button>
<a-button
size="large"
class="back-btn"
@click="navigateTo('/console')"
>
返回用户中心
</a-button>
</div>
<div class="tips">
<p>💡 收到应用邀请后刷新此页面即可进入开发者中心</p>
<p>如有疑问请联系客服或发送邮件至 support@websopy.com</p>
</div>
</div>
</div>
</div>
<template v-else>
<!-- 侧边栏 -->
<a-layout-sider
class="sider"
:width="240"
breakpoint="lg"
theme="light"
:trigger="null"
collapsible
v-model:collapsed="collapsed"
>
<!-- 品牌区 -->
<div class="sider-brand" :class="{ collapsed }">
<div class="sider-brand-icon">
<span class="text-lg">🛠</span>
</div>
<div v-if="!collapsed" class="sider-brand-text">
<div class="sider-title">开发者中心</div>
<div class="sider-subtitle">Developer Console</div>
</div>
</div>
<!-- 用户信息非折叠状态 -->
<div v-if="!collapsed && user" class="sider-user">
<a-avatar :size="32" class="sider-user-avatar">
{{ userDisplayName.charAt(0).toUpperCase() }}
</a-avatar>
<div class="sider-user-info">
<div class="sider-user-name">{{ userDisplayName }}</div>
<a-tag :color="isPlatformDev ? 'green' : 'blue'" class="sider-user-role">
{{ isPlatformDev ? '平台开发者' : '协作成员' }}
</a-tag>
</div>
</div>
<!-- 分组导航菜单 -->
<div class="sider-nav">
<template v-for="(group, gIdx) in navGroups" :key="gIdx">
<!-- 分组标题 -->
<div v-if="group.name" class="nav-group-label">{{ group.name }}</div>
<!-- 菜单项 -->
<div
v-for="item in group.items"
:key="item.key"
class="nav-item"
:class="{ active: isActive(item.to), collapsed }"
@click="navigateTo(item.to)"
>
<span class="nav-item-icon">{{ item.icon }}</span>
<span v-if="!collapsed" class="nav-item-label">{{ item.label }}</span>
<a-tag
v-if="!collapsed && item.badge"
:color="item.badge === 'NEW' ? 'green' : 'orange'"
class="nav-item-badge"
>{{ item.badge }}</a-tag>
</div>
</template>
</div>
<!-- 底部返回入口 -->
<div v-if="!collapsed" class="sider-footer">
<div class="sider-footer-link" @click="navigateTo('/developer-center')">
<span>📄</span> 开发文档
</div>
<div class="sider-footer-link" @click="navigateTo('/')">
<span>🏠</span> 返回首页
</div>
</div>
<!-- 折叠触发器 -->
<div
class="sider-collapse-trigger"
role="button"
tabindex="0"
:aria-label="collapsed ? '展开菜单' : '收起菜单'"
@click="toggleCollapsed"
@keydown.enter.prevent="toggleCollapsed"
@keydown.space.prevent="toggleCollapsed"
>
<RightOutlined :style="{ fontSize: '10px' }" v-if="collapsed" />
<LeftOutlined :style="{ fontSize: '10px' }" v-else />
</div>
</a-layout-sider>
<!-- 主内容区 -->
<a-layout class="main">
<a-layout-content class="content">
<a-spin v-if="!ready && isClient" size="large" tip="加载中..." class="spin" />
<NuxtPage />
</a-layout-content>
</a-layout>
</template>
</a-layout>
<SiteFooter />
</a-layout>
<!-- 邀请通知组件 -->
<ClientOnly>
<InviteNotification />
</ClientOnly>
</a-layout>
</template>
<script setup lang="ts">
import { LeftOutlined, RightOutlined } from '@ant-design/icons-vue'
import { message } from 'ant-design-vue'
import { developerNav } from '@/config/developer-nav'
import { getUserInfo } from '@/api/layout'
import { getToken } from '@/utils/token-util'
import { clearAuthz, setAuthzFromUser } from '@/utils/permission'
import type { User } from '@/api/system/user/model'
import { useAppPermission } from '@/composables/useAppPermission'
import SiteHeader from '~/components/SiteHeader.vue'
import SiteFooter from '~/components/SiteFooter.vue'
import InviteNotification from '~/components/invite/InviteNotification.vue'
const route = useRoute()
const collapsed = ref(false)
const user = ref<User | null>(null)
const isClient = ref(false)
onMounted(() => {
isClient.value = true
})
const userDisplayName = computed(() => {
const u = user.value
const nickname = u?.nickname?.trim()
const username = u?.username?.trim()
const phone = u?.phone?.trim()
const mobile = u?.mobile?.trim()
if (nickname) return nickname
if (username) return username
if (phone) return phone
if (mobile) return mobile
return '开发者'
})
// 按分组整理导航
const navGroups = computed(() => {
const groups: { name: string; items: typeof developerNav }[] = []
const groupMap = new Map<string, typeof developerNav[0][]>()
for (const item of developerNav) {
const g = item.group ?? ''
if (!groupMap.has(g)) groupMap.set(g, [])
groupMap.get(g)!.push(item)
}
for (const [name, items] of groupMap) {
groups.push({ name, items })
}
return groups
})
function isActive(to: string) {
if (to === '/developer') return route.path === '/developer'
return route.path === to || route.path.startsWith(to + '/')
}
function toggleCollapsed() {
collapsed.value = !collapsed.value
}
// ============ 权限控制 ============
const ready = ref(false)
const accessible = ref(false)
const accessChecked = ref(false)
const hasJoinedApps = ref(false)
const isPlatformDev = ref(false)
const applying = ref(false)
const { checkDeveloperAccess, loadAppPermissions, isPlatformDeveloper } = useAppPermission()
onMounted(async () => {
const token = getToken()
if (!token) {
clearAuthz()
message.warning('请先登录后访问开发者中心')
await navigateTo(`/login?from=${encodeURIComponent(route.path)}`)
ready.value = true
return
}
try {
const me = await getUserInfo()
user.value = me
setAuthzFromUser(me)
// 检查开发者访问权限type===2 或有参与的应用都可以进入
const result = await checkDeveloperAccess()
accessChecked.value = true
accessible.value = result.accessible
hasJoinedApps.value = result.hasJoinedApps
isPlatformDev.value = result.isPlatformDeveloper
// 如果有权限,加载完整的应用权限列表
if (result.accessible) {
await loadAppPermissions()
}
}
catch {
// check-access 接口失败时降级type=2 仍然放行
const userType = localStorage.getItem('UserType')
if (userType === '2') {
accessible.value = true
accessChecked.value = true
isPlatformDev.value = true
}
else {
accessible.value = false
accessChecked.value = true
}
}
ready.value = true
})
// 申请开发者资质
async function handleApply() {
applying.value = true
try {
message.info('开发者资质申请功能即将上线,敬请期待!')
}
finally {
applying.value = false
}
}
</script>
<style scoped>
.layout-shell {
background: #f0f2f5;
}
/* 侧边栏 */
.sider {
border-radius: 14px;
overflow: visible;
position: relative;
background: #fff;
border: 1px solid rgba(0, 0, 0, 0.06);
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.04);
}
.sider-brand {
display: flex;
align-items: center;
gap: 10px;
padding: 18px 16px 12px;
border-bottom: 1px solid #f0f0f0;
}
.sider-brand.collapsed {
justify-content: center;
padding: 18px 8px 12px;
}
.sider-brand-icon {
flex-shrink: 0;
width: 36px;
height: 36px;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 10px;
font-size: 18px;
}
.sider-brand-text {
flex: 1;
min-width: 0;
}
.sider-title {
color: rgba(0, 0, 0, 0.88);
font-size: 14px;
font-weight: 600;
line-height: 1.3;
}
.sider-subtitle {
color: rgba(0, 0, 0, 0.35);
font-size: 11px;
line-height: 1.3;
margin-top: 1px;
}
/* 用户信息 */
.sider-user {
display: flex;
align-items: center;
gap: 10px;
padding: 12px 16px;
background: linear-gradient(135deg, #f5f0ff 0%, #eff6ff 100%);
margin: 10px 10px 0;
border-radius: 10px;
}
.sider-user-avatar {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
flex-shrink: 0;
font-size: 14px;
font-weight: 600;
}
.sider-user-info {
flex: 1;
min-width: 0;
}
.sider-user-name {
font-size: 13px;
font-weight: 500;
color: rgba(0, 0, 0, 0.88);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.sider-user-role {
font-size: 10px;
margin: 0;
line-height: 1.5;
}
/* 导航 */
.sider-nav {
padding: 8px 8px 0;
flex: 1;
}
.nav-group-label {
font-size: 11px;
font-weight: 600;
color: rgba(0, 0, 0, 0.35);
text-transform: uppercase;
letter-spacing: 0.05em;
padding: 14px 10px 5px;
user-select: none;
}
.nav-item {
display: flex;
align-items: center;
gap: 9px;
padding: 9px 10px;
border-radius: 9px;
cursor: pointer;
transition: all 0.15s ease;
color: rgba(0, 0, 0, 0.72);
font-size: 14px;
margin-bottom: 2px;
user-select: none;
}
.nav-item:hover {
background: rgba(99, 102, 241, 0.07);
color: #5145cd;
}
.nav-item.active {
background: rgba(99, 102, 241, 0.12);
color: #4f46e5;
font-weight: 500;
}
.nav-item.collapsed {
justify-content: center;
padding: 9px;
}
.nav-item-icon {
font-size: 16px;
flex-shrink: 0;
width: 20px;
text-align: center;
line-height: 1;
}
.nav-item-label {
flex: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.nav-item-badge {
font-size: 10px;
padding: 0 5px;
line-height: 16px;
height: 16px;
flex-shrink: 0;
}
/* 底部快捷入口 */
.sider-footer {
padding: 12px 10px 14px;
border-top: 1px solid #f0f0f0;
margin-top: 8px;
}
.sider-footer-link {
display: flex;
align-items: center;
gap: 8px;
padding: 7px 10px;
border-radius: 8px;
cursor: pointer;
color: rgba(0, 0, 0, 0.45);
font-size: 13px;
transition: all 0.15s;
margin-bottom: 2px;
}
.sider-footer-link:hover {
background: #f5f5f5;
color: rgba(0, 0, 0, 0.72);
}
/* 折叠触发器 */
.sider-collapse-trigger {
position: absolute;
top: 50%;
right: -16px;
transform: translateY(-50%);
width: 16px;
height: 36px;
display: flex;
align-items: center;
justify-content: center;
background: #fff;
border: 1px solid rgba(0, 0, 0, 0.1);
border-left: 0;
border-radius: 0 9999px 9999px 0;
box-shadow: 2px 0 8px rgba(0, 0, 0, 0.06);
color: rgba(0, 0, 0, 0.45);
cursor: pointer;
z-index: 2;
transition: all 0.15s;
}
.sider-collapse-trigger:hover {
background: #f5f5f5;
color: #4f46e5;
}
/* 内容区 */
.body {
margin: 16px auto;
padding: 0 16px;
}
.main {
margin-left: 16px;
background: transparent;
}
.content {
min-height: 520px;
background: #fff;
border-radius: 14px;
padding: 0;
overflow: hidden;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.04);
border: 1px solid rgba(0, 0, 0, 0.05);
}
.forbidden-wrap {
display: flex;
align-items: center;
justify-content: center;
min-height: 480px;
}
.spin {
display: flex;
align-items: center;
justify-content: center;
min-height: 480px;
}
/* 申请开发者页面 */
.apply-developer-page {
display: flex;
align-items: center;
justify-content: center;
min-height: 600px;
padding: 40px 20px;
}
.apply-card {
max-width: 560px;
width: 100%;
background: #fff;
border-radius: 16px;
padding: 48px 40px;
text-align: center;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
border: 1px solid rgba(0, 0, 0, 0.06);
}
.icon-wrapper {
width: 80px;
height: 80px;
margin: 0 auto 24px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 20px;
display: flex;
align-items: center;
justify-content: center;
}
.icon {
font-size: 40px;
}
.title {
font-size: 24px;
font-weight: 600;
color: rgba(0, 0, 0, 0.88);
margin-bottom: 12px;
}
.subtitle {
font-size: 14px;
color: rgba(0, 0, 0, 0.45);
margin-bottom: 32px;
line-height: 1.6;
}
.features {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
margin-bottom: 32px;
text-align: left;
}
.feature-item {
display: flex;
align-items: center;
gap: 10px;
padding: 12px 16px;
background: #f8f9fa;
border-radius: 10px;
font-size: 13px;
color: rgba(0, 0, 0, 0.65);
}
.feature-icon {
font-size: 18px;
}
.actions {
display: flex;
gap: 12px;
justify-content: center;
margin-bottom: 24px;
}
.apply-btn {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border: none;
font-weight: 500;
}
.apply-btn:hover {
opacity: 0.9;
}
.back-btn {
color: rgba(0, 0, 0, 0.65);
}
.tips {
font-size: 12px;
color: rgba(0, 0, 0, 0.35);
line-height: 1.8;
}
@media (max-width: 640px) {
.apply-card {
padding: 32px 24px;
}
.features {
grid-template-columns: 1fr;
}
.actions {
flex-direction: column;
}
}
</style>

247
app/pages/admin/account.vue Normal file
View File

@@ -0,0 +1,247 @@
<script setup lang="ts">
definePageMeta({ layout: 'admin' })
const { user } = useUser()
// 个人信息数据
const profile = reactive({
username: 'admin',
nickname: '系统管理员',
email: 'admin@company.com',
phone: '138****8888',
department: '信息技术部',
position: '系统管理员',
joinDate: '2024-01-15',
lastLogin: '2026-04-09 07:00',
avatar: '',
})
// 安全设置
const securitySettings = reactive({
emailVerified: true,
phoneVerified: true,
twoFactorEnabled: false,
loginPwdChanged: '2026-03-15',
})
// 修改密码表单
const passwordForm = reactive({
oldPassword: '',
newPassword: '',
confirmPassword: '',
})
const passwordRules = {
newPassword: [{ required: true, message: '请输入新密码', trigger: 'blur' }],
confirmPassword: [
{ required: true, message: '请确认新密码', trigger: 'blur' },
{
validator: (_rule: any, value: string) => {
if (value !== passwordForm.newPassword) {
return Promise.reject('两次输入的密码不一致')
}
return Promise.resolve()
},
trigger: 'blur',
},
],
}
// 操作日志
const loginHistory = ref([
{ time: '2026-04-09 07:00:00', ip: '192.168.1.100', device: 'Chrome / Windows 11', location: '广东深圳', status: '成功' },
{ time: '2026-04-08 18:30:00', ip: '192.168.1.100', device: 'Chrome / macOS', location: '广东深圳', status: '成功' },
{ time: '2026-04-08 09:15:00', ip: '192.168.1.101', device: 'Safari / iOS', location: '广东广州', status: '成功' },
{ time: '2026-04-07 16:45:00', ip: '10.0.0.1', device: 'Firefox / Ubuntu', location: '广东深圳', status: '成功' },
{ time: '2026-04-07 08:00:00', ip: '192.168.1.100', device: 'Chrome / Windows 11', location: '广东深圳', status: '成功' },
])
const activeTab = ref('profile')
</script>
<template>
<div class="account-page">
<a-tabs v-model:activeKey="activeTab" class="account-tabs">
<!-- 基本信息 -->
<a-tab-pane key="profile" tab="基本信息">
<a-row :gutter="24">
<a-col :xs="24" :lg="16">
<a-card title="个人信息" class="info-card">
<a-descriptions :column="{ xs: 1, sm: 2 }" bordered>
<a-descriptions-item label="用户名">{{ profile.username }}</a-descriptions-item>
<a-descriptions-item label="昵称">{{ profile.nickname }}</a-descriptions-item>
<a-descriptions-item label="邮箱">
{{ profile.email }}
<a-tag v-if="securitySettings.emailVerified" color="success" size="small">已认证</a-tag>
</a-descriptions-item>
<a-descriptions-item label="手机号">
{{ profile.phone }}
<a-tag v-if="securitySettings.phoneVerified" color="success" size="small">已认证</a-tag>
</a-descriptions-item>
<a-descriptions-item label="部门">{{ profile.department }}</a-descriptions-item>
<a-descriptions-item label="职位">{{ profile.position }}</a-descriptions-item>
<a-descriptions-item label="入职日期">{{ profile.joinDate }}</a-descriptions-item>
<a-descriptions-item label="上次登录">{{ profile.lastLogin }}</a-descriptions-item>
</a-descriptions>
<div class="mt-4">
<a-button type="primary">编辑资料</a-button>
</div>
</a-card>
</a-col>
<a-col :xs="24" :lg="8">
<a-card title="头像设置" class="avatar-card">
<div class="avatar-upload">
<a-avatar :size="100" :src="profile.avatar">
<template #icon><UserOutlined /></template>
</a-avatar>
<div class="mt-4">
<a-button size="small">更换头像</a-button>
<p class="text-xs text-gray-400 mt-2">支持 JPGPNG 格式文件小于 2MB</p>
</div>
</div>
</a-card>
</a-col>
</a-row>
</a-tab-pane>
<!-- 账号安全 -->
<a-tab-pane key="security" tab="账号安全">
<a-row :gutter="24">
<a-col :xs="24" :lg="16">
<!-- 登录密码 -->
<a-card title="登录密码" class="security-card mb-4">
<div class="security-item">
<div class="security-info">
<div class="security-title">登录密码</div>
<div class="security-desc">上次修改于 {{ securitySettings.loginPwdChanged }}</div>
</div>
<a-button>修改密码</a-button>
</div>
</a-card>
<!-- 邮箱绑定 -->
<a-card title="邮箱绑定" class="security-card mb-4">
<div class="security-item">
<div class="security-info">
<div class="security-title">{{ profile.email }}</div>
<div class="security-desc">
<a-tag v-if="securitySettings.emailVerified" color="success" size="small">已验证</a-tag>
<span v-else class="text-orange-500">未验证</span>
</div>
</div>
<a-button type="primary" ghost>更换邮箱</a-button>
</div>
</a-card>
<!-- 手机绑定 -->
<a-card title="手机绑定" class="security-card mb-4">
<div class="security-item">
<div class="security-info">
<div class="security-title">{{ profile.phone }}</div>
<div class="security-desc">
<a-tag v-if="securitySettings.phoneVerified" color="success" size="small">已验证</a-tag>
</div>
</div>
<a-button type="primary" ghost>更换手机</a-button>
</div>
</a-card>
<!-- 两步验证 -->
<a-card title="两步验证" class="security-card">
<div class="security-item">
<div class="security-info">
<div class="security-title">开启两步验证</div>
<div class="security-desc">启用后登录需输入手机验证码提升账号安全</div>
</div>
<a-switch v-model:checked="securitySettings.twoFactorEnabled" />
</div>
</a-card>
</a-col>
</a-row>
</a-tab-pane>
<!-- 登录历史 -->
<a-tab-pane key="history" tab="登录历史">
<a-card title="最近登录记录">
<a-table :dataSource="loginHistory" :pagination="false" rowKey="time" size="small">
<a-table-column title="登录时间" dataIndex="time" width="180" />
<a-table-column title="IP 地址" dataIndex="ip" width="140" />
<a-table-column title="设备" dataIndex="device" />
<a-table-column title="位置" dataIndex="location" width="100" />
<a-table-column title="状态" dataIndex="status" width="80" align="center">
<template #default="{ text }">
<a-tag :color="text === '成功' ? 'success' : 'error'">{{ text }}</a-tag>
</template>
</a-table-column>
</a-table>
</a-card>
</a-tab-pane>
</a-tabs>
</div>
</template>
<style scoped>
.account-page {
max-width: 1200px;
}
.account-tabs :deep(.ant-tabs-nav) {
margin-bottom: 20px;
}
.info-card,
.security-card,
.avatar-card {
border-radius: 8px;
}
.avatar-upload {
display: flex;
flex-direction: column;
align-items: center;
padding: 20px;
}
.security-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px 0;
}
.security-info {
flex: 1;
}
.security-title {
font-size: 15px;
font-weight: 500;
color: #1f2937;
margin-bottom: 4px;
}
.security-desc {
font-size: 13px;
color: #6b7280;
}
.text-xs {
font-size: 12px;
}
.text-gray-400 {
color: #9ca3af;
}
.text-orange-500 {
color: #f97316;
}
.mt-4 {
margin-top: 16px;
}
.mb-4 {
margin-bottom: 16px;
}
</style>

View File

@@ -1,415 +1,234 @@
<template> <template>
<div class="developers-page"> <div class="developers-page">
<div class="page-header"> <a-card :bordered="false">
<div> <template #title>开发者管理</template>
<h2 class="page-title">🧑💻 开发者管理</h2>
<p class="page-desc">管理平台上有应用发布记录的开发者账号</p>
</div>
<a-space>
<a-button @click="loadData" :loading="loading">
<template #icon><ReloadOutlined /></template>
刷新
</a-button>
</a-space>
</div>
<!-- 统计卡片 --> <a-tabs v-model:activeKey="activeTab">
<a-row :gutter="[16, 16]" class="mb-6"> <!-- 开发者申请 -->
<a-col :xs="12" :md="6" v-for="stat in stats" :key="stat.label"> <a-tab-pane key="apply" tab="开发者申请">
<div class="stat-card" :class="stat.color">
<div class="stat-icon">{{ stat.icon }}</div>
<div class="stat-info">
<div class="stat-value">{{ stat.value }}</div>
<div class="stat-label">{{ stat.label }}</div>
</div>
</div>
</a-col>
</a-row>
<!-- 筛选栏 -->
<div class="filter-bar"> <div class="filter-bar">
<a-radio-group v-model:value="filterType" button-style="solid" @change="handleFilterChange"> <a-input-search v-model:value="searchKeyword" placeholder="搜索申请人、企业..." style="width: 280px" allow-clear />
<a-radio-button :value="2">开发者用户</a-radio-button> <a-select v-model:value="filterType" placeholder="申请类型" style="width: 150px" allow-clear>
<a-radio-button :value="null">全部用户</a-radio-button> <a-select-option value="api">API 开发者</a-select-option>
</a-radio-group> <a-select-option value="plugin">插件开发者</a-select-option>
<a-input-search <a-select-option value="template">模板开发者</a-select-option>
v-model:value="searchKeyword" </a-select>
placeholder="搜索用户名/昵称/手机号" <a-button @click="resetFilter">重置</a-button>
style="width: 240px"
allow-clear
@search="handleSearch"
/>
</div> </div>
<!-- 开发者列表 --> <a-table :columns="applyColumns" :data-source="applyData" row-key="id">
<div class="panel">
<div class="panel-header">
<span class="panel-title">🧑💻 用户列表</span>
<a-tag color="blue"> {{ pagination.total }} </a-tag>
</div>
<a-table
:columns="columns"
:data-source="developers"
:loading="loading"
:pagination="pagination"
row-key="userId"
@change="handleTableChange"
size="middle"
>
<template #bodyCell="{ column, record }"> <template #bodyCell="{ column, record }">
<!-- 开发者信息 --> <template v-if="column.key === 'applicant'">
<template v-if="column.key === 'devInfo'"> <div class="user-cell">
<div class="dev-info-cell"> <a-avatar style="background: linear-gradient(135deg, #11998e, #38ef7d)" :size="36">{{ record.name[0] }}</a-avatar>
<a-avatar :size="38" :src="record.avatar || record.avatarUrl"> <div>
<template #icon><UserOutlined /></template> <p class="name-text">{{ record.name }}</p>
</a-avatar> <p class="sub-text">{{ record.email }}</p>
<div class="dev-info-text">
<div class="dev-name">{{ record.nickname || record.username || '-' }}</div>
<div class="dev-sub" v-if="record.username">@{{ record.username }}</div>
<div class="dev-sub" v-if="record.phone || record.mobile">
📱 {{ record.phone || record.mobile }}
</div>
</div> </div>
</div> </div>
</template> </template>
<template v-else-if="column.key === 'type'">
<!-- 用户类型 --> <a-tag :color="typeColor[record.type]">{{ typeMap[record.type] }}</a-tag>
<template v-if="column.key === 'userType'">
<a-tag v-if="record.type === 2" color="purple">开发者</a-tag>
<a-tag v-else-if="record.type === 1" color="blue">企业用户</a-tag>
<a-tag v-else color="default">普通用户</a-tag>
</template> </template>
<template v-else-if="column.key === 'status'">
<!-- 应用数量 --> <a-badge :status="statusBadge[record.status]" :text="statusText[record.status]" />
<template v-if="column.key === 'appCount'"> </template>
<template v-else-if="column.key === 'actions'">
<a-space> <a-space>
<a-tag color="blue">{{ appCountMap[record.userId!] ?? 0 }} 个应用</a-tag> <a-button v-if="record.status === 'pending'" type="primary" size="small" @click="handleApprove(record)">审核</a-button>
<a-tag color="success" v-if="publishedCountMap[record.userId!]"> <a-button type="link" size="small" @click="handleViewApply(record)">详情</a-button>
{{ publishedCountMap[record.userId!] }} 已上架
</a-tag>
</a-space>
</template>
<!-- 注册时间 -->
<template v-if="column.key === 'createTime'">
<span class="text-sm text-gray">{{ record.createTime?.substring(0, 10) || '-' }}</span>
</template>
<!-- 状态 -->
<template v-if="column.key === 'status'">
<a-tag :color="record.status === 0 ? 'success' : 'error'">
{{ record.status === 0 ? '正常' : '已冻结' }}
</a-tag>
</template>
<!-- 操作 -->
<template v-if="column.key === 'action'">
<a-space>
<a-button type="link" size="small" @click="handleViewDev(record)">查看应用</a-button>
<a-popconfirm
v-if="record.type !== 2"
title="确认将该用户设为开发者?"
ok-text="确认"
cancel-text="取消"
@confirm="handleSetDeveloper(record, 2)"
>
<a-button type="link" size="small">设为开发者</a-button>
</a-popconfirm>
<a-popconfirm
v-else
title="确认取消该用户的开发者资质?"
ok-text="确认"
cancel-text="取消"
@confirm="handleSetDeveloper(record, 0)"
>
<a-button type="link" size="small" danger>取消资质</a-button>
</a-popconfirm>
</a-space> </a-space>
</template> </template>
</template> </template>
</a-table> </a-table>
</a-tab-pane>
<!-- 权限审核 -->
<a-tab-pane key="audit" tab="权限审核">
<div class="filter-bar">
<a-select v-model:value="auditFilterType" placeholder="权限类型" style="width: 150px" allow-clear>
<a-select-option value="api">API 权限</a-select-option>
<a-select-option value="plugin">插件权限</a-select-option>
<a-select-option value="admin">管理权限</a-select-option>
</a-select>
<a-button type="primary" @click="auditModalVisible = true">新增审核</a-button>
</div> </div>
<!-- 开发者应用列表弹窗 --> <a-table :columns="auditColumns" :data-source="auditData" row-key="id">
<a-modal <template #bodyCell="{ column, record }">
v-model:open="showAppsModal" <template v-if="column.key === 'type'">
:title="`${currentDev?.nickname || currentDev?.username || '开发者'} 的应用`" <a-tag :color="auditTypeColor[record.type]">{{ auditTypeMap[record.type] }}</a-tag>
width="780px"
:footer="null"
>
<div v-if="loadingApps" class="modal-spin">
<a-spin />
</div>
<template v-else>
<a-empty v-if="devApps.length === 0" description="该开发者暂无应用" />
<div v-else class="dev-apps-grid">
<div v-for="app in devApps" :key="app.productId" class="dev-app-card">
<div class="dev-app-header">
<img v-if="app.icon" :src="app.icon" class="dev-app-icon" />
<div v-else class="dev-app-icon-placeholder" :style="{ background: iconBgColor(app.productName) }">
{{ (app.productName || 'A').charAt(0).toUpperCase() }}
</div>
<div class="dev-app-info">
<div class="dev-app-name">
{{ app.productName }}
<a-tag color="blue" style="margin-left:6px;font-size:11px">{{ APP_TYPE_NAME[app.appType ?? 10] || '网站' }}</a-tag>
</div>
<div class="dev-app-code">{{ app.productCode }}</div>
</div>
<a-tag :color="pubStatusColor(app.publishStatus)" style="margin-left:auto">
{{ pubStatusText(app.publishStatus) }}
</a-tag>
</div>
<div class="dev-app-desc">{{ app.description || '暂无简介' }}</div>
</div>
</div>
</template> </template>
<template v-else-if="column.key === 'level'">
<a-tag :color="levelColor[record.level]">{{ levelMap[record.level] }}</a-tag>
</template>
<template v-else-if="column.key === 'status'">
<a-badge :status="statusBadge[record.status]" :text="statusText[record.status]" />
</template>
<template v-else-if="column.key === 'actions'">
<a-space>
<a-button v-if="record.status === 'pending'" type="primary" size="small" @click="handleAudit(record)">审核</a-button>
<a-button type="link" size="small" @click="handleViewAudit(record)">详情</a-button>
</a-space>
</template>
</template>
</a-table>
</a-tab-pane>
</a-tabs>
</a-card>
<!-- 审核弹窗 -->
<a-modal
v-model:open="auditModalVisible"
:title="editingApply ? '审核开发者申请' : '权限审核'"
width="560px"
:confirm-loading="submitting"
@ok="handleSubmitAudit"
@cancel="auditModalVisible = false"
>
<a-alert v-if="editingApply" :message="`正在审核:${editingApply.name} (${editingApply.email})`" type="info" show-icon class="mb-4" />
<a-form :model="auditForm" :label-col="{ span: 6 }" :wrapper-col="{ span: 16 }">
<a-form-item label="申请人">
<a-input v-model:value="auditForm.name" />
</a-form-item>
<a-form-item label="权限类型">
<a-select v-model:value="auditForm.type">
<a-select-option value="api">API 权限</a-select-option>
<a-select-option value="plugin">插件权限</a-select-option>
<a-select-option value="admin">管理权限</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="权限等级">
<a-select v-model:value="auditForm.level">
<a-select-option value="basic">基础</a-select-option>
<a-select-option value="standard">标准</a-select-option>
<a-select-option value="advance">高级</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="审核结果">
<a-radio-group v-model:value="auditForm.result">
<a-radio value="approved">通过</a-radio>
<a-radio value="rejected">拒绝</a-radio>
</a-radio-group>
</a-form-item>
<a-form-item label="审核备注">
<a-textarea v-model:value="auditForm.remark" :rows="3" placeholder="选填" />
</a-form-item>
</a-form>
</a-modal> </a-modal>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ReloadOutlined, UserOutlined } from '@ant-design/icons-vue'
import { message } from 'ant-design-vue' import { message } from 'ant-design-vue'
import { getUserAppStats, pageAppProductAll } from '@/api/app/appProduct'
import { pageUsers, updateUser } from '@/api/system/user/index'
import type { AppProduct } from '@/api/app/appProduct/model'
import { APP_TYPE_NAME } from '@/api/app/appProduct/model'
import type { User } from '@/api/system/user/model'
definePageMeta({ layout: 'admin' }) definePageMeta({ layout: 'admin' })
useHead({ title: '开发者管理 - 平台管理' })
const loading = ref(false) const activeTab = ref('apply')
const loadingApps = ref(false)
const developers = ref<User[]>([])
const searchKeyword = ref('') const searchKeyword = ref('')
const filterType = ref<number | null>(2) // 默认只看开发者 const filterType = ref<string | undefined>()
const auditFilterType = ref<string | undefined>()
const auditModalVisible = ref(false)
const submitting = ref(false)
const editingApply = ref<any>(null)
// 应用数量映射 userId -> count const auditForm = reactive({
const appCountMap = ref<Record<number, number>>({}) name: '',
const publishedCountMap = ref<Record<number, number>>({}) type: 'api',
level: 'basic',
const showAppsModal = ref(false) result: 'approved',
const currentDev = ref<User | null>(null) remark: '',
const devApps = ref<AppProduct[]>([])
const pagination = reactive({
current: 1,
pageSize: 20,
total: 0,
showSizeChanger: true,
showQuickJumper: true,
}) })
const stats = reactive([ const typeMap: Record<string, string> = { api: 'API 开发者', plugin: '插件开发者', template: '模板开发者' }
{ icon: '🧑‍💻', label: '开发者总数', value: 0, color: 'blue' }, const typeColor: Record<string, string> = { api: 'blue', plugin: 'green', template: 'purple' }
{ icon: '📦', label: '应用总数', value: 0, color: 'green' }, const statusText: Record<string, string> = { pending: '待审核', approved: '已通过', rejected: '已拒绝' }
{ icon: '✅', label: '已上架应用', value: 0, color: 'orange' }, const statusBadge: Record<string, any> = { pending: 'warning', approved: 'success', rejected: 'error' }
{ icon: '⏳', label: '待审核', value: 0, color: 'red' }, const auditTypeMap: Record<string, string> = { api: 'API 权限', plugin: '插件权限', admin: '管理权限' }
]) const auditTypeColor: Record<string, string> = { api: 'blue', plugin: 'green', admin: 'purple' }
const levelMap: Record<string, string> = { basic: '基础', standard: '标准', advance: '高级' }
const levelColor: Record<string, string> = { basic: 'default', standard: 'blue', advance: 'red' }
const columns = [ const applyColumns = [
{ title: '用户', key: 'devInfo', width: 240 }, { title: '申请人', key: 'applicant', width: 220 },
{ title: '类型', key: 'userType', width: 100 }, { title: '企业', dataIndex: 'enterprise', key: 'enterprise', width: 160 },
{ title: '应用数量', key: 'appCount', width: 180 }, { title: '申请类型', key: 'type', width: 110 },
{ title: '注册时间', key: 'createTime', width: 120 }, { title: '申请理由', dataIndex: 'reason', key: 'reason', ellipsis: true },
{ title: '状态', key: 'status', width: 90 }, { title: '申请时间', dataIndex: 'date', key: 'date', width: 120 },
{ title: '操作', key: 'action', width: 160 }, { title: '状态', key: 'status', width: 100 },
{ title: '操作', key: 'actions', width: 140, fixed: 'right' },
] ]
async function loadData() { const applyData = ref([
loading.value = true { id: 1, name: '陈志远', email: 'chenzy@example.com', enterprise: '腾云科技', type: 'api', reason: '需要调用平台 API 实现数据对接功能', date: '2026-04-08', status: 'pending' },
try { { id: 2, name: '林晓东', email: 'linxd@example.com', enterprise: '华创数据', type: 'plugin', reason: '希望开发企业级插件产品', date: '2026-04-07', status: 'approved' },
const res = await pageUsers({ { id: 3, name: '周文博', email: 'zhouwb@example.com', enterprise: '云智科技', type: 'api', reason: '进行系统集成开发', date: '2026-04-06', status: 'approved' },
page: pagination.current, { id: 4, name: '吴浩宇', email: 'wuhao@example.com', enterprise: '数智科技', type: 'template', reason: '发布企业模板到应用市场', date: '2026-04-05', status: 'pending' },
limit: pagination.pageSize, { id: 5, name: '郑海峰', email: 'zhenghf@example.com', enterprise: '腾云科技', type: 'api', reason: '需要高级 API 权限进行批量操作', date: '2026-04-04', status: 'approved' },
keywords: searchKeyword.value || undefined, ])
type: filterType.value ?? undefined,
})
developers.value = res?.list || []
pagination.total = res?.count || 0
stats[0].value = pagination.total
// 加载完用户后,单次请求批量加载应用数量
loadAppCounts()
} catch {
message.error('加载用户列表失败')
} finally {
loading.value = false
}
}
async function loadAppCounts() { const auditColumns = [
if (!developers.value.length) return { title: '申请人', dataIndex: 'name', key: 'name', width: 140 },
try { { title: '企业', dataIndex: 'enterprise', key: 'enterprise', width: 140 },
// 单次 POST 请求,一条 SQL 批量统计所有用户的应用数 { title: '权限类型', key: 'type', width: 110 },
const userIds = developers.value.map(u => u.userId!).filter(Boolean) { title: '权限等级', key: 'level', width: 100 },
const rows = await getUserAppStats(userIds) { title: '申请时间', dataIndex: 'date', key: 'date', width: 120 },
const countMap: Record<number, number> = {} { title: '审核人', dataIndex: 'auditor', key: 'auditor', width: 100 },
const pubMap: Record<number, number> = {} { title: '状态', key: 'status', width: 100 },
let totalApps = 0 { title: '操作', key: 'actions', width: 140, fixed: 'right' },
let totalPublished = 0 ]
for (const row of rows) {
const uid = Number(row.userId)
const total = Number(row.totalCount) || 0
const pub = Number(row.publishedCount) || 0
countMap[uid] = total
if (pub > 0) pubMap[uid] = pub
totalApps += total
totalPublished += pub
}
appCountMap.value = countMap
publishedCountMap.value = pubMap
stats[1].value = totalApps
stats[2].value = totalPublished
} catch { /* ignore */ }
// 异步加载全局统计(待审核数)
loadPendingCount()
}
async function loadPendingCount() { const auditData = ref([
try { { id: 1, name: '陈志远', enterprise: '腾云科技', type: 'api', level: 'standard', date: '2026-04-08', auditor: '李明', status: 'pending' },
const res = await pageAppProductAll({ current: 1, size: 1, publishStatus: 'pending_review' }) { id: 2, name: '吴浩宇', enterprise: '数智科技', type: 'plugin', level: 'advance', date: '2026-04-05', auditor: '李明', status: 'pending' },
stats[3].value = res?.count ?? 0 { id: 3, name: '林晓东', enterprise: '华创数据', type: 'plugin', level: 'standard', date: '2026-04-07', auditor: '李明', status: 'approved' },
} catch { /* ignore */ } { id: 4, name: '周文博', enterprise: '云智科技', type: 'api', level: 'basic', date: '2026-04-06', auditor: '李明', status: 'approved' },
} ])
function handleSearch() { const resetFilter = () => { searchKeyword.value = ''; filterType.value = undefined }
pagination.current = 1 const handleViewApply = (r: any) => message.info('查看申请:' + r.name)
loadData() const handleViewAudit = (r: any) => message.info('查看审核:' + r.name)
const handleApprove = (r: any) => { editingApply.value = r; Object.assign(auditForm, { name: r.name, type: r.type }); auditModalVisible.value = true }
const handleAudit = (r: any) => { editingApply.value = r; Object.assign(auditForm, { name: r.name, type: r.type }); auditModalVisible.value = true }
const handleSubmitAudit = async () => {
submitting.value = true
await new Promise((r) => setTimeout(r, 800))
message.success('审核提交成功')
auditModalVisible.value = false
submitting.value = false
editingApply.value = null
} }
function handleFilterChange() {
pagination.current = 1
loadData()
}
function handleTableChange(pag: any) {
pagination.current = pag.current
pagination.pageSize = pag.pageSize
loadData()
}
async function handleSetDeveloper(record: User, type: number) {
try {
await updateUser({ userId: record.userId, type })
record.type = type
message.success(type === 2 ? '已设为开发者用户' : '已取消开发者资质')
// 如果当前只展示开发者,取消后刷新列表
if (filterType.value === 2 && type !== 2) loadData()
} catch (e: any) {
message.error(e?.message || '操作失败')
}
}
async function handleViewDev(record: User) {
currentDev.value = record
showAppsModal.value = true
loadingApps.value = true
try {
const res = await pageAppProductAll({
current: 1,
size: 100,
userId: record.userId,
})
devApps.value = res?.list || []
} catch {
message.error('加载应用列表失败')
} finally {
loadingApps.value = false
}
}
function pubStatusColor(status?: string) {
const map: Record<string, string> = {
developing: 'default', pending_review: 'orange',
published: 'success', rejected: 'error', deprecated: 'default',
}
return map[status || ''] || 'default'
}
function pubStatusText(status?: string) {
const map: Record<string, string> = {
developing: '开发中', pending_review: '待审核',
published: '已上架', rejected: '已拒绝', deprecated: '已下架',
}
return map[status || ''] || '-'
}
const PALETTE = ['#4e6ef2', '#f4a261', '#e76f51', '#2a9d8f', '#e9c46a', '#457b9d']
function iconBgColor(name?: string) {
if (!name) return PALETTE[0]
let h = 0
for (let i = 0; i < name.length; i++) h = (h * 31 + name.charCodeAt(i)) & 0xffffffff
return PALETTE[Math.abs(h) % PALETTE.length]
}
onMounted(() => loadData())
</script> </script>
<style scoped> <style scoped>
.developers-page { min-height: 100%; }
.page-header {
display: flex; align-items: center;
justify-content: space-between; margin-bottom: 20px;
}
.page-title { font-size: 18px; font-weight: 700; color: #1f2937; margin: 0; }
.page-desc { font-size: 13px; color: #9ca3af; margin: 2px 0 0; }
.stat-card {
display: flex; align-items: center;
gap: 12px; padding: 16px;
border-radius: 12px; border: 2px solid transparent; transition: all 0.2s;
}
.stat-card:hover { transform: translateY(-2px); box-shadow: 0 4px 12px rgba(0,0,0,0.08); }
.stat-card.blue { background: #eff6ff; border-color: #dbeafe; }
.stat-card.green { background: #f0fdf4; border-color: #bbf7d0; }
.stat-card.orange { background: #fff7ed; border-color: #fed7aa; }
.stat-card.red { background: #fff1f2; border-color: #fecdd3; }
.stat-icon { font-size: 28px; flex-shrink: 0; }
.stat-value { font-size: 22px; font-weight: 700; color: rgba(0,0,0,0.85); line-height: 1.2; }
.stat-label { font-size: 12px; color: rgba(0,0,0,0.45); margin-top: 2px; }
.panel { background: #fff; border: 1px solid #f0f0f0; border-radius: 12px; overflow: hidden; }
.panel-header {
display: flex; align-items: center; justify-content: space-between;
padding: 14px 18px; border-bottom: 1px solid #f5f5f5; flex-wrap: wrap; gap: 10px;
}
.panel-title { font-size: 14px; font-weight: 600; color: rgba(0,0,0,0.85); }
.filter-bar { .filter-bar {
display: flex; align-items: center; justify-content: space-between; display: flex;
flex-wrap: wrap; gap: 12px; margin-bottom: 16px; align-items: center;
gap: 12px;
margin-bottom: 16px;
flex-wrap: wrap;
} }
.dev-info-cell { display: flex; align-items: center; gap: 10px; } .user-cell {
.dev-info-text { flex: 1; min-width: 0; } display: flex;
.dev-name { font-size: 14px; font-weight: 500; color: rgba(0,0,0,0.85); } align-items: center;
.dev-sub { font-size: 12px; color: rgba(0,0,0,0.45); margin-top: 1px; } gap: 10px;
/* 应用弹窗卡片 */
.dev-apps-grid { display: flex; flex-direction: column; gap: 12px; max-height: 520px; overflow-y: auto; padding-right: 4px; }
.dev-app-card {
border: 1px solid #f0f0f0; border-radius: 10px; padding: 14px;
transition: all 0.15s;
} }
.dev-app-card:hover { border-color: #d0d0ff; box-shadow: 0 2px 8px rgba(0,0,0,0.06); }
.dev-app-header { display: flex; align-items: center; gap: 12px; margin-bottom: 8px; }
.dev-app-icon { width: 40px; height: 40px; border-radius: 8px; object-fit: cover; }
.dev-app-icon-placeholder {
width: 40px; height: 40px; border-radius: 8px;
display: flex; align-items: center; justify-content: center;
font-size: 16px; font-weight: 600; color: #fff; flex-shrink: 0;
}
.dev-app-info { flex: 1; }
.dev-app-name { font-size: 14px; font-weight: 500; color: rgba(0,0,0,0.85); }
.dev-app-code { font-size: 12px; color: rgba(0,0,0,0.45); }
.dev-app-desc { font-size: 12px; color: rgba(0,0,0,0.45); padding-left: 52px; }
.modal-spin { display: flex; align-items: center; justify-content: center; min-height: 200px; } .name-text {
.text-sm { font-size: 12px; } font-weight: 500;
.text-gray { color: rgba(0,0,0,0.45); } color: #111827;
.mb-6 { margin-bottom: 24px; } margin: 0;
}
.sub-text {
font-size: 12px;
color: #9ca3af;
margin: 0;
}
.mb-4 {
margin-bottom: 16px;
}
</style> </style>

View File

@@ -0,0 +1,119 @@
<template>
<div class="developer-audit-page">
<a-card :bordered="false" title="权限审核">
<div class="filter-bar">
<a-select v-model:value="filterType" placeholder="权限类型" style="width: 150px" allow-clear>
<a-select-option value="api">API 权限</a-select-option>
<a-select-option value="plugin">插件权限</a-select-option>
<a-select-option value="admin">管理权限</a-select-option>
</a-select>
<a-select v-model:value="filterStatus" placeholder="审核状态" style="width: 140px" allow-clear>
<a-select-option value="pending">待审核</a-select-option>
<a-select-option value="approved">已通过</a-select-option>
<a-select-option value="rejected">已拒绝</a-select-option>
</a-select>
<a-button type="primary" @click="auditModalVisible = true">新增审核</a-button>
</div>
<a-table :columns="columns" :data-source="data" row-key="id">
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'type'">
<a-tag :color="typeColor[record.type]">{{ typeMap[record.type] }}</a-tag>
</template>
<template v-else-if="column.key === 'level'">
<a-tag :color="levelColor[record.level]">{{ levelMap[record.level] }}</a-tag>
</template>
<template v-else-if="column.key === 'status'">
<a-badge :status="statusBadge[record.status]" :text="statusText[record.status]" />
</template>
<template v-else-if="column.key === 'actions'">
<a-space>
<a-button v-if="record.status === 'pending'" type="primary" size="small" @click="handleAudit(record)">审核</a-button>
<a-button type="link" size="small" @click="handleView(record)">详情</a-button>
</a-space>
</template>
</template>
</a-table>
</a-card>
<a-modal v-model:open="auditModalVisible" title="权限审核" width="520px" @ok="handleSubmit">
<a-form :model="form" :label-col="{ span: 6 }" :wrapper-col="{ span: 16 }">
<a-form-item label="申请人">
<a-input v-model:value="form.name" />
</a-form-item>
<a-form-item label="权限类型">
<a-select v-model:value="form.type">
<a-select-option value="api">API 权限</a-select-option>
<a-select-option value="plugin">插件权限</a-select-option>
<a-select-option value="admin">管理权限</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="权限等级">
<a-select v-model:value="form.level">
<a-select-option value="basic">基础</a-select-option>
<a-select-option value="standard">标准</a-select-option>
<a-select-option value="advance">高级</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="审核结果">
<a-radio-group v-model:value="form.result">
<a-radio value="approved">通过</a-radio>
<a-radio value="rejected">拒绝</a-radio>
</a-radio-group>
</a-form-item>
<a-form-item label="审核备注">
<a-textarea v-model:value="form.remark" :rows="3" />
</a-form-item>
</a-form>
</a-modal>
</div>
</template>
<script setup lang="ts">
import { message } from 'ant-design-vue'
definePageMeta({ layout: 'admin' })
const filterType = ref<string | undefined>()
const filterStatus = ref<string | undefined>()
const auditModalVisible = ref(false)
const form = reactive({ name: '', type: 'api', level: 'basic', result: 'approved', remark: '' })
const typeMap: Record<string, string> = { api: 'API 权限', plugin: '插件权限', admin: '管理权限' }
const typeColor: Record<string, string> = { api: 'blue', plugin: 'green', admin: 'purple' }
const levelMap: Record<string, string> = { basic: '基础', standard: '标准', advance: '高级' }
const levelColor: Record<string, string> = { basic: 'default', standard: 'blue', advance: 'red' }
const statusText: Record<string, string> = { pending: '待审核', approved: '已通过', rejected: '已拒绝' }
const statusBadge: Record<string, any> = { pending: 'warning', approved: 'success', rejected: 'error' }
const columns = [
{ title: '申请人', dataIndex: 'name', key: 'name', width: 140 },
{ title: '企业', dataIndex: 'enterprise', key: 'enterprise', width: 160 },
{ title: '权限类型', key: 'type', width: 110 },
{ title: '权限等级', key: 'level', width: 100 },
{ title: '申请时间', dataIndex: 'date', key: 'date', width: 120 },
{ title: '审核人', dataIndex: 'auditor', key: 'auditor', width: 100 },
{ title: '状态', key: 'status', width: 100 },
{ title: '操作', key: 'actions', width: 140 },
]
const data = ref([
{ id: 1, name: '陈志远', enterprise: '腾云科技', type: 'api', level: 'standard', date: '2026-04-08', auditor: '李明', status: 'pending' },
{ id: 2, name: '吴浩宇', enterprise: '数智科技', type: 'plugin', level: 'advance', date: '2026-04-05', auditor: '李明', status: 'pending' },
{ id: 3, name: '林晓东', enterprise: '华创数据', type: 'plugin', level: 'standard', date: '2026-04-07', auditor: '李明', status: 'approved' },
{ id: 4, name: '周文博', enterprise: '云智科技', type: 'api', level: 'basic', date: '2026-04-06', auditor: '李明', status: 'approved' },
])
const handleAudit = (r: any) => { Object.assign(form, { name: r.name, type: r.type }); auditModalVisible.value = true }
const handleView = (r: any) => message.info('查看:' + r.name)
const handleSubmit = () => { auditModalVisible.value = false; message.success('审核成功') }
</script>
<style scoped>
.filter-bar {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 16px;
}
</style>

View File

@@ -0,0 +1,301 @@
<template>
<div class="enterprises-page">
<a-card :bordered="false">
<template #title>
<div class="card-title">
<span>企业管理</span>
<a-button type="primary" @click="handleAdd">
<template #icon><PlusOutlined /></template>
新增企业
</a-button>
</div>
</template>
<!-- 筛选栏 -->
<div class="filter-bar">
<a-input-search
v-model:value="searchKeyword"
placeholder="搜索企业名称、联系人..."
style="width: 280px"
allow-clear
@search="handleSearch"
/>
<a-select
v-model:value="filterStatus"
placeholder="状态筛选"
style="width: 140px"
allow-clear
>
<a-select-option value="active">已认证</a-select-option>
<a-select-option value="pending">待审核</a-select-option>
<a-select-option value="suspended">已停用</a-select-option>
</a-select>
<a-range-picker
v-model:value="dateRange"
style="width: 260px"
/>
<a-button @click="resetFilter">重置</a-button>
</div>
<!-- 数据表格 -->
<a-table
:columns="columns"
:data-source="tableData"
:pagination="pagination"
:loading="loading"
row-key="id"
@change="handleTableChange"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'name'">
<div class="enterprise-cell">
<a-avatar
style="background: linear-gradient(135deg, #667eea, #764ba2); flex-shrink: 0"
:size="36"
>
{{ record.name[0] }}
</a-avatar>
<div>
<p class="name-text">{{ record.name }}</p>
<p class="sub-text">ID: {{ record.id }}</p>
</div>
</div>
</template>
<template v-else-if="column.key === 'status'">
<a-badge
:status="record.status === 'active' ? 'success' : record.status === 'pending' ? 'warning' : 'error'"
:text="statusMap[record.status]"
/>
</template>
<template v-else-if="column.key === 'plan'">
<a-tag :color="planColor[record.plan]">{{ planMap[record.plan] }}</a-tag>
</template>
<template v-else-if="column.key === 'actions'">
<a-space>
<a-button type="link" size="small" @click="handleView(record)">详情</a-button>
<a-button type="link" size="small" @click="handleEdit(record)">编辑</a-button>
<a-dropdown>
<a-button type="link" size="small">更多</a-button>
<template #overlay>
<a-menu>
<a-menu-item key="suspend" @click="handleSuspend(record)">
{{ record.status === 'suspended' ? '启用' : '停用' }}
</a-menu-item>
<a-menu-item key="reset">重置密码</a-menu-item>
<a-menu-divider />
<a-menu-item key="delete" style="color: #ff4d4f">删除</a-menu-item>
</a-menu>
</template>
</a-dropdown>
</a-space>
</template>
</template>
</a-table>
</a-card>
<!-- 新增/编辑弹窗 -->
<a-modal
v-model:open="modalVisible"
:title="editingId ? '编辑企业' : '新增企业'"
width="640px"
:confirm-loading="submitting"
@ok="handleSubmit"
@cancel="modalVisible = false"
>
<a-form
ref="formRef"
:model="formState"
:label-col="{ span: 6 }"
:wrapper-col="{ span: 16 }"
>
<a-form-item label="企业名称" name="name" :rules="[{ required: true, message: '请输入企业名称' }]">
<a-input v-model:value="formState.name" placeholder="请输入企业名称" />
</a-form-item>
<a-form-item label="联系人" name="contact" :rules="[{ required: true, message: '请输入联系人' }]">
<a-input v-model:value="formState.contact" placeholder="请输入联系人姓名" />
</a-form-item>
<a-form-item label="联系电话" name="phone">
<a-input v-model:value="formState.phone" placeholder="请输入联系电话" />
</a-form-item>
<a-form-item label="企业邮箱" name="email">
<a-input v-model:value="formState.email" placeholder="请输入企业邮箱" />
</a-form-item>
<a-form-item label="套餐等级" name="plan">
<a-select v-model:value="formState.plan">
<a-select-option value="basic">基础版</a-select-option>
<a-select-option value="standard">标准版</a-select-option>
<a-select-option value="enterprise">企业版</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="状态" name="status">
<a-select v-model:value="formState.status">
<a-select-option value="active">已认证</a-select-option>
<a-select-option value="pending">待审核</a-select-option>
<a-select-option value="suspended">已停用</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="备注" name="remark">
<a-textarea v-model:value="formState.remark" :rows="3" placeholder="选填" />
</a-form-item>
</a-form>
</a-modal>
</div>
</template>
<script setup lang="ts">
import { PlusOutlined } from '@ant-design/icons-vue'
import { message } from 'ant-design-vue'
import type { TableProps } from 'ant-design-vue'
definePageMeta({ layout: 'admin' })
const searchKeyword = ref('')
const filterStatus = ref<string | undefined>()
const dateRange = ref<[any, any] | null>(null)
const loading = ref(false)
const modalVisible = ref(false)
const editingId = ref<number | null>(null)
const submitting = ref(false)
const formRef = ref()
const formState = reactive({
name: '',
contact: '',
phone: '',
email: '',
plan: 'basic',
status: 'pending',
remark: '',
})
const statusMap: Record<string, string> = {
active: '已认证',
pending: '待审核',
suspended: '已停用',
}
const planMap: Record<string, string> = {
basic: '基础版',
standard: '标准版',
enterprise: '企业版',
}
const planColor: Record<string, string> = {
basic: 'default',
standard: 'blue',
enterprise: 'purple',
}
const columns = [
{ title: '企业信息', key: 'name', width: 260 },
{ title: '联系人', dataIndex: 'contact', key: 'contact', width: 120 },
{ title: '联系电话', dataIndex: 'phone', key: 'phone', width: 140 },
{ title: '套餐', key: 'plan', width: 100 },
{ title: '状态', key: 'status', width: 100 },
{ title: '注册时间', dataIndex: 'createdAt', key: 'createdAt', width: 120 },
{ title: '操作', key: 'actions', width: 200, fixed: 'right' },
]
const tableData = ref([
{ id: 1001, name: '深圳市腾云科技有限公司', contact: '李明', phone: '13800138001', plan: 'enterprise', status: 'pending', createdAt: '2026-04-08' },
{ id: 1002, name: '杭州智联网络技术有限公司', contact: '王芳', phone: '13800138002', plan: 'standard', status: 'active', createdAt: '2026-04-07' },
{ id: 1003, name: '北京华创数据服务有限公司', contact: '张伟', phone: '13800138003', plan: 'enterprise', status: 'active', createdAt: '2026-04-06' },
{ id: 1004, name: '广州云智科技有限公司', contact: '陈静', phone: '13800138004', plan: 'basic', status: 'pending', createdAt: '2026-04-05' },
{ id: 1005, name: '上海数智科技有限公司', contact: '刘强', phone: '13800138005', plan: 'standard', status: 'active', createdAt: '2026-04-04' },
{ id: 1006, name: '成都万物互联有限公司', contact: '赵丽', phone: '13800138006', plan: 'basic', status: 'suspended', createdAt: '2026-04-03' },
{ id: 1007, name: '武汉云帆科技有限公司', contact: '孙磊', phone: '13800138007', plan: 'standard', status: 'active', createdAt: '2026-04-02' },
])
const pagination = reactive({
current: 1,
pageSize: 10,
total: 7,
})
const handleSearch = () => {
message.info('搜索:' + searchKeyword.value)
}
const resetFilter = () => {
searchKeyword.value = ''
filterStatus.value = undefined
dateRange.value = null
}
const handleTableChange: TableProps['onChange'] = (pag) => {
pagination.current = pag.current || 1
pagination.pageSize = pag.pageSize || 10
}
const handleAdd = () => {
editingId.value = null
Object.assign(formState, { name: '', contact: '', phone: '', email: '', plan: 'basic', status: 'pending', remark: '' })
modalVisible.value = true
}
const handleEdit = (record: any) => {
editingId.value = record.id
Object.assign(formState, { ...record })
modalVisible.value = true
}
const handleView = (record: any) => {
message.info('查看企业详情:' + record.name)
}
const handleSuspend = (record: any) => {
const action = record.status === 'suspended' ? '启用' : '停用'
message.success(`${action}成功`)
}
const handleSubmit = async () => {
try {
await formRef.value.validate()
submitting.value = true
await new Promise((r) => setTimeout(r, 800))
message.success(editingId.value ? '编辑成功' : '新增成功')
modalVisible.value = false
submitting.value = false
} catch {
submitting.value = false
}
}
</script>
<style scoped>
.card-title {
display: flex;
align-items: center;
justify-content: space-between;
}
.filter-bar {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 16px;
flex-wrap: wrap;
}
.enterprise-cell {
display: flex;
align-items: center;
gap: 10px;
}
.name-text {
font-weight: 500;
color: #111827;
margin: 0;
line-height: 1.4;
}
.sub-text {
font-size: 12px;
color: #9ca3af;
margin: 0;
}
</style>

View File

@@ -0,0 +1,78 @@
<template>
<div class="enterprise-detail">
<a-card :bordered="false">
<template #title>
<a-space>
<a-button type="text" @click="navigateTo('/admin/enterprises')">
<LeftOutlined /> 返回
</a-button>
<span>企业详情</span>
</a-space>
</template>
<a-descriptions bordered :column="2">
<a-descriptions-item label="企业名称">深圳市腾云科技有限公司</a-descriptions-item>
<a-descriptions-item label="企业ID">ENT-20260408-001</a-descriptions-item>
<a-descriptions-item label="联系人">李明</a-descriptions-item>
<a-descriptions-item label="联系电话">138****8001</a-descriptions-item>
<a-descriptions-item label="企业邮箱">liming@tengyun.com</a-descriptions-item>
<a-descriptions-item label="套餐等级">
<a-tag color="purple">企业版</a-tag>
</a-descriptions-item>
<a-descriptions-item label="认证状态">
<a-badge status="success" text="已认证" />
</a-descriptions-item>
<a-descriptions-item label="注册时间">2026-04-08 10:30</a-descriptions-item>
<a-descriptions-item label="营业执照">
<a-image :width="120" src="https://via.placeholder.com/120x80?text=执照" />
</a-descriptions-item>
<a-descriptions-item label="备注">-</a-descriptions-item>
</a-descriptions>
</a-card>
<a-row :gutter="[16, 16]" class="mt-4">
<a-col :xs="24" :xl="12">
<a-card title="成员信息" :bordered="false">
<a-list :data-source="members">
<template #renderItem="{ item }">
<a-list-item>
<a-list-item-meta>
<template #title>{{ item.name }}</template>
<template #description>{{ item.role }} · {{ item.email }}</template>
<template #avatar>
<a-avatar>{{ item.name[0] }}</a-avatar>
</template>
</a-list-item-meta>
</a-list-item>
</template>
</a-list>
</a-card>
</a-col>
<a-col :xs="24" :xl="12">
<a-card title="账单信息" :bordered="false">
<a-descriptions :column="1" size="small">
<a-descriptions-item label="账户余额">¥ 15,680.00</a-descriptions-item>
<a-descriptions-item label="本月消费">¥ 12,800.00</a-descriptions-item>
<a-descriptions-item label="累计充值">¥ 100,000.00</a-descriptions-item>
</a-descriptions>
</a-card>
</a-col>
</a-row>
</div>
</template>
<script setup lang="ts">
import { LeftOutlined } from '@ant-design/icons-vue'
definePageMeta({ layout: 'admin' })
const members = [
{ name: '李明', role: '管理员', email: 'liming@tengyun.com' },
{ name: '王芳', role: '财务', email: 'wangfang@tengyun.com' },
{ name: '张伟', role: '开发', email: 'zhangwei@tengyun.com' },
]
</script>
<style scoped>
.mt-4 { margin-top: 16px; }
</style>

267
app/pages/admin/finance.vue Normal file
View File

@@ -0,0 +1,267 @@
<template>
<div class="finance-page">
<!-- 统计概览 -->
<a-row :gutter="[16, 16]" class="mb-6">
<a-col :xs="24" :sm="12" :xl="6">
<a-card class="fin-stat-card" :bordered="false">
<div class="fin-stat-inner">
<div>
<p class="fin-label">本月收入</p>
<p class="fin-value">¥ 287.5 </p>
<p class="fin-trend up"><ArrowUpOutlined /> 15.3%</p>
</div>
<div class="fin-icon green"><AccountBookOutlined /></div>
</div>
</a-card>
</a-col>
<a-col :xs="24" :sm="12" :xl="6">
<a-card class="fin-stat-card" :bordered="false">
<div class="fin-stat-inner">
<div>
<p class="fin-label">待结算</p>
<p class="fin-value">¥ 43.2 </p>
<p class="fin-trend neutral">平稳</p>
</div>
<div class="fin-icon blue"><DollarOutlined /></div>
</div>
</a-card>
</a-col>
<a-col :xs="24" :sm="12" :xl="6">
<a-card class="fin-stat-card" :bordered="false">
<div class="fin-stat-inner">
<div>
<p class="fin-label">充值总额</p>
<p class="fin-value">¥ 1,856.3 </p>
<p class="fin-trend up"><ArrowUpOutlined /> 8.6%</p>
</div>
<div class="fin-icon purple"><PayCircleOutlined /></div>
</div>
</a-card>
</a-col>
<a-col :xs="24" :sm="12" :xl="6">
<a-card class="fin-stat-card" :bordered="false">
<div class="fin-stat-inner">
<div>
<p class="fin-label">退款笔数</p>
<p class="fin-value">12 </p>
<p class="fin-trend down"><ArrowDownOutlined /> 2 </p>
</div>
<div class="fin-icon orange"><ExclamationCircleOutlined /></div>
</div>
</a-card>
</a-col>
</a-row>
<a-card :bordered="false">
<template #title>账单管理</template>
<a-tabs v-model:activeKey="activeTab">
<!-- 账单列表 -->
<a-tab-pane key="bills" tab="账单列表">
<div class="filter-bar">
<a-input-search v-model:value="searchKeyword" placeholder="企业名称、订单号..." style="width: 280px" allow-clear @search="handleSearch" />
<a-select v-model:value="filterBillType" placeholder="账单类型" style="width: 140px" allow-clear>
<a-select-option value="recharge">充值</a-select-option>
<a-select-option value="consume">消费</a-select-option>
<a-select-option value="refund">退款</a-select-option>
</a-select>
<a-select v-model:value="filterBillStatus" placeholder="支付状态" style="width: 140px" allow-clear>
<a-select-option value="paid">已支付</a-select-option>
<a-select-option value="pending">待支付</a-select-option>
<a-select-option value="failed">已失败</a-select-option>
</a-select>
<a-range-picker v-model:value="dateRange" style="width: 260px" />
<a-button @click="resetFilter">重置</a-button>
</div>
<a-table :columns="billColumns" :data-source="billData" row-key="id" :pagination="pagination">
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'enterprise'">
<div class="enterprise-cell">
<a-avatar style="background: linear-gradient(135deg, #667eea, #764ba2); flex-shrink: 0" :size="32">{{ record.enterprise[0] }}</a-avatar>
<span>{{ record.enterprise }}</span>
</div>
</template>
<template v-else-if="column.key === 'amount'">
<span :class="record.type === 'refund' ? 'refund-amount' : record.type === 'consume' ? 'consume-amount' : ''">
{{ record.type === 'refund' ? '-' : record.type === 'consume' ? '-' : '+' }}¥ {{ record.amount.toLocaleString() }}
</span>
</template>
<template v-else-if="column.key === 'billType'">
<a-tag :color="billTypeColor[record.billType]">{{ billTypeMap[record.billType] }}</a-tag>
</template>
<template v-else-if="column.key === 'status'">
<a-badge :status="billStatusBadge[record.status]" :text="billStatusMap[record.status]" />
</template>
<template v-else-if="column.key === 'actions'">
<a-space>
<a-button type="link" size="small" @click="handleViewBill(record)">详情</a-button>
<a-button v-if="record.status === 'pending'" type="link" size="small" @click="handleRemind(record)">催款</a-button>
</a-space>
</template>
</template>
</a-table>
</a-tab-pane>
<!-- 充值记录 -->
<a-tab-pane key="recharge" tab="充值管理">
<div class="filter-bar">
<a-input-search v-model:value="rechargeKeyword" placeholder="企业名称、充值账号..." style="width: 280px" allow-clear />
<a-select v-model:value="rechargeChannel" placeholder="支付渠道" style="width: 140px" allow-clear>
<a-select-option value="alipay">支付宝</a-select-option>
<a-select-option value="wechat">微信支付</a-select-option>
<a-select-option value="bank">银行转账</a-select-option>
</a-select>
<a-button type="primary" @click="rechargeModalVisible = true">
<template #icon><PlusOutlined /></template>
手动充值
</a-button>
</div>
<a-table :columns="rechargeColumns" :data-source="rechargeData" row-key="id">
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'enterprise'">
<span>{{ record.enterprise }}</span>
</template>
<template v-else-if="column.key === 'amount'">
<span class="income-amount">+ ¥ {{ record.amount.toLocaleString() }}</span>
</template>
<template v-else-if="column.key === 'channel'">
<a-tag>{{ channelMap[record.channel] }}</a-tag>
</template>
<template v-else-if="column.key === 'status'">
<a-badge :status="billStatusBadge[record.status]" :text="billStatusMap[record.status]" />
</template>
<template v-else-if="column.key === 'actions'">
<a-button type="link" size="small" @click="handleViewRecharge(record)">凭证</a-button>
</template>
</template>
</a-table>
</a-tab-pane>
</a-tabs>
</a-card>
<!-- 手动充值弹窗 -->
<a-modal v-model:open="rechargeModalVisible" title="手动充值" width="480px" @ok="handleRechargeSubmit">
<a-form :model="rechargeForm" :label-col="{ span: 6 }" :wrapper-col="{ span: 16 }">
<a-form-item label="企业名称" :rules="[{ required: true, message: '请输入企业名称' }]">
<a-input v-model:value="rechargeForm.enterprise" placeholder="请输入企业名称" />
</a-form-item>
<a-form-item label="充值金额" :rules="[{ required: true, message: '请输入充值金额' }]">
<a-input-number v-model:value="rechargeForm.amount" :min="0" :precision="2" style="width: 100%" placeholder="请输入金额" />
</a-form-item>
<a-form-item label="支付渠道">
<a-select v-model:value="rechargeForm.channel">
<a-select-option value="manual">手动充值</a-select-option>
<a-select-option value="bank">银行转账</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="备注">
<a-textarea v-model:value="rechargeForm.remark" :rows="2" />
</a-form-item>
</a-form>
</a-modal>
</div>
</template>
<script setup lang="ts">
import { ArrowUpOutlined, ArrowDownOutlined, AccountBookOutlined, DollarOutlined, PlusOutlined, ExclamationCircleOutlined, PayCircleOutlined } from '@ant-design/icons-vue'
import { message } from 'ant-design-vue'
definePageMeta({ layout: 'admin' })
const activeTab = ref('bills')
const searchKeyword = ref('')
const rechargeKeyword = ref('')
const filterBillType = ref<string | undefined>()
const filterBillStatus = ref<string | undefined>()
const rechargeChannel = ref<string | undefined>()
const dateRange = ref<[any, any] | null>(null)
const rechargeModalVisible = ref(false)
const rechargeForm = reactive({ enterprise: '', amount: 0, channel: 'manual', remark: '' })
const billTypeMap: Record<string, string> = { recharge: '充值', consume: '消费', refund: '退款' }
const billTypeColor: Record<string, string> = { recharge: 'green', consume: 'blue', refund: 'orange' }
const billStatusMap: Record<string, string> = { paid: '已支付', pending: '待支付', failed: '已失败' }
const billStatusBadge: Record<string, any> = { paid: 'success', pending: 'warning', failed: 'error' }
const channelMap: Record<string, string> = { alipay: '支付宝', wechat: '微信支付', bank: '银行转账', manual: '手动' }
const billColumns = [
{ title: '企业', key: 'enterprise', width: 200 },
{ title: '账单类型', key: 'billType', width: 100 },
{ title: '金额', key: 'amount', width: 140 },
{ title: '订单号', dataIndex: 'orderNo', key: 'orderNo', width: 180 },
{ title: '支付状态', key: 'status', width: 100 },
{ title: '创建时间', dataIndex: 'createdAt', key: 'createdAt', width: 160 },
{ title: '操作', key: 'actions', width: 120, fixed: 'right' },
]
const billData = ref([
{ id: 1, enterprise: '腾云科技有限公司', type: 'consume', billType: 'consume', amount: 12800, orderNo: 'ORD20260408001', status: 'paid', createdAt: '2026-04-08 10:30' },
{ id: 2, enterprise: '华创数据服务有限公司', type: 'recharge', billType: 'recharge', amount: 50000, orderNo: 'ORD20260408002', status: 'paid', createdAt: '2026-04-08 09:15' },
{ id: 3, enterprise: '云智科技有限公司', type: 'consume', billType: 'consume', amount: 6800, orderNo: 'ORD20260407003', status: 'pending', createdAt: '2026-04-07 16:20' },
{ id: 4, enterprise: '数智科技有限公司', type: 'refund', billType: 'refund', amount: 3200, orderNo: 'ORD20260406004', status: 'paid', createdAt: '2026-04-06 14:00' },
{ id: 5, enterprise: '万物互联有限公司', type: 'recharge', billType: 'recharge', amount: 100000, orderNo: 'ORD20260405005', status: 'paid', createdAt: '2026-04-05 11:00' },
{ id: 6, enterprise: '云帆科技有限公司', type: 'consume', billType: 'consume', amount: 9200, orderNo: 'ORD20260403006', status: 'failed', createdAt: '2026-04-03 09:30' },
])
const rechargeColumns = [
{ title: '企业', key: 'enterprise', width: 180 },
{ title: '充值金额', key: 'amount', width: 140 },
{ title: '支付渠道', key: 'channel', width: 110 },
{ title: '交易流水', dataIndex: 'flowNo', key: 'flowNo', width: 180 },
{ title: '充值时间', dataIndex: 'createdAt', key: 'createdAt', width: 160 },
{ title: '状态', key: 'status', width: 100 },
{ title: '操作', key: 'actions', width: 100 },
]
const rechargeData = ref([
{ id: 1, enterprise: '腾云科技有限公司', amount: 50000, channel: 'alipay', flowNo: 'ZF20260408001', status: 'paid', createdAt: '2026-04-08 09:15' },
{ id: 2, enterprise: '华创数据服务有限公司', amount: 100000, channel: 'bank', flowNo: 'BK20260405002', status: 'paid', createdAt: '2026-04-05 11:00' },
{ id: 3, enterprise: '云智科技有限公司', amount: 20000, channel: 'wechat', flowNo: 'WX20260403003', status: 'paid', createdAt: '2026-04-03 14:30' },
{ id: 4, enterprise: '数智科技有限公司', amount: 30000, channel: 'manual', flowNo: 'MN20260402004', status: 'paid', createdAt: '2026-04-02 10:00' },
{ id: 5, enterprise: '万物互联有限公司', amount: 50000, channel: 'alipay', flowNo: 'ZF20260328005', status: 'paid', createdAt: '2026-03-28 16:20' },
])
const pagination = reactive({ current: 1, pageSize: 10, total: 6 })
const handleSearch = () => message.info('搜索:' + searchKeyword.value)
const resetFilter = () => { searchKeyword.value = ''; filterBillType.value = undefined; filterBillStatus.value = undefined; dateRange.value = null }
const handleViewBill = (r: any) => message.info('查看账单:' + r.orderNo)
const handleRemind = (r: any) => message.success('已发送催款通知')
const handleViewRecharge = (r: any) => message.info('查看充值凭证')
const handleRechargeSubmit = () => { rechargeModalVisible.value = false; message.success('充值成功') }
</script>
<style scoped>
.mb-6 { margin-bottom: 20px; }
.fin-stat-card {
border-radius: 12px;
}
.fin-stat-inner {
display: flex;
align-items: center;
justify-content: space-between;
}
.fin-label { font-size: 13px; color: #6b7280; margin: 0 0 4px; }
.fin-value { font-size: 24px; font-weight: 700; color: #111827; margin: 0 0 4px; }
.fin-trend { font-size: 12px; margin: 0; }
.fin-trend.up { color: #22c55e; }
.fin-trend.down { color: #ef4444; }
.fin-trend.neutral { color: #6b7280; }
.fin-icon { width: 44px; height: 44px; border-radius: 10px; display: flex; align-items: center; justify-content: center; font-size: 20px; flex-shrink: 0; }
.fin-icon.green { background: #ecfdf5; color: #10b981; }
.fin-icon.blue { background: #eff6ff; color: #3b82f6; }
.fin-icon.purple { background: #f5f3ff; color: #8b5cf6; }
.fin-icon.orange { background: #fff7ed; color: #f59e0b; }
.filter-bar {
display: flex; align-items: center; gap: 12px; margin-bottom: 16px; flex-wrap: wrap;
}
.enterprise-cell { display: flex; align-items: center; gap: 8px; }
.income-amount { color: #22c55e; font-weight: 600; }
.consume-amount { color: #ef4444; font-weight: 600; }
.refund-amount { color: #f59e0b; font-weight: 600; }
</style>

View File

@@ -0,0 +1,89 @@
<template>
<div class="recharge-page">
<a-card :bordered="false" title="充值管理">
<div class="filter-bar">
<a-input-search v-model:value="keyword" placeholder="企业名称..." style="width: 280px" allow-clear />
<a-select v-model:value="channel" placeholder="支付渠道" style="width: 140px" allow-clear>
<a-select-option value="alipay">支付宝</a-select-option>
<a-select-option value="wechat">微信支付</a-select-option>
<a-select-option value="bank">银行转账</a-select-option>
</a-select>
<a-button type="primary" @click="modalVisible = true">
<template #icon><PlusOutlined /></template>
手动充值
</a-button>
</div>
<a-table :columns="columns" :data-source="data" row-key="id">
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'amount'">
<span style="color: #22c55e; font-weight: 600">+ ¥ {{ record.amount.toLocaleString() }}</span>
</template>
<template v-else-if="column.key === 'channel'">
<a-tag>{{ channelMap[record.channel] }}</a-tag>
</template>
<template v-else-if="column.key === 'status'">
<a-badge :status="statusBadge[record.status]" :text="statusMap[record.status]" />
</template>
<template v-else-if="column.key === 'actions'">
<a-button type="link" size="small">凭证</a-button>
</template>
</template>
</a-table>
</a-card>
<a-modal v-model:open="modalVisible" title="手动充值" width="480px" @ok="handleSubmit">
<a-form :model="form" :label-col="{ span: 6 }" :wrapper-col="{ span: 16 }">
<a-form-item label="企业名称">
<a-input v-model:value="form.enterprise" />
</a-form-item>
<a-form-item label="充值金额">
<a-input-number v-model:value="form.amount" :min="0" :precision="2" style="width: 100%" />
</a-form-item>
<a-form-item label="备注">
<a-textarea v-model:value="form.remark" :rows="2" />
</a-form-item>
</a-form>
</a-modal>
</div>
</template>
<script setup lang="ts">
import { PlusOutlined } from '@ant-design/icons-vue'
import { message } from 'ant-design-vue'
definePageMeta({ layout: 'admin' })
const keyword = ref('')
const channel = ref<string | undefined>()
const modalVisible = ref(false)
const form = reactive({ enterprise: '', amount: 0, remark: '' })
const channelMap: Record<string, string> = { alipay: '支付宝', wechat: '微信支付', bank: '银行转账', manual: '手动' }
const statusMap: Record<string, string> = { paid: '已支付', pending: '待支付', failed: '已失败' }
const statusBadge: Record<string, any> = { paid: 'success', pending: 'warning', failed: 'error' }
const columns = [
{ title: '企业', dataIndex: 'enterprise', key: 'enterprise', width: 180 },
{ title: '充值金额', key: 'amount', width: 140 },
{ title: '支付渠道', key: 'channel', width: 110 },
{ title: '交易流水', dataIndex: 'flowNo', key: 'flowNo', width: 180 },
{ title: '充值时间', dataIndex: 'createdAt', key: 'createdAt', width: 160 },
{ title: '状态', key: 'status', width: 100 },
{ title: '操作', key: 'actions', width: 100 },
]
const data = ref([
{ id: 1, enterprise: '腾云科技有限公司', amount: 50000, channel: 'alipay', flowNo: 'ZF20260408001', status: 'paid', createdAt: '2026-04-08 09:15' },
{ id: 2, enterprise: '华创数据服务有限公司', amount: 100000, channel: 'bank', flowNo: 'BK20260405002', status: 'paid', createdAt: '2026-04-05 11:00' },
{ id: 3, enterprise: '云智科技有限公司', amount: 20000, channel: 'wechat', flowNo: 'WX20260403003', status: 'paid', createdAt: '2026-04-03 14:30' },
])
const handleSubmit = () => { modalVisible.value = false; message.success('充值成功') }
</script>
<style scoped>
.filter-bar {
display: flex; align-items: center; gap: 12px; margin-bottom: 16px;
}
</style>

View File

@@ -1,271 +1,379 @@
<template>
<div class="admin-home">
<!-- 欢迎横幅 -->
<div class="welcome-banner">
<div class="welcome-left">
<h2 class="welcome-title">🎛 平台管理中心</h2>
<p class="welcome-sub">欢迎回来{{ adminName }}今日数据已更新</p>
</div>
<div class="welcome-right">
<a-space>
<a-tag color="red" style="font-size:13px;padding:4px 12px">超级管理员</a-tag>
<a-button size="small" @click="loadStats" :loading="loadingStats">
<template #icon><ReloadOutlined /></template>
刷新数据
</a-button>
</a-space>
</div>
</div>
<!-- 核心数据统计 -->
<a-row :gutter="[16, 16]">
<a-col :xs="12" :sm="12" :md="6" v-for="stat in coreStats" :key="stat.label">
<div class="stat-block" :class="stat.color" @click="navigateTo(stat.to)" :style="{ cursor: stat.to ? 'pointer' : 'default' }">
<div class="stat-block-header">
<span class="stat-block-icon">{{ stat.icon }}</span>
<span class="stat-block-label">{{ stat.label }}</span>
</div>
<div class="stat-block-value">
<template v-if="loadingStats">
<a-skeleton-input :active="true" size="small" style="width:60px" />
</template>
<template v-else>{{ stat.value }}</template>
</div>
<div class="stat-block-desc">{{ stat.desc }}</div>
</div>
</a-col>
</a-row>
<!-- 待办事项 + 快速入口 -->
<a-row :gutter="[16, 16]">
<!-- 待处理事项 -->
<a-col :xs="24" :md="12">
<div class="panel">
<div class="panel-header">
<span class="panel-title">🔔 待处理事项</span>
</div>
<div class="todo-list">
<div
v-for="todo in todoItems"
:key="todo.label"
class="todo-item"
:class="{ 'todo-item-urgent': todo.urgent }"
@click="navigateTo(todo.to)"
>
<div class="todo-dot" :class="todo.dotColor"></div>
<div class="todo-content">
<span class="todo-label">{{ todo.label }}</span>
<a-tag :color="todo.tagColor" style="margin-left:8px">
<template v-if="loadingStats">...</template>
<template v-else>{{ todo.value }}</template>
</a-tag>
</div>
<RightOutlined class="todo-arrow" />
</div>
<div v-if="!loadingStats && todoItems.every(t => t.value === 0)" class="todo-empty">
🎉 暂无待处理事项一切正常
</div>
</div>
</div>
</a-col>
<!-- 快速导航 -->
<a-col :xs="24" :md="12">
<div class="panel">
<div class="panel-header">
<span class="panel-title"> 快速入口</span>
</div>
<div class="quick-grid">
<div
v-for="item in quickLinks"
:key="item.to"
class="quick-card"
@click="navigateTo(item.to)"
>
<div class="quick-icon" :style="{ background: item.bg }">{{ item.icon }}</div>
<div class="quick-label">{{ item.label }}</div>
</div>
</div>
</div>
</a-col>
</a-row>
</div>
</template>
<script setup lang="ts"> <script setup lang="ts">
import { ReloadOutlined, RightOutlined } from '@ant-design/icons-vue'
import { getUserInfo } from '@/api/layout'
import { getToken } from '@/utils/token-util'
import { pageAppProductAll } from '@/api/app/appProduct'
import { pageUsers } from '@/api/system/user/index'
import { listAppArticle as listCmsArticle } from '@/api/app/article'
import { pageGitAccounts } from '@/api/developer'
definePageMeta({ layout: 'admin' }) definePageMeta({ layout: 'admin' })
useHead({ title: '平台管理首页' })
const adminName = ref('管理员') const { activeTab } = useNav()
const loadingStats = ref(false)
const coreStats = reactive([ const stats = ref([
{ icon: '📦', label: '应用总数', value: 0, desc: '全平台应用', color: 'blue', to: '/admin/apps' }, { label: '生产工单', value: 128, unit: '单', change: '+12%', up: true, icon: '📋', color: '#6366f1' },
{ icon: '👥', label: '用户总数', value: 0, desc: '注册用户', color: 'green', to: '/admin/users' }, { label: '在制品数量', value: 3, unit: '', change: '+8%', up: true, icon: '🏭', color: '#10b981' },
{ icon: '', label: '待审核应用', value: 0, desc: '等待审核中', color: 'orange', to: '/admin/app-review' }, { label: '设备利用率', value: 87, unit: '%', change: '+3%', up: true, icon: '⚙️', color: '#f59e0b' },
{ icon: '🛒', label: '上架应用', value: 0, desc: '市场在售', color: 'purple', to: '/admin/market' }, { label: '订单交付率', value: 96, unit: '%', change: '+1%', up: true, icon: '🚚', color: '#3b82f6' },
]) ])
const todoItems = reactive([ const quickActions = ref([
{ label: '待审核应用', value: 0, to: '/admin/app-review', tagColor: 'orange', dotColor: 'dot-orange', urgent: false }, { label: '计划排程', icon: '📅', color: 'from-indigo-500 to-purple-500', path: '/admin/production/schedule' },
{ label: '待审核Git账号', value: 0, to: '/admin/git-review', tagColor: 'cyan', dotColor: 'dot-cyan', urgent: false }, { label: '生产管控', icon: '🎛️', color: 'from-emerald-500 to-teal-500', path: '/admin/production/control' },
{ label: '草稿文章', value: 0, to: '/admin/articles', tagColor: 'blue', dotColor: 'dot-blue', urgent: false }, { label: '质量检测', icon: '🔍', color: 'from-amber-500 to-orange-500', path: '/admin/production/quality' },
{ label: '冻结用户', value: 0, to: '/admin/users', tagColor: 'red', dotColor: 'dot-red', urgent: false }, { label: '设备监控', icon: '📊', color: 'from-blue-500 to-cyan-500', path: '/admin/production/equipment' },
{ label: '采购申请', icon: '🛒', color: 'from-pink-500 to-rose-500', path: '/admin/supply/purchase' },
{ label: '库存查询', icon: '📦', color: 'from-violet-500 to-purple-500', path: '/admin/supply/warehouse' },
]) ])
const ANNOUNCE_MODEL = 'announcement' const recentOrders = ref([
{ id: 'WO2026040901', product: '精密轴承组件 A型', quantity: 500, status: '生产中', progress: 65, startDate: '2026-04-09' },
const quickLinks = [ { id: 'WO2026040802', product: '液压缸体 B型', quantity: 200, status: '待排产', progress: 0, startDate: '2026-04-08' },
{ to: '/admin/app-review', icon: '🔍', label: '应用审核', bg: '#fff7ed' }, { id: 'WO2026040801', product: '传动齿轮组 C型', quantity: 1000, status: '已完成', progress: 100, startDate: '2026-04-07' },
{ to: '/admin/git-review', icon: '🔧', label: 'Git 审核', bg: '#ecfdf5' }, { id: 'WO2026040703', product: '密封圈组件 D型', quantity: 3000, status: '已完成', progress: 100, startDate: '2026-04-06' },
{ to: '/admin/apps', icon: '📦', label: '应用管理', bg: '#eff6ff' }, { id: 'WO2026040702', product: '弹簧组件 E型', quantity: 800, status: '已取消', progress: 30, startDate: '2026-04-05' },
{ to: '/admin/market', icon: '🛒', label: '应用市场', bg: '#faf5ff' },
{ to: '/admin/users', icon: '👥', label: '用户管理', bg: '#f0fdf4' },
{ to: '/admin/developers', icon: '🧑‍💻', label: '开发者', bg: '#f0f9ff' },
{ to: '/admin/tickets', icon: '🎫', label: '工单处理', bg: '#fdf4ff' },
{ to: '/admin/articles', icon: '📝', label: '文章管理', bg: '#fefce8' },
{ to: '/admin/article-categories', icon: '🗂️', label: '文章分类', bg: '#ecfeff' },
{ to: '/admin/announcements', icon: '📢', label: '公告管理', bg: '#fff1f2' },
{ to: '/admin/settings', icon: '⚙️', label: '平台设置', bg: '#f9fafb' },
]
async function loadStats() {
loadingStats.value = true
try {
const [appsRes, usersRes, pendingRes, marketRes, draftRes, frozenRes, gitPendingRes] = await Promise.allSettled([
pageAppProductAll({ current: 1, size: 1 }),
pageUsers({ page: 1, limit: 1 }),
pageAppProductAll({ current: 1, size: 1, publishStatus: 'pending_review' }),
pageAppProductAll({ current: 1, size: 1, publishStatus: 'published' }),
listCmsArticle({ status: 1 }),
pageUsers({ page: 1, limit: 1, status: 1 }),
pageGitAccounts({ page: 1, size: 1, status: 'pending' }),
]) ])
coreStats[0].value = appsRes.status === 'fulfilled' ? appsRes.value?.count || 0 : 0 const qualityAlerts = ref([
coreStats[1].value = usersRes.status === 'fulfilled' ? usersRes.value?.count || 0 : 0 { level: 'warning', title: '质检异常:批次 B-20260408-03', desc: '尺寸超出公差范围,已自动隔离', time: '10分钟前' },
coreStats[2].value = pendingRes.status === 'fulfilled' ? pendingRes.value?.count || 0 : 0 { level: 'info', title: '质检报告生成', desc: '批次 A-20260409-01 质检完成', time: '30分钟前' },
coreStats[3].value = marketRes.status === 'fulfilled' ? marketRes.value?.count || 0 : 0 { level: 'warning', title: '设备报警CNC-03', desc: '主轴温度异常,请及时处理', time: '1小时前' },
])
const pendingCount = pendingRes.status === 'fulfilled' ? pendingRes.value?.count || 0 : 0 const equipmentStatus = ref([
const draftCount = draftRes.status === 'fulfilled' { name: 'CNC-01', status: '运行中', utilization: 92 },
? (draftRes.value || []).filter(item => (item.model || '').trim() !== ANNOUNCE_MODEL).length { name: 'CNC-02', status: '运行中', utilization: 88 },
: 0 { name: 'CNC-03', status: '告警', utilization: 0 },
const frozenCount = frozenRes.status === 'fulfilled' ? frozenRes.value?.count || 0 : 0 { name: '铣床-01', status: '待机', utilization: 0 },
const gitPendingCount = gitPendingRes.status === 'fulfilled' ? (gitPendingRes.value as any)?.data?.data?.total || 0 : 0 { name: '铣床-02', status: '运行中', utilization: 75 },
])
todoItems[0].value = pendingCount const statusMap: Record<string, string> = {
todoItems[0].urgent = pendingCount > 0 '生产中': 'processing',
todoItems[1].value = gitPendingCount '待排产': 'warning',
todoItems[1].urgent = gitPendingCount > 0 '已完成': 'success',
todoItems[2].value = draftCount '已取消': 'default',
todoItems[3].value = frozenCount
} catch { /* ignore */ } finally {
loadingStats.value = false
} }
const equipStatusMap: Record<string, string> = {
'运行中': 'success',
'告警': 'error',
'待机': 'default',
} }
onMounted(async () => {
const token = getToken()
if (!token) return
// 并发加载用户信息和统计数据
Promise.allSettled([
getUserInfo().then(me => {
adminName.value = me?.nickname?.trim() || me?.username?.trim() || '管理员'
}),
loadStats(),
])
})
</script> </script>
<template>
<div class="dashboard">
<!-- 统计卡片 -->
<a-row :gutter="[20, 20]" class="mb-6">
<a-col :xs="24" :sm="12" :xl="6" v-for="stat in stats" :key="stat.label">
<div class="stat-card" :style="{ '--accent': stat.color }">
<div class="stat-header">
<span class="stat-icon">{{ stat.icon }}</span>
<span :class="['stat-change', stat.up ? 'up' : 'down']">{{ stat.change }}</span>
</div>
<div class="stat-value">{{ stat.value }}<span class="stat-unit">{{ stat.unit }}</span></div>
<div class="stat-label">{{ stat.label }}</div>
</div>
</a-col>
</a-row>
<a-row :gutter="[20, 20]">
<!-- 左侧主体 -->
<a-col :xs="24" :xl="16">
<!-- 快捷入口 -->
<div class="card mb-6">
<div class="card-title">快捷入口</div>
<div class="quick-grid">
<NuxtLink
v-for="action in quickActions"
:key="action.label"
:to="action.path"
class="quick-item"
>
<div class="quick-icon" :class="action.color">{{ action.icon }}</div>
<span class="quick-label">{{ action.label }}</span>
</NuxtLink>
</div>
</div>
<!-- 工单列表 -->
<div class="card">
<div class="card-title">近期工单</div>
<a-table
:dataSource="recentOrders"
:pagination="false"
size="small"
rowKey="id"
:scroll="{ x: 600 }"
>
<a-table-column title="工单编号" dataIndex="id" width="140" />
<a-table-column title="产品名称" dataIndex="product" />
<a-table-column title="数量" dataIndex="quantity" width="80" align="center" />
<a-table-column title="状态" dataIndex="status" width="100" align="center">
<template #default="{ text }">
<a-tag :color="statusMap[text] === 'success' ? 'success' : statusMap[text] === 'processing' ? 'processing' : statusMap[text] === 'warning' ? 'warning' : 'default'">
{{ text }}
</a-tag>
</template>
</a-table-column>
<a-table-column title="进度" dataIndex="progress" width="140" align="center">
<template #default="{ record }">
<a-progress
:percent="record.progress"
:status="record.progress === 100 ? 'success' : 'active'"
:showInfo="false"
size="small"
/>
<span class="text-xs text-gray-400 ml-2">{{ record.progress }}%</span>
</template>
</a-table-column>
<a-table-column title="开始日期" dataIndex="startDate" width="120" />
</a-table>
</div>
</a-col>
<!-- 右侧 -->
<a-col :xs="24" :xl="8">
<!-- 质量告警 -->
<div class="card mb-6">
<div class="card-title">质量告警</div>
<div class="alert-list">
<div v-for="(alert, idx) in qualityAlerts" :key="idx" class="alert-item" :class="alert.level">
<div class="alert-header">
<span class="alert-dot" :class="alert.level"></span>
<span class="alert-title">{{ alert.title }}</span>
</div>
<div class="alert-desc">{{ alert.desc }}</div>
<div class="alert-time">{{ alert.time }}</div>
</div>
</div>
</div>
<!-- 设备状态 -->
<div class="card">
<div class="card-title">设备状态</div>
<div class="equip-list">
<div v-for="equip in equipmentStatus" :key="equip.name" class="equip-item">
<span class="equip-name">{{ equip.name }}</span>
<a-tag :color="equipStatusMap[equip.status] === 'success' ? 'success' : equipStatusMap[equip.status] === 'error' ? 'error' : 'default'" size="small">
{{ equip.status }}
</a-tag>
<span class="equip-util">{{ equip.utilization }}%</span>
</div>
</div>
</div>
</a-col>
</a-row>
</div>
</template>
<style scoped> <style scoped>
.admin-home { .dashboard {
display: flex; padding: 24px;
flex-direction: column;
gap: 20px;
} }
/* 欢迎横幅 */ .stat-card {
.welcome-banner { background: white;
display: flex;
align-items: center;
justify-content: space-between;
background: linear-gradient(135deg, #1a0f0f 0%, #3d1515 100%);
border-radius: 14px;
padding: 24px 28px;
color: #fff;
flex-wrap: wrap;
gap: 12px;
}
.welcome-title { font-size: 20px; font-weight: 700; color: #fff; margin: 0 0 6px; }
.welcome-sub { font-size: 14px; color: rgba(255,255,255,0.7); margin: 0; }
/* 核心统计块 */
.stat-block {
padding: 18px 20px;
border-radius: 12px; border-radius: 12px;
border: 2px solid transparent; padding: 20px;
transition: all 0.2s; box-shadow: 0 1px 3px rgba(0,0,0,0.06);
border: 1px solid #f0f0f0;
position: relative;
overflow: hidden;
} }
.stat-block:hover { transform: translateY(-2px); box-shadow: 0 6px 16px rgba(0,0,0,0.08); }
.stat-block.blue { background: #eff6ff; border-color: #dbeafe; }
.stat-block.green { background: #f0fdf4; border-color: #bbf7d0; }
.stat-block.orange { background: #fff7ed; border-color: #fed7aa; }
.stat-block.purple { background: #faf5ff; border-color: #e9d5ff; }
.stat-block-header { display: flex; align-items: center; gap: 6px; margin-bottom: 10px; } .stat-card::before {
.stat-block-icon { font-size: 18px; } content: '';
.stat-block-label { font-size: 13px; color: rgba(0,0,0,0.55); } position: absolute;
.stat-block-value { font-size: 32px; font-weight: 800; color: rgba(0,0,0,0.85); line-height: 1.1; margin-bottom: 4px; } top: 0;
.stat-block-desc { font-size: 12px; color: rgba(0,0,0,0.4); } left: 0;
width: 4px;
/* Panel */ height: 100%;
.panel { background: #fff; border: 1px solid #f0f0f0; border-radius: 12px; overflow: hidden; } background: var(--accent);
.panel-header { padding: 14px 18px; border-bottom: 1px solid #f5f5f5; } }
.panel-title { font-size: 14px; font-weight: 600; color: rgba(0,0,0,0.85); }
.stat-header {
/* 待办 */ display: flex;
.todo-list { padding: 8px 0; } justify-content: space-between;
.todo-item { align-items: center;
display: flex; align-items: center; gap: 12px; margin-bottom: 12px;
padding: 12px 18px; cursor: pointer; transition: background 0.15s; }
.stat-icon {
font-size: 24px;
}
.stat-change {
font-size: 12px;
padding: 2px 6px;
border-radius: 4px;
}
.stat-change.up {
color: #10b981;
background: #ecfdf5;
}
.stat-change.down {
color: #ef4444;
background: #fef2f2;
}
.stat-value {
font-size: 32px;
font-weight: 700;
color: #1f2937;
margin-bottom: 4px;
}
.stat-unit {
font-size: 14px;
font-weight: 400;
color: #6b7280;
margin-left: 4px;
}
.stat-label {
font-size: 13px;
color: #9ca3af;
}
.card {
background: white;
border-radius: 12px;
padding: 20px;
box-shadow: 0 1px 3px rgba(0,0,0,0.06);
border: 1px solid #f0f0f0;
}
.card-title {
font-size: 16px;
font-weight: 600;
color: #1f2937;
margin-bottom: 16px;
padding-bottom: 12px;
border-bottom: 1px solid #f0f0f0;
} }
.todo-item:hover { background: #f9fafb; }
.todo-item-urgent .todo-label { font-weight: 600; }
.todo-dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; }
.dot-orange { background: #f97316; }
.dot-blue { background: #3b82f6; }
.dot-cyan { background: #06b6d4; }
.dot-red { background: #ef4444; }
.todo-content { flex: 1; display: flex; align-items: center; }
.todo-label { font-size: 14px; color: rgba(0,0,0,0.75); }
.todo-arrow { font-size: 11px; color: rgba(0,0,0,0.3); }
.todo-empty { text-align: center; padding: 20px 0; color: rgba(0,0,0,0.4); font-size: 14px; }
/* 快速入口九宫格 */
.quick-grid { .quick-grid {
display: grid; display: grid;
grid-template-columns: repeat(3, 1fr); grid-template-columns: repeat(3, 1fr);
gap: 1px; gap: 12px;
background: #f5f5f5;
} }
.quick-card {
display: flex; flex-direction: column; align-items: center; .quick-item {
gap: 8px; padding: 18px 12px; background: #fff; display: flex;
cursor: pointer; transition: background 0.15s; flex-direction: column;
align-items: center;
padding: 16px 8px;
border-radius: 10px;
background: #fafafa;
text-decoration: none;
transition: all 0.2s;
} }
.quick-card:hover { background: #f9fafb; }
.quick-item:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0,0,0,0.08);
}
.quick-icon { .quick-icon {
width: 44px; height: 44px; border-radius: 10px; width: 48px;
display: flex; align-items: center; justify-content: center; font-size: 22px; height: 48px;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
margin-bottom: 8px;
background: linear-gradient(135deg, rgba(99,102,241,0.1), rgba(168,85,247,0.1));
}
.quick-label {
font-size: 13px;
color: #374151;
font-weight: 500;
}
.alert-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.alert-item {
padding: 12px;
border-radius: 8px;
background: #fafafa;
}
.alert-item.warning {
background: #fffbeb;
border-left: 3px solid #f59e0b;
}
.alert-item.info {
background: #eff6ff;
border-left: 3px solid #3b82f6;
}
.alert-header {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 4px;
}
.alert-dot {
width: 6px;
height: 6px;
border-radius: 50%;
}
.alert-dot.warning {
background: #f59e0b;
}
.alert-dot.info {
background: #3b82f6;
}
.alert-title {
font-size: 13px;
font-weight: 600;
color: #1f2937;
}
.alert-desc {
font-size: 12px;
color: #6b7280;
margin-bottom: 4px;
}
.alert-time {
font-size: 11px;
color: #9ca3af;
}
.equip-list {
display: flex;
flex-direction: column;
gap: 10px;
}
.equip-item {
display: flex;
align-items: center;
padding: 8px 12px;
background: #fafafa;
border-radius: 8px;
gap: 8px;
}
.equip-name {
font-size: 13px;
font-weight: 500;
color: #374151;
flex: 1;
}
.equip-util {
font-size: 12px;
color: #6b7280;
width: 40px;
text-align: right;
} }
.quick-label { font-size: 13px; color: rgba(0,0,0,0.75); font-weight: 500; }
</style> </style>

View File

@@ -0,0 +1,144 @@
<script setup lang="ts">
definePageMeta({ layout: 'admin' })
const { activeTab } = useNav()
activeTab.value = 'management-decision'
const kpis = ref([
{ name: '设备综合效率 OEE', value: 78.5, target: 85, unit: '%', trend: '+2.3%', color: '#6366f1' },
{ name: '订单准时交付率', value: 96.2, target: 98, unit: '%', trend: '+1.5%', color: '#10b981' },
{ name: '产品一次合格率', value: 97.8, target: 98.5, unit: '%', trend: '+0.3%', color: '#3b82f6' },
{ name: '人均产值', value: 8.5, target: 9.0, unit: '万/月', trend: '+0.3', color: '#f59e0b' },
])
const analysis = ref([
{ title: '生产效能分析', desc: '本月产能利用率达85%较上月提升5个百分点', type: 'production', icon: '📊' },
{ title: '质量趋势分析', desc: '近30天良品率稳定在97%以上,呈上升趋势', type: 'quality', icon: '✅' },
{ title: '成本分析报告', desc: '本月生产成本控制良好材料利用率提升3%', type: 'cost', icon: '💰' },
{ title: '库存周转分析', desc: '库存周转天数28天处于健康水平', type: 'inventory', icon: '📦' },
])
const typeColor: Record<string, string> = {
production: 'indigo',
quality: 'green',
cost: 'orange',
inventory: 'blue',
}
</script>
<template>
<div class="page-container">
<div class="page-header">
<h2 class="page-title">决策支持</h2>
<a-button @click="() => {}">导出报告</a-button>
</div>
<!-- KPI 指标卡 -->
<a-row :gutter="[16, 16]" class="mb-6">
<a-col :xs="24" :sm="12" :xl="6" v-for="kpi in kpis" :key="kpi.name">
<div class="kpi-card">
<div class="kpi-header">
<span class="kpi-name">{{ kpi.name }}</span>
<span class="kpi-trend">{{ kpi.trend }}</span>
</div>
<div class="kpi-value" :style="{ color: kpi.color }">{{ kpi.value }}<span class="kpi-unit">{{ kpi.unit }}</span></div>
<div class="kpi-bar">
<a-progress :percent="Math.round(kpi.value / kpi.target * 100)" :showInfo="false" :strokeColor="kpi.color" size="small" />
<span class="kpi-target">目标: {{ kpi.target }}{{ kpi.unit }}</span>
</div>
</div>
</a-col>
</a-row>
<a-row :gutter="[20, 20]">
<a-col :xs="24" :xl="16">
<div class="card">
<div class="card-title">经营分析报告</div>
<div class="analysis-list">
<div v-for="item in analysis" :key="item.title" class="analysis-item">
<div class="analysis-icon" :class="item.type">{{ item.icon }}</div>
<div class="analysis-body">
<div class="analysis-title">{{ item.title }}</div>
<div class="analysis-desc">{{ item.desc }}</div>
</div>
<a-button type="link" size="small">查看详情</a-button>
</div>
</div>
</div>
</a-col>
<a-col :xs="24" :xl="8">
<div class="card">
<div class="card-title">数据趋势</div>
<div class="trend-list">
<div class="trend-item">
<div class="trend-label">营收趋势</div>
<div class="trend-bar">
<div class="trend-fill" style="width: 72%; background: #10b981;"></div>
</div>
<div class="trend-val">72%</div>
</div>
<div class="trend-item">
<div class="trend-label">利润趋势</div>
<div class="trend-bar">
<div class="trend-fill" style="width: 65%; background: #6366f1;"></div>
</div>
<div class="trend-val">65%</div>
</div>
<div class="trend-item">
<div class="trend-label">产能趋势</div>
<div class="trend-bar">
<div class="trend-fill" style="width: 85%; background: #f59e0b;"></div>
</div>
<div class="trend-val">85%</div>
</div>
<div class="trend-item">
<div class="trend-label">质量趋势</div>
<div class="trend-bar">
<div class="trend-fill" style="width: 78%; background: #3b82f6;"></div>
</div>
<div class="trend-val">78%</div>
</div>
</div>
</div>
</a-col>
</a-row>
</div>
</template>
<style scoped>
.page-container { padding: 24px; }
.page-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; }
.page-title { font-size: 20px; font-weight: 600; color: #1f2937; margin: 0; }
.kpi-card { background: white; border-radius: 12px; padding: 18px; box-shadow: 0 1px 3px rgba(0,0,0,0.06); border: 1px solid #f0f0f0; }
.kpi-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; }
.kpi-name { font-size: 13px; color: #6b7280; }
.kpi-trend { font-size: 12px; color: #10b981; font-weight: 600; }
.kpi-value { font-size: 30px; font-weight: 700; }
.kpi-unit { font-size: 13px; font-weight: 400; color: #9ca3af; margin-left: 2px; }
.kpi-bar { margin-top: 10px; }
.kpi-target { font-size: 11px; color: #9ca3af; margin-top: 4px; display: block; }
.card { background: white; border-radius: 12px; padding: 20px; box-shadow: 0 1px 3px rgba(0,0,0,0.06); border: 1px solid #f0f0f0; }
.card-title { font-size: 16px; font-weight: 600; color: #1f2937; margin-bottom: 16px; }
.analysis-list { display: flex; flex-direction: column; gap: 14px; }
.analysis-item { display: flex; align-items: center; gap: 14px; padding: 14px; background: #fafafa; border-radius: 10px; }
.analysis-icon { width: 44px; height: 44px; border-radius: 10px; display: flex; align-items: center; justify-content: center; font-size: 20px; background: #f0f0f0; }
.analysis-icon.production { background: #eef2ff; }
.analysis-icon.quality { background: #ecfdf5; }
.analysis-icon.cost { background: #fffbeb; }
.analysis-icon.inventory { background: #eff6ff; }
.analysis-body { flex: 1; }
.analysis-title { font-size: 14px; font-weight: 600; color: #374151; margin-bottom: 4px; }
.analysis-desc { font-size: 12px; color: #9ca3af; }
.trend-list { display: flex; flex-direction: column; gap: 16px; }
.trend-item { display: flex; align-items: center; gap: 10px; }
.trend-label { width: 80px; font-size: 13px; color: #6b7280; }
.trend-bar { flex: 1; height: 8px; background: #f0f0f0; border-radius: 4px; overflow: hidden; }
.trend-fill { height: 100%; border-radius: 4px; }
.trend-val { width: 40px; text-align: right; font-size: 13px; font-weight: 600; color: #374151; }
.mb-6 { margin-bottom: 20px; }
</style>

View File

@@ -0,0 +1,383 @@
<script setup lang="ts">
definePageMeta({ layout: 'admin' })
// 财务统计
const financeStats = ref([
{ label: '本月收入', value: '128.5', unit: '万', icon: 'fa-arrow-down', gradient: 'from-green-500 to-teal-500', change: '+18%', up: true },
{ label: '本月支出', value: '89.5', unit: '万', icon: 'fa-arrow-up', gradient: 'from-red-500 to-pink-500', change: '+12%', up: false },
{ label: '本月利润', value: '39.0', unit: '万', icon: 'fa-chart-line', gradient: 'from-blue-500 to-purple-500', change: '+25%', up: true },
{ label: '应收账款', value: '256.8', unit: '万', icon: 'fa-wallet', gradient: 'from-orange-500 to-yellow-500', change: '-5%', up: true },
])
// 收支记录
const transactions = ref([
{ id: 'TX-2026040901', type: 'income', category: '销售收款', amount: 58000, customer: '深圳市精密机械有限公司', date: '2026-04-09', status: 'completed' },
{ id: 'TX-2026040802', type: 'expense', category: '采购付款', amount: 36000, supplier: '上海五金工具厂', date: '2026-04-08', status: 'completed' },
{ id: 'TX-2026040801', type: 'expense', category: '工资支出', amount: 125000, supplier: '人力资源部', date: '2026-04-08', status: 'completed' },
{ id: 'TX-2026040703', type: 'income', category: '销售收款', amount: 42000, customer: '东莞市金属材料公司', date: '2026-04-07', status: 'completed' },
{ id: 'TX-2026040702', type: 'expense', category: '水电费', amount: 8500, supplier: '电力公司', date: '2026-04-07', status: 'completed' },
{ id: 'TX-2026040601', type: 'income', category: '销售收款', amount: 75000, customer: '苏州液压设备厂', date: '2026-04-06', status: 'completed' },
{ id: 'TX-2026040501', type: 'expense', category: '设备维修', amount: 12000, supplier: '设备维修部', date: '2026-04-05', status: 'completed' },
{ id: 'TX-2026040502', type: 'expense', category: '办公费用', amount: 3200, supplier: '办公用品供应商', date: '2026-04-05', status: 'completed' },
])
// 应收应付
const receivables = ref([
{ customer: '深圳市精密机械有限公司', amount: 85000, dueDate: '2026-04-15', status: 'pending' },
{ customer: '东莞市金属材料公司', amount: 68000, dueDate: '2026-04-20', status: 'pending' },
{ customer: '苏州液压设备厂', amount: 42000, dueDate: '2026-04-25', status: 'overdue' },
{ customer: '广州电子科技有限公司', amount: 95000, dueDate: '2026-05-01', status: 'pending' },
])
const typeFilter = ref('all')
const searchKeyword = ref('')
const activeTab = ref('transactions')
const filteredTransactions = computed(() => {
return transactions.value.filter((item) => {
const matchType = typeFilter.value === 'all' || item.type === typeFilter.value
const matchSearch = !searchKeyword.value ||
item.id.toLowerCase().includes(searchKeyword.value.toLowerCase()) ||
item.category.toLowerCase().includes(searchKeyword.value.toLowerCase()) ||
(item.customer && item.customer.toLowerCase().includes(searchKeyword.value.toLowerCase()))
return matchType && matchSearch
})
})
</script>
<template>
<div class="finance-page">
<!-- 页面标题 -->
<div class="page-header mb-6">
<div>
<h2 class="text-2xl font-bold text-gray-800">财务管理</h2>
<p class="text-gray-500 mt-1">监控收支状况管理应收账款</p>
</div>
<div class="flex gap-3">
<a-button>
<template #icon><i class="fas fa-download mr-1"></i></template>
导出报表
</a-button>
<a-button type="primary">
<template #icon><i class="fas fa-plus mr-1"></i></template>
记账
</a-button>
</div>
</div>
<!-- 统计卡片 -->
<div class="grid grid-cols-4 gap-6 mb-6">
<div
v-for="stat in financeStats"
:key="stat.label"
class="glass rounded-2xl p-6 card-hover cursor-pointer"
>
<div class="flex items-center justify-between mb-4">
<div
class="w-12 h-12 rounded-xl flex items-center justify-center text-white"
:class="`bg-gradient-to-br ${stat.gradient}`"
>
<i :class="`fas ${stat.icon}`"></i>
</div>
<span
class="text-sm font-medium"
:class="stat.up ? 'text-green-500' : 'text-red-500'"
>
<i :class="stat.up ? 'fas fa-arrow-up' : 'fas fa-arrow-down'"></i>
{{ stat.change }}
</span>
</div>
<h3 class="text-3xl font-bold text-gray-800 mb-1">
<span class="text-base text-gray-500">¥</span>{{ stat.value }}<span class="text-base text-gray-500 ml-1">{{ stat.unit }}</span>
</h3>
<p class="text-gray-500 text-sm">{{ stat.label }}</p>
</div>
</div>
<!-- Tab切换 -->
<div class="glass rounded-2xl p-4 mb-6">
<a-radio-group v-model:value="activeTab" button-style="solid">
<a-radio-button value="transactions">
<i class="fas fa-list mr-1"></i>收支记录
</a-radio-button>
<a-radio-button value="receivables">
<i class="fas fa-hand-holding-usd mr-1"></i>应收账款
</a-radio-button>
</a-radio-group>
</div>
<!-- 收支记录 -->
<div v-show="activeTab === 'transactions'" class="glass rounded-2xl p-6">
<!-- 筛选 -->
<div class="flex items-center justify-between mb-4">
<div class="flex items-center gap-3">
<span class="text-gray-600 font-medium">类型筛选</span>
<a-radio-group v-model:value="typeFilter" button-style="solid">
<a-radio-button value="all">全部</a-radio-button>
<a-radio-button value="income">收入</a-radio-button>
<a-radio-button value="expense">支出</a-radio-button>
</a-radio-group>
</div>
<a-input-search
v-model:value="searchKeyword"
placeholder="搜索单号、类别、对象..."
style="width: 280px"
allow-clear
/>
</div>
<a-table
:dataSource="filteredTransactions"
:pagination="{ pageSize: 10 }"
rowKey="id"
:scroll="{ x: 1000 }"
>
<a-table-column title="单号" dataIndex="id" width="140" />
<a-table-column title="类型" dataIndex="type" width="80" align="center">
<template #default="{ text }">
<a-tag :color="text === 'income' ? 'success' : 'error'">
{{ text === 'income' ? '收入' : '支出' }}
</a-tag>
</template>
</a-table-column>
<a-table-column title="类别" dataIndex="category" width="120" />
<a-table-column title="金额(元)" dataIndex="amount" width="130" align="right">
<template #default="{ record, text }">
<span class="font-medium" :class="record.type === 'income' ? 'text-green-600' : 'text-red-600'">
{{ record.type === 'income' ? '+' : '-' }}¥{{ text.toLocaleString() }}
</span>
</template>
</a-table-column>
<a-table-column title="对方" width="200">
<template #default="{ record }">
<span class="font-medium">{{ record.customer || record.supplier || '-' }}</span>
</template>
</a-table-column>
<a-table-column title="日期" dataIndex="date" width="120" />
<a-table-column title="状态" width="100" align="center">
<template #default>
<a-tag color="success">已完成</a-tag>
</template>
</a-table-column>
<a-table-column title="操作" width="100" align="center" fixed="right">
<template #default>
<div class="flex items-center gap-2 justify-center">
<a-button type="link" size="small" title="详情">
<i class="fas fa-eye"></i>
</a-button>
</div>
</template>
</a-table-column>
</a-table>
</div>
<!-- 应收账款 -->
<div v-show="activeTab === 'receivables'" class="glass rounded-2xl p-6">
<div class="flex items-center justify-between mb-4">
<h3 class="font-bold text-lg text-gray-800">
<i class="fas fa-hand-holding-usd text-orange-500 mr-2"></i>
应收账款列表
</h3>
<a-button type="primary">
<template #icon><i class="fas fa-plus mr-1"></i></template>
登记收款
</a-button>
</div>
<a-table
:dataSource="receivables"
:pagination="false"
rowKey="customer"
>
<a-table-column title="客户名称" dataIndex="customer">
<template #default="{ text }">
<div class="flex items-center gap-2">
<div class="w-8 h-8 rounded-lg bg-gradient-to-br from-blue-500 to-purple-500 flex items-center justify-center text-white text-xs">
<i class="fas fa-building"></i>
</div>
<span class="font-medium">{{ text }}</span>
</div>
</template>
</a-table-column>
<a-table-column title="应收金额(元)" dataIndex="amount" width="150" align="right">
<template #default="{ text }">
<span class="font-medium text-orange-600">¥{{ text.toLocaleString() }}</span>
</template>
</a-table-column>
<a-table-column title="到期日期" dataIndex="dueDate" width="140" />
<a-table-column title="状态" dataIndex="status" width="100" align="center">
<template #default="{ text }">
<a-tag v-if="text === 'pending'" color="processing">待收款</a-tag>
<a-tag v-else-if="text === 'overdue'" color="error">已逾期</a-tag>
<a-tag v-else color="success">已收款</a-tag>
</template>
</a-table-column>
<a-table-column title="操作" width="160" align="center" fixed="right">
<template #default>
<div class="flex items-center gap-2 justify-center">
<a-button type="primary" size="small" ghost>催款</a-button>
<a-button type="link" size="small">详情</a-button>
</div>
</template>
</a-table-column>
</a-table>
</div>
</div>
</template>
<style scoped>
.finance-page {
padding: 0;
}
.page-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
}
.glass {
background: rgba(255, 255, 255, 0.85);
backdrop-filter: blur(20px);
border: 1px solid rgba(255, 255, 255, 0.3);
}
.card-hover {
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.card-hover:hover {
transform: translateY(-4px);
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
}
.grid {
display: grid;
}
.grid-cols-4 {
grid-template-columns: repeat(4, 1fr);
}
@media (max-width: 1200px) {
.grid-cols-4 {
grid-template-columns: repeat(2, 1fr);
}
}
.mb-6 {
margin-bottom: 24px;
}
.mb-4 {
margin-bottom: 16px;
}
.mr-1 {
margin-right: 4px;
}
.mr-2 {
margin-right: 8px;
}
.ml-1 {
margin-left: 4px;
}
.text-2xl {
font-size: 24px;
}
.text-3xl {
font-size: 30px;
}
.text-lg {
font-size: 18px;
}
.text-sm {
font-size: 14px;
}
.text-base {
font-size: 16px;
}
.rounded-2xl {
border-radius: 16px;
}
.rounded-lg {
border-radius: 10px;
}
.p-6 {
padding: 24px;
}
.p-4 {
padding: 16px;
}
.gap-6 {
gap: 24px;
}
.gap-3 {
gap: 12px;
}
.gap-2 {
gap: 8px;
}
.flex {
display: flex;
}
.items-center {
align-items: center;
}
.justify-between {
justify-content: space-between;
}
.font-bold {
font-weight: 700;
}
.font-medium {
font-weight: 500;
}
.text-gray-800 {
color: #1f2937;
}
.text-gray-600 {
color: #4b5563;
}
.text-gray-500 {
color: #6b7280;
}
.text-green-600 {
color: #16a34a;
}
.text-red-600 {
color: #dc2626;
}
.text-orange-600 {
color: #ea580c;
}
.gap-2 {
gap: 8px;
}
</style>

View File

@@ -0,0 +1,453 @@
<script setup lang="ts">
definePageMeta({ layout: 'admin' })
// 人事统计
const hrStats = ref([
{ label: '员工总数', value: 56, icon: 'fa-users', gradient: 'from-blue-500 to-cyan-500', change: '+3', up: true },
{ label: '在职', value: 52, icon: 'fa-user-check', gradient: 'from-green-500 to-teal-500', change: '+2', up: true },
{ label: '本月入职', value: 3, icon: 'fa-user-plus', gradient: 'from-purple-500 to-pink-500', change: '+2', up: true },
{ label: '本月离职', value: 1, icon: 'fa-user-minus', gradient: 'from-orange-500 to-red-500', change: '-1', up: true },
])
// 员工列表
const employees = ref([
{ id: 'EMP-001', name: '张三', department: '生产部', position: '生产主管', phone: '138****1234', email: 'zhangsan@company.com', status: 'active', joinDate: '2020-03-15' },
{ id: 'EMP-002', name: '李四', department: '技术部', position: '技术工程师', phone: '139****5678', email: 'lisi@company.com', status: 'active', joinDate: '2021-06-20' },
{ id: 'EMP-003', name: '王五', department: '采购部', position: '采购专员', phone: '137****9012', email: 'wangwu@company.com', status: 'active', joinDate: '2022-01-10' },
{ id: 'EMP-004', name: '赵六', department: '财务部', position: '财务经理', phone: '136****3456', email: 'zhaoliu@company.com', status: 'active', joinDate: '2019-08-25' },
{ id: 'EMP-005', name: '孙七', department: '人事部', position: '人事专员', phone: '135****7890', email: 'sunqi@company.com', status: 'active', joinDate: '2023-02-15' },
{ id: 'EMP-006', name: '周八', department: '生产部', position: '操作工', phone: '134****2345', email: 'zhouba@company.com', status: 'active', joinDate: '2024-01-08' },
])
// 考勤记录
const attendanceRecords = ref([
{ date: '2026-04-09', total: 56, present: 54, absent: 0, late: 2, leave: 0, off: 0 },
{ date: '2026-04-08', total: 56, present: 55, absent: 0, late: 1, leave: 0, off: 0 },
{ date: '2026-04-07', total: 56, present: 56, absent: 0, late: 0, leave: 0, off: 0 },
{ date: '2026-04-06', total: 56, present: 48, absent: 0, late: 0, leave: 3, off: 5 },
])
// 部门分布
const departmentStats = ref([
{ name: '生产部', count: 28, percentage: 50 },
{ name: '技术部', count: 10, percentage: 18 },
{ name: '采购部', count: 6, percentage: 11 },
{ name: '财务部', count: 5, percentage: 9 },
{ name: '人事部', count: 4, percentage: 7 },
{ name: '其他', count: 3, percentage: 5 },
])
const activeTab = ref('employees')
const searchKeyword = ref('')
const filteredEmployees = computed(() => {
return employees.value.filter((emp) => {
return !searchKeyword.value ||
emp.name.toLowerCase().includes(searchKeyword.value.toLowerCase()) ||
emp.id.toLowerCase().includes(searchKeyword.value.toLowerCase()) ||
emp.department.toLowerCase().includes(searchKeyword.value.toLowerCase())
})
})
</script>
<template>
<div class="hr-page">
<!-- 页面标题 -->
<div class="page-header mb-6">
<div>
<h2 class="text-2xl font-bold text-gray-800">人力资源</h2>
<p class="text-gray-500 mt-1">管理员工信息考勤记录和薪资发放</p>
</div>
<div class="flex gap-3">
<a-button>
<template #icon><i class="fas fa-calendar mr-1"></i></template>
考勤统计
</a-button>
<a-button type="primary">
<template #icon><i class="fas fa-plus mr-1"></i></template>
新增员工
</a-button>
</div>
</div>
<!-- 统计卡片 -->
<div class="grid grid-cols-4 gap-6 mb-6">
<div
v-for="stat in hrStats"
:key="stat.label"
class="glass rounded-2xl p-6 card-hover cursor-pointer"
>
<div class="flex items-center justify-between mb-4">
<div
class="w-12 h-12 rounded-xl flex items-center justify-center text-white"
:class="`bg-gradient-to-br ${stat.gradient}`"
>
<i :class="`fas ${stat.icon}`"></i>
</div>
<span
class="text-sm font-medium"
:class="stat.up ? 'text-green-500' : 'text-red-500'"
>
<i :class="stat.up ? 'fas fa-arrow-up' : 'fas fa-arrow-down'"></i>
{{ stat.change }}
</span>
</div>
<h3 class="text-3xl font-bold text-gray-800 mb-1">{{ stat.value }}</h3>
<p class="text-gray-500 text-sm">{{ stat.label }}</p>
</div>
</div>
<!-- Tab切换 -->
<div class="glass rounded-2xl p-4 mb-6">
<a-radio-group v-model:value="activeTab" button-style="solid">
<a-radio-button value="employees">
<i class="fas fa-users mr-1"></i>员工管理
</a-radio-button>
<a-radio-button value="attendance">
<i class="fas fa-calendar-check mr-1"></i>考勤记录
</a-radio-button>
<a-radio-button value="salary">
<i class="fas fa-money-bill mr-1"></i>薪资管理
</a-radio-button>
</a-radio-group>
<a-input-search
v-model:value="searchKeyword"
placeholder="搜索员工姓名、工号、部门..."
style="width: 280px; float: right"
allow-clear
/>
</div>
<!-- 员工列表 -->
<div v-show="activeTab === 'employees'" class="glass rounded-2xl p-6">
<div class="flex items-center justify-between mb-4">
<h3 class="font-bold text-lg text-gray-800">
<i class="fas fa-users text-blue-500 mr-2"></i>
员工列表
</h3>
<span class="text-sm text-gray-500"> {{ filteredEmployees.length }} 名员工</span>
</div>
<a-table
:dataSource="filteredEmployees"
:pagination="{ pageSize: 10 }"
rowKey="id"
:scroll="{ x: 1000 }"
>
<a-table-column title="工号" dataIndex="id" width="100" />
<a-table-column title="姓名" dataIndex="name" width="100">
<template #default="{ text }">
<div class="flex items-center gap-2">
<div class="w-8 h-8 rounded-full bg-gradient-to-br from-blue-500 to-purple-500 flex items-center justify-center text-white text-sm font-medium">
{{ text.charAt(0) }}
</div>
<span class="font-medium">{{ text }}</span>
</div>
</template>
</a-table-column>
<a-table-column title="部门" dataIndex="department" width="100" />
<a-table-column title="职位" dataIndex="position" width="120" />
<a-table-column title="手机号" dataIndex="phone" width="130" />
<a-table-column title="邮箱" dataIndex="email" width="180" />
<a-table-column title="入职日期" dataIndex="joinDate" width="120" />
<a-table-column title="状态" dataIndex="status" width="80" align="center">
<template #default>
<a-tag color="success">在职</a-tag>
</template>
</a-table-column>
<a-table-column title="操作" width="120" align="center" fixed="right">
<template #default>
<div class="flex items-center gap-2 justify-center">
<a-button type="link" size="small" title="详情">
<i class="fas fa-eye"></i>
</a-button>
<a-button type="link" size="small" title="编辑">
<i class="fas fa-edit"></i>
</a-button>
</div>
</template>
</a-table-column>
</a-table>
</div>
<!-- 考勤记录 -->
<div v-show="activeTab === 'attendance'" class="glass rounded-2xl p-6">
<div class="flex items-center justify-between mb-4">
<h3 class="font-bold text-lg text-gray-800">
<i class="fas fa-calendar-check text-green-500 mr-2"></i>
考勤记录
</h3>
<a-button>导出考勤表</a-button>
</div>
<!-- 今日考勤统计 -->
<a-row :gutter="16" class="mb-6">
<a-col :span="6">
<div class="stat-mini glass rounded-xl p-4 text-center">
<div class="text-2xl font-bold text-blue-600">{{ attendanceRecords[0].total }}</div>
<div class="text-sm text-gray-500">应到人数</div>
</div>
</a-col>
<a-col :span="6">
<div class="stat-mini glass rounded-xl p-4 text-center">
<div class="text-2xl font-bold text-green-600">{{ attendanceRecords[0].present }}</div>
<div class="text-sm text-gray-500">实际出勤</div>
</div>
</a-col>
<a-col :span="6">
<div class="stat-mini glass rounded-xl p-4 text-center">
<div class="text-2xl font-bold text-orange-600">{{ attendanceRecords[0].late }}</div>
<div class="text-sm text-gray-500">迟到</div>
</div>
</a-col>
<a-col :span="6">
<div class="stat-mini glass rounded-xl p-4 text-center">
<div class="text-2xl font-bold text-purple-600">{{ attendanceRecords[0].leave + attendanceRecords[0].off }}</div>
<div class="text-sm text-gray-500">请假/休息</div>
</div>
</a-col>
</a-row>
<a-table
:dataSource="attendanceRecords"
:pagination="false"
rowKey="date"
>
<a-table-column title="日期" dataIndex="date" width="120" />
<a-table-column title="应到人数" dataIndex="total" width="100" align="center" />
<a-table-column title="实际出勤" dataIndex="present" width="100" align="center">
<template #default="{ text, record }">
<span class="text-green-600 font-medium">{{ text }}</span>
</template>
</a-table-column>
<a-table-column title="缺勤" dataIndex="absent" width="80" align="center">
<template #default="{ text }">
<span :class="text > 0 ? 'text-red-600 font-medium' : 'text-gray-400'">{{ text }}</span>
</template>
</a-table-column>
<a-table-column title="迟到" dataIndex="late" width="80" align="center">
<template #default="{ text }">
<span :class="text > 0 ? 'text-orange-600 font-medium' : 'text-gray-400'">{{ text }}</span>
</template>
</a-table-column>
<a-table-column title="请假" dataIndex="leave" width="80" align="center" />
<a-table-column title="休息" dataIndex="off" width="80" align="center" />
<a-table-column title="出勤率" width="120">
<template #default="{ record }">
<div class="flex items-center gap-2">
<a-progress
:percent="Math.round((record.present / record.total) * 100)"
:showInfo="false"
size="small"
style="width: 60px"
/>
<span class="text-sm">{{ Math.round((record.present / record.total) * 100) }}%</span>
</div>
</template>
</a-table-column>
</a-table>
<!-- 部门分布 -->
<div class="mt-6">
<h4 class="font-bold text-gray-800 mb-4">部门人数分布</h4>
<a-row :gutter="[16, 16]">
<a-col :span="8" v-for="dept in departmentStats" :key="dept.name">
<div class="glass rounded-xl p-4">
<div class="flex justify-between mb-2">
<span class="font-medium">{{ dept.name }}</span>
<span class="text-blue-600 font-medium">{{ dept.count }}</span>
</div>
<a-progress :percent="dept.percentage" :showInfo="false" stroke-color="#6366f1" />
</div>
</a-col>
</a-row>
</div>
</div>
<!-- 薪资管理 -->
<div v-show="activeTab === 'salary'" class="glass rounded-2xl p-6">
<div class="flex items-center justify-between mb-4">
<h3 class="font-bold text-lg text-gray-800">
<i class="fas fa-money-bill text-green-500 mr-2"></i>
薪资发放记录
</h3>
<a-button type="primary">
<template #icon><i class="fas fa-plus mr-1"></i></template>
生成工资单
</a-button>
</div>
<a-empty description="薪资数据将在每月固定日期生成" />
</div>
</div>
</template>
<style scoped>
.hr-page {
padding: 0;
}
.page-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
}
.glass {
background: rgba(255, 255, 255, 0.85);
backdrop-filter: blur(20px);
border: 1px solid rgba(255, 255, 255, 0.3);
}
.card-hover {
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.card-hover:hover {
transform: translateY(-4px);
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
}
.stat-mini {
background: rgba(255, 255, 255, 0.85);
backdrop-filter: blur(10px);
}
.grid {
display: grid;
}
.grid-cols-4 {
grid-template-columns: repeat(4, 1fr);
}
@media (max-width: 1200px) {
.grid-cols-4 {
grid-template-columns: repeat(2, 1fr);
}
}
.mb-6 {
margin-bottom: 24px;
}
.mb-4 {
margin-bottom: 16px;
}
.mb-2 {
margin-bottom: 8px;
}
.mt-6 {
margin-top: 24px;
}
.mr-1 {
margin-right: 4px;
}
.mr-2 {
margin-right: 8px;
}
.text-2xl {
font-size: 24px;
}
.text-3xl {
font-size: 30px;
}
.text-lg {
font-size: 18px;
}
.text-sm {
font-size: 14px;
}
.rounded-2xl {
border-radius: 16px;
}
.rounded-xl {
border-radius: 12px;
}
.p-6 {
padding: 24px;
}
.p-4 {
padding: 16px;
}
.gap-6 {
gap: 24px;
}
.gap-3 {
gap: 12px;
}
.gap-2 {
gap: 8px;
}
.gap-16 {
gap: 16px;
}
.flex {
display: flex;
}
.items-center {
align-items: center;
}
.justify-between {
justify-content: space-between;
}
.font-bold {
font-weight: 700;
}
.font-medium {
font-weight: 500;
}
.text-gray-800 {
color: #1f2937;
}
.text-gray-500 {
color: #6b7280;
}
.text-gray-400 {
color: #9ca3af;
}
.text-blue-600 {
color: #2563eb;
}
.text-green-600 {
color: #16a34a;
}
.text-orange-600 {
color: #ea580c;
}
.text-purple-600 {
color: #9333ea;
}
.text-red-600 {
color: #dc2626;
}
</style>

View File

@@ -0,0 +1,477 @@
<script setup lang="ts">
definePageMeta({ layout: 'admin' })
// 协同办公统计
const officeStats = ref([
{ label: '公告通知', value: 12, icon: 'fa-bullhorn', gradient: 'from-red-500 to-pink-500', change: '+3', up: true },
{ label: '待审批', value: 8, icon: 'fa-clock', gradient: 'from-orange-500 to-yellow-500', change: '-2', up: true },
{ label: '已完成', value: 156, icon: 'fa-check-circle', gradient: 'from-green-500 to-teal-500', change: '+25', up: true },
{ label: '会议预约', value: 5, icon: 'fa-video', gradient: 'from-blue-500 to-purple-500', change: '+1', up: false },
])
// 公告列表
const announcements = ref([
{ id: 'NOTICE-001', title: '关于清明节放假安排的通知', author: '人事行政部', date: '2026-04-01', views: 1258, important: true, content: '清明节放假时间为4月4日至4月6日共3天...' },
{ id: 'NOTICE-002', title: '2026年第一季度财报公告', author: '财务部', date: '2026-03-28', views: 986, important: true, content: '公司2026年第一季度营收同比增长18%...' },
{ id: 'NOTICE-003', title: '新版本系统功能更新说明', author: '技术部', date: '2026-03-25', views: 756, important: false, content: '本次更新新增设备管理模块...' },
{ id: 'NOTICE-004', title: '生产车间设备维护通知', author: '生产部', date: '2026-03-20', views: 423, important: false, content: '2号车间将于本周六进行设备维护...' },
])
// 审批列表
const approvals = ref([
{ id: 'APPR-001', type: 'leave', title: '张三年假申请', applicant: '张三', department: '生产部', amount: '5天', status: 'pending', date: '2026-04-09' },
{ id: 'APPR-002', type: 'reimburse', title: '李四差旅费报销', applicant: '李四', department: '技术部', amount: '¥2,580', status: 'pending', date: '2026-04-09' },
{ id: 'APPR-003', type: 'purchase', title: '王五办公用品采购', applicant: '王五', department: '采购部', amount: '¥3,200', status: 'pending', date: '2026-04-08' },
{ id: 'APPR-004', type: 'leave', title: '赵六病假申请', applicant: '赵六', department: '财务部', amount: '2天', status: 'approved', date: '2026-04-08' },
{ id: 'APPR-005', type: 'overtime', title: '孙七加班申请', applicant: '孙七', department: '生产部', amount: '8小时', status: 'approved', date: '2026-04-07' },
])
const typeMap: Record<string, { label: string; color: string }> = {
leave: { label: '请假', color: 'blue' },
reimburse: { label: '报销', color: 'orange' },
purchase: { label: '采购', color: 'purple' },
overtime: { label: '加班', color: 'cyan' },
}
const statusMap: Record<string, { label: string; color: string }> = {
pending: { label: '待审批', color: 'processing' },
approved: { label: '已通过', color: 'success' },
rejected: { label: '已驳回', color: 'error' },
}
const activeTab = ref('announcements')
const addAnnouncementVisible = ref(false)
const addAnnouncementForm = reactive({
title: '',
content: '',
important: false,
})
const approveVisible = ref(false)
const selectedApproval = ref<typeof approvals.value[0] | null>(null)
function showApproveDetail(item: typeof approvals.value[0]) {
selectedApproval.value = item
approveVisible.value = true
}
function handleApprove(status: string) {
approveVisible.value = false
message.success(status === 'approved' ? '已通过审批' : '已驳回申请')
}
function handleAddAnnouncement() {
addAnnouncementVisible.value = false
message.success('公告发布成功')
}
</script>
<template>
<div class="office-page">
<!-- 页面标题 -->
<div class="page-header mb-6">
<div>
<h2 class="text-2xl font-bold text-gray-800">协同办公</h2>
<p class="text-gray-500 mt-1">发布公告处理审批管理会议</p>
</div>
<div class="flex gap-3">
<a-button @click="addAnnouncementVisible = true">
<template #icon><i class="fas fa-bullhorn mr-1"></i></template>
发布公告
</a-button>
<a-button type="primary">
<template #icon><i class="fas fa-plus mr-1"></i></template>
新建审批
</a-button>
</div>
</div>
<!-- 统计卡片 -->
<div class="grid grid-cols-4 gap-6 mb-6">
<div
v-for="stat in officeStats"
:key="stat.label"
class="glass rounded-2xl p-6 card-hover cursor-pointer"
>
<div class="flex items-center justify-between mb-4">
<div
class="w-12 h-12 rounded-xl flex items-center justify-center text-white"
:class="`bg-gradient-to-br ${stat.gradient}`"
>
<i :class="`fas ${stat.icon}`"></i>
</div>
<span
class="text-sm font-medium"
:class="stat.up ? 'text-green-500' : 'text-red-500'"
>
<i :class="stat.up ? 'fas fa-arrow-up' : 'fas fa-arrow-down'"></i>
{{ stat.change }}
</span>
</div>
<h3 class="text-3xl font-bold text-gray-800 mb-1">{{ stat.value }}</h3>
<p class="text-gray-500 text-sm">{{ stat.label }}</p>
</div>
</div>
<!-- Tab切换 -->
<div class="glass rounded-2xl p-4 mb-6">
<a-radio-group v-model:value="activeTab" button-style="solid">
<a-radio-button value="announcements">
<i class="fas fa-bullhorn mr-1"></i>公告通知
</a-radio-button>
<a-radio-button value="approvals">
<i class="fas fa-tasks mr-1"></i>审批流程
</a-radio-button>
<a-radio-button value="meetings">
<i class="fas fa-video mr-1"></i>会议管理
</a-radio-button>
</a-radio-group>
</div>
<!-- 公告通知 -->
<div v-show="activeTab === 'announcements'" class="glass rounded-2xl p-6">
<div class="flex items-center justify-between mb-4">
<h3 class="font-bold text-lg text-gray-800">
<i class="fas fa-bullhorn text-red-500 mr-2"></i>
最新公告
</h3>
<a-button type="link">全部公告</a-button>
</div>
<div class="space-y-4">
<div
v-for="notice in announcements"
:key="notice.id"
class="notice-item glass rounded-xl p-5 card-hover cursor-pointer"
:class="notice.important ? 'border-l-4 border-red-500' : 'border-l-4 border-blue-500'"
>
<div class="flex items-start justify-between">
<div class="flex-1">
<div class="flex items-center gap-2 mb-2">
<a-tag v-if="notice.important" color="red">重要</a-tag>
<h4 class="font-bold text-gray-800">{{ notice.title }}</h4>
</div>
<p class="text-gray-500 text-sm mb-3 line-clamp-2">{{ notice.content }}</p>
<div class="flex items-center gap-4 text-xs text-gray-400">
<span><i class="fas fa-user mr-1"></i>{{ notice.author }}</span>
<span><i class="fas fa-clock mr-1"></i>{{ notice.date }}</span>
<span><i class="fas fa-eye mr-1"></i>{{ notice.views }} 阅读</span>
</div>
</div>
<div class="flex items-center gap-2 ml-4">
<a-button type="link" size="small">查看</a-button>
<a-button type="link" size="small">编辑</a-button>
</div>
</div>
</div>
</div>
</div>
<!-- 审批流程 -->
<div v-show="activeTab === 'approvals'" class="glass rounded-2xl p-6">
<div class="flex items-center justify-between mb-4">
<h3 class="font-bold text-lg text-gray-800">
<i class="fas fa-tasks text-orange-500 mr-2"></i>
待审批列表
</h3>
</div>
<a-table
:dataSource="approvals"
:pagination="{ pageSize: 10 }"
rowKey="id"
:scroll="{ x: 900 }"
>
<a-table-column title="类型" dataIndex="type" width="100" align="center">
<template #default="{ text }">
<a-tag :color="typeMap[text]?.color">{{ typeMap[text]?.label }}</a-tag>
</template>
</a-table-column>
<a-table-column title="标题" dataIndex="title" width="200">
<template #default="{ text }">
<span class="font-medium">{{ text }}</span>
</template>
</a-table-column>
<a-table-column title="申请人" dataIndex="applicant" width="100" />
<a-table-column title="部门" dataIndex="department" width="100" />
<a-table-column title="金额/时长" dataIndex="amount" width="100" align="right">
<template #default="{ text }">
<span class="font-medium text-orange-600">{{ text }}</span>
</template>
</a-table-column>
<a-table-column title="状态" dataIndex="status" width="100" align="center">
<template #default="{ text }">
<a-tag :color="statusMap[text]?.color">{{ statusMap[text]?.label }}</a-tag>
</template>
</a-table-column>
<a-table-column title="申请日期" dataIndex="date" width="120" />
<a-table-column title="操作" width="120" align="center" fixed="right">
<template #default="{ record }">
<div class="flex items-center gap-2 justify-center">
<a-button type="primary" size="small" ghost @click="showApproveDetail(record)">
审批
</a-button>
</div>
</template>
</a-table-column>
</a-table>
</div>
<!-- 会议管理 -->
<div v-show="activeTab === 'meetings'" class="glass rounded-2xl p-6">
<div class="flex items-center justify-between mb-4">
<h3 class="font-bold text-lg text-gray-800">
<i class="fas fa-video text-blue-500 mr-2"></i>
会议预约
</h3>
<a-button type="primary">
<template #icon><i class="fas fa-plus mr-1"></i></template>
预约会议
</a-button>
</div>
<a-empty description="暂无会议安排" />
</div>
<!-- 发布公告弹窗 -->
<a-modal
v-model:open="addAnnouncementVisible"
title="发布公告"
@ok="handleAddAnnouncement"
ok-text="立即发布"
cancel-text="取消"
width="600px"
>
<a-form :model="addAnnouncementForm" layout="vertical">
<a-form-item label="公告标题" required>
<a-input v-model:value="addAnnouncementForm.title" placeholder="请输入公告标题" />
</a-form-item>
<a-form-item label="公告内容" required>
<a-textarea v-model:value="addAnnouncementForm.content" :rows="5" placeholder="请输入公告内容" />
</a-form-item>
<a-form-item>
<a-checkbox v-model:checked="addAnnouncementForm.important">设为重要公告</a-checkbox>
</a-form-item>
</a-form>
</a-modal>
<!-- 审批详情弹窗 -->
<a-modal
v-model:open="approveVisible"
:title="`审批 - ${selectedApproval?.title}`"
width="500px"
:footer="selectedApproval?.status === 'pending' ? [
h('a-button', { type: 'primary', onClick: () => handleApprove('approved') }, '批准'),
h('a-button', { danger: true, onClick: () => handleApprove('rejected') }, '驳回'),
h('a-button', { onClick: () => approveVisible = false }, '取消'),
] : null"
>
<template v-if="selectedApproval">
<a-descriptions :column="2" bordered size="small">
<a-descriptions-item label="申请类型" :span="2">
<a-tag :color="typeMap[selectedApproval.type]?.color">{{ typeMap[selectedApproval.type]?.label }}</a-tag>
</a-descriptions-item>
<a-descriptions-item label="申请人">{{ selectedApproval.applicant }}</a-descriptions-item>
<a-descriptions-item label="部门">{{ selectedApproval.department }}</a-descriptions-item>
<a-descriptions-item label="申请金额/时长">{{ selectedApproval.amount }}</a-descriptions-item>
<a-descriptions-item label="申请日期">{{ selectedApproval.date }}</a-descriptions-item>
<a-descriptions-item label="当前状态" :span="2">
<a-tag :color="statusMap[selectedApproval.status]?.color">{{ statusMap[selectedApproval.status]?.label }}</a-tag>
</a-descriptions-item>
</a-descriptions>
</template>
</a-modal>
</div>
</template>
<style scoped>
.office-page {
padding: 0;
}
.page-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
}
.glass {
background: rgba(255, 255, 255, 0.85);
backdrop-filter: blur(20px);
border: 1px solid rgba(255, 255, 255, 0.3);
}
.card-hover {
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.card-hover:hover {
transform: translateY(-4px);
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
}
.notice-item {
background: rgba(255, 255, 255, 0.6);
}
.grid {
display: grid;
}
.grid-cols-4 {
grid-template-columns: repeat(4, 1fr);
}
@media (max-width: 1200px) {
.grid-cols-4 {
grid-template-columns: repeat(2, 1fr);
}
}
.space-y-4 > * + * {
margin-top: 16px;
}
.mb-6 {
margin-bottom: 24px;
}
.mb-4 {
margin-bottom: 16px;
}
.mb-3 {
margin-bottom: 12px;
}
.mb-2 {
margin-bottom: 8px;
}
.mr-1 {
margin-right: 4px;
}
.mr-2 {
margin-right: 8px;
}
.ml-4 {
margin-left: 16px;
}
.text-2xl {
font-size: 24px;
}
.text-3xl {
font-size: 30px;
}
.text-lg {
font-size: 18px;
}
.text-sm {
font-size: 14px;
}
.text-xs {
font-size: 12px;
}
.rounded-2xl {
border-radius: 16px;
}
.rounded-xl {
border-radius: 12px;
}
.p-6 {
padding: 24px;
}
.p-5 {
padding: 20px;
}
.p-4 {
padding: 16px;
}
.gap-6 {
gap: 24px;
}
.gap-2 {
gap: 8px;
}
.gap-4 {
gap: 16px;
}
.flex {
display: flex;
}
.items-center {
align-items: center;
}
.items-start {
align-items: flex-start;
}
.justify-between {
justify-content: space-between;
}
.flex-1 {
flex: 1;
}
.font-bold {
font-weight: 700;
}
.font-medium {
font-weight: 500;
}
.text-gray-800 {
color: #1f2937;
}
.text-gray-500 {
color: #6b7280;
}
.text-gray-400 {
color: #9ca3af;
}
.text-orange-600 {
color: #ea580c;
}
.line-clamp-2 {
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.border-l-4 {
border-left-width: 4px;
}
.border-red-500 {
border-left-color: #ef4444;
}
.border-blue-500 {
border-left-color: #3b82f6;
}
</style>

328
app/pages/admin/members.vue Normal file
View File

@@ -0,0 +1,328 @@
<template>
<div class="members-page">
<a-card :bordered="false">
<template #title>成员管理</template>
<!-- Tab 切换 -->
<a-tabs v-model:activeKey="activeTab">
<a-tab-pane key="users" tab="用户列表">
<!-- 筛选栏 -->
<div class="filter-bar">
<a-input-search
v-model:value="searchKeyword"
placeholder="搜索用户名、邮箱、手机号..."
style="width: 300px"
allow-clear
@search="handleSearch"
/>
<a-select v-model:value="filterRole" placeholder="角色筛选" style="width: 140px" allow-clear>
<a-select-option value="super_admin">超级管理员</a-select-option>
<a-select-option value="admin">企业管理员</a-select-option>
<a-select-option value="member">普通成员</a-select-option>
<a-select-option value="developer">开发者</a-select-option>
</a-select>
<a-select v-model:value="filterStatus" placeholder="状态筛选" style="width: 140px" allow-clear>
<a-select-option value="active">正常</a-select-option>
<a-select-option value="disabled">已禁用</a-select-option>
</a-select>
<a-button @click="resetFilter">重置</a-button>
</div>
<a-table
:columns="userColumns"
:data-source="userData"
:pagination="pagination"
row-key="id"
:scroll="{ x: 1100 }"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'user'">
<div class="user-cell">
<a-avatar :src="record.avatar" :size="36">
{{ record.name[0] }}
</a-avatar>
<div>
<p class="name-text">{{ record.name }}</p>
<p class="sub-text">{{ record.email }}</p>
</div>
</div>
</template>
<template v-else-if="column.key === 'role'">
<a-tag :color="roleColor[record.role]">{{ roleMap[record.role] }}</a-tag>
</template>
<template v-else-if="column.key === 'enterprise'">
{{ record.enterprise || '-' }}
</template>
<template v-else-if="column.key === 'status'">
<a-badge :status="record.status === 'active' ? 'success' : 'error'" :text="record.status === 'active' ? '正常' : '已禁用'" />
</template>
<template v-else-if="column.key === 'actions'">
<a-space>
<a-button type="link" size="small" @click="handleViewUser(record)">详情</a-button>
<a-button type="link" size="small" @click="handleEditUser(record)">编辑</a-button>
<a-dropdown>
<a-button type="link" size="small">更多</a-button>
<template #overlay>
<a-menu>
<a-menu-item key="role">调整角色</a-menu-item>
<a-menu-item key="disable">{{ record.status === 'active' ? '禁用账户' : '启用账户' }}</a-menu-item>
<a-menu-divider />
<a-menu-item key="delete" style="color: #ff4d4f">删除</a-menu-item>
</a-menu>
</template>
</a-dropdown>
</a-space>
</template>
</template>
</a-table>
</a-tab-pane>
<a-tab-pane key="roles" tab="角色权限">
<div class="roles-section">
<div class="roles-header">
<a-button type="primary" @click="roleModalVisible = true">
<template #icon><PlusOutlined /></template>
新增角色
</a-button>
</div>
<a-row :gutter="[16, 16]">
<a-col :xs="24" :md="12" :xl="8" v-for="role in roles" :key="role.key">
<a-card class="role-card" :bordered="false">
<div class="role-card-header">
<a-tag :color="role.color">{{ role.count }} </a-tag>
<span class="role-name">{{ role.label }}</span>
</div>
<p class="role-desc">{{ role.desc }}</p>
<div class="role-permissions">
<a-tag v-for="p in role.permissions" :key="p" size="small">{{ p }}</a-tag>
</div>
<div class="role-actions">
<a-button type="link" size="small" @click="handleEditRole(role)">编辑</a-button>
<a-button type="link" size="small" danger>删除</a-button>
</div>
</a-card>
</a-col>
</a-row>
</div>
</a-tab-pane>
</a-tabs>
</a-card>
<!-- 用户编辑弹窗 -->
<a-modal
v-model:open="userModalVisible"
title="编辑用户"
width="540px"
@ok="saveUser"
@cancel="userModalVisible = false"
>
<a-form :model="userForm" :label-col="{ span: 6 }" :wrapper-col="{ span: 16 }">
<a-form-item label="用户名">
<a-input v-model:value="userForm.name" />
</a-form-item>
<a-form-item label="手机号">
<a-input v-model:value="userForm.phone" />
</a-form-item>
<a-form-item label="邮箱">
<a-input v-model:value="userForm.email" />
</a-form-item>
<a-form-item label="角色">
<a-select v-model:value="userForm.role">
<a-select-option value="super_admin">超级管理员</a-select-option>
<a-select-option value="admin">企业管理员</a-select-option>
<a-select-option value="member">普通成员</a-select-option>
<a-select-option value="developer">开发者</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="状态">
<a-select v-model:value="userForm.status">
<a-select-option value="active">正常</a-select-option>
<a-select-option value="disabled">已禁用</a-select-option>
</a-select>
</a-form-item>
</a-form>
</a-modal>
<!-- 角色编辑弹窗 -->
<a-modal
v-model:open="roleModalVisible"
title="新增角色"
width="540px"
@ok="saveRole"
@cancel="roleModalVisible = false"
>
<a-form :model="roleForm" :label-col="{ span: 6 }" :wrapper-col="{ span: 16 }">
<a-form-item label="角色名称" :rules="[{ required: true, message: '请输入角色名称' }]">
<a-input v-model:value="roleForm.label" placeholder="如:财务管理员" />
</a-form-item>
<a-form-item label="角色描述">
<a-textarea v-model:value="roleForm.desc" :rows="2" />
</a-form-item>
<a-form-item label="权限配置">
<a-checkbox-group v-model:value="roleForm.permissions">
<a-row>
<a-col :span="12" v-for="perm in allPermissions" :key="perm.key">
<a-checkbox :value="perm.key">{{ perm.label }}</a-checkbox>
</a-col>
</a-row>
</a-checkbox-group>
</a-form-item>
</a-form>
</a-modal>
</div>
</template>
<script setup lang="ts">
import { PlusOutlined } from '@ant-design/icons-vue'
import { message } from 'ant-design-vue'
definePageMeta({ layout: 'admin' })
const activeTab = ref('users')
const searchKeyword = ref('')
const filterRole = ref<string | undefined>()
const filterStatus = ref<string | undefined>()
const userModalVisible = ref(false)
const roleModalVisible = ref(false)
const userForm = reactive({ id: null as number | null, name: '', phone: '', email: '', role: 'member', status: 'active' })
const roleForm = reactive({ label: '', desc: '', permissions: [] as string[] })
const roleMap: Record<string, string> = {
super_admin: '超级管理员',
admin: '企业管理员',
member: '普通成员',
developer: '开发者',
}
const roleColor: Record<string, string> = {
super_admin: 'red',
admin: 'purple',
member: 'blue',
developer: 'green',
}
const userColumns = [
{ title: '用户', key: 'user', width: 240 },
{ title: '手机号', dataIndex: 'phone', key: 'phone', width: 130 },
{ title: '角色', key: 'role', width: 110 },
{ title: '所属企业', key: 'enterprise', width: 160 },
{ title: '注册时间', dataIndex: 'createdAt', key: 'createdAt', width: 120 },
{ title: '最后登录', dataIndex: 'lastLogin', key: 'lastLogin', width: 140 },
{ title: '状态', key: 'status', width: 90 },
{ title: '操作', key: 'actions', width: 180, fixed: 'right' },
]
const userData = ref([
{ id: 1, name: '李明', email: 'liming@example.com', phone: '138****8001', role: 'super_admin', enterprise: '平台', status: 'active', createdAt: '2025-01-01', lastLogin: '2026-04-08 10:23' },
{ id: 2, name: '王芳', email: 'wangfang@example.com', phone: '138****8002', role: 'admin', enterprise: '腾云科技', status: 'active', createdAt: '2025-03-15', lastLogin: '2026-04-08 09:15' },
{ id: 3, name: '张伟', email: 'zhangwei@example.com', phone: '138****8003', role: 'member', enterprise: '腾云科技', status: 'active', createdAt: '2025-06-20', lastLogin: '2026-04-07 18:30' },
{ id: 4, name: '陈静', email: 'chenjing@example.com', phone: '138****8004', role: 'developer', enterprise: '华创数据', status: 'active', createdAt: '2025-09-10', lastLogin: '2026-04-08 11:00' },
{ id: 5, name: '刘强', email: 'liuqiang@example.com', phone: '138****8005', role: 'member', enterprise: '华创数据', status: 'disabled', createdAt: '2025-11-05', lastLogin: '2026-03-01 14:00' },
])
const pagination = reactive({ current: 1, pageSize: 10, total: 5 })
const roles = [
{ key: 'super_admin', label: '超级管理员', desc: '平台最高权限,可管理所有模块', count: 1, color: 'red', permissions: ['全部权限'] },
{ key: 'admin', label: '企业管理员', desc: '管理本企业内的用户、配置、账单', count: 12, color: 'purple', permissions: ['用户管理', '账单查看', '应用管理'] },
{ key: 'member', label: '普通成员', desc: '使用平台基础功能,无管理权限', count: 845, color: 'blue', permissions: ['功能使用'] },
{ key: 'developer', label: '开发者', desc: '拥有开发者权限,可创建和管理应用', count: 342, color: 'green', permissions: ['API调用', '插件开发', '模板发布'] },
]
const allPermissions = [
{ key: 'user_manage', label: '用户管理' },
{ key: 'app_manage', label: '应用管理' },
{ key: 'finance_view', label: '账单查看' },
{ key: 'finance_pay', label: '充值缴费' },
{ key: 'developer_api', label: 'API 调用' },
{ key: 'developer_plugin', label: '插件开发' },
{ key: 'setting_base', label: '基础配置' },
{ key: 'setting_advance', label: '高级配置' },
]
const handleSearch = () => message.info('搜索:' + searchKeyword.value)
const resetFilter = () => { searchKeyword.value = ''; filterRole.value = undefined; filterStatus.value = undefined }
const handleViewUser = (u: any) => message.info('查看用户:' + u.name)
const handleEditUser = (u: any) => { Object.assign(userForm, u); userModalVisible.value = true }
const handleEditRole = (r: any) => { Object.assign(roleForm, r); roleModalVisible.value = true }
const saveUser = () => { userModalVisible.value = false; message.success('保存成功') }
const saveRole = () => { roleModalVisible.value = false; message.success('保存成功') }
</script>
<style scoped>
.filter-bar {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 16px;
flex-wrap: wrap;
}
.user-cell {
display: flex;
align-items: center;
gap: 10px;
}
.name-text {
font-weight: 500;
color: #111827;
margin: 0;
}
.sub-text {
font-size: 12px;
color: #9ca3af;
margin: 0;
}
.roles-section {
padding-top: 4px;
}
.roles-header {
margin-bottom: 16px;
}
.role-card {
border-radius: 10px;
transition: box-shadow 0.2s;
}
.role-card:hover {
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08);
}
.role-card-header {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 8px;
}
.role-name {
font-weight: 600;
font-size: 15px;
color: #111827;
}
.role-desc {
font-size: 13px;
color: #6b7280;
margin: 0 0 10px;
}
.role-permissions {
display: flex;
flex-wrap: wrap;
gap: 4px;
margin-bottom: 10px;
}
.role-actions {
display: flex;
gap: 4px;
}
</style>

View File

@@ -0,0 +1,96 @@
<template>
<div class="roles-page">
<a-card :bordered="false" title="角色权限管理">
<template #extra>
<a-button type="primary" @click="modalVisible = true">
<template #icon><PlusOutlined /></template>
新增角色
</a-button>
</template>
<a-row :gutter="[16, 16]">
<a-col :xs="24" :md="12" :xl="8" v-for="role in roles" :key="role.key">
<a-card class="role-card" :bordered="false">
<div class="role-header">
<span class="role-name">{{ role.label }}</span>
<a-tag :color="role.color">{{ role.count }} </a-tag>
</div>
<p class="role-desc">{{ role.desc }}</p>
<div class="role-perms">
<a-tag v-for="p in role.permissions" :key="p" size="small">{{ p }}</a-tag>
</div>
<div class="role-actions">
<a-button type="link" size="small" @click="handleEdit(role)">编辑</a-button>
<a-button type="link" size="small" danger>删除</a-button>
</div>
</a-card>
</a-col>
</a-row>
</a-card>
<a-modal v-model:open="modalVisible" :title="editingRole ? '编辑角色' : '新增角色'" width="540px" @ok="handleSubmit">
<a-form :model="form" :label-col="{ span: 6 }" :wrapper-col="{ span: 16 }">
<a-form-item label="角色名称">
<a-input v-model:value="form.label" />
</a-form-item>
<a-form-item label="角色描述">
<a-textarea v-model:value="form.desc" :rows="2" />
</a-form-item>
<a-form-item label="权限配置">
<a-checkbox-group v-model:value="form.permissions">
<a-row>
<a-col :span="12" v-for="perm in allPermissions" :key="perm.key">
<a-checkbox :value="perm.key">{{ perm.label }}</a-checkbox>
</a-col>
</a-row>
</a-checkbox-group>
</a-form-item>
</a-form>
</a-modal>
</div>
</template>
<script setup lang="ts">
import { PlusOutlined } from '@ant-design/icons-vue'
import { message } from 'ant-design-vue'
definePageMeta({ layout: 'admin' })
const modalVisible = ref(false)
const editingRole = ref<any>(null)
const form = reactive({ label: '', desc: '', permissions: [] as string[] })
const roles = [
{ key: 'super_admin', label: '超级管理员', desc: '平台最高权限,可管理所有模块', count: 1, color: 'red', permissions: ['全部权限'] },
{ key: 'admin', label: '企业管理员', desc: '管理本企业内的用户、配置、账单', count: 12, color: 'purple', permissions: ['用户管理', '账单查看', '应用管理'] },
{ key: 'member', label: '普通成员', desc: '使用平台基础功能,无管理权限', count: 845, color: 'blue', permissions: ['功能使用'] },
{ key: 'developer', label: '开发者', desc: '拥有开发者权限,可创建和管理应用', count: 342, color: 'green', permissions: ['API调用', '插件开发', '模板发布'] },
]
const allPermissions = [
{ key: 'user_manage', label: '用户管理' },
{ key: 'app_manage', label: '应用管理' },
{ key: 'finance_view', label: '账单查看' },
{ key: 'finance_pay', label: '充值缴费' },
{ key: 'developer_api', label: 'API 调用' },
{ key: 'developer_plugin', label: '插件开发' },
{ key: 'setting_base', label: '基础配置' },
{ key: 'setting_advance', label: '高级配置' },
]
const handleEdit = (r: any) => { editingRole.value = r; Object.assign(form, r); modalVisible.value = true }
const handleSubmit = () => { modalVisible.value = false; message.success(editingRole.value ? '编辑成功' : '新增成功') }
</script>
<style scoped>
.role-card {
border-radius: 10px;
transition: box-shadow 0.2s;
}
.role-card:hover { box-shadow: 0 4px 16px rgba(0,0,0,0.08); }
.role-header { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; }
.role-name { font-weight: 600; font-size: 15px; color: #111827; }
.role-desc { font-size: 13px; color: #6b7280; margin: 0 0 10px; }
.role-perms { display: flex; flex-wrap: wrap; gap: 4px; margin-bottom: 10px; }
.role-actions { display: flex; gap: 4px; }
</style>

View File

@@ -0,0 +1,352 @@
<script setup lang="ts">
definePageMeta({ layout: 'admin' })
const { activeTab } = useNav()
activeTab.value = 'product-design'
// 产品数据
const products = ref([
{ id: 'PD-001', name: '精密轴承组件 A型', code: 'BA-A-001', category: '轴承类', version: 'V2.1', status: '设计中', progress: 75, designer: '张工', updateTime: '2026-04-08' },
{ id: 'PD-002', name: '液压缸体 B型', code: 'HA-B-002', category: '液压类', version: 'V1.5', status: '评审中', progress: 90, designer: '李工', updateTime: '2026-04-07' },
{ id: 'PD-003', name: '传动齿轮组 C型', code: 'GA-C-003', category: '传动类', version: 'V3.0', status: '已发布', progress: 100, designer: '王工', updateTime: '2026-04-01' },
{ id: 'PD-004', name: '密封圈组件 D型', code: 'SA-D-004', category: '密封类', version: 'V1.2', status: '设计中', progress: 45, designer: '赵工', updateTime: '2026-04-09' },
{ id: 'PD-005', name: '弹簧组件 E型', code: 'SA-E-005', category: '弹簧类', version: 'V2.0', status: '已发布', progress: 100, designer: '张工', updateTime: '2026-03-28' },
])
const statusMap: Record<string, string> = {
'设计中': 'processing',
'评审中': 'warning',
'已发布': 'success',
}
// 设计任务
const designTasks = ref([
{ id: 1, task: '优化轴承组件公差设计', assignee: '张工', deadline: '2026-04-15', priority: 'high' },
{ id: 2, task: '完成缸体3D建模', assignee: '李工', deadline: '2026-04-12', priority: 'medium' },
{ id: 3, task: '齿轮强度校核报告', assignee: '王工', deadline: '2026-04-10', priority: 'high' },
{ id: 4, task: '密封圈材料选型', assignee: '赵工', deadline: '2026-04-18', priority: 'low' },
])
const priorityMap: Record<string, string> = {
high: 'error',
medium: 'warning',
low: 'default',
}
// BOM 清单
const bomItems = ref([
{ no: 1, code: 'BA-A-001-01', name: '外圈', material: 'GCr15', qty: 2, unit: '件', supplier: '洛阳轴承' },
{ no: 2, code: 'BA-A-001-02', name: '内圈', material: 'GCr15', qty: 2, unit: '件', supplier: '洛阳轴承' },
{ no: 3, code: 'BA-A-001-03', name: '滚动体', material: 'Si3N4', qty: 12, unit: '件', supplier: '日本精工' },
{ no: 4, code: 'BA-A-001-04', name: '保持架', material: 'PA66', qty: 1, unit: '件', supplier: '本地供应商' },
])
const selectedProduct = ref<any>(null)
const showBomModal = ref(false)
const viewBom = (product: any) => {
selectedProduct.value = product
showBomModal.value = true
}
const modal = reactive({ visible: false, title: '', record: null as any })
const openModal = (title: string, record?: any) => {
modal.title = title
modal.record = record || {}
modal.visible = true
}
const statusFilter = ref<string[]>([])
</script>
<template>
<div class="page-container">
<div class="page-header">
<h2 class="page-title">产品设计</h2>
<a-button type="primary" @click="openModal('新增产品')">
<template #icon><PlusOutlined /></template>
新建产品
</a-button>
</div>
<a-row :gutter="[20, 20]">
<a-col :xs="24" :xl="16">
<!-- 产品列表 -->
<div class="card">
<div class="card-header">
<span class="card-title">产品设计列表</span>
<a-select
v-model:value="statusFilter"
mode="multiple"
placeholder="筛选状态"
style="width: 200px"
allowClear
>
<a-select-option value="设计中">设计中</a-select-option>
<a-select-option value="评审中">评审中</a-select-option>
<a-select-option value="已发布">已发布</a-select-option>
</a-select>
</div>
<a-table
:dataSource="products"
:pagination="{ pageSize: 10 }"
size="small"
rowKey="id"
>
<a-table-column title="产品编号" dataIndex="id" width="100" />
<a-table-column title="产品名称" dataIndex="name">
<template #default="{ record }">
<a @click="viewBom(record)" class="link">{{ record.name }}</a>
</template>
</a-table-column>
<a-table-column title="产品编码" dataIndex="code" width="120" />
<a-table-column title="类别" dataIndex="category" width="100" />
<a-table-column title="版本" dataIndex="version" width="80" />
<a-table-column title="状态" dataIndex="status" width="100" align="center">
<template #default="{ text }">
<a-tag :color="statusMap[text]">{{ text }}</a-tag>
</template>
</a-table-column>
<a-table-column title="设计进度" dataIndex="progress" width="140" align="center">
<template #default="{ record }">
<a-progress :percent="record.progress" size="small" :showInfo="false" />
<span class="text-xs text-gray-400 ml-2">{{ record.progress }}%</span>
</template>
</a-table-column>
<a-table-column title="负责人" dataIndex="designer" width="80" align="center" />
<a-table-column title="更新时间" dataIndex="updateTime" width="110" />
<a-table-column title="操作" width="120" align="center" fixed="right">
<template #default="{ record }">
<a-space>
<a @click="openModal('编辑产品', record)">编辑</a>
<a-divider type="vertical" />
<a-popconfirm title="确认删除?" ok-text="确认" cancel-text="取消">
<a class="danger">删除</a>
</a-popconfirm>
</a-space>
</template>
</a-table-column>
</a-table>
</div>
</a-col>
<a-col :xs="24" :xl="8">
<!-- 设计任务 -->
<div class="card mb-6">
<div class="card-header">
<span class="card-title">设计任务</span>
<a-button type="link" size="small">全部</a-button>
</div>
<div class="task-list">
<div v-for="task in designTasks" :key="task.id" class="task-item">
<div class="task-header">
<span class="task-name">{{ task.task }}</span>
<a-tag :color="priorityMap[task.priority]" size="small">
{{ task.priority === 'high' ? '高' : task.priority === 'medium' ? '中' : '低' }}
</a-tag>
</div>
<div class="task-meta">
<span><UserOutlined /> {{ task.assignee }}</span>
<span><CalendarOutlined /> {{ task.deadline }}</span>
</div>
</div>
</div>
</div>
<!-- 设计统计 -->
<div class="card">
<div class="card-title">设计统计</div>
<div class="stat-row">
<div class="stat-item">
<span class="stat-num">12</span>
<span class="stat-lbl">设计中</span>
</div>
<div class="stat-item">
<span class="stat-num">5</span>
<span class="stat-lbl">评审中</span>
</div>
<div class="stat-item">
<span class="stat-num">28</span>
<span class="stat-lbl">已发布</span>
</div>
</div>
</div>
</a-col>
</a-row>
<!-- BOM 弹窗 -->
<a-modal
v-model:open="showBomModal"
:title="`BOM清单 - ${selectedProduct?.name || ''}`"
width="700px"
:footer="null"
>
<a-table
:dataSource="bomItems"
:pagination="false"
size="small"
rowKey="no"
>
<a-table-column title="序号" dataIndex="no" width="60" align="center" />
<a-table-column title="物料编码" dataIndex="code" width="130" />
<a-table-column title="物料名称" dataIndex="name" />
<a-table-column title="材质" dataIndex="material" width="100" />
<a-table-column title="用量" dataIndex="qty" width="60" align="center" />
<a-table-column title="单位" dataIndex="unit" width="60" align="center" />
<a-table-column title="供应商" dataIndex="supplier" />
</a-table>
</a-modal>
<!-- 新增/编辑弹窗 -->
<a-modal
v-model:open="modal.visible"
:title="modal.title"
width="500px"
@ok="modal.visible = false"
>
<a-form layout="vertical">
<a-form-item label="产品名称">
<a-input v-model:value="modal.record.name" placeholder="请输入产品名称" />
</a-form-item>
<a-form-item label="产品编码">
<a-input v-model:value="modal.record.code" placeholder="请输入产品编码" />
</a-form-item>
<a-form-item label="产品类别">
<a-select v-model:value="modal.record.category" placeholder="请选择类别">
<a-select-option value="轴承类">轴承类</a-select-option>
<a-select-option value="液压类">液压类</a-select-option>
<a-select-option value="传动类">传动类</a-select-option>
<a-select-option value="密封类">密封类</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="负责人">
<a-select v-model:value="modal.record.designer" placeholder="请选择负责人">
<a-select-option value="张工">张工</a-select-option>
<a-select-option value="李工">李工</a-select-option>
<a-select-option value="王工">王工</a-select-option>
<a-select-option value="赵工">赵工</a-select-option>
</a-select>
</a-form-item>
</a-form>
</a-modal>
</div>
</template>
<style scoped>
.page-container {
padding: 24px;
}
.page-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.page-title {
font-size: 20px;
font-weight: 600;
color: #1f2937;
margin: 0;
}
.card {
background: white;
border-radius: 12px;
padding: 20px;
box-shadow: 0 1px 3px rgba(0,0,0,0.06);
border: 1px solid #f0f0f0;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
}
.card-title {
font-size: 16px;
font-weight: 600;
color: #1f2937;
margin-bottom: 16px;
}
.task-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.task-item {
padding: 12px;
background: #fafafa;
border-radius: 8px;
}
.task-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
.task-name {
font-size: 13px;
font-weight: 500;
color: #374151;
}
.task-meta {
display: flex;
gap: 16px;
font-size: 12px;
color: #9ca3af;
}
.task-meta span {
display: flex;
align-items: center;
gap: 4px;
}
.stat-row {
display: flex;
justify-content: space-around;
}
.stat-item {
text-align: center;
}
.stat-num {
display: block;
font-size: 28px;
font-weight: 700;
color: #4f46e5;
}
.stat-lbl {
font-size: 12px;
color: #9ca3af;
}
.link {
color: #4f46e5;
cursor: pointer;
}
.link:hover {
text-decoration: underline;
}
.danger {
color: #ef4444;
}
.mb-6 {
margin-bottom: 20px;
}
</style>

View File

@@ -0,0 +1,169 @@
<script setup lang="ts">
definePageMeta({ layout: 'admin' })
const { activeTab } = useNav()
activeTab.value = 'product-marketing'
// 销售数据
const salesData = ref([
{ month: '1月', orders: 245, revenue: 128.5, target: 120 },
{ month: '2月', orders: 312, revenue: 156.8, target: 130 },
{ month: '3月', orders: 278, revenue: 142.3, target: 135 },
])
const stats = ref([
{ label: '本月订单', value: 156, unit: '单', change: '+12%', icon: '📋', color: '#6366f1' },
{ label: '本月营收', value: 86.5, unit: '万', change: '+8%', icon: '💰', color: '#10b981' },
{ label: '新增客户', value: 23, unit: '家', change: '+15%', icon: '🏢', color: '#f59e0b' },
{ label: '平均单价', value: 5546, unit: '元', change: '-2%', icon: '💎', color: '#3b82f6' },
])
// 客户列表
const customers = ref([
{ id: 'C001', name: '比亚迪股份有限公司', industry: '汽车制造', contact: '李经理', phone: '0755-89888888', orderCount: 45, amount: 280.5, status: 'VIP' },
{ id: 'C002', name: '宁德时代新能源', industry: '新能源', contact: '王经理', phone: '0591-87654321', orderCount: 32, amount: 198.2, status: 'VIP' },
{ id: 'C003', name: '华为技术有限公司', industry: '电子通信', contact: '张经理', phone: '0755-28780808', orderCount: 28, amount: 156.8, status: '重点' },
{ id: 'C004', name: '富士康科技集团', industry: '电子制造', contact: '刘经理', phone: '0755-28129999', orderCount: 21, amount: 125.3, status: '普通' },
{ id: 'C005', name: '美的集团', industry: '家电制造', contact: '陈经理', phone: '0757-26608888', orderCount: 18, amount: 98.6, status: '重点' },
])
// 跟进记录
const followRecords = ref([
{ id: 1, customer: '比亚迪股份有限公司', content: '拜访客户沟通轴承采购需求预计月订单量增加30%', contact: '李经理', nextDate: '2026-04-15', status: '跟进中' },
{ id: 2, customer: '宁德时代新能源', content: '技术方案对接完成,等待客户内部评审', contact: '王经理', nextDate: '2026-04-12', status: '待联系' },
{ id: 3, customer: '华为技术有限公司', content: '完成样品交付,客户反馈良好', contact: '张经理', nextDate: '-', status: '已完成' },
])
const statusColor: Record<string, string> = {
'VIP': 'purple',
'重点': 'blue',
'普通': 'default',
}
</script>
<template>
<div class="page-container">
<div class="page-header">
<h2 class="page-title">营销管理</h2>
<a-space>
<a-button @click="() => {}">导出数据</a-button>
<a-button type="primary" @click="() => {}">
<template #icon><PlusOutlined /></template>
新建客户
</a-button>
</a-space>
</div>
<!-- 统计卡片 -->
<a-row :gutter="[16, 16]" class="mb-6">
<a-col :xs="12" :sm="6" v-for="stat in stats" :key="stat.label">
<div class="stat-card" :style="{ '--accent': stat.color }">
<div class="stat-icon">{{ stat.icon }}</div>
<div class="stat-body">
<div class="stat-value">{{ stat.value }}<span class="stat-unit">{{ stat.unit }}</span></div>
<div class="stat-label">{{ stat.label }}</div>
<div :class="['stat-change', stat.change.startsWith('+') ? 'up' : 'down']">{{ stat.change }}</div>
</div>
</div>
</a-col>
</a-row>
<a-row :gutter="[20, 20]">
<a-col :xs="24" :xl="16">
<!-- 客户列表 -->
<div class="card">
<div class="card-title">客户列表</div>
<a-table :dataSource="customers" :pagination="{ pageSize: 8 }" size="small" rowKey="id">
<a-table-column title="客户编码" dataIndex="id" width="90" />
<a-table-column title="客户名称" dataIndex="name" />
<a-table-column title="行业" dataIndex="industry" width="100" />
<a-table-column title="联系人" dataIndex="contact" width="80" />
<a-table-column title="订单数" dataIndex="orderCount" width="80" align="center" />
<a-table-column title="累计金额(万)" dataIndex="amount" width="110" align="right" />
<a-table-column title="等级" dataIndex="status" width="80" align="center">
<template #default="{ text }">
<a-tag :color="statusColor[text]">{{ text }}</a-tag>
</template>
</a-table-column>
<a-table-column title="操作" width="100" align="center">
<template #default>
<a-space>
<a>详情</a>
<a-divider type="vertical" />
<a>跟进</a>
</a-space>
</template>
</a-table-column>
</a-table>
</div>
</a-col>
<a-col :xs="24" :xl="8">
<!-- 跟进记录 -->
<div class="card">
<div class="card-title">跟进记录</div>
<div class="follow-list">
<div v-for="record in followRecords" :key="record.id" class="follow-item">
<div class="follow-header">
<span class="follow-customer">{{ record.customer }}</span>
<a-tag size="small" :color="record.status === '已完成' ? 'success' : record.status === '跟进中' ? 'processing' : 'warning'">
{{ record.status }}
</a-tag>
</div>
<div class="follow-content">{{ record.content }}</div>
<div class="follow-meta">
<span><UserOutlined /> {{ record.contact }}</span>
<span v-if="record.nextDate !== '-'"><CalendarOutlined /> 下次: {{ record.nextDate }}</span>
</div>
</div>
</div>
</div>
</a-col>
</a-row>
</div>
</template>
<style scoped>
.page-container { padding: 24px; }
.page-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; }
.page-title { font-size: 20px; font-weight: 600; color: #1f2937; margin: 0; }
.stat-card {
background: white;
border-radius: 12px;
padding: 16px;
display: flex;
align-items: center;
gap: 12px;
box-shadow: 0 1px 3px rgba(0,0,0,0.06);
border: 1px solid #f0f0f0;
position: relative;
overflow: hidden;
}
.stat-card::before {
content: '';
position: absolute;
top: 0; left: 0;
width: 4px; height: 100%;
background: var(--accent);
}
.stat-icon { font-size: 28px; }
.stat-value { font-size: 22px; font-weight: 700; color: #1f2937; }
.stat-unit { font-size: 12px; color: #9ca3af; margin-left: 2px; }
.stat-label { font-size: 12px; color: #9ca3af; margin: 2px 0; }
.stat-change { font-size: 12px; }
.stat-change.up { color: #10b981; }
.stat-change.down { color: #ef4444; }
.card { background: white; border-radius: 12px; padding: 20px; box-shadow: 0 1px 3px rgba(0,0,0,0.06); border: 1px solid #f0f0f0; }
.card-title { font-size: 16px; font-weight: 600; color: #1f2937; margin-bottom: 16px; }
.follow-list { display: flex; flex-direction: column; gap: 12px; }
.follow-item { padding: 12px; background: #fafafa; border-radius: 8px; }
.follow-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 6px; }
.follow-customer { font-size: 13px; font-weight: 600; color: #374151; }
.follow-content { font-size: 12px; color: #6b7280; margin-bottom: 8px; line-height: 1.5; }
.follow-meta { display: flex; gap: 16px; font-size: 11px; color: #9ca3af; }
.follow-meta span { display: flex; align-items: center; gap: 4px; }
.mb-6 { margin-bottom: 20px; }
</style>

View File

@@ -0,0 +1,127 @@
<script setup lang="ts">
definePageMeta({ layout: 'admin' })
const { activeTab } = useNav()
activeTab.value = 'product-service'
const activeTabKey = ref('tickets')
const tabs = [
{ key: 'tickets', label: '服务工单' },
{ key: 'feedback', label: '客户反馈' },
{ key: 'warranty', label: '质保管理' },
]
// 服务工单
const tickets = ref([
{ id: 'ST-2026040901', customer: '比亚迪股份有限公司', product: '精密轴承组件 A型', type: '技术支持', status: '处理中', assignee: '张工', createTime: '2026-04-09 10:30', urgent: 'high' },
{ id: 'ST-2026040802', customer: '宁德时代新能源', product: '液压缸体 B型', type: '故障报修', status: '待派单', assignee: '-', createTime: '2026-04-08 15:20', urgent: 'medium' },
{ id: 'ST-2026040801', customer: '华为技术有限公司', product: '传动齿轮组 C型', type: '咨询', status: '已完成', assignee: '李工', createTime: '2026-04-08 09:15', urgent: 'low' },
{ id: 'ST-2026040703', customer: '美的集团', product: '密封圈组件 D型', type: '技术支持', status: '已完成', assignee: '王工', createTime: '2026-04-07 14:00', urgent: 'medium' },
])
const statusColor: Record<string, string> = { '处理中': 'processing', '待派单': 'warning', '已完成': 'success' }
const urgentColor: Record<string, string> = { 'high': 'error', 'medium': 'warning', 'low': 'default' }
const urgentLabel: Record<string, string> = { 'high': '紧急', 'medium': '普通', 'low': '低' }
// 客户反馈
const feedback = ref([
{ id: 'FB-001', customer: '比亚迪股份有限公司', content: '产品性能稳定,交付及时,服务态度好', rating: 5, createTime: '2026-04-08' },
{ id: 'FB-002', customer: '宁德时代新能源', content: '希望增加技术培训频次', rating: 4, createTime: '2026-04-07' },
{ id: 'FB-003', customer: '华为技术有限公司', content: '产品质量可靠,但包装可以改进', rating: 4, createTime: '2026-04-06' },
{ id: 'FB-004', customer: '富士康科技集团', content: '技术响应速度需要提升', rating: 3, createTime: '2026-04-05' },
])
// 质保列表
const warranty = ref([
{ id: 'WA-001', product: '精密轴承组件 A型', batch: 'B-20260301-01', quantity: 500, warrantyPeriod: '2年', expireDate: '2028-03-01', status: '有效' },
{ id: 'WA-002', product: '液压缸体 B型', batch: 'B-20260215-02', quantity: 200, warrantyPeriod: '1年', expireDate: '2027-02-15', status: '有效' },
{ id: 'WA-003', product: '传动齿轮组 C型', batch: 'B-20251201-03', quantity: 1000, warrantyPeriod: '2年', expireDate: '2027-12-01', status: '即将到期' },
])
</script>
<template>
<div class="page-container">
<div class="page-header">
<h2 class="page-title">售后服务</h2>
<a-button type="primary">
<template #icon><PlusOutlined /></template>
新建工单
</a-button>
</div>
<a-tabs v-model:activeKey="activeTabKey">
<a-tab-pane key="tickets" tab="服务工单">
<div class="card">
<a-table :dataSource="tickets" :pagination="{ pageSize: 10 }" size="small" rowKey="id">
<a-table-column title="工单编号" dataIndex="id" width="150" />
<a-table-column title="客户" dataIndex="customer" />
<a-table-column title="产品" dataIndex="product" />
<a-table-column title="类型" dataIndex="type" width="100" align="center" />
<a-table-column title="紧急度" dataIndex="urgent" width="90" align="center">
<template #default="{ text }">
<a-tag :color="urgentColor[text]">{{ urgentLabel[text] }}</a-tag>
</template>
</a-table-column>
<a-table-column title="状态" dataIndex="status" width="100" align="center">
<template #default="{ text }">
<a-badge :status="statusColor[text] === 'processing' ? 'processing' : statusColor[text] === 'success' ? 'success' : 'warning'" :text="text" />
</template>
</a-table-column>
<a-table-column title="处理人" dataIndex="assignee" width="80" align="center" />
<a-table-column title="创建时间" dataIndex="createTime" width="150" />
<a-table-column title="操作" width="100" align="center">
<template #default>
<a>详情</a>
</template>
</a-table-column>
</a-table>
</div>
</a-tab-pane>
<a-tab-pane key="feedback" tab="客户反馈">
<div class="card">
<a-table :dataSource="feedback" :pagination="{ pageSize: 10 }" size="small" rowKey="id">
<a-table-column title="反馈编号" dataIndex="id" width="100" />
<a-table-column title="客户" dataIndex="customer" />
<a-table-column title="反馈内容" dataIndex="content" />
<a-table-column title="评分" dataIndex="rating" width="120" align="center">
<template #default="{ text }">
<a-rate :value="text" disabled :tooltips="['很差', '较差', '一般', '满意', '非常满意']" />
</template>
</a-table-column>
<a-table-column title="时间" dataIndex="createTime" width="120" />
</a-table>
</div>
</a-tab-pane>
<a-tab-pane key="warranty" tab="质保管理">
<div class="card">
<a-table :dataSource="warranty" :pagination="false" size="small" rowKey="id">
<a-table-column title="产品名称" dataIndex="product" />
<a-table-column title="批次号" dataIndex="batch" width="160" />
<a-table-column title="数量" dataIndex="quantity" width="80" align="center" />
<a-table-column title="质保期" dataIndex="warrantyPeriod" width="90" align="center" />
<a-table-column title="到期日期" dataIndex="expireDate" width="120" />
<a-table-column title="状态" dataIndex="status" width="100" align="center">
<template #default="{ text }">
<a-tag :color="text === '有效' ? 'success' : 'warning'">{{ text }}</a-tag>
</template>
</a-table-column>
<a-table-column title="操作" width="100" align="center">
<template #default>
<a>详情</a>
</template>
</a-table-column>
</a-table>
</div>
</a-tab-pane>
</a-tabs>
</div>
</template>
<style scoped>
.page-container { padding: 24px; }
.page-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; }
.page-title { font-size: 20px; font-weight: 600; color: #1f2937; margin: 0; }
.card { background: white; border-radius: 12px; padding: 20px; box-shadow: 0 1px 3px rgba(0,0,0,0.06); border: 1px solid #f0f0f0; }
</style>

View File

@@ -0,0 +1,113 @@
<script setup lang="ts">
definePageMeta({ layout: 'admin' })
const { activeTab } = useNav()
activeTab.value = 'production-control'
const realTimeData = ref([
{ id: 'WO2026040901', product: '精密轴承组件 A型', line: '产线1', planQty: 500, doneQty: 325, passQty: 318, passRate: 97.8, status: '生产中' },
{ id: 'WO2026040703', product: '密封圈组件 D型', line: '产线3', planQty: 3000, doneQty: 2100, passQty: 2058, passRate: 98.0, status: '生产中' },
])
const alerts = ref([
{ id: 1, type: 'warning', msg: '产线1主轴温度偏高当前78°C', time: '10:30' },
{ id: 2, type: 'info', msg: 'WO2026040901 完成500件当前进度65%', time: '10:15' },
{ id: 3, type: 'error', msg: '产线2刀具寿命预警请及时更换', time: '09:45' },
])
const activeTab2 = ref('realtime')
</script>
<template>
<div class="page-container">
<div class="page-header">
<h2 class="page-title">生产管控</h2>
<a-space>
<a-button @click="() => {}">导出报表</a-button>
<a-button type="primary">
<template #icon><PlusOutlined /></template>
新建工单
</a-button>
</a-space>
</div>
<a-tabs v-model:activeKey="activeTab2">
<a-tab-pane key="realtime" tab="实时监控">
<a-row :gutter="[20, 20]">
<a-col :xs="24" :xl="16">
<div class="card">
<div class="card-title">实时生产进度</div>
<a-table :dataSource="realTimeData" :pagination="false" size="small" rowKey="id">
<a-table-column title="工单编号" dataIndex="id" width="150" />
<a-table-column title="产品" dataIndex="product" />
<a-table-column title="产线" dataIndex="line" width="80" align="center" />
<a-table-column title="计划数量" dataIndex="planQty" width="100" align="center" />
<a-table-column title="完成数量" dataIndex="doneQty" width="100" align="center">
<template #default="{ record }">
<span class="num-highlight">{{ record.doneQty }}</span>
</template>
</a-table-column>
<a-table-column title="良品数量" dataIndex="passQty" width="100" align="center" />
<a-table-column title="良品率" dataIndex="passRate" width="90" align="center">
<template #default="{ text }">
<span :class="text >= 97 ? 'rate-good' : 'rate-warn'">{{ text }}%</span>
</template>
</a-table-column>
<a-table-column title="进度" dataIndex="doneQty" width="150" align="center">
<template #default="{ record }">
<a-progress :percent="Math.round(record.doneQty / record.planQty * 100)" size="small" />
</template>
</a-table-column>
<a-table-column title="状态" dataIndex="status" width="100" align="center">
<template #default="{ text }">
<a-badge status="processing" :text="text" />
</template>
</a-table-column>
</a-table>
</div>
</a-col>
<a-col :xs="24" :xl="8">
<div class="card">
<div class="card-title">实时告警</div>
<div class="alert-list">
<div v-for="alert in alerts" :key="alert.id" class="alert-item" :class="alert.type">
<span class="alert-icon">{{ alert.type === 'error' ? '🔴' : alert.type === 'warning' ? '🟡' : '🔵' }}</span>
<div class="alert-body">
<div class="alert-msg">{{ alert.msg }}</div>
<div class="alert-time">{{ alert.time }}</div>
</div>
</div>
</div>
</div>
</a-col>
</a-row>
</a-tab-pane>
<a-tab-pane key="history" tab="历史记录">
<div class="card">
<a-empty description="历史记录列表" />
</div>
</a-tab-pane>
</a-tabs>
</div>
</template>
<style scoped>
.page-container { padding: 24px; }
.page-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; }
.page-title { font-size: 20px; font-weight: 600; color: #1f2937; margin: 0; }
.card { background: white; border-radius: 12px; padding: 20px; box-shadow: 0 1px 3px rgba(0,0,0,0.06); border: 1px solid #f0f0f0; }
.card-title { font-size: 16px; font-weight: 600; color: #1f2937; margin-bottom: 16px; }
.num-highlight { font-weight: 700; color: #4f46e5; }
.rate-good { color: #10b981; font-weight: 600; }
.rate-warn { color: #f59e0b; font-weight: 600; }
.alert-list { display: flex; flex-direction: column; gap: 10px; }
.alert-item { display: flex; align-items: flex-start; gap: 10px; padding: 12px; border-radius: 8px; }
.alert-item.error { background: #fef2f2; }
.alert-item.warning { background: #fffbeb; }
.alert-item.info { background: #eff6ff; }
.alert-icon { font-size: 14px; }
.alert-msg { font-size: 13px; color: #374151; }
.alert-time { font-size: 11px; color: #9ca3af; margin-top: 4px; }
</style>

View File

@@ -0,0 +1,111 @@
<script setup lang="ts">
definePageMeta({ layout: 'admin' })
const { activeTab } = useNav()
activeTab.value = 'production-energy'
const stats = ref([
{ label: '今日用电', value: 2856, unit: 'kWh', cost: '¥2,142', icon: '⚡', color: '#f59e0b' },
{ label: '今日用水', value: 128, unit: 'm³', cost: '¥384', icon: '💧', color: '#3b82f6' },
{ label: '今日用气', value: 560, unit: 'm³', cost: '¥840', icon: '🌬️', color: '#6366f1' },
{ label: '碳排放', value: 1.8, unit: '吨', icon: '🌱', color: '#10b981' },
])
const energyUsage = ref([
{ name: 'CNC-01', electric: 450, water: 8, gas: 120, cost: 337.5 },
{ name: 'CNC-02', electric: 420, water: 7, gas: 115, cost: 315.0 },
{ name: 'CNC-03', electric: 0, water: 5, gas: 0, cost: 0 },
{ name: 'MILL-01', electric: 0, water: 3, gas: 0, cost: 0 },
{ name: 'MILL-02', electric: 380, water: 6, gas: 100, cost: 285.0 },
])
</script>
<template>
<div class="page-container">
<div class="page-header">
<h2 class="page-title">能耗管理</h2>
<a-space>
<a-select value="2026-04" style="width: 120px">
<a-select-option value="2026-04">2026年4月</a-select-option>
<a-select-option value="2026-03">2026年3月</a-select-option>
</a-select>
<a-button @click="() => {}">导出报表</a-button>
</a-space>
</div>
<a-row :gutter="[16, 16]" class="mb-6">
<a-col :xs="12" :sm="6" v-for="stat in stats" :key="stat.label">
<div class="stat-card" :style="{ '--c': stat.color }">
<div class="stat-icon">{{ stat.icon }}</div>
<div class="stat-body">
<div class="stat-value">{{ stat.value }}<span class="stat-unit">{{ stat.unit }}</span></div>
<div class="stat-sub">{{ stat.cost }}</div>
<div class="stat-label">{{ stat.label }}</div>
</div>
</div>
</a-col>
</a-row>
<div class="card">
<div class="card-title">设备能耗明细</div>
<a-table :dataSource="energyUsage" :pagination="false" size="small" rowKey="name">
<a-table-column title="设备" dataIndex="name" width="120" />
<a-table-column title="用电(kWh)" dataIndex="electric" width="120" align="center">
<template #default="{ text }">
<span :class="text > 0 ? 'val' : 'dim'">{{ text }}</span>
</template>
</a-table-column>
<a-table-column title="用水(m³)" dataIndex="water" width="120" align="center">
<template #default="{ text }">
<span :class="text > 0 ? 'val' : 'dim'">{{ text }}</span>
</template>
</a-table-column>
<a-table-column title="用气(m³)" dataIndex="gas" width="120" align="center">
<template #default="{ text }">
<span :class="text > 0 ? 'val' : 'dim'">{{ text }}</span>
</template>
</a-table-column>
<a-table-column title="能耗成本(元)" dataIndex="cost" width="130" align="right">
<template #default="{ text }">
<span :class="text > 0 ? 'val' : 'dim'">¥{{ text.toFixed(1) }}</span>
</template>
</a-table-column>
<a-table-column title="占比" dataIndex="cost" align="center">
<template #default="{ record, text }">
<a-progress
:percent="Math.round(text / energyUsage.reduce((a, b) => a + b.cost, 0) * 100)"
size="small"
:showInfo="false"
/>
</template>
</a-table-column>
</a-table>
<div class="total-row">
<span>合计</span>
<span>2856 kWh</span>
<span>128 </span>
<span>560 </span>
<span>¥3,366.00</span>
</div>
</div>
</div>
</template>
<style scoped>
.page-container { padding: 24px; }
.page-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; }
.page-title { font-size: 20px; font-weight: 600; color: #1f2937; margin: 0; }
.stat-card { background: white; border-radius: 12px; padding: 16px; display: flex; align-items: center; gap: 12px; box-shadow: 0 1px 3px rgba(0,0,0,0.06); border: 1px solid #f0f0f0; }
.stat-icon { font-size: 28px; }
.stat-value { font-size: 22px; font-weight: 700; color: #1f2937; }
.stat-unit { font-size: 12px; color: #9ca3af; margin-left: 2px; }
.stat-sub { font-size: 13px; color: #6b7280; }
.stat-label { font-size: 12px; color: #9ca3af; margin-top: 2px; }
.card { background: white; border-radius: 12px; padding: 20px; box-shadow: 0 1px 3px rgba(0,0,0,0.06); border: 1px solid #f0f0f0; }
.card-title { font-size: 16px; font-weight: 600; color: #1f2937; margin-bottom: 16px; }
.val { font-weight: 600; color: #374151; }
.dim { color: #d1d5db; }
.total-row { display: flex; justify-content: space-between; padding: 12px 8px; border-top: 1px solid #f0f0f0; font-weight: 600; font-size: 13px; color: #374151; }
.total-row span:nth-child(2), .total-row span:nth-child(3), .total-row span:nth-child(4) { width: 120px; text-align: center; }
.total-row span:nth-child(5) { width: 130px; text-align: right; }
.mb-6 { margin-bottom: 20px; }
</style>

View File

@@ -0,0 +1,617 @@
<script setup lang="ts">
definePageMeta({ layout: 'admin' })
// 设备统计数据
const equipmentStats = ref([
{ label: '设备总数', value: 128, icon: 'fa-cogs', color: 'blue', gradient: 'from-blue-500 to-purple-500', change: '+12%', up: true },
{ label: '运行中', value: 96, icon: 'fa-play-circle', color: 'green', gradient: 'from-green-500 to-teal-500', change: '+8%', up: true },
{ label: '待机中', value: 24, icon: 'fa-pause-circle', color: 'orange', gradient: 'from-orange-500 to-yellow-500', change: '-5%', up: false },
{ label: '故障告警', value: 8, icon: 'fa-exclamation-triangle', color: 'red', gradient: 'from-red-500 to-pink-500', change: '+2', up: false },
])
// 设备列表
const equipmentList = ref([
{ id: 'EQ-001', name: 'CNC加工中心 01', model: 'CNC-850', location: '1号车间 A区', status: 'running', utilization: 92, temp: 45, vibration: 0.8, lastMaintenance: '2026-04-01', nextMaintenance: '2026-05-01' },
{ id: 'EQ-002', name: 'CNC加工中心 02', model: 'CNC-850', location: '1号车间 A区', status: 'running', utilization: 88, temp: 43, vibration: 0.7, lastMaintenance: '2026-04-02', nextMaintenance: '2026-05-02' },
{ id: 'EQ-003', name: 'CNC加工中心 03', model: 'CNC-650', location: '1号车间 B区', status: 'warning', utilization: 0, temp: 78, vibration: 2.5, lastMaintenance: '2026-03-15', nextMaintenance: '2026-04-15' },
{ id: 'EQ-004', name: '数控铣床 01', model: 'XK-500', location: '2号车间', status: 'idle', utilization: 0, temp: 28, vibration: 0.3, lastMaintenance: '2026-03-28', nextMaintenance: '2026-04-28' },
{ id: 'EQ-005', name: '数控铣床 02', model: 'XK-500', location: '2号车间', status: 'running', utilization: 75, temp: 38, vibration: 0.6, lastMaintenance: '2026-04-03', nextMaintenance: '2026-05-03' },
{ id: 'EQ-006', name: '冲压机 01', model: 'CP-200T', location: '3号车间', status: 'running', utilization: 85, temp: 42, vibration: 1.2, lastMaintenance: '2026-03-20', nextMaintenance: '2026-04-20' },
{ id: 'EQ-007', name: '激光切割机 01', model: 'JC-3000', location: '3号车间', status: 'idle', utilization: 0, temp: 25, vibration: 0.2, lastMaintenance: '2026-04-05', nextMaintenance: '2026-05-05' },
{ id: 'EQ-008', name: '空压机 01', model: 'KY-50', location: '动力站房', status: 'running', utilization: 68, temp: 55, vibration: 1.5, lastMaintenance: '2026-03-10', nextMaintenance: '2026-05-10' },
])
// 筛选状态
const statusFilter = ref('all')
const searchKeyword = ref('')
// 筛选后的列表
const filteredList = computed(() => {
return equipmentList.value.filter((item) => {
const matchStatus = statusFilter.value === 'all' || item.status === statusFilter.value
const matchSearch = !searchKeyword.value ||
item.name.toLowerCase().includes(searchKeyword.value.toLowerCase()) ||
item.id.toLowerCase().includes(searchKeyword.value.toLowerCase())
return matchStatus && matchSearch
})
})
const statusMap: Record<string, { label: string; color: string; bg: string }> = {
running: { label: '运行中', color: 'text-green-600', bg: 'bg-green-100' },
idle: { label: '待机', color: 'text-orange-600', bg: 'bg-orange-100' },
warning: { label: '告警', color: 'text-red-600', bg: 'bg-red-100' },
offline: { label: '离线', color: 'text-gray-600', bg: 'bg-gray-100' },
}
// 设备详情
const selectedEquipment = ref<typeof equipmentList.value[0] | null>(null)
const detailVisible = ref(false)
function showDetail(item: typeof equipmentList.value[0]) {
selectedEquipment.value = item
detailVisible.value = true
}
// 维保记录
const maintenanceRecords = ref([
{ date: '2026-04-01', type: '例行保养', technician: '李师傅', cost: 500, status: '完成' },
{ date: '2026-03-01', type: '例行保养', technician: '李师傅', cost: 480, status: '完成' },
{ date: '2026-02-01', type: '更换配件', technician: '王师傅', cost: 1200, status: '完成' },
])
// 设备效率趋势(模拟数据)
const efficiencyTrend = ref([85, 88, 82, 90, 92, 88, 91, 89, 93, 92])
const days = ['周一', '周二', '周三', '周四', '周五', '周六', '周日', '周一', '周二', '今天']
// 添加设备表单
const addFormVisible = ref(false)
const addForm = reactive({
name: '',
model: '',
location: '',
supplier: '',
purchaseDate: '',
warrantyEnd: '',
})
function handleAdd() {
// 模拟添加
addFormVisible.value = false
message.success('设备添加成功')
}
</script>
<template>
<div class="equipment-page">
<!-- 页面标题 -->
<div class="page-header mb-6">
<div>
<h2 class="text-2xl font-bold text-gray-800">设备管理</h2>
<p class="text-gray-500 mt-1">实时监控设备状态优化生产效率</p>
</div>
<a-button type="primary" @click="addFormVisible = true">
<template #icon><i class="fas fa-plus mr-1"></i></template>
添加设备
</a-button>
</div>
<!-- 统计卡片 -->
<div class="grid grid-cols-4 gap-6 mb-6">
<div
v-for="stat in equipmentStats"
:key="stat.label"
class="glass rounded-2xl p-6 card-hover cursor-pointer"
>
<div class="flex items-center justify-between mb-4">
<div
class="w-12 h-12 rounded-xl flex items-center justify-center text-white"
:class="`bg-gradient-to-br ${stat.gradient}`"
>
<i :class="`fas ${stat.icon}`"></i>
</div>
<span
class="text-sm font-medium"
:class="stat.up ? 'text-green-500' : 'text-red-500'"
>
<i :class="stat.up ? 'fas fa-arrow-up' : 'fas fa-arrow-down'"></i>
{{ stat.change }}
</span>
</div>
<h3 class="text-3xl font-bold text-gray-800 mb-1">{{ stat.value }}</h3>
<p class="text-gray-500 text-sm">{{ stat.label }}</p>
</div>
</div>
<!-- 筛选栏 -->
<div class="glass rounded-2xl p-4 mb-6">
<div class="flex items-center justify-between gap-4">
<div class="flex items-center gap-3">
<span class="text-gray-600 font-medium">状态筛选</span>
<a-radio-group v-model:value="statusFilter" button-style="solid">
<a-radio-button value="all">全部</a-radio-button>
<a-radio-button value="running">运行中</a-radio-button>
<a-radio-button value="idle">待机</a-radio-button>
<a-radio-button value="warning">告警</a-radio-button>
</a-radio-group>
</div>
<a-input-search
v-model:value="searchKeyword"
placeholder="搜索设备名称或编号..."
style="width: 280px"
allow-clear
/>
</div>
</div>
<!-- 设备列表 -->
<div class="glass rounded-2xl p-6">
<div class="flex items-center justify-between mb-4">
<h3 class="font-bold text-lg text-gray-800">
<i class="fas fa-list text-purple-500 mr-2"></i>
设备列表
</h3>
<span class="text-sm text-gray-500"> {{ filteredList.length }} 台设备</span>
</div>
<a-table
:dataSource="filteredList"
:pagination="{ pageSize: 10 }"
rowKey="id"
:scroll="{ x: 1200 }"
>
<a-table-column title="设备编号" dataIndex="id" width="100" />
<a-table-column title="设备名称" dataIndex="name" width="180">
<template #default="{ record }">
<div class="flex items-center gap-2">
<div
class="w-8 h-8 rounded-lg flex items-center justify-center text-white text-xs"
:class="statusMap[record.status]?.bg.replace('100', '500') || 'bg-gray-500'"
>
<i class="fas fa-cog"></i>
</div>
<span class="font-medium">{{ record.name }}</span>
</div>
</template>
</a-table-column>
<a-table-column title="型号" dataIndex="model" width="100" />
<a-table-column title="位置" dataIndex="location" width="120" />
<a-table-column title="状态" dataIndex="status" width="100" align="center">
<template #default="{ text }">
<span
:class="statusMap[text]?.color"
class="px-2 py-1 rounded-lg text-sm font-medium"
:class="statusMap[text]?.bg"
>
{{ statusMap[text]?.label }}
</span>
</template>
</a-table-column>
<a-table-column title="利用率" dataIndex="utilization" width="140">
<template #default="{ record }">
<div class="flex items-center gap-2">
<a-progress
:percent="record.utilization"
:status="record.utilization > 80 ? 'success' : 'active'"
:showInfo="false"
size="small"
:stroke-color="record.utilization > 80 ? '#10b981' : '#6366f1'"
style="width: 80px"
/>
<span class="text-sm text-gray-600">{{ record.utilization }}%</span>
</div>
</template>
</a-table-column>
<a-table-column title="温度(°C)" dataIndex="temp" width="100" align="center">
<template #default="{ text, record }">
<span :class="text > 60 ? 'text-red-500 font-medium' : 'text-gray-600'">
{{ text }}
</span>
</template>
</a-table-column>
<a-table-column title="振动(mm/s)" dataIndex="vibration" width="110" align="center">
<template #default="{ text }">
<span :class="text > 2 ? 'text-red-500 font-medium' : 'text-gray-600'">
{{ text }}
</span>
</template>
</a-table-column>
<a-table-column title="下次保养" dataIndex="nextMaintenance" width="120" />
<a-table-column title="操作" width="120" align="center" fixed="right">
<template #default="{ record }">
<div class="flex items-center gap-2 justify-center">
<a-button type="link" size="small" @click="showDetail(record)">
<i class="fas fa-eye"></i>
</a-button>
<a-button type="link" size="small">
<i class="fas fa-edit"></i>
</a-button>
</div>
</template>
</a-table-column>
</a-table>
</div>
<!-- 设备详情抽屉 -->
<a-drawer
v-model:open="detailVisible"
:title="selectedEquipment?.name"
width="500"
placement="right"
>
<template v-if="selectedEquipment">
<!-- 基本信息 -->
<div class="mb-6">
<h4 class="font-bold text-gray-800 mb-3 flex items-center gap-2">
<i class="fas fa-info-circle text-blue-500"></i>
基本信息
</h4>
<a-descriptions :column="2" bordered size="small">
<a-descriptions-item label="设备编号">{{ selectedEquipment.id }}</a-descriptions-item>
<a-descriptions-item label="设备型号">{{ selectedEquipment.model }}</a-descriptions-item>
<a-descriptions-item label="安装位置">{{ selectedEquipment.location }}</a-descriptions-item>
<a-descriptions-item label="当前状态">
<span
:class="statusMap[selectedEquipment.status]?.color"
class="px-2 py-0.5 rounded text-sm font-medium"
:class="statusMap[selectedEquipment.status]?.bg"
>
{{ statusMap[selectedEquipment.status]?.label }}
</span>
</a-descriptions-item>
</a-descriptions>
</div>
<!-- 实时监控 -->
<div class="mb-6">
<h4 class="font-bold text-gray-800 mb-3 flex items-center gap-2">
<i class="fas fa-chart-line text-green-500"></i>
实时监控
</h4>
<div class="grid grid-cols-3 gap-3">
<div class="bg-gray-50 rounded-xl p-4 text-center">
<div class="text-2xl font-bold text-blue-600">{{ selectedEquipment.utilization }}%</div>
<div class="text-xs text-gray-500 mt-1">设备利用率</div>
</div>
<div class="bg-gray-50 rounded-xl p-4 text-center">
<div
class="text-2xl font-bold"
:class="selectedEquipment.temp > 60 ? 'text-red-600' : 'text-orange-600'"
>
{{ selectedEquipment.temp }}°C
</div>
<div class="text-xs text-gray-500 mt-1">当前温度</div>
</div>
<div class="bg-gray-50 rounded-xl p-4 text-center">
<div
class="text-2xl font-bold"
:class="selectedEquipment.vibration > 2 ? 'text-red-600' : 'text-purple-600'"
>
{{ selectedEquipment.vibration }}
</div>
<div class="text-xs text-gray-500 mt-1">振动值</div>
</div>
</div>
</div>
<!-- 效率趋势图 -->
<div class="mb-6">
<h4 class="font-bold text-gray-800 mb-3 flex items-center gap-2">
<i class="fas fa-chart-area text-purple-500"></i>
效率趋势
</h4>
<div class="h-32 flex items-end gap-2">
<div
v-for="(value, idx) in efficiencyTrend"
:key="idx"
class="flex-1 flex flex-col items-center"
>
<div
class="w-full bg-gradient-to-t from-purple-500 to-purple-300 rounded-t-lg transition-all hover:opacity-80"
:style="{ height: value + '%' }"
></div>
<span class="text-xs text-gray-400 mt-1">{{ days[idx] }}</span>
</div>
</div>
</div>
<!-- 维保记录 -->
<div>
<h4 class="font-bold text-gray-800 mb-3 flex items-center gap-2">
<i class="fas fa-wrench text-orange-500"></i>
维保记录
</h4>
<a-timeline>
<a-timeline-item
v-for="record in maintenanceRecords"
:key="record.date"
:color="record.status === '完成' ? 'green' : 'gray'"
>
<div class="flex justify-between">
<span class="font-medium">{{ record.type }}</span>
<span class="text-gray-500 text-sm">{{ record.date }}</span>
</div>
<div class="text-sm text-gray-500">
技师:{{ record.technician }} | 费用:¥{{ record.cost }}
</div>
</a-timeline-item>
</a-timeline>
</div>
<!-- 操作按钮 -->
<div class="mt-6 flex gap-3">
<a-button type="primary" block>发起保养</a-button>
<a-button block>导出报表</a-button>
</div>
</template>
</a-drawer>
<!-- 添加设备弹窗 -->
<a-modal
v-model:open="addFormVisible"
title="添加新设备"
@ok="handleAdd"
ok-text="确认添加"
cancel-text="取消"
>
<a-form :model="addForm" layout="vertical">
<a-form-item label="设备名称" required>
<a-input v-model:value="addForm.name" placeholder="请输入设备名称" />
</a-form-item>
<a-form-item label="设备型号" required>
<a-input v-model:value="addForm.model" placeholder="请输入设备型号" />
</a-form-item>
<a-form-item label="安装位置" required>
<a-input v-model:value="addForm.location" placeholder="请输入安装位置" />
</a-form-item>
<a-form-item label="供应商">
<a-input v-model:value="addForm.supplier" placeholder="请输入供应商" />
</a-form-item>
<a-form-item label="采购日期">
<a-date-picker v-model:value="addForm.purchaseDate" style="width: 100%" />
</a-form-item>
<a-form-item label="保修截止日期">
<a-date-picker v-model:value="addForm.warrantyEnd" style="width: 100%" />
</a-form-item>
</a-form>
</a-modal>
</div>
</template>
<style scoped>
.equipment-page {
padding: 0;
}
.page-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
}
.glass {
background: rgba(255, 255, 255, 0.85);
backdrop-filter: blur(20px);
border: 1px solid rgba(255, 255, 255, 0.3);
}
.card-hover {
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.card-hover:hover {
transform: translateY(-4px);
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
}
.grid {
display: grid;
}
.grid-cols-4 {
grid-template-columns: repeat(4, 1fr);
}
@media (max-width: 1200px) {
.grid-cols-4 {
grid-template-columns: repeat(2, 1fr);
}
}
@media (max-width: 768px) {
.grid-cols-4 {
grid-template-columns: 1fr;
}
}
.mb-6 {
margin-bottom: 24px;
}
.mb-4 {
margin-bottom: 16px;
}
.mb-3 {
margin-bottom: 12px;
}
.mt-1 {
margin-top: 4px;
}
.mr-2 {
margin-right: 8px;
}
.mr-1 {
margin-right: 4px;
}
.mt-6 {
margin-top: 24px;
}
.text-2xl {
font-size: 24px;
}
.text-3xl {
font-size: 30px;
}
.text-lg {
font-size: 18px;
}
.text-sm {
font-size: 14px;
}
.text-xs {
font-size: 12px;
}
.rounded-2xl {
border-radius: 16px;
}
.p-6 {
padding: 24px;
}
.p-4 {
padding: 16px;
}
.gap-6 {
gap: 24px;
}
.gap-4 {
gap: 16px;
}
.gap-3 {
gap: 12px;
}
.gap-2 {
gap: 8px;
}
.flex {
display: flex;
}
.grid {
display: grid;
}
.items-center {
align-items: center;
}
.justify-between {
justify-content: space-between;
}
.text-gray-800 {
color: #1f2937;
}
.text-gray-600 {
color: #4b5563;
}
.text-gray-500 {
color: #6b7280;
}
.text-gray-400 {
color: #9ca3af;
}
.text-blue-600 {
color: #2563eb;
}
.text-green-600 {
color: #16a34a;
}
.text-orange-600 {
color: #ea580c;
}
.text-red-600 {
color: #dc2626;
}
.text-purple-600 {
color: #9333ea;
}
.bg-gray-50 {
background-color: #f9fafb;
}
.bg-green-100 {
background-color: #dcfce7;
}
.bg-orange-100 {
background-color: #ffedd5;
}
.bg-red-100 {
background-color: #fee2e2;
}
.font-bold {
font-weight: 700;
}
.font-medium {
font-weight: 500;
}
.from-blue-500 {
--tw-gradient-from: #3b82f6;
}
.to-purple-500 {
--tw-gradient-to: #a855f7;
}
.from-green-500 {
--tw-gradient-from: #22c55e;
}
.to-teal-500 {
--tw-gradient-to: #14b8a6;
}
.from-orange-500 {
--tw-gradient-from: #f97316;
}
.to-yellow-500 {
--tw-gradient-to: #eab308;
}
.from-red-500 {
--tw-gradient-from: #ef4444;
}
.to-pink-500 {
--tw-gradient-to: #ec4899;
}
.rounded-xl {
border-radius: 10px;
}
.rounded-lg {
border-radius: 8px;
}
</style>

View File

@@ -0,0 +1,98 @@
<script setup lang="ts">
definePageMeta({ layout: 'admin' })
const { activeTab } = useNav()
activeTab.value = 'production-quality'
const qualityRecords = ref([
{ id: 'QC-2026040901', wo: 'WO2026040901', product: '精密轴承组件 A型', batch: 'B-20260409-01', inspector: '质检员A', checkTime: '2026-04-09 10:30', sampleSize: 50, passSize: 49, passRate: 98.0, result: '合格', issues: '1件尺寸超差' },
{ id: 'QC-2026040801', wo: 'WO2026040703', product: '密封圈组件 D型', batch: 'B-20260408-03', inspector: '质检员B', checkTime: '2026-04-08 14:20', sampleSize: 100, passSize: 100, passRate: 100, result: '合格', issues: '-' },
{ id: 'QC-2026040702', wo: 'WO2026040702', product: '弹簧组件 E型', batch: 'B-20260407-02', inspector: '质检员A', checkTime: '2026-04-07 09:00', sampleSize: 80, passSize: 72, passRate: 90.0, result: '不合格', issues: '8件弹性不足' },
])
const resultColor: Record<string, string> = { '合格': 'success', '不合格': 'error' }
const activeTab3 = ref('records')
</script>
<template>
<div class="page-container">
<div class="page-header">
<h2 class="page-title">质量管理</h2>
<a-button type="primary">
<template #icon><PlusOutlined /></template>
新建质检
</a-button>
</div>
<a-row :gutter="[16, 16]" class="mb-6">
<a-col :xs="12" :sm="6">
<div class="stat-card" style="--c:#10b981">
<div class="stat-num">98.5%</div>
<div class="stat-lbl">今日良品率</div>
</div>
</a-col>
<a-col :xs="12" :sm="6">
<div class="stat-card" style="--c:#3b82f6">
<div class="stat-num">156</div>
<div class="stat-lbl">今日检验数</div>
</div>
</a-col>
<a-col :xs="12" :sm="6">
<div class="stat-card" style="--c:#f59e0b">
<div class="stat-num">3</div>
<div class="stat-lbl">待处理异常</div>
</div>
</a-col>
<a-col :xs="12" :sm="6">
<div class="stat-card" style="--c:#6366f1">
<div class="stat-num">12</div>
<div class="stat-lbl">质检标准数</div>
</div>
</a-col>
</a-row>
<a-tabs v-model:activeKey="activeTab3">
<a-tab-pane key="records" tab="检验记录">
<div class="card">
<a-table :dataSource="qualityRecords" :pagination="{ pageSize: 10 }" size="small" rowKey="id">
<a-table-column title="质检编号" dataIndex="id" width="150" />
<a-table-column title="工单号" dataIndex="wo" width="150" />
<a-table-column title="产品" dataIndex="product" />
<a-table-column title="批次" dataIndex="batch" width="160" />
<a-table-column title="检验员" dataIndex="inspector" width="90" align="center" />
<a-table-column title="检验时间" dataIndex="checkTime" width="160" />
<a-table-column title="抽样数" dataIndex="sampleSize" width="80" align="center" />
<a-table-column title="良品数" dataIndex="passSize" width="80" align="center" />
<a-table-column title="良品率" dataIndex="passRate" width="90" align="center">
<template #default="{ text, record }">
<span :class="record.result === '合格' ? 'ok' : 'warn'">{{ text }}%</span>
</template>
</a-table-column>
<a-table-column title="结果" dataIndex="result" width="90" align="center">
<template #default="{ text }">
<a-tag :color="resultColor[text]">{{ text }}</a-tag>
</template>
</a-table-column>
<a-table-column title="问题描述" dataIndex="issues" />
</a-table>
</div>
</a-tab-pane>
<a-tab-pane key="standards" tab="质检标准">
<div class="card"><a-empty description="质检标准管理" /></div>
</a-tab-pane>
</a-tabs>
</div>
</template>
<style scoped>
.page-container { padding: 24px; }
.page-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; }
.page-title { font-size: 20px; font-weight: 600; color: #1f2937; margin: 0; }
.stat-card { background: white; border-radius: 12px; padding: 20px; text-align: center; box-shadow: 0 1px 3px rgba(0,0,0,0.06); border: 1px solid #f0f0f0; position: relative; overflow: hidden; }
.stat-card::before { content: ''; position: absolute; top: 0; left: 0; width: 100%; height: 3px; background: var(--c); }
.stat-num { font-size: 28px; font-weight: 700; color: #1f2937; }
.stat-lbl { font-size: 13px; color: #9ca3af; margin-top: 4px; }
.card { background: white; border-radius: 12px; padding: 20px; box-shadow: 0 1px 3px rgba(0,0,0,0.06); border: 1px solid #f0f0f0; }
.ok { color: #10b981; font-weight: 600; }
.warn { color: #ef4444; font-weight: 600; }
.mb-6 { margin-bottom: 20px; }
</style>

View File

@@ -0,0 +1,95 @@
<script setup lang="ts">
definePageMeta({ layout: 'admin' })
const { activeTab } = useNav()
activeTab.value = 'production-safety'
const stats = ref([
{ label: '安全生产天数', value: 156, unit: '天', icon: '🛡️', color: '#10b981' },
{ label: '本月巡检次数', value: 28, unit: '次', icon: '🔍', color: '#3b82f6' },
{ label: '安全隐患', value: 3, unit: '条', icon: '⚠️', color: '#f59e0b' },
{ label: '应急演练', value: 2, unit: '次', icon: '🚨', color: '#6366f1' },
])
const safetyLogs = ref([
{ id: 'SL-001', type: '巡检', area: '生产车间A区', inspector: '安全员A', result: '正常', time: '2026-04-09 08:30', remark: '-' },
{ id: 'SL-002', type: '巡检', area: '电工房', inspector: '安全员B', result: '隐患', time: '2026-04-09 09:15', remark: '发现1处接线松动' },
{ id: 'SL-003', type: '设备检查', area: 'CNC-03区域', inspector: '安全员A', result: '告警', time: '2026-04-09 10:00', remark: '设备温度异常,已停机' },
{ id: 'SL-004', type: '巡检', area: '仓储区', inspector: '安全员C', result: '正常', time: '2026-04-08 14:00', remark: '-' },
])
const resultColor: Record<string, string> = { '正常': 'success', '隐患': 'warning', '告警': 'error' }
</script>
<template>
<div class="page-container">
<div class="page-header">
<h2 class="page-title">安全生产</h2>
<a-button type="primary">
<template #icon><PlusOutlined /></template>
新建巡检
</a-button>
</div>
<a-row :gutter="[16, 16]" class="mb-6">
<a-col :xs="12" :sm="6" v-for="stat in stats" :key="stat.label">
<div class="stat-card" :style="{ '--c': stat.color }">
<div class="stat-icon">{{ stat.icon }}</div>
<div class="stat-body">
<div class="stat-value">{{ stat.value }}<span class="stat-unit">{{ stat.unit }}</span></div>
<div class="stat-label">{{ stat.label }}</div>
</div>
</div>
</a-col>
</a-row>
<a-row :gutter="[20, 20]">
<a-col :xs="24" :xl="16">
<div class="card">
<div class="card-title">安全巡检记录</div>
<a-table :dataSource="safetyLogs" :pagination="{ pageSize: 10 }" size="small" rowKey="id">
<a-table-column title="记录编号" dataIndex="id" width="100" />
<a-table-column title="类型" dataIndex="type" width="100" align="center" />
<a-table-column title="区域" dataIndex="area" />
<a-table-column title="检查人" dataIndex="inspector" width="100" align="center" />
<a-table-column title="结果" dataIndex="result" width="90" align="center">
<template #default="{ text }">
<a-tag :color="resultColor[text]">{{ text }}</a-tag>
</template>
</a-table-column>
<a-table-column title="备注" dataIndex="remark" />
<a-table-column title="时间" dataIndex="time" width="160" />
</a-table>
</div>
</a-col>
<a-col :xs="24" :xl="8">
<div class="card">
<div class="card-title">安全知识库</div>
<div class="doc-list">
<div class="doc-item">📄 安全生产管理制度 v3.2</div>
<div class="doc-item">📄 应急救援预案</div>
<div class="doc-item">📄 特种设备操作规程</div>
<div class="doc-item">📄 危险源辨识清单</div>
</div>
</div>
</a-col>
</a-row>
</div>
</template>
<style scoped>
.page-container { padding: 24px; }
.page-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; }
.page-title { font-size: 20px; font-weight: 600; color: #1f2937; margin: 0; }
.stat-card { background: white; border-radius: 12px; padding: 16px; display: flex; align-items: center; gap: 12px; box-shadow: 0 1px 3px rgba(0,0,0,0.06); border: 1px solid #f0f0f0; }
.stat-card::before { content: ''; position: absolute; top: 0; left: 0; width: 4px; height: 100%; background: var(--c); }
.stat-icon { font-size: 28px; }
.stat-value { font-size: 22px; font-weight: 700; color: #1f2937; }
.stat-unit { font-size: 12px; color: #9ca3af; margin-left: 2px; }
.stat-label { font-size: 12px; color: #9ca3af; }
.card { background: white; border-radius: 12px; padding: 20px; box-shadow: 0 1px 3px rgba(0,0,0,0.06); border: 1px solid #f0f0f0; }
.card-title { font-size: 16px; font-weight: 600; color: #1f2937; margin-bottom: 16px; }
.doc-list { display: flex; flex-direction: column; gap: 10px; }
.doc-item { padding: 10px; background: #fafafa; border-radius: 6px; font-size: 13px; color: #374151; cursor: pointer; }
.doc-item:hover { background: #f0f0f0; }
.mb-6 { margin-bottom: 20px; }
</style>

View File

@@ -0,0 +1,155 @@
<script setup lang="ts">
definePageMeta({ layout: 'admin' })
const { activeTab } = useNav()
activeTab.value = 'production-schedule'
const selectedDate = ref('2026-04-09')
const scheduleData = ref([
{ time: '08:00', tasks: [
{ wo: 'WO2026040901', product: '精密轴承组件 A型', qty: 500, line: '产线1', status: 'running' },
]},
{ time: '10:00', tasks: [
{ wo: 'WO2026040802', product: '液压缸体 B型', qty: 200, line: '产线2', status: 'pending' },
]},
{ time: '14:00', tasks: [
{ wo: 'WO2026040703', product: '密封圈组件 D型', qty: 3000, line: '产线3', status: 'pending' },
]},
])
const productionLines = ref([
{ id: 'L1', name: '产线1', status: 'running', utilization: 92, output: 245, target: 260 },
{ id: 'L2', name: '产线2', status: 'idle', utilization: 0, output: 0, target: 200 },
{ id: 'L3', name: '产线3', status: 'running', utilization: 78, output: 180, target: 220 },
{ id: 'L4', name: '产线4', status: 'maintenance', utilization: 0, output: 0, target: 180 },
])
const statusColor: Record<string, string> = {
running: '#10b981',
pending: '#f59e0b',
idle: '#9ca3af',
maintenance: '#ef4444',
}
const lineStatusMap: Record<string, string> = {
running: { status: 'running', text: '运行中' },
idle: { status: 'default', text: '空闲' },
maintenance: { status: 'exception', text: '维护中' },
}
</script>
<template>
<div class="page-container">
<div class="page-header">
<h2 class="page-title">计划排程</h2>
<a-space>
<a-date-picker v-model:value="selectedDate" />
<a-button type="primary">
<template #icon><PlusOutlined /></template>
新建排程
</a-button>
</a-space>
</div>
<a-row :gutter="[20, 20]">
<!-- 排程甘特图 -->
<a-col :xs="24" :xl="16">
<div class="card">
<div class="card-title">生产排程</div>
<div class="gantt">
<div class="gantt-header">
<div class="gantt-time">时间段</div>
<div class="gantt-tasks">排程任务</div>
</div>
<div v-for="slot in scheduleData" :key="slot.time" class="gantt-row">
<div class="gantt-time">{{ slot.time }}</div>
<div class="gantt-tasks">
<div v-for="task in slot.tasks" :key="task.wo" class="task-card" :class="task.status">
<div class="task-wo">{{ task.wo }}</div>
<div class="task-info">{{ task.product }} · {{ task.qty }}</div>
<div class="task-line">{{ task.line }}</div>
</div>
<div v-if="slot.tasks.length === 0" class="task-empty">暂无排程</div>
</div>
</div>
</div>
</div>
</a-col>
<!-- 产线状态 -->
<a-col :xs="24" :xl="8">
<div class="card">
<div class="card-title">产线状态</div>
<div class="line-list">
<div v-for="line in productionLines" :key="line.id" class="line-item">
<div class="line-header">
<span class="line-name">{{ line.name }}</span>
<a-badge :status="lineStatusMap[line.status].status as any" :text="lineStatusMap[line.status].text" />
</div>
<div class="line-stats">
<div class="line-stat">
<span class="line-num">{{ line.utilization }}%</span>
<span class="line-lbl">利用率</span>
</div>
<div class="line-stat">
<span class="line-num">{{ line.output }}</span>
<span class="line-lbl">实际产量</span>
</div>
<div class="line-stat">
<span class="line-num">{{ line.target }}</span>
<span class="line-lbl">目标产量</span>
</div>
</div>
<a-progress
:percent="line.utilization"
:showInfo="false"
size="small"
:status="line.status === 'running' ? 'active' : 'exception'"
:strokeColor="statusColor[line.status]"
/>
</div>
</div>
</div>
</a-col>
</a-row>
</div>
</template>
<style scoped>
.page-container { padding: 24px; }
.page-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; }
.page-title { font-size: 20px; font-weight: 600; color: #1f2937; margin: 0; }
.card { background: white; border-radius: 12px; padding: 20px; box-shadow: 0 1px 3px rgba(0,0,0,0.06); border: 1px solid #f0f0f0; margin-bottom: 20px; }
.card-title { font-size: 16px; font-weight: 600; color: #1f2937; margin-bottom: 16px; }
.gantt { border: 1px solid #f0f0f0; border-radius: 8px; overflow: hidden; }
.gantt-header { display: flex; background: #fafafa; border-bottom: 1px solid #f0f0f0; font-weight: 600; font-size: 13px; }
.gantt-time { width: 80px; padding: 10px 12px; border-right: 1px solid #f0f0f0; }
.gantt-tasks { flex: 1; padding: 10px 12px; }
.gantt-row { display: flex; border-bottom: 1px solid #f0f0f0; }
.gantt-row:last-child { border-bottom: none; }
.gantt-row .gantt-time { padding: 16px 12px; font-size: 13px; color: #6b7280; }
.gantt-row .gantt-tasks { display: flex; flex-wrap: wrap; gap: 8px; padding: 12px; min-height: 60px; align-items: center; }
.task-card {
padding: 8px 12px;
border-radius: 6px;
font-size: 12px;
}
.task-card.running { background: #ecfdf5; border-left: 3px solid #10b981; }
.task-card.pending { background: #fffbeb; border-left: 3px solid #f59e0b; }
.task-wo { font-weight: 600; color: #374151; }
.task-info { color: #6b7280; margin: 2px 0; }
.task-line { color: #9ca3af; font-size: 11px; }
.task-empty { color: #d1d5db; font-size: 13px; }
.line-list { display: flex; flex-direction: column; gap: 16px; }
.line-item { padding: 14px; background: #fafafa; border-radius: 8px; }
.line-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px; }
.line-name { font-weight: 600; color: #374151; font-size: 14px; }
.line-stats { display: flex; justify-content: space-between; margin-bottom: 10px; }
.line-stat { text-align: center; }
.line-num { display: block; font-size: 16px; font-weight: 700; color: #1f2937; }
.line-lbl { font-size: 11px; color: #9ca3af; }
</style>

View File

@@ -0,0 +1,465 @@
<script setup lang="ts">
definePageMeta({ layout: 'admin' })
// 采购统计
const purchaseStats = ref([
{ label: '采购单总数', value: 156, icon: 'fa-file-alt', gradient: 'from-blue-500 to-purple-500', change: '+23%', up: true },
{ label: '待审批', value: 12, icon: 'fa-clock', gradient: 'from-orange-500 to-yellow-500', change: '+3', up: false },
{ label: '已完成', value: 138, icon: 'fa-check-circle', gradient: 'from-green-500 to-teal-500', change: '+18%', up: true },
{ label: '采购总额', value: '89.5', unit: '万', icon: 'fa-wallet', gradient: 'from-pink-500 to-rose-500', change: '+15%', up: true },
])
// 采购单列表
const purchaseOrders = ref([
{ id: 'PO-2026040901', supplier: '深圳市精密机械有限公司', material: '轴承组件 A型', quantity: 500, unit: '套', amount: 25000, status: 'pending', applicant: '张经理', date: '2026-04-09' },
{ id: 'PO-2026040802', supplier: '上海五金工具厂', material: '数控刀具套装', quantity: 20, unit: '套', amount: 36000, status: 'approved', applicant: '李主管', date: '2026-04-08' },
{ id: 'PO-2026040801', supplier: '东莞市金属材料公司', material: '铝合金板材', quantity: 200, unit: '张', amount: 48000, status: 'processing', applicant: '王经理', date: '2026-04-08' },
{ id: 'PO-2026040703', supplier: '苏州液压设备厂', material: '液压缸体 B型', quantity: 50, unit: '件', amount: 75000, status: 'completed', applicant: '赵主管', date: '2026-04-07' },
{ id: 'PO-2026040702', supplier: '广州润滑油脂公司', material: '工业润滑油', quantity: 100, unit: '桶', amount: 15000, status: 'completed', applicant: '张经理', date: '2026-04-07' },
{ id: 'PO-2026040601', supplier: '深圳市精密机械有限公司', material: '密封圈组件', quantity: 1000, unit: '个', amount: 8000, status: 'completed', applicant: '李主管', date: '2026-04-06' },
])
const statusMap: Record<string, { label: string; color: string; bg: string }> = {
pending: { label: '待审批', color: 'text-orange-600', bg: 'bg-orange-100' },
approved: { label: '已审批', color: 'text-blue-600', bg: 'bg-blue-100' },
processing: { label: '执行中', color: 'text-purple-600', bg: 'bg-purple-100' },
completed: { label: '已完成', color: 'text-green-600', bg: 'bg-green-100' },
rejected: { label: '已驳回', color: 'text-red-600', bg: 'bg-red-100' },
}
const statusFilter = ref('all')
const searchKeyword = ref('')
const filteredList = computed(() => {
return purchaseOrders.value.filter((item) => {
const matchStatus = statusFilter.value === 'all' || item.status === statusFilter.value
const matchSearch = !searchKeyword.value ||
item.id.toLowerCase().includes(searchKeyword.value.toLowerCase()) ||
item.supplier.toLowerCase().includes(searchKeyword.value.toLowerCase()) ||
item.material.toLowerCase().includes(searchKeyword.value.toLowerCase())
return matchStatus && matchSearch
})
})
// 供应商列表
const suppliers = ref([
{ name: '深圳市精密机械有限公司', contact: '陈经理', phone: '138****1234', items: 45, total: '120万', rating: 4.8 },
{ name: '上海五金工具厂', contact: '李总', phone: '139****5678', items: 32, total: '85万', rating: 4.6 },
{ name: '东莞市金属材料公司', contact: '王经理', phone: '137****9012', items: 28, total: '200万', rating: 4.9 },
{ name: '苏州液压设备厂', contact: '张工', phone: '136****3456', items: 15, total: '95万', rating: 4.7 },
])
const addFormVisible = ref(false)
const addForm = reactive({
supplier: '',
material: '',
quantity: '',
unit: '',
estimatedAmount: '',
deliveryDate: '',
remark: '',
})
function handleAdd() {
addFormVisible.value = false
message.success('采购单创建成功')
}
</script>
<template>
<div class="purchase-page">
<!-- 页面标题 -->
<div class="page-header mb-6">
<div>
<h2 class="text-2xl font-bold text-gray-800">采购管理</h2>
<p class="text-gray-500 mt-1">管理采购订单跟踪供应商履约情况</p>
</div>
<a-button type="primary" @click="addFormVisible = true">
<template #icon><i class="fas fa-plus mr-1"></i></template>
新建采购单
</a-button>
</div>
<!-- 统计卡片 -->
<div class="grid grid-cols-4 gap-6 mb-6">
<div
v-for="stat in purchaseStats"
:key="stat.label"
class="glass rounded-2xl p-6 card-hover cursor-pointer"
>
<div class="flex items-center justify-between mb-4">
<div
class="w-12 h-12 rounded-xl flex items-center justify-center text-white"
:class="`bg-gradient-to-br ${stat.gradient}`"
>
<i :class="`fas ${stat.icon}`"></i>
</div>
<span
class="text-sm font-medium"
:class="stat.up ? 'text-green-500' : 'text-red-500'"
>
<i :class="stat.up ? 'fas fa-arrow-up' : 'fas fa-arrow-down'"></i>
{{ stat.change }}
</span>
</div>
<h3 class="text-3xl font-bold text-gray-800 mb-1">
{{ stat.value }}<span v-if="stat.unit" class="text-base text-gray-500 ml-1">{{ stat.unit }}</span>
</h3>
<p class="text-gray-500 text-sm">{{ stat.label }}</p>
</div>
</div>
<!-- 筛选栏 -->
<div class="glass rounded-2xl p-4 mb-6">
<div class="flex items-center justify-between gap-4">
<div class="flex items-center gap-3">
<span class="text-gray-600 font-medium">状态筛选</span>
<a-radio-group v-model:value="statusFilter" button-style="solid">
<a-radio-button value="all">全部</a-radio-button>
<a-radio-button value="pending">待审批</a-radio-button>
<a-radio-button value="approved">已审批</a-radio-button>
<a-radio-button value="processing">执行中</a-radio-button>
<a-radio-button value="completed">已完成</a-radio-button>
</a-radio-group>
</div>
<a-input-search
v-model:value="searchKeyword"
placeholder="搜索采购单号、供应商、物料..."
style="width: 300px"
allow-clear
/>
</div>
</div>
<!-- 采购单列表 -->
<div class="glass rounded-2xl p-6">
<div class="flex items-center justify-between mb-4">
<h3 class="font-bold text-lg text-gray-800">
<i class="fas fa-file-alt text-blue-500 mr-2"></i>
采购单列表
</h3>
<span class="text-sm text-gray-500"> {{ filteredList.length }} 条记录</span>
</div>
<a-table
:dataSource="filteredList"
:pagination="{ pageSize: 10 }"
rowKey="id"
:scroll="{ x: 1200 }"
>
<a-table-column title="采购单号" dataIndex="id" width="140" />
<a-table-column title="供应商" dataIndex="supplier" width="200">
<template #default="{ text }">
<span class="font-medium">{{ text }}</span>
</template>
</a-table-column>
<a-table-column title="采购物料" dataIndex="material" width="150" />
<a-table-column title="数量" dataIndex="quantity" width="100" align="center">
<template #default="{ record }">
{{ record.quantity }} {{ record.unit }}
</template>
</a-table-column>
<a-table-column title="金额(元)" dataIndex="amount" width="120" align="right">
<template #default="{ text }">
<span class="font-medium text-orange-600">¥{{ text.toLocaleString() }}</span>
</template>
</a-table-column>
<a-table-column title="状态" dataIndex="status" width="100" align="center">
<template #default="{ text }">
<span
:class="statusMap[text]?.color"
class="px-2 py-1 rounded-lg text-sm font-medium"
:class="statusMap[text]?.bg"
>
{{ statusMap[text]?.label }}
</span>
</template>
</a-table-column>
<a-table-column title="申请人" dataIndex="applicant" width="100" align="center" />
<a-table-column title="申请日期" dataIndex="date" width="120" />
<a-table-column title="操作" width="160" align="center" fixed="right">
<template #default>
<div class="flex items-center gap-2 justify-center">
<a-button type="link" size="small" title="查看详情">
<i class="fas fa-eye"></i>
</a-button>
<a-button type="link" size="small" title="审批" v-if="statusFilter === 'pending'">
<i class="fas fa-check"></i>
</a-button>
<a-button type="link" size="small" title="编辑">
<i class="fas fa-edit"></i>
</a-button>
</div>
</template>
</a-table-column>
</a-table>
</div>
<!-- 供应商管理 -->
<div class="glass rounded-2xl p-6 mt-6">
<div class="flex items-center justify-between mb-4">
<h3 class="font-bold text-lg text-gray-800">
<i class="fas fa-building text-green-500 mr-2"></i>
供应商列表
</h3>
<a-button type="link">
<i class="fas fa-plus mr-1"></i>添加供应商
</a-button>
</div>
<a-row :gutter="[16, 16]">
<a-col :xs="24" :sm="12" :lg="6" v-for="supplier in suppliers" :key="supplier.name">
<div class="supplier-card glass rounded-xl p-4 card-hover">
<div class="flex items-start justify-between mb-3">
<div class="w-10 h-10 rounded-lg bg-gradient-to-br from-blue-500 to-purple-500 flex items-center justify-center text-white">
<i class="fas fa-building"></i>
</div>
<a-rate :default-value="supplier.rating" disabled size="small" />
</div>
<h4 class="font-bold text-gray-800 mb-1">{{ supplier.name }}</h4>
<p class="text-sm text-gray-500 mb-2">{{ supplier.contact }} | {{ supplier.phone }}</p>
<div class="flex justify-between text-sm">
<span class="text-gray-500">合作物料<span class="text-blue-600 font-medium">{{ supplier.items }}</span> </span>
<span class="text-gray-500">累计<span class="text-green-600 font-medium">{{ supplier.total }}</span></span>
</div>
</div>
</a-col>
</a-row>
</div>
<!-- 新建采购单弹窗 -->
<a-modal
v-model:open="addFormVisible"
title="新建采购单"
@ok="handleAdd"
ok-text="确认创建"
cancel-text="取消"
width="600px"
>
<a-form :model="addForm" layout="vertical">
<a-form-item label="供应商" required>
<a-select v-model:value="addForm.supplier" placeholder="请选择供应商">
<a-select-option v-for="s in suppliers" :key="s.name" :value="s.name">{{ s.name }}</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="采购物料" required>
<a-input v-model:value="addForm.material" placeholder="请输入物料名称" />
</a-form-item>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="数量" required>
<a-input-number v-model:value="addForm.quantity" style="width: 100%" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="单位">
<a-input v-model:value="addForm.unit" placeholder="如:套、件、个" />
</a-form-item>
</a-col>
</a-row>
<a-form-item label="预估金额(元)">
<a-input-number v-model:value="addForm.estimatedAmount" style="width: 100%" :min="0" />
</a-form-item>
<a-form-item label="期望交货日期">
<a-date-picker v-model:value="addForm.deliveryDate" style="width: 100%" />
</a-form-item>
<a-form-item label="备注">
<a-textarea v-model:value="addForm.remark" :rows="3" placeholder="请输入备注信息" />
</a-form-item>
</a-form>
</a-modal>
</div>
</template>
<style scoped>
.purchase-page {
padding: 0;
}
.page-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
}
.glass {
background: rgba(255, 255, 255, 0.85);
backdrop-filter: blur(20px);
border: 1px solid rgba(255, 255, 255, 0.3);
}
.card-hover {
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.card-hover:hover {
transform: translateY(-4px);
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
}
.grid {
display: grid;
}
.grid-cols-4 {
grid-template-columns: repeat(4, 1fr);
}
@media (max-width: 1200px) {
.grid-cols-4 {
grid-template-columns: repeat(2, 1fr);
}
}
.mb-6 {
margin-bottom: 24px;
}
.mb-4 {
margin-bottom: 16px;
}
.mb-3 {
margin-bottom: 12px;
}
.mb-2 {
margin-bottom: 8px;
}
.mb-1 {
margin-bottom: 4px;
}
.mt-1 {
margin-top: 4px;
}
.mt-6 {
margin-top: 24px;
}
.ml-1 {
margin-left: 4px;
}
.mr-2 {
margin-right: 8px;
}
.mr-1 {
margin-right: 4px;
}
.text-2xl {
font-size: 24px;
}
.text-3xl {
font-size: 30px;
}
.text-lg {
font-size: 18px;
}
.text-sm {
font-size: 14px;
}
.text-xs {
font-size: 12px;
}
.text-base {
font-size: 16px;
}
.rounded-2xl {
border-radius: 16px;
}
.rounded-xl {
border-radius: 12px;
}
.p-6 {
padding: 24px;
}
.p-4 {
padding: 16px;
}
.gap-6 {
gap: 24px;
}
.gap-4 {
gap: 16px;
}
.gap-3 {
gap: 12px;
}
.gap-2 {
gap: 8px;
}
.flex {
display: flex;
}
.items-center {
align-items: center;
}
.items-start {
align-items: flex-start;
}
.justify-between {
justify-content: space-between;
}
.justify-end {
justify-content: flex-end;
}
.font-bold {
font-weight: 700;
}
.font-medium {
font-weight: 500;
}
.text-gray-800 {
color: #1f2937;
}
.text-gray-600 {
color: #4b5563;
}
.text-gray-500 {
color: #6b7280;
}
.text-orange-600 {
color: #ea580c;
}
.text-blue-600 {
color: #2563eb;
}
.text-green-600 {
color: #16a34a;
}
.bg-gradient-to-br {
background-image: linear-gradient(to bottom right, var(--tw-gradient-stops));
}
</style>

View File

@@ -0,0 +1,462 @@
<script setup lang="ts">
definePageMeta({ layout: 'admin' })
// 仓储统计
const warehouseStats = ref([
{ label: '物料种类', value: 5230, icon: 'fa-boxes', gradient: 'from-blue-500 to-cyan-500', change: '+126', up: true },
{ label: '库存总量', value: '8.5', unit: '万', icon: 'fa-warehouse', gradient: 'from-green-500 to-teal-500', change: '+1.2万', up: true },
{ label: '待入库', value: 45, icon: 'fa-arrow-down', gradient: 'from-orange-500 to-yellow-500', change: '+12', up: false },
{ label: '待出库', value: 28, icon: 'fa-truck', gradient: 'from-purple-500 to-pink-500', change: '-8', up: true },
])
// 库存列表
const inventoryList = ref([
{ code: 'MAT-001', name: '轴承组件 A型', category: '标准件', warehouse: 'A区', location: 'A-01-03', stock: 1500, safeStock: 500, unit: '套', status: 'normal' },
{ code: 'MAT-002', name: '铝合金板材', category: '原材料', warehouse: 'B区', location: 'B-02-05', stock: 320, safeStock: 200, unit: '张', status: 'normal' },
{ code: 'MAT-003', name: '液压缸体 B型', category: '半成品', warehouse: 'C区', location: 'C-01-02', stock: 80, safeStock: 100, unit: '件', status: 'low' },
{ code: 'MAT-004', name: '数控刀具套装', category: '工装', warehouse: 'D区', location: 'D-03-01', stock: 45, safeStock: 20, unit: '套', status: 'normal' },
{ code: 'MAT-005', name: '密封圈组件', category: '标准件', warehouse: 'A区', location: 'A-02-01', stock: 2800, safeStock: 1000, unit: '个', status: 'normal' },
{ code: 'MAT-006', name: '传动齿轮组 C型', category: '零部件', warehouse: 'C区', location: 'C-02-03', stock: 150, safeStock: 200, unit: '组', status: 'low' },
])
const statusMap: Record<string, { label: string; color: string; bg: string }> = {
normal: { label: '正常', color: 'text-green-600', bg: 'bg-green-100' },
low: { label: '偏低', color: 'text-orange-600', bg: 'bg-orange-100' },
out: { label: '缺货', color: 'text-red-600', bg: 'bg-red-100' },
over: { label: '超储', color: 'text-blue-600', bg: 'bg-blue-100' },
}
// 入库记录
const inboundRecords = ref([
{ id: 'IN-2026040901', material: '轴承组件 A型', quantity: 500, unit: '套', type: '采购入库', operator: '仓管员-张三', date: '2026-04-09 09:30', status: 'completed' },
{ id: 'IN-2026040802', material: '铝合金板材', quantity: 200, unit: '张', type: '采购入库', operator: '仓管员-李四', date: '2026-04-08 14:20', status: 'completed' },
{ id: 'IN-2026040801', material: '数控刀具套装', quantity: 20, unit: '套', type: '采购入库', operator: '仓管员-张三', date: '2026-04-08 10:15', status: 'completed' },
{ id: 'IN-2026040703', material: '工业润滑油', quantity: 50, unit: '桶', type: '采购入库', operator: '仓管员-王五', date: '2026-04-07 16:45', status: 'completed' },
])
// 出库记录
const outboundRecords = ref([
{ id: 'OUT-2026040901', material: '轴承组件 A型', quantity: 100, unit: '套', type: '生产领料', recipient: '1号车间', operator: '仓管员-张三', date: '2026-04-09 08:30', status: 'completed' },
{ id: 'OUT-2026040902', material: '密封圈组件', quantity: 200, unit: '个', type: '生产领料', recipient: '2号车间', operator: '仓管员-李四', date: '2026-04-09 10:20', status: 'completed' },
{ id: 'OUT-2026040801', material: '铝合金板材', quantity: 50, unit: '张', type: '生产领料', recipient: '3号车间', operator: '仓管员-王五', date: '2026-04-08 15:30', status: 'completed' },
])
const activeTab = ref('inventory')
const searchKeyword = ref('')
const filteredInventory = computed(() => {
return inventoryList.value.filter((item) => {
return !searchKeyword.value ||
item.name.toLowerCase().includes(searchKeyword.value.toLowerCase()) ||
item.code.toLowerCase().includes(searchKeyword.value.toLowerCase()) ||
item.category.toLowerCase().includes(searchKeyword.value.toLowerCase())
})
})
// 入库表单
const inboundVisible = ref(false)
const inboundForm = reactive({
material: '',
quantity: '',
unit: '',
type: 'purchase',
supplier: '',
remark: '',
})
function handleInbound() {
inboundVisible.value = false
message.success('入库登记成功')
}
</script>
<template>
<div class="warehouse-page">
<!-- 页面标题 -->
<div class="page-header mb-6">
<div>
<h2 class="text-2xl font-bold text-gray-800">仓储物流</h2>
<p class="text-gray-500 mt-1">管理库存物料跟踪出入库记录</p>
</div>
<div class="flex gap-3">
<a-button @click="inboundVisible = true">
<template #icon><i class="fas fa-arrow-down mr-1"></i></template>
入库登记
</a-button>
<a-button type="primary">
<template #icon><i class="fas fa-arrow-up mr-1"></i></template>
出库登记
</a-button>
</div>
</div>
<!-- 统计卡片 -->
<div class="grid grid-cols-4 gap-6 mb-6">
<div
v-for="stat in warehouseStats"
:key="stat.label"
class="glass rounded-2xl p-6 card-hover cursor-pointer"
>
<div class="flex items-center justify-between mb-4">
<div
class="w-12 h-12 rounded-xl flex items-center justify-center text-white"
:class="`bg-gradient-to-br ${stat.gradient}`"
>
<i :class="`fas ${stat.icon}`"></i>
</div>
<span
class="text-sm font-medium"
:class="stat.up ? 'text-green-500' : 'text-red-500'"
>
<i :class="stat.up ? 'fas fa-arrow-up' : 'fas fa-arrow-down'"></i>
{{ stat.change }}
</span>
</div>
<h3 class="text-3xl font-bold text-gray-800 mb-1">
{{ stat.value }}<span v-if="stat.unit" class="text-base text-gray-500 ml-1">{{ stat.unit }}</span>
</h3>
<p class="text-gray-500 text-sm">{{ stat.label }}</p>
</div>
</div>
<!-- Tab切换 -->
<div class="glass rounded-2xl p-4 mb-6">
<a-radio-group v-model:value="activeTab" button-style="solid">
<a-radio-button value="inventory">
<i class="fas fa-boxes mr-1"></i>库存查询
</a-radio-button>
<a-radio-button value="inbound">
<i class="fas fa-arrow-down mr-1"></i>入库记录
</a-radio-button>
<a-radio-button value="outbound">
<i class="fas fa-arrow-up mr-1"></i>出库记录
</a-radio-button>
</a-radio-group>
<a-input-search
v-model:value="searchKeyword"
placeholder="搜索物料名称、编号..."
style="width: 280px; float: right"
allow-clear
/>
</div>
<!-- 库存列表 -->
<div v-show="activeTab === 'inventory'" class="glass rounded-2xl p-6">
<div class="flex items-center justify-between mb-4">
<h3 class="font-bold text-lg text-gray-800">
<i class="fas fa-boxes text-blue-500 mr-2"></i>
库存列表
</h3>
<span class="text-sm text-gray-500"> {{ filteredInventory.length }} 种物料</span>
</div>
<a-table
:dataSource="filteredInventory"
:pagination="{ pageSize: 10 }"
rowKey="code"
:scroll="{ x: 1100 }"
>
<a-table-column title="物料编码" dataIndex="code" width="110" />
<a-table-column title="物料名称" dataIndex="name" width="160">
<template #default="{ text }">
<span class="font-medium">{{ text }}</span>
</template>
</a-table-column>
<a-table-column title="分类" dataIndex="category" width="100" />
<a-table-column title="仓库" dataIndex="warehouse" width="80" align="center" />
<a-table-column title="库位" dataIndex="location" width="100" align="center" />
<a-table-column title="库存量" dataIndex="stock" width="120" align="right">
<template #default="{ record }">
<span class="font-medium">{{ record.stock.toLocaleString() }} {{ record.unit }}</span>
</template>
</a-table-column>
<a-table-column title="安全库存" dataIndex="safeStock" width="100" align="right">
<template #default="{ record }">
<span class="text-gray-500">{{ record.safeStock }} {{ record.unit }}</span>
</template>
</a-table-column>
<a-table-column title="状态" dataIndex="status" width="100" align="center">
<template #default="{ text }">
<span
:class="statusMap[text]?.color"
class="px-2 py-1 rounded-lg text-sm font-medium"
:class="statusMap[text]?.bg"
>
{{ statusMap[text]?.label }}
</span>
</template>
</a-table-column>
<a-table-column title="操作" width="120" align="center" fixed="right">
<template #default>
<div class="flex items-center gap-2 justify-center">
<a-button type="link" size="small" title="详情">
<i class="fas fa-eye"></i>
</a-button>
<a-button type="link" size="small" title="调整">
<i class="fas fa-edit"></i>
</a-button>
</div>
</template>
</a-table-column>
</a-table>
</div>
<!-- 入库记录 -->
<div v-show="activeTab === 'inbound'" class="glass rounded-2xl p-6">
<div class="flex items-center justify-between mb-4">
<h3 class="font-bold text-lg text-gray-800">
<i class="fas fa-arrow-down text-green-500 mr-2"></i>
入库记录
</h3>
</div>
<a-table
:dataSource="inboundRecords"
:pagination="{ pageSize: 10 }"
rowKey="id"
>
<a-table-column title="入库单号" dataIndex="id" width="140" />
<a-table-column title="物料名称" dataIndex="material" width="160">
<template #default="{ text }">
<span class="font-medium">{{ text }}</span>
</template>
</a-table-column>
<a-table-column title="数量" width="120" align="center">
<template #default="{ record }">
<span class="text-green-600 font-medium">+{{ record.quantity }} {{ record.unit }}</span>
</template>
</a-table-column>
<a-table-column title="入库类型" dataIndex="type" width="120">
<template #default="{ text }">
<a-tag :color="text === '采购入库' ? 'blue' : 'purple'">{{ text }}</a-tag>
</template>
</a-table-column>
<a-table-column title="操作员" dataIndex="operator" width="120" />
<a-table-column title="入库时间" dataIndex="date" width="160" />
<a-table-column title="状态" width="100" align="center">
<template #default>
<a-tag color="success">已完成</a-tag>
</template>
</a-table-column>
</a-table>
</div>
<!-- 出库记录 -->
<div v-show="activeTab === 'outbound'" class="glass rounded-2xl p-6">
<div class="flex items-center justify-between mb-4">
<h3 class="font-bold text-lg text-gray-800">
<i class="fas fa-arrow-up text-orange-500 mr-2"></i>
出库记录
</h3>
</div>
<a-table
:dataSource="outboundRecords"
:pagination="{ pageSize: 10 }"
rowKey="id"
>
<a-table-column title="出库单号" dataIndex="id" width="140" />
<a-table-column title="物料名称" dataIndex="material" width="160">
<template #default="{ text }">
<span class="font-medium">{{ text }}</span>
</template>
</a-table-column>
<a-table-column title="数量" width="120" align="center">
<template #default="{ record }">
<span class="text-orange-600 font-medium">-{{ record.quantity }} {{ record.unit }}</span>
</template>
</a-table-column>
<a-table-column title="出库类型" dataIndex="type" width="120">
<template #default="{ text }">
<a-tag :color="text === '生产领料' ? 'orange' : 'cyan'">{{ text }}</a-tag>
</template>
</a-table-column>
<a-table-column title="领用部门" dataIndex="recipient" width="120" />
<a-table-column title="操作员" dataIndex="operator" width="120" />
<a-table-column title="出库时间" dataIndex="date" width="160" />
<a-table-column title="状态" width="100" align="center">
<template #default>
<a-tag color="success">已完成</a-tag>
</template>
</a-table-column>
</a-table>
</div>
<!-- 入库登记弹窗 -->
<a-modal
v-model:open="inboundVisible"
title="入库登记"
@ok="handleInbound"
ok-text="确认入库"
cancel-text="取消"
>
<a-form :model="inboundForm" layout="vertical">
<a-form-item label="物料" required>
<a-select v-model:value="inboundForm.material" placeholder="请选择物料">
<a-select-option v-for="item in inventoryList" :key="item.code" :value="item.name">
{{ item.name }} ({{ item.code }})
</a-select-option>
</a-select>
</a-form-item>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="数量" required>
<a-input-number v-model:value="inboundForm.quantity" style="width: 100%" :min="1" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="单位">
<a-input v-model:value="inboundForm.unit" placeholder="如:套、件、个" />
</a-form-item>
</a-col>
</a-row>
<a-form-item label="入库类型">
<a-radio-group v-model:value="inboundForm.type">
<a-radio value="purchase">采购入库</a-radio>
<a-radio value="return">退货入库</a-radio>
<a-radio value="transfer">调拨入库</a-radio>
</a-radio-group>
</a-form-item>
<a-form-item label="供应商" v-if="inboundForm.type === 'purchase'">
<a-input v-model:value="inboundForm.supplier" placeholder="请输入供应商名称" />
</a-form-item>
<a-form-item label="备注">
<a-textarea v-model:value="inboundForm.remark" :rows="2" placeholder="请输入备注信息" />
</a-form-item>
</a-form>
</a-modal>
</div>
</template>
<style scoped>
.warehouse-page {
padding: 0;
}
.page-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
}
.glass {
background: rgba(255, 255, 255, 0.85);
backdrop-filter: blur(20px);
border: 1px solid rgba(255, 255, 255, 0.3);
}
.card-hover {
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.card-hover:hover {
transform: translateY(-4px);
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
}
.grid {
display: grid;
}
.grid-cols-4 {
grid-template-columns: repeat(4, 1fr);
}
@media (max-width: 1200px) {
.grid-cols-4 {
grid-template-columns: repeat(2, 1fr);
}
}
.mb-6 {
margin-bottom: 24px;
}
.mb-4 {
margin-bottom: 16px;
}
.mr-1 {
margin-right: 4px;
}
.mr-2 {
margin-right: 8px;
}
.ml-1 {
margin-left: 4px;
}
.text-2xl {
font-size: 24px;
}
.text-3xl {
font-size: 30px;
}
.text-lg {
font-size: 18px;
}
.text-sm {
font-size: 14px;
}
.text-base {
font-size: 16px;
}
.rounded-2xl {
border-radius: 16px;
}
.p-6 {
padding: 24px;
}
.p-4 {
padding: 16px;
}
.gap-6 {
gap: 24px;
}
.flex {
display: flex;
}
.items-center {
align-items: center;
}
.justify-between {
justify-content: space-between;
}
.font-bold {
font-weight: 700;
}
.font-medium {
font-weight: 500;
}
.text-gray-800 {
color: #1f2937;
}
.text-gray-500 {
color: #6b7280;
}
.text-green-600 {
color: #16a34a;
}
.text-orange-600 {
color: #ea580c;
}
</style>

View File

@@ -1,342 +0,0 @@
<template>
<div class="page">
<!-- Hero -->
<section class="relative overflow-hidden bg-gradient-to-br from-violet-900 via-purple-800 to-fuchsia-900 text-white">
<!-- 背景装饰 -->
<div class="absolute inset-0 overflow-hidden">
<div class="absolute -top-1/2 -right-1/4 w-[800px] h-[800px] rounded-full bg-purple-500/20 blur-3xl"></div>
<div class="absolute -bottom-1/2 -left-1/4 w-[600px] h-[600px] rounded-full bg-fuchsia-500/20 blur-3xl"></div>
<div class="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[1000px] h-[1000px] rounded-full bg-violet-500/10 blur-3xl"></div>
<!-- 网格线 -->
<div class="absolute inset-0 bg-[linear-gradient(rgba(255,255,255,0.03)_1px,transparent_1px),linear-gradient(90deg,rgba(255,255,255,0.03)_1px,transparent_1px)] bg-[size:60px_60px]"></div>
<!-- 浮动光点 -->
<div class="absolute top-20 left-20 w-2 h-2 rounded-full bg-white/40 animate-pulse"></div>
<div class="absolute top-40 right-40 w-3 h-3 rounded-full bg-purple-300/30 animate-pulse" style="animation-delay: 0.5s"></div>
<div class="absolute bottom-32 left-1/3 w-2 h-2 rounded-full bg-fuchsia-300/40 animate-pulse" style="animation-delay: 1s"></div>
<div class="absolute top-1/3 right-1/4 w-1.5 h-1.5 rounded-full bg-white/30 animate-pulse" style="animation-delay: 1.5s"></div>
</div>
<div class="relative mx-auto max-w-screen-xl px-4 py-20 md:py-28">
<div class="grid items-center gap-10 md:grid-cols-2">
<div>
<div class="mb-4 flex flex-wrap gap-2">
<a-tag color="purple" class="text-white border-white/30 bg-white/20">AI 驱动</a-tag>
<a-tag color="orange" class="border-orange-300/50 bg-orange-400/20 text-orange-100">🦞 OpenClaw 兼容</a-tag>
</div>
<h1 class="mb-4 text-3xl font-bold md:text-5xl">
AI 智能体
</h1>
<p class="mb-6 text-lg text-white/90">
基于大语言模型打造企业专属 AI 助手兼容 OpenClaw 生态支持知识库问答智能客服数据分析自动化工作流 AI 成为您的得力员工
</p>
<div class="flex flex-wrap gap-3">
<a-button type="primary" size="large" class="bg-white text-purple-700 border-white hover:bg-white/90" @click="navigateTo('/flow')">立即体验</a-button>
<a-button ghost size="large" class="border-white text-white hover:bg-white/10" @click="navigateTo('/contact')">预约演示</a-button>
</div>
</div>
<div class="relative">
<div class="rounded-2xl border border-white/20 bg-white/10 p-6 backdrop-blur">
<div class="mb-4 flex items-center gap-3">
<div class="h-10 w-10 rounded-full bg-gradient-to-br from-purple-400 to-pink-400 flex items-center justify-center text-lg">🤖</div>
<div class="flex-1">
<div class="rounded-lg bg-white/20 px-3 py-2 text-sm">您好我是您的 AI 助手有什么可以帮您</div>
</div>
</div>
<div class="mb-4 flex items-center gap-3 justify-end">
<div class="flex-1 text-right">
<div class="rounded-lg bg-purple-500/40 px-3 py-2 text-sm inline-block text-left">帮我分析一下上个月的销售数据</div>
</div>
<div class="h-8 w-8 rounded-full bg-white/20 flex items-center justify-center text-xs"></div>
</div>
<div class="flex items-center gap-3">
<div class="h-10 w-10 rounded-full bg-gradient-to-br from-purple-400 to-pink-400 flex items-center justify-center text-lg">🤖</div>
<div class="flex-1">
<div class="rounded-lg bg-white/20 px-3 py-2 text-sm">好的正在为您分析...<br/>上月销售额 ¥128,560环比增长 23%主要增长来自...</div>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
<!-- OpenClaw 介绍 -->
<section class="py-16 md:py-20 bg-gradient-to-br from-orange-50 via-amber-50 to-white overflow-hidden">
<div class="mx-auto max-w-screen-xl px-4">
<div class="grid items-center gap-12 md:grid-cols-2">
<!-- 左侧文字介绍 -->
<div>
<div class="mb-3 flex items-center gap-2">
<span class="text-4xl">🦞</span>
<a-tag color="orange" class="text-sm font-medium">当下最热开源 AI Agent</a-tag>
</div>
<h2 class="mb-4 text-2xl font-bold md:text-3xl">
什么是 OpenClaw
</h2>
<p class="mb-4 text-slate-600 leading-relaxed">
OpenClaw又称"AI 龙虾"吉祥物为一只太空龙虾 2026 年爆火的开源个人 AI 助手框架GitHub 收获 <strong class="text-orange-500">310,000+</strong> Star被开发者誉为"能真正干活的 AI 助手"
</p>
<p class="mb-4 text-slate-600 leading-relaxed">
它运行在你自己的设备上支持接入 WhatsAppTelegramDiscord飞书企业微信钉钉等 30 多个平台可以自由切换 ClaudeGPT-4DeepSeek 等模型也支持本地离线模型
</p>
<p class="mb-6 text-slate-600 leading-relaxed">
OpenClaw 的核心理念是 <strong>"任务驱动"</strong>不只是问答而是真正执行任务浏览器操作文件处理代码运行工作流自动化一气呵成
</p>
<div class="flex flex-wrap gap-3">
<div v-for="tag in openclawTags" :key="tag" class="rounded-full bg-orange-100 px-4 py-1.5 text-sm font-medium text-orange-700">
{{ tag }}
</div>
</div>
</div>
<!-- 右侧特性卡片 -->
<div class="grid gap-4 grid-cols-2">
<div v-for="item in openclawFeatures" :key="item.title"
class="rounded-2xl border border-orange-100 bg-white p-5 shadow-sm hover:shadow-md transition-shadow">
<div class="mb-3 text-2xl">{{ item.emoji }}</div>
<h4 class="mb-1 font-semibold text-slate-800">{{ item.title }}</h4>
<p class="text-xs text-slate-500 leading-relaxed">{{ item.desc }}</p>
</div>
<!-- 底部 banner -->
<div class="col-span-2 rounded-2xl bg-gradient-to-r from-orange-500 to-amber-500 p-5 text-white flex items-center justify-between">
<div>
<div class="font-bold text-lg">我们的平台已兼容 OpenClaw</div>
<div class="text-sm text-white/80 mt-1">一键接入无缝协作享受同等生态</div>
</div>
<span class="text-4xl">🦞</span>
</div>
</div>
</div>
</div>
</section>
<!-- 核心功能 -->
<section class="py-16 md:py-24">
<div class="mx-auto max-w-screen-xl px-4">
<div class="mb-12 text-center">
<h2 class="mb-3 text-2xl font-bold md:text-3xl">核心能力</h2>
<p class="text-slate-500">开箱即用的 AI 能力快速赋能业务</p>
</div>
<div class="grid gap-6 md:grid-cols-3">
<a-card v-for="f in features" :key="f.title" class="h-full hover:shadow-lg transition-shadow">
<div class="mb-4 flex h-12 w-12 items-center justify-center rounded-xl bg-purple-50 text-purple-600">
<component :is="f.icon" class="text-xl" />
</div>
<h3 class="mb-2 text-lg font-semibold">{{ f.title }}</h3>
<p class="text-slate-500">{{ f.desc }}</p>
</a-card>
</div>
</div>
</section>
<!-- 应用场景 -->
<section class="bg-slate-50 py-16 md:py-24">
<div class="mx-auto max-w-screen-xl px-4">
<div class="mb-12 text-center">
<h2 class="mb-3 text-2xl font-bold md:text-3xl">应用场景</h2>
<p class="text-slate-500">AI 智能体在各行业的落地实践</p>
</div>
<div class="grid gap-6 md:grid-cols-4">
<div v-for="s in scenarios" :key="s.title" class="rounded-xl bg-white p-6 shadow-sm">
<div class="mb-4 text-3xl">{{ s.emoji }}</div>
<h3 class="mb-2 font-semibold">{{ s.title }}</h3>
<p class="text-sm text-slate-500">{{ s.desc }}</p>
</div>
</div>
</div>
</section>
<!-- 模型能力基础设施 -->
<section class="py-16 md:py-20 bg-gradient-to-br from-slate-900 via-slate-800 to-slate-900 text-white overflow-hidden relative">
<div class="pointer-events-none absolute left-0 top-0 h-full w-full overflow-hidden">
<div class="absolute -top-32 left-1/4 h-[400px] w-[400px] rounded-full bg-purple-500/10 blur-3xl"></div>
<div class="absolute bottom-0 right-1/4 h-[300px] w-[300px] rounded-full bg-fuchsia-500/10 blur-3xl"></div>
</div>
<div class="relative mx-auto max-w-screen-xl px-4">
<!-- 标题 -->
<div class="mb-12 text-center">
<div class="mb-3 inline-flex items-center gap-2 rounded-full bg-purple-500/20 px-4 py-1.5 text-sm text-purple-300">
<span></span> AI 模型能力基础设施
</div>
<h2 class="mb-3 text-2xl font-bold md:text-3xl">自建模型中转平台灵活分配 AI 能力</h2>
<p class="mx-auto max-w-xl text-slate-400">
基于 New API 搭建的模型聚合层统一接入 GPTClaudeDeepSeek 等主流大模型按需为客户分配独立 Key控制额度追踪用量
</p>
</div>
<!-- 架构示意 -->
<div class="mb-10 flex flex-col items-center gap-3 md:flex-row md:justify-center">
<div class="rounded-xl border border-white/10 bg-white/5 px-6 py-4 text-center">
<div class="mb-1 text-2xl">🤖</div>
<div class="text-sm font-medium">你的客户</div>
<div class="mt-1 text-xs text-slate-400">调用 AI 能力</div>
</div>
<div class="text-slate-500 text-xl md:mx-2"></div>
<div class="rounded-xl border border-purple-500/40 bg-purple-500/10 px-6 py-4 text-center ring-1 ring-purple-500/30">
<div class="mb-1 text-2xl">🔑</div>
<div class="text-sm font-medium text-purple-300">platform.websoft.top</div>
<div class="mt-1 text-xs text-slate-400">Key 分发 · 额度管控 · 用量统计</div>
</div>
<div class="text-slate-500 text-xl md:mx-2"></div>
<div class="grid grid-cols-2 gap-2">
<div v-for="m in modelList" :key="m" class="rounded-lg border border-white/10 bg-white/5 px-3 py-2 text-center text-xs text-slate-300">
{{ m }}
</div>
</div>
</div>
<!-- 特性卡片 -->
<div class="mb-10 grid gap-4 md:grid-cols-4">
<div v-for="f in platformFeatures" :key="f.title"
class="rounded-xl border border-white/10 bg-white/5 p-5 hover:border-purple-500/40 transition-colors">
<div class="mb-3 text-2xl">{{ f.emoji }}</div>
<div class="mb-1 font-semibold text-sm">{{ f.title }}</div>
<div class="text-xs text-slate-400 leading-relaxed">{{ f.desc }}</div>
</div>
</div>
<!-- CTA 按钮 -->
<div class="text-center">
<a href="https://platform.websoft.top" target="_blank" rel="noopener"
class="inline-flex items-center gap-2 rounded-lg bg-purple-600 hover:bg-purple-500 px-8 py-3 text-sm font-semibold text-white transition-colors">
<span>🚀</span> 进入模型管理平台
</a>
<p class="mt-3 text-xs text-slate-500">基于 New API 开源项目自建数据私有完全可控</p>
</div>
</div>
</section>
<!-- 套餐 -->
<section class="py-16 md:py-24">
<div class="mx-auto max-w-screen-xl px-4">
<div class="mb-12 text-center">
<h2 class="mb-3 text-2xl font-bold md:text-3xl">套餐与价格</h2>
<p class="text-slate-500">按调用量计费灵活选择</p>
</div>
<div class="grid gap-6 md:grid-cols-3">
<a-card v-for="p in plans" :key="p.name" class="relative h-full">
<div v-if="p.recommended" class="absolute -top-3 left-1/2 -translate-x-1/2">
<a-tag color="purple">推荐</a-tag>
</div>
<div class="mb-4 text-center">
<h3 class="text-lg font-semibold">{{ p.name }}</h3>
<div class="mt-2 text-3xl font-bold text-purple-600">{{ p.price }}</div>
<div class="text-sm text-slate-400">{{ p.period }}</div>
</div>
<a-divider />
<ul class="space-y-3">
<li v-for="feat in p.features" :key="feat" class="flex items-start gap-2">
<CheckOutlined class="mt-0.5 text-green-500" />
<span class="text-sm text-slate-600">{{ feat }}</span>
</li>
</ul>
<a-button :type="p.recommended ? 'primary' : 'default'" block class="mt-6" @click="navigateTo('/flow')">
立即开通
</a-button>
</a-card>
</div>
</div>
</section>
<!-- CTA -->
<section class="bg-gradient-to-r from-violet-600 to-fuchsia-600 py-16 text-white">
<div class="mx-auto max-w-screen-xl px-4 text-center">
<h2 class="mb-4 text-2xl font-bold md:text-3xl"> AI 为您的业务赋能</h2>
<p class="mb-6 text-white/90">7×24 小时在线永不疲倦的智能助手</p>
<div class="flex justify-center gap-3">
<a-button type="primary" size="large" ghost @click="navigateTo('/contact')">预约演示</a-button>
<a-button size="large" class="bg-white text-purple-700 border-white hover:bg-white/90" @click="navigateTo('/flow')">立即体验</a-button>
</div>
</div>
</section>
</div>
</template>
<script setup lang="ts">
import {
RobotOutlined,
BookOutlined,
MessageOutlined,
BarChartOutlined,
ApiOutlined,
SafetyOutlined,
ThunderboltOutlined,
GlobalOutlined,
CheckOutlined
} from '@ant-design/icons-vue'
useHead({ title: 'AI 智能体 - 企业级 AI 助手' })
definePageMeta({ layout: 'default' })
// OpenClaw 标签
const openclawTags = ['开源免费', '本地运行', '多平台接入', '任务驱动', '模型自由切换', '310k+ GitHub Star']
// 模型能力基础设施
const modelList = ['GPT-4o', 'Claude 3.5', 'DeepSeek', 'Gemini', '本地模型', '更多...']
const platformFeatures = [
{ emoji: '🔑', title: 'Key 独立分配', desc: '为每个客户/项目创建独立 API Key互不干扰' },
{ emoji: '📊', title: '额度精准管控', desc: '设置 Token 用量上限,超额自动停用,杜绝超支' },
{ emoji: '🔀', title: '多模型统一入口', desc: '一个 Key 按需调用所有接入模型,客户无感切换' },
{ emoji: '📈', title: '用量实时追踪', desc: '对话量、消耗 Token、费用明细一目了然' },
]
// OpenClaw 核心特性
const openclawFeatures = [
{ emoji: '🌐', title: '30+ 平台接入', desc: 'WhatsApp、飞书、企业微信、Telegram、Discord 等全面支持' },
{ emoji: '🧠', title: '模型自由切换', desc: '支持 Claude、GPT-4、DeepSeek、本地离线模型按需使用' },
{ emoji: '⚡', title: '真正执行任务', desc: '浏览器操作、文件处理、代码运行,不只是聊天' },
{ emoji: '🔒', title: '本地私有部署', desc: '数据不出设备,完全掌握在自己手中,安全可控' },
]
const features = [
{ icon: BookOutlined, title: '知识库问答', desc: '上传文档、网页、表格AI 自动学习并回答相关问题,支持多格式数据源' },
{ icon: MessageOutlined, title: '智能客服', desc: '7×24 小时自动回复,理解上下文,处理复杂咨询,无缝转接人工' },
{ icon: BarChartOutlined, title: '数据分析', desc: '自然语言查询业务数据,自动生成图表和洞察报告,降低数据分析门槛' },
{ icon: ThunderboltOutlined, title: '自动化工作流', desc: 'AI 自动执行重复性任务,如数据录入、邮件回复、日程安排等' },
{ icon: ApiOutlined, title: 'API 集成', desc: '开放 API 接口,轻松对接现有系统,将 AI 能力嵌入到您的应用中' },
{ icon: GlobalOutlined, title: '多语言支持', desc: '支持中英文及 20+ 语种,自动识别语言并回复,服务全球用户' },
{ icon: SafetyOutlined, title: '数据安全', desc: '企业级数据隔离,支持私有化部署,确保敏感信息不泄露' },
{ icon: RobotOutlined, title: '自定义角色', desc: '设定 AI 的人设、语气、专业知识领域,打造品牌专属形象' }
]
const scenarios = [
{ emoji: '🏢', title: '企业知识管理', desc: '构建企业知识库,员工随时查询制度、流程、技术文档' },
{ emoji: '🛒', title: '电商智能导购', desc: '根据用户需求推荐商品,解答产品问题,提升转化率' },
{ emoji: '📚', title: '在线教育辅导', desc: '个性化答疑、作业批改、学习计划制定,提升教学效率' },
{ emoji: '🏥', title: '医疗预问诊', desc: '症状询问、科室推荐、健康知识普及,缓解医疗资源压力' },
{ emoji: '💼', title: 'HR 智能助手', desc: '解答员工问题、协助招聘筛选、自动化入职流程' },
{ emoji: '📊', title: '金融理财顾问', desc: '产品咨询、风险评估、市场分析,提供专业理财建议' },
{ emoji: '🎧', title: '售后技术支持', desc: '故障排查、使用指导、工单创建,提升客户满意度' },
{ emoji: '📝', title: '内容创作辅助', desc: '文案撰写、标题优化、内容润色,提升创作效率' }
]
const plans = [
{
name: '体验版',
price: '¥0',
period: '永久免费',
recommended: false,
features: ['1,000 次/月对话额度', '1 个知识库', '基础模型', '社区支持', '标准响应速度']
},
{
name: '专业版',
price: '¥499',
period: '/月',
recommended: true,
features: ['50,000 次/月对话额度', '10 个知识库', '高级模型(GPT-4)', 'API 接口', '优先技术支持', '数据分析功能']
},
{
name: '企业版',
price: '¥1,999',
period: '/月',
recommended: false,
features: ['无限对话额度', '无限知识库', '专属模型微调', '私有化部署选项', 'SLA 保障', '专属客户成功经理']
}
]
</script>
<style scoped>
.page {
min-height: 100vh;
}
</style>

View File

@@ -1,11 +0,0 @@
<template>
<a-spin class="w-full" tip="跳转中..." />
</template>
<script setup lang="ts">
definePageMeta({ layout: 'blank' })
onMounted(() => {
navigateTo('/developer/apps', { replace: true })
})
</script>

View File

@@ -1,283 +0,0 @@
<template>
<div class="bind-phone-page">
<div class="bind-card">
<div class="bind-header">
<h1>绑定手机号</h1>
<p>首次通过公众号登录请先完成手机号绑定</p>
</div>
<div v-if="pageState === 'loading'" class="bind-state">
<a-spin size="large" />
<span>正在校验登录状态...</span>
</div>
<div v-else-if="pageState === 'error'" class="bind-state error">
<CloseCircleOutlined class="state-icon" />
<p>{{ pageMessage }}</p>
<a-button type="primary" @click="goToLogin">返回登录</a-button>
</div>
<div v-else class="bind-form-wrap">
<a-alert
type="warning"
show-icon
:message="pageMessage || '绑定成功后将自动完成当前扫码登录'"
class="bind-alert"
/>
<a-form ref="formRef" :model="form" :rules="rules" layout="vertical">
<a-form-item label="手机号" name="phone">
<a-input v-model:value="form.phone" size="large" placeholder="请输入手机号" />
</a-form-item>
<a-form-item label="短信验证码" name="smsCode">
<div class="sms-row">
<a-input v-model:value="form.smsCode" size="large" placeholder="请输入短信验证码" />
<a-button :disabled="countdown > 0" :loading="sendingSms" size="large" @click="sendSmsCode">
{{ countdown > 0 ? `${countdown}s 后重试` : '发送验证码' }}
</a-button>
</div>
</a-form-item>
<a-button type="primary" block size="large" :loading="submitting" @click="submit">
绑定手机号并登录
</a-button>
</a-form>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { computed, onMounted, onUnmounted, reactive, ref } from 'vue'
import { message, type FormInstance } from 'ant-design-vue'
import { CloseCircleOutlined } from '@ant-design/icons-vue'
import { checkQrCodeStatus, bindQrLoginPhone, type QrCodeStatusResponse } from '@/api/passport/qrLogin'
import { sendSmsCaptcha } from '@/api/passport/login'
import { setToken } from '@/utils/token-util'
definePageMeta({ layout: 'blank' })
const route = useRoute()
const router = useRouter()
const token = computed(() => String(route.query.token || ''))
const formRef = ref<FormInstance>()
const submitting = ref(false)
const sendingSms = ref(false)
const countdown = ref(0)
const pageState = ref<'loading' | 'ready' | 'error'>('loading')
const pageMessage = ref('')
let countdownTimer: ReturnType<typeof setInterval> | null = null
const form = reactive({
phone: '',
smsCode: ''
})
const phoneReg = /^1[3-9]\d{9}$/
const rules = reactive({
phone: [
{ required: true, message: '请输入手机号', type: 'string' },
{ pattern: phoneReg, message: '手机号格式不正确', trigger: 'blur' }
],
smsCode: [{ required: true, message: '请输入短信验证码', type: 'string' }]
})
function stopCountdown() {
if (countdownTimer) {
clearInterval(countdownTimer)
countdownTimer = null
}
countdown.value = 0
}
function persistUserInfo(result: QrCodeStatusResponse) {
const accessToken = result.accessToken || result.access_token
if (accessToken) {
setToken(String(accessToken), true)
}
if (import.meta.client) {
if (result.tenantId) {
localStorage.setItem('TenantId', String(result.tenantId))
}
const userId = result.userInfo?.userId
if (userId) {
localStorage.setItem('UserId', String(userId))
}
}
}
async function applyLoginResult(result: QrCodeStatusResponse, successText = '登录成功') {
persistUserInfo(result)
message.success(successText)
await router.replace('/')
}
async function loadStatus() {
if (!token.value) {
pageState.value = 'error'
pageMessage.value = '缺少二维码参数,请重新扫码'
return
}
try {
const result = await checkQrCodeStatus(token.value)
if (result.status === 'confirmed') {
await applyLoginResult(result)
return
}
if (result.status === 'bind_phone') {
pageState.value = 'ready'
pageMessage.value = result.message || '请输入手机号和短信验证码,完成首次登录'
return
}
if (result.status === 'expired') {
pageState.value = 'error'
pageMessage.value = '二维码已过期,请返回登录页重新扫码'
return
}
pageState.value = 'error'
pageMessage.value = '当前二维码尚未进入绑定流程,请先完成扫码关注'
} catch (error: unknown) {
pageState.value = 'error'
pageMessage.value = error instanceof Error ? error.message : '校验扫码状态失败'
}
}
async function sendSmsCode() {
if (!phoneReg.test(form.phone)) {
return message.warning('请先输入正确的手机号')
}
sendingSms.value = true
try {
await sendSmsCaptcha({ phone: form.phone })
message.success('验证码已发送')
stopCountdown()
countdown.value = 60
countdownTimer = setInterval(() => {
countdown.value -= 1
if (countdown.value <= 0) {
stopCountdown()
}
}, 1000)
} catch (error: unknown) {
message.error(error instanceof Error ? error.message : '发送验证码失败')
} finally {
sendingSms.value = false
}
}
async function submit() {
if (!formRef.value || !token.value) return
submitting.value = true
try {
await formRef.value.validate()
const result = await bindQrLoginPhone({
token: token.value,
phone: form.phone,
code: form.smsCode
})
await applyLoginResult(result, '手机号绑定成功,已完成登录')
} catch (error: unknown) {
message.error(error instanceof Error ? error.message : '绑定手机号失败')
} finally {
submitting.value = false
}
}
function goToLogin() {
router.replace('/login')
}
onMounted(async () => {
await loadStatus()
})
onUnmounted(() => {
stopCountdown()
})
</script>
<style scoped>
.bind-phone-page {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 24px 16px;
background: linear-gradient(135deg, #eff6ff 0%, #f5f3ff 100%);
}
.bind-card {
width: 460px;
max-width: 100%;
padding: 32px;
border-radius: 20px;
background: #fff;
box-shadow: 0 24px 60px rgba(15, 23, 42, 0.08);
}
.bind-header {
text-align: center;
margin-bottom: 24px;
}
.bind-header h1 {
margin: 0 0 8px;
font-size: 28px;
font-weight: 600;
color: #111827;
}
.bind-header p {
margin: 0;
color: #6b7280;
font-size: 14px;
}
.bind-state {
min-height: 240px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 16px;
text-align: center;
color: #6b7280;
}
.bind-state.error {
color: #ef4444;
}
.state-icon {
font-size: 52px;
}
.bind-form-wrap {
display: flex;
flex-direction: column;
gap: 20px;
}
.bind-alert {
margin-bottom: 4px;
}
.sms-row {
display: grid;
grid-template-columns: 1fr 132px;
gap: 12px;
}
@media (max-width: 640px) {
.bind-card {
padding: 24px 20px;
}
.sms-row {
grid-template-columns: 1fr;
}
}
</style>

View File

@@ -1,595 +0,0 @@
<template>
<div class="mx-auto max-w-screen-xl px-4 py-12">
<a-typography-title :level="1" class="!mb-2">创建应用</a-typography-title>
<a-typography-paragraph class="!text-gray-600 !mb-8">
选择产品与时长填写租户与绑定信息生成订单并支付后自动开通产品并分配管理账号
</a-typography-paragraph>
<a-alert
class="mb-6"
type="info"
show-icon
message="当前页面已打通前端流程与接口对接点;如你的后端返回字段不同(订单号/二维码/账号信息),我可以按实际接口再调整。"
/>
<a-steps :current="step" class="mb-8">
<a-step title="选择产品" />
<a-step title="选择时长" />
<a-step title="填写信息" />
<a-step title="生成订单" />
<a-step title="支付订单" />
<a-step title="开通交付" />
</a-steps>
<a-card v-if="step === 0" title="选择产品">
<a-row :gutter="[16, 16]">
<a-col v-for="p in products" :key="p.code" :xs="24" :md="12" :lg="8">
<a-card
hoverable
:class="selectedProduct?.code === p.code ? 'card-active' : ''"
@click="selectProduct(p)"
>
<template #title>
<div class="flex items-center justify-between gap-2">
<span>{{ p.name }}</span>
<a-tag v-if="p.recommend" color="green">推荐</a-tag>
</div>
</template>
<a-typography-paragraph class="!text-gray-600">
{{ p.desc }}
</a-typography-paragraph>
<div class="flex flex-wrap gap-2">
<a-tag v-for="t in p.tags" :key="t">{{ t }}</a-tag>
</div>
<div class="mt-4 text-sm text-gray-500">
起步价¥{{ p.pricePerMonth }}/
</div>
</a-card>
</a-col>
</a-row>
<div class="mt-6 flex justify-end">
<a-button type="primary" :disabled="!selectedProduct" @click="next()">下一步</a-button>
</div>
</a-card>
<a-card v-else-if="step === 1" title="选择时长">
<a-row :gutter="[16, 16]">
<a-col :xs="24" :lg="14">
<a-segmented v-model:value="durationMonths" :options="durationOptions" block />
<div class="mt-6">
<a-descriptions bordered size="small" :column="1">
<a-descriptions-item label="产品">{{ selectedProduct?.name }}</a-descriptions-item>
<a-descriptions-item label="购买时长">{{ durationMonths }} 个月</a-descriptions-item>
<a-descriptions-item label="单价">¥{{ selectedProduct?.pricePerMonth }}/</a-descriptions-item>
<a-descriptions-item label="应付金额">
<span class="text-lg font-semibold text-green-600">¥{{ priceTotal }}</span>
</a-descriptions-item>
</a-descriptions>
</div>
</a-col>
<a-col :xs="24" :lg="10">
<a-card title="支持加购" size="small">
<a-list size="small" :data-source="addons">
<template #renderItem="{ item }">
<a-list-item>{{ item }}</a-list-item>
</template>
</a-list>
</a-card>
</a-col>
</a-row>
<div class="mt-6 flex justify-between">
<a-button @click="prev()">上一步</a-button>
<a-button type="primary" @click="next()">下一步</a-button>
</div>
</a-card>
<a-card v-else-if="step === 2" title="填写租户与绑定信息">
<a-form ref="formRef" layout="vertical" :model="form" :rules="rules" @finish="next">
<a-row :gutter="16">
<a-col :xs="24" :md="12">
<a-form-item label="租户名称" name="tenantName">
<a-input v-model:value="form.tenantName" placeholder="例如:某某科技有限公司" />
</a-form-item>
</a-col>
<a-col :xs="24" :md="12">
<a-form-item label="绑定域名" name="domain">
<a-input v-model:value="form.domain" placeholder="例如example.com" />
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :xs="24" :md="12">
<a-form-item label="邮箱" name="email">
<a-input v-model:value="form.email" placeholder="用于接收交付信息" />
</a-form-item>
</a-col>
<a-col :xs="24" :md="12">
<a-form-item label="手机号" name="phone">
<a-input v-model:value="form.phone" placeholder="用于短信验证与管理员账号" />
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16" align="middle">
<a-col :xs="24" :md="12">
<a-form-item label="短信验证码" name="smsCode">
<a-input v-model:value="form.smsCode" placeholder="请输入验证码" />
</a-form-item>
</a-col>
<a-col :xs="24" :md="12" class="flex items-center">
<a-button :disabled="smsCountdown > 0" :loading="smsSending" @click="onSendSms">
{{ smsCountdown > 0 ? `${smsCountdown}s 后重试` : '发送验证码' }}
</a-button>
</a-col>
</a-row>
<a-alert
class="mb-4"
type="warning"
show-icon
message="短信验证码接口复用登录短信验证码sendSmsCaptcha。如你有专用的开通验证码接口可替换。"
/>
<div class="flex justify-between">
<a-button @click="prev()">上一步</a-button>
<a-button type="primary" html-type="submit">下一步</a-button>
</div>
</a-form>
</a-card>
<a-card v-else-if="step === 3" title="生成订单">
<a-descriptions bordered :column="1">
<a-descriptions-item label="产品">{{ selectedProduct?.name }}</a-descriptions-item>
<a-descriptions-item label="购买时长">{{ durationMonths }} 个月</a-descriptions-item>
<a-descriptions-item label="租户名称">{{ form.tenantName }}</a-descriptions-item>
<a-descriptions-item label="绑定域名">{{ form.domain }}</a-descriptions-item>
<a-descriptions-item label="邮箱">{{ form.email }}</a-descriptions-item>
<a-descriptions-item label="手机号">{{ form.phone }}</a-descriptions-item>
<a-descriptions-item label="应付金额">
<span class="text-lg font-semibold text-green-600">¥{{ priceTotal }}</span>
</a-descriptions-item>
</a-descriptions>
<a-alert
class="mt-4"
type="info"
show-icon
message="点击“生成订单”后将创建订单并请求微信 Native 支付二维码。"
/>
<div class="mt-6 flex justify-between">
<a-button @click="prev()">上一步</a-button>
<a-button type="primary" :loading="creatingOrder" @click="createOrderAndPay">生成订单</a-button>
</div>
</a-card>
<a-card v-else-if="step === 4" title="支付订单">
<a-row :gutter="[24, 24]">
<a-col :xs="24" :lg="12">
<a-descriptions bordered size="small" :column="1">
<a-descriptions-item label="订单号">{{ order?.orderNo || '-' }}</a-descriptions-item>
<a-descriptions-item label="金额">¥{{ order?.payPrice || priceTotal }}</a-descriptions-item>
<a-descriptions-item label="支付方式">微信 Native扫码支付</a-descriptions-item>
</a-descriptions>
<div class="mt-6 flex flex-wrap gap-2">
<a-button :loading="checkingPay" @click="checkPayStatus">查询支付状态</a-button>
<a-button @click="rebuildPayCode" :disabled="!order">重新获取二维码</a-button>
<a-button danger @click="resetAll">取消并重来</a-button>
</div>
<a-alert
class="mt-6"
type="warning"
show-icon
message="支付成功后点击“查询支付状态”,确认到账后自动进入开通交付。"
/>
</a-col>
<a-col :xs="24" :lg="12">
<a-card title="扫码支付" size="small">
<div class="flex items-center justify-center py-6">
<a-qrcode v-if="payCodeUrl" :value="payCodeUrl" :size="220" />
<a-empty v-else description="暂无二维码" />
</div>
</a-card>
</a-col>
</a-row>
</a-card>
<a-card v-else-if="step === 5" title="开通交付">
<a-result
v-if="provisioned"
status="success"
title="开通成功"
sub-title="租户已创建并完成初始化可使用管理员账号登录后台"
>
<template #extra>
<a-space>
<a-button type="primary" @click="navigateTo('/contact')">获取后台地址</a-button>
<a-button @click="resetAll">再创建一个</a-button>
</a-space>
</template>
</a-result>
<a-alert
v-else
type="info"
show-icon
message="正在开通中..."
/>
<a-divider />
<a-descriptions bordered size="small" :column="1">
<a-descriptions-item label="TenantId">
{{ provisionInfo?.user?.tenantId ?? '-' }}
</a-descriptions-item>
<a-descriptions-item label="租户名称">{{ form.tenantName }}</a-descriptions-item>
<a-descriptions-item label="绑定域名">{{ form.domain }}</a-descriptions-item>
<a-descriptions-item label="管理员账号">
{{ provisionInfo?.user?.username || form.phone }}
</a-descriptions-item>
<a-descriptions-item label="初始密码">
{{ adminPasswordHint }}
</a-descriptions-item>
<a-descriptions-item label="Access Token">
<a-typography-text
v-if="provisionInfo?.access_token"
:copyable="{ text: provisionInfo.access_token }"
>
点击复制
</a-typography-text>
<span v-else>-</span>
</a-descriptions-item>
</a-descriptions>
</a-card>
</div>
</template>
<script setup lang="ts">
import { message } from 'ant-design-vue'
import { usePageSeo } from '@/composables/usePageSeo'
import { sendSmsCaptcha } from '@/api/passport/login'
import { createWithOrder, getNativeCode, type PaymentCreateResult } from '@/api/system/payment'
import { getOrder } from '@/api/system/order'
import request from '@/utils/request'
import { SERVER_API_URL } from '@/config/setting'
import type { ApiResult } from '@/api'
import type { Order } from '@/api/system/order/model'
type Product = {
code: string
name: string
desc: string
tags: string[]
pricePerMonth: number
recommend?: boolean
}
usePageSeo({
title: '创建应用 - 选品/支付/自动开通',
description:
'选择产品与时长,填写租户信息并短信验证,生成订单并支付后自动创建租户、初始化模块与数据并交付管理账号。',
path: '/create-app'
})
const step = ref(0)
const products: Product[] = [
{
code: 'website',
name: '企业官网',
desc: '品牌展示与获客转化支持模板、SEO 与可视化配置。',
tags: ['模板', 'SEO', '多语言'],
pricePerMonth: 199,
recommend: true
},
{
code: 'shop',
name: '电商系统',
desc: '商品/订单/支付/营销基础能力,插件化扩展。',
tags: ['支付', '插件', '营销'],
pricePerMonth: 399,
recommend: true
},
{
code: 'mp',
name: '小程序/公众号',
desc: '多端渠道接入与统一管理,适配内容与电商场景。',
tags: ['多端', '渠道'],
pricePerMonth: 299
}
]
const selectedProduct = ref<Product | null>(null)
const durationMonths = ref(12)
const durationOptions = [
{ label: '1个月', value: 1 },
{ label: '3个月', value: 3 },
{ label: '12个月', value: 12 },
{ label: '24个月', value: 24 }
]
const addons = ['模板加购(示例)', '插件加购(示例)', '私有化交付(示例)']
const priceTotal = computed(() => {
const base = selectedProduct.value?.pricePerMonth || 0
return base * Number(durationMonths.value || 0)
})
const formRef = ref()
const form = reactive({
tenantName: '',
domain: '',
email: '',
phone: '',
smsCode: ''
})
function isDomainLike(v: string) {
const value = v.trim().toLowerCase()
return /^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,}$/.test(value)
}
function isPhoneLike(v: string) {
const value = v.trim()
return /^1\d{10}$/.test(value)
}
function isSmsCodeLike(v: string) {
const value = v.trim()
return /^\d{4,8}$/.test(value)
}
const rules = {
tenantName: [{ required: true, message: '请填写租户名称' }],
domain: [
{ required: true, message: '请填写绑定域名' },
{ validator: (_: unknown, v: string) => (isDomainLike(v) ? Promise.resolve() : Promise.reject(new Error('域名格式不正确'))) }
],
email: [{ required: true, type: 'email', message: '请填写正确邮箱' }],
phone: [
{ required: true, message: '请填写手机号' },
{ validator: (_: unknown, v: string) => (isPhoneLike(v) ? Promise.resolve() : Promise.reject(new Error('手机号格式不正确'))) }
],
smsCode: [
{ required: true, message: '请填写短信验证码' },
{ validator: (_: unknown, v: string) => (isSmsCodeLike(v) ? Promise.resolve() : Promise.reject(new Error('验证码格式不正确'))) }
]
}
const smsSending = ref(false)
const smsCountdown = ref(0)
let countdownTimer: ReturnType<typeof setInterval> | undefined
async function onSendSms() {
if (!form.phone) {
message.warning('请先填写手机号')
return
}
smsSending.value = true
try {
await sendSmsCaptcha({ phone: form.phone })
message.success('验证码已发送')
smsCountdown.value = 60
if (countdownTimer) clearInterval(countdownTimer)
countdownTimer = setInterval(() => {
smsCountdown.value -= 1
if (smsCountdown.value <= 0) {
smsCountdown.value = 0
if (countdownTimer) clearInterval(countdownTimer)
countdownTimer = undefined
}
}, 1000)
} catch (e) {
message.error(String(e))
} finally {
smsSending.value = false
}
}
function selectProduct(p: Product) {
selectedProduct.value = p
}
function next() {
if (step.value < 5) step.value += 1
}
function prev() {
if (step.value > 0) step.value -= 1
}
const creatingOrder = ref(false)
const order = ref<Order | null>(null)
const payCodeUrl = ref<string>('')
const payment = ref<PaymentCreateResult | null>(null)
function pickFirstString(obj: unknown, keys: string[]) {
if (!obj || typeof obj !== 'object') return ''
const record = obj as Record<string, unknown>
for (const key of keys) {
const value = record[key]
if (typeof value === 'string' && value) return value
}
return ''
}
async function createOrderAndPay() {
if (!selectedProduct.value) return
creatingOrder.value = true
try {
const orderInfo: Partial<Order> & Record<string, unknown> = {
type: 0,
channel: 0,
realName: form.tenantName,
phone: form.phone,
totalNum: durationMonths.value,
totalPrice: String(priceTotal.value),
payPrice: String(priceTotal.value),
description: JSON.stringify({
product: selectedProduct.value.code,
months: durationMonths.value,
tenantName: form.tenantName,
domain: form.domain,
email: form.email
})
}
const unifiedPayload = {
paymentChannel: 'WECHAT_NATIVE',
paymentType: 1,
amount: priceTotal.value,
subject: `${selectedProduct.value.name}${durationMonths.value}个月)`,
description: `租户:${form.tenantName};域名:${form.domain}`,
goodsId: selectedProduct.value.code,
quantity: 1,
orderType: 0,
buyerRemarks: orderInfo.description,
extraParams: {
product: selectedProduct.value.code,
months: durationMonths.value,
tenantName: form.tenantName,
domain: form.domain,
email: form.email,
phone: form.phone
},
order: orderInfo
}
const data = await createWithOrder(unifiedPayload)
payment.value = data || null
const orderFromApi = (data as any)?.order || (data as any)?.orderInfo || (data as any)?.orderDTO
order.value = {
...(orderFromApi || {}),
orderId: (data as any)?.orderId ?? orderFromApi?.orderId,
orderNo: (data as any)?.orderNo ?? orderFromApi?.orderNo,
payPrice: (orderFromApi || {})?.payPrice ?? String(priceTotal.value)
} as Order
payCodeUrl.value =
pickFirstString(data, ['codeUrl', 'url', 'payUrl', 'paymentUrl', 'qrcode']) ||
pickFirstString(order.value, ['qrcode'])
if (!payCodeUrl.value && order.value?.orderId) {
await rebuildPayCode()
}
if (!payCodeUrl.value) {
message.warning('后端未返回二维码地址codeUrl/url/payUrl请确认统一下单接口返回格式或提供支付二维码接口')
}
step.value = 4
} catch (e) {
message.error(String(e))
} finally {
creatingOrder.value = false
}
}
async function rebuildPayCode() {
if (!order.value) return
try {
const data = await getNativeCode(order.value)
payCodeUrl.value = pickFirstString(data, ['codeUrl', 'url', 'payUrl', 'paymentUrl', 'qrcode']) || String(data || '')
if (!payCodeUrl.value) {
message.warning('后端未返回二维码地址codeUrl/url请确认接口返回格式')
}
} catch (e) {
message.error(String(e))
}
}
const checkingPay = ref(false)
const provisioned = ref(false)
const adminPasswordHint = '初始密码将通过短信/邮件发送(或由客服提供)'
async function checkPayStatus() {
if (!order.value?.orderId) {
message.warning('缺少订单ID暂无法查询支付状态请确认统一下单接口是否返回 orderId或提供按 orderNo/paymentNo 查询的接口)')
return
}
checkingPay.value = true
try {
const latest = await getOrder(order.value.orderId)
order.value = latest
if (Number(latest.payStatus) === 1) {
message.success('已支付,开始开通...')
step.value = 5
await provision()
} else {
message.info('订单未支付或未到账,请稍后重试')
}
} catch (e) {
message.error(String(e))
} finally {
checkingPay.value = false
}
}
async function provision() {
try {
const payload = {
productName: form.tenantName,
domain: form.domain,
email: form.email,
phone: form.phone,
username: form.phone,
smsCode: form.smsCode,
code: form.smsCode,
description: JSON.stringify({
product: selectedProduct.value?.code,
months: durationMonths.value,
orderNo: order.value?.orderNo
})
}
const res = await request.post<ApiResult<unknown>>(SERVER_API_URL + '/superAdminRegister', payload)
if (res.data.code !== 0) throw new Error(res.data.message || '开通失败')
provisionInfo.value = (res.data.data || null) as any
provisioned.value = true
} catch (e) {
provisioned.value = false
message.error(String(e))
}
}
type ProvisionUser = {
tenantId?: number
tenantName?: string | null
username?: string
phone?: string
email?: string | null
} & Record<string, unknown>
type ProvisionInfo = {
access_token?: string
user?: ProvisionUser
} & Record<string, unknown>
const provisionInfo = ref<ProvisionInfo | null>(null)
function resetAll() {
step.value = 0
selectedProduct.value = null
durationMonths.value = 12
form.tenantName = ''
form.domain = ''
form.email = ''
form.phone = ''
form.smsCode = ''
order.value = null
payCodeUrl.value = ''
payment.value = null
provisioned.value = false
provisionInfo.value = null
}
</script>
<style scoped>
.card-active {
border-color: #16a34a;
box-shadow: 0 0 0 2px rgba(22, 163, 74, 0.15);
}
</style>

View File

@@ -1,91 +0,0 @@
<template>
<div class="mx-auto max-w-screen-xl px-4 py-12">
<a-typography-title :level="1" class="!mb-2">部署方案</a-typography-title>
<a-typography-paragraph class="!text-gray-600 !mb-8">
支持 SaaS私有化与混合部署针对安全合规/数据隔离/运维可控等需求提供交付物清单与验收流程
</a-typography-paragraph>
<a-alert
class="mb-6"
type="info"
show-icon
message="支持私有化部署:提供部署文档、初始化脚本、升级/回滚建议与验收清单。"
/>
<a-table :columns="columns" :data-source="rows" :pagination="false" row-key="key" />
<a-row class="mt-10" :gutter="[16, 16]">
<a-col :xs="24" :md="12">
<a-card title="私有化交付清单(示例)">
<a-list :data-source="deliverables" size="small" bordered>
<template #renderItem="{ item }">
<a-list-item>{{ item }}</a-list-item>
</template>
</a-list>
</a-card>
</a-col>
<a-col :xs="24" :md="12">
<a-card title="上线与升级策略">
<a-collapse>
<a-collapse-panel key="1" header="升级方式">
<div class="text-gray-600">
支持版本升级与兼容性说明建议灰度升级并保留回滚方案
</div>
</a-collapse-panel>
<a-collapse-panel key="2" header="数据安全">
<div class="text-gray-600">
提供租户隔离权限审计数据备份/恢复建议可对接客户既有安全体系
</div>
</a-collapse-panel>
<a-collapse-panel key="3" header="高可用建议">
<div class="text-gray-600">
提供多实例部署负载均衡与健康检查建议根据业务量规划资源与扩容策略
</div>
</a-collapse-panel>
</a-collapse>
</a-card>
</a-col>
</a-row>
<div class="mt-10">
<a-space>
<a-button @click="navigateTo('/flow')">查看开通流程</a-button>
<a-button type="primary" @click="navigateTo('/contact')">获取部署方案</a-button>
</a-space>
</div>
</div>
</template>
<script setup lang="ts">
import { usePageSeo } from '@/composables/usePageSeo'
usePageSeo({
title: '部署方案 - SaaS / 私有化 / 混合部署',
description: '支持 SaaS、私有化与混合部署提供交付物清单与验收流程满足安全合规与运维可控需求。',
path: '/deploy'
})
const columns = [
{ title: '对比项', dataIndex: 'name' },
{ title: 'SaaS', dataIndex: 'saas' },
{ title: '私有化', dataIndex: 'private' },
{ title: '混合', dataIndex: 'hybrid' }
]
const rows = [
{ key: 'k1', name: '交付速度', saas: '最快', private: '中', hybrid: '中' },
{ key: 'k2', name: '数据与合规', saas: '标准', private: '最高可控', hybrid: '可定制' },
{ key: 'k3', name: '运维成本', saas: '最低', private: '客户自运维', hybrid: '可分担' },
{ key: 'k4', name: '扩展能力', saas: '强', private: '强', hybrid: '强' },
{ key: 'k5', name: '适用场景', saas: '快速试用/中小团队', private: '政企/强合规', hybrid: '集团/多系统' }
]
const deliverables = [
'部署包/镜像(示例)',
'部署与运维文档(示例)',
'初始化脚本与默认配置(示例)',
'验收清单与检查项(示例)',
'升级/回滚建议(示例)'
]
</script>

View File

@@ -1,364 +0,0 @@
<template>
<div class="page">
<!-- Hero -->
<section class="relative overflow-hidden bg-gradient-to-br from-slate-950 via-slate-900 to-indigo-950 text-white">
<div class="absolute inset-0 overflow-hidden pointer-events-none">
<div class="absolute -top-1/2 -right-1/4 w-[700px] h-[700px] rounded-full bg-indigo-500/15 blur-3xl"></div>
<div class="absolute -bottom-1/2 -left-1/4 w-[600px] h-[600px] rounded-full bg-violet-500/10 blur-3xl"></div>
<div class="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[800px] h-[800px] rounded-full bg-cyan-500/8 blur-3xl"></div>
<div class="absolute inset-0 bg-[linear-gradient(rgba(255,255,255,0.02)_1px,transparent_1px),linear-gradient(90deg,rgba(255,255,255,0.02)_1px,transparent_1px)] bg-[size:60px_60px]"></div>
<!-- 浮动光点 -->
<div class="absolute top-20 left-20 w-1.5 h-1.5 rounded-full bg-indigo-400/60 animate-pulse"></div>
<div class="absolute top-48 right-32 w-2 h-2 rounded-full bg-cyan-400/40 animate-pulse" style="animation-delay:0.6s"></div>
<div class="absolute bottom-28 left-1/3 w-1.5 h-1.5 rounded-full bg-violet-400/50 animate-pulse" style="animation-delay:1.2s"></div>
<div class="absolute top-1/3 right-1/5 w-1 h-1 rounded-full bg-white/30 animate-pulse" style="animation-delay:1.8s"></div>
</div>
<div class="relative mx-auto max-w-screen-xl px-4 py-20 md:py-28">
<div class="grid items-center gap-12 md:grid-cols-2">
<div>
<div class="mb-5 flex flex-wrap gap-2">
<a-tag class="border-indigo-400/40 bg-indigo-500/20 text-indigo-200">🛠 开发者中心</a-tag>
<a-tag class="border-cyan-400/30 bg-cyan-500/15 text-cyan-200">开放 API</a-tag>
<a-tag class="border-violet-400/30 bg-violet-500/15 text-violet-200">源码交付</a-tag>
</div>
<h1 class="mb-4 text-4xl font-bold leading-tight md:text-5xl">
构建下一代<br>
<span class="bg-gradient-to-r from-indigo-400 via-cyan-400 to-violet-400 bg-clip-text text-transparent">AI 原生应用</span>
</h1>
<p class="mb-8 text-lg text-slate-300 leading-relaxed">
完整的开放 APISDK源码交付与二次开发支持 SaaS 到私有化部署让您的团队完全掌控每一行代码
</p>
<div class="flex flex-wrap gap-3">
<a-button type="primary" size="large"
class="bg-indigo-600 border-indigo-600 hover:bg-indigo-500 font-semibold"
@click="navigateTo('/developer')">
进入开发者控制台
</a-button>
<a-button ghost size="large" class="border-white/30 text-white hover:bg-white/10"
@click="navigateTo('/developer/docs')">
查看开发文档
</a-button>
</div>
</div>
<!-- 右侧代码卡片 -->
<div class="relative">
<div class="rounded-2xl border border-white/10 bg-slate-800/80 backdrop-blur overflow-hidden shadow-2xl">
<!-- 顶栏 -->
<div class="flex items-center gap-2 px-4 py-3 border-b border-white/10 bg-slate-900/60">
<div class="w-3 h-3 rounded-full bg-red-500/80"></div>
<div class="w-3 h-3 rounded-full bg-yellow-500/80"></div>
<div class="w-3 h-3 rounded-full bg-green-500/80"></div>
<span class="ml-3 text-xs text-slate-400 font-mono">quickstart.ts</span>
</div>
<pre class="p-5 text-sm font-mono leading-relaxed overflow-x-auto"><code><span class="text-slate-400">// 初始化 SDK</span>
<span class="text-cyan-400">import</span> <span class="text-white">{ WebsopyClient }</span> <span class="text-cyan-400">from</span> <span class="text-green-400">'@websopy/sdk'</span>
<span class="text-cyan-400">const</span> <span class="text-indigo-300">client</span> <span class="text-white">= new</span> <span class="text-yellow-300">WebsopyClient</span><span class="text-white">({</span>
<span class="text-slate-300">tenantId</span><span class="text-white">:</span> <span class="text-green-400">'your-tenant-id'</span><span class="text-white">,</span>
<span class="text-slate-300">apiKey</span><span class="text-white">:</span> <span class="text-green-400">'sk-xxxxxxxxxxxxxxxx'</span>
<span class="text-white">})</span>
<span class="text-slate-400">// 调用 AI 智能体</span>
<span class="text-cyan-400">const</span> <span class="text-indigo-300">reply</span> <span class="text-white">=</span> <span class="text-cyan-400">await</span> <span class="text-indigo-300">client</span><span class="text-white">.</span><span class="text-yellow-300">agent</span><span class="text-white">.</span><span class="text-yellow-300">chat</span><span class="text-white">({</span>
<span class="text-slate-300">message</span><span class="text-white">:</span> <span class="text-green-400">'帮我分析本月销售数据'</span>
<span class="text-white">})</span>
<span class="text-indigo-300">console</span><span class="text-white">.</span><span class="text-yellow-300">log</span><span class="text-white">(</span><span class="text-indigo-300">reply</span><span class="text-white">.</span><span class="text-slate-300">content</span><span class="text-white">)</span>
</code></pre>
</div>
<!-- 浮动标签 -->
<div class="absolute -top-3 -right-3 rounded-xl bg-green-500 px-3 py-1.5 text-xs font-semibold text-white shadow-lg">
TypeScript 支持
</div>
<div class="absolute -bottom-3 -left-3 rounded-xl bg-indigo-500 px-3 py-1.5 text-xs font-semibold text-white shadow-lg">
OpenAPI 3.0 规范
</div>
</div>
</div>
</div>
</section>
<!-- 数据横幅 -->
<section class="bg-gradient-to-r from-indigo-600 via-violet-600 to-indigo-700 py-8">
<div class="mx-auto max-w-screen-xl px-4">
<div class="grid grid-cols-2 gap-6 md:grid-cols-4">
<div v-for="s in bannerStats" :key="s.label" class="text-center text-white">
<div class="text-2xl font-bold md:text-3xl">{{ s.value }}</div>
<div class="mt-1 text-sm text-indigo-200">{{ s.label }}</div>
</div>
</div>
</div>
</section>
<!-- 核心能力 -->
<section class="py-16 md:py-24 bg-white">
<div class="mx-auto max-w-screen-xl px-4">
<div class="mb-12 text-center">
<a-tag color="blue" class="mb-3">核心能力</a-tag>
<h2 class="text-2xl font-bold md:text-3xl mb-3">为开发者提供完整的工具链</h2>
<p class="text-slate-500 max-w-2xl mx-auto"> API 调用到源码级二次开发覆盖所有开发场景</p>
</div>
<div class="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
<div v-for="cap in capabilities" :key="cap.title"
class="group relative rounded-2xl border border-slate-100 bg-white p-7 shadow-sm hover:border-indigo-200 hover:shadow-lg transition-all cursor-default">
<div class="mb-5 flex h-14 w-14 items-center justify-center rounded-2xl text-2xl"
:class="cap.iconBg">
{{ cap.emoji }}
</div>
<h3 class="mb-2 font-bold text-lg text-slate-800">{{ cap.title }}</h3>
<p class="text-sm text-slate-500 leading-relaxed mb-4">{{ cap.desc }}</p>
<div class="flex flex-wrap gap-1.5">
<span v-for="tag in cap.tags" :key="tag"
class="rounded-full bg-slate-100 px-2.5 py-0.5 text-xs text-slate-600">{{ tag }}</span>
</div>
</div>
</div>
</div>
</section>
<!-- 开发流程 -->
<section class="py-16 md:py-20 bg-gradient-to-b from-slate-50 to-white">
<div class="mx-auto max-w-screen-xl px-4">
<div class="mb-12 text-center">
<a-tag color="blue" class="mb-3">快速上手</a-tag>
<h2 class="text-2xl font-bold md:text-3xl mb-3">4 步完成接入</h2>
<p class="text-slate-500">从注册到上线最快 30 分钟</p>
</div>
<div class="relative grid gap-6 md:grid-cols-4">
<div v-for="(step, i) in steps" :key="step.title" class="relative text-center">
<!-- 连接线 -->
<div v-if="i < steps.length - 1"
class="absolute top-8 left-[calc(50%+36px)] right-0 h-0.5 bg-gradient-to-r from-indigo-300 to-transparent hidden md:block"></div>
<div class="mx-auto mb-4 flex h-16 w-16 items-center justify-center rounded-2xl bg-gradient-to-br from-indigo-500 to-violet-600 text-white text-2xl shadow-lg">
{{ step.emoji }}
</div>
<div class="mb-1 text-xs font-bold text-indigo-500 uppercase tracking-wider">Step {{ i + 1 }}</div>
<h4 class="mb-2 font-bold text-slate-800">{{ step.title }}</h4>
<p class="text-sm text-slate-500 leading-relaxed">{{ step.desc }}</p>
</div>
</div>
</div>
</section>
<!-- API & SDK 展示 -->
<section class="py-16 md:py-20 bg-slate-950 text-white">
<div class="mx-auto max-w-screen-xl px-4">
<div class="grid items-start gap-12 md:grid-cols-2">
<div>
<a-tag class="mb-4 border-indigo-400/40 bg-indigo-500/20 text-indigo-200">REST API</a-tag>
<h2 class="text-2xl font-bold md:text-3xl mb-4">完整的 REST API</h2>
<p class="text-slate-400 mb-6 leading-relaxed">
基于 OpenAPI 3.0 规范支持所有主流语言调用提供完整的 Swagger 文档支持在线调试
</p>
<div class="space-y-3 mb-8">
<div v-for="api in apiFeatures" :key="api" class="flex items-center gap-3 text-slate-300">
<span class="text-indigo-400 flex-shrink-0"></span>{{ api }}
</div>
</div>
<a-button size="large"
class="bg-indigo-600 border-indigo-600 text-white hover:bg-indigo-500"
@click="navigateTo('/developer/apps')">
查看 API 文档
</a-button>
</div>
<div class="space-y-4">
<div v-for="sdk in sdkList" :key="sdk.lang"
class="flex items-center gap-4 rounded-xl border border-white/10 bg-white/5 px-5 py-4 hover:border-indigo-500/50 hover:bg-white/8 transition-all">
<span class="text-3xl flex-shrink-0">{{ sdk.emoji }}</span>
<div class="flex-1">
<div class="font-semibold text-white">{{ sdk.lang }}</div>
<div class="text-sm text-slate-400 mt-0.5">{{ sdk.desc }}</div>
</div>
<span class="rounded-full px-2.5 py-0.5 text-xs font-medium" :class="sdk.statusClass">{{ sdk.status }}</span>
</div>
</div>
</div>
</div>
</section>
<!-- 源码交付 -->
<section class="py-16 md:py-20 bg-white">
<div class="mx-auto max-w-screen-xl px-4">
<div class="grid items-center gap-12 md:grid-cols-2">
<div class="grid grid-cols-2 gap-4">
<div v-for="item in sourceItems" :key="item.title"
class="rounded-2xl border border-slate-100 bg-gradient-to-br p-5 shadow-sm hover:shadow-md transition-shadow"
:class="item.bg">
<div class="text-3xl mb-3">{{ item.emoji }}</div>
<div class="font-bold text-slate-800 mb-1">{{ item.title }}</div>
<div class="text-xs text-slate-500 leading-relaxed">{{ item.desc }}</div>
</div>
</div>
<div>
<a-tag color="blue" class="mb-4">源码交付</a-tag>
<h2 class="text-2xl font-bold md:text-3xl mb-4">拿到代码完全掌控</h2>
<p class="text-slate-600 mb-6 leading-relaxed">
购买源码权益后通过 Git 仓库直接获取完整项目代码支持私有化部署二次开发无任何商用限制
</p>
<ul class="space-y-3 mb-8">
<li v-for="point in sourcePoints" :key="point" class="flex items-center gap-3 text-slate-700">
<span class="flex h-5 w-5 items-center justify-center rounded-full bg-indigo-100 text-indigo-600 text-xs flex-shrink-0"></span>
{{ point }}
</li>
</ul>
<a-button type="primary" size="large"
class="bg-indigo-600 border-indigo-600 hover:bg-indigo-500"
@click="navigateTo('/developer/source')">
申请源码访问权限
</a-button>
</div>
</div>
</div>
</section>
<!-- 开发者社区 -->
<section class="py-16 md:py-20 bg-gradient-to-br from-slate-50 to-indigo-50">
<div class="mx-auto max-w-screen-xl px-4">
<div class="mb-12 text-center">
<a-tag color="blue" class="mb-3">开发者生态</a-tag>
<h2 class="text-2xl font-bold md:text-3xl mb-3">加入开发者社区</h2>
<p class="text-slate-500">与数千名开发者共同构建 AI 应用生态</p>
</div>
<div class="grid gap-5 sm:grid-cols-2 lg:grid-cols-4">
<div v-for="community in communities" :key="community.title"
class="rounded-2xl bg-white border border-slate-100 p-6 text-center shadow-sm hover:shadow-md hover:border-indigo-200 transition-all">
<div class="text-4xl mb-4">{{ community.emoji }}</div>
<h4 class="font-bold text-slate-800 mb-2">{{ community.title }}</h4>
<p class="text-sm text-slate-500 leading-relaxed">{{ community.desc }}</p>
</div>
</div>
</div>
</section>
<!-- CTA -->
<section class="relative overflow-hidden bg-gradient-to-r from-indigo-600 via-violet-600 to-indigo-700 py-16 text-white">
<div class="pointer-events-none absolute inset-0">
<div class="absolute -top-1/2 right-0 w-[500px] h-[500px] rounded-full bg-white/5 blur-3xl"></div>
<div class="absolute -bottom-1/2 left-0 w-[400px] h-[400px] rounded-full bg-white/5 blur-3xl"></div>
</div>
<div class="relative mx-auto max-w-screen-xl px-4 text-center">
<h2 class="text-2xl font-bold md:text-3xl mb-3">准备好开始构建了吗</h2>
<p class="text-indigo-200 mb-8 max-w-xl mx-auto">注册开发者账号立即获取 API Key免费体验全部接口</p>
<div class="flex justify-center flex-wrap gap-4">
<a-button size="large"
class="bg-white text-indigo-700 border-white font-semibold hover:bg-white/90"
@click="navigateTo('/developer')">
🚀 进入开发者控制台
</a-button>
<a-button ghost size="large" class="border-white text-white hover:bg-white/10"
@click="navigateTo('/developer/docs')">
📖 查看开发文档
</a-button>
</div>
</div>
</section>
</div>
</template>
<script setup lang="ts">
useHead({ title: '开发者中心 - 开放 API · SDK · 源码交付' })
definePageMeta({ layout: 'default' })
const bannerStats = [
{ value: '200+', label: 'REST API 接口' },
{ value: '30min', label: '最快接入时间' },
{ value: '5+', label: 'SDK 语言支持' },
{ value: '99.9%', label: 'API 可用性 SLA' },
]
const capabilities = [
{
emoji: '🔌',
title: 'REST API',
iconBg: 'bg-indigo-50',
desc: '200+ 标准接口覆盖用户、内容、AI、支付等全部业务模块OpenAPI 3.0 规范,支持 Swagger 在线调试。',
tags: ['OpenAPI 3.0', 'Swagger UI', 'Webhook', 'OAuth 2.0'],
},
{
emoji: '📦',
title: 'SDK & 客户端库',
iconBg: 'bg-violet-50',
desc: '官方提供 TypeScript / JavaScript SDK社区维护 Python、Java、Go、PHP 版本,降低接入成本。',
tags: ['TypeScript', 'Python', 'Java', 'Go'],
},
{
emoji: '💻',
title: '源码交付',
iconBg: 'bg-cyan-50',
desc: '购买源码权益后,通过私有 Git 仓库获取完整源代码,支持私有化部署与无限制二次开发。',
tags: ['Git 仓库', '私有化部署', '二次开发', 'MIT 授权'],
},
{
emoji: '🤖',
title: 'AI 智能体 API',
iconBg: 'bg-orange-50',
desc: '基于 OpenClaw 框架的 AI Agent 接口,支持会话管理、知识库召回、工作流触发、多模型切换。',
tags: ['OpenClaw', 'RAG', 'Function Call', '流式输出'],
},
{
emoji: '🔔',
title: 'Webhook 事件',
iconBg: 'bg-green-50',
desc: '订阅业务事件推送订单支付、用户注册、AI 任务完成等,实时触发您的业务逻辑。',
tags: ['事件订阅', '重试机制', '签名验证', '日志追踪'],
},
{
emoji: '🛡️',
title: '权限与安全',
iconBg: 'bg-rose-50',
desc: 'API Key 多级权限控制,支持 IP 白名单、请求频率限制、操作审计日志,保障接口安全。',
tags: ['API Key', 'IP 白名单', 'Rate Limit', '审计日志'],
},
]
const steps = [
{ emoji: '📝', title: '注册开发者账号', desc: '使用平台账号申请开发者权限,审核后即可获取 API Key' },
{ emoji: '🔑', title: '获取 API Key', desc: '在控制台生成专属 API Key支持多 Key 管理与权限分级' },
{ emoji: '📖', title: '阅读接入文档', desc: '参考快速开始指南,选择合适的 SDK 或直接调用 REST API' },
{ emoji: '🚀', title: '上线发布', desc: '完成联调测试,切换生产环境,享受 99.9% 可用性保障' },
]
const apiFeatures = [
'完整的 RESTful 设计,语义清晰',
'支持 JSON / multipart 请求格式',
'统一的错误码与响应格式',
'分页、过滤、排序参数规范',
'支持流式响应SSE',
'提供 Postman Collection 下载',
]
const sdkList = [
{ emoji: '🟦', lang: 'TypeScript / JavaScript', desc: '官方维护,完整类型定义,支持 Node.js 与浏览器端', status: '稳定版', statusClass: 'bg-green-500/20 text-green-400' },
{ emoji: '🐍', lang: 'Python', desc: '社区维护,支持 asyncio适合数据处理与 AI 场景', status: '稳定版', statusClass: 'bg-green-500/20 text-green-400' },
{ emoji: '☕', lang: 'Java', desc: '适用于企业级 Spring Boot 项目快速接入', status: 'Beta', statusClass: 'bg-yellow-500/20 text-yellow-400' },
{ emoji: '🐹', lang: 'Go', desc: '高性能微服务场景,轻量无依赖', status: 'Beta', statusClass: 'bg-yellow-500/20 text-yellow-400' },
{ emoji: '🐘', lang: 'PHP', desc: '适用于 Laravel / ThinkPHP 等框架', status: '规划中', statusClass: 'bg-slate-500/20 text-slate-400' },
]
const sourceItems = [
{ emoji: '🗂️', title: 'Nuxt 4 前端', desc: 'Vue 3 + TypeScript完整 SSR 源码', bg: 'from-indigo-50 to-blue-50' },
{ emoji: '⚙️', title: 'Java 后端', desc: 'Spring Boot 3多租户 SaaS 架构', bg: 'from-violet-50 to-purple-50' },
{ emoji: '🤖', title: 'AI 模块', desc: 'OpenClaw 集成RAG 知识库引擎', bg: 'from-orange-50 to-amber-50' },
{ emoji: '🚢', title: '部署脚本', desc: 'Docker Compose + CI/CD 流水线', bg: 'from-green-50 to-emerald-50' },
]
const sourcePoints = [
'完整的多租户 SaaS 源码',
'私有 Git 仓库,持续同步更新',
'无商用限制,可自由二次开发',
'提供详细的架构说明文档',
'支持私有化本地/云端部署',
]
const communities = [
{ emoji: '💬', title: '开发者论坛', desc: '提问、讨论、分享最佳实践,社区互助' },
{ emoji: '📘', title: '开发文档', desc: '完整 API 参考、教程、示例代码库' },
{ emoji: '🐙', title: 'GitHub', desc: '开源组件、SDK 源码、示例项目' },
{ emoji: '🎓', title: '视频教程', desc: '从入门到进阶的系列开发实战课程' },
]
</script>

View File

@@ -1,41 +0,0 @@
# 2026-03-30
## 应用配置页面修复
### 问题1: 页面内容贴边
- **现象**: 应用配置页面内容紧贴浏览器边缘
- **解决**: 给 `.app-config-page` 容器添加了 `padding: 24px`
### 问题2: 数据库加密错误
- **现象**: `InvalidKeyOrParametersException: Key length not 128/192/256 bits`
- **原因**: 后端 Hutool 加密库在处理敏感字段时出错
- **解决**:
- 移除了所有配置字段的 `secret: true` 标记
- 保存时设置 `isEncrypted``isSecret` 为 0
- 移除了"敏感信息,已加密存储"的 UI 提示
- **注意**: 后端加密密钥 `WLgNsWJ8rPjRtnjzX/Gx2RGS80Kwnm/ZeLbvIL+NrBs=` 长度为 32字节,符合要求,问题可能是 Base64 解码处理不当
### 问题3: 刷新页面 404
- **现象**: 访问 `/developer/config?websiteId=6268` 刷新后显示 "Page not found"
- **尝试的方案**:
1. 添加 `ssr: false` - 未解决
2. 使用 `ClientOnly` 包裹 - 未解决
3. 添加参数验证和自动跳转 - 未解决
4. 添加 `mounted` 标志延迟渲染 - 未解决
- **最终解决方案**:
- 将查询参数改为路由参数: `/developer/config?websiteId=6268``/developer/config/6268`
- 创建正确的文件结构: `/app/pages/developer/config/[id].vue`
- 修改参数读取逻辑: 支持 `route.params.id``route.query.websiteId` 两种方式
- 更新 AppDetail.vue 中的跳转链接
- **注意**: 修改文件结构后需要重启开发服务器
- **状态**: 已解决
### 问题4: 唯一键冲突错误
- **现象**: `Duplicate entry '6268-api.baseUrl-1' for key 'app_config.uk_website_key'`
- **原因**: 后端批量保存时先执行 `DELETE WHERE website_id = ?`,但数据库唯一键 `uk_website_key` 包含 `website_id``config_key`,软删除的记录 (deleted=1) 仍会导致唯一键冲突
- **解决**:
- 先加载配置列表建立 `configKey -> configId` 映射
- 保存时判断: 有 configId 则用 `updateAppConfig`,无则用 `saveAppConfig`
- 避免后端批量删除逻辑导致的唯一键冲突
- **状态**: 已解决

View File

@@ -1,126 +0,0 @@
# 2026-03-31 开发者侧功能完善工作日志
## 完成的工作
### 1. 首页统计卡片API接入 (index.vue)
- 创建了开发者API服务文件: `/app/api/developer/index.ts`
- 添加了`getDeveloperStats()` API调用
- 实现了首页统计数据的实时获取和显示
- 添加了数据刷新按钮和加载状态
- 统计卡片现在显示真实API数据而不是硬编码值
### 2. 数据统计页面API接入 (analytics.vue)
- 添加了`getAppAnalytics()``exportAnalyticsReport()` API调用
- 实现了完整的数据绑定:
- 核心指标卡片(总安装量、活跃用户、本月收入、平均评分)
- 安装量趋势图表(基于时间序列数据)
- 收入数据分类展示
- 应用排行(支持按安装量、收入、评分筛选)
- 用户活跃度分布DAU/WAU/MAU
- API调用统计和用量监控
- 添加了数据导出功能Excel格式
### 3. API Key管理功能完善 (apikeys.vue)
- 添加了完整的API Key CRUD接口
- `getApiKeys()` - 获取列表
- `createApiKey()` - 创建新Key
- `updateApiKeyStatus()` - 启用/禁用
- `deleteApiKey()` - 删除Key
- `getApiRateLimits()` - 获取速率限制信息
- 实现了完整的API Key生命周期管理
- 添加了安全的Key显示/隐藏功能
- 集成了速率限制信息显示
## 技术实现细节
- 所有API调用都使用统一的错误处理
- 添加了加载状态指示器
- 使用了响应式数据绑定
- 实现了日期格式化工具函数
- 保持了与现有UI设计的一致性
## 遇到的问题
1. **API接口路径需要确认** - 基于现有CMS统计API结构但需要确保后端已实现对应接口
2. **数据格式适配** - 部分页面需要根据实际API返回的数据结构进行适配
3. **错误处理** - 确保所有异步操作都有适当的错误处理和用户反馈
## 下一步计划
1. 完善权限申请记录功能 (`requests.vue`)
2. 完善版本管理功能 (`versions.vue`)
3. 完善其他辅助页面功能
## 今日继续完善的工作 (继续会话)
### 4. 权限申请记录功能完善 (requests.vue)
- 在API服务中添加了权限申请相关接口
- `getPermissionRequests()` - 获取申请列表
- `createPermissionRequest()` - 提交新申请
- `getPermissionRequestStats()` - 获取申请统计
- `getAvailableRepositories()` - 获取可申请仓库列表
- 实现了完整的申请列表加载和筛选功能
- 添加了状态统计卡片(全部、待审核、已通过、已拒绝)
- 实现了提交新申请表单,包含仓库选择、理由填写等功能
- 添加了加载状态管理、错误处理和成功反馈
- 集成了日期格式化显示和状态标签
### 5. 版本管理功能完善 (versions.vue)
- 在API服务中添加了版本管理相关接口
- `getAppVersions()` - 获取应用版本列表
- `publishAppVersion()` - 发布新版本(支持文件上传)
- `setCurrentVersion()` - 设置当前版本
- `rollbackVersion()` - 版本回滚
- `deleteAppVersion()` - 删除版本
- 实现了版本时间线显示,支持按类型筛选
- 添加了发布新版本功能,支持:
- 版本号格式验证(语义化版本号)
- 更新日志编辑(支持增删改)
- 安装包文件上传(.zip, .tar.gz最大100MB
- 是否设为当前版本选项
- 实现了版本回滚和设置当前版本功能
- 添加了文件大小格式化显示
- 集成了完整的加载状态和错误处理
### 6. Git账号绑定功能完善 (git.vue)
- 在API服务中添加了Git账号绑定接口
- `saveGitAccount()` - 保存绑定信息
- `getGitAccountStatus()` - 获取绑定状态
- `getGiteaServerInfo()` - 获取Gitea服务器信息
- 实现了绑定状态检查和自动填充表单
- 添加了状态标签显示(待审核、已通过、已拒绝、未绑定)
- 实现了审核状态详情显示
- 集成了动态Gitea服务器URL获取
- 添加了状态警告和成功提示信息
## 技术实现亮点
1. **文件上传处理**: 版本发布支持FormData格式文件上传包含文件类型和大小验证
2. **状态管理**: 所有页面都有统一的加载状态管理,防止并发操作
3. **错误处理**: 统一的错误处理机制,包含特定状态码的友好提示
4. **数据验证**: 表单验证、文件验证、版本号格式验证
5. **用户体验**: 统一的空状态、加载状态、成功反馈设计
## 完成状态
- ✅ 首页统计卡片API接入
- ✅ 数据统计页面API接入
- ✅ API Key管理功能完善
- ✅ 权限申请记录功能完善
- ✅ 版本管理功能完善
- ✅ Git账号绑定功能完善
- ⏳ 其他静态页面source.vue, support.vue, tutorial.vue主要功能完整无需API集成
## 标签错误修复 (2026-03-31)
- **问题**: 运行时报错 "Element is missing end tag"
- **文件**: `requests.vue`
- **原因**: 文件结尾有两个连续的 `</script>` 标签第337行有多余标签
- **修复**: 移除多余的第337行 `</script>` 标签
- **验证**:
- 所有Vue文件通过linter检查无语法错误
- Nuxt开发服务器正常启动无标签错误报告
- **状态**: ✅ 已完全修复
## 备注
- 所有修改都保持了向后兼容性
- 模拟数据仍然作为fallback保留
- 代码结构清晰,便于后续维护和扩展
## 虚假报错处理2026-03-31 晚)
**问题**`servers.vue``Identifier 'handleTableChange' has already been declared`
**根因**:文件实际只有一个 `handleTableChange` 定义,`read_lints` 验证 0 错误。报错来自 **IDE/Vue Language Server 的旧缓存**,代码本身正确。
**解决方案**:重启 Volar`Cmd+Shift+P``Volar: Restart Vue Language Server`),无需修改代码。

View File

@@ -1,102 +0,0 @@
# 开发者中心页面规划
## 规划概述
已在之前对话中完成对 `/developer` 开发者中心页面的完整分析。
## 页面结构13个页面
| 页面 | 文件 | 功能定位 |
|------|------|----------|
| 概览 | `index.vue` | 首页仪表盘 ✅ |
| 应用管理 | `apps.vue` | 创建/管理企业应用 ✅ |
| 发布管理 | `publish.vue` | 应用上架审核 ✅ |
| API Key | `apikeys.vue` | API密钥管理 ✅ |
| 源码与仓库 | `source.vue` | 仓库权限流程 ✅ |
| Git绑定 | `git.vue` | Gitea账号绑定 ✅ |
| 权限申请 | `requests.vue` | 仓库访问申请 ✅ |
| 版本管理 | `versions.vue` | 应用版本发布 ✅ |
| 数据统计 | `analytics.vue` | 数据分析面板 ✅ |
| 支持与反馈 | `support.vue` | 工单/客服 ✅ |
| 开发教程 | `tutorial.vue` | 文档/教程 ✅ |
| 工单系统 | `tickets.vue` | 工单列表 ✅ |
| 应用配置 | `config/[id].vue` | 单个应用配置 ⚠️ |
## 已完成功能增强
1. **统一UI/UX规范** - 所有页面使用统一的 stat-card、panel、page-header 样式
2. **首页(index.vue)** - 已包含欢迎横幅、统计数据、快捷入口、开发者公告、快速帮助、SDK状态
3. **应用管理(apps.vue)** - 完整的创建应用功能和列表展示
4. **数据统计(analytics.vue)** - 核心指标、安装量趋势、收入概览、应用排行、用户活跃、API统计
5. **页面间导航** - 使用统一的导航组件
## 导航分组建议
```
├── 📊 概览 (/developer)
├── 📦 应用
│ ├── 应用管理 (apps)
│ ├── 发布管理 (publish)
│ └── 版本管理 (versions)
├── 🔑 开发资源
│ ├── API Key (apikeys)
│ └── 数据统计 (analytics)
├── 💻 源码
│ ├── 源码与仓库 (source)
│ ├── Git绑定 (git)
│ └── 权限申请 (requests)
└── 🆘 支持
├── 支持与反馈 (support)
├── 工单系统 (tickets)
└── 开发教程 (tutorial)
```
## 资源中心页面resources/
| 页面 | 文件 |
|------|------|
| 资源总览 | `resources/index.vue` |
| 服务器 | `resources/servers.vue` |
| 数据库 | `resources/databases.vue` |
| 云存储 | `resources/storage.vue` |
| 域名管理 | `resources/domains.vue` |
| SSL 证书 | `resources/ssl.vue` |
**注意**resources/ 子页面全部需要 `definePageMeta({ layout: 'developer' })` 才能显示左侧菜单(已修复)。
## Bug 记录
- **2026-03-31 修复**`resources/` 下所有子页面index/servers/databases/storage/domains/ssl缺少 `definePageMeta({ layout: 'developer' })`导致没有左侧导航菜单域名和SSL页面也因此无法正常访问。已全部补齐。
- **2026-03-31 修复**`layouts/developer.vue``isActive()` 使用 `startsWith` 导致父路径 `/developer/resources` 在访问子页面时被同时高亮。改为 `path === to || path.startsWith(to + '/')` 精确匹配。
## 当前状态
所有页面(含 resources/ 6个子页面均已完成开发功能完善layout 正确。
## 资源中心后端 API 接入2026-03-31
所有 resources/ 页面已完整对接后端,使用统一的 `app/appResource` 模块:
| 文件 | resourceType | API 接入 |
|------|-------------|---------|
| `resources/index.vue` | - | `statsAppResource()` + `pageAppResource()` 展示统计与最近资源 |
| `resources/servers.vue` | `server` | CRUD 全部对接 |
| `resources/databases.vue` | `database` | CRUD 全部对接 |
| `resources/storage.vue` | `storage` | CRUD 全部对接 |
| `resources/domains.vue` | `domain` | CRUD 全部对接 |
| `resources/ssl.vue` | `ssl` | CRUD 全部对接 |
**API 位置**`app/api/app/appResource/index.ts`
**后端路径**`/api/app/app//developer-resource/...`
**分页参数**`page` + `limit`(继承自 PageParam
**分页结果**`result.list` + `result.count`
**主键字段**`resourceId`(非 `id`
**通用 Model**`AppResource``resourceType` 字段区分类型
同时修复了所有页面:
- `row-key``"id"` 改为 `"resourceId"`
- `popconfirm` 参数从 `record.id` 改为 `record.resourceId`
- 去除重复的 `definePageMeta/useHead`
## 更新日期
2026-03-31

View File

@@ -1,979 +0,0 @@
<template>
<div class="dev-page">
<!-- 顶部横幅 -->
<div class="analytics-hero">
<div class="analytics-hero-content">
<h1 class="analytics-hero-title">📊 数据统计</h1>
<p class="analytics-hero-desc">实时监控应用安装量用户活跃收入趋势等核心指标</p>
</div>
</div>
<div class="analytics-body">
<!-- 时间筛选 -->
<div class="filter-bar">
<a-segmented
v-model:value="dateRange"
:options="dateRangeOptions"
@change="onDateRangeChange"
/>
<a-space class="ml-auto">
<a-button :loading="loading" @click="refresh">刷新</a-button>
<a-button @click="exportReport">导出报告</a-button>
</a-space>
</div>
<!-- 核心指标卡片 -->
<a-row :gutter="[16, 16]" class="mb-6">
<a-col :xs="12" :md="6" v-for="metric in coreMetrics" :key="metric.label">
<div class="metric-card" :class="metric.color">
<div class="metric-header">
<span class="metric-icon">{{ metric.icon }}</span>
<a-tooltip :title="metric.tooltip">
<span class="metric-trend" :class="metric.trendDir">
{{ metric.trendDir === 'up' ? '↑' : metric.trendDir === 'down' ? '↓' : '-' }}
{{ metric.trend }}
</span>
</a-tooltip>
</div>
<div class="metric-value">{{ metric.value }}</div>
<div class="metric-label">{{ metric.label }}</div>
</div>
</a-col>
</a-row>
<a-row :gutter="[16, 16]">
<!-- 安装量趋势 -->
<a-col :xs="24" :lg="16">
<div class="panel">
<div class="panel-header">
<span class="panel-title">📈 安装量趋势</span>
<a-radio-group v-model:value="installChartType" size="small">
<a-radio-button value="line">折线图</a-radio-button>
<a-radio-button value="bar">柱状图</a-radio-button>
</a-radio-group>
</div>
<div class="chart-area">
<div class="chart-placeholder">
<div class="chart-bar-group">
<div
v-for="(bar, i) in installData"
:key="i"
class="chart-bar-col"
>
<div
class="chart-bar"
:style="{ height: bar.percent + '%' }"
:class="{ active: bar.highlight }"
/>
<span class="chart-bar-label">{{ bar.label }}</span>
</div>
</div>
<div class="chart-legend">
<span class="legend-dot green" /> 新安装
<span class="legend-dot blue ml-4" /> 累计安装
</div>
</div>
</div>
</div>
</a-col>
<!-- 收入概览 -->
<a-col :xs="24" :lg="8">
<div class="panel">
<div class="panel-header">
<span class="panel-title">💰 收入概览</span>
<a-tag color="green">本月</a-tag>
</div>
<div class="revenue-content">
<div class="revenue-total">
<div class="revenue-amount">¥{{ revenueData.total }}</div>
<div class="revenue-label">本月总收入</div>
</div>
<a-divider style="margin: 16px 0" />
<div class="revenue-items">
<div v-for="item in revenueData.items" :key="item.label" class="revenue-item">
<div class="revenue-item-left">
<span class="revenue-item-icon">{{ item.icon }}</span>
<span class="revenue-item-label">{{ item.label }}</span>
</div>
<div class="revenue-item-value">
¥{{ item.value }}
<span class="revenue-item-trend" :class="item.trendDir">
{{ item.trend }}
</span>
</div>
</div>
</div>
</div>
</div>
</a-col>
</a-row>
<a-row :gutter="[16, 16]" class="mt-4">
<!-- 应用排行 -->
<a-col :xs="24" :lg="12">
<div class="panel">
<div class="panel-header">
<span class="panel-title">🏆 应用排行</span>
<a-select
v-model:value="rankType"
size="small"
style="min-width: 120px"
@change="refresh"
>
<a-select-option value="installs">按安装量</a-select-option>
<a-select-option value="revenue">按收入</a-select-option>
<a-select-option value="rating">按评分</a-select-option>
</a-select>
</div>
<div class="rank-list">
<div v-for="(app, index) in topApps" :key="app.name" class="rank-item">
<div class="rank-pos" :class="{ top3: index < 3 }">{{ index + 1 }}</div>
<div class="rank-info">
<div class="rank-icon">{{ app.icon }}</div>
<div class="rank-detail">
<div class="rank-name">{{ app.name }}</div>
<div class="rank-desc">{{ app.desc }}</div>
</div>
</div>
<div class="rank-value">{{ app.value }}</div>
</div>
</div>
</div>
</a-col>
<!-- 用户活跃分布 -->
<a-col :xs="24" :lg="12">
<div class="panel">
<div class="panel-header">
<span class="panel-title">👥 用户活跃</span>
</div>
<div class="activity-content">
<!-- 活跃度概要 -->
<div class="activity-summary">
<div class="activity-item">
<div class="activity-value blue">{{ activityData.dau }}</div>
<div class="activity-label">日活跃 (DAU)</div>
</div>
<div class="activity-item">
<div class="activity-value green">{{ activityData.wau }}</div>
<div class="activity-label">周活跃 (WAU)</div>
</div>
<div class="activity-item">
<div class="activity-value purple">{{ activityData.mau }}</div>
<div class="activity-label">月活跃 (MAU)</div>
</div>
</div>
<a-divider style="margin: 12px 0" />
<!-- 活跃度分布 -->
<div class="activity-dist">
<div class="dist-title">活跃度分布</div>
<div v-for="bar in activityDist" :key="bar.label" class="dist-row">
<span class="dist-label">{{ bar.label }}</span>
<div class="dist-bar-wrap">
<div
class="dist-bar"
:style="{ width: bar.percent + '%', background: bar.color }"
/>
</div>
<span class="dist-value">{{ bar.value }}</span>
</div>
</div>
</div>
</div>
</a-col>
</a-row>
<!-- API 调用统计 -->
<div class="panel mt-4">
<div class="panel-header">
<span class="panel-title"> API 调用统计</span>
<a-space>
<a-tag color="blue">近7天</a-tag>
<a-button size="small" type="link">查看详情</a-button>
</a-space>
</div>
<div class="api-stats">
<a-row :gutter="[16, 16]">
<a-col :xs="24" :sm="8" v-for="apiStat in apiStats" :key="apiStat.label">
<div class="api-stat-card">
<div class="api-stat-header">
<span class="api-stat-label">{{ apiStat.label }}</span>
<a-tag :color="apiStat.tagColor" size="small">{{ apiStat.status }}</a-tag>
</div>
<div class="api-stat-value">{{ apiStat.value }}</div>
<div class="api-stat-bar-wrap">
<div class="api-stat-bar" :style="{ width: apiStat.usage + '%', background: apiStat.barColor }" />
</div>
<div class="api-stat-footer">
<span>已用 {{ apiStat.usage }}%</span>
<span>限额 {{ apiStat.limit }}</span>
</div>
</div>
</a-col>
</a-row>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
// TODO: 后端接口就绪后解除注释
// import { getAppAnalytics, exportAnalyticsReport } from '@/api/developer'
import { message } from 'ant-design-vue'
definePageMeta({ layout: 'developer' })
useHead({ title: '数据统计 - 开发者中心' })
const loading = ref(false)
const dateRange = ref('7d')
const installChartType = ref('line')
const rankType = ref('installs')
// ========== Mock 数据(后端接口就绪后替换为真实 API 调用) ==========
const MOCK_ANALYTICS = {
totalInstalls: 128460,
activeUsers: 38920,
monthlyRevenue: 156780.50,
averageRating: 4.7,
installTrend: [
{ date: '03/28', newInstalls: 820, cumulativeInstalls: 124500 },
{ date: '03/29', newInstalls: 960, cumulativeInstalls: 125460 },
{ date: '03/30', newInstalls: 1100, cumulativeInstalls: 126560 },
{ date: '03/31', newInstalls: 750, cumulativeInstalls: 127310 },
{ date: '04/01', newInstalls: 680, cumulativeInstalls: 127990 },
{ date: '04/02', newInstalls: 920, cumulativeInstalls: 128910 },
{ date: '04/03', newInstalls: 550, cumulativeInstalls: 128460 },
] as Array<{ date: string; newInstalls: number; cumulativeInstalls: number }>,
revenueBreakdown: [
{ type: 'subscription', label: '订阅收入', value: 98400, trend: 18.5 },
{ type: 'one-time', label: '一次性购买', value: 32600, trend: 5.2 },
{ type: 'addon', label: '增值服务', value: 15800, trend: 32.1 },
{ type: 'license', label: '授权许可', value: 9980.50, trend: -3.4 },
] as Array<{ type: string; label: string; value: number; trend: number }>,
topApps: [
{ name: '企业官网建站', description: '一站式企业官网生成平台', installs: 45800, revenue: 52000, rating: 4.8 },
{ name: '电商系统', description: '全功能电商解决方案', installs: 32100, revenue: 45000, rating: 4.6 },
{ name: '小程序助手', description: '微信小程序快速开发工具', installs: 28900, revenue: 38000, rating: 4.9 },
{ name: 'AI客服机器人', description: '智能客服对话系统', installs: 15600, revenue: 21500, rating: 4.5 },
{ name: '数据分析平台', description: '业务数据可视化分析', installs: 6060, revenue: 12800, rating: 4.7 },
] as Array<{ name: string; description: string; installs: number; revenue: number; rating: number }>,
userActivity: {
dau: 5830,
wau: 18200,
mau: 38920,
activityDistribution: [
{ level: 'daily', label: '每日活跃', value: 5830, percentage: 15 },
{ level: 'weekly', label: '每周活跃', value: 12370, percentage: 32 },
{ level: 'monthly', label: '每月活跃', value: 20720, percentage: 53 },
] as Array<{ level: string; label: string; value: number; percentage: number }>,
},
apiUsage: [
{ apiName: 'app-list', label: '应用列表 API', calls: 156800, usagePercentage: 52, dailyLimit: 300000, status: '正常' as const },
{ apiName: 'user-auth', label: '用户认证 API', calls: 89200, usagePercentage: 75, dailyLimit: 120000, status: '警告' as const },
{ apiName: 'data-query', label: '数据查询 API', calls: 210500, usagePercentage: 88, dailyLimit: 240000, status: '警告' as const },
] as Array<{ apiName: string; label: string; calls: number; usagePercentage: number; dailyLimit: number; status: '正常' | '警告' | '超限' }>,
}
// API 数据状态
const analyticsData = ref({ ...MOCK_ANALYTICS })
const dateRangeOptions = [
{ label: '近7天', value: '7d' },
{ label: '近30天', value: '30d' },
{ label: '近90天', value: '90d' },
{ label: '全部', value: 'all' },
]
// 核心指标 - 使用API数据
const coreMetrics = computed(() => {
const formatNumber = (num: number) => {
if (num >= 1000000) return (num / 1000000).toFixed(1) + 'M'
if (num >= 1000) return (num / 1000).toFixed(1) + 'K'
return num.toString()
}
const formatCurrency = (amount: number) => {
return '¥' + amount.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })
}
return [
{
icon: '📦',
label: '总安装量',
value: formatNumber(analyticsData.value.totalInstalls),
trend: '+12.5%', trendDir: 'up', color: 'blue',
tooltip: '所有应用的累计安装次数'
},
{
icon: '👥',
label: '活跃用户',
value: formatNumber(analyticsData.value.activeUsers),
trend: '+8.3%', trendDir: 'up', color: 'purple',
tooltip: '近30天活跃用户数'
},
{
icon: '💰',
label: '本月收入',
value: formatCurrency(analyticsData.value.monthlyRevenue),
trend: '+23.1%', trendDir: 'up', color: 'green',
tooltip: '本月累计收入'
},
{
icon: '⭐',
label: '平均评分',
value: analyticsData.value.averageRating.toFixed(1),
trend: '+0.2', trendDir: 'up', color: 'orange',
tooltip: '所有应用的平均评分'
},
]
})
// 安装量趋势 - 使用API数据
const installData = computed(() => {
const trendData = analyticsData.value.installTrend
if (!trendData.length) {
return []
}
// 计算最大安装量用于百分比
const maxCumulative = Math.max(...trendData.map(item => item.cumulativeInstalls))
const maxNew = Math.max(...trendData.map(item => item.newInstalls))
const maxValue = Math.max(maxCumulative, maxNew)
return trendData.map((item, index) => {
// 使用新安装量作为图表高度
const percent = maxValue > 0 ? (item.newInstalls / maxValue) * 100 : 0
return {
label: item.date,
percent: Math.min(100, Math.max(5, percent)), // 确保在5-100%范围内
highlight: index === trendData.length - 1,
newInstalls: item.newInstalls,
cumulativeInstalls: item.cumulativeInstalls
}
})
})
// 收入数据 - 使用API数据
const revenueData = computed(() => {
const formatCurrency = (amount: number) => {
return amount.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })
}
const getIcon = (type: string) => {
const icons: Record<string, string> = {
'subscription': '🛒',
'one-time': '一次性',
'addon': '🎁',
'license': '📄',
'support': '🆘'
}
return icons[type] || '💰'
}
const breakdown = analyticsData.value.revenueBreakdown
const total = breakdown.reduce((sum, item) => sum + item.value, 0)
return {
total: formatCurrency(total),
items: breakdown.map(item => ({
icon: getIcon(item.type),
label: item.label,
value: formatCurrency(item.value),
trend: item.trend > 0 ? `+${item.trend}%` : `${item.trend}%`,
trendDir: item.trend >= 0 ? 'up' : 'down' as 'up' | 'down'
}))
}
})
// 应用排行 - 使用API数据
const topApps = computed(() => {
const apps = analyticsData.value.topApps
const getDisplayValue = (app: typeof apps[0]) => {
if (rankType.value === 'installs') return `${app.installs}`
if (rankType.value === 'revenue') return `¥${app.revenue.toLocaleString()}`
if (rankType.value === 'rating') return app.rating.toFixed(1)
return `${app.installs}`
}
const getIcon = (name: string) => {
const icons: Record<string, string> = {
'企业官网': '🌐',
'电商': '🛒',
'小程序': '📱',
'AI': '🤖',
'数据': '📊',
'官网': '🌐',
'电商系统': '🛒',
'小程序助手': '📱',
'AI客服': '🤖',
'数据分析': '📊'
}
for (const [key, icon] of Object.entries(icons)) {
if (name.includes(key)) return icon
}
return '📦'
}
return apps.map(app => ({
icon: getIcon(app.name),
name: app.name,
desc: app.description,
value: getDisplayValue(app)
}))
})
// 用户活跃 - 使用API数据
const activityData = computed(() => ({
dau: analyticsData.value.userActivity.dau.toLocaleString(),
wau: analyticsData.value.userActivity.wau.toLocaleString(),
mau: analyticsData.value.userActivity.mau.toLocaleString(),
}))
const activityDist = computed(() => {
return analyticsData.value.userActivity.activityDistribution.map(item => ({
label: item.label,
value: `${item.percentage}%`,
percent: item.percentage,
color: getDistributionColor(item.level)
}))
})
// 根据活跃度级别获取颜色
function getDistributionColor(level: string) {
const colors: Record<string, string> = {
'high': '#4f46e5', // 高活跃
'medium': '#8b5cf6', // 中活跃
'low': '#a78bfa', // 低活跃
'inactive': '#e5e7eb', // 沉默
'daily': '#4f46e5', // 每日活跃
'weekly': '#8b5cf6', // 每周活跃
'monthly': '#a78bfa', // 每月活跃
'silent': '#e5e7eb' // 沉默用户
}
return colors[level] || '#e5e7eb'
}
// API 调用统计 - 使用API数据
const apiStats = computed(() => {
return analyticsData.value.apiUsage.map(item => {
let tagColor = 'green'
let statusText = '正常'
if (item.status === '警告') {
tagColor = 'orange'
statusText = '警告'
} else if (item.status === '超限') {
tagColor = 'red'
statusText = '超限'
}
return {
label: item.label,
value: `${item.calls.toLocaleString()}`,
usage: item.usagePercentage,
limit: `${item.dailyLimit.toLocaleString()}/日`,
status: statusText,
tagColor,
barColor: getApiBarColor(item.usagePercentage)
}
})
})
// 根据使用率获取进度条颜色
function getApiBarColor(usage: number) {
if (usage < 60) return '#22c55e' // 绿色:正常
if (usage < 85) return '#f59e0b' // 黄色:警告
return '#ef4444' // 红色:超限
}
// 加载统计数据TODO: 后端接口就绪后替换 Mock 为真实 API 调用)
// async function loadAnalyticsData() {
// loading.value = true
// try {
// const response = await getAppAnalytics({ dateRange: dateRange.value })
// if (response.data?.success) {
// analyticsData.value = response.data.data
// }
// } catch (error) {
// console.error('加载统计数据失败:', error)
// message.error('加载统计数据失败,请稍后重试')
// } finally {
// loading.value = false
// }
// }
function onDateRangeChange() {
// TODO: 后端接口就绪后调用 loadAnalyticsData()
}
async function refresh() {
loading.value = true
// TODO: 后端接口就绪后替换为 loadAnalyticsData()
setTimeout(() => {
analyticsData.value = { ...MOCK_ANALYTICS }
loading.value = false
message.success('数据已刷新')
}, 300)
}
async function exportReport() {
message.info('导出功能将在后端接口就绪后开放')
}
// 页面无需初始化加载,直接使用 Mock 数据渲染
</script>
<style scoped>
.dev-page {
min-height: 100%;
}
/* Hero 区域 */
.analytics-hero {
background: linear-gradient(135deg, #1e1b4b 0%, #312e81 40%, #1e3a5f 100%);
padding: 28px 28px 24px;
position: relative;
overflow: hidden;
}
.analytics-hero::before {
content: '';
position: absolute;
top: -80px;
right: -80px;
width: 300px;
height: 300px;
border-radius: 50%;
background: rgba(99, 102, 241, 0.2);
filter: blur(60px);
}
.analytics-hero-content {
position: relative;
z-index: 1;
}
.analytics-hero-title {
color: #fff;
font-size: 24px;
font-weight: 700;
margin: 0 0 6px;
}
.analytics-hero-desc {
color: rgba(199, 210, 254, 0.8);
font-size: 14px;
margin: 0;
}
/* 主体内容 */
.analytics-body {
padding: 20px 24px 24px;
}
/* 筛选栏 */
.filter-bar {
display: flex;
align-items: center;
margin-bottom: 20px;
}
/* 指标卡片 */
.metric-card {
padding: 18px;
border-radius: 12px;
border: 1px solid transparent;
transition: all 0.2s;
}
.metric-card:hover { transform: translateY(-1px); }
.metric-card.blue { background: #eff6ff; border-color: #dbeafe; }
.metric-card.purple { background: #f5f3ff; border-color: #e9d5ff; }
.metric-card.green { background: #f0fdf4; border-color: #bbf7d0; }
.metric-card.orange { background: #fff7ed; border-color: #fed7aa; }
.metric-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 8px;
}
.metric-icon { font-size: 24px; }
.metric-trend {
font-size: 12px;
font-weight: 500;
padding: 2px 6px;
border-radius: 4px;
}
.metric-trend.up { color: #16a34a; background: #dcfce7; }
.metric-trend.down { color: #dc2626; background: #fef2f2; }
.metric-value {
font-size: 24px;
font-weight: 700;
color: rgba(0, 0, 0, 0.85);
line-height: 1.2;
}
.metric-label {
font-size: 12px;
color: rgba(0, 0, 0, 0.45);
margin-top: 4px;
}
/* 面板通用 */
.panel {
background: #fff;
border: 1px solid #f0f0f0;
border-radius: 12px;
overflow: hidden;
}
.panel-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 14px 18px;
border-bottom: 1px solid #f5f5f5;
}
.panel-title {
font-size: 14px;
font-weight: 600;
color: rgba(0, 0, 0, 0.85);
}
/* 图表区域 */
.chart-area {
padding: 20px;
}
.chart-bar-group {
display: flex;
align-items: flex-end;
gap: 12px;
height: 200px;
padding-top: 10px;
}
.chart-bar-col {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
height: 100%;
justify-content: flex-end;
}
.chart-bar {
width: 100%;
max-width: 48px;
border-radius: 6px 6px 0 0;
background: linear-gradient(180deg, #6366f1, #4f46e5);
min-height: 8px;
transition: all 0.3s;
cursor: pointer;
}
.chart-bar:hover,
.chart-bar.active {
background: linear-gradient(180deg, #818cf8, #6366f1);
box-shadow: 0 -4px 12px rgba(99, 102, 241, 0.3);
}
.chart-bar-label {
font-size: 11px;
color: rgba(0, 0, 0, 0.4);
margin-top: 8px;
}
.chart-legend {
display: flex;
align-items: center;
gap: 4px;
margin-top: 16px;
font-size: 12px;
color: rgba(0, 0, 0, 0.5);
justify-content: center;
}
.legend-dot {
display: inline-block;
width: 10px;
height: 10px;
border-radius: 2px;
margin-right: 4px;
}
.legend-dot.green { background: #22c55e; }
.legend-dot.blue { background: #4f46e5; }
/* 收入 */
.revenue-content {
padding: 20px;
}
.revenue-total { text-align: center; padding: 4px 0; }
.revenue-amount {
font-size: 32px;
font-weight: 700;
color: rgba(0, 0, 0, 0.88);
}
.revenue-label {
font-size: 13px;
color: rgba(0, 0, 0, 0.4);
margin-top: 4px;
}
.revenue-items { padding: 0; }
.revenue-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px 0;
}
.revenue-item-left {
display: flex;
align-items: center;
gap: 8px;
}
.revenue-item-icon { font-size: 18px; }
.revenue-item-label {
font-size: 13px;
color: rgba(0, 0, 0, 0.65);
}
.revenue-item-value {
font-size: 14px;
font-weight: 600;
color: rgba(0, 0, 0, 0.85);
}
.revenue-item-trend {
font-size: 11px;
margin-left: 6px;
padding: 1px 4px;
border-radius: 3px;
}
.revenue-item-trend.up { color: #16a34a; background: #dcfce7; }
.revenue-item-trend.down { color: #dc2626; background: #fef2f2; }
/* 排行 */
.rank-list { padding: 8px 14px; }
.rank-item {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 0;
border-bottom: 1px solid #f9f9f9;
}
.rank-item:last-child { border-bottom: none; }
.rank-pos {
width: 28px;
height: 28px;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
font-size: 13px;
font-weight: 600;
color: rgba(0, 0, 0, 0.4);
background: #f5f5f5;
flex-shrink: 0;
}
.rank-pos.top3 {
background: linear-gradient(135deg, #f59e0b, #f97316);
color: #fff;
}
.rank-info {
flex: 1;
display: flex;
align-items: center;
gap: 10px;
min-width: 0;
}
.rank-icon { font-size: 22px; flex-shrink: 0; }
.rank-detail { min-width: 0; }
.rank-name {
font-size: 14px;
font-weight: 500;
color: rgba(0, 0, 0, 0.85);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.rank-desc {
font-size: 12px;
color: rgba(0, 0, 0, 0.4);
margin-top: 2px;
}
.rank-value {
font-size: 14px;
font-weight: 600;
color: #4f46e5;
flex-shrink: 0;
}
/* 活跃度 */
.activity-content { padding: 20px; }
.activity-summary {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
gap: 12px;
text-align: center;
}
.activity-value {
font-size: 22px;
font-weight: 700;
line-height: 1.2;
}
.activity-value.blue { color: #3b82f6; }
.activity-value.green { color: #22c55e; }
.activity-value.purple { color: #8b5cf6; }
.activity-label {
font-size: 12px;
color: rgba(0, 0, 0, 0.4);
margin-top: 4px;
}
.activity-dist { padding: 0; }
.dist-title {
font-size: 13px;
font-weight: 500;
color: rgba(0, 0, 0, 0.65);
margin-bottom: 12px;
}
.dist-row {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 10px;
}
.dist-label {
font-size: 12px;
color: rgba(0, 0, 0, 0.5);
width: 100px;
flex-shrink: 0;
text-align: right;
}
.dist-bar-wrap {
flex: 1;
height: 8px;
background: #f5f5f5;
border-radius: 4px;
overflow: hidden;
}
.dist-bar {
height: 100%;
border-radius: 4px;
transition: width 0.3s;
}
.dist-value {
font-size: 12px;
font-weight: 500;
color: rgba(0, 0, 0, 0.65);
width: 36px;
flex-shrink: 0;
}
/* API 统计 */
.api-stats { padding: 16px; }
.api-stat-card {
padding: 16px;
border-radius: 10px;
border: 1px solid #f0f0f0;
background: #fafafa;
transition: all 0.15s;
}
.api-stat-card:hover {
border-color: #e0e7ff;
background: #f5f7ff;
}
.api-stat-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 8px;
}
.api-stat-label {
font-size: 13px;
font-weight: 500;
color: rgba(0, 0, 0, 0.65);
}
.api-stat-value {
font-size: 20px;
font-weight: 700;
color: rgba(0, 0, 0, 0.85);
margin-bottom: 10px;
}
.api-stat-bar-wrap {
height: 6px;
background: #f0f0f0;
border-radius: 3px;
overflow: hidden;
margin-bottom: 6px;
}
.api-stat-bar {
height: 100%;
border-radius: 3px;
transition: width 0.3s;
}
.api-stat-footer {
display: flex;
justify-content: space-between;
font-size: 11px;
color: rgba(0, 0, 0, 0.4);
}
@media (max-width: 768px) {
.activity-summary {
grid-template-columns: 1fr;
gap: 8px;
}
.dist-label { width: 80px; }
}
</style>

View File

@@ -1,675 +0,0 @@
<template>
<div class="dev-page">
<!-- 页面头部 -->
<div class="page-header">
<div>
<h2 class="page-title">🔑 API Key 管理</h2>
<p class="page-desc">创建和管理你的 API Key用于调用平台 REST API SDK</p>
</div>
<a-button type="primary" @click="showCreateModal = true">
+ 创建 API Key
</a-button>
</div>
<div class="page-body">
<!-- 使用提示 -->
<a-alert
class="mb-5"
show-icon
type="info"
message="安全提示"
description="API Key 具有完整的账号访问权限,请勿在前端代码中明文使用,建议通过服务端调用 API。创建后请妥善保管丢失后需重新生成。"
/>
<!-- API Key 列表 -->
<div class="panel">
<div class="panel-header">
<span class="panel-title">我的 API Key</span>
<a-tag color="blue">{{ keyList.length }} </a-tag>
</div>
<a-spin :spinning="loading">
<div v-if="keyList.length === 0 && !loading" class="empty-state">
<div class="empty-icon">🔑</div>
<div class="empty-title">还没有 API Key</div>
<div class="empty-desc">创建第一个 API Key开始调用平台接口</div>
<a-button type="primary" class="mt-4" @click="showCreateModal = true">创建 API Key</a-button>
</div>
<div v-else-if="keyList.length > 0" class="key-list">
<div v-for="key in keyList" :key="key.id" class="key-item">
<div class="key-item-main">
<div class="key-name-row">
<span class="key-name">{{ key.name }}</span>
<a-tag :color="key.status === 'active' ? 'green' : 'default'">
{{ key.status === 'active' ? '正常' : '已禁用' }}
</a-tag>
</div>
<div class="key-value-row">
<code class="key-value">{{ key.visible ? key.value : maskKey(key.value) }}</code>
<a-tooltip title="显示/隐藏">
<a-button
type="text"
size="small"
class="key-action-btn"
@click="toggleVisible(key)"
>{{ key.visible ? '🙈' : '👁️' }}</a-button>
</a-tooltip>
<a-tooltip title="复制">
<a-button
type="text"
size="small"
class="key-action-btn"
@click="copyKey(key.value)"
>📋</a-button>
</a-tooltip>
</div>
<div class="key-meta">
<span>📅 创建于 {{ key.createdAt }}</span>
<span> 最近使用{{ key.lastUsed }}</span>
<span v-if="key.expireAt">🕐 到期{{ key.expireAt }}</span>
<span v-else> 永不过期</span>
</div>
</div>
<div class="key-item-actions">
<a-button
size="small"
:type="key.status === 'active' ? 'default' : 'primary'"
@click="toggleStatus(key)"
>
{{ key.status === 'active' ? '禁用' : '启用' }}
</a-button>
<a-popconfirm
title="确认删除该 API Key此操作不可撤销。"
ok-text="删除"
ok-type="danger"
cancel-text="取消"
@confirm="deleteKey(key.id)"
>
<a-button size="small" danger>删除</a-button>
</a-popconfirm>
</div>
</div>
</div>
</a-spin>
</div>
<!-- API 接入示例 -->
<div class="panel mt-4">
<div class="panel-header">
<span class="panel-title">📘 接入示例</span>
<a-radio-group v-model:value="activeTab" size="small" button-style="solid">
<a-radio-button v-for="tab in codeTabs" :key="tab.key" :value="tab.key">{{ tab.label }}</a-radio-button>
</a-radio-group>
</div>
<div class="code-example">
<div class="code-toolbar">
<span class="code-lang">{{ currentTab?.lang }}</span>
<a-button type="text" size="small" @click="copyCode">📋 复制代码</a-button>
</div>
<pre class="code-pre"><code>{{ currentTab?.code }}</code></pre>
</div>
</div>
<!-- 速率限制说明 -->
<div class="panel mt-4">
<div class="panel-header">
<span class="panel-title"> 速率限制</span>
</div>
<div class="rate-grid">
<div v-for="rate in rateLimits" :key="rate.plan" class="rate-card" :class="rate.highlight ? 'highlight' : ''">
<div class="rate-plan">{{ rate.plan }}</div>
<div class="rate-value">{{ rate.rps }} <span class="rate-unit">/</span></div>
<div class="rate-daily">日限 {{ rate.daily }}</div>
</div>
</div>
</div>
</div>
<!-- 创建 API Key 弹窗 -->
<a-modal
v-model:open="showCreateModal"
title="创建 API Key"
ok-text="创建"
cancel-text="取消"
:confirm-loading="creating"
@ok="handleCreate"
>
<a-form layout="vertical" class="mt-2">
<a-form-item label="名称" required>
<a-input
v-model:value="createForm.name"
placeholder="例如:生产环境、测试 Key..."
:maxlength="50"
show-count
/>
</a-form-item>
<a-form-item label="有效期">
<a-radio-group v-model:value="createForm.expire">
<a-radio value="">永不过期</a-radio>
<a-radio value="30d">30 </a-radio>
<a-radio value="90d">90 </a-radio>
<a-radio value="1y">1 </a-radio>
</a-radio-group>
</a-form-item>
<a-form-item label="权限范围">
<a-checkbox-group v-model:value="createForm.scopes">
<a-checkbox value="read">读取GET</a-checkbox>
<a-checkbox value="write">写入POST/PUT</a-checkbox>
<a-checkbox value="delete">删除DELETE</a-checkbox>
<a-checkbox value="ai">AI 接口</a-checkbox>
</a-checkbox-group>
</a-form-item>
<a-form-item label="备注">
<a-textarea
v-model:value="createForm.remark"
:rows="2"
placeholder="可选,用途说明"
/>
</a-form-item>
</a-form>
</a-modal>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { message } from 'ant-design-vue'
import {
pageAppApiKey,
createAppApiKey,
updateAppApiKeyStatus,
removeAppApiKey,
getApiRateLimits
} from '@/api/app/apikey'
definePageMeta({ layout: 'developer' })
useHead({ title: 'API Key 管理 - 开发者中心' })
const showCreateModal = ref(false)
const activeTab = ref('ts')
const loading = ref(false)
const creating = ref(false)
const createForm = reactive({
name: '',
expire: '',
scopes: ['read', 'write'],
remark: '',
})
// API Key 列表
const keyList = ref<Array<{
id: string
name: string
value: string
status: 'active' | 'disabled' | 'expired'
createdAt: string
lastUsed: string
expireAt: string
visible: boolean
usageCount?: number
scopes?: string[]
remark?: string
}>>([])
// 速率限制信息
const rateLimits = ref<Array<{
plan: string
rps: string
daily: string
dailyLimit?: number
usedToday?: number
remainingToday?: number
highlight: boolean
}>>([])
// 加载API Key列表
async function loadApiKeys() {
loading.value = true
try {
const result = await pageAppApiKey({ page: 1, limit: 100 })
console.log('API Key 返回数据:', result)
keyList.value = (result.list || []).map((key) => ({
id: String(key.id),
name: key.name,
value: key.apiKey || key.keyPrefix,
status: key.status === 0 ? 'active' : 'disabled',
createdAt: formatDate(key.createTime),
lastUsed: key.lastUsedAt ? formatRelativeTime(key.lastUsedAt) : '从未使用',
expireAt: key.expireTime ? formatDate(key.expireTime) : '',
visible: false,
usageCount: key.usageCount,
scopes: key.scopes ? (typeof key.scopes === 'string' ? JSON.parse(key.scopes) : key.scopes) : [],
remark: key.remark
}))
} catch (error: any) {
console.error('加载API Key列表失败:', error)
message.error(error.message || '加载API Key列表失败请稍后重试')
} finally {
loading.value = false
}
}
// 加载速率限制信息
async function loadRateLimits() {
try {
const rateData = await getApiRateLimits()
// 更新默认的速率限制显示
rateLimits.value = [
{ plan: '免费版', rps: '5', daily: '1,000 次', highlight: rateData.plan === '免费版' },
{ plan: '基础版', rps: '20', daily: '10,000 次', highlight: rateData.plan === '基础版' },
{ plan: '专业版', rps: '100', daily: '100,000 次', highlight: rateData.plan === '专业版' },
{ plan: '企业版', rps: '自定义', daily: '不限', highlight: rateData.plan === '企业版' },
]
// 如果API返回了实际数据更新当前套餐
const currentPlanIndex = rateLimits.value.findIndex(r => r.plan.includes(rateData.plan))
if (currentPlanIndex !== -1) {
rateLimits.value[currentPlanIndex] = {
...rateLimits.value[currentPlanIndex],
dailyLimit: rateData.dailyLimit,
usedToday: rateData.usedToday,
remainingToday: rateData.remainingToday,
daily: `${(rateData.usedToday || 0).toLocaleString()} / ${(rateData.dailyLimit || 0).toLocaleString()}`,
highlight: true
}
}
} catch (error) {
console.error('加载速率限制失败:', error)
}
}
function maskKey(val: string) {
if (!val) return ''
return val.slice(0, 8) + '••••••••••••••••' + val.slice(-4)
}
function toggleVisible(key: any) {
key.visible = !key.visible
}
async function copyKey(val: string) {
try {
await navigator.clipboard.writeText(val)
message.success('已复制到剪贴板')
} catch {
message.error('复制失败,请手动复制')
}
}
// 切换API Key状态
async function toggleStatus(key: any) {
try {
const newStatus = key.status === 'active' ? 1 : 0 // 后端: 0=正常, 1=禁用
await updateAppApiKeyStatus(Number(key.id), newStatus)
key.status = newStatus === 0 ? 'active' : 'disabled'
message.success(key.status === 'active' ? '已启用' : '已禁用')
} catch (error: any) {
console.error('更新API Key状态失败:', error)
message.error(error.message || '操作失败,请稍后重试')
}
}
// 删除API Key
async function deleteKey(id: string) {
try {
await removeAppApiKey(Number(id))
await loadApiKeys()
message.success('API Key 已删除')
} catch (error: any) {
console.error('删除API Key失败:', error)
message.error(error.message || '删除失败,请稍后重试')
}
}
// 创建API Key
async function handleCreate() {
if (!createForm.name.trim()) {
message.error('请填写 API Key 名称')
return
}
// 将过期选项转换为过期时间
let expireTime: string | undefined
if (createForm.expire) {
const now = new Date()
switch (createForm.expire) {
case '30d': now.setDate(now.getDate() + 30); break
case '90d': now.setDate(now.getDate() + 90); break
case '1y': now.setFullYear(now.getFullYear() + 1); break
}
expireTime = now.toISOString()
}
creating.value = true
try {
const result = await createAppApiKey({
name: createForm.name.trim(),
expireTime,
scopes: JSON.stringify(createForm.scopes),
remark: createForm.remark.trim() || undefined
})
message.success('API Key 创建成功')
showCreateModal.value = false
// 重置表单
Object.assign(createForm, { name: '', expire: '', scopes: ['read', 'write'], remark: '' })
// 重新加载列表
await loadApiKeys()
// 显示新创建的Key提示
message.info(`新Key前缀: ${result.keyPrefix || 'sk-'}...,请在列表中查看完整密钥`)
} catch (error: any) {
console.error('创建API Key失败:', error)
message.error(error.message || '创建失败,请稍后重试')
} finally {
creating.value = false
}
}
// 工具函数:格式化日期
function formatDate(dateStr: string) {
const date = new Date(dateStr)
return date.toLocaleDateString('zh-CN')
}
// 工具函数:格式化相对时间
function formatRelativeTime(dateStr: string) {
const date = new Date(dateStr)
const now = new Date()
const diffMs = now.getTime() - date.getTime()
const diffMins = Math.floor(diffMs / (1000 * 60))
const diffHours = Math.floor(diffMs / (1000 * 60 * 60))
const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24))
if (diffMins < 1) return '刚刚'
if (diffMins < 60) return `${diffMins}分钟前`
if (diffHours < 24) return `${diffHours}小时前`
if (diffDays < 30) return `${diffDays}天前`
return formatDate(dateStr)
}
const codeTabs = [
{
key: 'ts',
label: 'TypeScript',
lang: 'TypeScript',
code: `import { WebsopyClient } from '@websopy/sdk'
const client = new WebsopyClient({
apiKey: 'sk-xxxxxxxxxxxxxxxx' // 替换为你的 API Key
})
const reply = await client.agent.chat({
message: '你好,帮我查询今日数据'
})
console.log(reply.content)`,
},
{
key: 'python',
label: 'Python',
lang: 'Python',
code: `from websopy import WebsopyClient
client = WebsopyClient(api_key="sk-xxxxxxxxxxxxxxxx")
reply = client.agent.chat(
message="你好,帮我查询今日数据"
)
print(reply.content)`,
},
{
key: 'curl',
label: 'cURL',
lang: 'Shell',
code: `curl -X POST https://api.websopy.com/v1/agent/chat \\
-H "Authorization: Bearer sk-xxxxxxxxxxxxxxxx" \\
-H "Content-Type: application/json" \\
-d '{"message": "你好,帮我查询今日数据"}'`,
},
]
const currentTab = computed(() => codeTabs.find(t => t.key === activeTab.value))
async function copyCode() {
const code = currentTab.value?.code || ''
try {
await navigator.clipboard.writeText(code)
message.success('代码已复制')
} catch {
message.error('复制失败')
}
}
// 页面加载时初始化
onMounted(() => {
loadApiKeys()
loadRateLimits()
})
</script>
<style scoped>
.dev-page {
min-height: 100%;
}
.page-header {
display: flex;
align-items: flex-start;
justify-content: space-between;
padding: 24px 28px 16px;
border-bottom: 1px solid #f0f0f0;
gap: 16px;
}
.page-title {
font-size: 20px;
font-weight: 700;
color: rgba(0, 0, 0, 0.88);
margin: 0 0 4px;
}
.page-desc {
font-size: 14px;
color: rgba(0, 0, 0, 0.45);
margin: 0;
}
.page-body {
padding: 20px 24px 28px;
}
.panel {
background: #fff;
border: 1px solid #f0f0f0;
border-radius: 12px;
overflow: hidden;
}
.panel-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 14px 18px;
border-bottom: 1px solid #f5f5f5;
}
.panel-title {
font-size: 14px;
font-weight: 600;
color: rgba(0, 0, 0, 0.85);
}
/* 空状态 */
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
padding: 48px 24px;
text-align: center;
}
.empty-icon { font-size: 48px; margin-bottom: 12px; }
.empty-title { font-size: 16px; font-weight: 600; color: rgba(0, 0, 0, 0.7); }
.empty-desc { font-size: 13px; color: rgba(0, 0, 0, 0.4); margin-top: 6px; }
/* Key 列表 */
.key-list {
padding: 8px 0;
}
.key-item {
display: flex;
align-items: center;
gap: 16px;
padding: 16px 20px;
border-bottom: 1px solid #f9f9f9;
transition: background 0.15s;
}
.key-item:hover { background: #fafafa; }
.key-item:last-child { border-bottom: none; }
.key-item-main { flex: 1; min-width: 0; }
.key-name-row {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 6px;
}
.key-name {
font-size: 14px;
font-weight: 600;
color: rgba(0, 0, 0, 0.85);
}
.key-value-row {
display: flex;
align-items: center;
gap: 6px;
margin-bottom: 6px;
}
.key-value {
font-family: 'Fira Code', 'JetBrains Mono', monospace;
font-size: 13px;
color: rgba(0, 0, 0, 0.65);
background: #f5f5f5;
padding: 3px 8px;
border-radius: 5px;
max-width: 420px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.key-action-btn {
padding: 0 5px;
height: 24px;
}
.key-meta {
display: flex;
flex-wrap: wrap;
gap: 12px;
font-size: 12px;
color: rgba(0, 0, 0, 0.38);
}
.key-item-actions {
display: flex;
gap: 8px;
flex-shrink: 0;
}
/* 代码示例 */
.code-example {
background: #0f0f23;
}
.code-toolbar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px 16px;
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
}
.code-lang {
font-size: 12px;
color: rgba(255, 255, 255, 0.4);
font-family: monospace;
}
.code-toolbar :deep(.ant-btn) {
color: rgba(255, 255, 255, 0.5);
}
.code-toolbar :deep(.ant-btn:hover) {
color: rgba(255, 255, 255, 0.9);
}
.code-pre {
padding: 16px 20px;
margin: 0;
font-family: 'Fira Code', 'JetBrains Mono', monospace;
font-size: 13px;
line-height: 1.7;
color: #e2e8f0;
overflow-x: auto;
}
/* 速率限制 */
.rate-grid {
padding: 16px;
display: grid;
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
gap: 12px;
}
.rate-card {
padding: 16px;
border-radius: 10px;
border: 1px solid #f0f0f0;
background: #fafafa;
text-align: center;
transition: all 0.15s;
}
.rate-card.highlight {
background: linear-gradient(135deg, #f0f0ff 0%, #f5f3ff 100%);
border-color: #c4b5fd;
}
.rate-plan {
font-size: 12px;
color: rgba(0, 0, 0, 0.45);
margin-bottom: 6px;
}
.rate-value {
font-size: 22px;
font-weight: 700;
color: rgba(0, 0, 0, 0.85);
line-height: 1.2;
}
.rate-unit {
font-size: 12px;
font-weight: normal;
color: rgba(0, 0, 0, 0.45);
}
.rate-daily {
font-size: 12px;
color: rgba(0, 0, 0, 0.4);
margin-top: 4px;
}
</style>

View File

@@ -1,235 +0,0 @@
<template>
<div class="dev-page">
<!-- 工具栏创建按钮 -->
<div class="page-toolbar">
<a-button type="primary" class="create-btn" @click="showCreate = true">
<template #icon><PlusOutlined /></template>
创建企业自建应用
</a-button>
</div>
<!-- 提示条 -->
<div class="notice-bar">
<InfoCircleOutlined class="notice-icon" />
<span>
<b>温馨提示</b>
应用仅供本企业内部使用应用发布需经过企业管理员审核请仔细阅读
<a href="javascript:void(0)" class="notice-link">应用审核说明</a>
</span>
</div>
<!-- 应用列表 -->
<AppsCenter ref="appsCenterRef" :user-id="userId" @create="showCreate = true" />
<!-- 创建应用弹窗 -->
<a-modal
v-model:open="showCreate"
title="创建企业自建应用"
:confirm-loading="createLoading"
ok-text="创建"
cancel-text="取消"
width="520px"
@ok="handleCreate"
@cancel="resetCreateForm"
>
<a-form
ref="createFormRef"
:model="createForm"
:rules="createRules"
layout="vertical"
style="margin-top: 8px"
>
<a-form-item label="应用名称" name="productName">
<a-input
v-model:value="createForm.productName"
placeholder="请输入应用名称,如:人事管理系统"
:maxlength="50"
show-count
/>
</a-form-item>
<a-form-item label="应用标识" name="productCode" required>
<a-input
v-model:value="createForm.productCode"
placeholder="如hr-system、my-app创建后不可修改"
:maxlength="30"
/>
<div class="form-hint">必须以小写字母开头只能包含小写字母数字和连字符连字符不能连续或结尾创建后不可修改</div>
</a-form-item>
<a-form-item label="应用描述" name="description">
<a-textarea
v-model:value="createForm.description"
placeholder="简单描述应用的功能和用途(选填)"
:rows="3"
:maxlength="200"
show-count
/>
</a-form-item>
<a-form-item label="应用类型" name="appType">
<a-select v-model:value="createForm.appType" placeholder="请选择应用类型">
<a-select-option
v-for="opt in APP_TYPE_OPTIONS"
:key="opt.type"
:value="opt.type"
>
{{ opt.name }}
</a-select-option>
</a-select>
</a-form-item>
</a-form>
</a-modal>
</div>
</template>
<script setup lang="ts">
import { InfoCircleOutlined, PlusOutlined } from '@ant-design/icons-vue'
import { message } from 'ant-design-vue'
import type { FormInstance } from 'ant-design-vue'
import AppsCenter from '@/components/developer/AppsCenter.vue'
import { addAppProduct } from '@/api/app/appProduct'
import { addAppEvent } from '@/api/app/appEvent/index'
definePageMeta({ layout: 'developer' })
useHead({ title: '应用管理 - 开发者中心' })
const userId = import.meta.client ? localStorage.getItem('UserId') : null
const appsCenterRef = ref<{ refresh: () => void } | null>(null)
const APP_TYPE_OPTIONS = [
{ type: 10, appType: 'web', name: 'Web 应用' },
{ type: 20, appType: 'miniprogram', name: '小程序' },
{ type: 30, appType: 'mobile', name: '移动 App' },
{ type: 40, appType: 'api', name: 'API 服务' },
{ type: 50, appType: 'internal', name: '内部工具' },
] as const
// 创建弹窗
const showCreate = ref(false)
const createLoading = ref(false)
const createFormRef = ref<FormInstance>()
const createForm = reactive({
productName: '',
productCode: '',
description: '',
appType: APP_TYPE_OPTIONS[0].type,
})
const createRules = {
productName: [
{ required: true, message: '请输入应用名称', trigger: 'blur' },
{ min: 2, max: 50, message: '应用名称长度 2~50 个字符', trigger: 'blur' },
],
productCode: [
{ required: true, message: '请输入应用标识', trigger: 'blur' },
{ min: 2, max: 30, message: '标识长度 2~30 个字符', trigger: 'blur' },
{
pattern: /^[a-z]([a-z0-9]|-(?!-))*[a-z0-9]$|^[a-z]$/,
message: '必须以小写字母开头,只能包含小写字母、数字和连字符,不能以连字符结尾,且连字符不能连续',
trigger: 'blur',
},
],
appType: [{ required: true, message: '请选择应用类型', trigger: 'change' }],
}
async function handleCreate() {
try {
await createFormRef.value?.validateFields()
} catch {
return
}
createLoading.value = true
try {
const created = await addAppProduct({
productName: createForm.productName,
productCode: createForm.productCode,
appType: createForm.appType,
description: createForm.description,
publishStatus: 'developing',
})
if (created?.productId) {
addAppEvent({
appId: created.productId,
eventType: 'create',
title: '创建了应用',
content: `应用名称:${createForm.productName},标识:${createForm.productCode}`,
})
}
message.success('应用创建成功')
showCreate.value = false
resetCreateForm()
appsCenterRef.value?.refresh()
} catch (e: unknown) {
const msg = e instanceof Error ? e.message : typeof e === 'string' ? e : '创建失败,请重试'
message.error(msg)
} finally {
createLoading.value = false
}
}
function resetCreateForm() {
createForm.productName = ''
createForm.productCode = ''
createForm.description = ''
createForm.appType = APP_TYPE_OPTIONS[0].type
createFormRef.value?.resetFields()
}
</script>
<style scoped>
.dev-page {
min-height: 100%;
padding: 20px 24px 28px;
}
/* 工具栏 */
.page-toolbar {
display: flex;
align-items: center;
margin-bottom: 14px;
}
.create-btn {
font-size: 14px;
height: 36px;
border-radius: 8px;
}
/* 提示条 */
.notice-bar {
display: flex;
align-items: flex-start;
gap: 8px;
background: #f0f5ff;
border: 1px solid #d6e4ff;
border-radius: 8px;
padding: 10px 14px;
font-size: 13px;
color: #555;
margin-bottom: 16px;
line-height: 1.6;
}
.notice-icon {
color: #1677ff;
font-size: 15px;
flex-shrink: 0;
margin-top: 2px;
}
.notice-link {
color: #1677ff;
text-decoration: none;
}
.notice-link:hover { text-decoration: underline; }
.form-hint {
font-size: 12px;
color: #999;
margin-top: 4px;
}
</style>

View File

@@ -1,701 +0,0 @@
<template>
<div class="dev-page">
<!-- 页面头部 -->
<div class="page-header">
<div>
<h2 class="page-title">🔨 构建任务</h2>
<p class="page-desc">管理应用的 CI/CD 构建任务触发构建查看构建状态和日志</p>
</div>
<a-space>
<a-button @click="loadBuilds">
<template #icon><ReloadOutlined /></template>
刷新
</a-button>
<a-button type="primary" :loading="triggering" @click="showTriggerModal = true">
<template #icon><PlayCircleOutlined /></template>
触发构建
</a-button>
</a-space>
</div>
<!-- 统计卡片 -->
<a-row :gutter="[16, 16]" class="mb-6">
<a-col :xs="12" :md="6" v-for="stat in buildStats" :key="stat.key">
<div class="stat-card" :class="stat.color">
<div class="stat-icon">{{ stat.icon }}</div>
<div class="stat-info">
<div class="stat-value">{{ stat.value }}</div>
<div class="stat-label">{{ stat.label }}</div>
</div>
</div>
</a-col>
</a-row>
<!-- 筛选栏 -->
<div class="panel mb-4">
<div class="panel-header">
<span class="panel-title">📋 构建记录</span>
<a-space>
<a-select v-model:value="filterStatus" style="width: 120px" placeholder="构建状态" allow-clear @change="loadBuilds">
<a-select-option value="">全部状态</a-select-option>
<a-select-option value="pending">排队中</a-select-option>
<a-select-option value="running">构建中</a-select-option>
<a-select-option value="success">成功</a-select-option>
<a-select-option value="failed">失败</a-select-option>
<a-select-option value="cancelled">已取消</a-select-option>
</a-select>
<a-select v-model:value="filterAppId" style="width: 200px" placeholder="选择应用" allow-clear @change="loadBuilds">
<a-select-option v-for="app in apps" :key="app.productId" :value="app.productId">
{{ app.productName }}
</a-select-option>
</a-select>
<a-input-search v-model:value="searchKeyword" placeholder="构建编号/分支" style="width: 180px" @search="loadBuilds" />
</a-space>
</div>
<!-- 构建列表 -->
<a-spin :spinning="loading">
<div v-if="builds.length > 0" class="build-list">
<div v-for="build in builds" :key="build.id" class="build-item" :class="'status-' + build.status">
<div class="build-status-bar" :class="build.status">
<div class="status-indicator">
<span class="status-dot"></span>
<span class="status-text">{{ statusText(build.status) }}</span>
</div>
<span class="build-time">{{ formatTime(build.createTime) }}</span>
</div>
<div class="build-content">
<div class="build-main">
<div class="build-info">
<div class="build-number">
<span class="ci-badge" :class="build.ciType">{{ ciTypeText(build.ciType) }}</span>
<span class="number-text">#{{ build.buildNumber || build.id }}</span>
</div>
<div class="build-meta">
<span v-if="build.branch" class="meta-item">
<CodeSandboxOutlined /> {{ build.branch }}
</span>
<span v-if="build.commitAuthor" class="meta-item">
<UserOutlined /> {{ build.commitAuthor }}
</span>
<span v-if="build.duration" class="meta-item">
<ClockCircleOutlined /> {{ formatDuration(build.duration) }}
</span>
</div>
<div v-if="build.commitMessage" class="commit-message">{{ build.commitMessage }}</div>
</div>
</div>
<div class="build-actions">
<a-button size="small" type="link" @click="viewLog(build)">查看日志</a-button>
<template v-if="build.status === 'pending' || build.status === 'running'">
<a-popconfirm title="确定要取消此构建?" @confirm="handleCancel(build)">
<a-button danger size="small">取消</a-button>
</a-popconfirm>
</template>
<template v-if="build.status === 'failed'">
<a-button type="primary" size="small" @click="handleRetry(build)">
<template #icon><ReloadOutlined /></template>
重试
</a-button>
</template>
<template v-if="build.artifactUrl">
<a-button size="small" type="link">
<template #icon><DownloadOutlined /></template>
<a :href="build.artifactUrl" target="_blank">下载产物</a>
</a-button>
</template>
</div>
</div>
<div v-if="build.status === 'failed' && build.errorMessage" class="build-error">
<WarningOutlined /> {{ build.errorMessage }}
</div>
</div>
</div>
<a-empty v-else description="暂无构建记录" class="py-8">
<template #image>
<div class="empty-icon">🔨</div>
</template>
</a-empty>
</a-spin>
<!-- 分页 -->
<div v-if="pagination.total > 0" class="pagination-wrapper">
<a-pagination
v-model:current="pagination.current"
v-model:pageSize="pagination.pageSize"
:total="pagination.total"
:show-size-changer="true"
:show-quick-jumper="true"
@change="handlePageChange"
/>
</div>
</div>
<!-- 触发构建弹窗 -->
<a-modal v-model:open="showTriggerModal" title="触发构建" :confirm-loading="triggering" @ok="handleTrigger" @cancel="resetTriggerForm">
<a-form :model="triggerForm" layout="vertical">
<a-form-item label="选择应用" required>
<a-select v-model:value="triggerForm.appId" placeholder="选择要构建的应用" @change="handleAppChange">
<a-select-option v-for="app in apps" :key="app.productId" :value="app.productId">
{{ app.productName }}
</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="选择流水线(可选)">
<a-select v-model:value="triggerForm.pipelineId" placeholder="默认使用第一个可用流水线">
<a-select-option v-for="p in pipelines" :key="p.id" :value="p.id">
{{ p.name }} ({{ p.env }})
</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="分支">
<a-input v-model:value="triggerForm.branch" placeholder="留空则使用默认分支main" />
</a-form-item>
<a-alert v-if="triggerForm.appId" type="info" show-icon>
<template #message>
提示构建任务将在 CI 系统后台执行可随时刷新页面查看构建进度
</template>
</a-alert>
</a-form>
</a-modal>
<!-- 构建日志弹窗 -->
<a-modal v-model:open="showLogModal" title="构建日志" width="800px" :footer="null">
<div class="log-container">
<pre class="log-content">{{ buildLog || '加载中...' }}</pre>
</div>
</a-modal>
</div>
</template>
<script setup lang="ts">
import {
ReloadOutlined,
PlayCircleOutlined,
CodeSandboxOutlined,
UserOutlined,
ClockCircleOutlined,
DownloadOutlined,
WarningOutlined,
} from '@ant-design/icons-vue'
import { message } from 'ant-design-vue'
import {
pageBuild,
listBuildByApp,
triggerBuild,
cancelBuild,
retryBuild,
getBuildLog,
getBuildStats,
} from '@/api/app/cicd'
import type { AppBuild } from '@/api/app/cicd'
import { getDeveloperApps } from '@/api/app/appProduct'
import type { AppProduct } from '@/api/app/appProduct/model'
import { listPipelineByApp } from '@/api/app/cicd'
import type { AppPipeline } from '@/api/app/cicd'
definePageMeta({ layout: 'developer' })
useHead({ title: '构建任务 - 开发者中心' })
const userId = import.meta.client ? localStorage.getItem('UserId') : null
// 加载状态
const loading = ref(false)
const triggering = ref(false)
const apps = ref<AppProduct[]>([])
const builds = ref<AppBuild[]>([])
const pipelines = ref<AppPipeline[]>([])
// 筛选
const filterStatus = ref('')
const filterAppId = ref<number | undefined>()
const searchKeyword = ref('')
// 分页
const pagination = reactive({
current: 1,
pageSize: 10,
total: 0,
})
// 构建统计
const buildStats = reactive([
{ key: 'total', icon: '📊', label: '总构建', value: 0, color: 'blue' },
{ key: 'running', icon: '⏳', label: '进行中', value: 0, color: 'orange' },
{ key: 'success', icon: '✅', label: '成功', value: 0, color: 'green' },
{ key: 'failed', icon: '❌', label: '失败', value: 0, color: 'red' },
])
// 触发构建弹窗
const showTriggerModal = ref(false)
const triggerForm = reactive({
appId: undefined as number | undefined,
pipelineId: undefined as number | undefined,
branch: '',
})
// 日志弹窗
const showLogModal = ref(false)
const buildLog = ref('')
const currentBuild = ref<AppBuild | null>(null)
// ========== 加载数据 ==========
async function loadApps() {
try {
const res = await getDeveloperApps({ page: 1, limit: 100, userId: userId ? Number(userId) : undefined })
apps.value = (res as any)?.data?.records || res?.list || []
} catch (e) {
console.error('加载应用列表失败:', e)
}
}
async function loadBuilds() {
loading.value = true
try {
const res = await pageBuild({
page: pagination.current,
limit: pagination.pageSize,
appId: filterAppId.value,
status: filterStatus.value || undefined,
})
if (res?.data?.code === 200) {
builds.value = res.data.data.records || []
pagination.total = res.data.data.total || 0
updateStats()
} else {
builds.value = []
pagination.total = 0
}
} catch (e) {
console.error('加载构建记录失败:', e)
builds.value = []
} finally {
loading.value = false
}
}
async function loadPipelines(appId: number) {
try {
const res = await listPipelineByApp(appId)
if (res?.data?.code === 200) {
pipelines.value = res.data.data || []
} else {
pipelines.value = []
}
} catch (e) {
pipelines.value = []
}
}
async function loadStats() {
if (!filterAppId.value) return
try {
const res = await getBuildStats(filterAppId.value)
if (res?.data?.code === 200) {
const stats = res.data.data
buildStats[0].value = stats.total || 0
buildStats[1].value = stats.running || 0
buildStats[2].value = stats.success || 0
buildStats[3].value = stats.failed || 0
}
} catch (e) {
console.error('加载统计数据失败:', e)
}
}
function updateStats() {
buildStats[0].value = builds.value.length
buildStats[1].value = builds.value.filter(b => b.status === 'pending' || b.status === 'running').length
buildStats[2].value = builds.value.filter(b => b.status === 'success').length
buildStats[3].value = builds.value.filter(b => b.status === 'failed').length
}
// ========== 操作 ==========
async function handleTrigger() {
if (!triggerForm.appId) {
message.error('请选择应用')
return
}
triggering.value = true
try {
const res = await triggerBuild(triggerForm.appId, triggerForm.branch || undefined)
if (res?.data?.code === 200) {
message.success('构建已触发!')
showTriggerModal.value = false
resetTriggerForm()
loadBuilds()
} else {
message.error(res?.data?.message || '触发失败')
}
} catch (e: any) {
message.error(e?.message || '触发构建失败')
} finally {
triggering.value = false
}
}
function handleCancel(build: AppBuild) {
cancelBuild(build.id!).then(res => {
if (res?.data?.code === 200) {
message.success('构建已取消')
loadBuilds()
} else {
message.error(res?.data?.message || '取消失败')
}
})
}
function handleRetry(build: AppBuild) {
retryBuild(build.id!).then(res => {
if (res?.data?.code === 200) {
message.success('构建已重试')
loadBuilds()
} else {
message.error(res?.data?.message || '重试失败')
}
})
}
async function viewLog(build: AppBuild) {
currentBuild.value = build
showLogModal.value = true
buildLog.value = ''
try {
const res = await getBuildLog(build.id!)
if (res?.data?.code === 200) {
buildLog.value = res.data.data?.log || '暂无日志'
} else {
buildLog.value = res?.data?.message || '获取日志失败'
}
} catch (e: any) {
buildLog.value = '获取日志失败: ' + (e?.message || '未知错误')
}
}
function handleAppChange(appId: number) {
triggerForm.pipelineId = undefined
triggerForm.branch = ''
if (appId) {
loadPipelines(appId)
}
}
function resetTriggerForm() {
triggerForm.appId = undefined
triggerForm.pipelineId = undefined
triggerForm.branch = ''
pipelines.value = []
}
function handlePageChange(page: number, pageSize: number) {
pagination.current = page
pagination.pageSize = pageSize
loadBuilds()
}
// ========== 辅助函数 ==========
function statusText(status?: string) {
const map: Record<string, string> = {
pending: '排队中',
running: '构建中',
success: '成功',
failed: '失败',
cancelled: '已取消',
}
return map[status || ''] || '未知'
}
function ciTypeText(type?: string) {
const map: Record<string, string> = {
gitea: 'Gitea',
jenkins: 'Jenkins',
github: 'GitHub',
}
return map[type || ''] || type || 'CI'
}
function formatTime(time?: string) {
if (!time) return ''
return new Date(time).toLocaleString('zh-CN')
}
function formatDuration(seconds?: number) {
if (!seconds) return ''
if (seconds < 60) return `${seconds}s`
const mins = Math.floor(seconds / 60)
const secs = seconds % 60
if (mins < 60) return `${mins}m ${secs}s`
const hours = Math.floor(mins / 60)
const mins2 = mins % 60
return `${hours}h ${mins2}m`
}
// 初始化
onMounted(() => {
loadApps()
loadBuilds()
loadStats()
})
</script>
<style scoped>
.dev-page {
min-height: 100%;
padding: 20px 24px 28px;
}
.page-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 20px;
}
.page-title {
font-size: 18px;
font-weight: 700;
color: #1f2937;
margin: 0;
}
.page-desc {
font-size: 13px;
color: #9ca3af;
margin: 4px 0 0;
}
/* 统计卡片 */
.stat-card {
display: flex;
align-items: center;
gap: 12px;
padding: 16px;
border-radius: 12px;
border: 1px solid transparent;
transition: all 0.2s;
}
.stat-card:hover { transform: translateY(-1px); }
.stat-card.blue { background: #eff6ff; border-color: #dbeafe; }
.stat-card.orange { background: #fff7ed; border-color: #fed7aa; }
.stat-card.green { background: #f0fdf4; border-color: #bbf7d0; }
.stat-card.red { background: #fff1f2; border-color: #fecdd3; }
.stat-icon { font-size: 28px; flex-shrink: 0; }
.stat-value { font-size: 22px; font-weight: 700; color: rgba(0,0,0,0.85); }
.stat-label { font-size: 12px; color: rgba(0,0,0,0.45); margin-top: 2px; }
/* 面板 */
.panel {
background: #fff;
border: 1px solid #f0f0f0;
border-radius: 12px;
overflow: hidden;
}
.panel-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 14px 18px;
border-bottom: 1px solid #f5f5f5;
}
.panel-title {
font-size: 14px;
font-weight: 600;
color: rgba(0,0,0,0.85);
}
/* 构建列表 */
.build-list {
padding: 0;
}
.build-item {
border-bottom: 1px solid #f0f0f0;
transition: background 0.2s;
}
.build-item:last-child {
border-bottom: none;
}
.build-item:hover {
background: #fafafa;
}
.build-status-bar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px 18px;
background: #f9f9f9;
border-bottom: 1px solid #f0f0f0;
}
.status-indicator {
display: flex;
align-items: center;
gap: 6px;
}
.status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: #999;
}
.status-pending .status-dot { background: #faad14; }
.status-running .status-dot { background: #1890ff; animation: pulse 1s infinite; }
.status-success .status-dot { background: #52c41a; }
.status-failed .status-dot { background: #ff4d4f; }
.status-cancelled .status-dot { background: #d9d9d9; }
.status-text {
font-size: 12px;
font-weight: 500;
}
.status-pending .status-text { color: #faad14; }
.status-running .status-text { color: #1890ff; }
.status-success .status-text { color: #52c41a; }
.status-failed .status-text { color: #ff4d4f; }
.status-cancelled .status-text { color: #999; }
.build-time {
font-size: 12px;
color: rgba(0,0,0,0.45);
}
.build-content {
display: flex;
align-items: center;
justify-content: space-between;
padding: 14px 18px;
gap: 16px;
}
.build-main {
flex: 1;
min-width: 0;
}
.build-number {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 6px;
}
.ci-badge {
font-size: 11px;
padding: 2px 6px;
border-radius: 4px;
font-weight: 600;
}
.ci-badge.gitea { background: #f0f0f0; color: #333; }
.ci-badge.jenkins { background: #dbeafe; color: #1d4ed8; }
.ci-badge.github { background: #f0f9ff; color: #0366d6; }
.number-text {
font-size: 14px;
font-weight: 600;
color: rgba(0,0,0,0.85);
}
.build-meta {
display: flex;
flex-wrap: wrap;
gap: 12px;
font-size: 12px;
color: rgba(0,0,0,0.45);
}
.meta-item {
display: flex;
align-items: center;
gap: 4px;
}
.commit-message {
margin-top: 6px;
font-size: 12px;
color: rgba(0,0,0,0.65);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 500px;
}
.build-actions {
display: flex;
gap: 8px;
flex-shrink: 0;
}
.build-error {
padding: 10px 18px;
background: #fff2f0;
border-top: 1px solid #ffccc7;
font-size: 12px;
color: #ff4d4f;
}
.py-8 { padding: 40px 0; }
.mb-6 { margin-bottom: 24px; }
.mb-4 { margin-bottom: 16px; }
.empty-icon {
font-size: 48px;
margin-bottom: 8px;
}
/* 日志 */
.log-container {
background: #1e1e1e;
border-radius: 8px;
padding: 16px;
max-height: 500px;
overflow: auto;
}
.log-content {
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
font-size: 12px;
line-height: 1.6;
color: #d4d4d4;
margin: 0;
white-space: pre-wrap;
word-break: break-all;
}
.pagination-wrapper {
padding: 16px;
display: flex;
justify-content: flex-end;
border-top: 1px solid #f0f0f0;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
</style>

View File

@@ -1,419 +0,0 @@
<template>
<div class="dev-page">
<!-- 页面头部 -->
<div class="page-header">
<div>
<h2 class="page-title"> 云账号凭证</h2>
<p class="page-desc">管理阿里云腾讯云华为云等云服务商的账号凭证用于创建存储桶和云资源</p>
</div>
<a-button type="primary" @click="showCreateModal = true">
+ 添加云账号
</a-button>
</div>
<div class="page-body">
<!-- 使用提示 -->
<a-alert
class="mb-5"
show-icon
type="info"
message="凭证说明"
description="云账号凭证用于调用云服务商 API 创建存储桶等资源。AccessKeySecret 会被加密存储,不会以明文形式返回。请妥善保管,丢失后需重新创建。"
/>
<!-- 凭证列表 -->
<div class="panel">
<div class="panel-header">
<span class="panel-title">我的云账号凭证</span>
<a-tag color="blue">{{ credentialList.length }} </a-tag>
</div>
<a-spin :spinning="loading">
<div v-if="credentialList.length === 0 && !loading" class="empty-state">
<div class="empty-icon"></div>
<div class="empty-title">还没有云账号凭证</div>
<div class="empty-desc">添加云服务商账号开始创建云存储桶</div>
<a-button type="primary" class="mt-4" @click="showCreateModal = true">添加云账号</a-button>
</div>
<a-table
v-else
:columns="columns"
:data-source="credentialList"
:pagination="false"
row-key="id"
class="credential-table"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'provider'">
<span class="provider-cell">
<!-- <span class="provider-icon">{{ getProviderIcon(record.provider) }}</span>-->
{{ getProviderLabel(record.provider) }}
</span>
</template>
<template v-else-if="column.key === 'status'">
<a-tag :color="record.status === 1 ? 'green' : 'default'">
{{ record.status === 1 ? '启用' : '禁用' }}
</a-tag>
</template>
<template v-else-if="column.key === 'testStatus'">
<a-tag v-if="record.testStatus === 1" color="green">
<template #icon><CheckCircleOutlined /></template>
连接正常
</a-tag>
<a-tag v-else-if="record.testStatus === 2" color="error">
<template #icon><CloseCircleOutlined /></template>
连接失败
</a-tag>
<a-tag v-else color="default">
<template #icon><QuestionCircleOutlined /></template>
未测试
</a-tag>
</template>
<template v-else-if="column.key === 'actions'">
<a-space>
<a-tooltip title="测试连接">
<a-button size="small" :loading="record.testing" @click="testConnection(record)">
<template #icon><ApiOutlined /></template>
</a-button>
</a-tooltip>
<a-tooltip title="编辑">
<a-button size="small" @click="editCredential(record)">
<template #icon><EditOutlined /></template>
</a-button>
</a-tooltip>
<a-tooltip :title="record.status === 1 ? '禁用' : '启用'">
<a-button size="small" @click="toggleStatus(record)">
<template #icon><StopOutlined v-if="record.status === 1" /><CheckCircleOutlined v-else /></template>
</a-button>
</a-tooltip>
<a-popconfirm
title="确认删除该云账号凭证?此操作不可撤销。"
ok-text="删除"
ok-type="danger"
cancel-text="取消"
@confirm="deleteCredential(record.id)"
>
<a-button size="small" danger>
<template #icon><DeleteOutlined /></template>
</a-button>
</a-popconfirm>
</a-space>
</template>
</template>
</a-table>
</a-spin>
</div>
</div>
<!-- 创建/编辑云账号凭证弹窗 -->
<a-modal
v-model:open="showCreateModal"
:title="editingId ? '编辑云账号凭证' : '添加云账号凭证'"
:ok-text="editingId ? '保存' : '创建'"
cancel-text="取消"
:confirm-loading="saving"
@ok="handleSave"
>
<a-form layout="vertical" class="mt-2">
<a-form-item label="云服务商" required>
<a-select
v-model:value="form.provider"
placeholder="选择云服务商"
:disabled="!!editingId"
>
<a-select-option v-for="opt in providerOptions" :key="opt.value" :value="opt.value">
<span>{{ opt.icon }} {{ opt.label }}</span>
</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="凭证名称" required>
<a-input
v-model:value="form.name"
placeholder="例如:阿里云生产账号、腾讯云测试账号..."
:maxlength="50"
show-count
/>
</a-form-item>
<a-form-item label="AccessKeyId" required>
<a-input
v-model:value="form.accessKeyId"
placeholder="请输入 AccessKeyId"
:maxlength="100"
/>
</a-form-item>
<a-form-item label="AccessKeySecret" required>
<a-input-password
v-model:value="form.accessKeySecret"
placeholder="请输入 AccessKeySecret"
:maxlength="200"
/>
</a-form-item>
<a-form-item label="备注">
<a-textarea
v-model:value="form.remark"
:rows="2"
placeholder="可选,用途说明"
/>
</a-form-item>
</a-form>
</a-modal>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { message } from 'ant-design-vue'
import {
CheckCircleOutlined,
CloseCircleOutlined,
QuestionCircleOutlined,
ApiOutlined,
EditOutlined,
DeleteOutlined,
StopOutlined,
} from '@ant-design/icons-vue'
import {
pageCloudCredential,
createCloudCredential,
updateCloudCredential,
removeCloudCredential,
testCloudCredential,
CLOUD_PROVIDER_OPTIONS,
getProviderLabel,
getProviderIcon,
} from '@/api/app/cloudCredential'
import type { AppCloudCredential, AppCloudCredentialParam } from '@/api/app/cloudCredential/model'
definePageMeta({ layout: 'developer' })
useHead({ title: '云账号凭证 - 开发者中心' })
const columns = [
// { title: '名称', dataIndex: 'name', key: 'name', width: 180 },
{ title: '服务商', key: 'provider', width: 140 },
{ title: 'AK', dataIndex: 'accessKeyId', key: 'accessKeyId', width: 180 },
{ title: '状态', key: 'status', width: 80 },
{ title: '连接测试', key: 'testStatus', width: 100 },
{ title: '创建时间', dataIndex: 'createTime', key: 'createTime', width: 160 },
{ title: '操作', key: 'actions', width: 200 },
]
const providerOptions = CLOUD_PROVIDER_OPTIONS
const showCreateModal = ref(false)
const editingId = ref<number | null>(null)
const loading = ref(false)
const saving = ref(false)
const form = reactive({
provider: '',
name: '',
accessKeyId: '',
accessKeySecret: '',
remark: '',
})
// 凭证列表
const credentialList = ref<AppCloudCredential[]>([])
// 加载凭证列表
async function loadCredentials() {
loading.value = true
try {
const params: AppCloudCredentialParam = { page: 1, limit: 100 }
console.log('请求参数:', params)
const result = await pageCloudCredential(params)
console.log('返回结果:', result)
console.log('列表数据:', result?.list)
credentialList.value = (result?.list || []).map((item: any) => ({
...item,
testing: false,
}))
console.log('最终数据:', credentialList.value)
} catch (error: any) {
console.error('加载云账号凭证失败:', error)
console.error('错误详情:', error.response?.data, error.config?.url)
message.error(error.message || '加载失败,请稍后重试')
} finally {
loading.value = false
}
}
// 测试连接
async function testConnection(record: AppCloudCredential) {
record.testing = true
try {
const result = await testCloudCredential(record.id!)
if (result?.success) {
message.success(result.message || '连接测试成功')
record.testStatus = 1
record.testMessage = result.message
} else {
message.error(result?.message || '连接测试失败')
record.testStatus = 2
record.testMessage = result?.message
}
} catch (error: any) {
console.error('测试连接失败:', error)
message.error(error.message || '测试连接失败')
record.testStatus = 2
record.testMessage = error.message
} finally {
record.testing = false
}
}
// 编辑凭证
function editCredential(record: AppCloudCredential) {
editingId.value = record.id || null
form.provider = record.provider || ''
form.name = record.name || ''
form.accessKeyId = ''
form.accessKeySecret = ''
form.remark = record.remark || ''
showCreateModal.value = true
}
// 切换状态
async function toggleStatus(record: AppCloudCredential) {
try {
const newStatus = record.status === 1 ? 0 : 1
await updateCloudCredential({
id: record.id,
status: newStatus,
})
record.status = newStatus
message.success(newStatus === 1 ? '已启用' : '已禁用')
} catch (error: any) {
console.error('更新状态失败:', error)
message.error(error.message || '操作失败,请稍后重试')
}
}
// 删除凭证
async function deleteCredential(id: number) {
try {
await removeCloudCredential(id)
await loadCredentials()
message.success('云账号凭证已删除')
} catch (error: any) {
console.error('删除凭证失败:', error)
message.error(error.message || '删除失败,请稍后重试')
}
}
// 保存凭证
async function handleSave() {
if (!form.provider) {
message.error('请选择云服务商')
return
}
if (!form.name.trim()) {
message.error('请输入凭证名称')
return
}
if (!form.accessKeyId.trim()) {
message.error('请输入 AccessKeyId')
return
}
if (!form.accessKeySecret.trim()) {
message.error('请输入 AccessKeySecret')
return
}
saving.value = true
try {
if (editingId.value) {
await updateCloudCredential({
id: editingId.value,
name: form.name,
accessKeyId: form.accessKeyId || undefined,
accessKeySecret: form.accessKeySecret || undefined,
remark: form.remark,
})
message.success('云账号凭证已更新')
} else {
await createCloudCredential({
provider: form.provider,
name: form.name,
accessKeyId: form.accessKeyId,
accessKeySecret: form.accessKeySecret,
remark: form.remark,
status: 1,
})
message.success('云账号凭证已创建')
}
showCreateModal.value = false
resetForm()
await loadCredentials()
} catch (error: any) {
console.error('保存云账号凭证失败:', error)
message.error(error.message || '保存失败,请稍后重试')
} finally {
saving.value = false
}
}
function resetForm() {
editingId.value = null
form.provider = ''
form.name = ''
form.accessKeyId = ''
form.accessKeySecret = ''
form.remark = ''
}
onMounted(() => {
loadCredentials()
})
</script>
<style scoped>
.dev-page {
@apply p-6;
}
.page-header {
@apply flex justify-between items-start mb-6;
}
.page-title {
@apply text-2xl font-bold m-0;
}
.page-desc {
@apply text-gray-500 mt-1;
}
.page-body {
@apply max-w-5xl;
}
.provider-cell {
@apply flex items-center gap-2;
}
.provider-icon {
@apply text-lg;
}
.credential-table {
@apply mt-4;
}
.empty-state {
@apply py-12 text-center;
}
.empty-icon {
@apply text-5xl mb-4;
}
.empty-title {
@apply text-lg font-medium text-gray-700;
}
.empty-desc {
@apply text-gray-500 mt-1 mb-4;
}
</style>

View File

@@ -1,693 +0,0 @@
<template>
<div v-if="mounted" class="app-config-page">
<!-- 页面头部 -->
<div class="page-header">
<!-- 应用信息卡片 -->
<div class="app-info-card">
<!-- 左侧返回 + 应用图标 + 基本信息 -->
<div class="app-info-main">
<a-button
type="text"
class="back-btn"
@click="navigateTo('/developer/apps')"
>
<template #icon><LeftOutlined /></template>
</a-button>
<div class="app-avatar">
<img
v-if="appInfo?.icon || appInfo?.logo"
:src="appInfo?.icon || appInfo?.logo"
class="app-avatar-img"
/>
<span v-else class="app-avatar-placeholder">
{{ appInfo?.productName?.charAt(0)?.toUpperCase() || '?' }}
</span>
</div>
<div class="app-meta">
<div class="app-name-row">
<span v-if="appInfo?.productName" class="app-name">{{ appInfo.productName }}</span>
<span v-else-if="appInfoLoading" class="app-name-skeleton">
<a-skeleton-input active size="small" style="width: 120px" />
</span>
<span v-else class="app-name app-name-unknown">未知应用</span>
<a-tag
v-if="appTypeLabel"
:color="appTypeColor"
class="app-type-tag"
>{{ appTypeLabel }}</a-tag>
<a-tag
v-if="appInfo?.status !== undefined"
:color="appStatusColor"
class="app-status-tag"
>{{ appStatusLabel }}</a-tag>
</div>
<div class="app-sub-info">
<span v-if="appInfo?.productCode" class="app-code">
<CodeOutlined class="sub-icon" />
{{ appInfo.productCode }}
</span>
<span v-if="appInfo?.domain" class="app-domain">
<GlobalOutlined class="sub-icon" />
{{ appInfo.domain }}
</span>
<span v-if="appInfo?.createTime" class="app-create-time">
<ClockCircleOutlined class="sub-icon" />
创建于 {{ appInfo.createTime?.slice(0, 10) }}
</span>
</div>
</div>
</div>
<!-- 右侧页面说明 -->
<div class="page-desc-block">
<div class="page-title-text">应用配置</div>
<div class="page-desc-text">管理应用的 API回调支付等配置</div>
</div>
</div>
</div>
<!-- 配置类型标签页 -->
<a-tabs v-model:activeKey="activeType" class="config-tabs" @change="handleTypeChange">
<a-tab-pane
v-for="type in configTypes"
:key="type.key"
:tab="type.name"
>
<!-- 配置表单 -->
<div v-if="!loading" class="config-content">
<a-alert
v-if="type.description"
:message="type.description"
type="info"
show-icon
class="mb-4"
/>
<a-form
:model="formData"
layout="vertical"
class="config-form"
>
<a-row :gutter="24">
<a-col
v-for="field in type.configs"
:key="field.key"
:span="field.type === 'textarea' ? 24 : 12"
>
<a-form-item
:label="field.label"
:name="field.key"
:required="field.required"
:rules="field.required ? [{ required: true, message: `请输入${field.label}` }] : []"
>
<!-- 密码输入 -->
<a-input-password
v-if="field.type === 'password'"
v-model:value="formData[field.key]"
:placeholder="field.placeholder"
/>
<!-- 文本域 -->
<a-textarea
v-else-if="field.type === 'textarea'"
v-model:value="formData[field.key]"
:placeholder="field.placeholder"
:rows="4"
/>
<!-- 下拉选择 -->
<a-select
v-else-if="field.type === 'select'"
v-model:value="formData[field.key]"
:placeholder="field.placeholder"
>
<a-select-option
v-for="opt in field.options"
:key="opt.value"
:value="opt.value"
>
{{ opt.label }}
</a-select-option>
</a-select>
<!-- 开关 -->
<a-switch
v-else-if="field.type === 'switch'"
v-model:checked="formData[field.key]"
/>
<!-- JSON 编辑器 -->
<a-textarea
v-else-if="field.type === 'json'"
v-model:value="formData[field.key]"
:placeholder="field.placeholder"
:rows="8"
style="font-family: monospace"
/>
<!-- 数字输入 -->
<a-input-number
v-else-if="field.type === 'number'"
v-model:value="formData[field.key]"
:placeholder="field.placeholder"
style="width: 100%"
/>
<!-- 普通输入 -->
<a-input
v-else
v-model:value="formData[field.key]"
:placeholder="field.placeholder"
/>
<!-- 字段说明 -->
<div v-if="field.description" class="field-desc">
<InfoCircleOutlined class="field-desc-icon" />
{{ field.description }}
</div>
</a-form-item>
</a-col>
</a-row>
</a-form>
<!-- 操作按钮 -->
<div class="form-actions">
<a-space>
<a-button @click="handleReset">
重置
</a-button>
<a-button type="primary" :loading="saving" @click="handleSave">
保存配置
</a-button>
</a-space>
</div>
</div>
<!-- 加载中 -->
<div v-else class="loading-wrap">
<a-spin size="large" tip="加载中..." />
</div>
</a-tab-pane>
</a-tabs>
</div>
</template>
<script setup lang="ts">
import { InfoCircleOutlined, LeftOutlined, CodeOutlined, GlobalOutlined, ClockCircleOutlined } from '@ant-design/icons-vue'
import { message } from 'ant-design-vue'
import { getConfigsMap, listAppConfig, saveAppConfig, updateAppConfig } from '@/api/app/appConfig'
import type { AppConfig, ConfigType, ConfigField } from '@/api/app/appConfig/model'
import { getAppProduct } from '@/api/app/appProduct'
import type { AppProduct } from '@/api/app/appProduct/model'
definePageMeta({ layout: 'developer' })
useHead({ title: '应用配置 - 开发者中心' })
const route = useRoute()
const mounted = ref(false)
const productId = computed(() => {
if (!mounted.value) return 0
return parseInt(route.params.id as string || route.query.productId as string || '0')
})
// ── 应用信息 ──────────────────────────────────────────
const appInfo = ref<AppProduct | null>(null)
const appInfoLoading = ref(false)
async function loadAppInfo(id: number) {
if (!id) return
appInfoLoading.value = true
try {
appInfo.value = await getAppProduct(id)
// 同步页面 title
if (appInfo.value?.productName) {
useHead({ title: `${appInfo.value.productName} - 应用配置` })
}
} catch {
// 加载应用信息失败不阻塞主流程,静默处理
} finally {
appInfoLoading.value = false
}
}
const APP_TYPE_MAP: Record<string, { label: string; color: string }> = {
web: { label: 'Web 应用', color: 'blue' },
miniprogram: { label: '小程序', color: 'green' },
mobile: { label: '移动 App', color: 'purple' },
api: { label: 'API 服务', color: 'orange' },
internal: { label: '内部工具', color: 'default' },
}
const appTypeLabel = computed(() => {
const t = appInfo.value?.appType
if (t === 10) return '网站'
if (t === 20) return '微信小程序'
if (t === 30) return '抖音小程序'
if (t === 40) return '百度小程序'
if (t === 50) return '支付宝小程序'
if (t === 60) return 'Android APP'
if (t === 70) return 'iOS APP'
if (t === 80) return 'macOS 应用'
if (t === 90) return 'Windows 应用'
if (t === 100) return '插件'
return t?.toString() || ''
})
const appTypeColor = computed(() => {
const t = appInfo.value?.appType
if (t === 20 || t === 30 || t === 40 || t === 50) return 'green'
if (t === 60 || t === 70 || t === 80 || t === 90) return 'purple'
return 'blue'
})
const APP_STATUS_MAP: Record<number, { label: string; color: string }> = {
0: { label: '未开通', color: 'default' },
1: { label: '运行中', color: 'success' },
2: { label: '维护中', color: 'warning' },
3: { label: '已关闭', color: 'error' },
4: { label: '已欠费', color: 'error' },
5: { label: '违规关停', color: 'error' },
}
const appStatusLabel = computed(() => {
const s = appInfo.value?.status
return s !== undefined ? (APP_STATUS_MAP[s]?.label ?? `状态${s}`) : ''
})
const appStatusColor = computed(() => {
const s = appInfo.value?.status
return s !== undefined ? (APP_STATUS_MAP[s]?.color ?? 'default') : 'default'
})
// 配置类型定义
const configTypes: ConfigType[] = [
{
key: 'api',
name: 'API 配置',
icon: '🔌',
description: '配置应用的 API 基础信息,包括地址、超时等',
configs: [
{ key: 'api.baseUrl', label: 'API 基础地址', type: 'input', placeholder: 'https://api.example.com' },
{ key: 'api.timeout', label: '请求超时(秒)', type: 'number', placeholder: '30', defaultValue: 30 },
{ key: 'api.enableCache', label: '启用缓存', type: 'switch', defaultValue: true },
]
},
{
key: 'callback',
name: '回调地址',
icon: '🔔',
description: '配置第三方平台的回调地址,用于接收异步通知',
configs: [
{ key: 'callback.url', label: '回调 URL', type: 'input', placeholder: 'https://yourdomain.com/callback', required: true },
{ key: 'callback.secret', label: '回调密钥', type: 'password', placeholder: '用于验证回调签名' },
]
},
{
key: 'wechat',
name: '微信配置',
icon: '💬',
description: '配置微信小程序或公众号的 AppID 和 AppSecret',
configs: [
{ key: 'wechat.appId', label: 'AppID', type: 'input', placeholder: 'wx1234567890abcdef', required: true },
{ key: 'wechat.appSecret', label: 'AppSecret', type: 'password', placeholder: '微信小程序密钥' },
{ key: 'wechat.type', label: '应用类型', type: 'select', defaultValue: 'miniprogram', options: [
{ label: '小程序', value: 'miniprogram' },
{ label: '公众号', value: 'mp' },
{ label: '网页应用', value: 'web' },
]},
]
},
{
key: 'payment',
name: '支付配置',
icon: '💰',
description: '配置微信支付、支付宝等支付渠道',
configs: [
{ key: 'payment.enabled', label: '启用支付', type: 'switch', defaultValue: false },
{ key: 'payment.provider', label: '支付渠道', type: 'select', defaultValue: 'wechat', options: [
{ label: '微信支付', value: 'wechat' },
{ label: '支付宝', value: 'alipay' },
{ label: 'Stripe', value: 'stripe' },
]},
{ key: 'payment.mchId', label: '商户号', type: 'input', placeholder: '支付商户号' },
{ key: 'payment.apiKey', label: 'API 密钥', type: 'password', placeholder: '支付 API 密钥' },
]
},
{
key: 'git',
name: 'Git 仓库',
icon: '🔧',
description: '配置代码仓库地址,用于持续集成和部署',
configs: [
{ key: 'git.repository', label: '仓库地址', type: 'input', placeholder: 'https://github.com/user/repo.git' },
{ key: 'git.branch', label: '默认分支', type: 'input', placeholder: 'main', defaultValue: 'main' },
{ key: 'git.accessToken', label: '访问令牌', type: 'password', placeholder: 'Git 访问令牌' },
]
},
]
const activeType = ref('api')
const loading = ref(false)
const saving = ref(false)
// 表单数据(根据当前配置类型动态生成)
const formData = reactive<Record<string, any>>({})
// 原始数据(用于重置)
let originalData: Record<string, any> = {}
// 已有配置映射(用于更新时获取 configId
const existingConfigs = ref<Map<string, number>>(new Map())
// 初始化表单数据
function initForm() {
const currentType = configTypes.find(t => t.key === activeType.value)
if (!currentType) return
// 清空表单
Object.keys(formData).forEach(key => delete formData[key])
// 设置默认值
currentType.configs.forEach(field => {
if (field.defaultValue !== undefined) {
formData[field.key] = field.defaultValue
}
})
}
// 加载配置
async function loadConfigs() {
const id = parseInt(route.params.id as string || route.query.productId as string || '0')
if (!id || id === 0) {
message.warning('缺少应用 ID')
navigateTo('/developer/apps')
return
}
loading.value = true
try {
const configs = await getConfigsMap(id)
originalData = { ...configs }
// 合并配置到表单
Object.keys(configs).forEach(key => {
formData[key] = configs[key]
})
// 加载配置列表以获取 configId
const configList = await listAppConfig({ productId: id })
existingConfigs.value = new Map()
configList.forEach(config => {
if (config.configId && config.configKey) {
existingConfigs.value.set(config.configKey, config.configId)
}
})
} catch (e: any) {
message.error(e.message || '加载配置失败')
} finally {
loading.value = false
}
}
// 切换配置类型
function handleTypeChange() {
initForm()
loadConfigs()
}
// 保存配置
async function handleSave() {
const currentType = configTypes.find(t => t.key === activeType.value)
if (!currentType) return
saving.value = true
try {
const id = parseInt(route.params.id as string || route.query.productId as string || '0')
if (!id || id === 0) {
message.error('缺少应用 ID')
return
}
const savePromises = currentType.configs.map(async (field) => {
const value = formData[field.key]
const configId = existingConfigs.value.get(field.key)
const config: AppConfig = {
configId,
productId: id,
configKey: field.key,
configValue: value !== undefined && value !== null ? String(value) : '',
configType: activeType.value,
isEncrypted: 0,
isSecret: 0,
description: field.label,
sortNumber: field.type === 'textarea' ? 999 : 0,
} as AppConfig
// 如果存在 configId 则更新,否则新增
if (configId) {
await updateAppConfig(config)
} else {
await saveAppConfig(config)
}
})
await Promise.all(savePromises)
message.success('配置保存成功')
originalData = { ...formData }
} catch (e: any) {
message.error(e.message || '保存配置失败')
} finally {
saving.value = false
}
}
// 重置
function handleReset() {
Object.keys(formData).forEach(key => {
formData[key] = originalData[key]
})
}
onMounted(() => {
// 标记页面已挂载
mounted.value = true
// 检查是否有 productId 参数
const paramId = route.params.id || route.query.productId
if (!paramId) {
navigateTo('/developer/apps')
return
}
const id = parseInt(paramId as string || '0')
loadAppInfo(id)
initForm()
loadConfigs()
})
</script>
<style scoped>
.app-config-page {
padding: 24px;
}
/* ── 页面头部 ─────────────────────────── */
.page-header {
margin-bottom: 20px;
}
.app-info-card {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
background: #fff;
border: 1px solid #f0f0f0;
border-radius: 12px;
padding: 16px 20px;
}
/* 左侧:返回按钮 + 图标 + 基本信息 */
.app-info-main {
display: flex;
align-items: center;
gap: 12px;
flex: 1;
min-width: 0;
}
.back-btn {
flex-shrink: 0;
color: rgba(0, 0, 0, 0.45);
padding: 0 6px;
height: 32px;
border-radius: 8px;
}
.back-btn:hover {
background: #f0f5ff;
color: #4f46e5;
}
/* 应用头像 */
.app-avatar {
width: 48px;
height: 48px;
flex-shrink: 0;
border-radius: 12px;
overflow: hidden;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
display: flex;
align-items: center;
justify-content: center;
border: 1px solid rgba(0, 0, 0, 0.06);
}
.app-avatar-img {
width: 100%;
height: 100%;
object-fit: cover;
}
.app-avatar-placeholder {
font-size: 20px;
font-weight: 700;
color: #fff;
line-height: 1;
text-transform: uppercase;
}
/* 应用 Meta 信息 */
.app-meta {
flex: 1;
min-width: 0;
}
.app-name-row {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
margin-bottom: 5px;
}
.app-name {
font-size: 16px;
font-weight: 600;
color: rgba(0, 0, 0, 0.85);
line-height: 1.4;
}
.app-name-unknown {
color: rgba(0, 0, 0, 0.35);
}
.app-type-tag,
.app-status-tag {
font-size: 11px;
line-height: 18px;
padding: 0 6px;
border-radius: 4px;
}
/* 次级信息行 */
.app-sub-info {
display: flex;
align-items: center;
gap: 16px;
flex-wrap: wrap;
}
.app-code,
.app-domain,
.app-create-time {
display: flex;
align-items: center;
gap: 4px;
font-size: 12px;
color: rgba(0, 0, 0, 0.45);
font-family: 'SFMono-Regular', Consolas, monospace;
}
.app-domain,
.app-create-time {
font-family: inherit;
}
.sub-icon {
font-size: 11px;
opacity: 0.7;
}
/* 右侧:页面说明 */
.page-desc-block {
flex-shrink: 0;
text-align: right;
padding-left: 16px;
border-left: 1px solid #f0f0f0;
}
.page-title-text {
font-size: 15px;
font-weight: 600;
color: rgba(0, 0, 0, 0.75);
line-height: 1.4;
}
.page-desc-text {
font-size: 12px;
color: rgba(0, 0, 0, 0.35);
margin-top: 3px;
}
/* ── 配置区域 ─────────────────────────── */
.config-tabs {
background: #fff;
padding: 20px;
border-radius: 12px;
min-height: 500px;
}
.config-content {
padding: 8px 0;
}
.config-form {
margin-bottom: 24px;
}
.field-desc {
display: flex;
align-items: center;
gap: 6px;
font-size: 12px;
color: #999;
margin-top: 4px;
}
.field-desc-icon {
color: #1890ff;
}
.form-actions {
margin-top: 32px;
padding-top: 24px;
border-top: 1px solid #f0f0f0;
}
.loading-wrap {
display: flex;
align-items: center;
justify-content: center;
min-height: 300px;
}
.mb-4 {
margin-bottom: 16px;
}
</style>

View File

@@ -1,567 +0,0 @@
<template>
<div class="docs-page">
<!-- 面包屑 -->
<div class="breadcrumb-bar">
<div class="breadcrumb-inner">
<a-breadcrumb>
<a-breadcrumb-item>
<a href="/developer">开发者中心</a>
</a-breadcrumb-item>
<a-breadcrumb-item>
<NuxtLink to="/developer/docs">开发文档</NuxtLink>
</a-breadcrumb-item>
<a-breadcrumb-item v-if="page">{{ page.title }}</a-breadcrumb-item>
</a-breadcrumb>
</div>
</div>
<div class="docs-layout">
<!-- 左侧导航 -->
<aside class="docs-sidebar">
<div class="sidebar-inner">
<div class="sidebar-back">
<NuxtLink to="/developer/docs" class="back-link">
返回文档中心
</NuxtLink>
</div>
<!-- 导航树 -->
<nav class="sidebar-nav">
<div v-for="cat in navCategories" :key="cat.key" class="nav-group">
<div class="nav-group-title" @click="toggleGroup(cat.key)">
<span>{{ cat.icon }}</span>
<span class="nav-group-label">{{ cat.label }}</span>
<span class="nav-group-arrow" :class="{ collapsed: !expandedGroups[cat.key] }"></span>
</div>
<div v-show="expandedGroups[cat.key]" class="nav-group-items">
<NuxtLink
v-for="doc in cat.items"
:key="doc.stem?.path"
:to="toPageUrl(doc.stem?.path)"
class="nav-item"
:class="{ active: isActive(doc) }"
>
{{ doc.title }}
</NuxtLink>
</div>
</div>
</nav>
</div>
</aside>
<!-- 右侧内容 -->
<main class="docs-main">
<div v-if="page" class="docs-content">
<h1 class="docs-title">{{ page.title }}</h1>
<p v-if="page.description" class="docs-desc">{{ page.description }}</p>
<ContentRenderer :value="page" />
<!-- 上下篇导航 -->
<div class="docs-nav-footer">
<div v-if="prevDoc" class="nav-footer-item prev" @click="navigateToDoc(prevDoc)">
<span class="nav-footer-dir"> 上一篇</span>
<span class="nav-footer-title">{{ prevDoc.title }}</span>
</div>
<div v-else />
<div v-if="nextDoc" class="nav-footer-item next" @click="navigateToDoc(nextDoc)">
<span class="nav-footer-dir">下一篇 </span>
<span class="nav-footer-title">{{ nextDoc.title }}</span>
</div>
</div>
</div>
<!-- 404 -->
<div v-else class="docs-not-found">
<div class="not-found-icon">📄</div>
<h2>文档未找到</h2>
<p>你访问的文档不存在或已被移除</p>
<NuxtLink to="/developer/docs" class="back-to-docs">
返回文档中心
</NuxtLink>
</div>
</main>
</div>
</div>
</template>
<script setup lang="ts">
definePageMeta({ layout: 'developer' })
const route = useRoute()
const router = useRouter()
// 将 /developer/docs/xxx 转换为 /docs/xxx 用于 Nuxt Content 查询
const contentPath = computed(() => {
return '/docs/' + (route.params.slug as string[]).join('/')
})
// 查询当前文档
const { data: page } = await useAsyncData(contentPath.value, () =>
queryCollection('docs').path(contentPath.value).first()
)
// 设置页面标题
watchEffect(() => {
if (page.value) {
useHead({ title: `${page.value.title} - 开发文档 - 开发者中心` })
}
})
// 查询所有文档
const { data: allDocs } = await useAsyncData('docs-nav', () =>
queryCollection('docs')
.order('order', 'ASC')
.all()
)
// 分类定义
const categoryMap: Record<string, { label: string; icon: string }> = {
'getting-started': { label: '快速开始', icon: '🚀' },
'api': { label: 'API 参考', icon: '🔌' },
'ai': { label: 'AI 功能', icon: '🤖' },
'deploy': { label: '部署运维', icon: '🚢' },
}
// 构建导航分类
const navCategories = computed(() => {
const docs = allDocs.value || []
const groups: { key: string; label: string; icon: string; items: any[] }[] = []
for (const [key, val] of Object.entries(categoryMap)) {
const items = docs.filter((d: any) => d.category === key)
if (items.length) {
groups.push({ key, label: val.label, icon: val.icon, items })
}
}
return groups
})
// 分组展开状态
const expandedGroups = reactive<Record<string, boolean>>({})
for (const cat of navCategories.value) {
expandedGroups[cat.key] = true
}
function toggleGroup(key: string) {
expandedGroups[key] = !expandedGroups[key]
}
// 判断是否为当前活动项
function isActive(doc: any) {
return doc.stem?.path === contentPath.value
}
// 路径转换:/docs/xxx → /developer/docs/xxx
function toPageUrl(path?: string) {
if (!path) return ''
return path.replace(/^\/docs/, '/developer/docs')
}
// 上/下篇文档
const currentIndex = computed(() => {
const docs = allDocs.value || []
return docs.findIndex((d: any) => d.stem?.path === contentPath.value)
})
const prevDoc = computed(() => {
const idx = currentIndex.value
return idx > 0 ? (allDocs.value as any[])?.[idx - 1] : null
})
const nextDoc = computed(() => {
const docs = allDocs.value || []
const idx = currentIndex.value
return idx < docs.length - 1 ? docs[idx + 1] : null
})
function navigateToDoc(doc: any) {
if (doc?.stem?.path) {
router.push(toPageUrl(doc.stem.path))
}
}
// 滚动到顶部
onMounted(() => {
window.scrollTo(0, 0)
})
</script>
<style scoped>
.docs-page {
min-height: 100%;
background: #f9fafb;
}
/* 面包屑 */
.breadcrumb-bar {
background: #fff;
border-bottom: 1px solid #f0f0f0;
}
.breadcrumb-inner {
max-width: 100%;
padding: 12px 28px;
}
/* 布局 */
.docs-layout {
display: flex;
min-height: calc(100vh - 160px);
}
/* 左侧导航 */
.docs-sidebar {
width: 260px;
flex-shrink: 0;
border-right: 1px solid #f0f0f0;
background: #fff;
overflow-y: auto;
height: calc(100vh - 160px);
position: sticky;
top: 0;
}
.sidebar-inner {
padding: 16px 0;
}
.sidebar-back {
padding: 0 16px 12px;
border-bottom: 1px solid #f5f5f5;
margin-bottom: 8px;
}
.back-link {
font-size: 13px;
color: #4f46e5;
text-decoration: none;
display: block;
}
.back-link:hover {
color: #3730a3;
}
/* 导航树 */
.sidebar-nav {
padding: 4px 0;
}
.nav-group {
margin-bottom: 4px;
}
.nav-group-title {
display: flex;
align-items: center;
gap: 6px;
padding: 8px 16px;
font-size: 12px;
font-weight: 600;
color: rgba(0, 0, 0, 0.45);
text-transform: uppercase;
letter-spacing: 0.05em;
cursor: pointer;
user-select: none;
transition: color 0.15s;
}
.nav-group-title:hover {
color: rgba(0, 0, 0, 0.65);
}
.nav-group-label { flex: 1; }
.nav-group-arrow {
font-size: 10px;
transition: transform 0.2s;
color: rgba(0, 0, 0, 0.3);
}
.nav-group-arrow.collapsed {
transform: rotate(-90deg);
}
.nav-group-items {
padding: 2px 0;
}
.nav-item {
display: block;
padding: 7px 16px 7px 36px;
font-size: 13px;
color: rgba(0, 0, 0, 0.6);
text-decoration: none;
transition: all 0.15s;
border-left: 2px solid transparent;
line-height: 1.5;
}
.nav-item:hover {
color: #4f46e5;
background: #f5f7ff;
}
.nav-item.active {
color: #4f46e5;
font-weight: 500;
background: #eef2ff;
border-left-color: #4f46e5;
}
/* 主内容区 */
.docs-main {
flex: 1;
min-width: 0;
}
.docs-content {
max-width: 800px;
margin: 0 auto;
padding: 32px 40px 60px;
}
.docs-title {
font-size: 28px;
font-weight: 700;
color: rgba(0, 0, 0, 0.88);
margin: 0 0 8px;
line-height: 1.3;
}
.docs-desc {
font-size: 15px;
color: rgba(0, 0, 0, 0.45);
margin: 0 0 32px;
line-height: 1.6;
}
/* Nuxt Content 渲染的 Markdown 内容样式 */
.docs-content :deep(h2) {
font-size: 22px;
font-weight: 600;
color: rgba(0, 0, 0, 0.85);
margin: 36px 0 16px;
padding-bottom: 10px;
border-bottom: 1px solid #f0f0f0;
}
.docs-content :deep(h3) {
font-size: 18px;
font-weight: 600;
color: rgba(0, 0, 0, 0.82);
margin: 28px 0 12px;
}
.docs-content :deep(h4) {
font-size: 15px;
font-weight: 600;
color: rgba(0, 0, 0, 0.78);
margin: 20px 0 10px;
}
.docs-content :deep(p) {
font-size: 14px;
line-height: 1.75;
color: rgba(0, 0, 0, 0.72);
margin: 0 0 16px;
}
.docs-content :deep(blockquote) {
margin: 16px 0;
padding: 12px 16px;
border-left: 3px solid #4f46e5;
background: #f5f7ff;
border-radius: 0 8px 8px 0;
}
.docs-content :deep(blockquote p) {
margin: 0;
color: rgba(0, 0, 0, 0.6);
}
.docs-content :deep(ul),
.docs-content :deep(ol) {
padding-left: 24px;
margin: 12px 0 20px;
}
.docs-content :deep(li) {
font-size: 14px;
line-height: 1.75;
color: rgba(0, 0, 0, 0.72);
margin-bottom: 4px;
}
.docs-content :deep(code) {
font-size: 13px;
padding: 2px 6px;
background: #f3f4f6;
border-radius: 4px;
font-family: 'JetBrains Mono', 'Fira Code', Consolas, monospace;
color: #e11d48;
}
.docs-content :deep(pre) {
margin: 16px 0;
padding: 16px 20px;
background: #1e1e2e;
border-radius: 10px;
overflow-x: auto;
}
.docs-content :deep(pre code) {
background: transparent;
padding: 0;
color: #cdd6f4;
font-size: 13px;
line-height: 1.6;
}
.docs-content :deep(table) {
width: 100%;
border-collapse: collapse;
margin: 16px 0;
font-size: 13px;
}
.docs-content :deep(th) {
background: #f9fafb;
padding: 10px 14px;
text-align: left;
font-weight: 600;
color: rgba(0, 0, 0, 0.72);
border-bottom: 2px solid #e5e7eb;
}
.docs-content :deep(td) {
padding: 10px 14px;
border-bottom: 1px solid #f0f0f0;
color: rgba(0, 0, 0, 0.65);
}
.docs-content :deep(tr:hover td) {
background: #fafbff;
}
.docs-content :deep(a) {
color: #4f46e5;
text-decoration: none;
}
.docs-content :deep(a:hover) {
text-decoration: underline;
}
.docs-content :deep(hr) {
border: none;
border-top: 1px solid #f0f0f0;
margin: 28px 0;
}
.docs-content :deep(strong) {
color: rgba(0, 0, 0, 0.85);
font-weight: 600;
}
/* 上下篇导航 */
.docs-nav-footer {
display: flex;
justify-content: space-between;
gap: 16px;
margin-top: 48px;
padding-top: 24px;
border-top: 1px solid #f0f0f0;
}
.nav-footer-item {
flex: 1;
max-width: 50%;
padding: 14px 16px;
border-radius: 10px;
border: 1px solid #f0f0f0;
background: #fff;
cursor: pointer;
transition: all 0.2s;
}
.nav-footer-item:hover {
border-color: #c7d2fe;
box-shadow: 0 2px 12px rgba(79, 70, 229, 0.08);
}
.nav-footer-item.next {
text-align: right;
margin-left: auto;
}
.nav-footer-dir {
display: block;
font-size: 12px;
color: rgba(0, 0, 0, 0.4);
margin-bottom: 4px;
}
.nav-footer-title {
font-size: 13px;
font-weight: 500;
color: #4f46e5;
display: block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* 404 */
.docs-not-found {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
padding: 80px 24px;
min-height: 50vh;
}
.not-found-icon {
font-size: 56px;
margin-bottom: 16px;
}
.docs-not-found h2 {
font-size: 20px;
font-weight: 600;
color: rgba(0, 0, 0, 0.72);
margin: 0 0 8px;
}
.docs-not-found p {
font-size: 14px;
color: rgba(0, 0, 0, 0.4);
margin: 0 0 20px;
}
.back-to-docs {
color: #4f46e5;
font-size: 14px;
text-decoration: none;
padding: 8px 16px;
border-radius: 8px;
border: 1px solid #e0e7ff;
transition: all 0.15s;
}
.back-to-docs:hover {
background: #eef2ff;
}
/* 响应式 */
@media (max-width: 1024px) {
.docs-sidebar {
display: none;
}
.docs-content {
padding: 24px 20px 48px;
}
}
</style>

View File

@@ -1,422 +0,0 @@
<template>
<div class="dev-page">
<div class="page-header">
<div>
<h2 class="page-title">📚 开发文档</h2>
<p class="page-desc">从快速上手到深度定制全面的开发指引与 API 参考</p>
</div>
<a-input-search
v-model:value="searchKeyword"
placeholder="搜索文档..."
style="width: 240px"
allow-clear
/>
</div>
<div class="page-body">
<!-- 精选文档 -->
<div class="featured-section">
<div class="section-label">🚀 推荐开始</div>
<a-row :gutter="[16, 16]">
<a-col :xs="24" :md="8" v-for="item in featuredDocs" :key="item.title">
<div class="featured-card" @click="navigateTo(item.to)">
<div class="featured-badge" :class="item.badgeColor">{{ item.badge }}</div>
<div class="featured-icon">{{ item.icon }}</div>
<h3 class="featured-title">{{ item.title }}</h3>
<p class="featured-desc">{{ item.desc }}</p>
</div>
</a-col>
</a-row>
</div>
<a-row :gutter="[16, 0]" class="mt-5">
<!-- 左侧分类目录 -->
<a-col :xs="24" :lg="6">
<div class="toc-panel">
<div class="toc-header">📂 文档分类</div>
<div
v-for="cat in categories"
:key="cat.key"
class="toc-category"
:class="{ active: activeCategory === cat.key }"
@click="activeCategory = cat.key"
>
<span class="toc-icon">{{ cat.icon }}</span>
<span class="toc-label">{{ cat.label }}</span>
<span class="toc-count">{{ cat.count }}</span>
</div>
</div>
</a-col>
<!-- 右侧文档列表 -->
<a-col :xs="24" :lg="18">
<div class="doc-list">
<div
v-for="doc in filteredDocs"
:key="doc.stem?.path || doc.title"
class="doc-item"
@click="navigateTo(doc.stem?.path || '')"
>
<div class="doc-icon">{{ getCategoryIcon(doc.category) }}</div>
<div class="doc-content">
<div class="doc-title-row">
<span class="doc-title">{{ doc.title }}</span>
</div>
<div class="doc-desc">{{ doc.description }}</div>
<div class="doc-meta">
<span>{{ getCategoryLabel(doc.category) }}</span>
</div>
</div>
<div class="doc-arrow"></div>
</div>
<div v-if="filteredDocs.length === 0" class="empty-state">
<div class="empty-icon">🔍</div>
<div class="empty-title">没有找到相关文档</div>
<div class="empty-desc">换个关键词试试吧</div>
</div>
</div>
</a-col>
</a-row>
</div>
</div>
</template>
<script setup lang="ts">
definePageMeta({ layout: 'developer' })
useHead({ title: '开发文档 - 开发者中心' })
const searchKeyword = ref('')
const activeCategory = ref('all')
const router = useRouter()
// 查询所有文档
const { data: docs } = await useAsyncData('docs-list', () =>
queryCollection('docs')
.order('order', 'ASC')
.all()
)
// 分类定义
const categoryMap: Record<string, { label: string; icon: string }> = {
'getting-started': { label: '快速开始', icon: '⚡' },
'api': { label: 'API 参考', icon: '🔌' },
'ai': { label: 'AI 功能', icon: '🤖' },
'deploy': { label: '部署运维', icon: '🚢' },
}
function getCategoryLabel(cat: string) {
return categoryMap[cat]?.label || cat
}
function getCategoryIcon(cat: string) {
return categoryMap[cat]?.icon || '📄'
}
// 分类列表(带数量)
const categories = computed(() => {
const cats: { key: string; icon: string; label: string; count: number }[] = [
{ key: 'all', icon: '🌐', label: '全部文档', count: docs.value?.length || 0 },
]
for (const [key, val] of Object.entries(categoryMap)) {
const count = docs.value?.filter((d: any) => d.category === key).length || 0
cats.push({ key, icon: val.icon, label: val.label, count })
}
return cats
})
// 精选文档
const featuredDocs = [
{
icon: '⚡',
badge: '入门必读',
badgeColor: 'blue',
title: '5 分钟快速上手',
desc: '安装 SDK获取 API Key发送第一个请求立即体验平台能力。',
to: '/developer/docs/getting-started/quickstart',
},
{
icon: '🤖',
badge: 'AI 推荐',
badgeColor: 'purple',
title: 'AI 智能体接入',
desc: '集成 AI Agent实现知识库问答、工作流触发与多模型切换。',
to: '/developer/docs/ai/agent',
},
{
icon: '🚢',
badge: '生产就绪',
badgeColor: 'green',
title: '私有化部署指南',
desc: 'Docker Compose 一键部署HTTPS 配置、备份策略与版本升级。',
to: '/developer/docs/deploy/private-deploy',
},
]
// 过滤文档
const filteredDocs = computed(() => {
let list = docs.value || []
if (activeCategory.value !== 'all') {
list = list.filter((d: any) => d.category === activeCategory.value)
}
const kw = searchKeyword.value.trim().toLowerCase()
if (kw) {
list = list.filter(
(d: any) =>
(d.title as string)?.toLowerCase().includes(kw) ||
(d.description as string)?.toLowerCase().includes(kw)
)
}
return list
})
function navigateTo(path: string) {
// Nuxt Content 的 path 是 /docs/xxx需要转换为 /developer/docs/xxx
const target = path.replace(/^\/docs/, '/developer/docs')
router.push(target)
}
</script>
<style scoped>
.dev-page { min-height: 100%; }
.page-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 24px 28px 16px;
border-bottom: 1px solid #f0f0f0;
gap: 16px;
}
.page-title {
font-size: 20px;
font-weight: 700;
color: rgba(0, 0, 0, 0.88);
margin: 0 0 4px;
}
.page-desc {
font-size: 14px;
color: rgba(0, 0, 0, 0.45);
margin: 0;
}
.page-body {
padding: 20px 24px 28px;
}
/* 精选文档 */
.featured-section { margin-bottom: 0; }
.section-label {
font-size: 13px;
font-weight: 600;
color: rgba(0, 0, 0, 0.5);
margin-bottom: 12px;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.featured-card {
padding: 20px;
border-radius: 12px;
border: 1px solid #f0f0f0;
background: #fff;
cursor: pointer;
transition: all 0.2s;
position: relative;
overflow: hidden;
}
.featured-card:hover {
border-color: #c7d2fe;
box-shadow: 0 4px 20px rgba(79, 70, 229, 0.1);
transform: translateY(-2px);
}
.featured-badge {
position: absolute;
top: 0;
right: 0;
padding: 4px 10px;
font-size: 11px;
font-weight: 600;
border-radius: 0 12px 0 10px;
}
.featured-badge.blue { background: #eff6ff; color: #3b82f6; }
.featured-badge.purple { background: #f5f3ff; color: #7c3aed; }
.featured-badge.green { background: #f0fdf4; color: #16a34a; }
.featured-icon {
font-size: 32px;
margin-bottom: 12px;
}
.featured-title {
font-size: 15px;
font-weight: 700;
color: rgba(0, 0, 0, 0.85);
margin: 0 0 8px;
}
.featured-desc {
font-size: 13px;
color: rgba(0, 0, 0, 0.5);
line-height: 1.6;
margin: 0 0 12px;
}
/* 目录面板 */
.toc-panel {
background: #fff;
border: 1px solid #f0f0f0;
border-radius: 12px;
overflow: hidden;
}
.toc-header {
padding: 14px 16px;
font-size: 13px;
font-weight: 600;
color: rgba(0, 0, 0, 0.55);
text-transform: uppercase;
letter-spacing: 0.05em;
border-bottom: 1px solid #f5f5f5;
}
.toc-category {
display: flex;
align-items: center;
gap: 8px;
padding: 10px 16px;
cursor: pointer;
transition: all 0.15s;
border-bottom: 1px solid #f9f9f9;
}
.toc-category:last-child { border-bottom: none; }
.toc-category:hover { background: #f5f7ff; }
.toc-category.active {
background: #f0f0ff;
color: #4f46e5;
}
.toc-icon { font-size: 16px; flex-shrink: 0; }
.toc-label {
flex: 1;
font-size: 13px;
color: rgba(0, 0, 0, 0.72);
}
.toc-category.active .toc-label {
color: #4f46e5;
font-weight: 500;
}
.toc-count {
font-size: 11px;
background: #f0f0f0;
color: rgba(0, 0, 0, 0.45);
padding: 1px 7px;
border-radius: 20px;
}
.toc-category.active .toc-count {
background: #e0e7ff;
color: #4f46e5;
}
/* 文档列表 */
.doc-list {
background: #fff;
border: 1px solid #f0f0f0;
border-radius: 12px;
overflow: hidden;
}
.doc-item {
display: flex;
align-items: center;
gap: 14px;
padding: 16px 18px;
border-bottom: 1px solid #f9f9f9;
cursor: pointer;
transition: all 0.15s;
}
.doc-item:last-child { border-bottom: none; }
.doc-item:hover { background: #f9faff; }
.doc-icon {
font-size: 24px;
flex-shrink: 0;
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
background: #f5f5f5;
border-radius: 10px;
}
.doc-content { flex: 1; min-width: 0; }
.doc-title-row {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 4px;
}
.doc-title {
font-size: 14px;
font-weight: 500;
color: rgba(0, 0, 0, 0.85);
}
.doc-desc {
font-size: 12px;
color: rgba(0, 0, 0, 0.4);
margin-bottom: 6px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.doc-meta {
display: flex;
align-items: center;
gap: 6px;
font-size: 11px;
color: rgba(0, 0, 0, 0.35);
}
.doc-arrow {
font-size: 20px;
color: rgba(0, 0, 0, 0.2);
flex-shrink: 0;
transition: all 0.15s;
}
.doc-item:hover .doc-arrow {
color: #4f46e5;
transform: translateX(3px);
}
/* 空状态 */
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
padding: 48px 24px;
text-align: center;
}
.empty-icon { font-size: 40px; margin-bottom: 10px; }
.empty-title { font-size: 15px; font-weight: 600; color: rgba(0, 0, 0, 0.7); }
.empty-desc { font-size: 13px; color: rgba(0, 0, 0, 0.4); margin-top: 4px; }
</style>

View File

@@ -1,524 +0,0 @@
<template>
<div class="dev-page">
<div class="page-header">
<div>
<h2 class="page-title">🐙 Git 账号绑定</h2>
<p class="page-desc">绑定你的 Gitea 账号用于申请源码仓库访问权限</p>
</div>
</div>
<div class="page-body">
<!-- 绑定说明 -->
<div class="info-banner">
<div class="info-icon">💡</div>
<div class="info-content">
<div class="info-title">什么是 Gitea 账号绑定</div>
<div class="info-desc">
平台使用 Gitea 私有 Git 服务管理源码仓库绑定账号后运营人员可将你加入对应仓库的访问组
之后你即可通过 Git 克隆完整源代码进行本地开发与私有化部署
</div>
</div>
</div>
<a-row :gutter="[20, 20]">
<!-- 绑定表单 -->
<a-col :xs="24" :lg="14">
<div class="panel">
<div class="panel-header">
<span class="panel-title">📝 填写 Git 信息</span>
<div v-if="loading">
<a-spin size="small" />
</div>
<div v-else>
<a-tag v-if="gitAccountStatus" :color="getStatusColor(gitAccountStatus.status)">
{{ getStatusText(gitAccountStatus.status) }}
</a-tag>
<a-tag v-else-if="isSaved" color="green"> 已保存</a-tag>
</div>
</div>
<div class="panel-body">
<a-form layout="vertical" :model="form">
<a-form-item label="Gitea 用户名" required>
<a-input
v-model:value="form.username"
placeholder="例如lily"
:prefix-slot="true"
size="large"
>
<template #prefix>🐙</template>
</a-input>
<div class="form-hint">你在平台 Gitea 上注册的用户名非邮箱</div>
</a-form-item>
<a-form-item label="联系邮箱(可选)">
<a-input
v-model:value="form.email"
placeholder="用于接收审核通知"
size="large"
>
<template #prefix>📧</template>
</a-input>
</a-form-item>
<a-form-item label="备注(可选)">
<a-textarea
v-model:value="form.remark"
:rows="3"
placeholder="例如:公司名称、项目名称、联系方式等"
:maxlength="200"
show-count
/>
</a-form-item>
<a-form-item>
<a-space>
<a-button
type="primary"
size="large"
:loading="saving"
:disabled="!form.username.trim()"
@click="handleSave"
>
💾 保存绑定信息
</a-button>
<a-button
size="large"
type="default"
@click="navigateTo('/developer/requests')"
>
📋 查看申请记录
</a-button>
</a-space>
</a-form-item>
</a-form>
<!-- 状态信息 -->
<div v-if="gitAccountStatus && gitAccountStatus.status !== 'not_bound'" class="status-info">
<div class="status-info-header">📋 当前状态信息</div>
<div class="status-info-content">
<div v-if="gitAccountStatus.lastUpdatedAt" class="status-item">
<span class="status-label">上次更新</span>
<span class="status-value">{{ gitAccountStatus.lastUpdatedAt }}</span>
</div>
<div v-if="gitAccountStatus.verificationNote" class="status-item">
<span class="status-label">审核备注</span>
<span class="status-value">{{ gitAccountStatus.verificationNote }}</span>
</div>
<div v-if="gitAccountStatus.status === 'rejected'" class="status-warning">
绑定被拒绝请根据备注修改信息后重新提交
</div>
<div v-if="gitAccountStatus.status === 'verified'" class="status-success">
绑定已成功现在可以提交仓库访问申请了
</div>
</div>
<!-- 绑定成功后显示申请权限按钮 -->
<div v-if="gitAccountStatus.status === 'verified'" class="action-buttons">
<a-button
type="primary"
block
size="large"
@click="navigateTo('/developer/requests')"
>
🚀 前往申请仓库权限
</a-button>
</div>
</div>
</div>
</div>
</a-col>
<!-- 操作步骤说明 -->
<a-col :xs="24" :lg="10">
<div class="panel">
<div class="panel-header">
<span class="panel-title">📌 操作步骤</span>
</div>
<div class="steps-list">
<div v-for="(step, i) in howToSteps" :key="i" class="how-step">
<div class="how-step-num">{{ i + 1 }}</div>
<div class="how-step-text">
<div class="how-step-title">{{ step.title }}</div>
<div class="how-step-desc">{{ step.desc }}</div>
</div>
</div>
</div>
</div>
<!-- Gitea 注册入口 -->
<div class="panel mt-4">
<div class="panel-header">
<span class="panel-title">🚀 还没有 Gitea 账号</span>
</div>
<div class="register-hint">
<p class="register-desc">
前往平台 Gitea 注册账号注册完成后将用户名填入上方表单
</p>
<a-button type="primary" ghost block @click="openGitea">
前往 Gitea 注册
</a-button>
</div>
</div>
</a-col>
</a-row>
</div>
</div>
</template>
<script setup lang="ts">
import { message } from 'ant-design-vue'
import { saveGitAccount, getGitAccountStatus, getGiteaServerInfo } from '@/api/developer'
definePageMeta({ layout: 'developer' })
useHead({ title: 'Git 账号绑定 - 开发者中心' })
const saving = ref(false)
const loading = ref(false)
const isSaved = ref(false)
const gitAccountStatus = ref<any>(null)
const giteaInfo = ref<any>(null)
const form = reactive({
username: '',
email: '',
remark: '',
})
// 加载Git账号绑定状态
async function loadGitAccountStatus() {
loading.value = true
try {
const res = await getGitAccountStatus()
if (res.data.code === 200) {
gitAccountStatus.value = res.data.data
if (gitAccountStatus.value.status !== 'not_bound' && gitAccountStatus.value.username) {
form.username = gitAccountStatus.value.username
form.email = gitAccountStatus.value.email || ''
form.remark = gitAccountStatus.value.remark || ''
isSaved.value = true
}
}
} catch (error) {
console.error('加载Git账号状态失败:', error)
} finally {
loading.value = false
}
}
// 加载Gitea服务器信息
async function loadGiteaInfo() {
try {
const res = await getGiteaServerInfo()
if (res.data.code === 200) {
giteaInfo.value = res.data.data
}
} catch (error) {
console.error('加载Gitea服务器信息失败:', error)
}
}
async function handleSave() {
if (!form.username.trim()) {
message.error('请填写 Gitea 用户名')
return
}
saving.value = true
try {
const res = await saveGitAccount({
username: form.username.trim(),
email: form.email.trim() || undefined,
remark: form.remark.trim() || undefined
})
if (res.data.code === 200) {
isSaved.value = true
message.success(res.data.message || 'Git 账号绑定成功')
// 重新加载状态
await loadGitAccountStatus()
} else {
message.error(res.data.message || '保存失败,请稍后重试')
}
} catch (error: any) {
console.error('保存Git账号信息失败:', error)
if (error.response?.status === 400) {
message.error('用户名格式不正确')
} else if (error.response?.status === 409) {
message.error('该用户名已被其他用户绑定')
} else {
message.error('保存失败,请检查网络连接后重试')
}
} finally {
saving.value = false
}
}
function openGitea() {
if (import.meta.client) {
const url = giteaInfo.value?.url || 'https://git.websoft.top'
window.open(url, '_blank', 'noopener,noreferrer')
}
}
// 获取状态标签颜色
function getStatusColor(status: string) {
const colors: Record<string, string> = {
pending: 'orange',
verified: 'green',
rejected: 'red',
not_bound: 'default'
}
return colors[status] || 'default'
}
// 获取状态标签文本
function getStatusText(status: string) {
const texts: Record<string, string> = {
pending: '待审核',
verified: '已通过',
rejected: '已拒绝',
not_bound: '未绑定'
}
return texts[status] || status
}
const howToSteps = [
{
title: '注册 Gitea 账号',
desc: '访问平台 Gitea使用邮箱注册一个账号。',
},
{
title: '填写用户名并保存',
desc: '在左侧表单填写你的 Gitea 用户名,点击保存。',
},
{
title: '申请仓库访问权限',
desc: '绑定成功后,前往权限申请页面提交仓库访问申请。',
},
{
title: '获得仓库访问权限',
desc: '申请通过后,通过 Gitea 即可克隆对应仓库。',
},
]
// 页面初始化
onMounted(async () => {
await Promise.all([
loadGitAccountStatus(),
loadGiteaInfo()
])
})
</script>
<style scoped>
.dev-page { min-height: 100%; }
.page-header {
display: flex;
align-items: flex-start;
justify-content: space-between;
padding: 24px 28px 16px;
border-bottom: 1px solid #f0f0f0;
gap: 16px;
}
.page-title {
font-size: 20px;
font-weight: 700;
color: rgba(0, 0, 0, 0.88);
margin: 0 0 4px;
}
.page-desc {
font-size: 14px;
color: rgba(0, 0, 0, 0.45);
margin: 0;
}
.page-body {
padding: 20px 24px 28px;
}
/* 说明横幅 */
.info-banner {
display: flex;
gap: 14px;
padding: 16px 18px;
background: linear-gradient(135deg, #eff6ff 0%, #f0f9ff 100%);
border-radius: 12px;
border: 1px solid #bfdbfe;
margin-bottom: 20px;
}
.info-icon { font-size: 22px; flex-shrink: 0; }
.info-title {
font-size: 14px;
font-weight: 600;
color: #1d4ed8;
margin-bottom: 4px;
}
.info-desc {
font-size: 13px;
color: #3b82f6;
line-height: 1.6;
}
.panel {
background: #fff;
border: 1px solid #f0f0f0;
border-radius: 12px;
overflow: hidden;
}
.panel-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 14px 18px;
border-bottom: 1px solid #f5f5f5;
}
.panel-title {
font-size: 14px;
font-weight: 600;
color: rgba(0, 0, 0, 0.85);
}
.panel-body {
padding: 20px 20px 10px;
}
.form-hint {
font-size: 12px;
color: rgba(0, 0, 0, 0.38);
margin-top: 5px;
}
/* 步骤列表 */
.steps-list {
padding: 14px 18px;
}
.how-step {
display: flex;
gap: 12px;
padding: 10px 0;
border-bottom: 1px solid #f9f9f9;
}
.how-step:last-child { border-bottom: none; }
.how-step-num {
width: 24px;
height: 24px;
flex-shrink: 0;
border-radius: 50%;
background: linear-gradient(135deg, #4f46e5, #7c3aed);
color: #fff;
font-size: 12px;
font-weight: 700;
display: flex;
align-items: center;
justify-content: center;
margin-top: 1px;
}
.how-step-title {
font-size: 13px;
font-weight: 500;
color: rgba(0, 0, 0, 0.85);
margin-bottom: 3px;
}
.how-step-desc {
font-size: 12px;
color: rgba(0, 0, 0, 0.4);
line-height: 1.5;
}
/* Gitea 注册 */
.register-hint {
padding: 16px 18px;
}
.register-desc {
font-size: 13px;
color: rgba(0, 0, 0, 0.5);
margin: 0 0 14px;
line-height: 1.6;
}
/* 状态信息 */
.status-info {
margin-top: 20px;
padding: 14px;
background: #f9fafb;
border-radius: 8px;
border: 1px solid #e5e7eb;
}
.status-info-header {
font-size: 13px;
font-weight: 600;
color: rgba(0, 0, 0, 0.85);
margin-bottom: 10px;
padding-bottom: 8px;
border-bottom: 1px solid #e5e7eb;
}
.status-info-content {
display: flex;
flex-direction: column;
gap: 8px;
}
.status-item {
display: flex;
align-items: flex-start;
gap: 8px;
font-size: 12px;
}
.status-label {
color: rgba(0, 0, 0, 0.45);
min-width: 60px;
flex-shrink: 0;
}
.status-value {
color: rgba(0, 0, 0, 0.65);
flex: 1;
word-break: break-word;
}
.status-warning {
font-size: 12px;
color: #dc2626;
background: #fef2f2;
padding: 8px 12px;
border-radius: 6px;
margin-top: 8px;
border: 1px solid #fecaca;
}
.status-success {
font-size: 12px;
color: #16a34a;
background: #f0fdf4;
padding: 8px 12px;
border-radius: 6px;
margin-top: 8px;
border: 1px solid #bbf7d0;
}
/* 操作按钮区域 */
.action-buttons {
margin-top: 16px;
padding-top: 16px;
border-top: 1px solid #e5e7eb;
}
</style>

View File

@@ -1,845 +0,0 @@
<template>
<div class="dev-page">
<!-- 顶部欢迎横幅 -->
<div class="dev-hero">
<div class="dev-hero-content">
<div class="dev-hero-left">
<div class="dev-hero-greeting">🛠 欢迎回来{{ userDisplayName }}</div>
<h1 class="dev-hero-title">开发者控制台</h1>
<p class="dev-hero-desc">管理你的应用API Key 和源码权限快速构建 AI 原生产品</p>
<a-space class="mt-4">
<a-button type="primary" @click="navigateTo('/developer/apikeys')">
🔑 获取 API Key
</a-button>
<a-button @click="navigateTo('/developer-center')">📖 查看文档</a-button>
</a-space>
</div>
<div class="dev-hero-right">
<div class="dev-code-snippet">
<div class="code-header">
<div class="code-dots">
<span class="dot red" /><span class="dot yellow" /><span class="dot green" />
</div>
<span class="code-filename">quickstart.ts</span>
</div>
<pre class="code-body"><code><span class="c">// 初始化 SDK</span>
<span class="kw">import</span> { <span class="cls">WebsopyClient</span> } <span class="kw">from</span> <span class="str">'@websopy/sdk'</span>
<span class="kw">const</span> <span class="var">client</span> = <span class="kw">new</span> <span class="cls">WebsopyClient</span>({
<span class="prop">apiKey</span>: <span class="str">'sk-xxxxxxxxxxxxxxxx'</span>
})
<span class="c">// 调用 AI 智能体</span>
<span class="kw">const</span> <span class="var">reply</span> = <span class="kw">await</span> <span class="var">client</span>.<span class="fn">agent</span>.<span class="fn">chat</span>({
<span class="prop">message</span>: <span class="str">'帮我分析本月销售数据'</span>
})</code></pre>
</div>
</div>
</div>
</div>
<div class="dev-body">
<!-- 数据统计卡片头部 -->
<div class="mb-4 flex items-center justify-between">
<div class="flex items-center gap-2">
<h3 class="text-base font-semibold text-gray-800">📊 数据概览</h3>
<a-tag v-if="!loading && statsData.totalUsage > 0" color="blue">
本月调用: {{ statsData.totalUsage }}
</a-tag>
<a-tag v-if="!loading && statsData.activeKeys > 0" color="green">
活跃 Key: {{ statsData.activeKeys }}
</a-tag>
</div>
<a-button size="small" :loading="loading" @click="loadStats">
<template #icon><reload-outlined /></template>
刷新数据
</a-button>
</div>
<!-- 数据统计卡片 -->
<a-row :gutter="[16, 16]" class="mb-6">
<a-col :xs="12" :md="6" v-for="stat in stats" :key="stat.label">
<a-spin :spinning="loading">
<div class="stat-card" :class="stat.color">
<div class="stat-icon">{{ stat.icon }}</div>
<div class="stat-info">
<div class="stat-value">{{ stat.value }}</div>
<div class="stat-label">{{ stat.label }}</div>
</div>
</div>
</a-spin>
</a-col>
</a-row>
<a-row :gutter="[16, 16]">
<!-- 快捷入口 -->
<a-col :xs="24" :lg="14">
<div class="panel">
<div class="panel-header">
<span class="panel-title"> 快捷入口</span>
</div>
<div class="quick-entries">
<div
v-for="entry in quickEntries"
:key="entry.label"
class="quick-entry"
@click="navigateTo(entry.to)"
>
<div class="quick-entry-icon" :class="entry.iconClass">{{ entry.icon }}</div>
<div class="quick-entry-text">
<div class="quick-entry-label">{{ entry.label }}</div>
<div class="quick-entry-desc">{{ entry.desc }}</div>
</div>
<div class="quick-entry-arrow"></div>
</div>
</div>
</div>
<!-- 最近动态时间线 -->
<div class="panel mt-4">
<div class="panel-header">
<span class="panel-title">🕐 最近动态</span>
<a-button size="small" type="link" @click="navigateTo('/developer/apps')">查看全部</a-button>
</div>
<div class="timeline-list">
<div v-if="recentActivities.length === 0" class="timeline-empty">
<div class="empty-icon">🗒</div>
<div class="empty-text">暂无操作记录</div>
</div>
<div v-for="(act, i) in recentActivities" :key="i" class="timeline-item">
<div class="timeline-dot" :class="act.type" />
<div class="timeline-line" v-if="i < recentActivities.length - 1" />
<div class="timeline-body">
<div class="timeline-title">{{ act.title }}</div>
<div class="timeline-meta">
<span class="timeline-app">{{ act.app }}</span>
<span class="timeline-sep">·</span>
<span class="timeline-time">{{ act.time }}</span>
</div>
</div>
</div>
</div>
</div>
</a-col>
<!-- 最新动态 / 开发者公告 -->
<a-col :xs="24" :lg="10">
<div class="panel">
<div class="panel-header">
<span class="panel-title">📢 开发者公告</span>
<a-tag color="red">NEW</a-tag>
</div>
<div class="notice-list">
<div v-for="notice in notices" :key="notice.title" class="notice-item">
<div class="notice-dot" :class="notice.type" />
<div class="notice-content">
<div class="notice-title">{{ notice.title }}</div>
<div class="notice-date">{{ notice.date }}</div>
</div>
</div>
</div>
</div>
<!-- 快速帮助 -->
<div class="panel mt-4">
<div class="panel-header">
<span class="panel-title">🆘 快速帮助</span>
</div>
<div class="help-links">
<a
v-for="link in helpLinks"
:key="link.label"
class="help-link"
@click="navigateTo(link.to)"
>
<span>{{ link.icon }}</span> {{ link.label }}
</a>
</div>
</div>
<!-- 服务状态 -->
<div class="panel mt-4">
<div class="panel-header">
<span class="panel-title">📡 服务状态</span>
<a-tag color="green"> 全部正常</a-tag>
</div>
<div class="srv-list">
<div v-for="srv in serviceStatus" :key="srv.name" class="srv-item">
<div class="srv-dot" :class="srv.status" />
<span class="srv-name">{{ srv.name }}</span>
<span class="srv-latency">{{ srv.latency }}</span>
</div>
</div>
</div>
</a-col>
</a-row>
<!-- SDK 支持状态 -->
<div class="panel mt-4">
<div class="panel-header">
<span class="panel-title">📦 SDK 支持状态</span>
<a-button size="small" type="link" @click="navigateTo('/developer/docs')">查看文档</a-button>
</div>
<div class="sdk-grid">
<div v-for="sdk in sdkStatus" :key="sdk.lang" class="sdk-item">
<span class="sdk-emoji">{{ sdk.emoji }}</span>
<div class="sdk-info">
<div class="sdk-lang">{{ sdk.lang }}</div>
<div class="sdk-desc">{{ sdk.desc }}</div>
</div>
<a-tag :color="sdk.tagColor" class="sdk-tag">{{ sdk.status }}</a-tag>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { getUserInfo } from '@/api/layout'
import { getToken } from '@/utils/token-util'
import { setAuthzFromUser } from '@/utils/permission'
// TODO: 后端接口就绪后解除注释
// import { getDeveloperStats } from '@/api/developer'
import { ReloadOutlined } from '@ant-design/icons-vue'
import type { User } from '@/api/system/user/model'
definePageMeta({ layout: 'developer' })
useHead({ title: '概览 - 开发者中心' })
const user = ref<User | null>(null)
const loading = ref(false)
const statsData = ref({
appCount: 0,
apiKeyCount: 0,
pendingRequests: 0,
repositoryAccess: 0,
totalUsage: 0,
activeKeys: 0
})
const userDisplayName = computed(() => {
const u = user.value
return u?.nickname?.trim() || u?.username?.trim() || u?.phone?.trim() || '开发者'
})
// 加载统计数据TODO: 后端接口就绪后替换 Mock
const loadStats = async () => {
loading.value = true
try {
// Mock 数据,后端接口就绪后替换为:
// const response = await getDeveloperStats()
// if (response.data?.success) { statsData.value = response.data.data }
statsData.value = { appCount: 12, apiKeyCount: 8, pendingRequests: 3, repositoryAccess: 5, totalUsage: 456200, activeKeys: 6 }
} catch (error) {
console.error('加载统计数据失败:', error)
} finally {
loading.value = false
}
}
onMounted(async () => {
const token = getToken()
if (!token) return
try {
const me = await getUserInfo()
user.value = me
setAuthzFromUser(me)
// 加载统计数据
await loadStats()
} catch { /* ignore */ }
})
const stats = computed(() => [
{
icon: '📦',
label: '可开发应用',
value: statsData.value.appCount > 0 ? statsData.value.appCount.toString() : '-',
color: 'blue'
},
{
icon: '🔑',
label: 'API Key 数量',
value: statsData.value.apiKeyCount > 0 ? statsData.value.apiKeyCount.toString() : '-',
color: 'purple'
},
{
icon: '📋',
label: '待处理申请',
value: statsData.value.pendingRequests > 0 ? statsData.value.pendingRequests.toString() : '-',
color: 'orange'
},
{
icon: '💻',
label: '已访问仓库',
value: statsData.value.repositoryAccess > 0 ? statsData.value.repositoryAccess.toString() : '-',
color: 'green'
},
])
const quickEntries = [
{
icon: '🔑', iconClass: 'purple',
label: 'API Key 管理',
desc: '创建、查看和管理你的 API Key',
to: '/developer/apikeys',
},
{
icon: '📦', iconClass: 'blue',
label: '应用中心',
desc: '查看订阅的应用与后台入口',
to: '/developer/apps',
},
{
icon: '💻', iconClass: 'cyan',
label: '源码与仓库',
desc: '申请仓库权限,获取完整源代码',
to: '/developer/source',
},
{
icon: '📚', iconClass: 'orange',
label: '开发文档',
desc: 'API 参考、SDK 使用、AI 功能与部署指南',
to: '/developer/docs',
},
{
icon: '🐙', iconClass: 'gray',
label: 'Git 账号绑定',
desc: '绑定 Gitea 账号,获取仓库访问权限',
to: '/developer/git',
},
{
icon: '💬', iconClass: 'green',
label: '支持与反馈',
desc: '遇到问题?提交工单或联系我们',
to: '/developer/support',
},
]
const notices = [
{ type: 'blue', title: 'TypeScript SDK v1.2.0 正式发布', date: '2026-03-25' },
{ type: 'green', title: 'OpenAPI 3.0 文档已全面更新', date: '2026-03-20' },
{ type: 'orange', title: 'AI Agent API 新增流式输出支持', date: '2026-03-15' },
{ type: 'gray', title: 'API Rate Limit 规则调整公告', date: '2026-03-10' },
]
const helpLinks = [
{ icon: '📘', label: '快速开始指南', to: '/developer/docs/getting-started/quickstart' },
{ icon: '🔌', label: 'REST API 参考', to: '/developer/docs/api/rest-api' },
{ icon: '🐙', label: '绑定 Git 账号', to: '/developer/git' },
{ icon: '📋', label: '权限申请流程', to: '/developer/source' },
]
const sdkStatus = [
{ emoji: '🟦', lang: 'TypeScript / JavaScript', desc: '官方维护,完整类型定义', status: '稳定版', tagColor: 'green' },
{ emoji: '🐍', lang: 'Python', desc: '支持 asyncio适合 AI 场景', status: '稳定版', tagColor: 'green' },
{ emoji: '☕', lang: 'Java', desc: '企业级 Spring Boot 接入', status: 'Beta', tagColor: 'orange' },
{ emoji: '🐹', lang: 'Go', desc: '高性能微服务场景', status: 'Beta', tagColor: 'orange' },
{ emoji: '🐘', lang: 'PHP', desc: 'Laravel / ThinkPHP 框架', status: '规划中', tagColor: 'default' },
]
// 最近动态(可替换为真实 API 数据)
const recentActivities = [
{ type: 'blue', title: '创建了 API Key', app: '人事管理系统', time: '10 分钟前' },
{ type: 'green', title: '应用上架审核通过', app: '电商平台', time: '2 小时前' },
{ type: 'orange', title: '提交了源码访问申请', app: '全局', time: '昨天 14:30' },
{ type: 'purple', title: '发布了新版本 v1.2.0', app: 'OA 系统', time: '2天前' },
{ type: 'gray', title: '绑定了 Gitea 账号', app: '全局', time: '3天前' },
]
// 服务状态
const serviceStatus = [
{ name: 'REST API', status: 'ok', latency: '28ms' },
{ name: 'AI Agent API', status: 'ok', latency: '312ms' },
{ name: 'Gitea 仓库', status: 'ok', latency: '45ms' },
{ name: 'SDK CDN', status: 'ok', latency: '18ms' },
{ name: 'Webhook', status: 'ok', latency: '—' },
]
</script>
<style scoped>
.dev-page {
min-height: 100%;
}
/* Hero 区域 */
.dev-hero {
background: linear-gradient(135deg, #1e1b4b 0%, #312e81 40%, #1e3a5f 100%);
padding: 32px 28px 28px;
position: relative;
overflow: hidden;
}
.dev-hero::before {
content: '';
position: absolute;
top: -80px;
right: -80px;
width: 300px;
height: 300px;
border-radius: 50%;
background: rgba(99, 102, 241, 0.2);
filter: blur(60px);
}
.dev-hero::after {
content: '';
position: absolute;
bottom: -60px;
left: 60px;
width: 200px;
height: 200px;
border-radius: 50%;
background: rgba(139, 92, 246, 0.15);
filter: blur(50px);
}
.dev-hero-content {
position: relative;
z-index: 1;
display: flex;
align-items: center;
gap: 24px;
}
.dev-hero-left {
flex: 1;
}
.dev-hero-greeting {
color: rgba(165, 180, 252, 0.9);
font-size: 14px;
margin-bottom: 8px;
}
.dev-hero-title {
color: #fff;
font-size: 26px;
font-weight: 700;
margin: 0 0 8px;
line-height: 1.3;
}
.dev-hero-desc {
color: rgba(199, 210, 254, 0.8);
font-size: 14px;
margin: 0;
line-height: 1.6;
}
.dev-hero-right {
flex-shrink: 0;
width: 360px;
}
@media (max-width: 900px) {
.dev-hero-right { display: none; }
}
/* 代码片段 */
.dev-code-snippet {
border-radius: 12px;
overflow: hidden;
border: 1px solid rgba(255, 255, 255, 0.12);
background: rgba(15, 15, 35, 0.8);
backdrop-filter: blur(10px);
}
.code-header {
display: flex;
align-items: center;
gap: 8px;
padding: 10px 14px;
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
background: rgba(0, 0, 0, 0.2);
}
.code-dots { display: flex; gap: 5px; }
.dot {
width: 10px; height: 10px; border-radius: 50%;
}
.dot.red { background: #ff5f57; }
.dot.yellow { background: #febc2e; }
.dot.green { background: #28c840; }
.code-filename {
font-family: monospace;
font-size: 12px;
color: rgba(255, 255, 255, 0.4);
}
.code-body {
padding: 14px 16px;
margin: 0;
font-family: 'Fira Code', 'JetBrains Mono', monospace;
font-size: 12px;
line-height: 1.7;
overflow-x: auto;
color: #e2e8f0;
}
.code-body .c { color: #64748b; }
.code-body .kw { color: #93c5fd; }
.code-body .cls { color: #fde68a; }
.code-body .str { color: #86efac; }
.code-body .var { color: #a5b4fc; }
.code-body .prop { color: #cbd5e1; }
.code-body .fn { color: #fde68a; }
/* 主体内容 */
.dev-body {
padding: 20px 24px 24px;
}
/* 统计卡片 */
.stat-card {
display: flex;
align-items: center;
gap: 12px;
padding: 16px;
border-radius: 12px;
border: 1px solid transparent;
transition: all 0.2s;
}
.stat-card:hover { transform: translateY(-1px); }
.stat-card.blue { background: #eff6ff; border-color: #dbeafe; }
.stat-card.purple { background: #f5f3ff; border-color: #e9d5ff; }
.stat-card.orange { background: #fff7ed; border-color: #fed7aa; }
.stat-card.green { background: #f0fdf4; border-color: #bbf7d0; }
.stat-icon {
font-size: 28px;
flex-shrink: 0;
}
.stat-value {
font-size: 22px;
font-weight: 700;
color: rgba(0, 0, 0, 0.85);
line-height: 1.2;
}
.stat-label {
font-size: 12px;
color: rgba(0, 0, 0, 0.45);
margin-top: 2px;
}
/* 面板通用 */
.panel {
background: #fff;
border: 1px solid #f0f0f0;
border-radius: 12px;
overflow: hidden;
}
.panel-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 14px 18px;
border-bottom: 1px solid #f5f5f5;
}
.panel-title {
font-size: 14px;
font-weight: 600;
color: rgba(0, 0, 0, 0.85);
}
/* 快捷入口 */
.quick-entries {
padding: 8px;
display: grid;
grid-template-columns: 1fr 1fr;
gap: 6px;
}
.quick-entry {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 14px;
border-radius: 10px;
cursor: pointer;
transition: all 0.15s;
border: 1px solid transparent;
}
.quick-entry:hover {
background: #f5f7ff;
border-color: #e0e7ff;
}
.quick-entry-icon {
width: 36px;
height: 36px;
flex-shrink: 0;
border-radius: 9px;
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
}
.quick-entry-icon.blue { background: #eff6ff; }
.quick-entry-icon.purple { background: #f5f3ff; }
.quick-entry-icon.cyan { background: #ecfeff; }
.quick-entry-icon.orange { background: #fff7ed; }
.quick-entry-icon.gray { background: #f9fafb; }
.quick-entry-icon.green { background: #f0fdf4; }
.quick-entry-text { flex: 1; min-width: 0; }
.quick-entry-label {
font-size: 13px;
font-weight: 500;
color: rgba(0, 0, 0, 0.85);
margin-bottom: 2px;
}
.quick-entry-desc {
font-size: 12px;
color: rgba(0, 0, 0, 0.4);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.quick-entry-arrow {
color: rgba(0, 0, 0, 0.25);
font-size: 14px;
flex-shrink: 0;
transition: transform 0.15s;
}
.quick-entry:hover .quick-entry-arrow {
color: #4f46e5;
transform: translateX(3px);
}
/* 公告列表 */
.notice-list {
padding: 8px 16px;
}
.notice-item {
display: flex;
align-items: flex-start;
gap: 10px;
padding: 10px 0;
border-bottom: 1px solid #f9f9f9;
}
.notice-item:last-child { border-bottom: none; }
.notice-dot {
width: 8px;
height: 8px;
border-radius: 50%;
flex-shrink: 0;
margin-top: 5px;
}
.notice-dot.blue { background: #4f46e5; }
.notice-dot.green { background: #16a34a; }
.notice-dot.orange { background: #ea580c; }
.notice-dot.gray { background: #9ca3af; }
.notice-title {
font-size: 13px;
color: rgba(0, 0, 0, 0.75);
line-height: 1.4;
}
.notice-date {
font-size: 11px;
color: rgba(0, 0, 0, 0.35);
margin-top: 2px;
}
/* 帮助链接 */
.help-links {
padding: 10px 14px 14px;
display: grid;
grid-template-columns: 1fr 1fr;
gap: 6px;
}
.help-link {
display: flex;
align-items: center;
gap: 6px;
padding: 8px 10px;
border-radius: 8px;
font-size: 13px;
color: rgba(0, 0, 0, 0.65);
cursor: pointer;
transition: all 0.15s;
}
.help-link:hover {
background: #f5f7ff;
color: #4f46e5;
}
/* SDK 状态 */
.sdk-grid {
padding: 12px 16px;
display: grid;
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
gap: 10px;
}
.sdk-item {
display: flex;
align-items: center;
gap: 10px;
padding: 12px 14px;
border-radius: 10px;
border: 1px solid #f0f0f0;
background: #fafafa;
transition: all 0.15s;
}
.sdk-item:hover {
border-color: #e0e7ff;
background: #f5f7ff;
}
.sdk-emoji {
font-size: 24px;
flex-shrink: 0;
}
.sdk-info { flex: 1; min-width: 0; }
.sdk-lang {
font-size: 13px;
font-weight: 500;
color: rgba(0, 0, 0, 0.85);
}
.sdk-desc {
font-size: 12px;
color: rgba(0, 0, 0, 0.4);
margin-top: 2px;
}
.sdk-tag {
flex-shrink: 0;
font-size: 11px;
}
/* 最近动态时间线 */
.timeline-list {
padding: 12px 18px 16px;
position: relative;
}
.timeline-empty {
display: flex;
flex-direction: column;
align-items: center;
padding: 24px;
text-align: center;
}
.timeline-item {
display: flex;
align-items: flex-start;
gap: 12px;
position: relative;
padding-bottom: 14px;
}
.timeline-item:last-child { padding-bottom: 0; }
.timeline-dot {
width: 8px;
height: 8px;
border-radius: 50%;
flex-shrink: 0;
margin-top: 5px;
position: relative;
z-index: 1;
}
.timeline-dot.blue { background: #4f46e5; }
.timeline-dot.green { background: #16a34a; }
.timeline-dot.orange { background: #ea580c; }
.timeline-dot.purple { background: #7c3aed; }
.timeline-dot.gray { background: #9ca3af; }
.timeline-line {
position: absolute;
left: 3px;
top: 14px;
bottom: 0;
width: 2px;
background: #f0f0f0;
}
.timeline-body { flex: 1; min-width: 0; }
.timeline-title {
font-size: 13px;
color: rgba(0, 0, 0, 0.8);
font-weight: 500;
margin-bottom: 3px;
}
.timeline-meta {
display: flex;
align-items: center;
gap: 5px;
font-size: 11px;
color: rgba(0, 0, 0, 0.38);
}
.timeline-sep { opacity: 0.5; }
.empty-icon { font-size: 28px; margin-bottom: 8px; }
.empty-text { font-size: 13px; color: rgba(0, 0, 0, 0.4); }
/* 服务状态 */
.srv-list {
padding: 8px 16px 12px;
}
.srv-item {
display: flex;
align-items: center;
gap: 10px;
padding: 7px 0;
border-bottom: 1px solid #f9f9f9;
}
.srv-item:last-child { border-bottom: none; }
.srv-dot {
width: 7px;
height: 7px;
border-radius: 50%;
flex-shrink: 0;
}
.srv-dot.ok { background: #16a34a; }
.srv-dot.warn { background: #f59e0b; }
.srv-dot.down { background: #dc2626; }
.srv-name {
flex: 1;
font-size: 13px;
color: rgba(0, 0, 0, 0.72);
}
.srv-latency {
font-size: 12px;
color: rgba(0, 0, 0, 0.35);
font-family: monospace;
}
</style>

View File

@@ -1,653 +0,0 @@
<template>
<div class="dev-page">
<!-- 页面头部 -->
<div class="page-header">
<div>
<h2 class="page-title"> 流水线管理</h2>
<p class="page-desc">配置 CI/CD 流水线对接 Gitea CI / Jenkins / GitHub Actions</p>
</div>
<a-space>
<a-button @click="loadPipelines">
<template #icon><ReloadOutlined /></template>
刷新
</a-button>
<a-button type="primary" @click="showPipelineModal = true">
<template #icon><PlusOutlined /></template>
新建流水线
</a-button>
</a-space>
</div>
<!-- 选择应用 -->
<div class="panel mb-4">
<div class="panel-header">
<span class="panel-title">📦 选择应用</span>
</div>
<div class="p-4">
<a-select
v-model:value="selectedAppId"
style="width: 300px"
placeholder="选择要管理流水线的应用"
:loading="loadingApps"
allow-clear
@change="handleAppChange"
>
<a-select-option v-for="app in apps" :key="app.productId" :value="app.productId">
{{ app.productName }}
</a-select-option>
</a-select>
</div>
</div>
<!-- 流水线列表 -->
<div v-if="selectedAppId" class="panel">
<div class="panel-header">
<span class="panel-title"> 流水线列表</span>
<a-tag v-if="pipelines.length > 0">{{ pipelines.length }} 条流水线</a-tag>
</div>
<a-spin :spinning="loading">
<div v-if="pipelines.length > 0" class="pipeline-list">
<div v-for="pipeline in pipelines" :key="pipeline.id" class="pipeline-item">
<div class="pipeline-header">
<div class="pipeline-info">
<span class="pipeline-name">{{ pipeline.name }}</span>
<a-tag :color="pipeline.enabled ? 'green' : 'default'" size="small">
{{ pipeline.enabled ? '启用' : '禁用' }}
</a-tag>
<a-tag size="small">{{ ciTypeText(pipeline.ciType) }}</a-tag>
<a-tag v-if="pipeline.autoDeploy" color="blue" size="small">自动部署</a-tag>
</div>
<div class="pipeline-actions">
<a-switch
:checked="pipeline.enabled"
size="small"
@change="(checked: boolean) => handleToggle(pipeline, checked)"
/>
<a-button type="link" size="small" @click="handleEdit(pipeline)">编辑</a-button>
<a-popconfirm title="确定要删除此流水线?" @confirm="handleDelete(pipeline)">
<a-button danger type="link" size="small">删除</a-button>
</a-popconfirm>
</div>
</div>
<div class="pipeline-body">
<div class="pipeline-desc">{{ pipeline.description || '暂无描述' }}</div>
<div class="pipeline-meta">
<span v-if="pipeline.repoFullName" class="meta-item">
<GithubOutlined /> {{ pipeline.repoFullName }}
</span>
<span v-if="pipeline.workflowFile" class="meta-item">
<FileOutlined /> {{ pipeline.workflowFile }}
</span>
<span class="meta-item">
<BranchesOutlined /> {{ pipeline.defaultBranch || 'main' }}
</span>
<span class="meta-item">
<NodeIndexOutlined /> {{ stagesText(pipeline.stages) }}
</span>
</div>
<div class="pipeline-stats">
<span class="stat-item success">
<CheckCircleOutlined /> {{ pipeline.successCount || 0 }} 成功
</span>
<span class="stat-item failed">
<CloseCircleOutlined /> {{ pipeline.failureCount || 0 }} 失败
</span>
<span v-if="pipeline.lastBuildTime" class="stat-item">
<ClockCircleOutlined /> {{ formatTime(pipeline.lastBuildTime) }}
</span>
</div>
</div>
<div v-if="pipeline.lastBuildStatus" class="pipeline-last-build" :class="pipeline.lastBuildStatus">
最近构建: <span>{{ statusText(pipeline.lastBuildStatus) }}</span>
</div>
</div>
</div>
<a-empty v-else description="暂无流水线配置" class="py-8">
<template #image>
<div class="empty-icon"></div>
</template>
<a-button type="primary" @click="showPipelineModal = true">创建第一条流水线</a-button>
</a-empty>
</a-spin>
</div>
<!-- 未选择应用提示 -->
<div v-else class="panel">
<div class="empty-state">
<div class="empty-icon">📦</div>
<div class="empty-title">请先选择应用</div>
<div class="empty-desc">选择应用后可以查看和管理该应用的 CI/CD 流水线</div>
</div>
</div>
<!-- 新建/编辑流水线弹窗 -->
<a-modal
v-model:open="showPipelineModal"
:title="editingPipeline ? '编辑流水线' : '新建流水线'"
width="600px"
:confirm-loading="submitting"
@ok="handleSubmit"
@cancel="resetForm"
>
<a-form :model="form" layout="vertical">
<a-form-item label="流水线名称" required>
<a-input v-model:value="form.name" placeholder="如:生产环境构建" />
</a-form-item>
<a-form-item label="描述">
<a-textarea v-model:value="form.description" :rows="2" placeholder="描述此流水线的用途" />
</a-form-item>
<a-form-item label="CI 系统" required>
<a-radio-group v-model:value="form.ciType">
<a-radio value="gitea">Gitea CI</a-radio>
<a-radio value="jenkins">Jenkins</a-radio>
<a-radio value="github">GitHub Actions</a-radio>
</a-radio-group>
</a-form-item>
<template v-if="form.ciType === 'gitea'">
<a-form-item label="仓库全称" required>
<a-input v-model:value="form.repoFullName" placeholder="如gxwebsoft/my-app" />
<div class="form-hint">Gitea 上的仓库完整名称用户名/仓库名</div>
</a-form-item>
<a-form-item label="工作流文件">
<a-input v-model:value="form.workflowFile" placeholder="build.yml" />
<div class="form-hint">.gitea/workflows/ 目录下的工作流文件名</div>
</a-form-item>
</template>
<template v-if="form.ciType === 'jenkins'">
<a-form-item label="Jenkins Job 名称" required>
<a-input v-model:value="form.repoFullName" placeholder="如my-app-build" />
</a-form-item>
</template>
<template v-if="form.ciType === 'github'">
<a-form-item label="仓库全称" required>
<a-input v-model:value="form.repoFullName" placeholder="如owner/repo" />
</a-form-item>
<a-form-item label="工作流文件">
<a-input v-model:value="form.workflowFile" placeholder=".github/workflows/build.yml" />
</a-form-item>
</template>
<a-form-item label="环境" required>
<a-radio-group v-model:value="form.env">
<a-radio value="development">开发环境</a-radio>
<a-radio value="staging">测试环境</a-radio>
<a-radio value="production">生产环境</a-radio>
</a-radio-group>
</a-form-item>
<a-form-item label="默认分支">
<a-input v-model:value="form.defaultBranch" placeholder="main" />
</a-form-item>
<a-form-item label="流水线阶段">
<a-checkbox-group v-model:value="form.selectedStages">
<a-checkbox value="build">构建</a-checkbox>
<a-checkbox value="test">测试</a-checkbox>
<a-checkbox value="deploy">部署</a-checkbox>
</a-checkbox-group>
</a-form-item>
<a-form-item label="超时时间(秒)">
<a-input-number v-model:value="form.timeout" :min="60" :max="7200" style="width: 200px" />
</a-form-item>
<a-form-item>
<a-checkbox v-model:checked="form.autoDeploy">自动部署</a-checkbox>
<div class="form-hint">构建成功后自动触发部署流程</div>
</a-form-item>
</a-form>
</a-modal>
</div>
</template>
<script setup lang="ts">
import {
ReloadOutlined,
PlusOutlined,
GithubOutlined,
FileOutlined,
BranchesOutlined,
NodeIndexOutlined,
CheckCircleOutlined,
CloseCircleOutlined,
ClockCircleOutlined,
} from '@ant-design/icons-vue'
import { message } from 'ant-design-vue'
import {
pagePipeline,
listPipelineByApp,
createPipeline,
updatePipeline,
deletePipeline,
togglePipeline,
} from '@/api/app/cicd'
import type { AppPipeline } from '@/api/app/cicd'
import { getDeveloperApps } from '@/api/app/appProduct'
import type { AppProduct } from '@/api/app/appProduct/model'
definePageMeta({ layout: 'developer' })
useHead({ title: '流水线管理 - 开发者中心' })
const userId = import.meta.client ? localStorage.getItem('UserId') : null
// 状态
const loading = ref(false)
const loadingApps = ref(false)
const submitting = ref(false)
const apps = ref<AppProduct[]>([])
const pipelines = ref<AppPipeline[]>([])
const selectedAppId = ref<number | undefined>()
// 弹窗
const showPipelineModal = ref(false)
const editingPipeline = ref<AppPipeline | null>(null)
// 表单
const form = reactive({
name: '',
description: '',
ciType: 'gitea' as 'gitea' | 'jenkins' | 'github',
repoFullName: '',
workflowFile: '',
env: 'production' as 'development' | 'staging' | 'production',
defaultBranch: 'main',
stages: '',
selectedStages: ['build', 'test', 'deploy'] as string[],
timeout: 3600,
autoDeploy: false,
})
// ========== 加载数据 ==========
async function loadApps() {
loadingApps.value = true
try {
const res = await getDeveloperApps({ page: 1, limit: 100, userId: userId ? Number(userId) : undefined })
apps.value = (res as any)?.data?.records || res?.list || []
} catch (e) {
console.error('加载应用列表失败:', e)
} finally {
loadingApps.value = false
}
}
async function loadPipelines() {
if (!selectedAppId.value) {
pipelines.value = []
return
}
loading.value = true
try {
const res = await listPipelineByApp(selectedAppId.value)
if (res?.data?.code === 200) {
pipelines.value = res.data.data || []
} else {
pipelines.value = []
}
} catch (e) {
console.error('加载流水线列表失败:', e)
pipelines.value = []
} finally {
loading.value = false
}
}
function handleAppChange(appId: number | undefined) {
selectedAppId.value = appId
if (appId) {
loadPipelines()
} else {
pipelines.value = []
}
}
// ========== 操作 ==========
function handleEdit(pipeline: AppPipeline) {
editingPipeline.value = pipeline
form.name = pipeline.name || ''
form.description = pipeline.description || ''
form.ciType = (pipeline.ciType as any) || 'gitea'
form.repoFullName = pipeline.repoFullName || ''
form.workflowFile = pipeline.workflowFile || ''
form.env = (pipeline.env as any) || 'production'
form.defaultBranch = pipeline.defaultBranch || 'main'
form.selectedStages = pipeline.stages ? pipeline.stages.split(',') : ['build', 'test', 'deploy']
form.timeout = pipeline.timeout || 3600
form.autoDeploy = pipeline.autoDeploy || false
showPipelineModal.value = true
}
async function handleToggle(pipeline: AppPipeline, enabled: boolean) {
try {
const res = await togglePipeline(pipeline.id!, enabled)
if (res?.data?.code === 200) {
message.success(enabled ? '流水线已启用' : '流水线已禁用')
loadPipelines()
} else {
message.error(res?.data?.message || '操作失败')
}
} catch (e: any) {
message.error(e?.message || '操作失败')
}
}
async function handleDelete(pipeline: AppPipeline) {
try {
const res = await deletePipeline(pipeline.id!)
if (res?.data?.code === 200) {
message.success('流水线已删除')
loadPipelines()
} else {
message.error(res?.data?.message || '删除失败')
}
} catch (e: any) {
message.error(e?.message || '删除失败')
}
}
async function handleSubmit() {
if (!form.name.trim()) {
message.error('请填写流水线名称')
return
}
if (!selectedAppId.value) {
message.error('请先选择应用')
return
}
submitting.value = true
try {
const data: Partial<AppPipeline> = {
appId: selectedAppId.value,
name: form.name,
description: form.description,
ciType: form.ciType,
repoFullName: form.repoFullName,
workflowFile: form.workflowFile,
env: form.env,
defaultBranch: form.defaultBranch,
stages: form.selectedStages.join(','),
timeout: form.timeout,
autoDeploy: form.autoDeploy,
}
let res
if (editingPipeline.value) {
data.id = editingPipeline.value.id
res = await updatePipeline(data)
} else {
res = await createPipeline(data)
}
if (res?.data?.code === 200) {
message.success(editingPipeline.value ? '流水线已更新' : '流水线已创建')
showPipelineModal.value = false
resetForm()
loadPipelines()
} else {
message.error(res?.data?.message || '操作失败')
}
} catch (e: any) {
message.error(e?.message || '操作失败')
} finally {
submitting.value = false
}
}
function resetForm() {
form.name = ''
form.description = ''
form.ciType = 'gitea'
form.repoFullName = ''
form.workflowFile = ''
form.env = 'production'
form.defaultBranch = 'main'
form.selectedStages = ['build', 'test', 'deploy']
form.timeout = 3600
form.autoDeploy = false
editingPipeline.value = null
}
// ========== 辅助函数 ==========
function ciTypeText(type?: string) {
const map: Record<string, string> = {
gitea: 'Gitea CI',
jenkins: 'Jenkins',
github: 'GitHub Actions',
}
return map[type || ''] || type || 'CI'
}
function stagesText(stages?: string) {
if (!stages) return '未知'
const map: Record<string, string> = {
build: '构建',
test: '测试',
deploy: '部署',
}
return stages.split(',').map(s => map[s] || s).join(' → ')
}
function statusText(status?: string) {
const map: Record<string, string> = {
pending: '排队中',
running: '构建中',
success: '成功',
failed: '失败',
cancelled: '已取消',
}
return map[status || ''] || status || '未知'
}
function formatTime(time?: string) {
if (!time) return ''
return new Date(time).toLocaleString('zh-CN')
}
// 初始化
onMounted(() => {
loadApps()
})
</script>
<style scoped>
.dev-page {
min-height: 100%;
padding: 20px 24px 28px;
}
.page-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 20px;
}
.page-title {
font-size: 18px;
font-weight: 700;
color: #1f2937;
margin: 0;
}
.page-desc {
font-size: 13px;
color: #9ca3af;
margin: 4px 0 0;
}
/* 面板 */
.panel {
background: #fff;
border: 1px solid #f0f0f0;
border-radius: 12px;
overflow: hidden;
}
.panel-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 14px 18px;
border-bottom: 1px solid #f5f5f5;
}
.panel-title {
font-size: 14px;
font-weight: 600;
color: rgba(0,0,0,0.85);
}
/* 流水线列表 */
.pipeline-list {
padding: 0;
}
.pipeline-item {
border-bottom: 1px solid #f0f0f0;
transition: background 0.2s;
}
.pipeline-item:last-child {
border-bottom: none;
}
.pipeline-item:hover {
background: #fafafa;
}
.pipeline-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 14px 18px;
border-bottom: 1px solid #f5f5f5;
}
.pipeline-info {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
}
.pipeline-name {
font-size: 15px;
font-weight: 600;
color: rgba(0,0,0,0.85);
}
.pipeline-actions {
display: flex;
align-items: center;
gap: 8px;
}
.pipeline-body {
padding: 14px 18px;
}
.pipeline-desc {
font-size: 13px;
color: rgba(0,0,0,0.65);
margin-bottom: 10px;
}
.pipeline-meta {
display: flex;
flex-wrap: wrap;
gap: 16px;
font-size: 12px;
color: rgba(0,0,0,0.45);
}
.meta-item {
display: flex;
align-items: center;
gap: 4px;
}
.pipeline-stats {
display: flex;
gap: 16px;
margin-top: 10px;
padding-top: 10px;
border-top: 1px dashed #f0f0f0;
}
.stat-item {
font-size: 12px;
color: rgba(0,0,0,0.45);
display: flex;
align-items: center;
gap: 4px;
}
.stat-item.success { color: #52c41a; }
.stat-item.failed { color: #ff4d4f; }
.pipeline-last-build {
padding: 8px 18px;
background: #f9f9f9;
font-size: 12px;
color: rgba(0,0,0,0.45);
}
.pipeline-last-build span {
font-weight: 500;
}
.pipeline-last-build.success span { color: #52c41a; }
.pipeline-last-build.failed span { color: #ff4d4f; }
.pipeline-last-build.running span { color: #1890ff; }
.pipeline-last-build.pending span { color: #faad14; }
/* 空状态 */
.empty-state {
padding: 60px 20px;
text-align: center;
}
.empty-icon {
font-size: 48px;
margin-bottom: 8px;
}
.empty-title {
font-size: 16px;
font-weight: 500;
color: rgba(0,0,0,0.85);
margin-bottom: 4px;
}
.empty-desc {
font-size: 14px;
color: rgba(0,0,0,0.45);
}
.py-8 { padding: 40px 0; }
.mb-4 { margin-bottom: 16px; }
.form-hint {
font-size: 12px;
color: rgba(0,0,0,0.38);
margin-top: 4px;
}
</style>

View File

@@ -1,699 +0,0 @@
<template>
<div class="dev-page">
<!-- 页面头部 -->
<div class="page-header">
<div>
<h2 class="page-title">🚀 发布管理</h2>
<p class="page-desc">管理应用的上架审核状态和版本发布</p>
</div>
<a-button type="primary" @click="showPublishModal = true">
<template #icon><PlusOutlined /></template>
提交上架申请
</a-button>
</div>
<!-- 状态统计卡片 -->
<a-row :gutter="[16, 16]" class="mb-6">
<a-col :xs="12" :md="6" v-for="stat in publishStats" :key="stat.key">
<div class="stat-card" :class="stat.color">
<div class="stat-icon">{{ stat.icon }}</div>
<div class="stat-info">
<div class="stat-value">{{ stat.value }}</div>
<div class="stat-label">{{ stat.label }}</div>
</div>
</div>
</a-col>
</a-row>
<!-- 应用发布列表 -->
<div class="panel">
<div class="panel-header">
<span class="panel-title">📦 我的应用发布</span>
<a-space>
<a-select v-model:value="filterStatus" style="width: 140px" placeholder="全部状态" @change="loadApps">
<a-select-option value="">全部状态</a-select-option>
<a-select-option value="developing">开发中</a-select-option>
<a-select-option value="pending_review">待审核</a-select-option>
<a-select-option value="published">已上架</a-select-option>
<a-select-option value="rejected">审核未通过</a-select-option>
<a-select-option value="deprecated">已下架</a-select-option>
</a-select>
<a-input-search
v-model:value="searchKeyword"
placeholder="搜索应用"
style="width: 200px"
@search="loadApps"
/>
</a-space>
</div>
<a-table
:columns="columns"
:data-source="filteredApps"
:loading="loading"
:pagination="pagination"
row-key="productId"
@change="handleTableChange"
>
<template #bodyCell="{ column, record }">
<!-- 应用信息列 -->
<template v-if="column.key === 'appInfo'">
<div class="app-info-cell">
<img v-if="record.icon" :src="record.icon" class="app-icon" />
<div v-else class="app-icon-placeholder" :style="{ background: iconBgColor(record.productName) }">
{{ (record.productName || 'A').charAt(0).toUpperCase() }}
</div>
<div class="app-info-text">
<div class="app-name">{{ record.productName }}</div>
<div class="app-code">{{ record.productCode }}</div>
</div>
</div>
</template>
<template v-if="column.key === 'publishStatus'">
<a-tag :color="statusColor(record.publishStatus)">
{{ statusText(record.publishStatus) }}
</a-tag>
</template>
<template v-if="column.key === 'price'">
<div class="price-cell">
<span v-if="record.priceType === 'free' || !record.priceType" class="price-free">免费</span>
<span v-else class="price-paid">¥{{ ((record.price || 0) / 100).toFixed(2) }}</span>
<span class="price-type">{{ priceTypeText(record.priceType) }}</span>
</div>
</template>
<template v-if="column.key === 'stats'">
<div class="stats-cell">
<span>安装 {{ record.installs || 0 }}</span>
<span>评分 {{ record.rating || '-' }}</span>
</div>
</template>
<template v-if="column.key === 'action'">
<a-space>
<!-- 开发中提交审核 -->
<a-button
v-if="!record.publishStatus || record.publishStatus === 'developing'"
type="primary"
size="small"
@click="handleSubmitReview(record)"
>
提交审核
</a-button>
<!-- 待审核撤回申请 -->
<a-popconfirm
v-if="record.publishStatus === 'pending_review'"
title="确认撤回审核申请?"
@confirm="handleWithdraw(record)"
>
<a-button size="small">撤回申请</a-button>
</a-popconfirm>
<!-- 已上架下架 -->
<a-popconfirm
v-if="record.publishStatus === 'published'"
title="确认下架此应用?"
ok-text="下架"
ok-type="danger"
@confirm="handleUnpublish(record)"
>
<a-button danger size="small">下架</a-button>
</a-popconfirm>
<!-- 审核未通过查看原因 + 重新提交 -->
<template v-if="record.publishStatus === 'rejected'">
<a-button type="link" size="small" @click="handleViewReason(record)">查看原因</a-button>
<a-button type="primary" size="small" @click="handleSubmitReview(record)">重新提交</a-button>
</template>
</a-space>
</template>
</template>
</a-table>
</div>
<!-- 审核记录 -->
<div class="panel mt-4">
<div class="panel-header">
<span class="panel-title">📋 审核记录</span>
<a-button type="link" size="small" @click="loadReviewLogs" :loading="logsLoading">刷新</a-button>
</div>
<a-spin :spinning="logsLoading">
<a-timeline class="timeline-content" v-if="reviewLogs.length > 0">
<a-timeline-item v-for="record in reviewLogs" :key="record.productId" :color="record.color">
<div class="timeline-item-content">
<div class="timeline-title">{{ record.title }}</div>
<div class="timeline-desc">{{ record.desc }}</div>
<div class="timeline-time">{{ record.time }}</div>
</div>
</a-timeline-item>
</a-timeline>
<a-empty v-else description="暂无审核记录" class="py-8" />
</a-spin>
</div>
<!-- 提交上架申请弹窗 -->
<a-modal
v-model:open="showPublishModal"
title="提交上架申请"
width="600px"
:confirm-loading="submitLoading"
@ok="handlePublishSubmit"
@cancel="resetPublishForm"
>
<a-form :model="publishForm" layout="vertical">
<a-form-item label="选择应用" required>
<a-select v-model:value="publishForm.productId" placeholder="选择要上架的应用">
<a-select-option v-for="app in developingApps" :key="app.productId" :value="app.productId">
{{ app.productName }}
</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="定价模式" required>
<a-radio-group v-model:value="publishForm.priceType">
<a-radio value="free">免费</a-radio>
<a-radio value="one_time">一次性付费</a-radio>
<a-radio value="subscription">订阅制</a-radio>
</a-radio-group>
</a-form-item>
<a-form-item v-if="publishForm.priceType !== 'free'" label="价格(元)" required>
<a-input-number v-model:value="publishForm.price" :min="0" :precision="2" style="width: 200px" />
</a-form-item>
<a-form-item v-if="publishForm.priceType === 'subscription'" label="订阅周期" required>
<a-radio-group v-model:value="publishForm.subscriptionPeriod">
<a-radio value="month">按月</a-radio>
<a-radio value="year">按年</a-radio>
</a-radio-group>
</a-form-item>
<a-form-item label="应用简介" required>
<a-textarea v-model:value="publishForm.description" :rows="3" placeholder="简要描述应用功能和特点" />
</a-form-item>
<a-form-item label="详细说明">
<a-textarea v-model:value="publishForm.content" :rows="5" placeholder="详细介绍应用功能、使用场景、技术架构等" />
</a-form-item>
</a-form>
</a-modal>
<!-- 查看审核原因弹窗 -->
<a-modal v-model:open="showReasonModal" title="审核未通过原因" :footer="null">
<a-alert type="error" :message="rejectReason" show-icon />
<div class="mt-4">
<p class="text-gray-600">请根据以上原因修改后重新提交审核</p>
</div>
</a-modal>
</div>
</template>
<script setup lang="ts">
import { PlusOutlined } from '@ant-design/icons-vue'
import { message } from 'ant-design-vue'
import {
pageAppProduct,
updateAppProduct,
submitReview,
withdrawPublishReview,
unpublishAppProduct,
} from '@/api/app/appProduct'
import type { AppProduct } from '@/api/app/appProduct/model'
definePageMeta({ layout: 'developer' })
useHead({ title: '发布管理 - 开发者中心' })
const userId = import.meta.client ? localStorage.getItem('UserId') : null
// 加载状态
const loading = ref(false)
const logsLoading = ref(false)
const apps = ref<AppProduct[]>([])
// 筛选
const filterStatus = ref('')
const searchKeyword = ref('')
// 分页
const pagination = reactive({
current: 1,
pageSize: 10,
total: 0,
showSizeChanger: true,
showQuickJumper: true,
})
// 发布统计
const publishStats = reactive([
{ key: 'developing', icon: '🔧', label: '开发中', value: 0, color: 'blue' },
{ key: 'pending_review', icon: '⏳', label: '待审核', value: 0, color: 'orange' },
{ key: 'published', icon: '✅', label: '已上架', value: 0, color: 'green' },
{ key: 'rejected', icon: '❌', label: '未通过', value: 0, color: 'red' },
])
// 审核记录(从已处理的应用中聚合生成)
interface ReviewLog {
productId?: number
title: string
desc: string
time: string
color: string
}
const reviewLogs = ref<ReviewLog[]>([])
// 表格列
const columns = [
{ title: '应用信息', key: 'appInfo', width: 280 },
{ title: '发布状态', key: 'publishStatus', width: 120 },
{ title: '定价', key: 'price', width: 150 },
{ title: '数据统计', key: 'stats', width: 150 },
{ title: '操作', key: 'action', width: 220 },
]
// 发布弹窗
const showPublishModal = ref(false)
const submitLoading = ref(false)
const publishForm = reactive({
productId: undefined as number | undefined,
priceType: 'free' as 'free' | 'one_time' | 'subscription',
price: 0,
subscriptionPeriod: 'month' as 'month' | 'year',
description: '',
content: '',
})
// 查看原因弹窗
const showReasonModal = ref(false)
const rejectReason = ref('')
// 开发中的应用列表(可提交上架)
const developingApps = computed(() => {
return apps.value.filter(
app => !app.publishStatus || app.publishStatus === 'developing' || app.publishStatus === 'rejected'
)
})
// 前端筛选(关键词过滤)
const filteredApps = computed(() => {
if (!searchKeyword.value) return apps.value
const kw = searchKeyword.value.toLowerCase()
return apps.value.filter(
app =>
app.productName?.toLowerCase().includes(kw) ||
app.productCode?.toLowerCase().includes(kw)
)
})
// 加载应用列表
async function loadApps() {
loading.value = true
try {
const queryUserId = userId ? Number(userId) : undefined
const res = await pageAppProduct({
page: pagination.current,
limit: pagination.pageSize,
userId: queryUserId,
publishStatus: filterStatus.value || undefined,
})
apps.value = res?.list || []
pagination.total = res?.count || 0
updateStats()
} catch {
message.error('加载应用列表失败')
} finally {
loading.value = false
}
}
// 加载审核记录(从应用列表聚合)
function loadReviewLogs() {
logsLoading.value = true
const logs: ReviewLog[] = []
for (const app of apps.value) {
const name = app.productName || '未命名应用'
if (app.publishStatus === 'published' && app.publishTime) {
logs.push({
productId: app.productId,
title: `应用「${name}」审核通过并上架`,
desc: '恭喜!您的应用已通过审核并上架到市场',
time: app.publishTime,
color: 'green',
})
}
if (app.publishStatus === 'rejected' && app.reviewTime) {
logs.push({
productId: app.productId,
title: `应用「${name}」审核未通过`,
desc: app.rejectReason || '请查看具体原因后修改重新提交',
time: app.reviewTime,
color: 'red',
})
}
if (app.publishStatus === 'pending_review' && app.publishTime) {
logs.push({
productId: app.productId,
title: `应用「${name}」已提交审核`,
desc: '等待平台审核人员审核,通常 1-3 个工作日',
time: app.publishTime,
color: 'blue',
})
}
}
// 按时间降序
logs.sort((a, b) => (a.time < b.time ? 1 : -1))
reviewLogs.value = logs
logsLoading.value = false
}
// 更新统计数据
function updateStats() {
publishStats[0].value = apps.value.filter(a => !a.publishStatus || a.publishStatus === 'developing').length
publishStats[1].value = apps.value.filter(a => a.publishStatus === 'pending_review').length
publishStats[2].value = apps.value.filter(a => a.publishStatus === 'published').length
publishStats[3].value = apps.value.filter(a => a.publishStatus === 'rejected').length
loadReviewLogs()
}
// 分页变化
function handleTableChange(pag: any) {
pagination.current = pag.current
pagination.pageSize = pag.pageSize
loadApps()
}
// 状态文本
function statusText(status?: string) {
const map: Record<string, string> = {
developing: '开发中',
pending_review: '待审核',
published: '已上架',
rejected: '审核未通过',
deprecated: '已下架',
}
return map[status || ''] || '开发中'
}
// 状态颜色
function statusColor(status?: string) {
const map: Record<string, string> = {
developing: 'default',
pending_review: 'orange',
published: 'success',
rejected: 'error',
deprecated: 'default',
}
return map[status || ''] || 'default'
}
// 价格类型文本
function priceTypeText(type?: string) {
const map: Record<string, string> = {
free: '',
one_time: '一次性',
subscription: '订阅',
}
return map[type || ''] ?? ''
}
// 图标背景色
const PALETTE = ['#4e6ef2', '#f4a261', '#e76f51', '#2a9d8f', '#e9c46a', '#457b9d']
function iconBgColor(name?: string) {
if (!name) return PALETTE[0]
let h = 0
for (let i = 0; i < name.length; i++) h = (h * 31 + name.charCodeAt(i)) & 0xffffffff
return PALETTE[Math.abs(h) % PALETTE.length]
}
// 操作:提交审核(打开弹窗)
function handleSubmitReview(record: AppProduct) {
publishForm.productId = record.productId
publishForm.priceType = record.priceType || 'free'
publishForm.price = record.price ? record.price / 100 : 0
publishForm.description = record.description || ''
publishForm.content = record.content || ''
showPublishModal.value = true
}
// 操作:撤回审核
async function handleWithdraw(record: AppProduct) {
try {
await withdrawPublishReview(record.productId!)
message.success('已撤回审核申请')
loadApps()
} catch (e: any) {
message.error(e?.message || '撤回失败')
}
}
// 操作:下架
async function handleUnpublish(record: AppProduct) {
try {
await unpublishAppProduct(record.productId!)
message.success('已下架')
loadApps()
} catch (e: any) {
message.error(e?.message || '下架失败')
}
}
// 操作:查看拒绝原因
function handleViewReason(record: AppProduct) {
rejectReason.value = record.rejectReason || '审核未通过,请联系平台客服了解详情。'
showReasonModal.value = true
}
// 提交上架申请
async function handlePublishSubmit() {
if (!publishForm.productId) {
message.error('请选择应用')
return
}
if (!publishForm.description.trim()) {
message.error('请填写应用简介')
return
}
submitLoading.value = true
try {
// 先更新产品信息(简介、定价等)
await updateAppProduct({
productId: publishForm.productId,
description: publishForm.description,
content: publishForm.content,
priceType: publishForm.priceType,
price: publishForm.priceType !== 'free' ? Math.round(publishForm.price * 100) : 0,
subscriptionPeriod: publishForm.priceType === 'subscription' ? publishForm.subscriptionPeriod : undefined,
} as Partial<AppProduct>)
// 再提交审核
await submitReview(publishForm.productId)
message.success('上架申请提交成功,等待审核(通常 1-3 个工作日)')
showPublishModal.value = false
resetPublishForm()
loadApps()
} catch (e: any) {
message.error(e?.message || '提交失败,请稍后重试')
} finally {
submitLoading.value = false
}
}
function resetPublishForm() {
publishForm.productId = undefined
publishForm.priceType = 'free'
publishForm.price = 0
publishForm.subscriptionPeriod = 'month'
publishForm.description = ''
publishForm.content = ''
}
onMounted(() => {
loadApps()
})
</script>
<style scoped>
.dev-page {
min-height: 100%;
padding: 20px 24px 28px;
}
.page-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 20px;
}
.page-title {
font-size: 18px;
font-weight: 700;
color: #1f2937;
margin: 0;
line-height: 1.4;
}
.page-desc {
font-size: 13px;
color: #9ca3af;
margin: 2px 0 0;
}
/* 统计卡片 */
.stat-card {
display: flex;
align-items: center;
gap: 12px;
padding: 16px;
border-radius: 12px;
border: 1px solid transparent;
transition: all 0.2s;
}
.stat-card:hover { transform: translateY(-1px); }
.stat-card.blue { background: #eff6ff; border-color: #dbeafe; }
.stat-card.orange { background: #fff7ed; border-color: #fed7aa; }
.stat-card.green { background: #f0fdf4; border-color: #bbf7d0; }
.stat-card.red { background: #fff1f2; border-color: #fecdd3; }
.stat-icon { font-size: 28px; flex-shrink: 0; }
.stat-value {
font-size: 22px;
font-weight: 700;
color: rgba(0, 0, 0, 0.85);
line-height: 1.2;
}
.stat-label {
font-size: 12px;
color: rgba(0, 0, 0, 0.45);
margin-top: 2px;
}
/* 面板 */
.panel {
background: #fff;
border: 1px solid #f0f0f0;
border-radius: 12px;
overflow: hidden;
}
.panel-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 14px 18px;
border-bottom: 1px solid #f5f5f5;
}
.panel-title {
font-size: 14px;
font-weight: 600;
color: rgba(0, 0, 0, 0.85);
}
/* 表格单元格 */
.app-info-cell {
display: flex;
align-items: center;
gap: 12px;
}
.app-icon {
width: 40px;
height: 40px;
border-radius: 8px;
object-fit: cover;
}
.app-icon-placeholder {
width: 40px;
height: 40px;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
font-weight: 600;
color: #fff;
flex-shrink: 0;
}
.app-info-text {
flex: 1;
min-width: 0;
}
.app-name {
font-size: 14px;
font-weight: 500;
color: rgba(0, 0, 0, 0.85);
}
.app-code {
font-size: 12px;
color: rgba(0, 0, 0, 0.45);
}
.price-cell {
display: flex;
flex-direction: column;
}
.price-free {
color: #22c55e;
font-weight: 500;
}
.price-paid {
color: #f59e0b;
font-weight: 600;
font-size: 16px;
}
.price-type {
font-size: 12px;
color: rgba(0, 0, 0, 0.45);
}
.stats-cell {
display: flex;
flex-direction: column;
gap: 4px;
font-size: 12px;
color: rgba(0, 0, 0, 0.65);
}
/* 时间线 */
.timeline-content {
padding: 20px;
}
.timeline-item-content {
display: flex;
flex-direction: column;
gap: 4px;
}
.timeline-title {
font-size: 14px;
font-weight: 500;
color: rgba(0, 0, 0, 0.85);
}
.timeline-desc {
font-size: 13px;
color: rgba(0, 0, 0, 0.65);
}
.timeline-time {
font-size: 12px;
color: rgba(0, 0, 0, 0.45);
}
.mb-6 { margin-bottom: 24px; }
.mt-4 { margin-top: 16px; }
.py-8 { padding: 32px 0; }
</style>

View File

@@ -1,564 +0,0 @@
<template>
<div class="dev-page">
<div class="page-header">
<div>
<h2 class="page-title">📋 权限申请记录</h2>
<p class="page-desc">查看你提交的 Git 仓库加组申请及审核状态</p>
</div>
<a-button type="primary" @click="showApplyModal = true">+ 提交新申请</a-button>
</div>
<div class="page-body">
<!-- 状态卡片 -->
<a-row :gutter="[14, 14]" class="mb-5">
<a-col :xs="12" :md="6" v-for="s in statusSummary" :key="s.label">
<div class="status-card" :class="s.color">
<div class="status-num">
<template v-if="loading.stats">-</template>
<template v-else>{{ s.num }}</template>
</div>
<div class="status-label">{{ s.label }}</div>
</div>
</a-col>
</a-row>
<!-- 申请记录列表 -->
<div class="panel">
<div class="panel-header">
<span class="panel-title">申请记录</span>
<a-radio-group v-model:value="filterStatus" size="small" button-style="solid">
<a-radio-button value="">全部</a-radio-button>
<a-radio-button value="pending">待审核</a-radio-button>
<a-radio-button value="approved">已通过</a-radio-button>
<a-radio-button value="rejected">已拒绝</a-radio-button>
</a-radio-group>
</div>
<!-- 加载状态 -->
<div v-if="loading.page" class="loading-state">
<div class="loading-icon">
<span class="loading-dot"></span>
<span class="loading-dot"></span>
<span class="loading-dot"></span>
</div>
<div class="loading-text">正在加载申请记录...</div>
</div>
<!-- 空状态 -->
<div v-else-if="filteredRequests.length === 0" class="empty-state">
<div class="empty-icon">📋</div>
<div class="empty-title">暂无申请记录</div>
<div class="empty-desc">绑定 Git 账号后提交仓库访问申请</div>
<a-space class="mt-4">
<a-button type="primary" @click="showApplyModal = true">提交申请</a-button>
<a-button @click="navigateTo('/developer/git')">绑定 Git 账号</a-button>
</a-space>
</div>
<!-- 申请列表 -->
<div v-else class="request-list">
<div
v-for="req in filteredRequests"
:key="req.id"
class="request-item"
>
<div class="request-status-dot" :class="req.status" />
<div class="request-content">
<div class="request-title-row">
<span class="request-title">{{ req.repoName || req.repo }}</span>
<a-tag :color="statusColor(req.status)">{{ statusText(req.status) }}</a-tag>
</div>
<div class="request-desc">{{ req.reason }}</div>
<div class="request-meta">
<span>🐙 {{ req.gitUsername || '未指定' }}</span>
<span class="meta-dot" />
<span>📅 {{ req.createdAt }}</span>
<span v-if="req.reviewedAt" class="meta-dot" />
<span v-if="req.reviewedAt"> {{ req.reviewedAt }} 审核</span>
<span v-if="req.reviewerName" class="meta-dot" />
<span v-if="req.reviewerName">👤 {{ req.reviewerName }}</span>
</div>
<div v-if="req.status === 'rejected' && req.rejectReason" class="reject-reason">
拒绝原因{{ req.rejectReason }}
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 提交申请弹窗 -->
<a-modal
v-model:open="showApplyModal"
title="📋 提交仓库访问申请"
ok-text="提交申请"
cancel-text="取消"
:ok-button-props="{ loading: loading.submit }"
:cancel-button-props="{ disabled: loading.submit }"
:mask-closable="!loading.submit"
@ok="handleApply"
>
<div v-if="loading.repos" class="modal-loading">
<a-spin size="small" />
<span class="ml-2">正在加载仓库列表...</span>
</div>
<a-form layout="vertical" class="mt-2" v-else>
<a-form-item label="申请仓库" required>
<a-select
v-model:value="applyForm.repo"
placeholder="请选择需要访问的仓库"
:loading="loading.repos"
:disabled="loading.repos"
allow-clear
>
<a-select-option v-for="r in repoOptions" :key="r.value" :value="r.value" :disabled="r.disabled">
{{ r.label }}
</a-select-option>
</a-select>
<div class="form-hint">
灰色选项表示你已有该仓库的访问权限
</div>
</a-form-item>
<a-form-item label="Git 用户名">
<a-input v-model:value="applyForm.gitUsername" placeholder="你绑定的 Gitea 用户名" />
<div class="form-hint">
未绑定<a @click="navigateTo('/developer/git')">前往绑定</a>
</div>
</a-form-item>
<a-form-item label="申请理由" required>
<a-textarea
v-model:value="applyForm.reason"
:rows="3"
placeholder="简述你申请该仓库的用途和背景..."
:maxlength="300"
show-count
:disabled="loading.submit"
/>
</a-form-item>
</a-form>
</a-modal>
</div>
</template>
<script setup lang="ts">
import { message } from 'ant-design-vue'
import type { SelectProps } from 'ant-design-vue'
import {
getPermissionRequests,
createPermissionRequest,
getPermissionRequestStats,
getAvailableRepositories
} from '@/api/developer'
definePageMeta({ layout: 'developer' })
useHead({ title: '权限申请记录 - 开发者中心' })
const showApplyModal = ref(false)
const filterStatus = ref('')
const loading = ref({
page: true,
stats: true,
repos: true,
submit: false,
})
const statsData = ref({
pending: 0,
approved: 0,
rejected: 0,
total: 0,
})
const applyForm = reactive({
repo: undefined as string | undefined,
gitUsername: '',
reason: '',
})
const statusSummary = computed(() => [
{ num: statsData.value.total, label: '全部申请', color: 'gray' },
{ num: statsData.value.pending, label: '待审核', color: 'orange' },
{ num: statsData.value.approved, label: '已通过', color: 'green' },
{ num: statsData.value.rejected, label: '已拒绝', color: 'red' },
])
// 申请记录数据
const requests = ref<any[]>([])
// 仓库选项
const repoOptions = ref<SelectProps['options']>([])
const filteredRequests = computed(() => {
if (!filterStatus.value) return requests.value
return requests.value.filter(r => r.status === filterStatus.value)
})
function statusText(status: string) {
const map: Record<string, string> = {
pending: '待审核',
approved: '已通过',
rejected: '已拒绝',
}
return map[status] || status
}
function statusColor(status: string) {
const map: Record<string, string> = {
pending: 'orange',
approved: 'green',
rejected: 'red',
}
return map[status] || 'default'
}
// 格式化日期
function formatDate(dateStr: string) {
const date = new Date(dateStr)
const now = new Date()
const diffMs = now.getTime() - date.getTime()
const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24))
if (diffDays === 0) {
// 今天
return `今天 ${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`
} else if (diffDays === 1) {
// 昨天
return `昨天 ${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`
} else if (diffDays < 7) {
// 本周内
return `${diffDays}天前`
} else {
// 超过一周
return `${date.getMonth() + 1}/${date.getDate()}`
}
}
// 加载数据
async function loadData() {
try {
loading.value.page = true
loading.value.stats = true
// 并行加载申请列表和统计数据
const [requestsRes, statsRes] = await Promise.all([
getPermissionRequests(),
getPermissionRequestStats()
])
if (requestsRes.data.code === 200 || requestsRes.data.code === 0) {
requests.value = requestsRes.data.data.records.map((item: any) => ({
...item,
createdAt: formatDate(item.createdAt),
reviewedAt: item.reviewedAt ? formatDate(item.reviewedAt) : null
}))
}
if (statsRes.data.code === 200 || statsRes.data.code === 0) {
statsData.value = statsRes.data.data
}
} catch (error) {
console.error('加载申请数据失败:', error)
message.error('加载申请数据失败,请稍后重试')
} finally {
loading.value.page = false
loading.value.stats = false
}
}
// 加载可申请仓库列表
async function loadAvailableRepos() {
try {
loading.value.repos = true
const res = await getAvailableRepositories()
if (res.data.code === 200 || res.data.code === 0) {
repoOptions.value = res.data.data.map((repo: any) => ({
value: repo.value,
label: repo.label,
disabled: repo.isAccessible // 已可访问的仓库禁用
}))
}
} catch (error) {
console.error('加载仓库列表失败:', error)
message.error('加载仓库列表失败,请稍后重试')
} finally {
loading.value.repos = false
}
}
async function handleApply() {
if (!applyForm.repo || !applyForm.reason.trim()) {
message.error('请填写完整申请信息')
return
}
try {
loading.value.submit = true
const res = await createPermissionRequest({
repo: applyForm.repo,
reason: applyForm.reason.trim(),
gitUsername: applyForm.gitUsername.trim() || undefined
})
if (res.data.code === 200 || res.data.code === 0) {
message.success('申请提交成功,请等待审核')
showApplyModal.value = false
// 重置表单
Object.assign(applyForm, { repo: undefined, gitUsername: '', reason: '' })
// 重新加载数据
await loadData()
} else {
message.error(res.data.message || '申请提交失败,请稍后重试')
}
} catch (error: any) {
console.error('提交申请失败:', error)
if (error.response?.status === 400) {
message.error('申请参数错误,请检查填写内容')
} else if (error.response?.status === 409) {
message.error('该仓库已存在待审核的申请')
} else {
message.error('申请提交失败,请稍后重试')
}
} finally {
loading.value.submit = false
}
}
// 页面初始化
onMounted(() => {
loadData()
loadAvailableRepos()
})
// 监听过滤状态变化
watch(filterStatus, () => {
// 可以在这里添加额外的逻辑,比如重新加载筛选后的数据
})
</script>
<style scoped>
.dev-page { min-height: 100%; }
.page-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 24px 28px 16px;
border-bottom: 1px solid #f0f0f0;
gap: 16px;
}
.page-title {
font-size: 20px;
font-weight: 700;
color: rgba(0, 0, 0, 0.88);
margin: 0 0 4px;
}
.page-desc {
font-size: 14px;
color: rgba(0, 0, 0, 0.45);
margin: 0;
}
.page-body {
padding: 20px 24px 28px;
}
/* 状态卡片 */
.status-card {
padding: 16px;
border-radius: 12px;
border: 1px solid transparent;
text-align: center;
}
.status-card.gray { background: #f9fafb; border-color: #f0f0f0; }
.status-card.orange { background: #fff7ed; border-color: #fed7aa; }
.status-card.green { background: #f0fdf4; border-color: #bbf7d0; }
.status-card.red { background: #fef2f2; border-color: #fecaca; }
.status-num {
font-size: 26px;
font-weight: 700;
color: rgba(0, 0, 0, 0.85);
line-height: 1.2;
margin-bottom: 4px;
}
.status-label {
font-size: 12px;
color: rgba(0, 0, 0, 0.45);
}
.panel {
background: #fff;
border: 1px solid #f0f0f0;
border-radius: 12px;
overflow: hidden;
}
.panel-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 14px 18px;
border-bottom: 1px solid #f5f5f5;
}
.panel-title {
font-size: 14px;
font-weight: 600;
color: rgba(0, 0, 0, 0.85);
}
/* 空状态 */
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
padding: 52px 24px;
text-align: center;
}
.empty-icon { font-size: 48px; margin-bottom: 12px; }
.empty-title { font-size: 16px; font-weight: 600; color: rgba(0, 0, 0, 0.7); }
.empty-desc { font-size: 13px; color: rgba(0, 0, 0, 0.4); margin-top: 6px; }
/* 申请列表 */
.request-list {
padding: 0;
}
.request-item {
display: flex;
gap: 14px;
padding: 16px 20px;
border-bottom: 1px solid #f9f9f9;
transition: background 0.15s;
}
.request-item:hover { background: #fafafa; }
.request-item:last-child { border-bottom: none; }
.request-status-dot {
width: 10px;
height: 10px;
border-radius: 50%;
flex-shrink: 0;
margin-top: 5px;
}
.request-status-dot.pending { background: #f59e0b; }
.request-status-dot.approved { background: #16a34a; }
.request-status-dot.rejected { background: #dc2626; }
.request-content { flex: 1; min-width: 0; }
.request-title-row {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 5px;
}
.request-title {
font-size: 14px;
font-weight: 500;
color: rgba(0, 0, 0, 0.85);
}
.request-desc {
font-size: 13px;
color: rgba(0, 0, 0, 0.5);
margin-bottom: 6px;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
.request-meta {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 6px;
font-size: 12px;
color: rgba(0, 0, 0, 0.38);
}
.meta-dot {
width: 3px;
height: 3px;
border-radius: 50%;
background: rgba(0, 0, 0, 0.2);
}
.reject-reason {
font-size: 12px;
color: #dc2626;
background: #fef2f2;
padding: 6px 10px;
border-radius: 6px;
margin-top: 8px;
}
.form-hint {
font-size: 12px;
color: rgba(0, 0, 0, 0.38);
margin-top: 4px;
}
.form-hint a {
color: #4f46e5;
cursor: pointer;
}
/* 加载状态 */
.loading-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 80px 24px;
text-align: center;
}
.loading-icon {
display: flex;
gap: 6px;
margin-bottom: 16px;
}
.loading-dot {
width: 10px;
height: 10px;
border-radius: 50%;
background: #4f46e5;
animation: pulse 1.4s ease-in-out infinite;
}
.loading-dot:nth-child(2) { animation-delay: 0.2s; }
.loading-dot:nth-child(3) { animation-delay: 0.4s; }
@keyframes pulse {
0%, 100% { opacity: 0.3; transform: scale(0.8); }
50% { opacity: 1; transform: scale(1); }
}
.loading-text {
font-size: 14px;
color: rgba(0, 0, 0, 0.45);
}
/* 弹窗加载状态 */
.modal-loading {
display: flex;
align-items: center;
justify-content: center;
padding: 40px 0;
color: rgba(0, 0, 0, 0.45);
}
.ml-2 { margin-left: 8px; }
</style>

Some files were not shown because too many files have changed in this diff Show More