更新 #9

Merged
nanxun merged 1 commits from dev into main 2025-08-09 22:57:15 +08:00
26 changed files with 2243 additions and 943 deletions

View File

@ -10,9 +10,6 @@
</template>
<script>
import Alert from '@/components/Alert.vue';
import { mapGetters, mapState } from 'vuex';
export default {
data(){
return {

BIN
src/assets/images/qq.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

View File

@ -1,70 +1,81 @@
<template>
<div v-if="visible" class="modal fade show" id="exampleModalCenter" tabindex="-1" aria-labelledby="exampleModalLabel" style="display: block; padding-right: 15px;" _mstvisible="0" aria-modal="true" role="dialog">
<div class="modal-dialog modal-dialog-centered" _mstvisible="1">
<div class="modal-content" _mstvisible="2">
<div class="modal-header" _mstvisible="3">
<h5 class="modal-title" id="exampleModalCenterTitle" _msttexthash="13222534" _msthash="147" _mstvisible="4">{{title}}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="关闭" _mstaria-label="59709" _msthash="148" _mstvisible="4" @click="cancel"></button>
</div>
<div class="formcontent">
<form>
<div class="row mb-3">
<label for="inputName3" class="col-sm-2 col-form-label">接口名称</label>
<div class="col-sm-10">
<input type="text" v-model="form.name" class="form-control" id="inputName3">
</div>
</div>
<div class="row mb-3">
<label for="inputDescription3" class="col-sm-2 col-form-label">接口描述</label>
<div class="col-sm-10">
<input type="text" v-model="form.description" class="form-control" id="inputDescription3">
</div>
</div>
<fieldset class="row mb-3">
<legend class="col-form-label col-sm-2 pt-0">调用方法</legend>
<div class="col-sm-10">
<div class="form-check">
<input class="form-check-input" type="radio" name="gridRadios" id="gridRadios1" v-model="form.method" value="0" :checked="form.method == 0 ? true : false">
<label class="form-check-label" for="gridRadios1">
GET
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="radio" name="gridRadios" id="gridRadios2" v-model="form.method" value="1" :checked="form.method == 1 ? true : false">
<label class="form-check-label" for="gridRadios1">
POST
</label>
</div>
</div>
</fieldset>
<div class="row mb-3">
<label class="visually-hidden" for="PackageSelect">所属套餐</label>
<select class="form-select" id="PackageSelect" v-model="form.packageId">
<option v-for="(item,index) in packageList" :key="index" :value="item.id" :selected="form.packageId == item.id ? true : false">{{item.name}}</option>
</select>
</div>
</form>
</div>
<div class="modal-footer" _mstvisible="3">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal" _msttexthash="5889065" _msthash="150" _mstvisible="4" @click="cancel">关闭</button>
<button type="button" class="btn btn-primary" _msttexthash="10744773" _msthash="151" _mstvisible="4" @click="submit">保存更改</button>
<div v-if="visible" class="modal fade show" id="exampleModalCenter" tabindex="-1" aria-labelledby="exampleModalLabel"
style="display: block; padding-right: 15px;" _mstvisible="0" aria-modal="true" role="dialog">
<div class="modal-dialog modal-dialog-centered" _mstvisible="1">
<div class="modal-content" _mstvisible="2">
<div class="modal-header" _mstvisible="3">
<h5 class="modal-title" id="exampleModalCenterTitle" _msttexthash="13222534" _msthash="147" _mstvisible="4">
{{ title }}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="关闭" _mstaria-label="59709"
_msthash="148" _mstvisible="4" @click="cancel"></button>
</div>
<div class="formcontent">
<form>
<div class="row mb-3">
<label for="inputName3" class="col-sm-2 col-form-label">接口名称</label>
<div class="col-sm-10">
<input type="text" v-model="form.name" class="form-control" id="inputName3">
</div>
</div>
<div class="row mb-3">
<label for="inputDescription3" class="col-sm-2 col-form-label">接口描述</label>
<div class="col-sm-10">
<input type="text" v-model="form.description" class="form-control" id="inputDescription3">
</div>
</div>
<fieldset class="row mb-3">
<legend class="col-form-label col-sm-2 pt-0">调用方法</legend>
<div class="col-sm-10">
<div class="form-check">
<input class="form-check-input" type="radio" name="gridRadios" id="gridRadios1" v-model="form.method"
value="0" :checked="form.method == 0 ? true : false">
<label class="form-check-label" for="gridRadios1">
GET
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="radio" name="gridRadios" id="gridRadios2" v-model="form.method"
value="1" :checked="form.method == 1 ? true : false">
<label class="form-check-label" for="gridRadios1">
POST
</label>
</div>
</div>
</fieldset>
<div class="row mb-3">
<label class="form-label fw-bold" for="PackageSelect">所属套餐</label>
<select class="form-select" id="PackageSelect" v-model="form.packageId" multiple size="5">
<option v-for="(item, index) in packageList" :key="index" :value="item.id"
>
{{ item.name }}
</option>
</select>
</div>
</form>
</div>
<div class="modal-footer" _mstvisible="3">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal" _msttexthash="5889065" _msthash="150"
_mstvisible="4" @click="cancel">关闭</button>
<button type="button" class="btn btn-primary" _msttexthash="10744773" _msthash="151" _mstvisible="4"
@click="submit">保存更改</button>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'ApiFormModal',
props:{
title:{
type:String
props: {
title: {
type: String
},
formType: {
type: String,
default: 'edit'
type: String,
default: 'edit'
}
},
data() {
@ -76,20 +87,20 @@ export default {
name: '',
description: '',
method: '0',
packageId: '0'
packageId: []
},
resolve: null
};
},
methods: {
open(apiId,apiInfo = null,packageList) {
open(apiId, apiInfo = null, packageList, apipackageitem) {
this.resetForm();
this.visible = true;
this.apiId = apiId;
this.form.name = apiInfo.name;
this.form.description = apiInfo.description;
this.form.method = apiInfo.method.toString();
this.form.packageId = apiInfo.packageId.toString();
this.form.packageId = apipackageitem
this.packageList = packageList
return new Promise((resolve) => {
this.resolve = resolve;
@ -98,19 +109,19 @@ export default {
submit() {
this.visible = false;
this.form.method = Number(this.form.method)
this.form.packageId = Number(this.form.packageId)
//this.form.packageId = Number(this.form.packageId)
this.resolve && this.resolve(this.form);
},
cancel() {
this.visible = false;
this.resolve && this.resolve(null);
this.resolve && this.resolve(null);
},
resetForm() {
this.form = {
name: '',
description: '',
method: '0',
packageId: '0'
packageId: []
};
}
}
@ -119,9 +130,20 @@ export default {
<style scoped>
.modal {
background: rgba(0, 0, 0, 0.5);
background: rgba(0, 0, 0, 0.5);
}
.formcontent {
padding: 0 30px;
padding: 0 30px;
}
#PackageSelect {
border-radius: 8px;
padding: 8px;
}
#PackageSelect option:checked {
background-color: #0d6efd;
color: white;
}
</style>

View File

@ -0,0 +1,19 @@
<template>
<div>
<div class="blockUI blockOverlay"
style="z-index: 1000; border: none; margin: 0px; padding: 0px; width: 100%; height: 100%; top: 0px; left: 0px; background-color: rgb(0, 0, 0); opacity: 0.6; cursor: wait; position: absolute;">
</div>
<div class="blockUI blockMsg blockElement"
style="z-index: 1011; position: absolute; padding: 0px; margin: 0px; width: 30%; top: 92px; left: 321.5px; text-align: center; color: rgb(0, 0, 0); border: 3px solid rgb(170, 170, 170); background-color: rgb(255, 255, 255); cursor: wait;">
<div class="spinner-grow text-primary" role="status"><span class="sr-only">Loading...</span></div>
</div>
</div>
</template>
<script>
export default {
name:'CardLoading'
}
</script>

View File

@ -1,6 +1,6 @@
<template>
<div>
<div class="card">
<div class="card-body">
<h5 class="card-title text-center card-header">{{ title }}</h5>
<div id="zero-conf_wrapper" class="dataTables_wrapper dt-bootstrap4">
@ -237,7 +237,6 @@
</div>
</div>
</div>
</div>
</template>
<script>
@ -368,6 +367,15 @@ export default {
this.updatePageBtn(this.currentPageIndex);
},
immediate: true,
},dataCount: {
handler(newVal) {
this.updatePageCount(this.pageSize);
//
this.currentPageIndex = 1;
//this.$emit("pageChanged", { pageIndex: this.currentPageIndex, pageSize: newVal });
this.updatePageBtn(this.currentPageIndex);
},
immediate: true,
},
//
currentPageIndex: {

View File

@ -1,6 +1,6 @@
<template>
<div>
<div class="card">
<div class="card-body">
<h5 class="card-title text-center card-header">{{ title }}</h5>
<div id="zero-conf_wrapper" class="dataTables_wrapper dt-bootstrap4">
@ -132,16 +132,21 @@
</span>
</div>
<div v-if="header.type == 'orderType'">
<span class="badge bg-primary" v-if="item[header.value] == 0">
<span class="badge bg-primary" v-if="item[header.value] == 1">
充值
</span>
<span class="badge bg-success" v-if="item[header.value] == 1">
<span class="badge bg-success" v-if="item[header.value] == 0">
购买
</span>
<span class="badge bg-info" v-if="item[header.value] == 2">
退款
</span>
</div>
<div v-if="header.type == 'obj'">
<span class="badge bg-dark">
{{ item[header.value][header.property] }}
</span>
</div>
</td>
</tr>
</tbody>
@ -241,7 +246,7 @@
</div>
</div>
</div>
</div>
</div>
</template>
@ -374,6 +379,16 @@ export default {
},
immediate: true,
},
dataCount: {
handler(newVal) {
this.updatePageCount(this.pageSize);
//
this.currentPageIndex = 1;
//this.$emit("pageChanged", { pageIndex: this.currentPageIndex, pageSize: newVal });
this.updatePageBtn(this.currentPageIndex);
},
immediate: true,
},
//
currentPageIndex: {
handler(newVal) {

View File

@ -44,6 +44,8 @@
</template>
<script>
import md5 from 'blueimp-md5';
export default {
@ -87,6 +89,7 @@ export default {
submit() {
this.visible = false;
this.form.balance = Number(this.form.balance)
this.form.password = md5(this.form.password)
this.resolve && this.resolve(this.form);
},
cancel() {

View File

@ -1,3 +1,4 @@
import { param } from "jquery"
import request from "./request"
// 导出一个名为login_api的函数该函数使用request.post方法发送post请求到/api/auth/login路径
const login = async (param)=> await request.post('/api/auth/login',param)
@ -64,10 +65,9 @@ const getPackageInfoById = async (id) => await request.get(`/api/Package/GetPack
const updateSystemConfig = async (configName,configBody) => await request.post('/api/SystemConfig/UpdateSystemConfig',{configName:configName,configBody:configBody})
//创建支付
const createPayment = async (PaymentType,Amount,ReturnUrl) => await request.post('/api/pay/createpayment',{PaymentType:PaymentType,Amount:Amount,ReturnUrl:ReturnUrl});
const createPayment = async (id,Amount,ReturnUrl) => await request.post('/api/pay/createpayment',{id:id,Amount:Amount,ReturnUrl:ReturnUrl});
//支付通知
const payNotice = async (pid, trade_no, out_trade_no, type, name, money, trade_status, sign, sign_type) => {
const query = new URLSearchParams({
pid,
trade_no,
@ -87,10 +87,74 @@ const payNotice = async (pid, trade_no, out_trade_no, type, name, money, trade_s
const getUserPackagesAdmin = async (pageIndex,pageSize,desc) => await request.get(`/api/Package/GetUserPackageListAdmin?pageIndex=${pageIndex}&pageSize=${pageSize}&desc=${desc}`);
//获取订单列表
const getOrderList = async (pageIndex,pageSize,desc) => await request.get(`/api/ordergetorders?pageIndex=${pageIndex}&pageSize=${pageSize}&desc=${desc}`);
const getOrderList = async (pageIndex,pageSize,desc) => await request.get(`/api/order/getorders?pageIndex=${pageIndex}&pageSize=${pageSize}&desc=${desc}`);
//获取个人订单列表
const getMyOrderList = async (pageIndex,pageSize,desc) => await request.get(`/api/ordergetmyorders?pageIndex=${pageIndex}&pageSize=${pageSize}&desc=${desc}`);
const getMyOrderList = async (pageIndex,pageSize,desc) => await request.get(`/api/order/getmyorders?pageIndex=${pageIndex}&pageSize=${pageSize}&desc=${desc}`);
//上传用户头像
const uploadAvatar = async (formData) => await request.post('/api/upload/UploadPic',formData,{
headers: {
'Content-Type': 'multipart/form-data',
},
})
//上传网站LOGO
const uploadLogo = async (formData) => await request.post('/api/upload/UploadLogo',formData,{
headers: {
'Content-Type': 'multipart/form-data',
},
})
//上传网站LOGO
const uploadFavicon = async (formData) => await request.post('/api/upload/UploadFavicon',formData,{
headers: {
'Content-Type': 'multipart/form-data',
},
})
//更新用户信息
const updateMyInfo = async (params) => await request.post('api/user/update',params)
//购买套餐
const buyPackage = async (params) => await request.post('api/pay/buy',params)
//获取订单数量
const getOrderNum = async () => await request.get('api/order/GetOrderNum')
//获取订单数量
const getMyOrderNum = async () => await request.get('api/order/GetMyOrderNum')
//重置apiKey
const setApiKey = async () => await request.post('api/user/setapikey')
//获取所有支付配置
const getAllPayment = async () => await request.get('api/payment/getAllPayment')
//获取所有支付配置
const getAllPublicPayment = async () => await request.get('api/payment/getAllPublicPayment')
//更新支付配置
const updatePayment = async (params) => await request.post('api/payment/updatePayment',params)
//获取公开api列表
const getPublicApiList = async (pageIndex,pageSize,desc) => await request.get(`/api/apis/getapispublic?pageIndex=${pageIndex}&pageSize=${pageSize}&desc=${desc}`)
//获取已订购套餐
const getUserPackages = async () => await request.get('/api/user/GetUserPackages')
//获取API对应套餐列表
const getApiPackages = async (ids) => await request.get('/api/apis/getapipackages',{
params: {
apiId: ids // Axios 会自动转换成 ?apiId=1&apiId=2&apiId=3
}
})
//获取系统统计信息
const getSystemInfo = async () => await request.get('/api/systeminfo/getinfo')
//设置api套餐
const setApiPackage = async (params) => await request.post('/api/package/setapipackageitem',params)
export default {
login,
register,
@ -117,5 +181,21 @@ export default {
addPackage,
getPackageInfoById,
getOrderList,
getMyOrderList
getMyOrderList,
uploadAvatar,
updateMyInfo,
uploadLogo,
uploadFavicon,
buyPackage,
getOrderNum,
getMyOrderNum,
setApiKey,
getAllPayment,
updatePayment,
getUserPackages,
getPublicApiList,
getApiPackages,
getAllPublicPayment,
getSystemInfo,
setApiPackage
}

View File

@ -4,7 +4,7 @@ import {attachAccessToken,authRequestError} from '@/utils/intercaptors/request/a
import { handlerefresherror } from "@/utils/intercaptors/response/refresh"
const request = axios.create({
baseURL:'http://192.168.5.100:2088',
baseURL:'http://localhost:5292',
timeout:10000,
// ✅ 只把 status >= 500 视为错误401/402/403 等都当作正常返回
validateStatus: function (status) {

View File

@ -17,18 +17,12 @@ const routes = [
redirect: '/home'
},
{
path: '/auth',
component: Login,
children: [
{
path: 'login',
component: Login
},
{
path: 'register',
component: Register
}
]
path: '/auth/login',
component: Login
},
{
path: '/auth/register',
component: Register
},
{
path: '/paynotice',
@ -44,7 +38,7 @@ const routes = [
path: 'index',
component: () => import('@/views/layout/Dashboard.vue'),
meta: {
title: '主页',
title: '控制台',
icon: 'home',
showInMenu: true,
showInUser: false,
@ -84,6 +78,17 @@ const routes = [
isHome: false
}
},
{
path: 'apilist',
component: () => import('@/views/layout/APIList.vue'),
meta: {
title: 'API列表',
icon: 'grid',
showInMenu: true,
showInUser: true,
isHome: false
}
},
{
path: 'buypackage',
component: () => import('@/views/layout/BuyPackage.vue'),
@ -95,6 +100,17 @@ const routes = [
isHome: false
}
},
{
path: 'mypackages',
component: () => import('@/views/layout/MyPackages.vue'),
meta: {
title: '已购套餐',
icon: 'shopping-bag',
showInMenu: true,
showInUser: true,
isHome: false
}
},
{
path: 'orders',
component: () => import('@/views/layout/Orders.vue'),
@ -102,7 +118,7 @@ const routes = [
title: '订单管理',
icon: 'shopping-cart',
showInMenu: true,
showInUser: false,
showInUser: true,
isHome: false
}
},
@ -117,6 +133,17 @@ const routes = [
isHome: false
}
},
{
path: 'paymentconfig',
component: () => import('@/views/layout/PayConfig.vue'),
meta: {
title: '支付配置',
icon: 'sliders',
showInMenu: true,
showInUser:false,
isHome:false
}
},
{
path: 'system',
component: () => import('@/views/layout/SystemConfig.vue'),

View File

@ -47,7 +47,8 @@ const getters = {
* @param {*} state
* @returns
*/
isAdmin:state => state.user.roles.some(x => x.role == 'Admin')
isAdmin:state => state.user.roles.some(x => x.role == 'Admin'),
Avatar:state => state.user.avatar
}

View File

@ -1,65 +1,116 @@
<template>
<div class="home-page">
<!-- 顶部导航栏 -->
<header class="navbar">
<div class="logo">API计费系统</div>
<nav class="nav-buttons">
<button @click="goToLogin">登录</button>
<button @click="goToRegister">注册</button>
</nav>
<div class="container nav-inner">
<div class="logo">API计费系统</div>
<nav class="nav-buttons">
<button class="btn btn-outline" @click="goToLogin">登录</button>
<button class="btn btn-primary" @click="goToRegister">注册</button>
</nav>
</div>
</header>
<!-- 主内容区 -->
<section class="hero">
<h1>轻松管理您的 API 使用和费用</h1>
<p>我们的系统让您以简单透明的方式追踪每一笔调用</p>
</section>
<main>
<section class="hero">
<div class="container hero-inner">
<div class="hero-text">
<h1>用更少的成本掌控更多的调用</h1>
<p class="lead">实时计费与可视化报表清晰明了的 API 使用与费用管理帮助你把注意力放回业务</p>
<!-- 套餐介绍 -->
<section class="plans">
<h2>套餐介绍</h2>
<div class="plan-list">
<div class="plan">
<h3>基础版</h3>
<p>适合初学者</p>
<ul>
<li>1000 /</li>
<li>限速10 /分钟</li>
<li>免费</li>
</ul>
<div class="cta-row">
<button class="btn btn-primary large" @click="goToRegister">立即开始 免费试用</button>
<button class="btn btn-ghost" @click="goToLogin">查看演示</button>
</div>
<ul class="kpis" aria-hidden="true">
<li><strong>100k+</strong><span>/ 监控</span></li>
<li><strong>99.99%</strong><span>可用性 SLA</span></li>
<li><strong>企业级</strong><span>安全与权限</span></li>
</ul>
</div>
<div class="hero-visual" aria-hidden="true">
<!-- 简洁安全不透明的占位插画 -->
<svg viewBox="0 0 480 320" xmlns="http://www.w3.org/2000/svg" role="img" aria-hidden="true">
<rect x="0" y="0" width="480" height="320" rx="12" fill="#ffffff"/>
<g transform="translate(24,24)" fill="none" stroke="#e6f0ff" stroke-width="10" stroke-linecap="round">
<path d="M0 64h200" opacity="0.95"/>
<path d="M0 120h160" opacity="0.75"/>
<path d="M0 176h220" opacity="0.6"/>
</g>
<circle cx="380" cy="80" r="44" fill="#eef9ff"/>
<circle cx="340" cy="220" r="72" fill="#f7f0ff"/>
</svg>
</div>
</div>
<div class="plan">
<h3>专业版</h3>
<p>适合中小企业</p>
<ul>
<li>10 万次/</li>
<li>限速100 /分钟</li>
<li>¥49/</li>
</ul>
</section>
<section class="plans">
<div class="container">
<h2>套餐介绍</h2>
<div class="plan-list">
<article class="plan">
<div class="plan-head">
<h3>基础版</h3>
<span class="tag">入门</span>
</div>
<p class="desc">适合个人开发者</p>
<ul class="features">
<li>1000 /</li>
<li>限速10 /分钟</li>
</ul>
<div class="price">免费</div>
<button class="btn btn-outline">开始使用</button>
</article>
<article class="plan featured">
<div class="plan-head">
<h3>专业版</h3>
<span class="tag pro">推荐</span>
</div>
<p class="desc">适合成长型团队</p>
<ul class="features">
<li>10 万次/</li>
<li>限速100 /分钟</li>
<li>报表与告警</li>
</ul>
<div class="price">¥49 / </div>
<button class="btn btn-primary">选择专业版</button>
</article>
<article class="plan">
<div class="plan-head">
<h3>企业版</h3>
<span class="tag">商务</span>
</div>
<p class="desc">为大型团队与 SLA 定制</p>
<ul class="features">
<li>不限调用</li>
<li>专属支持 & SLA</li>
</ul>
<div class="price">¥199 / </div>
<button class="btn btn-outline">联系我们</button>
</article>
</div>
</div>
<div class="plan">
<h3>企业版</h3>
<p>无限使用</p>
<ul>
<li>不限调用</li>
<li>专属支持</li>
<li>¥199/</li>
</ul>
</section>
<section class="about">
<div class="container">
<h2>关于我们</h2>
<p>我们是一支专注于 API 管理与计费的技术团队提供高性能低成本且可扩展的方案帮助开发者聚焦产品价值</p>
</div>
</section>
</main>
<footer class="footer">
<div class="container foot-inner">
<div>© 2025 API计费系统</div>
<div class="foot-links">
<a href="#" @click.prevent>隐私</a>
<a href="#" @click.prevent>服务条款</a>
</div>
</div>
</section>
<!-- 关于我们 -->
<section class="about">
<h2>关于我们</h2>
<p>
我们是一支专注于API管理与计费的技术团队致力于提供高性能低成本的解决方案帮助开发者专注于业务本身
</p>
</section>
<!-- 页脚 -->
<footer class="footer">
<p>© 2025 API计费系统. 保留所有权利</p>
</footer>
</div>
</template>
@ -68,103 +119,221 @@
export default {
name: "HomeView",
methods: {
goToLogin() {
this.$router.push("/auth/login");
},
goToRegister() {
this.$router.push("/auth/register");
}
goToLogin() { this.$router.push("/auth/login"); },
goToRegister() { this.$router.push("/auth/register"); }
}
};
</script>
<style scoped>
/* 明确变量(局部使用) */
.home-page {
font-family: "Helvetica Neue", sans-serif;
color: #333;
line-height: 1.6;
--bg: #f4f7fb;
--card: #ffffff;
--text: #0f1720;
--muted: #6b7280;
--primary: #0b75ff;
--primary-dark: #0857c6;
--shadow: rgba(15,23,32,0.06);
background: var(--bg);
color: var(--text);
font-family: Inter, "Helvetica Neue", Arial, sans-serif;
min-height: 100vh;
-webkit-font-smoothing:antialiased;
-moz-osx-font-smoothing:grayscale;
}
/* container */
.container {
max-width: 1100px;
margin: 0 auto;
padding: 0 20px;
}
/* navbar */
.navbar {
display: flex;
justify-content: space-between;
padding: 20px 40px;
background: #2c3e50;
color: #fff;
position: relative;
z-index: 5;
border-bottom: 1px solid rgba(15,23,32,0.04);
background: transparent;
}
.nav-inner {
display:flex;
justify-content:space-between;
align-items:center;
padding: 16px 0;
}
.logo {
font-size: 24px;
font-weight: bold;
font-weight:700;
font-size:18px;
color:var(--text);
}
.nav-buttons button {
margin-left: 10px;
padding: 8px 16px;
background: #3498db;
border: none;
color: white;
border-radius: 4px;
cursor: pointer;
/* buttons */
.nav-buttons { display:flex; gap:12px; align-items:center; }
.btn {
padding: 8px 14px;
border-radius: 10px;
font-weight:600;
cursor:pointer;
border:none;
display:inline-flex;
align-items:center;
gap:8px;
transition: transform .12s ease, box-shadow .12s ease;
color:var(--text);
background:transparent;
}
.nav-buttons button:hover {
background: #2980b9;
.btn:active { transform: translateY(1px); }
.btn-primary {
background: linear-gradient(90deg,var(--primary),var(--primary-dark));
color:#fff;
box-shadow: 0 8px 20px rgba(11,117,255,0.12);
}
.btn-outline {
background: var(--card);
border: 1px solid rgba(15,23,32,0.06);
box-shadow: 0 8px 16px var(--shadow);
}
.btn-ghost {
background: transparent;
color: var(--primary-dark);
border: 1px dashed rgba(11,117,255,0.12);
}
/* hero */
.hero {
text-align: center;
padding: 80px 20px;
background: #f4f6f8;
padding: 56px 0 40px;
}
.hero-inner {
display:flex;
gap:28px;
align-items:center;
}
.hero-text {
flex:1;
max-width:640px;
}
.hero h1 {
font-size: 36px;
margin-bottom: 20px;
font-size:34px;
margin:0 0 10px;
line-height:1.05;
color:var(--text);
/* 不使用大面积模糊阴影 */
text-shadow: none;
}
.plans {
padding: 60px 20px;
background: #fff;
text-align: center;
.lead { color:var(--muted); margin:0 0 18px; font-size:16px; }
.cta-row { display:flex; gap:12px; align-items:center; margin-bottom:16px; }
.btn.large { padding:12px 20px; border-radius:12px; font-size:15px; }
/* KPI row */
.kpis { display:flex; gap:18px; margin-top:12px; list-style:none; padding:0; color:var(--muted); font-size:13px; }
.kpis li strong { display:block; color:var(--text); font-size:15px; }
/* hero-visual */
.hero-visual {
width:440px;
height:260px;
border-radius:12px;
background: var(--card);
box-shadow: 0 18px 40px rgba(15,23,32,0.06);
display:flex;
align-items:center;
justify-content:center;
border:1px solid rgba(15,23,32,0.04);
}
/* plans */
.plans { padding:44px 0; }
.plans h2 {
text-align:center;
font-size:22px;
margin-bottom:24px;
color:var(--text);
}
.plan-list {
display: flex;
justify-content: center;
gap: 20px;
flex-wrap: wrap;
margin-top: 30px;
display:flex;
gap:18px;
justify-content:center;
flex-wrap:wrap;
}
.plan {
background: #ecf0f1;
padding: 20px;
border-radius: 12px;
width: 250px;
box-shadow: 0 2px 8px rgba(0,0,0,0.05);
width:300px;
background:var(--card);
border-radius:12px;
padding:18px;
box-shadow: 0 10px 30px var(--shadow);
border:1px solid rgba(15,23,32,0.03);
display:flex;
flex-direction:column;
gap:10px;
transition: transform .18s ease, box-shadow .18s ease;
}
.plan h3 {
margin-bottom: 10px;
.plan:hover {
transform: translateY(-6px);
box-shadow: 0 20px 48px rgba(15,23,32,0.08);
}
.plan ul {
list-style: none;
padding: 0;
text-align: left;
.plan-head { display:flex; justify-content:space-between; align-items:center; }
.plan h3 { margin:0; font-size:18px; color:var(--text); }
.tag {
font-size:12px;
color:var(--muted);
background:#f7fafc;
padding:6px 8px;
border-radius:8px;
border:1px solid rgba(15,23,32,0.03);
}
.about {
padding: 60px 20px;
background: #f9f9f9;
text-align: center;
.tag.pro {
background: linear-gradient(90deg, rgba(11,117,255,0.12), rgba(126,231,255,0.06));
color:var(--primary-dark);
font-weight:700;
}
.footer {
text-align: center;
padding: 20px;
background: #2c3e50;
color: white;
.desc { color:var(--muted); margin:0; font-size:14px; }
.features { list-style:none; padding:0; margin:8px 0; color:var(--muted); font-size:14px; }
.price { font-weight:800; font-size:18px; color:var(--text); margin-top:auto; }
/* about & footer */
.about { padding:36px 0; text-align:center; color:var(--muted); }
.footer { padding:18px 0; border-top:1px solid rgba(15,23,32,0.04); color:var(--muted); }
.foot-inner { display:flex; justify-content:space-between; align-items:center; gap:12px; max-width:1100px; margin:0 auto; }
/* small screens */
@media (max-width:1000px) {
.hero-inner { flex-direction:column-reverse; align-items:flex-start; gap:18px; }
.hero-visual { width:100%; height:180px; border-radius:10px; }
.plan-list { gap:14px; }
}
@media (max-width:520px) {
.container { padding:0 14px; }
.hero h1 { font-size:22px; }
.cta-row { flex-direction:column; align-items:stretch; }
.btn.large { width:100%; }
.plan { width:100%; }
.foot-inner { flex-direction:column; text-align:center; gap:10px; }
}
</style>

View File

@ -32,12 +32,15 @@
<label class="form-check-label" for="exampleCheck1">记住我</label>
</div>
<div class="d-grid">
<button type="button" class="btn btn-info m-b-xs" @click="login">登录</button>
<button type="button" class="btn btn-info m-b-xs" @click="login" :disabled="isLogining">
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true" v-if="isLogining"></span>
{{isLogining ? 'Loading...' : '登录'}}
</button>
<button class="btn btn-primary">三方登录</button>
</div>
</form>
<div class="authent-reg">
<p>没有账户<router-link to="register">点击创建</router-link></p>
<p>没有账户<router-link to="/auth/register">点击创建</router-link></p>
</div>
</div>
</div>
@ -57,7 +60,8 @@ export default {
formdata: {
username: '',
password: ''
}
},
isLogining:false
}
},
methods: {
@ -65,6 +69,7 @@ export default {
* 用户登录
*/
async login() {
this.isLogining = true
//MD5
const passwordOld = this.formdata.password
this.formdata.password = md5(passwordOld)
@ -94,10 +99,11 @@ export default {
} catch (e) {
console.log(e)
this.$alert('未知错误', 'error')
this.$alert('服务不可用', 'danger')
} finally {
//
this.formdata.password = passwordOld
this.isLogining = false
}
}
},

View File

@ -0,0 +1,276 @@
<template>
<div class="main-wrapper py-5" style="background: #f1f3f5;">
<div class="container">
<!-- 外层卡片淡色背景与白色小卡片形成对比 -->
<div class="card p-4 mb-4">
<div class="row mb-3 align-items-center">
<div class="col-md-6">
<h4 class="mb-0">API 列表</h4>
<div class="small text-muted">下面显示公开的 API 及其使用示例与套餐关联</div>
</div>
<div class="col-md-6 text-right">
<input
type="text"
v-model="search"
class="form-control d-inline-block w-auto"
placeholder="搜索 API 名称或描述"
/>
</div>
</div>
<div class="row">
<div
v-for="api in filteredApis"
:key="api.id"
class="col-12 col-md-6 col-lg-4 mb-4"
>
<!-- 小卡片白底轻边框轻阴影 -->
<div class="card api-card h-100">
<div class="card-body d-flex flex-column">
<div class="d-flex justify-content-between align-items-start mb-3">
<h5 class="card-title mb-0">{{ api.name }}</h5>
<span :class="['badge','badge-pill', 'method-badge', 'method-' + api.method.toLowerCase()]">
{{ api.method }}
</span>
</div>
<p class="card-text text-muted mb-2" style="flex-grow:0.2;">
{{ api.description }}
</p>
<div class="endpoint mb-3 px-2 py-1 bg-light rounded small endpoint-box">
<code>{{ baseUrl + '/api/public' + api.endpoint }}</code>
</div>
<!-- 套餐如果存在显示为 tag 列表白卡底上用浅色 tag-->
<div v-if="api.packages && api.packages.length" class="mb-3">
<p class="mb-1"><strong>对应套餐</strong></p>
<div>
<span
v-for="(pkg, idx) in api.packages"
:key="idx"
class="pkg-badge mr-2"
:title="pkg"
>
{{ pkg }}
</span>
</div>
</div>
<!-- 详细示例保持紧凑 -->
<div class="mt-auto">
<p class="mb-1 font-weight-bold">示例详情</p>
<pre class="code-block mb-2"><code>{{ api.example }}</code></pre>
<div v-if="api.requestExample">
<p class="mb-1"><strong>请求参数</strong></p>
<pre class="code-block small"><code>{{ api.requestExample.request }}</code></pre>
<p class="mb-1 mt-3"><strong>响应参数</strong></p>
<pre class="code-block small"><code>{{ api.requestExample.response }}</code></pre>
<p class="mb-1 mt-3"><strong>响应体格式</strong></p>
<pre class="code-block small"><code>{{ api.requestExample.responseType }}</code></pre>
</div>
</div>
</div>
</div>
</div>
<div v-if="filteredApis.length === 0" class="col-12 text-center text-muted">
未找到匹配的 API
</div>
</div>
<!-- 加载提示保持你原有逻辑v-if 控制 -->
<div class="text-center mt-3" v-if="!isLoaded">
<card-loading />
</div>
</div>
</div>
</div>
</template>
<script>
import axiosIntance from '@/request/request'
import CardLoading from '../../components/CardLoading.vue';
export default {
components: { CardLoading },
name: 'ApiList',
data() {
return {
search: '',
pageIndex: 1,
pageSize: 100,
desc: false,
apiList: [],
baseUrl: '',
isLoaded: false
}
},
computed: {
filteredApis() {
const term = this.search.trim().toLowerCase();
return term
? this.apiList.filter(api =>
api.name.toLowerCase().includes(term) ||
api.description.toLowerCase().includes(term)
)
: this.apiList;
}
},
methods: {
async loadApiList() {
this.isLoaded = false
try {
const res = await this.$api.getPublicApiList(
this.pageIndex,
this.pageSize,
this.desc
);
if (res.code === 1000) {
this.apiList = res.data.map(item => {
const ex = item.apiRequestExamples && item.apiRequestExamples[0];
return {
id: item.id,
name: item.name,
description: item.description,
endpoint: item.endpoint.startsWith('/') ? item.endpoint : `/${item.endpoint}`,
method: this.mapMethod(item.method),
badgeClass: this.mapBadge(item.method),
example: this.buildExample(item),
requestExample: ex ? {
request: ex.request,
response: ex.response,
responseType: ex.responseType
} : null,
packages: [] // getApiPackages
};
});
} else {
this.$alert(res.message, 'danger');
}
} catch (e) {
this.$alert('加载 API 列表失败', 'danger');
console.error(e);
} finally {
this.isLoaded = true
}
},
mapMethod(code) {
switch(code) {
case 1: return 'GET';
case 2: return 'POST';
case 3: return 'PUT';
case 4: return 'DELETE';
default: return 'GET';
}
},
mapBadge(code) {
switch(code) {
case 1: return 'info';
case 2: return 'success';
case 3: return 'warning';
case 4: return 'danger';
default: return 'secondary';
}
},
buildExample(item) {
const method = this.mapMethod(item.method);
const url = item.endpoint;
if (method === 'GET') {
return `axios.get('${url}')\n .then(res => console.log(res.data));`;
} else {
return `axios.${method.toLowerCase()}('${url}', { /* 参数 */ })\n .then(res => console.log(res.data));`;
}
}
},
created() {
this.loadApiList();
this.baseUrl = axiosIntance.defaults.baseURL
}
}
</script>
<style scoped>
/* 颜色变量,便于快速调整 */
:root {
--outer-bg: #f6fbff; /* 外层卡的淡色背景 */
--card-shadow: rgba(16,24,40,0.06);
--card-border: rgba(16,24,40,0.06);
--pkg-bg: #eef6ff;
}
/* 外层容器卡(与白色小卡形成对比) */
.card {
background: var(--outer-bg);
border-radius: 10px;
padding: 18px;
box-shadow: 0 1px 0 rgba(0,0,0,0.02);
}
/* 小卡片样式:白底、边框、轻阴影 */
.api-card {
background: #fff;
border: 1px solid var(--card-border);
border-radius: 8px;
box-shadow: 0 2px 6px var(--card-shadow);
transition: transform .18s ease, box-shadow .18s ease;
}
.api-card:hover {
transform: translateY(-6px);
box-shadow: 0 8px 20px rgba(16,24,40,0.08);
}
/* 方法 badge更柔和的配色 */
.method-badge {
padding: 6px 8px;
font-weight: 600;
color: #fff;
font-size: 0.78rem;
border-radius: 999px;
}
.method-get { background:#17a2b8; } /* GET */
.method-post { background:#28a745; } /* POST */
.method-put { background:#ffc107; color:#212529; } /* PUT */
.method-delete { background:#dc3545; } /* DELETE */
/* endpoint 区块 */
.endpoint-box {
background: #fafbfd;
border: 1px dashed rgba(0,0,0,0.03);
color: #333;
}
/* 套餐 tag 样式 */
.pkg-badge {
display:inline-block;
background: var(--pkg-bg);
color: #0366d6;
padding: 4px 8px;
border-radius: 999px;
font-size: 0.85rem;
border: 1px solid rgba(3,102,214,0.06);
}
/* code block 样式(保持高对比)*/
.code-block {
background: #1f2937; /* 深色更适合代码示例 */
color: #e6eef8;
padding: 0.65rem;
border-radius: 6px;
font-size: 0.82rem;
line-height: 1.45;
overflow-x: auto;
max-height: 160px;
margin-bottom: 0.5rem;
}
/* 细微调整 */
.card-title { font-size: 1.05rem; font-weight: 600; }
.endpoint code { font-size: 0.92rem; }
.small { font-size: 0.85rem; }
</style>

View File

@ -2,7 +2,8 @@
<div class="main-wrapper">
<div class="row">
<div class="col" v-if="isLoaded">
<div class="col">
<div class="card">
<DataTable title="API管理"
:headers="tableHeaders"
:rows="tableData"
@ -10,6 +11,8 @@
@pageChanged="pageChangedHandle"
@dataDelete="deleteHandle"
@dataModify="modifyHandle" />
<card-loading v-if="!isLoaded"/>
</div>
<api-form-modal title="修改接口" ref="apiModal" />
</div>
</div>
@ -19,13 +22,15 @@
<script>
import DataTable from '@/components/DataTable.vue';
import ApiFormModal from '../../components/ApiFormModal.vue';
import CardLoading from '../../components/CardLoading.vue';
export default {
name: "APIs",
components: {
DataTable,
ApiFormModal
ApiFormModal,
CardLoading
},
data() {
return {
@ -46,6 +51,7 @@ export default {
},
methods: {
async loadApiList(pageIndex = 1, pageSize = 10, desc = false) {
this.isLoaded = false
try {
const res = await this.$api.getApiList(pageIndex, pageSize, desc)
if (res.code == '1000') {
@ -54,7 +60,10 @@ export default {
} catch (e) {
this.$alert('API列表数据加载失败!', 'danger')
console.error(e)
}finally{
this.isLoaded = true
}
},
async pageChangedHandle(newval) {
await this.loadApiList(newval.pageIndex, newval.pageSize, false)
@ -63,6 +72,7 @@ export default {
try {
//API
const apiinfoRes = await this.$api.getApiInfoById(id)
if (apiinfoRes.code != 1000) {
this.$alert(apiinfoRes.message, 'danger')
return
@ -73,6 +83,7 @@ export default {
//null
if (res == null) return
const modifyRes = await this.$api.updateApiInfo(id, res)
const packageRes = await this.setApiPackage({packageId:res.packageId,apiId:id})
if (modifyRes.code != 1000) {
this.$alert(modifyRes.message, 'danger')
return
@ -110,13 +121,17 @@ export default {
this.$alert('加载套餐列表失败!', 'danger')
console.error(e)
}
}
},
async setApiPackage(params){
return await this.$api.setApiPackage(params)
}
},
async mounted() {
await this.loadApiList()
await this.loadPackageList()
await this.loadApiList()
this.tableHeaders.find(x => x.value == 'packageId').findValue = this.packageList
this.isLoaded = true
}
}
</script>

View File

@ -1,273 +1,390 @@
<template>
<div class="main-wrapper">
<div class="row justify-content-center">
<div class="card custom-card">
<div class="card-header text-center">
<h3 class="card-title">充值中心</h3>
</div>
<div class="card-body">
<div class="row mb-4">
<div class="col text-start">
<div class="balance-box">
<h5 class="balance-text">
<i class="fas fa-wallet balance-icon"></i>
剩余余额<span class="balance-amount">{{ userInfo.balance }}</span>
</h5>
<div class="main-wrapper py-5" style="background: #f5f7fa;">
<div class="container">
<div class="row justify-content-center">
<div class="col-12 col-md-8 col-lg-6">
<div class="card custom-card">
<!-- 卡头 -->
<div class="card-header text-center card-header-gradient">
<h3 class="card-title mb-0">充值中心</h3>
<p class="card-subtitle mb-0">为账户充值以调用付费 API</p>
</div>
</div>
</div>
<div class="mb-4">
<label class="form-label" for="formGroupExampleInput">充值金额</label>
<div class="input-group">
<input
type="number"
class="form-control custom-input"
id="formGroupExampleInput"
placeholder="请输入充值金额"
v-model="formattedRechargeAmount"
/>
<div class="input-group-text custom-input-text"></div>
</div>
</div>
<div class="mb-4">
<p class="payment-title">充值方式</p>
<div class="container">
<div class="row d-flex justify-content-between">
<!-- 单选按钮 1 -->
<div
class="col-6 d-flex justify-content-center mb-3 option-box"
:class="{ 'selected-box': selectedpay === 0 }"
>
<input
type="radio"
id="option1"
name="options"
class="hidden-radio"
:value="0"
v-model="selectedpay"
/>
<label for="option1" class="label-style">
<img src="../../assets/images/Alipay.png" alt="支付宝" />
</label>
<!-- 卡体 -->
<div class="card-body">
<!-- 余额展示 -->
<div class="balance-box mb-4">
<div class="balance-left">
<i class="fas fa-wallet balance-icon" aria-hidden="true"></i>
</div>
<div class="balance-right">
<div class="small text-muted">剩余余额</div>
<div class="balance-amount">{{ userInfo.balance }} <span class="currency"></span></div>
</div>
</div>
<!-- 单选按钮 2 -->
<div
class="col-6 d-flex justify-content-center mb-3 option-box"
:class="{ 'selected-box': selectedpay === 1 }"
>
<input
type="radio"
id="option2"
name="options"
class="hidden-radio"
:value="1"
v-model="selectedpay"
/>
<label for="option2" class="label-style">
<img src="../../assets/images/wechat.png" alt="微信支付" />
</label>
<!-- 充值金额 -->
<div class="mb-4">
<label class="form-label font-weight-bold">充值金额</label>
<div class="input-group custom-input-group">
<input type="text" inputmode="decimal" class="form-control custom-input" placeholder="请输入充值金额"
v-model="formattedRechargeAmount" />
<span class="input-group-text custom-input-text"></span>
</div>
<small class="form-text text-muted mt-2">支持支付宝微信等常用支付方式输入金额后点击立即充值</small>
</div>
<!-- 支付方式 -->
<div class="mb-4">
<p class="font-weight-bold mb-2 section-title">选择支付方式</p>
<div class="d-flex flex-wrap payment-options">
<div v-for="item in payOptions" :key="item.id" class="option-box"
:class="{ 'selected-box': selectedpay === item.id }" @click="selectedpay = item.id" role="button"
:aria-pressed="selectedpay === item.id">
<div class="option-inner">
<img :src="item.img" :alt="item.name" class="option-img" />
<div class="option-name">{{ item.name }}</div>
</div>
<span v-if="selectedpay === item" class="check-badge"><i class="fa fa-check"></i></span>
</div>
</div>
</div>
</div>
<!-- 卡尾 -->
<div class="card-footer bg-white border-0 text-center">
<button class="btn custom-btn w-100" @click="deposit">
立即充值
</button>
</div>
</div>
</div>
<div class="text-center">
<button type="button" class="btn custom-btn" @click="deposit">立刻充值</button>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import alipayIcon from '@/assets/images/Alipay.png';
import wechatIcon from '@/assets/images/wechat.png';
import qqIcon from '@/assets/images/qq.png'
import axiosIntance from '@/request/request'
export default {
name: "Balance",
data() {
return {
userInfo:{
balance:0
},
userInfo: { balance: 0 },
rechargeAmount: 0,
selectedpay: 0,
payMap: ['支付宝', '微信支付', '银行卡支付', 'QQ'],
payIconMap: [alipayIcon,wechatIcon,qqIcon,qqIcon],
payOptions: [
//{ name: "", img: require("@/assets/images/Alipay.png") },
//{ name: "", img: require("@/assets/images/wechat.png") }
]
};
},
computed: {
formattedRechargeAmount: {
get() {
if (typeof this.rechargeAmount !== 'number') return '0.00';
return this.rechargeAmount
.toFixed(2)
.replace(/\B(?=(\d{3})+(?!\d))/g, ',');
},
set(val) {
//
const num = parseFloat(val.replace(/,/g, ''));
if (!isNaN(num)) {
this.rechargeAmount = num;
}
}
}
},
methods: {
async getUserInfo(){
try{
const res = await this.$api.getUserInfo();
if(res.code != 1000){
this.$alert('加载失败','danger')
return
}
this.userInfo = res.data
}catch(e){
this.$alert('加载失败','danger')
console.error(e)
}
},
async deposit(){
const returnUrl = `${window.location.protocol}//${window.location.host}/paynotice`;
try{
const res = await this.$api.createPayment(this.selectedpay,this.rechargeAmount,returnUrl);
if(!res.success){
this.$alert('跳转支付失败','danger')
return
}
window.location.href = res.payUrl
}catch(e){
this.$alert('跳转支付失败','danger')
console.error(e)
get() {
const num = Number(this.rechargeAmount) || 0;
return num
.toFixed(2)
.replace(/\B(?=(\d{3})+(?!\d))/g, ",");
},
set(val) {
const num = parseFloat(String(val).replace(/,/g, ""));
if (!isNaN(num)) this.rechargeAmount = num;
}
}
},
async created(){
await this.getUserInfo()
methods: {
async getUserInfo() {
try {
const res = await this.$api.getUserInfo();
if (res.code === 1000) this.userInfo = res.data;
else this.$alert("加载失败", "danger");
} catch {
this.$alert("加载失败", "danger");
}
},
async deposit() {
if (this.rechargeAmount <= 0) {
this.$alert('充值金额必须大于0', 'warning')
return
}
const returnUrl = `${location.origin}/paynotice`;
try {
const res = await this.$api.createPayment(
this.selectedpay,
this.rechargeAmount,
returnUrl
);
if (res.success) window.location.href = res.payUrl;
else this.$alert("跳转支付失败", "danger");
} catch {
this.$alert("跳转支付失败", "danger");
}
},
async getAllPayments() {
try {
const res = await this.$api.getAllPublicPayment();
if (res.code != 1000) {
this.$alert(res.message, 'danger')
return
}
//this.payments = res.data
this.setPayoptions(res.data);
} catch (e) {
this.$alert('加载支付方式列表失败', 'danger')
console.error(e)
}
},
setPayoptions(payments) {
this.payOptions = payments.map(item => ({
id: item.id,
name: this.payMap[item.method],
img: this.payIconMap[item.method]
}));
}
},
async created() {
await this.getUserInfo();
await this.getAllPayments();
}
}
};
</script>
<style scoped>
input[type="radio"].hidden-radio {
display: none; /* 隐藏原生单选按钮 */
}
/* 禁用数字输入框的调节按钮 */
input[type="number"]::-webkit-inner-spin-button,
input[type="number"]::-webkit-outer-spin-button {
-webkit-appearance: none;
margin: 0;
/* 基准色彩,可按需调整 */
:root {
--brand-1: #5b6cff;
--brand-2: #6dd3ff;
--muted: #8b94a6;
--card-bg: #ffffff;
--surface: #f5f7fa;
}
input[type="number"] {
-moz-appearance: textfield; /* 针对 Firefox */
/* 容器 */
.main-wrapper {
background: var(--surface);
min-height: 60vh;
font-family: 'Segoe UI', Roboto, "Helvetica Neue", Arial, "Noto Sans CJK SC", sans-serif;
}
/* 余额盒子样式 */
/* 卡片 */
.custom-card {
border: none;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 6px 18px rgba(39, 44, 52, 0.06);
background: linear-gradient(180deg, #ffffff 0%, #ffffff 100%);
}
/* 渐变卡头 */
.card-header-gradient {
background: linear-gradient(90deg, rgba(91, 108, 255, 1) 0%, rgba(109, 211, 255, 0.9) 100%);
color: #fff;
padding: 20px 18px;
}
.card-header-gradient .card-title {
font-weight: 700;
font-size: 1.25rem;
}
.card-header-gradient .card-subtitle {
opacity: 0.95;
font-size: 0.85rem;
margin-top: 6px;
color: rgba(255, 255, 255, 0.9);
}
/* 卡体的内边距更大 */
.card-body {
padding: 20px;
}
/* 余额盒子 */
.balance-box {
background-color: #f0f8ff; /* 柔和的背景色 */
border: 1px solid #d1e7ff; /* 边框颜色 */
border-radius: 10px; /* 圆角 */
padding: 15px; /* 内边距 */
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); /* 阴影 */
display: flex;
align-items: center;
}
/* 余额文本样式 */
.balance-text {
font-size: 1.2rem;
color: #333;
margin: 0;
}
img {
width: 100%;
height: 100%;
object-fit: contain;
cursor: pointer;
}
/* 余额数字样式 */
.balance-amount {
font-weight: bold;
color: #007bff;
}
/* 图标样式 */
.balance-icon {
color: #007bff;
margin-right: 8px;
font-size: 1.5rem;
}
/* 卡片样式 */
.custom-card {
border-radius: 15px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
padding: 20px;
max-width: 600px;
background-color: #ffffff;
}
/* 标题样式 */
.card-title {
font-size: 1.8rem;
font-weight: bold;
color: #333;
}
/* 余额样式 */
.balance-text {
font-size: 1.2rem;
color: #666;
}
.balance-amount {
font-weight: bold;
color: #007bff;
}
/* 输入框样式 */
.custom-input {
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.custom-input-text {
background-color: #f8f9fa;
border-radius: 0 8px 8px 0;
}
/* 支付方式样式 */
.option-box {
padding: 10px;
border: 2px solid transparent;
gap: 14px;
padding: 14px;
background: linear-gradient(180deg, #fff 0%, #fbfdff 100%);
border-radius: 10px;
transition: all 0.3s ease;
border: 1px solid rgba(16, 24, 40, 0.04);
box-shadow: 0 2px 8px rgba(16, 24, 40, 0.03) inset;
}
.option-box.selected-box {
border-color: #007bff;
background-color: #e9f5ff;
.balance-left {
width: 56px;
height: 56px;
background: linear-gradient(135deg, rgba(91, 108, 255, 0.12), rgba(109, 211, 255, 0.08));
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
}
.label-style {
width: 100px;
height: 100px;
display: inline-block;
cursor: pointer;
.balance-icon {
font-size: 22px;
color: var(--brand-1);
}
.balance-right .small {
color: var(--muted);
}
.balance-amount {
font-size: 1.4rem;
font-weight: 700;
color: #0b2b53;
margin-top: 2px;
}
.currency {
font-size: 0.85rem;
color: var(--muted);
margin-left: 6px;
}
/* 输入组 */
.custom-input-group {
display: flex;
align-items: center;
border-radius: 10px;
overflow: hidden;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
border: 1px solid rgba(16, 24, 40, 0.06);
background: #fff;
}
/* 按钮样式 */
.custom-btn {
background: linear-gradient(45deg, #007bff, #0056b3);
color: #fff;
.custom-input {
border: none;
border-radius: 8px;
padding: 10px 20px;
font-size: 1rem;
font-weight: bold;
transition: 0.3s ease;
}
.custom-btn:hover {
background: linear-gradient(45deg, #0056b3, #003f7f);
padding: 14px;
font-size: 1.05rem;
outline: none;
box-shadow: none;
}
/* 全局字体 */
body {
font-family: "Arial", sans-serif;
background-color: #f5f5f5;
.custom-input:focus {
outline: none;
box-shadow: none;
}
.custom-input-text {
background: #fbfdff;
padding: 10px 14px;
border-left: 1px solid rgba(16, 24, 40, 0.03);
color: var(--muted);
}
/* 支付方式选项 */
.payment-options {
gap: 12px;
}
.option-box {
flex: 1 1 48%;
display: flex;
align-items: center;
justify-content: space-between;
position: relative;
padding: 12px;
background: #fff;
border-radius: 10px;
border: 1px solid rgba(16, 24, 40, 0.04);
transition: transform .16s ease, box-shadow .16s ease, border-color .16s ease, background .16s ease;
cursor: pointer;
min-height: 96px;
margin-bottom: 6px;
}
.option-box+.option-box {
margin-left: 8px;
}
.option-box .option-inner {
display: flex;
align-items: center;
gap: 12px;
}
.option-img {
width: 56px;
height: 56px;
object-fit: contain;
}
.option-name {
font-weight: 600;
color: #17233b;
}
/* 选中态 */
.option-box.selected-box {
border-color: rgba(91, 108, 255, 0.55);
background: linear-gradient(180deg, rgba(91, 108, 255, 0.04), rgba(109, 211, 255, 0.02));
box-shadow: 0 6px 18px rgba(91, 108, 255, 0.06);
transform: translateY(-4px);
}
/* 勾选徽章 */
.check-badge {
position: absolute;
top: 10px;
right: 10px;
background: var(--brand-1);
color: #fff;
width: 26px;
height: 26px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
font-size: 12px;
box-shadow: 0 6px 14px rgba(91, 108, 255, 0.14);
}
/* 底部按钮 */
.custom-btn {
background: linear-gradient(90deg, #5b6cff, #2bb7ff);
color: #fff;
border-radius: 10px;
padding: 12px 16px;
font-size: 1rem;
font-weight: 700;
border: none;
transition: transform .12s ease, box-shadow .12s ease;
}
.custom-btn:hover {
transform: translateY(-2px);
box-shadow: 0 8px 24px rgba(43, 127, 255, 0.14);
}
/* small screens */
@media (max-width: 576px) {
.option-box {
flex-basis: 100%;
}
.option-box+.option-box {
margin-left: 0;
}
.custom-card {
margin: 0 12px;
}
.card-header-gradient {
padding: 16px;
}
.balance-amount {
font-size: 1.2rem;
}
}
</style>

View File

@ -1,64 +1,197 @@
<template>
<div class="main-wrapper py-5" style="background: #f8f9fa;">
<div class="container">
<div class="row justify-content-center">
<div class="main-wrapper">
<div class="row">
<div class="col">
<div class="card">
<div class="card-body">
<div class="row">
<div class="col m-b-sm" v-for="(item,index) in packageList" :key="index">
<ul class="list-group io-pricing-table">
<li class="list-group-item">
<h3>{{ item.name }}</h3>
</li>
<li class="list-group-item">每分钟调用次数限制{{ item.oneMinuteLimit }} </li>
<li class="list-group-item">周期总调用次数{{ item.callLimit }} </li>
<li class="list-group-item">
<h3>{{ item.price }}</h3>
<span>每月</span>
</li>
<li class="list-group-item">
<button type="button" class="btn btn-primary">购买</button>
</li>
</ul>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div
v-for="(item, index) in packageList"
:key="item.id || index"
class="col-12 col-md-6 col-lg-4 mb-4"
>
<div class="card package-card h-100">
<!-- 卡片头 -->
<div class="card-header text-center bg-primary text-white">
<h5 class="mb-0">{{ item.name }}</h5>
</div>
<div class="card-body d-flex flex-column">
<ul class="list-unstyled flex-grow-1 mb-4">
<!-- 关联 API放在信息列表里 -->
<li class="mb-3 api-list">
<i class="fas fa-plug text-primary mr-2"></i>
<span class="api-label">关联 API</span>
<template v-if="item.apiPackageItems && item.apiPackageItems.length">
<span
v-for="(api, idx) in item.apiPackageItems"
:key="idx"
class="api-pill"
:title="(api && api.api && api.api.name) ? api.api.name : '未知接口'"
>
{{ (api && api.api && api.api.name) ? api.api.name : '未知接口' }}
</span>
</template>
<span v-else class="text-muted small">暂无关联</span>
</li>
<li class="mb-3">
<i class="fas fa-tachometer-alt text-primary mr-2"></i>
每分钟限制<strong>{{ item.oneMinuteLimit }} </strong>
</li>
<li class="mb-3">
<i class="fas fa-sync-alt text-primary mr-2"></i>
周期总调用<strong>{{ item.callLimit }} </strong>
</li>
<li class="mb-3">
<i class="fas fa-tags text-primary mr-2"></i>
价格<strong>¥{{ item.price }}/</strong>
</li>
</ul>
<button
type="button"
class="btn btn-outline-primary btn-block mt-auto"
@click="onBuyPackageHandle(item.id, item.name, item.price)"
>
立即购买
</button>
</div>
</div>
</div>
<div v-if="packageList.length === 0" class="col-12 text-center text-muted">
正在加载套餐...
</div>
</div>
</div>
</div>
</template>
<script>
export default {
name:'BuyPackage',
data(){
return {
packageList: []
}
},
methods: {
//
async loadPackageList(){
try{
const res = await this.$api.getPackageList(1,100,false)
if(res.code != 1000){
this.$alert(res.message,'danger')
return
}
this.packageList = res.data
return
}catch(e){
this.$alert('加载套餐列表失败!','danger')
console.error('套餐列表加载失败:',e)
}
}
},
async mounted(){
await this.loadPackageList()
name: 'BuyPackage',
data() {
return {
packageList: []
}
},
methods: {
//
async loadPackageList() {
try {
const res = await this.$api.getPackageList(1, 100, false);
if (res.code != 1000) {
this.$alert(res.message || '加载套餐列表失败', 'danger');
return;
}
this.packageList = res.data || [];
} catch (e) {
this.$alert('加载套餐列表失败!', 'danger');
console.error('套餐列表加载失败:', e);
}
},
//
async onBuyPackageHandle(id, name, price) {
try {
const confirmRes = await this.$confirm('购买', `是否花费${price}元订购一个月${name}`);
if (!confirmRes) return;
const res = await this.$api.buyPackage({ apiPackageId: id });
if (res.code != 1000) {
this.$alert(res.message, 'danger');
return;
}
this.$alert(res.message || '购买成功', 'success');
} catch (e) {
this.$alert('购买套餐失败', 'danger');
console.error(e);
}
}
},
async mounted() {
await this.loadPackageList();
}
}
</script>
<style scoped>
.package-card {
border: none;
border-radius: 8px;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.08);
transition: transform 0.18s ease, box-shadow 0.18s ease;
overflow: hidden;
}
.package-card:hover {
transform: translateY(-6px);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
}
</script>
.card-header {
border-bottom: none;
padding: 18px 16px;
}
/* 信息列表内的 API 部分 */
.api-list {
display: flex;
align-items: center;
gap: 10px;
flex-wrap: wrap;
}
/* 前缀文本 */
.api-label {
font-weight: 500;
margin-right: 6px;
color: #333;
}
/* pill 标签 */
.api-pill {
display: inline-block;
font-size: 0.82rem;
padding: 6px 10px;
border-radius: 999px;
background: rgba(0, 123, 255, 0.08);
color: #0056b3;
border: 1px solid rgba(0, 86, 179, 0.08);
max-width: 160px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
line-height: 1;
cursor: default;
}
/* 悬停轻微高亮 */
.api-pill:hover {
filter: brightness(0.96);
}
/* 列表条目通用样式 */
.list-unstyled li {
display: flex;
align-items: center;
font-size: 0.95rem;
color: #555;
}
.list-unstyled i {
width: 20px;
}
/* 按钮全宽 */
.btn-block {
width: 100%;
}
/* 小屏设备时调整 pill 大小 */
@media (max-width: 575.98px) {
.api-pill {
max-width: 120px;
font-size: 0.72rem;
padding: 5px 8px;
}
}
</style>

View File

@ -1,12 +1,13 @@
<template>
<div class="main-wrapper">
<div class="row">
<div class="col-md-6 col-xl-3">
<div class="card stat-widget">
<div class="card-body">
<h5 class="card-title">New Customers</h5>
<h2>132</h2>
<p>From last week</p>
<div class="card-body" v-if="isLoaded">
<h5 class="card-title">订单</h5>
<h2>{{ systemInfo.orderCount }}</h2>
<p>系统订单总数</p>
<div class="progress">
<div class="progress-bar bg-info progress-bar-striped" role="progressbar" style="width: 25%" aria-valuenow="25" aria-valuemin="0" aria-valuemax="100"></div>
</div>
@ -15,10 +16,10 @@
</div>
<div class="col-md-6 col-xl-3">
<div class="card stat-widget">
<div class="card-body">
<h5 class="card-title">Orders</h5>
<h2>287</h2>
<p>Orders in waitlist</p>
<div class="card-body" v-if="isLoaded">
<h5 class="card-title">用户</h5>
<h2>{{ systemInfo.userCount }}</h2>
<p>系统用户数</p>
<div class="progress">
<div class="progress-bar bg-success progress-bar-striped" role="progressbar" style="width: 50%" aria-valuenow="50" aria-valuemin="0" aria-valuemax="100"></div>
</div>
@ -27,10 +28,10 @@
</div>
<div class="col-md-6 col-xl-3">
<div class="card stat-widget">
<div class="card-body">
<h5 class="card-title">Monthly Profit</h5>
<h2>7.4K</h2>
<p>For last 30 days</p>
<div class="card-body" v-if="isLoaded">
<h5 class="card-title">API调用</h5>
<h2>{{ systemInfo.callCount }}</h2>
<p>API调用总次数</p>
<div class="progress">
<div class="progress-bar bg-danger progress-bar-striped" role="progressbar" style="width: 60%" aria-valuenow="60" aria-valuemin="0" aria-valuemax="100"></div>
</div>
@ -39,10 +40,10 @@
</div>
<div class="col-md-6 col-xl-3">
<div class="card stat-widget">
<div class="card-body">
<h5 class="card-title">Orders</h5>
<h2>87</h2>
<p>Orders in waitlist</p>
<div class="card-body" v-if="isLoaded">
<h5 class="card-title">金额</h5>
<h2>{{ systemInfo.money }}</h2>
<p>统计金额</p>
<div class="progress">
<div class="progress-bar bg-primary progress-bar-striped" role="progressbar" style="width: 50%" aria-valuenow="50" aria-valuemin="0" aria-valuemax="100"></div>
</div>
@ -50,139 +51,35 @@
</div>
</div>
</div>
<div class="row">
<div class="col-sm-6 col-xl-8">
<div class="card">
<div class="card-body">
<h5 class="card-title">Revenue</h5>
<div id="apex1"></div>
</div>
</div>
</div>
<div class="col-sm-6 col-xl-4">
<div class="card stat-widget">
<div class="card-body">
<h5 class="card-title">Social Media</h5>
<div class="transactions-list">
<div class="tr-item">
<div class="tr-company-name">
<div class="tr-icon tr-card-icon tr-card-bg-primary text-primary">
<i data-feather="thumbs-up"></i>
</div>
<div class="tr-text">
<h4>New post reached 7k+ likes</h4>
<p>02 March</p>
</div>
</div>
</div>
</div>
<div class="transactions-list">
<div class="tr-item">
<div class="tr-company-name">
<div class="tr-icon tr-card-icon tr-card-bg-info text-info">
<i data-feather="twitch"></i>
</div>
<div class="tr-text">
<h4>Developer AMA is now live</h4>
<p>01 March</p>
</div>
</div>
</div>
</div>
<div class="transactions-list">
<div class="tr-item">
<div class="tr-company-name">
<div class="tr-icon tr-card-icon tr-card-bg-danger text-danger">
<i data-feather="instagram"></i>
</div>
<div class="tr-text">
<h4>52 unread messages</h4>
<p>23 February</p>
</div>
</div>
</div>
</div>
<div class="transactions-list">
<div class="tr-item">
<div class="tr-company-name">
<div class="tr-icon tr-card-icon tr-card-bg-warning text-warning">
<i data-feather="shopping-bag"></i>
</div>
<div class="tr-text">
<h4>2 new orders from shop page</h4>
<p>17 February</p>
</div>
</div>
</div>
</div>
<div class="transactions-list">
<div class="tr-item">
<div class="tr-company-name">
<div class="tr-icon tr-card-icon tr-card-bg-info text-info">
<i data-feather="twitter"></i>
</div>
<div class="tr-text">
<h4>Hashtag #circl is trending</h4>
<p>03 February</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-md-12 col-lg-8">
<div class="card table-widget">
<div class="card-body">
<h5 class="card-title">Recent Orders</h5>
<div class="card-body" v-if="isLoaded">
<h5 class="card-title">新增订单</h5>
<div class="table-responsive">
<table class="table">
<thead>
<tr>
<th scope="col">Customer</th>
<th scope="col">Product</th>
<th scope="col">Invoice</th>
<th scope="col">Price</th>
<th scope="col">Status</th>
<th scope="col">用户</th>
<th scope="col">类型</th>
<th scope="col">订单号</th>
<th scope="col">金额</th>
<th scope="col">状态</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row"><img src="../../assets/images/avatars/profile-image.png" alt="">Anna Doe</th>
<td>Modern</td>
<td>#53327</td>
<td>$20</td>
<td><span class="badge bg-info">Shipped</span></td>
</tr>
<tr>
<th scope="row"><img src="../../assets/images/avatars/profile-image.png" alt="">John Doe</th>
<td>Alpha</td>
<td>#13328</td>
<td>$25</td>
<td><span class="badge bg-success">Paid</span></td>
</tr>
<tr>
<th scope="row"><img src="../../assets/images/avatars/profile-image.png" alt="">Anna Doe</th>
<td>Lime</td>
<td>#35313</td>
<td>$20</td>
<td><span class="badge bg-danger">Pending</span></td>
</tr>
<tr>
<th scope="row"><img src="../../assets/images/avatars/profile-image.png" alt="">John Doe</th>
<td>Circl Admin</td>
<td>#73423</td>
<td>$23</td>
<td><span class="badge bg-primary">Shipped</span></td>
</tr>
<tr>
<th scope="row"><img src="../../assets/images/avatars/profile-image.png" alt="">Nina Doe</th>
<td>Space</td>
<td>#54773</td>
<td>$20</td>
<td><span class="badge bg-success">Paid</span></td>
<tr v-for="(item,index) in systemInfo.recentOrder" :key="index">
<th scope="row"><img src="../../assets/images/avatars/profile-image.png" alt="">{{ item.user.username }}</th>
<td>{{ orderTypeMap[item.orderType] }}</td>
<td>{{ item.orderNumber }}</td>
<td>{{ item.amount }}</td>
<td>
<span class="badge bg-info" v-if="item.status == 0">待支付</span>
<span class="badge bg-success" v-if="item.status == 1">已完成</span>
<span class="badge bg-primary" v-if="item.status == 2">已取消</span>
<span class="badge bg-danger" v-if="item.status == 3">已退款</span>
</td>
</tr>
</tbody>
</table>
@ -191,197 +88,64 @@
</div>
</div>
<div class="col-md-12 col-lg-4">
<div class="card stat-widget">
<div class="card-body">
<h5 class="card-title">Orders</h5>
<div id="apex2"></div>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-sm-12 col-md-4">
<div class="card stat-widget">
<div class="card-body">
<h5 class="card-title">Tasks Overview</h5>
<div class="transactions-list">
<div class="card-body" v-if="isLoaded">
<h5 class="card-title">新增用户</h5>
<div class="transactions-list" v-for="(item,index) in systemInfo.recentUser" :key="index">
<div class="tr-item">
<div class="tr-company-name">
<div class="tr-icon tr-card-icon tr-card-bg-primary text-primary">
<i data-feather="user"></i>
</div>
<div class="tr-text">
<a href="#"><h4>Project Managment</h4></a>
<p>Management</p>
<a href="#"><h4>{{ item.userName }}</h4></a>
<p>{{ item.email }}</p>
</div>
</div>
</div>
</div>
<div class="transactions-list">
<div class="tr-item">
<div class="tr-company-name">
<div class="tr-icon tr-card-icon tr-card-bg-info text-info">
<i data-feather="user"></i>
</div>
<div class="tr-text">
<a href="#"><h4>Design</h4></a>
<p>Creative</p>
</div>
</div>
</div>
</div>
<div class="transactions-list">
<div class="tr-item">
<div class="tr-company-name">
<div class="tr-icon tr-card-icon tr-card-bg-secondary">
<i data-feather="user"></i>
</div>
<div class="tr-text">
<a href="#"><h4>Financial Accounting</h4></a>
<p>Finance</p>
</div>
</div>
</div>
</div>
<div class="transactions-list">
<div class="tr-item">
<div class="tr-company-name">
<div class="tr-icon tr-card-icon tr-card-bg-primary text-primary">
<i data-feather="user"></i>
</div>
<div class="tr-text">
<a href="#"><h4>Testing</h4></a>
<p>Manager</p>
</div>
</div>
</div>
</div>
<div class="transactions-list">
<div class="tr-item">
<div class="tr-company-name">
<div class="tr-icon tr-card-icon tr-card-bg-secondary text-secondary">
<i data-feather="user"></i>
</div>
<div class="tr-text">
<a href="#"><h4>Development</h4></a>
<p>Developers</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-12 col-lg-4">
<div class="card">
<img src="../../assets/images/card-bg.png" class="card-img-top" alt="...">
<div class="card-body">
<div class="card-meet-header">
<div class="card-meet-day">
<h6>WED</h6>
<h3>7</h3>
</div>
<div class="card-meet-text">
<h6>Developer AMA</h6>
<p>Meet project developers</p>
</div>
</div>
<p class="card-text m-b-md">Lorem ipsum dolor sit amet, consectetur adipiscing elit</p>
<a href="#" class="btn btn-info">Join</a>
<a href="#" class="btn btn-primary">Invite</a>
</div>
</div>
</div>
<div class="col-sm-12 col-md-4">
<div class="card stat-widget">
<div class="card-body">
<h5 class="card-title">Transactions</h5>
<div class="transactions-list">
<div class="tr-item">
<div class="tr-company-name">
<div class="tr-icon tr-card-icon tr-card-bg-primary text-primary">
<i data-feather="thumbs-up"></i>
</div>
<div class="tr-text">
<h4>Facebook</h4>
<p>02 March</p>
</div>
</div>
<div class="tr-rate">
<p><span class="text-success">+ $24</span></p>
</div>
</div>
</div>
<div class="transactions-list">
<div class="tr-item">
<div class="tr-company-name">
<div class="tr-icon tr-card-icon tr-card-bg-success text-success">
<i data-feather="credit-card"></i>
</div>
<div class="tr-text">
<h4>Visa</h4>
<p>02 March</p>
</div>
</div>
<div class="tr-rate">
<p><span class="text-success">+ $300</span></p>
</div>
</div>
</div>
<div class="transactions-list">
<div class="tr-item">
<div class="tr-company-name">
<div class="tr-icon tr-card-icon tr-card-bg-danger text-danger">
<i data-feather="tv"></i>
</div>
<div class="tr-text">
<h4>Netflix</h4>
<p>02 March</p>
</div>
</div>
<div class="tr-rate">
<p><span class="text-danger">- $17</span></p>
</div>
</div>
</div>
<div class="transactions-list">
<div class="tr-item">
<div class="tr-company-name">
<div class="tr-icon tr-card-icon tr-card-bg-warning text-warning">
<i data-feather="shopping-cart"></i>
</div>
<div class="tr-text">
<h4>Themeforest</h4>
<p>02 March</p>
</div>
</div>
<div class="tr-rate">
<p><span class="text-danger">- $220</span></p>
</div>
</div>
</div>
<div class="transactions-list">
<div class="tr-item">
<div class="tr-company-name">
<div class="tr-icon tr-card-icon tr-card-bg-info text-info">
<i data-feather="dollar-sign"></i>
</div>
<div class="tr-text">
<h4>PayPal</h4>
<p>02 March</p>
</div>
</div>
<div class="tr-rate">
<p><span class="text-success">+20%</span></p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<card-loading v-if="!isLoaded"/>
</div>
</template>
</template>
<script>
import CardLoading from '../../components/CardLoading.vue'
import feather from 'feather-icons'
export default {
components: { CardLoading },
name:'Dashboard',
data(){
return {
systemInfo:null,
isLoaded:false,
orderTypeMap:['购买','充值','退款']
}
},
methods:{
async getSystemInfo(){
this.isLoaded = false
try{
const res = await this.$api.getSystemInfo()
if(res.code != 1000){
this.$alert(res.message,'danger')
return
}
this.systemInfo = res.data
}catch(e){
this.$alert('加载数据失败','danger');
}finally{
this.isLoaded = true
}
}
},
async created(){
await this.getSystemInfo()
feather.replace()
}
}
</script>

View File

@ -10,7 +10,7 @@
<li class="nav-item">
<router-link class="nav-link" to="/layout/index">主页</router-link>
</li>
<li class="nav-item">
<li class="nav-item" v-if="isAdmin">
<router-link class="nav-link" to="/layout/system">设置</router-link>
</li>
<li class="nav-item">
@ -114,8 +114,8 @@
</li>
<li class="nav-item dropdown">
<a class="nav-link profile-dropdown" href="#" id="profileDropDown" role="button"
data-bs-toggle="dropdown" aria-expanded="false"><img
src="../../assets/images/avatars/profile-image.png" alt=""></a>
data-bs-toggle="dropdown" aria-expanded="false"><img class="rounded-circle border"
:src="Avatar ? baseURL + Avatar : '../../assets/images/avatars/profile-image.png'" alt=""></a>
<div class="dropdown-menu dropdown-menu-end profile-drop-menu"
aria-labelledby="profileDropDown">
<router-link class="dropdown-item" to="/layout/center"><i data-feather="user"></i>个人中心</router-link>
@ -124,7 +124,7 @@
class="badge rounded-pill bg-success">12</span></a>
<a class="dropdown-item" href="#"><i data-feather="check-circle"></i>任务</a>
<div class="dropdown-divider"></div>
<router-link class="dropdown-item" to="/layout/system"><i data-feather="settings"></i>系统设置</router-link>
<router-link class="dropdown-item" to="/layout/system" v-if="isAdmin"><i data-feather="settings"></i>系统设置</router-link>
<a class="dropdown-item" href="#" @click.prevent="unlock"><i data-feather="unlock"></i>锁定</a>
<a class="dropdown-item" href="#" @click.prevent="logout"><i data-feather="log-out"></i>注销</a>
</div>
@ -134,7 +134,7 @@
</nav>
</div>
<div class="page-sidebar">
<ul class="list-unstyled accordion-menu">
<ul class="list-unstyled accordion-menu scroll-box">
<li class="sidebar-title">
主页
</li>
@ -150,7 +150,7 @@
</ul>
</div>
<div class="page-content">
<router-view/>
<router-view :key="componentKey" @refresh="refreshHandle"/>
</div>
</div>
@ -161,13 +161,20 @@
import auth from '@/utils/auth'
import feather from 'feather-icons'
import { mapGetters } from 'vuex'
import axiosIntance from '@/request/request'
import { Avatar } from 'ant-design-vue'
export default {
data(){
return{
routeMenu: []
routeMenu: [],
baseURL: null,
componentKey:0
}
},
methods:{
refreshHandle(){
this.componentKey += 1
},
/**
* 展开/关闭侧边菜单栏
*/
@ -197,9 +204,10 @@ export default {
}
},
computed:{
...mapGetters('userinfo',['isAdmin'])
...mapGetters('userinfo',['isAdmin','Avatar'])
},
mounted(){
this.baseURL = axiosIntance.defaults.baseURL
feather.replace()
},
beforeCreate(){
@ -209,6 +217,19 @@ export default {
}
</script>
<style>
<style scoped>
.rounded-circle {
width: 40px;
height: 40px;
}
.scroll-box{
height: 300px; /* 或 max-height, 或 calc(...) */
overflow-y: auto; /* 超出时显示滚动(有内容时才出现) */
overflow-x: hidden; /* 横向不要滚动时禁止 */
padding: 12px;
background: #fff;
border-radius: 6px;
border: 1px solid #eee;
}
</style>

View File

@ -0,0 +1,117 @@
<template>
<div class="main-wrapper py-5" style="background: #f1f3f5;">
<div class="container">
<!-- 标题与搜索 -->
<div class="row mb-4 align-items-center">
<div class="col-md-6">
<h4 class="mb-0">已订购套餐</h4>
</div>
<div class="col-md-6 text-right">
<input
type="text"
v-model="search"
class="form-control d-inline-block w-auto"
placeholder="搜索套餐名称"
/>
</div>
</div>
<!-- 卡片列表 -->
<div class="row">
<div
v-for="item in filteredPackages"
:key="item.id"
class="col-12 col-md-6 col-lg-4 mb-4"
>
<div class="card subscription-card h-100">
<div class="card-body d-flex flex-column">
<h5 class="card-title mb-2">{{ item.package.name }}</h5>
<p class="text-muted mb-2">订购时间{{ formatDate(item.purchasedAt) }}</p>
<p class="text-muted mb-3">到期时间{{ formatDate(item.expiryDate) }}</p>
<ul class="list-unstyled mb-3">
<li>每分钟限额<strong>{{ item.package.oneMinuteLimit }} </strong></li>
<li>周期总调用<strong>{{ item.package.callLimit }} </strong></li>
<li>剩余调用<strong>{{ item.remainingCalls }} </strong></li>
<!--<li>已用调用<strong>{{ item.package.callLimit - item.remainingCalls }} </strong></li>-->
</ul>
<div class="mt-auto d-flex justify-content-between align-items-center">
<span class="price">¥{{ item.package.price }}<small class="text-muted">/</small></span>
<button class="btn btn-outline-primary btn-sm" @click="viewDetails(item.package.id)">详情</button>
</div>
</div>
</div>
</div>
<div v-if="filteredPackages.length === 0" class="col-12 text-center text-muted">
未找到匹配的套餐
</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'SubscribedPackages',
data() {
return {
search: '',
packages: []
}
},
computed: {
filteredPackages() {
const term = this.search.trim().toLowerCase();
if (!term) return this.packages;
return this.packages.filter(item =>
item.package.name.toLowerCase().includes(term)
);
}
},
methods: {
async fetchPackages() {
try {
const res = await this.$api.getUserPackages();
if (res.code !== 1000) {
this.$alert(res.message || '加载已购套餐失败', 'danger');
return;
}
this.packages = res.data;
} catch (err) {
this.$alert('加载已购套餐失败', 'danger');
console.error('获取已订购套餐出错:', err);
}
},
formatDate(dateStr) {
if (!dateStr) return '';
const d = new Date(dateStr);
const y = d.getFullYear();
const m = String(d.getMonth() + 1).padStart(2, '0');
const dd = String(d.getDate()).padStart(2, '0');
return `${y}-${m}-${dd}`;
},
viewDetails(packageId) {
this.$router.push({ name: 'PackageDetail', params: { id: packageId } });
}
},
async created() {
await this.fetchPackages();
}
}
</script>
<style scoped>
.subscription-card {
border: none;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
transition: transform 0.2s;
}
.subscription-card:hover {
transform: translateY(-4px);
}
.price {
font-size: 1.25rem;
font-weight: bold;
color: #007bff;
}
</style>

View File

@ -2,9 +2,12 @@
<div class="main-wrapper">
<div class="row">
<div class="col" v-if="isLoaded">
<order-data-table title="订单管理" :headers="tableHeaders" :rows="tableData" :data-count="dataCount"
<div class="col" >
<div class="card">
<order-data-table title="订单管理" :headers="tableHeaders" :rows="tableData" :data-count="dataCount" @pageChanged="pageChangedHandle"
/>
<card-loading v-if="!isLoaded"/>
</div>
</div>
</div>
</div>
@ -12,10 +15,13 @@
<script>
import OrderDataTable from '@/components/OrderDataTable.vue';
import { mapGetters } from 'vuex'
import CardLoading from '../../components/CardLoading.vue';
export default {
components: {
OrderDataTable
OrderDataTable,
CardLoading
},
data() {
return {
@ -23,7 +29,7 @@ export default {
isLoaded: false,
tableHeaders: [
{ text: "编号", value: "id", width: "155px" },
{ text: "用户", value: "userId", width: "214px" },
{ text: "用户", value: "user", width: "214px",type:"obj",property:"username"},
{ text: "订单号", value: "orderNumber", width: "48px" },
{ text: "商户订单号", value: "thirdPartyOrderId", width: "82px" },
{ text: "金额", value: "amount", width: "103px", type: "money"},
@ -35,25 +41,51 @@ export default {
};
},
methods: {
async loadOrderList(pageIndex = 1, pageSize = 10, desc = false) {
async loadOrderList(pageIndex = 1, pageSize = 10, desc = true) {
this.isLoaded = false
try {
const res = await this.$api.getOrderList(pageIndex, pageSize, desc)
if (res.code == '1000') {
this.tableData = res.data
let res = null
if(this.isAdmin) res = await this.$api.getOrderList(pageIndex, pageSize, desc)
else res = await this.$api.getMyOrderList(pageIndex, pageSize, desc)
if (res.code != 1000) {
this.$alert(res.message, 'danger')
return;
}
this.tableData = res.data
} catch (e) {
this.$alert('订单列表数据加载失败!', 'danger')
console.error(e)
}finally{
this.isLoaded = true
}
},
async loadOrderCount(){
let res = null
try{
if(this.isAdmin) res = await this.$api.getOrderNum();
else res = await this.$api.getMyOrderNum();
if (res.code != 1000) {
this.$alert(res.message, 'danger')
return;
}
this.dataCount = res.data
} catch (e) {
this.$alert('订单列表数据加载失败!', 'danger')
console.error(e)
}
},
async pageChangedHandle(newval) {
await this.loadUserList(newval.pageIndex, newval.pageSize, false)
await this.loadOrderList(newval.pageIndex, newval.pageSize, true)
}
},
computed: {
...mapGetters('userinfo',['isAdmin'])
},
async mounted() {
await this.loadOrderList()
this.dataCount = 100
this.isLoaded = true
await this.loadOrderCount()
}
};
</script>

View File

@ -2,10 +2,14 @@
<div class="main-wrapper">
<div class="row">
<div class="col" v-if="isLoaded">
<div class="col">
<div class="card">
<DataTable title="套餐管理" :headers="tableHeaders" :rows="tableData" :data-count="dataCount"
@pageChanged="pageChangedHandle" @dataDelete="deleteHandle" @dataModify="modifyHandle" @dataAdd="addHandle"
/>
<card-loading v-if="!isLoaded"/>
</div>
<package-form-modal title="修改套餐" ref="userModal" />
<package-form-modal title="添加套餐" formType="add" ref="addUserModal" />
</div>
@ -16,11 +20,13 @@
<script>
import DataTable from '@/components/DataTable.vue';
import PackageFormModal from '../../components/PackageFormModal.vue';
import CardLoading from '../../components/CardLoading.vue';
export default {
components: {
DataTable,
PackageFormModal
PackageFormModal,
CardLoading
},
data() {
return {
@ -38,6 +44,7 @@ export default {
},
methods: {
async loadPackageList(pageIndex = 1, pageSize = 10, desc = false) {
this.isLoaded = false
try {
const res = await this.$api.getPackageList(pageIndex, pageSize, desc)
if (res.code == '1000') {
@ -46,6 +53,8 @@ export default {
} catch (e) {
this.$alert('套餐列表数据加载失败!', 'danger')
console.error(e)
}finally{
this.isLoaded = true
}
},
async pageChangedHandle(newval) {

View File

@ -0,0 +1,263 @@
<template>
<div class="main-wrapper">
<div class="container-fluid">
<div class="row justify-content-center">
<div class="col-12">
<div class="card shadow-lg">
<div
class="card-header bg-primary text-white d-flex justify-content-between align-items-center">
<h5 class="mb-0">支付设置</h5>
<button class="btn btn-outline-light btn-sm" @click="resetForm">
<i class="fa fa-sync-alt"></i> 重置
</button>
</div>
<div class="card-body">
<!-- 支付方式 -->
<div class="mb-5">
<label class="d-block font-weight-bold mb-3">支付方式</label>
<div class="d-flex flex-wrap">
<button v-for="method in paymentMethodsObj" :key="method.id"
class="btn btn-outline-primary mr-3 mb-2"
:class="{ active: form.method === method.name }"
@click="form.method = method.name; form.payType = ''">
{{ method.name }}
</button>
</div>
</div>
<!-- 接口类型 -->
<transition name="fade">
<div v-if="form.method" class="mb-5">
<label class="d-block font-weight-bold mb-3">接口类型</label>
<div class="d-flex flex-wrap">
<button v-for="type in interfaceTypesObj" :key="type.id"
class="btn btn-outline-success mr-3 mb-2"
:class="{ active: form.payType === type.name }"
@click="form.payType = type.name">
{{ type.name }}
</button>
</div>
</div>
</transition>
<!-- 详细配置 -->
<transition name="slide-fade">
<div v-if="form.method && form.payType" class="payment-form">
<div class="row">
<!-- 是否启用 -->
<div class="col-12 mb-4">
<label class="form-label">是否启用</label>
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" id="enabled"
v-model="form.isEnabled">
<label class="form-check-label" for="enabled">
{{ form.isEnabled ? '已启用' : '已停用' }}
</label>
</div>
</div>
<div class="col-md-6 mb-4">
<label for="appId" class="form-label">App ID</label>
<input type="text" id="appId" class="form-control" v-model="form.appId"
placeholder="请输入 App ID" />
</div>
<div class="col-md-6 mb-4">
<label for="secretKey" class="form-label">密钥 Secret Key</label>
<input type="text" id="secretKey" class="form-control"
v-model="form.secretKey" placeholder="请输入 Secret Key" />
</div>
<div class="col-md-6 mb-4" v-if="form.method !== '支付宝'">
<label for="mchId" class="form-label">商户号Mch ID</label>
<input type="text" id="mchId" class="form-control" v-model="form.mchId"
placeholder="请输入 商户号" />
</div>
<div class="col-md-6 mb-4">
<label for="publicKey" class="form-label">API 公钥</label>
<div class="input-group">
<input type="text" id="publicKey" class="form-control"
v-model="form.publicKey" placeholder="请输入 API 公钥" />
<div class="input-group-append">
<button class="btn btn-outline-secondary" @click="copyApiKey"
type="button" title="复制">
<i class="fa fa-copy"></i>
</button>
<button class="btn btn-outline-danger" @click="regenerateApiKey"
type="button" title="重置">
<i class="fa fa-redo"></i>
</button>
</div>
</div>
</div>
<!-- 第四方支付特有 网站地址 配置 -->
<div class="col-12 mb-4" v-if="form.payType === '易支付'">
<label for="url" class="form-label">支付网关地址</label>
<input type="text" id="url" class="form-control" v-model="form.url"
placeholder="请输入第四方支付网关地址" />
</div>
</div>
</div>
</transition>
<!-- 操作按钮 -->
<div class="mt-4 text-right">
<button class="btn btn-success" @click="saveSettings">
<i class="fa fa-save"></i> 保存设置
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import axiosIntance from '@/request/request'
export default {
name: 'PaymentSettings',
data() {
return {
paymentMethodsObj: [
{ id: 0, name: '支付宝' },
{ id: 1, name: '微信' },
{ id: 3, name: 'QQ' }
],
interfaceTypesObj: [
{ id: 1, name: '官方' },
{ id: 2, name: '易支付' }
],
form: {
method: '',
payType: '',
appId: '',
secretKey: '',
mchId: '',
publicKey: '',
url: '',
isEnabled: false
},
payments: [],
componentKey: 0
}
},
methods: {
resetForm() {
this.form = {
method: '', payType: '', appId: '', secretKey: '', mchId: '', publicKey: '', url: ''
}
},
async saveSettings() {
if (!this.form.method || !this.form.payType) {
this.$alert('请选择支付方式和接口类型', 'danger')
return
}
// TODO: form
//this.$emit('refresh')
try {
let data = JSON.parse(JSON.stringify(this.form));
data.payType = this.interfaceTypesObj.find(x => x.name == this.form.payType).id
data.method = this.paymentMethodsObj.find(x => x.name == this.form.method).id
data.notifyUrl = axiosIntance.defaults.baseURL + '/api/pay/notice'
const res = await this.$api.updatePayment(data)
if(res.code != 1000){
this.$alert(res.message,'danger')
return;
}
this.$alert('支付设置已保存', 'success')
await this.loadPayments()
} catch (e) {
this.$alert('支付配置保存失败', 'danger')
console.error(e)
}
},
copyApiKey() {
if (!this.form.publicKey) return
navigator.clipboard.writeText(this.form.publicKey).then(() => {
this.$alert('已复制 API 公钥', 'success')
})
},
regenerateApiKey() {
if (confirm('确定要重置 API 公钥?旧公钥将失效。')) {
// TODO:
//this.form.publicKey = 'API'
this.$alert('测试功能...', 'success')
}
},
async loadPayments() {
try {
const res = await this.$api.getAllPayment()
if (res.code != 1000) {
this.$alert(res.message, 'danger')
return;
}
this.payments = res.data
} catch (e) {
this.$alert('支付配置加载失败', 'danger')
console.error(e)
}
}
},
watch: {
'form.payType': {
handler(newVal) {
if(this.form.payType == '' || this.form.method == '') return;
const method = this.paymentMethodsObj.find(x => x.name == this.form.method)
const payType = this.interfaceTypesObj.find(x => x.name == this.form.payType)
const payment = this.payments.find(x => x.method == method.id && x.payType == payType.id)
if (payment) {
this.form.id = payment.id
this.form.appId = payment.appId
this.form.mchId = ''
this.form.publicKey = payment.publicKey
this.form.secretKey = payment.secretKey
this.form.url = payment.url
this.form.isEnabled = payment.isEnabled
} else {
this.form.appId = '';
this.form.secretKey = '';
this.form.mchId = '';
this.form.publicKey = '';
this.form.url = '';
this.form.isEnabled = false
}
},
immediate: false
}
},
async mounted() {
await this.loadPayments()
}
}
</script>
<style scoped>
.card {
max-width: 1200px;
margin: 0 auto;
}
.fade-enter-active,
.fade-leave-active {
transition: opacity .3s;
}
.fade-enter,
.fade-leave-to {
opacity: 0;
}
.slide-fade-enter-active {
transition: all .4s ease;
}
.slide-fade-enter,
.slide-fade-leave-to {
transform: translateY(-10px);
opacity: 0;
}
</style>

View File

@ -11,7 +11,7 @@
<!-- 用户头像 -->
<div class="text-center mb-4">
<img :src="user.avatar" class="rounded-circle border" width="100" height="100" />
<img :src="baseUrl + user.avatar" class="rounded-circle border" width="100" height="100" />
<div class="mt-2">
<button class="btn btn-outline-primary btn-sm" @click="editingSection = 'avatar'">修改头像</button>
</div>
@ -20,24 +20,34 @@
<!-- 用户信息列表 -->
<ul class="list-group list-group-flush">
<li class="list-group-item d-flex justify-content-between align-items-center">
<span><strong>昵称</strong>{{ user.nickname }}</span>
<button class="btn btn-link" @click="openEdit('nickname')">修改</button>
<span><strong>用户名</strong>{{ user.userName }}</span>
<button class="btn btn-link" @click="openEdit('userName')" disabled>修改</button>
</li>
<li class="list-group-item d-flex justify-content-between align-items-center">
<span><strong>邮箱</strong>{{ user.email }}</span>
<button class="btn btn-link" @click="openEdit('email')">修改</button>
<button class="btn btn-link" @click="openEdit('email')" disabled>修改</button>
</li>
<li class="list-group-item d-flex justify-content-between align-items-center">
<span><strong>手机号</strong>{{ user.phone }}</span>
<button class="btn btn-link" @click="openEdit('phone')">修改</button>
<span><strong>权限</strong>{{ (user.roles[1] ? '管理员' : '普通用户') || '无' }}</span>
<button class="btn btn-link" @click="openEdit('bio')" disabled>修改</button>
</li>
<li class="list-group-item d-flex justify-content-between align-items-center">
<span><strong>性别</strong>{{ getGenderLabel(user.gender) }}</span>
<button class="btn btn-link" @click="openEdit('gender')">修改</button>
</li>
<li class="list-group-item d-flex justify-content-between align-items-center">
<span><strong>个人简介</strong>{{ user.bio || '未填写' }}</span>
<button class="btn btn-link" @click="openEdit('bio')">修改</button>
<div class="flex-grow-1">
<strong>API Key</strong>
<span v-if="!showApiKey">************</span>
<span v-else>{{ user.apiKey ? user.apiKey : '首次请重置Key' }}</span>
</div>
<div class="btn-group">
<button class="btn btn-sm btn-outline-secondary" @click="toggleApiKey">
{{ showApiKey ? '隐藏' : '显示' }}
</button>
<button class="btn btn-sm btn-outline-primary" @click="copyApiKey">
复制
</button>
<button class="btn btn-sm btn-outline-danger" @click="regenerateApiKey">
重置
</button>
</div>
</li>
<li class="list-group-item d-flex justify-content-between align-items-center">
<span><strong>登录密码</strong>********</span>
@ -79,10 +89,10 @@
<textarea class="form-control" rows="3" v-model="form.bio" placeholder="简单介绍一下你自己"></textarea>
</div>
<div v-else-if="editingSection === 'password'">
<input type="password" class="form-control mb-2" v-model="form.oldPassword" placeholder="当前密码" />
<input type="password" class="form-control mb-2" v-model="form.newPassword" placeholder="新密码" />
<input type="password" class="form-control" v-model="form.confirmPassword" placeholder="确认新密码" />
<div class="text-danger mt-1" v-if="form.newPassword !== form.confirmPassword">两次密码不一致</div>
<div class="invalid-feedback" style="display: block;" v-if="form.newPassword !== form.confirmPassword">
两次密码不一致</div>
</div>
<div v-else-if="editingSection === 'avatar'">
<input type="file" class="form-control" @change="onAvatarChange" accept="image/*" />
@ -100,21 +110,27 @@
</template>
<script>
import axiosInstance from '@/request/request';
import md5 from 'blueimp-md5';
export default {
name: "UserProfilePanel",
data() {
return {
showApiKey:false,
baseUrl: '',
user: {
avatar: "https://via.placeholder.com/100",
nickname: "张三",
email: "zhangsan@example.com",
phone: "13800000000",
gender: "male",
bio: "热爱开源,热爱技术"
userName: '',
email: '',
roles: [],
avatar: '',
apiKey: null
},
editingSection: null,
form: {},
previewAvatar: null
previewAvatar: null,
loaded: false,
avatarFile: null
};
},
methods: {
@ -126,16 +142,16 @@ export default {
this.editingSection = null;
this.previewAvatar = null;
},
saveEdit() {
async saveEdit() {
if (this.editingSection === 'password') {
if (this.form.newPassword !== this.form.confirmPassword) {
return;
}
//
this.$alert && this.$alert("密码已修改", "success");
await this.updatePassword()
} else if (this.editingSection === 'avatar') {
if (this.previewAvatar) {
this.user.avatar = this.previewAvatar;
await this.saveAvatar()
}
} else {
Object.assign(this.user, this.form);
@ -143,16 +159,26 @@ export default {
this.closeEdit();
},
getGenderLabel(value) {
return value === 'male' ? '男' : value === 'female' ? '女' : '其他';
async saveAvatar() {
//
try {
const formData = new FormData();
formData.append('file', this.avatarFile);
const res = await this.$api.uploadAvatar(formData)
if (res.code != 1000) {
this.$alert(res.message, 'danger')
return
}
this.$alert('保存成功', 'success')
} catch (e) {
this.$alert('上传头像失败', 'danger')
console.error(e)
}
},
getSectionTitle(section) {
const map = {
nickname: "昵称",
userName: "用户名",
email: "邮箱",
phone: "手机号",
gender: "性别",
bio: "个人简介",
password: "密码",
avatar: "头像"
};
@ -166,7 +192,66 @@ export default {
this.previewAvatar = event.target.result;
};
reader.readAsDataURL(file);
this.avatarFile = file
},
async loadUserInfo() {
try {
const res = await this.$api.getUserInfo();
if (res.code != 1000) {
this.$alert(res.message, 'danger')
return
}
this.user = res.data
} catch (e) {
this.$alert('获取用户信息失败', 'danger')
console.error(e)
}
},
async updatePassword() {
try {
const res = await this.$api.updateMyInfo({ password: md5(this.form.newPassword) })
if (res.code != 1000) {
this.$alert(res.message, 'danger')
return
}
this.$alert('修改成功', 'success')
} catch (e) {
this.$alert('修改密码失败', 'danger')
console.error(e)
}
},
toggleApiKey(){
if(this.showApiKey){
this.showApiKey = false
}else{
this.showApiKey = true
}
},
async regenerateApiKey(){
try{
const res = await this.$api.setApiKey()
if(res.code != 1000){
this.$alert(res.message,'danger')
return
}
this.$alert('重置成功','success')
this.user.apiKey = res.data
}catch(e){
this.$alert('生成key失败','danger')
console.error(e)
}
},
copyApiKey(){
navigator.clipboard.writeText(this.user.apiKey).then(() => {
this.$alert('已复制到剪贴板','success');
})
}
},
async mounted() {
await this.loadUserInfo()
this.baseUrl = axiosInstance.defaults.baseURL
this.loaded = true
}
};
</script>
@ -175,9 +260,11 @@ export default {
.bg-gradient {
background: linear-gradient(to right, #4e73df, #1cc88a);
}
.modal {
z-index: 1050;
}
.btn-close {
background: none;
border: none;

View File

@ -1,80 +1,151 @@
<template>
<div class="main-wrapper">
<div class="row">
<div class="col">
<div class="card">
<div class="card-body">
<h5 class="card-title">系统设置</h5>
<p class="card-description">在此页面中配置系统的基本参数</p>
<form>
<!-- 系统名称 -->
<div class="mb-3">
<label for="systemName" class="form-label">系统名称</label>
<input type="text" class="form-control" id="systemName" v-model="systemName"
placeholder="请输入系统名称">
</div>
<!-- 管理员邮箱 -->
<div class="mb-3">
<label for="adminEmail" class="form-label">管理员邮箱</label>
<input type="email" v-model="systemEmail" class="form-control" id="adminEmail"
aria-describedby="emailHelp" placeholder="admin@example.com">
<div id="emailHelp" class="form-text">我们将在网站显示此邮箱</div>
</div>
<!-- 管理员手机号 -->
<div class="mb-3">
<label for="adminPhone" class="form-label">管理员手机号</label>
<input type="text" v-model="systemPhone" class="form-control" id="adminPhone"
aria-describedby="PhoneHelp" placeholder="13800000000">
<div id="PhoneHelp" class="form-text">我们将在网站显示此邮箱</div>
</div>
<!-- 系统描述 -->
<div class="mb-3">
<label for="adminDescription" class="form-label">系统描述</label>
<textarea class="form-control" v-model="systemDescription"
aria-label="With textarea"></textarea>
</div>
<!--网站LOGO-->
<div class="mb-3">
<label for="formFile" class="form-label">网站LOGO</label>
<input class="form-control" type="file" id="formFile" accept="image/*">
</div>
<!--网站Favorite-->
<div class="mb-3">
<label for="formFile" class="form-label">网站Favorite</label>
<input class="form-control" type="file" id="formFile" accept="image/*">
</div>
<!-- 启用用户注册 -->
<div class="mb-3 form-check">
<input type="checkbox" v-model="enableSystemRegister" class="form-check-input" id="enableRegistration">
<label class="form-check-label" for="enableRegistration">启用用户注册功能</label>
</div>
<!-- 启用注册邮箱验证 -->
<div class="mb-3 form-check">
<input type="checkbox" v-model="enableSystemRegisterEmailValidate" class="form-check-input" id="enableRegistrationValidate">
<label class="form-check-label" for="enableRegistrationValidate">启用用户注册邮箱验证</label>
</div>
<!-- 提交按钮 -->
<button type="button" class="btn btn-primary" @click="submit">保存设置</button>
</form>
</div>
</div>
<div class="main-wrapper">
<div class="container-fluid">
<div class="row justify-content-center">
<div class="col-12 col-md-10">
<div class="card shadow-sm">
<div class="card-header bg-info text-white d-flex justify-content-between align-items-center">
<div>
<h5 class="mb-0">系统设置</h5>
<p class="mb-0 small">在此页面中配置系统的基本参数</p>
</div>
<button class="btn btn-outline-light btn-sm" @click="submit">
<i class="fa fa-save"></i> 保存设置
</button>
</div>
</div>
</div>
</template>
<div class="card-body">
<form>
<div class="row">
<!-- 系统名称 -->
<div class="col-md-6 mb-4">
<label for="systemName" class="form-label font-weight-bold">系统名称</label>
<input
type="text"
class="form-control"
id="systemName"
v-model="systemName"
placeholder="请输入系统名称"
/>
</div>
<!-- 管理员邮箱 -->
<div class="col-md-6 mb-4">
<label for="adminEmail" class="form-label font-weight-bold">管理员邮箱</label>
<input
type="email"
class="form-control"
id="adminEmail"
v-model="systemEmail"
placeholder="admin@example.com"
/>
<small class="form-text text-muted">我们将在网站显示此邮箱</small>
</div>
<!-- 管理员手机号 -->
<div class="col-md-6 mb-4">
<label for="adminPhone" class="form-label font-weight-bold">管理员手机号</label>
<input
type="text"
class="form-control"
id="adminPhone"
v-model="systemPhone"
placeholder="13800000000"
/>
<small class="form-text text-muted">我们将在网站显示此手机号</small>
</div>
<!-- 系统描述 -->
<div class="col-md-6 mb-4">
<label for="adminDescription" class="form-label font-weight-bold">系统描述</label>
<textarea
class="form-control"
id="adminDescription"
rows="3"
v-model="systemDescription"
></textarea>
</div>
<!-- 网站LOGO -->
<div class="col-md-6 mb-4">
<label for="logoFile" class="form-label font-weight-bold">网站 LOGO</label>
<div class="d-flex align-items-center">
<input
class="form-control-file"
type="file"
id="logoFile"
accept="image/*"
@change="onLogoChange"
/>
<div v-if="logoPreview" class="ml-3">
<img :src="logoPreview" alt="logo" class="img-thumbnail" style="height:40px" />
</div>
</div>
</div>
<!-- 网站 Favorite -->
<div class="col-md-6 mb-4">
<label for="faviconFile" class="form-label font-weight-bold">网站 Favorite</label>
<div class="d-flex align-items-center">
<input
class="form-control-file"
type="file"
id="faviconFile"
accept="image/*"
@change="onFaviconChange"
/>
<div v-if="faviconPreview" class="ml-3">
<img :src="faviconPreview" alt="favicon" class="img-thumbnail" style="height:24px" />
</div>
</div>
</div>
<!-- 启用用户注册 -->
<div class="col-md-6 mb-3 form-check">
<input
type="checkbox"
class="form-check-input"
id="enableRegistration"
v-model="enableSystemRegister"
/>
<label class="form-check-label" for="enableRegistration">
启用用户注册功能
</label>
</div>
<!-- 启用注册邮箱验证 -->
<div class="col-md-6 mb-3 form-check">
<input
type="checkbox"
class="form-check-input"
id="enableRegistrationValidate"
v-model="enableSystemRegisterEmailValidate"
/>
<label class="form-check-label" for="enableRegistrationValidate">
启用注册邮箱验证
</label>
</div>
</div>
</form>
</div>
<div class="card-footer text-right">
<button class="btn btn-primary" @click="submit">
<i class="fa fa-check-circle"></i> 保存设置
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: "SystemConfig",
data() {
return {
systemConfig: []
systemConfig: [],
logoFile: null,
favicon: null
}
},
methods: {
@ -84,15 +155,49 @@ export default {
const configBody = item.configBody
try {
const res = await this.$api.updateSystemConfig(configName, configBody)
if(res.code != 1000) this.$alert(res.message,'danger')
if (res.code != 1000) this.$alert(res.message, 'danger')
} catch (err) {
this.$alert('更新配置失败','danger')
this.$alert('更新配置失败', 'danger')
console.error(`更新 ${configName} 失败`, err)
}
}
//
this.$alert('系统配置已保存','success')
this.$alert('系统配置已保存', 'success')
},
async onLogoChange(event) {
const file = event.target.files[0]; //
if (!file) return;
let formdata = new FormData()
formdata.append('file',file)
try{
const res = await this.$api.uploadLogo(formdata)
if(res.code != 1000){
this.$alert(res.message,'danger')
return
}
this.$alert('上传LOGO成功','success')
}catch(e){
this.$alert('上传LOGO失败','danger')
console.error(e)
}
},
async onFaviconChange(event) {
const file = event.target.files[0]; //
if (!file) return;
let formdata = new FormData()
formdata.append('file',file)
try{
const res = await this.$api.uploadFavicon(formdata)
if(res.code != 1000){
this.$alert(res.message,'danger')
return
}
this.$alert('上传Favicon成功','success')
}catch(e){
this.$alert('上传Favicon失败','danger')
console.error(e)
}
}
},
computed: {
@ -144,12 +249,12 @@ export default {
}
}
},
enableSystemRegister:{
get(){
enableSystemRegister: {
get() {
const item = this.systemConfig.find(x => x.configName == 'RegisterConfig')
return item ? JSON.parse(item.configBody).RegisterOn : false
},
set(newValue){
set(newValue) {
const item = this.systemConfig.find(x => x.configName == 'RegisterConfig')
if (item) {
let jsonBody = JSON.parse(item.configBody)
@ -158,12 +263,12 @@ export default {
}
}
},
enableSystemRegisterEmailValidate:{
get(){
enableSystemRegisterEmailValidate: {
get() {
const item = this.systemConfig.find(x => x.configName == 'RegisterConfig')
return item ? JSON.parse(item.configBody).Emailvalidate : false
},
set(newValue){
set(newValue) {
const item = this.systemConfig.find(x => x.configName == 'RegisterConfig')
if (item) {
let jsonBody = JSON.parse(item.configBody)
@ -179,4 +284,9 @@ export default {
}
}
</script>
</script>
<style scoped>
.card-header p {
color: rgba(255, 255, 255, 0.85);
}
</style>

View File

@ -2,10 +2,13 @@
<div class="main-wrapper">
<div class="row">
<div class="col" v-if="isLoaded">
<div class="col">
<div class="card">
<DataTable title="用户管理" :headers="tableHeaders" :rows="tableData" :data-count="dataCount"
@pageChanged="pageChangedHandle" @dataDelete="deleteHandle" @dataModify="modifyHandle" @dataAdd="addHandle"
/>
<card-loading v-if="!isLoaded"/>
</div>
<user-form-modal title="修改用户" ref="userModal" />
<user-form-modal title="添加用户" formType="add" ref="addUserModal" />
</div>
@ -16,11 +19,13 @@
<script>
import DataTable from '@/components/DataTable.vue';
import UserFormModal from '../../components/UserFormModal.vue';
import CardLoading from '../../components/CardLoading.vue';
export default {
components: {
DataTable,
UserFormModal
UserFormModal,
CardLoading
},
data() {
return {
@ -40,6 +45,7 @@ export default {
},
methods: {
async loadUserList(pageIndex = 1, pageSize = 10, desc = false) {
this.isLoaded = false
try {
const res = await this.$api.getUserList(pageIndex, pageSize, desc)
if (res.code == '1000') {
@ -48,6 +54,8 @@ export default {
} catch (e) {
this.$alert('用户列表数据加载失败!', 'danger')
console.error(e)
}finally{
this.isLoaded = true
}
},
async pageChangedHandle(newval) {
@ -97,6 +105,7 @@ export default {
if(res == null) return
try{
const modifyRes = await this.$api.addUser(res)
if(modifyRes.code != 1000){
this.$alert(modifyRes.message, 'danger')
return
@ -111,7 +120,7 @@ export default {
async mounted() {
await this.loadUserList()
this.dataCount = (await this.$api.getUserCount()).data
this.isLoaded = true
}
};
</script>