diff --git a/frontend/src/views/admin/RedeemView.vue b/frontend/src/views/admin/RedeemView.vue index 50c55ba3..907c7541 100644 --- a/frontend/src/views/admin/RedeemView.vue +++ b/frontend/src/views/admin/RedeemView.vue @@ -238,7 +238,30 @@ v-model="generateForm.group_id" :options="subscriptionGroupOptions" :placeholder="t('admin.redeem.selectGroupPlaceholder')" - /> + > + + +
@@ -370,7 +393,7 @@ import { useAppStore } from '@/stores/app' import { useClipboard } from '@/composables/useClipboard' import { adminAPI } from '@/api/admin' import { formatDateTime } from '@/utils/format' -import type { RedeemCode, RedeemCodeType, Group } from '@/types' +import type { RedeemCode, RedeemCodeType, Group, GroupPlatform, SubscriptionType } from '@/types' import type { Column } from '@/components/common/types' import AppLayout from '@/components/layout/AppLayout.vue' import TablePageLayout from '@/components/layout/TablePageLayout.vue' @@ -378,12 +401,23 @@ import DataTable from '@/components/common/DataTable.vue' import Pagination from '@/components/common/Pagination.vue' import ConfirmDialog from '@/components/common/ConfirmDialog.vue' import Select from '@/components/common/Select.vue' +import GroupBadge from '@/components/common/GroupBadge.vue' +import GroupOptionItem from '@/components/common/GroupOptionItem.vue' import Icon from '@/components/icons/Icon.vue' const { t } = useI18n() const appStore = useAppStore() const { copyToClipboard: clipboardCopy } = useClipboard() +interface GroupOption { + value: number + label: string + description: string | null + platform: GroupPlatform + subscriptionType: SubscriptionType + rate: number +} + const showGenerateDialog = ref(false) const showResultDialog = ref(false) const generatedCodes = ref([]) @@ -395,7 +429,11 @@ const subscriptionGroupOptions = computed(() => { .filter((g) => g.subscription_type === 'subscription') .map((g) => ({ value: g.id, - label: g.name + label: g.name, + description: g.description, + platform: g.platform, + subscriptionType: g.subscription_type, + rate: g.rate_multiplier })) }) diff --git a/frontend/src/views/admin/SubscriptionsView.vue b/frontend/src/views/admin/SubscriptionsView.vue index 87c9a029..9b0e5ecb 100644 --- a/frontend/src/views/admin/SubscriptionsView.vue +++ b/frontend/src/views/admin/SubscriptionsView.vue @@ -466,7 +466,28 @@ v-model="assignForm.group_id" :options="subscriptionGroupOptions" :placeholder="t('admin.subscriptions.selectGroup')" - /> + > + + +

{{ t('admin.subscriptions.groupHint') }}

@@ -599,7 +620,7 @@ import { ref, reactive, computed, onMounted, onUnmounted } from 'vue' import { useI18n } from 'vue-i18n' import { useAppStore } from '@/stores/app' import { adminAPI } from '@/api/admin' -import type { UserSubscription, Group } from '@/types' +import type { UserSubscription, Group, GroupPlatform, SubscriptionType } from '@/types' import type { SimpleUser } from '@/api/admin/usage' import type { Column } from '@/components/common/types' import { formatDateOnly } from '@/utils/format' @@ -612,11 +633,21 @@ import ConfirmDialog from '@/components/common/ConfirmDialog.vue' import EmptyState from '@/components/common/EmptyState.vue' import Select from '@/components/common/Select.vue' import GroupBadge from '@/components/common/GroupBadge.vue' +import GroupOptionItem from '@/components/common/GroupOptionItem.vue' import Icon from '@/components/icons/Icon.vue' const { t } = useI18n() const appStore = useAppStore() +interface GroupOption { + value: number + label: string + description: string | null + platform: GroupPlatform + subscriptionType: SubscriptionType + rate: number +} + // User column display mode: 'email' or 'username' const userColumnMode = ref<'email' | 'username'>('email') const USER_COLUMN_MODE_KEY = 'subscription-user-column-mode' @@ -792,7 +823,14 @@ const groupOptions = computed(() => [ const subscriptionGroupOptions = computed(() => groups.value .filter((g) => g.subscription_type === 'subscription' && g.status === 'active') - .map((g) => ({ value: g.id, label: g.name })) + .map((g) => ({ + value: g.id, + label: g.name, + description: g.description, + platform: g.platform, + subscriptionType: g.subscription_type, + rate: g.rate_multiplier + })) ) const applyFilters = () => { diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index 267158ea..d88c6eed 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -1,4 +1,4 @@ -import { defineConfig, Plugin } from 'vite' +import { defineConfig, loadEnv, Plugin } from 'vite' import vue from '@vitejs/plugin-vue' import checker from 'vite-plugin-checker' import { resolve } from 'path' @@ -7,9 +7,7 @@ import { resolve } from 'path' * Vite 插件:开发模式下注入公开配置到 index.html * 与生产模式的后端注入行为保持一致,消除闪烁 */ -function injectPublicSettings(): Plugin { - const backendUrl = process.env.VITE_DEV_PROXY_TARGET || 'http://localhost:8080' - +function injectPublicSettings(backendUrl: string): Plugin { return { name: 'inject-public-settings', transformIndexHtml: { @@ -35,15 +33,21 @@ function injectPublicSettings(): Plugin { } } -export default defineConfig({ - plugins: [ - vue(), - checker({ - typescript: true, - vueTsc: true - }), - injectPublicSettings() - ], +export default defineConfig(({ mode }) => { + // 加载环境变量 + const env = loadEnv(mode, process.cwd(), '') + const backendUrl = env.VITE_DEV_PROXY_TARGET || 'http://localhost:8080' + const devPort = Number(env.VITE_DEV_PORT || 3000) + + return { + plugins: [ + vue(), + checker({ + typescript: true, + vueTsc: true + }), + injectPublicSettings(backendUrl) + ], resolve: { alias: { '@': resolve(__dirname, 'src'), @@ -102,17 +106,18 @@ export default defineConfig({ } } }, - server: { - host: '0.0.0.0', - port: Number(process.env.VITE_DEV_PORT || 3000), - proxy: { - '/api': { - target: process.env.VITE_DEV_PROXY_TARGET || 'http://localhost:8080', - changeOrigin: true - }, - '/setup': { - target: process.env.VITE_DEV_PROXY_TARGET || 'http://localhost:8080', - changeOrigin: true + server: { + host: '0.0.0.0', + port: devPort, + proxy: { + '/api': { + target: backendUrl, + changeOrigin: true + }, + '/setup': { + target: backendUrl, + changeOrigin: true + } } } }