This commit is contained in:
张成
2026-03-24 16:07:02 +08:00
commit aa8eaa6ccd
121 changed files with 34042 additions and 0 deletions

View File

@@ -0,0 +1,21 @@
class SysAdServer {
async getAll(param) {
return await window.framework.http.get("/sys_ad/index", param);
}
async add(row) {
return await window.framework.http.post("/sys_ad/add", row);
}
async edit(row) {
return await window.framework.http.post("/sys_ad/edit", row);
}
async del(row) {
return await window.framework.http.post("/sys_ad/del", row);
}
}
const sysAdServer = new SysAdServer();
export default sysAdServer;

View File

@@ -0,0 +1,11 @@
class BizPaymentServer {
async confirmOffline(row) {
return window.framework.http.post("/biz_payment/confirm-offline", row);
}
async confirmLink(row) {
return window.framework.http.post("/biz_payment/confirm-link", row);
}
}
export default new BizPaymentServer();

View File

@@ -0,0 +1,27 @@
class BizPlanServer {
async page(row) {
return window.framework.http.post("/biz_plan/page", row);
}
async add(row) {
return window.framework.http.post("/biz_plan/add", row);
}
async edit(row) {
return window.framework.http.post("/biz_plan/edit", row);
}
async del(row) {
return window.framework.http.post("/biz_plan/del", row);
}
async toggle(row) {
return window.framework.http.post("/biz_plan/toggle", row);
}
async all() {
return window.framework.http.get("/biz_plan/all", {});
}
}
export default new BizPlanServer();

View File

@@ -0,0 +1,27 @@
class BizSubscriptionServer {
async page(row) {
return window.framework.http.post("/biz_subscription/page", row);
}
async byUser(userId) {
return window.framework.http.get("/biz_subscription/by_user", { user_id: userId });
}
async open(row) {
return window.framework.http.post("/biz_subscription/open", row);
}
async upgrade(row) {
return window.framework.http.post("/biz_subscription/upgrade", row);
}
async renew(row) {
return window.framework.http.post("/biz_subscription/renew", row);
}
async cancel(row) {
return window.framework.http.post("/biz_subscription/cancel", row);
}
}
export default new BizSubscriptionServer();

View File

@@ -0,0 +1,15 @@
class BizTokenServer {
async page(row) {
return window.framework.http.post("/biz_token/page", row);
}
async create(row) {
return window.framework.http.post("/biz_token/create", row);
}
async revoke(row) {
return window.framework.http.post("/biz_token/revoke", row);
}
}
export default new BizTokenServer();

View File

@@ -0,0 +1,27 @@
class BizUserServer {
async page(row) {
return window.framework.http.post("/biz_user/page", row);
}
async add(row) {
return window.framework.http.post("/biz_user/add", row);
}
async edit(row) {
return window.framework.http.post("/biz_user/edit", row);
}
async del(row) {
return window.framework.http.post("/biz_user/del", row);
}
async detail(id) {
return window.framework.http.get(`/biz_user/detail?id=${encodeURIComponent(id)}`, {});
}
async disable(row) {
return window.framework.http.post("/biz_user/disable", row);
}
}
export default new BizUserServer();

View File

@@ -0,0 +1,33 @@
import request from '@/libs/http'
export const getList = (params) => {
return request({
url: '/sys_file/page',
method: 'get',
params
})
}
export const add = (data) => {
return request({
url: '/sys_file',
method: 'post',
data
})
}
export const edit = (data) => {
return request({
url: '/sys_file',
method: 'put',
data
})
}
export const del = (params) => {
return request({
url: '/sys_file',
method: 'delete',
params
})
}

View File

@@ -0,0 +1,36 @@
import config from '../../../config/index.js'
/**
* 使用原生 fetch 发送 multipart/form-data不设置 Content-Type由浏览器自动带 boundary
* 确保后端从 ctx.request.files 接收文件
*/
async function uploadOosImgWithFormData(formData) {
const apiUrl = (config.apiUrl || '').replace(/\/$/, '')
const url = `${apiUrl}/sys_file/upload_oos_img`
const headers = {}
if (window.framework && typeof window.framework.getToken === 'function') {
const token = window.framework.getToken()
if (token) headers['Authorization'] = token
}
const response = await fetch(url, { method: 'POST', headers, body: formData })
const data = await response.json().catch(() => ({}))
return data
}
class FileServe {
async upload_oos_img(row) {
if (row instanceof FormData) {
return uploadOosImgWithFormData(row)
}
let res = await window.framework.http.postFormData('/sys_file/upload_oos_img', row)
return res
}
async upload_Img(row) {
let res = await window.framework.http.postFormData("/file/upload_Img", row);
return res;
}
}
const fileServe = new FileServe();
export default fileServe;

View File

@@ -0,0 +1,30 @@
class RolePermissionServer {
async getRoles(callback) {
let res = await window.framework.http.get('/SysRolePermission/Query', {})
return res
}
async getRole(row) {
let res = await window.framework.http.get('/SysRolePermission/QueryByRoleId', row)
return res
}
async add(row) {
let res = await window.framework.http.post('/SysRolePermission/add', row)
return res
}
async edit(row) {
let res = await window.framework.http.post('/SysRolePermission/edit', row)
return res
}
async del(row) {
let res = await window.framework.http.post('/SysRolePermission/del', row)
return res
}
}
const rolePermissionServer = new RolePermissionServer()
export default rolePermissionServer

View File

