Commit ad7da22d by renyizhao

添加按次计费 登录可选获取令牌

parent 60a32cda
...@@ -61,3 +61,22 @@ export function getRecharge(id) { ...@@ -61,3 +61,22 @@ export function getRecharge(id) {
params: { id } params: { id }
}) })
} }
// 获取个人token日志(消费/充值)
// type: 1=充值 2=消费
export function getTokenLog(params) {
return request({
url: '/app/ai-log/self',
method: 'get',
params
})
}
// 获取个人token日志统计(quota / rpm / tpm)
export function getTokenLogStat(params) {
return request({
url: '/app/ai-log/stat',
method: 'get',
params
})
}
...@@ -260,6 +260,18 @@ export const constantRoutes = [ ...@@ -260,6 +260,18 @@ export const constantRoutes = [
{ {
path: '/console', path: '/console',
component: ManageLayout, component: ManageLayout,
children: [
{
path: 'tokenLog',
component: () => import('@/views/console/tokenLog.vue'),
name: 'TokenLog',
meta: {title: 'token使用记录', icon: 'instock'}
}
]
},
{
path: '/console',
component: ManageLayout,
hidden: true, hidden: true,
children: [ children: [
{ {
......
...@@ -279,6 +279,7 @@ ...@@ -279,6 +279,7 @@
<!-- 底部固定:未登录或未获取令牌时显示 --> <!-- 底部固定:未登录或未获取令牌时显示 -->
<transition name="el-fade-in" v-if="tabActive === 999"> <transition name="el-fade-in" v-if="tabActive === 999">
<!-- 未获取令牌:引导用户获取 -->
<div v-if="!hasToken" class="bottom-cta"> <div v-if="!hasToken" class="bottom-cta">
<div class="cta-card"> <div class="cta-card">
<div class="cta-text"> <div class="cta-text">
...@@ -294,6 +295,20 @@ ...@@ -294,6 +295,20 @@
>{{ ctaButtonText }}</el-button> >{{ ctaButtonText }}</el-button>
</div> </div>
</div> </div>
<!-- 已有令牌:提示可在个人中心查看 -->
<div v-else class="bottom-cta">
<div class="cta-card cta-card--info">
<div class="cta-text">
<div class="cta-title">已获取 API 令牌</div>
<div class="cta-subtitle">控制台个人中心可查看令牌</div>
</div>
<el-button
size="large"
class="cta-btn"
@click="$router.push('/console/overview')"
>前往查看</el-button>
</div>
</div>
</transition> </transition>
</div> </div>
...@@ -1064,6 +1079,24 @@ onMounted(() => { ...@@ -1064,6 +1079,24 @@ onMounted(() => {
0 0 0 1px rgba(46, 119, 227, 0.05); 0 0 0 1px rgba(46, 119, 227, 0.05);
} }
// 已有令牌:信息提示样式(弱化主按钮,降低视觉权重)
.cta-card--info {
box-shadow:
0 -2px 12px rgba(0, 0, 0, 0.03),
0 6px 18px rgba(46, 119, 227, 0.06),
0 0 0 1px rgba(46, 119, 227, 0.04);
.cta-btn {
color: var(--el-color-primary);
background: #f0f5ff;
border-color: #dbe5ff;
}
.cta-btn:hover {
background: #e0ebff;
}
}
.cta-text { .cta-text {
flex: 1; flex: 1;
min-width: 0; min-width: 0;
......
<template> <template>
<div class="model-card"> <div class="model-card">
<!-- 右上角:按量收费 --> <!-- 右上角:按量/按次收费 -->
<div class="tag-top">按量收费</div> <div class="tag-top">{{ billingTagText }}</div>
<!-- 头部:图标 + 模型名 --> <!-- 头部:图标 + 模型名 -->
<div class="card-header"> <div class="card-header">
...@@ -20,15 +20,22 @@ ...@@ -20,15 +20,22 @@
{{ model.description || '暂无描述' }} {{ model.description || '暂无描述' }}
</div> </div>
<!-- 底部:收费价格 tag + 输入/输出价格 --> <!-- 底部:收费价格 tag + 价格(按量/按次不同展示) -->
<div class="card-footer"> <div class="card-footer">
<div class="tag-price">收费价格</div> <div class="tag-price">{{ priceTagText }}</div>
<div class="price-text"> <div class="price-text">
<span>输入:<b>¥{{ formatPrice(model.inputPrice) }}</b></span> <!-- 按次计费:单次价格 -->
<span class="unit"> / 1M tokens</span> <template v-if="isPerCall">
<span class="divider"></span> <span>每次调用<b>¥{{ formatPrice(model.inputPrice) }}</b></span>
<span>输出:<b>¥{{ formatPrice(model.outputPrice) }}</b></span> </template>
<span class="unit"> / 1M tokens</span> <!-- 按量计费:输入/输出 -->
<template v-else>
<span>输入:<b>¥{{ formatPrice(model.inputPrice) }}</b></span>
<span class="unit"> / 1M tokens</span>
<span class="divider"></span>
<span>输出:<b>¥{{ formatPrice(model.outputPrice) }}</b></span>
<span class="unit"> / 1M tokens</span>
</template>
</div> </div>
</div> </div>
</div> </div>
...@@ -57,6 +64,15 @@ const iconInfo = computed(() => { ...@@ -57,6 +64,15 @@ const iconInfo = computed(() => {
return { name: '', variant: 'Color' } return { name: '', variant: 'Color' }
}) })
// 是否按次计费(quotaType === 1)
const isPerCall = computed(() => Number(props.model?.quotaType) === 1)
// 右上角 tag 文案
const billingTagText = computed(() => (isPerCall.value ? '按次收费' : '按量收费'))
// 底部 tag 文案
const priceTagText = computed(() => (isPerCall.value ? '单次价格' : '收费价格'))
function formatPrice(price) { function formatPrice(price) {
if (price === null || price === undefined) return '-' if (price === null || price === undefined) return '-'
const num = Number(price) const num = Number(price)
......
...@@ -63,6 +63,10 @@ ...@@ -63,6 +63,10 @@
</el-tab-pane> </el-tab-pane>
</el-tabs> </el-tabs>
<div class="mb20" style="height: 20px;">
<el-checkbox v-model="loginForm.autoCreateNewApi">若无token令牌,自动创建</el-checkbox>
</div>
<div class="flex-align-center flex-space-between mb20" style="height: 32px;"> <div class="flex-align-center flex-space-between mb20" style="height: 32px;">
<el-checkbox v-show="activeName === 1" v-model="loginForm.rememberMe">记住密码</el-checkbox> <el-checkbox v-show="activeName === 1" v-model="loginForm.rememberMe">记住密码</el-checkbox>
<div v-if="register"> <div v-if="register">
...@@ -96,6 +100,7 @@ import {encrypt, decrypt} from '@/utils/jsencrypt' ...@@ -96,6 +100,7 @@ import {encrypt, decrypt} from '@/utils/jsencrypt'
import useUserStore from '@/store/modules/user' import useUserStore from '@/store/modules/user'
import {useRoute, useRouter} from 'vue-router' import {useRoute, useRouter} from 'vue-router'
import {getInfo, sendCode} from "@/api/login.js"; import {getInfo, sendCode} from "@/api/login.js";
import {getToken} from "@/api/console.js";
import {ElMessage} from "element-plus"; import {ElMessage} from "element-plus";
const userStore = useUserStore() const userStore = useUserStore()
...@@ -109,7 +114,8 @@ const loginForm = ref({ ...@@ -109,7 +114,8 @@ const loginForm = ref({
rememberMe: false, rememberMe: false,
code: '', code: '',
uuid: '', uuid: '',
phoneNumber: '' phoneNumber: '',
autoCreateNewApi: true
}) })
const loginRules = { const loginRules = {
...@@ -178,10 +184,19 @@ function handleClick() { ...@@ -178,10 +184,19 @@ function handleClick() {
proxy.$refs.loginRef.resetFields() proxy.$refs.loginRef.resetFields()
} }
function handleLogin() { async function handleLogin() {
proxy.$refs.loginRef.validate(valid => { proxy.$refs.loginRef.validate(async valid => {
if (valid) { if (!valid) return
loading.value = true loading.value = true
try {
const query = route.query
const otherQueryParams = Object.keys(query).reduce((acc, cur) => {
if (cur !== 'redirect') {
acc[cur] = query[cur]
}
return acc
}, {})
if (activeName.value === 1) { if (activeName.value === 1) {
// 勾选了需要记住密码设置在 cookie 中设置记住用户名和密码 // 勾选了需要记住密码设置在 cookie 中设置记住用户名和密码
if (loginForm.value.rememberMe) { if (loginForm.value.rememberMe) {
...@@ -195,37 +210,30 @@ function handleLogin() { ...@@ -195,37 +210,30 @@ function handleLogin() {
Cookies.remove('rememberMe') Cookies.remove('rememberMe')
} }
// 调用action的登录方法 // 调用action的登录方法
userStore.login(loginForm.value).then(() => { await userStore.login(loginForm.value)
const query = route.query useUserStore().getInfo().then(res => {
const otherQueryParams = Object.keys(query).reduce((acc, cur) => { localStorage.setItem('avatar', res.data.avatar);
if (cur !== 'redirect') {
acc[cur] = query[cur]
}
return acc
}, {})
useUserStore().getInfo().then(res => {
localStorage.setItem('avatar', res.data.avatar);
})
router.push({path: redirect.value || '/', query: otherQueryParams})
}).catch(() => {
loading.value = false
}) })
} else { } else {
userStore.cellPhoneLogin({mobile: loginForm.value.phoneNumber, code: loginForm.value.code}).then(res => { await userStore.cellPhoneLogin({
const query = route.query mobile: loginForm.value.phoneNumber,
const otherQueryParams = Object.keys(query).reduce((acc, cur) => { code: loginForm.value.code
if (cur !== 'redirect') {
acc[cur] = query[cur]
}
return acc
}, {})
useUserStore().getInfo().then(res => {
})
router.push({path: redirect.value || '/', query: otherQueryParams})
}).catch(() => {
loading.value = false
}) })
useUserStore().getInfo().then(res => {
})
}
// 关键:等 New API 令牌落库后再跳转,避免首页第一时间出现"无 apiKey"状态
if (loginForm.value.autoCreateNewApi) {
try {
await getToken()
} catch (err) {
console.warn('[login] 自动创建 New API token 失败,不影响登录', err)
}
} }
router.push({path: redirect.value || '/', query: otherQueryParams})
} catch {
loading.value = false
} }
}) })
} }
...@@ -236,6 +244,7 @@ function getCookie() { ...@@ -236,6 +244,7 @@ function getCookie() {
const password = Cookies.get('password') const password = Cookies.get('password')
const rememberMe = Cookies.get('rememberMe') const rememberMe = Cookies.get('rememberMe')
loginForm.value = { loginForm.value = {
...loginForm.value,
mobile: mobile === undefined ? loginForm.value.mobile : mobile, mobile: mobile === undefined ? loginForm.value.mobile : mobile,
password: password === undefined ? loginForm.value.password : decrypt(password), password: password === undefined ? loginForm.value.password : decrypt(password),
rememberMe: rememberMe === undefined ? false : Boolean(rememberMe) rememberMe: rememberMe === undefined ? false : Boolean(rememberMe)
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment