refactor(router): 将管理页面移至admin文件夹
部署开发环境 / deploy-dev (push) Successful in 2m55s

This commit is contained in:
2026-06-13 23:41:32 +08:00
parent 9e8db7754f
commit ae0dddd255
3 changed files with 2 additions and 2 deletions
+533
View File
@@ -0,0 +1,533 @@
<script setup lang="ts">
import {onMounted, onUnmounted, ref, watch} from 'vue'
import {useCounterStore} from "@/stores/counter.ts"
import api from "@/api"
import {ElMessage, ElMessageBox} from 'element-plus'
import {DataAnalysis, Delete, Document, InfoFilled, Key, Refresh, Search, Warning} from '@element-plus/icons-vue'
const store = useCounterStore()
const result = ref<any>(null)
const value = ref('')
const options = ref<string[]>([])
const loading = ref(false)
const lastUpdate = ref('')
const deleteSpecifyDataVisible = ref(false)
const inputSpecifyData = ref('')
const deleteSpecifyDedupVisible = ref(false)
const inputSpecifyDedup = ref('')
const deleteSpecifyRawVisible = ref(false)
const inputSpecifyRaw = ref('')
const isLoading = ref(false)
const fetchInfo = async () => {
if (!value.value) return
const res = await api.getTokenInfo({token: value.value})
if (res.status === 200) {
result.value = res.data.data
lastUpdate.value = new Date().toLocaleTimeString()
}
}
const fetchTokens = async () => {
try {
const res = await api.getTokenList()
if (res.status === 200) {
options.value = res.data.data?.map((item: any) => item.token) || []
}
} catch (error) {
console.error('Failed to fetch tokens:', error)
}
}
const refresh = async () => {
loading.value = true
await fetchInfo()
loading.value = false
ElMessage({message: '刷新成功', type: 'success', duration: 1500})
}
const deleteDedup = () => {
ElMessageBox.confirm('确定要删除全部去重参考值吗?', '警告', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}).then(async () => {
await api.deleteTokenInfo({token: value.value, dedup_bf: "all"})
ElMessage({message: '删除成功', type: 'success'})
await fetchInfo()
}).catch(() => {
})
}
const deleteRedis = () => {
ElMessageBox.confirm('确定要删除全部原始数据吗?', '警告', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}).then(async () => {
await api.deleteTokenInfo({token: value.value, cache_list: "all"})
ElMessage({message: '删除成功', type: 'success'})
await fetchInfo()
}).catch(() => {
})
}
const deleteSpecifyDedup = async () => {
isLoading.value = true
await api.deleteTokenInfo({token: value.value, dedup_bf: inputSpecifyDedup.value})
isLoading.value = false
ElMessage({message: '删除成功', type: 'success'})
deleteSpecifyDedupVisible.value = false
inputSpecifyDedup.value = ''
await fetchInfo()
}
const deleteSpecifyRaw = async () => {
isLoading.value = true
await api.deleteTokenInfo({token: value.value, cache_list: inputSpecifyRaw.value})
isLoading.value = false
ElMessage({message: '删除成功', type: 'success'})
deleteSpecifyRawVisible.value = false
inputSpecifyRaw.value = ''
await fetchInfo()
}
const deleteSpecifyData = async () => {
isLoading.value = true
await api.deleteTokenInfo({token: value.value, both_number: inputSpecifyData.value})
isLoading.value = false
ElMessage({message: '删除成功', type: 'success'})
deleteSpecifyDataVisible.value = false
inputSpecifyData.value = ''
await fetchInfo()
}
let timer: number
onMounted(() => {
fetchTokens()
if (store.token) {
value.value = store.token
}
})
watch(value, (newValue) => {
store.token = newValue
if (newValue) fetchInfo()
})
onMounted(() => {
if (value.value) fetchInfo()
timer = window.setInterval(fetchInfo, 5000)
})
onUnmounted(() => {
clearInterval(timer)
})
const statCards = [
{label: '去重对象', key: 'dedup_object', icon: DataAnalysis, color: '#409eff'},
{label: '上传数据格式', key: 'data_format', icon: Document, color: '#67c23a'},
{label: '去重参考值数量', key: 'dedup_items_number', icon: Key, color: '#e6a23c'},
{label: '原始数据数量', key: 'cache_list_number', icon: Warning, color: '#909399'},
]
</script>
<template>
<div class="token-detail">
<!-- 检查权限 -->
<div v-if="!store.isAdmin" class="no-permission">
<div class="no-permission-card">
<el-icon size="64" color="#f56c6c">
<InfoFilled/>
</el-icon>
<div class="no-permission-title">无权限访问</div>
<div class="no-permission-text">您没有权限访问此页面</div>
</div>
</div>
<!-- 管理页面 -->
<div v-else class="detail-page">
<div class="content-card">
<div class="page-title">Token 详情</div>
<!-- 选择 Token -->
<div class="token-select">
<el-select v-model="value" placeholder="选择 Token" clearable style="width: 100%; max-width: 300px">
<el-option v-for="item in options" :key="item" :value="item" :label="item"/>
</el-select>
<el-button type="primary" @click="refresh" :loading="loading">
<el-icon>
<Refresh/>
</el-icon>
刷新
</el-button>
<span v-if="lastUpdate" class="update-time">上次更新: {{ lastUpdate }}</span>
</div>
<!-- Token 信息 -->
<div v-if="result && value" class="info-section">
<div class="section-title">Token 信息</div>
<div class="stat-cards">
<div v-for="card in statCards" :key="card.key" class="stat-card">
<div class="stat-icon" :style="{ background: card.color + '20', color: card.color }">
<el-icon size="24">
<component :is="card.icon"/>
</el-icon>
</div>
<div class="stat-content">
<div class="stat-label">{{ card.label }}</div>
<div class="stat-value">{{ result[card.key] || '-' }}</div>
</div>
</div>
</div>
<!-- 管理操作 -->
<div class="manage-section">
<div class="section-title">数据管理</div>
<div class="action-grid">
<div class="action-card danger">
<div class="action-icon">
<el-icon size="28">
<Delete/>
</el-icon>
</div>
<div class="action-info">
<div class="action-label">删除全部去重参考值</div>
<div class="action-desc">清除该 Token 的所有去重布隆过滤器</div>
</div>
<el-button type="danger" @click="deleteDedup">执行</el-button>
</div>
<div class="action-card danger">
<div class="action-icon">
<el-icon size="28">
<Delete/>
</el-icon>
</div>
<div class="action-info">
<div class="action-label">删除全部原始数据</div>
<div class="action-desc">清除该 Token 的所有原始缓存数据</div>
</div>
<el-button type="danger" @click="deleteRedis">执行</el-button>
</div>
</div>
<div class="section-subtitle">按数量删除</div>
<div class="action-grid">
<div class="action-card">
<div class="action-icon warning">
<el-icon size="28">
<Search/>
</el-icon>
</div>
<div class="action-info">
<div class="action-label">删除指定数量去重参考值</div>
<div class="action-desc">指定要删除的去重参考值数量</div>
</div>
<el-button type="warning" @click="deleteSpecifyDedupVisible = true">指定</el-button>
</div>
<div class="action-card">
<div class="action-icon warning">
<el-icon size="28">
<Search/>
</el-icon>
</div>
<div class="action-info">
<div class="action-label">删除指定数量原始数据</div>
<div class="action-desc">指定要删除的原始数据数量</div>
</div>
<el-button type="warning" @click="deleteSpecifyRawVisible = true">指定</el-button>
</div>
<div class="action-card">
<div class="action-icon warning">
<el-icon size="28">
<Search/>
</el-icon>
</div>
<div class="action-info">
<div class="action-label">删除指定数量数据</div>
<div class="action-desc">同时删除去重参考值和原始数据</div>
</div>
<el-button type="warning" @click="deleteSpecifyDataVisible = true">指定</el-button>
</div>
</div>
</div>
</div>
<!-- 空状态 -->
<div v-else-if="value" class="empty-state">
<el-icon size="64" color="#dcdfe6">
<InfoFilled/>
</el-icon>
<div class="empty-text">暂无数据</div>
</div>
<!-- 未选择 -->
<div v-else class="empty-state">
<el-icon size="64" color="#dcdfe6">
<Key/>
</el-icon>
<div class="empty-text">请选择 Token 查看详情</div>
</div>
</div>
<!-- 删除确认对话框 -->
<el-dialog v-model="deleteSpecifyDedupVisible" title="删除指定数量去重参考值" width="400">
<el-input v-model="inputSpecifyDedup" placeholder="请输入删除数量" type="number"/>
<template #footer>
<el-button @click="deleteSpecifyDedupVisible = false">取消</el-button>
<el-button type="primary" @click="deleteSpecifyDedup" :loading="isLoading">确定</el-button>
</template>
</el-dialog>
<el-dialog v-model="deleteSpecifyRawVisible" title="删除指定数量原始数据" width="400">
<el-input v-model="inputSpecifyRaw" placeholder="请输入删除数量" type="number"/>
<template #footer>
<el-button @click="deleteSpecifyRawVisible = false">取消</el-button>
<el-button type="primary" @click="deleteSpecifyRaw" :loading="isLoading">确定</el-button>
</template>
</el-dialog>
<el-dialog v-model="deleteSpecifyDataVisible" title="删除指定数量数据" width="400">
<el-input v-model="inputSpecifyData" placeholder="请输入删除数量" type="number"/>
<template #footer>
<el-button @click="deleteSpecifyDataVisible = false">取消</el-button>
<el-button type="primary" @click="deleteSpecifyData" :loading="isLoading">确定</el-button>
</template>
</el-dialog>
</div>
</div>
</template>
<style scoped>
.token-detail {
max-width: 1600px;
margin: 0 auto;
width: 100%;
padding: 0;
}
/* 无权限 */
.no-permission {
display: flex;
justify-content: center;
align-items: center;
min-height: 60vh;
}
.no-permission-card {
text-align: center;
padding: 48px;
}
.no-permission-title {
font-size: 24px;
font-weight: 600;
margin-top: 24px;
margin-bottom: 8px;
}
.no-permission-text {
color: var(--text-secondary);
}
/* Token 选择 */
.token-select {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 24px;
flex-wrap: wrap;
}
.update-time {
font-size: 13px;
color: var(--text-secondary);
}
/* 信息卡片 */
.info-section {
animation: fadeIn 0.3s ease;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.stat-cards {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 16px;
margin-bottom: 32px;
}
.stat-card {
display: flex;
align-items: center;
gap: 16px;
padding: 20px;
background: var(--bg-page);
border-radius: 12px;
transition: transform 0.2s, box-shadow 0.2s;
}
.stat-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
}
.stat-icon {
width: 56px;
height: 56px;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
}
.stat-label {
font-size: 13px;
color: var(--text-secondary);
margin-bottom: 4px;
}
.stat-value {
font-size: 18px;
font-weight: 600;
color: var(--text-primary);
word-break: break-all;
}
/* 管理操作 */
.manage-section {
margin-top: 32px;
}
.section-subtitle {
font-size: 14px;
font-weight: 600;
color: var(--text-secondary);
margin: 24px 0 16px;
}
.action-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 16px;
}
.action-card {
display: flex;
align-items: center;
gap: 16px;
padding: 20px;
background: var(--bg-page);
border-radius: 12px;
transition: transform 0.2s, box-shadow 0.2s;
}
.action-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
}
.action-icon {
width: 56px;
height: 56px;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
background: var(--primary-color);
color: #fff;
}
.action-icon.warning {
background: var(--warning-color);
}
.action-info {
flex: 1;
min-width: 0;
}
.action-label {
font-size: 15px;
font-weight: 600;
color: var(--text-primary);
margin-bottom: 4px;
}
.action-desc {
font-size: 13px;
color: var(--text-secondary);
}
/* 空状态 */
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
padding: 64px;
color: var(--text-secondary);
}
.empty-text {
margin-top: 16px;
font-size: 15px;
}
@media (max-width: 1200px) {
.stat-cards {
grid-template-columns: repeat(2, 1fr);
}
.action-grid {
grid-template-columns: repeat(2, 1fr);
}
}
@media (max-width: 768px) {
.token-select {
flex-direction: column;
align-items: stretch;
}
.stat-cards {
grid-template-columns: 1fr;
}
.action-grid {
grid-template-columns: 1fr;
}
.action-card {
flex-direction: column;
text-align: center;
}
}
</style>
+528
View File
@@ -0,0 +1,528 @@
<script setup lang="ts">
import {onMounted, ref} from "vue"
import api from "@/api"
import {ElMessage} from 'element-plus'
import {useCounterStore} from "@/stores/counter.ts"
import {useRouter} from "vue-router"
import {Delete, Edit, Key, Lock, Memo, Plus, View} from '@element-plus/icons-vue'
import AddDataDialog from '@/components/AddDataDialog.vue'
const store = useCounterStore()
const router = useRouter()
const tableData = ref<any[]>([])
const loading = ref(false)
const input = ref('')
const value = ref('')
const dataFormat = ref('')
const inputNotes = ref('')
const inputPassWord = ref('')
const passwordVisible = ref(true)
const rowOut = ref<any>(null)
const addDataDialogVisible = ref(false)
const dedupObjectVisible = ref(false)
const dataFormatVisible = ref(false)
const inputNotesVisible = ref(false)
const options = [
{value: 'uid', label: 'uid'},
{value: 'secid', label: 'secid'},
{value: 'pid', label: 'pid'},
{value: 'comment_id', label: 'comment_id'},
{value: 'dyid', label: 'dyid'}
]
const dataFormatOptions = [
{value: 'uid----secid----pid----comment_id', label: 'uid----secid----pid----comment_id'},
{value: 'uid----secid', label: 'uid----secid'},
{value: 'dyid', label: 'dyid'}
]
const fetchTokens = async () => {
loading.value = true
try {
const res = await api.getTokenList()
tableData.value = res.data.data || []
} finally {
loading.value = false
}
}
onMounted(() => {
if (!store.isAdmin) {
passwordVisible.value = true
} else {
fetchTokens()
}
})
const checkPassword = () => {
if (inputPassWord.value === "haha") {
ElMessage({message: '登录成功', type: 'success'})
store.isAdmin = true
passwordVisible.value = false
fetchTokens()
} else {
ElMessage.error('密码错误')
}
}
const addToken = async () => {
if (!input.value || !value.value || !dataFormat.value) {
ElMessage.warning('请填写完整信息')
return
}
try {
await api.createToken({
token: input.value,
dedup_object: value.value,
data_format: dataFormat.value,
notes: inputNotes.value,
})
ElMessage({message: '添加成功', type: 'success'})
input.value = ''
value.value = ''
dataFormat.value = ''
inputNotes.value = ''
fetchTokens()
} catch (error: any) {
ElMessage.error(error.response?.data?.error || '添加失败')
}
}
const viewDetails = (row: any) => {
store.token = row.token
router.push({name: "TokenDetail"})
}
const dialogDedupObjectVisible = (row: any) => {
rowOut.value = row
value.value = row.dedup_object
dedupObjectVisible.value = true
}
const dialogDataFormatVisible = (row: any) => {
rowOut.value = row
dataFormat.value = row.data_format
dataFormatVisible.value = true
}
const dialogNotesVisible = (row: any) => {
rowOut.value = row
inputNotes.value = row.notes
inputNotesVisible.value = true
}
const dialogDataADDVisible = (row: any) => {
rowOut.value = row
addDataDialogVisible.value = true
}
const updateDedupObject = async () => {
try {
await api.updateToken({
token: rowOut.value.token,
dedup_object: value.value,
data_format: rowOut.value.data_format,
notes: rowOut.value.notes,
})
ElMessage({message: '更新成功', type: 'success'})
dedupObjectVisible.value = false
fetchTokens()
} catch (error: any) {
ElMessage.error(error.response?.data?.error || '更新失败')
}
}
const updateDataFormat = async () => {
try {
await api.updateToken({
token: rowOut.value.token,
dedup_object: rowOut.value.dedup_object,
data_format: dataFormat.value,
notes: rowOut.value.notes,
})
ElMessage({message: '更新成功', type: 'success'})
dataFormatVisible.value = false
fetchTokens()
} catch (error: any) {
ElMessage.error(error.response?.data?.error || '更新失败')
}
}
const updateNotes = async () => {
try {
await api.updateToken({
token: rowOut.value.token,
dedup_object: rowOut.value.dedup_object,
data_format: rowOut.value.data_format,
notes: inputNotes.value,
})
ElMessage({message: '更新成功', type: 'success'})
inputNotesVisible.value = false
fetchTokens()
} catch (error: any) {
ElMessage.error(error.response?.data?.error || '更新失败')
}
}
const deleteToken = async (row: any) => {
try {
await api.deleteToken({token: row.token})
ElMessage({message: '删除成功', type: 'success'})
fetchTokens()
} catch (error: any) {
ElMessage.error(error.response?.data?.error || '删除失败')
}
}
</script>
<template>
<div class="token-manage">
<!-- 登录页面 -->
<div v-if="!store.isAdmin" class="login-page">
<div class="login-card">
<div class="login-icon">
<el-icon size="48" color="#409eff">
<Lock/>
</el-icon>
</div>
<div class="login-title">管理员登录</div>
<div class="login-subtitle">请输入管理员密码访问管理后台</div>
<el-input
v-model="inputPassWord"
type="password"
placeholder="请输入管理员密码"
size="large"
show-password
@keyup.enter="checkPassword"
>
<template #prefix>
<el-icon>
<Lock/>
</el-icon>
</template>
</el-input>
<el-button type="primary" size="large" class="login-btn" @click="checkPassword">
登录
</el-button>
</div>
</div>
<!-- 管理页面 -->
<div v-else class="manage-page">
<div class="content-card">
<div class="page-title">Token 管理</div>
<!-- 添加表单 -->
<div class="add-form">
<div class="form-title">添加新 Token</div>
<div class="form-grid">
<el-input v-model="input" placeholder="Token 名称" clearable>
<template #prefix>
<el-icon>
<Key/>
</el-icon>
</template>
</el-input>
<el-select v-model="value" placeholder="去重对象" clearable>
<el-option v-for="item in options" :key="item.value" :value="item.value" :label="item.label"/>
</el-select>
<el-select v-model="dataFormat" placeholder="数据格式" clearable>
<el-option v-for="item in dataFormatOptions" :key="item.value" :value="item.value" :label="item.label"/>
</el-select>
<el-input v-model="inputNotes" placeholder="备注(可选)" clearable>
<template #prefix>
<el-icon>
<Memo/>
</el-icon>
</template>
</el-input>
<el-button type="primary" @click="addToken">
<el-icon>
<Plus/>
</el-icon>
添加
</el-button>
</div>
</div>
<!-- Token 列表 -->
<div class="table-section">
<div class="section-header">
<span class="section-title">Token 列表</span>
<span class="table-count"> {{ tableData.length }} </span>
</div>
<el-table :data="tableData" v-loading="loading" stripe style="width: 100%">
<el-table-column prop="token" label="Token" min-width="100" show-overflow-tooltip>
<template #default="{ row }">
<div class="token-cell">
<el-icon>
<Key/>
</el-icon>
<span>{{ row.token }}</span>
</div>
</template>
</el-table-column>
<el-table-column prop="dedup_object" label="去重对象" width="150">
<template #default="{ row }">
<el-tag type="primary" effect="plain">{{ row.dedup_object }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="data_format" label="数据格式" min-width="200" show-overflow-tooltip>
<template #default="{ row }">
<div class="format-cell">{{ row.data_format }}</div>
</template>
</el-table-column>
<el-table-column prop="notes" label="备注" min-width="150" show-overflow-tooltip>
<template #default="{ row }">
<span class="notes-text">{{ row.notes || '-' }}</span>
</template>
</el-table-column>
<el-table-column label="操作" width="500" fixed="right">
<template #default="{ row }">
<div class="action-buttons">
<el-button type="primary" size="small" text @click="viewDetails(row)">
<el-icon>
<View/>
</el-icon>
详情
</el-button>
<el-button size="small" text @click="dialogDataADDVisible(row)">
<el-icon>
<Plus/>
</el-icon>
数据
</el-button>
<el-button size="small" text @click="dialogDedupObjectVisible(row)">
<el-icon>
<Edit/>
</el-icon>
去重对象
</el-button>
<el-button size="small" text @click="dialogDataFormatVisible(row)">
<el-icon>
<Edit/>
</el-icon>
数据格式
</el-button>
<el-button size="small" text @click="dialogNotesVisible(row)">
<el-icon>
<Edit/>
</el-icon>
备注
</el-button>
<el-popconfirm
title="确认删除此 Token 吗?"
confirm-button-text="确认"
cancel-button-text="取消"
@confirm="deleteToken(row)"
>
<template #reference>
<el-button type="danger" size="small" text>
<el-icon>
<Delete/>
</el-icon>
删除
</el-button>
</template>
</el-popconfirm>
</div>
</template>
</el-table-column>
</el-table>
</div>
</div>
<!-- 编辑对话框 -->
<el-dialog v-model="dedupObjectVisible" title="更改去重对象" width="400">
<el-select v-model="value" placeholder="选择去重对象" style="width: 100%">
<el-option v-for="item in options" :key="item.value" :value="item.value" :label="item.label"/>
</el-select>
<template #footer>
<el-button @click="dedupObjectVisible = false">取消</el-button>
<el-button type="primary" @click="updateDedupObject">确定</el-button>
</template>
</el-dialog>
<el-dialog v-model="dataFormatVisible" title="更改数据格式" width="400">
<el-select v-model="dataFormat" placeholder="选择数据格式" style="width: 100%">
<el-option v-for="item in dataFormatOptions" :key="item.value" :value="item.value" :label="item.label"/>
</el-select>
<template #footer>
<el-button @click="dataFormatVisible = false">取消</el-button>
<el-button type="primary" @click="updateDataFormat">确定</el-button>
</template>
</el-dialog>
<el-dialog v-model="inputNotesVisible" title="更改备注" width="400">
<el-input v-model="inputNotes" placeholder="请输入备注"/>
<template #footer>
<el-button @click="inputNotesVisible = false">取消</el-button>
<el-button type="primary" @click="updateNotes">确定</el-button>
</template>
</el-dialog>
<AddDataDialog
v-model="addDataDialogVisible"
:token="rowOut?.token"
/>
</div>
</div>
</template>
<style scoped>
.token-manage {
max-width: 1600px;
margin: 0 auto;
width: 100%;
padding: 0;
}
/* 登录页面 */
.login-page {
display: flex;
justify-content: center;
align-items: center;
min-height: 60vh;
}
.login-card {
width: 100%;
max-width: 400px;
text-align: center;
padding: 48px 40px;
background: var(--bg-card);
border-radius: 16px;
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.08);
}
.login-icon {
margin-bottom: 24px;
}
.login-title {
font-size: 24px;
font-weight: 600;
color: var(--text-primary);
margin-bottom: 8px;
}
.login-subtitle {
font-size: 14px;
color: var(--text-secondary);
margin-bottom: 32px;
}
.login-card :deep(.el-input) {
margin-bottom: 16px;
}
.login-btn {
width: 100%;
margin-top: 8px;
}
/* 管理页面 */
.add-form {
margin-bottom: 32px;
padding: 24px;
background: var(--bg-page);
border-radius: 12px;
}
.form-title {
font-size: 15px;
font-weight: 600;
color: var(--text-primary);
margin-bottom: 16px;
}
.form-grid {
display: flex;
flex-wrap: wrap;
gap: 12px;
align-items: center;
}
.form-grid .el-input {
width: 220px;
}
.form-grid .el-select {
width: 280px;
}
/* 表格 */
.table-section {
margin-top: 8px;
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
}
.table-count {
font-size: 13px;
color: var(--text-secondary);
}
.token-cell {
display: flex;
align-items: center;
gap: 8px;
}
.format-cell {
font-size: 12px;
word-break: break-all;
}
.notes-text {
color: var(--text-secondary);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.action-buttons {
display: flex;
gap: 4px;
flex-wrap: wrap;
}
@media (max-width: 768px) {
.form-grid {
flex-direction: column;
align-items: stretch;
}
.form-grid .el-input,
.form-grid .el-select {
width: 100%;
}
.form-grid .el-button {
width: 100%;
}
.action-buttons {
flex-direction: column;
}
}
</style>