Merge pull request #345 from whoismonay/main

mod(frontend): 管理员订阅/兑换码分组选择展示备注
This commit is contained in:
Wesley Liddick
2026-01-20 16:22:53 +08:00
committed by GitHub
3 changed files with 111 additions and 30 deletions

View File

@@ -238,7 +238,30 @@
v-model="generateForm.group_id" v-model="generateForm.group_id"
:options="subscriptionGroupOptions" :options="subscriptionGroupOptions"
:placeholder="t('admin.redeem.selectGroupPlaceholder')" :placeholder="t('admin.redeem.selectGroupPlaceholder')"
/> >
<template #selected="{ option }">
<GroupBadge
v-if="option"
:name="(option as unknown as GroupOption).label"
:platform="(option as unknown as GroupOption).platform"
:subscription-type="(option as unknown as GroupOption).subscriptionType"
:rate-multiplier="(option as unknown as GroupOption).rate"
/>
<span v-else class="text-gray-400">{{
t('admin.redeem.selectGroupPlaceholder')
}}</span>
</template>
<template #option="{ option, selected }">
<GroupOptionItem
:name="(option as unknown as GroupOption).label"
:platform="(option as unknown as GroupOption).platform"
:subscription-type="(option as unknown as GroupOption).subscriptionType"
:rate-multiplier="(option as unknown as GroupOption).rate"
:description="(option as unknown as GroupOption).description"
:selected="selected"
/>
</template>
</Select>
</div> </div>
<div> <div>
<label class="input-label">{{ t('admin.redeem.validityDays') }}</label> <label class="input-label">{{ t('admin.redeem.validityDays') }}</label>
@@ -370,7 +393,7 @@ import { useAppStore } from '@/stores/app'
import { useClipboard } from '@/composables/useClipboard' import { useClipboard } from '@/composables/useClipboard'
import { adminAPI } from '@/api/admin' import { adminAPI } from '@/api/admin'
import { formatDateTime } from '@/utils/format' 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 type { Column } from '@/components/common/types'
import AppLayout from '@/components/layout/AppLayout.vue' import AppLayout from '@/components/layout/AppLayout.vue'
import TablePageLayout from '@/components/layout/TablePageLayout.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 Pagination from '@/components/common/Pagination.vue'
import ConfirmDialog from '@/components/common/ConfirmDialog.vue' import ConfirmDialog from '@/components/common/ConfirmDialog.vue'
import Select from '@/components/common/Select.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' import Icon from '@/components/icons/Icon.vue'
const { t } = useI18n() const { t } = useI18n()
const appStore = useAppStore() const appStore = useAppStore()
const { copyToClipboard: clipboardCopy } = useClipboard() const { copyToClipboard: clipboardCopy } = useClipboard()
interface GroupOption {
value: number
label: string
description: string | null
platform: GroupPlatform
subscriptionType: SubscriptionType
rate: number
}
const showGenerateDialog = ref(false) const showGenerateDialog = ref(false)
const showResultDialog = ref(false) const showResultDialog = ref(false)
const generatedCodes = ref<RedeemCode[]>([]) const generatedCodes = ref<RedeemCode[]>([])
@@ -395,7 +429,11 @@ const subscriptionGroupOptions = computed(() => {
.filter((g) => g.subscription_type === 'subscription') .filter((g) => g.subscription_type === 'subscription')
.map((g) => ({ .map((g) => ({
value: g.id, value: g.id,
label: g.name label: g.name,
description: g.description,
platform: g.platform,
subscriptionType: g.subscription_type,
rate: g.rate_multiplier
})) }))
}) })

View File