@@ -0,0 +1,26 @@
class RoleServer {
async list() {
let res = await window.framework.http.get("/sys_role/index", {});
return res;
}
async add(row) {
let res = await window.framework.http.post("/sys_role/add", row);
return res;
}
async edit(row) {
let res = await window.framework.http.post("/sys_role/edit", row);
return res;
}
async del(row) {
let res = await window.framework.http.post("/sys_role/del", row);
return res;
}
}
const roleServer = new RoleServer();
export default roleServer;

View File

@@ -0,0 +1,10 @@
class SysAddress {
async index(param) {
let res = await window.framework.http.get("/sys_address/index", param);
return res;
}
}
const sysAddress = new SysAddress();
export default sysAddress;

View File

@@ -0,0 +1,30 @@
class SysModuleServer {
async all() {
let res = await window.framework.http.get("/sys_menu/all", {});
return res;
}
async list(row) {
let res = await window.framework.http.get("/sys_menu/all", row);
return res;
}
async add(row) {
let res = await window.framework.http.post("/sys_menu/add", row);
return res;
}
async edit(row) {
let res = await window.framework.http.post("/sys_menu/edit", row);
return res;
}
async del(row) {
let res = await window.framework.http.post("/sys_menu/del", row);
return res;
}
}
const sysModuleServer = new SysModuleServer();
export default sysModuleServer;

View File

@@ -0,0 +1,30 @@
class SysLogServe {
async all(param) {
let res = await window.framework.http.get("/sys_log/all", param);
return res;
}
async detail(param) {
let res = await window.framework.http.get("/sys_log/detail", param);
return res;
}
async delete(param) {
let res = await window.framework.http.get("/sys_log/delete", param);
return res;
}
async delete_all(param) {
let res = await window.framework.http.get("/sys_log/delete_all", param);
return res;
}
async operates(param) {
let res = await window.framework.http.get("/sys_log/operates", param);
return res;
}
}
const sys_log_serve = new SysLogServe();
export default sys_log_serve;

View File

@@ -0,0 +1,38 @@
class systemTypeClServer {
async all(param) {
let res= await window.framework.http.get('/sys_project_type/all', param);
return res;
}
async page(row) {
let res= await window.framework.http.post('/sys_project_type/page', row);
return res;
}
async exportCsv(row) {
let res = window.framework.http.fileExport("/sys_project_type/export", row);
return res;
}
async add(row) {
let res= await window.framework.http.post('/sys_project_type/add', row);
return res;
}
async edit(row) {
let res= await window.framework.http.post('/sys_project_type/edit', row);
return res;
}
async del(row) {
let res= await window.framework.http.post('/sys_project_type/del', row);
return res;
}
}
const systemTypeServer = new systemTypeClServer();
export default systemTypeServer;

View File

@@ -0,0 +1,31 @@
class TableServer {
async getAll(callback) {
return await window.framework.http.get('/table/index', {})
}
async add(row, callback) {
return await window.framework.http.post('/table/add', row)
}
async edit(row, callback) {
return await window.framework.http.post('/table/edit', row, function(res) {
callback && callback(res)
})
}
async del(row, callback) {
return await window.framework.http.post('/table/del', row)
}
async autoApi(id) {
return await window.framework.http.get('/template/api', { id: id })
}
async autoDb(id) {
return await window.framework.http.get('/template/autoDb', { id: id })
}
}
const tableServer = new TableServer()
export default tableServer

View File

@@ -0,0 +1,40 @@
class UserServer {
async login(row) {
let res = await window.framework.http.post("/sys_user/login", row);
return res;
}
async all() {
let res = await window.framework.http.get("/sys_user/index", {});
return res;
}
async exportCsv(row) {
let res = window.framework.http.fileExport("/sys_user/export", row);
return res;
}
async authorityMenus() {
let res = await window.framework.http.post("/sys_user/authorityMenus", {});
return res;
}
async add(row) {
let res = await window.framework.http.post("/sys_user/add", row);
return res;
}
async edit(row) {
let res = await window.framework.http.post("/sys_user/edit", row);
return res;
}
async del(row) {
let res = await window.framework.http.post("/sys_user/del", row);
return res;
}
}
const userServer = new UserServer();
export default userServer;

View File

@@ -0,0 +1,66 @@
<template>
<div v-show="isActive" class="tab-pane">
<slot></slot>
</div>
</template>
<script>
export default {
name: 'CustomTabPane',
props: {
label: {
type: String,
required: true
},
name: {
type: String,
required: true
}
},
data() {
return {
isActive: false,
parentTabs: null
}
},
mounted() {
this.findParentTabs()
if (this.parentTabs) {
this.parentTabs.registerTab({
label: this.label,
name: this.name
})
this.updateActiveState(this.parentTabs.activeTab)
}
},
beforeDestroy() {
if (this.parentTabs) {
this.parentTabs.unregisterTab(this.name)
}
},
methods: {
findParentTabs() {
let parent = this.$parent
while (parent) {
if (parent.$options.name === 'CustomTabs') {
this.parentTabs = parent
break
}
parent = parent.$parent
}
},
updateActiveState(activeTab) {
this.isActive = activeTab === this.name
}
}
}
</script>
<style scoped>
.tab-pane {
width: 100%;
overflow-x: auto;
overflow-y: visible;
}
</style>

View File

