支付模块重构完成

This commit is contained in:
RockYang
2025-08-30 16:27:39 +08:00
parent 3a6f8ccc16
commit 3c065b99fb
17 changed files with 661 additions and 317 deletions

View File

@@ -81,7 +81,8 @@
width: 100%;
height: 100%;
z-index: 0;
background: url('data:image/svg+xml;utf8,<svg width="100%25" height="100%25" viewBox="0 0 400 200" fill="none" xmlns="http://www.w3.org/2000/svg"><circle cx="100" cy="100" r="80" fill="%23e0eaff"/><circle cx="300" cy="60" r="40" fill="%23f0f7ff"/><circle cx="320" cy="180" r="30" fill="%23e0eaff"/></svg>') no-repeat center/cover;
background: url('data:image/svg+xml;utf8,<svg width="100%25" height="100%25" viewBox="0 0 400 200" fill="none" xmlns="http://www.w3.org/2000/svg"><circle cx="100" cy="100" r="80" fill="%23e0eaff"/><circle cx="300" cy="60" r="40" fill="%23f0f7ff"/><circle cx="320" cy="180" r="30" fill="%23e0eaff"/></svg>')
no-repeat center/cover;
opacity: 0.08;
pointer-events: none;
}
@@ -99,115 +100,259 @@
}
.list-box {
.product-col {
animation: fadeInUp 0.6s ease-out;
animation-fill-mode: both;
&:nth-child(1) {
animation-delay: 0.1s;
}
&:nth-child(2) {
animation-delay: 0.2s;
}
&:nth-child(3) {
animation-delay: 0.3s;
}
&:nth-child(4) {
animation-delay: 0.4s;
}
&:nth-child(5) {
animation-delay: 0.5s;
}
&:nth-child(6) {
animation-delay: 0.6s;
}
}
.product-item {
// border: 1px solid #666666;
background-color: var(--chat-bg);
border-radius: 6px;
background: linear-gradient(135deg, var(--panel-bg) 0%, var(--chat-bg) 100%);
border-radius: 16px;
overflow: hidden;
cursor: pointer;
transition: all 0.3s ease; /* 添加过渡效果 */
margin-bottom: 20px;
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
margin-bottom: 24px;
border: 1px solid rgba(255, 255, 255, 0.1);
position: relative;
.image-container {
display: flex;
justify-content: center;
.product-header {
position: relative;
.el-image {
padding: 6px;
.el-image__inner {
border-radius: 10px;
}
}
}
.product-title {
display: flex;
padding: 10px;
.name {
width: 100%;
text-align: center;
font-size: 16px;
font-weight: bold;
color: var(--el-color-primary);
}
}
.product-info {
padding: 10px 20px;
font-size: 14px;
color: #999999;
.info-line {
display: flex;
width: 100%;
padding: 5px 0;
.label {
display: flex;
width: 100%;
}
.price, .expire, calls {
display: flex;
width: 90px;
justify-content: right;
}
.discount {
color: #f56c6c;
font-size: 20px;
}
.expire {
color: #409eff;
}
.power {
color: #f2cb51;
}
}
.pay-way {
padding: 10px 0;
.image-container {
position: relative;
display: flex;
justify-content: center;
flex-wrap: wrap;
padding: 20px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
.el-button {
margin: 10px 5px 0 5px;
height: 32px;
filter: none;
.el-image {
width: 80px;
height: 80px;
border-radius: 50%;
overflow: hidden;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
transition: transform 0.3s ease;
.icon-alipay, .icon-wechat-pay {
color: #ffffff;
&:hover {
transform: scale(1.1);
}
.icon-qq {
color: #15a6e8;
font-size: 24px;
}
.image-overlay {
position: absolute;
top: 10px;
right: 10px;
.vip-badge {
background: linear-gradient(135deg, #ffd700 0%, #ffed4e 100%);
color: #333;
padding: 4px 12px;
border-radius: 20px;
font-size: 12px;
font-weight: bold;
box-shadow: 0 4px 12px rgba(255, 215, 0, 0.4);
animation: pulse 2s infinite;
}
.icon-jd-pay {
color: var(--text-theme-color);
font-size: 24px;
}
}
.product-title {
padding: 20px 20px 0;
text-align: center;
.name {
font-size: 20px;
font-weight: 700;
color: var(--text-theme-color);
margin: 0 0 8px 0;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.description {
font-size: 14px;
color: var(--text-secondary-color, #666);
margin: 0;
line-height: 1.4;
}
}
}
.product-content {
padding: 20px;
.price-section {
text-align: center;
margin-bottom: 20px;
.price-info {
display: flex;
align-items: baseline;
justify-content: center;
margin-bottom: 8px;
.currency {
font-size: 18px;
color: #f56c6c;
font-weight: 600;
margin-right: 2px;
}
.icon-douyin {
color: #0a0a0a;
font-size: 22px;
.price-value {
font-size: 32px;
font-weight: 800;
color: #f56c6c;
line-height: 1;
}
.icon-paypal {
.price-unit {
font-size: 14px;
color: #009cde;
color: var(--text-secondary-color, #666);
margin-left: 4px;
}
}
.original-price {
font-size: 12px;
color: #999;
text-decoration: line-through;
}
}
.features-list {
.feature-item {
display: flex;
align-items: center;
margin-bottom: 8px;
font-size: 14px;
color: var(--text-secondary-color, #666);
i {
color: #67c23a;
margin-right: 8px;
font-size: 16px;
}
}
}
}
.product-actions {
padding: 0 20px 20px;
.payment-buttons {
display: flex;
gap: 12px;
.payment-btn {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
padding: 12px 16px;
border: none;
border-radius: 12px;
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
position: relative;
overflow: hidden;
transform: translateZ(0);
&::before {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(
90deg,
transparent,
rgba(255, 255, 255, 0.2),
transparent
);
transition: left 0.5s;
}
&:hover::before {
left: 100%;
}
&:active {
transform: translateY(1px) scale(0.98);
}
i {
font-size: 18px;
transition: transform 0.3s ease;
}
span {
font-weight: 600;
transition: transform 0.3s ease;
}
&:hover {
i,
span {
transform: scale(1.05);
}
}
&.wechat-btn {
background: linear-gradient(135deg, #07c160 0%, #06ad56 100%);
color: white;
box-shadow: 0 4px 16px rgba(7, 193, 96, 0.3);
&:hover {
transform: translateY(-2px);
box-shadow: 0 8px 24px rgba(7, 193, 96, 0.4);
background: linear-gradient(135deg, #06ad56 0%, #07c160 100%);
}
}
&.alipay-btn {
background: linear-gradient(135deg, #1677ff 0%, #0e5fd8 100%);
color: white;
box-shadow: 0 4px 16px rgba(22, 119, 255, 0.3);
&:hover {
transform: translateY(-2px);
box-shadow: 0 8px 24px rgba(22, 119, 255, 0.4);
background: linear-gradient(135deg, #0e5fd8 0%, #1677ff 100%);
}
}
}
}
}
&:hover {
// box-shadow: 0 0 10px rgba(71, 255, 241, 0.6); /* 添加阴影效果 */
transform: translateY(-10px); /* 向上移动10像素 */
box-shadow: 0 0 10px var(--shadow-color);
background-color: var(--hover-deep-color);
transform: translateY(-8px);
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.15);
border-color: rgba(102, 126, 234, 0.3);
}
}
}
@@ -234,4 +379,168 @@
font-weight: 700;
}
}
}
}
// 添加动画效果
@keyframes pulse {
0%,
100% {
transform: scale(1);
opacity: 1;
}
50% {
transform: scale(1.05);
opacity: 0.8;
}
}
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(30px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
// 响应式优化
@media (max-width: 768px) {
.member {
.inner {
.product-box {
.list-box {
.el-col {
width: 100% !important;
margin-bottom: 16px;
}
.product-item {
.product-header {
.image-container {
padding: 16px;
.el-image {
width: 60px;
height: 60px;
}
}
.product-title {
padding: 16px 16px 0;
.name {
font-size: 18px;
}
}
}
.product-content {
padding: 16px;
.price-section {
.price-info {
.price-value {
font-size: 28px;
}
}
}
}
.product-actions {
padding: 0 16px 16px;
.payment-buttons {
flex-direction: column;
gap: 8px;
.payment-btn {
padding: 10px 14px;
}
}
}
}
}
}
}
}
}
@media (max-width: 480px) {
.member {
.inner {
padding: 10px 0 10px 10px;
.product-box {
padding: 0 10px;
.list-box {
.product-item {
margin-bottom: 16px;
.product-header {
.image-container {
padding: 12px;
.el-image {
width: 50px;
height: 50px;
}
}
.product-title {
padding: 12px 12px 0;
.name {
font-size: 16px;
}
.description {
font-size: 12px;
}
}
}
.product-content {
padding: 12px;
.price-section {
.price-info {
.price-value {
font-size: 24px;
}
.currency {
font-size: 16px;
}
}
}
.features-list {
.feature-item {
font-size: 12px;
}
}
}
.product-actions {
padding: 0 12px 12px;
.payment-buttons {
.payment-btn {
padding: 8px 12px;
font-size: 12px;
i {
font-size: 16px;
}
}
}
}
}
}
}
}
}
}

View File

@@ -18,7 +18,7 @@
<span>{{ scope.row.remark && scope.row.remark.power }}</span>
</template>
</el-table-column>
<el-table-column prop="pay_method" label="支付渠道" />
<el-table-column prop="channel_name" label="支付渠道" />
<el-table-column prop="pay_name" label="支付名称" />
<el-table-column label="支付时间">
<template #default="scope">

View File

@@ -340,7 +340,7 @@
<VideoPause />
</el-icon>
</el-button>
<el-button @click="sendMessage" style="color: #754ff6" v-else>
<el-button @click="sendMessage()" style="color: #754ff6" v-else>
<el-tooltip class="box-item" effect="dark" content="发送">
<el-icon><Promotion /></el-icon>
</el-tooltip>
@@ -832,7 +832,7 @@ const sendSSERequest = async (message) => {
}
// 发送消息
const sendMessage = (messageId) => {
const sendMessage = (messageId = 0) => {
if (!isLogin.value) {
console.log('未登录')
store.setShowLoginDialog(true)
@@ -895,7 +895,7 @@ const sendMessage = (messageId) => {
tools: toolSelected.value,
stream: stream.value,
files: files.value,
last_msg_id: messageId,
last_msg_id: messageId || 0,
})
prompt.value = ''

View File

@@ -35,69 +35,56 @@
<div class="profile-bg"></div>
<div class="product-box">
<!-- <div class="info" v-if="orderPayInfoText !== ''">
<el-alert type="success" show-icon :closable="false" effect="dark">
<strong>说明:</strong> {{ vipInfoText }}
</el-alert>
</div> -->
<el-row v-if="list.length > 0" :gutter="20" class="list-box">
<el-col v-for="item in list" :key="item" :span="6">
<el-row v-if="list.length > 0" :gutter="24" class="list-box">
<el-col
v-for="item in list"
:key="item"
:xs="24"
:sm="12"
:md="8"
:lg="6"
class="product-col"
>
<div class="product-item">
<div class="image-container">
<el-image :src="vipImg" fit="cover" />
</div>
<div class="product-title">
<span class="name">{{ item.name }}</span>
</div>
<div class="product-info">
<div class="info-line">
<span class="label">商品原价</span>
<span class="price"
><del>{{ item.price }}</del></span
>
<div class="product-header">
<div class="image-container">
<el-image :src="vipImg" fit="cover" />
<div class="image-overlay">
<div class="vip-badge">热销</div>
</div>
</div>
<div class="info-line">
<span class="label">优惠价</span>
<span class="discount">{{ item.discount }}</span>
<div class="product-title">
<h3 class="name">{{ item.name }}</h3>
<p class="description">{{ item.description || '全模型通用算力' }}</p>
</div>
<div class="info-line">
<span class="label">有效期</span>
<span class="expire" v-if="item.days > 0">{{ item.days }}</span>
<span class="expire" v-else>长期有效</span>
</div>
<div class="product-content">
<div class="price-section">
<div class="price-info">
<span class="currency"></span>
<span class="price-value">{{ item.price }}</span>
</div>
</div>
<div class="info-line">
<span class="label">算力值</span>
<span class="power">{{ item.power }}</span>
<div class="features-list" v-if="item.features">
<div class="feature-item" v-for="feature in item.features" :key="feature">
<i class="iconfont icon-check"></i>
<span>{{ feature }}</span>
</div>
</div>
</div>
<div class="pay-way">
<span
type="primary"
v-for="payWay in payWays"
@click="pay(item, payWay)"
:key="payWay"
>
<el-button v-if="payWay.pay_type === 'alipay'" color="#15A6E8" circle>
<i class="iconfont icon-alipay"></i>
</el-button>
<el-button v-else-if="payWay.pay_type === 'qqpay'" circle>
<i class="iconfont icon-qq"></i>
</el-button>
<el-button v-else-if="payWay.pay_type === 'paypal'" class="paypal" round>
<i class="iconfont icon-paypal"></i>
</el-button>
<el-button v-else-if="payWay.pay_type === 'jdpay'" color="#E1251B" circle>
<i class="iconfont icon-jd-pay"></i>
</el-button>
<el-button v-else-if="payWay.pay_type === 'douyin'" class="douyin" circle>
<i class="iconfont icon-douyin"></i>
</el-button>
<el-button v-else circle class="wechat" color="#67C23A">
<i class="iconfont icon-wechat-pay"></i>
</el-button>
</span>
<div class="product-actions">
<div class="payment-buttons">
<button class="payment-btn wechat-btn" @click="wxPay(item)">
<i class="iconfont icon-wechat-pay"></i>
<span>微信支付</span>
</button>
<button class="payment-btn alipay-btn" @click="alipay(item)">
<i class="iconfont icon-alipay"></i>
<span>支付宝</span>
</button>
</div>
</div>
</div>
@@ -154,23 +141,20 @@
</el-dialog>
</div>
<!--支付二维码-->
<el-dialog
v-model="showDialog"
:show-close="false"
:close-on-click-modal="false"
hide-footer
width="auto"
v-model="showQrCode"
:show-close="true"
style="width: 334px; height: 368px"
class="pay-dialog"
>
<div v-if="qrImg !== ''">
<div class="product-info">
请使用微信扫码支付<span class="price">{{ price }}</span>
<template #header>
<div class="flex items-center justify-center text-base">
<span style="color: var(--el-text-color-regular)">{{ title }}</span>
</div>
<el-image :src="qrImg" fit="cover" />
</div>
<div style="padding-bottom: 10px; text-align: center">
<el-button type="success" @click="payCallback(true)">支付成功</el-button>
<el-button type="danger" @click="payCallback(false)">支付失败</el-button>
</template>
<div class="qr-container">
<el-image :src="qrImg" style="height: 300px; width: 300px" />
</div>
</el-dialog>
</div>
@@ -188,7 +172,9 @@ import { useSharedStore } from '@/store/sharedata'
import { httpGet, httpPost } from '@/utils/http'
import { ElMessage } from 'element-plus'
import QRCode from 'qrcode'
import { onMounted, ref } from 'vue'
import { onMounted, ref, onUnmounted } from 'vue'
import { showLoading, closeLoading } from '@/utils/dialog'
import { isMobile } from '@/utils/libs'
const list = ref([])
const vipImg = ref('/images/menu/member.png')
@@ -203,15 +189,13 @@ const isLogin = ref(false)
const orderTimeout = ref(1800)
const loading = ref(true)
const loadingText = ref('加载中...')
const orderPayInfoText = ref('')
const payWays = ref([])
const vipInfoText = ref('')
const store = useSharedStore()
const userOrderKey = ref(0)
const showDialog = ref(false)
const showQrCode = ref(false)
const qrImg = ref('')
const price = ref(0)
const title = ref('')
const handler = ref(null)
onMounted(() => {
checkSession()
@@ -236,68 +220,78 @@ onMounted(() => {
.then((res) => {
rewardImg.value = res.data['reward_img']
enableReward.value = res.data['enabled_reward']
orderPayInfoText.value = res.data['order_pay_info_text']
if (res.data['order_pay_timeout'] > 0) {
orderTimeout.value = res.data['order_pay_timeout']
}
vipInfoText.value = res.data['vip_info_text']
})
.catch((e) => {
ElMessage.error('获取系统配置失败:' + e.message)
})
httpGet('/api/payment/payWays')
.then((res) => {
payWays.value = res.data
})
.catch((e) => {
ElMessage.error('获取支付方式失败:' + e.message)
})
})
const pay = (product, payWay) => {
if (!isLogin.value) {
store.setShowLoginDialog(true)
return
}
loading.value = true
loadingText.value = '正在生成支付订单...'
let host = import.meta.env.VITE_API_HOST
if (host === '') {
host = `${location.protocol}//${location.host}`
}
httpPost(`${import.meta.env.VITE_API_HOST}/api/payment/doPay`, {
product_id: product.id,
pay_way: payWay.pay_way,
pay_type: payWay.pay_type,
user_id: user.value.id,
host: host,
device: 'jump',
const selectedPid = ref(0)
const wxPay = (product) => {
selectedPid.value = product.id
title.value = '请打开微信扫码支付'
generateOrder('wxpay')
}
const alipay = (product) => {
selectedPid.value = product.id
title.value = '请打开支付宝扫码支付'
generateOrder('alipay')
}
const generateOrder = (payWay) => {
showLoading('正在生成支付订单...')
// 生成支付订单
httpPost('/api/payment/create', {
pid: selectedPid.value,
pay_way: payWay,
domain: `${window.location.protocol}//${window.location.host}`,
device: isMobile() ? 'mobile' : 'pc',
})
.then((res) => {
showDialog.value = true
loading.value = false
if (payWay.pay_way === 'wechat') {
price.value = Number(product.discount)
QRCode.toDataURL(res.data, { width: 300, height: 300, margin: 2 }, (error, url) => {
closeLoading()
if (isMobile()) {
window.location.href = res.data.pay_url
} else {
QRCode.toDataURL(res.data.pay_url, { width: 300, height: 300, margin: 2 }, (error, url) => {
if (error) {
console.error(error)
} else {
qrImg.value = url
}
})
} else {
window.open(res.data, '_blank')
// 查询订单状态
if (handler.value) {
clearTimeout(handler.value)
}
handler.value = setTimeout(() => queryOrder(res.data.order_no), 3000)
showQrCode.value = true
}
})
.catch((e) => {
setTimeout(() => {
ElMessage.error('生成支付订单失败:' + e.message)
loading.value = false
}, 500)
closeLoading()
ElMessage.error('生成支付订单失败:' + e.message)
})
}
const queryOrder = async (orderNo) => {
const res = await httpGet('/api/order/query?order_no=' + orderNo)
if (res?.data.status === 2) {
// 订单支付成功
clearTimeout(handler.value)
ElMessage.success('支付成功')
showQrCode.value = false
// 更新用户积分
user.value.scores += res.data.credit
} else {
handler.value = setTimeout(() => queryOrder(orderNo), 3000)
}
}
const redeemCallback = (success) => {
showRedeemVerifyDialog.value = false
@@ -306,15 +300,67 @@ const redeemCallback = (success) => {
}
}
const payCallback = (success) => {
showDialog.value = false
if (success) {
userOrderKey.value += 1
// 组件卸载时清理定时器
onUnmounted(() => {
if (handler.value) {
clearTimeout(handler.value)
handler.value = null
}
}
})
</script>
<style lang="scss" scoped>
@use '../assets/css/custom-scroll.scss' as *;
@use '../assets/css/member.scss' as *;
@use '@/assets/css/custom-scroll.scss' as *;
@use '@/assets/css/member.scss' as *;
// 支付弹窗样式优化
.pay-dialog {
.qr-container {
text-align: center;
position: relative;
.qr-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(255, 255, 255, 0.9);
display: flex;
align-items: center;
justify-content: center;
.success-text {
background: #67c23a;
color: white;
border-radius: 4px;
font-size: 14px;
}
}
}
}
// 支付按钮样式
.pay-way {
.row {
margin: 0;
.col {
padding: 0 5px;
button {
border: none;
cursor: pointer;
transition: all 0.3s ease;
font-size: 14px;
font-weight: 500;
&:hover {
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
}
}
}
}
}
</style>

View File

@@ -42,14 +42,23 @@
</template>
</el-table-column>
<el-table-column label="支付时间">
<template #default="scope">
<span v-if="scope.row['pay_time']">{{ dateFormat(scope.row['pay_time']) }}</span>
<el-tag v-else>未支付</el-tag>
<el-table-column label="订单状态">
<template #default="{ row }">
<el-tag v-if="row.status === 2" type="success">已支付</el-tag>
<el-tag v-else-if="row.status === 3" type="danger">已关闭</el-tag>
<el-tag v-else type="warning">未支付</el-tag>
</template>
</el-table-column>
<el-table-column prop="pay_method" label="支付渠道" />
<el-table-column prop="pay_name" label="支付名称" />
<!-- 支付时间 -->
<el-table-column label="支付时间">
<template #default="row">
<span>{{ dateFormat(row.pay_time) || '--' }}</span>
</template>
</el-table-column>
<el-table-column prop="pay_name" label="支付渠道" min-width="100" />
<el-table-column prop="channel_name" label="支付渠道" min-width="100" />
<el-table-column label="操作" width="180">
<template #default="scope">

View File

@@ -14,14 +14,7 @@
</span>
</template>
</el-table-column>
<el-table-column prop="price" label="商品" />
<el-table-column prop="discount" label="优惠价" />
<el-table-column prop="days" label="有效期()">
<template #default="scope">
<el-tag v-if="scope.row.days === 0">长期有效</el-tag>
<span v-else>{{ scope.row.days }}</span>
</template>
</el-table-column>
<el-table-column prop="price" label="商品价" />
<el-table-column prop="power" label="算力" />
<el-table-column prop="sales" label="销量" />
<el-table-column prop="enabled" label="启用状态">
@@ -55,18 +48,10 @@
<el-input v-model="item.name" autocomplete="off" />
</el-form-item>
<el-form-item label="商品" prop="price">
<el-form-item label="商品价" prop="price">
<el-input v-model="item.price" autocomplete="off" />
</el-form-item>
<el-form-item label="优惠价" prop="discount">
<el-input v-model="item.discount" autocomplete="off" />
</el-form-item>
<el-form-item label="有效期" prop="days">
<el-input v-model.number="item.days" autocomplete="off" placeholder="会员有效期()" />
</el-form-item>
<el-form-item label="算力" prop="power">
<el-input v-model.number="item.power" autocomplete="off" placeholder="增加算力值" />
</el-form-item>