refactor(developer-config): 移除开发者配置页面相关代码和文档
- 删除应用配置页面及相关组件,重构路由为 /developer/config/[id].vue - 移除开发者文档页面及其导航与样式实现 - 清理开发者侧功能完善工作日志文件 - 删除全局.gitignore配置文件,清理无用忽略规则 - 优化应用配置页面的参数读取和路由结构,解决刷新404问题 - 解决数据库配置唯一键冲突,调整保存逻辑避免重复插入 - 移除对后端配置加密字段的 secret 标记,修正加密异常问题
This commit is contained in:
Binary file not shown.
26
.gitignore
vendored
26
.gitignore
vendored
@@ -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
21
.nuxt/app.config.mjs
Normal 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
162
.nuxt/components.d.ts
vendored
Normal 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[]
|
||||||
58
.nuxt/content/components.ts
Normal file
58
.nuxt/content/components.ts
Normal 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"]
|
||||||
1
.nuxt/content/database.compressed.mjs
Normal file
1
.nuxt/content/database.compressed.mjs
Normal file
File diff suppressed because one or more lines are too long
33
.nuxt/content/manifest.ts
Normal file
33
.nuxt/content/manifest.ts
Normal 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": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
18
.nuxt/content/sql_dump.txt
Normal file
18
.nuxt/content/sql_dump.txt
Normal file
File diff suppressed because one or more lines are too long
21
.nuxt/content/types.d.ts
vendored
Normal file
21
.nuxt/content/types.d.ts
vendored
Normal 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
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
1
.nuxt/dev/index.mjs.map
Normal file
File diff suppressed because one or more lines are too long
3
.nuxt/i18n-route-resources.mjs
Normal file
3
.nuxt/i18n-route-resources.mjs
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
// Generated by @nuxtjs/i18n
|
||||||
|
export const pathToI18nConfig = {};
|
||||||
|
export const i18nPathToPath = {};
|
||||||
49
.nuxt/imports.d.ts
vendored
Normal file
49
.nuxt/imports.d.ts
vendored
Normal 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
8
.nuxt/mdc-configs.mjs
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
let configs
|
||||||
|
export function getMdcConfigs () {
|
||||||
|
if (!configs) {
|
||||||
|
configs = Promise.all([
|
||||||
|
])
|
||||||
|
}
|
||||||
|
return configs
|
||||||
|
}
|
||||||
217
.nuxt/mdc-highlighter.mjs
Normal file
217
.nuxt/mdc-highlighter.mjs
Normal 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
|
||||||
1
.nuxt/mdc-image-component.mjs
Normal file
1
.nuxt/mdc-image-component.mjs
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export default "img"
|
||||||
12
.nuxt/mdc-imports.mjs
Normal file
12
.nuxt/mdc-imports.mjs
Normal 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
17
.nuxt/nitro.json
Normal 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
26
.nuxt/nuxt.d.ts
vendored
Normal 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
9
.nuxt/nuxt.json
Normal 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
16
.nuxt/nuxt.node.d.ts
vendored
Normal 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
5
.nuxt/nuxt.shared.d.ts
vendored
Normal 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
17
.nuxt/schema/nuxt.schema.d.ts
vendored
Normal 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 {}
|
||||||
|
}
|
||||||
3
.nuxt/schema/nuxt.schema.json
Normal file
3
.nuxt/schema/nuxt.schema.json
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"id": "#"
|
||||||
|
}
|
||||||
16
.nuxt/tailwind/postcss.mjs
Normal file
16
.nuxt/tailwind/postcss.mjs
Normal 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
263
.nuxt/tsconfig.app.json
Normal 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
264
.nuxt/tsconfig.json
Normal 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
140
.nuxt/tsconfig.node.json
Normal 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
194
.nuxt/tsconfig.server.json
Normal 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
188
.nuxt/tsconfig.shared.json
Normal 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
35
.nuxt/types/app.config.d.ts
vendored
Normal 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
26
.nuxt/types/build.d.ts
vendored
Normal 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
1
.nuxt/types/builder-env.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
import "vite/client";
|
||||||
167
.nuxt/types/components.d.ts
vendored
Normal file
167
.nuxt/types/components.d.ts
vendored
Normal 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
115
.nuxt/types/i18n-plugin.d.ts
vendored
Normal 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
485
.nuxt/types/imports.d.ts
vendored
Normal 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
7
.nuxt/types/layouts.d.ts
vendored
Normal 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
7
.nuxt/types/middleware.d.ts
vendored
Normal 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
139
.nuxt/types/modules.d.ts
vendored
Normal 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
14
.nuxt/types/nitro-config.d.ts
vendored
Normal 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
171
.nuxt/types/nitro-imports.d.ts
vendored
Normal 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
11
.nuxt/types/nitro-middleware.d.ts
vendored
Normal 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
61
.nuxt/types/nitro-nuxt.d.ts
vendored
Normal 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
50
.nuxt/types/nitro-routes.d.ts
vendored
Normal 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
3
.nuxt/types/nitro.d.ts
vendored
Normal 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
45
.nuxt/types/plugins.d.ts
vendored
Normal 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
184
.nuxt/types/runtime-config.d.ts
vendored
Normal 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
0
.nuxt/types/vue-shim.d.ts
vendored
Normal 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
|
||||||
}
|
}
|
||||||
35
.workbuddy/memory/2026-04-09.md
Normal file
35
.workbuddy/memory/2026-04-09.md
Normal 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 - 协同办公
|
||||||
@@ -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,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -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' }
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -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
247
app/pages/admin/account.vue
Normal 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">支持 JPG、PNG 格式,文件小于 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>
|
||||||
@@ -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>
|
||||||
|
|||||||
119
app/pages/admin/developers/audit.vue
Normal file
119
app/pages/admin/developers/audit.vue
Normal 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>
|
||||||
301
app/pages/admin/enterprises.vue
Normal file
301
app/pages/admin/enterprises.vue
Normal 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>
|
||||||
78
app/pages/admin/enterprises/detail.vue
Normal file
78
app/pages/admin/enterprises/detail.vue
Normal 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
267
app/pages/admin/finance.vue
Normal 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>
|
||||||
89
app/pages/admin/finance/recharge.vue
Normal file
89
app/pages/admin/finance/recharge.vue
Normal 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>
|
||||||
@@ -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>
|
||||||
|
|||||||
144
app/pages/admin/management/decision.vue
Normal file
144
app/pages/admin/management/decision.vue
Normal 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>
|
||||||
383
app/pages/admin/management/finance.vue
Normal file
383
app/pages/admin/management/finance.vue
Normal 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>
|
||||||
453
app/pages/admin/management/hr.vue
Normal file
453
app/pages/admin/management/hr.vue
Normal 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>
|
||||||
477
app/pages/admin/management/office.vue
Normal file
477
app/pages/admin/management/office.vue
Normal 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
328
app/pages/admin/members.vue
Normal 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>
|
||||||
96
app/pages/admin/members/roles.vue
Normal file
96
app/pages/admin/members/roles.vue
Normal 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>
|
||||||
352
app/pages/admin/product/design.vue
Normal file
352
app/pages/admin/product/design.vue
Normal 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>
|
||||||
169
app/pages/admin/product/marketing.vue
Normal file
169
app/pages/admin/product/marketing.vue
Normal 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>
|
||||||
127
app/pages/admin/product/service.vue
Normal file
127
app/pages/admin/product/service.vue
Normal 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>
|
||||||
113
app/pages/admin/production/control.vue
Normal file
113
app/pages/admin/production/control.vue
Normal 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>
|
||||||
111
app/pages/admin/production/energy.vue
Normal file
111
app/pages/admin/production/energy.vue
Normal 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 m³</span>
|
||||||
|
<span>560 m³</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>
|
||||||
617
app/pages/admin/production/equipment.vue
Normal file
617
app/pages/admin/production/equipment.vue
Normal 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>
|
||||||
98
app/pages/admin/production/quality.vue
Normal file
98
app/pages/admin/production/quality.vue
Normal 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>
|
||||||
95
app/pages/admin/production/safety.vue
Normal file
95
app/pages/admin/production/safety.vue
Normal 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>
|
||||||
155
app/pages/admin/production/schedule.vue
Normal file
155
app/pages/admin/production/schedule.vue
Normal 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>
|
||||||
465
app/pages/admin/supply/purchase.vue
Normal file
465
app/pages/admin/supply/purchase.vue
Normal 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>
|
||||||
462
app/pages/admin/supply/warehouse.vue
Normal file
462
app/pages/admin/supply/warehouse.vue
Normal 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>
|
||||||
@@ -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">
|
|
||||||
它运行在你自己的设备上,支持接入 WhatsApp、Telegram、Discord、飞书、企业微信、钉钉等 30 多个平台,可以自由切换 Claude、GPT-4、DeepSeek 等模型,也支持本地离线模型。
|
|
||||||
</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 搭建的模型聚合层,统一接入 GPT、Claude、DeepSeek 等主流大模型,按需为客户分配独立 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>
|
|
||||||
@@ -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>
|
|
||||||
@@ -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>
|
|
||||||
@@ -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>
|
|
||||||
@@ -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>
|
|
||||||
|
|
||||||
@@ -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">
|
|
||||||
完整的开放 API、SDK、源码交付与二次开发支持。从 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>
|
|
||||||
@@ -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`
|
|
||||||
- 避免后端批量删除逻辑导致的唯一键冲突
|
|
||||||
- **状态**: 已解决
|
|
||||||
|
|
||||||
@@ -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`),无需修改代码。
|
|
||||||
@@ -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
|
|
||||||
@@ -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>
|
|
||||||
@@ -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>
|
|
||||||
@@ -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>
|
|
||||||
@@ -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>
|
|
||||||
@@ -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>
|
|
||||||
@@ -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>
|
|
||||||
@@ -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>
|
|
||||||
@@ -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>
|
|
||||||
@@ -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>
|
|
||||||
@@ -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>
|
|
||||||
@@ -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>
|
|
||||||
@@ -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>
|
|
||||||
@@ -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
Reference in New Issue
Block a user