初始化2
This commit is contained in:
622
app/pages/console/app-review.vue
Normal file
622
app/pages/console/app-review.vue
Normal file
@@ -0,0 +1,622 @@
|
||||
<template>
|
||||
<div class="review-page">
|
||||
<!-- 页面头部 -->
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h2 class="page-title">🔍 应用审核管理</h2>
|
||||
<p class="page-desc">审核开发者提交的应用上架申请</p>
|
||||
</div>
|
||||
<a-space>
|
||||
<a-button @click="loadApps" :loading="loading">
|
||||
<template #icon><ReloadOutlined /></template>
|
||||
刷新
|
||||
</a-button>
|
||||
</a-space>
|
||||
</div>
|
||||
|
||||
<!-- 状态统计 -->
|
||||
<a-row :gutter="[16, 16]" class="mb-6">
|
||||
<a-col :xs="12" :md="6" v-for="stat in reviewStats" :key="stat.key">
|
||||
<div
|
||||
class="stat-card"
|
||||
:class="[stat.color, { active: filterStatus === stat.key }]"
|
||||
@click="handleStatFilter(stat.key)"
|
||||
>
|
||||
<div class="stat-icon">{{ stat.icon }}</div>
|
||||
<div class="stat-info">
|
||||
<div class="stat-value">{{ stat.value }}</div>
|
||||
<div class="stat-label">{{ stat.label }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<!-- 筛选栏 -->
|
||||
<div class="panel">
|
||||
<div class="panel-header">
|
||||
<span class="panel-title">📋 审核列表</span>
|
||||
<a-space>
|
||||
<a-select
|
||||
v-model:value="filterStatus"
|
||||
style="width: 140px"
|
||||
@change="loadApps"
|
||||
>
|
||||
<a-select-option value="">全部状态</a-select-option>
|
||||
<a-select-option value="pending_review">待审核</a-select-option>
|
||||
<a-select-option value="published">已上架</a-select-option>
|
||||
<a-select-option value="rejected">已拒绝</a-select-option>
|
||||
<a-select-option value="deprecated">已下架</a-select-option>
|
||||
</a-select>
|
||||
<a-input-search
|
||||
v-model:value="searchKeyword"
|
||||
placeholder="搜索应用名称"
|
||||
style="width: 200px"
|
||||
@search="loadApps"
|
||||
/>
|
||||
</a-space>
|
||||
</div>
|
||||
|
||||
<a-table
|
||||
:columns="columns"
|
||||
:data-source="apps"
|
||||
:loading="loading"
|
||||
:pagination="pagination"
|
||||
row-key="productId"
|
||||
@change="handleTableChange"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<!-- 应用信息 -->
|
||||
<template v-if="column.key === 'appInfo'">
|
||||
<div class="app-info-cell">
|
||||
<img v-if="record.icon" :src="record.icon" class="app-icon" />
|
||||
<div v-else class="app-icon-placeholder" :style="{ background: iconBgColor(record.productName) }">
|
||||
{{ (record.productName || 'A').charAt(0).toUpperCase() }}
|
||||
</div>
|
||||
<div class="app-info-text">
|
||||
<div class="app-name">{{ record.productName }}</div>
|
||||
<div class="app-code">{{ record.productCode }}</div>
|
||||
<div class="app-developer" v-if="record.developer">
|
||||
<UserOutlined style="font-size:11px;margin-right:3px" />{{ record.developer }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 发布状态 -->
|
||||
<template v-if="column.key === 'publishStatus'">
|
||||
<a-tag :color="statusColor(record.publishStatus)">
|
||||
{{ statusText(record.publishStatus) }}
|
||||
</a-tag>
|
||||
</template>
|
||||
|
||||
<!-- 定价 -->
|
||||
<template v-if="column.key === 'price'">
|
||||
<span v-if="record.priceType === 'free' || !record.priceType" class="price-free">免费</span>
|
||||
<span v-else class="price-paid">¥{{ ((record.price || 0) / 100).toFixed(2) }}
|
||||
<span class="price-period">{{ subscriptionText(record.subscriptionPeriod) }}</span>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<!-- 申请时间 -->
|
||||
<template v-if="column.key === 'applyTime'">
|
||||
<span class="text-sm text-gray-500">{{ record.publishTime || record.updateTime || '-' }}</span>
|
||||
</template>
|
||||
|
||||
<!-- 操作 -->
|
||||
<template v-if="column.key === 'action'">
|
||||
<a-space>
|
||||
<a-button type="link" size="small" @click="handleViewDetail(record)">详情</a-button>
|
||||
<!-- 待审核:通过/拒绝 -->
|
||||
<template v-if="record.publishStatus === 'pending_review'">
|
||||
<a-popconfirm title="确认通过此应用上架申请?" @confirm="handleApprove(record)">
|
||||
<a-button type="primary" size="small">通过</a-button>
|
||||
</a-popconfirm>
|
||||
<a-button danger size="small" @click="handleReject(record)">拒绝</a-button>
|
||||
</template>
|
||||
<!-- 已上架:下架 -->
|
||||
<a-popconfirm
|
||||
v-if="record.publishStatus === 'published'"
|
||||
title="确认下架此应用?"
|
||||
@confirm="handleAdminUnpublish(record)"
|
||||
>
|
||||
<a-button danger size="small">下架</a-button>
|
||||
</a-popconfirm>
|
||||
</a-space>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</div>
|
||||
|
||||
<!-- 审核详情弹窗 -->
|
||||
<a-modal
|
||||
v-model:open="showDetailModal"
|
||||
:title="`应用详情:${currentApp?.productName || ''}`"
|
||||
width="700px"
|
||||
:footer="null"
|
||||
>
|
||||
<template v-if="currentApp">
|
||||
<a-descriptions :column="2" bordered size="small">
|
||||
<a-descriptions-item label="应用名称">{{ currentApp.productName }}</a-descriptions-item>
|
||||
<a-descriptions-item label="应用标识">{{ currentApp.productCode }}</a-descriptions-item>
|
||||
<a-descriptions-item label="开发者">{{ currentApp.developer || '-' }}</a-descriptions-item>
|
||||
<a-descriptions-item label="发布状态">
|
||||
<a-tag :color="statusColor(currentApp.publishStatus)">{{ statusText(currentApp.publishStatus) }}</a-tag>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="定价模式">{{ priceTypeText(currentApp.priceType) }}</a-descriptions-item>
|
||||
<a-descriptions-item label="价格">
|
||||
<span v-if="currentApp.priceType === 'free' || !currentApp.priceType">免费</span>
|
||||
<span v-else>¥{{ ((currentApp.price || 0) / 100).toFixed(2) }}</span>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="提交时间" :span="2">{{ currentApp.publishTime || '-' }}</a-descriptions-item>
|
||||
<a-descriptions-item label="应用简介" :span="2">
|
||||
{{ currentApp.description || '-' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="详细说明" :span="2">
|
||||
<div class="detail-desc">{{ currentApp.content || '-' }}</div>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item v-if="currentApp.rejectReason" label="拒绝原因" :span="2">
|
||||
<a-alert type="error" :message="currentApp.rejectReason" show-icon />
|
||||
</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
|
||||
<!-- 待审核时的操作区 -->
|
||||
<div v-if="currentApp.publishStatus === 'pending_review'" class="detail-actions">
|
||||
<a-popconfirm title="确认通过此应用上架申请?" @confirm="handleApprove(currentApp)">
|
||||
<a-button type="primary" :loading="approving">✅ 审核通过</a-button>
|
||||
</a-popconfirm>
|
||||
<a-button danger @click="handleReject(currentApp)">❌ 拒绝上架</a-button>
|
||||
</div>
|
||||
</template>
|
||||
</a-modal>
|
||||
|
||||
<!-- 拒绝原因弹窗 -->
|
||||
<a-modal
|
||||
v-model:open="showRejectModal"
|
||||
title="填写拒绝原因"
|
||||
:confirm-loading="rejecting"
|
||||
@ok="confirmReject"
|
||||
@cancel="showRejectModal = false"
|
||||
>
|
||||
<a-form layout="vertical">
|
||||
<a-form-item label="拒绝原因" required>
|
||||
<a-textarea
|
||||
v-model:value="rejectReasonInput"
|
||||
:rows="4"
|
||||
placeholder="请填写具体的拒绝原因,以便开发者修改后重新提交"
|
||||
:maxlength="500"
|
||||
show-count
|
||||
/>
|
||||
</a-form-item>
|
||||
<div class="reject-tips">
|
||||
<p>💡 常见拒绝原因:</p>
|
||||
<a-space wrap>
|
||||
<a-tag
|
||||
v-for="tip in rejectTips"
|
||||
:key="tip"
|
||||
class="reject-tip-tag"
|
||||
@click="rejectReasonInput = tip"
|
||||
>{{ tip }}</a-tag>
|
||||
</a-space>
|
||||
</div>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ReloadOutlined, UserOutlined } from '@ant-design/icons-vue'
|
||||
import { message } from 'ant-design-vue'
|
||||
import {
|
||||
pagePublishReviews,
|
||||
approvePublishReview,
|
||||
rejectPublishReview,
|
||||
unpublishAppProduct,
|
||||
} from '@/api/app/appProduct'
|
||||
import type { AppProduct } from '@/api/app/appProduct/model'
|
||||
|
||||
definePageMeta({ layout: 'console' })
|
||||
useHead({ title: '应用审核管理 - 控制台' })
|
||||
|
||||
// 加载状态
|
||||
const loading = ref(false)
|
||||
const apps = ref<AppProduct[]>([])
|
||||
|
||||
// 筛选
|
||||
const filterStatus = ref('pending_review')
|
||||
const searchKeyword = ref('')
|
||||
|
||||
// 分页
|
||||
const pagination = reactive({
|
||||
current: 1,
|
||||
pageSize: 20,
|
||||
total: 0,
|
||||
showSizeChanger: true,
|
||||
showQuickJumper: true,
|
||||
})
|
||||
|
||||
// 统计
|
||||
const reviewStats = reactive([
|
||||
{ key: 'pending_review', icon: '⏳', label: '待审核', value: 0, color: 'orange' },
|
||||
{ key: 'published', icon: '✅', label: '已上架', value: 0, color: 'green' },
|
||||
{ key: 'rejected', icon: '❌', label: '已拒绝', value: 0, color: 'red' },
|
||||
{ key: '', icon: '📦', label: '全部应用', value: 0, color: 'blue' },
|
||||
])
|
||||
|
||||
// 表格列
|
||||
const columns = [
|
||||
{ title: '应用信息', key: 'appInfo', width: 260 },
|
||||
{ title: '审核状态', key: 'publishStatus', width: 110 },
|
||||
{ title: '定价', key: 'price', width: 130 },
|
||||
{ title: '提交时间', key: 'applyTime', width: 160 },
|
||||
{ title: '操作', key: 'action', width: 200 },
|
||||
]
|
||||
|
||||
// 详情弹窗
|
||||
const showDetailModal = ref(false)
|
||||
const currentApp = ref<AppProduct | null>(null)
|
||||
const approving = ref(false)
|
||||
|
||||
// 拒绝弹窗
|
||||
const showRejectModal = ref(false)
|
||||
const rejectReasonInput = ref('')
|
||||
const rejecting = ref(false)
|
||||
const rejectTargetApp = ref<AppProduct | null>(null)
|
||||
|
||||
const rejectTips = [
|
||||
'功能描述不完整,缺少使用说明文档',
|
||||
'应用简介过于简单,请补充详细功能介绍',
|
||||
'应用名称与实际功能不符',
|
||||
'价格设置不合理,请重新评估',
|
||||
'存在违规内容,请修改后重新提交',
|
||||
'截图不清晰或与功能描述不符',
|
||||
]
|
||||
|
||||
// 加载审核列表
|
||||
async function loadApps() {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await pagePublishReviews({
|
||||
page: pagination.current,
|
||||
limit: pagination.pageSize,
|
||||
publishStatus: filterStatus.value || undefined,
|
||||
keywords: searchKeyword.value || undefined,
|
||||
})
|
||||
apps.value = res?.list || []
|
||||
pagination.total = res?.count || 0
|
||||
updateStats()
|
||||
} catch {
|
||||
message.error('加载审核列表失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 更新统计(单独加载全量统计)
|
||||
async function updateStats() {
|
||||
try {
|
||||
const [pendingRes, publishedRes, rejectedRes, allRes] = await Promise.allSettled([
|
||||
pagePublishReviews({ page: 1, limit: 1, publishStatus: 'pending_review' }),
|
||||
pagePublishReviews({ page: 1, limit: 1, publishStatus: 'published' }),
|
||||
pagePublishReviews({ page: 1, limit: 1, publishStatus: 'rejected' }),
|
||||
pagePublishReviews({ page: 1, limit: 1 }),
|
||||
])
|
||||
if (pendingRes.status === 'fulfilled') reviewStats[0].value = pendingRes.value?.count || 0
|
||||
if (publishedRes.status === 'fulfilled') reviewStats[1].value = publishedRes.value?.count || 0
|
||||
if (rejectedRes.status === 'fulfilled') reviewStats[2].value = rejectedRes.value?.count || 0
|
||||
if (allRes.status === 'fulfilled') reviewStats[3].value = allRes.value?.count || 0
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
|
||||
// 统计卡片点击筛选
|
||||
function handleStatFilter(key: string) {
|
||||
filterStatus.value = key
|
||||
pagination.current = 1
|
||||
loadApps()
|
||||
}
|
||||
|
||||
// 分页变化
|
||||
function handleTableChange(pag: any) {
|
||||
pagination.current = pag.current
|
||||
pagination.pageSize = pag.pageSize
|
||||
loadApps()
|
||||
}
|
||||
|
||||
// 查看详情
|
||||
function handleViewDetail(record: AppProduct) {
|
||||
currentApp.value = record
|
||||
showDetailModal.value = true
|
||||
}
|
||||
|
||||
// 审核通过
|
||||
async function handleApprove(record: AppProduct) {
|
||||
approving.value = true
|
||||
try {
|
||||
await approvePublishReview(record.productId!)
|
||||
message.success(`「${record.productName}」已通过审核并上架`)
|
||||
showDetailModal.value = false
|
||||
loadApps()
|
||||
} catch (e: any) {
|
||||
message.error(e?.message || '操作失败')
|
||||
} finally {
|
||||
approving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 打开拒绝弹窗
|
||||
function handleReject(record: AppProduct) {
|
||||
rejectTargetApp.value = record
|
||||
rejectReasonInput.value = ''
|
||||
showRejectModal.value = true
|
||||
}
|
||||
|
||||
// 确认拒绝
|
||||
async function confirmReject() {
|
||||
if (!rejectReasonInput.value.trim()) {
|
||||
message.warning('请填写拒绝原因')
|
||||
return
|
||||
}
|
||||
if (!rejectTargetApp.value) return
|
||||
rejecting.value = true
|
||||
try {
|
||||
await rejectPublishReview({
|
||||
productId: rejectTargetApp.value.productId!,
|
||||
rejectReason: rejectReasonInput.value,
|
||||
})
|
||||
message.success('已拒绝并通知开发者')
|
||||
showRejectModal.value = false
|
||||
showDetailModal.value = false
|
||||
loadApps()
|
||||
} catch (e: any) {
|
||||
message.error(e?.message || '操作失败')
|
||||
} finally {
|
||||
rejecting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 管理员下架
|
||||
async function handleAdminUnpublish(record: AppProduct) {
|
||||
try {
|
||||
await unpublishAppProduct(record.productId!)
|
||||
message.success(`「${record.productName}」已下架`)
|
||||
loadApps()
|
||||
} catch (e: any) {
|
||||
message.error(e?.message || '操作失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 状态相关
|
||||
function statusText(status?: string) {
|
||||
const map: Record<string, string> = {
|
||||
developing: '开发中',
|
||||
pending_review: '待审核',
|
||||
published: '已上架',
|
||||
rejected: '已拒绝',
|
||||
deprecated: '已下架',
|
||||
}
|
||||
return map[status || ''] || '开发中'
|
||||
}
|
||||
|
||||
function statusColor(status?: string) {
|
||||
const map: Record<string, string> = {
|
||||
developing: 'default',
|
||||
pending_review: 'orange',
|
||||
published: 'success',
|
||||
rejected: 'error',
|
||||
deprecated: 'default',
|
||||
}
|
||||
return map[status || ''] || 'default'
|
||||
}
|
||||
|
||||
function priceTypeText(type?: string) {
|
||||
const map: Record<string, string> = {
|
||||
free: '免费',
|
||||
one_time: '一次性付费',
|
||||
subscription: '订阅制',
|
||||
}
|
||||
return map[type || ''] || '免费'
|
||||
}
|
||||
|
||||
function subscriptionText(period?: string) {
|
||||
if (period === 'month') return '/月'
|
||||
if (period === 'year') return '/年'
|
||||
return ''
|
||||
}
|
||||
|
||||
const PALETTE = ['#4e6ef2', '#f4a261', '#e76f51', '#2a9d8f', '#e9c46a', '#457b9d']
|
||||
function iconBgColor(name?: string) {
|
||||
if (!name) return PALETTE[0]
|
||||
let h = 0
|
||||
for (let i = 0; i < name.length; i++) h = (h * 31 + name.charCodeAt(i)) & 0xffffffff
|
||||
return PALETTE[Math.abs(h) % PALETTE.length]
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadApps()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.review-page {
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
color: #1f2937;
|
||||
margin: 0;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.page-desc {
|
||||
font-size: 13px;
|
||||
color: #9ca3af;
|
||||
margin: 2px 0 0;
|
||||
}
|
||||
|
||||
/* 统计卡片 */
|
||||
.stat-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 16px;
|
||||
border-radius: 12px;
|
||||
border: 2px solid transparent;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.stat-card:hover { transform: translateY(-2px); box-shadow: 0 4px 12px rgba(0,0,0,0.08); }
|
||||
.stat-card.active { border-color: currentColor; box-shadow: 0 4px 12px rgba(0,0,0,0.12); }
|
||||
|
||||
.stat-card.blue { background: #eff6ff; border-color: #dbeafe; }
|
||||
.stat-card.orange { background: #fff7ed; border-color: #fed7aa; }
|
||||
.stat-card.green { background: #f0fdf4; border-color: #bbf7d0; }
|
||||
.stat-card.red { background: #fff1f2; border-color: #fecdd3; }
|
||||
|
||||
.stat-card.active.blue { border-color: #3b82f6; }
|
||||
.stat-card.active.orange { border-color: #f97316; }
|
||||
.stat-card.active.green { border-color: #22c55e; }
|
||||
.stat-card.active.red { border-color: #ef4444; }
|
||||
|
||||
.stat-icon { font-size: 28px; flex-shrink: 0; }
|
||||
|
||||
.stat-value {
|
||||
font-size: 22px;
|
||||
font-weight: 700;
|
||||
color: rgba(0, 0, 0, 0.85);
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 12px;
|
||||
color: rgba(0, 0, 0, 0.45);
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
/* 面板 */
|
||||
.panel {
|
||||
background: #fff;
|
||||
border: 1px solid #f0f0f0;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.panel-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 14px 18px;
|
||||
border-bottom: 1px solid #f5f5f5;
|
||||
}
|
||||
|
||||
.panel-title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: rgba(0, 0, 0, 0.85);
|
||||
}
|
||||
|
||||
/* 应用信息格 */
|
||||
.app-info-cell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.app-icon {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border-radius: 8px;
|
||||
object-fit: cover;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.app-icon-placeholder {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.app-info-text { flex: 1; min-width: 0; }
|
||||
|
||||
.app-name {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: rgba(0, 0, 0, 0.85);
|
||||
}
|
||||
|
||||
.app-code {
|
||||
font-size: 12px;
|
||||
color: rgba(0, 0, 0, 0.45);
|
||||
}
|
||||
|
||||
.app-developer {
|
||||
font-size: 12px;
|
||||
color: rgba(0, 0, 0, 0.45);
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
/* 价格 */
|
||||
.price-free { color: #22c55e; font-weight: 500; font-size: 13px; }
|
||||
.price-paid { color: #f59e0b; font-weight: 600; font-size: 14px; }
|
||||
.price-period { font-size: 11px; color: rgba(0,0,0,0.45); font-weight: 400; margin-left: 2px; }
|
||||
|
||||
/* 详情弹窗 */
|
||||
.detail-actions {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
justify-content: flex-end;
|
||||
margin-top: 20px;
|
||||
padding-top: 16px;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.detail-desc {
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
/* 拒绝原因提示 */
|
||||
.reject-tips {
|
||||
margin-top: 12px;
|
||||
padding: 12px;
|
||||
background: #fafafa;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.reject-tips p {
|
||||
font-size: 12px;
|
||||
color: rgba(0,0,0,0.45);
|
||||
margin: 0 0 8px;
|
||||
}
|
||||
|
||||
.reject-tip-tag {
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
|
||||
.reject-tip-tag:hover {
|
||||
color: #4f46e5;
|
||||
border-color: #4f46e5;
|
||||
}
|
||||
|
||||
.mb-6 { margin-bottom: 24px; }
|
||||
.text-sm { font-size: 12px; }
|
||||
.text-gray-500 { color: rgba(0,0,0,0.45); }
|
||||
</style>
|
||||
Reference in New Issue
Block a user