新增cursor续杯界面
This commit is contained in:
parent
93ece610ff
commit
c6b97f79f2
65
src/api/cursorEquipment.js
Normal file
65
src/api/cursorEquipment.js
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
import request from '@/utils/request';
|
||||||
|
|
||||||
|
const baseUrl = '/platform/cursor/equipment';
|
||||||
|
|
||||||
|
export function getCursorEquipmentList(params) {
|
||||||
|
return request({
|
||||||
|
url: `${baseUrl}/list`,
|
||||||
|
method: 'get',
|
||||||
|
params,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getCursorEquipmentDetail(id) {
|
||||||
|
return request({
|
||||||
|
url: `${baseUrl}/detail/${id}`,
|
||||||
|
method: 'get',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function addCursorEquipment(data) {
|
||||||
|
return request({
|
||||||
|
url: `${baseUrl}/add`,
|
||||||
|
method: 'post',
|
||||||
|
data,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateCursorEquipment(data) {
|
||||||
|
return request({
|
||||||
|
url: `${baseUrl}/update`,
|
||||||
|
method: 'post',
|
||||||
|
data,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deleteCursorEquipment(id) {
|
||||||
|
return request({
|
||||||
|
url: `${baseUrl}/delete/${id}`,
|
||||||
|
method: 'post',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function activateCursorEquipment(data) {
|
||||||
|
return request({
|
||||||
|
url: `${baseUrl}/activate`,
|
||||||
|
method: 'post',
|
||||||
|
data,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getCursorEquipmentActivationRecords(params) {
|
||||||
|
return request({
|
||||||
|
url: `${baseUrl}/activationRecords`,
|
||||||
|
method: 'get',
|
||||||
|
params,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getCursorEquipmentExtractRecords(params) {
|
||||||
|
return request({
|
||||||
|
url: `${baseUrl}/extractRecords`,
|
||||||
|
method: 'get',
|
||||||
|
params,
|
||||||
|
});
|
||||||
|
}
|
||||||
137
src/views/cursor/equipment/components/activationRecords.vue
Normal file
137
src/views/cursor/equipment/components/activationRecords.vue
Normal file
@ -0,0 +1,137 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
defineProps({
|
||||||
|
modelValue: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
row: {
|
||||||
|
type: Object,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
loading: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
records: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
total: {
|
||||||
|
type: Number,
|
||||||
|
default: 0,
|
||||||
|
},
|
||||||
|
page: {
|
||||||
|
type: Number,
|
||||||
|
default: 1,
|
||||||
|
},
|
||||||
|
pageSize: {
|
||||||
|
type: Number,
|
||||||
|
default: 20,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['update:modelValue', 'update:page', 'update:pageSize', 'refresh']);
|
||||||
|
|
||||||
|
function statusText(status: unknown) {
|
||||||
|
const value = String(status || '');
|
||||||
|
if (value === 'success' || value === '1') return '成功';
|
||||||
|
if (value === 'failed' || value === '0') return '失败';
|
||||||
|
return value || '-';
|
||||||
|
}
|
||||||
|
|
||||||
|
function statusType(status: unknown) {
|
||||||
|
const value = String(status || '');
|
||||||
|
if (value === 'success' || value === '1') return 'success';
|
||||||
|
if (value === 'failed' || value === '0') return 'danger';
|
||||||
|
return 'info';
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<el-drawer
|
||||||
|
class="equipment-record-drawer"
|
||||||
|
:model-value="modelValue"
|
||||||
|
title="激活记录"
|
||||||
|
size="760px"
|
||||||
|
direction="rtl"
|
||||||
|
@update:model-value="(v) => emit('update:modelValue', v)"
|
||||||
|
@opened="emit('refresh')"
|
||||||
|
>
|
||||||
|
<div class="record-header">
|
||||||
|
<div>
|
||||||
|
<div class="record-title">{{ row?.name || '-' }}</div>
|
||||||
|
<div class="record-subtitle">设备编号:{{ row?.deviceNo || '-' }}</div>
|
||||||
|
</div>
|
||||||
|
<el-button :loading="loading" @click="emit('refresh')">刷新</el-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-table v-loading="loading" :data="records" border stripe style="width: 100%">
|
||||||
|
<el-table-column prop="id" label="ID" width="80" />
|
||||||
|
<el-table-column label="状态" width="90" align="center">
|
||||||
|
<template #default="{ row: item }">
|
||||||
|
<el-tag :type="statusType(item.status)" size="small">
|
||||||
|
{{ statusText(item.status) }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="account" label="激活账号" min-width="150" show-overflow-tooltip />
|
||||||
|
<el-table-column prop="ip" label="IP" min-width="130" show-overflow-tooltip />
|
||||||
|
<el-table-column prop="clientVersion" label="客户端版本" width="120" />
|
||||||
|
<el-table-column prop="createdAt" label="激活时间" width="180" />
|
||||||
|
<el-table-column prop="remark" label="备注" min-width="180" show-overflow-tooltip />
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<div class="record-pager">
|
||||||
|
<el-pagination
|
||||||
|
:current-page="page"
|
||||||
|
:page-size="pageSize"
|
||||||
|
background
|
||||||
|
layout="total, prev, pager, next, jumper"
|
||||||
|
:total="total"
|
||||||
|
@update:current-page="(v) => emit('update:page', v)"
|
||||||
|
@update:page-size="(v) => emit('update:pageSize', v)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</el-drawer>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="less">
|
||||||
|
.record-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.record-title {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #303133;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.record-subtitle {
|
||||||
|
color: #909399;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.record-pager {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
margin-top: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
:deep(.equipment-record-drawer) {
|
||||||
|
width: 100vw !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.record-header {
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.record-pager {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
85
src/views/cursor/equipment/components/delete.vue
Normal file
85
src/views/cursor/equipment/components/delete.vue
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
const props = defineProps({
|
||||||
|
modelValue: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
row: {
|
||||||
|
type: Object,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
loading: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['update:modelValue', 'confirm']);
|
||||||
|
|
||||||
|
function close() {
|
||||||
|
emit('update:modelValue', false);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<el-dialog
|
||||||
|
class="equipment-delete-dialog"
|
||||||
|
:model-value="modelValue"
|
||||||
|
title="删除设备"
|
||||||
|
width="460px"
|
||||||
|
@update:model-value="(v) => emit('update:modelValue', v)"
|
||||||
|
>
|
||||||
|
<el-alert type="warning" :closable="false" show-icon>
|
||||||
|
删除后设备及相关记录可能无法恢复,请谨慎操作。
|
||||||
|
</el-alert>
|
||||||
|
|
||||||
|
<div v-if="props.row" class="delete-content">
|
||||||
|
<div class="delete-row">
|
||||||
|
<span class="label">设备名称:</span>
|
||||||
|
<span>{{ props.row.name || '-' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="delete-row">
|
||||||
|
<span class="label">设备编号:</span>
|
||||||
|
<span>{{ props.row.deviceNo || '-' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="delete-row">
|
||||||
|
<span class="label">机器码:</span>
|
||||||
|
<span class="code">{{ props.row.machineCode || '-' }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="close">取消</el-button>
|
||||||
|
<el-button type="danger" :loading="loading" @click="emit('confirm')">确认删除</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="less">
|
||||||
|
.delete-content {
|
||||||
|
margin-top: 16px;
|
||||||
|
padding: 12px 14px;
|
||||||
|
background: #f8fafc;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.delete-row {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
line-height: 1.8;
|
||||||
|
font-size: 14px;
|
||||||
|
|
||||||
|
.label {
|
||||||
|
flex-shrink: 0;
|
||||||
|
color: #909399;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code {
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.equipment-delete-dialog) {
|
||||||
|
max-width: calc(100vw - 24px);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
163
src/views/cursor/equipment/components/detail.vue
Normal file
163
src/views/cursor/equipment/components/detail.vue
Normal file
@ -0,0 +1,163 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { computed } from 'vue';
|
||||||
|
import { ElMessage } from 'element-plus';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
modelValue: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
row: {
|
||||||
|
type: Object,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['update:modelValue']);
|
||||||
|
|
||||||
|
const statusMap: Record<string, { label: string; type: string }> = {
|
||||||
|
active: { label: '正常', type: 'success' },
|
||||||
|
inactive: { label: '未激活', type: 'info' },
|
||||||
|
disabled: { label: '禁用', type: 'danger' },
|
||||||
|
expired: { label: '已过期', type: 'warning' },
|
||||||
|
};
|
||||||
|
|
||||||
|
const statusInfo = computed(() => {
|
||||||
|
const status = String(props.row?.status || 'inactive');
|
||||||
|
return statusMap[status] || { label: status || '-', type: 'info' };
|
||||||
|
});
|
||||||
|
|
||||||
|
function display(value: unknown) {
|
||||||
|
if (value === null || value === undefined || value === '') return '-';
|
||||||
|
return String(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function copyText(text: unknown, label = '内容') {
|
||||||
|
const value = String(text || '').trim();
|
||||||
|
if (!value) {
|
||||||
|
ElMessage.warning(`暂无${label}可复制`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
navigator.clipboard.writeText(value).then(() => {
|
||||||
|
ElMessage.success(`${label}已复制`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<el-dialog
|
||||||
|
class="equipment-detail-dialog"
|
||||||
|
:model-value="modelValue"
|
||||||
|
title="设备详情"
|
||||||
|
width="760px"
|
||||||
|
@update:model-value="(v) => emit('update:modelValue', v)"
|
||||||
|
>
|
||||||
|
<el-descriptions v-if="row" :column="2" border>
|
||||||
|
<el-descriptions-item label="设备ID">
|
||||||
|
{{ display(row.id) }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="设备状态">
|
||||||
|
<el-tag :type="statusInfo.type">{{ statusInfo.label }}</el-tag>
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="设备名称">
|
||||||
|
{{ display(row.name) }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="设备编号">
|
||||||
|
<span class="value-with-action">
|
||||||
|
<span>{{ display(row.deviceNo) }}</span>
|
||||||
|
<el-button v-if="row.deviceNo" link type="primary" @click="copyText(row.deviceNo, '设备编号')">
|
||||||
|
复制
|
||||||
|
</el-button>
|
||||||
|
</span>
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="机器码">
|
||||||
|
<span class="code-text">{{ display(row.machineCode) }}</span>
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="授权码">
|
||||||
|
<span class="code-text">{{ display(row.licenseCode) }}</span>
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="系统平台">
|
||||||
|
{{ display(row.os) }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="版本">
|
||||||
|
{{ display(row.version) }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="绑定账号">
|
||||||
|
{{ display(row.account) }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="归属用户">
|
||||||
|
{{ display(row.owner) }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="激活次数">
|
||||||
|
{{ Number(row.activationCount || 0) }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="提取次数">
|
||||||
|
{{ Number(row.extractCount || 0) }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="最后激活时间">
|
||||||
|
{{ display(row.lastActivatedAt) }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="最后提取时间">
|
||||||
|
{{ display(row.lastExtractedAt) }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="过期时间">
|
||||||
|
{{ display(row.expiredAt) }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="创建时间">
|
||||||
|
{{ display(row.createdAt) }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="备注" :span="2">
|
||||||
|
<span class="remark-text">{{ display(row.remark) }}</span>
|
||||||
|
</el-descriptions-item>
|
||||||
|
</el-descriptions>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="emit('update:modelValue', false)">关闭</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="less">
|
||||||
|
.value-with-action {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-text,
|
||||||
|
.remark-text {
|
||||||
|
word-break: break-all;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.equipment-detail-dialog) {
|
||||||
|
max-width: calc(100vw - 24px);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
:deep(.equipment-detail-dialog) {
|
||||||
|
width: calc(100vw - 24px) !important;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.equipment-detail-dialog .el-dialog__body) {
|
||||||
|
padding: 12px;
|
||||||
|
max-height: 70vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-descriptions__body .el-descriptions__table) {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-descriptions__body tbody),
|
||||||
|
:deep(.el-descriptions__body tr),
|
||||||
|
:deep(.el-descriptions__body th),
|
||||||
|
:deep(.el-descriptions__body td) {
|
||||||
|
display: block;
|
||||||
|
width: 100% !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
200
src/views/cursor/equipment/components/edit.vue
Normal file
200
src/views/cursor/equipment/components/edit.vue
Normal file
@ -0,0 +1,200 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { reactive, ref, watch } from 'vue';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
modelValue: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
row: {
|
||||||
|
type: Object,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
loading: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['update:modelValue', 'submit']);
|
||||||
|
|
||||||
|
const formRef = ref();
|
||||||
|
|
||||||
|
const form = reactive({
|
||||||
|
id: '',
|
||||||
|
name: '',
|
||||||
|
deviceNo: '',
|
||||||
|
machineCode: '',
|
||||||
|
licenseCode: '',
|
||||||
|
os: '',
|
||||||
|
version: '',
|
||||||
|
account: '',
|
||||||
|
owner: '',
|
||||||
|
status: 'inactive',
|
||||||
|
expiredAt: '',
|
||||||
|
remark: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
const rules = {
|
||||||
|
name: [{ required: true, message: '请输入设备名称', trigger: 'blur' }],
|
||||||
|
deviceNo: [{ required: true, message: '请输入设备编号', trigger: 'blur' }],
|
||||||
|
machineCode: [{ required: true, message: '请输入机器码', trigger: 'blur' }],
|
||||||
|
status: [{ required: true, message: '请选择设备状态', trigger: 'change' }],
|
||||||
|
};
|
||||||
|
|
||||||
|
function resetForm() {
|
||||||
|
form.id = '';
|
||||||
|
form.name = '';
|
||||||
|
form.deviceNo = '';
|
||||||
|
form.machineCode = '';
|
||||||
|
form.licenseCode = '';
|
||||||
|
form.os = '';
|
||||||
|
form.version = '';
|
||||||
|
form.account = '';
|
||||||
|
form.owner = '';
|
||||||
|
form.status = 'inactive';
|
||||||
|
form.expiredAt = '';
|
||||||
|
form.remark = '';
|
||||||
|
formRef.value?.clearValidate?.();
|
||||||
|
}
|
||||||
|
|
||||||
|
function fillForm(row: any) {
|
||||||
|
form.id = row?.id || '';
|
||||||
|
form.name = row?.name || '';
|
||||||
|
form.deviceNo = row?.deviceNo || '';
|
||||||
|
form.machineCode = row?.machineCode || '';
|
||||||
|
form.licenseCode = row?.licenseCode || '';
|
||||||
|
form.os = row?.os || '';
|
||||||
|
form.version = row?.version || '';
|
||||||
|
form.account = row?.account || '';
|
||||||
|
form.owner = row?.owner || '';
|
||||||
|
form.status = row?.status || 'inactive';
|
||||||
|
form.expiredAt = row?.expiredAt || '';
|
||||||
|
form.remark = row?.remark || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.modelValue,
|
||||||
|
(visible) => {
|
||||||
|
if (!visible) return;
|
||||||
|
resetForm();
|
||||||
|
if (props.row) fillForm(props.row);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
async function handleSubmit() {
|
||||||
|
await formRef.value?.validate?.();
|
||||||
|
emit('submit', { ...form });
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<el-dialog
|
||||||
|
class="equipment-edit-dialog"
|
||||||
|
:model-value="modelValue"
|
||||||
|
:title="row?.id ? '编辑设备' : '新增设备'"
|
||||||
|
width="720px"
|
||||||
|
destroy-on-close
|
||||||
|
@update:model-value="(v) => emit('update:modelValue', v)"
|
||||||
|
>
|
||||||
|
<el-form ref="formRef" :model="form" :rules="rules" label-width="96px">
|
||||||
|
<el-row :gutter="14">
|
||||||
|
<el-col :xs="24" :sm="12">
|
||||||
|
<el-form-item label="设备名称" prop="name">
|
||||||
|
<el-input v-model="form.name" placeholder="请输入设备名称" clearable />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :xs="24" :sm="12">
|
||||||
|
<el-form-item label="设备编号" prop="deviceNo">
|
||||||
|
<el-input v-model="form.deviceNo" placeholder="请输入设备编号" clearable />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="24">
|
||||||
|
<el-form-item label="机器码" prop="machineCode">
|
||||||
|
<el-input v-model="form.machineCode" placeholder="请输入设备机器码 / 指纹" clearable />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="24">
|
||||||
|
<el-form-item label="授权码">
|
||||||
|
<el-input v-model="form.licenseCode" placeholder="请输入授权码" clearable />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :xs="24" :sm="12">
|
||||||
|
<el-form-item label="系统平台">
|
||||||
|
<el-select v-model="form.os" placeholder="请选择系统平台" clearable style="width: 100%">
|
||||||
|
<el-option label="Windows" value="Windows" />
|
||||||
|
<el-option label="macOS" value="macOS" />
|
||||||
|
<el-option label="Linux" value="Linux" />
|
||||||
|
<el-option label="其他" value="Other" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :xs="24" :sm="12">
|
||||||
|
<el-form-item label="版本">
|
||||||
|
<el-input v-model="form.version" placeholder="请输入客户端版本" clearable />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :xs="24" :sm="12">
|
||||||
|
<el-form-item label="绑定账号">
|
||||||
|
<el-input v-model="form.account" placeholder="请输入绑定账号" clearable />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :xs="24" :sm="12">
|
||||||
|
<el-form-item label="归属用户">
|
||||||
|
<el-input v-model="form.owner" placeholder="请输入归属用户" clearable />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :xs="24" :sm="12">
|
||||||
|
<el-form-item label="设备状态" prop="status">
|
||||||
|
<el-select v-model="form.status" placeholder="请选择状态" style="width: 100%">
|
||||||
|
<el-option label="未激活" value="inactive" />
|
||||||
|
<el-option label="正常" value="active" />
|
||||||
|
<el-option label="禁用" value="disabled" />
|
||||||
|
<el-option label="已过期" value="expired" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :xs="24" :sm="12">
|
||||||
|
<el-form-item label="过期时间">
|
||||||
|
<el-date-picker
|
||||||
|
v-model="form.expiredAt"
|
||||||
|
type="datetime"
|
||||||
|
value-format="YYYY-MM-DD HH:mm:ss"
|
||||||
|
placeholder="请选择过期时间"
|
||||||
|
style="width: 100%"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="24">
|
||||||
|
<el-form-item label="备注">
|
||||||
|
<el-input v-model="form.remark" type="textarea" :rows="4" placeholder="请输入备注" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</el-form>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="emit('update:modelValue', false)">取消</el-button>
|
||||||
|
<el-button type="primary" :loading="loading" @click="handleSubmit">保存</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="less">
|
||||||
|
:deep(.equipment-edit-dialog) {
|
||||||
|
max-width: calc(100vw - 24px);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
:deep(.equipment-edit-dialog) {
|
||||||
|
width: calc(100vw - 24px) !important;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.equipment-edit-dialog .el-dialog__body) {
|
||||||
|
padding: 12px;
|
||||||
|
max-height: 70vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
159
src/views/cursor/equipment/components/extractRecords.vue
Normal file
159
src/views/cursor/equipment/components/extractRecords.vue
Normal file
@ -0,0 +1,159 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { ElMessage } from 'element-plus';
|
||||||
|
|
||||||
|
defineProps({
|
||||||
|
modelValue: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
row: {
|
||||||
|
type: Object,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
loading: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
records: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
total: {
|
||||||
|
type: Number,
|
||||||
|
default: 0,
|
||||||
|
},
|
||||||
|
page: {
|
||||||
|
type: Number,
|
||||||
|
default: 1,
|
||||||
|
},
|
||||||
|
pageSize: {
|
||||||
|
type: Number,
|
||||||
|
default: 20,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['update:modelValue', 'update:page', 'update:pageSize', 'refresh']);
|
||||||
|
|
||||||
|
function copyContent(content: unknown) {
|
||||||
|
const text = String(content || '').trim();
|
||||||
|
if (!text) {
|
||||||
|
ElMessage.warning('暂无提取内容可复制');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
navigator.clipboard.writeText(text).then(() => {
|
||||||
|
ElMessage.success('提取内容已复制');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function statusText(status: unknown) {
|
||||||
|
const value = String(status || '');
|
||||||
|
if (value === 'success' || value === '1') return '成功';
|
||||||
|
if (value === 'failed' || value === '0') return '失败';
|
||||||
|
return value || '-';
|
||||||
|
}
|
||||||
|
|
||||||
|
function statusType(status: unknown) {
|
||||||
|
const value = String(status || '');
|
||||||
|
if (value === 'success' || value === '1') return 'success';
|
||||||
|
if (value === 'failed' || value === '0') return 'danger';
|
||||||
|
return 'info';
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<el-drawer
|
||||||
|
class="equipment-record-drawer"
|
||||||
|
:model-value="modelValue"
|
||||||
|
title="提取记录"
|
||||||
|
size="860px"
|
||||||
|
direction="rtl"
|
||||||
|
@update:model-value="(v) => emit('update:modelValue', v)"
|
||||||
|
@opened="emit('refresh')"
|
||||||
|
>
|
||||||
|
<div class="record-header">
|
||||||
|
<div>
|
||||||
|
<div class="record-title">{{ row?.name || '-' }}</div>
|
||||||
|
<div class="record-subtitle">设备编号:{{ row?.deviceNo || '-' }}</div>
|
||||||
|
</div>
|
||||||
|
<el-button :loading="loading" @click="emit('refresh')">刷新</el-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-table v-loading="loading" :data="records" border stripe style="width: 100%">
|
||||||
|
<el-table-column prop="id" label="ID" width="80" />
|
||||||
|
<el-table-column label="状态" width="90" align="center">
|
||||||
|
<template #default="{ row: item }">
|
||||||
|
<el-tag :type="statusType(item.status)" size="small">
|
||||||
|
{{ statusText(item.status) }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="platform" label="提取平台" width="110" />
|
||||||
|
<el-table-column prop="type" label="提取类型" width="110" />
|
||||||
|
<el-table-column prop="account" label="提取账号" min-width="150" show-overflow-tooltip />
|
||||||
|
<el-table-column label="提取内容" min-width="220" show-overflow-tooltip>
|
||||||
|
<template #default="{ row: item }">
|
||||||
|
<span>{{ item.content || '-' }}</span>
|
||||||
|
<el-button v-if="item.content" link type="primary" @click="copyContent(item.content)">
|
||||||
|
复制
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="ip" label="IP" min-width="130" show-overflow-tooltip />
|
||||||
|
<el-table-column prop="createdAt" label="提取时间" width="180" />
|
||||||
|
<el-table-column prop="remark" label="备注" min-width="180" show-overflow-tooltip />
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<div class="record-pager">
|
||||||
|
<el-pagination
|
||||||
|
:current-page="page"
|
||||||
|
:page-size="pageSize"
|
||||||
|
background
|
||||||
|
layout="total, prev, pager, next, jumper"
|
||||||
|
:total="total"
|
||||||
|
@update:current-page="(v) => emit('update:page', v)"
|
||||||
|
@update:page-size="(v) => emit('update:pageSize', v)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</el-drawer>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="less">
|
||||||
|
.record-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.record-title {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #303133;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.record-subtitle {
|
||||||
|
color: #909399;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.record-pager {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
margin-top: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
:deep(.equipment-record-drawer) {
|
||||||
|
width: 100vw !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.record-header {
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.record-pager {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
675
src/views/cursor/equipment/index.vue
Normal file
675
src/views/cursor/equipment/index.vue
Normal file
@ -0,0 +1,675 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { computed, onMounted, onUnmounted, reactive, ref, watch } from 'vue';
|
||||||
|
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||||
|
import DetailDialog from './components/detail.vue';
|
||||||
|
import EditDialog from './components/edit.vue';
|
||||||
|
import DeleteDialog from './components/delete.vue';
|
||||||
|
import ActivationRecords from './components/activationRecords.vue';
|
||||||
|
import ExtractRecords from './components/extractRecords.vue';
|
||||||
|
import {
|
||||||
|
activateCursorEquipment,
|
||||||
|
addCursorEquipment,
|
||||||
|
deleteCursorEquipment,
|
||||||
|
getCursorEquipmentActivationRecords,
|
||||||
|
getCursorEquipmentDetail,
|
||||||
|
getCursorEquipmentExtractRecords,
|
||||||
|
getCursorEquipmentList,
|
||||||
|
updateCursorEquipment,
|
||||||
|
} from '@/api/cursorEquipment';
|
||||||
|
|
||||||
|
type EquipmentRow = Record<string, any>;
|
||||||
|
|
||||||
|
const loading = ref(false);
|
||||||
|
const actionLoading = ref(false);
|
||||||
|
const detailVisible = ref(false);
|
||||||
|
const editVisible = ref(false);
|
||||||
|
const deleteVisible = ref(false);
|
||||||
|
const activationVisible = ref(false);
|
||||||
|
const extractVisible = ref(false);
|
||||||
|
const currentRow = ref<EquipmentRow | null>(null);
|
||||||
|
const selectedRows = ref<EquipmentRow[]>([]);
|
||||||
|
const tableData = ref<EquipmentRow[]>([]);
|
||||||
|
const total = ref(0);
|
||||||
|
const isMobile = ref(false);
|
||||||
|
|
||||||
|
const query = reactive({
|
||||||
|
keyword: '',
|
||||||
|
status: '',
|
||||||
|
os: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
const pagination = reactive({
|
||||||
|
page: 1,
|
||||||
|
pageSize: 20,
|
||||||
|
});
|
||||||
|
|
||||||
|
const activationState = reactive({
|
||||||
|
loading: false,
|
||||||
|
records: [] as EquipmentRow[],
|
||||||
|
total: 0,
|
||||||
|
page: 1,
|
||||||
|
pageSize: 20,
|
||||||
|
});
|
||||||
|
|
||||||
|
const extractState = reactive({
|
||||||
|
loading: false,
|
||||||
|
records: [] as EquipmentRow[],
|
||||||
|
total: 0,
|
||||||
|
page: 1,
|
||||||
|
pageSize: 20,
|
||||||
|
});
|
||||||
|
|
||||||
|
const statusOptions = [
|
||||||
|
{ label: '未激活', value: 'inactive' },
|
||||||
|
{ label: '正常', value: 'active' },
|
||||||
|
{ label: '禁用', value: 'disabled' },
|
||||||
|
{ label: '已过期', value: 'expired' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const osOptions = [
|
||||||
|
{ label: 'Windows', value: 'Windows' },
|
||||||
|
{ label: 'macOS', value: 'macOS' },
|
||||||
|
{ label: 'Linux', value: 'Linux' },
|
||||||
|
{ label: '其他', value: 'Other' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const statusMap: Record<string, { label: string; type: string }> = {
|
||||||
|
active: { label: '正常', type: 'success' },
|
||||||
|
inactive: { label: '未激活', type: 'info' },
|
||||||
|
disabled: { label: '禁用', type: 'danger' },
|
||||||
|
expired: { label: '已过期', type: 'warning' },
|
||||||
|
};
|
||||||
|
|
||||||
|
const summary = computed(() => {
|
||||||
|
const active = tableData.value.filter((item) => item.status === 'active').length;
|
||||||
|
const inactive = tableData.value.filter((item) => item.status === 'inactive').length;
|
||||||
|
const disabled = tableData.value.filter((item) => item.status === 'disabled').length;
|
||||||
|
const expired = tableData.value.filter((item) => item.status === 'expired').length;
|
||||||
|
return [
|
||||||
|
{ label: '当前页设备', value: tableData.value.length, type: 'primary' },
|
||||||
|
{ label: '正常设备', value: active, type: 'success' },
|
||||||
|
{ label: '未激活', value: inactive, type: 'info' },
|
||||||
|
{ label: '禁用/过期', value: disabled + expired, type: 'danger' },
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => [query.keyword, query.status, query.os],
|
||||||
|
() => {
|
||||||
|
pagination.page = 1;
|
||||||
|
fetchList();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => [pagination.page, pagination.pageSize],
|
||||||
|
() => {
|
||||||
|
fetchList();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => [activationState.page, activationState.pageSize],
|
||||||
|
() => {
|
||||||
|
if (activationVisible.value) fetchActivationRecords();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => [extractState.page, extractState.pageSize],
|
||||||
|
() => {
|
||||||
|
if (extractVisible.value) fetchExtractRecords();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
function pick(raw: any, ...keys: string[]) {
|
||||||
|
for (const key of keys) {
|
||||||
|
if (raw?.[key] !== undefined && raw?.[key] !== null) return raw[key];
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatTime(value: any) {
|
||||||
|
if (!value) return '';
|
||||||
|
const d = new Date(value);
|
||||||
|
if (Number.isNaN(d.getTime())) return String(value);
|
||||||
|
const p = (v: number) => String(v).padStart(2, '0');
|
||||||
|
return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeRow(raw: any): EquipmentRow {
|
||||||
|
const status = String(pick(raw, 'status', 'Status') || 'inactive');
|
||||||
|
return {
|
||||||
|
id: pick(raw, 'id', 'ID', 'Id'),
|
||||||
|
name: pick(raw, 'name', 'device_name', 'deviceName', 'Name', 'DeviceName'),
|
||||||
|
deviceNo: pick(raw, 'device_no', 'deviceNo', 'DeviceNo', 'serial_no', 'serialNo'),
|
||||||
|
machineCode: pick(raw, 'machine_code', 'machineCode', 'MachineCode', 'fingerprint'),
|
||||||
|
licenseCode: pick(raw, 'license_code', 'licenseCode', 'LicenseCode'),
|
||||||
|
os: pick(raw, 'os', 'OS', 'platform', 'Platform'),
|
||||||
|
version: pick(raw, 'version', 'Version', 'client_version', 'clientVersion'),
|
||||||
|
account: pick(raw, 'account', 'Account', 'email', 'Email'),
|
||||||
|
owner: pick(raw, 'owner', 'Owner', 'user_name', 'userName', 'tenant_name', 'tenantName'),
|
||||||
|
status,
|
||||||
|
activationCount: Number(pick(raw, 'activation_count', 'activationCount', 'ActivationCount') || 0),
|
||||||
|
extractCount: Number(pick(raw, 'extract_count', 'extractCount', 'ExtractCount') || 0),
|
||||||
|
lastActivatedAt: formatTime(pick(raw, 'last_activated_at', 'lastActivatedAt', 'activated_at')),
|
||||||
|
lastExtractedAt: formatTime(pick(raw, 'last_extracted_at', 'lastExtractedAt', 'extracted_at')),
|
||||||
|
expiredAt: formatTime(pick(raw, 'expired_at', 'expiredAt', 'expire_time', 'expireTime')),
|
||||||
|
createdAt: formatTime(pick(raw, 'create_time', 'created_at', 'createdAt', 'CreatedAt')),
|
||||||
|
remark: pick(raw, 'remark', 'Remark'),
|
||||||
|
raw,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeRecord(raw: any): EquipmentRow {
|
||||||
|
return {
|
||||||
|
id: pick(raw, 'id', 'ID', 'Id'),
|
||||||
|
status: pick(raw, 'status', 'Status', 'result', 'Result'),
|
||||||
|
account: pick(raw, 'account', 'Account', 'email', 'Email'),
|
||||||
|
platform: pick(raw, 'platform', 'Platform', 'source', 'Source'),
|
||||||
|
type: pick(raw, 'type', 'Type', 'data_type', 'dataType'),
|
||||||
|
content: pick(raw, 'content', 'Content', 'extract_content', 'extractContent', 'token', 'Token'),
|
||||||
|
ip: pick(raw, 'ip', 'IP', 'client_ip', 'clientIp'),
|
||||||
|
clientVersion: pick(raw, 'client_version', 'clientVersion', 'version', 'Version'),
|
||||||
|
createdAt: formatTime(pick(raw, 'create_time', 'created_at', 'createdAt', 'CreatedAt')),
|
||||||
|
remark: pick(raw, 'remark', 'Remark', 'message', 'Message'),
|
||||||
|
raw,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function statusLabel(status: string) {
|
||||||
|
return statusMap[status]?.label || status || '-';
|
||||||
|
}
|
||||||
|
|
||||||
|
function statusTagType(status: string) {
|
||||||
|
return statusMap[status]?.type || 'info';
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetQuery() {
|
||||||
|
query.keyword = '';
|
||||||
|
query.status = '';
|
||||||
|
query.os = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleSelectionChange(rows: EquipmentRow[]) {
|
||||||
|
selectedRows.value = rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchList() {
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
const res = await getCursorEquipmentList({
|
||||||
|
page: pagination.page,
|
||||||
|
pageSize: pagination.pageSize,
|
||||||
|
keyword: query.keyword || undefined,
|
||||||
|
status: query.status || undefined,
|
||||||
|
os: query.os || undefined,
|
||||||
|
});
|
||||||
|
if (res?.code !== 200) {
|
||||||
|
ElMessage.error(res?.msg || '获取设备列表失败');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const list = Array.isArray(res?.data?.list) ? res.data.list : Array.isArray(res?.data) ? res.data : [];
|
||||||
|
tableData.value = list.map(normalizeRow);
|
||||||
|
total.value = Number(res?.data?.total || list.length || 0);
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function openDetail(row: EquipmentRow) {
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
const res = await getCursorEquipmentDetail(row.id);
|
||||||
|
if (res?.code === 200) {
|
||||||
|
currentRow.value = normalizeRow(res.data || row.raw || row);
|
||||||
|
} else {
|
||||||
|
currentRow.value = row;
|
||||||
|
ElMessage.warning(res?.msg || '详情接口异常,已展示列表数据');
|
||||||
|
}
|
||||||
|
detailVisible.value = true;
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function openAdd() {
|
||||||
|
currentRow.value = null;
|
||||||
|
editVisible.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function openEdit(row: EquipmentRow) {
|
||||||
|
currentRow.value = row;
|
||||||
|
editVisible.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function openDelete(row: EquipmentRow) {
|
||||||
|
currentRow.value = row;
|
||||||
|
deleteVisible.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleSave(payload: EquipmentRow) {
|
||||||
|
actionLoading.value = true;
|
||||||
|
try {
|
||||||
|
const api = payload.id ? updateCursorEquipment : addCursorEquipment;
|
||||||
|
const res = await api(payload);
|
||||||
|
if (res?.code !== 200) {
|
||||||
|
ElMessage.error(res?.msg || '保存失败');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ElMessage.success(payload.id ? '设备已更新' : '设备已新增');
|
||||||
|
editVisible.value = false;
|
||||||
|
await fetchList();
|
||||||
|
} finally {
|
||||||
|
actionLoading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleDelete() {
|
||||||
|
if (!currentRow.value?.id) return;
|
||||||
|
actionLoading.value = true;
|
||||||
|
try {
|
||||||
|
const res = await deleteCursorEquipment(currentRow.value.id);
|
||||||
|
if (res?.code !== 200) {
|
||||||
|
ElMessage.error(res?.msg || '删除失败');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ElMessage.success('设备已删除');
|
||||||
|
deleteVisible.value = false;
|
||||||
|
await fetchList();
|
||||||
|
} finally {
|
||||||
|
actionLoading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleActivate(row: EquipmentRow) {
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm(`确认激活设备「${row.name || row.deviceNo || row.id}」?`, '激活设备', {
|
||||||
|
type: 'info',
|
||||||
|
confirmButtonText: '确认激活',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
const res = await activateCursorEquipment({ id: row.id });
|
||||||
|
if (res?.code !== 200) {
|
||||||
|
ElMessage.error(res?.msg || '激活失败');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ElMessage.success('设备已激活');
|
||||||
|
await fetchList();
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function openActivationRecords(row: EquipmentRow) {
|
||||||
|
currentRow.value = row;
|
||||||
|
activationState.page = 1;
|
||||||
|
activationState.records = [];
|
||||||
|
activationState.total = 0;
|
||||||
|
activationVisible.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function openExtractRecords(row: EquipmentRow) {
|
||||||
|
currentRow.value = row;
|
||||||
|
extractState.page = 1;
|
||||||
|
extractState.records = [];
|
||||||
|
extractState.total = 0;
|
||||||
|
extractVisible.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchActivationRecords() {
|
||||||
|
if (!currentRow.value?.id) return;
|
||||||
|
activationState.loading = true;
|
||||||
|
try {
|
||||||
|
const res = await getCursorEquipmentActivationRecords({
|
||||||
|
equipmentId: currentRow.value.id,
|
||||||
|
page: activationState.page,
|
||||||
|
pageSize: activationState.pageSize,
|
||||||
|
});
|
||||||
|
if (res?.code !== 200) {
|
||||||
|
ElMessage.error(res?.msg || '获取激活记录失败');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const list = Array.isArray(res?.data?.list) ? res.data.list : Array.isArray(res?.data) ? res.data : [];
|
||||||
|
activationState.records = list.map(normalizeRecord);
|
||||||
|
activationState.total = Number(res?.data?.total || list.length || 0);
|
||||||
|
} finally {
|
||||||
|
activationState.loading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchExtractRecords() {
|
||||||
|
if (!currentRow.value?.id) return;
|
||||||
|
extractState.loading = true;
|
||||||
|
try {
|
||||||
|
const res = await getCursorEquipmentExtractRecords({
|
||||||
|
equipmentId: currentRow.value.id,
|
||||||
|
page: extractState.page,
|
||||||
|
pageSize: extractState.pageSize,
|
||||||
|
});
|
||||||
|
if (res?.code !== 200) {
|
||||||
|
ElMessage.error(res?.msg || '获取提取记录失败');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const list = Array.isArray(res?.data?.list) ? res.data.list : Array.isArray(res?.data) ? res.data : [];
|
||||||
|
extractState.records = list.map(normalizeRecord);
|
||||||
|
extractState.total = Number(res?.data?.total || list.length || 0);
|
||||||
|
} finally {
|
||||||
|
extractState.loading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateDeviceType() {
|
||||||
|
isMobile.value = window.innerWidth <= 768;
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
updateDeviceType();
|
||||||
|
window.addEventListener('resize', updateDeviceType);
|
||||||
|
fetchList();
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
window.removeEventListener('resize', updateDeviceType);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="cursor-equipment-page">
|
||||||
|
<el-card shadow="never">
|
||||||
|
<template #header>
|
||||||
|
<div class="card-header">
|
||||||
|
<span>设备管理(Cursor)</span>
|
||||||
|
<el-button type="primary" @click="openAdd">新增设备</el-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div class="summary-grid">
|
||||||
|
<div v-for="item in summary" :key="item.label" class="summary-card">
|
||||||
|
<div class="summary-label">{{ item.label }}</div>
|
||||||
|
<div class="summary-value" :class="`is-${item.type}`">{{ item.value }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="toolbar">
|
||||||
|
<div class="toolbar-left">
|
||||||
|
<el-input
|
||||||
|
v-model="query.keyword"
|
||||||
|
placeholder="搜索设备名称 / 编号 / 机器码 / 账号"
|
||||||
|
clearable
|
||||||
|
class="w-300"
|
||||||
|
/>
|
||||||
|
<el-select v-model="query.status" placeholder="设备状态" clearable class="w-140">
|
||||||
|
<el-option v-for="item in statusOptions" :key="item.value" :label="item.label" :value="item.value" />
|
||||||
|
</el-select>
|
||||||
|
<el-select v-model="query.os" placeholder="系统平台" clearable class="w-140">
|
||||||
|
<el-option v-for="item in osOptions" :key="item.value" :label="item.label" :value="item.value" />
|
||||||
|
</el-select>
|
||||||
|
<el-button @click="resetQuery">重置</el-button>
|
||||||
|
</div>
|
||||||
|
<div class="toolbar-right">
|
||||||
|
<el-button :loading="loading" @click="fetchList">刷新</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="table-scroll">
|
||||||
|
<el-table
|
||||||
|
v-loading="loading"
|
||||||
|
class="equipment-table"
|
||||||
|
:data="tableData"
|
||||||
|
border
|
||||||
|
stripe
|
||||||
|
style="width: 100%"
|
||||||
|
@selection-change="handleSelectionChange"
|
||||||
|
>
|
||||||
|
<el-table-column type="selection" width="52" />
|
||||||
|
<el-table-column prop="id" label="ID" width="80" />
|
||||||
|
<el-table-column label="设备信息" min-width="220">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<div class="device-name">{{ row.name || '-' }}</div>
|
||||||
|
<div class="device-no">编号:{{ row.deviceNo || '-' }}</div>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="machineCode" label="机器码" min-width="220" show-overflow-tooltip />
|
||||||
|
<el-table-column label="状态" width="100" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag :type="statusTagType(row.status)">
|
||||||
|
{{ statusLabel(row.status) }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="os" label="系统" width="100" align="center">
|
||||||
|
<template #default="{ row }">{{ row.os || '-' }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="version" label="版本" width="110" align="center">
|
||||||
|
<template #default="{ row }">{{ row.version || '-' }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="account" label="绑定账号" min-width="160" show-overflow-tooltip />
|
||||||
|
<el-table-column prop="owner" label="归属用户" min-width="130" show-overflow-tooltip />
|
||||||
|
<el-table-column label="激活/提取" width="120" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<div class="count-line">
|
||||||
|
<span>激活 {{ row.activationCount }}</span>
|
||||||
|
<span>提取 {{ row.extractCount }}</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="lastActivatedAt" label="最后激活" width="180">
|
||||||
|
<template #default="{ row }">{{ row.lastActivatedAt || '-' }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="expiredAt" label="过期时间" width="180">
|
||||||
|
<template #default="{ row }">{{ row.expiredAt || '-' }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="remark" label="备注" min-width="160" show-overflow-tooltip />
|
||||||
|
<el-table-column label="操作" width="310" fixed="right" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-button link type="primary" @click="openDetail(row)">详情</el-button>
|
||||||
|
<el-button link type="warning" @click="openEdit(row)">编辑</el-button>
|
||||||
|
<el-button v-if="row.status !== 'active'" link type="success" @click="handleActivate(row)">激活</el-button>
|
||||||
|
<el-button link type="info" @click="openActivationRecords(row)">激活记录</el-button>
|
||||||
|
<el-button link type="info" @click="openExtractRecords(row)">提取记录</el-button>
|
||||||
|
<el-button link type="danger" @click="openDelete(row)">删除</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pager">
|
||||||
|
<el-pagination
|
||||||
|
v-model:current-page="pagination.page"
|
||||||
|
v-model:page-size="pagination.pageSize"
|
||||||
|
background
|
||||||
|
:layout="isMobile ? 'prev, pager, next' : 'total, sizes, prev, pager, next, jumper'"
|
||||||
|
:page-sizes="[20, 50, 100]"
|
||||||
|
:total="total"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<DetailDialog v-model="detailVisible" :row="currentRow" />
|
||||||
|
|
||||||
|
<EditDialog
|
||||||
|
v-model="editVisible"
|
||||||
|
:row="currentRow"
|
||||||
|
:loading="actionLoading"
|
||||||
|
@submit="handleSave"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<DeleteDialog
|
||||||
|
v-model="deleteVisible"
|
||||||
|
:row="currentRow"
|
||||||
|
:loading="actionLoading"
|
||||||
|
@confirm="handleDelete"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ActivationRecords
|
||||||
|
v-model="activationVisible"
|
||||||
|
v-model:page="activationState.page"
|
||||||
|
v-model:page-size="activationState.pageSize"
|
||||||
|
:row="currentRow"
|
||||||
|
:loading="activationState.loading"
|
||||||
|
:records="activationState.records"
|
||||||
|
:total="activationState.total"
|
||||||
|
@refresh="fetchActivationRecords"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ExtractRecords
|
||||||
|
v-model="extractVisible"
|
||||||
|
v-model:page="extractState.page"
|
||||||
|
v-model:page-size="extractState.pageSize"
|
||||||
|
:row="currentRow"
|
||||||
|
:loading="extractState.loading"
|
||||||
|
:records="extractState.records"
|
||||||
|
:total="extractState.total"
|
||||||
|
@refresh="fetchExtractRecords"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.cursor-equipment-page {
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, minmax(120px, 1fr));
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-card {
|
||||||
|
padding: 14px 16px;
|
||||||
|
border: 1px solid #ebeef5;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #fbfcff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-label {
|
||||||
|
color: #909399;
|
||||||
|
font-size: 13px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-value {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #409eff;
|
||||||
|
|
||||||
|
&.is-success {
|
||||||
|
color: #67c23a;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.is-info {
|
||||||
|
color: #909399;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.is-danger {
|
||||||
|
color: #f56c6c;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar-left,
|
||||||
|
.toolbar-right {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.w-300 {
|
||||||
|
width: 300px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.w-140 {
|
||||||
|
width: 140px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-scroll {
|
||||||
|
width: 100%;
|
||||||
|
overflow-x: auto;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.equipment-table {
|
||||||
|
min-width: 1280px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.device-name {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #303133;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.device-no {
|
||||||
|
color: #909399;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.count-line {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
color: #606266;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pager {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
margin-top: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.cursor-equipment-page {
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header {
|
||||||
|
align-items: flex-start;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-grid {
|
||||||
|
grid-template-columns: repeat(2, minmax(120px, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar-left,
|
||||||
|
.toolbar-right,
|
||||||
|
.w-300,
|
||||||
|
.w-140 {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar-right .el-button {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pager {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Loading…
Reference in New Issue
Block a user