feat:新增授权管理

This commit is contained in:
13895237362 committed 2025-10-16 16:00:30 +08:00
1 parent a6b38c0fdc
commit c9fed52b29
11 files changed
+176 -414

No files matched your search

+3 -2
View File
@@ -1,9 +1,10 @@
{
"permissions": {
"allow": [
"WebFetch(domain:www.figma.com)"
"WebFetch(domain:www.figma.com)",
"Bash(git checkout:*)"
],
"deny": [],
"ask": []
}
}
}
+57 -33
View File
@@ -1,6 +1,24 @@
import Axios from './https'
// 授权码(AuthorizationCode)类型 - 供授权管理页面使用
export interface AuthorizationCode {
id: string;
code: string;
customer_id?: string;
customer_name?: string;
description?: string;
status: 'normal' | 'locked' | 'expired';
status_display?: string;
created_at: string;
updated_at?: string;
start_date?: string;
end_date?: string;
is_locked?: boolean;
max_activations?: number;
current_activations?: number;
deployment_type_display?: string; // 部署类型显示文本
}
// 授权接口类型定义
// 许可证(License)类型 - 供其他页面使用(保持兼容)
export interface License {
id: string;
license_code: string;
@@ -29,24 +47,20 @@ export interface License {
export interface LicenseQueryRequest {
page?: number;
page_size?: number;
search?: string;
customer_id?: string;
status?: 'active' | 'inactive' | 'expired';
license_type?: string;
sort?: 'created_at' | 'updated_at' | 'expiry_date' | 'activation_date';
status?: 'normal' | 'locked' | 'expired';
sort?: 'created_at' | 'updated_at' | 'code';
order?: 'asc' | 'desc';
start_date?: string;
end_date?: string;
}
// 创建授权请求参数
export interface LicenseCreateRequest {
export interface AuthorizationCodeCreateRequest {
customer_id: string;
description: string;
license_type: string;
expiry_date: string;
license_type?: string;
expiry_date?: string;
max_users?: number;
features: string[];
features?: string[];
ip_restrictions?: string[];
hardware_id?: string;
}
@@ -77,7 +91,7 @@ export interface LicenseQueryResponse {
message: string;
timestamp: string;
data: {
list: License[];
list: AuthorizationCode[];
total: number;
page: number;
page_size: number;
@@ -97,56 +111,52 @@ export interface ApiResponse<T = any> {
* 查询授权列表
*/
export function getLicenses(params?: LicenseQueryRequest): Promise<LicenseQueryResponse> {
return Axios.get('/api/licenses', { params })
return Axios.get('/api/v1/authorization-codes', { params })
}
/**
* 获取授权详情
*/
export function getLicenseDetail(id: string): Promise<ApiResponse<License>> {
return Axios.get(`/api/licenses/${id}`)
export function getLicenseDetail(id: string): Promise<ApiResponse<AuthorizationCode>> {
return Axios.get(`/api/v1/authorization-codes/${id}`)
}
/**
* 创建授权
*/
export function createLicense(data: LicenseCreateRequest): Promise<ApiResponse<License>> {
return Axios.post('/api/licenses', data)
export function createLicense(data: AuthorizationCodeCreateRequest): Promise<ApiResponse<AuthorizationCode>> {
return Axios.post('/api/v1/authorization-codes', data)
}
/**
* 更新授权
*/
export function updateLicense(id: string, data: LicenseUpdateRequest): Promise<ApiResponse<License>> {
return Axios.put(`/api/licenses/${id}`, data)
export function updateLicense(id: string, data: LicenseUpdateRequest): Promise<ApiResponse<AuthorizationCode>> {
return Axios.put(`/api/v1/authorization-codes/${id}`, data)
}
/**
* 删除授权
*/
export function deleteLicense(id: string): Promise<ApiResponse> {
return Axios.delete(`/api/licenses/${id}`)
return Axios.delete(`/api/v1/authorization-codes/${id}`)
}
/**
* 激活授权
* 锁定/解锁授权码
*/
export function activateLicense(id: string): Promise<ApiResponse<License>> {
return Axios.patch(`/api/licenses/${id}/activate`)
export function lockAuthorizationCode(
id: string,
data: { is_locked: boolean; lock_reason?: string; reason?: string }
): Promise<ApiResponse<AuthorizationCode>> {
return Axios.put(`/api/v1/authorization-codes/${id}/lock`, data)
}
/**
* 停用授权
*/
export function deactivateLicense(id: string): Promise<ApiResponse<License>> {
return Axios.patch(`/api/licenses/${id}/deactivate`)
}
/**
* 获取授权统计数据
*/
export function getLicenseStats(): Promise<ApiResponse<LicenseStats>> {
return Axios.get('/api/licenses/stats')
return Axios.get('/api/v1/authorization-codes/stats')
}
/**
@@ -159,6 +169,20 @@ export function getLicensesByCustomer(customerId: string, params?: Omit<LicenseQ
/**
* 续期授权
*/
export function renewLicense(id: string, expiry_date: string): Promise<ApiResponse<License>> {
return Axios.patch(`/api/licenses/${id}/renew`, { expiry_date })
export function renewLicense(id: string, expiry_date: string): Promise<ApiResponse<AuthorizationCode>> {
return Axios.patch(`/api/v1/authorization-codes/${id}/renew`, { expiry_date })
}
/**
* 激活授权
*/
export function activateLicense(id: string): Promise<ApiResponse<AuthorizationCode>> {
return Axios.patch(`/api/v1/authorization-codes/${id}/activate`)
}
/**
* 停用授权
*/
export function deactivateLicense(id: string): Promise<ApiResponse<AuthorizationCode>> {
return Axios.patch(`/api/v1/authorization-codes/${id}/deactivate`)
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 178 KiB

@@ -78,7 +78,8 @@ const router = useRouter()
const navItems = computed(() => {
return defaultNavItems.value.map(item => ({
...item,
active: route.path === item.href
// 对于有子路由的项目,匹配路径前缀
active: route.path === item.href || route.path.startsWith(item.href + '/')
}))
})
+2
View File
@@ -63,6 +63,8 @@
"dashboard": "Dashboard",
"customers": "Customer Management",
"licenses": "License Management",
"licenseList": "License List",
"createLicense": "Create License",
"roles": "Roles & Permissions",
"users": "System Users",
"login": "Login",
+2
View File
@@ -63,6 +63,8 @@
"dashboard": "ダッシュボード",
"customers": "顧客管理",
"licenses": "ライセンス管理",
"licenseList": "ライセンス一覧",
"createLicense": "ライセンス作成",
"roles": "ロールと権限",
"users": "システムユーザー",
"login": "ログイン",
+2
View File
@@ -63,6 +63,8 @@
"dashboard": "仪表盘",
"customers": "客户管理",
"licenses": "授权管理",
"licenseList": "授权列表",
"createLicense": "创建授权",
"roles": "角色权限",
"users": "系统用户",
"login": "登录",
+21 -5
View File
@@ -2,7 +2,7 @@
* @Author: 13895237362 2205451508@qq.com
* @Date: 2025-08-01 09:32:42
* @LastEditors: 13895237362 2205451508@qq.com
* @LastEditTime: 2025-08-14 09:10:33
* @LastEditTime: 2025-10-13 16:00:25
* @FilePath: /frontend/src/router/index.ts
* @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
*/
@@ -22,7 +22,7 @@ const routes: Array<RouteRecordRaw> = [
{
path: "/dashboard",
component: () => import("@/views/Dashboard.vue"),
meta: { title: "仪表板", requiresAuth: true }
meta: { title: "仪表盘", requiresAuth: true }
},
{
path: "/customers",
@@ -31,8 +31,23 @@ const routes: Array<RouteRecordRaw> = [
},
{
path: "/licenses",
name: "licenses",
component: () => import("@/views/Licenses/index.vue"),
meta: { title: "授权管理", requiresAuth: true }
meta: { title: "授权管理", requiresAuth: true },
children: [
{
path: "list",
name: "licenses-list",
component: () => import("@/views/Licenses/LinenseList.vue"),
meta: { title: "授权列表", requiresAuth: true }
},
{
path: "create",
name: "licenses-create",
component: () => import("@/views/Licenses/LicenseForm.vue"),
meta: { title: "创建授权", requiresAuth: true }
}
]
},
{
path: "/roles",
@@ -56,7 +71,7 @@ router.beforeEach((to, _from, next) => {
// 设置页面标题
document.title = typeof (to.meta.title) === "string" ? to.meta.title : "授权管理平台";
// 检查是否需要认证
// 检查是否需要认�?
if (to.meta.requiresAuth) {
const token = localStorage.getItem("token");
if (!token) {
@@ -68,4 +83,5 @@ router.beforeEach((to, _from, next) => {
next();
});
export default router;
export default router;
+35 -1
View File
@@ -36,7 +36,41 @@ export function useBreadcrumb() {
// 生成面包屑
const breadcrumbs = computed<BreadcrumbItem[]>(() => {
const currentPath = route.path
// 特殊处理授权管理子路由
if (currentPath.startsWith('/licenses/')) {
const items: BreadcrumbItem[] = []
// 添加授权管理作为父级
items.push({
title: t('navigation.breadcrumb.licenses'),
path: '/licenses'
})
// 如果有客户名称查询参数,显示客户名称
if (route.query.customerName && typeof route.query.customerName === 'string') {
items.push({
title: route.query.customerName
})
} else {
// 否则显示当前路由的标题
const pathSegments = currentPath.split('/').filter(Boolean)
const lastSegment = pathSegments[pathSegments.length - 1]
if (lastSegment === 'list') {
items.push({
title: t('navigation.breadcrumb.licenseList')
})
} else if (lastSegment === 'create') {
items.push({
title: t('navigation.breadcrumb.createLicense')
})
}
}
return items
}
// 从配置中获取面包屑
const configKeys = routeBreadcrumbKeys[currentPath]
if (configKeys) {
+52 -372
View File
@@ -2,7 +2,7 @@
* @Author: 13895237362 2205451508@qq.com
* @Date: 2025-08-12 00:00:00
* @LastEditors: 13895237362 2205451508@qq.com
* @LastEditTime: 2025-09-29 17:29:42
* @LastEditTime: 2025-10-16 15:29:00
* @FilePath: /frontend/src/views/Licenses.vue
* @Description: 授权管理页面
-->
@@ -10,7 +10,7 @@
<Layout app-name="Cedar-V" :page-title="t('pages.licenses.title')">
<div class="license-container">
<!-- 主要内容区域 -->
<div class="main-content">
<div v-if="!showSubRoute" class="main-content">
<!-- 背景图片 -->
<div class="background-section">
<!-- 中央标题 -->
@@ -70,223 +70,40 @@
</div>
</div>
<!-- 授权列表区域 -->
<div v-if="showLicenseList" class="license-list-section">
<div class="list-header">
<div class="search-controls">
<el-input
v-model="searchText"
:placeholder="t('pages.licenses.search.placeholder')"
prefix-icon="Search"
clearable
size="large"
class="search-input"
@input="handleSearch"
/>
</div>
<el-date-picker
v-model="dateRange"
type="daterange"
:range-separator="t('pages.licenses.search.dateRange')"
:start-placeholder="t('pages.licenses.search.startDate')"
:end-placeholder="t('pages.licenses.search.endDate')"
size="large"
class="date-picker"
@change="handleDateRangeChange"
/>
<el-button
type="primary"
size="large"
@click="refreshLicenseList"
>
{{ t('pages.licenses.actions.refresh') }}
</el-button>
</div>
</div>
<!-- 授权表格 -->
<div class="table-container">
<el-table
:data="licenseList"
v-loading="loading"
:element-loading-text="t('pages.licenses.table.loading')"
stripe
style="width: 100%"
class="license-table"
>
<el-table-column
prop="license_code"
:label="t('pages.licenses.table.licenseCode')"
width="200"
show-overflow-tooltip
/>
<el-table-column
prop="customer_name"
:label="t('pages.licenses.table.customerName')"
width="180"
show-overflow-tooltip
/>
<el-table-column
prop="description"
:label="t('pages.licenses.table.description')"
min-width="200"
show-overflow-tooltip
/>
<el-table-column
prop="status"
:label="t('pages.licenses.table.status')"
width="120"
>
<template #default="{ row }">
<el-tag
:type="getStatusType(row.status)"
effect="plain"
>
{{ getStatusText(row.status) }}
</el-tag>
</template>
</el-table-column>
<el-table-column
prop="license_type_display"
:label="t('pages.licenses.table.licenseType')"
width="140"
show-overflow-tooltip
/>
<el-table-column
prop="expiry_date"
:label="t('pages.licenses.table.expiryDate')"
width="180"
>
<template #default="{ row }">
{{ formatDate(row.expiry_date) }}
</template>
</el-table-column>
<el-table-column
prop="created_at"
:label="t('pages.licenses.table.createTime')"
width="180"
>
<template #default="{ row }">
{{ formatDate(row.created_at) }}
</template>
</el-table-column>
<el-table-column
:label="t('pages.licenses.table.operation')"
width="200"
fixed="right"
>
<template #default="{ row }">
<el-button
type="primary"
size="small"
@click="handleEdit(row)"
>
{{ t('pages.licenses.actions.edit') }}
</el-button>
<el-button
v-if="row.status === 'inactive'"
type="success"
size="small"
@click="handleActivate(row)"
>
{{ t('pages.licenses.actions.activate') }}
</el-button>
<el-button
v-if="row.status === 'active'"
type="warning"
size="small"
@click="handleDeactivate(row)"
>
{{ t('pages.licenses.actions.deactivate') }}
</el-button>
<el-button
type="danger"
size="small"
@click="handleDelete(row)"
>
{{ t('pages.licenses.actions.delete') }}
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<div class="pagination-container">
<el-pagination
v-model:current-page="currentPage"
v-model:page-size="pageSize"
:page-sizes="[10, 20, 50, 100]"
:total="total"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
</div>
</div>
<!-- 子路由视图区域 -->
<div v-if="showSubRoute" class="sub-route-section">
<router-view />
</div>
</div>
<!-- 创建/编辑授权对话框 -->
<LicenseForm
v-model:visible="formDialogVisible"
:license-data="editingLicense"
:customers="customers"
@success="handleFormSuccess"
/>
</Layout>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { ref, onMounted, computed, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import { ElMessage, ElMessageBox } from 'element-plus'
import { useRouter, useRoute } from 'vue-router'
import { ElMessage } from 'element-plus'
import Layout from '@/components/common/layout/Layout.vue'
import LicenseForm from './LicenseForm.vue'
import { getLicenses, deleteLicense, activateLicense, deactivateLicense, type License, type LicenseQueryRequest } from '@/api/license'
import { getCustomers, type Customer } from '@/api/customer'
const { t } = useI18n()
const router = useRouter()
const route = useRoute()
// 响应式数据
const selectedCustomer = ref<string>('')
const customers = ref<Customer[]>([])
const showLicenseList = ref(false)
const searchText = ref('')
const dateRange = ref<[Date, Date] | null>(null)
const loading = ref(false)
const licenseList = ref<License[]>([])
const currentPage = ref(1)
const pageSize = ref(20)
const total = ref(0)
const formDialogVisible = ref(false)
const editingLicense = ref<License | null>(null)
const showSubRoute = ref(false)
// 查询参数
const queryParams = reactive<LicenseQueryRequest>({
page: 1,
page_size: 20,
search: '',
customer_id: '',
sort: 'created_at',
order: 'desc'
// 选中的客户信息
const selectedCustomerInfo = computed(() => {
if (!selectedCustomer.value) return null
return customers.value.find(c => c.id === selectedCustomer.value) || null
})
// 计算属性
// const formattedCustomers = computed(() => {
// return customers.value.filter(customer => customer.status === 'active')
// })
// 方法
const loadCustomers = async () => {
@@ -299,190 +116,45 @@ const loadCustomers = async () => {
}
}
const loadLicenseList = async () => {
try {
loading.value = true
const response = await getLicenses(queryParams)
licenseList.value = response.data.list
total.value = response.data.total
currentPage.value = response.data.page
} catch (error) {
console.error('Failed to load licenses:', error)
ElMessage.error(t('pages.licenses.message.loadError'))
} finally {
loading.value = false
}
}
const handleCustomerChange = (value: string) => {
queryParams.customer_id = value
const handleCustomerChange = () => {
// 客户选择变化时的处理逻辑
}
const handleQuery = () => {
showLicenseList.value = true
queryParams.page = 1
currentPage.value = 1
loadLicenseList()
// 跳转到列表页,并传递客户信息参数
showSubRoute.value = true
router.push({
name: 'licenses-list',
query: {
customerId: selectedCustomer.value || '',
customerName: selectedCustomerInfo.value?.customer_name || ''
}
})
}
const handleCreateLicense = () => {
editingLicense.value = null
formDialogVisible.value = true
}
const handleSearch = () => {
queryParams.search = searchText.value
queryParams.page = 1
currentPage.value = 1
if (showLicenseList.value) {
loadLicenseList()
}
}
const handleDateRangeChange = (value: [Date, Date] | null) => {
if (value) {
queryParams.start_date = value[0].toISOString().split('T')[0]
queryParams.end_date = value[1].toISOString().split('T')[0]
} else {
queryParams.start_date = undefined
queryParams.end_date = undefined
}
if (showLicenseList.value) {
queryParams.page = 1
currentPage.value = 1
loadLicenseList()
}
}
const refreshLicenseList = () => {
if (showLicenseList.value) {
loadLicenseList()
}
}
const handleSizeChange = (size: number) => {
queryParams.page_size = size
queryParams.page = 1
currentPage.value = 1
loadLicenseList()
}
const handleCurrentChange = (page: number) => {
queryParams.page = page
loadLicenseList()
}
const handleEdit = (license: License) => {
editingLicense.value = license
formDialogVisible.value = true
}
const handleActivate = async (license: License) => {
try {
await ElMessageBox.confirm(
t('pages.licenses.confirm.activateMessage', { code: license.license_code }),
t('pages.licenses.confirm.activateTitle'),
{
confirmButtonText: t('pages.licenses.confirm.confirm'),
cancelButtonText: t('pages.licenses.confirm.cancel'),
type: 'warning'
}
)
await activateLicense(license.id)
ElMessage.success(t('pages.licenses.message.activateSuccess'))
loadLicenseList()
} catch (error) {
if (error !== 'cancel') {
console.error('Failed to activate license:', error)
ElMessage.error('激活授权失败')
// 跳转到创建授权页,并传递客户信息参数
showSubRoute.value = true
router.push({
name: 'licenses-create',
query: {
customerId: selectedCustomer.value || '',
customerName: selectedCustomerInfo.value?.customer_name || ''
}
}
}
const handleDeactivate = async (license: License) => {
try {
await ElMessageBox.confirm(
t('pages.licenses.confirm.deactivateMessage', { code: license.license_code }),
t('pages.licenses.confirm.deactivateTitle'),
{
confirmButtonText: t('pages.licenses.confirm.confirm'),
cancelButtonText: t('pages.licenses.confirm.cancel'),
type: 'warning'
}
)
await deactivateLicense(license.id)
ElMessage.success(t('pages.licenses.message.deactivateSuccess'))
loadLicenseList()
} catch (error) {
if (error !== 'cancel') {
console.error('Failed to deactivate license:', error)
ElMessage.error('停用授权失败')
}
}
}
const handleDelete = async (license: License) => {
try {
await ElMessageBox.confirm(
t('pages.licenses.confirm.deleteMessage', { code: license.license_code }),
t('pages.licenses.confirm.deleteTitle'),
{
confirmButtonText: t('pages.licenses.confirm.confirm'),
cancelButtonText: t('pages.licenses.confirm.cancel'),
type: 'warning'
}
)
await deleteLicense(license.id)
ElMessage.success(t('pages.licenses.message.deleteSuccess'))
loadLicenseList()
} catch (error) {
if (error !== 'cancel') {
console.error('Failed to delete license:', error)
ElMessage.error(t('pages.licenses.message.deleteError'))
}
}
}
const handleFormSuccess = () => {
formDialogVisible.value = false
if (showLicenseList.value) {
loadLicenseList()
}
}
const getStatusType = (status: string) => {
switch (status) {
case 'active':
return 'success'
case 'inactive':
return 'info'
case 'expired':
return 'danger'
default:
return 'info'
}
}
const getStatusText = (status: string) => {
return t(`pages.licenses.status.${status}`)
}
const formatDate = (dateString: string) => {
if (!dateString) return '--'
const date = new Date(dateString)
return date.toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit'
})
}
// 监听路由变化,控制子路由显示
watch(() => route.name, (newName) => {
// 如果是子路由,显示子路由区域
showSubRoute.value = newName === 'licenses-list' || newName === 'licenses-create'
}, { immediate: true })
// 生命周期
onMounted(() => {
loadCustomers()
@@ -684,6 +356,14 @@ onMounted(() => {
}
}
.sub-route-section {
flex: 1;
background: #fff;
display: flex;
flex-direction: column;
overflow: hidden;
}
.license-list-section {
flex: 1;
background: #fff;