Merge branch 'main' into feature/antigravity_auth
This commit is contained in:
@@ -1,5 +1,10 @@
|
||||
<template>
|
||||
<Modal :show="show" :title="t('admin.accounts.usageStatistics')" size="2xl" @close="handleClose">
|
||||
<BaseDialog
|
||||
:show="show"
|
||||
:title="t('admin.accounts.usageStatistics')"
|
||||
width="extra-wide"
|
||||
@close="handleClose"
|
||||
>
|
||||
<div class="space-y-6">
|
||||
<!-- Account Info Header -->
|
||||
<div
|
||||
@@ -521,7 +526,7 @@
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
</Modal>
|
||||
</BaseDialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
@@ -539,7 +544,7 @@ import {
|
||||
Filler
|
||||
} from 'chart.js'
|
||||
import { Line } from 'vue-chartjs'
|
||||
import Modal from '@/components/common/Modal.vue'
|
||||
import BaseDialog from '@/components/common/BaseDialog.vue'
|
||||
import LoadingSpinner from '@/components/common/LoadingSpinner.vue'
|
||||
import ModelDistributionChart from '@/components/charts/ModelDistributionChart.vue'
|
||||
import { adminAPI } from '@/api/admin'
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<template>
|
||||
<Modal
|
||||
<BaseDialog
|
||||
:show="show"
|
||||
:title="t('admin.accounts.testAccountConnection')"
|
||||
size="md"
|
||||
width="normal"
|
||||
@close="handleClose"
|
||||
>
|
||||
<div class="space-y-4">
|
||||
@@ -273,13 +273,13 @@
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
</Modal>
|
||||
</BaseDialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch, nextTick } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import Modal from '@/components/common/Modal.vue'
|
||||
import BaseDialog from '@/components/common/BaseDialog.vue'
|
||||
import { useClipboard } from '@/composables/useClipboard'
|
||||
import { adminAPI } from '@/api/admin'
|
||||
import type { Account, ClaudeModel } from '@/types'
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
<template>
|
||||
<Modal :show="show" :title="t('admin.accounts.bulkEdit.title')" size="lg" @close="handleClose">
|
||||
<form class="space-y-5" @submit.prevent="handleSubmit">
|
||||
<BaseDialog
|
||||
:show="show"
|
||||
:title="t('admin.accounts.bulkEdit.title')"
|
||||
width="wide"
|
||||
@close="handleClose"
|
||||
>
|
||||
<form id="bulk-edit-account-form" class="space-y-5" @submit.prevent="handleSubmit">
|
||||
<!-- Info -->
|
||||
<div class="rounded-lg bg-blue-50 p-4 dark:bg-blue-900/20">
|
||||
<p class="text-sm text-blue-700 dark:text-blue-400">
|
||||
@@ -19,20 +24,30 @@
|
||||
<!-- Base URL (API Key only) -->
|
||||
<div class="border-t border-gray-200 pt-4 dark:border-dark-600">
|
||||
<div class="mb-3 flex items-center justify-between">
|
||||
<label class="input-label mb-0">{{ t('admin.accounts.baseUrl') }}</label>
|
||||
<label
|
||||
id="bulk-edit-base-url-label"
|
||||
class="input-label mb-0"
|
||||
for="bulk-edit-base-url-enabled"
|
||||
>
|
||||
{{ t('admin.accounts.baseUrl') }}
|
||||
</label>
|
||||
<input
|
||||
v-model="enableBaseUrl"
|
||||
id="bulk-edit-base-url-enabled"
|
||||
type="checkbox"
|
||||
aria-controls="bulk-edit-base-url"
|
||||
class="rounded border-gray-300 text-primary-600 focus:ring-primary-500"
|
||||
/>
|
||||
</div>
|
||||
<input
|
||||
v-model="baseUrl"
|
||||
id="bulk-edit-base-url"
|
||||
type="text"
|
||||
:disabled="!enableBaseUrl"
|
||||
class="input"
|
||||
:class="!enableBaseUrl && 'cursor-not-allowed opacity-50'"
|
||||
:placeholder="t('admin.accounts.bulkEdit.baseUrlPlaceholder')"
|
||||
aria-labelledby="bulk-edit-base-url-label"
|
||||
/>
|
||||
<p class="input-hint">
|
||||
{{ t('admin.accounts.bulkEdit.baseUrlNotice') }}
|
||||
@@ -42,15 +57,28 @@
|
||||
<!-- Model restriction -->
|
||||
<div class="border-t border-gray-200 pt-4 dark:border-dark-600">
|
||||
<div class="mb-3 flex items-center justify-between">
|
||||
<label class="input-label mb-0">{{ t('admin.accounts.modelRestriction') }}</label>
|
||||
<label
|
||||
id="bulk-edit-model-restriction-label"
|
||||
class="input-label mb-0"
|
||||
for="bulk-edit-model-restriction-enabled"
|
||||
>
|
||||
{{ t('admin.accounts.modelRestriction') }}
|
||||
</label>
|
||||
<input
|
||||
v-model="enableModelRestriction"
|
||||
id="bulk-edit-model-restriction-enabled"
|
||||
type="checkbox"
|
||||
aria-controls="bulk-edit-model-restriction-body"
|
||||
class="rounded border-gray-300 text-primary-600 focus:ring-primary-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div :class="!enableModelRestriction && 'pointer-events-none opacity-50'">
|
||||
<div
|
||||
id="bulk-edit-model-restriction-body"
|
||||
:class="!enableModelRestriction && 'pointer-events-none opacity-50'"
|
||||
role="group"
|
||||
aria-labelledby="bulk-edit-model-restriction-label"
|
||||
>
|
||||
<!-- Mode Toggle -->
|
||||
<div class="mb-4 flex gap-2">
|
||||
<button
|
||||
@@ -267,19 +295,27 @@
|
||||
<div class="border-t border-gray-200 pt-4 dark:border-dark-600">
|
||||
<div class="mb-3 flex items-center justify-between">
|
||||
<div>
|
||||
<label class="input-label mb-0">{{ t('admin.accounts.customErrorCodes') }}</label>
|
||||
<label
|
||||
id="bulk-edit-custom-error-codes-label"
|
||||
class="input-label mb-0"
|
||||
for="bulk-edit-custom-error-codes-enabled"
|
||||
>
|
||||
{{ t('admin.accounts.customErrorCodes') }}
|
||||
</label>
|
||||
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ t('admin.accounts.customErrorCodesHint') }}
|
||||
</p>
|
||||
</div>
|
||||
<input
|
||||
v-model="enableCustomErrorCodes"
|
||||
id="bulk-edit-custom-error-codes-enabled"
|
||||
type="checkbox"
|
||||
aria-controls="bulk-edit-custom-error-codes-body"
|
||||
class="rounded border-gray-300 text-primary-600 focus:ring-primary-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-if="enableCustomErrorCodes" class="space-y-3">
|
||||
<div v-if="enableCustomErrorCodes" id="bulk-edit-custom-error-codes-body" class="space-y-3">
|
||||
<div class="rounded-lg bg-amber-50 p-3 dark:bg-amber-900/20">
|
||||
<p class="text-xs text-amber-700 dark:text-amber-400">
|
||||
<svg
|
||||
@@ -321,11 +357,13 @@
|
||||
<div class="flex items-center gap-2">
|
||||
<input
|
||||
v-model="customErrorCodeInput"
|
||||
id="bulk-edit-custom-error-code-input"
|
||||
type="number"
|
||||
min="100"
|
||||
max="599"
|
||||
class="input flex-1"
|
||||
:placeholder="t('admin.accounts.enterErrorCode')"
|
||||
aria-labelledby="bulk-edit-custom-error-codes-label"
|
||||
@keyup.enter="addCustomErrorCode"
|
||||
/>
|
||||
<button type="button" class="btn btn-secondary px-3" @click="addCustomErrorCode">
|
||||
@@ -374,20 +412,26 @@
|
||||
<div class="border-t border-gray-200 pt-4 dark:border-dark-600">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex-1 pr-4">
|
||||
<label class="input-label mb-0">{{
|
||||
t('admin.accounts.interceptWarmupRequests')
|
||||
}}</label>
|
||||
<label
|
||||
id="bulk-edit-intercept-warmup-label"
|
||||
class="input-label mb-0"
|
||||
for="bulk-edit-intercept-warmup-enabled"
|
||||
>
|
||||
{{ t('admin.accounts.interceptWarmupRequests') }}
|
||||
</label>
|
||||
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ t('admin.accounts.interceptWarmupRequestsDesc') }}
|
||||
</p>
|
||||
</div>
|
||||
<input
|
||||
v-model="enableInterceptWarmup"
|
||||
id="bulk-edit-intercept-warmup-enabled"
|
||||
type="checkbox"
|
||||
aria-controls="bulk-edit-intercept-warmup-body"
|
||||
class="rounded border-gray-300 text-primary-600 focus:ring-primary-500"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="enableInterceptWarmup" class="mt-3">
|
||||
<div v-if="enableInterceptWarmup" id="bulk-edit-intercept-warmup-body" class="mt-3">
|
||||
<button
|
||||
type="button"
|
||||
:class="[
|
||||
@@ -409,15 +453,27 @@
|
||||
<!-- Proxy -->
|
||||
<div class="border-t border-gray-200 pt-4 dark:border-dark-600">
|
||||
<div class="mb-3 flex items-center justify-between">
|
||||
<label class="input-label mb-0">{{ t('admin.accounts.proxy') }}</label>
|
||||
<label
|
||||
id="bulk-edit-proxy-label"
|
||||
class="input-label mb-0"
|
||||
for="bulk-edit-proxy-enabled"
|
||||
>
|
||||
{{ t('admin.accounts.proxy') }}
|
||||
</label>
|
||||
<input
|
||||
v-model="enableProxy"
|
||||
id="bulk-edit-proxy-enabled"
|
||||
type="checkbox"
|
||||
aria-controls="bulk-edit-proxy-body"
|
||||
class="rounded border-gray-300 text-primary-600 focus:ring-primary-500"
|
||||
/>
|
||||
</div>
|
||||
<div :class="!enableProxy && 'pointer-events-none opacity-50'">
|
||||
<ProxySelector v-model="proxyId" :proxies="proxies" />
|
||||
<div id="bulk-edit-proxy-body" :class="!enableProxy && 'pointer-events-none opacity-50'">
|
||||
<ProxySelector
|
||||
v-model="proxyId"
|
||||
:proxies="proxies"
|
||||
aria-labelledby="bulk-edit-proxy-label"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -425,38 +481,58 @@
|
||||
<div class="grid grid-cols-2 gap-4 border-t border-gray-200 pt-4 dark:border-dark-600">
|
||||
<div>
|
||||
<div class="mb-3 flex items-center justify-between">
|
||||
<label class="input-label mb-0">{{ t('admin.accounts.concurrency') }}</label>
|
||||
<label
|
||||
id="bulk-edit-concurrency-label"
|
||||
class="input-label mb-0"
|
||||
for="bulk-edit-concurrency-enabled"
|
||||
>
|
||||
{{ t('admin.accounts.concurrency') }}
|
||||
</label>
|
||||
<input
|
||||
v-model="enableConcurrency"
|
||||
id="bulk-edit-concurrency-enabled"
|
||||
type="checkbox"
|
||||
aria-controls="bulk-edit-concurrency"
|
||||
class="rounded border-gray-300 text-primary-600 focus:ring-primary-500"
|
||||
/>
|
||||
</div>
|
||||
<input
|
||||
v-model.number="concurrency"
|
||||
id="bulk-edit-concurrency"
|
||||
type="number"
|
||||
min="1"
|
||||
:disabled="!enableConcurrency"
|
||||
class="input"
|
||||
:class="!enableConcurrency && 'cursor-not-allowed opacity-50'"
|
||||
aria-labelledby="bulk-edit-concurrency-label"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<div class="mb-3 flex items-center justify-between">
|
||||
<label class="input-label mb-0">{{ t('admin.accounts.priority') }}</label>
|
||||
<label
|
||||
id="bulk-edit-priority-label"
|
||||
class="input-label mb-0"
|
||||
for="bulk-edit-priority-enabled"
|
||||
>
|
||||
{{ t('admin.accounts.priority') }}
|
||||
</label>
|
||||
<input
|
||||
v-model="enablePriority"
|
||||
id="bulk-edit-priority-enabled"
|
||||
type="checkbox"
|
||||
aria-controls="bulk-edit-priority"
|
||||
class="rounded border-gray-300 text-primary-600 focus:ring-primary-500"
|
||||
/>
|
||||
</div>
|
||||
<input
|
||||
v-model.number="priority"
|
||||
id="bulk-edit-priority"
|
||||
type="number"
|
||||
min="1"
|
||||
:disabled="!enablePriority"
|
||||
class="input"
|
||||
:class="!enablePriority && 'cursor-not-allowed opacity-50'"
|
||||
aria-labelledby="bulk-edit-priority-label"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -464,39 +540,69 @@
|
||||
<!-- Status -->
|
||||
<div class="border-t border-gray-200 pt-4 dark:border-dark-600">
|
||||
<div class="mb-3 flex items-center justify-between">
|
||||
<label class="input-label mb-0">{{ t('common.status') }}</label>
|
||||
<label
|
||||
id="bulk-edit-status-label"
|
||||
class="input-label mb-0"
|
||||
for="bulk-edit-status-enabled"
|
||||
>
|
||||
{{ t('common.status') }}
|
||||
</label>
|
||||
<input
|
||||
v-model="enableStatus"
|
||||
id="bulk-edit-status-enabled"
|
||||
type="checkbox"
|
||||
aria-controls="bulk-edit-status"
|
||||
class="rounded border-gray-300 text-primary-600 focus:ring-primary-500"
|
||||
/>
|
||||
</div>
|
||||
<div :class="!enableStatus && 'pointer-events-none opacity-50'">
|
||||
<Select v-model="status" :options="statusOptions" />
|
||||
<div id="bulk-edit-status" :class="!enableStatus && 'pointer-events-none opacity-50'">
|
||||
<Select
|
||||
v-model="status"
|
||||
:options="statusOptions"
|
||||
aria-labelledby="bulk-edit-status-label"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Groups -->
|
||||
<div class="border-t border-gray-200 pt-4 dark:border-dark-600">
|
||||
<div class="mb-3 flex items-center justify-between">
|
||||
<label class="input-label mb-0">{{ t('nav.groups') }}</label>
|
||||
<label
|
||||
id="bulk-edit-groups-label"
|
||||
class="input-label mb-0"
|
||||
for="bulk-edit-groups-enabled"
|
||||
>
|
||||
{{ t('nav.groups') }}
|
||||
</label>
|
||||
<input
|
||||
v-model="enableGroups"
|
||||
id="bulk-edit-groups-enabled"
|
||||
type="checkbox"
|
||||
aria-controls="bulk-edit-groups"
|
||||
class="rounded border-gray-300 text-primary-600 focus:ring-primary-500"
|
||||
/>
|
||||
</div>
|
||||
<div :class="!enableGroups && 'pointer-events-none opacity-50'">
|
||||
<GroupSelector v-model="groupIds" :groups="groups" />
|
||||
<div id="bulk-edit-groups" :class="!enableGroups && 'pointer-events-none opacity-50'">
|
||||
<GroupSelector
|
||||
v-model="groupIds"
|
||||
:groups="groups"
|
||||
aria-labelledby="bulk-edit-groups-label"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<!-- Action buttons -->
|
||||
<div class="flex justify-end gap-3 pt-4">
|
||||
<template #footer>
|
||||
<div class="flex justify-end gap-3">
|
||||
<button type="button" class="btn btn-secondary" @click="handleClose">
|
||||
{{ t('common.cancel') }}
|
||||
</button>
|
||||
<button type="submit" :disabled="submitting" class="btn btn-primary">
|
||||
<button
|
||||
type="submit"
|
||||
form="bulk-edit-account-form"
|
||||
:disabled="submitting"
|
||||
class="btn btn-primary"
|
||||
>
|
||||
<svg
|
||||
v-if="submitting"
|
||||
class="-ml-1 mr-2 h-4 w-4 animate-spin"
|
||||
@@ -522,8 +628,8 @@
|
||||
}}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</Modal>
|
||||
</template>
|
||||
</BaseDialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
@@ -532,7 +638,7 @@ import { useI18n } from 'vue-i18n'
|
||||
import { useAppStore } from '@/stores/app'
|
||||
import { adminAPI } from '@/api/admin'
|
||||
import type { Proxy, Group } from '@/types'
|
||||
import Modal from '@/components/common/Modal.vue'
|
||||
import BaseDialog from '@/components/common/BaseDialog.vue'
|
||||
import Select from '@/components/common/Select.vue'
|
||||
import ProxySelector from '@/components/common/ProxySelector.vue'
|
||||
import GroupSelector from '@/components/common/GroupSelector.vue'
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
<template>
|
||||
<Modal :show="show" :title="t('admin.accounts.createAccount')" size="xl" @close="handleClose">
|
||||
<BaseDialog
|
||||
:show="show"
|
||||
:title="t('admin.accounts.createAccount')"
|
||||
width="wide"
|
||||
@close="handleClose"
|
||||
>
|
||||
<!-- Step Indicator for OAuth accounts -->
|
||||
<div v-if="isOAuthFlow" class="mb-6 flex items-center justify-center">
|
||||
<div class="flex items-center space-x-4">
|
||||
@@ -34,7 +39,12 @@
|
||||
</div>
|
||||
|
||||
<!-- Step 1: Basic Info -->
|
||||
<form v-if="step === 1" @submit.prevent="handleSubmit" class="space-y-5">
|
||||
<form
|
||||
v-if="step === 1"
|
||||
id="create-account-form"
|
||||
@submit.prevent="handleSubmit"
|
||||
class="space-y-5"
|
||||
>
|
||||
<div>
|
||||
<label class="input-label">{{ t('admin.accounts.accountName') }}</label>
|
||||
<input
|
||||
@@ -1018,11 +1028,40 @@
|
||||
<!-- Group Selection -->
|
||||
<GroupSelector v-model="form.group_ids" :groups="groups" :platform="form.platform" />
|
||||
|
||||
<div class="flex justify-end gap-3 pt-4">
|
||||
</form>
|
||||
|
||||
<!-- Step 2: OAuth Authorization -->
|
||||
<div v-else class="space-y-5">
|
||||
<OAuthAuthorizationFlow
|
||||
ref="oauthFlowRef"
|
||||
:add-method="form.platform === 'anthropic' ? addMethod : 'oauth'"
|
||||
:auth-url="currentAuthUrl"
|
||||
:session-id="currentSessionId"
|
||||
:loading="currentOAuthLoading"
|
||||
:error="currentOAuthError"
|
||||
:show-help="form.platform === 'anthropic'"
|
||||
:show-proxy-warning="form.platform !== 'openai' && !!form.proxy_id"
|
||||
:allow-multiple="form.platform === 'anthropic'"
|
||||
:show-cookie-option="form.platform === 'anthropic'"
|
||||
:platform="form.platform"
|
||||
:show-project-id="geminiOAuthType === 'code_assist'"
|
||||
@generate-url="handleGenerateUrl"
|
||||
@cookie-auth="handleCookieAuth"
|
||||
/>
|
||||
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<div v-if="step === 1" class="flex justify-end gap-3">
|
||||
<button @click="handleClose" type="button" class="btn btn-secondary">
|
||||
{{ t('common.cancel') }}
|
||||
</button>
|
||||
<button type="submit" :disabled="submitting" class="btn btn-primary">
|
||||
<button
|
||||
type="submit"
|
||||
form="create-account-form"
|
||||
:disabled="submitting"
|
||||
class="btn btn-primary"
|
||||
>
|
||||
<svg
|
||||
v-if="submitting"
|
||||
class="-ml-1 mr-2 h-4 w-4 animate-spin"
|
||||
@@ -1052,28 +1091,7 @@
|
||||
}}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<!-- Step 2: OAuth Authorization -->
|
||||
<div v-else class="space-y-5">
|
||||
<OAuthAuthorizationFlow
|
||||
ref="oauthFlowRef"
|
||||
:add-method="form.platform === 'anthropic' ? addMethod : 'oauth'"
|
||||
:auth-url="currentAuthUrl"
|
||||
:session-id="currentSessionId"
|
||||
:loading="currentOAuthLoading"
|
||||
:error="currentOAuthError"
|
||||
:show-help="form.platform === 'anthropic'"
|
||||
:show-proxy-warning="form.platform !== 'openai' && !!form.proxy_id"
|
||||
:allow-multiple="form.platform === 'anthropic'"
|
||||
:show-cookie-option="form.platform === 'anthropic'"
|
||||
:platform="form.platform"
|
||||
:show-project-id="geminiOAuthType === 'code_assist'"
|
||||
@generate-url="handleGenerateUrl"
|
||||
@cookie-auth="handleCookieAuth"
|
||||
/>
|
||||
|
||||
<div class="flex justify-between gap-3 pt-4">
|
||||
<div v-else class="flex justify-between gap-3">
|
||||
<button type="button" class="btn btn-secondary" @click="goBackToBasicInfo">
|
||||
{{ t('common.back') }}
|
||||
</button>
|
||||
@@ -1111,8 +1129,8 @@
|
||||
}}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</template>
|
||||
</BaseDialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
@@ -1129,7 +1147,7 @@ import { useOpenAIOAuth } from '@/composables/useOpenAIOAuth'
|
||||
import { useGeminiOAuth } from '@/composables/useGeminiOAuth'
|
||||
import { useAntigravityOAuth } from '@/composables/useAntigravityOAuth'
|
||||
import type { Proxy, Group, AccountPlatform, AccountType } from '@/types'
|
||||
import Modal from '@/components/common/Modal.vue'
|
||||
import BaseDialog from '@/components/common/BaseDialog.vue'
|
||||
import ProxySelector from '@/components/common/ProxySelector.vue'
|
||||
import GroupSelector from '@/components/common/GroupSelector.vue'
|
||||
import OAuthAuthorizationFlow from './OAuthAuthorizationFlow.vue'
|
||||
|
||||
@@ -1,6 +1,16 @@
|
||||
<template>
|
||||
<Modal :show="show" :title="t('admin.accounts.editAccount')" size="xl" @close="handleClose">
|
||||
<form v-if="account" @submit.prevent="handleSubmit" class="space-y-5">
|
||||
<BaseDialog
|
||||
:show="show"
|
||||
:title="t('admin.accounts.editAccount')"
|
||||
width="wide"
|
||||
@close="handleClose"
|
||||
>
|
||||
<form
|
||||
v-if="account"
|
||||
id="edit-account-form"
|
||||
@submit.prevent="handleSubmit"
|
||||
class="space-y-5"
|
||||
>
|
||||
<div>
|
||||
<label class="input-label">{{ t('common.name') }}</label>
|
||||
<input v-model="form.name" type="text" required class="input" />
|
||||
@@ -459,11 +469,19 @@
|
||||
<!-- Group Selection -->
|
||||
<GroupSelector v-model="form.group_ids" :groups="groups" :platform="account?.platform" />
|
||||
|
||||
<div class="flex justify-end gap-3 pt-4">
|
||||
</form>
|
||||
|
||||
<template #footer>
|
||||
<div v-if="account" class="flex justify-end gap-3">
|
||||
<button @click="handleClose" type="button" class="btn btn-secondary">
|
||||
{{ t('common.cancel') }}
|
||||
</button>
|
||||
<button type="submit" :disabled="submitting" class="btn btn-primary">
|
||||
<button
|
||||
type="submit"
|
||||
form="edit-account-form"
|
||||
:disabled="submitting"
|
||||
class="btn btn-primary"
|
||||
>
|
||||
<svg
|
||||
v-if="submitting"
|
||||
class="-ml-1 mr-2 h-4 w-4 animate-spin"
|
||||
@@ -487,8 +505,8 @@
|
||||
{{ submitting ? t('admin.accounts.updating') : t('common.update') }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</Modal>
|
||||
</template>
|
||||
</BaseDialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
@@ -497,7 +515,7 @@ import { useI18n } from 'vue-i18n'
|
||||
import { useAppStore } from '@/stores/app'
|
||||
import { adminAPI } from '@/api/admin'
|
||||
import type { Account, Proxy, Group } from '@/types'
|
||||
import Modal from '@/components/common/Modal.vue'
|
||||
import BaseDialog from '@/components/common/BaseDialog.vue'
|
||||
import Select from '@/components/common/Select.vue'
|
||||
import ProxySelector from '@/components/common/ProxySelector.vue'
|
||||
import GroupSelector from '@/components/common/GroupSelector.vue'
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div
|
||||
class="rounded-lg border border-blue-200 bg-blue-50 p-6 dark:border-blue-700 dark:bg-blue-900/30"
|
||||
class="rounded-lg border border-blue-200 bg-blue-50 p-4 dark:border-blue-700 dark:bg-blue-900/30"
|
||||
>
|
||||
<div class="flex items-start gap-4">
|
||||
<div class="flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-lg bg-blue-500">
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<template>
|
||||
<Modal
|
||||
<BaseDialog
|
||||
:show="show"
|
||||
:title="t('admin.accounts.reAuthorizeAccount')"
|
||||
size="lg"
|
||||
width="wide"
|
||||
@close="handleClose"
|
||||
>
|
||||
<div v-if="account" class="space-y-5">
|
||||
<div v-if="account" class="space-y-4">
|
||||
<!-- Account Info -->
|
||||
<div
|
||||
class="rounded-lg border border-gray-200 bg-gray-50 p-4 dark:border-dark-600 dark:bg-dark-700"
|
||||
@@ -53,8 +53,8 @@
|
||||
</div>
|
||||
|
||||
<!-- Add Method Selection (Claude only) -->
|
||||
<div v-if="isAnthropic">
|
||||
<label class="input-label">{{ t('admin.accounts.oauth.authMethod') }}</label>
|
||||
<fieldset v-if="isAnthropic" class="border-0 p-0">
|
||||
<legend class="input-label">{{ t('admin.accounts.oauth.authMethod') }}</legend>
|
||||
<div class="mt-2 flex gap-4">
|
||||
<label class="flex cursor-pointer items-center">
|
||||
<input
|
||||
@@ -79,11 +79,11 @@
|
||||
}}</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<!-- Gemini OAuth Type Selection -->
|
||||
<div v-if="isGemini">
|
||||
<label class="input-label">{{ t('admin.accounts.oauth.gemini.oauthTypeLabel') }}</label>
|
||||
<fieldset v-if="isGemini" class="border-0 p-0">
|
||||
<legend class="input-label">{{ t('admin.accounts.oauth.gemini.oauthTypeLabel') }}</legend>
|
||||
<div class="mt-2 grid grid-cols-2 gap-3">
|
||||
<button
|
||||
type="button"
|
||||
@@ -187,7 +187,7 @@
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<OAuthAuthorizationFlow
|
||||
ref="oauthFlowRef"
|
||||
@@ -207,7 +207,10 @@
|
||||
@cookie-auth="handleCookieAuth"
|
||||
/>
|
||||
|
||||
<div class="flex justify-between gap-3 pt-4">
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<div v-if="account" class="flex justify-between gap-3">
|
||||
<button type="button" class="btn btn-secondary" @click="handleClose">
|
||||
{{ t('common.cancel') }}
|
||||
</button>
|
||||
@@ -245,8 +248,8 @@
|
||||
}}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</template>
|
||||
</BaseDialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
@@ -262,7 +265,7 @@ import {
|
||||
import { useOpenAIOAuth } from '@/composables/useOpenAIOAuth'
|
||||
import { useGeminiOAuth } from '@/composables/useGeminiOAuth'
|
||||
import type { Account } from '@/types'
|
||||
import Modal from '@/components/common/Modal.vue'
|
||||
import BaseDialog from '@/components/common/BaseDialog.vue'
|
||||
import OAuthAuthorizationFlow from './OAuthAuthorizationFlow.vue'
|
||||
|
||||
// Type for exposed OAuthAuthorizationFlow component
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
<template>
|
||||
<Modal
|
||||
<BaseDialog
|
||||
:show="show"
|
||||
:title="t('admin.accounts.syncFromCrsTitle')"
|
||||
size="lg"
|
||||
width="normal"
|
||||
close-on-click-outside
|
||||
@close="handleClose"
|
||||
>
|
||||
<div class="space-y-4">
|
||||
<form id="sync-from-crs-form" class="space-y-4" @submit.prevent="handleSync">
|
||||
<div class="text-sm text-gray-600 dark:text-dark-300">
|
||||
{{ t('admin.accounts.syncFromCrsDesc') }}
|
||||
</div>
|
||||
@@ -84,25 +84,30 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<template #footer>
|
||||
<div class="flex justify-end gap-3">
|
||||
<button class="btn btn-secondary" :disabled="syncing" @click="handleClose">
|
||||
<button class="btn btn-secondary" type="button" :disabled="syncing" @click="handleClose">
|
||||
{{ t('common.cancel') }}
|
||||
</button>
|
||||
<button class="btn btn-primary" :disabled="syncing" @click="handleSync">
|
||||
<button
|
||||
class="btn btn-primary"
|
||||
type="submit"
|
||||
form="sync-from-crs-form"
|
||||
:disabled="syncing"
|
||||
>
|
||||
{{ syncing ? t('admin.accounts.syncing') : t('admin.accounts.syncNow') }}
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
</Modal>
|
||||
</BaseDialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, reactive, ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import Modal from '@/components/common/Modal.vue'
|
||||
import BaseDialog from '@/components/common/BaseDialog.vue'
|
||||
import { useAppStore } from '@/stores/app'
|
||||
import { adminAPI } from '@/api/admin'
|
||||
|
||||
|
||||
118
frontend/src/components/common/BaseDialog.vue
Normal file
118
frontend/src/components/common/BaseDialog.vue
Normal file
@@ -0,0 +1,118 @@
|
||||
<template>
|
||||
<Teleport to="body">
|
||||
<div
|
||||
v-if="show"
|
||||
class="modal-overlay"
|
||||
aria-labelledby="modal-title"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
@click.self="handleClose"
|
||||
>
|
||||
<!-- Modal panel -->
|
||||
<div :class="['modal-content', widthClasses]" @click.stop>
|
||||
<!-- Header -->
|
||||
<div class="modal-header">
|
||||
<h3 id="modal-title" class="modal-title">
|
||||
{{ title }}
|
||||
</h3>
|
||||
<button
|
||||
@click="emit('close')"
|
||||
class="-mr-2 rounded-xl p-2 text-gray-400 transition-colors hover:bg-gray-100 hover:text-gray-600 dark:text-dark-500 dark:hover:bg-dark-700 dark:hover:text-dark-300"
|
||||
aria-label="Close modal"
|
||||
>
|
||||
<svg
|
||||
class="h-5 w-5"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Body -->
|
||||
<div class="modal-body">
|
||||
<slot></slot>
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<div v-if="$slots.footer" class="modal-footer">
|
||||
<slot name="footer"></slot>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Teleport>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, watch, onMounted, onUnmounted } from 'vue'
|
||||
|
||||
type DialogWidth = 'narrow' | 'normal' | 'wide' | 'extra-wide' | 'full'
|
||||
|
||||
interface Props {
|
||||
show: boolean
|
||||
title: string
|
||||
width?: DialogWidth
|
||||
closeOnEscape?: boolean
|
||||
closeOnClickOutside?: boolean
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'close'): void
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
width: 'normal',
|
||||
closeOnEscape: true,
|
||||
closeOnClickOutside: false
|
||||
})
|
||||
|
||||
const emit = defineEmits<Emits>()
|
||||
|
||||
const widthClasses = computed(() => {
|
||||
const widths: Record<DialogWidth, string> = {
|
||||
narrow: 'max-w-md',
|
||||
normal: 'max-w-lg',
|
||||
wide: 'max-w-4xl',
|
||||
'extra-wide': 'max-w-6xl',
|
||||
full: 'max-w-7xl'
|
||||
}
|
||||
return widths[props.width]
|
||||
})
|
||||
|
||||
const handleClose = () => {
|
||||
if (props.closeOnClickOutside) {
|
||||
emit('close')
|
||||
}
|
||||
}
|
||||
|
||||
const handleEscape = (event: KeyboardEvent) => {
|
||||
if (props.show && props.closeOnEscape && event.key === 'Escape') {
|
||||
emit('close')
|
||||
}
|
||||
}
|
||||
|
||||
// Prevent body scroll when modal is open
|
||||
watch(
|
||||
() => props.show,
|
||||
(isOpen) => {
|
||||
if (isOpen) {
|
||||
document.body.style.overflow = 'hidden'
|
||||
} else {
|
||||
document.body.style.overflow = ''
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
onMounted(() => {
|
||||
document.addEventListener('keydown', handleEscape)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
document.removeEventListener('keydown', handleEscape)
|
||||
document.body.style.overflow = ''
|
||||
})
|
||||
</script>
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<Modal :show="show" :title="title" size="sm" @close="handleCancel">
|
||||
<BaseDialog :show="show" :title="title" width="narrow" @close="handleCancel">
|
||||
<div class="space-y-4">
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">{{ message }}</p>
|
||||
</div>
|
||||
@@ -27,13 +27,13 @@
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
</Modal>
|
||||
</BaseDialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import Modal from './Modal.vue'
|
||||
import BaseDialog from './BaseDialog.vue'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
|
||||
@@ -24,37 +24,6 @@
|
||||
>
|
||||
<div class="flex items-center space-x-1">
|
||||
<span>{{ column.label }}</span>
|
||||
<!-- 操作列展开/折叠按钮 -->
|
||||
<button
|
||||
v-if="column.key === 'actions' && hasExpandableActions"
|
||||
type="button"
|
||||
@click.stop="toggleActionsExpanded"
|
||||
class="ml-2 flex items-center justify-center rounded p-1 text-gray-500 hover:bg-gray-200 hover:text-gray-700 dark:text-gray-400 dark:hover:bg-dark-600 dark:hover:text-gray-300"
|
||||
:title="actionsExpanded ? t('table.collapseActions') : t('table.expandActions')"
|
||||
>
|
||||
<!-- 展开状态:收起图标 -->
|
||||
<svg
|
||||
v-if="actionsExpanded"
|
||||
class="h-4 w-4"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="2"
|
||||
>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M18.75 19.5l-7.5-7.5 7.5-7.5m-6 15L5.25 12l7.5-7.5" />
|
||||
</svg>
|
||||
<!-- 折叠状态:展开图标 -->
|
||||
<svg
|
||||
v-else
|
||||
class="h-4 w-4"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="2"
|
||||
>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M11.25 4.5l7.5 7.5-7.5 7.5m-6-15l7.5 7.5-7.5 7.5" />
|
||||
</svg>
|
||||
</button>
|
||||
<span v-if="column.sortable" class="text-gray-400 dark:text-dark-500">
|
||||
<svg
|
||||
v-if="sortKey === column.key"
|
||||
@@ -182,8 +151,8 @@ const checkActionsColumnWidth = () => {
|
||||
// 等待DOM更新
|
||||
nextTick(() => {
|
||||
// 测量所有按钮的总宽度
|
||||
const buttons = actionsContainer.querySelectorAll('button')
|
||||
if (buttons.length <= 2) {
|
||||
const actionItems = actionsContainer.querySelectorAll('button, a, [role="button"]')
|
||||
if (actionItems.length <= 2) {
|
||||
actionsColumnNeedsExpanding.value = false
|
||||
actionsExpanded.value = wasExpanded
|
||||
return
|
||||
@@ -191,9 +160,9 @@ const checkActionsColumnWidth = () => {
|
||||
|
||||
// 计算所有按钮的总宽度(包括gap)
|
||||
let totalWidth = 0
|
||||
buttons.forEach((btn, index) => {
|
||||
totalWidth += (btn as HTMLElement).offsetWidth
|
||||
if (index < buttons.length - 1) {
|
||||
actionItems.forEach((item, index) => {
|
||||
totalWidth += (item as HTMLElement).offsetWidth
|
||||
if (index < actionItems.length - 1) {
|
||||
totalWidth += 4 // gap-1 = 4px
|
||||
}
|
||||
})
|
||||
@@ -211,6 +180,7 @@ const checkActionsColumnWidth = () => {
|
||||
|
||||
// 监听尺寸变化
|
||||
let resizeObserver: ResizeObserver | null = null
|
||||
let resizeHandler: (() => void) | null = null
|
||||
|
||||
onMounted(() => {
|
||||
checkScrollable()
|
||||
@@ -223,17 +193,20 @@ onMounted(() => {
|
||||
resizeObserver.observe(tableWrapperRef.value)
|
||||
} else {
|
||||
// 降级方案:不支持 ResizeObserver 时使用 window resize
|
||||
const handleResize = () => {
|
||||
resizeHandler = () => {
|
||||
checkScrollable()
|
||||
checkActionsColumnWidth()
|
||||
}
|
||||
window.addEventListener('resize', handleResize)
|
||||
window.addEventListener('resize', resizeHandler)
|
||||
}
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
resizeObserver?.disconnect()
|
||||
window.removeEventListener('resize', checkScrollable)
|
||||
if (resizeHandler) {
|
||||
window.removeEventListener('resize', resizeHandler)
|
||||
resizeHandler = null
|
||||
}
|
||||
})
|
||||
|
||||
interface Props {
|
||||
@@ -298,26 +271,6 @@ const sortedData = computed(() => {
|
||||
})
|
||||
})
|
||||
|
||||
// 检查是否有可展开的操作列
|
||||
const hasExpandableActions = computed(() => {
|
||||
// 如果明确指定了actionsCount,使用它来判断
|
||||
if (props.actionsCount !== undefined) {
|
||||
return props.expandableActions && props.columns.some((col) => col.key === 'actions') && props.actionsCount > 2
|
||||
}
|
||||
|
||||
// 否则使用原来的检测逻辑
|
||||
return (
|
||||
props.expandableActions &&
|
||||
props.columns.some((col) => col.key === 'actions') &&
|
||||
actionsColumnNeedsExpanding.value
|
||||
)
|
||||
})
|
||||
|
||||
// 切换操作列展开/折叠状态
|
||||
const toggleActionsExpanded = () => {
|
||||
actionsExpanded.value = !actionsExpanded.value
|
||||
}
|
||||
|
||||
// 检查第一列是否为勾选列
|
||||
const hasSelectColumn = computed(() => {
|
||||
return props.columns.length > 0 && props.columns[0].key === 'select'
|
||||
|
||||
@@ -206,10 +206,6 @@ const handlePageSizeChange = (value: string | number | boolean | null) => {
|
||||
if (value === null || typeof value === 'boolean') return
|
||||
const newPageSize = typeof value === 'string' ? parseInt(value) : value
|
||||
emit('update:pageSize', newPageSize)
|
||||
// Reset to first page when page size changes
|
||||
if (props.page !== 1) {
|
||||
emit('update:page', 1)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -30,7 +30,11 @@
|
||||
</button>
|
||||
|
||||
<Transition name="select-dropdown">
|
||||
<div v-if="isOpen" class="select-dropdown">
|
||||
<div
|
||||
v-if="isOpen"
|
||||
ref="dropdownRef"
|
||||
:class="['select-dropdown', dropdownPosition === 'top' && 'select-dropdown-top']"
|
||||
>
|
||||
<!-- Search input -->
|
||||
<div v-if="searchable" class="select-search">
|
||||
<svg
|
||||
@@ -141,6 +145,8 @@ const isOpen = ref(false)
|
||||
const searchQuery = ref('')
|
||||
const containerRef = ref<HTMLElement | null>(null)
|
||||
const searchInputRef = ref<HTMLInputElement | null>(null)
|
||||
const dropdownRef = ref<HTMLElement | null>(null)
|
||||
const dropdownPosition = ref<'bottom' | 'top'>('bottom')
|
||||
|
||||
const getOptionValue = (
|
||||
option: SelectOption | Record<string, unknown>
|
||||
@@ -184,13 +190,37 @@ const isSelected = (option: SelectOption | Record<string, unknown>): boolean =>
|
||||
return getOptionValue(option) === props.modelValue
|
||||
}
|
||||
|
||||
const calculateDropdownPosition = () => {
|
||||
if (!containerRef.value) return
|
||||
|
||||
nextTick(() => {
|
||||
if (!containerRef.value || !dropdownRef.value) return
|
||||
|
||||
const triggerRect = containerRef.value.getBoundingClientRect()
|
||||
const dropdownHeight = dropdownRef.value.offsetHeight || 240 // Max height fallback
|
||||
const viewportHeight = window.innerHeight
|
||||
const spaceBelow = viewportHeight - triggerRect.bottom
|
||||
const spaceAbove = triggerRect.top
|
||||
|
||||
// If not enough space below but enough space above, show dropdown on top
|
||||
if (spaceBelow < dropdownHeight && spaceAbove > dropdownHeight) {
|
||||
dropdownPosition.value = 'top'
|
||||
} else {
|
||||
dropdownPosition.value = 'bottom'
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const toggle = () => {
|
||||
if (props.disabled) return
|
||||
isOpen.value = !isOpen.value
|
||||
if (isOpen.value && props.searchable) {
|
||||
nextTick(() => {
|
||||
searchInputRef.value?.focus()
|
||||
})
|
||||
if (isOpen.value) {
|
||||
calculateDropdownPosition()
|
||||
if (props.searchable) {
|
||||
nextTick(() => {
|
||||
searchInputRef.value?.focus()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -275,6 +305,10 @@ onUnmounted(() => {
|
||||
@apply overflow-hidden;
|
||||
}
|
||||
|
||||
.select-dropdown-top {
|
||||
@apply bottom-full mb-2 mt-0;
|
||||
}
|
||||
|
||||
.select-search {
|
||||
@apply flex items-center gap-2 px-3 py-2;
|
||||
@apply border-b border-gray-100 dark:border-dark-700;
|
||||
@@ -322,6 +356,17 @@ onUnmounted(() => {
|
||||
.select-dropdown-enter-from,
|
||||
.select-dropdown-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
/* Animation for dropdown opening downward (default) */
|
||||
.select-dropdown:not(.select-dropdown-top).select-dropdown-enter-from,
|
||||
.select-dropdown:not(.select-dropdown-top).select-dropdown-leave-to {
|
||||
transform: translateY(-8px);
|
||||
}
|
||||
|
||||
/* Animation for dropdown opening upward */
|
||||
.select-dropdown-top.select-dropdown-enter-from,
|
||||
.select-dropdown-top.select-dropdown-leave-to {
|
||||
transform: translateY(8px);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -178,17 +178,19 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, onBeforeUnmount } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import subscriptionsAPI from '@/api/subscriptions'
|
||||
import { useSubscriptionStore } from '@/stores'
|
||||
import type { UserSubscription } from '@/types'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const subscriptionStore = useSubscriptionStore()
|
||||
|
||||
const containerRef = ref<HTMLElement | null>(null)
|
||||
const tooltipOpen = ref(false)
|
||||
const activeSubscriptions = ref<UserSubscription[]>([])
|
||||
const loading = ref(false)
|
||||
|
||||
const hasActiveSubscriptions = computed(() => activeSubscriptions.value.length > 0)
|
||||
// Use store data instead of local state
|
||||
const activeSubscriptions = computed(() => subscriptionStore.activeSubscriptions)
|
||||
const hasActiveSubscriptions = computed(() => subscriptionStore.hasActiveSubscriptions)
|
||||
|
||||
const displaySubscriptions = computed(() => {
|
||||
// Sort by most usage (highest percentage first)
|
||||
@@ -275,37 +277,18 @@ function handleClickOutside(event: MouseEvent) {
|
||||
}
|
||||
}
|
||||
|
||||
async function loadSubscriptions() {
|
||||
try {
|
||||
loading.value = true
|
||||
activeSubscriptions.value = await subscriptionsAPI.getActiveSubscriptions()
|
||||
} catch (error) {
|
||||
console.error('Failed to load subscriptions:', error)
|
||||
activeSubscriptions.value = []
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
document.addEventListener('click', handleClickOutside)
|
||||
loadSubscriptions()
|
||||
// Trigger initial fetch if not already loaded
|
||||
// The actual data loading is handled by App.vue globally
|
||||
subscriptionStore.fetchActiveSubscriptions().catch((error) => {
|
||||
console.error('Failed to load subscriptions in SubscriptionProgressMini:', error)
|
||||
})
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
document.removeEventListener('click', handleClickOutside)
|
||||
})
|
||||
|
||||
// Refresh subscriptions periodically (every 5 minutes)
|
||||
let refreshInterval: ReturnType<typeof setInterval> | null = null
|
||||
onMounted(() => {
|
||||
refreshInterval = setInterval(loadSubscriptions, 5 * 60 * 1000)
|
||||
})
|
||||
onBeforeUnmount(() => {
|
||||
if (refreshInterval) {
|
||||
clearInterval(refreshInterval)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
export { default as DataTable } from './DataTable.vue'
|
||||
export { default as Pagination } from './Pagination.vue'
|
||||
export { default as Modal } from './Modal.vue'
|
||||
export { default as BaseDialog } from './BaseDialog.vue'
|
||||
export { default as ConfirmDialog } from './ConfirmDialog.vue'
|
||||
export { default as StatCard } from './StatCard.vue'
|
||||
export { default as Toast } from './Toast.vue'
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<template>
|
||||
<Modal
|
||||
<BaseDialog
|
||||
:show="show"
|
||||
:title="t('keys.useKeyModal.title')"
|
||||
size="lg"
|
||||
width="wide"
|
||||
@close="emit('close')"
|
||||
>
|
||||
<div class="space-y-4">
|
||||
@@ -112,13 +112,13 @@
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
</Modal>
|
||||
</BaseDialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, h, watch, type Component } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import Modal from '@/components/common/Modal.vue'
|
||||
import BaseDialog from '@/components/common/BaseDialog.vue'
|
||||
import { useClipboard } from '@/composables/useClipboard'
|
||||
import type { GroupPlatform } from '@/types'
|
||||
|
||||
|
||||
Reference in New Issue
Block a user