mirror of
https://github.com/woodchen-ink/aimodels-prices.git
synced 2025-07-18 13:41:59 +08:00
1174 lines
34 KiB
Vue
1174 lines
34 KiB
Vue
<template>
|
||
<div class="prices">
|
||
<el-card v-loading="loading" element-loading-text="加载中...">
|
||
<template #header>
|
||
<div class="card-header">
|
||
<div class="header-left">
|
||
<span>价格列表</span>
|
||
</div>
|
||
<div class="header-buttons">
|
||
<template v-if="isAdmin && selectedPrices.length > 0">
|
||
<el-button type="success" @click="batchUpdateStatus('approved')">批量通过</el-button>
|
||
<el-button type="danger" @click="batchUpdateStatus('rejected')">批量拒绝</el-button>
|
||
<el-divider direction="vertical" />
|
||
</template>
|
||
<el-button type="primary" @click="handleBatchAdd">批量添加</el-button>
|
||
<el-button type="primary" @click="handleAdd">提交价格</el-button>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<div class="filter-section">
|
||
<div class="filter-label">厂商筛选:</div>
|
||
<div class="provider-filters">
|
||
<el-button
|
||
:type="!selectedProvider ? 'primary' : ''"
|
||
@click="selectedProvider = ''"
|
||
>全部</el-button>
|
||
<el-button
|
||
v-for="provider in providers"
|
||
:key="provider.id"
|
||
:type="selectedProvider === provider.id.toString() ? 'primary' : ''"
|
||
@click="selectedProvider = provider.id.toString()"
|
||
>
|
||
<div style="display: flex; align-items: center; gap: 8px">
|
||
<el-image
|
||
v-if="provider.icon"
|
||
:src="provider.icon"
|
||
style="width: 16px; height: 16px"
|
||
/>
|
||
<span>{{ provider.name }}</span>
|
||
</div>
|
||
</el-button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="filter-section">
|
||
<div class="filter-label">模型类别:</div>
|
||
<div class="model-type-filters">
|
||
<el-button
|
||
:type="!selectedModelType ? 'primary' : ''"
|
||
@click="selectedModelType = ''"
|
||
>全部</el-button>
|
||
<el-button
|
||
v-for="(label, key) in modelTypeMap"
|
||
:key="key"
|
||
:type="selectedModelType === key ? 'primary' : ''"
|
||
@click="selectedModelType = key"
|
||
>
|
||
{{ label }}
|
||
</el-button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 添加骨架屏 -->
|
||
<template v-if="loading">
|
||
<div v-for="i in 5" :key="i" class="skeleton-row">
|
||
<el-skeleton :rows="1" animated />
|
||
</div>
|
||
</template>
|
||
|
||
<el-table
|
||
:data="prices"
|
||
style="width: 100%"
|
||
@selection-change="handlePriceSelectionChange"
|
||
v-loading="tableLoading"
|
||
element-loading-text="加载中..."
|
||
>
|
||
<el-table-column v-if="isAdmin" type="selection" width="55" />
|
||
<el-table-column label="模型">
|
||
<template #default="{ row }">
|
||
<div class="value-container">
|
||
<span>{{ row.model }}</span>
|
||
<el-tag v-if="row.temp_model" type="warning" size="small" effect="light">
|
||
待审核: {{ row.temp_model }}
|
||
</el-tag>
|
||
</div>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="模型类型" width="120">
|
||
<template #default="{ row }">
|
||
<div class="value-container">
|
||
<span>{{ getModelType(row.model_type) }}</span>
|
||
<el-tag v-if="row.temp_model_type" type="warning" size="small" effect="light">
|
||
待审核: {{ getModelType(row.temp_model_type) }}
|
||
</el-tag>
|
||
</div>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="计费类型" width="120">
|
||
<template #default="{ row }">
|
||
<div class="value-container">
|
||
<span>{{ getBillingType(row.billing_type) }}</span>
|
||
<el-tag v-if="row.temp_billing_type" type="warning" size="small" effect="light">
|
||
待审核: {{ getBillingType(row.temp_billing_type) }}
|
||
</el-tag>
|
||
</div>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="模型厂商" width="180">
|
||
<template #default="{ row }">
|
||
<div class="value-container">
|
||
<div style="display: flex; align-items: center; gap: 8px">
|
||
<el-image
|
||
v-if="getProvider(row.channel_type)?.icon"
|
||
:src="getProvider(row.channel_type)?.icon"
|
||
style="width: 24px; height: 24px"
|
||
/>
|
||
<span>{{ getProvider(row.channel_type)?.name || row.channel_type }}</span>
|
||
</div>
|
||
<el-tag v-if="row.temp_channel_type" type="warning" size="small" effect="light">
|
||
待审核: {{ getProvider(row.temp_channel_type)?.name || row.temp_channel_type }}
|
||
</el-tag>
|
||
</div>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="货币" width="80">
|
||
<template #default="{ row }">
|
||
<div class="value-container">
|
||
<span>{{ row.currency }}</span>
|
||
<el-tag v-if="row.temp_currency" type="warning" size="small" effect="light">
|
||
待审核: {{ row.temp_currency }}
|
||
</el-tag>
|
||
</div>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="输入价格(M)" width="120">
|
||
<template #default="{ row }">
|
||
<div class="value-container">
|
||
<span>{{ row.input_price === 0 ? '免费' : row.input_price }}</span>
|
||
<el-tag v-if="row.temp_input_price !== null && row.temp_input_price !== undefined" type="warning" size="small" effect="light">
|
||
待审核: {{ row.temp_input_price === 0 ? '免费' : row.temp_input_price }}
|
||
</el-tag>
|
||
</div>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="输出价格(M)" width="120">
|
||
<template #default="{ row }">
|
||
<div class="value-container">
|
||
<span>{{ row.output_price === 0 ? '免费' : row.output_price }}</span>
|
||
<el-tag v-if="row.temp_output_price !== null && row.temp_output_price !== undefined" type="warning" size="small" effect="light">
|
||
待审核: {{ row.temp_output_price === 0 ? '免费' : row.temp_output_price }}
|
||
</el-tag>
|
||
</div>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column width="80">
|
||
<template #default="{ row }">
|
||
<el-popover
|
||
placement="left"
|
||
:width="200"
|
||
trigger="hover"
|
||
>
|
||
<template #reference>
|
||
<el-button link type="primary">详情</el-button>
|
||
</template>
|
||
<div class="price-detail">
|
||
<div class="detail-item">
|
||
<span class="detail-label">创建者:</span>
|
||
<span class="detail-value">{{ row.created_by }}</span>
|
||
</div>
|
||
<div class="detail-item">
|
||
<span class="detail-label">价格来源:</span>
|
||
<div class="detail-value">
|
||
<span>{{ row.price_source }}</span>
|
||
<el-tag v-if="row.temp_price_source !== null && row.temp_price_source !== undefined" type="warning" size="small" effect="light">
|
||
待审核: {{ row.temp_price_source }}
|
||
</el-tag>
|
||
</div>
|
||
</div>
|
||
<div class="detail-item">
|
||
<span class="detail-label">状态:</span>
|
||
<span class="detail-value">{{ getStatus(row.status) }}</span>
|
||
</div>
|
||
</div>
|
||
</el-popover>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column v-if="isAdmin" label="操作">
|
||
<template #default="{ row }">
|
||
<el-button-group>
|
||
<el-button type="primary" size="small" @click="handleEdit(row)">编辑</el-button>
|
||
<el-button type="danger" size="small" @click="handleDelete(row)">删除</el-button>
|
||
<el-button type="success" size="small" @click="updateStatus(row.id, 'approved')" :disabled="row.status !== 'pending'">通过</el-button>
|
||
<el-button type="danger" size="small" @click="updateStatus(row.id, 'rejected')" :disabled="row.status !== 'pending'">拒绝</el-button>
|
||
</el-button-group>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
|
||
<!-- 修改分页组件 -->
|
||
<div class="pagination-container">
|
||
<el-pagination
|
||
v-model:current-page="currentPage"
|
||
v-model:page-size="pageSize"
|
||
:page-sizes="[10, 20, 50, 100]"
|
||
:total="total"
|
||
layout="total, sizes, prev, pager, next"
|
||
:small="false"
|
||
@size-change="handleSizeChange"
|
||
@current-change="handleCurrentChange"
|
||
size-change-label="条/页"
|
||
>
|
||
<template #sizes>
|
||
<el-select v-model="pageSize" :options="[10, 20, 50, 100].map(item => ({ value: item, label: item + ' 条/页' }))">
|
||
<template #prefix>每页</template>
|
||
</el-select>
|
||
</template>
|
||
</el-pagination>
|
||
</div>
|
||
</el-card>
|
||
|
||
<!-- 批量添加对话框 -->
|
||
<el-dialog v-model="batchDialogVisible" title="批量添加模型价格" width="1200px">
|
||
<div class="batch-add-container">
|
||
<div class="batch-toolbar">
|
||
<el-button type="primary" @click="addRow">添加行</el-button>
|
||
<el-button type="danger" @click="removeSelectedRows" :disabled="!selectedRows.length">删除选中行</el-button>
|
||
<el-divider direction="vertical" />
|
||
<el-popover
|
||
placement="bottom"
|
||
:width="400"
|
||
trigger="click"
|
||
>
|
||
<template #reference>
|
||
<el-button type="success">从表格导入</el-button>
|
||
</template>
|
||
<div class="import-popover">
|
||
<p class="import-tip">请粘贴表格数据(支持从Excel复制),每行格式为:</p>
|
||
<p class="import-format">模型名称 计费类型 厂商 货币 输入价格 输出价格</p>
|
||
<el-input
|
||
v-model="importText"
|
||
type="textarea"
|
||
:rows="8"
|
||
placeholder="例如:
|
||
dall-e-2 按Token收费 OpenAI 美元 16.000000 16.000000
|
||
dall-e-3 按Token收费 OpenAI 美元 40.000000 40.000000"
|
||
/>
|
||
<div class="import-actions">
|
||
<el-button type="primary" @click="handleImport">导入</el-button>
|
||
</div>
|
||
</div>
|
||
</el-popover>
|
||
</div>
|
||
|
||
<el-table
|
||
:data="batchForms"
|
||
style="width: 100%"
|
||
@selection-change="handleSelectionChange"
|
||
height="400"
|
||
>
|
||
<el-table-column type="selection" width="55" />
|
||
<el-table-column label="模型" width="180">
|
||
<template #default="{ row }">
|
||
<el-input v-model="row.model" placeholder="请输入模型名称" />
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="模型类型" width="120">
|
||
<template #default="{ row }">
|
||
<el-select
|
||
v-model="row.model_type"
|
||
placeholder="请选择或输入"
|
||
allow-create
|
||
filterable
|
||
@create="handleModelTypeCreate"
|
||
>
|
||
<el-option
|
||
v-for="(label, value) in modelTypeMap"
|
||
:key="value"
|
||
:label="label"
|
||
:value="value"
|
||
/>
|
||
</el-select>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="计费类型" width="120">
|
||
<template #default="{ row }">
|
||
<el-select v-model="row.billing_type" placeholder="请选择">
|
||
<el-option label="按量计费" value="tokens" />
|
||
<el-option label="按次计费" value="times" />
|
||
</el-select>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="模型厂商" width="180">
|
||
<template #default="{ row }">
|
||
<el-select v-model="row.channel_type" placeholder="请选择">
|
||
<el-option
|
||
v-for="provider in providers"
|
||
:key="provider.id"
|
||
:label="provider.name"
|
||
:value="provider.id.toString()"
|
||
>
|
||
<div style="display: flex; align-items: center; gap: 8px">
|
||
<el-image
|
||
v-if="provider.icon"
|
||
:src="provider.icon"
|
||
style="width: 24px; height: 24px"
|
||
/>
|
||
<span>{{ provider.name }}</span>
|
||
</div>
|
||
</el-option>
|
||
</el-select>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="货币" width="120">
|
||
<template #default="{ row }">
|
||
<el-select v-model="row.currency" placeholder="请选择">
|
||
<el-option label="美元" value="USD" />
|
||
<el-option label="人民币" value="CNY" />
|
||
</el-select>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="输入价格(M)" width="150">
|
||
<template #default="{ row }">
|
||
<el-input-number v-model="row.input_price" :precision="4" :step="0.0001" style="width: 100%" />
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="输出价格(M)" width="150">
|
||
<template #default="{ row }">
|
||
<el-input-number v-model="row.output_price" :precision="4" :step="0.0001" style="width: 100%" />
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="价格来源" min-width="200" width="200">
|
||
<template #default="{ row }">
|
||
<el-input v-model="row.price_source" placeholder="请输入价格来源" />
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
</div>
|
||
<template #footer>
|
||
<span class="dialog-footer">
|
||
<el-button @click="batchDialogVisible = false">取消</el-button>
|
||
<el-button type="primary" @click="submitBatchForms" :loading="batchSubmitting">
|
||
{{ batchSubmitting ? '提交中...' : '确定' }}
|
||
</el-button>
|
||
</span>
|
||
</template>
|
||
</el-dialog>
|
||
|
||
<!-- 现有的单个添加对话框 -->
|
||
<el-dialog v-model="dialogVisible" title="提交价格" width="700px">
|
||
<el-form :model="form" label-width="100px">
|
||
<el-row :gutter="20">
|
||
<el-col :span="12">
|
||
<el-form-item label="模型">
|
||
<el-input v-model="form.model" />
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="12">
|
||
<el-form-item label="模型类型">
|
||
<el-select
|
||
v-model="form.model_type"
|
||
placeholder="请选择或输入"
|
||
allow-create
|
||
filterable
|
||
@create="handleModelTypeCreate"
|
||
>
|
||
<el-option
|
||
v-for="(label, value) in modelTypeMap"
|
||
:key="value"
|
||
:label="label"
|
||
:value="value"
|
||
/>
|
||
</el-select>
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="12">
|
||
<el-form-item label="计费类型">
|
||
<el-select v-model="form.billing_type" placeholder="请选择">
|
||
<el-option label="按量计费" value="tokens" />
|
||
<el-option label="按次计费" value="times" />
|
||
</el-select>
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="12">
|
||
<el-form-item label="模型厂商">
|
||
<el-select v-model="form.channel_type" placeholder="请选择">
|
||
<el-option
|
||
v-for="provider in providers"
|
||
:key="provider.id"
|
||
:label="provider.name"
|
||
:value="provider.id.toString()"
|
||
>
|
||
<div style="display: flex; align-items: center; gap: 8px">
|
||
<el-image
|
||
v-if="provider.icon"
|
||
:src="provider.icon"
|
||
style="width: 24px; height: 24px"
|
||
/>
|
||
<span>{{ provider.name }}</span>
|
||
</div>
|
||
</el-option>
|
||
</el-select>
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="12">
|
||
<el-form-item label="货币">
|
||
<el-select v-model="form.currency" placeholder="请选择">
|
||
<el-option label="美元" value="USD" />
|
||
<el-option label="人民币" value="CNY" />
|
||
</el-select>
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="12">
|
||
<el-form-item label="输入价格(M)">
|
||
<el-input-number v-model="form.input_price" :precision="4" :step="0.0001" style="width: 100%" />
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="12">
|
||
<el-form-item label="输出价格(M)">
|
||
<el-input-number v-model="form.output_price" :precision="4" :step="0.0001" style="width: 100%" />
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="24">
|
||
<el-form-item label="价格来源">
|
||
<el-input v-model="form.price_source" />
|
||
</el-form-item>
|
||
</el-col>
|
||
</el-row>
|
||
</el-form>
|
||
<template #footer>
|
||
<span class="dialog-footer">
|
||
<el-button @click="dialogVisible = false">取消</el-button>
|
||
<el-button type="primary" @click="submitForm">确定</el-button>
|
||
</span>
|
||
</template>
|
||
</el-dialog>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { ref, computed, onMounted, watch } from 'vue'
|
||
import axios from 'axios'
|
||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||
import { useRouter } from 'vue-router'
|
||
|
||
const props = defineProps({
|
||
user: Object
|
||
})
|
||
|
||
const prices = ref([])
|
||
const dialogVisible = ref(false)
|
||
const form = ref({
|
||
model: '',
|
||
model_type: '',
|
||
billing_type: 'tokens',
|
||
channel_type: '',
|
||
currency: 'USD',
|
||
input_price: 0,
|
||
output_price: 0,
|
||
price_source: '',
|
||
created_by: ''
|
||
})
|
||
const router = useRouter()
|
||
const selectedProvider = ref('')
|
||
const selectedModelType = ref('')
|
||
|
||
const isAdmin = computed(() => props.user?.role === 'admin')
|
||
|
||
const providers = ref([])
|
||
const getProvider = (id) => providers.value.find(p => p.id.toString() === id)
|
||
|
||
const statusMap = {
|
||
'pending': '待审核',
|
||
'approved': '已通过',
|
||
'rejected': '已拒绝'
|
||
}
|
||
|
||
const billingTypeMap = {
|
||
'tokens': '按量计费',
|
||
'times': '按次计费'
|
||
}
|
||
|
||
const getStatus = (status) => statusMap[status] || status
|
||
const getBillingType = (type) => billingTypeMap[type] || type
|
||
|
||
// 添加getModelType函数
|
||
const getModelType = (type) => {
|
||
if (!type) return ''
|
||
return modelTypeMap.value[type] || type
|
||
}
|
||
|
||
const calculateRate = (price, currency) => {
|
||
if (!price) return 0
|
||
return currency === 'USD' ? (price / 2).toFixed(4) : (price / 14).toFixed(4)
|
||
}
|
||
|
||
const filteredPrices = computed(() => prices.value)
|
||
|
||
const editingPrice = ref(null)
|
||
|
||
const loading = ref(true)
|
||
const tableLoading = ref(true)
|
||
|
||
// 添加分页相关的状态
|
||
const currentPage = ref(1)
|
||
const pageSize = ref(20)
|
||
const total = ref(0)
|
||
const cachedPrices = ref(new Map()) // 用于缓存数据
|
||
|
||
const loadPrices = async () => {
|
||
tableLoading.value = true
|
||
|
||
// 构建查询参数
|
||
const params = {
|
||
page: currentPage.value,
|
||
pageSize: pageSize.value
|
||
}
|
||
|
||
// 添加筛选参数
|
||
if (selectedProvider.value) {
|
||
params.channel_type = selectedProvider.value
|
||
}
|
||
if (selectedModelType.value) {
|
||
params.model_type = selectedModelType.value
|
||
}
|
||
|
||
try {
|
||
const [pricesRes, providersRes] = await Promise.all([
|
||
axios.get('/api/prices', { params }),
|
||
axios.get('/api/providers')
|
||
])
|
||
|
||
prices.value = pricesRes.data.prices
|
||
total.value = pricesRes.data.total
|
||
providers.value = providersRes.data
|
||
|
||
// 缓存数据
|
||
const cacheKey = `${currentPage.value}-${pageSize.value}-${selectedProvider.value}-${selectedModelType.value}`
|
||
cachedPrices.value.set(cacheKey, {
|
||
prices: pricesRes.data.prices,
|
||
total: pricesRes.data.total
|
||
})
|
||
|
||
// 限制缓存大小
|
||
if (cachedPrices.value.size > 10) {
|
||
const firstKey = cachedPrices.value.keys().next().value
|
||
cachedPrices.value.delete(firstKey)
|
||
}
|
||
} catch (error) {
|
||
console.error('Failed to load data:', error)
|
||
ElMessage.error('加载数据失败')
|
||
} finally {
|
||
loading.value = false
|
||
tableLoading.value = false
|
||
}
|
||
}
|
||
|
||
const handleEdit = (price) => {
|
||
editingPrice.value = price
|
||
form.value = { ...price }
|
||
dialogVisible.value = true
|
||
}
|
||
|
||
const handleDelete = (price) => {
|
||
ElMessageBox.confirm(
|
||
'确定要删除这个价格吗?',
|
||
'警告',
|
||
{
|
||
confirmButtonText: '确定',
|
||
cancelButtonText: '取消',
|
||
type: 'warning',
|
||
}
|
||
).then(async () => {
|
||
try {
|
||
await axios.delete(`/api/prices/${price.id}`)
|
||
await loadPrices()
|
||
ElMessage.success('删除成功')
|
||
} catch (error) {
|
||
console.error('Failed to delete price:', error)
|
||
if (error.response?.data?.error) {
|
||
ElMessage.error(error.response.data.error)
|
||
} else {
|
||
ElMessage.error('删除失败')
|
||
}
|
||
}
|
||
})
|
||
}
|
||
|
||
const handleAdd = () => {
|
||
if (!props.user) {
|
||
router.push('/login')
|
||
ElMessage.warning('请先登录')
|
||
return
|
||
}
|
||
editingPrice.value = null
|
||
form.value = {
|
||
model: '',
|
||
model_type: '',
|
||
billing_type: 'tokens',
|
||
channel_type: '',
|
||
currency: 'USD',
|
||
input_price: 0,
|
||
output_price: 0,
|
||
price_source: '',
|
||
created_by: ''
|
||
}
|
||
dialogVisible.value = true
|
||
}
|
||
|
||
const submitForm = async () => {
|
||
try {
|
||
form.value.created_by = props.user.username
|
||
let response
|
||
if (editingPrice.value) {
|
||
// 更新已存在的价格
|
||
response = await axios.put(`/api/prices/${editingPrice.value.id}`, form.value)
|
||
} else {
|
||
// 检查模型是否已存在
|
||
const existingPrice = prices.value?.find(p =>
|
||
p.model === form.value.model &&
|
||
p.channel_type === form.value.channel_type
|
||
)
|
||
if (existingPrice) {
|
||
ElMessageBox.confirm(
|
||
'该模型价格已存在,是否要更新?',
|
||
'提示',
|
||
{
|
||
confirmButtonText: '更新',
|
||
cancelButtonText: '取消',
|
||
type: 'warning',
|
||
}
|
||
).then(async () => {
|
||
response = await axios.put(`/api/prices/${existingPrice.id}`, form.value)
|
||
handleSubmitResponse(response)
|
||
}).catch(() => {
|
||
// 用户取消更新
|
||
})
|
||
return
|
||
}
|
||
// 创建新价格
|
||
response = await axios.post('/api/prices', form.value)
|
||
}
|
||
handleSubmitResponse(response)
|
||
} catch (error) {
|
||
console.error('Failed to submit price:', error)
|
||
if (error.response?.data?.error) {
|
||
ElMessage.error(error.response.data.error)
|
||
} else {
|
||
ElMessage.error('操作失败')
|
||
}
|
||
}
|
||
}
|
||
|
||
const handleSubmitResponse = async (response) => {
|
||
const { data } = response
|
||
if (data.error) {
|
||
ElMessage.error(data.error)
|
||
return
|
||
}
|
||
await loadPrices()
|
||
dialogVisible.value = false
|
||
ElMessage.success(editingPrice.value ? '更新成功' : '添加成功')
|
||
editingPrice.value = null
|
||
form.value = {
|
||
model: '',
|
||
model_type: '',
|
||
billing_type: 'tokens',
|
||
channel_type: '',
|
||
currency: 'USD',
|
||
input_price: 0,
|
||
output_price: 0,
|
||
price_source: '',
|
||
created_by: ''
|
||
}
|
||
}
|
||
|
||
const updateStatus = async (id, status) => {
|
||
try {
|
||
const { data } = await axios.put(`/api/prices/${id}/status`, { status })
|
||
await loadPrices()
|
||
ElMessage.success(data.message || '更新成功')
|
||
} catch (error) {
|
||
console.error('Failed to update status:', error)
|
||
if (error.response?.data?.error) {
|
||
ElMessage.error(error.response.data.error)
|
||
} else if (error.response?.status === 401) {
|
||
ElMessage.error('请先登录')
|
||
router.push('/login')
|
||
} else if (error.response?.status === 403) {
|
||
ElMessage.error('需要管理员权限')
|
||
} else {
|
||
ElMessage.error('更新失败')
|
||
}
|
||
}
|
||
}
|
||
|
||
// 批量添加相关的状态
|
||
const batchDialogVisible = ref(false)
|
||
const batchForms = ref([])
|
||
const selectedRows = ref([])
|
||
const batchSubmitting = ref(false)
|
||
|
||
// 添加模型类型映射
|
||
const modelTypeMap = ref({})
|
||
|
||
// 加载模型类型
|
||
const loadModelTypes = async () => {
|
||
try {
|
||
const response = await axios.get('/api/model-types')
|
||
const types = response.data
|
||
const map = {}
|
||
types.forEach(type => {
|
||
map[type.key] = type.label
|
||
})
|
||
modelTypeMap.value = map
|
||
} catch (error) {
|
||
console.error('Failed to load model types:', error)
|
||
ElMessage.error('加载模型类型失败')
|
||
}
|
||
}
|
||
|
||
// 处理新增的模型类型
|
||
const handleModelTypeCreate = async (value) => {
|
||
// 如果输入的是中文描述,尝试查找对应的key
|
||
const existingKey = Object.entries(modelTypeMap.value).find(([_, label]) => label === value)?.[0]
|
||
if (existingKey) {
|
||
return existingKey
|
||
}
|
||
|
||
// 如果输入的是英文key,直接使用
|
||
let key = value
|
||
let label = value
|
||
if (!/^[a-zA-Z0-9_]+$/.test(value)) {
|
||
// 如果是中文描述,生成一个新的key
|
||
key = `type_${Date.now()}`
|
||
label = value
|
||
}
|
||
|
||
try {
|
||
await axios.post('/api/model-types', { key, label })
|
||
modelTypeMap.value[key] = label
|
||
return key
|
||
} catch (error) {
|
||
console.error('Failed to create model type:', error)
|
||
ElMessage.error('创建模型类型失败')
|
||
return 'other'
|
||
}
|
||
}
|
||
|
||
// 创建新行的默认数据
|
||
const createNewRow = () => ({
|
||
model: '',
|
||
model_type: '',
|
||
billing_type: 'tokens',
|
||
channel_type: '',
|
||
currency: 'USD',
|
||
input_price: 0,
|
||
output_price: 0,
|
||
price_source: '',
|
||
created_by: props.user?.username || ''
|
||
})
|
||
|
||
// 添加新行
|
||
const addRow = () => {
|
||
batchForms.value.push(createNewRow())
|
||
}
|
||
|
||
// 处理选择变化
|
||
const handleSelectionChange = (rows) => {
|
||
selectedRows.value = rows
|
||
}
|
||
|
||
// 删除选中的行
|
||
const removeSelectedRows = () => {
|
||
const selectedIds = new Set(selectedRows.value.map(row => batchForms.value.indexOf(row)))
|
||
batchForms.value = batchForms.value.filter((_, index) => !selectedIds.has(index))
|
||
selectedRows.value = []
|
||
}
|
||
|
||
// 打开批量添加对话框
|
||
const handleBatchAdd = () => {
|
||
if (!props.user) {
|
||
router.push('/login')
|
||
ElMessage.warning('请先登录')
|
||
return
|
||
}
|
||
batchForms.value = [createNewRow()]
|
||
batchDialogVisible.value = true
|
||
}
|
||
|
||
// 提交批量表单
|
||
const submitBatchForms = async () => {
|
||
if (!batchForms.value.length) {
|
||
ElMessage.warning('请至少添加一条数据')
|
||
return
|
||
}
|
||
|
||
// 验证数据
|
||
const invalidForms = batchForms.value.filter(form =>
|
||
!form.model || !form.channel_type || !form.price_source
|
||
)
|
||
|
||
if (invalidForms.length) {
|
||
ElMessage.error('请填写完整所有必填字段')
|
||
return
|
||
}
|
||
|
||
batchSubmitting.value = true
|
||
try {
|
||
// 逐个提交数据
|
||
for (const form of batchForms.value) {
|
||
await axios.post('/api/prices', form)
|
||
}
|
||
|
||
await loadPrices()
|
||
batchDialogVisible.value = false
|
||
ElMessage.success('批量添加成功')
|
||
} catch (error) {
|
||
console.error('Failed to submit batch prices:', error)
|
||
if (error.response?.data?.error) {
|
||
ElMessage.error(error.response.data.error)
|
||
} else {
|
||
ElMessage.error('批量添加失败')
|
||
}
|
||
} finally {
|
||
batchSubmitting.value = false
|
||
}
|
||
}
|
||
|
||
// 添加导入相关的状态
|
||
const importText = ref('')
|
||
|
||
// 处理导入
|
||
const handleImport = () => {
|
||
if (!importText.value.trim()) {
|
||
ElMessage.warning('请先粘贴数据')
|
||
return
|
||
}
|
||
|
||
const lines = importText.value.trim().split('\n')
|
||
const newRows = lines.map(line => {
|
||
// 使用正则表达式匹配制表符或多个空格作为分隔符
|
||
const parts = line.trim().split(/\t+|\s{2,}/)
|
||
if (!parts || parts.length < 6) {
|
||
ElMessage.warning(`行格式不正确:${line}`)
|
||
return null
|
||
}
|
||
|
||
const [model, billingType, providerName, currency, inputPrice, outputPrice] = parts
|
||
|
||
// 查找模型厂商ID
|
||
const provider = providers.value.find(p => p.name === providerName)
|
||
if (!provider) {
|
||
ElMessage.warning(`未找到模型厂商:${providerName}`)
|
||
return null
|
||
}
|
||
|
||
// 处理计费类型
|
||
let billing_type = 'tokens'
|
||
if (billingType.includes('Token')) {
|
||
billing_type = 'tokens'
|
||
} else if (billingType.includes('次')) {
|
||
billing_type = 'times'
|
||
}
|
||
|
||
// 处理货币
|
||
let currencyCode = 'USD'
|
||
if (currency.includes('美元')) {
|
||
currencyCode = 'USD'
|
||
} else if (currency.includes('人民币') || currency.includes('CNY')) {
|
||
currencyCode = 'CNY'
|
||
}
|
||
|
||
return {
|
||
model,
|
||
billing_type,
|
||
channel_type: provider.id.toString(),
|
||
currency: currencyCode,
|
||
input_price: parseFloat(inputPrice),
|
||
output_price: parseFloat(outputPrice),
|
||
price_source: '官方',
|
||
created_by: props.user?.username || ''
|
||
}
|
||
}).filter(row => row !== null)
|
||
|
||
if (newRows.length > 0) {
|
||
batchForms.value = [...batchForms.value, ...newRows]
|
||
importText.value = ''
|
||
ElMessage.success(`成功导入 ${newRows.length} 条数据`)
|
||
}
|
||
}
|
||
|
||
const selectedPrices = ref([])
|
||
|
||
const handlePriceSelectionChange = (selection) => {
|
||
selectedPrices.value = selection
|
||
}
|
||
|
||
const batchUpdateStatus = async (status) => {
|
||
if (!selectedPrices.value.length) {
|
||
ElMessage.warning('请先选择要审核的价格')
|
||
return
|
||
}
|
||
|
||
// 过滤出待审核的价格
|
||
const pendingPrices = selectedPrices.value.filter(price => price.status === 'pending')
|
||
if (!pendingPrices.length) {
|
||
ElMessage.warning('选中的价格中没有待审核的项目')
|
||
return
|
||
}
|
||
|
||
try {
|
||
// 确认操作
|
||
await ElMessageBox.confirm(
|
||
`确定要${status === 'approved' ? '通过' : '拒绝'}选中的 ${pendingPrices.length} 条待审核价格吗?`,
|
||
'提示',
|
||
{
|
||
confirmButtonText: '确定',
|
||
cancelButtonText: '取消',
|
||
type: status === 'approved' ? 'success' : 'warning'
|
||
}
|
||
)
|
||
|
||
// 批量更新状态
|
||
for (const price of pendingPrices) {
|
||
await axios.put(`/api/prices/${price.id}/status`, { status })
|
||
}
|
||
|
||
await loadPrices()
|
||
ElMessage.success('批量审核成功')
|
||
} catch (error) {
|
||
if (error === 'cancel') return
|
||
console.error('Failed to batch update status:', error)
|
||
ElMessage.error('批量审核失败')
|
||
}
|
||
}
|
||
|
||
// 处理分页变化
|
||
const handleSizeChange = (val) => {
|
||
pageSize.value = val
|
||
currentPage.value = 1
|
||
loadPrices()
|
||
}
|
||
|
||
const handleCurrentChange = (val) => {
|
||
currentPage.value = val
|
||
loadPrices()
|
||
}
|
||
|
||
// 当选择厂商时重新加载数据
|
||
watch(selectedProvider, () => {
|
||
currentPage.value = 1 // 重置到第一页
|
||
loadPrices()
|
||
})
|
||
|
||
// 监听模型类型选择变化
|
||
watch(selectedModelType, () => {
|
||
currentPage.value = 1 // 重置到第一页
|
||
loadPrices()
|
||
})
|
||
|
||
onMounted(async () => {
|
||
await loadModelTypes()
|
||
await loadPrices()
|
||
})
|
||
</script>
|
||
|
||
<style scoped>
|
||
.card-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
}
|
||
|
||
.header-left {
|
||
display: flex;
|
||
align-items: center;
|
||
}
|
||
|
||
.filter-section {
|
||
margin: 16px 0;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
}
|
||
|
||
.filter-label {
|
||
font-size: 14px;
|
||
color: #606266;
|
||
}
|
||
|
||
.provider-filters {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 8px;
|
||
align-items: center;
|
||
}
|
||
|
||
.model-type-filters {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 8px;
|
||
align-items: center;
|
||
}
|
||
|
||
:deep(.el-button) {
|
||
margin: 0;
|
||
}
|
||
|
||
.dialog-footer {
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
gap: 1rem;
|
||
}
|
||
|
||
:deep(.el-dialog__body) {
|
||
padding-right: 20px;
|
||
max-height: calc(100vh - 200px);
|
||
overflow-y: auto;
|
||
}
|
||
|
||
:deep(.el-dialog) {
|
||
margin: 0 !important;
|
||
position: absolute;
|
||
top: 50%;
|
||
left: 50%;
|
||
transform: translate(-50%, -50%);
|
||
}
|
||
|
||
.prices {
|
||
padding-right: 0;
|
||
}
|
||
|
||
.value-container {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 4px;
|
||
}
|
||
|
||
.value-container :deep(.el-tag) {
|
||
margin: 0;
|
||
width: fit-content;
|
||
}
|
||
|
||
.value-container span {
|
||
word-break: break-all;
|
||
}
|
||
|
||
.header-buttons {
|
||
display: flex;
|
||
gap: 12px;
|
||
}
|
||
|
||
.batch-add-container {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 16px;
|
||
}
|
||
|
||
.batch-toolbar {
|
||
display: flex;
|
||
gap: 12px;
|
||
padding: 8px 0;
|
||
}
|
||
|
||
:deep(.el-input-number) {
|
||
width: 100%;
|
||
}
|
||
|
||
:deep(.el-select) {
|
||
width: 100%;
|
||
}
|
||
|
||
.import-popover {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 12px;
|
||
}
|
||
|
||
.import-tip {
|
||
margin: 0;
|
||
color: #606266;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.import-format {
|
||
margin: 0;
|
||
color: #409EFF;
|
||
font-size: 13px;
|
||
background-color: #ecf5ff;
|
||
padding: 8px;
|
||
border-radius: 4px;
|
||
}
|
||
|
||
.import-actions {
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
margin-top: 8px;
|
||
}
|
||
|
||
.price-detail {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 12px;
|
||
}
|
||
|
||
.detail-item {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 4px;
|
||
}
|
||
|
||
.detail-label {
|
||
color: #909399;
|
||
font-size: 13px;
|
||
}
|
||
|
||
.detail-value {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 4px;
|
||
}
|
||
|
||
.creator-name {
|
||
display: inline-block;
|
||
width: 100%;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
:deep(.el-loading-spinner) {
|
||
.el-loading-text {
|
||
color: #409EFF;
|
||
}
|
||
.path {
|
||
stroke: #409EFF;
|
||
}
|
||
}
|
||
|
||
.skeleton-row {
|
||
padding: 10px;
|
||
border-bottom: 1px solid #EBEEF5;
|
||
}
|
||
|
||
.pagination-container {
|
||
margin-top: 20px;
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
}
|
||
|
||
/* 添加表格行动画 */
|
||
:deep(.el-table__body-wrapper) {
|
||
.el-table__row {
|
||
transition: all 0.3s ease;
|
||
}
|
||
}
|
||
|
||
/* 添加分页选择框样式 */
|
||
:deep(.el-pagination) {
|
||
.el-select {
|
||
width: auto !important;
|
||
margin: 0 8px;
|
||
}
|
||
|
||
.el-select .el-input {
|
||
width: 110px !important;
|
||
}
|
||
|
||
.el-select-dropdown__item {
|
||
padding-right: 15px;
|
||
}
|
||
}
|
||
</style> |