|
|
@@ -155,6 +155,63 @@
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
+ <div class="action-divider"></div>
|
|
|
+ <div class="btn-usage-wrapper" ref="usagePopupRef" @mouseenter="showUsagePopup = true" @mouseleave="showUsagePopup = false">
|
|
|
+ <a-tooltip>
|
|
|
+ <div class="btn-usage" @click="toggleUsagePopup">
|
|
|
+ <svg class="usage-ring" viewBox="0 0 32 32">
|
|
|
+ <circle class="usage-ring-bg" cx="16" cy="16" r="13" />
|
|
|
+ <circle
|
|
|
+ class="usage-ring-fill"
|
|
|
+ cx="16"
|
|
|
+ cy="16"
|
|
|
+ r="13"
|
|
|
+ :stroke-dasharray="usageCircumference"
|
|
|
+ :stroke-dashoffset="usageOffset"
|
|
|
+ :style="{ stroke: usageColor }"
|
|
|
+ />
|
|
|
+ </svg>
|
|
|
+ <span class="usage-symbol">¥</span>
|
|
|
+ </div>
|
|
|
+ </a-tooltip>
|
|
|
+ <div v-if="showUsagePopup" class="usage-popup" @click.stop>
|
|
|
+ <div class="usage-popup-title">今日用量</div>
|
|
|
+ <div class="usage-popup-info">
|
|
|
+ <span class="usage-spent">¥{{ usageSpentYuan }}</span>
|
|
|
+ <span class="usage-sep">/</span>
|
|
|
+ <span class="usage-limit">¥{{ usageLimitYuan }}</span>
|
|
|
+ <span class="usage-pct">({{ usagePercent }}%)</span>
|
|
|
+ </div>
|
|
|
+ <div class="usage-popup-bar-track">
|
|
|
+ <div class="usage-popup-bar-fill" :style="{ width: usagePercent + '%', background: usageColor }"></div>
|
|
|
+ </div>
|
|
|
+ <div class="usage-remaining">剩余额度 ¥{{ usageRemainingYuan }}</div>
|
|
|
+ <div v-if="usageByModel.length > 0" class="usage-breakdown">
|
|
|
+ <div v-for="item in usageByModel" :key="item.model" class="usage-model-item">
|
|
|
+ <!-- <div class="usage-model-header">
|
|
|
+ <span class="usage-model-name">{{ item.model }}</span>
|
|
|
+ <span class="usage-model-cost">¥{{ fenToYuan(item.cost_fen) }}</span>
|
|
|
+ </div> -->
|
|
|
+ <div class="usage-model-row">
|
|
|
+ <span class="usage-model-label">输入命中</span>
|
|
|
+ <span class="usage-model-value">{{ formatTokens(item.cache_hit_tokens) }}</span>
|
|
|
+ </div>
|
|
|
+ <div class="usage-model-row">
|
|
|
+ <span class="usage-model-label">输入未命中</span>
|
|
|
+ <span class="usage-model-value">{{ formatTokens(item.cache_miss_tokens) }}</span>
|
|
|
+ </div>
|
|
|
+ <div class="usage-model-row">
|
|
|
+ <span class="usage-model-label">输出</span>
|
|
|
+ <span class="usage-model-value">{{ formatTokens(item.output_tokens) }}</span>
|
|
|
+ </div>
|
|
|
+ <div class="usage-model-row">
|
|
|
+ <span class="usage-model-label">请求次数</span>
|
|
|
+ <span class="usage-model-value">{{ item.request_count }} 次</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
<!-- <div class="action-divider"></div>
|
|
|
<div class="btn-model-wrapper" ref="modelPopupRef">
|
|
|
<a-tooltip title="选择模型">
|
|
|
@@ -279,6 +336,7 @@
|
|
|
import { CloseOutlined } from '@ant-design/icons-vue';
|
|
|
import { formatFileSize } from './utils';
|
|
|
import { getSkills } from '../../api';
|
|
|
+ import type { UsageByModel } from '../../api';
|
|
|
import type { AttachedFile, Task } from './types';
|
|
|
|
|
|
const props = withDefaults(
|
|
|
@@ -291,6 +349,10 @@
|
|
|
contextMax?: number;
|
|
|
contextPercent?: number;
|
|
|
contextBreakdown?: { messages: number; mcp: number; skills: number; system_prompt: number; other: number };
|
|
|
+ usageLimit?: number;
|
|
|
+ usageSpent?: number;
|
|
|
+ usageRemaining?: number;
|
|
|
+ usageByModel?: UsageByModel[];
|
|
|
initialThinkLevel?: string;
|
|
|
initialEditMode?: string;
|
|
|
taskList?: Task[];
|
|
|
@@ -302,6 +364,10 @@
|
|
|
contextPercent: 0,
|
|
|
streaming: false,
|
|
|
contextBreakdown: () => ({ messages: 0, mcp: 0, skills: 0, system_prompt: 0, other: 0 }),
|
|
|
+ usageLimit: 1000,
|
|
|
+ usageSpent: 0,
|
|
|
+ usageRemaining: 0,
|
|
|
+ usageByModel: () => [],
|
|
|
initialThinkLevel: 'off',
|
|
|
initialEditMode: 'full',
|
|
|
taskList: () => [],
|
|
|
@@ -374,6 +440,38 @@
|
|
|
showContextPopup.value = false;
|
|
|
});
|
|
|
|
|
|
+ // 今日用量
|
|
|
+ const usagePopupRef = ref<HTMLElement>();
|
|
|
+ const showUsagePopup = ref(false);
|
|
|
+
|
|
|
+ const usagePercent = computed(() => {
|
|
|
+ if (!props.usageLimit) return 0;
|
|
|
+ return Math.min(100, Math.round((props.usageSpent / props.usageLimit) * 100));
|
|
|
+ });
|
|
|
+
|
|
|
+ const usageCircumference = computed(() => 2 * Math.PI * 13);
|
|
|
+ const usageOffset = computed(() => usageCircumference.value * (1 - usagePercent.value / 100));
|
|
|
+
|
|
|
+ const usageColor = computed(() => {
|
|
|
+ if (usagePercent.value >= 90) return '#ff4d4f';
|
|
|
+ if (usagePercent.value >= 70) return '#faad14';
|
|
|
+ return '#52c41a';
|
|
|
+ });
|
|
|
+
|
|
|
+ const fenToYuan = (fen: number) => (fen / 100).toFixed(2);
|
|
|
+
|
|
|
+ const usageSpentYuan = computed(() => fenToYuan(props.usageSpent));
|
|
|
+ const usageLimitYuan = computed(() => fenToYuan(props.usageLimit));
|
|
|
+ const usageRemainingYuan = computed(() => fenToYuan(Math.max(0, props.usageRemaining ?? props.usageLimit - props.usageSpent)));
|
|
|
+
|
|
|
+ const toggleUsagePopup = () => {
|
|
|
+ showUsagePopup.value = !showUsagePopup.value;
|
|
|
+ };
|
|
|
+
|
|
|
+ onClickOutside(usagePopupRef, () => {
|
|
|
+ showUsagePopup.value = false;
|
|
|
+ });
|
|
|
+
|
|
|
// 模型选择
|
|
|
const modelPopupRef = ref<HTMLElement>();
|
|
|
const showModelPopup = ref(false);
|
|
|
@@ -1309,6 +1407,187 @@
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
+ .btn-usage-wrapper {
|
|
|
+ position: relative;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ }
|
|
|
+ .btn-usage {
|
|
|
+ width: 30px;
|
|
|
+ height: 30px;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ cursor: pointer;
|
|
|
+ position: relative;
|
|
|
+ border-radius: 6px;
|
|
|
+ transition: background 0.2s;
|
|
|
+
|
|
|
+ &:hover {
|
|
|
+ background: rgba(10, 132, 255, 0.1);
|
|
|
+ }
|
|
|
+
|
|
|
+ .usage-ring {
|
|
|
+ width: 24px;
|
|
|
+ height: 24px;
|
|
|
+ transform: rotate(-90deg);
|
|
|
+
|
|
|
+ .usage-ring-bg {
|
|
|
+ fill: none;
|
|
|
+ stroke: rgba(255, 255, 255, 0.1);
|
|
|
+ stroke-width: 3;
|
|
|
+ }
|
|
|
+
|
|
|
+ .usage-ring-fill {
|
|
|
+ fill: none;
|
|
|
+ stroke-width: 3;
|
|
|
+ stroke-linecap: round;
|
|
|
+ transition: stroke-dashoffset 0.3s ease;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .usage-symbol {
|
|
|
+ position: absolute;
|
|
|
+ font-size: 9px;
|
|
|
+ color: #e0e6ed;
|
|
|
+ font-weight: 600;
|
|
|
+ line-height: 1;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ .usage-popup {
|
|
|
+ position: absolute;
|
|
|
+ bottom: 42px;
|
|
|
+ left: 50%;
|
|
|
+ transform: translateX(-50%);
|
|
|
+ background: #0a2a3f;
|
|
|
+ border: 1px solid #1a4a6f;
|
|
|
+ border-radius: 8px;
|
|
|
+ min-width: 200px;
|
|
|
+ padding: 14px 16px;
|
|
|
+
|
|
|
+ &::before {
|
|
|
+ content: '';
|
|
|
+ position: absolute;
|
|
|
+ bottom: -12px;
|
|
|
+ left: 0;
|
|
|
+ right: 0;
|
|
|
+ height: 12px;
|
|
|
+ }
|
|
|
+ z-index: 1000;
|
|
|
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.4);
|
|
|
+
|
|
|
+ .usage-popup-title {
|
|
|
+ font-size: 13px;
|
|
|
+ color: #8b949e;
|
|
|
+ margin-bottom: 8px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .usage-popup-info {
|
|
|
+ display: flex;
|
|
|
+ align-items: baseline;
|
|
|
+ gap: 2px;
|
|
|
+ margin-bottom: 10px;
|
|
|
+
|
|
|
+ .usage-spent {
|
|
|
+ font-size: 18px;
|
|
|
+ font-weight: 600;
|
|
|
+ color: #e0e6ed;
|
|
|
+ }
|
|
|
+
|
|
|
+ .usage-sep {
|
|
|
+ font-size: 14px;
|
|
|
+ color: #8b949e;
|
|
|
+ }
|
|
|
+
|
|
|
+ .usage-limit {
|
|
|
+ font-size: 14px;
|
|
|
+ color: #8b949e;
|
|
|
+ }
|
|
|
+
|
|
|
+ .usage-pct {
|
|
|
+ font-size: 13px;
|
|
|
+ color: #8b949e;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .usage-popup-bar-track {
|
|
|
+ width: 100%;
|
|
|
+ height: 6px;
|
|
|
+ background: rgba(255, 255, 255, 0.08);
|
|
|
+ border-radius: 3px;
|
|
|
+ overflow: hidden;
|
|
|
+
|
|
|
+ .usage-popup-bar-fill {
|
|
|
+ height: 100%;
|
|
|
+ border-radius: 3px;
|
|
|
+ transition: width 0.3s ease;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .usage-remaining {
|
|
|
+ margin-top: 6px;
|
|
|
+ font-size: 14px;
|
|
|
+ color: #8b949e;
|
|
|
+ }
|
|
|
+
|
|
|
+ .usage-breakdown {
|
|
|
+ margin-top: 10px;
|
|
|
+ padding-top: 10px;
|
|
|
+ border-top: 1px solid rgba(255, 255, 255, 0.08);
|
|
|
+
|
|
|
+ .usage-model-item {
|
|
|
+ padding: 6px 0;
|
|
|
+ border-bottom: 1px dashed rgba(255, 255, 255, 0.08);
|
|
|
+
|
|
|
+ &:last-child {
|
|
|
+ border-bottom: none;
|
|
|
+ padding-bottom: 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ .usage-model-header {
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ align-items: center;
|
|
|
+ gap: 12px;
|
|
|
+ margin-bottom: 4px;
|
|
|
+
|
|
|
+ .usage-model-name {
|
|
|
+ font-size: 14px;
|
|
|
+ font-weight: 600;
|
|
|
+ color: #e0e6ed;
|
|
|
+ overflow: hidden;
|
|
|
+ text-overflow: ellipsis;
|
|
|
+ white-space: nowrap;
|
|
|
+ }
|
|
|
+
|
|
|
+ .usage-model-cost {
|
|
|
+ font-size: 14px;
|
|
|
+ font-weight: 600;
|
|
|
+ color: #3a8fd4;
|
|
|
+ white-space: nowrap;
|
|
|
+ flex-shrink: 0;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .usage-model-row {
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ align-items: center;
|
|
|
+ padding: 2px 0;
|
|
|
+
|
|
|
+ .usage-model-label {
|
|
|
+ font-size: 12px;
|
|
|
+ color: #8b949e;
|
|
|
+ }
|
|
|
+
|
|
|
+ .usage-model-value {
|
|
|
+ font-size: 12px;
|
|
|
+ color: #e0e6ed;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
.btn-model-wrapper {
|
|
|
position: relative;
|
|
|
display: flex;
|