@@ -0,0 +1,118 @@
<template>
<div class="custom-tabs">
<div class="tabs-header">
<div
v-for="tab in tabs"
:key="tab.name"
:class="['tab-item', { active: activeTab === tab.name }]"
@click="handleTabClick(tab.name)">
{{ tab.label }}
</div>
</div>
<div class="tabs-content">
<slot></slot>
</div>
</div>
</template>
<script>
export default {
name: 'CustomTabs',
props: {
value: {
type: String,
default: ''
}
},
data() {
return {
activeTab: this.value || '',
tabs: []
}
},
watch: {
value(newVal) {
this.activeTab = newVal
this.updateChildren()
},
activeTab(newVal) {
this.updateChildren()
}
},
mounted() {
this.updateChildren()
},
methods: {
handleTabClick(name) {
this.activeTab = name
this.$emit('input', name)
this.$emit('change', name)
this.updateChildren()
},
registerTab(tab) {
if (!this.tabs.find(t => t.name === tab.name)) {
this.tabs.push(tab)
// 如果没有激活的tab设置第一个为激活
if (!this.activeTab && this.tabs.length > 0) {
this.activeTab = this.tabs[0].name
this.$emit('input', this.activeTab)
}
}
},
unregisterTab(tabName) {
const index = this.tabs.findIndex(t => t.name === tabName)
if (index > -1) {
this.tabs.splice(index, 1)
}
},
updateChildren() {
this.$children.forEach(child => {
if (child.$options.name === 'CustomTabPane') {
child.updateActiveState(this.activeTab)
}
})
}
}
}
</script>
<style scoped>
.custom-tabs {
width: 100%;
}
.tabs-header {
display: flex;
border-bottom: 1px solid #e8eaec;
background: #fff;
}
.tab-item {
padding: 12px 24px;
cursor: pointer;
color: #666;
font-size: 14px;
border-bottom: 2px solid transparent;
transition: all 0.3s;
user-select: none;
}
.tab-item:hover {
color: #333;
}
.tab-item.active {
color: #333;
border-bottom-color: #333;
font-weight: 500;
}
.tabs-content {
padding: 20px 0;
min-height: 400px;
overflow-x: auto;
overflow-y: visible;
width: 100%;
}
</style>

File diff suppressed because one or more lines are too long

39
admin/src/main.js Normal file
View File

@@ -0,0 +1,39 @@
// 引入 Admin Framework框架内部已包含所有依赖和样式
import AdminFramework from './framework/admin-framework.js'
import Vue from 'vue'
// 引入组件映射表
import componentMap from './router/component-map.js'
// 引入全局组件
import CustomTabs from './components/CustomTabs.vue'
import CustomTabPane from './components/CustomTabPane.vue'
import config from '../config/index.js'
const apiUrl = config.apiUrl
// 【超级简化】只需一个函数调用!
const app = AdminFramework.createApp({
title: '管理后台',
apiUrl: apiUrl, // API 地址uploadUrl 会自动设置为 apiUrl + 'upload'
componentMap: componentMap, // 传入组件映射表,用于动态路由
onReady() {
// 可选:应用启动完成后的回调
console.log('应用已准备就绪!')
}
})
// 注册全局组件
AdminFramework.registerComponents(Vue, {
'CustomTabs': CustomTabs,
'CustomTabPane': CustomTabPane
})
// 挂载应用
app.$mount('#app')

View File

@@ -0,0 +1,18 @@
// 组件映射表:后端菜单返回的 component 路径需与此处 key 一致(不含 .vue
import TestPage from '../views/test/test.vue'
import BizUsers from '../views/biz/biz_users.vue'
import BizPlans from '../views/biz/biz_plans.vue'
import BizSubscriptions from '../views/biz/biz_subscriptions.vue'
import BizTokens from '../views/biz/biz_tokens.vue'
import BizPayment from '../views/biz/biz_payment.vue'
const componentMap = {
'test/test': TestPage,
'biz/user': BizUsers,
'biz/plan': BizPlans,
'biz/subscription': BizSubscriptions,
'biz/token': BizTokens,
'biz/payment': BizPayment,
}
export default componentMap;

View File

@@ -0,0 +1,96 @@
<template>
<div class="biz-page">
<h2 class="biz-title">支付确认轻量</h2>
<p class="biz-desc"> <code>pending</code> 订阅置为 <code>active</code>并写入支付单号</p>
<Card dis-hover title="线下确认" style="max-width: 520px; margin-bottom: 16px">
<Form :label-width="110">
<FormItem label="订阅ID">
<Input v-model="offline.subscription_id" type="number" placeholder="biz_subscriptions.id" />
</FormItem>
<FormItem label="支付单号">
<Input v-model="offline.payment_ref" placeholder="流水号/凭证号" />
</FormItem>
<FormItem>
<Button type="primary" :loading="loading1" @click="doOffline">确认线下收款</Button>
</FormItem>
</Form>
</Card>
<Card dis-hover title="链接支付确认" style="max-width: 520px">
<Form :label-width="110">
<FormItem label="订阅ID">
<Input v-model="link.subscription_id" type="number" />
</FormItem>
<FormItem label="第三方单号">
<Input v-model="link.payment_ref" />
</FormItem>
<FormItem>
<Button type="primary" :loading="loading2" @click="doLink">确认链接支付</Button>
</FormItem>
</Form>
</Card>
</div>
</template>
<script>
import bizPaymentServer from '@/api/biz/biz_payment_server.js'
export default {
name: 'BizPayment',
data() {
return {
offline: { subscription_id: '', payment_ref: '' },
link: { subscription_id: '', payment_ref: '' },
loading1: false,
loading2: false,
}
},
methods: {
async doOffline() {
this.loading1 = true
try {
const res = await bizPaymentServer.confirmOffline({
subscription_id: Number(this.offline.subscription_id),
payment_ref: this.offline.payment_ref,
})
if (res && res.code === 0) {
this.$Message.success('已确认,订阅已激活')
} else {
this.$Message.error((res && res.message) || '失败')
}
} finally {
this.loading1 = false
}
},
async doLink() {
this.loading2 = true
try {
const res = await bizPaymentServer.confirmLink({
subscription_id: Number(this.link.subscription_id),
payment_ref: this.link.payment_ref,
})
if (res && res.code === 0) {
this.$Message.success('已确认,订阅已激活')
} else {
this.$Message.error((res && res.message) || '失败')
}
} finally {
this.loading2 = false
}
},
},
}
</script>
<style scoped>
.biz-page {
padding: 16px;
}
.biz-title {
margin: 0 0 8px;
font-size: 18px;
}
.biz-desc {
color: #666;
margin-bottom: 16px;
}
</style>

