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_ENCRYPTED_MODE=false
|
||||
# 子账号专属前端域名(生产建议配置,如 https://subsole.tianyuanapi.com)
|
||||
VITE_SUB_PORTAL_BASE_URL=""
|
||||
VITE_SUB_PORTAL_BASE_URL="https://subsole.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']
|
||||
ElSelect: typeof import('element-plus/es')['ElSelect']
|
||||
ElSkeleton: typeof import('element-plus/es')['ElSkeleton']
|
||||
ElSpace: typeof import('element-plus/es')['ElSpace']
|
||||
ElSubMenu: typeof import('element-plus/es')['ElSubMenu']
|
||||
ElSwitch: typeof import('element-plus/es')['ElSwitch']
|
||||
ElTable: typeof import('element-plus/es')['ElTable']
|
||||
|
||||
@@ -49,6 +49,10 @@ export const subordinateApi = {
|
||||
listSubordinates: (params) => request.get('/subordinate/subordinates', { params }),
|
||||
allocate: (data) => request.post('/subordinate/allocate', data),
|
||||
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),
|
||||
listChildSubscriptions: (params) => request.get('/subordinate/child-subscriptions', { params }),
|
||||
removeChildSubscription: (subscriptionId, data) => request.delete(`/subordinate/child-subscriptions/${subscriptionId}`, { data })
|
||||
|
||||
@@ -107,7 +107,6 @@ export const getMenuItems = (userType = 'user') => {
|
||||
|
||||
// 子账号/子站壳:受限侧栏(不展示主站运营外链入口由布局控制)
|
||||
export const getSubordinateMenuItems = () => {
|
||||
const financeGroup = userMenuItems.find((g) => g.group === '财务管理')?.children || []
|
||||
const devGroup = userMenuItems.find((g) => g.group === '开发者中心')?.children || []
|
||||
|
||||
return [
|
||||
@@ -126,11 +125,6 @@ export const getSubordinateMenuItems = () => {
|
||||
{ name: '企业入驻', path: '/profile/certification', icon: ShieldCheck }
|
||||
]
|
||||
},
|
||||
{
|
||||
group: '财务管理',
|
||||
icon: Wallet,
|
||||
children: financeGroup.filter((item) => item.path === '/finance/transactions')
|
||||
},
|
||||
{
|
||||
group: '开发者中心',
|
||||
icon: Setting,
|
||||
|
||||
@@ -73,7 +73,7 @@
|
||||
|
||||
<div v-else-if="transactions.length === 0" class="text-center py-12">
|
||||
<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-empty>
|
||||
@@ -203,9 +203,12 @@ import FilterSection from '@/components/common/FilterSection.vue'
|
||||
import ListPageLayout from '@/components/common/ListPageLayout.vue'
|
||||
import { useMobileTable } from '@/composables/useMobileTable'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
|
||||
// 移动端检测
|
||||
const { isMobile } = useMobileTable()
|
||||
const userStore = useUserStore()
|
||||
const isSubordinate = computed(() => userStore.accountKind === 'subordinate')
|
||||
|
||||
// 响应式数据
|
||||
const loading = ref(false)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<ListPageLayout
|
||||
title="下属"
|
||||
subtitle="管理下属企业信息、余额与订阅配置"
|
||||
subtitle="管理下属企业信息与额度购买"
|
||||
>
|
||||
<template #filters>
|
||||
<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>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="180" fixed="right">
|
||||
<el-table-column label="操作" width="110" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<div class="flex items-center gap-1">
|
||||
<el-button
|
||||
@@ -72,16 +72,7 @@
|
||||
bg
|
||||
@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>
|
||||
</div>
|
||||
</template>
|
||||
@@ -107,7 +98,7 @@
|
||||
</template>
|
||||
|
||||
<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="info-grid">
|
||||
<div class="info-card">
|
||||
@@ -115,15 +106,34 @@
|
||||
<div class="info-value text-blue-600">¥{{ formatMoney(allocForm.parentBalance) }}</div>
|
||||
</div>
|
||||
<div class="info-card">
|
||||
<div class="info-label">下属余额</div>
|
||||
<div class="info-value text-emerald-600">¥{{ formatMoney(allocForm.childBalance) }}</div>
|
||||
<div class="info-label">预计扣款</div>
|
||||
<div class="info-value text-emerald-600">¥{{ formatMoney(allocTotalAmount) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-sm text-gray-600">下属企业:{{ allocForm.childCompany || '未认证' }}</div>
|
||||
<el-input v-model="allocForm.amount" placeholder="金额(元)">
|
||||
<template #prefix>¥</template>
|
||||
</el-input>
|
||||
<div class="text-sm font-medium text-gray-700">当前已订阅产品与剩余额度</div>
|
||||
<el-table :data="subscribedProductRows" border stripe size="small" max-height="220">
|
||||
<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">
|
||||
<el-input v-model="allocForm.verifyCode" maxlength="6" placeholder="请输入手机验证码" />
|
||||
@@ -139,7 +149,7 @@
|
||||
|
||||
<div class="flex justify-end">
|
||||
<el-button class="table-action-btn action-subscribe" text bg @click="openAllocationRecords">
|
||||
划款记录
|
||||
额度购买记录
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -149,82 +159,22 @@
|
||||
</template>
|
||||
</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-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 }">
|
||||
<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>
|
||||
</el-table-column>
|
||||
<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 }">
|
||||
{{ formatDateTime(row.created_at) }}
|
||||
</template>
|
||||
@@ -249,13 +199,15 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { financeApi, productApi, subordinateApi, subscriptionApi } from '@/api'
|
||||
import { financeApi, subordinateApi, subscriptionApi } from '@/api'
|
||||
import ListPageLayout from '@/components/common/ListPageLayout.vue'
|
||||
import { useAliyunCaptcha } from '@/composables/useAliyunCaptcha'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import { Loading } from '@element-plus/icons-vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
const userStore = useUserStore()
|
||||
const { runWithCaptcha } = useAliyunCaptcha()
|
||||
const list = ref([])
|
||||
const total = ref(0)
|
||||
const page = ref(1)
|
||||
@@ -271,11 +223,14 @@ let allocCodeTimer = null
|
||||
const allocForm = ref({
|
||||
childId: '',
|
||||
childCompany: '',
|
||||
amount: '',
|
||||
productId: '',
|
||||
callCount: 1,
|
||||
verifyCode: '',
|
||||
parentBalance: '0.00',
|
||||
childBalance: '0.00'
|
||||
parentBalance: '0.00'
|
||||
})
|
||||
const allocProducts = ref([])
|
||||
const childSubscriptions = ref([])
|
||||
const childQuotaAccounts = ref([])
|
||||
|
||||
const allocRecordVisible = ref(false)
|
||||
const allocationRecords = ref([])
|
||||
@@ -283,20 +238,21 @@ const allocationTotal = ref(0)
|
||||
const allocationPage = ref(1)
|
||||
const allocationPageSize = ref(10)
|
||||
|
||||
const subVisible = ref(false)
|
||||
const subLoading = ref(false)
|
||||
const productLoading = ref(false)
|
||||
const products = ref([])
|
||||
const parentSubscriptions = ref([])
|
||||
const childSubscriptions = ref([])
|
||||
const subForm = ref({
|
||||
childId: '',
|
||||
childCompany: '',
|
||||
productId: '',
|
||||
price: ''
|
||||
})
|
||||
|
||||
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) => {
|
||||
if (!value) return '-'
|
||||
@@ -311,16 +267,6 @@ const formatMoney = (value) => {
|
||||
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 () => {
|
||||
listLoading.value = true
|
||||
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 () => {
|
||||
invLoading.value = true
|
||||
try {
|
||||
@@ -388,11 +302,14 @@ const resetAllocForm = () => {
|
||||
allocForm.value = {
|
||||
childId: '',
|
||||
childCompany: '',
|
||||
amount: '',
|
||||
productId: '',
|
||||
callCount: 1,
|
||||
verifyCode: '',
|
||||
parentBalance: '0.00',
|
||||
childBalance: '0.00'
|
||||
parentBalance: '0.00'
|
||||
}
|
||||
allocProducts.value = []
|
||||
childSubscriptions.value = []
|
||||
childQuotaAccounts.value = []
|
||||
}
|
||||
|
||||
const ensureCertified = (row, actionName) => {
|
||||
@@ -402,16 +319,58 @@ const ensureCertified = (row, actionName) => {
|
||||
}
|
||||
|
||||
const openAlloc = async (row) => {
|
||||
if (!ensureCertified(row, '划款')) return
|
||||
if (!ensureCertified(row, '购买额度')) return
|
||||
resetAllocForm()
|
||||
allocForm.value.childId = row.child_user_id
|
||||
allocForm.value.childCompany = row.company_name
|
||||
allocForm.value.childBalance = row.balance || '0.00'
|
||||
try {
|
||||
const walletRes = await financeApi.getWallet()
|
||||
const walletRes = await financeApi.getWallet().catch(() => null)
|
||||
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 {
|
||||
allocForm.value.parentBalance = '0.00'
|
||||
allocProducts.value = []
|
||||
childSubscriptions.value = []
|
||||
childQuotaAccounts.value = []
|
||||
}
|
||||
allocVisible.value = true
|
||||
}
|
||||
@@ -425,28 +384,38 @@ const sendAllocVerifyCode = async () => {
|
||||
}
|
||||
sendCodeLoading.value = true
|
||||
try {
|
||||
const result = await userStore.sendCode(phone, 'login')
|
||||
if (result.success) {
|
||||
ElMessage.success('验证码已发送')
|
||||
allocCodeCountdown.value = 60
|
||||
allocCodeTimer = setInterval(() => {
|
||||
allocCodeCountdown.value--
|
||||
if (allocCodeCountdown.value <= 0 && allocCodeTimer) {
|
||||
clearInterval(allocCodeTimer)
|
||||
allocCodeTimer = null
|
||||
await runWithCaptcha(
|
||||
async (captchaVerifyParam) => {
|
||||
return await userStore.sendCode(phone, 'login', captchaVerifyParam)
|
||||
},
|
||||
(result) => {
|
||||
if (result.success) {
|
||||
ElMessage.success('验证码已发送')
|
||||
allocCodeCountdown.value = 60
|
||||
allocCodeTimer = setInterval(() => {
|
||||
allocCodeCountdown.value--
|
||||
if (allocCodeCountdown.value <= 0 && allocCodeTimer) {
|
||||
clearInterval(allocCodeTimer)
|
||||
allocCodeTimer = null
|
||||
}
|
||||
}, 1000)
|
||||
} else {
|
||||
ElMessage.error(result.error?.response?.data?.message || '发送验证码失败')
|
||||
}
|
||||
}, 1000)
|
||||
} else {
|
||||
ElMessage.error(result.error?.response?.data?.message || '发送验证码失败')
|
||||
}
|
||||
}
|
||||
)
|
||||
} finally {
|
||||
sendCodeLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const submitAlloc = async () => {
|
||||
if (!allocForm.value.amount) {
|
||||
ElMessage.warning('请输入金额')
|
||||
if (!allocForm.value.productId) {
|
||||
ElMessage.warning('请选择产品')
|
||||
return
|
||||
}
|
||||
if (!allocForm.value.callCount || Number(allocForm.value.callCount) <= 0) {
|
||||
ElMessage.warning('请输入有效购买次数')
|
||||
return
|
||||
}
|
||||
if (!allocForm.value.verifyCode || allocForm.value.verifyCode.length !== 6) {
|
||||
@@ -455,18 +424,19 @@ const submitAlloc = async () => {
|
||||
}
|
||||
allocLoading.value = true
|
||||
try {
|
||||
const res = await subordinateApi.allocate({
|
||||
const res = await subordinateApi.purchaseQuota({
|
||||
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
|
||||
})
|
||||
if (res?.success) {
|
||||
ElMessage.success('划款成功')
|
||||
ElMessage.success('额度购买成功')
|
||||
allocVisible.value = false
|
||||
loadList()
|
||||
}
|
||||
} catch (e) {
|
||||
ElMessage.error(e?.response?.data?.message || e?.message || '划款失败')
|
||||
ElMessage.error(e?.response?.data?.message || e?.message || '额度购买失败')
|
||||
} finally {
|
||||
allocLoading.value = false
|
||||
}
|
||||
@@ -480,7 +450,7 @@ const openAllocationRecords = async () => {
|
||||
|
||||
const loadAllocationRecords = async () => {
|
||||
try {
|
||||
const res = await subordinateApi.listAllocations({
|
||||
const res = await subordinateApi.listQuotaPurchases({
|
||||
child_user_id: allocForm.value.childId,
|
||||
page: allocationPage.value,
|
||||
page_size: allocationPageSize.value
|
||||
@@ -488,145 +458,7 @@ const loadAllocationRecords = async () => {
|
||||
allocationRecords.value = res?.data?.items || []
|
||||
allocationTotal.value = res?.data?.total || 0
|
||||
} catch (e) {
|
||||
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
|
||||
ElMessage.error(e?.response?.data?.message || e?.message || '加载额度购买记录失败')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -756,21 +588,6 @@ onUnmounted(() => {
|
||||
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) {
|
||||
:deep(.el-table) {
|
||||
font-size: 12px;
|
||||
@@ -808,18 +625,5 @@ onUnmounted(() => {
|
||||
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>
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
</el-tag>
|
||||
</div>
|
||||
</div>
|
||||
<div class="overview-balance">
|
||||
<div v-if="!isSubordinateUser" class="overview-balance">
|
||||
<div class="balance-label">当前账户余额</div>
|
||||
<div class="balance-value">¥{{ walletBalance }}</div>
|
||||
</div>
|
||||
@@ -84,10 +84,10 @@
|
||||
</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="flex items-center">
|
||||
<el-icon class="mr-2 text-orange-600">
|
||||
@@ -449,6 +449,7 @@ import { ElMessage } from 'element-plus'
|
||||
|
||||
const router = useRouter()
|
||||
const userStore = useUserStore()
|
||||
const isSubordinateUser = computed(() => userStore.accountKind === 'subordinate')
|
||||
|
||||
const loading = ref(false)
|
||||
const userInfo = ref(null)
|
||||
@@ -473,9 +474,13 @@ const loadUserInfo = async () => {
|
||||
const result = await userStore.fetchUserProfile()
|
||||
if (result.success) {
|
||||
userInfo.value = result.data
|
||||
await loadWalletBalance()
|
||||
if (!isSubordinateUser.value) {
|
||||
await loadWalletBalance()
|
||||
} else {
|
||||
walletBalance.value = '0.00'
|
||||
}
|
||||
// 只有认证用户才加载余额预警设置
|
||||
if (result.data.is_certified) {
|
||||
if (result.data.is_certified && !isSubordinateUser.value) {
|
||||
await loadAlertSettings()
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<template>
|
||||
<div class="w-full auth-fade-in">
|
||||
<div class="text-center mb-6">
|
||||
<h2 class="auth-title">子账号登录</h2>
|
||||
<p class="auth-subtitle">适用于主账号邀请的成员访问</p>
|
||||
<h2 class="auth-title">欢迎登录</h2>
|
||||
<p class="auth-subtitle">请输入账号信息继续访问控制台</p>
|
||||
</div>
|
||||
|
||||
<div class="mb-6">
|
||||
@@ -42,9 +42,9 @@
|
||||
</div>
|
||||
|
||||
<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/register" class="auth-link" replace>子账号注册</router-link>
|
||||
<router-link to="/sub/auth/register" class="auth-link" replace>注册账号</router-link>
|
||||
</div>
|
||||
|
||||
<el-button type="primary" size="large" class="auth-button w-full !h-12" native-type="submit" :loading="loading"
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
<template>
|
||||
<div class="w-full auth-fade-in">
|
||||
<div class="text-center mb-6">
|
||||
<h2 class="auth-title">子账号注册</h2>
|
||||
<p class="auth-subtitle">填写邀请码与手机号,完成成员账号开通</p>
|
||||
<h2 class="auth-title">注册账号</h2>
|
||||
<p class="auth-subtitle">填写邀请码与手机号,完成账号注册</p>
|
||||
</div>
|
||||
|
||||
<form class="space-y-4" @submit.prevent="onRegister">
|
||||
<div>
|
||||
<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" />
|
||||
</div>
|
||||
<div>
|
||||
@@ -44,7 +44,7 @@
|
||||
</form>
|
||||
|
||||
<p class="mt-4 text-xs text-center text-slate-400">
|
||||
请确保邀请码来自可信主账号,避免账号安全风险
|
||||
请确认邀请码来源可信,避免账号安全风险
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -67,7 +67,7 @@
|
||||
|
||||
<div v-else-if="subscriptions.length === 0" class="text-center py-12">
|
||||
<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-empty>
|
||||
@@ -122,6 +122,7 @@
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column
|
||||
v-if="!isSubordinate"
|
||||
prop="price"
|
||||
label="订阅价格"
|
||||
:width="isMobile ? 100 : 120"
|
||||
@@ -131,6 +132,16 @@
|
||||
</template>
|
||||
</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">
|
||||
<template #default="{ row }">
|
||||
<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-label">API调用次数</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-label">订阅价格</div>
|
||||
</div>
|
||||
@@ -281,16 +292,19 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { subscriptionApi } from '@/api'
|
||||
import { subordinateApi, subscriptionApi } from '@/api'
|
||||
import FilterItem from '@/components/common/FilterItem.vue'
|
||||
import FilterSection from '@/components/common/FilterSection.vue'
|
||||
import ListPageLayout from '@/components/common/ListPageLayout.vue'
|
||||
import { useMobileTable } from '@/composables/useMobileTable'
|
||||
import { ArrowDown } from '@element-plus/icons-vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
|
||||
const router = useRouter()
|
||||
const { isMobile } = useMobileTable()
|
||||
const userStore = useUserStore()
|
||||
const isSubordinate = computed(() => userStore.accountKind === 'subordinate')
|
||||
|
||||
// 响应式数据
|
||||
const loading = ref(false)
|
||||
@@ -302,6 +316,7 @@ const usageDialogVisible = ref(false)
|
||||
const selectedSubscription = ref(null)
|
||||
const usageData = ref(null)
|
||||
const loadingUsage = ref(false)
|
||||
const myQuotaMap = ref({})
|
||||
|
||||
// 统计数据
|
||||
const stats = ref({
|
||||
@@ -344,6 +359,9 @@ const loadSubscriptions = async () => {
|
||||
const response = await subscriptionApi.getMySubscriptions(params)
|
||||
subscriptions.value = response.data?.items || []
|
||||
total.value = response.data?.total || 0
|
||||
if (isSubordinate.value) {
|
||||
await loadMyQuotas()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载订阅失败:', 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 () => {
|
||||
try {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { isCurrentOrigin, isPortalDomainConfigReady, isSubPortal, mainPortalOrigin, subPortalOrigin } from '@/constants/portal'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import { isCurrentOrigin, isPortalDomainConfigReady, isSubPortal, mainPortalOrigin, subPortalOrigin } from '@/constants/portal'
|
||||
import { statisticsRoutes } from './modules/statistics'
|
||||
|
||||
// 路由配置
|
||||
@@ -61,13 +61,13 @@ const routes = [
|
||||
path: 'login',
|
||||
name: 'SubLogin',
|
||||
component: () => import('@/pages/sub-portal/SubLogin.vue'),
|
||||
meta: { title: '子账号登录' }
|
||||
meta: { title: '登录' }
|
||||
},
|
||||
{
|
||||
path: 'register',
|
||||
name: 'SubRegister',
|
||||
component: () => import('@/pages/sub-portal/SubRegister.vue'),
|
||||
meta: { title: '子账号注册' }
|
||||
meta: { title: '注册' }
|
||||
},
|
||||
{
|
||||
path: 'reset',
|
||||
@@ -440,13 +440,10 @@ router.beforeEach(async (to, from, next) => {
|
||||
return
|
||||
}
|
||||
|
||||
// 登录态下强制要求主/子域配置完整,避免混域运行
|
||||
if (userStore.isLoggedIn && !isPortalDomainConfigReady) {
|
||||
if (import.meta.env.PROD) {
|
||||
userStore.logout()
|
||||
next(loginPathByRoute)
|
||||
return
|
||||
}
|
||||
// 登录态下若未配置主/子域名,降级为单域运行(不强制登出)
|
||||
// 否则会出现“登录成功后立即被清会话”的问题。
|
||||
if (userStore.isLoggedIn && !isPortalDomainConfigReady && import.meta.env.PROD) {
|
||||
console.warn('[router] 未配置主/子域名,当前以单域模式运行,跳过域名隔离跳转')
|
||||
}
|
||||
|
||||
// 域名隔离:子账号登录态必须在子账号专属域名
|
||||
@@ -467,7 +464,7 @@ router.beforeEach(async (to, from, next) => {
|
||||
|
||||
// 已登录用户访问认证页面,按账号类型重定向到对应首页
|
||||
if (isAuthRoute && userStore.isLoggedIn) {
|
||||
next(userStore.accountKind === 'subordinate' ? '/subscriptions' : '/dashboard')
|
||||
next(userStore.accountKind === 'subordinate' ? '/subscriptions' : '/products')
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user