3 Commits
29 changed files with 3489 additions and 688 deletions
-3
View File
@@ -10,9 +10,6 @@
</template>
<script>
import Alert from '@/components/Alert.vue';
import { mapGetters, mapState } from 'vuex';
export default {
data(){
return {
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

+149
View File
@@ -0,0 +1,149 @@
<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="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>
</template>
<script>
export default {
name: 'ApiFormModal',
props: {
title: {
type: String
},
formType: {
type: String,
default: 'edit'
}
},
data() {
return {
visible: false,
ApiId: -2,
packageList: [],
form: {
name: '',
description: '',
method: '0',
packageId: []
},
resolve: null
};
},
methods: {
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 = apipackageitem
this.packageList = packageList
return new Promise((resolve) => {
this.resolve = resolve;
});
},
submit() {
this.visible = false;
this.form.method = Number(this.form.method)
//this.form.packageId = Number(this.form.packageId)
this.resolve && this.resolve(this.form);
},
cancel() {
this.visible = false;
this.resolve && this.resolve(null);
},
resetForm() {
this.form = {
name: '',
description: '',
method: '0',
packageId: []
};
}
}
};
</script>
<style scoped>
.modal {
background: rgba(0, 0, 0, 0.5);
}
.formcontent {
padding: 0 30px;
}
#PackageSelect {
border-radius: 8px;
padding: 8px;
}
#PackageSelect option:checked {
background-color: #0d6efd;
color: white;
}
</style>
+19
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>
+30 -3
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">
@@ -109,6 +109,20 @@
<div v-if="header.type == 'datetime'">
<span>{{ formatDateTime(item[header.value]) }}</span>
</div>
<div v-if="header.type == 'method'">
<span
:class="[
'badge',
'bg-warning',
]"
>{{ callMethod[item[header.value]] }}</span
>
</div>
<div v-if="header.type == 'find'">
<span class="badge bg-info">
{{ header.findValue.find(x => x.id == item[header.value]).name }}
</span>
</div>
</td>
<td>
<button
@@ -223,7 +237,6 @@
</div>
</div>
</div>
</div>
</template>
<script>
@@ -259,6 +272,8 @@ export default {
pageCount: 0,
//分页按钮数据,[{text:'1',index:1},.....]
pageBtn: [],
//调用方法
callMethod:['GET','POST']
};
},
mounted() {},
@@ -335,7 +350,10 @@ export default {
this.$emit("dataDelete", id);
},
handleSearch() {},
handleAddUser() {},
handleAddUser() {
this.$emit("dataAdd");
}
,
handleBatchDelete() {},
},
watch: {
@@ -349,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: {
+411
View File
@@ -0,0 +1,411 @@
<template>
<div>
<div class="card-body">
<h5 class="card-title text-center card-header">{{ title }}</h5>
<div id="zero-conf_wrapper" class="dataTables_wrapper dt-bootstrap4">
<div class="row align-items-center">
<div class="col-sm-12 col-md-6 d-flex align-items-center">
<div class="dataTables_length" id="zero-conf_length">
<label class="d-flex align-items-center" style="white-space: nowrap">
展示
<select
name="zero-conf_length"
aria-controls="zero-conf"
class="custom-select custom-select-sm form-control form-control-sm mx-2"
v-model="pageSize"
>
<option value="10">10</option>
<option value="25">25</option>
<option value="50">50</option>
<option value="100">100</option>
</select>
条数据
</label>
</div>
<div class="ms-3">
<label class="d-flex align-items-center" style="white-space: nowrap">
搜索:
<input
type="search"
class="form-control form-control-sm ms-2"
placeholder="请输入关键字"
aria-controls="zero-conf"
/>
<button class="btn btn-sm btn-secondary ms-2" @click="handleSearch">
搜索
</button>
</label>
</div>
</div>
<div class="col-sm-12 col-md-6 text-center">
</div>
</div>
<div class="row">
<div class="col-sm-12">
<table
id="zero-conf"
class="display dataTable"
style="width: 100%"
role="grid"
aria-describedby="zero-conf_info"
>
<thead>
<tr role="row">
<th
class="sorting_asc"
tabindex="0"
aria-controls="zero-conf"
rowspan="1"
colspan="1"
aria-sort="ascending"
aria-label="Name: activate to sort column descending"
style="width: 85.5469px"
v-for="(item, index) in headers"
:key="index"
>
{{ item.text }}
</th>
</tr>
</thead>
<tbody>
<tr
role="row"
v-for="(item, rowIndex) in rows"
:key="rowIndex"
:class="rowIndex % 2 ? 'even' : 'odd'"
>
<td v-for="(header, colIndex) in headers" :key="colIndex">
<div v-if="header.type == null">
<span>{{ item[header.value] ? item[header.value] : '空' }}</span>
</div>
<div v-if="header.type == 'bool'">
<span
:class="[
'badge',
item[header.value] ? 'bg-danger' : 'bg-success',
]"
>{{ item[header.value] ? "禁用" : "正常" }}</span
>
</div>
<div v-if="header.type == 'arrObj'">
<span
class="badge bg-info"
v-for="(objItem, objIndex) in item[header.value]"
:key="objIndex"
>
{{ objItem[header.child] }}
</span>
</div>
<div v-if="header.type == 'money'">
<span>{{ item[header.value].toFixed(2) }}</span>
</div>
<div v-if="header.type == 'datetime'">
<span>{{ formatDateTime(item[header.value]) }}</span>
</div>
<div v-if="header.type == 'method'">
<span
:class="[
'badge',
'bg-warning',
]"
>{{ callMethod[item[header.value]] }}</span
>
</div>
<div v-if="header.type == 'find'">
<span class="badge bg-info">
{{ header.findValue.find(x => x.id == item[header.value]).name }}
</span>
</div>
<div v-if="header.type == 'orderStatus'">
<span class="badge bg-info" v-if="item[header.value] == 0">
待支付
</span>
<span class="badge bg-success" v-if="item[header.value] == 1">
已支付
</span>
<span class="badge bg-warning" v-if="item[header.value] == 2">
已取消
</span>
<span class="badge bg-danger" v-if="item[header.value] == 3">
失败
</span>
</div>
<div v-if="header.type == 'orderType'">
<span class="badge bg-primary" v-if="item[header.value] == 1">
充值
</span>
<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>
<tfoot>
<tr>
<th
rowspan="1"
colspan="1"
v-for="(header, index) in headers"
:key="index"
>
{{ header.value }}
</th>
</tr>
</tfoot>
</table>
</div>
</div>
<div class="row">
<div class="col-sm-12 col-md-5">
<div
class="dataTables_info"
id="zero-conf_info"
role="status"
aria-live="polite"
>
正在展示 {{ currentPageIndex * pageSize - (pageSize - 1) }}
{{ currentPageIndex * pageSize }} 条数据 总计 {{ dataCount }}
</div>
</div>
<div class="col-sm-12 col-md-7">
<div
class="dataTables_paginate paging_simple_numbers"
id="zero-conf_paginate"
>
<ul class="pagination">
<li
:class="[
'paginate_button',
'page-item',
'previous',
currentPageIndex == 1 ? 'disabled' : '',
]"
id="zero-conf_previous"
>
<a
href="#"
aria-controls="zero-conf"
data-dt-idx="0"
tabindex="0"
class="page-link"
@click.prevent="previousPage"
>上一页</a
>
</li>
<li
:class="[
'paginate_button',
'page-item',
item.index == currentPageIndex ? 'active' : '',
]"
v-for="(item, index) in pageBtn"
:key="index"
>
<a
href="#"
aria-controls="zero-conf"
:data-dt-idx="item.index"
tabindex="0"
class="page-link"
@click.prevent="currentPageIndex = item.index"
>{{ item.text }}</a
>
</li>
<li
:class="[
'paginate_button',
'page-item',
'next',
currentPageIndex == pageCount ? 'disabled' : '',
]"
id="zero-conf_next"
>
<a
href="#"
aria-controls="zero-conf"
data-dt-idx="7"
tabindex="0"
class="page-link"
@click.prevent="nextPage"
>下一页</a
>
</li>
</ul>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: "OrderDataTable",
props: {
title: {
type: String,
default: "标题",
},
headers: {
// 表头数组 [{ text: '名字', value: 'name'}, ...]
type: Array,
required: true,
},
rows: {
// 数据数组 [{ name: '佐藤爱理', position: '会计', office: '东京', age: 33, startDate: '2008/11/28', salary: '162,700 元' }, ...]
type: Array,
required: true,
},
dataCount: {
type: Number,
required: true,
},
},
data() {
return {
//当前页索引
currentPageIndex: 1,
//单页大小
pageSize: 10,
//总页数
pageCount: 0,
//分页按钮数据,[{text:'1',index:1},.....]
pageBtn: [],
//调用方法
callMethod:['GET','POST']
};
},
mounted() {},
methods: {
//下一页
nextPage() {
if (this.currentPageIndex < this.pageCount) this.currentPageIndex++;
},
//上一页
previousPage() {
if (this.currentPageIndex > 1) {
this.currentPageIndex--;
}
},
changePage(pageIndex) {
this.currentPageIndex == pageIndex;
},
//更新总页数
updatePageCount(newPageSize) {
this.pageCount = Math.floor(this.dataCount / newPageSize);
this.pageCount += this.dataCount % newPageSize == 0 ? 0 : 1;
},
//更新分页按钮数据
updatePageBtn(newPageIndex) {
this.pageBtn = [];
//初始化首页
this.pageBtn.push({ text: "1", index: 1 });
//总页数小于8时显示全部页码
if (this.pageCount <= 8) {
for (let i = 2; i < this.pageCount; i++) {
this.pageBtn.push({ text: `${i}`, index: i });
}
} else {
//当前页左侧显示2格页码,当显示最小页码距离首页中间间隔大于1时隐藏间隔页面
if (newPageIndex - 2 > 3) {
this.pageBtn.push({ text: "...", index: newPageIndex });
}
//渲染当前页码左右各两格页码
const numleft = newPageIndex - 2 < 2 ? newPageIndex - 2 : 2;
const numright =
newPageIndex + 2 < this.pageCount ? 2 : this.pageCount - newPageIndex - 1;
for (let i = newPageIndex - numleft; i <= newPageIndex + numright; i++) {
this.pageBtn.push({ text: `${i}`, index: i });
}
//当前页右侧显示2格页码,当显示最大页码距离尾页中间间隔大于1时隐藏间隔页面
if (newPageIndex + 2 < this.pageCount - 2) {
this.pageBtn.push({ text: "...", index: newPageIndex });
}
}
//渲染尾页
this.pageBtn.push({ text: `${this.pageCount}`, index: this.pageCount });
},
formatDateTime(str) {
if (!str) return "-";
const date = new Date(str);
if (isNaN(date)) return "-"; // 防止 Safari 报 Invalid Date
const pad = (n) => n.toString().padStart(2, "0");
const Y = date.getFullYear();
const M = pad(date.getMonth() + 1);
const D = pad(date.getDate());
const h = pad(date.getHours());
const m = pad(date.getMinutes());
const s = pad(date.getSeconds());
return `${Y}-${M}-${D} ${h}:${m}:${s}`;
},
modify(id) {
this.$emit("dataModify", id);
},
deleteData(id) {
this.$emit("dataDelete", id);
},
handleSearch() {},
handleAddUser() {
this.$emit("dataAdd");
}
,
handleBatchDelete() {},
},
watch: {
//监听分页大小变化
pageSize: {
handler(newVal) {
this.updatePageCount(newVal);
//单页显示数据量改变时重置当前索引,防止数据以及控件异常
this.currentPageIndex = 1;
this.$emit("pageChanged", { pageIndex: this.currentPageIndex, pageSize: newVal });
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: {
handler(newVal) {
this.$emit("pageChanged", { pageIndex: newVal, pageSize: this.pageSize });
this.updatePageBtn(newVal);
},
immediate: true,
},
},
};
</script>
<style scoped>
/* 可自定义样式 */
.card-title {
font-size: 1.8rem;
font-weight: bold;
color: #333;
}
</style>
+123
View File
@@ -0,0 +1,123 @@
<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="inpuCallLimit3" class="col-sm-2 col-form-label">月调用次数</label>
<div class="col-sm-10">
<input type="text" v-model="form.callLimit" class="form-control" id="inputCallLimit3">
</div>
</div>
<div class="row mb-3">
<label for="inputPrice3" class="col-sm-2 col-form-label">价格</label>
<div class="col-sm-10">
<input type="text" v-model="form.price" class="form-control" id="inputPrice3" v-money>
</div>
</div>
<div class="row mb-3">
<label for="inputOneMinuteLimit3" class="col-sm-2 col-form-label">调用频率限制</label>
<div class="col-sm-10">
<input type="text" v-model="form.oneMinuteLimit" class="form-control"
id="inputOneMinuteLimit3">
</div>
</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>
</template>
<script>
export default {
name: 'PackageFormModal',
props: {
title: {
type: String
},
formType: {
type: String,
default: 'edit'
}
},
data() {
return {
visible: false,
packageId: -2,
packageList: [],
form: {
name: '',
callLimit: 0,
price: '0',
oneMinuteLimit: 0
},
resolve: null
};
},
methods: {
open(packageId = null, packageInfo = null) {
this.resetForm();
this.visible = true;
if (packageInfo != null) {
this.packageId = packageId;
this.form.name = packageInfo.name;
this.form.callLimit = packageInfo.callLimit;
this.form.price = packageInfo.price.toString();
this.form.oneMinuteLimit = packageInfo.oneMinuteLimit;
}
return new Promise((resolve) => {
this.resolve = resolve;
});
},
submit() {
this.visible = false;
this.resolve && this.resolve(this.form);
},
cancel() {
this.visible = false;
this.resolve && this.resolve(null);
},
resetForm() {
this.form = {
name: '',
callLimit: 0,
price: '0',
oneMinuteLimit: 0
};
}
}
};
</script>
<style scoped>
.modal {
background: rgba(0, 0, 0, 0.5);
}
.formcontent {
padding: 0 30px;
}
</style>
+11 -4
View File
@@ -44,6 +44,9 @@
</template>
<script>
import md5 from 'blueimp-md5';
export default {
name: 'UserFormModal',
@@ -63,26 +66,30 @@ export default {
form: {
userName: '',
email: '',
balance: '',
balance: '0',
password: ''
},
resolve: null
};
},
methods: {
open(userId,userInfo = null) {
open(userId = null,userInfo = null) {
this.resetForm();
this.visible = true;
this.userId = userId;
this.form.email = userInfo.email;
if(userInfo != null){
this.form.email = userInfo.email;
this.form.userName = userInfo.userName;
this.form.balance = userInfo.balance;
}
return new Promise((resolve) => {
this.resolve = resolve;
});
},
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() {
@@ -93,7 +100,7 @@ export default {
this.form = {
userName: '',
email: '',
balance: '',
balance: '0',
password: ''
};
}
+160 -1
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)
@@ -28,6 +29,132 @@ const getUserInfoById = async (id) => await request.get(`/api/Admin/UserInfo?use
//更新用户信息
const updateUserInfo = async (id,param) => await request.post(`/api/Admin/UpdateUser?userId=${id}`,param)
//添加用户
const addUser =async (param) => await request.post('/api/admin/adduser',param)
//获取api列表
const getApiList = async (pageIndex,pageSize,desc) => await request.get(`/api/apis/ApiList?pageIndex=${pageIndex}&pageSize=${pageSize}&desc=${desc}`)
//删除API
const deleteApi = async (id) => await request.delete(`/api/Apis/DeleteApi?apiId=${id}`)
//获取指定API信息
const getApiInfoById = async (id) => await request.get(`/api/Apis/ApiInfo?apiId=${id}`)
//更新API信息
const updateApiInfo = async (id,param) => await request.post(`/api/Apis/UpdateApi?apiId=${id}`,param)
//获取套餐列表
const getPackageList = async (pageIndex,pageSize,desc) => await request.get(`/api/Package/GetPackageList?pageIndex=${pageIndex}&pageSize=${pageSize}&desc=${desc}`)
//删除套餐
const deletePackageById = async (id) => await request.delete(`/api/Package/DeletePackage?packageId=${id}`)
//更新套餐信息
const updatePackage = async (id,param) => await request.post(`/api/Package/UpdatePackage?packageId=${id}`,param)
//添加套餐
const addPackage = async (param) => await request.post("/api/Package/AddPackage",param)
//获取套餐信息
const getPackageInfoById = async (id) => await request.get(`/api/Package/GetPackageInfo?packageId=${id}`)
//更新系统配置
const updateSystemConfig = async (configName,configBody) => await request.post('/api/SystemConfig/UpdateSystemConfig',{configName:configName,configBody:configBody})
//创建支付
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,
out_trade_no,
type,
name,
money,
trade_status,
sign,
sign_type
}).toString();
return await request.get(`/api/pay/notice?${query}`);
};
//获取用户套餐列表
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/order/getorders?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,
@@ -38,5 +165,37 @@ export default {
getUserCount,
deleteUser,
getUserInfoById,
updateUserInfo
updateUserInfo,
getApiList,
deleteApi,
getApiInfoById,
updateApiInfo,
getPackageList,
updateSystemConfig,
payNotice,
createPayment,
addUser,
getUserPackagesAdmin,
deletePackageById,
updatePackage,
addPackage,
getPackageInfoById,
getOrderList,
getMyOrderList,
uploadAvatar,
updateMyInfo,
uploadLogo,
uploadFavicon,
buyPackage,
getOrderNum,
getMyOrderNum,
setApiKey,
getAllPayment,
updatePayment,
getUserPackages,
getPublicApiList,
getApiPackages,
getAllPublicPayment,
getSystemInfo,
setApiPackage
}
+1 -1
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) {
+41 -20
View File
@@ -17,18 +17,17 @@ 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',
name: 'paynotice',
component: () => import('@/views/PayNotice.vue')
},
{
path: '/layout',
@@ -39,7 +38,7 @@ const routes = [
path: 'index',
component: () => import('@/views/layout/Dashboard.vue'),
meta: {
title: '主页',
title: '控制台',
icon: 'home',
showInMenu: true,
showInUser: false,
@@ -79,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'),
@@ -90,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'),
@@ -97,7 +118,7 @@ const routes = [
title: '订单管理',
icon: 'shopping-cart',
showInMenu: true,
showInUser: false,
showInUser: true,
isHome: false
}
},
@@ -113,14 +134,14 @@ const routes = [
}
},
{
path: 'records',
component: () => import('@/views/layout/Records.vue'),
path: 'paymentconfig',
component: () => import('@/views/layout/PayConfig.vue'),
meta: {
title: '消费记录',
icon: 'archive',
title: '支付配置',
icon: 'sliders',
showInMenu: true,
showInUser: true,
isHome: false
showInUser:false,
isHome:false
}
},
{
+2 -1
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
}
+281 -112
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>
+36
View File
@@ -0,0 +1,36 @@
<template>
<div class="pay-redirect">
<h2>正在处理支付结果请稍候...</h2>
</div>
</template>
<script>
export default {
name: 'PayRedirect',
data(){
return {
text:'正在处理支付结果,请稍候...'
}
},
mounted() {
const query = this.$route.query;
// 校验必要字段
if (!query || !query.out_trade_no || !query.trade_status) {
alert('参数不完整,无法确认支付结果');
return;
}
this.$api.payNotice(query.pid,query.trade_no,query.out_trade_no,query.type,query.name,query.money,query.trade_status,query.sign,query.sign_type)
.then(res => {
console.log('通知后端成功', res.data);
this.$router.push('/layout/balance');
})
.catch(err => {
console.error('通知后端失败', err);
this.text = '支付失败'
});
}
};
</script>
+10 -4
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
}
}
},
+276
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>
+102 -6
View File
@@ -2,11 +2,18 @@
<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"
:data-count="dataCount" />
:data-count="dataCount"
@pageChanged="pageChangedHandle"
@dataDelete="deleteHandle"
@dataModify="modifyHandle" />
<card-loading v-if="!isLoaded"/>
</div>
<api-form-modal title="修改接口" ref="apiModal" />
</div>
</div>
</div>
@@ -14,28 +21,117 @@
<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
DataTable,
ApiFormModal,
CardLoading
},
data() {
return {
isLoaded: false,
packageList: [],
tableHeaders: [
{ text: "", value: "", width: "" }
{ text: "编号", value: "id", width: "100px" },
{ text: "名称", value: "name", width: "100px" },
{ text: "描述", value: "description", width: "100px" },
{ text: "路径", value: "endpoint", width: "100px" },
{ text: "调用方法", value: "method", width: "100px", type: "method" },
{ text: "状态", value: "isactive", width: "100px", type: "bool" },
{ text: "所属套餐", value: "packageId", width: "100px", type: "find", findProperty: "id", findValue: [] },
],
tableData: [],
dataCount: 0
}
},
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') {
this.tableData = res.data
}
} catch (e) {
this.$alert('API列表数据加载失败!', 'danger')
console.error(e)
}finally{
this.isLoaded = true
}
},
async pageChangedHandle(newval) {
await this.loadApiList(newval.pageIndex, newval.pageSize, false)
},
async modifyHandle(id) {
try {
//加载API信息到新窗口
const apiinfoRes = await this.$api.getApiInfoById(id)
if (apiinfoRes.code != 1000) {
this.$alert(apiinfoRes.message, 'danger')
return
}
//拉起修改弹窗
const res = await this.$refs.apiModal.open(id, apiinfoRes.data, this.packageList)
//弹窗返回为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
}
this.$alert('修改成功', 'success')
} catch (e) {
this.$alert(e, 'danger')
}
},
async deleteHandle(id) {
const confirmRes = await this.$confirm('删除', '是否删除API')
if (!confirmRes) return;
try {
const res = await this.$api.deleteApi(id)
if (res.code != 1000) {
this.$alert(res.message, 'danger')
return;
}
this.$alert('删除成功', 'success')
} catch (e) {
this.$alert(e, 'danger')
}
},
async loadPackageList(pageIndex = 1, pageSize = 10, desc = false) {
//获取套餐列表
try{
const packageRes = await this.$api.getPackageList(1, 1000, false)
if (packageRes.code != 1000) {
this.$alert(packageRes.message, 'danger')
}
this.packageList = packageRes.data
}catch(e){
this.$alert('加载套餐列表失败!', 'danger')
console.error(e)
}
},
async setApiPackage(params){
return await this.$api.setApiPackage(params)
}
},
mounted(){
this.isLoaded = true
async mounted() {
await this.loadPackageList()
await this.loadApiList()
this.tableHeaders.find(x => x.value == 'packageId').findValue = this.packageList
}
}
</script>
+338 -204
View File
@@ -1,256 +1,390 @@
<template>
<div class="row justify-content-center mt-5">
<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">{{ 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 === 'alipay' }"
>
<input
type="radio"
id="option1"
name="options"
class="hidden-radio"
value="alipay"
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 === 'wechat' }"
>
<input
type="radio"
id="option2"
name="options"
class="hidden-radio"
value="wechat"
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>
</template>
<script>
import { mapState } from "vuex";
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 },
rechargeAmount: 0,
selectedpay: "",
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: {
...mapState("userinfo", {
balance: (state) => (state.user ? state.user.balance : 0),
}),
formattedRechargeAmount: {
get() {
return this.rechargeAmount === 0 ? "" : this.rechargeAmount;
const num = Number(this.rechargeAmount) || 0;
return num
.toFixed(2)
.replace(/\B(?=(\d{3})+(?!\d))/g, ",");
},
set(value) {
this.rechargeAmount = value === "" ? 0 : Number(value);
},
},
},
created() {
this.$store.dispatch("userinfo/fetchUserInfo");
set(val) {
const num = parseFloat(String(val).replace(/,/g, ""));
if (!isNaN(num)) this.rechargeAmount = num;
}
}
},
methods: {
deposit() {
if (this.rechargeAmount <= 0) {
this.$alert("充值金额必须大于0", "danger");
return;
}
if (!this.selectedpay) {
this.$alert("请选择充值方式", "danger");
return;
}
async getUserInfo() {
try {
console.log(`Depositing ${this.rechargeAmount} using ${this.selectedpay}`);
this.$alert("充值成功", "success");
} catch (error) {
console.error("充值失败:", error);
this.$alert("充值失败,请稍后再试", "danger");
const res = await this.$api.getUserInfo();
if (res.code === 1000) this.userInfo = res.data;
else this.$alert("加载失败", "danger");
} catch {
this.$alert("加载失败", "danger");
}
this.rechargeAmount = 0;
this.selectedpay = "";
},
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>
+197
View File
@@ -0,0 +1,197 @@
<template>
<div class="main-wrapper py-5" style="background: #f8f9fa;">
<div class="container">
<div class="row justify-content-center">
<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 || [];
} 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);
}
.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>
+79 -315
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>
<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>
+31 -9
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
},
/**
* 展开/关闭侧边菜单栏
*/
@@ -186,6 +193,7 @@ export default {
auth.clear()
this.$alert('登录状态失效,请重新登录...','info')
this.$router.push('/auth/login')
}
},watch:{
isAdmin:{
@@ -196,9 +204,10 @@ export default {
}
},
computed:{
...mapGetters('userinfo',['isAdmin'])
...mapGetters('userinfo',['isAdmin','Avatar'])
},
mounted(){
this.baseURL = axiosIntance.defaults.baseURL
feather.replace()
},
beforeCreate(){
@@ -208,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>
+117
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>
+91
View File
@@ -0,0 +1,91 @@
<template>
<div class="main-wrapper">
<div class="row">
<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>
</template>
<script>
import OrderDataTable from '@/components/OrderDataTable.vue';
import { mapGetters } from 'vuex'
import CardLoading from '../../components/CardLoading.vue';
export default {
components: {
OrderDataTable,
CardLoading
},
data() {
return {
dataCount: 0,
isLoaded: false,
tableHeaders: [
{ text: "编号", value: "id", width: "155px" },
{ 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"},
{ text: "订单类型", value: "orderType", width: "103px",type:"orderType"},
{ text: "订单状态", value: "status", width: "103px",type:"orderStatus"},
{ text: "创建时间", value: "createdAt", width: "103px",type:"datetime"},
],
tableData: [],
};
},
methods: {
async loadOrderList(pageIndex = 1, pageSize = 10, desc = true) {
this.isLoaded = false
try {
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.loadOrderList(newval.pageIndex, newval.pageSize, true)
}
},
computed: {
...mapGetters('userinfo',['isAdmin'])
},
async mounted() {
await this.loadOrderList()
await this.loadOrderCount()
}
};
</script>
+124
View File
@@ -0,0 +1,124 @@
<template>
<div class="main-wrapper">
<div class="row">
<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>
</div>
</div>
</template>
<script>
import DataTable from '@/components/DataTable.vue';
import PackageFormModal from '../../components/PackageFormModal.vue';
import CardLoading from '../../components/CardLoading.vue';
export default {
components: {
DataTable,
PackageFormModal,
CardLoading
},
data() {
return {
dataCount: 0,
isLoaded: false,
tableHeaders: [
{ text: "编号", value: "id", width: "155px" },
{ text: "套餐名", value: "name", width: "214px" },
{ text: "套餐月限额", value: "callLimit", width: "48px" },
{ text: "价格", value: "price", width: "29px", type: "money" },
{ text: "限速", value: "oneMinuteLimit", width: "82px" },
],
tableData: [],
};
},
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') {
this.tableData = res.data
}
} catch (e) {
this.$alert('套餐列表数据加载失败!', 'danger')
console.error(e)
}finally{
this.isLoaded = true
}
},
async pageChangedHandle(newval) {
await this.loadPackageList(newval.pageIndex, newval.pageSize, false)
},
async deleteHandle(id) {
const confirmRes = await this.$confirm('删除', '是否删除套餐?')
if (!confirmRes) return;
try {
const res = await this.$api.deletePackage(id)
if (res.code != 1000) {
this.$alert(res.message, 'danger')
return;
}
this.$alert('删除成功', 'success')
} catch (e) {
this.$alert(e, 'danger')
}
},
async modifyHandle(id) {
try {
//加载用户信息到新窗口
const packageInfoRes = await this.$api.getPackageInfoById(id)
if(packageInfoRes.code != 1000){
this.$alert(packageInfoRes.message,'danger')
return
}
//拉起修改弹窗
const res = await this.$refs.userModal.open(id,packageInfoRes.data)
//弹窗返回为null表示用户关闭了窗口,不执行操作
if(res == null) return
const modifyRes = await this.$api.updatePackage(id,res)
if(modifyRes.code != 1000){
this.$alert(modifyRes.message, 'danger')
return
}
this.$alert('修改成功', 'success')
} catch (e) {
this.$alert(e, 'danger')
}
},
async addHandle(){
//拉起添加弹窗
const res = await this.$refs.addUserModal.open()
if(res == null) return
try{
const modifyRes = await this.$api.addPackage(res)
if(modifyRes.code != 1000){
this.$alert(modifyRes.message, 'danger')
return
}
this.$alert('添加成功', 'success')
}catch (e) {
this.$alert(e, 'danger')
}
}
},
async mounted() {
await this.loadPackageList()
this.dataCount = 10
this.isLoaded = true
}
};
</script>
+263
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>
+273
View File
@@ -0,0 +1,273 @@
<template>
<div class="main-wrapper">
<div class="row justify-content-center">
<div class="col-lg-8">
<div class="card shadow-sm border-0 mt-4">
<div class="card-header bg-gradient text-white py-4">
<h4 class="mb-0">个人中心</h4>
<p class="mb-0 small">管理您的账户信息与安全设置</p>
</div>
<div class="card-body">
<!-- 用户头像 -->
<div class="text-center mb-4">
<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>
</div>
<!-- 用户信息列表 -->
<ul class="list-group list-group-flush">
<li class="list-group-item d-flex justify-content-between align-items-center">
<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')" disabled>修改</button>
</li>
<li class="list-group-item d-flex justify-content-between align-items-center">
<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">
<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>
<button class="btn btn-link" @click="openEdit('password')">修改</button>
</li>
</ul>
</div>
</div>
</div>
</div>
<!-- 弹出框 -->
<div v-if="editingSection" class="modal fade show d-block" tabindex="-1" style="background-color: rgba(0,0,0,0.5);">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">修改 {{ getSectionTitle(editingSection) }}</h5>
<button type="button" class="btn-close" @click="closeEdit"></button>
</div>
<div class="modal-body">
<!-- 各字段弹出表单内容 -->
<div v-if="editingSection === 'nickname'">
<input type="text" class="form-control" v-model="form.nickname" placeholder="请输入昵称" />
</div>
<div v-else-if="editingSection === 'email'">
<input type="email" class="form-control" v-model="form.email" placeholder="请输入邮箱" />
</div>
<div v-else-if="editingSection === 'phone'">
<input type="text" class="form-control" v-model="form.phone" placeholder="请输入手机号" />
</div>
<div v-else-if="editingSection === 'gender'">
<select class="form-control" v-model="form.gender">
<option value="male"></option>
<option value="female"></option>
<option value="other">其他</option>
</select>
</div>
<div v-else-if="editingSection === 'bio'">
<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.newPassword" placeholder="新密码" />
<input type="password" class="form-control" v-model="form.confirmPassword" placeholder="确认新密码" />
<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/*" />
<img :src="previewAvatar" class="mt-3 rounded-circle" width="80" v-if="previewAvatar" />
</div>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" @click="closeEdit">取消</button>
<button class="btn btn-primary" @click="saveEdit">保存</button>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import axiosInstance from '@/request/request';
import md5 from 'blueimp-md5';
export default {
name: "UserProfilePanel",
data() {
return {
showApiKey:false,
baseUrl: '',
user: {
userName: '',
email: '',
roles: [],
avatar: '',
apiKey: null
},
editingSection: null,
form: {},
previewAvatar: null,
loaded: false,
avatarFile: null
};
},
methods: {
openEdit(section) {
this.editingSection = section;
this.form = { ...this.user }; // clone
},
closeEdit() {
this.editingSection = null;
this.previewAvatar = null;
},
async saveEdit() {
if (this.editingSection === 'password') {
if (this.form.newPassword !== this.form.confirmPassword) {
return;
}
// 处理密码更新逻辑
await this.updatePassword()
} else if (this.editingSection === 'avatar') {
if (this.previewAvatar) {
await this.saveAvatar()
}
} else {
Object.assign(this.user, this.form);
}
this.closeEdit();
},
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 = {
userName: "用户名",
email: "邮箱",
password: "密码",
avatar: "头像"
};
return map[section] || "信息";
},
onAvatarChange(e) {
const file = e.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = (event) => {
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>
<style scoped>
.bg-gradient {
background: linear-gradient(to right, #4e73df, #1cc88a);
}
.modal {
z-index: 1050;
}
.btn-close {
background: none;
border: none;
font-size: 1.2rem;
}
</style>
View File
+292
View File
@@ -0,0 +1,292 @@
<template>
<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 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: [],
logoFile: null,
favicon: null
}
},
methods: {
async submit() {
for (const item of this.systemConfig) {
const configName = item.configName
const configBody = item.configBody
try {
const res = await this.$api.updateSystemConfig(configName, configBody)
if (res.code != 1000) this.$alert(res.message, 'danger')
} catch (err) {
this.$alert('更新配置失败', 'danger')
console.error(`更新 ${configName} 失败`, err)
}
}
// 全部成功后提示
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: {
systemName: {
get() {
const systemName = this.systemConfig.find(x => x.configName == 'SystemName')
return systemName ? systemName.configBody : ''
},
set(newValue) {
const systemName = this.systemConfig.find(x => x.configName == 'SystemName')
if (systemName) {
systemName.configBody = newValue
}
}
},
systemEmail: {
get() {
const item = this.systemConfig.find(x => x.configName == 'Email')
return item ? item.configBody : ''
},
set(newValue) {
const item = this.systemConfig.find(x => x.configName == 'Email')
if (item) {
item.configBody = newValue
}
}
},
systemPhone: {
get() {
const item = this.systemConfig.find(x => x.configName == 'Phone')
return item ? item.configBody : ''
},
set(newValue) {
const item = this.systemConfig.find(x => x.configName == 'Phone')
if (item) {
item.configBody = newValue
}
}
},
systemDescription: {
get() {
const item = this.systemConfig.find(x => x.configName == 'SystemDescription')
return item ? item.configBody : ''
},
set(newValue) {
const item = this.systemConfig.find(x => x.configName == 'SystemDescription')
if (item) {
item.configBody = newValue
}
}
},
enableSystemRegister: {
get() {
const item = this.systemConfig.find(x => x.configName == 'RegisterConfig')
return item ? JSON.parse(item.configBody).RegisterOn : false
},
set(newValue) {
const item = this.systemConfig.find(x => x.configName == 'RegisterConfig')
if (item) {
let jsonBody = JSON.parse(item.configBody)
jsonBody.RegisterOn = newValue
item.configBody = JSON.stringify(jsonBody)
}
}
},
enableSystemRegisterEmailValidate: {
get() {
const item = this.systemConfig.find(x => x.configName == 'RegisterConfig')
return item ? JSON.parse(item.configBody).Emailvalidate : false
},
set(newValue) {
const item = this.systemConfig.find(x => x.configName == 'RegisterConfig')
if (item) {
let jsonBody = JSON.parse(item.configBody)
jsonBody.Emailvalidate = newValue
item.configBody = JSON.stringify(jsonBody)
}
}
}
},
created() {
this.systemConfig = this.$store.state.config.systemconfig
}
}
</script>
<style scoped>
.card-header p {
color: rgba(255, 255, 255, 0.85);
}
</style>
+31 -4
View File
@@ -2,10 +2,15 @@
<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" />
@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>
</div>
</div>
@@ -14,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 {
@@ -38,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') {
@@ -46,6 +54,8 @@ export default {
} catch (e) {
this.$alert('用户列表数据加载失败!', 'danger')
console.error(e)
}finally{
this.isLoaded = true
}
},
async pageChangedHandle(newval) {
@@ -88,12 +98,29 @@ export default {
} catch (e) {
this.$alert(e, 'danger')
}
},
async addHandle(){
//拉起添加弹窗
const res = await this.$refs.addUserModal.open()
if(res == null) return
try{
const modifyRes = await this.$api.addUser(res)
if(modifyRes.code != 1000){
this.$alert(modifyRes.message, 'danger')
return
}
this.$alert('添加成功', 'success')
}catch (e) {
this.$alert(e, 'danger')
}
}
},
async mounted() {
await this.loadUserList()
this.dataCount = (await this.$api.getUserCount()).data
this.isLoaded = true
}
};
</script>