@@ -466,7 +466,28 @@
v-model="assignForm.group_id" v-model="assignForm.group_id"
:options="subscriptionGroupOptions" :options="subscriptionGroupOptions"
:placeholder="t('admin.subscriptions.selectGroup')" :placeholder="t('admin.subscriptions.selectGroup')"
/> >
<template #selected="{ option }">
<GroupBadge
v-if="option"
:name="(option as unknown as GroupOption).label"
:platform="(option as unknown as GroupOption).platform"
:subscription-type="(option as unknown as GroupOption).subscriptionType"
:rate-multiplier="(option as unknown as GroupOption).rate"
/>
<span v-else class="text-gray-400">{{ t('admin.subscriptions.selectGroup') }}</span>
</template>
<template #option="{ option, selected }">
<GroupOptionItem
:name="(option as unknown as GroupOption).label"
:platform="(option as unknown as GroupOption).platform"
:subscription-type="(option as unknown as GroupOption).subscriptionType"
:rate-multiplier="(option as unknown as GroupOption).rate"
:description="(option as unknown as GroupOption).description"
:selected="selected"
/>
</template>
</Select>
<p class="input-hint">{{ t('admin.subscriptions.groupHint') }}</p> <p class="input-hint">{{ t('admin.subscriptions.groupHint') }}</p>
</div> </div>
<div> <div>
@@ -599,7 +620,7 @@ import { ref, reactive, computed, onMounted, onUnmounted } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { useAppStore } from '@/stores/app' import { useAppStore } from '@/stores/app'
import { adminAPI } from '@/api/admin' 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 { SimpleUser } from '@/api/admin/usage'
import type { Column } from '@/components/common/types' import type { Column } from '@/components/common/types'
import { formatDateOnly } from '@/utils/format' import { formatDateOnly } from '@/utils/format'
@@ -612,11 +633,21 @@ import ConfirmDialog from '@/components/common/ConfirmDialog.vue'
import EmptyState from '@/components/common/EmptyState.vue' import EmptyState from '@/components/common/EmptyState.vue'
import Select from '@/components/common/Select.vue' import Select from '@/components/common/Select.vue'
import GroupBadge from '@/components/common/GroupBadge.vue' import GroupBadge from '@/components/common/GroupBadge.vue'
import GroupOptionItem from '@/components/common/GroupOptionItem.vue'
import Icon from '@/components/icons/Icon.vue' import Icon from '@/components/icons/Icon.vue'
const { t } = useI18n() const { t } = useI18n()
const appStore = useAppStore() 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' // User column display mode: 'email' or 'username'
const userColumnMode = ref<'email' | 'username'>('email') const userColumnMode = ref<'email' | 'username'>('email')
const USER_COLUMN_MODE_KEY = 'subscription-user-column-mode' const USER_COLUMN_MODE_KEY = 'subscription-user-column-mode'
@@ -792,7 +823,14 @@ const groupOptions = computed(() => [
const subscriptionGroupOptions = computed(() => const subscriptionGroupOptions = computed(() =>
groups.value groups.value
.filter((g) => g.subscription_type === 'subscription' && g.status === 'active') .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 = () => { const applyFilters = () => {

View File

@@ -1,4 +1,4 @@
import { defineConfig, Plugin } from 'vite' import { defineConfig, loadEnv, Plugin } from 'vite'
import vue from '@vitejs/plugin-vue' import vue from '@vitejs/plugin-vue'
import checker from 'vite-plugin-checker' import checker from 'vite-plugin-checker'
import { resolve } from 'path' import { resolve } from 'path'
@@ -7,9 +7,7 @@ import { resolve } from 'path'
* Vite 插件:开发模式下注入公开配置到 index.html * Vite 插件:开发模式下注入公开配置到 index.html
* 与生产模式的后端注入行为保持一致,消除闪烁 * 与生产模式的后端注入行为保持一致,消除闪烁
*/ */
function injectPublicSettings(): Plugin { function injectPublicSettings(backendUrl: string): Plugin {
const backendUrl = process.env.VITE_DEV_PROXY_TARGET || 'http://localhost:8080'
return { return {
name: 'inject-public-settings', name: 'inject-public-settings',
transformIndexHtml: { transformIndexHtml: {
@@ -35,15 +33,21 @@ function injectPublicSettings(): Plugin {
} }
} }
export default defineConfig({ export default defineConfig(({ mode }) => {
plugins: [ // 加载环境变量
vue(), const env = loadEnv(mode, process.cwd(), '')
checker({ const backendUrl = env.VITE_DEV_PROXY_TARGET || 'http://localhost:8080'
typescript: true, const devPort = Number(env.VITE_DEV_PORT || 3000)
vueTsc: true
}), return {
injectPublicSettings() plugins: [
], vue(),
checker({
typescript: true,
vueTsc: true
}),
injectPublicSettings(backendUrl)
],
resolve: { resolve: {
alias: { alias: {
'@': resolve(__dirname, 'src'), '@': resolve(__dirname, 'src'),
@@ -102,17 +106,18 @@ export default defineConfig({
} }
} }
}, },
server: { server: {
host: '0.0.0.0', host: '0.0.0.0',
port: Number(process.env.VITE_DEV_PORT || 3000), port: devPort,
proxy: { proxy: {
'/api': { '/api': {
target: process.env.VITE_DEV_PROXY_TARGET || 'http://localhost:8080', target: backendUrl,
changeOrigin: true changeOrigin: true
}, },
'/setup': { '/setup': {
target: process.env.VITE_DEV_PROXY_TARGET || 'http://localhost:8080', target: backendUrl,
changeOrigin: true changeOrigin: true
}
} }
} }
} }