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')"
- />
+ >
+
+
+ {{
+ 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.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
+ }
}
}
}