feat:新增授权管理子页面:列表页面,新增授权页面

This commit is contained in:
13895237362 committed 2025-10-16 16:01:20 +08:00
1 parent c9fed52b29
commit 8f0e20f46a
2 files changed
+656 -21

No files matched your search

+278 -21
View File
@@ -1,5 +1,164 @@
<template>
<!-- 页面模式 -->
<div v-if="isPageMode" class="license-form-page">
<div class="page-header">
<div class="header-content">
<h1 class="page-title">{{ isEdit ? '编辑授权' : t('pages.licenses.actions.createLicense') }}</h1>
<div class="header-actions">
<el-button @click="handleBack">
{{ t('customers.actions.cancel') }}
</el-button>
<el-button
type="primary"
:loading="submitting"
@click="handleSubmit"
>
{{ t('customers.actions.save') }}
</el-button>
</div>
</div>
</div>
<div class="page-content">
<el-form
ref="formRef"
:model="formData"
:rules="formRules"
label-width="120px"
class="license-form"
>
<div class="form-section">
<h3 class="section-title">{{ t('pages.licenses.form.basicInfo') }}</h3>
<el-form-item
:label="t('pages.licenses.form.selectCustomer')"
prop="customer_id"
>
<el-select
v-model="formData.customer_id"
:placeholder="t('pages.licenses.form.placeholder.selectCustomer')"
filterable
style="width: 100%"
:disabled="isEdit"
>
<el-option
v-for="customer in customers"
:key="customer.id"
:label="customer.customer_name"
:value="customer.id"
/>
</el-select>
</el-form-item>
<el-form-item
:label="t('pages.licenses.form.description')"
prop="description"
>
<el-input
v-model="formData.description"
:placeholder="t('pages.licenses.form.placeholder.enterDescription')"
type="textarea"
:rows="3"
maxlength="500"
show-word-limit
/>
</el-form-item>
<el-form-item
:label="t('pages.licenses.form.licenseType')"
prop="license_type"
>
<el-select
v-model="formData.license_type"
:placeholder="t('pages.licenses.form.placeholder.selectLicenseType')"
style="width: 100%"
>
<el-option label="标准版" value="standard" />
<el-option label="专业版" value="professional" />
<el-option label="企业版" value="enterprise" />
<el-option label="定制版" value="custom" />
</el-select>
</el-form-item>
<el-form-item
:label="t('pages.licenses.form.expiryDate')"
prop="expiry_date"
>
<el-date-picker
v-model="formData.expiry_date"
type="datetime"
:placeholder="t('pages.licenses.form.placeholder.selectExpiryDate')"
style="width: 100%"
format="YYYY-MM-DD HH:mm:ss"
value-format="YYYY-MM-DD HH:mm:ss"
:disabled-date="disabledDate"
/>
</el-form-item>
<el-form-item
:label="t('pages.licenses.form.maxUsers')"
prop="max_users"
>
<el-input-number
v-model="formData.max_users"
:min="1"
:max="10000"
:placeholder="t('pages.licenses.form.placeholder.enterMaxUsers')"
style="width: 100%"
/>
</el-form-item>
</div>
<div class="form-section">
<h3 class="section-title">高级配置</h3>
<el-form-item
:label="t('pages.licenses.form.features')"
prop="features"
>
<el-checkbox-group v-model="formData.features">
<el-checkbox label="basic" value="basic">基础功能</el-checkbox>
<el-checkbox label="advanced" value="advanced">高级功能</el-checkbox>
<el-checkbox label="api" value="api">API接口</el-checkbox>
<el-checkbox label="export" value="export">数据导出</el-checkbox>
<el-checkbox label="backup" value="backup">数据备份</el-checkbox>
<el-checkbox label="analytics" value="analytics">数据分析</el-checkbox>
</el-checkbox-group>
</el-form-item>
<el-form-item
:label="t('pages.licenses.form.ipRestrictions')"
prop="ip_restrictions"
>
<el-input
v-model="ipRestrictionsText"
:placeholder="t('pages.licenses.form.placeholder.enterIpRestrictions')"
type="textarea"
:rows="3"
@blur="handleIpRestrictionsChange"
/>
<div class="form-tip">多个IP地址请用换行分隔,支持CIDR格式(如:192.168.1.0/24)</div>
</el-form-item>
<el-form-item
:label="t('pages.licenses.form.hardwareId')"
prop="hardware_id"
>
<el-input
v-model="formData.hardware_id"
:placeholder="t('pages.licenses.form.placeholder.enterHardwareId')"
maxlength="100"
/>
<div class="form-tip">硬件ID用于绑定特定设备,留空表示不限制设备</div>
</el-form-item>
</div>
</el-form>
</div>
</div>
<!-- 对话框模式 -->
<el-dialog
v-else
:model-value="visible"
:title="isEdit ? '编辑授权' : t('pages.licenses.actions.createLicense')"
width="800px"
@@ -158,21 +317,24 @@
</template>
<script setup lang="ts">
import { ref, reactive, computed, watch } from 'vue'
import { ref, reactive, computed, watch, onMounted } from 'vue'
import { useI18n } from 'vue-i18n'
import { useRouter, useRoute } from 'vue-router'
import { ElMessage, type FormInstance, type FormRules } from 'element-plus'
import { createLicense, updateLicense, type License, type LicenseCreateRequest, type LicenseUpdateRequest } from '@/api/license'
import { type Customer } from '@/api/customer'
import { createLicense, updateLicense, type License, type AuthorizationCode, type AuthorizationCodeCreateRequest, type LicenseUpdateRequest } from '@/api/license'
import { getCustomers, type Customer } from '@/api/customer'
// Props & Emits
interface Props {
visible: boolean
licenseData?: License | null
customers: Customer[]
visible?: boolean
licenseData?: AuthorizationCode | null
customers?: Customer[]
}
const props = withDefaults(defineProps<Props>(), {
licenseData: null
visible: false,
licenseData: null,
customers: () => []
})
const emit = defineEmits<{
@@ -181,14 +343,27 @@ const emit = defineEmits<{
}>()
const { t } = useI18n()
const router = useRouter()
const route = useRoute()
// 判断是否为页面模式
const isPageMode = computed(() => {
return route.name === 'licenses-create' || route.name === 'licenses-edit'
})
// Refs
const formRef = ref<FormInstance>()
const submitting = ref(false)
const pageCustomers = ref<Customer[]>([])
// Computed
const isEdit = computed(() => !!props.licenseData)
// 获取客户列表(页面模式下使用)
const customers = computed(() => {
return isPageMode.value ? pageCustomers.value : props.customers
})
// Form Data
const formData = reactive({
customer_id: '',
@@ -239,15 +414,15 @@ const resetForm = () => {
const loadFormData = () => {
if (props.licenseData) {
formData.customer_id = props.licenseData.customer_id
formData.description = props.licenseData.description
formData.license_type = props.licenseData.license_type
formData.expiry_date = props.licenseData.expiry_date
formData.max_users = props.licenseData.max_users || 1
formData.features = [...(props.licenseData.features || [])]
formData.ip_restrictions = [...(props.licenseData.ip_restrictions || [])]
formData.hardware_id = props.licenseData.hardware_id || ''
ipRestrictionsText.value = formData.ip_restrictions.join('\n')
formData.customer_id = props.licenseData.customer_id || ''
formData.description = props.licenseData.description || ''
formData.license_type = ''
formData.expiry_date = props.licenseData.end_date || ''
formData.max_users = props.licenseData.max_activations || 1
formData.features = []
formData.ip_restrictions = []
formData.hardware_id = ''
ipRestrictionsText.value = ''
} else {
resetForm()
}
@@ -308,7 +483,7 @@ const handleSubmit = async () => {
ElMessage.success(t('pages.licenses.message.updateSuccess'))
} else {
// 创建授权
const createData: LicenseCreateRequest = {
const createData: AuthorizationCodeCreateRequest = {
customer_id: formData.customer_id,
description: formData.description,
license_type: formData.license_type,
@@ -323,7 +498,13 @@ const handleSubmit = async () => {
ElMessage.success(t('pages.licenses.message.createSuccess'))
}
emit('success')
if (isPageMode.value) {
// 页面模式下跳转回主页面
router.push({ name: 'licenses' })
} else {
// 对话框模式下触发成功事件
emit('success')
}
} catch (error) {
console.error('Failed to save license:', error)
ElMessage.error(isEdit.value
@@ -344,21 +525,97 @@ const handleClose = () => {
}, 300)
}
// 页面模式下的返回功能
const handleBack = () => {
router.push({ name: 'licenses' })
}
// 加载客户数据(页面模式)
const loadCustomers = async () => {
try {
const response = await getCustomers({ status: 'active', page_size: 1000 })
pageCustomers.value = response.data.list
} catch (error) {
console.error('Failed to load customers:', error)
ElMessage.error('加载客户列表失败')
}
}
// Watchers
watch(() => props.visible, (visible) => {
if (visible) {
loadFormData()
}
})
// 页面模式下的初始化
watch(() => route.query, (query) => {
if (isPageMode.value && query.customerId) {
formData.customer_id = query.customerId as string
}
}, { immediate: true })
// 生命周期
onMounted(() => {
if (isPageMode.value) {
loadCustomers()
loadFormData()
}
})
</script>
<style scoped lang="scss">
@import '@/assets/styles/variables.scss';
.license-form {
max-height: 70vh;
// 页面模式样式
.license-form-page {
height: 100%;
display: flex;
flex-direction: column;
background: #fff;
}
.page-header {
padding: 24px;
border-bottom: 1px solid $border-color-light;
background: #fff;
}
.header-content {
display: flex;
justify-content: space-between;
align-items: center;
max-width: 1200px;
margin: 0 auto;
}
.page-title {
font-size: 24px;
font-weight: 600;
color: $text-color-primary;
margin: 0;
}
.header-actions {
display: flex;
gap: 12px;
}
.page-content {
flex: 1;
padding: 24px;
overflow-y: auto;
padding-right: 8px;
max-width: 1200px;
margin: 0 auto;
width: 100%;
box-sizing: border-box;
}
.license-form {
max-height: none;
overflow-y: visible;
padding-right: 0;
}
.form-section {
+378
View File
@@ -0,0 +1,378 @@
<template>
<div class="license-list-page">
<!-- 顶部操作区 -->
<div class="top-actions">
<el-button type="primary" @click="handleCreate">
创建授权
</el-button>
<div class="filters">
<span class="filter-label">筛选:</span>
<el-select v-model="filterStatus" placeholder="状态" clearable class="filter-select">
<el-option label="正常" value="normal" />
<el-option label="锁定" value="locked" />
<el-option label="过期" value="expired" />
</el-select>
<el-date-picker
v-model="filterCreateDate"
type="date"
placeholder="创建时间"
clearable
class="filter-date"
/>
<el-date-picker
v-model="filterExpiryDate"
type="date"
placeholder="到期时间"
clearable
class="filter-date"
/>
<el-button type="primary" @click="handleQuery">
查询
</el-button>
</div>
</div>
<!-- 表格 -->
<div class="table-container">
<el-table
:data="tableData"
stripe
style="width: 100%"
v-loading="loading"
empty-text="暂无数据"
>
<el-table-column prop="code" label="授权码" width="200" />
<el-table-column prop="status" label="状态" width="100">
<template #default="{ row }">
<el-tag :type="getStatusTagType(row.status)">
{{ getStatusText(row.status) }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="current_activations" label="激活状态" width="120" />
<el-table-column prop="end_date" label="到期时间" width="180" />
<el-table-column prop="description" label="备注" min-width="200" show-overflow-tooltip />
<el-table-column label="操作" width="200" fixed="right">
<template #default="{ row }">
<el-button type="primary" size="small" @click="handleDetail(row)">
详情
</el-button>
<el-button
v-if="row.is_locked"
type="success"
size="small"
@click="handleUnlock(row)"
>
解锁
</el-button>
<el-button
v-else
type="warning"
size="small"
@click="handleLock(row)"
>
锁定
</el-button>
<el-button type="danger" size="small" @click="handleDelete(row)">
删除
</el-button>
</template>
</el-table-column>
</el-table>
</div>
<!-- 分页 -->
<div class="pagination-container">
<el-pagination
:current-page="currentPage"
:page-size="pageSize"
:page-sizes="[10, 20, 50, 100]"
:total="total"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleSizeChange"
@current-change="handlePageChange"
/>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, computed } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { useRouter, useRoute } from 'vue-router'
import { getLicenses, lockAuthorizationCode, type AuthorizationCode, type LicenseQueryRequest } from '@/api/license'
const router = useRouter()
const route = useRoute()
// 从路由参数获取客户信息
const customerInfo = computed(() => {
return {
id: route.query.customerId as string || '',
name: route.query.customerName as string || ''
}
})
// 响应式数据
const loading = ref(false)
const filterStatus = ref('')
const filterCreateDate = ref('')
const filterExpiryDate = ref('')
const tableData = ref<AuthorizationCode[]>([])
const currentPage = ref(1)
const pageSize = ref(20)
const total = ref(0)
// 方法
const getStatusTagType = (status: string) => {
switch (status) {
case 'normal':
return 'success'
case 'locked':
return 'warning'
case 'expired':
return 'danger'
default:
return 'info'
}
}
const getStatusText = (status: string) => {
switch (status) {
case 'normal':
return '正常'
case 'locked':
return '锁定'
case 'expired':
return '过期'
default:
return status
}
}
const handleCreate = () => {
router.push({
name: 'licenses-create',
query: {
customerId: customerInfo.value.id,
customerName: customerInfo.value.name
}
})
}
const handleQuery = () => {
console.log('点击查询按钮')
loadData()
}
const handleDetail = (_row: any) => {
ElMessage.info('查看详情功能待实现')
}
const handleLock = async (row: any) => {
ElMessageBox.confirm('确定要锁定此授权吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(async () => {
try {
await lockAuthorizationCode(row.id, {
is_locked: true,
lock_reason: '手动锁定',
reason: '管理员手动锁定'
})
ElMessage.success('锁定成功')
loadData()
} catch (error) {
console.error('锁定失败:', error)
ElMessage.error('锁定失败')
}
}).catch(() => {
// 取消操作
})
}
const handleUnlock = async (row: any) => {
ElMessageBox.confirm('确定要解锁此授权吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(async () => {
try {
await lockAuthorizationCode(row.id, {
is_locked: false,
lock_reason: '手动解锁',
reason: '管理员手动解锁'
})
ElMessage.success('解锁成功')
loadData()
} catch (error) {
console.error('解锁失败:', error)
ElMessage.error('解锁失败')
}
}).catch(() => {
// 取消操作
})
}
const handleDelete = (_row: any) => {
ElMessageBox.confirm('确定要删除此授权吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
ElMessage.success('删除成功')
loadData()
}).catch(() => {
// 取消操作
})
}
const handleSizeChange = (size: number) => {
pageSize.value = size
loadData()
}
const handlePageChange = (page: number) => {
currentPage.value = page
loadData()
}
const loadData = async () => {
loading.value = true
try {
const queryParams: LicenseQueryRequest = {
page: currentPage.value,
page_size: pageSize.value,
customer_id: customerInfo.value.id || '',
sort: 'created_at',
order: 'desc'
}
// 添加状态筛选
if (filterStatus.value) {
queryParams.status = filterStatus.value as 'normal' | 'locked' | 'expired'
}
console.log('查询参数:', queryParams)
const response = await getLicenses(queryParams)
console.log('API响应:', response)
tableData.value = response.data.list
total.value = response.data.total
console.log('表格数据:', tableData.value)
} catch (error) {
console.error('加载数据失败:', error)
ElMessage.error('加载数据失败')
} finally {
loading.value = false
}
}
// 生命周期
onMounted(() => {
console.log('LinenseList组件已挂载')
console.log('客户信息:', customerInfo.value)
loadData()
})
</script>
<style scoped lang="scss">
.license-list-page {
padding: 20px;
background: #fff;
min-height: calc(100vh - 80px);
}
.customer-info {
margin-bottom: 20px;
padding: 16px;
background: #f5f7fa;
border-radius: 8px;
border-left: 4px solid #409eff;
h2 {
margin: 0;
font-size: 18px;
font-weight: 600;
color: #303133;
}
}
.debug-info {
margin-bottom: 20px;
padding: 16px;
background: #fff3cd;
border-radius: 8px;
border-left: 4px solid #ffc107;
p {
margin: 4px 0;
font-size: 14px;
color: #856404;
}
}
.top-actions {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
gap: 16px;
flex-wrap: wrap;
}
.filters {
display: flex;
align-items: center;
gap: 12px;
flex-wrap: wrap;
}
.filter-label {
font-size: 14px;
color: #606266;
}
.filter-select {
width: 120px;
}
.filter-date {
width: 160px;
}
.table-container {
margin-bottom: 20px;
}
.pagination-container {
display: flex;
justify-content: center;
}
// 响应式设计
@media (max-width: 768px) {
.license-list-page {
padding: 12px;
}
.top-actions {
flex-direction: column;
align-items: stretch;
}
.filters {
width: 100%;
.filter-select,
.filter-date {
flex: 1;
min-width: 100px;
}
}
}
</style>