From dd8d5e2c42aedb1a993b86875584cad9afdac843 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=A2=A8=E9=A2=9C?= Date: Tue, 20 Jan 2026 15:02:48 +0800 Subject: [PATCH 1/2] =?UTF-8?q?mod(frontend):=20=E8=AE=A2=E9=98=85?= =?UTF-8?q?=E5=88=86=E7=BB=84=E4=B8=8B=E6=8B=89=E6=98=BE=E7=A4=BA=E5=A4=87?= =?UTF-8?q?=E6=B3=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 订阅管理/分配订阅:下拉项展示分组备注 - 兑换码/订阅类型:下拉项展示分组备注 - 复用 GroupOptionItem/GroupBadge 保持一致体验 --- frontend/src/views/admin/RedeemView.vue | 44 +++++++++++++++++-- .../src/views/admin/SubscriptionsView.vue | 44 +++++++++++++++++-- 2 files changed, 82 insertions(+), 6 deletions(-) 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 d5a47788..e609d95d 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') }}

@@ -584,7 +605,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' @@ -597,11 +618,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' @@ -777,7 +808,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 = () => { From 88b6358472be7e90de8a44afa016e621d2872499 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=A2=A8=E9=A2=9C?= Date: Tue, 20 Jan 2026 15:04:18 +0800 Subject: [PATCH 2/2] =?UTF-8?q?build(frontend):=20vite=20=E5=8A=A0?= =?UTF-8?q?=E8=BD=BD=E5=BC=80=E5=8F=91=E7=8E=AF=E5=A2=83=E5=8F=98=E9=87=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 使用 loadEnv 读取 VITE_DEV_PROXY_TARGET/VITE_DEV_PORT - 注入 public settings 与 dev proxy 使用同源后端地址 --- frontend/vite.config.ts | 53 ++++++++++++++++++++++------------------- 1 file changed, 29 insertions(+), 24 deletions(-) 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 + } } } }