Files
par/web/src/views/Psks.vue

203 lines
6.3 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div>
<div style="display:flex;justify-content:space-between;align-items:center">
<h2>PSK 管理</h2>
<el-button type="primary" @click="showAddDialog">
<el-icon><Plus /></el-icon>
生成 PSK
</el-button>
</div>
<el-card style="margin-top:16px">
<el-alert type="info" :closable="false" show-icon>
<template #title>
PSK(预共享密钥)用于外部客户端(如 mediabot)接入认证。
每个 PSK 生成后需将密钥分发给客户端配置使用。
</template>
</el-alert>
</el-card>
<el-table :data="pskList" border stripe v-loading="loading" style="width: 100%; margin-top: 16px">
<el-table-column prop="id" label="ID" width="70" />
<el-table-column prop="version" label="版本" width="100" />
<el-table-column label="密钥" min-width="200">
<template #default="{ row }">
<span v-if="!row.showSecret">{{ '•'.repeat(32) }}</span>
<span v-else class="secret-text">{{ row.secret }}</span>
<el-button link type="primary" size="small" @click="row.showSecret = !row.showSecret" style="margin-left: 8px">
{{ row.showSecret ? '隐藏' : '显示' }}
</el-button>
<el-button link type="primary" size="small" @click="copySecret(row.secret)">复制</el-button>
</template>
</el-table-column>
<el-table-column prop="description" label="描述" min-width="150">
<template #default="{ row }">
{{ row.description || '-' }}
</template>
</el-table-column>
<el-table-column label="状态" width="90">
<template #default="{ row }">
<el-tag :type="row.isActive ? 'success' : 'danger'" size="small">
{{ row.isActive ? '启用' : '禁用' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="createdAt" label="创建时间" width="180" />
<el-table-column label="操作" width="100" fixed="right">
<template #default="{ row }">
<el-popconfirm
:title="row.isActive ? '确认禁用此 PSK?禁用后客户端将无法使用该密钥认证' : '确认启用此 PSK?'"
@confirm="handleToggle(row)"
>
<template #reference>
<el-button :type="row.isActive ? 'danger' : 'success'" size="small" link>
{{ row.isActive ? '禁用' : '启用' }}
</el-button>
</template>
</el-popconfirm>
</template>
</el-table-column>
</el-table>
<!-- 生成 PSK 弹窗 -->
<el-dialog v-model="addDialogVisible" title="生成 PSK" width="480px" :close-on-click-modal="false">
<el-form :model="addForm" label-width="80px">
<el-form-item label="版本号">
<el-input v-model="addForm.version" placeholder="如 v2, v3" />
<div class="form-tip">唯一标识,不可与已有版本重复</div>
</el-form-item>
<el-form-item label="描述">
<el-input v-model="addForm.description" placeholder="此 PSK 的用途或使用方" />
</el-form-item>
<el-form-item label="密钥">
<el-input v-model="addForm.secret" placeholder="留空自动生成" />
<el-button link type="primary" size="small" @click="generateSecret" style="margin-top: 4px">
随机生成
</el-button>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="addDialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleAdd" :loading="adding">生成</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import api from '../api'
import { ElMessage } from 'element-plus'
const pskList = ref([])
const loading = ref(false)
const adding = ref(false)
const addDialogVisible = ref(false)
const addForm = reactive({
version: '',
description: '',
secret: ''
})
const fetchList = async () => {
loading.value = true
try {
const res = await api.psks.list()
pskList.value = (res.data || []).map(item => ({ ...item, showSecret: false }))
} catch {
ElMessage.error('获取 PSK 列表失败')
} finally {
loading.value = false
}
}
const showAddDialog = () => {
addForm.version = 'v' + (pskList.value.length + 1)
addForm.description = ''
addForm.secret = ''
addDialogVisible.value = true
}
const generateSecret = () => {
const chars = 'abcdefghijklmnopqrstuvwxyz0123456789'
let result = ''
for (let i = 0; i < 32; i++) {
result += chars[Math.floor(Math.random() * chars.length)]
}
addForm.secret = result
}
const handleAdd = async () => {
if (!addForm.version.trim()) {
ElMessage.warning('请输入版本号')
return
}
if (!addForm.secret.trim()) {
ElMessage.warning('请输入或生成密钥')
return
}
adding.value = true
try {
await api.psks.add({
version: addForm.version.trim(),
description: addForm.description.trim(),
secret: addForm.secret.trim()
})
ElMessage.success('PSK 已生成')
addDialogVisible.value = false
fetchList()
} catch (e) {
ElMessage.error(e.response?.data?.message || '生成失败')
} finally {
adding.value = false
}
}
const handleToggle = async (row) => {
try {
await api.psks.toggle(row.id)
ElMessage.success(row.isActive ? 'PSK 已禁用' : 'PSK 已启用')
fetchList()
} catch {
ElMessage.error('操作失败')
}
}
const copySecret = async (secret) => {
try {
// 优先 clipboard API(需要 HTTPS)
if (navigator.clipboard && window.isSecureContext) {
await navigator.clipboard.writeText(secret)
} else {
// HTTP 降级:execCommand
const textarea = document.createElement('textarea')
textarea.value = secret
textarea.style.position = 'fixed'
textarea.style.opacity = '0'
document.body.appendChild(textarea)
textarea.select()
document.execCommand('copy')
document.body.removeChild(textarea)
}
ElMessage.success('已复制到剪贴板')
} catch {
ElMessage.error('复制失败,请手动复制')
}
}
onMounted(fetchList)
</script>
<style scoped>
.secret-text {
font-family: monospace;
word-break: break-all;
}
.form-tip {
font-size: 12px;
color: #909399;
margin-top: 4px;
}
</style>