View File

@@ -0,0 +1,258 @@
<template>
<div class="biz-page">
<div class="biz-toolbar">
<h2 class="biz-title">套餐</h2>
<Button type="primary" @click="openEdit(null)">新增套餐</Button>
<Button class="ml8" @click="load(1)">刷新</Button>
</div>
<div class="biz-search">
<Form inline :label-width="70">
<FormItem label="条件">
<Select v-model="param.seachOption.key" style="width: 140px">
<Option value="plan_code">编码</Option>
<Option value="plan_name">名称</Option>
<Option value="status">状态</Option>
</Select>
<Input v-model="param.seachOption.value" class="ml8" style="width: 220px" search @on-search="load(1)" />
</FormItem>
<Button type="primary" @click="load(1)">查询</Button>
</Form>
</div>
<Table :columns="columns" :data="rows" border stripe />
<div class="biz-page-bar">
<Page
:total="total"
:current="param.pageOption.page"
:page-size="param.pageOption.pageSize"
show-total
@on-change="onPage"
@on-page-size-change="onSize"
/>
</div>
<Modal v-model="modal" :title="form.id ? '编辑套餐' : '新增套餐'" width="720" :loading="saving" @on-ok="save">
<Form ref="formRef" :model="form" :rules="rules" :label-width="120">
<FormItem label="套餐编码" prop="plan_code">
<Input v-model="form.plan_code" :disabled="!!form.id" />
</FormItem>
<FormItem label="套餐名称" prop="plan_name">
<Input v-model="form.plan_name" />
</FormItem>
<FormItem label="月费">
<Input v-model="form.monthly_price" type="number" />
</FormItem>
<FormItem label="授权费">
<Input v-model="form.auth_fee" type="number" />
</FormItem>
<FormItem label="账号上限">
<Input v-model="form.account_limit" type="number" />
</FormItem>
<FormItem label="活跃上限">
<Input v-model="form.active_user_limit" type="number" />
</FormItem>
<FormItem label="消息额度">
<Input v-model="form.msg_quota" type="number" />
</FormItem>
<FormItem label="群发额度">
<Input v-model="form.mass_quota" type="number" />
</FormItem>
<FormItem label="好友额度">
<Input v-model="form.friend_quota" type="number" />
</FormItem>
<FormItem label="朋友圈额度">
<Input v-model="form.sns_quota" type="number" />
</FormItem>
<FormItem label="功能点 JSON">
<Input v-model="featuresText" type="textarea" :rows="4" placeholder='如 {"msg":true} 或 ["msg","mass"]' />
</FormItem>
<FormItem label="状态" prop="status">
<Select v-model="form.status" style="width: 100%">
<Option value="active">上线</Option>
<Option value="inactive">下线</Option>
</Select>
</FormItem>
</Form>
</Modal>
</div>
</template>
<script>
import bizPlanServer from '@/api/biz/biz_plan_server.js'
export default {
name: 'BizPlans',
data() {
return {
rows: [],
total: 0,
param: {
seachOption: { key: 'plan_code', value: '' },
pageOption: { page: 1, pageSize: 20, total: 0 },
},
modal: false,
saving: false,
form: {},
featuresText: '{}',
rules: {
plan_code: [{ required: true, message: '必填', trigger: 'blur' }],
plan_name: [{ required: true, message: '必填', trigger: 'blur' }],
status: [{ required: true, message: '必选', trigger: 'change' }],
},
}
},
computed: {
columns() {
return [
{ title: 'ID', key: 'id', width: 70 },
{ title: '编码', key: 'plan_code', width: 120 },
{ title: '名称', key: 'plan_name', minWidth: 140 },
{ title: '月费', key: 'monthly_price', width: 90 },
{ title: '状态', key: 'status', width: 90 },
{
title: '操作',
key: 'a',
width: 260,
render: (h, p) =>
h('div', [
h('Button', { props: { type: 'info', size: 'small' }, on: { click: () => this.openEdit(p.row) } }, '编辑'),
h('Button', { props: { type: 'warning', size: 'small' }, class: { ml8: true }, on: { click: () => this.toggle(p.row) } }, '上下线'),
h('Button', { props: { type: 'error', size: 'small' }, class: { ml8: true }, on: { click: () => this.doDel(p.row) } }, '删除'),
]),
},
]
},
},
mounted() {
this.load(1)
},
methods: {
async load(page) {
if (page) this.param.pageOption.page = page
const res = await bizPlanServer.page({ param: this.param })
if (res && res.code === 0) {
this.rows = res.data.rows || []
this.total = res.data.count || 0
} else {
this.$Message.error((res && res.message) || '加载失败')
}
},
onPage(p) {
this.param.pageOption.page = p
this.load()
},
onSize(s) {
this.param.pageOption.pageSize = s
this.load(1)
},
openEdit(row) {
if (row) {
this.form = { ...row }
this.featuresText =
row.enabled_features == null
? ''
: typeof row.enabled_features === 'string'
? row.enabled_features
: JSON.stringify(row.enabled_features, null, 2)
} else {
this.form = {
plan_code: '',
plan_name: '',
monthly_price: 0,
auth_fee: 0,
account_limit: 0,
active_user_limit: 0,
msg_quota: 0,
mass_quota: 0,
friend_quota: 0,
sns_quota: 0,
status: 'active',
}
this.featuresText = '{}'
}
this.modal = true
},
save() {
this.saving = true
this.$refs.formRef.validate(async (ok) => {
if (!ok) {
this.saving = false
return
}
let enabled_features = null
const t = (this.featuresText || '').trim()
if (t) {
try {
enabled_features = JSON.parse(t)
} catch (e) {
this.$Message.error('功能点 JSON 格式错误')
this.saving = false
return
}
}
const payload = { ...this.form, enabled_features }
try {
const res = this.form.id ? await bizPlanServer.edit(payload) : await bizPlanServer.add(payload)
if (res && res.code === 0) {
this.$Message.success('保存成功')
this.modal = false
this.load(1)
} else {
this.$Message.error((res && res.message) || '保存失败')
}
} finally {
this.saving = false
}
})
},
async toggle(row) {
const res = await bizPlanServer.toggle({ id: row.id })
if (res && res.code === 0) {
this.$Message.success('状态已更新为 ' + (res.data && res.data.status))
this.load()
} else {
this.$Message.error((res && res.message) || '失败')
}
},
doDel(row) {
this.$Modal.confirm({
title: '删除套餐',
content: '确认删除?若已被订阅引用可能失败。',
onOk: async () => {
const res = await bizPlanServer.del({ id: row.id })
if (res && res.code === 0) {
this.$Message.success('已删除')
this.load(1)
} else {
this.$Message.error((res && res.message) || '删除失败')
}
},
})
},
},
}
</script>
<style scoped>
.biz-page {
padding: 16px;
}
.biz-toolbar {
margin-bottom: 12px;
}
.biz-title {
display: inline-block;
margin: 0 16px 0 0;
font-size: 18px;
vertical-align: middle;
}
.ml8 {
margin-left: 8px;
}
.biz-search {
margin-bottom: 12px;
}
.biz-page-bar {
margin-top: 12px;
text-align: right;
}
</style>

