feat: 操作日志重写+已审核标签+配方名自适应
All checks were successful
PR Preview / teardown-preview (pull_request) Has been skipped
Test / unit-test (push) Successful in 4s
Test / build-check (push) Successful in 3s
PR Preview / test (pull_request) Successful in 4s
PR Preview / deploy-preview (pull_request) Successful in 15s
Test / e2e-test (push) Successful in 1m0s
All checks were successful
PR Preview / teardown-preview (pull_request) Has been skipped
Test / unit-test (push) Successful in 4s
Test / build-check (push) Successful in 3s
PR Preview / test (pull_request) Successful in 4s
PR Preview / deploy-preview (pull_request) Successful in 15s
Test / e2e-test (push) Successful in 1m0s
操作日志: - 完全重写,正确映射所有 action 类型(配方/精油/用户/标签/审核) - 三级筛选:操作类型、用户、对象类型 - 正确解析 detail JSON 显示来源/原因/角色等 - 包含精油价目修改记录 已审核标签: - editor+ 可见可编辑,viewer 不可见 - 蓝色样式区分 其他: - 配方卡片名称过长自动缩小字号 - 配方编辑器加新增标签输入框 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,8 +1,8 @@
|
||||
<template>
|
||||
<div class="recipe-card" @click="$emit('click', index)">
|
||||
<div class="recipe-card-name">{{ recipe.name }}</div>
|
||||
<div v-if="recipe.tags && recipe.tags.length" class="recipe-card-tags">
|
||||
<span v-for="tag in recipe.tags" :key="tag" class="tag">{{ tag }}</span>
|
||||
<div class="recipe-card-name" :style="{ fontSize: recipe.name.length > 12 ? (recipe.name.length > 20 ? '12px' : '14px') : '16px' }">{{ recipe.name }}</div>
|
||||
<div v-if="visibleTags.length" class="recipe-card-tags">
|
||||
<span v-for="tag in visibleTags" :key="tag" class="tag" :class="{ 'tag-reviewed': tag === '已审核' }">{{ tag }}</span>
|
||||
</div>
|
||||
<div class="recipe-card-oils">{{ oilNames }}</div>
|
||||
<div class="recipe-card-bottom">
|
||||
@@ -21,6 +21,9 @@
|
||||
import { computed } from 'vue'
|
||||
import { useOilsStore } from '../stores/oils'
|
||||
import { useRecipesStore } from '../stores/recipes'
|
||||
import { useAuthStore } from '../stores/auth'
|
||||
|
||||
const EDITOR_ONLY_TAGS = ['已审核']
|
||||
|
||||
const props = defineProps({
|
||||
recipe: { type: Object, required: true },
|
||||
@@ -31,6 +34,13 @@ defineEmits(['click', 'toggle-fav'])
|
||||
|
||||
const oilsStore = useOilsStore()
|
||||
const recipesStore = useRecipesStore()
|
||||
const auth = useAuthStore()
|
||||
|
||||
const visibleTags = computed(() => {
|
||||
if (!props.recipe.tags) return []
|
||||
if (auth.canEdit) return props.recipe.tags
|
||||
return props.recipe.tags.filter(t => !EDITOR_ONLY_TAGS.includes(t))
|
||||
})
|
||||
|
||||
const oilNames = computed(() =>
|
||||
props.recipe.ingredients.map(i => i.oil).join('、')
|
||||
@@ -79,6 +89,11 @@ const isFav = computed(() => recipesStore.isFavorite(props.recipe))
|
||||
color: #5a7d5e;
|
||||
}
|
||||
|
||||
.tag-reviewed {
|
||||
background: #e3f2fd;
|
||||
color: #1565c0;
|
||||
}
|
||||
|
||||
.recipe-card-oils {
|
||||
font-size: 12px;
|
||||
color: #9a8570;
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
<!-- Action Type Filters -->
|
||||
<div class="filter-row">
|
||||
<span class="filter-label">操作类型:</span>
|
||||
<span class="filter-label">操作:</span>
|
||||
<button
|
||||
v-for="action in actionTypes"
|
||||
:key="action.value"
|
||||
@@ -15,7 +15,7 @@
|
||||
</div>
|
||||
|
||||
<!-- User Filters -->
|
||||
<div class="filter-row" v-if="uniqueUsers.length > 0">
|
||||
<div class="filter-row" v-if="uniqueUsers.length > 1">
|
||||
<span class="filter-label">用户:</span>
|
||||
<button
|
||||
v-for="u in uniqueUsers"
|
||||
@@ -26,26 +26,30 @@
|
||||
>{{ u }}</button>
|
||||
</div>
|
||||
|
||||
<!-- Target Type Filters -->
|
||||
<div class="filter-row">
|
||||
<span class="filter-label">对象:</span>
|
||||
<button
|
||||
v-for="t in targetTypes"
|
||||
:key="t.value"
|
||||
class="filter-btn"
|
||||
:class="{ active: selectedTarget === t.value }"
|
||||
@click="selectedTarget = selectedTarget === t.value ? '' : t.value"
|
||||
>{{ t.label }}</button>
|
||||
</div>
|
||||
|
||||
<!-- Log List -->
|
||||
<div class="log-list">
|
||||
<div v-for="log in filteredLogs" :key="log._id || log.id" class="log-item">
|
||||
<div v-for="log in filteredLogs" :key="log.id" class="log-item">
|
||||
<div class="log-header">
|
||||
<span class="log-action" :class="actionClass(log.action)">{{ actionLabel(log.action) }}</span>
|
||||
<span class="log-action" :class="actionColorClass(log.action)">{{ actionLabel(log.action) }}</span>
|
||||
<span class="log-user">{{ log.user_name || log.username || '系统' }}</span>
|
||||
<span class="log-time">{{ formatTime(log.created_at) }}</span>
|
||||
</div>
|
||||
<div class="log-detail">
|
||||
<span v-if="log.target_type" class="log-target">{{ log.target_type }}: </span>
|
||||
<span class="log-desc">{{ log.description || log.detail || formatDetail(log) }}</span>
|
||||
<span v-if="log.target_name" class="log-target-name">{{ log.target_name }}</span>
|
||||
<span v-if="parsedDetail(log)" class="log-extra">{{ parsedDetail(log) }}</span>
|
||||
</div>
|
||||
<div v-if="log.changes" class="log-changes">
|
||||
<pre class="changes-pre">{{ typeof log.changes === 'string' ? log.changes : JSON.stringify(log.changes, null, 2) }}</pre>
|
||||
</div>
|
||||
<button
|
||||
v-if="log.undoable"
|
||||
class="btn-undo"
|
||||
@click="undoLog(log)"
|
||||
>↩ 撤销</button>
|
||||
</div>
|
||||
<div v-if="filteredLogs.length === 0" class="empty-hint">暂无日志记录</div>
|
||||
</div>
|
||||
@@ -61,29 +65,46 @@
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { useAuthStore } from '../stores/auth'
|
||||
import { useUiStore } from '../stores/ui'
|
||||
import { api } from '../composables/useApi'
|
||||
import { showConfirm } from '../composables/useDialog'
|
||||
|
||||
const auth = useAuthStore()
|
||||
const ui = useUiStore()
|
||||
|
||||
const logs = ref([])
|
||||
const loading = ref(false)
|
||||
const hasMore = ref(true)
|
||||
const page = ref(0)
|
||||
const pageSize = 50
|
||||
const pageSize = 100
|
||||
const selectedAction = ref('')
|
||||
const selectedUser = ref('')
|
||||
const selectedTarget = ref('')
|
||||
|
||||
const ACTION_MAP = {
|
||||
create_recipe: '新增配方',
|
||||
update_recipe: '编辑配方',
|
||||
delete_recipe: '删除配方',
|
||||
adopt_recipe: '采纳配方',
|
||||
reject_recipe: '拒绝配方',
|
||||
undo_delete_recipe: '恢复配方',
|
||||
upsert_oil: '编辑精油',
|
||||
delete_oil: '删除精油',
|
||||
create_tag: '新增标签',
|
||||
delete_tag: '删除标签',
|
||||
create_user: '创建用户',
|
||||
update_user: '修改用户',
|
||||
delete_user: '删除用户',
|
||||
undo_delete_user: '恢复用户',
|
||||
}
|
||||
|
||||
const actionTypes = [
|
||||
{ value: 'create', label: '创建' },
|
||||
{ value: 'update', label: '更新' },
|
||||
{ value: 'delete', label: '删除' },
|
||||
{ value: 'login', label: '登录' },
|
||||
{ value: 'approve', label: '审核' },
|
||||
{ value: 'export', label: '导出' },
|
||||
{ value: 'recipe', label: '配方' },
|
||||
{ value: 'oil', label: '精油' },
|
||||
{ value: 'user', label: '用户' },
|
||||
{ value: 'tag', label: '标签' },
|
||||
{ value: 'adopt', label: '审核' },
|
||||
]
|
||||
|
||||
const targetTypes = [
|
||||
{ value: 'recipe', label: '配方' },
|
||||
{ value: 'oil', label: '精油' },
|
||||
{ value: 'user', label: '用户' },
|
||||
]
|
||||
|
||||
const uniqueUsers = computed(() => {
|
||||
@@ -98,59 +119,55 @@ const uniqueUsers = computed(() => {
|
||||
const filteredLogs = computed(() => {
|
||||
let result = logs.value
|
||||
if (selectedAction.value) {
|
||||
result = result.filter(l => l.action === selectedAction.value)
|
||||
result = result.filter(l => l.action.includes(selectedAction.value))
|
||||
}
|
||||
if (selectedUser.value) {
|
||||
result = result.filter(l =>
|
||||
(l.user_name || l.username) === selectedUser.value
|
||||
)
|
||||
result = result.filter(l => (l.user_name || l.username) === selectedUser.value)
|
||||
}
|
||||
if (selectedTarget.value) {
|
||||
result = result.filter(l => l.target_type === selectedTarget.value)
|
||||
}
|
||||
return result
|
||||
})
|
||||
|
||||
function actionLabel(action) {
|
||||
const map = {
|
||||
create: '创建',
|
||||
update: '更新',
|
||||
delete: '删除',
|
||||
login: '登录',
|
||||
approve: '审核',
|
||||
reject: '拒绝',
|
||||
export: '导出',
|
||||
undo: '撤销',
|
||||
}
|
||||
return map[action] || action
|
||||
return ACTION_MAP[action] || action
|
||||
}
|
||||
|
||||
function actionClass(action) {
|
||||
return {
|
||||
'action-create': action === 'create',
|
||||
'action-update': action === 'update',
|
||||
'action-delete': action === 'delete' || action === 'reject',
|
||||
'action-login': action === 'login',
|
||||
'action-approve': action === 'approve',
|
||||
function actionColorClass(action) {
|
||||
if (action.includes('create') || action.includes('upsert')) return 'color-create'
|
||||
if (action.includes('update')) return 'color-update'
|
||||
if (action.includes('delete') || action.includes('reject')) return 'color-delete'
|
||||
if (action.includes('adopt') || action.includes('undo')) return 'color-approve'
|
||||
return ''
|
||||
}
|
||||
|
||||
function parsedDetail(log) {
|
||||
if (!log.detail) return ''
|
||||
try {
|
||||
const d = JSON.parse(log.detail)
|
||||
const parts = []
|
||||
if (d.from_user) parts.push(`来自: ${d.from_user}`)
|
||||
if (d.reason) parts.push(`原因: ${d.reason}`)
|
||||
if (d.role) parts.push(`角色: ${d.role}`)
|
||||
if (d.display_name) parts.push(`显示名: ${d.display_name}`)
|
||||
if (d.original_log_id) parts.push(`恢复自 #${d.original_log_id}`)
|
||||
if (parts.length) return parts.join(' · ')
|
||||
// For deleted users, show username
|
||||
if (d.username) return `用户名: ${d.username}`
|
||||
return ''
|
||||
} catch {
|
||||
return log.detail.length > 100 ? log.detail.substring(0, 100) + '...' : log.detail
|
||||
}
|
||||
}
|
||||
|
||||
function formatTime(t) {
|
||||
if (!t) return ''
|
||||
const d = new Date(t)
|
||||
return d.toLocaleString('zh-CN', {
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit',
|
||||
return new Date(t + 'Z').toLocaleString('zh-CN', {
|
||||
month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit',
|
||||
})
|
||||
}
|
||||
|
||||
function formatDetail(log) {
|
||||
if (log.target_name) return log.target_name
|
||||
if (log.recipe_name) return log.recipe_name
|
||||
if (log.oil_name) return log.oil_name
|
||||
return ''
|
||||
}
|
||||
|
||||
async function fetchLogs() {
|
||||
loading.value = true
|
||||
try {
|
||||
@@ -158,9 +175,7 @@ async function fetchLogs() {
|
||||
if (res.ok) {
|
||||
const data = await res.json()
|
||||
const items = Array.isArray(data) ? data : data.logs || data.items || []
|
||||
if (items.length < pageSize) {
|
||||
hasMore.value = false
|
||||
}
|
||||
if (items.length < pageSize) hasMore.value = false
|
||||
logs.value.push(...items)
|
||||
}
|
||||
} catch {
|
||||
@@ -174,207 +189,52 @@ function loadMore() {
|
||||
fetchLogs()
|
||||
}
|
||||
|
||||
async function undoLog(log) {
|
||||
const ok = await showConfirm('确定撤销此操作?')
|
||||
if (!ok) return
|
||||
try {
|
||||
const id = log._id || log.id
|
||||
const res = await api(`/api/audit-log/${id}/undo`, { method: 'POST' })
|
||||
if (res.ok) {
|
||||
ui.showToast('已撤销')
|
||||
// Refresh
|
||||
logs.value = []
|
||||
page.value = 0
|
||||
hasMore.value = true
|
||||
await fetchLogs()
|
||||
} else {
|
||||
ui.showToast('撤销失败')
|
||||
}
|
||||
} catch {
|
||||
ui.showToast('撤销失败')
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchLogs()
|
||||
})
|
||||
onMounted(() => fetchLogs())
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.audit-log {
|
||||
padding: 0 12px 24px;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
margin: 0 0 16px;
|
||||
font-size: 16px;
|
||||
color: #3e3a44;
|
||||
}
|
||||
.audit-log { padding: 0 12px 24px; }
|
||||
.page-title { margin: 0 0 16px; font-size: 16px; color: #3e3a44; }
|
||||
|
||||
.filter-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
margin-bottom: 10px;
|
||||
flex-wrap: wrap;
|
||||
display: flex; align-items: center; gap: 6px; margin-bottom: 8px; flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.filter-label {
|
||||
font-size: 13px;
|
||||
color: #6b6375;
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.filter-label { font-size: 13px; color: #6b6375; font-weight: 500; white-space: nowrap; }
|
||||
.filter-btn {
|
||||
padding: 5px 14px;
|
||||
border-radius: 16px;
|
||||
border: 1.5px solid #e5e4e7;
|
||||
background: #fff;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
font-family: inherit;
|
||||
color: #6b6375;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
|
||||
.filter-btn.active {
|
||||
background: #e8f5e9;
|
||||
border-color: #7ec6a4;
|
||||
color: #2e7d5a;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.filter-btn:hover {
|
||||
border-color: #d4cfc7;
|
||||
}
|
||||
|
||||
.log-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
padding: 4px 12px; border-radius: 16px; border: 1.5px solid #e5e4e7;
|
||||
background: #fff; font-size: 12px; cursor: pointer; font-family: inherit; color: #6b6375;
|
||||
}
|
||||
.filter-btn.active { background: #e8f5e9; border-color: #7ec6a4; color: #2e7d5a; font-weight: 600; }
|
||||
.filter-btn:hover { border-color: #d4cfc7; }
|
||||
|
||||
.log-list { display: flex; flex-direction: column; gap: 4px; }
|
||||
.log-item {
|
||||
padding: 12px 14px;
|
||||
background: #fff;
|
||||
border: 1.5px solid #e5e4e7;
|
||||
border-radius: 10px;
|
||||
transition: border-color 0.15s;
|
||||
}
|
||||
|
||||
.log-item:hover {
|
||||
border-color: #d4cfc7;
|
||||
}
|
||||
|
||||
.log-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 4px;
|
||||
padding: 10px 14px; background: #fff; border: 1.5px solid #e5e4e7; border-radius: 10px;
|
||||
}
|
||||
.log-item:hover { border-color: #d4cfc7; }
|
||||
|
||||
.log-header { display: flex; align-items: center; gap: 8px; }
|
||||
.log-action {
|
||||
padding: 2px 10px;
|
||||
border-radius: 10px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
background: #f0eeeb;
|
||||
color: #6b6375;
|
||||
padding: 2px 10px; border-radius: 10px; font-size: 11px; font-weight: 600;
|
||||
background: #f0eeeb; color: #6b6375; white-space: nowrap;
|
||||
}
|
||||
.color-create { background: #e8f5e9; color: #2e7d5a; }
|
||||
.color-update { background: #e3f2fd; color: #1565c0; }
|
||||
.color-delete { background: #ffebee; color: #c62828; }
|
||||
.color-approve { background: #f3e5f5; color: #7b1fa2; }
|
||||
|
||||
.action-create { background: #e8f5e9; color: #2e7d5a; }
|
||||
.action-update { background: #e3f2fd; color: #1565c0; }
|
||||
.action-delete { background: #ffebee; color: #c62828; }
|
||||
.action-login { background: #fff3e0; color: #e65100; }
|
||||
.action-approve { background: #f3e5f5; color: #7b1fa2; }
|
||||
|
||||
.log-user {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: #3e3a44;
|
||||
}
|
||||
|
||||
.log-time {
|
||||
font-size: 11px;
|
||||
color: #b0aab5;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.log-detail {
|
||||
font-size: 13px;
|
||||
color: #6b6375;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.log-target {
|
||||
font-weight: 500;
|
||||
color: #3e3a44;
|
||||
}
|
||||
|
||||
.log-changes {
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
.changes-pre {
|
||||
font-size: 11px;
|
||||
background: #f8f7f5;
|
||||
padding: 8px 10px;
|
||||
border-radius: 6px;
|
||||
overflow-x: auto;
|
||||
margin: 0;
|
||||
color: #6b6375;
|
||||
font-family: ui-monospace, Consolas, monospace;
|
||||
line-height: 1.5;
|
||||
max-height: 120px;
|
||||
}
|
||||
|
||||
.btn-undo {
|
||||
margin-top: 8px;
|
||||
padding: 4px 12px;
|
||||
border: 1.5px solid #e5e4e7;
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
font-family: inherit;
|
||||
color: #6b6375;
|
||||
}
|
||||
|
||||
.btn-undo:hover {
|
||||
border-color: #7ec6a4;
|
||||
color: #4a9d7e;
|
||||
}
|
||||
|
||||
.load-more {
|
||||
text-align: center;
|
||||
margin-top: 16px;
|
||||
}
|
||||
.log-user { font-size: 13px; font-weight: 500; color: #3e3a44; }
|
||||
.log-time { font-size: 11px; color: #b0aab5; margin-left: auto; white-space: nowrap; }
|
||||
.log-detail { font-size: 13px; color: #6b6375; margin-top: 2px; }
|
||||
.log-target-name { font-weight: 500; color: #3e3a44; margin-right: 8px; }
|
||||
.log-extra { color: #999; font-size: 12px; }
|
||||
|
||||
.load-more { text-align: center; margin-top: 16px; }
|
||||
.btn-outline {
|
||||
background: #fff;
|
||||
color: #6b6375;
|
||||
border: 1.5px solid #d4cfc7;
|
||||
border-radius: 10px;
|
||||
padding: 9px 28px;
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.btn-outline:hover {
|
||||
background: #f8f7f5;
|
||||
}
|
||||
|
||||
.btn-outline:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.empty-hint {
|
||||
text-align: center;
|
||||
color: #b0aab5;
|
||||
font-size: 13px;
|
||||
padding: 32px 0;
|
||||
background: #fff; color: #6b6375; border: 1.5px solid #d4cfc7; border-radius: 10px;
|
||||
padding: 9px 28px; font-size: 13px; cursor: pointer; font-family: inherit;
|
||||
}
|
||||
.btn-outline:hover { background: #f8f7f5; }
|
||||
.btn-outline:disabled { opacity: 0.5; cursor: default; }
|
||||
.empty-hint { text-align: center; color: #b0aab5; font-size: 13px; padding: 32px 0; }
|
||||
</style>
|
||||
|
||||
@@ -293,6 +293,10 @@
|
||||
<div class="candidate-tags" v-if="formCandidateTags.length">
|
||||
<span v-for="tag in formCandidateTags" :key="tag" class="candidate-tag" @click="toggleFormTag(tag)">+ {{ tag }}</span>
|
||||
</div>
|
||||
<div class="tag-input-row">
|
||||
<input v-model="newTagInput" type="text" class="editor-input" placeholder="添加新标签..." @keydown.enter="addNewFormTag" style="flex:1" />
|
||||
<button class="action-btn action-btn-sm" @click="addNewFormTag" :disabled="!newTagInput.trim()">+</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Total cost -->
|
||||
@@ -597,10 +601,20 @@ function onOilBlur(ing) {
|
||||
}, 150)
|
||||
}
|
||||
|
||||
const newTagInput = ref('')
|
||||
|
||||
const formCandidateTags = computed(() =>
|
||||
recipeStore.allTags.filter(t => !formTags.value.includes(t))
|
||||
)
|
||||
|
||||
function addNewFormTag() {
|
||||
const tag = newTagInput.value.trim()
|
||||
if (tag && !formTags.value.includes(tag)) {
|
||||
formTags.value.push(tag)
|
||||
}
|
||||
newTagInput.value = ''
|
||||
}
|
||||
|
||||
const DROPS_PER_ML = 18.6
|
||||
|
||||
const formDilutionHint = computed(() => {
|
||||
@@ -1367,6 +1381,7 @@ watch(() => recipeStore.recipes, () => {
|
||||
.candidate-tags { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 8px; }
|
||||
.candidate-tag { background: #f0eeeb; color: #6b6375; padding: 4px 10px; border-radius: 12px; font-size: 12px; cursor: pointer; }
|
||||
.candidate-tag:hover { background: #e8f5e9; color: #2e7d5a; }
|
||||
.tag-input-row { display: flex; gap: 6px; align-items: center; margin-top: 6px; }
|
||||
.editor-total { text-align: right; font-size: 15px; font-weight: 600; color: #4a9d7e; padding: 10px 0; border-top: 1px solid #eee; }
|
||||
.action-btn { border: 1.5px solid #d4cfc7; background: #fff; color: #6b6375; border-radius: 8px; padding: 6px 14px; font-size: 13px; cursor: pointer; font-family: inherit; white-space: nowrap; }
|
||||
.action-btn:hover { background: #f8f7f5; }
|
||||
|
||||
Reference in New Issue
Block a user