f
This commit is contained in:
4
.env
4
.env
@@ -4,6 +4,6 @@ VITE_API_URL="https://api.tianyuanapi.com"
|
|||||||
VITE_CAPTCHA_SCENE_ID="wynt39to"
|
VITE_CAPTCHA_SCENE_ID="wynt39to"
|
||||||
VITE_CAPTCHA_ENCRYPTED_MODE=false
|
VITE_CAPTCHA_ENCRYPTED_MODE=false
|
||||||
# 子账号专属前端域名(生产建议配置,如 https://subsole.tianyuanapi.com)
|
# 子账号专属前端域名(生产建议配置,如 https://subsole.tianyuanapi.com)
|
||||||
VITE_SUB_PORTAL_BASE_URL=""
|
VITE_SUB_PORTAL_BASE_URL="https://subsole.tianyuanapi.com"
|
||||||
# 主控制台前端域名(用于从子域回跳主域,如 https://console.tianyuanapi.com)
|
# 主控制台前端域名(用于从子域回跳主域,如 https://console.tianyuanapi.com)
|
||||||
VITE_MAIN_PORTAL_BASE_URL=""
|
VITE_MAIN_PORTAL_BASE_URL="https://console.tianyuanapi.com"
|
||||||
1
components.d.ts
vendored
1
components.d.ts
vendored
@@ -58,7 +58,6 @@ declare module 'vue' {
|
|||||||
ElSegmented: typeof import('element-plus/es')['ElSegmented']
|
ElSegmented: typeof import('element-plus/es')['ElSegmented']
|
||||||
ElSelect: typeof import('element-plus/es')['ElSelect']
|
ElSelect: typeof import('element-plus/es')['ElSelect']
|
||||||
ElSkeleton: typeof import('element-plus/es')['ElSkeleton']
|
ElSkeleton: typeof import('element-plus/es')['ElSkeleton']
|
||||||
ElSpace: typeof import('element-plus/es')['ElSpace']
|
|
||||||
ElSubMenu: typeof import('element-plus/es')['ElSubMenu']
|
ElSubMenu: typeof import('element-plus/es')['ElSubMenu']
|
||||||
ElSwitch: typeof import('element-plus/es')['ElSwitch']
|
ElSwitch: typeof import('element-plus/es')['ElSwitch']
|
||||||
ElTable: typeof import('element-plus/es')['ElTable']
|
ElTable: typeof import('element-plus/es')['ElTable']
|
||||||
|
|||||||
@@ -49,6 +49,10 @@ export const subordinateApi = {
|
|||||||
listSubordinates: (params) => request.get('/subordinate/subordinates', { params }),
|
listSubordinates: (params) => request.get('/subordinate/subordinates', { params }),
|
||||||
allocate: (data) => request.post('/subordinate/allocate', data),
|
allocate: (data) => request.post('/subordinate/allocate', data),
|
||||||
listAllocations: (params) => request.get('/subordinate/allocations', { params }),
|
listAllocations: (params) => request.get('/subordinate/allocations', { params }),
|
||||||
|
purchaseQuota: (data) => request.post('/subordinate/purchase-quota', data),
|
||||||
|
listQuotaPurchases: (params) => request.get('/subordinate/quota-purchases', { params }),
|
||||||
|
listChildQuotas: (params) => request.get('/subordinate/child-quotas', { params }),
|
||||||
|
listMyQuotas: () => request.get('/subordinate/my-quotas'),
|
||||||
assignSubscription: (data) => request.post('/subordinate/assign-subscription', data),
|
assignSubscription: (data) => request.post('/subordinate/assign-subscription', data),
|
||||||
listChildSubscriptions: (params) => request.get('/subordinate/child-subscriptions', { params }),
|
listChildSubscriptions: (params) => request.get('/subordinate/child-subscriptions', { params }),
|
||||||
removeChildSubscription: (subscriptionId, data) => request.delete(`/subordinate/child-subscriptions/${subscriptionId}`, { data })
|
removeChildSubscription: (subscriptionId, data) => request.delete(`/subordinate/child-subscriptions/${subscriptionId}`, { data })
|
||||||
|
|||||||
@@ -107,7 +107,6 @@ export const getMenuItems = (userType = 'user') => {
|
|||||||
|
|
||||||
// 子账号/子站壳:受限侧栏(不展示主站运营外链入口由布局控制)
|
// 子账号/子站壳:受限侧栏(不展示主站运营外链入口由布局控制)
|
||||||
export const getSubordinateMenuItems = () => {
|
export const getSubordinateMenuItems = () => {
|
||||||
const financeGroup = userMenuItems.find((g) => g.group === '财务管理')?.children || []
|
|
||||||
const devGroup = userMenuItems.find((g) => g.group === '开发者中心')?.children || []
|
const devGroup = userMenuItems.find((g) => g.group === '开发者中心')?.children || []
|
||||||
|
|
||||||
return [
|
return [
|
||||||
@@ -126,11 +125,6 @@ export const getSubordinateMenuItems = () => {
|
|||||||
{ name: '企业入驻', path: '/profile/certification', icon: ShieldCheck }
|
{ name: '企业入驻', path: '/profile/certification', icon: ShieldCheck }
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
|
||||||
group: '财务管理',
|
|
||||||
icon: Wallet,
|
|
||||||
children: financeGroup.filter((item) => item.path === '/finance/transactions')
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
group: '开发者中心',
|
group: '开发者中心',
|
||||||
icon: Setting,
|
icon: Setting,
|
||||||
|
|||||||
@@ -73,7 +73,7 @@
|
|||||||
|
|
||||||
<div v-else-if="transactions.length === 0" class="text-center py-12">
|
<div v-else-if="transactions.length === 0" class="text-center py-12">
|
||||||
<el-empty description="暂无消费记录">
|
<el-empty description="暂无消费记录">
|
||||||
<el-button type="primary" @click="$router.push('/finance/wallet')">
|
<el-button v-if="!isSubordinate" type="primary" @click="$router.push('/finance/wallet')">
|
||||||
前往钱包充值
|
前往钱包充值
|
||||||
</el-button>
|
</el-button>
|
||||||
</el-empty>
|
</el-empty>
|
||||||
@@ -203,9 +203,12 @@ import FilterSection from '@/components/common/FilterSection.vue'
|
|||||||
import ListPageLayout from '@/components/common/ListPageLayout.vue'
|
import ListPageLayout from '@/components/common/ListPageLayout.vue'
|
||||||
import { useMobileTable } from '@/composables/useMobileTable'
|
import { useMobileTable } from '@/composables/useMobileTable'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
|
import { useUserStore } from '@/stores/user'
|
||||||
|
|
||||||
// 移动端检测
|
// 移动端检测
|
||||||
const { isMobile } = useMobileTable()
|
const { isMobile } = useMobileTable()
|
||||||
|
const userStore = useUserStore()
|
||||||
|
const isSubordinate = computed(() => userStore.accountKind === 'subordinate')
|
||||||
|
|
||||||
// 响应式数据
|
// 响应式数据
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<ListPageLayout
|
<ListPageLayout
|
||||||
title="下属"
|
title="下属"
|
||||||
subtitle="管理下属企业信息、余额与订阅配置"
|
subtitle="管理下属企业信息与额度购买"
|
||||||
>
|
>
|
||||||
<template #filters>
|
<template #filters>
|
||||||
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-4 mb-4">
|
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-4 mb-4">
|
||||||
@@ -62,7 +62,7 @@
|
|||||||
<span class="font-semibold text-emerald-600">¥{{ formatMoney(row.balance) }}</span>
|
<span class="font-semibold text-emerald-600">¥{{ formatMoney(row.balance) }}</span>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="操作" width="180" fixed="right">
|
<el-table-column label="操作" width="110" fixed="right">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<div class="flex items-center gap-1">
|
<div class="flex items-center gap-1">
|
||||||
<el-button
|
<el-button
|
||||||
@@ -72,16 +72,7 @@
|
|||||||
bg
|
bg
|
||||||
@click="openAlloc(row)"
|
@click="openAlloc(row)"
|
||||||
>
|
>
|
||||||
划款
|
购买额度
|
||||||
</el-button>
|
|
||||||
<el-button
|
|
||||||
class="table-action-btn action-subscribe"
|
|
||||||
:class="{ 'action-disabled': !row.is_certified }"
|
|
||||||
text
|
|
||||||
bg
|
|
||||||
@click="openSubscribe(row)"
|
|
||||||
>
|
|
||||||
订阅
|
|
||||||
</el-button>
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -107,7 +98,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #extra>
|
<template #extra>
|
||||||
<el-dialog v-model="allocVisible" title="向下属划款" width="520px" @close="resetAllocForm">
|
<el-dialog v-model="allocVisible" title="为下属购买额度" width="560px" @close="resetAllocForm">
|
||||||
<div class="space-y-3">
|
<div class="space-y-3">
|
||||||
<div class="info-grid">
|
<div class="info-grid">
|
||||||
<div class="info-card">
|
<div class="info-card">
|
||||||
@@ -115,15 +106,34 @@
|
|||||||
<div class="info-value text-blue-600">¥{{ formatMoney(allocForm.parentBalance) }}</div>
|
<div class="info-value text-blue-600">¥{{ formatMoney(allocForm.parentBalance) }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="info-card">
|
<div class="info-card">
|
||||||
<div class="info-label">下属余额</div>
|
<div class="info-label">预计扣款</div>
|
||||||
<div class="info-value text-emerald-600">¥{{ formatMoney(allocForm.childBalance) }}</div>
|
<div class="info-value text-emerald-600">¥{{ formatMoney(allocTotalAmount) }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="text-sm text-gray-600">下属企业:{{ allocForm.childCompany || '未认证' }}</div>
|
<div class="text-sm text-gray-600">下属企业:{{ allocForm.childCompany || '未认证' }}</div>
|
||||||
<el-input v-model="allocForm.amount" placeholder="金额(元)">
|
<div class="text-sm font-medium text-gray-700">当前已订阅产品与剩余额度</div>
|
||||||
<template #prefix>¥</template>
|
<el-table :data="subscribedProductRows" border stripe size="small" max-height="220">
|
||||||
</el-input>
|
<el-table-column prop="product_name" label="产品" min-width="180" show-overflow-tooltip />
|
||||||
|
<el-table-column label="剩余额度" width="120">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<span class="text-emerald-600 font-medium">{{ row.available_quota }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
<div v-if="!subscribedProductRows.length" class="text-xs text-gray-500">
|
||||||
|
当前下属暂无已订阅产品,可直接在下方选择产品购买并自动新增。
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-select v-model="allocForm.productId" placeholder="请选择产品" filterable class="w-full">
|
||||||
|
<el-option
|
||||||
|
v-for="item in allocProducts"
|
||||||
|
:key="item.product_id"
|
||||||
|
:label="`${item.product_name || item.product_id}(单价 ¥${formatMoney(item.price)} / 次,剩余 ${item.available_quota})`"
|
||||||
|
:value="item.product_id"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
<el-input-number v-model="allocForm.callCount" :min="1" :step="1" controls-position="right" class="w-full" />
|
||||||
|
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<el-input v-model="allocForm.verifyCode" maxlength="6" placeholder="请输入手机验证码" />
|
<el-input v-model="allocForm.verifyCode" maxlength="6" placeholder="请输入手机验证码" />
|
||||||
@@ -139,7 +149,7 @@
|
|||||||
|
|
||||||
<div class="flex justify-end">
|
<div class="flex justify-end">
|
||||||
<el-button class="table-action-btn action-subscribe" text bg @click="openAllocationRecords">
|
<el-button class="table-action-btn action-subscribe" text bg @click="openAllocationRecords">
|
||||||
划款记录
|
额度购买记录
|
||||||
</el-button>
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -149,82 +159,22 @@
|
|||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
|
||||||
<el-dialog v-model="subVisible" title="下属订阅管理" width="860px" @close="resetSubForm">
|
|
||||||
<div class="space-y-3">
|
|
||||||
<div class="text-sm text-gray-600">下属企业:{{ subForm.childCompany || '未认证' }}</div>
|
|
||||||
|
|
||||||
<div class="subscribe-add-panel">
|
|
||||||
<el-select
|
|
||||||
v-model="subForm.productId"
|
|
||||||
placeholder="选择产品后可新增或更新订阅"
|
|
||||||
filterable
|
|
||||||
class="product-select"
|
|
||||||
:loading="productLoading"
|
|
||||||
>
|
|
||||||
<el-option
|
|
||||||
v-for="item in products"
|
|
||||||
:key="item.id"
|
|
||||||
:label="`${item.name}(目录价 ¥${formatMoney(item.price)})`"
|
|
||||||
:value="item.id"
|
|
||||||
/>
|
|
||||||
</el-select>
|
|
||||||
<el-input v-model="subForm.price" placeholder="下属价格" class="price-input">
|
|
||||||
<template #prefix>¥</template>
|
|
||||||
</el-input>
|
|
||||||
<el-button class="toolbar-btn toolbar-btn-primary" type="primary" :loading="subLoading" @click="submitSubscribe">
|
|
||||||
新增订阅
|
|
||||||
</el-button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<el-table :data="childSubscriptions" border stripe>
|
|
||||||
<el-table-column prop="product_id" label="产品ID" min-width="180" show-overflow-tooltip />
|
|
||||||
<el-table-column label="产品名称" min-width="180">
|
|
||||||
<template #default="{ row }">
|
|
||||||
{{ getProductName(row.product_id) }}
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column label="主账号订阅价" width="150">
|
|
||||||
<template #default="{ row }">
|
|
||||||
<span class="text-blue-600">¥{{ formatMoney(getParentPrice(row.product_id)) }}</span>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column label="下属订阅价" width="180">
|
|
||||||
<template #default="{ row }">
|
|
||||||
<el-input v-model="row._editPrice" size="small">
|
|
||||||
<template #prefix>¥</template>
|
|
||||||
</el-input>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column label="操作" width="180" fixed="right">
|
|
||||||
<template #default="{ row }">
|
|
||||||
<el-button class="table-action-btn action-allocate" text bg :loading="row._saving" @click="updateSubscription(row)">
|
|
||||||
保存
|
|
||||||
</el-button>
|
|
||||||
<el-button class="table-action-btn action-danger" text bg :loading="row._deleting" @click="removeSubscription(row)">
|
|
||||||
删除
|
|
||||||
</el-button>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
</el-table>
|
|
||||||
<div v-if="!childSubscriptions.length" class="text-center text-gray-400 py-3">暂无下属订阅,可通过上方新增</div>
|
|
||||||
<div class="text-xs text-gray-500">
|
|
||||||
规则:下属订阅价不能低于主账号订阅价;主账号未订阅时可先为主账号订阅再配置下属。
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<template #footer>
|
|
||||||
<el-button class="dialog-btn dialog-btn-secondary" @click="subVisible = false">关闭</el-button>
|
|
||||||
</template>
|
|
||||||
</el-dialog>
|
|
||||||
|
|
||||||
<el-dialog v-model="allocRecordVisible" title="划款记录" width="760px">
|
<el-dialog v-model="allocRecordVisible" title="划款记录" width="760px">
|
||||||
<el-table :data="allocationRecords" border stripe>
|
<el-table :data="allocationRecords" border stripe>
|
||||||
<el-table-column prop="amount" label="金额" width="140">
|
<el-table-column prop="product_id" label="产品ID" min-width="180" show-overflow-tooltip />
|
||||||
|
<el-table-column prop="call_count" label="购买次数" width="120" />
|
||||||
|
<el-table-column prop="unit_price" label="单价" width="120">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<span class="text-emerald-600 font-medium">¥{{ formatMoney(row.amount) }}</span>
|
<span class="text-blue-600 font-medium">¥{{ formatMoney(row.unit_price) }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="total_amount" label="总金额" width="140">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<span class="text-emerald-600 font-medium">¥{{ formatMoney(row.total_amount) }}</span>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column prop="business_ref" label="业务单号" min-width="220" show-overflow-tooltip />
|
<el-table-column prop="business_ref" label="业务单号" min-width="220" show-overflow-tooltip />
|
||||||
<el-table-column prop="created_at" label="划款时间" width="220">
|
<el-table-column prop="created_at" label="购买时间" width="220">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
{{ formatDateTime(row.created_at) }}
|
{{ formatDateTime(row.created_at) }}
|
||||||
</template>
|
</template>
|
||||||
@@ -249,13 +199,15 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { financeApi, productApi, subordinateApi, subscriptionApi } from '@/api'
|
import { financeApi, subordinateApi, subscriptionApi } from '@/api'
|
||||||
import ListPageLayout from '@/components/common/ListPageLayout.vue'
|
import ListPageLayout from '@/components/common/ListPageLayout.vue'
|
||||||
|
import { useAliyunCaptcha } from '@/composables/useAliyunCaptcha'
|
||||||
import { useUserStore } from '@/stores/user'
|
import { useUserStore } from '@/stores/user'
|
||||||
import { Loading } from '@element-plus/icons-vue'
|
import { Loading } from '@element-plus/icons-vue'
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
|
|
||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
|
const { runWithCaptcha } = useAliyunCaptcha()
|
||||||
const list = ref([])
|
const list = ref([])
|
||||||
const total = ref(0)
|
const total = ref(0)
|
||||||
const page = ref(1)
|
const page = ref(1)
|
||||||
@@ -271,11 +223,14 @@ let allocCodeTimer = null
|
|||||||
const allocForm = ref({
|
const allocForm = ref({
|
||||||
childId: '',
|
childId: '',
|
||||||
childCompany: '',
|
childCompany: '',
|
||||||
amount: '',
|
productId: '',
|
||||||
|
callCount: 1,
|
||||||
verifyCode: '',
|
verifyCode: '',
|
||||||
parentBalance: '0.00',
|
parentBalance: '0.00'
|
||||||
childBalance: '0.00'
|
|
||||||
})
|
})
|
||||||
|
const allocProducts = ref([])
|
||||||
|
const childSubscriptions = ref([])
|
||||||
|
const childQuotaAccounts = ref([])
|
||||||
|
|
||||||
const allocRecordVisible = ref(false)
|
const allocRecordVisible = ref(false)
|
||||||
const allocationRecords = ref([])
|
const allocationRecords = ref([])
|
||||||
@@ -283,20 +238,21 @@ const allocationTotal = ref(0)
|
|||||||
const allocationPage = ref(1)
|
const allocationPage = ref(1)
|
||||||
const allocationPageSize = ref(10)
|
const allocationPageSize = ref(10)
|
||||||
|
|
||||||
const subVisible = ref(false)
|
|
||||||
const subLoading = ref(false)
|
|
||||||
const productLoading = ref(false)
|
|
||||||
const products = ref([])
|
|
||||||
const parentSubscriptions = ref([])
|
const parentSubscriptions = ref([])
|
||||||
const childSubscriptions = ref([])
|
|
||||||
const subForm = ref({
|
|
||||||
childId: '',
|
|
||||||
childCompany: '',
|
|
||||||
productId: '',
|
|
||||||
price: ''
|
|
||||||
})
|
|
||||||
|
|
||||||
const lastInvite = ref('')
|
const lastInvite = ref('')
|
||||||
|
const allocTotalAmount = computed(() => {
|
||||||
|
const selected = allocProducts.value.find((item) => item.product_id === allocForm.value.productId)
|
||||||
|
if (!selected) return '0.00'
|
||||||
|
const count = Number(allocForm.value.callCount || 0)
|
||||||
|
const unit = Number(selected.price || 0)
|
||||||
|
if (Number.isNaN(count) || Number.isNaN(unit) || count <= 0 || unit <= 0) return '0.00'
|
||||||
|
return (unit * count).toFixed(2)
|
||||||
|
})
|
||||||
|
|
||||||
|
const subscribedProductRows = computed(() => {
|
||||||
|
return allocProducts.value.filter((item) => item.is_subscribed)
|
||||||
|
})
|
||||||
|
|
||||||
const formatDateTime = (value) => {
|
const formatDateTime = (value) => {
|
||||||
if (!value) return '-'
|
if (!value) return '-'
|
||||||
@@ -311,16 +267,6 @@ const formatMoney = (value) => {
|
|||||||
return n.toFixed(2)
|
return n.toFixed(2)
|
||||||
}
|
}
|
||||||
|
|
||||||
const getProductName = (productId) => {
|
|
||||||
const p = products.value.find((item) => item.id === productId)
|
|
||||||
return p?.name || '-'
|
|
||||||
}
|
|
||||||
|
|
||||||
const getParentPrice = (productId) => {
|
|
||||||
const sub = parentSubscriptions.value.find((item) => (item.product_id || item.product?.id) === productId)
|
|
||||||
return Number(sub?.price || 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
const loadList = async () => {
|
const loadList = async () => {
|
||||||
listLoading.value = true
|
listLoading.value = true
|
||||||
try {
|
try {
|
||||||
@@ -336,38 +282,6 @@ const loadList = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const loadProducts = async () => {
|
|
||||||
productLoading.value = true
|
|
||||||
try {
|
|
||||||
const res = await productApi.getProducts({ page: 1, page_size: 500 })
|
|
||||||
products.value = res?.data?.items || []
|
|
||||||
} catch (e) {
|
|
||||||
ElMessage.error(e?.message || '加载产品失败')
|
|
||||||
} finally {
|
|
||||||
productLoading.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const loadParentSubscriptions = async () => {
|
|
||||||
try {
|
|
||||||
const res = await subscriptionApi.getMySubscriptions({ page: 1, page_size: 1000 })
|
|
||||||
parentSubscriptions.value = res?.data?.items || []
|
|
||||||
} catch (e) {
|
|
||||||
ElMessage.error(e?.message || '加载主账号订阅失败')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const loadChildSubscriptions = async () => {
|
|
||||||
const res = await subordinateApi.listChildSubscriptions({ child_user_id: subForm.value.childId })
|
|
||||||
const items = res?.data || []
|
|
||||||
childSubscriptions.value = items.map((item) => ({
|
|
||||||
...item,
|
|
||||||
_editPrice: formatMoney(item.price),
|
|
||||||
_saving: false,
|
|
||||||
_deleting: false
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
const createInvite = async () => {
|
const createInvite = async () => {
|
||||||
invLoading.value = true
|
invLoading.value = true
|
||||||
try {
|
try {
|
||||||
@@ -388,11 +302,14 @@ const resetAllocForm = () => {
|
|||||||
allocForm.value = {
|
allocForm.value = {
|
||||||
childId: '',
|
childId: '',
|
||||||
childCompany: '',
|
childCompany: '',
|
||||||
amount: '',
|
productId: '',
|
||||||
|
callCount: 1,
|
||||||
verifyCode: '',
|
verifyCode: '',
|
||||||
parentBalance: '0.00',
|
parentBalance: '0.00'
|
||||||
childBalance: '0.00'
|
|
||||||
}
|
}
|
||||||
|
allocProducts.value = []
|
||||||
|
childSubscriptions.value = []
|
||||||
|
childQuotaAccounts.value = []
|
||||||
}
|
}
|
||||||
|
|
||||||
const ensureCertified = (row, actionName) => {
|
const ensureCertified = (row, actionName) => {
|
||||||
@@ -402,16 +319,58 @@ const ensureCertified = (row, actionName) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const openAlloc = async (row) => {
|
const openAlloc = async (row) => {
|
||||||
if (!ensureCertified(row, '划款')) return
|
if (!ensureCertified(row, '购买额度')) return
|
||||||
resetAllocForm()
|
resetAllocForm()
|
||||||
allocForm.value.childId = row.child_user_id
|
allocForm.value.childId = row.child_user_id
|
||||||
allocForm.value.childCompany = row.company_name
|
allocForm.value.childCompany = row.company_name
|
||||||
allocForm.value.childBalance = row.balance || '0.00'
|
|
||||||
try {
|
try {
|
||||||
const walletRes = await financeApi.getWallet()
|
const walletRes = await financeApi.getWallet().catch(() => null)
|
||||||
allocForm.value.parentBalance = walletRes?.data?.balance || '0.00'
|
allocForm.value.parentBalance = walletRes?.data?.balance || '0.00'
|
||||||
|
const [parentSubResult, childSubResult, childQuotaResult] = await Promise.allSettled([
|
||||||
|
subscriptionApi.getMySubscriptions({ page: 1, page_size: 1000 }),
|
||||||
|
subordinateApi.listChildSubscriptions({ child_user_id: row.child_user_id }),
|
||||||
|
subordinateApi.listChildQuotas({ child_user_id: row.child_user_id, page: 1, page_size: 1000 })
|
||||||
|
])
|
||||||
|
parentSubscriptions.value = parentSubResult.status === 'fulfilled' ? (parentSubResult.value?.data?.items || []) : []
|
||||||
|
childSubscriptions.value = childSubResult.status === 'fulfilled' ? (childSubResult.value?.data || []) : []
|
||||||
|
childQuotaAccounts.value = childQuotaResult.status === 'fulfilled'
|
||||||
|
? (childQuotaResult.value?.data?.items || childQuotaResult.value?.data || [])
|
||||||
|
: []
|
||||||
|
|
||||||
|
const childSubProductSet = new Set(
|
||||||
|
childSubscriptions.value
|
||||||
|
.map((item) => item.product_id || item.product?.id)
|
||||||
|
.filter(Boolean)
|
||||||
|
)
|
||||||
|
const quotaMap = new Map(
|
||||||
|
childQuotaAccounts.value.map((item) => [
|
||||||
|
item.product_id,
|
||||||
|
{
|
||||||
|
available_quota: Number(item.available_quota || 0),
|
||||||
|
total_quota: Number(item.total_quota || 0)
|
||||||
|
}
|
||||||
|
])
|
||||||
|
)
|
||||||
|
|
||||||
|
allocProducts.value = parentSubscriptions.value
|
||||||
|
.map((item) => ({
|
||||||
|
product_id: item.product_id || item.product?.id,
|
||||||
|
product_name: item.product_name || item.product?.name || item.product?.title || item.product_id,
|
||||||
|
price: item.price,
|
||||||
|
is_subscribed: childSubProductSet.has(item.product_id || item.product?.id),
|
||||||
|
available_quota: quotaMap.get(item.product_id || item.product?.id)?.available_quota ?? 0,
|
||||||
|
total_quota: quotaMap.get(item.product_id || item.product?.id)?.total_quota ?? 0
|
||||||
|
}))
|
||||||
|
.filter((item) => item.product_id && Number(item.price) > 0)
|
||||||
|
|
||||||
|
if (!allocProducts.value.some((item) => item.product_id === allocForm.value.productId)) {
|
||||||
|
const subscribedFirst = allocProducts.value.find((item) => item.is_subscribed)
|
||||||
|
allocForm.value.productId = subscribedFirst?.product_id || allocProducts.value[0]?.product_id || ''
|
||||||
|
}
|
||||||
} catch {
|
} catch {
|
||||||
allocForm.value.parentBalance = '0.00'
|
allocProducts.value = []
|
||||||
|
childSubscriptions.value = []
|
||||||
|
childQuotaAccounts.value = []
|
||||||
}
|
}
|
||||||
allocVisible.value = true
|
allocVisible.value = true
|
||||||
}
|
}
|
||||||
@@ -425,7 +384,11 @@ const sendAllocVerifyCode = async () => {
|
|||||||
}
|
}
|
||||||
sendCodeLoading.value = true
|
sendCodeLoading.value = true
|
||||||
try {
|
try {
|
||||||
const result = await userStore.sendCode(phone, 'login')
|
await runWithCaptcha(
|
||||||
|
async (captchaVerifyParam) => {
|
||||||
|
return await userStore.sendCode(phone, 'login', captchaVerifyParam)
|
||||||
|
},
|
||||||
|
(result) => {
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
ElMessage.success('验证码已发送')
|
ElMessage.success('验证码已发送')
|
||||||
allocCodeCountdown.value = 60
|
allocCodeCountdown.value = 60
|
||||||
@@ -439,14 +402,20 @@ const sendAllocVerifyCode = async () => {
|
|||||||
} else {
|
} else {
|
||||||
ElMessage.error(result.error?.response?.data?.message || '发送验证码失败')
|
ElMessage.error(result.error?.response?.data?.message || '发送验证码失败')
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
} finally {
|
} finally {
|
||||||
sendCodeLoading.value = false
|
sendCodeLoading.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const submitAlloc = async () => {
|
const submitAlloc = async () => {
|
||||||
if (!allocForm.value.amount) {
|
if (!allocForm.value.productId) {
|
||||||
ElMessage.warning('请输入金额')
|
ElMessage.warning('请选择产品')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!allocForm.value.callCount || Number(allocForm.value.callCount) <= 0) {
|
||||||
|
ElMessage.warning('请输入有效购买次数')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (!allocForm.value.verifyCode || allocForm.value.verifyCode.length !== 6) {
|
if (!allocForm.value.verifyCode || allocForm.value.verifyCode.length !== 6) {
|
||||||
@@ -455,18 +424,19 @@ const submitAlloc = async () => {
|
|||||||
}
|
}
|
||||||
allocLoading.value = true
|
allocLoading.value = true
|
||||||
try {
|
try {
|
||||||
const res = await subordinateApi.allocate({
|
const res = await subordinateApi.purchaseQuota({
|
||||||
child_user_id: allocForm.value.childId,
|
child_user_id: allocForm.value.childId,
|
||||||
amount: allocForm.value.amount,
|
product_id: allocForm.value.productId,
|
||||||
|
call_count: Number(allocForm.value.callCount),
|
||||||
verify_code: allocForm.value.verifyCode
|
verify_code: allocForm.value.verifyCode
|
||||||
})
|
})
|
||||||
if (res?.success) {
|
if (res?.success) {
|
||||||
ElMessage.success('划款成功')
|
ElMessage.success('额度购买成功')
|
||||||
allocVisible.value = false
|
allocVisible.value = false
|
||||||
loadList()
|
loadList()
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
ElMessage.error(e?.response?.data?.message || e?.message || '划款失败')
|
ElMessage.error(e?.response?.data?.message || e?.message || '额度购买失败')
|
||||||
} finally {
|
} finally {
|
||||||
allocLoading.value = false
|
allocLoading.value = false
|
||||||
}
|
}
|
||||||
@@ -480,7 +450,7 @@ const openAllocationRecords = async () => {
|
|||||||
|
|
||||||
const loadAllocationRecords = async () => {
|
const loadAllocationRecords = async () => {
|
||||||
try {
|
try {
|
||||||
const res = await subordinateApi.listAllocations({
|
const res = await subordinateApi.listQuotaPurchases({
|
||||||
child_user_id: allocForm.value.childId,
|
child_user_id: allocForm.value.childId,
|
||||||
page: allocationPage.value,
|
page: allocationPage.value,
|
||||||
page_size: allocationPageSize.value
|
page_size: allocationPageSize.value
|
||||||
@@ -488,145 +458,7 @@ const loadAllocationRecords = async () => {
|
|||||||
allocationRecords.value = res?.data?.items || []
|
allocationRecords.value = res?.data?.items || []
|
||||||
allocationTotal.value = res?.data?.total || 0
|
allocationTotal.value = res?.data?.total || 0
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
ElMessage.error(e?.response?.data?.message || e?.message || '加载划款记录失败')
|
ElMessage.error(e?.response?.data?.message || e?.message || '加载额度购买记录失败')
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const resetSubForm = () => {
|
|
||||||
subForm.value = {
|
|
||||||
childId: '',
|
|
||||||
childCompany: '',
|
|
||||||
productId: '',
|
|
||||||
price: ''
|
|
||||||
}
|
|
||||||
childSubscriptions.value = []
|
|
||||||
}
|
|
||||||
|
|
||||||
const openSubscribe = async (row) => {
|
|
||||||
if (!ensureCertified(row, '订阅')) return
|
|
||||||
if (!products.value.length) {
|
|
||||||
await loadProducts()
|
|
||||||
}
|
|
||||||
await loadParentSubscriptions()
|
|
||||||
subForm.value = {
|
|
||||||
childId: row.child_user_id,
|
|
||||||
childCompany: row.company_name,
|
|
||||||
productId: '',
|
|
||||||
price: ''
|
|
||||||
}
|
|
||||||
await loadChildSubscriptions()
|
|
||||||
subVisible.value = true
|
|
||||||
}
|
|
||||||
|
|
||||||
const ensureParentSubscribed = async (productId) => {
|
|
||||||
let parentPrice = getParentPrice(productId)
|
|
||||||
if (parentPrice > 0) return parentPrice
|
|
||||||
|
|
||||||
await ElMessageBox.confirm('主账号未订阅该产品,是否先为主账号订阅?', '提示', {
|
|
||||||
type: 'warning',
|
|
||||||
confirmButtonText: '先订阅主账号',
|
|
||||||
cancelButtonText: '取消'
|
|
||||||
})
|
|
||||||
const subRes = await productApi.subscribeProduct(productId)
|
|
||||||
if (!subRes?.success) {
|
|
||||||
throw new Error('主账号订阅失败,请稍后重试')
|
|
||||||
}
|
|
||||||
await loadParentSubscriptions()
|
|
||||||
parentPrice = getParentPrice(productId)
|
|
||||||
if (parentPrice <= 0) {
|
|
||||||
throw new Error('主账号订阅未生效,请稍后重试')
|
|
||||||
}
|
|
||||||
return parentPrice
|
|
||||||
}
|
|
||||||
|
|
||||||
const submitSubscribe = async () => {
|
|
||||||
if (!subForm.value.productId) {
|
|
||||||
ElMessage.warning('请选择产品')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const priceNum = Number(subForm.value.price)
|
|
||||||
if (Number.isNaN(priceNum) || priceNum <= 0) {
|
|
||||||
ElMessage.warning('请输入有效订阅价格')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
subLoading.value = true
|
|
||||||
try {
|
|
||||||
const parentPrice = await ensureParentSubscribed(subForm.value.productId)
|
|
||||||
if (priceNum < parentPrice) {
|
|
||||||
ElMessage.warning(`下属订阅价不能低于主账号订阅价(¥${formatMoney(parentPrice)})`)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const res = await subordinateApi.assignSubscription({
|
|
||||||
child_user_id: subForm.value.childId,
|
|
||||||
product_id: subForm.value.productId,
|
|
||||||
price: priceNum.toFixed(2)
|
|
||||||
})
|
|
||||||
if (res?.success) {
|
|
||||||
ElMessage.success('下属订阅已保存')
|
|
||||||
subForm.value.productId = ''
|
|
||||||
subForm.value.price = ''
|
|
||||||
await loadChildSubscriptions()
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
if (e !== 'cancel' && e !== 'close') {
|
|
||||||
ElMessage.error(e?.response?.data?.message || e?.message || '保存订阅失败')
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
subLoading.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const updateSubscription = async (row) => {
|
|
||||||
const priceNum = Number(row._editPrice)
|
|
||||||
if (Number.isNaN(priceNum) || priceNum <= 0) {
|
|
||||||
ElMessage.warning('请输入有效价格')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const parentPrice = await ensureParentSubscribed(row.product_id)
|
|
||||||
if (priceNum < parentPrice) {
|
|
||||||
ElMessage.warning(`下属订阅价不能低于主账号订阅价(¥${formatMoney(parentPrice)})`)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
row._saving = true
|
|
||||||
try {
|
|
||||||
const res = await subordinateApi.assignSubscription({
|
|
||||||
child_user_id: subForm.value.childId,
|
|
||||||
product_id: row.product_id,
|
|
||||||
price: priceNum.toFixed(2)
|
|
||||||
})
|
|
||||||
if (res?.success) {
|
|
||||||
ElMessage.success('订阅价格已更新')
|
|
||||||
await loadChildSubscriptions()
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
ElMessage.error(e?.response?.data?.message || e?.message || '更新失败')
|
|
||||||
} finally {
|
|
||||||
row._saving = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const removeSubscription = async (row) => {
|
|
||||||
await ElMessageBox.confirm('确定删除该下属订阅吗?', '确认删除', {
|
|
||||||
type: 'warning',
|
|
||||||
confirmButtonText: '删除',
|
|
||||||
cancelButtonText: '取消'
|
|
||||||
})
|
|
||||||
row._deleting = true
|
|
||||||
try {
|
|
||||||
const res = await subordinateApi.removeChildSubscription(row.id, {
|
|
||||||
child_user_id: subForm.value.childId
|
|
||||||
})
|
|
||||||
if (res?.success) {
|
|
||||||
ElMessage.success('订阅已删除')
|
|
||||||
await loadChildSubscriptions()
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
if (e !== 'cancel' && e !== 'close') {
|
|
||||||
ElMessage.error(e?.response?.data?.message || e?.message || '删除失败')
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
row._deleting = false
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -756,21 +588,6 @@ onUnmounted(() => {
|
|||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
|
|
||||||
.subscribe-add-panel {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.product-select {
|
|
||||||
flex: 1 1 auto;
|
|
||||||
min-width: 420px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.price-input {
|
|
||||||
width: 120px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
:deep(.el-table) {
|
:deep(.el-table) {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
@@ -808,18 +625,5 @@ onUnmounted(() => {
|
|||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
.subscribe-add-panel {
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: stretch;
|
|
||||||
}
|
|
||||||
|
|
||||||
.product-select {
|
|
||||||
min-width: 0;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.price-input {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -30,7 +30,7 @@
|
|||||||
</el-tag>
|
</el-tag>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="overview-balance">
|
<div v-if="!isSubordinateUser" class="overview-balance">
|
||||||
<div class="balance-label">当前账户余额</div>
|
<div class="balance-label">当前账户余额</div>
|
||||||
<div class="balance-value">¥{{ walletBalance }}</div>
|
<div class="balance-value">¥{{ walletBalance }}</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -84,10 +84,10 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 分隔线(仅认证用户显示) -->
|
<!-- 分隔线(仅认证用户显示) -->
|
||||||
<el-divider v-if="userInfo.is_certified" />
|
<el-divider v-if="userInfo.is_certified && !isSubordinateUser" />
|
||||||
|
|
||||||
<!-- 下半部分:余额预警配置(仅认证用户显示) -->
|
<!-- 下半部分:余额预警配置(仅认证用户显示) -->
|
||||||
<div v-if="userInfo.is_certified" class="balance-alert-section">
|
<div v-if="userInfo.is_certified && !isSubordinateUser" class="balance-alert-section">
|
||||||
<div class="section-header">
|
<div class="section-header">
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<el-icon class="mr-2 text-orange-600">
|
<el-icon class="mr-2 text-orange-600">
|
||||||
@@ -449,6 +449,7 @@ import { ElMessage } from 'element-plus'
|
|||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
|
const isSubordinateUser = computed(() => userStore.accountKind === 'subordinate')
|
||||||
|
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const userInfo = ref(null)
|
const userInfo = ref(null)
|
||||||
@@ -473,9 +474,13 @@ const loadUserInfo = async () => {
|
|||||||
const result = await userStore.fetchUserProfile()
|
const result = await userStore.fetchUserProfile()
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
userInfo.value = result.data
|
userInfo.value = result.data
|
||||||
|
if (!isSubordinateUser.value) {
|
||||||
await loadWalletBalance()
|
await loadWalletBalance()
|
||||||
|
} else {
|
||||||
|
walletBalance.value = '0.00'
|
||||||
|
}
|
||||||
// 只有认证用户才加载余额预警设置
|
// 只有认证用户才加载余额预警设置
|
||||||
if (result.data.is_certified) {
|
if (result.data.is_certified && !isSubordinateUser.value) {
|
||||||
await loadAlertSettings()
|
await loadAlertSettings()
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="w-full auth-fade-in">
|
<div class="w-full auth-fade-in">
|
||||||
<div class="text-center mb-6">
|
<div class="text-center mb-6">
|
||||||
<h2 class="auth-title">子账号登录</h2>
|
<h2 class="auth-title">欢迎登录</h2>
|
||||||
<p class="auth-subtitle">适用于主账号邀请的成员访问</p>
|
<p class="auth-subtitle">请输入账号信息继续访问控制台</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-6">
|
<div class="mb-6">
|
||||||
@@ -42,9 +42,9 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center justify-between text-sm">
|
<div class="flex items-center justify-between text-sm">
|
||||||
<span class="text-slate-400">邀请成员请先完成注册</span>
|
<span class="text-slate-400">还没有账号?</span>
|
||||||
<router-link to="/sub/auth/reset" class="auth-link" replace>忘记密码?</router-link>
|
<router-link to="/sub/auth/reset" class="auth-link" replace>忘记密码?</router-link>
|
||||||
<router-link to="/sub/auth/register" class="auth-link" replace>子账号注册</router-link>
|
<router-link to="/sub/auth/register" class="auth-link" replace>注册账号</router-link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<el-button type="primary" size="large" class="auth-button w-full !h-12" native-type="submit" :loading="loading"
|
<el-button type="primary" size="large" class="auth-button w-full !h-12" native-type="submit" :loading="loading"
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="w-full auth-fade-in">
|
<div class="w-full auth-fade-in">
|
||||||
<div class="text-center mb-6">
|
<div class="text-center mb-6">
|
||||||
<h2 class="auth-title">子账号注册</h2>
|
<h2 class="auth-title">注册账号</h2>
|
||||||
<p class="auth-subtitle">填写邀请码与手机号,完成成员账号开通</p>
|
<p class="auth-subtitle">填写邀请码与手机号,完成账号注册</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form class="space-y-4" @submit.prevent="onRegister">
|
<form class="space-y-4" @submit.prevent="onRegister">
|
||||||
<div>
|
<div>
|
||||||
<label class="auth-label">邀请码 <span class="text-red-500">*</span></label>
|
<label class="auth-label">邀请码 <span class="text-red-500">*</span></label>
|
||||||
<el-input v-model="form.inviteToken" placeholder="请输入主账号提供的邀请码" size="large" clearable :disabled="loading"
|
<el-input v-model="form.inviteToken" placeholder="请输入邀请码" size="large" clearable :disabled="loading"
|
||||||
class="auth-input" />
|
class="auth-input" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@@ -44,7 +44,7 @@
|
|||||||
</form>
|
</form>
|
||||||
|
|
||||||
<p class="mt-4 text-xs text-center text-slate-400">
|
<p class="mt-4 text-xs text-center text-slate-400">
|
||||||
请确保邀请码来自可信主账号,避免账号安全风险
|
请确认邀请码来源可信,避免账号安全风险
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -67,7 +67,7 @@
|
|||||||
|
|
||||||
<div v-else-if="subscriptions.length === 0" class="text-center py-12">
|
<div v-else-if="subscriptions.length === 0" class="text-center py-12">
|
||||||
<el-empty description="暂无订阅数据">
|
<el-empty description="暂无订阅数据">
|
||||||
<el-button type="primary" @click="$router.push('/products')">
|
<el-button v-if="!isSubordinate" type="primary" @click="$router.push('/products')">
|
||||||
去数据大厅订阅产品
|
去数据大厅订阅产品
|
||||||
</el-button>
|
</el-button>
|
||||||
</el-empty>
|
</el-empty>
|
||||||
@@ -122,6 +122,7 @@
|
|||||||
</el-table-column>
|
</el-table-column>
|
||||||
|
|
||||||
<el-table-column
|
<el-table-column
|
||||||
|
v-if="!isSubordinate"
|
||||||
prop="price"
|
prop="price"
|
||||||
label="订阅价格"
|
label="订阅价格"
|
||||||
:width="isMobile ? 100 : 120"
|
:width="isMobile ? 100 : 120"
|
||||||
@@ -131,6 +132,16 @@
|
|||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column
|
||||||
|
v-if="isSubordinate"
|
||||||
|
label="剩余额度"
|
||||||
|
:width="isMobile ? 110 : 130"
|
||||||
|
>
|
||||||
|
<template #default="{ row }">
|
||||||
|
<span class="font-semibold text-blue-600">{{ getRemainingQuota(row) }} 次</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
<!-- <el-table-column prop="api_used" label="API调用次数" width="140">
|
<!-- <el-table-column prop="api_used" label="API调用次数" width="140">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<span class="font-medium">{{ row.api_used || 0 }}</span>
|
<span class="font-medium">{{ row.api_used || 0 }}</span>
|
||||||
@@ -258,7 +269,7 @@
|
|||||||
<div class="usage-stat-value">{{ usageData?.api_used || 0 }}</div>
|
<div class="usage-stat-value">{{ usageData?.api_used || 0 }}</div>
|
||||||
<div class="usage-stat-label">API调用次数</div>
|
<div class="usage-stat-label">API调用次数</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="usage-stat-card">
|
<div v-if="!isSubordinate" class="usage-stat-card">
|
||||||
<div class="usage-stat-value">¥{{ formatPrice(selectedSubscription.price) }}</div>
|
<div class="usage-stat-value">¥{{ formatPrice(selectedSubscription.price) }}</div>
|
||||||
<div class="usage-stat-label">订阅价格</div>
|
<div class="usage-stat-label">订阅价格</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -281,16 +292,19 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { subscriptionApi } from '@/api'
|
import { subordinateApi, subscriptionApi } from '@/api'
|
||||||
import FilterItem from '@/components/common/FilterItem.vue'
|
import FilterItem from '@/components/common/FilterItem.vue'
|
||||||
import FilterSection from '@/components/common/FilterSection.vue'
|
import FilterSection from '@/components/common/FilterSection.vue'
|
||||||
import ListPageLayout from '@/components/common/ListPageLayout.vue'
|
import ListPageLayout from '@/components/common/ListPageLayout.vue'
|
||||||
import { useMobileTable } from '@/composables/useMobileTable'
|
import { useMobileTable } from '@/composables/useMobileTable'
|
||||||
import { ArrowDown } from '@element-plus/icons-vue'
|
import { ArrowDown } from '@element-plus/icons-vue'
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
|
import { useUserStore } from '@/stores/user'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const { isMobile } = useMobileTable()
|
const { isMobile } = useMobileTable()
|
||||||
|
const userStore = useUserStore()
|
||||||
|
const isSubordinate = computed(() => userStore.accountKind === 'subordinate')
|
||||||
|
|
||||||
// 响应式数据
|
// 响应式数据
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
@@ -302,6 +316,7 @@ const usageDialogVisible = ref(false)
|
|||||||
const selectedSubscription = ref(null)
|
const selectedSubscription = ref(null)
|
||||||
const usageData = ref(null)
|
const usageData = ref(null)
|
||||||
const loadingUsage = ref(false)
|
const loadingUsage = ref(false)
|
||||||
|
const myQuotaMap = ref({})
|
||||||
|
|
||||||
// 统计数据
|
// 统计数据
|
||||||
const stats = ref({
|
const stats = ref({
|
||||||
@@ -344,6 +359,9 @@ const loadSubscriptions = async () => {
|
|||||||
const response = await subscriptionApi.getMySubscriptions(params)
|
const response = await subscriptionApi.getMySubscriptions(params)
|
||||||
subscriptions.value = response.data?.items || []
|
subscriptions.value = response.data?.items || []
|
||||||
total.value = response.data?.total || 0
|
total.value = response.data?.total || 0
|
||||||
|
if (isSubordinate.value) {
|
||||||
|
await loadMyQuotas()
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('加载订阅失败:', error)
|
console.error('加载订阅失败:', error)
|
||||||
ElMessage.error('加载订阅失败')
|
ElMessage.error('加载订阅失败')
|
||||||
@@ -352,6 +370,30 @@ const loadSubscriptions = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 加载我的额度账户(子账号按次数扣减时展示剩余额度)
|
||||||
|
const loadMyQuotas = async () => {
|
||||||
|
try {
|
||||||
|
const res = await subordinateApi.listMyQuotas()
|
||||||
|
const items = res?.data || []
|
||||||
|
const map = {}
|
||||||
|
items.forEach((item) => {
|
||||||
|
map[item.product_id] = item.available_quota
|
||||||
|
})
|
||||||
|
myQuotaMap.value = map
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载我的额度账户失败:', error)
|
||||||
|
myQuotaMap.value = {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getRemainingQuota = (row) => {
|
||||||
|
const productID = row?.product_id || row?.product?.id
|
||||||
|
if (!productID) return '-'
|
||||||
|
const value = myQuotaMap.value[productID]
|
||||||
|
if (value === undefined || value === null) return '0'
|
||||||
|
return String(value)
|
||||||
|
}
|
||||||
|
|
||||||
// 加载统计数据
|
// 加载统计数据
|
||||||
const loadStats = async () => {
|
const loadStats = async () => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
|
import { isCurrentOrigin, isPortalDomainConfigReady, isSubPortal, mainPortalOrigin, subPortalOrigin } from '@/constants/portal'
|
||||||
import { useUserStore } from '@/stores/user'
|
import { useUserStore } from '@/stores/user'
|
||||||
import { createRouter, createWebHistory } from 'vue-router'
|
import { createRouter, createWebHistory } from 'vue-router'
|
||||||
import { isCurrentOrigin, isPortalDomainConfigReady, isSubPortal, mainPortalOrigin, subPortalOrigin } from '@/constants/portal'
|
|
||||||
import { statisticsRoutes } from './modules/statistics'
|
import { statisticsRoutes } from './modules/statistics'
|
||||||
|
|
||||||
// 路由配置
|
// 路由配置
|
||||||
@@ -61,13 +61,13 @@ const routes = [
|
|||||||
path: 'login',
|
path: 'login',
|
||||||
name: 'SubLogin',
|
name: 'SubLogin',
|
||||||
component: () => import('@/pages/sub-portal/SubLogin.vue'),
|
component: () => import('@/pages/sub-portal/SubLogin.vue'),
|
||||||
meta: { title: '子账号登录' }
|
meta: { title: '登录' }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'register',
|
path: 'register',
|
||||||
name: 'SubRegister',
|
name: 'SubRegister',
|
||||||
component: () => import('@/pages/sub-portal/SubRegister.vue'),
|
component: () => import('@/pages/sub-portal/SubRegister.vue'),
|
||||||
meta: { title: '子账号注册' }
|
meta: { title: '注册' }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'reset',
|
path: 'reset',
|
||||||
@@ -440,13 +440,10 @@ router.beforeEach(async (to, from, next) => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 登录态下强制要求主/子域配置完整,避免混域运行
|
// 登录态下若未配置主/子域名,降级为单域运行(不强制登出)
|
||||||
if (userStore.isLoggedIn && !isPortalDomainConfigReady) {
|
// 否则会出现“登录成功后立即被清会话”的问题。
|
||||||
if (import.meta.env.PROD) {
|
if (userStore.isLoggedIn && !isPortalDomainConfigReady && import.meta.env.PROD) {
|
||||||
userStore.logout()
|
console.warn('[router] 未配置主/子域名,当前以单域模式运行,跳过域名隔离跳转')
|
||||||
next(loginPathByRoute)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 域名隔离:子账号登录态必须在子账号专属域名
|
// 域名隔离:子账号登录态必须在子账号专属域名
|
||||||
@@ -467,7 +464,7 @@ router.beforeEach(async (to, from, next) => {
|
|||||||
|
|
||||||
// 已登录用户访问认证页面,按账号类型重定向到对应首页
|
// 已登录用户访问认证页面,按账号类型重定向到对应首页
|
||||||
if (isAuthRoute && userStore.isLoggedIn) {
|
if (isAuthRoute && userStore.isLoggedIn) {
|
||||||
next(userStore.accountKind === 'subordinate' ? '/subscriptions' : '/dashboard')
|
next(userStore.accountKind === 'subordinate' ? '/subscriptions' : '/products')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user