View File

@@ -0,0 +1,280 @@
<template>
<div class="biz-page">
<div class="biz-toolbar">
<h2 class="biz-title">订阅</h2>
<Button type="primary" @click="openOpen">开通订阅</Button>
<Button class="ml8" @click="load(1)">刷新</Button>
</div>
<div class="biz-search">
<Form inline>
<FormItem label="用户ID">
<Input v-model="param.seachOption.value" style="width: 140px" placeholder="筛选 user_id" />
</FormItem>
<FormItem>
<Select v-model="param.seachOption.key" style="width: 120px">
<Option value="user_id">用户ID</Option>
<Option value="status">状态</Option>
</Select>
</FormItem>
<Button type="primary" @click="load(1)">查询</Button>
</Form>
</div>
<Table :columns="columns" :data="rows" border stripe />
<div class="biz-page-bar">
<Page
:total="total"
:current="param.pageOption.page"
:page-size="param.pageOption.pageSize"
show-total
@on-change="onPage"
@on-page-size-change="onSize"
/>
</div>
<Modal v-model="openModal" title="开通订阅" width="640" :loading="saving" @on-ok="submitOpen">
<Form :label-width="110">
<FormItem label="用户ID"><Input v-model="openForm.user_id" type="number" /></FormItem>
<FormItem label="套餐ID"><Input v-model="openForm.plan_id" type="number" /></FormItem>
<FormItem label="开始时间"><Input v-model="openForm.start_time" placeholder="2025-01-01 00:00:00" /></FormItem>
<FormItem label="结束时间"><Input v-model="openForm.end_time" placeholder="2025-12-31 23:59:59" /></FormItem>
<FormItem label="状态">
<Select v-model="openForm.status" style="width: 100%">
<Option value="pending">pending</Option>
<Option value="active">active</Option>
</Select>
</FormItem>
<FormItem label="续费方式">
<Select v-model="openForm.renew_mode" style="width: 100%">
<Option value="manual">manual</Option>
<Option value="auto">auto</Option>
</Select>
</FormItem>
<FormItem label="支付渠道">
<Select v-model="openForm.payment_channel" clearable style="width: 100%">
<Option value="offline">offline</Option>
<Option value="pay_link">pay_link</Option>
</Select>
</FormItem>
<FormItem label="支付单号"><Input v-model="openForm.payment_ref" /></FormItem>
</Form>
</Modal>
<Modal v-model="renewModal" title="续费" :loading="saving" @on-ok="submitRenew">
<Form :label-width="100">
<FormItem label="新结束时间"><Input v-model="renewForm.end_time" /></FormItem>
</Form>
</Modal>
<Modal v-model="upgradeModal" title="升级套餐" :loading="saving" @on-ok="submitUpgrade">
<Form :label-width="100">
<FormItem label="新套餐ID"><Input v-model="upgradeForm.new_plan_id" type="number" /></FormItem>
<FormItem label="开始"><Input v-model="upgradeForm.start_time" /></FormItem>
<FormItem label="结束"><Input v-model="upgradeForm.end_time" /></FormItem>
</Form>
</Modal>
</div>
</template>
<script>
import bizSubscriptionServer from '@/api/biz/biz_subscription_server.js'
export default {
name: 'BizSubscriptions',
data() {
return {
rows: [],
total: 0,
param: {
seachOption: { key: 'user_id', value: '' },
pageOption: { page: 1, pageSize: 20, total: 0 },
},
openModal: false,
renewModal: false,
upgradeModal: false,
saving: false,
currentRow: null,
openForm: {},
renewForm: { end_time: '' },
upgradeForm: { new_plan_id: '', start_time: '', end_time: '' },
}
},
computed: {
columns() {
return [
{ title: 'ID', key: 'id', width: 70 },
{ title: '用户', key: 'user_id', width: 90 },
{ title: '套餐', key: 'plan_id', width: 90 },
{ title: '状态', key: 'status', width: 100 },
{ title: '开始', key: 'start_time', minWidth: 150 },
{ title: '结束', key: 'end_time', minWidth: 150 },
{
title: '操作',
key: 'a',
width: 200,
render: (h, p) =>
h('div', [
h('Button', { props: { size: 'small' }, on: { click: () => this.openRenew(p.row) } }, '续费'),
h('Button', { props: { size: 'small' }, class: { ml8: true }, on: { click: () => this.openUpgrade(p.row) } }, '升级'),
h('Button', { props: { type: 'error', size: 'small' }, class: { ml8: true }, on: { click: () => this.doCancel(p.row) } }, '取消'),
]),
},
]
},
},
mounted() {
this.load(1)
},
methods: {
async load(page) {
if (page) this.param.pageOption.page = page
const res = await bizSubscriptionServer.page({ param: this.param })
if (res && res.code === 0) {
this.rows = res.data.rows || []
this.total = res.data.count || 0
} else {
this.$Message.error((res && res.message) || '加载失败')
}
},
onPage(p) {
this.param.pageOption.page = p
this.load()
},
onSize(s) {
this.param.pageOption.pageSize = s
this.load(1)
},
openOpen() {
const now = new Date()
const end = new Date(now)
end.setFullYear(end.getFullYear() + 1)
const fmt = (d) =>
`${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')} ${String(
d.getHours()
).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}:00`
this.openForm = {
user_id: '',
plan_id: '',
start_time: fmt(now),
end_time: fmt(end),
status: 'pending',
renew_mode: 'manual',
payment_channel: '',
payment_ref: '',
}
this.openModal = true
},
async submitOpen() {
this.saving = true
try {
const body = {
user_id: Number(this.openForm.user_id),
plan_id: Number(this.openForm.plan_id),
start_time: this.openForm.start_time,
end_time: this.openForm.end_time,
status: this.openForm.status,
renew_mode: this.openForm.renew_mode,
payment_channel: this.openForm.payment_channel || null,
payment_ref: this.openForm.payment_ref || null,
}
const res = await bizSubscriptionServer.open(body)
if (res && res.code === 0) {
this.$Message.success('已创建订阅')
this.openModal = false
this.load(1)
} else {
this.$Message.error((res && res.message) || '失败')
}
} finally {
this.saving = false
}
},
openRenew(row) {
this.currentRow = row
this.renewForm = { end_time: row.end_time || '' }
this.renewModal = true
},
async submitRenew() {
if (!this.currentRow) return
this.saving = true
try {
const res = await bizSubscriptionServer.renew({
subscription_id: this.currentRow.id,
end_time: this.renewForm.end_time,
})
if (res && res.code === 0) {
this.$Message.success('已续费')
this.renewModal = false
this.load(1)
} else {
this.$Message.error((res && res.message) || '失败')
}
} finally {
this.saving = false
}
},
openUpgrade(row) {
this.currentRow = row
this.upgradeForm = { new_plan_id: row.plan_id, start_time: '', end_time: '' }
this.upgradeModal = true
},
async submitUpgrade() {
if (!this.currentRow) return
this.saving = true
try {
const res = await bizSubscriptionServer.upgrade({
subscription_id: this.currentRow.id,
new_plan_id: Number(this.upgradeForm.new_plan_id),
start_time: this.upgradeForm.start_time || undefined,
end_time: this.upgradeForm.end_time || undefined,
})
if (res && res.code === 0) {
this.$Message.success('已升级')
this.upgradeModal = false
this.load(1)
} else {
this.$Message.error((res && res.message) || '失败')
}
} finally {
this.saving = false
}
},
doCancel(row) {
this.$Modal.confirm({
title: '取消订阅',
content: '确认取消?',
onOk: async () => {
const res = await bizSubscriptionServer.cancel({ subscription_id: row.id })
if (res && res.code === 0) {
this.$Message.success('已取消')
this.load(1)
} else {
this.$Message.error((res && res.message) || '失败')
}
},
})
},
},
}
</script>
<style scoped>
.biz-page {
padding: 16px;
}
.biz-toolbar {
margin-bottom: 12px;
}
.biz-title {
display: inline-block;
margin: 0 16px 0 0;
font-size: 18px;
vertical-align: middle;
}
.ml8 {
margin-left: 8px;
}
.biz-page-bar {
margin-top: 12px;
text-align: right;
}
</style>

