Commit 5e6d28fd by lijinqi

api应用市场截止到创建api交易订单

parent f43c21f8
import request from '@/utils/request'
// 获取应用信息列表
export function getAppInfoList(params) {
return request({
url: '/apihub/api/page',
method: 'get',
params: params
})
}
export function getAppCategoryList(params) {
return request({
url: '/apihub/api-category/list',
method: 'get',
params: params
})
}
export function getAppInfoDetail(params) {
return request({
url: '/apihub/api/get',
method: 'get',
params: params
})
}
// 商家信息
export function getMerchantInfo(params) {
return request({
url: '/api/v1/merchantInfo',
method: 'get',
params: params
})
}
// 相关API
export function getAppRecommendList(params) {
return request({
url: '/api/v1/appRecommend',
method: 'get',
params: params
})
}
export function createApiOrderSubmit(query){
return request({
url: '/apihub/api-order/create',
method: 'post',
data: query
})
}
export function createPay(query){
return request({
url: '/pay/order/submit',
method: 'post',
data: query
})
}
\ No newline at end of file
...@@ -5,35 +5,13 @@ ...@@ -5,35 +5,13 @@
@import './sidebar.scss'; @import './sidebar.scss';
@import './btn.scss'; @import './btn.scss';
@import './ruoyi.scss'; @import './ruoyi.scss';
@import './reset.scss';
@import './media.scss';
body {
height: 100%;
margin: 0;
-moz-osx-font-smoothing: grayscale;
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
font-family: Helvetica Neue, Helvetica, PingFang SC, Hiragino Sans GB, Microsoft YaHei, Arial, sans-serif;
}
label {
font-weight: 700;
}
html {
height: 100%;
box-sizing: border-box;
}
#app { #app {
height: 100%; height: 100%;
} }
*,
*:before,
*:after {
box-sizing: inherit;
}
.no-padding { .no-padding {
padding: 0px !important; padding: 0px !important;
} }
...@@ -41,24 +19,6 @@ html { ...@@ -41,24 +19,6 @@ html {
.padding-content { .padding-content {
padding: 4px 0; padding: 4px 0;
} }
a:focus,
a:active {
outline: none;
}
a,
a:focus,
a:hover {
cursor: pointer;
color: inherit;
text-decoration: none;
}
div:focus {
outline: none;
}
.fr { .fr {
float: right; float: right;
} }
...@@ -128,6 +88,38 @@ aside { ...@@ -128,6 +88,38 @@ aside {
flex-shrink: 1; flex-shrink: 1;
position: relative; position: relative;
} }
.custom-wrapper {
background-color: #EFF0F2;
padding: 0 0 120px 0;
overflow-x: hidden; // 防止横向滚动条
}
.custom-main-w {
margin: 0 auto;
position: relative;
max-width: 1300px; // 你可以改成 1140px / 1280px
padding: 0 20px; // 给点左右留白,避免贴边
box-sizing: border-box;
}
.com-breadcrumb{
background-color: #FFFFFF;
border-bottom: 1px solid #EBEDED;
padding: 30px;
margin-top:30px;
.el-breadcrumb{
font-size: 16px ;
}
.el-breadcrumb__inner{
color: #333!important;
}
.el-breadcrumb__item {
&:last-child .el-breadcrumb__inner {
color: #999!important;
}
}
}
.components-container { .components-container {
margin: 30px 50px; margin: 30px 50px;
...@@ -232,3 +224,145 @@ aside { ...@@ -232,3 +224,145 @@ aside {
display: flex; display: flex;
justify-content: flex-end; justify-content: flex-end;
} }
.joinus-box{
// height: 160px;
width: 100%;
background-color: #2C53AD;
color:#Fff;
padding: 26px 0;
// padding-top: 15px;
}
.joinus-inner-box{
display: flex;
justify-content:space-between;
align-items: center;
height: 100%;
.left{
flex:1;
}
.title{
font-size: 30px;
font-weight: bold;
padding: 10px 0;
}
.desc{
font-size: 18px;
line-height: 3;
}
.a-btn{
cursor: pointer;
color: #Fff;
display: inline-block;
width: 160px;
height: 50px;
line-height: 48px;
border: 1px solid #fff;
border-radius: 24px;
font-weight: bold;
text-align: center;
font-size: 18px;
}
}
.com-tabs-wrapper{
.el-tabs__header{
padding:40px 0;
margin-bottom: 60px;
background-color: rgba(46,83,175,1);
}
.el-tabs__nav-wrap {
max-width: 1440px;
min-width: 1200px;
margin: 0 auto;
}
.el-tabs__nav-wrap::after{
content: none!important;
}
.el-tabs__nav {
width: 100%;
}
.el-tabs__item{
color:#a0b0d6;
font-size: 22px;
padding: 20px 0 20px 20px !important;
font-weight: 400;
justify-content:flex-start;
}
.el-tabs__item.is-active{
color:#FFF;
font-weight: bold;
font-size: 24px;
position: relative;
&::before{
position: absolute;
content: "";
left: 0;
top:5px;
bottom: 5px;
border-left: 2px solid #fff;
}
}
.el-tabs__active-bar{
background-color:transparent;
}
}
.com-breadcrumb-1 {
font-size: 14px;
color: #fff;
background: var(--el-color-primary);
padding: 12px 24px;
padding: 30px;
.el-breadcrumb {
font-size: 16px;
}
.el-breadcrumb__inner,
.el-breadcrumb__separator,
.el-breadcrumb__item:last-child .el-breadcrumb__inner {
color: #fff !important;
}
}
// 控制台的全局样式
.console-main{
margin: 1px 60px 0 30px;
padding: 20px 30px;
background-color: #fff;
// border-radius: 10px;
min-height: 100%;
}
.console-header{
font-size: 24px;
font-weight: 600;
margin: 0 0 20px;
border-bottom: 1px solid #eee;
padding: 10px 30px ;
.title{
height: 36px;
line-height: 36px;
}
}
.console-content{
padding: 10px 30px;
.search-bar {
display: flex;
align-items: center;
margin-bottom: 24px;
}
.search-bar label {
margin-right: 8px;
font-weight: 500;
}
.search-input {
width: 260px;
margin-right: 16px;
}
}
@media (min-width: 1600px) {
.app-wrapper{
width: 100%;
}
.custom-main-w{
max-width: 1440px;
width: 100%;
}
}
// 宽度大于等于1200px
@media (min-width: 1200px) and (max-width: 1600px) {
.app-wrapper{
width: 100%;
}
.custom-main-w{
max-width: 1440px;
min-width: 1180px;
}
}
// 宽度大于等于1200px
@media (max-width: 1200px) {
.app-wrapper{
width: 100%;
min-width: 1200px;
}
.custom-main-w{
max-width: 1160px;
}
.nav-bar {
position: relative!important;
}
.app-main{
padding: 0!important;
}
}
/**
* css reset
*/
html {
font-family: 'Helvetica Neue', Helvetica, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei',
'微软雅黑', Arial, sans-serif;
line-height: 1.5;
background-color: #EFF0F2;
width: 100%;
font-size: 14px;
box-sizing: border-box;
min-height: 100%;
overflow-y: auto;
overflow-x: auto;
outline: 0;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
-webkit-font-smoothing: antialiased;
-webkit-text-size-adjust: 100%;
-ms-text-size-adjust: 100%;
-webkit-backface-visibility:hidden;
}
html * {
outline: none;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0); }
*,*::after,*::before{
box-sizing: border-box;
}
body {
padding: 0;
margin: 0 auto;
outline: 0;
color: #333;
height: 100%;
width: 100%;
// overflow-x: auto;
-webkit-font-smoothing: antialiased;
-webkit-backface-visibility:hidden;
}
a,
button {
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
article,
aside,
footer,
header,
nav,
section {
display: block;
}
body,
div,
dl,
dt,
dd,
ul,
ol,
li,
h1,
h2,
h3,
h4,
h5,
h6,
pre,
code,
form,
fieldset,
legend,
input,
textarea,
p,
blockquote,
th,
td,
hr,
button,
article,
aside,
details,
figcaption,
figure,
footer,
header,
hgroup,
menu,
nav,
section {
margin: 0;
padding: 0; }
input,
select,
textarea {
font-size: 100%; }
table {
border-collapse: collapse;
border-spacing: 0; }
fieldset,
img {
border: 0; }
abbr,
acronym {
border: 0;
font-variant: normal; }
del {
text-decoration: line-through; }
address,
caption,
cite,
code,
dfn,
em,
th,
var {
font-style: normal;
font-weight: 500; }
ol,
ul {
list-style: none; }
caption,
th {
text-align: left; }
h1,
h2,
h3,
h4,
h5,
h6 {
font-size: 100%;
font-weight: 500; }
q:before,
q:after {
content: ''; }
sub,
sup {
font-size: 75%;
line-height: 0;
position: relative;
vertical-align: baseline; }
sup {
top: -0.5em; }
sub {
bottom: -0.25em; }
ins,
a {
text-decoration: none; }
/*input*/
button {
border: none;
outline: none;
}
button, html input[type='button'], input[type='reset'], input[type='submit'] {
-webkit-appearance: button;
text-transform: none;
outline: none; }
input::-webkit-input-placeholder,
textarea::-webkit-input-placeholder {
color: #999; }
input::-webkit-inner-spin-button {
-webkit-appearance: none; }
input::-webkit-outer-spin-button {
-webkit-appearance: none; }
textarea {
vertical-align: top; }
button, input {
line-height: normal; }
select {
margin: 0;
outline: 0; }
textarea.fixAndroidKeyboard:focus, input.fixAKeyboard:focus {
-webkit-tap-highlight-color: rgba(255, 255, 255, 0);
-webkit-user-modify: read-write-plaintext-only; }
input:-webkit-autofill {
-webkit-box-shadow: 0 0 0px 1000px white inset !important; }
input[type=submit],
input[type=reset],
input[type=button],
input[type=checkbox],
button, label {
cursor: pointer;
user-select: none;
-ms-user-select: none;
-moz-user-select: none;
-webkit-user-select: none; }
input[type=submit] {
-webkit-user-modify: read-plaintext-only;
-moz-user-modify: read-plaintext-only;
-ms-user-modify: read-plaintext-only;
-o-user-modify: read-plaintext-only;
user-modify: read-plaintext-only; }
input[type='search']::-webkit-search-cancel-button,
input[type='search']::-webkit-search-decoration {
-webkit-appearance: none; }
input[type='search'] {
-webkit-box-sizing: content-box;
-moz-box-sizing: content-box;
box-sizing: content-box;
-webkit-appearance: textfield; }
li,
ol,
ul,
dl,
dt,
dd {
list-style: none;
}
b,
em,
h1,
h2,
h3,
h4,
h5,
h6,
i,
th,
td {
font-weight: 400;
font-style: normal;
}
/**
* 1. Remove the gray background on active links in IE 10.
* 2. Remove gaps in links underline in iOS 8+ and Safari 8+.
*/
a {
background-color: transparent;
/* 1 */
-webkit-text-decoration-skip: objects;
/* 2 */
}
/**
* Remove the outline on focused links when they are also active or hovered
* in all browsers (opinionated).
*/
a {
color: #333;
}
a:active,
a:hover {
outline-width: 0;
color: #333;
}
button,
input,
select,
textarea,
img {
outline: 0;
color: #000;
font-size: 100%;
background-color: transparent;
}
button,
input[type='button'],
input[type='password'],
input[type='submit'],
input[type='text'],
textarea {
-webkit-appearance: none;
-webkit-appearance: button;
/* 2 */
}
a,
a:visited {
text-decoration: none;
/*color: #5c5658*/
}
img,
video {
vertical-align: middle;
border: 0;
border-style: none;
}
/**
pc
* Remove the inner border and padding in Firefox.
*/
button::-moz-focus-inner,
[type='button']::-moz-focus-inner,
[type='reset']::-moz-focus-inner,
[type='submit']::-moz-focus-inner {
border-style: none;
padding: 0;
}
/**
* 1. Add the correct box sizing in IE 10-.
* 2. Remove the padding in IE 10-.
*/
[type='checkbox'],
[type='radio'] {
box-sizing: border-box;
/* 1 */
padding: 0;
/* 2 */
}
/**
* Correct the cursor style of increment and decrement buttons in Chrome.
*/
[type='number']::-webkit-inner-spin-button,
[type='number']::-webkit-outer-spin-button {
height: auto;
}
*,
*:before,
*:after {
box-sizing: border-box;
margin: 0;
}
...@@ -16,6 +16,7 @@ ...@@ -16,6 +16,7 @@
mode="horizontal" mode="horizontal"
style="--el-menu-horizontal-height:48px" style="--el-menu-horizontal-height:48px"
@select="menuSelect"> @select="menuSelect">
<el-menu-item index="/marketplace/ai">AI应用市场</el-menu-item>
<el-sub-menu index="computingResource"> <el-sub-menu index="computingResource">
<template #title>计算资源</template> <template #title>计算资源</template>
<el-menu-item <el-menu-item
......
...@@ -18,7 +18,10 @@ const whiteList = [ ...@@ -18,7 +18,10 @@ const whiteList = [
'/componentServices/componentServicesList', '/componentServices/componentServicesList',
'/partnership/partnershipList', '/partnership/partnershipList',
'/information/informationDetail', '/information/informationDetail',
'/information/informationList' '/information/informationList',
'/marketplace/ai',
'/marketplace/ai/detail',
'/marketplace/ai/orderConfirm',
] ]
router.beforeEach((to, from, next) => { router.beforeEach((to, from, next) => {
......
...@@ -227,6 +227,30 @@ export const constantRoutes = [ ...@@ -227,6 +227,30 @@ export const constantRoutes = [
meta: {title: '审核认证', icon: 'order'} meta: {title: '审核认证', icon: 'order'}
} }
] ]
},
{
path: '/marketplace',
component: UserLayout,
children: [
{
path: 'ai',
component: () => import('@/views/marketplace/AIMarketplace.vue'),
name: 'AIMarketplace',
meta: { title: 'AI应用市场' }
},
{
path: 'ai/detail',
component: () => import('@/views/marketplace/AIMarketplaceDetail.vue'),
name: 'AIMarketplaceDetail',
meta: { title: 'API详情' }
},
{
path: 'orderConfirm',
component: () => import('@/views/marketplace/OrderConfirm.vue'),
name: 'OrderConfirm',
meta: { title: '订单确认' }
}
]
} }
] ]
......
<template>
<div class="app-container">
<!-- 头部搜索区域 -->
<div class="search-section">
<div class="custom-main-w">
<h1 class="page-title">API应用市场</h1>
<h2 class="page-subtitle">一站式API管理、配置、调用、维护</h2>
<div class="search-box">
<el-input
v-model="searchQuery"
placeholder="可输入关键词搜索"
@keyup.enter="handleSearch"
clearable
>
<template #append>
<el-button type="primary" @click="handleSearch">
<el-icon style="margin-right: 10px">
<Search />
</el-icon>
立即搜索
</el-button>
</template>
</el-input>
</div>
</div>
</div>
<!-- API分类标签 -->
<div class="category-tabs-wrapper">
<div class="custom-main-w">
<el-tabs
v-model="activeCategory"
class="category-tabs"
@tab-change="handleCategoryChange"
>
<el-tab-pane label="全部" name="" />
<el-tab-pane
v-for="category in categoryList"
:key="category.id"
:label="category.name"
:name="String(category.id)"
/>
</el-tabs>
</div>
</div>
<!-- API列表区域 -->
<div class="api-list custom-main-w">
<div v-if="loading" class="loading-container">
<el-skeleton :rows="3" animated />
</div>
<el-row :gutter="50" v-else-if="apiList.length > 0">
<el-col :span="8" v-for="api in apiList" :key="api.id">
<APICard
:id="api.id"
:coverImage="api.coverImage"
:priceInfo="api.priceInfo"
:title="api.name"
:description="api.description"
:tag="getAppTag(api)"
/>
</el-col>
</el-row>
<div v-else class="empty-container">
<el-empty description="暂无应用数据" />
</div>
</div>
<!-- 分页组件 -->
<div class="pagination-container custom-main-w" v-if="total > 0">
<el-pagination
v-model="currentPage"
:page-size="pageSize"
:page-sizes="[9, 18, 27, 36]"
:total="total"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
</div>
</template>
<script>
import { ElMessage } from 'element-plus';
import { Search } from '@element-plus/icons-vue';
import APICard from './components/APICard.vue';
import { getAppInfoList, getAppCategoryList } from '@/api/marketplace';
export default {
name: 'Marketplace',
components: {
APICard,
Search
},
data() {
return {
searchQuery: '',
activeCategory: '',
apiList: [],
categoryList: [],
loading: false,
currentPage: 1,
pageSize: 9,
total: 0,
_searchTimer: null
};
},
methods: {
async getCategoryList() {
try {
const response = await getAppCategoryList();
if (response.code === 0) {
this.categoryList = response.data || [];
} else {
ElMessage.error(response.msg || '获取分类列表失败');
}
} catch (error) {
console.error('获取分类列表失败:', error);
ElMessage.error('获取分类列表失败');
}
},
async getAppList() {
try {
this.loading = true;
const params = {
pageNo: this.currentPage,
pageSize: this.pageSize,
// activeCategory 为空(即选择了全部)时,不传 categoryId
categoryId: this.activeCategory
? parseInt(this.activeCategory, 10)
: undefined,
searchQuery: this.searchQuery || undefined
};
const response = await getAppInfoList(params);
if (response.code === 0) {
this.apiList = response.data.list || [];
this.total = response.data.total || 0;
} else {
ElMessage.error(response.msg || '获取应用列表失败');
}
} catch (error) {
console.error('获取应用列表失败:', error);
ElMessage.error('获取应用列表失败');
} finally {
this.loading = false;
}
},
handleSearch() {
this.currentPage = 1;
this.getAppList();
},
handleCategoryChange() {
this.currentPage = 1;
this.getAppList();
},
handleSizeChange(val) {
this.pageSize = val;
this.currentPage = 1;
this.getAppList();
},
handleCurrentChange(val) {
this.currentPage = val;
this.getAppList();
},
getAppImage() {
return ``;
},
getAppTag(api) {
const m = this.categoryList.find(c => c.id === api.categoryId);
return m ? m.name : '';
}
},
watch: {
searchQuery(newVal) {
if (this._searchTimer) clearTimeout(this._searchTimer);
this._searchTimer = setTimeout(() => {
if (newVal === '') this.handleSearch();
}, 500);
}
},
mounted() {
this.getCategoryList();
this.getAppList();
}
};
</script>
<style scoped lang="scss">
.search-section {
text-align: center;
padding: 170px 0 150px;
background: linear-gradient(
180deg,
rgba(225, 237, 255, 1) 0%,
rgba(255, 255, 255, 0) 100%
);
}
.page-title {
font-size: 40px;
margin-bottom: 16px;
color: #333;
}
.page-subtitle {
font-size: 26px;
color: #6c6c6c;
margin-bottom: 24px;
}
.search-box {
max-width: 800px;
margin: 30px auto 0;
:deep(.el-input__wrapper) {
padding: 15px 11px;
}
:deep(.el-input-group__append) {
color: #fff;
background-color: var(--el-color-primary) !important;
}
}
.api-list {
margin-bottom: 60px;
min-height: 400px;
}
.loading-container {
padding: 40px 0;
}
.empty-container {
padding: 60px 0;
text-align: center;
}
.pagination-container {
display: flex;
justify-content: center;
margin-bottom: 60px;
padding: 20px 0;
}
// 分类标签
// .category-title {
// position: absolute;
// left: -60px;
// color: #333;
// font-size: 24px;
// line-height: 60px;
// top: 0;
// font-weight: bold;
// }
.category-tabs-wrapper {
position: relative;
margin-bottom: 70px;
padding: 35px 0;
background-color: #fff;
.category-tabs {
// padding-left: 100px;
}
:deep(.el-tabs__header) {
margin: 0;
}
:deep(.el-tabs__nav-wrap) {
max-width: 1440px;
min-width: 1200px;
margin: 0 auto;
}
:deep(.el-tabs__nav-wrap::after) {
content: none !important;
}
:deep(.el-tabs__nav) {
width: 100%;
}
:deep(.el-tabs__item) {
color: #333;
font-size: 20px;
margin-right: 20px;
padding: 22px 24px !important;
font-weight: 400;
justify-content: flex-start;
}
:deep(.el-tabs__item.is-active) {
color: #fff;
font-weight: bold;
position: relative;
border-radius: 3px;
background-color: var(--el-color-primary) !important;
}
:deep(.el-tabs__active-bar) {
background-color: transparent;
}
}
</style>
<template>
<div class="custom-wrapper detail-container">
<div class="com-breadcrumb-1">
<div class="custom-main-w">
<el-breadcrumb separator=">">
<el-breadcrumb-item :to="{ path: '/' }">首页</el-breadcrumb-item>
<el-breadcrumb-item :to="{ path: '/marketplace/ai' }"
>AI应用市场</el-breadcrumb-item
>
<el-breadcrumb-item v-if="categoryName">{{
categoryName
}}</el-breadcrumb-item>
</el-breadcrumb>
</div>
</div>
<div class="custom-main-w detail-content">
<!-- 左侧内容区 -->
<div class="left-section">
<div class="product-info">
<!-- 商品基本信息 -->
<div class="line-item">
<div class="lable">
<img
class="product-icon"
:src="productData.coverImage"
alt="API服务"
/>
</div>
<div class="value product-meta">
<div class="product-title-row">
<h1 class="product-title">
{{ productData.name || "应用详情" }}
</h1>
<el-tag size="small" type="warning" effect="dark"
>官方自营</el-tag>
</div>
<p class="product-desc">{{ productData.description }}</p>
<div class="product-tags">
<el-tag effect="plain" v-if="categoryName">{{
categoryName
}}</el-tag>
</div>
<div class="main-price-card">
<div class="price-left">
<div class="main-amount">{{ currentPrice }}</div>
<div class="main-currency">{{ currentTimes }}</div>
</div>
<div class="main-period">{{ currentValidDays }}</div>
</div>
</div>
</div>
<!-- 价格主卡片 -->
<!-- 资源包规格区块 -->
<div class="line-item">
<div class="lable">资源包规格:</div>
<div class="spec-list value">
<div
v-for="(spec, idx) in specs"
:key="spec.id"
class="spec-card"
:class="{ active: selectedSpec === idx, annual: spec.annual }"
@click="selectSpec(idx)"
>
<el-tag v-if="spec.annual" type="danger" class="spec-tag">
包年套餐</el-tag
>
<div class="spec-info">
<div class="spec-name">{{ spec.name }}</div>
<div class="spec-price">{{ spec.price }}</div>
<div class="spec-count">{{ spec.count }}</div>
</div>
<div class="spec-desc">{{ spec.unitPrice }}元/次</div>
</div>
</div>
</div>
<!-- 数量选择器 -->
<!-- <div class="line-item">
<div class="lable">数量:</div>
<el-button
size="small"
@click="decreaseQty"
:disabled="quantity <= 1"
>-</el-button
>
<span class="qty">{{ quantity }}</span>
<el-button size="small" @click="increaseQty">+</el-button>
</div> -->
<!-- 操作按钮 -->
<div class="line-item action-btns">
<div class="lable"></div>
<el-button type="primary" size="large" @click="goOrderConfirm"
>立即购买</el-button
>
<!-- <el-button size="large" plain>在线测试</el-button> -->
</div>
</div>
<!-- 详情Tab区块 -->
<div class="detail-tabs">
<el-tabs v-model="mainTab" class="main-tabs">
<el-tab-pane label="应用说明" name="detail"></el-tab-pane>
<el-tab-pane label="接口文档" name="doc"></el-tab-pane>
</el-tabs>
<div class="tab-content-area">
<div class="main-content" v-html="currentHtml"></div>
</div>
</div>
</div>
<el-drawer v-model="showDrawer" class="drawer" direction="rtl" :size="694" title="订单详情">
<template #default>
<div class="info-block">
<div class="info-item flex-align-center flex-space-between">
<div class="label">api名称</div>
<div class="value">{{ productData.name }}</div>
</div>
<div class="info-item flex-align-center flex-space-between">
<div class="label">资源包名称</div>
<div class="value">{{ currentPackageName }}</div>
</div>
<div class="info-item flex-align-center flex-space-between">
<div class="label">资源包次数</div>
<div class="value">{{ currentTimes }}</div>
</div>
<div class="info-item flex-align-center flex-space-between">
<div class="label">有效期</div>
<div class="value">{{ orderValidDays }}</div>
</div>
<div class="info-item flex-align-center flex-space-between">
<div class="label">apiId</div>
<div class="value">{{ productData.id }}</div>
</div>
<div class="info-item flex-align-center flex-space-between">
<div class="label">资源包Id</div>
<div class="value">{{ currentPackageId }}</div>
</div>
</div>
<div class="info-block">
<div class="info-item flex-align-center flex-space-between">
<div class="label">购买数量(个)</div>
<div class="value">{{ form.num }}</div>
</div>
</div>
</template>
<template #footer>
<div class="checkbox-info">
<el-checkbox v-model="checkbox" :value="true" size="large">
<template #default>
我已阅读和理解上述
</template>
</el-checkbox>
<span class="link" @click="handleCheckProtocol">《购买协议》</span>
</div>
<el-divider style="margin: 0 !important;"/>
<div class="drawer-footer flex-space-between flex-align-center">
<div>应付费用</div>
<div class="flex-align-center">
<div class="mr20">
<div class="price">¥{{ currentPrice }}</div>
</div>
<el-button type="primary" @click="create" :disabled="!checkbox">
立即购买
</el-button>
</div>
</div>
</template>
</el-drawer>
<!-- 右侧内容区 -->
<!-- <div class="right-section">-->
<!-- &lt;!&ndash; 商家信息 &ndash;&gt;-->
<!-- <div class="merchant-info">-->
<!-- <div class="preview-image">-->
<!-- <el-image src="/src/assets/logo/nscc-logo-copy.png" fit="cover" />-->
<!-- </div>-->
<!-- <ul class="info-list">-->
<!-- <li>-->
<!-- <span class="label">商家:</span>-->
<!-- <span>自营</span>-->
<!--&lt;!&ndash; <span>{{ merchantInfo.merchant }}</span>&ndash;&gt;-->
<!-- </li>-->
<!-- <li>-->
<!-- <span class="label">电话:</span>-->
<!-- <span>13348681789</span>-->
<!--&lt;!&ndash; <span>{{ merchantInfo.phone }}</span>&ndash;&gt;-->
<!-- </li>-->
<!-- <li>-->
<!-- <span class="label">邮箱:</span>-->
<!--&lt;!&ndash; <span>{{ merchantInfo.email }}</span>&ndash;&gt;-->
<!-- </li>-->
<!-- </ul>-->
<!-- </div>-->
<!-- &lt;!&ndash; 相关API推荐 &ndash;&gt;-->
<!-- <RelatedApis :apis="relatedApis" />-->
<!-- </div>-->
</div>
</div>
</template>
<script setup>
import { ref, computed, onMounted, watch } from "vue";
import { useRouter, useRoute } from "vue-router";
import {
//getMerchantInfo,
getAppRecommendList,
//getAppCategoryList,
getAppInfoDetail, createApiOrderSubmit,
} from "../../api/marketplace";
import RelatedApis from "./components/RelatedApis.vue";
import {createOrderSubmit, createPay} from "@/api/computingResource.js";
import {ElMessageBox} from "element-plus";
const router = useRouter();
const route = useRoute();
const baseUrl = import.meta.env?.VITE_APP_BASE_API || "";
const withBaseUrl = (p) => {
if (!p) return "";
if (/^https?:\/\//.test(p)) return p;
return `${baseUrl}${p}`;
};
const showDrawer = ref(false)
const form = ref({
num: 1
})
const checkbox = ref(false)
// 分类
const categoryList = ref([]);
const categoryName = computed(() => {
const id = productData.value.categoryId;
const m = categoryList.value.find(
(c) => c.categoryId === id || c.id === id
);
console.log(id);
console.log(categoryList);
return m ? m.categoryName || m.name : "";
});
const specs = ref([]);
const selectedSpec = ref(0);
const selectSpec = (idx) => {
selectedSpec.value = idx;
};
// 详情数据
const productData = ref({
createTime: "",
remark: null,
id: undefined,
categoryId: undefined,
categoryName: "",
name: "",
description: "",
coverImage: "",
datail: "",
doc: "",
apiPackages: [],
});
// 获取详情
const fetchDetail = async () => {
const id = route.query.id || route.params?.id;
if (!id) return;
const res = await getAppInfoDetail({ id });
const d = res?.data;
if (!d) return;
productData.value = { ...productData.value, ...d };
//productData.value.appImage = withBaseUrl(productData.value.appImage);
specs.value = Array.isArray(d.apiPackages)
? d.apiPackages.map((p) => ({
id: p.id,
name: p.name,
price: p.price/100,
times: `${p.times}次`,
validDays: p.validDays,
unitPrice:
p.price && p.times ? (p.price/100 / p.times).toFixed(4) : "-",
}))
: [];
selectedSpec.value = specs.value.length ? 0 : -1;
//fetchRelatedApis();
};
//获取分类列表
const fetchCategories = async () => {
const res = await getAppCategoryList({});
categoryList.value = Array.isArray(res?.data) ? res.data : [];
};
// 商家信息
// const merchantInfo = ref({ image: "", phone: "", merchant: "", email: "" });
// const fetchDetailMerchant = async () => {
// const res = await getMerchantInfo({});
// const d = res?.data || {};
// merchantInfo.value = {
// image: d.image,
// phone: d.phone || "",
// merchant: d.merchant || "",
// email: d.email || "",
// };
// };
// 相关API
const relatedApis = ref([]);
const fetchRelatedApis = async () => {
if (!productData.value.categoryId) return;
const res = await getAppRecommendList({
categoryId: productData.value.categoryId,
});
const list = Array.isArray(res?.data) ? res.data : [];
relatedApis.value = list.map((item) => ({
id: item.appId,
name: item.appName,
description: item.appIntro,
image: withBaseUrl(item.appImage),
}));
};
onMounted(() => {
fetchDetail();
//fetchDetailMerchant();
//fetchCategories();
});
// 监听路由变化
watch(
() => route.query.id,
(newId) => {
if (newId) {
fetchDetail();
}
}
);
const currentPackageName = computed(() => {
const s = specs.value[selectedSpec.value];
return s ? s.name : "-";
});
const currentPackageId = computed(() => {
const s = specs.value[selectedSpec.value];
return s ? s.id : "-";
});
const currentTimes = computed(() => {
const s = specs.value[selectedSpec.value];
return s ? s.times : "-";
});
const currentPrice = computed(() => {
const s = specs.value[selectedSpec.value];
return s ? s.price : "-";
});
const currentValidDays = computed(() => {
const s = specs.value[selectedSpec.value];
return s && s.validDays
? `(自购买起有限期为${s.validDays}天)`
: "";
});
const orderValidDays = computed(() => {
const s = specs.value[selectedSpec.value];
return s && s.validDays
? `${s.validDays}天`
: "";
});
// mainTab 切换
const mainTab = ref("detail");
const currentHtml = computed(() => {
if (mainTab.value === "detail") return productData.value.detail;
if (mainTab.value === "doc") return productData.value.doc;
return "";
});
// 去订单确认页// 详情页 goOrderConfirm
const goOrderConfirm = () => {
showDrawer.value = true
// router.push({
// name: "OrderConfirm",
// query: {
// id: productData.value.id,
// name: productData.value.name,
// price: currentPrice.value,
// validDays: specs.value[selectedSpec.value]?.validDays,
// times: specs.value[selectedSpec.value]?.times,
// quantity: quantity.value,
// },
// });
};
function getCreateData(){
}
function create() {
// 用户点击“确认”时执行
const createData = getCreateData();
//创建订单
createApiOrderSubmit(
{
apiId: productData.value.id,
packageId:currentPackageId.value,
}).then(res => {
if (res.data.payOrderId !== '') {
// 弹出确认对话框
ElMessageBox.confirm(
'确定购买吗?', // 对话框提示文字
'购买确认', // 对话框标题
{
confirmButtonText: '确认', // 确认按钮文字
cancelButtonText: '取消', // 取消按钮文字
type: 'warning' // 对话框类型(警告样式)
}
)
.then(() => {
createPay({id: res.data.payOrderId, channelCode: 'wx_native'}).then(i => {
if (i.code === 0) {
getCode(i.data.displayContent, res.data.payOrderId)
}
})
// showDrawer.value = false;
})
.catch(() => {
ElMessageBox.confirm(
'订单已创建,请前往控制台-我的订单查看',
'提示',
{
confirmButtonText: '确认',
showCancelButton: false,
type: 'success'
}
).then(() => {
showDrawer.value = false;
})
});
}
}).catch(err => {
// 接口调用失败的处理(如提示错误)
});
}
// 暴露 goOrderConfirm 到模板
defineExpose({ goOrderConfirm });
</script>
<style scoped lang="scss">
.detail-content {
display: block; // 或者直接删掉
}
.left-section {
flex: none; // 不要自适应拉伸
width: 100%;
max-width: 1300px; // 你可以改成 800 / 1000 / 1200px
margin: 0 auto; // 居中
}
.product-info {
background: #fff;
padding: 40px 50px;
}
.right-section {
display: none;
width: 400px;
}
.line-item {
display: flex;
align-items: start;
margin-bottom: 35px;
}
.lable {
font-size: 16px;
color: #666;
width: 150px;
margin-right: 40px;
text-align: right;
}
.value {
flex: 1;
}
.product-icon {
width: 100px;
height: auto;
max-height: 100px;
background: #fff;
border-radius: 12px;
padding: 2px;
border: 1px solid #e4e7ed;
object-fit: cover;
}
.product-title-row {
display: flex;
align-items: center;
gap: 12px;
}
.product-title {
font-size: 22px;
font-weight: bold;
margin: 0;
}
.product-desc {
display: -webkit-box;
-webkit-line-clamp: 2; /* 最多两行 */
-webkit-box-orient: vertical;
overflow: hidden;
word-break: break-word;
white-space: normal;
color: #666;
font-size: 14px;
line-height: 24px;
}
.product-tags {
display: flex;
margin: 0 0 20px;
gap: 10px;
.el-tag {
height: 26px;
}
}
.main-price-card {
display: flex;
align-items: baseline;
gap: 16px;
background: #fef6f6; // 淡粉色背景,突出价格
border-radius: 8px;
padding: 14px 24px;
font-weight: bold;
.price-left {
display: flex;
flex-direction: column;
line-height: 1.2;
}
.main-amount {
font-size: 32px;
color: #ff3b30; // 主色(红)
}
.main-currency {
font-size: 14px;
color: #999; // 灰色
margin-top: 4px;
}
.main-period {
font-size: 14px;
color: #666;
font-weight: normal;
}
}
.spec-list {
display: flex;
gap: 20px;
flex-wrap: wrap;
}
.spec-card {
border: 1px solid #e4e7ed;
border-radius: 9px;
border: 1px solid rgba(230, 230, 230, 1);
min-width: 160px;
text-align: center;
cursor: pointer;
transition: all 0.2s;
position: relative;
z-index: 1;
.spec-name {
font-size: 16px;
color: #333;
margin-bottom: 10px;
font-weight: bold;
}
.spec-price {
font-size: 24px;
color: rgba(255, 85, 82, 1);
font-weight: bold;
margin-bottom: 2px;
}
.spec-info {
color: #666;
padding: 30px 20px;
}
.spec-count {
font-size: 14px;
color: #333;
margin-bottom: 2px;
}
.spec-desc {
font-size: 14px;
color: #666;
padding: 15px 0;
background: rgba(236, 237, 245, 1);
border-bottom-right-radius: 8px;
border-bottom-left-radius: 8px;
}
.spec-tag {
position: absolute;
top: 1px;
z-index: 2;
right: 0px;
}
}
.spec-card.active {
border-color: var(--el-color-primary);
.spec-desc {
color: #fff;
background: var(--el-color-primary);
}
// background: #ecf5ff;
// box-shadow: 0 2px 8px #256dff22;
}
.spec-card.annual {
// border-color: #f56c6c;
}
.qty {
display: inline-block;
min-width: 32px;
text-align: center;
font-size: 16px;
}
.merchant-info,
.related-apis {
background: #fff;
padding: 40px;
margin-bottom: 32px;
.title {
font-size: 24px;
margin-bottom: 30px;
font-weight: bold;
}
}
.preview-image {
height: auto;
background: #fff;
overflow: hidden;
max-width: 45%;
max-height: 100px;
object-fit: contain;
margin: 0 auto;
}
.info-list {
border-top: 1px dashed #eee;
padding: 30px 10px 40px;
margin-top: 30px;
list-style: none;
}
.info-list li {
margin-bottom: 10px;
display: flex;
color: #666;
font-size: 18px;
}
.info-list .label {
width: 60px;
}
.api-item {
display: flex;
margin-bottom: 15px;
}
.api-icon {
width: 64px;
height: auto;
max-height: 64px;
// padding: 20px;
border: 1px solid #eee;
object-fit: contain;
// object-fit: cover;
}
.api-info {
margin: 0 0 0 24px;
}
.api-title,
.api-desc {
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
height: 30px;
line-height: 30px;
}
.api-title {
font-size: 18px;
margin-bottom: 5px;
}
.api-desc {
color: #666;
}
/* 详情Tab区块样式 */
.detail-tabs {
margin-top: 32px;
background: #fff;
box-shadow: 0 2px 12px #e6f0fa;
}
.detail-tabs {
:deep(.el-tabs__nav) {
width: 100%;
padding-left: 20px;
}
:deep(.el-tabs__item) {
font-size: 18px;
padding: 40px 20px 40px 30px !important;
font-weight: 400;
justify-content: flex-start;
}
:deep(.el-tabs__item.is-active) {
color: var(--el-color-primary);
font-weight: bold;
position: relative;
}
}
.tab-content-area {
display: flex;
min-height: 500px;
padding: 20px 40px;
}
.side-menu {
width: 160px;
border-right: 1px solid #f0f0f0;
padding: 24px 0;
}
.menu-item {
padding: 10px 24px;
cursor: pointer;
color: #444;
font-size: 15px;
border-left: 3px solid transparent;
transition: all 0.2s;
}
.menu-item.active {
color: #256dff;
font-weight: bold;
background: #f6fbff;
border-left: 3px solid #256dff;
}
.main-content {
flex: 1;
min-width: 0;
}
:deep(.drawer) {
.el-drawer__header {
margin: 0;
padding-bottom: 20px;
}
.el-drawer__title {
font-weight: bold;
font-size: 32px;
color: #303233;
}
.el-drawer__body {
padding-top: 12px;
background-color: #F7F8F9;
}
}
.info-block {
background-color: #ffffff;
padding: 4px 0;
margin-bottom: 14px;
}
.info-item {
padding: 12px 48px;
.label {
width: 100px;
font-weight: 400;
font-size: 14px;
color: #626566;
}
.value {
font-weight: bold;
font-size: 14px;
color: #303233;
display: flex; // 使用 flex
justify-content: center; // 水平居中
align-items: center; // 垂直居中
}
}
.drawer-footer {
padding: 20px 48px 10px 48px;
.price {
font-weight: bold;
font-size: 24px;
color: #FF9811;
& + div {
font-weight: 400;
font-size: 14px;
color: #949899;
text-align: right;
}
}
.el-button {
width: 180px;
height: 46px;
font-size: 20px;
border-radius: 2px 2px 2px 2px;
}
}
.checkbox-info {
display: flex;
align-items: center;
background: #ffffff;
padding-left: 48px;
padding-bottom: 10px;
.link {
color: #409eff;
cursor: pointer;
}
}
#pdf-container {
width: 100%; /* 设置容器宽度 */
height: 600px; /* 设置容器高度 */
border: 1px solid #ccc; /* 可选:添加边框 */
}
.pdf-box {
height: 600px;
//overflow: hidden;
overflow-y: auto;
}
</style>
<template>
<div class="custom-wrapper order-confirm-container">
<div class="com-breadcrumb-1">
<div class="custom-main-w">
<el-breadcrumb separator=">">
</el-breadcrumb>
</div>
</div>
<div class="custom-main-w order-main">
<h2 class="order-title">确认订单</h2>
<div class="order-info-list">
<!-- <div class="order-info-item">-->
<!-- <span class="label">应付金额:</span><span class="amount">¥1</span>-->
<!-- </div>-->
<div class="order-info-item">
<span class="label">套餐金额:</span>{{orderInfo.price}}
</div>
<div class="order-info-item">
<span class="label">有效期:</span>{{orderInfo.validDays}}
</div>
<div class="order-info-item">
<span class="label">调用量:</span>{{orderInfo.times}}
</div>
<div class="order-info-item">
<span class="label">购买数量:</span>{{orderInfo.quantity}}
</div>
<div class="order-info-item order-agreement">
<el-checkbox v-model="checked"
>同意并阅读《API服务购买协议》,并确保合法使用此数据。</el-checkbox
>
</div>
</div>
<div class="order-pay-btns">
<el-button type="success">立即支付</el-button>
</div>
<div class="order-tips">
<div class="tips-title">温馨提示:</div>
<ul>
<li>
1、一个接口是一组接入点,每个接入点有自己的计费率;资源包过期或用完,会立即失效;
</li>
<li>2、可购买多个资源包,会自动递延生效联使用</li>
<li>3、如需要更多的请求次数、更优惠的价格请与我们联系;</li>
<li>
4、您可以联系到我们的商务进行线下充值,确认到账后我们将会为您开通相应的数据技术服务;
</li>
</ul>
</div>
</div>
</div>
</template>
<script setup>
import { ref } from "vue";
const checked = ref(false);
import { useRoute } from "vue-router";
const route = useRoute();
const orderInfo = computed(() => ({
id: route.query.id,
name: route.query.name,
price: route.query.price,
validityDays: route.query.validDays,
times: route.query.times,
quantity: route.query.quantity,
amount: (route.query.price * route.query.quantity).toFixed(2),
}));
</script>
<style scoped lang="scss">
.order-confirm-container {
min-height: 100vh;
}
.order-main {
background: #fff;
margin: 0 auto;
box-shadow: 0 2px 12px #e6f0fa;
padding: 50px 160px 100px;
}
.order-title {
text-align: center;
font-size: 28px;
font-weight: bold;
margin-bottom: 32px;
}
.order-info-list {
margin-bottom: 30px;
}
.order-info-item {
font-size: 20px;
color: #666;
margin-bottom: 18px;
.amount {
color: #f56c6c;
font-size: 22px;
font-weight: bold;
}
}
.order-info-item .label {
min-width: 90px;
font-size: 20px;
color: #333;
margin-right: 20px;
display: inline-block;
}
.order-tips {
border: 1px solid var(--el-color-primary);
border-radius: 6px;
background: #f6fbff;
padding: 18px 24px;
margin: 24px 0 35px 0;
}
.tips-title {
color: var(--el-color-primary);
font-weight: bold;
margin-bottom: 8px;
}
.order-tips ul {
font-size: 15px;
padding-left: 18px;
}
.order-agreement {
:deep .el-checkbox__label {
font-size: 16px;
}
}
.order-pay-btns {
display: flex;
align-items: center;
gap: 18px;
margin-top: 18px;
padding-top: 40px;
border-top: 1px solid var(--el-border-color);
:deep .el-button {
height: 45px;
}
}
.order-pay-btns .balance {
color: #888;
font-size: 20px;
margin-left: 4px;
}
.order-pay-btns .recharge {
color: #f56c6c;
font-size: 20px;
margin-left: 4px;
cursor: pointer;
}
</style>
<template>
<el-card class="api-card" @click="goToDetail">
<div class="card-content">
<div v-if="props.tag" class="corner-tag">{{ props.tag }}</div>
<div class="icon-wrapper">
<el-image :src="props.coverImage" fit="cover" class="icon" />
<div class="title-wrapper">
<h3 class="title">
{{ props.title }}
</h3>
<!-- <div class="tags">-->
<!-- <el-tag effect="plain">{{props.tag}}</el-tag>-->
<!-- </div>-->
</div>
</div>
<div class="info-wrapper">
<p class="description">
{{ props.description }}{{ props.description }}
</p>
<div class="price">
<span class="main_currency"> 低至 ¥</span>
<span class="amount">{{ props.priceInfo }}</span>
<span class="unit"> /次</span>
</div>
</div>
</div>
</el-card>
</template>
<script setup lang="ts">
import { useRouter } from "vue-router";
interface Props {
id?: number;
coverImage: string;
title: string;
description: string;
priceInfo: string;
tag?: string;
}
const props = defineProps<Props>();
const router = useRouter();
const goToDetail = () => {
if (props.id) {
router.push(`/marketplace/ai/detail?id=${props.id}`);
}
};
</script>
<style scoped lang="scss">
.api-card {
width: 100%;
margin-bottom: 40px;
cursor: pointer;
transition: all 0.3s;
border-radius: 10px;
}
.api-card:hover {
transform: translateY(-2px);
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
}
:deep(.el-card__body) {
padding: 0 !important;
}
.card-content {
padding: 20px 30px;
position: relative;
}
.corner-tag {
position: absolute;
top: 0;
right: 0;
background-color: #f56c6c;
color: white;
font-size: 12px;
padding: 6px 15px;
border-bottom-left-radius: 10px;
}
.icon-wrapper {
position: relative;
margin-bottom: 12px;
border-bottom: 1px solid #e8e8e8;
padding: 0 10px 15px;
display: flex;
.icon {
width: 70px;
height: 70px;
margin-right: 20px;
object-fit: cover;
}
}
.title-wrapper {
flex: 1;
width: calc(100% - 74px);
}
.title {
font-size: 20px;
line-height: 40px;
height: 40px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-weight: 600;
margin: 0 0 8px 0;
}
.tags {
display: flex;
gap: 10px;
.el-tag {
height: 26px;
}
}
.description {
font-size: 16px;
color: #666;
margin: 10px 0;
line-height: 25px;
height: 50px;
overflow: hidden;
width: 100%;
text-indent: 32px;
display: -webkit-box;
-webkit-line-clamp: 2; /* 限制文本块显示两行 */
-webkit-box-orient: vertical;
}
.price {
display: flex;
align-items: baseline; // 保证文字基线对齐
gap: 4px; // 控制间距
color: #f56c6c;
.main-currency {
font-size: 14px;
font-weight: 500;
}
.amount {
font-size: 22px;
font-weight: bold;
line-height: 1;
}
.unit {
font-size: 14px;
color: #999;
}
}
</style>
<template>
<div class="related-apis">
<h3 class="title">相关API</h3>
<div
class="api-item"
v-for="api in apis"
:key="api.id"
@click="goToDetail(api.id)"
>
<img class="api-icon" :src="api.image" :alt="api.name" />
<div class="api-info">
<p class="api-title">{{ api.name }}</p>
<p class="api-desc">{{ api.description }}</p>
</div>
</div>
</div>
</template>
<script setup>
import { defineProps } from "vue";
import { useRouter } from "vue-router";
const router = useRouter();
const props = defineProps({
apis: {
type: Array,
required: true,
},
});
const goToDetail = (appId) => {
router.push({ path: "/marketplace/ai/detail", query: { id: appId } });
};
</script>
<style scoped>
.related-apis {
margin-top: 32px;
}
.title {
font-size: 18px;
font-weight: 600;
margin-bottom: 16px;
}
.api-item {
display: flex;
align-items: flex-start;
gap: 16px;
margin-bottom: 18px;
cursor: pointer;
transition: all 0.2s;
padding: 8px;
border-radius: 8px;
}
.api-item:hover {
background-color: #f5f5f5;
}
.api-icon {
width: 50px;
/* height: 40px; */
border-radius: 8px;
background: #f5f5f5;
/* width: 64px; */
height: auto;
max-height: 50px;
}
.api-info {
flex: 1;
}
.api-title {
font-size: 15px;
font-weight: 500;
margin-bottom: 4px;
}
.api-desc {
font-size: 13px;
color: #888;
height: 48px;
line-height: 24px;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2; /* 显示两行 */
}
</style>
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