502 lines
14 KiB
Vue
502 lines
14 KiB
Vue
<script setup lang="ts">
|
|
import { formatDateTime } from '@/utils/datetime'
|
|
/** 店铺密钥管理页 · 像素复刻旧版 admin.html panel-shop-keys(自绘:面板头+新增店铺密钥、序号列、令牌掩码、白名单状态药丸+悬停详情、旧式分页)。
|
|
* script 逻辑沿用现有 Vue 实现(编辑留空令牌=沿用);弹窗保留现有组件。 */
|
|
import { computed, onMounted, reactive, ref } from 'vue'
|
|
import { ElMessage } from 'element-plus'
|
|
import { fetchShopKeyList, submitCreateShopKey, updateShopKey, deleteShopKey } from './shop-key-api.ts'
|
|
import { emptyShopKeyForm, normalizeZiniaoToken } from './shop-key-form-model.ts'
|
|
import type { ShopKeyItem } from './shop-dto.ts'
|
|
import { whitelistStatusMeta } from './shop-key-model.ts'
|
|
import OldPagination from '@/components/OldPagination.vue'
|
|
|
|
const loading = ref(false)
|
|
const rows = ref<ShopKeyItem[]>([])
|
|
const total = ref(0)
|
|
const page = ref(1)
|
|
const pageSize = ref(10)
|
|
const jumpPage = ref('')
|
|
const totalPages = computed(() => Math.max(1, Math.ceil(total.value / pageSize.value)))
|
|
/** 筛选:备注名/紫鸟账号(用户要求店铺密钥列表补筛选)。 */
|
|
const filterKeyword = ref('')
|
|
const filteredRows = computed(() => {
|
|
const kw = (filterKeyword.value || '').trim().toLowerCase()
|
|
if (!kw) return rows.value
|
|
return rows.value.filter((r) =>
|
|
`${r.remarkName || ''} ${r.ziniaoAccountName || ''}`.toLowerCase().includes(kw),
|
|
)
|
|
})
|
|
|
|
const dialogVisible = ref(false)
|
|
const editingId = ref<number | null>(null)
|
|
const saving = ref(false)
|
|
const form = reactive(emptyShopKeyForm())
|
|
|
|
function maskToken(token: string): string {
|
|
const value = token || ''
|
|
if (value.length <= 8) return '••••••••'
|
|
return `${value.slice(0, 4)}••••••••${value.slice(-4)}`
|
|
}
|
|
|
|
/** 编辑时保留原令牌的内存引用:令牌不落页面明文展示,留空提交则沿用原令牌。 */
|
|
const originalToken = ref('')
|
|
|
|
/** 白名单状态药丸悬停详情:检测时间 + 检测消息(对齐 admin)。 */
|
|
function whitelistTooltip(row: ShopKeyItem): string {
|
|
const parts: string[] = []
|
|
if (row.ipWhitelistCheckedAt) parts.push(`检测时间:${formatDateTime(row.ipWhitelistCheckedAt)}`)
|
|
if (row.ipWhitelistMessage) parts.push(row.ipWhitelistMessage)
|
|
if (parts.length === 0) parts.push(`白名单状态:${whitelistStatusMeta(row.ipWhitelistStatus).label}`)
|
|
return parts.join('\n')
|
|
}
|
|
|
|
async function load() {
|
|
loading.value = true
|
|
try {
|
|
const result = await fetchShopKeyList({ page: page.value, pageSize: pageSize.value })
|
|
rows.value = result.items
|
|
total.value = result.total
|
|
page.value = result.page
|
|
} catch (error) {
|
|
ElMessage.error(error instanceof Error ? error.message : '密钥列表加载失败')
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
function openCreate() {
|
|
editingId.value = null
|
|
Object.assign(form, emptyShopKeyForm())
|
|
dialogVisible.value = true
|
|
}
|
|
|
|
function openEdit(row: ShopKeyItem) {
|
|
editingId.value = row.id
|
|
originalToken.value = row.ziniaoToken
|
|
Object.assign(form, emptyShopKeyForm())
|
|
form.remarkName = row.remarkName
|
|
form.ziniaoAccountName = row.ziniaoAccountName
|
|
dialogVisible.value = true
|
|
}
|
|
|
|
async function submit() {
|
|
const token = normalizeZiniaoToken(form.ziniaoToken)
|
|
const creating = editingId.value == null
|
|
if (creating && !token) {
|
|
ElMessage.warning('请完整填写紫鸟账号名称、紫鸟令牌')
|
|
return
|
|
}
|
|
if (!(form.ziniaoAccountName || '').trim()) {
|
|
ElMessage.warning('请完整填写紫鸟账号名称、紫鸟令牌')
|
|
return
|
|
}
|
|
// 编辑留空令牌 = 沿用原令牌(令牌不明文展示)。
|
|
form.ziniaoToken = creating ? token : token || originalToken.value
|
|
saving.value = true
|
|
try {
|
|
if (creating) {
|
|
await submitCreateShopKey({ ...form })
|
|
} else {
|
|
await updateShopKey(editingId.value as number, { ...form })
|
|
}
|
|
ElMessage.success(creating ? '已新增' : '已保存')
|
|
dialogVisible.value = false
|
|
load()
|
|
} catch (error) {
|
|
ElMessage.error(error instanceof Error ? error.message : '保存失败')
|
|
} finally {
|
|
saving.value = false
|
|
}
|
|
}
|
|
|
|
async function remove(row: ShopKeyItem) {
|
|
// 对齐旧版:原生 confirm。
|
|
if (!window.confirm(`确定删除店铺密钥“${row.ziniaoAccountName}”吗?`)) return
|
|
try {
|
|
await deleteShopKey(row.id)
|
|
ElMessage.success('已删除')
|
|
load()
|
|
} catch (error) {
|
|
ElMessage.error(error instanceof Error ? error.message : '删除失败')
|
|
}
|
|
}
|
|
|
|
function changePage(next: number) {
|
|
if (next < 1 || next > totalPages.value) return
|
|
page.value = next
|
|
load()
|
|
}
|
|
|
|
|
|
function changeSize(size: number) {
|
|
pageSize.value = size
|
|
page.value = 1
|
|
load()
|
|
}
|
|
|
|
function goJump() {
|
|
const n = Number.parseInt(jumpPage.value, 10)
|
|
if (Number.isNaN(n)) {
|
|
ElMessage.warning('请输入页码')
|
|
return
|
|
}
|
|
changePage(Math.min(Math.max(n, 1), totalPages.value))
|
|
}
|
|
|
|
onMounted(load)
|
|
</script>
|
|
|
|
<template>
|
|
<div class="keys-view">
|
|
<section class="panel-box">
|
|
<div class="keys-head">
|
|
<h3>店铺密钥列表</h3>
|
|
<button class="btn" type="button" @click="openCreate">新增店铺密钥</button>
|
|
</div>
|
|
|
|
<div class="form-row keys-filter-row">
|
|
<div class="form-group" style="min-width: 220px">
|
|
<label>备注名 / 紫鸟账号</label>
|
|
<input v-model="filterKeyword" type="text" placeholder="模糊搜索备注名或紫鸟账号" />
|
|
</div>
|
|
</div>
|
|
|
|
<div class="shop-key-table-scroll">
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th style="width: 58px">序号</th>
|
|
<th style="width: 150px">备注名</th>
|
|
<th style="width: 160px">紫鸟账号名称</th>
|
|
<th style="width: 220px">紫鸟令牌</th>
|
|
<th style="width: 150px">白名单状态 <span class="table-help" aria-hidden="true">?</span></th>
|
|
<th style="width: 170px">创建时间</th>
|
|
<th style="width: 170px">修改时间</th>
|
|
<th style="width: 160px">操作</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<template v-if="filteredRows.length">
|
|
<tr v-for="(row, index) in filteredRows" :key="row.id">
|
|
<td>{{ (page - 1) * pageSize + index + 1 }}</td>
|
|
<td>{{ row.remarkName || '—' }}</td>
|
|
<td>{{ row.ziniaoAccountName }}</td>
|
|
<td>
|
|
<span class="mono-mask" :title="row.ziniaoToken">{{ maskToken(row.ziniaoToken) }}</span>
|
|
</td>
|
|
<td>
|
|
<span
|
|
class="wh-pill"
|
|
:class="{
|
|
'is-allowed': whitelistStatusMeta(row.ipWhitelistStatus).tag === 'success',
|
|
'is-blocked': whitelistStatusMeta(row.ipWhitelistStatus).tag === 'danger',
|
|
'is-unknown': whitelistStatusMeta(row.ipWhitelistStatus).tag === 'info',
|
|
}"
|
|
:title="whitelistTooltip(row)"
|
|
>
|
|
{{ whitelistStatusMeta(row.ipWhitelistStatus).label }}
|
|
</span>
|
|
</td>
|
|
<td>{{ formatDateTime(row.createdAt) }}</td>
|
|
<td>{{ formatDateTime(row.updatedAt) }}</td>
|
|
<td class="ops-cell">
|
|
<button class="btn btn-sm" type="button" @click="openEdit(row)">编辑</button>
|
|
<button class="btn btn-sm btn-danger" type="button" @click="remove(row)">删除</button>
|
|
</td>
|
|
</tr>
|
|
</template>
|
|
<tr v-else-if="loading">
|
|
<td colspan="8" class="empty-tip">加载中...</td>
|
|
</tr>
|
|
<tr v-else>
|
|
<td colspan="8" class="empty-tip">{{ filterKeyword ? '暂无匹配密钥' : '暂无店铺密钥' }}</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<OldPagination :total="total" :page="page" :page-size="pageSize" @change="changePage" @size-change="changeSize" />
|
|
</section>
|
|
|
|
<el-dialog v-model="dialogVisible" :title="editingId == null ? '新增密钥' : '编辑密钥'" width="560px">
|
|
<el-form label-width="110px">
|
|
<el-form-item label="备注名">
|
|
<el-input v-model="form.remarkName" placeholder="可选" />
|
|
</el-form-item>
|
|
<el-form-item label="紫鸟账号" required>
|
|
<el-input v-model="form.ziniaoAccountName" placeholder="紫鸟浏览器账号名称" />
|
|
</el-form-item>
|
|
<el-form-item label="紫鸟令牌" required>
|
|
<el-input
|
|
v-model="form.ziniaoToken"
|
|
type="textarea"
|
|
:rows="3"
|
|
:placeholder="editingId == null ? '粘贴紫鸟令牌(将自动去除 Bearer 前缀)' : '留空则沿用原令牌;如需更换请粘贴新令牌(将自动去除 Bearer 前缀)'"
|
|
/>
|
|
</el-form-item>
|
|
</el-form>
|
|
<template #footer>
|
|
<el-button @click="dialogVisible = false">取消</el-button>
|
|
<el-button type="primary" :loading="saving" @click="submit">保存</el-button>
|
|
</template>
|
|
</el-dialog>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
/* 像素复刻旧版 admin.html panel-shop-keys(蓝白末层)。 */
|
|
.keys-view {
|
|
font-family: inherit;
|
|
color: #24384d;
|
|
}
|
|
.panel-box {
|
|
width: 100%;
|
|
min-width: 0;
|
|
padding: 20px 22px 24px;
|
|
border: 1px solid #d8e3ee;
|
|
border-radius: 14px;
|
|
background: linear-gradient(145deg, #ffffff, #f9fbfd);
|
|
box-shadow: 0 1px 2px rgba(39, 67, 94, 0.04), 0 14px 30px -24px rgba(39, 67, 94, 0.28);
|
|
}
|
|
h3 {
|
|
margin: 0;
|
|
font-size: 15px;
|
|
font-weight: 650;
|
|
color: #24384d;
|
|
letter-spacing: 0.2px;
|
|
}
|
|
.keys-head {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 12px;
|
|
margin-bottom: 16px;
|
|
}
|
|
.keys-filter-row {
|
|
margin: 0 0 16px;
|
|
}
|
|
.form-row {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
align-items: flex-end;
|
|
gap: 14px 18px;
|
|
}
|
|
.form-group {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 7px;
|
|
margin-bottom: 0;
|
|
}
|
|
.form-group label {
|
|
color: #5b6f83;
|
|
font-size: 12.5px;
|
|
font-weight: 600;
|
|
}
|
|
.form-group input,
|
|
.form-group select {
|
|
min-width: 0;
|
|
min-height: 42px;
|
|
padding: 8px 12px;
|
|
border: 1px solid #cbd9e6;
|
|
border-radius: 9px;
|
|
background: #f8fbfd;
|
|
color: #24384d;
|
|
font-size: 13.5px;
|
|
font-family: inherit;
|
|
color-scheme: light;
|
|
outline: none;
|
|
transition: border-color 160ms ease, box-shadow 160ms ease, background 160ms ease;
|
|
}
|
|
.form-group input:hover,
|
|
.form-group select:hover {
|
|
border-color: #9fb7cd;
|
|
}
|
|
.form-group input:focus,
|
|
.form-group select:focus {
|
|
background: #ffffff;
|
|
border-color: #5f85ad;
|
|
box-shadow: 0 0 0 3px rgba(95, 133, 173, 0.16);
|
|
}
|
|
.btn {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: 8px;
|
|
min-height: 42px;
|
|
padding: 9px 18px;
|
|
border: 1px solid #4f78a5;
|
|
border-radius: 9px;
|
|
background: linear-gradient(135deg, #5f85ad, #4f78a5);
|
|
color: #ffffff;
|
|
font-family: inherit;
|
|
font-size: 13.5px;
|
|
cursor: pointer;
|
|
box-shadow: 0 8px 18px -14px rgba(79, 120, 165, 0.72);
|
|
transition: background 160ms ease, box-shadow 160ms ease, transform 120ms ease;
|
|
}
|
|
.btn:hover:not(:disabled) {
|
|
background: linear-gradient(135deg, #7094ba, #5d83ac);
|
|
box-shadow: 0 11px 22px -14px rgba(79, 120, 165, 0.8);
|
|
}
|
|
.btn:disabled {
|
|
cursor: not-allowed;
|
|
opacity: 0.55;
|
|
}
|
|
.btn-danger {
|
|
background: linear-gradient(135deg, #c06d77, #b35f6a);
|
|
border-color: #b35f6a;
|
|
}
|
|
.btn-danger:hover:not(:disabled) {
|
|
background: linear-gradient(135deg, #cb7c84, #b96570);
|
|
}
|
|
.btn-sm {
|
|
min-height: 36px;
|
|
padding: 7px 12px;
|
|
}
|
|
.shop-key-table-scroll {
|
|
width: 100%;
|
|
min-width: 0;
|
|
overflow-x: auto;
|
|
border: 1px solid #dbe5ee;
|
|
border-radius: 10px;
|
|
background: #ffffff;
|
|
}
|
|
.shop-key-table-scroll table {
|
|
width: 100%;
|
|
border-collapse: collapse;
|
|
table-layout: fixed;
|
|
min-width: 1000px;
|
|
}
|
|
.shop-key-table-scroll th,
|
|
.shop-key-table-scroll td {
|
|
padding: 10px 12px;
|
|
text-align: left;
|
|
font-size: 13.5px;
|
|
line-height: 1.5;
|
|
border-bottom: 1px solid #e0e8ef;
|
|
vertical-align: middle;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
.shop-key-table-scroll th {
|
|
background: #edf4fa;
|
|
color: #4e6479;
|
|
border-bottom-color: #d5e1eb;
|
|
font-size: 12.5px;
|
|
font-weight: 600;
|
|
letter-spacing: 0.4px;
|
|
}
|
|
.shop-key-table-scroll tbody tr:hover td {
|
|
background: #f1f7fb;
|
|
}
|
|
.shop-key-table-scroll tbody tr:last-child td {
|
|
border-bottom: 0;
|
|
}
|
|
.table-help {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
width: 14px;
|
|
height: 14px;
|
|
margin-left: 3px;
|
|
border-radius: 50%;
|
|
background: #c7d7e5;
|
|
color: #fff;
|
|
font-size: 10px;
|
|
line-height: 1;
|
|
}
|
|
.mono-mask {
|
|
font-family: Consolas, "Cascadia Mono", monospace;
|
|
font-size: 12.5px;
|
|
letter-spacing: 0.5px;
|
|
}
|
|
.wh-pill {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
padding: 2px 10px;
|
|
border-radius: 999px;
|
|
border: 1px solid;
|
|
font-size: 12px;
|
|
line-height: 1.7;
|
|
}
|
|
.wh-pill.is-allowed {
|
|
color: #3d7158;
|
|
background: #e8f2eb;
|
|
border-color: #bdd7c5;
|
|
}
|
|
.wh-pill.is-blocked {
|
|
color: #91474f;
|
|
background: #f8ebeb;
|
|
border-color: #e4c2c5;
|
|
}
|
|
.wh-pill.is-unknown,
|
|
.wh-pill.is-pending,
|
|
.wh-pill.is-notchecked {
|
|
color: #5b6f83;
|
|
background: #f1f6fa;
|
|
border-color: #d6e2ec;
|
|
}
|
|
.ops-cell {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.empty-tip {
|
|
padding: 44px 24px;
|
|
text-align: center;
|
|
color: #8293a5;
|
|
font-size: 13.5px;
|
|
}
|
|
.pagination {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
align-items: center;
|
|
gap: 10px;
|
|
margin-top: 18px;
|
|
color: #5b6f83;
|
|
font-size: 13px;
|
|
}
|
|
.pagination button {
|
|
min-height: 30px;
|
|
padding: 4px 12px;
|
|
border: 1px solid #c7d7e5;
|
|
border-radius: 8px;
|
|
background: #ffffff;
|
|
color: #5b6f83;
|
|
font-family: inherit;
|
|
font-size: 13px;
|
|
cursor: pointer;
|
|
}
|
|
.pagination button:hover:not(:disabled) {
|
|
background: #edf5fb;
|
|
border-color: #95b1cb;
|
|
color: #2f5d8b;
|
|
}
|
|
.pagination button:disabled {
|
|
background: #eef3f7;
|
|
color: #9baaba;
|
|
cursor: not-allowed;
|
|
}
|
|
.page-total {
|
|
margin-right: 4px;
|
|
}
|
|
.page-jump {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
}
|
|
.page-jump input {
|
|
width: 56px;
|
|
min-height: 30px;
|
|
padding: 4px 8px;
|
|
border: 1px solid #cbd9e6;
|
|
border-radius: 8px;
|
|
background: #f8fbfd;
|
|
color: #24384d;
|
|
font-size: 13px;
|
|
font-family: inherit;
|
|
}
|
|
</style>
|