View File

@@ -0,0 +1,193 @@
<template>
<div class="biz-page">
<div class="biz-toolbar">
<h2 class="biz-title">API Token</h2>
<Button type="primary" @click="openCreate">创建 Token</Button>
<Button class="ml8" @click="load(1)">刷新</Button>
</div>
<div class="biz-search">
<Form inline>
<FormItem label="用户ID">
<Input v-model="param.seachOption.value" style="width: 140px" />
</FormItem>
<FormItem>
<Select v-model="param.seachOption.key" style="width: 120px">
<Option value="user_id">用户ID</Option>
<Option value="status">状态</Option>
</Select>
</FormItem>
<Button type="primary" @click="load(1)">查询</Button>
</Form>
</div>
<Table :columns="columns" :data="rows" border stripe />
<div class="biz-page-bar">
<Page
:total="total"
:current="param.pageOption.page"
:page-size="param.pageOption.pageSize"
show-total
@on-change="onPage"
@on-page-size-change="onSize"
/>
</div>
<Modal v-model="createModal" title="创建 Token" width="560" :loading="saving" @on-ok="submitCreate">
<Form :label-width="100">
<FormItem label="用户ID"><Input v-model="createForm.user_id" type="number" /></FormItem>
<FormItem label="名称"><Input v-model="createForm.token_name" placeholder="default" /></FormItem>
<FormItem label="过期时间"><Input v-model="createForm.expire_at" placeholder="2026-12-31 23:59:59" /></FormItem>
</Form>
</Modal>
<Modal v-model="plainModal" title="请立即保存 Token 明文" width="560" :closable="false">
<Alert type="error">仅此一次展示关闭后无法再次查看明文</Alert>
<Input type="textarea" :rows="4" v-model="plainToken" readonly />
<div slot="footer">
<Button type="primary" @click="plainModal = false">已保存</Button>
</div>
</Modal>
</div>
</template>
<script>
import bizTokenServer from '@/api/biz/biz_token_server.js'
export default {
name: 'BizTokens',
data() {
return {
rows: [],
total: 0,
param: {
seachOption: { key: 'user_id', value: '' },
pageOption: { page: 1, pageSize: 20, total: 0 },
},
createModal: false,
plainModal: false,
plainToken: '',
saving: false,
createForm: {},
}
},
computed: {
columns() {
return [
{ title: 'ID', key: 'id', width: 70 },
{ title: '用户', key: 'user_id', width: 90 },
{ title: '套餐', key: 'plan_id', width: 90 },
{ title: '名称', key: 'token_name', width: 120 },
{ title: '状态', key: 'status', width: 90 },
{ title: '过期', key: 'expire_at', minWidth: 150 },
{ title: '最后使用', key: 'last_used_at', minWidth: 150 },
{
title: '操作',
key: 'a',
width: 100,
render: (h, p) =>
h(
'Button',
{
props: { type: 'error', size: 'small' },
on: {
click: () => this.doRevoke(p.row),
},
},
'吊销'
),
},
]
},
},
mounted() {
this.load(1)
},
methods: {
async load(page) {
if (page) this.param.pageOption.page = page
const res = await bizTokenServer.page({ param: this.param })
if (res && res.code === 0) {
this.rows = res.data.rows || []
this.total = res.data.count || 0
} else {
this.$Message.error((res && res.message) || '加载失败')
}
},
onPage(p) {
this.param.pageOption.page = p
this.load()
},
onSize(s) {
this.param.pageOption.pageSize = s
this.load(1)
},
openCreate() {
const d = new Date()
d.setFullYear(d.getFullYear() + 1)
const fmt = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(
2,
'0'
)} 23:59:59`
this.createForm = { user_id: '', token_name: 'default', expire_at: fmt }
this.createModal = true
},
async submitCreate() {
this.saving = true
try {
const res = await bizTokenServer.create({
user_id: Number(this.createForm.user_id),
token_name: this.createForm.token_name || 'default',
expire_at: this.createForm.expire_at,
})
if (res && res.code === 0) {
if (res.data.warn) this.$Message.warning(res.data.warn)
this.createModal = false
this.plainToken = res.data.plain_token
this.plainModal = true
this.load(1)
} else {
this.$Message.error((res && res.message) || '创建失败')
}
} finally {
this.saving = false
}
},
doRevoke(row) {
this.$Modal.confirm({
title: '吊销 Token',
content: '确认吊销?',
onOk: async () => {
const res = await bizTokenServer.revoke({ id: row.id })
if (res && res.code === 0) {
this.$Message.success('已吊销')
this.load(1)
} else {
this.$Message.error((res && res.message) || '失败')
}
},
})
},
},
}
</script>
<style scoped>
.biz-page {
padding: 16px;
}
.biz-toolbar {
margin-bottom: 12px;
}
.biz-title {
display: inline-block;
margin: 0 16px 0 0;
font-size: 18px;
vertical-align: middle;
}
.ml8 {
margin-left: 8px;
}
.biz-page-bar {
margin-top: 12px;
text-align: right;
}
</style>

View File

@@ -0,0 +1,270 @@
<template>
<div class="biz-page">
<div class="biz-toolbar">
<h2 class="biz-title">业务用户</h2>
<Button type="primary" @click="openEdit(null)">新增</Button>
<Button class="ml8" @click="load(1)">刷新</Button>
</div>
<div class="biz-search">
<Form inline :label-width="70">
<FormItem label="条件">
<Select v-model="param.seachOption.key" style="width: 140px">
<Option value="mobile">手机</Option>
<Option value="company_name">公司</Option>
<Option value="status">状态</Option>
</Select>
<Input v-model="param.seachOption.value" placeholder="关键字" style="width: 220px" class="ml8" search @on-search="load(1)" />
</FormItem>
<FormItem>
<Button type="primary" @click="load(1)">查询</Button>
</FormItem>
</Form>
</div>
<Table :columns="columns" :data="rows" border stripe />
<div class="biz-page-bar">
<Page
:total="total"
:current="param.pageOption.page"
:page-size="param.pageOption.pageSize"
show-total
@on-change="onPage"
@on-page-size-change="onSize"
/>
</div>
<Modal v-model="modal" :title="form.id ? '编辑用户' : '新增用户'" width="640" :loading="saving" @on-ok="save">
<Form ref="formRef" :model="form" :rules="rules" :label-width="100">
<FormItem label="名称" prop="name">
<Input v-model="form.name" />
</FormItem>
<FormItem label="手机" prop="mobile">
<Input v-model="form.mobile" />
</FormItem>
<FormItem label="邮箱">
<Input v-model="form.email" />
</FormItem>
<FormItem label="公司">
<Input v-model="form.company_name" />
</FormItem>
<FormItem label="状态" prop="status">
<Select v-model="form.status" style="width: 100%">
<Option value="active">正常</Option>
<Option value="disabled">禁用</Option>
</Select>
</FormItem>
</Form>
</Modal>
<Modal v-model="detailVisible" title="用户详情" width="720" footer-hide>
<p v-if="detail">Token 数量{{ detail.tokenCount }}</p>
<Table v-if="detail && detail.subscriptions" :columns="subCols" :data="detail.subscriptions" size="small" border />
</Modal>
</div>
</template>
<script>
import bizUserServer from '@/api/biz/biz_user_server.js'
export default {
name: 'BizUsers',
data() {
return {
rows: [],
total: 0,
param: {
seachOption: { key: 'mobile', value: '' },
pageOption: { page: 1, pageSize: 20, total: 0 },
},
modal: false,
saving: false,
form: {},
rules: {
name: [{ required: true, message: '请输入名称', trigger: 'blur' }],
status: [{ required: true, message: '请选择状态', trigger: 'change' }],
},
detailVisible: false,
detail: null,
subCols: [
{ title: 'ID', key: 'id', width: 80 },
{ title: '套餐ID', key: 'plan_id', width: 90 },
{ title: '状态', key: 'status', width: 100 },
{ title: '开始', key: 'start_time', minWidth: 160 },
{ title: '结束', key: 'end_time', minWidth: 160 },
],
}
},
computed: {
columns() {
return [
{ title: 'ID', key: 'id', width: 80 },
{ title: '名称', key: 'name', minWidth: 120 },
{ title: '手机', key: 'mobile', width: 130 },
{ title: '公司', key: 'company_name', minWidth: 140 },
{ title: '状态', key: 'status', width: 90 },
{
title: '操作',
key: 'a',
width: 220,
render: (h, p) => {
return h('div', [
h(
'Button',
{
props: { type: 'info', size: 'small' },
on: { click: () => this.openEdit(p.row) },
},
'编辑'
),
h(
'Button',
{
props: { type: 'default', size: 'small' },
class: { ml8: true },
on: { click: () => this.showDetail(p.row) },
},
'详情'
),
h(
'Button',
{
props: { type: 'warning', size: 'small' },
class: { ml8: true },
on: { click: () => this.doDisable(p.row) },
},
'禁用'
),
h(
'Button',
{
props: { type: 'error', size: 'small' },
class: { ml8: true },
on: { click: () => this.doDel(p.row) },
},
'删除'
),
])
},
},
]
},
},
mounted() {
this.load(1)
},
methods: {
async load(page) {
if (page) this.param.pageOption.page = page
const res = await bizUserServer.page({ param: this.param })
if (res && res.code === 0) {
this.rows = res.data.rows || []
this.total = res.data.count || 0
} else {
this.$Message.error((res && res.message) || '加载失败')
}
},
onPage(p) {
this.param.pageOption.page = p
this.load()
},
onSize(s) {
this.param.pageOption.pageSize = s
this.load(1)
},
openEdit(row) {
if (row) {
this.form = { ...row }
} else {
this.form = { name: '', mobile: '', email: '', company_name: '', status: 'active' }
}
this.modal = true
},
save() {
this.saving = true
this.$refs.formRef.validate(async (ok) => {
if (!ok) {
this.saving = false
return
}
try {
const res = this.form.id
? await bizUserServer.edit(this.form)
: await bizUserServer.add(this.form)
if (res && res.code === 0) {
this.$Message.success('保存成功')
this.modal = false
this.load(1)
} else {
this.$Message.error((res && res.message) || '保存失败')
}
} finally {
this.saving = false
}
})
},
async showDetail(row) {
const res = await bizUserServer.detail(row.id)
if (res && res.code === 0) {
this.detail = res.data
this.detailVisible = true
} else {
this.$Message.error((res && res.message) || '加载详情失败')
}
},
doDisable(row) {
this.$Modal.confirm({
title: '禁用用户',
content: '确认禁用该用户?',
onOk: async () => {
const res = await bizUserServer.disable({ id: row.id })
if (res && res.code === 0) {
this.$Message.success('已禁用')
this.load(1)
} else {
this.$Message.error((res && res.message) || '操作失败')
}
},
})
},
doDel(row) {
this.$Modal.confirm({
title: '删除用户',
content: '确认删除?若存在订阅/Token 可能受外键限制。',
onOk: async () => {
const res = await bizUserServer.del({ id: row.id })
if (res && res.code === 0) {
this.$Message.success('已删除')
this.load(1)
} else {
this.$Message.error((res && res.message) || '删除失败')
}
},
})
},
},
}
</script>
<style scoped>
.biz-page {
padding: 16px;
}
.biz-toolbar {
margin-bottom: 12px;
}
.biz-title {
display: inline-block;
margin: 0 16px 0 0;
font-size: 18px;
vertical-align: middle;
}
.ml8 {
margin-left: 8px;
}
.biz-search {
margin-bottom: 12px;
}
.biz-page-bar {
margin-top: 12px;
text-align: right;
}
</style>

View File

@@ -0,0 +1,35 @@
<template>
<div class="test-page">
<h2 class="test-page__title">Test</h2>
<p class="test-page__desc">基础测试页面动态菜单的 component 请填 <code>test/test</code></p>
</div>
</template>
<script>
export default {
name: 'TestPage'
}
</script>
<style scoped>
.test-page {
padding: 24px;
}
.test-page__title {
margin: 0 0 12px;
font-size: 18px;
font-weight: 600;
}
.test-page__desc {
margin: 0;
color: #666;
font-size: 14px;
line-height: 1.6;
}
.test-page__desc code {
padding: 2px 6px;
font-size: 13px;
background: #f5f5f5;
border-radius: 4px;
}
</style>